UVM实战第7章:UVM中的寄存器模型

使用背景:DUT有一组控制端口,通过控制端口,配置DUT中的寄存器,DUT可以根据寄存器的值来改变行为,这组控制端口称为寄存器配置总线。

我们需要在参考模型中读取寄存器的值,然后在参考模型对该值输出的transaction做寄存器相同的操作。问题在于如何在参考模型中读取一个寄存器的值?

初步设想通过bus_driver向总线发送读指令,并给出要读的寄存器地址。需要启动一个sequence,该sequence发送transaction给bus_driver。

那么就需要考虑两个问题:

1、如何在参考模型的控制下启动sequence读取寄存器?

2、sequence读取的寄存器的值如何传递给参考模型?

这个过程可以用寄存器模型来实现,启动sequence和读取结果返回这些操作都将由寄存器模型自动完成。

没有rgm之前,只能通过启动sequence进行前门访问读取寄存器,在scoreboard中难以控制。

而有了rgm之后,scoreboard只与寄存器模型打交道,发送读指令和获取读操作的返回值都可以由寄存器模型完成。可以在任何耗时的phase中使用rgm进行前门/后门访问获取寄存器的值;也可以在不耗时phase中进行后门访问来读取寄存器的值

基本概念

uvm_reg_field(药丸):寄存器的,是rgm中最小的单位。

uvm_reg(小瓶子):寄存器,比reg_field高一个级别,一个寄存器中最少包含一个域。

uvm_reg_block(大箱子):在块中可以加入很多寄存器,也可以加入其他块,一个rgm最少包含一个块。

uvm_reg_map:寄存器在加入rgm时都有地址,reg_map用于存放这些地址,并将其转换为可以访问的物理地址,寄存器存入rgm的地址都是偏移地址,基地址+偏移地址=绝对地址。当rgm使用前门访问的方式实现读/写操作时,reg_map就会将偏移地址转换为绝对地址,启动读/写sequence,并将读/写的结果返回。

简单的寄存器模型

只有一个寄存器的rgm

从uvm_reg中派生出一个reg类,在reg类中rand声明uvm_reg_field。每个派生自uvm_reg的类都有一个build函数,这个build不同于component的build_phase(),它不会自动执行也不会构建树形结构(rgm是object),必须要手工调用,但是功能是一样的,就是在其中将所有的uvm_reg_field进行实例化,然后通过configure函数进行配置。

然后在new函数中传递寄存器的总位数,总位数一般与系统总线宽度一致。

 定义好uvm_reg寄存器后,在uvm_reg_block块派生的类中对其实例化,同之前一样,定义build函数,声明例化,配置,手动调用寄存器的build函数,在这里需要注意的是一个uvm_reg_block中一定要对应一个uvm_reg_map,一般用系统声明好的default_map即可,别忘记将寄存器加入到default_map中,使用add_reg函数,因为uvm_reg_map需要存储所有寄存器的地址,因此必须将实例化的寄存器加入到default_map中,否则无法进行前门访问。

总结一下建模过程:

1. 声明各个寄存器,并对寄存器中各个域进行buildconfigure

2. 在uvm_reg_block中声明各个寄存器,添加uvm_reg_map

3. 在uvm_reg_block的build函数中例化配置并调用各个寄存器的build函数

4. uvm_reg_map 例化,并将各个寄存器加入到uvm_reg_map中。

5. lock_model,防止外部的修改。

将寄存器模型集成到验证环境中

rgm前门访问操作分为两种:读和写。无论读写,rgm都会通过sequence产生一个uvm_reg_bus_op的变量该变量存储着操作类型,操作地址和可能有的待写入数据。这些信息需要adapter转换然后交给bus_sequencer,随后交给bus_driver,由bus_driver实现最终的前门访问读写操作,因此adapter转换器是必需的。

adapter要定义两个函数,一个是reg2bus,另一个是bus2reg。

reg2bus:顾名思义,reg --> bus。将rgm通过sequence发出的uvm_reg_bus_op型变量转换为bus_sequencer能接受的形式;

bus2reg:bus --> reg。当监测到总线有操作时,将收集来的transaction转换为rgm能接受的形式,方便更新相应寄存器的值。

