前回はFreeRTOSのスケジューラを起動をしたものの、Taskは1個だけでスケジューラのご利益はありませんでした。今回はTaskを2個にして、Blocking APIを使って「自主的に」制御権を手放すのと「プリエンプティブ」に制御権を取り上げて再スケジュールするのと両方のケースを観察してみたいと思います。実験に使用するのは、前回に続きSAMD21マイコン搭載Seeeduino XAIOボードです。
※「モダンOSのお砂場」投稿順Indexはこちら
(末尾に実験で使用したソース全文を掲げました)
FreeRTOSのTaskの状態遷移
FreeRTOSの上でタスクがどのような状態を遷移しているのかは、以下に説明がありました。
FreeRTOS task states and state transitions described
状態遷移はシンプルで以下の4つの状態しかないようです。
-
- Running
- Ready
- Blocked
- Suspended
今回使用のSAMD21G18マイコンの場合、CPUコアは1個しかないので、同時にRunning状態になるTaskは1個しかない筈です。他の実行可能なTaskはReady状態にあり、スケジューラが自分に制御権を付与してくれるのを待っている、と。Blocking API(例えばvTaskDelay)により自ら制御権を返上して待ちに入ったTaskはBlocked状態で待つ。そこでevent(例えばvTaskDelayであれば設定時間の経過)が発生すれば再びReady状態になり、今度は制御権がもらえるのを待つ、と。
上記以外にTaskを「棚上げ状態」にするAPIがあり、その対象となるとSuspended状態に入るみたいです。Suspendedは今回使わないので省略します。
実験する状態遷移
実験してみたのは以下の3つのケースです。。
-
- 2つのTaskがあり、どちらもBlocking APIにより制御権を自主的に返す
次の2つのケースにも2つのTaskがありますが、Blocking APIは使いません。どちらも制御権は自主返納せず処理を継続します。もしスケジューラがプリエンプティブに制御権を召し上げなければ無限にCPUを占有しつづけます。ただし2つのケースで以下が異なります。
-
- 第1のケースは2つのTaskにプライオリティの差がある
- 第2のケースは2つのTaskのプライオリティが等しい
冒頭のアイキャッチ画像に状態遷移図を掲げましたが、上の図がBlocking APIのケースであり、下の図がプリエンプティブなケースの2つに相当します。
実験用の設定
以下の写真のように、Seeeduino Xiaoボードの2番、3番ピンにそれぞれ赤と青のLEDを取り付けました。第1のTaskはRedTaskとし、赤のLEDを点滅させます。第2のTaskはBlueTaskとし、青のLEDを点滅です。ただし、予定通りの周期で点滅した場合高速すぎて目に見えないので、オシロ(Digilent Analog Discovery2)も接続して各信号を観察します(LOWでLED点灯。)
Blocking API使用のケース
末尾のソースの1か所、以下のように設定を変更すると Blocking APIを使って待つようになります。
#define ENABLE_NOP_LOOP (0)
この時の2番、3番の端子の様子を観察したものが以下です。RedTask側が2番で黄色のC1、BlueTask側が3番で青色のC2です。なお Taskのプライオリティは BlueTask > RedTask としてありました。
上記のように2つのTaskは期待通り「並行」に動作しているように見えます。また、右側にHigh、Lowの期間の測定値が出るようにしましたが、Red側10ms、Blue側7msの設定値に極めて近い値が出ています。RTOS側でタイミングをとってくれるこの方法は使い易いです。
プリエンプティブ、でもプライオリティに差があるケース
以下のように変更すると、Blocking APIを使わず、一度制御権をもらうと自らは決して手放さなくなります。
#define ENABLE_NOP_LOOP (1)
また、なお Taskのプライオリティは BlueTask > RedTask としてあります。プライオリティの設定は、xTaskCreateの第5引数のところです。
xTaskCreate(RedTask, "RED", STACK_DEPTH, NULL, tskIDLE_PRIORITY + 1, &Handle_RedTask); xTaskCreate(BlueTask, "BLUE", STACK_DEPTH, NULL, tskIDLE_PRIORITY + 2, &Handle_BlueTask);
この時の2番、3番の端子の様子を観察したものが以下です。
プライオリティの高いC2のBlueTaskの方は、ほぼ設定した7msのHIGH/LOW期間で走行していますが、プライオリティの低いC1のRedTaskの方がまったく走っている形跡がありません。これは、毎回のタイマチックによりBlueTaskの制御権は一度は召し上げられてスケジューラの再スケジュール対象になるものの、プライオリティが低いRedTaskが選択されることは無く、結局 BlueTaskに制御が戻る、という繰り返しになっていると考えられます。
なお、SAMD21上の本件の実装では、スケジューリングをするタイマチックは1msみたいでした。
プリエンプティブ、同一プライオリティ
末尾のソース通りにすると、プリエンプティブでかつRed、Blue同一プライオリティでの実行となります。そのときの様子がこちら。
RedTaskもBlueTaskも並列に動作しているように見えますが、点滅の周期がほぼほぼ倍になっています。単独Taskであれば、RedTaskは10ms毎にHigh/Low、BlueTaskは7ms毎が設定値です。
同一プライオリティの場合、ラウンドロビンでスケジュールされるようなので、RedTaskとBlueTaskに交互にCPUが割り当てられた結果、つまり50%のCPU時間で実行した結果、完全ソフトウエア制御(NOPループ)の点滅周期が倍になってしまった、ということのようです。分かり易い。
限りあるCPU資源、有効に使いましょう、ということでいいかな?
なお、今回使用したSAMD21の環境の場合、特に設定しなくてもプリエンプティブな制御はイネーブルになっていました。この辺の許可、不許可、タイマチックの周期などは freeRTOSConfig.h というファイルに記載されてます。
モダンOSのお砂場(28) FreeRTOS、スケジューラの起動、SAMD21 へ戻る
モダンOSのお砂場(30) FreeRTOS、タイマAPI対vTaskDelay へ進む
実験に使用した Arduino環境向けC++コード全文
設定は最後のプリエンプティブな制御の場合。
#include <Seeed_Arduino_FreeRTOS.h> // D2/A2=PA10, D3/A3=PA11 #define LED_RED (2) #define LED_BLUE (3) #define LED_ACTIVE (0x0) #define LED_INACTIVE (0x1) #define STACK_DEPTH (256) #define ENABLE_NOP_LOOP (1) int counter; TaskHandle_t Handle_RedTask; TaskHandle_t Handle_BlueTask; void waitLoop(int ms) { #if ENABLE_NOP_LOOP > 0 vNopDelayMS(ms); #else vTaskDelay(ms / portTICK_RATE_MS); #endif } // For the preemptive scheduling test only. static void RedTask(void * pvParameters) { while (1) { digitalWrite(LED_RED, LED_ACTIVE); waitLoop(10); digitalWrite(LED_RED, LED_INACTIVE); waitLoop(10); } } // For the preemptive scheduling test only. static void BlueTask(void * pvParameters) { while (1) { digitalWrite(LED_BLUE, LED_ACTIVE); waitLoop(7); digitalWrite(LED_BLUE, LED_INACTIVE); waitLoop(7); } } void setup() { Serial.begin(9600); vNopDelayMS(1000); while(!Serial); Serial.printf("ATSAMD21 FreeRTOS Preemtive task switch test. 000\r\n"); pinMode(LED_RED, OUTPUT); pinMode(LED_BLUE, OUTPUT); digitalWrite(LED_RED, LED_INACTIVE); digitalWrite(LED_BLUE, LED_INACTIVE); counter = 0; xTaskCreate(RedTask, "RED", STACK_DEPTH, NULL, tskIDLE_PRIORITY + 1, &Handle_RedTask); xTaskCreate(BlueTask, "BLUE", STACK_DEPTH, NULL, tskIDLE_PRIORITY + 1, &Handle_BlueTask); Serial.printf("Start FreeRTOS Scheduler.\r\n"); vTaskStartScheduler(); } void loop() { // If the RTOS scheduler fails, while(1) { printf("Counter: %d\r\n",counter); delay(10000); } }