9、状态机与定时器:超时事件处理、定时器状态机、看门狗状态机
嵌入式系统里,时间是个绕不开的话题。我做了这么多年嵌入式开发,发现很多bug其实不是逻辑错了,而是时间没处理好。比如一个串口接收,你等数据等多久?等不到怎么办?设备死机了怎么自动恢复?这些问题,说白了都得靠定时器配合状态机来解决。
这一章,我们就来聊聊状态机里怎么玩转时间。我会从最基础的超时事件处理讲起,然后带你看看定时器状态机和看门狗状态机的实战写法。嗯,都是我在项目里踩过坑之后总结出来的经验。
9.1 超时事件处理:别让状态机死等
先问个问题:你的状态机在某个状态里,等一个外部事件。如果这个事件一直不来呢?
我见过不少新手写的代码,状态机进入等待状态后,就在那里死循环轮询。CPU跑满,功耗爆炸,关键是万一硬件出问题,整个系统就卡死了。这显然不行。
正确的做法是:给每个等待状态加一个超时机制。超时到了,就触发一个超时事件,状态机据此做出反应——重试、报错、或者回退到安全状态。
来看一个简单的超时状态机框架:
typedef enum {
STATE_IDLE,
STATE_WAITING,
STATE_TIMEOUT,
STATE_DONE
} State_t;
typedef struct {
State_t state;
uint32_t timeout_ms; // 超时阈值
uint32_t elapsed_ms; // 已等待时间
void (*on_timeout)(void); // 超时回调
} TimeoutSM_t;
void TimeoutSM_Tick(TimeoutSM_t *sm, uint32_t tick_ms) {
switch (sm->state) {
case STATE_IDLE:
// 啥也不干
break;
case STATE_WAITING:
sm->elapsed_ms += tick_ms;
if (sm->elapsed_ms >= sm->timeout_ms) {
sm->state = STATE_TIMEOUT;
if (sm->on_timeout) sm->on_timeout();
}
break;
case STATE_TIMEOUT:
// 超时处理,可以重试或报错
break;
case STATE_DONE:
// 正常完成
break;
}
}
这个模式我用了很多年。核心思想就一句话:状态机里不要有阻塞等待,用计数器代替延时。每次系统滴答(比如1ms中断)调用一次Tick函数,状态机自己累加时间,到了就触发超时。
重要原则:状态机里的所有等待,都必须有超时保护。没有超时的等待,就是一颗定时炸弹。
9.2 定时器状态机:把定时器本身当成一个状态机
你想想看,一个定时器从启动到超时,其实就是一个简单的两状态状态机:运行态和停止态。但实际项目中,定时器的行为往往更复杂——比如单次定时、周期定时、甚至带暂停和恢复功能。
我个人习惯把定时器也封装成状态机。这样上层逻辑清晰,而且容易扩展。来看一个支持单次和周期模式的定时器状态机:
typedef enum {
TIMER_STOPPED,
TIMER_RUNNING,
TIMER_EXPIRED,
TIMER_PAUSED
} TimerState_t;
typedef enum {
TIMER_MODE_ONESHOT,
TIMER_MODE_PERIODIC
} TimerMode_t;
typedef struct {
TimerState_t state;
TimerMode_t mode;
uint32_t period_ms;
uint32_t counter_ms;
void (*callback)(void);
} TimerSM_t;
void TimerSM_Start(TimerSM_t *tmr, uint32_t period_ms, TimerMode_t mode) {
tmr->period_ms = period_ms;
tmr->counter_ms = 0;
tmr->mode = mode;
tmr->state = TIMER_RUNNING;
}
void TimerSM_Tick(TimerSM_t *tmr, uint32_t tick_ms) {
switch (tmr->state) {
case TIMER_RUNNING:
tmr->counter_ms += tick_ms;
if (tmr->counter_ms >= tmr->period_ms) {
tmr->state = TIMER_EXPIRED;
if (tmr->callback) tmr->callback();
if (tmr->mode == TIMER_MODE_PERIODIC) {
tmr->counter_ms = 0;
tmr->state = TIMER_RUNNING; // 自动重载
}
}
break;
case TIMER_EXPIRED:
// 单次模式下,这里可以自动回到STOPPED
if (tmr->mode == TIMER_MODE_ONESHOT) {
tmr->state = TIMER_STOPPED;
}
break;
case TIMER_PAUSED:
// 暂停时不计数
break;
case TIMER_STOPPED:
default:
break;
}
}
我在项目中遇到过一个问题:多个定时器同时运行时,回调函数里如果修改了定时器状态,很容易造成竞态。后来我定了个规矩——定时器回调里只发事件,不做状态变更。状态变更统一放在主循环里处理。这个习惯帮我避免了很多难以复现的bug。
小技巧:如果你需要很多定时器,可以用一个定时器链表。每次Tick遍历链表,更新所有定时器。这样只需要一个硬件定时器,就能管理N个软件定时器。
9.3 看门狗状态机:系统的最后一道防线
看门狗(Watchdog)这东西,说白了就是一条「保命绳」。系统正常运行时,定期喂狗;如果系统卡死或者跑飞了,看门狗超时就会复位系统。
但看门狗怎么喂?很多人的做法是在主循环里随便找个地方喂一下。这其实很危险——如果某个分支卡死了,但主循环还在跑,看门狗照样被喂,系统就不会复位。你想想看,这等于看门狗白装了。
正确的做法是:用状态机来管理看门狗。把喂狗操作放在状态机的特定状态里,只有所有关键路径都正常通过了,才允许喂狗。
来看一个我常用的看门狗状态机模型:
typedef enum {
WDG_IDLE,
WDG_ARMED,
WDG_FEEDING,
WDG_EXPIRED
} WdgState_t;
typedef struct {
WdgState_t state;
uint32_t feed_interval_ms;
uint32_t last_feed_ms;
uint32_t max_feed_interval_ms;
uint8_t feed_count; // 喂狗次数统计
uint8_t feed_threshold; // 必须达到的喂狗次数
} WdgSM_t;
void WdgSM_Feed(WdgSM_t *wdg) {
// 只有处于ARMED状态才能喂狗
if (wdg->state == WDG_ARMED) {
wdg->feed_count++;
wdg->last_feed_ms = 0;
if (wdg->feed_count >= wdg->feed_threshold) {
wdg->state = WDG_FEEDING;
// 这里执行真正的硬件喂狗操作
HAL_Watchdog_Refresh();
wdg->feed_count = 0;
wdg->state = WDG_ARMED;
}
}
}
void WdgSM_Tick(WdgSM_t *wdg, uint32_t tick_ms) {
switch (wdg->state) {
case WDG_ARMED:
wdg->last_feed_ms += tick_ms;
if (wdg->last_feed_ms > wdg->max_feed_interval_ms) {
wdg->state = WDG_EXPIRED;
// 超时未喂狗,触发系统复位
NVIC_SystemReset();
}
break;
case WDG_FEEDING:
// 喂狗完成后自动回到ARMED
wdg->state = WDG_ARMED;
break;
case WDG_EXPIRED:
// 已经复位了,这里不会执行到
break;
default:
break;
}
}
这个设计的关键点在于:喂狗不是随便调的,必须满足条件。比如你可以要求:只有任务A、任务B、任务C都正常跑完一轮,并且通信模块确认无异常,才允许喂狗。任何一个环节卡住,喂狗次数就达不到阈值,看门狗就会超时复位。
警告:千万不要在中断服务函数里喂狗!我曾经犯过这个错——中断频繁触发,看门狗一直被喂,主循环卡死了都不知道。后来产品在现场出了问题,排查了三天才找到原因。从那以后,我规定喂狗只能在主循环的特定位置进行。
9.4 三种定时器状态机的对比与选择
好了,三种定时器相关的状态机都讲完了。我整理了一个表格,方便你对比:
| 类型 | 核心用途 | 状态数 | 典型场景 | 注意事项 |
|---|---|---|---|---|
| 超时事件处理 | 给等待状态加超时保护 | 3-4个 | 串口接收超时、按键长按检测 | 每个等待状态都要有超时 |
| 定时器状态机 | 管理定时器的启停和模式 | 4-5个 | 周期任务调度、延时触发 | 回调里不要改状态 |
| 看门狗状态机 | 安全喂狗,防止误喂 | 3-4个 | 系统异常检测、自动复位 | 不要在中断里喂狗 |
实际项目中,这三种状态机经常组合使用。比如一个通信协议栈:用超时状态机处理报文等待,用定时器状态机管理重发间隔,用看门狗状态机确保整个协议栈正常运行。三者配合,系统的健壮性会提升一个档次。
9.5 核心知识体系
为了让你更直观地理解这一章的内容,我画了一张图,把三种定时器状态机的关系和核心逻辑展示出来:
从这张图可以看得很清楚:三种状态机各有侧重,但核心思想是一致的——用状态机管理时间,而不是用延时函数阻塞CPU。这是嵌入式实时系统的基本功,也是区分新手和老手的分水岭。
好了,这一章的内容就到这里。定时器和状态机的组合,说白了就是给系统装上「时间感知」能力。你可以在自己的项目里试试看,从最简单的超时保护开始,逐步加入定时器状态机和看门狗状态机。相信我,系统的稳定性会有质的提升。
公众号:蓝海资料掘金营,微信deep3321