# 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占用。本文详细介绍了配置流程和关键代码,开发者可在此基础上扩展至多通道、链表等高级应用。掌握此技术,将显著提升嵌入式系统的数据吞吐能力。