022、PCIE配置读写事务:从一次诡异的设备失联说起
022、PCIE配置读写事务:从一次诡异的设备失联说起
上个月调试一块自研的FPGA加速卡,系统启动后设备时有时无。lspci命令能扫到设备,但驱动加载时总报配置空间读取失败。用示波器抓链路训练信号都正常,最后发现是Type1配置请求的Bus Number字段没处理好——这种问题在PCIE调试中太典型了,今天我们就深入聊聊配置事务那些事儿。
配置空间:PCIE设备的身份证
每个PCIE设备都有256字节的标准配置空间,前64字节是PCI兼容部分,后面是PCIE扩展部分。这个空间就像设备的身份证,系统启动时就是通过读取它来识别设备类型、申请资源、分配BAR的。最经典的例子就是读取Vendor ID和Device ID:如果读出来全是0xFF,说明设备根本不存在或者链路有问题;如果读出来是0xFFFF,可能是设备还没完成初始化。
// 读取配置空间的经典代码片段uint32_tpci_read_config(uint8_tbus,uint8_tdev,uint8_tfunc,uint8_toffset){// 构造配置地址:注意Bit31必须置1,这是老PCI的遗留设计uint32_taddress=(1<<31)|(bus<<16)|(dev<<11)|(func<<8)|(offset&0xFC);// 写到0xCF8端口(配置地址端口)outl(0xCF8,address);// 从0xCFC端口(配置数据端口)读取数据// 这里有个坑:x86是小端,但PCIE配置空间是大端视图uint32_tvalue=inl(0xCFC);// 根据offset的低2位调整字节对齐return(value>>((offset&3)*8))&0xFF;}上面这种IO端口方式只适用于Host CPU直接访问,在真实的PCIE交换网络中,配置请求是以TLP包的形式传递的。
配置请求TLP:细节决定成败
配置读写事务有两种类型:Type0和Type1。Type0用于访问当前总线上的设备,Type1用于跨总线访问。我踩过的那个坑就是:FPGA逻辑在处理Type1请求时,没有正确检查Bus Number是否匹配,直接把所有配置请求都当Type0处理了。
配置TLP的Header长这样:
Byte 0: Fmt[7:5] + Type[4:0] // Type=01000或01001 Byte 1: TC[2:0] + Attr[1:0] + TH + TD + EP Byte 2: Length[9:0] // 配置读写永远是DW长度,所以是1 Byte 3: Requester ID[15:0] Byte 4: Tag[7:0] + Last DW BE[3:0] + 1st DW BE[3:0] Byte 5: Bus Number[7:0] Byte 6: Device Number[4:0] + Function Number[2:0] + Ext Reg Number[7:0] Byte 7: Register Number[3:0] + 2'b0关键点在于Byte 5的Bus Number字段。对于Type0请求,这个字段应该被接收设备忽略;对于Type1请求,设备需要比较这个值是否等于自己的上游交换机的Secondary Bus Number。很多FPGA的PCIE IP核默认只支持Type0,如果你要做多级交换,一定要确认IP核是否支持Type1转发。
配置访问的三种方式
传统PCIE配置访问(ECAM方式)现在更常见。以x86平台为例,每个PCIE Segment有256MB的MMIO空间,其中前16MB用于配置空间访问。计算公式是:
物理地址 = 0xE0000000 + (Segment << 27) + (Bus << 20) + (Dev << 15) + (Func << 12) + Offset在Linux内核里,我们常用pci_read_config_dword()这类函数,它们底层最终会转换成对ECAM区域的MMIO访问。在设备驱动开发时,直接调用这些API就好,不用关心底层是IO端口还是MMIO。
但在嵌入式或裸机环境,你可能需要自己实现配置访问。这时要注意:配置读写不支持突发传输,每次只能读/写一个DW(4字节)。想读一个字节怎么办?先读整个DW,再掩码出需要的字节——这就是为什么上面的代码示例里有那个移位操作。
调试实战:当配置访问失败时
回到开头那个问题。设备能识别但配置读写失败,可以按这个顺序排查:
- 确认链路训练成功:查看设备的LTSSM状态机是否进入L0状态
- 检查配置请求路由:Type0/Type1是否正确,Bus/Dev/Func是否匹配
- 观察配置响应:设备是否返回了Completion TLP(CPL)
- 检查Completion状态:是CA还是UR?CA(Completer Abort)通常表示设备内部错误,UR(Unsupported Request)表示地址无效
那次调试中,我们最终在FPGA逻辑分析仪里看到:Host发来Type1请求,Bus Number=0x02,但我们的设备在Bus 0x01上。设备本应忽略这个请求(让上游交换机继续转发),却错误地尝试处理,结果访问了不存在的内部地址,返回CA。
个人经验:配置空间访问的“潜规则”
做了十几年PCIE相关开发,总结几条教科书上不写的经验:
配置空间在系统启动早期就被访问,这时设备可能还没完全初始化。有些FPGA的PCIE硬核需要几十毫秒才能稳定,但BIOS可能在电源稳定后几毫秒就开始枚举了。解决办法是在FPGA代码里加个“就绪”标志位,等所有模块初始化完成再置位,在此之前对配置访问返回全0xFF。
对于多功能设备(Single Function vs Multi-Function),Function 0的配置空间必须能访问,即使这个Function实际不存在。系统可能通过读取Function 0的Header Type寄存器来判断是否有多功能。
配置空间的某些字段是只读的,但有些FPGA设计允许动态修改(比如通过JTAG)。这在调试时很方便,但正式产品中一定要锁死,防止意外修改导致系统识别异常。
最后,如果你在做PCIE交换芯片或FPGA的PCIE端点设计,一定要实现完整的配置错误处理。该返回UR时就返回UR,别吞掉请求——吞掉请求会导致Host侧超时,调试起来更麻烦。好的错误处理不是负担,而是最好的调试工具。
