rk3588/kernel-6.1 驱动 DMA 编程与 PCIe 设备 DMA 传输分析

rk3588/kernel-6.1 驱动 DMA 编程与 PCIe 设备 DMA 传输分析

1. DMA 编程的核心概念

DMA(Direct Memory Access)允许外设绕过 CPU,直接读写系统内存。
驱动 DMA 编程的关键不是“把 CPU 虚拟地址给硬件”,而是:

  1. 准备一块可 DMA 的内存;
  2. 通过 DMA mapping API 得到设备可访问的 dma_addr_t
  3. dma_addr_t 写入设备寄存器或 DMA 描述符;
  4. 启动设备 DMA;
  5. 等待中断/轮询完成;
  6. 做 cache 同步和 unmap;
  7. CPU 再访问结果。

核心文件:

  • include/linux/dma-mapping.h
  • kernel/dma/mapping.c
  • kernel/dma/direct.c
  • kernel/dma/coherent.c
  • kernel/dma/contiguous.c
  • Documentation/core-api/dma-api.rst
  • Documentation/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 发起者:

  1. 外设自带 DMA master

    • PCIe endpoint、NVMe、USB xHCI、以太网、UFS/eMMC、GPU/NPU/VPU 等;
    • 这些设备自己通过总线读写内存;
    • 驱动用 DMA mapping API 给它们准备可访问地址。
  2. SoC 通用 DMA 控制器

    • 用于 SPI/I2C/UART/音频等外设搬运;
    • 通过 DMAengine 框架抽象为若干 dma_chan
    • channel 数量由硬件和设备树决定,不能无限申请。
  3. PCIe/DWC eDMA 或 endpoint 内部 DMA

    • 例如 DesignWare PCIe eDMA;
    • 可能有独立 read/write channel;
    • 通道数由 PCIe 控制器或 endpoint IP 决定。

因此“CPU 上 DMA 有多少个”更准确应问:

1
2
3
4
某个 DMA 控制器有多少 channel?
某个 PCIe 设备有多少 DMA queue / descriptor ring?
某个设备支持多大 DMA 地址宽度和多少 outstanding request?
系统有多少可用于 DMA 的内存/IOMMU IOVA/CMA?

这些数量不是由 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
2
3
buf = dma_alloc_coherent(dev, size, &dma, GFP_KERNEL);
if (!buf)
return -ENOMEM;

驱动必须检查返回值,不能假设一定成功。

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
2
3
4
5
6
dma = dma_map_single(dev, buf, len, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma))
return -EIO;

/* 设备完成 DMA 后 */
dma_unmap_single(dev, dma, 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
2
3
4
5
驱动分配 submission/completion queue
-> 分配/映射 request buffer
-> 构造 PRP/SGL
-> 写 doorbell
-> NVMe 控制器自己发起 PCIe DMA

队列数量和深度由 NVMe 控制器能力、CPU 数、MSI-X 向量、内存和驱动策略共同决定。

3.3 驱动中 DMA 申请的推荐顺序

设备 probe 阶段通常:

1
2
3
4
5
6
1. 使能设备
2. 设置 DMA mask
3. 申请 BAR/寄存器资源
4. 申请 IRQ
5. 分配长期 coherent DMA ring/descriptor
6. 运行时按请求 map/unmap streaming buffer

示例:

1
2
3
4
5
6
7
8
9
ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(64));
if (ret)
ret = dma_set_mask_and_coherent(dev, DMA_BIT_MASK(32));
if (ret)
return ret;

ring = dma_alloc_coherent(dev, ring_size, &ring_dma, GFP_KERNEL);
if (!ring)
return -ENOMEM;

运行时数据传输:

1
2
3
4
5
6
7
dma = dma_map_single(dev, buf, len, DMA_FROM_DEVICE);
if (dma_mapping_error(dev, dma))
return -EIO;

/* 启动硬件 DMA */

dma_unmap_single(dev, dma, len, DMA_FROM_DEVICE);

4. DMA API 两大模式

4.1 Coherent DMA

接口:

1
2
3
4
5
void *dma_alloc_coherent(struct device *dev, size_t size,
dma_addr_t *dma_handle, gfp_t gfp);

void dma_free_coherent(struct device *dev, size_t size,
void *cpu_addr, dma_addr_t dma_handle);

