Arduino – ESP32 のマルチタスク ( Dual Core ) を試す

記事公開日:2017年7月19日
最終修正日:2017年7月24日

スポンサーリンク

こんばんは。

今回は、とにかくスゴイです。
ようやく、趣味の電子工作でこんな時代がやってきました!!!

ESP32 ( ESP-WROOM-32 ) のマルチタスク ( Dual Core ) です。
ただし、Arduino core for ESP32 についてです。

ESP32 は CPU がデュアルコアになっているので、1つのCPUコアの動作中に別のCPUを同時に動かすことができる優れものです。
これを使ってしまうと、他のマイコンには移れないくらいの素晴らしさを感じてしまいます。

何度も言いますが、とにかくスゴイです!!!

電子工作でデュアルコアを自在に操作できるなんて、一昔前までは思っても見ませんでした。

前回の記事でも述べましたが、Arduino core for ESP32 ではマルチタスクを使う前提で Arduino 関数が組まれています。
FreeRTOS というリアルタイム OS で Arduino core プログラムが作られていたというわけです。

FreeRTOS についてはまだ勉強中で良く分かりませんが、Arduino core for ESP32 上ではその関数を直接呼ぶことができます。
ちょっと特殊なところもありますが、あるポイントを押さえておくと、それほど難解なこともなく使えます。

では、私なりに Arduino – ESP32 のマルチタスクを検証してみたので説明してみたいと思います。

なお、今回の記事を書くにあたって、以下のサイトもたいへん参考にさせていただきましたので、合わせてご参照ください。
KERIさんとgarretlabさんの記事は頻繁に参考にさせていただいております。
改めて感謝いたします。m(_ _)m

ESP32でデュアルコアを使おう!

マルチタスク・デュアルコアの実験(ESP-WROOM-32)

準備するもの

ESP-WROOM-32 ( ESP32 )開発ボード

スイッチサイエンスさんの以下のボードはお薦めです。
ESPr_Developer32_01

ESPr® Developer 32

このボードをレビューした以下の記事もご参照ください。

ESPr Developer 32 ( スイッチサイエンス製 ) を使ってみました

ESP32-DevKitC でも特に問題無く使えます。

Arduino IDE 等の設定

Arduino IDE は 1.8.3 を推奨します。

Arduino IDE で ESP32 開発ができるようにするための方法は以下の記事を参照してください。

Arduino core for the ESP32 のインストール方法

Arduino core for ESP32 でのマルチタスクの組み方

では、Arduino core for ESP32 のスケッチ上でFree RTOS ライブラリを使う方法を説明します。

メインloopとは別の、もう1つのタスクプログラムを組む方法は以下のような感じになります。

void タスク名(void *pvParameters){
  while(1){
    任意のプログラム
  }
}

void setup(){
  xTaskCreatePinnedToCore(
           タスク名,
           "タスク名",
           スタックメモリサイズ,
           NULL,
           タスク優先順位,
           タスクハンドルポインタ,
           Core ID
  );
}

void loop(){

}

これで、setup関数でメインloop とは別のタスクが生成され、メインloop と同時に 新タスクの while ループが実行されるわけです。

他のサイトで、xTaskCreate という関数でマルチタスクプログラムを組んでいるところがありますが、Arduino core for ESP32 の場合は、xTaskCreatePinnedToCore でないとダメです。

このことについては、GitHub のESP-IDFの以下のところでも明確に述べられています。

https://github.com/espressif/esp-idf/blob/master/components/freertos/readme_smp.txt

要するに、xTaskCreate は下位互換性のためのもので、1番目のコアのみ動作する関数です。
他のCPUコアを使うためには、xTaskCreatePinnedToCore を使うこと、というような記載がされています。
(英訳が間違えていたらスイマセン)

タスク名は好きな名前にすることができます。

スタックメモリサイズは、あまり大きくし過ぎないようにした方が賢明です。
因みに次の節で述べますが、メインループのデフォルトでは 4096 byte と、意外と小さいです。

タスク優先順位は 1~25 の範囲です。
大きい方が優先順位が高まります。
シングルコアで同時に実行するプログラムがある場合、優先順位が高い方が優先され、もう一方のプログラムは実行されないようです。

