欢迎光临
我们一直在努力

89.【SV】SystemVerilog Testbench Example 1

🔧 SystemVerilog 测试平台实战 —— 从零搭建一个完整的验证环境

作为芯片验证工程师,不仅需要理解单个概念,更要学会如何将这些概念组合起来,构建一个完整的、可重用的测试平台。
今天,将以一个实际的寄存器控制器为例,带你一步步搭建一个完整的 SystemVerilog 测试平台。这个例子包含了验证环境的所有核心组件:事务对象、驱动器、监视器、计分板、环境、测试用例,以及它们之间的连接方式。


🎯 一、设计规格:我们要验证什么?

设计功能简介

我们有一个寄存器控制模块 reg_ctrl,它就像一个小型的存储器,可以对内部寄存器进行读写操作。它的协议特点如下:

  • 写操作:在同一个时钟周期内提供地址和数据。
  • 读操作:发送读请求后,数据在下一个时钟周期返回。
  • 忙信号:读操作期间,ready 信号会拉低,表示无法接收新的操作。

接口信号

信号方向描述
clk 输入 时钟
rstn 输入 异步复位,低有效
addr[7:0] 输入 地址
sel 输入 片选信号,高有效
wr 输入 写使能(1=写,0=读)
wdata[15:0] 输入 写数据
rdata[15:0] 输出 读数据
ready 输出 就绪信号,高表示可以接收新操作

🏗️ 二、验证环境架构

我们的测试平台采用标准的分层结构,各个组件分工明确:

┌───────────────────────────────────────────────────────┐
│ Test (测试用例) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────┐ │
│ │ Generator│ │ Driver │ │ Monitor │ │Score-│ │
│ │(本例省略)│ │ │ │ │ │board │ │
│ └──────────┘ └────┬─────┘ └────┬─────┘ └───┬──┘ │
│ │ │ │ │
│ drv_mbx │ scb_mbx │
│ │ │ │ │
└──────────────────────┼──────────────┼────────────┼────┘
│ │ │
┌─────▼──────┐ ┌─────▼──────┐ │
│ Driver │ │ Monitor │ │
│ (线程) │ │ (线程) │ │
└─────┬──────┘ └─────┬──────┘ │
│ │ │
▼ ▼ ▼
┌─────────────────────────────────┐
│ Interface (_if) │
└───────────────┬─────────────────┘


┌───────────────┐
│ DUT │
│ reg_ctrl │
└───────────────┘

组件职责:

  • 事务对象(Transaction):定义数据包格式,包含随机化约束。
  • 驱动器(Driver):从 mailbox 获取事务,按照协议驱动到 DUT。
  • 监视器(Monitor):监测 DUT 接口,捕获事务并发送给计分板。
  • 计分板(Scoreboard):比对预期结果和实际结果。
  • 环境(Environment):容器,实例化并连接所有组件。
  • 测试用例(Test):生成激励,控制测试流程。
  • 接口(Interface):连接 DUT 和验证组件的“总线”。
  • 顶层(Testbench Top):实例化 DUT、接口、测试,启动仿真。

📦 三、核心组件详解

1. 事务对象(Transaction)—— 数据快递

事务对象定义了在验证环境中流动的数据包。它包含了地址、数据、读写类型,并提供了打印函数。

class reg_item;
rand bit [7:0] addr; // 地址(随机化)
rand bit [15:0] wdata; // 写数据(随机化)
bit [15:0] rdata; // 读数据(从DUT返回)
rand bit wr; // 读写类型(1=写,0=读)

function void print(string tag="");
$display("T=%0t [%s] addr=0x%0h wr=%0d wdata=0x%0h rdata=0x%0h",
$time, tag, addr, wr, wdata, rdata);
endfunction
endclass

比喻:这就像快递公司的一个包裹,里面有收件人地址(addr)、货物内容(wdata)、回执(rdata)等。rand 关键字让包裹可以随机生成不同内容。


2. 驱动器(Driver)—— 快递员

驱动器从 mailbox 中取出事务,然后按照协议驱动到 DUT 的接口上。

class driver;
virtual reg_if vif; // 虚拟接口句柄
event drv_done; // 驱动完成事件
mailbox drv_mbx; // 接收事务的邮箱

