2. TLM接口基础:put/get/transport等核心TLM操作原语
TLM(Transaction-Level Modeling)接口是UVM验证平台中组件间通信的基石。它通过一组标准化的操作原语,将事务级数据的传输、同步与时间消耗抽象出来,使验证组件(如driver、monitor、scoreboard)之间能够以统一的方式进行数据交换,而无需关心底层信号级细节。
2.1 核心操作原语概览
TLM 1.0 定义了三种最基本的单向通信原语:put、get 和 transport。它们分别对应数据的生产、消费和组合操作。下表列出了这些原语的语义、方向及典型使用场景:
| 原语 | 方向 | 语义描述 | 典型使用场景 |
|---|---|---|---|
| put | 发起方 → 接收方 | 发起方将事务推送给接收方,接收方阻塞直到完成处理。 | driver从sequencer获取sequence item后,将其发送给DUT接口。 |
| get | 发起方 ← 接收方 | 发起方向接收方请求一个事务,接收方阻塞直到有数据可用。 | monitor从接口收集事务后,将其发送给scoreboard进行分析。 |
| transport | 发起方 ↔ 接收方 | 组合操作:先发起一个请求事务,再等待一个响应事务返回。 | 寄存器模型通过总线UVC对DUT进行读写操作(request + response)。 |
关键理解: put/get 是单向原语,数据流只有一个方向;transport 是双向原语,内部封装了 request 和 response 两个阶段。在适配器封装中,正确区分单向与双向原语是避免接口不匹配的首要条件。
2.2 put 与 get 原语详解
put 和 get 是 TLM 中最基础的两个操作。它们都支持 阻塞(blocking)和 非阻塞(non-blocking)两种调用方式,分别对应不同的时序要求。
2.2.1 阻塞 put (blocking_put)
调用方调用 put(T t) 后,会一直阻塞,直到接收方完成对该事务的处理并返回。接收方通过实现 put() 方法来定义处理逻辑。
// 发起方(Producer)调用示例
class producer extends uvm_component;
uvm_blocking_put_port #(my_transaction) put_port;
task run_phase(uvm_phase phase);
my_transaction tr;
tr = my_transaction::type_id::create("tr");
// ... 填充事务数据 ...
put_port.put(tr); // 阻塞直到接收方处理完成
endtask
endclass
// 接收方(Consumer)实现示例
class consumer extends uvm_component;
uvm_blocking_put_imp #(my_transaction, consumer) put_export;
function void put(my_transaction t);
// 处理事务 t 的逻辑
$display("Received transaction: %s", t.sprint());
endfunction
endclass
2.2.2 阻塞 get (blocking_get)
调用方调用 get(output T t) 后,会一直阻塞,直到接收方有事务可供取出。接收方通过实现 get() 方法来提供数据。
// 发起方(Consumer)调用示例
class consumer extends uvm_component;
uvm_blocking_get_port #(my_transaction) get_port;
task run_phase(uvm_phase phase);
my_transaction tr;
get_port.get(tr); // 阻塞直到有事务可用
// 分析事务 tr
endtask
endclass
// 接收方(Producer)实现示例
class producer extends uvm_component;
uvm_blocking_get_imp #(my_transaction, producer) get_export;
my_transaction fifo[$];
function void get(output my_transaction t);
t = fifo.pop_front(); // 从内部FIFO取出事务
endfunction
endclass
2.3 transport 原语详解
transport 原语将 put 和 get 组合成一个原子操作,用于实现 请求-响应 模式。它包含两个阶段:
- 请求阶段: 发起方将请求事务发送给接收方。
- 响应阶段: 接收方处理请求后,将响应事务返回给发起方。
// transport 原语接口定义
class uvm_transport #(type REQ = int, type RSP = int);
// 阻塞 transport
task transport(REQ request, output RSP response);
// 实现:处理 request,生成 response
endtask
// 非阻塞 transport
function bit transport(REQ request, output RSP response);
// 返回 1 表示立即完成,0 表示无法完成
endfunction
endclass
// 实际使用示例:寄存器读写
class reg_adapter extends uvm_reg_adapter;
function uvm_sequence_item reg2bus(const ref uvm_reg_bus_op rw);
bus_transaction tr = bus_transaction::type_id::create("tr");
tr.addr = rw.addr;
tr.data = rw.data;
tr.kind = (rw.kind == UVM_READ) ? READ : WRITE;
return tr;
endfunction
function void bus2reg(uvm_sequence_item bus_item, ref uvm_reg_bus_op rw);
bus_transaction tr;
$cast(tr, bus_item);
rw.data = tr.data;
rw.status = tr.status;
endfunction
endclass
适配器封装要点: 当使用 transport 原语封装寄存器访问时,必须确保适配器内部的 reg2bus 和 bus2reg 方法能够正确地将寄存器操作(读/写)映射为总线事务的 request 和 response。任何映射错误都会导致仿真数据不一致。
2.4 阻塞与非阻塞变体
TLM 为每个原语提供了阻塞和非阻塞两种变体,以适应不同的时序需求。下表总结了它们的差异:
| 变体 | 接口名称 | 返回值 | 阻塞行为 | 适用场景 |
|---|---|---|---|---|
| 阻塞 put | blocking_put |
无(task) | 调用方阻塞直到完成 | 同步数据传输,如driver驱动DUT |
| 非阻塞 put | nonblocking_put |
bit(function) |
立即返回,1=成功,0=失败 | 零延时试探,如monitor采样 |
| 阻塞 get | blocking_get |
无(task) | 调用方阻塞直到有数据 | scoreboard等待比对数据 |
| 非阻塞 get | nonblocking_get |
bit(function) |
立即返回,1=有数据,0=无数据 | 轮询检查FIFO状态 |
| 阻塞 transport | blocking_transport |
无(task) | 调用方阻塞直到响应返回 | 寄存器同步读写 |
| 非阻塞 transport | nonblocking_transport |
bit(function) |
立即返回,1=完成,0=未完成 | 流水线式总线访问 |
2.5 TLM操作原语关系图
下图展示了 put、get、transport 三种原语在验证组件间的数据流关系,以及它们与阻塞/非阻塞变体的组合方式:
2.6 适配器封装中的常见陷阱
陷阱1:原语方向不匹配。 例如,在适配器中错误地将 driver 的 put 接口连接到了 scoreboard 的 get 接口,导致数据流方向相反,仿真时出现死锁或数据丢失。
陷阱2:阻塞与非阻塞混用。 在同一个连接中,一方使用 blocking_put,另一方使用 nonblocking_put_imp,会导致编译通过但运行时行为异常(如非阻塞端无法处理阻塞端的等待)。
陷阱3:transport 的 request/response 类型不匹配。 适配器在 reg2bus 中返回的事务类型与 bus2reg 中期望的类型不一致,导致 $cast 失败。