3. PCIe协议基础(二):事务层详解

好,咱们接着聊PCIe的事务层。这一层说白了,就是PCIe协议里最核心、最贴近咱们工程师日常打交道的地方。你写驱动、调IP、抓链路数据,最后分析来分析去,十有八九都是在跟事务层打交道。

我个人习惯把事务层理解成「快递公司」——它不关心你的包裹是怎么装箱的(数据链路层的事),也不关心卡车走哪条路(物理层的事),它只管一件事:包裹的格式对不对,地址写没写错,以及快递员(流量控制)有没有超载

3.1 TLP格式:快递单长什么样?

TLP,全称Transaction Layer Packet,就是事务层的报文。每个TLP就像一张快递单,上面写满了收发信息。我刚开始学PCIe时,总觉得TLP格式很复杂,后来发现只要抓住几个关键字段就行。

一个典型的TLP由三部分组成:

  • Header(报头):12或16字节,包含事务类型、地址、长度、标签等信息
  • Data Payload(数据负载):0~4096字节,可选
  • Digest(摘要):4字节,可选,用于端到端CRC校验

嗯,这里要注意,不是所有TLP都有数据负载。比如配置读写请求,有时候就只带个Header,不带数据。我在项目中就踩过这个坑——抓包时发现TLP长度不对,后来才意识到是「无数据」的读请求。

核心字段速查表(32位地址模式)

字段位宽说明
Fmt2 bits格式:00=无数据,01=有数据,10=4DW Header,11=6DW Header
Type5 bits事务类型:MRd(读请求)、MWr(写请求)、Cpl(完成)等
TC3 bits流量类别,用于QoS
TD1 bit是否带Digest
EP1 bit毒包标记,置1表示数据无效
Attr2 bits属性:顺序模型、缓存策略等
Length10 bits数据负载长度,单位是DW(4字节)
Requester ID16 bits请求者总线号+设备号+功能号
Tag8 bits标签,用于匹配请求和完成
Address32/64 bits目标地址

你看,其实核心字段就那么十来个。我当年背这些字段时,最喜欢用「Fmt+Type」的组合来判断事务类型。比如Fmt=00, Type=00000就是存储器读请求(MRd),Fmt=01, Type=00000就是带数据的存储器写请求(MWr)。

3.2 地址空间:四种门牌号

PCIe定义了四种地址空间,每种空间都有自己的「门牌号」规则。你想想看,如果地址空间搞混了,数据就会送到别人家去。

  1. 存储器空间(Memory Space):最常用,32位或64位地址,用于DMA、BAR映射等
  2. IO空间(IO Space):32位地址,主要用于兼容老设备,新设计中越来越少
  3. 配置空间(Configuration Space):用于设备枚举和初始化,地址格式为Bus:Device:Function
  4. 消息空间(Message Space):没有地址,靠Message Code区分,比如中断、错误通知

避坑指南:我曾经在一个项目中,把IO空间的读写请求误当成存储器空间来处理。结果设备枚举时总是报错,查了两天才发现是地址空间类型配错了。记住:TLP Header里的Type字段决定了地址空间,不是地址值本身

3.3 读写请求:快递怎么发?

读写请求是TLP里最常见的两种事务。我习惯把它们类比成「寄快递」和「查快递」。

3.3.1 存储器读请求(MRd)

读请求不带数据负载。发起者发一个MRd TLP,目标设备收到后,返回一个Completion(Cpl)TLP,里面带上读到的数据。

// 读请求TLP示例(32位地址)
Header: 
  Fmt=00, Type=00000  // MRd, 无数据
  Length=01            // 读1个DW(4字节)
  Requester ID=0x0100  // Bus 1, Dev 0, Func 0
  Tag=0x5A             // 标签
  Address=0x8000_0000  // 目标地址

// 完成TLP示例
Header:
  Fmt=01, Type=01010  // CplD, 带数据
  Length=01           // 返回1个DW
  Requester ID=0x0100 // 匹配请求者
  Tag=0x5A            // 匹配标签
  Data=0xDEAD_BEEF    // 读到的数据

