UVM 高阶用法

第 5 章 · 共 6 章

UVM RAL:寄存器抽象层

给 axil_regfile 的四个寄存器搭一套真正的 uvm_reg 模型,一个把 front door 访问桥接到 axi_txn 的 uvm_reg_adapter,以及具体的收获:RAL 的镜像值只会因为访问自己寄存器地址而更新,所以四次 DATA 写下来 COUNT 的镜像一直停在 0——只有一次 front door 读才能补上这个真实存在的缺口。

第 1 章的寄存器映射(CTRL/STATUS/DATA/COUNT)到目前为止每一章都是按裸地址访问的——写的是 write(8'h08, 32'hAA),从来不是"写 DATA"。UVM RAL 就是用来终结这种写法的标准手段:每个寄存器一个 uvm_reg 对象,用具名字段代替位置,外加 RAL 能自动追踪到什么程度这件事上一个真实存在的缺口——这一章的收获会把它具体演示出来,而不只是嘴上说说。

这一章刻意没做的事

一套完整的 RAL 部署通常会把 front door 访问(真正的总线事务,这一章要搭的东西)和一个**寄存器预测器(register predictor)**配对使用——一个被动监听总线流量、连 RAL 自己没发起的访问也能帮着更新镜像的组件。这是真实、有用的机制,但这里刻意不做,是为了让这一章保持容易上手——front door 的 read()/write() 加上手动的"镜像 vs 实际值"检查,就是这一章全部的收获;而且正如最后一节展示的,这个手动检查做的是真活,不是一个本该被自动化取代的占位符。

寄存器模型:映射表每一行对应一个 uvm_reg

