ESP32 RTOS ラウンドロビンに失敗する(Arduino)

ESP32 RTOS ラウンドロビンに失敗する(Arduino)

今回も Arduino のフレームワークで RTOS を使います。
今回はラウンドロビンの動作を確認してみます。

この記事は JTAG でデバッグすることを前提にして書いています。
環境構築については こちら をご覧になってください。

投稿時の開発環境を記しておきます。

PC:
Windows10 OS

開発ボード :
ESP32-DevKitCーVE
(Soc : ESP32-D0WD-V3)

デバッガー(H/W):
FT2232D

デバッガー (S/W) :
Visual Studio Code + PlatformIO + Arduino Framework

オシロスコープ : 波形確認用

今回の環境

いつものパターンで環境を書いてしまいましたが、今回JTAGデバッガーはさほど使いません。
その代わりにタスクの動作をオシロスコープで確認することにします。

同時に観測するために、できれば3チャンネルあると良いです。

なければ2チャンネルを使って時分割で観測してください。

FreeRTOS

こちら が本家ですが、ESP32用としては このへん を見るのが良さそうです。

プロジェクトをつくる

VSCodeで、もし使っていたプロジェクトを開いていたら、File – Close Folder して閉じておきます。

その後にVSCodeからPlatformIOをOpenします。

以下の内容でプロジェクトを新規に作成します。

Name : ESP32A-RTOS-RoundRobin
Board : Espressif ESP32 Dev Module
Framework : Arduino Framework

Name : ESP32A の “A” は Framework (Arduino Framework)の頭文字を示しています。

(後から見てわかるように、Arduinoを使うことを明示しています)

platformio.ini に以下の4行を追加して、 Ctrl + s で保存しておきます。

COM[8]の8の部分はデバイスマネージャーのポート(COMとLPT)で Silicon Labs CP210x から始まるCOMの番号を記述します。

upload_port はプログラムをアップロードする時に使うCOMポートを指定します。
monitor_port は printf()出力をモニターするCOMポートを指定します。

debug_tool = minimodule
upload_port = COM[8]
monitor_port = COM[8]
monitor_speed = 115200

ラウンドロビンとは

今回はタスク切り替えの中でも脇役のラウンドロビン方式に着目してみました。
ラウンドロビンはRTOSで決められた時間分だけ処理を実行すると次のタスクに処理が切り替わる方式です。
この時間をタイムスライスと言い、Arduinoでは1msecです。

動作確認の結果からわかった仕様ですが、同一のコアで優先順位に差があると低い方のタスクは実行されません。

お断りとして今回の実験では、vTaskDelay()などの処理を委譲するAPIは使いません。

ですからタスクの切り替えはタイムスライスによってのみ行われることに注意してください。

ESP32とコア

開発ボード ESP32-DevKitCーVE に搭載されている Soc ESP32-D0WD-V3 は Dual Core(デュアルコア) です。
Soc のデータシートの表23を載せておきます。

デュアルコアの場合それぞれを、コア0、コア1と呼んでいます。
そして無線用にコア0が使われるようになっています。

無線に負荷をかけないようにコア1を使うのが一般的かも知れません。

RTOSは時分割で処理を分けて実行し並行処理しているようにみせかけていますが、デュアルコアはそれぞれの処理が同時に動きます。

Arduinoとコアとタスク

ビルドしてエラーがないことを確認して、メニューから Run – Start Debugging (またはF5キー)すると少ししてから

loopTaskWDTEnabled = false; の行で停止します。

その2行下に以下の関数があります。

xTaskCreateUniversal(loopTask, "loopTask", getArduinoLoopTaskStackSize(), NULL, 1, &loopTaskHandle, ARDUINO_RUNNING_CORE);

この関数の中に if() があり、コアを0 または1に指定する場合には xTaskCreatePinnedToCore() が呼ばれます。

これはコアを指定してタスクを作る関数でしたね。
指定するコアは ARDUINO_RUNNING_CORE(==1) です。

ということで loopTask()というタスクがコア1で動くことになります。

loopTask()をダブルクリックして F12キーを押すと関数の中を見ることができます。

