Task Notificationとは?FreeRTOSの軽量なタスク通知を初心者向けに解説

組み込み基礎

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との使い分けまで順番に解説します。

FreeRTOSのTask Notificationの仕組み。タスクへの直接通知、通知値、ISRからの通知、SemaphoreやQueueとの違いを示した図
FreeRTOSのTask Notificationによるタスクへの直接通知と、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,
    &notificationValue,
    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設計の考え方が見えてきます。

KUMITATE

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

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

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