文章目录
- 寄存器模型的集成
- 总线UVC的实现
- 总线UVC的示例
- Adapter的实现
- Adapter的集成
- 访问方式
- 前门访问
- 后门访问
寄存器模型的集成
总线UVC的实现
- MCDF访问寄存器的总线接口时序较为简单。控制寄存器接口首先需要在每一个时钟解析cmd。
- 当cmd为写指令时,即需要把数据cmd_data_in写入到cmd_addr对应的寄存器中。
- 当cmd为读指令时,即需要从cmd_addr对应的寄存器中读取数据,在下一个周期,cmd_addr对应的寄存器数据被输送至cmd_data_out接口。
- 我们给出一段8位地址线,32位数据线的总线UVC实现代码。
总线UVC的示例
class mcdf_bus_trans extends uvm_sequence_item;
rand bit[1:0] cmd;
rand bit[7:0] addr;
rand bit[31:0] wdata;
bit[31:0] rdata;
'uvm_object_utils_begin (mcdf_bus_trans)
.......
'uvm_object_utils_end
endclass
class mcdf_bus_sequencer extends uvm_sequencer;
virtual mcdf_if vif;
'uvm_component_utils (mcdf_bus_sequencer)
...
function void build phase (uvm_phase phase) ;
if ( ! uvm_config_db# (virtual mcdf_if) : :get(this, "","vif", vif) ) begin
'uvm_error("GETVIF", "no virtual interface is assigned")
end
endfunction
endclass
class mcdf bus_monitor extends uvm_monitor;
virtual mcdf_if vif ;
uvm _analysis _port # (mcdf_bus_trans) ap;
'uvm_component_utils (mcdf bus_monitor)
...
function void build_phase (uvm phase phase) ;
if (!uvm_config_db#(virtual mcdf_if) : :get(this, "","vif", vif)) begin
'uvm_error("GETVIF", "no virtual interface is assigned" )
end
ap = new ("ap" , this);
endfunction
task run_ phase(uvm phase phase);
forever begin
mon_trans();
end
endtask
task mon_trans ();
mcdf_bus_trans t;
@(posedge vif.clk) ;
if(vif.cmd ==`WR工TE) begin
t = new();
t.cmd =`WRITE;t.addr = vif.addr;
t.wdata = vif.wdata ;ap . write(t) ;
end
else if (vif .cmd ==`READ) begin
t = new () ;
t.cmd = `READ ;
t.addr = vif.addr ;
fork
begin
@(posedge vif.clk);
#10ps ;
t.rdata = vif.rdata;
ap.write (t) ;
end
join_none
end
endtask
endclass: mcdf_bus_monitor
class mcdf_bus_driver extends uvm_driver;
virtual mcdf_if vif;
'uvm_component_utils (mcdf_bus_driver)
...
function void build phase(uvm phase phase) ;
if (!uvm_config_db# (virtual mcdf_if)::get(this, "","vif", vif)) begin
'uvm_error( "GETVIF", "no virtual interface is assigned" )
end
endfunction
task run phase (uvm_phase phase);
REQ tmp ;
mcdf bus_trans req, rsp;
reset_listener () ;
forever begin
seq_item_port.get_next_item ( tmp) ;
void' ($cast(req, tmp)) ;
uvm_info("DRV",$sformatf ("got a item \n %s", req.sprint() ),UVM_LOW)
void' ($cast(rsp, req.clone())) ;
rsp.set_sequence_id(req.get_sequence_id() ) ;
rsp.set_transaction_id(req.get_transaction_id() );
drive_bus (rsp) ;
seq_item port.item_done(rsp) ;
uvm_info( "DRV",$sformatf( "sent a item \n %s",rsp.sprint()),UVM_LOW)
end
endtask
task drive_ read(mcdf bus_trans t);
@(posedge vif.clk);
vif .cmd <= t.emd;vif.addr <= t.addr ;e(posedge vif.clk);
#10ps;
t.rdata = vif.rdata;
endtask
task drive_idle (bit is_sync =0) ;
if(is_sync) e(posedge vif.clk) ;
vif.cmd <= 'h0 ;
vif.addr <= 'h0 ;vif.wdata <= 'h0;
endtask
endclass
示例囊括了mdf_bus_agent的所有组件:sequence item、sequencer、driver、monitor和agent。我们对这些代码的部分实现给出解释:
- mcdf_bus_trans包括了可随机化的数据成员cmd、addr、wdata和不可随机化的rdata。rdata之所以没有声明为rand类型,是因为它应从总线读出或者观察,不应随机化。
- mcdf_bus_monitor会观测总线,其后通过analysis port写出到目标analysis组件,在本节中它稍后将连接到uvm_reg_predictor.
- mcdf_bus_driver主要实现了总线驱动和复位功能,通过模块化的方法reset_listener()、drive_bus()、drive_write()、drive_read()和drive_idle()可以解析三种命令模式IDLE、WRITE和READ,并且在READ模式下,将读回的数据通过item_done(rsp)写回到sequencer和sequence—侧。建议读者在通过clone()命令创建RSP对象后,通过set_sequence_id()和 set_transaction_id()两个函数保证REQ和RSP的中保留的ID信息一致。
Adapter的实现
在具备了MCDF总线UVC之后,需要实现adapter。每一个总线对应的adapter所完成的桥接功能即是在uvm_reg _bus_op和总线transaction之间的转换。用户在开发某一个总线adapter类型时,需要实现下面几点:
- uvm_reg_bus_op与总线transaction中各自的数据映射。
- 实现reg2bus()和bus2reg()两个函数,这两个函数即实现了两种transaction的数据映射。
- 如果总线支持byte访问,可以使能supports_byte_enable;如果总线UVC要返回response数据,则应当使能provides_responses。在本例中,mcdf_bus_driver在读数时会将读回的数据填入到RSP并返回至sequencer,因此需要在adapter中使能provides_responses。由此使得bus2reg()函数调用时得到的数据是总线返回时的transaction,但读者需要注意如果总线UVC不支持返回RSP(没有调用put_response(RSP)或者item_done(RSP)),那么不应该置此位,否则adapter将会使得验证环境挂起。默认情况下,上述的两个成员的复位值都是0.
class reg2mcdf_adapter extends uvm_reg_adapter;
`uvm_object_utils(reg2mcdf_adapter)
function new (string name = "mcdf_bus_trans" );
super.new (name) ;
provides_responses = 1;
endfunction
function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
mcdf _bus_trans t = mcdf_bus_trans : : type_id : :create ("t") ;
t.cmd = (rw.kind == UVM_WRITE)?`WRITE : ‘READ;
t.addr = rw .addr ;
t.wdata = rw.data;
return t;
endfunction
function void bus2reg(uvm_sequence_item bus_item,ref uvm_reg_bus_op rw);
mcdf_bus_trans t;
if (!$cast(t, busmitem)) begin
'uvm_ fatal( "Nor_MCDF_BUs_TYPE", "Provided bus_item is not of the correct type")
return;
end
rw. kind = (t.cmd ==`WRITE) ? UVM_WRITE : UVM_READ;
rw . addr = t.addr ;
rw.data = (t.cmd ==`WRITE) ? t.wdata : t.rdata;
rw.status = UVM_IS_OK;
endfunction
endclass
关于uvm_reg_bus_op 有以下几个属性:
- 该类在构建函数中使能了provide_responses,这是因为mcdf_bus_driver在发起总线访问之后会将RSP一并返回至sequencer。
- reg2bus()完成的桥接场景是,如果用户在寄存器级别做了操作,那么寄存器级别操作的信息uvm_reg_bus_op会被记录,同时调用uvm_reg_adapter:reg2bus()函数。
- 在完成了将uvm_reg_bus_op的信息映射到mcdf_bus_trans之后,函数将mcdf_bus_trans实例返回。而在返回mcdf_bus_trans之后,该实例将通过mcdf_bus_sequencer传入到mcdf_bus_driver。这里的transaction传输是后台隐式调用的,不需要读者自己发起。
- 寄存器无论读写,都应当知道总线操作后的状态返回,对于读操作时,也需要知道总线返回的读数据,因此uvm_reg_adapter::bus2reg()即是从mcdf_bus_driver()将数据写回至mcdf_bus_sequencer,而一直保持监听的reg2mcdf_adapter一旦从sequencer获取了RSP (mcdf_bus_trans)之后,就将自动调用bus2reg()函数。
- bus2reg()函数的功能与reg2bus()相反,完成了从mcdf_bus_trans到uvm_reg_bus_op的内容映射。在完成映射之后,更新的uvm_reg_bus_op数据最终返回至寄存器操作场景层。
- 对于寄存器操作,无论读操作还是写操作,都需要经历调用reg2bus(),继而发起总线事务,而在完成事务发回反馈之后,又需要调用bus2reg(),将总线的数据返回至寄存器操作层面。
Adapter的集成
在具备了寄存器模型mcdf_rgm、总线UVC mcdf_bus_agent和桥接reg2mcdf_adapter之后,就需要考虑如何将adapter集成到验证环境中去:。
- 对于mcdf_rgm的集成,我们倾向于顶层传递的方式,即最终从test层传入寄存器模型句柄。这种方式有利于验证环境mcdf_bus_env的闭合性,在后期不同test如果要对rgm做不同的配置,都可以在顶层例化,而后通过uvm_config_db来传递。
- 寄存器模型在创建之后,还需要显式调用build()函数。需要注意uvm_reg_block是uvm_object类型,因此其预定义的build()函数并不会自动执行,还需要单独调用。
- 在还没集成predictor之前,我们采用了auto prediction的方式,因此调用了函数set_auto_predict()。
- 在顶层环境的connect阶段中,需要将寄存器模型的map组件与bus_sequencer和adapter连接。这么做的必要性在于将map (寄存器信息)、sequencer(总线侧激励驱动和adapter(寄存器级别和硬件总线级别的桥接关联在一起。也只有通过这一步,adapter的桥接功能才可以工作。
class mcdf_bus_env extends uvm_env;
mcdf_bus_agent agent;
mcdf_rgm rgm ;
reg2mcdf_adapter reg2mcdf ;
'uvm_component_utils (mcdf_bus_env)
...
function void build_ phase(uvm _phase phase) ;
agent = mcdf_bus_agent : : type_id : : create ( "agent", this) ;
if(!uvm_config_db# (mcdf_rgm): :get(this,"","rgm",rgm)) begin
'uvm_info( "GETRGM","no top-down RGM handle is assigned",UVM_LOW)
rgm = mcdf_rgm : : type_id: : create ("rgm" , this) ;
'uvm_info("NEWRGM","created rgm instance locally",UVM_IOW)
end
rgm . build() ;
rgm . map.set_auto_predict() ;
reg2mcdf = reg2mcdf_adapter : : type_id: : create ( "reg2mcdf" );
endfunction
function void connect _phase (uvm phase phase) ;
rgm.map.set_sequencer (agent.sequencer, reg2mcdf);
endfunction
endclass
访问方式
利用寄存器模型,我们可以更方便地对寄存器做操作。我们分成两种访问寄存器的方式,即前门访问(front-door)和后门访问(back-door)。
- 前门访问,顾名思义指的是在寄存器模型上做的读写操作,最终会通过总线UVC来实现总线上的物理时序访问,因此是真实的物理操作。
- 后门访问,指的是利用UVM DPI (uvm_hdl_read()、uvm_hdl_deposit()),将寄存器的操作直接作用到DUT内的寄存器变量,而不通过物理总线访问。
前门访问
- 接下来前门访问的示例中的sequence继承于uvm_reg _sequence.uvm_reg_sequence除了具备一般uvm_sequence的预定义方法外,还具有跟寄存器操作相关的方法。
- 在对寄存器操作的示例中,用户可以看到两种方式:
·第一种即uvm_reg:read()/write()。在传递时,用户需要注意将参数path指定为UVM_FRONTDOOR。uvm_reg:read()/write()方法可传入的参数较多,除了status和value两个参数需要传入,其它参数如果不指定,可采用默认值。
·第二种即uvm_reg_sequence:read_reg()/write_reg()。在使用时,也需要将path指定为UVM_FRONTDOOR。
class mcdf_example_seq extends uvm_reg_sequence ;
mcdf_rgm rgm ;
`uvm_object_utils (mcdf_example_seq)
'uvm_declare_p_sequencer (mcdf_bus_sequencer)
...
task body ( ) ;
uvm_status_e status ;uvm_reg_data_t data ;
if (!uvm_config_db#(mcdf_rgm) : : get(null,get_full_name(),"rgm",rgm)) begin
'uvm_error ("GETRGM","no top-down RGM handle is assigned")
end
//register model access write()/read()
rgm. chn10_ctrl_reg.read (status,data,UVM_FRONTDOOR,parent(this));
rgm.chn10_ctrl_reg.write(status,'h11,UVM_FRONTDOOR,parent(this));
rgm. chn10_ctrl_reg.read (status,data,uVM_FRONTDOOR,.parent(this));
//pre-defined methods access
read_reg (rgm. chnl1_ctrl_reg,status, data,uVM_FRONTDOOR);
write_reg (rgm. chnl1_ctrl_reg, status,'h22,uVM_PRONTDOOR);
read_reg (rgm.chnl1_ctrl_reg,status,data,UVM_FRONTDOOR);
endtask
endclass
后门访问
- 在进行后门访问时,用户首先需要确保寄存器模型在建立时,是否将各个寄存器映射到了DUT一侧的HDL路径。
- 下面的例码即实现了寄存器模型与DUT各个寄存器的路径映射:
class mcdf_rgm extends uvm_reg_block ;
... //寄存器成员和map声明
virtual function build() ;
...//寄存器成员和map创建
//关联寄存器模型和HDL
add_hdl_path ( "reg_backdoor_access.dut") ;
chn10_ctrl_reg.add_hdl path_slice($sformatf ("regs[%0d] ",‘SLVO_Rm_REG),0,32);
chnll_ctrl_reg.add_hdl_path_slice($sformatf ( "regs[%0d]",‘sLV1_RW_REG),0,32);
chn12_ctrl_reg.add_hdl _path_slice($sformatf("regs[%0d] ",‘SLV2_RW_REG),0,32);
chn10_stat_reg.add_hdl_path_slice($sformatf ( "regs[%0d]",‘SLVo_R_REG ),0,32);
chnll_stat_reg.add_hdl_path_slice($sformatf ("regs[%0d] ",‘SLV1_R_REG ),0,32);
chn12_stat_reg.add_hdl path_slice($sformatf ( "regs[%0d]",‘SIV2_R_REG ) ,0,32);
lock_model () ;
endfunction
endclass
- 示例中通过uvm_reg_blockadd_hdl_path(),将寄存器模型关联到了DUT一端,而通过uvm_reg:add_hdl_path_slice完成了将寄存器模型各个寄存器成员与HDL一侧的地址映射。
- 另外,寄存器模型build()函数最后以lock_model()结尾,该函数的功能是结束地址映射关系,并且保证模型不会被其它用户修改。
- 在寄存器模型完成了HDL路径映射后,我们才可以利用uvm_reg或者uvm_reg_sequence自带的方法进行后门访问。后门访问也有几类方法提供:
. uvm_reg:read()/write(),在调用该方法时需要注明UVM_BACKDOOR的访问方式。
. uvm_reg_sequenceread_reg()/write_reg(),在使用时也需要注明UVM_BACKDOOR的访问方式。
·另外,uvm reg::peek()/poke()两个方法,也分别对应了读取寄存器(peek)和修改寄存器(poke)两种操作,而用户无需指定访问方式尾UVM_BACKDOOR,因为这两个方法本来就只针对于后门访问。
class mcdf_example_seq extends uvm_reg_sequence ;
mcdf_rgm rgm ;
'uvm_object_utils (mcdf_example_seq)
'uvm_declare _p_sequencer(mcdf_bus_sequencer)
task body () ;
uvm status_e status ;uvm _reg_data_t data ;
uvm_status_e status ;uvm_reg_data_t data;
if (!uvm_config_db# (madf_rgm) : : get(null,get_full_name (), "rgm",rgm)) begin
'uvm_error("GETRGM","no top-down RGM handle is assigned")
end
//register model access write ( ) /read()
rgm.chn10_ctrl_reg.read (status,data,UVM_BACKDOOR,parent(this)) ;
rgm. chn10_ctrl_reg.write(status,'h11,,UVM_BACKDOOR,parent (this) );
rgm. chn10_ctrl_reg.read (status,data,UVM_BACKDOOR,.parent(this) );
//register model access poke( ) /peed ()
rgm. chnl1_ctrl_reg.peek (status,data,.parent (this));
rgm. chnl1_ctrl_reg.poke (status, 'h22,.parent(this));
rgm. chnl1_ctrl_reg.peek (status, data,.parent(this));
//pre-defined methods read_reg()/write_reg ()
read_reg (rgm.chn12_ctrl_reg,status,data,UVM_BACKDOOR);
write_reg (rgm.chn12_ctrl_reg,status,'h22,UVM_BACKDOOR);
read_reg (rgm.chn12_ctrl_reg,status,data,UVM_BACKDOOR);
//pre-defined methods peek _reg ()/poke_reg()
peek_reg (rgm.chnl2_ctr1_reg, status, data);
poke_reg (rgm.chnl2_ctrl_reg, status, 'h33);
peek_reg (rgm. chnl2_ctrl_reg, status, data) ;