Queue(キュー)とは?FreeRTOSのタスク間通信を初心者向けに解説

組み込み基礎

RTOSを使って複数のタスクを作ると、次に必要になるのが「タスク同士でどうやってデータを受け渡すか」という仕組みです。

たとえば、センサーを読み取るタスクとOLEDへ表示するタスクを分けたとします。

Sensor Task
     ↓
温度を測定
     ↓
   25.4℃
     ↓
???
     ↓
Display Task
     ↓
OLEDへ表示

Sensor Taskで取得した「25.4℃」というデータを、Display Taskへ安全に渡す必要があります。

このようなタスク間でデータを受け渡すための仕組みとしてFreeRTOSが提供しているのが、Queue(キュー)です。

Queueを使うと、送信側のタスクがデータを格納し、受信側のタスクが必要なタイミングでそのデータを取り出せます。

この記事では、Queueの基本的な仕組みから、FIFO、Queueの作成、送信・受信、待ち時間、構造体の受け渡し、割り込みからの利用まで順番に解説します。

FreeRTOSのQueueの仕組み。FIFO、xQueueCreate、xQueueSend、xQueueReceive、タスク間通信、待ち時間を示した図
FreeRTOSのQueueを使ったタスク間通信と、送信・受信・待ち時間の基本

Queue(キュー)とは?

Queueは、FreeRTOSでタスク間や割り込みとタスクの間でデータを受け渡すために利用できる仕組みです。

たとえば、

Sensor Task
     ↓
   データ
     ↓
   Queue
     ↓
   データ
     ↓
Display Task

という構成にできます。

送信側はQueueへデータを入れ、受信側はQueueからデータを取り出します。

FreeRTOSがQueueの管理やタスクの待ち合わせを行うため、自分で単純な共有配列を用意する場合と比べて、タスク間通信を整理しやすくなります。

QueueはFIFOで動作する

FreeRTOSのQueueでは通常、先に入れたデータから先に取り出すFIFO(First In, First Out)方式で利用します。

日本語では「先入れ先出し」と呼ばれます。

送信

A → B → C → D
        ↓
      Queue


Queue

[A][B][C][D]


受信

A → B → C → D

A、B、C、Dの順番でQueueへ入れた場合、基本的にはA、B、C、Dの順番で取り出します。

なお、FreeRTOSにはQueueの前方へデータを入れるAPIなどもありますが、まずは通常のFIFOとして理解しておけばよいでしょう。

Queueにはデータそのものがコピーされる

FreeRTOSのQueueを理解するうえで重要なのが、通常のQueueでは送信したデータがQueue内部へコピーされるという点です。

int value = 100;

xQueueSend(
    queue,
    &value,
    portMAX_DELAY
);

ここでは&valueを渡していますが、「valueへのポインタそのものをQueueへ保存する」という意味ではありません。

Queue作成時に指定したサイズ分のデータが、Queueの保存領域へコピーされます。

value
 100
  ↓
xQueueSend()
  ↓
[ 100 ] ← Queue内部へコピー

受信時にもQueueから受信先へデータがコピーされます。

Queueを作成するxQueueCreate()

FreeRTOSでQueueを使用するには、まずQueueを作成します。

QueueHandle_t queue;

queue = xQueueCreate(
    10,
    sizeof(int)
);

xQueueCreate()の主な引数は次の2つです。

引数意味
10Queueへ保存できる要素数
sizeof(int)1要素あたりのデータサイズ

この例では、int型のデータを最大10個保存できるQueueを作っています。

QueueHandle_tとは?

xQueueCreate()で作成したQueueを操作するために使用するのがQueueHandle_tです。

QueueHandle_t sensorQueue;

このハンドルをxQueueSend()やxQueueReceive()へ渡すことで、どのQueueを操作するのか指定します。

sensorQueue
      ↓
FreeRTOSが管理しているQueue
      ↓
送信 / 受信

Queueの作成に失敗することもある

xQueueCreate()はQueueの作成に失敗するとNULLを返します。

そのため実際のプログラムでは、作成できたことを確認しておくと安全です。

sensorQueue = xQueueCreate(
    10,
    sizeof(float)
);

if (sensorQueue == NULL) {

    Serial.println("Queue create failed");

    return;
}

xQueueSend()でデータを送信する

作成したQueueへデータを送信するには、xQueueSend()を使用できます。

int data = 100;

xQueueSend(
    queue,
    &data,
    portMAX_DELAY
);

主な引数は次のようになります。

引数意味
queue送信先のQueue
&data送信するデータが置かれている場所
portMAX_DELAYQueueに空きがない場合の待ち時間

Queueが満杯だったらどうなる?

Queueには保存できる要素数の上限があります。

Queueサイズ = 4

[A][B][C][D]

      ↓

満杯

