4、Verilog基础(下):时序逻辑(时钟/复位)、状态机设计、仿真与Testbench

好,咱们接着聊。上一章我们把组合逻辑和基本语法捋了一遍,这一章才是真正让FPGA“活起来”的关键——时序逻辑。说白了,就是让电路有“记忆”,能按着节拍一步步干活。

我个人习惯把时序逻辑比作一支军队:时钟是司令的鼓点,复位是集合号,而状态机就是作战计划。鼓点一响,全军按计划行动。没有时钟,电路就是一盘散沙;没有复位,上电后谁都不知道自己在哪。

核心要点:时序逻辑 = 组合逻辑 + 寄存器(存储单元)。寄存器在时钟沿采样数据,这就是“时序”二字的由来。

4.1 时钟与复位——设计的“心跳”与“归零键”

先说说时钟。在Verilog里,我们通常用 always @(posedge clk) 来描述时序逻辑。这个 posedge 就是时钟的上升沿。为什么用上升沿?因为信号在电平中间跳变最稳定,采样不容易出错。

我在项目中遇到过一个问题:两个模块用了不同相位的时钟,结果数据老是丢。查了半天,原来是跨时钟域没处理好。所以,同一个时钟域内,所有寄存器必须对同一个时钟沿敏感,这是铁律。

// 标准时序逻辑模板
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;      // 异步复位,低电平有效
    else
        q <= d;          // 时钟上升沿采样
end

这里有个细节:复位。我建议用异步复位、同步释放的方式。为什么?纯异步复位容易受毛刺影响,纯同步复位又可能错过复位窗口。我曾经在一个高速设计中吃过这个亏——复位信号刚好在时钟沿附近变化,导致部分寄存器复位了,部分没复位,整个状态机跑飞了。

我的习惯:复位信号 rst_n 用低电平有效,这样符合大多数FPGA器件的内部结构。而且,复位逻辑里一定要把未用到的状态也覆盖到,否则综合时会给你“惊喜”。

4.2 状态机设计——有限状态机(FSM)实战

状态机是时序逻辑的“灵魂”。说白了,就是让电路记住“现在在干什么”,然后根据输入决定“下一步干什么”。

我常用的状态机分三种:Moore型(输出只跟当前状态有关)、Mealy型(输出跟当前状态和输入都有关),以及三段式状态机。我个人强烈推荐三段式,因为它把“状态跳转”、“状态寄存”、“输出逻辑”分得清清楚楚,代码可读性强,也容易维护。

你想想看,如果所有逻辑揉在一个always块里,出错了你找半天都找不到。三段式就不一样,每个部分各司其职。

// 三段式状态机示例:一个简单的序列检测器(检测"101")
// 第一段:状态跳转(组合逻辑)
always @(*) begin
    next_state = current_state;
    case (current_state)
        S0: if (din) next_state = S1;
        S1: if (!din) next_state = S2;
        S2: if (din) next_state = S3;
        S3: next_state = S0;
        default: next_state = S0;
    endcase
end

// 第二段:状态寄存(时序逻辑)
always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        current_state <= S0;
    else
        current_state <= next_state;
end

// 第三段:输出逻辑(组合逻辑)
assign dout = (current_state == S3) ? 1'b1 : 1'b0;

注意:状态编码时,一定要用 default 把未定义状态引回到安全状态。我曾经见过一个设计,状态机跑到了非法状态,结果输出乱跳,整机复位才恢复。这就是“状态机锁死”,很要命的。

状态编码方式上,我建议小状态机(少于8个状态)用独热码(One-hot),因为译码简单、速度快;大状态机用二进制码,节省寄存器资源。别问我怎么选——看你的资源余量和速度要求。

4.3 仿真与Testbench——不仿真的设计都是耍流氓

好,代码写完了,直接上板子?千万别。我见过太多人“代码写完直接下载”,结果板子冒烟或者没反应。仿真,是验证设计的最后一道防线。

Testbench 说白了就是“给设计喂激励,看它拉什么屎”。它不需要综合,所以你可以用很多高级语法,比如 initial、repeat、$display 等。

// 一个简单的Testbench示例
module tb_fsm_detector();

reg clk, rst_n, din;
wire dout;

// 实例化被测试模块
fsm_detector u_fsm (
    .clk    (clk),
    .rst_n  (rst_n),
    .din    (din),
    .dout   (dout)
);

// 生成时钟:周期10ns
initial begin
    clk = 0;
    forever #5 clk = ~clk;
end

// 测试激励
initial begin
    rst_n = 0;
    din = 0;
    #20 rst_n = 1;      // 复位释放
    
    // 输入序列:1 0 1 0 1 1 0 ...
    #10 din = 1;
    #10 din = 0;
    #10 din = 1;        // 此时dout应该变高
    #10 din = 0;
    #10 din = 1;        // 再次检测到
    #10 din = 1;
    #10 din = 0;
    
    #50 $finish;
end

// 监视输出
initial begin
    $monitor("Time=%0t, din=%b, dout=%b", $time, din, dout);
end

endmodule

这里我有个习惯:先写Testbench,再写设计代码。这叫“测试驱动设计”。你想想看,如果你先写设计,再写测试,很容易被自己的设计思路带偏,测不出隐藏bug。反过来,你先想好“我要测什么”,再写设计,思路会清晰很多。

避坑指南:我曾经在仿真时发现波形对了,但上板就是不行。后来发现是Testbench里用了 #1 这种延迟,而实际电路里没有这种“理想延迟”。记住:Testbench里的延迟只用于仿真,不代表实际硬件行为。真正的时序要靠时钟沿来驱动。

4.4 本章知识体系

下面这张图,是我自己总结的时序逻辑设计流程。你照着这个思路走,基本不会出大错。

时序逻辑设计知识体系 时钟与复位 posedge/negedge 状态机设计 三段式 / 独热码 仿真与Testbench 激励 / 波形 / 调试 异步复位同步释放 时钟域划分 Moore / Mealy 选择 default状态保护 时钟生成 / 激励 $monitor / 波形查看 设计流程:时钟规划 → 状态机编码 → 仿真验证 → 上板调试 每一步都不可跳过,否则bug会找上门 —— 经验之谈:仿真时间 = 设计时间 × 2 ——

嗯,这一章的内容就这些。时钟、复位、状态机、仿真,这四个东西是FPGA设计的“四梁八柱”。你把这四个点吃透了,后面学什么高速接口、图像处理,都会轻松很多。

记住一句话:时序逻辑设计,就是跟时间打交道。你尊重时间,时间就给你稳定的电路。你糊弄时间,时间就在板子上给你颜色看。

最后送大家一句话:写代码前先画状态转移图,仿真前先列测试用例。这两步做好了,你的设计成功率至少提高50%。

可可精品资料库