状态机设计文档:状态图绘制、状态转移表与设计说明书

做嵌入式开发这么多年,我越来越觉得一个道理:代码写得好不好,往往在设计文档阶段就决定了。状态机这东西,说白了就是“画图+填表+写说明”三件套。你想想看,如果连状态图都画不清楚,那代码写出来八成也是乱的。

今天咱们就聊聊这三样东西怎么做。我个人习惯是:先画图、再填表、最后写说明书。这个顺序别搞反了,因为状态图是给人看的,转移表是给机器看的,说明书是给团队看的。

一、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版本,你看一眼就明白了:

LED控制状态机 - UML状态图 STATE_OFF entry: 关灯 STATE_ON entry: 开灯 STATE_BLINK entry: 启动定时器 STATE_ERROR entry: 报警 BUTTON_PRESS BUTTON_PRESS TIMEOUT BUTTON_PRESS TIMEOUT FAULT FAULT RESET POWER_OFF

画图要点:

  • 每个状态只画一个框,别搞嵌套(除非你用的是层次状态机)
  • 转移箭头上必须写事件名,别偷懒
  • 如果转移太多,考虑用“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

我的填表习惯:

  • 每一行就是一个“状态+事件”的组合,别合并单元格(除非你确定不会漏)
  • 动作列要写清楚“做什么”,比如“开灯”而不是“处理事件”
  • 最后一行一定要写“其他”事件,这是兜底逻辑

你可能会问:为什么要有“其他”这一行?嗯,我在一个项目中吃过亏。当时有个状态机,漏了一个事件没处理,结果系统跑飞了。从那以后,我每个状态都加一个“其他”兜底,哪怕只是原地不动。

三、设计说明书:把逻辑讲清楚

状态图和转移表都有了,但还不够。为什么?因为图是给人看的,表是给机器看的,但说明书是给团队看的。你想想看,如果新同事接手你的代码,光看图能看懂吗?

一份好的设计说明书,至少包含这几部分:

  1. 概述:这个状态机是干什么的?用在哪个模块?
  2. 状态定义:每个状态的含义、进入条件、退出条件
  3. 事件定义:每个事件的触发源、触发时机
  4. 转移规则:核心转移逻辑,尤其是那些“容易搞混”的
  5. 边界情况:比如同时来两个事件怎么办?
  6. 测试用例:怎么验证这个状态机是对的?

举个例子,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