2. 核心接口:put/get、transport、nb_transport 与时序模型
好,咱们今天聊聊 TLM 通信里最核心的几个接口。说实话,我刚接触 TLM 那会儿,也被这些接口搞得有点晕——put/get、transport、nb_transport,还有阻塞非阻塞,到底什么时候该用哪个?
我个人的习惯是,先别急着背接口定义,而是想清楚一件事:你的模型到底要模拟什么样的通信行为? 是简单的数据搬运,还是带时序的精确交互?想清楚这个,接口选择就顺理成章了。
2.1 put/get 接口:最简单的数据搬运
put/get 接口,说白了就是两个操作:放数据和取数据。它是最基础的 TLM 通信方式,适合那些不需要关心时序细节的场景。
核心特点:
- put():发起方把数据塞给接收方
- get():发起方从接收方把数据拿过来
- 两者都是阻塞调用——调用没返回,你就等着
我在项目中遇到过这样一个场景:一个简单的 FIFO 模型,只需要把事务从一个模块搬到另一个模块。用 put/get 就足够了,代码写起来也清爽。
// 一个简单的 put/get 示例
class producer extends uvm_component;
uvm_blocking_put_port #(my_transaction) put_port;
virtual task run_phase(uvm_phase phase);
my_transaction tr;
// 创建事务,然后发出去
tr = my_transaction::type_id::create("tr");
put_port.put(tr); // 阻塞在这里,直到对方取走
endtask
endclass
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
嗯,这里要注意:put/get 接口默认是阻塞的。如果你想要非阻塞版本,可以用 try_put() 和 try_get(),它们会立即返回一个状态值告诉你成功还是失败。
我的小建议: 如果你的模型只是做功能验证,不关心精确时序,put/get 是最省事的。但如果你要做性能分析,就得考虑 transport 接口了。
2.2 transport 接口:带往返的通信
transport 接口,你可以把它理解成一次完整的请求-响应交互。它把 put 和 get 合并成了一个操作——你发一个请求出去,然后等着响应回来。
为什么会需要这个?你想想看,很多总线协议(比如 AXI)都是请求和响应分开的。你用 put 发请求,再用 get 收响应,中间还得自己维护关联关系,多麻烦。transport 直接帮你搞定。
transport 接口有两种形式:
- transport():阻塞版本,发完请求后一直等到响应回来
- nb_transport():非阻塞版本,发完请求就返回,响应通过回调通知
// 阻塞 transport 示例
class master extends uvm_component;
uvm_blocking_transport_port #(req_t, rsp_t) trans_port;
virtual task run_phase(uvm_phase phase);
req_t req;
rsp_t rsp;
req.addr = 32'h1000;
req.data = 32'hDEAD_BEEF;
trans_port.transport(req, rsp); // 发请求,等响应
// 此时 rsp 里已经有结果了
endtask
endclass
我曾经在一个 DDR 控制器模型里用过 transport 接口。当时需要模拟一次读操作——先发地址,再等数据回来。用 transport 接口,一行代码就搞定了请求和响应的配对,省了不少事。
注意: transport 接口是原子操作,也就是说,在 transport 返回之前,发起方不能做其他事。如果你的模型需要并发处理多个事务,就得考虑非阻塞版本了。
2.3 nb_transport 接口:非阻塞的时序交互
nb_transport 接口,全称是 non-blocking transport。它和 transport 最大的区别在于:调用立即返回,不等待。
你可能会问:「那响应怎么回来?」嗯,这就是 nb_transport 的精髓了——它通过回调机制来处理响应。具体来说,nb_transport 会返回一个状态值,告诉调用方「我收下了,你忙你的」,然后等处理完了再通过另一个方向的通知把结果送回来。
nb_transport 的返回值:
| 返回值 | 含义 |
|---|---|
| TLM_ACCEPTED | 请求被接受,但还没处理完 |
| TLM_UPDATED | 请求被接受,并且处理了一部分 |
| TLM_COMPLETED | 请求已经处理完成,响应可用 |
// nb_transport 示例
class slave extends uvm_component;
uvm_nonblocking_transport_imp #(req_t, rsp_t, slave) imp;
virtual function tlm_sync_state nb_transport_fw(req_t req, ref rsp_t rsp);
// 立即接受请求
if (can_accept(req)) begin
// 把请求加入队列,稍后处理
request_queue.push_back(req);
return TLM_ACCEPTED;
end else begin
return TLM_INCOMPLETE;
end
endfunction
// 处理完请求后,通过 nb_transport_bw 回调通知 master
virtual function void process_requests();
// ... 处理逻辑
master.nb_transport_bw(rsp);
endfunction
endclass
我个人觉得,nb_transport 是 TLM 里最强大但也最容易用错的接口。为什么?因为它引入了时序的概念——你不再只是「放数据」和「取数据」,而是在模拟一个带延迟的通信过程。
避坑指南: 我曾经在一个项目里把所有通信都用了 nb_transport,结果代码变得极其复杂,调试起来想哭。后来我学乖了:只有当你真的需要模拟时序延迟时,才用 nb_transport。否则,用阻塞接口更省心。
2.4 时序与阻塞/非阻塞模型
好,咱们来聊聊时序模型。说白了,TLM 的时序模型就两种:粗略时序和精确时序。
- 粗略时序(Loosely Timed, LT):用阻塞接口,时间跳跃式前进。适合快速仿真和功能验证。
- 精确时序(Approximately Timed, AT):用非阻塞接口,时间精确到时钟周期。适合性能分析和架构探索。
两种时序模型的对比:
| 特性 | LT(粗略时序) | AT(精确时序) |
|---|---|---|
| 接口类型 | 阻塞(put/get, transport) | 非阻塞(nb_transport) |
| 时间精度 | 低,以事务为单位 | 高,以时钟周期为单位 |
| 仿真速度 | 快 | 慢 |
| 适用场景 | 功能验证、软件调试 | 性能分析、架构优化 |
我个人的经验是:前期做功能验证时,用 LT 模型就够了。等到需要分析系统瓶颈、优化带宽时,再切换到 AT 模型。别一上来就搞精确时序,否则仿真跑一天都跑不完一个用例。
你想想看,一个 SoC 里有几十个 master 和 slave,如果每个通信都用 nb_transport 精确到时钟周期,那仿真速度会慢到什么程度?所以,选择合适的时序模型,本身就是一种性能调优。
再强调一次: 阻塞接口(put/get, transport)适合 LT 模型,非阻塞接口(nb_transport)适合 AT 模型。混用的时候要小心,别在 LT 模型里用 nb_transport,那会白白增加复杂度。
2.5 本章知识体系
为了让你更直观地理解这些接口的关系,我画了一张图:
这张图把今天讲的内容串起来了。你从上往下看:先选接口类型(阻塞还是非阻塞),再选时序模型(LT 还是 AT),最后根据场景做取舍。说白了,没有最好的接口,只有最合适的。
好,这一章就到这里。记住我的一句话:接口是工具,不是目的。搞清楚你的模型要模拟什么行为,再去选接口,事半功倍。