2、验证平台架构设计:模块化设计思想、验证平台的分层结构(激励层、监测层、检查层)、可重用性设计。
好,咱们进入第二章。上一章聊了验证的全局观,这一章咱们得落地了——怎么搭一个像样的验证平台。
说实话,我见过太多人一上来就写 testbench,写到后面发现激励和监测混在一起,换个 DUT 就得重写。嗯,这其实是个架构问题。今天咱们就聊聊怎么用模块化的思想,把验证平台搭得既清晰又好用。
2.1 模块化设计思想:别把鸡蛋放在一个篮子里
模块化,说白了就是「分而治之」。你想想看,一个复杂的验证平台,如果所有代码都堆在一个文件里,那调试起来得多痛苦?
我个人习惯,把验证平台拆成几个独立的模块,每个模块只干一件事。比如:
- 激励生成模块:只管产生数据,不管数据怎么送到 DUT。
- 驱动模块:只管把数据按接口时序送进去,不管数据长什么样。
- 监测模块:只管抓 DUT 的输出,不管数据对不对。
- 检查模块:只管比对数据,不管数据怎么来的。
这样做的好处很明显——高内聚、低耦合。任何一个模块出了问题,你只需要改它自己,不会牵连到其他模块。
2.2 验证平台的分层结构:三层架构
模块化是思想,分层是具体实现。我一般把验证平台分成三层:激励层、监测层、检查层。这三层各司其职,互不干扰。
2.2.1 激励层(Stimulus Layer)
激励层负责产生测试数据。它不关心数据怎么送到 DUT,只关心数据本身。
- 事务生成器(Transaction Generator):产生随机或定向的事务。
- 序列器(Sequencer):管理事务的顺序和优先级。
举个例子,你要验证一个 FIFO,激励层就负责生成写数据和读请求。至于这些数据怎么通过接口送进去,那是驱动的事。
2.2.2 监测层(Monitor Layer)
监测层负责「看」。它被动地监听 DUT 的输入和输出接口,把信号级的数据转换成事务级的数据。
- 输入监测器:抓 DUT 输入接口上的数据。
- 输出监测器:抓 DUT 输出接口上的数据。
监测层不判断数据对不对,它只负责「看到什么就记下来」。判断对错是检查层的事。
2.2.3 检查层(Check Layer)
检查层负责「判对错」。它把监测层抓到的数据,和期望值做比对。
- 记分板(Scoreboard):存储期望值,和实际值比对。
- 断言(Assertion):检查时序协议是否满足。
我曾经犯过一个错:在检查层里直接写监测逻辑。结果检查逻辑一改,监测逻辑也得跟着改,耦合度太高。后来我学乖了,检查层只做比对,不做监测。
2.3 分层架构的 SVG 结构图
下面这张图,是我常用的验证平台分层架构。你看一眼就能明白各层之间的关系。
2.4 可重用性设计:一次编写,到处使用
可重用性,是验证平台设计的终极目标。你想想看,如果每个项目都从头搭平台,那得多累?
我总结了几条可重用性设计的原则:
- 参数化:用参数控制数据位宽、深度、时钟频率等。不要硬编码。
- 接口标准化:用统一的接口(比如 UVM 的 TLM 接口)连接各模块。
- 配置独立:把测试用例的配置写在外部文件里,不写在代码里。
- 组件化:把常用的驱动、监测器做成独立的组件,随取随用。
2.5 可重用性设计对比表
下面这张表,对比了「可重用设计」和「不可重用设计」的差异。你看完就知道该怎么做了。
| 设计维度 | 可重用设计 | 不可重用设计 |
|---|---|---|
| 数据位宽 | 参数化,通过配置设定 | 硬编码为固定值 |
| 接口协议 | 使用标准 TLM 接口 | 直接访问信号级接口 |
| 测试用例 | 外部配置文件驱动 | 写在 testbench 内部 |
| 组件依赖 | 低耦合,独立替换 | 高耦合,牵一发动全身 |
| 跨项目复用 | 直接复用,少量适配 | 需要重写大部分代码 |
2.6 一个简单的可重用驱动示例
光说不练假把式。下面是一个参数化的驱动模块示例,你看它怎么实现可重用性。
// 参数化驱动模块
class driver #(parameter DATA_WIDTH = 8, parameter CLK_PERIOD = 10);
virtual dut_if vif;
mailbox #(transaction) gen2drv;
function new(virtual dut_if vif, mailbox #(transaction) gen2drv);
this.vif = vif;
this.gen2drv = gen2drv;
endfunction
task run();
transaction tr;
forever begin
gen2drv.get(tr);
// 按接口时序驱动数据
@(posedge vif.clk);
vif.data_in <= tr.data;
vif.valid <= 1;
@(posedge vif.clk);
vif.valid <= 0;
end
endtask
endclass
你看,DATA_WIDTH 和 CLK_PERIOD 都是参数。换一个 DUT,只需要改参数值,驱动逻辑完全不用动。
2.7 本章小结
好,咱们总结一下今天的内容:
- 模块化设计:每个模块只干一件事,高内聚、低耦合。
- 三层架构:激励层、监测层、检查层,各层职责清晰。
- 可重用性:参数化、接口标准化、配置独立、组件化。