4. 序列检测器 (101)

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

前面几章讲了状态机的基本概念、编码方式、三段式写法。说实话,那些都是基本功。但真正让你对状态机有感觉的,一定是动手写一个完整的例子,然后看着仿真波形跑起来。

这一章,我们就拿「101序列检测器」开刀。这个例子我几乎在每个项目里都会用到——不是真的用这个模块,而是用它来快速验证工具链、验证仿真环境、甚至给新人做培训。你想想看,一个简单的序列检测器,能把状态机的所有关键点都覆盖到。

4.1 需求分析:我们要检测什么?

先明确一下功能:

  • 输入是一个串行数据流 din,每个时钟周期进来1 bit
  • 我们要检测连续的「101」这个模式
  • 检测到了,输出 dout 拉高一个周期
  • 注意:序列可以重叠。比如输入「10101」,应该检测到两次「101」

重叠检测这个点,很多人第一次写会漏掉。我当年刚入行时,写了一个非重叠的版本,结果测试同学拿着波形来找我,说少了一个脉冲。嗯,从那以后我每次写序列检测器,都会在注释里标清楚「是否支持重叠」。

4.2 状态转移图:画出来再说

写RTL之前,我个人的习惯是先画状态转移图。别嫌麻烦,一张图顶你写半小时代码。

对于101序列检测器(重叠),我们需要4个状态:

  • S0:初始状态,什么都没收到
  • S1:收到了「1」
  • S2:收到了「10」
  • S3:收到了「101」,输出拉高

转移逻辑是这样的:

  • S0:输入0 → 留在S0;输入1 → 跳到S1
  • S1:输入0 → 跳到S2(因为有了「10」);输入1 → 留在S1(因为连续的1可以看作新序列的开始)
  • S2:输入0 → 回到S0(「100」没戏了);输入1 → 跳到S3(「101」成了)
  • S3:输入0 → 跳到S2(因为「1010」的最后两位是「10」);输入1 → 跳到S1(「1011」的最后一位是「1」)

你看,S3的转移是关键。重叠检测的奥妙就在这里——检测到101之后,不能直接回到S0,而是要看看最后几位能不能复用。

下面我用SVG画一张状态转移图,帮你把逻辑理清楚:

S0 S1 S2 S3 0 1 1 0 0 1 0 1 S3: dout = 1

核心要点:状态转移图是状态机设计的灵魂。画图的时候,重点关注「边界情况」——比如S3收到0或1之后怎么走,这直接决定了你的设计是否支持重叠检测。

4.3 RTL代码实现:三段式写法

好,图有了,代码就好写了。我用三段式状态机来实现,这是我最推荐的写法——可读性强、综合结果好、仿真也容易调。

module seq_detector_101 (
    input  wire       clk,
    input  wire       rst_n,
    input  wire       din,
    output reg        dout
);

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

    reg [3: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: begin
                if (din == 1'b1)
                    next_state = S1;
                else
                    next_state = S0;
            end
            S1: begin
                if (din == 1'b0)
                    next_state = S2;
                else
                    next_state = S1;
            end
            S2: begin
                if (din == 1'b1)
                    next_state = S3;
                else
                    next_state = S0;
            end
            S3: begin
                if (din == 1'b0)
                    next_state = S2;  // 重叠:1010 → 最后两位是10
                else
                    next_state = S1;  // 重叠:1011 → 最后一位是1
            end
            default: next_state = S0;
        endcase
    end

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

endmodule

个人经验:第二段组合逻辑里,我习惯先给 next_state 赋一个默认值。为什么?因为如果不这样做,有些综合工具会给你插一堆锁存器。我曾经在一个项目里吃过这个亏,综合面积直接翻倍,查了半天才发现是 case 语句没写全。

4.4 Testbench 与仿真验证

代码写完了,不仿真等于白写。我见过太多人「代码写完就觉得对了」,结果一跑仿真全是bug。

下面是一个完整的testbench,我故意加了一些边界测试:

`timescale 1ns/1ps

module tb_seq_detector_101;

    reg  clk, rst_n;
    reg  din;
    wire dout;

    seq_detector_101 u_dut (
        .clk  (clk),
        .rst_n(rst_n),
        .din  (din),
        .dout (dout)
    );

    // 时钟生成
    always #5 clk = ~clk;

    // 测试序列
    initial begin
        clk   = 0;
        rst_n = 0;
        din   = 0;
        #20 rst_n = 1;

        // 测试1:简单序列 101
        @(posedge clk); din = 1;
        @(posedge clk); din = 0;
        @(posedge clk); din = 1;  // 这里应该输出1
        @(posedge clk); din = 0;

        // 测试2:重叠序列 10101
        @(posedge clk); din = 1;
        @(posedge clk); din = 0;
        @(posedge clk); din = 1;  // 第一次检测到
        @(posedge clk); din = 0;
        @(posedge clk); din = 1;  // 第二次检测到(重叠)
        @(posedge clk); din = 0;

        // 测试3:非匹配序列 1001
        @(posedge clk); din = 1;
        @(posedge clk); din = 0;
        @(posedge clk); din = 0;
        @(posedge clk); din = 1;  // 不应该输出

        #50 $finish;
    end

    // 波形打印
    initial begin
        $monitor("time=%0t, din=%b, state=%b, dout=%b", 
                  $time, din, u_dut.state, dout);
    end

endmodule

跑完仿真,你会看到:

  • 第一次「101」出现时,dout 拉高一个周期
  • 「10101」中,dout 拉高了两次——这就是重叠检测的效果
  • 「1001」没有输出,因为中间插了个0

避坑指南:我曾经在仿真时发现输出总是晚一个周期。查了半天,原来是第三段输出逻辑用了组合逻辑,而我的testbench在同一个时钟沿采样。记住:输出逻辑如果是时序的(always @(posedge clk)),输出会比状态变化晚一拍。这在设计流水线时要注意。

4.5 代码风格与可读性建议

最后聊几句代码风格。说实话,代码是写给人看的,不是写给机器看的。机器只在乎语法对不对,人在乎的是能不能看懂。

项目 推荐做法 不推荐做法
状态编码 用 localparam 定义有意义的名称 直接用二进制数字 2'b00, 2'b01
三段式结构 明确分开状态寄存器、次态逻辑、输出逻辑 把所有逻辑写在一个 always 块里
默认赋值 组合逻辑中给 next_state 赋默认值 依赖综合工具自动推断
注释 在关键转移处加注释,比如重叠逻辑 每行都注释,反而干扰阅读

嗯,这一章的内容就到这里。序列检测器虽然简单,但麻雀虽小五脏俱全。你把这个例子吃透了,状态机的基本功就算打牢了。

可可精品资料库