ESP32用タスク管理ライブラリ ESP32TaskKit

概要

上記のESP32AutoTaskライブラリはvoid LoopCore1_Normal()などの決められた関数で定義することでタスクをかんたんに動かすことができるライブラリです。

ESP32TaskKitはもう少しFreeRTOSのタスクで必要なパラメーターを自由に設定しながらタスクを利用できるライブラリとなっています。

FreeRTOSのタスクとは?

FreeRTOSだとESP32AutoTaskで設定可能なコアと優先度以外にもいろいろな設定が可能になっています。ESP32TaskKitは細かいパラメーターを指定しつつ、サンプルのスケッチ例を見ることでFreeRTOSのタスクをより理解できるようにと考えて作ったライブラリとなります。

01_BasicLoop.ino

  worker.startLoop(
      []()
      {
        Serial.printf("[+%lu ms] Hello from ESP32TaskKit\n", millis());
        return true;
      },
      cfg,
      1000);

一番基礎的な呼び出しです。無名関数を利用して定義していますが、startLoopでタスクを作成した場合にはこの関数が定期的に呼び出されます。最後のパラメーターが1000なので1秒(1000ms)間隔で呼び出されることになります。最後に継続実行をするかをreturnで返却しています。

02_CStyleTask.ino

void WorkerTask(void *pv)
{
  for (;;)
  {
    Serial.printf("[+%lu ms] C-style task running\n", millis());
    delay(1000);
  }
}

void setup()
{
  Serial.begin(115200);
  ESP32TaskKit::TaskConfig cfg;
  worker.start(&WorkerTask, nullptr, cfg);
}

もう少しFreeRTOSのタスクに近いスタイルです。本来はタスクを登録してからループを実行するのは自前となります。通常は無限ループを利用して実行を行います。この際にdelay(1000)で間隔を制御するとSerial.printf()の時間と1000msの処理時間になりますので、徐々にズレていく可能性があります。

実際のサンプルはSerial.printf()の文字数をわざと増やして時間を伸ばしていますので、ズレていくのが確認できると思います。

03_RequestStop.ino

void loop()
{
  if (worker.isRunning() && millis() - startedAt > 5000)
  {
    Serial.println("requestStop()");
    worker.requestStop();
  }

  Serial.printf("[+%lu ms] isRunning=%d isStopRequested=%d\n", millis(), worker.isRunning(), worker.isStopRequested());
  delay(500);
}

タスクは外部から停止や再開することが可能です。実際のところあまり停止や再開を利用するのはおすすめしていません。常に動いていて、キューなどを利用して処理待ちをする形が好ましいと思います。

04_RequestStopCStyle.inoもほぼ同じサンプルなのでスキップ。

05_TwoTasks.ino

  fastTask.startLoop(
      []()
      {
        Serial.printf("[+%lu ms] fast\n", millis());
        return true;
      },
      cfgFast,
      200);

  slowTask.startLoop(
      []()
      {
        Serial.printf("[+%lu ms] slow\n", millis());
        return true;
      },
      cfgSlow,
      1000);

複数タスクを利用する例です。たんに複数定義すれば良いだけになります。

06_CustomName.ino

  ESP32TaskKit::TaskConfig cfg;
  cfg.name = "CustomName";

  autoTask.startLoop(
      []()
      {
        Serial.printf("[+%lu ms] name=%s\n", millis(), pcTaskGetName(nullptr));
        return true;
      },
      cfg,
      800);

TaskConfigにてタスクの設定を変更可能です。名前は実際のところデバッグ用のタスク名でしか利用しないので、何を設定していても構いません。デフォルトは連番の名前になっていますので、複数タスクを利用して、デバッグで不便になったときに設定するぐらいで問題ありません。

07_InlineConfig.ino

  inlineCfgTask.startLoop(
      []()
      {
        Serial.printf("[+%lu ms] inline cfg task\n", millis());
        return true;
      },
      []()
      {
        ESP32TaskKit::TaskConfig cfg;
        cfg.name = "InlineCustom";
        cfg.stackSize = 2048;
        cfg.priority = 0;
        cfg.core = 0;
        return cfg;
      }(),
      700);

インラインで設定する場合です。TaskConfigを直接定義してもいいのですが、関数化することで項目が増えても自由な順番で設定が可能となります。

