在嵌入式开发领域,数据搬运效率往往是系统性能的瓶颈。传统CPU搬运数据的方式就像用勺子运水——每次只能处理少量数据,且全程占用CPU资源。以STM32F103系列为例,用CPU搬运1KB数据需要约5200个时钟周期(72MHz主频下约72μs),而DMA仅需约1.4μs。这就是为什么DMA(Direct Memory Access)技术会成为嵌入式开发的必备技能。
DMA的本质是硬件级的数据搬运工,它通过独立于CPU的总线矩阵进行操作。当配置好源地址、目标地址和数据量后,DMA控制器会自动完成传输,期间CPU可以继续执行其他任务。这种机制特别适合以下场景:
关键认知:DMA不是外设,而是一种总线访问机制。STM32的DMA控制器像是个智能快递分拣系统,可以同时处理多个"包裹"(数据流)的转运需求。
以STM32F4系列为例,其DMA控制器采用双AHB总线架构:
c复制// DMA数据流配置结构体示例(HAL库)
typedef struct {
uint32_t Channel; // 数据流通道选择
uint32_t Direction; // 传输方向
uint32_t PeriphInc; // 外设地址递增
uint32_t MemInc; // 内存地址递增
uint32_t PeriphDataAlignment; // 外设数据宽度
uint32_t MemDataAlignment; // 内存数据宽度
uint32_t Mode; // 循环/普通模式
uint32_t Priority; // 优先级
} DMA_InitTypeDef;
| 模式类型 | 触发方式 | 典型应用 | 配置要点 |
|---|---|---|---|
| 普通模式 | 单次触发 | 非连续数据传输 | 需手动重启传输 |
| 循环模式 | 自动重载 | ADC连续采集 | 缓冲区需对齐 |
| 存储器到存储器 | 软件触发 | 内存数据搬移 | 不可用在外设到外设 |
硬件细节:STM32F4的DMA2控制器才能访问存储器到存储器的传输,DMA1仅支持外设相关传输。这个限制在芯片参考手册中经常被忽略。
以STM32F407的USART1为例,实现DMA发送的完整流程:
c复制__HAL_RCC_DMA2_CLK_ENABLE(); // USART1_TX使用DMA2 Stream7
__HAL_RCC_USART1_CLK_ENABLE();
c复制hdma_usart1_tx.Instance = DMA2_Stream7;
hdma_usart1_tx.Init.Channel = DMA_CHANNEL_4; // 查手册确定通道号
hdma_usart1_tx.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_usart1_tx.Init.MemInc = DMA_MINC_ENABLE; // 内存地址递增
hdma_usart1_tx.Init.PeriphDataAlignment = DMA_PDATAALIGN_BYTE;
hdma_usart1_tx.Init.Mode = DMA_NORMAL; // 非循环模式
HAL_DMA_Init(&hdma_usart1_tx);
启动DMA传输的正确时序:
c复制// 先关联DMA到USART
__HAL_LINKDMA(&huart1, hdmatx, hdma_usart1_tx);
// 再启动传输(注意数据长度单位)
HAL_UART_Transmit_DMA(&huart1, (uint8_t*)tx_buffer, sizeof(tx_buffer));
中断配置经验:
c复制void DMA2_Stream7_IRQHandler(void) {
if(__HAL_DMA_GET_FLAG(&hdma_usart1_tx, DMA_FLAG_TCIF3_7)) {
__HAL_DMA_CLEAR_FLAG(&hdma_usart1_tx, DMA_FLAG_TCIF3_7);
// 用户处理代码
}
}
__attribute__((aligned(4)))修饰缓冲区c复制hdma_usart1_tx.Init.MemBurst = DMA_MBURST_INC4; // 4字节突发
hdma_usart1_tx.Init.PeriphBurst = DMA_PBURST_SINGLE;
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 数据传输不完整 | 缓冲区未持久化 | 添加volatile修饰 |
| 仅首字节正确 | 地址递增未使能 | 检查MemInc/PeriphInc |
| 随机数据错误 | 时钟不同步 | 确认DMA与外设时钟使能顺序 |
| 中断不触发 | 优先级冲突 | 调整NVIC优先级分组 |
调试秘籍:当DMA异常时,首先检查DMA->LISR和DMA->HISR寄存器值,这些状态位会精确指示错误类型(如传输错误、FIFO错误等)。
在ADC采集等场景中,双缓冲机制能实现无缝数据切换:
c复制#define BUF_SIZE 256
__ALIGN_BEGIN uint16_t adc_buf1[BUF_SIZE] __ALIGN_END;
__ALIGN_BEGIN uint16_t adc_buf2[BUF_SIZE] __ALIGN_END;
c复制hadc1.Init.DMAContinuousRequests = ENABLE;
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)adc_buf1, BUF_SIZE);
c复制void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
if(hadc->Instance == ADC1) {
// 切换缓冲区处理
static uint8_t buf_idx = 0;
buf_idx ^= 0x01; // 异或切换0/1
process_data(buf_idx ? adc_buf2 : adc_buf1);
}
}
实测案例:在72MHz STM32F407上,双缓冲DMA采集1Msps ADC数据时,CPU占用率从100%降至不足5%。
PWM生成场景下的配置示例:
c复制// TIM1 CH1 DMA触发配置
hdma_tim1_ch1.Instance = DMA2_Stream1;
hdma_tim1_ch1.Init.Channel = DMA_CHANNEL_6;
hdma_tim1_ch1.Init.Direction = DMA_MEMORY_TO_PERIPH;
hdma_tim1_ch1.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_tim1_ch1.Init.MemInc = DMA_MINC_ENABLE;
hdma_tim1_ch1.Init.Mode = DMA_CIRCULAR; // 循环模式关键
HAL_DMA_Init(&hdma_tim1_ch1);
__HAL_TIM_ENABLE_DMA(&htim1, TIM_DMA_CC1);
HAL_TIM_PWM_Start(&htim1, TIM_CHANNEL_1);
c复制hspi1.Init.NSS = SPI_NSS_SOFT; // 必须软件控制片选
c复制hspi1.Init.CRCCalculation = SPI_CRCCALCULATION_DISABLE;
HAL_SPI_Transmit_DMA(&hspi1, tx_buf, len/2)经过实际测试,在SPI 42MHz时钟下,DMA传输比中断方式效率提升约15倍。