1. 初识DMA:为什么你的STM32需要这个"搬运工"

如果你正在用STM32做项目,大概率遇到过这样的场景:串口要收发大量数据,ADC需要连续采样,或者需要把数据从内存搬到外设。如果这些活都让CPU来干,就像让一个工程师去搬砖,不仅大材小用,还会拖慢整个系统的运行速度。

我这里有个真实的例子:去年我做了一个智能家居控制器,需要同时处理串口通信、ADC采样和SPI Flash读写。最初没用DMA,CPU占用率直接飙到70%以上,系统响应慢得让人抓狂。后来上了DMA,同样的工作量,CPU占用降到20%以下,整个系统流畅得不像同一块芯片。

DMA的全称是Direct Memory Access(直接存储器访问),说白了就是芯片内部的一个专职"搬运工"。它能在不需要CPU参与的情况下,自动完成数据搬运工作。想象一下,CPU是公司里的项目经理,DMA就是专职的行政助理——项目经理只需要说"把这份文件从A部门送到B部门",剩下的具体工作就完全交给助理了。

在STM32中,DMA可以处理多种数据传输方向:

  • 从外设到内存(比如ADC采样数据存到数组)
  • 从内存到外设(比如把数组数据通过串口发送)
  • 内存到内存(部分型号支持,比如大数据块拷贝)

2. DMA工作原理深度解析

2.1 DMA的核心工作机制

DMA的工作原理其实很直观。当外设需要传输数据时,它会向DMA控制器发出请求。DMA控制器接收到请求后,就会接管系统总线,直接在外设和内存之间搬运数据。整个过程CPU完全不用参与,可以继续执行其他任务。

我特别喜欢用快递站的比喻来解释DMA:CPU就像快递站老板,数据包裹就像需要派送的快递。如果没有DMA,每个包裹都需要老板亲自处理——接收、登记、分拣、派送,忙得不可开交。有了DMA之后,老板只需要雇佣几个快递员(DMA通道),告诉它们从哪里取件、送到哪里、送多少件,剩下的工作就完全不用操心了。

2.2 关键寄存器详解

DMA的配置主要通过一组寄存器完成,每个通道都有自己的一套寄存器。最重要的包括:

DMA_CPARx:存放外设地址。比如你要从ADC的数据寄存器取数据,就把ADC_DR的地址写在这里。

DMA_CMARx:存放内存地址。这就是数据要搬运到的目的地,通常是我们定义的数组或缓冲区。

DMA_CNDTRx:设置要传输的数据量。这是个递减计数器,每传输一个数据就自动减1,减到0就表示传输完成。

DMA_CCRx:这个寄存器最复杂,包含了通道的各种配置选项:

  • 数据传输方向(内存到外设还是外设到内存)
  • 循环模式还是单次模式
  • 地址是否自动递增
  • 数据宽度(8位、16位还是32位)
  • 通道优先级
// 示例:配置DMA通道的基本寄存器设置
DMA1_Channel1->CPAR = (uint32_t)&ADC1->DR;  // 外设地址:ADC数据寄存器
DMA1_Channel1->CMAR = (uint32_t)adc_buffer; // 内存地址:自定义数组
DMA1_Channel1->CNDTR = 100;                // 传输100个数据
DMA1_Channel1->CCR |= DMA_CCR_EN;           // 使能DMA通道

3. DMA vs CPU传输:性能对决

3.1 传统CPU传输的局限性

在没有DMA的情况下,所有数据搬运都得CPU亲力亲为。比如从ADC读取数据,CPU需要:

  1. 等待ADC转换完成
  2. 从ADC数据寄存器读取值
  3. 把这个值写入内存中的数组
  4. 更新数组索引
  5. 检查是否采集了足够的数据

这个过程每个数据都要消耗多个CPU周期。如果采样率是10kHz,意味着CPU每100微秒就要被中断一次,根本干不了其他正事。

3.2 DMA传输的优势

同样的ADC采样任务,用DMA来实现:

  1. CPU初始化DMA,设置好源地址(ADC数据寄存器)、目标地址(内存数组)、数据量
  2. 启动DMA和ADC
  3. DMA自动把每个ADC转换结果搬运到数组
  4. 传输完成后产生中断通知CPU

在这个过程中,CPU只在开始和结束时参与,中间可以完全专注于其他任务。实测下来,使用DMA后系统效率提升非常明显,特别是在需要高速数据处理的场合。

3.3 实际性能对比

为了直观展示差异,我做了个实验:用STM32F103同时处理串口通信和ADC采样,比较使用DMA前后的性能表现。

