文章目录
- 前言
- 一、寄存器模型简介
- 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



















