引出背景:为什么要用寄存器模型?
在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