HDLBits答案(25)_编写Testbench
编写Testbench
前言
恭喜!我们终于来到了HDLBits系列的最后一章——编写Testbench!这也是非常重要的一章,因为在实际的工程开发中,写完代码只是第一步,更重要的是通过仿真来验证代码的功能是否正确。
什么是Testbench? Testbench(测试平台)就是一段专门用来测试设计代码的程序。它会给我们的设计输入激励信号,然后观察输出是否符合预期。
为什么我们需要Testbench? 想象一下,如果你设计了一个复杂的CPU芯片,送去工厂流片后,发现芯片有BUG,那损失可就太大了!所以在流片之前,我们必须通过仿真来确保设计的正确性。Testbench就是我们进行仿真验证的工具。
生活实例类比:Testbench就像是一个产品测试员。设计代码是生产线上下来的产品,Testbench会给产品输入各种测试信号(比如按按键、通电、断电),然后检查产品的输出是否符合预期(比如屏幕亮不亮、声音对不对)。
这一章我们会从简单的时钟生成开始,逐步学习如何编写完整的Testbench。让我们开始吧!
题库
Clock
题目理解
首先我们来看第一道题:生成一个周期为10ps的时钟,初始值为0。
题目提供了一个待测试的模块(DUT,Device Under Test):
1 | module dut ( input clk ) ; |
我们需要做的就是在Testbench中生成一个符合要求的时钟信号,然后连接到这个dut模块。
什么是时钟信号? 时钟信号就像电路的心跳,它以固定的频率在0和1之间跳变。所有的时序电路都是在时钟的节拍下工作的。
时钟周期:时钟信号从0变1,再变0,所需要的时间就是一个时钟周期。题目要求周期是10ps,也就是说:
- 从0到1:5ps
- 从1到0:5ps
- 整个周期:10ps

代码实现
1 | module top_module (); |
要点小结
- Testbench通常是一个没有输入输出端口的模块(顶层模块)
- 待测试的输入信号声明为reg类型,因为我们要在always或initial块中给它赋值
- 待测试的输出信号声明为wire类型,因为它是由DUT驱动的
initial块只在仿真开始时执行一次,通常用来初始化信号always块会循环执行,通常用来生成时钟#n表示延迟n个时间单位
Testbench 1
题目理解
第二道题:生成如下图所示的A、B激励信号。

