モダンOSのお砂場(29) FreeRTOS、プリエンプティブなスケジューリング

Joseph Halfmoon

前回は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点灯。)

PreEmptiveDUT

Blocking API使用のケース

末尾のソースの1か所、以下のように設定を変更すると Blocking APIを使って待つようになります。

#define ENABLE_NOP_LOOP (0)

この時の2番、3番の端子の様子を観察したものが以下です。RedTask側が2番で黄色のC1、BlueTask側が3番で青色のC2です。なお Taskのプライオリティは BlueTask > RedTask としてありました。

API_Blocking
上記のように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番の端子の様子を観察したものが以下です。

PreEmptiveBlueHigh
プライオリティの高いC2のBlueTaskの方は、ほぼ設定した7msのHIGH/LOW期間で走行していますが、プライオリティの低いC1のRedTaskの方がまったく走っている形跡がありません。これは、毎回のタイマチックによりBlueTaskの制御権は一度は召し上げられてスケジューラの再スケジュール対象になるものの、プライオリティが低いRedTaskが選択されることは無く、結局 BlueTaskに制御が戻る、という繰り返しになっていると考えられます。

なお、SAMD21上の本件の実装では、スケジューリングをするタイマチックは1msみたいでした。

プリエンプティブ、同一プライオリティ

末尾のソース通りにすると、プリエンプティブでかつRed、Blue同一プライオリティでの実行となります。そのときの様子がこちら。

PreEmptivePrioEqual
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);
    }
}