动态仿真基础:Testbench结构、激励生成、波形查看、覆盖率概念

说实话,动态仿真这东西,我做了十几年,每次带新人时都会发现一个现象——很多人觉得写个Testbench就是给信号赋值、跑个波形就完事了。嗯,其实远没那么简单。

动态仿真,说白了就是让我们的设计在计算机里“跑”起来。你写了一个Verilog模块,它到底能不能正常工作?光靠眼睛看代码是看不出来的。你得给它输入,看它输出,就像你造了一台机器,得通电试试才知道好不好使。

Testbench的基本结构

一个标准的Testbench,我个人习惯把它分成四个部分:

  • 实例化DUT(Design Under Test)—— 把你写的设计模块“装”进来
  • 激励生成 —— 给DUT喂数据
  • 结果监测 —— 看DUT吐出来什么
  • 自动比对 —— 判断结果对不对(这个很多人会忽略)

我在项目中遇到过好几次这样的情况:新人写Testbench,只写了激励生成,跑完波形用肉眼盯着看半天,然后跟我说“好像没问题”。你想想看,一个几千行的设计,你盯着波形看,能看出什么来?

来看一个最简单的Testbench模板:

module tb_counter();

  // 1. 信号声明
  reg        clk;
  reg        rst_n;
  reg        en;
  wire [7:0] count;

  // 2. 实例化DUT
  counter u_counter (
    .clk   (clk),
    .rst_n (rst_n),
    .en    (en),
    .count (count)
  );

  // 3. 时钟生成
  always #5 clk = ~clk;

  // 4. 激励生成
  initial begin
    clk   = 0;
    rst_n = 0;
    en    = 0;
    #20;
    rst_n = 1;
    #10;
    en = 1;
    #200;
    en = 0;
    #100;
    $finish;
  end

  // 5. 结果监测(自动比对)
  initial begin
    $monitor("Time=%0t, count=%d", $time, count);
  end

endmodule

注意看,这个Testbench里我特意加了一个$monitor。为什么?因为跑仿真的时候,你不可能一直盯着波形。让工具帮你打印关键信息,这才是正经做法。

激励生成的几种方式

激励生成这块,我总结下来就三种套路:

方式 适用场景 我的建议
直接赋值 简单测试、边界条件 适合验证单个功能点
循环生成 遍历所有输入组合 适合控制通路验证
随机激励 复杂场景、压力测试 适合数据通路验证

直接赋值最简单,就是#10 a = 1; #10 a = 0;这种。但说实话,你要是只靠这个,验证覆盖率肯定不够。

循环生成我用的比较多,尤其是验证状态机的时候:

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

随机激励呢?嗯,这里有个坑。我曾经吃过一次亏——用$random生成激励,跑了一晚上仿真,第二天一看,关键路径根本没跑到。为什么?因为纯随机分布不均匀,有些边界条件永远碰不到。

注意: 随机激励不等于“随便激励”。建议用约束随机,比如SystemVerilog里的randomize() with {},或者至少对随机结果做一下范围限制。

波形查看——别只会看波形

波形查看,很多人觉得这有什么好讲的?不就是点一下run,然后看波形吗?

其实不然。我见过太多工程师,仿真跑完了,打开波形,从头拖到尾,然后说“嗯,没问题”。你问他怎么判断的?他说“看着好像都对”。

我个人习惯的做法是:

  • 先看关键信号 —— 时钟、复位、使能这些“骨架”信号
  • 再看数据流 —— 输入输出对不对得上
  • 最后看时序关系 —— 建立时间、保持时间有没有违例

还有一个技巧:加标记。在波形上标出关键时间点,比如“这里应该输出0x5A”,然后对照着看。别嫌麻烦,这比事后返工省时间多了。

覆盖率概念——验证的“体检报告”

覆盖率,说白了就是告诉你“你的验证做得够不够”。

我刚开始做验证的时候,根本不管覆盖率。直到有一次,一个模块仿真跑了三天三夜,所有测试用例都过了,结果流片回来发现有个bug——某个状态机状态从来没进入过。从那以后,我再也不敢不看覆盖率了。

覆盖率主要分这么几种:

类型 含义 我的经验
代码覆盖率 代码行、分支、条件是否都执行到 最基本的要求,但不够
翻转覆盖率 信号是否从0变1、从1变0 适合检查信号活动性
功能覆盖率 设计的功能点是否都验证到 这才是关键!
核心观点: 代码覆盖率100%不代表功能没问题。我曾经见过一个模块,代码覆盖率100%,但功能覆盖率只有60%——因为有些功能场景代码里根本没体现出来。

功能覆盖率怎么收集?我一般用SystemVerilog的covergroup:

covergroup cg_alu_op;
  coverpoint alu_op {
    bins add = {0};
    bins sub = {1};
    bins mul = {2};
    bins div = {3};
    bins others = default;
  }
endgroup

这样跑完仿真,你就能看到每个操作码被覆盖了多少次。如果某个操作码一次都没被覆盖到,那你就知道该补什么测试用例了。

知识体系总览

下面这张图,是我自己总结的动态仿真知识体系。你看一眼,心里就有数了:

动态仿真基础 Testbench结构 实例化DUT + 激励 + 监测 激励生成 直接赋值 / 循环 / 随机 (约束随机才是王道) 波形查看 骨架信号 → 数据流 → 时序 加标记!别用肉眼硬看 覆盖率概念 代码覆盖率 ≠ 功能覆盖率 功能覆盖率才是验证的“体检报告”
我的小建议: 刚开始学动态仿真,别急着跑复杂的用例。先把Testbench结构搭好,激励生成写稳,波形会看,覆盖率会读。这四个基本功打扎实了,后面学形式化验证才能事半功倍。

好了,动态仿真基础就聊到这儿。记住一句话:验证不是跑一遍就完事,而是用数据证明你的设计是对的。覆盖率就是那个数据。