状态机:让单片机知道自己正在做什么
导入
刚开始写单片机程序时,我很喜欢直接堆 if:
c
if (key) motor_start();
if (stop_line) motor_stop();
if (error) led_on();功能少的时候看不出问题。功能一多,就会出现“这个条件到底在哪个阶段生效”的疑问。
例如小车还没启动时检测到了停车线,应该停车吗?已经进入故障状态后,按下启动键还能直接启动吗?
状态机就是把“当前处于什么阶段”这件事明确写出来。
主要思想
一个状态机主要包含三样东西:
- 状态:系统现在正在做什么
- 事件:发生了什么
- 转移:发生事件后应该去哪个状态
以循迹小车为例:
text
待机 --按下启动键--> 循迹 --检测到停车线--> 停车
|
+--传感器异常--> 故障状态机并不是什么复杂算法。它只是要求我在写功能前,先回答两个问题:
- 这段代码属于哪个状态?
- 什么条件下允许切换到另一个状态?
简单实现
先定义状态:
c
typedef enum
{
CAR_IDLE,
CAR_TRACKING,
CAR_STOP,
CAR_ERROR
} CarState_t;
static CarState_t car_state = CAR_IDLE;再根据当前状态分别处理:
c
void Car_StateMachine(void)
{
switch (car_state)
{
case CAR_IDLE:
Motor_Stop();
if (Key_Start())
car_state = CAR_TRACKING;
break;
case CAR_TRACKING:
Track_Control();
if (Sensor_Error())
car_state = CAR_ERROR;
else if (Find_Stop_Line())
car_state = CAR_STOP;
break;
case CAR_STOP:
Motor_Stop();
break;
case CAR_ERROR:
Motor_Stop();
LED_Error_On();
break;
}
}把它放进固定周期任务中运行即可:
c
while (1)
{
Car_StateMachine();
Delay_ms(10);
}实际工程里更推荐使用定时器或系统节拍调用,而不是阻塞式延时。这里使用 Delay_ms() 只是为了让例子足够直观。
进入状态时只执行一次
有些操作只应该在切换状态时执行一次,例如清空PID:
c
void Car_SetState(CarState_t new_state)
{
if (new_state == car_state)
return;
car_state = new_state;
if (car_state == CAR_TRACKING)
PID_Reset();
}之后不要直接修改 car_state,统一调用 Car_SetState()。这样进入状态时的初始化不会散落在各处。
容易踩的坑
一个状态做了太多事
如果一个 case 已经写了几百行,说明状态还可以继续拆分。比如“启动”可以拆成传感器校准、电机准备、开始运行。
在很多文件里直接修改状态
这样很难找到状态为什么突然变化。最好把状态切换集中在一个函数或一个模块中。
忘记处理非法状态
变量被意外修改后,switch 可能一个分支也不进入。可以增加默认保护:
c
default:
car_state = CAR_ERROR;
Motor_Stop();
break;个人理解
我以前觉得状态机只是把 if 换成 switch,写完后才发现真正有用的不是语法,而是它强迫我先整理程序的行为。
没有状态机时,我想的是“检测到这个条件就执行什么”;用了状态机后,我会多想一步:“系统现在允许响应这个条件吗?”
对于小项目,状态机可能显得有点多余。但项目只要开始加入按键、传感器、PID、保护和不同工作模式,它就能明显减少那种改了这里、另一处突然出问题的情况。
总结
状态机就是:
text
当前状态 + 发生的事件 -> 下一个状态它不能让代码自动变正确,但能把程序的运行过程写得更清楚。对单片机来说,知道“现在该做什么”和知道“现在不该做什么”同样重要。