STM32H7 零CPU干预:MDMA与DMA双缓冲协同实现ADC连续采样的时序设计
👁 1 阅读 · 2026-08-27 · 嵌入式
在高速数据采集系统中,CPU干预常成为性能瓶颈。本文深入探讨STM32H7系列利用MDMA(存储器到存储器DMA)与DMA双缓冲机制,实现ADC连续采样全程零CPU干预的时序设计。通过合理配置DMA双缓冲乒乓切换与MDMA的自动搬运,数据可无缝流转至大容量RAM,显著降低中断负载。文章涵盖原理剖析、寄存器级配置步骤、完整代码示例及关键注意事项,助你打造高效嵌入式采集系统。
# STM32H7 零CPU干预:MDMA与DMA双缓冲协同实现ADC连续采样的时序设计
## 引言
在工业自动化、音频处理或振动监测等场景,ADC需要以MHz级速率连续采样,传统中断方式会频繁打断CPU,导致系统响应迟缓。STM32H7系列凭借其强大的MDMA(Master DMA)和灵活的双缓冲DMA机制,可构建一条从ADC外设到内存的“高速公路”,实现数据采集零CPU干预。本文将带你一步步设计并实现这一方案。
## 原理剖析
### 1. DMA双缓冲(Double Buffer)
DMA双缓冲模式(Double Buffer Mode)允许DMA在两个内存缓冲区(Buffer A和Buffer B)之间交替传输数据。当DMA正在填充Buffer A时,CPU或MDMA可以安全处理Buffer B中的数据,反之亦然。这种乒乓机制避免了数据覆盖和访问冲突,是连续采样的基石。
在STM32H7中,DMA1/2支持双缓冲,但每个DMA通道仅能管理一个外设请求。对于ADC多通道或高速率,单DMA可能力不从心,此时MDMA登场。
### 2. MDMA(Master DMA)
MDMA是STM32H7新增的高性能DMA控制器,支持存储器到存储器(M2M)传输,可配置为链接模式(Linked List)。它能在DMA完成一个缓冲区填充后,自动将数据搬运到另一块更大的内存区域(如SRAM或SDRAM),而无需CPU介入。MDMA的突发传输和16/32/64位数据宽度,使其非常适合大数据块搬运。
### 3. 协同工作流程
- **阶段1**:ADC连续采样,触发DMA1通道,数据写入Buffer A(双缓冲)。
- **阶段2**:当Buffer A填满,DMA1产生传输完成中断(但可关闭以省CPU),同时自动切换到Buffer B继续接收。
- **阶段3**:MDMA检测到DMA1的完成事件(通过硬件信号或中断),启动M2M传输,将Buffer A的数据搬运至大容量存储区(如SRAM1或外部SDRAM)。
- **阶段4**:MDMA传输完成后,可触发中断通知CPU处理(可选),或继续等待下一轮。
通过合理配置,CPU仅在需要处理数据时被唤醒,而采样和搬运全程自动进行。
## 硬件与软件准备
- **硬件**:STM32H743 Nucleo或自定义板,ADC输入信号源。
- **软件**:STM32CubeIDE(或Keil),HAL库。
- **关键外设**:ADC1(12位,采样率1MHz),DMA1(双缓冲),MDMA(M2M)。
## 配置步骤
### 1. 配置ADC
使用CubeMX或直接寄存器配置:
- 开启ADC1,选择扫描模式(多通道)或单通道。
- 设置采样时间,确保采样率符合需求。
- 使能DMA连续请求(DMA Continuous Requests)。
### 2. 配置DMA1双缓冲
- 选择DMA1通道,关联ADC1。
- 设置数据宽度(如半字),方向为外设到内存。
- 启用双缓冲模式,指定两个缓冲区地址(需对齐)。
- 关闭DMA中断(或仅开启错误中断),避免CPU负担。
### 3. 配置MDMA
- 选择MDMA通道,模式设为M2M。
- 设置源地址为DMA缓冲区(动态切换),目的地址为大存储区。
- 配置传输大小(与DMA缓冲区大小一致)。
- 启用链接模式,并设置链接列表(可选,用于多缓冲循环)。
- 触发方式:选择硬件触发(如DMA1完成信号)或软件触发。
### 4. 编写初始化代码
以下为关键代码片段(基于HAL库):
```c
// 缓冲区定义(需对齐到32字节)
#define BUFFER_SIZE 1024
uint16_t dma_buffer_a[BUFFER_SIZE] __attribute__((aligned(32)));
uint16_t dma_buffer_b[BUFFER_SIZE] __attribute__((aligned(32)));
uint16_t mdma_dest[2 * BUFFER_SIZE] __attribute__((aligned(32)));
// ADC初始化
void ADC_Init(void) {
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ContinuousConvMode = ENABLE;
hadc1.Init.DMAContinuousRequests = ENABLE;
HAL_ADC_Init(&hadc1);
}
// DMA双缓冲配置
void DMA_Init(void) {
hdma_dac.Instance = DMA1_Stream1;
hdma_dac.Init.Request = DMA_REQUEST_ADC1;
hdma_dac.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_dac.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_dac.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_dac.Init.Mode = DMA_CIRCULAR; // 循环模式配合双缓冲
hdma_dac.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
HAL_DMA_Init(&hdma_dac);
// 配置双缓冲
HAL_DMAEx_MultiBufferStart(&hdma_dac, (uint32_t)&hadc1.Instance->DR, (uint32_t)dma_buffer_a, (uint32_t)dma_buffer_b, BUFFER_SIZE);
// 注意:此处使用MultiBufferStart,但需确保DMA模式为循环
}
// MDMA配置
void MDMA_Init(void) {
hmdma_mdma.Instance = MDMA_Channel0;
hmdma_mdma.Init.Request = MDMA_REQUEST_SW; // 软件触发,或硬件触发
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_HALFWORD;
hmdma_mdma.Init.DestInc = MDMA_DEST_INC_HALFWORD;
hmdma_mdma.Init.SourceDataSize = MDMA_SRC_DATASIZE_HALFWORD;
hmdma_mdma.Init.DestDataSize = MDMA_DEST_DATASIZE_HALFWORD;
hmdma_mdma.Init.DataAlignment = MDMA_DATAALIGN_PACKEN;
hmdma_mdma.Init.BufferTransferLength = BUFFER_SIZE;
HAL_MDMA_Init(&hmdma_mdma);
// 设置源地址为dma_buffer_a,目的为mdma_dest
HAL_MDMA_Start(&hmdma_mdma, (uint32_t)dma_buffer_a, (uint32_t)mdma_dest, BUFFER_SIZE);
}
// 启动采集
void Start_Acquisition(void) {
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)dma_buffer_a, BUFFER_SIZE); // 启动ADC+DMA
// 注意:HAL_ADC_Start_DMA内部会配置DMA,但双缓冲需额外配置
}
```
**注意**:上述代码中,HAL_ADC_Start_DMA通常用于单缓冲,双缓冲需使用HAL_DMAEx_MultiBufferStart并确保ADC的DMA请求已使能。实际应用中,建议直接使用寄存器操作或参考HAL库的DMA双缓冲示例。
### 5. 时序设计要点
- **缓冲区大小**:需保证DMA填充一个缓冲区的时间大于MDMA搬运该缓冲区的时间,否则数据会丢失。例如,ADC采样率1MHz,每个采样2字节,缓冲区1024字节,填充时间约1ms;MDMA搬运1024字节在STM32H7上约需几十微秒,远小于1ms,故安全。
- **触发同步**:MDMA的触发可配置为DMA1的传输完成事件(通过硬件互连),但STM32H7的MDMA触发源有限,通常使用软件触发或定时器触发。更可靠的方法是:在DMA双缓冲切换时,利用DMA的传输完成中断(即使关闭,也可通过状态标志查询)来启动MDMA,但这样会引入少量CPU开销。为实现完全零干预,可配置MDMA的链接模式,使其自动循环搬运两个缓冲区。
**链接模式示例**:
```c
// 定义链接列表项
MDMA_LinkNodeTypeDef link_node1, link_node2;
// 初始化节点1:搬运dma_buffer_a到mdma_dest
link_node1.SourceAddress = (uint32_t)dma_buffer_a;
link_node1.DestAddress = (uint32_t)mdma_dest;
link_node1.BufferLength = BUFFER_SIZE;
link_node1.NextNode = &link_node2;
// 初始化节点2:搬运dma_buffer_b到mdma_dest+BUFFER_SIZE
link_node2.SourceAddress = (uint32_t)dma_buffer_b;
link_node2.DestAddress = (uint32_t)(mdma_dest + BUFFER_SIZE);
link_node2.BufferLength = BUFFER_SIZE;
link_node2.NextNode = &link_node1; // 循环
// 配置MDMA使用链接模式
HAL_MDMA_Start_IT(&hmdma_mdma, (uint32_t)&link_node1, NULL, 0); // 启动链接列表
```
这样,MDMA会在DMA切换缓冲区时自动搬运对应数据,无需CPU干预。
## 完整代码示例
以下为整合后的初始化与启动代码(简化版):
```c
// 全局句柄
ADC_HandleTypeDef hadc1;
DMA_HandleTypeDef hdma_dac;
MDMA_HandleTypeDef hmdma_mdma;
void MX_ADC1_Init(void) {
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.ScanConvMode = DISABLE;
hadc1.Init.ContinuousConvMode = ENABLE;
hadc1.Init.NbrOfConversion = 1;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.DMAContinuousRequests = ENABLE;
HAL_ADC_Init(&hadc1);
}
void MX_DMA_Init(void) {
__HAL_RCC_DMA1_CLK_ENABLE();
hdma_dac.Instance = DMA1_Stream1;
hdma_dac.Init.Request = DMA_REQUEST_ADC1;
hdma_dac.Init.Direction = DMA_PERIPH_TO_MEMORY;
hdma_dac.Init.PeriphInc = DMA_PINC_DISABLE;
hdma_dac.Init.MemInc = DMA_MINC_ENABLE;
hdma_dac.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
hdma_dac.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
hdma_dac.Init.Mode = DMA_CIRCULAR;
hdma_dac.Init.Priority = DMA_PRIORITY_HIGH;
HAL_DMA_Init(&hdma_dac);
__HAL_LINKDMA(&hadc1, DMA_Handle, hdma_dac);
}
void MX_MDMA_Init(void) {
__HAL_RCC_MDMA_CLK_ENABLE();
hmdma_mdma.Instance = MDMA_Channel0;
hmdma_mdma.Init.Request = MDMA_REQUEST_SW;
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_HALFWORD;
hmdma_mdma.Init.DestInc = MDMA_DEST_INC_HALFWORD;
hmdma_mdma.Init.SourceDataSize = MDMA_SRC_DATASIZE_HALFWORD;
hmdma_mdma.Init.DestDataSize = MDMA_DEST_DATASIZE_HALFWORD;
hmdma_mdma.Init.DataAlignment = MDMA_DATAALIGN_PACKEN;
hmdma_mdma.Init.BufferTransferLength = BUFFER_SIZE;
HAL_MDMA_Init(&hmdma_mdma);
}
void Start_ZeroCPU_ADC(void) {
// 配置双缓冲
HAL_DMAEx_MultiBufferStart(&hdma_dac, (uint32_t)&hadc1.Instance->DR, (uint32_t)dma_buffer_a, (uint32_t)dma_buffer_b, BUFFER_SIZE);
// 启动MDMA链接模式(假设已配置好链接节点)
HAL_MDMA_Start_IT(&hmdma_mdma, (uint32_t)&link_node1, NULL, 0);
// 启动ADC
HAL_ADC_Start_DMA(&hadc1, (uint32_t*)dma_buffer_a, BUFFER_SIZE);
}
```
## 注意事项
- **内存对齐**:DMA和MDMA要求缓冲区地址对齐到数据宽度(半字对齐,建议32字节对齐以优化性能)。
- **缓存一致性**:如果使用带缓存的内存区域(如SRAM),需在CPU访问前调用`SCB_InvalidateDCache`,或在MDMA传输完成后使缓存无效。
- **时序预算**:务必计算DMA填充时间与MDMA搬运时间,确保前者大于后者,否则数据覆盖。可调整缓冲区大小或MDMA突发长度。
- **中断处理**:虽然零CPU干预,但建议保留MDMA完成中断用于数据就绪通知,以便CPU及时处理。
- **链接列表内存**:链接节点必须常驻内存,且不可被优化掉。
- **调试技巧**:使用逻辑分析仪或DMA的传输计数寄存器验证时序。
## 结语
通过STM32H7的MDMA与DMA双缓冲协同,我们成功实现了ADC连续采样的零CPU干预,释放了CPU资源用于更复杂的处理。该方案可扩展至多通道、多ADC同步等场景,是高性能嵌入式系统的利器。希望本文能助你在项目中游刃有余。