void loopTask(void *pvParameters)
{
    setup();
    for(;;) {
        if(loopTaskWDTEnabled){
            esp_task_wdt_reset();
        }
        loop();
        if (serialEventRun) serialEventRun();
    }
}

Arduinoでお馴染みの setup() と loop() はこの中にありました。

setup(), loop()はコア1で実行されていることを覚えておいてください。

コア0にタスクを2つつくる

まずコア0で優先順位2のタスクを2つつくって動作確認してみました。
setup()でタスクをつくるので、タスクをつくる場所はコア1になります。

タスク コア 優先順位 タスクをつくる場所 先につくるタスク GPIO出力
Task0 0 2 コア1 出る
Task1 0 2 コア1 出る
loop 1 1 コア1 出る

main.cppのソースコードは以下の通りです。
タスクの中でGPIO出力ポートに LOW, HIGH を繰り返し出力しているので、そのポートがパタパタしていれば対応するタスクが動いていると判断できます。
PRO_CPU_NUM がコア0の指定です。

#include <Arduino.h>

TaskHandle_t taskHandle[4];

#define IO25 25
#define IO26 26
#define IO27 27

void Task0(void *args) {

  while (true) {
    digitalWrite(IO25, LOW);
    digitalWrite(IO25, HIGH);
  }
}

void Task1(void *args) {

  while (true) {
    digitalWrite(IO26, LOW);
    digitalWrite(IO26, HIGH);
  }
}

void setup() {

  pinMode(IO25, OUTPUT);
  pinMode(IO26, OUTPUT);
  pinMode(IO27, OUTPUT);

  xTaskCreatePinnedToCore(Task0, "Task0", 4096, NULL, 2, &taskHandle[0], PRO_CPU_NUM);
  xTaskCreatePinnedToCore(Task1, "Task1", 4096, NULL, 2, &taskHandle[1], PRO_CPU_NUM);
}

void loop() {
    digitalWrite(IO27, LOW);
    digitalWrite(IO27, HIGH);
}

写真を貼っておきます。
上から Task0, Task1, loop です。
それぞれ GPIO25, GPIO26, GPIO27 を観測しています。

タスク切り替えは1msec周期で行われています。
コア0とコア1が同時に動いていること(デュアル コアであること)が確認できます。

コア1に優先順位1のタスクを2つつくる

次にコア1に優先順位1のタスクを2つつくって動作確認してみました。
setup()でタスクをつくるので、タスクをつくる場所はコア1になります。
問題なく動作しました。

タスク コア 優先順位 タスクをつくる場所 先につくるタスク GPIO出力
Task0 1 1 コア1 出る
Task1 1 1 コア1 出る
loop 1 1 コア1 出る

main.cppのソースコードは以下の通りです。
APP_CPU_NUM がコア1の指定です。

#include <Arduino.h>

TaskHandle_t taskHandle[4];

#define IO25 25
#define IO26 26
#define IO27 27

void Task0(void *args) {

  while (true) {
    digitalWrite(IO25, LOW);
    digitalWrite(IO25, HIGH);
  }
}

void Task1(void *args) {

  while (true) {
    digitalWrite(IO26, LOW);
    digitalWrite(IO26, HIGH);
  }
}

void setup() {

  pinMode(IO25, OUTPUT);
  pinMode(IO26, OUTPUT);
  pinMode(IO27, OUTPUT);

  xTaskCreatePinnedToCore(Task0, "Task0", 4096, NULL, 1, &taskHandle[0], APP_CPU_NUM);
  xTaskCreatePinnedToCore(Task1, "Task1", 4096, NULL, 1, &taskHandle[1], APP_CPU_NUM);
}

void loop() {
    digitalWrite(IO27, LOW);
    digitalWrite(IO27, HIGH);
}

上から Task0, Task1, loop です。

コアも優先順位も同じなので、きれいにタイムスライスされています。

コア1に優先順位2のタスクを2つつくる (1)

次にコア1に優先順位2のタスクを2つつくって動作確認してみました。
setup()でタスクをつくるので、タスクをつくる場所はコア1になります。
こちらがうまく動きませんでした。

