2. 状态机基础理论:有限状态机(FSM)概念、状态、事件、转移、动作
好,咱们正式开始聊状态机。说实话,我最早接触状态机是在做一款工业仪表的时候。那时候代码写得那叫一个乱——if-else 套了七八层,改一个 bug 能引出三个新 bug。后来一位老前辈甩给我一句话:「小伙子,去学学状态机吧。」嗯,从那以后,我的嵌入式生涯才算真正开了窍。
这一节,咱们就把状态机最核心的几个概念掰开揉碎。你只要把这几个东西吃透了,后面写血压计的状态机就跟搭积木一样简单。
2.1 什么是有限状态机?
有限状态机,英文叫 Finite State Machine,简称 FSM。说白了,就是一个系统在任何时刻只能处于「有限个状态」中的某一个。它不能同时处于两个状态,也不能处于一个未定义的状态。
举个例子,你家里的电灯开关。它只有两个状态:开和关。你按一下,它就切换一次。这就是一个最简单的状态机。
我习惯把状态机比作一个「有记忆的自动售货机」。你投币、选商品、取货,每一步都对应一个状态。机器不会因为你投了币就直接出货——它得先确认你投了多少钱,然后等你选商品,最后才出货。这个过程,就是状态机在背后控制着。
核心要点:有限状态机 = 有限个状态 + 状态之间的转移规则。系统在任何时刻只能处于一个状态,并且只能通过「事件」触发「转移」到另一个状态。
2.2 状态(State)
状态,是状态机最基础的概念。它描述的是系统在某一时刻的「稳定情况」。在血压计里,状态就是设备正在做什么——是待机、正在充气、正在测量,还是正在显示结果。
我在项目中遇到过一个问题:有人把「正在充气」和「正在测量」合并成一个状态。结果呢?充气过程中如果用户按了停止键,设备不知道是该停止充气还是继续测量。你看,状态划分不清晰,后面全是坑。
设计状态时,我建议你记住三个原则:
- 互斥性:同一时刻只能处于一个状态。不能既在充气又在测量。
- 完备性:所有可能的情况都要有对应的状态。比如「电池电量低」也得算一个状态。
- 稳定性:状态是稳定的,不会自己跳变。只有事件来了,才会转移。
咱们血压计里常见的状态,我列个表给你看:
| 状态名称 | 描述 | 典型行为 |
|---|---|---|
| IDLE | 空闲待机 | 屏幕熄灭,等待按键或定时唤醒 |
| INIT | 初始化 | 自检传感器、校准零点 |
| INFLATING | 正在充气 | 启动气泵,监测压力值 |
| MEASURING | 正在测量 | 采集脉搏波,计算血压值 |
| DEFLATING | 正在放气 | 打开放气阀,缓慢降压 |
| DISPLAY | 显示结果 | 在屏幕上显示收缩压/舒张压 |
| ERROR | 错误状态 | 显示错误码,等待用户处理 |
2.3 事件(Event)
事件,就是触发状态转移的那个「导火索」。没有事件,状态机就永远待在当前状态,一动不动。
事件可以是外部的,比如用户按了一个按钮、传感器检测到压力超限。也可以是内部的,比如定时器超时、数据计算完成。
我习惯把事件分成三类:
- 外部事件:来自用户或外设。比如按键按下、蓝牙连接断开。
- 内部事件:来自系统自身。比如定时器溢出、ADC转换完成。
- 超时事件:一种特殊的内部事件。比如充气超过5秒压力还没到,就触发超时事件。
你想想看,如果事件定义得不够细,会出什么问题?我曾经见过一个项目,把「按键按下」和「按键长按」混成一个事件。结果短按和长按的处理逻辑打架,设备时不时抽风。所以,事件一定要定义得清晰、唯一。
我的小技巧:给每个事件起一个见名知意的名字。比如 EVT_BUTTON_PRESSED、EVT_PRESSURE_OVERFLOW、EVT_TIMEOUT。别用 EVT_01、EVT_02 这种,三个月后你自己都看不懂。
2.4 转移(Transition)
转移,就是状态机从一个状态切换到另一个状态的过程。它由三部分组成:当前状态 + 触发事件 + 下一个状态。
用数学语言说:转移 = (当前状态, 事件) → 下一个状态。
举个例子:
// 伪代码示例
// 当前状态: IDLE
// 触发事件: EVT_START_BUTTON
// 下一个状态: INIT
if (current_state == IDLE && event == EVT_START_BUTTON) {
current_state = INIT;
// 执行进入INIT状态的动作
init_sensors();
calibrate_zero();
}
这里有个关键点:转移是「瞬间完成」的。也就是说,状态机不会在转移过程中停下来。它要么在状态A,要么在状态B,没有中间态。
嗯,这里要注意:转移不一定非要改变状态。有时候事件来了,但条件不满足,状态机可以「自转移」——也就是留在当前状态,只执行一些动作。比如在 MEASURING 状态,收到一个无效的传感器数据,你可以忽略它,继续测量。
2.5 动作(Action)
动作,是状态机在转移过程中执行的具体操作。它是状态机的「输出」。没有动作的状态机,就像一台只亮灯不干活的机器——光有状态变化,没有实际功能。
动作可以发生在三个时机:
- 进入状态时:比如进入 INIT 状态时,执行传感器初始化。
- 离开状态时:比如离开 INFLATING 状态时,关闭气泵。
- 转移过程中:比如从 MEASURING 转移到 DISPLAY 时,把计算结果存入显示缓冲区。
我个人的习惯是:把「进入动作」和「离开动作」写得清清楚楚。这样代码可读性极高,别人一看就知道「哦,进这个状态要干啥,出这个状态要干啥」。
避坑指南:我曾经在一个项目里,把动作写在了转移条件里。结果呢?同一个动作在多个转移路径里重复执行,导致资源浪费和逻辑混乱。后来我强制自己:动作只写在状态的进入/离开函数里,转移只负责「选路」,不负责「干活」。
2.6 一个完整的例子
咱们把上面这些概念串起来,看一个血压计里最简单的状态机片段:
// 状态定义
typedef enum {
STATE_IDLE,
STATE_INIT,
STATE_INFLATING,
STATE_MEASURING,
STATE_DISPLAY,
STATE_ERROR
} state_t;
// 事件定义
typedef enum {
EVT_START_BUTTON,
EVT_INIT_DONE,
EVT_PRESSURE_READY,
EVT_MEASURE_DONE,
EVT_TIMEOUT,
EVT_ERROR
} event_t;
// 状态转移表
state_t transition(state_t current, event_t evt) {
switch (current) {
case STATE_IDLE:
if (evt == EVT_START_BUTTON) return STATE_INIT;
break;
case STATE_INIT:
if (evt == EVT_INIT_DONE) return STATE_INFLATING;
if (evt == EVT_ERROR) return STATE_ERROR;
break;
case STATE_INFLATING:
if (evt == EVT_PRESSURE_READY) return STATE_MEASURING;
if (evt == EVT_TIMEOUT) return STATE_ERROR;
break;
// ... 其他状态处理
default:
return STATE_ERROR;
}
return current; // 没有匹配的转移,留在当前状态
}
你看,这个结构多清晰。每个状态只处理它关心的事件,不关心的事件直接忽略。这就是状态机的魅力——逻辑简单、容易调试、不容易出 bug。
2.7 小结
好了,这一节咱们把状态机的五个核心概念捋了一遍。状态、事件、转移、动作,这四个词你记住了,状态机就算入门了。
下一节,我会带你看看状态机的两种实现方式——switch-case 和状态表。到时候咱们再聊哪种更适合血压计这种嵌入式系统。
嗯,先消化这些吧。有问题随时问我。