最大値が 25 という根拠は、FreeRTOSConfig.h ファイルに configMAX_PRIORITIESという定数で、それらしき定義があったためです。
このファイルは以下のフォルダにあります。(Windows10の場合)

C:\Users\User-Name\Documents\Arduino\hardware\espressif\esp32\tools\sdk\include\freertos\freertos

タスクハンドルポインタは、TaskHandle_t型の引数を定義して、そのアドレスを指定すれば良いです。
これについては、後述します。

CoreID は CPU のコア番号です。
ESP32 ( ESP-WROOM-32 )の場合は、デュアルコアなので、0 か 1 のどちらかになります。

そのタスク中のコア番号( Core ID ) を取得するためには以下の関数を使えば取得できます。

xPortGetCoreID()

また、そのタスクの優先順位を取得するためには、以下の関数を使います。

uxTaskPriorityGet( タスクハンドルポインタ )

Arduino core for ESP32 の メイン loop 関数のマルチタスク構成について

では、上記を踏まえて、Arduino core for ESP32 のメイン loop 関数の構成をテキストエディタで見てみましょう。

Windows10 の場合は以下のフォルダの main.cpp というファイルです。

C:\Users\User-Name\Documents\Arduino\hardware\espressif\esp32\cores\esp32

main.cpp

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "Arduino.h"

#if CONFIG_AUTOSTART_ARDUINO

#if CONFIG_FREERTOS_UNICORE
#define ARDUINO_RUNNING_CORE 0
#else
#define ARDUINO_RUNNING_CORE 1
#endif

void loopTask(void *pvParameters)
{
    setup();
    for(;;) {
        micros(); //update overflow
        loop();
    }
}

extern "C" void app_main()
{
    initArduino();
    xTaskCreatePinnedToCore(loopTask, "loopTask", 4096, NULL, 1, NULL, ARDUINO_RUNNING_CORE);
}

#endif

これを見ても分かる通り、FreeRTOS ライブラリがインクルードされていて、

xTaskCreatePinnedToCore

という FreeRTOS の関数でタスクが生成されて、loopTask という関数内で無限ループが構成されています。

7行目の CONFIG_FREERTOS_UNICORE はどこで定義されているか突き止められなかったのですが、値は常時 false となっています。
これは、Serial.print などでシリアルモニターに出力すると false になっていることがわかります。

ということは、UNICORE ではないということになって、デュアルコア対応ではないのかな? と疑問に思ってしまいますね。
これは、Twitter で つきしまみなつさんから教えてもらったのですが、CONFIG_FREERTOS_UNICORE の設定は ESP-IDF で設定できるそうで、Arduino-ESP32 では、IDF で設定したものを使っているようです。
( ESP-IDF についてはこちらの記事を参照してください )

そして、FreeRTOSConfig.h というファイルにこんな記述がありました。

/* ESP31 and ESP32 are dualcore processors. */
#ifndef CONFIG_FREERTOS_UNICORE
#define portNUM_PROCESSORS 2
#else 
#define portNUM_PROCESSORS 1
#endif

この、portNUM_PROCESSORS という定数をシリアルモニタに表示させると、2と表示されました。
これから恐らく、Arduino-ESP32 では常時デュアルコアで動いていると言えそうです。

また、スタックサイズはデフォルトで 4096 byteとなっています。
ちょっと小さいですよね。

今まで、HALT エラーになっていた場合は、このスタックサイズを超えてメモリーを使っていた可能性があります。
これを大きくすれば解消される場合がありますが、大きすぎると他のプログラムが動かなくなってしまうので、要注意です。

前回の記事で述べましたが、SSL の Webページから記事を取得する場合はこのスタックサイズが小さすぎるので変える必要があります。

タスク優先度は1となっていて、一番低い値です。
新たにタスクを生成する場合には1以上にすれば優先されます。

タスクハンドルは NULL となっています。
これについては良く分かりませんが、メインループは NULL と覚えておけばよいと思います。

CoreID は
CONFIG_FREERTOS_UNICORE = false
なので、
ARDUINO_RUNNING_CORE = 1
となります。
とすると、別のタスクをデュアルコアで動かしたい場合は 0 とすれば良いということになります。

