状态机在嵌入式系统中的应用:不只是switch-case这么简单

先看一段代码。一个按键消抖的典型实现:

c 复制代码
typedef enum {
    KEY_IDLE,
    KEY_DEBOUNCE,
    KEY_PRESSED,
    KEY_RELEASE_WAIT
} KeyState;

KeyState state = KEY_IDLE;

void key_scan(void) {
    uint8_t level = GPIO_ReadPin(KEY_PIN);
    switch(state) {
        case KEY_IDLE:
            if(level == 0) state = KEY_DEBOUNCE;
            break;
        case KEY_DEBOUNCE:
            if(level == 0) state = KEY_PRESSED;
            else state = KEY_IDLE;
            break;
        case KEY_PRESSED:
            key_event = KEY_DOWN;
            state = KEY_RELEASE_WAIT;
            break;
        case KEY_RELEASE_WAIT:
            if(level == 1) state = KEY_IDLE;
            break;
    }
}

这段代码很多单片机工程师都写过。用switch-case管理几个状态,逻辑清晰,跑起来也没问题。

但随着状态增多------比如一个通信协议解析器,可能有十多个状态,再加上状态内的子条件判断------switch-case就会变得又长又散,维护起来像是在翻一本没有目录的手册。

设计模式在这里不是理论,是刚需。

从状态表到状态机引擎

一个有意思的思路是:把状态和动作解耦,用查表代替分支判断。

我们设计一个状态表结构:

c 复制代码
typedef struct {
    uint8_t  current_state;
    uint8_t  event;
    uint8_t  next_state;
    void (*action)(void);
} StateTableEntry;

每一行定义:当前在什么状态,收到什么事件,执行什么动作,然后跳转到什么状态。协议栈、UI菜单、通信链路管理......凡是状态流转清晰的场景,都能用这张表来驱动。

来看看怎么用。假设有一个串口AT指令解析器,状态包括"等待AT"、"接收参数"、"校验"、"响应":

c 复制代码
StateTableEntry at_parser_table[] = {
    {ST_WAIT_AT,    EVT_RX_A,    ST_RECV_PARAM, at_send_ok},
    {ST_WAIT_AT,    EVT_RX_OTHER, ST_WAIT_AT,   at_send_error},
    {ST_RECV_PARAM, EVT_CR,      ST_CHECKSUM,   NULL},
    {ST_CHECKSUM,   EVT_OK,      ST_WAIT_AT,    at_exec_cmd},
    {ST_CHECKSUM,   EVT_FAIL,    ST_WAIT_AT,    at_send_error},
    // ... 更多条目
};

然后用一个统一的引擎函数来驱动:

c 复制代码
uint8_t state_machine_run(StateTableEntry *table, uint8_t table_size,
                          uint8_t current_state, uint8_t event) {
    for(uint8_t i = 0; i < table_size; i++) {
        if(table[i].current_state == current_state &&
           table[i].event == event) {
            if(table[i].action) table[i].action();
            return table[i].next_state;
        }
    }
    return current_state; // 未匹配,保持状态
}

核心逻辑浓缩到十几行代码里。要加一个新状态或新事件,不用动这个引擎------只需要往表里加一行。这就是典型的策略模式在状态机上的应用,只不过我们用C语言实现得足够轻量。

为什么这套模式在嵌入式场景中特别有价值

状态表驱动的好处,不只是代码好看。

第一,可读性飞跃。 一个同事接手你的代码,看几百行的switch-case和看一张结构清晰的状态表,完全是两种体验。表格本身就是文档------每一行明确表达"在XX状态下收到XX事件就做XX然后跳转到XX状态"。

第二,可测试性。 写单元测试的时候,我们可以直接遍历状态表,构造每个状态+事件的组合,验证跳转是否正确。switch-case的测试覆盖要人工走读,状态表可以做到自动化覆盖。

