第4章:PCIe事务层:TLP包结构详解
事务层,说白了就是PCIe协议栈里最贴近用户的那一层。你写驱动、做DMA、发中断,最终都要转化成事务层包(TLP)在链路上跑。我刚开始接触PCIe时,总觉得TLP结构很复杂,后来发现只要抓住几个关键字段,其实没那么玄乎。
4.1 TLP包的基本骨架
一个完整的TLP包,由三部分组成:Header、Data Payload、可选的ECRC。嗯,这里要注意,不是所有TLP都有数据负载,比如配置读写就没有。
我习惯把TLP想象成一封信:
- Header:信封上的地址和说明(谁发的、发给谁、干什么)
- Data Payload:信纸里的内容(要写的数据)
- ECRC:信封上的防伪标签(可选,但建议开启)
Header长度分两种:3DW(12字节)和4DW(16字节)。DW是双字,1DW=4字节。为什么会有两种?因为4DW的Header多了一个64位地址字段,用于支持64位寻址。我在项目中遇到过32位地址不够用的情况,后来全改成了4DW Header。
4.2 Header字段详解
Header里最重要的几个字段,我列个表方便你对照:
| 字段 | 位宽 | 说明 |
|---|---|---|
| Fmt | 2位 | 包格式:00=无数据3DW,01=有数据3DW,10=无数据4DW,11=有数据4DW |
| Type | 5位 | 事务类型:存储器读写、IO、配置、消息、完成 |
| TC | 3位 | 流量类别,用于QoS优先级控制 |
| TD | 1位 | 是否包含ECRC(1=有ECRC) |
| EP | 1位 | 毒包标记(1=数据已损坏) |
| Attr | 2位 | 属性:顺序模型、缓存策略等 |
| Length | 10位 | 数据负载长度,单位是DW,最大1024DW |
| Requester ID | 16位 | 请求者总线号+设备号+功能号 |
| Tag | 8位 | 标签,用于匹配请求和完成 |
4.3 存储器读写事务
这是最常用的事务类型,没有之一。你写驱动时用mmap映射的BAR空间,底层走的就是存储器读写TLP。
存储器读TLP的Header结构:
Byte 0-3: Fmt=01, Type=00000, TC, TD, EP, Attr, Length
Byte 4-7: Requester ID, Tag, Last DW BE, First DW BE
Byte 8-11: 地址[31:2] + 保留位
(如果是4DW Header,Byte 12-15存地址[63:32])
存储器写TLP类似,但多了Data Payload。我个人习惯在写TLP时,把First DW BE和Last DW BE字段仔细算好。这两个字段控制哪些字节有效,搞错了会导致数据错位。
4.4 IO事务
IO事务在PCIe里已经很少用了,主要是为了兼容老设备。IO读写和存储器读写结构类似,但地址空间不同。IO地址空间只有64KB,而且访问速度慢。
说实话,我做了这么多年PCIe,只在调试老式串口卡时用过IO事务。新设计建议直接用MMIO替代。
4.5 配置事务
配置事务用于访问设备的配置空间。Type字段为00100(配置读)或00101(配置写)。配置事务分两种:
- Type 0:访问端点设备的配置空间
- Type 1:访问桥设备或下游设备的配置空间
配置事务的Header里有个特殊字段——Extended Register Number,用于访问配置空间扩展部分(256字节以后)。
4.6 消息事务
消息事务是PCIe里最灵活的事务类型。它没有地址,靠Message Code来区分用途。常见消息包括:
- INTx中断消息:传统中断方式
- PME消息:电源管理事件
- 错误消息:报告各种错误状态
- Vendor Defined消息:厂商自定义,可以传私有数据
消息事务的Header里,Fmt固定为01(有数据3DW),Type为01110。数据负载里放Message Code和具体参数。
嗯,这里要注意,MSI/MSI-X中断不是通过消息事务实现的,而是通过存储器写TLP。很多人搞混这个,我刚开始也犯过这个错。
4.7 完成事务
完成事务是PCIe里唯一一个由接收端主动发起的TLP。它用于回应非发布请求(比如读请求)。完成事务分三种:
- 完成(Completion, CPL):正常响应,带数据
- 完成带数据(Completion with Data, CPLD):读请求的响应
- 完成不带数据(Completion without Data, CPL):写请求的响应(很少用)
完成事务的Header里,有个关键字段——Completion Status,表示请求处理结果:
| Status值 | 含义 |
|---|---|
| 000 | 成功完成 |
| 001 | 不支持的请求 |
| 010 | 完成者中止 |
| 100 | 完成超时 |
4.8 ECRC的作用
ECRC(端到端CRC)是TLP包尾的可选字段,占1个DW。它覆盖整个TLP包(Header+Data),用于检测传输过程中的数据错误。
为什么已经有了链路层的LCRC,还要ECRC?因为LCRC只在相邻设备间校验,而ECRC是端到端的。如果中间有桥或交换机,ECRC能保证数据从源到目的地的完整性。
我建议在所有FPGA设计中都开启ECRC。虽然多了4字节开销,但能避免很多诡异的数据错误。我曾经在一个项目中没开ECRC,结果数据经过三层交换机后偶尔出现比特翻转,排查了整整一周才发现是这个问题。
4.9 实战:用逻辑分析仪抓TLP包
理论说完了,咱们来点实际的。用逻辑分析仪抓TLP包时,我一般这么操作:
- 设置触发条件:比如抓所有Type=存储器写的TLP
- 观察Header的Fmt和Type字段,确认包类型
- 检查Length字段和实际数据长度是否匹配
- 看Requester ID和Tag,确认请求-完成配对
- 如果有ECRC,验证CRC是否正确
举个例子,抓到一个TLP包,原始数据如下:
01 00 00 04 // Fmt=01, Type=00000, Length=4
10 00 00 01 // Requester ID=0x10, Tag=0x00, BE=0x01
00 00 00 80 // 地址=0x80000000
DE AD BE EF // 数据负载(4字节)
这个包的含义是:设备0x10发起一个存储器写请求,往地址0x80000000写入0xDEADBEEF,数据长度4字节(1个DW)。
嗯,看到这里你应该对TLP包结构有了清晰的认识。下一章我们会深入DMA传输机制,到时候这些知识都会用上。