개념 정리
이번 포스트에서는 FreeRTOS의 핵심 개념인 Task 를 정리하고, 실습으로 LED Blink(Periodic Task)와 버튼으로 LED 제어(Event-driven Task)를 직접 구현할 예정이다.
Task란?
Task는 FreeRTOS에서 독립적으로 실행되는 작업 단위다. 일반 OS의 스레드(Thread)와 비슷하지만, 훨씬 단순하다.
FreeRTOS의 Task는 반드시 무한루프 구조로 작성한다. 일반 함수처럼 return 하면 안 된다.
void myTask(void *pvParameters) {
while(1) {
// 반복할 작업
vTaskDelay(pdMS_TO_TICKS(100)); // 100ms 대기
}
}
각 Task는 자신만의 Stack 을 가진다. 코드 영역, 전역변수, Heap은 Task 간에 공유된다.
Task의 4가지 상태
Task는 항상 아래 4가지 상태 중 하나에 있다.
Running
현재 CPU를 점유하고 실행 중인 상태다. 단일 코어 MCU에서는 한 번에 하나의 Task만 Running 상태가 될 수 있다.
Ready
실행할 준비는 됐지만, 더 높은 우선순위의 Task가 Running 중이라 대기하는 상태다. 스케줄러가 선택하는 순간 Running으로 전환된다.
Blocked
이벤트나 시간을 기다리는 상태다. vTaskDelay(), xQueueReceive(), xSemaphoreTake() 같은 대기 함수를 호출하면 진입한다. Blocked 상태에서는 CPU를 전혀 사용하지 않는다. 조건이 충족되면 자동으로 Ready 상태로 전환된다.
Suspended
vTaskSuspend() 로 명시적으로 정지시킨 상태다. vTaskResume() 을 호출하기 전까지는 어떤 조건이 충족되어도 깨어나지 않는다.
스케줄러 — 어떤 Task를 언제 실행할까?
FreeRTOS의 기본 스케줄러는 Priority-based Preemptive(우선순위 기반 선점형) 방식이다.
규칙은 단순하다.
Ready 상태의 Task 중 우선순위가 가장 높은 Task를 실행한다. 더 높은 우선순위의 Task가 Ready 상태가 되는 순간, 현재 실행 중인 Task를 즉시 밀어낸다.
// 우선순위 2가 더 높으므로 sensorTask가 우선 실행
xTaskCreate(sensorTask, "Sensor", 128, NULL, 2, NULL);
xTaskCreate(displayTask, "Display", 128, NULL, 1, NULL);
같은 우선순위라면? — Round Robin
우선순위가 동일한 Task가 여러 개면 Round Robin 방식으로 Tick마다 번갈아 실행된다.
taskA 실행 (1 Tick) → taskB 실행 (1 Tick) → taskA → taskB → ...
Tick — RTOS의 시간 단위
FreeRTOS는 Tick 을 시간의 기본 단위로 사용한다. FreeRTOSConfig.h 에서 설정한다.
#define configTICK_RATE_HZ 1000 // 1ms마다 Tick 발생 (기본값)
Tick이 발생할 때마다 SysTick 인터럽트가 발생하고, 스케줄러가 실행할 Task를 결정한다.
시간을 지정할 때는 pdMS_TO_TICKS() 매크로로 ms 단위를 Tick으로 변환한다.
vTaskDelay(pdMS_TO_TICKS(500)); // 500ms = 500 Ticks
Context Switching — Task 전환 시 내부에서 일어나는 일
스케줄러가 Task를 전환할 때, CPU는 현재 Task의 상태를 Stack에 저장하고 다음 Task의 상태를 복원한다. 이를 Context Switch 라고 한다.
Cortex-M4 기준으로 저장되는 항목들이다.
PC (Program Counter — 어디까지 실행했는지)
LR (Link Register)
PSR (상태 레지스터)
R0~R12 (범용 레지스터)
S0~S31 (FPU 사용 시 부동소수점 레지스터)
FreeRTOS에서 Context Switch는 PendSV 인터럽트 핸들러 안에서 일어난다. SysTick이 매 Tick마다 PendSV를 트리거하는 구조다.
Task의 실행 패턴 3가지
Periodic Task — 주기적 실행
일정 주기마다 반복 실행되는 패턴이다. vTaskDelay() 로 대기하면서 CPU를 양보한다.
void ledBlinkTask(void *pvParameters) {
while(1) {
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
vTaskDelay(pdMS_TO_TICKS(500)); // 500ms마다 실행
}
}
센서 읽기, 디스플레이 업데이트처럼 "몇 ms마다 한 번" 해야 하는 작업에 적합하다.
Event-driven Task — 이벤트 기반 실행
특정 이벤트가 발생할 때만 깨어나서 처리하는 패턴이다. 평소에는 Blocked 상태라 CPU를 전혀 사용하지 않는다.
void buttonTask(void *pvParameters) {
while(1) {
// 세마포어가 올 때까지 무한 대기 (CPU 사용 0%)
xSemaphoreTake(buttonSemaphore, portMAX_DELAY);
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}
}
// 버튼 인터럽트 핸들러
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin) {
if (GPIO_Pin == GPIO_PIN_13) {
xSemaphoreGiveFromISR(buttonSemaphore, NULL); // Task 깨움
}
}
버튼 입력, UART 수신처럼 "언제 올지 모르는" 이벤트 처리에 적합하다.
여기서 중요한 점이 있다. 버튼 감지는 ISR(인터럽트 핸들러) 이 하고, 실제 처리는 Task 가 한다. ISR은 최대한 짧게 끝내고(세마포어만 전달), 실제 처리는 Task에 맡기는 것이 RTOS의 기본 패턴이다. 이를 Deferred Interrupt Processing 이라고 하며, 이후 포스트에서 자세히 다룬다.
Continuous Task — 연속 실행
가장 낮은 우선순위로 설정해, 다른 Task들이 모두 Blocked 상태일 때 남는 CPU 시간을 사용하는 패턴이다. 로그 처리, 백그라운드 연산 등에 활용한다. FreeRTOS의 Idle Task 가 이 패턴의 대표적인 예다.
자주 하는 실수 — Task Starvation
vTaskDelay() 없이 while(1)만 돌면, 낮은 우선순위의 Task는 영원히 실행되지 못한다. 이를 Starvation(기아 현상) 이라고 한다.
// 잘못된 예 — 다른 Task가 실행되지 못함
void badTask(void *p) {
while(1) {
doSomething();
// delay 없음 → Starvation 발생
}
}
// 올바른 예 — CPU를 양보하여 다른 Task 실행 가능
void goodTask(void *p) {
while(1) {
doSomething();
vTaskDelay(pdMS_TO_TICKS(10)); // 반드시 대기 호출
}
}
정리
항목 내용
| Task 구조 | 무한루프 + 대기 호출 |
| 상태 | Running / Ready / Blocked / Suspended |
| 스케줄러 | 우선순위 기반 선점형 |
| 시간 단위 | Tick (기본 1ms) |
| 실행 패턴 | Periodic / Event-driven / Continuous |
다음 파트에서는 위 개념을 바탕으로 Nucleo-F401RE 보드에서 직접 Task를 만들어 LED를 제어해본다. 먼저 Periodic Task로 LED를 500ms마다 깜빡이게 하고, 이후 Event-driven Task로 버튼을 누를 때 LED가 켜지도록 구현한다.