第一章:寄存器模型基础

大家好,我是你们的讲师。今天咱们聊聊UVM寄存器模型。说实话,我刚接触UVM那会儿,觉得寄存器模型就是个花架子——不就是读写寄存器吗?直接拿interface干不就完了?后来被现实狠狠教育了一顿。

你想想看,一个稍微复杂点的芯片,寄存器少说几百个,多则上千。每个寄存器还要考虑复位值、域段、访问权限、后门访问...如果全靠手写driver去对接,那代码量简直不敢想。更别提验证后期要改寄存器定义,那简直是噩梦。

嗯,这就是UVM寄存器模型要解决的问题。它本质上是一套寄存器抽象层,让你用面向对象的方式去描述和操作DUT中的寄存器。

1.1 UVM寄存器模型能干什么?

我总结了一下,寄存器模型主要干三件事:

  • 抽象化描述:用类来描述寄存器,包括它的位宽、域段、属性、复位值
  • 自动化访问:前门访问(通过总线协议)和后门访问(直接通过HDL路径)都给你封装好了
  • 预测与镜像:自动维护一个软件镜像,随时知道DUT里寄存器的当前值

说白了,有了寄存器模型,你的testbench就不用再跟繁琐的总线时序打交道了。你只需要调用reg_model.REG_NAME.read().write(),剩下的脏活累活模型帮你干。

核心思想:寄存器模型是DUT寄存器的"影子",你在模型上做什么操作,模型会帮你同步到DUT上。

1.2 三大核心类

UVM寄存器模型里有三个核心类,我习惯把它们比作"零件-板卡-系统"的关系。咱们一个一个说。

1.2.1 uvm_reg —— 最小的零件

uvm_reg代表一个寄存器。它包含:

  • 寄存器的位宽(nbits)
  • 内部的域段(uvm_reg_field)
  • 访问属性(RO, RW, RC等)
  • 复位值

举个例子,一个控制寄存器可能长这样:

