3. FSM建模方法:状态图、状态转移表、状态转移矩阵的绘制与编写规范
有限状态机(FSM)与层次状态机(HSM)的建模质量,直接决定了后续代码实现的可读性、可维护性与可验证性。在工程实践中,同一套状态逻辑可以用状态图(State Diagram)、状态转移表(State Transition Table)和状态转移矩阵(State Transition Matrix)三种不同视角来描述。三种建模载体各有侧重:状态图适合表达状态间的层次关系和并发结构,状态转移表适合逐条审查事件与动作的对应关系,状态转移矩阵则适合做完备性检查和自动化验证。本章将系统讲解三种建模方法的绘制规范、编写要点与工程适用场景,帮助读者建立一套可落地、可评审、可追溯的状态建模标准。
3.1 状态图(State Diagram)的绘制规范
3.1.1 核心原理:状态图是状态机的“架构蓝图”
状态图(UML State Machine Diagram)是描述状态机最直观的图形化语言。它以圆角矩形表示状态,以带箭头的有向线段表示状态转移,箭头上标注触发事件与执行动作。状态图的核心价值在于能够清晰表达状态的层次关系(嵌套状态)、并发区域(正交状态)以及历史状态(History State),这是状态转移表和矩阵难以直观呈现的。
在FSM建模中,状态图应遵循以下绘制规范:
- 状态命名:使用“名词+形容词”或“动词过去式”风格,如
IDLE、CONNECTING、DATA_READY,避免使用含义模糊的STATE1、S2等编号。 - 初始状态:使用实心圆点表示,并引出箭头指向初始状态;终止状态使用“牛眼”符号(实心圆外套空心圆)。
- 转移标注:格式为
事件(参数) [守卫条件] / 动作,例如START_REQ [buffer_empty] / init_dma()。守卫条件必须用方括号括起,动作以斜杠开头。 - 层次关系:子状态嵌套在父状态内部,父状态用大圆角矩形包裹子状态;子状态共享父状态的转移。
- 并发区域:用虚线将父状态分割为多个区域,每个区域独立运行,用
fork和join节点表示并发开始与汇合。
3.1.2 操作流程:从需求到状态图的五步法
- 收集事件集:列出所有外部输入、内部定时器超时、硬件中断等触发源,形成事件清单。
- 定义状态集:根据系统行为划分稳定状态,每个状态代表一段可区分的稳定行为窗口。
- 绘制主转移路径:先画正常流程(happy path),再补充异常分支和超时处理。
- 检查完备性:对每个状态逐一检查每个事件是否有对应转移;无转移的事件需显式标注为
IGNORED或INTERNAL。 - 评审与迭代:组织硬件、软件、测试三方评审,确认状态划分与事件语义一致。
某MCU项目需要设计UART接收状态机,支持可变帧长(1~256字节)和CRC校验。初始设计仅定义了 IDLE、RECEIVING、CRC_CHECK 三个状态,但在评审中发现以下问题:
- 缺少
FRAME_ERROR状态,导致接收超时后无法区分“空闲超时”与“帧中断超时”; - 未定义
OVERRUN处理路径,硬件FIFO溢出后状态机仍停留在RECEIVING,造成数据错位; - CRC校验失败后直接回到
IDLE,但未丢弃已接收的帧数据,导致DMA缓冲区残留脏数据。
修正后的状态图增加了 WAIT_FOR_EOT(等待帧结束)和 ERROR_HANDLING 两个状态,并在 CRC_CHECK 失败时先执行 flush_dma_buffer() 动作再转移至 IDLE。评审通过后,该状态图直接映射为C语言中的 typedef enum 和 switch-case 结构,代码行数减少约30%。
3.1.3 常见误区与总结
误区2:忽略“自转移”——状态收到不相关事件时,应显式画出自转移箭头并标注
IGNORED,否则阅读者无法判断是遗漏还是故意忽略。误区3:动作放在状态内部而非转移上——进入状态时执行的
entry 动作、退出时的 exit 动作、状态内的 do 活动应使用标准 UML 标记(entry/、exit/、do/),避免与转移动作混淆。
3.2 状态转移表(State Transition Table)的编写规范
3.2.1 核心原理:表格化的“逐条审查”工具
状态转移表将状态图的图形信息转化为结构化文本,每一行描述一个“当前状态 + 触发事件 → 次态 + 动作”的完整转移规则。其核心优势在于:可逐行审查、便于版本对比、可直接转换为测试用例。在汽车电子、医疗设备等安全关键领域,状态转移表是需求追溯(Traceability)的必要交付物。
标准的状态转移表应包含以下列:
| 当前状态 | 触发事件 | 守卫条件 | 次态 | 动作(输出) | 优先级 |
| IDLE | START_REQ | buffer_empty | RECEIVING | init_dma(); enable_irq() | 1 |
| RECEIVING | BYTE_RCVD | frame_len < 256 | RECEIVING | write_fifo(data) | 1 |
| RECEIVING | EOT_DETECT | — | CRC_CHECK | disable_irq() | 2 |
| CRC_CHECK | CRC_PASS | — | IDLE | notify_app() | 1 |
| CRC_CHECK | CRC_FAIL | — | ERROR | flush_dma(); set_err_flag() | 1 |
3.2.2 编写规范与操作流程
- 状态枚举:在表格顶部注释中列出全部状态和事件的枚举定义,确保名称与代码一致。
- 逐状态穷举:对每个状态,列出所有可能事件的转移行;无转移的事件填写
—或IGNORED。 - 守卫条件明确:守卫条件必须使用可判定的布尔表达式,如
counter > 3,避免使用模糊描述。 - 动作原子化:每个动作应是一个不可分割的操作,如
start_timer(100ms),不要写do_something_and_cleanup()这类复合动作。 - 优先级标注:当同一状态在同一事件下有多条转移(由守卫条件区分)时,必须标注优先级,数字越小优先级越高。
3.2.3 案例拆解:电源管理状态机的表格化
某便携设备电源管理模块包含 ACTIVE、SLEEP、DEEPSLEEP、POWER_OFF 四个状态。初始表格中遗漏了 ACTIVE 状态下收到 LOW_BATTERY 事件的处理,导致系统在低电量时仍保持全速运行。通过逐状态穷举检查,补充了以下转移行:
| ACTIVE | LOW_BATTERY | voltage < 3.3V | SLEEP | save_context(); reduce_cpu_freq() | 1 |
| ACTIVE | LOW_BATTERY | voltage < 3.0V | DEEPSLEEP | save_context(); disable_periph() | 2 |
该补充使得测试团队能够针对不同电压阈值设计边界测试用例,最终在硬件实测中发现了ADC采样误差导致的电压抖动问题,通过增加迟滞比较器逻辑解决。
3.3 状态转移矩阵(State Transition Matrix)的构建方法
3.3.1 核心原理:矩阵化的“完备性检查”利器
状态转移矩阵以行表示当前状态、列表示触发事件,矩阵交叉格填写“次态/动作”。这种二维结构天然支持完备性检查:如果矩阵中存在空格,说明该状态在该事件下没有定义转移,可能是遗漏或故意忽略。矩阵特别适合状态数较多(超过10个)且事件数固定的场景,例如网络协议栈、通信链路控制等。
矩阵的构建规则:
- 行与列的数量分别等于状态数和事件数,矩阵维度为
N × M。 - 每个交叉格填写
次态 / 动作,若为自转移且无动作,填写—或self。 - 若同一交叉格存在多条带守卫条件的转移,可拆分为多个子行,或在格内用分号分隔并标注优先级。
- 矩阵应配套一个“事件-动作”字典,解释每个动作的具体含义和参数。
3.3.2 操作流程:从状态图到矩阵的转换
- 列出所有状态作为矩阵行标题,所有事件作为列标题。
- 逐行扫描状态图,将每个转移填入对应交叉格。
- 检查矩阵空位:对每个空格,确认是“忽略事件”还是“遗漏转移”。
- 使用工具(如Python脚本或Excel宏)自动生成矩阵,并输出未覆盖事件列表。
- 将矩阵与状态转移表交叉验证,确保两者信息完全一致。
以TCP协议状态机为例(简化版),包含 CLOSED、LISTEN、SYN_SENT、ESTABLISHED、FIN_WAIT 五个状态,事件包括 APP_OPEN、SYN_RCVD、ACK_RCVD、APP_CLOSE、FIN_RCVD。构建5×5矩阵后,发现 SYN_SENT 状态下收到 APP_CLOSE 事件未定义转移。查阅RFC 793后确认应转移到 CLOSED 并发送RST报文。该遗漏在代码评审中未被发现,但矩阵检查直接暴露了问题。
矩阵还揭示了另一个设计缺陷:FIN_WAIT 状态下收到 SYN_RCVD 事件(对端同时发起新连接)时,原设计直接忽略,但实际应进入 CLOSING 状态。通过矩阵的“行-列”交叉审视,这些问题在编码前全部修正。
3.3.3 常见误区与总结
误区2:矩阵中填写“复合动作”——例如在格内写
do_a(); do_b(); do_c(),这会使矩阵难以阅读。应将这些动作封装为一个函数名,如 handle_timeout()。误区3:忽略“事件不存在”的语义——矩阵空格应显式标注
N/A(该事件在此状态不可能发生)或 IGNORED(事件发生但忽略),避免歧义。
3.4 三种建模方法的对比与选型
| 维度 | 状态图 | 状态转移表 | 状态转移矩阵 |
| 表达层次/并发 | 强(原生支持) | 弱(需额外标注) | 弱(不适合表达) |
| 逐条审查效率 | 中(需视觉追踪) | 高(逐行阅读) | 高(交叉定位) |
| 完备性检查 | 弱(依赖人工) | 中(需逐状态核对) | 强(空格即问题) |
| 自动化支持 | 中(需解析图形) | 高(CSV/Excel直接处理) | 高(二维数组直接映射) |
| 适用阶段 | 架构设计、评审 | 详细设计、编码 | 验证、测试用例生成 |
| 文档体积 | 小(图形紧凑) | 中(行数=转移数) | 大(N×M固定) |
在实际项目中,推荐采用“状态图为主、转移表为辅、矩阵做验证”的组合策略。状态图用于设计评审和团队沟通,状态转移表用于指导编码实现,状态转移矩阵用于测试用例生成和完备性审计。三种文档应保持同步更新,建议在代码仓库中维护一份 state_machine_spec.xlsx,包含三个工作表(StateDiagram、TransitionTable、TransitionMatrix),并通过CI脚本检查三者的一致性。
3.5 建模规范的综合复盘
本章围绕FSM建模的三种核心载体,建立了从图形化设计到表格化实现再到矩阵化验证的完整方法论。关键要点总结如下:
- 状态图是设计的“源头”,必须清晰表达层次、并发和守卫条件,评审时重点关注状态划分的合理性和转移的完备性。
- 状态转移表是实现的“桥梁”,每一行对应代码中的一个
case分支,动作必须原子化、可命名、可测试。 - 状态转移矩阵是验证的“标尺”,空格即风险,交叉格即测试点,应作为测试用例设计的输入。
- 三种文档必须保持同步,任何状态变更都应同时更新三份文档,并触发回归检查。
在实际工程中,建议在项目启动阶段就建立状态建模规范模板,明确命名规则、表格格式、矩阵维度要求,并纳入代码评审的检查清单。通过本章的学习,读者应能够独立完成一个中等复杂度FSM的三种建模工作,并能够利用矩阵进行完备性验证,从而在编码前消除大部分状态逻辑缺陷。