简单的寄存器模型
只有一个寄存器的寄存器模型
为DUT建立寄存器模型:这个DUT非常简单,只有一个寄存器invert。要为其建造寄存器模型, 首先要从uvm_reg派生一个invert类:
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, individually accessible
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
new函数中要将invert寄存器的宽度作为参数传递给super.new函数。这里的宽度并不是指这个寄存器的有效宽度,而是指这个寄存器中总共的位数。如对于一个16位的寄存器,其中可能只使用了8位,那么这里要填写的是16。这个数字一般与系统总线的宽度一致。super.new中另一个参数是是否要加入覆盖率的支持,这里选择UVM_NO_COVERAGE,即不支持。
每个派生自uvm_reg的类都有一个build,这与uvm_component的build_phase并不一样,它不会自动执行而需手工调用,所有uvm_reg_field都在这里实例化。当reg_data实例化后,要调用data.configure函数来配置这个字段。
- 第一个参数就是此域(uvm_reg_field)的父辈,也即此域位于哪个寄存器中,这里是填写this。
- 第二个参数是此域的宽度,由于DUT中invert的宽度为1,所以这里为1。
- 第三个参数是此域的最低位在整个寄存器中的位置,从0开始计数。假如一个寄存器如下图所示,其低3位和高5位没有使用,只有一个有效宽度为8位的字段,那么在调用configure时第二个参数就要填写8,第三个参数则要填写3,因为此reg_field是从第4位开始的。
- 第四个参数表示此字段的存取方式。 UVM共支持如下25种存取方式:(需要时搜索即可)
1) RO: 读写此域都无影响。
2) RW: 会尽量写入, 读取时对此域无影响。
3) RC: 写入时无影响, 读取时会清零。
4) RS: 写入时无影响, 读取时会设置所有的位。
5) WRC: 尽量写入, 读取时会清零。
6) WRS: 尽量写入, 读取时会设置所有的位。
7) WC: 写入时会清零, 读取时无影响。
8) WS: 写入时会设置所有的位, 读取时无影响。
9) WSRC: 写入时会设置所有的位, 读取时会清零。
10) WCRS: 写入时会清零, 读取时会设置所有的位。
11) W1C: 写1清零, 写0时无影响, 读取时无影响。
12) W1S: 写1设置所有的位, 写0时无影响, 读取时无影响。
13) W1T: 写1入时会翻转, 写0时无影响, 读取时无影响。
14) W0C: 写0清零, 写1时无影响, 读取时无影响。
15) W0S: 写0设置所有的位, 写1时无影响, 读取时无影响。
16) W0T: 写0入时会翻转, 写1时无影响, 读取时无影响。
17) W1SRC: 写1设置所有的位, 写0时无影响, 读清零。
18) W1CRS: 写1清零, 写0时无影响, 读设置所有位。
19) W0SRC: 写0设置所有的位, 写1时无影响, 读清零。
20) W0CRS: 写0清零, 写1时无影响, 读设置所有位。
21) WO: 尽可能写入, 读取时会出错。
22) WOC: 写入时清零, 读取时出错。
23) WOS: 写入时设置所有位, 读取时会出错。
24) W1: 在复位( reset) 后, 第一次会尽量写入, 其他写入无影响, 读取时无影响。
25) WO1: 在复位后, 第一次会尽量写入, 其他的写入无影响, 读取时会出错。
寄存器的种类多种多样,如上25种存取方式有时并不能满足用户的需求,这时就需要自定义寄存器的模型。(有点离谱)
- 第五个参数表示是否是易失的(volatile),这个参数一般不会使用。
- 第六个参数表示此域上电复位后的默认值。
- 第七个参数表示此域是否有复位,一般的寄存器或者寄存器的域都有上电复位值,因此这里一般也填写1。
- 第八个参数表示此域是否可以随机化。这主要用于对寄存器进行随机写测试,如果选择0,那么此域将不会随机化而一直是复位值,否则将会随机出一个数值。该参数当且仅当第四个参数为RW、WRC、WRS、WO、W1、WO1时才有效。
- 第九个参数表示此域是否可以单独存取。
定义好此寄存器后,需要在一个由reg_block派生的类中将其实例化:
class reg_model extends uvm_reg_block;
rand reg_invert invert;
virtual function void build();
default_map = create_map("default_map", 0, 2, UVM_BIG_ENDIAN, 0);
invert = reg_invert::type_id::create("invert", , get_full_name());
invert.configure(this, null, "");
invert.build();
default_map.add_reg(invert, 'h9, "RW");
endfunction
`uvm_object_utils(reg_model)
function new(input string name="reg_model");
super.new(name, UVM_NO_COVERAGE);
endfunction
endclass
- 同uvm_reg派生的类一样,每个由uvm_reg_block派生的类也要定义build函数实现所有寄存器的实例化。
- 一个uvm_reg_block中一定要对应一个uvm_reg_map,系统已经有声明好的default_map,只需要在build中将其实例化。这个实例化的过程并不是直接调用uvm_reg_map的new函数,而是通过调用uvm_reg_block的create_map来实现,create_map有众多的参数:名字、基地址、系统总线的宽度(单位是byte)、大小端,最后一个参数表示是否能够按照byte寻址。
- 随后实例化invert并调用invert.configure函数。这个函数的主要功能是指定寄存器进行后门访问操作时的路径。第一个参数是此寄存器所在uvm_reg_block的指针,这里填写this,第二个是reg_file的指针,这里暂时填写null,第三个是此寄存器的后门访问路径,这里暂且为空。
- 当调用完configure时,需要手动调用invert的build函数,将invert中的域实例化。
- 最后一步是将此寄存器加入default_map中。uvm_reg_map的作用是存储所有寄存器的地址, 因此必须将实例化的寄存器加入default_map中,否则无法进行前门访问操作。add_reg函数的三个参数分别是:要加入的寄存器、寄存器的地址(这里是16’h9)、此寄存器的存取方式。到此为止, 一个简单的寄存器模型已经完成。
总结:
- uvm_reg_field是最小的单位,是具体存储寄存器数值的变量,可直接用这个类。
- uvm_reg是一个“空壳子”,或用专业名词来说它是一个纯虚类,因此是不能直接使用的,必须由其派生一个新类,在这个新类中至少加入一个uvm_reg_field,然后这个新类才可以使用
- uvm_reg_block是用于组织大量uvm_reg的大容器。
- 打个比方:uvm_reg是个小瓶子,必须装上药丸(uvm_reg_field)才有意义,装药丸的过程就是定义派生类的过程,而uvm_reg_block是个大箱子,可以放许多小瓶子(uvm_reg),也可以放其他稍微小一点的箱子(uvm_reg_block)。整个寄存器模型就是一个大箱子(uvm_reg_block)。
*将寄存器模型集成到验证平台中
寄存器模型的前门访问方式工作流程如图所示,其中图a为读操作,图b为写操作:
寄存器模型的前门访问操作分为读和写两种。无论是读或写,寄存器模型都会通过sequence产生一个uvm_reg_bus_op的变量,此变量存储操作类型(读还是写)和操作的地址,如果是写操作,还有要写入的数据。此变量中的信息要经过转换器(adapter)转换后交给bus_sequencer,随后交给bus_driver,由其实现最终的前门访问读写操作。
因此,必须要定义好一个转换器。如下例为一个简单的转换器的代码:
class my_adapter extends uvm_reg_adapter;
string tID = get_type_name();
`uvm_object_utils(my_adapter)
function new(string name="my_adapter");
super.new(name);
endfunction : new
function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
bus_transaction tr;
tr = new("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 : reg2bus
function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
bus_transaction tr;
if(!$cast(tr, bus_item)) begin
`uvm_fatal(tID,
"Provided bus_item is not of the correct type. Expecting bus_trans action")
return;
end
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 : bus2reg
endclass : my_adapter
转换器要定义好两个函数:reg2bus——将寄存器模型通过sequence发出的uvm_reg_bus_op型的变量转换成bus_sequencer能够接受的形式;bus2reg——当监测到总线上有操作时,将收集的transaction转换成寄存器模型能够接受的形式,以便寄存器模型能够更新相应寄存器的值。
寄存器模型发起读操作的数值如何返回给寄存器模型:由于总线的特殊性,bus_driver驱动总线进行读操作时能获取要读的数值,如果它将此值放入从bus_sequencer获得的bus_transaction中时,那么bus_transaction中就会有读取的值,此值经过adapter的bus2reg函数的传递,最终被寄存器模型获取,过程如图a所示。由于没有实际的transaction传递,所以从driver到adapter使用了虚线。
转换器写好之后,就可以在base_test中加入寄存器模型了:
class base_test extends uvm_test;
my_env env;
my_vsqr v_sqr;
reg_model rm;
my_adapter reg_sqr_adapter;
…
endclass
function void base_test::build_phase(uvm_phase phase);
super.build_phase(phase);
env = my_env::type_id::create("env", this);
v_sqr = my_vsqr::type_id::create("v_sqr", this);
rm = reg_model::type_id::create("rm", this);
//4 processes
rm.configure(null, "");
rm.build();
rm.lock_model();
rm.reset();
reg_sqr_adapter = new("reg_sqr_adapter");
env.p_rm = this.rm;
endfunction
function void base_test::connect_phase(uvm_phase phase);
super.connect_phase(phase);
v_sqr.p_my_sqr = env.i_agt.sqr;
v_sqr.p_bus_sqr = env.bus_agt.sqr;
v_sqr.p_rm = this.rm;
rm.default_map.set_sequencer(env.bus_agt.sqr, reg_sqr_adapter);
rm.default_map.set_auto_predict(1);
endfunction
要将寄存器模型集成到base_test中,至少需要在base_test中定义两个成员变量:reg_model和reg_sqr_adapter。将所有用到的类在build_phase中实例化。实例 化后reg_model还要做四件事:
- 第一是调用configure函数,其第一个参数是parent block,由于是最顶层的reg_block,因此填写null,第二个是后门访问路径,这里传入一个空的字符串;
- 第二是调用build函数,将所有寄存器实例化;
- 第三是调用lock_model函数,调用此函数后,reg_model中就不能再加入新的寄存器了;
- 第四是调用reset函数,如果不调用此函数,那么reg_model中所有寄存器的值都是0,调用此函数后,所有寄存器的值都将变为设置的复位值。
寄存器模型的前门访问操作最终都由uvm_reg_map完成,因此在connect_phase中需要将转换器和bus_sequencer通过set_sequencer函数告知reg_model的default_map,并将default_map设置为自动预测状态。
*在验证平台中使用寄存器模型
当寄存器模型被建立好后可以在sequence和其他component中使用。以在参考模型中使用为例,需要在参考模型中有一个寄存器模型的指针:
class my_model extends uvm_component;
…
reg_model p_rm;
…
endclass
在base_test代码中已经为env的p_rm赋值,因此只需要在env中将p_rm传递给参考模型即可:
function void my_env::connect_phase(uvm_phase phase);
…
mdl.p_rm = this.p_rm;
endfunction
对于寄存器,寄存器模型提供了两个基本的任务:read和write。若要在参考模型中读取寄存器,使用read任务:
task my_model::main_phase(uvm_phase phase);
my_transaction tr;
my_transaction new_tr;
uvm_status_e status;
uvm_reg_data_t value;
super.main_phase(phase);
p_rm.invert.read(status, value, UVM_FRONTDOOR);
while(1) begin
port.get(tr);
new_tr = new("new_tr");
new_tr.copy(tr);
//`uvm_info("my_model", "get one transaction, copy and print it:", UV M_LOW)
//new_tr.print();
if(value)
invert_tr(new_tr);
ap.write(new_tr);
end
endtask
read任务的原型如下所示:
extern virtual task read(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 uvm_sequence_base parent = null,
input int prior = -1,
input uvm_object extension = null,
input string fname = "",
input int lineno = 0);
它有多个参数,常用的是前三个参数。第一个是uvm_status_e型的变量(输出),用于表明读操作是否成功;第二个是读取的数值(输出);第三个是读取的方式,可选UVM_FRONTDOOR和UVM_BACKDOOR。
由于参考模型一般不会写寄存器,因此对于write任务,以在virtual sequence进行写操作为例说明。在sequence中使用寄存器模型,通常通过p_sequencer的形式引用。需要首先在sequencer中有一个寄存器模型的指针,在上面base_test代码中已经为v_sqr.p_rm赋值了。因此可以直接以如下方式进行写操作:
class case0_cfg_vseq extends uvm_sequence;
…
virtual task body();
uvm_status_e status;
uvm_reg_data_t value;
…
p_sequencer.p_rm.invert.write(status, 1, UVM_FRONTDOOR);
…
endtask
endclass
write任务的原型为:
extern virtual task 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);
其参数有很多个,与read类似,常用的只有前三个。第一个为uvm_status_e型的变量(输出),用于表明写操作是否成功。第二个要写的值(输入),第三个是写操作的方式,可选UVM_FRONTDOOR和UVM_BACKDOOR。
寄存器模型对sequence的transaction类型没有任何要求。因此,可以在一个发送my_transaction的sequence中使用寄存器模型对寄存器进行读写操作。