UVM——RAL

1. 寄存器模型介绍

每个IP都有总线接口,连接到总线上,用来对DUT中寄存器进行配置,改变其行为。

再验证环境中,如果改变了DUT的行为,那么参考模型需要知道DUT做了哪些改变,并同步改变,否则参考模型和当前DUT的功能不一致。

参考模型要想获得寄存器值,需要做两件事:

  1. 在参考模型中启动sequence用来产生一个读取DUT寄存器的操作。
  2. 读取的值传递给参考模型。

如果有了寄存器模型,可以直接在参考模型中读取DUT中寄存器的值,如下:

1
2
3
4
5
6
7
class model extends uvm_model;
task main_phase(uvm_phase phase);
..
reg_model.INVERT_REG.read(status,value,UVM_FRONTEND); //读取寄存器值
..
endtask
endclass

无论是读取还是设置寄存器都由参考模型处理。

1.1 基本概念

uvm_reg_filed:field是寄存器中最小单位。比如一个16bit的寄存器,它的0-7位表示地址,8-15位表示数据,那么8位的地址是一个field,8位的数据也是一个filed

uvm_reg:比field高一个级别,reg中可以有一个或者多个field。每个寄存器模型中至少一个reg。

uvm_reg_block:这就是寄存器模型,其中可以有很多个uvm_reg

uvm_reg_map:每个寄存器加入到寄存器模型中都有其地址,map就是用来存这些地址的,并将地址转化成可以访问的物理地址(加入寄存器模型中的地址一般是偏移地址,而不是绝对地址)。在进行读写时,map将寄存器地址转化成绝对地址。每个block中至少有一个(通常只有一个)map。

1.2 前门访问

前门操作有两种:读和写。模拟CPU在总线上发出读指令,仿真时间一直往前走。

1.3 后门访问

后门操作不进行总线读写,通过层次化引用来改变寄存器的值。

有时DUT中的计数器没法通过前门总线来进行读取

1.4 前门后门的不同

后门访问的优点:

  1. 不需要仿真时间,大型设计中前门访问中的寄存器配置可能需要几个小时,后门缩短为1/100。

    1. 有些DUT中的计数器没法通过前门写入值,当我们需要测试它的进位的时候,需要一直仿真到它进位;但如果通过后门访问,给它设置了一个很大的初始值,那么只需要几次累加就能产生进位信号。

缺点:

  1. 后门访问不能产生波形,只能通过打印信息来查看结果。

1.5 注意

寄存器模型一般例化在base_test中,便于env级集成验证。

2. 简单的寄存器模型

2.1 寄存器模型

2.1.1 创建一个uvm_reg类。

可以在reg类中定义多个field。

1
2
3
4
5
6
7
8
9
10
11
12
13
class reg_invert extends uvm_reg;
rand uvm_reg_field reg_data; //在reg中定义field
virtual function void build(); //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); //16是这个寄存器的宽度,
endfunction
endclass

每个派生自uvm_reg的类都有一个build()函数,它只能手工调用.

2.1.2 创建uvm_reg_block 寄存器模块类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class reg_model extends uvm_reg_block;
rand reg_invert invert; // 定义reg

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_block中已经声明好了map——default_map。只需创建create_map.

1
2
virtual function uvm_reg_map create_map(string 	name, uvm_reg_addr_t 	base_addr,	  	
int unsigned n_bytes, uvm_endianness_e endian, bit byte_addressing = 1 )
参数
base_addr the base address for the map. All registers, memories, and sub-blocks within the map will be at offsets to this address
n_bytes the byte-width of the bus on which this map is used
endian the endian format. See uvm_endianness_e for possible values
byte_addressing specifies whether consecutive addresses refer are 1 byte apart (TRUE) or n_bytes apart (FALSE). Default is TRUE

configure()函数主要用来指定后门访问的路径。

1
function void configure (	uvm_reg_block 	blk_parent,	uvm_reg_file 	regfile_parent	 = 	null,string 	hdl_path	 = 	""	)

add_reg()将寄存器添加到default_map中。

2.2 将寄存器模型集成到验证平台

2.2.1 先写一个转换器adapter

通过寄存器模型进行前门读写操作时,寄存器模型都会通过sequence产生一个uvm_reg_bus_op(uvm结构体)的变量,其中保存操作类型(RW)、地址、数据等。此变量需要经过一个转换器(adapter)交给bus_sequencer。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// uvm_reg_tiem.svh
typedef struct {
// Kind of access: READ or WRITE.
uvm_access_e kind;
// The bus address.
uvm_reg_addr_t addr;
// The data to write. If the bus width is smaller than the register or
// memory width, ~data~ represents only the portion of ~value~ that is
uvm_reg_data_t data;
// The number of bits of <uvm_reg_item::value> being transferred by
// this transaction.
int n_bits;
// Enables for the byte lanes on the bus. Meaningful only when the
// bus supports byte enables and the operation originates from a field write/read.
uvm_reg_byte_en_t byte_en;
// The result of the transaction: UVM_IS_OK, UVM_HAS_X, UVM_NOT_OK.
// See <uvm_status_e>.
uvm_status_e status;
} uvm_reg_bus_op;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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
// uvm_reg_bus_op转换成bus_transaction类型
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
// bus_transaction转换成uvm_reg_bus_op
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_transaction")
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

