FreeRTOS 소프트웨어 타이머 완벽 가이드: 생성, 제어 및 활용

1. 소프트웨어 타이머의 개요

소프트웨어 타이머는 운영체제에서 제공하는 시간 기반 알림 메커니즘입니다. 실제 알람 시계처럼 특정 시간이 경과하거나 주기적으로 특정 함수(콜백 함수)를 실행하도록 설정할 수 있습니다. FreeRTOS에서는 시스템 틱(Tick) 인터럽트를 기반으로 여러 개의 소프트웨어 타이머를 생성하고 관리할 수 있습니다.

소프트웨어 타이머로 구현 가능한 주요 기능은 다음과 같습니다.

  • 단일 실행(One-shot): 설정된 시간이 지나면 콜백 함수를 한 번만 실행하고 타이머가 정지됩니다.
  • 주기 실행(Auto-reload): 설정된 시간마다 콜백 함수를 반복해서 실행합니다.

2. 소프트웨어 타이머의 주요 특성

2.1 타이머 상태

  • 실행 중(Running/Active): 타이머가 활성화되어 설정된 시간이 도달하면 콜백 함수가 호출됩니다.
  • 휴면(Dormant): 타이머가 비활성화되어 콜백 함수가 호출되지 않습니다. 타이머 핸들을 통해 객체 자체에 접근하는 것은 가능합니다.

2.2 데몬 태스크 (Daemon Task)

FreeRTOS에서는 타이머의 콜백 함수를 인터럽트 서비스 루틴(ISR) 내에서 직접 실행하지 않습니다. 대신, RTOS 데몬 태스크라는 특별한 시스템 태스크에서 모든 타이머 관련 작업을 처리합니다. 이는 인터럽트 컨텍스트에서 블로킹(Blocking) 동작이나 시간이 오래 걸리는 작업을 수행하는 것을 방지하기 위함입니다.

configUSE_TIMERS 매크로를 1로 설정하면 스케줄러가 시작될 때 자동으로 데몬 태스크가 생성됩니다. 사용자 태스크는 타이머 명령 큐를 통해 데몬 태스크와 통신합니다.

타이머 명령 큐 구조

데몬 태스크의 주요 역할은 다음과 같습니다.

  1. 명령 처리: 명령 큐에서 명령을 꺼내어 타이머 생성, 시작, 중지 등 요청을 수행합니다.
  2. 콜백 실행: 시간이 만료된 타이머의 콜백 함수를 호출합니다.

데몬 태스크의 우선순위는 configTIMER_TASK_PRIORITY로 설정하며, 일반적으로 높은 우선순위를 할당하여 타이머 명령과 콜백이 지연 없이 처리되도록 하는 것이 좋습니다.

2.3 콜백 함수 작성 시 주의사항

콜백 함수는 데몬 태스크 컨텍스트에서 실행됩니다. 따라서 다른 타이머나 시스템 동작에 영향을 주지 않도록 다음 규칙을 준수해야 합니다.

  • 함수는 가능한 짧고 빠르게 실행되어야 합니다.
  • vTaskDelay() 등 태스크를 블로킹 상태로 만드는 API를 호출해서는 안 됩니다.
  • xQueueReceive() 같은 함수를 호출할 경우 타임아웃 값을 0으로 설정하여 블로킹되지 않도록 해야 합니다.

3. 핵심 API 함수

3.1 타이머 생성

// 동적 메모리 할당 방식
TimerHandle_t xTimerCreate(
    const char * const pcTimerName,
    const TickType_t xTimerPeriodInTicks,
    const UBaseType_t uxAutoReload,
    void * const pvTimerID,
    TimerCallbackFunction_t pxCallbackFunction
);

