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提供。
__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)以持续传输
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模式或更新事件,并设置预分频和自动重载值。
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的请求线,这通过硬件互连矩阵实现,通常无需软件配置。
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")))指定地址。
#define BUFFER_SIZE 128
uint32_t adc_buffer[BUFFER_SIZE] __attribute__((section(".ARM.__at_0x24000000")));
6. 启动传输
初始化完成后,启动定时器和ADC,MDMA自动等待触发。
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) {
// 可处理缓冲区数据,但注意双缓冲避免冲突
}
}
完整代码示例
以下为整合后的关键代码(省略时钟配置等基础部分):
// 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占用。本文详细介绍了配置流程和关键代码,开发者可在此基础上扩展至多通道、链表等高级应用。掌握此技术,将显著提升嵌入式系统的数据吞吐能力。