跳到主要内容

测试平台

测试平台(Testbench)是验证设计功能正确性的核心工具。一个完善的测试平台不仅能够验证设计的功能,还能帮助发现潜在的边界问题和时序问题。本章将详细介绍如何编写高效、可靠的测试平台。

测试平台基础

测试平台的作用

测试平台在数字电路设计中扮演着至关重要的角色。与软件测试不同,硬件设计一旦制造完成就无法修改,因此在设计阶段进行充分的验证至关重要。

测试平台的主要作用包括:

  1. 功能验证:确认设计是否满足功能规范
  2. 边界测试:验证设计在边界条件下的行为
  3. 错误检测:发现设计中的错误和潜在问题
  4. 回归测试:在修改设计后确保原有功能正常

测试平台架构

一个完整的测试平台通常包含以下几个部分:

各部分职责

  • 测试激励生成器:产生输入信号,模拟实际工作场景
  • 被测模块(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

异步复位的优点是响应快,但需要注意复位释放时的同步问题。

复位序列

一个完整的复位序列通常包括:

  1. 上电复位:模拟系统上电
  2. 等待稳定:等待时钟和 PLL 锁定
  3. 释放复位:释放复位信号
  4. 等待初始化:等待设计完成内部初始化
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通用文本格式,通用性好
VCDGTKWave开源波形查看器
FSDBVerdi商业工具,性能好
WDBModelSimMentor 专用格式
LXTGTKWave压缩格式

使用 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, %CASCII 字符%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

测试平台最佳实践

代码组织

  1. 使用清晰的代码结构:按功能分段,添加注释
  2. 参数化设计:使用参数提高可重用性
  3. 封装常用功能:使用任务和函数
  4. 添加自检机制:自动检查结果,减少人工验证

测试用例设计

  1. 边界测试:测试最小值、最大值、边界条件
  2. 异常测试:测试非法输入、错误恢复
  3. 随机测试:使用随机激励发现隐藏问题
  4. 回归测试:修改后重新运行所有测试

调试技巧

  1. 使用波形查看器:直观观察信号变化
  2. 打印关键信息:在关键点打印状态
  3. 分段调试:先测试简单场景,逐步增加复杂度
  4. 断言检查:在关键条件处添加断言

小结

本章介绍了 Verilog 测试平台编写:

  • 测试平台基础:架构、基本结构、DUT 与 Testbench 的关系
  • 时钟生成:简单时钟、参数化时钟、多时钟域
  • 复位生成:同步复位、异步复位、复位序列
  • 测试激励:直接激励、循环激励、随机激励、文件激励
  • 波形输出:VCD 文件、选择性记录
  • 结果检查:自动比较、统计测试结果
  • 文件操作:读取测试数据、保存仿真结果
  • 高级技术:任务封装、并行测试、断言

编写完善的测试平台是验证设计正确性的关键。下一章将介绍高级特性。