3. PCIe协议基础:体系结构与分层
PCIe这个协议,说起来复杂,但核心思想其实很纯粹——把并行的PCI总线改成串行差分对,再用分层的方法把复杂任务拆解。我做高速接口这么多年,最深的一个体会就是:不理解分层,你就永远搞不懂PCIe。
为什么要分层?说白了,就是为了解耦。每一层只管自己的事,上层不用管下层怎么传,下层不用管上层的数据含义。这种设计思路,在工程上非常优雅。
3.1 三层架构概览
PCIe协议栈从上到下分为三层:
| 层次名称 | 英文缩写 | 核心职责 |
|---|---|---|
| 事务层 | TL | 生成/解析事务层包(TLP)、处理事务排序、流量控制 |
| 数据链路层 | DLL | 数据校验(CRC)、ACK/NAK确认、重传机制 |
| 物理层 | PHY | 串行/反串行、编码(8b/10b或128b/130b)、电气特性 |
我个人习惯把这三层的关系理解成一个“洋葱”。最外层是事务层,直接面对软件和驱动;最内层是物理层,面对的是铜线或者光纤;中间的数据链路层,负责让上下两层之间的通信不出错。
关键认知:每一层都有自己独立的协议数据单元。事务层叫TLP,数据链路层叫DLLP,物理层叫PLP。数据从上层往下传递时,每一层都会加上自己的头部和校验信息。
3.2 事务层详解
事务层是离软件最近的一层。它的主要产出就是TLP(Transaction Layer Packet)。你可以把TLP想象成一个快递包裹,头部写着从哪里来到哪里去,中间是货物(数据),尾部还有校验信息。
TLP有几种常见类型:
- Memory Read/Write:内存读写,这是最常用的操作
- I/O Read/Write:对I/O空间的访问,现在用得越来越少
- Configuration Read/Write:配置空间访问,枚举阶段不可或缺
- Message:消息类TLP,比如中断、错误通知等
小技巧:调试PCIe的时候,用逻辑分析仪抓TLP是最快的定位手段。我一般会先看TLP的Fmt和Type字段,这两个字段告诉你包类型和是否有数据负载。只要这两个是对的,后面的解析基本不会错。
事务层还有一个重要概念——流量控制(Flow Control)。别把这个和物理层的链路训练搞混。事务层的流量控制,是为了防止发送方过快发送导致接收方缓存溢出。双方通过交换FC Credits(信用量)来管理缓冲区。
我刚开始调PCIe的时候,就遇到过一个诡异的丢包问题。每次读写压力一大,就偶尔丢一个包。查了三天,最后发现是事务层的VC(Virtual Channel)缓冲区配置太小,导致Credit耗尽。你想想看,软件以为发送成功了,实际上包在事务层就被丢掉了。
3.3 数据链路层
数据链路层在事务层下面。它要做的事很纯粹:保证事务层发下来的TLP能完整、不丢失地送到对端。
- CRC校验:对TLP附加32位CRC(LCRC),检测传输错误
- ACK/NAK协议:接收方收到TLP后,返回ACK表示确认,NAK表示拒收
- 重传机制:发送方收到NAK或超时未收到ACK,就重发TLP
- DLLP:数据链路层包,用于ACK/NAK、PM(电源管理)等
这里有个细节:DLLP是数据链路层的本地包,不会向上传给事务层。ACK/NAK就是通过DLLP传递的。我曾经犯过一个错,在分析TLP序列的时候,把DLLP也当成TLP来解析,结果数据完全对不上。后来才意识到,DLLP和TLP在数据链路层是“分道扬镳”的。
注意:数据链路层的重传机制,保证了端到端的可靠传输。但是,它只能保证“传输不出错”,不能保证“数据一定合法”。如果发送方生成的就是错误数据,数据链路层也无能为力。这个边界要想清楚。
3.4 物理层
物理层离硬件最近,也是我在实际调优中花时间最多的地方。物理层又分为两个子层:
| 子层 | 主要功能 |
|---|---|
| 逻辑子层 | 编码/解码(8b/10b或128b/130b)、加扰/解扰、弹性缓冲 |
| 电气子层 | 差分信号的驱动/接收、阻抗匹配、去加重/预加重、PLL |
物理层的第一个任务是链路训练(Link Training)。上电后,两端的PHY通过TS1/TS2序列相互协商,确定链路宽度和速度。这个过程类似两个人握手——你先说“我能跑Gen3”,对方回“我也能跑Gen3,但只能x4”,最后双方商定一个彼此都能接受的配置。
我记得刚转行做PCIe的时候,遇到一个链路只能协商到Gen1的问题。当时百思不得其解,眼图看起来也不差。后来用示波器仔细量了一下差分对的摆幅,发现发送端电压偏低了100mV。就是这个0.1V的差距,让链路训练一直降速。嗯,物理层的阈值就是这么挑剔。
3.5 分层间的关系
三个层之间的协作,可以用一段伪代码来描述发送一个Memory Write TLP的过程:
// 发送侧
事务层:收到软件写请求 → 构建MemWr TLP → 添加TLP头部
数据链路层:接收TLP → 附加序列号 → 计算并附加LCRC → 封装成DLLP
物理层:接收DLLP → 8b/10b编码 → 加扰 → 串行化 → 差分发送
// 接收侧
物理层:差分接收 → CDR恢复时钟 → 弹性缓冲 → 8b/10b解码 → 传输给DLL
数据链路层:检查LCRC → 验证序列号 → 发送ACK → 去除DLLP包装 → 传递TLP给TL
事务层:解析TLP头部 → 将数据写入内存 → 更新事务状态
你看到没有,每一层都在做“加壳”和“拆壳”的事。接收侧的层层剥离,跟发送侧的层层封装是对称的。这种对等通信(Peer-to-Peer Communication)的模型,让不同厂商的PCIe IP核可以互相通信。只要大家遵循同样的分层规范就行。
实战建议:当你调试PCIe问题的时候,按照分层的思路去排查,效率会高很多。发现包丢了?先查物理层链路是否稳定(看Link Status寄存器)。再查数据链路层CRC是否有错误计数。最后才查事务层的流量控制和路由。从下往上查,是我这些年总结出的经验。
3.6 避坑:常见误解整理
这里我把刚入行时踩过的坑总结一下,供大家参考:
- 认为TLP可以无限大:TLP的最大Payload长度是有限制的,由设备能力决定。超过Max Payload Size的TLP会被拆分。
- 忽视Ordering规则:事务层有一套严格的排序规则,比如同ID写操作的顺序不能乱。我曾经跳过这个规则去优化性能,结果导致数据一致性崩溃。
- 混淆链路训练和链路调优:训练是建链,调优(Equalization)是建链后优化信号质量。Gen3以上有独立的Equalization阶段,很多人搞混了。
- 把数据链路层的重传当成万能药:重传次数多了,会导致性能断崖式下降。根本方法还是保证物理层的信号质量。
一句话总结分层思维:碰到PCIe问题,先问自己“这是哪一层的问题?”物理层看眼图,数据链路层看CRC计数,事务层看TLP流。分对了层,问题就解决了一半。