第一部分:DMA(Direct Memory Access) 基础概念 - 为什么需要DMA?

1. 传统数据传输的问题 (CPU搬运工模式)

  • 场景: 假设你的嵌入式设备有一个高速外设(如网卡、SSD控制器、高速ADC/DAC),需要频繁地在外设的寄存器/缓冲区主内存(RAM) 之间传输大量数据(比如网络数据包、磁盘块、采样波形)。
  • 传统方式: CPU负责搬运数据。
    1. CPU从外设读取一个字节/字的数据到其内部寄存器。
    2. CPU将该数据写入到内存的指定地址。
    3. 重复步骤1-2,直到所有数据传输完毕。
  • 痛点:
    • CPU占用率高: CPU全程参与数据搬运,无法执行其他计算任务,系统整体性能下降。
    • 速度瓶颈: CPU的读写速度(受限于总线带宽、指令执行周期)可能远低于外设或内存本身的速度,成为传输瓶颈。
    • 延迟增加: 其他任务需要等待CPU完成数据搬运才能执行,导致系统响应变慢。
    • 功耗增加: CPU持续工作导致功耗上升。

2. DMA 的解决方案 (外设直接搬运工)

  • 核心思想: 让一个专门的硬件模块——DMA控制器(DMAC)——来代替CPU完成外设和内存之间的数据搬运。CPU只需启动传输,然后就可以去做其他事情,传输完成后DMAC会通知CPU。
  • DMA控制器(DMAC)的角色:
    • 独立单元: DMAC是系统总线上的一个主设备(Master),可以独立发起对内存和外设的读写操作。
    • 配置者: CPU需要告诉DMAC:
      • 源地址(Source Address): 数据从哪里来(外设地址或内存地址)。
      • 目标地址(Destination Address): 数据到哪里去(内存地址或外设地址)。
      • 传输大小(Transfer Size): 要传输多少数据(字节、字、块)。
      • 传输方向(Direction): 内存到外设(DMA_TO_DEVICE)、外设到内存(DMA_FROM_DEVICE)、内存到内存(DMA_MEM_TO_MEM)。
      • 传输模式: 单次、循环、突发等。
      • 中断使能: 传输完成后是否产生中断通知CPU。
    • 执行者: DMAC根据配置,在总线上发起读写操作,直接在外设和内存之间搬运数据,无需CPU干预每个字节/字。
    • 通知者: 传输完成后(或出错时),DMAC通过中断信号通知CPU。
DMA 请求
总线控制
数据读写
传输完成中断
外设
DMA 控制器
内存
CPU

3. DMA 的优势

  • 解放CPU: CPU只需启动和结束传输,中间过程完全由DMAC处理,CPU可以执行其他任务,系统吞吐量和响应性大幅提升。
  • 高速传输: DMAC专门为数据传输优化,通常能达到接近总线理论带宽的传输速度,远快于CPU逐字节搬运。
  • 降低功耗: CPU在传输期间可以进入低功耗状态。
  • 实时性增强: 对于需要高速、连续数据流的场景(如音视频、高速采集),DMA是必不可少的。

