UVM实战 卷I学习笔记10——UVM中的寄存器模型(6)


寄存器模型的高级用法

*使用reg_predictor

前面读操作的返回值介绍了左图的方式,这种方式要依赖于driver。当driver将读取值返回后,寄存器模型会更新寄存器的镜像值和期望值,这是寄存器模型的auto predict功能。在建立寄存器模型时使用如下语句打开此功能:rm.default_map.set_auto_predict(1);

除了左图使用driver的返回值更新寄存器模型外,还存在另一种形式如右图所示。这种形式是由monitor将从总线上收集到的transaction交给寄存器模型,后者更新相应寄存器的值
在这里插入图片描述
要使用这种方式更新数据,需要实例化一个reg_predictor,并为这个reg_predictor实例化一个adapter

class base_test extends uvm_test;
	…
	reg_model rm;
	my_adapter reg_sqr_adapter;
	my_adapter mon_reg_adapter;
	uvm_reg_predictor#(bus_transaction) reg_predictor;
	…
endclass
	function void base_test::build_phase(uvm_phase phase);
		…
		rm = reg_model::type_id::create("rm", this);
		rm.configure(null, "");
		rm.build();
		rm.lock_model();
		rm.reset();
		reg_sqr_adapter = new("reg_sqr_adapter");
		mon_reg_adapter = new("mon_reg_adapter");
		reg_predictor = new("reg_predictor", this); //notice
		env.p_rm = this.rm;
	endfunction
	function void base_test::connect_phase(uvm_phase phase);
		…
		rm.default_map.set_sequencer(env.bus_agt.sqr, reg_sqr_adapter);
		rm.default_map.set_auto_predict(1); //notice
		reg_predictor.map = rm.default_map;
		reg_predictor.adapter = mon_reg_adapter;
		env.bus_agt.ap.connect(reg_predictor.bus_in);
	endfunction

connect_phase中需要将reg_predictor和bus_agt的ap口连接在一起,并设置reg_predictor的adapter和map只有设置了map后,才能将predictor和寄存器模型关联在一起

当总线上**只有一个主设备(master)**时,左图和右图完全等价有多个主设备时左图会漏掉某些trasaction

经过上面代码的设置,事实上存在两条更新寄存器模型的路径:一是上面右图虚线所示的自动预测途径,二是经由predictor的途径。如果要彻底关掉虚线的更新路径:rm.default_map.set_auto_predict(0);

*使用UVM_PREDICT_DIRECT功能与mirror操作

UVM提供mirror操作用于读取DUT中寄存器的值并将它们更新到寄存器模型中。它的函数原型为:

task uvm_reg::mirror(output uvm_status_e status,
					input uvm_check_e check = UVM_NO_CHECK,
					input uvm_path_e path = UVM_DEFAULT_PATH,);

它常用的参数只有前三个。其中第二个参数指的是如果发现DUT中寄存器的值与寄存器模型中的镜像值不一致,那么在更新寄存器模型之前是否给出错误提示。其可选的值为UVM_CHECK和UVM_NO_CHECK

它有两种应用场景,一是在仿真中不断地调用它,使整个寄存器模型的值与DUT中寄存器的值保持一致,此时check选项是关闭的。二是在仿真即将结束时检查DUT中寄存器的值与寄存器模型中寄存器的镜像值是否一致,这种情况下check选项是打开的。

mirror操作会更新期望值和镜像值。同update操作类似,mirror操作可以在uvm_reg级别和uvm_reg_block级别被调用。当调用一个uvm_reg_block的mirror时,其实质是调用加入其中的所有寄存器的mirror。

前文说过在通信系统中存在大量的计数器。当网络出现异常时借助这些计数器能够快速找出问题所在,所以必须要保证这些计数器的正确性。一般会在仿真即将结束时使用mirror操作检查这些计数器的值是否与预期值一致。

在DUT中的计数器是不断累加的,但寄存器模型中的计数器则保持静止。参考模型会不断统计收到了多少包,那么怎么将这些统计数据传递给寄存器模型呢?

