임베디드 시스템에서 특정 주기로 LED를 깜빡이거나 0.5초마다 센서 값을 읽어야 하는 요구사항은 매우 흔하게 발생합니다. 이러한 주기적 작업을 구현할 때마다 매번 독립된 태스크(Task)를 새로 생성하고 내부 무한 루프에서 osDelay()를 호출하는 방식을 사용하는 개발자가 많습니다. 하지만 태스크의 개수가 늘어날수록 각 태스크가 차지하는 독립적인 스택(Stack) 메모리 소모량이 급증하고, 스케줄러가 관리해야 할 컨텍스트 스위칭 오버헤드가 커져 리소스가 제한된 MCU 환경에서는 큰 부담이 됩니다.
이때 가장 효율적인 대안이 바로 FreeRTOS가 제공하는 소프트웨어 타이머(Software Timer) 메커니즘입니다. 이번 글에서는 글로벌 표준 인터페이스인 CMSIS-RTOS v2 API를 기준으로 소프트웨어 타이머의 내부 동작 원리를 이해하고, 하드웨어 자원을 최소화하면서 주기적 작업을 처리하는 실무 예제와 설계 규칙을 정리해 드리겠습니다.
핵심 요약 3줄
- 효율적인 자원 관리: 소프트웨어 타이머는 여러 개를 생성하더라도 커널 내 단 하나의 '타이머 서비스 태스크'에서 통합 관리하므로 램(RAM) 자원을 획기적으로 아낍니다.
- 유연한 모드 전환: 한 번만 실행되고 멈추는 단발성(One-shot) 모드와 지정한 간격으로 무한 반복 실행되는 주기적(Periodic) 모드를 API 인자 하나로 쉽게 제어합니다.
- 엄격한 콜백 제약: 타이머 만료 시 구동되는 콜백 함수는 데몬 스레드 컨텍스트에서 실행되므로 내부에서 블로킹 함수나 지연 루프를 절대로 사용해서는 안 됩니다.
1. 소프트웨어 타이머의 개념과 내부 메커니즘
소프트웨어 타이머는 개발자가 설정한 목표 시간이 경과(만료)했을 때, 미리 등록해 둔 특정 콜백 함수(Callback Function)를 커널이 자동으로 알아서 실행해 주는 시간 관리 메커니즘입니다. 하드웨어 타이머 인터럽트와 달리, RTOS 스케줄러가 관리하는 소프트웨어 카운터를 기반으로 동작하므로 MCU 내부의 물리적인 타이머 주변장치 자원을 소모하지 않는다는 강력한 장점이 있습니다.
| 주요 특징 | 세부 동작 및 이점 |
| 비차단(Non-blocking) 타이밍 | 타이머의 시간 카운팅 연산은 백그라운드 영역에서 조용히 수행되므로 다른 일반 메인 태스크의 비즈니스 로직 실행을 방해하지 않습니다. |
| 두 가지 동작 모드 제공 | 필요에 따라 일회성 지연 이벤트를 처리하는 단발성 모드와, 고정된 타임슬롯을 갖는 주기적 반복 모드를 명확히 분리하여 설정할 수 있습니다. |
| 메모리 절약 효과 | 10개의 주기적 작업을 태스크로 만들면 10개의 독립 스택이 필요하지만, 소프트웨어 타이머로 구현하면 10개 모두 하나의 공유 데몬 스택을 사용합니다. |
2. 핵심 API 사용법 (CMSIS-RTOS v2)
CMSIS-RTOS v2 표준 규격에서는 소프트웨어 타이머를 다루기 위해 직관적인 구조의 두 가지 핵심 API를 제공합니다.
2.1 osTimerNew(): 타이머 객체 생성 및 콜백 등록
새로운 타이머 인스턴스를 메모리에 할당하고 만료 시 구동될 핸들러 함수를 매핑합니다.
osTimerId_t osTimerNew(osTimerFunc_t func, osTimerType_t type, void *argument, const osTimerAttr_t *attr);
- func: 타이머가 만료되는 순간 커널에 의해 강제로 호출될 콜백 함수의 주소 포인터입니다.
- type: 타이머의 동작 모드를 결정합니다. 단발성은 osTimerOnce, 지속 반복은 osTimerPeriodic 상수를 대입합니다.
- argument: 콜백 함수가 실행될 때 첫 번째 인자로 넘겨줄 데이터의 void 포인터 주소입니다. 활용할 데이터가 없다면 NULL을 입력합니다.
- attr: 정적 할당 정보나 디버깅용 타이머 이름을 지정하는 구조체 주소이며, 기본 커널 설정을 따를 때는 NULL을 넣습니다.
2.2 osTimerStart(): 타이머 카운팅 기동
생성된 타이머의 카운터를 활성화하고 만료 시간을 지정합니다.
osStatus_t osTimerStart(osTimerId_t timer_id, uint32_t ticks);
- timer_id: osTimerNew 함수를 통해 정상적으로 발급된 타이머 식별 핸들 ID입니다.
- ticks: 타이머가 만료될 주기를 시스템 틱 단위로 설정합니다. 만약 1ms당 1틱으로 설정된 시스템 환경이라면 500을 대입했을 때 정확히 500ms 주기로 콜백 함수가 실행됩니다.
3. 실전 활용 예제
소프트웨어 타이머를 활용하여 실제 임베디드 프로젝트에서 빈번하게 사용되는 두 가지 시나리오를 코드로 구현한 모습입니다.
예제 1: 500ms 주기 LED 점멸 제어
독립적인 태스크 스택을 단 1바이트도 낭비하지 않고 오직 타이머 콜백 구조만으로 깔끔하게 LED를 토글하는 코드입니다.
#include "cmsis_os2.h"
#include "stm32f4xx_hal.h" // 사용 칩셋에 맞는 HAL 적용
// 1. 타이머 만료 시 실행될 콜백 함수 구현
void LED_Callback(void *argument) {
// 하드웨어 GPIO 핀 토글 수행
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
}
int main(void) {
HAL_Init();
SystemClock_Config();
MX_GPIO_Init();
osKernelInitialize();
// 2. 500ms 간격으로 무한 반복되는 주기적 타이머 객체 생성
osTimerId_t led_timer = osTimerNew(LED_Callback, osTimerPeriodic, NULL, NULL);
// 타이머가 메모리에 정상 생성되었다면 기동 시작
if (led_timer != NULL) {
osTimerStart(led_timer, 500);
}
osKernelStart();
while (1) {
;
}
}
예제 2: 1초 주기 센서 데이터 업데이트
주기적인 폴링(Polling) 방식으로 센서 레지스터 값을 읽어 시스템 상태를 갱신해야 할 때 적합한 구조입니다.
// 센서 데이터 갱신 전용 콜백 함수
void Sensor_Callback(void *argument) {
// 센서 하드웨어 레지스터를 읽어오는 가상 함수 호출
float raw_data = Read_Sensor_Value();
// 글로벌 시스템 버퍼 및 필터링 알고리즘 데이터 업데이트
Update_System_Status(raw_data);
}
// 메인 함수 또는 초기화 전용 태스크 내 설정 영역
void System_Timer_Init(void) {
// 1000ms(1초) 주기로 동작하는 정기 폴링 타이머 생성 및 시작
osTimerId_t sensor_timer = osTimerNew(Sensor_Callback, osTimerPeriodic, NULL, NULL);
if (sensor_timer != NULL) {
osTimerStart(sensor_timer, 1000);
}
}
4. 개발을 위한 실무 팁
- 타이머 서비스 태스크의 우선순위 최적화: FreeRTOS의 소프트웨어 타이머 콜백들은 커널이 시작될 때 자동으로 배후에 생성되는 'Timer Service Task(또는 Tmr Svc 데몬 태스크)' 내부에서 순차적으로 실행됩니다. 이 데몬 태스크의 우선순위(configTIMER_TASK_PRIORITY)는 FreeRTOSConfig.h에서 설정할 수 있습니다. 시스템 내에서 주기적 타이머의 시간 정밀도가 매우 중요하다면, 이 타이머 서비스 태스크의 우선순위를 일반 어플리케이션 태스크들보다 가장 높게 설정해 두어야 제시간에 콜백이 밀리지 않고 수행됩니다.
- 타이머 명령 큐 크기 관리: osTimerStart나 osTimerStop 같은 API를 호출하면, 커널은 이 명령을 타이머 전용 큐(configTIMER_QUEUE_LENGTH)에 집어넣어 데몬 태스크가 처리하도록 만듭니다. 인터럽트나 고우선순위 태스크에서 타이머 제어 함수를 너무 짧은 찰나에 연속적으로 과도하게 호출하면 이 명령 큐가 가득 차서 osErrorResource 에러 코드가 반환될 수 있으므로, 큐 크기를 프로젝트 규모에 맞게 넉넉히 조정해 주는 편이 좋습니다.
5. 흔히 하는 실수
- 콜백 함수 내부에 지연(Delay) 함수 배치: 가장 치명적이면서 흔한 논리적 오류입니다. 소프트웨어 타이머 콜백 함수 내부에서 주기를 제어하겠다고 osDelay()나 대규모 for문 기반의 단순 Blocking 루프를 집어넣으면 안 됩니다. 모든 소프트웨어 타이머 콜백은 하나의 데몬 태스크 안에서 순서대로 줄을 서서 실행되므로, 한 콜백이 멈추면 뒤이어 대기 중인 다른 모든 소프트웨어 타이머들까지 전부 멈춰버려 시스템 시간 축이 완전히 꼬이게 됩니다.
- 콜백 함수 내부에서의 무거운 동기화 시도: 콜백 내부에서 특정 세마포어가 풀릴 때까지 기다리는 osSemaphoreAcquire(sem_id, osWaitForever) 문맥을 작성하거나, 다른 태스크와 무겁게 데이터를 주고받는 통신 로직을 구현하면 시스템 교착 상태(Deadlock)를 유발할 확률이 급격히 높아집니다. 타이머 콜백은 하드웨어 ISR(인터럽트)과 유사하게 "최대한 짧고 빠르게" 플래그 변수만 토글하거나 데이터를 큐에 바로 던진 후 즉시 함수를 빠져나오도록 얇게 설계해야 합니다.
6. 결론
CMSIS-RTOS v2 기반의 소프트웨어 타이머 표준 인터페이스는 가벼운 주기적 모니터링 작업이나 상태 머신(State Machine)의 타임아웃 예외 처리를 구현할 때 코드의 복잡성을 낮추고, 한정된 MCU의 RAM 자원을 방어할 수 있는 훌륭한 엔지니어링 솔루션입니다.
단순히 시간이 찰 때마다 동작을 수행한다고 해서 무조건 무거운 태스크를 남발하기보다, 오늘 정리해 드린 데몬 태스크 기반의 실행 제약을 명확히 인지하고 가벼운 작업들은 osTimerNew 구조로 이관해 보시기 바랍니다. 소스코드의 결합도가 낮아져 이식성이 향상되는 것은 물론, 칩셋 마이그레이션 시 최적화 공수가 크게 절감되는 효과를 체감하실 수 있습니다. 구현 도중 타이머 정밀도가 어긋나거나 우선순위 충돌로 콜백 딜레이 현상을 겪고 계신다면 언제든 댓글로 질문을 공유해 주세요.
'Firmware & RTOS > FreeRTOS & Real-time Scheduling' 카테고리의 다른 글
| FreeRTOS 이벤트 그룹(Event Group) 완벽 가이드: 다중 태스크 동기화 (CMSIS-RTOS v2) (0) | 2025.01.17 |
|---|---|
| FreeRTOS 저전력 최적화 가이드: Idle Task와 Tickless Idle 모드 활용법 (0) | 2025.01.16 |
| FreeRTOS 세마포어(Semaphore) vs 뮤텍스(Mutex) 차이점과 올바른 사용법 (0) | 2025.01.14 |
| FreeRTOS 메시지 큐(Queue) 완벽 가이드: 태스크 간 데이터 통신 및 예제 (CMSIS-RTOS v2) (0) | 2025.01.13 |
| CMSIS-RTOS v2 태스크 관리 완벽 가이드: 생성부터 상태 전환까지 (0) | 2025.01.12 |