task run();
@(posedge vif.clk); // 等待第一个时钟沿
forever begin
reg_item item;
drv_mbx.get(item); // 阻塞,直到邮箱中有事务
item.print("Driver");

// 驱动信号到接口
vif.sel <= 1;
vif.addr <= item.addr;
vif.wr <= item.wr;
vif.wdata <= item.wdata;
@(posedge vif.clk); // 等待一个时钟

// 如果 DUT 忙,则等待 ready 变高
while (!vif.ready) @(posedge vif.clk);

// 事务完成,撤销片选
vif.sel <= 0;
->drv_done;
end
endtask
endclass

比喻:驱动器就像快递员,他需要从仓库(mailbox)拿包裹(事务),然后按照客户的地址(协议)把包裹送到指定位置(DUT)。如果门铃响了但没人开门(ready 低),他就要一直等。


3. 监视器(Monitor)—— 监控摄像头

监视器始终盯着 DUT 的接口,一旦发现有效事务,就捕获信息并打包发给计分板。

class monitor;
virtual reg_if vif;
mailbox scb_mbx; // 通向计分板的邮箱

task run();
forever begin
@(posedge vif.clk);
if (vif.sel) begin // 发现事务开始
reg_item item = new;
item.addr = vif.addr;
item.wr = vif.wr;
item.wdata = vif.wdata;

if (!vif.wr) begin // 如果是读操作
@(posedge vif.clk); // 等待下一个时钟
item.rdata = vif.rdata; // 捕获读数据
end
item.print("Monitor");
scb_mbx.put(item); // 发给计分板
end
end
endtask
endclass

比喻:监视器就像监控摄像头,它一直盯着门口(接口),每当有人进出(事务发生),就拍下照片(捕获数据),然后传给保安室(计分板)。


4. 计分板(Scoreboard)—— 保安室

计分板负责检查数据的正确性。它内部有一个参考模型,这里是一个简单的寄存器数组,用于模拟 DUT 的行为。

class scoreboard;
mailbox scb_mbx;
reg_item refq[256]; // 参考模型:256个寄存器

task run();
forever begin
reg_item item;
scb_mbx.get(item);
item.print("Scoreboard");