タスク コア 優先順位 タスクをつくる場所 先につくるタスク GPIO出力
Task0 1 2 コア1 出る
Task1 1 2 コア1 出ない
loop 1 1 コア1 出ない

main.cppのソースコードは以下の通りです。
APP_CPU_NUM がコア1の指定です。

#include <Arduino.h>

TaskHandle_t taskHandle[4];

#define IO25 25
#define IO26 26
#define IO27 27

void Task0(void *args) {

  while (true) {
    digitalWrite(IO25, LOW);
    digitalWrite(IO25, HIGH);
  }
}

void Task1(void *args) {

  while (true) {
    digitalWrite(IO26, LOW);
    digitalWrite(IO26, HIGH);
  }
}

void setup() {

  pinMode(IO25, OUTPUT);
  pinMode(IO26, OUTPUT);
  pinMode(IO27, OUTPUT);

  xTaskCreatePinnedToCore(Task0, "Task0", 4096, NULL, 2, &taskHandle[0], APP_CPU_NUM); // (1)
  xTaskCreatePinnedToCore(Task1, "Task1", 4096, NULL, 2, &taskHandle[1], APP_CPU_NUM); // (2)
}

void loop() {
    digitalWrite(IO27, LOW);
    digitalWrite(IO27, HIGH);
}

上から Task0, Task1, loop です。

Task0しが動いていません。(ラウンドロビン失敗の図)
私はさっぱり理由がわからず悩んでいたのですが、有識者の方から次のアドバイスを頂きました。

setup()の中の (1) を実行すると優先順位2のTask0が動き始める。
そうすると setup()自身は優先順位1だから(2)のコードが実行されない。

そこで、setup()の中でタスクをつくるためのタスクをコア0指定でつくってみたらどうか?
解決策を次に示します。

コア1に優先順位2のタスクを2つつくる (2)

次にコア1に優先順位2のタスクを2つつくって動作確認してみました。
タスクをつくる場所はコア0になります。
タスクをつくる場所をコア0にするための CreateTasks をコア0指定でつくります。

タスク コア 優先順位 タスクをつくる場所 先につくるタスク GPIO出力
Task0 1 2 コア0 出る
Task1 1 2 コア0 出る
loop 1 1 コア1 出ない

main.cppのソースコードは以下の通りです。
APP_CPU_NUM がコア1の指定です。

#include <Arduino.h>

TaskHandle_t taskHandle[4];

#define IO25 25
#define IO26 26
#define IO27 27

void Task0(void *args) {

  while (true) {
    digitalWrite(IO25, LOW);
    digitalWrite(IO25, HIGH);
  }
}

void Task1(void *args) {

  while (true) {
    digitalWrite(IO26, LOW);
    digitalWrite(IO26, HIGH);
  }
}

void CreateTasks(void *args) {

  xTaskCreatePinnedToCore(Task0, "Task0", 4096, NULL, 2, &taskHandle[0], APP_CPU_NUM);
  xTaskCreatePinnedToCore(Task1, "Task1", 4096, NULL, 2, &taskHandle[1], APP_CPU_NUM);

  while (true) {
    ;
  }
}

void setup() {

  pinMode(IO25, OUTPUT);
  pinMode(IO26, OUTPUT);
  pinMode(IO27, OUTPUT);

  xTaskCreatePinnedToCore(CreateTasks, "CreateTasks", 4096, NULL, 2, &taskHandle[0], PRO_CPU_NUM);
  
}
void loop() {
    digitalWrite(IO27, LOW);
    digitalWrite(IO27, HIGH);
}

上から Task0, Task1, loop です。

す、素晴らしい。
Task0, Task1 がラウンドロビンで動作しています。
loopが動かないのは仕様なのかも知れません。

優先順位を上げた場合でも Task1 を動かすことができたので満足しています。

通常は vTaskDelay()等の委譲APIを使われると思うので、さほど気にならない事象かも知れませんがコアのつくり方によっても挙動がかわることに関して大きな発見がありました。

アドバイスをくださった皆さま、ありがとうございました。

RoundRobinカテゴリの最新記事