// 정적 메모리 할당 방식
TimerHandle_t xTimerCreateStatic(
    const char * const pcTimerName,
    TickType_t xTimerPeriodInTicks,
    UBaseType_t uxAutoReload,
    void * pvTimerID,
    TimerCallbackFunction_t pxCallbackFunction,
    StaticTimer_t *pxTimerBuffer
);
  • pcTimerName: 디버깅 용도의 타이머 이름 (선택 사항)
  • xTimerPeriodInTicks: 타이머 주기 (틱 단위)
  • uxAutoReload: pdTRUE는 자동 재시작, pdFALSE는 단일 실행
  • pvTimerID: 콜백 함수에서 사용할 사용자 정의 ID
  • 반환값: 성공 시 TimerHandle_t, 실패 시 NULL

콜백 함수의 시그니처는 다음과 같습니다.

typedef void (* TimerCallbackFunction_t)( TimerHandle_t xTimer );

3.2 타이머 삭제

BaseType_t xTimerDelete( TimerHandle_t xTimer, TickType_t xTicksToWait );

동적으로 할당된 타이머를 메모리에서 해제합니다. xTicksToWait 매개변수는 삭제 명령이 명령 큐에 성공적으로 쓰일 때까지 기다리는 최대 시간입니다.

3.3 타이머 시작 및 중지

// 시작 (태스크 컨텍스트)
BaseType_t xTimerStart( TimerHandle_t xTimer, TickType_t xTicksToWait );

// 시작 (ISR 컨텍스트)
BaseType_t xTimerStartFromISR( TimerHandle_t xTimer,
                               BaseType_t *pxHigherPriorityTaskWoken );

// 중지 (태스크 컨텍스트)
BaseType_t xTimerStop( TimerHandle_t xTimer, TickType_t xTicksToWait );

// 중지 (ISR 컨텍스트)
BaseType_t xTimerStopFromISR( TimerHandle_t xTimer,
                              BaseType_t *pxHigherPriorityTaskWoken );
  • 시작: 타이머를 휴면 상태에서 실행 상태로 전환합니다. xTimerStart()가 호출된 시점부터 주기가 계산됩니다.
  • 중지: 타이머를 실행 상태에서 휴면 상태로 전환합니다.
  • ISR 버전: pxHigherPriorityTaskWoken 인자를 사용하여 문맥 전환이 필요한지 여부를 알려줍니다.

3.4 타이머 리셋

BaseType_t xTimerReset( TimerHandle_t xTimer, TickType_t xTicksToWait );
BaseType_t xTimerResetFromISR( TimerHandle_t xTimer,
                               BaseType_t *pxHigherPriorityTaskWoken );

타이머의 시작 시간을 현재 시점으로 재설정합니다. 이미 실행 중인 타이머에 대해 호출하면, 새로운 시작 시간을 기준으로 주기가 다시 계산됩니다.

3.5 주기 변경

BaseType_t xTimerChangePeriod( TimerHandle_t xTimer,
                               TickType_t xNewPeriod,
                               TickType_t xTicksToWait );
BaseType_t xTimerChangePeriodFromISR( TimerHandle_t xTimer,
                                      TickType_t xNewPeriod,
                                      BaseType_t *pxHigherPriorityTaskWoken );

타이머의 주기를 변경하고 동시에 타이머를 실행 상태로 만듭니다. 새 주기는 함수가 호출된 시점부터 적용됩니다.

3.6 타이머 ID 관리

// ID 조회
void *pvTimerGetTimerID( TimerHandle_t xTimer );

// ID 설정
void vTimerSetTimerID( TimerHandle_t xTimer, void *pvNewID );

타이머 ID는 사용자 정의 데이터를 저장하거나 타이머를 식별하는 용도로 사용됩니다. 이 함수들은 명령 큐를 거치지 않고 직접 타이머 구조체에 접근하므로 블로킹되지 않습니다.

4. 실전 예제 1: 기본 타이머 사용

설정

/* FreeRTOSConfig.h */
#define configUSE_TIMERS             1
#define configTIMER_TASK_PRIORITY    31
#define configTIMER_QUEUE_LENGTH     5
#define configTIMER_TASK_STACK_DEPTH 32

코드 예제

#include "timers.h"

static volatile uint8_t flagOneShot = 0;
static volatile uint8_t flagAutoReload = 0;

