第七章 UVM的寄存器模型

寄存器模型简介

带寄存器配置总线的DUT

在本书以前所有的例子中,使用的DUT几乎都是基于2.2.1节中所示的最简单的DUT,只有一组数据输入输出口,而没有行为控制口,这样的DUT几乎是没有任何价值的。通常来说,DUT中会有一组控制端口,通过控制端口,可以配置DUT中的寄存器,DUT可以根据寄存器的值来改变其行为。这组控制端口就是寄存器配置总线。这样的DUT的一个示例如附录B的代码清单B-2所示。

在这个DUT中,只有一个1bit的寄存器invert,为其分配地址16’h9。如果它的值为1,那么DUT在输出时会将输入的数据取反;如果为0,则将输入数据直接发送出去。

新验证平台框图

新验证平台框图

需要寄存器模型才能做的事情

考虑如下一个问题,在上节所示的DUT中,invert寄存器用于控制DUT是否将输入的激励按位取反。在取反的情况下,参考模型需要读取此寄存器的值,如果为1,那么其输出transaction也需要进行反转。可是如何在参考模型中读取一个寄存器的值呢?

  • 一是全局变量,这个太蠢了不考虑
  • 二是在virtual sequencer和scoreboard设置一个config_object,然后在object设置一个事件,如rd_reg_event,然后在scoreboard中触发这个事件。
  • 二还是太复杂了,使用寄存器模型,可以简化为:
task my_model::main_phase(uvm_phase phase);
…
    reg_model.INVERT_REG.read(status, value, UVM_FRONTDOOR);
…
endtask

只要一条语句就可以实现上述复杂的过程。像启动sequence及将读取结果返回这些事情,都会由寄存器模型来自动完成。

 title=

寄存器模型中的基本概念

uvm_reg_field:这是寄存器模型中的最小单位。什么是reg_field?假如有一个状态寄存器,它各个位的含义如图7-3所示。

 title=

如上的状态寄存器共有四个域,分别是empty、full、overflow、underflow。这四个域对应寄存器模型中的uvm_reg_field。名字为“reserved”的并不是一个域。

uvm_reg:它比uvm_reg_field高一个级别,但是依然是比较小的单位。一个寄存器中至少包含一个uvm_reg_field。

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。

简单的寄存器模型

只有一个寄存器的寄存器模型

本节为7.1.1节所示的DUT建立寄存器模型。这个DUT非常简单,它只有一个寄存器invert。要为其建造寄存器模型,首先要从uvm_reg派生一个invert类:

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

 title=

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

当一个寄存器模型被建立好后,可以在sequence和其他component中使用。以在参考模型中使用为例,需要在参考模型中有一个寄存器模型的指针:

文件:src/ch7/section7.2/my_model.sv
4 class my_model extends uvm_component;
…
9 reg_model p_rm;
…
16 endclass

对于寄存器,寄存器模型提供了两个基本的任务:read和write。若要在参考模型中读取寄存器,使用read任务:

文件:src/ch7/section7.2/my_model.sv
37 task my_model::main_phase(uvm_phase phase);
38 my_transaction tr;
39 my_transaction new_tr;
40 uvm_status_e status;
41 uvm_reg_data_t value;
42 super.main_phase(phase);
43 p_rm.invert.read(status, value, UVM_FRONTDOOR);
44 while(1) begin
45 port.get(tr);
46 new_tr = new("new_tr");
47 new_tr.copy(tr);
48 //`uvm_info("my_model", "get one transaction, copy and print it:", UV M_LOW)
49 //new_tr.print();
50 if(value)
51 invert_tr(new_tr);
52 ap.write(new_tr);
53 end
54 endtask

read任务的原型如下所示:

来源:UVM
源代码
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任务原型如下:

来源:UVM
源代码
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。

后门访问与前门访问

UVM中前门访问的实现

所谓前门访问操作就是通过寄存器配置总线(如APB协议、OCP协议、I2C协议等)来对DUT进行操作。无论在任何总线协议中,前门访问操作只有两种:读操作和写操作。前门访问操作是比较正统的用法。对一块实际焊接在电路板上正常工作的芯片来说,此时若要访问其中的某些寄存器,前门访问操作是唯一的方法。

sequence是自动执行的,但是在其执行完毕后(body及post_body调用完成),为此sequence分配的内存依然是有效的,所以可以使用reg_seq继续引用此sequence。上述读操作正是用到了这一点。

