[esp32] RTOS周りの事


前提

  • VSCode + PlatformIO
  • フレームワークはArduino

FreeRTOS configファイル

~/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/freertos/freertos/FreeRTOSConfig.h

CONFIG_FREERTOS_UNICOREみたいないろんな変数がどこから来てるのか?
覚える必要はないけど、どこにあるのかぐらい知っておくと理解が早い。

delay

生のesp32-IDF(IoT Development Framework)を使わないなら、普通はArduinoフレームワークでやってるはず。

いろいろな場所でRTOSを使うのならvTaskDelayを使わないといけないとあるが、

main.cpp
#include <Arduino.h>
...

こうすると、多分Arduinoっぽくesp32-IDFを使えるのでArduinoと同じ感覚でdelayは使える。

内部ではvTaskDelayを使ってるのでArduinoのdelayのようにブロックしない。

void delay(uint32_t ms)
{
    vTaskDelay(ms / portTICK_PERIOD_MS);
}

タスクの作成

xTaskCreateとかxTaskCreatePinnedToCoreとかどっちを使うべきなのか?

正解はxTaskCreatePinnedToCore xTaskCreateは古い書き方(Coreが1個で固定 - なのでCoreの指定ができない)で、esp32は元々が2Coreで、WIFIはCore0側で専有している?はず。
WIFIを同時に使わないならCore0、1両方でタスクを展開できるけど、そうでないならタスクはCore1に作るほうがエラーがなさそう。

#if CONFIG_FREERTOS_UNICORE
static const BaseType_t appCpu = 0;
#else
static const BaseType_t appCpu = 1;
#endif

なのでこう言う記述をよく見る。esp32はuni coreではないので必然的にappCpu = 1が選ばれることになる。なので、

xTaskCreatePinnedToCore(taskName, "taskNameForHuman", 1024, NULL, 0, NULL, appCpu);

流れとしてタスクを作る時は最後のパラメーターとして1が入ることになる(WIFIが専有しない側のCore)。

タスク作成時の各種パラメーター

  1. タスク関数ポインタ
  2. デバッグ用としてのタスク名
  3. スタックのサイズ
  4. タスク関数に渡したいパラメーターのポインター
  5. 優先度
  6. タスクハンドル
  7. コア番号

タスク関数ポインタは関数名をそのまま渡せばそれでいい。

デバッグ用としてのタスク名は分かりやすければ何でも良い。

スタックのサイズは最低でも768(FreeRTOSConfig.hでconfigMINIMAL_STACK_SIZEに定義されている)
だいたいどこの説明でも「とりあえず1024で始めて、だめなら上げたら良い」というが、何を持って足りないのか、何を持って余分のかが分からないが、

  • 足りないとクラッシュする
  • 取りすぎると全体としてのメモリが足りなくなる
こう言うのが出たらタスクのスタックサイズ不足と考えていい
Guru Meditation Error: Core  1 panic'ed (Unhandled debug exception)
Debug exception reason: Stack canary watchpoint triggered 

多すぎるかどうか問題はuxTaskGetStackHighWaterMark(NULL)を使う
パラメーターはタスクハンドルでNULLだと自分自身のタスクという意味。intで数値が返ってくる。
タスク実行中にこの関数が呼ばれた時点までで一番低かったタスクに割り当てられたスタックサイズ(最大に使用した時点)がわかる。

  • 1024で割り当てた
  • タスク実行で100使った(+100)
  • 処理中500使った(+100 +500)
  • この処理が終わって400開放された(+100 +500 -400)
  • ...

こう言う流れで考えて100+500の時点が一番使ってるので、1024−600=424がこの関数で返ってくる。
処理の流れにもよるが処理の最後でこの関数を呼んで、後どれぐらいスタックサイズが残っているのかを見てゼロに近いほうが良いが、あまりギリギリのラインで行って足りなくなってクラッシュするのは避けたい。

スタックサイズを途中で上げられないと思うので、開発中にデバッグログとして監視しておくしか無いかも。

タスク関数に渡したいパラメーターのポインターは何個かみたyoutubeの解説でも「まーこれは気にするな」的に説明を避けてたのが多かった。
怖がることはなくて、単純に渡したい引数なんだけど別スレッドでタスクは走るので単純にどういう値であっても渡せない。
なのでポインターだけを渡せばいいよね?って言うことでタスクの宣言は必ず

void taskName(void *parameter) { ... }