この状態でさらにデータを送ろうとした場合の動作は、xQueueSend()に指定する待ち時間によって変わります。

待ち時間0ならすぐ戻る

xQueueSend(
    queue,
    &data,
    0
);

待ち時間を0にすると、Queueが満杯の場合は空きができるまで待たず、その場で結果を返します。

そのため、送信できなかった場合の処理も考えておく必要があります。

if (xQueueSend(queue, &data, 0) != pdPASS) {

    Serial.println("Queue Full");
}

指定した時間だけ待つこともできる

一定時間だけ空きを待つこともできます。

xQueueSend(
    queue,
    &data,
    pdMS_TO_TICKS(100)
);

この場合、Queueに空きがなければ最大100ms待ちます。

待っている間、タスクはBlocked状態になり、CPUは別のReady状態のタスクを実行できます。

portMAX_DELAYで待つ

xQueueSend(
    queue,
    &data,
    portMAX_DELAY
);

portMAX_DELAYを指定すると、FreeRTOSの設定などの条件のもとで、送信可能になるまで非常に長い時間、実質的に無期限の待ちとして利用できます。

「必ず無限時間待つ」と単純に覚えるより、使用しているFreeRTOSの設定を確認したうえで利用するのが適切です。

xQueueReceive()でデータを受信する

Queueからデータを取り出すにはxQueueReceive()を使用します。

int data;

xQueueReceive(
    queue,
    &data,
    portMAX_DELAY
);

Queueから取り出されたデータがdataへコピーされます。

Queueが空の場合はどうなる?

受信するデータがない場合も、指定した待ち時間によって動作が変わります。

Queue

[ ][ ][ ][ ]

     ↓

データなし

たとえば、

xQueueReceive(
    queue,
    &data,
    portMAX_DELAY
);

とすると、受信できるデータが来るまでタスクを待機させる使い方ができます。

Display Task
      ↓
xQueueReceive()
      ↓
Queueが空
      ↓
Blocked
      ↓
Sensor Taskがデータ送信
      ↓
受信タスクがReady
      ↓
データを処理

ポーリングし続けなくてもよい

Queueの大きな利点の1つが、データが来るまでタスクをBlocked状態にできることです。

たとえば次のように何度も確認し続ける必要はありません。

while (1) {

    if (dataAvailable) {

        // 処理
    }
}

Queueを使えば、

xQueueReceive(
    queue,
    &data,
    portMAX_DELAY
);

として、データが届くまでそのタスクを待機させられます。

CPU時間を使って「データ来た?まだ?来た?」と繰り返し確認する必要がないわけです。

センサータスクから表示タスクへデータを渡す

具体例として、Sensor Taskで取得した温度をDisplay Taskへ送ってみましょう。

Sensor Task
     ↓
温度測定
     ↓
   Queue
     ↓
Display Task
     ↓
温度表示

まずQueueを用意します。

QueueHandle_t temperatureQueue;

Sensor Task

