第二章:状态机理论——有限状态机(FSM)概念、状态与事件定义、状态转移图绘制

好,咱们正式开始啃状态机这块硬骨头。说实话,我做了十几年嵌入式,接触过的心率监测、输液泵、呼吸机,底层核心几乎都离不开状态机。起搏器更是典型中的典型——你想想看,一个巴掌大的设备要在人体内跳上好几年,状态管理要是出了岔子,那可是要出人命的。

这一章,咱们就把状态机最基础的东西掰开揉碎了讲清楚。别嫌基础,我见过太多人栽在概念不清上。

2.1 有限状态机到底是什么?

有限状态机,英文叫 Finite State Machine,简称 FSM。说白了,就是一个系统在任意时刻只能处于「有限个状态」中的某一个。系统根据外部事件,从一个状态跳到另一个状态。

举个例子,你家里的电灯开关。它只有两个状态:开和关。你按一下(事件),它就切换一次。这就是最简单的状态机。

起搏器呢?状态就多了。比如:

  • 待机状态:等待心跳信号
  • 感知状态:检测到自身心跳,开始计时
  • 起搏状态:没检测到心跳,发放电脉冲
  • 不应期状态:刚发完脉冲,暂时不响应任何信号

嗯,这里要注意:状态的数量必须是「有限」的。你不能说「系统处于某种未知状态」,那在医疗设备里就是灾难。我在项目中遇到过,某个起搏器原型在测试时莫名其妙进入了未定义状态,结果连续发放了三次脉冲——还好是在体外测试,否则后果不堪设想。

核心要点:FSM 的三个基本要素——状态、事件、转移。缺一不可。

2.2 状态的定义——别含糊

定义状态这件事,看起来简单,做起来全是坑。我个人习惯,每个状态必须回答三个问题:

  1. 这个状态下,系统在做什么?(比如:等待、计时、输出)
  2. 这个状态下,系统忽略什么?(比如:不应期内忽略所有感知事件)
  3. 这个状态下,超时了怎么办?(比如:等待超时,进入起搏状态)

拿起搏器的一个典型状态来举例:

状态名称 VVI 模式下的「心室不应期」状态
做什么 等待不应期计时结束
忽略什么 所有心室感知事件(防止误触发)
超时处理 计时结束 → 进入「心室待机」状态

你看,这样定义下来,每个状态都是「有边界」的。我曾经见过一个团队,把「感知到信号后处理中」定义成了一个状态,结果这个状态里既要做计时又要做滤波还要做决策,最后代码乱成一锅粥。记住:状态要单一职责。

2.3 事件的定义——信号还是动作?

事件,就是触发状态转移的那个「东西」。在起搏器里,事件通常分两类:

  • 外部事件:比如心电信号、传感器中断、定时器超时
  • 内部事件:比如某个计算完成、某个标志位被置位

我建议你把事件定义成「不可再分的原子动作」。举个例子,「检测到R波」是一个事件,但「检测到R波并且确认不是噪声」这就不是原子事件了——它包含了两个步骤。正确的做法是:先检测到R波(事件1),然后进入确认状态,确认完成后(事件2)再转移。

我的经验:事件命名用「动词+名词」的格式,比如:evR波检测到ev定时器超时ev起搏脉冲发出。这样读代码时一目了然。

2.4 状态转移图——画出来再说

状态转移图,是状态机设计的「蓝图」。我个人的工作流程是:先画图,再写代码。图没画清楚,代码肯定写不好。

画图时,有几个约定俗成的符号:

  • 圆圈/圆角矩形:表示状态
  • 箭头:表示转移方向
  • 箭头上的标签:事件/条件
  • 实心圆点:表示初始状态

咱们画一个最简单的起搏器状态转移图(VVI模式,单腔心室):

        [初始状态]
            |
            v
    +------------------+
    |  心室待机状态     |  <--- 等待心跳或超时
    +------------------+
       |            |
       | (感知到R波) | (超时)
       v            v
    +------------------+    +------------------+
    |  心室感知状态     |    |  心室起搏状态     |
    |  (开始计时)       |    |  (发放脉冲)       |
    +------------------+    +------------------+
       |            |            |
       | (计时结束)  |            | (脉冲发出)
       v            v            v
    +------------------+    +------------------+
    |  心室不应期状态   |    |  心室不应期状态   |
    |  (忽略所有感知)   |    |  (忽略所有感知)   |
    +------------------+    +------------------+
            |                       |
            | (不应期结束)           | (不应期结束)
            +----------+------------+
                       v
                +------------------+
                |  心室待机状态     |
                +------------------+

你看,这个图里每个状态都有明确的入口和出口。没有「悬空」的箭头,也没有「自循环」没标注条件的情况。

避坑指南:我曾经在画图时漏掉了一个「超时转移」,结果代码写完后测试发现,起搏器在某个状态下卡死了——因为没有定义超时后的去向。从那以后,我要求每个状态必须至少有一个「兜底转移」,通常是超时或错误处理。

2.5 状态转移表——另一种视角

除了画图,我还喜欢用状态转移表来辅助设计。表格的好处是:不容易遗漏组合情况。

当前状态 事件 下一状态 动作
心室待机 感知到R波 心室感知 启动感知计时器
心室待机 超时 心室起搏 发放起搏脉冲
心室感知 计时结束 心室不应期 启动不应期计时器
心室起搏 脉冲发出 心室不应期 启动不应期计时器
心室不应期 计时结束 心室待机

你想想看,如果某个状态+事件的组合在表里找不到,那代码里就会出 bug。所以画完图之后,我建议你再用表格过一遍,确保每个格子都填满了。

2.6 状态机设计的几个原则

最后,分享几个我这些年总结出来的原则:

  • 状态不要太多:一个状态机超过10个状态,就要考虑分层了。起搏器通常分主状态机和子状态机。
  • 事件要正交:两个事件不能同时发生。如果可能同时发生,需要定义优先级。
  • 转移要确定:同一个状态+同一个事件,只能有一个确定的下一状态。不能有歧义。
  • 要有初始状态:系统上电后,必须进入一个已知的初始状态。起搏器通常是「安全模式」或「待机模式」。

一句话总结:状态机设计,就是给系统画一张「地图」。每个状态是一个地点,每个事件是一条路。地图画清楚了,代码自然就写顺了。

好,这一章就到这里。下一章咱们会把这些理论落地到代码里,用 C 语言实现一个真正的起搏器状态机。到时候你会发现,画图时多花的一小时,写代码时能省下三天。