前文中介绍的所有操作都无法完成这个事情,无论是set还是write,或是poke;无论是后门访问还是前门访问。这个问题的实质是想人为地更新镜像值,但同时又不要对DUT进行任何操作

UVM提供predict操作来实现这样的功能

function bit uvm_reg::predict (uvm_reg_data_t value,
							uvm_reg_byte_en_t be = -1,
							uvm_predict_e kind = UVM_PREDICT_DIRECT,
							uvm_path_e path = UVM_FRONTDOOR,);

其中第一个参数表示要预测的值,第二个参数是byte_en,默认-1的意思是全部有效,第三个参数是预测的类型,第四个参数是后门访问或者是前门访问。第三个参数预测类型有如下几种可以选择:

typedef enum {
	UVM_PREDICT_DIRECT,
	UVM_PREDICT_READ,
	UVM_PREDICT_WRITE
} uvm_predict_e;

read/peek和write/poke操作在对DUT完成读写后也会调用此函数,只是给出的参数是UVM_PREDICT_READ和UVM_PREDICT_WRITE

要实现在参考模型中更新寄存器模型而又不影响DUT的值,需要使用UVM_PREDICT_DIRECT,即默认值:

task my_model::main_phase(uvm_phase phase);
	…
	p_rm.invert.read(status, value, UVM_FRONTDOOR);
	while(1) begin
		port.get(tr);if(value)
			invert_tr(new_tr);
		counter = p_rm.counter.get();
		length = new_tr.pload.size() + 18;
		counter = counter + length;
		p_rm.counter.predict(counter); //notice
		ap.write(new_tr);
	end
endtask

在my_model中每得到一个新的transaction,就先从寄存器模型中得到counter的期望值(此时与镜像值一致),之后将新的transaction的长度加到counter中,最后使用predict函数将新的counter值更新到寄存器模型中predict操作会更新镜像值和期望值

在测试用例中,仿真完成后可以检查DUT中counter的值是否与寄存器模型中的counter值一致

class case0_vseq extends uvm_sequence;
	…
	virtual task body();
		…
		dseq = case0_sequence::type_id::create("dseq");
		dseq.start(p_sequencer.p_my_sqr);
		#100000;
		p_sequencer.p_rm.counter.mirror(status, UVM_CHECK, UVM_FRONTDOOR); //注意是check模式
		…
	endtask
endclass

*寄存器模型的随机化与update

前文在向uvm_reg中加入uvm_reg_field时,是将加入的uvm_reg_field定义为rand类型:

class reg_invert extends uvm_reg;
	rand uvm_reg_field reg_data;
	… 
endclass

在将uvm_reg加入uvm_reg_block中时,同样定义为rand类型:

class reg_model extends uvm_reg_block;
	rand reg_invert invert;
	…
endclass

由此可以判断register_model支持randomize操作。可以在uvm_reg_block级别调用randomize函数,也可以在uvm_reg级别,甚至可以在uvm_reg_field级别调用:

assert(rm.randomize());
assert(rm.invert.randomize());
assert(rm.invert.reg_data.randomize());

但要使某个field能够随机化,只是将其定义为rand类型是不够的。在每个reg_field加入uvm_reg时,要调用其configure函数:

// parameter: parent, size, lsb_pos, access, volatile, reset value, has_reset,
// is_rand, individually accessible
reg_data.configure(this, 1, 0, "RW", 1, 0, 1, 1, 0);

这个函数的第八个参数决定此field是否会在randomize时被随机化。但即使此参数为1,也不一定能够保证此field被随机化。当一个field的类型中没有写操作时,此参数设置是无效的。即此参数只在此field类型为RW、WRC、WRS、WO、W1、WO1时才有效。

因此要避免一个field被随机化,可以在以下三种方式中任选其一:

  1. 当在uvm_reg中定义此field时,不要设置为rand类型
  2. 在调用此field的configure函数时,第八个参数设置为0
  3. 设置此field的类型为RO、RC、RS、WC、WS、W1C、W1S、W1T、W0C、W0S、W0T、W1SRC、W1CRS、W0SRC、W0CRS、WSRC、WCRS、WOC、WOS中的一种(没有写操作)。

