Vivado 里的 Testbench 到底在干什么?一篇带你看懂每一部分
2026/6/26 2:51:21 网站建设 项目流程

写在前面:很多时候,Verilog 的 testbench 不一定要自己从零手写,完全可以先让 AI 生成一个版本,再由我们来检查、修改和补强。

所以这篇文章的目标不是把你训练成“手搓 testbench 大师”,而是先解决一个更实际的问题:

当别人给你一份 testbench,或者 AI 帮你生成了一份 testbench 时,你至少要能看懂它每一部分在干什么,知道它到底在测什么。

如果你能做到这一点,后面无论是改代码、补激励、加校验,还是定位 bug,都会轻松很多。


一、先用一句话理解:什么是 testbench?

可以把 testbench 理解成:

专门用来“喂数据、看结果、判断对错”的仿真测试脚本。

如果把 DUT(Design Under Test,被测设计)比作一个学生,那么 testbench 就像:

  • 出题的人
  • 发卷子的人
  • 盯考试过程的人
  • 最后判卷的人

也就是说,testbench 本身不是我们要交付的硬件功能模块,而是专门用来测试那个模块是否正常工作的。

它的核心任务只有 3 件事:

  1. 给 DUT 输入数据
  2. 观察 DUT 输出结果
  3. 判断输出是否符合预期

这三件事,分别对应 testbench 里最常见的三个概念:

  • 激励(stimulus)
  • 监视/观察(monitor)
  • 校验(check / checker)

二、初学者最容易卡住的概念:什么叫“激励”?

很多人第一次看 testbench 时,会被“激励”这个词劝退,感觉很抽象。

其实一点都不玄。

所谓激励,说白了就是:

你主动喂给 DUT 的输入动作。

比如:

  • 给一个加法器输入a=3,b=5
  • 给一个串口模块输入一串字节
  • 给一个滤波器连续输入一段正弦波
  • 先拉低复位,再拉高复位
  • 隔几个时钟切换一次配置参数

这些都叫激励。

你可以把它理解成“给模块出题”。

一个更通俗的类比

如果 DUT 是一个豆浆机:

  • 你按下启动键,是激励
  • 你倒入黄豆和水,是激励
  • 你切换“米糊模式”,也是激励

而 testbench 的工作,就是按顺序把这些动作做出来,然后看机器的反应对不对。

所以以后看到 testbench 里的initialalwaystask里在给信号赋值,不要慌,它本质上就是在:

“模拟外部世界如何去操作你的硬件模块。”


三、看 testbench 时,先抓住这条主线

你拿到任何一份 testbench,都可以先别急着看细节,而是先问自己这 4 个问题:

  1. 它在测哪个 DUT?
  2. 它给 DUT 喂了什么输入?
  3. 它希望 DUT 输出什么结果?
  4. 它是怎么判断“通过”还是“失败”的?

只要这四个问题能回答出来,这份 testbench 你就已经看懂一大半了。


四、testbench 的常见组成部分,到底分别在干什么?

下面结合一份验证 CIC IP 动态切换抽取率的 testbench,来讲 testbench 常见结构。

一份典型的 testbench,通常包括这些部分:

  1. timescale
  2. TB 模块定义
  3. 信号声明
  4. 时钟生成
  5. 复位控制
  6. 激励生成
  7. DUT 实例化
  8. 输出监视与校验
  9. 仿真结束控制

五、timescale是干什么的?

