3. 验证环境搭建:UVM基础架构、Testbench组件、DUT封装与接口

好,咱们进入第三章。这一章是真正动手干活儿的地方。

前面两章聊了概念和策略,现在我们要把袖子撸起来,搭一个能跑起来的验证环境。说白了,就是给DUT(待测设计)搭个台子,让它能唱戏。

我个人习惯,搭环境之前先想清楚三件事:用什么框架需要哪些零件DUT怎么接进来。这三点搞定了,环境就稳了。

3.1 UVM基础架构:为什么选它?

UVM,全称Universal Verification Methodology。你想想看,如果没有UVM,每个人写验证环境都是野路子,今天张三用C写个driver,明天李四用SystemVerilog写个monitor,项目交接的时候简直灾难。

UVM说白了就是一套标准化打法。它把验证环境拆成几个固定的组件,每个组件有固定的职责和通信方式。我最早接触UVM是在一个AI加速器项目里,当时团队里有人觉得UVM太重,想自己搞一套轻量级的。结果呢?三个月后,那哥们儿自己默默把UVM捡回来了——因为维护成本太高了。

UVM的核心思想: 用面向对象的方式,把验证环境拆成可复用的组件。每个组件只干一件事,组件之间通过TLM(Transaction Level Modeling)通信。

UVM的架构,从上到下大概是这样:

  • test:最顶层,控制整个验证流程。你可以在test里配置参数、启动sequence。
  • env:环境层,把所有的agent、scoreboard、coverage collector组装在一起。
  • agent:代理层,里面包含了driver、monitor、sequencer。一个agent对应一种接口协议。
  • driver:驱动层,负责把transaction转成DUT能识别的信号时序。
  • monitor:监视层,负责抓取DUT的输入输出信号,转成transaction。
  • sequencer:序列器,负责把sequence里的transaction发给driver。
  • scoreboard:计分板,负责比对期望值和实际值。

嗯,这里要注意:UVM的组件都是uvm_component的子类,它们有生命周期——build、connect、run、report。你写代码的时候,一定要在正确的phase里做正确的事。

我的小技巧: 刚开始学UVM,别急着把所有组件都塞进去。先搭一个最简单的环境——一个driver、一个monitor、一个scoreboard就够了。跑通了再往上加东西。我曾经见过有人一上来就搭了十几个agent,结果连编译都过不了,debug了一周。

3.2 Testbench组件:每个零件怎么用?

咱们一个一个来看。Testbench里的组件,说白了就是验证环境的“零件”。每个零件都有它该待的位置。

3.2.1 Driver:给DUT喂数据

Driver的职责很简单:从sequencer拿到transaction,然后按照接口时序,把数据打到DUT的输入端口上。

举个例子,假设DUT是一个简单的加法器,输入是a和b,输出是sum。Driver要做的就是把a和b的值,在时钟上升沿打出去。

class my_driver extends uvm_driver #(my_transaction);
  `uvm_component_utils(my_driver)

  virtual my_if vif;

  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction

  function void build_phase(uvm_phase phase);
    if(!uvm_config_db #(virtual my_if)::get(this, "", "vif", vif))
      `uvm_fatal("NOVIF", "vif not set")
  endfunction

  task run_phase(uvm_phase phase);
    my_transaction tr;
    forever begin
      seq_item_port.get_next_item(tr);
      // 把transaction转成信号
      @(posedge vif.clk);
      vif.a <= tr.a;
      vif.b <= tr.b;
      seq_item_port.item_done();
    end
  endtask
endclass

你看,代码其实不长。关键点在于:seq_item_port.get_next_itemseq_item_port.item_done。这两个调用必须成对出现,否则sequencer会卡住。

我曾经踩过的坑: 有一次我忘了调item_done(),结果sequence发了一个transaction之后,driver就再也不拿下一个了。整个仿真卡在那里,我还以为是DUT的问题,debug了两天才发现是driver少写了一行代码。所以,get_next_item和item_done一定要配对

3.2.2 Monitor:监听DUT的行为

Monitor和Driver正好相反。Driver是往外发数据,Monitor是往里收数据。Monitor把DUT的输入输出信号抓下来,打包成transaction,然后发给scoreboard。

class my_monitor extends uvm_monitor;
  `uvm_component_utils(my_monitor)

  virtual my_if vif;
  uvm_analysis_port #(my_transaction) ap;

  function new(string name, uvm_component parent);
    super.new(name, parent);
    ap = new("ap", this);
  endfunction

  task run_phase(uvm_phase phase);
    my_transaction tr;
    forever begin
      @(posedge vif.clk);
      tr = my_transaction::type_id::create("tr");
      tr.a = vif.a;
      tr.b = vif.b;
      tr.sum = vif.sum;
      ap.write(tr);  // 通过analysis port发出去
    end
  endtask
endclass

Monitor里有个重要的东西叫uvm_analysis_port。它是个广播端口,可以连接多个接收者。比如你可以同时连到scoreboard和coverage collector。

3.2.3 Scoreboard:比对结果

Scoreboard是验证环境的“裁判”。它从monitor拿到实际结果,然后和期望值做比对。

class my_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(my_scoreboard)

  uvm_analysis_imp #(my_transaction, my_scoreboard) imp;

  function new(string name, uvm_component parent);
    super.new(name, parent);
    imp = new("imp", this);
  endfunction

  function void write(my_transaction tr);
    int expected_sum;
    expected_sum = tr.a + tr.b;
    if(tr.sum !== expected_sum) begin
      `uvm_error("MISMATCH", $sformatf("a=%0d, b=%0d, expected=%0d, got=%0d",
                                        tr.a, tr.b, expected_sum, tr.sum))
    end
  endfunction