其中第一种方式也适用于关闭某个uvm_reg或者某个uvm_reg_block的randomize功能

既然存在randomize,那么也可以为它们定义constraint

class reg_invert extends uvm_reg;
	rand uvm_reg_field reg_data;
	constraint cons{
		reg_data.value == 0;
	}
…
endclass

在施加约束时,要深入reg_field的value变量。

randomize会更新寄存器模型中的预期值

function void uvm_reg_field::post_randomize();
	m_desired = value;
endfunction: post_randomize

与set函数类似。因此,可以在randomize完成后调用update任务将随机化后的参数更新到DUT中。这特别适用于在仿真开始时随机化并配置参数

扩展位宽

在reg_invert的new函数中,调用super.new时的第二个参数是16,这个数字一般表示系统总线的宽度,它可以是32、64、128等。

class reg_invert extends uvm_reg;
	rand uvm_reg_field reg_data;
	virtual function void build();
		reg_data = uvm_reg_field::type_id::create("reg_data");
		// parameter: parent, size, lsb_pos, access, volatile, reset value, has_reset, is_rand, indi
		reg_data.configure(this, 1, 0, "RW", 1, 0, 1, 1, 0);
	endfunction
	`uvm_object_utils(reg_invert)
	function new(input string name="reg_invert");
		//parameter: name, size, has_coverage
		super.new(name, 16, UVM_NO_COVERAGE);
	endfunction
endclass

但在寄存器模型中,这个数字的默认最大值是64,它是通过一个宏来控制的:

`ifndef UVM_REG_DATA_WIDTH
	`define UVM_REG_DATA_WIDTH 64
`endif

如果想要扩展系统总线的位宽,可通过重新定义这个宏来扩展

与数据位宽相似的是地址位宽也有默认最大值限制,其默认值也是64:

`ifndef UVM_REG_ADDR_WIDTH
	`define UVM_REG_ADDR_WIDTH 64
