2. 状态机基础:状态、转移、事件、动作。摩尔型与米利型

好,我们正式开始聊状态机。说实话,很多工程师干了几年,状态机写得飞快,但真让他把基本概念讲清楚——嗯,反而有点含糊。我见过不少面试者,代码能跑,但问起「你的输出到底跟时钟沿什么关系」,就开始挠头。

今天这一章,我们就来把这些基础夯实。状态、转移、事件、动作,这四个词说白了就是状态机的全部。外加摩尔型和米利型这哥俩的区别,你搞明白了,后面写训练序列才不心虚。

2.1 状态机的四个核心要素

我个人习惯把状态机看作一个「有记忆的控制逻辑」。它不像组合逻辑那样,输入一变输出马上变。它得记住自己现在在哪,下一步要去哪。

  • 状态(State):就是状态机当前所处的位置。好比一个人站在哪个路口。
  • 事件(Event):触发转移的原因。可能是某个管脚电平跳变,也可能是内部计数器的溢出。
  • 转移(Transition):满足条件后,从当前状态跳转到下一个状态。
  • 动作(Action):进入某个状态时,或者转移过程中,执行的具体操作。比如拉高某个信号、置位一个标志位。
我的避坑指南: 我曾经在一个通信协议项目里,把「事件」和「动作」混在一起写。结果状态机跑起来,逻辑乱得自己都不想看。后来我强制自己分开:事件只管「要不要走」,动作只管「走了之后干什么」。代码立马清爽了。

来看一段简单的状态机代码,我用 Python 写个示意,你们感受一下结构:

class SimpleFSM:
    def __init__(self):
        self.state = "IDLE"
    
    def on_event(self, event):
        if self.state == "IDLE":
            if event == "START":
                self.action_start()
                self.state = "BUSY"
        elif self.state == "BUSY":
            if event == "DONE":
                self.action_done()
                self.state = "IDLE"
    
    def action_start(self):
        print("动作:启动处理")
    
    def action_done(self):
        print("动作:完成处理")

看到没有?状态、事件、转移、动作,四个东西各司其职。你别嫌简单,很多复杂状态机的大框架,底层就是这么个朴素的玩法。

2.2 摩尔型 vs 米利型

这是我每次讲课都重点对比的。你想想看,同样是状态机,为什么有的输出跟时钟沿同步,有的却对输入变化敏感?

对比项 摩尔型(Moore) 米利型(Mealy)
输出决定因素 只由当前状态决定 由当前状态 + 当前输入共同决定
输出时机 状态转移后,下一个时钟沿输出 输入变化的瞬间,输出可能立刻变化
安全性 更安全,输出稳定无毛刺 有风险,组合逻辑可能产生毛刺
状态数 通常需要更多状态来描述同一行为 可以用更少状态实现相同功能
典型场景 控制信号、寄存器写使能 高速数据传输、握手协议
注意: 很多初学者以为米利型比摩尔型「高级」,其实不然。我见过一个做 SPI 控制器的人,非要全用米利型,结果输出毛刺导致从机采样频频出错。后来换成摩尔型,问题直接消失。选型不是比谁厉害,是比谁更适合场景。

2.3 两种模型的代码对比

我们写两个简单的例子,一个是摩尔型检测上升沿,一个是米利型检测上升沿。你一看就懂了。

摩尔型上升沿检测

# 摩尔型:输出只跟当前状态有关
# 状态 S0: 低电平
# 状态 S1: 高电平
def moore_rising_edge(clk, din):
    if clk & 0x1:  # 时钟上升沿
        if state == "S0":
            if din == 1:
                state = "S1"
                out = 1  # 进入S1时输出1
            else:
                out = 0
        elif state == "S1":
            if din == 0:
                state = "S0"
            out = 0  # 在S1时输出0
    return out

米利型上升沿检测

# 米利型:输出由当前状态和输入共同决定
# 状态 S0: 低电平
# 状态 S1: 高电平
def mealy_rising_edge(clk, din):
    if clk & 0x1:  # 时钟上升沿
        if state == "S0":
            if din == 1:
                state = "S1"
                out = 1  # 转移中输出1
            else:
                out = 0
        elif state == "S1":
            if din == 0:
                state = "S0"
            out = 0
    return out

看起来代码几乎一样对吧?关键区别在于:摩尔型是在进入 S1 这个状态时输出 1;米利型是在 S0 状态下收到 din=1 这个事件的 转移过程 中输出 1。你品,你细品。

一句话总结:

  • 摩尔型:输出 = f(当前状态)
  • 米利型:输出 = f(当前状态, 当前输入)

多了一个输入维度,反应更快,但代价是稳定性和复杂度。

2.4 本章知识体系总览

为了让你直观理解这些概念怎么串起来,我画了一张图。别小看这张图,它基本概括了状态机的全部玩法。

状态机基础结构图 状态机核心结构:摩尔型 vs 米利型 状态 A 状态 B 状态 C 事件1 事件2 事件3 摩尔输出 仅由状态决定 状态 X 状态 Y 输入 I 米利输出 由状态+输入决定 核心区别:摩尔型输出依赖当前状态,米利型输出依赖当前状态 + 当前输入 摩尔型更稳定但状态多,米利型反应快但可能有毛刺——根据场景选型

画完这张图,我自己又看了看,觉得该说的都在上面了。状态、转移、事件、动作,加上摩尔和米利的输出差异,一目了然。

2.5 实战选型建议

最后我分享点个人的经验。项目中到底选摩尔还是米利?

  • 控制类逻辑(比如按键消抖、LED闪烁):我闭着眼睛用摩尔型。输出稳定,不容易出奇奇怪怪的bug。
  • 数据通路(比如 FIFO 读写控制、DMA 传输):米利型有时更高效,因为可以用组合路径减少延时。但要做好时序约束和毛刺过滤。
一个小技巧: 当你拿不准时,先画状态转移图,把每个状态的输出写清楚。如果发现某个状态的输出需要因为输入变化而不同,那就是米利型的信号。否则,就是摩尔型。我这些年一直用这个办法,几乎没翻过车。

好,这一章的内容就到这。记住:状态是骨架,事件是触发器,转移是路径,动作是行为。摩尔型输出只认状态,米利型输出还看输入。把这四个东西刻在脑子里,后面讲训练序列的时候,你自然能跟上。