3. PCIe协议体系:事务层、数据链路层、物理层分层结构及关键功能

谈PCIe,绕不开它的分层结构。很多新手工程师一上来就被各种TLP、DLLP、Ordering规则给绕晕了。我个人习惯,先不急着看细节,而是问一个问题:这条链路到底想把数据从一端搬到另一端,中间要解决哪些麻烦?

答案是三个核心问题:发什么(事务层)、怎么可靠地发(数据链路层)、怎么用物理线路发(物理层)。PCIe很聪明地把这些问题拆给了三层,各管一段。

3.1 三层架构总览

层次 英文缩写 核心对象 一句话定位
事务层 TL 事务层包 (TLP) 定义“读写什么、读写哪、多大”
数据链路层 DLL 数据链路层包 (DLLP) 保证TLP完整到达对端,并管理流量
物理层 PHY 符号 (Symbol) / 电气信号 负责真正的高速串行收发

你想想看,上层发一个“读内存地址0x1000”的请求,这个请求会从事务层打包成TLP,交给数据链路层加序列号、CRC,再交给物理层编码、串化、通过差分线发出去。接收端则反过来:物理层还原比特流,数据链路层校验正确,事务层解析出读请求,然后交给内核。说白了,这就是一个层层打包、层层解包的过程。

3.2 事务层——协议的“大脑”

事务层是离软件最近的一层。我接触过的绝大多数PCIe调试问题,根源都在事务层——要么TLP格式错了,要么地址空间搞混了。

3.2.1 事务类型

  • Memory 事务:最常用,支持 32 位 / 64 位地址。读写性能直接决定了你的数据传输速率。
  • IO 事务:基本上只存在于规范里,实际设备很少用。我在项目中唯一碰到过IO事务的场景,是调试一块老式转接卡。
  • Configuration 事务:主要用于枚举和配置。系统上电时靠它给每个Endpoint分配BDF和BAR空间。
  • Message 事务:不带地址,靠消息编码传递中断、错误通知、电源管理等信息。比如MSI/MSI-X中断就是通过Message TLP发送的。

3.2.2 TLP 格式

每个TLP都有固定的头部,但不同事务的头部长度不一样。Mem 请求通常是 3 个DW(12字节),Completion带数据的话还会更长。我记得有次给团队新人review代码,对方把Completion头部长度写成了3DW——嗯,结果RC端根本解析不了,直接报UR(不支持的请求)。

TLP 包头关键字段(以Mem Read 32为例)

  • Fmt/Type:标识包格式和类型(如 0000_0000 表示Mem Read 32)
  • TC:Traffic Class,影响QoS
  • Length:要读/写的Data Payload长度,以DW为单位
  • Requester ID:Bus/Device/Function号
  • Tag:请求标识,接收方靠它匹配对应的Completion
  • Address:32位目标地址

3.2.3 地址空间与路由

事务层通过地址空间决定怎么把包送到目标设备。PCIe支持三种路由方式:基于地址路由(Memory/IO)、基于ID路由(Configuration)、隐式路由(Message)。说白了,你的TLP头部里写什么,硬件就按什么规则去查转发路径。

3.3 数据链路层——协议的“监护者”

数据链路层就像个忠实的管家。它不关心你送的是读请求还是写数据,它只管一件事:确保每个TLP完整、不重不漏地到达对端。

3.3.1 核心职责

  1. 序列号与CRC:每个TLP到达DLL层时,会被压入一个递增的序列号,并附加CRC(LCRC)。接收端DLL会检查序列号是否连续、CRC是否匹配。一旦发现错误,立刻要求重传。
  2. ACK/NAK 重传机制:接收端收到一个TLP后,如果校验通过,回复ACK DLLP;如果校验失败,回复NAK DLLP。发送端收到NAK或超时未收到ACK,就会重传那个TLP。我有个朋友曾踩过坑:RTL里NAK逻辑写错了,导致无限循环重传,调试了一周才发现是状态机漏了一个条件。
  3. 流控 (Flow Control):通过发送FC DLLP,通知对端自己还有多少接收缓冲区(Credit)。发送端只有拿到足够的Credits才允许发出TLP。这能防止发送方“刷爆”接收端的缓冲区。