class ctrl_reg extends uvm_reg;
    `uvm_object_utils(ctrl_reg)
    
    uvm_reg_field reserved;
    uvm_reg_field enable;
    uvm_reg_field mode;
    
    function new(string name = "ctrl_reg");
        super.new(name, 32, UVM_NO_COVERAGE);
    endfunction
    
    virtual function void build();
        reserved = uvm_reg_field::type_id::create("reserved");
        enable   = uvm_reg_field::type_id::create("enable");
        mode     = uvm_reg_field::type_id::create("mode");
        
        // 参数:父类、位宽、LSB位置、访问权限、是否易失、复位值、是否有复位、是否可随机化、是否单独访问
        reserved.configure(this, 30, 0, "RO", 0, 30'h0, 1, 0, 0);
        enable.configure(this,   1, 30, "RW", 0, 1'b0, 1, 1, 0);
        mode.configure(this,     1, 31, "RW", 0, 1'b0, 1, 1, 0);
    endfunction
endclass

这里有个坑,我当年踩过。你看configure函数那堆参数,顺序千万别搞错。尤其是LSB位置位宽,写反了仿真不会报错,但读写结果全乱套。我曾经因为这个查了整整两天...

注意:uvm_reg_field的configure参数顺序是:(parent, size, lsb_pos, access, volatile, reset, has_reset, is_rand, individually_accessible)。建议每次写的时候都对照着手册核对一遍。

1.2.2 uvm_reg_block —— 板卡级容器

uvm_reg_block是寄存器的容器。一个block可以包含多个寄存器,也可以包含子block。它负责:

  • 管理所有寄存器的创建和配置
  • 定义地址映射(通过uvm_reg_map)
  • 提供默认的地址解码

我一般这样组织block:

class my_reg_block extends uvm_reg_block;
    `uvm_object_utils(my_reg_block)
    
    rand ctrl_reg ctrl;
    rand status_reg status;
    // ... 其他寄存器
    
    function new(string name = "my_reg_block");
        super.new(name, UVM_NO_COVERAGE);
    endfunction
    
    virtual function void build();
        // 创建寄存器
        ctrl   = ctrl_reg::type_id::create("ctrl");
        status = status_reg::type_id::create("status");
        
        // 配置寄存器(参数:父block,寄存器地址偏移)
        ctrl.configure(this, null, "");
        ctrl.build();
        status.configure(this, null, "");
        status.build();
        
        // 创建默认map
        default_map = create_map("default_map", 0, 4, UVM_LITTLE_ENDIAN, 1);
        
        // 将寄存器添加到map中(参数:寄存器,偏移地址,访问权限)
        default_map.add_reg(ctrl,   32'h0000, "RW");
        default_map.add_reg(status, 32'h0004, "RO");
        
        // 锁定配置
        lock_model();
    endfunction
endclass

注意看lock_model()这个调用。它告诉模型:配置完成了,后面不能再改了。这是UVM推荐的做法,能防止后续误操作。

1.2.3 uvm_reg_map —— 地址翻译官

uvm_reg_map负责把寄存器的逻辑地址翻译成物理总线上的地址。它主要干这些事:

  • 管理地址映射关系
  • 处理地址对齐和字节序
  • 支持多个map(比如不同总线域)

创建map时那几个参数,我解释一下:

参数 含义 我的建议
name map的名字 一般用"default_map"
base_addr 基地址 如果DUT有多个实例,这里设0,上层再偏移
n_bytes 总线位宽(字节数) 32位总线填4,64位填8
endian 字节序 ARM用little,PowerPC用big
byte_addressing 是否按字节寻址 通常填1

小技巧:如果你的DUT有多个总线接口(比如APB和AHB各管一部分寄存器),可以创建多个map。每个map对应一个总线接口,然后通过adapter来桥接。

1.3 寄存器模型与DUT的桥接

模型建好了,怎么跟DUT通信呢?这就需要一个"翻译官"——uvm_reg_adapter

adapter的作用很简单:把寄存器模型的通用操作(read/write)转换成总线协议的具体时序。比如APB总线,adapter就要把read请求变成PADDR、PWRITE、PRDATA这些信号的变化。

一个典型的adapter长这样:

class apb_reg_adapter extends uvm_reg_adapter;
    `uvm_object_utils(apb_reg_adapter)
    
    function new(string name = "apb_reg_adapter");
        super.new(name);
        // 是否支持字节使能
        supports_byte_enable = 0;
        // 是否提供物理地址
        provides_responses = 1;
    endfunction
    
    // 寄存器模型 -> 总线事务
    virtual function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
        apb_transfer tr = apb_transfer::type_id::create("tr");
        tr.addr = rw.addr;
        tr.data = rw.data;
        tr.kind = (rw.kind == UVM_READ) ? APB_READ : APB_WRITE;
        return tr;
    endfunction
    
    // 总线事务 -> 寄存器模型
    virtual function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
        apb_transfer tr;
        if (!$cast(tr, bus_item)) begin
            `uvm_fatal("CAST_FAIL", "bus_item is not apb_transfer type")
        end
        rw.kind = (tr.kind == APB_READ) ? UVM_READ : UVM_WRITE;
        rw.addr = tr.addr;
        rw.data = tr.data;
        rw.status = UVM_IS_OK;
    endfunction
endclass

这里有两个函数要成对实现:

  • reg2bus:把寄存器模型的读写请求转成总线事务
  • bus2reg:把总线返回的结果转回寄存器模型能理解的格式

最后,在环境中把这几样东西串起来:

class my_env extends uvm_env;
    my_reg_block reg_model;
    apb_reg_adapter adapter;
    uvm_reg_predictor #(apb_transfer) predictor;
    
    virtual function void build_phase(uvm_phase phase);
        reg_model = my_reg_block::type_id::create("reg_model");
        reg_model.build();
        reg_model.lock_model();
        
        adapter = apb_reg_adapter::type_id::create("adapter");
        
        // 设置前门访问的sequencer
        reg_model.default_map.set_sequencer(apb_agent.sequencer, adapter);
        
        // 设置预测器(用于自动更新镜像)
        predictor = uvm_reg_predictor #(apb_transfer)::type_id::create("predictor", this);
        predictor.map = reg_model.default_map;
        predictor.adapter = adapter;
        apb_agent.monitor.item_collected_port.connect(predictor.bus_in);
    endfunction
endclass

嗯,到这里,寄存器模型的基础框架就搭起来了。你可以在sequence里这样用:

class my_sequence extends uvm_sequence #(uvm_sequence_item);
    virtual task body();
        uvm_status_e status;
        uvm_reg_data_t data;
        
        // 写寄存器
        reg_model.ctrl.write(status, 32'h8000_0001);
        
        // 读寄存器
        reg_model.status.read(status, data);
        
        // 后门访问(不经过总线,直接通过HDL路径)
        reg_model.ctrl.peek(status, data);
        reg_model.ctrl.poke(status, 32'h0);
    endtask
endclass

看到没?读写操作变得极其简洁。你不需要关心总线协议,不需要处理时序,甚至不需要知道DUT里寄存器的具体地址。这就是寄存器模型的威力。

总结一下今天的内容

  1. 寄存器模型是DUT寄存器的软件抽象,帮你屏蔽总线细节
  2. uvm_reg是最小单元,描述单个寄存器的域段和属性
  3. uvm_reg_block是容器,管理寄存器和地址映射
  4. uvm_reg_map负责地址翻译,支持多总线域
  5. adapter是桥梁,连接寄存器模型和实际总线协议

下一章咱们聊聊寄存器模型的前门访问和后门访问。这两个概念很多人搞混,其实区别很大。我会结合一个实际项目中的例子来讲——那个项目里,就因为后门访问没用好,导致仿真和芯片行为对不上,差点流片翻车。到时候细说。