if (item.wr) begin // 写操作:更新参考模型
if (refq[item.addr] == null)
refq[item.addr] = new;
refq[item.addr] = item;
$display("Store addr=0x%0h data=0x%0h", item.addr, item.wdata);
end
else begin // 读操作:比较实际值与期望值
if (refq[item.addr] == null) begin
// 第一次读该地址,期望值是复位值 0x1234
if (item.rdata != 'h1234)
$display("ERROR! First read addr=0x%0h exp=1234 act=0x%0h",
item.addr, item.rdata);
else
$display("PASS! First read addr=0x%0h", item.addr);
end else begin
if (item.rdata != refq[item.addr].wdata)
$display("ERROR! addr=0x%0h exp=0x%0h act=0x%0h",
item.addr, refq[item.addr].wdata, item.rdata);
else
$display("PASS! addr=0x%0h", item.addr);
end
end
end
endtask
endclass

比喻:计分板就像保安室的监控记录员,他手里有一本记录本(参考模型),记录着每个房间(地址)应该存放什么货物。每当监视器传来新记录,他就核对一下,如果对不上就报警(打印错误)。


5. 环境(Environment)—— 办公室

环境是一个容器,负责实例化所有组件并连接它们。

class env;
driver d0;
monitor m0;
scoreboard s0;
mailbox scb_mbx; // monitor 到 scoreboard 的邮箱
virtual reg_if vif;

function new();
d0 = new;
m0 = new;
s0 = new;
scb_mbx = new(); // 创建邮箱
endfunction

virtual task run();
// 连接各个组件
d0.vif = vif;
m0.vif = vif;
m0.scb_mbx = scb_mbx;
s0.scb_mbx = scb_mbx;

// 并发运行所有组件
fork
s0.run();
d0.run();
m0.run();
join_any // 这里用 join_any,因为 driver 和 monitor 是 forever 循环
endtask
endclass

比喻:环境就像一个办公室,里面有工位(driver)、摄像头(monitor)、记录员(scoreboard),还有连接他们的内部管道(mailbox)。新员工(test)来了就可以直接使用这个办公室。


6. 测试用例(Test)—— 任务指令

测试用例负责生成具体的激励,驱动整个测试流程。

class test;
env e0;
mailbox drv_mbx; // 通向 driver 的邮箱

function new();
drv_mbx = new();
e0 = new();
endfunction

virtual task run();
e0.d0.drv_mbx = drv_mbx; // 连接 driver 邮箱
fork
e0.run(); // 启动环境(后台运行)
join_none

apply_stim(); // 生成激励
endtask

virtual task apply_stim();
reg_item item;

// 写操作:向地址 0xaa 写入随机数据
item = new;
item.randomize() with { addr == 8'haa; wr == 1; };
drv_mbx.put(item);

// 读操作:从地址 0xaa 读出数据
item = new;
item.randomize() with { addr == 8'haa; wr == 0; };
drv_mbx.put(item);
endtask
endclass

比喻:测试就像老板下达的任务指令。他告诉办公室(env)要开始工作了(run),然后给快递员(driver)派发具体任务(put items)。他发完指令后就去干别的事了(join_none)。


7. 接口(Interface)—— 接线板

接口把所有信号封装在一起,方便连接。

interface reg_if (input bit clk);
logic rstn;
logic [7:0] addr;
logic [15:0] wdata;
logic [15:0] rdata;
logic wr;
logic sel;
logic ready;
endinterface


8. 顶层(Testbench Top)—— 总控室

顶层模块实例化所有硬件组件(DUT、接口、时钟),并启动软件测试。

module tb;
reg clk;
always #10 clk = ~clk; // 生成时钟

reg_if _if (clk); // 实例化接口
reg_ctrl u0 ( .clk(clk), .*); // 实例化 DUT,用 .* 自动连接同名信号

initial begin
test t0;
clk <= 0;
_if.rstn <= 0; // 复位
_if.sel <= 0;
#20 _if.rstn <= 1; // 释放复位

t0 = new;
t0.e0.vif = _if; // 传递接口给环境
t0.run(); // 运行测试

#200 $finish; // 仿真结束
end
endmodule


🔄 四、数据流与执行流程

  • 初始化阶段:顶层模块产生时钟,释放复位。
  • 创建环境:测试用例创建环境,环境创建并连接所有组件。
  • 启动后台线程:test.run() 中启动环境的 run 方法,fork join_none 让 driver、monitor、scoreboard 在后台并发运行。
  • 发送激励:测试用例将事务放入 drv_mbx。
  • 驱动:driver 从邮箱取出事务,驱动到接口。
  • 监测:monitor 看到接口上的事务,捕获后放入 scb_mbx。
  • 比对:scoreboard 从邮箱取出事务,与内部参考模型比对,打印结果。
  • 结束:仿真运行指定时间后结束。

  • 🧠 五、总结与心法

    组件角色比喻
    reg_item 数据包 快递包裹
    driver 驱动激励 快递员
    monitor 监测输出 监控摄像头
    scoreboard 数据比对 保安记录员
    env 组件容器 办公室
    test 激励生成 老板的任务指令
    interface 信号连接 接线板
    mailbox 线程通信 内部管道

    验证工程师的心法:

    一个好的测试平台应该像一个高效的办公室:每个人各司其职(driver、monitor、scoreboard),通过标准化的管道(mailbox)传递信息,由统一的管理者(env)协调,最终完成老板(test)下达的任务。这种分层结构让环境易于理解、调试和重用。

    最后的小贴士:

    • 用 mailbox 连接组件,实现解耦。
    • 用 virtual interface 让多个组件共享同一个物理接口。
    • 用 fork…join_none 启动后台线程,主线程继续生成激励。
    • 用 $display 加上时间戳和标签,方便调试。
    • 别忘了在顶层调用 $finish,否则 forever 循环会让仿真永远跑下去。

    掌握了这个示例,你就掌握了构建任何复杂验证环境的基础!

    赞(0)
    未经允许不得转载:171主机测评 » 89.【SV】SystemVerilog Testbench Example 1
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址