3. FSM建模方法:状态图、状态转移表、状态转移矩阵的绘制与编写规范

有限状态机(FSM)与层次状态机(HSM)的建模质量,直接决定了后续代码实现的可读性、可维护性与可验证性。在工程实践中,同一套状态逻辑可以用状态图(State Diagram)、状态转移表(State Transition Table)和状态转移矩阵(State Transition Matrix)三种不同视角来描述。三种建模载体各有侧重:状态图适合表达状态间的层次关系和并发结构,状态转移表适合逐条审查事件与动作的对应关系,状态转移矩阵则适合做完备性检查和自动化验证。本章将系统讲解三种建模方法的绘制规范、编写要点与工程适用场景,帮助读者建立一套可落地、可评审、可追溯的状态建模标准。

FSM 三种建模视角的工程关系 状态机需求/规格 事件集 + 状态集 + 动作集 状态图(State Diagram) · 状态圆圈 + 转移箭头 · 层次嵌套 / 并发区域 · 适合设计评审与架构沟通 状态转移表(Transition Table) · 当前态 + 事件 → 次态/动作 · 逐条审查,便于代码映射 · 适合详细设计与测试用例编写 状态转移矩阵(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 操作流程:从需求到状态图的五步法

  1. 收集事件集:列出所有外部输入、内部定时器超时、硬件中断等触发源,形成事件清单。
  2. 定义状态集:根据系统行为划分稳定状态,每个状态代表一段可区分的稳定行为窗口。
  3. 绘制主转移路径:先画正常流程(happy path),再补充异常分支和超时处理。
  4. 检查完备性:对每个状态逐一检查每个事件是否有对应转移;无转移的事件需显式标注为 IGNORED 或 INTERNAL。
  5. 评审与迭代:组织硬件、软件、测试三方评审,确认状态划分与事件语义一致。
案例拆解:UART 接收模块的状态图设计

某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 常见误区与总结

误区1:状态图过于“扁平化”——所有状态画在同一层,导致转移箭头交叉混乱。应利用层次状态机(HSM)将公共行为提取到父状态。
误区2:忽略“自转移”——状态收到不相关事件时,应显式画出自转移箭头并标注 IGNORED,否则阅读者无法判断是遗漏还是故意忽略。
误区3:动作放在状态内部而非转移上——进入状态时执行的 entry 动作、退出时的 exit 动作、状态内的 do 活动应使用标准 UML 标记(entry/、exit/、do/),避免与转移动作混淆。

3.2 状态转移表(State Transition Table)的编写规范

3.2.1 核心原理:表格化的“逐条审查”工具

状态转移表将状态图的图形信息转化为结构化文本,每一行描述一个“当前状态 + 触发事件 → 次态 + 动作”的完整转移规则。其核心优势在于:可逐行审查、便于版本对比、可直接转换为测试用例。在汽车电子、医疗设备等安全关键领域,状态转移表是需求追溯(Traceability)的必要交付物。

标准的状态转移表应包含以下列:

当前状态触发事件守卫条件次态动作(输出)优先级
IDLESTART_REQbuffer_emptyRECEIVINGinit_dma(); enable_irq()1
RECEIVINGBYTE_RCVDframe_len < 256RECEIVINGwrite_fifo(data)1
RECEIVINGEOT_DETECT—CRC_CHECKdisable_irq()2
CRC_CHECKCRC_PASS—IDLEnotify_app()1
CRC_CHECKCRC_FAIL—ERRORflush_dma(); set_err_flag()1

3.2.2 编写规范与操作流程

  1. 状态枚举:在表格顶部注释中列出全部状态和事件的枚举定义,确保名称与代码一致。
  2. 逐状态穷举:对每个状态,列出所有可能事件的转移行;无转移的事件填写 — 或 IGNORED。
  3. 守卫条件明确:守卫条件必须使用可判定的布尔表达式,如 counter > 3,避免使用模糊描述。
  4. 动作原子化:每个动作应是一个不可分割的操作,如 start_timer(100ms),不要写 do_something_and_cleanup() 这类复合动作。
  5. 优先级标注:当同一状态在同一事件下有多条转移(由守卫条件区分)时,必须标注优先级,数字越小优先级越高。
实战经验:在编写状态转移表时,建议使用Excel或CSV格式管理,并设置数据有效性校验。例如,在“次态”列设置下拉列表,只能选择已定义的状态名,从工具层面防止拼写错误。同时,利用条件格式高亮“当前状态=次态”的自转移行,便于快速识别轮询型状态。

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 操作流程:从状态图到矩阵的转换

  1. 列出所有状态作为矩阵行标题,所有事件作为列标题。
  2. 逐行扫描状态图,将每个转移填入对应交叉格。
  3. 检查矩阵空位:对每个空格,确认是“忽略事件”还是“遗漏转移”。
  4. 使用工具(如Python脚本或Excel宏)自动生成矩阵,并输出未覆盖事件列表。
  5. 将矩阵与状态转移表交叉验证,确保两者信息完全一致。
案例拆解:TCP连接状态机的矩阵验证

以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 常见误区与总结

误区1:矩阵与状态图不一致——修改状态图后忘记同步更新矩阵,导致文档漂移。应建立“状态图→转移表→矩阵”的单向生成关系,以状态图为准,表格和矩阵由工具自动生成。
误区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脚本检查三者的一致性。

工程建议:对于状态数超过20个或事件数超过15个的复杂状态机,建议使用开源工具(如 Yakindu Statechart Tools、SinelaboreRT)自动生成状态图、表格和代码骨架。工具生成的表格天然保证一致性,且支持状态图到C/C++代码的直接转换,可减少约40%的手工建模工作量。

3.5 建模规范的综合复盘

本章围绕FSM建模的三种核心载体,建立了从图形化设计到表格化实现再到矩阵化验证的完整方法论。关键要点总结如下:

  • 状态图是设计的“源头”,必须清晰表达层次、并发和守卫条件,评审时重点关注状态划分的合理性和转移的完备性。
  • 状态转移表是实现的“桥梁”,每一行对应代码中的一个 case 分支,动作必须原子化、可命名、可测试。
  • 状态转移矩阵是验证的“标尺”,空格即风险,交叉格即测试点,应作为测试用例设计的输入。
  • 三种文档必须保持同步,任何状态变更都应同时更新三份文档,并触发回归检查。

在实际工程中,建议在项目启动阶段就建立状态建模规范模板,明确命名规则、表格格式、矩阵维度要求,并纳入代码评审的检查清单。通过本章的学习,读者应能够独立完成一个中等复杂度FSM的三种建模工作,并能够利用矩阵进行完备性验证,从而在编码前消除大部分状态逻辑缺陷。