这是四篇系列文章中的第一篇,提供 PCI Express (PCIe) 协议的概览。PCIe 本身内容庞大,因此我把它拆分为几份更容易消化的文档。即使如此,本系列也只是简要介绍。目标读者是希望进一步理解 PCIe 的人,例如在系统中使用该协议的工程师,或者在深入研究规范之前需要一个入门的人。
在本篇中,将介绍 PCIe 架构,并重点讨论协议三层中的第一层——物理层。事务层和数据链路层的细节会留在后续部分。
为了配合本文,我编写了一个用于 Verilog 的 PCIe 行为级仿真模型(pcievhost)。它运行一个 C 语言实现的 PCIe 协议模型,通过虚拟处理器 (VProc) 接口与 Verilog 环境交互。模型文档说明了如何搭建环境,以及如何使用 API 生成 PCIe 流量。本文中我会引用该模型的输出作为示例。模型还附带了一个测试环境,可以生成大部分常见流量,读者可自行修改驱动代码来探索 PCIe 协议。
当 PCIe 协议最初发布(1.0a 版)时,我们所设计的高性能计算系统决定从 PCI 升级到 PCIe,以获得更高的带宽潜力。虽然当时有一些第三方 IP,但经评估后发现它们无法满足我们低延迟等设计需求。于是,我接到任务,设计一个符合 PCIe 规范的 16 通道端点接口,并满足其他要求。
拿到 500 多页的规范时,最初感觉压力巨大。后来在伦敦的一次 PCIe 会议上,我问一位已经实现过 PCIe 接口的团队负责人,她说她的团队用了 4 名工程师一年时间,外加 20 名验证方面的外包人员。而我只有 18 个月时间,并且是一个人完成。最终,通过分解规范、舍弃一些可选功能,我逐步把复杂问题拆解成可管理的部分,最后成功实现了符合 1.1 规范的 16 通道端点,以及符合 2.0 规范的 8 通道端点。
在这份文档中,我会以类似的可控步骤,介绍当时实现过程中学到的概念。
与前身 PCI 不同,PCIe 不是总线架构,而是点对点协议(类似 AXI)。PCIe 系统的结构由多个点对点接口组成,这些接口通过交换结构(fabric)连接 CPU 和外设。
拓扑层次具有明确的方向:CPU 连接到根复合体(Root Complex,RC),它是顶层互连组件。RC 可通过开关 (Switch) 扩展更多 PCIe 接口。开关之间还能互联,从而进一步扩展。最终,外设(端点,Endpoint, EP)通过接口连接,比如显卡、网卡等。
在每条链路上,有“下行链路”(从上游组件如 RC 到 EP/交换机)和“上行链路”(从 EP/交换机到 RC)。PCIe 协议在链路上定义了三层:
物理层:负责电气连接、串行化、编码、链路初始化和训练、功耗状态切换。
数据链路层:基于物理层,负责流控、ACK/NAK、功耗管理。
事务层:负责内存/IO 读写请求、返回读完成包,并提供配置空间访问(独立于主地址映射的控制寄存器)。
PCIe 通过串行通道传输数据,每个通道是一对 AC 耦合的差分线。支持的通道宽度有 ×1、×2、×4、×8、×12、×16 和 ×32。通道越多,带宽越大。
例如显卡通常使用 ×16,而简单串行外设可能只需 ×1。两端接口不必宽度相同,链路初始化时会协商出双方都支持的最大宽度。
PCIe 的基本数据单位是字节。在序列化之前,字节会先经过扰码处理(基于 LFSR)。这样可以避免长时间的直流分量,提升信号质量。不同代际使用的多项式不同(如 2.0 使用 16 阶,3.0 使用 23 阶)。
PCIe 使用编码方式将字节映射为 DC 平衡的符号:
2.1 及之前使用 8b/10b 编码
3.0 及之后使用 128b/130b 编码
这些编码保证:
消除直流分量
提供时钟恢复能力
区分控制符号和数据符号
在链路训练和初始化阶段,会发送有序集(Ordered Set),例如:
TS1/TS2 训练序列:用于链路速度和通道编号协商
EIOS:进入电气空闲前发送
FTS:快速训练序列,用于低功耗状态恢复
SKP:用于通道对齐和时钟偏差补偿
链路的状态由 LTSSM(链路训练与状态机)管理。主要阶段包括:
Detect:检测电气连接
Polling:发送/接收训练序列,完成极性反转检查
Configuration:分配链路编号、通道编号、执行对齐和反转
L0:正常工作状态
此外还有低功耗状态(L0s、L1、L2)、Loopback、Disabled 等状态。
PCIe 的 SERDES(串并转换器)高速实现依赖不同工艺。Intel 提出的 PIPE 规范 把物理层划分为 MAC、PCS、PMA 三部分,并标准化了 MAC 与 PCS 的接口。这大大简化了逻辑设计与验证,使其更容易移植到不同的 SERDES 实现。
在本部分中,我们介绍了:
PCIe 的层次结构(RC、Switch、EP)
通道数量与协商
数据扰码与编码方式
有序集及其在链路训练中的作用
LTSSM 状态机及初始化流程
PIPE 规范如何屏蔽底层 SERDES 差异
物理层的核心在于:建立稳定链路,使其能承载上层(数据链路层和事务层)的数据包。
在第一部分中,我们介绍了 PCIe 架构,包括根复合体 (RC)、交换机 (Switch) 和端点 (EP),并深入讲解了最底层——物理层。物理层定义了数据传输的串行通道及其电气要求,高速数据传输需要编码与扰码,链路初始化通过有序集和状态机来实现,并且有 PIPE 规范虚拟化 SERDES 接口。
现在链路已经准备好传输字节,接下来我们进入 数据链路层 (DLL, Data Link Layer)。数据链路层位于物理层之上,主要负责:
保证链路上的数据完整性(CRC 校验与重传机制)
提供链路流量控制(基于 credit)
支持部分电源管理
提供厂商自定义的扩展通信
在讨论数据链路层之前,先复习一下事务层的数据包分类,DLL 必须知道这些:
Posted (P):不需要响应(例如写入内存)
Non-Posted (NP):需要响应(例如读取内存)
Completion (Cpl):对 NP 请求的响应(例如返回读取的数据)
数据链路层不仅要承载事务层数据包 (TLP),还有自己的一类小包,称为 数据链路层包 (DLLP)。
DLLP 固定长度为 6 字节:
第 1 字节:类型 (Type)
第 2-4 字节:依类型而定
最后 2 字节:16 位 CRC(按位倒序 + 结果取反)
流控包 (Flow Control)
初始化 (InitFC)
更新 (UpdateFC)
确认包 (ACK/NAK)
电源管理包 (PM)
厂商自定义包 (Vendor Specific)
DLLP/TLP 的传输同样依赖物理层:
开始标记:SDP(DLLP)或 STP(TLP)
结束标记:END(DLLP),TLP 可能用 END 或 EDB(错误终止)
宽链路情况:如果结束后无新包,则用 PAD 填充
在 PCIe 3.0+ 的 128b/130b 编码中,两个控制位指示这是数据帧还是有序集,数据帧内部再用符号表示 SDP/STP/IDL/EDB 等标记。
一条 PCIe 链路在同一时刻只能传输一个完整包,不能中断。但在跨越多个交换机的路径中,如果只依赖单一通道,低优先级数据可能阻塞高优先级数据。
解决方案是 虚拟通道 (VC0–VC7):
VC0 必须实现,其他可选
每个 VC 有独立缓冲区
调度器根据事务包的 Traffic Class (TC) 决定发哪个 VC 的数据
TC0 总是映射到 VC0
这样,在交换机等节点上,可以实现不同优先级流量的复用。
PCIe 使用基于 credit 的流控:
每类事务(P/NP/Cpl)都单独计数
Header 与 Data 各自有 credit 值
单位 credit = 16 字节 (4 DW)
例如一次内存写需要:
1 个 P Header credit
N 个 P Data credit(取决于数据量)
这样可以避免小包被大包阻塞,也允许先处理包头。
当链路进入 L0 状态后,发送端并不知道接收端有多少可用 buffer。于是 DLL 需先初始化:
仅 VC0 必须完成初始化即可进入 DL_LinkUp
InitFC DLLP 包含:
VC 编号
Header credit 数
Data credit 数
若 credit=0,表示无限(常用于 Completion)
流程:
发送方周期性发 InitFC1 (P→NP→Cpl)
接收方收到后,记录数值并开始发 InitFC2
双方完成 InitFC 交换 → 链路进入 DL_LinkUp
后续流控通过 UpdateFC DLLP:
其值为累计计数,而非绝对剩余值
例如初始 128,若收到更新=130,则表示只多了 2 个 credit
因此发送方通过 “累计发放 – 累计消耗” 算可用量
定时要求:必须周期性发更新包(默认 30µs,一般不超过 120µs),防止 DLLP 丢包导致死锁。
TLP 在 DLL 封装时:
添加 序列号 (12 bit)
添加 LCRC (32 bit)
接收端:
校验 LCRC
成功 → 回 ACK DLLP
失败 → 回 NAK DLLP
发送端维护一个 重传缓存 (Retry Buffer):
收到 ACK:序列号 ≤ ACK 的包均删除
收到 NAK:删除 ≤ NAK 的包,未确认的包全部重发
最多重试 4 次,再失败则物理层重训 (Retrain)
序列号范围 0–4095,最多允许 2048 个未确认包,以确保回绕不混淆。
电源管理主要由更高层驱动,但 DLL 提供 DLLP 形式的支持:
PM_Enter_L1 / L23:下行端请求进入低功耗
PM_Request_Ack:上行端确认并进入空闲
Active State Power Management (ASPM):下行端在空闲时可请求进入 L1
如果被拒绝 → 上行端发 Message NAK,下行端必须进入 L0s
类型字段定义为 Vendor Specific
内容未定义,供厂商扩展功能
标准规范中不要求常规使用
本部分介绍了 PCIe 数据链路层:
使用 credit 机制实现流控
DLL 初始化 (InitFC) 与更新 (UpdateFC)
ACK/NAK + 重传机制确保 TLP 完整性
支持电源管理 (PM DLLP)
允许厂商扩展 (Vendor DLLP)
数据链路层夹在物理层与事务层之间,不可避免要涉及上下层的内容,例如虚拟通道和配置空间。但它的核心任务就是:保证可靠传输 + 提供流控。
下一部分将进入 事务层 (Transaction Layer),讲解如何在链路上传输内存/IO/配置访问,以及消息(电源管理、错误报告、中断等)。
在前两部分中,我们介绍了 PCIe 的物理层和数据链路层。到目前为止,我们已经有了一个可以传输数据的物理通道,并通过 CRC 与重传机制保证了数据的可靠性,同时利用基于 credit 的流控管理了数据流量。
现在,我们终于可以进入 事务层 (Transaction Layer, TL),在链路上传输实际的事务数据包 (TLP)。事务层定义了三类数据传输事务:读、写和完成 (Completion),以及一类用于通信和状态信号的 消息 (Message)。相比物理层和数据链路层,事务层包含了更多详细规则,但整体概念并不复杂。
本部分将逐一介绍不同的事务层数据包类型 (TLP),分析它们的结构与用途,帮助形成对 PCIe 工作机制的整体认识。
正如第二部分提到的,事务被分为三大类:
Posted (P):无需响应(如写入内存)。
Non-Posted (NP):需要响应(如读取内存)。
Completion (Cpl):对 NP 请求的响应(如返回读数据)。
事务层定义了多种 TLP 类型,每个 TLP 属于上述三类之一:
| TLP 类型 | 分类 |
|---|---|
| Memory Read (MRd) | NP |
| Memory Write (MWr) | P |
| I/O Read (IORd) | NP |
| I/O Write (IOWr) | NP |
| Config Read Type 0/1 (CfgRd0/1) | NP |
| Config Write Type 0/1 (CfgWr0/1) | NP |
| Message | P |
| Completion (Cpl, CplD, CplLk) | Cpl |
其中:
内存写与消息是 Posted
读请求与配置/IO 写是 Non-Posted,必须等待 Completion
Completion 是对 NP 请求的响应
一个 TLP 包含:
Header(3DW 或 4DW)
可选数据负载(最多 4096 字节,端点可在配置空间中声明更小)
可选 CRC (TLP Digest, ECRC)
事务层所有 TLP 的头部第一字包含公共字段:
Fmt (Format):
bit0 = 0 → 3DW header,=1 → 4DW header
bit1 = 0 → 无数据,=1 → 有数据
Type:与 Fmt 一起确定具体 TLP 类型
TC (Traffic Class):流量类别,映射到虚拟通道 VC
TD (TLP Digest):=1 表示存在 ECRC
EP (Error Poisoned):=1 表示数据被标记为错误,但包仍被转发(错误转发)
Attr[1:0] (属性):
bit1 = 1 → 放宽顺序 (Relaxed Ordering)
bit0 = 0/1 → 是否需要 Cache 一致性探测 (Snoop)
AT (Address Type):地址翻译类型(Gen2 引入),支持 ATS 扩展
不同 TLP 的 Fmt+Type 编码如下:
| TLP | Fmt/Type | 说明 |
|---|---|---|
| MRd / MRdLk | 00/01 00000 / 00001 | 内存读(32/64位地址,可锁定) |
| MWr | 10/11 00000 | 内存写(32/64位地址) |
| IORd | 00 00010 | IO 读 |
| IOWr | 10 00010 | IO 写 |
| CfgRd0/1 | 00 00100 / 00101 | 配置读 Type 0/1 |
| CfgWr0/1 | 10 00100 / 00101 | 配置写 Type 0/1 |
| Msg/MsgD | 01/11 10rrr | 消息请求(可带数据) |
| Cpl/CplD | 00/10 01010 | Completion (无数据/有数据) |
| CplLk/CplDLk | 00/10 01011 | 对锁定读的 Completion |
MWr (Posted):写入内存,不需要返回
MRd (Non-Posted):读取内存,必须等待 Completion
地址可为 32 位 (3DW Header) 或 64 位 (4DW Header)
可指定字节使能 (Byte Enable),决定哪些字节有效
数据负载最大 4096 字节
若出错,可返回 CplD(带错误状态)
Completion 是对 NP 请求(如 MRd、IORd、CfgRd)的响应:
Cpl:无数据(通常表示错误)
CplD:带数据(正常返回读数据)
CplLk / CplDLk:对锁定事务的响应
Completion 头部包含:
状态码(成功/超时/错误)
字节计数
请求者 ID 与 Tag(标识原始请求)
为兼容 PCI 传统 I/O 地址空间而保留:
IORd / IOWr:均为 NP,必须有 Completion
很多现代系统已减少使用,但规范仍支持
配置空间是 PCIe 设备的寄存器空间,用于:
标识设备 (Vendor ID, Device ID)
广告能力 (Capabilities)
设置参数 (最大负载大小、VC 映射等)
访问方式:
CfgRd0 / CfgWr0:访问本总线的设备 (Type 0)
CfgRd1 / CfgWr1:跨越交换机,访问下游设备 (Type 1)
消息是一类特殊的 TLP,用于系统内的管理与信号传递:
中断消息 (MSI/MSI-X)
电源管理 (PME, PwrMgmt)
错误报告 (ERR_CORR/ERR_FATAL 等)
锁定事务 (Locked Transaction)
插槽功耗限制 (Slot Power Limit)
厂商自定义消息
消息包可以带数据 (MsgD) 或不带数据 (Msg)。
事务层定义了 PCIe 的核心通信模型:
内存/IO/配置 读写访问
Completion 机制保证 NP 请求有响应
消息 用于电源管理、中断、错误信号、扩展功能
Header 中的 Fmt/Type/TC/Attr/AT 等字段控制事务行为
事务层与上层软件/配置空间紧密相关,而其传输则依赖下层数据链路层的可靠传输保障。