UVM入门与进阶学习笔记17——寄存器模型(2)

目录

  • 寄存器模型集成
    • 总线UVC的实现
    • 总线UVC解析
    • MCDF寄存器设计代码
    • Adapter的实现
    • Adapter的集成
  • 访问方式
    • 前门访问
    • 后门访问
    • 前门与后门的比较
    • 前门与后门的混合应用


寄存器模型集成

总线UVC的实现

  • MCDF访问寄存器的总线接口时序较为简单。控制寄存器接口上首先需要在每个时钟解析cmd。
  • cmd为指令时,需要把数据cmd_data_in写入到cmd_addr对应的寄存器中
  • cmd为指令时,需要从cmd_addr对应的寄存器中读取数据,在下一个周期cmd_addr对应的寄存器数据被输送至cmd_data_out接口
  • 按照上面的时序,给出一段8位地址线,32位数据线的总线UVC实现代码:
class mcdf_bus_trans extends uvm_sequence_item;
	rand bit[1:0] cmd;
	rand bit[7:0] addr;
	rand bit[31:0] wdata;
	bit[31:0] rdata;  //没有用rand是因为它应从总线读出或观察
	`uvm_object_utils_begin(mcdf_bus_trans)
		...
	`uvm_object_utils_end
	...
endclass
class mcdf_bus_sequencer extends uvm_sequencer;
	virtual mcdf_if vif;
	`uvm_component_utils(mcdf_bus_sequencer)
	...
	function void build_phase(uvm_phase phase);
		if(!uvm_config_db#(virtual mcdf_if)::get(this, "", "vif", vif)) begin
			`uvm_error("GETVIF", "no virtual interface is assigned")
		end
	endfunction
