文章目录
- 前言
- 一、寄存器模型简介
- 1.1 带寄存器配置总线的DUT
- 1.2 参考模型如何读取寄存器的值
- 1.3 寄存器模型的基本概念
- 二、简单的寄存器模型
- 2.1 只有一个寄存器的寄存器模型
- 2.2 将寄存器模型集成到验证平台
- 2.3 在验证平台中使用寄存器模型
- 三、前门访问和后门访问
- 3.1 前门访问
- 3.2 后门访问
- 3.3 前门访问VS后门访问
- 3.4 前门和后门混合应用的场景
- 四、常见面试题
- 4.1 为什么需要寄存器模型
前言
2023.3.8 热
2023.4.23 小雨
一、寄存器模型简介
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 将寄存器模型集成到验证平台
寄存器模型的前门访问操作:分为读和写两种
读和写都会通过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