Event Groupsとは?FreeRTOSで複数イベントを待ち合わせる仕組みを初心者向けに解説

組み込み基礎

FreeRTOSで複数のタスクを動かしていると、「1つのイベントが発生するまで待つ」だけではなく、複数の条件がそろうまで待ちたい場面が出てきます。

たとえばIoT機器の起動時に、次の3つの処理が必要だとします。

  • Wi-Fiへの接続
  • NTPによる時刻同期
  • センサーの初期化

それぞれ別のタスクで処理していて、3つすべてが完了してからアプリケーションを開始したい場合、どうすればよいでしょうか。

Wi-Fi接続完了 ─────┐
                   │
時刻同期完了 ──────┼→ すべて完了 → アプリ開始
                   │
センサー初期化完了 ─┘

このような複数のイベントをまとめて管理し、条件がそろうまでタスクを待たせるために使えるのが、FreeRTOSのEvent Groups(イベントグループ)です。

Event Groupsでは、1つの整数値の各ビットをイベントの状態として利用します。

この記事では、Event Groupsの基本から、イベントビット、AND待ち・OR待ち、xEventGroupSetBits()、xEventGroupWaitBits()、ISRからの利用、Task NotificationやSemaphoreとの違いまで順番に解説します。

FreeRTOSのEvent Groupsの仕組み。複数イベントをビットで管理し、AND待ちとOR待ちでタスクを同期する方法を示した図
Event Groupsによる複数イベントのビット管理とAND待ち・OR待ち

Event Groupsとは?

Event Groupsは、FreeRTOSで複数のイベント状態をビットで管理するための仕組みです。

たとえば、3つのイベントを次のように割り当てます。

bit 0:Wi-Fi接続完了
bit 1:時刻同期完了
bit 2:センサー初期化完了

イベントが発生したら、対応するビットを1にします。

初期状態

00000000


Wi-Fi接続完了

00000001


時刻同期も完了

00000011


センサー初期化も完了

00000111

このビットの集合を利用して、「どの処理が完了しているのか」を管理できます。

イベントをビットで表す

イベントビットは、通常ビットシフトを使って定義します。

#define WIFI_BIT    (1UL << 0)
#define TIME_BIT    (1UL << 1)
#define SENSOR_BIT  (1UL << 2)

それぞれの値は次のようになります。

イベントビット16進数
Wi-Fibit 00x01
時刻同期bit 10x02
センサーbit 20x04

複数のイベントが発生すると、それぞれのビットが同時に1になります。

ここでは以前学習したビット演算がそのまま利用されています。

ビット演算については「ビット演算とは?AND・OR・XOR・NOT・シフト演算を初心者向けに解説」も参照してください。

Event Groupを作成する

Event Groupを使用するには、まずEvent Groupのハンドルを用意します。

EventGroupHandle_t eventGroup;

そしてxEventGroupCreate()で作成します。

eventGroup = xEventGroupCreate();

if (eventGroup == NULL) {
    Serial.println("Event Group create failed");
}

作成に失敗した場合はNULLが返るため、実際のプログラムでは戻り値を確認しておくと安全です。

イベントが発生したらビットをセットする

イベントが発生したら、xEventGroupSetBits()で対応するビットを1にします。

たとえばWi-Fiへの接続が完了した場合は次のようにします。

xEventGroupSetBits(
    eventGroup,
    WIFI_BIT
);

時刻同期が完了した場合は、

xEventGroupSetBits(
    eventGroup,
    TIME_BIT
);

センサー初期化が完了したら、

xEventGroupSetBits(
    eventGroup,
    SENSOR_BIT
);

とします。

複数のビットを一度にセットできる

ビットORを使えば、複数のビットをまとめて指定することもできます。

xEventGroupSetBits(
    eventGroup,
    WIFI_BIT | TIME_BIT
);

この場合、Wi-Fiと時刻同期に対応する2つのビットがセットされます。

WIFI_BIT = 00000001
TIME_BIT = 00000010

OR
       ↓

           00000011

xEventGroupWaitBits()でイベントを待つ

Event Groupsの重要な機能が、xEventGroupWaitBits()です。

このAPIを使うと、指定したイベントビットがセットされるまでタスクを待たせることができます。

EventBits_t bits;

bits = xEventGroupWaitBits(
    eventGroup,
    WIFI_BIT,
    pdFALSE,
    pdTRUE,
    portMAX_DELAY
);