となっててパラメーター変数のポインターだけを一個取れるようになっている。
パラメーターなのでその時々に応じた変数を渡したいと思うんだけど、ローカル変数のポインターを渡してもタスクが実際に起動してパラメーターをポインターから取り出してタスク側のローカル変数なりにコピーするとかするまで、呼び出し元のローカル変数が存在することを保証されないといけない(呼び出し元の関数が終了すればそこのローカル変数は消えると言うか保証できなくなるわけで)。
そうなると急に話がややこしくなるので、ネットにあるような解説ではこの辺は避けて通ってる気がする。

  • 単純に変数のポインターを渡したいが、その変数はいつまで生き残っているのか?

の理解がとても重要。今パラメーターを渡す例が思いつかないので、前に書いた記事を読んでみると分かりやすいかも。

優先度は同じくFreeRTOSConfig.hにconfigMAX_PRIORITIESとして宣言されている(0-24までの25個、0が最低)。この辺は自分のタスクの数、それぞれの重要性によって変わるので、最初は0から始めて調整していくしか無いかも。

ただし、ESP32のライブラリの中で作成されるタスクで優先度が高いものは23で作られています。他のどんな処理よりも優先されるべきタスクは最高の優先度である24で作る必要があります。

タスクハンドルはもし外部からタスクを一時停止、再開したり、完全に消したりしたい場合にのみ必要。

通常はグローバル変数として宣言しておいて、タスクを作成時にその変数のポインターを渡す。

TaskHandle_t taskHandle;
...
xTaskCreatePinnedToCore(..., &taskHandle, appCpu);
// 不意にNULLで(実行タスク自身)操作しないためにこうしておくとか?
while(taskHandle == NULL); 
// もしくは操作する前にハンドルがNULLじゃないことを確認したほうが良い
// esp32+arduinoフレームワークでは、loopも実際にはタスクなので
// loop内でvTaskSuspend(NULL)とかやるとloopが止まる

因みにloopのスタックサイズは ~/.platformio/packages/framework-arduinoespressif32/cores/esp32/main.cpp にCONFIG_ARDUINO_LOOP_STACK_SIZEで指定されている。

xTaskCreateUniversal(loopTask, "loopTask", CONFIG_ARDUINO_LOOP_STACK_SIZE, NULL, 1, &loopTaskHandle, CONFIG_ARDUINO_RUNNING_CORE);

CONFIG_ARDUINO_LOOP_STACK_SIZE
~/.platformio/packages/framework-arduinoespressif32/tools/sdk/include/config/sdkconfig.h の中で8192

後は必要な時にそのハンドルを使って操作するだけ。

void someMethod(){
  vTaskSuspend(taskHandle);
}

因みに自分自身が操作する場合、ハンドルはNULLでいい。

自分自身が自分のタスク内で操作する場合
void taskName(void *parameter){
  ...
  vTaskSuspend(NULL);
}

コア番号はWIFIを使ってる場合は、1にしておいたほうが良いと思う。
そうでないならCore0、1を両方フルに使えるのでタスクが多いなら状況に応じて作成時に0や1を指定してやるとCoreを取りこぼれなく使える。

同じタスクの作成個数

xTaskCreatePinnedToCore(taskName, "taskNameForHuman", 1024, NULL, 0, NULL, appCpu);

は、必要であれば

for(auto i = 0; i < 10; i++){
  xTaskCreatePinnedToCore(taskName, "taskNameForHuman", 1024, NULL, 0, NULL, appCpu);
}

としたって構わない。メモリが足りるかどうかの問題。

タスクから使うグローバル変数

プリミティブな変数だとvolatileを付けたほうが無難。
そうじゃないとコンパイラが勝手にソースコードを判断して「この変数変更してなくね?」とか思って勝手に「最適化しておきましたんで!」とかやって、変数を見ないようにしたりするっぽい。

ESP_LOG*でのログ出力

これを使ってログを出す場合、どうも自分の場合はタスク内で単純にESP_LOG*を使っても表示されなくて、タスクが動いているのかどうかわからないことがあった。
だけどなぜかloop{}内に単純なESP_LOGD(TAG, "heart beat ...");みたいなのを置くだけでタスク側のログも表示されだして、ここはちょっとよく分からないが、loopで何もしなくても

void loop() {
  ESP_LOGD(TAG, "[%s] uxTaskGetStackHighWaterMark: %d", "loop",
           uxTaskGetStackHighWaterMark(NULL));
  delay(10000);
}

とかして、とりあえずスタックサイズが十分あるかとか適当なことをログに出しておくと良いのかも。

参考