4、单次定时器与周期定时器:一次性定时器配置、自动重载定时器配置、两种模式的切换技巧

大家好,我是你们的老朋友。今天我们来聊聊FreeRTOS软件定时器里最基础、也最容易被忽视的两个概念——单次定时器和周期定时器。

说实话,我刚开始用FreeRTOS那会儿,觉得这俩玩意儿不就是个定时器嘛,有啥好研究的?直到有一次,我在一个数据采集项目里,因为把周期定时器当单次用,导致系统内存泄漏,排查了整整两天……嗯,从那以后,我再也不敢小看这个“简单”的配置了。

4.1 一次性定时器:用完即焚

一次性定时器,说白了就是“一锤子买卖”。你启动它,它跑完设定的时间,执行一次回调函数,然后就自动删除自己。就像你设置了一个闹钟,响一次就再也不响了。

配置方法:

创建定时器时,将uxAutoReload参数设置为pdFALSE,这就是单次模式。

TimerHandle_t xOneShotTimer;
xOneShotTimer = xTimerCreate(
    "OneShot",          // 定时器名字,调试用
    pdMS_TO_TICKS(1000), // 定时周期:1000ms
    pdFALSE,            // uxAutoReload = pdFALSE,单次模式
    (void *)0,          // 定时器ID,可以传指针
    vTimerCallback      // 回调函数
);

我个人习惯给单次定时器起名时加个“OS”前缀,比如OS_LED_Blink,这样一看就知道是单次的。当然,这只是个人习惯,不强求。

典型应用场景:

  • 延时执行:比如按键消抖后,延迟100ms再读取按键状态
  • 超时检测:等待某个外设响应,超时了就报错
  • 一次性任务:系统启动后,延迟5秒执行一次初始化检查
小技巧:单次定时器执行完回调后,定时器句柄仍然有效,但定时器状态变为“已删除”。如果你想再次使用它,必须重新调用xTimerCreate()创建。不过,更常见的做法是调用xTimerReset()来重新启动它——嗯,这其实就有点“伪周期”的意思了,后面会讲。

4.2 自动重载定时器:永动机模式

自动重载定时器,就是“周而复始”的模式。每次定时时间到,执行回调,然后自动重新开始计时。就像心跳一样,一直跳下去,直到你主动停止它。

配置方法:

创建时把uxAutoReload设为pdTRUE即可。

TimerHandle_t xPeriodicTimer;
xPeriodicTimer = xTimerCreate(
    "Periodic",         // 名字
    pdMS_TO_TICKS(500), // 500ms周期
    pdTRUE,             // uxAutoReload = pdTRUE,周期模式
    (void *)1,          // ID设为1,区分不同定时器
    vTimerCallback
);

我在项目中遇到过一个问题:周期定时器的回调函数里如果执行了耗时操作(比如打印日志),会导致定时器“丢拍”。因为FreeRTOS的软件定时器是在一个守护任务里执行的,回调函数必须尽快返回。你想想看,如果回调执行了200ms,而定时周期只有100ms,那后面的定时事件就会被阻塞,甚至丢失。

避坑指南:我曾经在一个电机控制项目里,把周期定时器的回调函数里放了一个printf(),结果电机转速控制完全乱套。后来才发现,printf()太慢了,占用了大量定时器任务时间。记住:回调函数里不要做阻塞操作,不要做耗时计算,不要调用可能阻塞的API。

4.3 两种模式的切换技巧

实际项目中,我们经常需要在单次和周期模式之间切换。比如:

  • 系统启动时,先单次延时等待外设就绪
  • 外设就绪后,切换为周期模式定期采集数据
  • 出现异常时,再切回单次模式做超时保护

FreeRTOS并没有提供直接切换模式的API。但我们可以用一个小技巧来实现:删除重建

方法一:删除重建(简单粗暴)

// 假设当前是周期模式,想切为单次
xTimerDelete(xMyTimer, 0);  // 删除旧定时器
xMyTimer = xTimerCreate(
    "MyTimer",
    pdMS_TO_TICKS(1000),
    pdFALSE,  // 改为单次
    NULL,
    vCallback
);
xTimerStart(xMyTimer, 0);

这个方法虽然简单,但有个缺点:删除和创建会消耗堆内存,频繁操作可能导致内存碎片。我一般只在初始化阶段用这个方法,运行时尽量少用。

方法二:利用xTimerReset实现“伪切换”

这个方法更优雅:保持定时器为单次模式,但在回调函数里判断是否需要“续命”。

void vTimerCallback(TimerHandle_t xTimer) {
    // 获取定时器ID,判断当前模式
    uint32_t ulMode = (uint32_t)pvTimerGetTimerID(xTimer);
    
    if (ulMode == 0) {
        // 单次模式:执行一次就结束
        printf("One-shot executed\n");
    } else if (ulMode == 1) {
        // 周期模式:执行后重新启动自己
        printf("Periodic tick\n");
        xTimerReset(xTimer, 0);  // 重新启动,实现周期效果
    }
}

// 切换模式:只需修改定时器ID
void SwitchToPeriodic(TimerHandle_t xTimer) {
    vTimerSetTimerID(xTimer, (void *)1);  // 改为周期模式
    xTimerReset(xTimer, 0);               // 重新启动
}

void SwitchToOneShot(TimerHandle_t xTimer) {
    vTimerSetTimerID(xTimer, (void *)0);  // 改为单次模式
    xTimerReset(xTimer, 0);               // 重新启动
}

这个方法的好处是:不需要删除和重建定时器,避免了内存操作。我个人的项目里,90%的场景都用这个方案。

核心要点:
  • 单次定时器:uxAutoReload = pdFALSE,执行一次即销毁
  • 周期定时器:uxAutoReload = pdTRUE,自动重复执行
  • 模式切换:推荐用xTimerReset + 修改定时器ID的方式,避免频繁创建/删除
  • 回调函数必须短小精悍,不要阻塞

4.4 知识体系图

下面这张图,是我自己总结的定时器模式切换逻辑,画出来方便大家理解:

软件定时器模式切换流程 创建定时器 uxAutoReload? pdFALSE 单次模式 执行一次结束 pdTRUE 周期模式 自动重载 切换:修改ID + xTimerReset 避免删除重建,减少内存碎片

从图中可以看到,核心就是创建时决定初始模式,运行时通过修改定时器ID配合xTimerReset实现模式切换。这个方案我用了好几年,在几十个项目里验证过,稳定可靠。

好了,关于单次和周期定时器的配置与切换,就聊到这里。记住:定时器虽小,用好了能省不少事;用不好,也能让你排查到怀疑人生。下次遇到定时器相关的问题,不妨先想想:我到底需要单次还是周期?切换时有没有更优雅的方式?


专注资料整理