验证那些事儿【UVM 寄存器模型】
验证那些事儿【UVM 寄存器模型】
UVM寄存器模型是硬件验证中一个重要的概念,它在验证环境中构建了一个与DUT侧寄存器组完全等价的模型。本文将从多个高频问题入手,深入剖析UVM寄存器模型的各个方面,包括其结构、集成方法、前门后门访问、镜像值和期望值的管理等。
高频问题 1:讲讲你理解的寄存器模型?
从本质上讲,UVM通过一系列复杂的机制,在验证环境中构建了一个【和DUT侧寄存器组完全等价】的寄存器模型。
- 对于验证环境的开发者而言,集成寄存器模型是比较复杂的:不仅涉及到寄存器模型本身的开发,还要设计配套的adapter和predictor和coverage model。
- 对于验证环境的使用者而言,使用寄存器模型是十分便捷的:使用者只需要和验证环境中的寄存器模型交互即可,不需要关心总线层面是如何通信的;与此同时,UVM还提供了一系列内建的sequence用于寄存器的各项检查。
因此,在实际的验证项目中,需要考虑到开发模型的成本:如果模块的寄存器组非常简单,并且后续没有迭代的需求,那么就没有开发的必要。
高频问题 2:讲讲寄存器模型的结构?
从寄存器模型内部看:
- 最基本的单元是uvm_reg_field,多个uvm_reg_field组成一个uvm_reg,多个uvm_reg组成一个uvm_reg_block。
- 其中,uvm_reg_block通常作为寄存器模型的顶层,它还可以包含其他的uvm_reg_block。
- 每个 uvm_reg_block还包括一个addresss map。
从寄存器模型外部的配套组件看:
- adapter是【map】和【DUT 总线 sequencer】之间的桥梁。
- predictor是【map】和【DUT 总线 monitor】之间的桥梁。
高频问题 3:如何集成寄存器模型?
寄存器模型的集成包含三个步骤:集成寄存器模型本身、集成 adapter、集成 predictor。
集成寄存器模型本身:
- 在 env 和 vsqr 中包含寄存器模型的指针。
- 在 base_test 中例化寄存器模型。
- 在 base_test 的 connect_phase 中将指针赋值。
这样,无论是验证环境的组件还是 sequence 都可以和寄存器模型交互。
集成 adapter:
- 在 base_test 中例化 adapter。
- 在 base_test 的 connect_phase 中,通过寄存器模型的set_sequencer函数将adapter和map、DUT 总线的 sequencer联系起来。
集成 predictor:
- 在 base_test 中例化 predictor。
- 在 base_test 的 connect_phase 中
- 给predictor的成员变量map和adapter赋值。
- 将DUT 总线的 monitor 的 TLM 端口连接到predictor 的 TLM 端口。
高频问题 4:讲讲 adapter?两个重要函数?
adapter 充当的是 map和总线 sequencer之间的桥梁:
寄存器模型无论读写,都会产生一个uvm_reg_bus_op类型的事务,这个 trans 如果想要被总线 sequener接收,势必要转换为总线 sequencer 对应的事务类型,这就是reg2bus的作用。
反之,如果寄存器模型希望从总线上获取一些数据,需要将总线事务转换为uvm_reg_bus_op类型,因为寄存器模型同样无法直接处理总线事务,这就是bus2reg的作用。
所以,adapter 需要通过set_sequencer关联特定的map和sequencer,保证自己是它们中间的通信桥梁。
// reg2bus
function reg2bus(const ref uvm_reg_bus_op rw);
wb_trans tr = new();
tr.kind = (rw.kind == UVM_READ) ? `WB_READ : `UVM_WRITE;
tr.addr = rw.addr;
tr.data = rw.data;
return tr;
endfunction: reg2bus
// bus2reg
function bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
wb_trans tr;
if (!$cast(tr, bus_item)) begin
`uvm_fatal(get_name(), "Provided bus_item is not of the correct type!!!")
return;
end
rw.kind = (tr.kind == `WB_READ) ? UVM_READ : UVM_WRITE;
rw.addr = tr.addr;
rw.data = tr.data;
rw.status = UVM_IS_OK;
endfunction: bus2reg
高频问题 5:前门访问和后门访问的区别?
前门访问:在寄存器模型上做的读写操作,最终会通过实际的总线访问 DUT 的寄存器。
后门访问:绕过实际总线,利用DPI+VPI 的层次结构索引直接操作 DUT 内的寄存器。
区别 前门访问 后门访问
时间上 由于是真实的物理操作,因此会消耗仿真时间 不消耗仿真时间
调试上 任何前门访问都会有波形文件,方便调试 没有波形文件的记录,调试难度增加
应用上 能验证总线协议本身是否正确 大规模寄存器的快速初始化能够操作只读寄存器注入故障
高频问题 6:后门访问路径如何配置?
某个寄存器的后门访问路径通常拆分为两个部分:该寄存器所在的 block 的路径、该 reg 相对于这个 block 的路径。
- 针对第一个部分,需要配置 uvm_reg_block 的路径,可以使用configure、add_hdl_path;两者没有区别。
- 针对第二个部分,需要配置 uvm_reg 的路径,可以使用configure、add_hdl_path_slice;两者的区别在于,如果一个寄存器的域支持单独存取,则不能使用 configure 配置 hdl 路径。
当然,针对 uvm_reg_block,还有一种配置 hdl 的方法:set_hdl_path_root,这个方法配置的 hdl 会覆盖其他方法配置的 hdl。
高频问题 7:有哪些前门访问和后门访问的方法?
先说最常见的四类:read、write、peek、poke。
其中,read 和 write 既可以前门访问,又可以后门访问,只需要在使用的之后指定访问类型即可。
// 前门访问
p_sequencer.p_reg_model.FLOW_CFG.read(status, data, UVM_FRONTDOOR);
p_sequencer.p_reg_model.FLOW_CFG.write(status, 0xffff_ffff, UVM_FRONTDOOR);
// 后门访问
p_sequencer.p_reg_model.FLOW_CFG.read(status, data, UVM_BACKDOOR);
p_sequencer.p_reg_model.FLOW_CFG.write(status, 0xffff_ffff, UVM_BACKDOOR);
peek 和 poke 是 UVM 针对后门访问内建的函数:相比于 read 和 write,无视寄存器的读写属性,更贴合后门访问的特性。
p_sequencer.p_reg_model.FLOW_CFG.peek(status, data);
p_sequencer.p_reg_model.FLOW_CFG.poke(status, 0xffff_ffff);
还有两个特殊的函数:mirror和update,它们也可以读取或修改硬件寄存器。
mirror()会调用read()读取硬件寄存器的值,并更新镜像值、期望值;它和 read 的区别在于,它不会返回读取的数值,只会返回读取的状态。
update()首先会检查镜像值和期望值是否一致,如果不一致,则调用write()修改 硬件寄存器的值,并更新镜像值;它和 write 的区别在于,它不需要人为指定写入的数据,因为写入的数据是 期望值。
因此在实际使用中,经常和set()配套使用:首先通过set()修改 期望值,再通过update()将这个新的期望值更新到DUT和镜像值上。
高频问题 8:讲讲镜像值、期望值?
UVM 源代码规定了每一个uvm_reg_field都有四个成员变量:复位值、随机值、镜像值、期望值。
复位值
复位值通过configure()配置;在配置好寄存器模型之后,通常使用reset()对寄存器模型进行复位,实际上就是将【剩余三个值】全部更新为【复位值】。
如果后续想修改复位值,可以使用set_reset();如果想读取复位值,可以使用get_reset()函数。
随机值
随机值是唯一使用 rand 修饰的成员变量;我们通常在configure()中配置 field 的随机属性。
- 如果允许随机:调用 randomize 函数就会更新随机值和期望值。
- 如果不允许随机:调用 randomize 函数就会将 期望值赋值给随机值。
镜像值和期望值
镜像值表示的是当前硬件寄存器值的映射;期望值表示的是user 期望硬件寄存器拥有的值;UVM 提供了predict()函数来修改这两个值,目的是保证【寄存器模型中的镜像值和期望值】最大程度的等价于【硬件寄存器组】;这个函数通常不需要 user 去调用。
- 如果 user 使用后门访问,那么这个函数会在每一次后门访问之后自动调用。
- 如果 user 使用前门访问,必须要打开 set_auto_predict(),这个函数才会在每一次前门访问之后自动调用。
高频问题 9:什么是预测?两种预测方式?
讲到预测,就要说一下每一个 uvm_reg_field 包含的两个值:镜像值和期望值;镜像值表示的是当前硬件寄存器值的映射;期望值表示的是user 期望硬件寄存器拥有的值。
预测的目的就是:保证【寄存器模型中的镜像值和期望值】最大程度的等价于【硬件寄存器组】;预测的方式有两种:自动预测和显示预测。
自动预测
自动预测是通过 UVM 内建的predict()完成的,这个函数的作用是更新寄存器模型的【镜像值】和【期望值】:如果 user 使用后门访问,那么这个函数会在每一次后门访问之后自动调用;如果 user 使用前门访问,必须要打开 set_auto_predict(),这个函数才会在每一次前门访问之后自动调用。
自动预测的缺点显而易见:如果 user 不通过寄存器模型指定的访问方式(前门、后门)去访问硬件寄存器,那么寄存器模型的值将无法更新;
显示预测
使用显示预测可以弥补自动预测的不足:只要寄存器配置总线上有活动,就可以通过 monitor 捕捉,再通过 predictor 更新到寄存器模型中;缺点就是工作量大,需要实现 monitor 并集成 predictor。
一旦集成了 predictor,就可以关闭 set_auto_predict(),因为寄存器模型的更新行为只和 monitor 有关,和predict()脱钩了;但仅限于前门访问,因为 monitor 本质上还是在监测总线上的行为;对于后门访问,仍然需要 predict() 来更新寄存器模型。
高频问题 10:讲讲内建的 sequnece?大概有哪些常用的?
内建的 sequence 和 user 定义的 sequence 在使用上有些差别:
- user 定义的 sequence,在创建实例完成之后,需要挂在到具体的 sequencer 上执行。
- 内建的 sequence,在创建实例完成之后,需要对其内部的 reg model 赋值,并不需要挂在到具体的 sequencer 上,start 参数传入 null 即可。
常用的流程是:检查复位值、检查 hdl 路径、检查读写一致性(还可以检查出地址不匹配的错误)。
virtual task body();
// 内建 sequence
uvm_reg_mem_hdl_paths_seq m_seq1 = new();
uvm_reg_hw_reset_seq m_seq2 = new();
uvm_reg_access_seq m_seq3 = new();
// 使用内建 sequence 检查
m_seq1.model = p_sequencer.m_reg_model;
m_seq1.start(null);
m_seq2.model = p_sequencer.m_reg_model;
m_seq2.start(null);
m_seq3.model = p_sequencer.m_reg_model;
m_seq3.start(null);
endtask: body
名称 作用 工作方式
uvm_reg_mem_hdl_paths_seq 检查 hdl 路径 使用后门访问的方式测试后门路径是否正确
uvm_reg_hw_reset_seq 检查复位值 使用前门访问的方式读取所有寄存器的值并将其与寄存器模型中的值比较
uvm_reg_access_seq 检查读写一致性 使用前门访问的方式向所有寄存器写数据,然后使用后门访问的方式读回,并比较结果使用后门访问的方式写入数据,再用前门访问读回,并比较结果
难题 1:只读寄存器如何验证?
验证寄存器地址:进行后门强制的写操作,再使用前门读。
验证只读属性:进行前门写操作 或 后门非强制的写操作,再使用 前门读 或者后门读。
难题 2:如果寄存器设置的数和你采集的数不一样怎么办?
第一步:检查寄存器模型本身是否有错
- 对比 spec 和寄存器模型代码。
- 使用寄存器模型对其他寄存器进行前门读写,看看其他寄存器能否正确读写。
第二步:检查寄存器地址是否匹配
- 前门写后门读。
- 前门读后门写。
第三步:检查寄存器是否粘连
- 如果 testcase 中同时对众多寄存器进行了写操作,有可能发生了寄存器粘连,也就是对一个寄存器写入值可能影响了其他的寄存器。
- 重新跑一个 tc,只对这个寄存器进行前门读写,检查结果是否正确。
第四步:检查比特位之间是否粘连
- 如果结果仍然不正确,有可能是同一寄存器中比特位之间的粘连。
- 通常,对一个寄存器中某一位进行置位或清零操作,不会影响到其他比特位,但是当发生寄存器内部比特位之间的粘连时,比特位之间的独立性就不存在了,对其中某个位进行置位或清零操作,就会影响到其他比特位的值。