2、验证平台架构设计:模块化设计思想、验证平台的分层结构(激励层、监测层、检查层)、可重用性设计。

好,咱们进入第二章。上一章聊了验证的全局观,这一章咱们得落地了——怎么搭一个像样的验证平台。

说实话,我见过太多人一上来就写 testbench,写到后面发现激励和监测混在一起,换个 DUT 就得重写。嗯,这其实是个架构问题。今天咱们就聊聊怎么用模块化的思想,把验证平台搭得既清晰又好用。

2.1 模块化设计思想:别把鸡蛋放在一个篮子里

模块化,说白了就是「分而治之」。你想想看,一个复杂的验证平台,如果所有代码都堆在一个文件里,那调试起来得多痛苦?

我个人习惯,把验证平台拆成几个独立的模块,每个模块只干一件事。比如:

  • 激励生成模块:只管产生数据,不管数据怎么送到 DUT。
  • 驱动模块:只管把数据按接口时序送进去,不管数据长什么样。
  • 监测模块:只管抓 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 结构图

下面这张图,是我常用的验证平台分层架构。你看一眼就能明白各层之间的关系。

验证平台分层架构图 验证平台分层架构 激励层(Stimulus Layer) 事务生成器 → 序列器 → 驱动 产生随机/定向测试数据,管理事务顺序 DUT(待测设计) 接收激励,输出响应 监测层(Monitor Layer) 输入监测器 → 输出监测器 被动监听接口,将信号级数据转为事务级数据 检查层(Check Layer) 记分板 + 断言 → 比对期望值与实际值 数据流

2.4 可重用性设计:一次编写,到处使用

可重用性,是验证平台设计的终极目标。你想想看,如果每个项目都从头搭平台,那得多累?

我总结了几条可重用性设计的原则:

  1. 参数化:用参数控制数据位宽、深度、时钟频率等。不要硬编码。
  2. 接口标准化:用统一的接口(比如 UVM 的 TLM 接口)连接各模块。
  3. 配置独立:把测试用例的配置写在外部文件里,不写在代码里。
  4. 组件化:把常用的驱动、监测器做成独立的组件,随取随用。
避坑指南: 我曾经在一个项目里,把时钟频率硬编码在驱动模块里。结果换了个 DUT,时钟频率变了,我不得不改驱动代码。后来我改成参数化配置,再也没出过这种问题。

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 本章小结

好,咱们总结一下今天的内容:

  • 模块化设计:每个模块只干一件事,高内聚、低耦合。
  • 三层架构:激励层、监测层、检查层,各层职责清晰。
  • 可重用性:参数化、接口标准化、配置独立、组件化。