STM32H7 使用 MDMA 与 DMA 双缓冲实现 ADC 连续采样零 CPU 干预的时序对齐技巧

引言

在工业自动化、音频处理和高速数据采集等场景中,ADC 连续采样要求极高的吞吐量和低延迟。STM32H7 系列内置了强大的 MDMA(Master DMA)和标准 DMA 控制器,通过巧妙组合,可以实现 ADC 数据从外设到内存的全自动搬运,全程无需 CPU 干预。然而,多 DMA 协同工作时,时序对齐成为关键难点。本文将分享一种基于双缓冲的时序对齐方案,确保数据连续性和一致性。

原理剖析

DMA 与 MDMA 的分工

  • DMA1/DMA2:负责外设(如 ADC)到内存(SRAM)的数据传输,支持循环模式,可配置双缓冲。
  • MDMA:作为高级 DMA,支持内存到内存、外设到内存等传输,具备链表模式和 64 位带宽,可处理大块数据搬运。

双缓冲机制

双缓冲(Double Buffering)利用两个内存缓冲区交替接收数据。当 DMA 正在填充缓冲区 A 时,CPU 或 MDMA 可处理缓冲区 B 的数据,从而避免数据覆盖。在 STM32H7 中,DMA 的双缓冲模式通过 DMA_SxCR 寄存器的 DBM 位启用,并利用 CT 位指示当前使用的缓冲区。

时序对齐的核心思想

ADC 连续采样产生固定速率的数据流。DMA 以循环模式将数据写入缓冲区,当缓冲区满时触发传输完成中断。此时,MDMA 被触发,将已满的缓冲区数据搬运至更大的存储区(如外部 SDRAM)或进行预处理。关键在于:

  • 缓冲区大小与采样率匹配:确保 DMA 填充一个缓冲区的时间足够 MDMA 完成搬运,避免数据丢失。
  • 中断优先级与触发时机:利用 DMA 的传输完成中断(TC)精确触发 MDMA,保证数据在缓冲区切换瞬间被搬走。

配置步骤

1. 硬件资源

  • STM32H743 开发板
  • ADC1 通道(如 PA3)
  • 内部 SRAM(缓冲区)和外部 SDRAM(可选)

2. 初始化 ADC

配置 ADC 为连续转换模式,采样率设为 1MHz,数据宽度 16 位。使用 HAL 库简化配置:

ADC_HandleTypeDef hadc1;

void ADC_Init(void) {
    hadc1.Instance = ADC1;
    hadc1.Init.ClockPrescaler = ADC_CLOCK_SYNC_PCLK_DIV4;
    hadc1.Init.Resolution = ADC_RESOLUTION_16B;
    hadc1.Init.ContinuousConvMode = ENABLE;
    hadc1.Init.DiscontinuousConvMode = DISABLE;
    hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
    hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
    hadc1.Init.NbrOfConversion = 1;
    HAL_ADC_Init(&hadc1);
}

3. 配置 DMA 双缓冲

使用 DMA2 Stream0,配置为循环模式,双缓冲,数据宽度半字(16 位)。定义两个缓冲区:

#define BUFFER_SIZE 1024
uint16_t bufferA[BUFFER_SIZE];
uint16_t bufferB[BUFFER_SIZE];

void DMA_Init(void) {
    __HAL_RCC_DMA2_CLK_ENABLE();
    hdma_adc.Instance = DMA2_Stream0;
    hdma_adc.Init.Request = DMA_REQUEST_ADC1;
    hdma_adc.Init.Direction = DMA_PERIPH_TO_MEMORY;
    hdma_adc.Init.PeriphInc = DMA_PINC_DISABLE;
    hdma_adc.Init.MemInc = DMA_MINC_ENABLE;
    hdma_adc.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
    hdma_adc.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
    hdma_adc.Init.Mode = DMA_CIRCULAR;
    hdma_adc.Init.Priority = DMA_PRIORITY_HIGH;
    hdma_adc.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
    HAL_DMA_Init(&hdma_adc);

    // 关联 DMA 到 ADC
    __HAL_LINKDMA(&hadc1, DMA_Handle, hdma_adc);

    // 配置双缓冲,先使用 bufferA
    HAL_DMAEx_MultiBufferStart(&hdma_adc, (uint32_t)&ADC1->DR, (uint32_t)bufferA, (uint32_t)bufferB, BUFFER_SIZE);
}

4. 配置 MDMA

