热度 1| |||
1. UVM REG Model入门
本章通过一个非常小的示例来解释UVM REG中一些常用操作的实现。
1.1. 示例
假设某个功能模块中包含如下寄存器,且寄存器通过APB接口进行配置。
1.1.1. status
状态寄存器,只读,Offset: 0h, Width: 32bits
Field | access | width | reset | comment |
Reserved | RO | [31:4] | 0 | 保留位 |
Error | RO | [3:1] | 0 | 每一位分别表示一种错误类型 |
Done | RO | [0] | 0 | 完成标志位 |
1.1.2. threshold
阈值寄存器,可读可写,Offset: 04h, Width: 32bits
Field | access | width | reset | comment |
Reserved | RO | [31:16] | 0 | 保留位 |
THD | RW | [15:0] | 11h | 阈值位 |
1.2. 建立寄存器模型
针对上述设计,需要建立一个相应的寄存器模型, 以下为一种实现方法,注:实现方法不唯一,仅供参考。
首先我们需要针对不同的寄存器去定义一些新的寄存器类,这些类都是继承自uvm_reg,包含了所有reg操作的一些信息
1.2.1. status_reg
首先定义一个status_reg类,继承自uvm_reg,该类就表示了status寄存器。
class status_reg extends uvm_reg; endclass
定义了一个reg之后需要定义其中的reserve,error和done三个field(保留位在一般情况下不需要定义一个额外的field,这里将所有的bit都填充进reg中)。
uvm_reg_field rsv_field
uvm_reg_field err_field
uvm_reg_field done_field
接下来就需要实例化这些reg_field,实例化的方式有很多,可以在status_reg中自定义一个类似build的函数用于实例化,这里将直接实例化在new函数中,实例化reg_field之后还需要对reg_field进行configure,用于指定一些初始信息,这里也都放在reg的new函数中。接下来定义new函数:
function new (name string="",int unsigned n_bits,int has_coverage);
super.new(name,n_bits,has_coverage);
rsv_field=new("rsv_field");
err_field=new("err_field");
done_field=new("done_field");
done_field.configure(this,1,0,"RO",0,0,1,0,0);
err_field.configure(this,3,1,"RO",0,0,1,0,0);
rsv_field.configure(this,28,4,"RO",0,0,1,0,0);
endfunction
reg_field的configure函数参数原型:
· uvm_reg parent,表示该reg_field属于哪个寄存器对象
· int unsigned size,指示reg_field的大小
· int unsigned lsb_pos,指示reg_field的最低位在其parent reg中的索引
· string access,访问类型,这里都是RO类型
· bit volatile,reg_field易失性,如果为1,每次调用field的needs_update都会返回需要update,另外默认的寄存器检查也会设置为NO_CHECK
· uvm_reg_data_t reset,复位值,仅表示HARD类型的复位值
· bit has_reset,如果为1,则表示默认HARD类型的reset有效,即有硬件复位,一般都选择设置为1
· bit is_rand,表示reg_field的value值是否可随机,一般设置为0即可
· bit individually_accessible,是否可单独访问reg_field,通常用不到,一般选0
另外提一下,uvm_reg中有一个add_field的函数,用于将当前reg和其包含的reg_field进行关联,不过我们不需要显示调用该函数,因为在reg_field的configure中,只要指定了包含它的reg,UVM会自动调用这个add_field。
至此,一个status_reg寄存器类建立完成。threshold_reg类的建立与status_reg基本相同,区别在于他们的reg_field不太一样,所以配置时需要注意reset值和access类型。
1.2.2. reg_block
建立完所有的reg类之后,需要建立一个reg_block类,将一组reg包含进对应的reg_block中。reg_block继承自uvm_reg_block。
class reg_block extends uvm_reg_block;
然后声明之前定义的reg类。
status_reg status;
threshold_reg thres;
然后例化这些reg,同样例化在reg_block的new函数中。
function new (string name="",int has_coverage=UVM_NO_COVERAGE);
super.new(name,has_coverage);
status = new("status",32,has_coverage);
thres = new("thres",32,has_coverage);
status.configure(this);
thres.configure(this);
endfunction
uvm_reg的new函数,带3个参数,除了name和coverage之外,中间的参数表示reg的size。
uvm_reg的configure,也有3个参数,
· uvm_reg_block blk_parent,表示包含reg的reg_block对象
· uvm_reg_file regfile_parent=null,一个用于处理hdl_path的类,这里不指定,默认
· string hdl_path=“”,用于指定hdl_path,后门的一种,这里不指定,默认
当然,reg_block中的add_reg关联操作也会在reg的configure中被调用。至此,一个完整的reg_block创建完成,可以直接在test,env或者其他component中进行例化。
1.2.3. uvm_reg_map
关于uvm_reg_map的创建,方法也很多,第一种,在例化uvm_reg_block的component中也例化uvm_reg_map,然后调用其configure函数,配置base address,大小端,关联的uvm_reg_block等。这里介绍另一种方法,uvm_reg_block自带了一个create_map函数,我们直接调用该函数,假设我们在uvm_test中定义了reg_block, regm。然后我们在build_phase中创建和配置regm:
function void build_phase (uvm_phase phase);
uvm_reg_map dmap;
reg_adapter reg_adp = new("adp");
super.build_phase(phase);
regm = new("regm");
regm.configure();
regm.create_map("default_map",'h0,8,UVM_LITTLE_ENDIAN);
dmap = regm.get_default_map();
dmap.set_sequencer(m_env.apbm.m_seqr,reg_adp);
regm.lock_model();
endfunction
因为是最简单的寄存器模型,因此regm的configure我们全部采用默认参数,即regm不再处于某个uvm_reg_block中,且没有后门操作。
create_map,用于uvm_reg_block直接定义,创建并且配置一个uvm_reg_map对象,相关配置由本函数的参数提供:
· string name,定义map的名字
· uvm_reg_addr_t base_addr,基地址
· int unsigned n_bytes,map包含的范围,以byte为单位
· uvm_endianness_e endian,大小端
· bit byte_address=1,按byte访问,默认为1
由于我们在本reg_block中只定义了一个map,因此可以直接用get_default_map获取该map,然后对该map设置sequencer和reg_adapter,sequencer其实就是在环境中用于配置寄存器的driver对应的那个sequencer,uvm_reg本质就是通过reg中的write,read等API,将读写请求,数据等通过API启动sequence发送给driver,然后通过driver去驱动interface写往DUT。
而reg_adapter其实就是基于uvm_reg_adapter定义的一个类,主要重载了bus2reg和reg2bus两个函数。
1.2.4. reg_adapter
定义reg_adapter类
class reg_adapter extends uvm_reg_adapter;
重载reg2bus,该函数主要是在发送write/read请求时,用于将reg item信息中关键的kind,data,addr信息转化到对应driver可识别的item,这里用了apb interface来配置DUT寄存器。
function uvm_sequence_item reg2bus (const ref uvm_reg_bus_op rw);
apb_sequence_item busitem = new("busitem");
if (rw.kind == UVM_WRITE) busitem.pwrite = 1; else busitem.pwrite = 0;
busitem.addr = rw.addr;
busitem.pwdata = rw.data;
return busitem;
endfunction
而bus2reg则相反,本例中不使用provides_responses属性,即返回的rdata直接存在于apb_sequence_item中。
function void bus2reg (uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
apb_sequence_item apbitem;
if ($cast(apbitem,bus_item))
`uvm_fatal("FATAL","bus item casting failed")
if (apbitem.pwrite == 1) rw.kind = UVM_WRITE; else rw.kind = UVM_READ;
rw.addr = apbitem.addr;
rw.rdata = apbitem.prdata;
endfunction
首先$cast用于将父类的uvm_sequence_item转化为我们需要的子类apb_sequence_item,然后将apbitem中的读写信息,数据信息转化为uvm_reg_bus_op中的相关信息。
1.3. 使用寄存器模型
上一节最后在uvm_test中建立了寄存器模型regm。这一节以regm为例描述一下基本用法。
1.3.1. reg.write
如果要对一个寄存器进行写操作,例如修改threshold,为'h100,先定义一个uvm_status_e,用于保存返回的状态,是否成功。然后调用:regm.thres.write(st,'h100)。基本调用过程为uvm_reg.write -> uvm_reg.do_write -> uvm_reg_map.do_write -> uvm_reg_map.do_bus_write -> uvm_reg_map.do_bus_access -> uvm_reg_map.perform_accesses -> uvm_reg_adapter.reg2bus -> start_item(bus_req) -> apb_driver -> apb_interface -> DUT
当然后面还会返回status等等,真是的UVM操作很复杂,但总体驱动流程就是这样。
1.3.2. reg.read
读操作与写类似,不过需要再定义一个uvm_reg_data_t dat用于接收读数据:regm.thres.read(t,dat);
流程与write大致一样:uvm_reg.read -> uvm_reg.do_read -> uvm_reg_map.do_read -> uvm_reg_map.do_bus_read -> uvm_reg_map.do_bus_access -> uvm_reg_map.perform_accesses -> uvm_reg_adapter.reg2bus -> start_item(bus_req) -> apb_driver -> apb_interface -> DUT -> uvm_reg_adapter.bus2reg, 最后返回到read,然后获取value和status信息。
1.3.3. reg.predict
regm.thres.predict('h100); reg的predict本质就是依次去调用其中所有reg_field的predict函数,然后将reg predict参数中的寄存器值分别拆成对应field的predict,传给reg_field.predict。针对UVM_DIRECT_PREDICT,也是一般验证中常用的predict,无论reg的access为甚么类型,都能够直接修改value,m_desired和m_mirrored成为我们期望predict的值。predict函数相当于将寄存器的值改成用户期望的值,但不会立刻与DUT进行比较,只有在mirror,或者read操作的时候才会将DUT中读出来的值与相应的mirror或者desire值进行比较,如果不一致,则会报error(当然前提是用户打开了check开关)。另外关于reg_map的auto_predict,一旦打开auto_predict,则每次在调用write的时候UVM会自动进行一次predict,read的时候一样会check。综上所述,predict是根据不同的predict类型将期望值提前设置到reg_field中,然后在每次操作mirror或者read时都会将当前的mirror或者desire值与读到的DUT值进行比较,mirror操作比较mirror值与DUT值,read操作比较desire值与DUT值。
1.3.4. reg.update
regm.thres.update(st); 先检查needs_update,如果mirror值和desire值不同,则根据不同的access类型,获取一个经过操作的desire值,一般来讲像RW这种access类型,就是直接获取一个desire值,然后将该值write到寄存器。
1.3.5. reg.mirror
regm.thres.mirror(st,UVM_CHECK); mirror操作就是从DUT读一个寄存器,然后与当前的mirror值做一个比较,可用于检查DUT是否与期望模型中的reg模型一致。