16、宏实现状态机:使用宏定义状态与事件,状态转换表的宏实现,简单状态机框架
状态机这东西,做嵌入式的基本都绕不开。按键消抖、通信协议解析、菜单切换……说白了,就是让程序知道“现在在干嘛,来了信号该干嘛”。
我早年刚入行时,写状态机就是一堆 switch-case 嵌套,代码又臭又长。后来被一个老工程师点醒——用宏来定义状态和事件,再配合一张转换表,代码立马清爽了。今天我就把这套方法掰开揉碎讲给你。
16.1 状态机的基本要素
一个状态机,核心就三样东西:状态、事件、转换规则。
- 状态:系统当前所处的模式。比如按键的“按下”、“释放”。
- 事件:触发状态变化的条件。比如“检测到电平变化”。
- 转换:在某个状态下,遇到某个事件,下一步该去哪个状态。
用宏来定义这些,好处是直观、可读性强,而且修改起来方便。你想想看,如果状态和事件都用裸数字,过两周你自己都看不懂。
16.2 用宏定义状态与事件
我个人习惯,先把所有状态和事件用 #define 或 enum 列出来。但为了灵活性,我更喜欢用宏,因为可以配合后面的转换表做字符串化。
/* 状态定义 */
#define STATE_IDLE 0
#define STATE_ACTIVE 1
#define STATE_ERROR 2
#define STATE_DONE 3
/* 事件定义 */
#define EVT_NONE 0
#define EVT_START 1
#define EVT_STOP 2
#define EVT_TIMEOUT 3
#define EVT_DATA_READY 4
嗯,这里要注意:状态和事件的编号不要冲突。我一般把状态从0开始,事件从100开始,或者干脆用不同的枚举类型。但用宏的话,你只要自己心里有数就行。
/* 用户按下启动键 */,这样半年后回来看代码,不用重新猜。
16.3 状态转换表的宏实现
状态转换表,说白了就是一个二维数组:行是当前状态,列是事件,单元格里存的是下一个状态。
但直接写二维数组太死板了。我更喜欢用宏来“生成”这张表。这样改一个状态或事件,表会自动调整,不容易出错。
/* 定义转换表宏:当前状态、事件、下一状态 */
#define TRANS(curr, evt, next) \
[curr][evt] = next
/* 声明转换表 */
static uint8_t state_table[4][5]; /* 4个状态,5个事件 */
/* 初始化转换表 */
void state_table_init(void)
{
/* 清零 */
memset(state_table, 0xFF, sizeof(state_table));
/* 填充转换规则 */
TRANS(STATE_IDLE, EVT_START, STATE_ACTIVE);
TRANS(STATE_ACTIVE, EVT_STOP, STATE_IDLE);
TRANS(STATE_ACTIVE, EVT_TIMEOUT, STATE_ERROR);
TRANS(STATE_ERROR, EVT_DATA_READY, STATE_DONE);
TRANS(STATE_DONE, EVT_START, STATE_ACTIVE);
}
你看,这样写是不是很清晰?每个转换规则就是一行宏。想加一个新规则,直接加一行 TRANS(...) 就行。我曾经在一个项目里维护了30多个状态、20多个事件,全靠这张表撑着,没出过逻辑混乱。
16.4 简单状态机框架
有了状态和事件的定义,有了转换表,剩下的就是跑框架了。一个典型的状态机执行流程是这样的:
- 等待事件到来
- 查表,得到下一个状态
- 执行状态进入/退出动作(可选)
- 更新当前状态
- 循环
下面是一个极简的框架实现:
/* 当前状态 */
static uint8_t current_state = STATE_IDLE;
/* 状态机主循环 */
void state_machine_run(uint8_t event)
{
uint8_t next_state;
/* 查表获取下一状态 */
next_state = state_table[current_state][event];
/* 如果查到的状态无效,保持不动 */
if (next_state == 0xFF) {
next_state = current_state;
}
/* 如果状态真的变了,执行切换 */
if (next_state != current_state) {
/* 退出当前状态(可扩展) */
/* 进入新状态(可扩展) */
current_state = next_state;
}
/* 执行当前状态的动作(可选) */
switch (current_state) {
case STATE_IDLE:
/* 空闲时做什么 */
break;
case STATE_ACTIVE:
/* 活跃时做什么 */
break;
/* ... */
}
}
这个框架虽然简单,但已经能覆盖大部分嵌入式场景了。你可以在状态切换时加入回调函数,比如 on_enter_state() 和 on_exit_state(),这样每个状态进入和离开时都能做点特定的事。
16.5 用SVG画一张状态机转换图
光说可能不够直观,我画了一张图,把上面例子里的状态转换关系展示出来。你看一眼就明白了。
16.6 宏实现状态机的进阶技巧
上面讲的是基础框架。在实际项目中,我还会加一些“佐料”,让状态机更健壮、更好用。
- 状态进入/退出钩子:用宏定义
STATE_ENTER(state)和STATE_EXIT(state),在状态切换时自动调用。这样每个状态可以有自己的初始化/清理逻辑。 - 事件队列:如果事件来得太快,状态机处理不过来,可以用一个环形缓冲区暂存事件。宏定义
EVT_QUEUE_PUSH(evt)和EVT_QUEUE_POP()来操作。 - 状态机调试:我习惯在宏里加一个调试打印开关。比如
#ifdef DEBUG_FSM时,每次状态切换都打印“从XX状态,因XX事件,切换到XX状态”。这个习惯帮我抓过不少逻辑bug。
核心要点: 宏定义状态机的精髓,在于把“状态-事件-转换”这三者的关系,从散落在代码各处的 if-else 中抽离出来,集中到一张表里。这样逻辑一目了然,修改也安全。
好了,关于宏实现状态机,我就讲这么多。你回去可以试着把项目里的一个 switch-case 状态机改成这种表驱动的方式。改完之后你会发现——嗯,代码确实顺眼多了。
公众号:蓝海资料掘金营,微信deep3321