MDMA 用于将已满的缓冲区搬运到目标区域(例如另一块内存或 SDRAM)。这里以搬运到 destBuffer 为例,并开启传输完成中断:

MDMA_HandleTypeDef hmdma;

void MDMA_Init(void) {
    __HAL_RCC_MDMA_CLK_ENABLE();
    hmdma.Instance = MDMA_Channel0;
    hmdma.Init.Request = MDMA_REQUEST_DMA2_Stream0_TC;
    hmdma.Init.TransferTriggerMode = MDMA_BLOCK_TRANSFER;
    hmdma.Init.Priority = MDMA_PRIORITY_HIGH;
    hmdma.Init.Endianness = MDMA_LITTLE_ENDIANNESS;
    hmdma.Init.SourceInc = MDMA_SRC_INC_WORD;
    hmdma.Init.DestinationInc = MDMA_DEST_INC_WORD;
    hmdma.Init.SourceDataSize = MDMA_SRC_DATASIZE_HALFWORD;
    hmdma.Init.DestDataSize = MDMA_DEST_DATASIZE_HALFWORD;
    hmdma.Init.DataAlignment = MDMA_DATAALIGN_PACKENABLE;
    hmdma.Init.BufferTransferLength = BUFFER_SIZE;
    HAL_MDMA_Init(&hmdma);

    // 配置 MDMA 目标地址,源地址在中断中动态设置
    HAL_MDMA_Start(&hmdma, (uint32_t)bufferA, (uint32_t)destBuffer, BUFFER_SIZE);
}

5. 中断处理与时序对齐

关键在于 DMA 的传输完成中断中,根据当前使用的缓冲区(通过 hdma_adc.Instance->CR & DMA_SxCR_CT 判断)来设置 MDMA 的源地址,并启动 MDMA。同时,确保 MDMA 在下一个 DMA 缓冲区切换前完成搬运。

void DMA2_Stream0_IRQHandler(void) {
    HAL_DMA_IRQHandler(&hdma_adc);
}

void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef* hadc) {
    // 判断当前 DMA 使用的缓冲区
    uint32_t currentBuffer = (hdma_adc.Instance->CR & DMA_SxCR_CT) ? (uint32_t)bufferB : (uint32_t)bufferA;
    // 设置 MDMA 源地址为当前已满的缓冲区
    HAL_MDMA_Start(&hmdma, currentBuffer, (uint32_t)destBuffer, BUFFER_SIZE);
    // 可选:启动 MDMA 中断,以便处理搬运完成后的数据
}

注意:HAL_ADC_ConvCpltCallback 在 DMA 传输完成一半或全部时调用(取决于 DMA_IT_HTDMA_IT_TC 配置)。为了时序对齐,应使用传输完成中断(TC),并确保 MDMA 优先级高于 DMA,以便立即响应。

完整代码示例

以下是一个简化但完整的初始化序列,整合了上述步骤:

int main(void) {
    HAL_Init();
    SystemClock_Config();
    ADC_Init();
    DMA_Init();
    MDMA_Init();

    // 启动 ADC 并开启 DMA 传输
    HAL_ADC_Start_DMA(&hadc1, (uint32_t*)bufferA, BUFFER_SIZE);

    while (1) {
        // 主循环无需干预,数据自动搬运
    }
}

注意事项

  • 缓冲区大小与采样率匹配:若采样率过高,MDMA 搬运时间可能超过 DMA 填充时间,导致数据覆盖。建议通过计算或实测调整缓冲区大小。
  • 中断优先级:MDMA 中断应设置为最高优先级,确保在 DMA 切换瞬间抢占处理。
  • 内存对齐:MDMA 要求源和目标地址按数据宽度对齐,建议使用 __attribute__((aligned(4))) 声明缓冲区。
  • 调试技巧:使用逻辑分析仪或调试器观察缓冲区切换时序,验证是否出现数据撕裂。
  • 功耗考虑:零 CPU 干预意味着 CPU 可进入低功耗模式,但需确保 MDMA 和 DMA 时钟保持开启。

结语

通过 MDMA 与 DMA 双缓冲的巧妙配合,STM32H7 实现了 ADC 连续采样的零 CPU 干预,极大提升了系统吞吐量。本文的时序对齐技巧确保了数据完整性,为高速数据采集系统提供了可靠方案。开发者可根据实际需求扩展至多通道或更复杂的数据处理流程。