rk3588/kernel-6.1 驱动 DMA 编程与 PCIe 设备 DMA 传输分析
1. DMA 编程的核心概念
DMA(Direct Memory Access)允许外设绕过 CPU,直接读写系统内存。
驱动 DMA 编程的关键不是“把 CPU 虚拟地址给硬件”,而是:
- 准备一块可 DMA 的内存;
- 通过 DMA mapping API 得到设备可访问的
dma_addr_t; - 把
dma_addr_t写入设备寄存器或 DMA 描述符; - 启动设备 DMA;
- 等待中断/轮询完成;
- 做 cache 同步和 unmap;
- CPU 再访问结果。
核心文件:
include/linux/dma-mapping.hkernel/dma/mapping.ckernel/dma/direct.ckernel/dma/coherent.ckernel/dma/contiguous.cDocumentation/core-api/dma-api.rstDocumentation/core-api/dma-api-howto.rst
2. DMA 地址与 CPU 地址区别
驱动中常见三种地址:
| 类型 | 含义 | 谁使用 |
|---|---|---|
| CPU 虚拟地址 | kmalloc() / dma_alloc_coherent() 返回的指针 |
CPU |
| CPU 物理地址 | 内存真实物理地址 | 内核/架构层 |
| DMA 地址 / bus 地址 | dma_addr_t,设备发起 DMA 时使用 |
外设 |
设备不能直接使用 CPU 虚拟地址。
在有 IOMMU 的平台上,dma_addr_t 甚至不等于 CPU 物理地址,它可能是 IOVA。
因此驱动必须使用 DMA API,而不是自己做 virt_to_phys()。
3. DMA 申请到底申请什么
驱动里说“申请 DMA”容易混淆,实际可能指三类不同资源:
| 说法 | 实际申请对象 | 常用接口 | 是否有数量限制 |
|---|---|---|---|
| 申请 DMA buffer | 一块设备可访问的内存或映射 | dma_alloc_coherent()、dma_map_single()、dma_map_sg() |
受内存、CMA、IOMMU、DMA mask 限制 |
| 申请 DMA channel | SoC DMA 控制器的一条通道 | dma_request_chan() |
受硬件 channel 数限制 |
| 申请 DMA 描述符/队列 | 设备或 DMA engine 用来描述传输的 ring/descriptor | 驱动私有分配、dmaengine_prep_*() |
受驱动队列深度、内存、硬件 ring 大小限制 |
所以要先区分:
dma_alloc_coherent()申请的是 DMA 一致性内存,不是申请一个“DMA 控制器”;dma_map_single()申请的是临时 DMA 地址映射,不是分配新内存;dma_request_chan()申请的是 DMAengine 通道,才和 SoC 上 DMA 控制器通道数量有关;- PCIe endpoint 自带 DMA engine 时,驱动通常只是把 host memory 映射成
dma_addr_t,然后把地址写给 PCIe 设备,不一定使用 SoC 通用 DMAengine。
3.1 CPU 上 DMA 有多少个
严格来说,DMA 不在 CPU 上。CPU core 负责执行指令,DMA 由外设或 DMA 控制器发起。
在 RK3588/Linux 中可能存在几类 DMA 发起者:
外设自带 DMA master
- PCIe endpoint、NVMe、USB xHCI、以太网、UFS/eMMC、GPU/NPU/VPU 等;
- 这些设备自己通过总线读写内存;
- 驱动用 DMA mapping API 给它们准备可访问地址。
SoC 通用 DMA 控制器
- 用于 SPI/I2C/UART/音频等外设搬运;
- 通过 DMAengine 框架抽象为若干
dma_chan; - channel 数量由硬件和设备树决定,不能无限申请。
PCIe/DWC eDMA 或 endpoint 内部 DMA
- 例如 DesignWare PCIe eDMA;
- 可能有独立 read/write channel;
- 通道数由 PCIe 控制器或 endpoint IP 决定。
因此“CPU 上 DMA 有多少个”更准确应问:
1 | 某个 DMA 控制器有多少 channel? |
这些数量不是由 CPU core 数直接决定的。
3.2 DMA 可以无限申请吗
不能。DMA 相关资源都不是无限的。
3.2.1 dma_alloc_coherent() 的限制
dma_alloc_coherent() 会分配 CPU 和设备长期共享的一致性 DMA 内存。它受以下因素限制:
- 系统可用物理内存;
- CMA 连续内存区域大小;
- IOMMU 映射资源;
- 设备
coherent_dma_mask; - DMA zone / DMA32 zone;
- 内存碎片;
- 调用时的 GFP 标志;
- 平台是否需要物理连续内存。
大块 coherent 内存尤其容易失败,因为它可能需要连续物理页或 CMA 支持。
典型失败处理:
1 | buf = dma_alloc_coherent(dev, size, &dma, GFP_KERNEL); |
驱动必须检查返回值,不能假设一定成功。
3.2.2 dma_map_single() / dma_map_sg() 的限制
streaming DMA 映射也不是无限的。它通常不分配数据内存,但会建立设备可访问的 DMA 地址映射。
受限于:
- IOMMU IOVA 地址空间;
- IOMMU page table 内存;
- SWIOTLB bounce buffer;
- 设备
dma_mask; - 单次映射最大长度;
- SG 段数量;
- cache 同步成本;
- 映射生命周期是否及时 unmap。
必须这样写:
1 | dma = dma_map_single(dev, buf, len, DMA_TO_DEVICE); |
如果 map 后忘记 unmap,会造成 IOMMU/调试资源泄漏,CONFIG_DMA_API_DEBUG 可能报 warning。
3.2.3 dma_request_chan() 的限制
如果使用 DMAengine:
1 | chan = dma_request_chan(dev, "rx"); |
申请的是 DMA 控制器通道。这个资源由硬件决定,数量有限。
特点:
- 一个 channel 被申请后通常独占,直到
dma_release_channel(); - 设备树中的
dmas/dma-names决定设备能申请哪些通道; - 如果通道已被其他驱动占用,可能返回
-EBUSY或-EPROBE_DEFER; - DMA 控制器硬件本身也有最大 channel 数、burst 能力、地址宽度限制。
DMAengine 文档中明确说明:dma_request_chan() 返回的 channel 在释放前由调用者独占。
3.2.4 PCIe DMA 的限制
PCIe DMA 传输中,常见限制来自 PCIe 设备本身:
- 设备 DMA mask 是 32-bit、48-bit 还是 64-bit;
- endpoint DMA engine 支持多少 channel;
- descriptor ring 深度;
- PRP/SGL/SG entry 数量;
- MSI/MSI-X 中断向量数;
- PCIe link speed 和 lane 数;
- IOMMU/SMMU 映射能力;
- host memory buffer 数量;
- 设备固件/硬件状态机是否支持并发队列。
例如 NVMe PCIe 设备不是“向 CPU 申请 DMA 个数”,而是:
1 | 驱动分配 submission/completion queue |
队列数量和深度由 NVMe 控制器能力、CPU 数、MSI-X 向量、内存和驱动策略共同决定。
3.3 驱动中 DMA 申请的推荐顺序
设备 probe 阶段通常:
1 | 1. 使能设备 |
示例:
1 | ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64)); |
运行时数据传输:
1 | dma = dma_map_single(dev, buf, len, DMA_FROM_DEVICE); |
4. DMA API 两大模式
4.1 Coherent DMA
接口:
1 | void *dma_alloc_coherent(struct device *dev, size_t size, |
特点:
- 分配后长期给设备和 CPU 共享;
- 返回 CPU 虚拟地址和 DMA 地址;
- CPU/设备访问一致性由平台保证;
- 常用于 DMA 描述符环、命令队列、完成队列、小型共享控制结构;
- 分配成本较高,不建议大量小块频繁申请释放。
示例:
1 | ring = dma_alloc_coherent(&pdev->dev, ring_size, &ring_dma, GFP_KERNEL); |
在本源码树中:
drivers/nvme/host/pci.c用dma_alloc_coherent()分配 NVMe SQ/CQ;drivers/misc/rockchip/pcie-rkep.c用dma_alloc_coherent()分配连续 DMA buffer;drivers/misc/pci_endpoint_test.c使用 streaming DMA 做传输测试。
4.2 Streaming DMA
接口:
1 | dma_addr_t dma_map_single(struct device *dev, void *cpu_addr, |
scatter-gather:
1 | int dma_map_sg(struct device *dev, struct scatterlist *sg, |
特点:
- 适合一次性或短期 DMA;
- map 时把 CPU buffer 转成设备可访问 DMA 地址;
- unmap 后 CPU 才能安全访问结果;
- 对非一致性 cache 平台,map/unmap 会处理必要 cache 同步;
- 必须检查
dma_mapping_error()。
方向含义:
DMA_TO_DEVICE:CPU 写好内存,设备读取;DMA_FROM_DEVICE:设备写内存,CPU 之后读取;DMA_BIDIRECTIONAL:双向;DMA_NONE:调试用,不用于真实传输。
示例:
1 | buf = kmalloc(size, GFP_KERNEL); |
5. DMA mask 设置
PCIe 设备驱动必须先声明设备支持的 DMA 地址宽度。
常用接口:
1 | dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64)); |
如果设备只支持 32-bit DMA:
1 | dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32)); |
典型 fallback:
1 | if (dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64)) && |
在 drivers/misc/pci_endpoint_test.c 中就是先尝试 48-bit,再 fallback 到 32-bit。
在 drivers/nvme/host/pci.c 中会根据控制器能力设置 DMA mask。
在 drivers/dma/dw-edma/dw-edma-pcie.c 中 DesignWare eDMA PCIe 驱动设置 64-bit DMA mask。
6. 什么内存可以做 DMA
推荐:
dma_alloc_coherent()返回内存;kmalloc()/kzalloc()返回内存,再dma_map_single();- page allocator 分配的 page,再
dma_map_page(); - block/network 子系统传来的 bio/skb buffer;
- scatterlist / sg_table,经
dma_map_sg()或dma_map_sgtable()。
避免:
- 直接对
vmalloc()地址做dma_map_single(); - 直接对内核栈地址做 DMA;
- 直接对模块 text/data 地址做 DMA;
- 使用
virt_to_phys()替代 DMA API; - map 后 CPU 和设备同时无同步地修改同一 streaming buffer。
7. PCIe 设备驱动 DMA 初始化流程
PCIe 驱动通常先完成 PCI 设备初始化,再处理 DMA。
典型 probe 流程:
1 | pci_driver.probe() |
关键点:
pci_enable_device():启用 PCI 设备资源;pci_request_regions():声明占用 BAR;pci_iomap():映射 BAR 寄存器;dma_set_mask_and_coherent():设置 DMA 地址能力;pci_set_master():允许设备作为 bus master 发起 DMA;pci_alloc_irq_vectors():申请 MSI/MSI-X/legacy 中断;request_irq():注册中断处理函数。
drivers/misc/pci_endpoint_test.c 的初始化就是典型参考:
1 | dma_set_mask_and_coherent() |
8. PCIe BAR 空间介绍
BAR(Base Address Register)是 PCI/PCIe 设备配置空间中的基址寄存器,用来告诉 host:这个设备需要哪些地址窗口。
Linux 枚举 PCIe 设备时,会读取 endpoint 的 BAR 需求,为它分配 CPU/PCIe 可访问的地址范围,然后驱动再把这些地址范围映射成内核可访问的 MMIO 指针。
8.1 BAR 的作用
PCIe 设备常用 BAR 暴露:
- 设备控制寄存器;
- DMA engine 控制寄存器;
- doorbell 寄存器;
- MSI-X table / PBA;
- 设备本地 SRAM 或共享内存窗口;
- endpoint 暴露给 host 的 inbound/outbound 地址窗口。
驱动通过 BAR 访问设备寄存器,本质是 CPU 发起 PCIe Memory Read/Write TLP 到 endpoint。
例如:
1 | CPU writel() |
8.2 BAR 与 DMA 地址的区别
BAR 地址和 DMA 地址不是一回事:
| 类型 | 谁使用 | 作用 |
|---|---|---|
| BAR MMIO 地址 | CPU/驱动 | CPU 访问 PCIe 设备寄存器或设备内存窗口 |
DMA 地址 dma_addr_t |
PCIe 设备 | 设备访问 host memory |
| CPU 虚拟地址 | CPU | 内核访问 host memory |
典型关系:
1 | BAR: CPU -> PCIe endpoint |
所以驱动启动 PCIe DMA 时,通常是:
1 | 1. CPU 分配/映射 host buffer,得到 dma_addr_t |
也就是说,BAR 是“控制通道”,DMA 地址是“数据搬运地址”。
8.3 Linux 驱动如何获取和映射 BAR
常见接口:
1 | resource_size_t start = pci_resource_start(pdev, bar); |
手动方式:
1 | ret = pci_request_regions(pdev, driver_name); |
或者托管方式:
1 | ret = pcim_iomap_regions(pdev, BIT(bar), driver_name); |
有些驱动也会使用:
1 | regs = pci_ioremap_bar(pdev, bar); |
访问 BAR 寄存器时应使用 MMIO API:
1 | writel(value, regs + REG_OFFSET); |
不要把 BAR 当普通内存随意 memcpy(),除非确认该 BAR 是可按内存语义访问的设备内存窗口,并且处理好 ordering、cache、端序和对齐要求。
8.4 BAR 类型
常见 BAR 类型:
- I/O BAR:传统 x86 I/O port 空间,现代 ARM64/RK3588 场景较少用;
- Memory BAR:最常见,映射为 MMIO;
- 32-bit Memory BAR:BAR 地址窗口位于 32-bit PCI 地址空间;
- 64-bit Memory BAR:占用两个 BAR slot,可映射到 64-bit PCI 地址空间;
- Prefetchable BAR:设备声明该窗口可预取,常用于 frame buffer 或设备内存,不常用于普通控制寄存器。
驱动通常通过 pci_resource_flags() 判断 BAR 类型:
1 | if (!(pci_resource_flags(pdev, bar) & IORESOURCE_MEM)) |
8.5 BAR 空间大小和限制
BAR 空间不是无限的,它受以下因素限制:
- endpoint 硬件实现的 BAR 数量和大小;
- Root Complex 分配的 PCI bus address window;
- 设备树中 PCIe
ranges描述的 MEM window; - 32-bit/64-bit BAR 类型;
- bridge 下游地址空间;
- IOMMU/SMMU 与 PCIe host bridge 配置;
- 多设备共享 PCIe 地址窗口时的资源分配结果。
如果 BAR 分配或映射失败,驱动通常会在 pci_request_regions()、pci_iomap()、pcim_iomap_regions() 阶段失败。
调试时可查看:
1 | lspci -vv |
8.6 PCIeDrv 中的 BAR0 用法
自定义 PCIeDrv 驱动只使用 BAR0 作为 FPGA 控制寄存器空间。
其 pcie_hw_init() 流程是:
1 | pci_resource_start(dev, 0) |
后续驱动通过 BAR0 写 FPGA 寄存器:
1 | TX_BUF_ADDR_L / TX_DESC_ADDR |
这些 BAR0 寄存器本身不搬运大数据。它们的作用是控制 FPGA DMA 逻辑,例如:
1 | CPU 通过 BAR0 写入 host DMA buffer 地址 |
因此在 PCIeDrv 中:
- BAR0 是 CPU 控制 FPGA 的寄存器通道;
dma_alloc_coherent()得到的dma_addr_t是 FPGA DMA 访问 host memory 的地址;pci_set_master()允许 FPGA endpoint 发起 PCIe Memory Read/Write;TX_SEND_POS/RX_CHNL_STATE这类 BAR0 寄存器相当于 DMA doorbell/start 控制。
9. PCIe DMA 传输模型
PCIe DMA 通常有两种角色视角。
9.1 RK3588 作为 Root Complex,驱动控制 Endpoint 设备 DMA
这是常见 PCIe 外设驱动模型,例如 NVMe、网卡、采集卡。
流程:
1 | CPU 分配 host memory buffer |
注意:PCIe endpoint 做 DMA 时,设备看到的是 DMA 地址,不是 CPU 虚拟地址。
9.2 RK3588 作为 Endpoint,被主机通过 PCIe 访问
Endpoint 侧常见做法:
- 通过 BAR 暴露寄存器或共享内存窗口;
- 在 EP 内部分配可 DMA buffer;
- 通过 endpoint controller 配置 inbound/outbound ATU;
- 和 host 通过 MSI/doorbell/共享寄存器同步;
- 可使用 DesignWare eDMA 或平台 DMA 引擎搬运数据。
在这棵 Rockchip 内核中,drivers/misc/rockchip/pcie-rkep.c 是重要参考:
pci_enable_device()pci_request_regions()pci_iomap()映射 BAR0/BAR2/BAR4pci_alloc_irq_vectors()申请 MSIdma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64))dma_alloc_coherent()分配连续 DMA bufferpcie_dw_dmatest_register()接入 DesignWare DMA 测试对象
10. PCIe streaming DMA 示例:pci_endpoint_test.c
drivers/misc/pci_endpoint_test.c 展示了 PCIe endpoint 测试驱动如何用 streaming DMA。
写测试的关键流程:
1 | kzalloc source buffer |
读测试的关键流程:
1 | kzalloc destination buffer |
copy 测试:
1 | source: DMA_TO_DEVICE |
这里的关键细节:
- 传给设备的是
dma_addr_t; DMA_FROM_DEVICEbuffer 必须 unmap 后 CPU 再读;- 通过
completion等待 IRQ; - 支持 alignment 修正;
- 可通过 flag 告诉 endpoint 是否使用 DMA。
11. PCIe coherent DMA 示例:Rockchip pcie-rkep.c
drivers/misc/rockchip/pcie-rkep.c 中:
1 | vir_addr = dma_alloc_coherent(&pcie_rkep->pdev->dev, param->size, |
它把:
- CPU 可访问地址保存为
vir_addr - 设备可访问地址保存为
dma_addr dma_addr返回给上层参数
释放时:
1 | dma_free_coherent(&pcie_rkep->pdev->dev, size, vir_addr, dma_addr); |
这种方式适合:
- host 与 endpoint 长期共享的 DMA buffer;
- 控制结构;
- 不希望每次传输都 map/unmap 的场景;
- 需要返回 DMA 地址给对端/用户态控制逻辑的场景。
注意:
- coherent 不是“无限制可随便并发修改”;
- 仍需要协议层同步所有权,例如 doorbell、状态位、memory barrier、中断;
- 大块 coherent 内存可能消耗 CMA/IOMMU 资源。
12. 自定义 PCIe FPGA 驱动 DMA 示例:PCIeDrv
/home/work2/SeagullYpcEncode/linuxDoc/pcie/drivers/PCIeDrv 是一个典型的 PCIe FPGA 设备驱动案例。
它不是使用 RK3588 的 dmac0 / dmac1 / dmac2 通用 DMA 控制器,而是让 PCIe FPGA endpoint 自己作为 bus master 发起 DMA。
12.1 probe 阶段使能 PCIe 设备 DMA 能力
在 main.c 的 pcie_fpga_probe() 中,驱动完成 PCIe 设备使能、DMA mask 设置、BAR 映射和 bus master 使能:
1 | pci_enable_device_mem() |
其中:
dma_set_mask_and_coherent(&dev->dev, DMA_BIT_MASK(32))表示该驱动当前按 32-bit DMA 地址工作;pci_set_master(dev)允许 PCIe FPGA 主动访问 host memory;pcie_hw_init()使用pci_resource_start()/pci_resource_len()获取 BAR0 资源,并用ioremap()映射 FPGA 寄存器空间;pcie_hw_configure_global_hardware()会写FPGA_REG_DMA_MODE,当前默认配置为FPGA_DMA32。
因此,只要 probe 成功,PCIe 设备已经具备发起 DMA 的前置条件。
12.2 分配 TX/RX DMA buffer 和 descriptor ring
在 pcie_hw.c 的 pcie_chnl_alloc_ring_resource() 中,驱动为每个通道分配 TX/RX 两组资源:
1 | TX descriptor coherent buffer |
核心接口是:
1 | ring_object->_desc_buf = dma_alloc_coherent(dev, desc_size, |
这里:
_desc_buf/_dma_buf是 CPU 访问的虚拟地址;_phy_desc_buf/_phy_dma_buf是 FPGA 通过 PCIe DMA 访问的dma_addr_t;- 使用 coherent DMA 后,CPU 和 FPGA 可以长期共享这些 ring/buffer;
- 仍然需要通过 token、rpos/wpos、寄存器 doorbell 和内存屏障同步所有权。
该驱动每个通道有 TX 和 RX 两套 DMA buffer。根据 channel.h:
1 | 每组 32 个元素 |
所以单通道数据 DMA buffer 规模大致是:
1 | TX: 12288 * 128 = 1.5 MiB |
如果启用 4 个通道,仅 TX/RX 数据 DMA buffer 就约 12 MiB,全部来自 dma_alloc_coherent(),不是无限资源。
12.3 把 DMA 地址写入 FPGA BAR0 寄存器
在 pcie_chnl_configure_ring_resource() 中,驱动把 coherent buffer 的 DMA 地址写入 FPGA 寄存器:
1 | TX_BUF_ADDR_L <- tx data dma_addr_t |
这些寄存器来自 pcie_hw_define.h,例如:
1 | FPGA_REG_TX0_BUF_ADDR_L |
这一步的含义是:驱动把 host 侧 DMA buffer 地址告诉 FPGA,后续 FPGA DMA engine 就可以直接通过 PCIe Memory Read/Write 访问这些地址。
需要注意:代码结构中虽然定义了 _tx_dma_buf_addr_h / _rx_dma_buf_addr_h,但实际配置路径主要写低 32 位地址,并且 DMA mask 也设置为 32-bit,因此当前实现是 32-bit DMA 模式。
12.4 RX DMA 启动路径
RX 方向表示 FPGA 向 host memory 写数据。
用户打开字符设备时:
1 | pcie_fpga_open() |
在 PMD 模式下:
1 | pcie_chnl_user_start() |
这一步会使能 FPGA RX 通道。之后 FPGA 逻辑可以把收到的数据 DMA 写入驱动分配的 RX coherent buffer,并更新 descriptor/token。
CPU 侧轮询或中断处理完成后,会移动 RX rpos,并写 FPGA_REG_RXx_RECV_POS 通知 FPGA 哪些 RX buffer 已经被 CPU 消费,可以继续复用。
12.5 TX DMA 启动路径
TX 方向表示 FPGA 从 host memory 读数据。
用户写字符设备时:
1 | pcie_fpga_write() |
_pcie_chnl_do_check_tx() 会:
- 从用户发送队列取数据;
- 拷贝到 TX coherent DMA buffer;
- 填写 TX descriptor;
- 更新 TX token 的 wpos;
- 执行
smp_mb(); - 写
FPGA_REG_TXx_SEND_POS通知 FPGA。
关键动作是:
1 | pcie_tx_add_wpos_value() |
写 TX_SEND_POS 之后,FPGA 逻辑知道 host TX buffer 中有新数据,于是通过 PCIe DMA 从 host memory 读取数据并发送到 FPGA 内部链路。
12.6 这个驱动是否“启动了 PCIe DMA”
结论:启动了。
判断依据:
- probe 中调用了
pci_set_master(),允许 PCIe endpoint 主动访问 host memory; - 使用
dma_alloc_coherent()分配 TX/RX descriptor 和数据 buffer; - 把
dma_addr_t写入 FPGA BAR0 寄存器; - RX 方向通过
RX_CHNL_STATE使能 FPGA 写 host memory; - TX 方向通过
TX_SEND_POSdoorbell 通知 FPGA 读 host memory; - 驱动中没有
dma_request_chan(),说明它不是使用 RK3588 SoC 通用 DMA channel; - 真正搬运数据的是 PCIe FPGA endpoint 内部 DMA/逻辑。
这个驱动的数据流可以概括为:
1 | RX: FPGA -> PCIe Memory Write -> host RX coherent buffer -> CPU/用户态读取 |
因此它属于“驱动分配 host DMA 内存 + 配置 endpoint BAR 寄存器 + endpoint 自己发起 DMA”的 PCIe DMA 模型。
13. Scatter-Gather DMA
大块或非连续用户 buffer 通常用 SG。
常见流程:
1 | pin user pages / 构造 sg_table |
NVMe PCI 驱动是典型例子:
dma_map_sgtable()映射 request 的 sg_table;- 根据设备能力构造 PRP 或 SGL;
- 提交 SQ doorbell;
- 完成后
dma_unmap_sgtable()。
drivers/nvme/host/pci.c 关键路径:
1 | nvme_queue_rq() |
14. DMAengine 与 PCIe eDMA
有些控制器不是设备自己管理 DMA 描述符,而是通过 Linux DMAengine 框架抽象。
关键头文件:
include/linux/dmaengine.h
常见 client 流程:
1 | dma_request_chan() |
PCIe DesignWare eDMA 驱动:
drivers/dma/dw-edma/dw-edma-pcie.cdrivers/dma/dw-edma/dw-edma-core.cdrivers/dma/dw-edma/dw-edma-v0-core.c
dw-edma-pcie.c 做了:
pcim_enable_device()pcim_iomap_regions()映射 eDMA BAR;pci_set_master()dma_set_mask_and_coherent(..., 64-bit)pci_alloc_irq_vectors()- 根据 VSEC 或静态表获取 eDMA register/linked-list/data region;
- 注册 DesignWare eDMA core。
Rockchip pcie-rkep.c 中的 pcie_dw_dmatest_register() 说明它可以接入 DesignWare PCIe DMA 测试/封装逻辑。
15. PCIe DMA 传输伪代码
15.1 probe 初始化
1 | static int my_pci_probe(struct pci_dev *pdev, |
15.2 Host-to-device DMA
设备从 host memory 读数据:
1 | buf = kmalloc(len, GFP_KERNEL); |
15.3 Device-to-host DMA
设备向 host memory 写数据:
1 | buf = kmalloc(len, GFP_KERNEL); |
16. Cache 同步与内存屏障
Streaming DMA 的所有权规则:
dma_map_*()后,buffer 属于设备;- CPU 不应再修改
DMA_TO_DEVICEbuffer; - CPU 不应读取未完成的
DMA_FROM_DEVICEbuffer; - 完成后通过
dma_unmap_*()或dma_sync_*_for_cpu()交还 CPU; - 如果要再次给设备用,调用
dma_sync_*_for_device()。
对于 coherent DMA:
- 不需要 streaming map/unmap;
- 但写寄存器通知设备前,仍可能需要
wmb()确保描述符内容先于 doorbell 可见; - 中断中读取设备写回状态时,按硬件协议考虑
readl()、dma_rmb()等同步。
典型顺序:
1 | desc->addr = cpu_to_le64(dma); |
17. PCIe DMA 常见错误
没有调用
pci_set_master()- 设备不能作为 bus master 发起 DMA。
没有设置 DMA mask
- 高地址内存可能映射出设备无法访问的 DMA 地址。
把 CPU 虚拟地址写给设备
- 设备必须使用
dma_addr_t。
- 设备必须使用
对
vmalloc/栈内存做 DMA- 可能地址不连续或 cacheline 不安全。
方向写错
DMA_TO_DEVICE和DMA_FROM_DEVICE方向错误会导致 cache 同步错误。
忘记 unmap
- IOMMU 映射泄漏,debug DMA API 会报错。
SG nents 用错
dma_map_sg()返回 mapped nents;unmap 仍使用原始 nents。
提前读取 DMA_FROM_DEVICE buffer
- 必须等完成并 unmap/sync 后 CPU 再读。
中断先于数据可见
- 需要按硬件协议使用 barrier/readl 保证顺序。
coherent buffer 被当作普通 cache buffer 优化
- coherent 适合控制结构,不一定适合超大吞吐数据面。
18. RK3588 调试建议
常用内核配置:
CONFIG_DMA_API_DEBUGCONFIG_DMA_CMACONFIG_CMACONFIG_IOMMU_SUPPORTCONFIG_PCI_ENDPOINTCONFIG_PCIE_DWCONFIG_DW_EDMA
常用观测:
dmesg | grep -i dmadmesg | grep -i iommudmesg | grep -i pcie/proc/iomem/proc/interrupts/sys/kernel/debug/dma-api/(启用 DMA API debug)/sys/kernel/debug/tracing/events/dma_fence/(部分场景)- PCIe 设备 BAR:
lspci -vv(目标系统有 pciutils 时)
PCIe DMA 传输排查顺序:
- PCIe 链路是否枚举成功;
- BAR 是否 request/iomap 成功;
- DMA mask 是否设置成功;
pci_set_master()是否调用;- MSI/MSI-X 是否申请并触发;
- DMA 地址是否按低 32/高 32 正确写入;
- DMA 方向是否正确;
- 设备端 DMA engine 状态寄存器是否报错;
- 是否有 IOMMU fault;
- 是否有 DMA API debug warning。
19. 总结
驱动里做 DMA 编程的核心原则:
- 用
dma_addr_t给设备,不给 CPU 虚拟地址; - probe 中先设置 DMA mask;
- PCIe 设备要
pci_set_master(); - DMA 不在 CPU 上,真正发起 DMA 的是外设、DMA 控制器或 PCIe endpoint DMA engine;
- DMA 不能无限申请,受内存、CMA、IOMMU IOVA、DMA mask、DMA channel、硬件队列深度限制;
- 控制结构用
dma_alloc_coherent(); - 数据面短期传输用
dma_map_single()/dma_map_sg(); - 完成后必须
dma_unmap_*()或dma_sync_*(); - PCIe DMA 通过 BAR 寄存器/描述符把 DMA 地址、长度、方向告诉 endpoint;
- 中断或轮询确认完成后,CPU 才能读取
DMA_FROM_DEVICE数据; - RK3588 PCIe 场景可参考
pci_endpoint_test.c、pcie-rkep.c、dw-edma-pcie.c以及自定义PCIeDrvFPGA 驱动。
一句话概括:
PCIe DMA 传输的本质是驱动把 host memory 映射成设备可见的 DMA 地址,再通过 PCIe BAR/描述符启动设备 DMA engine,由设备发起 Memory Read/Write 完成数据搬运。
正在加载留言…