Seeeduino XIAO(ARM Cortex-M0+)でマルチタスク:FreeRTOSを使って動作確認/おさらい

Arduino/SAMDCortex-M0+,FreeRTOS,SAMD21,Seeeduino,XIAO,チャタリング,マルチタスク,割り込み

先日XIAOでハロワを動かしてみましたが、その際にvTaskDelayを書いたらエラーになってしまいまして

ESP32では何にも考えずに使えた訳ですが、、

delayを入れると全体が停止してしまいますので、マイコンにいくつかの機能を持たせる場合にはRTOSは必須と言ってもいいと思います。

てな訳で、Seeeduino XIAOでもRTOSを一通り使える様にしておこうと思います。

利用するのはオフィシャルSeeed-StudioのFreeRTOSライブラリ。

GitHub: Seeed-Studio / Seeed_Arduino_FreeRTOS

他にもRTOSはいくつかある様ですが、、

Arduinoな脳の感覚では敷居が高く、きっと躓くに違いない。

私の様なnewbieにはとりあえずFreeRTOSが良いと思います。

さてライブラリのページを見ていきなり使おうとしても意味判らないかもしれませんが、、、

ちゃんとチュートリアルが用意されておりました。

Arduino FreeRTOS Tutorial – How to use FreeRTOS to Multi-tasking in Arduino

というわけで、基本中の基本、Lチカで動かしてみて感じを掴もうと思います。

チュートリアルの方が判り易いかもしれませんが・・・

シングルタスクだと何が不便なのか、どう困るのか

さて、いきなりシングルタスクだのマルチタスクだのと言われてもピンと来ないかもしれませんので、何故わざわざFreeRTOSを使うのかという辺りを。

例えば1秒毎にLチカをするプログラムを書いたとします。

簡単に、LEDに繋いだGPIOをHIGHにして、1秒まってGPIOをLOWにする

こんな感じの動作をさせたとします。

void loop()
{
	digitalWrite(LED_PIN_A, HIGH);	// LED_A 点灯
	digitalWrite(LED_PIN_B, HIGH);	// LED_B 点灯

	delay(1000);	// 1秒待つ


	digitalWrite(LED_PIN_A, LOW); // LED_A 消灯
	digitalWrite(LED_PIN_B, LOW); // LED_B 消灯

	delay(1000);	// 1秒待つ
}

凄くシンプルですが、とりあえずこれで1秒毎に2つのLEDは点灯と消灯を繰り返すと思います。

しかしこの1秒(1000ms)待つディレイの間、マイコンは他の仕事を全くしません。

さてここで、LED_Aは1秒毎に、LED_Bは2秒毎に点灯させたい場合はどうしましょう。

1秒と2秒なら、シングルタスクでも出来ない事はありませんが、、、

int led_count = 0;

void loop()
{
	// led_countインクリメント
	led_count++;

	digitalWrite(LED_PIN_A, HIGH);	// LED_A 点灯
	if(led_count > 1) {
		digitalWrite(LED_PIN_B, HIGH);	// LED_B 点灯
	}

	delay(1000);	// 1秒待つ

	digitalWrite(LED_PIN_A, LOW); // LED_A 消灯
	if(led_count > 1) {
		digitalWrite(LED_PIN_B, LOW); // LED_B 消灯
		led_count = 0;
	}

	delay(1000);	// 1秒待つ
}

フラグでも持たせて、2回目の時にだけ実行、カウンタリセット なんて泥臭い事で動くには動きますけども、、

こんなのを積み重ね増やしていけば、正直ワケわからんくなります。

LED_Bは0秒~1秒の間のランダムで点灯する、みたいな事をしたい時、シングルタスクではちょっと直ぐにロジックが浮かんできません。

簡単に済ますには、各LEDが裏で個別に勝手に動いていて欲しいわけです。

FreeRTOSを使ってマルチタスクで実行する

さてマルチタスクと言っても処理を物凄く細切れにして交互に動かしているだけですので、厳密には同時実行では無いのですが、、

片方の処理の終了までもう片方が動けないという事態にはならず、あたかも同時に実行しているかの様に動いてくれます。

マルチタスクでプログラムを動かしたい時は、以下の様な感じで書いていくと良いと思います。

・マルチタスクで動かしたい分だけ、タスクハンドラを用意
・RTOSで実行する関数をFreeRTOS的に作成
・必要であれば割り込み処理等を利用し、FreeRTOSのタスクを操作させる関数を作成
・タスクを登録・実行