特点:

  • 分配后长期给设备和 CPU 共享;
  • 返回 CPU 虚拟地址和 DMA 地址;
  • CPU/设备访问一致性由平台保证;
  • 常用于 DMA 描述符环、命令队列、完成队列、小型共享控制结构;
  • 分配成本较高,不建议大量小块频繁申请释放。

示例:

1
2
3
4
5
6
ring = dma_alloc_coherent(&pdev->dev, ring_size, &ring_dma, GFP_KERNEL);
if (!ring)
return -ENOMEM;

writel(lower_32_bits(ring_dma), regs + RING_BASE_LO);
writel(upper_32_bits(ring_dma), regs + RING_BASE_HI);

在本源码树中:

  • drivers/nvme/host/pci.cdma_alloc_coherent() 分配 NVMe SQ/CQ;
  • drivers/misc/rockchip/pcie-rkep.cdma_alloc_coherent() 分配连续 DMA buffer;
  • drivers/misc/pci_endpoint_test.c 使用 streaming DMA 做传输测试。

4.2 Streaming DMA

接口:

1
2
3
4
5
dma_addr_t dma_map_single(struct device *dev, void *cpu_addr,
size_t size, enum dma_data_direction dir);

void dma_unmap_single(struct device *dev, dma_addr_t dma_addr,
size_t size, enum dma_data_direction dir);

scatter-gather:

1
2
3
4
5
int dma_map_sg(struct device *dev, struct scatterlist *sg,
int nents, enum dma_data_direction dir);

void dma_unmap_sg(struct device *dev, struct scatterlist *sg,
int nents, enum dma_data_direction dir);

特点:

  • 适合一次性或短期 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
2
3
4
5
6
7
8
9
10
11
12
13
14
buf = kmalloc(size, GFP_KERNEL);
if (!buf)
return -ENOMEM;

dma = dma_map_single(dev, buf, size, DMA_TO_DEVICE);
if (dma_mapping_error(dev, dma)) {
kfree(buf);
return -EIO;
}

/* 将 dma 写入设备寄存器/描述符,启动 DMA */

dma_unmap_single(dev, dma, size, DMA_TO_DEVICE);
kfree(buf);

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
2
3
4
5
if (dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64)) &&
dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32))) {
dev_err(&pdev->dev, "no suitable DMA mask\n");
return -ENODEV;
}

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
2
3
4
5
6
7
8
9
10
pci_driver.probe()
-> pci_enable_device() / pcim_enable_device()
-> pci_request_regions() / pcim_iomap_regions()
-> pci_iomap() / pcim_iomap_table()
-> dma_set_mask_and_coherent()
-> pci_set_master()
-> pci_alloc_irq_vectors()
-> request_irq() / pci_request_irq()
-> 初始化 DMA ring / descriptor / buffer
-> 注册 char/block/net/misc 设备

关键点:

  • 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
2
3
4
5
6
7
8
dma_set_mask_and_coherent()
pci_enable_device()
pci_request_regions()
pci_set_master()
pci_alloc_irq_vectors()
pci_ioremap_bar()
devm_request_irq()
misc_register()

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
2
3
4
5
CPU writel()
-> host bridge
-> PCIe Memory Write TLP
-> endpoint BAR 命中
-> endpoint 内部寄存器被写入

8.2 BAR 与 DMA 地址的区别

BAR 地址和 DMA 地址不是一回事:

类型 谁使用 作用
BAR MMIO 地址 CPU/驱动 CPU 访问 PCIe 设备寄存器或设备内存窗口
DMA 地址 dma_addr_t PCIe 设备 设备访问 host memory
CPU 虚拟地址 CPU 内核访问 host memory

典型关系:

1
2
BAR: CPU -> PCIe endpoint
DMA: PCIe endpoint -> host memory

所以驱动启动 PCIe DMA 时,通常是:

1
2
3
4
1. CPU 分配/映射 host buffer,得到 dma_addr_t
2. CPU 通过 BAR 寄存器把 dma_addr_t 写给 endpoint
3. CPU 通过 BAR doorbell/start 寄存器通知 endpoint
4. endpoint 使用 dma_addr_t 对 host memory 发起 DMA

