モダンOSのお砂場(30) FreeRTOS、タイマAPI対vTaskDelay

Joseph Halfmoon

前回は、自ら制御権を放棄するスケジューリングとプリエンプティブに取り出されるまで手放さないものを比べてみました。今回はタスクを決めた期間休ませるのに使っていたvTaskDelayを使わずタイマAPIを使って定期的に仕事をさせる方法と、今まで通りのvTaskDelayを使う方法を比べてみたいと思います。

※「モダンOSのお砂場」投稿順Indexはこちら

(末尾に実験で使用したソース全文を掲げました。)

FreeRTOSのSoftware Timer API

Software Timer APIは、スケジューラの制御単位であるタイマチック(今回使用のSAMD21の環境では1ms)を単位として、決まった周期あるいは、ワンショット期間の経過後に指定のコールバック関数を起動してくれるサービスです。ソフトウエアループではありません(「真の」くるくる回るループはvNopDelayMSです。)ソフトウエア・タイマ・デーモンとよばれるタスクが走っており、コールバック関数はそのタスクのコンテキスト内で処理されるようです。

処理すべき関数はキュー管理されているみたいです。コンフィギュレーションで決まる上限個数までをキューに登録して走らせることができます。ドキュメントは以下に。

FreeRTOS Software Timer API Functions

以前にワンショットで、タイマAPIを使ってみた回へのリンクはこちら

前回までの「繰り返し」との違い

前回まで、タスクに繰り返し仕事をさせるのに使ってきたのは、vTaskDelayという関数でした。その機能は、以下のような感じです。

    • 制御権をスケジューラに返す
    • 指定した時間経過後、再びスケジュールしてもらう

これにより「指定した時間」毎に周期的に仕事をするように「見えて」ました。しかし、問題は以下です。

    • タスク内で処理にかかる時間は「指定した時間」に入っていない
    • スケジュールしてもらうまでにかかる時間も入っていない

上の方は、自分自身が使う時間の問題ですが、下は他のタスクの状況次第です。時間そのものを正確(タイマチックの精度内で)に仕事をしたいのであれば、タイマAPIを使った方が正確です。

以下では、vTaskDelayでタイミングをとっているTask(赤色LEDを駆動するオシロ上黄色の信号を10mS単位でON/OFFしている)と、タイマAPIを使って定周期で起動されるコールバック関数(青色LEDを駆動するオシロ上青色の信号を10mS単位でON/OFFしている)を比べています。

実験1

末尾のソースを以下の設定でビルドすると、vTaskDelay()を使うタスクもタイマコールバック関数も信号のON/OFFに必要な処理だけで無駄な時間を使うことなく短い時間で制御を戻してくるようになります。

#define SOFTWAIT (0)

この場合の時間間隔は以下のようです。どちらの方式も、High期間、Low期間の両方でほぼ10mSを確保できています。予定どおりの時間間隔で処理できていることが分かります。

NO_WAIT_MES

実験2

末尾のソースを以下の設定でビルドすると、vTaskDelay()を使うタスクもタイマコールバック関数も、「重い処理負荷」にみたてた無駄な時間、それぞれ1mSを消費するようになります。

#define SOFTWAIT (1)

この場合の処理の様子を観察すると、黄色のvTaskDelayを使っているタスクの方は、「重い処理負荷」の時間がもろ見えです。それに対して青色のタイマAPIを使う方では、実験1と同様に定周期を保てています。

1mS_WAIT
また、黄色と青色の関係も変化するのを観察しました。実験1では、黄色と青のタイミングは毎回ほぼ一定でほとんど時間的揺らぎはありません。ところが実験2では絶えず揺らぎが見られ、時間的な関係が変動しつづけているようです。このことからも、黄色のタスクの定時性が揺らいでいることがわかります。

定時性を優先するなら、やはりタイマAPIでキッカケをつくるべきじゃないかと思います(ホントにクリティカルかつ高速なタイミングであればハードウエア割り込み利用ですが。)ただ、タイマAPIはデーモンタスクを複数のタイマコールバックで共用しています。今回実験2でやってみたように、コールバック内で長時間を消費するのはマズイと思われます。コールバックではトリガのみかけて、時間かかる部分は他所でやる、というのが正統的な使い方ではないかと。

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

モダンOSのお砂場(31) FreeRTOS、タスク・リストを一覧する

実験に使用したソース全文

Seeeduino Xiao用、VSCode + PlatformIO環境にて、Arduinoプラットフォーム上でビルドしたもの。

#include <Seeed_Arduino_FreeRTOS.h>

// D2/A2=PA10, D3/A3=PA11, D10/A10=PA6
#define LED_RED   (2)
#define LED_BLUE  (3)
#define LED_ACTIVE    (0x0)
#define LED_INACTIVE  (0x1)
#define STACK_DEPTH (256)
#define ENABLE_NOP_LOOP (0)
#define TEST_PIN  (10)
#define BLUE_TIMER_TICK  (10)
#define SOFTWAIT  (0)


int counter;
int blueFlag;
TaskHandle_t  Handle_RedTask;
TimerHandle_t Handle_BlueTask;

void waitLoop(int ms) {
  #if ENABLE_NOP_LOOP > 0
    vNopDelayMS(ms);
  #else
    vTaskDelay(ms / portTICK_RATE_MS);
  #endif
}

static void RedTask(void * pvParameters) {
    while (1) {
    digitalWrite(LED_RED, LED_ACTIVE);
    waitLoop(10);
    digitalWrite(LED_RED, LED_INACTIVE);
    waitLoop(10);
    #if SOFTWAIT == 1
      vNopDelayMS(1);
    #endif
  }
}

static void BlueTask(TimerHandle_t xTimer) {
  if (blueFlag == 0) {
    digitalWrite(LED_BLUE, LED_ACTIVE);
    blueFlag = 1;
  } else {
    digitalWrite(LED_BLUE, LED_INACTIVE);
    blueFlag = 0;
  }
  #if SOFTWAIT == 1
    vNopDelayMS(1);
  #endif
}

void setup() {
  Serial.begin(9600);
  vNopDelayMS(1000);
  while(!Serial);
  Serial.printf("ATSAMD21 FreeRTOS Preemtive task switch test. 001\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 + 2, &Handle_RedTask);
  blueFlag = 0;
//  xTaskCreate(BlueTask, "BLUE", STACK_DEPTH, NULL, tskIDLE_PRIORITY + 1, &Handle_BlueTask);
  Handle_BlueTask = xTimerCreate("BlueTimer", BLUE_TIMER_TICK, pdTRUE, (void *)0, BlueTask);
  xTimerStart(Handle_BlueTask, 0);
  Serial.printf("Start FreeRTOS Scheduler.\r\n");
  vTaskStartScheduler();
}

void loop() {
    while(1) {
        printf("Counter: %d\n",counter);
        digitalWrite(TEST_PIN, 1);
        delay(100);
        digitalWrite(TEST_PIN, 0);
        delay(1000);
    }
}