
Queue는 어떤 타입의 데이터든
uxItemSize바이트만큼 복사하여 저장합니다. 구조체도 예외가 없습니다. 이 복사 특성이 안전성의 핵심이며, 동시에 크기 설계가 중요해지는 이유입니다.
xQueueCreate(uxQueueLength, uxItemSize)에서 uxItemSize는 전달할 데이터 타입의 sizeof 값입니다. Queue는 생성 시 uxQueueLength * uxItemSize 바이트의 내부 버퍼를 Heap에 할당합니다. xQueueSend()를 호출하면 전달한 주소에서 uxItemSize 바이트를 그대로 내부 버퍼에 memcpy합니다.
/*
* Queue 내부 동작 이해
*
* xQueueCreate(10, sizeof(SensorData_t)):
* Heap에 10 * sizeof(SensorData_t) 바이트 할당
* → Queue 깊이 10, 아이템 하나가 sizeof(SensorData_t) 바이트
*
* xQueueSend(xQueue, &data, timeout):
* 내부적으로 memcpy(내부버퍼[tail], &data, uxItemSize) 수행
* → data 변수의 현재 값이 Queue에 스냅샷으로 저장됨
* → 이후 data 변수가 변경되어도 Queue 내부에는 영향 없음
*
* xQueueReceive(xQueue, &out, timeout):
* 내부적으로 memcpy(&out, 내부버퍼[head], uxItemSize) 수행
*/
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include <stdio.h>
#include <stdint.h>
typedef struct
{
uint8_t sensor_id;
uint16_t raw_value;
float temperature;
uint32_t timestamp_ms;
} SensorData_t; /* sizeof = 1 + 1(패딩) + 2 + 4 + 4 = 12바이트 (정렬에 따라 다를 수 있음) */
static QueueHandle_t xSensorQueue;
/* 생산자 Task */
void vSensorTask(void *pvParameters)
{
SensorData_t data;
uint16_t raw = 0;
while (1)
{
data.sensor_id = 1U;
data.raw_value = raw;
data.temperature = raw * 3.3f / 4095.0f * 100.0f; /* 시뮬레이션 */
data.timestamp_ms = xTaskGetTickCount();
/*
* xQueueSend는 data의 현재 값 전체를 Queue에 복사합니다.
* 이 줄 이후 data를 덮어써도 Queue 내부 값은 보존됩니다.
*/
if (xQueueSend(xSensorQueue, &data, pdMS_TO_TICKS(10)) != pdTRUE)
{
printf("[Sensor] Queue full, data dropped\r\n");
}
raw = (raw + 100U) % 4096U;
vTaskDelay(pdMS_TO_TICKS(100));
}
}
/* 소비자 Task */
void vLoggerTask(void *pvParameters)
{
SensorData_t received;
while (1)
{
if (xQueueReceive(xSensorQueue, &received, portMAX_DELAY) == pdTRUE)
{
printf("[Logger] id=%u raw=%u temp=%.1f°C @%lums\r\n",
received.sensor_id,
received.raw_value,
received.temperature,
received.timestamp_ms);
}
}
}
int main(void)
{
/*
* Queue 깊이 20, 아이템 크기 = sizeof(SensorData_t)
* Heap 사용량 = 20 * sizeof(SensorData_t) + Queue 제어 블록(~80바이트)
*/
xSensorQueue = xQueueCreate(20, sizeof(SensorData_t));
configASSERT(xSensorQueue != NULL);
xTaskCreate(vSensorTask, "Sensor", 256, NULL, 3, NULL);
xTaskCreate(vLoggerTask, "Logger", 512, NULL, 2, NULL);
vTaskStartScheduler();
while (1);
}
Queue에 데이터 자체 대신 포인터를 전달하는 방식은 Heap 사용량을 줄이는 것처럼 보이지만 심각한 문제를 일으킬 수 있습니다.
/*
* 포인터 전송 — 위험한 패턴 예시
*
* 스택 변수의 주소를 Queue에 넣으면 함수가 반환된 후 해당 주소는 무효가 됩니다.
* 이 패턴은 댕글링 포인터(Dangling Pointer)로 이어집니다.
*/
#include "FreeRTOS.h"
#include "queue.h"
#include <stdio.h>
static QueueHandle_t xPtrQueue; /* SensorData_t* 포인터를 담는 Queue */
/* 잘못된 패턴: 스택 변수 주소 전송 */
void vBadSensorTask(void *pvParameters)
{
while (1)
{
SensorData_t local_data; /* 스택에 할당 */
local_data.raw_value = 1234U;
SensorData_t *ptr = &local_data;
/*
* Queue에 포인터를 넣습니다.
* 이 함수(Task)는 무한 루프이므로 지금은 우연히 동작할 수 있습니다.
* 하지만 일반 함수에서 이 패턴을 사용하면 함수 반환 후 스택이 재사용되어
* 수신 측에서 포인터를 역참조하는 순간 쓰레기 값을 읽거나 크래시가 발생합니다.
*/
xQueueSend(xPtrQueue, &ptr, 0);
vTaskDelay(pdMS_TO_TICKS(100));
}
}
/*
* 안전한 포인터 전송 패턴:
* 동적으로 할당된 메모리의 주소를 전달하고,
* 수신 측에서 사용 후 반드시 해제합니다.
*
* 주의: FreeRTOS Heap 단편화 위험으로 인해 빈번한 동적 할당/해제는 권장하지 않습니다.
* 가능하면 데이터 복사 방식을 사용하고, 포인터 방식은 대용량 데이터(>64바이트)에만 고려합니다.
*/
void vGoodPointerTask(void *pvParameters)
{
while (1)
{
SensorData_t *pData = pvPortMalloc(sizeof(SensorData_t));
if (pData != NULL)
{
pData->raw_value = 5678U;
pData->sensor_id = 2U;
if (xQueueSend(xPtrQueue, &pData, pdMS_TO_TICKS(10)) != pdTRUE)
{
/* 전송 실패: 직접 해제해야 합니다 (수신 측에서 해제 불가) */
vPortFree(pData);
}
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
void vPointerReceiverTask(void *pvParameters)
{
SensorData_t *pData;
while (1)
{
if (xQueueReceive(xPtrQueue, &pData, portMAX_DELAY) == pdTRUE)
{
printf("[Receiver] raw=%u\r\n", pData->raw_value);
/* 수신 측에서 반드시 해제 */
vPortFree(pData);
}
}
}
| 기준 | 데이터 복사 방식 | 포인터 전송 방식 |
|---|---|---|
| 안전성 | 높음: Queue 내부에 독립적 복사본 | 낮음: 원본 수명 관리 필요 |
| Heap 사용 | Queue 깊이 × sizeof(구조체) 고정 | 구조체 크기 무관, 포인터 크기(4바이트)만 |
| 권장 구조체 크기 | 64바이트 이하 | 64바이트 초과 |
| 해제 책임 | 없음 (자동) | 수신 측 또는 실패 시 송신 측 |
| ISR 사용 | xQueueSendFromISR 가능 | 동적 할당은 ISR 내 불가 |
컴파일러는 구조체 멤버를 자연 정렬(natural alignment)에 맞게 배치합니다. 멤버 순서에 따라 패딩이 삽입되어 실제 크기가 예상보다 클 수 있습니다. Queue 깊이가 깊을수록 이 차이는 메모리에 직접 영향을 줍니다.
/*
* 구조체 크기 비교 예제
*
* [비효율적 순서]
* struct Inefficient {
* uint8_t a; // offset 0, size 1
* // 패딩 3바이트 (uint32_t 정렬)
* uint32_t b; // offset 4, size 4
* uint8_t c; // offset 8, size 1
* // 패딩 1바이트 (uint16_t 정렬)
* uint16_t d; // offset 10, size 2
* // 패딩 0바이트 (구조체 전체는 uint32_t 정렬 → 크기 12)
* };
* sizeof = 12
*
* [효율적 순서: 큰 타입 먼저]
* struct Efficient {
* uint32_t b; // offset 0, size 4
* uint16_t d; // offset 4, size 2
* uint8_t a; // offset 6, size 1
* uint8_t c; // offset 7, size 1
* };
* sizeof = 8
*
* Queue 깊이 20이라면:
* 비효율: 20 × 12 = 240바이트
* 효율적: 20 × 8 = 160바이트 (80바이트 절약)
*/
#include "FreeRTOS.h"
#include "queue.h"
#include <stdio.h>
#include <stdint.h>
/* 비효율적 레이아웃 */
typedef struct
{
uint8_t sensor_id; /* 1바이트 + 패딩 3바이트 */
uint32_t timestamp_ms; /* 4바이트 */
uint8_t status; /* 1바이트 + 패딩 1바이트 */
uint16_t raw_value; /* 2바이트 */
/* 합계: 12바이트 (정렬 패딩 포함) */
} SensorPacketBad_t;
/* 효율적 레이아웃: 큰 타입 → 작은 타입 순서 */
typedef struct
{
uint32_t timestamp_ms; /* 4바이트 */
uint16_t raw_value; /* 2바이트 */
uint8_t sensor_id; /* 1바이트 */
uint8_t status; /* 1바이트 */
/* 합계: 8바이트 (패딩 없음) */
} SensorPacketGood_t;
void print_sizes(void)
{
printf("SensorPacketBad_t size: %u bytes\r\n", (unsigned)sizeof(SensorPacketBad_t));
printf("SensorPacketGood_t size: %u bytes\r\n", (unsigned)sizeof(SensorPacketGood_t));
/* Queue 깊이 20 기준 */
printf("Queue(20, Bad): %u bytes\r\n", (unsigned)(20 * sizeof(SensorPacketBad_t)));
printf("Queue(20, Good): %u bytes\r\n", (unsigned)(20 * sizeof(SensorPacketGood_t)));
}
여러 종류의 메시지를 하나의 Queue로 처리해야 할 때, 공용체를 사용하면 Queue 아이템 크기를 단일 타입으로 통일할 수 있습니다.
/*
* 다형 메시지 패턴:
* 메시지 종류에 따라 다른 데이터를 담지만
* Queue 아이템 크기는 항상 sizeof(Message_t)로 고정합니다.
*/
#include <stdint.h>
#include <string.h>
typedef enum
{
MSG_SENSOR_DATA = 0U,
MSG_ERROR = 1U,
MSG_COMMAND = 2U,
} MsgType_t;
typedef struct
{
uint8_t sensor_id;
uint16_t raw;
float value;
} SensorPayload_t;
typedef struct
{
uint32_t code;
uint8_t source;
} ErrorPayload_t;
typedef struct
{
uint8_t cmd;
uint8_t param[4];
} CmdPayload_t;
/*
* 공용체 메시지: 가장 큰 페이로드 크기가 전체 크기를 결정합니다.
* sizeof(Message_t) = sizeof(MsgType_t) + max(sizeof(SensorPayload_t), ...)
*/
typedef struct
{
MsgType_t type; /* 메시지 종류 */
union
{
SensorPayload_t sensor;
ErrorPayload_t error;
CmdPayload_t cmd;
} payload;
} Message_t;
static QueueHandle_t xMsgQueue;
/* 송신 측: 종류별 헬퍼 함수 */
void send_sensor_data(uint8_t id, uint16_t raw, float val)
{
Message_t msg;
msg.type = MSG_SENSOR_DATA;
msg.payload.sensor.sensor_id = id;
msg.payload.sensor.raw = raw;
msg.payload.sensor.value = val;
xQueueSend(xMsgQueue, &msg, pdMS_TO_TICKS(10));
}
void send_error(uint32_t code, uint8_t source)
{
Message_t msg;
msg.type = MSG_ERROR;
msg.payload.error.code = code;
msg.payload.error.source = source;
xQueueSend(xMsgQueue, &msg, pdMS_TO_TICKS(10));
}
/* 수신 측: type 필드로 분기 */
void vDispatchTask(void *pvParameters)
{
Message_t msg;
while (1)
{
if (xQueueReceive(xMsgQueue, &msg, portMAX_DELAY) == pdTRUE)
{
switch (msg.type)
{
case MSG_SENSOR_DATA:
printf("[Sensor] id=%u val=%.2f\r\n",
msg.payload.sensor.sensor_id,
msg.payload.sensor.value);
break;
case MSG_ERROR:
printf("[Error] code=0x%08lX src=%u\r\n",
msg.payload.error.code,
msg.payload.error.source);
break;
case MSG_COMMAND:
printf("[Cmd] cmd=0x%02X\r\n", msg.payload.cmd.cmd);
break;
default:
break;
}
}
}
}
TIM2 100ms 주기로 여러 센서(온도, 습도, 조도)를 읽고 구조체로 묶어 Queue에 전달하면 로거 Task가 수신하여 UART로 출력하는 완전한 예제입니다.
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include "stm32f4xx_hal.h"
#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
/* ---- 구조체 정의 (크기 최적화: 큰 타입 먼저) ---- */
typedef struct
{
uint32_t timestamp_ms; /* 4바이트 */
float temperature; /* 4바이트: 섭씨 */
float humidity; /* 4바이트: % */
uint16_t lux; /* 2바이트: 조도 */
uint8_t sequence; /* 1바이트: 패킷 순서 번호 */
uint8_t flags; /* 1바이트: 비트 플래그 */
/* 합계: 16바이트, 패딩 없음 */
} EnvData_t;
#define FLAG_TEMP_VALID (1U << 0)
#define FLAG_HUM_VALID (1U << 1)
#define FLAG_LUX_VALID (1U << 2)
#define ENV_QUEUE_DEPTH 16U
static QueueHandle_t xEnvQueue;
/* ---- 시뮬레이션 센서 읽기 함수 ---- */
static float read_temperature(void)
{
/* NTC 온도 센서: ADC 값으로부터 온도 계산 시뮬레이션 */
uint16_t raw = (uint16_t)(ADC1->DR & 0x0FFFU);
return 25.0f + (raw - 2048) * 0.01f;
}
static float read_humidity(void)
{
/* SHT31 I2C 센서 읽기 시뮬레이션 */
return 60.0f + (float)(HAL_GetTick() % 20);
}
static uint16_t read_lux(void)
{
/* BH1750 I2C 조도 센서 읽기 시뮬레이션 */
return (uint16_t)(100U + (HAL_GetTick() / 100U) % 900U);
}
/* ---- 수집 Task ---- */
void vCollectTask(void *pvParameters)
{
EnvData_t data;
uint8_t seq = 0;
while (1)
{
data.timestamp_ms = xTaskGetTickCount();
data.sequence = seq++;
data.flags = 0;
/* 각 센서 읽기 및 유효 플래그 설정 */
data.temperature = read_temperature();
data.flags |= FLAG_TEMP_VALID;
data.humidity = read_humidity();
data.flags |= FLAG_HUM_VALID;
data.lux = read_lux();
data.flags |= FLAG_LUX_VALID;
/* 구조체 전체를 Queue에 복사 */
if (xQueueSend(xEnvQueue, &data, pdMS_TO_TICKS(20)) != pdTRUE)
{
/* Queue 포화: 가장 오래된 데이터를 버리고 최신 데이터 삽입 */
EnvData_t dummy;
xQueueReceive(xEnvQueue, &dummy, 0);
xQueueSend(xEnvQueue, &data, 0);
}
vTaskDelay(pdMS_TO_TICKS(100));
}
}
/* ---- 출력 Task ---- */
void vPrintTask(void *pvParameters)
{
EnvData_t data;
printf("=== 환경 데이터 모니터 시작 ===\r\n");
printf("sizeof(EnvData_t) = %u bytes\r\n", (unsigned)sizeof(EnvData_t));
printf("Queue 메모리 = %u bytes\r\n",
(unsigned)(ENV_QUEUE_DEPTH * sizeof(EnvData_t)));
while (1)
{
if (xQueueReceive(xEnvQueue, &data, portMAX_DELAY) == pdTRUE)
{
printf("[%5lums #%3u]", data.timestamp_ms, data.sequence);
if (data.flags & FLAG_TEMP_VALID)
printf(" T=%.1f°C", data.temperature);
if (data.flags & FLAG_HUM_VALID)
printf(" H=%.0f%%", data.humidity);
if (data.flags & FLAG_LUX_VALID)
printf(" L=%ulux", data.lux);
printf("\r\n");
}
}
}
/* ---- 상태 모니터 Task ---- */
void vMonitorTask(void *pvParameters)
{
while (1)
{
vTaskDelay(pdMS_TO_TICKS(5000));
printf("[Monitor] Queue 대기: %u / %u\r\n",
(unsigned)uxQueueMessagesWaiting(xEnvQueue),
(unsigned)ENV_QUEUE_DEPTH);
}
}
int main(void)
{
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
MX_ADC1_Init();
MX_USART2_UART_Init();
xEnvQueue = xQueueCreate(ENV_QUEUE_DEPTH, sizeof(EnvData_t));
configASSERT(xEnvQueue != NULL);
xTaskCreate(vCollectTask, "Collect", 256, NULL, 3, NULL);
xTaskCreate(vPrintTask, "Print", 512, NULL, 2, NULL);
xTaskCreate(vMonitorTask, "Monitor", 256, NULL, 1, NULL);
vTaskStartScheduler();
while (1);
}
| 항목 | 내용 |
|---|---|
| Queue 복사 동작 | xQueueSend 시 uxItemSize 바이트를 memcpy로 내부 버퍼에 복사 |
| 구조체 크기와 메모리 | Queue 깊이 × sizeof(구조체) = Heap 사용량, 크기 최적화 중요 |
| 데이터 복사 vs 포인터 | 64바이트 이하는 복사, 초과는 동적 할당 후 포인터 (수명 관리 필수) |
| 구조체 정렬 최적화 | 큰 타입을 먼저 선언하면 패딩 최소화 |
| 공용체 메시지 | 다양한 메시지 타입을 하나의 Queue로 처리, type 필드로 분기 |
| Queue 포화 대응 | 오래된 데이터 드롭 후 최신 데이터 삽입 또는 드롭 카운터 유지 |
SensorPacketBad_t와 SensorPacketGood_t의 sizeof를 실제로 출력하고, Queue 깊이 50에서 두 구조체의 Heap 사용량 차이를 계산하여 출력하는 코드를 작성하십시오.EnvData_t 구조체에 GPS 좌표 필드(float lat, lon)를 추가했을 때 Queue 메모리가 얼마나 늘어나는지 계산하고, 포인터 방식으로 전환해야 하는 임계 크기를 판단하십시오.vCollectTask에 추가하십시오.xQueueSend가 pdFALSE를 반환하면 즉시 uxQueueSpacesAvailable()로 여유 공간을 확인한다. 0이면 Queue 깊이를 늘리거나 소비자 Task 우선순위를 올려야 한다.sizeof를 xQueueCreate에 전달한 크기와 비교한다. 타입이 바뀌었는데 Queue를 재생성하지 않았을 수 있다.type 필드가 잘못된 값을 가리키면 default 케이스에 configASSERT(0)을 추가하여 즉시 오류를 발견할 수 있다.