endclass

这里要注意:Scoreboard用的是uvm_analysis_imp,它和monitor的uvm_analysis_port是一对。一个发,一个收。

3.2.4 Sequence和Sequencer:控制数据流

Sequence是数据的“剧本”,Sequencer是“导演”。Sequence定义了一组transaction怎么产生,Sequencer负责把它们调度给Driver。

class my_sequence extends uvm_sequence #(my_transaction);
  `uvm_object_utils(my_sequence)

  function new(string name = "my_sequence");
    super.new(name);
  endfunction

  task body();
    my_transaction tr;
    repeat(10) begin
      tr = my_transaction::type_id::create("tr");
      tr.randomize();
      start_item(tr);
      finish_item(tr);
    end
  endtask
endclass

嗯,这里有个细节:start_itemfinish_item之间,你可以插入一些延迟或者等待条件。我习惯在start_item之后加一个#1ns,模拟真实场景中的时序不确定性。

3.3 DUT封装与接口:怎么把DUT接进来?

DUT封装,说白了就是给DUT穿件“外套”。这件外套叫wrapper或者interface。它的作用是让UVM环境能和DUT通信。

我一般用SystemVerilog的interface来做这件事。Interface里定义了时钟、复位、数据信号,以及一些控制信号。

interface my_if(input clk, input rst_n);
  logic [7:0] a;
  logic [7:0] b;
  logic [8:0] sum;

  // 时钟块,控制时序
  clocking cb @(posedge clk);
    output a, b;
    input sum;
  endclocking

  // 驱动时序
  task drive(input [7:0] a_val, input [7:0] b_val);
    @(posedge clk);
    a <= a_val;
    b <= b_val;
  endtask

  // 监视时序
  task monitor(output [7:0] a_val, output [7:0] b_val, output [8:0] sum_val);
    @(posedge clk);
    a_val = a;
    b_val = b;
    sum_val = sum;
  endtask
endinterface

Interface写好后,怎么把它传给UVM组件呢?答案是uvm_config_db。在test层,我们把interface set进去,然后在driver和monitor里get出来。

// 在test的build_phase里
function void build_phase(uvm_phase phase);
  uvm_config_db #(virtual my_if)::set(this, "env.agent.driver", "vif", vif);
  uvm_config_db #(virtual my_if)::set(this, "env.agent.monitor", "vif", vif);
endfunction

关键点: uvm_config_db的路径必须和组件的层次路径完全一致。路径写错了,组件就拿不到interface。我见过最离谱的bug,就是路径里多了一个点或者少了一个斜杠,结果仿真跑起来全是X态。

DUT本身怎么封装?通常我们在top层例化DUT和interface,然后把interface的端口和DUT的端口连起来。

module top;
  reg clk, rst_n;
  my_if if_inst(clk, rst_n);
  my_dut dut_inst(
    .clk(clk),
    .rst_n(rst_n),
    .a(if_inst.a),
    .b(if_inst.b),
    .sum(if_inst.sum)
  );

  initial begin
    clk = 0;
    forever #5 clk = ~clk;
  end

  initial begin
    rst_n = 0;
    #20 rst_n = 1;
  end

  initial begin
    run_test("my_test");
  end
endmodule

你看,整个环境就串起来了。Top层负责生成时钟和复位,例化DUT和interface,然后调用run_test启动UVM环境。

我的建议: 刚开始搭环境,别用太复杂的DUT。找一个简单的模块,比如加法器、计数器,先把UVM的流程跑通。我当年第一个UVM环境就是给一个8位加法器搭的,虽然简单,但让我彻底理解了UVM的组件交互方式。后来做AI芯片的验证,那些复杂的DMA、MAC单元,本质上也是这套框架。

3.4 环境搭建的常见问题

最后,我总结几个我踩过的坑,你们遇到类似问题可以少走弯路。

问题 原因 解决办法
仿真卡住不动 driver忘了调item_done() 检查driver的run_phase,确保get_next_item和item_done成对出现
interface信号全是X态 uvm_config_db路径写错了 打印config_db的路径,和组件的层次路径对比
scoreboard收不到数据 analysis_port和analysis_imp没连上 在env的connect_phase里检查port连接
sequence不执行 忘了在test里启动sequence 在test的run_phase里调sequence.start()

好了,这一章的内容就到这儿。UVM环境搭建,说白了就是搭积木——把driver、monitor、scoreboard、sequencer这些组件拼起来,然后用interface把DUT接进来。框架是固定的,但每个项目都有自己的细节。多动手,多debug,慢慢就熟了。

下一章,咱们聊聊验证计划怎么写。那玩意儿看着简单,但写好了能省一半的验证时间。