ただ、まだよく調べていないのですが、ESP32 の場合は Core 0 で WiFi や Bluetooth などの制御用のプログラムが動いている可能性があります。
そう考えると、Core 0 に大量のユーザープログラムを入れてしまうと、うまく動作しない場合があります。
これは要注意したいところですね。

マルチタスク ( デュアルコア )スケッチを作る

では、実際にマルチタスク ( デュアルコア )のプログラムを作ってみたいと思います。

ここでは、三角関数のサイン波の値をArduino IDE のシリアルモニターに出力するプログラムを組んでみます。

なぜ、三角関数のサイン波にしたかというと、これを Arduino IDE のシリアルプロッタに出力すれば、滑らかな曲線が連続しているので、途中でプログラムに割込みが入れば、マルチタスクで動作していないことが容易に判別できると思ったからです。

シリアルプロッタは最近のバージョンで Arduino IDE に搭載された機能で、リアルタイムにグラフ化できるので、これを使わない手はありません。

シリアルプロッタで表示する方法は後の節で述べますが、まず、以下のスケッチを入力してみてください。
サイン波を見るというよりは、Core ID や、タスク優先度をシリアルモニタで確認するためのプログラムを動かしてみます。
ディレイ時間を多くして、数値を見やすくしています。

ここで注意していただきたいのは、マルチタスク中ではArduino標準の delay() や delayMicroseconds() 関数が使えないので、vTaskDelay という関数を使います。
FreeRTOS のマルチタスク中では、Tick という単位で時間をカウントしているようです。

Arduino の delay 関数は、vTaskDelay で作られていました。コメント欄でけりさんから教えていただきました。
ありがとうございます。m(_ _)m
float pi = 3.14159; //円周率
float radA = (2 * pi) / 360; //ラジアンを角度(0-360度)単位にする

int mainAngle = 0; //メインloop のサイン波の初期角度
float mainSin = 0; //メインloop のサイン波の値

portTickType Delay1000 = 1000 / portTICK_RATE_MS; //freeRTOS 用の遅延時間定義
TaskHandle_t th[2];

void Task1(void *pvParameters) {
  int task1Angle = 45; //Task1 のサイン波の初期角度
  float task1Sin = 0; //Task1 のサイン波の値
  
  while(1) {
    Serial.printf("Task1 coreID = %d, Task1 priority = %d, task1Sin = %F\r\n", xPortGetCoreID(), uxTaskPriorityGet(th[0]), task1Sin);
    task1Sin = float(sin(radA * task1Angle)) * 1000; //角度をラジアンに変換しsin値を得て、1000倍にして表示しやすいようにする
    task1Angle++;
    if(task1Angle >= 360) task1Angle = 0; //角度が360度になったらゼロ
    vTaskDelay(Delay1000); //freeRTOS用のディレイタイム実行
  }
}

void Task2(void *pvParameters) {
  int task2Angle = 90; //Task2 のサイン波の初期角度
  float task2Sin = 0; //Task2 のサイン波の値
  
  while(1) {
    Serial.printf("Task2 coreID = %d, Task2 priority = %d, task2Sin = %F\r\n", xPortGetCoreID(), uxTaskPriorityGet(th[1]), task2Sin);
    task2Sin = float(sin(radA * task2Angle)) * 1000; //角度をラジアンに変換しsin値を得て、1000倍にして表示しやすいようにする
    task2Angle++;
    if(task2Angle >= 360) task2Angle = 0; //角度が360度になったらゼロ
    vTaskDelay(Delay1000); //freeRTOS用のディレイタイム実行
  }
}

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

  xTaskCreatePinnedToCore(Task1,"Task1", 4096, NULL, 3, &th[0], 0); //Task1実行
  xTaskCreatePinnedToCore(Task2,"Task2", 4096, NULL, 5, &th[1], 1); //Task2実行
}

void loop() {
  Serial.printf("loop  coreID = %d, loop  priority = %d, mainSin = %F\r\n", xPortGetCoreID(), uxTaskPriorityGet(NULL), mainSin);
  mainSin = float(sin(radA * mainAngle)) * 1000; //角度をラジアンに変換しsin値を得て、1000倍にして表示しやすいようにする
  mainAngle++;
  if(mainAngle >= 360) mainAngle = 0; //角度が360度になったらゼロ
  delay(1000);
}

【解説】

