見出し画像

FreeRTOSのイベントグループの使い方を理解する

経緯

  • 前々回の記事で、FreeRTOS Windows Simulatorを使えるようにして、タスクとキューの使い方のサンプルを確認した

  • 前回の記事で、Arduino IDEとESP32を使いタスクの使い方の理解を深めた

  • 今回は、イベントグループの使い方について理解を深める

    • FreeRTOS Windows Simulatorの簡単なサンプルには含まれていなかったため、ゼロからコードを書いてみる

環境

  • Windows 10

  • FreeRTOS 202212.01 Windows Simulator

  • Visual Studio 2019

処理の流れ

  • 3つのタスクがあるとする(Task1、Task2、Task3と命名)

  • Task1は、完了するのに、1秒かかる

  • Task2は、Task1が完了しないと処理できない

  • Task3は、Task1とTask2が完了しないと処理できない

  • このような処理をしたいとき、イベントグループを使うと、可読性、保守性が高いコードになると感じる

    • (GOTOを使ったコードの方が読みやすいという方も一定数いると思うので一概には言えない)

イベントグループを使ったシンプルなコード

  • 一見、長く見えるが(シンプルといいながら、複雑に見えるが)

  • printf()の最中にコンテキストのスイッチが発生してデッドロックが起こることを防ぐためのコードの影響で長くみえるだけである

    • taskENTER_CRITICAL()とtaskEXIT_CRITICAL()は、イベントグループの理解において、本質的ではないので考えなくてよい

#include "FreeRTOS.h"
#include "task.h"
#include "event_groups.h"

static EventGroupHandle_t app_event;

typedef enum {
    TASK1_WAS_COMPLETED = 0x1,
    TASK2_WAS_COMPLETED = 0x2,
} app_event_t;

static void prvMyTask1(void* args)
{
    TickType_t xNextWakeTime = xTaskGetTickCount();

    for (int i = 0; i < 10; i++)
    {
        xTaskDelayUntil(&xNextWakeTime, 100);

        taskENTER_CRITICAL();
        {
            printf("Task1's progress is %d%%.\r\n", (i + 1) * 10);
        }
        taskEXIT_CRITICAL();
    }

    xEventGroupSetBits(app_event, TASK1_WAS_COMPLETED);

    vTaskDelete(NULL);
}

static void prvMyTask2(void* args)
{
    TickType_t xNextWakeTime = xTaskGetTickCount();

    taskENTER_CRITICAL();
    {
        printf("Task2 is awaitng Task1.\r\n");
    }
    taskEXIT_CRITICAL();

    EventBits_t event = xEventGroupWaitBits(app_event, TASK1_WAS_COMPLETED, pdFALSE, pdFALSE, portMAX_DELAY);

    taskENTER_CRITICAL();
    {
        printf("Task2 is working.\r\n");
    }
    taskEXIT_CRITICAL();

    xTaskDelayUntil(&xNextWakeTime, 500);

    taskENTER_CRITICAL();
    {
        printf("Task2 was completed.\r\n");
    }
    taskEXIT_CRITICAL();

    xEventGroupSetBits(app_event, TASK2_WAS_COMPLETED);

    vTaskDelete(NULL);
}

static void prvMyTask3(void* args)
{
    TickType_t xNextWakeTime = xTaskGetTickCount();

    taskENTER_CRITICAL();
    {
        printf("Task3 is awaitng both Task1 and Task2.\r\n");
    }
    taskEXIT_CRITICAL();

    EventBits_t event = xEventGroupWaitBits(app_event, (TASK1_WAS_COMPLETED | TASK2_WAS_COMPLETED), pdFALSE, pdTRUE, portMAX_DELAY);

    xTaskDelayUntil(&xNextWakeTime, 100);

    taskENTER_CRITICAL();
    {
        printf("Task3 was completed.\r\n");
    }
    taskEXIT_CRITICAL();

    vTaskDelete(NULL);
}


