ESP32でFreeRTOSを使い始めると、最初にぶつかりやすいのが「共有リソースの同時アクセス」です。
たとえば 複数タスクが同時にUART出力すると、ログが混ざって読めなくなります。
この記事では、FreeRTOSの Semaphore(セマフォ) を使って「混在→改善」を体験しながら、Binary SemaphoreとMutexの使い分けまで一気に理解できるようにまとめます。
この記事のコードは ESP-IDF + FreeRTOS でそのまま動作します。
タスクA/Bを同時に動かし、UARTログ混在 → セマフォ改善を再現できます。
Contents
この記事でわかること
- セマフォ(Semaphore)とは何か
- セマフォを使わないと何が起きるか(ログ混在の再現)
- セマフォを使うメリット(排他制御)
- Binary Semaphore と Mutex の違い(優先度逆転と優先度継承)
こんな方におすすめ
- ESP32でFreeRTOSを使っていて、ログや通信の挙動が不安定
- UART / I2C / SPI / SDカードなど「共有リソース」の扱いが不安
- Binary SemaphoreとMutexの違いが曖昧で、選べない
FreeRTOS Semaphore(セマフォ)とは?役割と用途
FreeRTOSの Semaphore(セマフォ) は、複数タスクが 同じ共有リソースを同時に使わない ようにする仕組みです。
共有リソースの例:
- UART(シリアル出力)
- I2Cセンサー
- SPIバス
- SDカード書き込み
- WiFi送信処理
- 共有メモリ / 共有データ構造
これらは同時アクセスすると、データ破損や処理の破綻につながります。
なぜ FreeRTOS でセマフォが必要か?(排他制御の重要性)
複数タスクが同時に printf() を実行すると、出力が混ざって読めなくなります。
ログが読めないだけならまだ軽症ですが、実際のIoTでは以下のような深刻な問題に繋がります。
- 通信フレームが壊れて送受信不能
- SDカード書き込みが壊れてファイル破損
- センサー読み取りが壊れて異常値連発
- タイムアウトやリトライが増えて不安定化
そこで、同時に使えるタスクを1つに制限する(排他制御)のが基本です。
セマフォ未使用の実装例(ログ混在を再現)
タスクAとタスクBが、それぞれ以下を出すとします。
- タスクA:
[A] START END - タスクB:
[B] BEGIN DONE
まずは セマフォ無しで実装します。
#include <cstdio>
#include <random>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dist(500, 2000);
// セマフォなし
void TaskA(void *pvParameters) {
for (;;) {
printf("[A] START ");
vTaskDelay(pdMS_TO_TICKS(dist(gen))); // ランダムな時間待つ
printf(" END\n");
vTaskDelay(pdMS_TO_TICKS(200));
}
}
void TaskB(void *pvParameters) {
for (;;) {
printf("[B] BEGIN ");
vTaskDelay(pdMS_TO_TICKS(dist(gen))); // ランダムな時間待つ
printf(" DONE\n");
vTaskDelay(pdMS_TO_TICKS(300));
}
}
extern "C" void app_main(void)
{
xTaskCreate(TaskA, "TaskA", 2048, NULL, 1, NULL);
xTaskCreate(TaskB, "TaskB", 2048, NULL, 1, NULL);
}
この状態では、出力タイミングが噛み合うと ログが混在します。
例:printf("[A] START "); と printf(" END\n"); の間に他タスクが割り込む → 混ざる
実際にログが混在していることが、確認できます。