第三,资源占用可控。 很多人担心查表比直接switch慢。我们来算一笔账:一张50条的状态表,用线性查找最多50次比较。在72MHz的STM32上,这连1微秒都用不到。如果状态表再按状态分组、用二分查找,查表次数可以降到log2(n)级别。对于绝大多数嵌入式场景,查表的开销比一个GPIO翻转还小。

一个常见的误区是认为状态机只适合简单场景。恰恰相反------状态越多,状态表方案的优势越明显。一个正在运行的MQTT客户端协议栈,状态数可能有20~30个,事件类型也在10个以上,用switch-case写出来的嵌套深度和代码行数都会让人头疼。

另一种思路:分层状态机

上面的状态表还有一个扩展方向。有些场景里,状态之间存在"继承"关系。比如一个设备有"正常运行"和"异常处理"两个大状态,而"正常运行"下又分"采集"、"发送"、"休眠"子状态。如果把所有状态平铺在一张表里,每个子状态都要重复处理"异常事件"的跳转逻辑。

分层状态机的想法是:子状态处理不了的事件,向上抛给父状态处理。这样父状态只写一次异常处理逻辑,所有子状态共享。

c 复制代码
// 子状态表只关心自己的业务事件
StateTableEntry child_table[] = {
    {ST_COLLECT, EVT_DATA_READY, ST_SEND,    sensor_read},
    {ST_SEND,    EVT_TX_DONE,    ST_SLEEP,   radio_send},
    {ST_SLEEP,   EVT_WAKEUP,     ST_COLLECT, wakeup_init},
};

// 父状态表统一处理公共事件,包括异常
StateTableEntry parent_table[] = {
    {ST_RUNNING, EVT_ERROR,      ST_ERROR_HANDLE, log_error},
    {ST_RUNNING, EVT_LOW_BATTERY, ST_SLEEP,      power_save},
};

引擎运行时先查子状态表,如果没匹配到,再查父状态表。这个机制用C语言实现只需要几十行,带来的状态管理便利性却很大。尤其在工业设备、无人机飞控这类对异常处理要求严格的系统里,分层状态机几乎成了标配。

回调函数指针和状态表的组合,本质上是在C语言里模拟了面向对象的多态能力。不夸张地说,理解了这套模式,嵌入式代码的结构层次会上一个台阶。

不妨想想你手头正在做的项目------LED灯效切换、菜单导航、通信协议解析、电源管理......是不是都能用状态表来重新组织?一个有意思的练习是:把你现有一个用switch-case实现的状态机,改写成状态表驱动,然后对比两种实现的代码行数和维护成本。结果可能会让你重新思考"设计模式"这个词在嵌入式开发中的意义。欢迎在实践中遇到具体问题时来讨论------每个协议、每个设备的状态转换逻辑,都有值得优化的空间。

相关推荐
程序员小八77710 小时前
MySQL 事务:一文把 ACID、隔离级别、MVCC、锁全串起来
数据库·mysql
lzhdim10 小时前
提高 SQL 语句执行速度的方法
java·开发语言·数据库·sql·oracle
ltl10 小时前
ClickHouse Distributed 引擎与分布式查询路由
数据库
Wang's Blog10 小时前
PostgreSQL笔记60: 权限与角色管理——从层级体系到最佳实践
数据库·笔记·postgresql
whcyhhh11 小时前
头歌实践教学平台:大数据存储2023(六)
大数据·数据库·python
Elastic 中国社区官方博客13 小时前
让大模型思考,让小模型执行:在 Elastic Workflows 中拆分 LLM 成本
大数据·运维·数据库·人工智能·elasticsearch·ai
Faith_xzc13 小时前
一条 SQL 顶一条 Flink 链路?Doris Streaming Job 持续导入全景解析
大数据·数据库·sql·flink
这个DBA有点耶15 小时前
多模数据库深度解读:从“多库拼装”到“一库多能”的架构演进
数据库·mysql·dba
程序员夏洛16 小时前
MySQL 默认的事务隔离级别是什么?为什么选择这个级别?
数据库·mysql
艺术留白16 小时前
MokaTest使用篇-SQL接口
数据库·sql