4. UVM核心组件(下):uvm_sequencer、uvm_scoreboard、uvm_env、uvm_test

好,咱们接着聊UVM的核心组件。上一章我们把driver、monitor、agent这几个干活的主力给捋了一遍。今天要讲的这四个——sequencer、scoreboard、env、test,说白了就是「调度中心」、「判卷老师」、「总装车间」和「总指挥」。

我个人习惯把这四个组件叫做「上层建筑」。为什么?因为它们不直接跟DUT打交道,但整个验证平台的运转逻辑全靠它们撑着。你想想看,没有sequencer,你的sequence往哪儿发?没有scoreboard,你比对谁去?没有env和test,你连个像样的测试用例都组织不起来。

核心要点:这四个组件共同构成了UVM验证平台的「骨架」和「大脑」。sequencer负责调度,scoreboard负责判决,env负责组装,test负责启动。

4.1 uvm_sequencer:序列的调度官

sequencer这个东西,说白了就是个「交通警察」。它的任务就是接收sequence发来的transaction,然后按顺序或者按优先级发给driver。

我在项目中遇到过一个问题:多个sequence同时往同一个sequencer上挂,结果driver收到的transaction乱成一锅粥。后来才发现,sequencer默认的仲裁策略是FIFO,但如果你不设置优先级,它就按注册顺序来。嗯,这里要注意——sequencer的仲裁机制是可以自定义的。

class my_sequencer extends uvm_sequencer #(my_transaction);
  `uvm_component_utils(my_sequencer)
  
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
endclass

你看,定义一个sequencer其实很简单。关键参数是那个#(my_transaction),它告诉sequencer:你要调度的transaction类型是什么。

个人经验:我建议你在定义sequencer时,尽量把transaction类型参数化。这样以后复用起来特别方便。我曾经在一个项目中,就因为transaction类型写死了,结果换个DUT就得重写一个sequencer,血泪教训啊。

4.2 uvm_scoreboard:验证的判卷老师

scoreboard,直译过来就是「计分板」。但在UVM里,它的作用远不止计分——它负责比对DUT的输出是否和预期一致。

你想想看,driver往DUT里灌数据,monitor从DUT里抓数据。谁来判断对错?就是scoreboard。它通常从reference model拿到预期值,再从monitor拿到实际值,然后一比对——对了就pass,错了就报错。

class my_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(my_scoreboard)
  
  uvm_analysis_imp #(my_transaction, my_scoreboard) mon_imp;
  uvm_analysis_imp #(my_transaction, my_scoreboard) ref_imp;
  
  function new(string name, uvm_component parent);
    super.new(name, parent);
    mon_imp = new("mon_imp", this);
    ref_imp = new("ref_imp", this);
  endfunction
  
  function void write_mon(my_transaction tr);
    // 从monitor收到的实际值
    actual_queue.push_back(tr);
  endfunction
  
  function void write_ref(my_transaction tr);
    // 从reference model收到的预期值
    expected_queue.push_back(tr);
  endfunction
endclass

避坑指南:我曾经犯过一个低级错误——scoreboard里只比对了一次就认为测试通过了。结果后来发现,DUT有流水线延迟,实际值和预期值根本不在同一个时间点到达。所以,一定要处理好时序对齐。我现在的习惯是:在scoreboard里加一个FIFO,先把数据缓存起来,等两边都到齐了再比对。

4.3 uvm_env:验证平台的组装车间

env,全称是environment。它的作用就是「搭积木」——把agent、scoreboard、reference model这些组件全部例化并连接起来。

我个人习惯把env分成两层:一层是sub_env(比如每个接口对应一个sub_env),另一层是顶层的top_env。这样做的最大好处是——复用。你想想看,如果每个接口的验证环境都独立封装,换个项目直接拿来用就行了。

class my_env extends uvm_env;
  `uvm_component_utils(my_env)
  
  my_agent    agt;
  my_scoreboard scb;
  my_reference_model ref;
  
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    agt = my_agent::type_id::create("agt", this);
    scb = my_scoreboard::type_id::create("scb", this);
    ref = my_reference_model::type_id::create("ref", this);
  endfunction
  
  function void connect_phase(uvm_phase phase);
    agt.mon.mon_port.connect(scb.mon_imp);
    ref.ref_port.connect(scb.ref_imp);
  endfunction
endclass

关键点:env的connect_phase里做的是「连线」工作。把monitor的analysis port连到scoreboard的analysis imp,把reference model的输出也连到scoreboard。这样数据流就通了。

4.4 uvm_test:验证的启动按钮

test,就是测试用例的顶层容器。它负责创建env,启动sequence,控制仿真流程。

我记得刚学UVM时,总觉得test就是个空壳子,没什么好写的。后来才发现——test才是整个验证平台的「总开关」。不同的test可以配置不同的参数、挂载不同的sequence、甚至替换不同的agent。

class my_test extends uvm_test;
  `uvm_component_utils(my_test)
  
  my_env env;
  my_sequence seq;
  
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    env = my_env::type_id::create("env", this);
  endfunction
  
  task run_phase(uvm_phase phase);
    phase.raise_objection(this);
    seq = my_sequence::type_id::create("seq");
    seq.start(env.agt.sqr);
    phase.drop_objection(this);
  endtask
endclass

实用技巧:我建议你在test里加一个config_db的设置环节。比如在build_phase之前,先通过set_config_int来配置interface的虚接口。这样不同的test可以指向不同的DUT实例,做多实例验证时特别方便。

4.5 核心逻辑关系图

说了这么多,这四个组件到底怎么配合?我画了一张图,你看一眼就明白了:

uvm_test uvm_env uvm_agent uvm_sequencer uvm_scoreboard reference model → driver ← monitor 数据流方向:sequence → sequencer → driver → DUT → monitor → scoreboard

从这张图你能看到:test创建并启动env,env内部例化agent和scoreboard。agent里包含sequencer,sequencer负责调度sequence。最终,数据从sequencer流向driver,再从monitor流回scoreboard进行比对。

4.6 实战中的一些建议

最后,我把自己这些年踩过的坑和积累的经验总结一下:

  • sequencer的仲裁策略:默认是SEQ_ARB_FIFO,但如果你有多个sequence需要抢占资源,建议改成SEQ_ARB_STRICT_FIFO或SEQ_ARB_RANDOM。我一般用SEQ_ARB_STRICT_FIFO,保证高优先级的sequence先执行。
  • scoreboard的比对时机:别在write函数里直接比对。先把数据存到队列里,等仿真结束或者某个检查点再统一比对。这样能避免时序问题导致的误报。
  • env的层次化设计:我建议每个接口对应一个sub_env,顶层env只做连接。这样换接口时只需要改sub_env,顶层基本不动。
  • test的配置管理:用config_db来传递参数,别在test里硬编码。这样不同的test可以复用同一个env,只是配置不同而已。

特别提醒:千万不要在scoreboard里做复杂的时序计算。我曾经见过有人把整个reference model的逻辑塞进scoreboard里,结果scoreboard变得又大又慢,调试起来痛苦得要命。记住——scoreboard只做比对,不做计算。计算的事情交给reference model去做。

好了,这四个组件就讲到这里。你想想看,有了sequencer、scoreboard、env和test,一个完整的UVM验证平台是不是就搭起来了?下一章我们聊聊UVM的phase机制——那个才是真正让UVM「活起来」的东西。

可可精品资料库