bus_driver在驱动总线进行读操作的时候,它能顺便获取到要读的值,如下面代码所示。将这个值在bus_transaction中,传给bus_sequencer,然后通过bus2reg函数传给寄存器模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
task bus_driver::drive_one_pkt(bus_transaction tr);
`uvm_info("bus_driver", "begin to drive one pkt", UVM_LOW);
repeat(1) @(posedge vif.clk);

vif.bus_cmd_valid <= 1'b1;
vif.bus_op <= ((tr.bus_op == BUS_RD) ? 0 : 1);
vif.bus_addr = tr.addr;
vif.bus_wr_data <= ((tr.bus_op == BUS_RD) ? 0 : tr.wr_data);

@(posedge vif.clk);
vif.bus_cmd_valid <= 1'b0;
vif.bus_op <= 1'b0;
vif.bus_addr <= 15'b0;
vif.bus_wr_data <= 15'b0;

@(posedge vif.clk);
if(tr.bus_op == BUS_RD) begin
tr.rd_data = vif.bus_rd_data;
//$display("@%0t, rd_data is %0h", $time, tr.rd_data);
end

//`uvm_info("bus_driver", "end drive one pkt", UVM_LOW);
endtask
2.2.2 在base_test中添加寄存器模型

​ 寄存器模型要写在base_test中,如果写在env中,那么当在芯片级验证中,env复用的时候不能在顶层env中指定各个ip寄存器模型的偏移地址。env中的只是寄存器模型句柄。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class base_test extends uvm_test;

my_env env;
my_vsqr v_sqr;
reg_model rm; // 声明
my_adapter reg_sqr_adapter;

...
`uvm_component_utils(base_test)
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);//创建寄存器模型
rm.configure(null, "");
rm.build();// 实例化所有寄存器
rm.lock_model(); //调用此函数后,寄存器模型中不能再添加寄存器
rm.reset(); // 设置寄存器的值为复位值,如果不用这个函数,那么寄存器都为0
reg_sqr_adapter = new("reg_sqr_adapter");
env.p_rm = this.rm; //给env中句柄赋值
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);// 将adapter和sqr告诉map
rm.default_map.set_auto_predict(1);
endfunction

2.3 在验证平台使用寄存器模型

可以再sequence和component中使用寄存器模型。

2.3.1 在参考模型中有个模型指针
1
2
3
class my_model extends uvm_model;
reg_model p_rm;
endclass
2.3.2 在env中连接指针和参考模型
1
2
3
4
5
class my_env extends uvm_env;
function void connect_phase(uvm_phase phase);
mdl.p_rm = this.p_rm;
endfucntion
endclass
2.3.3 在参考模型中读取寄存器
uvm_status_e
UVM_IS_OK Operation completed successfully
UVM_NOT_OK Operation completed with error
UVM_HAS_X Operation completed successfully bit had unknown bits

uvm_reg_data_t 是64bit的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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); //read是寄存器的函数
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:", UVM_LOW)
//new_tr.print();
if(value)
invert_tr(new_tr);
ap.write(new_tr);
end
endtask
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
virtual 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 )
virtual task uvm_reg::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 )
2.3.4 在sequence中写寄存器
1
2
3
4
5
6
7
8
9
10
11
virtual task case0_cfg_vseq::body();
uvm_status_e status;
uvm_reg_data_t value;
if(starting_phase != null)
starting_phase.raise_objection(this);
p_sequencer.p_rm.invert.write(status, 1, UVM_FRONTDOOR);
p_sequencer.p_rm.invert.read(status, value, UVM_FRONTDOOR);
`uvm_info("case0_cfg_vseq", $sformatf("after set, invert's value is %0h", value), UVM_LOW)
if(starting_phase != null)
starting_phase.drop_objection(this);
endtask

3. 前门操作

前门操作有两种:读和写。模拟CPU在总线上发出读指令,仿真时间一直往前走。

上面讲的就是前门操作的读写 UVM_FRONTDOOR。

4. 后门操作

后门操作不进行总线读写,通过层次化引用来改变寄存器的值。

有时DUT中的计数器没法通过前门总线来进行读取。

4.1 不用UVM进行后门读写

方法一、直接在top层设置

1
2
3
4
initial begin
@(posedge rst_n);
my_dut.counter = 32'hFFFD;
end

方法二、在接口中定义函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface backdoor_if(input clk, input rst_n);

function void poke_counter(input bit[31:0] value);
top_tb.my_dut.counter = value;
endfunction

