3. Moore状态机设计实战:以序列检测器为例

好,咱们今天来点真家伙。

前面讲了那么多理论,什么状态编码、状态转移图,说白了都是纸上谈兵。真正上手写代码的时候,你会发现——嗯,其实也没那么难。但坑也不少。

我个人习惯,学状态机最好的入门例子就是序列检测器。为什么?因为它逻辑清晰,状态不多,而且能完整展示Moore机的特点。今天我们就以检测“101”序列为例,手把手走一遍。

3.1 什么是序列检测器?

序列检测器,说白了就是在连续的比特流中,找出特定的子序列。比如我们要找“101”,那么输入“110101”时,检测到第3-5位是“101”,就输出一个高电平脉冲。

你想想看,这在实际项目里多常见?通信协议里的帧头检测、数据包同步,都离不开它。我在一个无线通信项目里就遇到过,当时要检测一个8位的同步头,用状态机做,稳得很。

3.2 Moore机 vs Mealy机:选哪个?

这里先插一句。同样是检测“101”,可以用Moore机,也可以用Mealy机。区别在哪?

  • Moore机:输出只取决于当前状态。检测到序列时,进入一个专门的“检测成功”状态,然后输出1。
  • Mealy机:输出取决于当前状态和输入。检测到序列的最后一位时,当场输出1,不需要额外状态。

我建议初学者先从Moore机入手。为什么?因为它的输出和输入是隔开的,不容易产生毛刺。我在项目中吃过Mealy机组合逻辑输出的亏,后来老老实实改成了Moore机。

核心要点:Moore机的输出比输入晚一个时钟周期,但更稳定。适合对时序要求严格的场景。

3.3 状态转移图:先画图再写代码

写状态机之前,我强烈建议你先画状态转移图。别急着敲代码,图画清楚了,代码就是翻译工作。

检测“101”的Moore机,需要几个状态?我们来推一下:

  • S0:初始状态,什么都没检测到
  • S1:检测到“1”
  • S2:检测到“10”
  • S3:检测到“101”(输出1)

注意,这里有个关键点:状态重叠。比如在S3(检测到101)之后,如果下一个输入是“1”,那其实已经构成了新的“10”序列的一部分,应该回到S1,而不是S0。我曾经在这里翻过车,以为检测完就回到起点,结果漏掉了重叠序列。

S0 S1 S2 S3 输出1 1 0 0 1 1 0 1 0

3.4 Verilog代码实现:三段式写法

我个人强烈推荐三段式状态机写法。为什么?因为可读性强,综合结果好,而且不容易出bug。我在公司里带新人,第一件事就是让他们用三段式。

三段式就是:

  1. 第一段:时序逻辑,描述状态转移(always @(posedge clk))
  2. 第二段:组合逻辑,描述下一状态逻辑(always @(*))
  3. 第三段:时序逻辑,描述输出(always @(posedge clk))
module moore_101_detector (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       data_in,
    output reg        detected
);

    // 状态编码:用独热码,综合效率高
    localparam S0 = 4'b0001;
    localparam S1 = 4'b0010;
    localparam S2 = 4'b0100;
    localparam S3 = 4'b1000;

    reg [3:0] current_state, next_state;

    // 第一段:状态转移
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            current_state <= S0;
        else
            current_state <= next_state;
    end

    // 第二段:下一状态逻辑
    always @(*) begin
        case (current_state)
            S0: begin
                if (data_in == 1'b1)
                    next_state = S1;
                else
                    next_state = S0;
            end
            S1: begin
                if (data_in == 1'b0)
                    next_state = S2;
                else
                    next_state = S1;  // 连续1,停留在S1
            end
            S2: begin
                if (data_in == 1'b1)
                    next_state = S3;
                else
                    next_state = S0;  // 遇到0,回到起点
            end
            S3: begin
                if (data_in == 1'b1)
                    next_state = S1;  // 重叠检测:1011 -> 检测到新的1
                else
                    next_state = S2;  // 重叠检测:1010 -> 检测到10
            end
            default: next_state = S0;
        endcase
    end

    // 第三段:输出
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            detected <= 1'b0;
        else if (current_state == S3)
            detected <= 1'b1;
        else
            detected <= 1'b0;
    end

endmodule

小技巧:状态编码我习惯用独热码(one-hot),虽然寄存器用得多一点,但组合逻辑简单,时序容易收敛。如果状态少于8个,独热码是首选。

3.5 仿真验证:别偷懒

代码写完了,一定要仿真。我见过太多人写完代码直接上板,结果跑不起来,回头查半天发现是状态机逻辑错了。

写个简单的testbench,覆盖这些场景:

  • 正常序列:输入101,看输出是否在第三个时钟沿拉高
  • 重叠序列:输入1011,看是否检测到两次(101和1011中的101)
  • 无效序列:输入100,看输出是否一直为0
  • 复位测试:复位后状态是否回到S0
module tb_moore_101;

    reg clk, rst_n, data_in;
    wire detected;

    moore_101_detector uut (
        .clk(clk),
        .rst_n(rst_n),
        .data_in(data_in),
        .detected(detected)
    );

    initial begin
        clk = 0;
        forever #5 clk = ~clk;
    end

    initial begin
        rst_n = 0; data_in = 0;
        #20 rst_n = 1;
        
        // 测试序列:1 0 1 1 0 1 0
        #10 data_in = 1;  // S0 -> S1
        #10 data_in = 0;  // S1 -> S2
        #10 data_in = 1;  // S2 -> S3, detected=1
        #10 data_in = 1;  // S3 -> S1 (重叠)
        #10 data_in = 0;  // S1 -> S2
        #10 data_in = 1;  // S2 -> S3, detected=1
        #10 data_in = 0;  // S3 -> S2
        #10 $finish;
    end

endmodule

避坑指南:我曾经在仿真时发现输出脉冲只持续了一个时钟周期,但实际需要两个周期。后来发现是testbench里采样时机不对。记住:Moore机的输出比输入晚一拍,采样时要在时钟沿之后。

3.6 综合与调试:Lattice环境下的注意事项

如果你用的是Lattice的FPGA,比如ICE40或ECP5系列,有几点要注意:

  • 状态编码:Lattice的合成工具对独热码支持很好,但如果你状态数超过16个,建议用二进制编码,否则寄存器太多。
  • 复位方式:Lattice的器件通常支持异步复位,但要注意复位信号的毛刺。我建议在复位路径上加一个同步器。
  • 调试手段:用Lattice的Reveal逻辑分析仪,把current_state信号拉出来看。我曾经靠这个抓到一个因为组合逻辑竞争导致的状态跳变错误。
调试场景 可能原因 解决方法
状态不跳变 复位未释放、时钟没来 检查rst_n和clk信号
输出脉冲宽度不对 Moore机输出晚一拍 调整采样时机
检测到错误序列 状态转移图有遗漏 重新检查重叠情况
综合后时序不满足 状态编码太大 改用二进制编码

嗯,到这里,一个完整的Moore状态机序列检测器就搞定了。你想想看,从状态图到代码,再到仿真调试,每一步都有坑,但每一步也都有套路。多做几次,你就能找到感觉。

总结一句话:Moore状态机的核心是“状态决定输出”,画好状态转移图,代码就是水到渠成的事。


公众号:蓝海资料掘金营,微信deep3321