寄存器模型 Register Model
一、RAL : Register Abstraction Layer
对于硬件有了解的读者,都知道寄存器是模块之间互相交谈的窗口。一方面可以通过读出寄存器的状态,获取硬件当前的状况,另外一方面也可以通过配置
寄存器,使得寄存器工作在一定的模式下。而在验证的过程中,寄存器的验证也排在了验证清单的前列,因为只有首先保证寄存器的功能正确,才会使得硬件与硬件之间的交谈是“语义一致”,否则如果寄存器配置的结果与寄存器配置内容不同,那么硬件无法工作在想要的模式下,同时寄存器也可能无法正确反映硬件的状态。
- 寄存器的硬件实现是通过
触发器
,每一个bit位的触发器都对应着寄存器的功能描述。寄存器一般由32个bit位构成,单个寄存器可以分为多个域(field)
,不同域具有不同的属性,大致可以分为WO(write-only,只写),RO(read-only,只读)和RW(read and write,读写),除过这些常见的操作属性以外,还有一些特殊行为(quirky)的寄存器,例如写后擦除模式(clean-on-read,RC),只写一次模式(write-one-to-set,W1S)。
寄存器与存储器的属性都近乎于读写功能。一个功能模块中的多个寄存器和存储器可以组团构成一个寄存器模型(register model)。
DUT中的寄存器模块(由硬件实现),还有属于验证环境的寄存器模型。从这两个模块包含的寄存器信息而言,是高度一致的(如上图),属于验证环境的寄存器模型也可以抽象出一个层次化的寄存器列表,该列表所包含的地址、域、属性等信息都与硬件一侧的寄存器内容一致。
二.UVM 寄存器模型的层次
- uvm_reg_field:是寄存器模型里面最小的单位,用来针对(DUT)寄存器功能域来构建对应的bit位;
- uvm_reg :和DUT中每个register对应,里面可以例化和配置多个uvm_reg_field对象;
- uvm_mem :匹配硬件(DUT)存储模型;
- uvm_reg_map :用来指定寄存器列表中各个寄存器的偏移地址、访问属性以及对应的总线;
- uvm_reg_block :可以容纳多个寄存器(uvm_reg)、存储器(uvm_mem)和寄存器列表(uvm_reg_map)。而且还能够嵌套其他的uvm_reg_block。寄存器模型中必须要包含一个或者多个uvm_reg_block。uvm_reg_block中的
uvm_reg_map类的对象default_map
,用来进行地址映射,以及用来完成寄存器前后门访问操作。一个uvm_reg_block可以包含多个uvm_reg_map, 从而实现一个reg_block应用到不同总线,或不同地址段上。
这些类均是继承自uvm_object类.将这些层次化的register元素放到顶层的uvm_reg_block中,就组成了一个完整的register model。一个uvm_reg_block可以包含子block,register,register file和memories,如下图。
三、RAL模型创建并使用
RAL模型的创建就是为了 :使UVM中的sequence能够集成RAL模型,进而通过一系列的write、read、mirror等方法来操作RAL寄存器模型,模拟DUT行为。
3.1.对每个寄存器进行定义并创建
class ctrl_reg extends uvm_reg; //控制寄存器
`uvm_oject_utils(ctrl_reg)
rand uvm_reg_field ie; //中断使能信号
rand uvm_reg_field char_len; //传输数据长度
rand uvm_reg_field rx_neg; //数据的时钟沿设置,rx_neg控制MISO
rand uvm_reg_field tx_neg; //数据的时钟沿设置,tx_neg控制MOSI
rand uvm_reg_field lsb; //收发bit序设置
rand uvm_reg_field ass; //自动控制从设备选择信号
rand uvm_reg_field go_bsy; //启动传输使能, 所有的域段默认值都为0, Reserved保留域段可以不设置
function new(input string name="ctrl");
super.new(name, 32, UVM_NO_COVERAGE);//寄存器的宽度32
endfunction
virtual function void build();
ie = uvm_reg_field::type_id::create("ie");
ie.configure(this,1,12,"RW",0,'b0,1,1,0);
char_len = uvm_reg_field::type_id::create("char_len");
char_len.configure(this,7,0,"RW",0,'b0,1,1,0);
rx_neg = uvm_reg_field::type_id::create("rx_neg");
rx_neg.configure(this,1,9,"RW",0,'b0,1,1,0);
tx_neg = uvm_reg_field::type_id::create("tx_neg");
tx_neg.configure(this,1,10,"RW",0,'b0,1,1,0);
lsb = uvm_reg_field::type_id::create("lsb");
lsb.configure(this,1,11,"RW",0,'b0,1,1,0);
ass = uvm_reg_field::type_id::create("ass");
ass.configure(this,1,13,"RW",0,'b0,1,1,0);
go_bsy = uvm_reg_field::type_id::create("go_bsy");
go_bsy.configure(this,1,8,"RW",0,'b0,1,1,0);
endfunction
endclass
class ss_reg extends uvm_reg; //从设备选择寄存器
rand uvm_reg_field ss;
`uvm_object_utils(ss_reg)
function new(string name = "ss");
super.new(name, 32, UVM_NO_COVERAGE);
endfunction
virtual function void build();
ss = uvm_reg_field::type_id::create("ss");
ss.configure(this, 8, 0, "RW", 0, 0, 1, 1, 0);
endfunction
endclass
- 定义单个寄存器时, 将寄存器的各个域field整理出来,声明为随机变量并利用create()函数创建实例,Reserved保留域可不声明;
- 在uvm_reg的
new()函数
,要将寄存器的宽度传入super.new()的第二个参数,第三个参数是uvm_coverage_model_e类型,用以设置寄存器是否参与加入覆盖率; - uvm_reg类中的
build()函数
,不同于UVM_component的bulid_phase,不会自动执行,需要手动调用。 - 创建各个域的实例后,通过uvm_reg_field中的configure()函数进一步配置各个属性,参数较多,需保证一一对应,如下:
field_name.configure( .parent ( this ), // 参数一是此域的父辈,也就是此域位于哪个寄存器中,即是this;
.size ( 1 ), // 参数二是此域的宽度;
.lsb_position ( 0 ), // 参数三是此域的最低位在整个寄存器的位置,从0开始计数;
.access ( "RW" ), // 参数四表示此域段的存取方式(属性);
.volatile ( 0 ), // 参数五表示是否是易失的(volatile),这个参数一般不会使用;
.reset ( 0 ), // 参数六表示此域上电复位后的默认值;
.has_reset ( 1 ), // 参数七表示此域可复位;
.is_randomize ( 1 ), // 参数八表示这个域是否可以随机化;
.individually_accessible( 0 ) // 参数九表示这个域是否可以单独存取。
);
3.2.将寄存器放入register block容器中,并加入到对应的Address Map
map的作用一方面用来表示寄存器和存储对应的偏移地址,同时由于一个reg_block可以包含多个map,各个map可以分别对应不同总线或者master访问uvm_reg_block时的地址段。在reg_block中创建了各个uvm_reg之后,需要调用uvm_reg::configure()去配置各个uvm_reg实例的属性。
class reg_block_rgm extends uvm_reg_block;
`uvm_object_utils(reg_block)
rand ctrl_reg ctrl; //控制寄存器
rand ss_reg ss; //从设备选择寄存器
function new(string name = "reg_block");
super.new(name, UVM_NO_COVERAGE);
endfunction
virtual function void build();
default_map = create_map(“default_map”,0,4,UVM_BIG_ENDINA);
//create_map第一个参数是名字,第二个参数是该reg block的基地址,第三个参数是寄存器所映射到的总线的宽度(单位是byte,不是bit),第四个参数是大小端,第五个参数表示该寄存器能否按byte寻址。
ctrl = ctrl_reg::type_id::create("ctrl");
ctrl.configure(this, null, "ctrl");
//configure第一个参数是所在reg block的指针,第二个参数是reg_file指针,第三个是寄存器后面访问路径—string类型。
ctrl.build();
default_map.add_reg(ctrl,`h10,"RW")
//第一个参数是要添加的寄存器名,第二个是地址,第三个是寄存器的读写属性。
ss = ss_reg::type_id::create("ss");
status.configure(this, null, "ss");
status.build();
default_map.add_reg(ss,`h18,"RO")
endfunction
endclass
reg_block中的build()函数,主要作用如下:
1. 实例化各寄存器,通过configure()操作,配置寄存器实例的属性;调用寄存器的build()函数,实现对各个寄存器域配置;
2. 调用create_map()函数完成default_map的实例化,其为寄存器列表uvm_reg_map的默认对象;
3. 通过uvm_reg_map::add_map()函数来添加uvm_reg的偏移地址和访问属性等,并把各个寄存器加入到default_map中;uvm_reg_map存有各个寄存器的地址信息。
3.3.创建RAL适配器(adapter)
寄存器模型的前门操作都会通过sequence产生一个uvm_reg_bus_op
类型的变量,他不能直接被sequencer和driver接受。需要定义一个继承自uvm_reg_adpater的adapter适配器,将RAL中的事物数据转换成agent/sequencer类型的总线事物,实现前门访问。 而从driver返回的响应rsp,也需要由adapter转换成uvm_reg_bus_op类型变量,返回给寄存器模型,用来更新内部值。
- reg2bus()函数: 其作用是将uvm_reg_bus_op类型变量转换成sequencer能够接受的transaction;
- bus2reg()函数: 将收集到的transaction转换成寄存器模型使用的uvm_reg_bus_op类型变量,用以更新寄存器模型中相应寄存器的值;
irtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
uvm_reg_bus_op属于类(或结构体),其成员如下,:
typedef struct{
uvm_access_e kind; //1. kind属于枚举变量,值可以为UVM_READ或者UVM_WRITE
uvm_reg_data_t data; //2. 读取或者写入的数据,默认64位
uvm_reg_addr_t addr; //3. 地址,默认64位 —— 见uvm_reg_field的源码宏定义
int n_bits; //4. 传输的bit位
uvm_reg_byte_en_t byte_en; //5. byte操作使能
uvm_status_t status; //6. 枚举变量,status可以取UVM_IS_OK,UVM_IS_X,UVM_NOT_OK
}uvm_reg_bus_op
class reg_adapter extends uvm_reg_adapter;
`uvm_object_utils(reg_adapter)
function new(string name = "reg_adapter");
super.new(name);
endfunction
virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
master_data tr = master_data::type_id::create("tr");
tr.kind = (rw.kind == UVM_READ) ? `WRITE : `READ; //带 `的为define宏定义
tr.data = rw.data;
tr.addr = rw.addr;
return tr;
endfunction
virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
master_data tr;
if(!$cast(tr, bus_item)) begin
`uvm_fatal("NOT_APB_TYPES","Provided bus_item is not correct type");
retuen;
end
rw.kind = (tr.kind == `WRITE) ? UVM_READ : UVM_WRITE;
rw.data = tr.data;
rw.addr = tr.addr;
case(tr.status)
master_data::IS_OK: rw.status=UVM_IS_OK;
master_data::NOT_OK: rw.status=UVM_NOT_OK;
master_data::HAS_X: rw.status=UVM_HAS_X;
default: `uvm_fatal(get_name(), $sformatf("Could not cast to acme_apb_mon_transfer: %s", bus_item.get_type_name()))
end
endfunction
endclass
UVM内建了一种transaction:uvm_reg_item。通过adapter的bus2reg及reg2bus,可以实现uvm_reg_item与目标transaction的转换。以读操作为例,其完整的流程为:
- 参考模型调用寄存器模型的读任务。
- 寄存器模型产生sequence,并产生uvm_reg_item:。
- 产生driver能够接受的transaction:bus_req=adapter.reg2bus(rw)。
- 把bus_req交给bus_sequencer。
- driver得到bus_req后驱动它,得到读取的值,并将读取值放入bus_req中,调用item_done。
- 寄存器模型调用adapter.bus2reg(bus_req,rw)将bus_req中的读取值传递给rw。
- 将rw中的读数据返回参考模型。
3.4.验证环境中实例化RAL模型并建立连接
整个仿真平台只创建一个reg model对象,在其他地方使用指针调用。一般在test中创建顶层reg_block,及adapter和predictor。在reg_block传完要调用其configure()函数,配置后面访问路径。
class spi_env extends uvm_env; //顶层环境
master_agent agent;
reg_block_rgm rgm;
reg_adapter adapter;
...
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
agent = master_agent::type_id::create("agent", this);
adapter = reg_adapter::type_id::create("adapter");
if(!uvm_config_db#(reg_block_rgm)::get(this,"","rgm",rgm))begin
`uvm_info("GETRGM","no top-down RGM handle is assigned",UVM_LOW)
rgm = reg_block_rgm::type_id::create("rgm",this); //1. 创建regmodel实体
`uvm_info("NEWRGM","created rgm instance locally",UVM_LOW)
rgm.build(); //2.显示调用build()函数,创建UVM寄存器层次
rgm.lock_model(); //3.锁定寄存器层次结构并创建地址映射
rgm.set_hdl_path_root("top_tb.my_dut"); //4.设制后门访问路径(张强,p235-236)
uvm_config_db#(reg_block_rgm)::set(this,"","rgm",rgm); //5.为sequence设置寄存器模型(build_phase自顶向下执行)
endfunction
function void connect_phase(uvm_phase phase);
rgm.default_map.set_sequencer(agent.sequencer,adapter); //4.建立连接
rgm.default_map.set_auto_predict(1); //4. 隐式自动预测
endfunction
endclass
- 对于reg_block_rgm的集成,倾向于顶层传递的方式,即最终从test层传入寄存器模型句柄。在后期不同的test对rgm做不同的配置时可以在顶层例化,而后通过uvm_config_db来传递;
- 寄存器模型创建之后要显示调用build()函数。注意uvm_reg_block是uvm_oject类型,因此build()函数并不自动执行,还需
单独调用
。 - 在还未集成predictor之前,采用auto prediction方式,因此调用了函数set_auto_predict(1)。具体用法见后。
- 在顶层环境connect()阶段,将寄存器模型的map组件与sequencer和adapter连接。以便将map(寄存器信息),sequencer(总线一侧激励驱动)和adapter(寄存器级别与硬件总线级别的桥接)关联在一起。
3.5.编写RAL的测试序列
- 在验证环境中实例化RAL模型并建立adapter与sequencer之间的连接,之后需要编写测试序列。首先需编写不含RAL的测试序列以验证前门访问通路OK。然后再编写含有RAL模型的sequence序列,对RAL模型进行read()或者write( )等操作。
- 在ral_sequence序列中,例化本地regmodel,通过uvm_config_db配置本地regmodel,实现连接,将外部的RAL寄存器模型集成到ral_sequence中,从而实现对寄存器的操作。
class sequence_base extends uvm_sequence#(transaction); //定义回调函数
virtual task pre_start(); //objection raised here
virtual task post_start(); //objection dropped here
endclass
class bfm_sequence extends sequence_base; //1. 定义了没有RAL的序列访问寄存器, 验证前门访问数据通路OK
...
virtual task body();
`uvm_do_with{req, {addr=='h0; kind==host_data::READ}};
`uvm_do_with{req, {addr=='h800; data=='1; kind==master_data::WRITE}};
endtask
endclass
class ral_sequence extends uvm_reg_sequence#(sequence_base); //2. 定义含有RAL模型的sequence序列
uvm_block_rgm rgm; //声明regmodel句柄
virtual task pre_start();
super.pre_start(); //调用回调函数,即raise_objection
uvm_config_db#(reg_block_rgm)::get(this,"","rgm",rgm); //将regmodel集成到sequence序列中
endtask
virtual task body();
rgm.ctrl.read(status, data, .parent(this)); //对寄存器进行read或者write操作
rgm.ss.write(status, 1, .parent(this));
endtask
endclass
3.6.显式或隐式的运行RAL序列
- ral_sequence序列的执行可以采用显式执行,也可采用隐式执行。显式执行
- 显式执行:需要在测试用例中先实例化序列,然后在启动仿真,通过start( )方法执行RAL序列,start任务的参数是一个sequencer指针,用于指定sequence向哪一个sequencer传送sequence_item数据包。
- 隐式执行:通过config_db机制,在build_phase阶段设置default_sequence即可,配置类型为uvm_object_wrapper,这是uvm的规定;同时指定在seqr的具体哪一个phase阶段,如下
第二个参数
中的main_phase阶段,具体操作如下所示(常用)。
区别两种执行seq序列的objection使用方法机制。
class test1 extends uvm_test; //1. 显式执行RAL序列,使用start()方法
...
virtual function void configure_phase(uvm_phase phase);
ral_sequence seq;
phase.raise_objection(this); //启动仿真运行,挂起objection
seq = ral_sequence::type_id::create("seq",this);
seq.start(env.agt.seqr); //start任务的参数是一个sequencer指针
phase.drop_objection; //结束仿真,落下objection
endfunction
endclass
class test1 extends uvm_test; //2.通过default_sequence来隐式执行RAL序列
virtual function void configure_phase(uvm_phase phase); //挂起和落下objection
super.start_of_simulation_phase(phase); //通过在sequence的body函数中使用starting_phase.raise/drop_objection(this);
uvm_config_db#(uvm_object_wrapper)::set(this,"*.seqr.main_phase","default_phase",ral_sequence::get_type());
endfunction
endclass
3.7.显式或隐式执行镜像预测
为什么设置镜像值
:由于DUT中寄存器的值可能是实时变更的,寄存器模型并不能实时的知道这种变更,因此寄存器模型的寄存器的值有时与DUT中相关寄存器的值并不一致。对于任意一个寄存器,寄存器模型都会有一个专门的变量用于最大可能与DUT保持同步,这个变量在寄存器模型中称为DUT的镜像值。
寄存器模型中的每一个寄存器成员都有两个值:一个是镜像值(mirrored value),一个是期望值(desireed value)。
- 镜像值(mirrored value):
表示当前硬件的已知状态。镜像值往往有模型预测给出
。 - 期望值(desireed value):先利用寄存器模型修改软件对象值,而后利用该值更新硬件值。
在应用寄存器模型时,除了利用它的寄存器信息外,还可以利用它来跟踪寄存器的值,这么做的目的有二:一是建立镜像值(mirrored value),二是建立期望值(desireed value)。然而镜像值有可能与硬件实际值(hardware actual value)不一致,例如状态寄存器的镜像值则无法保持与硬件实际值保持同步更新。
需要说明的是,mirrored value与desireed value是寄存器模型的属性,而actual value是硬件的真实数值
。
UVM提供了两种跟踪寄存器值得方法,分别是自动预测(auto prediction)和显式预测(explicit)。两种预测方式的显著差别在于:显式预测对寄存器数值预测更准确。
自动(隐式)预测(auto prediction)
如果用户并没有集成独立的predictor在环境中,而是利用寄存器的操作来自动记录每一次寄存器的读写数值,并在后台自动调用predict()方法的话,这种方式被称之为自动预测。通过自动预测的方式获取寄存器的值需要调用函数uvm_reg_map::set_auto_predict(1)。详情参考验证环境中实例化RAL模型并建立连接3.4.部分,如下。
rgm.build(); //1.显示调用build()函数,创建UVM寄存器层次
rgm.default_map.set_auto_predict(1); //2. 自动隐式预测寄存器的属性值
- auto prediction 的不足之处:对于sequence 直接在总线层面上对寄存器操作(不用ral model 的write/read task)无法自动得到register的的mirror value 和期望值。
显式预测
显式预测可以弥补上述缺陷,默认情况下,系统将采用显式预测的方式。显式预测在物理总线上通过监视器
来捕捉总线事务,并将捕捉到的事务传递给外部例化的predictor(预测器)
。再利用adapter的桥接
方法,实现事务信息转换,然后手动调用该寄存器的predict()方法直接更新镜像值,将转化后的寄存器模型有关信息更新到map
中。
- 该predictor由UVM参数化类uvm_reg_predictor例化并集成在顶层环境中。集成的过程中,需要将adapter与map的句柄也传给predictor;
- 将monitor采集好的事务通过analysis port接入到predictor一侧将monitor采集好的事务通过analysis port接入到predictor一侧。monitor一旦捕捉到有效事务,会发送给predicotr。
- 调用函数uvm_reg_map::set_auto_predict(0) 进行显式预测,在调用predict()函数更新镜像值,不需要观察实际的物理变化。
class mcdf_env extends uvm_env;
...
mcdf_rgm rgm;
reg2mcdf_adapter adapter;
uvm_reg_predictor #(reg_trans) predictor;//1. uvm_reg_predictor例化在环境中
`uvm_component_utils(mcdf_env)
...
function void build_phase(uvm_phase phase);
super.build_phase(phase);
agt = reg_agent::type_id::create("reg_agt", this);
...
rgm = mcdf_rgm::type_id::create("rgm", this);// 创建regmodel实体
rgm.build(); //显示调用build()函数,创建UVM寄存器层次
adapter = reg2mcdf_adapter::type_id::create("adapter", this);
predictor = uvm_reg_predictor#(reg_trans)::type_id::create("predictor", this);
endfunction
function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
...
reg_agt.monitor.mon_bp_port.connect(chker.reg_bp_imp);
//map, adapter和sequencer关联
rgm.map.set_sequencer(reg_agt.sequencer, adapter); //4. 显式预测寄存器的属性值 区别set_auto_predict
reg_agt.monitor.mon_ana_port.connect(predictor.bus_in);
predictor.map = rgm.map;//map句柄传给predictor
predictor.adapter = adapter;//adapter句柄传给predictor
// connect the virtual sequencer's rgm handle with rgm object
virt_sqr.rgm = rgm;
endfunction
endclass: mcdf_env
四、总结
RAL模型的创建就是为了 :使UVM中的sequence能够集成RAL模型,进而通过一系列的write、read、mirror等方法来操作RAL寄存器模型,模拟DUT行为,这样对寄存器的一切维护修改工作只需要通过修改该模型即可。
寄存器模型有如下好处:
- 寄存器模型建立的目的,初始目的,是为了验证寄存器配置过程的。
- uvm里提供内置sequence,可以实现后门访问的路径检查、寄存器初始值的检查、寄存器读写属性的检查等。
- 寄存器模型里,有一套标准的访问接口,暂且叫做reg接口;可以通过uvm_adapter实现apb到reg的接口转换,以及reg到apb的接口转换。ps:其中apb是apb协议的事务接口,而非apb协议的信号接口。
- 增加了后门访问机制。带来的好处是,不消耗仿真时间;提供直接访问hdl path的方式,而非DUT apb接口信号方式,所以能够做的事情更多,比如强制改变某寄存器的初始值等。
- 寄存器模型,可以跟寄存器描述文档对应。所以,可以根据寄存器模型,更新寄存器描述文档;又或者根据寄存器描述文档,更新寄存器模型。最终实现,寄存器描述文档与设计代码的准确对应。
- 有了寄存器模型,任意uvm_component都可以很方便的获取寄存器的状态值。比如reference model组件。
参考:
https://www.testandverification.com/DVClub/19_Nov_2012/9%20-%20Dialog%20-%20Steve%20Holloway%20%28User%20Paper%29.pdf
http://cluelogic.com/2013/02/uvm-tutorial-for-candy-lovers-register-access-methods/
http://blog.eetop.cn/blog-1561828-6266222.html