static void oneShotCallback(TimerHandle_t xTimer);
static void autoReloadCallback(TimerHandle_t xTimer);

#define ONE_SHOT_PERIOD_MS     10
#define AUTO_RELOAD_PERIOD_MS  20

int main(void)
{
    TimerHandle_t oneShotTimer;
    TimerHandle_t autoReloadTimer;

    // 단일 실행 타이머 생성
    oneShotTimer = xTimerCreate(
        "OneShot",
        pdMS_TO_TICKS(ONE_SHOT_PERIOD_MS),
        pdFALSE,  // 단일 실행
        0,
        oneShotCallback
    );

    // 자동 재시작 타이머 생성
    autoReloadTimer = xTimerCreate(
        "AutoReload",
        pdMS_TO_TICKS(AUTO_RELOAD_PERIOD_MS),
        pdTRUE,   // 자동 재시작
        0,
        autoReloadCallback
    );

    if (oneShotTimer != NULL && autoReloadTimer != NULL)
    {
        xTimerStart(oneShotTimer, 0);
        xTimerStart(autoReloadTimer, 0);
        vTaskStartScheduler();
    }

    // 메모리 부족 등으로 인한 오류 처리
    return 0;
}

static void oneShotCallback(TimerHandle_t xTimer)
{
    static int count = 0;
    flagOneShot = !flagOneShot;
    printf("단일 타이머 실행: %d\n", count++);
}

static void autoReloadCallback(TimerHandle_t xTimer)
{
    static int count = 0;
    flagAutoReload = !flagAutoReload;
    printf("주기 타이머 실행: %d\n", count++);
}

5. 실전 예제 2: 채터링(Chattering) 제거 (Debouncing)

기계식 스위치의 접점 바운싱(채터링) 현상으로 인해 짧은 시간 동안 여러 번의 인터럽트가 발생하는 문제를 해결하는 방법입니다.

동작 원리: 인터럽트가 발생할 때마다 타이머를 리셋합니다. 설정한 시간(예: 20ms) 동안 추가 인터럽트가 없으면 스위치가 안정화된 것으로 간주하고 실제 키 값을 읽습니다.

채터링 제거 동작 방식

static TimerHandle_t debounceTimer;
static const TickType_t DEBOUNCE_PERIOD_MS = 20;

static void debounceCallback(TimerHandle_t xTimer);

int main(void)
{
    // 단일 실행 타이머 생성
    debounceTimer = xTimerCreate(
        "Debounce",
        pdMS_TO_TICKS(DEBOUNCE_PERIOD_MS),
        pdFALSE,   // 단일 실행
        0,
        debounceCallback
    );

    // 스위치 채터링을 시뮬레이션하는 태스크
    xTaskCreate(emulateKeyTask, "EmulateKey", 1000, NULL, 1, NULL);

    vTaskStartScheduler();
    return 0;
}

void emulateKeyTask(void *pvParameters)
{
    int count = 0;
    const TickType_t delayMs = pdMS_TO_TICKS(200);

    for (;;)
    {
        // 채터링 시뮬레이션: 여러 번 연속 리셋 호출
        xTimerReset(debounceTimer, 0);
        xTimerReset(debounceTimer, 0);
        xTimerReset(debounceTimer, 0);
        count += 3;

        printf("키 채터링 발생 횟수: %d\n", count);
        vTaskDelay(delayMs);
    }
}

static void debounceCallback(TimerHandle_t xTimer)
{
    static int callCount = 0;
    // 실제 키 값을 읽는 로직 구현
    printf("디바운스 완료, 콜백 호출: %d\n", callCount++);
}

위 예제에서는 의도적으로 xTimerReset을 여러 번 호출했지만, 설정된 디바운스 시간(20ms) 동안 추가 리셋이 발생하면 타이머가 계속 재설정되므로 콜백은 한 번만 호출됩니다.

태그: freertos 소프트웨어 타이머 RTOS 데몬 태스크 타이머 명령 큐

7월 17일 03:01에 게시됨