FreeRTOSにはある期間の間Taskの実行をブロックするDelayの名がつくAPIが3種あります。前回まで一番お手軽なvTaskDelay(引数も一つしかないし)ばかりを使ってきたのですが、今回は3種の違いについて勉強したいと思います。その上で実機上で動かして実感してみると。軽負荷だとあまり差が見えないけれど。
※「モダンOSのお砂場」投稿順Indexはこちら
3種のDelayに関するAPI
3種のAPIを列挙すると以下の3つです。
-
- vTaskDelay()
- vTaskDelayUntil()
- xTaskDelayUntil()
上記3つに共通するのは、これらAPIを呼び出すとある時間の間、Taskの実行はブロックされ(ブロックされている期間はRTOS側に制御が移り他の仕事が実行される)るということです。時間設定の単位は Tick です。今回のターゲット機、Arduino UNO R4 MinimaのArduino環境のFreeRTOSのデフォルト値では 1 Tick は 1m秒ということです。
上記のAPIに関する御本家ドキュメントは以下に。
RTOS素人が、勝手に3つのAPIの比較説明図を描いたものが以下に。
vTaskDelayは、指定のTick数だけ実行をブロック(遅延させる)APIです。1000待てと言われたら 1=1msなので1秒待ちます。このため、上記の図のように定期的に何かお仕事をしていて、お仕事にかかる時間が変化する場合は、周期は当然変動することになります。
一方、vTaskDelayUntilとxTaskDelayUntilは、どちらも「指定Tick時刻」までの遅延です。目覚まし時計みたいなものです。毎回、起床したときに現在時刻を取得しておいて、目覚ましで起きたら次の起床時刻をセットするというキッチリしたやりかたです。指定時刻は絶対値なので、お仕事の負荷が変動しても、かなり正確に周期を守れる筈。
なお、vTaskDelayUntilとxTaskDelayUntilの差は、頭のvとxで分かるのですが戻り値があるか無いかの差です。vはvoidのvです。戻り値が無いAPIっす。一方 x は「ナンチャラ_t」みたいな typedef された値が戻ってくるときのプリフィックスです。その辺のお作法については、御本家の以下に記述があります。
Coding Standard, Testing and Style Guide
さて、戻り値がある xTaskDelayUnitl がありがたいのは、お仕事が重すぎたのか、既に指定の目覚まし時刻を過ぎてしまっていたような場合です。戻り値みればそのような問題発生を検出できるので対処が可能かと。どうする?
一方 vTaskDelayUntil の方は、超過してしまったらすぐに帰ってくるものの、超過したのかどうかは知る由もないと。
実験用のソース
さて、今回実験に使ったソースは以下です。3つのTaskを並行に動作させ、
-
- loop_taskには vTaskDelay
- task1 にはvTaskDelayUntil
- task2にはxTaskDelayUntil
のように割り当ててそれぞれTaskの実行を制御させます。全て同じ 10 Tick(=10msec)の定周期実行としました。
taskの実行は、各タスクに1ピンづつ割り当て、Delayが解除される度にピンをトグルすることで外部で確認できるようにしてみました。
-
- loop_taskには D5端子
- task1 にはD6端子
- task2にはD7端子
ソースが以下に。Arduino IDE上の「スケッチ」形式です。
#include <Arduino_FreeRTOS.h> #define T2PIN (7) #define T1PIN (6) #define LPIN (5) #define DUMMY (4) #define LWAIT (10) TaskHandle_t loop_task, task1, task2; void initPins() { pinMode(LPIN, OUTPUT); digitalWrite(LPIN, 1); pinMode(T1PIN, OUTPUT); digitalWrite(T1PIN, 1); pinMode(T2PIN, OUTPUT); digitalWrite(T2PIN, 1); pinMode(DUMMY, OUTPUT); digitalWrite(DUMMY, 1); } bool togglePin(pin_size_t pin, bool flag) { if (flag) { digitalWrite(pin, 1); } else { digitalWrite(pin, 0); } return !flag; } void loop_thread_func(void *pvParameters) { bool flag = true; int idx = 0; int waitLis[3] = {100, 200, 300}; while (1) { vTaskDelay(LWAIT); for (int i=0; i<waitLis[idx]; i++) { digitalWrite(DUMMY, 1); } idx = idx < 2 ? idx+1 : 0; flag = togglePin(LPIN, flag); } } void task1_func(void *pvParameters) { bool flag = true; TickType_t xLastWakeTime; const TickType_t xFrequency = 10; xLastWakeTime = xTaskGetTickCount(); while (1) { vTaskDelayUntil(&xLastWakeTime, xFrequency); flag = togglePin(T1PIN, flag); } } void task2_func(void *pvParameters) { bool flag = true; TickType_t xLastWakeTime; const TickType_t xFrequency = 10; BaseType_t xWasDelayed; xLastWakeTime = xTaskGetTickCount(); while (1) { xWasDelayed = xTaskDelayUntil(&xLastWakeTime, xFrequency); flag = togglePin(T2PIN, flag); } } void setup() { Serial.begin(115200); while (!Serial) { } initPins(); 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; } auto const rc_task1 = xTaskCreate ( task1_func, static_cast<const char*>("Task1"), 512 / 4, nullptr, 1, &task1 ); if (rc_task1 != pdPASS) { Serial.println("Failed to create 'task1' thread"); return; } auto const rc_task2 = xTaskCreate ( task2_func, static_cast<const char*>("Task2"), 512 / 4, nullptr, 1, &task2 ); if (rc_task2 != pdPASS) { Serial.println("Failed to create 'task2' thread"); return; } Serial.println("Starting scheduler ..."); vTaskStartScheduler(); for( ;; ); /* Never! */ } /* NEVER CALLED! */ void loop() { }
実機上での実行結果
ビルドしてオブジェクトを書き込み後、シリアルモニタを接続すると実行が始まります(setup()の中で、while (!Serial) { } しているのでシリアルモニタがスタートしないと実行開始されません。)
D5、D6、D7の各端子にはAnalog Discovery2のロジアナ端子を接続したので、それらがトグルする様子が観察できます。
横方向の1divが5msです。眺めていると、微妙に立下りエッジが僅かにズレたりはします。しかし上記のような「軽い(短時間で終わる)お仕事」を、このように遠くから引いて眺めている(サンプリングは80kHzくらいの筈)分には3種のAPIともほぼほぼ10ms周期を守れている感じです。差が見たければもっと負荷を増やして「寄る」ことが必要そうです。ま、いっか。