概要
ESP32はマルチコアで、FreeRTOSを搭載しているのでマルチタスクでプログラムを実行できます。しかしながら最初はちょっと概念の理解が大変なので、単純にマルチタスクが使えるライブラリを作ってみました。
マルチタスクとは?
上記に過去にまとめた記事がありますが、マルチタスクを使ってみたいだけの場合にはちょっと大げさになります。
このライブラリはFreeRTOSの入門と、概念といてのタスクを学ぶためのものになります。このライブラリを使い続けてもいいですし、もう少し細かい指定がでできるタスクライブラリも作成済みになります。

ざっくりとしたタスク概念図です。FreeRTOSのタスクは優先度があって、優先度の高いタスクから実行されていきます。時間単位でTickという単位に区切りArduinoでは1ms単位、ESP-IDFだとデフォルトが10ms単位で動いています。タスク1からタスク2に実行が移るタイミングはタスク1が明示的に処理を解放したときになります。Arduinoの場合にはdelay()関数を1以上の数値で呼び出した場合になります。
Tick1を見ているとタスク4がTickの変わり目にTick2のタスク1の動作に移行しています。ここは明示的なdelay()ではなくTickの変わり目なので、再度優先順位が高いタスクから実行を行います。
Tick2を見ると優先順位が低いタスク4が実行されていません。このように優先順位が高いタスクが明示的なdelay()を呼び出さないと、優先順位が低いタスクが呼び出されないことになります。これは非常に注意が必要なので、一定の処理が実行したらdelay(1)を最低限呼び出すようにしてください。
void loop()
{
delay(1);
}
ちなみにloop()関数もdelay(1)を入れることを推奨します。loop()はかなり優先順位が低いので、処理を解放しなくても構わないのですがなんと消費電力が下がります。1ms程度の遅延が許容できるのであれば入れておくことをおすすめします。
ESP32のタスク構造
ESP32はデュアルコアになっていて、Core0(PRO_CPU)とCore1(APP_CPU)が独立して動くことができます。そのためCore0の処理が重くてもCore1への影響がありません。
一般的なOSのように空いているコアで処理を実行することなどはできませんので、どのコアのどの優先順位で指定して起動する必要があります。一応コアは自動設定できるのですが、起動時にコアを選択してそこで動き続ける動きとなります。
| Core0(PRO_CPU) | Core1(APP_CPU) | |
|---|---|---|
| 優先度24 | ipc0(内部管理用) | ipc1(内部管理用) |
| 優先度23 | wifi(WiFi処理) | |
| 優先度22 | esp_timer(Arduinoのタイマー処理) | |
| 優先度21 | ||
| 優先度20 | sys_evt(システムイベント処理) | |
| 優先度19 | arduino_events(アプリイベント処理) | |
| 優先度18 | tiT(FreeRTOSのタイマー) | |
| ~ | ||
| 優先度4 | ||
| 優先度3 | ||
| 優先度2 | ||
| 優先度1 | Tmr Svc(FreeRTOSのタイマー) | loop(loop関数) |
| 優先度0 | IDLE0(アイドル用関数) | IDLE1(アイドル用関数) |
Arduinoでデフォルトで動いているタスクの一覧になります。一番優先順位の高い優先度24にはマルチコア間でのデータ制御を行うipc0とipc1が動いています。処理自体は軽いので特に意識する必要がありません。
問題はWi-Fiの処理がCore0の優先度23で動いていることです。このタスクが処理によって非常に重くなります。とくに周りのアクセスポイントを検索するモードでは秒単位でCore0の処理をすべて占有する可能性があるぐらいのイメージでいてください。つまりWi-Fiを利用する場合にCore0でタスクを動かすと安定しない可能性があります。とくにタイマー系は影響を受けやすくArduinoとFreeRTOSのタイマー処理はCore0で動いているのでWi-Fi通信中のタイマー動作の時間精度はかなり悪化しますので注意してください。
次に意識するのがArduinoのloopタスクです。このタスクがCore1の優先度1で動いているので、ユーザーが使うタスクは2から4ぐらいの優先度がおすすめです。
ライブラリの使い方
#include <ESP32AutoTask.h>
// コア1・Normal優先度で呼ばれる(既定は1ms周期)。
void LoopCore1_Normal()
{
static uint32_t count = 0;
if ((count++ % 1000) == 0)
{
Serial.printf("[Core1 Normal] millis=%lu\n", millis());
}
}
void setup()
{
Serial.begin(115200);
ESP32AutoTask::AutoTask.begin();
}
void loop()
{
// メインloopも動くので、フックが回るよう軽くしておく。
delay(1);
}
上記のように利用します。ESP32AutoTask::AutoTask.begin()で自動的にタスクが実行されるようになります。動くタスクは規定された関数名のものが定期的に呼び出される形となります。またタスク関数が完了した段階でdelay(1)を内部で呼び出していますので安全にマルチタスクが利用可能です。
| 関数名 | コア | 優先度 |
|---|---|---|
| void LoopCore0_Low() | Core0 | 1 |
| void LoopCore0_Normal() | Core0 | 2 |
| void LoopCore0_High() | Core0 | 3 |
| void LoopCore1_Low() | Core1 | 1 |
| void LoopCore1_Normal() | Core1 | 2 |
| void LoopCore1_High() | Core1 | 3 |
定義されている関数は上記の6個になります。Lowの優先順位はloopと同じ1で、NormalとHighで優先度がさらに上がっている形となります。
Wi-Fiを利用しない場合にはCore0を、Wi-Fiを利用している場合にはCore1を選択するのが好ましいと思います。
応用でタイマー的に利用する(delay)
void LoopCore1_Normal()
{
Serial.printf("[Core1 Normal] millis=%lu\n", millis());
delay(1000);
}
上記のようにタスクの最後に次回までのウエイトを入れることで、だいたい1秒後に再度呼び出されます。ただし、Serial.printf()関数の実行時間+1,000msですので、呼び出し周期は1秒ちょっとになります。
あまり時間精度にこだわりがない場合にはこの指定で問題ありません。
応用でタイマー的に利用する(periodMs)
#include <ESP32AutoTask.h>
void LoopCore0_Low()
{
Serial.printf("[Core0 Low] millis=%lu\n", millis());
}
void setup()
{
Serial.begin(115200);
ESP32AutoTask::Config cfg;
cfg.core0.low.periodMs = 1000; // Lowを1000ms周期に
ESP32AutoTask::AutoTask.begin(cfg);
}
void loop()
{
delay(1);
}
初期化のセッティングで優先度と呼び出し周期を設定可能です。この場合にはタスク内での実行時間に左右されず概ね1000ms周期で呼び出されます。1ms以下の時間精度はありませんが、10ms周期で実行したい場合にはこの設定で安定して動くはずです。ただし、Core0で動かす場合にはWi-Fiを利用すると処理が占有される可能性があるのでそこは注意してください。
仕組み
__attribute__((weak)) void LoopCore0_Low() { vTaskDelete(nullptr); }
__attribute__((weak)) void LoopCore0_Normal() { vTaskDelete(nullptr); }
__attribute__((weak)) void LoopCore0_High() { vTaskDelete(nullptr); }
__attribute__((weak)) void LoopCore1_Low() { vTaskDelete(nullptr); }
__attribute__((weak)) void LoopCore1_Normal() { vTaskDelete(nullptr); }
__attribute__((weak)) void LoopCore1_High() { vTaskDelete(nullptr); }
あらかじめすぐにタスクを終了する関数を6個定義してあります。weak属性付きなので、ユーザーが同じ名前で関数を定義することで上書きすることが可能です。つまりユーザーが定義していない場合には即完了するタスクを実行、ユーザーが停止した場合にはそのタスクを定期実行する処理になっています。
void taskLoop(uint32_t periodMs, void (*fn)())
{
TickType_t lastWake = xTaskGetTickCount();
for (;;)
{
fn();
vTaskDelayUntil(&lastWake, pdMS_TO_TICKS(periodMs));
}
}
定期実行をする処理は上記になります。初回実行時間を取得して、該当タスクを実行。その後にvTaskDelayUntil関数で指定時間のインターバルに到達するまでdelayする関数を利用しています。
ライブラリではこのインターバル時間の他にタスクの優先順位などを変更することができますが、変更するのであればこのライブラリではなく、今後紹介する上記のESP32TaskKitの利用をおすすめします。
まとめ
FreeRTOSのマルチタスクをかんたんに安全に利用したい場合に便利なライブラリを作ってみました。このライブラリを使ってみてマルチタスクの仕組みを理解できた場合にはFreeRTOSの理解も深まっていると思います。
examplesやドキュメントも作り込んでありますので、ぜひ一通り目を通して貰えればなと思います。個人的にはマルチタスクの入門ライブラリであり、もう少しカスタマイズが可能なESP32TaskKitライブラリへのステップとなるライブラリとして位置づけています。
あとは上記のQueueやMutexなどのタスク間通信や排他制御などよりFreeRTOSっぽい機能を利用することができるESP32SyncKitライブラリも作成してあります。
FreeRTOSは比較的シンプルな構造になっていますので、ライブラリを利用せずに直接呼び出してもいいとは思います。ただし最初はわかりにくいところがあるので、これらのライブラリを通してFreeRTOSの概要を学んでからの利用をおすすめしたいと思います。




コメント