前節を踏まえて、見ていただければ分かると思うのですが、一応説明します。

●2行目:
ここで、三角関数でいうラジアン 2π を 360°で割って、1°あたりのラジアン値を求めておきます。

●7行目:
FreeRTOS の portTickType 型で Delay1000 という引数を定義します。
先にも述べたように、マルチタスク中の関数内では、時間のカウンタは Tick という単位だそうですので、それをミリセコンド相当にするためには、portTICK_TATE_MS という定数で割ります。
ですから、
1000/ portTICK_TATE_MS
という値が 1000ms と等しい時間になります。
実は、Arduino-ESP32 の delay関数は vTaskDelay で作られています。

●8行目:
タスクハンドルを格納するためのものです。
TaskHandle_t 型の配列で定義しています。

●10-21行目:
一つ目のタスクです。
Task1 というのは好きな名前にすることができます。
*pvParameters は変更不可です。
11行目で、メインループのサイン波と異なって、初期角度を45°ずらしています。
14-20行で無限ループにしています。
15行目で、xPortGetCorID() で CPU core ID を取得できます。
そして、uxTaskPriorityGet にタスクハンドルポインタを代入すれば、そのタスクの優先順位を取得できます。
19行目で、vTaskDelay 関数を使い、Tick カウントを 1000ms 相当にしています。

●23-34行目:
2つ目のタスクです。
ここでは、サイン波を90°ずらしています。

●40-41行目:
セットアップ関数内で、タスクを生成します。
前々節で述べたように、xTaskCreatePinnedToCore 関数を使う必要があります。
スタックサイズはメインループと同じ 4096 としています。
優先順位は Task1 が 3、Task2 が 5 としました。
タスクハンドルはポインタなので、アドレスを渡せばOKです。
Task1 の CPU core は 0 とし、Task2 のcore は 1 としました。つまり、Task2 はメインloop と同じ core になります。

●44-50行目;
45行目の uxTaskPriorityGet では、メインループのタスクハンドルは NULL なのでそれを代入します。
メインループでは、delay関数をそのまま使えます。

以上です。

では、これをコンパイル実行して、シリアルモニタを 115200bps で起動してみてください。
こんな感じになります。

Multi_Task01

この、xPortGetCorID() という関数が正しいのであれば、デュアルコアで確実に動いていると言えます。

ただ、これから分かる通り、Task2 と メインloop の CPU core が 1 で同じになっています。
つまり、3つのタスクが同時に動くのか???
・・・って思ってしまいますよね。
同じ Core でもマルチタスクにできるのかという疑問が出てきます。

では、次にそれを検証してみます

シリアルプロッタのサイン波形で、マルチタスクを検証

では、シリアルプロッタに連続したサイン波形を表示させてみます。

下図の様なスケッチを入力してみてください。

float pi = 3.14159; //円周率
float radA = (2 * pi) / 360; //ラジアンを角度(0-360度)単位にする

int mainAngle = 0; //メインloop のサイン波の初期角度
float mainSin = 0; //メインloop のサイン波の値

portTickType Delay10 = 10 / portTICK_RATE_MS; //freeRTOS 用の遅延時間定義
TaskHandle_t th[2];

void Task1(void *pvParameters) {
  int task1Angle = 45; //Task1 のサイン波の初期角度
  float task1Sin = 0; //Task1 のサイン波の値
  
  while(1) {
    Serial.println(task1Sin);
    task1Sin = float(sin(radA * task1Angle)) * 1000; //角度をラジアンに変換しsin値を得て、1000倍にして表示しやすいようにする
    task1Angle++;
    if(task1Angle >= 360) task1Angle = 0; //角度が360度になったらゼロ
    vTaskDelay(Delay10); //freeRTOS用のディレイタイム実行
  }
}

void Task2(void *pvParameters) {
  int task2Angle = 90; //Task2 のサイン波の初期角度
  float task2Sin = 0; //Task2 のサイン波の値
  
  while(1) {
    Serial.println(task2Sin);
    task2Sin = float(sin(radA * task2Angle)) * 1000; //角度をラジアンに変換しsin値を得て、1000倍にして表示しやすいようにする
    task2Angle++;
    if(task2Angle >= 360) task2Angle = 0; //角度が360度になったらゼロ
    vTaskDelay(Delay10); //freeRTOS用のディレイタイム実行
  }
}

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

  xTaskCreatePinnedToCore(Task1,"Task1", 4096, NULL, 3, &th[0], 0); //Task1実行
  xTaskCreatePinnedToCore(Task2,"Task2", 4096, NULL, 5, &th[1], 1); //Task2実行
}

