Week 4 Day 3: Queue 고급 활용

학습 목표

  • xQueueSendFromISR()portYIELD_FROM_ISR()을 정확히 이해하고 ISR-Task 파이프라인을 구현한다
  • xQueuePeek()xQueueReset()의 용도를 파악하고 시스템 복구 시나리오에 적용한다
  • 여러 Task가 하나의 Queue를 공유할 때 발생하는 경쟁 조건과 안전한 설계 방법을 이해한다
  • ISR에서 Queue로 데이터를 전달하는 완전한 ADC-UART 파이프라인을 구현한다

Day 17에서 xQueueSend/Receive의 Blocking과 Timeout 동작을 다뤘습니다. 이번 시간에는 ISR에서의 Queue 사용(xQueueSendFromISR), Peek/Reset 유틸리티 API, 그리고 여러 Task가 Queue를 공유하는 패턴을 심화 학습합니다.


1. xQueueSendFromISR() 심화

1.1 portYIELD_FROM_ISR()의 역할

Day 16에서 xQueueSendFromISR()의 기본 사용법을 소개했습니다. 이번에는 pxHigherPriorityTaskWokenportYIELD_FROM_ISR()이 실제로 어떤 일을 하는지 살펴봅니다.

Queue에 데이터를 넣으면 그 Queue를 기다리던 Task가 Blocked 상태에서 Ready 상태로 전환됩니다. 만약 그 Task의 우선순위가 현재 실행 중이던 Task보다 높다면, ISR이 끝난 직후 즉시 컨텍스트 전환이 일어나야 합니다. portYIELD_FROM_ISR()은 이 컨텍스트 전환 요청을 ISR 종료 직전에 등록합니다.

/*
 * portYIELD_FROM_ISR() 동작 비교
 *
 * [호출 안 했을 때]
 *   ISR 완료 → 이전에 실행 중이던 낮은 우선순위 Task 재개
 *   → 다음 스케줄러 틱(최대 1ms 지연)에서야 높은 우선순위 Task 실행
 *
 * [호출 했을 때]
 *   ISR 완료 → 즉시 컨텍스트 전환 → 높은 우선순위 Task 실행
 *   → 실시간 응답성 확보 (최대 수십 µs 지연)
 */

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

typedef struct
{
    uint16_t raw;
    uint32_t tick;
} AdcData_t;

static QueueHandle_t xAdcQueue;

/* ADC 변환 완료 인터럽트 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;

    AdcData_t data;
    data.raw  = (uint16_t)HAL_ADC_GetValue(hadc);
    data.tick = xTaskGetTickCountFromISR();

    /*
     * xQueueSendFromISR() 내부 동작:
     *   1. 큐에 데이터 복사
     *   2. 이 큐를 기다리던 Task가 있으면 Ready 상태로 전환
     *   3. 해제된 Task의 우선순위가 현재 Task보다 높으면
     *      xHigherPriorityTaskWoken = pdTRUE 세트
     */
    xQueueSendFromISR(xAdcQueue, &data, &xHigherPriorityTaskWoken);

    /*
     * portYIELD_FROM_ISR(pdTRUE):
     *   ISR 종료 직전에 PendSV 인터럽트를 등록합니다.
     *   ISR 스택 언와인딩이 끝나면 PendSV가 실행되어 컨텍스트를 전환합니다.
     *
     * portYIELD_FROM_ISR(pdFALSE):
     *   아무 일도 하지 않습니다. 스케줄러 틱까지 대기합니다.
     */
    portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}

/* 높은 우선순위 ADC 처리 Task */
void vAdcHighPrioTask(void *pvParameters)
{
    AdcData_t data;

    while (1)
    {
        /*
         * portMAX_DELAY: ISR이 Queue에 넣을 때까지 Blocked 상태 유지
         * ISR → xQueueSendFromISR → portYIELD_FROM_ISR → 즉시 이 Task 실행
         */
        xQueueReceive(xAdcQueue, &data, portMAX_DELAY);

        float voltage = data.raw * 3.3f / 4095.0f;
        printf("[ADC] tick=%lu raw=%u volt=%.3fV\r\n",
               data.tick, data.raw, voltage);
    }
}

