TLP 格式精讲:头字段逐字节判读
PCIe 协议特性精讲 · 第 03 篇 | 基准:PCIe 5.0(Base Spec r5.0)| 内核:Linux v7.0.0
系列主线:39 篇,一篇一个协议特性——是什么、为什么、怎么工作、怎么测、坑在哪
一、TLP 是什么
TLP(Transaction Layer Packet,事务层报文)是 PCIe 事务层的最小数据单元,读、写、配置、中断消息全装进 TLP 走(Base Spec r5.0)。一条 TLP 由头(Header)、可选载荷(Data Payload)、可选 Digest 组成。头固定 3 或 4 个 DW(12/16 字节);载荷按 DW 整数倍;Digest 只有 1 个 DW,是端到端 ECRC 校验,头里 TD 位为 1 才带。
02 篇讲过,事务层把软件要做的读写翻译成 TLP,往下交给数据链路层加序列号和 LCRC,再进物理层编码上链路。这一篇不往上追,就盯住这 12/16 字节的头。抓包第一眼就是 Byte0:Fmt 和 Type 决定这条包什么类型、头多长、有没有数据,全挤在一个字节里。硬件解包能做流水线,靠的就是这种自描述设计——接收端不用等整包收完,读 Byte0 就知道包边界在哪。
顺带说清一个边界,抓包两类都看得到,别拿 TLP 的字段去套 DLLP:
- TLP 端到端:经过每个 Switch 都被解析转发
- DLLP 只在相邻两个端口间交换:不跨链路
这篇的目标很实在:拿一条抓包记录,能从头拆到尾,类型、路由、长度、Tag,一眼定。
二、头字段逐字节:Byte0 到 Byte15
先看 3DW 头的完整布局(4DW 就是在后面多 4 字节存高 32 位地址,其余字段位置不变):

