4. UVM组件家族(下):深入理解uvm_sequencer、uvm_sequence、uvm_scoreboard、uvm_env、uvm_test

好,咱们接着聊UVM组件家族的下半部分。上一章我们把driver、monitor、agent这些跑腿的伙计们捋清楚了。这一章要聊的这几个,才是真正的大脑和心脏——sequencer、sequence、scoreboard、env、test。说白了,前面那些是手和脚,后面这些才是做决策、做检查、搭框架的。

4.1 uvm_sequencer:指挥交通的调度员

Sequencer这玩意儿,我刚开始学的时候总觉得它可有可无。不就是把sequence产生的transaction发给driver吗?我自己写个fifo不就行了?后来被现实狠狠教育了一回——当你同时跑十几个sequence的时候,没有sequencer帮你仲裁,代码会乱成一锅粥。

Sequencer的核心职责就两个:

  • 接收来自sequence的transaction请求
  • 按照优先级和仲裁策略,把transaction发给driver

你想想看,如果driver正在处理一个读操作,突然来了个写操作,谁先谁后?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

注意那个参数化写法 #(my_transaction)。这个模板参数告诉sequencer:我管的是哪种类型的transaction。我曾经见过有人忘了写这个参数,结果sequence和sequencer类型对不上,编译报错找了半天。嗯,这种坑踩过一次就记住了。

4.2 uvm_sequence:造数据的工厂

Sequence是干什么的?说白了就是造transaction的。它不负责发送,只负责生成。生成完了交给sequencer,sequencer再转发给driver。

我个人的习惯是把sequence分成两类:

  • 基础sequence:只产生一种类型的transaction,比如只产生读操作
  • 组合sequence:把多个基础sequence串起来,模拟复杂的业务场景

一个小技巧:写sequence的时候,记得在body任务里加个超时保护。我曾经有个sequence死循环了,仿真跑了三天三夜没停,最后发现是body里忘了加`uvm_do宏的结束条件。

class my_sequence extends uvm_sequence #(my_transaction);
  `uvm_object_utils(my_sequence)
  
  task body();
    my_transaction tr;
    repeat(10) begin
      `uvm_do(tr)
      // 这里可以加一些随机约束
      tr.addr = $urandom_range(0, 255);
    end
  endtask
endclass

看到那个 repeat(10) 了吗?这就是控制sequence产生多少笔transaction的地方。实际项目中,这个数字通常是从配置里读的,而不是硬编码。

4.3 uvm_scoreboard:火眼金睛的裁判

Scoreboard,名字起得挺唬人,其实就是个比较器。它从monitor那里拿到DUT的实际输出,再跟参考模型(reference model)的预期输出做对比。对上了就pass,对不上就报错。

我在项目中遇到过最头疼的问题,不是scoreboard不会写,而是参考模型本身就有bug。你想想看,参考模型错了,scoreboard拿错误的结果去比对,那能对吗?所以我的建议是:先单独验证参考模型,再把它集成到scoreboard里。

避坑指南:我曾经犯过一个低级错误——scoreboard里用了全局变量来存预期结果。结果多个sequence同时跑的时候,全局变量被覆盖了,比对结果全乱套。后来老老实实改成用关联数组(associative array),以transaction的ID作为key来存储。

class my_scoreboard extends uvm_scoreboard;
  `uvm_component_utils(my_scoreboard)
  
  uvm_analysis_imp #(my_transaction, my_scoreboard) actual_imp;
  uvm_analysis_imp #(my_transaction, my_scoreboard) expect_imp;
  
  my_transaction expect_queue[$];
  
  function void write_actual(my_transaction tr);
    my_transaction expect_tr;
    if(expect_queue.size() == 0) begin
      `uvm_error("SCOREBOARD", "收到实际结果,但预期队列为空")
      return;
    end
    expect_tr = expect_queue.pop_front();
    if(tr.data !== expect_tr.data)
      `uvm_error("SCOREBOARD", $sformatf("数据不匹配: actual=%0d, expect=%0d", tr.data, expect_tr.data))
  endfunction
endclass

