寄存器模型

目录

  • 寄存器模型的概念
    • 建造只有一个寄存器的寄存器模型
    • 寄存器模型的集成
    • 寄存器模型的使用
  • 后门访问与前门访问
  • 复杂的寄存器模型
    • 层次化的寄存器模型
    • reg_file
    • 存储器
  • 寄存器模型对DUT的模拟
    • 期望值与镜像值
    • 常用操作
  • 其他用法
    • reg_predictor
    • mirror操作

寄存器模型的概念

寄存器配置总线:通过控制端口,配置DUT中的寄存器,DUT可以根据寄存器的值来改变其行为。

uvm_reg_field:寄存器模型中最小的单位是具体存储寄存器数值的变量。
uvm_reg:比uvm_reg_field高一个级别,但依然是比较小的单位。下图为uvm_reg_field与uvm_reg_的关系:
寄存器模型_第1张图片

uvm_reg_block:一个比较大的单位,在其中可以加入许多的uvm_reg,也可以加入其他的uvm_reg_block,一个寄存器模型中至少包含一个uvm_reg_block。
uvm_reg_map:每个寄存器在加入寄存器模型时都有其地址,uvm_reg_map就是存储这些地址,并将其转换成可以访问的物理地址。

建造只有一个寄存器的寄存器模型

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");
		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");
		super.new(name, 16, UVM_NO_COVERAGE);
	endfunction
endclass
  • new函数中的位数一般与系统总线的宽度一致,另外一个参数为是否要加入覆盖率的支持,这里选择不支持。
  • 每一个派生自uvm_reg的类都有一个build,所有的uvm_reg_field都在这里实例化,当reg_data实例化后,要调用reg_data.configure函数来配置这个字段。
    定义好这个寄存器后,需要在一个由reg_block派生的类中将其实例化:
