前回、FreeRTOSの各種設定を決めるFreeRTOSConfig.hを読んでいて「思っていたのと違う」ところがありました。勝手に思いこんでいただけなんだけれども。今回もそんな思い込みを正して参りたいと思います。メモリの割り当てについてです。どっかヒープから割り当ててくれているのでないの?
※「モダンOSのお砂場」投稿順Indexはこちら
※Arduino IDE上で「スケッチ」形式のソースからFreeRTOSを使って実験してみてます。ターゲット機はArduino UNO R4。泣く子も黙る?Armコアのルネサスマイコン搭載機です。
FreeRTOSのバージョン
「メモリについて」と書きながらバージョンについて説き起こすのは、特定バージョンを境にメモリアロケーションの仕組みが変わった部分があるからです。
-
- FreeRTOS V9.0.0より前は、FreeRTOSのヒープメモリからRTOSで使う各種オブジェクトが使うメモリが切り出されて割り当てられていた
- FreeRTOS V9.0.0以降は、ビルド時に静的に割り当てること「も」可能となった
さて、手元のArduino環境で使用可能なFreeRTOSのバージョンはどうなっているのでしょう?Arduinoのpackage共がインストールされているフォルダの奥底の “renesas_uno” がインストールされているそのまた奥
lib\FreeRTOS-Kernel-v10.5.1\task.h
に以下のマクロが定義されておりました。
#define tskKERNEL_VERSION_NUMBER “V10.5.1”
なんだマクロの値を見なくても上のパスみたらもろ見えじゃん。ともかくV10.5.1ってことね。
デフォルト設定を確認
バージョンはV10.5.1ということでスタティックかと思えば違います。FreeRTOSConfig.hのデフォルト設定を確認してみます。
#define configSUPPORT_STATIC_ALLOCATION (0)
#define configSUPPORT_DYNAMIC_ALLOCATION (1)
以上のように設定されておりました。つまりデフォルトでは、スタティックなアロケーションはしない。ダイナミックなアロケーション(ヒープから切り出してアロケートする)です。スタティックな設定にすることもできるけれども、そのときは自分で設定しろ、ってこってす。
このスタティックかダイナミックかという話は御本家の以下のページで詳しく議論されとります。
Static Vs Dynamic Memory Allocation
しかし、ヒープメモリはどれだけ使えるのだろう?そう思って確かめると
#define configTOTAL_HEAP_SIZE (0x2000)
だそうです。0x2000=8Kバイトですがな。確かArduino UNO R4機のSRAM量は32Kバイトです。4分の1をヒープに割り当ててあるということみたいです。そして、もひとつ「謎の」設定
#define configAPPLICATION_ALLOCATED_HEAP (4)
なにやら 4 に設定してます。これは、以下の御本家ページを参照するとようやく分かります。
heap_4.c にて定義されているヒープのschemeのアルゴリズムみたいです。ヒープの断片化を防ぐために連続するフリーブロックを結合してくれるらしいデス。
今回実験のソース
ここまでの調査にて、当方手元のArduino環境でのFreeRTOSのデフォルトでは、タスクだとかタイマだとかもろもろのRTOSのオブジェクトを作る度に、8Kバイトのヒープからメモリが切り出されてくることがわかりました。打ち出の小槌かと思いきや案外上限は小さかったのね。
そこで前回実験で使ったソース、タスクを4個も作っているものをもってきて、その中で実際どれだけヒープからメモリを使っているのか調べてみることにいたしました。
ソースは以下に(前回ソースの転用、チョイ直しです。)
#include <Arduino_FreeRTOS.h> #define T3PIN (7) #define T2PIN (6) #define T1PIN (5) #define LWAIT (10000) #define NTASKS (3) #define PRIOBASE (1) TaskHandle_t loop_task; TaskHandle_t tasks[NTASKS] = {NULL, NULL, NULL}; const char taskname[NTASKS][6] = {"Task1", "Task2", "Task3"}; pin_size_t taskParam[NTASKS] = {T1PIN, T2PIN, T3PIN}; void initPins() { pinMode(T1PIN, OUTPUT); digitalWrite(T1PIN, 0); pinMode(T2PIN, OUTPUT); digitalWrite(T2PIN, 0); pinMode(T3PIN, OUTPUT); digitalWrite(T3PIN, 0); } void printFreeHeapSize() { size_t freeSize = xPortGetFreeHeapSize(); Serial.print("Free Heap Size: "); Serial.println(freeSize); } void printMinHeapSize() { size_t hSize = xPortGetMinimumEverFreeHeapSize(); Serial.print("Min Heap Size(Ever): "); Serial.println(hSize); } void loop_thread_func(void *pvParameters) { while (1) { vTaskDelay(LWAIT); printFreeHeapSize(); printMinHeapSize(); } } void task_func(void *pvParameters) { TickType_t xLastWakeTime; const TickType_t xFrequency = 1; pin_size_t pinNumber = *(pin_size_t *)pvParameters; xLastWakeTime = xTaskGetTickCount(); while (1) { vTaskDelayUntil(&xLastWakeTime, xFrequency); digitalWrite(pinNumber, 1); delayMicroseconds(100); digitalWrite(pinNumber, 0); } } void setup() { Serial.begin(115200); while (!Serial) { } printFreeHeapSize(); initPins(); auto const rc_loop = xTaskCreate ( loop_thread_func, static_cast<const char*>("Loop Thread"), 512 / 4, nullptr, PRIOBASE, &loop_task ); if (rc_loop != pdPASS) { Serial.println("Failed to create 'loop' thread"); return; } TaskHandle_t xHandle = NULL; BaseType_t xReturned; for (int i=0; i<NTASKS; i++) { xReturned = xTaskCreate ( task_func, taskname[i], 512 / 4, (void *)&taskParam[i], PRIOBASE, &xHandle ); if (xReturned != pdPASS) { Serial.println("Failed to create task."); return; } else { tasks[i] = xHandle; } } Serial.println("Starting scheduler ..."); vTaskStartScheduler(); for( ;; ); /* Never! */ } /* NEVER CALLED! */ void loop() { }
実機上での実行結果
上記のソースをビルドして実行すると
-
- setup()内でSerialオブジェクトを初期化した直後のヒープの残高を報告する
- loop task内で10秒に1度、ヒープの残高と今までで一番ヒープ残高が減ったときにミニマム値を報告する
ようになっています。
セットアップ開始直後はFree Heap Sizeは0だと。そうだ、まだFreeRTOS自体が動いてないじゃん。あたりまえか。
その後FreeRTOSのスケジューラが起動すると。残高は4240バイトと。Minサイズの方も同じ。一度も掴んだメモリを解放したりしていないので一致するのは当たり前か。タスク4個作ってだいたいヒープの半分くらい使ってしまうのね。