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 知识体系图
下面这张图,是我自己总结的定时器模式切换逻辑,画出来方便大家理解:
从图中可以看到,核心就是创建时决定初始模式,运行时通过修改定时器ID配合xTimerReset实现模式切换。这个方案我用了好几年,在几十个项目里验证过,稳定可靠。
好了,关于单次和周期定时器的配置与切换,就聊到这里。记住:定时器虽小,用好了能省不少事;用不好,也能让你排查到怀疑人生。下次遇到定时器相关的问题,不妨先想想:我到底需要单次还是周期?切换时有没有更优雅的方式?