`endif

在默认情况下,字选择信号的位宽等于数据位宽除以8,它通过如下的宏来控制:

ifndef UVM_REG_BYTENABLE_WIDTH
	`define UVM_REG_BYTENABLE_WIDTH ((`UVM_REG_DATA_WIDTH-1)/8+1) //notice
`endif

如果想要使用其他值,也可以重新定义这个宏。

寄存器模型的其他常用函数

get_root_blocks

在以前例子中,若某处要使用寄存器模型,则必须将寄存器模型的指针传递过去,如在virtual sequence中使用,需要传递给virtual sequencer:

function void base_test::connect_phase(uvm_phase phase);
	…
	v_sqr.p_rm = this.rm;
endfunction

UVM还提供其他函数,使得可以在不使用指针传递的情况下得到寄存器模型的指针

function void uvm_reg_block::get_root_blocks(ref uvm_reg_block blks[$]);

get_root_blocks函数得到验证平台上所有的根块(root block——最顶层的reg_block)。其使用示例如下:

class case0_cfg_vseq extends uvm_sequence;
	…
	virtual task body();
		uvm_status_e status;
		uvm_reg_data_t value;
		bit[31:0] counter;
		uvm_reg_block blks[$];
		reg_model p_rm;
		…
		uvm_reg_block::get_root_blocks(blks);
		if(blks.size() == 0)
			`uvm_fatal("case0_cfg_vseq", "can't find root blocks")
		else begin
			if(!$cast(p_rm, blks[0]))
				`uvm_fatal("case0_cfg_vseq", "can't cast to reg_model")
		end
		p_rm.invert.read(status, value, UVM_FRONTDOOR); //notice
		…
	endtask
endclass

在使用get_root_blocks函数得到reg_block的指针后,要使用cast将其转化为目标reg_block形式( 示例中为reg_model)。以后就可以直接使用p_rm来进行寄存器操作,而不必使用p_sequencer.p_rm。

get_reg_by_offset函数

建立寄存器模型后,可直接通过层次引用的方式访问寄存器:rm.invert.read(…);

但出于某些原因依然要使用地址来访问寄存器模型,可使用get_reg_by_offset函数通过寄存器的地址得到一个uvm_reg的指针,再调用此uvm_reg的read或者write就可以进行读写操作

virtual task read_reg(input bit[15:0] addr, output bit[15:0] value);
	uvm_status_e status;
	uvm_reg target;
	uvm_reg_data_t data;
	uvm_reg_addr_t addrs[];
	target = p_sequencer.p_rm.default_map.get_reg_by_offset(addr); //notice
	if(target == null)
		`uvm_error("case0_cfg_vseq", $sformatf("can't find reg in register model with address: 'h%
	target.read(status, data, UVM_FRONTDOOR);
	void'(target.get_addresses(null,addrs));
	if(addrs.size() == 1)
39		value = data[15:0];
	else begin
41		int index;
42		for(int i = 0; i < addrs.size(); i++) begin
43			if(addrs[i] == addr) begin
44				data = data >> (16*(addrs.size() - i));
45				value = data[15:0];
46				break;
47			end
48		end
	end
endtask

通过调用最顶层的reg_block的get_reg_by_offset可以得到任一寄存器的指针。如果使用层次的寄存器模型,从最顶层的reg_block的get_reg_by_offset也可以得到子reg_block中的寄存器

假如buf_blk的地址偏移是’h1000,其中有偏移为’h3的寄存器(即其物理地址是’h1003),那么可以直接由p_rm.get_reg_by_offset('h1003)得到此寄存器,而不必使用p_rm.buf_blk.get_reg_by_offset('h3)。

如果没有使用多地址寄存器则情况比较简单,上述代码会运行第39行。当存在多个地址时,通过get_addresses函数可以得到这个函数的所有地址,其返回值是一个动态数组addrs。无论是大端还是小端,addrs[0]是LSB对应的地址。第41到48行通过比较addrs中的地址与目标地址,最终得到要访问的数据。

写寄存器与读操作类似。

  • 1
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
UVM提供了uvm_reg_backdoor类,用于在测试访问寄存器的内部实现。这个类可以让我们在测试使用不同的方式来访问寄存器,以验证寄存器的功能和性能。 uvm_reg_backdoor类主要有两个方法: - `void read(uvm_reg_item rw)`:读取寄存器的值,将结果存储在rw.value。 - `void write(uvm_reg_item rw)`:写入寄存器的值,将值存储在rw.value。 其,`uvm_reg_item`是一个包含寄存器地址、写入/读取值等信息的uvm序列化对象。 要使用uvm_reg_backdoor类,我们需要创建一个新类,继承自uvm_reg_backdoor。在新类的构造函数,我们需要调用基类的构造函数,并通过该函数将要访问的寄存器作为参数传递。 下面是一个使用uvm_reg_backdoor类的示例: ```systemverilog class my_reg_backdoor extends uvm_reg_backdoor; `uvm_object_utils(my_reg_backdoor) function new(string name = "my_reg_backdoor"); super.new(name); endfunction virtual function void read(uvm_reg_item rw); // 从寄存器读取值 endfunction virtual function void write(uvm_reg_item rw); // 将值写入寄存器 endfunction endclass ``` 在测试,我们可以使用uvm_reg_backdoor类的实例来访问寄存器。例如: ```systemverilog my_reg_backdoor my_bd = new; uvm_reg_item rw = new; rw.element = my_reg; rw.kind = UVM_REG; rw.path = UVM_FRONTDOOR; rw.offset = 0; rw.value[0] = 0x1234; my_bd.write(rw); // 从寄存器读取值 my_bd.read(rw); $display("value = %h", rw.value[0]); ``` 使用uvm_reg_backdoor类可以方便地访问寄存器的内部实现,从而进行更全面和深入的验证。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值