Linux驱动之DMA(一)
掌握Linux驱动中的DMA编程是嵌入式Linux开发者的核心技能之一。深刻理解DMA为什么需要、缓存一致性的挑战以及物理/虚拟地址的区别。熟练运用Linux DMA API(映射/解映射、同步、一致性分配、DMA Engine)是基础。DMA Engine API是现代首选。从简单例子开始(如单次映射),逐步过渡到复杂的SG和DMA Engine。务必仔细阅读硬件手册!DMA编程容错性低,错误处
目录
第一部分:DMA(Direct Memory Access) 基础概念 - 为什么需要DMA?
1. 传统数据传输的问题 (CPU搬运工模式)
- 场景: 假设你的嵌入式设备有一个高速外设(如网卡、SSD控制器、高速ADC/DAC),需要频繁地在外设的寄存器/缓冲区 和 主内存(RAM) 之间传输大量数据(比如网络数据包、磁盘块、采样波形)。
- 传统方式: CPU负责搬运数据。
- CPU从外设读取一个字节/字的数据到其内部寄存器。
- CPU将该数据写入到内存的指定地址。
- 重复步骤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。
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 device和dma_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_coherent 或 kmalloc(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,通常不需要特殊操作,但调用也无害)。
- 作用: 在CPU即将访问一个已映射的DMA缓冲区之前调用。确保CPU看到的是内存中最新的数据(对于
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,通常不需要特殊操作)。
- 作用: 在CPU访问完一个已映射的DMA缓冲区,并且DMA设备即将再次访问它之前调用。确保DMA设备看到的是内存中最新的数据(对于
- SG版本:
dma_sync_sg_for_cpu(),dma_sync_sg_for_device() - 使用场景:
- 环形缓冲区: 驱动程序和DMA控制器共享一个环形缓冲区。驱动写入一部分数据后,调用
_for_device启动DMA传输。DMA传输完成后,驱动调用_for_cpu读取DMA写入的数据。 - 部分缓冲区更新: 只更新缓冲区的一部分,然后让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)通过 标记内存为Device或Normal 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的设备驱动)。
- 主要流程:
- 请求通道:
// 定义匹配过滤器 (根据需求,如传输方向、能力) 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是一个可选的回调函数,用于进一步筛选通道(比如根据物理通道号)。 - 配置通道 (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) { /* 处理错误 */ } - 准备传输描述符:
- 单次传输:
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覆盖
- 单次传输:
- 设置回调:
tx->callback = my_dma_callback; // 传输完成回调函数 tx->callback_param = my_private_data; // 传递给回调的参数 - 提交传输:
dma_cookie_t cookie = dmaengine_submit(tx); if (dma_submit_error(cookie)) { /* 处理错误 */ } dma_async_issue_pending(chan); // 启动传输 - 回调处理:
void my_dma_callback(void *param) { struct my_private_data *data = (struct my_private_data *)param; // 传输完成! // 1. 解映射缓冲区 (如果是单次/SG) // 2. 处理数据 // 3. 如果需要,准备下一次传输 } - 释放通道:
dma_release_channel(chan);
- 请求通道:
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_*)。
- 外设内置FIFO DMA: 外设自身有简单的DMA逻辑,只需CPU配置几个寄存器(源/目标地址、长度)。通常使用
- 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通道状态)。 - 在中断上下文(回调函数)中,只能使用
spinlock和GFP_ATOMIC分配。 - 确保在DMA传输期间,相关的内存缓冲区不会被意外释放或修改。
- 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开发者的核心技能之一。关键在于:
- 理解原理: 深刻理解DMA为什么需要、缓存一致性的挑战以及物理/虚拟地址的区别。
- 掌握API: 熟练运用Linux DMA API(映射/解映射、同步、一致性分配、DMA Engine)是基础。DMA Engine API是现代首选。
- 实践为王: 从简单例子开始(如单次映射),逐步过渡到复杂的SG和DMA Engine。务必仔细阅读硬件手册!
- 严谨细致: DMA编程容错性低,错误处理、资源管理、并发控制必须做到位。缓存一致性是重中之重!
- 持续学习: 内核DMA机制也在不断发展(如IOMMU支持),关注社区和文档更新。
DMA编程虽然复杂,但一旦掌握,你就能编写出高性能、高效率的嵌入式Linux驱动,充分发挥硬件潜力。祝你学习顺利!
欢迎阅读下一篇:Linux驱动之DMA(二)
更多推荐



所有评论(0)