STM32H7 MDMA与定时器联动:实现外设到内存零拷贝传输的配置细节
👁 2 阅读 · 2026-08-27 · 嵌入式
在STM32H7高性能系列中,MDMA(Master Direct Memory Access)配合定时器触发,可实现外设数据到内存的零拷贝传输,大幅降低CPU负载并提升实时性。本文深入解析MDMA与定时器联动的原理,详细讲解从时钟配置、MDMA通道设置到定时器触发同步的完整步骤,并给出基于HAL库的实战代码示例,帮助开发者规避常见配置陷阱,实现高效、可靠的数据搬运。
# STM32H7 MDMA与定时器联动:实现外设到内存零拷贝传输的配置细节
## 引言
在高速数据采集或通信系统中,传统DMA(如DMA1/DMA2)受限于传输长度和通道数量,且无法直接访问所有存储器。STM32H7引入的MDMA(Master DMA)支持8/16/32位数据宽度、突发传输、链表模式,并能通过硬件触发(如定时器事件)启动传输,实现真正的外设到内存零拷贝。本文以ADC连续采样为例,演示如何配置MDMA与定时器联动,将外设数据直接搬运到SRAM,全程无需CPU干预。
## 原理概述
### MDMA与DMA的区别
- **DMA**:仅能访问部分总线,传输长度受限于FIFO(通常≤65535字节),且触发源有限。
- **MDMA**:作为系统总线主设备,可访问所有存储器(包括DTCM、AXI SRAM),支持无限传输长度,支持链表和循环模式,触发源可来自软件、外部事件或定时器。
### 定时器触发机制
STM32H7的定时器(如TIM1/TIM2)可产生更新事件或比较事件,通过硬件连接(如DMA请求映射)直接触发MDMA。MDMA的请求信号由定时器事件产生,无需CPU配置中断,实现周期性自动搬运。
### 零拷贝实现
数据从外设(如ADC数据寄存器)通过MDMA直接写入内存缓冲区,不经过CPU或中间缓存,且传输完成后可自动循环,保证数据连续性。
## 硬件准备
- STM32H743 Nucleo-144板(或类似)
- 外部信号源(如信号发生器)连接到ADC输入引脚(如PA3)
- 调试器(ST-Link)
## 软件配置步骤
### 1. 时钟配置
使用CubeMX或直接寄存器配置,确保MDMA和定时器时钟使能。在HAL中,MDMA时钟由AHB3总线提供,定时器由APB1/APB2提供。
```c
__HAL_RCC_MDMA_CLK_ENABLE();
__HAL_RCC_TIM2_CLK_ENABLE();
__HAL_RCC_ADC12_CLK_ENABLE();
```
### 2. MDMA配置
MDMA通道配置需指定源地址(外设寄存器)、目标地址(内存缓冲区)、数据宽度、突发大小等。关键参数:
- `MDMA_InitTypeDef`:设置传输方向(外设到内存)、数据宽度(如32位)、突发大小(如4)
- 使能循环模式(`MDMA_CIRCULAR_MODE`)以持续传输
```c
MDMA_HandleTypeDef hmdma;
void MDMA_Init(void) {
hmdma.Instance = MDMA_Channel0;
hmdma.Init.Request = MDMA_REQUEST_TIM2_TRIG; // 定时器2触发
hmdma.Init.TransferTriggerMode = MDMA_BUFFER_TRANSFER; // 缓冲传输
hmdma.Init.Priority = MDMA_PRIORITY_HIGH;
hmdma.Init.TransferEventMode = MDMA_TC_INTERRUPT; // 传输完成中断(可选)
hmdma.Init.SourceInc = MDMA_SRC_INC_FIX; // 外设地址固定
hmdma.Init.DestInc = MDMA_DEST_INC_WORD; // 内存地址递增
hmdma.Init.SourceDataSize = MDMA_SRC_DATASIZE_WORD;
hmdma.Init.DestDataSize = MDMA_DEST_DATASIZE_WORD;
hmdma.Init.SourceBurst = MDMA_SOURCE_BURST_4BEATS;
hmdma.Init.DestBurst = MDMA_DEST_BURST_4BEATS;
hmdma.Init.DataAlignment = MDMA_DATAALIGN_PACK;
hmdma.Init.BufferTransferLength = 128; // 每次传输128个字
HAL_MDMA_Init(&hmdma);
}
```
### 3. 定时器配置
定时器需产生周期性的触发事件,频率由采样率决定。配置TIM2为PWM模式或更新事件,并设置预分频和自动重载值。
```c
TIM_HandleTypeDef htim2;
void TIM2_Init(uint32_t freq) {
htim2.Instance = TIM2;
htim2.Init.Prescaler = 100-1; // 假设时钟100MHz,预分频100
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = (1000000 / freq) - 1; // 1MHz时钟下
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
HAL_TIM_Base_Init(&htim2);
// 使能更新事件触发MDMA
TIM_MasterConfigTypeDef sMasterConfig = {0};
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig);
}
```
### 4. 外设(ADC)配置
ADC需配置为连续转换模式,并开启DMA请求(但实际由MDMA触发)。注意:ADC的DMA请求信号需连接到MDMA的请求线,这通过硬件互连矩阵实现,通常无需软件配置。
```c
ADC_HandleTypeDef hadc1;
void ADC1_Init(void) {
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ScanConvMode = DISABLE;
hadc1.Init.ContinuousConvMode = ENABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T2_TRGO; // 定时器2触发
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
HAL_ADC_Init(&hadc1);
// 配置通道
ADC_ChannelConfTypeDef sConfig = {0};
sConfig.Channel = ADC_CHANNEL_3;
sConfig.Rank = ADC_REGULAR_RANK_1;
sConfig.SamplingTime = ADC_SAMPLETIME_8CYCLES_5;
HAL_ADC_ConfigChannel(&hadc1, &sConfig);
}
```
### 5. 内存缓冲区定义
缓冲区需放置在MDMA可访问的内存区域,如AXI SRAM或D2域SRAM。使用`__attribute__((section(".ARM.__at_0x24000000")))`指定地址。
```c
#define BUFFER_SIZE 128
uint32_t adc_buffer[BUFFER_SIZE] __attribute__((section(".ARM.__at_0x24000000")));
```
### 6. 启动传输
初始化完成后,启动定时器和ADC,MDMA自动等待触发。
```c
int main(void) {
HAL_Init();
SystemClock_Config();
MDMA_Init();
TIM2_Init(1000); // 1kHz采样率
ADC1_Init();
HAL_ADC_Start(&hadc1);
HAL_TIM_Base_Start(&htim2);
// 主循环不做任何事,数据自动搬运
while(1) {
// 可处理缓冲区数据,但注意双缓冲避免冲突
}
}
```
## 完整代码示例
以下为整合后的关键代码(省略时钟配置等基础部分):
```c
// main.c
#include "main.h"
MDMA_HandleTypeDef hmdma;
TIM_HandleTypeDef htim2;
ADC_HandleTypeDef hadc1;
#define BUFFER_SIZE 128
uint32_t adc_buffer[BUFFER_SIZE] __attribute__((section(".ARM.__at_0x24000000")));
void Error_Handler(void) { while(1); }
int main(void) {
HAL_Init();
SystemClock_Config();
// 使能时钟
__HAL_RCC_MDMA_CLK_ENABLE();
__HAL_RCC_TIM2_CLK_ENABLE();
__HAL_RCC_ADC12_CLK_ENABLE();
// 初始化MDMA
hmdma.Instance = MDMA_Channel0;
hmdma.Init.Request = MDMA_REQUEST_TIM2_TRIG;
hmdma.Init.TransferTriggerMode = MDMA_BUFFER_TRANSFER;
hmdma.Init.Priority = MDMA_PRIORITY_HIGH;
hmdma.Init.TransferEventMode = MDMA_TC_INTERRUPT;
hmdma.Init.SourceInc = MDMA_SRC_INC_FIX;
hmdma.Init.DestInc = MDMA_DEST_INC_WORD;
hmdma.Init.SourceDataSize = MDMA_SRC_DATASIZE_WORD;
hmdma.Init.DestDataSize = MDMA_DEST_DATASIZE_WORD;
hmdma.Init.SourceBurst = MDMA_SOURCE_BURST_4BEATS;
hmdma.Init.DestBurst = MDMA_DEST_BURST_4BEATS;
hmdma.Init.DataAlignment = MDMA_DATAALIGN_PACK;
hmdma.Init.BufferTransferLength = BUFFER_SIZE;
if (HAL_MDMA_Init(&hmdma) != HAL_OK) Error_Handler();
// 配置TIM2触发
htim2.Instance = TIM2;
htim2.Init.Prescaler = 100-1;
htim2.Init.CounterMode = TIM_COUNTERMODE_UP;
htim2.Init.Period = 999; // 1kHz
htim2.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
if (HAL_TIM_Base_Init(&htim2) != HAL_OK) Error_Handler();
TIM_MasterConfigTypeDef sMasterConfig = {0};
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
HAL_TIMEx_MasterConfigSynchronization(&htim2, &sMasterConfig);
// 配置ADC
hadc1.Instance = ADC1;
hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
hadc1.Init.Resolution = ADC_RESOLUTION_12B;
hadc1.Init.ScanConvMode = DISABLE;
hadc1.Init.ContinuousConvMode = ENABLE;
hadc1.Init.DiscontinuousConvMode = DISABLE;
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T2_TRGO;
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING;
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
hadc1.Init.NbrOfConversion = 1;
if (HAL_ADC_Init(&hadc1) != HAL_OK) Error_Handler();
ADC_ChannelConfTypeDef sConfig = {0};
sConfig.Channel = ADC_CHANNEL_3;
sConfig.Rank = ADC_REGULAR_RANK_1;
sConfig.SamplingTime = ADC_SAMPLETIME_8CYCLES_5;
if (HAL_ADC_ConfigChannel(&hadc1, &sConfig) != HAL_OK) Error_Handler();
// 启动
HAL_ADC_Start(&hadc1);
HAL_TIM_Base_Start(&htim2);
while(1) {
// 主循环可处理数据,但注意缓冲区访问同步
}
}
```
## 注意事项
- **内存一致性**:MDMA与CPU访问同一缓冲区时,需使用`__DSB()`或`SCB_CleanDCache()`等确保缓存一致性,尤其在带Cache的SRAM区域。
- **触发源映射**:不同定时器触发MDMA的请求线不同,需查阅参考手册的互连矩阵,确保`MDMA_REQUEST_TIM2_TRIG`正确。
- **缓冲区大小**:MDMA的`BufferTransferLength`最大为65535,若需更大,使用链表模式。
- **中断处理**:若启用传输完成中断,在回调中需及时处理数据,避免覆盖。建议使用双缓冲。
- **调试技巧**:使用逻辑分析仪或调试器观察MDMA传输是否触发,若未触发,检查定时器是否使能、ADC是否启动。
## 总结
通过MDMA与定时器联动,STM32H7可高效实现外设到内存的零拷贝传输,极大降低CPU占用。本文详细介绍了配置流程和关键代码,开发者可在此基础上扩展至多通道、链表等高级应用。掌握此技术,将显著提升嵌入式系统的数据吞吐能力。