1. 什么是DMA,为什么需要它?

如果你正在用STM32做项目,肯定遇到过数据搬运的问题。比如从ADC读取大量采样数据,或者通过串口发送接收大块数据。传统做法是让CPU一个个字节地搬运,这就像是你自己一趟趟跑腿拿快递,效率低还累个半死。DMA(直接存储器访问)就像是雇了个专职快递小哥,你只需要告诉他从哪里取货、送到哪里,剩下的搬运工作完全不用操心。

我在实际项目中就吃过亏。有一次做音频采集,需要实时保存ADC的数据到内存。开始傻傻地用CPU搬运,结果发现采集频率一高,CPU使用率直接飙到80%以上,其他任务根本跑不动。后来换成DMA来处理数据传输,CPU使用率直接降到10%以下,系统瞬间轻松多了。

DMA在STM32里就像是个高效的后勤部长,专门负责数据搬运的脏活累活。STM32F1系列有两个DMA控制器(DMA1和DMA2),总共12个通道。每个通道都可以独立工作,支持存储器到存储器、外设到存储器、存储器到外设三种传输模式。最重要的是,所有这些传输都不需要CPU参与,大大提升了系统效率。

2. DMA的三种经典应用场景

2.1 存储器到存储器传输

这是最基础的DMA应用,就像是在两个仓库之间搬运货物。我们来看个实际例子:需要在内存中快速复制一个大数组。用传统方法需要循环遍历每个元素,而用DMA只需要配置好源地址和目的地址,一键启动就能完成批量搬运。

我做过一个图像处理项目,需要将摄像头采集的原始数据复制到处理缓冲区。用DMA之后,复制1024x768的图像数据只需要几毫秒,而且CPU全程可以处理其他任务。这就是DMA的威力所在。

// 存储器到存储器传输配置示例
void DMA_MemToMem_Config(uint32_t srcAddr, uint32_t dstAddr, uint16_t size)
{
    DMA_InitTypeDef DMA_InitStructure;
    
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    
    DMA_InitStructure.DMA_PeripheralBaseAddr = srcAddr;
    DMA_InitStructure.DMA_MemoryBaseAddr = dstAddr;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
    DMA_InitStructure.DMA_BufferSize = size;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Enable;
    
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);
}

2.2 外设到存储器传输

这个模式在数据采集场景中特别有用。比如用ADC采集传感器数据,或者从SPI、I2C接口接收数据。DMA会自动把外设数据搬到指定内存区域,完全不需要CPU干预。

我在做一个环境监测系统时,需要连续采集温湿度数据。用DMA配合ADC,设置了循环缓冲模式,ADC自动采样,DMA自动搬运,CPU只需要定时处理缓冲区的数据即可,系统响应速度提升明显。

实际使用中要注意数据对齐问题。如果外设数据宽度是16位,内存也最好使用16位变量来接收,否则可能出现数据错位。

2.3 存储器到外设传输

这种模式常用于数据发送场景,比如通过串口发送大量数据,或者通过SPI驱动显示屏。配置好要发送的数据地址和外设地址,DMA就会自动完成发送工作。

有个坑我踩过:DMA传输过程中如果源数据被修改,可能会发送错误的数据。所以最好在DMA传输完成中断后再更新数据源,或者使用双缓冲机制。

3. DMA配置详解与实战代码

3.1 时钟使能与初始化

DMA挂在AHB总线上,所以要先开启AHB时钟。这个步骤很容易忘记,但又是必须的。我建议在项目初期就写好所有外设时钟的初始化函数,避免后期调试时找不到问题。

void DMA_Initialization(void)
{
    // 开启DMA时钟
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    
    // 复位DMA通道
    DMA_DeInit(DMA1_Channel1);
    
    // 等待DMA配置完成
    Delay_us(10);
}

3.2 参数配置要点

DMA的配置结构体有很多参数,每个都很重要。这里分享几个实际项目中容易出错的点:

数据传输方向:这个参数决定了数据流动的方向。如果是存储器到存储器,要设置DMA_DIR_PeripheralSRC,表示外设端是数据源。

地址自增设置:大多数情况下都需要开启地址自增。如果忘记开启,DMA会一直在同一个地址读写,导致数据覆盖。

数据宽度匹配:源和目的的数据宽度最好一致。如果外设是16位,内存也用16位变量,否则需要特别处理字节序问题。

我在一次项目中就因为数据宽度不匹配栽了跟头。外设是8位数据,内存用了32位变量,结果数据完全对不上。调试了半天才发现是数据宽度配置错误。

3.3 传输模式选择

DMA支持正常模式和循环模式。正常模式传输指定数量后停止,适合单次传输。循环模式则持续传输,适合持续数据流。

对于ADC连续采样,我推荐用循环模式。设置好缓冲区和传输数量,DMA就会自动循环填充缓冲区,CPU只需要处理数据即可。

// 循环模式配置示例
void DMA_CircularMode_Config(void)
{
    DMA_InitTypeDef DMA_InitStructure;
    
    // 其他配置...
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;  // 循环模式
    DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE;
    
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);
    
    // 开启传输完成中断
    DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE);
}

4. 实际项目中的DMA应用技巧

4.1 双缓冲机制

在处理实时数据流时,双缓冲是个很有用的技巧。原理很简单:准备两个缓冲区,DMA向一个缓冲区写数据时,CPU处理另一个缓冲区的数据。两个缓冲区交替使用,既保证了数据完整性,又提高了处理效率。

我在音频处理项目中就用到了这个技术。DMA正在填充缓冲区A时,CPU在处理缓冲区B的数据。当DMA填满A后,自动切换到填充B,同时CPU开始处理A的数据。如此循环,实现了无缝数据处理。