08_TaskState.ino

name=ESP32TaskKit#1
taskNumber=0
state=Blocked
priority=2
stackHighWaterMark(words)=6684
coreid=NO_AFFINITY
info.name=ESP32TaskKit#1
info.taskNumber=10
info.state=Blocked
info.currentPriority=2
info.basePriority=2
info.runTimeCounter=17125
info.pxStackBase=0x3ffb2524
info.stackHighWaterMark(words)=6684
info.coreID=NO_AFFINITY
isRunning=1

タスクの状態を出力するサンプルです。ESP32TaskKitはほぼ関係なく、FreeRTOSのタスクの状況取得関数を呼び出しています。

ちょっと情報取得系はFreeRTOS標準関数が使えないものが何個かあったり、クセがあるので注意してください。

09_TaskList.ino

---- vTaskList ----
tasks=8
Name            State   Prio    Stack   Num     Core
loopTask        X       1       4640    8       1
IDLE0           R       0       568     5       0
IDLE1           R       0       580     6       1
ESP32TaskKit#1  B       2       6684    10      -1
Tmr Svc         B       1       3600    7       -1
ipc1            S       24      476     2       1
ipc0            S       24      484     1       0
esp_timer       S       22      8160    3       0

vTaskList関数を利用して、タスク一覧を取得しています。ただし、結果が文字列で戻って来るので注意が必要です。バッファ用の文字列は大きめで確保したほうが安全です。

標準で動いているタスクも一覧で取得可能です。Wi-Fiなども利用するとタスクが増えていきます。

10_TaskStatusArray.ino

tasks=8
loopTask state=Running basePrio=1 stackHWM=6156 core=1 runtime=452643
IDLE0 state=Ready basePrio=0 stackHWM=568 core=0 runtime=2959655
IDLE1 state=Ready basePrio=0 stackHWM=580 core=1 runtime=2521966
ESP32TaskKit#1 state=Blocked basePrio=2 stackHWM=6684 core=2147483647 runtime=128
esp_timer state=Suspended basePrio=22 stackHWM=8160 core=0 runtime=22
Tmr Svc state=Blocked basePrio=1 stackHWM=3600 core=2147483647 runtime=21
ipc1 state=Suspended basePrio=24 stackHWM=476 core=1 runtime=26947
ipc0 state=Suspended basePrio=24 stackHWM=484 core=0 runtime=17497

タスク一覧を構造体で取得した場合のサンプルです。細かい情報を取得する場合にはこちらがおすすめですが、通常は文字列のvTaskList関数で問題ないはずです。

11_RunTimeStats.ino

---- vTaskGetRunTimeStats ----
Name            Time            Percentage
loopTask        452883          15%
IDLE0           2959641         98%
IDLE1           2521683         83%
ESP32TaskKit#1  137             <1%
esp_timer       19              <1%
Tmr Svc         21              <1%
ipc1            27009           <1%
ipc0            17531           <1%

Percentageで各タスクがどれぐらいCPUを利用しているのかがわかります。古いバージョンだとArduino側でこのへんの情報が取れなかったので嬉しい機能ですね。ただ、この情報だとコアがわからないので、コアごとに分類する必要があります。IDLE0がコア0での空きCPUなので、あまり使われていなく、IDLE1がコア1の空きCPUで83%ということは17%近く使われているのがわかります。なのでloopTaskはコア1で動いていますね。このへんはvTaskListでコアを確認すれば正解がわかると思います。

12_TaskSystemState.ino

---- uxTaskGetSystemState ----
tasks=8
loopTask state=Running basePrio=1 stackHWM=6156 core=1 runtime=458812
IDLE0 state=Ready basePrio=0 stackHWM=568 core=0 runtime=5962447
IDLE1 state=Ready basePrio=0 stackHWM=580 core=1 runtime=5518775
ESP32TaskKit#1 state=Blocked basePrio=2 stackHWM=6684 core=2147483647 runtime=327
Tmr Svc state=Blocked basePrio=1 stackHWM=3600 core=2147483647 runtime=22
ipc1 state=Suspended basePrio=24 stackHWM=476 core=1 runtime=26982
ipc0 state=Suspended basePrio=24 stackHWM=484 core=0 runtime=17521
esp_timer state=Suspended basePrio=22 stackHWM=8160 core=0 runtime=20