1.2 ISR에서 Queue 수신

ISR에서 Queue를 읽는 패턴(xQueueReceiveFromISR)은 드물지만, 여러 Task가 미리 준비한 응답을 ISR이 즉시 꺼내 하드웨어로 전송해야 할 때 사용합니다.

/*
 * 패턴: Task가 송신 데이터를 xTxQueue에 미리 준비해두면
 *       USART TX 인터럽트(TXE)에서 xQueueReceiveFromISR로 꺼내 전송합니다.
 * 이렇게 하면 UART DMA 없이도 Task 간 블로킹 없이 비동기 전송이 가능합니다.
 */

static QueueHandle_t xTxQueue;   /* 전송 대기 바이트 Queue */

/* USART2 TX 인터럽트: TXE (송신 레지스터 비어있음) 발생 시 */
void USART2_IRQHandler(void)
{
    BaseType_t xWoken = pdFALSE;
    uint8_t    byte_to_send;

    if (USART2->SR & USART_SR_TXE)
    {
        if (xQueueReceiveFromISR(xTxQueue, &byte_to_send, &xWoken) == pdTRUE)
        {
            USART2->DR = byte_to_send;   /* 레지스터에 직접 쓰기 */
        }
        else
        {
            /* 보낼 데이터 없음: TXE 인터럽트 비활성화 */
            USART2->CR1 &= ~USART_CR1_TXEIE;
        }
    }

    portYIELD_FROM_ISR(xWoken);
}