void loop() {
  Serial.println(mainSin);
  mainSin=float(sin(radA * mainAngle)) * 1000; //角度をラジアンに変換しsin値を得て、1000倍にして表示しやすいようにする
  mainAngle++;
  if(mainAngle >= 360) mainAngle = 0; //角度が360度になったらゼロ
  delay(10);
}

【解説】

先ほどのスケッチと変えたところは、以下の通りです。

●7行目:
10ms 相当にして、連続的な表示ができるようにしました。

●15行目:
Task1 のsin値だけをシリアルモニタに出力するように変更

●28行目:
Task2 のsin値だけをシリアルモニタに出力するように変更

●45行目:
メインloop の sin値だけをシリアルモニタに出力するように変更

●49行目:
delay を 10ms に変更

以上です。

では、コンパイル実行してみてください。
今度はシリアルモニタではなく、シリアルプロッタを起動します。

すると以下の動画のようになります。

シリアルプロッタはマルチタスクには対応していないので、別のタスクでシリアル出力したものが同じ色のグラフに融合されてしまっています。

でも、よく見ると、45°づつズレた波形がボヤ~ッと薄ら見えますね。

Multi_Task02

薄ら見えているところに線を引いてみるとこんな感じで見えています。

Multi_Task03

見事に45°ズレたサイン波形が永遠と連続して表示されているのがわかります。

プログラムを見ても分かる通り、Task1 とTask2 は Setup 関数内で実行されていて、loop中の時間差で出力されているわけではありませんので、完全にマルチタスクが実行されていると言えるのではないでしょうか?

しかも、3つ異なるタスクが同時です。

こりゃぁ、スゴイですよ!!!

・・・と思ったのですが、実は裏がありました!

実際に GPIO に電圧を出力するとどうなるのでしょうか?
次で見ていきます。

ESP32 のマルチタスク(デュアルコア)で GPIO を制御した場合の挙動

先の節では、シリアルプロッタやシリアルモニタに出力した場合、あたかも3つのタスクが同時に行われているかのように見えました。

では、実際に ESP32 ( ESP-WROOM-32 )のGPIO を制御したらどうなるのでしょうか?

単純な digitalWrite 関数を使って見てみたいと思います。

以下の簡単な普通のシングルコア用の通常のスケッチを入力してみてください。

void setup() {
  pinMode(4, OUTPUT);
}

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

digitalWrite 関数を使って、GPIO を HIGH ⇔ LOW で切り替える簡単なプログラムです。
これをオシロスコープで電圧波形を見てみるとこうなります。
使っているオシロは 100MHz のストレージオシロスコープです。

Multi_Task10

兼ねてから、digitalWrite 関数は速度が遅いということは分かっていましたが、これはあまりに酷いですね。
1MHzしか速度が出ていません。

digitalWrite で高速パルスを出して制御するようなことはあまり無いと思いますので良いのですが、これは頭に入れておいた方が良いですね。

では、今度はマルチタスクの以下のスケッチを入力してみてください。

TaskHandle_t th[2];

void Task1(void *pvParameters) {
  pinMode(4, OUTPUT);

  while(1) {
    digitalWrite(4, LOW);
    digitalWrite(4, HIGH);
  }
}

void Task2(void *pvParameters) {
  pinMode(5, OUTPUT);
  
  while(1) {
    digitalWrite(5, LOW);
    digitalWrite(5, HIGH);
  }
}

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

  pinMode(18, OUTPUT);

  xTaskCreatePinnedToCore(Task1,"Task1", 4096, NULL, 3, &th[0], 0); //Task1実行
  xTaskCreatePinnedToCore(Task2,"Task2", 4096, NULL, 5, &th[1], 1); //Task2実行
}

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

これは、前節のシリアルプロッタに出力したマルチタスクのCPUコア割り当てと同じにしてみました。

Task 1 は Core 0
Task 2 は Core 1
メイン loop は Core 1

