文章目录
前言
2023.3.8 热
2023.4.23 小雨
寄存器模型其实是DUT内各个寄存器的“拷贝文件”,它还能够根据DUT的变化,及时更新内部属性值,达到和 DUT内部寄存器同步的目的。
一、寄存器模型简介
1.1 带寄存器配置总线的DUT
最简单的DUT:只有一组数据输入输出端口,而没有行为控制口
带寄存器配置总线的DUT:通过总线来配置寄存器,DUT根据寄存器的值来改变其行为
这里的例子是DUT中有一个1bit的寄存器invert
,分配地址为16‘h9
,如果值为1,DUT在输出时把输入数据取反,如果值为0,直接输出输入的数据。
1.2 参考模型如何读取寄存器的值
全局事件
:在参考模型触发事件,virtual sequence等待事件,启动sequence去采样值(尽量避免使用全局事件)非全局事件
:分别设置object,在object里面去设置事件,再去触发事件,等到这个事件触发就启动sequence去读取寄存器,利用的是config机制寄存器模型
:上面方法都比较麻烦,所以引出了寄存器模型。
UVM寄存器模型的本质就是重新定义了验证平台与DUT的寄存器接口,使验证人员更好地组织及配置寄存器,简化流程、减少工作量。
task my_model::main_phase(uvm_phase phase);
reg_model.INVERT_REG.read(status, value, UVM_FRONTDOOR); //一句话来完成读取寄存器的操作
endtask
-
任何消耗时间的phase:可以通过寄存器模型以
前门或后门
的方式来读取寄存器的值(前门访问是需要消耗时间的) -
某些不消耗时间的phase:如check_phase,使用
后门访问
来读取寄存器的值
1.3 寄存器模型的基本概念
一个寄存器一般是32bit位
uvm_reg_field
:寄存器模型中的最小单位,具体存储寄存器数值的变量。针对寄存器功能域来构建的比特位,单个域可能由多个/单一比特位构成
reserved域
:表示的是该域包含的比特位暂时保留以作日后功能的扩展使用,无法写入,读出来是复位值
uvm_reg
:与寄存器匹配,内部可以例化和配置多个uvm_reg_field对象,一个寄存器至少包含一个uvm_reg_field
uvm_mem
:匹配硬件存储模型
uvm_reg_map
:用来指定寄存器列表中各个寄存器的偏移地址、访问属性以及对应的总线。当寄存器模型使用前门访问方式来实现读或写操作时,uvm_reg_map就会将地址转换成绝对地址,后启动一个读或写的sequence。并将读或写的结果返回。在每个uvm_reg_block内部,至少有一个(通常也只有一个) uvm_reg_map
uvm_reg_block
:比较大的单位,可以容纳多个寄存器(uvm_reg)、存储器(uvm_mem)和存储器列表(uvm_reg_map)。一个寄存器模型中至少包含一个uvm_reg_block
二、简单的寄存器模型
2.1 只有一个寄存器的寄存器模型
(1)为前文提到的invert寄存器创建寄存器模型,从uvm_reg派生出一个类
build函数
:不同于build_phase,不是自动执行,需要手动调用,例化所有的uvm_reg_field,再调用configure
函数进行配置new函数
:三个参数,名称,寄存器宽度,覆盖率相关
class reg_invert extedns uvm_reg;
`uvm_object_utils(reg_invert)
rand uvm_reg_field reg_data;
virtual function void build();
reg_data = uvm_reg_field::type_id::create("reg_data");
//parent,size,lsb_pos,access,volatile,resetvalue,has_reset,is_rand,
reg_data.configure(this, 1, 0, "RW", 1, 0, 1, 1, 0);
endfunction
function new(input string name = "reg_invert");
//name size:整个寄存器的宽度,一般和总线宽度相同,不是实际使用的宽度 has_coverage:是否加入覆盖率的支持
super.new(name, 16, UVM_NO_COVERAGE);
endfunction
endclass
uvm_reg_field的configure函数的九个参数:
- 此域的父辈:也就是这个域位于哪个寄存器,此处填
this
- 此域的宽度:这个寄存器宽度为1
- 此域的最低位在整个寄存器中的位置:从0开始算
- 此字段的存取方式:有25种,
RW
的意思是尽量写入,读取时对此域无影响 - 是否易失,很少使用
- 上电复位的默认值
- 是否复位,一般都有复位默认值
- 是否可以随机化,主要用于对寄存器进行随机写测试,如果是0,则不能随机,是复位值;且这个参数当且仅当第四个参数为RW(读写)、WRC、WRS、WO(只写)、RO(只读)、W1、WO1时才有效
- 此域是否可以单独存取
(2)定义好这个寄存器后,再从uvm_reg_block派生一个类将其实例化
class reg_model extends uvm_reg_block;
`uvm_object_utils(reg_model)
rand reg_invert invert;
virtual function void build();
default_map = create_map("default_map", 0, 2, UVM_BIG_ENDIAN, 0); //用来存储不同reg在reg block中的地址
invert = reg_invert::type_id::create("invert", , get_full_name());
invert.configure(this, null, "invert"); //配置这个寄存器
invert.build(); //手动调用,实例化各个域
default_map.add_reg(invert, 'h9, "RW"); //要加入的寄存器,寄存器地址,寄存器的存取方式
endfunction
function new(input string name = "reg_model");
super.new(name, UVM_NO_COVERAGE);
endfunction
endclass
build函数
:例化所有的寄存器。
一个uvm_reg_block
中一定要对应一个uvm_reg_map
,系统已经有一个声明好的default_map
,只需要在build中将其实例化,通过调用uvm_reg_block的create_map
来实现。
随后实例化invert并调用它的configure
函数。再手动调用invert的build
函数,实例化invert里面的域。
最后一步是把此寄存器加入default_map,否则无法进行前门访问。new函数
:名字,是否支持覆盖率
uvm_reg_block的create_map函数的五个参数:
- 名字
- 基地址
- 系统总线的宽度,单位为byte
- 大小端
- 是否按照byte寻址
uvm_reg的configure函数的三个参数:
- 此寄存器所在的uvm_reg_block的指针,这里填this
- reg_file的指针,这里暂时填null
- 此寄存器的后门访问路径
2.2 将寄存器模型集成到验证平台
由于寄存器模型中存储数据的格式和在验证平台中使用的格式不一致,所以需要一个adapter
模块负责格式转换
,使寄存器模型能够和 sequencer 实现数据交互
。
寄存器模型的前门访问操作:分为读和写两种
读和写都会通过sequence产生一个uvm_reg_bus_op
的变量,里面存储着操作类型和操作地址以及写的数据。通过转换器adapter交给bus_sequencer,随后给bus_driver,由bus_driver来实现最终的前门访问读写操作。
图中的虚线(driver指向adapter):表示并没有实际的transaction的传递
class my_adapter extends uvm_reg_adapter;
string tID = get_type_name();
`uvm_object_utils(my_adapter) //adapter是object类型
function new(string name = "my_adapter");
super.new(name);
endfunction
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 actiion")
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
reg2bus函数
:将寄存器模型通过sequence发出的uvm_reg_bus_op类型变量转换为bus_sequencer接受的类型bus2reg函数
:当检测到总线上有操作时,将收集到的transaction转换为寄存器模型接受的类型,以便寄存器模型去更新相应的寄存器的值
转换器写好后,再base_test
里面加入寄存器模型:
class base_test extends uvm_test;
my_env env;
my_adapter reg_sqr_adapter;
my_vsqr v_sqr;
reg_model rm;
function void 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, "") ; //parent block:由于是最顶层的,所以null;后门访问路径
rm.build ( ) ; //例化所有的寄存器
rm.reset ( ) ; //调用后,所有寄存器的值变成复位值,不调用,则全为0
rm.lock_model ( ); //调用后不能再加入新的寄存器
rm.set_hdl_path_root("top_tb.my_dut"); //设置后门访问的绝对路径
reg_sqr_adapter = new ( "reg_sqr_adapter" ); //实例化adapter
env.p_rm = this.rm;
endfunction
function void 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) ; //将sqr和adapter连接起来
rm.default_map.set_auto_predict (1); //设置为自动预测状态,意味着reg model中的镜像值时刻和DUT中对应的reg值一样
endfunction
endclass
寄存器模型的前门访问操作最终都将由uvm_reg_map完成﹐因此在connect _phase中,需要将转换器和bus_sequencer通过set_sequencer
函数告知reg_model的default_map,并将default_map设置为自动预测状态。
2.3 在验证平台中使用寄存器模型
可以在sequence和其他component中使用。
以参考模型使用寄存器模型为例,需要在参考模型中有一个指向寄存器模型的指针。
class my_model extneds uvm_component; //这个model指的是参考模型
reg_model p_rm;
...
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.invert.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:",UVM_LOW)
new_tr.print ( );
if (value)
invert_tr (new_tr) ;
ap.write (new_tr);
end
endtask
endclass
//my_env把p_rm传递给参考模型
mdl.p_rm = this.p_rm;
read任务:
uvm_status_e
:表明读操作是否成功uvm_reg_data_t
:读取的数值- 读取的方式:前门或者后门
参考模型一般不会写寄存器,因此在virtual sequence
里面进行写操作。
class case0_cfg_vseq extends uvm_sequence;
virtual task body();
uvm_status_e status;
uvm_reg_data_t value;
p_sequencer.p_rm.invert.write(status, 1, UVM_FRONTDOOR) ;
endtask
endclass
寄存器模型对transaction类型没有要求。因此可以在一个发送my_transaction的sequence中使用寄存器模型来对寄存器进行读写操作。
三、前门访问和后门访问
uvm_reg_sequence
:继承于uvm_sequence,所以包含之前预定义的宏,还具有寄存器操作的方法
3.1 前门访问
定义:通过寄存器配置总线
来对DUT进行读写操作。在这个过程中,仿真时间($time函数得到的时间)是一直往前走的。是消耗仿真时间的。有两者操作方法。
uvm_reg::read()/write()
:传递时需要注意将参数path指定为UVM_FRONTDOOR
。uvm_reg::read()/write()方法可传入的参数较多,除了status和value两个参数需要传入,其它参数如果不指定,可采用默认值。uvm_reg_sequence::read_reg()/write_reg()
:在使用时,也需要将path指定为UVM_FRONTDOOR。
读操作的完整流程:
- 参考模型调用寄存器模型的读任务
- 寄存器模型产生sequence,并产生uvm_reg_item:rw
- 产生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中的读数据返回参考模型
如果driver一直发送应答而sequence不收集应答,那么将会导致sequencer的应答队列溢出。因此在adapter中设置了provide_responses选项。
provides_responses
:将读入的数据写到rsp并且返回sequencer,要进行设置(调用put_response和item_done的时候要返回rsp),否则两个值默认为0
3.2 后门访问
定义:与前门访问相对的操作,它并不通过总线进行读写操作,而是直接通过层次化的引用
来改变寄存器的值。
- 所有后门访问操作都是不消耗仿真时间(即$time打印的时间)而只消耗运行时间的。
- 从广义上来说,所有不通过DUT的总线而对DUT内部的寄存器或者存储器进行存取的操作都是后门访问操作。
- 通过设置好
每个寄存器的路径
进行访问的。配置reg的configur函数以及在base_test里面set_hdl_path_root,两者合在一起就是寄存器的绝对路径。
后门访问有三种操作方法:
uvm_reg::read()/write()
:uvm_reg_sequence::read_reg()/write_reg()
:在调用该方法时需要注明UVM_BACKDOOR
的访问方式uvm_reg::peek()/poke()
:分别对应了读取寄存器(peek)和修改寄存器(poke)两种操作,而用户无需指定访问方式为UVM_BACKDOOR,因为这两个方法本来就只针对于后门访问。
reg_model.INVERT_REG.peek(status, value, UVM_BACKDOOR);
// 通过后门访问方式读取寄存器的值,不关心DUT的行为,即使寄存器的读写类型是不能读,也可以将值读出来
reg_model.INVERT_REG.poke(status, 16'h1, UVM_BACKDOOR);
//通过后门访问方式写入寄存器的值,不关心DUT的行为,即使寄存器的读写类型是不能写,也可以将值写进去
3.3 前门访问VS后门访问
前门访问 | 后门访问 |
---|---|
通过总线访问需要消耗时间,总线访问结束时才能结束前门访问 | 通过UVM DPI关联硬件寄存器信号路径,直接读取或修改硬件,不需要访问时间,零时刻响应 |
一般读写只能按字word读写(总线为32位),无法直接读写寄存器域 | 直接读写寄存器或寄存器域 |
正确反映时序关系 | 不受时序控制,可能访问时发送冲突 |
依靠监控总线来对寄存器模型内容做预测 | 依靠auto prediction方式对寄存器内容做预测 |
有效捕捉总线错误,进而验证总线访问路径 | 不受总线时序功能影响 |
3.4 前门和后门混合应用的场景
(1)只能写一次的寄存器:用物理访问的方式去反应硬件的真实情况
(2)先用前门访问去判断物理通路是否正常,遍历所有寄存器,然后再用后门访问去节约时间
(3)寄存器随机设置:考虑日常不可预测的场景,先通过后门访问随机化整个寄存器列表(在一定的随机限制下),随后再通过前门访问来配置寄存器。
(4)解决地址映射到内部错误寄存器的问题:前门写后门读,或者后门写前门读的方式
(5)状态寄存器:有延时,有时外界的激励条件修改会依赖这些状态寄存器。了需要前门和后门来访问寄存器,也需要映射一些重要的信号来反映第一时间的信息。
四、常见面试题
4.1 为什么需要寄存器模型
DUT的寄存器可以对DUT的行为进行配置,一般通过发送sequence对寄存器进行读写操作,但是当寄存器数目较多时,这样使用起来不方便且容易出错,因此使用寄存器模型,内部分别定义了不同的寄存器,直接对寄存器名字进行索引,就可以读写。而且可以修改访问的方式,是前门还是后门。
下面是通过sequence来读写寄存器,当个数较多是比较复杂
`uvm_do_with(m_trans, {m_trans.addr == 16'h9;
m_trans.bus_op == BUS_RD;
})//对地址为16'h9的reg发起一笔读操作
`uvm_do_with(m_trans, {m_trans.addr == 16'h9;
m_trans.bus_op == BUS_WR;
m_trans.wr_data == 16'h1;
})//对地址为16'h9的reg发起一笔写操作,写入1
reg_model.INVERT_REG.read(status, value, UVM_FRONTDOOR); //对名字是INVERT_REG的寄存器执行读操作
reg_model.INVERT_REG.write(status, 16'h1, UVM_FRONTDOOR); //对名字是INVERT_REG的寄存器执行写操作,写1