状态机设计文档:状态图绘制、状态转移表与设计说明书
做嵌入式开发这么多年,我越来越觉得一个道理:代码写得好不好,往往在设计文档阶段就决定了。状态机这东西,说白了就是“画图+填表+写说明”三件套。你想想看,如果连状态图都画不清楚,那代码写出来八成也是乱的。
今天咱们就聊聊这三样东西怎么做。我个人习惯是:先画图、再填表、最后写说明书。这个顺序别搞反了,因为状态图是给人看的,转移表是给机器看的,说明书是给团队看的。
一、UML状态图:把逻辑画出来
UML状态图,我刚开始用的时候也觉得麻烦。后来有一次,一个同事拿着他画的“蜘蛛网”来找我debug,我看了半小时没看懂。从那以后,我就坚持用标准UML画法。
一个标准的状态图,至少要有这几样东西:
- 起始点:实心圆,表示状态机从哪开始
- 状态框:圆角矩形,里面写状态名
- 转移箭头:带箭头的线,上面写触发事件
- 终止点:实心圆加外圈,表示结束
举个例子,一个简单的LED灯控制状态机:
// 状态定义
typedef enum {
STATE_OFF, // 关灯
STATE_ON, // 开灯
STATE_BLINK, // 闪烁
STATE_ERROR // 错误
} LedState_t;
// 事件定义
typedef enum {
EVT_BUTTON_PRESS, // 按键按下
EVT_TIMEOUT, // 超时
EVT_FAULT, // 故障
EVT_RESET // 复位
} LedEvent_t;
对应的UML状态图,我画了一个SVG版本,你看一眼就明白了:
画图要点:
- 每个状态只画一个框,别搞嵌套(除非你用的是层次状态机)
- 转移箭头上必须写事件名,别偷懒
- 如果转移太多,考虑用“else”分支简化
二、状态转移表:把逻辑填进去
状态图画完了,接下来就是填表。我见过很多人直接跳过这步,结果代码里一堆bug。其实状态转移表就是状态图的“表格版”,更精确、更不容易遗漏。
标准的转移表长这样:
| 当前状态 | 事件 | 动作 | 下一状态 |
|---|---|---|---|
| STATE_OFF | BUTTON_PRESS | 开灯 | STATE_ON |
| FAULT | 报警 | STATE_ERROR | |
| 其他 | 无 | STATE_OFF | |
| STATE_ON | BUTTON_PRESS | 关灯 | STATE_OFF |
| TIMEOUT | 启动定时器 | STATE_BLINK | |
| FAULT | 报警 | STATE_ERROR | |
| STATE_BLINK | BUTTON_PRESS | 开灯 | STATE_ON |
| TIMEOUT | 关灯 | STATE_OFF | |
| FAULT | 报警 | STATE_ERROR | |
| STATE_ERROR | RESET | 关灯、清报警 | STATE_OFF |
| 其他 | 无 | STATE_ERROR |
我的填表习惯:
- 每一行就是一个“状态+事件”的组合,别合并单元格(除非你确定不会漏)
- 动作列要写清楚“做什么”,比如“开灯”而不是“处理事件”
- 最后一行一定要写“其他”事件,这是兜底逻辑
你可能会问:为什么要有“其他”这一行?嗯,我在一个项目中吃过亏。当时有个状态机,漏了一个事件没处理,结果系统跑飞了。从那以后,我每个状态都加一个“其他”兜底,哪怕只是原地不动。
三、设计说明书:把逻辑讲清楚
状态图和转移表都有了,但还不够。为什么?因为图是给人看的,表是给机器看的,但说明书是给团队看的。你想想看,如果新同事接手你的代码,光看图能看懂吗?
一份好的设计说明书,至少包含这几部分:
- 概述:这个状态机是干什么的?用在哪个模块?
- 状态定义:每个状态的含义、进入条件、退出条件
- 事件定义:每个事件的触发源、触发时机
- 转移规则:核心转移逻辑,尤其是那些“容易搞混”的
- 边界情况:比如同时来两个事件怎么办?
- 测试用例:怎么验证这个状态机是对的?
举个例子,LED控制状态机的说明书片段:
## 1. 概述
本状态机用于控制LED灯的三种模式:关、开、闪烁。
当检测到故障时,进入错误状态并报警。
## 2. 状态说明
- STATE_OFF:LED熄灭。进入时执行关灯操作。
- STATE_ON:LED常亮。进入时执行开灯操作。
- STATE_BLINK:LED以1Hz频率闪烁。进入时启动定时器。
- STATE_ERROR:错误状态。进入时启动蜂鸣器报警。
## 3. 事件说明
- BUTTON_PRESS:按键按下,上升沿触发。
- TIMEOUT:定时器超时,1秒周期。
- FAULT:硬件故障检测,电平触发。
- RESET:复位信号,下降沿触发。
## 4. 边界情况
- 如果在STATE_BLINK状态下同时收到BUTTON_PRESS和TIMEOUT,
优先处理BUTTON_PRESS(按键优先级高于定时器)。
- 如果在STATE_ERROR状态下收到FAULT,忽略(已在错误状态)。
写说明书最容易犯的错:
- 只写“做什么”,不写“为什么”——这是最要命的
- 边界情况一笔带过——我曾经因为这个被测试同事追着问了一周
- 测试用例写得太笼统——比如“测试正常流程”,等于没写
四、三件套的关系
最后总结一下这三样东西的关系:
- 状态图:给你自己看的,快速理清逻辑
- 转移表:给代码生成用的,精确无歧义
- 说明书:给团队看的,解释为什么这么设计
我个人习惯是:先画图,再填表,最后写说明书。画图的时候发现逻辑漏洞,填表的时候发现遗漏事件,写说明书的时候发现设计不合理。这三步走下来,代码基本不会有大问题。
嗯,今天就聊到这儿。记住一句话:设计文档不是写给领导看的,是写给未来的自己看的。你三个月后回来看自己的代码,如果看不懂,那说明文档没写好。
公众号:蓝海资料掘金营,微信deep3321