也就是说,BAR 是“控制通道”,DMA 地址是“数据搬运地址”。

8.3 Linux 驱动如何获取和映射 BAR

常见接口:

1
2
3
resource_size_t start = pci_resource_start(pdev, bar);
resource_size_t len = pci_resource_len(pdev, bar);
unsigned long flags = pci_resource_flags(pdev, bar);

手动方式:

1
2
ret = pci_request_regions(pdev, driver_name);
regs = pci_iomap(pdev, bar, 0);

或者托管方式:

1
2
ret = pcim_iomap_regions(pdev, BIT(bar), driver_name);
regs = pcim_iomap_table(pdev)[bar];

有些驱动也会使用:

1
regs = pci_ioremap_bar(pdev, bar);

访问 BAR 寄存器时应使用 MMIO API:

1
2
writel(value, regs + REG_OFFSET);
value = readl(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
2
if (!(pci_resource_flags(pdev, bar) & IORESOURCE_MEM))
return -ENODEV;

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
2
3
4
lspci -vv
/sys/bus/pci/devices/0000:xx:yy.z/resource
/proc/iomem
dmesg 中 PCI resource allocation 日志

8.6 PCIeDrv 中的 BAR0 用法

自定义 PCIeDrv 驱动只使用 BAR0 作为 FPGA 控制寄存器空间。

pcie_hw_init() 流程是:

1
2
3
4
5
pci_resource_start(dev, 0)
-> pci_resource_len(dev, 0)
-> pci_resource_flags(dev, 0) 检查 IORESOURCE_MEM
-> request_mem_region()
-> ioremap()

后续驱动通过 BAR0 写 FPGA 寄存器:

1
2
3
4
5
6
TX_BUF_ADDR_L / TX_DESC_ADDR
RX_BUF_ADDR_L / RX_DESC_ADDR
TX_SEND_POS
RX_RECV_POS
RX_CHNL_STATE
DMA_MODE

这些 BAR0 寄存器本身不搬运大数据。它们的作用是控制 FPGA DMA 逻辑,例如:

1
2
3
CPU 通过 BAR0 写入 host DMA buffer 地址
CPU 通过 BAR0 写 TX_SEND_POS / RX_CHNL_STATE
FPGA 根据这些寄存器对 host memory 发起 PCIe DMA

因此在 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
2
3
4
5
6
7
8
CPU 分配 host memory buffer
-> dma_map_single/dma_map_sg 得到 DMA 地址
-> 驱动把 DMA 地址写入 PCIe endpoint 的 BAR 寄存器或描述符
-> writel(COMMAND_START) 启动 endpoint DMA engine
-> endpoint 从/向 host memory 发起 PCIe Memory Read/Write
-> endpoint 触发 MSI/MSI-X
-> 中断处理完成,driver unmap buffer
-> CPU 读取结果

注意: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/BAR4
  • pci_alloc_irq_vectors() 申请 MSI
  • dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64))
  • dma_alloc_coherent() 分配连续 DMA buffer
  • pcie_dw_dmatest_register() 接入 DesignWare DMA 测试对象

10. PCIe streaming DMA 示例:pci_endpoint_test.c

drivers/misc/pci_endpoint_test.c 展示了 PCIe endpoint 测试驱动如何用 streaming DMA。

写测试的关键流程:

1
2
3
4
5
6
7
8
9
kzalloc source buffer
-> get_random_bytes()
-> dma_map_single(..., DMA_TO_DEVICE)
-> 写 LOWER_SRC_ADDR / UPPER_SRC_ADDR
-> 写 SIZE / FLAGS / IRQ_TYPE
-> 写 COMMAND_READ
-> wait_for_completion()
-> dma_unmap_single(..., DMA_TO_DEVICE)
-> kfree()

读测试的关键流程:

1
2
3
4
5
6
7
8
9
kzalloc destination buffer
-> dma_map_single(..., DMA_FROM_DEVICE)
-> 写 LOWER_DST_ADDR / UPPER_DST_ADDR
-> 写 SIZE / FLAGS / IRQ_TYPE
-> 写 COMMAND_WRITE
-> wait_for_completion()
-> dma_unmap_single(..., DMA_FROM_DEVICE)
-> CPU 校验 CRC
-> kfree()

copy 测试:

