掌握了单片机各个外设的驱动,但在构建完整程序时仍感觉逻辑混乱、缺乏框架?这往往意味着编程水平仍停留在初级阶段。要突破这一瓶颈,你需要掌握一种优秀的编程框架或思想,例如模块化编程、分层设计或状态机编程。本文将深入探讨状态机(State Machine) 这一在单片机与嵌入式系统开发中极为重要的编程范式。
状态机的核心五要素
一个标准的状态机包含以下五个基本要素:
- 状态(State):系统在某一时刻所处的稳定工作情况。在整个工作周期中,系统可能拥有多个状态。例如,一个电机可能有“正转”、“反转”和“停转”三种状态。状态机需要指定一个状态作为初始状态。
- 迁移(Transition):系统从一个状态切换到另一个状态的过程。迁移不会自动发生,必须由外部因素触发。例如,停转的电机不会自己启动,必须通电才能“迁移”到旋转状态。
- 事件(Event):在特定时刻发生的、对系统有意义的事情。状态迁移的直接原因就是事件的发生。对电机而言,“施加正电压”、“施加负电压”、“断电”就是事件。
- 动作(Action):在状态迁移过程中,状态机所执行的附加行为,即对事件的响应。例如,给停转的电机施加正电压,使其迁移到正转状态,同时启动电机的过程就是“动作”。
- 条件(Guard):状态机对事件并非无条件响应。即使事件发生,也必须满足特定“条件”,状态迁移才会真正执行。例如,电机虽已合闸(事件),但如果供电线路断路(条件不满足),则仍无法完成从停转到旋转的迁移。
实战案例:基于状态机的按键控制流水灯
1. 问题定义
假设我们有如下电路:一个单片机(MCU)、一个按键 K0、两个 LED 灯 L1 和 L2。
需要实现的功能如下:
- L1 和 L2 的状态按以下顺序循环:OFF/OFF -> ON/OFF -> ON/ON -> OFF/ON -> OFF/OFF。
- 通过按键 K0 控制状态转换,每次状态转换需要连续按下按键 5 次。
- L1 和 L2 的初始状态为 OFF/OFF。

2. 设计状态转换图
在状态机编程中,正确的流程是先设计状态转换图,再编写程序。程序代码应是状态图逻辑的直接体现。
下图展示了本例的按键控制流水灯状态转换图(采用近似UML的表示法):

图例说明:
- 圆角矩形:代表状态机的各个状态,内部标注状态名。
- 带箭头的线:代表状态迁移,从原状态指向新状态。
- 标签“事件[条件]/动作”:表示在当前状态下,如果发生某个“事件”且满足“条件”,则执行状态“迁移”并产生相应的“动作”列表(条件和动作为可选项)。本例中,用“KEY”表示击键事件。
- 实心黑点:表示状态机启动前的未知状态,程序初始化时必须强制从此处迁移到初始状态。
- 带圈的实心点:表示状态机生命周期的结束(本例为循环,未使用)。
3. 程序实现
以下是依据上述状态转换图编写的C语言代码核心逻辑:
// 状态定义
#define LS_OFFOFF 0
#define LS_ONOFF 1
#define LS_ONON 2
#define LS_OFFON 3
// 状态机结构体
struct {
uint8_t u8LedStat; // 当前LED状态(质变因子)
uint8_t u8KeyCnt; // 连续按键计数(量变因子)
} g_stFSM;
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 {
; /* 空闲时可执行其他任务 */
}
}
}
void fsm_active(void) {
if(g_stFSM.u8KeyCnt > 3) { // 是否已连续按键5次?(计数从0开始,>3即>=4)
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++; // 状态未迁移,仅记录按键次数
}
}
4. 代码分析与思想延伸
观察 fsm_active() 函数,g_stFSM.u8KeyCnt = 0; 在 switch-case 中出现了多次。虽然从代码简化角度可以将其合并到 switch 语句之前,但当前的写法旨在清晰对应状态图中每次迁移所伴随的独立动作,体现了状态机设计的直观性。
关键在于 g_stFSM 这个结构体,它包含两个成员:
u8LedStat:代表 LED 的实质状态(主变量/质变因子)。
u8KeyCnt:记录连续按键次数(辅助变量/量变因子)。
这种设计体现了 扩展状态机(Extended State Machine) 的思想。量变因子(按键次数)的积累,最终触发质变因子(LED状态)的改变。
对比思考:能否只用一个 u8LedStat 变量实现?可以,但需要将每个LED状态拆分成5个子状态,总共需要20个状态。如果需求改为“连续按键100次才改变状态”,则需要400个状态!代码将变得冗长且难以维护。
而采用扩展状态机的方式,只需将判断条件改为 if(g_stFSM.u8KeyCnt > 98) 即可。这充分证明了良好的状态机设计能极大提升代码的可扩展性和可维护性,是处理复杂逻辑流的强大工具。
|