第 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
endclassuvm_reg_field::configure 的第四个参数(volatile)值得仔细看一下:status_reg.irq 和 count_reg.value 标成了 volatile(1),ctrl_reg.enable 和 data_reg.value 没有。这是对第 1 章 RTL 一次直接、刻意的对照:CTRL.ENABLE 和 DATA 只会因为访问它们自己的地址而变化,所以 RAL 那套"写完就信任镜像"的常规行为对它们是准确的。COUNT 和 STATUS.IRQ 则是因为写了一个完全不同的地址(DATA、CTRL)才变化的——把它们标成 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
endclasscreate_map 里的 4 是这张映射表的总线宽度,单位字节(32 位的 axi4lite_if,第 1 章);四个 add_reg 的偏移量就是第 1 章的寄存器映射,原封不动,只是换成了具名调用,不用再靠人肉去对照一张表。uvm_reg_block 继承自 uvm_object,不是 uvm_component——没有 phase 机制会自动构建它,这正是下面 regfile_env 的 build_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
endclassreg2bus 把一个抽象的寄存器操作变成一个真正的 axi_txn——正是第 2 章 driver 已经知道怎么驱动的那个类,所以 axi_driver 完全不需要改动。bus2reg 在事务完成之后运行,把成功与否报回去——rw.status 正是第 1 章 DECERR/SLVERR 的区分最终会传给 RAL 的地方,不过这一章的场景从来没有触发过这两者中的任何一个。
接起来:regfile_env 与 regfile_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
endclassset_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
endclassregmodel.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.IRQ和COUNT.value标成 volatile(而CTRL.ENABLE/DATA.value不标),是对第 1 章 RTL 的一次直接、准确的对照:哪些字段只因为自己的总线地址被访问而变化,哪些是别处写操作的副作用。 uvm_reg_adapter的reg2bus/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写操作之后,报告的还是0;COUNT.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 和实际总线之间的回路?