
xQueueCreate(), xQueueSend(), xQueueReceive()의 기본 사용법을 익힌다Week 3에서 학습한 Semaphore는 이벤트 발생 여부만 전달합니다. "ADC 변환이 완료되었다"는 신호는 줄 수 있지만 "ADC 값이 2048이다"라는 데이터 자체는 함께 전달하지 못합니다. 데이터를 전달하려면 Semaphore와 별개로 전역 변수를 두어야 하고, 이때 volatile 선언과 임계 구역 처리를 직접 관리해야 합니다.
Queue는 이벤트 신호와 데이터를 하나의 메커니즘으로 통합합니다. 생산자(Producer)가 Queue에 데이터를 넣으면 소비자(Consumer)가 꺼내며, FreeRTOS가 내부적으로 복사와 동기화를 모두 처리합니다.
| 비교 항목 | Semaphore (Binary/Counting) | Queue |
|---|---|---|
| 전달 내용 | 신호(이벤트 발생 여부)만 | 데이터 값 자체 |
| 전역 변수 필요 | 데이터용 전역 변수 별도 필요 | 불필요 (Queue 내부 버퍼 사용) |
| 복수 데이터 누적 | Counting Semaphore로 부분 가능 | 큐 깊이만큼 누적 가능 |
| 데이터 타입 | 없음 | 생성 시 지정한 임의 타입 |
| ISR 사용 | xSemaphoreGiveFromISR() | xQueueSendFromISR() |
FreeRTOS Queue는 선입선출(FIFO) 방식으로 동작합니다. 데이터를 xQueueSend()로 넣으면 FreeRTOS가 내부 버퍼에 데이터를 바이트 단위로 복사하고, xQueueReceive()로 꺼낼 때 다시 복사합니다. 포인터가 아닌 값 자체를 복사하므로, 송신 측 변수가 이후에 변경되어도 수신 측에는 영향이 없습니다.
[Queue 내부 구조: 깊이 4, 아이템 크기 sizeof(uint32_t)]
Send: 10 → [10][ ][ ][ ] head=0, tail=1
Send: 20 → [10][20][ ][ ] head=0, tail=2
Send: 30 → [10][20][30][ ] head=0, tail=3
Recv: 10 ← [ ][20][30][ ] head=1, tail=3
Send: 40 → [ ][20][30][40] head=1, tail=0 (wrap)
Recv: 20 ← [ ][ ][30][40] head=2, tail=0
아이템을 꺼낼 때 FreeRTOS가 out 버퍼로 memcpy 수행
Queue 아이템은 포인터가 아닌 값을 복사합니다. 구조체를 아이템으로 사용할 때 구조체가 크면 복사 오버헤드가 있습니다. 이 경우 구조체 포인터를 아이템으로 사용하는 패턴을 쓰지만, 포인터가 가리키는 메모리의 생명주기 관리에 주의해야 합니다.
/* FreeRTOSConfig.h — Queue는 별도 설정 없이 기본 활성화 */
#include "FreeRTOS.h"
#include "queue.h"
/*
* xQueueCreate(uxQueueLength, uxItemSize)
* uxQueueLength : 동시에 보관 가능한 아이템 수 (큐 깊이)
* uxItemSize : 아이템 하나의 크기 (바이트), sizeof()로 지정
* 반환값 : QueueHandle_t, 실패 시 NULL
*/
/* uint32_t 4개를 보관할 수 있는 Queue */
QueueHandle_t xQueue = xQueueCreate(4, sizeof(uint32_t));
configASSERT(xQueue != NULL);
/* 구조체를 아이템으로 사용 */
typedef struct
{
uint16_t sensor_id;
uint32_t timestamp_ms;
float value;
} SensorMsg_t;
QueueHandle_t xSensorQueue = xQueueCreate(8, sizeof(SensorMsg_t));
configASSERT(xSensorQueue != NULL);
/*
* xQueueSend(xQueue, pvItemToQueue, xTicksToWait)
* pvItemToQueue : 전송할 데이터의 주소 (FreeRTOS가 내부 버퍼로 복사)
* xTicksToWait : 큐가 꽉 찼을 때 대기할 최대 틱 수
* 0 : 즉시 반환 (비차단)
* portMAX_DELAY : 무한 대기
* 반환값 : pdTRUE(성공) / errQUEUE_FULL(타임아웃 또는 즉시 실패)
*
* xQueueSendToBack() : xQueueSend()와 동일 (FIFO 끝에 추가)
* xQueueSendToFront() : 우선 처리가 필요한 아이템을 FIFO 앞에 삽입
*/
void vProducerTask(void *pvParameters)
{
uint32_t count = 0;
while (1)
{
count++;
/* 큐에 빈 공간이 생길 때까지 최대 100ms 대기 */
if (xQueueSend(xQueue, &count, pdMS_TO_TICKS(100)) == pdTRUE)
{
printf("[Producer] 전송: %lu\n", count);
}
else
{
printf("[Producer] 큐 포화, 데이터 %lu 버림\n", count);
}
vTaskDelay(pdMS_TO_TICKS(200));
}
}
/*
* xQueueReceive(xQueue, pvBuffer, xTicksToWait)
* pvBuffer : 수신 데이터를 저장할 버퍼의 주소
* xTicksToWait : 큐가 비었을 때 대기할 최대 틱 수
* 반환값 : pdTRUE(성공) / pdFALSE(타임아웃)
*
* xQueuePeek() : 아이템을 꺼내지 않고 복사만 (큐에 아이템이 남음)
*/
void vConsumerTask(void *pvParameters)
{
uint32_t received;
while (1)
{
/* 아이템이 들어올 때까지 무한 대기 */
if (xQueueReceive(xQueue, &received, portMAX_DELAY) == pdTRUE)
{
printf("[Consumer] 수신: %lu\n", received);
}
}
}
/*
* uxQueueMessagesWaiting(xQueue) : 현재 저장된 아이템 수
* uxQueueSpacesAvailable(xQueue) : 추가로 저장 가능한 아이템 수
*
* ISR 안에서 사용하려면 FromISR 버전 사용:
* uxQueueMessagesWaitingFromISR(xQueue)
*/
void vMonitorTask(void *pvParameters)
{
while (1)
{
vTaskDelay(pdMS_TO_TICKS(1000));
UBaseType_t waiting = uxQueueMessagesWaiting(xQueue);
UBaseType_t free = uxQueueSpacesAvailable(xQueue);
printf("Queue 상태 — 대기: %u, 여유: %u\n",
(unsigned)waiting, (unsigned)free);
}
}
ISR에서는 일반 xQueueSend() 대신 xQueueSendFromISR()을 사용해야 합니다. 일반 API는 내부적으로 Task 스케줄러에 의존하며 차단 동작을 포함할 수 있어 인터럽트 컨텍스트에서 호출하면 시스템이 멈춥니다.
/*
* xQueueSendFromISR(xQueue, pvItemToQueue, pxHigherPriorityTaskWoken)
* pxHigherPriorityTaskWoken : 더 높은 우선순위 Task가 깨어났으면 pdTRUE 세트
* 반환값 : pdTRUE(성공) / errQUEUE_FULL(즉시 실패)
*
* ISR에서는 xTicksToWait 매개변수가 없습니다. 큐가 꽉 찼으면 즉시 실패합니다.
*/
typedef struct
{
uint16_t raw;
uint32_t tick;
} AdcMsg_t;
QueueHandle_t xAdcQueue;
/* ADC 변환 완료 인터럽트 */
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
AdcMsg_t msg;
msg.raw = (uint16_t)HAL_ADC_GetValue(hadc);
msg.tick = xTaskGetTickCountFromISR();
/* 큐가 꽉 찼으면 errQUEUE_FULL 반환, 데이터 드롭 */
xQueueSendFromISR(xAdcQueue, &msg, &xHigherPriorityTaskWoken);
/* 높은 우선순위 Task가 깨어났으면 ISR 종료 후 즉시 컨텍스트 전환 */
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
/* ADC 처리 Task */
void vAdcProcessTask(void *pvParameters)
{
AdcMsg_t msg;
while (1)
{
xQueueReceive(xAdcQueue, &msg, portMAX_DELAY);
float voltage = msg.raw * 3.3f / 4095.0f;
printf("[ADC] tick=%lu raw=%u volt=%.3fV\n",
msg.tick, msg.raw, voltage);
}
}
ISR에서 xQueueSend() (FromISR 아닌 버전)를 호출하면 FreeRTOS 내부 어설션이 실패하거나 HardFault가 발생합니다. ISR과 Task 어느 쪽에서 호출하는지에 따라 반드시 API를 구분해야 합니다.
ISR에서 Queue를 읽어야 하는 경우(드물지만 가능)도 FromISR 버전을 사용합니다.
/*
* UART RX 인터럽트에서 명령 Queue를 읽는 패턴은 권장하지 않습니다.
* 일반적으로 ISR은 데이터를 Queue에 넣고, Task가 꺼내는 구조가 맞습니다.
* 아래는 드문 상황(ISR에서 Queue 확인 후 즉각 응답)을 위한 참고 예시입니다.
*/
QueueHandle_t xCmdQueue;
void USART1_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
uint8_t cmd = (uint8_t)(USART1->DR & 0xFF);
uint8_t response;
/* xQueueReceiveFromISR: 큐에 응답이 있으면 즉시 꺼냄 */
if (xQueueReceiveFromISR(xCmdQueue, &response, &xHigherPriorityTaskWoken) == pdTRUE)
{
USART1->DR = response;
}
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
}
생산자 Task가 200ms마다 카운터를 Queue에 넣고, 소비자 Task가 꺼내 출력합니다.
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include <stdio.h>
#define QUEUE_LENGTH 4
#define ITEM_SIZE sizeof(uint32_t)
static QueueHandle_t xDataQueue;
void vProducerTask(void *pvParameters)
{
uint32_t count = 0;
while (1)
{
count++;
BaseType_t result = xQueueSend(xDataQueue, &count, pdMS_TO_TICKS(50));
if (result == pdTRUE)
{
printf("[P] sent: %lu (queue: %u/%u)\n",
count,
(unsigned)uxQueueMessagesWaiting(xDataQueue),
QUEUE_LENGTH);
}
else
{
printf("[P] queue full, dropped: %lu\n", count);
}
vTaskDelay(pdMS_TO_TICKS(200));
}
}
void vConsumerTask(void *pvParameters)
{
uint32_t received;
while (1)
{
if (xQueueReceive(xDataQueue, &received, portMAX_DELAY) == pdTRUE)
{
printf("[C] received: %lu\n", received);
vTaskDelay(pdMS_TO_TICKS(350)); /* 소비가 생산보다 느림 */
}
}
}
int main(void)
{
xDataQueue = xQueueCreate(QUEUE_LENGTH, ITEM_SIZE);
configASSERT(xDataQueue != NULL);
xTaskCreate(vProducerTask, "Prod", 256, NULL, 2, NULL);
xTaskCreate(vConsumerTask, "Cons", 256, NULL, 2, NULL);
vTaskStartScheduler();
while (1);
}
ADC 측정값과 타임스탬프를 묶어 전달합니다.
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include <stdio.h>
#include <stdint.h>
typedef struct
{
uint8_t channel;
uint16_t raw;
uint32_t timestamp_ms;
} AdcSample_t;
static QueueHandle_t xSampleQueue;
/* ADC 시뮬레이션 Task (실제: HAL_ADC_ConvCpltCallback에서 FromISR 사용) */
void vAdcSampleTask(void *pvParameters)
{
uint8_t ch = 0;
while (1)
{
AdcSample_t sample;
sample.channel = ch;
sample.raw = (uint16_t)(1000 + ch * 500); /* 시뮬레이션 값 */
sample.timestamp_ms = xTaskGetTickCount();
if (xQueueSend(xSampleQueue, &sample, pdMS_TO_TICKS(10)) != pdTRUE)
{
printf("[ADC] queue full, ch%u 드롭\n", ch);
}
ch = (ch + 1) % 4;
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void vProcessTask(void *pvParameters)
{
AdcSample_t s;
while (1)
{
xQueueReceive(xSampleQueue, &s, portMAX_DELAY);
float voltage = s.raw * 3.3f / 4095.0f;
printf("[Proc] ch%u raw=%u volt=%.3fV ts=%lums\n",
s.channel, s.raw, voltage, s.timestamp_ms);
}
}
int main(void)
{
xSampleQueue = xQueueCreate(8, sizeof(AdcSample_t));
configASSERT(xSampleQueue != NULL);
xTaskCreate(vAdcSampleTask, "AdcSample", 256, NULL, 3, NULL);
xTaskCreate(vProcessTask, "Process", 256, NULL, 2, NULL);
vTaskStartScheduler();
while (1);
}
타이머 인터럽트에서 데이터를 Queue에 넣고 Task가 처리하는 완전한 파이프라인을 구현합니다.
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "stm32f4xx_hal.h"
#include <stdio.h>
typedef struct
{
uint32_t tick;
uint16_t adc_raw;
} SensorData_t;
static QueueHandle_t xSensorQueue;
static volatile uint16_t g_last_adc = 0;
/* TIM2 100ms 주기 인터럽트 */
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if (htim->Instance != TIM2) return;
BaseType_t xWoken = pdFALSE;
SensorData_t data;
data.tick = xTaskGetTickCountFromISR();
data.adc_raw = g_last_adc; /* 직전 ADC 값 사용 */
/* 큐가 꽉 찼으면 드롭 (ISR에서 대기 불가) */
xQueueSendFromISR(xSensorQueue, &data, &xWoken);
portYIELD_FROM_ISR(xWoken);
}
/* 센서 데이터 처리 Task */
void vSensorTask(void *pvParameters)
{
SensorData_t data;
uint32_t processed = 0;
while (1)
{
xQueueReceive(xSensorQueue, &data, portMAX_DELAY);
processed++;
float voltage = data.adc_raw * 3.3f / 4095.0f;
printf("[Sensor #%lu] tick=%lu raw=%u volt=%.3fV\n",
processed, data.tick, data.adc_raw, voltage);
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_TIM2_Init();
xSensorQueue = xQueueCreate(16, sizeof(SensorData_t));
configASSERT(xSensorQueue != NULL);
xTaskCreate(vSensorTask, "Sensor", 256, NULL, 2, NULL);
HAL_TIM_Base_Start_IT(&htim2);
vTaskStartScheduler();
while (1);
}
[동기화 메커니즘 선택 흐름]
데이터를 함께 전달해야 하는가?
YES → Queue 사용
단일 아이템 + 포인터로 충분한가?
YES → Queue + 정적 버퍼 포인터
NO → Queue + 구조체 복사
NO → 이벤트 신호만 전달하는가?
YES → Binary Semaphore (ISR → Task)
또는 Counting Semaphore (이벤트 누적)
NO → 공유 자원 보호?
YES → Mutex
NO → EventGroup 고려
| 상황 | 권장 메커니즘 |
|---|---|
| ISR → Task, 이벤트 신호만 | Binary Semaphore |
| ISR 반복 발생, 이벤트 누적 | Counting Semaphore |
| ISR → Task, 데이터 포함 | Queue (xQueueSendFromISR) |
| Task → Task, 데이터 전달 | Queue |
| 공유 자원 상호 배제 | Mutex |
| 여러 이벤트 플래그 조합 | EventGroup |
Queue는 데이터를 값으로 복사합니다. 큰 구조체(수십 바이트 이상)를 자주 전달하면 복사 오버헤드가 쌓입니다. 이 경우 메모리 풀(Week 3 Day 5 참조)에서 블록을 할당하고, 블록 포인터를 Queue 아이템으로 사용하는 패턴이 효율적입니다.
Queue 기본 구조
configASSERT(xQueue != NULL)으로 생성 실패 즉시 감지핵심 API
xQueueSend(): 큐 끝에 추가, 꽉 찼으면 대기 또는 실패xQueueReceive(): 앞에서 꺼냄, 비었으면 대기 또는 실패xQueuePeek(): 꺼내지 않고 복사만 (큐 상태 유지)uxQueueMessagesWaiting(): 현재 저장된 아이템 수 조회ISR 전용 API
xQueueSendFromISR() 사용pxHigherPriorityTaskWoken + portYIELD_FROM_ISR() 패턴 필수errQUEUE_FULL 반환Semaphore와의 구분
| 함수 | 용도 | 컨텍스트 |
|---|---|---|
xQueueCreate(len, size) | Queue 생성 | Task/초기화 |
xQueueSend(q, &item, ticks) | 큐 끝에 추가 | Task |
xQueueSendToFront(q, &item, ticks) | 큐 앞에 추가 (우선 처리) | Task |
xQueueReceive(q, &buf, ticks) | 꺼내기 (삭제됨) | Task |
xQueuePeek(q, &buf, ticks) | 확인만 (삭제 안 됨) | Task |
xQueueSendFromISR(q, &item, &woken) | ISR에서 추가 | ISR |
xQueueReceiveFromISR(q, &buf, &woken) | ISR에서 꺼내기 | ISR |
uxQueueMessagesWaiting(q) | 저장 아이템 수 | Task |
uxQueueSpacesAvailable(q) | 여유 공간 수 | Task |
4개 ADC 채널에서 100ms마다 측정값을 수집하고, 채널 번호와 측정값을 구조체로 Queue에 넣어 처리 Task가 전압으로 변환 후 출력하는 시스템을 구현하십시오.
요구사항:
channel(uint8_t), raw(uint16_t), timestamp_ms(uint32_t)일반 명령(LED 제어)과 긴급 명령(비상 정지)을 같은 Queue로 처리하되, 긴급 명령은 xQueueSendToFront()를 사용하여 즉시 처리되도록 구현하십시오.
요구사항:
type(uint8_t), param(uint32_t)xQueueSend(), 긴급 명령: xQueueSendToFront()UART 수신 인터럽트에서 바이트를 Queue에 넣고, 처리 Task에서 바이트를 꺼내 줄 단위로 조합하여 명령을 파싱하는 구조를 구현하십시오.
요구사항:
sizeof(uint8_t), 큐 깊이: 128HAL_UART_RxCpltCallback에서 xQueueSendFromISR() 사용\n 수신 시까지 누적 후 명령 파싱/*
* 증상: configASSERT 실패 또는 NULL 포인터 접근으로 HardFault
* 원인 1: FreeRTOS 힙 메모리 부족
* → FreeRTOSConfig.h에서 configTOTAL_HEAP_SIZE 증가
*
* 진단: xPortGetFreeHeapSize()로 남은 힙 확인
*/
printf("남은 힙: %u bytes\n", (unsigned)xPortGetFreeHeapSize());
/* 큐 하나가 사용하는 메모리 근사치:
* 큐 구조체(~80 bytes) + 아이템 버퍼(uxLength × uxItemSize) */
/*
* 증상: 인터럽트 발생 직후 시스템 응답 없음 또는 HardFault
* 원인: ISR에서 일반 xQueueSend() 호출 (차단 동작 시도)
*/
/* 오류 */
void TIM2_IRQHandler(void)
{
xQueueSend(xQueue, &data, portMAX_DELAY); /* ISR에서 사용 금지 */
}
/* 올바름 */
void TIM2_IRQHandler(void)
{
BaseType_t xWoken = pdFALSE;
xQueueSendFromISR(xQueue, &data, &xWoken);
portYIELD_FROM_ISR(xWoken);
}
/*
* 증상: 수신 측에서 꺼낸 데이터가 쓰레기값
* 원인: 스택 변수의 주소를 Queue에 넣고 함수가 반환됨
*
* Queue는 pvItemToQueue가 가리키는 데이터를 즉시 내부 버퍼로 복사합니다.
* 구조체 값 자체를 넣으면 안전하지만, 구조체 포인터를 넣고
* 원본이 스택에 있으면 함수 반환 후 포인터가 무효화됩니다.
*/
/* 오류: 스택 변수 포인터를 Queue에 저장 */
void bad_pattern(void)
{
SensorMsg_t msg = {1, 2048, 1000};
SensorMsg_t *ptr = &msg;
xQueueSend(xPtrQueue, &ptr, 0); /* ptr이 가리키는 msg는 반환 후 소멸 */
}
/* 올바름: 구조체 값을 직접 복사 */
void good_pattern(void)
{
SensorMsg_t msg = {1, 2048, 1000};
xQueueSend(xMsgQueue, &msg, 0); /* FreeRTOS가 msg 내용을 즉시 복사 */
}