M5StickC Plusを含むESP32ベースのArduinoには、FreeRTOSと言うリアルタイムOSが載っています。
FreeRTOS(公式HP)は、リアルタイムOSと呼ばれる、実時間制御が必要な用途に適応可能なオペレーティングシステムです。プリエンプティブマルチタスク(時分割並列処理)が可能で、スレッドと呼ばれる実行単位を起動して複数のタスクを時分割並列処理することができます。
プリエンプティブマルチタスクと言うのは、複数のタスクが有った時、OSが時分割でタスクを切り替えて、あたかも同時並列に処理しているかの様に見せる技術のことです。WindowsやUNIXなどのOSも、プリエンプティブマルチタスクです。
ESP32の様なマルチコアMCUでは、複数のコアにスレッドを飛ばして、マルチコア並列処理することも可能なOSです。
これがですね、上手く説明されたページが中々見つからないんだ……。頭では理解したつもりなのですが、説明できるか?と言うと、難しいのです。短くまとめてみると、以下のような感じでしょうか?
実時間の時間的制約に縛られたタスクを処理する機能を備えたOS
伝わりましたか?
これ以上は、実際の例を示してごまかしてみます……m(1__)m。
「#7-3 Raspberry Piで気温・湿度センサを読み出す」
上の記事は、昔Raspberry Pi ZeroWを使って、気温・湿度センサDHT11からデータを読み出そうと試みた記事です。実験したところ、うまく通信できないケースが頻発しました。
Pi ZeroWのRaspbian(Linux系)というOSは、リアルタイムOSではないので、並列実行されている、他のタスクに処理を奪われてしまうと、数10msかえってこない場合があって、その間にDHT11との通信が破綻したことが原因でした。
DHT11との通信では、通信を始めたら、いついつまでにデータを受信して、いついつまでに通信を終了しなくてはならない、と言う実時間上の制約があります(下図、リンク)。
※上図は、DHT11と姉妹品のAM2302という気温センサについて描いたチャートですが、DHT11でもほぼ同じです。
それにもかかわらずLinuxは、その処理の途中で別なタスクにスイッチしてしまうため、通信が破綻してしまうのです。
リアルタイムOSであれば、タスクの優先順位(プライオリティ)を上げる事で、他のタスクに邪魔されることなくDHT11と通信することができます。あるいは、そもそも他のタスクが存在しないのであれば、初めから全CPUタイムは現在実行中のタスクに割り当てられます。
つまるところ、Arduinoみたいな組み込み用途で、外部I/Oモジュール等を実時間で制御する環境には、Linux等のOSではダメで、何らかのリアルタイムOSが必要なのです。
ESP32ベースのArduinoでは、まずESP32にFreeRTOSが載っていて、その上でArduinoコアが動作しています。従って、Arduinoのプログラムコード中からFreeRTOSのAPI関数をコールすることで、スレッドと呼ばれるプログラムを起動して、並列処理を実行することができるのです。
スレッドを起動するAPI関数には以下の様なものがあります。
普通は、ネイティブのxTaskCreate()関数で事足ります。xTaskCreate()関数で起動したスレッドは、FreeRTOSのタスクスケジューラによって、自動的にデュアルコアであるESP32の空いている方のCPUで実行されるので、「今、どちらのCPUで実行されているのか?」とか、「空いているCPUはどっちだ?」とか、ユーザが考える必要は、ありません。
espressif社が拡張したxTaskCreatePinToCore()関数は、ユーザが、スレッドを実行するCPUを一々指定することで、FreeRTOSのタスクスケジューラの働きを肩代わりする時に使います。言い換えると、タスクスケジューラに任せたくなくて、自分でスケジューリングしたいときに使う関数です。
わざわざ手間を増やすだけですので、特段の理由が無い限り、普段は使わなくて良い関数です。
ここから、円周率計算プログラムをFreeRTOSのスレッドを使って実行するように改造して行きます。
以下図に2つのタスクを実行する様子を示して説明します。
円周率計算プログラムをオリジナル16ビットCPUコアMoscoviumのデュアルコアで計算させた以下の記事も復習しておいてください。
「#17-8 円周率とデュアルコア」
上記事中の実験に於いて、デュアルコアで分散処理させるのに最適な公式がKlingenstierna公式であると判りました。
Klingenstierna公式: 8×atan (1/10)−atan (1/239)−4×atan (1/515)=π/4
今回はESP32のデュアルコアに、Klingenstierna公式を分散処理させて、デュアルコアMoscoviumと同様に高速演算させてみたいと思います。
つまり、上図c)が目指す形となります。
スレッドを起動するAPI関数は、xTaskCreate()関数です。関数の使い方は、FreeRTOSの公式HPから、API関数のリファレンスページに辿り着くことができます。ただ、このリファレンスページは検索が使えないので、所望の関数仕様に辿り着くのが困難です(執筆時点)。お勧めしません。
espressif社のESP-IDF APIリファレンスページから、FreeRTOSと言う章を辿ると、xTaskCreate()関数の仕様に辿り着けるし、こっちは検索も使えるので、アクセスは楽です。お勧め。
xTaskCreate()関数の実行に必要な情報(引数):
円周率計算用として、以下の関数をスレッドとして起動することとします。
スレッド関数: void calc_pi (void *argv);
引数argvには、int16配列へのポインタを渡すと決めておきます。
int16配列は、配列要素2個を1組として、m×atan (1/x)項に於けるパラメータ、mとxを設定します。角かっこ[ ]の中の添え字を0から数えて、偶数番目に係数m、奇数番目にxの値を設定します。
Klingenstierna公式であれば、以下の通りです。
// Klingenstierna公式
// 8×atan (1/10)−atan (1/239)−4×atan (1/515)=π/4
int16 pipm_klin[ ]=
{
// m, x: m*atan (1/x)
8*4, 10,
-1*4, 239,
-4*4, 515,
0,0 // end mark
};
偶数番目のパラメータが0だったところで演算終了(end mark)とします。シングルコアで実行する場合は、以下の関数コールで、戻ってきた時には円周率の計算が終了しています。
calc_pi (pipm_klin);
以降、デュアルコアに分散処理させるための議論を進めます。
円周率の計算には、多倍長固定小数型を実装するためのint16配列が2個必要です。
static int16 lfp_A1[LFP_MAX]; // lfp: long fixed point array
static int16 lfp_B1[LFP_MAX];
多倍長固定小数lfp_A1[ ]に最終的な計算結果(円周率)が返ります。lfp_B1[ ]には、lfp_A1[ ]に加算する、Gregory級数を計算して行きます。
デュアルコアに分散するので、この多倍長固定小数型配列変数が、もう1組必要となります。
static int16 lfp_A2[LFP_MAX]; // lfp: long fixed point array #2
static int16 lfp_B2[LFP_MAX];
2個のタスクは同時並列に処理が進むので、タスク毎に異なる配列を使って計算しなくてはならないためです。atan (−x)=−atan (x)の関係式を使うと、常にx>0にできるので、引数のint16配列に設定したxの正負を使って、以下の様なコマンドをスレッド関数に伝えることにしました。
また、task2はスレッドを起動しますが、task1は、そのまま現在のプロセスが実行するので、指定された処理終了後、task1の場合はリターン、task2の場合は無限ループしなくてはなりません。これを、int16配列に設定した「end mark」の奇数番目パラメータで切り替えます。
task1(リターン): 0,0 // end mark
task2(無限ループ): 0,1 // end mark
以上を踏まえて、ESP32デュアルコアへ分散処理させるコードは以下の様になります。
// メインプロセス(task #1)用
int16 pipm_klin_c1[ ]=
{
-1*4, 239,
-4*4, 515,
0,0 // end mark
};
// サブスレッド(task #2)用
int16 pipm_klin_c2[ ]=
{
8*4, -10,
0,1 // end mark
};
// start task #2
TaskHandle_t htsk=NULL;
xTaskCreate (
calc_pi, // thread function
"calc_pi_2", // thread name
1024, // stack size
pipm_klin_c2, // parameter
uxTaskPriorityGet (NULL), // priority
&htsk); // task handle
// start task #1
calc_pi (pipm_klin_c1);
Klingenstierna公式の3個のm×atan(1/x)項を、task1に2個、task2に1個割り振って、分散処理させています。FreeRTOSが載っていたお陰で、xTaskCreate()関数を使うだけでマルチコア並列処理を実現できてしまいました。Good。
以下に完成したプログラムをリンクしておきます。
完成プログラム:calc_pi.ino
コンパイルしてM5StickC Plusへ書き込んでおいてください。必要に応じて、プログラム(calc_pi.ino)先頭付近にある、以下のコンパイルオプションを有効/無効化してコンパイルしてください。
コンパイルオプション:
#define USE_ESP32_DUAL // use dual core cpu for ESP32
デフォルト無効になっていて、シングルコアで計算します。有効にすると、デュアルコアに分散処理します。
起動すると、シリアルモニタから、こちらのコマンドを受け付けています。
M5StickC Plus(下図)を使って実験して行きます。普通にUSBケーブルでPCとM5StickC Plusを接続してください。他のESP32ボードでも動作するはずですが、当方では未確認です。
実験サンプル:【 】内はシリアルモニタからの送信データ
【pi,5116】
OK+ start calculation
OK+ Klingenstierna's formula
OK+ PI=3.
1415926535 8979323846 2643383279 5028841971 6939937510
:
: (中略)
:
4786724229 2465436680 0980676928 2382806899 6400482435
4037014163 149648
OK+ digits=5116 (size=1280)
OK+ time=845882[us]
OK+ dual core cpu #1=691750[us] #2=844978[us]
OK+ check=0x3202d455
OK: pi F_CPU=240000000
コンパイルオプション「USE_ESP32_DUAL」を有効にして、5,116桁の円周率を計算してみました。こちらの期待値と比較して、約5,000桁の円周率計算値が正しいことを確認しました。プログラムに誤りはなく、正しく計算できているようです。
また、上の実行結果ログより、デュアルコアに分散処理した結果、CPU1(task1)が約692[ms]、CPU2(task2)が約845[ms]、全体の計算時間が約846[ms]と、並列処理できた事を確認できました。
以下表に、過去「#17-8 円周率とデュアルコア」にて計測した各機体の計算時間と、今回の実験結果を追加して、まとめます。
円周率計算時間 (calc_pi) | 5,116桁 [s] |
速度比 @1MHz |
備考 (Klingenstierna公式) |
|||
ボード | マイコン [MHz] | 実測 | @1MHz | |||
ESP32ボード | ESP32 (Xtensa LX6) | 240 | 1.525 | 366 | 1.00 | 32bit,MUL,DIV |
M5StickC Plus | ESP32 (Xtensa LX6) | 240 | 1.520 | 365 | 1.00 | 32bit,MUL,DIV |
ESP32 (Xtensa LX6) | 240 | 0.846 | 203 | 1.80 | 32bit,MUL,DIV,DUAL | |
Arduino DUE | SAM3X8E | 84 | 7.105 | 597 | -1.63 | 32bit,MUL,DIV |
STM32ボード | STM32F103C | 72 | 7.493 | 540 | -1.47 | 32bit,MUL,DIV |
FPGAボード (Moscovium) |
MCOC115CP0408 | 24 | 49.017 | 1,176 | -3.21 | 16bit,MUL,DIV |
MCOC115DC0416 | 24 | 27.577 | 662 | -1.81 | 16bit,MUL,DIV,DUAL |
考察:
FreeRTOSのプリエンプティブマルチタスクを使うと、複数のタスクを並列処理することができます。マルチタスクの使えない他の機体では、タイマ割り込みなどを駆使して並列処理を実装しなければならないのですが、FreeRTOSが載っている環境ならxTaskCreate()関数でお終いなので、容易に並列処理を実装することができます。
余談ですが、STM32ボード等のArduinoにも、FreeRTOSが載っているみたいです。STM32ボードでも、FreeRTOSの恩恵を受けることができるかもしれません(当方では未確認です)。
ただし、STM32ボードはシングルコアなので、時分割並列処理が関の山です。ESP32のマルチコア並列処理と違って、STM32で複数タスクに分散しても、円周率の計算は速くなりません。こちらの図でb)の動作になるだけです。
ESP32が持っている優位の1つにデュアルコア💕が挙げられます。FreeRTOSのスレッドを使わないと、デュアルコアの内、1個のCPUしか使っていないので勿体ないです。使わない手は無いですよ。