PCIe协议基础(一):拓扑结构、分层模型、事务层包TLP

各位同学好。我们正式开始接触PCIe协议的核心内容。说实话,很多做FPGA的同学一开始就被协议规范吓住了——上千页的文档,谁看了都头大。但做DMA驱动,你不需要通篇啃完。把拓扑结构、分层模型和TLP这三个东西吃透,底子就算打牢了。

PCIe 协议基础三层结构概览 事务层(Transaction Layer) TLP 组装/解析 存储器读写 | 配置 | 消息 事务排序与流控 VC 仲裁 | 信用量管理 地址转换 / 路由 ID 路由 | 地址路由 数据链路层(Data Link Layer) DLLP 封装与检错 CRC 校验 | Ack/Nak 重传 链路管理与流量初始化 电源管理 | 链路训练 物理层(Physical Layer) 电气子层 差分对 SerDes PMA/PCS 逻辑子层 8b/10b 或 128b/130b 编码 加扰 / 解扰 链路训练 速率协商 | Lane 对齐 LTSSM 状态机 数据流从上到下封装,从下到上解封装 —— 每一层只关心本层的工作

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 转接会有额外的延迟。

实践经验: 做 FPGA 开发时,先看清你的板卡插在哪根 PCIe 槽上。槽位不同,到 Root Complex 的跳数也不同。跳数多,延迟大。我习惯先在 BIOS 里查看 BDF(总线号:设备号:功能号),例如 01:00.0 就表示 Bus 1 上的设备 0。

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 控制器的读写粒度可能就不对。

注意地址路由: TLP 的路由方式有两种。用内存地址写的叫"地址路由",用在设备间通信的叫"ID 路由"。我见过有新手在 FPGA 里给配置读写 TLP 配了个内存地址,结果另一端收不到。因为配置读写走的是 BDF(ID 路由),不是地址。

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 协议的基本框架。记住三点:

  1. 拓扑结构是树形的,点对点通信,不是共享总线。
  2. 分层模型让各层专注自己的事,你在 FPGA 里主要关注事务层。
  3. TLP 是通信的原子单位,理解它的头和路由方式,后面 DMA 和中断的事情才好办。

下一类内容我们会深入事务层的 TLP 细节,看不同类型的 TLP 怎么通过 FPGA IP 核收发。先到这,有什么疑问欢迎在学员群交流。