FreeRTOS Queue와 ISR 패턴
EmbeddedFreeRTOSRTOS
Queue는 FreeRTOS에서 태스크 간 데이터를 전달하는 기본 메커니즘이다. FIFO 구조이며, 읽기/쓰기 양쪽에서 블로킹할 수 있다.
기본 사용
// 생성: 10개 슬롯, 각 슬롯 sizeof(int)
QueueHandle_t queue = xQueueCreate(10, sizeof(int));
// 쓰기 (생산자 태스크)
int value = 42;
xQueueSend(queue, &value, pdMS_TO_TICKS(100));
// 100ms 이내에 빈 슬롯이 없으면 pdFAIL 반환
// 읽기 (소비자 태스크)
int received;
if (xQueueReceive(queue, &received, portMAX_DELAY) == pdTRUE) {
// received == 42
}
// portMAX_DELAY: 데이터가 올 때까지 무한 대기 (CPU 소모 없음)
xQueueSend는 값을 복사한다. 포인터가 아니라 데이터 자체가 큐에 들어간다. 큰 데이터는 포인터를 큐에 넣되, 포인터가 가리키는 메모리의 수명을 관리해야 한다.
xQueueSend 변형
| 함수 | 동작 |
|---|---|
xQueueSend / xQueueSendToBack | 큐 뒤에 삽입 (FIFO) |
xQueueSendToFront | 큐 앞에 삽입 (긴급 메시지) |
xQueueOverwrite | 큐 길이 1 전용. 기존 값을 덮어씀 |
xQueueOverwrite는 “최신 값만 의미 있는” 경우에 쓴다. 센서 최신 측정값을 다른 태스크에 전달할 때 유용하다.
QueueHandle_t latest_temp = xQueueCreate(1, sizeof(float));
// 센서 태스크: 항상 최신 값으로 덮어씀
float temp = read_temperature();
xQueueOverwrite(latest_temp, &temp);
// 제어 태스크: 최신 값 읽기 (제거하지 않음)
float current;
xQueuePeek(latest_temp, ¤t, portMAX_DELAY);
ISR에서 큐 사용
ISR(인터럽트 서비스 루틴)에서는 일반 API를 호출할 수 없다. 일반 API는 내부에서 스케줄러를 조작하는데, ISR에서 이를 수행하면 크래시가 발생한다. FromISR 접미사가 붙은 전용 API를 사용해야 한다.
// GPIO 인터럽트 핸들러
void IRAM_ATTR gpio_isr_handler(void* arg) {
BaseType_t higher_priority_woken = pdFALSE;
uint32_t event = GPIO_EVENT;
xQueueSendFromISR(event_queue, &event, &higher_priority_woken);
// 핵심: 더 높은 우선순위 태스크가 깨어났으면 즉시 컨텍스트 스위칭
portYIELD_FROM_ISR(higher_priority_woken);
}
higher_priority_woken 파라미터
xQueueSendFromISR이 큐에 데이터를 넣으면, 그 큐에서 xQueueReceive로 대기 중이던 태스크가 Blocked → Ready로 전환된다. 그 태스크의 우선순위가 현재 인터럽트 전에 실행 중이던 태스크보다 높으면, higher_priority_woken이 pdTRUE가 된다.
portYIELD_FROM_ISR(pdTRUE)를 호출하면 ISR 종료 시 즉시 높은 우선순위 태스크로 전환된다. 호출하지 않으면 다음 tick까지 대기해야 한다.
ISR에서 큐에 데이터 삽입
│
├── higher_priority_woken == pdTRUE
│ └── portYIELD_FROM_ISR → ISR 직후 높은 우선순위 태스크 실행
│
└── higher_priority_woken == pdFALSE
└── ISR 직전 태스크로 복귀 (깨어난 태스크는 다음 스케줄링 시점에 실행)
ISR에서의 제약
| 일반 API | ISR 전용 API |
|---|---|
xQueueSend | xQueueSendFromISR |
xQueueReceive | xQueueReceiveFromISR |
xSemaphoreGive | xSemaphoreGiveFromISR |
xEventGroupSetBits | xEventGroupSetBitsFromISR |
ISR에서 절대 하면 안 되는 것:
xQueueReceive등 블로킹 가능 API 호출 (ISR은 대기할 수 없다)vTaskDelay호출printf/ESP_LOGI등 무거운 함수 호출
실무 패턴: ISR → 태스크 위임
ISR은 최대한 짧아야 한다. 이벤트 발생만 큐에 넣고, 실제 처리는 태스크에서 수행한다.
// 이벤트 큐
static QueueHandle_t gpio_evt_queue;
// ISR: 이벤트만 큐에 넣고 즉시 리턴
void IRAM_ATTR gpio_isr_handler(void* arg) {
uint32_t gpio_num = (uint32_t)arg;
BaseType_t woken = pdFALSE;
xQueueSendFromISR(gpio_evt_queue, &gpio_num, &woken);
portYIELD_FROM_ISR(woken);
}
// 태스크: 큐에서 이벤트를 꺼내 처리
void gpio_task(void* arg) {
uint32_t gpio_num;
while (1) {
if (xQueueReceive(gpio_evt_queue, &gpio_num, portMAX_DELAY)) {
// 여기서 무거운 처리 수행
printf("GPIO %lu triggered\n", gpio_num);
process_gpio_event(gpio_num);
}
}
}
void app_main(void) {
gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
xTaskCreate(gpio_task, "gpio_task", 4096, NULL, 10, NULL);
gpio_install_isr_service(0);
gpio_isr_handler_add(GPIO_NUM_4, gpio_isr_handler, (void*)GPIO_NUM_4);
}
메모
xQueueSendFromISR의 timeout은 항상 0이다 (파라미터 자체가 없음). 큐가 꽉 차면 즉시 실패한다. ISR에서는 대기할 수 없기 때문이다portYIELD_FROM_ISR을 빠뜨리면 동작은 하지만, 응답 지연이 최대 1 tick(보통 1ms) 발생한다. 실시간성이 중요하면 반드시 호출해야 한다- 큐 깊이를 너무 작게 잡으면 ISR에서 데이터가 드롭된다. 최악 케이스의 이벤트 빈도를 고려해 설정한다
- ESP-IDF에서 ISR 핸들러는
IRAM_ATTR을 붙여야 한다. 플래시에서 실행하면 캐시 미스 시 크래시가 발생한다 xQueueReceive는 데이터를 큐에서 제거한다. 제거하지 않고 읽으려면xQueuePeek을 사용한다