UVM 中的寄存器模型
寄存器模型简介
寄存器读取方式
需要寄存器模型才能做的事情
寄存器模型可以简化操作, 如:
|
|
像启动sequence及将读取结果返回这些事情, 都会由寄存器模型来自动完成。
在没有寄存器模型之前, 只能启动sequence通过前门( FRONTDOOR) 访问的方式来读取寄存器, 局限较大,
在 scoreboard( 或者其他component) 中难以控制。 而有了寄存器模型之后, scoreboard只与寄存器模型打交道,
无论是发送读的指令还是获取读操作的返回值, 都可以由寄存器模型完成。 有了寄存器模型后, 可以在任何耗费时间的phase中使用寄存器模型以
前门访问或后门( BACKDOOR) 访问的方式来读取寄存器的值, 同时还能在某些不耗费时间的phase( 如check_phase) 中使用后门访问的方式来读取寄存器的值。
前门访问与后门访问是两种寄存器的访问方式。 所谓前门访问, 指的是通过模拟cpu在总线上发出读指令, 进行读写操作。
在这个过程中, 仿真时间( $time函数得到的时间) 是一直往前走的
寄存器模型还提供一些任务, 如mirror、 update, 它们可以批量完成寄存器模型与DUT中相关寄存器的交互
寄存器模型中的基本概念
uvm_reg_field
这是寄存器模型中的最小单位。 什么是reg_field? 假如有一个状态寄存器, 它各个位的含义下图所示。
如上的状态寄存器共有四个域, 分别是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
简单的寄存器模型
只有一个寄存器的寄存器模型
reg 代码
DUT 代码
|
|
reg_model.sv 代码:
|
|
在new函数中, 要将invert寄存器的宽度作为参数传递给super.new函数。 这里的宽度并不是指这个寄存器的有效宽度,
而是指这个寄存器中总共的位数。 如对于一个16位的寄存器, 其中可能只使用了8位, 那么这里要填写的是16, 而不是8。
这个数字一般与系统总线的宽度一致。 super.new中另外一个参数是是否要加入覆盖率的支持, 这里选择UVM_NO_COVERAGE, 即不支持。
每一个派生自uvm_reg的类都有一个build, 这个build与uvm_component的build_phase并不一样, 它不会自动执行, 而需要手工调用
, 与build_phase相似的是所有的uvm_reg_field都在这里实例化。 当reg_data实例化后, 要调用data.configure函数来配置这个字
段。
configure 参数
-
第一个参数: 第一个参数就是此域( uvm_reg_field) 的父辈, 也即此域位于哪个寄存器中, 这里当然是填写this了
-
第二个参数: 此域的宽度, 由于DUT中invert的宽度为1, 所以这里为
-
第三个参数:
此域的最低位在整个寄存器中的位置, 从0开始计数
。
假如一个寄存器如上图所示, 其低3位和高5位没有使用, 其中只有一个字段, 此字段的有效宽度为8位,
那么在调用configure时, 第二个参数就要填写8, 第三个参数则要填写3, 因为此reg_field是从第4位开始的 -
第四个参数: 此字段的存取方式
UVM共支持如下25种存取方式:
1) RO: 读写此域都无影响。
2) RW: 会尽量写入, 读取时对此域无影响。
3) RC: 写入时无影响, 读取时会清零。
4) RS: 写入时无影响, 读取时会设置所有的位。
5) WRC: 尽量写入, 读取时会清零。
6) WRS: 尽量写入, 读取时会设置所有的位。
7) WC: 写入时会清零, 读取时无影响。
8) WS: 写入时会设置所有的位, 读取时无影响。
9) WSRC: 写入时会设置所有的位, 读取时会清零。
10) WCRS: 写入时会清零, 读取时会设置所有的位。
11) W1C: 写1清零, 写0时无影响, 读取时无影响。
12) W1S: 写1设置所有的位, 写0时无影响, 读取时无影响。
13) W1T: 写1入时会翻转, 写0时无影响, 读取时无影响。
14) W0C: 写0清零, 写1时无影响, 读取时无影响。
15) W0S: 写0设置所有的位, 写1时无影响, 读取时无影响。
16) W0T: 写0入时会翻转, 写1时无影响, 读取时无影响。
17) W1SRC: 写1设置所有的位, 写0时无影响, 读清零。
18) W1CRS: 写1清零, 写0时无影响, 读设置所有位。
19) W0SRC: 写0设置所有的位, 写1时无影响, 读清零。
20) W0CRS: 写0清零, 写1时无影响, 读设置所有位。
21) WO: 尽可能写入, 读取时会出错。
22) WOC: 写入时清零, 读取时出错。
23) WOS: 写入时设置所有位, 读取时会出错。
24) W1: 在复位( reset) 后, 第一次会尽量写入, 其他写入无影响, 读取时无影响。
25) WO1: 在复位后, 第一次会尽量写入, 其他的写入无影响, 读取时会出错。
事实上, 寄存器的种类多种多样, 如上25种存取方式有时并不能满足用户的需求, 这时就需要自定义寄存器的模型 -
第五个参数: 表示是否是易失的( volatile) , 这个参数一般不会使用
-
第六个参数: 此域上电复位后的默认值
-
第七个参数: 此域是否有复位, 一般的寄存器或者寄存器的域都有上电复位值, 因此这里一般也填写1。
-
第八个参数: 这个域是否可以随机化。
这主要用于对寄存器进行随机写测试, 如果选择了0, 那么此域将不会随机化, 而一直是复位值, 否则将会随机出一个数值来。
这一个参数当且仅当第四个参数为RW、 WRC、 WRS、 WO、 W1、 WO1时才有效
。 -
第九个参数: 这个域是否可以单独存取
实例化寄存器
|
|
同uvm_reg派生的类一样, 每一个由uvm_reg_block派生的类也要定义一个build函数, 一般在此函数中实现所有寄存器的实例化。
-
对应 uvm_reg_map
一个uvm_reg_block中一定要对应一个uvm_reg_map, 系统已经有一个声明好的default_map, 只需要在build中将其实例化。
这个实例化的过程并不是直接调用uvm_reg_map的new函数, 而是通过调用uvm_reg_block的create_map来实现
, create_map有众多的参数, 其中:- 第一个参数是名字,
- 第二个参数是基地址,
- 第三个参数则是系统总线的宽度, 这里的单位是byte而不是bit,
- 第四个参数是大小端,
- 最后一个参数表示是否能够按照byte寻址。
-
实例化 reg field
随后实例化invert并调用invert.configure函数。
这个函数的主要功能是指定寄存器进行后门访问操作时的路径
。- 第一个参数是此寄存器所在uvm_reg_block的指针, 这里填写this,
- 第二个参数是reg_file的指针这里暂时填写null,
- 第三个参数是此寄存器的后门访问路径, 这里暂且为空。 当调用完configure时, 需要手动调用 invert的build函数, 将invert中的域实例化。
-
加入default_map
最后一步则是将此寄存器加入default_map中。 uvm_reg_map的作用是存储所有寄存器的地址, 因此必须将实例化的寄存器加
入default_map中, 否则无法进行前门访问操作。
add_reg函数的参数:- 第一个参数是要加入的寄存器,
- 第二个参数是寄存器的地址, 这里是16’h9, 第三个参数是此寄存器的存取方式。
-
小结
到此为止, 一个简单的寄存器模型已经完成
回顾一下前面介绍过的寄存器模型中的一些常用概念。 uvm_reg_field是最小的单位, 是具体存储寄存器数值的变量, 可以直接用这个类。
uvm_reg则是一个“空壳子”, 或者用专业名词来说, 它是一个纯虚类, 因此是不能直接使用的
, 必须由其派生一个新类,
在这个新类中至少加入一个uvm_reg_field, 然后这个新类才可以使用。 uvm_reg_block则是用于组织大量uvm_reg的一个大容器。
打个比方说, uvm_reg是一个小瓶子, 其中必须装上药丸( uvm_reg_field) 才有意义, 这个装药丸的过程就是定义派生类的过程,
而uvm_reg_block则是一个大箱子, 它中可以放许多小瓶子( uvm_reg) , 也可以放其他稍微小一点的箱子( uvm_reg_block) 。
整个寄存器模型就是一个大箱子( uvm_reg_block) 。
将寄存器模型集成到验证平台中
寄存器模型的前门访问方式工作流程如下图所示, 其中图a为读操作, 图b为写操作:
寄存器模型的前门访问操作可以分成读和写两种。 无论是读或写, 寄存器模型都会通过sequence产生一个uvm_reg_bus_op的变量,
此变量中存储着操作类型( 读还是写) 和操作的地址, 如果是写操作, 还会有要写入的数据。 此变量中的信息要经过一个
转换器( adapter) 转换后交给bus_sequencer, 随后交给bus_driver, 由bus_driver实现最终的前门访问读写操作。
因此, 必须要定义好一个转换器
。 如下例为一个简单的转换器的代码
my_adapter.sv
|
|
一个转换器要定义好两个函数:
- reg2bus, 其作用为将寄存器模型通过sequence发出的uvm_reg_bus_op型的变量转换成 bus_sequencer能够接受的形式,
- bus2reg, 其作用为当监测到总线上有操作时, 它将收集来的transaction转换成寄存器模型能够接受的形式, 以便寄存器模型能够更新相应的寄存器的值。
说到这里, 不得不考虑寄存器模型发起的读操作的数值是如何返回给寄存器模型的? 由于总线的特殊性, bus_driver在驱动总
线进行读操作时, 它也能顺便获取要读的数值, 如果它将此值放入从bus_sequencer获得的bus_transaction中时, 那么bus_transaction
中就会有读取的值, 此值经过adapter的bus2reg函数的传递, 最终被寄存器模型获取, 这个过程如图 将寄存器模型集成到验证平台中 所示
由于并没有实际的 transaction的传递, 所以从driver到adapter使用了虚线
转换器写好之后, 就可以在base_test中加入寄存器模型了:
base_test.sv
|
|
要将一个寄存器模型集成到base_test中, 那么至少需要在base_test中定义两个成员变量,
一是reg_model, 另外一个就是 reg_sqr_adapter。 将所有用到的类在build_phase中实例化。
在实例化后reg_model还要做四件事:
- 第一是调用configure函数, 其第一 个参数是parent block, 由于是最顶层的reg_block, 因此填写null, 第二个参数是后门访问路径, 这里传入一个空的字符串。
- 第二是调用build函数, 将所有的寄存器实例化。
- 第三是调用lock_model函数, 调用此函数后, reg_model中就不能再加入新的寄存器了。
- 第四是调用reset函数, 如果不调用此函数, 那么reg_model中所有寄存器的值都是0, 调用此函数后, 所有寄存器的值都将变为设置的复位值。
寄存器模型的前门访问操作最终都将由uvm_reg_map完成, 因此在connect_phase中, 需要将转换器和bus_sequencer通过 set_sequencer函数告知reg_model的default_map, 并将default_map设置为自动预测状态。
在验证平台中使用寄存器模型
当一个寄存器模型被建立好后, 可以在sequence和其他component中使用。 以在参考模型中使用为例, 需要在参考模型中有一个寄存器模型的指针
base_test.sv
|
|
my_model.sv
|
|
在 base_test.sv 中已经为env的p_rm赋值, 因此只需要在env中将p_rm传递给参考模型即可:
my_env.sv
|
|
寄存器模型提供了两个基本的任务: read和write。
-
read
若要在参考模型中读取寄存器, 使用read任务
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
文件: src/ch7/section7.2/my_model.sv 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); //`uvm_info("my_model", "get one transaction, copy and print it:", UV M_LOW) //new_tr.print(); if(value) invert_tr(new_tr); ap.write(new_tr); end endtask
read任务的原型如下所示
1 2 3 4 5 6 7 8 9 10
来源: 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
由于参考模型一般不会写寄存器, 因此对于write任务, 以在virtual sequence进行写操作为例说明。
在sequence中使用寄存器模型, 通常通过p_sequencer的形式引用。 需要首先在sequencer中有一个寄存器模型的指针, base_test.sv 中已经为v_sqr.p_rm赋值
了。 因此可以直接以如下方式进行写操作
my_case0.sv1 2 3 4 5 6 7 8 9 10 11
文件: src/ch7/section7.2/my_case0.sv class case0_cfg_vseq extends uvm_sequence; … virtual task body(); uvm_status_e status; uvm_reg_data_t value; … p_sequencer.p_rm.invert.write(status, 1, UVM_FRONTDOOR); … endtask endclass
write任务的原型为:
1 2 3 4 5 6 7 8 9 10
来源: 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。
寄存器模型对sequence的transaction类型没有任何要求。 因此, 可以在一个发送my_transaction的sequence中使用寄存器模型对寄存器进行读写操作。
后门访问与前门访问
前门访问
所谓前门访问操作就是通过寄存器配置总线( 如APB协议、 OCP协议、 I2C协议等) 来对DUT进行操作。 无论在任何总线协
议中, 前门访问操作只有两种: 读操作和写操作。 前门访问操作是比较正统的用法。 对一块实际焊接在电路板上正常工作的芯片
来说, 此时若要访问其中的某些寄存器, 前门访问操作是唯一的方法
对于参考模型来说, 最大的问题是如何在其中启动一个sequence, 当时列举了全局变量和config_db的两种方式。
除了这两种方式之外, 如果能够在参考模型中得到一个sequencer的指针, 也可以在此sequencer上启动一个sequence。
这通常比较容易实现, 只要在其中设置一个p_sqr的变量, 并在env中将sequencer的指针赋值给此变量即可。
用来写的 sequence
|
|
用来读的 sequence
|
|
sequence是自动执行的, 但是在其执行完毕后( body及post_body调用完成) , 为此sequence分配的内存依然是有效的, 所以可以使用reg_seq继续引用此sequence。 上述读操作正是用到了这一点
在寄存器模型中访问
上述sequence 的操作方式的关键是在参考模型中有一个sequencer的指针, 而在
寄存器模型中也有一个这样的指针, 在base_test.sv 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中的读数据返回参考模型。
根据 sequence的应答机制, 如果driver一直发送应答而sequence不收集应答, 那么将会导致sequencer的应
答队列溢出。 UVM考虑到这种情况, 在adapter中设置了provide_responses选项:
|
|
在设置了此选项后, 寄存器模型在调用bus2reg将目标transaction转换成uvm_reg_item时, 其传入的参数是rsp, 而不是req。 使用应答机制的操作流程为
· 参考模型调用寄存器模型的读任务。
· 寄存器模型产生sequence, 并产生uvm_reg_item: rw。
· 产生driver能够接受的transaction: bus_req=adapter.reg2bus(rw) 。
· 将bus_req交给bus_sequencer。
· driver得到bus_req, 驱动它, 得到读取的值, 并将读取值放入rsp中, 调用item_done。
· 寄存器模型调用adapter.bus2reg(rsp, rw) 将rsp中的读取值传递给rw。
· 将rw中的读数据返回参考模型。
后门访问
DUT
|
|
这个 DUT中加入了寄存器counter。 它的功能就是统计rx_dv为高电平的时钟数
在通信系统中, 有大量计数器用于统计各种包裹的数量, 如超长包、 长包、 中包、 短包、 超短包等。 这些计数器的一个共同的特点是它们是只读的,
DUT的总线接口无法通过前门访问操作对其进行写操作。 除了是只读外, 这些寄存器的位宽一般都比较宽, 如32位、 48位或者64位等,
它们的位宽超过了设计中对加法器宽度的上限限制。 计数器在计数过程中需要使用加法器, 对于加法器来说, 在同等工艺下, 位宽越宽则其时序越差,
因此在设计中一般会规定加法器的最大位宽。 在上述DUT中, 加法器的位宽被限制在16位。 要实现32位的counter的加法操作, 需要使用两个叠加的16位加法器。
为counter分配16‘h5和16’h6的地址, 采用大端格式将高位数据存放在低地址。 此计数器是可读的, 另外可以对其进行写1清0 操作。
如果对其写入其他数值, 则不会起作用。
后门操作举例
后门访问是与前门访问相对的操作, 从广义上来说, 所有不通过DUT的总线而对DUT内部的寄存器或者存储器进行存取的操作都是后门访问操作。
如在top_tb中可以使用如下方式对counter赋初值
top_tb.sv
|
|
所有后门访问操作都是不消耗仿真时间( 即$time打印的时间) 而只消耗运行时间的。 这是后门访问操作的最大优势。 既然有了前门访问操作,
那么为什么还需要后门访问操作呢? 后门访问操作存在的意义在于:
-
后门访问操作能够更好地完成前门访问操作所做的事情。 后门访问不消耗仿真时间, 与前门访问操作相比, 它消耗的运行时间要远小于前门访问操作的运行时间。
在一个大型芯片的验证中, 在其正常工作前需要配置众多的寄存器, 配置时间可能要达到一个或几个小时, 而如果使用后门访问操作, 则时间可能缩短为原来的1/100。 -
后门访问操作能够完成前门访问操作不能完成的事情。 如在网络通信系统中, 计数器通常都是只读的(有一些会附加清零功能) , 无法对其指定一个非零的初值。
而大部分计数器都是多个加法器的叠加, 需要测试它们的进位操作。 本节DUT的counter 使用了两个叠加的16位加法器,
需要测试当计数到32‘ hFFFF时能否顺利进位成为32’ h1_0000, 这可以通过延长仿真时间来使其计数到32‘ hFFFF
这在本节的DUT中是可以的, 因为计数器每个时钟都加1。 但是在实际应用中, 可能要几万个或者更多的时钟才会加1, 因此需要大量的运行时间, 如几天。
这只是32位加法器的情况, 如果是48位的计数器, 情况则会更坏。 这种情况下, 后门访问操作能够完成前门访问操作完成的事情, 给只读的寄存器一个初值
当然, 与前门访问操作相比, 后门访问操作也有其劣势。 如所有的前门访问操作都可以在波形文件中找到总线信号变化的波形及所有操作的记录。但是后门访问操作则无法在波形文件中找到操作痕迹。 其操作记录只能仰仗验证平台编写者在进行后门访问操作时输出的打印信息, 这样便增加了调试的难度
使用interface进行后门访问操作
backdoor_if.sv
|
|
如果在monitor 中需要监测某个内部信号的话, 可以定义一个接口, 然后在 top_tb.sv 中把内部信号连到接口上
poke_counter为后门写, 而peek_counter为后门读。 在测试用例( 或者drvier、 scoreboard) 中, 若要对寄存器赋初值可以直接调用此函数:
my_case0.sv
|
|
小结
如果有n个寄存器, 那么需要写n个poke函数, 同时如果有读取要求的话, 还要写n个peek函数, 这限制了其使用, 且此文件完全没有任何移植性。
这种方式在实际中是有应用的, 它适用于不想使用寄存器模型提供的后门访问或者根本不想建立寄存器模型, 同时又必须要对DUT中的一个寄存器或一块存储器( memory) 进行后门访问操作的情况。
UVM中后门访问操作的实现: DPI+VPI
VIP 接口:
常用的VPI接口有如下两个:
|
|
其中vpi_get_value用于从RTL中得到一个寄存器的值。 vpi_put_value用于将RTL中的寄存器设置为某个值。
但是如果单纯地使用VPI进行后门访问操作, 在SystemVerilog与C/C++之间传递参数时将非常麻烦。 VPI是Verilog提供的接口,
DPI + VIP:
为了调用C/C++中的函数, 提供更好的用户体验, SystemVerilog提供了一种更好的接口: DPI。 如果使用DPI, 以读操作为例, 在 C/C++中定义如下一个函数:
|
|
在这个函数中通过最终调用vpi_get_value得到寄存器的值。
在SystemVerilog中首先需要使用如下的方式将在C/C++中定义的函数导入:
|
|
以后就可以在SystemVerilog中像普通函数一样调用uvm_hdl_read函数了。 这种方式比单纯地使用VPI的方式简练许多。 它可以直接将参数传递给C/C++中的相应函数, 省去了单纯使用VPI时繁杂的注册系统函数的步骤
整个过程如下图所示:
小结
在这种DPI+VPI的方式中, 要操作的寄存器的路径被抽像成了一个字符串, 而不再是一个绝对路径:
|
|
与 backdoor_if.sv 相比, 可以发现这种方式的优势:
路径被抽像成了一个字符串, 从而可以以参数的形式传递, 并可以存储, 这为建立寄存器模型提供了可能。
一个单纯的Verilog路径, 如top_tb.my_dut.counter, 它是不能被传递的, 也是无法存储的。
UVM中使用DPI+VPI的方式来进行后门访问操作, 它大体的流程是:
-
在建立寄存器模型时将路径参数设置好。
-
在进行后门访问的写操作时, 寄存器模型调用uvm_hdl_deposit函数:
1 2
来源: UVM 源代码 import "DPI-C" context function int uvm_hdl_deposit(string path, uvm_hdl_data_t value);
在C/C++侧, 此函数内部会调用vpi_put_value函数来对DUT中的寄存器进行写操作。
-
进行后门访问的读操作时, 调用uvm_hdl_read函数, 在C/C++侧, 此函数内部会调用vpi_get_value函数来对DUT中的寄存器进行读操作, 并将读取值返回。
UVM中后门访问操作接口
在掌握UVM中后门访问操作的原理后, 就可以使用寄存器模型的后门访问功能。 要使用这个功能, 需要做如下的准备
在reg_block中调用uvm_reg的configure函数时, 设置好第三个路径参数
:
|
|
由于counter是32bit, 占据两个地址, 因此在寄存器模型中它是作为两个寄存器存在的。
当上述工作完成后, 在将寄存器模型集成到验证平台时, 需要设置好根路径hdl_root
:
|
|
确保 DUT 中有要访问的寄存器, 如果要后门访问counter 但是DUT 中没有的话会报错如
:
|
|
UVM提供两类后门访问的函数: 一是UVM_BACKDOOR形式的read和write, 二是peek和poke。 这两类函数的区别是, 第一类
会在进行操作时模仿DUT的行为, 第二类则完全不管DUT的行为。 如对一个只读的寄存器进行写操作, 那么第一类由于要模拟
DUT的只读行为, 所以是写不进去的, 但是使用第二类可以写进去
poke函数用于第二类写操作, 其原型为:
|
|
peek函数用于第二类的读操作, 其原型为:
|
|
无论是peek还是poke, 其常用的参数都是前两个。 各自的第一个参数表示操作是否成功, 第二个参数表示读写的数
在sequence中, 可以使用如下的方式来调用这两个任务:
|
|
复杂的寄存器模型
层次化的寄存器模型
一般的, 只会在第一级的uvm_reg_block中加入寄存器, 而第二级的uvm_reg_block通常只添加uvm_reg_block。
这样从整体上呈现出一个比较清晰的结构。 假如一个DUT分了三个子模块: 用于控制全局的global模块、 用于缓存数据的buf模块、 用于接收发送以太网帧的mac模块。
global模块寄存器的地址为0x0000~0x0FFF, buf部分的寄存器地址为0x1000~0x1FFF, mac部分的寄存器地址为0x2000~0x2FFF, 那么可以按照如下方式定义寄存器模型:
|
|
要将一个子reg_block加入父reg_block中,
第一步是先实例化子reg_block
。第二步是调用子reg_block的configure函数
。
如果需要使用后门访问, 则在这个函数中要说明子reg_block的路径, 这个路径不是绝对路径, 而是相对于父reg_block来说的路径( 简单起见, 上述代码中的路径参数设置为空字符串, 不能发起后门访问操作) 。第三步是调用子reg_block的build函数
。第四步是调用子 reg_block的lock_model函数
。第五步则是将子reg_block的default_map以子map的形式加入父reg_block的default_map中
。
这是可以理解的, 因为一般在子reg_block中定义寄存器时, 给定的都是寄存器的偏移地址, 其实际物理地址还要再加上一个基地址。
寄存器前门访问的读写操作最终都要通过default_map来完成。 很显然, 子reg_block的default_map并不知道寄存器的基地址,
它只知道寄存器的偏移地址, 只有将其加入父reg_block的default_map, 并在加入的同时告知子map的偏移地址,
这样父reg_block的default_map 就可以完成前门访问操作了。
因此, 一般将具有同一基地址的寄存器作为整体加入一个uvm_reg_block中, 而不同的基地址对应不同的uvm_reg_block。
每个uvm_reg_block一般都有与其对应的物理地址空间。 对于本节介绍的子reg_block, 其里面还可以加入小的reg_block, 这相当于将地址空间再次细化。
reg_file 的作用
到目前为止, 引入了uvm_reg_field、 uvm_reg、 uvm_reg_block的概念, 这三者的组合已经能够组成一个可以使用的寄存器模型了。
然而, UVM的寄存器模型中还有一个称为uvm_reg_file的概念。 这个类的引入主要是用于区分不同的hdl路径。
假设有两个寄存器regA和regB, 它们的hdl路径分别为top_tb.mac_reg.fileA.regA和top_tb.mac_reg.fileB.regB, 延续上一节的例
子, 设top_tb.mac_reg下面所有寄存器的基地址为0x2000, 这样, 在最顶层的reg_block中加入mac模块时, 其hdl路径要写成
|
|
相应的, 在mac_blk的build中, 要通过如下方式将regA和regB的路径告知寄存器模型:
|
|
当fileA中的寄存器只有一个regA时, 这种写法是没有问题的, 但是假如fileA中有几十个寄存器时, 那么很显然, fileA.*会几十
次地出现在这几十个寄存器的configure函数里。 假如有一天, fileA的名字忽然变为filea_inst, 那么就需要把这几十行中所有fileA替
换成filea_inst, 这个过程很容易出错
为了适应这种情况, 在UVM的寄存器模型中引入了uvm_reg_file的概念。 uvm_reg_file同uvm_reg相同是一个纯虚类, 不能直接使用, 而必须使用其派生类:
|
|
如上所示, 先从uvm_reg_file派生一个类, 然后在my_blk中实例化此类, 之后调用其configure函数,
-
此函数的第一个参数是其所在的reg_block的指针 ,
-
第二个参数是假设此reg_file是另外一个reg_file的父文件, 那么这里就填写其父reg_file的指针 。
由于这里只有这一级reg_file, 因此填写null。 -
第三个参数则是此reg_file的hdl路径。 当把reg_file定义好后 , 在调用寄存器的configure参数时,就可以将其第二个参数设为reg_file的指针
加入reg_file的概念后, 当fileA变为filea_inst时, 只需要将file_a的configure参数值改变一下即可, 其他则不需要做任何改变。 这
大大减少了出错的概率
多个域的寄存器
前面所有例子中的寄存器都是只有一个域的, 如果一个寄存器有多个域时, 那么在建立模型时会稍有改变。
设某个寄存器有三个域, 其中最低两位为filedA, 接着三位为filedB, 接着四位为filedC, 其余位未使用。
这个寄存器从逻辑上来看是一个寄存器, 但是从物理上来看, 即它的DUT实现中是三个寄存器,
因此这一个寄存器实际上对应着三个不同的hdl路径: fieldA、 fieldB、 fieldC。 对于这种情况, 前面介绍的模型建立方法已经不适用了
|
|
这里要先从uvm_reg派生一个类, 在此类中加入3个uvm_reg_field。 在reg_block中将此类实例化后, 调用tf_reg.configure时要注
意, 最后一个代表hdl路径的参数已经变为了空的字符串, 在调用tf_reg.build之后要调用tf_reg.fieldA的configure函数。
调用完fieldA的configure函数后, 需要将fieldA的hdl路径加入tf_reg中, 此时用到的函数是add_hdl_path_slice。
这个函数的
- 第一个参数是要加入的路径,
- 第二个参数则是此路径对应的域在此寄存器中的起始位数, 如fieldA是从0开始的, 而fieldB是从2开始
的,
- 第三个参数则是此路径对应的域的位宽。
上述fieldA.configure和tf_reg.add_hdl_path_slice其实也可以如 reg 代码 那样在three_field_reg的build中被调用。 这两者有什么区别呢?
如果是在所定义的uvm_reg类中调用, 那么此uvm_reg其实就已经定型了, 不能更改了。 reg 代码 节中定义了具有一个域的uvm_reg派生类,
现在假如有一个新的寄存器, 它也是只有一个域, 但是这个域并不是如 reg 代码 节中那样占据了1bit, 而只占据了8bit,
那么此时就需要重新从uvm_reg派生一个类, 然后再重新定义。 如果reg 代码 节中的reg_invert在定义时并没有在其build中调用 reg_data的configure函数, 那么就不必重新定义。 因为没有调用configure之前, 这个域是不确定的
多个地址的寄存器
实际的DUT中, 有些寄存器会同时占据多个地址。 如 DUT 中的counter是32bit的, 而系统的数据位宽是16位的, 所以就占据了两个地址
UVM中后门访问操作接口 将一个寄存器分割成两个寄存器的方式加入寄存器模型中的。 因其每次要读取counter
的值时, 都需要对counter_low和counter_high各进行一次读取操作, 然后再将两次读取的值合成一个counter的值, 所以这种方式使
用起来非常不方便。
UVM提供另外一种方式, 可以使一个寄存器占据多个地址:
|
|
这种方法相对简单, 可以定义一个reg_counter, 并在其构造函数中指明此寄存器的大小为32位, 此寄存器中只有一个域, 此域的宽度也为32bit,
之后在reg_model中将其实例化即可。 在调用default_map的add_reg函数时, 要指定寄存器的地址, 这里只需要指明最小的一个地址即可。
这是因为在前面实例化default_map时, 已经指明了它使用UVM_LITTLE_ENDIAN形式, 同时总线的宽度为2byte, 即16bit,
UVM会自动根据这些信息计算出此寄存器占据两个地址。 当使用前门访问的形式读写此寄存器时, 寄存器模型会进行两次读写操作,
即发出两个transaction, 这两个transaction对应的读写操作的地址从0x05一直递增到0x06。
当将counter作为一个整体时, 可以一次性地访问它:
|
|
加入存储器
除了寄存器外, DUT中还存在大量的存储器。 这些存储器有些被分配了地址空间, 有些没有。 验证人员有时需要在仿真过程中得到存放在这些存储器中数据的值, 从而与期望的值比较并给出结果。
例如, 一个DUT的功能是接收一种数据, 它经过一些相当复杂的处理( 操作A) 后将数据存储在存储器中, 这块存储器是DUT内部的存储器, 并没有为其分配地址。
当存储器中的数据达到一定量时, 将它们读出, 并再另外做一些复杂处理( 如封装成另外一种形式的帧, 操作B) 后发送出去。
在验证平台中如果只是将DUT输出接口的数据与期望值相比较, 当数据不匹配情况出现时, 则无法确定问题是出在操作A还是操作B中,
如下图a所示。 此时, 如果在输出接口之前再增加一级比较, 就可以快速地定位问题所在了, 如图b所示。
要在寄存器模型中加入存储器非常容易。 在一个16位的系统中加入一块1024×16( 深度为1024, 宽度为16) 的存储器的代码如下:
|
|
首先由uvm_mem派生一个类my_memory, 在其new函数中调用super.new函数。 这个函数有三个参数:
- 第一个是名字,
- 第二个 是存储器的深度,
- 第三个是宽度。
在reg_model的build函数中, 将存储器实例化, 调用其configure函数, 第一个参数是所在 reg_block的指针, 第二个参数是此块存储器的hdl路径 。
最后调用default_map.add_mem函数, 将此块存储器加入default_map中, 从而可以对其进行前门访问操作。
如果没有对此块存储器分配地址空间, 那么这里可以不将其加入default_map中。 在这种情况下,只能使用后门访问的方式对其进行访问
read、 write、 peek、 poke。
要对此存储器进行读写, 可以通过调用以下四种方法实现
相比uvm_reg来说, 这四个任务/函数在调用的时候需要额外加入一个offset的参数, 说明读取此存储器的哪个地址
|
|
reg_model.sv
上面存储器的宽度与系统总线位宽恰好相同。 假如存储器的宽度大于系统总线位宽时, 情况会略有不同。 如在一个16位的系统中加入512×32的存储器
|
|
在派生my_memory时, 就要在其new函数中指明其宽度为32bit, 在my_block中加入此memory的方法与前面的相同。
很明显,这里加入的存储器的一个单元占据两个物理地址, 共占据1024个地址。 那么当使用read、 write、 peek、 poke时,
输入的参数offset 代表实际的物理地址偏移还是某一个存储单元偏移呢? 答案是存储单元偏移。 在访问这块512×32的存储器时,
offset的最大值是, 而不是1023。 当指定一个offset, 使用前门访问操作读写时, 由于一个offset对应的是两个物理地址,
所以寄存器模型会在总线上进行两次读写操作
寄存器模型对DUT的模拟
期望值与镜像值
由于DUT中寄存器的值可能是实时变更的, 寄存器模型并不能实时地知道这种变更, 因此, 寄存器模型中的寄存器的值有时与DUT中相关寄存器的值并不一致。
对于任意一个寄存器, 寄存器模型中都会有一个专门的变量用于最大可能地与DUT保持同步, 这个变量在寄存器模型中称为DUT的镜像值( mirrored value)
除了DUT的镜像值外, 寄存器模型中还有期望值( desired value) 。 如目前DUT中invert的值为’h0, 寄存器模型中的镜像值也为’h0,
但是希望向此寄存器中写入一个’h1, 此时一种方法是直接调用前面介绍的write任务, 将’h1写入, 期望值与镜像值都更新为’h1;
另外一种方法是通过set函数将期望值设置为’h1( 此时镜像值依然为0) , 之后调用update任务, update任务会检查期望值和镜像值是否一致,
如果不一致, 那么将会把期望值写入DUT中, 并且更新镜像值
|
|
通过get函数可以得到寄存器的期望值, 通过get_mirrored_value可以得到镜像值。 其使用方式分别见上述代码
对于存储器来说, 并不存在期望值和镜像值。 寄存器模型不对存储器进行任何模拟。 若要得到存储器中某个存储单元的值,
只能使用 read、 write、 peek、 poke。 中的四种操作
常用操作及其对期望值和镜像值的影响
read&write操作:
这两个操作在前面已经使用过了。 无论通过后门访问还是前门访问的方式从DUT中读取或写入寄存器的值,
在操作完成后, 寄存器模型都会根据读写的结果更新期望值和镜像值( 二者相等) 。
peek&poke操作:
前文中也讲述过这两个操作的示例。 在操作完成后, 寄存器模型会根据操作的结果更新期望值和镜像值 ( 二者相等) 。
get&set操作:
set操作会更新期望值, 但是镜像值不会改变。 get操作会返回寄存器模型中当前寄存器的期望值。
update操作:
这个操作会检查寄存器的期望值和镜像值是否一致, 如果不一致, 那么就会将期望值写入DUT中, 并且更新镜像值, 使其与期望值一致。
每个由uvm_reg派生来的类都会有update操作, 其使用方式在上一节中已经介绍过。 每个由 uvm_reg_block派生来的类也有update操作,
它会递归地调用所有加入此reg_block的寄存器的update任务。
randomize操作:
寄存器模型提供randomize接口。 randomize之后, 期望值将会变为随机出的数值, 镜像值不会改变。 但是并不是寄存器模型中所有寄存器都支持此函数。
如果不支持, 则randomize调用后其期望值不变。 若要关闭随机化功能,build中调用reg_xxx.configure时将其第八个参数设置为0即可
。
一般的, randomize不会单独使用而是和update一起。
如在DUT上电复位后, 需要配置一些寄存器的值。 这些寄存器的值通过randomize获得, 并使用update任务配置到DUT中。
寄存器模型中一些内建的sequence
检查后门访问中hdl路径的sequence
UVM提供了一系列的sequence, 可以用于检查寄存器模型及DUT中的寄存器。 其中uvm_reg_mem_hdl_paths_seq即用于检查hdl 路径的正确性。 这个sequence的原型为
|
|
这个sequence的运行依赖于在基类uvm_sequence中定义的一个变量:
|
|
在启动此sequence时必须给model赋值。 在任意的sequence中, 可以启动此sequence:
|
|
在调用这个sequence的start任务时, 传入的sequencer参数为null。 因为它正常工作不依赖于这个sequencer, 而依赖于model变量。
这个sequence会试图读取hdl所指向的寄存器, 如果无法读取, 则给出错误提示
由这个sequence的名字也可以看出, 它除了检查寄存器外, 还检查存储器。 如果某个寄存器/存储器在加入寄存器模型时没有指定其hdl路径,
那么此sequence在检查时会跳过这个寄存器/存储器
检查默认值的sequence
uvm_reg_hw_reset_seq用于检查上电复位后寄存器模型与DUT中寄存器的默认值是否相同, 它的原型为:
|
|
对于DUT来说, 在复位完成后, 其值就是默认值。 但是对于寄存器模型来说, 如果只是将它集成在验证平台上, 而不做任何处理,
那么它所有寄存器的值为0, 此时需要调用reset函数来使其内寄存器的值变为默认值( 复位值)
|
|
这个sequence在其检查前会调用model的reset函数, 所以即使在集成到验证平台时没有调用reset函数, 这个sequence也能正常工作。
除了复位( reset) 外, 这个sequence所做的事情就是使用前门访问的方式读取所有寄存器的值, 并将其与寄存器模型中的值比较。
这个sequence在启动时也需要指定其model变量。 如果想跳过某个寄存器的检查, 可以在启动此sequence前使用resource_db设置不检查此寄存器。
resource_db机制与config_db机制的底层实现是一样的, uvm_config_db类就是从uvm_resource_db类派生而来的。 由于在寄存器模型的sequence中,
get操作是通过 resource_db来进行的, 所以这里使用resource_db来进行设置:
|
|
或者使用:
|
|
检查读写功能的sequence
UVM提供两个sequence分别用于检查寄存器和存储器的读写功能。 uvm_reg_access_seq用于检查寄存器的读写, 它的原型为
|
|
使用此sequence也需要指定其model变量。
这个sequence会使用前门访问的方式向所有寄存器写数据, 然后使用后门访问的方式读回, 并比较结果。 最后把这个过程反过来,
使用后门访问的方式写入数据, 再用前门访问读回。 这个sequence要正常工作必须为所有的寄存器设置好hdl路径。
如果要跳过某个寄存器的读写检查, 则可以在启动sequence前使用如下的两种方式之一进行设置:
|
|
uvm_mem_access_seq用于检查存储器的读写, 它的原型为:
|
|
启动此sequence同样需要指定其model变量。 这个sequence会通过使用前门访问的方式向所有存储器写数据, 然后使用后门访问的方式读回, 并比较结果。
最后把这个过程反过来, 使用后门访问的方式写入数据, 再用前门访问读回。 这个sequence要正常工作必须为所有的存储器设置好HDL路径。
如果要跳过某块存储器的检查, 则可以使用如下的三种方式之一进行设置:
|
|
寄存器模型的高级用法
使用reg_predictor
将寄存器模型集成到验证平台中 讲述读操作的返回值时, 介绍了下图中的左图的方式, 这种方式要依赖于driver。
当driver将读取值返回后, 寄存器模型会更新寄存器的镜像值和期望值。 这个功能被称为寄存器模型的auto predict功能。
在建立寄存器模型时使用如下的语句打开此功能:
|
|
除了左图使用driver的返回值更新寄存器模型外, 还存在另外一种形式, 如右图所示。
在这种形式中, 是由monitor 将从总线上收集到的transaction交给寄存器模型, 后者更新相应寄存器的值。
要使用这种方式更新数据, 需要实例化一个reg_predictor, 并为这个reg_predictor实例化一个adapter:
|
|
在connect_phase中, 需要将reg_predictor和bus_agt的ap口连接在一起, 并设置reg_predictor的adapter和map。
只有设置了map 后, 才能将predictor和寄存器模型关联在一起。
当总线上只有一个主设备( master) 时, 则图7-9的左图和右图是完全等价的。 如果有多个主设备, 则左图会漏掉某些 trasaction。
经过上述代码的设置, 事实上存在着两条更新寄存器模型的路径: 右图虚线所示的自动预测途径, 二是经由 predictor的途径。
如果要彻底关掉虚线的更新路径, 则需要
|
|
使用UVM_PREDICT_DIRECT功能与mirror操作
UVM提供mirror操作, 用于读取DUT中寄存器的值并将它们更新到寄存器模型中。 它的函数原型为:
|
|
它有多个参数, 但是常用的只有前三个。
其中第二个参数指的是如果发现DUT中寄存器的值与寄存器模型中的镜像值不一致, 那么在更新寄存器模型之前是否给出错误提示。
其可选的值为UVM_CHECK和UVM_NO_CHECK。 它有两种应用场景:
- 一是在仿真中不断地调用它, 使得到整个寄存器模型的值与DUT中寄存器的值保持一致, 此时check选项是关闭的
- 二是在仿真即将结束时, 检查DUT中寄存器的值与寄存器模型中寄存器的镜像值是否一致, 这种情况下, check选项是打开的
mirror操作会更新期望值和镜像值。 同update操作类似, mirror操作既可以在uvm_reg级别被调用, 也可以在uvm_reg_block级别被调用。
当调用一个uvm_reg_block的mirror时, 其实质是调用加入其中的所有寄存器的mirror。
前文已经说过, 在通信系统中存在大量的计数器。当网络出现异常时, 借助这些计数器能够快速地找出问题所在, 所以必须要保证这些计数器的正确性。
一般的, 会在仿真即将结束时使用mirror操作检查这些计数器的值是否与预期值一致。
在DUT中的计数器是不断累加的, 但是寄存器模型中的计数器则保持静止。 参考模型会不断统计收到了多少包,
那么怎么将这些统计数据传递给寄存器模型呢?
前文中介绍的所有的操作都无法完成这个事情, 无论是set, 还是write, 或是poke; 无论是后门访问还是前门访问。
这个问题的实质是想人为地更新镜像值, 但是同时又不要对DUT进行任何操作。
UVM提供predict操作来实现这样的功能:
|
|
其中
- 第一个参数表示要预测的值,
- 第二个参数是byte_en, 默认-1的意思是全部有效,
- 第三个参数是预测的类型,
- 第四个参数是后门访问或者是前门访问。
第三个参数预测类型有如下几种可以选择:
|
|
read/peek和write/poke操作在对DUT完成读写后, 也会调用此函数, 只是它们给出的参数是UVM_PREDICT_READ和 UVM_PREDICT_WRITE。
要实现在参考模型中更新寄存器模型而又不影响DUT的值, 需要使用UVM_PREDICT_DIRECT, 即默认值:
|
|
在my_model中, 每得到一个新的transaction, 就先从寄存器模型中得到counter的期望值( 此时与镜像值一致) ,
之后将新的 transaction的长度加到counter中, 最后使用predict函数将新的counter值更新到寄存器模型中。
predict操作会更新镜像值和期望值。
在测试用例中, 仿真完成后可以检查DUT中counter的值是否与寄存器模型中的counter值一致:
|
|
寄存器模型的随机化与update
前文中在向uvm_reg中加入uvm_reg_field时, 是将加入的uvm_reg_field定义为rand类型:
|
|
在将uvm_reg加入uvm_reg_block中时, 同样定义为rand类型
|
|
由此可以判断对register_model来说, 支持randomize操作。 可以在uvm_reg_block级别调用randomize函数, 也可以在uvm_reg级别,
甚至可以在uvm_reg_field级别调用
|
|
但是, 要使某个field能够随机化, 只是将其定义为rand类型是不够的。 在每个reg_field加入uvm_reg时, 要调用其configure函数:
|
|
这个函数的第八个参数即决定此field是否会在randomize时被随机化。 但是即使此参数为1, 也不一定能够保证此field被随机化。
当一个field的类型中没有写操作时, 此参数设置是无效的。 换言之, 此参数只在此field类型为RW、 WRC、 WRS、 WO、W1、 WO1时才有效
因此, 要避免一个field被随机化, 可以在以下三种方式中任选其一:
1) 当在uvm_reg中定义此field时, 不要设置为rand类型。
2) 在调用此field的configure函数时, 第八个参数设置为0。
3) 设置此field的类型为RO、 RC、 RS、 WC、 WS、 W1C、 W1S、 W1T、 W0C、 W0S、 W0T、 W1SRC、 W1CRS、 W0SRC、W0CRS、 WSRC、 WCRS、 WOC、 WOS中的一种。
其中第一种方式也适用于关闭某个uvm_reg或者某个uvm_reg_block的randomize功能。
既然存在randomize, 那么也可以为它们定义constraint:
|
|
在施加约束时, 要深入reg_field的value变量。
randomize会更新寄存器模型中的预期值:
|
|
这与set函数类似。 因此, 可以在randomize完成后调用update任务, 将随机化后的参数更新到DUT中。
这特别适用于在仿真开始时随机化并配置参数
扩展位宽
reg 代码 的new函数中, 调用super.new时的第二个参数是16, 这个数字一般表示系统总线的宽度, 它可以是32、64、128等。
但是在寄存器模型中, 这个数字的默认最大值是64, 它是通过一个宏来控制的:
|
|
如果想要扩展系统总线的位宽, 可以通过重新定义这个宏来扩展。
与数据位宽相似的是地址位宽也有默认最大值限制, 其默认值也是64:
|
|
在默认情况下, 字选择信号的位宽等于数据位宽除以8, 它通过如下的宏来控制
|
|
如果想要使用一个其他值, 也可以重新定义这个宏。
寄存器模型的其他常用函数
get_root_blocks
之前的例子中若某处要使用寄存器模型, 则必须将寄存器模型的指针传递过去, 如在virtual sequence中使用, 需要传
递给virtual sequencer:
|
|
除了这种指针传递的形式外, UVM还提供其他函数, 使得可以在不使用指针传递的情况下得到寄存器模型的指针:
|
|
get_root_blocks函数得到验证平台上所有的根块( root block) 。 根块指最顶层的reg_block。
如 层次化的寄存器模型 中的reg_model是root block, 但是global_blk、 buf_blk和mac_blk不是
一个get_root_blocks函数的使用示例如下:
|
|
在使用get_root_blocks函数得到reg_block的指针后, 要使用cast将其转化为目标reg_block形式( 示例中为reg_model) 。
以后就可以直接使用p_rm来进行寄存器操作, 而不必使用p_sequencer.p_rm。
get_reg_by_offset函数
在建立了寄存器模型后, 可以直接通过层次引用的方式访问寄存器:
|
|
但是出于某些原因, 如果依然要使用地址来访问寄存器模型, 那么此时可以使用get_reg_by_offset函数通过寄存器的地址得到一个uvm_reg的指针,
再调用此uvm_reg的read或者write就可以进行读写操作:
|
|
通过调用最顶层的reg_block的get_reg_by_offset, 即可以得到任一寄存器的指针。 如果如7.4.1节那样使用了层次的寄存器模型,
从最顶层的reg_block的get_reg_by_offset也可以得到子reg_block中的寄存器。 即假如buf_blk的地址偏移是’h1000,
其中有偏移为’h3的寄存器( 即此寄存器的实际物理地址是’h1003) , 那么可以直接由p_rm.get_reg_by_offset( ‘h1003) 得到此寄存器,
而不必使用p_rm.buf_blk.get_reg_by_offset( ‘h3)
如果没有使用 多个地址的寄存器, 那么情况比较简单, 上述代码会运行第39行的分支。
当存在多个地址的情况下, 通过get_addresses函数可以得到这个函数的所有地址, 其返回值是一个动态数组addrs。
其中无论是大端还是小端, addrs[0]是LSB 对应的地址。 即对于DUT 中的counter( 此DUT是大端) ,
那么addrs[0]中存放的是‘h6。 而假如是小端, 两个地址分别是’h1005和’h1006, 那么addrs[0]中存放的是’h1005。
第41到48行通过比较addrs中的地址与目标地址, 最终得到要访问的数据
写寄存器与读操作类似, 这里不再列出。