class reg_model enxtends uvm_reg_block;
	rand reg_invert invert;
	virtual function void build();
		default_map = create_map("default_map", 0, 2, UVM_BIG_ENDIAN, 0);
		invert = reg_invert::type_id::create("invert",  , get_full_name());
		invert.configure(this, null, " ");
		invert.build();
		default_map.add_reg(invert, 'h9', "RW");
	endfucntion
	`uvm_object_utils(reg_model)
	function new(input string name = "reg_model");
		super.new(name, UVM_NO_COVERAGE);
	endfunction
endclass

同reg_reg派生的类一样,每一个由uvm_reg_block派生的类也要定义一个build函数,一般在此函数中实现所有寄存器的例化。

  • 一个uvm_reg_block中一定要对应一个uvm_reg_map,通过调用uvm_reg_block的create_map在build中将其实例化。create_map的参数中,第一个参数是名字,第二个参数是基地址,第三个参数是系统总线的宽度(byte),第四个参数是大小端,最后一个参数表示是否能够按照byte进行寻址。
  • 随后实例化invert并调用invert.configure函数。这个函数的主要功能是指定寄存器进行后门访问操作时的路径。第一个参数是此寄存器所在uvm_reg_block指针,这里填写this;第二个参数是reg_file的指针;第三个参数是此寄存器的后门访问路径,这里暂且为空。当调用完configure时,需要手动调用invert的build函数,将invert中的域实例化。
  • 最后一步是将此寄存器加入default_map中,uvm_reg_map的作用是存储所有寄存器的地址,因此必须将实例化的寄存器加入default_map中,否则无法进行前门访问操作。add_reg函数的第一个参数是要加入的寄存器,第二个参数是寄存器的地址,第三个参数是此寄存器的存取方式。

寄存器模型的集成

寄存器模型的前门访问操作可以分成读和写两种。无论是读或写,寄存器模型都会通过sequence产生一个uvm_reg_bus_op的变量,此变量中存储着操作类型(读还是写)和操作的地址,如果是写操作,还会有要写入的数据。此变量中的信息要经过一个转换器(adapter)转换后交给bus_sequencer,随后交给bus_driver,由bus_driver实现最终的前门访问读写操作。因此,必须要定义一个adapter。
寄存器模型_第2张图片

在adapter中需要定义两个函数:

  • reg2bus:将寄存器模型通过sequence发出的uvm_reg_bus_op型的变量转换成bus_sequencer能够接受的形式。
  • bus2reg:当监测到总线上有操作时,将收集来的transaction转换成寄存器模型能够接受的形式。
    定义好adapter后,在base_test中加入寄存器模型:
class base_test extends uvm_test;
	my_env 			env;
	my_vsqr 		v_srq;
	reg_model 		rm;
	my_adapter 		reg_sqr_adapter;
...
endclass
function void base_test::build_phase(uvm_phase phase);
	super.build_phase(phase);
	env = my_env::type_id::create("env", this);
	v_sqr = my_vsqr::typr_id::create("v_sqr", this);
	rm = reg_model::typr_id::create("rm", this);
	rm.configure(null, " ");
	rm.build();
	rm.lock_build();
	rm.reset();
	reg_sqr_adapter = new("reg_sqr_adapter");
	env.p_rm = this.rm;
endfunction

function void base_test::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);
	rm.default_map.set_auto_predoct(1);
endfunction

要将一个寄存器模型集成到base_test中,至少需要在base_test中定义reg_model和reg_sqr_adapter。将所有用到的类在build_phase中例化,在例化后reg_model还要做四件事:

  1. 调用configure函数,第一个参数是parent block,由于是最顶层的reg_block,因此填null;第二个参数是后门访问路径,这里传入一个空的字符串。
  2. 调用build函数,将所有的寄存器实例化。
  3. 调用lock_model函数,调用此函数后,reg_model中就不能再加入新的寄存器了。
  4. 调用reset函数,如果不调用此函数,那么reg_model中所有寄存器的值都是0。调用此函数后,所有寄存器的值都将变为设置的复位值。
    寄存器模型的前门访问操作最终都将由uvm_reg_map完成,因此在connect_phase中,需要将adapter和bus_sequencer通过set_sequencer函数告知reg_model的default_map,并将default_map设置为自动预测状态。

寄存器模型的使用

寄存器模型提供了read和write两个task,对于read:

p_rm.invert.read(status, value, UVM_FRONTDOOR);

read的第一个参数为uvm_status_e型的变量,作为一个输出,其用于表明读操作是否成功;第二个参数是读取的数值;第三个是读取的方式,可选UVM_FRONTDOOR和UVM_BACKDOOR。
对于write:

p_sequencer.p_rm.invert.write(status, 1, UVM_FRONTDOOR);

第一个参数也是uvm_status_e型的变量,用于表明写操作是否成9功;第二个参数是要写的值;第三个参数是写操作的方式,同样可选UVM_FRONTDOOR和UVM_BACKDOOR。
寄存器模型对sequence的transaction类型没有任何要求,所以可以在一个发送my_transaction的sequence中使用寄存器模型对寄存器进行读写操作。

后门访问与前门访问

  • 前门访问:通过寄存器配置总线来对DUT进行操作。在这个过程中,仿真时间是一直往前走的。
    通过adapter的bus2reg及reg2bus,可以实现uvm_reg_item与目标transaction的转换,以读操作为例,完整的流程为:
  1. 参考模型调用寄存器模型的读任务。
  2. 寄存器模型产生sequence,并产生uvm_reg_item:rw。
  3. 产生driver能够接受的transaction:bus_req=adapter.reg2bus(rw)。
  4. 把bus_req交给bus_sequencer。
  5. driver得到bus_req后驱动它,得到读取的值,并将读取值放入bus_req中,调用item_done。
  6. 寄存器模型调用adapter.bus2reg(bus_req,rw)将bus_req中的读取值传递给rw。
  7. 将rw中的读数据返回参考模型。
  • 后门访问:不通过总线进行读写操作,而是直接通过层次化的引用来改变寄存器的值。所有的后门访问都是不消耗仿真时间而只消耗运行时间的。

    可以使用interface以及DPI+VPI的方式来进行后门访问。
    UVM中使用DPI+VPI的方式来进行后门访问操作,它大体的流程是:
    1)在建立寄存器模型时将路径参数设置好。
    2)在进行后门访问的写操作时,寄存器模型调用uvm_hdl_deposit函数。
    3)进行后门访问的读操作时,调用uvm_hdl_read函数,在C/C++侧,此函数内部会调用vpi_get_value函数来对DUT中的寄存器进行读操作,并将读取值返回。

    在使用寄存器模型的后门访问功能时,需要做如下准备:
    (1)在reg_block中调用uvm_reg的configure函数时,需要设置好第三个参数:

class reg_model extends uvm_reg_block;
	rand reg_invert invert;
	rand reg_counter_high counter_high;
	rand reg_counter_low counter_low;
	virtual function void build();
		invert.configure(this, null, "invert");
		counter_high.configure(this, null, "counter[31:16]");
		counter_low.configure(this, null, "counter[15:0]");
	endfunction
endclass

(2)在将寄存器模型集成到验证平台时,需要设置好根路径hdl_root:

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();
	rm.set_hdl_path_root("top_tb.my_dut");
...
endfunction

UVM会提供两类后门访问的函数,一类是read和write,一类是peek和poke,区别在于:第一类在进行操作时会模仿DUT的行为,第二类则完全不管DUT的行为。例如,对一个只读寄存器进行写操作,第一类由于要模拟DUT的只读行为所以写不进去,这时就可以用第二类去写。

p_sequencer.p_rm.counter_low.poke(status, 16'hFFFD);
p_sequencer.p_rm.counter_low.peek(status, value);

poke和peek的第一个参数表示操作是否成功,第二个参数表示读写的数据。

复杂的寄存器模型

层次化的寄存器模型

一般只会在第一级的uvm_reg_block中加入寄存器,而第二级的uvm_reg_block通常只添加uvm_reg_block,这样从整体上就能呈现出如下图中比较清晰的结构。
寄存器模型_第3张图片
例如,一个DUT分了三个子模块:用于控制全局的global模块、用于缓存数据的buf模块、用于接收发送以太网帧的mac模块。global模块寄存器的地址为0x0000~0x0FFF,buf部分的寄存器地址为0x1000~0x1FFF,mac部分的寄存器地址为0x2000~0x2FFF,那么可以按照如下方式定义寄存器模型:

class reg_model extends uvm_reg_block;

	rand global_blk gb_ins;
	rand buf_blk bb_ins;
	rand mac_blk mb_ins;
	virtual function void build();
		default_map = create_map("default_map", 0, 2, UVM_BIG_ENDIAN, 0);
		gb_ins = global_blk::type_id::create("gb_ins");
		gb_ins.configure(this, "");
		gb_ins.build();
		gb_ins.lock_model();
		default_map.add_submap(gb_ins.default_map, 16'h0);

		bb_ins = buf_blk::type_id::create("bb_ins");
		bb_ins.configure(this, "");
		bb_ins.build();
		bb_ins.lock_model();
		default_map.add_submap(bb_ins.default_map, 16'h1000);

		mb_ins = mac_blk::type_id::create("mb_ins");
		mb_ins.configure(this, "");
		mb_ins.build();
		mb_ins.lock_model();
		default_map.add_submap(mb_ins.default_map, 16'h2000);

	endfunction

	`uvm_object_utils(reg_model)

	function new(input string name="reg_model");
		super.new(name, UVM_NO_COVERAGE);
	endfunction