1
2
3
4
5
6
7
source: DMA_TO_DEVICE
destination: DMA_FROM_DEVICE
写 src/dst DMA address 到 endpoint
COMMAND_COPY
等待中断
unmap dst 后 CPU 校验
unmap src

这里的关键细节:

  • 传给设备的是 dma_addr_t
  • DMA_FROM_DEVICE buffer 必须 unmap 后 CPU 再读;
  • 通过 completion 等待 IRQ;
  • 支持 alignment 修正;
  • 可通过 flag 告诉 endpoint 是否使用 DMA。

11. PCIe coherent DMA 示例:Rockchip pcie-rkep.c

drivers/misc/rockchip/pcie-rkep.c 中:

1
2
vir_addr = dma_alloc_coherent(&pcie_rkep->pdev->dev, param->size,
&dma_addr, GFP_KERNEL);

它把:

  • 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.cpcie_fpga_probe() 中,驱动完成 PCIe 设备使能、DMA mask 设置、BAR 映射和 bus master 使能:

1
2
3
4
5
6
7
pci_enable_device_mem()
-> dma_set_mask_and_coherent(&dev->dev, DMA_BIT_MASK(32))
-> pcie_hw_init() 映射 BAR0
-> pci_set_master()
-> pcie_hw_configure_global_hardware()
-> pcie_chnl_init()
-> pcie_chnl_start()

其中:

  • 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.cpcie_chnl_alloc_ring_resource() 中,驱动为每个通道分配 TX/RX 两组资源:

1
2
3
4
TX descriptor coherent buffer
TX data coherent buffer
RX descriptor coherent buffer
RX data coherent buffer

核心接口是:

1
2
3
4
5
6
7
ring_object->_desc_buf = dma_alloc_coherent(dev, desc_size,
&ring_object->_phy_desc_buf,
GFP_KERNEL);

ring_object->_dma_buf = dma_alloc_coherent(dev, data_size,
&ring_object->_phy_dma_buf,
GFP_KERNEL);

这里:

  • _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
2
3
4
每组 32 个元素
共 4 组
每个 packet buffer 为 12288 字节
每个方向共 128 个 packet buffer

所以单通道数据 DMA buffer 规模大致是:

1
2
3
TX: 12288 * 128 = 1.5 MiB
RX: 12288 * 128 = 1.5 MiB
合计约 3 MiB,不含 descriptor/token

如果启用 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
2
3
4
TX_BUF_ADDR_L  <- tx data dma_addr_t
TX_DESC_ADDR <- tx descriptor dma_addr_t
RX_BUF_ADDR_L <- rx data dma_addr_t
RX_DESC_ADDR <- rx descriptor dma_addr_t

这些寄存器来自 pcie_hw_define.h,例如:

1
2
3
4
5
6
7
FPGA_REG_TX0_BUF_ADDR_L
FPGA_REG_TX0_DESC_ADDR
FPGA_REG_RX0_BUF_ADDR_L
FPGA_REG_RX0_DESC_ADDR
FPGA_REG_TX0_SEND_POS
FPGA_REG_RX0_RECV_POS
FPGA_REG_RX0_CHNL_STATE

这一步的含义是:驱动把 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
2
pcie_fpga_open()
-> pcie_chnl_user_start()

在 PMD 模式下:

1
2
3
4
5
pcie_chnl_user_start()
-> chnl_state = PCS_Working
-> PMD thread 调用 _pcie_chnl_start_working_in_pmd_thread()
-> pcie_chnl_enable_rx()
-> 写 FPGA_REG_RXx_CHNL_STATE = FPGA_CHNL_RX_ENABLE

这一步会使能 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
2
3
4
pcie_fpga_write()
-> 把用户数据放入发送队列
-> pcie_chnl_user_start_send()
-> _pcie_chnl_do_check_tx()

_pcie_chnl_do_check_tx() 会:

  1. 从用户发送队列取数据;
  2. 拷贝到 TX coherent DMA buffer;
  3. 填写 TX descriptor;
  4. 更新 TX token 的 wpos;
  5. 执行 smp_mb()
  6. FPGA_REG_TXx_SEND_POS 通知 FPGA。

关键动作是:

