2. PCIe体系结构:三层次架构的拆解与落地

说实话,很多初学者一上来就盯着PCIe的寄存器或者DMA描述符猛啃,结果越学越乱。我个人习惯是先花点时间把PCIe的层次体系搞清楚。你只有理解了这三层各自干什么,出了问题才知道该查哪里。

PCIe的体系结构,简单说就是三层堆叠:事务层(Transaction Layer)、数据链路层(Data Link Layer)、物理层(Physical Layer)。每一层都有自己明确的职责,有点像公司里的前中后台:前台接单(事务层)、中台排期与核对(数据链路层)、后台发货走线(物理层)。

核心观点: 应用程序(或驱动)只跟事务层打交道,数据链路层和物理层对软件开发者来说通常是透明的,但理解它们的机制能帮你写出更健壮的驱动。

下面我按从上到下的顺序,逐层拆解。

2.1 总体层次:从软件请求到物理比特流

当你的驱动发起一次DMA读或者写请求时,数据并不是直接从内存飞到设备端的。它会经过一个完整的流水线处理。我画了一张图来帮你看清这个流程:

PCIe 三层次体系结构图 PCIe 三层次体系结构(发送端视角) 软件/驱动层(内存读写请求) 事务层(TLP 封装/解封装) 数据链路层(DLLP + CRC/ACK) 物理层(串行化/解串 + 电气特性) 对端设备(接收端:逐层向上解包) MemRd / MemWr / CfgRd 请求及完成TLP ACK / NAK 数据重传机制

这张图要表达的逻辑很简单:数据发起端从上到下逐层打包,接收端从下到上逐层解包。每一层只关心自己那一层该怎么处理,这是一种非常好的分层解耦设计。

2.2 事务层:TLP 的装载与调度

事务层是离驱动最近的一层。我个人习惯把它理解为“订单中心”。当你的驱动要读一块设备内存时,事务层会把这件事封装成一个Memory Read Request TLP。

事务层主要干这几件事:

  • TLP 类型识别:是内存读写(Memory)、IO读写(IO)、配置空间访问(Configuration)还是消息(Message)?
  • 事务描述符生成:包含 Requester ID、Tag、Address、Length 等信息。
  • 流量分类与排序:PCIe 有严格的流量优先级(VC 概念),防止低优先级请求堵塞高优先级事务。
TLP 类型 用途场景 我见过的问题
Memory Read (MRd) DMA 读操作的核心包 曾经遇到读取地址返回全0,发现是TLP地址没对齐
Memory Write (MWr) DMA 写操作的核心包 写缓存未刷新导致数据丢失
Completion (CplD) 读操作的返回数据包 Completer ID 不匹配导致超时
Configuration (CfgRd/Wr) 枚举与初始化阶段使用 配置空间长度字段填写错误导致挂死
避坑指南: 我曾经在调试一块FPGA加速卡时,发现DMA传送总超时。用一个PCIe分析仪抓包一看,原来是事务层发出的Memory Write TLP中,Length字段写的是0。系统认为这是一个非法长度,直接丢弃了包。记住:TLP 的每个字段都是硬规则,少一个bit都不行。

2.3 数据链路层:可靠性的守护者

数据链路层主要做两件事:差错检测和流量控制。它负责把事务层打包好的TLP交给物理层发送,同时确保对端能正确收到。

具体来说:

  • CRC 计算与校验:每个TLP都带一个32位的LCRC(链路层CRC),接收端数据链路层收到后重新算一遍,不匹配则会请求重传。
  • ACK/NAK 重传协议:发送端会在内部缓冲一份拷贝,收到ACK后才丢弃;收到NAK则立刻重发。这里有一个关键点——重传缓冲区大小是有限的。
  • 初始化与链路训练状态机:数据链路层也参与了链路初始化的状态转换,但那是物理层先做完 的底层握手之后的事。

你想想看,为什么 PCIe 能保证数据不丢?就是靠数据链路层这一套 ACK/NAK 机制。我在写驱动时曾遇到一个坑:数据链路层的重传缓冲区被撑满,导致发送端卡死。原因是我在一次 DMA 中连续发送了大量小包,但接收端处理太慢,ACK 迟迟不回来。

注意事项: 数据链路层对驱动开发者来说是不可见的,但它的错误状态(Data Link Layer Link Active / Link Up)可以通过 PCIe 配置空间的 Device Status 寄存器读取。当你的 DMA 莫名其妙卡住时,去查一下这个寄存器的 Link Training Error 位,往往能揪出问题根源。

2.4 物理层:真正的比特搬运工

物理层,说白了就是那些电信号的事。PCIe 的物理层分为两个子层:

  • 逻辑子层:负责字节拆包/打包、8b/10b 或 128b/130b 编码(看是哪一代)、加扰(Scrambling)、Ordered Set(SKP、COM等)插入。
  • 电气子层:负责差分信号驱动、接收均衡、时钟恢复。这些东西跟驱动代码关系不大,但你要知道:物理层的链路速度(Gen1/Gen2/Gen3)和通道数(x1/x2/x4/x8/x16)是在 Link Training & Status State Machine (LTSSM) 中协商出来的。

我记得早期在一个项目上,用了很长的 PCIe 插槽转接线,结果设备一直工作在 Gen1 速度。当时我第一反应是驱动代码写错了,查了半天。后来发现是物理层的信号完整性太差,LTSSM 协商过程反复降速到 Gen1 才能稳定。嗯,这个坑给我的教训是:软件能做的事情有限,物理层的问题最终得回到硬件设计上解决。

2.5 三层之间的协作:一个完整的 DMA 写请求示例

假设你的驱动程序发起一个 256 字节的 DMA 写,目标是设备的内存空间。看看这三层是如何协作的:

  1. 驱动层:配置 DMA 引擎,发起写请求。
  2. 事务层:封装一个 Memory Write 64 TLP,包含目标地址(64-bit)、大小(256字节)、Requester ID(你的设备 BDF)、Tag(事务标签)。
  3. 数据链路层:在这个 TLP 前面加上 Sequence Number,末尾计算 LCRC。然后将整个包加入重传缓冲,准备发送。
  4. 物理层:将数据链路层传来的包进行 128b/130b 编码(如果是 Gen3+),加扰,然后串行化成差分信号,从 Tx 引脚发送出去。
  5. 接收端物理层:收到信号,解串,解码,去扰,恢复出数据包,交给接收端数据链路层。
  6. 接收端数据链路层:校验 LCRC,检查 Sequence Number 是否连续。通过后,发送 ACK DLLP 回去,同时把 TLP 递给接收端事务层。
  7. 接收端事务层:解析 TLP 类型,将数据写入目标内存地址。
关键点: 以上这 7 步,在没有错误的情况下,在 Gen3 x8 链路上完成的延迟通常在微秒级别。你的驱动代码甚至感觉不到延迟——但一旦出错,重传机制会把延迟放大到几毫秒甚至超时。

好了,这就是 PCIe 三层次体系结构的大致模样。你如果能把这三层的职责边界和协作关系记在脑子里,后面学 DMA 引擎、中断处理、性能调优时会少走很多弯路。这部分内容我就讲到这里,记住:分层是 PCIe 强大能力的基础,也是调试时的第一把钥匙。