この記事でわかること
- セマフォ(Semaphore)とは何か
- セマフォを使わないと何が起きるか
- セマフォを使うメリット
- Binary Semaphore と Mutex の違い
この記事に掲載しているコードは Arduino for ESP32 + FreeRTOS でそのまま動作します。タスクAとタスクBを同時に動かし、UARTログ混在 → セマフォ改善を体験できます。
目次
- FreeRTOS Semaphore(セマフォ)とは?役割と用途
- なぜ FreeRTOS でセマフォが必要か?(排他制御の重要性)
- セマフォ未使用の実装例
- Binary Semaphoreで改善する
- FreeRTOS Binary Semaphore の限界
- 「優先度逆転」という現象について
- FreeRTOS Mutexが優先度逆転を解決する理由
- FreeRTOSで Binary Semaphore と Mutex を使い分ける基準
- まとめ
1. FreeRTOS Semaphore(セマフォ)とは?役割と用途
FreeRTOSの Semaphore(セマフォ) は、複数タスクが1つの共有リソースを同時に使わないようにする仕組みです。
共有リソースとは…
- UART(シリアル出力)
- I2Cセンサー
- SPIバス
- SDカード書き込み
- WiFi送信処理
- 共有メモリ
など、同時アクセスすると壊れる可能性があるものです。
2. なぜ FreeRTOS でセマフォが必要か?(排他制御の重要性)
複数タスクが同時に Serial.print() などを実行すると、ログが混在したりデータが破損します。
そのため、リソース数を制限しデータの破損を防ぐ必要があります。
3. セマフォ未使用の実装例
Serial.printでタスクA・Bに以下のような表示をするよう実装します。
タスクA:[A] START END
タスクB:[B] BEGIN DONE
まずは、セマフォを使わずに実装してみます。
#include <Arduino.h>
// セマフォなし
void TaskA(void *pvParameters) {
for (;;) {
Serial.print("[A] START ");
delayMicroseconds(random(500, 2000));
Serial.println(" END");
vTaskDelay(pdMS_TO_TICKS(200));
}
}
void TaskB(void *pvParameters) {
for (;;) {
Serial.print("[B] BEGIN ");
delayMicroseconds(random(500, 2000));
Serial.println(" DONE");
vTaskDelay(pdMS_TO_TICKS(300));
}
}
void setup() {
Serial.begin(115200);
randomSeed(esp_random());
xTaskCreate(TaskA, "TaskA", 2048, NULL, 1, NULL);
xTaskCreate(TaskB, "TaskB", 2048, NULL, 1, NULL);
}
void loop() {
vTaskDelay(portMAX_DELAY);
}
以下実際のログになります。
タスクAとタスクBのログが混ざっており、正しく表示されていないことがわかります。

今は正しく表示されないだけで済みますが、実際に通信やログが正しく機能してくれないとなるとIoTとして致命的です。
4. Binary Semaphoreで改善する
それではFreeRTOSのセマフォを使用し、どちらかのタスクがSerial.printを行う際は、もう片方タスクはSerial.printを行わないように修正します。
以下、修正コードになります。
#include <Arduino.h>
SemaphoreHandle_t uartLock;
void TaskA(void *pvParameters) {
for (;;) {
if (xSemaphoreTake(uartLock, portMAX_DELAY) == pdTRUE) {
Serial.print("[A] START ");
delayMicroseconds(random(500, 2000));
Serial.println(" END");
xSemaphoreGive(uartLock);
}
vTaskDelay(pdMS_TO_TICKS(200));
}
}
void TaskB(void *pvParameters) {
for (;;) {
if (xSemaphoreTake(uartLock, portMAX_DELAY) == pdTRUE) {
Serial.print("[B] BEGIN ");
delayMicroseconds(random(500, 2000));
Serial.println(" DONE");
xSemaphoreGive(uartLock);
}
vTaskDelay(pdMS_TO_TICKS(300));
}
}
void setup() {
Serial.begin(115200);
randomSeed(esp_random());
uartLock = xSemaphoreCreateBinary();
xSemaphoreGive(uartLock);
xTaskCreate(TaskA, "TaskA", 2048, NULL, 1, NULL);
xTaskCreate(TaskB, "TaskB", 2048, NULL, 1, NULL);
}
void loop() {
vTaskDelay(portMAX_DELAY);
}
それでは、修正された実装のログはどうでしょうか

タスクAとタスクBのログが混ざらず、正しく表示されていることがわかります。
これが「排他制御」になります。
5. FreeRTOS Binary Semaphore の限界
Binary Semaphoreは
- 軽い
- シンプル
- ISR通知にも使える
…というメリットがありますが、
ある問題を解決できません。
6. 「優先度逆転」という現象について
FreeRTOSはタスクごとに優先度を持っています。
本来なら、
高優先度タスク(H)
↓
中優先度タスク(M)
↓
低優先度タスク(L)
の順で実行されるべきです。
しかし Binary Semaphoreだけを使うと、こういう問題が起きます:
低優先度タスク L がリソースを使っている(ロック保持中)
↓
高優先度タスク H が「使いたい!」と待つ
↓
その間に中優先度タスク M がCPUを独占
↓
H はずっと待たされる……
結果的に低優先度タスク L がリソースを独占することで、
高優先度タスク H が実行できなくなってしまいます。
これが 優先度逆転(Priority Inversion)。
優先度逆転が起きると、リアルタイム性が重要な IoT(MQTT送信・ログ収集・センサー処理など)が遅延し、結果としてデータ欠損や通信タイムアウトにつながることがあります。
7. FreeRTOS Mutexが優先度逆転を解決する理由
Mutexは Binary Semaphore で発生する優先度逆転を解決できます。
低優先度 L が鍵を保持中
↓
高優先度 H が待っている
このとき MutexはL の優先度を一時的に H と同じレベルに引き上げます。
- MよりもLが優先されるためすぐ処理を終わらせる
- セマフォのロックが解除される
- H が実行できる
優先度違いのタスク間で同じセマフォのリソースを使用する場合はMutexが有効であることが、なんとなくイメージできたと思います。
8. FreeRTOSで Binary Semaphore と Mutex を使い分ける基準
軽くて短い処理
Binary Semaphore
例:
- UARTの1行出力
- I2Cの単発読み取り
- ISR通知
優先度が絡む、長い処理、安全性が重要
Mutex
例:
- データ構造保護
- SDカード書き込み
- 長時間ロックする処理
- WiFi/MQTT送信
- ファイルシステム
9. まとめ
今回は FreeRTOS の Semaphore(セマフォ) について紹介しました。
Semaphoreは排他制御の仕組み
- UART・I2C・SDカードなど共有リソースを守る
- 同時アクセスによるデータ破損を防ぐ
Binary Semaphoreは軽量で簡単
- 短い排他処理、ISR通知に向いている
Mutexは優先度逆転を防げる
- “所有者”の概念を持ち、低優先度タスクがロックしていても高優先度を守る
- リアルタイム性が必要なIoTに最適
使い分けの基準
- 短時間排他 → Binary Semaphore
- 優先度・長時間処理 → Mutex
- SDカード、データ構造、ネットワーク通信は Mutex推奨
今後も FreeRTOS の機能について解説していきます。
次は、EventGroup に関して、実装を交えて解説したいと思います。
FreeRTOS × ESP32 記事シリーズ
ESP-IDFで動くコードやセマフォの関数の使い方はこちらに載せていますので、良ければご覧ください