UVM寄存器模型简介
在IC验证中常会模拟软件对硬件进行操作,这个操作就是软件会通过总线来对设计中的一些寄存器进行配置,来修改硬件表现出来的行为。
在实际的情况中,硬件可能有很多寄存器,比如对soc而言,在soc top level可能会有一些寄存器,同时soc中又集成了很多个IP,IP又有不同的submodule,每个IP和submodule又可能拥有属于自己的寄存器,在实际的仿真中,为了便于这些寄存器的统一管理,通常会使用uvm的寄存器模型。
uvm的寄存器模型就是为了方便在实际仿真中读写DUT的寄存器。通常对寄存器的访问又可以分为前门访问(Frontdoor)和后门访问(Backdoor)。前门访问就是模拟实际软件的行为,通过总线来对寄存器读写。由于前门访问会消耗实际仿真时间,对于一个需要大量配置寄存器的DUT来说,为了减小仿真的时间,在前门访问通路都accessable之后,会选择用后门访问的方式来读写这些寄存器。后门访问就是通过RTL hierarchy(dut.a.b.register = xx)的方式,直接把结果写道DUT中,这种方式是不消耗仿真时间的。
uvm的寄存器模型最主要就是为了实现这两种寄存器的访问方式,因此在寄存器模型中必要需要管理访问寄存器所需的address(前门访问)和hierarchy(后门访问)。而管理这两样的就是uvm_reg_map,也就是每个uvm_reg或者uvm_reg_block的default_map。
从uvm的源码来看,寄存器的容器可以分为uvm_reg_field,uvm_reg,uvm_reg_block。其中uvm_reg_field对应的就是实际RTL中的寄存器,以32位总线位宽的宽寄存器模型来说,一个32bit对应一个uvm_reg,一个uvm_reg可能包含多个uvm_reg_field,如图:
class dut_ID extends uvm_reg;
uvm_reg_field REVISION_ID;
uvm_reg_field CHIP_ID;
uvm_reg_field PRODUCT_ID;
function new(string name = "dut_ID");
super.new(name,32,UVM_NO_COVERAGE);
endfunction
virtual function void build();
this.REVISION_ID = uvm_reg_field::type_id::create("REVISION_ID");
this.CHIP_ID = uvm_reg_field::type_id::create("CHIP_ID");
this.PRODUCT_ID = uvm_reg_field::type_id::create("PRODUCT_ID");
this.REVISION_ID.configure(this, 8, 0, "RO", 0, 8'h03, 1, 0, 1);
this.CHIP_ID.configure(this, 8, 8, "RO", 0, 8'h5A, 1, 0, 1);
this.PRODUCT_ID.configure(this, 10, 16,"RO", 0, 10'h176, 1, 0, 1);
endfunction
`uvm_object_utils(dut_ID)
endclass
这个里面最重要的就是configure函数:
// Function: configure
//
// Instance-specific configuration
//
// Specify the ~parent~ register of this field, its
// ~size~ in bits, the position of its least-significant bit
// within the register relative to the least-significant bit
// of the register, its ~access~ policy, volatility,
// "HARD" ~reset~ value,
// whether the field value is actually reset
// (the ~reset~ value is ignored if ~FALSE~),
// whether the field value may be randomized and
// whether the field is the only one to occupy a byte lane in the register.
//
// See <set_access> for a specification of the pre-defined
// field access policies.
//
// If the field access policy is a pre-defined policy and NOT one of
// "RW", "WRC", "WRS", "WO", "W1", or "WO1",
// the value of ~is_rand~ is ignored and the rand_mode() for the
// field instance is turned off since it cannot be written.
//
function void uvm_reg_field::configure(uvm_reg parent,
int unsigned size,
int unsigned lsb_pos,
string access,
bit volatile,
uvm_reg_data_t reset,
bit has_reset,
bit is_rand,
bit individually_accessible);
通过这个函数配置完之后的寄存器大概得到这些信息:
这些信息能够让TB知道寄存器在初始化之后的值以及在一个32bit的地址中的相对位置,还没有具体的前后门可以访问的信息。当他被集成到uvm_reg_block之后;
class dut_regmodel extends uvm_reg_block;
rand dut_ID ID;
function new(string name = "slave");
super.new(name,UVM_NO_COVERAGE);
endfunction
virtual function void build();
// create
ID = dut_ID::type_id::create("ID");
// configure
ID.configure(this,null,"ID");
ID.build();
default_map = create_map("default_map", 'h0, 4, UVM_LITTLE_ENDIAN, 1);
default_map.add_reg(ID, 'h0, "RW");
endfunction
`uvm_object_utils(dut_regmodel)
endclass : dut_regmodel
这里面的几个函数一个一个看:
1. configure函数
// Function: configure
//
// Instance-specific configuration
//
// Specify the parent block of this register.
// May also set a parent register file for this register,
//
// If the register is implemented in a single HDL variable,
// its name is specified as the ~hdl_path~.
// Otherwise, if the register is implemented as a concatenation
// of variables (usually one per field), then the HDL path
// must be specified using the <add_hdl_path()> or
// <add_hdl_path_slice> method.
//
function void uvm_reg::configure (uvm_reg_block blk_parent,
uvm_reg_file regfile_parent=null,
string hdl_path = "");
if (blk_parent == null) begin
`uvm_error("UVM/REG/CFG/NOBLK", {"uvm_reg::configure() called without a parent block for instance \"", get_name(), "\" of register type \"", get_type_name(), "\"."})
return;
end
m_parent = blk_parent;
m_parent.add_reg(this);
m_regfile_parent = regfile_parent;
if (hdl_path != "")
add_hdl_path_slice(hdl_path, -1, -1);
endfunction: configure
configure的第一个参数表示当前reg属于哪个block,,第二个参数是regfile,暂时不讲,最后一个参数就是hdl_path,就是表示这个寄存器在DUT中的hierachy的一个节点,也就是后门路径对应的名字。
其中add_reg函数会把当前的uvm_reg添加到parent参数也就是uvm_reg_block的uvm_reg队列中。
2.build函数
这个函数就是掉用了我们例化uvm_reg_field定义的函数,需要手动调用。
3. create_map函数
uvm_reg_block有一个预先定义好的default_map变量,这个default_map会管理所有寄存器相关的操作,
// Variable: default_map
//
// Default address map
//
// Default address map for this block, to be used when no
// address map is specified for a register operation and that
// register is accessible from more than one address map.
//
// It is also the implicit address map for a block with a single,
// unnamed address map because it has only one physical interface.
//
uvm_reg_map default_map;
//------------------------------------------------------------------------------
//
// Class: uvm_reg_map
//
// :Address map abstraction class
//
// This class represents an address map.
// An address map is a collection of registers and memories
// accessible via a specific physical interface.
// Address maps can be composed into higher-level address maps.
//
// Address maps are created using the <uvm_reg_block::create_map()>
// method.
//------------------------------------------------------------------------------
class uvm_reg_map extends uvm_object;
`uvm_object_utils(uvm_reg_map)
// info that is valid only if top-level map
local uvm_reg_addr_t m_base_addr;
local int unsigned m_n_bytes;
local uvm_endianness_e m_endian;
local bit m_byte_addressing;
local uvm_object_wrapper m_sequence_wrapper;
local uvm_reg_adapter m_adapter;
local uvm_sequencer_base m_sequencer;
local bit m_auto_predict;
local bit m_check_on_read;
local uvm_reg_block m_parent;
local int unsigned m_system_n_bytes;
local uvm_reg_map m_parent_map;
local uvm_reg_addr_t m_parent_maps[uvm_reg_map]; // value=offset of this map at parent level
local uvm_reg_addr_t m_submaps[uvm_reg_map]; // value=offset of submap at this level
local string m_submap_rights[uvm_reg_map]; // value=rights of submap at this level
在调用default_map的add_reg函数:
// add_reg
function void uvm_reg_map::add_reg(uvm_reg rg,
uvm_reg_addr_t offset,
string rights = "RW",
bit unmapped=0,
uvm_reg_frontdoor frontdoor=null);
if (m_regs_info.exists(rg)) begin
`uvm_error("RegModel", {"Register '",rg.get_name(),
"' has already been added to map '",get_name(),"'"})
return;
end
if (rg.get_parent() != get_parent()) begin
`uvm_error("RegModel",
{"Register '",rg.get_full_name(),"' may not be added to address map '",
get_full_name(),"' : they are not in the same block"})
return;
end
rg.add_map(this);
begin
uvm_reg_map_info info = new;
info.offset = offset;
info.rights = rights;
info.unmapped = unmapped;
info.frontdoor = frontdoor;
m_regs_info[rg] = info;
end
endfunction
可以看出来,add_reg函数主要做了两件事,1,像uvm_reg对象中添加map信息 2,default_map保留一份记录register的info,包含了寄存器的偏移地址,access权限还有frontdoor信息。
至此寄存器的基本配置基本完成,后续只需要将寄存器模型集成到环境中。
寄存器模型集成到tb
virtual function void build_phase(uvm_phase phase);
if (regmodel == null) begin
regmodel = dut_regmodel::type_id::create("regmodel",,get_full_name());
regmodel.build();
regmodel.lock_model();
apb = apb_agent::type_id::create("apb", this);
`ifdef EXPLICIT_MON
apb2reg_predictor = new("apb2reg_predictor", this);
`endif
end
begin
string hdl_root = "tb_top.dut";
void'($value$plusargs("ROOT_HDL_PATH=%s",hdl_root));
regmodel.set_hdl_path_root(hdl_root);
end
endfunction
先例化model,最后调用lock_model函数,防止后续添加寄存器,对模型修改。
// Function: lock_model
//
// Lock a model and build the address map.
//
// Recursively lock an entire register model
// and build the address maps to enable the
// <uvm_reg_map::get_reg_by_offset()> and
// <uvm_reg_map::get_mem_by_offset()> methods.
//
// Once locked, no further structural changes,
// such as adding registers or memories,
// can be made.
//
// It is not possible to unlock a model.
//
virtual function void connect_phase(uvm_phase phase);
if (apb != null) begin
reg2apb_adapter reg2apb = new;
regmodel.default_map.set_sequencer(apb.sqr,reg2apb);
`ifdef EXPLICIT_MON
apb2reg_predictor.map = regmodel.default_map;
apb2reg_predictor.adapter = reg2apb;
regmodel.default_map.set_auto_predict(0);
apb.mon.ap.connect(apb2reg_predictor.bus_in);
`else
regmodel.default_map.set_auto_predict(1);
`endif
end
regmodel.print();
endfunctio
其中在调用lock_model的时候也会完成寄存的address initial。
Xinit_address_mapsX();
function void uvm_reg_block::Xinit_address_mapsX();
foreach (maps[map_]) begin
uvm_reg_map map = map_;
map.Xinit_address_mapX();
end
//map.Xverify_map_configX();
endfunction
最后调用map的初始化方法,对所有map的所有registers进行初始化:
foreach (m_submaps[l]) begin
uvm_reg_map map=l;
map.Xinit_address_mapX();
end
foreach (m_regs_info[rg_]) begin
uvm_reg rg = rg_;
m_regs_info[rg].is_initialized=1;
if (!m_regs_info[rg].unmapped) begin
string rg_acc = rg.Xget_fields_accessX(this);
uvm_reg_addr_t addrs[];
bus_width = get_physical_addresses(m_regs_info[rg].offset,0,rg.get_n_bytes(),addrs);
m_regs_info[rg].addr = addrs;
通过 get_physical_addresses可以得到所有寄存器的物理地址,也就是前门地址。
寄存器模型前门访问
以寄存器的write_reg_by_name方法为例:
// write_reg_by_name
task uvm_reg_block::write_reg_by_name(output uvm_status_e status,
input string name,
input uvm_reg_data_t data,
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 rg;
this.fname = fname;
this.lineno = lineno;
status = UVM_NOT_OK;
rg = this.get_reg_by_name(name);
if (rg != null)
rg.write(status, data, path, map, parent, prior, extension);
endtask: write_reg_by_name
其实也就是调用uvm_reg的write方法:
task uvm_reg::write(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 uvm_sequence_base parent = null,
input int prior = -1,
input uvm_object extension = null,
input string fname = "",
input int lineno = 0);
// create an abstract transaction for this operation
uvm_reg_item rw;
XatomicX(1);
set(value);
rw = uvm_reg_item::type_id::create("write_item",,get_full_name());
rw.element = this;
rw.element_kind = UVM_REG;
rw.kind = UVM_WRITE;
rw.value[0] = value;
rw.path = path;
rw.map = map;
rw.parent = parent;
rw.prior = prior;
rw.extension = extension;
rw.fname = fname;
rw.lineno = lineno;
do_write(rw);
status = rw.status;
XatomicX(0);
endtask
创建一个uvm_reg_item的对象,也就是最后seq传输的对象,其中XatomicX
函数是旗语,保证同时只有一个访问:
// ...VIA USER FRONTDOOR
if (map_info.frontdoor != null) begin
uvm_reg_frontdoor fd = map_info.frontdoor;
fd.rw_info = rw;
if (fd.sequencer == null)
fd.sequencer = system_map.get_sequencer();
fd.start(fd.sequencer, rw.parent);
end
// ...VIA BUILT-IN FRONTDOOR
else begin : built_in_frontdoor
rw.local_map.do_write(rw);
end
如果我们再add_reg时添加了frontdoor对象则会优先使用我们定义的函数。否则默认使用我们定义的adapter来进行前门访问。
// uvm_reg_sequence line 147
uvm_reg_item reg_item;
reg_seqr.peek(reg_item);
do_reg_item(reg_item);
reg_seqr.get(reg_item);
#0;
//uvm_reg_sequence line 147
// do_write(uvm_reg_item rw)
task uvm_reg_map::do_write(uvm_reg_item rw);
uvm_sequence_base tmp_parent_seq;
uvm_reg_map system_map = get_root_map();
uvm_reg_adapter adapter = system_map.get_adapter();
uvm_sequencer_base sequencer = system_map.get_sequencer();
if (adapter != null && adapter.parent_sequence != null) begin
uvm_object o;
uvm_sequence_base seq;
o = adapter.parent_sequence.clone();
assert($cast(seq,o));
seq.set_parent_sequence(rw.parent);
rw.parent = seq;
tmp_parent_seq = seq;
end
if (rw.parent == null) begin
rw.parent = new("default_parent_seq");
tmp_parent_seq = rw.parent;
end
if (adapter == null) begin
rw.set_sequencer(sequencer);
rw.parent.start_item(rw,rw.prior);
rw.parent.finish_item(rw);
rw.end_event.wait_on();
end
else begin
do_bus_write(rw, sequencer, adapter);
end
if (tmp_parent_seq != null)
sequencer.m_sequence_exiting(tmp_parent_seq);
endtask
最后do_bus_write函数就是直接调用我们adapter中定义的reg2bus,
adapter.m_set_item(rw);
bus_req = adapter.reg2bus(rw_access);
adapter.m_set_item(null);