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_rootadd_hdl_path_slice是后门访问的关键。如果你不设置这些路径,后门访问就无法工作。我建议你在仿真开始时,打印一下寄存器模型的层次结构,确认路径是否正确。

好了,这一章的内容就到这里。寄存器模型的创建和配置是UVM验证的基础,看似简单,但细节很多。你想想看,如果地址映射错了,或者权限配置错了,那整个验证环境都会跑偏。所以,花点时间把这一步做扎实,后面会省很多事。