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

好,咱们今天来点真格的。前面讲了那么多理论,说白了,状态机到底怎么用,还得上手练。我选了一个非常经典的例子——序列检测器。这东西在通信协议解析、数据帧头识别里太常见了。我当年做SDI视频解码的时候,就靠它来锁定同步头。

这次咱们要检测的序列是“101”。我会分别用Mealy和Moore两种方式实现,然后你就能直观地看到它们的区别。嗯,先上Mealy。

4.1 Mealy型序列检测器设计

Mealy状态机的核心特点,就是输出不仅取决于当前状态,还取决于当前输入。说白了,输出是“即时反应”。检测到目标序列的最后一个bit时,输出立刻拉高,不需要等下一个时钟周期。

4.1.1 状态转移图

我个人习惯,写代码前一定先画图。脑子里的想法再清晰,落到纸上才能发现漏洞。咱们要检测“101”,需要几个状态?

  • S0:初始状态,啥也没检测到
  • S1:检测到了第一个bit “1”
  • S2:检测到了前两个bit “10”

注意,这里只需要3个状态。为什么?因为Mealy的输出是在转移路径上完成的。当我们在S2状态,输入为“1”时,不仅转移到下一个状态,同时输出“1”表示检测成功。

关键点:Mealy状态数通常比Moore少。这是它的优势,也是容易让人迷惑的地方。

咱们用SVG画一张状态转移图,你一看就明白。

S0 初始 S1 检测到"1" S2 检测到"10" 0/0 1/0 0/0 1/0 0/0 1/1 ✓ 图例 输入/输出 红色:检测成功 绿色:正常转移

看到没?从S2到S1的那条红色路径,标注的是“1/1”。意思是输入为1时,输出为1,同时状态回到S1。为什么回到S1?因为输入的“1”可以作为下一个序列的开始。这种重叠检测在实际中很常见。

4.1.2 Verilog代码实现

好,图有了,代码就顺理成章了。我习惯用三段式状态机,清晰、好维护、时序也干净。

// Mealy型 101序列检测器
module mealy_101_detector (
    input       clk,
    input       rst_n,
    input       din,
    output reg  dout
);

    // 状态编码
    localparam S0 = 2'b00,
               S1 = 2'b01,
               S2 = 2'b10;

    reg [1:0] state, next_state;

    // 第一段:时序逻辑,状态更新
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            state <= S0;
        else
            state <= next_state;
    end

    // 第二段:组合逻辑,次态和输出
    always @(*) begin
        next_state = S0;  // 默认值
        dout = 1'b0;      // 默认输出为0

        case (state)
            S0: begin
                if (din == 1'b1) begin
                    next_state = S1;
                end else begin
                    next_state = S0;
                end
            end

            S1: begin
                if (din == 1'b0) begin
                    next_state = S2;
                end else begin
                    next_state = S1;  // 连续1,停留在S1
                end
            end

            S2: begin
                if (din == 1'b1) begin
                    next_state = S1;  // 检测成功,且重叠
                    dout = 1'b1;      // Mealy输出:即时拉高
                end else begin
                    next_state = S0;
                end
            end

            default: begin
                next_state = S0;
            end
        endcase
    end

endmodule

小技巧:注意第二段是组合逻辑,我给next_state和dout都赋了默认值。这样做可以避免产生锁存器(latch)。我曾经见过一个同事,就是因为少写了默认赋值,综合出来一堆latch,时序怎么都修不过去。

4.2 Moore型序列检测器对比

咱们再来看看Moore型。Moore的输出只取决于当前状态,所以检测到“101”后,需要进入一个专门的“成功状态”,在下一个时钟周期输出才拉高。

4.2.1 状态转移图

Moore需要4个状态:

  • S0:初始
  • S1:检测到“1”
  • S2:检测到“10”
  • S3:检测到“101”(输出为1)

你对比一下,Mealy是3个状态,Moore是4个。这就是区别。

S0 输出0 S1 输出0 S2 输出0 S3 输出1 0 1 0 1 1 0 1 0 图例 输入 红色:检测成功

4.2.2 Verilog代码实现

// Moore型 101序列检测器
module moore_101_detector (
    input       clk,
    input       rst_n,
    input       din,
    output reg  dout
);

    localparam S0 = 2'b00,
               S1 = 2'b01,
               S2 = 2'b10,
               S3 = 2'b11;

    reg [1:0] state, next_state;

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

    // 第二段:次态逻辑
    always @(*) begin
        next_state = S0;
        case (state)
            S0: next_state = (din) ? S1 : S0;
            S1: next_state = (din) ? S1 : S2;
            S2: next_state = (din) ? S3 : S0;
            S3: next_state = (din) ? S1 : S0;
            default: next_state = S0;
        endcase
    end

    // 第三段:输出逻辑(Moore,只取决于当前状态)
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            dout <= 1'b0;
        else
            dout <= (state == S3) ? 1'b1 : 1'b0;
    end

endmodule

注意:Moore的输出在第三段,是时序逻辑。这意味着检测到“101”后,输出要等到下一个时钟周期才拉高。如果你需要“即时响应”,Mealy更合适。但Moore的输出更干净,没有毛刺风险。

4.3 Mealy vs Moore:实战对比

咱们用一张表来总结,这样你面试时也能说得清楚。

对比项 Mealy Moore
状态数 较少(本例3个) 较多(本例4个)
输出时机 输入变化时立即输出 时钟沿到来后输出
输出延迟 一个组合逻辑延迟 一个时钟周期延迟
毛刺风险 较高(组合输出) 较低(寄存器输出)
适用场景 高速、需要快速响应 对时序要求严格、需要稳定输出

我个人在实际项目中,更倾向于用Moore。为什么?因为它的输出是寄存器输出,时序上更好把控。我曾经在一个高速SerDes项目中,用了Mealy状态机,结果输出毛刺导致后级逻辑误触发,排查了两天才找到原因。从那以后,除非对延迟有极致要求,否则我都用Moore。

当然,Mealy也有它的用武之地。比如在通信协议中,需要在一个时钟周期内完成“检测+响应”,Mealy就是唯一选择。你想想看,如果每个响应都要等一个时钟周期,那协议效率就大打折扣了。

核心结论

  • 追求低延迟、少状态 → 选Mealy
  • 追求稳定输出、易调试 → 选Moore
  • 没有绝对的好坏,只有合不合适的场景

好了,这一章的内容就到这儿。代码你可以直接拿去仿真,用Vivado或Lattice Diamond都行。试着改一改,比如检测“1101”序列,看看状态图怎么画,代码怎么写。动手练才是王道。


专注资料整理