ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

PCIE DMA例程深度解析:从数据通路到性能优化

PCIE DMA例程深度解析:从数据通路到性能优化 简介一套围绕PCIe DMA技术从FPGA端到主机端完整落地的示例工程面向驱动开发、FPGA逻辑设计或高速数据传输应用的工程师帮助理解BMDBus Master DMA工作模式、描述符管理与中断处理流程。压缩包共41个文件以Verilog源码.v、C/C代码.c/.h为主同时包含Xilinx约束文件.ucf/.xst、Windows驱动安装信息.inf与驱动文件.sys并附带cab驱动包、exe上位机程序及makefile/批处理脚本可分别对应硬件逻辑、驱动安装和软件调用三个层面。资源包约1.76MB其中还包含setup.exe与安装配置文件便于在仿真或实机上快速搭建验证环境。目前已有485人学习下载适合需要借鉴BMD框架实现高速DMA传输、调试PCIE中断与带宽瓶颈的开发者也可作为FPGA与Windows驱动联合调试的起步参考。1. PCIE DMA 例子是什么解压前先搞清楚它在解决什么问题解压一个 PCIE DMA 例子.7z很多人第一反应是找 README第二反应是直接 make。但一个 PCIE DMA 例程在这两个动作之间藏着的是整套数据通路驱动怎么找到设备、描述符怎么排队、门铃怎么触发、中断怎么收尾。PCIE 链路本身是物理层的功夫DMA 才是把链路带宽变成实际吞吐的关键。这类例程最典型的场景是高速数据采集、网卡和存储卡的原型验证适合手里有一块 FPGA 板卡或一颗 PCIE 控制器、想把数据快速搬进系统内存的人。例程的价值不是开箱即跑而是让你少走从零写数据通路的弯路。拿到这种压缩包先别急着编译。搞清楚它包含哪几部分Linux 驱动源码、用户态测试程序如果配套 FPGA 逻辑可能还有 RTL 或者 bit 流。例程里的驱动通常已经实现了 probe、DMA 描述符管理、中断处理和字符设备接口你要做的是理解它然后改到自己的板卡上。2. PCIE DMA 怎么搬运数据数据通路、描述符和中断先立住三个概念再动手2.1 从 CPU 搬运到 DMA 搬运为什么 PCIE 场景下 DMA 几乎是唯一选择PCIE 链路的 LTSSM 走到 L0 状态配置空间能被 RC 枚举到TLP 才能在主机和外设之间流动。这时候你面对一个现实问题数据怎么从板卡进内存用 CPU 做搬运工是可行的但代价很高。每笔 PCIE 读写事务都有地址、长度、标签等开销CPU 一次读一个寄存器可能只有几十字节的有效数据。吞吐一上来CPU 占用率直接拉满Cache 还被污染得厉害。DMA 引擎的存在就是把这件事从 CPU 手里接过去——外设侧的 DMA 控制器自己发起读请求或写请求直接把数据搬到内存里的指定缓冲区搬完再发一个中断通知你。DMA 不是 PCIE 的专利MCU 上的串口 DMA、SPI DMA 也是同样的思路外设数据到了硬件自动搬运CPU 只在开始和结束时参与。但 PCIE DMA 有个特殊之处——它的地址空间是系统物理地址不是外设自己的地址。描述符里填的是物理地址翻译由 IOMMU 或直通逻辑完成。你在 STM32 上写 DMA 的经验到这里只能当参考不能直接套因为描述符、门铃、中断完成机制完全是另一套复杂度。2.2 描述符环和门铃机制例程里最核心的数据结构PCIE DMA 例程的核心不是中断也不是寄存器而是描述符。描述符是一个结构体描述了“这段数据要从哪里搬到哪里、搬多长、搬完怎么标记”。软件把多个描述符放进一块内存形成环形队列硬件按顺序消费它们。常见的描述符结构长这样struct dma_desc { u64 src_addr; /* 源地址H2C 时是外设侧地址C2H 时是系统内存地址 */ u64 dst_addr; /* 目的地址对应另一侧 */ u32 len; /* 本段搬运的字节数 */ u32 ctrl; /* 控制位是否生成中断、是否最后一段 */ u32 status; /* 完成状态硬件搬完回写 */ u32 next; /* 指向下一个描述符的地址环状时可能用不到 */ };字段的意义很清楚src_addr 和 dst_addr 决定了搬运的起止len 决定了这段要搬多少ctrl 里的中断位决定搬完要不要打扰 CPUstatus 是硬件写回来的完成标记。例程里通常有两个环H2CHost to Card和 C2HCard to Host每个方向各一个生产者消费者队列。软件准备好描述符后写一次门铃寄存器硬件才去取描述符。门铃的作用是告诉 DMA 引擎“队列里有新活”。读描述符、搬运、回写 status、发中断这一串动作全由硬件完成。整个过程里 CPU 只做了两件事填描述符和写门铃。这就是 PCIE DMA 效率高的根本原因。注意门铃通常是一块 4KB BAR 空间里的一个偏移。改例程时最容易翻车的点就是门铃偏移——驱动里写的是寄存器偏移不是物理地址。2.3 读方向和写方向H2C 与 C2H 的差异H2C 是主机发往外设的数据比如你要往板卡上的 DDR 写一段配置或波形C2H 是外设发往主机的数据比如采集卡采到的数据送进内存。这两个方向在例程里往往各有一套描述符环和完成队列。C2H 方向通常比 H2C 方向难调。原因在于地址端主机侧缓冲区要提前分配好物理地址固定而且需要按对齐要求给出。很多例程会用一个dma_alloc_coherent或dma_map_single分配的缓冲区让硬件可以直接写入。如果你从用户态传一个普通 malloc 的地址下来驱动必须先把页表钉住、做地址映射否则 DMA 搬运到一半页面被换出数据就错了。H2C 方向的问题往往出在长度上有些例程的长度字段是 16 位的单次最多 64KB。你一次提交 1MB 的数据硬件可能只搬走低 16 位能表示的长度剩下的数据停在队列里表现为“传输卡死”。这种事不是代码 bug是描述符字段位宽的限制很多例程要自己拆分大块传输。3. PCIE DMA 例程第一次跑不通5 个必查的避坑点3.1 链路没起来lspci 里根本没有这张卡现象插上板卡后lspci看不到任何新设备驱动 probe 根本不会被调用。原因PCIE 枚举过程是 RC 在复位后发配置请求来完成的。如果板卡的 LTSSM 卡在 Detect 或 Polling 阶段RC 侧根本不知道这个设备存在后续一切免谈。解决先查物理层。供电和 100MHz 差分参考时钟是第一步金手指没插到位也很常见。然后确认主板上这个 PCIE 插槽本身是好的。lspci -vvv可以看到根端口下的总线状态对照热词里“pcie ltssm阶段的configuration阶段”的说法——如果根端口显示链路 Down问题几乎都在物理层不用急着怀疑代码。3.2 驱动加载成功但 DMA 传输超时门铃写进去了没反应现象驱动 probe 通过了/dev设备节点也出现了但每次启动 DMA 都卡死在等待完成的地方看dmesg没有任何错误。原因门铃寄存器调用错了。PCIE 设备的寄存器空间通过 BAR 映射驱动拿到的是pci_resource_start()返回的物理地址ioremap 之后才能用 readl/writel 访问。门铃的偏移在例程里写死了但你的板卡 BAR 布局和例程不同写到了错误的地址上。解决先用lspci -vvv查看设备的 BAR 资源确认寄存器空间落在哪个 BAR 和大小。然后用devmem直接读门铃对应偏移确认硬件是否有反应。驱动里加一个模块参数暴露门铃地址调试期从命令行传入省得反复编译。3.3 小包能传大包卡死长度字段位宽和地址对齐现象传 1KB 数据正常传 1MB 数据时要么只传了一部分要么直接超时。原因描述符的长度字段是 16 位或 24 位单次传输长度超过上限另一个常见原因是缓冲区首地址没有按 64 字节对齐硬件要求对齐才能启动搬运。解决把大块数据拆成多个描述符每个描述符的长度控制在字段上限以内。地址对齐问题用ALIGN()宏处理或者在分配缓冲区时直接按页对齐。很多例程里都会有一个dma_map_single()的调用检查传入的地址是不是已经被内核对齐过。3.4 中断风暴CPU 占用率 100%现象DMA 能传输但完成一次搬运后 CPU 占用率飙升系统响应变慢top里可以看到中断占用接近饱和。原因中断完成位没有在中断处理函数里清掉或者清的顺序不对。硬件发出中断后软件读完 status 寄存器必须写回完成标志硬件才会停止拉高中断线。还有一个常见情况MSI/MSI-X 配置不正确导致中断一直在触发。用 MSI 时软件侧清的是设备内部寄存器不是中断控制器。解决仔细看例程里的中断处理函数确认读 status 之后有没有写回。再看驱动的pci_alloc_irq_vectors()参数MSI 和 MSI-X 的申请方式不同中断号也不一样。用cat /proc/interrupts看中断号是否对应到设备。3.5 带宽只有理论值的一半MPS 和 MRRS 的参数在拖后腿现象链路显示 Gen3 x4理论上 4GB/s 的带宽实际测下来只有 2GB/s 左右读方向尤其明显。原因PCIE 报文大小不匹配。MPSMax Payload Size影响写方向MRRSMax Read Request Size影响读方向。DMA 读方向发起的读请求如果被 RC 限制在 512B效率会大打折扣因为要发很多个请求才能凑满一页。解决lspci -vvv 里看 DevCtl 的 MaxPayload 和 MaxReadReq 字段。驱动里在初始化时主动设置设备的 MaxReadRequest用pcie_set_readrq()把 MRRS 提上去。很多例程默认没做这一步性能差距在这里体现得最明显。4. 把例程改到自己的板卡BAR 映射、寄存器偏移和中断号的三处改动4.1 先搞清楚例程是为哪块板写的看厂商 ID 和设备 ID改例程的第一步不是改代码而是确认设备的识别信息。例程的驱动里通常有一个pci_device_id表列出它支持的厂商 ID 和设备 ID。你的板卡如果用的 FPGA 型号不同ID 很可能对不上。static const struct pci_device_id my_pcie_ids[] { { PCI_DEVICE(PCI_VENDOR_ID_XILINX, 0x9038) }, { PCI_DEVICE(PCI_VENDOR_ID_XILINX, 0x903F) }, {} }; MODULE_DEVICE_TABLE(pci, my_pcie_ids);这个表决定了设备枚举时驱动会不会绑定它。拿到板卡后先用lspci -n读出实际的 vendor ID 和 device ID填到这里。FPGA 的 device ID 通常是 bit 流里设定的同一个 IP 不同版本生成的 ID 可能不一样。改完别忘update-pciids或用 lspci -n 直接确认。4.2 改 BAR从 256MB 映射到 4KB 的寄存器空间PCIE 设备可以有好几个 BAR每个 BAR 映射一段物理空间。常见做法是 BAR0 放寄存器控制块BAR1 放 DMA 缓冲区或大块存储空间。例程里可能默认映射了 BAR0 和 BAR2你的板卡可能只有 BAR0 和 BAR1。static int my_pcie_probe(struct pci_dev *pdev, const struct pci_device_id *id) { resource_size_t bar0_start, bar0_len; void __iomem *bar0_virt; bar0_start pci_resource_start(pdev, 0); bar0_len pci_resource_len(pdev, 0); bar0_virt pci_ioremap_bar(pdev, 0); if (!bar0_virt) { pci_disable_device(pdev); return -ENOMEM; } pci_set_drvdata(pdev, bar0_virt); return 0; }pci_ioremap_bar 会把 BAR0 的物理地址映射到内核虚拟地址空间。注意 bar0_len 是从配置空间读出来的不是你想映射多大就映射多大。如果你的寄存器块只有 4KB而 BAR 声明的是 256MB那是 bit 流里配的问题驱动侧不用管只管读写对应偏移。寄存器偏移是另一个坑。例程里访问门铃的代码是writel(value, bar0_virt DOORBELL_OFFSET)DOORBELL_OFFSET 这个宏是例程作者定义的不一定适用于你的硬件。对照你的寄存器手册确认门铃、状态、控制寄存器各自的偏移改宏定义而不是改调用点。4.3 中断从 MSI 换到 MSI-X或反过来同一套 DMA 逻辑在不同 RC 平台上的中断行为可能差很多。例程默认用 MSI你的平台可能支持 MSI-X 更稳定或者例程用 MSI-X但你的 FPGA 配置没把 MSI-X 的 table 加上。改动集中在pci_alloc_irq_vectors的调用参数上。int nr_vecs pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI); if (nr_vecs 0) { nr_vecs pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_LEGACY); } err request_irq(pci_irq_vector(pdev, 0), my_pcie_irq_handler, IRQF_SHARED, my_pcie, pdev);pci_alloc_irq_vectors 的第一个参数是设备第二第三个参数是期望的最小和最大中断向量数第四个是中断类型标志。改成 PCI_IRQ_MSI_X 就是 MSI-X。注意 MSI 在有些老主板上只支持一个向量MSI-X 可以支持多个多队列 DMA 必须用 MSI-X。request_irq 之前用 pci_irq_vector 取得实际中断号不要在 probe 里写死。4.4 地址映射别忽略 IOMMU 对 DMA 地址的影响如果平台开启了 IOMMU驱动里拿到的物理地址并不一定是设备实际使用的地址。PCIE DMA 使用的是 DMA 地址dma_map_single返回的才是设备侧能用的地址。例程如果没有做 DMA API 的映射拿到物理地址直接填描述符在开启了 IOMMU 的平台上会搬运失败。dma_addr_t dma_handle; void *cpu_addr; cpu_addr dma_alloc_coherent(pdev-dev, buf_size, dma_handle, GFP_KERNEL);dma_alloc_coherent 返回两个地址CPU 侧虚拟地址和 DMA 侧地址。描述符里填的是 dma_handle不是 cpu_addr。反过来读取完成状态时要用 cpu_addr。这个双地址模型是 PCIE DMA 驱动最容易出错的地方很多时候驱动“看起来是对的”但搬运结果全是错的问题就出在这里。5. DMA 例程的性能验证带宽、延迟和缓存一致性5.1 用自写工具测吞吐读方向容易虚高例程跑通以后第一件事不是上业务而是测吞吐。dd 可以用来做粗测但 dd 走的是块设备路径并不适合直接验证字符设备的 DMA 吞吐。常见做法是写一个小工具用 mmap 映射驱动里的 DMA 缓冲区然后循环触发 DMA 搬运记录时间。int fd open(/dev/my_pcie, O_RDWR); void *buf mmap(NULL, BUF_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); struct timespec t0, t1; clock_gettime(CLOCK_MONOTONIC, t0); for (int i 0; i LOOP_COUNT; i) { ioctl(fd, CMD_START_DMA, cfg); ioctl(fd, CMD_WAIT_DONE, NULL); } clock_gettime(CLOCK_MONOTONIC, t1);mmap 返回的地址是用户态虚拟地址对应的物理页已经被驱动钉住。这里有一个经典的虚高陷阱如果测试 C2H 方向时硬件写进来的数据没有真正被 CPU 读走Cache 不回写DMA 缓冲区里的数据可能还停留在 Cache 中导致下一次搬运覆盖了旧数据测试结果看起来很快但数据是错的。刷 Cache 的操作在用户态不可控所以严格测试要在驱动侧做dma_sync_single_for_device或使用一致性 DMA 映射。5.2 用 lspci -vvv 确认链路速率和宽度在 Ubuntu 上查看 PCIE 速率最直接的方式是lspci -vvv。输出里找到你的设备节点看 LnkSta 一行的 Speed 和 Width。Speed 代表链路速率Width 代表通道数两者的组合决定理论带宽。链路速率编码单通道带宽 (GB/s)x4 带宽 (GB/s)x8 带宽 (GB/s)Gen1 (2.5GT/s)8b/10b0.251.02.0Gen2 (5GT/s)8b/10b0.52.04.0Gen3 (8GT/s)128b/130b0.9853.947.88Gen4 (16GT/s)128b/130b0.9857.8815.75理论值按字节算实际能到 70% 就算正常。如果 LnkSta 显示 Speed 是 2.5GT/s 而你的 FPGA 支持 Gen3原因是链路训练降级了PCIE 协议会在训练阶段协商出一个双方都支持的最低速率。影响降级的因素包括 PCB 走线长度、连接器质量和参考时钟稳定性。带宽上不去的另一个疑点是 MPS 和 MRRS。查看 DevCtl 字段里的 MaxPayload 和 MaxReadReq 值如果 MaxPayload 是 128B写方向性能会明显受限如果 MaxReadReq 是 512B 或更小读方向性能会明显受限。这个参数可以在这个设备的 -vvv 输出里直接看到也可以在驱动里用pcie_set_readrq()在 probe 阶段显式设置。5.3 Cache 一致性和屏障DMA 完成以后数据真的在内存里吗DMA 完成后驱动在中断处理里读取描述符的 status 字段确认搬运完成然后唤醒等待的进程。这里有一个关键问题CPU 读到 status 字段的时机可能早于硬件把数据写到内存的时机。因为 PCIE 写事务是 posted 的硬件可能已经发出写请求但数据还驻留在 RC 侧的写缓冲里。解决方式是用 DMA 屏障。Linux 下的dma_rmb()或mb()确保读操作发生在写请求真正落盘之后。描述符的 status 字段本身是硬件写回的CPU 读它的时候需要保证之前的读取不会被重排。在驱动代码里找到wait_event之前的读取操作确认有一道屏障否则调试时会出现“偶尔数据错位”的玄学问题。6. 从例子到产品在例程驱动里加一个 debugfs 手动触发节点例程跑通只是起点产品化时最常见的需求是脱离用户态工具独立验证 DMA。很多场景下没有屏幕、没有串口只能靠/sys和/debug看状态。我给例程驱动加一个 debugfs 节点用来手动触发一轮 DMA 并把寄存器快照打出来这个技巧在排查门铃和中断问题时特别有效。static ssize_t dbg_trigger_write(struct file *file, const char __user *buf, size_t count, loff_t *pos) { struct pci_dev *pdev file-private_data; void __iomem *bar0 pci_get_drvdata(pdev); struct dma_desc desc {0}; desc.src_addr dma_handle; desc.len TRIGGER_SIZE; desc.ctrl 1; /* 完成时产生中断 */ memcpy_toio(bar0 DESC_ADDR_OFFSET, desc, sizeof(desc)); writel(1, bar0 DOORBELL_OFFSET); pr_info(dbg: desc written, len%u\n, desc.len); return count; }这个节点做的事情很简单构造一个描述符、写门铃、打印日志。引脚和数据内容都不重要关键是让 DMA 引擎动起来然后你可以在dmesg里看中断有没有触发、status 有没有被回写。有一次我就是靠这个节点在没有上位机的情况下抓到了门铃偏移写错的问题——寄存器写下去了但硬件没有任何动作对照寄存器手册才发现门铃偏移多算了 0x10。产品化时这个 debugfs 节点还能升级成“DMA 环回测试”入口把 H2C 搬过去的数据再通过 C2H 搬回来比较内容是否一致用来验证链路质量。如果你要加多队列支持也可以从这里开始扩展——每个队列一个 debugfs 节点逐个验证。PCIE DMA 例子.7z 这类压缩包本质是一张地图地图画的是数据从板卡到内存的完整路径。照图走一遍比什么都强。希望帮到你。本文还有配套的精品资源点击获取
返回列表