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, &current, 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_wokenpdTRUE가 된다.

portYIELD_FROM_ISR(pdTRUE)를 호출하면 ISR 종료 시 즉시 높은 우선순위 태스크로 전환된다. 호출하지 않으면 다음 tick까지 대기해야 한다.

ISR에서 큐에 데이터 삽입

  ├── higher_priority_woken == pdTRUE
  │     └── portYIELD_FROM_ISR → ISR 직후 높은 우선순위 태스크 실행

  └── higher_priority_woken == pdFALSE
        └── ISR 직전 태스크로 복귀 (깨어난 태스크는 다음 스케줄링 시점에 실행)

ISR에서의 제약

일반 APIISR 전용 API
xQueueSendxQueueSendFromISR
xQueueReceivexQueueReceiveFromISR
xSemaphoreGivexSemaphoreGiveFromISR
xEventGroupSetBitsxEventGroupSetBitsFromISR

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을 사용한다