4. 事务级建模(TLM)

TLM,说白了就是让组件之间能「递东西」。

我在刚接触UVM那会儿,总觉得TLM是个玄学。后来做项目做多了才明白——它其实就是一套标准接口,让不同组件之间能高效地传递事务对象。你想想看,如果没有TLM,每个组件都得自己写通信逻辑,那代码得多乱?

4.1 TLM的核心思想

TLM把验证环境里的通信抽象成「端口」和「事务」。端口就是接口,事务就是数据包。组件之间通过端口连接,传递事务对象。

我个人习惯把TLM理解成「快递系统」:

  • 端口 = 快递收发点
  • 事务 = 包裹
  • 通信方式 = 快递服务类型(普通、加急、只看看不拿走)

TLM的优势:组件之间解耦。你改一个组件的内部实现,只要端口不变,其他组件完全不受影响。

4.2 五种基本端口

UVM里最常用的TLM端口有五种。我按使用频率排个序:

端口类型 方向 行为描述 典型场景
put 主动方→被动方 发送方把事务「塞」给接收方 driver从sequencer拿item
get 主动方←被动方 接收方从发送方「取」事务 monitor从fifo读数据
peek 主动方←被动方 看一眼数据,但不拿走 scoreboard检查但不消费
transport 双向 请求+响应,一次完成 总线事务的读写操作
analysis 广播 一对多,发布者不关心谁在收 monitor向多个组件广播数据

我的经验:项目中80%的场景用put和analysis就够了。别一上来就想着用transport,它虽然强大,但容易把代码搞复杂。

4.3 put和get:最基础的通信

put和get是TLM的「亲儿子」。一个负责塞,一个负责拿。

先看put的用法:

// 发送方
class producer extends uvm_component;
  uvm_blocking_put_port #(my_transaction) put_port;
  
  virtual task run_phase(uvm_phase phase);
    my_transaction tr;
    repeat(10) begin
      tr = my_transaction::type_id::create("tr");
      // 填充数据...
      put_port.put(tr);  // 把事务塞出去
    end
  endtask
endclass

// 接收方
class consumer extends uvm_component;
  uvm_blocking_put_imp #(my_transaction, consumer) put_export;
  
  virtual function void put(my_transaction tr);
    // 处理收到的事务
    `uvm_info("CONSUMER", $sformatf("收到事务: %s", tr.sprint()), UVM_LOW)
  endfunction
endclass

我记得第一次用put时犯了个低级错误——忘了在接收方实现put函数。结果仿真跑起来直接报空指针,查了半天才发现。嗯,这里要注意:export端必须实现对应的函数。

get的用法类似,只是方向反了:

// 接收方主动去拿
class consumer extends uvm_component;
  uvm_blocking_get_port #(my_transaction) get_port;
  
  virtual task run_phase(uvm_phase phase);
    my_transaction tr;
    get_port.get(tr);  // 阻塞等待,直到拿到数据
    // 处理tr...
  endtask
endclass

4.4 peek:只看不拿

peek是个有意思的端口。它和get的区别在于:get会把数据从队列里移除,而peek只是看一眼,数据还在。

我曾经在做一个协议检查器时用过peek。场景是这样的:scoreboard需要先看一眼当前数据是不是期望的,如果是就get走,如果不是就报错。用peek正好合适。

class scoreboard extends uvm_component;
  uvm_blocking_peek_imp #(my_transaction, scoreboard) peek_export;
  uvm_blocking_get_imp  #(my_transaction, scoreboard) get_export;
  
  my_transaction expected_tr;
  
  virtual function void peek(output my_transaction tr);
    tr = expected_tr;  // 只是复制,不移除
  endfunction
  
  virtual function void get(output my_transaction tr);
    tr = expected_tr;
    expected_tr = null;  // 真正移除
  endfunction
endclass

避坑指南:peek和get配合使用时,一定要保证先peek再get。我曾经在项目中搞反了顺序,结果数据被get走了,peek永远拿不到东西。调试了一下午才发现。

4.5 transport:一次搞定请求和响应

transport是双向通信的「快捷方式」。它把请求和响应打包成一个操作,适合总线协议那种「发个读命令,等数据回来」的场景。

class bus_driver extends uvm_component;
  uvm_blocking_transport_port #(bus_req, bus_rsp) transport_port;
  
  virtual task run_phase(uvm_phase phase);
    bus_req req;
    bus_rsp rsp;
    
    req = bus_req::type_id::create("req");
    req.addr = 32'h1000;
    req.cmd  = READ;
    
    transport_port.transport(req, rsp);  // 发请求,等响应
    `uvm_info("DRIVER", $sformatf("读回数据: %h", rsp.data), UVM_LOW)
  endtask
