3、RTL仿真进阶:任务与函数,系统任务($display, $monitor, $time),文件读写
各位同学,咱们继续往下走。RTL仿真基础打牢了,接下来要聊点更实用的东西。说白了,就是怎么让你的仿真代码更聪明、更高效。
我个人习惯把这一节叫做「仿真调试三板斧」——任务与函数、系统任务、文件读写。这三样东西,你几乎每天都会用到。尤其是做后仿真的时候,没有它们,你连波形都看不懂。
3.1 任务(Task)与函数(Function)
先说说任务和函数。很多初学者搞不清它俩的区别,我刚开始也迷糊过。其实很简单:
- 函数:不能带时序,不能有延迟,必须返回一个值。适合做纯组合逻辑的计算。
- 任务:可以带时序,可以有延迟,可以没有返回值。适合做复杂的激励序列。
嗯,这里要注意:函数里不能写 #10 这种延迟语句,也不能用 @(posedge clk)。任务就没这个限制。
我的经验:写验证环境时,我习惯把「检查结果」的逻辑写成函数,把「发送激励」的逻辑写成任务。这样代码结构清晰,不容易出错。
来看个例子:
// 函数:计算CRC校验值
function [7:0] calc_crc;
input [31:0] data;
integer i;
begin
calc_crc = 8'h00;
for (i = 0; i < 32; i = i + 1) begin
if (data[i])
calc_crc = calc_crc ^ (8'h07 << (i % 8));
end
end
endfunction
// 任务:发送一组数据包
task send_packet;
input [31:0] addr;
input [7:0] data;
begin
@(posedge clk);
valid <= 1'b1;
addr_out <= addr;
data_out <= data;
@(posedge clk);
valid <= 1'b0;
end
endtask
你想想看,如果不用任务,每次发数据都要重复写那几行时序控制代码,多累啊。封装成任务后,一行 send_packet(addr, data); 就搞定了。
小技巧:函数可以调用任务吗?不行。但任务可以调用函数。所以如果你不确定用哪个,优先用任务,灵活性更高。
3.2 系统任务:$display, $monitor, $time
系统任务是Verilog/SystemVerilog内置的「打印函数」。我最常用的三个:$display、$monitor、$time。
$display:打印一次
$display 就像C语言的 printf。每次执行到它,就打印一行信息。格式控制符跟C语言几乎一样:
$display("time = %0t, data = %h, addr = %h", $time, data, addr);
这里 %0t 打印时间,%h 打印十六进制。我个人习惯用 %0t 而不是 %t,因为前者不会打印多余的空格。
$monitor:持续监控
$monitor 是个好东西。它会在仿真过程中,只要监控的变量发生变化,就自动打印一次。你想想看,不用写循环,不用写条件判断,一行代码搞定全程监控。
initial begin
$monitor("time=%0t, clk=%b, rst=%b, data=%h", $time, clk, rst, data);
end
我曾经在一个复杂的DMA验证项目中,用 $monitor 监控了十几个关键信号。结果发现一个地址跳变异常,顺着这个线索找到了一个地址对齐的bug。嗯,这招确实省了不少事。
注意:$monitor 在每个时间步长内只打印一次。如果同一个时间点多个变量同时变化,它只会打印一次,但会显示所有变化后的值。别指望它帮你区分变化的先后顺序。
$time:获取当前仿真时间
$time 返回当前仿真时间,单位是仿真器的时间精度。配合 $display 或 $monitor 使用,可以精确定位事件发生的时间点。
我建议你在每个关键打印信息里都加上 $time。为什么?因为后仿真时,时间信息就是你的「坐标」。没有时间戳的打印信息,基本等于废纸。
3.3 文件读写
文件读写是验证中绕不开的一环。尤其是做后仿真时,你需要把仿真结果写出来,跟预期值做对比。
打开文件
integer fd;
fd = $fopen("output.txt", "w");
模式跟C语言一样:"r" 读,"w" 写,"a" 追加。
写入文件
$fwrite(fd, "time=%0t, data=%h\n", $time, data);
$fdisplay(fd, "time=%0t, data=%h", $time, data);
$fwrite 和 $fdisplay 的区别?$fdisplay 会自动换行,$fwrite 不会。我个人习惯用 $fdisplay,少写一个 \n。
读取文件
integer fd;
reg [7:0] mem [0:1023];
fd = $fopen("stimulus.hex", "r");
$readmemh("stimulus.hex", mem); // 读取十六进制文件
$readmemb("stimulus.bin", mem); // 读取二进制文件
这里有个坑:$readmemh 和 $readmemb 读取的文件,每行只能放一个数据。如果你从EDA工具导出的文件格式不对,读进来全是乱的。
避坑指南:我曾经在做一个图像处理IP的验证时,用 $readmemh 读取测试图像数据。结果仿真结果一直不对,查了两天才发现是文件里多了个空行。从那以后,我每次读文件前都会先检查文件格式。
关闭文件
$fclose(fd);
别忘了关文件。虽然仿真器会在仿真结束时自动关闭,但如果你在同一个仿真中反复打开文件而不关闭,文件描述符会耗尽。嗯,我见过有人因为这个导致仿真崩溃。
3.4 综合示例
最后,给一个综合的例子。假设你要验证一个FIFO,需要把写入的数据和读出的数据分别记录到文件里,然后对比:
module fifo_tb;
reg clk, rst, wr_en, rd_en;
reg [7:0] wr_data, rd_data;
integer wr_fd, rd_fd;
initial begin
wr_fd = $fopen("write_data.txt", "w");
rd_fd = $fopen("read_data.txt", "w");
// 监控写入
always @(posedge clk) begin
if (wr_en)
$fdisplay(wr_fd, "%0t %h", $time, wr_data);
end
// 监控读出
always @(posedge clk) begin
if (rd_en)
$fdisplay(rd_fd, "%0t %h", $time, rd_data);
end
// 仿真结束关闭文件
#10000;
$fclose(wr_fd);
$fclose(rd_fd);
$finish;
end
// ... 其他激励代码
endmodule
仿真结束后,用 diff 命令对比两个文件,就能快速定位数据不一致的地方。这个方法我在多个项目中用过,效率很高。
进阶技巧:如果你用SystemVerilog,可以考虑用 $fscanf 逐行读取文件,配合 assert 做自动化比对。这样连手动 diff 都省了。
好了,这一节的内容就到这里。任务与函数帮你组织代码,系统任务帮你调试,文件读写帮你记录和比对。这三样东西组合起来,你的仿真验证效率至少提升一倍。
下一节我们聊聊覆盖率分析,那是验证的「体检报告」,告诉你到底测全了没有。