4. 事务级建模(TLM):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放在中间——它确实是整个通信架构的「交通枢纽」。
4.9 实战中的选择建议
说了这么多,到底该怎么选?我总结了几条经验:
- driver从sequencer拿item:用
put或get都行。我个人习惯用get,因为driver是主动方,它决定什么时候要下一个item。 - monitor向scoreboard发数据:用
analysis_port+ FIFO。monitor只管发,FIFO负责缓冲,scoreboard慢慢处理。 - 需要检查但不消费数据:用
peek。比如协议检查器需要看当前数据是否合法,但不影响后续处理。 - 总线事务:用
transport。但如果你需要更精细的控制,拆成两个put操作也行。
曾经踩过的坑:我在一个项目中用了analysis_port直接连scoreboard,没加FIFO。结果monitor一秒钟发10万个事务,scoreboard处理不过来,仿真内存直接爆了。后来加了FIFO,设了个合适的深度,问题就解决了。所以——FIFO不是可选项,是必需品。
好了,TLM通信基础就聊到这儿。记住一句话:端口是接口,FIFO是缓冲,analysis是广播。把这三点吃透了,TLM就算入门了。
可可精品资料库