endclass
class mcdf_bus_monitor extends uvm_monitor;
	virtual mcdf_if vif;
	uvm_analysis_port#(mcdf_bus_trans) ap;
	`uvm_component_utils(mcdf_bus_monitor)
	...
	function void build_phase(uvm_phase phase);
		if(!uvm_config_db#(virtual mcdf_if)::get(this, "", "vif", vif)) begin
			`uvm_error("GETVIF", "no virtual interface is assigned")
		end
		ap = new("ap", this);  //component类
	endfunction
	task run_phase(uvm_phase phase);
		forever begin //注意forever
			mon_trans();
		end
	endtask
	task mon_trans();
		mcdf_bus_trans t;
		@(posedge vif.clk);
		if(vif.cmd == `WRITE) begin
			t = new();
			t.cmd = `WRITE;
			t.addr = vif.addr;
			t.wdata = vif.wdata;
			ap.write(t);
		end
		else if(vif.cmd == `READ) begin
			t = new();
			t.cmd = `READ;
			t.addr = vif.addr;
			fork
				begin
					@(posedge vif.clk);
					#10ns;
					t.rdata = vif.rdata;
					ap.write(t);
				end
			join_none  //防止错过下一拍的数据
		end
	endtask
endclass
class mcdf_bus_driver extends uvm_driver;
	virtual mcdf_if vif;
	`uvm_component_utils(mcdf_bus_driver)
	...
	function void build_phase(uvm_phase phase);
		if(!uvm_config_db#(virtual mcdf_if)::get(this, "", "vif", vif)) begin
			`uvm_error("GETVIF", "no virtual interface is assigned")
		end
	endfunction
	task run_phase(uvm_phase phase);
		REQ tmp;
		mcdf_bus_trans req, rsp;
		reset_listener();
		forever begin
			seq_item_port.get_next_item(tmp);
			void'($cast(req, tmp));
			`uvm_info("DRV", $sformatf("got a item \n %s", req.sprint()), UVM_LOW)
			void'($cast(rsp, req.clone()));
			rsp.set_sequence_id(req.get_sequence_id());
			rsp.set_transaction_id(req.get_transaction_id());
			driver_bus(rsp);
			seq_item_port.item_done(rsp);
			`uvm_info("DRV", $sformatf("sent a item \n %s", req.sprint()), UVM_LOW)
		end
	endtask
	task reset_listener();
		fork
			forever begin
				@(negedge vif.rstn) driver_idle();
			end
		join_none
	endtask
	task drive_bus(mcdf_bus_trans t);
		case(t.cmd)
			`WRITE: drive_write(t);
			`READ:  drive_read(t);
			`IDLE:  drive_idle(t);
			default: `uvm_error("DRIVE", "invalid mcdf command type received!")
		endcase
	endtask
	task drive_write(mcdf_bus_trans t);
		@(posedge vif.clk);
		vif.cmd <= t.cmd;
		vif.addr <= t.addr;
		vif.wdata <= t.wdata;
	endtask
	task drive_read(mcdf_bus_trans t);
		@(posedge vif.clk);
		vif.cmd <= t.cmd;
		vif.addr <= t.addr;
		@(posedge vif.clk);
		#10ns;
		t.rdata = vif.rdata;
	endtask
	task drive_idle(bit is_sync=0);
		if(is_sync) 
		@(posedge vif.clk);
		vif.cmd <= 'h0;
		vif.addr <= 'h0;
		vif.wdata <= 'h0;
	endtask
endclass
class mcdf_bus_agent extends uvm_agent;
	mcdf_bus_driver driver;
	mcdf_bus_sequencer sequencer;
	mcdf_bus_monitor monitor;
	`uvm_component_utils(mcdf_bus_agent)
	...
	function void build_phase(uvm_phase phase);
		driver = mcdf_bus_driver::type_id::create("driver", this);
		sequencer = mcdf_bus_sequencer::type_id::create("sequencer", this);
		monitor = mcdf_bus_monitor::type_id::create("monitor", this);
	endfunction
	function void connect_phase(uvm_phase phase);
		driver.seq_item_port.connect(sequencer.seq_item_export);
	endfunction
endclass

总线UVC解析

示例中囊括了mcdf_bus_agent的所有组件,包括sequence_itemsequencerdrivermonitoragent

  • mcdf_bus_trans包括可随机化的数据成员cmdaddrwdata和不可随机化的rdatardata没有声明为rand类型是因为它应从总线读出或观察,不应随机化。
  • mcdf_bus_monitor会观测总线,其后通过analysis port写出到目标analysis组件。
  • mcdf_bus_driver主要实现了总线驱动和复位功能,通过模块化的方法reset_listener()driver_bus()drive_write()drive_read()drive_idle()可以解析三种命令模式IDLEWRITEREAD,并且在READ模式下将读回的数据通过item_done(rsp)写回到sequencersequence一侧。

MCDF寄存器设计代码

`define IDLE 2'b00
`define WRITE 2'b01
`define READ 2'b10
`define SLVO_RW_ADDR 8'h00
`define SLV1_RW_ADDR 8'h04
`define SLV2_RW_ADDR 8'h08
`define SLVO_R_ADDR 8'h10
`define SLV1_R_ADDR 8'h14
`define SIV2_R_ADDR 8'h18
`define SLVO_RN_REG 0
`define SLV1_RW_REG 1
`define SLV2_RN_REG 2
`define SLVO_R_REG 3
`define SLV1_R_REG 4
`define SIV2_R_REG 5
module ctrl_regs(
clk_i,rstn_i, 
cmd_i, cmd_addr_i,cmd_data_i, cmd_data_o,
s1v0_len_o,slv1_len_o,slv2_len_o,
slv0_prio_o,slv1_prio_o,slv2_prio_o,
slv0_margin_i,slv1_margin_i,slv2_margin_i,
slv0_en_o,slv1_en_o,slv2_en_o);
input clk_i, rstn_i; 
input [1:0] cmd_i; 
input [7:0] cmdaddr_i;
input [31:0] cmd_data_i; 
input [7:0] slvo_margin_i;
input [7:0] slvl_margin_i;
input [7:0] slv2_margin_i; 
output [31:0] cmd_data_o; 
output [2:0] slv0_len_o;
output [2:0] slv1_len_o;
output [2:0] slv2_len_o; 
output [1:0] slv0_prio_o;
output [1:0] slv1_prio_o;
output [1:0] slv2_prio_o; 
output slv0_en_o;
output slv1_en_o;
output slv2 en_o; 
reg [31:0] reg [5:0];
reg [31:0] cmd_data_reg;
always @(posedge clk_i or negedge rstn_i) begin 
	if (!rstn_i) begin
	regs [`SLV0_RW_REG] <= 32'h00000007; 
	regs [`SLV1_RW_REG] <= 32'h00000007;
	regs [`SLV2_RW_REG] <= 32'h00000007;
	regs [`SLV0_R_REG] <= 32'h00000010;
	regs [`SLV1_R_REG] <= 32'h00000010; 
	regs [`SLV2_R_REG] <= 32'h00000010;
end
else begin
	if (cmd_i == `WRITE) begin
		case (cmd_addr_i)
		`SLV0_RW_ADDR: regs [`SLV0_RW_REG] [5:0] <= cmd_data_i;
		`SLV1_RW_ADDR: regs [`SLV1_RW_REG] [5:0] <= cmd_data_i; 
		`SLV2_RW_ADDR: regs [`SLV2_RW_REG] [5:0] <= cmd_data_i; 
		endcase
	end
	else if(cmd_i == `READ)begin 
		case(cmd_addr_i)
		`SLV0_RW_ADDR: cmd_data_reg <= regs[`SLV0_RW_REG];
		`SLV1_RW_ADDR: cmd_data_reg <= regs[`SLV1_RW_REG];
		`SLV2_RW_ADDR: cnd_data_reg <= regs[`SLV2_RW_REG];
		`SLV0_R_ADDR: cmd_data_reg <= regs[`SLV0_R_REG];
		`SLV1_R_ADDR: cmd_data_reg <= regs[`SLV1_R_REG];
		`SLV2_R_ADDR: cmd_data-reg <= regs[`SLV2_R_REG];
		endcase
	end
	regs[`SLV0_R_REG][70] <= slv0_margin_i;
	regs[`SLV1_R_REG][70] <= slv1_margin_i;
	regs[`SLV2_R_REG][70] <= slv2_margin_i;
	end
end
	assign cmd_data_o = cmd_data_reg;
	assign slv0_len_o = regs[`SLV0_RW_REG][5:3];
	assign s1v1_len_o = regs[`SLV1_RW_REG][5:3];
	assign slv2_len_o = regs[`SLV2_RW_REG][5:3];
	assign slv0_prio_o = regs[`SLV0_RW_REG][2:1];
	assign slv1_prio_o = regs[`SLV1_RW_REG][2:1];
	assign slv2_prio_o = regs[`SLV2_RW_REG][2:1];
	assign slv0_en_o = regs[`SLV0_RW_REG][0];
	assign slv1_en_o = regs[`SLV1_RW_REG][0];
	assign slv2_en_o = regs[`SLV2_RW_REG][0];
