简介
加入带总线的DUT后,我们就需要使用寄存器模型加入一个总线的driver了
UVM寄存器模型的本质就是重新定义了验证平台与DUT的寄存器接口, 使验证人员更好地组织及配置寄存器, 简化流程、 减少工作量。
如何定义
上述是reg field 、 uvm_reg 、
uvm_reg_block:
它是一个比较大的单位, 在其中可以加入许多的uvm_reg, 也可以加入其他的uvm_reg_block。 一个寄存器模型中至少包含一个uvm_reg_block。
uvm_reg_map:
用于存储寄存器地址, 并转换成物理地址( 因为加入寄存器模型中的寄存器地址一般都是偏移地址, 而不是绝对地址) 。
当寄存器模型使用前门访问方式来实现读或写操作时, uvm_reg_map就会将地址转换成绝对地址, 启动一个读或写的sequence, 并将读或写的结果返回。
在每个reg_block内部, 至少有一个( 通常也只有一个) uvm_reg_map
构建一个叫nvert的寄存器
super.new(寄存器名字,寄存器位数(一般与系统总线宽度一致),是否覆盖)
当reg_data实例化后, 要调用data.configure函数来配置这个字段,在build中配置
config(属于谁,位域宽度,最低位在reg的下标,存取方式,是否易失,复位值,是否存在复位,可否随机化,可否单独存取)
UVM共支持如下25种存取方式:
- RO: 读写此域都无影响。
- RW: 会尽量写入, 读取时对此域无影响。
- RC: 写入时无影响, 读取时会清零。
- RS: 写入时无影响, 读取时会设置所有的位。
- WRC: 尽量写入, 读取时会清零。
- WRS: 尽量写入, 读取时会设置所有的位
- WC: 写入时会清零, 读取时无影响。
- WS: 写入时会设置所有的位, 读取时无影响。
- WSRC: 写入时会设置所有的位, 读取时会清零。
- WCRS: 写入时会清零, 读取时会设置所有的位。
- W1C: 写1清零, 写0时无影响, 读取时无影响。
- W1S: 写1设置所有的位, 写0时无影响, 读取时无影响。
- W1T: 写1入时会翻转, 写0时无影响, 读取时无影响。
- W0C: 写0清零, 写1时无影响, 读取时无影响。
- W0S: 写0设置所有的位, 写1时无影响, 读取时无影响。
- W0T: 写0入时会翻转, 写1时无影响, 读取时无影响。
- W1SRC: 写1设置所有的位, 写0时无影响, 读清零。
- W1CRS: 写1清零, 写0时无影响, 读设置所有位
- W0SRC: 写0设置所有的位, 写1时无影响, 读清零。
- W0CRS: 写0清零, 写1时无影响, 读设置所有位。
- WO: 尽可能写入, 读取时会出错。
- WOC: 写入时清零, 读取时出错。
- WOS: 写入时设置所有位, 读取时会出错。
- W1: 在复位( reset) 后, 第一次会尽量写入, 其他写入无影响, 读取时无影响。
- WO1: 在复位后, 第一次会尽量写入, 其他的写入无影响, 读取时会出错
定义好此寄存器后, 需要在一个由reg_block派生的类中将其实例化:
在block中定义了map,实现了reg,build,并添加到map
config:第一个参数是此寄存器所在uvm_reg_block的指针, 这里填写this, 第二个参数是reg_file的指针 这里暂时填写null, 第三个参数是此寄存器的后门访问路径
map定义:第一个参数是名字, 第二个参数是基地址, 第三个参数则是系统总线的宽度, 这里的单位是byte而不是bit, 第四个参数是大小端, 最后一个参数表示是否能够按照byte寻址
集成
首先寄存器模型都会通过sequence产生一个uvm_reg_bus_op的变量, 此变量中存储着操作类型( 读还是写) 和操作的地址
adapter起到转换数据类型的作用
作用是uvm_reg_bus_op到bus trans的转化
在test实现
寄存器模型的前门访问操作最终都将由uvm_reg_map完成
因此在connect_phase中, 需要将adapter和bus_sequencer通过set_sequencer函数告知reg_model的default_map, 并将default_map设置为自动预测状态。
使用
在sequence和其他component中使用,声明之后reg_model p_rm;然后例化即可,可以在env传值例化
寄存器模型提供了两个基本的任务: read和write。
p_rm.invert.read(status, value, UVM_FRONTDOOR);
(是否成功,读取的值,读取方式)
参考模型一般不会写寄存器
在sequence中使用寄存器模型, 通常通过p_sequencer的形式引用,需要首先在sequencer中有一个寄存器模型的指针
p_sequencer.p_rm.invert.write(status, 1, UVM_FRONTDOOR);
前门 后门访问
前门
举例读操作
如果driver一直发送应答而sequence不收集应答, 那么将会导致sequencer的应答队列溢出。 UVM考虑到这种情况, 在adapter中设置了provide_responses选项:
在设置了此选项后, 寄存器模型在调用bus2reg将目标transaction转换成uvm_reg_item时, 其传入的参数是rsp, 而不是req。 使用应答机制的操作流程为:
后门
直接操作寄存器,不消耗仿真时间,消耗运行时间
一般使用接口,新建一个后门interface:
缺点是只能单独调用,操作一个寄存器
我们可以用c+dpi
DPI VPI
用于对RTL写值
我们要在C定义函数
int uvm_hdl_read(char *path, p_vpi_vecval value);
此函数要调用VPI读
然后在C
import "DPI-C" context function int uvm_hdl_read(string path, output uvm_hdl_d ata_t value);
以后就可以在SystemVerilog中像普通函数一样调用uvm_hdl_read函数了
然后要操作的寄存器的路径被抽像成了一个字符串, 而不再是一个绝对路径。
从而可以以参数的形式传递, 并可以存储,这为建立寄存器模型提供了可能。 一个单纯的Verilog路径, 如top_tb.my_dut.counter, 它是不能被传递的, 也是无法存储的。
注意还要在reg_block中调用uvm_reg的configure函数时, 设置好第三个路径参数:
设置好根路径hdl_root:
rm.set_hdl_path_root("top_tb.my_dut");
UVM提供两类后门访问的函数:
一是UVM_BACKDOOR形式的read和write:操作时模仿DUT的行为
二是peek和poke :完全不管DUT的行为
第 如对一个只读的寄存器进行写操作, 那么第一类由于要模拟DUT的只读行为, 所以是写不进去的, 但是使用第二类可以写进去。
peek还是poke, 其常用的参数都是前两个。 各自的第一个参数表示操作是否成功, 第二个参数表示读写的数据。
如此调用
复杂的rm
一般使用两级reg block
在第一级的uvm_reg_block中加入寄存器, 而第二级的uvm_reg_block通常只添加uvm_reg_block
uvm file
用于区分不同的hdl路径。
多域寄存器
物理上来看, 它的DUT实现中是三个寄存器, 因此这一个寄存器实际上对应着三个不同的hdl路径
首先定义一个reg包含三个域
然后在block中
占多个地址的reg
第一个方法是分割成两个寄存器
reg_data.configure(this, 32, 0, "W1C", 1, 0, 1, 1, 0);
直接定义两个地址长度就行了
加入存储器
在寄存器模型中加入存储器如下 深度1024,宽度16
config第二个参数是真实的dut存储器地址
对此存储器进行读写, 可以通过调用read、 write、 peek、 poke实现
当指定一个offset, 使用前门访问操作读写时, 由于一个offset对应的是两个物理地址, 所以寄存器模型会在总线上进行两次读写操作。
寄存器模型对DUT的模拟
在寄存器模型存在期望值以及镜像值,期望值是提前预判dut即将变化的值
read&write操作: 无论通过后门访问还是前门访问的方式, 操作完成后, 寄存器模型都会根据读写的结果更新期望值和镜像值( 二者相等) 。
peek&poke操作: 在操作完成后, 寄存器模型会根据操作的结果更新期望值和镜像值( 二者相等) 。
get&set操作: set操作会更新期望值, 但是镜像值不会改变。 get操作会返回寄存器模型中当前寄存器的期望值。
update操作: 这个操作会检查寄存器的期望值和镜像值是否一致, 如果不一致, 那么就会将期望值写入DUT中, 并且更新镜像值, 使其与期望值一致。 每个由uvm_reg_block派生来的类也有update操作, 它会递归地调用所有加入此reg_block的寄存器的update任务。
randomize操作: randomize之后, 期望值将会变为随机出的数值, 镜像值不会改变。 但是并不是寄存器模型中所有寄存器都支持此函数。 如果不支持, 则randomize调用后其期望值不变。 若要关闭随机化功能, 如7.2.1节所示, 在reg_invert的build中调用reg_data.configure时将其第八个参数设置为0即可。 一般的, randomize不会单独使用而是和update一起。 如在DUT上电复位后, 需要配置一些寄存器的值。 这些寄存器的值通过randomize获得, 并使用update任务配置到DUT中。
内建seq
检查后门hdl路径
在启动此sequence时必须给model赋值。 在任意的sequence中, 可以启动此sequence
调用start任务时, 传入的sequencer参数为null。
因为它正常工作不依赖于这个sequencer, 而依赖于model变量。
这个sequence会试图读取hdl所指向的寄存器, 如果无法读取, 则给出错误提示。
由这个sequence的名字也可以看出, 它除了检查寄存器外, 还检查存储器。 如果某个寄存器/存储器在加入寄存器模型时没有指定其hdl路径, 那么此sequence在检查时会跳过这个寄存器/存储器
检查默认值
uvm_reg_hw_reset_seq用于检查上电复位后寄存器模型与DUT中寄存器的默认值是否相同
对于寄存器模型来说, 需要调用reset函数来使其内寄存器的值变为默认值( 复位值)
此seq会调用reset,然后用前门访问读取dut值,然后比较。启用此seq要指定rm
可以用resource_db设置不检查此寄存器
或者
检查读写功能
使用前门访问的方式向所有寄存器写数据, 然后使用后门访问的方式读回, 并比较结果。然后把这个过程反过来, 使用后门访问的方式写入数据, 再用前门访问读回
必须为所有的寄存器设置好hdl路径
不检查寄存器,这样设置
uvm_resource_db#(bit)::set({"REG::",rm.invert.get_full_name(),".*"},"NO_REG_TESTS", 1, this);
uvm_resource_db#(bit)::set({"REG::",rm.invert.get_full_name(),".*"},"NO_REG_ACCESS_TEST", 1, this);
还有一个也可以起作用
class uvm_mem_access_seq extends uvm_reg_sequence #(uvm_sequence #(uvm_reg_it em)
不访问寄存器
高级用法
reg_predictor
左边是driver抓取返回值后,rm更新镜像和期望值,这个叫auto predicate
如此打开此功能rm.default_map.set_auto_predict(1);
右边是直接从真实的总线上获取值,使用monitor抓取然后送到predicator
我们需要做如下操作
bus agent和reg predictor连接在一起,通过ap接口
设置reg predictor的adaptor、map,rm通过map和predictor连接在一起
右图中如果要彻底关掉虚线的更新路径, 则需要
关掉自动预测
UVM_PREDICT_DIRECT功能与mirror操作
常用的只有前三个参数
把dut 的参数映射到rm
应用场景:仿真中调用,使rm和dut的reg值一样。此时check不需要check。要么在结束时调用检查一不一样。这时候需要check
mirror操作既可以在uvm_reg级别被调用, 也可以在uvm_reg_block级别被调用。 当调用一个uvm_reg_block的mirror时, 其实质是调用加入其中的所有寄存器的mirror。
设想一个场景,我们要统计cnt的值但是不想访问dut,那么我们就可以在rm使用预测
针对要预测的value,第二个参数是代表全部有效,第三个是预测的类型,具体如下
、
选择第一个参数实现在参考模型中更新寄存器模型而又不影响DUT的值
举个例子
如此在rm中用predict更新counter。
随机化rm以及update
对rm,支持randomize,可以对reg、field定义成rand类型,然后调用config
第八个参数即决定此field是否会在randomize时被随机化。但是即使定义 为1,也要加上“field存在写操作”才能实现随机化
constraint如下
一般可以randomize后使用update实现dut的更新
扩展位宽
定义地址位宽
默认情况下, 字选择信号的位宽等于数据位宽除以8, 它通过如下的宏来控制
其他函数
get_root_blocks
使得可以在不使用指针传递的情况下得到寄存器模型的指针
获取验证平台上所有的根块( root block) 。 根块指最顶层的reg_block。
使用get_root_blocks函数得到reg_block的指针后, 要用cast将其转化为目标reg_block形式( 示例中为reg_model) 。
以后就可以直接使用p_rm来进行寄存器操作, 而不必使用p_sequencer.p_rm。
get_reg_by_offset
调用最顶层的reg_block的get_reg_by_offset, 即可以得到任一寄存器的指针