2. 寄存器模型创建与配置:从零搭起你的寄存器世界
好,咱们直接进入正题。寄存器模型这东西,说白了就是DUT(待测设计)里那些寄存器的“软件镜像”。你想想看,如果每次访问寄存器都要靠前门(Front Door)通过总线协议去读写,那验证效率得多低?所以UVM给了我们一套标准化的方法,把寄存器抽象成对象,让验证环境能高效地操作它们。
我个人习惯,在搭建寄存器模型时,会按照“从内到外”的顺序来:先定义最小的单元——uvm_reg_field,再组装成uvm_reg,最后把多个寄存器集成到uvm_reg_block里。这个过程就像搭积木,每一步都有讲究。
2.1 uvm_reg_field:寄存器的最小单元
uvm_reg_field是寄存器模型里最基础的构件。一个寄存器可能包含多个域(Field),比如一个32位的控制寄存器,可能高8位是状态位,低8位是使能位,中间还有各种配置位。每个域都有自己的属性:宽度、访问权限、复位值等等。
定义域的时候,我通常会这样做:
class my_reg extends uvm_reg;
`uvm_object_utils(my_reg)
rand uvm_reg_field field_a; // 使能域
rand uvm_reg_field field_b; // 配置域
function new(string name = "my_reg");
super.new(name, 32, UVM_NO_COVERAGE); // 32位寄存器
endfunction
virtual function void build();
field_a = uvm_reg_field::type_id::create("field_a");
field_b = uvm_reg_field::type_id::create("field_b");
// 配置域:宽度、LSB位置、访问权限、是否易失、复位值、是否可复位、是否可随机化
field_a.configure(this, 8, 0, "RW", 0, 8'h00, 1, 1, 0);
field_b.configure(this, 24, 8, "RO", 0, 24'h000000, 1, 0, 0);
endfunction
endclass
这里要注意configure函数的参数顺序。我曾经在这个地方栽过跟头——参数传错了,结果仿真半天发现寄存器行为不对。嗯,这里我建议你写代码时对照着UVM手册看,或者像我一样,在代码里加个注释说明每个参数的含义。
关键参数说明:
- parent:所属的寄存器对象(this)
- size:域的位宽
- lsb_pos:域在寄存器中的最低有效位位置
- access:访问权限("RW", "RO", "WO", "RC"等)
- volatile:是否易失(硬件会改变的值设为1)
- reset:复位值
- has_reset:是否有复位行为
- is_rand:是否可随机化
- individually_accessible:是否可单独访问
2.2 uvm_reg:把域组装成完整的寄存器
有了域之后,下一步就是构建寄存器。寄存器本身是一个容器,它管理着所有域的地址偏移、访问策略和镜像更新。
构建寄存器时,我习惯在build()函数里完成所有域的创建和配置。为什么?因为UVM的build phase保证了执行顺序,你不用担心域还没创建就被使用的问题。
class status_reg extends uvm_reg;
`uvm_object_utils(status_reg)
rand uvm_reg_field reserved;
rand uvm_reg_field error_flag;
rand uvm_reg_field busy_flag;
function new(string name = "status_reg");
super.new(name, 16, UVM_NO_COVERAGE);
endfunction
virtual function void build();
reserved = uvm_reg_field::type_id::create("reserved");
error_flag = uvm_reg_field::type_id::create("error_flag");
busy_flag = uvm_reg_field::type_id::create("busy_flag");
reserved.configure(this, 14, 2, "RO", 0, 14'h0, 1, 0, 0);
error_flag.configure(this, 1, 1, "RC", 0, 1'h0, 1, 1, 0); // 读清零
busy_flag.configure(this, 1, 0, "RO", 1, 1'h0, 1, 0, 0); // 易失位
endfunction
endclass
这里有个小技巧:对于只读(RO)且硬件会改变的域,记得把volatile参数设为1。否则,当你用后门(Back Door)访问时,读到的永远是镜像值,而不是硬件真实值。我刚开始做项目时就吃过这个亏,查了半天才发现是volatile没设对。
个人经验: 对于状态寄存器(status register),里面的标志位通常都是volatile的。而对于配置寄存器(control register),大部分域都不是volatile的。这个区分很重要,直接影响后门访问的正确性。
2.3 uvm_reg_block:把寄存器组织成地址空间
寄存器块(reg_block)是最高层的容器。它负责管理所有寄存器的地址映射、提供前门和后门访问的接口,以及处理地址解码逻辑。
集成寄存器块时,我通常会这样做:
class my_reg_block extends uvm_reg_block;
`uvm_object_utils(my_reg_block)
rand my_reg ctrl_reg;
rand status_reg stat_reg;
function new(string name = "my_reg_block");
super.new(name, UVM_NO_COVERAGE);
endfunction
virtual function void build();
// 创建寄存器实例
ctrl_reg = my_reg::type_id::create("ctrl_reg");
stat_reg = status_reg::type_id::create("stat_reg");
// 配置寄存器:在块中的地址偏移
ctrl_reg.configure(this, null, "");
stat_reg.configure(this, null, "");
// 创建地址映射
default_map = create_map("default_map", 0, 4, UVM_LITTLE_ENDIAN, 1);
// 将寄存器添加到地址映射中
default_map.add_reg(ctrl_reg, 32'h0000_0000, "RW");
default_map.add_reg(stat_reg, 32'h0000_0004, "RO");
// 锁定配置(防止后续修改)
lock_model();
endfunction
endclass
注意create_map的参数:第一个是映射名称,第二个是基地址,第三个是字节宽度(4表示32位),第四个是字节序,第五个表示是否支持字节使能。这些参数要根据你的总线协议来设置。
2.4 地址映射与访问权限配置
地址映射是寄存器模型的核心。它告诉验证环境:哪个寄存器在哪个地址,用什么权限访问。
我见过不少新手在配置地址映射时出错,最常见的问题就是地址重叠或者权限不匹配。这里我分享一个我常用的检查方法:
| 寄存器名称 | 基地址偏移 | 地址范围 | 访问权限 | 备注 |
|---|---|---|---|---|
| ctrl_reg | 0x0000 | 0x0000 - 0x0003 | RW | 控制寄存器 |
| stat_reg | 0x0004 | 0x0004 - 0x0007 | RO | 状态寄存器 |
| cfg_reg | 0x0008 | 0x0008 - 0x000B | RW | 配置寄存器 |
在代码中配置访问权限时,要注意权限字符串必须与UVM定义的一致。常用的权限有:
- "RW":可读可写
- "RO":只读
- "WO":只写
- "RC":读清零
- "WS":写置位
- "W1C":写1清零
避坑指南: 我曾经在一个项目中,把某个寄存器的权限设成了"RW",但实际硬件是"W1C"(写1清零)。结果仿真时,我通过后门写了个1,期望它清零,但模型里还是1。查了三天才发现是权限配置错了。所以,一定要和RTL设计人员核对清楚每个寄存器的真实行为。
2.5 完整的集成示例
最后,我给出一个完整的寄存器块集成示例,包含了前门和后门访问的配置:
class top_reg_block extends uvm_reg_block;
`uvm_object_utils(top_reg_block)
// 声明所有寄存器
rand control_reg ctrl;
rand status_reg stat;
rand interrupt_reg intr;
function new(string name = "top_reg_block");
super.new(name, UVM_NO_COVERAGE);
endfunction
virtual function void build();
// 创建寄存器
ctrl = control_reg::type_id::create("ctrl");
stat = status_reg::type_id::create("stat");
intr = interrupt_reg::type_id::create("intr");
// 配置寄存器
ctrl.configure(this, null, "");
stat.configure(this, null, "");
intr.configure(this, null, "");
// 创建地址映射
default_map = create_map("default_map", 0, 4, UVM_LITTLE_ENDIAN, 1);
// 添加寄存器到映射
default_map.add_reg(ctrl, 32'h0000_0000, "RW");
default_map.add_reg(stat, 32'h0000_0004, "RO");
default_map.add_reg(intr, 32'h0000_0008, "RW");
// 设置后门访问的路径(与DUT层次对应)
// 假设DUT的层次是:top.u_dut.reg_file
set_hdl_path_root("top.u_dut.reg_file");
// 为每个寄存器设置后门路径
ctrl.add_hdl_path_slice("ctrl_reg", 0, 32);
stat.add_hdl_path_slice("stat_reg", 0, 32);
intr.add_hdl_path_slice("intr_reg", 0, 32);
// 锁定模型
lock_model();
endfunction
endclass
这里set_hdl_path_root和add_hdl_path_slice是后门访问的关键。如果你不设置这些路径,后门访问就无法工作。我建议你在仿真开始时,打印一下寄存器模型的层次结构,确认路径是否正确。
好了,这一章的内容就到这里。寄存器模型的创建和配置是UVM验证的基础,看似简单,但细节很多。你想想看,如果地址映射错了,或者权限配置错了,那整个验证环境都会跑偏。所以,花点时间把这一步做扎实,后面会省很多事。