void main_event(void)
{
    app_event = xEventGroupCreate();

    xTaskCreate(prvMyTask1, "Task1", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
    xTaskCreate(prvMyTask2, "Task2", configMINIMAL_STACK_SIZE, NULL, 1, NULL);
    xTaskCreate(prvMyTask3, "Task3", configMINIMAL_STACK_SIZE, NULL, 1, NULL);

    vTaskStartScheduler();
}

コードの説明

Windows Simulatorでの動かし方

  • FreeRTOS Windows Simulatorのプロジェクトを開き、新規に、main_event.cを追加

  • main_event()が実行されるように、FreeRTOS Windows Simulatorのmain()関数を編集する

    • 手間と感じる場合は、main_blinky.cを編集してもよい

    • (気楽に試せるサンドボックスの環境を作りたい)

準備

  • イベントグループを使うときは、#include "event_groups.h"を記述する

  • イベントの状態を示す変数をグローバル変数で定義:static EventGroupHandle_t app_event

  • 以下のイベントをenumで定義:app_event_t

    • Task1が完了を示すイベント

    • Task2が完了を示すイベント

main_evnet()

  • main_event()関数がmain()関数から呼ばれる

  • xEventGroupCreate()を使い、イベントグループを作る

  • xTaskCreate()で、3つのタスクを作る

  • vTaskStartScheduler()を実行することで、Task1、Task2、Task3が実行される

Task1の処理

  • 処理をするのに1秒かかる

  • 処理が完了したら、xEventGroupSetBits()を使い、app_event変数の「Task1が完了したことを示すビット」を1にする

Task2の処理

  • xEventGroupWaitBits()関数で、Task1が完了する(イベントグループのビットが1になる)のを待つ

  • xEventGroupWaitBits()関数の第3引数をpdFALSE設定して、完了時にクリアしないようにする

  • xEventGroupWaitBits()関数の第5引数は待ち時間で、今回はportMAX_DELAYを設定している、portMAX_DELAYは32ビット環境で0xffffffffUL

  • 処理が完了したら、xEventGroupSetBits()でTask2が完了したことを示すビットを1にする

Task3の処理

  • xEventGroupWaitBits()で、Task1とTask2が完了する(イベントグループの変数下位2ビットの両方が1になる)のを待つ

  • xEventGroupWaitBits()の第2引数は、Task1とTask2をの完了を表現するために、OR演算(|:縦棒)をしている

    • この例において、下位4ビットのみの2進数で表現すると、0001b OR 0010b = 0011bとなる

  • xEventGroupWaitBits()の第4引数をpdTRUEにして、第2引数で1に設定したビットのすべてが1になることを待つようにする

    • これ例だと、Task1とTask2の両方が完了することも待つことを意味する

    • 一方、pdFALSEにすると、Task1とTask2のどちらかが完了したときに待機が完了する、という具体になる

実行結果

Task2 is awaitng Task1.
Task3 is awaitng both Task1 and Task2.
Task1's progress is 10%.
Task1's progress is 20%.
Task1's progress is 30%.
Task1's progress is 40%.
Task1's progress is 50%.
Task1's progress is 60%.
Task1's progress is 70%.
Task1's progress is 80%.
Task1's progress is 90%.
Task1's progress is 100%.
Task2 is working.
Task2 was completed.
Task3 was completed.

感想

  • 3回の記事を書くことを通して、FreeRTOSのタスクとイベントグループとキューの基本的な使い方が理解できた

  • まだ、FreeRTOSの知識が不足していると感じるが(セマフォを確認できていないなど)

  • 後は、実装しながら学ぶことにする(いよいよ、ESP32-S3のUSB Hostの実装に入る)

おまけ

  • イベントグループ内の変数のビットの状況を確認したく以下のようにするがコンパイルできない

printf("app_event: %x\n", app_event->uxEventBits);
  • EventGroup_tが、event_groups.cに定義されているからだと思われる

    • 直接、値の参照、書き換えるなどを防止するためという理解で正しいだろうか

  • 仕方ないので、以下のような同じデータ構造の構造体を作って

typedef struct
{
    EventBits_t uxEventBits;
    List_t xTasksWaitingForBits;

#if ( configUSE_TRACE_FACILITY == 1 )
    UBaseType_t uxEventGroupNumber;
#endif

#if ( ( configSUPPORT_STATIC_ALLOCATION == 1 ) && ( configSUPPORT_DYNAMIC_ALLOCATION == 1 ) )
    uint8_t ucStaticallyAllocated;
#endif
} EventGroup_t_Tmp;
  • 以下のように無理やりキャストして、値を参照したら、期待通り動いてくれた

    • 今回は、デバッグ用途なので、このような乱暴なことをしたが、実際のコードでは書くべきではない

printf("app_event: %x\n", ((EventGroup_t_Tmp*)app_event)->uxEventBits);
  • app_eventのuxEventBitsは、xEventGroupCreate()実行時(初期化時)に0で、

  • Task1が完了して1になり、

  • Task2が完了して3になる、という期待通りの動作であった

  • WindowsのSimulatorは、高速にイテレーションできて、ブレークもできて、理解が捗ると改めて感じた

    • 導入しようと考えた当初、遠回りとも思えたが、導入した価値があった

    • 今回、ANDとORの演算を間違えるという初歩的なミスをおかして、軽くはまってしまったが、もしESP32実機で検証していたら、かなりの時間を浪費したと思われる

    • Task3は、Task1とTask2の両方を待つのだから、AND、と勘違いしてしまった

この記事が気に入ったらサポートをしてみませんか?