那么rgm发起的读操作的值是如何返回给rgm的呢?

在bus_driver驱动总线进行读操作时,其也能获取要读的数值,如果将这个数值放入从bus_sequencer获得的bus_transaction中,bus_transaction就将有读取的值,这个值经过bus2reg函数的传递,将被rgm获取(没有实际transaction的传递)。

转换器完成后就可以在base_test中加入rgm了,要将rgm集成到base_test中,要在base_test中定义reg_model和reg_sqe_adapter。

将所有用到的类在build_phase()中实例化。实例化之后reg_model进行configure,build,lock_model和reset函数的调用。

在connect_phase()中,除了进行连接还要将adapter和bus_sequencer通过set_sequencer函数告知reg_model的default_map,并将default_map设置为自动预测状态,rgm的前门访问最终都由uvm_reg_map完成。

如何使用rgm

rgm建立好以后可以在sequence和其它component中使用,以ref model为例,要先在ref model例化rgm,有一个rgm的指针,然后在base_test中为env的指针赋值,再在env中将指针传递给ref model。

 对于寄存器,rgm提供了read和write两项基本任务。如果要在ref model中读取寄存器,使用read任务,而write任务一般不出现在ref model中,以virtual sequence的写操作为例,直接调用指针通过索引找到寄存器的write函数进行调用即可。

前门访问和后门访问

前门访问:模拟cpu在总线上发出指令(通过寄存器配置总线如APB、OCP、I2C对DUT进行操作),进行读/写操作,在整个过程中,仿真时间一直往前走。

后门访问:不通过总线进行读写操作,直接通过层次化的引用来改变寄存器的值。


前门访问中,重点在于启动sequence,我们可以在ref model中得到一个sequencer的指针,然后在该sequencer上启动sequence。

S1:参考模型调用寄存器模型读任务;

S2:寄存器模型产生sequence,并产生uvm_reg_item:rw;

S3:产生driver能接受的transaction:bus_req = adapter.reg2bus(rw);

S4:将bus_req交给bus_sequencer;

S5:driver得到bus_req,驱动req得到读取的值并将值放入rsp,调用item_done;

S6:寄存器模型调用adapter.bus2reg(rsp, rw)将rsp中的值传递给rw;

S7:将rw的读数据返回参考模型。

 后门访问存在的意义

1、不消耗仿真时间,只消耗运行时间且远小于前门访问消耗的运行时间。在大型芯片验证配置寄存器时,配置时间相交于前门访问大大缩短。

2、在需要大量运行时间的寄存器配置时,后门访问可以直接给只读寄存器一个初值,节省时间的同时完成了前门仿真操作完成的事情。

缺点:

前门访问可以从波形文件中找到总线信号波形的变化,但后门访问无法从波形文件中找到操作痕迹,只能参考后门访问时的打印信息,增加调试难度。

interface进行后门访问

为了代码可复用性,不使用绝对路径进行后门访问操作,而使用接口。即新建一个后门interface,在tc或者driver/scoreboard中对寄存器赋初值时可以调用接口中定义的后门读写函数。

这种方法仅适用于不想用rgm提供的后门访问或者不想建立rgm,但又必须要对DUT中的一个寄存器或一块存储器进行后门访问的情况。

C/C++平台通过DPI + VPI接口实现后门访问

如果在C/C++中对DUT的寄存器有读写要求,可以通过DPI+VPI接口来实现。

VPI:由verilog提供,将DUT层次结构开放给外部C/C++代码。

常用VPI接口:

// 从RTL中得到寄存器的值
vpi_get_value(obj, p_value); 
// 将RTL中寄存器设为某个值
vpi_put_value(obj, p_value, p_time, flags);

但是SV与C/C++之间传参时通过VPI接口很麻烦,因此引用DPI接口

以读操作为例,在C/C++中定义如下函数:

// 通过调用vpi_get_value得到寄存器的值
int uvm_hdl_read(char *path, p_vpi_vecval value);

在SV中通首先将C/C++中定义的函数import进去

import "DPI-C" context function int uvm_hdl_read(string path, output uvm_hdl_d ata_t value);

