DMA基础:DMA控制器工作原理、DMA传输模式与CPU协同机制
2.1 DMA控制器工作原理
直接存储器访问(DMA)是一种允许硬件子系统直接访问系统内存的机制,无需CPU干预。DMA控制器是系统中的专用硬件模块,负责在源地址与目的地址之间搬运数据,从而将CPU从繁重的数据拷贝任务中解放出来。
核心组件
| 组件 | 功能描述 |
|---|---|
| 源地址寄存器 | 存储数据读取的起始内存地址或外设地址 |
| 目的地址寄存器 | 存储数据写入的目标内存地址或外设地址 |
| 传输计数寄存器 | 记录待传输的数据单元数量(字节/字/块) |
| 控制寄存器 | 配置传输方向、模式、宽度、优先级等参数 |
| 状态寄存器 | 指示传输完成、错误、中断等状态信息 |
| 请求/应答逻辑 | 处理来自外设的DMA请求,向总线仲裁器发起总线控制权申请 |
基本工作流程
- 初始化:CPU配置DMA控制器的源地址、目的地址、传输长度和传输模式。
- 请求:外设(如ADC、UART、摄像头)通过硬件信号线向DMA控制器发出传输请求。
- 仲裁:DMA控制器向总线仲裁器申请总线控制权,获得授权后接管系统总线。
- 传输:DMA控制器在源与目的之间直接搬运数据,每次传输后更新地址和计数寄存器。
- 完成:传输计数归零后,DMA控制器释放总线,向CPU发出中断信号或触发回调。
关键指标:DMA传输延迟通常为几个总线时钟周期,远低于CPU逐字节拷贝的软件开销。在嵌入式系统中,DMA可将CPU的I/O数据搬运开销从80%降低至5%以下。
2.2 DMA传输模式
根据应用场景的不同,DMA控制器支持多种传输模式。选择合适的模式可以显著提升系统吞吐量并降低CPU负载。
三种核心传输模式对比
| 模式 | 工作方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|---|
| 单次传输 | 每次触发传输一个数据单元(字节/字),完成后停止 | 低速外设、单次数据采集 | 实现简单,资源占用少 | 频繁重新配置,效率低 |
| 循环传输 | 传输完成后自动重置地址和计数,持续循环 | 音频流、ADC连续采样、环形缓冲区 | 无需CPU干预,实时性好 | 需处理缓冲区覆盖问题 |
| 链表传输 | 通过描述符链表链接多个传输任务,自动切换 | 复杂数据流、多通道采集、分散/聚集I/O | 灵活高效,支持任意地址组合 | 描述符管理复杂,内存开销大 |
单次传输模式
单次传输是最基本的DMA模式。CPU配置一次传输参数后,DMA控制器在收到外设请求时传输一个数据单元,然后停止并等待下一次配置。该模式适用于按键、温度传感器等低速设备。
循环传输模式
循环传输模式下,DMA控制器在传输计数归零后自动将地址和计数寄存器重置为初始值,形成环形缓冲区。音频播放、麦克风采集等连续数据流场景广泛使用此模式。开发者需注意缓冲区半满中断和覆盖保护机制。
实践建议:在循环传输中,通常使用双缓冲(ping-pong buffer)技术:一块缓冲区用于DMA写入,另一块供CPU读取,交替使用以避免数据竞争。
链表传输模式
链表传输(也称为描述符链或散聚DMA)是最高级的传输模式。DMA控制器维护一个描述符链表,每个描述符包含源地址、目的地址、传输长度和下一个描述符指针。当前传输完成后,硬件自动加载下一个描述符,无需CPU介入。
2.3 DMA与CPU的协同机制
DMA与CPU共享系统总线和内存资源,必须建立高效的协同机制以避免冲突和保证数据一致性。
总线仲裁
当DMA控制器和外设同时请求总线时,总线仲裁器根据优先级策略决定谁获得总线控制权。常见策略包括:
- 固定优先级:DMA优先级高于CPU,适用于实时性要求高的场景
- 循环优先级:各主设备轮流获得总线,保证公平性
- 动态优先级:根据当前负载动态调整,实现最优吞吐
Cache一致性
DMA直接访问物理内存,而CPU可能使用Cache缓存数据。当DMA修改了内存内容,CPU的Cache中可能仍保留旧数据,导致不一致问题。解决方案包括:
| 方法 | 操作 | 适用场景 |
|---|---|---|
| Cache刷新(Flush) | DMA传输前将CPU Cache写回内存 | CPU写、DMA读 |
| Cache失效(Invalidate) | DMA传输后将CPU Cache标记为无效 | DMA写、CPU读 |
| 非Cache内存 | 将DMA缓冲区映射为不可Cache区域 | 高频DMA传输 |
| 硬件一致性 | 使用支持Cache Coherent的总线协议 | 高性能SoC |
中断与轮询
DMA传输完成后,通过中断通知CPU是最常用的方式。但在高频率小数据量场景下,中断开销可能过高,此时可考虑轮询状态寄存器。
性能陷阱:在中断服务程序中执行复杂操作会阻塞后续DMA传输。建议ISR中仅设置标志位或唤醒等待队列,将数据处理推迟到任务上下文中执行。
典型协同流程
- CPU分配DMA缓冲区并确保Cache一致性(Flush/Invalidate)
- CPU配置DMA控制器参数并启动传输
- DMA控制器独立传输数据,CPU继续执行其他任务
- DMA传输完成,触发中断
- CPU在中断上下文中标记数据就绪,唤醒处理任务
- CPU处理数据前执行Cache失效操作(若DMA写入内存)
2.4 实际应用中的DMA配置示例
以下代码展示在Linux内核中配置DMA循环传输的典型流程,用于音频采集场景:
#include <linux/dmaengine.h>
#include <linux/dma-mapping.h>
struct dma_chan *chan;
struct dma_slave_config config;
struct dma_async_tx_descriptor *tx;
dma_cookie_t cookie;
/* 1. 请求DMA通道 */
chan = dma_request_chan(dev, "audio_rx");
if (IS_ERR(chan))
return PTR_ERR(chan);
/* 2. 配置从设备参数 */
memset(&config, 0, sizeof(config));
config.direction = DMA_DEV_TO_MEM;
config.src_addr = (phys_addr_t)audio_base + AUDIO_FIFO;
config.src_addr_width = DMA_SLAVE_BUSWIDTH_4_BYTES;
config.src_maxburst = 4;
dmaengine_slave_config(chan, &config);
/* 3. 准备循环传输描述符 */
tx = dmaengine_prep_dma_cyclic(chan,
dma_map_single(dev, buf, buf_size, DMA_FROM_DEVICE),
buf_size,
period_size,
DMA_DEV_TO_MEM,
DMA_PREP_INTERRUPT);
if (!tx)
goto err_free;
/* 4. 注册完成回调 */
tx->callback = audio_dma_complete;
tx->callback_param = audio_dev;
/* 5. 提交并启动传输 */
cookie = dmaengine_submit(tx);
dma_async_issue_pending(chan);
性能优化要点:
• 使用
•
• 回调函数中应使用
• 使用
dma_map_single 确保Cache一致性,避免数据错乱•
period_size 决定中断频率,通常设置为缓冲区大小的1/4或1/2• 回调函数中应使用
tasklet 或 workqueue 处理数据,避免在中断上下文阻塞