Software Timer 분석 — Daemon Task·자료구조·ISR-Safe API
#한 줄 요약
“Software Timer는 하나의 hardware tick으로 수많은 software 만기를 관리합니다.” — 정확도는 떨어지지만 갯수가 사실상 무제한입니다.
#어떤 문제를 푸는가
hardware timer는 정확합니다. STM32 TIM2 같은 peripheral은 수십 ns 단위로 만기를 잡고 ISR을 직접 호출합니다. 다만 그 수가 한정적입니다. STM32F4에 들어 있는 general-purpose timer는 10개 남짓에 불과합니다.
현실 시스템은 수십~수백 개의 timeout을 동시에 관리합니다. TCP retransmit timer, watchdog kick, LED blink, 센서 polling, session timeout, heartbeat. 모두에 hardware timer를 하나씩 붙일 수는 없습니다.
해결책이 software timer입니다. 단 하나의 hardware tick만 받고, 그 위에서 수많은 software timer를 sorted 구조로 관리합니다. tick마다 가장 가까운 만기를 검사해 expire된 timer의 callback을 실행합니다.
대표 구현이 FreeRTOS의 timer service task입니다. 이번 편은 이 구조의 자료구조 선택과 ISR-safe API를 풀어 봅니다.
#Hardware vs Software Timer
| 항목 | Hardware Timer | Software Timer |
|---|---|---|
| 정확도 | cycle 단위 | tick 단위 (보통 1~10 ms) |
| 갯수 | peripheral 수 | 메모리만큼 |
| 만기 처리 | IRQ에서 직접 | daemon task의 callback |
| 비용 | 매우 낮음 | tick + daemon overhead |
| 적합 용도 | 정확 주기 control loop | 일반 timeout, LED, polling |
RT 제어 루프처럼 주기 정확성이 핵심인 작업은 hardware timer가 맞습니다. 반대로 watchdog 갱신이나 session timeout처럼 수 ms 오차가 무관한 작업은 software timer가 효율적입니다.
#FreeRTOS Timer 구조
각 timer는 list item + 만기 tick + period + callback을 가진 구조체입니다.
typedef struct timer_t { char *name; ListItem_t list_item; /* sorted list 노드 */ TickType_t period; UBaseType_t auto_reload; void *id; TimerCallbackFunction_t callback;} Timer_t;활성 timer들은 만기 tick 오름차순의 단일 list에 매달려 있습니다. head가 가장 빨리 만료될 timer입니다.
#Timer Service Task — Daemon
FreeRTOS는 timer 처리를 위한 전용 task를 부팅 시 자동으로 만듭니다. 일반적으로 Tmr Svc라는 이름으로 보이는 그 task입니다.
void prvTimerTask(void *p) { for (;;) { TickType_t now = xTaskGetTickCount();
/* 1. 만기 도달한 timer들 처리 */ while (head && head->expiry_tick <= now) { Timer_t *t = pop_head(timer_list); t->callback(t); if (t->auto_reload) { t->expiry_tick = now + t->period; insert_sorted(t); } }
/* 2. 다음 만기까지 또는 새 command까지 wait */ TickType_t wait = head ? head->expiry_tick - now : portMAX_DELAY; TimerCmd_t cmd; if (xQueueReceive(timer_cmd_queue, &cmd, wait)) { process_command(&cmd); } }}핵심은 tick ISR이 직접 callback을 호출하지 않는다는 것입니다. SysTick은 tick count만 증가시키고, daemon task가 깨어나 처리합니다. 이 한 단계가 callback을 task 컨텍스트에서 실행하게 하여 RTOS API 호출을 안전하게 만듭니다.
configTIMER_TASK_PRIORITY로 daemon priority를 정합니다. 일반적으로 높은 priority를 줘서 timer 처리가 다른 task에 밀리지 않게 합니다.
#자료구조 — Sorted List, Delta List, Timer Wheel, Heap
만기가 가까운 순서를 빠르게 찾아야 합니다. 자료구조 선택이 전체 timer 시스템의 비용 곡선을 결정합니다.
#Sorted List
가장 단순합니다. 만기 tick 오름차순으로 linked list를 유지합니다.
- insert — O(N), 적절한 위치 찾기
- expire check — O(1), head만 확인
- pop expired — O(1)
timer 수가 적으면(수십 개) 충분합니다. FreeRTOS가 이 구조를 씁니다.
#Delta List
각 노드가 절대 만기가 아닌 앞 노드와의 차이를 저장합니다.
head → +5 → +3 → +10 → +2 → ...tick마다 head의 delta만 1 감소시키면 됩니다. insert 비용은 sorted list와 같지만 *tick 처리가 O(1)*입니다. tick ISR에서 직접 처리하는 시스템에 적합합니다.
#Timer Wheel
원형 배열을 시계처럼 사용합니다. 각 slot이 그 시각에 만료될 timer들의 list를 가집니다.
slot[0] → timer A, timer Bslot[1] → (empty)slot[2] → timer C...slot[N-1] → timer D- insert — O(1),
slot index = expiry_tick % N - expire check — O(slot 안 timer 수), 보통 O(1)
Linux kernel의 hrtimer는 hierarchical timer wheel을 씁니다. 수 ms 정확도에서 *수천 개 timer를 O(1)*로 다룹니다.
#Min-Heap
priority queue로 구현하면 *insert와 pop 모두 O(log N)*입니다. Linux posix-timers가 이 구조 위에서 동작합니다.
#비교
| Timer 수 | Sorted list | Min-heap | Timer wheel |
|---|---|---|---|
| 10 | insert 5 cycle 평균 | — | 4 cycle (slot 접근) |
| 1000 | 500 cycle 평균 (worst 1000) | ~30 cycle (log₂ 1000 ≈ 10) | 4 cycle |
| 10000 | 사실상 못 씀 | ~40 cycle | 4 cycle |
embedded에서 timer 수가 수십 개라면 sorted list로 충분합니다. 수백 개 이상이면 wheel이 답입니다.
#API 사용
FreeRTOS의 timer API는 모두 command를 timer queue로 보내는 형태입니다. 실제 작업은 daemon이 합니다.
TimerHandle_t led = xTimerCreate( "LED", /* name */ pdMS_TO_TICKS(500), /* period */ pdTRUE, /* auto-reload */ (void*)0, /* timer id */ led_callback); /* callback */
xTimerStart(led, 0); /* 시작 */xTimerStop(led, 0); /* 정지 */xTimerReset(led, 0); /* 만기 시각 = now + period */xTimerChangePeriod(led, pdMS_TO_TICKS(1000), 0); /* 주기 변경 */xTimerDelete(led, 0); /* 제거 */마지막 인자는 command queue가 가득 찼을 때 block할 tick 수입니다. 0이면 immediate fail/return입니다.
#One-Shot vs Auto-Reload
/* One-shot — 한 번 발화 후 자동 stop */TimerHandle_t once = xTimerCreate( "delay", pdMS_TO_TICKS(1000), pdFALSE, NULL, cb);xTimerStart(once, 0);/* callback 한 번 실행 후 timer는 dormant 상태. 다시 start 호출로 재실행 */
/* Auto-reload — 주기적 발화 */TimerHandle_t periodic = xTimerCreate( "tick", pdMS_TO_TICKS(100), pdTRUE, NULL, cb);xTimerStart(periodic, 0);/* 매 100 ms마다 callback 자동 호출 */one-shot은 timeout 처리에, auto-reload는 주기 작업에 씁니다.
#Callback Context — Daemon Task에서 실행
callback은 daemon task 컨텍스트에서 실행됩니다. 일반 task API를 모두 호출할 수 있습니다.
static void worker_callback(TimerHandle_t t) { /* daemon task context — RTOS API 모두 사용 가능 */ int id = (int)pvTimerGetTimerID(t); work_item_t w = make_work(id); xQueueSend(work_queue, &w, 0);}다만 callback이 길어지면 모든 timer가 늦어집니다. daemon이 한 callback을 처리하는 동안 다른 timer는 처리되지 않기 때문입니다.
static void bad_callback(TimerHandle_t t) { vTaskDelay(pdMS_TO_TICKS(100)); /* daemon이 100 ms 동안 멈춤 */ process_heavy(); /* 다른 timer 모두 지연 */}원칙은 callback은 짧게, 무거운 일은 work queue로 defer입니다.
#ISR-Safe API
ISR에서는 일반 API 대신 *FromISR 변형을 씁니다.
void some_isr(void) { BaseType_t higher_prio_woken = pdFALSE;
xTimerStartFromISR(t, &higher_prio_woken); xTimerStopFromISR(t, &higher_prio_woken); xTimerResetFromISR(t, &higher_prio_woken);
portYIELD_FROM_ISR(higher_prio_woken);}내부적으로 interrupt-safe queue API로 command를 daemon에 전달합니다. daemon이 그 명령을 받아 task 컨텍스트에서 처리하므로 ISR이 timer 자료구조를 직접 만지지 않습니다.
#xTimerPendFunctionCall — 임의 함수 Deferral
timer 자체가 필요 없을 때도 daemon을 워크 deferral 큐로 활용할 수 있습니다.
static void deferred_work(void *arg1, uint32_t arg2) { /* daemon task context */ process_isr_event((int)arg2, arg1);}
void __attribute__((interrupt)) some_isr(void) { BaseType_t higher_prio_woken = pdFALSE; xTimerPendFunctionCallFromISR( deferred_work, event_data, event_id, &higher_prio_woken); portYIELD_FROM_ISR(higher_prio_woken);}ISR에서 복잡한 처리를 직접 하지 않고 daemon으로 미루는 deferred interrupt handling의 전형입니다. ISR을 짧게 유지하면서 task 컨텍스트의 자유로움을 얻습니다.
#정확도 한계
software timer의 정확도는 tick 주기 + daemon scheduling의 합으로 제한됩니다.
configTICK_RATE_HZ = 100이면 tick 주기는 10 ms입니다. timer 만기를 50 ms로 설정하면 5 tick 후에 daemon이 처리합니다.
- best case — 50.0 ms
- worst case — 50 ms + daemon 대기 + 다른 callback 처리 시간 → 보통 51~52 ms, 부하 심하면 60+ ms
수 µs 정확도가 필요한 control loop은 hardware timer + semaphore로 가야 합니다.
/* Hardware timer ISR */void TIM2_IRQHandler(void) { HAL_TIM_IRQHandler(&brake_tim);}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *t) { if (t == &brake_tim) { BaseType_t hp = pdFALSE; xSemaphoreGiveFromISR(brake_sem, &hp); portYIELD_FROM_ISR(hp); }}
/* 제어 task — 1 ms 정확 주기 */void brake_task(void *p) { for (;;) { xSemaphoreTake(brake_sem, portMAX_DELAY); do_brake_cycle(); }}hardware timer가 정확한 주기를, semaphore가 task 컨텍스트로의 전달을 담당합니다.
#Tickless Idle — Battery 시스템
저전력이 중요한 IoT에서는 tick 자체를 끄고 다음 만기에 깨어나는 tickless 모드를 씁니다.
#define configUSE_TICKLESS_IDLE 1void portSUPPRESS_TICKS_AND_SLEEP(TickType_t idle_ticks) { /* 다음 만기에 맞춰 SysTick reload */ uint32_t reload = idle_ticks * cycles_per_tick - 1; SysTick->LOAD = reload; SysTick->VAL = 0;
__WFI(); /* sleep */
/* 깨어난 뒤 실제 경과 tick 보정 */ TickType_t actual = compute_elapsed(); vTaskStepTick(actual);}활성 timer 중 가장 가까운 만기까지 MCU 전체가 sleep하므로 평균 전류가 수십 µA 수준으로 떨어집니다. 배터리 IoT 펌웨어의 표준입니다.
#자주 보는 함정과 안티패턴
경고 — callback에서 긴 작업
static void cb(TimerHandle_t t) { long_blocking_io(); /* 다른 timer 모두 정지 */ vTaskDelay(pdMS_TO_TICKS(100)); /* daemon이 100 ms 멈춤 */}callback은 수 µs 안에 끝내거나 signal만 보내고 즉시 return하는 형태로 짭니다.
경고 — daemon priority가 너무 낮음
#define configTIMER_TASK_PRIORITY 1 /* 다른 task가 daemon을 자주 막음 */낮은 priority면 timer 정확도가 떨어지고 callback이 수 tick씩 늦게 호출됩니다. 일반적으로 application의 highest priority 근처로 둡니다.
경고 — timer command queue가 너무 작음
#define configTIMER_QUEUE_LENGTH 5 /* burst 시 overflow */xTimerStart가 silent fail하기 시작합니다. 동시에 만지는 timer 수의 2~3배 이상으로 잡습니다.
경고 — ISR에서 task API 사용
void isr(void) { xTimerStart(t, 0); /* 잘못 — `*FromISR` 변형 필요 */}immediate hard fault가 나지 않고 silent 자료구조 corruption으로 이어지는 경우가 많습니다. xTimerStartFromISR로 교체합니다.
#정리
- Software timer는 하나의 hardware tick으로 수많은 software 만기를 관리하며, 갯수 제한 없이 timeout, polling, blink 같은 작업을 처리합니다.
- 만기 처리는 daemon task에서 수행되어 callback이 task 컨텍스트에서 동작합니다.
- 자료구조는 sorted list, delta list, timer wheel, min-heap 중에서 timer 수와 정확도 요구에 맞춰 고릅니다. 수십 개면 sorted list, 수백 이상이면 wheel이 합리적입니다.
- one-shot은 timeout에, auto-reload는 주기 작업에 어울리며, callback은 짧게 + signal이 원칙입니다.
- ISR에서는
*FromISR변형과xTimerPendFunctionCall로 작업을 daemon에 deferral합니다. - µs 단위 정확도가 필요하면 hardware timer + semaphore 조합으로 가야 합니다.
- 저전력이 중요하면 tickless idle로 sleep 시간을 극대화합니다.
다음 편은 4-10 System Call에서 user/kernel 모드 분리와 SVC trap을 다룹니다.
#관련 항목
Practical RTOS Internals · 42 of 53
- 1Practical RTOS Internals — 실시간 커널 내부 분석 시리즈 소개
- 2RTOS가 필요한 이유 — 일반 OS와의 결정적 차이
- 3Task와 Thread 개념 — TCB·상태 머신·생명 주기 분석
- 4실시간 스케줄링 알고리즘 비교 — RR·Priority·EDF·RMS
- 5Preemption과 Cooperation — 강제 전환 vs 자발 양보
- 6인터럽트와 RTOS — ISR Context·Deferred Processing·FromISR API
- 7동기화 기초 분석 — Critical Section·Mutual Exclusion·Race Condition
- 8Semaphore 개념 분해 — Counting·Binary·P/V 연산
- 9Mutex 개념 분해 — Ownership·Recursive·Priority Inheritance
- 10큐와 메시지 패싱 — Producer-Consumer·Ring Buffer·전달 의미
- 11실시간성 분석 — Latency·Jitter·Deadline·WCET·RMA
- 12Ready List 자료구조 분석 — Linked List·Bitmap·O(1) Scheduler
- 13Blocked List 자료구조 — Timeout 정렬·Delta List·Two-List Scheme
- 14Scheduler 알고리즘 구현 추적 — Next-Task Selection 로직
- 15Context Switch 원리 분석 — 레지스터 저장·복원·Stack Frame
- 16ARM Cortex-M Context Switch — PendSV·MSP/PSP 어셈블리 추적
- 17ARM Cortex-A Context Switch — Mode 전환·SVC·Banked Registers
- 18RISC-V Context Switch 분석 — ECALL·mret·CSR
- 19RTOS Tick과 타이머 — SysTick·Generic Timer·configTICK_RATE_HZ
- 20Tickless 모드 구현 — Idle Tick Suppression·Sleep·Wake 보정
- 21Scheduler Latency 측정 기법 — GPIO Toggle·DWT·ftrace·cyclictest
- 22RTOS Tracing과 Observability — Tracealyzer·SystemView·ITM/ETM
- 23Critical Section 구현 비교 — IRQ Disable·BASEPRI·Spinlock
- 24Semaphore 내부 구현 추적 — Counter·Wait List·ISR-Safe Variant
- 25Mutex 내부 구현 추적 — Owner·Recursion Count·ISR 금지
- 26Priority Inversion 문제 — Mars Pathfinder 사례·Bounded vs Unbounded
- 27Priority Inheritance 구현 — Inherit·Disinherit·Chain
- 28Priority Ceiling Protocol — Immediate vs Original 비교
- 29Queue 내부 구현 추적 — Ring Buffer·2 Wait Lists·Atomic Send/Receive
- 30Event Group 분석 — Bit Flag·AND/OR Wait·Sync Barrier
- 31ISR-Safe API 설계 — FromISR 패턴·Higher Priority Wake·Deferred Work
- 32Deadlock 분석 — 4 조건·Wait-for Graph·Lock Ordering·Timeout
- 33Stream Buffer와 Message Buffer — FreeRTOS 10의 Lock-Free SPSC
- 34실시간 메모리 요구사항 — Determinism·Fragmentation·WCET
- 35FreeRTOS Heap_1~5 분석 — 5종 Allocator의 구조와 트레이드오프
- 36TLSF Allocator 분석 — Two-Level Segregated Fit O(1)
- 37Static Allocation — 컴파일 타임으로 동적 위험 제거하기
- 38Memory Pool — Fixed-Size Block Allocator의 단순함과 강력함
- 39Stack Overflow 탐지 — Canary·MPU·Watermark 3중 방어
- 40SMP RTOS 설계 — Ready List·Affinity·IPI·Load Balancing
- 41SMP Spinlock 구현 — LDREX/STREX·Ticket Lock·MCS·WFE/SEV
- 42Software Timer 분석 — Daemon Task·자료구조·ISR-Safe API
- 43RTOS System Call — SVC·ECALL·User/Kernel 분리·FreeRTOS-MPU
- 44TrustZone과 TF-M — Secure/Non-Secure·NSC Veneer·PSA
- 45AMP와 OpenAMP — Heterogeneous SoC·RPMsg·remoteproc
- 46C++ in RTOS — RAII·std::thread·ETL·Coroutine
- 47FreeRTOS 소스 분석 — tasks.c·queue.c·port.c 추적
- 48Zephyr 커널 분석 — k_thread·k_sem·Driver Model
- 49RT-Thread 분석 — Object 모델·Components·Smart·Studio
- 50RTOS 포팅 가이드 — 새 아키텍처에 옮기는 절차
- 51RTOS 선택 가이드 — Footprint·License·Certification·Ecosystem
- 52Apache NuttX 분석 — POSIX·PX4·NASA Ingenuity
- 53PREEMPT_RT Linux — Mainline 6.12·Xenomai 4·EVL
관련 글
PREEMPT_RT Linux — Mainline 6.12·Xenomai 4·EVL
2024년 9월 Linux 6.12 mainline에 합류한 PREEMPT_RT의 핵심 변경을 정리하고, Xenomai 4·EVL과 함께 RTOS와의 선택 기준을 비교합니다. threaded IRQ·sleeping spinlock·cyclictest까지 한 지도에 모읍니다.
Apache NuttX 분석 — POSIX·PX4·NASA Ingenuity
NuttX의 POSIX-compliant 구조를 따라가며 PX4 autopilot과 NASA Ingenuity 화성 헬리콥터 채택 배경을 정리합니다. Flat/Protected/Kernel 빌드, VFS, 네트워크, NSH, micro-ROS 통합까지 한 지도로 모읍니다.
RTOS 선택 가이드 — Footprint·License·Certification·Ecosystem
FreeRTOS·Zephyr·ThreadX·RT-Thread·NuttX·VxWorks·QNX·INTEGRITY·SafeRTOS·µC/OS·PX5를 한 표에 모아 비교합니다. IoT·자동차·항공·산업·의료·웨어러블·드론별 추천과 결정 기준을 정리합니다.