2、定时器核心API详解:xTimerCreate、xTimerStart、xTimerStop、xTimerReset、xTimerChangePeriod

好,咱们直接进入正题。软件定时器这块,说白了就是FreeRTOS给你的一把「软刀子」——它不像硬件定时器那么硬核,但用好了,很多周期性的活儿都能交给它。我个人习惯把定时器API分成两组:创建/销毁控制/查询。今天咱们重点啃后面这五个核心API。

先看一张图,把这五个API的关系理清楚:

软件定时器核心API调用关系 xTimerCreate 定时器状态:休眠/运行 xTimerStart xTimerStop xTimerReset xTimerChangePeriod 虚线箭头:xTimerReset 内部会调用 xTimerChangePeriod 所有控制API都通过定时器命令队列发送消息

2.1 xTimerCreate —— 定时器的「出生证明」

这个函数我用的最多。你想想看,一个定时器从无到有,总得给它办个「身份证」吧?xTimerCreate干的就是这事。

TimerHandle_t xTimerCreate(
    const char * const pcTimerName,      // 定时器名字,调试用
    TickType_t xTimerPeriodInTicks,      // 周期,单位是tick
    UBaseType_t uxAutoReload,            // pdTRUE=自动重载,pdFALSE=单次
    void * pvTimerID,                    // 定时器ID,可以传结构体指针
    TimerCallbackFunction_t pxCallback   // 回调函数
);

这里有几个坑,我一个个说。

第一个坑:周期单位。很多人上来就写1000,以为是一秒。错了!xTimerPeriodInTicks的单位是tick,不是毫秒。你得用pdMS_TO_TICKS(1000)来转换。我记得刚用FreeRTOS那会儿,就因为这个bug,一个LED闪烁程序调了一下午——灯闪得跟抽风似的。

第二个坑:回调函数里别干重活。定时器回调是在daemon task(守护任务)的上下文里执行的。你要是敢在里面放个vTaskDelay(1000)或者死循环,整个系统的定时器都得瘫痪。我见过一个同事在回调里调了printf,结果串口打印太慢,直接把其他定时器饿死了。

我的小技巧:回调函数里只做轻量级操作,比如设置一个标志位、发一个信号量。真正耗时的处理,交给其他任务去做。

2.2 xTimerStart —— 让定时器「跑起来」

定时器创建完是休眠状态,你得给它一脚油门——xTimerStart

BaseType_t xTimerStart(TimerHandle_t xTimer, TickType_t xBlockTime);

xBlockTime这个参数很有意思。它表示:如果定时器命令队列满了,你愿意等多久?我个人习惯传0,因为定时器启动失败通常意味着系统负载太高,与其阻塞,不如赶紧报错。

嗯,这里要注意:xTimerStart不是立即生效的。它只是往定时器命令队列里发了一条消息,真正执行是在daemon task下一次处理队列的时候。所以别指望调用完立马就能看到效果。

我曾经踩过的坑:在中断服务函数里调xTimerStart?不行!你得用xTimerStartFromISR版本。否则系统直接断言失败。这个错误我犯过两次,第二次还是在同一个项目里……说出来都是泪。

2.3 xTimerStop —— 踩刹车

这个API简单,就是让正在跑的定时器停下来。

BaseType_t xTimerStop(TimerHandle_t xTimer, TickType_t xBlockTime);

你可能会问:停下来的定时器还能再启动吗?当然可以。调用xTimerStart就行,它会从当前时间开始重新计时。

我有个习惯:在系统进入低功耗模式之前,把所有不用的定时器都停掉。这样能省点电,虽然不多,但积少成多嘛。

2.4 xTimerReset —— 重新「上发条」

这个API我特别喜欢。它的作用是把定时器重置到初始状态,重新开始计时。

BaseType_t xTimerReset(TimerHandle_t xTimer, TickType_t xBlockTime);

说白了,就是让定时器「满血复活」。比如你做了一个看门狗定时器,每次收到心跳包就调一下xTimerReset,这样定时器永远不会超时。一旦心跳断了,定时器到期,回调函数里执行复位操作。

这里有个细节:xTimerReset内部其实调用了xTimerChangePeriod,把周期设回原来的值。所以它的开销比xTimerStart稍微大一点点。不过对于大多数应用来说,这点差别可以忽略不计。

2.5 xTimerChangePeriod —— 动态改周期

这个API允许你在定时器运行过程中修改它的周期。

BaseType_t xTimerChangePeriod(
    TimerHandle_t xTimer,
    TickType_t xNewPeriod,
    TickType_t xBlockTime
);

你想想看,有些场景下定时器的周期需要动态调整。比如一个温度采集程序,温度变化快的时候每秒采一次,温度稳定了每10秒采一次。用xTimerChangePeriod就能优雅地实现。

调用这个API时,定时器会立即用新周期重新开始计时。如果定时器当时是停止状态,它会被自动启动。这个行为有时候会让人意外,我建议你在调用前先确认一下定时器的状态。

API 作用 能否在ISR中调用 是否改变周期
xTimerStart 启动定时器 否(用FromISR版本)
xTimerStop 停止定时器 否(用FromISR版本)
xTimerReset 重置定时器 否(用FromISR版本)
xTimerChangePeriod 修改周期并重启 否(用FromISR版本)
核心要点:这五个API都是通过定时器命令队列与daemon task通信的。所以它们都不是实时响应的,但保证了线程安全。如果你需要精确到几个tick以内的定时控制,建议用硬件定时器。

最后说一句:这些API的返回值一定要检查。pdPASS表示成功,pdFAIL表示命令队列满了或者参数错误。我见过太多人忽略返回值,结果定时器没启动也不知道,整个系统行为异常还找不到原因。

好了,这五个API就讲到这里。下一节咱们聊聊定时器的高级用法——怎么用软件定时器实现超时检测、心跳机制和软件PWM。这些东西在实际项目中非常实用。


专注资料整理