寄存器模型 — UVM

一、基本概念

寄存器模型的组成:

  • 寄存器块(uvm_reg_block)由包含很多的寄存器(uvm_reg),也可以有存储器(uvm_mem);
  • 每个寄存器(uvm_reg)都由很多的域(uvm_reg_field)组成;
  • 单个域(uvm_reg_filed)包含多个bit位。

寄存器模型相关类(均为Object类型)

类名功能
uvm_reg_field用来针对寄存器功能域来构建对应的比特位
uvm_reg与寄存器相匹配,其内部可以例化和配置多个uvm_reg_field对象
uvm_mem匹配硬件存储模型
uvm_reg_map用来指定寄存器列表中各个寄存器的偏移地址、访问属性以及对应的总线 ,并将其转换成可以访问的物理地址(因为寄存器模型中的地址一般都是偏移地址,而不是绝对地址)
uvm_reg_block可以容纳多个寄存器(uvm_reg)、存储器(uvm_mem)和寄存器列表(uvm_reg_map)

寄存器模型配置的代码如下:

class ctrl_reg extends uvm_reg;//寄存器
    uvm_reg_field reserved;
    rand uvm_reg_field fifo_avail;
    ...
    virtual function build();
        reserved = uvm_reg_field::type_id::create("reserved");  
        reserved.configure(this,24,8,"RO",0,24'h0,1,0,0);
        fifo_avail = uvm_reg_field::type_id::create("fifo_avail");
        fifo_avail.configure(this,8,0,"RW",0,8'h0,1,1,0);
        ...
    endfunction
endclass

class top_rgm extends uvm_reg_block;
    rand ctrl_reg chnl0_ctrl_reg;
    rand ctrl_reg chnl1_ctrl_reg;
	...
    uvm_reg_map map; //声明 map
    
    virtual function build();
        chnl0_ctrl_reg = ctrl_reg::type_id::create("chnl0_ctrl_reg"); //实例化 uvm_reg 
        chnl0_ctrl_reg.configure(this);// 配置uvm_reg,将 uvm_reg 关联到 uvm_reg_block 上
        chnl0_ctrl_reg.build(); //手动调用 uvm_reg 的build(),实例化reg中的各个域
        ...
        map = create_map("map",'h0,4,UVM_LITTLE_ENDIAN,0);//实例化map
        map.add_reg(chnl0_ctrl_reg,32'h0000_0000,"RW");//将 uvm_reg 添加到 map 中
        ...
        lock_model();//不允许外部访问
    endfunction
endclass

解释上面代码:

  • uvm_reg_field(域)要在创建和实例化之后,还要在 uvm_reg 中调用configure( )函数对该域进行参数配置。其中:第一个参数指定关联到的寄存器uvm_reg;第二个参数是位数长,第三个是起点位,第四个为访问属性(RW、RO等),第五个为是否volatile,第六个为上电复位后的默认值,第七个为是否可以复位(一般都可以复位,为1),第八个表示是否可以随机化,第九个表示是否可以单独存取。(详细见UVM实战P222)
  • 一个uvm_reg_block 中一定要对应一个uvm_reg_map,调用uvm_reg_block中的 create_map()实例化 map 。其中:第一个参数时名字,第二个参数是基地址,第三个参数是系统总线的宽度(注意单位是按byte计),第四个是大小端,第五个参数是是否可以按byte寻址。
  • uvm_reg_map 是为了存储所有寄存器(uvm_reg)的地址,因此必须要将实例化的寄存器加入到 map 中,否则将无法实现前门访问。通过map.add_reg( ) 添加寄存器,其中:第一个参数是要加入的 uvm_reg_block ,第二个参数是寄存器地址,第三个参数是此寄存器的存起方式。

注意:

  • uvm_reg在创建后还要调用 configure( )函数,将 uvm_reg 指定关联到哪个 uvm_reg_block 上;
  • 同一个 uvm_reg_block 可以包含多个 uvm_reg_map ,各个 map 可以分别对应不同总线或不同地址段。
  • 寄存器的地址由基地址和偏移地址组成;
  • lock_model( ),不允许外部对这个寄存器模型做访问;
  • 在uvm_reg、uvm_reg_block 中都要定义 build( )函数,由于它们都是 object 类型,所以需要在上一层自己手动调用 build()函数,实现层层递进的寄存器配置。
  • 寄存器模型(top_rgm)在集成环境中时,也需要调用 configure() 和 build() 来完成配置和实例化。但是参数含义是不同的,具体见后文2.3章节。

二、寄存器模型的集成

    寄存器模型可以使的硬件级别的抽象级上升到寄存器级别,将过去由具体地址指定寄存器的方式,通过指定寄存器名称来替代,同时寄存器模型封装的一些函数使得可以直接对域进行操作。无论寄存器基地址如何变化,寄存器级别实现的配置序列都要比硬件级别的序列具备更好的维护性。那么寄存器模型是怎么集成到测试平台中的呢?

1. 寄存器模型与总线的桥接

    首先,需要建立寄存器模型与总线的沟通桥梁,让寄存器模型的操作和总线上的操作可以相互转换。而对寄存器模型的操作,Driver 是不能直接“读懂”的。同样地,Monitor采集的总线事务,寄存器模型也不认识,寄存器就没办法做相应地更新和检查。这时就需要一个 adapter 来完成寄存器事务到总线事务的转换。其中步骤包括:

  • 寄存器序列将带有目标寄存器的相关信息存放到 uvm_reg_item 实例(uvm_reg_bus_op)中,送往 adapter;
  • adapter 在接收到 uvm_reg_item(uvm_reg_bus_op)之后,转换成总线可以“认识”的 bus_seq_item 总线事务类型,再由 adapter 将 bus_seq_item 送给sequencer,继而发给Driver;
  • 总线从bus_seq_item中获取地址、数据、操作模式等信息后,发起总线的读写访问;
  • 如果总线上有反馈信号返回,则该信号会成为一个bus_seq_item 的总线事务,由总线sequencer 按照response item 的路径返回至 adapter,adapter对信号处理后,生成 uvm_reg_bus_op,最终作为返回值交给寄存器操作有关的方法。

2. adapter的实现

adapter 承担着寄存器模型与总线之间相互沟通的重要作用,那要怎么实现?如下:

  • uvm_reg_bus_op(寄存器操作的transaction)与总线transaction中各自的数据映射,即完成uvm_reg_bus_op和总线transaction之间的转换;
  • 必须要实现uvm_reg_adapter中预定义的两个函数 reg2bus( ) 和 bus2reg( ) ,通过这两个函数实现两种transaction的数据映射;
  • 如果总线支持byte访问,可以在adapter中使能support_byte_enable = 1;
  • 如果总线要返回response数据,应当在adapter中使能provides_response = 1;如果总线不支持返回RSP(如没有调用put_response(RSP)或item_done(RSP) ),则不能使能,否则adapter会致使验证环境挂起;
  • 寄存器的操作,无论是读操作还是写操作,都要经历调用reg2bus(),继而发起总线事务,在完成事务发回反馈后调用bus2reg(),将总线的数据返回至寄存器操作层面。
  • uvm_reg_bus_op类的成员有6个域,详见参考手册或红宝书P415

adapter 的主要代码如下:

class top_adapter extends uvm_reg_adapter;
	`uvm_object_utils(adapter)
    ...
    //reg2bus()函数,完成uvm_reg_bus_op --> bus_trans的内容映射
    function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
    	...
    endfunction
    //bus2reg()函数,完成bus_trans --> uvm_reg_bus_op的内容映射
    function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
    	...
    endfunction
endclass

注意:

  • adapter 要继承于 uvm_reg_adapter ;
  • adapter 是 Object 类型;
  • 要实现 reg2bus 和 bus2reg 两个方法,完成寄存器事务和总线事务的转换。

3. 寄存器模型rgm和adapter的集成

步骤如下:

  • 对于寄存器模型的集成,一般倾向于从顶层传递的方式,即最终从test层传入寄存器模型句柄;
  • 寄存器模型在创建之后,要显式的调用build函数,因为uvm_reg_block是object类型,build函数并不会自动执行;
  • 顶层环境的connect_phase 阶段,需要通过uvm_reg_map::set_sequencer(“bus sequencer句柄”,“adapter句柄”)的方式,将寄存器模型的map组件与bus sequencer和adapter连接,这会将map(寄存器信息)、sequencer(总线侧激励驱动)和 adapter(寄存器级别和硬件总线级别的桥接)关联在一起。只有通过这一步,才能完成adapter的桥接功能。

代码如下:如在 top_env 中集成rgm 和 adapter

class top_env extends uvm_env;
	top_rgm rgm;
	top_adapter adapter;
...
endclass

function void top_env::build_phase(uvm_phase phase);
	super.build_phase(phase);

	rgm = top_rgm::type_id::create("rgm",this);
	rgm.configure(null,"");
	rgm.build();
	
	adapter = top_adapter::type_id::create("adapter");
	
endfunction

function void top_env::connect_phase(uvm_phase phase);
	rgm.map.set_sequencer(env.agt.sqr,adapter);//将 map 与 sequencer 和 adapter 连接
endfunction

注意:

  • 寄存器模型在集成时,调用了configure() ,其中:第一个参数是 parent block ,第二个参数是后门访问路径。

三、访问寄存器的方式

  • 前门访问:是在寄存器模型上做的读写操作,通过总线UVC实现总线上的物理时序访问,是真实的物理操作;
  • 后面访问:是利用UVM DPI( uvm_hdl_read()、uvm_hdl_deposite() ),将寄存器的操作直接作用到DUT内的寄存器变量,而不是通过物理总线访问。这种方式速度非常快。

1. 前门访问

在uvm_reg_sequence里对寄存器操作的两种方式:

  • uvm_reg::read()/write() 。参数path需要指定为UVM_FRONTDOOR,其他除了status和value需要传入,其他可采用默认值;
  • uvm_reg_sequence::read_reg()/write_reg() 。参数path需要指定为UVM_FRONTDOOR;

2. 后门访问

在进行后门访问时,在寄存器模型建立时将各个寄存器映射到了DUT一侧的HDL路径

  • uvm_reg_block::add_hdl_path( ) 将寄存器模型block关联到DUT一端;
  • uvm_reg::add_hdl_path_slice( ) 将寄存器模型中各个寄存器成员与HDL一侧的地址映射;

寄存器模型与DUT各个寄存器的路径映射:

class mcdf_reg extends uvm_reg_block;
    ...
    virtual function build();
        ...
        add_hdl_path("reg_backdoor_access.dut");
        chnl0_ctrl_reg.add_hdl_path_slice($sformatf("reg[%0d]",`SLV0_RW_REG),0,32);
        ...
       
        lock_model();//保证模型不会被其他用户修改
    endfunction    
endclass

在寄存器模型完成HDL路径映射后,才可以利用uvm_reg或uvm_reg_sequence 自带的方法进行后门访问:

  • uvm_reg::read()/write() 。参数path需要指定为UVM_BACKDOOR,其他除了status和value需要传入,其他可采用默认值;
  • uvm_reg_sequence::read_reg()/write_reg() 。参数path需要指定为UVM_BACKDOOR;
  • uvm_reg::peek()/poke() 。分别对应读取寄存器(peek)和修改寄存器(poke)两种操作。这两种方法只针对后门访问,无需指定UVM_BACKDOOR。

3.前门访问和后门访问的比较

前门访问后门访问
通过总线协议访问,耗时,且在总线访问结束时,才能结束前门访问通过UVM DPI关联寄存器信号路径,直接读取或修改硬件,不耗时, 零时刻响应
一般读写只能按字(word)读写,无法直接读写寄存器域可以对寄存器或寄存器域直接做读写
依靠监测总线来对寄存器模型内容做预测依靠auto predication方式自动对寄存器内容做预测
正确反映了时序关系不受硬件时序控制,对硬件做后门访问可能发生时序冲突
通过总线协议,可以有效捕捉总线错误,继而验证总线访问路径不受总线时序功能影响

实际应用场景:(P420)

  • 可以先通过前门访问验证寄存器访问的物理通路工作正常,在前门访问被验证充分的前提下,可以在后续的测试中使用后门访问来节省访问多个寄存器的时间;
  • 先通过后门访问随机化整个寄存器列表(在一定的随机限制),随后再通过前门访问来配置寄存器。好处在于,不再只是通过设置复位之后的寄存器这种更有确定性的场景,而是通过让测试序列一开始的寄存器值都随机化来模拟无法预期的硬件配置场景,可以检查意想不到的边界情况;
  • 当寄存器出现地址不匹配的情况时,即便通过先写再读的方式,也无法有效测试出来。可以先通过前门访问配置寄存器之后,在通过后门访问来判断HDL地址映射的寄存器变量值是否改变,最后再通过前门访问读取寄存器的值,由此可以发现地址映射到错误寄存器的问题;
  • 如果DUT实现了一些特殊的寄存器,例如只能写一次的寄存器,建议使用前门访问确保反映真实的硬件行为。

四、寄存器模型的常规方法

1. mirror、desired 和 actual value

  • 寄存器模型中每个寄存器的域中都有两个值:镜像值(mirror value)和期望值(desired value);
  • 镜像值:表示当前硬件的已知状态值。往往由模型预测给出。
  • 期望值:是先利用寄存器模型修改软件对象值,而后利用该值更新硬件值(actual value)。

注意:

  • 镜像值可以在前门访问时通过观察总线或后门访问时通过自动预测的方式得到;
  • 镜像值可能与真实硬件值不一致,例如,状态寄存器的镜像值就无法与硬件实际值保持同步,;
  • mirror value 和 desired value是寄存器模型的属性, actual value对应着硬件的真实数值;
  • mirror value只能做predictor时可以修改,是predict预测出来的。而randomize( )寄存器模型中每个field的时候,就是在设定desired value;desired value紧接着会去更新硬件的actual value,在硬件更新后,又会更新软件一侧的mirror value。

2. prediction的分类

分为两种 predict 方式,自动预测和显示预测。

2.1 自动预测(auto predication)

    如果在环境中没有继承predicator,而是利用寄存器操作来自动记录每一次寄存器的读写数值,并且在后台调自动用predict( ) 方法的话,称为自动预测。自动预测需要在寄存器模型中打开此功能,如下:在env中集成寄存器模型时打开

function void top_env::connect_phase(uvm_phase phase);
	...
	rgm.map.set_auto_predict(1); //打开自动预测功能
endfunction

注意:

  • 如果其他sequence直接在总线层面上对寄存器进行操作,没有通过寄存器级别的 write( ) / read( )操作方法,或者是其他总线来访问寄存器等,都无法自动得到寄存器的镜像值和期望值;
  • 后门访问时,只能使用自动预测,因为后门访问没有对总线层面操作;

2.2 显示预测

    显示预测需要在顶层环境中集成 predictor(由参数化类uvm_reg_predictor例化),在总线上通过 monitor 捕捉总线事务。monitor一旦捕捉到了有效事务,会发送给 predictor,再利用 adapter 的桥接作用,实现事务信息转换,并将转换后的寄存器模型有关信息更新到map中,完成对寄存器模型的更新。显示预测对寄存器数值的预测更加准确。

predictor的集成

  • 声明uvm_reg_predictor #(“传递事务的类型”) mcdf2reg_predictor ,指明需要传递的参数的类型;
  • 给mcdf2reg_predictor做例化,注意是component类型,需要传递两个参数;
  • 需要将寄存器模型中的adapter和map的句柄与predictor中的adapter和map句柄做连接。这样predicator可以利用adapter里面的bus2reg( )函数,生成一个uvm_reg_bus_op事务,根据reg_map里面各个寄存器的地址,更新对应每一个寄存器的数值;
  • 将采集事务的monitor中的analysis port与predictor做连接;

代码如下:如在 top_env 中集成

class top_env extends uvm_env;
...
	top_rgm rgm;
	top_adapter adapter;
	uvm_reg_predict#(bus_transaction) predict;
...
endclass

function void top_env::build_phase(uvm_phase phase);
	super.build_phase(phase);

	rgm = top_rgm::type_id::create("rgm",this);
	rgm.configure(null,"");
	rgm.build();
	
	adapter = top_adapter::type_id::create("adapter");
	predict = uvm_reg_predict#(bus_transaction)::type_id::create("predict",this);
endfunction

function void top_env::connect_phase(uvm_phase phase);
	rgm.map.set_sequencer(env.agt.sqr,adapter);//将 map 与 sequencer 和 adapter 连接
    agt.monitor.analysis_port.connect(predictor.bus_in);//将monitor 的 TLM 端口与 predict 连接
    predictor.map = rgm.map;//设置predict中的map
    predictor.adapter = adapter;//连接 predict 和 adapter 
endfunction

注意:

  • predictor是一个component类型
  • 注意map组件与bus sequencer和adapter连接(set_sequencer),这样adapter才能起到桥接的作用;

3. uvm_reg 的访问方法

【常用访问方法:】

  • read( )/write( ): 前门(reg)、后门( reg和field )访问都可,读回/修改寄存器的实际值(actual value);
  • peek( )/poke( ): 只能在后门( reg和field )访问中用,读回/修改寄存器的实际值(actual value);
  • mirror( ): 前门(reg)、后门(block和reg)访问,读回硬件的实际值,然后更新或检查寄存器模型的镜像值
  • updata( ): 前门、后门访问(均为block和reg),如果期望值/镜像值不同于实际值,则修改硬件的真实值
  • get( )/set( ): 只能在前门访问中使用,获取reg和field的期望值
  • reset( )/get_reset( ): 只能在前门访问中使用,复位/获取复位后的block、 reg和field的期望值和镜像值,即复位的是寄存器模型;

uvm_reg_sequence提供类似的方法的方法,均是针对reg对象,不涉及field和block;

注:在register model中,存放这些mirror 和desired value的地方其实是分别存储在各个uvm_reg_field ,而不是uvm_reg中。


【mirror、desired 和 actual value数值变化的时序:】

  • 对于前门访问的read( )和write( ) ,在总线事务完成时,镜像值和期望值才会更新为与总线上相同的值,这种预测方式是显示预测。
  • 对于peek( )/poke( ),以及后门访问模式下的read( )和write( ) ,由于不通过总线,默认采取自动预测的方式,因此在方法调用返回后,镜像值和期望值也就相应修改

【寄存器访问方法的使用:】

  • 复位操作——reset( )复位的是寄存器模型,所以应当在寄存器模型捕捉到复位事件时(如@(negedge p_sequencer.vif.rst_n)),为了保持和硬件行为的同步,应当对寄存器模型也进行复位。复位后,可以用get_reset( ) 获取寄存器模型复位值,同前门访问read( )读回来的寄存器复位值做比较 ,判断硬件各个寄存器是否复位;
  • 检查上一次配置是否生效——mirror()不会返回读回来的硬件实际值,但是会将对应的镜像值修改。在修改镜像值之前,还可以传递UVM_CHECK参数,将读回来的数值与模型的镜像值进行比较。代码如: “block/reg”.mirror(status,UVM_CHECK,UVM_FRONTDOOR,.PARENT(this));对于状态寄存器也可以不做比较,因为状态寄存器随时可能被硬件内部逻辑修改;
  • 对寄存器做批量修改——运用set( ) 和 updata( )。在配置寄存器时先对寄存器模型随机化(随机化就是在设定期望值),然后再用set()方法配置个别寄存器或寄存器域,当寄存器的期望值与镜像值不相同时,可以通过updata( )方法将不相同的寄存器通过前门或后门访问的方式做全部修改。

4. uvm_mem与uvm_reg的联系与差别

uvm_mem类可以用来模拟RW(读写)、RO(只读)和WO(只写)类型的存储,并且可以配置存储器的数据宽度和地址范围。


虽然uvm寄存器模型也可以用来存储建模,但是与uvm_men的异同:

  • uvm_mem不支持预测和影子存储(即没有镜像值和期望值);
  • uvm_mem也支持前门访问和后门访问;
  • uvm_mem不但具有常规的访问方法read( )、write( )、peek( )和poke( ),也提供了burst_read( ) 和 burst_write( ),可以实现BURST访问形式。

uvm_men具有访问硬件存储的方法,相比于直接利用总线访问的优点在于:

  • 类似于寄存器模型访问寄存器,利用uvm_mem访问硬件存储器便于维护和复用;
  • 在访问过程中,可以利用模型的地址范围来预测硬件的地址范围是否全部覆盖;
  • uvm_mem可以先通过后门访问预先加载存储内容,然后通过前门访问读取存储内容,继而做数据对比,不但可以节省时间,还可以在测试方式上保持前后一致性;

uvm_mem要想实现BURST形式,需要考虑的因素:

  • 目前挂载的总线UVC是否支持BURST访问,例如APB不支持;
  • burst_read( ) 和 burst_write( )参数列表中的一项**uvm_reg_data_t value[ ]**采用的是数组的形式,不再是单一的变量,即表示用户可以传递多个数据。在后台,value[ ]的数据首先需要装载到uvm_reg_item对象中,但是uvm_reg_item的两个成员变量需要指定:element_kind = UVM_MEM,kind = UVM_BURST_READ ;
  • 其他的见《芯片漫游指南》P426
  • 10
    点赞
  • 128
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
SV(SystemVerilog寄存器模型是一种用于设计和验证寄存器寄存器文件的方法。在不使用UVM(Universal Verification Methodology)的情况下,可以通过以下步骤来实现SV寄存器模型: 1. 定义寄存器寄存器字段: 使用SV定义寄存器寄存器字段。例如,可以使用`reg`或`logic`类型定义寄存器,使用`bit`类型定义寄存器字段。为每个寄存器和字段提供适当的名称和位宽。 2. 定义寄存器模型: 创建一个寄存器模型,用于组织和管理寄存器和字段。可以使用各种SV的数据结构(例如`struct`),将寄存器和字段组织在一起,方便统一管理和访问。 3. 初始化寄存器模型: 在测试环境的构建阶段,为各个寄存器和字段提供适当的初始值。可以在测试环境中以硬编码方式完成初始化,或使用任务/函数动态地为模型赋值。 4. 编写寄存器读写操作: 实现对寄存器和字段的读写操作,可以使用SV中的`assign`语句完成赋值操作,或使用`always`块实现寄存器的行为和逻辑。确保按照设计规范和功能规约来编写这些操作。 5. 驱动寄存器访问: 在测试环境中创建驱动模块,用于驱动寄存器的读写操作。可在该模块中使用stimulus generator生成读写操作,或接收来自测试程序的控制指令,并将这些操作应用到寄存器模型中。 6. 验证寄存器行为: 使用监控模块来检测寄存器的读写操作,并进行验证。监控模块可以通过比较寄存器模型中的实际值和期望值,来检测是否存在寄存器读写的错误,例如读写冲突或非法操作。 7. 编写测试程序: 编写测试程序,调用驱动模块来执行寄存器的读写操作。可以针对不同的寄存器功能和使用场景编写不同的测试案例,以覆盖尽可能多的情况。 通过以上步骤,可以在SV中实现寄存器模型,用于设计和验证寄存器寄存器文件的行为和功能,而无需使用UVM。这种方法可以有效地加速寄存器开发和验证的过程,提高设计效率和可靠性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小小verifier

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值