这样就可以直接在SV中调用uvm_hdl_read函数了,可以直接将参数传给C/C++的相应函数。

 这种方式将寄存器路径抽象成了字符串,而不是绝对路径,因此可以以参数形式传递,并且存储。

uvm_hdl_read("top_tb.my_dut.counter", value);

因此总结通过DPI + VPI进行后门访问的流程:

S1:建立rgm时设置好路径参数;

S2:后门访问操作时通过rgm调用uvm_hdl_deposit函数,在C/C++一侧将调用vpi_put_value对DUT中的寄存器进行写操作;

import "DPI-C" context function int uvm_hdl_deposit(string path, uvm_hdl_data_t value);

S3:后门访问操作时调用uvm_hdl_read函数,在C/C++一侧将调用vpi_get_value函数对DUT中的寄存器进行读操作并将值返回。

后门访问实现步骤

S1:设置每个寄存器的后门访问路径

S2:设置根路径

// block一侧设置后门访问路径
class reg_block extends uvm_reg_block;
    ······
    invert.configure(this, null, "invert"); // 第三个寄存器路径参数
endclass
 
//test一侧设置绝对路径
function void base_test::build_phase(uvm_phase phase);
    ······
    rm.set_hdl_path_root("top_tb.my_dut"); // 根路径
endfunction

两类后门访问函数

1、write/read:操作时模仿DUT行为,如对只读寄存器进行写操作,写不进去;

2、poke/peek:不管DUT行为,可以对只读寄存器进行写操作。

复杂的寄存器模型

层次化rgm

之前的rgm只是将一个寄存器加入了uvm_reg_block中,并在最后的base_test中实例化reg_block,如果只有一个寄存器的时候可以这么做。但是实际应用中,一般将uvm_reg_block再放入一个uvm_reg_block中,实现两层的rgm,如何在base_test中例化第二个uvm_reg_blcok。

我们只在第一级的uvm_reg_block中加入寄存器,在第二级的uvm_reg_block中添加uvm_reg_block。

将子reg_block加入到父reg_block中,要在父reg_block的build函数中:

S1、实例化子reg_block

S2、调用reg_block的configure函数,如果需要后门访问要在该函数中说明子reg_block的路径(相对路径)

S3、调用子reg_block的build函数

S4、调用子reg_block的lock_model函数

S5、将子reg_block的default_map以子map形式加入父reg_block的default_map中(基地址+偏移地址,实现前门访问)

加入存储器

除了寄存器外,DUT中有大量的存储器,这些存储器有的被分配了地址空间,有的没有,验证人员需要在仿真过程中得到存放在这些存储器中数据的值,并与期望值比较给出结果。

比如:DUT功能为接收一种数据,经过一些复杂处理(操作A)后将数据存储在mem中,mem是DUT内部的存储器,没有为其分配地址。当mem中数据达到一定量时,将它们全部读出,做另一复杂处理(操作B)后发送。如果在验证平台中只将DUT输出接口的数据和期望值做比较,当存在BUG时,无法判断是操作A还是操作B出现了问题,如果此时在输出接口之前再加一级比较,就可以快速定位BUG。

S1、由uvm_mem派生类my_memory

S2、在其new函数中调用super.new函数(名字,mem深度,mem宽度)

S3、在reg_model的build函数中将存储器实例化,调用其configure函数

S4、调用default_map.add_mem函数,将存储器加入default_map中,从而可以实现前门访问操作,如果没用对存储器分配地址空间,那么这里不加入default_map,只能通过后门访问对其进行访问

镜像值和期望值

镜像值:DUT一侧寄存器里的值会不断变更,寄存器模型一侧设置了专门用于最大可能与DUT保持同步的值,这个值为镜像值。通过get_mirrored_value函数获得镜像值。

期望值:想要修改DUT一侧寄存器的值。先通过set方法设置期望值,然后通过update方法更新,update方法会检验镜像值和期望值是否一致,若不一致,就将期望值写入DUT中,并更新镜像值。通过get函数获得期望值。

注:对于存储器来说,不存在期望值和镜像值,所以rgm不能对存储器进行模拟。获得存储器中某个存储单元的方法只有read/write、peek/poke。

