构建了寄存器模型之后,那如何将寄存器模型集成到UVM的验证框架中去呢?
1. 总线适配器 adapter
就是说,有了寄存器块reg_block类还不行,还要写一个适配器adapter类。
因为对寄存器模型操作的transaction是uvm_reg_bus_op类的
该类与driver驱动dut的总线bus_trans不一样,uvm_reg_bus_op具有更高的可读性,所以需要一个adapter来作trans类型转换。
1.1. uvm_reg_adapter
例子
class my_adapter extends uvm_reg_adapter;
`uvm_object_utils(my_adapter)
function new(string name="my_adapter");
super.new(name);
provides_responses = 1;
endfunction
function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
bus_trans tr;
tr = bus_trans::type_id::create("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
function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
bus_trans tr;
void'($cast(tr, bus_item));
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
endclass
有一些重要方法和属性
uvm_reg_adapter::reg2bus() 与 uvm_reg_adapter::bus2reg()
这两个方法是必须要实现,因为adapter要实现两种transaction格式的相互转换。
pure virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
pure virtual function void bus2reg(uvm_sequence_item bus_item,
ref uvm_reg_bus_op rw);
uvm_reg_bus_op
该类是寄存器模型的访问transaction格式,从例子中可以看到需要访问这里面的很多成员以转换为bus_trans
typedef enum{UVM_READ, UVM_WRITE} uvm_access_e;
typedef enum{UVM_IS_OK, UVM_IS_X,UVM_NOT_OK} uvm_status_e;
typedef struct{
uvm_reg_addr_t addr;
uvm_reg_data_t data;
uvm_access_e kind;
int n_bits;
uvm_reg_byte_en_t byte_en;
uvm_status_e status;
} uvm_reg_bus_op;
2. 寄存器模型 集成
需要说明的是,寄存器模型并不是用来去改变reference model的,而是用来使用可读性更高的方式对dut的寄存器模块进行控制的。
比起以往将sequence直接产生bus_trans的方式送至sequencer,接下来我们将使用reg_block与reg_adapter向sequencer发送bus_trans。
先介绍如何将reg_block与reg_adapter进行连接。
uvm_reg_map::set_sequencer(sqr,adapter)
reg_block.default_map 不仅可以实现寄存器块、寄存器的地址和读写控制,还可将reg_block与reg_adapter进行连接,并挂载sequencer。
看源码
function void uvm_reg_map::set_sequencer(uvm_sequencer_base sequencer,
uvm_reg_adapter adapter=null);
m_sequencer = sequencer;
m_adapter = adapter;
endfunction
好了,现在将reg_block和reg_adapter绑在一起了,接下来的问题就是将这俩东西放在验证平台的哪个地方。
2.1. 集成于reg_sequence(个人思路)
reg_block与adapter是绑定的,这俩是例化在env内呢?还是test内呢?还是其他什么地方?
笔者个人的思路是放在reg_sequence内。
原因很简单:尽量将reg_block与reg_adapter在sequence的封装下,这样来套用原来virtual sequence的思路。
而且反正最后都要给sequencer发一个bus_trans,而且二者还都是object类型的,容易理解。
uvm_reg_sequence
uvm_reg_sequence有针对reg和mem独有的读、写等方法,可以从uvm_reg_sequence继承一个新的类reg_sequence
注意读的数据不是寄存器模型的数据!!!是dut中寄存器的数据!!!所以根据不同的总线会产生不同的耗时!!!
本质上是driver在生成rsp时的耗时,因为reg_sequence是通过接受driver的反馈trans,即rsp判断读写是否成功的!!! 所以如果driver的run_phase写错了,可能会给reg_sequence带来错误!
那么有个问题:如何判断寄存器模型中的数据和dut寄存器的数据是一致的?
virtual task write_reg( input uvm_reg rg,
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 int prior = -1,
input uvm_object extension = null,
input string fname = "",
input int lineno = 0);
if (rg == null)
`uvm_error("NO_REG","Register argument is null")
else
rg.write(status,value,path,map,this,prior,extension,fname,lineno);
endtask
virtual task read_reg(input uvm_reg rg,
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 int prior = -1,
input uvm_object extension = null,
input string fname = "",
input int lineno = 0);
virtual task poke_reg(input uvm_reg rg,
output uvm_status_e status,
input uvm_reg_data_t value,
input string kind = "",
input uvm_object extension = null,
input string fname = "",
input int lineno = 0);
virtual task peek_reg(input uvm_reg rg,
output uvm_status_e status,
output uvm_reg_data_t value,
input string kind = "",
input uvm_object extension = null,
input string fname = "",
input int lineno = 0);
virtual task update_reg(input uvm_reg rg,
output uvm_status_e status,
input uvm_path_e path = UVM_DEFAULT_PATH,
input uvm_reg_map map = null,
input int prior = -1,
input uvm_object extension = null,
input string fname = "",
input int lineno = 0);
virtual task mirror_reg(input uvm_reg rg,
output uvm_status_e status,
input uvm_check_e check = UVM_NO_CHECK,
input uvm_path_e path = UVM_DEFAULT_PATH,
input uvm_reg_map map = null,
input int prior = -1,
input uvm_object extension = null,
input string fname = "",
input int lineno = 0);
例码
具体实现方式见下面例码,注意reg_seq中reg_block和reg_adapter的例化和挂载都在reg_seq.body() 中实现了,所以使用使只需调用一次`uvm_do_on即可。
class reg_sequence extends uvm_reg_sequence;
`uvm_object_utils(reg_sequence)
my_rgm rgm;
my_adapter adapter;
task body();
rgm = my_rgm::type_id::create("rgm");
rgm.build();
adapter = my_adapter::type_id::create("adapter");
rgm.default_map.set_auto_predict(1);
rgm.default_map.set_sequencer(m_sequencer,adapter);
rgm.reset();
endtask
endclass
class base_virtual_sequence extends uvm_sequence;
`uvm_declare_p_sequencer(virtual_sequencer)
reg_sequence reg_seq;
task body();
do_reg();
endtask
virtual task do_reg();
endtask
endclass
class consistence_basic_virtual_sequence extends base_virtual_sequence;
task do_reg();
bit[31:0] wr_val, rd_val;
uvm_status_e status;
wr_val = (1<<3)+(0<<1)+1;
`uvm_do_on(reg_seq,p_sequencer.reg_sqr)
reg_seq.write_reg(reg_seq.rgm.chnl0_ctrl_reg,status,wr_val,UVM_FRONTDOOR);
reg_seq.read_reg(reg_seq.rgm.chnl0_ctrl_reg,status,rd_val,UVM_FRONTDOOR);
void'(this.diff_value(wr_val, rd_val, "SLV0_WR_REG"));
endtask
endclass
注意个人非常想在consistence_basic_virtual_sequence::do_reg(); 里将读写寄存器用一句话uvm_do_on_with(req_seq,p_sequencer.reg_sqr,{kind == UVM_READ; reg == reg_seq.rgm.ctrl_reg;}) 等等 但是不行,因为uvm_do 是含有uvm_create 的,所以每次调用都回重新创建以一个新的reg_sequence,进而reg_block每次都是新创建的。
2.2. 集成于test,再向底层配置
来源于参考资料的思路:在test的build_phase中例化并通过config机制配置到env.rgm和env.adapter中,之后在env的build_phase中接收,
之后在connect_phase执行reg_block.default_map.set_sequencer() 方法,并配置到virtual_sequencer.rgm 句柄中。
访问的话,在virtual_sequence中通过p_sequencer.rgm 执行访问。
笔者认为这个过程略微复杂,不易理解。
3. 寄存器模型 访问
将reg_block、reg_adapter和sequencer蓝上了,那么如何对寄存器模型进行访问以实现对dut的硬件寄存器进行访问呢?
3.1. 前门访问
前门访问是指通过总线(APB协议、OPB协议、I2C协议等)上的通过时序对dut的寄存器进行读写访问。
实际上在2.1.节中的consistence_basic_virtual_sequence ::do_reg(); 中就已经实现了前门访问,是借助uvm_reg_sequence::write_reg() 与uvm_reg_sequence::read_reg() 实现的。
访问流程
可以从源码中查看整个访问流程,这里简单说一下:本质上还是调用的uvm_reg::write() 和uvm_reg::read() 实现的
内部产生一个uvm_reg_item rw 的变量,记载着访问的属性等内容,然后将它传给reg_sequence.rgm.default_map 。
别忘了default_map 已经与adapter 和reg_sequencer 建立了连接,然后default_map 调用
task uvm_reg_map::do_write(uvm_reg_item rw);
do_bus_write(rw, sequencer, adapter);
endtask
task uvm_reg_map::do_bus_write (uvm_reg_item rw,
uvm_sequencer_base sequencer,
uvm_reg_adapter adapter);
uvm_reg_bus_op rw_access=accesses[i];
uvm_sequence_item bus_req;
bus_req = adapter.reg2bus(rw_access);
rw.parent.start_item(bus_req,rw.prior);
rw.parent.finish_item(bus_req);
bus_req.end_event.wait_on();
if (adapter.provides_responses) begin
uvm_sequence_item bus_rsp;
rw.parent.get_base_response(bus_rsp);
adapter.bus2reg(bus_rsp,rw_access);
end
endtask
adapter.reg2bus() 、reg_sequence.start_item() 、reg_sequence.finish_item() 和adapter.bus2reg() 以实现trans的发送反馈和格式转化。
显然trans反馈需要根据dut中总线协议来的,所以一定会耗时。
3.2. 后门访问
后门访问是指直接访问dut内部的寄存器变量。其实就是把dut里的reg 变量扒开看数值。
不太正规,但是不耗仿真时间,通过UVM DPI完成。
使用后门访问的时候,只需将UVM_FRONTDOOR 改成UVM_BACKDOOR 即可,例如
reg_seq.write_reg(reg_seq.rgm.chnl0_ctrl_reg,status,wr_val,UVM_BACKDOOR);
reg_seq.read_reg(reg_seq.rgm.chnl0_ctrl_reg,status,rd_val,UVM_BACKDOOR);
建立映射
需要将reg_block内的寄存器与dut中的reg型变量作映射。
在reg_block::build() 内完成,例子如下
class reg_block extends uvm_reg_block;
virtual function build();
add_hdl_path("tb.dut.ctrl_regs_inst");
chnls_ctrl_reg[0].add_hdl_path_slice($sformats("mem[%0d]",`SLV0_RW_REG),1,16);
lock_model();
endfunction
endclass
而在tb.dut.ctrl_regs_inst 的module块中是这么定义的
module ctrl_regs( input clk_i,
input rstn_i,
);
reg [`CMD_DATA_WIDTH-1:0] mem [5:0];
endmodule
使用了以下方法。
注意一般来说dut中一个寄存器都单独一个module,进而对应于reg_sequence中的一个reg_block,所以reg_block类内一般只有一句add_hdl_path
function void uvm_reg_block::add_hdl_path(string path, string kind = "RTL");
function void uvm_reg::add_hdl_path_slice(string name,
int offset,
int size,
bit first = 0,
string kind = "RTL");
3.3. 比较与应用
前门访问和后门访问最大的区别就在于协议时序。后门仿真不参照协议,所以读写数据不占用仿真时间、立即进行的。而前门访问是按照总线协议的时序来的,所以可能会阻塞住占用仿真时间。
如下表
项目 | 前门访问 | 后门访问 |
---|
访问方式 | 总线协议 | UVM DPI | 时序 | 依赖协议时序,消耗仿真时间 | 直接读取,不消耗仿真时间 | 功能 | 按字(32bit)读写,不可读写寄存器域 | 可对域读写 | 预测 | 检测总线作预测 | auto prediction |
下面讲混合应用
物理通路
就是说使用前门能否按照协议说的那样读写数据,例如只写一次的寄存器,我写一次写进去了,再写发现不能写了,OK
而且,通过前门写进去的数据对不对啊。就可以先前门写、再后门看,最后前门读,以防止寄存器模型和dut寄存器模块地址不匹配的情况
随机化场景
想让dut中的寄存器为随机值的情况下观察响应,但随机值毕竟是随机的所以不能通过前门作配置,用后门。
方法就是先把软件一侧的寄存器模型随机化,然后将这些随机了的数值通过后门配置给硬件一侧的寄存器,然后再使用软件的寄存器模型对dut寄存器进行配置,观察是否有边界情况的产生。
|