3、Testbench基础:Testbench结构、initial/always块、时钟与复位生成、基本激励编写

好,咱们今天聊聊Testbench。说白了,Testbench就是咱们给DUT(Design Under Test)搭的一个“测试台”。你想想看,芯片流片前,你总得先验证一下它能不能干活吧?Testbench就是这个干活的平台。

我个人习惯,写Testbench之前,脑子里先画个草图。这个草图就是Testbench的结构。它不复杂,但每个部分都有讲究。

3.1 Testbench的基本结构

一个标准的Testbench,通常包含这么几个部分:

  • DUT实例化:把你要测的模块“放”进来。
  • 激励生成:给DUT喂数据、控制信号。
  • 时钟与复位:提供最基本的“心跳”和“复位”信号。
  • 结果监测:看DUT的输出对不对。
  • 仿真控制:什么时候开始,什么时候结束。

嗯,这里要注意,Testbench本身是不需要端口的。它就是一个顶层模块,内部把所有信号都定义好,然后直接连到DUT上。

我给大家画个图,这样更直观:

Testbench 结构图 时钟/复位生成 clk_gen / rst_gen 激励生成 stimulus_gen DUT 待测设计 结果 监测 仿真 控制 图例说明: 时钟/复位与激励生成(蓝色) DUT(橙色) 结果监测(绿色) 仿真控制(红色)

你看,这个结构很清晰。激励从左边进来,DUT在中间处理,结果从右边出去。时钟和复位是全局的,仿真控制则负责协调整个流程。

3.2 initial 与 always 块

这两个是Verilog里最核心的过程块。我刚开始学的时候,也经常搞混它们。其实很简单:

  • initial块:只执行一次。适合做初始化、配置、产生单次激励。
  • always块:循环执行。适合做时钟、周期性信号、组合逻辑。

举个例子,你一看就明白:

// initial 块:只执行一次
initial begin
    a = 0;
    b = 1;
    #10 a = 1;
    #20 b = 0;
    #30 $finish;
end

// always 块:永远循环
always begin
    #5 clk = ~clk;  // 产生周期为10的时钟
end

我在项目中遇到过一个问题:有人用initial块去驱动一个需要持续变化的信号,结果仿真跑了一会儿信号就停了。为什么?因为initial只执行一次啊!你想想看,如果你想让一个信号一直变化,就得用always块。

小技巧: 我个人习惯,把initial块只用来做“一次性”的事情,比如配置寄存器、加载测试向量。而always块则用来做“持续性”的事情,比如时钟、数据流。

3.3 时钟与复位生成

时钟和复位,是Testbench的“心脏”和“开关”。没有它们,DUT根本动不起来。

时钟生成:

最常用的方法,就是用always块:

reg clk;
initial clk = 0;
always #5 clk = ~clk;  // 50MHz时钟(周期10ns)

嗯,这里要注意,如果你需要多个不同频率的时钟,可以这样:

reg clk_100m, clk_50m;
initial begin
    clk_100m = 0;
    clk_50m  = 0;
end
always #5  clk_100m = ~clk_100m;  // 100MHz
always #10 clk_50m  = ~clk_50m;   // 50MHz

复位生成:

复位信号,我一般用initial块来产生:

reg rst_n;
initial begin
    rst_n = 0;      // 先复位
    #100;
    rst_n = 1;      // 释放复位
    #200;
    rst_n = 0;      // 再来一次复位(模拟复位抖动)
    #50;
    rst_n = 1;
end
注意: 我曾经吃过一次亏。复位释放的时间点,一定要确保时钟已经稳定。否则DUT可能会进入一个不确定的状态。我建议,先让时钟跑几个周期,再释放复位。

3.4 基本激励编写

激励,就是你要喂给DUT的数据。怎么写呢?我给大家总结了几种常见模式:

模式一:直接赋值

最简单,适合控制信号:

initial begin
    data_in = 8'h00;
    #10 data_in = 8'hA5;
    #20 data_in = 8'h5A;
end

模式二:循环赋值

适合批量数据:

integer i;
initial begin
    for(i=0; i<256; i=i+1) begin
        data_in = i;
        #10;
    end
end

模式三:任务封装

我个人最喜欢这种方式。把常用的激励写成任务,调用起来特别方便:

task write_reg(input [7:0] addr, input [7:0] data);
    begin
        @(posedge clk);
        cs_n = 0;
        wr_n = 0;
        addr_bus = addr;
        data_bus = data;
        @(posedge clk);
        cs_n = 1;
        wr_n = 1;
    end
endtask

initial begin
    // 调用任务
    write_reg(8'h01, 8'hFF);
    #100;
    write_reg(8'h02, 8'h00);
end
核心要点: 写激励的时候,一定要考虑时序。你的激励什么时候给,DUT什么时候采样,这两个时间点要对齐。否则,你给的信号DUT没采到,那仿真结果就是错的。

好了,关于Testbench的基础,咱们就聊这么多。记住,写Testbench就像搭积木,结构清晰了,剩下的就是往里面填内容。时钟、复位、激励,这三样东西搞定了,你的Testbench就成功了一大半。


专注资料整理