4. DMA 的关键挑战 (在驱动中必须面对)

  • 缓存一致性(Cache Coherency): 这是DMA最核心、最复杂的问题!
    • 问题根源: 现代CPU都有缓存(Cache),用于加速对内存的访问。CPU操作的是缓存中的数据副本,最终会写回内存。而DMAC直接访问的是物理内存
    • 场景1 (DMA_FROM_DEVICE - 外设到内存):
      • DMAC将数据写入物理内存地址A。
      • CPU之前可能读取过地址A,其数据副本在CPU缓存中是旧的。
      • 当CPU再次读取地址A时,它可能直接从缓存中获取旧数据,而不是DMAC刚写入的新数据,导致数据不一致!
    • 场景2 (DMA_TO_DEVICE - 内存到外设):
      • CPU将要发送的数据写入内存地址B(数据先进入CPU缓存)。
      • CPU启动DMA传输,DMAC从物理内存地址B读取数据发送给外设。
      • 如果CPU缓存中的数据尚未写回物理内存,DMAC读到的就是旧数据或无效数据,导致外设收到错误数据!
  • 解决方案: DMA映射(DMA Mapping)同步(DMA Synchronization)。Linux内核提供了专门的API来处理这个问题,确保CPU缓存和DMA操作之间的一致性。这是驱动开发者必须掌握的核心技能。
    • 映射/解映射时自动缓存维护
    • 手动同步(dma_sync_single_for_*
    • 一致性内存(dma_alloc_coherent
  • 物理地址 vs 虚拟地址:
    • CPU使用虚拟地址访问内存。
    • DMAC使用物理地址访问内存。
    • 驱动程序运行在内核空间,使用的是虚拟地址。在配置DMAC时,必须将驱动中使用的虚拟地址转换为DMAC能理解的物理地址。Linux的DMA API也封装了这种转换。
  • 平台/架构差异: 不同的SoC(如ARM, x86, MIPS)其DMAC的寄存器布局、功能、能力(如支持的通道数、传输宽度、突发长度、是否支持Scatter-Gather)差异很大。驱动需要适配具体的硬件。
  • 资源管理: DMAC通道是有限的系统资源。驱动需要申请、配置、使用、释放DMA通道。

第二部分:Linux 内核 DMA 框架 - 抽象与API

  Linux内核提供了一个通用的DMA API层,旨在隐藏底层硬件DMAC的细节,让驱动开发者能用一套相对统一的接口来使用DMA功能,同时正确处理缓存一致性和地址转换问题。这套API主要围绕struct devicedma_addr_t展开。

1. 核心概念与数据类型

  • struct device *dev: 代表使用DMA的设备(如PCI设备、Platform设备)。所有DMA操作都必须关联到一个struct device。这非常重要,因为:

    • 它代表了设备的DMA能力(如32位/64位地址空间)。
    • 它关联了设备的IOMMU/IOVA(如果存在),用于地址转换和保护。
    • 它是缓存一致性操作的基础。
  • dma_addr_t: 用于表示DMA操作中使用的设备可见的物理地址(或IOVA)。它的大小和格式取决于平台和设备(可能是32位或64位)。驱动程序绝不能对dma_addr_t进行指针运算或直接解引用! 它只是传递给DMA引擎的地址令牌。

    • 当存在 IOMMU 时,dma_addr_t 是 IOVA(IO虚拟地址),而非物理地址。IOMMU 负责 IOVA 到物理地址的转换,提供内存保护和地址重映射。
    //kernel源码/include/linux/types.h
    /*
     * @brief DMA地址类型定义
     * 
     * 根据系统配置定义DMA地址的位宽类型,确保兼容不同硬件平台的DMA寻址能力。
     * 该类型用于存储DMA API返回的地址,实际宽度取决于底层硬件实现。
     * 
     * @note
     * 1. 当配置为64位DMA地址时,使用u64类型
     * 2. 默认情况下(未配置64位支持时),使用u32类型
     * 3. 驱动程序应通过DMA API获取该类型地址进行内存映射I/O操作
     * 
     * @see CONFIG_ARCH_DMA_ADDR_T_64BIT 配置宏定义
     */
    #ifdef CONFIG_ARCH_DMA_ADDR_T_64BIT
    typedef u64 dma_addr_t;
    #else
    typedef u32 dma_addr_t;
    #endif
    
  • enum dma_data_direction: 定义DMA传输方向:

    • DMA_TO_DEVICE: 内存 -> 外设 (数据发送)
    • DMA_FROM_DEVICE: 外设 -> 内存 (数据接收)
    • DMA_BIDIRECTIONAL: 双向,会强制 双向缓存同步(写回 + 无效化),可能引入性能损耗。建议仅在缓冲区确实被双向读写时使用,否则明确指定单向(TO_DEVICE/FROM_DEVICE)。
    • DMA_NONE: 未使用 (调试用)
//kernel源码/include/linux/dma-direction.h
enum dma_data_direction {
	DMA_BIDIRECTIONAL = 0,
	DMA_TO_DEVICE = 1,
	DMA_FROM_DEVICE = 2,
	DMA_NONE = 3,
};

2. DMA 缓冲区管理 (核心:映射与同步)

  Linux DMA API的核心思想是:驱动程序不能直接使用普通的内核内存(如kmalloc分配的)进行DMA传输。必须通过DMA API来分配和映射缓冲区,以确保缓存一致性。

  kmalloc 分配的内存物理地址可能不连续(尤其在分配大内存时)。若硬件要求物理连续,应改用 dma_alloc_coherentkmalloc(GFP_DMA)(小内存)确保连续性。

a. DMA 映射 (Mapping)

映射操作告诉内核:“这块内存区域即将被用于DMA传输,方向是XXX,请做好必要的准备(如刷新缓存、建立IOMMU映射)”。映射会返回一个dma_addr_t供DMAC使用。

  • 单次映射 (Single Mapping): 适用于一次性的、连续的DMA传输。

    • dma_addr_t dma_map_single(struct device *dev, void *cpu_addr, size_t size, enum dma_data_direction dir)
      • dev: 关联的设备。
      • cpu_addr: 驱动中使用的内核虚拟地址(通常由kmalloc分配)。
      • size: 要映射的缓冲区大小。
      • dir: DMA方向。
      • 返回值: 成功返回dma_addr_t(设备地址),失败返回DMA_MAPPING_ERROR
      • 作用:cpu_addr对应的物理地址转换为设备可访问的dma_addr_t,并根据dir执行必要的缓存操作(DMA_TO_DEVICE时确保缓存写回内存,DMA_FROM_DEVICE时使对应缓存行无效)。
    • 解映射 (Unmapping): 当DMA传输完成(或被取消)后,必须解映射。
      • void dma_unmap_single(struct device *dev, dma_addr_t dma_addr, size_t size, enum dma_data_direction dir)
      • 作用:释放映射资源,并根据dir执行缓存操作(DMA_FROM_DEVICE时使缓存行无效,确保CPU下次读到新数据)。
    • 示例流程 (发送数据):
      // 1. 分配内核缓冲区
      void *cpu_buf = kmalloc(BUF_SIZE, GFP_KERNEL);
      if (!cpu_buf) // 处理错误
      
      // 2. 准备数据到cpu_buf (例如: memcpy(cpu_buf, data, BUF_SIZE))
      
      // 3. 映射缓冲区用于DMA_TO_DEVICE
      dma_addr_t dma_handle = dma_map_single(my_device, cpu_buf, BUF_SIZE, DMA_TO_DEVICE);
      if (dma_mapping_error(my_device, dma_handle)) {
          kfree(cpu_buf);
          // 处理错误
      }
      
      // 4. 配置并启动DMA传输 (使用dma_handle作为源/目标地址)
      // ... (硬件相关操作,写入DMA寄存器)
      
      // 5. 等待DMA传输完成 (通常通过中断)
      // ... (在中断处理函数或等待队列中)
      
      // 6. 解映射缓冲区
      dma_unmap_single(my_device, dma_handle, BUF_SIZE, DMA_TO_DEVICE);
      
      // 7. 释放内核缓冲区
      kfree(cpu_buf);
      
  • 分散/聚集映射 (Scatter-Gather Mapping): 这是现代驱动中最常用、最高效的方式! 适用于需要传输多个不连续的物理内存块(如sk_buff在网络驱动中,struct bio在块设备驱动中)的情况。DMAC支持SG模式时,可以一次性将这些分散的物理块传输完成,无需CPU多次介入。

    • struct scatterlist: 用于描述一个物理内存段。
      • page_address: 页的虚拟地址。
      • offset: 页内偏移。
      • length: 本段长度。
    /*
     * scatterlist结构体用于描述分散/聚集DMA操作的内存块
     * 包含页面链接、偏移量、长度以及DMA地址信息
     */
    struct scatterlist {
    	unsigned long	page_link;	/* 页面链接指针(低3位用于标记) */
    	unsigned int	offset;		/* 缓冲区内的字节偏移量 */
    	unsigned int	length;		/* 缓冲区长度(字节) */
    	dma_addr_t	dma_address;	/* DMA物理地址 */
    #ifdef CONFIG_NEED_SG_DMA_LENGTH
    	unsigned int	dma_length;	/* DMA映射长度(字节),仅当配置启用时有效 */
    #endif
    };
    
    • int dma_map_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction dir)
      • sg: 指向scatterlist数组的指针。
      • nents: 数组中scatterlist条目的数量。
      • 返回值: 成功映射的DMA缓冲区段数(可能小于nents,因为硬件可能合并相邻段)。应通过sg_dma_address(sg)sg_dma_len(sg) 获取映射后的地址和长度,而非原始scatterlist
      • 作用: 遍历scatterlist数组,将每个条目映射为设备地址(sg_dma_address(sg)),并调整长度(sg_dma_len(sg)),同时执行必要的全局缓存操作。
    • void dma_unmap_sg(struct device *dev, struct scatterlist *sg, int nents, enum dma_data_direction dir)
      • 解映射SG列表。
    • 零拷贝技术集成
      // 用户空间内存直接DMA
      struct page **pages;
      int ret = get_user_pages(user_buf, num_pages, &pages);
      
      // 映射用户页面
      sg_set_page(&sg, pages[0], PAGE_SIZE, 0);
      dma_map_sg(dev, &sg, 1, DMA_FROM_DEVICE);
      
      // 传输完成后
      dma_unmap_sg(dev, &sg, 1, DMA_FROM_DEVICE);
      put_page(pages[0]);
      
    • 示例流程 (接收数据 - 网络驱动简化):
      // 1. 分配并初始化一个接收缓冲区 (通常是环形缓冲区,每个元素是一个page)
      //    假设rx_ring[i].page指向一个分配好的页,rx_ring[i].offset=0, len=PAGE_SIZE
      
      // 2. 构建scatterlist数组 (假设一次接收一个page)
      struct scatterlist sg;
      sg_init_table(&sg, 1); // 初始化一个scatterlist
      sg_set_page(&sg, rx_ring[i].page, PAGE_SIZE, 0);
      
      // 3. 映射SG用于DMA_FROM_DEVICE
      int mapped = dma_map_sg(my_device, &sg, 1, DMA_FROM_DEVICE);
      if (!mapped) {
          // 处理错误
      }
      dma_addr_t dma_handle = sg_dma_address(&sg); // 获取映射后的设备地址
      
      // 4. 配置DMA控制器,将接收到的数据写入dma_handle,长度为sg_dma_len(&sg)
      // ... (硬件相关操作)
      
      // 5. 等待DMA完成 (接收中断)
      
      // 6. 在中断处理程序中:
      //    a. 处理接收到的数据 (在rx_ring[i].page + rx_ring[i].offset处,长度为实际接收长度)
      //    b. 解映射SG, dma_unmap_sg(my_device, &sg, 1, DMA_FROM_DEVICE);
      //    c. 更新接收环形缓冲区指针
      //    d. 重新映射该缓冲区用于下一次接收 (回到步骤3)
      
b. DMA 同步 (Synchronization)

  在某些复杂场景下,CPU和DMA可能需要交替访问同一个缓冲区。在CPU访问之前或之后,需要显式地同步缓存状态,确保看到的是最新数据。

  • void dma_sync_single_for_cpu(struct device *dev, dma_addr_t dma_handle, size_t size, enum dma_data_direction dir)
    • 作用: 在CPU即将访问一个已映射的DMA缓冲区之前调用。确保CPU看到的是内存中最新的数据(对于DMA_FROM_DEVICE,使缓存无效;对于DMA_TO_DEVICE,通常不需要特殊操作,但调用也无害)。
  • void dma_sync_single_for_device(struct device *dev, dma_addr_t dma_handle, size_t size, enum dma_data_direction dir)
    • 作用: 在CPU访问完一个已映射的DMA缓冲区,并且DMA设备即将再次访问它之前调用。确保DMA设备看到的是内存中最新的数据(对于DMA_TO_DEVICE,将缓存写回内存;对于DMA_FROM_DEVICE,通常不需要特殊操作)。
  • SG版本: dma_sync_sg_for_cpu(), dma_sync_sg_for_device()
  • 使用场景:
    • 环形缓冲区: 驱动程序和DMA控制器共享一个环形缓冲区。驱动写入一部分数据后,调用_for_device启动DMA传输。DMA传输完成后,驱动调用_for_cpu读取DMA写入的数据。
    • 部分缓冲区更新: 只更新缓冲区的一部分,然后让DMA传输整个缓冲区(或另一部分)。
    • 重要: 同步操作不能替代映射/解映射!它只适用于已经处于映射状态的缓冲区。对于一次性的完整传输,映射->传输->解映射是更清晰、更推荐的方式。
c. DMA 内存池 (DMA Pool)

  当需要频繁分配/释放大小相同的小DMA缓冲区时(如网络驱动的描述符环),使用dma_alloc_coherent/dma_free_coherent效率较低(每次都要处理映射/解映射和缓存一致性)。DMA Pool提供了一种更高效的机制。

  • struct dma_pool *dma_pool_create(const char *name, struct device *dev, size_t size, size_t align, size_t alloc)
    • 创建一个DMA内存池。
    • name: 池的名字(调试用)。
    • dev: 关联设备。
    • size: 池中每个缓冲区的大小。
    • align: 对齐要求。
    • alloc: 边界分配要求(0表示不关心)。
  • void *dma_pool_alloc(struct dma_pool *pool, gfp_t mem_flags, dma_addr_t *handle)
    • 从池中分配一个缓冲区。
    • 返回内核虚拟地址,并通过handle返回设备地址。
    • 分配的内存已经是DMA一致的(相当于dma_alloc_coherent的效果),无需额外映射/同步!
  • void dma_pool_free(struct dma_pool *pool, void *vaddr, dma_addr_t addr)
    • 释放池中缓冲区。
  • void dma_pool_destroy(struct dma_pool *pool)
    • 销毁整个池(需确保所有分配的缓冲区都已释放)。

3. 一致性DMA映射 (Coherent DMA Mapping)

  对于某些设备(如网络控制器的描述符环、USB控制器的数据结构),它们需要CPU和DMA控制器同时、持续地访问同一块内存区域。映射/解映射或同步的开销和复杂性太大。Linux提供了“一致性DMA映射”来解决。

  • void *dma_alloc_coherent(struct device *dev, size_t size, dma_addr_t *dma_handle, gfp_t gfp)
    • 分配一块同时适用于CPU和DMA访问的内存区域。
    • dev: 设备。
    • size: 分配大小。
    • dma_handle: 输出参数,返回设备地址(dma_addr_t)。
    • gfp: 分配标志(如GFP_KERNEL)。
    • 返回值: 成功返回内核虚拟地址,失败返回NULL
    • 特性:
      • 缓存一致性保证: 内核确保这块内存区域在CPU缓存和主存之间是一致的。CPU的任何写操作会立即(或很快)反映到主存,DMA的任何写操作也会立即(或很快)对CPU可见。无需调用任何映射/解映射或同步API!
      • 性能代价: 这种一致性通常是通过禁止该内存区域的缓存或使用特殊的写回(Write-Through)缓存策略实现的,这会降低CPU访问该内存的速度。因此,只用于必须同时访问的、对性能不极端敏感的小块控制结构(如描述符)。现代架构(如ARMv8)通过 标记内存为DeviceNormal Non-Cacheable 实现一致性,而非简单禁用缓存。
      • 物理地址连续: 返回的内存区域在物理地址上是连续的(除非平台支持IOMMU且配置为允许非连续)。
  • void dma_free_coherent(struct device *dev, size_t size, void *cpu_addr, dma_addr_t dma_handle)
    • 释放由dma_alloc_coherent分配的内存。

4. DMA 引擎API (DMA Engine API)

  现代SoC通常包含功能强大的DMA控制器(如ARM的PL330/DMAC-330, TI的EDMA, Intel的IOAT)。Linux内核提供了一个统一的DMA Engine子系统来管理这些复杂的DMAC,驱动开发者可以通过它来请求DMA通道、配置传输、提交请求。

  • 核心对象:
    • struct dma_chan: 代表一个可用的DMA通道。
    • struct dma_client: 代表一个DMA客户端(即使用DMA的设备驱动)。
  • 主要流程:
    1. 请求通道:
      // 定义匹配过滤器 (根据需求,如传输方向、能力)
      struct dma_slave_config config = { ... };
      dma_cap_mask_t mask;
      dma_cap_zero(mask);
      dma_cap_set(DMA_SLAVE, mask); // 设置需要的能力
      
      // 请求通道
      struct dma_chan *chan = dma_request_channel(mask, my_filter_fn, &config);
      if (!chan) { /* 处理错误 */ }
      
      my_filter_fn是一个可选的回调函数,用于进一步筛选通道(比如根据物理通道号)。
    2. 配置通道 (Slave模式):
      struct dma_slave_config slave_config = {
          .direction = DMA_MEM_TO_DEV, // 或 DMA_DEV_TO_MEM
          .src_addr = ... , // 源地址 (物理地址或寄存器地址)
          .dst_addr = ... , // 目标地址
          .src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES, // 地址宽度
          .dst_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES,
          .src_maxburst = 16, // 突发长度
          .dst_maxburst = 16,
          // ... 其他平台相关配置
      };
      int ret = dmaengine_slave_config(chan, &slave_config);
      if (ret) { /* 处理错误 */ }
      
    3. 准备传输描述符:
      • 单次传输:
        struct dma_async_tx_descriptor *tx;
        dma_addr_t dma_buf = dma_map_single(dev, cpu_buf, size, direction);
        // ... 检查错误
        
        tx = dmaengine_prep_slave_single(chan, dma_buf, size, direction, DMA_PREP_INTERRUPT);
        if (!tx) { /* 处理错误 */ }
        
      • SG传输:
        struct scatterlist sg[2];
        sg_init_table(sg, 2);
        sg_set_buf(&sg[0], buf1, len1);
        sg_set_buf(&sg[1], buf2, len2);
        
        int nents = dma_map_sg(dev, sg, 2, direction);
        // ... 检查错误
        
        tx = dmaengine_prep_slave_sg(chan, sg, nents, direction, DMA_PREP_INTERRUPT);
        if (!tx) { /* 处理错误 */ }
        
      • 循环传输: dmaengine_prep_dma_cyclic(),需使用双缓冲区(Ping-Pong Buffer)避免处理数据时DMA覆盖
    4. 设置回调:
      tx->callback = my_dma_callback; // 传输完成回调函数
      tx->callback_param = my_private_data; // 传递给回调的参数
      
    5. 提交传输:
      dma_cookie_t cookie = dmaengine_submit(tx);
      if (dma_submit_error(cookie)) { /* 处理错误 */ }
      dma_async_issue_pending(chan); // 启动传输
      
    6. 回调处理:
      void my_dma_callback(void *param)
      {
          struct my_private_data *data = (struct my_private_data *)param;
          // 传输完成!
          // 1. 解映射缓冲区 (如果是单次/SG)
          // 2. 处理数据
          // 3. 如果需要,准备下一次传输
      }
      
    7. 释放通道: dma_release_channel(chan);
驱动 DMA引擎 硬件 dma_request_channel() dmaengine_slave_config() dmaengine_prep_slave_sg() dmaengine_submit() dma_async_issue_pending() 传输完成中断 回调函数执行 dma_unmap_sg() dma_release_channel() 驱动 DMA引擎 硬件

DMA Engine API的优势:

  • 抽象硬件差异: 屏蔽了不同DMAC的寄存器操作细节。
  • 通道管理: 自动处理DMA通道的申请、释放、复用。
  • 高级功能: 支持复杂的传输模式(循环、SG)、中断处理、链式描述符。
  • 性能优化: 内核可以对DMA传输进行调度和优化。

何时使用DMA Engine API?

  • 当你的外设连接到一个通用DMA控制器(而非外设内置的简单FIFO DMA)时。
  • 当你需要利用DMA控制器的高级特性(如SG、循环、链式传输)时。
  • 现代嵌入式Linux驱动开发的首选方式! 除非硬件极其简单。

第三部分:嵌入式Linux驱动中使用DMA的实践指南

1. 明确需求与硬件

  • 外设类型: 是高速数据流设备(网卡、SD卡、摄像头、高速ADC)?还是只需要简单DMA搬运的设备(UART, SPI)?
  • DMA连接方式:
    • 外设内置FIFO DMA: 外设自身有简单的DMA逻辑,只需CPU配置几个寄存器(源/目标地址、长度)。通常使用dma_map_single/dma_unmap_single
    • 连接到通用DMAC: 外设作为DMAC的一个“从设备”(Slave)。需要使用DMA Engine API (dma_request_channel, dmaengine_prep_slave_*)。
  • DMAC能力: 支持Scatter-Gather?支持循环传输?最大传输大小?地址宽度?通道数?查阅SoC手册!

2. 选择合适的DMA API

  • 简单、单次、连续传输: dma_map_single/dma_unmap_single + 直接配置硬件寄存器(如果外设内置DMA)。
  • 复杂、多块、高性能传输: 强烈推荐使用DMA Engine API (dmaengine_prep_slave_sg)。这是现代、高效、可维护的方式。
  • CPU/DMA持续共享的小控制结构: dma_alloc_coherent/dma_free_coherent
  • 频繁分配/释放大小相同的小缓冲区: dma_pool_create/dma_pool_alloc/dma_pool_free

3. 编码步骤 (以DMA Engine API + SG为例 - 接收数据)

#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>

// 设备结构体 (假设)
struct my_device {
    struct device *dev;
    struct dma_chan *rx_chan; // 接收DMA通道
    // ... 其他成员 (如寄存器基地址、锁、缓冲区等)
};

// 1. 初始化时请求DMA通道
static int my_device_probe(struct platform_device *pdev)
{
    struct my_device *mydev;
    dma_cap_mask_t mask;

    // ... 分配mydev, 初始化其他资源 ...

    // 设置DMA能力需求
    dma_cap_zero(mask);
    dma_cap_set(DMA_SLAVE, mask); // 我们需要从设备(slave)到内存的传输

    // 请求DMA通道
    mydev->rx_chan = dma_request_channel(mask, NULL, NULL); // 使用默认过滤器
    if (!mydev->rx_chan) {
        dev_err(&pdev->dev, "Failed to request RX DMA channel\n");
        return -ENODEV;
    }

    // 配置DMA通道 (Slave模式)
    struct dma_slave_config rx_config = {
        .direction = DMA_DEV_TO_MEM, // 外设到内存
        .src_addr = mydev->phys_base + MYDEV_DATA_REG, // 外设数据寄存器物理地址
        .src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES, // 假设32位数据宽度
        .src_maxburst = 16, // 突发长度 (根据硬件能力)
        // ... 其他平台相关配置 (如握手信号等)
    };
    if (dmaengine_slave_config(mydev->rx_chan, &rx_config)) {
        dev_err(&pdev->dev, "Failed to configure RX DMA slave\n");
        dma_release_channel(mydev->rx_chan);
        return -EINVAL;
    }

    // ... 初始化接收缓冲区 (环形缓冲区) ...
    // 例如:分配多个page,构建struct scatterlist数组或使用dma_pool

    return 0;
}

// 2. 启动一次DMA接收 (通常在开启接收或接收到数据后)
void start_rx_dma_transfer(struct my_device *mydev)
{
    struct dma_async_tx_descriptor *tx;
    dma_addr_t dma_handle;
    struct scatterlist sg; // 假设一次接收一个page

    // 获取当前接收缓冲区 (从环形缓冲区)
    struct page *page = get_next_rx_buffer_page(mydev);
    if (!page) return; // 无可用缓冲区

    // 构建scatterlist
    sg_init_table(&sg, 1);
    sg_set_page(&sg, page, PAGE_SIZE, 0);

    // 映射SG用于DMA_FROM_DEVICE
    int mapped = dma_map_sg(mydev->dev, &sg, 1, DMA_FROM_DEVICE);
    if (!mapped) {
        dev_err(mydev->dev, "Failed to map RX buffer\n");
        put_rx_buffer_page(mydev, page); // 放回缓冲区
        return;
    }
    dma_handle = sg_dma_address(&sg); // 获取设备地址

    // 准备DMA传输描述符
    tx = dmaengine_prep_slave_sg(mydev->rx_chan, &sg, mapped, DMA_DEV_TO_MEM,
                                 DMA_PREP_INTERRUPT | DMA_CTRL_ACK);
    if (!tx) {
        dev_err(mydev->dev, "Failed to prepare RX DMA descriptor\n");
        dma_unmap_sg(mydev->dev, &sg, 1, DMA_FROM_DEVICE);
        put_rx_buffer_page(mydev, page);
        return;
    }

    // 设置回调
    tx->callback = my_rx_dma_callback;
    tx->callback_param = mydev; // 传递设备结构体

    // 提交传输
    dma_cookie_t cookie = dmaengine_submit(tx);
    if (dma_submit_error(cookie)) {
        dev_err(mydev->dev, "Failed to submit RX DMA\n");
        dma_unmap_sg(mydev->dev, &sg, 1, DMA_FROM_DEVICE);
        put_rx_buffer_page(mydev, page);
        return;
    }

    // 启动传输
    dma_async_issue_pending(mydev->rx_chan);
}

// 3. DMA接收完成回调
void my_rx_dma_callback(void *param)
{
    struct my_device *mydev = (struct my_device *)param;
    struct dma_tx_state state;
    enum dma_status status;
    dma_cookie_t cookie;
    struct page *page;
    size_t len;

    // 获取传输状态和实际传输长度
    cookie = mydev->rx_chan->cookie;
    status = dma_async_is_tx_complete(mydev->rx_chan, cookie, NULL, &state);
    if (status != DMA_COMPLETE) {
        dev_err(mydev->dev, "RX DMA transaction failed!\n");
        // 错误处理:重新启动传输或复位设备
        return;
    }
    len = state.residue; // 剩余字节数 (0表示全部传输完成)
    // 或者: len = PAGE_SIZE - state.residue; // 实际接收长度

    // 获取接收缓冲区 (在start_rx_dma_transfer中映射的那个)
    page = get_current_rx_buffer_page(mydev); // 需要设计环形缓冲区管理逻辑

    // 解映射SG
    struct scatterlist sg;
    sg_init_table(&sg, 1);
    sg_set_page(&sg, page, PAGE_SIZE, 0);
    dma_unmap_sg(mydev->dev, &sg, 1, DMA_FROM_DEVICE);

    // 处理接收到的数据 (在page_address(page)处,长度为实际接收长度)
    process_received_data(mydev, page_address(page), len);

    // 将缓冲区放回环形缓冲区或重新用于下一次接收
    recycle_rx_buffer(mydev, page);

    // 如果需要持续接收,启动下一次DMA传输
    start_rx_dma_transfer(mydev);
}

// 4. 移除设备时释放资源
static int my_device_remove(struct platform_device *pdev)
{
    struct my_device *mydev = platform_get_drvdata(pdev);

    // 停止DMA传输 (可能需要先禁用中断)
    // ... (硬件相关操作,停止外设和DMAC)

    // 释放DMA通道
    if (mydev->rx_chan) {
        dma_release_channel(mydev->rx_chan);
        mydev->rx_chan = NULL;
    }

    // 释放接收缓冲区 (环形缓冲区)
    // ... 释放所有page ...

    // ... 释放其他资源 ...

    return 0;
}

4. 关键注意事项与最佳实践

  • 错误处理: DMA操作(映射、通道请求、描述符准备、提交)都可能失败!必须检查所有返回值并进行适当的错误处理(释放资源、打印错误、尝试恢复)。
  • 资源释放: 确保在驱动卸载(remove)或错误路径中释放所有申请的资源:DMA通道、映射的缓冲区、分配的内存。内存泄漏和DMA通道泄漏是严重问题。
  • 并发与同步:
    • DMA传输是异步的。使用 (spinlock_t, mutex) 保护共享数据结构(如环形缓冲区指针、DMA通道状态)。
    • 在中断上下文(回调函数)中,只能使用spinlockGFP_ATOMIC分配。
    • 确保在DMA传输期间,相关的内存缓冲区不会被意外释放或修改
  • 调试:
    • CONFIG_DMA_API_DEBUG: 在内核配置中启用此选项,内核会检查DMA API的使用是否正确(如未解映射、双重映射)。
    • 使用dev_dbg(), printk()打印关键步骤和地址(虚拟地址、物理地址、dma_addr_t)。
    • 检查DMAC寄存器状态(如果可访问)。
    • 使用逻辑分析仪或总线分析仪监控DMA传输活动。
  • 平台特定代码: 虽然DMA API是通用的,但配置DMA通道(dma_slave_config)和获取外设寄存器物理地址是平台相关的。需要仔细阅读SoC手册和参考板级支持包(BSP)中的示例代码。
  • 性能考虑:
    • 优先使用Scatter-Gather减少CPU介入次数。
    • 合理设置突发长度(burst size),匹配外设、内存和总线的最佳性能点。
    • 对于持续数据流,使用循环DMA(dmaengine_prep_dma_cyclic),避免频繁启动/停止传输的开销。
    • 避免在热路径(如网络NAPI轮询)中使用dma_alloc_coherent,其CPU访问性能较差。
  • 缓存一致性是生命线: 永远不要假设缓存是一致的!严格遵循映射/解映射或同步API的规则。错误的一致性操作会导致极其难以调试的数据损坏问题。

5. 学习资源

  • 内核文档:
    • Documentation/driver-api/dmaengine/: DMA Engine子系统文档。
    • Documentation/core-api/dma-api.rst: 较新的RST格式文档。
  • 书籍:
    • Linux Device Drivers, 3rd Edition (O’Reilly) - 虽然有些旧,但DMA基础概念部分仍然适用。
    • Embedded Linux Primer (Christopher Hallinan) - 包含嵌入式系统DMA的讨论。
    • Linux Kernel Development (Robert Love) - 了解内核机制。
  • 示例代码:
    • 研究内核中主流驱动的实现:
      • 网络驱动:drivers/net/ethernet/
      • 存储驱动:drivers/mmc/host/ (SD/MMC), drivers/ata/ (SATA)
      • 串行驱动:drivers/tty/serial/ (如8250_dma.c)
      • 多媒体驱动:drivers/media/ (如V4L2驱动)
    • 关注它们如何申请通道、配置、准备描述符、处理回调、管理缓冲区。
  • 社区:
    • Linux Kernel Mailing List (LKML)
    • Stack Overflow

总结

掌握Linux驱动中的DMA编程是嵌入式Linux开发者的核心技能之一。关键在于:

  1. 理解原理: 深刻理解DMA为什么需要、缓存一致性的挑战以及物理/虚拟地址的区别。
  2. 掌握API: 熟练运用Linux DMA API(映射/解映射、同步、一致性分配、DMA Engine)是基础。DMA Engine API是现代首选。
  3. 实践为王: 从简单例子开始(如单次映射),逐步过渡到复杂的SG和DMA Engine。务必仔细阅读硬件手册!
  4. 严谨细致: DMA编程容错性低,错误处理、资源管理、并发控制必须做到位。缓存一致性是重中之重!
  5. 持续学习: 内核DMA机制也在不断发展(如IOMMU支持),关注社区和文档更新。

DMA编程虽然复杂,但一旦掌握,你就能编写出高性能、高效率的嵌入式Linux驱动,充分发挥硬件潜力。祝你学习顺利!


  欢迎阅读下一篇:Linux驱动之DMA(二)

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