
xQueueSendFromISR()과 portYIELD_FROM_ISR()을 정확히 이해하고 ISR-Task 파이프라인을 구현한다xQueuePeek()과 xQueueReset()의 용도를 파악하고 시스템 복구 시나리오에 적용한다Day 17에서 xQueueSend/Receive의 Blocking과 Timeout 동작을 다뤘습니다. 이번 시간에는 ISR에서의 Queue 사용(xQueueSendFromISR), Peek/Reset 유틸리티 API, 그리고 여러 Task가 Queue를 공유하는 패턴을 심화 학습합니다.
Day 16에서 xQueueSendFromISR()의 기본 사용법을 소개했습니다. 이번에는 pxHigherPriorityTaskWoken과 portYIELD_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);
}
}
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));
}
}
#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));
}
/*
* 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); */
}
여러 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);
}
/*
* 여러 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)); /* 작업 시뮬레이션 */
}
}
}
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_ISR | pdTRUE 전달 시 ISR 종료 후 즉시 PendSV로 컨텍스트 전환 |
| xQueueSendFromISR | ISR 전용, xTicksToWait 없음, 큐 포화 시 즉시 실패 |
| xQueueReceiveFromISR | ISR에서 큐 꺼내기, UART TX 비동기 전송 패턴에 활용 |
| xQueuePeek | 아이템 확인만, 큐에서 제거하지 않음 |
| xQueueReset | 모든 아이템 삭제, 오류 복구 시 사용 |
| 멀티 생산자 | FreeRTOS 내부 보호로 별도 뮤텍스 없이 안전 |
| 멀티 소비자 | 하나의 아이템은 하나의 Task만 수신, Broadcast 불가 |
portYIELD_FROM_ISR()을 생략하면 ISR이 끝나도 즉시 스케줄러가 전환하지 않는다. 응답 시간이 예상보다 길다면 먼저 이 매크로가 빠져있는지 확인한다.uxQueueMessagesWaiting() 대신 uxQueueMessagesWaitingFromISR()을 사용해야 한다. Task 전용 API를 ISR에서 호출하면 FreeRTOS 내부 어설션이 실패한다.vTaskDelay() 시점에 따라 편중이 생길 수 있다.