通知条件が成立していない間、タスクはBlocked状態で待つことができます。

xEventGroupWaitBits()の引数を理解しよう

xEventGroupWaitBits()には複数の引数があります。

xEventGroupWaitBits(
    eventGroup,
    bitsToWaitFor,
    clearOnExit,
    waitForAllBits,
    ticksToWait
);
引数意味
eventGroup対象のEvent Group
bitsToWaitFor待ちたいイベントビット
clearOnExit条件成立後に指定ビットをクリアするか
waitForAllBitsすべてのビットを待つか、どれか1つを待つか
ticksToWait最大待ち時間

特に重要なのがwaitForAllBitsです。

AND待ちとは?

複数のイベントがすべて発生するまで待ちたい場合はAND待ちを使います。

xEventGroupWaitBits()のwaitForAllBitsをpdTRUEにします。

EventBits_t bits;

bits = xEventGroupWaitBits(
    eventGroup,
    WIFI_BIT |
    TIME_BIT |
    SENSOR_BIT,
    pdFALSE,
    pdTRUE,
    portMAX_DELAY
);

この場合、次の3つがすべてセットされるまで待ちます。

Wi-Fi    = 1
時刻同期  = 1
センサー  = 1

すべて1
   ↓
待ち解除

つまり、

00000001 → まだ待つ

00000011 → まだ待つ

00000111 → 条件成立

となります。

OR待ちとは?

指定したイベントのどれか1つでも発生したら処理を進めたい場合はOR待ちを使います。

waitForAllBitsをpdFALSEにします。

EventBits_t bits;

bits = xEventGroupWaitBits(
    eventGroup,
    WIFI_BIT |
    TIME_BIT |
    SENSOR_BIT,
    pdFALSE,
    pdFALSE,
    portMAX_DELAY
);

Wi-Fi、時刻同期、センサー初期化のうち、どれか1つでもビットがセットされれば待ち状態が解除されます。

00000000
   ↓
待つ


00000010
   ↓
TIME_BITが1
   ↓
待ち解除

AND待ちとOR待ちの違い

待ち方条件例
AND待ち指定したすべてのビットが1Wi-Fi接続+時刻同期+センサー初期化をすべて待つ
OR待ち指定したビットのどれかが1ボタン・タイマー・通信のどれかを待つ

Event Groupsの大きな特徴は、この複数条件の待ち合わせを簡単に記述できることです。

イベント発生後にビットをクリアする

xEventGroupWaitBits()の第3引数clearOnExitをpdTRUEにすると、条件成立後に待っていたビットをクリアできます。

xEventGroupWaitBits(
    eventGroup,
    WIFI_BIT,
    pdTRUE,
    pdTRUE,
    portMAX_DELAY
);

たとえば、

00000001
   ↓
条件成立
   ↓
タスクを起こす
   ↓
00000000

という動作になります。

一方、pdFALSEならビットはそのまま残ります。

「現在の状態」を表したいのか、「一度発生したイベント」を表したいのかによって使い分けます。

ビットを明示的にクリアする

イベントビットはxEventGroupClearBits()を使って明示的に0へ戻すこともできます。

xEventGroupClearBits(
    eventGroup,
    WIFI_BIT
);

たとえばWi-Fiが切断されたときにWi-Fi接続状態のビットをクリアする、といった使い方ができます。

Wi-Fi接続

WIFI_BIT = 1


Wi-Fi切断

WIFI_BIT = 0

このようにEvent Groupsは、一瞬のイベントだけでなくシステムの状態を表すフラグとしても利用できます。

現在のイベントビットを確認する

現在どのビットがセットされているかを確認したい場合は、xEventGroupGetBits()を利用できます。

EventBits_t bits;

bits = xEventGroupGetBits(
    eventGroup
);

if (bits & WIFI_BIT) {

    Serial.println(
        "Wi-Fi connected"
    );
}

ここでもAND演算を使って特定のビットを確認しています。

複数のタスクでEvent Groupsを使ってみよう

3つの初期化タスクと、すべての完了を待つアプリケーションタスクを考えてみます。

#define WIFI_BIT    (1UL << 0)
#define TIME_BIT    (1UL << 1)
#define SENSOR_BIT  (1UL << 2)

EventGroupHandle_t eventGroup;


