Week 4 Day 5: Event Groups

학습 목표

  • FreeRTOS Event Groups가 Queue와 Semaphore와 다른 점을 이해하고 적합한 사용 시나리오를 파악한다
  • xEventGroupSetBits()xEventGroupWaitBits()의 매개변수 의미와 동작 방식을 정확히 파악한다
  • 여러 이벤트를 AND/OR 조건으로 동시에 대기하는 패턴을 구현한다
  • 다중 센서 이벤트를 Event Group으로 처리하는 완전한 실습 예제를 구현한다

Queue는 데이터를 전달하고, Semaphore는 단일 이벤트를 신호로 알립니다. Event Group은 여러 이벤트 비트를 하나의 객체에 묶어 AND/OR 조건으로 대기할 수 있는 메커니즘입니다. "센서 A, B, C가 모두 준비됐을 때 처리를 시작한다"는 요구사항에 가장 적합합니다.


1. Event Group 개념

1.1 비트 플래그로 이벤트 표현

FreeRTOS Event Group은 내부적으로 EventBits_t 타입(최소 24비트)의 비트 필드를 유지합니다. 각 비트가 하나의 이벤트를 나타냅니다. 여러 Task 또는 ISR이 서로 다른 비트를 세트할 수 있고, 대기하는 Task는 특정 비트 조합이 충족될 때까지 Blocked 상태를 유지합니다.

/*
 * Event Group 비트 정의 예시
 *
 * EventBits_t (최소 24비트 유효 비트 사용, 비트 24~31은 내부 예약)
 *
 * Bit 0: 온도 센서 데이터 준비
 * Bit 1: 습도 센서 데이터 준비
 * Bit 2: 조도 센서 데이터 준비
 * Bit 3: UART 전송 완료
 * Bit 4: ADC 보정 완료
 *
 * AND 대기: Bit 0, 1, 2가 모두 세트될 때까지 대기 (모든 센서 준비)
 * OR  대기: Bit 0, 1, 2 중 하나라도 세트되면 즉시 처리 (하나씩 처리)
 */

#include "FreeRTOS.h"
#include "event_groups.h"
#include <stdio.h>
#include <stdint.h>

#define EVT_TEMP_READY    (1U << 0)   /* 온도 센서 준비 */
#define EVT_HUM_READY     (1U << 1)   /* 습도 센서 준비 */
#define EVT_LUX_READY     (1U << 2)   /* 조도 센서 준비 */
#define EVT_UART_TX_DONE  (1U << 3)   /* UART 전송 완료 */
#define EVT_ADC_CALIB     (1U << 4)   /* ADC 보정 완료 */

#define EVT_ALL_SENSORS   (EVT_TEMP_READY | EVT_HUM_READY | EVT_LUX_READY)

static EventGroupHandle_t xSensorEvents;

int main(void)
{
    /*
     * xEventGroupCreate():
     *   Heap에 Event Group 제어 블록을 할당합니다.
     *   모든 비트는 0으로 초기화됩니다.
     *   반환값이 NULL이면 Heap 부족 → configASSERT로 즉시 확인합니다.
     */
    xSensorEvents = xEventGroupCreate();
    configASSERT(xSensorEvents != NULL);

    /* Task 생성 및 스케줄러 시작 */
    vTaskStartScheduler();
    while (1);
}

2. xEventGroupSetBits()와 xEventGroupWaitBits()

2.1 비트 세트 — 이벤트 발생 알림

/*
 * xEventGroupSetBits(xEventGroup, uxBitsToSet):
 *   지정한 비트들을 세트합니다.
 *   세트 후 해당 비트를 기다리던 Task들의 대기 조건을 검사하고
 *   조건이 충족된 Task를 Unblock합니다.
 *
 *   반환값: 함수 반환 시점의 Event Group 비트 값
 *           (Unblock된 Task가 비트를 클리어했을 수도 있음)
 *
 * xEventGroupSetBitsFromISR():
 *   ISR 안전 버전. portYIELD_FROM_ISR()과 함께 사용합니다.
 */

/* 온도 센서 Task: 측정 완료 후 이벤트 비트 세트 */
void vTempSensorTask(void *pvParameters)
{
    while (1)
    {
        /* 온도 측정 시뮬레이션 (100ms) */
        vTaskDelay(pdMS_TO_TICKS(100));

        /* 측정 완료: 비트 0 세트 */
        xEventGroupSetBits(xSensorEvents, EVT_TEMP_READY);

        printf("[Temp] 측정 완료, 이벤트 세트\r\n");
    }
}

/* 습도 센서 Task */
void vHumSensorTask(void *pvParameters)
{
    while (1)
    {
        vTaskDelay(pdMS_TO_TICKS(150));

        xEventGroupSetBits(xSensorEvents, EVT_HUM_READY);

        printf("[Hum] 측정 완료, 이벤트 세트\r\n");
    }
}

