STM32H7 使用 MDMA 与双缓冲实现 ADC 连续采样零 CPU 干预的配置陷阱
👁 2 阅读 · 2026-08-27 · 嵌入式
STM32H7 的 MDMA 与双缓冲机制可让 ADC 连续采样完全脱离 CPU 干预,但配置中暗藏多个陷阱:从 MDMA 通道选择、缓冲区地址对齐到传输结束回调的触发条件,稍有不慎便会导致数据错乱或 DMA 死锁。本文基于实际调试经验,剖析这些陷阱的根源,并给出经过验证的配置步骤与完整代码示例,助你避开雷区,实现高效稳定的零 CPU 数据采集。
# STM32H7 使用 MDMA 与双缓冲实现 ADC 连续采样零 CPU 干预的配置陷阱
## 引言
在高速数据采集系统中,ADC 连续采样往往要求 CPU 零干预,以释放算力给核心算法。STM32H7 系列凭借其强大的 MDMA(Master DMA)和双缓冲(Double Buffer)机制,成为实现这一目标的理想平台。然而,MDMA 的灵活性与复杂性并存,配置不当极易引发数据错乱、传输中断甚至系统死锁。本文将深入剖析配置中的关键陷阱,并提供一套经过验证的解决方案。
## 原理基础
### MDMA 与 DMA 的区别
STM32H7 拥有两种 DMA:传统 DMA(DMA1/DMA2)和 MDMA。MDMA 支持 128 位带宽、4KB 突发传输和链表模式,可访问所有存储器(包括 AXI SRAM),并能与 DMA 级联。在 ADC 连续采样场景中,我们通常使用 DMA1 将 ADC 数据搬运至 SRAM,再由 MDMA 将数据块搬移至最终缓冲区,实现两级流水线。
### 双缓冲机制
双缓冲(Double Buffer)通过两个缓冲区交替使用,避免数据覆盖。在 MDMA 中,可通过配置 `MDMA_CTR` 寄存器的 `DBUF` 位启用双缓冲,并设置两个目标地址。当 MDMA 完成一次传输后,自动切换至另一缓冲区,同时触发中断通知 CPU 处理已满缓冲区。
## 配置陷阱剖析
### 陷阱一:MDMA 通道选择错误
MDMA 有 16 个通道,但并非所有通道都支持双缓冲。查阅参考手册,只有通道 0-7 支持 `DBUF` 功能。若使用通道 8-15,双缓冲配置将被忽略,导致数据覆盖。
**解决方案**:务必选择通道 0-7 之一,并在初始化时检查 `MDMA_CCR` 的 `DBUF` 位是否置位。
### 陷阱二:缓冲区地址对齐要求
MDMA 要求源和目的地址必须按传输宽度对齐。若使用 32 位数据,地址需 4 字节对齐;若使用 128 位突发,需 16 字节对齐。未对齐时,MDMA 可能产生总线错误或数据错位。
**解决方案**:使用 `__attribute__((aligned(32)))` 声明缓冲区,或通过 `memalign` 动态分配。
### 陷阱三:传输结束回调的触发条件
MDMA 的传输完成中断(TCIF)在每次块传输完成后触发,但若未正确配置 `TCIE` 和 `TEIE`,中断可能不产生。此外,双缓冲模式下,中断标志需在回调中手动清除,否则会持续触发。
**解决方案**:在中断服务函数中读取 `MDMA_CISR` 清除标志,并确保 `MDMA_CCR` 的 `TCIE` 置位。
### 陷阱四:与 DMA 级联时的同步问题
当 DMA1 搬运 ADC 数据至中间缓冲区,MDMA 再搬运至最终缓冲区时,需确保 DMA1 完成一帧后 MDMA 才启动。若两者独立运行,可能产生数据竞争。
**解决方案**:使用 DMA1 的传输完成中断来触发 MDMA 启动,或利用 MDMA 的硬件请求信号(如 `MDMA_REQ`)与 DMA1 联动。
## 配置步骤
1. **初始化 ADC**:配置 ADC 为连续扫描模式,使能 DMA 请求。
2. **配置 DMA1**:选择 ADC 的 DMA 通道,设置数据宽度为半字(16 位),循环模式,并指定中间缓冲区。
3. **配置 MDMA**:选择支持双缓冲的通道(如通道 0),设置源地址为中间缓冲区,目的地址为双缓冲区的两个地址,启用 `DBUF`,传输宽度为 32 位,突发大小为 4。
4. **使能中断**:在 MDMA 中断服务函数中处理数据并清除标志。
5. **启动流程**:先使能 MDMA,再使能 DMA1,最后启动 ADC。
## 完整代码示例
以下代码基于 STM32H743,使用 HAL 库,实现 16 位 ADC 连续采样,双缓冲大小为 1024 字节。
```c
#include "stm32h7xx_hal.h"
// 缓冲区定义,32 字节对齐
__attribute__((aligned(32))) uint32_t dma_buf[256]; // 中间缓冲区(DMA1 写入)
__attribute__((aligned(32))) uint32_t mdma_buf[2][256]; // 双缓冲区(MDMA 写入)
volatile uint8_t buf_index = 0; // 当前使用的缓冲区索引
// ADC 句柄和 DMA 句柄
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_adc1;
MDMA_HandleTypeDef hmdma_mdma;
void MDMA_Init(void) {
__HAL_RCC_MDMA_CLK_ENABLE();
hmdma_mdma.Instance = MDMA_Channel0;
hmdma_mdma.Init.Request = MDMA_REQUEST_DMA1_ADC1; // 由 DMA1 触发
hmdma_mdma.Init.TransferTriggerMode = MDMA_BLOCK_TRANSFER; // 块传输
hmdma_mdma.Init.Priority = MDMA_PRIORITY_HIGH;
hmdma_mdma.Init.Endianness = MDMA_LITTLE_ENDIAN;
hmdma_mdma.Init.SourceInc = MDMA_SRC_INC_WORD; // 源地址递增,字
hmdma_mdma.Init.DestinationInc = MDMA_DEST_INC_WORD; // 目的地址递增
hmdma_mdma.Init.SourceDataSize = MDMA_SRC_DATASIZE_WORD; // 32 位
hmdma_mdma.Init.DestDataSize = MDMA_DEST_DATASIZE_WORD;
hmdma_mdma.Init.DataAlignment = MDMA_DATAALIGN_PACK;
hmdma_mdma.Init.BufferTransferLength = 256; // 每次传输 256 字
hmdma_mdma.Init.SourceBurst = MDMA_SOURCE_BURST_4BEATS;
hmdma_mdma.Init.DestBurst = MDMA_DEST_BURST_4BEATS;
hmdma_mdma.Init.SourceBlockAddressOffset = 0;
hmdma_mdma.Init.DestBlockAddressOffset = 0;
hmdma_mdma.Init.DoubleBufferMode = MDMA_DOUBLEBUFFER_MODE_ENABLE; // 启用双缓冲
hmdma_mdma.Init.DoubleBufferBaseAddress = (uint32_t)mdma_buf; // 双缓冲基地址
hmdma_mdma.Init.DoubleBufferCurrentBuffer = 0; // 初始使用缓冲区 0
HAL_MDMA_Init(&hmdma_mdma);
// 配置中断
HAL_NVIC_SetPriority(MDMA_IRQn, 0, 0);
HAL_NVIC_EnableIRQ(MDMA_IRQn);
}
void MDMA_Start(void) {
// 源地址为 DMA1 的中间缓冲区,目的地址为双缓冲基地址
HAL_MDMA_Start(&hmdma_mdma, (uint32_t)dma_buf, (uint32_t)mdma_buf, 256, 1);
}
void MDMA_IRQHandler(void) {
HAL_MDMA_IRQHandler(&hmdma_mdma);
}
void HAL_MDMA_TCIncCallback(MDMA_HandleTypeDef *hmdma) {
// 传输完成,处理当前缓冲区数据
buf_index = hmdma->Init.DoubleBufferCurrentBuffer ^ 1; // 获取已满的缓冲区索引
// 此处可处理 mdma_buf[buf_index] 中的数据
// 注意:需在回调中清除中断标志,HAL 库已自动清除,但若使用非 HAL 需手动
}
// 主函数中初始化并启动
int main(void) {
HAL_Init();
// 配置时钟、ADC、DMA1 等(略)
MDMA_Init();
MDMA_Start();
// 启动 ADC 和 DMA1(略)
while (1) {
// 主循环无需干预数据搬运
}
}
```
## 注意事项
- **缓冲区大小**:MDMA 的 `BufferTransferLength` 必须与 DMA1 的传输长度匹配,否则可能丢失数据或产生溢出。
- **中断优先级**:MDMA 中断优先级应高于其他非关键中断,但低于实时性要求更高的中断(如系统节拍)。
- **调试技巧**:若数据错乱,先检查地址对齐和 `DBUF` 位;若中断不触发,检查 `TCIE` 和 `TEIE` 是否使能。
- **功耗考虑**:零 CPU 干预虽好,但 MDMA 和 DMA 的持续运行会增加功耗,低功耗场景需权衡。
## 结语
STM32H7 的 MDMA 与双缓冲组合为 ADC 连续采样提供了强大的零 CPU 干预方案,但配置陷阱众多。通过理解原理、遵循正确的配置步骤,并注意地址对齐、通道选择等细节,即可稳定实现高速数据采集。希望本文能助你在嵌入式开发中少走弯路。