第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位 标签,用于匹配请求和完成
重点提醒:Length字段的单位是DW,不是字节。如果你要传输64字节数据,Length应该填16(64/4=16)。我曾经见过新手直接填64,结果接收端只收到16字节数据,排查了半天。

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字段仔细算好。这两个字段控制哪些字节有效,搞错了会导致数据错位。

小技巧:调试时可以用逻辑分析仪抓TLP包,看Header里的Length和实际数据长度是否一致。我经常用这个方法来快速定位DMA传输问题。

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字节以后)。

避坑指南:配置事务的完成超时时间默认是50ms。我曾经在调试一块FPGA板卡时,配置读总是超时,后来发现是FPGA内部配置空间逻辑没做好,响应太慢。建议在FPGA设计里把配置空间访问优先级设高。

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 完成超时
关键点:完成事务必须和请求事务一一对应。Requester ID + Tag 就是匹配的钥匙。如果收到一个完成事务,但找不到对应的请求,那就是协议错误。

4.8 ECRC的作用

ECRC(端到端CRC)是TLP包尾的可选字段,占1个DW。它覆盖整个TLP包(Header+Data),用于检测传输过程中的数据错误。

为什么已经有了链路层的LCRC,还要ECRC?因为LCRC只在相邻设备间校验,而ECRC是端到端的。如果中间有桥或交换机,ECRC能保证数据从源到目的地的完整性。

我建议在所有FPGA设计中都开启ECRC。虽然多了4字节开销,但能避免很多诡异的数据错误。我曾经在一个项目中没开ECRC,结果数据经过三层交换机后偶尔出现比特翻转,排查了整整一周才发现是这个问题。

4.9 实战:用逻辑分析仪抓TLP包

理论说完了,咱们来点实际的。用逻辑分析仪抓TLP包时,我一般这么操作:

  1. 设置触发条件:比如抓所有Type=存储器写的TLP
  2. 观察Header的Fmt和Type字段,确认包类型
  3. 检查Length字段和实际数据长度是否匹配
  4. 看Requester ID和Tag,确认请求-完成配对
  5. 如果有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传输机制,到时候这些知识都会用上。