/* 조도 센서 Task */
void vLuxSensorTask(void *pvParameters)
{
    while (1)
    {
        vTaskDelay(pdMS_TO_TICKS(200));

        xEventGroupSetBits(xSensorEvents, EVT_LUX_READY);

        printf("[Lux] 측정 완료, 이벤트 세트\r\n");
    }
}

2.2 비트 대기 — 이벤트 수신

/*
 * xEventGroupWaitBits(
 *   xEventGroup,      : 대기할 Event Group
 *   uxBitsToWaitFor,  : 대기할 비트 마스크
 *   xClearOnExit,     : pdTRUE = 조건 충족 시 해당 비트 자동 클리어
 *   xWaitForAllBits,  : pdTRUE = AND 대기 (모두 세트), pdFALSE = OR 대기 (하나라도)
 *   xTicksToWait      : 대기 타임아웃
 * )
 *
 * 반환값: 조건 충족 시점의 비트 값 (xClearOnExit 이전)
 *         타임아웃 시 현재 비트 값 (조건 미충족일 수 있음)
 */

/* AND 대기: 세 센서 모두 준비될 때까지 대기 */
void vDataProcessTask(void *pvParameters)
{
    while (1)
    {
        /*
         * EVT_ALL_SENSORS의 모든 비트가 세트될 때까지 Blocked 상태 유지
         *
         * xClearOnExit = pdTRUE:
         *   조건 충족 후 EVT_ALL_SENSORS 비트들을 자동으로 0으로 클리어합니다.
         *   클리어하지 않으면 다음 반복에서도 즉시 조건이 충족됩니다.
         *
         * xWaitForAllBits = pdTRUE:
         *   AND 조건: Bit 0 AND Bit 1 AND Bit 2가 모두 세트여야 합니다.
         */
        EventBits_t bits = xEventGroupWaitBits(
            xSensorEvents,
            EVT_ALL_SENSORS,   /* 대기할 비트: 0b00000111 */
            pdTRUE,            /* 조건 충족 시 해당 비트 클리어 */
            pdTRUE,            /* AND 조건 */
            pdMS_TO_TICKS(1000)/* 1초 타임아웃 */
        );

        if ((bits & EVT_ALL_SENSORS) == EVT_ALL_SENSORS)
        {
            /* 모든 센서 데이터 준비됨: 처리 시작 */
            printf("[Process] 세 센서 모두 준비 → 데이터 처리 시작\r\n");
            /* 실제 데이터 읽기 및 처리 */
        }
        else
        {
            /* 타임아웃: 일부 센서가 준비되지 않음 */
            printf("[Process] 타임아웃 (준비된 비트: 0x%02lX)\r\n",
                   (uint32_t)bits);
        }
    }
}

/* OR 대기: 어느 센서든 준비되는 즉시 처리 */
void vRealTimeLogTask(void *pvParameters)
{
    while (1)
    {
        /*
         * xWaitForAllBits = pdFALSE:
         *   OR 조건: Bit 0 OR Bit 1 OR Bit 2 중 하나라도 세트되면 즉시 Unblock
         *
         * xClearOnExit = pdTRUE:
         *   충족된 비트만 클리어됩니다.
         *   (OR 조건에서는 세트된 비트 전체를 클리어합니다)
         */
        EventBits_t bits = xEventGroupWaitBits(
            xSensorEvents,
            EVT_ALL_SENSORS,
            pdTRUE,    /* 클리어 */
            pdFALSE,   /* OR 조건 */
            portMAX_DELAY
        );

        if (bits & EVT_TEMP_READY)
            printf("[Log] 온도 데이터 수신\r\n");
        if (bits & EVT_HUM_READY)
            printf("[Log] 습도 데이터 수신\r\n");
        if (bits & EVT_LUX_READY)
            printf("[Log] 조도 데이터 수신\r\n");
    }
}

3. ISR에서 Event Group 사용

3.1 xEventGroupSetBitsFromISR()

/*
 * ISR에서는 xEventGroupSetBitsFromISR()을 사용합니다.
 * xQueueSendFromISR과 마찬가지로 portYIELD_FROM_ISR()이 필요합니다.
 *
 * 주의: xEventGroupSetBitsFromISR()은 내부적으로 Timer Daemon Task를 통해
 *       비트를 세트합니다. 따라서 configUSE_TIMERS = 1이 필요합니다.
 */

#include "FreeRTOS.h"
#include "event_groups.h"
#include "stm32f4xx_hal.h"

#define EVT_ADC_DONE    (1U << 0)
#define EVT_BUTTON_A    (1U << 1)
#define EVT_BUTTON_B    (1U << 2)

