目录
寄存器模型概述
1.为什么要使用寄存器模型?
在复杂的UVM验证环境中,访问DUT中的寄存器可能会涉及到多个模块和层级的交互。初学者可能会发现手动处理这些交互过于复杂。
使用UVM寄存器模型,验证工程师可以直接调用内置的函数来访问寄存器,而无需处理复杂的交互过程。
1)寄存器检测的方法
通常来说,DUT中会有一组控制端口,通过控制端口,可以配置DUT中的寄存器,DUT可以根据寄存器的值来改变其行为。这组控制端口就是寄存器配置总线。
a. 无寄存器模型:
启动读写的sequence进出完成,sequence产生的读写事物通过sequencer传入到对应的driver当中,driver讲对应的信号驱动给dut中,dut根据驱动的信号做出相应的动作,如改变寄存器的值或者返回寄存器的值,monitor再从接口上捕获到接口信息,可以判断drive操作的哪个寄存器以及对应的值,再讲信息送到sb中,进行对比。
这个方法很难在其他组件中在适当的时间启动读写寄存器。
ral提供了用于对设计当中的寄存器和存储器进行建模的register model,是一套数据结构,需要使用他们建立寄存器模型。
b.有寄存器模型:
有了寄存器模型之后,scoreboard只与寄存器模型打交道,无论是发送读的指令还是获取读操作的返回值,都可以由寄存器模型完成。有了寄存器模型后,可以在任何耗费时间的phase中使用寄存器模型以前门访问或后门(BACKDOOR)访问的方式来读取寄存器的值,同时还能在某些不耗费时间的phase(如check_phase)中使用后门访问的方式来读取寄存器的值。
前门访问与后门访问是两种寄存器的访问方式。所谓前门访问,指的是通过模拟cpu在总线上发出读指令,进行读写操作。在这个过程中,仿真时间($time函数得到的时间)是一直往前走的。
UVM寄存器模型的本质就是重新定义了验证平台与DUT的寄存器接口,使验证人员更好地组织及配置寄存器,简化流程、减少工作量。
2)寄存器模型的作用
解耦Scoreboard和Sequence:
有了寄存器模型,Scoreboard可以通过寄存器模型间接访问DUT中的寄存器和存储器,而不需要直接与Sequence紧密耦合。这种解耦使得Scoreboard的设计更加灵活,能够更好地适应复杂的验证环境。简化寄存器访问流程:
使用寄存器模型提供的API,Scoreboard可以直接对寄存器和存储器进行访问,而不需要通过Sequence来启动访问操作。这简化了寄存器访问的流程,提高了验证环境的可维护性和可扩展性。提供读写接口:
寄存器模型提供了read和write等接口,使得Scoreboard可以方便地读取寄存器的值或向指定的寄存器写入值。这种直接的读写接口使得Scoreboard能够更加灵活地与DUT进行通信和交互。事务转换器的使用:
为了让寄存器模型能够与Sequence进行交互,通常需要使用事务转换器。这个转换器能够将寄存器模型产生的事务类型转换成Sequence能够接受的类型,从而实现寄存器模型与Sequence之间的无缝连接和通讯。寄存器模型与Sequencer的连接:
通过事务转换器,寄存器模型和Sequencer得以连接。这样一来,寄存器模型就可以直接与DUT进行通信,而不需要依赖于Sequence的启动,从而进一步简化了验证流程和提高了效率。
3)Froutdoor
scoreboard或者是平台的其他组件当中,调用寄存器模型的相关API进行寄存器访问时,寄存器模型会根据这个API以及寄存器的名称自动产生出一个类型为uvm_reg_item的事物对象。
比如,如果是read操作,那么这个对象中的操作类型就是独操作,访问的地址会根据寄存器的名称,register name自动生成。接着,寄存器模型会将这个事物对象进一步进行转换,转换为uvm_reg_ bus_op类型的事物对象。并将事物对象传入adapter转换器,转换器再将其转换为sequencer可接受的事物类型。sequencer在收到后,通过driver驱动到相应的dut,最终返回相应的值。
下面是一个简单的伪代码示例,展示了如何使用寄存器模型进行寄存器的读取操作:
import uvm_pkg::*;
class scoreboard extends uvm_component;
// 寄存器模型
uvm_reg_model reg_model;
// 构造函数
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
// 重写build_phase方法
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
// 初始化寄存器模型
reg_model = new("reg_model", this);
endfunction
// 读取寄存器值的方法
virtual task read_register(string reg_name);
// 创建一个uvm_reg_item类型的事务对象
uvm_reg_item reg_item = new();
// 设置事务对象的操作类型为读操作
reg_item.operation = uvm_reg::UVM_READ;
// 设置访问的寄存器名称
reg_item.element = reg_model.get_reg_by_name(reg_name);
// 将uvm_reg_item类型的事务对象转换为uvm_reg_bus_op类型
uvm_reg_bus_op bus_op = reg_model.convert2bus(reg_item);
// 将事务对象传递给适当的adapter转换器进行进一步处理
// 这里省略了转换器的具体实现
// 最终将事务对象传递给Sequencer并驱动到DUT中
uvm_sequencer#(uvm_reg_bus_op) sequencer;
sequencer.drive(bus_op);
endtask
endclass
4)Backdoor
通过dut的层次结构直接进行引用,操作完成后寄存器模型再将结果返回sb。以下是一个简单的伪代码示例,演示了如何使用后门方式操作寄存器:
import uvm_pkg::*;
class scoreboard extends uvm_component;
// 寄存器模型
uvm_reg_model reg_model;
// 构造函数
function new(string name, uvm_component parent);
super.new(name, parent);
endfunction
// 重写build_phase方法
virtual function void build_phase(uvm_phase phase);
super.build_phase(phase);
// 初始化寄存器模型
reg_model = new("reg_model", this);
endfunction
// 后门写寄存器值的方法
virtual task write_register_backdoor(string reg_name, bit [31:0] value);
// 获取寄存器对象
uvm_reg reg = reg_model.get_reg_by_name(reg_name);
if(reg == null) begin
`uvm_error("SCOREBOARD", $sformatf("Register %s not found!", reg_name));
return;
end
// 使用后门方式写入寄存器值
reg_model.do_write(reg, value);
// 从寄存器模型中读取更新后的寄存器值
bit [31:0] updated_value;
reg_model.do_read(reg, updated_value);
// 将更新后的寄存器值传递给 Scoreboard 进行后续处理
handle_register_update(reg_name, updated_value);
endtask
// 后续处理更新后的寄存器值
virtual task handle_register_update(string reg_name, bit [31:0] value);
// 在这里进行 Scoreboard 对更新后的寄存器值的处理
`uvm_info("SCOREBOARD", $sformatf("Register %s updated with value %h", reg_name, value), UVM_HIGH);
endtask
endclass
2.RAL模型层次结构
uvm_reg_field
表示寄存器中的一个字段,uvm_reg
表示一个完整的寄存器,uvm_reg_block
用于组织和管理寄存器,而 uvm_reg_map
则管理寄存器地址映射关系。这些组件共同构成了 UVM 寄存器模型中的层次结构。
以下是层次结构图:
1)uvm_reg_field
2)uvm_reg
// 定义一个状态寄存器的 uvm_reg_field
class status_reg_field extends uvm_reg_field;
function new(string name = "status_reg_field");
super.new(name, 1, 0, "RO", 0);
endfunction
endclass
// 定义一个状态寄存器的 uvm_reg
class status_reg extends uvm_reg;
status_reg_field empty_field;
status_reg_field full_field;
status_reg_field overflow_field;
status_reg_field underflow_field;
function new(string name = "status_reg");
super.new(name, 32, UVM_NO_COVERAGE);
empty_field = new("empty", 0, 0);
full_field = new("full", 1, 1);
overflow_field = new("overflow", 2, 2);
underflow_field = new("underflow", 3, 3);
endfunction
endclass
3)uvm_reg_block
它是一个比较大的单位,在其中可以加入许多的uvm_reg,也可以加入其他的uvm_reg_block。一个寄存器模型中至少包含一个uvm_reg_block。
以下是uvm_reg_block相关的伪代码:
// 定义一个 uvm_reg_block,包含一个状态寄存器
class my_reg_block extends uvm_reg_block;
status_reg my_status_reg;
function new(string name = "my_reg_block");
super.new(name);
my_status_reg = new("my_status_reg");
endfunction
endclass
4)uvm_reg_map:
// 定义一个 uvm_reg_map,用于管理寄存器地址映射
class my_reg_map extends uvm_reg_map;
function new(string name = "my_reg_map");
super.new(name);
// 设置寄存器地址映射关系
add_reg(my_reg_block.my_status_reg, 0x1000);
endfunction
endclass
// 在每个 reg_block 内部,至少有一个 uvm_reg_map
// 创建一个 reg_block 实例,并添加到 uvm_reg_map 中
initial begin
my_reg_block my_block = new();
my_reg_map my_map = new();
// 在 reg_block 内部添加 uvm_reg_map
my_block.add_map(my_map, 0);
end
3.Adapter
在 UVM(Universal Verification Methodology)中,Adapter 通常用于将事务或数据从一个接口传递到另一个接口,并在传递过程中进行必要的格式转换或协议转换。
在寄存器模型中,Adapter 的主要作用是将前门(Frontdoor)访问请求从验证环境传递给 DUT(Design Under Test),或者将后门(Backdoor)访问请求从 DUT 传递给验证环境。它负责在验证环境和 DUT 之间建立通信桥梁,确保正确而高效地进行寄存器访问。
Adapter 可以在不同层次或不同接口之间进行数据格式的转换、地址的映射、时序的调整等操作,以确保寄存器访问的正确性和一致性。它通常由一系列方法和类组成,用于处理不同类型的访问请求,并将其转换为适合目标接口的格式。
总的来说,Adapter 在寄存器模型中扮演着连接验证环境和 DUT 的关键角色,它通过数据转换和传递实现了验证环境与 DUT 之间的通信和交互。
1)reg2bus
它的作用是将由寄存器模型产生的uvm_reg_bus_op类型的事物对象转换为我们自定义类型的事物。对象转换之后的事物对象会自动的发往sequencer当中。
class my_transaction extends uvm_sequence_item;
// 用户自定义事务类型
// 可以根据需要添加字段和方法
endclass
class reg2bus_adapter extends uvm_adapter;
// 适配器,用于将寄存器模型产生的 uvm_reg_bus_op 转换为用户自定义事务类型
virtual function uvm_sequence_item convert2bus(uvm_object obj);
my_transaction trans = new();
uvm_reg_bus_op bus_op = obj;
// 在这里执行转换逻辑,将 bus_op 中的信息转换为 my_transaction 类型的事务对象
// 示例中简单地将 bus_op 中的数据拷贝到 trans 中
trans.address = bus_op.addr;
trans.data = bus_op.data;
return trans;
endfunction
endclass
class my_env extends uvm_env;
// 测试环境,用于接收转换后的事务对象并发送到 sequencer 中
reg2bus_adapter adapter;
function new(string name = "my_env", uvm_component parent = null);
super.new(name, parent);
adapter = new();
endfunction
virtual task handle_bus_op(uvm_reg_bus_op bus_op);
my_transaction trans;
// 使用适配器进行转换
trans = adapter.convert2bus(bus_op);
// 将转换后的事务对象发送到 sequencer 中
seqr.put(trans);
endtask
endclass
2)bus2reg
它的作用与reg2Bus刚好相反,是将我们自定义的事物类型的对象转化为uvm_reg_bus_op类型的事物。对象转换之后的事物对象会被寄存器模型所分析。之后更新寄存器模型中相关寄存器的值。
// 定义适配器,实现bus2reg的转换
class bus_to_reg_adapter extends uvm_adapter;
// 重载unadapt方法,实现bus2reg的转换
virtual function uvm_object unadapt(uvm_sequence_item seq_item);
my_transaction seq_item_in = seq_item;
// 实现bus2reg的转换逻辑,将用户自定义类型的事务对象转换为uvm_reg_bus_op类型的事务对象
// ...
return uvm_reg_bus_op::type_id::create("bus_to_reg_trans"); // 返回转换后的事务对象
endfunction
endclass
3)register model 和adapter 以及 sequencer的连接:
实现这个连接的方式很巧妙,要借助于寄存器模型当中的map来完成。
在这个map当中,不但包含了各个寄存器以及存储器的地址映射,而且还包含了一个用于与adapt连接的adapt类型的句柄,连接的过程其实就是将env当中的adapt对象。复制给这个句柄的过程,同样的也包含了用于与sequencer连接的句柄连接的过程,也是将agent当中的sequencer对象复制给这个句柄的过程,此外还有一些任务,用于寄存器模型和平台协调。
class my_env extends uvm_env;
reg_bus_adapter adapter; // 适配器
my_sequencer seqr; // Sequencer
function new(string name = "my_env", uvm_component parent = null);
super.new(name, parent);
adapter = new();
seqr = my_sequencer::type_id::create("seqr", this);
endfunction
// 连接register model、adapter和sequencer
virtual function void connect_register_model(uvm_reg_block reg_block);
uvm_reg_map map;
// 创建映射,并设置adapter和sequencer
map = reg_block.create_map("reg_map", 0, UVM_BACKDOOR, 32'h0, 32'h1000);
map.set_sequencer(seqr); // 设置映射的sequencer
map.set_adapter(adapter); // 设置映射的adapter
endfunction
// 任务用于启动sequence,以访问DUT中的寄存器
virtual task start_sequence();
my_sequence seq;
seq = my_sequence::type_id::create("seq");
seq.start(seqr); // 启动sequence并传递sequencer
endtask
endclass
在这个示例中,
my_env
类中的connect_register_model
方法用于连接寄存器模型、适配器和 sequencer。在该方法中,首先创建了一个寄存器映射(uvm_reg_map
),然后设置了该映射的 adapter 和 sequencer。当寄存器模型需要访问寄存器时,会通过该映射将请求转发给 adapter,然后 adapter 再将请求转发给 sequencer 进行处理。另外,
start_sequence
任务用于启动 sequence,以访问 DUT 中的寄存器。在该任务中,创建了一个 sequence 对象,并调用其start
方法启动 sequence,并传递 sequencer 给它,从而开始访问 DUT 中的寄存器。