ではまずは一個目から

ライブラリの読み込みと、マルチタスクで動かしたい分だけタスクハンドラを用意

2個のLEDをマルチタスクで点灯させるので、2個のタスクハンドラを用意します。

// Seeed FreeRTOSライブラリ
#include <Seeed_Arduino_FreeRTOS.h>

TaskHandle_t pLed_A;	//	タスクハンドラ構造体のポインタLED_A
TaskHandle_t pLed_B;	//	ポインタLED_A

ハンドラをグローバルに用意しておく事で、この処理を中断させたり別の動作をさせたりし易くなります。

どこかの処理の中で作ってガベージされたりスコープ外で行方不明になったりするのも防げますので、放りっぱなしの単純なLチカであっても私は問答無用でグローバル。

このサイズのマイコンならカプセル化に拘らなくてもワケわからんくなったりしないでしょうし。

そもそも仕事じゃ無いですし(ぇ

RTOSで実行する関数をFreeRTOS的に作成

// 0.5秒毎に点灯を繰り返すLED_A
static void Task_Led_A(void *pvParameters)
{
	while (1)
	{
		digitalWrite(LED_PIN_A, HIGH); // LED_A 点灯
		delay(500);				   // 0.5秒待つ

		digitalWrite(LED_PIN_A, LOW); // LED_A 消灯
		delay(500);				  // 0.5秒待つ
	}
	// ループを抜ける事は無いが、あった場合にはタスクを削除しておく
	vTaskDelete(NULL);
}


// n秒毎にランダムに点灯を繰り返すLED_B
static void Task_Led_B(void *pvParameters)
{
	while (1)
	{
		digitalWrite(LED_PIN_B, HIGH); // LED_B 点灯
		delay(random(1000));		   // 0~999ms のランダムミリ秒待つ

		digitalWrite(LED_PIN_B, LOW); // LED_B 消灯
		delay(random(1000));		  // 0~999ms のランダムミリ秒待つ
	}

	// ループを抜ける事は無いが、あった場合にはタスクを削除しておく
	vTaskDelete(NULL);
}

裏でずっと動かしておくタスクですので、無限ループで作成します。

一回きりのタスクや、ある条件を満たしたら終了させる場合には、タスクを終了させておくと良いです。

その場合には終了させるタイミングでvTaskDeleteを。

NULLを渡す事で自爆します。

ちなみに終了させないで放置すると何か動作がおかしくなる様で・・・

ESP32だと再起動を繰り返してたっけ・・・( ノД`)

また、前述のタスクハンドラがグローバルにあるので、外部からvTaskDeleteをかけられます。

void vTaskDelete(TaskHandle_t xTaskToDelete)
例:	vTaskDelete(pLed_B);

必要であれば割り込み処理等を利用し、FreeRTOSのタスクを操作させる関数を作成

少し調べたところ、ArduinoのattachInterruptがそのまま動作する様でしたので、簡単にボタンを押したらLEDの点滅を一時停止させる処理を書いてみます。

// チャタリング防止の為のフラグ
volatile bool untiChattering = false;

// ボタンが押された時の割り込み処理
void SuspendLed()
{

	// チャタリングフラグがfalseの時のみ実行
	if (!untiChattering)
	{
		// チャタリング防止の為、チャタリングフラグを上げる
		untiChattering = true;

		// LED_Aの点滅を一時停止/停止解除する
		// 実行状態を確認 返り値はenumの為そのまま比較
		if (eTaskGetState(pLed_A) == eSuspended)
		{
			// 停止中だったので再開する
			vTaskResume(pLed_A);
			digitalWrite(LED_SUSPEND, LOW); // LED_SUSPEND 消灯
		}
		else
		{
			// 停止中以外だったので停止する
			vTaskSuspend(pLed_A);
			digitalWrite(LED_SUSPEND, HIGH); // LED_SUSPEND 点灯
		}

		// 200msほどボタン動作無効化する
		//	ISRの中ではdelay vTaskDelay millis等が動作しない為、影響を与えず動作するdelayMicrosecondsを使う
		for (int i = 0; i < 200; i++)
		{
			// 10万us=100msなんですが、、桁が多過ぎて目盛りを喰いそうなのでループで減らす
			// delayMicroseconds(100000);
			delayMicroseconds(1000);
		}

		// チャタリングフラグを下げる
		untiChattering = false;
	}
}
// setupの中に記載
void setup() {
	:
	:

	// SW_PINピンのプルアップがLOWレベルに落とされた時に割り込みイベント発生、SuspendLedを呼び出す
	attachInterrupt(SW_PIN, SuspendLed, FALLING);

	:
	:
}

チャタリングによる超連打で割り込みが発生しない様に、untiChatteringフラグがfalseの時にしか動作しない様にし、中でもディレイをかけてからフラグを戻す様にしています。

このくらいやればチャタリングは殆ど出ないと思います。

尚、割り込みハンドラの中での処理では通常のディレイやタイマーの類が動作しませんので、FreeRTOSの外で動くdelayMicrosecondsをわざわざ使っています。

ISRの中で長時間停止すると何やら動作がおかしくなる様ですので、長時間停止しない様に設計して下さい。

ESP32だとwatchdogタイマーにやられます・・・・

タスクを登録・実行

setupの中でタスクを登録し、vTaskStartSchedulerで実行します。

void setup() {
	:
	:

	// LED_Aのタスクの登録
	xTaskCreate(
			Task_Led_A,		// タスクの関数名
			"Task LED_A"	// 名前 テキトーに
			256				// 割り当てるメモリサイズ bytes
			NULL			// タスクの関数に渡すパラメータへのポインタ 特にパラメータを受け取る処理は書いてないのでNULLで。
			tskIDLE_PRIORITY + 1	// プライオリティ 0-20 アイドリングよりも3ほど優先させる
			&pLed_A			// タスクハンドラを渡して関連付ける
		);

	// LED_Bのタスクの登録
	xTaskCreate(
			Task_Led_B,
			"Task LED_B",
			256,
			NULL,
			tskIDLE_PRIORITY + 1,
			&pLed_B
		);

	// タスク実行開始
	vTaskStartScheduler();

	:
	:
}

void loop() {
	// メインループは何もしない ずっと動いていて欲しい・・・
}

長いので畳んでおきますが、全部まとめるとこんな感じに。

#include <Arduino.h>
// Seeed FreeRTOSライブラリ
#include <Seeed_Arduino_FreeRTOS.h>

//====================================================================================

const int LED_SUSPEND = 1; // D1
const int LED_PIN_A = 2;   // D2
const int LED_PIN_B = 3;   // D3
const int SW_PIN = 10;	   // D10

TaskHandle_t pLed_A; //	タスクハンドラ構造体のポインタLED_A
TaskHandle_t pLed_B; //	ポインタLED_A

// チャタリング防止の為のフラグ
volatile bool untiChattering = false;

// 1秒毎に点灯を繰り返すLED_A
static void Task_Led_A(void *pvParameters)
{

	while (1)
	{
		digitalWrite(LED_PIN_A, HIGH); // LED_A 点灯
		delay(500);					   // 0.5秒待つ

		digitalWrite(LED_PIN_A, LOW); // LED_A 消灯
		delay(500);					  // 0.5秒待つ
	}

	// ループを抜ける事は無いが、あった場合にはタスクを削除しておく
	vTaskDelete(NULL);
}

// n秒毎にランダムに点灯を繰り返すLED_B
static void Task_Led_B(void *pvParameters)
{

	while (1)
	{
		digitalWrite(LED_PIN_B, HIGH); // LED_B 点灯
		delay(random(1000));		   // 0~999ms のランダムミリ秒待つ

		digitalWrite(LED_PIN_B, LOW); // LED_B 消灯
		delay(random(1000));		  // 0~999ms のランダムミリ秒待つ
	}

	// ループを抜ける事は無いが、あった場合にはタスクを削除しておく
	vTaskDelete(NULL);
}

// ボタンが押された時の割り込み処理
void SuspendLed()
{

	// チャタリングフラグがfalseの時のみ実行
	if (!untiChattering)
	{
		// チャタリング防止の為、チャタリングフラグを上げる
		untiChattering = true;

		// LED_Aの点滅を一時停止/停止解除する
		// 実行状態を確認 返り値はenumの為そのまま比較
		if (eTaskGetState(pLed_A) == eSuspended)
		{
			// 停止中だったので再開する
			vTaskResume(pLed_A);
			digitalWrite(LED_SUSPEND, LOW); // LED_SUSPEND 消灯
		}
		else
		{
			// 停止中以外だったので停止する
			vTaskSuspend(pLed_A);
			digitalWrite(LED_SUSPEND, HIGH); // LED_SUSPEND 点灯
		}

		// 200msほどボタン動作無効化する
		//	ISRの中ではdelay vTaskDelay millis等が動作しない為、影響を与えず動作するdelayMicrosecondsを使う
		for (int i = 0; i < 200; i++)
		{
			// 10万us=100msなんですが、、桁が多過ぎて目盛りを喰いそうなのでループで減らす
			// delayMicroseconds(100000);
			delayMicroseconds(1000);
		}

		// チャタリングフラグを下げる
		untiChattering = false;
	}
}

void setup()
{
	// シリアル
	Serial.begin(115200);
	while (!Serial)
		;

	pinMode(LED_SUSPEND, OUTPUT); //	D1
	pinMode(LED_PIN_A, OUTPUT);	  //	D2
	pinMode(LED_PIN_B, OUTPUT);	  //	D3
	// タクトスイッチ プルアップしておく
	pinMode(SW_PIN, INPUT_PULLUP); //	D10

	// 5番ピンのプルアップがLOWレベルに落とされた時に割り込みイベント発生、SuspendLedを呼び出す
	attachInterrupt(SW_PIN, SuspendLed, FALLING);

	// LED_Aのタスクの登録
	xTaskCreate(
		Task_Led_A,			  // タスクの関数名
		"Task LED_A",		  // 名前 テキトーに
		128,				  // 割り当てるメモリサイズ bytes
		NULL,				  // タスクの関数に渡すパラメータへのポインタ 特にパラメータを受け取る処理は書いてないのでNULLで。
		tskIDLE_PRIORITY + 1, // プライオリティ 0-20 アイドリングよりも1ほど優先させる
		&pLed_A				  // タスクハンドラを渡して関連付ける
	);

	// LED_Bのタスクの登録
	xTaskCreate(
		Task_Led_B,
		"Task LED_B",
		128,
		NULL,
		tskIDLE_PRIORITY + 1,
		&pLed_B
	);

	// タスク実行開始
	vTaskStartScheduler();
}

void loop()
{
	// メインループは何もしない ずっと動いていて欲しい・・・
}

ちょこっと処理を追加してますが、大した内容でもないので省きます

XIAOとの接続

回路図はこんな感じ。

XIAO_FreeRTOS02

図を出す必要も無い位ですが、、、

3本のLEDをテキトーに抵抗を挟んでXIAOに接続しています。

前述のコードに出て来ていない赤色LEDは、タスクの中断状態が判り易い様に中断させた時に光らせています。

XIAO_FreeRTOS01

相変わらずテキトーなブレッドボード具合。

ではちょっと動かしてみまうす

黄LEDは0.5秒毎に点灯を繰り返し、青LEDは勝手にランダム時間で点灯を繰り返しています。

割り込みボタンを押した時には黄LEDの点灯タスクが一時停止してポーズ状態になりますが、青LEDは全く気にせず勝手に点灯を繰り返しています。

黄LEDと青LEDが独立して勝手に動いてるっぽいのが判ると思います。

マルチタスク!

らしいですわ( ノД`)

まとめ

ひとまずはXIAOでも基本的なFreeRTOSの動作を確認できたと思います。

FreeRTOSを利用するにあたり、大体どのマイコンもデバイス固有の違いをラッパーで吸収している感じですので、ご利用の際にはデバイスの提供元がラッパーを用意していないかを確認して下さい。

特に窮屈な感じも無く、すんなり動作して良かったです。

とても小さいですがパワフルなマイコンですので、シングルタスクで使い潰すなんて勿体ないですしおすし

有効に遊ばせていただこうと思います。

そういえばESP32の時は、特にFreeRTOSのライブラリを明示的に呼ばすとも動作したんですが、、、

ESP32はコアも2コアですし、どうやら全部FreeRTOSで動いているみたいな感じの様で、メインループさえFreeRTOS管理下とかどこかで見た記憶が。

そんなわけで元からロードする様になっているらしいです。

おかげでwatchdogタイマー周りでトラブルもある様ですが、なんでも出来る系のマイコンですので些細な事ですね(?)

ひとまず今日はこの辺で。

次は、、えっと、LCDかな・・・