この記事でわかること
- FreeRTOS の Queue が何か理解できる
- グローバル変数ではなぜ危険かがわかる
- ESP32で動くqueueを使った実用コードが手に入る
目次
- FreeRTOS Queueとは — FreeRTOS における「タスク間通信手段」
- FreeRTOS Queueとグローバル変数の違い
- 【実装】 ESP32で動かす FreeRTOS Queue 利用の Lチカ実装例
- グローバル変数を使用した場合の問題点
- FreeRTOS Queue を使うべき場面と設計指針
- FreeRTOS Queue が溢れたとき・データ取りこぼし
- まとめ
1. FreeRTOS Queueとは — FreeRTOS における「タスク間通信手段」
FreeRTOS の Queue(キュー) は、タスク間でデータを受け渡しするための仕組みです。
一言でいうと、
タスク間で値を安全に保管できる“箱(メッセージバッファ)”
です。
FreeRTOS Queue はどんな箱?
- Queue は RAM 上に一定サイズのメモリ領域を確保
-
sendすると、値は コピーされて Queue 内に格納 -
receiveすると、Queue の 先頭から順番に取り出される - 複数件保持可能(上限は作成時に決める)
- FIFO(先入れ先出し)
- FreeRTOS が内部で排他制御してくれる
📦 イメージ:
[ TaskA ] --send--> [ Queueの箱 ] --receive--> [ TaskB ]
Queue の箱には、
┌─────────────┐
│ 500 ms │
├─────────────┤
│ 200 ms │
├─────────────┤
│ 500 ms │
└─────────────┘
のように 複数データを順番に保持できます。
ここが グローバル変数とはまったく違う点です。
FreeRTOS Queue が保持できる容量はこう決まる
Queue を作成する関数:
xQueueCreate( queueLength, itemSize );
| 引数 | 説明 |
|---|---|
queueLength |
Queue 内に保持できる“件数” |
itemSize |
1件あたりのデータサイズ(例:sizeof(uint32_t)) |
例:
xQueueCreate(3, sizeof(uint32_t));
→ uint32_t を最大 3 件まで保持できる箱
📦 内部イメージ:
[500][200][500] ← 最大3件まで
FreeRTOS Queue の超大事なポイント(初心者が落としがちな部分)
Queueは「変数の参照を共有する」のではなく、「値をコピーして内部に保持する」
つまり、
-
sendした瞬間、値は Queue 内に保存される -
send元の変数が上書きされても Queue 内のデータは上書きされない - FIFO順で確実に取り出せる
- 読み取りタイミングに左右されない
この「コピー保持」という仕組みが、
安定したタスク間通信を可能にしています。
2. FreeRTOS Queueとグローバル変数の違い
グローバル変数
- 状態(最後の1つ)しか持てない
- 上書きされると過去の値は消失
- 読み取りタイミングが悪いと 古い値を参照する可能性
- 排他制御を自分で実装する必要がある
例:
blinkInterval = 500
blinkInterval = 200
blinkInterval = 500
最終的には 500 しか残らない
途中の 200 が存在した事実は “消える”。
Queue
[500][200][500]
というように 送信された順番で蓄積される
- 履歴を保持できる
- 読み取りのタイミングがズレても “取りこぼしがない”
- FreeRTOS が排他制御を内部実装
- 最新値だけ扱いたい場合は
xQueueOverwrite()が便利
Queue = イベント共有
グローバル変数 = 状態共有
3. 【実装】 ESP32で動かす FreeRTOS Queue 利用の Lチカ実装例
以下は Arduino + ESP32(DevKitC)で動くコードです:
- TaskA:500ms ⇔ 200msを切り替え、Queue に送信
- TaskB:Queue を受信して点滅周期を変更
- Queue 長 = 1(最新値だけ保持)
-
xQueueOverwrite()により「最新値を保存し続ける」
#include <Arduino.h>
QueueHandle_t blinkQueue;
const int LED_PIN = 16; //タスクの記事同様のピンを使用
void TaskA(void *pvParameters) {
uint32_t interval = 500;
for (;;) {
interval = (interval == 500) ? 200 : 500;
xQueueOverwrite(blinkQueue, &interval);
vTaskDelay(pdMS_TO_TICKS(2000));
}
}
void TaskB(void *pvParameters) {
uint32_t currentInterval = 500;
pinMode(LED_PIN, OUTPUT);
for (;;) {
if (xQueueReceive(blinkQueue, ¤tInterval, 0) == pdPASS) {
// 最新設定を反映
}
digitalWrite(LED_PIN, HIGH);
vTaskDelay(pdMS_TO_TICKS(currentInterval));
digitalWrite(LED_PIN, LOW);
vTaskDelay(pdMS_TO_TICKS(currentInterval));
}
}
void setup() {
blinkQueue = xQueueCreate(1, sizeof(uint32_t)); // 最新値だけ保持
if (!blinkQueue) while (1) delay(1000);
xTaskCreate(TaskA, "TaskA", 2048, NULL, 1, NULL);
xTaskCreate(TaskB, "TaskB", 2048, NULL, 1, NULL);
}
void loop() {
vTaskDelay(portMAX_DELAY); // CPU占有を避ける
}
4. グローバル変数を使用した場合の問題点
今回の Lチカ(更新が 2秒間隔、点滅が 200~500ms)では、
グローバル変数でも一見正しく動きます。
理由:
- TaskB の delay が終わる頃には、最新値を読むことができる
- “タイミングがゆるい” と、問題が発生しにくい
しかし本当に危ないのは:
タスクBが delay 中に A が何度も更新しても、途中の値がすべて無視されること
例:更新が高速だと…
500 → 200 → 500 → 200 → …
TaskB は delay中なので、
次に読むときには「最後の値しか残らない」
= 途中のイベントは 完全に消える
これは LED なら許されても、
- センサーイベント
- エラー通知
- コマンド処理
- ログ
などだと 致命的。
5. FreeRTOS Queue を使うべき場面と設計指針
| 要件 | 設計 |
|---|---|
| 最新値だけほしい | Queue長=1 + xQueueOverwrite()
|
| 履歴も処理したい | Queue長>1 + xQueueSend()
|
| ロスOK | xQueueSend(..., 0) |
| ロス禁止 | xQueueSend(..., portMAX_DELAY) |
| 複数イベント/通知/コマンド | 迷わず Queue |
Queue の強みは 設計の自由度が高いこと。
6. FreeRTOS Queue が溢れたとき・データ取りこぼし
xQueueSend(..., 0)
- 満杯なら送信失敗
- ロスが発生する(捨てられる)
xQueueSend(..., portMAX_DELAY)
- 空きができるまで待機
- ロスしない
xQueueOverwrite(queue, &data)
- Queue長=1なら最適
- 常に最新値を保持する
- 古い値は自動的に消える(というか上書き)
FreeRTOS は勝手に古い値を捨てたりしない
どう扱うかは設計者が選ぶ
7. まとめ
今回はqueueとグローバル変数について比較し、
queueの有効性について紹介しました。
-
グローバル変数は状態共有
→ 最新値1つしか見えない
→ 途中イベントが消える -
Queueはイベント共有
→ 履歴を扱える
→ 読み取りタイミングに影響しない
→ FreeRTOS が排他制御 -
現実の組み込み開発では Queue が必須
LED点滅ならたまたま許されるが、
イベント通知・ログ・エラー処理ではグローバル変数は破綻する。
今後も、タスク間通信について投稿していきます。
次は FreeRTOS の セマフォ(Mutex / バイナリセマフォ)の使いどころと、
Queue との 明確な使い分けを解説します。
FreeRTOS × ESP32 記事シリーズ
Queueについて詳細にまとめていますので良ければご覧ください