void wifiTask(void *pvParameters)
{
    // Wi-Fi接続処理

    xEventGroupSetBits(
        eventGroup,
        WIFI_BIT
    );

    vTaskDelete(NULL);
}


void timeTask(void *pvParameters)
{
    // NTP時刻同期処理

    xEventGroupSetBits(
        eventGroup,
        TIME_BIT
    );

    vTaskDelete(NULL);
}


void sensorTask(void *pvParameters)
{
    // センサー初期化処理

    xEventGroupSetBits(
        eventGroup,
        SENSOR_BIT
    );

    vTaskDelete(NULL);
}


void appTask(void *pvParameters)
{
    EventBits_t bits;

    bits = xEventGroupWaitBits(
        eventGroup,
        WIFI_BIT |
        TIME_BIT |
        SENSOR_BIT,
        pdFALSE,
        pdTRUE,
        portMAX_DELAY
    );

    Serial.println(
        "All initialization completed!"
    );

    while (1) {

        // メイン処理

        vTaskDelay(
            pdMS_TO_TICKS(1000)
        );
    }
}

Wi-Fi、時刻同期、センサー初期化の順番は問いません。

3つすべてが完了して対応するビットがセットされた時点で、appTaskの待ち状態が解除されます。

Event Groupsは処理の順番を固定しない

Event Groupsの便利なところは、それぞれの処理が完了する順番を固定する必要がないことです。

パターンA

Wi-Fi
 ↓
時刻同期
 ↓
センサー


パターンB

センサー
 ↓
Wi-Fi
 ↓
時刻同期


どちらでも

すべてのビットが1
 ↓
アプリ開始

複数の非同期処理を並行して行い、その完了を待ち合わせるようなシステムで便利です。

ISRからEvent Groupのビットをセットする

割り込みからイベントを通知したい場合は、ISR用のAPIを使用します。

BaseType_t higherPriorityTaskWoken =
    pdFALSE;

xEventGroupSetBitsFromISR(
    eventGroup,
    BUTTON_BIT,
    &higherPriorityTaskWoken
);

if (higherPriorityTaskWoken) {
    portYIELD_FROM_ISR();
}

ISRから通常のxEventGroupSetBits()を直接呼ぶのではなく、xEventGroupSetBitsFromISR()を使用します。

ただし、Event GroupsのISR用APIにはFreeRTOS内部の処理上の特徴があります。ISRからの単純で軽量な1対1通知が目的なら、Task Notificationの方が適している場合もあります。

Task Notificationについては「Task Notificationとは?FreeRTOSの軽量なタスク通知を初心者向けに解説」も参照してください。

Event GroupsとTask Notificationの違い

Task Notificationでも通知値をビットフラグとして利用できるため、Event Groupsと似ているように見えます。

大きな違いは、Task Notificationが特定のタスクへ直接通知する仕組みなのに対し、Event Groupsは複数のイベント状態を共有して複数タスクから利用できる同期オブジェクトだという点です。

Event GroupsTask Notification
主な用途複数イベントの状態管理・待ち合わせ特定タスクへの軽量な通知
イベント表現複数ビット通知値・ビット・カウンタ
AND/OR待ち対応用途によって実装可能だが性格が異なる
複数タスクから利用可能通知先は特定タスク
独立オブジェクト必要不要

Event GroupsとSemaphoreの違い

Semaphoreは、イベント通知やタスク間の同期に利用できます。

Binary Semaphoreであれば、基本的には「イベントが発生したか」という1つの状態を扱います。

Binary Semaphore

イベント
   ↓
Give
   ↓
Task
   ↓
Take


Event Groups

Wi-Fi ─────┐
Timer ─────┼→ Event Bits → Task
Sensor ────┘

複数のイベント状態をまとめて管理したい場合は、Event Groupsが分かりやすいことがあります。

Semaphoreについては「Semaphore(セマフォ)とは?FreeRTOSのタスク同期を初心者向けに解説」も参照してください。

Event GroupsとQueueの違い

Event Groupsは状態やイベントの有無を表す仕組みであり、Queueのようにデータを順番に蓄積して渡すものではありません。

Wi-Fi接続した?
時刻同期した?
センサー初期化した?

        ↓

Event Groups


温度 = 25.3℃
湿度 = 48.2%
時刻 = 12:30:05

        ↓

Queue

「何が起きたか・どの状態か」を管理するならEvent Groups、「データそのものを渡す」ならQueueという考え方が分かりやすいでしょう。

