提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
本章节主要介绍UVM实战第7章寄存器模型的内容
一、寄存器模型简介
通常来说,DUT中会有一组控制端口,通过控制端口,可以配置DUT中的寄存器,DUT可以根据寄存器的值来改变行为,这组控制端口就是寄存器配置总线。
暂时以uvm实战附录B代码为例,后续要改成apb总线控制的rtl代码
module dut(clk,rst_n,bus_cmd_valid,bus_op,bus_addr,bus_wr_data,bus_rd_data,rxd,rx_dv,txd,tx_en);
input clk;
input rst_n;
input bus_cmd_valid;
input bus_op;
input [15:0] bus_addr;
input [15:0] bus_wr_data;
output [15:0] bus_rd_data;
input [7:0] rxd;
input rx_dv;
output [7:0] txd;
output tx_en;
reg[7:0] txd;
reg tx_en;
reg invert;
always @(posedge clk) begin
if(!rst_n) begin
txd <= 8'b0;
tx_en <= 1'b0;
end
else if(invert) begin
txd <= ~rxd;
tx_en <= rx_dv;
end
else begin
txd <= rxd;
tx_en <= rx_dv;
end
end
always @(posedge clk) begin
if(!rst_n)
invert <= 1'b0;
else if(bus_cmd_valid && bus_op) begin
case(bus_addr)
16'h9: begin
invert <= bus_wr_data[0];
end
default: begin
end
endcase
end
end
reg [15:0] bus_rd_data;
always @(posedge clk) begin
if(!rst_n)
bus_rd_data <= 16'b0;
else if(bus_cmd_valid && !bus_op) begin
case(bus_addr)
16'h9: begin
bus_rd_data <= {
15'b0, invert};
end
default: begin
bus_rd_data <= 16'b0;
end
endcase
end
end
endmodule
如上述代码,控制总线即为bus_op,bus_addr,bus_rd_data,bus_wr_data,bus_cmd_valid等,相同的控制总线如apb总线,而寄存器是invert,也就是通过控制总线来配置寄存器的值,从而改变dut的行为(上述代码33-45)
1.引入寄存器模型
上述的rtl代码,invert寄存器用于控制DUT是否将输入的激励按位取反。在取反的情况下,参考模型需要读取此寄存器的值,如果为1,那么其输出的transaction也需要进行反转。参考模型如何去读此寄存器的值呢?
- 假设没有寄存器模型:利用config机制分别为virtual sequencer和scoreboard设置一个config_object,在此object设置一个事件,然后在scoreboard中触发,随后启动一个sequence,这个sequence发送一个transaction给bus_driver。sequence读取到寄存器后,再通过config_db传递给参考模型,还需要使用wait_modified来更新数据。--------相当麻烦!!!
- 有寄存器模型:整个过程可以简化为:reg_model.INVERT_REG.read(status, value, UVM_FRONTDOOR);
可以看出寄存器模型的优点:
- 可以在任何耗费时间的phase中使用寄存器模型以前门或后门进行访问,还可以在不耗费时间的phase中使用后门访问的方式来读取寄存器的值;(前门后门的概念后面会描述)
- 寄存器模型提供一些任务,如mirror,updata,批量完成寄存器模型与DUT中相关寄存器的交互;
- 重新定义验证平台与DUT的寄存器接口,使验证人员更好地组织及配置寄存器,简化流程、减少工作量;
2.寄存器模型中的基本概念
uvm_reg_field:寄存器模型中的最小单位,个人理解就是域,如上的状态寄存器共有四个域,分别是empty、full、overflow、underflow,注意reserved不是一个域,是预留的空位。
uvm_reg:它比uvm_reg_field高一个级别,但是依然是比较小的单位。这里我理解就是上面的FIFO_STATUS,就是常说的寄存器名字,它包含了好多的域。
uvm_reg_block:它是一个比较大的单位,在其中可以加入许多的uvm_reg,也可以加入其他的uvm_reg_block。一个寄存器模型中至少包含一个uvm_reg_block。
uvm_reg_map:每个寄存器在加入寄存器模型时都有其地址,uvm_reg_map就是存储这些地址,并将其转换成可以访问的物理地址(因为加入寄存器模型中的寄存器地址一般都是偏移地址,而不是绝对地址)。当寄存器模型使用前门访问方式来实现读或写操作时,uvm_reg_map就会将地址转换成绝对地址,启动一个读或写的sequence,并将读或写的结果返回。每个reg_block内部,至少有一个(通常也只有一个)uvm_reg_map.
3.简单的寄存器模型
为上节的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);//16:这个宽度指的是寄存器中总共的位数。这个数字一般与系统总线的宽度一致。
endfunction
endclass
- new函数的写法:第二个参数是要将invert寄存器的宽度作为参数传递给super.new函数。第三个参数是是否要加入覆盖率的支持
- build函数的写法:1) 这个build不会自动执行,需要手动调用;2) 所有的uvm_reg_field都在这里实例化; 3) reg_data实例化后,要调用reg_data.configure函数来配置这个字段,configure的参数解释如下:
1.第一个参数是此域(uvm_reg_field)的父辈,也就是此域位于哪个寄存器中,这里就是填this
2.第二个参数是此域的宽度,在上节的dut中,invert的宽度为1,所以这里就是1
3.第三个参数是此域的最低为在整个寄存器中的位置,从0开始计数
4.第四个参数表示字段的存取方式,一般有这几种:RO-只读,RW-读写,RC-读清,WC-写清,WO-只写
5.第五个参数表示是否易失的(volatile),这个参数一般不会使用
6.第六个参数表示此域上电复位后的默认值
7.第七个参数表示此域是否有复位,一般的寄存器或者寄存器的域都有上电复位值,因此这里一般都填1
8.第八个参数表示这个域是否可以随机化。主要用于对寄存器进行随机写测试
9.第九哥参数表示这个域是否可以单独存取
上面代码定义好invert寄存器后,需要在一个有reg_block派生的类中将invert类实例化:
class reg_model extends uvm_reg_block;
rand reg_invert invert;
virtual function void build();
//parameter:parameter name, base_addr, bus width(byte),large/small end,address by byte
default_map = create_map("default_map", 0, 2, UVM_BIG_ENDIAN, 0);//这里必须用create_map来实现
invert = reg_invert::type_id::create("invert", , get_full_name());
invert.configure(this, null, "");
invert.build(); //手动调用invert的build函数,将invert中的域实例化
default_map.add_reg(invert, 'h9, "RW");//将此寄存器加入default_map中
endfunction
`uvm_object_utils(reg_model)
function new(input string name="reg_model");
super.new(name, UVM_NO_COVERAGE);
endfunction
endclass
- build函数的写法:1) 实例化default_map(一个uvm_reg_block中一定要对应一个uvm_reg_map,系统已经有一个声明好的default_map);2) 在此build函数中对所有的寄存器进行实例化;
第一步实例化default map
- 第一个参数是名字
- 第二个参数是基地址
- 第三个参数是系统总线的宽度,这里的单位是byte而不是bit
- 第四个参数是大小端
- 第五个参数表示能否按照byte寻址
第二步实例化寄存器并调用invert.configure函数,目的是指定寄存器进行后门访问操作时的路径
- 第一个参数是此寄存器所在uvm_reg_block的指针,这里填this
- 第二个参数是reg_file的指针
- 第三个参数是此寄存器的后门访问路径
第三步将此寄存器加入default_map中,uvm_reg_map的作用就是存储所有寄存器的地址,因此必读加入default_map
- 第一个参数是要加入的寄存器
- 第二个参数是寄存器的地址,这里是’h9
- 第三个参数是此寄存器的存取方式
总结:uvm_reg_file是最小的单位,是具体存储寄存器数值的变量,可以直接使用这个类(就是上面提的reg_invert),它得继承于uvm_reg,uvm_reg是一个空壳子,就是纯虚类,不能直接使用,所以它需要派生一个新类(reg_invert)。uvm_reg_block则用于组织大量uvm_reg的大容器。
4.将寄存器模型集成到验证平台中
1) 加入adapter
寄存器的读和写本质是需要通过sequence产生一个uvm_reg_bus_op的变量,这个变量里面包含这读写类型、读写地址、写入的数据等信息,然后这些信息要交给bus_sequencer,随后交给driver,由bus_driver实现最终的前门访问读写操作。但是,这个产生的uvm_reg_bus_op是不能直接给到bus_sequencer的,需要转换一下,这就引入一个转换器:adapter
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_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
这个转换器定义了两个函数:reg2bus和bus2reg
reg2bus:将寄存器模型通过sequence发出的uvm_reg_bus_op型的变量转换成bus_sequencer能够接受的形式;
bus2reg:为当检测到总线上有操作时,它将收集来的transaction转换成寄存器模型能够接受的形式,以便寄存器模型能够更新相应的寄存器的值
实际寄存器的读和写流程如上图所示,写流程比较简单,暂且不说;读操作中,读到的数值是如何返回到寄存器模型的呢?
由于总线的特殊性,bus_driver在驱动总线进行读操作时,它也能顺便获取要读的数值,如果它将此值放入从bus_sequencer获得的bus_transaction中时,那么bus_transaction中就会有读取的值,此值经过adapter的bus2reg函数的传递,最终被寄存器模型获取,如上图读操作的虚线所示,完整流程如下:
一,参考模型调用寄存器模型的读任务;
二,寄存器模型产生sequence,并产生uvm_reg_item:rw;
三,产生driver能够接受的transaction:bus_req=adapter.reg2bus(rw);
四,把bus_req交给bus_sequencer;
五,driver得到bus_req后驱动它,得到读取的值,并将读取值放入bus_req[rep]中,调用item_done;
六,寄存器模型调用adapter.bus2reg(bus_req[rep], rw)将bus_req中的读取值传递给rw;
七,将rw中的读数据返回参考模型
如果driver一直发送应答而sequence不收集应答,那么将会导致sequencer的应答队列溢出,UVM在adpter中设置了provide_responses选项,寄存器在调用bus2reg将目标transaction转换成uvm_reg_item时,其传入的参数是rep,而不是req,见上述步骤中括号内[ ];
2) 加入寄存器模型
在base_test中加入寄存器模型:
class base_test extends uvm_test;
my_env env;
my_vsqr v_sqr;
reg_model rm; //加入的寄存器模型
my_adapter reg_sqr_adapter; //加入的adapter
function new(string name = "base_test", uvm_component parent = null);
super.new(name,parent)