# PCIe 协议基础:体系结构、分层与事务类型

协议分析仪抓包与Config Space解读教程 - 第3节

3.1 PCIe 为什么需要分层?

说实话,我第一次接触PCIe时,也被它的三层架构搞懵过。

为什么要分三层?直接像传统总线那样不好吗?

后来在实际项目中抓包分析,我才明白——分层设计是为了解耦。每一层只管自己的事,上层不用操心底层怎么传,底层也不用关心上层传的是什么。这对高速串行总线来说,太重要了。

打个比方:

  • 事务层 = 快递公司接单员,只管包裹是什么、送到哪
  • 数据链路层 = 分拣中心,保证包裹不丢、不坏、不乱序
  • 物理层 = 运输车队,管车辆、道路、信号

你从协议分析仪上看到的TLP(事务层包),实际是经过三层封装后才变成线上比特流的。抓包时要是搞不清哪一层在处理什么,很容易被乱花渐欲迷人眼。

3.2 三层架构总览

这张图帮你建立整体认知。建议先看一遍,再往下细读每一层。

牢记:软件配置空间和事务层直接交互,而数据链路层和物理层对软件基本透明。你在抓包时,事务层包是你最常打交道的东西。

PCIe 三层架构与数据流向 PCIe 三层架构与数据封装流程 事务层 (Transaction Layer) 核心职责:生成/解析 TLP,管理事务顺序与流控 数据链路层 (Data Link Layer) 核心职责:添加序列号、CRC校验、ACK/NAK重传机制 物理层 (Physical Layer) 核心职责:串行/解串、8b/10b或128b/130b编码、电气特性 封装/解封装 添加/去除帧 数据 流向 设备→核 核→设备 协议分析仪抓包视角:看到的是物理层上的原始比特流,但解析时需逐层剥离封装,还原出事务层TLP

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

怎么认?

  1. Fmt=00, Type=00000 → 3DW头部,Memory 读(MRd)
  2. Length=1 → 请求1个DW = 4字节
  3. 没有数据负载,因为是读请求
  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,就能快速定位问题。