PCIe 分层结构:事务层、数据链路层、物理层
你有没有想过,为什么 PCIe 要拆成三层来设计?
说白了,这就像一家公司要有不同部门分工。事务层是销售部,负责跟客户谈业务;数据链路层是物流部,保证货物安全送达;物理层是运输队,负责实际送货。各干各的,互不干扰。
我在项目中遇到过不少工程师,上来就抓着物理层信号量来量,链路都还没训练完就怀疑硬件坏了。其实很多时候问题出在上层——事务层配置错了,或者数据链路层的重播缓冲溢出了。不理解分层结构,调试就像无头苍蝇。
1. 为什么 PCIe 要分层设计?
分层设计最大的好处是职责分离。每一层只关心自己的事,层与层之间通过明确定义的接口通信。你想想看,如果物理层要同时管信号质量和事务排序,那代码得多乱?
- 事务层:只负责生成和解析 TLP(事务层包),不管信号怎么传。
- 数据链路层:只负责可靠性,加 CRC、序列号,做重播。
- 物理层:只负责把比特流发出去,做串并转换。
这样设计还有一个好处——某一层升级了,其他层不用动。比如从 PCIe 3.0 升到 4.0,物理层编码方式变了,但事务层的 TLP 格式基本没变。
2. 事务层——芯片的"外交官"
事务层在最顶层,直接跟软件和 DMA 引擎打交道。它的核心工作是生成和解析 TLP(Transaction Layer Packet)。
你可以把 TLP 想象成一封信。事务层决定信里写什么、寄给谁、要什么回执。至于信怎么送到、路上会不会丢,它不管——那是下层的事。
TLP 主要分几种类型:
- MRd(Memory Read):读内存请求。
- MWr(Memory Write):写内存请求。
- Cpl / CplD(Completion / Completion with Data):对读请求的完成响应。
- Msg(Message):中断、错误上报等消息。
每种 TLP 都有固定的头格式。下面是一个简化版 TLP 头结构(C 语言定义),方便你理解它的组成:
/* 简化版 TLP 头结构 (3DW 头) */
struct tlp_header {
unsigned int fmt_type : 8; /* [7:0] 格式+类型 */
unsigned int tc : 3; /* [10:8] 流量类别 */
unsigned int td : 1; /* [11] TLP 摘要位 */
unsigned int ep : 1; /* [12] 中毒位 */
unsigned int attr : 2; /* [14:13] 属性 */
unsigned int rsvd : 1; /* [15] 保留 */
unsigned int length : 10; /* [25:16] 数据长度(DW) */
uint16_t requester_id; /* 请求者 Bus:Dev:Func */
uint8_t tag; /* 标签 */
uint8_t last_be; /* 最后 DW 字节使能 */
uint8_t first_be; /* 首 DW 字节使能 */
};
fmt_type 决定了这条 TLP 是读还是写,是请求还是完成。requester_id 标明谁发起的。我当年刚接触时,经常搞混 tc 和 attr 的作用,后来在项目里调了一次 QoS 才彻底弄明白。
length 字段以 DW(4字节)为单位,不是字节!如果你要读 64 字节,length 得填 16。填错了,数据链路层就直接丢弃了,连个报错都没有。
3. 数据链路层——可靠性的守护者
数据链路层夹在中间,干的是"脏活累活"。它负责保证 TLP 在发送端和接收端之间可靠传输。
它的核心机制包括:
- 序列号:每个 TLP 发送时都带一个 12 位序列号,接收端用来检测丢包。
- LCRC(链路 CRC):32 位 CRC,覆盖整个 TLP,检测传输错误。
- Ack / Nak 协议:接收端收到 TLP 后,回 Ack 确认;如果发现错误,回 Nak 要求重发。
- 重播缓冲:发出去的 TLP 先存一份在缓冲区,等收到 Ack 才丢弃。收到 Nak 就从缓冲里重发。
数据链路层自己也会发一种包,叫 DLLP(Data Link Layer Packet)。DLLP 用于管理链路状态,比如 Ack/Nak、电源管理、流控等。它不经过事务层,直接在本层处理。
我记得有一次在项目中,发现链路偶尔出现 CRC 错误,但重播机制总能自动恢复。要不是数据链路层扛住了,上层应用早就崩了。说它是"守护者"一点也不为过。
4. 物理层——信号的"搬运工"
物理层在最底层,负责把比特流实打实地发出去。它又细分为逻辑子层和电气子层。
| 子层 | 职责 | 关键操作 |
|---|---|---|
| 逻辑子层 | 数据编码、加扰、链路训练 | 8b/10b 或 128b/130b 编码、加扰/解扰、LTSSM 状态机 |
| 电气子层 | 差分信号驱动、时钟恢复 | 串行/解串、去加重、接收均衡 |
逻辑子层负责把数据链路层送来的数据做编码和加扰,防止电磁干扰太强。电气子层负责把编码后的比特流变成高速差分信号,在 PCIe 总线上飞。
你想想看,PCIe 4.0 的单 Lane 速率已经到 16 GT/s,电气子层的压力有多大。信号抖动、串扰、损耗,任何一个环节出问题,物理层就训练不过去。
物理层的 LTSSM(链路训练和状态机) 负责链路的初始化、训练、电源管理。从 Detect 到 Polling 到 L0,每一步都要握手确认。这个我们在后面章节会细讲。
5. 三层如何协作?
数据在 PCIe 链路中传输时,从上层到下层层层封装,接收时再层层解封。流程大概是这样的:
- 事务层:生成 TLP,填入头信息(类型、地址、长度、请求者 ID 等)。
- 数据链路层:在 TLP 前后加上序列号和 LCRC,形成 DLLP 帧。同时存一份到重播缓冲。
- 物理层:对 DLLP 帧做编码、加扰,转换成串行比特流,通过差分线发出去。
- 接收端物理层:解串、解码、解扰,还原出 DLLP 帧。
- 接收端数据链路层:检查 LCRC 和序列号,发 Ack 确认,去掉序列号和 CRC,交给事务层。
- 接收端事务层:解析 TLP 头,执行读/写操作。
下面这张图展示了三层结构以及数据流动的关系:
从图中可以看出,每一层只跟相邻层交互。事务层从不直接碰物理层的寄存器,物理层也看不懂 TLP 头里的地址。这种逐层封装的设计,让代码维护和协议升级都变得简单。
6. 三层对比速查表
| 层级 | 核心职责 | 数据单元 | 关键机制 |
|---|---|---|---|
| 事务层 | TLP 生成/解析、事务管理 | TLP | 事务类型、流量类别、标签管理 |
| 数据链路层 | 可靠性保证、错误检测与恢复 | DLLP | Ack/Nak、LCRC、重播缓冲 |
| 物理层 | 信号传输、链路训练、编解码 | PHY Frame | 8b/10b 或 128b/130b 编码、LTSSM |
这张表建议你贴在工作台旁边。遇到 PCIe 问题,先定位问题出在哪个层,再翻对应层的文档,能省下不少时间。
嗯,关于分层结构就讲到这里。理解了这三层各自干什么、怎么配合,后面学链路训练和 TLP 路由就会轻松很多。