1. 什么是DMA?为什么需要它?
DMA(Direct Memory Access)直接存储器访问,是嵌入式系统中一种高效的数据传输机制。简单来说,它就像是一个专门负责搬数据的"快递员",可以在不打扰CPU的情况下,自动完成内存与外设之间的数据搬运。
想象一下这个场景:你正在用STM32的串口接收大量传感器数据。传统方式下,每收到一个字节,CPU都要停下手中的工作去处理中断,就像每收一个快递都要亲自下楼签收一样低效。而DMA则像雇了个快递柜——数据到达后自动存入指定位置,等积累到一定量再通知CPU处理,解放了CPU的计算资源。
DMA在STM32中的典型应用场景包括:
- ADC采集数据直接存入内存
- 串口大批量数据收发
- SPI/I2C与外部设备通信
- 内存到内存的快速拷贝(如图像处理)
以F1系列为例,STM32最多有2个DMA控制器(DMA2仅大容量型号有),DMA1有7个通道,DMA2有5个通道。每个通道可以绑定到特定外设,比如:
- DMA1通道4对应USART1_TX
- DMA1通道5对应ADC1
- DMA2通道3对应SPI1_RX
2. STM32 DMA硬件架构解析
2.1 DMA控制器工作原理
DMA控制器的核心是一个多路复用的数据传输引擎。当外设准备好数据后,会通过硬件信号线向DMA控制器发起请求(DMA Request)。仲裁器根据优先级决定处理哪个请求,然后DMA控制器就会自动执行数据传输。
关键组件解析:
- 通道仲裁器:处理多个通道的竞争问题
- 软件可设4级优先级(很高/高/中/低)
- 同优先级时通道号小的优先
- 数据寄存器:支持不同位宽转换
- 可处理8/16/32位数据
- 自动处理大小端问题
- 地址发生器:
- 支持地址自动递增
- 外设地址通常固定,内存地址通常递增
2.2 寄存器精要
掌握这几个核心寄存器就能玩转DMA:
| 寄存器 | 功能说明 | 关键位域 |
|---|---|---|
| DMA_ISR | 中断状态寄存器 | TCIFx(传输完成标志) |
| DMA_IFCR | 中断标志清除寄存器 | 写0清除对应中断标志 |
| DMA_CCRx | 通道配置寄存器(最重要!) | 数据传输方向、位宽、模式等 |
| DMA_CNDTRx | 数据量寄存器 | 实时显示剩余传输量 |
| DMA_CPARx | 外设地址寄存器 | 如&USART1->DR |
| DMA_CMARx | 内存地址寄存器 | 如SendBuff数组首地址 |
3. 实战配置:串口DMA发送示例
3.1 CubeMX配置步骤
- 使能USART1的DMA传输功能
- 添加TX方向的DMA通道
- 配置参数:
- Mode: Normal(非循环)
- Priority: Medium
- Memory Increment: Enable
- Peripheral Increment: Disable
- Data Width: Byte
3.2 手动编码实现
// DMA初始化结构体 DMA_InitTypeDef DMA_InitStruct = {0}; // 1. 使能DMA时钟 __HAL_RCC_DMA1_CLK_ENABLE(); // 2. 配置DMA参数 DMA_InitStruct.Direction = DMA_MEMORY_TO_PERIPH; // 内存到外设 DMA_InitStruct.PeriphInc = DMA_PINC_DISABLE; // 外设地址不递增 DMA_InitStruct.MemInc = DMA_MINC_ENABLE; // 内存地址递增 DMA_InitStruct.PeriphDataAlignment = DMA_PDATAALIGN_BYTE; DMA_InitStruct.MemDataAlignment = DMA_MDATAALIGN_BYTE; DMA_InitStruct.Mode = DMA_NORMAL; // 普通模式 DMA_InitStruct.Priority = DMA_PRIORITY_MEDIUM; // 中优先级 HAL_DMA_Init(&hdma_usart1_tx); // 3. 绑定DMA到串口 __HAL_LINKDMA(&huart1, hdmatx, hdma_usart1_tx); // 4. 启动传输 HAL_UART_Transmit_DMA(&huart1, (uint8_t*)SendBuff, BUFF_SIZE);3.3 调试技巧
- 使用
__HAL_DMA_GET_COUNTER()获取剩余传输量 - 检查DMA_FLAG_TCx标志判断传输完成
- 内存地址要对齐(特别是32位传输时)
- 循环模式记得重设CNDTR值
4. 高级应用技巧
4.1 双缓冲技术
在需要连续传输的场景(如音频播放),可以使用双缓冲避免数据冲突:
// 定义双缓冲 uint8_t buffer1[256], buffer2[256]; // 初始化时配置 DMA_InitStruct.Mode = DMA_CIRCULAR; // 循环模式 DMA_InitStruct.Memory0BaseAddr = (uint32_t)buffer1; DMA_InitStruct.Memory1BaseAddr = (uint32_t)buffer2; DMA_InitStruct.MemoryBurst = DMA_MBURST_INC4;4.2 内存到内存传输
某些型号支持内存间DMA传输(如STM32F4),比CPU拷贝快3-5倍:
DMA_InitStruct.Direction = DMA_MEMORY_TO_MEMORY; DMA_InitStruct.PeriphInc = DMA_PINC_ENABLE; DMA_InitStruct.MemInc = DMA_MINC_ENABLE;4.3 中断组合使用
合理利用半传输中断和完成中断:
// 在HAL_DMA_Start_IT()后添加 __HAL_DMA_ENABLE_IT(&hdma, DMA_IT_HT | DMA_IT_TC); // 中断回调函数 void HAL_DMA_XferHalfCpltCallback(DMA_HandleTypeDef *hdma) { // 处理前半段数据 } void HAL_DMA_XferCpltCallback(DMA_HandleTypeDef *hdma) { // 处理后半段数据 }5. 常见问题排查
数据错位:
- 检查Memory/Peripheral数据宽度设置
- 确认地址递增配置正确
传输不启动:
- 确认外设已使能DMA请求
- 检查DMA通道与外设映射关系
只传输一次:
- 循环模式需设置DMA_CIRCULAR
- 检查CNDTR是否自动重载
性能优化:
- 使用突发传输(Burst Mode)
- 合理设置FIFO阈值
- 优先选择支持DMA的外设
实际项目中,我曾遇到SPI DMA传输偶尔丢失数据的问题。后来发现是SPI时钟速度过高导致DMA来不及响应,通过降低时钟频率并启用DMA FIFO后问题解决。这也提醒我们,DMA性能不仅取决于配置,还需要考虑外设特性与总线负载的平衡。