です。
そこで、これをコンパイルする前に、Debug設定を以下のようにしてみて下さい。

Multi_Task15

そして、コンパイルして、シリアルモニターを起動してみてください。
すると、こんな感じのメッセージが表示されると思います。

Multi_Task14

これは、つまり、Task1 や Task2 の while ループで ウォッチドッグタイマが介入できないというメッセージです。
ウォッチドッグタイマが動作する余地を与えるためには、delay(1); を入れてやるか、main.cpp のようにmicros() 関数を入れてやる必要があります。
また、vTaskDelay(1) でもOKです。
ウォッチドッグタイマはマイコンを監視するプログラムなので、動作しないとプログラムが落ちますから、無視するわけにいきません。
※ESP8266 で活躍した yield() は、ESP32 のマルチタスクでは効きませんので要注意です。
ですが、今回はオシロで波形を見るだけなので、あえて無視します。

オシロ波形はこんな感じになります。

Multi_Task11

メインloop の digitalWrite 関数は全く効いておらず、パルスが出力されていませんね。
やはり、Task2 と メインloop が Core 1 でダブっているために、優先順位の低い メインloop はタスクが実行されていないようです。

また、Task 1 および Task 2 は while ループにより、3.3 MHz まで速度アップしました。
ということは、main.cpp ファイルにあるように、メインloop 関数には、micros() というオーバーフローを監視する関数が入っており、速度が遅くなっている原因のようです。

そして、Task1 と Task2 の波形は同時ではなく、少しズレていますね。
立ち上がりや立下りが一致するところはありませんので、おそらくGPIO レジスタ操作はマルチタスクとはいえ、同時に実行できないと思われます。

では、
Task1 は Core 0
Task2 は Core 0
メインloop は Core 1
の場合はこうなります。

Multi_Task12

今度は、Task 1 が実行されていません。
そして、相変わらず メインloop 関数の速度は遅いですね。

メインloop 以外のタスクでも、Core が同じ番号ならば、優先順位の高い方だけしか実行されませんね。

では、次はすべて同じ Core で、
Task1 → Core 1
Task2 → Core 1
メインloop → Core 1
としてみたらどうなるのでしょうか?

Multi_Task13

予想に反して、Task1 だけが実行され、Task2 は常時 HIGHレベルになっています。
なぜかタスク優先順位が効いていません。

普通、同じ Core ならば、タスク優先順位の高い、Task2 が実行されなければおかしいのですが・・・。
これは良く分かりません・・・。

実は、Twitter のつきしまみなつさんや、コメント投稿欄の yitabashi さんから情報を頂いたのですが、whileループにvTaskDelayがあれば、そのTaskが休止している間、他の優先順位の高いTaskが実行されるとのことです。
つまり、同じ Core 上の場合、メインloop の前に まず Task1 が実行され、vTaskDelay が無いので、次の Task2 が実行できる隙間が全くありません。当然、その後に実行されるメインloop が実行される余地もありません。
よって、Task1 だけが実行されたということです。
vTaskDelay(1)でも入れると、同じタスク優先順位ならば、交互にタスクが実行されるとのことです。
ということは、前節のサイン波シリアルプロッタの波形も、同じCore上のタスクはvTaskDelay の間に別のタスクが実行されているから、あたかも3つのタスクが動いているように見えたと言えそうです。
みなさん、実験してみてください。

いずれにせよ、これでマルチタスクの Core ID は別にするべき、ということがハッキリわかりました。

メインloop とマルチタスクを組む場合、タスクは 0 にすべきで、結局、GPIOを使う場合のタスクは2つまでということで考えておけばよいと思います。

余談

実は、この後、レジスタを直接叩いてマルチタスクを実行してみました。
シングルタスクでは 10MHz くらいは出たのですが、マルチタスクにすると 2.5MHz まで落ちてしまいました。
それは、同時にHIGH⇔LOW 切り替えしているわけではなく、時間差でHIGH⇔LOW となっているために、2.5MHz まで速度が落ちたと思われます。

これについてはまだ実験中で、正確なことは言えず、あくまで予想ですが、レジスタを直接叩くような GPIO の HIGH⇔LOW 切り替えのマルチタスクは基本的に不可能と思われ、実際の速度はシングルタスクと変わらないものと思われます。

