4. TLM适配器工程案例2:将非标准TLM接口(如APB总线协议)转换为标准TLM put/get接口

在复杂的SoC验证环境中,大量IP核使用总线协议(如APB、AHB、AXI)进行通信,而非标准的TLM接口。本案例聚焦于如何将APB从设备接口封装为UVM中标准的TLM put/get接口,使得验证组件(如激励发生器、记分板)能够通过统一的TLM调用与APB从设备交互,而无需关心底层总线时序。

4.1 案例背景与设计意图

APB(Advanced Peripheral Bus)是一种简单的、低功耗的总线协议,通常用于连接低速外设。其典型信号包括:PCLK、PRESETn、PADDR、PWRITE、PSEL、PENABLE、PWDATA、PRDATA 和 PREADY。APB从设备接口是一个典型的非标准TLM接口,因为它基于时钟周期和握手信号,而不是基于事务级的方法调用。

本案例的目标是创建一个 apb2tlm_adapter 组件,该组件在内部处理APB总线时序,对外则提供两个标准的TLM端口:

  • put_port:用于接收写事务(apb_write_txn)。
  • get_port:用于请求读事务(apb_read_txn)并返回读取的数据。
设计要点:适配器应作为被动组件存在,等待上游组件通过TLM端口发起事务。适配器内部维护一个有限状态机(FSM)来模拟APB总线的 SETUP 和 ACCESS 状态,从而将TLM方法调用转换为精确的APB时序波形。

4.2 适配器架构与SVG结构图

下图展示了 apb2tlm_adapter 在验证环境中的位置及其内部架构。适配器位于TLM激励发生器与APB从设备DUT之间,起到协议转换与桥接的作用。

APB到TLM适配器架构图 TLM激励发生器 (Driver / Sequence) apb2tlm_adapter TLM put/get 端口 ↓ APB FSM 时序转换 虚拟接口 (vif) DUT APB从设备 put(apb_write_txn) get(apb_read_txn) APB 时序信号 关键组件说明 1. TLM激励发生器:通过 put/get 端口发起事务,不感知APB协议。 2. apb2tlm_adapter:核心转换器,包含TLM端口、事务FIFO和APB FSM。 3. DUT (APB从设备):接收适配器驱动的APB信号,返回响应。 4. 虚拟接口 (vif):适配器通过vif驱动和监测DUT的APB端口信号。 5. 数据流:TLM事务 → 适配器FSM → APB时序 → DUT响应 → 返回TLM。

4.3 核心实现:事务定义与适配器组件

4.3.1 定义APB事务对象

首先,需要定义一个继承自 uvm_sequence_item 的事务类,用于封装APB读写操作的所有必要信息。

class apb_txn extends uvm_sequence_item;
  rand bit [31:0]  addr;
  rand bit [31:0]  data;
  rand bit         write;  // 1: write, 0: read
  bit [31:0]       rdata;  // 用于存储读取返回的数据
  bit              error;  // 用于指示APB传输错误(如SLVERR)

  `uvm_object_utils_begin(apb_txn)
    `uvm_field_int(addr, UVM_ALL_ON)
    `uvm_field_int(data, UVM_ALL_ON)
    `uvm_field_int(write, UVM_ALL_ON)
    `uvm_field_int(rdata, UVM_ALL_ON)
    `uvm_field_int(error, UVM_ALL_ON)
  `uvm_object_utils_end

  function new(string name = "apb_txn");
    super.new(name);
  endfunction
endclass

4.3.2 适配器组件实现

适配器 apb2tlm_adapter 继承自 uvm_component,并实现TLM put/get 方法。其核心是一个处理APB时序的有限状态机。

class apb2tlm_adapter extends uvm_component;
  // TLM 端口
  uvm_put_port #(apb_txn)   put_port;
  uvm_get_port #(apb_txn)   get_port;

  // 虚拟接口
  virtual apb_if vif;

  // 内部状态
  typedef enum {IDLE, SETUP, ACCESS} apb_state_e;
  apb_state_e state;

  `uvm_component_utils(apb2tlm_adapter)

  function new(string name, uvm_component parent);
    super.new(name, parent);
    put_port = new("put_port", this);
    get_port = new("get_port", this);
  endfunction

  // 在run_phase中实现APB时序转换
  virtual task run_phase(uvm_phase phase);
    apb_txn txn;
    state = IDLE;
    forever begin
      // 等待TLM端口有事务到达
      fork
        begin : put_handler
          put_port.get(txn);  // 阻塞等待写事务
          drive_apb_write(txn);
        end
        begin : get_handler
          get_port.get(txn);  // 阻塞等待读事务请求
          drive_apb_read(txn);
          // 将读取的数据返回给请求者
          // 注意:get_port的get方法通常需要返回数据,这里简化处理
          // 实际工程中可能需要使用peek或额外的TLM返回路径
        end
      join_any
      disable fork;
    end
  endtask

  // 驱动APB写时序
  task drive_apb_write(apb_txn txn);
    @(posedge vif.pclk);
    // SETUP phase
    vif.psel    <= 1;
    vif.penable <= 0;
    vif.paddr   <= txn.addr;
    vif.pwrite  <= 1;
    vif.pwdata  <= txn.data;
    @(posedge vif.pclk);
    // ACCESS phase
    vif.penable <= 1;
    wait(vif.pready == 1);
    @(posedge vif.pclk);
    // 完成传输
    vif.psel    <= 0;
    vif.penable <= 0;
    txn.error   <= (vif.pslverr === 1) ? 1 : 0;
  endtask

  // 驱动APB读时序
  task drive_apb_read(apb_txn txn);
    @(posedge vif.pclk);
    // SETUP phase
    vif.psel    <= 1;
    vif.penable <= 0;
    vif.paddr   <= txn.addr;
    vif.pwrite  <= 0;
    @(posedge vif.pclk);
    // ACCESS phase
    vif.penable <= 1;
    wait(vif.pready == 1);
    @(posedge vif.pclk);
    // 采样数据
    txn.rdata   <= vif.prdata;
    txn.error   <= (vif.pslverr === 1) ? 1 : 0;
    // 完成传输
    vif.psel    <= 0;
    vif.penable <= 0;
  endtask