常用操作

get&set:set更新期望值,不改变镜像值。get返回rgm中当前寄存器的期望值。

update:检查寄存器的期望值和镜像值是否一致,如果不一致,将期望值写入DUT并更新镜像值使之与期望值一致。

randomize:随机化期望值,不改变镜像值。randomize不会单独使用而是和update一起,使用时需要打开随机化功能(configur函数第八个参数设为1)。

mirror: 读回硬件实际值,更新或检查镜像值

reset:复位 block/register/filed的期望值和镜像值

get_reset: 获取register/field的复位值

内建sequence

uvm_reg_mem_hdl_paths_seq

检查hdl路径正确性

uvm_reg_hw_reset_seq

检查上电复位后寄存器模型与DUT中寄存器的默认值是否相同。

对于DUT来说,复位完成后其值即为默认值。但对rgm来说,如果只将其集成在验证平台,那么所有寄存器的值为0,需要调用reset函数使其内寄存器的值变为默认值(复位值)。

 uvm_reg_access_seq & uvm_mem_access_seq

用于检查寄存器&存储器的读写功能。

该内建序列通过前门访问的方式向所有寄存器写数据,然后通过后门访问的方式读回并比较结果。最后再反过来操作,后门访问写入,前门访问读回。

可以看到再reg_access_test中通过uvm_resource_db对寄存器AHB_DFLT_MASTER跳过了检查,因为在随机化时,AHB_DFLT_MASTER有这样的读写要求:

在AHB_matrix中,AHB_MASTER由0、1、2、3共四个,因此大于3的数写入到寄存器中都会被重置为0,因此为了复用性,我们将屏蔽此寄存器的该内建序列,而在seq中对该寄存器进行单独测试,对其它寄存器使用前门写、后门读,后门写、前门读的内建序列。

uvm_reg_bit_bash_seq

检查所有支持读写访问的域,对每一个可读写域分别写入1和0并且读出后做比较,用来检查寄存器域属性的有效性。

使用时注意调整时序,Factory机制覆盖内建测试序列。

寄存器模型的高级用法

reg_predictor

1、当driver将读取值返回后,rgm更新寄存器的镜像值和期望值。auto predict功能,在建立rgm时打开该功能。

rgm.default_map.set_auto_predict(1);

 2、由monitor将从总线上收集到的transaction交给rgm,rgm更新相应寄存器的值。

需要实例化一个reg_predictor,并为这个reg_predictor实例化一个adapter。

当总线上只有一个master时,左图和右图完全等价;如果由多个master,则左图会漏掉某些transaction。

右图中虚线为auto predict途径,可以通过将其设为0关闭自动预测途径,只保留经由predictor的途径。

 mirror

UVM的mirror操作用于读取DUT中寄存器的值,并将它们更新到寄存器当中。其函数原型为:

task uvm_reg::mirror(output uvm_status_e status,
                     input uvm_check_e check = UVM_NO_CHECK,
                     input uvm_path_e path = UVM_DEFAULT_PATH,
                     …);

通常只用前三个参数,第二个参数指如果发现DUT中寄存器的值和rgm中镜像值不一致,那么更新rgm之前是否给出错误提示。

1、在仿真中不断调用,使得整个rgm的值与DUT中寄存器的值保持一致,此时check关闭。

2、在仿真即将结束时,检查DUT中寄存器的值和rgm中寄存器的镜像值是否一致,此时check打开。

mirror可以读回硬件实际值并更新期望值和镜像值,同update类似。当调用一个uvm_reg_block的mirror时,本质是调用加入所有寄存器的mirror。

randomize和update

要避免一个field被随机化:

1、当在uvm_reg中定义field时,不要设置为rand

2、调用该field的configure函数时,第八个参数设置为0

3、不要设置此field的类型为RW、WRC、WRS、WO、 W1、WO1

三选一即可,其中第一种方式也适用于关闭某个uvm_reg或uvm_reg_block的randomize。

注:randomize会更新rgm的预期值,类似于set函数,因此可以在randomize完成后调用update任务,更新DUT,适用于在仿真开始时随机化并配置参数。

  • 5
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值