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 映射没对齐。
一个典型的拓扑长这样:
- Root Complex(RC):根联合体。控制总线事务,生成配置请求、内存请求、I/O 请求。
- Switch:相当于 PCIe 的交换机。内部由上行端口(Upstream Port)和下行端口(Downstream Port)组成,不处理数据内容,只负责转发。
- Endpoint(EP):终端设备。比如显卡、NVMe SSD、网卡。只能作为事务的发起者或接收者,不能继续扩展。
- Bridge:遗留的 PCI/PCI-X 桥。老设备可以通过桥接到 PCIe 树上,但我个人建议新设计直接上原生 EP。
为了让你更直观地理解,我画了一张拓扑结构图。
调试时用 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 问题的利器。