【寄存器模型】一、一个简单的寄存器模型

  • Post author:
  • Post category:其他



引出背景:为什么要用寄存器模型?

在DUT


内部


会定义很多寄存器,比如状态寄存器,用于描述当前DUT的状态;配置寄存器,用于控制DUT工作在某种模式下。

在验证过程中,寄存器的验证是最先开始的,只有保证寄存器配置正确,才能保证硬件交互正确。这需要reference model对DUT内部的寄存器进行频繁的访问。

假若没有寄存器模型,那么访问DUT内部数据的方式有两种:

第一种方法是在环境中通过DUT实例化名进行点操作,也就是后门访问,但这需要使用绝对路径,如果寄存器数量庞大,就会导致验证环境臃肿复杂,可移植性不强。

第二种是在参考模型的控制下,启动一个sequence产生寄存器地址,寄存器读写命令,由driver将该req驱动到总线(BUS)上,由moniter采集总线输出,通过config_db传递给reference model。如下图所示:这种方式是需要消耗仿真时间的。

这种通过启动sequence的方式来访问DUT内部寄存器的方式被称为:前门访问。 这种方式消耗仿真时间,且其启动比较难以控制,此外传递数据给reference model的过程也很繁琐。因此提出了寄存器模型这一抽象概念。


什么是寄存器模型?

在描述寄存器模型之前,首先来了解一下实际寄存器的组成结构:如下图所示的DUT内部真实的寄存器

寄存器中最小的一个单位是

域(field)

,域是由一个或多个bit拼接而成的。

一个或多个域就构成了一个

寄存器(reg)

一个或多个寄存器就构成了一个

块(block)

将DUT中的寄存器抽象出来,建立寄存器模型,如下图所示:uvm library已经定义好了field、reg、block等相关的概念,构建寄存器模型时,只需要继承这些类即可。


uvm_reg_field

:构建寄存器模型中的域段。


uvm_reg

:在该类中例化了一个或多个域段。


uvm_reg_block

:例化了多个寄存器,同时还可以包括别的block。寄存器模型中至少有一个block


uvm_reg_map

:用来存储寄存器的地址,由于寄存器模型中的地址是偏移地址,在寄存器模型使用前门访问的时候,再将偏移地址转换成绝对地址,然后再启动sequence。并将读写的结果返回。在寄存器模型中,至少要有一个map。


所以UVM寄存器模型的本质就是重新定义了验证平台与DUT的寄存器接口,使验证人员更好地组织及配置寄存器,简化流程、减少工作量。


寄存器模型文件怎么生成?

使用excel表格,然后通过脚本工具生成.ralf文件,再通过ralgen生成ral_dut_regmodel.sv文件。该文件可以直接用在环境里。


寄存器模型能够解决哪些问题?

在第一节中通过前门访问的方式读写寄存器,不仅要在reference model中控制启动sequence,还要将读写结果通过config_db的方式返回,过程相当繁琐,且不便控制。

有了寄存器模型之后,上述繁琐的前门访问过程都可以由寄存器模型自动完成,不仅可以在耗费时间的phase中使用前门访问或者后门访问的方式访问寄存器,还可以在不耗费时间的phase中以后门访问的方式访问寄存器。

如何使用寄存器模型进行前门访问?


1、读写任务

前门访问:模拟总线对DUT进行操作,在这个过程中仿真时间是一直往前走的。


前门访问分为读和写两种,uvm提供了寄存器模型读写的任务:


read:

extern virtual task read(output uvm_status_e            status,
                         output uvm_reg_data_t          value,
                         input uvm_path_e               path = UVM_DEFAULT_PATH,
                         input uvm_reg_map              map = null,
                         input uvm_sequence_base        parent = null,
                         input int                      prior = -1,
                         input uvm_object               extension = null,
                         input string                   fname = "",
                         input int                      lineno = 0
);


write:

extern virtual task write(output uvm_status_e           status,
                         input uvm_reg_data_t           value, 
                         input uvm_path_e               path = UVM_DEFAULT_PATH,
                         input uvm_reg_map              map = null,
                         input uvm_sequence_base        parent = null,
                         input int                      prior = -1,
                         input uvm_object               extension = null,
                         input string                   fname = "",
                         input int                      lineno = 0
         );

可以看出,读写任务定义了很多参数,实际上常用的只有前三个:status表示读/写操作是否成功。value表示读出或者写入的数值,对于write来说,vlaue是input;对read来说,vlaue是output。path表示访问的方式:有UVM_FRONTDOOR和UVM_BACKDOOR。


2、Adapter转换器

已知前门访问是通过启动sequence,这些sequence,实例化了寄存器模型,在这些sequence的body函数中,寄存器模型可以调用read/write函数,调用的时候会产生一个

uvm_reg_bus_op

的变量,这个变量存储着操作类型(读/写)和操作地址。如果是写操作,这个操作还会存储写入的数据。


这个由sequence产生的变量uvm_reg_bus_op可以发送给sequencer么?答案是不可以。

因为sequencer接受的sequence应该是sequence_item类型的,而变量uvm_reg_bus_op显然不是。

所以需要将寄存器模型能够识别的类型uvm_reg_bus_op与sequencer能够识别的sequence_item类型做一个转换:


当调用read时,需要将读出的sequence_item类型转换成uvm_reg_bus_op类型。


当调用write时,需要将uvm_reg_bus_op转换成sequence_item类型。


完成这种转换功能的模块叫做

adapter(转换器)

:该模块派生自uvm_reg_adapter

class my_adapter extends uvm_reg_adapter;
    string tID = get_type_name();
    `uvm_object_utils(my_adapter)
    function new(string name="my_adapter");
        super.new(name);
    endfunction : new
    function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw); //将uvm_reg_bus_op转换成sequencer能够识别的sequence_item类型
        bus_transaction tr;  //定义一个能够被总线识别的sequence_item类型的tr
        tr = new("tr");
        tr.addr = rw.addr;
        tr.bus_op = (rw.kind == UVM_READ) ? BUS_RD: BUS_WR;
        if (tr.bus_op == BUS_WR)
            tr.wr_data = rw.data;
        return tr;
    endfunction : reg2bus
    function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
        bus_transaction tr;
        if(!$cast(tr, bus_item)) begin
            `uvm_fatal(tID,"Provided bus_item is not of the correct type. Expecting bus_trans action")
            return;
        end
        rw.kind = (tr.bus_op == BUS_RD) UVM_READ : UVM_WRITE;
        rw.addr = tr.addr;
        rw.byte_en = 'h3;
        rw.data = (tr.bus_op == BUS_RD) tr.rd_data : tr.wr_data;
        rw.status = UVM_IS_OK;
    endfunction : bus2reg
endclass : adapter


3、寄存器发起读操作,数值如何传回寄存器模型?

由于总线的特殊性,当driver对总线进行读操作时,driver也会得到要读取的数值,这个值放在有bus_sequence传递过来的transaction中,这个transaction可以通过bus2reg函数传回寄存器模型,adapter与driver之间由于没有实际的transaction传递,所以采用虚线。


4、将寄存器模型与adapter加入到test中

将寄存器模型加入test中需要做四件事情:

reg.build()将寄存器例化,注意例化方式是通过build函数。

reg.configure() 有两个参数,第一个是parent_block,如果是顶层的block则为null;第二个参数是后门访问路径,为字符串类型。

reg.lock_mode() 调用该函数表明不能再加入新的寄存器了。

reg.reset() 将寄存器模型的值设置为复位值,如果不调用该模型,则寄存器模型所有值均为0。

