ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

嵌入式开发绕不开的PCIe:从物理形态到驱动调试全攻略

嵌入式开发绕不开的PCIe:从物理形态到驱动调试全攻略 嵌入式开发里摸爬滚打久了你会发现一个规律不管做的是消费类主板、工业控制器还是边缘AI盒子最终都绕不开PCIe这个接口。作为目前嵌入式系统里吞吐量最大、扩展性最强的总线之一PCIe承担的不只是“插个网卡、插个SSD”这种表面工作它还决定了CPU与FPGA、GPU、NVMe设备之间的数据通路能不能高效跑起来。我始终认为PCIe是嵌入式工程师从“会用接口”走向“懂系统”的一道分水岭。这篇文章想用尽量直白的方式把我这些年踩过的PCIe坑和沉淀下来的理解捋一遍从物理形态怎么选、协议层怎么运转到枚举过程、驱动开发、FPGA侧XDMA设计再到实际调带宽和排查故障的思路一次性讲清楚。适合正在做嵌入式Linux、FPGA高速数据采集、边缘计算硬件选型的工程师也适合准备嵌入式面试、手头在啃嵌入式八股文的朋友拿来当系统复习提纲。看完你至少能回答清楚几个高频问题mini PCIe和M.2到底差在哪PCIe枚举到底在干什么LKMSM训练失败怎么查带宽算出来和实测为什么对不上。1. 整体思路为什么嵌入式场景绕不开PCIe1.1 从PCIE的定位看它在嵌入式体系里的分量先纠正一个常见误区很多人觉得PCIe是PC上的东西嵌入式系统用SPI、I2C、UART就够了。这个想法在单片机时代成立但放到今天的嵌入式SoC上就不现实了。随便列几个常见平台瑞芯微RK3588带PCIe 3.0 x4NXP i.MX8M Plus带PCIe 3.0Xilinx Zynq UltraScale MPSoC集成了PCIe硬核Intel和AMD的嵌入式处理器干脆把PCIe作为CPU对外的主要高速通路。所以PCIe在嵌入式领域不是“锦上添花”而是实打实的基础设施。从接口演进的历史看PCIe替代PCI是必然。老式PCI是32位并行总线工作频率33MHz或66MHz理论峰值不过533MB/s而且并行信号在高速下对走线长度、时钟偏移极其敏感。PCIe把这些痛点全改了改用串行差分对传输每条lane就是一对发送和一对接收靠高频时钟内嵌和链路训练自动对齐扩展性从x1一路到x16。换句话说PCIe把“并行总线难以跑高频率”的物理天花板掀掉了靠串行速率和多lane堆出几十GB/s的带宽这正好契合嵌入式领域对高吞吐外设的诉求比如视频采集卡、NVMe存储、AI加速卡。1.2 掰扯清楚物理形态选型mini PCIe、M.2、标准插槽热词里有个高频问题网卡mini PCIe接口和M.2接口有什么区别。这个问题我在项目评审里几乎每次都会遇到硬件同事和软件同事各执一词。先说结论两者底层电气协议都可以是PCIe但物理尺寸、Key定义、供电能力和支持的总线类型不同。mini PCIe是PCIe mini Card标准52-pin金手指宽度30mm常见于旧款笔记本和工业主板的WiFi/4G模块位兼容USB 2.0和PCIe x1信号部分还带SIM卡槽定义。M.2是后起之秀宽度有22mm、30mm等规格长度从2230到22110都有按Key区分功能B Key通常支持SATA和PCIe x2/x4M Key支持PCIe x4E Key多用于WiFi/蓝牙模块且引出PCIe x1和USB。绝大多数新平台的无线网卡和NVMe SSD都走M.2接口。还有MXM、CFast、定制金手指等形态但嵌入式主板里见得少就不展开了。选型上我的经验是优先看SoC的PCIe控制器数量和外设带宽需求。如果只是接一颗WiFi 6网卡比如Realtek RTL8852BE这种Gen1 x1都绰绰有余M.2 E Key和mini PCIe都行如果是接NVMe SSD做高速缓存那必须M.2 M Key并且链路至少Gen3 x4否则顺序读性能直接腰斩。说句题外话PCIe半高挡板尺寸图也是硬件工程师常翻的资料它和mini PCIe配套使用标准半高挡板高度约79.2mm3U全高约120mm挡板上的开孔位置和金手指暴露长度是有行业规范的自己打样时照着参考设计抄最稳妥别想当然开孔。1.3 嵌入式里PCIe的典型应用拓扑嵌入式设备里PCIe的拓扑一般比服务器简单但也有自己的复杂性。最常见的有三种第一种是“CPU接单设备”比如SoC的PCIe控制器直接接一片NVMe SSD或无线网卡链路固定为x1或x2/x4拓扑简单软件只需枚举到单个Root Port下的Endpoint。第二种是“CPU接PCIe Switch扩展”比如一块接口卡需要同时挂多路千兆网卡或多张采集卡主板只有一路x4就通过PCIe Switch拆分成多路x1。这种拓扑引入了Switch/Bridge枚举时会出现多个Bus号分配驱动配置稍复杂热词里的pcie switch就是讨论这类场景。第三种是“CPU接FPGA/GPU作加速器”常见于边缘AI盒子、软件无线电、高速数据采集设备FPGA作为Endpoint大块DMA搬运数据CPU侧跑Linux驱动。这类场景对PCIe的吞吐、延迟、MSI中断响应要求最高也是我后面重点展开的实操部分。2. 核心细节拆解PCIe协议分层与运行机制2.1 事务层、数据链路层、物理层各管哪一段很多人一看到PCIe协议栈“三层两段”就头皮发麻其实拿快递物流打比方就通了。你在电商下单填收货地址这是事务层Transaction Layer的事快递公司把包裹分拣、贴条形码、装车这是数据链路层Data Link Layer的事货车和司机在公路上跑是物理层Physical Layer的事。PCIe也一样软件发出读寄存器、写内存、DMA请求最后都先被包装成事务层报文TLPTLP经过数据链路层加上序列号和CRC校验再交给物理层编码后从差分线上发出去。收到端逐层剥皮校验最终把结果返回给软件。对嵌入式工程师来说事务层最关键因为驱动读写配置空间、访问BAR空间、发起DMA本质都是在组装和解析TLP。TLP有多种类型Memory Read/Write、IO Read/Write、Configuration Read/Write、Completion、Message。实际开发里用到最多的就是Memory Write、Memory Read和Completion。比如CPU访问设备BAR空间里的寄存器就是发起一个Memory Read TLP设备收到后返回一个Completion TLP把数据带回来而设备做DMA写内存则是设备直接发出Memory Write TLPCPU在内存侧拿数据。理解了这个你就明白为什么访问PCIe设备寄存器时有时会感觉“慢半拍”或者偶发卡顿——其实是一次TLP请求-完成事务的开销不是设备本身的问题。2.2 链路训练LTSSM设备是怎么“握手”的PCIe物理层有个状态机叫LTSSMLink Training and Status State Machine这是排查“设备不识别”问题时必须懂的东西。链路训练的大致流程是上电后两端PHY先做阻抗检测看看对方在不在Detect然后发训练序列协商速率Polling接着在Configuration状态交换lane信息确定链路宽度x1还是x4等最终稳定到L0状态正常传数据。如果中途出错会进入Recovery或Disabled状态链路就起不来。实际项目中链路训练失败是硬件问题重灾区。常见原因包括参考时钟100MHz没给对、差分对一正一负接反、某条lane断线导致宽度协商不到预期、AC耦合电容漏贴或贴错位置甚至有遇到过PCIe复位时序不对导致设备总是训练到一半掉线。软件能做的检查有限但能用lspci或uboot下的pci命令读到链路状态寄存器确认LnkSta链路速率和宽度是否达标。比如一颗Gen2 x4的设备读到的LnkSta如果显示2.5GT/s x1基本可以断定物理链路或配置有问题后面我会专门讲排查流程。2.3 速率与带宽计算Gen1到Gen5别再凭感觉热词里“pcie带宽测试”被搜得很多说明大家都关心怎么验证带宽达标。要测带宽先要会算理论值。PCIe每代速率和编码方式都不一样速率单位是GT/s每秒传输Gigatransfer但因为编码引入了冗余有效数据带宽要打个折扣Gen12.5GT/s8b/10b编码每个lane有效带宽约2Gbps即250MB/s。Gen25GT/s8b/10b编码每个lane有效带宽约4Gbps即500MB/s。Gen38GT/s128b/130b编码每个lane有效带宽约984.6MB/s接近1GB/s。Gen416GT/s128b/130b编码每个lane有效带宽约1969MB/s。Gen532GT/s128b/130b编码每个lane有效带宽约3938MB/s。所以一条Gen3 x4链路的双向理论带宽大约是4GB/s单向约3.94GB/s。这个数字看起来很大但实际吞吐很难跑满原因是协议开销、TLP头、ACK/NACK、FC更新、中断处理都会占掉一部分。加总起来实际稳定吞吐一般是理论值的7~8折很多新手测出来“只有2.8GB/s就以为出问题了”其实完全正常。我后面给的测试方法可以帮你快速定位瓶颈是协议开销、驱动问题还是硬件问题。3. 实操过程从枚举到驱动到DMA再到带宽测试3.1 PCIe枚举过程BIOS/uboot怎么发现你的设备热词里pcie枚举过程被反复搜索说明这是嵌入式工程师普遍觉得“黑盒”的部分。实际上枚举不神秘就是把PCIe树从头到尾扫一遍。Root ComplexRC先作为Bus 0的设备软件从Bus 0的Device 0 Function 0开始读取每个BDFBus/Device/Function的Vendor ID和Device ID寄存器。如果读回来是0xFFFFFFFF说明这个位置没有设备否则就说明有个设备在位进一步读它的Header Type判断它是Endpoint还是Bridge。如果是Bridge设备包括PCIe Switch的上行口和下行口软件会给它分配一个从属Bus号然后接着往下一级Bus遍历如果是Endpoint就分配BAR资源。所谓BAR就是设备的寄存器或内存区域映射到CPU地址空间的基地址驱动和应用程序访问设备能力全靠这一组基地址。枚举的最后一步是把所有设备的BAR地址、中断引脚、总线号、设备号写入各自的配置空间并把Host桥的地址窗口设置好让CPU能通过MMIO访问到设备。在小系统里如果你不用BIOS而是用U-Boot启动那么U-Boot里的pci enum逻辑也在干同样的事只是它只输出简略信息你可以在命令行敲pci或pci header查看枚举结果。做uboot下枚举排查时有几点经验先用pci bar看BAR是否分配到地址再用pci header 0.0.0看详细配置空间。如果看到Vendor ID正常但Device ID是乱码优先怀疑供电时序如果所有BDF都读到0xFF查PCIe复位引脚和参考时钟。还有一个很容易忽略的地方某些SoC的PCIe控制器需要先在设备树或寄存器里使能PHY并设置lane数否则出厂默认PCIe是关闭的枚举自然一片空白。3.2 驱动开发三板斧probe、BAR空间、中断到了Linux驱动侧PCIe设备的驱动套路非常固定。一个PCIe驱动的核心是注册pci_driver结构体里面最关键的是id_table和probe函数。设备在枚举完成后Linux PCI核心会把匹配到Vendor/Device ID的设备交给驱动调用其probe。简化示例static const struct pci_device_id demo_pci_ids[] { { PCI_DEVICE(0x10EE, 0x9038) }, /* 厂商ID 0x10EE, 设备ID 0x9038 */ { 0 } }; MODULE_DEVICE_TABLE(pci, demo_pci_ids); static int demo_probe(struct pci_dev *pdev, const struct pci_device_id *id) { int ret; ret pci_enable_device(pdev); if (ret) return ret; ret pci_request_regions(pdev, demo); if (ret) goto err_disable; /* 使能总线主控允许设备发起DMA读写内存 */ pci_set_master(pdev); /* 设置DMA掩码设备地址宽度 */ ret dma_set_mask_and_coherent(pdev-dev, DMA_BIT_MASK(64)); if (ret) ret dma_set_mask_and_coherent(pdev-dev, DMA_BIT_MASK(32)); ... }那pci_enable_device到底做了什么很多人只记得“必须调用”不深究原因。它实际上是确保设备的IO和Memory资源可用并且让PCI核心启动电源管理相关的底层操作。之后调用pci_request_regions是为了独占BAR资源避免别的驱动乱访问。pci_set_master则非常关键它设置配置空间里的Bus Master位设备才能发起Memory Write TLP做DMA。以前调板子时漏了这一步设备发起的DMA全部被RC拒绝数据一动不动查了整整半天。DMA掩码也要注意现代设备大多支持64位地址但如果你的SoC总线或内存布局只支持32位地址要显式降级否则会碰到严重的地址映射错误甚至系统崩溃。中断方面PCIe支持传统INTx和MSI/MSI-X。强烈建议嵌入式项目优先用MSI它能避免共享中断风暴在CPU亲和性做得好的时候吞吐和延迟都比共享INTx稳定。申请MSI用Linux新接口就行几行代码搞定int nr pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI | PCI_IRQ_MSIX); devm_request_threaded_irq(pdev-dev, pci_irq_vector(pdev, 0), demo_irq_handler, demo_thread_fn, IRQF_SHARED, demo, dev);经常有人问“为什么我用cat /proc/interrupts看到设备中断挂在同一个CPU上性能上不去”这是因为MSI向量默认路由可能集中在一个核上。解决问题通常要配合中断亲和性设置例如写/proc/irq/中断号/smp_affinity或者用irqbalance做自动均衡后者在嵌入式系统里要注意别引入太大的CPU开销。3.3 FPGA侧PCIe设计XDMA到底怎么用热词里fpga pcie和pcie xdma出现频率很高这个方向这几年确实火。很多国产FPGA芯片做高速数据采集、信号处理都拿Xilinx的XDMA IP核或者国产厂商类推的DMA引擎来干PCIe后端。XDMA的优势是它把PCIe Endpoint和DMA控制逻辑封装成IP用户不需要自己实现TLP收发只需要通过AXI-Stream或AXI-MM接口接入自己的逻辑。我第一次用XDMA时最大的误区是以为它插上就能用。实际你必须理解三个角色CPU侧驱动通过BAR空间配置XDMA控制寄存器、描述符表和状态寄存器XDMA IP内部根据描述符自动从FPGA侧内存读取数据并作为Master发起PCIe Memory Write送给CPU内存FPGA逻辑只要把数据送入AXI接口或从AXI接口取出。所以软件和FPGA工程师的切分点非常清楚软件管BAR和中断FPGA管AXI侧的数据通路。实操上XDMA最容易出问题的是描述符循环和地址对齐。描述符里存放DMA源的地址、目的地址、长度、控制位软件写好后要按顺序写到H2C通道的描述符表硬件读走并执行。一个常见的坑是描述符和缓冲区地址没有做cache一致性处理Linux侧要用dma_alloc_coherent申请缓冲区FPGA侧如果走AXI且不是OCM内部访问的话也要注意CACHE属性不然数据看上去“脏了”或干脆读不到最新值。另一个坑是中断合流XDMA的H2C/C2H完成中断默认可以合到同一个MSI向量中断处理里要多读状态寄存器判断是哪个通道完成的处理不当很容易丢中断。至于和CPU侧驱动握手我们项目里的约定是这样驱动probe时分配DMA缓冲区并把物理地址写入XDMA的寄存器FPGA逻辑在收到某个AXI-Lite寄存器写命令后开始搬运数据搬运完成XDMA回写状态并触发MSI驱动中断处理里读取完成状态通知应用层处理数据。整个过程用一句话总结就是“软件下命令、硬件跑DMA、MSI报完成”。如果哪天发现应用层拿到的数据总是差一截优先查描述符的长度字段和硬件实际搬运字节数是否一致。3.4 PCIe Switch的桥接与拓扑注意事项再说说pcie switch。嵌入式设备需要扩展多个PCIe设备时Switch就是“集线器”它把上游的一路x4/x8分成下游的多个x1/x2/x4端口。Switch内部本质上是若干PCIe Bridge每个下行端口对应一个独立的Bus段所以枚举时你会看到多级Bus号结构类似Bus 0 - Root Port (Bridge 1) - Bus 1 - Switch Upstream Port - Bus 2 - Downstream Port 0 - Bus 3 - Endpoint A - Downstream Port 1 - Bus 4 - Endpoint B这意味着驱动的设备树或启动参数里要正确配置bus范围否则Linux会枚举不到Switch下游的设备。更麻烦的是有些Switch特别是高端产品还支持NTBNon-Transparent Bridging两个主机可以通过一个Switch互通。这个功能在嵌入式多主控热备场景很常用但驱动复杂度和调试难度都上了一个量级我建议新手先别碰除非项目真有跨CPU通信需求。用Switch还有个容易忽略的点功耗和散热。PCIe链路训练起来后每lane功耗不算低一个48 lane的大号Switch功耗能到10W甚至更高板级设计时不加散热片或气流设计不足很容易出现设备正常运行半小时后链路Recovery的问题。别问我怎么知道的曾经一块设备在高温箱里跑测试PCIe链路随机掉线最后发现是Switch芯片过热触发了保护。从那以后我凡是板上有Switch都在原理图阶段就在附近预留好散热安装孔和风扇接口。3.5 带宽测试理论值之外的真实世界测PCIe带宽最直接的办法是在Linux下用lspci -vvv看设备的LnkSta字段得知当前的Link Speed和Width。比如看到LnkSta: Speed 8GT/s, Width x4说明链路协商在Gen3 x4理论单向约3.94GB/s。但确认链路协商只是第一步实际吞吐还要做读写测试。针对NVMe SSD可以用fio测顺序读和顺序写命令参考fio --nameseqread --rwread --bs1M --size8G --numjobs1 \ --iodepth32 --filename/dev/nvme0n1 --direct1 \ --ioenginelibaio --group_reporting如果你的设备是字符设备比如FPGA数据采集卡那就要自己写一个简单的read/write程序循环调用read()/write()用gettimeofday或者clock_gettime统计平均速率。这里有个常见误区用单线程同步读写测出来的速率通常远低于理论值因为一次TLP只能搬运一定字节且要等完成想榨干带宽必须发异步DMA用多个描述符并发。对XDMA这类设备驱动里要打开多队列或多缓冲并发才能看到接近线速的效果。除了Linux下的软件测试也可以用PCIe分析仪直接抓总线流量。几万到几十万的设备虽然贵但排查时序和数据完整性问题时是真神器。如果项目预算不够退而求其次的办法是看设备自身的错误计数器Linux下/sys/bus/pci/devices/bdf/下很多统计文件能反映AER错误、CRC错误等指标。比如/sys/kernel/debug/下开启PCIe调试后能看到大量的BadTLP、BadDLLP计数如果这两个数字一直涨链路质量多半有问题。4. 常见问题与排查技巧实录4.1 链路怎么都训不起来的排查清单链路训练失败是最折磨人的。我的经验是先用排除法锁定是大方向还是细节问题。大方向指的是供电、复位、参考时钟、差分走线这些基础项细节项再往协议层靠比如lane极性反转、信道翻转、AC耦合电容值。基本排查顺序测量设备的3.3V和1.8V/0.9V供电上电时序是不是符合数据手册要求PCIe设备通常要求先有VCC后有PERST且PERST释放后至少等100ms才能开始训练。确认REFCLK 100MHz差分时钟是否稳定很多SoC要求差分时钟摆幅在特定范围用示波器看波形成绩最直观。复位信号PERST#是否干净有没有毛刺释放时机是否晚于电源稳定。检查PCIe差分对的AC耦合电容是否都在靠近发送端位置一般0.1uF或0.22uF极性方向如果接反也会导致无法训练。最后才去抓链路训练序列判断是卡在Detect还是Polling还是Configuration。卡在Detect大概率物理连接有问题卡在Polling/Configuration大概率是速率协商或lane配置不匹配。4.2 设备枚举到了但驱动probe不执行怎么办这类问题更偏软件。最常见的原因是设备树和PCIe控制器的时钟、复位、PHY配置没写好。嵌入式Linux下先看内核启动日志里有没有类似pci 0000:01:00.0: [10ee:9038] type 00 class 0x058000的打印。如果设备有打印但驱动没被probe跑一遍lspci -k看内核是否把驱动绑上了。如果显示kernel driver in use是空的大概率是Vendor ID或Device ID没匹配上或者MODULE_DEVICE_TABLE未被正确导出需要强制重新加载模块或检查编译选项。另一种隐蔽原因是设备本身有“隐藏BAR”或复杂的电源管理机制驱动probe时访问BAR空间前需要先做特定初始化没做的话设备不放行资源访问probe里某一步失败导致退出。经验是probe函数里每一步都要检查返回值并打印日志别图省事把错误吞掉。4.3 DMA丢数据、数据错位、带宽上不去这类问题排在嵌入式PCIe调试里的“终极BOSS”位置。我总结过几个高频根因首先cache一致性是最常见的坑。CPU通过DMA读到的数据和设备实际写入的数据不一致多半是没走dma_alloc_coherent或dma_map_single而是直接用kmalloc的内核地址。正确做法是DMA缓冲区用DMA API申请并且每次驱动读写前做正确的dma_sync_*操作。其次忽略描述符的WMBwrite memory barrier会导致硬件拿到陈旧描述符。Linux驱动里写完描述符内容后一定要调用wmb()或dma_wmb()再写Doorbell寄存器通知硬件。如果省了这步在乱序执行的SoC上硬件可能先把新的门铃读完再读描述符结果验到旧内容数据错位就来了。还有很多工程师被“DMA地址是物理地址还是I/O虚拟地址”折磨。正常情况下pci_map_single返回的是总线地址可以直接填入描述符但如果系统启用了IOMMU或SMMU返回的可能是经过转换的地址这时必须保证描述符使用的是映射后的地址。IOMMU在嵌入式平台已经越来越常见不要默认它关闭。至于带宽上不去除了上一节说的多队列并发优化还要检查ACPI或设备树里是否开启了对PCIe的ASPM电源管理让链路降到了低功耗状态一来一回的性能损耗很大。可以临时通过echo 0 /sys/module/pcie_aspm/parameters/policy或内核实参pcie_aspmoff来排除ASPM对带宽的影响。4.4 高负载下系统卡顿与中断风暴的解决思路当一个PCIe设备吞吐很高而系统UI或实时任务出现卡顿通常是中断或DMA访问CPU缓存/内存带宽被占用。先用top或perf top看CPU占用分布如果软中断和中断占比很高大概率是MSI中断都跑到了一个核上。调整中断亲和性是个快捷办法把不同向量拆到不同CPU核。另一个办法是减少中断频率尽量在驱动里用聚合中断或NAPI式批处理一次中断处理完所有已完成DMA的描述符而不是来一个完成就触发一次中断。做视频采集或雷达数据处理的板卡这个优化往往能让CPU占用下降20~30个百分点效果非常直接。5. 给新手的几条切身建议如果你是刚入行遇到PCIe个人建议按“硬件形态→枚举逻辑→驱动流程→DMA”这个顺序学一步一个坎地过。先把好理解的BAR和MMIO搞明白再琢磨TLP和LTSSM最后啃DMA描述符和中断处理。网上资料虽然多但真正值钱的往往是那些不成文的“经验教训”比如AC耦合电容位置、MSI亲和性、cache一致性这些它们不会写在datasheet里只能从项目中踩坑获得。这些年做下来我最大的体会是PCIe调试问题大多不是“某一个点错了”而是“多个点同时都有瑕疵”比如复位时序勉强够、REFCLK抖动偏高、驱动又没做MSI均衡单独看每个问题都不致命组合起来设备就拉胯。所以排查时不要放过任何一个不起眼的异常链路偶尔训练失败、错误计数缓慢增长这类“软故障”最终往往比彻底不通的硬故障更难缠。好在这条路的经验是累积的踩过一次的坑下次就能绕开。希望这篇对你也有同样的价值。
返回列表