(数据出处:Base Spec r5.0)
Byte0:Fmt[2:0] + Type[4:0],一条包的身份证。
高 3 位 Fmt(Format)管格式,低 5 位 Type 管类型:
| Fmt[2:0] | 含义 | 典型用途 |
|---|---|---|
| 000 | 3 DW 头,无数据 | MRd、IORd、CfgRd、Cpl |
| 001 | 4 DW 头,无数据 | 64 位地址 MRd、Msg |
| 010 | 3 DW 头,有数据 | MWr、IOWr、CfgWr、CplD |
| 011 | 4 DW 头,有数据 | 64 位地址 MWr、MsgD |
| 100 | TLP Prefix | 前缀包,日常抓包基本见不到 |
Fmt 低两位有规律:bit1 = 有没有数据,bit0 = 3DW 还是 4DW。没定义的编码是 Reserved。Fmt=100 是 TLP Prefix,挂在头前面,日常见不到,常见 TPH 提示和 PASID 前缀——PASID 是 ATS 共享虚拟内存用,TPH 是缓存提示,两者不绑定,先知道有这东西就行。
Type 编码全表在 Spec 里二十多行,抓包常用的就这几个:
| 事务 | Fmt | Type | 说明 |
|---|---|---|---|
| MRd / MWr | 000/010 或 001/011 | 0 0000 | 内存读/写,Type 相同,靠 Fmt 区分 |
| IORd / IOWr | 000 / 010 | 0 0010 | I/O 读/写 |
| CfgRd0 / CfgWr0 | 000 / 010 | 0 0100 | 配置读写,Type0(访问目标自身) |
| CfgRd1 / CfgWr1 | 000 / 010 | 0 0101 | 配置读写,Type1(经 Switch 转发) |
| Cpl / CplD | 000 / 010 | 0 1010 | 完成报文,靠 Fmt 区分带不带数据 |
| Msg / MsgD | 001 / 011 | 10rrr | 消息,低 3 位 rrr 是路由方式 |
| FetchAdd / Swap / CAS | 010 / 011 | 0 1100 / 0 1101 / 0 1110 | 原子操作,带操作数载荷 |
Byte1:T9、TC、T8、Attr[2]、LN、TH。
从高到低 7 个位:
- bit7 = T9:10-bit Tag 的最高位
- bits 6:4 = TC[2:0]:流量类别,8 个 TC,TC0 默认必支持
- bit3 = T8:10-bit Tag 次高位
- bit2 = Attr[2]:ID-Based Ordering(IDO)
- bit1 = LN:Lightweight Notification 指示位
- bit0 = TH:TLP Processing Hints 指示位
日常抓包 Byte1 基本是 0x00(TC0、无属性),10-bit Tag 没使能时 T9/T8 必须为 0。TC 决定这条包走哪个虚拟通道(VC),QoS 才用得到。注意 Attr 三个位不连续:Attr[2] 在 Byte1,Attr[1:0] 在 Byte2,IDO 是后加进 ECN 的,解头别按连续位读。
Byte2/Byte3:TD、EP、Attr[1:0]、AT、Length。
Byte2 从高到低:
- bit7 = TD:TLP Digest 指示,1 表示包尾带 1 个 DW 的 ECRC
- bit6 = EP:毒化位(Error Poisoned),1 表示载荷被判损坏(头错误绝不毒化转发,EP 只对带载荷的 TLP 有意义)
- bits 5:4 = Attr[1:0]:bit5 是 Relaxed Ordering(RO)、bit4 是 No Snoop(NS)
- bits 3:2 = AT[1:0]:地址类型,非 ATS 场景全 0
- bits 1:0 = Length[9:8]:长度高 2 位
Byte3 整字节是 Length[7:0],和 Byte2 低 2 位拼成 10 位 Length,单位是 DW。Length 只算载荷不算 Digest;全 0 编码表示 1024 DW,不是 0,也就是说单包载荷最大 4 KB,正好顶到 MPS 的上限。写长度超 MPS 属于畸形 TLP,第四节展开。MRRS 是请求者发起时的约束,超了是配置问题,接收端不判 Malformed。
Byte4-7:Requester ID、Tag、字节使能。
Byte4-5 = Requester ID,16 位 = Bus[7:0] + Device[4:0] + Function[2:0],lspci 里的 01:00.0 就是它。Bus 号枚举时软件分配,Dev 号看桥下物理位置,Func 号对应多功能设备各 Function;同一块卡多 Function 共用一条链路,谁发的请求靠 Requester ID 区分。Byte6 = Tag。Byte7 高 4 位 Last DW BE、低 4 位 First DW BE(BE = 字节使能)。
Tag 是完成报文回家的钥匙:未完成的 Non-Posted 请求 Tag 必须唯一,Completer 回 Cpl 带同样的 Requester ID + Tag,请求者才能对上号。默认只用低 5 位(32 个 outstanding),Extended Tag 使能后 8 位(256),10-bit Tag 到 768,这三档就是第六节那两个开关。
BE 规则也在这:
- 1 DW 的读:Last DW BE 必须为 0000、First DW BE 必须非 0
- 多 DW 的读:First/Last BE 要连续使能
BE 的存在是为了支持部分字节访问:一个 DW 里只有某几个字节有效,就由 BE 指明;读回的数据里没使能的字节,值未定义。驱动做非对齐访问时这里最容易出幺蛾子,抓包看到奇怪的 BE,先查驱动。
Byte8-11(3DW)或 Byte8-15(4DW):目标地址。
3DW 头存 32 位地址(Address[31:2],低 2 位隐含为 0,DW 对齐);4DW 头 Byte8-11 存高 32 位、Byte12-15 存低 32 位。规则一句话:地址 <4 GB 必须用 3DW,≥4 GB 用 4DW;<4 GB 却发 4DW,接收方行为未定义(Spec 原文:behavior of the Receiver is not specified)。低地址统一走 3DW,别图省事。
三、四类请求 + Completion:头里差在哪
四类事务(Memory、I/O、Configuration、Message,Base Spec r5.0 §2.1.1),再加上对应的完成报文。前 3 个 DW 的结构都一样,差异在 DW2(字节 8-11)和有没有载荷:
| 事务 | 头 | DW2 内容 | 路由 | 有 Cpl? |
|---|---|---|---|---|
| MRd / MWr | 3DW/4DW | 32/64 位地址 | 地址路由 | 读有、写无 |
| IORd / IOWr | 3DW | 32 位地址 | 地址路由 | 读有、写无 |
| CfgRd/Wr0 | 3DW | 目标 Bus/Dev/Func(Byte8-9)+ 寄存器号(Byte10-11) | ID 路由 | 有 |
| CfgRd/Wr1 | 3DW | Byte8-9 保留,目标总线号 + Dev/Func 在 Byte10-11 | ID 路由 | 有 |
| Msg / MsgD | 4DW | 按 rrr 放路由子字段或地址/ID | 隐式/地址/ID | 无(Posted) |
| Cpl / CplD | 3DW | Requester ID + Tag + Lower Address | ID 路由 | — |
几个容易记混的点:
配置请求的 Type0/Type1 区别就在 DW2 的 BDF 位置。Type0 直接给目标 Bus/Dev/Func 加寄存器号,访问挂在桥下第一层的设备;Type1 是发给下游桥的,桥判断目标在不在自己下游,在就转成 Type0 再发。CfgRd0 的 BDF 在 Byte8-9 而不是 Byte4-5,这里抓包最容易看走眼:Byte4-5 永远是发起方自己的 Requester ID。
消息全部 4DW 头,Type 低 3 位 rrr 是路由方式(000 = 到 RC,011 = 广播下行,100 = 本地终止)。隐式路由不配地址不配 ID,靠拓扑相对位置走,这是消息类特有的。最常见的是电源管理(PM_)、错误上报(ERR_)、LTR,发完即走,出错靠 AER 兜底。
完成报文(Cpl/CplD)的头是另一个结构:
- Byte4-5 = Completer ID(谁回的)
- Byte6 = Status[2:0] + BCM + Byte Count 高 4 位
- Byte7 = Byte Count 低 8 位
- Byte8-9 = Requester ID(回给谁)
- Byte10 = Tag
- Byte11 = Reserved + Lower Address[6:0](第一个使能字节的低位地址)
Status 四个值:
- SC:成功
- UR:不支持请求
- CRS:配置重试,只允许回给 CfgRd
- CA:被拒
BCM 是 PCI-X 遗留,PCIe 不许置它。Byte Count 全 0 编码 = 4096 字节,跟 Length 一个套路。
请求者收到 Cpl 先干一件事:拿 Requester ID + Tag 找对应的未完成请求,找不到就是 Unexpected Completion,多半是 Tag 冲突或对端实现错误。Status 决定后续:SC 收数据,UR 记 Unsupported Request 错误,CRS 是配置重试,软件过会儿重读就行,CA 表示请求被拒。
这里能看出设计逻辑:请求头带"我是谁 + 去哪 + 第几个",完成头带"谁回的 + 回给谁 + 回了多少 + 从哪个字节开始"。请求和完成完全解耦,隔多少个 Switch 都无所谓,靠 ID 路由回家。
四、字段自洽检查:Malformed 五条
接收端收到 TLP 不是拿来就用,头字段先过自洽检查,一票否决。下面五条,违反任何一条,这条包就是畸形 TLP(Malformed TLP),按规范处理:不得转发、不得响应,能报 AER 就报:
- TD=1 但包尾没有 1 个 DW 的 Digest;
- Length 和实际载荷的 DW 数对不上,多了少了都算;
- 长度超限:MWr 载荷超 MPS(MRRS 超限不在此列,是请求者配置问题);
- 地址路由的 TLP 跨了 4 KB 边界(可选检查项,多数实现开着);
- 字节使能非法:1 DW 读 Last DW BE 非 0、First DW BE 全 0、多 DW 读 BE 不连续。
这五条看着琐碎,排障里 Malformed 最容易误判:对端收到畸形包直接丢,什么都不回,发送方等不到 Cpl 就报 CTO。你看到的现象是"超时",根因却是自己发出去的包本身就是畸形的。这坑我见得多了,先查自己包的 Length 和 TD,再怀疑对端。
Malformed 的判定分级:头自相矛盾(前三条)接收端必须处理;4 KB 边界和 BE 连续性是可选检查。验证时先确认被测设备开了哪些可选检查,否则注入的违例包可能被放行,别把实现缺陷误判成自己的问题。上报走 AER,Malformed 属 Uncorrectable 错误,默认 Fatal。
另有两条同源的经典故障,不属于 Malformed:Tag 不唯一会让两个请求的完成报文撞车,收到的是别人的 Cpl,触发 Unexpected Completion;EP=1 的毒化包,接收方丢载荷但事务照常完成。抓包看到 EP=1,往上游数据源查,别在链路上找,链路错走 LCRC 重放,轮不到 EP。
五、版本差异:6.4 把头改了,7.0 没改
本系列基准钉在 5.0(当前产品)。6.0 起引入 Flit Mode,TLP 头是实打实的改动(Base r6.4):
| 维度 | r5.0(基准) | r6.4 Flit Mode | 验证含义 |
|---|---|---|---|
| 头格式 | NFM 3/4 DW | Header Base 1-7 DW + OHC 0-7 DW + Trailer | 抓包工具要支持 FM 帧解析 |
| 类型编码 | Fmt[2:0] + Type[4:0] 合并判 | Type[7:0] 8 位单一编码 | 解码库、过滤规则全换 |
| Tag | 8b(ExtTag 到 256)/ 10b 可选 | 架构化 14/10/8/5b 四档,UIO 固定 14b | 新增 14-Bit Tag 使能位 |
| LN 位 | Byte1 bit1 = LN | 弃用,变 Reserved | 依赖 LN 的固件要重设计 |
| 新增 | — | NOP TLP、DMWr;NFM↔FM 要转换 | 转换本身是测试点 |
对验证来说,6.4 最要命的是 NFM/FM 转换:6.0 链路只跑 FM,和 5.0 设备混接要做转换,转换点、丢不丢字段都是测试点。抓包工具解码库要跟着升,拿 5.0 的库解 FM 帧全是垃圾。
7.0 在 TLP 头格式上和 6.4 没有功能变化(Base r7.0 Revision History 原话:7.0 = 6.4 + 128 GT/s + 编辑性修订)。所以 5.0 到 7.0 的抓包升级清单就是 6.4 那一列,不叠加新项。反方向看 3.0:没有 10-bit Tag(4.0 才引入)、没有 LN 位(3.1 的 ECN)、TLP Prefix 只是 ECN 雏形(没有 PASID 前缀)、Locked 事务还在。拿 Gen3 老设备的抓包记录,别按 5.0 的字段表硬套。
六、Capability 锚点:Tag 宽度的两个开关
头字段每个都有对应的配置空间开关,其中 Tag 最值钱,直接决定 outstanding 上限,也就是读吞吐。outstanding 的意义在于延迟带宽积:一次读来回几百纳秒到微秒级,能并行挂多少个未完成的读,决定链路带宽能吃满多少。32 个 Tag 在高延迟链路上往往不够用,这是 Extended Tag 存在的意义;10-bit Tag 推到 768,给高并发场景准备。
PCI Express 能力结构(Capability ID = 10h)里两个寄存器管这事:
Device Control(Offset 08h):bit8 = Extended Tag Field Enable。配套 Device Capabilities(Offset 04h)bit5 = Extended Tag Field Supported,先看支持再看使能,顺序不能反。这个寄存器还管着约束 Length 的两个值:bits 7:5 = MPS(默认 000b = 128 字节),bits 14:12 = MRRS(默认 010b = 512 字节)。
Device Control 2(Offset 28h):bit12 = 10-Bit Tag Requester Enable(默认 0)。使能后 Tag 用满 10 位,outstanding 上限到 768。前提是链路上可能收到你请求的设备都得支持 10-Bit Tag Completer,Spec 要求支持 16 GT/s 及以上的功能必须实现它,所以 Gen5 设备基本都能接;真遇到不支持的对端,10-bit Tag 请求会撞出 Unexpected Completion。
lspci 里直接可见:
DevCtl: CorrErr+ NonFatalErr+ FatalErr+ UnsupReq+ RlxdOrd+ ExtTag+ NoSnoop+
MaxPayload 256 bytes, MaxReadReq 512 bytes
10BitTag+ ← DevCtl2 使能后显示
ExtTag+ 就是 Extended Tag 已使能,10BitTag+ 是 10-bit Tag 已使能,MaxPayload / MaxReadReq 是 MPS / MRRS 的当前值。抓包看到 Length 超这两个数,回来先对 DevCtl 这一行。多数是软件配置问题,先别怀疑设备实现。
七、三表:故障、测试、判断
故障模式(头字段相关的五类典型故障):
| 症状 | 可能根因 | 可观测证据 |
|---|---|---|
| MRd 超时(CTO) | 目标未收到(路由错)/ Completer 无响应 / Cpl 丢失 | 抓包只见 MRd 无 Cpl;dmesg completion timeout |
| Cpl 状态 = UR | 地址未命中 BAR / 请求类型不支持 | 抓包 Cpl UR;dmesg Unsupported Request |
| Unexpected Completion | Tag 冲突 / Completer 重复回包 / 10b Tag 发给不支持端 | 抓包 Cpl 无对应请求;AER Unexpected Completion |
| Malformed TLP 丢包 | Length 不符 / TD=1 无 Digest / 超 MPS / 跨 4KB / BE 非法 | 对端无任何反应;发送方 CTO;AER Malformed |
| EP=1(毒化) | 上游数据损坏被转发 / ECRC 失败 | 抓包 EP 位 = 1;dmesg data link protocol |
测试方法(三招够用):
| 手段 | 命令/工具 | 预期 |
|---|---|---|
| 分析仪抓包判读 | 协议分析仪 + setpci 读写配置 | 抓包可见 CfgRd0 → CplD(SC),头字段逐字节可判 |
| 构造异常头注入 | 抓包工具改 Length > MPS / TD=1 无 Digest 后注入 | 接收端报 Malformed TLP,验证一票否决生效 |
| BAR 内外读写对比 | devmem2 读写 BAR 内外地址 | BAR 内 → CplD;BAR 外 → Cpl(UR),验证地址解码 |
判断依据(抓包判读四把尺子):
| 判据 | 期望值/特征 | 反例特征 |
|---|---|---|
| Fmt/Type 编码 | Fmt 000/001/010/011/100 合法;Type 查编码表 | 其他值 = Reserved,接收端按 Reserved 处理 |
| 地址与头长匹配 | <4GB 用 3DW;≥4GB 用 4DW | 低地址发 4DW,接收方行为未定义 |
| TD-EP-Length 自洽 | TD=1 必有 Digest;Length = 实际载荷 DW 数 | 任一不符 = Malformed |
| Tag 唯一性 | 未完成 Non-Posted 请求 Tag 一一对应 | Tag 重复 → Unexpected Completion / 数据错乱 |
八、观测命令:抓包第一课的实操
先说清楚一件事:PCIe 没有网卡那种"抓包口",tcpdump 抓的是网卡驱动经网络栈收发的报文,看不到总线上的 TLP。要看 TLP 只有两条路:协议分析仪(探针插进链路,能看也能改包),或者平台自己的总线调试手段。下面几条命令配合分析仪用:
1. 对 BDF,抓 Requester ID
lspci -s 01:00.0
输出 01:00.0 就是抓包里 Requester ID 的 Bus:Dev.Func 三段,Requester ID = 0100h 就是它发的。
2. setpci 触发 CfgRd,抓完整往返
setpci -s 01:00.0 0.w # 读 Vendor ID
setpci -s 01:00.0 0x10.l # 读 BAR0
分析仪挂在 Root Port 口上,这两条命令就是最干净的抓包入门样例:CfgRd0 → CplD(SC),一趟完整的 ID 路由往返。
3. 练手:一条 MRd 头从头拆到尾
抓一条 1 DW 的 MRd(驱动读 BAR0 里一个寄存器),头 12 字节:
00 00 00 01 01 00 10 0F 00 00 10 00
Byte0 = 0x00:Fmt = 000(3DW 无数据)+ Type = 00000(MRd),读请求。Byte1 = 0x00:TC0、无属性。Byte2 = 0x00、Byte3 = 0x01:Length = 1 DW。Byte4-5 = 0x0100:Requester ID = 01:00.0。Byte6 = 0x10:Tag = 16。Byte7 = 0x0F:Last DW BE = 0000(1 DW 读必须全 0)、First DW BE = 1111(4 字节全使能)。Byte8-11 = 0x00001000:地址 0x1000。谁、读哪、读多少、带什么属性,全出来了。如果抓到的 MRd 是 64 位地址(Fmt = 001),头 16 字节,Byte8-11 是高 32 位、Byte12-15 是低 32 位,其余字段位置不变,拆法一样。
等 CplD 回来再对一眼:Byte4-5 是 Completer ID,Byte6-7 是 Status / Byte Count(SC,4 字节),Byte8-9 回填 Requester ID 0100h,Byte10 回填 Tag 0x10。请求和完成严丝合缝对上,这条链路就是健康的。
分析仪一般直接显示解析好的字段名,但底层能力是这套字节布局。工具坏了、协议版本不对、或者抓的是 6.0 的 FM 帧,手拆就是兜底。
4. 对账 Tag 开关和 MPS/MRRS
lspci -vvv -s 01:00.0 | grep -E "DevCtl|MaxPayload|MaxReadReq"
ExtTag+ / 10BitTag+ / MaxPayload / MaxReadReq 一行对完。抓包里的 Length 和 Tag 宽度,都要能解释到这个寄存器层面,解释不了就是配置或实现有问题。
系列首发:CSDN「PCIe 协议特性精讲」| 基准 PCIe 5.0(Base Spec r5.0)| 内核 Linux v7.0.0
数字均以 Base Spec r5.0 为准(6.4/7.0 差异单列),错误欢迎评论区指出,勘误会更新在文末。

2638

被折叠的 条评论
为什么被折叠?