配列で情報取得した場合です。こちらの場合にはruntimeにCPUが使った時間が保存されていますので自分で割合を計算する必要があります。ただしこの値は累計なので、定期的に取得して差分で計算することでvTaskGetRunTimeStatsと同じ値が求めることが可能です。

13_LoopWithArgs.ino

bool blinkOnce(uint8_t pin)
{
  int current = digitalRead(pin);
  int next = current == HIGH ? LOW : HIGH;
  digitalWrite(pin, next);
  Serial.printf("[+%lu ms] GPIO %u -> %s\n", millis(), pin, next == HIGH ? "HIGH" : "LOW");
  return true; // en: continue / ja: 継続
}

void setup()
{
  Serial.begin(115200);
  ESP32TaskKit::TaskConfig cfg;
  blinkTask1.startLoop(
      []()
      {
        return blinkOnce(LED_PIN_1);
      },
      cfg,
      300);
}

タスクから呼び出す関数に引数を追加したい場合です。複数のピンに対して同じような処理を実行したい場合には上記のように呼び出してください。タスク側の引数を利用するよりは、無名関数で自分で呼び出したほうが楽です。

14_CStyleArgs.ino

struct BlinkArgs
{
  uint8_t pin;
  TickType_t periodMs;
};

BlinkArgs blinkArgs1{LED_PIN_1, 300};

void BlinkTask(void *pv)
{
  BlinkArgs *args = static_cast<BlinkArgs *>(pv);
  TickType_t lastWake = xTaskGetTickCount();

  for (;;)
  {
    int current = digitalRead(args->pin);
    int next = current == HIGH ? LOW : HIGH;
    digitalWrite(args->pin, next);
    Serial.printf("[+%lu ms] GPIO %u -> %s\n", millis(), args->pin, next == HIGH ? "HIGH" : "LOW");

    vTaskDelayUntil(&lastWake, pdMS_TO_TICKS(args->periodMs));
  }
}

void setup()
{
  Serial.begin(115200);

  ESP32TaskKit::TaskConfig cfg;
  blinkTask1.start(&BlinkTask, &blinkArgs1, cfg);
}

FreeRTOS本来の引数の渡し方です。関数登録はシンプルになりましたが、*void型で引数を渡すので利用するときにキャストする必要があります。型チェックとかができないので、あまりおすすめはしません。

また、loop内のdelayはvTaskDelayUntilを利用していますので、処理時間にかかわらず定期的な実行ができるようになっています。

ここまでいくとFreeRTOSの関数を直接呼び出すのとほぼ変わらない形となります。

Task::start

        BaseType_t rc = xTaskCreatePinnedToCore(
            &Task::taskEntry,
            name,
            cfg.stackSize,
            ctx,
            cfg.priority,
            &_handle,
            cfg.core);

いろいろ引数チェックとかをしていますが、上記の関数でタスクを登録しているだけになります。taskEntryが登録する関数、nameが登録名、stackSizeがtaskEntryで利用できるスタックサイズ、ctxが引数、priorityが優先度、_handleが登録したタスクハンドル、coreが登録するコアになります。

スタックサイズだけがちょっと特殊でして、タスク内部で利用できるスタックメモリ量となります。ループなどの内部で利用する変数などで利用されており、足りなくなると動かないので少し大きめのサイズを指定することが多いですが、大きすぎても未使用の部分は無駄になるだけです。

vTaskListのStackや、stackHighWaterMarkなどの関数を利用して、タスク内でどれだけのメモリを利用しているかを確認することで微調整が可能です。ライブラリのデフォルトはArduinoと同じ8Kに設定しているので、ちょっと大きめになります。

まとめ

タイマー的に実行するタスクはこのライブラリのstartLoopを利用して、定期実行するのが好ましいと思います。まずはタスクをこのライブラリで理解してから、FreeRTOSのタスクを学ぶとより深く理解できると思います。

ただし、タスク単体で利用することもあるのですが、深く利用しようと思ったらキューや排他制御などの他の仕組みが必要となります。FreeRTOSでも用意されていますので、ESP32SyncKitライブラリとタスク系ライブラリを組み合わせての使い方を今後紹介していきたいと思います。

コメント