对UVM来说,其在寄存器模型中使用的方式也与此类似。上述操作方式的关键是在参考模型中有一个sequencer的指针,而在 寄存器模型中也有一个这样的指针,它就是7.2.2节中,在base_test的connect_phase为default map设置的sequencer指针。

当然,对于UVM来说,它是一种通用的验证方法学,所以要能够处理各种transaction类型。幸运的是,这些要处理的transaction都非常相似,在综合了它们的特征后,UVM内建了一种transaction:uvm_reg_item。通过adapter的bus2reg及reg2bus,可以实现uvm_reg_item与目标transaction的转换。以读操作为例,其完整的流程为:

  • 参考模型调用寄存器模型的读任务。
  • 寄存器模型产生sequence,并产生uvm_reg_item:rw。
  • 产生driver能够接受的transaction:bus_req=adapter.reg2bus(rw)。
  • 把bus_req交给bus_sequencer。
  • driver得到bus_req后驱动它,得到读取的值,并将读取值放入bus_req中,调用item_done。
  • 寄存器模型调用adapter.bus2reg(bus_req,rw)将bus_req中的读取值传递给rw。
  • 将rw中的读数据返回参考模型。

后门访问操作的定义

在通信系统中,有大量计数器用于统计各种包裹的数量,如超长包、长包、中包、短包、超短包等。这些计数器的一个共同的特点是它们是只读的,DUT的总线接口无法通过前门访问操作对其进行写操作。除了是只读外,这些寄存器的位宽一般都比较宽,如32位、48位或者64位等,它们的位宽超过了设计中对加法器宽度的上限限制。计数器在计数过程中需要使用加法器,对于加法器来说,在同等工艺下,位宽越宽则其时序越差,因此在设计中一般会规定加法器的最大位宽。在上述DUT中,加法器的位宽被限制在16位。要实现32位的counter的加法操作,需要使用两个叠加的16位加法器。

后门访问是与前门访问相对的操作,从广义上来说,所有不通过DUT的总线而对DUT内部的寄存器或者存储器进行存取的操作都是后门访问操作。如在top_tb中可以使用如下方式对counter赋初值:

文件:src/ch7/section7.3/7.3.2/top_tb.sv
50 initial begin
51 @(posedge rst_n);
52 my_dut.counter = 32'hFFFD;
53 end

后门访问操作能够更好地完成前门访问操作所做的事情。后门访问不消耗仿真时间,与前门访问操作相比,它消耗的运行时间要远小于前门访问操作的运行时间。在一个大型芯片的验证中,在其正常工作前需要配置众多的寄存器,配置时间可能要达到一个或几个小时,而如果使用后门访问操作,则时间可能缩短为原来的1/100。

后门访问操作能够完成前门访问操作不能完成的事情。如在网络通信系统中,计数器通常都是只读的(有一些会附加清零功能),无法对其指定一个非零的初值。而大部分计数器都是多个加法器的叠加,需要测试它们的进位操作。本节DUT的counter使用了两个叠加的16位加法器,需要测试当计数到32‘hFFFF时能否顺利进位成为32’h1_0000,这可以通过延长仿真时间来使其计数到32‘hFFFF,这在本节的DUT中是可以的,因为计数器每个时钟都加1。但是在实际应用中,可能要几万个或者更多的时钟才会加1,因此需要大量的运行时间,如几天。这只是32位加法器的情况,如果是48位的计数器,情况则会更坏。这种情况下,后门访问操作能够完成前门访问操作完成的事情,给只读的寄存器一个初值。

后门访问的问题是,访问操作无法在波形文件中找到操作痕迹,增加了调试难度。

使用interface进行后门访问操作

上一节中提到过在top_tb中使用绝对路径对寄存器进行后门访问操作,这需要更改top_tb.sv文件,但是这个文件一般是固定的,不会因测试用例的不同而变化,所以这种方式的可操作性不强。在driver等组件中也可以使用这种绝对路径的方式进行后门访问操作,但强烈建议不要在driver等验证平台的组件中使用绝对路径。这种方式的可移植性不强。如果想在driver或monitor中使用后门访问,一种方法是使用接口。

可以新建一个后门interface:

Last modification:February 28, 2022
恰饭环节