1. 寄存器模型介绍 每个IP都有总线接口,连接到总线上,用来对DUT中寄存器进行配置,改变其行为。
再验证环境中,如果改变了DUT的行为,那么参考模型需要知道DUT做了哪些改变,并同步改变,否则参考模型和当前DUT的功能不一致。
参考模型要想获得寄存器值,需要做两件事:
在参考模型中启动sequence用来产生一个读取DUT寄存器的操作。
读取的值传递给参考模型。
如果有了寄存器模型,可以直接在参考模型中读取DUT中寄存器的值,如下:
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/100。
有些DUT中的计数器没法通过前门写入值,当我们需要测试它的进位的时候,需要一直仿真到它进位;但如果通过后门访问,给它设置了一个很大的初始值,那么只需要几次累加就能产生进位信号。
缺点:
后门访问不能产生波形,只能通过打印信息来查看结果。
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; virtual function void build(); reg_data = uvm_reg_field::type_id::create("reg_data" ); 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" ); super .new (name, 16 , UVM_NO_COVERAGE); 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; 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 typedef struct { uvm_access_e kind; uvm_reg_addr_t addr; uvm_reg_data_t data; int n_bits; uvm_reg_byte_en_t byte_en; 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 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 : bus2regendclass : 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 ; end 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 (); 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
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 ; endfucntionendclass
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); while (1 ) begin port.get (tr); new_tr = new ("new_tr" ); new_tr.copy (tr); 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 ); 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 ); ... 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 = "" )
使用方法跟之前一样,只是层次化路径多了一层:
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; 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" ); regA = reg_regA::type_id::create("regA" , , get_full_name()); regA.configure (this , file_a, "regA" ); 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