RTOS #16

홍태준·2026년 5월 4일

RTOS

목록 보기
16/20
post-thumbnail

Week 4 Day 1: Queue 기본 개념

학습 목표

  • Queue가 Semaphore와 달리 데이터 자체를 전달할 수 있는 이유를 이해한다
  • FreeRTOS Queue의 FIFO 구조와 내부 복사(by value) 동작 원리를 파악한다
  • xQueueCreate(), xQueueSend(), xQueueReceive()의 기본 사용법을 익힌다
  • ISR에서 안전하게 Queue를 사용하는 FromISR API 패턴을 실습한다

1. Queue가 필요한 이유

1.1 Semaphore의 한계

Week 3에서 학습한 Semaphore는 이벤트 발생 여부만 전달합니다. "ADC 변환이 완료되었다"는 신호는 줄 수 있지만 "ADC 값이 2048이다"라는 데이터 자체는 함께 전달하지 못합니다. 데이터를 전달하려면 Semaphore와 별개로 전역 변수를 두어야 하고, 이때 volatile 선언과 임계 구역 처리를 직접 관리해야 합니다.

Queue는 이벤트 신호와 데이터를 하나의 메커니즘으로 통합합니다. 생산자(Producer)가 Queue에 데이터를 넣으면 소비자(Consumer)가 꺼내며, FreeRTOS가 내부적으로 복사와 동기화를 모두 처리합니다.

비교 항목Semaphore (Binary/Counting)Queue
전달 내용신호(이벤트 발생 여부)만데이터 값 자체
전역 변수 필요데이터용 전역 변수 별도 필요불필요 (Queue 내부 버퍼 사용)
복수 데이터 누적Counting Semaphore로 부분 가능큐 깊이만큼 누적 가능
데이터 타입없음생성 시 지정한 임의 타입
ISR 사용xSemaphoreGiveFromISR()xQueueSendFromISR()

1.2 FIFO 구조와 내부 복사 동작

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 아이템은 포인터가 아닌 값을 복사합니다. 구조체를 아이템으로 사용할 때 구조체가 크면 복사 오버헤드가 있습니다. 이 경우 구조체 포인터를 아이템으로 사용하는 패턴을 쓰지만, 포인터가 가리키는 메모리의 생명주기 관리에 주의해야 합니다.


2. Queue 생성 및 기본 API

2.1 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);

2.2 데이터 전송: xQueueSend()

/*
 * 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));
    }
}

2.3 데이터 수신: xQueueReceive()

/*
 * 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);
        }
    }
}

2.4 Queue 상태 조회

/*
 * 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);
    }
}

3. ISR에서 Queue 사용

3.1 FromISR API

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를 구분해야 합니다.

3.2 ISR에서 데이터 수신

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

4. 실습: 간단한 메시지 전달

실습 1: 단일 생산자-소비자 (숫자 전달)

생산자 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);
}

실습 2: 구조체 메시지 전달

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

실습 3: ISR → Queue → Task 파이프라인

타이머 인터럽트에서 데이터를 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);
}

5. Semaphore vs Queue 선택 기준

5.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 아이템으로 사용하는 패턴이 효율적입니다.


학습 정리

오늘 배운 핵심 내용

  1. Queue 기본 구조

    • FIFO 방식, 생성 시 큐 깊이와 아이템 크기 지정
    • 아이템을 값으로 복사하므로 송신 측 변수가 이후 변경되어도 안전
    • configASSERT(xQueue != NULL)으로 생성 실패 즉시 감지
  2. 핵심 API

    • xQueueSend(): 큐 끝에 추가, 꽉 찼으면 대기 또는 실패
    • xQueueReceive(): 앞에서 꺼냄, 비었으면 대기 또는 실패
    • xQueuePeek(): 꺼내지 않고 복사만 (큐 상태 유지)
    • uxQueueMessagesWaiting(): 현재 저장된 아이템 수 조회
  3. ISR 전용 API

    • ISR에서 반드시 xQueueSendFromISR() 사용
    • pxHigherPriorityTaskWoken + portYIELD_FROM_ISR() 패턴 필수
    • ISR에서는 xTicksToWait 없음 → 큐 포화 시 즉시 errQUEUE_FULL 반환
  4. Semaphore와의 구분

    • 데이터 전달 필요 → Queue
    • 이벤트 신호만 → Semaphore
    • 공유 자원 보호 → Mutex

핵심 API 요약

함수용도컨텍스트
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

실습 과제

과제 1: 멀티 채널 ADC 메시지 Queue

4개 ADC 채널에서 100ms마다 측정값을 수집하고, 채널 번호와 측정값을 구조체로 Queue에 넣어 처리 Task가 전압으로 변환 후 출력하는 시스템을 구현하십시오.

요구사항:

  • 구조체 아이템: channel(uint8_t), raw(uint16_t), timestamp_ms(uint32_t)
  • 큐 깊이: 16 (처리 지연 시 최대 16개 누적)
  • 처리 Task 우선순위: 수집 Task보다 낮게 설정하여 큐 포화 시나리오 실습
  • 큐 포화 시 드롭 카운터 증가 및 500ms마다 통계 출력

과제 2: 우선순위 명령 처리기

일반 명령(LED 제어)과 긴급 명령(비상 정지)을 같은 Queue로 처리하되, 긴급 명령은 xQueueSendToFront()를 사용하여 즉시 처리되도록 구현하십시오.

요구사항:

  • 명령 구조체: type(uint8_t), param(uint32_t)
  • 일반 명령: xQueueSend(), 긴급 명령: xQueueSendToFront()
  • 처리 Task에서 type에 따라 분기 처리
  • 긴급 명령이 일반 명령보다 먼저 처리됨을 로그로 확인

과제 3: UART RX → Queue → 처리 파이프라인

UART 수신 인터럽트에서 바이트를 Queue에 넣고, 처리 Task에서 바이트를 꺼내 줄 단위로 조합하여 명령을 파싱하는 구조를 구현하십시오.

요구사항:

  • Queue 아이템 크기: sizeof(uint8_t), 큐 깊이: 128
  • ISR: HAL_UART_RxCpltCallback에서 xQueueSendFromISR() 사용
  • 처리 Task: \n 수신 시까지 누적 후 명령 파싱
  • 수신 오버런(큐 포화) 발생 시 오류 LED 표시

디버깅 팁

문제 1: xQueueCreate()가 NULL 반환

/*
 * 증상: configASSERT 실패 또는 NULL 포인터 접근으로 HardFault
 * 원인 1: FreeRTOS 힙 메모리 부족
 *   → FreeRTOSConfig.h에서 configTOTAL_HEAP_SIZE 증가
 *
 * 진단: xPortGetFreeHeapSize()로 남은 힙 확인
 */
printf("남은 힙: %u bytes\n", (unsigned)xPortGetFreeHeapSize());

/* 큐 하나가 사용하는 메모리 근사치:
 * 큐 구조체(~80 bytes) + 아이템 버퍼(uxLength × uxItemSize) */

문제 2: ISR에서 xQueueSend() 호출로 시스템 멈춤

/*
 * 증상: 인터럽트 발생 직후 시스템 응답 없음 또는 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);
}

문제 3: 데이터를 포인터로 Queue에 넣었을 때 손상

/*
 * 증상: 수신 측에서 꺼낸 데이터가 쓰레기값
 * 원인: 스택 변수의 주소를 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 내용을 즉시 복사 */
}
profile
당신의 코딩 메이트

0개의 댓글