FreeRTOS Task와 스케줄링

EmbeddedFreeRTOSRTOS

FreeRTOS는 임베디드 시스템에서 가장 많이 사용되는 실시간 운영체제다. Task(태스크)는 FreeRTOS의 실행 단위로, 일반 OS의 스레드에 해당한다.

Task 생성

TaskHandle_t handle;
xTaskCreate(
    task_function,   // 태스크 함수 포인터
    "TaskName",      // 이름 (디버깅용)
    2048,            // 스택 크기 (워드 단위, 32bit MCU면 2048 × 4 = 8KB)
    NULL,            // 파라미터 (void*)
    5,               // 우선순위 (숫자가 클수록 높음)
    &handle          // 핸들 (NULL 가능)
);

태스크 함수는 반드시 무한 루프여야 한다. 리턴하면 정의되지 않은 동작이 발생한다.

void task_function(void* param) {
  while (1) {
    // 작업 수행
    vTaskDelay(pdMS_TO_TICKS(100));  // 100ms 대기
  }
  // 여기에 도달하면 안 된다
  // 만약 태스크를 종료해야 하면: vTaskDelete(NULL);
}

우선순위와 선점

FreeRTOS는 선점형(preemptive) 스케줄러가 기본이다. 높은 우선순위 태스크가 Ready 상태가 되면 현재 실행 중인 낮은 우선순위 태스크를 즉시 밀어낸다.

  • 우선순위 범위: 0 (최저, Idle Task) ~ configMAX_PRIORITIES - 1 (최고)
  • 같은 우선순위: 라운드 로빈. configTICK_RATE_HZ마다 번갈아 실행
  • Idle Task (우선순위 0): 다른 태스크가 모두 Blocked일 때 실행. 삭제된 태스크의 메모리를 정리한다

Task 상태

                    xTaskCreate()


                    ┌─────────┐
         ┌─────────│  Ready   │◀──── 이벤트 발생/타임아웃
         │         └─────────┘
         │ 스케줄러 선택    ▲
         ▼              │ 선점 (더 높은 우선순위 Ready)
    ┌──────────┐        │
    │ Running  │────────┘
    └──────────┘

         │ vTaskDelay / xQueueReceive / xSemaphoreTake 등

    ┌──────────┐
    │ Blocked  │  타이머 만료 또는 대기 중인 이벤트 발생 시 → Ready
    └──────────┘

         │ vTaskSuspend()

    ┌───────────┐
    │ Suspended │  vTaskResume()으로만 복귀 → Ready
    └───────────┘

Blocked와 Suspended의 차이: Blocked는 조건(시간 경과, 큐에 데이터 도착 등)이 충족되면 자동으로 Ready가 된다. Suspended는 명시적으로 vTaskResume()을 호출해야 한다.

vTaskDelay vs vTaskDelayUntil

// vTaskDelay: 호출 시점부터 상대적 대기
void sensor_task(void* param) {
  while (1) {
    read_sensor();          // 실행 시간 가변
    vTaskDelay(pdMS_TO_TICKS(100));  // 실행 완료 후 100ms 대기
  }
  // 주기: 실행시간 + 100ms (일정하지 않음)
}

// vTaskDelayUntil: 절대 시점 기준 대기
void control_task(void* param) {
  TickType_t last_wake = xTaskGetTickCount();
  while (1) {
    run_control_loop();     // 실행 시간 가변
    vTaskDelayUntil(&last_wake, pdMS_TO_TICKS(10));  // 정확히 10ms 주기
  }
  // 주기: 정확히 10ms (실행시간이 10ms 미만이면)
}

제어 루프처럼 일정한 주기가 필요하면 vTaskDelayUntil을 사용한다.

스택 크기 결정

스택 오버플로우는 임베디드에서 가장 흔한 크래시 원인이다.

스택에 쌓이는 것:

  • 지역 변수
  • 함수 호출 체인 (재귀 포함)
  • 컨텍스트 스위칭 시 레지스터 저장
  • 인터럽트 발생 시 중첩 프레임 (일부 아키텍처)

디버깅 방법:

// FreeRTOSConfig.h
#define configCHECK_FOR_STACK_OVERFLOW 2  // 워터마크 방식

// 콜백 구현
void vApplicationStackOverflowHook(TaskHandle_t task, char* name) {
  printf("Stack overflow: %s\n", name);
  while (1);  // 여기서 멈추면 디버거로 호출 스택 확인
}

// 런타임에 남은 스택 확인
UBaseType_t remaining = uxTaskGetStackHighWaterMark(handle);
// remaining이 0에 가까우면 스택 부족

실무 규칙: 예상 사용량의 1.5~2배로 설정하고, uxTaskGetStackHighWaterMark로 실측 후 조정한다.

메모

  • xTaskCreate는 힙에서 스택을 할당한다. 정적 할당이 필요하면 xTaskCreateStatic을 사용한다
  • vTaskDelay(0)은 yield와 같다. 같은 우선순위의 다른 태스크에 실행 기회를 준다
  • 태스크 이름은 configMAX_TASK_NAME_LEN (기본 16) 이내로 잘린다. 디버깅 전용이다
  • ESP-IDF에서는 xTaskCreatePinnedToCore로 특정 코어에 태스크를 바인딩할 수 있다. BLE 호스트 태스크를 Core 0에, 애플리케이션을 Core 1에 분리하는 패턴이 일반적이다
  • Idle Task를 굶기면(우선순위 0 이상의 태스크가 항상 Running) 삭제된 태스크의 메모리가 정리되지 않아 힙이 고갈된다