寄存器模型的概念
寄存器配置总线:通过控制端口,配置DUT中的寄存器,DUT可以根据寄存器的值来改变其行为。
uvm_reg_field:寄存器模型中最小的单位是具体存储寄存器数值的变量。
uvm_reg:比uvm_reg_field高一个级别,但依然是比较小的单位。下图为uvm_reg_field与uvm_reg_的关系:
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。
在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还要做四件事:
- 调用configure函数,第一个参数是parent block,由于是最顶层的reg_block,因此填null;第二个参数是后门访问路径,这里传入一个空的字符串。
- 调用build函数,将所有的寄存器实例化。
- 调用lock_model函数,调用此函数后,reg_model中就不能再加入新的寄存器了。
- 调用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的转换,以读操作为例,完整的流程为:
- 参考模型调用寄存器模型的读任务。
- 寄存器模型产生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中的读数据返回参考模型。
-
后门访问:不通过总线进行读写操作,而是直接通过层次化的引用来改变寄存器的值。所有的后门访问都是不消耗仿真时间而只消耗运行时间的。
可以使用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,这样从整体上就能呈现出如下图中比较清晰的结构。
例如,一个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
除了像上图中左边一样使用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选项是打开的。文章来源:https://www.toymoban.com/news/detail-432115.html
(mirror更新的是寄存器模型,update更新的是DUT。)文章来源地址https://www.toymoban.com/news/detail-432115.html
到了这里,关于寄存器模型的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!