一、PCIe概述与基本概念
我先从最本质的问题说起:为什么我们需要PCIe?
你想想看,早期的PCI总线是并行架构,32位宽、33MHz,理论带宽也就133MB/s。放到今天,一个高速NVMe SSD随便就能跑到3GB/s以上,更别提GPU动不动几十GB/s的需求了。PCIe(PCI Express)就是用来解决这个瓶颈的。它把并行改成串行,用差分信号、点对点连接,速度翻着跟头往上走。
入行那会儿,我拿着PCIe的规范手册,光看那几千页就被吓住了。后来才明白,驱动开发不需要你把所有细节都背下来,但核心概念必须吃透。今天这一章节,我就带你把这几个地基打牢。
1.1 PCIe的拓扑结构
PCIe采用树形拓扑,示意图如下:
PCIe拓扑里,最关键的角色有三个:
- Root Complex (RC):相当于树根,直接连接CPU、内存。我见过不少初学者把RC当成单纯的“北桥”,其实它还管理着中断、错误报告。
- Switch:用来扩展端口。注意Switch并不是类似路由器的“交换”,它内部是通过一个逻辑的、硬连线仲裁器把多个Endpoints映射到Root Complex。
- Endpoint (EP):终端设备,比如你的GPU、NVMe盘、采集卡。我做驱动开发时,80%的代码都是在和Endpoint的配置空间、BAR打交道。
1.2 链路层与传输层概念
PCIe协议栈分为三层,从底到顶分别是:物理层、数据链路层、事务层。驱动开发中最常打交道的是事务层,但底层原理也得懂。
| 层级 | 主要职责 | 驱动关注点 |
|---|---|---|
| 物理层 | 差分信号发送/接收、时钟恢复、链路训练 | 链路状态检测(Link Status) |
| 数据链路层 | CRC校验、重传、流量控制(用ACK/NAK) | 很少直接控制,但错误计数器能查 |
| 事务层 | 生成和解析TLP(事务层数据包) | 读写请求、完成报文、消息中断 |
拿一个DMA读操作举例:驱动发了一个Memory Read TLP(事务层包),数据链路层加上序列号和CRC,物理层再串行化发出去。响应回来时,物理层把串行转并行,数据链路层校验完整性,事务层把数据提取出来。
1.3 链路宽度与速率
PCIe是一组串行通道(Lane)捆在一起工作的。一个Lane就是一对差分发送线+一对差分接收线。
| PCIe版本 | 每Lane单方向速率 | x1带宽 | x16带宽 |
|---|---|---|---|
| PCIe 1.0 | 2.5 GT/s | ~250 MB/s | ~4 GB/s |
| PCIe 2.0 | 5.0 GT/s | ~500 MB/s | ~8 GB/s |
| PCIe 3.0 | 8.0 GT/s (128b/130b编码) | ~1 GB/s | ~16 GB/s |
| PCIe 4.0 | 16.0 GT/s | ~2 GB/s | ~32 GB/s |
| PCIe 5.0 | 32.0 GT/s | ~4 GB/s | ~64 GB/s |
注意:PCIe 3.0开始采用了128b/130b编码,有效利用率约98.5%,所以带宽计算不再是简单用GT/s除以10。以前我写驱动时,按PCIe 2.0的20%开销估算总够用,后来3.0时代吃了一次亏——原来编码效率不一样。
Link Capabilities 和 Link Status 寄存器来确认当前速率。
1.4 配置空间与BAR
每个PCIe设备(包括Root Port和Switch)都有个配置空间,前256字节是标准PCI兼容区域,后面还有PCIe扩展空间。驱动开发最重要的就是读/写这个配置空间。
配置空间的前64字节用于Header(Type 0或Type 1)。对于Endpoint(Type 0),大家最关心的是:
- Vendor ID / Device ID:用来识别制造厂商和设备型号。
- Class Code:告诉OS这是存储、网络还是显示设备。
- BAR (Base Address Register):6个寄存器映射I/O或Memory空间。
BAR是DMA驱动的心脏。一个Endpoint通过BAR告诉CPU:“我的寄存器在这里,我的DMA缓冲区在这里。” 我刚开始写驱动时,忘记配置BAR的Memory Space Enable位,结果访问内存地址时CPU直接报总线错误,查了半小时。
pci_resource_start() 和 pci_resource_len() 获取基地址和长度,不要自己去解析配置空间里的raw值,不同架构的字节序容易搞错。
1.5 中断机制简介
PCIe支持三种中断方式:
- INTx (Legacy):传统中断线,中断线通过路由到Root Complex。现在很少用了。
- MSI (Message Signaled Interrupt):设备往CPU指定的地址写一个DW(双字),触发中断。支持32条消息,但每消息必须绑定一个向量。
- MSI-X:MSI的升级版,最多支持2048个中断向量,每个向量可以指定独立的目标地址和数据。写DMA驱动时,我几乎只用MSI-X,可以为每个DMA通道分配独立中断,降低延迟。
你可能会问:“为什么不用INTx?省事啊。” 嗯,INTx是共享中断线,多个设备共享一条线,中断处理上来要遍历所有设备,效率低下。我调一个高速数据采集卡时,就是因为用了INTx导致中断丢失,后来改成MSI-X一下子就稳了。
pci_alloc_irq_vectors() 分配向量,不用自己操作配置空间里的Message Address/Data寄存器,内核帮你搞定。
1.6 本章小结
说到底,PCIe就是一个高速串行总线树。Root Complex是根,Switch负责扩展,Endpoint是实际干活的设备。
你需要记住的要点:
- 拓扑结构决定了你能枚举到哪些设备。
- 链路训练成功后,通过配置空间读取设备身份和能力。
- BAR映射是驱动访问设备寄存器和DMA缓冲区的桥梁。
- 中断首选MSI-X,高效且可扩展。
好了,这一章的概念是基础中的基础。往后我们会深入到TLP、DMA描述符、数据传输模式,这些都会反复用到今天的知识。先把这些核心概念刻在脑子里,后面写代码才能游刃有余。