第3章 TLP包格式详解:Memory Read/Write TLP、Completion TLP、Message TLP的格式与字段解析
好,咱们今天来啃一块硬骨头——TLP包的格式。说实话,很多工程师做了好几年PCIe,一说到TLP包头里每个bit是干嘛的,还是有点含糊。我个人觉得,搞懂TLP格式是PCIe通信的“内功”,内功练好了,后面调链路、抓bug才会顺手。
咱们这节课重点讲三类最常用的TLP:Memory读写、Completion完成包、还有Message消息包。我会把每个字段掰开揉碎了讲,再配上我实际项目中踩过的坑。
3.1 TLP包的整体结构
先看个全景。一个完整的TLP包长这样:
+----------------+----------------+----------------+----------------+
| 起始(1B) | 序列号(2B) | 包头(12B/16B) | 数据负载(0-4KB)|
+----------------+----------------+----------------+----------------+
| ECRC(4B) | LCRC(4B) | 结束(1B) | |
+----------------+----------------+----------------+----------------+
嗯,这里要注意:起始、序列号、LCRC、结束这些是数据链路层加的,事务层只负责构造包头+数据+ECRC。我刚开始学的时候老是把这两层搞混,后来画了个分层图才理清楚。
核心要点:事务层只关心“我要发什么数据”,链路层关心“怎么可靠地发过去”。调试时抓到的包,通常是完整的数据链路层帧。
3.2 Memory Read TLP格式
Memory Read,说白了就是“读内存”。RC(根复合体)或者EP(端点)都可以发起。格式如下:
Byte 0: Fmt(2b) | Type(5b) | R(1b)
Byte 1: TC(3b) | R(5b) | Attr(2b) | R(1b) | TH(1b) | R(1b) | TD(1b) | EP(1b) | Attr(1b) | R(1b)
Byte 2-3: Length(10b) | R(6b)
Byte 4-7: Requester ID(16b) | Tag(8b) | Last DW BE(4b) | First DW BE(4b)
Byte 8-11: Address[63:32] (仅64位寻址时存在)
Byte 12-15: Address[31:2] | R(2b)
字段太多记不住?我教你个笨办法:先记住前4个关键字段——Fmt/Type、Length、Requester ID、Address。剩下的用到再查。
说说我踩过的坑。有一次调试FPGA与CPU通信,CPU发了个Memory Read请求,FPGA死活不响应。抓包一看,Tag字段全是0。原来PCIe规范要求Tag不能重复,我那个老代码里Tag没做递增,CPU以为所有请求都是同一个,直接丢弃了。从那以后,我习惯在代码里加个Tag计数器,每发一个请求就加1。
避坑指南:我曾经在Memory Read请求中忘记设置First DW BE和Last DW BE,结果读回来的数据全是乱的。这两个字段决定了你要读哪些字节,千万别默认填0。
3.3 Memory Write TLP格式
Memory Write和Read的包头结构基本一样,区别在于:
- Fmt字段:Read是00或01,Write是10或11
- Write必须带数据负载,Read不带
- Write不需要Completion,属于Posted事务
格式对比:
| 字段 | Memory Read (32位) | Memory Write (32位) |
|---|---|---|
| Fmt | 00 (无数据) | 10 (有数据) |
| Type | 00000 | 00000 |
| Length | 要读取的DW数 | 要写入的DW数 |
| 数据负载 | 无 | 有 (跟在包头后) |
你想想看,Write不需要回应,所以性能比Read好很多。我做过一个高速数据采集项目,所有控制寄存器都用Write操作,只有读数据时才用Read。这样CPU不用等回应,吞吐量直接翻倍。
注意:Memory Write是Posted事务,意味着发送端不保证接收端已经处理完。如果你需要确认写入完成,要么读回验证,要么用Message中断通知。
3.4 Completion TLP格式
Completion是PCIe里最容易被忽视的包。很多人以为发完Read请求就完事了,其实Read请求必须对应一个Completion,否则发起方会一直等,直到超时。
Completion的包头:
Byte 0: Fmt(2b) | Type(5b) | R(1b)
Byte 1: TC(3b) | R(5b) | Attr(2b) | R(1b) | TH(1b) | R(1b) | TD(1b) | EP(1b) | Attr(1b) | R(1b)
Byte 2-3: Length(10b) | R(6b)
Byte 4-7: Completer ID(16b) | Completion Status(3b) | BCM(1b) | Byte Count(12b)
Byte 8-11: Requester ID(16b) | Tag(8b) | R(1b) | Lower Address(7b)
Byte 12-15: 数据负载 (可选)
这里有个关键字段——Completion Status:
- 000:成功完成(SC)
- 001:不支持的请求(UR)
- 010:配置请求重试(CRS)
- 100:完成者异常(CA)
我记得有一次调试,FPGA老是返回UR状态。查了半天,原来是Completer ID没配对。FPGA的Bus/Device/Function号必须和配置空间里的一致,否则CPU不认。
经验之谈:Completion里的Tag必须和对应的Request Tag一致。我习惯在FPGA里维护一个“未完成请求表”,收到Completion后查表匹配,超时未匹配的就报错。
3.5 Message TLP格式
Message TLP是PCIe里最灵活的包,它不访问内存,而是传递“消息”。比如中断、错误通知、电源管理事件等。
Message的包头很特殊:
Byte 0: Fmt(2b) | Type(5b) | R(1b)
Byte 1: TC(3b) | R(5b) | Attr(2b) | R(1b) | TH(1b) | R(1b) | TD(1b) | EP(1b) | Attr(1b) | R(1b)
Byte 2-3: Length(10b) | R(6b)
Byte 4-7: Requester ID(16b) | Tag(8b) | Message Code(8b)
Byte 8-11: Message Data (可选)
注意看,Type字段是10xxx,和Memory/IO/Config都不一样。Message Code决定了消息类型:
| Message Code | 含义 | 说明 |
|---|---|---|
| 0x10 | INTx中断 | 传统中断信号 |
| 0x20-0x2F | PME电源管理事件 | 唤醒、状态变化等 |
| 0x30 | 错误消息 | ERR_COR、ERR_NONFATAL等 |
| 0x40-0x4F | 厂商自定义消息 | 可以自己定义用途 |
我项目中常用的是厂商自定义消息。比如FPGA处理完一批数据后,发一个Message给CPU,CPU收到后就知道可以来取数据了。这比用中断线灵活多了。
小技巧:Message TLP是Posted事务,不需要Completion。但如果你需要确认对方收到了,可以用Vendor Defined Message带个序列号,对方回一个带相同序列号的Message确认。
3.6 三种TLP的对比总结
最后做个对比,方便你记忆:
| 特性 | Memory Read | Memory Write | Completion | Message |
|---|---|---|---|---|
| 事务类型 | Non-Posted | Posted | Non-Posted | Posted |
| 是否需要回应 | 是(Completion) | 否 | 否 | 否 |
| 数据负载 | 无 | 有 | 有(返回数据) | 可选 |
| 典型用途 | 读寄存器/内存 | 写寄存器/内存 | 返回读结果 | 中断/错误/自定义 |
嗯,到这里TLP格式就讲完了。说实话,这些字段光靠背是记不住的,最好的办法是抓包看。我每次调PCIe都会开逻辑分析仪,对着波形看每个字段,看多了自然就熟了。
下一章咱们讲TLP的路由机制,看看包是怎么在PCIe拓扑里找到目的地的。到时候你会明白,为什么Requester ID和Completer ID这么重要。