3、状态机设计模式:Mealy与Moore状态机区别、单层与分层状态机、状态机与任务调度

状态机这东西,说白了就是控制逻辑的骨架。我做了十几年运动控制,见过太多项目因为状态机设计不合理,后期改得死去活来。今天咱们就把这块掰开揉碎了聊。

3.1 Mealy与Moore状态机:输出时机决定一切

先问个问题:你的输出是跟状态走,还是跟跳转走?

这就是Mealy和Moore最本质的区别。

Moore状态机:输出只取决于当前状态。换句话说,只要状态不变,输出就不变。我早期做的一个步进电机驱动器,用的就是Moore。每个状态对应一组固定的IO输出——比如状态A输出脉冲,状态B输出方向信号。逻辑清晰,调试起来也省心。

Mealy状态机:输出不仅取决于当前状态,还取决于输入。这意味着同一状态下,输入不同,输出可能完全不同。我在做伺服驱动器位置同步时用过Mealy——同样是“同步等待”状态,如果收到位置偏差过大信号,立即输出急停;如果偏差正常,则输出继续运行。同一个状态,两种输出。

对比项 Moore Mealy
输出依赖 仅当前状态 当前状态 + 输入
响应速度 慢一拍(需等时钟沿) 快(输入变化立即响应)
状态数量 通常较多 通常较少
安全性 高(输出稳定) 需注意毛刺
典型场景 安全联锁、模式切换 实时响应、数据流控制

我的经验之谈:运动控制中,安全相关的逻辑(比如急停、限位)我强烈建议用Moore。输出稳定,不会因为输入抖动导致误动作。而性能相关的逻辑(比如速度跟随、位置补偿)可以用Mealy,响应更快。

小技巧:如果你不确定用哪种,先画Moore。因为Moore可以无损转换为Mealy,反过来却不行。我习惯先用Moore搭框架,性能瓶颈处再局部改成Mealy。

3.2 单层与分层状态机:别让状态图变成蜘蛛网

你见过那种一张纸上画了50个状态、100条跳转线的状态图吗?我见过。那是我刚入行时一个同事画的,他自己后来都看不懂了。

这就是单层状态机的极限。所有状态平铺在一个层级里,状态多了,跳转关系就乱成一锅粥。

分层状态机的思路很简单:把相关的状态打包成一个“子状态机”,上层只关心这个子状态机的入口和出口。就像写代码一样,把复杂逻辑封装成函数。

举个例子。一个工业机器人控制,整体状态机可以分成三层:

  • 顶层:待机、运行、故障、急停
  • 中层:运行状态下,再分回零、示教、自动执行
  • 底层:自动执行状态下,再分直线插补、圆弧插补、点位运动

每一层只关心自己的事。顶层不用管底层怎么插补,底层也不用管顶层要不要急停。各司其职。

注意:分层不是越多越好。我见过有人分了七八层,结果一个跳转要穿透四层才能到达目标状态,调试时脑子都要炸了。一般三层足够,最多四层。

下面是我画的一个分层状态机结构图,你看一眼就明白了:

顶层状态机 待机 运行 故障 急停 中层状态机(运行子状态) 回零 示教 自动执行 底层状态机(自动执行子状态) 直线插补 圆弧插补 点位运动 进入运行状态 进入自动执行

3.3 状态机与任务调度:谁指挥谁?

这个问题我当年也困惑过:状态机跑在任务里,还是任务跑在状态机里?

答案是:状态机是逻辑框架,任务调度是执行引擎

我常用的模式是这样的:

// 伪代码:状态机驱动的任务调度
void main_loop() {
    while(1) {
        // 1. 读取输入(传感器、指令、定时器)
        input = read_inputs();
        
        // 2. 状态机跳转
        next_state = fsm_transition(current_state, input);
        
        // 3. 执行状态动作
        fsm_action(next_state);
        
        // 4. 任务调度(非阻塞)
        schedule_tasks();
        
        // 5. 更新状态
        current_state = next_state;
        
        // 6. 等待下一个周期
        delay(1); // 1ms周期
    }
}

这里的关键是非阻塞。每个状态动作执行时间必须远小于调度周期。我在做多轴同步控制时,每个状态动作控制在100微秒以内,剩下的900微秒留给任务调度。

避坑指南:我曾经在一个项目里把状态动作写成了阻塞式——比如在“等待到位”状态里用while循环死等。结果其他任务全被卡死了,电机直接飞车。后来改成状态机+定时器轮询,才解决问题。

任务调度这块,我推荐用时间片轮转或者优先级抢占。运动控制里,优先级一般是:

  1. 最高:急停、限位、故障处理(硬实时)
  2. :位置环、速度环(1ms以内)
  3. :状态机跳转、IO刷新(5ms以内)
  4. :通信、日志、界面刷新(10ms以上)

你想想看,如果状态机跳转和急停抢CPU,那后果是什么?所以状态机本身不应该占用太多CPU,它只是“决策者”,不是“执行者”。

我的习惯:状态机代码单独放一个文件,任务调度代码放另一个文件。接口就两个函数:fsm_run()fsm_get_state()。这样换平台或者换调度策略时,状态机逻辑完全不用动。

最后说一句:状态机设计没有银弹。Mealy还是Moore,单层还是分层,取决于你的具体场景。但有一条原则永远不会错——让逻辑清晰可读,比追求花哨更重要。毕竟,代码是写给人看的,顺便给机器执行。