// 双缓冲配置示例
uint16_t buffer1[BUFFER_SIZE];
uint16_t buffer2[BUFFER_SIZE];

void DMA_DoubleBuffer_Config(void)
{
    DMA_InitTypeDef DMA_InitStructure;
    
    // 基本配置...
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)buffer1;
    DMA_InitStructure.DMA_Memory1BaseAddr = (uint32_t)buffer2;
    DMA_InitStructure.DMA_BufferSize = BUFFER_SIZE;
    
    // 启用双缓冲模式
    DMA_DoubleBufferModeConfig(DMA1_Channel1, (uint32_t)buffer1, DMA_Memory_0);
    DMA_DoubleBufferModeCmd(DMA1_Channel1, ENABLE);
    
    DMA_Init(DMA1_Channel1, &DMA_InitStructure);
}

4.2 中断使用技巧

DMA提供了多种中断:传输完成、半传输、传输错误。合理使用这些中断可以进一步提升系统效率。

传输完成中断很好理解,就是整个缓冲区传输完成时触发。半传输中断则是在传输完成一半时触发,这个中断在双缓冲机制中特别有用。

我通常这样配置:开启传输完成和半传输中断。在半传输中断中处理前一半数据,在传输完成中断中处理后一半数据。这样数据处理的延迟更小,实时性更好。

中断处理函数要尽量简短,只做标志位设置和数据拷贝,繁重的处理放到主循环中。否则可能影响DMA性能。

4.3 性能优化建议

经过多个项目的实践,我总结出几个DMA性能优化建议:

内存对齐:确保源地址和目的地址都按照数据宽度对齐。32位数据最好4字节对齐,这样DMA传输效率最高。

缓冲区大小:缓冲区不是越大越好。太大会增加延迟,太小会导致频繁中断。一般根据数据速率和处理时间折中选择。

优先级设置:多个DMA通道同时工作时,要合理设置优先级。实时性要求高的通道设置更高优先级。

5. 常见问题与调试技巧

5.1 DMA传输不工作

这是最常见的问题,通常有几个原因:时钟没有开启、地址配置错误、传输数量为0、没有使能DMA通道。

我的调试步骤是:首先检查时钟是否开启,然后确认源地址和目的地址是否正确,再看传输数量是否大于0,最后检查DMA通道使能位。

// DMA调试检查清单
void DMA_DebugCheck(void)
{
    // 1. 检查时钟
    if(RCC_AHBPeriph_GetClockCmd(RCC_AHBPeriph_DMA1) == DISABLE)
    {
        printf("DMA时钟未开启!\n");
    }
    
    // 2. 检查传输数量
    if(DMA_GetCurrDataCounter(DMA1_Channel1) == 0)
    {
        printf("传输数量为0!\n");
    }
    
    // 3. 检查通道使能
    if((DMA1_Channel1->CCR & DMA_CCR1_EN) == 0)
    {
        printf("DMA通道未使能!\n");
    }
}

5.2 数据错位问题

数据错位通常是因为数据宽度不匹配或者字节序问题。比如源是16位数据,目的是8位变量,就会出现数据截断。

解决方案是确保源和目的的数据宽度一致,并且注意处理器的大小端模式。STM32是小端模式,低地址存放低位字节。

5.3 传输完成检测

检测DMA传输完成有多种方法:查询标志位、使用中断、回调函数。我个人推荐用中断方式,这样不会阻塞主程序。

如果选择查询方式,一定要设置超时机制,避免DMA传输异常时程序死等。

// 带超时的DMA传输等待
uint8_t DMA_WaitForTransfer(uint32_t timeout)
{
    uint32_t startTime = GetTickCount();
    
    while(DMA_GetFlagStatus(DMA1_FLAG_TC1) == RESET)
    {
        if(GetTickCount() - startTime > timeout)
        {
            return 0; // 超时
        }
    }
    
    DMA_ClearFlag(DMA1_FLAG_TC1);
    return 1; // 成功
}

6. 进阶应用:DMA与其它外设配合使用

6.1 DMA与ADC的完美组合

ADC连续采样配合DMA传输是最经典的组合。配置好ADC的采样率和DMA的缓冲区,就可以实现无人值守的数据采集。

我做过一个振动监测系统,用ADC采集振动传感器数据,DMA将数据实时传输到内存,CPU只需要进行频谱分析。采样率做到100kHz仍然游刃有余。

关键配置点:ADC要设置为连续转换模式,DMA设置为循环模式,并开启传输完成中断。这样ADC就会自动采样,DMA自动传输,形成一个完整的数据流水线。

6.2 DMA与串口的高效通信

串口大量数据传输时,DMA可以大大减轻CPU负担。特别是Modbus、自定义协议等需要处理大量数据的场景。

发送时,配置DMA为存储器到外设模式,数据源是要发送的缓冲区。接收时,配置为外设到存储器模式,目的是接收缓冲区。

有个实用技巧:串口空闲中断配合DMA接收。当串口检测到空闲时,说明一帧数据接收完成,此时处理DMA接收到的数据,非常高效。

6.3 DMA与SPI/I2C的配合

SPI和I2C通信也经常需要传输大量数据,比如读写Flash、驱动显示屏等。DMA可以自动处理这些数据传输,让CPU腾出手来做其他事情。

SPI通常支持全双工DMA,可以同时使用发送DMA和接收DMA。配置时要注意SPI的DR寄存器是同一个,所以发送和接收的DMA通道要正确配置。

我用DMA驱动过SPI显示屏,刷屏速度比用CPU搬运快了3倍以上,而且CPU占用率几乎为零。

Logo

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

更多推荐