static EventGroupHandle_t xHwEvents;

/* ADC 변환 완료 콜백 (ISR 컨텍스트) */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    if (hadc->Instance != ADC1) return;

    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    /*
     * xEventGroupSetBitsFromISR():
     *   Timer Daemon Task 큐를 통해 비트 세트를 요청합니다.
     *   즉시 세트가 아님 — Daemon Task가 실행될 때 반영됩니다.
     *   configTIMER_TASK_PRIORITY가 충분히 높아야 지연이 줄어듭니다.
     */
    if (xEventGroupSetBitsFromISR(xHwEvents, EVT_ADC_DONE,
                                   &xHigherPriorityTaskWoken) == pdPASS)
    {
        portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
    }
}

/* EXTI 인터럽트 (버튼 A) */
void EXTI0_IRQHandler(void)
{
    __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);

    BaseType_t xWoken = pdFALSE;
    xEventGroupSetBitsFromISR(xHwEvents, EVT_BUTTON_A, &xWoken);
    portYIELD_FROM_ISR(xWoken);
}

4. 실습: 다중 센서 이벤트 처리 시스템

세 개의 센서 Task가 각각 독립적으로 측정을 수행하고, 처리 Task는 모든 센서가 준비될 때까지 대기한 후 데이터를 통합 처리합니다. 동시에 실시간 로그 Task는 개별 이벤트를 즉시 기록합니다.

#include "FreeRTOS.h"
#include "task.h"
#include "event_groups.h"
#include "queue.h"
#include "stm32f4xx_hal.h"
#include <stdio.h>
#include <stdint.h>

/* 이벤트 비트 정의 */
#define EVT_TEMP_READY    (1U << 0)
#define EVT_HUM_READY     (1U << 1)
#define EVT_LUX_READY     (1U << 2)
#define EVT_ERROR_TEMP    (1U << 3)
#define EVT_ERROR_HUM     (1U << 4)
#define EVT_ALL_SENSORS   (EVT_TEMP_READY | EVT_HUM_READY | EVT_LUX_READY)
#define EVT_ANY_ERROR     (EVT_ERROR_TEMP | EVT_ERROR_HUM)

/* 센서 데이터: Queue로 실제 값 전달 */
typedef struct
{
    float    temperature;
    float    humidity;
    uint16_t lux;
    uint32_t timestamp_ms;
} EnvSnapshot_t;

static EventGroupHandle_t xSensorEvents;
static QueueHandle_t      xSnapshotQueue;

/* 공유 센서 값: 각 Task가 채움, Process Task가 스냅샷 */
static volatile float    g_temp = 0.0f;
static volatile float    g_hum  = 0.0f;
static volatile uint16_t g_lux  = 0;

/* ---- 센서 Task들 ---- */
void vTempTask(void *pvParameters)
{
    while (1)
    {
        vTaskDelay(pdMS_TO_TICKS(100));

        /* NTC 온도 센서 읽기 시뮬레이션 */
        g_temp = 25.0f + (float)(xTaskGetTickCount() % 100) * 0.1f;

        xEventGroupSetBits(xSensorEvents, EVT_TEMP_READY);
    }
}

void vHumTask(void *pvParameters)
{
    while (1)
    {
        vTaskDelay(pdMS_TO_TICKS(150));

        /* SHT31 습도 센서 읽기 시뮬레이션 */
        g_hum = 60.0f + (float)(xTaskGetTickCount() % 200) * 0.05f;

        xEventGroupSetBits(xSensorEvents, EVT_HUM_READY);
    }
}

void vLuxTask(void *pvParameters)
{
    while (1)
    {
        vTaskDelay(pdMS_TO_TICKS(200));

        /* BH1750 조도 센서 읽기 시뮬레이션 */
        g_lux = (uint16_t)(100U + (xTaskGetTickCount() / 10U) % 900U);

        xEventGroupSetBits(xSensorEvents, EVT_LUX_READY);
    }
}

/* ---- 통합 처리 Task: AND 대기 ---- */
void vProcessTask(void *pvParameters)
{
    EnvSnapshot_t snap;

    while (1)
    {
        /*
         * 세 센서 모두 준비될 때까지 최대 500ms 대기
         * 조건 충족 시 EVT_ALL_SENSORS 비트 자동 클리어 (다음 사이클 준비)
         */
        EventBits_t bits = xEventGroupWaitBits(
            xSensorEvents,
            EVT_ALL_SENSORS,
            pdTRUE,             /* 클리어 */
            pdTRUE,             /* AND */
            pdMS_TO_TICKS(500)
        );

        if ((bits & EVT_ALL_SENSORS) == EVT_ALL_SENSORS)
        {
            /* 스냅샷 생성 */
            snap.temperature  = g_temp;
            snap.humidity     = g_hum;
            snap.lux          = g_lux;
            snap.timestamp_ms = xTaskGetTickCount();

            /* Queue로 전달 */
            xQueueSend(xSnapshotQueue, &snap, 0);
        }
        else
        {
            /* 타임아웃: 미준비 센서 확인 */
            if (!(bits & EVT_TEMP_READY)) printf("[Warn] 온도 센서 타임아웃\r\n");
            if (!(bits & EVT_HUM_READY))  printf("[Warn] 습도 센서 타임아웃\r\n");
            if (!(bits & EVT_LUX_READY))  printf("[Warn] 조도 센서 타임아웃\r\n");
        }
    }
}

