1. PCIe 概述与架构基础
说到 PCIe,我最早接触是在做一块高速数据采集卡的时候。那时候总线还是 PCI 和 PCI-X 的天下,突然冒出来个 PCI Express,说实话第一反应是——又得重新学一套东西。但后来跑起来才发现,这家伙确实是脱胎换骨的改进。
1.1 为什么要有 PCIe?
老 PCI 总线是并行的,共享一条 32 位或 64 位的数据通道。所有设备挂在这条线上,谁想通信都得先申请仲裁。你想想看,当显卡、网卡、存储控制器都挤在一起的时候,争抢带宽的场景有多热闹。我曾在项目里测过一条满载的 PCI 总线,传输一个大文件时,鼠标都开始卡顿了。
PCIe 把并行改成串行,把共享改成点对点。每个设备跟 Root Complex 之间都是独立的链路,不需要仲裁。说白了,就是给每个设备修了一条专用高速公路,而不是大家共用一条乡间小道。
关键变化:
- 并行 → 串行差分信号
- 共享总线 → 点对点交换
- 仲裁 → 无仲裁,全双工
- 带宽可扩展(x1/x2/x4/x8/x16/x32)
1.2 拓扑结构
PCIe 的拓扑是一个树形结构,核心是 Root Complex(根复合体)。它连接 CPU、内存和 PCIe 交换网络。下面挂 Switch(交换器)和 Endpoint(端点设备)。
我记得第一次看 PCIe 拓扑图时觉得挺简单,但实际调试时发现 Switch 内部的虚拟通道和仲裁逻辑才是坑。我曾经遇到过一块 Switch 在多个 Endpoint 同时发起大流量时,优先级配置没做好,导致低优先级设备直接饿死。
1.3 分层架构
PCIe 协议栈分为三层:事务层、数据链路层、物理层。每层各司其职,有点像 OSI 模型,但更精简。
| 层次 | 主要职责 | 核心单元 |
|---|---|---|
| 事务层 | 生成/解析 TLP,处理读写请求、完成、消息 | 事务层包 (TLP) |
| 数据链路层 | 错误检测、重传、流量控制、序列号 | 数据链路层包 (DLLP) |
| 物理层 | 串行/解串、加扰/解扰、电气特性、链路训练 | 物理层包 (PLP) / 逻辑物理子层 |
我的经验: 刚开始学 PCIe 时,我老把 TLP 和 DLLP 搞混。记住一个技巧:TLP 是给上层软件看的,DLLP 是给相邻设备的数据链路层之间通信用的,软件根本碰不到它。
1.4 事务层详解
事务层是 PCIe 中最靠近软件的一层。它负责把 CPU 发出的内存读写、IO 读写、配置读写打包成 TLP,然后交给下层。嗯,这里有个关键概念——TLP 的格式。
一个典型的 Memory Read TLP 包含:
- 起始字节(Start Frame)
- TLP 头部(Header,3DW 或 4DW)
- 数据负载(Data Payload,可选)
- ECRC(端到端 CRC,可选)
- 结束字节(End Frame)
为什么要有 ECRC?我在一个项目中遇到过,数据经过 Switch 中转后,LCRC(链路层 CRC)被正确校验了,但 Switch 内部的 buffer 发生了位翻转导致错误数据被转发。ECRC 就是为了防止这种“端到端”的静默错误。
1.5 数据链路层
数据链路层插在事务层和物理层中间,主要做两件事:可靠性传输和流量控制。
可靠性靠的是 Ack/Nak 协议。发送方每发一个 TLP,接收方必须回一个 DLLP(Ack 或 Nak)。如果没收到 Ack,发送方会重传。我曾经在调试时遇到过大量 Nak 重传的情况,最后发现是物理层信号质量太差,导致 LCRC 频繁错误。
流量控制则用 Credit 机制。每个设备告诉对端自己有多少可用缓冲区空间。发数据前必须先确认对方有足够的 Credit,否则不能发送。这有点像你点外卖前先看看钱包里还有多少钱。
注意: Credit 机制并不会保证 TLP 顺序,顺序由事务层的事务 ID 保证。数据链路层只保证可靠传输,不保证重排序。
1.6 物理层
物理层分为逻辑子层和电气子层。逻辑子层负责加扰、对齐、弹性缓冲等,电气子层负责差分信号驱动、接收均衡等。
我最头疼的是链路训练状态机(LTSSM)。PCIe 上电后,物理层会经历 Detect、Polling、Configuration、L0 等状态,最后才能进入正常工作模式。有一次板卡调试,链路一直卡在 Polling 状态,查了三天发现是参考时钟的抖动超标。
简单列一下 LTSSM 的主要状态:
- Detect:检测对端是否存在
- Polling:比特对齐和符号对齐
- Configuration:链路宽度协商、通道反转校正
- L0:正常工作状态
- Recovery:出现错误后重新训练
1.7 配置空间与枚举
每个 PCIe 设备都有配置空间(256 字节的 Legacy 配置空间,以及扩展配置空间)。BIOS 或操作系统通过配置读写来发现设备、分配资源。
我自己写过一个简单的枚举工具,遍历总线号、设备号、功能号,读出 Vendor ID 和 Device ID。这里有个坑:PCIe 支持 Multi-Function 设备,但 Endpoint 必须实现 Function 0,否则枚举会跳过。
// 伪代码:配置空间读取示例
uint32_t read_pcie_config(uint8_t bus, uint8_t dev, uint8_t func, uint16_t offset)
{
uint32_t addr = 0x80000000 | (bus << 16) | (dev << 11) | (func << 8) | (offset & 0xFC);
// 写配置地址端口 0xCF8
outl(0xCF8, addr);
// 读配置数据端口 0xCFC
return inl(0xCFC);
}
枚举结果示例(某主机查看 lspci 输出):
| Bus:Dev.Func | Vendor ID | Device ID | 描述 |
| 00:00.0 | 8086 | 1901 | Host Bridge |
| 00:01.0 | 8086 | 1905 | PCIe Root Port |
| 01:00.0 | 10DE | 1B80 | VGA Controller |
| 02:00.0 | 8086 | 1539 | Ethernet Controller |
1.8 性能指标
PCIe 每一代的单 Lane 速率几乎翻倍。注意,这里的速率是传输速率(Gbps),不是有效数据带宽。因为编码开销(8b/10b 或 128b/130b)会吃掉一部分。
| PCIe 版本 | 单 Lane 速率 | 编码方式 | 有效带宽(x1 单向) |
|---|---|---|---|
| 1.0 | 2.5 GT/s | 8b/10b | 250 MB/s |
| 2.0 | 5.0 GT/s | 8b/10b | 500 MB/s |
| 3.0 | 8.0 GT/s | 128b/130b | ~985 MB/s |
| 4.0 | 16.0 GT/s | 128b/130b | ~1969 MB/s |
| 5.0 | 32.0 GT/s | 128b/130b | ~3938 MB/s |
避坑指南: 我曾经在设计 PCIe Gen3 链路时,以为按 8 GT/s 配线就够了。实际上,128b/130b 编码下每个 Lane 的物理层还需要考虑插入损耗、回损和抖动的模板测试。一句话:速率越高,通道设计越敏感。