지노랩 /JinoLab
FreeRTOS 큐에 데이터 보내기: xQueueSendToFront() & xQueueSendToBack() 본문
임베디드 시스템/RTOS
FreeRTOS 큐에 데이터 보내기: xQueueSendToFront() & xQueueSendToBack()
지노랩/JinoLab 2025. 6. 30. 09:40FreeRTOS에서 **큐(Queue)**는 태스크 간 또는 ISR과 태스크 간 데이터를 주고받는 기본 메커니즘입니다.
이 글에서는 큐에 데이터를 넣는 두 가지 주요 API—xQueueSendToFront()와 xQueueSendToBack()—를 왜, 언제, 어떻게 사용하는지를 “블로그 포스트” 스타일로 자세히 안내합니다. 각 매개변수의 의미부터 동작 원리, 예제 코드, 주의사항까지 빠짐없이 다룹니다.
1. 큐에 데이터를 보내는 이유
- 태스크 간 통신: 센서값, 명령, 로그 등을 생산자(Producer) 태스크가 큐에 넣으면 소비자(Consumer) 태스크가 꺼내 처리
- 우선순위 조절:
- xQueueSendToBack(): 일반적인 “FIFO(First-In, First-Out)” 방식. 데이터가 큐의 끝(Tail) 에 추가되어, 들어온 순서대로 꺼내진다.
- xQueueSendToFront(): 긴급한 데이터나 우선순위가 높은 메시지를 큐의 앞(Head) 에 추가. 기존에 들어와 있던 데이터보다 먼저 처리하게 할 때 사용
- ISR ↔ 태스크 통신: 인터럽트 내부에서 발생한 이벤트나 수집한 외부 데이터를 큐에 넣어, 태스크가 나중에 받아 처리하도록 할 수도 있다(xQueueSendToBackFromISR() 등).
2. 「xQueueSendToBack()」: 일반적인 FIFO 전송
2.1. 함수 시그니처
BaseType_t xQueueSendToBack(
QueueHandle_t xQueue, // (1) 큐 핸들
const void *pvItemToQueue, // (2) 보내고 싶은 데이터 주소
TickType_t xTicksToWait // (3) 큐가 가득 차 있으면 대기할 최대 틱 수
);
- 반환값:
- pdPASS(== 1) → 보내기 성공
- errQUEUE_FULL(== 0) → 큐가 가득 차서 보내지 못함 (지정된 대기 시간 xTicksToWait 동안 큐가 비워지지 않으면 실패)
2.2. 매개변수 해설
- xQueue (QueueHandle_t)
- xQueueCreate()로 미리 생성한 큐 핸들
- 큐 내부 자료구조(Queue Control Block + Circular Buffer)를 가리키는 포인터
- *pvItemToQueue (const void )
- “보낼 아이템”의 메모리 주소(포인터)
- 큐는 **아이템 크기(ItemSize)**만큼 바이트를 내부 버퍼에 복사하므로,
- 예) sizeof(uint32_t)을 크기로 한 큐라면, 4바이트짜리 정수 값을 가리키는 포인터를 전달
- 예) 구조체 포인터 큐(sizeof(MyStruct*))일 경우, 포인터(4~8바이트)를 복사
- xTicksToWait (TickType_t)
- 큐가 **“가득 차 있는 상태”**라면, 최대 몇 틱(RTOS Tick)만큼 기다릴지 지정
- 값 해석:
- 0 → 절대로 기다리지 않고, 큐가 풀(Full)이면 즉시 실패
- pdMS_TO_TICKS(ms) → 밀리초를 틱으로 환산하여 지정 가능, 예: pdMS_TO_TICKS(100)→100ms 동안 대기
- portMAX_DELAY → 무한 대기(최대 틱 수만큼 막힘)
- 주의:
- 이 대기 시간 동안 태스크는 “Ready → Blocked” 상태로 들어가며, 다른 태스크가 실행될 수 있음
- 즉, xQueueSendToBack()가 반환될 때까지 CPU는 해당 태스크에게 할당되지 않음 → 블로킹(Blocking)
2.3. 작동 원리
- 호출 시점에 큐가 비어 있으면(empty) → Tail 위치에 바로 아이템 복사 후, Tail 인덱스를 한 칸(Ahead) 이동 → pdPASS 반환
- 큐가 일부만 찬 상태(Not Full) → 동일 처리
- 큐가 가득 찬 상태(Full)
- xTicksToWait == 0 → 즉시 errQUEUE_FULL 반환
- xTicksToWait > 0 →
- 해당 태스크를 Blocked List로 이동
- 다른 어떤 태스크가 xQueueReceive() 등으로 큐에서 데이터를 꺼내, 여유 공간이 생기면 →
- 지정한 태스크가 “다시 Ready”로 복귀 → 다시 CPU 스케줄링 대기
- 블록된 태스크가 재개(resume)되면 Tail 위치에 아이템 복사 → pdPASS 반환
- 만약 xTicksToWait만큼 대기했는데도 큐가 비워지지 않으면 → Block 해제 후 바로 errQUEUE_FULL 반환
힌트: 큐 내부 구조는 원형 버퍼(Circular Buffer) 형태로,
- headIndex: 가장 오래된 아이템(Next to Read)이 들어있는 위치
- tailIndex: 다음으로 “새로운 아이템”을 쓸 부분(Next to Write)
- uxMessagesWaiting: 현재 큐에 들어있는 아이템 개수
3. 「xQueueSendToFront()」: 앞쪽(Head)에 긴급 아이템 삽입
3.1. 함수 시그니처
BaseType_t xQueueSendToFront(
QueueHandle_t xQueue,
const void *pvItemToQueue,
TickType_t xTicksToWait
);
- 반환값/동작 원리는 xQueueSendToBack()와 동일
- 차이점: **“아이템 복사 위치”**가 Tail(뒤)→Head(앞) 로 바뀜
- 큐 내부 원형 버퍼에서 **Head(Queue Front)**로 데이터를 강제로 밀어 넣고, 기존 모든 아이템은 뒤로 밀림
- 즉, “가장 먼저 꺼낼 아이템” 우선순위 설정 효과
3.2. 왜 xQueueSendToFront()가 필요한가?
- 긴급 메시지 전송
- 예: 오류 발생 알림, 긴급 제어 명령, 우선순위 높은 센서 알람 등
- 이미 큐에 여타 “일반 메시지”가 대기 중이어도, 새로운 “긴급 메시지”가 앞에 들어가서 가장 먼저 꺼내짐
- 재시도 로직 (Retry Leader-Follower)
- 수신 실패 시 “재시도 요청”을 큐 앞에 붙여 바로 재전송 시도
- Ex) 통신 ACK(응답)이 오지 않을 때, “재전송 요청” 메시지를 앞에 삽입
- 정책 기반 큐 관리
- “상태 변화”나 “긴급 확인” 같은 이벤트를 언제든 큐 맨 앞에 넣어 시스템이 즉시 처리하도록
4. 기본 예제: xQueueSendToBack() + xQueueSendToFront()
다음 예제는 “정수형 큐”를 사용하여,
- 태스크 A → 1~100 정수 전송(뒤쪽으로)
- 태스크 B → 50~60 범위의 번호는 맨 앞으로 보내기(긴급 처리)
- 태스크 C → 큐에서 앞쪽(Head)부터 읽어 출력
4.1. 전처리 및 큐 생성
#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"
#include <stdio.h>
#define QUEUE_LENGTH 10 // 큐 최대 10개 아이템
#define ITEM_SIZE sizeof(uint32_t)
static QueueHandle_t xIntQueue;
/* 정수형 아이템을 뒤쪽(Tail)에 보내는 태스크 */
void ProducerTask(void *pvParameters)
{
for(uint32_t i = 1; i <= 100; i++) {
BaseType_t xStatus;
// 숫자 50~60은 앞쪽(Head)에 삽입하여 우선 처리
if( i >= 50 && i <= 60 ) {
xStatus = xQueueSendToFront( xIntQueue, &i, pdMS_TO_TICKS(10) );
if( xStatus != pdPASS ) {
// 큐 풀 → 10틱 기다려도 실패 시, 간단히 오류 로그
printf("SendToFront FAILED: %u\n", (unsigned)i);
}
}
else {
xStatus = xQueueSendToBack( xIntQueue, &i, pdMS_TO_TICKS(10) );
if( xStatus != pdPASS ) {
printf("SendToBack FAILED: %u\n", (unsigned)i);
}
}
vTaskDelay(pdMS_TO_TICKS(100)); // 100ms 간격으로 데이터 전송
}
// 끝나면 태스크 삭제(예시용)
vTaskDelete(NULL);
}
/* 큐에서 정수를 읽어들이는 태스크 (맨 앞부터 순서대로) */
void ConsumerTask(void *pvParameters)
{
uint32_t uxReceived;
for( ;; ) {
// 아이템이 들어올 때까지 무한 대기
if( xQueueReceive( xIntQueue, &uxReceived, portMAX_DELAY ) == pdPASS ) {
printf("Received: %u\n", (unsigned)uxReceived);
// 처리 지연을 주어(예: 200ms) 일부러 소비 속도 느림
vTaskDelay(pdMS_TO_TICKS(200));
}
}
}
int main(void)
{
/* 1) 큐 생성: 길이 10, 아이템 크기 4바이트 */
xIntQueue = xQueueCreate( QUEUE_LENGTH, ITEM_SIZE );
if( xIntQueue == NULL ) {
/* 메모리 부족으로 큐 생성 실패 → 무한 루프 */
for( ;; );
}
/* 2) 태스크들 생성 */
xTaskCreate( ProducerTask, "PROD", 256, NULL, 2, NULL );
xTaskCreate( ConsumerTask, "CONS", 256, NULL, 1, NULL );
/* 3) 스케줄러 시작 */
vTaskStartScheduler();
for( ;; );
}
코드 동작 요약
- ProducerTask(우선순위 2)
- 매 100ms마다 i 값을 증가시켜 보냄
- 만약 i가 50~60 사이면, xQueueSendToFront()로 큐의 맨 앞(Head) 에 넣어 즉시 처리되게 함
- 나머진 xQueueSendToBack()로 큐 맨 뒤(Tail) 에 넣어 FIFO 순서
- ConsumerTask(우선순위 1)
- xQueueReceive()로 큐 맨 앞부터 꺼내 출력
- 꺼낸 후 200ms 지연 → 일부러 처리 속도를 느리게 하여 큐가 금방 차도록 유도
- 큐 동작
- 생산 속도 100ms < 소비 속도 200ms → 큐가 빠르게 가득 참
- 50~60 구간은 긴급 → 앞쪽으로 밀어 넣어 곧바로 처리
- 가득 찼을 때는 최대 10ms 기다렸다가 풀 공간이 생기지 않으면 전송 실패
5. 주요 파라미터 심화 해설
5.1. xTicksToWait의 의미
- 즉시 실패: xTicksToWait == 0
→ “큐 풀(Full)” 시 즉시 errQUEUE_FULL 반환 - 유한 대기: xTicksToWait == pdMS_TO_TICKS(100)
→ 큐 풀 상태라면, 최대 100ms(해당 틱 수) 동안 큐가 비워질 때까지 블로킹
→ 100ms 이내 “내려감(큐에 자리 생김)” → 보내기 성공
→ 100ms 지나도 풀 상태 유지 → errQUEUE_FULL 반환 - 무한 대기: xTicksToWait == portMAX_DELAY
→ “무조건” 큐에 들어갈 때까지 블로킹
→ 주의: Idle Hook이 configUSE_TASK_NOTIFICATIONS_ON_IDLE 등으로 얽혀있거나,
INCLUDE_vTaskSuspend 옵션에 따라 잠재적으로 데드락(Deadlock) 가능 → 설계 시 유의
Tip:
- 생산자 태스크는 “실패 시 간단 무시” 혹은 “짧게 재시도” 패턴에 따라 적절한 대기 시간을 두세요.
- 임베디드 환경에서는 대기 시간을 짧게 잡는 편이 시스템 응답성을 위해 좋습니다.
6. ISRs(인터럽트)에서 큐에 넣을 때: “FromISR” 버전 사용
태스크 컨텍스트가 아닌 **ISR(Interrupt Service Routine)**는
- 절대 일반 xQueueSendToBack() / xQueueSendToFront()를 사용하면 안 됩니다.
- 반드시 ***FromISR**로 끝나는 인터럽트 안전(Interrupt-Safe) 버전 API 사용
6.1. xQueueSendToBackFromISR()
BaseType_t xQueueSendToBackFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
- xQueue : 큐 핸들
- pvItemToQueue : 보내려는 아이템의 메모리 주소
- pxHigherPriorityTaskWoken :
- ISR 내부에서 “큐에 넣자마자 우선순위가 높은 태스크가 깨워져야 하는지?” 반환받는 포인터
- pdTRUE가 되면, ISR 끝날 무렵 portYIELD_FROM_ISR() 호출하여 그 태스크로 즉시 컨텍스트 전환
6.2. xQueueSendToFrontFromISR() 역시 동작 유사
BaseType_t xQueueSendToFrontFromISR(
QueueHandle_t xQueue,
const void *pvItemToQueue,
BaseType_t *pxHigherPriorityTaskWoken
);
- “맨 앞(Head)”에 삽입. 뒤섞인 태스크 우선순위가 변동될 수 있으므로,
pxHigherPriorityTaskWoken를 통해 “더 높은 우선순위 Task가 대기 중인지” 확인 후,
ISR 종료 직전에 portYIELD_FROM_ISR() 호출해주세요.
주의:
- ISR에서 큐가 가득 찼을 때 “block until space available” 개념이 없으므로,
xQueueSendToBackFromISR()는 “즉시 실패”만 가능 (xTicksToWait 파라미터 없음).- “보냈다” 즉시 데이터 전송의 성공/실패를 리턴 값(pdPASS/errQUEUE_FULL)으로 확인.
7. “FromISR” 사용 예시 (ISR + 태스크 통신)
7.1. 상황: “UART RX 인터럽트”에서 FIFO 큐에 수신 바이트를 넣기
#include "FreeRTOS.h"
#include "queue.h"
#include "stm32f4xx.h" // (예시 MCU 헤더)
static QueueHandle_t xUartRxQueue;
/* UART 설정 코드 생략... */
/* UART 수신 인터럽트 핸들러 (예시) */
void USART1_IRQHandler(void)
{
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
uint8_t rxByte;
if( USART1->SR & USART_SR_RXNE ) { // 수신 데이터 레지스터 비었는지?
rxByte = (uint8_t)USART1->DR; // 한 바이트 꺼내옴
// 1) 큐에 넣기 (Tail): 최대 내부적으로 재배치 발생 시 태스크 깨울 수 있도록 파라미터 사용
if( xQueueSendToBackFromISR( xUartRxQueue, &rxByte, &xHigherPriorityTaskWoken ) != pdPASS ) {
// 큐 풀: 데이터 손실 시 로깅 또는 카운터 증가
}
// 2) “우선순위 높은 태스크”가 대기 중이면, 컨텍스트 스위치
portYIELD_FROM_ISR( xHigherPriorityTaskWoken );
}
}
/* 수신 처리 태스크 (우선순위 2) */
void UartConsumerTask(void *pvParameters)
{
uint8_t data;
for( ;; ) {
// 무한 대기하며 UART RX 큐에서 꺼내기
if( xQueueReceive( xUartRxQueue, &data, portMAX_DELAY ) == pdPASS ) {
// data 처리 로직 (예: 버퍼에 저장, 프로토콜 파서 호출 등)
}
}
}
int main(void)
{
// 1) 큐 생성: 길이 64, 크기 1바이트
xUartRxQueue = xQueueCreate( 64, sizeof(uint8_t) );
if( xUartRxQueue == NULL ) {
// 메모리 부족 시 에러 루프
for( ;; );
}
// 2) UART 초기화 (펄스 클럭, GPIO, NVIC 설정 등) → USART1_IRQHandler에 우선순위 지정
// (우선순위 값은 반드시 configMAX_SYSCALL_INTERRUPT_PRIORITY 이하로 설정)
// 3) 소비자 태스크 생성
xTaskCreate( UartConsumerTask, "UART_RX_CONS", 128, NULL, 2, NULL );
// 4) 스케줄러 시작
vTaskStartScheduler();
for( ;; );
}
주요 포인트
- ISR 내부
- xQueueSendToBackFromISR() 호출 시 pxHigherPriorityTaskWoken 파라미터 사용
- 데이터 전송 성공/실패(pdPASS vs errQUEUE_FULL) 확인
- 만약 “우선순위 더 높은 태스크가 Unblock 되어야” 하는 상황이라면,
portYIELD_FROM_ISR(xHigherPriorityTaskWoken)를 호출해야 즉시 컨텍스트 스위치
- NVIC 우선순위
- 반드시 USART1_IRQn 우선순위를 configMAX_SYSCALL_INTERRUPT_PRIORITY 이하로 설정
- 예) NVIC_SetPriority(USART1_IRQn, (configMAX_SYSCALL_INTERRUPT_PRIORITY >> (8 - __NVIC_PRIO_BITS)));
- 만약 더 높은(숫자 작은) 우선순위라면 ISR용 큐 API를 써도 제대로 작동하지 않을 수 있음
- 소비자 태스크
- xQueueReceive(…, portMAX_DELAY) → 큐에 데이터 없으면 무한 대기(blocked)
- 다른 인터럽트가 데이터 넣으면 Ready → Since priority=2 → 즉시 실행
8. “큐 삽입 실패” 처리 전략
- 큐 버퍼(full) 상황
- 데이터 손실 가능성
- xQueueSend…()가 즉시(xTicksToWait=0) 실패 → 그냥 무시하거나 카운터를 올려서 “손실 로깅”
- 잠시 대기 후 재시도
- xTicksToWait = pdMS_TO_TICKS(50)와 같이 짧게 대기했다가, 공간이 생기면 “성공” → 안정성↑
- 무한 대기(portMAX_DELAY)
- 어쩔 수 없이 “무조건 데이터 전송”이 필요할 때. 태스크가 큐에 자리 생길 때까지 영원히 Block → Deadlock 주의!
- 데이터 손실 가능성
- “FromISR”의 경우
- ISR은 블로킹(Waiting)이 불가능 → 항상 즉시 실패(errQUEUE_FULL) 리턴
- ISR 내부에서 “① 다른 태스크에 알림(예: xHigherPriorityTaskWoken), ② 간단 로깅/카운터 누적 → 반환” 패턴
9. 실전 팁 & 주의사항
- 아이템 크기(Item Size) 주의
- 32비트 MCU 기준, sizeof(void*) == 4바이트
- “구조체를 통째로” 큐에 저장하려면, sizeof(MyStruct) × 길이만큼 큐 버퍼가 필요 → 메모리 불안정
- 일반적으로는 “포인터 큐”(sizeof(Type*))→ 4~8바이트만 버퍼링 후, 메모리는 pvPortMalloc() 등으로 따로 할당
- 우선순위 설정
- 생산자(Producer) 태스크 우선순위 > 소비자(Consumer) 태스크 우선순위 → FIFO 큐가 쌓여도 안정적으로 “집중 처리”
- ISR에서 보낼 때 → xQueueSendToBackFromISR() 호출 직후, portYIELD_FROM_ISR()로 소비자 태스크 우선순위에 따라 즉시 전환
- xTicksToWait를 0으로 두면 “절대 안 블록”
- ISR이 아닌, 태스크 컨텍스트에서 일반 xQueueSendToBack()를 호출할 때,
- “큐가 가득 차 있으면 바로 실패” 로 하려면 xTicksToWait=0을 지정
- 너무 큰 xTicksToWait → 시스템 응답성 저하 또는 데드락 위험 → 짧은 시간(예: 10~50ticks) 권장
- 인터럽트 우선순위 configMAX_SYSCALL_INTERRUPT_PRIORITY 확인
- ISR 내부에서 FreeRTOS 큐 API(*FromISR) 사용하려면, 반드시 해당 IRQ 우선순위 설정이
configMAX_SYSCALL_INTERRUPT_PRIORITY (예: 0x50) 보다 “큰 수치(낮은 우선순위)” 여야 함 - 우선순위가 더 높으면(숫자 작음) → FreeRTOS 커널 API가 제대로 동작하지 않거나 동기화 실패
- ISR 내부에서 FreeRTOS 큐 API(*FromISR) 사용하려면, 반드시 해당 IRQ 우선순위 설정이
- 큐 삭제(Delete) 시 주의
- vQueueDelete(xQueue)로 큐를 삭제하면, 내부 버퍼 메모리가 해제됨 → NULL 처리 / 재사용 시 생성 필요
- 큐가 삭제된 뒤에 기존 핸들로 xQueueSend…() 등을 호출하면 Undefined Behavior 발생
10. 결론
- xQueueSendToBack() (기본 FIFO 전송)
- 데이터를 큐 뒤(Tail) 로 넣음 → 순차 처리
- “큐가 가득 차면, 지정된 틱만큼 블록 + 재시도” or “즉시 실패”
- xQueueSendToFront() (긴급 전송)
- 데이터를 큐 앞(Head) 로 밀어 넣음 → 모든 기존 아이템은 뒤로 한 칸씩 밀림
- 끝까지 긴급 처리가 필요할 때 사용
- ISR 환경
- xQueueSendToBackFromISR() / xQueueSendToFrontFromISR()
- ISR은 블로킹 불가능 → 즉시 “성공/실패” 반환
- pxHigherPriorityTaskWoken 파라미터 사용 → 데이터 전송과 동시에 즉시 태스크 깨우기 가능
- 실제 시스템 설계 팁
- 작은 아이템 크기: 구조체 대신 “포인터 큐” → 메모리 효율 극대화
- 짧은 블록 타임: pdMS_TO_TICKS(10~50) 정도로, 시스템 응답성 방해 최소화
- 인터럽트 우선순위: 반드시 configMAX_SYSCALL_INTERRUPT_PRIORITY 이하로 설정
이제 FreeRTOS 큐 전송 API의 기본 개념과 사용법을 완전히 이해하셨습니다.
다음 단계로는, **“큐에서 데이터 읽기(xQueueReceive)”**와 **“큐 삭제 및 정적 큐”**에 대해 깊게 공부해 보세요.
참고: FreeRTOS 공식 문서
'임베디드 시스템 > RTOS' 카테고리의 다른 글
FreeRTOS_Queues_n_Timers: UART 명령어로 LED와 RTC 제어하기 (0) | 2025.07.01 |
---|---|
FreeRTOS 큐(Queue)에서 데이터 읽기: xQueueReceive() & xQueuePeek() (1) | 2025.06.30 |
FreeRTOS 큐(Queue) 생성하기: xQueueCreate API (1) | 2025.06.29 |
FreeRTOS 큐(Queue): 개념부터 코드 예제까지 (0) | 2025.06.29 |
FreeRTOS Idle Hook로 ‘공짜’ 저전력 얻기 (0) | 2025.06.28 |