PCIe 分层结构:事务层、数据链路层、物理层

你有没有想过,为什么 PCIe 要拆成三层来设计?

说白了,这就像一家公司要有不同部门分工。事务层是销售部,负责跟客户谈业务;数据链路层是物流部,保证货物安全送达;物理层是运输队,负责实际送货。各干各的,互不干扰。

我在项目中遇到过不少工程师,上来就抓着物理层信号量来量,链路都还没训练完就怀疑硬件坏了。其实很多时候问题出在上层——事务层配置错了,或者数据链路层的重播缓冲溢出了。不理解分层结构,调试就像无头苍蝇。

1. 为什么 PCIe 要分层设计?

分层设计最大的好处是职责分离。每一层只关心自己的事,层与层之间通过明确定义的接口通信。你想想看,如果物理层要同时管信号质量和事务排序,那代码得多乱?

  • 事务层:只负责生成和解析 TLP(事务层包),不管信号怎么传。
  • 数据链路层:只负责可靠性,加 CRC、序列号,做重播。
  • 物理层:只负责把比特流发出去,做串并转换。

这样设计还有一个好处——某一层升级了,其他层不用动。比如从 PCIe 3.0 升到 4.0,物理层编码方式变了,但事务层的 TLP 格式基本没变。

我个人习惯:调试 PCIe 问题时,先看物理层链路是否训练完成,再看数据链路层的 Ack/Nak 是否有异常,最后才查事务层的 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 才彻底弄明白。

我曾经踩过一个坑:TLP 的 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 错误,但重播机制总能自动恢复。要不是数据链路层扛住了,上层应用早就崩了。说它是"守护者"一点也不为过。

核心要点:数据链路层对上层是透明的。事务层完全不知道下面发生了重播或 CRC 错误。只要数据链路层能恢复,上层就当什么都没发生过。这大大简化了软件设计。

4. 物理层——信号的"搬运工"

物理层在最底层,负责把比特流实打实地发出去。它又细分为逻辑子层和电气子层。

子层 职责 关键操作
逻辑子层 数据编码、加扰、链路训练 8b/10b 或 128b/130b 编码、加扰/解扰、LTSSM 状态机
电气子层 差分信号驱动、时钟恢复 串行/解串、去加重、接收均衡

逻辑子层负责把数据链路层送来的数据做编码和加扰,防止电磁干扰太强。电气子层负责把编码后的比特流变成高速差分信号,在 PCIe 总线上飞。

你想想看,PCIe 4.0 的单 Lane 速率已经到 16 GT/s,电气子层的压力有多大。信号抖动、串扰、损耗,任何一个环节出问题,物理层就训练不过去。

物理层的 LTSSM(链路训练和状态机) 负责链路的初始化、训练、电源管理。从 Detect 到 Polling 到 L0,每一步都要握手确认。这个我们在后面章节会细讲。

特别注意:物理层训练失败时,数据链路层和事务层都拿不到链路状态,整个设备就像"死"了一样。我建议你在板卡调试时,先确保参考时钟干净、差分对等长,再去看物理层是否能进入 L0 状态。

5. 三层如何协作?

数据在 PCIe 链路中传输时,从上层到下层层层封装,接收时再层层解封。流程大概是这样的:

  1. 事务层:生成 TLP,填入头信息(类型、地址、长度、请求者 ID 等)。
  2. 数据链路层:在 TLP 前后加上序列号和 LCRC,形成 DLLP 帧。同时存一份到重播缓冲。
  3. 物理层:对 DLLP 帧做编码、加扰,转换成串行比特流,通过差分线发出去。
  4. 接收端物理层:解串、解码、解扰,还原出 DLLP 帧。
  5. 接收端数据链路层:检查 LCRC 和序列号,发 Ack 确认,去掉序列号和 CRC,交给事务层。
  6. 接收端事务层:解析 TLP 头,执行读/写操作。

下面这张图展示了三层结构以及数据流动的关系:

PCIe 分层结构与数据流动 事务层 TLP 生成 / 解析 事务管理 · 流量类别 数据链路层 序列号 · LCRC · Ack/Nak 重播缓冲 · 流控 物理层 编码/解码 · 加扰/解扰 串并转换 · 链路训练 TLP 事务层包(含头+数据) DLLP 数据链路层包(Ack/Nak/流控) PHY Frame 物理层帧(编码后比特流) 数据发送:事务层 → 数据链路层 → 物理层 → 差分总线

从图中可以看出,每一层只跟相邻层交互。事务层从不直接碰物理层的寄存器,物理层也看不懂 TLP 头里的地址。这种逐层封装的设计,让代码维护和协议升级都变得简单。

一句话总结三层的关系:事务层决定"干什么",数据链路层保证"不出错",物理层负责"发出去"。各司其职,缺一不可。

6. 三层对比速查表

层级 核心职责 数据单元 关键机制
事务层 TLP 生成/解析、事务管理 TLP 事务类型、流量类别、标签管理
数据链路层 可靠性保证、错误检测与恢复 DLLP Ack/Nak、LCRC、重播缓冲
物理层 信号传输、链路训练、编解码 PHY Frame 8b/10b 或 128b/130b 编码、LTSSM

这张表建议你贴在工作台旁边。遇到 PCIe 问题,先定位问题出在哪个层,再翻对应层的文档,能省下不少时间。

我建议:在设计 PCIe 设备时,每一层都留出调试接口。事务层可以输出 TLP 计数,数据链路层可以暴露 CRC 错误统计,物理层可以读取链路状态寄存器。这样量产测试时,哪一层出问题一目了然。

嗯,关于分层结构就讲到这里。理解了这三层各自干什么、怎么配合,后面学链路训练和 TLP 路由就会轻松很多。