3. Verilog基础语法(下):组合逻辑与时序逻辑、阻塞赋值与非阻塞赋值、状态机基础、Testbench编写

好,咱们接着聊。上一章我们把Verilog的基本语法框架搭起来了,这一章要深入几个核心概念。说实话,这几个概念是新手最容易翻车的地方。我在带项目的时候,见过太多因为赋值方式用错、状态机写崩导致的bug。咱们一个一个来拆解。

3.1 组合逻辑 vs 时序逻辑

先问个问题:你写的代码,最终在FPGA里长什么样?

其实就两种东西:组合逻辑和时序逻辑。

组合逻辑,说白了就是纯门电路。输入一变,输出立刻跟着变,没有记忆能力。比如一个与门、一个加法器,都是组合逻辑。

时序逻辑,则是有存储元件(触发器)的电路。它依赖时钟沿来更新状态。换句话说,输出不仅取决于当前输入,还取决于之前的状态。

我刚开始学的时候,总觉得这两者界限模糊。后来想明白一个道理:组合逻辑是“线”,时序逻辑是“沿”。你写always块时,敏感列表里是电平信号(如 @(*)),那就是组合逻辑;敏感列表里是时钟沿(如 @(posedge clk)),那就是时序逻辑。

核心区别速记:

  • 组合逻辑:无时钟、无记忆、输出即时响应
  • 时序逻辑:有时钟、有记忆、输出在时钟沿更新

嗯,这里要注意:千万不要把组合逻辑和时序逻辑混写在同一个always块里。我见过有人为了省事,一个always块里既赋值又触发时钟,结果综合出来的电路完全不是想要的样子。分开写,清爽又安全。

3.2 阻塞赋值与非阻塞赋值

这个知识点,可以说是Verilog的“鬼门关”。很多面试题就爱考这个。

阻塞赋值(=):执行完当前赋值语句,再执行下一条。它是“串行”的,适合描述组合逻辑。

非阻塞赋值(<=):所有赋值语句同时计算,在块结束时统一更新。它是“并行”的,适合描述时序逻辑。

我曾经在一个项目里,把时序逻辑的always块里用了阻塞赋值。结果仿真波形看起来没问题,但上板跑起来就是不对。查了两天才发现,是因为阻塞赋值导致多个触发器之间产生了意外的依赖关系。从那以后,我给自己定了个规矩:

铁律:

  • 组合逻辑 always 块:用 =(阻塞赋值)
  • 时序逻辑 always 块:用 <=(非阻塞赋值)

你想想看,为什么会有这个区别?

因为时序逻辑里,多个触发器是在同一个时钟沿同时采样的。如果用阻塞赋值,就会变成“先更新A,再用A的新值去更新B”,这完全违背了硬件并行工作的本质。而非阻塞赋值,则能保证所有赋值在时钟沿到来时同时生效。

看个例子就明白了:

// 正确的时序逻辑写法
always @(posedge clk or negedge rst_n) begin
    if (!rst_n) begin
        reg_a <= 1'b0;
        reg_b <= 1'b0;
    end else begin
        reg_a <= data_in;
        reg_b <= reg_a;  // 这里用的是reg_a的旧值
    end
end

如果是阻塞赋值,reg_b会拿到reg_a的新值,相当于两个寄存器变成了一个移位寄存器?不对,实际上会变成更奇怪的行为。总之,记住铁律,别乱来。

3.3 状态机基础

状态机是FPGA设计的灵魂。说白了,它就是一个会“记住自己走到哪一步”的电路。

常见的状态机分两种:

  • Moore型:输出只取决于当前状态
  • Mealy型:输出取决于当前状态和输入

我个人习惯用三段式状态机。为什么?因为清晰、好维护、不容易出bug。

三段式结构如下:

  1. 第一段:时序逻辑,描述状态跳转(当前状态 → 次态)
  2. 第二段:组合逻辑,描述次态如何产生(根据输入和当前状态)
  3. 第三段:时序逻辑或组合逻辑,描述输出

我曾经接手过一个项目,里面全是用两段式状态机写的。代码读起来像天书,状态和输出混在一起,改一个地方要牵动全身。后来我全部重写成三段式,调试效率直接翻倍。

看个简单的例子:一个检测“101”序列的状态机。