任务类型 CPU占用率(无DMA) CPU占用率(有DMA) 性能提升
串口发送1KB数据 35% 5% 7倍
ADC连续采样1000点 45% 8% 5.6倍
SPI传输2KB数据 50% 10% 5倍

从数据可以看出,DMA带来的性能提升是实实在在的。特别是在多任务系统中,这种提升会更加明显。

4. DMA配置实战:一步步教你设置

4.1 寄存器级配置详解

虽然现在很多开发者喜欢用HAL库,但了解寄存器级的配置仍然很重要,这能帮你真正理解DMA的工作原理。

首先需要开启DMA时钟,这个很多人会忘记:

RCC->AHBENR |= RCC_AHBENR_DMA1EN;  // 使能DMA1时钟

然后配置DMA通道的各个寄存器。以内存到外设的串口发送为例:

void dma_uart_tx_init(void)
{
    // 设置外设地址(串口数据寄存器)
    DMA1_Channel4->CPAR = (uint32_t)&USART1->DR;
    
    // 设置内存地址(发送缓冲区)
    DMA1_Channel4->CMAR = (uint32_t)tx_buffer;
    
    // 设置传输数据量
    DMA1_Channel4->CNDTR = TX_BUFFER_SIZE;
    
    // 配置通道参数
    DMA1_Channel4->CCR = 
        DMA_CCR_DIR |         // 内存到外设
        DMA_CCR_MINC |        // 内存地址递增
        DMA_CCR_PSIZE_0 |     // 外设数据宽度8位
        DMA_CCR_MSIZE_0 |     // 内存数据宽度8位  
        DMA_CCR_PL_0;         // 中等优先级
    
    // 使能通道
    DMA1_Channel4->CCR |= DMA_CCR_EN;
}

4.2 使用标准库配置

STM32标准库让DMA配置变得简单很多。首先定义初始化结构体,然后调用库函数:

void dma_config_using_lib(void)
{
    DMA_InitTypeDef dma_init;
    
    // 使能DMA时钟
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    
    // 配置DMA初始化结构体
    dma_init.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
    dma_init.DMA_MemoryBaseAddr = (uint32_t)tx_buffer;
    dma_init.DMA_DIR = DMA_DIR_PeripheralDST;  // 内存到外设
    dma_init.DMA_BufferSize = TX_BUFFER_SIZE;
    dma_init.DMA_PeripheralInc = DMA_PeripheralInc_Disable; // 外设地址不递增
    dma_init.DMA_MemoryInc = DMA_MemoryInc_Enable;          // 内存地址递增
    dma_init.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    dma_init.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    dma_init.DMA_Mode = DMA_Mode_Normal;        // 单次模式
    dma_init.DMA_Priority = DMA_Priority_Medium;
    dma_init.DMA_M2M = DMA_M2M_Disable;         // 非内存到内存模式
    
    // 初始化DMA通道
    DMA_Init(DMA1_Channel4, &dma_init);
    
    // 使能DMA通道
    DMA_Cmd(DMA1_Channel4, ENABLE);
}

4.3 使用HAL库配置

HAL库进一步简化了配置过程,特别适合快速开发:

void dma_config_using_hal(void)
{
    // 声明DMA句柄
    DMA_HandleTypeDef hdma_uart_tx;
    
    // 配置DMA句柄
    hdma_uart_tx.Instance = DMA1_Channel4;
    hdma_uart_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
    hdma_uart_tx.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_uart_tx.Init.MemInc = DMA_MINC_ENABLE;
    hdma_uart_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
    hdma_uart_tx.Init.MemDataAlignment = DMA_MDATAALIGN_BYTE;
    hdma_uart_tx.Init.Mode = DMA_NORMAL;
    hdma_uart_tx.Init.Priority = DMA_PRIORITY_MEDIUM;
    
    // 初始化DMA
    HAL_DMA_Init(&hdma_uart_tx);
    
    // 关联DMA到串口
    __HAL_LINKDMA(&huart1, hdmatx, hdma_uart_tx);
}

5. 串口DMA实战应用

5.1 串口DMA发送配置

串口DMA发送是最常见的应用场景之一。传统方式下,每个字节都需要CPU参与发送,效率极低。使用DMA后,只需要设置好发送缓冲区和数据量,DMA就会自动完成全部发送工作。

这是我实际项目中用的串口DMA发送配置:

void uart_dma_tx_config(void)
{
    // 使能DMA和USART时钟
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
    
    // DMA配置
    DMA_DeInit(DMA1_Channel4);
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)tx_buffer;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
    DMA_InitStructure.DMA_BufferSize = TX_BUFFER_SIZE;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    DMA_Init(DMA1_Channel4, &DMA_InitStructure);
    
    // 使能USART的DMA发送请求
    USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE);
    
    // 使能DMA通道
    DMA_Cmd(DMA1_Channel4, ENABLE);
}

5.2 串口DMA接收配置

DMA接收更加实用,特别是处理不定长数据。我们可以设置DMA为循环模式,这样接收缓冲区就像一个环形缓冲区,新数据会自动覆盖旧数据。

void uart_dma_rx_config(void)
{
    // 初始化DMA接收
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR;
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)rx_buffer;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
    DMA_InitStructure.DMA_BufferSize = RX_BUFFER_SIZE;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Circular;  // 循环模式
    DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    DMA_Init(DMA1_Channel5, &DMA_InitStructure);
    
    // 使能USART DMA接收
    USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE);
    
    // 使能DMA通道
    DMA_Cmd(DMA1_Channel5, ENABLE);
}

5.3 实用技巧:处理不定长数据

DMA接收固定长度数据很简单,但实际项目中经常需要处理不定长数据。我这里有个很实用的方法:

// 获取当前DMA接收的数据量
uint16_t get_uart_rx_count(void)
{
    // 总缓冲区大小减去剩余未传输的数据量
    return RX_BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5);
}

// 处理接收到的数据
void process_rx_data(void)
{
    uint16_t rx_count = get_uart_rx_count();
    if (rx_count != last_rx_count) {
        // 有新数据到达
        uint16_t new_data_len = (rx_count > last_rx_count) ? 
                               (rx_count - last_rx_count) :
                               (RX_BUFFER_SIZE - last_rx_count + rx_count);
        
        // 处理新数据
        process_new_data(new_data_len);
        
        last_rx_count = rx_count;
    }
}

这种方法利用DMA的循环缓冲特性,实现了高效的串口数据接收。我在多个项目中都采用这种方案,稳定性和效率都很不错。

6. ADC与DMA的完美组合

6.1 ADC连续采样配置

ADC连续采样是DMA的另一个典型应用场景。传统方式需要CPU频繁中断来读取采样值,使用DMA后可以实现真正的"设置后不管"。

以下是一个ADC多通道连续采样的配置示例:

void adc_dma_config(void)
{
    ADC_InitTypeDef adc_init;
    DMA_InitTypeDef dma_init;
    
    // 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_ADC1, ENABLE);
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    
    // 配置DMA
    dma_init.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR;
    dma_init.DMA_MemoryBaseAddr = (uint32_t)adc_values;
    dma_init.DMA_DIR = DMA_DIR_PeripheralSRC;
    dma_init.DMA_BufferSize = ADC_CHANNEL_COUNT;
    dma_init.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    dma_init.DMA_MemoryInc = DMA_MemoryInc_Enable;
    dma_init.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord;
    dma_init.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord;
    dma_init.DMA_Mode = DMA_Mode_Circular;
    dma_init.DMA_Priority = DMA_Priority_High;
    dma_init.DMA_M2M = DMA_M2M_Disable;
    DMA_Init(DMA1_Channel1, &dma_init);
    
    // 配置ADC
    adc_init.ADC_Mode = ADC_Mode_Independent;
    adc_init.ADC_ScanConvMode = ENABLE;        // 扫描模式
    adc_init.ADC_ContinuousConvMode = ENABLE;  // 连续转换
    adc_init.ADC_ExternalTrigConv = ADC_ExternalTrigConv_None;
    adc_init.ADC_DataAlign = ADC_DataAlign_Right;
    adc_init.ADC_NbrOfChannel = ADC_CHANNEL_COUNT;
    ADC_Init(ADC1, &adc_init);
    
    // 配置ADC通道
    ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_239Cycles5);
    ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_239Cycles5);
    
    // 使能ADC DMA
    ADC_DMACmd(ADC1, ENABLE);
    
    // 使能DMA和ADC
    DMA_Cmd(DMA1_Channel1, ENABLE);
    ADC_Cmd(ADC1, ENABLE);
    
    // 开始转换
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);
}

6.2 多通道采样数据处理

配置好多通道采样后,ADC会按顺序转换每个通道,DMA自动将结果存放到指定数组。比如配置了3个通道,那么adc_values[0]存放通道1的值,adc_values[1]存放通道2的值,以此类推。

这种方式的优势非常明显:无论采样速率多高,CPU都不需要参与数据搬运,只需要在需要时直接读取数组中的最新值即可。