endclass

要将一个子reg_block加入父reg_block中,第一步是先实例化子reg_block。第二步是调用子reg_block的configure函数。第三步是调用子reg_block的build函数。第四步是调用子reg_block的lock_model函数。第五步则是将子reg_block的default_map以子map的形式加入父reg_block的default_map中。

reg_file

uvm_reg_file的引入主要是为了区分不同的hdl路径。

class regfile extends uvm_reg_file;
	function new(string name = "regfile");
		super.new(name);
	endfunction

	`uvm_object_utils(regfile)
endclass

class mac_blk extends uvm_reg_block;

	rand regfile file_a;
	rand regfile file_b;
	rand reg_regA regA;
	rand reg_regB regB;
	rand reg_vlan vlan;

	virtual function void build();
		default_map = create_map("default_map", 0, 2, UVM_BIG_ENDIAN, 0);
		file_a = regfile::type_id::create("file_a", , get_full_name());
		file_a.configure(this, null, "fileA");
		file_b = regfile::type_id::create("file_b", , get_full_name());
		file_b.configure(this, null, "fileB");
		regA.configure(this, file_a, "regA");
		regB.configure(this, file_b, "regB");
	endfunction
endclass

如上所示,先从uvm_reg_file派生一个类,然后在mac_blk中实例化此类,之后调用其configure函数,此函数的第一个参数是其所在的reg_block的指针;第二个参数是假设此reg_file是另外一个reg_file的父文件,那么这里就填写其父reg_file的指针(由于这里
只有这一级reg_file,因此填写null);第三个参数则是此reg_file的hdl路径。
当把reg_file定义好后,在调用寄存器的configure参数时,就可以将其第二个参数设为reg_file的指针。

存储器

在寄存器模型中加入存储器的代码如下:

class my_memory extends uvm_mem;
	function new(string name="my_memory");
		super.new(name, 1024, 16);
	endfunction

	`uvm_object_utils(my_memory)
