跳转至

软件定时器

软件定时器让你可以在指定时间后执行一个回调函数,而不需要占用硬件定时器资源。FreeRTOS 的软件定时器由专门的守护任务(Timer Service Task)管理,适合实现超时检测、周期性操作等场景。


软件定时器 vs 硬件定时器

特性 硬件定时器(TIM) 软件定时器
精度 极高(微秒级) 依赖 tick 精度(默认 1ms)
数量 有限(取决于芯片) 几乎无限(受内存限制)
资源占用 占用硬件外设 仅占用少量内存
执行上下文 在中断中执行 在定时器守护任务中执行
适用场景 PWM、精确计时、编码器 超时、周期性检查、延迟操作

什么时候用软件定时器?

  • 不需要微秒级精度的定时操作
  • 硬件定时器资源不够用
  • 需要大量不同周期的定时器
  • 需要动态创建/销毁定时器

基本概念

定时器类型

类型 行为 适用场景
单次定时器(One-shot) 到期执行一次回调,然后自动停止 超时检测、延迟执行
周期定时器(Periodic) 到期执行回调后自动重置,周而复始 心跳包、LED 闪烁、定时巡检
gantt
    title 定时器类型对比
    dateFormat X
    axisFormat %s

    section 单次定时器
    等待    :a1, 0, 100
    回调执行 :crit, a2, 100, 105
    停止    :a3, 105, 200

    section 周期定时器
    等待    :b1, 0, 100
    回调执行 :crit, b2, 100, 105
    等待    :b3, 105, 205
    回调执行 :crit, b4, 205, 210
    等待    :b5, 210, 310
    回调执行 :crit, b6, 310, 315

定时器守护任务

所有软件定时器的回调都在同一个任务(Timer Service Task / Daemon Task)中执行:

graph TD
    T1[定时器1] -->|到期| D[定时器守护任务<br>Timer Daemon]
    T2[定时器2] -->|到期| D
    T3[定时器3] -->|到期| D
    D -->|执行| CB1[回调函数1]
    D -->|执行| CB2[回调函数2]
    D -->|执行| CB3[回调函数3]

回调函数中的限制

因为所有定时器回调都在同一个任务中串行执行:

  • 不能调用阻塞 API(如 osDelay()osMessageQueueGet(osWaitForever)
  • 回调应尽量短,否则会影响其他定时器的精度
  • 如果需要长时间处理,应在回调中释放信号量,由专门的任务去处理

CubeMX 中配置软件定时器

配置步骤

  1. Middleware → FREERTOS → Timers and Semaphores 标签页
  2. 在 Timers 区域点击 Add
  3. 配置参数:
参数 说明 示例
Timer Name 定时器名称 HeartbeatTimer
Callback 回调函数名 HeartbeatCallback
Type 单次/周期 osTimerPeriodic

CubeMX 配置定时器守护任务参数

Config parameters 中:

参数 说明 建议值
USE_TIMERS 启用软件定时器 1(启用)
TIMER_TASK_PRIORITY 守护任务优先级 2(默认,高于 Idle)
TIMER_TASK_STACK_DEPTH 守护任务栈大小 256(默认,按需调整)
TIMER_QUEUE_LENGTH 定时器命令队列长度 10(默认)

生成的代码

/* 自动生成的定时器定义 */
osTimerId_t HeartbeatTimerHandle;

const osTimerAttr_t HeartbeatTimer_attributes = {
    .name = "HeartbeatTimer"
};

void MX_FREERTOS_Init(void)
{
    /* 创建周期定时器 */
    HeartbeatTimerHandle = osTimerNew(HeartbeatCallback,
                                      osTimerPeriodic,
                                      NULL,
                                      &HeartbeatTimer_attributes);
}

/* 回调函数(需要在 USER CODE 区域实现) */
void HeartbeatCallback(void *argument)
{
    /* USER CODE BEGIN HeartbeatCallback */
    // 定时器到期时执行的代码
    /* USER CODE END HeartbeatCallback */
}

软件定时器 API

创建定时器

osTimerId_t osTimerNew(
    osTimerFunc_t func,         // 回调函数
    osTimerType_t type,         // osTimerOnce 或 osTimerPeriodic
    void *argument,             // 传给回调函数的参数
    const osTimerAttr_t *attr   // 属性(名称等)
);

启动 / 停止 / 重启

// 启动定时器(设置周期 1000ms)
osTimerStart(HeartbeatTimerHandle, 1000);

// 停止定时器
osTimerStop(HeartbeatTimerHandle);

// 重启定时器(重新计时,周期不变)
// 用 osTimerStart 再次调用即可重置计时
osTimerStart(HeartbeatTimerHandle, 1000);

// 检查定时器是否在运行
uint32_t running = osTimerIsRunning(HeartbeatTimerHandle);

// 删除定时器(释放资源)
osTimerDelete(HeartbeatTimerHandle);

定时器创建后不会自动启动

osTimerNew() 只是创建定时器,必须调用 osTimerStart() 才开始计时。


实战案例

案例 1:LED 心跳灯

最常见的用法——用周期定时器实现 LED 闪烁:

/* 回调函数:每次到期翻转 LED */
void HeartbeatCallback(void *argument)
{
    HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
}

/* 在某个任务中启动定时器 */
void System_Task(void *argument)
{
    /* 启动心跳定时器,500ms 周期 */
    osTimerStart(HeartbeatTimerHandle, 500);

    for (;;)
    {
        // 做其他事情,LED 自动在后台闪烁
        osDelay(1000);
    }
}

心跳灯的意义

嵌入式开发中,心跳灯是最简单的"系统还活着"指示器。如果 LED 停止闪烁,说明系统可能死机了。

案例 2:通信超时检测

使用单次定时器实现串口通信的超时机制:

osTimerId_t TimeoutTimerHandle;
volatile uint8_t timeout_flag = 0;

/* 超时回调 */
void TimeoutCallback(void *argument)
{
    timeout_flag = 1;  // 标记超时
}

/* 创建单次定时器 */
void Init_Timeout(void)
{
    const osTimerAttr_t attr = { .name = "TimeoutTimer" };
    TimeoutTimerHandle = osTimerNew(TimeoutCallback, osTimerOnce,
                                    NULL, &attr);
}

/* 通信任务 */
void Comm_Task(void *argument)
{
    for (;;)
    {
        /* 发送请求 */
        HAL_UART_Transmit(&huart1, tx_data, sizeof(tx_data), 100);

        /* 启动 500ms 超时定时器 */
        timeout_flag = 0;
        osTimerStart(TimeoutTimerHandle, 500);

        /* 等待回复 */
        while (!rx_complete && !timeout_flag)
        {
            osDelay(1);
        }

        if (timeout_flag)
        {
            /* 超时处理:重发或报错 */
            printf("通信超时!\r\n");
        }
        else
        {
            /* 收到回复,停止定时器 */
            osTimerStop(TimeoutTimerHandle);
            process_response();
        }

        osDelay(1000);
    }
}

案例 3:按键消抖

使用单次定时器实现按键消抖,避免在任务中使用 osDelay 消抖造成的延迟:

osTimerId_t DebounceTimerHandle;
volatile uint8_t button_confirmed = 0;

/* 消抖定时器回调(30ms 后确认按键状态) */
void DebounceCallback(void *argument)
{
    /* 30ms后再次读取按键状态 */
    if (HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0) == GPIO_PIN_RESET)
    {
        button_confirmed = 1;  // 确认按下
    }
}

