第二章:验证方法论基础

UVM基础回顾

UVM,说白了就是SystemVerilog的一套标准库。我刚开始接触UVM时,觉得这东西太复杂了——又是agent又是sequencer的。但用久了你会发现,它其实就是一套搭积木的方法论。

核心组件就这几个:

  • uvm_test:测试用例的入口,就像你写C程序的main函数
  • uvm_env:环境容器,把所有的验证组件装进去
  • uvm_agent:代理,负责和DUT打交道
  • uvm_driver:驱动器,把transaction转成信号
  • uvm_monitor:监视器,把信号转成transaction
  • uvm_scoreboard:计分板,比对期望值和实际值

我记得第一次搭UVM环境时,犯了个低级错误——把driver和monitor的接口搞反了。结果仿真跑了一天,数据全对不上。嗯,从那以后我养成了个习惯:先画好组件连接图再写代码。

关键点:UVM的核心思想是transaction-level modeling。你不需要关心信号怎么跳变,只需要关心数据怎么流动。

来个简单的UVM组件示例:

class my_driver extends uvm_driver#(my_transaction);
  `uvm_component_utils(my_driver)
  
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
  
  virtual task run_phase(uvm_phase phase);
    forever begin
      seq_item_port.get_next_item(req);
      // 把transaction驱动到DUT接口上
      drive_transaction(req);
      seq_item_port.item_done();
    end
  endtask
endclass

这里要注意的是get_next_itemitem_done必须成对出现。我曾经见过一个同事忘了调item_done,结果sequence卡死在那,整个仿真都停了。

基于Scenario的验证思想

传统的验证方法是什么?写一堆testcase,每个testcase测一个功能点。但问题来了——你永远不知道哪些场景组合会触发bug。

基于Scenario的验证思想,说白了就是「讲故事」。你不是在测功能点,你是在模拟真实的使用场景。比如测一个DMA控制器,别只测「单次传输」和「突发传输」,你得想想:

  • 如果CPU在DMA传输过程中改了配置寄存器会怎样?
  • 如果两个DMA通道同时请求总线会怎样?
  • 如果DMA传输到一半,外设突然断连会怎样?

我建议的做法是:先列出一张「场景清单」,把正常场景、异常场景、边界场景都写下来。然后每个场景对应一个sequence。这样你的验证覆盖率会高很多。

个人经验:我习惯把场景分成三类——happy path(正常路径)、sad path(异常路径)、edge case(边界情况)。每个类至少写3个场景,这样基本能覆盖90%的问题。

举个例子,测一个FIFO:

场景类型 具体描述 预期行为
Happy path 连续写入10个数据,然后连续读出 读写指针正常递增
Sad path FIFO满时继续写入 产生满标志,数据不丢失
Edge case 读写指针同时到达边界 空/满标志正确

你想想看,如果只测happy path,那满标志的bug根本发现不了。我遇到过最坑的一次——FIFO满标志延迟了两个时钟周期才拉高,结果数据全写丢了。就是因为场景设计不够全面。

覆盖率驱动的验证流程

覆盖率驱动验证(CDV),说白了就是「用数据说话」。你不再凭感觉说「我觉得测够了」,而是看覆盖率报告说「这里还差5%没覆盖到」。

覆盖率分三种:

  • 代码覆盖率:你的RTL代码跑了多少行?哪些分支没走到?
  • 功能覆盖率:你的验证计划里的功能点覆盖了多少?
  • 断言覆盖率:你写的assertion触发了多少次?

我个人习惯的流程是这样的:

  1. 先写验证计划,列出所有功能点
  2. 根据功能点设计covergroup
  3. 跑回归测试,收集覆盖率
  4. 分析覆盖率报告,找出未覆盖的点
  5. 补充场景或约束,继续跑
  6. 重复直到覆盖率达标

注意:覆盖率达标不等于验证完备。我曾经见过一个项目,功能覆盖率100%,但流片回来还是挂了。为什么?因为覆盖率只覆盖了「你想到的」场景,没想到的bug照样存在。

写个简单的covergroup示例:

covergroup fifo_cg @(posedge clk);
  wr_addr_cp: coverpoint wr_addr {
    bins low = {[0:15]};
    bins mid = {[16:31]};
    bins high = {[32:47]};
    bins full = {63};
  }
  rd_addr_cp: coverpoint rd_addr {
    bins low = {[0:15]};
    bins mid = {[16:31]};
    bins high = {[32:47]};
    bins empty = {0};
  }
  cross_cp: cross wr_addr_cp, rd_addr_cp;
endgroup

这里有个坑——cross coverage的bin数量会爆炸。如果你两个coverpoint各有10个bin,cross之后就是100个bin。我建议只cross那些真正有交互关系的信号,别什么都往里面塞。

嗯,最后说一句:覆盖率驱动验证不是目的,是手段。你的目标是找到bug,不是让覆盖率数字好看。我见过有人为了凑覆盖率,故意写一些永远不会发生的场景——这完全是在自欺欺人。

总结:UVM是工具,Scenario是思路,覆盖率是度量。三者缺一不可。工具用得好,思路想得全,度量做得准,你的验证质量自然就上去了。