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窗口。我画了一张图,帮你理解这个过程:

TLP地址路由流程 Root Complex Switch Endpoint 1 Endpoint 2 地址: 0xFC00_0000 BAR0: 0xFC00_0000 BAR0: 0xFD00_0000 ✓ 命中Endpoint 1 TLP携带目标地址 Switch查转发表 Endpoint检查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