很多开发者都能熟练驱动各种单片机外设,但一写完整的项目代码就容易迷失,毫无逻辑与框架感。这说明仅停留在操作外设的层面还不够,掌握一套优秀的编程思想才是进阶的关键——比如模块化编程、状态机 编程、分层架构等。
本文就来深入聊聊状态机编程,并通过一个具体的按键控制 LED 灯的例子,展示如何从状态转换图到 C 代码落地实现。
什么是状态机?
状态机(State Machine)包含五个核心要素:
- 状态(State):系统在某一时刻所处的稳定工作模式,整个生命周期中可能包含多个状态。例如,一台电动机具有正转、反转、停转三种状态。
- 迁移(Transition):系统从一个状态转移到另一个状态的过程,迁移需要外部事件驱动,不会自动发生。停转的电机必须上电才能转动。
- 事件(Event):在某一时刻发生的、对系统有意义的事情,是触发状态迁移的原因。对电机而言,加正电压、加负电压、断电就是事件。
- 动作(Action):状态机在迁移过程中执行的其它行为,是对事件的响应。比如给停转的电机加正电压时,电机不仅迁移到正转状态,还会启动转动——这个启动过程就是动作。
- 条件(Guard):即使事件发生,状态机也可能需要满足一定条件才执行迁移。例如,电机虽然已合闸,但若供电线路故障,仍然不能转起来。
一个状态机必须从初始状态开始运行。
实例:按键控制 LED 灯
电路与功能需求
电路如下图所示,包含一个单片机 MCU、一个按键 K0 和两个 LED 灯 L1、L2。
功能需求如下:
- L1 和 L2 的状态按顺序转换:OFF/OFF → ON/OFF → ON/ON → OFF/ON → OFF/OFF
- 每次状态转换需要连续按键 5 次
- 初始状态为 OFF/OFF
状态转换图
在状态机编程中,先设计状态转换图,再编写代码才是正确的顺序。下面是一张使用类似 UML 语法绘制的按键控制流水灯状态转换图:
图中:
- 圆角矩形表示状态,内部标注状态名称(如
LS_OFFOFF、LS_ONOFF 等)。
- 带箭头的直线或弧线表示状态迁移,从初态指向次态。
- 迁移上的标注格式为:
事件[条件] / 动作列表(条件和动作列表可选)。例如 KEY[g_sIFSM_u8KeyCnt > 3] / g_sIFSM_u8KeyCnt--; le_d_off(LED1); 表示发生按键事件,且条件满足时执行相应动作并迁移状态。
- 黑色实心圆点表示运行前的不可知状态,启动时强制迁移到初始状态,该迁移不需要事件但可以有动作。
- 包含实心圆点的圆圈表示状态机生命周期的结束,本例中的状态机循环运行,因此没有状态指向该圆圈。
代码实现
根据状态转换图,写出如下 C 代码:
void main(void)
{
sys_init();
led_off(LED1);
led_off(LED2);
g_stFSM.u8LedStat = LS_OFFOFF;
g_stFSM.u8KeyCnt = 0;
while(1)
{
if(test_key() == TRUE)
{
fsm_active();
}
else
{
; /*idle code*/
}
}
}
void fsm_active(void)
{
if(g_stFSM.u8KeyCnt > 3) /*击键是否满 5 次*/
{
switch(g_stFSM.u8LedStat)
{
case LS_OFFOFF:
led_on(LED1); /*输出动作*/
g_stFSM.u8KeyCnt = 0;
g_stFSM.u8LedStat = LS_ONOFF; /*状态迁移*/
break;
case LS_ONOFF:
led_on(LED2); /*输出动作*/
g_stFSM.u8KeyCnt = 0;
g_stFSM.u8LedStat = LS_ONON; /*状态迁移*/
break;
case LS_ONON:
led_off(LED1); /*输出动作*/
g_stFSM.u8KeyCnt = 0;
g_stFSM.u8LedStat = LS_OFFON; /*状态迁移*/
break;
case LS_OFFON:
led_off(LED2); /*输出动作*/
g_stFSM.u8KeyCnt = 0;
g_stFSM.u8LedStat = LS_OFFOFF; /*状态迁移*/
break;
default: /*非法状态*/
led_off(LED1);
led_off(LED2);
g_stFSM.u8KeyCnt = 0;
g_stFSM.u8LedStat = LS_OFFOFF; /*恢复初始状态*/
break;
}
}
else
{
g_stFSM.u8KeyCnt++; /*状态不迁移,仅记录击键次数*/
}
}
代码解析与扩展性分析
在 fsm_active() 函数中,g_stFSM.u8KeyCnt = 0; 在 switch-case 里出现了多次,其实完全可以合并到 switch 之前。这里保持重复写法,目的是清晰展示每个状态迁移时的完整动作,与状态转换图的意图完全对齐。
状态机变量 g_stFSM 包含两个成员:u8LedStat(LED 状态,质变因子)和 u8KeyCnt(按键计数,量变因子)。量变因子逐步积累,达到阈值后才触发质变因子变化,从而实现状态迁移。这种结构称为扩展状态机(Extended State Machine)。
假设需求从“连按 5 次”改为“连按 100 次”,只需将条件 g_stFSM.u8KeyCnt > 3 改成 g_stFSM.u8KeyCnt > 98 即可。而如果仅用一个状态变量,每个状态需要拆分成 5 个小状态,连按 100 次就需要 400 个状态,switch-case 会膨胀到无法维护。扩展状态机通过分离质变与量变,让代码的扩展性和可读性都得到了质的提升。
通过这个简单的例子,状态机编程的威力可见一斑。当你面对复杂度更高的嵌入式系统时,这种思想能帮助你构建出逻辑清晰、便于维护的程序框架。
|