7. SPI通信中的DMA应用

7.1 SPI DMA发送接收配置

SPI通信通常速率较高,使用DMA可以显著提升效率。特别是在需要传输大量数据时,比如读写SPI Flash、与显示屏通信等。

以下是一个SPI DMA传输的配置示例:

void spi_dma_config(void)
{
    // 使能时钟
    RCC_APB2PeriphClockCmd(RCC_APB2Periph_SPI1, ENABLE);
    RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE);
    
    // 配置DMA发送通道
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR;
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)tx_data;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST;
    DMA_InitStructure.DMA_BufferSize = DATA_SIZE;
    DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable;
    DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable;
    DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte;
    DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte;
    DMA_InitStructure.DMA_Mode = DMA_Mode_Normal;
    DMA_InitStructure.DMA_Priority = DMA_Priority_High;
    DMA_InitStructure.DMA_M2M = DMA_M2M_Disable;
    DMA_Init(DMA1_Channel3, &DMA_InitStructure);
    
    // 配置DMA接收通道
    DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&SPI1->DR;
    DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)rx_data;
    DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC;
    DMA_InitStructure.DMA_BufferSize = DATA_SIZE;
    DMA_Init(DMA1_Channel2, &DMA_InitStructure);
    
    // 使能SPI DMA请求
    SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Tx, ENABLE);
    SPI_I2S_DMACmd(SPI1, SPI_I2S_DMAReq_Rx, ENABLE);
    
    // 使能DMA通道
    DMA_Cmd(DMA1_Channel2, ENABLE);
    DMA_Cmd(DMA1_Channel3, ENABLE);
}

7.2 SPI DMA传输注意事项

SPI DMA传输有些细节需要特别注意。首先是传输长度的对齐问题,SPI通常是8位或16位传输,要确保缓冲区地址和数据长度都正确对齐。

另一个常见问题是传输完成判断。由于发送和接收是同时进行的,但速度可能略有差异,需要妥善处理传输完成中断:

void DMA1_Channel2_IRQHandler(void)  // SPI接收完成中断
{
    if (DMA_GetITStatus(DMA1_IT_TC2)) {
        DMA_ClearITPendingBit(DMA1_IT_TC2);
        // 处理接收完成
        spi_rx_complete = 1;
    }
}

void DMA1_Channel3_IRQHandler(void)  // SPI发送完成中断
{
    if (DMA_GetITStatus(DMA1_IT_TC3)) {
        DMA_ClearITPendingBit(DMA1_IT_TC3);
        // 处理发送完成
        spi_tx_complete = 1;
    }
}

在实际使用中,我通常等待发送完成和接收完成都触发后再处理数据,确保数据传输的完整性。

8. DMA使用中的常见问题与解决方案

8.1 内存对齐问题

DMA对内存对齐有严格要求,如果地址没有正确对齐,可能会导致传输失败或数据错误。比如设置16位传输时,内存地址必须是2的倍数;32位传输时,地址必须是4的倍数。

解决方案是使用编译器指令确保缓冲区正确对齐:

// 确保缓冲区按4字节对齐
__align(4) uint8_t dma_buffer[BUFFER_SIZE];

// 或者使用C11标准后的方式
_Alignas(4) uint8_t dma_buffer[BUFFER_SIZE];

8.2 缓冲区溢出问题

在使用循环模式时,如果数据处理速度跟不上数据产生速度,会导致缓冲区溢出,新数据覆盖未处理的数据。

我的解决方案是使用双缓冲区或环形缓冲区:

// 双缓冲区实现
uint8_t dma_buffer[2][BUFFER_SIZE];
uint8_t current_buffer = 0;

void DMA1_Channel1_IRQHandler(void)
{
    if (DMA_GetITStatus(DMA1_IT_HT1)) {
        // 半传输完成,处理前半部分数据
        process_data(dma_buffer[current_buffer], BUFFER_SIZE/2);
    } else if (DMA_GetITStatus(DMA1_IT_TC1)) {
        // 传输完成,处理后半部分数据
        process_data(dma_buffer[current_buffer] + BUFFER_SIZE/2, BUFFER_SIZE/2);
        
        // 切换缓冲区
        current_buffer = 1 - current_buffer;
        DMA_SetMemoryAddress(DMA1_Channel1, (uint32_t)dma_buffer[current_buffer]);
    }
    DMA_ClearITPendingBit(DMA1_IT_GL1);
}

8.3 优先级配置问题