这段代码里,expect_queue是个队列,先进先出。参考模型产生的预期结果先入队,monitor抓到的实际结果来了就出队比对。这个模式叫"顺序比对",是最常用的一种。

4.4 uvm_env:搭积木的底盘

Env,全称environment,就是验证环境的顶层容器。它负责把agent、scoreboard、reference model这些组件实例化,然后连起来。

我习惯把env分成两层:

  • sub_env:每个协议或每个接口对应一个子环境
  • top_env:把所有的sub_env拼在一起

这样做的好处是复用。比如你有一个AXI接口和一个APB接口,各写一个sub_env。下次换个项目,接口没变,直接把sub_env拿过来用就行。

class my_env extends uvm_env;
  `uvm_component_utils(my_env)
  
  my_agent    agent;
  my_scoreboard sb;
  my_ref_model ref;
  
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    agent = my_agent::type_id::create("agent", this);
    sb    = my_scoreboard::type_id::create("sb", this);
    ref   = my_ref_model::type_id::create("ref", this);
  endfunction
  
  function void connect_phase(uvm_phase phase);
    agent.monitor.item_collected_port.connect(sb.actual_imp);
    ref.expect_port.connect(sb.expect_imp);
  endfunction
endclass

看到 connect_phase 了吗?这里就是把monitor和scoreboard、参考模型和scoreboard连起来的地方。端口连接是UVM里最容易出错的地方,我建议你每次写完env,先检查一遍port的宽度和方向对不对。

4.5 uvm_test:总指挥

Test是验证环境的最高层。它决定跑哪个sequence,用哪个env,配置参数是什么。一个项目里通常有几十个test,每个test对应一个测试用例。

我个人的习惯是:

  • 写一个base_test,把公共的东西都放进去(比如env的创建、公共配置)
  • 每个具体的test继承base_test,只写自己特有的逻辑

一个典型的base_test长这样:

class base_test extends uvm_test;
  `uvm_component_utils(base_test)
  
  my_env env;
  
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    env = my_env::type_id::create("env", this);
  endfunction
  
  function void end_of_elaboration_phase(uvm_phase phase);
    uvm_top.print_topology();  // 打印环境结构,方便调试
  endfunction
endclass

class test_simple_rw extends base_test;
  `uvm_component_utils(test_simple_rw)
  
  task run_phase(uvm_phase phase);
    my_sequence seq;
    phase.raise_objection(this);
    seq = my_sequence::type_id::create("seq");
    seq.start(env.agent.sequencer);
    phase.drop_objection(this);
  endtask
endclass

注意那个 raise_objection 和 drop_objection。这是UVM里控制仿真结束的机制。如果你忘了raise,仿真可能一启动就结束了。我刚开始学的时候经常犯这个错,仿真跑了个寂寞。

4.6 组件之间的协作关系

说了这么多,咱们用一张图来总结一下这些组件是怎么配合的:

uvm_test uvm_env uvm_agent sequencer driver monitor uvm_scoreboard reference model uvm_sequence 顶层测试 环境容器 代理 比对/参考模型 激励序列

这张图里,test创建env,env创建agent和scoreboard。Sequence产生transaction,通过sequencer发给driver。Monitor抓到DUT的输出,送给scoreboard。参考模型算出预期结果,也送给scoreboard。Scoreboard一比对,结果就出来了。

说白了,整个验证环境就是一条流水线:造数据 → 发数据 → 收数据 → 比数据。每个组件各司其职,谁也别越界。我在项目中见过有人把比对逻辑写在monitor里,结果monitor变得又臭又长,维护起来想死的心都有。记住:单一职责原则,每个组件只干自己那一亩三分地的事。

最后分享一个经验:写test的时候,别忘了在run_phase里加个超时。我曾经有个test因为sequence里的循环条件写错了,仿真跑了整整一个周末。从那以后,我每个test都会加一句 phase.phase_done.set_drain_time(this, 100ns),确保仿真不会无限跑下去。

好了,UVM组件家族到这里就聊完了。从driver到test,每个组件都有自己的定位和职责。你写代码的时候,多想想这个组件该不该干这件事,不该干的千万别往里塞。保持组件的纯净,你的验证环境才能经得起项目的考验。

可可精品资料库