有了模型了,那么如何对寄存器模型进行访问以实现对dut的硬件寄存器进行访问呢?包括两种访问模式,前门访问与后门访问
前门访问和后门访问最大的区别就在于协议时序。
后门仿真不参照协议,所以读写数据不占用仿真时间、是立即进行的。
而前门访问是按照总线协议的时序来的,所以可能会阻塞住占用仿真时间。
如下表
项目 | 前门访问 | 后门访问 |
---|
访问方式 | 总线协议 | UVM DPI | 时序 | 依赖协议时序,消耗仿真时间 | 直接读取,不消耗仿真时间 | 功能 | 按字(32bit)读写,不可读写寄存器域 | 可对域读写 | 预测 | 检测总线作预测 | auto prediction |
1. 前门访问
前门访问是指通过总线(APB协议、OPB协议、I2C协议等)上的通过时序对dut的寄存器实际值进行读写访问。
与硬件寄存器一样,寄存器模型也有域、寄存器、寄存器组、基地址和偏移地址的概念,但是软件侧寄存器模型中的域是有两种值的:
● 域的 期望值(desired value):顾名思义,我们想要为该域配置的值
● 域的 镜像值(mirror value):反应硬件侧寄存器该域的值
而硬件侧寄存器域的值也称实际值(actual value)
比如,复位之后某域的期望值、镜像值和实际值均为’h00。然后我们想配置成’h01,所以设置期望值为’h01,然后用寄存器模型写入,发现这是个只读寄存器,所以实际值为’h00,镜像值也保持为’h00,期望值也归为’h00 如果该域可写,那么一看期望值是’h01,所以实际值就通过总线写变成了’h01,寄存器模型一看硬件那边域成了’h01,自己这边的镜像值就变为了’h01
先上一个访问方法
| 前门访问 | 后门访问 | |
---|
方法名 | uvm_reg_sequence | uvm_reg_block | uvm_reg | uvm_reg_field | uvm_reg_sequence | uvm_reg_block | uvm_reg | uvm_reg_field | 功能 |
---|
write / read | 只有write_reg / read_reg | 无 | 有 | 无 | 只有write_reg / read_reg | 无 | 有 | 有 | 将传入参数写 / 读实际值,再更改期望值和镜像值 |
---|
set / get | 无 | 无 | 有 | 有 | | 设定 / 获取期望值 |
---|
update / mirror | 只有update_reg / mirror_reg | 有 | 有 | 无 | 只有update_reg / mirror_reg | 有 | 有 | 无 | 将期望值写 / 读实际值,再更改期望值和镜像值 |
---|
reset / get_reset | 无 | 有 / 无 | 有 | 有 | | 进行 / 获取 复位期望值与镜像值 |
---|
无表示该类没有该方法,有则表示该类有该方法。 有update方法的类,一定有mirror方法,反之也成立。 uvm_reg_block前门后门都无法write / read,但前后门都有update / mirror uvm_reg_field无法前门write / read
下面介绍一些常用访问方法和方法组,无论哪个方法,只需记住任何方法,最终都会为期望值和镜像值赋予实际值,以三者相同。
1.1. 写 实际值
write和update都实现了写入实际值,此处介绍二者的流程和区别。
write
功能是这样的,写入实际值,之后再更新期望值与镜像值 为实际值
可以从源码中查看整个访问流程,uvm_reg_sequence::write_reg 本质上还是调用的uvm_reg::write() 实现的,见源码
task uvm_reg::write(output uvm_status_e status,
input uvm_reg_data_t value,
);
uvm_reg_item rw;
set(value);
rw = uvm_reg_item::type_id::create("write_item",,get_full_name());
rw.kind = UVM_WRITE;
rw.value[0] = value;
do_write(rw);
endtask
task uvm_reg::do_write (uvm_reg_item rw);
case (rw.path)
UVM_FRONTDOOR: begin
else begin : built_in_frontdoor
rw.local_map.do_write(rw);
end
if (system_map.get_auto_predict()) begin
do_predict(rw, UVM_PREDICT_WRITE);
end
end
endcase
endtask: do_write
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;
rw_access.kind = rw.kind;
rw_access.addr = addrs[i];
rw_access.data = data;
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
从源码中可以看出,先是根据写的数据啥的构造一个uvm_reg_item rw; 如果是UVM_FRONTDOOR,那么就会调用reg所在的reg_block的default_map.do_write() 。 然后这个uvm_reg_map::do_bus_write() ,其实就是将rw又转化成uvm_reg_bus_op rw_access ,再调用adapter.reg2bus(rw_access) 得到总线trans的父类变量uvm_sequence_item bus_req; 。 之后如同之前讲的一样,调用start_item 、finish_item 、get_base_response ,实现sequence发送req、获得rsp整个过程(注意此处隐含有类型转换,uvm_reg_item转bus_trans)。获得rsp后又转化为uvm_reg_bus_op rw_access ,但这个反馈量最终没有被使用!!! 然后,如果system_map.get_auto_predict() == 1 ,就执行do_predict(rw, UVM_PREDICT_WRITE); ,是这一步实现了期望值和镜像值的更新,叫作自动预测,下文讲
set、update
set的作用是,设定该reg的期望值
update的本质就是write,不过是将期望值 写入实际值,再更新期望值和镜像值 为实际值,且reg_block有update方法无write方法!!!
见源码
function void uvm_reg::set(uvm_reg_data_t value,
string fname = "",
int lineno = 0);
foreach (m_fields[i])
m_fields[i].set((value >> m_fields[i].get_lsb_pos()) &
((1 << m_fields[i].get_n_bits()) - 1));
endfunction: set
function void uvm_reg_field::set(uvm_reg_data_t value,
string fname = "",
int lineno = 0);
case (m_access)
"RO": m_desired = m_desired;
"RW": m_desired = value;
endcase
this.value = m_desired;
endfunction: set
可见,set也是每个域调用自己的set方法,并且会根据各域的存取方式,决定如何修改期望值。 期望值并不完全是我期望是几,就 能改成几
task uvm_reg::update(output uvm_status_e status,
input uvm_path_e path = UVM_DEFAULT_PATH,
input uvm_reg_map map = null,
);
uvm_reg_data_t upd = 0;
foreach (m_fields[i])
upd |= m_fields[i].XupdateX() << m_fields[i].get_lsb_pos();
write(status, upd, path, map, parent, prior, extension, fname, lineno);
endtask: update
function uvm_reg_data_t uvm_reg_field::XupdateX();
XupdateX = 0;
case (m_access)
"RO": XupdateX = m_desired;
"RW": XupdateX = m_desired;
endcase
XupdateX &= (1 << m_size) - 1;
endfunction: XupdateX
可以看出,uvm_reg::update 其实就是执行的uvm_reg::write ,只不过写的值是uvm_reg_field::m_desired 期望值。 注意write中也有do_predict自动预测 注意update无需传入数据value
1.2. 读 实际值
read和mirror都可以会读实际值,但二者有区别
read
功能是这样的,读出实际值,之后再更新期望值与镜像值 为实际值
read的源码就不再介绍了,与write类似。注意read最终也会调用uvm_reg_field::do_predict 以改变各域的期望值和镜像值。
mirror、get
get的作用是,获取该reg的期望值
mirror的本质就是read,但不会读出实际值,会更新期望值与镜像值 为实际值,并可做实际值和更新前镜像值的匹配检查,且reg_block有mirror方法无read方法!!!
task uvm_reg::mirror(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 uvm_sequence_base parent = null,
input int prior = -1,
input uvm_object extension = null,
input string fname = "",
input int lineno = 0);
uvm_reg_data_t v;
if (check == UVM_CHECK)
exp = get_mirrored_value();
XreadX(status, v, path, map, parent, prior, extension, fname, lineno);
if (check == UVM_CHECK)
void'(do_check(exp, v, map));
endtask: mirror
task uvm_reg::XreadX(output uvm_status_e status,
output uvm_reg_data_t value,
);
uvm_reg_item rw;
rw = uvm_reg_item::type_id::create("read_item",,get_full_name());
rw.element = this;
rw.element_kind = UVM_REG;
rw.kind = UVM_READ;
do_read(rw);
endtask: XreadX
task uvm_reg::do_read(uvm_reg_item rw);
do_predict(rw, UVM_PREDICT_READ);
endtask
function bit uvm_reg::do_check(input uvm_reg_data_t expected,
input uvm_reg_data_t actual,
uvm_reg_map map);
foreach(m_fields[i]) begin
uvm_reg_data_t val = actual >> m_fields[i].get_lsb_pos() & mask;
uvm_reg_data_t exp = expected >> m_fields[i].get_lsb_pos() & mask;
if (val !== exp) begin
`uvm_info("RegModel",
$sformatf("Field %s (%s[%0d:%0d]) mismatch read=%0d'h%0h mirrored=%0d'h%0h ",
)
end
end
endfunction
从上面几个源码可知道,mirror本质就是读实际值,不过不会把读出的实际值给我们看,读完了照样更改期望值和镜像值。 当check == UVM_CHECK 时,可将实际值和读之前的镜像值作匹配检查,检查结果通过`uvm_info打印出来
1.3. uvm_reg::do_predict
前文讲到,前门访问是通过创建一个uvm_reg_item rw 表达了全部的访问信息,包括存取、数值、状态等等。
然后default_map 基于rw实现总线的访问。
访问结束之后,有一步uvm_reg::do_predict(rw,...) ,是这一步实现了期望值和镜像值统一为实际值,这一步叫作预测
预测的含义非常简单,指通过总线事务来判断dut寄存器那边值的变化,而不是通过二次读
源码如下
function void uvm_reg::do_predict(uvm_reg_item rw,
uvm_predict_e kind = UVM_PREDICT_DIRECT,
uvm_reg_byte_en_t be = -1);
foreach (m_fields[i]) begin
m_fields[i].do_predict(rw, kind, be>>(m_fields[i].get_lsb_pos()/8));
end
endfunction: do_predict
function void uvm_reg_field::do_predict(uvm_reg_item rw,
uvm_predict_e kind = UVM_PREDICT_DIRECT,
uvm_reg_byte_en_t be = -1);
uvm_reg_data_t field_val = rw.value[0] & ((1 << m_size)-1);
case (kind)
UVM_PREDICT_WRITE:
begin
field_val &= ('b1 << m_size)-1;
end
UVM_PREDICT_READ:
begin
field_val &= ('b1 << m_size)-1;
end
UVM_PREDICT_DIRECT:
endcase
m_mirrored = field_val;
m_desired = field_val;
this.value = field_val;
endfunction: do_predict
调用uvm_reg::do_predict() ,其实就是每个域调用自己的uvm_reg_field::do_predict(); 而在uvm_reg_field::do_predict(); 中可以看到,无论怎么算,最终都会将镜像值uvm_reg_field::m_mirrored 、期望值uvm_reg_field::m_desired 和没名字的值uvm_reg_field::value 更新成相同的值,而这个相同的值曾在default_map.do_write() 用于更新实际值
uvm_reg_map::set_auto_predict(1)
但是别忘了在前门访问时,无论是uvm_reg::do_write() 还是uvm_reg::do_read() 都先判断一个条件才执行do_predict。
task uvm_reg::do_write (uvm_reg_item rw);
if (system_map.get_auto_predict()) begin
status = rw.status;
do_predict(rw, UVM_PREDICT_WRITE);
rw.status = status;
end
endtask
function bit get_auto_predict(); return m_auto_predict; endfunction
function void set_auto_predict(bit on=1); m_auto_predict = on; endfunction
也就是说,只有开启uvm_reg_map::m_auto_predict == 1 才会执行uvm_reg::do_predict() ,并且该方法是基于reg产生的对象rw!
意思是,期望值和镜像值的更新是基于交给dut的总线trans的!不是driver反馈来的rsp!!!!
这叫做自动预测。
那么问题来了,如果其他处理器通过总线对dut寄存器进行了操作,就不会存在rw这个东西,这种自动预测就无效了!所以UVM又引入了predictor
uvm_reg_predictor extends uvm_component
是的,这个是个component!
reg_predictor的核心在于,通过解析monitor监视的总线操作,将监视到的bus_trans转化为uvm_reg_bus_op,再转化为uvm_reg_item,最后实现期望值和镜像值的更新!
这就解决了上述问题,总线上对dut的任意操作都会被monitor监视到!
predictor集成
predictor内有一个uvm_analysis_imp类型端口,用于接受monitor发来的bus_trans,并在uvm_reg_predictor::write() 内实现类型转换和显式预测。
在uvm_reg_predictor::write() 内,predictor借助adapter执行bus2reg,然后借助map实现reg索引和do_predict
源码如下:
class uvm_reg_predictor #(type BUSTYPE=int) extends uvm_component;
uvm_analysis_imp #(BUSTYPE, uvm_reg_predictor #(BUSTYPE)) bus_in;
uvm_reg_map map;
uvm_reg_adapter adapter;
virtual function void write(BUSTYPE tr);
uvm_reg rg;
uvm_reg_bus_op rw;
adapter.bus2reg(tr,rw);
rg = map.get_reg_by_offset(rw.addr, (rw.kind == UVM_READ));
local_map = rg.get_local_map(map,"predictor::write()");
if (reg_item.kind == UVM_READ && local_map.get_check_on_read() && reg_item.status != UVM_NOT_OK)
void'(rg.do_check(ir.get_mirrored_value(), reg_item.value[0], local_map));
rg.do_predict(reg_item, predict_kind, rw.byte_en);
endfunction
endclass
注意参数类。可以看出一个write就实现了期望值和镜像值更新 使用uvm_reg_predictor,把自动预测关了就行reg_block::default_map.set_auto_predict(0); ,当然默认是关的
给个predictor集成的例子
class reg_agent extends uvm_agent;
uvm_reg_predictor #(reg_trans) predictor;
reg_monitor monitor;
function void connect_phase(uvm_phase);
monitor.mon_ana_port.connect(predictor.bus_in);
endfunction
endclass
class base_test extends uvm_test;
base_virtual_sequence v_seq;
function void connect_phase(uvm_phase phase);
env.reg_agt.predictor.map = v_seq.rgm.map;
env.reg_agt.predictor.adapter = v_seq.adapter;
endfunction
endclass
1.4. 前门访问流程(以write为例)
第一幅图,设定reg_block.default_map.set_auto_predict(1); ,整个写的主要流程如下:
红色表示总线写入过程,绿色表示自动预测过程,红色步骤全部完成之后才进行蓝色的步骤,带括号的表示方法。
可以看出,map的作用不仅仅是为reg定义基地址和存取方式,还在其中与adapter进行通信 第二幅图,设定reg_block.default_map.set_auto_predict(0); ,整个写的主要流程如下:
红色表示总线写入过程,绿色表示自动预测过程,红色步骤全部完成之后才进行蓝色的步骤,带括号的表示方法。
predictor收到bus_trans,之后调用adapter作bus2reg,然后需要map根据偏移地址确定是哪个reg,然后才执行do_predict
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");
|