2. 验证环境搭建:Verilog测试平台结构、时钟与复位生成、DUT实例化
好,咱们正式开始动手搭环境了。
说实话,很多刚入行的朋友觉得验证环境搭建就是写几个always块、例化一下DUT就完事了。嗯,我当年也这么想。直到有一次,我因为时钟和复位的时序没处理好,整整调了三天才发现是复位释放早了半个周期……从那以后,我对环境搭建这件事就再也不敢马虎了。
2.1 Verilog测试平台的基本结构
一个标准的Verilog测试平台,说白了就是三大部分:激励生成、DUT实例化、结果检查。我个人习惯把这三块分得清清楚楚,哪怕是一个很小的模块,也绝不混在一起写。
先看一个最基础的模板:
module tb_fp_add();
// 1. 信号声明
logic clk;
logic rst_n;
logic [31:0] a, b;
logic [31:0] result;
// 2. DUT实例化
fp_add dut (
.clk (clk),
.rst_n (rst_n),
.a (a),
.b (b),
.result(result)
);
// 3. 时钟生成
initial begin
clk = 0;
forever #5 clk = ~clk;
end
// 4. 复位生成
initial begin
rst_n = 0;
#20 rst_n = 1;
end
// 5. 激励与检查
initial begin
// 测试逻辑写在这里
end
endmodule
你想想看,这个结构是不是一目了然?每个部分各司其职。我见过有人把时钟生成和激励写在一个initial块里,结果调试的时候改一个地方就得动另一个逻辑,特别容易出问题。
2.2 时钟生成——别小看这个always
时钟生成看起来简单,但里面有几个坑。最常见的写法是:
initial begin
clk = 0;
forever #5 clk = ~clk;
end
这里有个细节:#5表示半周期5个时间单位,所以周期是10个时间单位,频率就是100MHz(如果时间单位是1ns)。
但如果你需要多个时钟域怎么办?比如我们RISC-V浮点单元里,有时候需要主时钟和慢速时钟。我建议这样写:
initial begin
clk_fast = 0;
forever #5 clk_fast = ~clk_fast;
end
initial begin
clk_slow = 0;
forever #20 clk_slow = ~clk_slow;
end
为什么要分开initial块?因为如果写在一起,代码可读性会变差。而且,万一你要单独关掉某个时钟,分开写更容易控制。
2.3 复位生成——异步复位同步释放
复位这块,很多教材上都会讲「异步复位同步释放」。但在验证环境里,我们通常简化处理。我个人习惯用这种写法:
initial begin
rst_n = 0;
repeat (5) @(posedge clk); // 保持5个时钟周期的复位
rst_n <= 1; // 在时钟上升沿释放
end
为什么要在时钟沿释放?因为这样可以避免复位释放时出现亚稳态。虽然仿真环境里不一定能复现,但养成好习惯总没错。
还有一种情况:你需要测试复位过程中DUT的行为。这时候可以加一个随机复位:
initial begin
rst_n = 0;
#( $urandom_range(10, 50) ); // 随机复位时长
@(posedge clk);
rst_n <= 1;
end
嗯,这个技巧我在做浮点除法器验证时用过。因为复位时长不同,DUT内部状态机的恢复行为可能不一样。随机化一下,能覆盖更多场景。
2.4 DUT实例化——接口匹配是重中之重
DUT实例化看起来就是连线,但这里最容易出问题。我总结了几条经验:
- 端口顺序要一致:Verilog允许按名字连接,也允许按位置连接。我强烈建议用名字连接,哪怕多打几个字。按位置连接一旦端口顺序变了,整个验证环境就废了。
- 未连接端口要处理:如果DUT有输出端口你没连,仿真器会报warning。但如果是输入端口没连,那就是X态,整个仿真都会出问题。
- 参数传递要小心:很多RISC-V浮点单元会通过参数配置位宽。比如:
fp_add #(
.WIDTH(32)
) dut (
.clk (clk),
.rst_n (rst_n),
.a (a),
.b (b),
.result(result)
);
这里有个坑:如果你在顶层改了参数,但忘了改testbench里的例化参数,那仿真结果就全错了。我建议把参数定义成一个宏或者parameter,统一管理。
2.5 整体结构图
下面这张图展示了我们搭建的测试平台整体结构。你可以看到,时钟和复位是独立的生成模块,DUT被包裹在中间,激励和检查分别从两侧接入。
2.6 一些实用的小技巧
最后,分享几个我在实际项目中积累的经验:
- 用`timescale统一时间单位:我习惯在testbench文件头部写
`timescale 1ns/1ps。这样所有的时间延迟都是基于1ns的,精度到1ps,够用了。 - 加一个初始化完成标志:比如
init_done信号,在复位释放后拉高。这样激励块可以等这个信号有效后再开始发数据,避免在复位期间误操作。 - 善用$time记录时间戳:在$display里加上
$time,方便定位问题。比如:$display("[%0t] 输入a=%h, b=%h", $time, a, b);
好了,环境搭建就讲到这里。你按照这个结构去写,基本上不会出大问题。记住:环境搭得稳,后面验证才省心。