1. Linux DMA开发概述
DMA(Direct Memory Access)作为现代计算机系统中的关键组件,已经存在了半个多世纪。我第一次接触DMA是在2005年调试一块音频采集卡时,当时为了优化数据传输性能,不得不深入研究这块"神秘"的技术。如今,DMA已经成为嵌入式Linux开发中不可或缺的一部分,特别是在需要高速数据传输的场景下。
DMA的本质是允许外设直接与内存交换数据,而无需CPU的全程参与。想象一下,如果没有DMA,CPU就像是一个忙碌的快递员,需要亲自处理每一个数据包的搬运工作;而有了DMA,CPU只需要告诉DMA控制器从哪里取数据、放到哪里,然后就可以去做其他更有价值的工作了。
在Linux内核中,DMA子系统已经发展得相当成熟。从早期的简单DMA API到现在的dmaengine框架,Linux为开发者提供了丰富的接口和抽象层。不过,这也意味着学习曲线相对陡峭,特别是对于刚接触Linux驱动开发的工程师来说。
注意:DMA虽然能显著提升系统性能,但如果使用不当,也可能导致系统崩溃或数据损坏。我在早期项目中就曾因为DMA缓冲区配置不当,导致整个系统死锁。
2. DMA核心概念解析
2.1 DMA工作原理
DMA工作的基本原理可以用三个关键要素来描述:
- 源地址:数据从哪里来
- 目的地址:数据到哪里去
- 传输长度:要传输多少数据
在内核中,一次典型的DMA传输流程如下:
- CPU配置DMA控制器,设置源地址、目的地址和传输长度
- CPU启动DMA传输
- DMA控制器接管总线,开始数据传输
- 传输完成后,DMA控制器产生中断通知CPU
- CPU处理中断,确认传输完成
这个过程中,最关键的优化点在于第3步——DMA控制器独立工作期间,CPU可以并行处理其他任务。
2.2 Linux DMA架构
Linux内核中的DMA子系统主要包含以下几个层次:
- 硬件抽象层:与具体DMA控制器硬件交互
- dmaengine框架:提供统一的DMA操作接口
- 客户端驱动:使用DMA服务的设备驱动
这种分层设计使得驱动开发者可以专注于业务逻辑,而不必关心底层DMA控制器的具体实现。
3. Linux DMA API详解
3.1 基本API函数
Linux内核提供了丰富的DMA API,以下是最常用的几个:
// 分配DMA缓冲区 dma_alloc_coherent(dev, size, dma_handle, gfp); // 释放DMA缓冲区 dma_free_coherent(dev, size, cpu_addr, dma_handle); // 创建DMA映射 dma_map_single(dev, ptr, size, direction); // 取消DMA映射 dma_unmap_single(dev, dma_addr, size, direction);我在实际项目中发现,很多开发者容易混淆dma_alloc_coherent和dma_map_single的使用场景。简单来说:
- 当需要长期存在的DMA缓冲区时,使用
dma_alloc_coherent - 当只需要临时DMA传输时,使用
dma_map_single
3.2 DMA传输方向
DMA传输方向是一个容易出错的地方,内核定义了四种方向:
DMA_TO_DEVICE // 内存 -> 设备 DMA_FROM_DEVICE // 设备 -> 内存 DMA_BIDIRECTIONAL // 双向传输 DMA_NONE // 用于调试重要提示:错误设置传输方向可能导致数据损坏或系统崩溃。我曾经因为将DMA_TO_DEVICE误设为DMA_FROM_DEVICE,导致设备寄存器被意外覆盖。
4. 实战:实现一个DMA驱动
4.1 硬件准备
假设我们有一个基于STM32H750的开发板,需要通过DMA驱动SPI接口的LCD。硬件连接如下:
- SPI时钟:25MHz
- LCD数据宽度:16位
- DMA通道:DMA1 Stream3
4.2 驱动实现步骤
- 初始化DMA控制器:
static int __init my_dma_driver_init(void) { // 申请DMA通道 dma_cap_zero(mask); dma_cap_set(DMA_SLAVE, mask); dma_chan = dma_request_channel(mask, NULL, NULL); // 配置DMA参数 struct dma_slave_config config = { .direction = DMA_MEM_TO_DEV, .dst_addr = spi_reg_base + SPI_DR_OFFSET, .dst_addr_width = DMA_SLAVE_BUSWIDTH_2_BYTES, .dst_maxburst = 4, }; dmaengine_slave_config(dma_chan, &config); return 0; }- 准备DMA传输描述符:
struct dma_async_tx_descriptor *tx_desc; tx_desc = dmaengine_prep_slave_single(dma_chan, dma_buf_phy, buf_len, DMA_MEM_TO_DEV, DMA_PREP_INTERRUPT | DMA_CTRL_ACK);- 设置完成回调:
tx_desc->callback = dma_transfer_complete; tx_desc->callback_param = &complete;- 提交并启动DMA传输:
dmaengine_submit(tx_desc); dma_async_issue_pending(dma_chan);4.3 性能优化技巧
在实际项目中,我总结了几个DMA性能优化的小技巧:
- 缓冲区对齐:确保DMA缓冲区按cache line大小对齐(通常是32或64字节),可以显著提升性能
- 批量传输:尽量使用scatter-gather列表,减少DMA启动开销
- 双缓冲:使用双缓冲技术可以避免传输过程中的数据竞争
5. 常见问题与调试技巧
5.1 DMA传输不启动
症状:配置了DMA但传输没有开始。
排查步骤:
- 检查DMA控制器的时钟是否使能
- 验证DMA通道是否被其他设备占用
- 检查传输长度是否为0
- 确认DMA中断是否被正确配置
5.2 数据损坏或不完整
症状:传输完成但数据有问题。
可能原因:
- 缓存一致性问题(最常见)
- 缓冲区越界
- 传输方向设置错误
解决方法:
// 在DMA操作前刷新缓存 dma_sync_single_for_device(dev, dma_handle, size, direction); // 在DMA操作后无效化缓存 dma_sync_single_for_cpu(dev, dma_handle, size, direction);5.3 系统死锁或崩溃
症状:系统在使用DMA时死机。
可能原因:
- DMA访问了非法内存地址
- 中断风暴(DMA完成中断未及时处理)
- 资源竞争
调试技巧:
- 使用
dma-debug工具检查DMA使用情况 - 在DMA完成中断中添加超时检测
- 使用
mem=XXXM内核参数限制DMA可用内存范围
6. 高级话题:分散/聚集DMA
对于复杂的数据传输场景,Linux提供了scatter-gather DMA支持。这种技术允许将多个不连续的缓冲区通过一次DMA传输完成。
实现步骤:
- 准备scatterlist:
struct scatterlist sg[2]; sg_init_table(sg, 2); sg_set_buf(&sg[0], buf1, len1); sg_set_buf(&sg[1], buf2, len2);- 映射scatterlist:
nents = dma_map_sg(dev, sg, 2, direction);- 准备并提交传输:
tx_desc = dmaengine_prep_slave_sg(dma_chan, sg, nents, direction, flags); dmaengine_submit(tx_desc); dma_async_issue_pending(dma_chan);我在一个视频采集项目中使用了scatter-gather DMA,成功将吞吐量提升了40%,同时CPU占用率降低了15%。
7. DMA与缓存一致性
缓存一致性是DMA开发中最棘手的问题之一。现代CPU的缓存架构使得内存中的数据可能有多个副本:
- CPU缓存中的副本
- 主内存中的副本
- DMA控制器看到的副本
当这些副本不一致时,就会导致各种奇怪的问题。Linux提供了几种处理方案:
- 一致性DMA缓冲区:使用
dma_alloc_coherent分配,但性能较低 - 流式DMA映射:使用
dma_map_single,需要手动维护缓存一致性 - DMA属性:通过
DMA_ATTR_NON_CONSISTENT等属性精细控制
我的经验法则是:对于频繁交换的小数据块,使用一致性DMA;对于大数据块传输,使用流式DMA并妥善处理缓存同步。
8. 实际案例分析:SPI LCD驱动优化
让我们看一个真实案例:优化STM32H750的SPI LCD驱动。
原始方案:使用CPU轮询方式传输,帧率只有15fps。
优化步骤:
- 改用DMA传输
- 实现双缓冲机制
- 调整SPI时钟分频
- 优化DMA描述符链
优化后结果:帧率提升到45fps,CPU占用率从80%降到15%。
关键代码片段:
// 双缓冲设置 struct lcd_buffer { void *virt_addr; dma_addr_t dma_addr; } buffers[2]; // 交替使用缓冲区 current_buf = (current_buf + 1) % 2; next_buf = (current_buf + 1) % 2; // 启动DMA传输 dma_transfer(buffers[current_buf]); // 准备下一个缓冲区 prepare_next_frame(buffers[next_buf]);这个案例展示了DMA在实际项目中的巨大价值,特别是在需要高性能数据传输的场景下。