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