前言:当拿到寄存器模型和总线后,就要实现总线适配器,这就是集成的过程。
接下来需要考虑选择与DUT寄存器接口一致的总线UVC, 该UVC会提供硬件级别的访问方式。 要完成一次硬件级别的总线传输, 往往需要考虑给出地址、 数据队列、 访问方式等, 而寄存器模型可以使得硬件级别的抽象级上升到寄存器级别。由此带来最直观的好处在于, 以往由具体地址来指定寄存器的方式, 将由寄存器名称来替代, 同时寄存器模型封装的一些函数使得可以对域做直接操作,这一升级使得转变后的测试序列更易读。 而伴随着项目变化, 无论寄存器基地址如何变化, 寄存器级别实现的配置序列都要比硬件级别的序列具备更好的维护性。
从激励的流向来看, 寄存器序列(而不是总线序列)将带有目标寄存器的相关信息存放到uvm_reg_item实例中, 送往adapter。
adapter在接收到uvm_reg_item之后, 从中抽取出总线UVC所需的信息 , 同时生成总线UVC需要的bus_seq_item类型。 在完成了数据内容抽取和二次写入之后,bus_seq_item由adapter送往总线UVC。
总线UVC从bus_seq _item获取地址、 数据、 操作模式等信息之后发起总线的读写访问 。如果总线上有反馈信号标示访问是否成功,则该标示应当由总线sequencer按照response item的路径返回至adapter,adapter也应对该反馈信号做出处理。这一反馈路径在读访问时也会将总线读回的数据返同至adapter, 并最终作为返回值交回到与寄存器操作有关的方法。
总线UVC的实现
MCDF访问寄存器的总线接口时序较为简单。控制寄存器接口首先需要在每一个时钟解析cmd。
- 当cmd为写指令时,即需要把数据cmd_data_in写入到cmd_addr对应的寄存器中。
- 当cmd为读指令时,即需要从cmd_addr对应的寄存器中读取数据,在下一个周期,cmd_addr对应的寄存器数据被输送至cmd_data_out接口。
总线UVC示例
下面是一段8位地址线,32位数据线的总线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("GETVIE","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==WRITE) 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 reset_listener();
fork
forever begin
@(negedge vif.rstn) drive_idle();
end
join_none
endtask
task drive_bus(mcdf_bus_trans t);
case(t.cmd)
`WRITE: drive_write(t);
`READ: drive_read(t);
`IDLE: drive_idle(1);
default:`uvm_error("DRIVE","invalid mcdf command type received!")
endcase
endtask
task drive_write(mcdf_bus_trans t);
@(posedge vif.clk);
vif.cmd<=t.cmd;
vif.addr <=t.addr;
vif.wdata<=t.wdata;
endtask
task drive_read(mcdf_bus_trans t);
@(posedge vif.clk);
vif.cmd<=t.cmd;
vif.addr<=t.addr;
@(posedge vif.clk);
#10ps;
t.rdata=vif.rdata;
endtask
task drive_idle(bit is_sync=0);
if(is_sync)@(posedge vif.clk);
vif.cmd <='h0;
vif.addr <='h0;
vif.wdata<='h0;
endtask
endclass
总线UVC的解析
上面给出的代码襄括了 mcdf_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信息一致。
MCDF寄存器设计代码
param_def.v
`define ADDR_WIDTH 8
`define CMD_DATA_WIDTH 32
`define WRITE 2'b10 //Register operation command
`define READ 2'b01
`define IDLE 2'b00
`define SLV0_RW_ADDR 8'h00 //Register address
`define SLV1_RW_ADDR 8'h04
`define SLV2_RW_ADDR 8'h08
`define SLV0_R_ADDR 8'h10
`define SLV1_R_ADDR 8'h14
`define SLV2_R_ADDR 8'h18
`define SLV0_RW_REG 0
`define SLV1_RW_REG 1
`define SLV2_RW_REG 2
`define SLV0_R_REG 3
`define SLV1_R_REG 4
`define SLV2_R_REG 5
`define FIFO_MARGIN_WIDTH 8
`define PRIO_WIDTH 2
`define PRIO_HIGH 2
`define PRIO_LOW 1
`define PAC_LEN_WIDTH 3
`define PAC_LEN_HIGH 5
`define PAC_LEN_LOW 3
reg.v
module ctrl_regs( clk_i,
rstn_i,
cmd_i,
cmd_addr_i,
cmd_data_i,
cmd_data_o,
slv0_pkglen_o,
slv1_pkglen_o,
slv2_pkglen_o,
slv0_prio_o,
slv1_prio_o,
slv2_prio_o,
slv0_margin_i,
slv1_margin_i,
slv2_margin_i,
slv0_en_o,
slv1_en_o,
slv2_en_o);
input clk_i;
input rstn_i;
input [1:0] cmd_i;
input [`ADDR_WIDTH-1:0] cmd_addr_i;
input [`CMD_DATA_WIDTH-1:0] cmd_data_i;
input [`FIFO_MARGIN_WIDTH-1:0] slv0_margin_i;
input [`FIFO_MARGIN_WIDTH-1:0] slv1_margin_i;
input [`FIFO_MARGIN_WIDTH-1:0] slv2_margin_i;
reg [`CMD_DATA_WIDTH-1:0] mem [5:0]; //5个reg
reg [`CMD_DATA_WIDTH-1:0] cmd_data_reg; //1个reg
output [`CMD_DATA_WIDTH-1:0] cmd_data_o;
output [`PAC_LEN_WIDTH-1:0] slv0_pkglen_o; //送给formator
output [`PAC_LEN_WIDTH-1:0] slv1_pkglen_o; //送给formator
output [`PAC_LEN_WIDTH-1:0] slv2_pkglen_o; //送给formator
output [`PRIO_WIDTH-1:0] slv0_prio_o; //优先级送给arbiter
output [`PRIO_WIDTH-1:0] slv1_prio_o; //优先级送给arbiter
output [`PRIO_WIDTH-1:0] slv2_prio_o; //优先级送给arbiter
output slv0_en_o; //送给chnal
output slv1_en_o; //送给chnal
output slv2_en_o; //送给chnal
always @ (posedge clk_i or negedge rstn_i) //Trace fifo's margin
begin
if (!rstn_i)
begin
mem [`SLV0_R_REG] <= 32'h00000020; //FIFO's depth is 32
mem [`SLV1_R_REG] <= 32'h00000020;
mem [`SLV2_R_REG] <= 32'h00000020;
end
else
begin
mem [`SLV0_R_REG] <= {24'b0,slv0_margin_i};
mem [`SLV1_R_REG] <= {24'b0,slv1_margin_i};
mem [`SLV2_R_REG] <= {24'b0,slv2_margin_i};
end
end
always @ (posedge clk_i or negedge rstn_i)begin //write R&W register
if (!rstn_i)begin
mem [`SLV0_RW_REG] = 32'h00000007;
mem [`SLV1_RW_REG] = 32'h00000007;
mem [`SLV2_RW_REG] = 32'h00000007;
end
else if (cmd_i== `WRITE) begin
case(cmd_addr_i)
`SLV0_RW_ADDR: mem[`SLV0_RW_REG]<= {26'b0,cmd_data_i[`PAC_LEN_HIGH:0]};
`SLV1_RW_ADDR: mem[`SLV1_RW_REG]<= {26'b0,cmd_data_i[`PAC_LEN_HIGH:0]};
`SLV2_RW_ADDR: mem[`SLV2_RW_REG]<= {26'b0,cmd_data_i[`PAC_LEN_HIGH:0]};
endcase
end
end
always@ (posedge clk_i or negedge rstn_i) // read R&W, R register
if(!rstn_i)
cmd_data_reg <= 32'b0;
else if(cmd_i == `READ)begin
case(cmd_addr_i)
`SLV0_RW_ADDR: cmd_data_reg <= mem[`SLV0_RW_REG];
`SLV1_RW_ADDR: cmd_data_reg <= mem[`SLV1_RW_REG];
`SLV2_RW_ADDR: cmd_data_reg <= mem[`SLV2_RW_REG];
`SLV0_R_ADDR: cmd_data_reg <= mem[`SLV0_R_REG];
`SLV1_R_ADDR: cmd_data_reg <= mem[`SLV1_R_REG];
`SLV2_R_ADDR: cmd_data_reg <= mem[`SLV2_R_REG];
endcase
end
assign cmd_data_o = cmd_data_reg;
assign slv0_pkglen_o = mem[`SLV0_RW_REG][`PAC_LEN_HIGH:`PAC_LEN_LOW];
assign slv1_pkglen_o = mem[`SLV1_RW_REG][`PAC_LEN_HIGH:`PAC_LEN_LOW];
assign slv2_pkglen_o = mem[`SLV2_RW_REG][`PAC_LEN_HIGH:`PAC_LEN_LOW];
assign slv0_prio_o = mem[`SLV0_RW_REG][`PRIO_HIGH:`PRIO_LOW];
assign slv1_prio_o = mem[`SLV1_RW_REG][`PRIO_HIGH:`PRIO_LOW];
assign slv2_prio_o = mem[`SLV2_RW_REG][`PRIO_HIGH:`PRIO_LOW];
assign slv0_en_o = mem[`SLV0_RW_REG][0];
assign slv1_en_o = mem[`SLV1_RW_REG][0];
assign slv2_en_o = mem[`SLV2_RW_REG][0];
endmodule
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_response。在本例中,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_response = 1;
endfunction
function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
mcdf_bus_trans t = mcdf_bus_trans:;tpe_id::create("t");
t.cmd = (rw.kind == UVM_WRITE) ? `WRITE : `READ;
t.addr = rw.addr;
t.wdata = rw.data;
return t;
endfucntion
function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
mcdf_bus_trans t;
if(!$cast(t, bus_item)) begin
`uvm_fatal("NOT_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
Adapter的解析
该类在构建函数中使能了provide_response,
这是因为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_seqeuncer传入到mcdf_bus_driver。这里的transaction传输是后台隐式调用的,不需要主动发起。
寄存器无论读写,都应当知道总线操作后的状态返回,对于读操作时,也需要知道总线返回的读数据,因此uvm_reg_adapter::bus2reg()即是从mcdf_bus_driver()将数据写回至mcdf_bus_sequencer,而一直保持监听的reg2mcdf_adapter一旦从sequence获取了RSP(mcdf_bus_trans)之后就将自动调用bus2reg()函数。
bus2reg()函数的功能与reg2bus()相反,完成了从mcdf_bus_trans到uvm_reg_bus_op的内容映射。在完成映射之后,更新的uvm_reg_bus_op数据最终返回至寄存器操作场景层。
对于寄存器操作,无论读操作还是写操作,都需要经历调用reg2bus(),继而发起总线事务,而完成事务发回反馈之后,又需要调用bus2reg(),将总线的数据返回至寄存器操作层面。
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_LOW)
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
class test1 extends uvm_test;
mcdf_rgm rgm;
mcdf_bus,env env;
`uvm_component_utils(test1)
...
function void build phase(uvm phase phase);
rgm = mcdf_rgm::type_id::create("rgm" , this);
uvm_config_db#(mcdf_rgm)::set(this, "env*" , "rgm" , rgm);
env = mcdf_bus_env::type_id::create("env" , this);
endfunction
task run_phase(uvm_phase phase);
...
endtask
endclass
在具备了寄存器模型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的桥接功能才可以工作。
访问方式
利用寄存器模型,可以更加方便地对寄存器做操作,将访问方式分为两种,前门访问(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_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_FRONPDOOR, .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_PRONTDOOR);
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");
chnl0_ctrl_reg.add_hdl_path_slice($sformatf("regs[%0d]", `SLVO_RW_REG), 0, 32);
chnl1_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]", `SLV0_R_REG ), 0, 32);
chnl1_stat_reg.add_hdl_path_slice($sformatf("regs[%0d]", `SLV1_R_REG ), 0, 32);
chn12_stat_reg.add_hdl_path_slice($sformatf("regs[%0d]", `SLV2_R_REG ), 0, 32);
lock_model() ;
endfunction
endclass
上例中通过uvm_reg_block::add_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_sequence::read_reg()/write_reg(),在调用该方法时需要注明UVM_BACKDOOR的访问方式。
- 另外,uvm_reg::peek()/poke()两个方法,也分别对应了读取寄存器(peek)和修改寄存器(pook)两种操作,而用户无需指定访问方式尾的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;
if(!uvm_config_db#(mcdf_rgm)::get(null, get_full_name(), "gm", rgm)) begin
'uvm_error("GETRGM", "no top-down RGM handle is assigned")
end
//register model access write()/read()
rgm.chnl0_ctrl_reg.read (status, data, UVM_BACKDOOR, .parent(this));
rgm.chnl0_ctrl_reg.write(status, 'h11, UVM_BACKDOOR, .parent(this));
rgm.chnl0_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.chnl2_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_ctrl_reg, status, data);
poke_reg(rgm.chnl2_ctrl_reg, status, 'h33);
peek_reg(rgm.chnl2_ctrl_reg, status, data);
endtask
endclass
前门与后门的比较
从上面的差别可以看出,后门访问较前门访问更便捷更快一些,但不能单纯依赖后门访问。因此利用寄存器模型的前门访问和后门访问混合方式,对寄存器验证的完备性更有帮助。
前门后门的混合应用
实际应用场景:
- 通过前门访问的方式,先验证寄存器访问的物理通路工作正常,并且有专门的寄存器测试的前门访问用例,来遍历所有的寄存器。在前门访问被验证充分的前提下,可以在后续测试中使用后门访问来节省访问多个寄存器的时间。
- 如果DUT实现了一些特殊寄存器,例如只能写一次的寄存器等,我们建议用物理方式去访问以确保反映真实的硬件行为。
- 寄存器随机设置的精髓不在于随机可设置的域值,而是为了考虑日常不可预期的场景,先通过后门访问随机化整个寄存器列表(在一定的随机限制下),随后再通过前门访问来配置寄存器。这么做的好处在于,不再只是通过设置复位之后的寄存器这种更有确定性的场景,而是通过让测试序列一开始的寄存器值都随机化来模拟无法预期的硬件配置前场景,而在稍后设置了必要的寄存器之后,再来看是否会有意想不到的边界情况发生。
- 有的时候,即便通过先写再读的方式来测试一个寄存器,也可能存在地址不匹配的情况。譬如寄存器A地址本应该0x10,寄存器B地址本应该为0x20;而在硬件实现中,寄存器A对应的地址为0x20,寄存器B对应的地址为0x10。像这种错误,即便通过先写再读的方式也无法有效测试出来,那么不妨在通过前门配置寄存器A之后,再通过后门访问来判断HDL地址映射的寄存器A变量值是否改变,最后通过前门访问来读取寄存器A的值。上述的方式是在前门测试的基础之上又加入了中途的后门访问和数值比较,可以发现地址映射到错误寄存器的问题。
- 对于一些状态寄存器,在一些时候外界的激励条件修改会依赖这些状态寄存器,并且在时序上的要求也可能很严格。例如,上面MCDF的寄存器中有一组状态寄存器表示各个channel中FIFO的余量,而channel中FIFO的余量对于激励驱动的行为也很重要。无论是前门访问还是后门访问,都可能无法第一时间反映FIFO当前时刻的余量。因此对于要求更严格的测试场景,除了需要前门和后门来访问寄存器,也需要映射一些重要的信号来反映更即时的信息。