function void peek_counter(output bit[31:0] value);
value = top_tb.my_dut.counter;
endfunction
endinterface
/////////////////////////////////////
task my_case0::configure_phase(uvm_phase phase);
phase.raise_objection(this);
@(posedge vif.rst_n);
vif.poke_counter(32'hFFFD); //设置reg
phase.drop_objection(this);
endtask

4.2 UVM中的后门访问

在寄存器configure()函数中的第三个参数设置路径。

1
2
3
4
5
6
7
8
9
virtual function void reg_model::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"); //第三个参数是后门路径
invert.build();
default_map.add_reg(invert, 'h9, "RW");
...
endfunction

设置根路径hdl_root

1
2
3
4
5
6
7
8
9
function void base_test::build_phase(uvm_phase phase);
super.build_phase(phase);
...
rm = reg_model::type_id::create("rm", this);
...
rm.set_hdl_path_root("top_tb.my_dut"); //设置跟路径
reg_sqr_adapter = new("reg_sqr_adapter");
env.p_rm = this.rm;
endfunction

上面两个路径合起来就是寄存器的路径。

4.3 后门访问函数

后门访问也有write和read。这两个函数在操作的时候会模拟DUT的行为,如果寄存器是只读的,如果要写,那么写不进去。

还有poke(写)和peek(读)这两个函数。可以使用poke向一个只读寄存器写入值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
virtual task poke(	output 	uvm_status_e 	status,	  	
input uvm_reg_data_t value,
input string kind = "",
input uvm_sequence_base parent = null,
input uvm_object extension = null,
input string fname = "",
input int lineno = 0 )
virtual task peek( output uvm_status_e status,
output uvm_reg_data_t value,
input string kind = "",
input uvm_sequence_base parent = null,
input uvm_object extension = null,
input string fname = "",
input int lineno = 0 )

4.4 使用后门操作

1
2
3
4
5
6
7
virtual task case0_cfg_vseq::body();
uvm_status_e status;
uvm_reg_data_t value;
p_sequencer.p_rm.counter_low.poke(status, 16'hFFFD); // 后门写寄存器
p_sequencer.p_rm.counter_low.read(status, value, UVM_FRONTDOOR); // 前门读

endtask

5. 层次化寄存器模型

​ 层次化寄存器模型就是uvm_reg_block(第一级)中有uvm_reg_block(第二级),一般只在第二级block中添加寄存器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class global_blk extends uvm_reg_block;
...
endclass
class buf_blk extends uvm_reg_block;
...
endclass
class mac_blk extends uvm_reg_block;
...
endclass
class reg_model extends uvm_reg_block;
rand global_blk gb_ins;
rand buf_blk bb_ins;
rand mac_blk mb_ins;
virtual function void build();
default_map = create_map("default_map", 0, 2, UVM_BIG_ENDIAN, 0);
gb_ins = global_blk::type_id::create("gb_ins");
gb_ins.configure(this, ""); // 这里只有两个参数
gb_ins.build();
gb_ins.lock_model();
default_map.add_submap(gb_ins.default_map, 16'h0); //将子map添加到父map
...
endfunction
..
endclass

​ 子block的default_map不知道寄存器的基地址,只知道偏移地址。add_submap()函数将子block的map添加到父block的map中,并告诉子map的基地址。

1
2
function void configure(	uvm_reg_block 	parent	 = 	null,string	hdl_path = 	""	)
// 第二个参数是reg block的hdl路径

使用方法跟之前一样,只是层次化路径多了一层:

1
p_rm.gb_ins.invert.poke(...);

6. reg_file

主要用来区分不同的hdl路径。

如果有寄存器:

1
2
3
4
5
6
7
8
top_tb.dut.fileA.rega
top_tb.dut.fileA.regb
top_tb.dut.fileA.regc
...
top_tb.dut.fileB.rega
top_tb.dut.fileB.regb
top_tb.dut.fileB.regc
...

那么需要将寄存器的hdl路径告诉寄存器模型

1
2
3
4
rega.configure(this,null,"fileA.rega");
rega.configure(this,null,"fileA.regb");
rega.configure(this,null,"fileA.regc");
....

需要写好多fileA,并且如果fileA改成其他的值,需要重写。

可以单独把fileA拿出来,这就是uvm_reg_file。

uvm_reg_file是个纯虚基类,需要扩展后使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class regfile extends uvm_reg_file;
function new(string name = "regfile");
super.new(name);
endfunction

`uvm_object_utils(regfile)
endclass
class mac_blk extends uvm_reg_block;

rand regfile file_a; //定义reg file
rand reg_regA rega;
rand reg_regB regb;
rand reg_vlan vlan;

virtual function void build();
default_map = create_map("default_map", 0, 2, UVM_BIG_ENDIAN, 0);

file_a = regfile::type_id::create("file_a", , get_full_name());
file_a.configure(this, null, "fileA"); //配置reg file路径

regA = reg_regA::type_id::create("regA", , get_full_name());
regA.configure(this, file_a, "regA"); // reg configure函数第二个参数是reg_file
regA.build();
default_map.add_reg(regA, 'h31, "RW");
regB = reg_regB::type_id::create("regB", , get_full_name());
regB.configure(this, file_a, "regB");
regB.build();
default_map.add_reg(regB, 'h32, "RW");
endfunction
endclass

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!