写过 verilog 硬件代码的同学应该都知道 DUT 会包含很多寄存器,它们是模块间交互的接口,其用途大致可以分为两类:
a. 通过读出寄存器当前的值获取 DUT 当前的状态,该类寄存器称为状态寄存器;
b. 通过对寄存器进行配置,可以使得 DUT 工作在一定模式下,该类寄存器称为配置寄存器。
在验证过程中,寄存器的验证是最新开始的,只有保证寄存器的配置正确,才能使得硬件之间的“交互”正确。在验证寄存器配置是否正确的过程中,我们需要频繁的对 DUT 内部的寄存器进行读写操作,如 reference model 需要获取指定 reg 的参数值,在验证平台中我们获取 DUT 内部寄存器的值的方式主要有两种:
前门访问(FRONTDOOR):启动 sequence 产生待操作寄存器的读写控制和地址,在 driver 中通过总线(Bus)驱动至 DUT,并在 monitor 中采集 Bus 输出数据,该方式需要消耗仿真时间 ;
后门访问(BACKDOOR):在仿真环境中通过 DUT 实例名进行点操作,直接访问 DUT 内部的寄存器,该方式的缺点是,点操作需要通过绝对路径操作,如果寄存器数量庞大,会导致验证环境臃肿繁杂,容易出错。
因为上述操作的不利因素,才导致寄存器模型 ( RAL Model ) 产生。
1. 什么是寄存器模型
RAL Model 对应于 DUT 中的寄存器,RAL Model 中有 DUT 每一个 寄存器的副本,它是真实硬件寄存器在软件环境中的行为模型;硬件寄存器的一个或多个 bit 的拼接称为一个域 ( field );多一个 field 形成一个 reg;多个 reg 构成一个块 ( block )。uvm library 已经为我们定义好了上述几个概念,我们在使用时只需继承即可。
uvm_reg_field:这是寄存器模型中的最小单位。
uvm_reg:它比 uvm_reg_field 高一个级别,但是依然是比较小的单位。一个寄存器中至少包含一个 uvm_reg_field。
uvm_reg_block:它是一个比较大的单位,在其中可以加入许多的 uvm_reg,也可以加入其他的 uvm_reg_block。一个寄存器模型中至少包含一个 uvm_reg_block。
uvm_reg_map:每个寄存器在加入寄存器模型时都有其地址,uvm_reg_map 就是存储这些地址,并将其转换成可以访问的物理地址(因为加入寄存器模型中的寄存器地址一般都是偏移地址,而不是绝对地址)。当寄存器模型使用前门访问方式来实现读或写操作时,uvm_reg_map 就会将地址转换成绝对地址,启动一个读或写的 sequence,并将读或写的结果返回。在每个 reg_block 内部,至少有一个(通常也只有一个)uvm_reg_map。
如下图所示,RAL Model 中包含 MEM 和 block,它们分别用于对 DUT 中的寄存器和 memory 进行建模,其行为和硬件行为保持一致(其实是尽可能保持一致),ADDR MAP 用于实现访问寄存器的相对地址和绝对地址的转换。
寄存器模型注意有以下优势:
a.方便对 DUT 中寄存器进行读写;
b.在软件仿真时,可以不耗时的获取寄存器的值(直接从 RAL Model 中获取);
c.可以很方便的正对寄存器的 coverage 验证点的收集。
如果有了寄存器模型,那么寄存器访问过程就可以简化为:
RAL Model
task my_model::main_phase(uvm_phase phase);
…
reg_model.version.read(status, value, UVM_FRONTDOOR);
reg_model.version.write(status, value, UVM_FRONTDOOR);
…
endtask
只要一条语句就可以实现上述复杂的过程。像启动 sequence 及将读取结果返回这些事情,都会由寄存器模型来自动完成。在没有寄存器模型之前,只能启动 sequence 通过前门(FRONTDOOR)访问的方式来读取寄存器,局限较大,在 scoreboard(或者其他 component )中难以控制。而有了寄存器模型之后,scoreboard 只与寄存器模型打交道,无论是发送读的指令还是获取读操作的返回值,都可以由寄存器模型完成。有了寄存器模型后,可以在任何耗费时间的phase中使用寄存器模型以前门访问或后门(BACKDOOR)访问的方式来读取寄存器的值,同时还能在某些不耗费时间的 phase(如 check_phase)中使用后门访问的方式来读取寄存器的值。
2. 寄存器模型实例
假设有如下的 DUT:
DUT
module dut (clk, data, addr, we_n, cs);
input clk;
inout[15:0] data;
input[15:0] addr;
input we_n;
input cs;
reg [16:0] version;
initial begin
version <= 16'h0000;
end
endmodule
这个 DUT 比较的简单,它只有一个寄存器 version
, 我们依据上述基础理论为其建造 RAL model。首先要从 uvm_reg 派生一个通用的寄存器类:
reg uvm_reg
class my_reg extends uvm_reg;
rand uvm_reg_field data;
`uvm_object_utils(my_reg)
virtual function void build();
data = uvm_reg_field::type_id::create("data");
//parameter: parent, size, lsb_pos, access, volatile, reset value, has_reset, is_rand, individually accessible
data.configure(this, 16, 0, "RW", 1, 0, 1, 1, 0);
endfunction
function new(input string name="my_reg");
//parameter: name, size, has_coverage
super.new(name, 16, UVM_NO_COVERAGE);
endfunction
endclass
data.configure 的 9 个parameter: parent, size, lsb_pos, access, volatile, reset value, has_reset, is_rand, individually accessible
参数一,是此域的父辈,也就是此域位于哪个寄存器中,即是 this;
参数二,是此域的宽度;
参数三,是此域的最低位在整个寄存器的位置,从0开始计数;
参数四,表示此字段的存取方式;
参数五,表示是否是易失的(volatile),这个参数一般不会使用;
参数六,表示此域上电复位后的默认值;
参数七,表示此域时都有复位;
参数八,表示这个域是否可以随机化;
参数九,表示这个域是否可以单独存取。
定义好了此通用寄存器后,我们需要在一个由 reg_block 派生的类中把其实例化:
reg uvm_reg_block
class my_regmodel extends uvm_reg_block;
rand my_reg version;
function void build();
default_map = create_map("default_map", 0, 2, UVM_LITTLE_ENDIAN, 0);
version = my_reg::type_id::create("version", , get_full_name());
version.configure(this, null, "version");
version.build();
default_map.add_reg(version, 16'h47, "RW");
endfunction
`uvm_object_utils(my_regmodel)
function new(input string name="my_regmodel");
super.new(name, UVM_NO_COVERAGE);
endfunction
endclass
reg block中也有 build 函数,在其中要做如下事情:
- 调用
create_map
函数完成default_map
的实例化,default_map = create_map(“default_map”, 0, 2, UVM_LITTLE_ENDIAN, 0)
;
第一个参数,表示名字;
第二个参数,表示该 reg block 的基地址;
第三个参数,表示寄存器所映射到的总线的宽度(单位是 byte,不是 bit );
第四个参数,表示大小端模式;
第五个参数,表示该寄存器能否按 byte 寻址。
- 完成每个寄存器的
build
及configure
操作,uvm_reg
的configure
函数原型:function void configure ( uvm_reg_block blk_parent, uvm_reg_file regfile_parent = null, string hdl_path = "" )
;
第一个参数,表示所在 reg block 的指针;
第二个参数,表示 reg_file 指针;
第三个参数,表示寄存器后面访问路径 - string 类型。
- 把每个寄存器加入到
default_map
中,uvm_reg_map
存有各个寄存器的地址信息,default_map.add_reg (version, 16'h47, "RW")
;
第一个参数,表示要添加的寄存器名;
第二个参数,表示地址;
第三个参数,表示寄存器的读写属性。
到此为止,一个简单的 register model 已经完成。
3. 寄存器模型应用
3.1寄存器模型读写操作
a.读操作
reg read
extern virtual task read(output uvm_status_e status,
output uvm_reg_data_t value,
input uvm_path_e path = UVM_DEFAULT_PATH,
input uvm_reg_map map = null,
input uvm_sequence_base parent = null,
input int prior = -1,
input uvm_object extension = null,
input string fname = "",
input int lineno = 0
);
b.写操作
reg read
extern virtual task write(output uvm_status_e status,
input uvm_reg_data_t value,
input uvm_path_e path = UVM_DEFAULT_PATH,
input uvm_reg_map map = null,
input uvm_sequence_base parent = null,
input int prior = -1,
input uvm_object extension = null,
input string fname = "",
input int lineno = 0
);
read/write
有多个参数,常用的是其前三个参数:
- 输出型,
uvm_status_e
型的变量,表示读/写操作是否成功; - 输出/入型, 读/写取的数值;
- 读/写取的方式,可选 UVM_FRONTDOOR 和 UVM_BACKDOOR。
register model 的 FRONTDOOR 方式工作流程如上图所示,其中左图为读操作,右图为写操作。无论是读或写,RAL model 在调用 read/write
函数时都会通过启动一个 sequence
(用户不可见) 产生一个 uvm_reg_bus_op
的变量,此变量中存储着操作类型(读还是写),操作的地址,如果是写操作,还会有要写入的数据。此变量中的信息要经过一个转换器( adapter
)转换之后,交给 bus_sequencer,之后 bus_sequencer 交给 bus_driver,bus_driver 实现最终的 FRONTDOOR 读写操作。因此,必须要定义好一个转换器。
adapter
class adapter extends uvm_reg_adapter;
string tID = get_type_name();
`uvm_object_utils(my_adapter)
function new(string name="my_adapter");
super.new(name);
endfunction : new
function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
bus_transaction tr;
tr = new("tr");
tr.addr = rw.addr;
tr.bus_op = (rw.kind == UVM_READ) BUS_RD: BUS_WR;
if (tr.bus_op == BUS_WR)
tr.wr_data = rw.data;
return tr;
endfunction : reg2bus
function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
bus_transaction tr;
if(!$cast(tr, bus_item)) begin
`uvm_fatal(tID,"Provided bus_item is not of the correct type. Expecting bus_trans action")
return;
end
rw.kind = (tr.bus_op == BUS_RD) UVM_READ : UVM_WRITE;
rw.addr = tr.addr;
rw.byte_en = 'h3;
rw.data = (tr.bus_op == BUS_RD) tr.rd_data : tr.wr_data;
rw.status = UVM_IS_OK;
endfunction : bus2reg
endclass : adapter
一个转换器要定义好两个函数,一是 reg2bus,其作用为将寄存器模型通过 sequence 发出的 uvm_reg_bus_op
型的变量转换成 bus_sequencer
能够接受的形式,二是 bus2reg,其作用为当监测到总线上有操作时,它将收集来的 transaction 转换成寄存器模型能够接受的形式,以便寄存器模型能够更新相应的寄存器的值。
说到这里,不得不考虑寄存器模型发起的读操作的数值是如何返回给寄存器模型的?由于总线的特殊性,bus_driver
在驱动总线进行读操作时,它也能顺便获取要读的数值,如果它将此值放入从 bus_sequencer
获得的 bus_transaction
中时,那么bus_transaction
中就会有读取的值,此值经过 adapter 的 bus2reg 函数的传递,最终被寄存器模型获取,这个过程如上图读操作虚线所示。
寄存器模型的读/写过程类似,现在以读操作为例,其完成流程为:
a.参考模型调用寄存器模型的读任务。
b.寄存器模型产生 sequence,并产生 uvm_reg_item:rw。
c.产生 driver 能够接受的transaction:bus_req=adapter.reg2bus(rw)。
d.把 bus_req 交给bus_sequencer。
e.driver 得到 bus_req 后驱动它,得到读取的值,并将读取值放入 bus_req 中,调用 item_done。
f.寄存器模型调用 adapter.bus2reg(bus_req,rw)将 bus_req 中的读取值传递给 rw。
g.将 rw 中的读数据返回参考模型。
3.2 寄存器模型例化
一般在 env 或者 test 中加入 RALmodel :
base_test
class base_test extends uvm_test;
my_env env;
my_vsqr v_sqr;
reg_model rm;
adapter reg_sqr_adapter;
…
endclass
function void base_test::build_phase(uvm_phase phase);
super.build_phase(phase);
env = my_env::type_id::create("env", this);
v_sqr = my_vsqr::type_id::create("v_sqr", this);
rm = reg_model::type_id::create("rm", this);
rm.configure(null, "");
rm.build();
rm.lock_model();
rm.reset();
rm.set_hdl_path_root("top_tb.my_dut");
reg_sqr_adapter = new("reg_sqr_adapter");
env.p_rm = this.rm;
endfunction
function void base_test::connect_phase(uvm_phase phase);
super.connect_phase(phase);
v_sqr.p_my_sqr = env.i_agt.sqr;
v_sqr.p_bus_sqr = env.bus_agt.sqr;
v_sqr.p_rm = this.rm;
rm.default_map.set_sequencer(env.bus_agt.sqr, reg_sqr_adapter);
rm.default_map.set_auto_predict(1);
endfunction
要将一个寄存器模型集成到 base_test 中,那么至少需要在 base_test 中定义两个成员变量,一是 reg_model,另外一个就是 adapter 。将所有用到的类在 build_phase 中实例化。在实例化后 reg_model 还要做四件事:
第一是调用 configure 函数,其第一个参数是parent block,由于是最顶层的reg_block,因此填写null,第二个参数是后门访问路径,这里传入一个空的字符串。
第二是调用 build 函数,将所有的寄存器实例化。
第三是调用 lock_model 函数,调用此函数后,reg_model 中就不能再加入新的寄存器了。
第四是调用 reset 函数,如果不调用此函数,那么reg_model中所有寄存器的值都是0,调用此函数后,所有寄存器的值都将变为设置的复位值。
寄存器模型的前门访问操作最终都将由 uvm_reg_map 完成,因此在 connect_phase 中,需要将转换器和 bus_sequencer 通过 set_sequencer 函数告知 reg_model 的 default_map,并将 default_map 设置为自动预测状态。
3.3 寄存器模型应用
寄存器模型定义好后,可以在 reference model 和 sequence,因为 uvm_component 和 uvm_object 的区别,其在 uvm_component 中的应用和在 uvm_object 类中存在些微差异。
- refencer model 场景使用
reference model 为 component 类型,为了使用在 base_test 中例化的 reg model ,需要在参考模型中定义一个寄存器模型的指针,并在env 的 connect_phase 中实现指针赋值:
base_test
class my_model extends uvm_component;
…
reg_model p_rm;
…
endclass
function void my_env::connect_phase(uvm_phase phase);
…
mdl.p_rm = this.p_rm;
endfunction
read/write实例如下:
my_model 应用
task my_model::main_phase(uvm_phase phase);
my_transaction tr;
my_transaction new_tr;
uvm_status_e status;
uvm_reg_data_t value;
super.main_phase(phase);
p_rm.version.read(status, value, UVM_FRONTDOOR);
while(1) begin
port.get(tr);
new_tr = new("new_tr");
new_tr.copy(tr);
//`uvm_info("my_model", "get one transaction, copy and print it:", UV M_LOW)
//new_tr.print();
if(value)
invert_tr(new_tr);
ap.write(new_tr);
end
endtask
- sequence 场景使用
reg model 的使用,关键在于获取 reg model 的实例,在 sequence 为了获取一个指向 reg model 的指针,我们可以借助于 sequencer。实例如下:
my_model 应用
class case0_cfg_vseq extends uvm_sequence;
…
virtual task body();
…
p_sequencer.p_rm.version.read(status, value, UVM_FRONTDOOR);
…
p_sequencer.p_rm.version.write(status, value, UVM_FRONTDOOR);
…
endtask
endclass