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) 삭제된 태스크의 메모리가 정리되지 않아 힙이 고갈된다