4、PCIe事务层协议(TLP):TLP格式、类型与路由方式

各位同学,今天我们聊聊PCIe协议里最核心的部分——事务层协议,也就是TLP。说实话,很多工程师调了几年PCIe,遇到链路不稳定或者设备不枚举,最后追根溯源,问题都出在TLP上。我个人习惯,拿到一个PCIe问题,先抓TLP包看看,十有八九能发现线索。

4.1 TLP长什么样?——格式解析

TLP的格式,说白了就是一套固定的“信封”。不管你是读数据还是写数据,信封的骨架都一样。我给大家拆开看看:

+--------+--------+--------+--------+--------+--------+--------+--------+
| 起始   | 序列号 | 头标   | 数据   | ECRC   | 结束   |
| (STP)  | (Seq#) | (Header)| (Data) | (可选) | (END)  |
+--------+--------+--------+--------+--------+--------+--------+--------+

嗯,这里要注意,我们平时调试时最关注的是头标(Header)数据(Data)部分。头标固定是3个或4个双字(DW),也就是12或16字节。为什么会有差异?取决于TLP的地址格式是32位还是64位。

头标关键字段一览:

  • Fmt[1:0]:格式类型,告诉你头标是3DW还是4DW,有没有数据负载。
  • Type[4:0]:事务类型,是存储器读写、IO读写、配置读写还是消息。
  • TC[2:0]:流量类别,用于QoS控制。
  • TD:是否包含ECRC。
  • EP:毒包标志,标记数据是否损坏。
  • Attr[1:0]:属性位,控制排序和缓存策略。
  • Length[9:0]:数据负载长度,单位是DW。
  • Requester ID:请求者总线/设备/功能号。
  • Tag:标签,用于匹配请求和完成。
  • Address/其他:目标地址或配置空间偏移。

我记得刚入行那会儿,有一次调一个FPGA的PCIe接口,设备总是枚举失败。抓了上千个TLP包,最后发现是头标里的Length字段填错了,多写了一个0。你想想看,一个字节的差错,整条链路都不认你。从那以后,我每次看TLP头标,都会拿协议手册逐位核对。

4.2 TLP的四种类型——各司其职

PCIe的TLP分为四大类:存储器事务IO事务配置事务消息事务。为什么这么分?因为PCIe要兼容PCI时代的各种用法,同时还要扩展新功能。

4.2.1 存储器事务(Memory TLP)

这是最常用的一类,占比超过90%。说白了就是CPU或者DMA引擎读写内存。分为读请求(MRd)、写请求(MWr)和带数据的读完成(CplD)。

实战经验:存储器写事务是Posted事务,不需要完成包。读事务是Non-Posted事务,必须等完成包回来。我曾经遇到一个性能问题,就是因为软件层把大量小尺寸读请求发下去,导致完成包太多,把链路堵死了。后来改成合并读,性能直接翻倍。

4.2.2 IO事务(IO TLP)

IO事务是为了兼容PCI时代的遗留设备。现在新设计的设备基本不用了。IO读(IORd)和IO写(IOWr)都是Non-Posted事务。我个人建议,新项目里尽量别用IO空间,直接用MMIO映射,省心省力。

4.2.3 配置事务(Configuration TLP)

配置事务用于枚举和初始化设备。分为Type 0(访问端点设备)和Type 1(访问桥/交换机)。配置读写都是Non-Posted事务。枚举阶段,RC会发配置读请求,设备必须回复完成包,告诉RC自己的Vendor ID、Device ID等信息。

避坑指南:我曾经调试过一个服务器主板,插上某款NVMe SSD后系统无法识别。抓包发现配置读请求的完成包超时了。原因是SSD的配置空间访问延迟超过了RC的等待时间。解决方案是在BIOS里调整配置超时参数。所以,如果你遇到设备枚举失败,先检查配置TLP的完成包是否正常返回。

4.2.4 消息事务(Message TLP)

消息事务是PCIe新增的,PCI时代没有。它用于中断(MSI/MSI-X)、错误报告(ERR_COR/ERR_NONFATAL/ERR_FATAL)、电源管理等。消息事务是Posted的,发出去就不管了。

这里有个细节:MSI中断本质上就是往一个特定地址写一个特定数据,所以它走的是存储器写TLP的格式,但Type字段标记为消息。嗯,这一点很多初学者会搞混。

4.3 TLP怎么找到路?——三种路由方式

TLP从发送端到接收端,怎么走?PCIe提供了三种路由方式:地址路由ID路由隐式路由。不同的TLP类型用不同的路由方式。

TLP类型 路由方式 说明
存储器读写 地址路由 根据目标地址,由交换机查路由表转发
IO读写 地址路由 类似存储器,但地址空间不同
配置读写(Type 0) ID路由 根据Bus/Device/Function号定位端点
配置读写(Type 1) ID路由 用于穿越桥/交换机
消息(PME/ERR等) 隐式路由 根据消息编码决定去向(RC、所有设备等)
完成(Cpl/CplD) ID路由 根据Requester ID和Tag回送

4.3.1 地址路由

存储器TLP和IO TLP使用地址路由。交换机内部维护一张地址映射表,收到TLP后,根据头标里的目标地址,找到对应的下游端口转发出去。如果地址不在任何端口的范围内,TLP会被转发到上游端口(如果存在的话)。

为什么会这样?因为PCIe拓扑是树形结构,RC是根,下面挂交换机和端点。地址路由就是逐级查表,直到找到目标设备。

4.3.2 ID路由

配置TLP和完成TLP使用ID路由。ID由三部分组成:Bus Number(8位)、Device Number(5位)、Function Number(3位),合起来就是16位的Requester ID或Completer ID。

交换机根据Bus Number决定转发方向。每个桥/交换机都有一个Primary Bus Number和Secondary Bus Number范围。如果目标Bus在范围内,就往下游转发;否则往上游转发。

关键点:ID路由依赖枚举阶段建立的Bus号分配。如果枚举出错,后续所有配置TLP都会迷路。我见过一个案例,因为交换机的Secondary Bus Number寄存器配置错误,导致下游设备永远无法被访问。查了三天才找到原因。

4.3.3 隐式路由

消息TLP使用隐式路由。什么意思?就是TLP头标里不直接写目标地址或ID,而是通过消息编码来隐含目标。比如PME_Turn_Off消息是发给所有设备的,ERR_COR消息是发给RC的。

交换机收到消息TLP后,根据消息编码决定:是广播到所有下游端口,还是只往上游转发,还是终结在自己这里。这种方式减少了头标开销,但增加了交换机的处理逻辑。

4.4 小结与调试建议

好了,这一章的内容就这些。总结一下:

  • TLP格式:头标(3/4 DW)+ 数据(可选)+ ECRC(可选)
  • 四种类型:存储器、IO、配置、消息,各有各的用途和规则
  • 三种路由:地址路由(存储器/IO)、ID路由(配置/完成)、隐式路由(消息)

调试小技巧:当你用逻辑分析仪或PCIe协议分析仪抓包时,先看TLP头标的Fmt和Type字段,确认事务类型是否正确。再看Length和Address,确认数据范围。最后看Completion Status(如果是完成包),确认操作是否成功。三步走,大部分问题都能定位。

下一章我们会讲数据链路层协议(DLLP),那是TLP能可靠传输的保障。到时候我会分享一个关于ACK/NAK超时导致链路降速的实战案例,很有意思。