Binary Semaphoreで改善する(排他制御)
次に、どちらかのタスクがprintf出力している間は、もう片方は待つようにします。
これが 排他制御です。
#include <cstdio>
#include <random>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
SemaphoreHandle_t uartLock;
std::random_device rd;
std::mt19937 gen(rd());
std::uniform_int_distribution<> dist(500, 2000);
// セマフォあり
void TaskA(void *pvParameters) {
for (;;) {
if (xSemaphoreTake(uartLock, portMAX_DELAY) == pdTRUE) {
printf("[A] START ");
vTaskDelay(pdMS_TO_TICKS(dist(gen))); // ランダムな時間待つ
printf(" END\n");
xSemaphoreGive(uartLock);
}
vTaskDelay(pdMS_TO_TICKS(200));
}
}
void TaskB(void *pvParameters) {
for (;;) {
if (xSemaphoreTake(uartLock, portMAX_DELAY) == pdTRUE) {
printf("[B] BEGIN ");
vTaskDelay(pdMS_TO_TICKS(dist(gen))); // ランダムな時間待つ
printf(" DONE\n");
xSemaphoreGive(uartLock);
}
vTaskDelay(pdMS_TO_TICKS(300));
}
}
extern "C" void app_main(void)
{
uartLock = xSemaphoreCreateBinary();
if (uartLock == NULL) {
printf("xSemaphoreCreateBinary failed\n");
return;
}
xSemaphoreGive(uartLock);
xTaskCreate(TaskA, "TaskA", 2048, NULL, 1, NULL);
xTaskCreate(TaskB, "TaskB", 2048, NULL, 1, NULL);
}
これでログは混ざりなくなり、「排他制御」が体感できます。
実際にログが混在せず正しく出力されることが確認できます。

