020、PCIE内存读写事务:从一次诡异的DMA超时说起
020、PCIE内存读写事务从一次诡异的DMA超时说起上周实验室的小张跑来找我说他的FPGA DMA传输总是随机超时。示波器抓到的REFCLK和PERST#信号都正常LTSSM状态机也显示链路已经训练到L0状态。但每当尝试发起DMA读操作时RC端等待完TLP的Timeout时间后就直接报错。“明明配置空间都能正常访问为什么内存读写就不行”他盯着WireShark抓到的TLP包头一脸困惑。这个问题让我想起多年前在调试第一代PCIE设备时踩过的坑。今天我们就从最基础的内存读写事务说起聊聊PCIE世界里数据到底是怎么“搬家”的。内存事务PCIE的搬运工机制PCIE总线本质上是个大规模的分组交换网络内存读写事务Memory Read/Write Transactions就是这个网络里最常用的“快递服务”。和CPU直连内存的并行总线不同PCIE的所有操作都封装在TLPTransaction Layer Packet里。当你写一行memcpy(dest, src, size)时RCRoot Complex会把它拆成若干个MWrMemory WriteTLP像发快递包裹一样通过Switch发往EPEndpoint。关键就在这里内存地址空间是映射的不是直连的。EP设备在BARBase Address Register里申领自己的“快递收发室地址”RC配置时把这个地址映射到系统内存的某个区域。EP发起的DMA读操作实际上是在向RC的“收发室”寄出个“到付取件单”MemRd TLPRC收到后去真实内存取数据再用CPLCompletionTLP把数据“快递”回来。小张的问题就出在地址映射上。他的FPGA代码里DMA源地址还是按老式PCI的32位地址算的但系统实际分配的是64位地址空间。MWr TLP里的地址字段只填了低32位高32位默认是0结果数据全写到了0x00000000开始的地址区域——那里可能是ROM或者根本没映射自然就超时了。TLP包头快递单上的关键信息看一个实际的MWr TLP包头3DW头不带数据// 这是WireShark里抓到的真实包头我们拆开看// 字节0: Fmt[2:0]10b(3DW不带数据), Type[4:0]00000b(MWr)// 字节1: TC[2:0]000, Attr[1:0]00, TH0, TD0, EP0// 字节2~3: Requester ID 01:00.0EP的设备号// 字节4~7: 地址低32位 0xFE00_0000// 字节8~11: 地址高32位 0x0000_0001注意这里// 后面跟着数据载荷...重点在地址字段。PCIE支持64位地址但很多驱动库的默认示例只给32位赋值。我见过有人这样写// 别这样写高32位可能随机tlp-address_lowtarget_addr0xFFFFFFFF;// 忘记写address_highMellanox的旧驱动就这么坑过人正确的做法是显式处理64位tlp-address_low(uint32_t)(target_addr0xFFFFFFFF);tlp-address_high(uint32_t)(target_addr32);// 即使高32位为0也要明确赋值这是好习惯读写事务的“潜规则”内存读事务比写事务复杂得多因为它是个“请求-响应”过程。MemRd TLP本身不带数据只是个“取件单”EP必须等待RC返回的CPL TLP。这里有几个容易翻车的地方长度限制单个TLP的最大载荷尺寸Max Payload Size由设备能力决定常见的是256B或512B。想读4KB数据RC会自动拆成多个CPL。但有些FPGA IP核的DMA引擎设计时没考虑多CPL情况第一个CPL收到就以为结束了。地址对齐RC对非对齐访问的处理很“玄学”。有的会拆成两个TLP有的直接返回URUnsupported Request。特别是跨4KB边界访问——PCIE规范明确禁止单个TLP跨过4KB边界。你代码里一个memcpy可能被拆成两个TLP如果驱动没处理好数据就对不齐了。Completion Timeout这就是小张遇到的问题。EP发出MemRd后启动定时器默认超时时间是50ms。如果RC没在规定时间内返回CPLEP就可以认为请求失败。但超时不一定是链路问题——可能是RC侧地址转换错误、内存页被换出甚至是RC的PCIE控制器挂了。调试实战抓包分析三板斧遇到PCIE问题别急着调代码先抓包。用Intel的PcieSpy、Xilinx的ILA或者高端示波器的协议分析功能都行。看三个东西一看TLP类型对不对。MWr/MRd/CPL的Type字段要确认曾经有兄弟把CfgWr当成MWr发配置空间都被写乱了。二看地址和长度。地址是不是64位对齐长度有没有超过MPS特别是Length字段的单位是DW4字节有人直接填字节数结果多读了3倍数据。三看Completion Status。CPL头里的Status字段是3位代码000是成功001是UR010是CACompleter Abort。如果是CA说明目标地址存在但访问出错比如写只读区域。UR则可能是地址根本没映射。去年调一个国产SSD控制器时发现它的DMA读在特定长度下必现CA。最后发现是他们的DMA引擎在收到RC的CPL后又偷偷往同一个地址发了个MWr写状态寄存器——但这个地址在RC侧没做映射。这种硬件设计缺陷软件再怎么改驱动都没用。给初入门的工程师几点建议PCIE调试像破案TLP就是现场痕迹。养成抓包习惯比瞎猜效率高十倍。新手常犯的错是只盯着自己端的代码忘了PCIE是个双向协议——RC和EP都可能出问题。地址映射问题占PCIE调试的七成。BAR配置、IOMMU、DMA掩码这三个地方要反复检查。特别是64位系统高地址位没处理好的问题太常见了。别迷信厂商的示例代码。很多IP核的Example Design只保证功能不保证性能。比如那个著名的“DMA环回示例”实际用起来吞吐量只有理论值的三成因为没做TLP合并和流水线。最后记住PCIE链路训练成功后只代表物理层通了事务层能不能工作还得看TLP。下次遇到DMA失败先看看MWr/MRd发没发出来再查地址映射最后看Completion状态。这个顺序能省你两天调试时间。注文中调试案例融合了多个真实项目经历厂商名称已做泛化处理。实际调试请结合具体芯片手册和协议分析工具。