多个DMA通道同时工作时,优先级配置不当可能导致高优先级任务被阻塞。我曾经遇到过ADC采样因为SPI传输优先级太高而丢失数据的情况。

解决方案是根据任务实时性要求合理设置优先级:

  • 高实时性任务(如ADC采样):最高优先级
  • 中等实时性任务(如串口通信):中等优先级
  • 低实时性任务(如内存拷贝):低优先级
// 设置通道优先级
DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;  // ADC采样
DMA_Init(DMA1_Channel1, &DMA_InitStructure);

DMA_InitStructure.DMA_Priority = DMA_Priority_Medium;    // 串口传输
DMA_Init(DMA1_Channel4, &DMA_InitStructure);

DMA_InitStructure.DMA_Priority = DMA_Priority_Low;       // 内存拷贝
DMA_Init(DMA1_Channel6, &DMA_InitStructure);

9. 高级技巧与性能优化

9.1 使用内存到内存传输

部分STM32型号支持内存到内存的DMA传输,这在需要大数据块拷贝时非常有用。不过要注意,内存到内存传输通常只能使用DMA2控制器。

void memory_copy_dma(uint32_t *src, uint32_t *dst, uint32_t size)
{
    // 检查DMA2是否可用
    if (!RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA2, ENABLE)) {
        // 不支持内存到内存传输,使用软件拷贝
        memcpy(dst, src, size * 4);
        return;
    }
    
    // 配置内存到内存传输
    DMA_InitTypeDef dma_init;
    dma_init.DMA_PeripheralBaseAddr = (uint32_t)src;
    dma_init.DMA_MemoryBaseAddr = (uint32_t)dst;
    dma_init.DMA_DIR = DMA_DIR_PeripheralSRC;
    dma_init.DMA_BufferSize = size;
    dma_init.DMA_PeripheralInc = DMA_PeripheralInc_Enable;
    dma_init.DMA_MemoryInc = DMA_MemoryInc_Enable;
    dma_init.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word;
    dma_init.DMA_MemoryDataSize = DMA_MemoryDataSize_Word;
    dma_init.DMA_Mode = DMA_Mode_Normal;
    dma_init.DMA_Priority = DMA_Priority_High;
    dma_init.DMA_M2M = DMA_M2M_Enable;  // 启用内存到内存模式
    
    DMA_Init(DMA2_Channel1, &dma_init);
    DMA_Cmd(DMA2_Channel1, ENABLE);
    
    // 等待传输完成
    while (DMA_GetFlagStatus(DMA2_FLAG_TC1) == RESET);
    DMA_ClearFlag(DMA2_FLAG_TC1);
}

9.2 优化传输性能

为了获得最佳的DMA传输性能,有几个关键点需要注意:

数据宽度匹配:尽量让源和目的的数据宽度一致,避免DMA需要做数据宽度转换。

地址对齐:确保地址按照数据宽度正确对齐,32位传输时地址要是4的倍数。

缓冲区大小:合理设置缓冲区大小,太小的缓冲区会增加中断频率,太大的缓冲区会增加内存占用和延迟。

总线带宽:注意DMA和CPU可能共享总线带宽,高优先级的DMA传输可能会暂时阻塞CPU对总线的访问。

我在一个音频处理项目中就遇到过因为DMA带宽占用过高导致CPU性能下降的问题。后来通过优化传输策略,将大块传输拆分成多个小块交错进行,解决了这个问题。

9.3 低功耗应用中的DMA

在低功耗应用中,DMA可以帮大忙。因为CPU可以在DMA工作时进入低功耗模式,等到DMA传输完成时再被唤醒。

void low_power_adc_sample(void)
{
    // 配置ADC和DMA
    adc_dma_config();
    
    // 使能DMA传输完成中断
    DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE);
    
    // 启动ADC转换
    ADC_SoftwareStartConvCmd(ADC1, ENABLE);
    
    // 进入低功耗模式,等待DMA中断唤醒
    __WFI();
}

// DMA中断处理函数
void DMA1_Channel1_IRQHandler(void)
{
    if (DMA_GetITStatus(DMA1_IT_TC1)) {
        DMA_ClearITPendingBit(DMA1_IT_TC1);
        // 处理采样数据
        process_adc_data();
    }
}

这种模式下,CPU大部分时间处于睡眠状态,只有在需要处理数据时才被唤醒,可以显著降低系统功耗。

我在一个电池供电的传感器项目中使用了这种技术,系统功耗降低了60%以上,电池寿命从3个月延长到了8个月,效果非常显著。

Logo

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

更多推荐