class ctrl_reg extends uvm_reg;
  `uvm_object_utils(ctrl_reg)
 
  rand uvm_reg_field enable;
  rand uvm_reg_field irq_clr;
 
  function new(string name = "ctrl_reg");
    super.new(name, 32, UVM_NO_COVERAGE);
  endfunction
 
  virtual function void build();
    enable = uvm_reg_field::type_id::create("enable");
    enable.configure(this, 1, 0, "RW", 0, 1'b0, 1, 1, 0);
 
    irq_clr = uvm_reg_field::type_id::create("irq_clr");
    irq_clr.configure(this, 1, 1, "WO", 0, 1'b0, 1, 1, 0);
  endfunction
endclass
 
class status_reg extends uvm_reg;
  `uvm_object_utils(status_reg)
 
  rand uvm_reg_field irq;
 
  function new(string name = "status_reg");
    super.new(name, 32, UVM_NO_COVERAGE);
  endfunction
 
  virtual function void build();
    irq = uvm_reg_field::type_id::create("irq");
    irq.configure(this, 1, 0, "RO", 1, 1'b0, 1, 0, 0);
  endfunction
endclass
 
class data_reg extends uvm_reg;
  `uvm_object_utils(data_reg)
 
  rand uvm_reg_field value;
 
  function new(string name = "data_reg");
    super.new(name, 32, UVM_NO_COVERAGE);
  endfunction
 
  virtual function void build();
    value = uvm_reg_field::type_id::create("value");
    value.configure(this, 32, 0, "RW", 0, 32'h0, 1, 1, 0);
  endfunction
endclass
 
class count_reg extends uvm_reg;
  `uvm_object_utils(count_reg)
 
  rand uvm_reg_field value;
 
  function new(string name = "count_reg");
    super.new(name, 32, UVM_NO_COVERAGE);
  endfunction
 
  virtual function void build();
    value = uvm_reg_field::type_id::create("value");
    value.configure(this, 32, 0, "RO", 1, 32'h0, 1, 0, 0);
  endfunction
endclass

uvm_reg_field::configure 的第四个参数(volatile)值得仔细看一下:status_reg.irqcount_reg.value 标成了 volatile(1),ctrl_reg.enabledata_reg.value 没有。这是对第 1 章 RTL 一次直接、刻意的对照:CTRL.ENABLEDATA 只会因为访问它们自己的地址而变化,所以 RAL 那套"写完就信任镜像"的常规行为对它们是准确的。COUNTSTATUS.IRQ 则是因为写了一个完全不同的地址(DATACTRL)才变化的——把它们标成 volatile,正是寄存器模型自己在文档里写明"这个字段的缓存值不能信",而这正是这一章最后一节要具体演示的那个缺口。UVM_NO_COVERAGE 关掉了 RAL 内置的功能覆盖率建模——衡量覆盖率是 dv-methodology 的工作,不是这一章的。

regfile_reg_block:把寄存器绑定到一张映射表上

class regfile_reg_block extends uvm_reg_block;
  `uvm_object_utils(regfile_reg_block)
 
  rand ctrl_reg   CTRL;
  rand status_reg STATUS;
  rand data_reg   DATA;
  rand count_reg  COUNT;
 
  function new(string name = "regfile_reg_block");
    super.new(name, UVM_NO_COVERAGE);
  endfunction
 
  virtual function void build();
    CTRL = ctrl_reg::type_id::create("CTRL");
    CTRL.configure(this);
    CTRL.build();
 
    STATUS = status_reg::type_id::create("STATUS");
    STATUS.configure(this);
    STATUS.build();
 
    DATA = data_reg::type_id::create("DATA");
    DATA.configure(this);
    DATA.build();
 
    COUNT = count_reg::type_id::create("COUNT");
    COUNT.configure(this);
    COUNT.build();
 
    default_map = create_map("default_map", 0, 4, UVM_LITTLE_ENDIAN);
    default_map.add_reg(CTRL,   8'h00, "RW");
    default_map.add_reg(STATUS, 8'h04, "RO");
    default_map.add_reg(DATA,   8'h08, "RW");
    default_map.add_reg(COUNT,  8'h0C, "RO");
 
    lock_model();
  endfunction
endclass

create_map 里的 4 是这张映射表的总线宽度,单位字节(32 位的 axi4lite_if,第 1 章);四个 add_reg 的偏移量就是第 1 章的寄存器映射,原封不动,只是换成了具名调用,不用再靠人肉去对照一张表。uvm_reg_block 继承自 uvm_object,不是 uvm_component——没有 phase 机制会自动构建它,这正是下面 regfile_envbuild_phase 要显式调用它的 .build() 的原因。lock_model() 在所有寄存器都加完之后,最终确定这个寄存器模型——标准做法,只调一次,放在最后。

axi_reg_adapter:在 RAL 和 axi_txn 之间做翻译

RAL 的寄存器级 API 对 AXI4-Lite 一无所知——它操作的是一个抽象的 uvm_reg_bus_op(kind/addr/data/status),需要一个翻译器,把它转换成真正 driver 认识的那种 bus transaction 类型,反过来也一样。uvm_reg_adapter 就是这个翻译器:

class axi_reg_adapter extends uvm_reg_adapter;
  `uvm_object_utils(axi_reg_adapter)
 
  function new(string name = "axi_reg_adapter");
    super.new(name);
    supports_byte_enable = 0;
    provides_responses    = 0;
  endfunction
 
  virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
    axi_txn txn = axi_txn::type_id::create("txn");
    txn.is_write = (rw.kind == UVM_WRITE);
    txn.addr     = rw.addr[7:0];
    if (rw.kind == UVM_WRITE) txn.wdata = rw.data;
    return txn;
  endfunction
 
  virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
    axi_txn txn;
    if (!$cast(txn, bus_item)) `uvm_fatal("REG_ADAPTER", "bus2reg: cast failed")
    rw.kind   = txn.is_write ? UVM_WRITE : UVM_READ;
    rw.addr   = txn.addr;
    rw.data   = txn.is_write ? txn.wdata : txn.rdata;
    rw.status = (txn.resp == 2'b00) ? UVM_IS_OK : UVM_NOT_OK;
  endfunction
endclass

reg2bus 把一个抽象的寄存器操作变成一个真正的 axi_txn——正是第 2 章 driver 已经知道怎么驱动的那个类,所以 axi_driver 完全不需要改动。bus2reg 在事务完成之后运行,把成功与否报回去——rw.status 正是第 1 章 DECERR/SLVERR 的区分最终会传给 RAL 的地方,不过这一章的场景从来没有触发过这两者中的任何一个。

接起来:regfile_envregfile_vsqr

regfile_vsqr(第 4 章)在原有的 axi_sqr 之外多了一个字段,regfile_env 负责构建寄存器模型,并通过 adapter 把它绑定到真正的 sequencer 上:

class regfile_vsqr extends uvm_sequencer;
  `uvm_component_utils(regfile_vsqr)
 
  axi_sequencer     axi_sqr;
  regfile_reg_block regmodel;
 
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
endclass
 
class regfile_env extends uvm_env;
  `uvm_component_utils(regfile_env)
 
  axi_agent         axi_agt;
  irq_agent         irq_agt;
  irq_watcher       irqw;
  regfile_vsqr      vsqr;
  regfile_reg_block regmodel;
  axi_reg_adapter   adapter;
 
  function new(string name, uvm_component parent);
    super.new(name, parent);
  endfunction
 
  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    axi_agt  = axi_agent::type_id::create("axi_agt", this);
    irq_agt  = irq_agent::type_id::create("irq_agt", this);
    irqw     = irq_watcher::type_id::create("irqw", this);
    vsqr     = regfile_vsqr::type_id::create("vsqr", this);
    regmodel = regfile_reg_block::type_id::create("regmodel");
    regmodel.build();
    adapter  = axi_reg_adapter::type_id::create("adapter");
  endfunction
 
  function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    irq_agt.mon.ap.connect(irqw.imp);
    vsqr.axi_sqr  = axi_agt.sqr;
    vsqr.regmodel = regmodel;
    regmodel.default_map.set_sequencer(axi_agt.sqr, adapter);
  endfunction
endclass

set_sequencer 就是最后闭环的那一步:调用之后,任何通过 regmodel.default_map 发起 front door 寄存器访问的 sequence,都会自动经由 axi_reg_adapter 路由到 axi_agt.sqr。相比第 4 章的做法,这是一个真正的简化——用 RAL 的 sequence 再也不用像 start_item(req, -1, p_sequencer.axi_sqr) 那样明确点名一个 sequencer 了。

这一章的收获:镜像 vs. 实际值

class regfile_ral_seq extends uvm_sequence;
  `uvm_object_utils(regfile_ral_seq)
  `uvm_declare_p_sequencer(regfile_vsqr)
 
  function new(string name = "regfile_ral_seq");
    super.new(name);
  endfunction
 
  task body();
    uvm_status_e      status;
    uvm_reg_data_t    rdata;
    regfile_reg_block regmodel = p_sequencer.regmodel;
 
    regmodel.CTRL.write(status, 32'h1, UVM_FRONTDOOR, regmodel.default_map, this);
 
    regmodel.DATA.write(status, 32'hAA, UVM_FRONTDOOR, regmodel.default_map, this);
    regmodel.DATA.write(status, 32'hBB, UVM_FRONTDOOR, regmodel.default_map, this);
    regmodel.DATA.write(status, 32'hCC, UVM_FRONTDOOR, regmodel.default_map, this);
    regmodel.DATA.write(status, 32'hDD, UVM_FRONTDOOR, regmodel.default_map, this);
 
    if (regmodel.COUNT.get() == 0)
      `uvm_info("RAL", "as expected: COUNT's mirror is still 0 -- nothing ever accessed COUNT's own address, so RAL never predicted the increments that happened as a side effect of the DATA writes", UVM_LOW)
    else
      `uvm_error("RAL", "unexpected: COUNT's mirror changed without a bus access to COUNT's own address")
 
    regmodel.COUNT.read(status, rdata, UVM_FRONTDOOR, regmodel.default_map, this);
    if (rdata == 32'd4)
      `uvm_info("RAL", $sformatf("confirmed: a real front-door read reports COUNT=%0d, and the mirror is resynced to match", rdata), UVM_LOW)
    else
      `uvm_error("RAL", $sformatf("expected COUNT=4 after 4 enabled DATA writes, got %0d", rdata))
 
    regmodel.CTRL.write(status, 32'h3, UVM_FRONTDOOR, regmodel.default_map, this);
 
    regmodel.STATUS.read(status, rdata, UVM_FRONTDOOR, regmodel.default_map, this);
    if (rdata[0] == 1'b0)
      `uvm_info("RAL", "confirmed: STATUS.IRQ reads back 0 after IRQ_CLR", UVM_LOW)
    else
      `uvm_error("RAL", "expected STATUS.IRQ to read 0 after IRQ_CLR")
  endtask
endclass

regmodel.COUNT.get() 从来不碰总线——它只是返回模型此刻相信的值,跟问一个普通变量的值没什么区别。四次 DATA 写之后,它相信的值仍然是 0,因为 RAL 只会因为访问了寄存器自己的地址才更新它的镜像,而这里从来没有谁直接访问过 COUNT 的地址。这不是寄存器模型的 bug,也不是缺了什么功能——这正是把 COUNT.value 标成 volatile、又刻意没接上这一章跳过的那个预测器之后,直接、正确的结果。regmodel.COUNT.read(...) 才是修正:一次真正针对 COUNT 自己地址的 front door 访问,既能报告 DUT 的实际值(4),也会让镜像跟着重新同步。get()read() 之间的这道缝隙,正是"镜像 vs. 实际值"是一个真正的验证问题、而不只是记账的全部理由——一个只信任 get() 来读一个 volatile 字段的测试,会在整整四次写操作期间,一直悄悄相信一个错误的值。

小结

  • uvm_reg/uvm_reg_field/uvm_reg_block/uvm_reg_map 是真正的 RAL 机制——具名的寄存器和字段,对应 axil_regfile 的地址映射表,不是一个简化的替代品。
  • STATUS.IRQCOUNT.value 标成 volatile(而 CTRL.ENABLE/DATA.value 不标),是对第 1 章 RTL 的一次直接、准确的对照:哪些字段只因为自己的总线地址被访问而变化,哪些是别处写操作的副作用。
  • uvm_reg_adapterreg2bus/bus2reg 把 RAL 抽象的 uvm_reg_bus_op 桥接到第 2 章 driver 早就会驱动的具体 axi_txn——axi_driver 完全不用改。
  • 寄存器映射表上的 set_sequencer,意味着用 RAL 的 sequence 再也不用像第 4 章的 start_item(..., p_sequencer.axi_sqr) 那样明确点名目标 sequencer。
  • 具体的收获:COUNT.get()(信任镜像)在四次实际上已经把 COUNT 推到 4 的 DATA 写操作之后,报告的还是 0COUNT.read()(去问 DUT)报告的是真相,并让镜像重新同步。这是 RAL 的模型通过 volatile 标志诚实记录下来的一个真实、可测量的缺口,不是手动检查随口重复一遍而已。

四次 DATA 写把 DUT 真实的 COUNT 推到 4 之后,regmodel.COUNT.get() 会返回什么,为什么?

为什么 count_reg.value 的 uvm_reg_field 配置成了 volatile=1,而 data_reg.value 配置的是 volatile=0?

uvm_reg_map 的哪个方法把寄存器模型的 front door 访问绑定到一个真正的 sequencer 和 adapter 上,闭合了 RAL 和实际总线之间的回路?