/* 外部中断回调 */
void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    if (GPIO_Pin == GPIO_PIN_0)
    {
        /* 按键触发中断,启动消抖定时器 */
        osTimerStart(DebounceTimerHandle, 30);
    }
}

/* 按键处理任务 */
void Button_Task(void *argument)
{
    for (;;)
    {
        if (button_confirmed)
        {
            button_confirmed = 0;
            /* 执行按键功能 */
            HAL_GPIO_TogglePin(GPIOC, GPIO_PIN_13);
        }
        osDelay(10);
    }
}

案例 4:多定时器管理

同时使用多个定时器实现不同周期的操作:

osTimerId_t Timer_1s_Handle;
osTimerId_t Timer_5s_Handle;
osTimerId_t Timer_30s_Handle;

/* 1 秒定时器:更新显示 */
void Timer_1s_Callback(void *argument)
{
    update_display();
}

/* 5 秒定时器:保存数据 */
void Timer_5s_Callback(void *argument)
{
    save_data_to_flash();
}

/* 30 秒定时器:发送心跳包 */
void Timer_30s_Callback(void *argument)
{
    send_heartbeat();
}

/* 初始化并启动所有定时器 */
void Start_All_Timers(void)
{
    const osTimerAttr_t attr1 = { .name = "Timer1s" };
    const osTimerAttr_t attr2 = { .name = "Timer5s" };
    const osTimerAttr_t attr3 = { .name = "Timer30s" };

    Timer_1s_Handle = osTimerNew(Timer_1s_Callback,
                                  osTimerPeriodic, NULL, &attr1);
    Timer_5s_Handle = osTimerNew(Timer_5s_Callback,
                                  osTimerPeriodic, NULL, &attr2);
    Timer_30s_Handle = osTimerNew(Timer_30s_Callback,
                                   osTimerPeriodic, NULL, &attr3);

    osTimerStart(Timer_1s_Handle, 1000);
    osTimerStart(Timer_5s_Handle, 5000);
    osTimerStart(Timer_30s_Handle, 30000);
}

定时器回调中的注意事项

不能做的事

void BadCallback(void *argument)
{
    osDelay(100);                    // ❌ 不能阻塞!
    osSemaphoreAcquire(sem, 1000);   // ❌ 不能长时间等待!
    osMessageQueueGet(q, &d, NULL,
                      osWaitForever); // ❌ 不能无限等待!

    heavy_computation();             // ⚠️ 避免耗时操作
}

正确做法:回调通知,任务处理

osSemaphoreId_t ProcessSemHandle;

/* 定时器回调:仅释放信号量 */
void TimerCallback(void *argument)
{
    osSemaphoreRelease(ProcessSemHandle);  // ✅ 快速,非阻塞
}

/* 专门的处理任务 */
void Process_Task(void *argument)
{
    for (;;)
    {
        if (osSemaphoreAcquire(ProcessSemHandle, osWaitForever) == osOK)
        {
            // ✅ 在任务中做复杂处理
            heavy_computation();
            HAL_UART_Transmit(&huart1, data, len, 1000);
        }
    }
}

常见问题

定时器精度不够怎么办?

软件定时器精度取决于 TICK_RATE_HZ(默认 1000 = 1ms)。如果需要更高精度:

  1. 增大 TICK_RATE_HZ(如 10000 = 0.1ms),但会增加上下文切换开销
  2. 使用硬件定时器(TIM)进行精确计时
  3. 如果定时器回调中有耗时操作,会影响后续定时器的触发时间

定时器太多会影响性能吗?

  • 创建大量定时器本身不会影响性能(不运行时不占 CPU)
  • 但如果大量定时器同时到期且回调执行时间较长,守护任务会排队处理
  • 建议:短回调 + 信号量通知模式

定时器回调和任务回调有什么区别?

定时器不是独立任务。所有定时器回调都在同一个守护任务中执行。如果你需要每个定时操作独立运行、互不影响,应该创建独立的任务并使用 osDelayosDelayUntil