![見出し画像](https://assets.st-note.com/production/uploads/images/105733919/rectangle_large_type_2_9ae659b0b10d398aa32be793f027d7aa.jpeg?width=1200)
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
INCLUDE_vTaskSuspendを設定してる場合は、無期限になるが、そうではない場合は、7週間程度なので注意
https://techoverflow.net/2022/01/07/how-long-does-portmax_delay-actually-wait-in-freertos/
処理が完了したら、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、と勘違いしてしまった
この記事が気に入ったらサポートをしてみませんか?