/* Task: 문자열을 바이트 단위로 Queue에 넣기 */
void vUartSendTask(void *pvParameters)
{
    const char *msg = "Hello RTOS\r\n";

    while (1)
    {
        for (const char *p = msg; *p; p++)
        {
            xQueueSend(xTxQueue, p, portMAX_DELAY);
        }

        /* 첫 바이트 삽입 후 TXE 인터럽트 활성화 */
        USART2->CR1 |= USART_CR1_TXEIE;

        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

2. xQueuePeek()과 xQueueReset()

2.1 xQueuePeek() 활용 시나리오

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

typedef struct
{
    uint8_t  priority;   /* 0=일반, 1=경고, 2=위험 */
    uint32_t data;
} Msg_t;

static QueueHandle_t xMsgQueue;

/*
 * xQueuePeek()을 사용하면 다음 아이템의 내용을 확인하고
 * 처리 가능한 상태일 때만 실제로 꺼낼 수 있습니다.
 *
 * 사용 사례:
 *   - 다음 메시지가 특정 우선순위 이상일 때만 처리
 *   - 메시지 타입에 따라 다른 Task로 라우팅
 *   - 처리 여부를 결정하기 전에 내용 확인
 */
void vFilterConsumerTask(void *pvParameters)
{
    Msg_t msg;

    while (1)
    {
        /* 내용 확인만: 큐에 아이템 남아있음 */
        if (xQueuePeek(xMsgQueue, &msg, pdMS_TO_TICKS(100)) == pdTRUE)
        {
            if (msg.priority >= 1)
            {
                /* 경고 이상: 즉시 처리 */
                xQueueReceive(xMsgQueue, &msg, 0);   /* 실제로 꺼냄 */
                printf("[HIGH] priority=%u data=%lu\r\n", msg.priority, msg.data);
            }
            else
            {
                /* 일반 메시지: 저우선순위 Task에게 양보 */
                vTaskDelay(pdMS_TO_TICKS(50));
            }
        }
    }
}

/*
 * xQueueReset(): Queue의 모든 아이템을 삭제하고 빈 상태로 초기화합니다.
 *
 * 사용 사례:
 *   - 시스템 오류 복구 후 이전 데이터를 모두 버려야 할 때
 *   - 통신 프로토콜 재동기화 (이전 수신 데이터 무효화)
 *   - 테스트 케이스 간 초기화
 *
 * 주의: xQueueReset()은 현재 Queue를 기다리는 Task를 Unblock하지 않습니다.
 *       기다리는 Task가 있으면 타임아웃까지 계속 기다립니다.
 */
void system_error_recovery(void)
{
    printf("[Recovery] 큐 초기화 시작\r\n");

    xQueueReset(xMsgQueue);   /* 모든 아이템 삭제 */

    printf("[Recovery] 큐 초기화 완료, 대기 중: %u\r\n",
           (unsigned)uxQueueMessagesWaiting(xMsgQueue));
}

2.2 Queue 상태 조회 API 전체 정리

/*
 * Queue 상태를 확인하는 모든 API 정리
 * ISR 버전은 FromISR 접미사가 붙으며 xHigherPriorityTaskWoken이 없는 것도 있습니다.
 */

void vQueueStatusDemo(QueueHandle_t xQueue)
{
    /* 현재 저장된 아이템 수 */
    UBaseType_t waiting = uxQueueMessagesWaiting(xQueue);

    /* 추가로 저장 가능한 아이템 수 (큐 깊이 - 현재 아이템) */
    UBaseType_t free_space = uxQueueSpacesAvailable(xQueue);

    printf("대기: %u, 여유: %u\r\n",
           (unsigned)waiting, (unsigned)free_space);

    /* ISR 버전 */
    /* UBaseType_t isr_waiting = uxQueueMessagesWaitingFromISR(xQueue); */
}

3. 여러 Task가 Queue 공유

3.1 멀티 생산자 패턴

여러 Task가 동시에 같은 Queue에 데이터를 넣는 경우, FreeRTOS가 내부적으로 Queue 접근을 보호합니다. 별도의 뮤텍스 없이 안전하게 사용할 수 있습니다.

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

/*
 * 여러 센서 Task가 하나의 Queue로 데이터를 보내고
 * 하나의 처리 Task가 꺼내 로깅하는 구조입니다.
 *
 * FreeRTOS Queue는 내부에 뮤텍스를 가지고 있어
 * 여러 Task가 동시에 Send/Receive를 호출해도 안전합니다.
 */

typedef struct
{
    uint8_t  source_id;   /* 데이터 출처 식별자 */
    uint16_t value;
    uint32_t timestamp;
} SensorMsg_t;

static QueueHandle_t xSensorQueue;

/* 센서 1: 온도 (100ms 주기) */
void vTempSensorTask(void *pvParameters)
{
    SensorMsg_t msg;
    msg.source_id = 1;   /* 온도 센서 ID */

    while (1)
    {
        msg.value     = (uint16_t)(25 + (xTaskGetTickCount() % 10));   /* 시뮬레이션 */
        msg.timestamp = xTaskGetTickCount();

        if (xQueueSend(xSensorQueue, &msg, pdMS_TO_TICKS(50)) != pdTRUE)
            printf("[Temp] 큐 포화\r\n");

        vTaskDelay(pdMS_TO_TICKS(100));
    }
}

/* 센서 2: 습도 (200ms 주기) */
void vHumSensorTask(void *pvParameters)
{
    SensorMsg_t msg;
    msg.source_id = 2;   /* 습도 센서 ID */

    while (1)
    {
        msg.value     = (uint16_t)(60 + (xTaskGetTickCount() % 20));
        msg.timestamp = xTaskGetTickCount();

        if (xQueueSend(xSensorQueue, &msg, pdMS_TO_TICKS(50)) != pdTRUE)
            printf("[Hum] 큐 포화\r\n");

        vTaskDelay(pdMS_TO_TICKS(200));
    }
}

/* 처리 Task: 출처별 분기 */
void vLoggerTask(void *pvParameters)
{
    SensorMsg_t msg;
    const char *source_names[] = {"?", "온도", "습도"};

    while (1)
    {
        xQueueReceive(xSensorQueue, &msg, portMAX_DELAY);

        const char *name = (msg.source_id < 3) ? source_names[msg.source_id] : "미상";
        printf("[Logger] %s: %u @%lums\r\n", name, msg.value, msg.timestamp);
    }
}

int main(void)
{
    xSensorQueue = xQueueCreate(20, sizeof(SensorMsg_t));
    configASSERT(xSensorQueue != NULL);

    xTaskCreate(vTempSensorTask, "Temp",   256, NULL, 3, NULL);
    xTaskCreate(vHumSensorTask,  "Hum",    256, NULL, 3, NULL);
    xTaskCreate(vLoggerTask,     "Logger", 512, NULL, 2, NULL);   /* 낮은 우선순위 */

    vTaskStartScheduler();
    while (1);
}

3.2 멀티 소비자 패턴의 주의사항

/*
 * 여러 Task가 같은 Queue에서 데이터를 꺼내는 멀티 소비자 패턴은
 * 하나의 아이템이 하나의 Task에만 전달되므로 의도적으로 사용해야 합니다.
 *
 * 안전한 사용 사례:
 *   - 작업 큐(Work Queue): 여러 Worker Task 중 하나가 처리
 *     → 처리 순서가 중요하지 않을 때 병렬 처리 효과
 *
 * 위험한 사용 사례:
 *   - 모든 소비자가 모든 아이템을 봐야 하는 Broadcast 패턴
 *     → Queue는 Broadcast를 지원하지 않음
 *     → 이 경우 EventGroup 또는 별도의 Queue를 각 소비자에게 배분
 */

#define WORKER_COUNT  3

/* 작업 큐: 여러 Worker 중 하나가 처리 (병렬 작업 분산) */
void vWorkerTask(void *pvParameters)
{
    uint8_t worker_id = (uint8_t)(uintptr_t)pvParameters;
    uint32_t job;

    while (1)
    {
        /*
         * 세 Worker 중 먼저 xQueueReceive를 호출한 하나만 아이템을 가져갑니다.
         * FreeRTOS가 내부적으로 경쟁을 해결합니다.
         */
        if (xQueueReceive(xSensorQueue, &job, portMAX_DELAY) == pdTRUE)
        {
            printf("[Worker %u] 작업 처리: %lu\r\n", worker_id, job);
            vTaskDelay(pdMS_TO_TICKS(100));   /* 작업 시뮬레이션 */
        }
    }
}

4. 실습: ISR에서 Queue로 데이터 전달 파이프라인

실습: ADC ISR → Queue → 처리 Task → UART 출력

TIM2 타이머 인터럽트가 ADC 샘플링을 트리거하고, ADC 완료 인터럽트에서 Queue로 전달하면 처리 Task가 꺼내 UART로 출력하는 완전한 파이프라인을 구현합니다.

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

#define QUEUE_DEPTH  32

typedef struct
{
    uint16_t raw;
    uint32_t tick;
    uint8_t  channel;
} AdcSample_t;

static QueueHandle_t    xAdcQueue;
static volatile bool    g_adc_triggered = false;

/* TIM2 100ms 주기: ADC 변환 트리거 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
    if (htim->Instance != TIM2) return;

    /* 소프트웨어 트리거로 ADC 변환 시작 */
    HAL_ADC_Start_IT(&hadc1);
}

/* ADC 변환 완료 인터럽트 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
    if (hadc->Instance != ADC1) return;

    BaseType_t xWoken = pdFALSE;

    AdcSample_t sample;
    sample.raw     = (uint16_t)HAL_ADC_GetValue(hadc);
    sample.tick    = xTaskGetTickCountFromISR();
    sample.channel = 0;   /* 단일 채널 예제 */

    /* 큐가 꽉 찼으면 데이터 드롭 (ISR에서 대기 불가) */
    if (xQueueSendFromISR(xAdcQueue, &sample, &xWoken) != pdTRUE)
    {
        /* 드롭 카운터 증가 (static: 이 함수에서만 접근) */
        static uint32_t drop_count = 0;
        drop_count++;
    }

    portYIELD_FROM_ISR(xWoken);
}