void sensorTask(void *pvParameters)
{
    float temperature;

    while (1) {

        temperature = readTemperature();

        xQueueSend(
            temperatureQueue,
            &temperature,
            portMAX_DELAY
        );

        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

1秒ごとに温度を取得し、Queueへ送信しています。

Display Task

void displayTask(void *pvParameters)
{
    float temperature;

    while (1) {

        if (xQueueReceive(
                temperatureQueue,
                &temperature,
                portMAX_DELAY
            ) == pdTRUE) {

            Serial.print("Temperature: ");
            Serial.println(temperature);
        }
    }
}

Display Taskは、温度データが届くまでBlocked状態になります。

Sensor Taskから温度が送信されるとデータを受信し、シリアルモニターへ表示します。

KUMITATE-C3でQueueを試してみよう

KUMITATE-C3でも、FreeRTOSのQueueを試すことができます。

まずはセンサーを接続しなくても確認できるように、送信タスクで数値を1ずつ増やし、受信タスクでシリアルモニターへ表示してみましょう。

QueueHandle_t dataQueue;


void senderTask(void *pvParameters)
{
    int value = 0;

    while (1) {

        value++;

        if (xQueueSend(
                dataQueue,
                &value,
                pdMS_TO_TICKS(100)
            ) == pdPASS) {

            Serial.print("Send: ");
            Serial.println(value);
        }

        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}


void receiverTask(void *pvParameters)
{
    int value;

    while (1) {

        if (xQueueReceive(
                dataQueue,
                &value,
                portMAX_DELAY
            ) == pdTRUE) {

            Serial.print("Receive: ");
            Serial.println(value);
        }
    }
}


void setup()
{
    Serial.begin(115200);

    dataQueue = xQueueCreate(
        5,
        sizeof(int)
    );

    if (dataQueue == NULL) {

        Serial.println("Queue create failed");

        return;
    }

    xTaskCreate(
        senderTask,
        "Sender",
        2048,
        NULL,
        1,
        NULL
    );

    xTaskCreate(
        receiverTask,
        "Receiver",
        2048,
        NULL,
        1,
        NULL
    );
}


void loop()
{
    vTaskDelay(pdMS_TO_TICKS(1000));
}

実行すると、送信タスクが作った値がQueueを通して受信タスクへ渡されます。

Sender Task
   ↓
   1
   ↓
 Queue
   ↓
   1
   ↓
Receiver Task


Sender Task
   ↓
   2
   ↓
 Queue
   ↓
   2
   ↓
Receiver Task

構造体をQueueで送ることもできる

Queueへ保存できるのはintやfloatだけではありません。

構造体を使えば、複数のデータをまとめて送ることもできます。

struct SensorData {

    float temperature;
    float humidity;
    uint32_t timestamp;
};

Queueは次のように作成できます。

QueueHandle_t sensorQueue;

sensorQueue = xQueueCreate(
    10,
    sizeof(SensorData)
);

そして、

SensorData data;

data.temperature = 25.4f;
data.humidity = 48.2f;
data.timestamp = millis();

xQueueSend(
    sensorQueue,
    &data,
    portMAX_DELAY
);

とすれば、温度・湿度・時刻を1つのデータとして別のタスクへ渡せます。

構造体については「構造体とは?C/C++のstructとメンバ・ポインタの使い方を初心者向けに解説」も参照してください。

大きなデータをQueueへ送る場合は注意

Queueではデータをコピーするため、非常に大きな構造体を何度も送信するとコピー量やメモリ使用量が増えます。

小さなデータ
 ↓
コピー負荷 小


大きなデータ
 ↓
コピー負荷 大

大きなバッファなどを扱う場合は、データそのものではなくポインタをQueueへ渡す設計が使われることもあります。

ただしポインタを渡す場合は、そのメモリを誰が所有し、誰がいつ解放・再利用するのかを明確にする必要があります。

ポインタについては「ポインタとは?アドレス・&・*の意味を初心者向けに解説」も確認してください。

リングバッファとQueueは何が違う?

Queueを見ると、以前学習したリングバッファとよく似ていると感じるかもしれません。

リングバッファFreeRTOS Queue
データ保持できるできる
FIFO実装可能基本的な利用方法
自分で管理することが多いRTOSが管理
タスクの待機自分で設計RTOSと連携
タスク間通信同期設計が必要用途として用意されている

リングバッファはデータ構造そのものの考え方です。

FreeRTOS Queueは、データ保持に加えてRTOSのタスクスケジューリングや待ち合わせと連携した通信機能として利用できます。

リングバッファについては「リングバッファとは?仕組みとUART受信での使い方を初心者向けに解説」で詳しく解説しています。

グローバル変数とQueueは何が違う?

タスク間で値を渡すだけなら、グローバル変数でもできるように見えます。

float temperature;

しかしグローバル変数では、複数のタスクが同じ変数へアクセスするタイミングや、値が更新されたことをどう通知するかを自分で設計する必要があります。

また、1つの変数では通常「最新値」は保持できますが、過去に届いた複数のデータを順番に保持することはできません。

共有変数

25.1
 ↓
25.2
 ↓
25.3

古い値は上書き


Queue

[25.1][25.2][25.3]

順番に保持

ただし、「常に最新値だけあればよい」という用途ではQueue以外の設計が適している場合もあります。用途によって使い分けることが重要です。

割り込みからQueueへ送ることもできる

FreeRTOSでは、割り込み処理からタスクへ情報を渡すためにQueueを利用することもできます。

GPIO / UART
    ↓
  割り込み
    ↓
   Queue
    ↓
待機しているTask
    ↓
本格的な処理

ただし、割り込みから通常のxQueueSend()をそのまま使用するのではなく、割り込み用APIを使用します。

xQueueSendFromISR(
    queue,
    &data,
    &higherPriorityTaskWoken
);

FreeRTOSにはxQueueSendFromISR()など、ISR(Interrupt Service Routine)から利用するためのAPIが用意されています。

割り込み処理では時間のかかる処理を行わず、必要な情報だけをQueueなどでタスクへ通知し、本格的な処理をタスク側で行う設計がよく使われます。

割り込みについては「割り込みとは?マイコンがイベントをすぐ処理できる仕組みを初心者向けに解説」も参照してください。

Queueとステートマシンを組み合わせる

Queueはステートマシンとも組み合わせられます。

Button Task
    ↓
STARTイベント
    ↓
   Queue
    ↓
Control Task
    ↓
ステートマシン
    ↓
IDLE → RUN

このように、Queueで単なる数値ではなくイベントを送ることもできます。

enum Event {
    EVENT_START,
    EVENT_STOP,
    EVENT_ERROR
};

通信・ボタン・センサーなど複数の場所から発生したイベントを1つの制御タスクへ集め、ステートマシンで処理する構成も考えられます。

ステートマシンについては「ステートマシン(状態遷移)とは?組み込み開発での設計と実装方法を初心者向けに解説」も参照してください。

Queueを使うメリット

  • タスク間でデータを受け渡せる
  • データを順番に保持できる
  • 送信側と受信側の処理を分離しやすい
  • データが来るまで受信タスクをBlocked状態にできる
  • Queueが満杯なら送信側を待たせることもできる
  • 構造体なども送信できる
  • 割り込みからタスクへの通知にも利用できる

Queueを使うときの注意点

  • Queueの長さを適切に決める
  • 1要素のデータサイズを正しく指定する
  • Queueが満杯になった場合の処理を考える
  • 受信側が遅すぎないか確認する
  • 待ち時間を適切に設定する
  • 大きなデータを大量にコピーすると負荷が増える
  • ポインタを送る場合はデータの寿命と所有権を管理する
  • ISRからはFromISR系のAPIを使う

Queueのサイズはどう決める?

Queueの要素数を大きくすれば、多くのデータを一時的に保持できます。

しかし、その分だけメモリも必要になります。

Queueが小さすぎる
      ↓
すぐ満杯になる


Queueが大きすぎる
      ↓
RAMを多く消費する

そのため、

  • 送信データが発生する頻度
  • 受信タスクの処理速度
  • 一時的に何件までデータが集中する可能性があるか
  • 1要素のデータサイズ
  • 使用可能なRAM

などから決めます。

Queueが満杯になること自体が重要な情報

実際のシステムでは、「Queueを大きくすれば問題解決」とは限りません。

Queueが継続的に満杯になる場合、

送信速度
   ↓
100データ/秒

受信処理
   ↓
50データ/秒

のように、生産する速度より消費する速度が遅い可能性があります。

この場合、Queueを大きくすると満杯になるまでの時間を延ばせますが、長期的には追いつきません。

Queueがあふれる場合は、Queueサイズだけでなく、送信側と受信側の処理速度そのものを確認することが重要です。

QueueとSemaphore・Mutexの違い

FreeRTOSを学んでいると、QueueのほかにSemaphoreやMutexという言葉も登場します。

仕組み主な目的
Queueデータの受け渡し
Semaphoreイベント通知や資源数の管理など
Mutex共有リソースの排他制御

内部実装やAPIには関連する部分がありますが、学習段階では「Queueはデータを渡すもの」と考えると整理しやすいでしょう。

これまで学んだ内容とQueueの関係

Queueまで進むと、これまで学習してきた内容が実際のRTOSプログラムとしてつながってきます。

センサー
   ↓
GPIO / I2C / SPI
   ↓
Sensor Task
   ↓
構造体
   ↓
Queue
   ↓
Control Task
   ↓
ステートマシン
   ↓
Motor / OLED / 通信


割り込み
   ↓
FromISR API
   ↓
Queue
   ↓
Task

単に「FreeRTOSの関数を覚える」のではなく、これまで学習したデータ型・ポインタ・構造体・割り込み・リングバッファ・ステートマシンなどが、どこで使われるのかを意識すると理解しやすくなります。

まとめ

Queueは、FreeRTOSでタスク間などのデータ受け渡しに利用できる重要な仕組みです。

  • Queueはタスク間でデータを受け渡すために利用できる
  • 基本的にはFIFOとして利用する
  • xQueueCreate()でQueueを作成する
  • xQueueSend()でデータを送信する
  • xQueueReceive()でデータを受信する
  • Queueでは通常データそのものがコピーされる
  • Queueが満杯・空の場合はタスクを待機させることができる
  • 待機中のタスクはBlocked状態になる
  • 構造体を使って複数の値をまとめて送信できる
  • 大きなデータではコピー量やメモリ使用量に注意する
  • 割り込みからはFromISR系APIを使用する
  • Queueはリングバッファと似ているが、RTOSの待ち合わせ機能と連携できる

RTOSを使う目的は、単に処理を複数のタスクへ分割することではありません。

分割したタスク同士をどのように安全に協調させるかまで考える必要があります。

Queueはその第一歩です。センサータスクで取得したデータをQueueへ入れ、別のタスクで受信する小さなプログラムを作ってみると、RTOSのタスク間通信を実感しやすいでしょう。

KUMITATE

読むだけでなく、
実際に動かして学びませんか?

クミタテは、組み込み開発や電子工作を 実践しながら学べる学習プラットフォームです。 ESP32を使ったプログラミングから、電子回路、センサー、 通信、基板設計まで、自分のペースで学習できます。

  • 無料で学べる実践的な教材を掲載
  • すぐに試せるサンプルプログラム付き
  • Googleアカウントですぐに登録可能
クミタテに無料登録する
組み込み基礎
スポンサーリンク