总线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("GETVIF", "no virual interface is assigned")
end
endfunction
endclass
class mcdf_bus_monitor extends uvm_monitor;
virtual macdf_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", $sofrmatf("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 dirver_bus(mcdf_trans t);
case(t.cmd)
`WRITE: drive_write;
`READ : drive_read;
`IDLE: drive_idle(1);
default : `uvm_error("DRIVE", "invaild mcdf command type received!")
endcase
endtask
task drive_wirte(mcdf_bus_trans t);
@(posedge vif.clk)
vif.cmd <= t.cmd;
vif.addr <= t.addr;
vif.wdata <= t.wdata;
endtask
task driver_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
class mcdf_bus_agent extends uvm_agent;
mcdf_bus_driver driver;
mcdf_bus_sequencer sequencer;
mcdf_bus_monitor monitor;
`uvm_component_utils(mcdf_bus_agent)
...
function void build_phase(uvm_phase phase);
driver = mcdf_bus_driver::type_id ::create("driver", this);
sequencer = mcdf_bus_sequencer::type_id::create("sequencer", this);
monitor = mcdf_bus_monitor::type_id::create("monitor", this);
endfunction
function void connect_phase(uvm_phase phase);
driver.seq_item_port.connect(sequencer.seq_item_export);
endfunction
class
三种命令模式IDLE、WRITE和READ,并且在READ模式下,将读回的数据通过item_done(rsp)写回到sequencer和squence一侧,通过clone()命令创建RSP对象后,通过set_sequncer_id()和set_transaction_id()两个函数保证REQ和RSP的中保留的ID信息一致。
`define IDLE 2'b00
`define WRITE 2'b01
`define READ 2'b10
`define SLV0_RW_ADDR 8'h00
...
`define SLV0_R_ADDR 8'h10
...
`define SLV0_RW_REG 0
...
`define SLV0_R_REG 3
...
module ctrl_regs(
clk_i, rstn_i,cmd_i, cmd_addr_i,cmd_data_i, cmd_data_o, slv0_len_o...,slv0_prio_o..., slv0_margin_i..., slv0_en_o...)
input clk_i,rstn_i;
input [1:0] cmd_i;
input [7:0] cmd_addr_i;
input [31:0] cmd_data_i;
input [7:0] slv0_margin_i;//表fifo余量
...
output [31:0] cmd_data_o;
output [2:0] slv0_len_o;//fmt
...
output [2:0] slv0_prio_o;//aribter
...
output slv0_en_o;//chnl
...
reg [31:0] regs [5:0];
reg [31:0] cmd_data_reg;
always@(posedge clk_i or negedge rstn_i) begin//异步复位
if(!rstn_i) begin
regs [`SLV0_RW_REG] <= 32'h00000007;
...
regs [`SLV0_R_REG] <= 32'h000000010;
...
end
else begin
if (cmd_i == `WRITE) begin
case(cmd_addr_i)
`SLV0_RW_ADDR: regs['SLV0_RW_REG][5:0] <= cmd_data_i;
...
endcase
end
else if(cmd_i === `READ) begin
case(cmd_addr_i)
`SLV0_RW_ADDR: cmd_data_reg <= regs[`SLV0_RW_REG];
...
`SLV0_R_ADDR: cmd_data_reg <= regs[`SLV0_R_REG];
endcase
end
regs[`SLV0_R_REG][7:0] <= slv0_margin_i;
...
end
assign cmd_data_o = cmd_data_reg;
assign slv0_len_o = regs[`SLV0_RW_REG][5:3];
...
assign slv0_prio_o = regs[`SLV0_RW_REG][2:1];
...
assign slv0_en_o regs[`SLV0_RW_REG][0];
...
endmodule: ctrl_regs
以上为MCDF寄存器设计代码
Adapter的实现
在具备mcdf的总线uvc之后,需要实现adapter。总线对应的adapter所完成的桥接功能是指在uvm_reg_bus_op和总线transaction之间的转换。在开发某一个总线adapter时,需要实现以下几点:
uvm_reg_bus_op和总线transaction中各自的数据映射
实现reg2bus()和bus2reg()两个函数,这两个函数及对应两种transaction中的数据映射
如果总线支持byte访问,可以使能supports_byte_enable;如果总线要返回response数据,则应当使能provides_responses。如果总线UVC不支持返回RSP(没有调用put_response(RSP)或者item_done(RSP)), 那么不应该置此位。
Adapter示例
class reg2mcdf_adapter extends uvm_reg_adapter;
`uvm_object_utils(reg2mcdf_adapter)
function new(string name = "mcdf_bus_trans");
super.new(name);
provide_response = 1;
endfunction
function uvm_sequnce_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;
returm t;
endfunction
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 t;
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_drvier在发起总线访问之后会将RSP一并返回至sequencer。
regsbus()完成桥接的场景是,如果用户在寄存器级别做了操作,那么寄存器级别操作的信息会被记录,同时调用uvm_reg_adapter::reg2bus()函数。在完成了将uvm_reg_bus_op的信息映射到mcdf_bus_trans之后,函数将uvm_bus_trans实例返回,而在返回uvm_bus_trans之后,该实例将通过mcdf_bus_sequncer传入到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_block是uvm_object类型,因此预定义的build()函数不会自动执行,还需要单独调用。
在还未集成predictor之前,我们采用auto_predictor的方式,因此调用了函数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("GETRRGM", "no top-down RGM handle is addigned", UVM_LOW)
rgm = mcdf_rgm::type_id::create("rgm" ,this);
`uvm_info("NEWRGM", "create rgm instance locally", UVM_LOW)
end
rgm.build();
rgm.map.set_auto_predict();
reg2mcdf = reg2mcdf_adapter::type_id::create("reg2bus");
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
访问方式
利用寄存器模型,我们很方便对寄存器进行操作,分为两种访问方式,及前门访问(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_FORNTDOOR。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.chnl0_ctrl_reg.read(status, data, UVM_FRONTDOOR, .parnet(this));
rgm.chnl0_ctrl_reg.write(status, 'h11, UVM_FRONTDOOR, .parnet(this));
rgm.chnl0_ctrl_reg.read(status, data, UVM_FRONTDOOR, .parnet(this));
//pre-defined methods access
read_reg(rgm.chnl1_ctrl_reg, status, data, UVM_FORNTDOOR);
write_req(rgm.chnl1_ctrl_reg, status, 'h22, UVM_FORNTDOOR);
read_reg(rgm.chnl1_ctrl_regf, 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");
chnl0_ctrl_reg.add_hdl_path_slice($sformatf("regs[%0d]", `SLV0_RW_REG), 0, 32);
...
chnl0_stat_reg.add_hdl_path_slice($sformatf("regs[%0d]", `SLV0_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)和修改寄存器(poke)两种操作,而用户无需指定访问方式为UVM_BACKDOOR,这两个方法只针对后门访问
后门访问示例
class modf_example_seq extends uvm_reg_sequence;
mcdf_rgm rgm;
`uvm_object_utils(mcdf_example_seq)
`uvm_declare_p_sequencer(mcdf_bus_sequencer)
...
task body();
uvn_status_e status;
uvm_reg_data_t data;
if(!uvm_config_db#(mcdf_rgm)::get(null, get_full_name(), "ram", rgm)) begin
`uvm_error("GETRGM", "no top-down RGM handle is assianed")
end
//reqister model access write()/read()
rgm.chnl0_ctrl_req.read(status, data, UVM_BACKDOOR, .parent(this));
rgm.chnl0_ctrl_req.write(status, 'h11, UVM_BACKDOOR, .parent(this));
rgm.chnl0_ctrl_req.read(status, data, UVM_BACKDOOR, .parent(this));
//register model access poke()/peek()
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_req, status, data, UVM_BACKDOOR);
write_reg(rgm.chn12_ctrl_req, status, 'h22, UVM_BACKDOOR);
read_reg(rgm.chn12_ctrl_req, 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在当前时刻的余量。因此对于需要要求更严格的测试场景,除了需要前门和后门来访问寄存器,也需要映射一些重要的信号来反映第一时间的信息。