1
2
3
4
pcie_tx_add_wpos_value()
-> smp_mb()
-> pcie_chnl_set_tx_pos_to_fpga()
-> 写 TX_SEND_POS doorbell

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_POS doorbell 通知 FPGA 读 host memory;
  • 驱动中没有 dma_request_chan(),说明它不是使用 RK3588 SoC 通用 DMA channel;
  • 真正搬运数据的是 PCIe FPGA endpoint 内部 DMA/逻辑。

这个驱动的数据流可以概括为:

1
2
RX: FPGA -> PCIe Memory Write -> host RX coherent buffer -> CPU/用户态读取
TX: 用户态/CPU -> host TX coherent buffer -> FPGA PCIe Memory Read -> FPGA 发送

因此它属于“驱动分配 host DMA 内存 + 配置 endpoint BAR 寄存器 + endpoint 自己发起 DMA”的 PCIe DMA 模型。


13. Scatter-Gather DMA

大块或非连续用户 buffer 通常用 SG。

常见流程:

1
2
3
4
5
6
pin user pages / 构造 sg_table
-> dma_map_sgtable()
-> 遍历 sg_dma_address(sg), sg_dma_len(sg)
-> 填写硬件 SG descriptor
-> 启动设备
-> 完成后 dma_unmap_sgtable()

NVMe PCI 驱动是典型例子:

  • dma_map_sgtable() 映射 request 的 sg_table;
  • 根据设备能力构造 PRP 或 SGL;
  • 提交 SQ doorbell;
  • 完成后 dma_unmap_sgtable()

drivers/nvme/host/pci.c 关键路径:

1
2
3
4
5
6
7
8
nvme_queue_rq()
-> nvme_map_data()
-> dma_map_sgtable()
-> nvme_pci_setup_sgls() / nvme_pci_setup_prps()
-> nvme_submit_cmd()
-> IRQ / poll completion
-> nvme_pci_unmap_rq()
-> dma_unmap_sgtable()

14. DMAengine 与 PCIe eDMA

有些控制器不是设备自己管理 DMA 描述符,而是通过 Linux DMAengine 框架抽象。

关键头文件:

  • include/linux/dmaengine.h

常见 client 流程:

1
2
3
4
5
6
7
dma_request_chan()
-> dmaengine_slave_config()
-> dmaengine_prep_slave_single() / dmaengine_prep_slave_sg()
-> dmaengine_submit()
-> dma_async_issue_pending()
-> callback / dmaengine_tx_status()
-> dmaengine_terminate_sync()

PCIe DesignWare eDMA 驱动:

  • drivers/dma/dw-edma/dw-edma-pcie.c
  • drivers/dma/dw-edma/dw-edma-core.c
  • drivers/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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
static int my_pci_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, "my_pcie_dma");
if (ret)
goto err_disable;

if (dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(64)) &&
dma_set_mask_and_coherent(&pdev->dev, DMA_BIT_MASK(32))) {
ret = -ENODEV;
goto err_regions;
}

pci_set_master(pdev);

regs = pci_iomap(pdev, 0, 0);
if (!regs) {
ret = -ENOMEM;
goto err_regions;
}

ret = pci_alloc_irq_vectors(pdev, 1, 1, PCI_IRQ_MSI | PCI_IRQ_LEGACY);
if (ret < 0)
goto err_iounmap;

ret = request_irq(pci_irq_vector(pdev, 0), my_irq, 0,
"my_pcie_dma", priv);
if (ret)
goto err_irq;

return 0;
}

15.2 Host-to-device DMA

设备从 host memory 读数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
buf = kmalloc(len, GFP_KERNEL);
memcpy(buf, src, len);

dma = dma_map_single(&pdev->dev, buf, len, DMA_TO_DEVICE);
if (dma_mapping_error(&pdev->dev, dma))
return -EIO;

writel(lower_32_bits(dma), regs + SRC_ADDR_LO);
writel(upper_32_bits(dma), regs + SRC_ADDR_HI);
writel(len, regs + DMA_LEN);
writel(DMA_READ_FROM_HOST, regs + DMA_CMD);

wait_for_completion(&done);

dma_unmap_single(&pdev->dev, dma, len, DMA_TO_DEVICE);
kfree(buf);

15.3 Device-to-host DMA

设备向 host memory 写数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
buf = kmalloc(len, GFP_KERNEL);

