#やること
ESP32DevKitCとArduinoIDEを使って、ESP32のマルチコア処理を試します。
マルチスレッドとマルチコアの違いについてはこちらにまとめました。
今回はイメージとしては図の右上になり、コアを2つ使いますが、プログラムで使うスレッドは1つずつという感じになります。
#参考URL
mgo-tec電子工作 Arduino – ESP32 のマルチタスク ( Dual Core ) を試す
が大変参考になりました。ありがとうございました。
#準備
- Arduino IDEの入ったPC
- Arduino IDEでESP32が使えるようにしてある
- ESP32DevKitC
最小限のスケッチ
なるべく最小限の構成でマルチコアを試してみます。
TaskHandle_t thp[1];//マルチスレッドのタスクハンドル格納用
long a = 0;
void setup() {
Serial.begin(115200);
xTaskCreatePinnedToCore(Core0a, "Core0a", 4096, NULL, 3, &thp[0], 0);
}
void loop() {
Serial.println(a);//メインで実行するプログラム
delay(100);
}
void Core0a(void *args) {
while (1) {
delay(1);//サブで実行するプログラムを書く
a++;
}
}
実行結果
ArduinoIDEのシリアルモニタ(115200bps)を開くと、100ごとに数字がカウントアップ表示されてきます。説明その1
ESP32にはCPUコアを2つ持っており、内部的にはFreeRTOSという分散処理が得意なOSが働いています。
CPUコアにはCore0とCore1があり、Arduino IDEで書いたスケッチのメインループはCore1が担当しています。
今回はCore0aという名前で、メインではない方のCPUコアでスレッドを1つ立て、サブとして働くプロセスを実行しています。
変数aに対し、Core0aのループはdelay(1)のペースで加算を行なっています。
一方、Core1で動くメインループは、delay(100)のペースで変数aの内容をシリアルモニタに表示しています。
つまり、Core1が1回実行される間に、Core0aは100回実行されます。結果として、表示される内容は100番とばしになります。
説明その2
マルチスレッド用のコマンドをスクリプトに解説を加える形で説明します。
//マルチスレッドのタスクハンドル格納用です。
//タスクハンドルはアドレスで渡す必要があるため、ここで配列として設定しています。
TaskHandle_t thp[1];
//カウントを加算し、シリアルモニタで表示するための変数を設定しています。
//ここで宣言した変数は、コアを跨いでアクセスできる変数になります。
long a = 0;
//セットアップ関数です。スレッドの設定はここで行います。
void setup() {
//シリアルモニタ表示用の宣言です。
Serial.begin(115200);
//ここでスレッドを立てることを宣言しています。
xTaskCreatePinnedToCore(Core0a, "Core0a", 4096, NULL, 3, &thp[0], 0);
//xTaskCreatePinnedToCore()がスレッドの宣言です。
//内容は([タスク名], "[タスク名]", [スタックメモリサイズ(4096or8192)],
// NULL, [タスク優先順位](1-24,大きいほど優先順位が高い)],
// [宣言したタスクハンドルのポインタ(&thp[0])], [Core ID(0 or 1)]);
}
void loop() {//メインCPU(Core1)で実行するプログラム
Serial.println(a);//変数aの中身をシリアル出力
delay(100);//100/1000秒待つ
}
void Core0a(void *args) {//サブCPU(Core0)で実行するプログラム
while (1) {//ここで無限ループを作っておく
//サブで実行するプログラムを書く
delay(1);//1/1000秒待つ
a++;//aに1を加算する
}
}
同じ要領で2つのコアそれぞれに対し、2つめ3つめのサブプロセスを追加してくことができます。
今回はマルチコアを試しましたが、1つのコアに複数のスレッドを立てればマルチスレッドとなります。
マルチコアとマルチスレッドは同時に設定できますので、冒頭の図の右下のような形でプログラムを走らせることもできます。
ただし、複数の独立したプロセスを同時に走らせると、同期やデータのやりとりのタイミングでいくつかの調整が必要になってきます。
FreeRTOSにはその制御を簡単にするための関数も用意されており、ArduinoIDEからも使うことができるようです。
実験
Core0,Core1それぞれに3つずつスレッドを立てます。
メインのloopも加え、合計7つのスレッドが動くか試します。
TaskHandle_t thp[6];//マルチスレッドのタスクハンドル格納用
long a ,b, c = 0;
void setup() {
Serial.begin(115200);
xTaskCreatePinnedToCore(Core0a, "Core0a", 4096, NULL, 7, &thp[0], 0);
xTaskCreatePinnedToCore(Core0b, "Core0b", 4096, NULL, 8, &thp[1], 0);
xTaskCreatePinnedToCore(Core0c, "Core0c", 4096, NULL, 9, &thp[2], 0);
xTaskCreatePinnedToCore(Core1a, "Core1a", 4096, NULL, 6, &thp[3], 1);
xTaskCreatePinnedToCore(Core1b, "Core1b", 4096, NULL, 5, &thp[4], 1);
xTaskCreatePinnedToCore(Core1c, "Core1c", 4096, NULL, 4, &thp[5], 1);
}
void loop() {//メインループ
Serial.print("main"); Serial.println(a+b+c); delay(100);
}
void Core1a(void *args) {//スレッド ①
while (1) {Serial.print("a:"); Serial.println(a); delay(100);}
}
void Core1b(void *args) {//スレッド ②
while (1) { Serial.print("b:"); Serial.println(b); delay(100);}
}
void Core1c(void *args) {//スレッド ③
while (1) { Serial.print("c:"); Serial.println(c); delay(100);}
}
void Core0a(void *args) {//スレッド ④
while (1) { delay(1); a++;}
}
void Core0b(void *args) {//スレッド ⑤
while (1) { delay(2); b++;}
}
void Core0c(void *args) {//スレッド ⑥
while (1) { delay(3); c++;}
}
実行結果
7つのスレッドが無事に動きました。
スレッド間の実行優先順位として、メインのvoid loop() {}は最下位に位置している模様です。
ウォッチドッグとディレイ
メインループ以外にあるdelay(1)は今回のプログラムでは必須です。
システム側で無限ループに陥っているスレッドを監視しており、そう判定すると自動的にシステムをリセットするウォッチドッグという仕組みがあります。
それを防ぐことができるのがdelay(1)のようです。
(デフォルトではメインループにはウォッチドッグが適用されません。)
また、delay中に他の処理を行うことで並行処理を実現させているらしく、そういう意味でも各スレッドにdelayが入っていると円滑です。
次回
次はプロセス間の数値の受け渡し方法や同期のとりかたなどについてまとめたいと思います。
次回記事:
次次回記事:
前回記事: