Contents
この記事でわかること
- StreamBufferで何ができるか(バイト単位の連続通信)
- Queue / MessageBuffer との決定的な違い
- ESP32(ESP-IDF)での動く最小サンプル
- 主要APIの引数・戻り値とTrigger Levelの役割
- 設計のコツ(1 writer/1 reader・オーバーヘッド・詰まり対策)
こんな方におすすめ
- UARTやSPIなどの「境界のない生バイト列」を効率よくタスクに渡したい
- MessageBufferを使っているが、実はメッセージの区切り(パケット管理)を必要としていない
- 「一定量溜まってからタスクを起こしたい」というバッファリング制御をしたい
- メモリ消費を極限まで抑えてタスク間通信をしたい
StreamBufferとは
FreeRTOSの StreamBuffer は、タスク間で「連続したバイト列」を高速に受け渡しする仕組みです。
- 送信側: 好きなタイミングで、好きなバイト数を書き込む。
- 受信側: 「指定したバイト数(トリガーレベル)」が溜まるまで待機し、一気に読み出す。
- 境界なし: MessageBufferと違い、「どこまでが1回分の送信か」という情報は保持されません。
Queue / StreamBuffer / MessageBuffer の違い
設計判断に迷ったらこの比較表を活用してください。
| 手段 | 何が得意? | 境界 | サイズ | 典型用途 |
|---|---|---|---|---|
| Queue | 固定サイズの“要素”をFIFO | あり(要素単位) | 固定(item size) | コマンド/構造体/小さな値 |
| StreamBuffer | バイト“ストリーム” | なし | 可変(読み出し側次第) | UARTの生バイト、連続データ |
| MessageBuffer | 可変長“メッセージ” | あり | 可変(メッセージ単位) | フレーム(JSON/行/ヘッダ付き) |
ポイント: StreamBufferは、データごとに「長さ情報」を付与しません。そのため、1バイトでも無駄にしたくないストリーム処理に最適です。
内部イメージと「Trigger Level」
StreamBufferを理解する上で最も重要なのが Trigger Level(トリガーレベル) です。
- 内部構造: シンプルなリングバッファです。
- オーバーヘッド: MessageBufferのような「各データごとのヘッダ(通常4〜8バイト)」がありません。
- トリガーレベル: 「バッファに何バイト溜まったら、受信タスクを起床させるか」という閾値です。
- 例:10バイトに設定すると、1〜9バイト書き込まれた時点では受信側はBlock(スリープ)し続け、10バイト目が入った瞬間に動き出します。
【最小サンプル】生バイトを送って、溜まったら受け取る(ESP32)
やりたいこと
SenderTask:A~Zまで1バイトずつ送る。ReceiverTask:10バイト溜まるまで待って、まとめて処理する。
#include <cstdio>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/stream_buffer.h"
static StreamBufferHandle_t s_buf;
static void SenderTask(void *pv)
{
char data = 'A';
for (;;) {
// 1バイトずつ送信
xStreamBufferSend(s_buf, &data, 1, portMAX_DELAY);
// A -> Z を繰り返す
data = (data == 'Z') ? 'A' : data + 1;
vTaskDelay(pdMS_TO_TICKS(100)); // 0.1秒おきに1バイト送る
}
}
static void ReceiverTask(void *pv)
{
uint8_t rx[20];
for (;;) {
// トリガーレベル(10バイト)溜まるまでここでブロック
// 起床後は、バッファにある分を最大 sizeof(rx) まで一気に読み出す
size_t n = xStreamBufferReceive(s_buf, rx, sizeof(rx), portMAX_DELAY);
if (n > 0) {
printf("[Receiver] Read %u bytes: ", (unsigned)n);
for(int i=0; i<n; i++) putchar(rx[i]);
printf("\n");
}
}
}
extern "C" void app_main(void)
{
// 第1引数:バッファ容量(100バイト)
// 第2引数:トリガーレベル(10バイト溜まったら起床)
s_buf = xStreamBufferCreate(100, 10);
if (s_buf == NULL) {
printf("Create Failed\n");
return;
}
xTaskCreate(SenderTask, "Sender", 2048, NULL, 5, NULL);
xTaskCreate(ReceiverTask, "Receiver", 2048, NULL, 5, NULL);
}
以下出力結果です。
10文字ずつ出力されていることがわかります。

主要API解説
xStreamBufferCreate()(作成する)
StreamBufferHandle_t sb = xStreamBufferCreate(1024, 10);
- 役割:StreamBuffer を作成する(バイトストリーム用のバッファ)
- 引数
- 第1引数:xBufferSizeBytes(確保するバッファ容量 [byte])
- 例:1024
- 第2引数:xTriggerLevelBytes(受信タスクが起床する閾値 [byte])
- 例:10(10バイト溜まるまで受信側はブロックされる)
- 第1引数:xBufferSizeBytes(確保するバッファ容量 [byte])
- 戻り値
- 成功:StreamBufferHandle_t(バッファのハンドル)
- 失敗:NULL(メモリ不足など)
xStreamBufferSend()(送信する)
size_t sent = xStreamBufferSend(sb, txData, dataLen, portMAX_DELAY);
- 役割:StreamBuffer にデータを書き込む
- 引数
- 第1引数:対象の StreamBuffer ハンドル
- 第2引数:送信データ先頭ポインタ
- 第3引数:送信バイト数
- 第4引数:待ち時間(Tick)
- 0:待たない、portMAX_DELAY:空きが出るまで待つ
- 戻り値
- 成功:実際に書き込めたバイト数
- 失敗:0(タイムアウト、空き不足など)
xStreamBufferReceive()(受信する)
size_t rxLen = xStreamBufferReceive(sb, rxBuffer, sizeof(rxBuffer), portMAX_DELAY);
- 役割:StreamBuffer からデータを読み出す
- 引数
- 第1引数:対象の StreamBuffer ハンドル
- 第2引数:受信バッファ先頭ポインタ
- 第3引数:受信バッファサイズ(最大で受け取れるサイズ)
- 第4引数:待ち時間(Tick)
- 戻り値
- 成功:実際に読み出せたバイト数
- 失敗:0(未到着、タイムアウトなど)
xStreamBufferSendFromISR()(ISRから送信する)
BaseType_t hpw = pdFALSE;
size_t sent = xStreamBufferSendFromISR(sb, txData, dataLen, &hpw);
if (hpw) portYIELD_FROM_ISR();
- 役割:割り込み(ISR)からデータを書き込む
- 引数
- 第1引数:対象の StreamBuffer ハンドル
- 第2引数:送信データ先頭ポインタ
- 第3引数:送信バイト数
- 第4引数:pxHigherPriorityTaskWoken(高優先度タスク起床フラグのポインタ)
- 戻り値
- 成功:実際に書き込めたバイト数
- 失敗:0(空き不足など)
xStreamBufferReceiveFromISR()(ISRから受信する)
BaseType_t hpw = pdFALSE;
size_t rxLen = xStreamBufferReceiveFromISR(sb, rxBuffer, sizeof(rxBuffer), &hpw);
if (hpw) portYIELD_FROM_ISR();
- 役割:割り込み(ISR)からデータを読み出す
- 引数
- 第1引数:対象の StreamBuffer ハンドル
- 第2引数:受信バッファ先頭ポインタ
- 第3引数:受信バッファサイズ
- 第4引数:pxHigherPriorityTaskWoken(高優先度タスク起床フラグのポインタ)
- 戻り値
- 成功:実際に読み出せたバイト数
- 失敗:0(未到着など)
設計のコツと注意点
注意1:1 writer / 1 reader 原提
StreamBufferは、複数タスクからの同時書き込み・読み出しに対して、内部でセマフォ等による排他制御を行っていません。
- 黄金パターン: UART受信ISR (1 writer) → 処理タスク (1 reader)
- もし複数タスクから送りたい場合は、Mutexでラップするか、送信専用タスクを用意してください。
注意2:トリガーレベルの「罠」
「100バイト溜まったら処理したい」とトリガーレベルを100に設定したが、送信側が50バイト送ったきり沈黙した場合、受信側は永遠に待ち続けます。
- 対策1:
xTicksToWaitを有限にし、タイムアウト時に残りを処理する。 - 対策2:
xStreamBufferSetTriggerLevel()を使って、送信側が「これで終わり」と分かったタイミングで強制的にトリガーを下げる。
注意3:MessageBufferへの移行は容易
StreamBufferを使ってみて「やっぱりデータの区切り(パケット境界)が欲しいな」となったら、APIを xMessageBuffer... に書き換えるだけでほぼ対応可能です。
内部的には、MessageBufferは「データの先頭に長さを書いてStreamBufferに投げている」というラッパー構造だからです。
まとめ
- StreamBufferは、境界を気にせず「生データ」を高速に流すための仕組み。
- メモリ効率が最高で、管理用ヘッダの消費がない。
- トリガーレベルを調整することで、不必要なコンテキストスイッチ(タスクの起床)を減らし、CPU負荷を下げられる。

コメント