精华内容
下载资源
问答
  • UVM寄存器模型

    2021-02-17 16:58:40
    为什么需要UVM寄存器模型 @啥是知乎–总结UVM中的寄存器模型 真实DUT里的任何IP都会有寄存器,这些IP都会有一组配置总线,通过配置总线配置IP里的寄存器来更改IP的行为。 若没有寄存器模型,怎么访问寄存器? 假设...

    为什么需要UVM寄存器模型

    @啥是知乎–总结UVM中的寄存器模型
    真实DUT里的任何IP都会有寄存器,这些IP都会有一组配置总线,通过配置总线配置IP里的寄存器来更改IP的行为。

    若没有寄存器模型,怎么访问寄存器?

    假设我们在reference model里需要读一个寄存器的值。
    第一,我们需要在reference model里启动一个sequence,这个sequence会发送一个transaction给bus driver,把寄存器的读出来。
    第二,我们需要把读出来的值传给reference model。
    虽然只有两步,但如何在reference model里启动一个sequence,又如何把拿到的寄存器的值传给reference model都很难解决。
    所以为了解决这个问题,UVM引进了寄存器模型。直接使用寄存器模型内置的函数完成寄存器的访问,像启动sequence及将结果返回这些事情,都会由寄存器模型来自动完成。
    在这里插入图片描述
    因此,UVM寄存器模型的本质就是重新定义了验证平台于DUT的寄存器接口,简化寄存器访问的流程

    如何定义一个最简单的寄存器模型(只有一个寄存器域的寄存器)

    1.从uvm_reg类里派生出一个寄存器类

    ①定义②注册③域的整理④new⑤build(创建、配置域)

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

    2.从uvm_reg_block里派生一个寄存器组(block)类

    ①定义②注册③寄存器的整理(uvm_reg、map)④new⑤build(创建、配置uvm_reg、build、添加、lock)

    class mcdf_rgm extend uvm_reg_block;
    	`uvm_object_utils(mcdf_rgm)
    	rand ctrl_reg chnl0_ctrl_reg;
    	uvm_reg_map map;
    	function new(string name == "mcdf_rgm");
    		super.new(name,UVM_NO_COVERAGE);
    	endfunction
    	virtual function build();
    		chnl0_ctrl_reg == ctrl_reg::type_id::create("chnl0_ctrl_reg");
    		chnl0_ctrl_reg.configure(this);
    		chnl0_ctrl_reg.build();
    		map == create_map("map", 'h0, 4, UVM_LITTLE_ENDIAN);
    		//name,offset, number of bytes, endianess
    		map.add_reg(chnl0_ctrl_reg, 32'h00000000, "RW");
    		lock.model();
    	endfunction
    endclass 
    

    寄存器模型的集成

    利用adapter实现寄存器模型与总线UVC的桥接(即完成寄存器类型的transaction和验证平台类型transaction的转换工作)
    在这里插入图片描述

    adapter实现两个函数

    class reg2mcdf_adapter extends uvm_reg_adapter;
    	`uvm_object_utils(reg2mcdf_adapter)
    	function new(string name == "reg2mcdf_adapter");
    		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;
    	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.addr == t.addr;
    		rw.data == (t.cmd == `WRITE) ? t.wdata : t.rdata;
    		rw.status == UVM_IS_OK;
    	endfunction
    endclass
    

    adapter的集成

    集成的步骤: 先实例化寄存器组类,然后依次调用configure,build,lock_model,reset函数完成配置,最后在connect_phase里将寄存器模型,转换器和sequencer连接起来。这样集成工作就做好了。

    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","create rgm instance locally",UVM_LOW)
    		end
    		rgm.build();
    		rgm.map.set_auto_predict();
    		reg2mcdf = reg2mcdf_adapter::type_id::create("reg2mcdf");
    	endfunction
    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
    

    完成adapter的集成后就可以通过寄存器模型完成寄存器的访问了。
    寄存器模型提供了两个基本任务,read和write
    在不同的平台组件里直接用寄存器模型指针调用read或write任务按照指定格式即可完成寄存器的访问。

    展开全文
  • 模块使用python实现excel到寄存器模型文件的转换功能。使用前在Windows平台需要安装python3.x 以及openpyxl模块,使用时将需要转换的模块寄存器按照examp.xlsx格式填好excel,excel文件名设置为寄存器名字,将excel...
  • UVM中的寄存器模型

    2020-10-15 15:45:03
    寄存器模型简介 1. 通常来说,DUT中会有一组控制端口,通过控制端口,可以配置DUT中的寄存器,DUT可以根据寄存器的值来改变其行为。这组控制端口就是寄存器配置总线。 在没有寄存器模型之前,只能启动sequence通过...

    寄存器模型简介

    1.

    通常来说,DUT中会有一组控制端口,通过控制端口,可以配置DUT中的寄存器,DUT可以根据寄存器的值来改变其行为。这组控制端口就是寄存器配置总线。

    在没有寄存器模型之前,只能启动sequence通过前门(FRONTDOOR)访问的方式来读取寄存器,局限较大,在
    scoreboard(或者其他component)中难以控制。而有了寄存器模型之后,scoreboard只与寄存器模型打交道,无论是发送读的指令还是获取读操作的返回值,都可以由寄存器模型完成。有了寄存器模型后,可以在任何耗费时间的phase中使用寄存器模型以前门访问或后门(BACKDOOR)访问的方式来读取寄存器的值,同时还能在某些不耗费时间的phase(如check_phase)中使用后门访问的方式来读取寄存器的值。

    前门访问与后门访问是两种寄存器的访问方式。所谓前门访问,指的是通过模拟cpu在总线上发出读指令,进行读写操作。在这个过程中,仿真时间($time函数得到的时间)是一直往前走的。

    UVM寄存器模型的本质就是重新定义了验证平台与DUT的寄存器接口,使验证人员更好地组织及配置寄存器,简化流程、减少工作量。

    2.

    • uvm_reg_field:这是寄存器模型中的最小单位。

    • uvm_reg:它比uvm_reg_field高一个级别,但是依然是比较小的单位。一个寄存器中至少包含一个uvm_reg_field。

    • uvm_reg_block:它是一个比较大的单位,在其中可以加入许多的uvm_reg,也可以加入其他的uvm_reg_block。一个寄存器模型中至少包含一个uvm_reg_block。

    • uvm_reg_map:每个寄存器在加入寄存器模型时都有其地址,uvm_reg_map就是存储这些地址,并将其转换成可以访问的物理地址。

    • 当寄存器模型使用前门访问方式来实现读或写操作时,uvm_reg_map就会将地址转换成绝对地址,启动一个读或写的sequence,并将读或写的结果返回。在每个reg_block内部,至少有一个(通常也只有一个)uvm_reg_map。

    简单的寄存器模型

    1.

    假设DUT非常简单,它只有一个寄存器invert。要为其建造寄存器模型,首先要从uvm_reg派生一个invert类:

    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,
    		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
    

    在new函数中,要将invert寄存器的宽度作为参数传递给super.new函数。这里的宽度并不是指这个寄存器的有效宽度,而是指这个寄存器中总共的位数。如对于一个16位的寄存器,其中可能只使用了8位,那么这里要填写的是16,而不是8。这个数字一般与系统总线的宽度一致。super.new中另外一个参数是是否要加入覆盖率的支持,这里选择UVM_NO_COVERAGE,即不支持。

    每一个派生自uvm_reg的类都有一个build,这个build与uvm_component的build_phase并不一样,它不会自动执行,而需要手工调用,与build_phase相似的是所有的uvm_reg_field都在这里实例化。当reg_data实例化后,要调用data.configure函数来配置这个字段。

    configure的第一个参数就是此域(uvm_reg_field)的父辈,也即此域位于哪个寄存器中,这里当然是填写this了。
    第二个参数是此域的宽度,由于DUT中invert的宽度为1,所以这里为1。

    第三个参数是此域的最低位在整个寄存器中的位置,从0开始计数。假如一个寄存器如图所示,其低3位和高5位没有使用,其中只有一个字段,此字段的有效宽度为8位,那么在调用configure时,第二个参数就要填写8,第三个参数则要填写3,因为此reg_field是从第4位开始的。
    在这里插入图片描述
    第四个参数表示此字段的存取方式。UVM共支持如下25种存取方式:
    1)RO:读写此域都无影响。
    2)RW:会尽量写入,读取时对此域无影响。
    3)RC:写入时无影响,读取时会清零。
    4)RS:写入时无影响,读取时会设置所有的位。
    5)WRC:尽量写入,读取时会清零。
    6)WRS:尽量写入,读取时会设置所有的位。

    7)WC:写入时会清零,读取时无影响。
    8)WS:写入时会设置所有的位,读取时无影响。
    9)WSRC:写入时会设置所有的位,读取时会清零。
    10)WCRS:写入时会清零,读取时会设置所有的位。
    11)W1C:写1清零,写0时无影响,读取时无影响。
    12)W1S:写1设置所有的位,写0时无影响,读取时无影响。
    13)W1T:写1入时会翻转,写0时无影响,读取时无影响。
    14)W0C:写0清零,写1时无影响,读取时无影响。
    15)W0S:写0设置所有的位,写1时无影响,读取时无影响。
    16)W0T:写0入时会翻转,写1时无影响,读取时无影响。
    17)W1SRC:写1设置所有的位,写0时无影响,读清零。
    18)W1CRS:写1清零,写0时无影响,读设置所有位。

    19)W0SRC:写0设置所有的位,写1时无影响,读清零。
    20)W0CRS:写0清零,写1时无影响,读设置所有位。
    21)WO:尽可能写入,读取时会出错。
    22)WOC:写入时清零,读取时出错。
    23)WOS:写入时设置所有位,读取时会出错。
    24)W1:在复位(reset)后,第一次会尽量写入,其他写入无影响,读取时无影响。
    25)WO1:在复位后,第一次会尽量写入,其他的写入无影响,读取时会出错。

    第五个参数表示是否是易失的(volatile),这个参数一般不会使用。
    第六个参数表示此域上电复位后的默认值。
    第七个参数表示此域是否有复位,一般的寄存器或者寄存器的域都有上电复位值,因此这里一般也填写1。
    第八个参数表示这个域是否可以随机化。这主要用于对寄存器进行随机写测试,如果选择了0,那么此域将不会随机化,而一直是复位值,否则将会随机出一个数值来。这一个参数当且仅当第四个参数为RW、WRC、WRS、WO、W1、WO1时才有效。

    第九个参数表示这个域是否可以单独存取。
    定义好此寄存器后,需要在一个由reg_block派生的类中将其实例化:

    class reg_model extends 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");
    	endfunction
    `uvm_object_utils(reg_model)
    function new(input string name="reg_model");
    super.new(name, UVM_NO_COVERAGE);
    endfunction
    endclass
    

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

    一个uvm_reg_block中一定要对应一个uvm_reg_map,系统已经有一个声明好的default_map,只需要在build中将其实例化。这个实例化的过程并不是直接调用uvm_reg_map的new函数,而是通过调用uvm_reg_blockcreate_map来实现。create_map有众多的参数:

    其中第一个参数是名字,第二个参数是基地址,第三个参数则是系统总线的宽度,这里的单位是byte而不是bit,第四个参数是大小端,最后一个参数表示是否能够按照byte寻址。

    随后实例化invert并调用invert.configure函数。这个函数的主要功能是指定寄存器进行后门访问操作时的路径。其第一个参数是此寄存器所在uvm_reg_block的指针,这里填写this,第二个参数是reg_file的指针,这里暂时填写null,第三个参数是此寄存器的后门访问路径,这里暂且为空。当调用完configure时,需要手动调用invert的build函数,将invert中的域实例化。

    最后一步则是将此寄存器加入default_map中。uvm_reg_map的作用是存储所有寄存器的地址,因此必须将实例化的寄存器加入default_map中,否则无法进行前门访问操作。

    add_reg函数的第一个参数是要加入的寄存器,第二个参数是寄存器的地址,这里是16’h9,第三个参数是此寄存器的存取方式。

    到此为止,一个简单的寄存器模型已经完成。下面总结一下:

    • uvm_reg_field是最小的单位,是具体存储寄存器数值的变量,可以直接用这个类。

    • uvm_reg则是一个“空壳子”,或者用专业名词来说,它是一个纯虚类,因此是不能直接使用的,必须由其派生一个新类,在这个新类中至少加入一个uvm_reg_field,然后这个新类才可以使用。

    • uvm_reg_block则是用于组织大量uvm_reg的一个大容器。

    打个比方说,uvm_reg是一个小瓶子,其中必须装上药丸(uvm_reg_field)才有意义,这个装药丸的过程就是定义派生类的过程,而uvm_reg_block则是一个大箱子,它中可以放许多小瓶子(uvm_reg),也可以放其他稍微小一点的箱子(uvm_reg_block)。整个寄存器模型就是一个大箱子(uvm_reg_block)。

    2.

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

    因此,必须要定义好一个转换器。如下例为一个简单的转换器的代码:

    class my_adapter extends uvm_reg_adapter;
    	string tID = get_type_name();
    	`uvm_object_utils(my_adapter)
    	function new(string name="my_adapter");
    		super.new(name);
    	endfunction : new
    	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 action")
    			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 : my_adapter
    

    一个转换器要定义好两个函数,一是reg2bus,其作用为将寄存器模型通过sequence发出的uvm_reg_bus_op型的变量转换成bus_sequencer能够接受的形式,二是bus2reg,其作用为当监测到总线上有操作时,它将收集来的transaction转换成寄存器模型能够接受的形式,以便寄存器模型能够更新相应的寄存器的值。

    在读操作时,由于总线的特殊性,bus_driver在驱动总线进行读操作时,它也能顺便获取要读的数值,如果它将此值放入从bus_sequencer获得的bus_transaction中时,那么bus_transaction中就会有读取的值,此值经过adapter的bus2reg函数的传递,最终被寄存器模型获取。由于并没有实际的transaction的传递,所以从driver到adapter使用了虚线。

    转换器写好之后,就可以在base_test中加入寄存器模型了:

    class base_test extends uvm_test;
    	my_env env;
    	my_vsqr v_sqr;
    	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::type_id::create("v_sqr", this);
    	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");
    	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_predict(1);
    endfunction
    

    要将一个寄存器模型集成到base_test中,那么至少需要在base_test中定义两个成员变量,一是reg_model,另外一个就是reg_sqr_adapter。将所有用到的类在build_phase中实例化。

    在实例化后reg_model还要做四件事:

    • 第一是调用configure函数,其第一个参数是parent block,由于是最顶层的reg_block,因此填写null,第二个参数是后门访问路径,这里传入一个空的字符串。

    • 第二是调用build函数,将所有的寄存器实例化。

    • 第三是调用lock_model函数,调用此函数后,reg_model中就不能再加入新的寄存器了。

    • 第四是调用reset函数,如果不调用此函数,那么reg_model中所有寄存器的值都是0,调用此函数后,所有寄存器的值都将变为设置的复位值。

    寄存器模型的前门访问操作最终都将由uvm_reg_map完成,因此在connect_phase中,需要将转换器和bus_sequencer通过set_sequencer函数告知reg_model的default_map,并将default_map设置为自动预测状态。

    3.

    当一个寄存器模型被建立好后,可以在sequence和其他component中使用。以在参考模型中使用为例,需要在参考模型中有一个寄存器模型的指针:

    class my_model extends uvm_component;
    	reg_model p_rm;
    endclass
    

    此前已经为env的p_rm赋值,因此只需要在env中将p_rm传递给参考模型即可:

    function void my_env::connect_phase(uvm_phase phase);
    	mdl.p_rm = this.p_rm;
    endfunction
    

    对于寄存器,寄存器模型提供了两个基本的任务:read和write。若要在参考模型中读取寄存器,使用read任务:

    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:", UV M_LOW)
    		//new_tr.print();
    		if(value)
    		invert_tr(new_tr);
    		ap.write(new_tr);
    	end
    endtask
    

    read任务的原型如下所示:

    extern virtual task read(output uvm_status_e status,
    	output uvm_reg_data_t value,
    	input uvm_path_e path = UVM_DEFAULT_PATH,
    	input uvm_reg_map map = null,
    	input uvm_sequence_base parent = null,
    	input int prior = -1,
    	input uvm_object extension = null,
    	input string fname = "",
    	input int lineno = 0);
    

    它有多个参数,常用的是其前三个参数。其中第一个是uvm_status_e型的变量,这是一个输出,用于表明读操作是否成功;第二个是读取的数值,也是一个输出;第三个是读取的方式,可选UVM_FRONTDOOR和UVM_BACKDOOR。

    由于参考模型一般不会写寄存器,因此对于write任务,以在virtual sequence进行写操作为例说明。在sequence中使用寄存器模型,通常通过p_sequencer的形式引用。需要首先在sequencer中有一个寄存器模型的指针,之前的base_test已经为v_sqr.p_rm赋值了。因此可以直接以如下方式进行写操作:

    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
    

    write任务的原型为:

    extern virtual task write(output uvm_status_e status,
    	input uvm_reg_data_t value,
    	input uvm_path_e path = UVM_DEFAULT_PATH,
    	input uvm_reg_map map = null,
    	input uvm_sequence_base parent = null,
    	input int prior = -1,
    	input uvm_object extension = null,
    	input string fname = "",
    	input int lineno = 0);
    

    它的参数也有很多个,但是与read类似,常用的也只有前三个。其中第一个为uvm_status_e型的变量,这是一个输出,用于表明写操作是否成功。第二个要写的值,是一个输入,第三个是写操作的方式,可选UVM_FRONTDOOR和UVM_BACKDOOR。

    寄存器模型对sequence的transaction类型没有任何要求。因此,可以在一个发送my_transaction的sequence中使用寄存器模型对寄存器进行读写操作。

    后门访问与前门访问

    1.

    所谓前门访问操作就是通过寄存器配置总线(如APB协议、OCP协议、I2C协议等)来对DUT进行操作。无论在任何总线协议中,前门访问操作只有两种:读操作和写操作。

    如果能够在参考模型中得到一个sequencer的指针,那么可以在此sequencer上启动一个sequence。这通常比较容易实现,只要在其中设置一个p_sqr的变量,并在env中将sequencer的指针赋值给此变量即可。

    接下来的关键就是分别写一个读写的sequence:

    class reg_access_sequence extends uvm_sequence#(bus_transaction);
    	string tID = get_type_name();
    	bit[15:0] addr;
    	bit[15:0] rdata;
    	bit[15:0] wdata;
        bit is_wr;
    	virtual task body();
    		bus_transaction tr;
    		tr = new("tr");
    		tr.addr = this.addr;
    		tr.wr_data = this.wdata;
    		tr.bus_op = (is_wr BUS_WR : BUS_RD);
    		`uvm_info(tID, $sformatf("begin to access register: is_wr = %0d, addr = %0h", is_wr, addr),
    		`uvm_send(tr)
    		`uvm_info(tID, "successfull access register", UVM_MEDIUM)
    		this.rdata = tr.rd_data;
    	endtask
    endclass
    

    之后,在参考模型中使用如下的方式来进行读操作:

    task my_model::main_phase(uvm_phase phase);
    	reg_access_sequence reg_seq;
    	super.main_phase(phase);
    	reg_seq = new("reg_seq");
    	reg_seq.addr = 16'h9;
    	reg_seq.is_wr = 0;
    	reg_seq.start(p_sqr);
    	while(1) begin
    		if(reg_seq.rdata)
                invert_tr(new_tr);
    		ap.write(new_tr);
    	end
    endtask
    

    2.

    UVM内建了一种transaction:uvm_reg_item。通过adapter的bus2reg及reg2bus,可以实现uvm_reg_item与目标transaction的转换。以读操作为例,其完整的流程为:

    • 参考模型调用寄存器模型的读任务。

    • 寄存器模型产生sequence,并产生uvm_reg_item:。

    • 产生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中的读数据返回参考模型。

    3.

    之前介绍sequence的应答机制时提到过,如果driver一直发送应答而sequence不收集应答,那么将会导致sequencer的应答队列溢出。UVM考虑到这种情况,在adapter中设置了provide_responses选项:

    virtual class uvm_reg_adapter extends uvm_object;
    	…
    	bit provides_responses;
    	…
    endclass
    

    在设置了此选项后,寄存器模型在调用bus2reg将目标transaction转换成uvm_reg_item时,其传入的参数是rsp,而不是req。使用应答机制的操作流程为:

    • 参考模型调用寄存器模型的读任务。

    • 寄存器模型产生sequence,并产生uvm_reg_item:rw。

    • 产生driver能够接受的transaction:bus_req=adapter.reg2bus(rw)。

    • 将bus_req交给bus_sequencer。

    • driver得到bus_req,驱动它,得到读取的值,并将读取值放入rsp中,调用item_done。

    • 寄存器模型调用adapter.bus2reg(rsp,rw)将rsp中的读取值传递给rw。

    • 将rw中的读数据返回参考模型。

    4.

    后门访问是与前门访问相对的操作,从广义上来说,所有不通过DUT的总线而对DUT内部的寄存器或者存储器进行存取的操作都是后门访问操作。如在top_tb中可以使用如下方式对counter赋初值:

    initial begin
        @(posedge rst_n);
    	my_dut.counter = 32'hFFFD;
    end
    

    如果想在driver或monitor中使用后门访问,一种方法是使用接口。可以新建一个后门interface:

    interface backdoor_if(input clk, input rst_n);
    	function void poke_counter(input bit[31:0] value);
    		top_tb.my_dut.counter = value;
    	endfunction
    	function void peek_counter(output bit[31:0] value);
    		value = top_tb.my_dut.counter;
    	endfunction
    endinterface
    

    poke_counter为后门写,而peek_counter为后门读。在测试用例(或者drvier、scoreboard)中,若要对寄存器赋初值可以直接调用此函数:

    task my_case0::configure_phase(uvm_phase phase);
    	phase.raise_objection(this);
    	@(posedge vif.rst_n);
    	vif.poke_counter(32'hFFFD);
    	phase.drop_objection(this);
    endtask
    

    这种方式在实际中是有应用的,它适用于不想使用寄存器模型提供的后门访问或者根本不想建立寄存器模型,同时又必须要对DUT中的一个寄存器或一块存储器(memory)进行后门访问操作的情况。

    5.

    在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
    

    由于counter是32bit,占据两个地址,因此在寄存器模型中它是作为两个寄存器存在的。

    当上述工作完成后,在将寄存器模型集成到验证平台时,需要设置好根路径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
    

    无论是peek还是poke,其常用的参数都是前两个。各自的第一个参数表示操作是否成功,第二个参数表示读写的数据。

    在sequence中,可以使用如下的方式来调用这两个任务:

    class case0_cfg_vseq extends uvm_sequence;
    	virtual task body();
    		p_sequencer.p_rm.counter_low.poke(status, 16'hFFFD);
    		p_sequencer.p_rm.counter_low.peek(status, value);
    		counter[15:0] = value[15:0];
    		p_sequencer.p_rm.counter_high.peek(status, value);
    		counter[31:16] = value[15:0];
    		`uvm_info("case0_cfg_vseq", $sformatf("after poke, counter's value(B ACKDOOR) is %0h", counter),
    	endtask
    endclass
    

    复杂的寄存器模型

    1.

    此前我们实现了一个最小、最简单的寄存器模型。在整个实现过程中,只是将一个寄存器加入了uvm_reg_block
    中,并在最后的base_test中实例化此reg_block。这个例子之所以这么做是因为只有一个寄存器。在现实应用中,一般会将uvm_reg_block再加入一个uvm_reg_block中,然后在base_test中实例化后者。从逻辑关系上看,呈现出的是两级的寄存器模型。

    假如一个DUT分了三个子模块:用于控制全局的global模块、用于缓存数据的buf模块、用于接收发
    送以太网帧的mac模块。global模块寄存器的地址为0x0000~0x0FFF,buf部分的寄存器地址为0x1000~0x1FFF,mac部分的寄存器地址为0x2000~0x2FFF,那么可以按照如下方式定义寄存器模型:

    class global_blk extends uvm_reg_block;
    endclass
    class buf_blk extends uvm_reg_block;
    endclass
    class mac_blk extends uvm_reg_block;
    endclass
    
    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的路径,这个路径不是绝对路径,而是相对于父reg_block来说的路径(简单起见,上述代码中的路径参数设置为空字符串,不能发起后门访问操作)。第三步是调用子reg_block的build函数。第四步是调用子reg_block的lock_model函数。第五步则是将子reg_block的default_map以子map的形式加入父reg_block的default_map中。

    这是可以理解的,因为一般在子reg_block中定义寄存器时,给定的都是寄存器的偏移地址,其实际物理地址还要再加上一个基地址。寄存器前门访问的读写操作最终都要通过default_map来完成。很显然,子reg_block的default_map并不知道寄存器的基地址,它只知道寄存器的偏移地址,只有将其加入父reg_block的default_map,并在加入的同时告知子map的偏移地址,这样父reg_block的default_map就可以完成前门访问操作了。因此,一般将具有同一基地址的寄存器作为整体加入一个uvm_reg_block中,而不同的基地址对应不同的uvm_reg_block。每个uvm_reg_block一般都有与其对应的物理地址空间。

    2.

    设某个寄存器有三个域,其中最低两位为filedA,接着三位为filedB,接着四位为filedC,其余位未使用。

    class three_field_reg extends uvm_reg;
    	rand uvm_reg_field fieldA;
    	rand uvm_reg_field fieldB;
    	rand uvm_reg_field fieldC;
    	virtual function void build();
    		fieldA = uvm_reg_field::type_id::create("fieldA");
    		fieldB = uvm_reg_field::type_id::create("fieldB");
    		fieldC = uvm_reg_field::type_id::create("fieldC");
    	endfunction
    endclass
    class mac_blk extends uvm_reg_block;
        rand three_field_reg tf_reg;
    	virtual function void build();
    		tf_reg = three_field_reg::type_id::create("tf_reg", , get_full_name());
    		tf_reg.configure(this, null, "");
    		tf_reg.build();
    		tf_reg.fieldA.configure(tf_reg, 2, 0, "RW", 1, 0, 1, 1, 1);
    		tf_reg.add_hdl_path_slice("fieldA", 0, 2);
    		tf_reg.fieldB.configure(tf_reg, 3, 2, "RW", 1, 0, 1, 1, 1);
    		tf_reg.add_hdl_path_slice("fieldA", 2, 3);
    		tf_reg.fieldC.configure(tf_reg, 4, 5, "RW", 1, 0, 1, 1, 1);
    		tf_reg.add_hdl_path_slice("fieldA", 5, 4);
    		default_map.add_reg(tf_reg, 'h41, "RW");
    	endfunction
    endclass
    

    这里要先从uvm_reg派生一个类,在此类中加入3个uvm_reg_field。在reg_block中将此类实例化后,调用tf_reg.configure时要注意,最后一个代表hdl路径的参数已经变为了空的字符串,在调用tf_reg.build之后要调用tf_reg.fieldA的configure函数。

    调用完fieldA的configure函数后,需要将fieldA的hdl路径加入tf_reg中,此时用到的函数是add_hdl_path_slice。这个函数的第一个参数是要加入的路径,第二个参数则是此路径对应的域在此寄存器中的起始位数,如fieldA是从0开始的,而fieldB是从2开始的,第三个参数则是此路径对应的域的位宽。

    3.

    UVM提供了可以使一个寄存器占据多个地址的方式:

    class reg_counter 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,
    		reg_data.configure(this, 32, 0, "W1C", 1, 0, 1, 1, 0);
    	endfunction
    	`uvm_object_utils(reg_counter)
        function new(input string name="reg_counter");
    		//parameter: name, size, has_coverage
    		super.new(name, 32, UVM_NO_COVERAGE);
    	endfunction
    endclass
    class reg_model extends uvm_reg_block;
    	rand reg_invert invert;
    	rand reg_counter counter;
    	virtual function void build();
    		counter= reg_counter::type_id::create("counter", , get_full_name());
    		counter.configure(this, null, "counter");
    		counter.build();
    		default_map.add_reg(counter, 'h5, "RW");
    	endfunction
    endclass
    

    这种方法相对简单,可以定义一个reg_counter,并在其构造函数中指明此寄存器的大小为32位,此寄存器中只有一个域,此域的宽度也为32bit,之后在reg_model中将其实例化即可。在调用default_map的add_reg函数时,要指定寄存器的地址,这里只需要指明最小的一个地址即可。这是因为在前面实例化default_map时,已经指明了它使用UVM_LITTLE_ENDIAN形式,同时总线的宽度为2byte,即16bit,UVM会自动根据这些信息计算出此寄存器占据两个地址。当使用前门访问的形式读写此寄存器时,寄存器模型会进行两次读写操作,即发出两个transaction,这两个transaction对应的读写操作的地址从0x05一直递增到0x06。

    当将counter作为一个整体时,可以一次性地访问它:

    class case0_cfg_vseq extends uvm_sequence;
    	virtual task body();
    		p_sequencer.p_rm.counter.read(status, value, UVM_FRONTDOOR);
    		`uvm_info("case0_cfg_vseq", $sformatf("counter's initial value(FRONT DOOR) is %0h", value),
    		p_sequencer.p_rm.counter.poke(status, 32'h1FFFD);
    		p_sequencer.p_rm.counter.read(status, value, UVM_FRONTDOOR);
    		`uvm_info("case0_cfg_vseq", $sformatf("after poke, counter's value(F RONTDOOR) is %0h", value),
    		p_sequencer.p_rm.counter.peek(status, value);
    		`uvm_info("case0_cfg_vseq", $sformatf("after poke, counter's value(B ACKDOOR) is %0h", value),
    	endtask
    endclass
    

    4.

    要在寄存器模型中加入存储器非常容易。在一个16位的系统中加入一块1024×16(深度为1024,宽度为16)的存储器的代码如下:

    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中。在这种情况下,只能使用后门访问的方式对其进行访问。

    要对此存储器进行读写,可以通过调用read、write、peek、poke实现。相比uvm_reg来说,这四个任务/函数在调用的时候需要额外加入一个offset的参数,说明读取此存储器的哪个地址。

    5.

    上面存储器的宽度与系统总线位宽恰好相同。假如存储器的宽度大于系统总线位宽时,情况会略有不同。如在一个16位的系统中加入512×32的存储器:

    class my_memory extends uvm_mem;
    	function new(string name="my_memory");
    		super.new(name, 512, 32);
    	endfunction
    	`uvm_object_utils(my_memory)
    endclass
    

    在派生my_memory时,就要在其new函数中指明其宽度为32bit,在my_block中加入此memory的方法与前面的相同。很明显,这里加入的存储器的一个单元占据两个物理地址,共占据1024个地址。那么当使用read、write、peek、poke时,输入的参数offset代表实际的物理地址偏移还是某一个存储单元偏移呢?答案是存储单元偏移。在访问这块512×32的存储器时,offset的最大值是511,而不是1023。当指定一个offset,使用前门访问操作读写时,由于一个offset对应的是两个物理地址,所以寄存器模型会在总线上进行两次读写操作。

    寄存器模型对DUT的模拟

    1.

    由于DUT中寄存器的值可能是实时变更的,寄存器模型并不能实时地知道这种变更,因此,寄存器模型中的寄存器的值有时与DUT中相关寄存器的值并不一致。对于任意一个寄存器,寄存器模型中都会有一个专门的变量用于最大可能地与DUT保持同步,这个变量在寄存器模型中称为DUT的镜像值(mirrored value)。
    除了DUT的镜像值外,寄存器模型中还有期望值(desired value)。如目前DUT中invert的值为’h0,寄存器模型中的镜像值也为’h0,但是希望向此寄存器中写入一个’h1,此时一种方法是直接调用前面介绍的write任务,将’h1写入,期望值与镜像值都更新为’h1;另外一种方法是通过set函数将期望值设置为’h1(此时镜像值依然为0),之后调用update任务,update任务会检查期望值和镜像值是否一致,如果不一致,那么将会把期望值写入DUT中,并且更新镜像值。

    class case0_cfg_vseq extends uvm_sequence;
    	virtual task body();
    		p_sequencer.p_rm.invert.set(16'h1);
    		value = p_sequencer.p_rm.invert.get();
    		`uvm_info("case0_cfg_vseq", $sformatf("invert's desired value is %0h ", value), UVM_LOW)
            value = p_sequencer.p_rm.invert.get_mirrored_value();
    		`uvm_info("case0_cfg_vseq", $sformatf("invert's mirrored value is %0h ", value), UVM_LOW)
    		p_sequencer.p_rm.invert.update(status, UVM_FRONTDOOR);
    		value = p_sequencer.p_rm.invert.get();
    		`uvm_info("case0_cfg_vseq", $sformatf("invert's desired value is %0h ", value), UVM_LOW)
    		value = p_sequencer.p_rm.invert.get_mirrored_value();
    		`uvm_info("case0_cfg_vseq", $sformatf("invert's mirrored value is %0h ", value), UVM_LOW)
    		p_sequencer.p_rm.invert.peek(status, value);
    		`uvm_info("case0_cfg_vseq", $sformatf("invert's actual value is %0h", value), UVM_LOW)
    		if(starting_phase != null)
    			starting_phase.drop_objection(this);
    endtask
    

    通过get函数可以得到寄存器的期望值,通过get_mirrored_value可以得到镜像值。其使用方式分别见上述代码。对于存储器来说,并不存在期望值和镜像值。寄存器模型不对存储器进行任何模拟。

    read&write操作:这两个操作在前面已经使用过了。无论通过后门访问还是前门访问的方式从DUT中读取或写入寄存器的值,在操作完成后,寄存器模型都会根据读写的结果更新期望值和镜像值(二者相等)。
    peek&poke操作:前文中也讲述过这两个操作的示例。在操作完成后,寄存器模型会根据操作的结果更新期望值和镜像值(二者相等)。
    get&set操作:set操作会更新期望值,但是镜像值不会改变。get操作会返回寄存器模型中当前寄存器的期望值。
    update操作:这个操作会检查寄存器的期望值和镜像值是否一致,如果不一致,那么就会将期望值写入DUT中,并且更新镜像值,使其与期望值一致。每个由uvm_reg派生来的类都会有update操作,每个由
    uvm_reg_block派生来的类也有update操作,它会递归地调用所有加入此reg_block的寄存器的update任务。
    randomize操作:寄存器模型提供randomize接口。randomize之后,期望值将会变为随机出的数值,镜像值不会改变。但是并不是寄存器模型中所有寄存器都支持此函数。如果不支持,则randomize调用后其期望值不变。若要关闭随机化功能,如前所述,在reg_invert的build中调用reg_data.configure时将其第八个参数设置为0即可。一般的,randomize不会单独使用而是和update一起。如在DUT上电复位后,需要配置一些寄存器的值。这些寄存器的值通过randomize获得,并使用update任务配置到DUT中。

    寄存器模型中一些内建的sequence

    1.

    UVM提供了一系列的sequence,可以用于检查寄存器模型及DUT中的寄存器。其中uvm_reg_mem_hdl_paths_seq即用于检查hdl路径的正确性。

    这个sequence的运行依赖于在基类uvm_sequence中定义的一个变量:

    uvm_reg_block model
    

    在启动此sequence时必须给model赋值。在任意的sequence中,可以启动此sequence:

    class case0_cfg_vseq extends uvm_sequence;
    	virtual task body();
    		uvm_reg_mem_hdl_paths_seq ckseq;
    		ckseq = new("ckseq");
    		ckseq.model = p_sequencer.p_rm;
    		ckseq.start(null);
    	endtask
    endclass
    

    在调用这个sequence的start任务时,传入的sequencer参数为null。因为它正常工作不依赖于这个sequencer,而依赖于model变量。这个sequence会试图读取hdl所指向的寄存器,如果无法读取,则给出错误提示。

    2.

    uvm_reg_hw_reset_seq用于检查上电复位后寄存器模型与DUT中寄存器的默认值是否相同。

    对于DUT来说,在复位完成后,其值就是默认值。但是对于寄存器模型来说,如果只是将它集成在验证平台上,而不做任何处理,那么它所有寄存器的值为0,此时需要调用reset函数来使其内寄存器的值变为默认值(复位值):

    function void base_test::build_phase(uvm_phase phase);
    	…
    	rm = reg_model::type_id::create("rm", this);
    	…
    	rm.reset();
    	…
    endfunction
    

    这个sequence在其检查前会调用model的reset函数,所以即使在集成到验证平台时没有调用reset函数,这个sequence也能正常工作。除了复位(reset)外,这个sequence所做的事情就是使用前门访问的方式读取所有寄存器的值,并将其与寄存器模型中的值比较。这个sequence在启动时也需要指定其model变量。

    如果想跳过某个寄存器的检查,可以在启动此sequence前使用resource_db设置不检查此寄存器。resource_db机制与config_db机制的底层实现是一样的,uvm_config_db类就是从uvm_resource_db类派生而来的。由于在寄存器模型的sequence中,get操作是通过resource_db来进行的,所以这里使用resource_db来进行设置:

    function void my_case0::build_phase(uvm_phase phase);
    	uvm_resource_db#(bit)::set({"REG::",rm.invert.get_full_name(),".*"},
    	"NO_REG_TESTS", 1, this);
    endfunction
    

    或者可以这样:

    function void my_case0::build_phase(uvm_phase phase);
    	uvm_resource_db#(bit)::set({"REG::",rm.invert.get_full_name(),".*"},
    	"NO_REG_HW_RESET_TEST", 1, this);
    endfunction
    

    3.

    UVM提供两个sequence分别用于检查寄存器和存储器的读写功能。uvm_reg_access_seq用于检查寄存器的读写。使用此sequence也需要指定其model变量。

    uvm_mem_access_seq用于检查存储器的读写。启动此sequence同样需要指定其model变量。

    寄存器模型的高级用法

    1.

    前文中在向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中。这特别适用于在仿真开始时随机化并配置参数。

    参考自张强《UVM实战》

    公众号:程序员Marshall

    展开全文
  • uvm寄存器模型RAL

    2021-01-31 16:32:12
    uvm寄存器模型RAL

    在这里插入图片描述

    在这里插入图片描述

    implicit predictor 自动预测

    在这里插入图片描述

    在这里插入图片描述

    explicit predictor

    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

    register model

    uvm_reg_block

    • 属于uvm_object

    reg

    field最小单位

    • m_mirrored
    • m_desired

    至少包含一个reg_map

    map

    • 调用reg2bus /bus2reg函数
    • 连接adapter/seqr
    • 子map

    uvm_mem

    seq中访问register model的两种方法

    a.通过config_db

    往seq中传递一个句柄;
    uvm_config_db #(reg_top_block)::set(this, "*", "reg_top_block", reg_rm);

    b. 通过sequencer

    通过p_sequencer调用sqr中的register model的句柄
    connect_phase 中 ref_mdl.p_rm = this.p_rm;

    RAL内建的一些sequence

    需要掌握下边两个sequence

    • uvm_reg_hw_reset_seq
    • uvm_reg_bit_bash_seq

    uvm_reg_hw_reset_seq 检查上电复位后寄存器模型与DUT 寄存器的默认值是否相同

    • 把register model的句柄赋值给这个sequence的model
    • 此seq通过前门读取寄存器的值与ral模型中的mirror值做比较,不一样则报错. 调用reg的mirror方法
    • 在测试时如果有些寄存器的默认值不需要关注,那么可以通过设置NO_REG_TESTS或者NO_REG_HW_RESET_TEST来实现.在实际的应用当中也支持对整个block做exclude处理.
      UVM:7.6.2 检查默认值的sequence
      uvm_reg_hw_reset_seq

    mirror

    virtual task mirror(    output  uvm_status_e    status,    //报错
      input     uvm_check_e     check    =  UVM_NO_CHECK,
      input     uvm_path_e  path     =  UVM_DEFAULT_PATH,
      input     uvm_reg_map     map  =  null,
      input     uvm_sequence_base   parent   =  null,
      input     int     prior    =  -1,
      input     uvm_object  extension    =  null,
      input     string  fname    =  "",
      input     int     lineno   =  0   )
    

    读DUT中寄存器的值,与update操作相反。如果第二个参数check为UVM_CHECK,那么会检查读取的值与镜像值是否一样,如果不一样报错。通过mirror读取DUT的寄存器值之后,会调用predict函数,更新镜像值。
    mirror有两种应用场景:一是在仿真中不断调用,但此时是UVM_NO_CHECK,保证镜像值与DUT中的值相等;二是在仿真结束的时候调用,这时是UVM_CHECK检查模型中的镜像值与DUT中的寄存器值是否一致。
    在这里插入图片描述

    uvm_reg_bit_bash_seq

    Verify the implementation of all registers in a block by executing the uvm_reg_single_bit_bash_seq sequence on it.
    Verify the implementation of a single register by attempting to write 1’s and 0’s to every bit in it, via every address map in which the register is mapped, making sure that the resulting value matches the mirrored value.
    uvm_reg_bit_bash_seq实现对寄存器每个bit的遍历操作,通过前门写前门读确认每个bit读写操作是否ok.此操作对RW寄存器有效,因为本身其就要支持读写操作,如果读写有问题就可以发现错误;对于诸如RO、RC这样属性的寄存器,本身就不支持写操作,会导致误报错误,所以后续需要exclude掉.

    展开全文
  • 寄存器模型理解

    2019-11-27 17:28:01
    寄存器模型 是一个model, 模拟的是reg的行为,就像reference model 模拟的是design的行为 需求: 包括各个寄存器字段描述、寄存器、寄存器组、寄存器地址映射等信息。 前门和后门访问 前门访问需要...
    • 是什么?

    RAL Model 对应于 DUT 中的寄存器,RAL Model 中有 DUT 每一个 寄存器的副本,它是真实硬件寄存器在软件环境中的行为模型;硬件寄存器的一个或多个 bit 的拼接称为一个域 ( field );多一个 field 形成一个 reg;多个 reg 构成一个块 ( block )。uvm library 已经为我们定义好了上述几个概念,我们在使用时只需继承即可。

    • 优势?

        a.方便对 DUT 中寄存器进行读写;
      b.在软件仿真时,可以不耗时的获取寄存器的值(直接从 RAL Model 中获取);
      c.可以很方便的正对寄存器的 coverage 验证点的收集。

     

    RAL Model

    task my_model::main_phase(uvm_phase phase);
        …
        reg_model.version.read(status, value, UVM_FRONTDOOR);
        reg_model.version.write(status, value, UVM_FRONTDOOR);
        …
    endtask

    只要一条语句就可以实现上述复杂的过程。像启动 sequence 及将读取结果返回这些事情,都会由寄存器模型来自动完成。在没有寄存器模型之前,只能启动 sequence 通过前门(FRONTDOOR)访问的方式来读取寄存器,局限较大,在 scoreboard(或者其他 component )中难以控制。而有了寄存器模型之后,scoreboard 只与寄存器模型打交道,无论是发送读的指令还是获取读操作的返回值,都可以由寄存器模型完成。有了寄存器模型后,可以在任何耗费时间的phase中使用寄存器模型以前门访问或后门(BACKDOOR)访问的方式来读取寄存器的值,同时还能在某些不耗费时间的 phase(如 check_phase)中使用后门访问的方式来读取寄存器的值。

     

    使用ralgen自动产生sv文件

    一般寄存器模型的代码是不用自己写的,有很多专门的工具可以生成,例如s家的工具 ralgen

    配置文件demo.ralf 如下

    block b1 {
        bytes 3;
        register r {
            bytes 1;
            field WDT_EN @'h5 {
                bits 1;
                reset 'h0;
                access rw;
                enum { ENABLE = 1, DISABLE = 0 };
            }
            field DATA_TEST @'h5 {
                bits 2;
                reset 'h0;
                access rw;
                enum { ENABLE = 1, DISABLE = 0 };
            }
        }
    }
    

    使用命令

    ralgen -t b1 -embed_enum_in_flds -uvm demo.ralf

    生成文件如下

    `ifndef RAL_B1
    `define RAL_B1
    
    import uvm_pkg::*;
    
    class ral_fld_b1_r_WDT_EN extends uvm_reg_field;
    
    	`uvm_object_utils(ral_fld_b1_r_WDT_EN)
    
    	function new(string name = "WDT_EN");
    		super.new(name);
    	endfunction : new
    
    	typedef enum bit[0:0] { 
    		ENABLE = 1, 
    		DISABLE = 0
    	} WDT_EN_values;
    endclass : ral_fld_b1_r_WDT_EN
    
    
    class ral_fld_b1_r_DATA_TEST extends uvm_reg_field;
    
    	`uvm_object_utils(ral_fld_b1_r_DATA_TEST)
    
    	function new(string name = "DATA_TEST");
    		super.new(name);
    	endfunction : new
    
    	typedef enum bit[1:0] { 
    		ENABLE = 1, 
    		DISABLE = 0
    	} DATA_TEST_values;
    endclass : ral_fld_b1_r_DATA_TEST
    
    
    class ral_reg_b1_r extends uvm_reg;
    	rand ral_fld_b1_r_WDT_EN WDT_EN;
    	rand ral_fld_b1_r_DATA_TEST DATA_TEST;
    
    	function new(string name = "b1_r");
    		super.new(name, 8,build_coverage(UVM_NO_COVERAGE));
    	endfunction: new
       virtual function void build();
          this.WDT_EN = ral_fld_b1_r_WDT_EN::type_id::create("WDT_EN",,get_full_name());
          this.WDT_EN.configure(this, 1, 5, "RW", 0, 1'h0, 1, 0, 0);
          this.DATA_TEST = ral_fld_b1_r_DATA_TEST::type_id::create("DATA_TEST",,get_full_name());
          this.DATA_TEST.configure(this, 2, 5, "RW", 0, 2'h0, 1, 0, 0);
       endfunction: build
    
    	`uvm_object_utils(ral_reg_b1_r)
    
    endclass : ral_reg_b1_r
    
    
    class ral_block_b1 extends uvm_reg_block;
    	rand ral_reg_b1_r r;
    	rand ral_fld_b1_r_WDT_EN r_WDT_EN;
    	rand ral_fld_b1_r_WDT_EN WDT_EN;
    	rand ral_fld_b1_r_DATA_TEST r_DATA_TEST;
    	rand ral_fld_b1_r_DATA_TEST DATA_TEST;
    
    	function new(string name = "b1");
    		super.new(name, build_coverage(UVM_NO_COVERAGE));
    	endfunction: new
    
       virtual function void build();
          this.default_map = create_map("", 0, 3, UVM_LITTLE_ENDIAN, 0);
          this.r = ral_reg_b1_r::type_id::create("r",,get_full_name());
          this.r.configure(this, null, "");
          this.r.build();
          this.default_map.add_reg(this.r, `UVM_REG_ADDR_WIDTH'h0, "RW", 0);
    		this.r_WDT_EN = this.r.WDT_EN;
    		this.WDT_EN = this.r.WDT_EN;
    		this.r_DATA_TEST = this.r.DATA_TEST;
    		this.DATA_TEST = this.r.DATA_TEST;
       endfunction : build
    
    	`uvm_object_utils(ral_block_b1)
    
    endclass : ral_block_b1
    
    
    
    `endif

     

    backup:

     

    包括各个寄存器字段描述、寄存器、寄存器组、寄存器地址映射等信息。

    前门和后门访问

    前门访问需要adapter

     

     

    展开全文
  • uvm之寄存器模型

    千次阅读 2017-04-19 14:58:39
    寄存器模型的意义 寄存器模型的搭建 寄存器模型的FAQ寄存器模型的意义寄存器模型,字面理解,跟参考模型类似。就是为工程里的寄存器提供一个参考模型。 这个模型里,包括各个寄存器字段描述、寄存器、寄存器组、...
  • 寄存器模型概览

    2021-03-05 14:24:57
    一、寄存器模型概览 寄存器是模块之间互相交谈的窗口,一方面可以通过读出寄存器的状态,获取硬件当前的状况,另外一方面也可以通过配置寄存器,使得寄存器工作在一定的模式下。在验证的过程中,寄存器的验证也排在...
  • 寄存器模型简介1.通常来说,DUT中会有一组控制端口,通过控制端口,可以配置DUT中的寄存器,DUT可以根据寄存器的值来改变其行为。这组控制端口就是寄存器配置总线。在没有寄存器模型之前,只能启动sequence通过前门...
  • 通过寄存器模型的常规方法,可以用来检查寄存器,以及协助检查硬件设计逻辑进和比对数据。 在软件实现硬件驱动和固件层时,也会实现类似寄存器模型镜像值的方法,即在寄存器配置的底层函数中,同时也声明一些全局的...
  • 在应用寄存器模型的时候,除了利用它的寄存器信息,也会利用它来跟踪寄存器的值。寄存器模型中的每一个寄存器,都应该有两个值,一个是镜像值(mirrored value),一个时期望值(desired value)。期望值是先利用寄存器...
  • UVM学习笔记--寄存器模型 Register Model

    万次阅读 多人点赞 2019-06-13 17:43:53
    1.寄存器模型( Register model )简介 UVM的寄存器模型是一组高级抽象的类,用来对DUT中具有地址映射的寄存器和存储器进行建模。它非常贴切的反映DUT中寄存器的各种特性,可以产生激励作用于DUT并进行寄存器功能检查...
  • 1. UVM基础之寄存器模型入门2. UVM RAL模型:用法和应用3. 层次寄存器的创建和集成4. 论文解读《Simpler Register Model Package for UVM Testbenches》(含案例和源码)5. 高级UVM寄存器建模6. 寄存器模型概览(上...
  •  寄存器模型中的寄存器值应该与DUT保持同步,但是由于DUT的值是实时更新的,所以寄存器模型并不能实时知道这种更新,在寄存器模型中专门有个值来尽可能与DUT中寄存器的值保持一致,叫镜像值(mirrorred value)。...
  • 1、介绍UVM的寄存器模型是验证各种寄存器设计的强大工具。利用寄存器模型的基本特点,可以验证简单的寄存器设计,更复杂的寄存器设计通常需要使用寄存器模型的hooks以及callbacks。层次化寄存器是一种相当常见的体系...
  • 寄存器模型构建 在构建UVM寄存器模型的过程中,读者需要了解下面这些与模型构建相关的类和它们的功能:     简化后的MCDF寄存器模型定义如下:     从上面的定义中,读者可以整理出关于寄存器...
  •  寄存器模型中的寄存器值应该与DUT保持同步,但是由于DUT的值是实时更新的,所以寄存器模型并不能实时知道这种更新,在寄存器模型中专门有个值来尽可能与DUT中寄存器的值保持一致,叫镜像值(mirrorred value)。...
  • 如何检查寄存器模型 在了解了寄存器模型的常规方法之后,我们需要考虑如何利用这些方法来检查寄存器、以及协助检查硬件设计的逻辑和数据比对。要知道,在软件实现硬件驱动和固件层时,也会实现类似寄存器模型镜像值...
  • 我们在上一节大致了解了与寄存器相关的流程,包括寄存器描述文件和UVM寄存器模型生成。从上节给的寄存器模型流程图中我们可以看到,接下来需要考虑选择与DUT寄存器接口一致的总线UVC,该UVC会提供硬件级别的访问方式...
  • 寄存器模型 Register Model一、RAL : Register Abstraction Layer二.UVM 寄存器模型的层次三、RAL模型创建并使用3.1.对每个寄存器进行定义并创建3.2.将寄存器放入register block容器中,并加入到对应的Address Map...
  • 前门访问 利用寄存器模型,我们可以更方便地对寄存器做操作。接下来我们分别两种访问...前门访问,顾名思义指的是在寄存器模型上做的读写操作,最终会通过总线UVC来实现总线上的物理时序访问,因此是真实的物理...
  • 在操作UVM寄存器模型时,模型中mirror值是如何更新的呢,在RAL机制中,有两种方式可以实现mirror值得更新。 (1)自动预测:所有的后门访问均是自动预测,对于前门访问,需要在map中进行设置 map.set_auto_predict(1...
  • MCDF寄存器模块代码 ...上面的设计中采取了宏的方式来替代一些寄存器的序列号和地址,这些宏在稍后的寄存器模型在映射硬件寄存器路径上也使用了。这么做使得设计和验证都因为采用同一套宏,而在后期寄存器...
  • 寄存器模型集成

    2021-03-05 16:17:10
    当cmd为读指令时,即需要从cmd_addr对应的寄存器中读出数据,在下一个周期,cmd_addr对应的寄存器数据被输送至cmd_data_out接口。 二、总线UVC的示例 class mcdf_bus_trans extends uvm_sequence_item; rand bit[1...
  • 1.为7.1.1的DUT建立寄存器模型。只有一个寄存器invert。首先从uvm_reg 派生一个invert 类: 1)new函数中,invert 的宽度作为参数传给super.new。不是有效宽度,而是这个寄存器总共的位数。 2)另一个参数是...
  • UVM提供标准的基类库,UVM的寄存器模型来自于继承自VMM的RAL(Register Abstract Layer),现在可以先将寄存器模型进行XML建模,再通过脚本工具直接生产寄存器模型。首先来看看uvm_reg_model的代码,该文件用来保存...
  • 在我们应用寄存器模型的时候,除了利用它的寄存器文件信息,也会利用它来跟踪寄存器的值。跟踪寄存器的值,一方面是建立mirrored value,另外一方面是为建立desired value。读到这里,读者们首先需要确立,...
  • 1.上一节,没有考虑总线的重用。集成到芯片时,芯片都有自己的配置总线,经过仲裁后分别链接到各个...在模块级别时,每个模块有各自的寄存器模型。很多user 习惯于在env 中例化寄存器模型: 1)但是如果要实现en
  • 1.上节的invert 寄存器...如果有寄存器模型,可以简化为: 启动sequence 及将读取结果返回,都有寄存器模型自动完成。 2.没有寄存器模型,只能通过FRONTDOOR 访问寄存器。有了之后,只与寄存器模型打交道。 3.可
  • 1.两级寄存器模型: 2.加入一个DUT 分成3个子模块,寄存器模型是: 1)定义block 时不用reset。在case 中使用时,lock后要reset。 2)一般将具有同一基地址的寄存器作为整体加入一个uvm_reg_block中。

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 2,432
精华内容 972
关键字:

寄存器模型