事务层基础:TLP 是一切通信的起点

大家好,欢迎来到第 3 章。今天聊的是 PCIe 协议里最核心的部分——事务层。我个人一直觉得,搞懂了事务层,就等于抓住了 PCIe 的“魂”。

TLP 包格式:数据要穿什么外衣?

TLP,全称是 Transaction Layer Packet。说白了,TLP 就是 PCIe 设备之间互相传递的“消息信封”。所有的读、写、回应,都封装在这个包里。

一个标准的 TLP 包长什么样?我大致拆成三个部分:

  • 起始 + 序列号(可选):数据链路层关心这部分,事务层先放一放。
  • TLP 前缀(可选):用于携带一些新特性,比如 TLP Processing Hints (TPH),刚接触时可以暂时忽略。
  • TLP 头(Header):这是灵魂。格式、长度、事务类型、地址……全在这里。
  • 数据负载(Data Payload):真正要传输的数据。写操作要带数据,读请求通常不带。
  • ECRC(可选):端到端 CRC,对可靠性要求极高的场景才启用。

你想想看,几十个字段怎么记?我个人习惯只记住前 6 个字节,它们决定了包的类型和走向。

Byte Offset 字段名 说明
0 Fmt[1:0] 格式。决定 Header 是 3DW 还是 4DW,有没有数据负载
0 Type[4:0] 事务类型。Memory Read? Write? 还是 Completion?
1 TC[2:0] Traffic Class。用于 QoS,优先级区分
2-3 Length[9:0] 数据负载长度,单位是 Dword(4字节)
4-7 Requester ID / Tag 谁发的请求,标签是多少,用于匹配请求和完成
8-15 或更长 地址 / 其他信息 Memory/IO/Configuration 地址,或者 Completion 的状态

核心记住:Fmt+Type 决定了“这是啥事”;Length 决定了“这事有多大”。

我在项目中调试过几次链路不通,最后发现都是 Header 里的 Length 字段算错了,导致接收方等不到数据直接超时。嗯,这里要注意,Length 是 Dword 为单位,不是字节。

地址空间:Memory、IO、Configuration

PCIe 协议定义了三种地址空间。为什么有三种?我自己的理解是:各自干各自的活,避免混乱。

  • Memory Space:的主角。不管是显卡、NVMe 硬盘还是网卡,它们把内部寄存器映射到系统的 Memory 地址上。CPU 直接读写就跟访问内存一样。最常用,性能最高。
  • IO Space:x86 体系的老传统。现在基本上快消失了。我最近几年做的新项目里,IO 读写几乎绝迹。但是兼容旧设备时还会遇到。
  • Configuration Space:每个 PCIe 设备必备。开机关机、检测设备、分配资源,都靠它。只有 4KB(通过 ECAM 机制扩展),配置周期专用。

讲到这里,你可能会问:为什么 IO 地址空间会被淘汰?它其实有硬伤:地址宽度只有 16 位(64KB),而且访问路径比 Memory 要绕。每次 IO 读写的原子性和排序规则都有特殊要求,性能限制很大。

我最开始做调试时,老盯着 Memory 读写,后来发现设备根本不枚举——原来是 Configuration 空间的访问路径没配置对。先搞定 Config Space,其他空间才有机会通信。

事务类型:MRd、MWr、Cpl、CplD

事务层的核心,就是几个基本操作的来回传递。记住这张表,你基本就掌握一半了。

缩写 全称 方向 说明
MRd Memory Read Request Requester → Completer 读内存空间的请求。Header 里带地址,无数据负载
MWr Memory Write Request Requester → Completer 写内存空间的请求。Header 带地址,后面跟着数据
Cpl Completion without Data Completer → Requester 用于回应那些不需要返回数据的操作,比如 IO 写完成的确认
CplD Completion with Data Completer → Requester 读操作的核心回应。携带读回来的数据

我当年刚开始做设计时,总觉得事务模型很复杂。后来想明白一个道理——把我自己想象成“寄明信片”。MRd 是“我给你寄一张空明信片,写着‘请把地址 0x1000 的数据寄回来’”;CplD 就是对方写满内容回寄给你。MWr 则是直接把写好的明信片塞给对方。

还有一个容易踩的坑:Completion 必须和 Request 一一对应。Requester ID + Tag 就是“暗号”。我曾经遇到过两次读到一样的 Tag,导致 Completion 把数据送错了请求者,系统瞬间乱了套。

千万不要忽略 Requester ID 和 Tag 的唯一性校验。我亲自见过 Tag 复用导致死锁,查了一整天才定位到是软件分配 Tag 的 Bug。

还是得画张图

前面讲了不少文字,我画一张图帮大家串起来。这张图涵盖了地址空间、事务类型以及 TLP 的封装层次关系,你扫一眼应该就很清楚了。

PCIe 事务层核心结构图 PCIe 事务层核心结构 应用/软件层 地址空间选择 Memory 空间 IO 空间 Configuration 空间 事务类型:MRd | MWr | Cpl | CplD TLP 包:起始 → 前缀 → TLP Header → Data Payload → ECRC Header 关键字段:Fmt + Type (决定“干啥事”) | Length (决定“干多少”) | Requester ID + Tag (决定“找谁对账”) 底层 → 数据链路层 → 物理层 → 链路传输

从这张图可以很清楚地看到,上层应用发出的请求,先通过地址空间选择,再映射为具体的事务类型,最后封装成标准的 TLP 包向下传递。事务层并不关心信号怎么在铜线上跑,它只负责“说什么事”和“怎么组织这件事”。

今天的章节量不小,但请记住一个总的原则:PCIe 事务层本质上是一种“请求-完成”模型。你发出 MRd,就会收到 CplD;你发出 MWr,对方默默写完后,可以选择给你一个 Cpl 确认。把这个循环刻进脑子里,后面学流控、学 VC、学死锁都会轻松许多。

我个人建议,学完事务层后马上找一块 FPGA 开发板,抓一次真实的 PCIe TLP 包看看。看一眼 Length 字段、看一眼 Tag 字段,比背十遍表格都管用。