3. 事务层协议(TLP):TLP的组成、分类与路由方式
各位同学,咱们今天聊聊事务层协议,也就是TLP。这是PCIe协议里最核心的部分,没有之一。你想想看,设备之间要传数据、要读配置、要发中断,全靠TLP这个载体。我刚开始接触PCIe时,总觉得TLP结构复杂得让人头大,后来摸透了规律,发现其实就那么几块东西拼在一起。
3.1 TLP的组成:Header、Data、Digest
一个完整的TLP,说白了就是三部分:头部(Header)、数据载荷(Data Payload)、可选的摘要(Digest)。嗯,这里要注意,Digest不是每个TLP都有的,只有启用了ECRC(端到端CRC)时才带上。
TLP基本结构:
+----------------+------------------+----------+
| TLP Header | Data Payload | Digest |
| (12或16字节) | (0~4096字节) | (4字节) |
+----------------+------------------+----------+
Header是TLP的灵魂。它告诉接收方:这个包要去哪、干什么、有多大。Header的长度取决于TLP类型——Memory读写是3个DW(12字节),Completion和Message是4个DW(16字节)。我个人习惯在调试时先看Header的Fmt和Type字段,这两个字段决定了整个包的解析方式。
Data Payload就是实际要传输的数据。最大可以到4096字节,但实际项目中很少用满。我记得有一次调试一个NVMe SSD,发现性能上不去,查了半天原来是Payload大小被限制在128字节。调整Max Payload Size后,吞吐量直接翻倍。
Digest是ECRC校验值,4字节。我曾经在项目中遇到过ECRC计算错误导致链路反复重传的问题,排查了整整两天才发现是某个IP核的ECRC生成逻辑有bug。从那以后,我每次做验证都会专门写一个ECRC检查的scoreboard。
3.2 TLP的分类:Memory、IO、Configuration、Message
TLP按用途可以分为四大类。你想想看,PCIe设备要访问内存、要读写配置空间、要发中断、要报告错误,每种场景都需要不同的TLP类型。
| TLP类型 | 用途 | 路由方式 | 常见场景 |
|---|---|---|---|
| Memory | 读写系统内存或设备BAR空间 | 地址路由 | DMA传输、显存访问 |
| IO | 读写IO空间(已逐渐淘汰) | 地址路由 | 传统设备兼容 |
| Configuration | 读写设备的配置空间 | ID路由 | 枚举、驱动初始化 |
| Message | 传递中断、错误、电源管理等事件 | 隐式路由 | MSI中断、热插拔 |
Memory TLP是最常用的。几乎所有数据搬运都靠它。Memory Read和Memory Write,加上带锁的Memory Read Lock(现在很少用了)。我建议你在做验证时,重点覆盖Memory TLP的各种对齐情况——地址不对齐、长度不对齐,这些边界条件最容易踩坑。
IO TLP基本可以忽略。PCIe规范里虽然还保留着,但新设计都不用了。你只要知道有这么个东西就行。
Configuration TLP是枚举阶段的主角。Type 0配置读写针对Endpoint,Type 1配置读写针对Switch。我记得刚做验证时,总是搞混Type 0和Type 1的区别。后来想明白了:Type 1是用来穿越Switch的,Type 0是终点站的。
Message TLP很有意思。它没有地址,也没有目标ID,全靠隐式路由。最常见的Message就是MSI/MSI-X中断。嗯,这里要注意,Message还有带数据的变种——Message with Data,比如PME_Turn_Off消息就属于这一类。
3.3 TLP的路由方式:地址路由、ID路由、隐式路由
TLP怎么找到目的地?三种路由方式:地址路由、ID路由、隐式路由。每种方式对应不同的TLP类型,也对应不同的硬件实现。
3.3.1 地址路由
Memory和IO TLP使用地址路由。说白了,就是根据Header里的地址字段,一路查Switch的转发表,直到找到匹配的BAR窗口。我画了一张图,帮你理解这个过程:
Switch内部维护着一张地址转发表,记录了每个下游端口的地址范围。当TLP到达Switch时,Switch提取Header里的地址,跟转发表逐条比对。匹配上了,就转发到对应端口。匹配不上,就转发到上游(默认路径)。
避坑指南:我曾经在验证Switch时,发现某个地址范围的TLP总是转发到错误的端口。查了三天,最后发现是转发表的地址掩码配置错了。BAR的基地址必须按大小对齐,比如4KB的BAR,基地址的低12位必须是0。这个对齐要求很容易被忽略。
3.3.2 ID路由
Configuration TLP使用ID路由。ID由三部分组成:Bus Number(总线号)、Device Number(设备号)、Function Number(功能号),合起来就是BDF。每个PCIe设备都有唯一的BDF。
ID路由的过程是这样的:Root Complex发出Type 1配置读写TLP,Header里包含目标BDF。Switch收到后,检查目标Bus Number是否等于自己的Secondary Bus Number。如果匹配,说明目标设备就在这个Switch的下游,Switch把TLP转成Type 0发出去。如果不匹配,Switch继续往下游转发。
ID路由示例:
TLP Header (Type 1 Configuration Read):
Bus Number = 0x02
Device Number = 0x00
Function Number = 0x00
Switch转发逻辑:
if (TLP.Bus == Switch.SecondaryBus) {
转发到下游端口,并转换为Type 0
} else if (TLP.Bus 在 Switch.SubordinateBus范围内) {
继续向下游转发(保持Type 1)
} else {
转发到上游
}
我记得在验证PCIe枚举流程时,经常需要构造各种BDF组合来测试Switch的转发逻辑。特别是当系统中有多级Switch级联时,Bus Number的分配和转发逻辑会变得非常复杂。
3.3.3 隐式路由
Message TLP使用隐式路由。为什么叫隐式?因为TLP Header里既没有地址,也没有BDF。接收方完全靠Message的类型来判断该由谁处理。
隐式路由分为三种:
- 广播到所有设备:比如PME_Turn_Off消息,所有设备都要响应。
- 路由到Root Complex:比如错误消息(ERR_COR、ERR_NONFATAL、ERR_FATAL),直接上报到RC。
- 路由到本地:比如INTx中断消息,由接收消息的端口自己处理。
嗯,这里要注意,MSI/MSI-X中断虽然也是Message,但它们用的是地址路由,不是隐式路由。因为MSI需要把中断向量写到指定的内存地址。这个区别很容易搞混,我刚开始学的时候也犯过这个错。
重要提醒:隐式路由的Message不能被Switch随意转发。Switch必须根据Message类型决定转发方向。比如一个ERR_NONFATAL消息,Switch收到后只能往上游转发,不能往下游广播。我曾经在项目中遇到过Switch错误地把错误消息广播到所有端口,导致整个系统中断风暴。
3.4 小结
TLP是PCIe通信的基本单元。Header决定了包的类型和路由方式,Data承载了实际数据,Digest保证了端到端的数据完整性。三种路由方式各有各的适用场景:地址路由用于数据搬运,ID路由用于配置访问,隐式路由用于系统事件传递。
做验证时,我建议你重点覆盖:TLP边界长度、地址对齐、ECRC错误注入、路由冲突这些场景。这些是实际项目中出问题最多的地方。
个人经验:我习惯在验证环境中加一个TLP监控器,实时抓取总线上的所有TLP,并自动解析Header字段。这样调试时能快速定位问题。比如某个TLP路由错了,一看Header里的地址和路由字段,马上就能知道是Switch的转发表配置错了,还是Endpoint的BAR没初始化。
公众号:蓝海资料掘金营,微信deep3321