dma = dma_map_single(&pdev->dev, buf, len, DMA_FROM_DEVICE);
if (dma_mapping_error(&pdev->dev, dma))
return -EIO;

writel(lower_32_bits(dma), regs + DST_ADDR_LO);
writel(upper_32_bits(dma), regs + DST_ADDR_HI);
writel(len, regs + DMA_LEN);
writel(DMA_WRITE_TO_HOST, regs + DMA_CMD);

wait_for_completion(&done);

dma_unmap_single(&pdev->dev, dma, len, DMA_FROM_DEVICE);

/* unmap 后 CPU 再读取 buf */
process_rx_data(buf, len);
kfree(buf);

16. Cache 同步与内存屏障

Streaming DMA 的所有权规则:

  • dma_map_*() 后,buffer 属于设备;
  • CPU 不应再修改 DMA_TO_DEVICE buffer;
  • CPU 不应读取未完成的 DMA_FROM_DEVICE buffer;
  • 完成后通过 dma_unmap_*()dma_sync_*_for_cpu() 交还 CPU;
  • 如果要再次给设备用,调用 dma_sync_*_for_device()

对于 coherent DMA:

  • 不需要 streaming map/unmap;
  • 但写寄存器通知设备前,仍可能需要 wmb() 确保描述符内容先于 doorbell 可见;
  • 中断中读取设备写回状态时,按硬件协议考虑 readl()dma_rmb() 等同步。

典型顺序:

1
2
3
4
5
6
desc->addr = cpu_to_le64(dma);
desc->len = cpu_to_le32(len);

/* 确保描述符写入先于 doorbell */
wmb();
writel(START, regs + DOORBELL);

17. PCIe DMA 常见错误

  1. 没有调用 pci_set_master()

    • 设备不能作为 bus master 发起 DMA。
  2. 没有设置 DMA mask

    • 高地址内存可能映射出设备无法访问的 DMA 地址。
  3. 把 CPU 虚拟地址写给设备

    • 设备必须使用 dma_addr_t
  4. vmalloc/栈内存做 DMA

    • 可能地址不连续或 cacheline 不安全。
  5. 方向写错

    • DMA_TO_DEVICEDMA_FROM_DEVICE 方向错误会导致 cache 同步错误。
  6. 忘记 unmap

    • IOMMU 映射泄漏,debug DMA API 会报错。
  7. SG nents 用错

    • dma_map_sg() 返回 mapped nents;unmap 仍使用原始 nents。
  8. 提前读取 DMA_FROM_DEVICE buffer

    • 必须等完成并 unmap/sync 后 CPU 再读。
  9. 中断先于数据可见

    • 需要按硬件协议使用 barrier/readl 保证顺序。
  10. coherent buffer 被当作普通 cache buffer 优化

  • coherent 适合控制结构,不一定适合超大吞吐数据面。

18. RK3588 调试建议

常用内核配置:

  • CONFIG_DMA_API_DEBUG
  • CONFIG_DMA_CMA
  • CONFIG_CMA
  • CONFIG_IOMMU_SUPPORT
  • CONFIG_PCI_ENDPOINT
  • CONFIG_PCIE_DW
  • CONFIG_DW_EDMA

常用观测:

  • dmesg | grep -i dma
  • dmesg | grep -i iommu
  • dmesg | 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 传输排查顺序:

  1. PCIe 链路是否枚举成功;
  2. BAR 是否 request/iomap 成功;
  3. DMA mask 是否设置成功;
  4. pci_set_master() 是否调用;
  5. MSI/MSI-X 是否申请并触发;
  6. DMA 地址是否按低 32/高 32 正确写入;
  7. DMA 方向是否正确;
  8. 设备端 DMA engine 状态寄存器是否报错;
  9. 是否有 IOMMU fault;
  10. 是否有 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.cpcie-rkep.cdw-edma-pcie.c 以及自定义 PCIeDrv FPGA 驱动。

一句话概括:
PCIe DMA 传输的本质是驱动把 host memory 映射成设备可见的 DMA 地址,再通过 PCIe BAR/描述符启动设备 DMA engine,由设备发起 Memory Read/Write 完成数据搬运。

文章互动

阅读 --

留言

0 条留言

正在加载留言…