3. 寄存器模型集成到验证环境:在env中实例化reg_block、通过adapter连接总线、设置default_map与sequencer

好,咱们今天聊一个实战中绕不开的话题——寄存器模型怎么真正“跑起来”。

前面几章我们把寄存器模型的结构讲清楚了,但说实话,模型写好了只是第一步。你得把它集成到验证环境里,让它能和DUT的总线打交道。这一步做不好,模型就是个摆设。

我个人习惯把集成过程拆成三步:实例化reg_block、连接adapter、配置default_map和sequencer。咱们一个一个说。

3.1 在env中实例化reg_block

首先,你得在env里把寄存器模型“造”出来。这步其实挺直接的,但有个坑——实例化的时机

核心原则:寄存器模型应该在build_phase中实例化,在connect_phase中完成连接。别搞反了。

我见过有人把实例化放在connect_phase里,结果后面用的时候模型还是空的。嗯,这种低级错误我也犯过,后来就长记性了。

看个例子:

class my_env extends uvm_env;
  `uvm_component_utils(my_env)

  my_reg_block reg_model;  // 寄存器模型句柄
  my_adapter    adapter;   // 适配器
  my_sequencer  sequencer; // 总线sequencer

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    // 实例化寄存器模型
    reg_model = my_reg_block::type_id::create("reg_model", this);
    // 实例化适配器和sequencer
    adapter   = my_adapter::type_id::create("adapter", this);
    sequencer = my_sequencer::type_id::create("sequencer", this);
    // 别忘了调用build(),把寄存器层级建起来
    reg_model.build();
  endfunction

注意最后一行reg_model.build()。这个很容易漏掉。寄存器模型内部有层级结构,你不调build(),里面的寄存器、块都是空的。我曾经因为这个查了一下午的bug,最后发现是build()没调——你说冤不冤?

3.2 通过adapter连接总线

好,模型实例化完了。但模型怎么和DUT的总线通信?这就需要adapter了。

adapter说白了就是个翻译官。寄存器模型发出的是uvm_reg_bus_op这种通用事务,而DUT的总线接口可能是APB、AHB、AXI等等。adapter负责把通用事务转成具体总线协议,再把总线响应转回通用事务。

我建议你在connect_phase里做这个连接:

function void connect_phase(uvm_phase phase);
  super.connect_phase(phase);
  // 设置adapter
  reg_model.default_map.set_sequencer(sequencer, adapter);
  // 或者更明确的方式:
  // reg_model.default_map.set_sequencer(this.sequencer, this.adapter);
endfunction

这里有个细节:set_sequencer的第二个参数就是adapter。UVM内部会通过adapter完成事务转换。你想想看,如果没有adapter,模型发出的读写请求直接扔给sequencer,sequencer根本不知道该怎么处理——因为总线协议不认识通用事务啊。

小技巧:如果你的总线是APB,UVM自带了一个uvm_reg_adapter的子类,你可以直接继承它,重写reg2bus()bus2reg()两个方法。我一般这么写:

class my_adapter extends uvm_reg_adapter;
  `uvm_object_utils(my_adapter)

  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

  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", "bus2reg: 类型转换失败")
    end
    rw.addr   = tr.addr;
    rw.data   = tr.data;
    rw.status = UVM_IS_OK;
  endfunction
endclass

你看,reg2bus把寄存器模型的读写请求转成APB事务,bus2reg把APB响应转回寄存器模型能理解的状态。就这么简单。

3.3 设置default_map与sequencer

最后一步,也是很多人容易搞混的——default_map是什么?为什么要设置它?

每个reg_block都有一个default_map,它本质上是一个地址映射表。寄存器模型通过default_map知道:某个寄存器在哪个地址上,读写它要走哪个sequencer。

说白了,default_map就是寄存器模型的“路由表”。没有它,模型不知道往哪儿发请求。

设置方式其实上面已经提到了:

reg_model.default_map.set_sequencer(sequencer, adapter);

但这里有个坑——地址偏移。如果你的DUT中寄存器的基地址不是0,你得在default_map里设置偏移量:

reg_model.default_map.set_base_addr(32'h4000_0000);

我记得有一次做项目,DUT的寄存器基地址是0x5000_0000,我忘了设偏移量。结果所有寄存器读写都发到了地址0上,DUT当然没反应。查了整整两天,最后发现是基地址没设——你说气不气人?

警告:如果你有多个总线接口(比如同时有APB和AHB),你需要为每个接口创建独立的map。default_map只能对应一个物理接口。别试图用一个map覆盖所有总线,那会出大问题。

3.4 完整的集成流程

好,我们把上面三步串起来,看看一个完整的env长什么样:

class my_env extends uvm_env;
  `uvm_component_utils(my_env)

  my_reg_block reg_model;
  my_adapter   adapter;
  my_sequencer sequencer;

  function void build_phase(uvm_phase phase);
    super.build_phase(phase);
    reg_model = my_reg_block::type_id::create("reg_model", this);
    adapter   = my_adapter::type_id::create("adapter", this);
    sequencer = my_sequencer::type_id::create("sequencer", this);
    reg_model.build();  // 别忘了!
    // 如果基地址不是0,在这里设置
    // reg_model.default_map.set_base_addr(32'h4000_0000);
  endfunction

  function void connect_phase(uvm_phase phase);
    super.connect_phase(phase);
    reg_model.default_map.set_sequencer(sequencer, adapter);
  endfunction
endclass

你看,核心代码就这么几行。但每一步都有讲究:

  • build_phase:实例化所有组件,包括模型、adapter、sequencer
  • connect_phase:把模型通过adapter挂到sequencer上
  • default_map:设置地址映射和基地址

3.5 避坑指南

最后,我把自己踩过的坑总结一下,你遇到了可以少走弯路:

  1. 忘记调reg_model.build()——模型内部寄存器全是空的,读写直接报错
  2. adapter的reg2bus和bus2reg写反了——读操作变成了写操作,数据全乱套
  3. 基地址没设——所有读写都发到地址0,DUT没响应
  4. 多个总线接口共用一个map——事务发错接口,仿真挂死
  5. 在build_phase里做连接——sequencer还没准备好,连接失败

我曾经在项目交付前一周发现寄存器读写全失败,就是因为adapter里bus2reg少写了一行rw.status = UVM_IS_OK。模型收不到成功状态,就一直重试,最后超时。嗯,从那以后我写adapter都会反复检查这两个方法。

好了,这一章就到这里。下一章咱们聊聊寄存器模型的前门访问和后门访问——这两个概念搞清楚了,你的验证效率能翻一倍。