事务层基础: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 的封装层次关系,你扫一眼应该就很清楚了。
从这张图可以很清楚地看到,上层应用发出的请求,先通过地址空间选择,再映射为具体的事务类型,最后封装成标准的 TLP 包向下传递。事务层并不关心信号怎么在铜线上跑,它只负责“说什么事”和“怎么组织这件事”。
今天的章节量不小,但请记住一个总的原则:PCIe 事务层本质上是一种“请求-完成”模型。你发出 MRd,就会收到 CplD;你发出 MWr,对方默默写完后,可以选择给你一个 Cpl 确认。把这个循环刻进脑子里,后面学流控、学 VC、学死锁都会轻松许多。
我个人建议,学完事务层后马上找一块 FPGA 开发板,抓一次真实的 PCIe TLP 包看看。看一眼 Length 字段、看一眼 Tag 字段,比背十遍表格都管用。