一、宏与状态机:从混乱到秩序
状态机这东西,说白了就是让代码按规矩办事。我早年做嵌入式协议解析时,代码里全是 if-else 嵌套,改一个状态要翻半天。后来用宏重构,整个世界清静了。
宏在状态机里能干什么?三个核心场景:
- 状态定义:用宏给状态起名字,别用裸数字
- 事件驱动:用宏封装事件触发逻辑
- 状态转换表:用宏生成查表代码
先看个反面教材:
// 别这么写
if (state == 0) { ... }
else if (state == 1) { ... }
else if (state == 2) { ... }
你想想看,三个月后你还能记得 0 是啥状态?我反正记不住。
核心原则:状态机宏的本质,是把「状态编号」变成「可读的名字」,把「转换逻辑」变成「表格数据」。
二、状态机宏定义:给状态起个好名字
2.1 基础状态宏
#define STATE_IDLE 0
#define STATE_HEADER 1
#define STATE_DATA 2
#define STATE_CHECKSUM 3
#define STATE_DONE 4
嗯,这里要注意:状态值最好从 0 开始连续递增。为什么?因为后面做状态转换表时,数组下标直接对应状态编号,省去查找开销。
2.2 状态行为宏
我个人习惯把「进入状态时的操作」也封装成宏:
#define ENTER_STATE(s) do { current_state = (s); on_entry(s); } while(0)
#define EXIT_STATE(s) do { on_exit(s); } while(0)
这个 do-while 结构是宏的经典写法,防止展开后出问题。我在项目中吃过亏,宏展开后 if 后面跟了多个语句,结果只有第一句被条件控制——从那以后,所有多语句宏我都套 do-while。
小技巧:状态宏的名字加个前缀,比如 STM_ 或 STATE_,避免和其他变量冲突。我习惯用 STM_,因为短。
三、事件驱动宏:让状态机活起来
状态机不能光有状态,还得有事件驱动它跳转。事件驱动宏就是干这个的。
3.1 事件定义
#define EVT_NONE 0
#define EVT_RX_BYTE 1
#define EVT_TIMEOUT 2
#define EVT_ERROR 3
#define EVT_COMPLETE 4
3.2 事件处理宏
#define HANDLE_EVENT(evt) \
do { \
event_t e = (evt); \
if (e != EVT_NONE) { \
process_event(e); \
} \
} while(0)
#define TRIGGER_TRANSITION(src, evt, dst) \
((src) == current_state && (evt) == current_event ? (dst) : -1)
TRIGGER_TRANSITION 这个宏,说白了就是查表用的。它检查当前状态和事件是否匹配,匹配就返回目标状态,不匹配返回 -1。我在做串口协议解析器时,就用这个宏配合状态转换表,代码量直接砍半。
注意:事件宏的参数不要有副作用。比如 HANDLE_EVENT(get_byte()),如果 get_byte 有内部状态变化,宏展开多次调用就会出问题。我曾经被这个坑过,调试了一下午才发现是宏展开导致函数调了两次。
四、状态转换表宏:用数据代替逻辑
这是状态机设计的精髓。把转换关系写成表格,代码就变成了查表,清晰又好维护。
4.1 转换表结构
typedef struct {
uint8_t src_state;
uint8_t event;
uint8_t dst_state;
void (*action)(void);
} trans_t;
4.2 表格定义宏
#define TRANS_ENTRY(src, evt, dst, act) \
{ .src_state = (src), .event = (evt), .dst_state = (dst), .action = (act) }
#define TRANS_TABLE_END \
{ .src_state = 0xFF, .event = 0xFF, .dst_state = 0xFF, .action = NULL }
用宏定义表格条目,比手写结构体初始化清晰多了。你看:
const trans_t protocol_trans[] = {
TRANS_ENTRY(STATE_IDLE, EVT_RX_BYTE, STATE_HEADER, on_rx_header),
TRANS_ENTRY(STATE_HEADER, EVT_RX_BYTE, STATE_DATA, on_rx_data),
TRANS_ENTRY(STATE_DATA, EVT_RX_BYTE, STATE_CHECKSUM, on_rx_checksum),
TRANS_ENTRY(STATE_CHECKSUM, EVT_RX_BYTE, STATE_DONE, on_complete),
TRANS_ENTRY(STATE_ANY, EVT_TIMEOUT, STATE_IDLE, on_timeout),
TRANS_TABLE_END
};
STATE_ANY 是个特殊状态,表示「任何状态遇到超时都回到空闲」。这种通配符在状态机里很实用。
4.3 查表执行宏
#define EXEC_TRANSITION(table, state, evt) \
do { \
const trans_t *t = (table); \
while (t->action != NULL) { \
if ((t->src_state == (state) || t->src_state == STATE_ANY) \
&& t->event == (evt)) { \
t->action(); \
state = t->dst_state; \
break; \
} \
t++; \
} \
} while(0)
这个宏遍历表格,找到匹配的条目就执行动作并更新状态。注意 STATE_ANY 的处理,它匹配任何状态。
为什么用宏而不是函数? 宏在编译时展开,没有函数调用开销。对于嵌入式系统,尤其是中断上下文里的状态机,这点性能差异可能决定系统能否跑在目标频率上。
五、实战:协议解析器
来个完整的例子。假设我们要解析一个简单协议:帧头(0xAA) + 长度(1字节) + 数据(n字节) + 校验(1字节)。
5.1 状态与事件定义
// 状态
#define STM_IDLE 0
#define STM_SYNC 1 // 等待同步头
#define STM_LENGTH 2 // 接收长度
#define STM_DATA 3 // 接收数据
#define STM_CHECK 4 // 接收校验
#define STM_DONE 5 // 完成
// 事件
#define EVT_BYTE 0
#define EVT_SYNC_OK 1
#define EVT_LEN_OK 2
#define EVT_DATA_DONE 3
#define EVT_CHECK_OK 4
#define EVT_ERROR 5
5.2 状态转换表
const trans_t parser_trans[] = {
// 空闲态:收到0xAA进入同步
TRANS_ENTRY(STM_IDLE, EVT_SYNC_OK, STM_SYNC, reset_packet),
// 同步态:收到长度字节
TRANS_ENTRY(STM_SYNC, EVT_BYTE, STM_LENGTH, save_length),
// 长度态:开始收数据
TRANS_ENTRY(STM_LENGTH, EVT_LEN_OK, STM_DATA, init_data_buf),
// 数据态:收完所有数据
TRANS_ENTRY(STM_DATA, EVT_DATA_DONE, STM_CHECK, finalize_data),
// 校验态:校验通过
TRANS_ENTRY(STM_CHECK, EVT_CHECK_OK, STM_DONE, signal_complete),
// 任何状态出错都回到空闲
TRANS_ENTRY(STM_ANY, EVT_ERROR, STM_IDLE, reset_packet),
TRANS_TABLE_END
};
5.3 主循环
void parser_run(uint8_t byte) {
uint8_t event = EVT_BYTE;
// 检测同步头
if (current_state == STM_IDLE && byte == 0xAA) {
event = EVT_SYNC_OK;
}
// 检测长度完成
if (current_state == STM_SYNC) {
packet_len = byte;
event = EVT_LEN_OK;
}
// 检测数据完成
if (current_state == STM_DATA && data_idx >= packet_len) {
event = EVT_DATA_DONE;
}
// 检测校验
if (current_state == STM_CHECK) {
event = (byte == calc_checksum()) ? EVT_CHECK_OK : EVT_ERROR;
}
// 执行转换
EXEC_TRANSITION(parser_trans, current_state, event);
}
你看,主循环里没有 if-else 嵌套,全是事件判断 + 查表执行。加一个新状态?加一行表格就行。改一个转换?改表格条目就行。代码的可维护性直接拉满。
避坑指南:我曾经在状态机里忘了处理「非法事件」,结果系统进入未定义状态。后来我在查表宏里加了个 default 处理,遇到未匹配事件就触发错误状态。建议你也加上。
六、知识体系总览
下面这张图总结了宏与状态机的核心关系:
这张图把三个核心模块串起来了。状态定义宏提供「名字」,事件驱动宏提供「触发」,状态转换表宏提供「规则」。三者结合,就是一套完整的协议解析框架。
总结一下:宏在状态机里的价值,不是炫技,而是让代码变得可读、可维护、可扩展。我做了十年嵌入式,见过太多被 if-else 堆死的协议栈。用宏 + 状态转换表,你也能写出优雅的解析器。
最后说一句:别把宏用得太花哨。宏的本质是文本替换,过度使用反而让代码难调试。我的原则是:宏只做「命名」和「查表」两件事,复杂的逻辑还是交给函数。
公众号:蓝海资料掘金营,微信deep3321