endclass

class reg_model extends uvm_reg_block;
…
	rand my_memory mm;

	virtual function void build();
…
		mm = my_memory::type_id::create("mm", , get_full_name());
		mm.configure(this, "stat_blk.ram1024x16_inst.array");
		default_map.add_mem(mm, 'h100);
	endfunction
…
endclass
  • 首先由uvm_mem派生一个类my_memory,在其new函数中调用super.new函数。这个函数有三个参数,第一个是名字,第二个是存储器的深度,第三个是宽度。
  • 然后在reg_model的build函数中,将存储器实例化,调用其configure函数,第一个参数是所在reg_block的指针,第二个参数是此块存储器的hdl路径。
  • 最后调用default_map.add_mem函数,将此块存储器加入default_map中,从而可以对其进行前门访问操作。如果没有对此块存储器分配地址空间,那么这里可以不将其加入default_map中。在这种情况下,只能使用后门访问的方式对其进行访问。

寄存器模型对DUT的模拟

期望值与镜像值

  • 镜像值(mirrored value):寄存器模型中的一个专门的变量,用于最大可能地与DUT保持同步。
  • 期望值(desired value):除了DUT的镜像值外,寄存器模型中还有期望值。如目前DUT中invert的值为’h0,寄存器模型中的镜像值也为’h0,但是希望向此寄存器中写入一个’h1,此时’h1便是期望值。
    一种方法是直接调用write任务,将’h1写入,期望值与镜像值都更新为’h1;另外一种方法是通过set函数将期望值设置为’h1(此时镜像值依然为0),之后调用update任务,update任务会检查期望值和镜像值是否一致,如果不一致,那么将会把期望值写入DUT中,并且更新镜像值。
  • 通过get函数可以得到寄存器的期望值,通过get_mirrored_value可以得到其镜像值。
value = p_sequencer.p_rm.invert.get();
value = p_sequencer.p_rm.invert.get_mirrored_value();

常用操作

  • read&write操作:无论通过后门访问还是前门访问的方式从DUT中读取或写入寄存器的值,在操作完成后,寄存器模型都会根据读写的结果更新期望值和镜像值(二者相等)。
  • peek&poke操作:在操作完成后,寄存器模型会根据操作的结果更新期望值和镜像值(二者相等)。
  • get&set操作:set操作会更新期望值,但是镜像值不会改变。get操作会返回寄存器模型中当前寄存器的期望值。
  • update操作:这个操作会检查寄存器的期望值和镜像值是否一致,如果不一致,那么就会将期望值写入DUT中,并且更新镜像值,使其与期望值一致。
  • randomize操作:寄存器模型提供randomize接口。randomize之后,期望值将会变为随机出的数值,镜像值不会改变。但是并不是寄存器模型中所有寄存器都支持此函数。如果不支持,则randomize调用后其期望值不变。

其他用法

reg_predictor

寄存器模型_第4张图片

除了像上图中左边一样使用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);
	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);
	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时,则上图中的左边和右边是完全等价的。如果有多个主设备,则左边会漏掉某些transaction。

mirror操作

UVM提供mirror操作,用于读取DUT中寄存器的值并将它们更新到寄存器模型中。
其有两种应用场景,一是在仿真中不断地调用它,使得到整个寄存器模型的值与DUT中寄存器的值保持一致,此时check选项是关闭的。二是在仿真即将结束时,检查DUT中寄存器的值与寄存器模型中寄存器的镜像值是否一致,这种情况下,check选项是打开的。

(mirror更新的是寄存器模型,update更新的是DUT。)

你可能感兴趣的:(UVM验证方法学,fpga开发,单片机,嵌入式硬件)