STM32H7 双缓冲零干预:MDMA 与 DMA 协同实现 ADC 连续采样的时序对齐技巧
👁 2 阅读 · 2026-08-27 · 嵌入式
在高速数据采集系统中,CPU 干预往往成为性能瓶颈。STM32H7 凭借其强大的 MDMA 和 DMA 控制器,可实现 ADC 连续采样下的零 CPU 干预。本文将深入剖析 MDMA 与 DMA 双缓冲的协同机制,重点讲解时序对齐技巧,包括环形缓冲区设计、传输完成中断的精准触发,以及如何避免数据撕裂。通过完整代码示例,助你掌握这一高级嵌入式技术,提升系统实时性。
# STM32H7 使用 MDMA 与 DMA 双缓冲实现 ADC 连续采样零 CPU 干预的时序对齐技巧
## 引言
在工业自动化、音频处理和高速数据采集等场景中,ADC 连续采样要求极高的吞吐量和低延迟。STM32H7 系列内置了强大的 MDMA(Master DMA)和标准 DMA 控制器,通过巧妙组合,可以实现 ADC 数据从外设到内存的全自动搬运,全程无需 CPU 干预。然而,多 DMA 协同工作时,时序对齐成为关键难点。本文将分享一种基于双缓冲的时序对齐方案,确保数据连续性和一致性。
## 原理剖析
### DMA 与 MDMA 的分工
- **DMA1/DMA2**:负责外设(如 ADC)到内存(SRAM)的数据传输,支持循环模式,可配置双缓冲。
- **MDMA**:作为高级 DMA,支持内存到内存、外设到内存等传输,具备链表模式和 64 位带宽,可处理大块数据搬运。
### 双缓冲机制
双缓冲(Double Buffering)利用两个内存缓冲区交替接收数据。当 DMA 正在填充缓冲区 A 时,CPU 或 MDMA 可处理缓冲区 B 的数据,从而避免数据覆盖。在 STM32H7 中,DMA 的双缓冲模式通过 `DMA_SxCR` 寄存器的 `DBM` 位启用,并利用 `CT` 位指示当前使用的缓冲区。
### 时序对齐的核心思想
ADC 连续采样产生固定速率的数据流。DMA 以循环模式将数据写入缓冲区,当缓冲区满时触发传输完成中断。此时,MDMA 被触发,将已满的缓冲区数据搬运至更大的存储区(如外部 SDRAM)或进行预处理。关键在于:
- **缓冲区大小与采样率匹配**:确保 DMA 填充一个缓冲区的时间足够 MDMA 完成搬运,避免数据丢失。
- **中断优先级与触发时机**:利用 DMA 的传输完成中断(TC)精确触发 MDMA,保证数据在缓冲区切换瞬间被搬走。
## 配置步骤
### 1. 硬件资源
- STM32H743 开发板
- ADC1 通道(如 PA3)
- 内部 SRAM(缓冲区)和外部 SDRAM(可选)
### 2. 初始化 ADC
配置 ADC 为连续转换模式,采样率设为 1MHz,数据宽度 16 位。使用 HAL 库简化配置:
```c
ADC_HandleTypeDef hadc1;
void ADC_Init(void) {
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
hadc1.Init.Resolution = ADC_RESOLUTION_16B;
hadc1.Init.ContinuousConvMode = ENABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
HAL_ADC_Init(&hadc1);
}
```
### 3. 配置 DMA 双缓冲
使用 DMA2 Stream0,配置为循环模式,双缓冲,数据宽度半字(16 位)。定义两个缓冲区:
```c
#define BUFFER_SIZE 1024
uint16_t bufferA[BUFFER_SIZE];
uint16_t bufferB[BUFFER_SIZE];
void DMA_Init(void) {
__HAL_RCC_DMA2_CLK_ENABLE();
hdma_adc.Instance = DMA2_Stream0;
hdma_adc.Init.Request = DMA_REQUEST_ADC1;
hdma_adc.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_adc.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_adc.Init.MemInc = DMA_MINC_ENABLE;
hdma_adc.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_adc.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_adc.Init.Mode = DMA_CIRCULAR;
hdma_adc.Init.Priority = DMA_PRIORITY_HIGH;
hdma_adc.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
HAL_DMA_Init(&hdma_adc);
// 关联 DMA 到 ADC
__HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc);
// 配置双缓冲,先使用 bufferA
HAL_DMAEx_MultiBufferStart(&hdma_adc, (uint32_t)&ADC1->DR, (uint32_t)bufferA, (uint32_t)bufferB, BUFFER_SIZE);
}
```
### 4. 配置 MDMA
MDMA 用于将已满的缓冲区搬运到目标区域(例如另一块内存或 SDRAM)。这里以搬运到 `destBuffer` 为例,并开启传输完成中断:
```c
MDMA_HandleTypeDef hmdma;
void MDMA_Init(void) {
__HAL_RCC_MDMA_CLK_ENABLE();
hmdma.Instance = MDMA_Channel0;
hmdma.Init.Request = MDMA_REQUEST_DMA2_Stream0_TC;
hmdma.Init.TransferTriggerMode = MDMA_BLOCK_TRANSFER;
hmdma.Init.Priority = MDMA_PRIORITY_HIGH;
hmdma.Init.Endianness = MDMA_LITTLE_ENDIANNESS;
hmdma.Init.SourceInc = MDMA_SRC_INC_WORD;
hmdma.Init.DestinationInc = MDMA_DEST_INC_WORD;
hmdma.Init.SourceDataSize = MDMA_SRC_DATASIZE_HALFWORD;
hmdma.Init.DestDataSize = MDMA_DEST_DATASIZE_HALFWORD;
hmdma.Init.DataAlignment = MDMA_DATAALIGN_PACKENABLE;
hmdma.Init.BufferTransferLength = BUFFER_SIZE;
HAL_MDMA_Init(&hmdma);
// 配置 MDMA 目标地址,源地址在中断中动态设置
HAL_MDMA_Start(&hmdma, (uint32_t)bufferA, (uint32_t)destBuffer, BUFFER_SIZE);
}
```
### 5. 中断处理与时序对齐
关键在于 DMA 的传输完成中断中,根据当前使用的缓冲区(通过 `hdma_adc.Instance->CR & DMA_SxCR_CT` 判断)来设置 MDMA 的源地址,并启动 MDMA。同时,确保 MDMA 在下一个 DMA 缓冲区切换前完成搬运。
```c
void DMA2_Stream0_IRQHandler(void) {
HAL_DMA_IRQHandler(&hdma_adc);
}
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
// 判断当前 DMA 使用的缓冲区
uint32_t currentBuffer = (hdma_adc.Instance->CR & DMA_SxCR_CT) ? (uint32_t)bufferB : (uint32_t)bufferA;
// 设置 MDMA 源地址为当前已满的缓冲区
HAL_MDMA_Start(&hmdma, currentBuffer, (uint32_t)destBuffer, BUFFER_SIZE);
// 可选:启动 MDMA 中断,以便处理搬运完成后的数据
}
```
注意:`HAL_ADC_ConvCpltCallback` 在 DMA 传输完成一半或全部时调用(取决于 `DMA_IT_HT` 和 `DMA_IT_TC` 配置)。为了时序对齐,应使用传输完成中断(TC),并确保 MDMA 优先级高于 DMA,以便立即响应。
## 完整代码示例
以下是一个简化但完整的初始化序列,整合了上述步骤:
```c
int main(void) {
HAL_Init();
SystemClock_Config();
ADC_Init();
DMA_Init();
MDMA_Init();
// 启动 ADC 并开启 DMA 传输
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)bufferA, BUFFER_SIZE);
while (1) {
// 主循环无需干预,数据自动搬运
}
}
```
## 注意事项
- **缓冲区大小与采样率匹配**:若采样率过高,MDMA 搬运时间可能超过 DMA 填充时间,导致数据覆盖。建议通过计算或实测调整缓冲区大小。
- **中断优先级**:MDMA 中断应设置为最高优先级,确保在 DMA 切换瞬间抢占处理。
- **内存对齐**:MDMA 要求源和目标地址按数据宽度对齐,建议使用 `__attribute__((aligned(4)))` 声明缓冲区。
- **调试技巧**:使用逻辑分析仪或调试器观察缓冲区切换时序,验证是否出现数据撕裂。
- **功耗考虑**:零 CPU 干预意味着 CPU 可进入低功耗模式,但需确保 MDMA 和 DMA 时钟保持开启。
## 结语
通过 MDMA 与 DMA 双缓冲的巧妙配合,STM32H7 实现了 ADC 连续采样的零 CPU 干预,极大提升了系统吞吐量。本文的时序对齐技巧确保了数据完整性,为高速数据采集系统提供了可靠方案。开发者可根据实际需求扩展至多通道或更复杂的数据处理流程。