协议分析仪抓包与Config Space解读教程 - 第3节
3.1 PCIe 为什么需要分层?
说实话,我第一次接触PCIe时,也被它的三层架构搞懵过。
为什么要分三层?直接像传统总线那样不好吗?
后来在实际项目中抓包分析,我才明白——分层设计是为了解耦。每一层只管自己的事,上层不用操心底层怎么传,底层也不用关心上层传的是什么。这对高速串行总线来说,太重要了。
打个比方:
- 事务层 = 快递公司接单员,只管包裹是什么、送到哪
- 数据链路层 = 分拣中心,保证包裹不丢、不坏、不乱序
- 物理层 = 运输车队,管车辆、道路、信号
你从协议分析仪上看到的TLP(事务层包),实际是经过三层封装后才变成线上比特流的。抓包时要是搞不清哪一层在处理什么,很容易被乱花渐欲迷人眼。
3.2 三层架构总览
这张图帮你建立整体认知。建议先看一遍,再往下细读每一层。
牢记:软件配置空间和事务层直接交互,而数据链路层和物理层对软件基本透明。你在抓包时,事务层包是你最常打交道的东西。
3.3 事务层——TLP 的诞生地
3.3.1 它在做什么?
事务层是三层中离软件最近的一层。说白了,CPU或驱动发一条读写指令,最终变成TLP,就是事务层干的事。
我个人习惯把事务层理解为协议翻译官:
- 收到软件发来的内存读写请求 → 打包成Memory Read/Write TLP
- 收到IO配置请求 → 打包成IO或Configuration TLP
- 接收来自对端的TLP → 拆包,把数据或状态交给上层
3.3.2 事务层包(TLP)的骨架
每个TLP都有固定的大致结构,我在项目里看了无数次抓包,总结下来就这四块:
| 字段 | 位置 | 说明 | 我踩过的坑 |
|---|---|---|---|
| TLP前缀 | 头部最前 | 可选字段,常用于扩展地址或消息类型 | 早期设备不支持前缀,会被直接丢弃 |
| TLP头部 | 固定位置 | 包含Fmt(格式)、Type(类型)、长度、地址/ID、Tag等 | 解析时一定要看Fmt字段,它决定头部是3DW还是4DW |
| 数据负载 | 头部之后 | 可选,写入操作时才有,读取完成时由返回方带上 | 长度字段的单位是DW(4字节),不是字节! |
| ECRC | 尾部 | 端到端CRC校验,可选字段 | 很多消费级设备不收ECRC,抓到也不报错 |
小技巧:抓包时看到DWord长度字段为0x002,实际数据是8字节?不对!长度0x002表示2个DW,即8字节。千万别拿它当字节数直接用。
3.3.3 事务类型有哪几种?
我在协议分析仪上最常打交道的事务类型,归纳起来四个:
| 事务类型 | 范畴 | 典型场景 | 注意点 |
|---|---|---|---|
| Memory (MRd/MWr) | 内存空间 | DMA传输、寄存器访问 | 支持32/64位地址,最常用 |
| I/O (IOR/IOW) | I/O空间 | 传统设备兼容 | PCIe原生不推荐,可被禁用 |
| Configuration (CfgRd/CfgWr) | 配置空间 | 枚举设备、配置BAR | 必须响应,否则设备无法被识别 |
| Message (Msg) | 消息空间 | 中断(MSI/MSI-X)、错误报告、电源管理 | 没有地址,靠消息编码区分 |
为什么要把I/O列出来? 有一次我在调试一个老旧的串口卡时,发现它还在用I/O读写。现在的CPU已经逐步弃用I/O空间了,但遗老设备还是有的。抓包时看到IOR/IOW别慌,确认它是不是必须的就行。
3.4 数据链路层——可靠传输的守护者
3.4.1 这层加了什么料?
事务层把TLP打包好以后,交给数据链路层。这层不关心TLP里面的内容,它只管两件事:
- 加序列号:每个TLP头部前面加一个12位的Sequence Number
- 加CRC:尾部加一个LCRC(链路层CRC)
这还没完,接收方收到后要校验CRC和序列号,对了就回ACK,错了就回NAK。NAK要求发送方重传。
我曾经踩过的大坑:协议分析仪在Capture模式下,如果你只看TLP列表,是看不到序列号和LCRC的——它们已经在数据链路层被剥掉了。要看到完整的DLLP,你需要在分析仪上开启"链路层解析"或"原始包"模式,否则你永远不知道这层在忙什么。
3.4.2 数据链路层包(DLLP)长啥样?
DLLP是链路层自己产生和消费的包,不像TLP那样透传到对端的事务层。它有几种类型:
| DLLP类型 | 用途 | 方向 |
|---|---|---|
| ACK/NAK | 确认或请求重传TLP | 双向 |
| InitFC1/InitFC2 | 初始化流控信用 | 双向 |
| UpdateFC | 更新流控信用 | 双向 |
| PM_* | 电源管理相关 | 双向 |
你看,数据链路层确实挺忙的。不过对于大部分调试场景,我们只需要知道它在背后默默保证数据可靠就够了。
3.5 物理层——电气上的"硬汉"
3.5.1 物理层又分两层
没错,物理层自己还分两层,这在标准里叫电气子层和逻辑子层。
- 电气子层:管差分信号、阻抗匹配、驱动器电流、接收灵敏度。这些在示波器上能看到,但在协议分析仪里基本不给你看。
- 逻辑子层:管编码(8b/10b或128b/130b)、加扰、解扰、训练状态机、链路训练。这部分在抓包时偶尔能瞥见——比如Link Training的Ordered Set。
3.5.2 链路训练状态机(LTSSM)
我记得在调试一块FPGA与CPU互联时,链路一直卡在Recovery状态不动。没招,只好把分析仪挂在PCIe插槽上,盯着物理层的Ordered Set看。原来是FPGA端的电气子层没有完成均衡,导致链路没法进入L0。
物理层状态机有十几个状态,但实际调试中你主要看这几个:
| 状态 | 含义 | 正常停留时间 |
|---|---|---|
| Detect | 检测对端是否存在 | 毫秒级 |
| Polling | 波特率协商 | 毫秒级 |
| Configuration | 协商链路宽度、极性 | 毫秒级 |
| L0 | 正常工作 | 绝大多数时间 |
| Recovery | 尝试重新训练 | 几百微秒~几秒 |
| L1/L2 | 低功耗 | 按需 |
抓包小技巧:如果你看到链路上长时间没有TLP传输,但有一堆Ordered Set(如TS1/TS2),说明链路处于Recovery或Configuration状态。这是不正常的。正常工作状态下Training Sequences只在刚上电或复位后出现。
3.6 事务类型详解——不只是读和写
3.6.1 Non-Posted 与 Posted 的区别
这是很多人刚开始容易混淆的概念。说白了就是:
- Posted:发出去不用等回应。比如Memory Write,写完本地就认为完成了,不要求对方回Completion。
- Non-Posted:发出去必须等对方回应。比如Memory Read、I/O Read/Write、Configuration Read/Write,必须收到Completion才算完。
为什么要有这个区分?性能。写操作如果每次都要等ACK,吞吐量就上不去。PCIe允许连续发多个Memory Write,对方只异步回ACK就行。
抓包时你要是看到一连串MWr后面没有Completion,完全正常。但看到MRd后面没跟着CplD(带数据的Completion),那就要小心了——可能事务被卡住了。
3.6.2 四种事务类型的抓包特征
我列一个对照表,方便你在分析仪上快速识别:
| 事务类型 | Fmt[1:0] | Type[4:0] | 是否带数据 | 是否有Completion |
|---|---|---|---|---|
| MRd (Memory Read) | 00 或 01 | 0 0000 | 否 | 是 (CplD) |
| MWr (Memory Write) | 10 或 11 | 0 0000 | 是 | 否 (Posted) |
| IOR/IOW | 00/10 | 0 0010 | 读:否 / 写:是 | 读:是 / 写:是 |
| CfgRd0/CfgWr0 | 00/10 | 0 0100 | 读:否 / 写:是 | 读:是 / 写:是 |
| Msg (Message) | 01 或 11 | 1 0111 等 | 有时带 | 否 (Posted) |
提醒:Type字段一共5位,但前两位在Fmt中已经用掉了一部分。实际解析时你可以看Fmt + Type的组合。分析仪上一般直接显示"MRd 64bit"或"Msg (INTx)"这种易读形式,你手动查看原始头时再看这几位即可。
3.6.3 Completion事务的特殊性
Completion是Non-Posted事务的"回执"。它有两种:
- Cpl(不带数据):用于I/O写或Configuration写完成后的确认
- CplD(带数据):用于Memory读、I/O读、Configuration读的返回
每个Completion都带着原来的Requester ID和Tag,这样发出方才知道这个Completion是对应哪个请求的。
我曾在项目中遇到过一个Bug:某SSD在读DMA时,Completion的Tag和Request总对不上号。原来它的内部队列满了,后续请求的Tag被错误覆盖。抓包一看,CplD里Request ID是对的,但Tag不对。这就是典型的标不配位。
3.7 抓包实操:从TLP头部识别事务类型
说了这么多理论知识,我们来个实际的。假设你从分析仪上抓到一个TLP头部的第一组字节(前4字节),看到:
0x00 0x00 0x00 0x01 → Fmt=00, Type=00000, TC=000, TD=0, EP=0, Attr=00, AT=00, Length=000000001
怎么认?
- Fmt=00, Type=00000 → 3DW头部,Memory 读(MRd)
- Length=1 → 请求1个DW = 4字节
- 没有数据负载,因为是读请求
- 后面会跟着一个CplD回来,带这4字节的数据
就这么简单。多抓几包,多看几眼,你也能一眼认出类型来。
重要提醒:不是所有TLP头部都等长。3DW头部和4DW头部在Fmt字段里决定。4DW头部用于64位地址的Memory请求。如果你按3DW去解析4DW的报文,地址会完全错误,读出来的就是垃圾数据。
3.8 本章小结
PCIe三层架构是理解一切抓包数据的基础:
- 事务层生成TLP,定义"做什么"
- 数据链路层给TLP加序列号和CRC,保证"不丢、不坏"
- 物理层把数据变成差分信号发出去,负责"怎么传"
事务类型分为Memory、I/O、Configuration、Message四大类,读/写、Posted/Non-Posted的区别决定了你要不要等Completion。抓包时看清Fmt和Type,就能快速定位问题。