ESP32×FreeRTOS:Queueとは?グローバル変数との違いとxQueueCreate/Send/Receiveを実装で解説

FreeRTOS

この記事でわかること

  • FreeRTOS Queueの機能内容(何ができるか)
  • Queueとグローバル変数の決定的な違い(状態共有 vs イベント共有)
  • ESP32で動くQueue利用のLチカ実装例
  • 主要なQueue関数(xQueueCreate / Send / Overwrite / Receive)の役割と引数
  • Queueが溢れたときの扱い(捨てる/待つ/上書き)

Queueとは(タスク間通信の基本)

FreeRTOSの Queue(キュー) は、タスク間でデータを受け渡しするための仕組みです。
一言でいうと、

送信された値を、順番に“ためておける箱”

です。

Queueの重要ポイント(初心者が最初に押さえるべきこと)

  • Queueは 値をコピーして保存します(変数の共有ではない)
  • 作成時に決めた件数(Queue長)まで保持できます
  • FIFO(先入れ先出し)で取り出します

イメージ:

[ TaskA ] –send–> [ Queueの箱 ] –receive–> [ TaskB ]


Queueとグローバル変数の違い

違いを一言でまとめるとこうです。

  • Queue:イベント共有(履歴を保持できる)
  • グローバル変数:状態共有(最後の1つしか残らない)

グローバル変数は「途中の指示」が消える

TaskAが短時間に何度も更新すると、

blinkInterval = 500
blinkInterval = 200
blinkInterval = 500

グローバル変数には 最後の500しか残りません
「200に変えた」という途中の事実は消えます。

LED点滅くらいなら許容できることもありますが、以下だと致命的になりやすいです。

  • ボタン押下イベント
  • エラー通知
  • コマンド処理
  • ログ収集
  • 通信送信キュー(MQTT/HTTPなど)

Queueは「送った順に溜められる」

Queueなら(長さが足りる限り)

[500][200][500]

のように 履歴として保持できます。
受信側のタスクが少し遅れても、溜まっている限り 順番に受け取れます


【実装】ESP32で動かすQueue利用のLチカ例(最新値だけ欲しい構成)

ここでは「点滅周期(200ms/500ms)」をTaskAが更新し、TaskBが受け取って反映します。

今回の設計方針:

  • 途中履歴はいらない
  • 最新の設定値だけあればよい

→ そのため Queue長=1 + xQueueOverwrite()(最新値箱 / Mailbox) を使います。

サンプルコード(Arduino-ESP32で動作)

#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;

    // 最新値だけ保持したい → Queue長=1 + Overwrite
    xQueueOverwrite(blinkQueue, &interval);

    vTaskDelay(pdMS_TO_TICKS(2000)); // 2秒ごとに更新
  }
}

