一、PCIe概述与基本概念

我先从最本质的问题说起:为什么我们需要PCIe?

你想想看,早期的PCI总线是并行架构,32位宽、33MHz,理论带宽也就133MB/s。放到今天,一个高速NVMe SSD随便就能跑到3GB/s以上,更别提GPU动不动几十GB/s的需求了。PCIe(PCI Express)就是用来解决这个瓶颈的。它把并行改成串行,用差分信号、点对点连接,速度翻着跟头往上走。

入行那会儿,我拿着PCIe的规范手册,光看那几千页就被吓住了。后来才明白,驱动开发不需要你把所有细节都背下来,但核心概念必须吃透。今天这一章节,我就带你把这几个地基打牢。

1.1 PCIe的拓扑结构

PCIe采用树形拓扑,示意图如下:

PCIe拓扑结构示意图 PCIe 拓扑结构 Root Complex CPU / 内存 / 控制器 Switch 桥接 / 转发 / 仲裁 Endpoint 1 NVMe SSD Endpoint 2 GPU Endpoint 3 网卡 说明: • Root Complex 连接 CPU 和内存,是整个树形结构的根。 • Switch 扩展出多个下游端口,每个 Endpoint 独占一条链路。

PCIe拓扑里,最关键的角色有三个:

  • Root Complex (RC):相当于树根,直接连接CPU、内存。我见过不少初学者把RC当成单纯的“北桥”,其实它还管理着中断、错误报告。
  • Switch:用来扩展端口。注意Switch并不是类似路由器的“交换”,它内部是通过一个逻辑的、硬连线仲裁器把多个Endpoints映射到Root Complex。
  • Endpoint (EP):终端设备,比如你的GPU、NVMe盘、采集卡。我做驱动开发时,80%的代码都是在和Endpoint的配置空间、BAR打交道。
我曾经踩过坑:以为所有离CPU最远的设备都叫Endpoint。但别忘了,PCIe桥(Bridge)本身也是一个特殊的Endpoint,但它下面还能挂东西。读PCIe配置空间时,Type 0 Header对应普通Endpoint,Type 1对应Bridge/Root Port。搞错了,访问寄存器会直接挂死。

1.2 链路层与传输层概念

PCIe协议栈分为三层,从底到顶分别是:物理层、数据链路层、事务层。驱动开发中最常打交道的是事务层,但底层原理也得懂。

层级主要职责驱动关注点
物理层差分信号发送/接收、时钟恢复、链路训练链路状态检测(Link Status)
数据链路层CRC校验、重传、流量控制(用ACK/NAK)很少直接控制,但错误计数器能查
事务层生成和解析TLP(事务层数据包)读写请求、完成报文、消息中断

拿一个DMA读操作举例:驱动发了一个Memory Read TLP(事务层包),数据链路层加上序列号和CRC,物理层再串行化发出去。响应回来时,物理层把串行转并行,数据链路层校验完整性,事务层把数据提取出来。

我个人的调试习惯:当DMA读不到数据时,先查事务层的Completion Timeout(完成超时),再查数据链路层的Bad TLP计数。物理层的问题反而少见——只要链路训练成功,通常不会出错。

1.3 链路宽度与速率

PCIe是一组串行通道(Lane)捆在一起工作的。一个Lane就是一对差分发送线+一对差分接收线。

PCIe版本每Lane单方向速率x1带宽x16带宽
PCIe 1.02.5 GT/s~250 MB/s~4 GB/s
PCIe 2.05.0 GT/s~500 MB/s~8 GB/s
PCIe 3.08.0 GT/s (128b/130b编码)~1 GB/s~16 GB/s
PCIe 4.016.0 GT/s~2 GB/s~32 GB/s
PCIe 5.032.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时代吃了一次亏——原来编码效率不一样。

关键点:实际应用中,设备会协商到双方都支持的最高版本和最大宽度。比如你插一张PCIe 4.0的显卡到PCIe 3.0的主板上,握手结果是PCIe 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直接报总线错误,查了半小时。

注意:BAR0~BAR5可以是64位,也可以32位。编写内核代码时,务必使用 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一下子就稳了。

经验之谈:现代内核都会优先分配MSI/MSI-X。驱动编写时,直接调用 pci_alloc_irq_vectors() 分配向量,不用自己操作配置空间里的Message Address/Data寄存器,内核帮你搞定。

1.6 本章小结

说到底,PCIe就是一个高速串行总线树。Root Complex是根,Switch负责扩展,Endpoint是实际干活的设备。

你需要记住的要点:

  1. 拓扑结构决定了你能枚举到哪些设备。
  2. 链路训练成功后,通过配置空间读取设备身份和能力。
  3. BAR映射是驱动访问设备寄存器和DMA缓冲区的桥梁。
  4. 中断首选MSI-X,高效且可扩展。

好了,这一章的概念是基础中的基础。往后我们会深入到TLP、DMA描述符、数据传输模式,这些都会反复用到今天的知识。先把这些核心概念刻在脑子里,后面写代码才能游刃有余。