前回まではマルチタスクLチカするのに複数のタスクを作ってました。各タスク毎に遅延時間を指定するというシングルタスク同様の構造です。しかし定周期で何かを実行するというのはRTOS環境下ではアリガチな作業です。FreeRTOSはそのための「軽い」仕組みもしっかり実装されてます。今回はこのTimerタスクを使ってみます。
※「モダンOSのお砂場」投稿順Indexはこちら
定周期実行の4つの方法
一定の時間間隔で何か実行したいというときに選択できる方法はいつくかあります。
-
- ハードウエア・タイマで割り込みをかけ、割り込みハンドラで作業を起動
- シングルタスクのソフトウエアの中でソフトウエアループを作りその中で作業
- RTOSのタスクとして作業タスクを作り、RTOSの遅延関数で待つ
- RTOSのソフトウエア・タイマ機能に、定期呼び出しタスクとして登録しておく
1の方法は、ベアメタル環境でも可能な上に、ハードウエアタイマによるのでインターバルも正確です。ただし、「他の仕事」を押しのける割り込みを使うだけに、他に与える影響は無視できないです。また割り込みハンドラ中では制限事項がいろいろ出てくるのでなんでもできるというわけにも行きませぬ。
2の方法も、ベアメタル環境でも可能なもの。ソフトウエアの待ちループで定周期を作り出すもの、いわゆるフツーにArduino上でのLチカがこれに相当しますな。ソフトウエアループなので周期は正確とは言えないし、ループしている間のCPUはただクルクル回っているだけで何も他の仕事をできないですが、ともかくお手軽、実装簡単。その上割り込みハンドラのような制限はありませぬ。
3の方法は、RTOSありき。1個のタスクの中身としては2の方法と同様にも見えますが、待ちの部分で遅延関数を使うことでRTOS側に制御を返します。それどころかプリエンプティブなスケジューリングによっては強制的に制御が取り上げられてRTOSが他のタスクにCPUを割り当ててくれます。ただし、RTOS側にインターバル期間を知らせているわけではないので、負荷の状況次第ではインターバル期間がデコボコする可能性もあり。
※2023年8月23日追記:vTaskDelayUntil()関数またはxTaskDelayUntil()関数を使うことでTICKベースの時間間隔であればほぼほぼ正確な定周期起動を実現できるのではないかと思います。それについての記事はこちら。
そして今回試してみる方法が4です。RTOSがサポートしている「タイマ」に周期的に呼び出しをお願いするもの。タイマといってもソフトウエアタイマなので割り込みハンドラのような負荷も制限もなく、「気軽に」お願いできるもの。ただし、割り込みハンドラには負けます。
FreeRTOSの「ソフトウエア」タイマ
公式ドキュメントが以下に。
また、既に以下の過去回にて、ESP32マイコンを使ってFreeRTOSのタイマAPIの動作を確認しております。
モダンOSのお砂場(30) FreeRTOS、タイマAPI対vTaskDelay
今回は、ルネサスRA4M1搭載のArduino UNO R4での実行確認ということで。実際に使ってみないと分からんし。
さて、今回のインターバルタイマタスク生成はxTimerCreateというAPIを使い以下のようです。
Handle_LedTTask = xTimerCreate("GreenTimer", pdMS_TO_TICKS(GREEN_TIMER_MS), pdTRUE, (void *)0, LedTTask);
引数は以下のとおり。
-
- “GreenTimer”
- pdMS_TO_TICKS(GREEN_TIMER_MS)
- pdTRUE
- (void *)0
- LedTTask
1はソフトウエア・タイマ「タスク」の人間可読なお名前です。2がタイマの周期です。ここはタイマTICKSの値で指定(実際には1TICKS=1msとなっている筈)するのですが、pdMS_TO_TICKSマクロのよりミリ秒単位の数字をタイマTICKSに変換できます。
3のpdTRUEは、オートリロード(インターバルタイマ動作)指定です。pdFALSEとするとワンショット動作となります。
4の(void *) 0は、ソフトウエアタイマのID番号みたいです。後でこのソフトウエアタイマ「タスク」を操作するときに使えるIDだと。
5が実体処理のためのCallBack関数への関数ポインタです。
実験したソース
前回のソースコードをベースに、なるべく簡素で見通しがよいようにしてみたものが以下に。ホントか?
#include <Arduino_FreeRTOS.h> TaskHandle_t loop_task; TimerHandle_t Handle_LedTTask; int greenFlag; #define LED_GREEN (0) #define LED_ACTIVE (0x1) #define LED_INACTIVE (0x0) #define GREEN_TIMER_MS (500) static void LedTTask(TimerHandle_t xTimer) { if (greenFlag == 0) { digitalWrite(LED_GREEN, LED_ACTIVE); greenFlag = 1; } else { digitalWrite(LED_GREEN, LED_INACTIVE); greenFlag = 0; } } void setup() { Serial.begin(115200); while (!Serial) { } Serial.println("Arduino UNO R4, FreeRTOS SoftTimer extercise."); pinMode(LED_GREEN, OUTPUT); digitalWrite(LED_GREEN, LED_INACTIVE); auto const rc_loop = xTaskCreate ( loop_thread_func, static_cast<const char*>("Loop Thread"), 512 / 4, nullptr, 1, &loop_task ); if (rc_loop != pdPASS) { Serial.println("Failed to create 'loop' thread"); return; } Handle_LedTTask = xTimerCreate("GreenTimer", pdMS_TO_TICKS(GREEN_TIMER_MS), pdTRUE, (void *)0, LedTTask); xTimerStart(Handle_LedTTask, 0); Serial.println("Starting scheduler ..."); vTaskStartScheduler(); for( ;; ); /* Never! */ } void loop() { Serial.println(millis()); vTaskDelay(configTICK_RATE_HZ); } void loop_thread_func(void *pvParameters) { for(;;) { loop(); taskYIELD(); } }
実行結果
まず、前回同様のloopタスクで動作しているloop関数からUSBシリアルに向けて出力されてくるタイムスタンプが以下に。
一方、OSのソフトウエアタイマサービスから呼び出されているコールバック関数はLチカ中。
何個でも作れるし、ソフトウエアタイマは軽くてお楽。調子に乗って作りすぎたらどうなるだろ?