注意:我曾经调试过一个带错误注入的PCIe测试卡,发现DLL层会把损坏的TLP直接丢弃,然后等对端超时重传。这意味着事务层根本感知不到物理链路上有过一次瞬间的毛刺。这种透明性设计很好,但也让上层工程师排查偶发错误变得更加困难。

3.4 物理层——协议的“手脚”

物理层负责最底层的比特传输。它把DLL层送来的数据流进行编码、加扰、串化,然后通过差分对发送出去。接收端则做反向操作。

3.4.1 两大子层

逻辑子层 电气子层
负责8B/10B(Gen1/2)或128B/130B(Gen3+)编码 负责差分驱动、接收、阻抗匹配
包含加扰器,减少电磁干扰 定义电压摆幅、去加重、RX均衡
管理链路训练状态机(LTSSM) 处理时钟恢复和Bit Lock

这里我想重点强调一下链路训练 (Link Training)。物理层的LTSSM状态机负责在初始化阶段协商链路宽度、速率、极性反转、通道位置反转。你想想看,插一张卡下去,PCIe端口怎么知道你是x1还是x4?怎么知道你要跑Gen3还是Gen2?这些全由LTSSM在几个毫秒内通过发送训练序列(TS1/TS2)协商完成。

实战提示:我在联调一块FPGA开发的PCIe Endpoint时,PC一直报“链路训练失败”。用分析仪抓了TS序列,发现是FPGA端的Receiver Detect阶段未能正确识别PC发出的Electrical Idle Exit序列。最后发现是参考时钟的抖动超标,导致CDR无法锁定。调了PLL的环路带宽后,问题解决。所以说,物理层的问题有时候不光是协议的事,信号完整性也紧密相关。

3.5 全章核心逻辑图

PCIe 三层架构与数据流 事务层 (TL) 生成 TLP 事务类型 / 路由 / 排序 数据链路层 (DLL) 序列号 + CRC ACK/NAK 重传 / 流控 物理层 (PHY) 编码 / 加扰 串化 / 差分线 → 跨链路传输 → 物理层 (PHY) 解串 / 解码 时钟恢复 数据链路层 (DLL) 校验 CRC / 检查序列号 发送 ACK/NAK 事务层 (TL) 解析 TLP 路由到对应逻辑 关键流程说明 TLP 从左侧事务层 → 数据链路层 → 物理层发出;对端物理层 → 数据链路层(校验)→ 事务层(解析) DLL 层通过 ACK/NAK 确保传输可靠性,事务层无需关心链路层细节

上面这张图总结了三层之间在发送端和接收端的完整数据流。你看,从TLP生成到最终送到对端事务层,每层只做自己的事。这种分层设计最大的好处是:替换物理层(比如从Gen2升级到Gen3)不会影响事务层逻辑,上层软件甚至不需要改驱动代码。

3.6 层间接口——分层能拆,更要能合

三层之间通过预定义的接口通信:

  • 事务层 ⇄ 数据链路层:传递TLP(发送方向)和已校验的TLP(接收方向),以及流控Credit值。
  • 数据链路层 ⇄ 物理层:传递经过封装的TLP(加上了序列号和CRC),物理层把这些数据当作普通比特流处理。

这些接口在RTL设计中通常体现为简单的valid/ready握手。我记得早期有一版设计,事务层发出的TLP没有等DLL层ready就拉高valid,导致丢包。排查了老半天,最后发现是一个跨时钟域同步打拍(CDC)把ready信号延迟了两拍,valid信号却直接跨域过去了。嗯,这种问题在异步设计中挺常见的,你最好在仿真阶段就用断言把它们抓出来。

好了,PCIe的分层结构就说到这里。三层各司其职、层层递进,是理解后续所有状态机(LTSSM、ACK/NAK重传、顺序规则)的基础。