// 三段式状态机示例
module seq_detector (
    input clk,
    input rst_n,
    input din,
    output reg dout
);

    // 状态编码
    localparam IDLE = 2'b00,
               S1   = 2'b01,
               S10  = 2'b10,
               S101 = 2'b11;

    reg [1:0] state, next_state;

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

    // 第二段:次态逻辑(组合逻辑)
    always @(*) begin
        case (state)
            IDLE:   next_state = din ? S1 : IDLE;
            S1:     next_state = din ? S1 : S10;
            S10:    next_state = din ? S101 : IDLE;
            S101:   next_state = din ? S1 : S10;
            default: next_state = IDLE;
        endcase
    end

    // 第三段:输出逻辑(时序逻辑,避免毛刺)
    always @(posedge clk or negedge rst_n) begin
        if (!rst_n)
            dout <= 1'b0;
        else if (state == S101)
            dout <= 1'b1;
        else
            dout <= 1'b0;
    end

endmodule

小技巧:状态编码尽量用localparam定义,别用数字硬编码。这样改状态编码时,只需要改一处,不会漏改。

嗯,这里还要提一嘴:状态机一定要有default分支。为什么?因为综合工具会认为你没有覆盖所有情况,从而生成不必要的锁存器。我曾经吃过这个亏,综合出来的电路面积比预期大了30%。

3.4 Testbench编写

写Testbench,说白了就是给你的设计“搭个戏台子”。你设计了一个模块,总得验证它能不能正常工作吧?

Testbench的核心要素:

  • 时钟生成:用always块产生时钟
  • 复位逻辑:模拟上电复位
  • 激励输入:给被测模块喂数据
  • 结果检查:看输出是否符合预期

我写Testbench的习惯是:先写一个简单的“冒烟测试”,确保模块能跑起来。然后再加各种边界条件、异常情况。别一上来就搞复杂的测试序列,否则出了问题你都不知道是模块的bug还是Testbench的bug。

看个简单的Testbench模板:

`timescale 1ns / 1ps

module tb_seq_detector;

    reg clk;
    reg rst_n;
    reg din;
    wire dout;

    // 实例化被测模块
    seq_detector uut (
        .clk(clk),
        .rst_n(rst_n),
        .din(din),
        .dout(dout)
    );

    // 时钟生成:周期10ns
    always #5 clk = ~clk;

    // 测试序列
    initial begin
        // 初始化
        clk = 0;
        rst_n = 0;
        din = 0;

        // 释放复位
        #20 rst_n = 1;

        // 输入序列:1 0 1 0 1 1
        #10 din = 1;
        #10 din = 0;
        #10 din = 1;  // 这里应该检测到一次
        #10 din = 0;
        #10 din = 1;  // 这里又检测到一次
        #10 din = 1;
        #10 din = 0;

        #50 $finish;
    end

    // 波形输出
    initial begin
        $dumpfile("wave.vcd");
        $dumpvars(0, tb_seq_detector);
    end

endmodule

避坑指南:

  • Testbench里不要用#0延时,容易产生竞争
  • 时钟和复位要分开生成,别混在一起
  • 用$display或$monitor打印关键信号,方便调试

我曾经在调试一个高速接口时,Testbench里忘了加复位释放的延时,结果模块一直处于复位状态,仿真波形全是0。查了半天才发现是Testbench的问题,不是设计的问题。所以,写Testbench要像写设计代码一样认真。

3.5 本章知识体系

下面这张图,把本章的核心知识点串起来了。你可以把它当作一个“思维导图”来看。

Verilog基础语法(下) 组合 vs 时序 组合逻辑:无记忆,即时响应 时序逻辑:有记忆,时钟沿更新 ⚠ 不要混写在同一个always块 阻塞 vs 非阻塞 阻塞赋值 = :串行,组合逻辑 非阻塞赋值 <=:并行,时序逻辑 ⚠ 铁律:时序用<=,组合用= 状态机基础 Moore型:输出只取决于状态 Mealy型:输出取决于状态+输入 推荐:三段式状态机 ⚠ 必须有default分支 Testbench编写 时钟生成 + 复位逻辑 激励输入 + 结果检查 先做冒烟测试 ⚠ 避免#0延时

这张图把四个核心知识点串成了一个整体。你可以看到,组合逻辑和时序逻辑是基础,阻塞/非阻塞赋值是具体实现手段,状态机是高级应用,Testbench是验证工具。它们环环相扣,缺一不可。

好了,这一章的内容就到这里。记住:理论要懂,但更重要的是动手写代码、跑仿真。下一章我们会开始真正的图像滤波实战,到时候这些基础语法都会派上用场。