4、验证环境架构设计:UVM验证环境组件概览

好,咱们进入第四章。这一章我打算聊聊验证环境的骨架——架构设计。说白了,就是你的UVM环境里那些组件该怎么摆、怎么连、怎么分工。

我记得刚入行那会儿,带我的老工程师丢给我一个UVM环境,说“你照着这个写”。我一看,好家伙,agent里塞了三个driver,monitor挂在外面,sequence直接写在test里。能跑,但想复用?门儿都没有。后来我自己踩了不少坑,才慢慢摸清楚这套架构的门道。

4.1 UVM验证环境组件概览

先快速过一遍UVM里那些核心组件,咱们心里有个底。

组件 职责 我常说的比喻
driver 把sequence_item转成接口时序 翻译官,把“人话”转成“机器话”
monitor 抓取接口信号,打包成transaction 监听员,只看不说话
sequencer 管理sequence,给driver喂item 调度员,排好队一个一个来
agent 把driver/monitor/sequencer打包 小团队,负责一个接口协议
env 组装所有agent和reference model 总装车间,把零件拼成整机
test 配置env,启动sequence 导演,喊“action”的那个人

嗯,这张表你最好存着。每次搭环境前看一眼,能少走不少弯路。

4.2 driver/monitor/sequencer 架构设计原则

这三个家伙是agent里的铁三角。我见过很多新手把driver和monitor的代码写成一团,结果换个接口协议就得重写。其实没那么复杂。

4.2.1 driver:只做一件事

driver的职责非常单一:从sequencer拿item,然后驱动到接口上。仅此而已。

我个人的习惯是:driver里绝对不放任何协议检查或数据处理的逻辑。那些东西应该放到monitor或者reference model里。

核心原则:driver只管“发”,不管“对不对”。

// 好的driver写法
class my_driver extends uvm_driver#(my_item);
  virtual task run_phase(uvm_phase phase);
    forever begin
      seq_item_port.get_next_item(req);
      drive_item(req);  // 只负责驱动时序
      seq_item_port.item_done();
    end
  endtask

  virtual task drive_item(my_item item);
    // 这里只做时序转换,不做数据检查
    @(posedge vif.clk);
    vif.data <= item.data;
    vif.valid <= 1'b1;
  endtask
endclass

4.2.2 monitor:只抓不推

monitor是环境的眼睛。它默默观察接口上的信号变化,然后打包成transaction发给scoreboard。

避坑指南:我曾经在一个项目里把monitor和driver写成了双向通信,结果环境跑起来死锁了。monitor应该是完全被动的,它不驱动任何信号,也不等待任何握手。

注意:monitor里不要加任何“等待某个条件”的逻辑。它只负责“看到什么就报告什么”。

class my_monitor extends uvm_monitor;
  virtual task run_phase(uvm_phase phase);
    forever begin
      @(posedge vif.clk);
      if (vif.valid) begin
        my_item item = my_item::type_id::create("item");
        item.data = vif.data;
        item_port.write(item);  // 直接扔出去,不判断对错
      end
    end
  endtask
endclass

4.2.3 sequencer:当好调度员

sequencer其实是最省心的组件。UVM已经帮你写好了,你直接继承就行。除非你有特殊的仲裁需求,否则别动它。

你想想看,sequencer的核心能力是什么?是管理多个sequence的优先级和并发。这个UVM做得已经很好了,咱们别画蛇添足。

4.3 agent/env/test 架构设计原则

好,从单个组件上升到模块级别。这里我踩过的坑最多。

4.3.1 agent:按接口协议打包

agent的划分标准只有一个:按接口协议。一个agent对应一种接口协议。

举个例子,你的DUT有APB接口和AXI接口,那就应该有两个agent:apb_agent和axi_agent。千万别按功能模块来分,否则复用性会大打折扣。

小技巧:agent里要不要包含sequencer?看情况。如果是master agent(主动发起事务),需要sequencer。如果是slave agent(被动响应),可以不要sequencer,但monitor一定要有。

4.3.2 env:只做组装,不做逻辑

env是总装车间。它的任务就是把各个agent、reference model、scoreboard、coverage collector拼在一起,连好TLM端口。

我见过最糟糕的env:里面塞了各种function和task,甚至还有状态机。这完全违背了env的设计初衷。env应该是一个“配置和连接”的容器,不是逻辑处理的地方。

class my_env extends uvm_env;
  my_agent    agt;
  my_model    mdl;
  my_scoreboard scb;

  function void build_phase(uvm_phase phase);
    agt = my_agent::type_id::create("agt", this);
    mdl = my_model::type_id::create("mdl", this);
    scb = my_scoreboard::type_id::create("scb", this);
  endfunction

  function void connect_phase(uvm_phase phase);
    agt.monitor.item_port.connect(mdl.analysis_export);
    mdl.result_port.connect(scb.expected_export);
  endfunction
  // 不要在这里加任何数据处理逻辑!
endclass

4.3.3 test:配置的起点

test是验证环境的入口。它的主要工作是:配置env的参数、设置verbosity、启动sequence。

说白了,test就是告诉环境“今天测什么”。不同的testcase应该通过不同的test类来实现,而不是在同一个test里加if-else。

4.4 可重用性设计

这一节是重点。我做了这么多年验证,最深的体会就是:可重用性不是写出来的,是设计出来的

4.4.1 参数化你的组件

别把数据位宽、时钟周期这些硬编码在组件里。用parameter或者config_db传进去。

class my_driver #(type REQ = uvm_sequence_item) extends uvm_driver#(REQ);
  int data_width;
  // 通过config_db配置,而不是写死在代码里
endclass

4.4.2 用factory覆盖机制

UVM的factory机制是重用的利器。你想换个driver?不用改env,直接在test里override就行。

核心思想:写环境的时候,永远假设“将来有人会替换掉这个组件”。

4.4.3 分离协议层和应用层

这是我最想强调的一点。driver/monitor处理的是协议层(时序、握手),sequence处理的是应用层(数据内容、场景)。这两层一定要分开。

为什么?因为协议层变化少(AHB就是AHB,不会变),应用层变化多(今天测读,明天测写,后天测并发)。分开了,你的sequence就能在不同项目间复用。

4.4.4 我踩过的一个坑

曾经有个项目,我把协议检查和数据比对都塞在了monitor里。结果换了个DUT版本,接口时序微调,我不得不重写整个monitor。后来我学乖了:monitor只负责抓数据,检查逻辑放到独立的checker组件里。这样接口变了,我只需要改monitor的抓取逻辑,checker完全不用动。

4.5 小结

这一章的内容其实就一句话:各司其职,松耦合,高内聚。driver只管驱动,monitor只管监听,env只管组装,test只管配置。谁也别越界。

下一章咱们聊聊sequence和sequence_item的设计,那又是另一番天地了。

课后思考:如果你的DUT有多个相同类型的接口(比如两个SPI),你会怎么设计agent?是共用一个agent实例化两次,还是写两个不同的agent?