让我们分析一下波形:
- 时间0:A=0,B=0
- 时间10:A=1,B=0
- 时间15:A=1,B=1
- 时间20:A=0,B=1
- 时间40:A=0,B=0
注意:这里的时间单位是任意的,因为题目没有指定,我们只要按比例写对就行。
代码实现
1 | module top_module ( |
要点小结
- 生成激励波形主要使用
initial块和延迟控制#n - 信号的变化是按顺序执行的:先执行上面的,再执行下面的
- 延迟是相对于上一条语句的时间,不是绝对时间
- 可以把多个操作写在同一个延迟后面:
1
#10 A = 1'b1; // 延迟10,然后A变1
AND gate
题目理解
第三道题:写测试激励来测试一个与门模块。

提供的AND模块声明如下:
1 | module andgate ( |
注意输入是2位的in,不是两个单独的输入。我们需要生成in的激励波形,然后观察out是否符合与门的行为。
从波形图可以看出:
- 时间0:in[0]=0, in[1]=0 → out=0
- 时间10:in[0]=1, in[1]=0 → out=0
- 时间20:in[0]=0, in[1]=1 → out=0
- 时间30:in[0]=1, in[1]=1 → out=1
代码实现
1 | module top_module(); |
要点小结
- 拼接运算符
{}可以把多个信号拼在一起:{in_1, in_0}表示把in_1放在高位,in_0放在低位 - Testbench的结构通常是:
- 声明激励信号(reg类型)
- 声明观察信号(wire类型)
- 在initial块中生成激励
- 实例化DUT,连接信号
Testbench 2
题目理解
第四道题:生成如下图波形图所示的激励信号,来测试模块q7。

模块q7的描述如下:
1 | module q7 ( |
这道题稍微复杂一点,因为它有时钟信号。我们需要:
- 生成时钟信号
- 生成
in和s的激励信号 - 观察
out的输出
代码实现
1 | module top_module(); |
要点小结
- 时钟信号的生成和其他激励信号是并行执行的
initial块和always块是并行的,它们同时开始执行- 多个
initial块也是并行的 - 生成时钟用单独的
initial+always块,生成其他激励用另一个initial块
T flip-flop
题目理解
最后一道题:测试一个T触发器。题目给出了T触发器的模块,我们需要:
- 生成时钟信号
- 生成复位信号,先复位,然后释放复位
- 生成T信号,让触发器翻转
- 观察输出q
什么是T触发器? T触发器是翻转触发器:
- 当T=1时,每个时钟沿输出都会翻转(0变1,1变0)
- 当T=0时,输出保持不变
代码实现
1 | module top_module (); |
要点小结
- 这道题使用了三种不同的过程块:
initial块生成时钟initial块生成复位信号always @(posedge clk)块生成T信号
- 这些块都是并行执行的
- 实际的Testbench中,我们通常还会加入
$display或$monitor等系统任务来打印信息,方便观察
入门者避坑指南
编写Testbench时,初学者很容易犯一些错误。下面我总结了最常见的5个问题:
坑点1:信号类型声明错误
错误表现:1
2
3
4
5// 错误示例
wire clk; // 应该是reg
initial begin
clk = 0; // wire类型不能在initial块中赋值!
end
错误原因:
- 混淆了reg和wire的用法
- reg类型才能在initial或always块中赋值
- wire类型只能由连续赋值assign或模块输出驱动
正确做法:1
2
3
4
5
6
7
8// 正确示例
reg clk; // Testbench中的激励信号声明为reg
initial begin
clk = 1'b0;
end
wire out; // DUT的输出声明为wire
andgate u0 (.out(out), ...);
调试技巧:
- 记住这个简单的规则:
- Testbench中,你要赋值的信号(激励)→ reg
- Testbench中,被DUT驱动的信号(观察)→ wire
坑点2:忘记生成时钟或时钟周期错误
错误表现:1
2
3
4
5
6
7
8// 错误示例1:只初始化了时钟,没有翻转
initial begin
clk = 0;
end
// 缺少always块来翻转时钟!
// 错误示例2:时钟周期不对
always #10 clk = ~clk; // 题目要求周期10ps,这样写周期是20ps!
错误原因:
- 忘记写always块来翻转时钟
- 没有计算好延迟时间:
#n是半周期,整个周期是2*n
正确做法:1
2
3
4
5// 正确示例:周期10ps(半周期5ps)
initial begin
clk = 1'b0;
end
always #5 clk = ~clk; // 半周期5ps,整个周期10ps
调试技巧:
- 时钟周期 = 2 × 半周期延迟
- 写完代码后,在仿真中看一下时钟波形,确认周期是否正确
坑点3:激励信号的时间顺序错误
错误表现:1
2
3
4
5
6// 错误示例:时间顺序搞反了
initial begin
#10 a = 1; // 先延迟10
a = 0; // 然后立即变0(没有延迟!)
#5 b = 1;
end
错误原因:
- 没有理解延迟的含义:
#n是延迟n个时间单位再执行后面的语句 - 如果没有延迟,语句会立即执行
正确做法:1
2
3
4
5
6
7
8
9// 正确示例
initial begin
a = 0; // 时间0
b = 0;
#10; // 延迟10
a = 1; // 时间10
#5; // 延迟5
b = 1; // 时间15
end
调试技巧:
- 每写一个激励变化,就加一个延迟
- 可以画一个简单的时间轴,标注每个信号变化的时间点
坑点4:在initial块中使用非阻塞赋值
错误表现:1
2
3
4
5
6
7// 错误示例
initial begin
clk <= 0; // 应该用阻塞赋值=
a <= 0;
#10;
a <= 1;
end
错误原因:
- 混淆了阻塞赋值和非阻塞赋值的使用场景
- Testbench的initial块中通常使用阻塞赋值
= - 非阻塞赋值
<=主要用在设计代码的时序逻辑中
正确做法:1
2
3
4
5
6
7// 正确示例
initial begin
clk = 1'b0; // 阻塞赋值
a = 1'b0;
#10;
a = 1'b1;
end
调试技巧:
- Testbench的initial块中:用
= - 设计代码的时序逻辑中:用
<= - 设计代码的组合逻辑中:用
=
坑点5:忘记实例化DUT或连接错误
错误表现:1
2
3
4
5
6
7
8
9
10
11
12
13// 错误示例1:忘记实例化DUT!
reg clk;
initial begin
clk = 0;
end
always #5 clk = ~clk;
// 缺少dut的实例化!
// 错误示例2:端口连接错误
dut u0 (
.clk(a), // 连接错了,应该是clk
.a(clk)
);
错误原因:
- 忘记写实例化语句
- 实例化时端口连接错误
正确做法:1
2
3
4
5
6
7// 正确示例
dut u0 (
.clk(clk), // 使用命名连接,不容易出错
.a(a),
.b(b),
.out(out)
);
调试技巧:
- 实例化时使用命名连接(
.port(signal)),不要使用位置连接 - 写完后检查一下:每个DUT的输入是否都连接到了Testbench的reg信号
- 检查一下:每个DUT的输出是否都连接到了Testbench的wire信号
结语
恭喜!HDLBits系列总算是更新结束了!非常感谢HDLBits网站的作者,为我们提供了这么好的学习资源。
回顾整个HDLBits系列,我们学习了:
- Verilog语言基础(向量、模块、过程块等)
- 组合逻辑电路(基本门、多路选择器、加法器等)
- 时序逻辑电路(触发器、计数器、移位寄存器等)
- 有限状态机FSM
- 调试和找BUG
- 从波形图反推电路
- 编写Testbench进行仿真验证
笔者认为,学习Verilog和数字电路设计,关键在于多练习。HDLBits就是一个非常好的练习平台,它的题目由浅入深,覆盖了数字电路设计的各个方面。
最后,感谢大家的阅读!如果代码有错误的地方,欢迎大家在评论区指正。祝大家学习进步!




