寄存器配置总线:通过控制端口,配置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
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函数,一般在此函数中实现所有寄存器的例化。
寄存器模型的前门访问操作可以分成读和写两种。无论是读或写,寄存器模型都会通过sequence产生一个uvm_reg_bus_op的变量,此变量中存储着操作类型(读还是写)和操作的地址,如果是写操作,还会有要写入的数据。此变量中的信息要经过一个转换器(adapter)转换后交给bus_sequencer,随后交给bus_driver,由bus_driver实现最终的前门访问读写操作。因此,必须要定义一个adapter。
在adapter中需要定义两个函数:
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还要做四件事:
寄存器模型提供了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中使用寄存器模型对寄存器进行读写操作。
后门访问:不通过总线进行读写操作,而是直接通过层次化的引用来改变寄存器的值。所有的后门访问都是不消耗仿真时间而只消耗运行时间的。
可以使用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中。
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
value = p_sequencer.p_rm.invert.get();
value = p_sequencer.p_rm.invert.get_mirrored_value();
除了像上图中左边一样使用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。
UVM提供mirror操作,用于读取DUT中寄存器的值并将它们更新到寄存器模型中。
其有两种应用场景,一是在仿真中不断地调用它,使得到整个寄存器模型的值与DUT中寄存器的值保持一致,此时check选项是关闭的。二是在仿真即将结束时,检查DUT中寄存器的值与寄存器模型中寄存器的镜像值是否一致,这种情况下,check选项是打开的。
(mirror更新的是寄存器模型,update更新的是DUT。)