calss base_test extends uvm_test;  //在这个基测试用例中例化env、adapter(有的也会放在env中)、regmodel,然后其他测试用例直接继承该基测试用例即可

  my_env env;
  my_vsqr v_sqr;  //定义virtual sequencer用于对多个agent的调度,内部保存了sequencer的句柄
  regmodel rm;
  my_adapter reg_sqr_adapter;
  
  extern function void build_phase(uvm_phase phase);
  extern function void connect_phase(uvm_phase phase);

endclass

function void base_test :: build_phase(uvm_phase phase);  //做一些例化操作
  super.build_phase(phase);
  env=my_env::type_id::creat("env",this);
  v_sqr=my_vsqr::type_id::creat("m_seqr",this);
  rm=regmodel::type_id::creat("rm",this);
  reg_sqr_adapter=new("reg_sqr_adapter"); //adapter采用new来例化
  
  rm.bulid();  //此处和build_phase不一样,要手动的显式调用
  rm.configer(null, "");
  rm.lock_model();
  rm.reset();
  rm.set_hdl_path_root("top_tb.dut"); //设置后仿路径
  env.rm=this.rm; //将regmodel实例化模型传递到环境中去。
  
endfunction

将adapter加入test中,需要将adapter与sequencer进行连接,语法相对固定:使用set_sequencer连接sequencer与adapter来告知default_map。然后使用set_auto_predict函数设置default_map为自动预测状态。

function void base_test :: conknect_phase(phase); //做一些连接工作,主要是将底层sequencer句柄传递给virtual sequencer
  super.connect_phase(pahse);

  v_sqr.p_my_sqr=env.i_agt.sqr;
  v_sqr.p_bus_sqr=env.bus_agt.sqr;

  //上面两句传递sequencer句柄的语句除了使用点操作,还可以使用config_db的方式:如下
  //uvm_config_db #(bus_sqr) set::(this,"*","p_bus_sqr",env.bus_agt.sqr);
  //uvm_config_db #(i_sqr)   set::(this,"*","p_my_sqr",env.iagt_aqr);
  //使用config_db的方式就是在test处将底层sqr句柄统一发送到virtual sequencer中去,也就是说在virtual sequence中也要使用config_db::get的方法来获取句。


  //adapter例化在了test中,就要再此连接adapter与sqr,语句格式固定,记住即可
  rm.default_map.set_sequencer(env.bus_agt.sqr , reg_bus_adapter);
  rm.default_map.set_auto_predict(1);  //除了连接adapter,还要做自动预测
endfunction


5. 在其他componnent中使用寄存器模型

使用寄存器模型进行读操作,常发生在scoreboard、reference model中

//以在refence model中使用寄存器模型为例子

class mdl extends uvm_component;
   reg_model rm;
   
   extern task main_phase(uvm_phase phase);
endclass

task mdl::main_phase();
   suer.main_phase(phase);
   
   m_transaction    tr;
   m_transaction    new_tr;
   uvm_status_e     status; //执行读写函数的第一个变量,表状态
   uvm_reg_data_t   data;   //执行读写函数的第二个变量,表示读取到的数值
   rm.invert.read(status,data,FRONTDOOR);  //以前门访问的方式读取寄存器的值
   
endtask

一般scoreboard不会进行寄存器写操作,使用前门方式对寄存器进行写操作,需要启动sequence,假设对寄存器模型进行写操作发生在virtual sequence中:

calass v_sequence extends uvm_sequence;
   'uvm_declare_p_sequencer(v_sequencer)  
   
   virtual task body();
     m_transaction   tr;   
     m_transaction   new_tr;
     
     uvm_status_e    status;
     uvm_reg_data_t  data;
     p_sequencer.rm.invert.write(status,1,FRONTDOOR); //对寄存器进行写操作
     
     'uvm_do_with(tr,p_sequencer.sqr1);
     'uvm_do_with(new_tr,p_sequence.sqr2);
   endtask
endclass



版权声明:本文为carrotbanana原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。