CQ出版インタフェース誌に「触発」されてFreeRTOSネタに戻った筈が、ついつい蝶ネクタイのHymel先生(DigiKey)のYouTubeビデオを真似してみる私です。今回はキューのビデオを見たのですが、ビデオ見てたらキューより気になる図があるのです。ふんわかした例だと思ってやり過ごせばいいのですが、気になってやり過ごせない。。。
※「モダンOSのお砂場」投稿順Indexはこちら
再開したFreeRTOSの「実習」は、ESP32ならばArduinoIDE環境でも実はFreeRTOSできる、ということでターゲットはESP32 DevKitC、ビルドはArduinoIDEを使用しています。これもDigi-Key社の「蝶ネクタイ」Shawn Hymel先生のYouTubeビデオのお陰であります。それで先生のビデオを端から視聴させていただいとります。今回拝見したのは以下のビデオです。テーマはQueueによるタスク間のデータの受け渡しです。
Introduction to RTOS Part 5 – Queue | Digi-Key Electronics
ビデオの1分7秒過ぎから現れるメモリの図、パッと見た瞬間、引っかかるものがありました。一応、先生の説明したい意図は良く分かるつもりです。64ビット幅の変数があって、2つのタスクが同じ変数に書き込みとする。もう一つの別のタスクが同じ変数から読み出すとする。そこで32ビット幅の操作はアトミックにできるけれど、64ビットはそうでないとする。第1のタスクが半分の32ビット書きこんだ時点で、第2のタスクがまた別なデータを書き始めたり、第3のタスクが読み取ったりする可能性があるでしょ。こういうときマルチタスク環境で何もしなかったら整合性がとれなくなるでしょ、という話、だと理解しています。趣旨が理解できれば細かいことにケチつけるな、という感じもあるのだけれど気になって仕方が無いデス。0x3FFC_0000番地から0x3FFC_0001番地、0x3FFC_0002番地と3番地分のメモリの箱が描いてあるのだけれど、各箱の中に4バイトづつ数字が書きこまれています。1番地あたり4バイトかい。それに先生がESP32は32ビット機で、メモリ幅は32ビットみたいなこともおっしゃっているし、リトルエンディアンだからなどと箱の中のバイト列の順番に言及されたりするので、余計気になってしまうんです。さらに例題に使っているアドレスも0x3FFC_0000番地とか妙に生生しい、実在するESP32のRAMのアドレス。遥か太古にはバイト毎にアドレスがふられていないマシンも存在したようですが、少なくともこの年寄りでも使った経験がないです。万が一、もしかして、まさか、ESP32ってワード毎アドレッシングなの?黒い疑問が持ち上がりました。
気になってしかたないので、とりあえずビデオを止めて、手元の
ESP32 Series Datasheet Version 2.5
の該当のメモリアドレス部分(Internal SRAM2エリアの丁度真ん中辺)をみてしまいましたぜ。データシートから該当部分をアイキャッチ画像に引用させていただきました。このアドレスの割り振りで200KBとあります。バイトだ。普通。よかった。
ずいぶん脇道にそれてしまいましたが、ようやく本題に戻ります。Queueです。FreeRTOSのQueue関係の説明はこちらにあります。APIはココです。数えてみると24個もAPIがあるのですが、基本、
- xQueueCreate()でQueueを作る
- データを送りたいタスクがxQueueSend()で送る
- データを受け取りたいタスクがxQueueReceive()で受け取る
という仕組みのようです。「蝶ネクタイ」のHymel先生のビデオではこのAPI使って配慮の行き届いたサンプルコードを書かれているのですが、こちらではエイヤーで単刀直入、配慮されていないサンプルを書いて走らせてみました。(全コードは末尾に掲げました。)処理の内容は以下のようです。
- Queueは1個だけ、3段。整数格納用に準備する。
- タスクはA,B,Cの3個走らせる。
- AのタスクはAのインターバル時間毎にデータをQueueに書きこむ。ただしQueueがFullで書きこめないときはカウントする。
- BのタスクはBのインターバル時間毎にデータをQueueに書きこむ。ただしQueueがFullで書きこめないときはカウントする。
- CのタスクはCのインターバル時間毎にQueueからデータを読みこみ、それを表示する。ただし、QueueがEmptyで読みこむデータが無い時はカウントする。また、受信データ数を数え、ある値に達したら、A,Bにそれ以上処理をしないように伝え、それぞれのFull回数と自身のEmpty回数を表示する。それ以降は空回り。
当然、各タスクのインターバル時間や、Queueの深さによってその挙動は異なる筈で、FullとかEmptyとかのカウントも変わってくる筈です。いろいろ変更して挙動を調べたら面白いかとも一瞬思ったですが、先ほどの件で疲れたので見送りました。それにCのタスクの総データ数は10個と大変少ない、手抜きなので直ぐに終わります。
実際、シリアルポートに出力された転送の様子が下です。10000番台の数字はタスクAが送ってきたもの、20000番台がタスクBです。Aから6個、Bから4個の合計10個送られてきたところで処理を停止。Aが待たされたのは3回、Bは0回。受信でEmptyは1回(多分初回か?)という結果でした。
10000 20000 10001 20001 10002 10003 20002 10004 20003 10005 AWAIT 3 BWAIT 0 cEmpty 1
まあ、一応、思った通りに動いているわな。
モダンOSのお砂場(19) FreeRTOS、メモリのアロケーション に戻る
モダンOSのお砂場(21) FreeRTOS、Mutex排他制御の効果が「分かる」サンプル に進む
今回のテスト用コード
#define AWAITMAX (100) #define BWAITMAX (200) #define CPERIOD (100) #define MAXCYC (10) static const BaseType_t app_cpu = 0; static const uint8_t queueSize = 3; static int aWait = 0; static int bWait = 0; static int cEmpty = 0; static volatile bool goFlag = true; static QueueHandle_t msgQueue; void generatorA(void *parameters) { int num = 10000; while (1) { if (goFlag) { if (xQueueSend(msgQueue, (void*)&num, 2) != pdTRUE) { aWait++; } else { num++; } } vTaskDelay(AWAITMAX); } } void generatorB(void *parameters) { int num = 20000; while (1) { if (goFlag) { if (xQueueSend(msgQueue, (void*)&num, 2) != pdTRUE) { bWait++; } else { num++; } } vTaskDelay(BWAITMAX); } } void setup() { Serial.begin(115200); while(!Serial) {}; msgQueue = xQueueCreate(queueSize, sizeof(int)); xTaskCreatePinnedToCore(generatorA, "tA", 1024, NULL, 0, NULL, app_cpu); xTaskCreatePinnedToCore(generatorB, "tB", 1024, NULL, 0, NULL, app_cpu); } void loop() { static int counter = 0; int item; if (goFlag) { if (xQueueReceive(msgQueue, (void *)&item, 0) == pdTRUE) { Serial.println(item); } else { cEmpty++; } if (++counter > MAXCYC) { goFlag = false; Serial.printf("AWAIT %d\r\n",aWait); Serial.printf("BWAIT %d\r\n",bWait); Serial.printf("cEmpty %d\r\n",cEmpty); } } vTaskDelay(CPERIOD); }