测试平台
测试平台(Testbench)是验证设计功能正确性的核心工具。一个完善的测试平台不仅能够验证设计的功能,还能帮助发现潜在的边界问题和时序问题。本章将详细介绍如何编写高效、可靠的测试平台。
测试平台基础
测试平台的作用
测试平台在数字电路设计中扮演着至关重要的角色。与软件测试不同,硬件设计一旦制造完成就无法修改,因此在设计阶段进行充分的验证至关重要。
测试平台的主要作用包括:
- 功能验证:确认设计是否满足功能规范
- 边界测试:验证设计在边界条件下的行为
- 错误检测:发现设计中的错误和潜在问题
- 回归测试:在修改设计后确保原有功能正常
测试平台架构
一个完整的测试平台通常包含以下几个部分:
各部分职责:
- 测试激励生成器:产生输入信号,模拟实际工作场景
- 被测模块(DUT):待验证的设计模块
- 结果检查器:比较实际输出与期望输出
- 时钟/复位生成:提供必要的时钟和复位信号
- 测试报告:记录测试结果和发现的错误
基本结构
一个基本的测试平台结构如下:
`timescale 1ns / 1ps
module tb_example;
// ==========================================
// 1. 信号声明
// ==========================================
reg clk; // 时钟信号
reg reset; // 复位信号
reg [7:0] data_in; // 输入数据
wire [7:0] data_out; // 输出数据
// ==========================================
// 2. 实例化被测模块(DUT)
// ==========================================
my_module u_dut (
.clk(clk),
.reset(reset),
.data_in(data_in),
.data_out(data_out)
);
// ==========================================
// 3. 时钟生成
// ==========================================
initial begin
clk = 0;
forever #5 clk = ~clk; // 100MHz 时钟
end
// ==========================================
// 4. 测试激励
// ==========================================
initial begin
// 初始化
reset = 1;
data_in = 0;
// 复位序列
#20 reset = 0;
// 测试用例
#10 data_in = 8'h55;
#10 data_in = 8'hAA;
#10 data_in = 8'hFF;
// 结束仿真
#100 $finish;
end
// ==========================================
// 5. 波形输出
// ==========================================
initial begin
$dumpfile("waveform.vcd");
$dumpvars(0, tb_example);
end
endmodule
DUT 与 Testbench 的关系
DUT(Design Under Test)是被测试的模块,Testbench 则是测试环境。两者通过端口连接,Testbench 向 DUT 提供激励,并检查 DUT 的输出。
// DUT:待测试的模块
module adder (
input [7:0] a,
input [7:0] b,
output [8:0] sum
);
assign sum = a + b;
endmodule
// Testbench:测试环境
module tb_adder;
reg [7:0] a, b;
wire [8:0] sum;
// 实例化 DUT
adder u_dut (
.a(a),
.b(b),
.sum(sum)
);
// 测试激励
initial begin
a = 8'd10; b = 8'd20; #10;
if (sum !== 9'd30)
$display("Error: 10 + 20 = %d (expected 30)", sum);
a = 8'd255; b = 8'd1; #10;
if (sum !== 9'd256)
$display("Error: 255 + 1 = %d (expected 256)", sum);
$finish;
end
endmodule
时钟生成
时钟是同步设计的核心。测试平台需要生成稳定的时钟信号来驱动 DUT。
简单时钟
最简单的时钟生成方式是使用 initial 块和 forever 循环:
// 生成 100MHz 时钟(周期 10ns)
initial begin
clk = 0;
forever #5 clk = ~clk;
end
这种方式的优点是简单直观,适用于大多数测试场景。时钟的周期由延迟值决定:#5 表示每 5ns 翻转一次,因此完整的周期是 10ns,对应 100MHz 的频率。
参数化时钟
为了提高代码的可维护性和可重用性,建议将时钟参数化:
parameter CLK_PERIOD = 10; // 时钟周期(ns)
initial begin
clk = 0;
forever #(CLK_PERIOD/2) clk = ~clk;
end
这样可以在实例化测试平台时覆盖时钟周期:
// 默认使用 10ns 周期
tb_example #(.CLK_PERIOD(10)) tb1();
// 使用 20ns 周期(50MHz)
tb_example #(.CLK_PERIOD(20)) tb2();
占空比可调时钟
某些应用场景需要非 50% 占空比的时钟:
parameter HIGH_TIME = 3; // 高电平时间(ns)
parameter LOW_TIME = 2; // 低电平时间(ns)
initial begin
clk = 0;
forever begin
#HIGH_TIME clk = 1; // 高电平持续 3ns
#LOW_TIME clk = 0; // 低电平持续 2ns
end
end
这种时钟的总周期是 5ns(HIGH_TIME + LOW_TIME),占空比为 60%。
相位偏移时钟
在多时钟域设计中,可能需要生成相位偏移的时钟:
// 主时钟
initial begin
clk0 = 0;
forever #5 clk0 = ~clk0;
end
// 相位偏移 90 度的时钟
initial begin
clk90 = 0;
#2.5; // 偏移 1/4 周期
forever #5 clk90 = ~clk90;
end
门控时钟
门控时钟用于在特定条件下禁用时钟:
reg clk_enable;
wire gated_clk;
initial begin
clk = 0;
clk_enable = 1;
forever #5 clk = ~clk;
end
// 生成门控时钟
assign gated_clk = clk & clk_enable;
需要注意的是,在实际设计中,门控时钟需要使用专用的时钟门控单元来避免毛刺。在测试平台中,简单的 AND 门通常就足够了。
多时钟域
多时钟域测试平台需要生成多个独立的时钟:
// 时钟域 A:100MHz
initial begin
clk_a = 0;
forever #5 clk_a = ~clk_a;
end
// 时钟域 B:50MHz
initial begin
clk_b = 0;
forever #10 clk_b = ~clk_b;
end
// 异步时钟关系
initial begin
clk_async = 0;
#3; // 异步偏移
forever #7 clk_async = ~clk_async;
end
复位生成
复位是确保设计从已知状态开始的关键信号。
同步复位
同步复位只在时钟边沿生效:
initial begin
reset = 0;
@(posedge clk); // 等待时钟上升沿
reset = 1; // 激活复位
@(posedge clk);
@(posedge clk);
reset = 0; // 释放复位
end
同步复位的优点是可预测性好,不会产生毛刺问题。
异步复位
异步复位立即生效,与时钟无关:
initial begin
reset = 1; // 立即激活复位
#100; // 保持 100ns
reset = 0; // 释放复位
end
异步复位的优点是响应快,但需要注意复位释放时的同步问题。
复位序列
一个完整的复位序列通常包括:
- 上电复位:模拟系统上电
- 等待稳定:等待时钟和 PLL 锁定
- 释放复位:释放复位信号
- 等待初始化:等待设计完成内部初始化
initial begin
// 上电复位
reset = 1;
#100;
// 释放复位
reset = 0;
#50;
// 可选:再次复位(测试复位功能)
reset = 1;
#20;
reset = 0;
end
复位与初始化的协调
复位后需要确保设计进入正确状态:
initial begin
// 初始化所有输入
reset = 1;
data_in = 0;
enable = 0;
// 复位序列
#100 reset = 0;
// 等待复位释放后的稳定
repeat(5) @(posedge clk);
// 开始正常测试
enable = 1;
end
测试激励
测试激励是测试平台的核心,用于驱动 DUT 并验证其行为。
直接激励
直接激励是最简单的测试方式,直接为输入信号赋值:
initial begin
// 测试用例 1:基本功能
data_in = 8'h00;
#10;
// 测试用例 2:典型值
data_in = 8'h55;
#10;
// 测试用例 3:边界值
data_in = 8'hFF;
#10;
$finish;
end
这种方式适用于简单的测试场景,但难以覆盖所有可能的输入组合。
循环激励
循环激励可以自动生成大量测试用例:
integer i;
initial begin
// 遍历所有可能的 8 位输入
for (i = 0; i < 256; i = i + 1) begin
data_in = i[7:0];
#10;
// 检查输出
if (expected_output[data_in] !== actual_output) begin
$display("Error at input %h", data_in);
end
end
$finish;
end
随机激励
随机激励可以发现难以预见的边界情况:
initial begin
repeat (100) begin
data_in = $random; // 32 位随机数
#10;
end
$finish;
end
$random 返回一个 32 位的有符号随机数。如果只需要部分位,可以截取:
data_in = $random[7:0]; // 只使用低 8 位
约束随机激励
约束随机激励可以生成更符合实际场景的测试用例:
integer delay;
integer data;
initial begin
repeat (50) begin
// 随机延迟(1-20ns)
delay = $urandom_range(1, 20);
#delay;
// 约束随机数据(0-255)
data = $urandom_range(0, 255);
data_in = data[7:0];
end
$finish;
end
$urandom_range(min, max) 返回指定范围内的无符号随机数。
从文件读取激励
从文件读取测试向量可以方便地管理大量测试用例:
// 定义测试向量存储器
reg [15:0] test_vectors [0:999];
integer vector_num;
initial begin
// 读取测试向量文件
$readmemh("test_vectors.txt", test_vectors);
// 应用测试向量
for (vector_num = 0; vector_num < 1000; vector_num = vector_num + 1) begin
// 解析测试向量
{data_in, expected_out} = test_vectors[vector_num];
// 等待稳定
#10;
// 检查结果
if (actual_out !== expected_out) begin
$display("Error at vector %d", vector_num);
end
end
$finish;
end
测试向量文件格式:
// test_vectors.txt
00_FF // data_in=00, expected_out=FF
55_AA // data_in=55, expected_out=AA
AA_55 // data_in=AA, expected_out=55
FF_00 // data_in=FF, expected_out=00
时序敏感激励
对于有时序要求的设计,需要在正确的时钟周期提供激励:
initial begin
// 等待时钟边沿后提供激励
@(posedge clk);
data_in = 8'h55;
valid = 1;
// 保持一个周期
@(posedge clk);
valid = 0;
// 等待响应
wait(ready == 1);
// 继续下一个操作
@(posedge clk);
data_in = 8'hAA;
valid = 1;
end
波形输出
波形文件是调试的重要工具,可以直观地观察信号的时序关系。
VCD 文件
VCD(Value Change Dump)是最通用的波形格式:
initial begin
$dumpfile("waveform.vcd"); // 输出文件名
$dumpvars(0, tb_example); // 记录所有层次
end
$dumpvars 的第一个参数控制记录深度:
0:记录所有层次1:只记录顶层n:记录 n 层深度
选择性记录
只记录感兴趣的信号可以减小文件大小:
initial begin
$dumpfile("waveform.vcd");
$dumpvars(1, tb_example); // 只记录顶层信号
$dumpvars(0, tb_example.u_dut); // 记录 u_dut 及其子模块
end
波形格式对比
| 格式 | 工具 | 特点 |
|---|---|---|
| VCD | 通用 | 文本格式,通用性好 |
| VCD | GTKWave | 开源波形查看器 |
| FSDB | Verdi | 商业工具,性能好 |
| WDB | ModelSim | Mentor 专用格式 |
| LXT | GTKWave | 压缩格式 |
使用 GTKWave 查看波形
生成 VCD 文件后,可以使用 GTKWave 查看:
# 编译和仿真
iverilog -o sim.vvp counter.v tb_counter.v
vvp sim.vvp
# 查看波形
gtkwave waveform.vcd
GTKWave 常用快捷键:
| 快捷键 | 功能 |
|---|---|
| Space | 放大 |
| Backspace | 缩小 |
| Home | 跳到开始 |
| End | 跳到结束 |
| Ctrl+S | 保存配置 |
结果检查
自动检查是测试平台的关键功能,可以快速发现设计错误。
自动比较
最简单的检查方式是直接比较期望值和实际值:
reg [7:0] expected;
wire [7:0] actual;
initial begin
// 测试用例 1
data_in = 8'h55;
expected = 8'hAA; // 期望输出
#10;
if (actual !== expected) begin
$display("Error: expected %h, got %h", expected, actual);
$finish;
end
// 测试用例 2
data_in = 8'hAA;
expected = 8'h55;
#10;
if (actual !== expected) begin
$display("Error: expected %h, got %h", expected, actual);
$finish;
end
$display("All tests passed!");
$finish;
end
使用任务检查
将检查逻辑封装成任务,提高代码复用性:
// 定义检查任务
task check_result;
input [7:0] expected;
input [7:0] actual;
input [255:0] test_name;
begin
if (actual !== expected) begin
$display("[%0t] FAIL: %s - expected %h, got %h",
$time, test_name, expected, actual);
end else begin
$display("[%0t] PASS: %s", $time, test_name);
end
end
endtask
// 使用任务
initial begin
data_in = 8'h55;
#10;
check_result(8'hAA, data_out, "Test case 1: invert 0x55");
data_in = 8'hAA;
#10;
check_result(8'h55, data_out, "Test case 2: invert 0xAA");
$finish;
end
使用函数计算期望值
对于复杂的计算,可以使用函数计算期望值:
// 计算期望输出
function [7:0] compute_expected;
input [7:0] data_in;
begin
compute_expected = ~data_in; // 示例:期望输出是输入的反
end
endfunction
initial begin
data_in = 8'h55;
#10;
if (data_out !== compute_expected(data_in)) begin
$display("Mismatch: input=%h, expected=%h, actual=%h",
data_in, compute_expected(data_in), data_out);
end
end
统计测试结果
统计测试通过率和错误数量:
integer total_tests;
integer passed_tests;
integer failed_tests;
initial begin
total_tests = 0;
passed_tests = 0;
failed_tests = 0;
end
task check_and_count;
input [7:0] expected;
input [7:0] actual;
begin
total_tests = total_tests + 1;
if (actual === expected) begin
passed_tests = passed_tests + 1;
end else begin
failed_tests = failed_tests + 1;
$display("FAIL: expected %h, got %h", expected, actual);
end
end
endtask
// 最终报告
initial begin
// ... 测试代码 ...
#1000;
$display("\n========================================");
$display("Test Summary:");
$display(" Total: %0d", total_tests);
$display(" Passed: %0d", passed_tests);
$display(" Failed: %0d", failed_tests);
$display("========================================\n");
$finish;
end
显示和监控
Verilog 提供了多种显示和监控任务来跟踪仿真过程。
$display
$display 是最常用的显示任务,输出后自动换行:
$display("Simulation started at time %0t", $time);
$display("Data: hexadecimal=%h, decimal=%d, binary=%b", data, data, data);
$display("String: %s", "Hello, Verilog!");
$write
$write 与 $display 类似,但不自动换行:
$write("Progress: ");
for (i = 0; i < 10; i = i + 1) begin
$write("%d ", i);
end
$write("\n"); // 手动换行
$monitor
$monitor 持续监控信号变化,当任何被监控的信号变化时自动输出:
initial begin
$monitor("[%0t] clk=%b, reset=%b, data=%h, result=%h",
$time, clk, reset, data_in, data_out);
end
注意:整个仿真中只能有一个活动的 $monitor。新的 $monitor 会替换旧的。
$strobe
$strobe 在当前时间步结束时输出,可以避免在时间步中间的多次变化:
always @(posedge clk) begin
$strobe("[%0t] After clock edge: data_out = %h", $time, data_out);
end
格式化说明符
| 说明符 | 描述 | 示例 |
|---|---|---|
| %h, %H | 十六进制 | %h → FF |
| %d, %D | 十进制 | %d → 255 |
| %b, %B | 二进制 | %b → 11111111 |
| %o, %O | 八进制 | %o → 377 |
| %t, %T | 时间 | %t → 100 |
| %s, %S | 字符串 | %s → Hello |
| %c, %C | ASCII 字符 | %c → A |
| %m, %M | 模块层次路径 | %m → tb.u_dut |
| %v, %V | 网络信号强度 | - |
文件操作
文件操作可以读取测试数据和保存仿真结果。
读取文件
$readmemh 读取十六进制文件
reg [7:0] mem [0:255];
initial begin
// 读取整个文件
$readmemh("data.hex", mem);
// 从指定地址开始
$readmemh("data.hex", mem, 16);
// 指定地址范围
$readmemh("data.hex", mem, 16, 31);
end
十六进制文件格式:
// data.hex
// 注释以 // 开头
00 // 地址 0
01 // 地址 1
02 // 地址 2
@10 // 跳转到地址 16(十六进制)
10 // 地址 16
11 // 地址 17
12 // 地址 18
$readmemb 读取二进制文件
reg [7:0] mem [0:255];
initial begin
$readmemb("data.bin", mem);
end
写入文件
integer file;
initial begin
// 打开文件
file = $fopen("output.txt", "w");
// 写入数据
$fdisplay(file, "Simulation Results");
$fdisplay(file, "Time: %0t", $time);
$fdisplay(file, "Data: %h", data_out);
// 关闭文件
$fclose(file);
end
文件打开模式
| 模式 | 描述 |
|---|---|
| "r" | 只读 |
| "w" | 只写(覆盖现有文件) |
| "a" | 追加 |
| "r+" | 读写 |
| "w+" | 读写(覆盖) |
逐行读取
integer file;
reg [255:0] line;
initial begin
file = $fopen("input.txt", "r");
while (!$feof(file)) begin
$fgets(line, file); // 读取一行
$display("%s", line);
end
$fclose(file);
end
高级测试技术
使用任务封装测试
将常用的测试序列封装成任务:
// 任务:发送一个字节
task send_byte;
input [7:0] data;
begin
@(posedge clk);
data_out = data;
valid = 1;
wait(ack == 1);
@(posedge clk);
valid = 0;
end
endtask
// 使用任务
initial begin
send_byte(8'h55);
send_byte(8'hAA);
send_byte(8'hFF);
$finish;
end
等待条件
Verilog 提供了多种等待条件的方式:
initial begin
// 等待信号变化
@(posedge ready);
// 等待特定值
wait(state == IDLE);
// 等待超时
fork
begin
wait(done == 1);
$display("Operation completed");
end
begin
#10000;
$display("Timeout!");
$finish;
end
join_any
disable fork; // 取消未完成的进程
end
并行测试
使用 fork-join 实现并行测试:
initial begin
fork
// 进程 1:数据输入
begin
repeat(10) begin
@(posedge clk);
data_in = $random;
valid = 1;
end
end
// 进程 2:控制信号
begin
repeat(5) begin
@(posedge clk);
enable = 1;
@(posedge clk);
enable = 0;
end
end
join
// 两个进程都完成后继续
$display("Both processes completed");
end
fork-join 变体
| 结构 | 行为 |
|---|---|
| fork...join | 等待所有进程完成 |
| fork...join_any | 等待任一进程完成 |
| fork...join_none | 不等待,立即继续 |
断言
使用简单断言检测错误条件:
always @(posedge clk) begin
// 简单断言
if (state == WRITE && !write_enable) begin
$display("Error: Write state without write enable");
$finish;
end
end
完整测试平台示例
计数器测试平台
`timescale 1ns / 1ps
module tb_counter;
// ==========================================
// 参数定义
// ==========================================
parameter WIDTH = 8;
parameter CLK_PERIOD = 10;
// ==========================================
// 信号声明
// ==========================================
reg clk;
reg reset;
reg enable;
wire [WIDTH-1:0] count;
wire carry;
// ==========================================
// 实例化被测模块
// ==========================================
counter #(.WIDTH(WIDTH)) u_dut (
.clk(clk),
.reset(reset),
.enable(enable),
.count(count),
.carry(carry)
);
// ==========================================
// 时钟生成
// ==========================================
initial begin
clk = 0;
forever #(CLK_PERIOD/2) clk = ~clk;
end
// ==========================================
// 波形输出
// ==========================================
initial begin
$dumpfile("counter.vcd");
$dumpvars(0, tb_counter);
end
// ==========================================
// 测试统计
// ==========================================
integer total_tests;
integer passed_tests;
integer failed_tests;
// ==========================================
// 测试任务
// ==========================================
task check_value;
input [WIDTH-1:0] expected;
input [255:0] test_name;
begin
total_tests = total_tests + 1;
if (count === expected) begin
passed_tests = passed_tests + 1;
$display("[%0t] PASS: %s (count=%h)", $time, test_name, count);
end else begin
failed_tests = failed_tests + 1;
$display("[%0t] FAIL: %s (expected=%h, got=%h)",
$time, test_name, expected, count);
end
end
endtask
// ==========================================
// 主测试流程
// ==========================================
initial begin
// 初始化统计
total_tests = 0;
passed_tests = 0;
failed_tests = 0;
// 初始化信号
reset = 1;
enable = 0;
// ========================
// 测试 1:复位功能
// ========================
$display("\n=== Test 1: Reset ===");
#20 reset = 0;
check_value(0, "After reset");
// ========================
// 测试 2:基本计数
// ========================
$display("\n=== Test 2: Basic Counting ===");
enable = 1;
repeat(10) @(posedge clk);
check_value(10, "After 10 clocks");
// ========================
// 测试 3:暂停计数
// ========================
$display("\n=== Test 3: Pause Counting ===");
enable = 0;
repeat(5) @(posedge clk);
check_value(10, "After pause");
// ========================
// 测试 4:继续计数
// ========================
$display("\n=== Test 4: Resume Counting ===");
enable = 1;
repeat(10) @(posedge clk);
check_value(20, "After resume");
// ========================
// 测试 5:复位中断
// ========================
$display("\n=== Test 5: Reset During Count ===");
reset = 1;
@(posedge clk);
reset = 0;
check_value(0, "After reset during count");
// ========================
// 测试 6:溢出
// ========================
$display("\n=== Test 6: Overflow ===");
enable = 1;
// 计数到接近最大值
while (count < {WIDTH{1'b1}} - 1) begin
@(posedge clk);
end
// 检查下一个周期溢出到 0
@(posedge clk);
check_value(0, "After overflow");
// ========================
// 测试报告
// ========================
#100;
$display("\n========================================");
$display("Test Summary:");
$display(" Total: %0d", total_tests);
$display(" Passed: %0d", passed_tests);
$display(" Failed: %0d", failed_tests);
$display("========================================\n");
$finish;
end
endmodule
FIFO 测试平台
`timescale 1ns / 1ps
module tb_fifo;
// ==========================================
// 参数定义
// ==========================================
parameter DATA_WIDTH = 8;
parameter DEPTH = 16;
parameter CLK_PERIOD = 10;
// ==========================================
// 信号声明
// ==========================================
reg clk;
reg reset;
reg wr_en;
reg rd_en;
reg [DATA_WIDTH-1:0] din;
wire [DATA_WIDTH-1:0] dout;
wire full;
wire empty;
// ==========================================
// 实例化被测模块
// ==========================================
sync_fifo #(
.DATA_WIDTH(DATA_WIDTH),
.DEPTH(DEPTH)
) u_dut (
.clk(clk),
.reset(reset),
.wr_en(wr_en),
.rd_en(rd_en),
.din(din),
.dout(dout),
.full(full),
.empty(empty)
);
// ==========================================
// 时钟生成
// ==========================================
initial begin
clk = 0;
forever #(CLK_PERIOD/2) clk = ~clk;
end
// ==========================================
// 测试数据
// ==========================================
reg [DATA_WIDTH-1:0] test_data [0:DEPTH-1];
reg [DATA_WIDTH-1:0] read_data [0:DEPTH-1];
integer i;
integer errors;
// ==========================================
// 主测试流程
// ==========================================
initial begin
// 初始化测试数据
for (i = 0; i < DEPTH; i = i + 1) begin
test_data[i] = i[DATA_WIDTH-1:0];
end
errors = 0;
// 初始化信号
reset = 1;
wr_en = 0;
rd_en = 0;
din = 0;
// 复位
#20 reset = 0;
// ========================
// 测试 1:写入数据
// ========================
$display("=== Test 1: Write Data ===");
for (i = 0; i < DEPTH; i = i + 1) begin
@(posedge clk);
wr_en = 1;
din = test_data[i];
$display("[%0t] Writing data[%0d] = %h", $time, i, din);
end
@(posedge clk);
wr_en = 0;
// 检查满标志
if (full !== 1) begin
$display("FAIL: FIFO not full after writing %0d items", DEPTH);
errors = errors + 1;
end else begin
$display("PASS: FIFO full");
end
// ========================
// 测试 2:读取数据
// ========================
$display("\n=== Test 2: Read Data ===");
for (i = 0; i < DEPTH; i = i + 1) begin
@(posedge clk);
rd_en = 1;
@(posedge clk);
read_data[i] = dout;
if (read_data[i] !== test_data[i]) begin
$display("FAIL: Data mismatch at index %0d, expected %h, got %h",
i, test_data[i], read_data[i]);
errors = errors + 1;
end else begin
$display("PASS: data[%0d] = %h", i, read_data[i]);
end
end
@(posedge clk);
rd_en = 0;
// 检查空标志
if (empty !== 1) begin
$display("FAIL: FIFO not empty after reading all data");
errors = errors + 1;
end else begin
$display("PASS: FIFO empty");
end
// ========================
// 测试 3:同时读写
// ========================
$display("\n=== Test 3: Simultaneous Read/Write ===");
// 写入一些数据
for (i = 0; i < DEPTH/2; i = i + 1) begin
@(posedge clk);
wr_en = 1;
din = test_data[i];
end
@(posedge clk);
wr_en = 0;
// 同时读写
fork
begin
// 写进程
for (i = DEPTH/2; i < DEPTH; i = i + 1) begin
@(posedge clk);
wr_en = 1;
din = test_data[i];
end
@(posedge clk);
wr_en = 0;
end
begin
// 读进程
for (i = 0; i < DEPTH/2; i = i + 1) begin
@(posedge clk);
rd_en = 1;
end
@(posedge clk);
rd_en = 0;
end
join
// ========================
// 测试报告
// ========================
#100;
$display("\n========================================");
$display("Test Summary:");
$display(" Total errors: %0d", errors);
if (errors == 0)
$display(" Result: ALL TESTS PASSED");
else
$display(" Result: SOME TESTS FAILED");
$display("========================================\n");
$finish;
end
endmodule
测试平台最佳实践
代码组织
- 使用清晰的代码结构:按功能分段,添加注释
- 参数化设计:使用参数提高可重用性
- 封装常用功能:使用任务和函数
- 添加自检机制:自动检查结果,减少人工验证
测试用例设计
- 边界测试:测试最小值、最大值、边界条件
- 异常测试:测试非法输入、错误恢复
- 随机测试:使用随机激励发现隐藏问题
- 回归测试:修改后重新运行所有测试
调试技巧
- 使用波形查看器:直观观察信号变化
- 打印关键信息:在关键点打印状态
- 分段调试:先测试简单场景,逐步增加复杂度
- 断言检查:在关键条件处添加断言
小结
本章介绍了 Verilog 测试平台编写:
- 测试平台基础:架构、基本结构、DUT 与 Testbench 的关系
- 时钟生成:简单时钟、参数化时钟、多时钟域
- 复位生成:同步复位、异步复位、复位序列
- 测试激励:直接激励、循环激励、随机激励、文件激励
- 波形输出:VCD 文件、选择性记录
- 结果检查:自动比较、统计测试结果
- 文件操作:读取测试数据、保存仿真结果
- 高级技术:任务封装、并行测试、断言
编写完善的测试平台是验证设计正确性的关键。下一章将介绍高级特性。