`timescale 1ns/1ps

它定义的是仿真的时间单位时间精度

  • 1ns:表示像#20这种延时,默认按 20ns 理解
  • 1ps:表示仿真能细到 1ps 的精度

你可以把它理解成什么?

它就像一把尺子。

你后面所有的#10#20,都得靠这把尺子来解释。

如果没有这个定义,延时语句的意义就会变得不明确,读代码的人也很难快速理解时序关系。


六、TB 模块为什么通常没有端口?

module tb_dual_cic_sync;

testbench 一般不需要对外连接其他模块,所以通常不带端口列表

因为它不是一个要被综合到 FPGA 里的功能模块,而是仿真环境本身。

可以这样理解

  • DUT:被测试的“产品”
  • TB:搭建出来的“实验室”

实验室不需要再对外提供输入输出接口,它只需要在内部把测试流程跑起来。


七、信号声明区,本质上是在准备“测试现场”

比如:

reg clk_50m; reg rst_n;
reg [7:0] cfg_tdata; reg cfg_tvalid; wire cfg_i_tready; wire cfg_q_tready;
reg signed [15:0] i_in_sample; reg i_in_valid; wire i_in_ready;

这里的本质不是“语法罗列”,而是在提前把测试中会用到的角色准备好。

一般可以分成几类看

  • 时钟/复位
    这是整个系统运行的基础节拍
  • DUT 输入信号
    TB 负责驱动它们,所以常常定义成reg
  • DUT 输出信号
    它们由 DUT 产生,所以常常定义成wire
  • 统计变量
    用来计数、记阶段、判超时

为什么很多输入写成reg

因为 testbench 里经常会在initialalways中给这些信号赋值。

谁来主动改这个值,谁就更像“由 TB 控制”,因此通常写成reg


八、时钟生成块,其实就是在“造节拍器”

initial clk_50m = 1'b0; always #10 clk_50m = ~clk_50m;

这两句的作用很简单:

  • 第一行:把时钟初始值设为 0
  • 第二行:每隔 10ns 翻转一次

所以整个时钟周期就是 20ns,也就是 50MHz。

通俗理解

这就像你在 testbench 里手动放了一个电子节拍器:

滴答 -> 滴答 -> 滴答

所有同步逻辑都跟着这个节拍走。

如果没有这个时钟,很多时序逻辑根本不会动。


九、function 是什么?为什么 testbench 里也会有函数?

原文里有一个正弦查找表函数:

function signed [15:0] sine_lut; input integer idx; begin ... end endfunction

它的作用是:

根据索引idx,返回一个正弦波采样值。

为什么要搞这个函数?

因为 testbench 需要给 DUT 持续输入测试数据。

这里选择的测试数据不是乱给,而是给一组有规律的正弦波采样值。这样做有两个好处:

  1. 输入信号更接近真实场景
  2. 输出结果更容易分析和校验

function 可以怎么理解?

它本质上就是一个“小工具函数”。

你给它一个输入,它马上算出一个结果给你。

在 testbench 里,function 常用来做:

  • 查表
  • 位宽转换
  • 简单计算
  • 期望值生成

这里最关键的一点

这份 testbench 里让:

  • I = sin
  • Q = -sin

这样理想情况下,两路输出如果严格同步,那么:

I_out + Q_out = 0

这个思路非常重要。

因为它告诉我们:

写 testbench 时,不只是“喂点数据”就完了,更重要的是设计一个容易验证对错的输入模式。


十、DUT 实例化,就是把“被测模块”接进实验台

比如:

cic_compiler_0 u_cic_i ( .aclk (clk_50m), .s_axis_config_tdata (cfg_tdata), ... );

这一步可以理解成:

把你真正想测试的硬件模块,插到 testbench 这个实验平台里。

看实例化时,重点看什么?

不用一上来就盯所有端口细节,先看这几件事:

  1. DUT 用的时钟是谁提供的
  2. 输入是谁在驱动
  3. 输出接到了哪些观测信号
  4. 有没有状态/告警信号被接出来

这篇 testbench 里有两个 CIC 实例,分别对应 I 路和 Q 路。

它们:

  • 共用同一个时钟
  • 共用同一套配置总线
  • 各自有独立的数据输入输出

这就为“验证双通道同步性”打下了基础。


十一、task 是什么?为什么配置写入更适合放 task 里?

task automatic write_rate; input [7:0] new_rate; begin ... end endtask

如果说 function 更像“立刻算个结果”的工具,那么 task 更像“执行一段完整流程”的工具。

这里的write_rate在干什么?

它负责完成一次“抽取率配置写入”动作:

  1. 把新配置放到总线上
  2. 拉高valid
  3. 等待对方ready
  4. 握手成功后再结束

为什么这很适合写成 task?

因为这不是一个单纯的计算,而是一个带时序等待的动作流程

它里面会出现:

  • @(posedge clk)
  • while(...)
  • 等待握手

这些都很适合封装进 task。

通俗理解

你可以把 task 看成:

“把一套固定操作流程打包成一个按钮。”

以后要改抽取率时,直接调用:

write_rate(8'd4); write_rate(8'd8);

比你每次手写一遍 AXIS 握手流程清晰太多。


十二、initial主流程,其实就是“测试脚本的导演”

在这份 testbench 里,主initial块承担的是总控作用。

它做了这些事:

  1. 初始化所有寄存器
  2. 先保持复位
  3. 释放复位
  4. 配置第一阶段抽取率
  5. 跑一段时间
  6. 切换第二阶段抽取率
  7. 再跑一段时间
  8. 打印统计结果
  9. $finish结束仿真

为什么这个块很重要?

因为它决定了:

整个测试是按什么顺序进行的。

你完全可以把它当作一份“实验步骤清单”来看。

看到一份 testbench 时,如果你不知道作者到底想测什么,先去看主initial,通常就能看出来。


十三、真正的“激励”通常藏在alwaystask

比如这段:

always @(posedge clk_50m) begin if (!rst_n) begin ... end else if (!cfg_tvalid) begin ... end end

这就是 testbench 中很典型的激励生成块。

它在持续做的事情是:

  • 复位时,清空输入
  • 正常工作时,持续给 I/Q 通道送入采样数据
  • 只有握手成功后,才推进到下一组采样

这一段为什么重要?

因为它体现了 testbench 的核心思想:

不是乱发数据,而是按照 DUT 的接口规则发数据。

尤其是 AXIS 这种握手接口,不能你想发就发,必须配合:

  • valid
  • ready

一个很重要的初学者认知

很多人以为“激励”就是一串赋值语句。

其实不是。

真正有价值的激励,应该满足三点:

  1. 有测试意图
  2. 符合接口时序
  3. 能覆盖你想验证的场景

这篇 testbench 里的激励就不是随便乱写的,它是在验证:

  • 两路输入是否同步
  • 改变抽取率后,模块是否还能稳定工作
  • 输出是否仍然保持预期关系

十四、什么叫“观察输出”?

testbench 不是把数据扔给 DUT 就结束了。

后半段更重要的事情是:

盯住 DUT 的输出,看它有没有按预期响应。

比如这份代码里会观察:

  • i_out_valid
  • q_out_valid
  • i_out_sample
  • q_out_sample
  • event_i_halted
  • event_q_halted

这些信号就是 testbench 的“观察窗口”。

你可以把它理解成医生在看监护仪:

  • 心跳有没有
  • 数值是否正常
  • 是否出现报警

十五、什么叫“校验”?

校验,就是:

把你看到的输出结果,和你心里预期的正确结果做比较。

这份 testbench 的校验思想非常适合教学,因为它很直观。

它是怎么判断对错的?

它用了三层判断:

  1. I/Q 同时输出时,检查I + Q是否等于 0
  2. 检查 I 路和 Q 路是不是同步输出
  3. 检查 IP 有没有出现 halt 告警

为什么这是个好 testbench?

因为它不是只盯一个“最终结果”,而是把常见错误拆成了几类:

  • 结果值错了
  • 通道不同步
  • 内部状态异常

这就是一个合格 checker 的思路。


十六、为什么说 checker 才是 testbench 的灵魂?

很多新手写 testbench 时,最容易犯的错误就是:

只会发激励,不会做判断。

最后仿真跑完了,只能打开波形一拍一拍人工看,非常痛苦。

而 checker 的价值就是:

让 testbench 自动告诉你哪里错了。

比如:

if (($signed(i_out_sample) + $signed(q_out_sample)) !== 35'sd0) begin sync_error_count <= sync_error_count + 1; $display(...); end

这段代码的意义不只是“加了个 if”,而是把“人工看波形判断是否同步”变成了“程序自动报错”。

这会极大提高调试效率。

所以判断一份 testbench 有没有水平,一个很重要的标准就是:

它的 checker 写得怎么样。


十七、为什么还要加“超时保护”?

if (global_timer >= GLOBAL_TIMEOUT) begin timeout_count <= timeout_count + 1; $display("[%0t] global timeout reached", $time); $finish; end

这部分很多初学者会忽略,但实际上非常重要。

为什么重要?

因为仿真里很容易出现这种情况:

  • 某个握手永远等不到
  • 某个状态机卡住了
  • while 循环一直不退出
  • 仿真一直跑,不结束

如果没有超时保护,你的仿真可能会一直挂在那里。

所以“超时退出”其实是一种自保护机制。

你可以把它理解成:

给 testbench 装了一个保险丝。


十八、怎么看一份 testbench 到底有没有测到点子上?

这也是很多人真正关心的问题。

不是 testbench 写得长就代表写得好,而是要看它是否真正验证了目标。

以这篇 CIC 例子来说,它真正验证的是:

  1. 动态切换抽取率时,配置是否成功写入
  2. I/Q 两路输入是否保持同步
  3. I/Q 两路输出是否保持同步
  4. 输出结果是否满足预期关系
  5. IP 是否出现 halt 异常

所以你以后看 testbench 时,不要只盯语法,而要盯:

“它到底在验证哪个行为?”


十九、初学者看 testbench 的推荐顺序

如果你现在还是觉得 testbench 很长、很乱,可以按这个顺序看:

第一步:先看 DUT 实例化

先搞清楚在测谁、有哪些接口。

第二步:看主initial

搞清楚整个测试流程是怎么安排的。

第三步:看激励块

搞清楚输入数据是怎么来的。

第四步:看 checker

搞清楚 testbench 是怎么判断对错的。

第五步:最后再看 function、task、计数器这些辅助结构

这样阅读压力会小很多。


二十、把 testbench 看成一句更完整的话

到这里,你可以把一份 testbench 理解为:

“我先搭好一个仿真环境,然后按一定时序给 DUT 喂输入,再持续观察输出,最后自动判断 DUT 的行为是否符合预期。”

如果你能带着这句话去看代码,很多以前看起来零散的initialalwaystaskfunction就都会串起来了。


二十一、这份 testbench 给我们的一个很重要启发

这篇代码真正值得学的,不只是语法,而是验证思路:

1. 输入不是乱给的,而是有设计过的

I 路给正弦,Q 路给负正弦,这样输出关系容易验证。

2. 校验不是靠肉眼,而是靠 checker 自动判断

能自动报错的 testbench,才是高效的 testbench。

3. 验证不是只测单一场景,而是分阶段测

先测 rate=4,再测 rate=8,这样才能覆盖动态切换场景。

4. testbench 也要防卡死

超时保护是很有工程味道的一部分。


二十二、给初学者的一个结论

如果你现在还不会完整手写 testbench,不用焦虑。

你现阶段最值得先掌握的是这 4 件事:

  1. 知道 testbench 是干什么的
  2. 知道什么叫激励、观察、校验
  3. 拿到一份 testbench 能快速找出主流程
  4. 能判断这份 testbench 究竟在验证什么

先做到“看懂”,再做到“会改”,最后才是“会从零写”。

这条学习路线会更顺。


二十三、最后给你一个万能阅读模板

以后再看到任何 testbench,都先问自己:

1. DUT 是谁? 2. 时钟和复位怎么来? 3. 激励从哪里发? 4. 输出看哪些信号? 5. 通过/失败的判据是什么? 6. 仿真什么时候结束?

如果这 6 个问题你都能答出来,这份 testbench 基本就已经被你拿下了。


结语

testbench 不是“为了仿真而仿真”,它本质上是在回答一个问题:

“我怎么证明这个设计真的按我想要的方式工作?”

而一份好的 testbench,不只是能跑通,更应该让你:

  • 知道输入是什么
  • 知道输出为什么对
  • 知道出错时会错在哪里

如果你正在学 Vivado、Verilog 或 FPGA 仿真,希望这篇文章能帮你从“看不懂 testbench”迈到“至少能拆开看懂每一块在干什么”。

完整代码

`timescale 1ns / 1ps module tb_dual_cic_sync; reg clk_50m; reg rst_n; reg [7:0] cfg_tdata; reg cfg_tvalid; wire cfg_i_tready; wire cfg_q_tready; reg signed [15:0] i_in_sample; reg i_in_valid; wire i_in_ready; reg signed [15:0] q_in_sample; reg q_in_valid; wire q_in_ready; wire [39:0] i_out_tdata_full; wire i_out_valid; reg i_out_ready; wire [39:0] q_out_tdata_full; wire q_out_valid; reg q_out_ready; wire signed [33:0] i_out_sample; wire signed [33:0] q_out_sample; assign i_out_sample = i_out_tdata_full[33:0]; assign q_out_sample = q_out_tdata_full[33:0]; wire event_i_halted; wire event_q_halted; integer sample_idx; integer accepted_input_count; integer paired_output_count; integer sync_error_count; integer phase_id; integer r4_pair_count; integer r8_pair_count; integer timeout_count; reg signed [34:0] iq_sum; reg [7:0] active_rate; reg [31:0] phase_timer; reg [31:0] global_timer; localparam integer PHASE0_CYCLES = 1200; localparam integer PHASE1_CYCLES = 1200; localparam integer GLOBAL_TIMEOUT = 6000; initial clk_50m = 1'b0; always #10 clk_50m = ~clk_50m; function signed [15:0] sine_lut; input integer idx; begin case (idx % 32) 0: sine_lut = 16'sd0; 1: sine_lut = 16'sd6393; 2: sine_lut = 16'sd12539; 3: sine_lut = 16'sd18205; 4: sine_lut = 16'sd23170; 5: sine_lut = 16'sd27245; 6: sine_lut = 16'sd30273; 7: sine_lut = 16'sd32137; 8: sine_lut = 16'sd32767; 9: sine_lut = 16'sd32137; 10: sine_lut = 16'sd30273; 11: sine_lut = 16'sd27245; 12: sine_lut = 16'sd23170; 13: sine_lut = 16'sd18205; 14: sine_lut = 16'sd12539; 15: sine_lut = 16'sd6393; 16: sine_lut = 16'sd0; 17: sine_lut = -16'sd6393; 18: sine_lut = -16'sd12539; 19: sine_lut = -16'sd18205; 20: sine_lut = -16'sd23170; 21: sine_lut = -16'sd27245; 22: sine_lut = -16'sd30273; 23: sine_lut = -16'sd32137; 24: sine_lut = -16'sd32767; 25: sine_lut = -16'sd32137; 26: sine_lut = -16'sd30273; 27: sine_lut = -16'sd27245; 28: sine_lut = -16'sd23170; 29: sine_lut = -16'sd18205; 30: sine_lut = -16'sd12539; default: sine_lut = -16'sd6393; endcase end endfunction cic_compiler_0 u_cic_i ( .aclk (clk_50m), .s_axis_config_tdata (cfg_tdata), .s_axis_config_tvalid(cfg_tvalid), .s_axis_config_tready(cfg_i_tready), .s_axis_data_tdata (i_in_sample), .s_axis_data_tvalid (i_in_valid), .s_axis_data_tready (i_in_ready), .m_axis_data_tdata (i_out_tdata_full), .m_axis_data_tvalid (i_out_valid), .m_axis_data_tready (i_out_ready), .event_halted (event_i_halted) ); cic_compiler_0 u_cic_q ( .aclk (clk_50m), .s_axis_config_tdata (cfg_tdata), .s_axis_config_tvalid(cfg_tvalid), .s_axis_config_tready(cfg_q_tready), .s_axis_data_tdata (q_in_sample), .s_axis_data_tvalid (q_in_valid), .s_axis_data_tready (q_in_ready), .m_axis_data_tdata (q_out_tdata_full), .m_axis_data_tvalid (q_out_valid), .m_axis_data_tready (q_out_ready), .event_halted (event_q_halted) ); task automatic write_rate; input [7:0] new_rate; begin cfg_tdata <= new_rate; cfg_tvalid <= 1'b1; @(posedge clk_50m); while (!(cfg_i_tready && cfg_q_tready)) begin @(posedge clk_50m); end @(posedge clk_50m); cfg_tvalid <= 1'b0; active_rate <= new_rate; $display("[%0t] config accepted, rate=%0d", $time, new_rate); end endtask initial begin rst_n = 1'b0; cfg_tdata = 8'd4; cfg_tvalid = 1'b0; i_in_sample = 16'sd0; i_in_valid = 1'b0; q_in_sample = 16'sd0; q_in_valid = 1'b0; i_out_ready = 1'b1; q_out_ready = 1'b1; sample_idx = 0; accepted_input_count= 0; paired_output_count = 0; sync_error_count = 0; phase_id = 0; r4_pair_count = 0; r8_pair_count = 0; timeout_count = 0; iq_sum = 35'sd0; active_rate = 8'd0; phase_timer = 0; global_timer = 0; #200; rst_n = 1'b1; phase_id = 1; write_rate(8'd4); phase_timer = 0; while (phase_timer < PHASE0_CYCLES) begin @(posedge clk_50m); phase_timer = phase_timer + 1; end repeat (16) @(posedge clk_50m); phase_id = 2; write_rate(8'd8); phase_timer = 0; while (phase_timer < PHASE1_CYCLES) begin @(posedge clk_50m); phase_timer = phase_timer + 1; end repeat (40) @(posedge clk_50m); $display("======================================================"); $display("accepted_input_count = %0d", accepted_input_count); $display("paired_output_count = %0d", paired_output_count); $display("r4_pair_count = %0d", r4_pair_count); $display("r8_pair_count = %0d", r8_pair_count); $display("sync_error_count = %0d", sync_error_count); $display("timeout_count = %0d", timeout_count); $display("======================================================"); $finish; end always @(posedge clk_50m) begin if (!rst_n) begin i_in_valid <= 1'b0; q_in_valid <= 1'b0; i_in_sample <= 16'sd0; q_in_sample <= 16'sd0; sample_idx <= 0; end else if (!cfg_tvalid) begin if ((!i_in_valid || i_in_ready) && (!q_in_valid || q_in_ready)) begin i_in_valid <= 1'b1; q_in_valid <= 1'b1; i_in_sample <= sine_lut(sample_idx); q_in_sample <= -sine_lut(sample_idx); end if (i_in_valid && i_in_ready && q_in_valid && q_in_ready) begin sample_idx <= sample_idx + 1; accepted_input_count <= accepted_input_count + 1; end end end always @(posedge clk_50m) begin if (!rst_n) begin iq_sum <= 35'sd0; paired_output_count <= 0; sync_error_count <= 0; r4_pair_count <= 0; r8_pair_count <= 0; timeout_count <= 0; end else begin global_timer <= global_timer + 1; if (global_timer >= GLOBAL_TIMEOUT) begin timeout_count <= timeout_count + 1; $display("[%0t] global timeout reached", $time); $finish; end if (i_out_valid && i_out_ready && q_out_valid && q_out_ready) begin iq_sum <= $signed(i_out_sample) + $signed(q_out_sample); paired_output_count <= paired_output_count + 1; if (phase_id == 1) begin r4_pair_count <= r4_pair_count + 1; end else if (phase_id == 2) begin r8_pair_count <= r8_pair_count + 1; end if (($signed(i_out_sample) + $signed(q_out_sample)) !== 35'sd0) begin sync_error_count <= sync_error_count + 1; $display("[%0t] phase=%0d rate=%0d FAIL: I=%0d Q=%0d sum=%0d", $time, phase_id, active_rate, $signed(i_out_sample), $signed(q_out_sample), $signed(i_out_sample) + $signed(q_out_sample)); end else begin $display("[%0t] phase=%0d rate=%0d OK: I=%0d Q=%0d sum=%0d", $time, phase_id, active_rate, $signed(i_out_sample), $signed(q_out_sample), $signed(i_out_sample) + $signed(q_out_sample)); end end if ((i_out_valid && i_out_ready) ^ (q_out_valid && q_out_ready)) begin sync_error_count <= sync_error_count + 1; $display("[%0t] phase=%0d rate=%0d skew: i_fire=%0b q_fire=%0b", $time, phase_id, active_rate, (i_out_valid && i_out_ready), (q_out_valid && q_out_ready)); end if (event_i_halted || event_q_halted) begin sync_error_count <= sync_error_count + 1; $display("[%0t] ERROR: halted_i=%0b halted_q=%0b", $time, event_i_halted, event_q_halted); end end end endmodule

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询