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位地址模式)
| 字段 | 位宽 | 说明 |
|---|---|---|
| Fmt | 2 bits | 格式:00=无数据,01=有数据,10=4DW Header,11=6DW Header |
| Type | 5 bits | 事务类型:MRd(读请求)、MWr(写请求)、Cpl(完成)等 |
| TC | 3 bits | 流量类别,用于QoS |
| TD | 1 bit | 是否带Digest |
| EP | 1 bit | 毒包标记,置1表示数据无效 |
| Attr | 2 bits | 属性:顺序模型、缓存策略等 |
| Length | 10 bits | 数据负载长度,单位是DW(4字节) |
| Requester ID | 16 bits | 请求者总线号+设备号+功能号 |
| Tag | 8 bits | 标签,用于匹配请求和完成 |
| Address | 32/64 bits | 目标地址 |
你看,其实核心字段就那么十来个。我当年背这些字段时,最喜欢用「Fmt+Type」的组合来判断事务类型。比如Fmt=00, Type=00000就是存储器读请求(MRd),Fmt=01, Type=00000就是带数据的存储器写请求(MWr)。
3.2 地址空间:四种门牌号
PCIe定义了四种地址空间,每种空间都有自己的「门牌号」规则。你想想看,如果地址空间搞混了,数据就会送到别人家去。
- 存储器空间(Memory Space):最常用,32位或64位地址,用于DMA、BAR映射等
- IO空间(IO Space):32位地址,主要用于兼容老设备,新设计中越来越少
- 配置空间(Configuration Space):用于设备枚举和初始化,地址格式为Bus:Device:Function
- 消息空间(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事务需要回复,两者的缓冲区管理策略不同。
流量控制初始化流程
- 链路训练完成后,设备进入FC Init状态
- 每个设备发送FC Init DLLP,宣告自己的初始Credit
- 双方交换FC Init信息后,进入FC Update状态
- 正常工作时,通过FC Update DLLP动态调整Credit
我记得第一次调PCIe IP时,发现链路训练通过了,但就是发不出数据。抓了DLLP一看,原来是FC Init阶段Credit没交换成功。后来查spec才发现,FC Init必须在数据链路层准备好之后才能进行,顺序搞反了就会卡住。
3.5 知识体系总览
说了这么多,咱们用一张图来总结事务层的核心逻辑。这张图是我自己画的,把TLP格式、地址空间、读写请求、流量控制串在了一起。
这张图把事务层的四个核心模块串在了一起。你从左往右看,就是一次完整的事务处理流程:先封装TLP,再选地址空间,然后发起读写请求,最后靠流量控制保证链路不崩溃。
个人经验:我建议你在调试PCIe IP时,先抓一份TLP log,对照着这张图逐字段分析。看多了,你就能一眼看出问题出在哪个环节。比如TLP格式不对,多半是Header拼错了;读写请求没响应,可能是Tag管理出问题了;链路卡住不动,十有八九是流量控制Credit耗尽了。
好了,事务层的内容就讲到这里。记住:TLP格式是基础,地址空间是门牌,读写请求是动作,流量控制是保障。把这四个点吃透了,PCIe协议你就掌握了七成。