4. RTL设计与仿真
仿真,说白了就是给设计“照CT”。你写了一大堆RTL代码,到底能不能跑?功能对不对?边界情况处理了没?这些问题,光靠眼睛看代码是看不出来的。我见过太多工程师,代码写得飞起,一上板就抓瞎——最后发现是仿真没做透。
这一章,咱们就聊聊仿真那些事儿。从Testbench怎么写,到工具怎么用,再到波形怎么看、覆盖率怎么分析。嗯,都是实战中摸爬滚打出来的经验。
4.1 Testbench编写——不只是“喂激励”
很多人觉得Testbench就是给输入信号,然后看输出对不对。其实没那么简单。一个好的Testbench,应该具备三个要素:可复用、自检查、可调试。
4.1.1 基本结构
我习惯把Testbench分成几个模块:
- 时钟与复位生成:最基础的部分,但容易出错
- 激励生成:模拟真实场景的输入
- 参考模型:用行为级描述实现预期行为
- 比较器:自动比对输出与预期
- 覆盖率收集:记录哪些场景被覆盖了
来看一个简单的例子。假设我们要测试一个同步FIFO:
module tb_fifo;
// 参数定义
parameter DATA_WIDTH = 8;
parameter DEPTH = 16;
// 信号声明
reg clk, rst_n;
reg wr_en, rd_en;
reg [DATA_WIDTH-1:0] wr_data;
wire [DATA_WIDTH-1:0] rd_data;
wire full, empty;
// 实例化DUT
fifo #(
.DATA_WIDTH(DATA_WIDTH),
.DEPTH(DEPTH)
) u_fifo (
.clk(clk),
.rst_n(rst_n),
.wr_en(wr_en),
.rd_en(rd_en),
.wr_data(wr_data),
.rd_data(rd_data),
.full(full),
.empty(empty)
);
// 时钟生成
initial begin
clk = 0;
forever #5 clk = ~clk; // 100MHz
end
// 复位
initial begin
rst_n = 0;
#20 rst_n = 1;
end
// 激励
initial begin
// 初始化
wr_en = 0;
rd_en = 0;
wr_data = 0;
// 等待复位释放
@(posedge rst_n);
#10;
// 写数据
repeat(10) begin
@(posedge clk);
wr_en = 1;
wr_data = $random;
end
wr_en = 0;
// 读数据
#20;
repeat(10) begin
@(posedge clk);
rd_en = 1;
end
rd_en = 0;
#100;
$finish;
end
// 自检查
initial begin
// 这里可以加入比对逻辑
// 比如用队列记录写入的数据,读出时比对
end
endmodule
4.1.2 自检查机制
手动看波形?那是小项目才做的事。真正的工程化开发,必须让Testbench自己判断对错。我常用的方法有两种:
- 队列比对法:把写入的数据存到一个队列里,读出时逐个比对
- 参考模型法:写一个行为级的“黄金模型”,输出与DUT实时比对
举个例子,用队列比对FIFO:
// 在Testbench中
reg [DATA_WIDTH-1:0] expected_queue [0:255];
int wr_ptr, rd_ptr;
// 写操作时记录
always @(posedge clk) begin
if (wr_en && !full) begin
expected_queue[wr_ptr] = wr_data;
wr_ptr++;
end
end
// 读操作时比对
always @(posedge clk) begin
if (rd_en && !empty) begin
if (rd_data !== expected_queue[rd_ptr]) begin
$error("Mismatch at time %0t: expected %h, got %h",
$time, expected_queue[rd_ptr], rd_data);
end
rd_ptr++;
end
end
4.2 ModelSim与Vivado Simulator使用
工具嘛,说白了就是顺手就行。ModelSim老牌稳定,Vivado Simulator跟Vivado集成度高。我个人更习惯用Vivado Simulator做综合后仿真,因为时序信息更准。
4.2.1 ModelSim快速上手
ModelSim的常用命令,我整理了一张表:
| 命令 | 作用 | 我的用法 |
|---|---|---|
vlib work |
创建库 | 每次新建项目第一步 |
vlog *.v |
编译Verilog文件 | 可以用通配符,但小心同名文件 |
vsim work.tb_fifo |
启动仿真 | 记得加`-voptargs=+acc`保留所有信号 |
add wave * |
添加波形 | 我习惯先加顶层,再逐层深入 |
run 1us |
运行指定时间 | 配合`run -all`使用 |
一个小技巧:用do文件(脚本)来跑仿真。这样每次改完代码,双击一下do文件就全自动了。我团队里,谁要是还在手动点按钮跑仿真,是要被笑话的。
4.2.2 Vivado Simulator特色
Vivado Simulator有个好处——可以直接用XDC约束文件里的时序信息。做后仿真的时,它能自动反标SDF(标准延迟格式)文件,模拟出真实的门级延迟。
启动方式也简单:
# 在Vivado Tcl Console中
launch_simulation -mode post-implementation -type functional
嗯,这里要注意:后仿真比前仿真慢得多。我一般只在关键模块或流片前才跑后仿。平时用前仿真就够了,省时间。
4.3 波形调试技巧——别在波形里“大海捞针”
波形调试,是仿真里最耗时的环节。我见过有人盯着波形看半小时,就为了找一根信号什么时候跳变的。其实有更好的方法。
4.3.1 设置标记与光标
ModelSim和Vivado都支持标记(Marker)和光标(Cursor)。我的习惯是:
- 先找到出错的时刻(比如$error报出的时间点)
- 在那个位置加一个光标
- 然后往前推,看关键信号的变化
比如,FIFO读数据出错,我会先看读使能(rd_en)和空标志(empty)的时序。是不是在空的时候读了?还是读地址跳错了?
4.3.2 使用波形分组与颜色
信号多了,波形图就是一团乱麻。我习惯把信号分组:
- 控制信号:用蓝色,比如使能、复位
- 数据信号:用绿色,比如数据总线
- 状态信号:用红色,比如满、空标志
这样一眼就能看出问题在哪。Vivado里右键信号就能改颜色,ModelSim里用`wave color`命令也行。
4.3.3 利用$display和$monitor
有时候波形太密,不如直接看打印信息。我常用:
// 在Testbench中
always @(posedge clk) begin
if (wr_en) begin
$display("[%0t] Write: addr=%0d, data=%h", $time, wr_addr, wr_data);
end
if (rd_en) begin
$display("[%0t] Read: addr=%0d, data=%h", $time, rd_addr, rd_data);
end
end
这样,仿真一跑完,看打印日志就能定位问题。比在波形里翻来翻去快多了。
4.4 覆盖率分析——你测全了吗?
覆盖率,说白了就是“你测了多少”。没有覆盖率分析,你根本不知道自己的仿真够不够。我见过有人跑了一万次仿真,结果只覆盖了20%的代码——那跟没跑有什么区别?
4.4.1 覆盖率的种类
| 类型 | 含义 | 我的关注点 |
|---|---|---|
| 代码覆盖率 | 每行代码是否被执行 | 必须100%,否则有死代码 |
| 翻转覆盖率 | 每个信号是否0→1和1→0都发生过 | 重点关注控制信号 |
| 条件覆盖率 | 每个分支条件是否都被评估过 | if-else、case语句容易漏 |
| 功能覆盖率 | 用户自定义的场景是否被覆盖 | 需要手动写covergroup |
4.4.2 如何收集覆盖率
在ModelSim里,加个命令就行:
vsim -coverage work.tb_fifo
add coverage -r /*
run -all
coverage report -file coverage.rpt
Vivado Simulator也类似,在仿真设置里勾上“Enable Coverage”就行。
4.4.3 功能覆盖率的实战
代码覆盖率只能告诉你“代码跑了没”,但跑的对不对,得靠功能覆盖率。比如FIFO,我想测“写满后继续写”的场景:
covergroup fifo_cg @(posedge clk);
// 写满后继续写
coverpoint {full, wr_en} {
bins full_write = {2'b11};
}
// 读空后继续读
coverpoint {empty, rd_en} {
bins empty_read = {2'b11};
}
// 同时读写
coverpoint {wr_en, rd_en} {
bins both = {2'b11};
}
endgroup
fifo_cg cg_inst = new();
这样,仿真结束后,我就能看到这些边界场景有没有被覆盖到。如果某个bin是0%,那就得补测试用例。
4.5 本章小结
仿真这件事,说难不难,说简单也不简单。关键是要形成一套自己的流程:
- Testbench要自检查,别靠眼睛看
- 工具要会用脚本,别手动点
- 波形调试要有方法,别大海捞针
- 覆盖率要分析,别盲目跑