第三课:状态图绘制——从符号到实战

好,咱们进入第三课。前两课我们把状态机的基本概念和分类聊透了,今天来点动手的——画状态图。

你可能觉得画图嘛,有啥难的?我当年也这么想。直到有一次,我画了个自以为完美的状态图,结果评审会上被老工程师问得哑口无言。嗯,从那以后我才明白,状态图不是随便画画就行的,它有自己的一套规矩。

状态图的基本符号

先说说最核心的几个符号。说白了,状态图就是用图形把状态机的逻辑画出来。你只要掌握下面这几种,就能应付绝大多数场景。

符号 名称 说明
圆角矩形 状态 表示系统的一个稳定状态,内部写状态名称
实心圆 起始点 状态机的初始位置,每个状态图只有一个
带边框的实心圆 终止点 表示状态机结束,可选
箭头 转移 从一个状态到另一个状态的路径
箭头上的标签 事件/条件 触发转移的条件,格式:事件[条件]/动作

我个人习惯把状态名称写在圆角矩形的正中间,字体加粗。事件标签写在箭头旁边,用斜杠分隔。举个例子:

// 状态图上的标签写法
按键按下[按键值==确认键]/蜂鸣器响一声

这个格式的意思是:当按键按下事件发生,并且按键值等于确认键时,执行蜂鸣器响一声的动作,然后转移到下一个状态。

关键点:状态图里的每个转移,必须明确三个要素——触发事件、转移条件、执行动作。缺一个,你的图就可能产生歧义。

绘制规范——别踩这些坑

我在项目中遇到过不少状态图,画得花里胡哨,但根本没法用。这里总结几条硬性规范,你照着做,至少不会出大错。

1. 状态命名要见名知意

别用S1、S2、S3这种编号。你想想看,三个月后你自己回来看,能记得S3是干啥的吗?我建议用动词+名词的组合,比如:

  • 等待按键
  • 读取传感器
  • 发送数据
  • 错误处理

2. 转移条件要写完整

很多人只写事件,不写条件。比如只写「按键按下」,但没说明按哪个键。这在简单系统里可能没问题,但复杂系统里就会出乱子。

我的习惯:事件用名词+动词,条件用方括号括起来,动作用斜杠引导。比如:定时器超时[计数值>=10]/启动电机。

3. 避免交叉线

状态图里最忌讳的就是线绕来绕去。我曾经见过一张图,箭头交叉了七八次,评审时没人能看懂。解决办法很简单:

  • 把相关的状态放在一起
  • 用子状态图拆分复杂逻辑
  • 实在避免不了,用连接点(小圆圈加标签)

4. 每个状态都要有出口

嗯,这里要注意。除了终止状态,每个状态至少应该有一个出口转移。否则你的系统就会卡死在那里。我调试过一个设备,就是因为在某个状态漏画了超时转移,导致设备死机。

避坑指南:我曾经在一个项目里,画状态图时漏掉了「初始化失败」的转移路径。结果设备上电后,如果初始化失败,就永远卡在初始化状态,连错误提示都出不来。后来我养成了习惯:画完图后,逐个状态检查出口,确保没有死胡同。

使用draw.io绘制状态图

工具方面,我个人最推荐draw.io。免费、开源、在线离线都能用。而且它内置了UML状态图的模板,省事不少。

第一步:新建画布

打开draw.io,选择「空白图表」或者「UML」分类下的「状态图」模板。我建议用空白图自己画,这样更灵活。

第二步:拖拽形状

左侧形状面板里,找到这些:

  • 圆角矩形:在「通用」分类里,叫「圆角矩形」
  • 实心圆:在「通用」分类里,叫「圆形」,把填充色改成黑色
  • 箭头:在「通用」分类里,叫「箭头」

如果你用的是UML模板,它会自动提供状态图的专用形状,更方便。

第三步:连接状态

把鼠标放到状态图形的边缘,会出现连接点。拖拽到另一个状态上,就会自动生成箭头。双击箭头,可以输入事件标签。

小技巧:按住Ctrl键拖拽,可以快速复制状态。我画复杂状态图时,都是先画好一个状态,然后Ctrl+拖拽复制,再改名字,效率高很多。

第四步:添加注释

有些逻辑用图形表达不清楚,可以加注释框。draw.io里有个「注释」形状,像个便签纸。我习惯在注释里写:

  • 状态变量的取值范围
  • 特殊情况的处理逻辑
  • 与硬件相关的时序要求

实战:画一个按键消抖状态图

光说不练假把式。咱们拿一个实际例子来画——按键消抖。这是嵌入式开发里最常见的场景之一。

按键按下时,由于机械特性,会产生抖动。我们需要一个状态机来过滤掉这些抖动。

状态定义:
- IDLE:空闲状态,等待按键按下
- DEBOUNCE_PRESS:消抖中,确认按下
- PRESSED:按键已按下,等待释放
- DEBOUNCE_RELEASE:消抖中,确认释放

转移条件:
- IDLE -> DEBOUNCE_PRESS:检测到按键电平为低(按下)
- DEBOUNCE_PRESS -> PRESSED:连续采样10ms均为低
- DEBOUNCE_PRESS -> IDLE:采样到高电平(抖动)
- PRESSED -> DEBOUNCE_RELEASE:检测到按键电平为高(释放)
- DEBOUNCE_RELEASE -> IDLE:连续采样10ms均为高
- DEBOUNCE_RELEASE -> PRESSED:采样到低电平(抖动)

用draw.io画出来,就是四个圆角矩形,用箭头连起来。每个箭头写上对应的事件和条件。画完之后,你一眼就能看出整个消抖逻辑,比看代码直观多了。

我的经验:画完状态图后,我会用纸笔走一遍所有可能的路径。比如按键在消抖过程中突然被松开怎么办?在按下状态又快速按了一下怎么办?把这些边界情况走一遍,能发现很多设计漏洞。

总结

状态图绘制,说白了就是把脑子里的逻辑搬到纸上。符号就那么几个,规范也就那么几条,真正考验人的是逻辑的完整性和清晰度。

我建议你从现在开始,每次写状态机代码之前,先画状态图。哪怕只是草稿,也能帮你理清思路。等你画上十张八张,就会发现——嗯,这东西真香。

下一课,咱们开始写真正的状态机代码。准备好了吗?