ESP32用ヘルパーライブラリ その2 タスク

概要

前回はシリアルモニター経由でリセットなどがかんたんに操作できるラッパークラスを紹介しました。今回はマルチタスク操作になります。

ESP32のコア構成

ESP32はSOLOというシングルコアのものを除いて、2コアあります。ただしESP32-C系は1コアのみなので注意してください。

コアとコアは独立して動いており、Core0をPRO_CPU、Core1をAPP_CPUと呼んでいます。個別のコアの中で優先順位の高いタスクから順に動くマルチタスクが可能です。どちらのコアで処理を動かすのかはタスクを起動するときに指定する必要があり、あとで変更することはできません。開いている方のコアで実行することなどはできず、指定したほうでのみ動くので注意してください。

マルチタスクの方式

PRO_CPUとAPP_CPUは分離して動いていますので、同時に2つの処理を実行可能です。それ以上のタスクを実行する場合には、1msの時間分割でのタスク起動になります。

上記のようにコア単位で1ms単位にタスクを切り替えながらマルチタスク動作をしていきます。

ルール1 他にタスクがなければ専有できる

左側のPRO_CPUは優先度0と一番低いですが、他のタスクがないので専有することができます。

ルール2 同じ優先度の場合には順番に実行

右側のAPP_CPUは優先度1のloop()タスクがありますので、同じく優先度1でtask2()を実行した場合には同じ優先度を順番に実行します。この場合、実行途中の処理が中断され、次のタスクが実行されます。そしてまた元のタスクに戻ってくるなどの処理がFreeRTOSによって裏側で自動的に動いています。利用者はあまりタスクの切り替えを意識する必要はありません。

ルール3 delay()をしない限り低い優先度タスクは実行されない

優先度2task3()タスクdelay()実行中以外は最優先で動く
優先度1loop()タスクtask3()タスクのdelay()中にのみ動く
優先度1task2()タスクtask3()タスクのdelay()中にのみ動く

上記のように優先度が違うタスクがあった場合、優先度が高いタスクがdelay()を実行して、低いタスクに実行権を与えないかぎり、優先度の低いloop()タスクとtask2()タスクは実行されません。

delay(1)などと、1以上の数字で実行することにより、より低い優先順位のタスクに実行権を譲るという効果があります。

優先順位のおさらい

PRO_CPUは無線系のタスクが優先順位高めで処理されています。そのため無線利用時には優先度24以外のタスクを実行していても無線系タスクに実行権を握られていて、実行されません。無線系は3秒程度処理が戻ってこないこともあるので注意して利用しましょう。とくにESP32の公式タイマークラスであるTickerがPRO_CPUで動くので、無線利用時には精度がかなり落ちます。

APP_CPUはloop()が優先度1で動いています。そのため、loop()より優先度が高いのか低いのかを考えて優先度を考える必要があります。基本的には少し高めの優先度にして、適度にdelay()を呼び出してloop()も実行される設定が使いやすいと思います。

基本的にはコア別に優先順位が高いものから実行していき、delay()を呼び出さないかぎり、下位の優先順位のタスクは呼び出されないことを気をつけてください。

あと一定以上delay()が呼ばれないとハングアップしたと認識して、リセットがかかるウォッチドッグタイマという機能があります。loop()が動いているAPP_CPUでデフォルトで無効になっていますが、PRO_CPUの場合には適度にdelay()を呼び出さないと自動リセットがかかるので注意しましょう。

EspEasyTaskの使い方

さて、実際にマルチタスクを実行する方法になります。loop()関数は約1秒間隔、loopTask()は約0.5秒間隔で処理を実行するプログラムが出来上がりました。

#include "EspEasyTask.h"

void loopTask() {
  uint32_t count = 0;
  while (1) {
    Serial.printf("loopTask count = %d\n", count);
    count++;
    delay(1000);
  }
}

EspEasyTask task;

void setup() {
  Serial.begin(115200);
  delay(500);
  Serial.println("Task test");

  task.begin(loopTask, 2, APP_CPU_NUM);
}

void loop() {

  Serial.println("loop()");
  delay(1000);
}

最低限のコードになります。

  task.begin(loopTask, 2, APP_CPU_NUM);

上記にてloopTask関数を優先度2でAPP_CPU_NUMで動作させる処理となります。

void loopTask() {
  uint32_t count = 0;
  while (1) {
    Serial.printf("loopTask count = %d\n", count);
    count++;
    delay(1000);
  }
}

上記が実際に実行されるタスク関数になります。全体をwhileなどで無限ループにさせます。無限ループにする必要はないのですが、その場合には1度だけ別タスクで実行して終了するワンショットのタスクになります。

中身はloop()関数などと同じで、適度にdelay()を呼び出してください。最低でもdelay(1)をループの最後に置くことで、より低い優先順位のタスクに優先権を譲ることができます。

まとめ

実際のラッパークラスの中をみてみると内部的な理解がすすむと思います。

https://github.com/tanakamasayuki/EspEasyUtils/blob/master/src/EspEasyTask.h

中身は結構シンプルな処理になっています。ただし、このままだと複数のタスク間で変数の受け渡しとかをするときに問題が発生する可能性があります。そこで次回はキューのラッパークラスを紹介したいと思います。

コメント