Event Groups・Task Notification・Semaphore・Queueを比較

仕組み主な用途向いている例
Event Groups複数イベントの状態管理・待ち合わせWi-Fi+時刻同期+センサー初期化
Task Notification特定タスクへの軽量な直接通知ISRから処理タスクを起こす
Semaphoreイベント通知・同期処理完了を別タスクへ通知
Queueデータの受け渡しセンサーデータを別タスクへ送る
Mutex排他制御I2Cなどの共有リソースを保護する

KUMITATE-C3でEvent Groupsを使うなら?

KUMITATE-C3でも、複数のイベントを待ち合わせる教材を作れます。

たとえば次の3つをイベントとしてみます。

  • SW1が押された
  • 起動から5秒経過した
  • Wi-Fiへの接続が完了した

それぞれにビットを割り当てます。

#define BUTTON_BIT  (1UL << 0)
#define TIMER_BIT   (1UL << 1)
#define WIFI_BIT    (1UL << 2)

そして3つすべてが成立したらLEDを点灯させます。

SW1押下 ─────────┐
                 │
5秒経過 ─────────┼→ Event Group
                 │       ↓
Wi-Fi接続完了 ───┘    3条件成立
                         ↓
                    LED点灯

KUMITATE-C3のサンプルコード

概念を理解するための簡単な例です。ボタン、タイマー、Wi-Fi接続に相当する3つのイベントがそろったらLEDを点灯します。

#include <Arduino.h>

#define BUTTON_BIT  (1UL << 0)
#define TIMER_BIT   (1UL << 1)
#define WIFI_BIT    (1UL << 2)

const int LED_PIN = 0;
const int SW_PIN  = 21;

EventGroupHandle_t eventGroup;


void buttonTask(void *pvParameters)
{
    while (1) {

        if (digitalRead(SW_PIN) == LOW) {

            xEventGroupSetBits(
                eventGroup,
                BUTTON_BIT
            );

            vTaskDelay(
                pdMS_TO_TICKS(300)
            );
        }

        vTaskDelay(
            pdMS_TO_TICKS(10)
        );
    }
}


void timerTask(void *pvParameters)
{
    vTaskDelay(
        pdMS_TO_TICKS(5000)
    );

    xEventGroupSetBits(
        eventGroup,
        TIMER_BIT
    );

    vTaskDelete(NULL);
}


void wifiTask(void *pvParameters)
{
    // 実際にはここでWi-Fi接続処理を行う

    vTaskDelay(
        pdMS_TO_TICKS(3000)
    );

    xEventGroupSetBits(
        eventGroup,
        WIFI_BIT
    );

    vTaskDelete(NULL);
}


void ledTask(void *pvParameters)
{
    xEventGroupWaitBits(
        eventGroup,
        BUTTON_BIT |
        TIMER_BIT |
        WIFI_BIT,
        pdFALSE,
        pdTRUE,
        portMAX_DELAY
    );

    digitalWrite(
        LED_PIN,
        HIGH
    );

    Serial.println(
        "All events completed!"
    );

    vTaskDelete(NULL);
}


void setup()
{
    Serial.begin(115200);

    pinMode(
        LED_PIN,
        OUTPUT
    );

    pinMode(
        SW_PIN,
        INPUT_PULLUP
    );

    eventGroup =
        xEventGroupCreate();

    if (eventGroup == NULL) {

        Serial.println(
            "Event Group create failed"
        );

        return;
    }

    xTaskCreate(
        buttonTask,
        "Button",
        2048,
        NULL,
        1,
        NULL
    );

    xTaskCreate(
        timerTask,
        "Timer",
        2048,
        NULL,
        1,
        NULL
    );

    xTaskCreate(
        wifiTask,
        "WiFi",
        2048,
        NULL,
        1,
        NULL
    );

    xTaskCreate(
        ledTask,
        "LED",
        2048,
        NULL,
        2,
        NULL
    );
}


void loop()
{
    vTaskDelay(
        pdMS_TO_TICKS(1000)
    );
}

この例では、実際のWi-Fi接続処理の代わりに3秒の待ち時間を入れています。

SW1押下、5秒経過、Wi-Fi処理完了の3つがそろうまでLED TaskはBlocked状態です。

最後のビットがセットされるとAND条件が成立し、LED Taskが実行可能になります。