主要なSemaphore関数(役割・引数・戻り値を最短で理解)
ここでは、この記事で使っている FreeRTOSのセマフォAPIを「何をする関数か」「引数は何か」だけに絞って整理します。
まず結論として、セマフォ操作で覚えるのは次の3つです。
- 作る:
xSemaphoreCreateBinary()/xSemaphoreCreateMutex() - 取る(ロック):
xSemaphoreTake() - 返す(アンロック):
xSemaphoreGive()
xSemaphoreCreateBinary()(Binary Semaphoreを作る)
SemaphoreHandle_t sem = xSemaphoreCreateBinary();
- 役割:Binary Semaphore(0/1の“合図”や簡易ロック)を作る
- 戻り値:
- 成功:セマフォのハンドル(
SemaphoreHandle_t) - 失敗:
NULL(メモリ不足など)
- 成功:セマフォのハンドル(
注意:Binary Semaphoreは作った直後は「空(取れない状態)」です。
排他ロックとして使うなら、最初にxSemaphoreGive()で“空き”にする必要があります。
uartLock = xSemaphoreCreateBinary();
xSemaphoreGive(uartLock); // 最初に「空き」にする
xSemaphoreCreateMutex()(Mutexを作る)
SemaphoreHandle_t mtx = xSemaphoreCreateMutex();
- 役割:Mutex(排他制御専用のロック)を作る
- 戻り値:
- 成功:Mutexハンドル
- 失敗:
NULL
Mutexは 排他制御向きで、Binary Semaphoreと違い「優先度逆転」対策(優先度継承)が効くのが特徴です。
※「共有リソース保護」が目的なら、基本はMutexが第一候補です。
xSemaphoreTake()(取る/ロックする)
BaseType_t ok = xSemaphoreTake(uartLock, portMAX_DELAY);
- 役割:セマフォ(またはMutex)を取得する=ロックする
- 引数
- 第1引数:対象のセマフォ/Mutexハンドル
- 第2引数:待ち時間(Tick)
0:待たない(今取れなければ失敗)portMAX_DELAY:ずっと待つ(ブロック)pdMS_TO_TICKS(100):最大100ms待つ、など
- 戻り値
pdTRUE:取得成功(ロックできた)pdFALSE:取得失敗(タイムアウト等)
コツ:デバッグ中は
pdMS_TO_TICKS(1000)のように有限待ちにすると「詰まり」が見つけやすいです。
量産コードで“絶対に守るべき資源”ならportMAX_DELAYもよく使います。
xSemaphoreGive()(返す/アンロックする)
xSemaphoreGive(uartLock);
- 役割:セマフォ/Mutexを解放する=アンロックする
- 引数
- 第1引数:対象のセマフォ/Mutexハンドル
- 戻り値
- 多くの場合
pdTRUE/pdFALSE(失敗するケースは少ないですが、ハンドル不正など)
- 多くの場合
重要:Takeしたら必ずGiveする
Giveし忘れると、他タスクが永久に待ち続ける(デッドロック)原因になります。
よくある実装パターン(テンプレ)
パターン1:ロックしてから処理、終わったら解放(基本形)
if (xSemaphoreTake(uartLock, portMAX_DELAY) == pdTRUE) {
// ここから共有リソースを安全に使える
printf("hello");
// ここまで
xSemaphoreGive(uartLock);
}
パターン2:短い処理は「待たない」判断もあり(取りこぼし許容)
if (xSemaphoreTake(uartLock, 0) == pdTRUE) {
printf("log");
xSemaphoreGive(uartLock);
} else {
// 今は使えないので諦める(ログ捨てなど)
}
(補足)ISR(割り込み)から使う場合は別API
割り込みハンドラからセマフォを操作する場合、通常の xSemaphoreGive() は使わず、xSemaphoreGiveFromISR() など FromISR系を使います。
この記事はまず「タスク間の排他制御」に集中するため、ISR用途は別記事で扱います。
FreeRTOS Binary Semaphore の限界(優先度逆転)
Binary Semaphoreは以下のメリットがあります。
- 軽い
- シンプル
- ISR通知にも使える
ただし、ある問題を解決できません。
それが次の「優先度逆転」です。
優先度逆転(Priority Inversion)とは?
FreeRTOSはタスクごとに優先度を持ち、基本は高優先度が先に動きます。
本来の期待:
高優先度タスク(H)
↓
中優先度タスク(M)
↓
低優先度タスク(L)
しかし Binary Semaphore だけで排他すると、次の状況が起こり得ます。
低優先度タスク L がリソースを保持(ロック中)
↓
高優先度タスク H が待ちに入る(ロック待ち)
↓
その間に中優先度タスク M がCPUを使い続ける
↓
L が実行できずロックを解放できない
↓
結果として H がずっと待たされる
これが 優先度逆転(Priority Inversion) です。
リアルタイム性が重要なIoT(MQTT送信・ログ収集・センサー処理など)では、遅延が積み重なると
データ欠損や通信タイムアウトの原因になります。
FreeRTOS Mutexが優先度逆転を解決する理由(優先度継承)
Mutex(ミューテックス)は、Binary Semaphoreで起きる優先度逆転を緩和する仕組みを持ちます。
それが 優先度継承(Priority Inheritance) です。
状況:
低優先度 L がロック保持中
高優先度 H がロック待ち
このときMutexは、L の優先度を一時的に引き上げます。
- LがMより優先される
- Lが早く処理を終えてロック解放できる
- Hがすぐ実行できる
つまり「ロックを持っている低優先度が、すぐ解放できるように押し上げる」ことで、
高優先度が不当に待たされる状況を減らします。
Binary Semaphore と Mutex を使い分ける基準
結論から言うと、“排他”目的ならMutexが第一候補で、
Binary Semaphoreは「通知/合図」用途でも強い、という捉え方が実務では分かりやすいです。
使い分け早見表
| 目的 | 推奨 | 代表例 |
|---|---|---|
| 排他制御(共有リソース保護) | Mutex | 共有データ、SD、ネットワーク、長めのI/O |
| 短時間の排他(優先度差が小さい) | Binary Semaphoreでも可 | UARTの短い出力など |
| タスク間の「合図」「通知」 | Binary Semaphore | ISR→タスク通知、イベントの発火 |
Mutex版の書き方(コード差分はほぼ同じ)
UART保護に「排他」目的で使うなら、Mutexは次のように作れます。
uartLock = xSemaphoreCreateMutex();
使い方(Take/Give)はほぼ同じです。
まとめ
- セマフォは 共有リソースを守る排他制御の仕組み
UART・I2C・SPI・SDカードなど同時アクセスで壊れるものを保護する - Binary Semaphoreは軽量で簡単
短い排他や、ISR通知など「合図」にも使える - Mutexは 優先度逆転を緩和できる(優先度継承)
優先度が絡む/長時間ロックする可能性がある処理はMutexが安全
次回はEventGroupについて記載します。


コメント