FreeRTOSでは、複数のタスクを動かしながら、タスク同士でデータを渡したり、イベントの発生を知らせたりできます。
これまでの記事では、そのための仕組みとしてQueueやSemaphoreを紹介しました。
しかし、「ボタンが押された」「通信データを受信した」「処理が完了した」といった単純な通知を行うだけなら、もっと軽量な方法があります。
それがTask Notification(タスク通知)です。
Task A ↓ Task Notification ↓ Task B ↓ 通知を受けて処理開始
Task Notificationでは、SemaphoreやQueueのような独立したオブジェクトを作成せず、タスクが内部に持っている通知機能を直接利用できます。
そのため、単純なイベント通知やカウンタ、ビットフラグ、32bit値の受け渡しなどを効率よく実装できます。
この記事では、Task Notificationとは何かという基本から、xTaskNotifyGive()、ulTaskNotifyTake()、xTaskNotify()、xTaskNotifyWait()、ISRからの通知、SemaphoreやQueueとの使い分けまで順番に解説します。
Task Notificationとは?
Task Notificationは、FreeRTOSで特定のタスクへ直接通知を送るための仕組みです。
FreeRTOSの各タスクは、Task Notificationに使用できる通知状態と通知値を内部に持っています。
送信側Task
↓
通知
↓
受信側Task
↓
通知を受け取る
QueueやSemaphoreでは、タスクとは別にQueueやSemaphoreのオブジェクトを作成します。
Semaphoreの場合 Task A ↓ Semaphore ↓ Task B Task Notificationの場合 Task A ↓ Task Bへ直接通知
この「タスクへ直接通知する」という点がTask Notificationの大きな特徴です。
Task Notificationの通知値とは?
Task Notificationでは、タスクが通知用の値を持っています。
この通知値は32bitの符号なし整数として扱われ、APIの使い方によってさまざまな用途に利用できます。
Task
Notification Value
0x00000000
↓ 通知
0x00000001
たとえば通知値を、
- イベント発生回数を数えるカウンタ
- 複数イベントを表すビットフラグ
- 32bitの値そのもの
として利用できます。
なお、FreeRTOSの設定によっては1つのタスクに複数のNotification Slot(通知エントリ)を持たせることもできます。初心者のうちは、まず「タスク自身が通知値を持っている」と理解すると分かりやすいでしょう。
Task Notificationの代表的なAPI
| API | 主な用途 |
|---|---|
xTaskNotifyGive() |
通知値を1増やす |
ulTaskNotifyTake() |
通知をカウンタとして受け取る |
xTaskNotify() |
値の設定・加算・ビット操作などを行う |
xTaskNotifyWait() |
通知を待ち、通知値を受け取る |
vTaskNotifyGiveFromISR() |
ISRから通知値を1増やす |
xTaskNotifyFromISR() |
ISRから値やビットを通知する |
xTaskNotifyGive()で通知する
もっとも分かりやすい使い方が、xTaskNotifyGive()による通知です。
送信側は、通知したいタスクのハンドルを指定します。
xTaskNotifyGive(
receiverTaskHandle
);
この操作によって、対象タスクの通知値が1増加します。
通知値 0 ↓ xTaskNotifyGive() 1 ↓ xTaskNotifyGive() 2 ↓ xTaskNotifyGive() 3
そのため、単純な「通知あり・なし」だけでなく、イベントが何回発生したかを数える用途にも利用できます。
ulTaskNotifyTake()で通知を待つ
xTaskNotifyGive()と組み合わせてよく使用されるのがulTaskNotifyTake()です。
ulTaskNotifyTake(
pdTRUE,
portMAX_DELAY
);
通知がまだ来ていない場合、タスクをBlocked状態にして通知を待つことができます。
Task ↓ ulTaskNotifyTake() ↓ 通知なし ↓ Blocked 別Task ↓ xTaskNotifyGive() ↓ 通知 待っていたTask ↓ Ready ↓ 処理開始
通知を待っている間、CPUを使って何度も状態を確認する必要はありません。
pdTRUEとpdFALSEの違い
ulTaskNotifyTake()の第1引数は、通知を受け取ったときに通知値をどう変更するかを指定します。
ulTaskNotifyTake(
pdTRUE,
portMAX_DELAY
);
pdTRUEの場合、通知を取得すると通知値を0へクリアします。
通知値 = 5
ulTaskNotifyTake(pdTRUE, ...)
↓
通知値 = 0
一方、pdFALSEの場合は、通知を取得すると通知値を1減らします。
通知値 = 5
ulTaskNotifyTake(pdFALSE, ...)
↓
通知値 = 4
pdFALSEを使えば、Counting Semaphoreのようにイベント回数を1つずつ処理できます。
タスクから別のタスクへ通知してみよう
簡単な例として、Sender TaskからReceiver Taskへ1秒ごとに通知してみます。
TaskHandle_t receiverTaskHandle;
void senderTask(void *pvParameters)
{
while (1) {
xTaskNotifyGive(
receiverTaskHandle
);
vTaskDelay(
pdMS_TO_TICKS(1000)
);
}
}
void receiverTask(void *pvParameters)
{
while (1) {
ulTaskNotifyTake(
pdTRUE,
portMAX_DELAY
);
Serial.println(
"Notification received!"
);
}
}
Receiver Taskは通知がない間はBlocked状態になります。
Sender TaskがxTaskNotifyGive()を実行するとReceiver Taskが通知を受け取り、処理を開始します。
TaskHandle_tとは?
Task Notificationでは、「どのタスクへ通知するのか」を指定する必要があります。
そこで使用するのがタスクハンドルです。
TaskHandle_t receiverTaskHandle;
タスク作成時にハンドルを取得できます。
xTaskCreate(
receiverTask,
"Receiver",
2048,
NULL,
1,
&receiverTaskHandle
);
このreceiverTaskHandleを使えば、別のタスクからReceiver Taskを指定して通知できます。
xTaskNotify()なら通知値を操作できる
xTaskNotifyGive()は通知値を1増やすシンプルなAPIです。
一方、xTaskNotify()では通知値に対してさまざまな操作を指定できます。
xTaskNotify(
receiverTaskHandle,
value,
eSetValueWithOverwrite
);
第3引数では、通知値をどのように更新するか指定します。
| 指定 | 動作 |
|---|---|
eNoAction |
通知状態だけを更新する |
eSetBits |
指定したビットをORする |
eIncrement |
通知値を1増やす |
eSetValueWithOverwrite |
通知値を新しい値で上書きする |
eSetValueWithoutOverwrite |
未処理の通知がなければ値を設定する |
Task Notificationをフラグとして使う
通知値は32bitなので、各ビットをイベントフラグとして利用することもできます。
#define EVENT_BUTTON (1UL << 0)
#define EVENT_UART (1UL << 1)
#define EVENT_TIMER (1UL << 2)
それぞれのイベントが発生したときに、eSetBitsを指定して通知します。
xTaskNotify(
receiverTaskHandle,
EVENT_BUTTON,
eSetBits
);
UARTイベントなら次のようにします。
xTaskNotify(
receiverTaskHandle,
EVENT_UART,
eSetBits
);
複数のイベントが発生すると、通知値の複数のビットが1になります。
bit 0 = Button bit 1 = UART bit 2 = Timer 通知値 00000000 ↓ 00000001 Button発生 ↓ 00000011 UARTも発生
xTaskNotifyWait()で通知値を受け取る
通知値そのものを受け取りたい場合は、xTaskNotifyWait()を利用できます。
uint32_t notificationValue;
xTaskNotifyWait(
0,
0xFFFFFFFF,
¬ificationValue,
portMAX_DELAY
);
通知を受信すると、notificationValueへ通知値が格納されます。
たとえばビットを確認して処理を分けられます。
if (notificationValue & EVENT_BUTTON) {
Serial.println(
"Button event"
);
}
if (notificationValue & EVENT_UART) {
Serial.println(
"UART event"
);
}
ここでは以前学習したビット演算がそのまま利用されています。
詳しくは「ビット演算とは?AND・OR・XOR・NOT・シフト演算を初心者向けに解説」も参照してください。
ISRからTask Notificationを送る
Task Notificationは、割り込み処理からタスクへイベントを通知する用途にも利用できます。
ISRでは通常のxTaskNotifyGive()ではなく、ISR用APIを使用します。
vTaskNotifyGiveFromISR(
taskHandle,
&higherPriorityTaskWoken
);
これによって、割り込み処理では通知だけを行い、時間のかかる処理はタスク側で実行できます。
GPIO割り込み
↓
ISR
↓
Task Notification
↓
Taskを起こす
↓
本格的な処理
これは組み込み開発で非常に重要な設計パターンです。
なぜ割り込み処理を短くするの?
ISRでは、できるだけ短時間で処理を終えることが基本です。
割り込みの中で、
- 長い計算
- 時間のかかる通信
- 長時間のループ
- 待ち処理
などを行うと、他の処理へ影響する可能性があります。
そこでISRでは「イベントが発生した」という通知だけを行います。
ISR イベント検出 ↓ 通知 ↓ 終了 Task 通知待ち ↓ 通知受信 ↓ 時間のかかる処理
ISRから通知した後のタスク切り替え
ISRから通知したことで、現在実行中のタスクより高優先度のタスクがReady状態になる場合があります。
ESP32のFreeRTOS環境では、一般に次のような形で必要に応じてコンテキストスイッチを要求します。
BaseType_t higherPriorityTaskWoken =
pdFALSE;
vTaskNotifyGiveFromISR(
taskHandle,
&higherPriorityTaskWoken
);
if (higherPriorityTaskWoken) {
portYIELD_FROM_ISR();
}
使用しているFreeRTOSポートや開発環境によってマクロの使い方が異なる場合があるため、実際の実装では対象環境のドキュメントも確認してください。
KUMITATE-C3でTask Notificationを試してみよう
KUMITATE-C3では、SW1とLEDを使ってTask Notificationを試すことができます。
KUMITATE-C3では、SW1をGPIO21、LEDをGPIO0として扱います。
SW1 GPIO21 ↓ GPIO割り込み ↓ ISR ↓ Task Notification ↓ LED Task ↓ LED GPIO0
ボタン割り込みではLEDを直接制御せず、Task Notificationを使ってLED Taskへ通知します。
KUMITATE-C3のサンプルコード
const int LED_PIN = 0;
const int SW_PIN = 21;
TaskHandle_t ledTaskHandle;
void IRAM_ATTR buttonISR()
{
BaseType_t higherPriorityTaskWoken =
pdFALSE;
vTaskNotifyGiveFromISR(
ledTaskHandle,
&higherPriorityTaskWoken
);
if (higherPriorityTaskWoken) {
portYIELD_FROM_ISR();
}
}
void ledTask(void *pvParameters)
{
bool ledState = false;
while (1) {
ulTaskNotifyTake(
pdTRUE,
portMAX_DELAY
);
ledState = !ledState;
digitalWrite(
LED_PIN,
ledState ? HIGH : LOW
);
}
}
void setup()
{
pinMode(
LED_PIN,
OUTPUT
);
pinMode(
SW_PIN,
INPUT_PULLUP
);
xTaskCreate(
ledTask,
"LED Task",
2048,
NULL,
2,
&ledTaskHandle
);
attachInterrupt(
digitalPinToInterrupt(SW_PIN),
buttonISR,
FALLING
);
}
void loop()
{
vTaskDelay(
pdMS_TO_TICKS(1000)
);
}
このプログラムでは、SW1を押してGPIO割り込みが発生するとISRが実行されます。
ISRはLEDを直接操作せず、vTaskNotifyGiveFromISR()でLED Taskへ通知します。
LED TaskはulTaskNotifyTake()で通知を待っており、通知を受けるたびにLEDの状態を反転します。
実際のボタンではチャタリングによって1回の押下で複数回の割り込みが発生する可能性があります。必要に応じてソフトウェアまたはハードウェアによるチャタリング対策を追加してください。
Task NotificationとBinary Semaphoreの違い
Task Notificationは、Binary Semaphoreの代わりとして使える場面が多くあります。
Binary Semaphore ISR ↓ Semaphore ↓ Task Task Notification ISR ↓ Taskへ直接通知
単純に1つのタスクへイベントを通知するだけなら、Task Notificationは非常に適しています。
一方、Semaphoreは独立した同期オブジェクトなので、複数のタスクが同じSemaphoreを待つような構成など、Task Notificationとは異なる設計ができます。
Task NotificationとCounting Semaphoreの違い
xTaskNotifyGive()とulTaskNotifyTake()を組み合わせると、通知値をカウンタとして利用できます。
イベント発生 ↓ 通知値 0 → 1 イベント発生 ↓ 通知値 1 → 2 イベント発生 ↓ 通知値 2 → 3
そのため、1つのタスクに対するイベント回数の通知ならCounting Semaphoreに似た用途で利用できます。
Task NotificationとQueueの違い
Task Notificationでは32bitの通知値を扱えますが、Queueのように任意のデータ構造を複数個ためて順番に受け渡す仕組みではありません。
たとえば温度・湿度・気圧をまとめて渡したい場合を考えます。
typedef struct {
float temperature;
float humidity;
float pressure;
} SensorData;
このような構造体をタスク間で順番に渡したい場合は、Queueの方が自然です。
単純なイベント
↓
Task Notification
32bitの値・フラグ
↓
Task Notification
構造体などのデータ
↓
Queue
Queueについては「Queue(キュー)とは?FreeRTOSのタスク間通信を初心者向けに解説」も参照してください。
Task Notification・Semaphore・Queueを比較
| 項目 | Task Notification | Semaphore | Queue |
|---|---|---|---|
| 主な用途 | 特定タスクへの通知 | 同期・イベント通知 | データの受け渡し |
| 通知先 | 基本的に特定タスク | 待っているタスク | Queueを利用するタスク |
| 値 | 32bit通知値 | 基本的にデータ転送目的ではない | 任意サイズの項目 |
| カウント | 可能 | Counting Semaphoreで可能 | 複数項目を保持可能 |
| ビットフラグ | 可能 | 主用途ではない | データとして送れば可能 |
| ISRから通知 | 可能 | 可能 | 可能 |
| 複数データの蓄積 | Queueのようにはできない | できない | 可能 |
Task Notificationのメリット
- タスクへ直接通知できる
- 専用のQueueやSemaphoreオブジェクトを作らずに使える
- 単純なタスク通知を軽量に実装できる
- 通知値をカウンタとして利用できる
- 32bitの値を通知できる
- ビットフラグとして利用できる
- ISRからタスクへ通知できる
- 通知待ち中はタスクをBlocked状態にできる
Task Notificationの注意点
便利なTask Notificationですが、すべてのQueueやSemaphoreを置き換えるものではありません。
- 通知先となるタスクを明確にする必要がある
- Queueのように複数のデータを順番に蓄積する用途には向かない
- 構造体など大きなデータを直接送る用途には向かない
- 複数タスクが同じ通知を待つ用途には向かない
- 通知値の使い方を設計しておかないと分かりにくくなる
- カウンタ・値・ビットフラグなど複数の使い方を混在させる場合は注意する
Task NotificationはMutexの代わりになる?
基本的には別の目的の仕組みです。
Task Notificationはタスクへの通知や同期に向いています。
一方、MutexはI2CやSPIなどの共有リソースを複数タスクから安全に利用するための排他制御に使用します。
イベントを知らせる
↓
Task Notification
共有リソースを守る
↓
Mutex
Mutexについては「Mutex(ミューテックス)とは?FreeRTOSの排他制御を初心者向けに解説」を参照してください。
Task Notificationを使うか判断する目安
どの仕組みを使うか迷った場合は、「何をしたいのか」で考えると分かりやすくなります。
特定の1タスクへ
イベントを知らせたい
↓
Task Notification
イベント回数を
特定タスクへ知らせたい
↓
Task Notification
32bitの値やフラグを
特定タスクへ渡したい
↓
Task Notification
複数のデータを
順番に渡したい
↓
Queue
複数タスク間で
同期したい
↓
Semaphore
共有リソースを
排他的に使いたい
↓
Mutex
KUMITATE-C3での利用例
KUMITATE-C3でRTOSを利用する場合にも、Task Notificationはさまざまな場面で利用できます。
- SW1のGPIO割り込みからボタン処理タスクへ通知する
- UART受信イベントを通信処理タスクへ通知する
- タイマーイベントを周期処理タスクへ通知する
- センサー読み取り完了を別タスクへ通知する
- Wi-Fi関連の処理完了をアプリケーションタスクへ知らせる
特に、「ISRでは通知だけ行い、実際の処理はタスク側で行う」という設計を学ぶ教材として非常に分かりやすい機能です。
RTOSのタスク間連携を整理しよう
| 仕組み | 主な役割 |
|---|---|
| Queue | データを順番に渡す |
| Semaphore | イベント通知や同期 |
| Mutex | 共有リソースの排他制御 |
| Task Notification | 特定タスクへ軽量に直接通知する |
どれか1つが常に優れているわけではありません。
通知する相手、渡したい情報、共有リソースの有無などによって適切な仕組みを選択します。
まとめ
Task Notificationは、FreeRTOSで特定のタスクへ直接通知を送るための軽量な仕組みです。
- Task Notificationではタスク自身が持つ通知機能を利用する
- 専用のSemaphoreやQueueを作らずにタスクへ直接通知できる
xTaskNotifyGive()で通知値を1増やせるulTaskNotifyTake()で通知を待てるpdTRUEなら取得時に通知値を0へクリアするpdFALSEなら取得時に通知値を1減らすxTaskNotify()では通知値の設定やビット操作ができるxTaskNotifyWait()では通知値を受け取れる- 通知値をカウンタやビットフラグとして利用できる
- ISRからタスクへ通知することもできる
- 単純なイベント通知ではSemaphoreの代わりとして適する場合がある
- Queueのように複数の任意データを蓄積する仕組みではない
- 共有リソースの排他制御を行うMutexとは目的が異なる
Task Notificationを理解するうえで特に重要なのは、「タスクが通知機能を直接持っている」という点です。
「ボタンが押された」「データを受信した」「タイマーが発生した」といったイベントを特定のタスクへ知らせるだけなら、QueueやSemaphoreを作らずTask Notificationだけでシンプルに実装できる場合があります。
Queue・Semaphore・Mutex・Task Notificationをそれぞれ使い分けられるようになると、FreeRTOSでタスクを作るだけでなく、複数の処理をどのように連携させるかというRTOS設計の考え方が見えてきます。