Event Groupsのメリット

  • 複数のイベントを1つのオブジェクトで管理できる
  • イベントの状態をビットで分かりやすく表現できる
  • 複数条件のAND待ちが簡単にできる
  • 複数条件のOR待ちもできる
  • 条件が成立するまでタスクをBlocked状態にできる
  • 複数のタスクからイベントビットをセットできる
  • イベントの完了順序に依存しない待ち合わせができる
  • システムの状態管理にも利用できる

Event Groupsの注意点

Event Groupsは便利ですが、用途を理解して使うことが重要です。

  • Queueのようにデータそのものを保存する仕組みではない
  • 同じイベントが何回発生したかを数える用途には向かない
  • ビットをセットしたままにするのかクリアするのかを設計する必要がある
  • 同じビットを複数の意味で使わない
  • イベント数には利用可能なビット数による上限がある
  • ISRから使用する場合はISR用APIを使用する

特に重要なのが、Event Groupsはイベントの回数を保存するQueueではないという点です。

BUTTON_BIT

1回押した
↓
1


さらに1回押した
↓
1


さらに1回押した
↓
1

同じビットを何度セットしても、状態は1のままです。

「ボタンが5回押された」のように回数そのものが重要なら、Task Notificationのカウンタ、Counting Semaphore、Queueなど別の方法を検討します。

Event Groupsで使えるビット数に注意

Event Groupsの内部値はEventBits_tですが、そのすべてのビットをユーザーイベントとして利用できるとは限りません。

FreeRTOSでは一部の上位ビットが内部制御用に予約されています。また、利用可能なビット数はFreeRTOSのtick型設定によって変わります。

そのため「32bit型だから32個のイベントを必ず使える」と考えず、使用しているFreeRTOSの設定と公式ドキュメントを確認してください。

Event Groupsを使うべき場面

Event Groupsが特に向いているのは、次のような処理です。

  • 複数の初期化処理が完了するまで待つ
  • Wi-Fi接続状態など複数のシステム状態を管理する
  • 複数イベントのどれかが発生するまで待つ
  • 複数イベントがすべて成立するまで待つ
  • 複数タスクから1つの状態集合を更新する

どのFreeRTOS機能を使えばいい?

データを順番に渡したい
        ↓
      Queue


イベントを通知・同期したい
        ↓
    Semaphore


共有リソースを守りたい
        ↓
      Mutex


特定タスクへ軽量に通知したい
        ↓
 Task Notification


複数イベントをまとめて
AND / OR条件で待ちたい
        ↓
  Event Groups

実際のシステムでは、これらを組み合わせて使用することも珍しくありません。

たとえば、Event Groupsで「Wi-Fi接続済み」という状態を管理し、Queueで送信データを受け渡し、MutexでI2Cバスを保護するといった設計ができます。

まとめ

Event Groupsは、FreeRTOSで複数のイベント状態をビットで管理し、タスクの待ち合わせを行うための仕組みです。

  • 1つのEvent Groupで複数のイベント状態を管理できる
  • 各イベントを1つのビットとして表現する
  • xEventGroupCreate()でEvent Groupを作成する
  • xEventGroupSetBits()でイベントビットをセットする
  • xEventGroupClearBits()でイベントビットをクリアできる
  • xEventGroupGetBits()で現在の状態を取得できる
  • xEventGroupWaitBits()でイベントを待てる
  • AND待ちでは指定したすべてのイベントを待つ
  • OR待ちでは指定したイベントのどれか1つを待つ
  • 条件成立後にビットを自動クリアすることもできる
  • 複数の非同期処理の完了待ちに向いている
  • イベント発生回数や任意データを保存する仕組みではない
  • ISRから使用する場合はISR用APIを使う
  • Task Notification、Semaphore、Queue、Mutexとはそれぞれ目的が異なる

Event Groupsを理解するうえで特に重要なのは、「複数の状態をビットで表現し、それらをANDまたはORの条件で待てる」という点です。

たとえば「Wi-Fi接続完了」「時刻同期完了」「センサー初期化完了」のように、複数の非同期処理がすべて完了してから次へ進みたい場合、Event Groupsを使うと処理の関係を分かりやすく表現できます。

Queue・Semaphore・Mutex・Task NotificationにEvent Groupsまで理解できると、FreeRTOSで「タスクを作る」だけでなく「複数のタスクをどう連携させるか」という設計がかなり見えるようになります。

KUMITATE

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

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

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