第二章:PCIe体系结构——三层架构的职责与协作

2.1 为什么需要三层?

PCIe不是一根简单的总线,它是一套完整的协议栈。我个人刚开始接触时也被吓到——三层?至于吗?后来踩了坑才明白:每一层解决一类问题,缺一不可。

事务层(Transaction Layer)负责“你要干什么”,数据链路层(Data Link Layer)负责“能不能可靠送到”,物理层(Physical Layer)负责“怎么在线上传信号”。你想想看,如果没有数据链路层的重传机制,TLP丢了都不知道,那系统直接死机。

PCIe三层协议栈架构 事务层 生成/解析TLP • 流量控制 • 事务排序 核心职责:应用程序接口(读写请求、完成包) 数据链路层 生成/解析DLLP • 重传机制 • CRC校验 核心职责:可靠传输(ACK/NAK) 物理层 比特流编码 • 链路训练 • 电气特性 核心职责:信号收发与链路初始化 数据流向:上层 → 下层(发送),下层 → 上层(接收) 每一层在发送方向添加头部/尾部,接收方向剥除 TLP在整个路径中不变,但DLLP只在数据链路层之间交换

2.2 事务层——TLP是主角

事务层是离软件最近的一层。你从驱动里发起一个MMIO读,事务层就会把它打包成一个Memory Read TLP。TLP格式我给大家拆开看:

TLP核心字段(DW = 32位)
字段位数说明
Fmt2 bits格式:3DW无数据 / 4DW无数据 / 3DW有数据 / 4DW有数据
Type5 bits事务类型:MRd, MWr, Cpl, CplD 等
TC3 bits流量类别(Traffic Class),用于QoS
TD1 bit是否包含TLP Digest(ECRC)
EP1 bit中毒位(Poisoned),标记数据已损坏
Attr3 bits属性:顺序、宽松排序、ID-based排序
Length10 bits数据负载长度(以DW为单位)
Requester ID16 bits总线|设备|功能(BDF)
Tag8 bits标签,用于匹配请求与完成
Last DW BE4 bits最后一个DW的字节使能
1st DW BE4 bits第一个DW的字节使能
个人习惯:我每次看TLP时,首先瞄Fmt+Type,确认是读还是写、有没有数据。其次是Requester ID + Tag,这是调试时追踪事务的“身份证”。

事务层还负责流量控制(Flow Control)。每个VC(Virtual Channel)有独立的信用量机制。发送方不能随意丢包,得先看接收方有没有足够的接收缓冲区。我曾在项目里遇到一个坑:某个EP的FC信用量公告逻辑写死了,导致RC一直拿不到信用,整条链路卡死——调了三天才发现。

2.3 数据链路层——可靠传输的守护者

数据链路层给TLP加了两样东西:序列号(Sequence Number)和LCRC。收到一个TLP后,链路层校验CRC,如果正确就发一个ACK DLLP回去;如果错误就发NAK,要求重传。这个机制叫Go-Back-N ARQ。

DLLP格式和TLP完全不同。DLLP是固定长度(6 bytes),没有地址概念,只在相邻的两个PCIe设备之间交换。常见的DLLP类型:

  • ACK/NAK:确认/否定确认
  • InitFC1/InitFC2:初始化流量控制信用量
  • UpdateFC:更新信用量
  • PM_*:电源管理相关
  • Vendor_Defined:厂商自定义
避坑指南:我曾经遇到一个测试用例,发送TLP后链路上总是超时。逻辑分析仪一抓,发现DLLP层根本没发ACK——因为物理层链路训练还没完成就发了数据。记住:DLLP的可靠传输依赖于底层的物理层已经处于L0状态。

数据链路层的重传缓冲区大小有上限。PCIe规范规定重传深度至少是3个TLP(Gen1/Gen2),Gen3以后可以更大。但在做FPGA原型验证时,我习惯把缓冲区开到8个TLP,给自己留点余量。

2.4 物理层——信号到底能不能过?

物理层分两部分:逻辑子层和电气子层。逻辑子层做编码(8b/10b或128b/130b)、加扰、弹性缓冲区;电气子层负责差分信号的驱动和接收。物理层最关键的阶段是链路训练状态机(LTSSM):从Detect到Polling,再到Config,最终进入L0正常工作。

物理层还负责链路宽度协商。比如一个x16的设备插到x8的槽上,物理层通过训练自动降宽。我debug时经常在LTSSM的Recovery状态卡住,通常原因是信号完整性差——眼图没睁开。

// 简单物理层状态机片段(伪代码)
state = Detect;
loop {
  switch (state) {
    Detect:   // 检测接收端是否存在
      if (receiver_detected) state = Polling;
      break;
    Polling:  // 位锁定和符号对齐
      if (bit_lock && symbol_lock) state = Config;
      break;
    Config:   // 交换链路宽度和速率
      if (link_width_negotiated) state = L0;
      break;
    L0:       // 正常操作
      // 发送TLP/DLLP
      break;
    Recovery: // 重新训练
      if (recovery_done) state = L0;
      break;
  }
}

2.5 三层协作:一个读事务的完整旅程

我们来看一个简单的Memory Read事务,从RC到EP再到RC:

  1. RC的事务层收到软件发起的读请求,构造一个Memory Read TLP(Fmt=2DW无数据,Type=MRd),加上Requester ID、Tag、地址。然后下发给数据链路层。
  2. 数据链路层在TLP前面加上序列号(比如N),后面加上LCRC。把结果交给物理层。同时把这个TLP放入重传缓冲区(Replay Buffer)。
  3. 物理层对数据做编码(Gen3使用128b/130b),加扰后通过lane发送。接收端物理层做解码、去扰、弹性缓冲,还原出TLP+DLLP边界。
  4. EP的数据链路层收到后,校验LCRC。正确则发ACK DLLP回RC,并剥离序列号和LCRC,把TLP往上送。错误则发NAK,RC收到NAK后会从重传缓冲区重新发送从N开始的所有TLP。
  5. EP的事务层解析TLP,发现是Memory Read,就去访问内部寄存器。然后构造一个Completion with Data TLP(CplD),同样走数据链路层和物理层返回RC。
  6. RC收到CplD后,匹配Tag,把数据返回给软件。

你想想看,每一步出错都可能让整个系统挂掉。我曾经在调试过程中,看到RC发出MRd后一直等不到CplD,最后发现是EP的BAR空间没初始化(分配了地址但没使能)——这在枚举过程中很常见,我们后面会细讲。

2.6 重要对比:TLP vs DLLP

对比项TLPDLLP
长度可变(最小3DW,最大4096DW)固定6 bytes
传输范围端到端(从发送端到接收端)仅相邻设备之间(点对点)
可靠机制依赖数据链路层自身带CRC,且可重传
用途数据/命令/完成链路管理(FC、ACK、电源管理)
序列号无(由数据链路层添加)无
面试常考: 面试官可能问“TLP的序列号在哪一层生成?”答案是数据链路层。事务层根本不关心序列号。另外,ECRC(端到端CRC)是可选的,由事务层添加,但大多数系统不用。

2.7 关于VC与TC的补充

事务层里还有一个VC(Virtual Channel)的概念。实际上物理上有多个独立的虚拟通道,每个VC有自己的一组信用量、重传缓冲区。TC(Traffic Class)是用来标记优先级。你可以配置TC到VC的映射,比如TC0走VC0,TC7走VC1。我建议你在做QoS相关设计时,仔细核对RC和EP的TC/VC映射表是否一致——不一致会导致事务被丢弃。

好了,这一章就把三层的基础过了一遍。下一部分我们会深入到具体的枚举流程——这才是面试真正的重头戏。