endclass

4.4 常见问题与解决方案

常见问题 现象 根因分析 解决方案
TLM端口阻塞导致死锁 仿真挂起,适配器无法处理新事务 put_port.get() 和 get_port.get() 同时阻塞,但只有一个事务到达,导致另一个分支永远等待。 使用 fork...join_any 配合 disable fork,或使用 uvm_tlm_fifo 解耦。
APB时序不匹配 DUT采样到错误的地址或数据 适配器未严格遵循APB的SETUP/ACCESS两周期握手,或未等待 PREADY 信号。 在 drive_apb_write/read 任务中严格按状态机实现,确保 PSEL 和 PENABLE 时序正确。
读事务数据返回路径缺失 激励发生器无法获取读取的数据 get_port 仅用于请求,但适配器未将读取的数据写回事务对象或通过另一个TLM端口返回。 使用 uvm_get_peek_port 或增加一个 put_export 用于返回数据,确保双向数据流完整。
背压处理不当 适配器在DUT未准备好时持续驱动 未在 ACCESS 阶段等待 PREADY 信号,导致数据丢失或协议违规。 在驱动循环中插入 wait(vif.pready == 1),实现真正的握手。

4.5 适配器FSM状态转换图

为了更清晰地展示适配器内部的APB时序控制逻辑,下图给出了状态机的详细转换过程。该FSM确保每个TLM事务都能被正确地转换为符合APB规范的波形。

APB适配器内部FSM状态转换图 IDLE 等待TLM事务 SETUP 驱动地址/控制 ACCESS 使能 & 等待PREADY 事务到达 时钟上升沿 PREADY=1 & 时钟上升沿 说明:FSM在IDLE状态等待TLM事务。一旦收到事务,进入SETUP状态驱动地址和写数据。 下一个时钟沿进入ACCESS状态,使能PENABLE并等待PREADY。传输完成后返回IDLE,准备处理下一个事务。

4.6 验证与调试建议

  • 使用TLM FIFO解耦:在适配器内部引入 uvm_tlm_fifo #(apb_txn),将TLM端口的接收与APB驱动分离,可以有效避免死锁,并支持背压。
  • 断言检查:在适配器内部添加APB协议断言(如 PSEL 与 PENABLE 的时序关系),确保转换后的波形符合APB规范。
  • 覆盖率收集:收集APB状态机状态转换覆盖率以及地址/数据范围的覆盖率,确保适配器在各种边界条件下都被充分测试。
  • 仿真波形调试:在调试阶段,将适配器的内部状态(state)和TLM事务内容(txn.addr, txn.write)通过 $display 或记录到日志中,便于快速定位协议转换错误。
工程陷阱提醒:不要试图在适配器内部处理复杂的APB流水线或Split传输。本案例的适配器设计为一次处理一个事务,适用于简单的APB从设备。对于高性能总线(如AXI),需要设计更复杂的适配器状态机,并考虑乱序传输和多个未完成事务的处理。