/* ---- 출력 Task: Queue에서 스냅샷 수신 ---- */
void vPrintTask(void *pvParameters)
{
    EnvSnapshot_t snap;

    while (1)
    {
        if (xQueueReceive(xSnapshotQueue, &snap, portMAX_DELAY) == pdTRUE)
        {
            printf("[%5lums] T=%.1f°C H=%.1f%% L=%uLux\r\n",
                   snap.timestamp_ms,
                   snap.temperature,
                   snap.humidity,
                   snap.lux);
        }
    }
}

/* ---- main ---- */
int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_USART2_UART_Init();

    xSensorEvents  = xEventGroupCreate();
    xSnapshotQueue = xQueueCreate(10, sizeof(EnvSnapshot_t));

    configASSERT(xSensorEvents  != NULL);
    configASSERT(xSnapshotQueue != NULL);

    xTaskCreate(vTempTask,    "Temp",    256, NULL, 3, NULL);
    xTaskCreate(vHumTask,     "Hum",     256, NULL, 3, NULL);
    xTaskCreate(vLuxTask,     "Lux",     256, NULL, 3, NULL);
    xTaskCreate(vProcessTask, "Process", 512, NULL, 2, NULL);
    xTaskCreate(vPrintTask,   "Print",   512, NULL, 1, NULL);

    vTaskStartScheduler();
    while (1);
}

학습 정리

항목내용
xEventGroupCreateHeap에 Event Group 할당, 모든 비트 0 초기화
xEventGroupSetBits비트 세트 후 대기 Task의 조건 자동 검사
xEventGroupWaitBitsAND(pdTRUE) / OR(pdFALSE) 조건 대기, 타임아웃 지원
xClearOnExit = pdTRUE조건 충족 시 해당 비트 자동 클리어 (재사용 준비)
xEventGroupSetBitsFromISRISR 전용, Timer Daemon 경유, configUSE_TIMERS 필요
Queue와 차이Event Group은 데이터 없는 신호만, 실제 값은 Queue로 별도 전달

실습 과제

  1. 위 예제에서 vTempTask에 의도적으로 200ms 지연을 추가하여 타임아웃 경고가 출력되는 조건을 만들고, 타임아웃 횟수를 카운터로 기록하여 5번 연속 타임아웃 시 EVT_ERROR_TEMP 비트를 세트하는 로직을 작성하십시오.
  2. EVT_ANY_ERROR를 OR 조건으로 대기하는 에러 핸들러 Task를 추가하고, 에러 발생 시 모든 센서 Task를 일시 중단(vTaskSuspend)하고 3초 후 재개(vTaskResume)하는 복구 시퀀스를 구현하십시오.
  3. xEventGroupGetBits()로 현재 비트 상태를 읽어 1초마다 비트 상태를 16진수로 출력하는 모니터 Task를 추가하십시오.

디버깅 팁

  1. xEventGroupWaitBits()가 즉시 반환되고 비트가 이미 세트되어 있다면 이전 사이클에서 xClearOnExit = pdTRUE로 클리어되지 않은 것입니다. xClearOnExit 값을 확인합니다.
  2. ISR에서 xEventGroupSetBitsFromISR()을 호출했는데 Task가 깨어나지 않으면 configUSE_TIMERS = 1과 Timer Daemon Task 스택 크기(configTIMER_TASK_STACK_DEPTH)를 확인합니다. Daemon Task가 스택 오버플로로 죽었을 수 있습니다.
  3. AND 대기에서 일부 비트만 세트되고 멈춰있다면 해당 센서 Task가 실행되고 있는지 vTaskList()로 Task 상태를 확인합니다. 높은 우선순위 Task에 의해 선점되어 실행 기회를 못 받을 수 있습니다.
  4. Event Group과 Queue를 혼용할 때 비트가 클리어되는 시점과 Queue에서 데이터를 꺼내는 시점이 어긋나면 데이터 불일치가 발생합니다. 비트 클리어는 데이터를 Queue에서 꺼낸 후에 처리하거나, 스냅샷 방식으로 설계합니다.
profile
당신의 코딩 메이트

0개의 댓글