介绍
何为UVM寄存器层 (What is the UVM register layer ?)
UVM寄存器层类用于为验证设计(DUV)中的内存映射寄存器和内存创建高级的、面向对象的模型。寄存器层定义了许多基类,这些基类可以被适当扩展以抽象DUV的读写操作。在深入UVM寄存器层的细节之前,让我们首先回顾一下寄存器是如何组织的以及它们在数字设计中是如何工作的。
何为寄存器(What are registers ?)
大多数数字设计模块都有可以通过外围总线访问的软件可控寄存器。当用特定的值编程时,这些寄存器允许硬件以特定的方式运行。例如,可能有一个32位的寄存器,其中有几个单独的字段。每个字段代表一个特定的特性,可以在需要时由软件配置。
上图是设计中具有五个不同功能字段的单个寄存器的示例。字段属性可以由设计器指定如下。
Bit Offset | Name | Read/Write | Width | Description |
---|---|---|---|---|
0 | En | RW | 1 | Enable |
1 | Mode | RW | 3 | Supports the following modes : Low Energy : 000 Medium Energy : 001 High Speed : 010 Auto Adjust : 011 Cruise Mode : 100 Balanced : 101 |
4 | Halt | RW | 1 | Halt. Set this bit to 1 to stop this module temporarily and set it to 0 to resume |
5 | auto | RW | 1 | Auto shutdown mode. Set this bit to 1 to start auto shutdown after 5 min |
6 | Reserved | RO | 5 | Reserved for future additions |
11 | Speed | RW | 5 | Set a value to control the speed from 100 rpm to 2000 rpm. |
16 | Reserved | RO | 16 | Reserved for future additions |
在RTL设计中会有一个模块来实现这些寄存器
// RTL representation
module reg_block (...);
wire [31:0] reg_ctl;
// Declare reg variables to store register field values
reg ctl_en; // Enable for the module
reg [2:0] ctl_mode; // Mode
reg ctl_halt; // Halt
reg ctl_auto; // Auto shutdown
reg [4:0] ctl_speed; // Speed control
assign reg_ctl = {16'b0, ctl_speed, 5'b0, ctl_auto, ctl_halt, ctl_mode, ctl_en};
// Logic for getting individual fields from the bus
...
endmodule
何为寄存器块(What is a register block ?)
RTL中会有一个寄存器集合,其中每个寄存器都有不同的字段和配置集。例如,REG_CTL可以放在偏移地址0x0处,以强调它是模块的主控寄存器。因为它需要32位(4字节),所以下一个寄存器REG_STAT将位于0x4并保存设计状态相关标志。
何为内存映射(What is a memory map ?)
一个SoC通常有一个或多个处理器核心,DMA引擎,总线网络互连和许多外设模块。每个外设模块都有一个关联的寄存器块,它们都被布置在一个称为内存映射的东西中。内存映射就像一个巨大的表,它为处理器总线上的各种内存和外设/设备寄存器定义地址范围。处理器可以通过诸如APB或WishBone之类的外围总线协议访问这些寄存器块。因此,每个外设块都需要一个基址,以便各自的寄存器块可以驻留在基址上。
让我们把上面描述的寄存器块看作Timer设计模块的一部分,它的基址为0xE7B3_0000。为了访问Timer块的REG_CTL,内核必须发送一个地址为0xE7B3_0000的事务,因为REG_CTL的基址偏移量为0x0。为了访问REG_DMACTL,核心将发送一个地址为(0xE7B3_0000 + 0xC)的事务,因为REG_DMACTL的偏移量为0xC。
UVM寄存器模型( UVM Register Model)
我们已经了解了内存映射中寄存器是如何布局的。因此,我们将简单地使用现有的UVM RAL(寄存器抽象层)类来定义单独的字段、寄存器和寄存器块。寄存器模型是包含并描述每个寄存器及其单独字段的类对象的层次结构的实体。我们可以使用寄存器模型对象对设计执行读和写操作。
模型中的每个寄存器都对应于设计中的一个实际硬件寄存器。在模型的寄存器中有两种变量。
期望值(Desired Value)
期望值是我们希望设计具有的价值。换言之,模型有一个内部变量来存储所需的值,该值可以在稍后的设计中更新。例如,如果我们希望设计中的寄存器REG_STAT具有0x1234_5678的值,那么必须在模型中将该寄存器的期望值设置为0x1234_5678,并且应该调用更新任务以在设计中反映这一点。
镜像值(Mirrored Value)
每次在设计上发生读或写操作时,该特定寄存器的镜像值将被更新。因此,模型中的镜像值是设计中最新的已知值。这是非常有用的,因为我们不必每次在设计中想要知道寄存器的值时都发出读操作。
每个寄存器字段也都存在期望值和镜像值。
创建一个寄存器的类
创建寄存器类的第一个逻辑步骤是识别设计中的所有寄存器,并为每个寄存器创建一个对象。在前文中,我们首先为REG_CTL创建一个寄存器类对象,因为它的字段已经很好地定义了。每个寄存器对象都应该继承自uvm_reg类,每个寄存器字段都应该是uvm_reg_field的对象。不必为一个寄存器字段创建一个单独的类,只需为每个寄存器字段创建一个对象,如下例所示。
class reg_ctl extends uvm_reg;
rand uvm_reg_field En;
rand uvm_reg_field Mode;
rand uvm_reg_field Halt;
rand uvm_reg_field Auto;
rand uvm_reg_field Speed;
字段被声明为rand,以便在需要时可以随机化,并且类型为uvm_reg_field。这只是针对所有字段的声明,实际的定义将在后面介绍。接下来,我们必须像处理任何UVM类一样编写new()函数。这个super调用需要三个参数,第一个参数是寄存器的名称,第二个参数表示寄存器的大小,在我们的例子中是32位。最后一个参数指定在寄存器抽象类的扩展中出现哪些功能覆盖模型。
function new (string name = "reg_ctl");
super.new (name, 32, UVM_NO_COVERAGE);
endfunction
到目前为止,我们还没有定义任何与寄存器字段相关的内容,因此,接下来让我们在一个名为build()的自定义用户函数中进行定义。我们将保持该函数(build)为虚函数,以防其他人试图重写此方法并调用自己版本的build()。
virtual function void build ();
// Create object instance for each field
this.En = uvm_reg_field::type_id::create ("En");
this.Mode = uvm_reg_field::type_id::create ("Mode");
this.Halt = uvm_reg_field::type_id::create ("Halt");
this.Auto = uvm_reg_field::type_id::create ("Auto");
this.Speed = uvm_reg_field::type_id::create ("Speed");
// Configure each field
this.En.configure (this, 1, 0, "RW", 0, 1'h0, 1, 1, 1);
this.Mode.configure (this, 3, 1, "RW", 0, 3'h2, 1, 1, 1);
this.Halt.configure (this, 1, 4, "RW", 0, 1'h1, 1, 1, 1);
this.Auto.configure (this, 1, 5, "RW", 0, 1'h0, 1, 1, 1);
this.Speed.configure (this, 5, 11, "RW", 0, 5'h1c, 1, 1, 1);
endfunction
endclass
configure()方法用于为类对象定义字段属性,它有以下参数。
function void configure(
uvm_reg parent,
int unsigned size,
int unsigned lsb_pos,
string access,
bit volatile,
uvm_reg_data_t reset,
bit has_reset,
bit is_rand,
bit individually_accessible
);
相似的可以为设计中的每个寄存器定义单独的类。
class reg_stat extends uvm_reg;
...
endclass
class reg_dmactl extends uvm_reg;
...
endclass
...
创建一个寄存器的块
定义完所有寄存器后,它需要成为寄存器块的一部分。为此,我们将定义一个继承自uvm_reg_block的寄存器块,并在其中创建所有寄存器的对象。为了达到我们的目的,我们只在这个块中放入三个寄存器。
class reg_block extends uvm_reg_block;
rand reg_ctl m_reg_ctl;
rand reg_stat m_reg_stat;
rand reg_inten m_reg_inten;
function new (string name = "reg_block");
super.new (name, UVM_NO_COVERAGE);
endfunction
virtual function void build ();
// Create an instance for every register
this.default_map = create_map ("", 0, 4, UVM_LITTLE_ENDIAN, 0);
this.m_reg_ctl = reg_ctl::type_id::create ("m_reg_ctl", , get_full_name);
this.m_reg_stat = reg_stat::type_id::create ("m_reg_stat", , get_full_name);
this.m_reg_inten = reg_inten::type_id::create ("m_reg_inten", , get_full_name);
// Configure every register instance
this.m_reg_ctl.configure (this, null, "");
this.m_reg_stat.configure (this, null, "");
this.m_reg_inten.configure (this, null, "");
// Call the build() function to build all register fields within each register
this.m_reg_ctl.build();
this.m_reg_stat.build();
this.m_reg_inten.build();
// Add these registers to the default map
this.default_map.add_reg (this.m_reg_ctl, `UVM_REG_ADDR_WIDTH'h0, "RW", 0);
this.default_map.add_reg (this.m_reg_stat, `UVM_REG_ADDR_WIDTH'h4, "RO", 0);
this.default_map.add_reg (this.m_reg_inten, `UVM_REG_ADDR_WIDTH'h8, "RW", 0);
endfunction
endclass
reg_block类的对象是寄存器模型,可用于访问所有寄存器并对其执行读写操作。我们只讨论了如何定义寄存器模型的第一部分。但是如何写入和读取DUV的问题还没有讨论过。要执行寄存器操作,必须通过外围总线发送有效的总线事务,这将在下一节中介绍。
UVM寄存器环境(UVM Register Environment)
在寄存器模型中,我们已经看到了如何创建一个模型来表示设计中的实际寄存器。现在,我们将了解执行寄存器访问(例如读和写操作)所需的寄存器环境中的不同组件。
寄存器环境基本上需要四个组件:
- 基于精确反映设计寄存器值的UVM类的register model
- 基于某种协议驱动实际总线事务到设计的agent
- 一个adapter,用于将模型中的读写语句转换为基于协议的总线事务
- 一个用来理解总线活动并更新寄存器模型以匹配设计内容的predictor
寄存器转换器(Register Adapter)
Uvm_reg具有read()和write()的内置方法,用于启动对设计的读和写操作。
class reg_ctl extends uvm_reg;
...
endclass
m_reg_ctl.write (status, addr, wdata); // Write wdata to addr
m_reg_ctl.read (status, addr, rdata); // Read rdata from addr
这些寄存器读/写访问调用创建了一个uvm_reg_bus_op类型的内部通用寄存器项,它是一个简单的结构,如下所示。
typedef struct {
uvm_access_e kind; // Access type: UVM_READ/UVM_WRITE
uvm_reg_addr_t addr; // Bus address, default 64 bits
uvm_reg_data_t data; // Read/Write data, default 64 bits
int n_bits; // Number of bits being transferred
uvm_reg_byte_en byte_en; // Byte enable
uvm_status_e status; // Result of transaction: UVM_IS_OK, UVM_HAS_X, UVM_NOT_OK
} uvm_reg_bus_op;
为了将读/写方法调用转换为实际的总线协议访问,通用寄存器通过适配器组件将transaction item转换为特定于协议的总线事务项。适配器需要是双向的,以便能够将transaction item转换为总线事务,并将总线事务响应转换回transaction item,以便能够在寄存器模型中进行更新。
adapter通过reg2bus()和bus2reg()函数实现这两个转换过程。顾名思义,reg2bus()将uvm_reg_bus_op类型的寄存器级对象转换为协议事务,而bus2reg()将总线级事务转换为寄存器对象。总线协议因设计而异,因此必须从uvm_reg_adapter继承一个自定义适配器来覆盖这些函数。
adapter中还有两个内建变量来处理字节启用和响应items。如果总线协议允许允许某些字节通道选择数据总线的某些有效字节,则supports_byte_enable位应该设置为1。如果目标agent的diver发送需要响应处理的独立响应项,则应该将位provides_responses设置为1。
// apb_adapter is inherited from "uvm_reg_adapter"
class reg2apb_adapter extends uvm_reg_adapter;
`uvm_object_utils (apb_adapter)
// Set default values for the two variables based on bus protocol
// APB does not support either, so both are turned off
function new(string name="apb_adapter");
super.new(name);
supports_byte_enable = 0;
provides_responses = 0;
endfunction
// This function accepts a register item of type "uvm_reg_bus_op" and assigns
// address, data and other required fields to the bus protocol sequence_item
virtual function uvm_sequence_item reg2bus (const ref uvm_reg_bus_op rw);
bus_pkt pkt = bus_pkt::type_id::create ("pkt");
pkt.write = (rw.kind == UVM_WRITE) ? 1: 0;
pkt.addr = rw.addr;
pkt.data = rw.data;
`uvm_info ("adapter", $sformatf ("reg2bus addr=0x%0h data=0x%0h kind=%s", pkt.addr, pkt.data, rw.kind.name), UVM_DEBUG)
return pkt;
endfunction
// This function accepts a bus sequence_item and assigns address/data fields to
// the register item
virtual function void bus2reg (uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
bus_pkt pkt;
// bus_item is a base class handle of type "uvm_sequence_item" and hence does not
// contain addr, data properties in it. Hence bus_item has to be cast into bus_pkt
if (! $cast (pkt, bus_item)) begin
`uvm_fatal ("reg2apb_adapter", "Failed to cast bus_item to pkt")
end
rw.kind = pkt.write ? UVM_WRITE : UVM_READ;
rw.addr = pkt.addr;
rw.data = pkt.data;
rw.status = UVM_IS_OK; // APB does not support slave response
`uvm_info ("adapter", $sformatf("bus2reg : addr=0x%0h data=0x%0h kind=%s status=%s", rw.addr, rw.data, rw.kind.name(), rw.status.name()), UVM_DEBUG)
endfunction
endclass
由于APB总线协议不支持字节启用,所以位supports_byte_enable设置为0。如果agent的driver通过put()或item_done()提供了一个单独的响应项,那么应该将provides_responses位设置为1,以便寄存器模型知道它必须等待一个响应,然后才能将其转换为寄存器item。如果设置了这个位,而agent的driver不提供任何响应,那么仿真就有可能挂起。由于APB协议不支持从响应,因此该位设置为0。
寄存器预测器(Register Predictor)
寄存器模型有不同的方法来更新模型并保持它的寄存器副本与DUT中的值同步。默认情况下,它在每次执行读或写事务时更新寄存器模型。例如,如果使用write()方法将一个值写入到设计中,它就可以使用写入的数据轻松地更新模型中该寄存器的镜像值。类似地,当read()方法从设计中获取读取数据时,它可以相应地更新镜像值。
由于具有地址和数据的单个序列可以在同一个目标agent上开始写入设计寄存器,因此并不要求总是使用寄存器模型来写入设计。这将使寄存器模型中的值过时,并且需要在每次其他序列对设计进行读写时进行更新。可以在目标总线agent的接口上放置一个称为预测器的组件,以监视任何事务并相应地更新寄存器模型。
// uvm_reg_predictor class definition
class uvm_reg_predictor #(type BUSTYPE=int) extends uvm_component;
`uvm_component_param_utils(uvm_reg_predictor#(BUSTYPE))
uvm_analysis_imp #(BUSTYPE, uvm_reg_predictor #(BUSTYPE)) bus_in;
uvm_reg_map map;
uvm_reg_adapter adapter;
...
endclass
uvm_reg_predictor组件是uvm_subscriber的子类,它有一个analysis端口,能够从目标监视器接收总线序列item。它使用寄存器适配器将传入的总线包转换为通用寄存器item,然后从寄存器映射中查找地址并更新寄存器内容。这是独立于协议的,因此我们不需要定义自定义类。然而,我们必须创建一个参数化版本的寄存器预测器,如下所示,它可以集成到我们的寄存器环境中。
预测器的集成步骤(Steps to integrate a predictor)
- 声明带有目标总线事务类型的参数化的preditor
// Here "bus_pkt" is the sequence item sent by the target monitor to this predictor
uvm_reg_predictor #(bus_pkt) m_apb_predictor;
- 在寄存器环境中构建predictor
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
m_apb_predictor = uvm_reg_predictor#(bus_pkt)::type_id::create("m_apb_predictor", this);
endfunction
- 连接register map、adapter和analysis port到predictor
virtual function void connect_phase(uvm_phase phase);
super.connect_phase(phase);
// 1. Provide register map to the predictor
m_apb_predictor.map = m_ral_model.default_map;
// 2. Provide an adapter to help convert bus packet into register item
m_apb_predictor.adapter = m_apb_adapter;
// 3. Connect analysis port of target monitor to analysis implementation of predictor
m_apb_agent.ap.connect(m_apb_predictor.bus_in);
endfunction
寄存器环境的集成(Register environment integration)
使用上面所有的组件集成到一个单独的寄存器环境中,以使具有可重用性。
class reg_env extends uvm_env;
`uvm_component_utils (reg_env)
function new (string name="reg_env", uvm_component parent);
super.new (name, parent);
endfunction
uvm_agent m_agent; // Agent handle
ral_my_design m_ral_model; // Register Model
reg2apb_adapter m_apb_adapter; // Convert Reg Tx <-> Bus-type packets
uvm_reg_predictor #(bus_pkt) m_apb_predictor; // Map APB tx to register in model
virtual function void build_phase (uvm_phase phase);
super.build_phase (phase);
m_ral_model = ral_my_design::type_id::create ("m_ral_model", this);
m_apb_adapter = m_apb_adapter :: type_id :: create ("m_apb_adapter");
m_apb_predictor = uvm_reg_predictor #(bus_pkt) :: type_id :: create ("m_apb_predictor", this);
m_ral_model.build ();
m_ral_model.lock_model ();
uvm_config_db #(ral_my_design)::set (null, "uvm_test_top", "m_ral_model", m_ral_model);
endfunction
virtual function void connect_phase (uvm_phase phase);
super.connect_phase (phase);
m_apb_predictor.map = m_ral_model.default_map;
m_apb_predictor.adapter = m_apb_adapter;
m_agent.ap.connect(m_apb_predictor.bus_in);
endfunction
endclass
在build_phase()中必须声明和创建model、adapter和predictor三个组件。需要注意的是,必须通过调用其lock()函数来锁定寄存器模型,以防止任何其他测试台组件或部分修改结构或向其添加寄存器。注册模型的build()方法是一个自定义函数,不是标准UVM库的一部分,只是在模型中初始化构建子块、映射和寄存器器。最好将此模型放在配置数据库中的某个位置,以便其他组件可以访问它。
现在,我们必须为预测器提供一个映射方案,以便它能够将地址值与模型中寄存器的值进行匹配,还需要适配器的句柄,以便它可以直接接受转换后的总线值。这最好在上面所示的connect_phase()方法中完成。
寄存器环境的连接
在此之前,我们已经定义和创建了寄存器环境中所需的一切。但是,还没有定义哪个agent负责驱动这些寄存器事务。
class my_env extends uvm_env;
`uvm_component_utils(my_env)
my_agent m_agent;
reg_env m_reg_env;
function new (string name = "my_env", uvm_component parent);
super.new(name,parent);
endfunction
virtual function void build_phase (uvm_phase phase);
super.build_phase(phase);
m_agent = my_agent::type_id::create ("m_agent", this);
m_reg_env = reg_env::type_id::create ("m_reg_env", this);
endfunction
virtual function void connect_phase (uvm_phase phase);
super.connect_phase(phase);
m_agent.m_mon.mon_ap.connect (m_reg_env.m_apb2reg_predictor.bus_in);
m_reg_env.m_ral_model.default_map.set_sequencer (m_agent.m_seqr,m_reg_env.m_reg2apb);
endfunction
endclass