/* 처리 Task: Queue에서 데이터를 꺼내 이동평균 적용 후 UART 출력 */
void vAdcProcessTask(void *pvParameters)
{
    AdcSample_t sample;

#define AVG_WIN  8
    static uint16_t avg_buf[AVG_WIN] = {0};
    static uint8_t  avg_idx          = 0;
    static uint32_t avg_sum          = 0;
    static uint8_t  avg_cnt          = 0;

    while (1)
    {
        xQueueReceive(xAdcQueue, &sample, portMAX_DELAY);

        /* 이동평균 */
        avg_sum -= avg_buf[avg_idx];
        avg_buf[avg_idx] = sample.raw;
        avg_sum += sample.raw;
        avg_idx  = (avg_idx + 1) % AVG_WIN;
        if (avg_cnt < AVG_WIN) avg_cnt++;

        uint16_t avg_raw = (uint16_t)(avg_sum / avg_cnt);
        float    voltage = avg_raw * 3.3f / 4095.0f;

        printf("ch%u raw=%4u avg=%4u volt=%.3fV @%lums\r\n",
               sample.channel, sample.raw, avg_raw, voltage, sample.tick);
    }
}

/* 모니터 Task: 1초마다 큐 상태 출력 */
void vMonitorTask(void *pvParameters)
{
    while (1)
    {
        vTaskDelay(pdMS_TO_TICKS(1000));
        printf("[Monitor] 큐 대기: %u, 여유: %u\r\n",
               (unsigned)uxQueueMessagesWaiting(xAdcQueue),
               (unsigned)uxQueueSpacesAvailable(xAdcQueue));
    }
}

