지노랩 /JinoLab

FreeRTOS 큐에 데이터 보내기: xQueueSendToFront() & xQueueSendToBack() 본문

임베디드 시스템/RTOS

FreeRTOS 큐에 데이터 보내기: xQueueSendToFront() & xQueueSendToBack()

지노랩/JinoLab 2025. 6. 30. 09:40

FreeRTOS에서 **큐(Queue)**는 태스크 간 또는 ISR과 태스크 간 데이터를 주고받는 기본 메커니즘입니다.
이 글에서는 큐에 데이터를 넣는 두 가지 주요 API—xQueueSendToFront()와 xQueueSendToBack()—를 왜, 언제, 어떻게 사용하는지를 “블로그 포스트” 스타일로 자세히 안내합니다. 각 매개변수의 의미부터 동작 원리, 예제 코드, 주의사항까지 빠짐없이 다룹니다.


1. 큐에 데이터를 보내는 이유

  1. 태스크 간 통신: 센서값, 명령, 로그 등을 생산자(Producer) 태스크가 큐에 넣으면 소비자(Consumer) 태스크가 꺼내 처리
  2. 우선순위 조절:
    • xQueueSendToBack(): 일반적인 “FIFO(First-In, First-Out)” 방식. 데이터가 큐의 끝(Tail) 에 추가되어, 들어온 순서대로 꺼내진다.
    • xQueueSendToFront(): 긴급한 데이터나 우선순위가 높은 메시지를 큐의 앞(Head) 에 추가. 기존에 들어와 있던 데이터보다 먼저 처리하게 할 때 사용
  3. 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. 매개변수 해설

  1. xQueue (QueueHandle_t)
    • xQueueCreate()로 미리 생성한 큐 핸들
    • 큐 내부 자료구조(Queue Control Block + Circular Buffer)를 가리키는 포인터
  2. *pvItemToQueue (const void )
    • “보낼 아이템”의 메모리 주소(포인터)
    • 큐는 **아이템 크기(ItemSize)**만큼 바이트를 내부 버퍼에 복사하므로,
      • 예) sizeof(uint32_t)을 크기로 한 큐라면, 4바이트짜리 정수 값을 가리키는 포인터를 전달
      • 예) 구조체 포인터 큐(sizeof(MyStruct*))일 경우, 포인터(4~8바이트)를 복사
  3. 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. 작동 원리

  1. 호출 시점에 큐가 비어 있으면(empty) → Tail 위치에 바로 아이템 복사 후, Tail 인덱스를 한 칸(Ahead) 이동 → pdPASS 반환
  2. 큐가 일부만 찬 상태(Not Full) → 동일 처리
  3. 큐가 가득 찬 상태(Full)
    • xTicksToWait == 0 → 즉시 errQUEUE_FULL 반환
    • xTicksToWait > 0 →
      1. 해당 태스크를 Blocked List로 이동
      2. 다른 어떤 태스크가 xQueueReceive() 등으로 큐에서 데이터를 꺼내, 여유 공간이 생기면 →
        • 지정한 태스크가 “다시 Ready”로 복귀 → 다시 CPU 스케줄링 대기
        • 블록된 태스크가 재개(resume)되면 Tail 위치에 아이템 복사 → pdPASS 반환
      3. 만약 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()가 필요한가?

  1. 긴급 메시지 전송
    • 예: 오류 발생 알림, 긴급 제어 명령, 우선순위 높은 센서 알람 등
    • 이미 큐에 여타 “일반 메시지”가 대기 중이어도, 새로운 “긴급 메시지”가 앞에 들어가서 가장 먼저 꺼내짐
  2. 재시도 로직 (Retry Leader-Follower)
    • 수신 실패 시 “재시도 요청”을 큐 앞에 붙여 바로 재전송 시도
    • Ex) 통신 ACK(응답)이 오지 않을 때, “재전송 요청” 메시지를 앞에 삽입
  3. 정책 기반 큐 관리
    • “상태 변화”나 “긴급 확인” 같은 이벤트를 언제든 큐 맨 앞에 넣어 시스템이 즉시 처리하도록

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( ;; );
}

