PCIe协议基础(一):拓扑结构、分层模型、事务层包TLP
各位同学好。我们正式开始接触PCIe协议的核心内容。说实话,很多做FPGA的同学一开始就被协议规范吓住了——上千页的文档,谁看了都头大。但做DMA驱动,你不需要通篇啃完。把拓扑结构、分层模型和TLP这三个东西吃透,底子就算打牢了。
1. 拓扑结构:树形层次,不是总线
PCIe 和传统 PCI 最大的区别在哪?它不再是共享总线了。PCIe 是点对点的树形拓扑。你想想看,传统 PCI 上多个设备挂在同一根总线上,谁要传输先得抢总线仲裁。PCIe 完全不同,每个设备都有自己的专用通道。
一个典型的 PCIe 系统包含下面这些角色:
| 组件 | 角色说明 | 常见形式 |
|---|---|---|
| Root Complex (RC) | 系统的大脑,连接 CPU、内存和 PCIe 交换结构 | CPU/芯片组内部 |
| Switch | 类似网络交换机,把一条上游链路分叉成多条下游链路 | PCIe 交换芯片 |
| Endpoint (EP) | 功能设备,比如我的 FPGA 板卡、显卡、NVMe SSD | FPGA 实现的 PCIe IP |
| Bridge | 连接老设备(如 PCI/PCI-X)用的,现在很少见了 | PCIe-PCI 桥片 |
我印象很深,第一次做 FPGA PCIe 项目时,总以为 FPGA 要直连 CPU。后来发现,我的板卡其实通过主板的 PCIe 插槽连到了一个 Switch 上,再到达 Root Complex。搞清这条路径对调试 DMA 性能非常关键——你想想,通过 Switch 转接会有额外的延迟。
2. 分层模型:各司其职,与网络协议异曲同工
PCIe 的分层模型说白了就是逐层封装与解封装。这就是我最喜欢拿网络协议栈来类比的地方。
- 事务层(Transaction Layer):处理 TLP 的组装和解析。上层软件发起一次读写,事务层把请求打包成 TLP 往下送。接收侧的事务层再解析 TLP 给上层。
- 数据链路层(Data Link Layer):给 TLP 加序列号和 CRC。目的就是可靠传输——如果对端发现 CRC 不对,会要求重传。这层你基本上不用管,PCIe IP 核已经全自动处理了。
- 物理层(Physical Layer):负责比特传输。把链路层来的数据包编码、加扰、串行化,然后通过差分对发出去。同时管着链路训练和速度协商。
数据从上往下传时,每层加自己的头/尾;接收时从下往上一层一层剥掉。我把这个逻辑总结成一句话:上层只管"我要做什么",下层只管"怎么传出去"。
关键理解:
- 事务层关心的是 TLP 的 内容(读哪个地址、写什么数据)。
- 数据链路层关心的是 TLP 的 可靠性(有没有丢、有没有错)。
- 物理层关心的是 TLP 的 电信号(电压、时序、编码)。
在 FPGA 上做 PCIe DMA 驱动,我们最主要的战场就是事务层——嗯,当然,偶尔也需要跟物理层聊两句,比如检查链路是否训练成功。
3. 事务层包 TLP:PCIe 世界的最小消息单位
TLP,全称 Transaction Layer Packet。它是 PCIe 通信的基本数据单元。你在 FPGA 和 CPU 之间搬数据,本质就是在搬 TLP。
一个 TLP 长成这样:
+--------+---------+-----------+----------+--------+--------+
| 起始 | 序列号 | TLP 头 | 数据载荷 | digest | CRC |
| (STP) | (2B) | (12/16B) | (0~4096B)| (4B) | (4B) |
+--------+---------+-----------+----------+--------+--------+
← 数据链路层添加 →← 事务层核心 →← 链路层 →
TLP 头是最重要的。它里面塞了这些信息:Fmt(格式)、Type(类型)、长度、请求者 ID、标签、地址、完成者 ID 等等。最常见的是存储器读/写 TLP(MRd/MWr)和完成 TLP(Cpl/CplD)。
我给你看个实际配置空间里读出来的东西。这不是完整代码,但能说明 TLP 头的结构:
// 一个典型的 32 位存储器写 TLP(4 DW 头,无数据载荷)
// Byte 0: Fmt=010, Type=00000 → MWr
// Byte 4: Length=01 (1 DW)
// Byte 8: Address = 0x0000_8000
// Byte 12: Requester ID + Tag + Last/BE
// 收到完成 TLP(CplD)时,头部会包含
// Completer ID 和 Byte Count 字段
// 软件通过 Tag 来匹配请求和完成
我踩过一个坑:TLP 的数据载荷最大只有 4096 字节(也就是 1 个 Memory Read/Write 最多传 4KB)。那我的 DMA 一次要搬 4MB 数据怎么办?很简单,拆成多个 TLP。FPGA 的 PCIe IP 核会自动帮你拆分和重组,但你得理解这个限制——否则你做的 DMA 控制器的读写粒度可能就不对。
4. 必备的 PCIe 枚举与配置空间
上电后,主机操作系统会扫描 PCIe 总线树,给每个设备分配总线号、设备号、功能号(BDF)以及BAR 空间。这块在写 DMA 驱动时太重要了——你的 FPGA 板卡必须正确声明 BAR 空间大小,否则驱动根本找不到你。
| 配置空间偏移 | 字段 | 作用 |
|---|---|---|
| 0x00 | Vendor ID / Device ID | 厂商和设备编号,驱动匹配用 |
| 0x04 | Command / Status | 控制总线主设备、Memory/IO 访问使能 |
| 0x10 ~ 0x24 | BAR0 ~ BAR5 | 设备寄存器/内存空间基地址 |
| 0x34 | Capabilities Pointer | 指向能力链表的起始位置 |
| 0x40+ | MSI/MSI-X Capability | 中断能力结构,DMA 必用 |
做 FPGA 板卡时,我习惯把 BAR0 设成 256KB,映射到 FPGA 内部的寄存器空间。这样驱动就可以通过 ioremap 直接读写寄存器。BAR 空间太小,功能受限;太大,浪费系统内存映射资源。
本章小结
这一章的内容主要帮大家搭建 PCIe 协议的基本框架。记住三点:
- 拓扑结构是树形的,点对点通信,不是共享总线。
- 分层模型让各层专注自己的事,你在 FPGA 里主要关注事务层。
- TLP 是通信的原子单位,理解它的头和路由方式,后面 DMA 和中断的事情才好办。
下一类内容我们会深入事务层的 TLP 细节,看不同类型的 TLP 怎么通过 FPGA IP 核收发。先到这,有什么疑问欢迎在学员群交流。