前回、ESP32をターゲットにしたとき、ArduinoIDEで「includeの一つもせず」即座にFreeRTOSの関数を呼び出して使えることが分かり驚きました(気付いてなかったのは私だけ?)今回は動的にメモリを確保してもいいんだ、ということに再び気付いてまた驚く、と。FreeRTOSのマルチタスク環境下では勿論、実はArduinoスタイルの「シングルタスク」でも最初から使えていたのでした。
※「モダンOSのお砂場」投稿順Indexはこちら
普通、マイコン(MCU、シングルチップマイクロコントローラ)のプログラムをC/C++で書く場合、動的なメモリの確保(端的にいったらmallocとかnewとか)はまず使わないと思います。キツキツのRAMを有効活用するためにどうしても管理構造などのオーバヘッドができるものは敬遠されます。また動的な構造は「挙動が予想できない」こともあまあり。「予想できない」挙動は制御を旨とするマイクロコントローラの天敵として目の敵にされます。Cでもありますが、C++を使っている場合など特に、各社、各プロジェクト毎こういう書き方はしないように、みたいな禁止リストがあるんじゃないかと思います。そういうことで動的なメモリ確保を多用するPC上のプログラミングとマイコンプログラミングの間には深い谷があるように思います。私もマイコン上では、大域、static, localでメモリ確保は済ませることになれておりまして、ヒープなどは確保したことがありません。しかし、FreeRTOSを使ってみて分かった(前回もご紹介いたしましたDigi-Key社のShawn Hymel先生のYouTubeビデオにはお世話になりっぱなしです。メモリ管理に関係の動画はこちら)のは「FreeRTOSのタスク」たちはFreeRTOSが管理しているヒープの上にそれぞれの管理用のメモリとローカルなスタックを確保している、という事実であります。つまり、
どうせ使っているなら、ヒープ使って動的確保するのもいいんじゃね
という至極真っ当な結論にいたりました。ま、実際はFreeRTOSも前述の問題に「鑑みて」、ヒープ管理の方法を複数用意しているようなので「そのマイコンのアプリ」に適合した方法を選択した上で、でしょう(しかし、今回は良く調べもせず、インストール時のデフォルトのまま。)まあね、ターゲットのESP32は「マイコンとしてはRAMが多め」で実験しているメモリの確保はささやかな量というお陰をこうむっているためですが。さらに言えば、
ESP32向けのArduino環境でならmalloc使ってもいいんじゃね
とも。どうせ裏ではFreeRTOSが動いているのです。AVRマイコンとは違う、ということで。
まずはArduino式シングルタスクの環境でmalloc使うサンプルを書いてみました。この記事の末尾に「非マルチタスク版Arduino環境でのmalloc()」ということでソースを貼り付けました。やっていることは単純で、ループの中で
- 初回、メモリを確保して印字用の文字列を詰め込む
- 2回目、詰め込まれた文字列を印字したあと、メモリを解放。
- これを繰り返す
これだけです。ところどころにヒープの残りバイト数を表示(FreeRTOSの関数利用)するようにしたので、確保、解放の度にどう変動するか分かります。地味な画面ですがこんな感じ。
印字するためにメモリを確保している文字列は末尾のNULL含めて100バイトですが、実際には、オーバヘッド分含め毎回116バイトが確保されています。解放すると初期状態に戻ります。普通にmalloc()、free()で書いて特に問題は出ませんでした。
次にFreeRTOSの環境で4つのタスクを実行する中でメモリを確保するサンプルを書いてみました。ソースは末尾の「マルチタスク版Arduino環境でのpvPortMalloc()」を御覧ください。FreeRTOSに作ってもらった4つのタスクは以下のようです。
- 他のタスクとは無関係にLEDを0.2秒周期で点滅させるタスク
- 15秒に1回印字用の文字列バッファを動的に確保し、その15秒後に印字フラグを立てるタスク
- 印字フラグが立っていることを見つけたら即座にバッファを印字、印字フラグを降ろすタスク
- Arduinoのloop()関数。メインタスク。ループ(毎10秒毎)の中でヒープの残りバイト数を監視
2と3のタスクには関係がありますが、1と4は無関係。
実際に走らせる前に一つTIPS。FreeRTOSの場合、Serialポートはちゃんと初期化してシリアルモニタを接続していた方が良いです。以下に画面キャプチャを貼り付けますが、シリアルモニタがあれば、バグったときに下のようなメッセージが得られます。
クラッシュするとリブートかかるのがデフォルトのようなので、こういう出力でももらえないと何が起こっているのか分からないです。
実際に4タスク並行動作のときの様子がこちら。(LEDがせわし気に点滅している現物写真は先頭に)
1から3のタスクには1024バイトずつのスタックを割り当てて生成しているのですが、それを含めて1412バイトずつが1個のタスクを生成する度に消費されています。100バイト長の文字列を確保する度に消費されるヒープの量は116バイト、先ほどのシングルタスクの時と同じでした。ちゃんと確保、解放を繰り返しています。
いやなんだかな~ ArduinoIDEだけれども、マルチタスクだし、動的にメモリ確保だし、面目一新だな。。。
モダンOSのお砂場(19) FreeRTOS、ArduinoIDEでビルドできたんだ へ戻る
モダンOSのお砂場(20) FreeRTOS、キュー構造。でも別件が気になってしまうっ! へ進む
非マルチタスク版Arduino環境でのmalloc()
const int bufSize = 100; bool okFlag = false; bool goFlag = false; char *bufPtr = NULL; char buf[bufSize]; void sendBuf(void) { if (!okFlag) { bufPtr = (char*)malloc(bufSize * sizeof(char)); memcpy(bufPtr, buf, bufSize); okFlag = true; } else { goFlag = true; } } void receiveBuf(void) { if (goFlag) { Serial.println(bufPtr); free(bufPtr); okFlag = false; goFlag = false; } } void setup() { Serial.begin(115200); while(!Serial) {}; delay(10000); Serial.print("Initial heap Size: "); Serial.println(xPortGetFreeHeapSize()); memset(buf, '0', bufSize-1); buf[bufSize - 1] = '\0'; } void loop() { Serial.print("Current Heap Size: "); Serial.println(xPortGetFreeHeapSize()); sendBuf(); receiveBuf(); delay(10000); }
マルチタスク版Arduino環境でのpvPortMalloc()
static const BaseType_t app_cpu = 0; static const int led_pin = 23; static const int bufSize = 100; static volatile bool okFlag = false; static volatile bool goFlag = false; static char *bufPtr = NULL; void toggleLED(void *parameter) { while (1) { digitalWrite(led_pin, HIGH); vTaskDelay(100); digitalWrite(led_pin, LOW); vTaskDelay(100); } } void sendBuf(void *params) { char c; char buf[bufSize]; memset(buf, '0', bufSize-1); buf[bufSize - 1] = '\0'; while (1) { if (!okFlag) { bufPtr = (char*)pvPortMalloc(bufSize * sizeof(char)); memcpy(bufPtr, buf, bufSize); okFlag = true; } else { goFlag = true; } vTaskDelay(15000); } } void receiveBuf(void *params) { while(1) { if (goFlag) { Serial.println(bufPtr); vPortFree(bufPtr); okFlag = false; goFlag = false; } } } void setup() { Serial.begin(115200); while(!Serial) {}; delay(10000); Serial.print("Initial heap Size: "); Serial.println(xPortGetFreeHeapSize()); pinMode(led_pin, OUTPUT); xTaskCreatePinnedToCore(toggleLED, "t1", 1024, NULL, 0, NULL, app_cpu); Serial.print("t1: "); Serial.println(xPortGetFreeHeapSize()); xTaskCreatePinnedToCore(sendBuf, "t2", 1024, NULL, 0, NULL, app_cpu); Serial.print("t2: "); Serial.println(xPortGetFreeHeapSize()); xTaskCreatePinnedToCore(receiveBuf, "t3", 1024, NULL, 0, NULL, app_cpu); Serial.print("t3: "); Serial.println(xPortGetFreeHeapSize()); } void loop() { Serial.print("Current Heap Size: "); Serial.println(xPortGetFreeHeapSize()); vTaskDelay(10000); }