1. PCIe 总线概述:从历史到分层模型

做底层驱动这么多年,我经常跟 PCIe 打交道。说实话,刚开始接触 BAR 地址映射时,我也觉得它像个黑盒子——读手册能读完,但一碰到实际 Bug 就懵了。这一章我们先打地基,把 PCIe 的来龙去脉、物理拓扑和内部分层理清楚。你想想看,连总线怎么连的都不知道,后面谈资源分配就是空中楼阁。

1.1 PCIe 发展史:从并行到串行的进化

PCIe 之前,主流的扩展总线是 PCI(Peripheral Component Interconnect)。PCI 是并行总线——32 位或 64 位数据线同时传输。但并行总线有个硬伤:频率一高,信号歪斜(skew)就爆炸。我记得十年前做个工控机,PCI 跑到 66 MHz 已经要加匹配电阻,布线得像绣花一样。

后来 PCI-X 试图在并行架构上提速,但效果有限。到了 2003 年左右,Intel 牵头推出了 PCI Express(后简称 PCIe)。它改用差分串行对,一条 lane 只管收和发,频率可以轻松往上拉。说白了,就是放弃并行这条拥挤的马路,改走高速单车道。

下面这个表格能让你快速看清几代 PCIe 的带宽变化。我在项目中常用的是 Gen3,但新板卡已经全面转向 Gen4 甚至 Gen5 了。

版本 单 Lane 速率(GT/s) 编码方式 x1 单向带宽(GB/s) 发布时间
Gen1 2.5 8b/10b 0.25 2003
Gen2 5.0 8b/10b 0.5 2007
Gen3 8.0 128b/130b ~0.985 2010
Gen4 16.0 128b/130b ~1.969 2017
Gen5 32.0 128b/130b ~3.938 2019
Gen6 64.0 PAM4 + FLIT ~7.877 2022(草案)

实际项目中 x4 或 x8 很常见。比如一块 x4 的网卡,Gen3 下单向带宽约 4 GB/s,足够收线速万兆。注意带宽是双向的,收发同时跑。

1.2 拓扑结构:树形、RC 和 Switch

PCIe 的物理拓扑是树形,根节点叫 Root Complex(RC),叶子节点是 Endpoint(EP,设备端点),中间通过 Switch 扩展。RC 通常集成在 CPU 内部,负责把 PCIe 域和内存域连起来。我曾经遇到一个怪问题:EP 收到的 DMA 地址总是写错。后来排查发现,RC 的地址翻译配置错了——BAR 映射没对齐。

一个典型的拓扑长这样:

  1. Root Complex(RC):根联合体。控制总线事务,生成配置请求、内存请求、I/O 请求。
  2. Switch:相当于 PCIe 的交换机。内部由上行端口(Upstream Port)和下行端口(Downstream Port)组成,不处理数据内容,只负责转发。
  3. Endpoint(EP):终端设备。比如显卡、NVMe SSD、网卡。只能作为事务的发起者或接收者,不能继续扩展。
  4. Bridge:遗留的 PCI/PCI-X 桥。老设备可以通过桥接到 PCIe 树上,但我个人建议新设计直接上原生 EP。

为了让你更直观地理解,我画了一张拓扑结构图。

PCIe 树形拓扑结构示意 Root Complex (RC) Switch Upstream Port → Downstream Port 转发 Endpoint 1 Endpoint 2 Endpoint 3 PCI/PCI-X Bridge(可选) 说明 • 每个 EP 通过独立 VLAN 链路连接 • Switch 内部采用点对点交换 • 最多可挂 256 个设备(实际受限于 RC)

调试时用 lspci -tv 可以看到 Linux 下的 PCIe 树结构。我曾靠这个命令半小时定位到一块万兆网卡为何没被正确枚举——原来是 Switch 的下行端口没加供电。

1.3 分层模型:事务层、数据链路层、物理层

PCIe 协议栈分三层,从上到下:事务层(Transaction Layer)、数据链路层(Data Link Layer)、物理层(Physical Layer)。每一层只对相邻层提供服务,内部细节不对外暴露。我刚开始看这个模型时觉得「这跟 OSI 七层有啥区别」,后来写驱动才明白——每一层加的 Header 和 CRC 刚好是调试抓包时能看到的东西。

下面这张表总结了各层的核心职责和封装单元:

层次 职责 协议数据单元(PDU) 关键特性
事务层 处理读写请求、完成包、消息。负责 BAR 地址翻译、TLP 头组装 TLP(Transaction Layer Packet) 支持 4 种地址空间:内存、I/O、配置、消息
数据链路层 保证 TLP 在链路上可靠传输:CRC 校验、重传、ACK/NAK DLLP(Data Link Layer Packet) 每发送一个 TLP 都附带序列号,接收方确认
物理层 串行化/反串行化、电气特性、链路训练、极性反转 PLP(Physical Layer Packet,以及逻辑子层处理后的 bit 流) 分逻辑子层和电气子层;差分信号

举个例子。当 CPU 读取某 EP 的 BAR0 地址时,事务层先构造一个 Memory Read TLP:填充请求类型、请求者 ID、事务 Tag、地址、长度等字段。然后交给数据链路层添加序列号和 CRC,再传给物理层做 8b/10b 编码(Gen1/2)或 128b/130b 编码(Gen3+),最后变成差分包发到线路上。

1.4 我个人踩过的坑——配置空间与 BAR

特别注意: 每个 EP 的配置空间前 64 字节是标准头,其中 BAR 占据 0x10~0x24 位置。BAR 的宽度由硬件决定,软件写全 1 到 BAR 寄存器读回可知道需要多大的地址空间。我遇到过最坑的是某 FPGA 设计把 BAR 设置成 1MB,但驱动中申请了 4MB 的 MMIO 窗口——结果跨页访问直接挂死。

PCIe 的分层模型还有个好处:驱动开发时可以几乎只关注事务层。数据链路层和物理层的细节由硬件自动处理。你想想看,要是让你手动重传丢包,那代码得多恐怖。

1.5 小结:这一章你该带走什么

  • 发展脉络: 并行(PCI)→ 串行(PCIe),速率持续翻倍,Gen6 已经用上 PAM4。
  • 拓扑结构: RC → Switch → EP,树形,一个 Switch 最多扩展 32 个端口(实际受限于 RC 的 Bus Number)。
  • 分层模型: 事务层(TLP)、数据链路层(DLLP)、物理层(bit 流)。调试时抓 TLP 包是定位 BAR 问题的利器。