int main(void)
{
    HAL_Init();
    SystemClock_Config();
    MX_GPIO_Init();
    MX_ADC1_Init();
    MX_TIM2_Init();
    MX_USART2_UART_Init();

    HAL_ADCEx_Calibration_Start(&hadc1);

    xAdcQueue = xQueueCreate(QUEUE_DEPTH, sizeof(AdcSample_t));
    configASSERT(xAdcQueue != NULL);

    xTaskCreate(vAdcProcessTask, "AdcProc",  512, NULL, 3, NULL);
    xTaskCreate(vMonitorTask,    "Monitor",  256, NULL, 1, NULL);

    HAL_TIM_Base_Start_IT(&htim2);

    vTaskStartScheduler();
    while (1);
}

학습 정리

항목내용
portYIELD_FROM_ISRpdTRUE 전달 시 ISR 종료 후 즉시 PendSV로 컨텍스트 전환
xQueueSendFromISRISR 전용, xTicksToWait 없음, 큐 포화 시 즉시 실패
xQueueReceiveFromISRISR에서 큐 꺼내기, UART TX 비동기 전송 패턴에 활용
xQueuePeek아이템 확인만, 큐에서 제거하지 않음
xQueueReset모든 아이템 삭제, 오류 복구 시 사용
멀티 생산자FreeRTOS 내부 보호로 별도 뮤텍스 없이 안전
멀티 소비자하나의 아이템은 하나의 Task만 수신, Broadcast 불가

실습 과제

  1. 위 실습 코드에서 TIM2 주기를 50ms로 줄이고 처리 Task에 200ms 지연을 추가하여 큐 포화가 발생하는 조건을 만든 뒤, 드롭 카운터가 출력에 표시되도록 수정하십시오.
  2. xQueueReset()을 호출하는 복구 Task를 추가하고, 드롭 카운터가 10 이상이 되면 자동으로 큐를 초기화한 후 카운터를 0으로 리셋하는 기능을 구현하십시오.
  3. 멀티 소비자 예제를 참고하여 Worker Task 3개가 처리 결과를 각각 다른 LED 핀에 출력하는 병렬 처리 시스템을 구현하십시오.

디버깅 팁

  1. portYIELD_FROM_ISR()을 생략하면 ISR이 끝나도 즉시 스케줄러가 전환하지 않는다. 응답 시간이 예상보다 길다면 먼저 이 매크로가 빠져있는지 확인한다.
  2. ISR 내에서 uxQueueMessagesWaiting() 대신 uxQueueMessagesWaitingFromISR()을 사용해야 한다. Task 전용 API를 ISR에서 호출하면 FreeRTOS 내부 어설션이 실패한다.
  3. 큐가 매우 빠르게 포화된다면 큐 깊이 증가 외에 소비자 Task 우선순위를 생산자보다 높게 설정하는 방법을 먼저 시도한다. 우선순위 역전이 포화의 원인인 경우가 많다.
  4. 멀티 소비자에서 예상보다 한 Task에만 메시지가 집중된다면 소비자 Task들의 우선순위와 지연 시간을 점검한다. 우선순위가 같으면 Round-Robin으로 동작하지만 vTaskDelay() 시점에 따라 편중이 생길 수 있다.
profile
당신의 코딩 메이트

0개의 댓글