endclass

说实话,transport我用的不多。为什么?因为它把两个操作绑死了,灵活性差。我更喜欢把请求和响应分开处理,用两个put端口。这样调试起来更清晰。

4.6 analysis端口:广播的艺术

analysis端口是TLM里的「广播站」。它不关心谁在听,只管往外发。一个monitor可以同时连接多个scoreboard、coverage collector、checker,monitor只管发,其他组件自己决定要不要收。

// 发布方
class monitor extends uvm_component;
  uvm_analysis_port #(my_transaction) analysis_port;
  
  virtual task run_phase(uvm_phase phase);
    my_transaction tr;
    forever begin
      // 从接口采样数据...
      analysis_port.write(tr);  // 广播出去
    end
  endtask
endclass

// 订阅方
class scoreboard extends uvm_component;
  uvm_analysis_imp #(my_transaction, scoreboard) analysis_export;
  
  virtual function void write(my_transaction tr);
    // 收到广播的数据
    check_transaction(tr);
  endfunction
endclass

关键点:analysis_port.write()是非阻塞的。发布方不会等订阅方处理完才继续。这在性能上是个巨大优势。

4.7 FIFO:通信的缓冲器

光有端口还不够。你想想看,如果发送方和接收方速度不匹配怎么办?比如driver跑得飞快,monitor处理不过来。这时候就需要FIFO来缓冲。

UVM提供了uvm_tlm_fifo,它本质上是一个带TLM端口的队列:

class env extends uvm_component;
  uvm_tlm_fifo #(my_transaction) fifo;
  
  function void build_phase(uvm_phase phase);
    fifo = uvm_tlm_fifo #(my_transaction)::type_id::create("fifo", this);
    fifo.set_size(16);  // 设置深度为16
  endfunction
  
  function void connect_phase(uvm_phase phase);
    // 把monitor的analysis_port连到fifo的analysis_export
    monitor.analysis_port.connect(fifo.analysis_export);
    // 把scoreboard的get_port连到fifo的get_export
    scoreboard.get_port.connect(fifo.get_export);
  endfunction
endclass

FIFO的好处是:

  • 解耦时序:发送方和接收方可以跑在不同的速率上
  • 内置同步:满了会阻塞发送方,空了会阻塞接收方
  • 支持多种端口:put、get、peek、analysis都能用

我的习惯:在monitor和scoreboard之间,我几乎必加一个FIFO。哪怕现在速度匹配,谁知道以后会不会加新功能?留个缓冲,心里踏实。

4.8 知识体系总览

下面这张图把TLM通信的核心逻辑串起来了。我画这张图时特意把FIFO放在中间——它确实是整个通信架构的「交通枢纽」。

TLM通信核心架构 发送方 (Producer) put_port analysis_port FIFO uvm_tlm_fifo put_export get_export get_port analysis_export 接收方 (Consumer) 通信方式说明: • put:发送方主动推送,接收方被动接收(阻塞) • get:接收方主动拉取,发送方被动提供(阻塞) • peek:接收方查看数据但不消费(阻塞) • transport:一次调用完成请求+响应(阻塞) • analysis:广播式通信,一对多,非阻塞

4.9 实战中的选择建议

说了这么多,到底该怎么选?我总结了几条经验:

  1. driver从sequencer拿item:用put或get都行。我个人习惯用get,因为driver是主动方,它决定什么时候要下一个item。
  2. monitor向scoreboard发数据:用analysis_port + FIFO。monitor只管发,FIFO负责缓冲,scoreboard慢慢处理。
  3. 需要检查但不消费数据:用peek。比如协议检查器需要看当前数据是否合法,但不影响后续处理。
  4. 总线事务:用transport。但如果你需要更精细的控制,拆成两个put操作也行。

曾经踩过的坑:我在一个项目中用了analysis_port直接连scoreboard,没加FIFO。结果monitor一秒钟发10万个事务,scoreboard处理不过来,仿真内存直接爆了。后来加了FIFO,设了个合适的深度,问题就解决了。所以——FIFO不是可选项,是必需品。

好了,TLM通信基础就聊到这儿。记住一句话:端口是接口,FIFO是缓冲,analysis是广播。把这三点吃透了,TLM就算入门了。

可可精品资料库