这里有个关键点:Tag必须唯一。如果两个未完成的读请求用了同一个Tag,返回的Completion就不知道是谁的了。我见过一个案例,某IP的Tag管理有bug,导致高负载下数据错乱,最后定位到是Tag复用问题。

3.3.2 存储器写请求(MWr)

写请求带数据负载,而且不需要Completion。这就是所谓的「Posted事务」——发出去就不管了,目标设备必须自己保证写完成。

// 写请求TLP示例(64位地址)
Header:
  Fmt=10, Type=00000  // MWr, 64位地址, 带数据
  Length=04           // 写4个DW(16字节)
  Address=0x0000_0001_0000_0000
Data:
  DW0=0x1234_5678
  DW1=0x9ABC_DEF0
  DW2=0x1111_2222
  DW3=0x3333_4444

注意:写请求虽然不需要Completion,但目标设备如果发现地址错误或权限不足,会通过错误消息通知发起者。所以别以为写请求就真的「无后顾之忧」了。

3.4 流量控制机制:别让快递员累趴下

流量控制(Flow Control, FC)是事务层一个非常巧妙的设计。它的目的很简单:防止发送方把接收方撑爆

你想想看,PCIe链路是双向的,每个方向都有独立的发送和接收缓冲区。如果发送方不管不顾地猛发TLP,接收方的缓冲区很快就会满,然后丢包、重传,性能一落千丈。

PCIe的流量控制是怎么做的?它用了信用制(Credit-based)。说白了就是:

  • 每个接收方告诉发送方:「我有N个缓冲单元(Credit),你可以发这么多数据」
  • 发送方每发一个TLP,就消耗掉对应的Credit
  • 接收方处理完数据后,返还Credit
  • 发送方如果Credit用完了,就必须停下来等

这里有个细节:Credit分两种——Posted Credit(用于写请求和消息)和Non-Posted Credit(用于读请求和Completion)。为什么这么分?因为Posted事务不需要回复,Non-Posted事务需要回复,两者的缓冲区管理策略不同。

流量控制初始化流程

  1. 链路训练完成后,设备进入FC Init状态
  2. 每个设备发送FC Init DLLP,宣告自己的初始Credit
  3. 双方交换FC Init信息后,进入FC Update状态
  4. 正常工作时,通过FC Update DLLP动态调整Credit

我记得第一次调PCIe IP时,发现链路训练通过了,但就是发不出数据。抓了DLLP一看,原来是FC Init阶段Credit没交换成功。后来查spec才发现,FC Init必须在数据链路层准备好之后才能进行,顺序搞反了就会卡住。

3.5 知识体系总览

说了这么多,咱们用一张图来总结事务层的核心逻辑。这张图是我自己画的,把TLP格式、地址空间、读写请求、流量控制串在了一起。

PCIe事务层核心知识体系 TLP格式 地址空间 读写请求 流量控制 Header + Data + Digest Fmt/Type/Length/Tag Memory / IO / Config Message 空间 MRd / MWr / Cpl Posted vs Non-Posted Credit-based机制 FC Init / FC Update 事务层核心逻辑 TLP封装数据 → 选择地址空间 → 发起读写请求 → 流量控制确保不溢出 每一层都依赖下一层,环环相扣

这张图把事务层的四个核心模块串在了一起。你从左往右看,就是一次完整的事务处理流程:先封装TLP,再选地址空间,然后发起读写请求,最后靠流量控制保证链路不崩溃。

个人经验:我建议你在调试PCIe IP时,先抓一份TLP log,对照着这张图逐字段分析。看多了,你就能一眼看出问题出在哪个环节。比如TLP格式不对,多半是Header拼错了;读写请求没响应,可能是Tag管理出问题了;链路卡住不动,十有八九是流量控制Credit耗尽了。

好了,事务层的内容就讲到这里。记住:TLP格式是基础,地址空间是门牌,读写请求是动作,流量控制是保障。把这四个点吃透了,PCIe协议你就掌握了七成。


专注资料整理