[UVM源代码研究] 聊聊寄存器模型的uvm_reg_adapter
引言
应某位粉丝的留言邀请,想让我讲讲寄存器模型如何适配不同的总线接口协议,回顾下之前写过的文章,写过不少关于寄存器模型的,但是竟然没有系统的写过uvm_reg_adapter相关的内容,这里就补上一篇。
这里要做一个基础概念的澄清,寄存器模型本质上是跟总线协议解耦的,只跟我们寄存器表的spec相对应,也就是说对于设计提供的同一份寄存器表,可以用在任何接口协议上,换句话说就是寄存器模型本质绑定的是寄存器表。
而总线接口绑定的是我们UVM验证环境中的agent,spi接口对应spi_agent,i2c接口对应i2c_agent,AMBA接口对应的就是几种AMBA协议的agent。
明白了上面两点基础概念,再回到粉丝的问题上来,各种接口协议的寄存器模型区别在哪里呢?那自然区别就在于接口协议不同,也就是用到了不同的agent,而寄存器模型本身都是相同的。那么同一个寄存器模型又是如何适配不同的接口协议的agent的呢?那就用到了我们本文的主角uvm_reg_adapter。可以打这么个比方,寄存器模型就像我们使用的手机,而agent就像不同国家的电网对应的不同的交直流电压标准,我们手机如何做到在不同的国家都能充电呢,这就用到了电压转换器(power adapter),uvm_reg_adapter就起到了类似于电压转换器的功能。
下面讲讲UVM源代码中与uvm_reg_adapter相关的内容。
UVM源代码分析
先看看uvm_reg_adapter的源代码,文件内容在存放在 \src\reg\uvm_reg_adapter.svh,内容不多,罗列如下:
//------------------------------------------------------------------------------//// Class: uvm_reg_adapter//// This class defines an interface for converting between <uvm_reg_bus_op>// and a specific bus transaction.//------------------------------------------------------------------------------virtual class uvm_reg_adapter extends uvm_object;// Function: new//// Create a new instance of this type, giving it the optional ~name~.function new(string name="");super.new(name);endfunction// Variable: supports_byte_enable//// Set this bit in extensions of this class if the bus protocol supports// byte enables.bit supports_byte_enable;// Variable: provides_responses//// Set this bit in extensions of this class if the bus driver provides// separate response items.bit provides_responses;// Variable: parent_sequence//// Set this member in extensions of this class if the bus driver requires// bus items be executed via a particular sequence base type. The sequence// assigned to this member must implement do_clone().uvm_sequence_base parent_sequence;// Function: reg2bus//// Extensions of this class ~must~ implement this method to convert the specified// <uvm_reg_bus_op> to a corresponding <uvm_sequence_item> subtype that defines the bus// transaction.//// The method must allocate a new bus-specific <uvm_sequence_item>,// assign its members from// the corresponding members from the given generic ~rw~ bus operation, then// return it.pure virtual function uvm_sequence_item reg2bus(constref uvm_reg_bus_op rw);// Function: bus2reg//// Extensions of this class ~must~ implement this method to copy members// of the given bus-specific ~bus_item~ to corresponding members of the provided// ~bus_rw~ instance. Unlike <reg2bus>, the resulting transaction// is not allocated from scratch. This is to accommodate applications// where the bus response must be returned in the original request.pure virtual function voidbus2reg(uvm_sequence_item bus_item,ref uvm_reg_bus_op rw);local uvm_reg_item m_item;// function: get_item//// Returns the bus-independent read/write information that corresponds to// the generic bus transaction currently translated to a bus-specific// transaction.// This function returns a value reference only when called in the// <uvm_reg_adapter::reg2bus()> method.// It returns null at all other times.// The content of the return <uvm_reg_item> instance must not be modified// and used strictly to obtain additional information about the operation.virtual function uvm_reg_item get_item();return m_item;endfunctionvirtual function void m_set_item(uvm_reg_item item);m_item = item;endfunctionendclass
我们来分析下其中包含的变量和方法。
support_byte_enable
这个变量主要用在支持byte选择的总线,比如说AMBA总线本身支持write strobe的功能,就可以使用byte enable,要想实现这个功能就必须将这个变量设为1,这样就可以对总线实现byte级别的访问,反之则必须访问完整的总线数据线。像spi/i2c这种就不支持,使用默认值0即可。
prvides_responses
这个变量用来控制adapter在调用bus2reg函数的时候从agent拿到的是resp还是原始发送的req,关于这个变量的用法我在之前的的文章 [UVM源代码研究] 当我们driver中使用put_response却最终导致Reponse queue overflow的UVM源代码解决思路(uvm-1.2版) 有详细介绍过,这里就不细讲了,有不清楚的可以过去看看。
local uvm_reg_item m_item
如注释部分描述,该变量可以通过调用get_item获取当前正在reg2bus中传输的寄存器信息,实际也很少会去使用。
reg2bus
将寄存器模型里的uvm_reg_bus_op类型的变量转化为接口协议类型对应的变量。结构体类型uvm_reg_bus_op定义在文件 /src/reg/uvm_reg_item.svh 中,代码如下所示。
//------------------------------------------------------------------------------//// CLASS: uvm_reg_bus_op//// Struct that defines a generic bus transaction for register and memory accesses, having// ~kind~ (read or write), ~address~, ~data~, and ~byte enable~ information.// If the bus is narrower than the register or memory location being accessed,// there will be multiple of these bus operations for every abstract// <uvm_reg_item> transaction. In this case, ~data~ represents the portion// of <uvm_reg_item::value> being transferred during this bus cycle.// If the bus is wide enough to perform the register or memory operation in// a single cycle, ~data~ will be the same as <uvm_reg_item::value>.//------------------------------------------------------------------------------typedef struct {// Variable: kind//// Kind of access: READ or WRITE.//uvm_access_e kind;// Variable: addr//// The bus address.//uvm_reg_addr_t addr;// Variable: data//// 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// being transferred this bus cycle.//uvm_reg_data_t data;// Variable: n_bits//// The number of bits of <uvm_reg_item::value> being transferred by// this transaction.int n_bits;/*constraint valid_n_bits {n_bits > 0;n_bits <= `UVM_REG_DATA_WIDTH;}*/// Variable: byte_en//// 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;// Variable: status//// 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;
这个结构体对应的变量是寄存器模型通过adapter跟agent之间转换的最小单位,一个寄存器模型操作对应一个uvm_reg_item变量,该变量可能转换为多个uvm_reg_bus_op操作,例如寄存器模型定义的寄存器是32位的,但是总线宽度是8,那么一次寄存器模型的操作会转换出4次总线访问,即reg2bus会被调用4次,uvm_reg_adapter不仅做总线协议的适配,还做了寄存器与总线位宽的适配。
uvm_access_e:定义在文件 /src/reg/uvm_reg_model.svh 中,代码如下所示:
// Enum: uvm_access_e//// Type of operation begin performed//// UVM_READ - Read operation// UVM_WRITE - Write operation//typedef enum {UVM_READ,UVM_WRITE,UVM_BURST_READ,UVM_BURST_WRITE} uvm_access_e;
对于UVM_BURST_READ和UVM_BURST_WRITE这两个访问类型,我们只在对memory建模的时候才会用到,uvm_mem中包含了方法burst_read()和burst_write()操作会产生BURST类型的kind操作传递到uvm_reg_adapter中,相应的reg2bus中就需要对busrt操作做建模,一般情况下如果我们只对寄存器进行访问的话是不需要关心UVM_BURST_READ, UVM_BURST_WRITE操作的。
uvm_reg_addr_t addr:uvm_reg_addr_t 类型定义在 /src/reg/uvm_reg_model.svh 中,如下所示:
// Type: uvm_reg_addr_logic_t//// 4-state address value with <`UVM_REG_ADDR_WIDTH> bits//typedef logic unsigned [`UVM_REG_ADDR_WIDTH-1:0] uvm_reg_addr_logic_t ;
UVM_REG_ADDR_WIDTH默认值为64,定义在文件 /src/macros/uvm_reg_defines.svh 中,UVM_REG_ADDR_WIDTH值与地址总线位宽相关,需要在编译阶段使用宏定义指定。
uvm_reg_data_t data:uvm_reg_data_t 类型定义在 /src/reg/uvm_reg_model.svh 中,如下所示:
// Type: uvm_reg_data_t//// 2-state data value with <`UVM_REG_DATA_WIDTH> bits//typedef bit unsigned [`UVM_REG_DATA_WIDTH-1:0] uvm_reg_data_t ;
UVM_REG_DATA_WIDTH默认值为64,定义在文件 /src/macros/uvm_reg_defines.svh 中,UVM_REG_DATA_WIDTH值与寄存器表寄存器的位宽严格对应,跟数据总线位宽无关,需要在编译阶段使用宏定义指定。
int n_bits:对应单次操作总线的宽度,一般不需要用户显式引用。
uvm_reg_byte_en_t byte_en:uvm_reg_byte_en_t 类型定义在 /src/reg/uvm_reg_model.svh 中,如下所示:
// Type: uvm_reg_byte_en_t//// 2-state byte_enable value with <`UVM_REG_BYTENABLE_WIDTH> bits//typedef bit unsigned [`UVM_REG_BYTENABLE_WIDTH-1:0] uvm_reg_byte_en_t ;
UVM_REG_BYTENABLE_WIDTH跟数据总线的位宽相关,UVM_REG_BYTENABLE_WIDTH=log2(UVM_REG_DATA_WIDTH),只有需要使用byte enable的总线擦需要再bus2reg/reg2bus函数内使用该变量,否则ignore该变量。
uvm_status_e status:uvm_status_e 类型定义在 /src/reg/uvm_reg_model.svh 中,如下所示:
// Enum: uvm_status_e//// Return status for register operations//// UVM_IS_OK - Operation completed successfully// UVM_NOT_OK - Operation completed with error// UVM_HAS_X - Operation completed successfully bit had unknown bits.//typedef enum {UVM_IS_OK,UVM_NOT_OK,UVM_HAS_X} uvm_status_e;
这个在解析bus2reg总线返回的数据是否有问题的的时候进行赋值,默认值是UVM_IS_OK,一般adapter也不会处理该变量,但是严格意义上讲是需要根据总线返回的数据是否存在问题来决定该变量的值的,然后寄存器模型调用的函数都包含返回信息status,根据这个status判断寄存器模型的相关是否有问题。
adapter的这个reg2bus是在uvm_reg_map中的的do_bus_write()和do_bus_read()方法中被调用的,即对寄存器模型的操作最终都会转化为uvm_reg_map中的do_bus_write()或do_bus_read(),进而完成了将寄存器模型的某次读写操作通过reg2bus函数转化为了总线上的某个transaction操作。
由于reg2bus这个函数原型需要匹配所有类型的总线协议,所以函数返回值需要使用一个基类uvm_sequence_item 类型,实际函数中return的类型是真实的总线协议类型,sequencer中拿到的就是一个指向具体协议类型transaction的基类uvm_sequence_item变量,sequencer中会执行$cast操作进行类型转换。
bus2reg
我们以uvm_reg_map.svh文件中do_bus_read()为例讲解下完整的寄存模型读操作adapter的reg2bus和bus2reg是如何被调用的,如图1所示。