endmodule:ctrl_regs

Adapter的实现

  • 在具备了MCDF总线UVC之后需要实现adapter。每一个总线对应的adapter所完成的桥接功能即是在uvm_reg_bus_op和总线transaction之间的转换,在开发某一个总线adapter类型时,需要实现下面几点:
    • uvm_reg_bus_op与总线transaction中各自的数据映射。
    • reg2bus()bus2reg()两个函数实现两种transaction的数据映射。
    • 如果总线支持byte访问,可以使能supports_byte_enable;如果总线UVC要返回response数据则应使能provides_responsemcdf_bus_driver在读数时会将读回的数据填入到RSP并返回至sequencer,因此需要在adapter中使能provides_responses。由此使得bus2reg()函数调用时得到的数据是总线返回时的transaction,如果总线UVC不支持返回RSP(没有调用put_response(RSP)或者item_done(RSP)),那么不应该置此位,否则adapter将会使得验证环境挂起。默认情况下上述两个成员的复位值都是0。

Adapter示例

class reg2mcdf_adapter extends uvm_reg_adapter;
	`uvm_object_utils(reg2mcdf_adapter)
	function new(string name = "mcdf_bus_trans");
		super.new(name);
		provides_response = 1;
	endfunction
	function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
		mcdf_bus_trans t = mcdf_bus_trans::type_id::create("t");
		t.cmd = (rw.kind == UVM_WRITE) ? `WRITE : `READ;
		t.addr = rw.addr;
		t.wdata = rw.data;
		return t;  //返回t是子类,会隐式转换为父类
	endfunction
	function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
		mcdf_bus_trans t;
		if(!$cast(t, bus_item)) begin //父类转换为子类
			`uvm_fatal("NOT_MCDF_BUS_TYPE", 
						"Provided bus_item is not of the correct type")
			return;
		end
		rw.kind = (t.cmd == `WRITE) ? UVM_WRITE : UVM_READ;
		rw.kind = t.addr;
		rw.data = (t.cmd == `WRITE) ? t.wdata : t.rdata;
		rw.status = UVM_IS_OK;
	endfunction
endclass