これについては、改めて分かり次第ご報告したいと思います。

まとめ

以上、ザッと Arduino core for ESP32 マルチタスクの注意点をまとめると、

●Arduino-ESP32 のマルチタスクでは、xTaskCreatePinnedToCore を使うこと。
●Arduino core for ESP32は、予め FreeRTOS ライブラリがインクルードされていて、メインloop は Core 1 で、タスク優先度が1 、スタックサイズはデフォルトで 4096 byte となっている。
●マルチタスク中の時間カウントは、Tick という単位で、遅延させるためには vTaskDelay を使う。delay関数は vTaskDelay で作らているが、delayMicroseconds はエラーになりやすいのでお勧めできない。
●メインloop の速度は、オーバーフロー監視関数っぽいものが入っているため、 while ループより遅い。
●シリアルモニタやシリアルプロッタでは、3タスク以上可能なように見えるが、GPIO 出力は2つ分のタスクしか実行できない。つまり、CPU Core 数のみということ。
●同じ Core 上で別々のタスクを動かす場合、vTaskDelay の間に他の優先順位が高いタスクが実行される。
同じCore で同じタスク優先順位ならば、vTaskDelayの間にそれぞれ交互にタスクが実行される

と、こんな感じでしょうか。
私なりの視点で Arduino – ESP32 のマルチタスクを検証してみましたが、まだ FreeRTOS については取組み始めたばかりなので、間違えていたらコメント等でご連絡いただけると助かります。

でも、これでマルチタスクが少し理解できたように思います。

今、電光掲示板で4つの記事をスクロールさせながら、別の Core で Web GET して記事を更新するということができています。
Web から GET リクエストして記事を取得していても、文字スクロールが停止しないので、改めて ESP32 のポテンシャルの凄さを実感しています。

次回こそはこれを紹介していこうと思います。

ではまた・・・

mgo-tec電子工作 関連コンテンツ ( 広告含む )

スポンサーリンク

Amazon.co.jp 当ブログのおすすめ







投稿者:

mgo-tec

Arduino , ESP32 ( ESP-WROOM-32 ) , ESP8266 ( ESP-WROOM-02 )等を使って、主にスマホと連携した電子工作やプログラミング記事を書いてます。ライブラリも作ったりしてます。趣味、独学でやってますので、動作保証はしません。 電子回路やプログラミングの専門家ではありません。 畑違いの仕事をしてます。 でも、少しだけ電気の知識が必要な仕事なので、電気工事士や工事担任者等の資格は持ってます。

「Arduino – ESP32 のマルチタスク ( Dual Core ) を試す」への5件のフィードバック

    1. けりさん

      コメントいただき、感謝いたします。
      あ、そうなんですね。
      delay() はあまり深くチェックしてなかったので、助かります。
      早速記事を修正したいと思います。
      そういえば、当方でも delay() はあまりエラーが出なかったので、不思議に思っておりました。
      けりさんの記事を参考にさせていただきながら、情報もいただき、感謝感謝です。
      ありがとうございました。
      m(_ _)m

    2. 今、確認しました。
      確かに、esp32-hal-misc.c に、delay() は vTaskDelay で作られていました。
      改めて、情報ありがとうございました。
      m(_ _)m

  1. 検証していないのですが、「予想に反して、Task1 だけが実行され、Task2 は常時 HIGHレベルになっています。なぜかタスク優先順位が効いていません。」の部分は、Task1がTask2より先に実行され、Task1中の無限ループのままOS側に制御を戻していないからだと思われます。
    while(1){}中にvTaskDelay(Delay1);を入れて、一度OS側に処理を明け渡してみてはどうでしょうか。

    1. yitabashi さん

      記事をご覧いただきありがとうございます。

      おっしゃる通りでした。
      Twitter でも つきしまみなつさんが検証されていて、vTaskDelay の間に優先度の低い他の Task が実行されているとの報告がありました。
      まだまだ FreeRTOS は勉強不足なのですが、 vTaskDelay の使い方は何となく分かって来ました。
      記事も修正しようと思います。
      情報ありがとうございました。
      m(_ _)m

コメントを残す

メールアドレスが公開されることはありません。

*画像の文字を入力してください。(スパム防止の為)