코드 동작 요약

  1. ProducerTask(우선순위 2)
    • 매 100ms마다 i 값을 증가시켜 보냄
    • 만약 i가 50~60 사이면, xQueueSendToFront()로 큐의 맨 앞(Head) 에 넣어 즉시 처리되게 함
    • 나머진 xQueueSendToBack()로 큐 맨 뒤(Tail) 에 넣어 FIFO 순서
  2. ConsumerTask(우선순위 1)
    • xQueueReceive()로 큐 맨 앞부터 꺼내 출력
    • 꺼낸 후 200ms 지연 → 일부러 처리 속도를 느리게 하여 큐가 금방 차도록 유도
  3. 큐 동작
    • 생산 속도 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) 상황
    1. 데이터 손실 가능성
      • xQueueSend…()가 즉시(xTicksToWait=0) 실패 → 그냥 무시하거나 카운터를 올려서 “손실 로깅”
    2. 잠시 대기 후 재시도
      • xTicksToWait = pdMS_TO_TICKS(50)와 같이 짧게 대기했다가, 공간이 생기면 “성공” → 안정성↑
    3. 무한 대기(portMAX_DELAY)
      • 어쩔 수 없이 “무조건 데이터 전송”이 필요할 때. 태스크가 큐에 자리 생길 때까지 영원히 Block → Deadlock 주의!
  • “FromISR”의 경우
    • ISR은 블로킹(Waiting)이 불가능 → 항상 즉시 실패(errQUEUE_FULL) 리턴
    • ISR 내부에서 “① 다른 태스크에 알림(예: xHigherPriorityTaskWoken), ② 간단 로깅/카운터 누적 → 반환” 패턴

9. 실전 팁 & 주의사항

  1. 아이템 크기(Item Size) 주의
    • 32비트 MCU 기준, sizeof(void*) == 4바이트
    • “구조체를 통째로” 큐에 저장하려면, sizeof(MyStruct) × 길이만큼 큐 버퍼가 필요 → 메모리 불안정
    • 일반적으로는 “포인터 큐”(sizeof(Type*))→ 4~8바이트만 버퍼링 후, 메모리는 pvPortMalloc() 등으로 따로 할당
  2. 우선순위 설정
    • 생산자(Producer) 태스크 우선순위 > 소비자(Consumer) 태스크 우선순위 → FIFO 큐가 쌓여도 안정적으로 “집중 처리”
    • ISR에서 보낼 때 → xQueueSendToBackFromISR() 호출 직후, portYIELD_FROM_ISR()로 소비자 태스크 우선순위에 따라 즉시 전환
  3. xTicksToWait를 0으로 두면 “절대 안 블록”
    • ISR이 아닌, 태스크 컨텍스트에서 일반 xQueueSendToBack()를 호출할 때,
    • “큐가 가득 차 있으면 바로 실패” 로 하려면 xTicksToWait=0을 지정
    • 너무 큰 xTicksToWait → 시스템 응답성 저하 또는 데드락 위험 → 짧은 시간(예: 10~50ticks) 권장
  4. 인터럽트 우선순위 configMAX_SYSCALL_INTERRUPT_PRIORITY 확인
    • ISR 내부에서 FreeRTOS 큐 API(*FromISR) 사용하려면, 반드시 해당 IRQ 우선순위 설정이
      configMAX_SYSCALL_INTERRUPT_PRIORITY (예: 0x50) 보다 “큰 수치(낮은 우선순위)” 여야 함
    • 우선순위가 더 높으면(숫자 작음) → FreeRTOS 커널 API가 제대로 동작하지 않거나 동기화 실패
  5. 큐 삭제(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 공식 문서