void TaskB(void *pvParameters) {
  uint32_t currentInterval = 500;
  pinMode(LED_PIN, OUTPUT);

  for (;;) {
    // 0:Queueが空なら待たずに即戻る(ポーリング)
    if (xQueueReceive(blinkQueue, &currentInterval, 0) == pdPASS) {
      // 受信できた場合は currentInterval が更新されている
    }

    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占有を避ける
}

主要なQueue関数まとめ(役割と引数)

Queueの主要関数を整理します。
まずは Create / Send(or Overwrite) / Receive の3つを押さえればOKです。

Queueを作る:xQueueCreate

blinkQueue = xQueueCreate(
  1,                 // uxQueueLength
  sizeof(uint32_t)    // uxItemSize
);
引数この例での値意味
uxQueueLength1Queueに保持できる件数(最大いくつ溜めるか)
uxItemSizesizeof(uint32_t)1件あたりのデータサイズ(バイト)

戻り値:QueueHandle_t(失敗時は NULL

Queueへ送る:xQueueSend(履歴を溜めたいとき)

xQueueSend(
  blinkQueue,     // xQueue
  &interval,      // pvItemToQueue
  0               // xTicksToWait
);
引数この例での値意味
xQueueblinkQueue送信先Queue
pvItemToQueue&interval送るデータのアドレス(値はQueue内にコピーされる)
xTicksToWait0Queueが満杯なら待たずに失敗で戻る

使い方:

xQueueSend(queue, &data, 0);              // 満杯なら捨てる(ロスOK)
xQueueSend(queue, &data, portMAX_DELAY);  // 空くまで待つ(ロス禁止)

Queueへ送る:xQueueOverwrite(最新値だけ欲しいとき)

xQueueOverwrite(
  blinkQueue,   // xQueue(Queue長=1で使うのが定番)
  &interval     // pvItemToQueue
);
引数この例での値意味
xQueueblinkQueue送信先Queue(Queue長=1の“最新値箱”向け)
pvItemToQueue&interval送るデータのアドレス(値はQueue内にコピーされる)

Queue長=1とセットで使うと、常に最新値だけ保持できます(古い値は残りません)

Queueから受け取る:xQueueReceive

if (xQueueReceive(
  blinkQueue,     // xQueue
  &interval,      // pvBuffer
  0               // xTicksToWait
) == pdPASS) {
  // 受信できたら interval が更新される
 //pdPASSはQueue APIが成功したかどうか(送信/受信できたか)」を示す戻り値
}
引数この例での値意味
xQueueblinkQueue受信元Queue
pvBuffer&interval受け取った値を書き込む先(ここにコピーされる)
xTicksToWait0Queueが空なら待たずに即戻る

使い方:

xQueueReceive(queue, &data, 0);              // 空なら即戻る(ポーリング)
xQueueReceive(queue, &data, portMAX_DELAY);  // 来るまで待つ(ブロッキング受信)

Queueを使うべき場面と設計指針(最新値/履歴)

Queue設計は、まずこの2択で迷いが減ります。

A. 最新値だけ欲しい(状態に近い)

例:温度の現在値、設定値、LED点滅周期など

  • Queue長:1
  • 送信:xQueueOverwrite()
  • 受信:必要なタイミングで読む

今の状態だけ欲しいならこの構成が最もシンプルです。

B. 履歴も処理したい(イベント)

例:ボタン押下、エラー通知、コマンド、ログなど

  • Queue長:2以上
  • 送信:xQueueSend()
  • 受信:基本はブロッキング受信(portMAX_DELAY など)

起きた順番に全部処理したいならこの構成が基本です。


Queueが溢れたときの選択肢(取りこぼし対策)

Queueが満杯のとき、FreeRTOSが勝手に良い感じに捨ててくれるわけではありません。
設計者が「どう扱うか」を決めます。

選択肢1:満杯なら捨てる(ロスOK)

if (xQueueSend(queue, &data, 0) != pdPASS) {
  // 送れなかった(捨てた)ことをログに残すなど
}
  • 送信側が止まらない
  • ロスが許容できるデータ向け(センサーの頻繁な更新など)

選択肢2:空くまで待つ(ロス禁止)

xQueueSend(queue, &data, portMAX_DELAY);
  • ロスしない
  • 送信側が止まる(設計次第で全体の遅延になる)

選択肢3:最新値だけ残す(Queue長=1 + Overwrite)

xQueueOverwrite(queue, &data);
  • 最新値を維持できる
  • 履歴(途中経過)は捨てる、という設計になる

まとめ

  • Queueは タスク間で値を安全に受け渡しする“箱”
  • グローバル変数は 最後の値しか残らず、途中イベントが消えやすい
  • 「最新値だけ欲しい」なら Queue長=1 + xQueueOverwrite
  • 「履歴を処理したい」なら Queue長>1 + xQueueSend/Receive
  • 溢れ対策は 捨てる/待つ/上書き を設計で選ぶ

次回はSemaphoreについて記載します。

コメント

タイトルとURLをコピーしました