reg2bus实现了将寄存器模型的uvm_reg_bus_op类型的数据转化为具体协议的transaction类型给到sequencer,sequencer将这个uvm_sequence_item类型的数据$cast成对应协议的transaction给到driver,driver将该transaction驱动到总线上再执行seq_item_port.item_done()标志一次总线操作的结束,即图1中的2022行等待的结果。然后adapter中的bus2reg会被调用执行,函数参数是用原始给到driver的transaction(即req)还是driver返回的transaction(rsp,如果有put_response(rsp)或者seq_item_port.item_done(rsp)带参数)由上文提到的provides_responses变量决定,这段代码实现即图1中的2024-2033行。
使用示例
下面以spi、apb、tilelink为例介绍三种不同的总线协议对应的uvm_reg_adapter使用差别。
spi_adapter
先介绍下spi协议访问总线的定义。寄存器都是8bit位宽的(编译需要加上参数+define+UVM_REG_DATA_WIDTH=8),spi每次访问总线位宽为8bit,每次寄存器操作分两个byte,第一个byte的最高位表示读(0)还是写(1),低7bit表示地址,第二byte表示读写数据。spi_transfer中只包含了wdata和rdata这两个动态数组,因为spi接口不包含地址总线,也就不存在地址控制信号的问题了。
这里直接偷懒默认所有的总线访问都是正确的。
代码如下所示。
`ifndef SPI_ADAPTER__SV`define SPI_ADAPTER__SVclass spi_adapter extends uvm_reg_adapter;`uvm_object_utils(spi_adapter)function new (string name="");super.new(name);endfunctionvirtual function uvm_sequence_item(const ref uvm_reg_bus_op rw);spi_transfer tr;tr = spi_transfer::type_id::create("tr");tr.randomize() with {wdata.size() == 2;if(rw.kink == UVM_WRITE) {wdata[0][7] == 1;wdata[1] = rw.data}else {wdata[0][7] == 0;}wdata[0][6:0] = rw.addr;};return tr;endfunctionvirtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op);spi_transfer tr;if(!$cast(tr, bus_item)) begin`uvm_fatal("TYPE_FATAL", "bus_item doesn't match!")endif(tr.wdata[0][7] == 0) beginrw.kind = UVM_READ;endelse beginrw.kind = UVM_WRITE;endrw.addr = tr.wdata[0][6:0];rw.data = tr.rdata[1];rw.status = UVM_IS_OK;endfunctionendclass`endif
apb_adapter
这个比较标准,没什么好解释的。
`ifndef APB_ADAPTER__SV`define APB_ADAPTER__SVclass apb_adapter extends uvm_reg_adapter;`uvm_object_utils(apb_adapter)function new (string name="");super.new(name);endfunctionvirtual function uvm_sequence_item(const ref uvm_reg_bus_op rw);apb_transfer tr;tr = apb_transfer::type_id::create("tr");tr.dir = (rw.kind == UVM_WRITE) ? APB_WRITE : APB_READ;tr.addr = rw.addr;tr.wdata = rw.data;return tr;endfunctionvirtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op);apb_transfer tr;if(!$cast(tr, bus_item)) begin`uvm_fatal("TYPE_FATAL", "bus_item doesn't match!")endrw.kind = (tr.dir == APB_READ) ? UVM_READ : UVM_WRITE ;rw.addr = tr.addr;rw.data = tr.rdata;rw.status = UVM_IS_OK;endfunctionendclass`endif
tilelink_adapter
这个是CPU总线,涉及到系统总线(ABCDE通道,总线数据位宽64位,需要支持缓存一致性设计,这里进行寄存器访问可以简化为AD通道)和外设总线(AD通道,总线数据位宽32位),稍微复杂一点,简单介绍下tilelink adapter几个关键功能:
-
tilelink协议主机端发送a通道,从机端返回d通道,所以写的数据是a_data,地址是a_address,从机端返回的数据是d_data。
-
需要支持系统总线和外设总线两种总线接口,对应的接口信号参数需要做好参数化,这里为了演示方便使用了宏定义,实际会定义为参数化的类,像SOURCE、UVM_REG_DATA_WIDTH都会用参数替代,方便集成不同的总线接口。
-
系统总线位宽是64位,但是寄存器都是定义为32位的,挂在系统总线上的寄存器在主机访问的时候会把高低32位赋值为相同值,通过d_mask[7:0]告知高低有效位(即byte enable),需要支持字节选通,support_byte_enable配置为1。
-
tilelink有标识总线访问返回是否出错的信号d_corrupt,类似于AMBA总线的pslverr或者hresp,不同的是d_corrupt跟d_data是绑定的,返回的每一笔d_data都对应一个d_corrupt,可以用这个信号作为判断寄存器操作是否成功的标志以返回status。
-
tilelink支持pipeline操作,所以req跟rsp要分开,返回的数据需要用rsp表示,provides_responses 配置为1。
代码如下:
`ifndef TILELINKE_ADAPTER__SV`define TILELINKE_ADAPTER__SV`ifndef SOURCE`define SOURCE 6`endifclasstilelink_adapterextendsuvm_reg_adapter;bit [`SOURCE-1 : 0] valid_source;`uvm_object_utils(tilelink_adapter)function new (string name="");super.new(name);support_byte_enable = 1;provides_responses = 1;endfunctionvirtual function uvm_sequence_item(const ref uvm_reg_bus_op rw);tilelink_sequence_item tr;tr = tilelink_sequence_item::type_id::create("tr");tr.randomize() with (a_size == 2; burst_num == 1;);tr.a_opcode = (rw.kind == UVM_WRITE) ? PutFullData : Get;tr.a_address = rw.addr;if(`UVM_REG_DATA_WIDTH == 32) begintr.a_data[0] = rw.data;tr.a_mask[0] = rw.byte_en;endelse if(`UVM_REG_DATA_WIDTH == 64) begintr.a_data[0] = {rw.data, rw.data};if(rw.addr % 8 == 0) begintr.a_mask[0] = {4'b0, rw.byte_en};endelse begintr.a_mask[0] = {rw.byte_en, 4'b0};endendtr.a_source = valid_source;return tr;endfunctionvirtual functionvoidbus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op);tilelink_sequence_item tr;if(!$cast(tr, bus_item)) begin`uvm_fatal("TYPE_FATAL", "bus_item doesn't match!")endrw.kind = (tr.a_opcode == Get) ? UVM_READ : UVM_WRITE ;rw.addr = tr.a_address;if(`UVM_REG_DATA_WIDTH == 32) beginrw.data = tr.d_data[0];rw.byte_en = tr.a_mask[0];endelse if(`UVM_REG_DATA_WIDTH == 64) beginif(tr.a_address % 8 == 0) beginrw.byte_en = tr.a_mask[0][3:0];rw.data = tr.d_data[0][31:0];endelse beginrw.byte_en = tr.a_mask[0][7:4];rw.data = tr.d_data[0][63:32];endendforeach(tr.d_corrupt[i]) beginif(tr.d_corrupt[i] == 1) beginrw.status = UVM_NOT_OK;`uvm_warning("bus2reg", "bus corrupt!")break;endelse beginrw.status = UVM_IS_OK;continue;endendendfunctionendclass`endif
由于tilelink包含一些寄存器模型中没有的变量,需要单独定义,例如上面代码中valid_source,这个可以直接通过adapter的的句柄赋值传递。
结语
本文来自一个粉丝的投搞,想让我讲讲非APB协议的寄存器模型怎么写,于是查阅过往文章发现没有任何系统介绍过adapter,于是乎结合具体的使用案例将uvm_reg_adapter的UVM源代码实现简单讲解了一遍。
夜雨聆风
