4、仿真激励编写:时钟与复位生成、数据激励构造、文件读写操作、任务与函数封装
仿真激励,说白了就是给我们的设计“喂”信号。你想想看,一个芯片或者模块,你总得给它时钟、给它复位、给它数据,它才能动起来吧?这部分写得好不好,直接决定了仿真效率高不高、调试顺不顺。我个人习惯是把激励代码写得像“剧本”一样——清晰、可复用、好维护。
4.1 时钟与复位生成——仿真的“心跳”和“开关”
时钟是仿真的基础。没有时钟,时序逻辑就是一潭死水。我见过不少新手直接写 #5 clk = ~clk; 来产生时钟,这当然可以,但不够灵活。
我建议用参数化写法:
`timescale 1ns / 1ps
module tb_top();
parameter CLK_PERIOD = 10; // 时钟周期,单位ns
reg clk;
reg rst_n;
// 时钟生成:永远为真
initial begin
clk = 0;
forever #(CLK_PERIOD/2) clk = ~clk;
end
// 复位生成:上电后复位一段时间
initial begin
rst_n = 0;
#(CLK_PERIOD * 10); // 复位10个时钟周期
rst_n = 1;
#(CLK_PERIOD * 2);
// 这里可以开始你的测试
end
endmodule
rst_n(低有效),这是行业惯例,不容易搞混。
嗯,这里要注意:forever 语句必须放在 initial 块里,否则仿真器会报错。我在项目中遇到过有人把 forever 直接写在 always 块外面,结果仿真器直接罢工了。
4.2 数据激励构造——给设计“喂”点像样的数据
时钟和复位搞定了,接下来就是数据激励。说白了,就是模拟真实场景下的输入信号。我个人习惯把激励分成几类:
- 固定值激励: 比如给一个寄存器写死 0x5A。
- 随机激励: 用
$random生成随机数,模拟不确定的输入。 - 序列激励: 按照特定时序,依次发送多个数据包。
举个例子,构造一个简单的 AXI 写地址通道激励:
reg [31:0] awaddr;
reg [7:0] awlen;
reg awvalid;
initial begin
// 等待复位结束
@(posedge rst_n);
#(CLK_PERIOD * 5);
// 发送第一个写地址
awaddr = 32'h1000_0000;
awlen = 8'd3; // 4个数据
awvalid = 1;
@(posedge clk);
awvalid = 0;
// 随机发送一些地址
repeat(10) begin
#(CLK_PERIOD * $urandom_range(1, 10));
awaddr = $urandom;
awlen = $urandom % 16;
awvalid = 1;
@(posedge clk);
awvalid = 0;
end
end
4.3 文件读写操作——让仿真“读得懂”外部数据
有时候,我们需要把测试数据放在文件里,仿真时读进来。或者把仿真结果写出去,方便用 Python、Matlab 分析。SystemVerilog 提供了 $fopen、$fread、$fwrite 等系统函数,非常好用。
读文件示例:
integer fd;
reg [7:0] mem [0:1023];
initial begin
fd = $fopen("input_data.txt", "r");
if (fd == 0) begin
$display("Error: Cannot open file!");
$finish;
end
// 读取十六进制数据
for (int i = 0; i < 1024; i++) begin
if ($fscanf(fd, "%h", mem[i]) != 1) break;
end
$fclose(fd);
end
写文件示例:
integer out_fd;
reg [31:0] result;
initial begin
out_fd = $fopen("output_data.txt", "w");
// 假设我们每拍产生一个结果
repeat(100) begin
@(posedge clk);
$fwrite(out_fd, "%h\n", result);
end
$fclose(out_fd);
$display("Data written to output_data.txt");
end
4.4 任务与函数封装——把重复劳动“包”起来
写仿真最怕什么?最怕重复劳动。比如你要反复发送同一个 AXI 写事务,每次都要写一堆 awaddr、wdata、bready 的时序,烦不烦?
我建议用 task 和 function 把这些操作封装起来。这样你的 testbench 会变得非常干净,就像在写“高级命令”一样。
封装一个 AXI 写事务的 task:
task automatic axi_write(
input [31:0] addr,
input [31:0] data,
input [7:0] len
);
// 发送地址
awaddr = addr;
awlen = len;
awvalid = 1;
@(posedge clk);
awvalid = 0;
// 发送数据
for (int i = 0; i <= len; i++) begin
wdata = data + i;
wvalid = 1;
@(posedge clk);
end
wvalid = 0;
// 等待写响应
@(posedge clk);
bready = 1;
@(posedge clk);
bready = 0;
endtask
然后在 initial 块里调用:
initial begin
@(posedge rst_n);
#(CLK_PERIOD * 5);
axi_write(32'h1000_0000, 32'hAABB_CCDD, 8'd3);
axi_write(32'h2000_0000, 32'h1234_5678, 8'd0);
// 想写多少写多少,一行一个事务
end
include 文件里,比如 axi_tasks.svh。这样多个 testbench 可以共用,改一个地方就行。另外,task 里的 automatic 关键字别忘了,否则递归调用时会出问题。我曾经因为这个 bug 查了整整一个下午。
4.5 本章知识体系总览
下面这张图,是我自己总结的仿真激励编写核心逻辑。你可以把它当作一个“检查清单”,每次写 testbench 时对照一下,看看有没有遗漏。
好了,这一章的内容就这些。时钟和复位是基础,数据激励是核心,文件读写是工具,任务和函数是效率。把这四块练熟了,写 testbench 就跟搭积木一样简单。记住,好的激励代码,不仅能让仿真跑得快,更能让你在调试时少掉几根头发。