目录
寄存器模型的高级用法
*使用reg_predictor
前面读操作的返回值介绍了左图的方式,这种方式要依赖于driver。当driver将读取值返回后,寄存器模型会更新寄存器的镜像值和期望值,这是寄存器模型的auto predict功能。在建立寄存器模型时使用如下语句打开此功能:rm.default_map.set_auto_predict(1);
除了左图使用driver的返回值更新寄存器模型外,还存在另一种形式如右图所示。这种形式是由monitor将从总线上收集到的transaction交给寄存器模型,后者更新相应寄存器的值。
要使用这种方式更新数据,需要实例化一个reg_predictor,并为这个reg_predictor实例化一个adapter:
class base_test extends uvm_test;
…
reg_model rm;
my_adapter reg_sqr_adapter;
my_adapter mon_reg_adapter;
uvm_reg_predictor#(bus_transaction) reg_predictor;
…
endclass
function void base_test::build_phase(uvm_phase phase);
…
rm = reg_model::type_id::create("rm", this);
rm.configure(null, "");
rm.build();
rm.lock_model();
rm.reset();
reg_sqr_adapter = new("reg_sqr_adapter");
mon_reg_adapter = new("mon_reg_adapter");
reg_predictor = new("reg_predictor", this); //notice
env.p_rm = this.rm;
endfunction
function void base_test::connect_phase(uvm_phase phase);
…
rm.default_map.set_sequencer(env.bus_agt.sqr, reg_sqr_adapter);
rm.default_map.set_auto_predict(1); //notice
reg_predictor.map = rm.default_map;
reg_predictor.adapter = mon_reg_adapter;
env.bus_agt.ap.connect(reg_predictor.bus_in);
endfunction
在connect_phase中需要将reg_predictor和bus_agt的ap口连接在一起,并设置reg_predictor的adapter和map。只有设置了map后,才能将predictor和寄存器模型关联在一起。
当总线上**只有一个主设备(master)**时,左图和右图完全等价。有多个主设备时左图会漏掉某些trasaction。
经过上面代码的设置,事实上存在两条更新寄存器模型的路径:一是上面右图虚线所示的自动预测途径,二是经由predictor的途径。如果要彻底关掉虚线的更新路径:rm.default_map.set_auto_predict(0);
*使用UVM_PREDICT_DIRECT功能与mirror操作
UVM提供mirror操作用于读取DUT中寄存器的值并将它们更新到寄存器模型中。它的函数原型为:
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,
…);
它常用的参数只有前三个。其中第二个参数指的是如果发现DUT中寄存器的值与寄存器模型中的镜像值不一致,那么在更新寄存器模型之前是否给出错误提示。其可选的值为UVM_CHECK和UVM_NO_CHECK。
它有两种应用场景,一是在仿真中不断地调用它,使整个寄存器模型的值与DUT中寄存器的值保持一致,此时check选项是关闭的。二是在仿真即将结束时检查DUT中寄存器的值与寄存器模型中寄存器的镜像值是否一致,这种情况下check选项是打开的。
mirror操作会更新期望值和镜像值。同update操作类似,mirror操作可以在uvm_reg级别和uvm_reg_block级别被调用。当调用一个uvm_reg_block的mirror时,其实质是调用加入其中的所有寄存器的mirror。
前文说过在通信系统中存在大量的计数器。当网络出现异常时借助这些计数器能够快速找出问题所在,所以必须要保证这些计数器的正确性。一般会在仿真即将结束时使用mirror操作检查这些计数器的值是否与预期值一致。
在DUT中的计数器是不断累加的,但寄存器模型中的计数器则保持静止。参考模型会不断统计收到了多少包,那么怎么将这些统计数据传递给寄存器模型呢?
前文中介绍的所有操作都无法完成这个事情,无论是set还是write,或是poke;无论是后门访问还是前门访问。这个问题的实质是想人为地更新镜像值,但同时又不要对DUT进行任何操作。
UVM提供predict操作来实现这样的功能:
function bit uvm_reg::predict (uvm_reg_data_t value,
uvm_reg_byte_en_t be = -1,
uvm_predict_e kind = UVM_PREDICT_DIRECT,
uvm_path_e path = UVM_FRONTDOOR,
…);
其中第一个参数表示要预测的值,第二个参数是byte_en,默认-1的意思是全部有效,第三个参数是预测的类型,第四个参数是后门访问或者是前门访问。第三个参数预测类型有如下几种可以选择:
typedef enum {
UVM_PREDICT_DIRECT,
UVM_PREDICT_READ,
UVM_PREDICT_WRITE
} uvm_predict_e;
read/peek和write/poke操作在对DUT完成读写后也会调用此函数,只是给出的参数是UVM_PREDICT_READ和UVM_PREDICT_WRITE。
要实现在参考模型中更新寄存器模型而又不影响DUT的值,需要使用UVM_PREDICT_DIRECT,即默认值:
task my_model::main_phase(uvm_phase phase);
…
p_rm.invert.read(status, value, UVM_FRONTDOOR);
while(1) begin
port.get(tr);
…
if(value)
invert_tr(new_tr);
counter = p_rm.counter.get();
length = new_tr.pload.size() + 18;
counter = counter + length;
p_rm.counter.predict(counter); //notice
ap.write(new_tr);
end
endtask
在my_model中每得到一个新的transaction,就先从寄存器模型中得到counter的期望值(此时与镜像值一致),之后将新的transaction的长度加到counter中,最后使用predict函数将新的counter值更新到寄存器模型中。predict操作会更新镜像值和期望值。
在测试用例中,仿真完成后可以检查DUT中counter的值是否与寄存器模型中的counter值一致:
class case0_vseq extends uvm_sequence;
…
virtual task body();
…
dseq = case0_sequence::type_id::create("dseq");
dseq.start(p_sequencer.p_my_sqr);
#100000;
p_sequencer.p_rm.counter.mirror(status, UVM_CHECK, UVM_FRONTDOOR); //注意是check模式
…
endtask
endclass
*寄存器模型的随机化与update
前文在向uvm_reg中加入uvm_reg_field时,是将加入的uvm_reg_field定义为rand类型:
class reg_invert extends uvm_reg;
rand uvm_reg_field reg_data;
…
endclass
在将uvm_reg加入uvm_reg_block中时,同样定义为rand类型:
class reg_model extends uvm_reg_block;
rand reg_invert invert;
…
endclass
由此可以判断register_model支持randomize操作。可以在uvm_reg_block级别调用randomize函数,也可以在uvm_reg级别,甚至可以在uvm_reg_field级别调用:
assert(rm.randomize());
assert(rm.invert.randomize());
assert(rm.invert.reg_data.randomize());
但要使某个field能够随机化,只是将其定义为rand类型是不够的。在每个reg_field加入uvm_reg时,要调用其configure函数:
// parameter: parent, size, lsb_pos, access, volatile, reset value, has_reset,
// is_rand, individually accessible
reg_data.configure(this, 1, 0, "RW", 1, 0, 1, 1, 0);
这个函数的第八个参数决定此field是否会在randomize时被随机化。但即使此参数为1,也不一定能够保证此field被随机化。当一个field的类型中没有写操作时,此参数设置是无效的。即此参数只在此field类型为RW、WRC、WRS、WO、W1、WO1时才有效。
因此要避免一个field被随机化,可以在以下三种方式中任选其一:
- 当在uvm_reg中定义此field时,不要设置为rand类型。
- 在调用此field的configure函数时,第八个参数设置为0。
- 设置此field的类型为RO、RC、RS、WC、WS、W1C、W1S、W1T、W0C、W0S、W0T、W1SRC、W1CRS、W0SRC、W0CRS、WSRC、WCRS、WOC、WOS中的一种(没有写操作)。
其中第一种方式也适用于关闭某个uvm_reg或者某个uvm_reg_block的randomize功能。
既然存在randomize,那么也可以为它们定义constraint:
class reg_invert extends uvm_reg;
rand uvm_reg_field reg_data;
constraint cons{
reg_data.value == 0;
}
…
endclass
在施加约束时,要深入reg_field的value变量。
randomize会更新寄存器模型中的预期值:
function void uvm_reg_field::post_randomize();
m_desired = value;
endfunction: post_randomize
这与set函数类似。因此,可以在randomize完成后调用update任务,将随机化后的参数更新到DUT中。这特别适用于在仿真开始时随机化并配置参数。
扩展位宽
在reg_invert的new函数中,调用super.new时的第二个参数是16,这个数字一般表示系统总线的宽度,它可以是32、64、128等。
class reg_invert extends uvm_reg;
rand uvm_reg_field reg_data;
virtual function void build();
reg_data = uvm_reg_field::type_id::create("reg_data");
// parameter: parent, size, lsb_pos, access, volatile, reset value, has_reset, is_rand, indi
reg_data.configure(this, 1, 0, "RW", 1, 0, 1, 1, 0);
endfunction
`uvm_object_utils(reg_invert)
function new(input string name="reg_invert");
//parameter: name, size, has_coverage
super.new(name, 16, UVM_NO_COVERAGE);
endfunction
endclass
但在寄存器模型中,这个数字的默认最大值是64,它是通过一个宏来控制的:
`ifndef UVM_REG_DATA_WIDTH
`define UVM_REG_DATA_WIDTH 64
`endif
如果想要扩展系统总线的位宽,可通过重新定义这个宏来扩展。
与数据位宽相似的是地址位宽也有默认最大值限制,其默认值也是64:
`ifndef UVM_REG_ADDR_WIDTH
`define UVM_REG_ADDR_WIDTH 64
`endif
在默认情况下,字选择信号的位宽等于数据位宽除以8,它通过如下的宏来控制:
ifndef UVM_REG_BYTENABLE_WIDTH
`define UVM_REG_BYTENABLE_WIDTH ((`UVM_REG_DATA_WIDTH-1)/8+1) //notice
`endif
如果想要使用其他值,也可以重新定义这个宏。
寄存器模型的其他常用函数
get_root_blocks
在以前例子中,若某处要使用寄存器模型,则必须将寄存器模型的指针传递过去,如在virtual sequence中使用,需要传递给virtual sequencer:
function void base_test::connect_phase(uvm_phase phase);
…
v_sqr.p_rm = this.rm;
endfunction
UVM还提供其他函数,使得可以在不使用指针传递的情况下得到寄存器模型的指针:
function void uvm_reg_block::get_root_blocks(ref uvm_reg_block blks[$]);
get_root_blocks函数得到验证平台上所有的根块(root block——最顶层的reg_block)。其使用示例如下:
class case0_cfg_vseq extends uvm_sequence;
…
virtual task body();
uvm_status_e status;
uvm_reg_data_t value;
bit[31:0] counter;
uvm_reg_block blks[$];
reg_model p_rm;
…
uvm_reg_block::get_root_blocks(blks);
if(blks.size() == 0)
`uvm_fatal("case0_cfg_vseq", "can't find root blocks")
else begin
if(!$cast(p_rm, blks[0]))
`uvm_fatal("case0_cfg_vseq", "can't cast to reg_model")
end
p_rm.invert.read(status, value, UVM_FRONTDOOR); //notice
…
endtask
endclass
在使用get_root_blocks函数得到reg_block的指针后,要使用cast将其转化为目标reg_block形式( 示例中为reg_model)。以后就可以直接使用p_rm来进行寄存器操作,而不必使用p_sequencer.p_rm。
get_reg_by_offset函数
建立寄存器模型后,可直接通过层次引用的方式访问寄存器:rm.invert.read(…);
但出于某些原因依然要使用地址来访问寄存器模型,可使用get_reg_by_offset函数通过寄存器的地址得到一个uvm_reg的指针,再调用此uvm_reg的read或者write就可以进行读写操作:
virtual task read_reg(input bit[15:0] addr, output bit[15:0] value);
uvm_status_e status;
uvm_reg target;
uvm_reg_data_t data;
uvm_reg_addr_t addrs[];
target = p_sequencer.p_rm.default_map.get_reg_by_offset(addr); //notice
if(target == null)
`uvm_error("case0_cfg_vseq", $sformatf("can't find reg in register model with address: 'h%
target.read(status, data, UVM_FRONTDOOR);
void'(target.get_addresses(null,addrs));
if(addrs.size() == 1)
39 value = data[15:0];
else begin
41 int index;
42 for(int i = 0; i < addrs.size(); i++) begin
43 if(addrs[i] == addr) begin
44 data = data >> (16*(addrs.size() - i));
45 value = data[15:0];
46 break;
47 end
48 end
end
endtask
通过调用最顶层的reg_block的get_reg_by_offset可以得到任一寄存器的指针。如果使用层次的寄存器模型,从最顶层的reg_block的get_reg_by_offset也可以得到子reg_block中的寄存器。
假如buf_blk的地址偏移是’h1000,其中有偏移为’h3的寄存器(即其物理地址是’h1003),那么可以直接由p_rm.get_reg_by_offset('h1003)得到此寄存器,而不必使用p_rm.buf_blk.get_reg_by_offset('h3)。
如果没有使用多地址寄存器则情况比较简单,上述代码会运行第39行。当存在多个地址时,通过get_addresses函数可以得到这个函数的所有地址,其返回值是一个动态数组addrs。无论是大端还是小端,addrs[0]是LSB对应的地址。第41到48行通过比较addrs中的地址与目标地址,最终得到要访问的数据。
写寄存器与读操作类似。