Adapter解析
UVM入门与进阶学习笔记17——寄存器模型(2)_第1张图片

  • 该类在构建函数中使能了provides_response,这是因为mcdf_bus_driver在发起总线访问之后会将RSP一并返回至sequencer
  • reg2bus()完成的桥接场景是如果在寄存器级别做了操作,那么寄存器级别操作的信息uvm_reg_bus_op会被记录,同时调用uvm_reg_adapter::reg2bus()函数。
  • 完成了将uvm_reg_bus_op的信息映射到mcdf_bus_trans之后,函数将mcdf_bus_trans实例返回。之后该实例将通过mcdf_bus_sequencer传入到mcdf_bus_driver。这里的transaction传输是后台隐式调用的,不用自己发起。
  • 寄存器无论读写,都应知道总线操作后的状态返回,对于操作也需要知道总线返回的读数据,因此uvm_reg_adapter::bus2reg()是从mcdf_bus_driver()将数据写回至mcdf_bus_sequence,而一直保持监听的reg2mcdf_adapter一旦从sequencer获取了RSP(mcdf_bus_trans)之后,就将自动调用bus2reg()函数。
  • bus2reg()函数的功能与reg2bus()相反,完成了从mcdf_bus_transuvm_reg_bus_op的内容映射。完成映射之后,更新的uvm_reg_bus_op数据最终返回至寄存器操作场景层。
  • 无论读操作还是写操作,都需要经历调用reg2bus(),继而发起总线事务,而在完成事务发回反馈之后,又需要调用bus2reg(),将总线的数据返回至寄存器操作层面。

Adapter的集成

  • 具备了寄存器模型mcdf_rgm、总线UVC mcdf_bus_agent和桥接reg2mcdf_adapter之后,就需要考虑如何将adapter集成到验证环境中去:
    • 对于mcdf_rgm的集成,倾向于顶层传递的方式,即最终从test层传入寄存器模型句柄。这种方式有利于验证环境mcdf_bus_env的闭合性,在后期不同test如果要对rgm做不同的配置,都可以在顶层例化,而后通过uvm_config_db传递。
    • 寄存器模型在创建后还需显式调用build()函数。需要注意uvm_reg_blockuvm_object类型,因此其预定义的build()函数不会自动执行,需单独调用。
    • 在还未集成predictor之前采用了auto prediction的方式,因此调用了函数set_auto_predict()
    • 顶层环境的connect阶段还需要将寄存器模型的map组件与bus_sequenceradapter连接。这样才能将map(寄存器信息)、sequencer(总线侧激励驱动)和adapter(寄存器级别和硬件总线级别的桥接)关联在一起。也只有通过这一步,adapter的桥接功能才可以工作。

Adapter集成示例

class mcdf_bus_env extends uvm_env;
	mcdf_bus_agent agent;
	mcdf_rgm rgm;
	reg2mcdf_adapter reg2mcdf;
	`uvm_component_utils(mcdf_bus_env)
	...
	function void build_phase(uvm_phase phase);
		agent = mcdf_bus_agent::type_id::create("agent", this);
		if(!uvm_config_db#(mcdf_rgm)::get(this, "", "rgm", rgm)) begin
			`uvm_info("GETRGM", "no top-down RGM handle is assigned", UVM_LOW)
			rgm = mcdf_rgm::type_id::create("rgm", this);
			`uvm_info("NEWRGM", "created rgm instance locally", UVM_LOW)
		end
		/*rgm.build()会创建各个uvm_reg,进而对uvm_reg作config,
		还会调用各个uvm_reg中的build(),去例化每个uvm_reg中的field,去配置各个field*/
		rgm.build();
		rgm.map.set_auto_predict();
		reg2mcdf = reg2mcdf::type_id::create("reg2mcdf");
	endfunction
	function void connect_phase(uvm_phase phase);
		rgm.map.set_sequencer(agent.sequencer, reg2mcdf); //notice
	endfunction
endclass
class test1 extends uvm_test;
	mcdf_rgm rgm;
	mcdf_bus_env env;
	`uvm_component_utils(test1)
	...
	function void build_phase(uvm_phase phase);
		rgm = mcdf_rgm::type_id::create("rgm", this);
		uvm_config_db#(mcdf_rgm)::set(this, "env*", "rgm", rgm);
		env = mcdf_bus_env::type_id::create("env", this);
	endfunction
	task run_phase(uvm_phase phase);
		...
	endtask
endclass

UVM入门与进阶学习笔记17——寄存器模型(2)_第2张图片

访问方式

