モダンOSのお砂場(22) FreeRTOS、ソフトウエアタイマAPIでワンショット

Joseph Halfmoon

前回はMutex排他制御でしたが、今回はソフトウエアタイマAPIであります。ソフトウエアタイマというと自前でループで回る「あれ」をつい想像してしまいますが違います。RTOSです。ちゃんとOSが面倒を見てくれる「タイマ」です。ある意味ハードウエアのリソースの制限が無い分、使いやすくもあるもの。

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

(今回使用のESP32 DevKitC上で動作するArduino環境用ソース全文は末尾に)

ハードウエアのタイマを使うべきか、ソフトウエアのタイマを使うべきかの判断ポイントの一つがレゾリューション、時間解像度と言っても良いかと思います。ハードウエアのタイマを使えば、タイマ(カウンタ)のカウントに使われる一定のクロック周波数で何等かの事象を測って対応することができます。ソフトウエアタイマの場合、マイクロコントローラ(MCU、マイコン)のクロック速度があまり速くないことが多いので、1命令の実行時間の制限でハードウエアほど細かく見れない、ということもあります。また、例えCPUのクロック速度が速くても、CPUの実行時間は、割り込みとかキャッシュミス等の諸々の外因で変動してしまうので、正確なところを測り難いということもありえます。

半面、ハードウエアのタイマには限りがあります。最近のマイクロコントローラでは5,6本から多いものでは10本以上ものタイマ・カウンタを搭載しているものがありますが、数に限りがあることに変わりはありません。ましてや高速なIO制御に使われるインプット・キャプチャ、アウトプット・コンペアなどの機能が設定されているタイマユニットの数はごく限られた数本だと思います。そして当然のことながらハードウエアタイマにはマイコンの機種依存性があります。同じマイコンシリーズでも高級機種では使えたタイマが廉価版では不在とかも。

結局、多くの場合、ハードウエアでなければ正確にハンドルできないような制御(例えばμ秒以下精度など)はハードに任せ、そうでなければソフトで処理するというのが妥協点になるかと思います。しかし仕事が全く無い時のアイドルループ(消費電力考えればsleepとかpower-downを考えるでしょう)を除けば、単純なソフトウエアループで時間待ちをするというのはごくごく短期間であることが保証されているのでなければ使用できないでしょう。「空回り」していては貴重なCPUパワーを無駄に使ってしまって他の仕事ができないからです。

そんな時に役に立つのが、FreeRTOSの Software Timer API じゃないかと思います。このAPIを使うと、

今からxx後にこの関数を起動して

といった機能を簡単に使うことができます。xxにはOSのタイマチックの数値が入ります。サンプルプログラムを作るのに使っている ESP32 DevKitCのArduinoプラットフォームでは1タイマチック=1ミリ秒です。ハードウエアのタイマを使っても当然実現できる機能ですが、ハードウエアのタイマと違い、このタイマには明示的な本数の制限は無いと思います(メモリが許せばですが。)ソフトウエアが本当に必要な本数を確保できます。また、設定は1回だけ(ワンショット)でも周期的(オートリロード)でも可能です。この仕組みについては以下のFreeRTOSのページに説明があります。

FreeRTOS – Software Timers

ここでポイントは、この仕組みはタイマ割り込みのハンドラの外に置かれていることです。タイマチックの奥底にはタイマハードウエアがあるのかも知れませんが、ソフトウエアタイマAPIで登録した関数は、割り込みハンドラのコンテキストで呼び出されているわけではなく、タイマタスクというタスクのコンテキストで呼び出されているようです。

ワンショットタイマ利用

今回は、ソフトウエアタイマAPIの練習として以下のような機能を実装してみました。

  • 赤色のボタンが押されたことを検出したら、赤色のLEDを点灯、3秒後に消灯する。
  • 上記とは独立で、緑色と青色のLEDを別周期でLチカさせておく。

複数Lチカは、過去にも別投稿で、スレッドとコルーチンで行ったり、複数タスクで行ったりと何度となくやったのですが、ブレッドボード上にこさえるハードが簡単なのでこれになりました。

赤色のボタンが押されたらLEDを点灯した上、3秒に設定したワンショットタイマを起動します。3秒後にタイマがイクスパイヤすると後始末のCallBack関数が呼ばれてLEDを消灯する、という塩梅。

お手軽なので、ついつい使ってしまいそうだな。しかし、FreeRTOSのドキュメントに曰く(1箇所引用させていただきます)

Software timer functionality is easy to implement, but difficult to implement efficiently.

裏じゃきっとテクを使っているのだ、これが。

モダンOSのお砂場(21) FreeRTOS、Mutex排他制御の効果が「分かる」サンプル へ戻る

モダンOSのお砂場(23) Arm MbedでBBC micro:bit プログラミング へ進む

「ワンショット」ソフトウエアタイマAPIのサンプルプログラム
#define LED_RED     (25)
#define LED_GREEN   (26)
#define LED_BLUE    (32)
#define SW_RED      (34)
#define SW_YELLOW0  (35)
#define SW_YELLOW1  (27)

static const BaseType_t app_cpu = 0;
static const int led_pin = 23;
static int counter;
static bool okFlag = true;

static bool inUseRED = false;
static TimerHandle_t oneShotRED = NULL;

void redCallback(TimerHandle_t xTimer) {
  digitalWrite(LED_RED, LOW);
  inUseRED = false;  
}

void setup() {
  Serial.begin(115200);
  while(!Serial) {};
  delay(10000);
  Serial.println("TEST SOFTWARE TIMER");
  pinMode(led_pin, OUTPUT);
  pinMode(LED_RED, OUTPUT);
  pinMode(LED_GREEN, OUTPUT);
  pinMode(LED_BLUE, OUTPUT);
  pinMode(SW_RED, INPUT);

  counter = 0;

  oneShotRED = xTimerCreate("one-shot-RED", 3000, pdFALSE, (void *)0, redCallback);
  if (oneShotRED == NULL) {
    Serial.println("SOFTWARE TIMER CREATION FAIL.");
    okFlag = false;  
  }
}

void loop() {
  counter++;
  if (okFlag && (digitalRead(SW_RED) == 0)) {
    Serial.println("RED");
    digitalWrite(LED_RED, HIGH);
    xTimerStart(oneShotRED, portMAX_DELAY); 
  }
  if (counter & 0x02) {
    digitalWrite(LED_GREEN, HIGH);
  } else {
    digitalWrite(LED_GREEN, LOW);    
  }
  if (counter & 0x04) {
    digitalWrite(LED_BLUE, HIGH);
  } else {
    digitalWrite(LED_BLUE, LOW);    
  }
  vTaskDelay(1000);  
}