第4章:Verilog基础回顾——模块化设计、always块、assign语句、寄存器与组合逻辑、Testbench编写基础

各位同学,欢迎来到DFT测试环境搭建的第一站。说实话,很多刚入行的朋友一上来就盯着Scan Chain、ATPG这些高大上的概念,结果写Testbench时连always和assign都分不清,代码跑起来全是红叉叉。

我当年带过一个新人,上来就问我:“老师,为什么我的寄存器没锁住数据?”我一看代码,好家伙,他把组合逻辑和时序逻辑全写在一个always块里了。嗯,这种坑咱们今天就得填上。

这一章,咱们把Verilog里最核心的几个基础概念过一遍。别嫌简单,DFT工程师写测试代码时,天天跟这些东西打交道。

4.1 模块化设计——把大问题拆成小积木

Verilog的设计哲学,说白了就是“搭积木”。一个复杂的芯片,你不可能在一个文件里写完所有逻辑。你得把它拆成一个个模块(module),每个模块干一件事。

举个例子,一个简单的DFT测试模块,可能包含:

  • 顶层模块:负责连接所有子模块
  • 扫描链控制模块:管理shift和capture模式
  • 测试时钟模块:产生测试用的时钟信号
  • 响应分析模块:比对输出结果

每个模块都有固定的结构:

module module_name (
    input  wire  clk,
    input  wire  rst_n,
    input  wire  data_in,
    output reg   data_out
);

    // 内部逻辑写在这里

endmodule

我个人习惯,每个模块只做一件事。比如一个模块只负责产生时钟,另一个只负责数据比对。这样调试的时候,你一眼就能看出问题出在哪。

小技巧:模块的端口命名要规范。我一般用“_i”表示输入,“_o”表示输出,“_io”表示双向。比如clk_i、data_o。这样看代码时不用猜方向。

4.2 assign语句——组合逻辑的直通车

assign语句,说白了就是“连线”。它描述的是组合逻辑,没有时钟,没有存储。输入一变,输出立刻跟着变。

比如,我们要做一个简单的与门:

assign result = a & b;

再比如,一个多路选择器:

assign out = sel ? data1 : data0;

我在项目中遇到过一个问题:有人用assign给寄存器赋值,结果综合出来一堆锁存器。为什么?因为assign只能描述组合逻辑,你没法用它描述“在时钟上升沿锁存数据”这种时序行为。

避坑指南:我曾经见过一个同事,在assign语句里写了类似“assign q = (clk) ? d : q;”的代码。综合工具直接报错,或者给你生成一个透明的锁存器。记住:assign不是always,它没有时钟的概念。

4.3 always块——时序逻辑与组合逻辑的舞台

always块是Verilog里最灵活、也最容易出错的语法。它既可以描述时序逻辑,也可以描述组合逻辑。关键看敏感列表怎么写。

时序逻辑(寄存器):

always @(posedge clk or negedge rst_n) begin
    if (!rst_n)
        q <= 1'b0;
    else
        q <= d;
end

注意这里用的是“<=”(非阻塞赋值)。为什么?因为时序逻辑需要模拟寄存器的行为:所有赋值在时钟沿同时发生,不会互相干扰。

组合逻辑:

always @(*) begin
    if (sel)
        out = data1;
    else
        out = data0;
end

这里用的是“=”(阻塞赋值)。敏感列表写成“@(*)”表示“所有输入信号变化都触发”。

你想想看,如果把组合逻辑写成“<=”,会怎么样?仿真结果会延迟一个时钟周期,综合出来的电路跟你想要的完全不一样。我刚开始学的时候,就因为这个bug调了整整两天。

核心原则:
  • 描述寄存器(时序逻辑):用 always @(posedge clk),用 <= 赋值
  • 描述组合逻辑:用 always @(*),用 = 赋值
  • 千万别混着用!一个always块里要么全是组合,要么全是时序

4.4 寄存器与组合逻辑——数字电路的两大基石

数字电路就两种东西:寄存器和组合逻辑。寄存器存数据,组合逻辑算数据。

寄存器(Flip-Flop):

  • 在时钟沿采样输入,保持输出
  • 用于状态存储、流水线、计数器
  • 在DFT中,扫描链就是把所有寄存器串起来

组合逻辑:

  • 输出只取决于当前输入
  • 用于算术运算、数据选择、译码
  • 没有存储能力,信号传播有延迟

我画了一张图,帮你理清它们的关系:

组合逻辑 assign / always @(*) 输出 = f(当前输入) 寄存器 always @(posedge clk) 输出 = 上一拍输入 时钟沿 采样 数字电路 = 组合逻辑 + 寄存器 组合逻辑算结果,寄存器存状态

在DFT测试中,我们最关心的就是这些寄存器能不能正常工作。扫描链测试,就是把所有寄存器连成一条链,然后灌数据进去,看能不能原样吐出来。

4.5 Testbench编写基础——给设计当“考官”

写Testbench,说白了就是给你的设计模块写一个“考官”。这个考官负责:

  • 产生输入激励(时钟、复位、数据)
  • 监控输出响应
  • 判断结果对不对

一个最简单的Testbench长这样:

module tb_counter();

    // 1. 声明信号
    reg        clk;
    reg        rst_n;
    wire [3:0] count;

    // 2. 实例化待测模块
    counter u_counter (
        .clk    (clk),
        .rst_n  (rst_n),
        .count  (count)
    );

    // 3. 产生时钟
    always #5 clk = ~clk;  // 周期10ns

    // 4. 产生复位
    initial begin
        clk   = 0;
        rst_n = 0;
        #20;
        rst_n = 1;
    end

    // 5. 监控输出
    initial begin
        #100;
        $display("count = %d", count);
        #200;
        $finish;
    end

endmodule

这里有几个要点:

  1. reg和wire:Testbench里,驱动输入用reg,观察输出用wire
  2. initial块:用来产生一次性的激励,比如复位
  3. always块:用来产生周期性的信号,比如时钟
  4. $display和$monitor:打印调试信息
我的习惯:写Testbench时,我会把时钟产生和激励产生分开。时钟用always块,激励用initial块。这样代码清晰,调试时也容易定位问题。

嗯,这里要注意一点:Testbench本身是不可综合的。它只在仿真器里跑,目的是验证你的设计功能对不对。所以你可以随便用initial、$display这些语法,不用考虑硬件能不能实现。

我曾经犯过一个低级错误:在Testbench里用always块驱动复位信号,结果复位一直在跳变,仿真根本没法跑。后来才反应过来,复位应该用initial块,只拉低一次就够了。

小结

这一章咱们把Verilog的四大基础过了一遍:

  • 模块化设计:把大问题拆成小模块,每个模块只干一件事
  • assign语句:描述组合逻辑,输入一变输出立刻变
  • always块:既能描述时序逻辑(寄存器),也能描述组合逻辑,关键看敏感列表和赋值方式
  • 寄存器与组合逻辑:数字电路的两大基石,一个存数据,一个算数据
  • Testbench编写:给设计当考官,产生激励、监控输出、判断结果

这些基础打牢了,后面学Scan Chain、ATPG、Boundary Scan的时候,你才能把精力放在DFT本身,而不是被Verilog语法绊住脚。

可可精品资料库