利用寄存器模型,可以更方便地对寄存器做操作,分为两种访问寄存器的方式,即前门访问和后门访问:

  • 前门访问,指的是在寄存器模型上做的读写操作,最终会通过总线UVC实现总线上的物理时序访问,因此是真实的物理操作
  • 后门访问,指的是利用UVM DPIuvm_hdl_read()uvm_hdl_deposit())将寄存器的操作直接作用到DUT内的寄存器变量,而不通过物理总线访问

前门访问

前门访问的示例中的sequence继承于uvm_reg_sequenceuvm_reg_sequence除具备一般uvm_sequence的预定义方法外,还具有跟寄存器操作相关的方法。

  • 第一种即uvm_reg::read()/write()。传递时需要注意将参数path指定为UVM_FRONTDOORuvm_reg::read()/write()方法可传入的参数较多,除了statusvalue两个参数需要传入,其它参数如果不指定,可采用默认值。
  • 第二种即uvm_reg_sequence::read_reg()/write_reg()。在使用时,也需要将path指定为UVM_FRONTDOOR

前门访问示例:

class mcdf_examnle_seq extends uvm_reg_seqnence;
	mcdf_rgn rgm;
	`uvm_object_utils(mcdf_example_seq)
	`uvm_declare_p_sequencer(mcdf_bus_sequencer)
	...
	task body();
		uvm_status_e status;
		uvm_reg_data_t data;
		if(!uvm_config_db#(mcdf_rgm)::get(null,get_full_name(),"rgm",rgm))begin
			`uvm_error("GETRGM","no top-down RGM handle is assigned")
		end
		//register model access write()/read()
		rgm.chn10_ctrl_reg.read(status, data, UVM_FRONTDOOR, .parent(this));
		rgm.chn10_ctrl_req.write(status, 'h11, UVM_FRONTDOOR, .parent(this));
		rgm.chnl0_ctrl_reg.read(status, data, UVM_FRONTDOOR, .parent(this));
		//pre-defined methods access 
		read_reg(rgm.chnl1_ctrl_reg, status, data, UVM_FRONTDOOR);
		write_reg(rgm.chnl1_ctrl_reg, status, 'h22, UVM_FRONTDOOR);
		read_reg(rgm.chnll_ctrl_reg, status, data, UVM_FRONTDOOR);
	endtask
endclass

后门访问

  • 在进行后门访问时首先需要确保寄存器模型在建立时,是否将各个寄存器映射到了DUT一侧的HDL路径
  • 下面例码即实现了寄存器模型与DUT各个寄存器的映射:
class medf_rgm extends uvm_reg_block;
	...//寄存器成员和map声明
	virtual function build();
		...//寄存器成员和map创建
		//关联寄存器模型和HDL
		add_hdl_path("req_backdoor_access.dut");
		chn10_ctrl_reg.add_hdl_path_slice($sformatf("regs[%0d]",`SLV0_RW_REG), 0, 32);
		chn11_ctrl_reg.add_hdl_path_slice($sformatf("regs[%0d]",`SLV1_RW_REG), 0, 32);
		chn12_ctrl_reg.add_hdl_path_slice($sformatf("regs[%0d]",`SLV2_RW_REG), 0, 32);
		chn10_stat_req.add_hdl_path_slice($sformatf("regs[%0d]",`SLV0_R_REG), 0, 32);
		chn11_stat_req.add_hdl_path_slice($sformatf("regs[%0d]",`SLV1_R_REG), 0, 32);
		chn12_stat_req.add_hdl_path_slice($sformatf("regs[%0d]",`SLV2_R_REG), 0, 32);
		lock_model();
	endfunction 
endclass
  • 例码中通过uvm_reg_block::add_hdl_path()将寄存器模型关联到DUT一端,而通过uvm_reg::add_hdl_path_slice完成将寄存器模型各个寄存器成员与HDL一侧的地址映射
  • 另外,寄存器模型build()函数最后一句以lock_model()结尾,该函数的功能是结束地址映射关系并保证模型不会被其它用户修改
  • 在寄存器模型完成了HDL路径映射后,才可以利用uvm_reguvm_reg_sequence自带的方法进行后门访问,后门访问也有几类方法提供:
    • uvm_reg::read()/write()uvm_reg_sequence::read_reg()/write_reg(),在调用该方法时需要注明UVM_BACKDOOR的访问方式。
    • uvm_reg::peek()/poke()两个方法,也分别对应了读取寄存器(peek)和修改寄存器(poke)两种操作,而用户无需指定访问方式为UVM_BACKDOOR,因为这两个方法本来就只针对于后门访问

后门访问示例:

class modf_example_seq extends uvm_reg_sequence;
	mcdf_rgm rgm;
	`uvm_object_utils(mcdf_example_seq)
	`uvm_declare_p_sequencer(mcdf_bus_sequencer)
	...
	task body();
		uvn_status_e status;
		uvm_reg_data_t data;
		if(!uvm_config_db#(mcdf_rgm)::get(null, get_full_name(), "ram", rgm)) begin
				`uvm_error("GETRGM", "no top-down RGM handle is assianed")
	end
	//reqister model access write()/read()
	rgm.chnl0_ctrl_req.read(status, data, UVM_BACKDOOR, .parent(this));
	rgm.chnl0_ctrl_req.write(status, 'h11, UVM_BACKDOOR, .parent(this));
	rgm.chnl0_ctrl_req.read(status, data, UVM_BACKDOOR, .parent(this));
	//register model access poke()/peek()
	rgm.chnl1_ctrl_reg.peek(status, data, .parent(this));
	rgm.chnl1_ctrl_reg.poke(status, 'h22, .parent(this));
	rgm.chnl1_ctrl_reg.peek(status, data, .parent(this));
	//pre-defined methods read_reg()/write_reg()
	read_reg(rgm.chn12_ctrl_req, status, data, UVM_BACKDOOR);
	write_reg(rgm.chn12_ctrl_req, status, 'h22, UVM_BACKDOOR);
	read_reg(rgm.chn12_ctrl_req, status, data, UVM_BACKDOOR);
	//pre-defined methods peek_reg()/poke_reg()
	peek_reg(rgm.chnl2_ctrl_reg, status, data);
	poke_reg(rgm.chnl2_ctrl_reg, status, 'h33);
	peek_reg(rgm.chnl2_ctrl_reg, status, data);
	endtask
endclass

前门与后门的比较

总结前门访问和后门访问的主要差别:
UVM入门与进阶学习笔记17——寄存器模型(2)_第3张图片

  • 从上面的差别可以看出,后门访问较前门访问更便捷且更快,但如果只依赖后门访问也不能称之为“正道”。
  • 实际上,利用寄存器模型的前门和后门访问两种混合方式,对寄存器验证的完备性更有帮助。

前门与后门的混合应用

  • 下面给出一些实际应用的场景:
    • 通过前门访问的方式先验证寄存器访问的物理通路工作正常,并且有专门的寄存器测试的前门访问用例来遍历所有寄存器。在前门访问被验证充分的前提下,可以在后续测试中使用后门访问来节省访问多个寄存器的时间
    • 如果DUT实现了一些特殊寄存器,例如只能写一次的寄存器等,建议用物理方式去访问以保证反映真实的硬件行为。
    • 寄存器随机设置的精髓不在于随机可设置的域值,而为了考虑日常不可预期的场景,先通过后门访问随机化整个寄存器列表(在一定的随机限制下),随后再通过前门访问来配置寄存器。这么做的好处在于不再只是通过设置复位后的寄存器这种更有确定性的场景,而是通过让测试序列一开始的寄存器值都随机化来模拟无法预期的硬件配置前场景,而在稍后设置了必要的寄存器之后,再来看是否会有意想不到的边界情况发生。
    • 有时即便通过先写再读的方式测试一个寄存器,也可能存在地址不匹配的情况。譬如寄存器A地址本应该0x10,寄存器B地址应为0x20;而在硬件实现中A对应的地址位0x20,B对应0x10。像这种错误即便通过先写再读的方式也无法有效测试出来,那么不妨在通过前门配置寄存器A之后,再通过后门访问来判断HDL地址映射的寄存器A变量值是否改变,最后通过前门访问来读取寄存器A的值。上述的方式是在前门测试的基础之上又加入了中途的后门访问和数值比较,可以解决地址映射到内部错误寄存器的问题。
    • 对一些状态寄存器,有时外界的激励条件修改会依赖这些状态寄存器,并在时序上的要求也可能很严格。例如MCDF的寄存器中有一组状态寄存器表示各个channel中FIFO的余量,而channel中FIFO的余量对于激励驱动的行为也很重要。无论是前门访问还是后门访问,都可能无法第一时间反映FIFO在当前时刻的余量。因此对于需要要求更严格的测试场景,除了需要前门和后门来访问寄存器,也需要映射一些重要的信号来反映第一时间的信息。

你可能感兴趣的:(UVM入门与进阶,测试)