引言

在嵌入式系统中,外设数据(如 ADC 采样值、UART 接收帧)通常需要搬运到内存进行处理。传统方法依赖中断服务函数(ISR)逐次读取数据,但高频中断会严重占用 CPU 时间,尤其在 STM32H7 这类高性能 MCU 上,浪费了宝贵的处理能力。MDMA(Master DMA)是 STM32H7 特有的增强型 DMA,支持内存到内存、外设到内存的突发传输,并可独立于 CPU 运行。本文将介绍如何利用定时器触发 MDMA,实现外设到内存的零中断搬运,让 CPU 专注于核心算法。

原理剖析

MDMA 与常规 DMA 的区别

  • 常规 DMA(如 DMA1/DMA2)主要面向外设与内存之间的传输,但每次传输需要 CPU 配置,且传输完成后通过中断通知 CPU。
  • MDMA 是更高层次的 DMA 控制器,支持内存到内存、外设到内存,具有 16 字节突发传输、双缓冲、链表模式等高级功能,可独立完成复杂传输任务,且不占用 CPU 中断资源。

定时器触发机制

STM32H7 的定时器(如 TIM1/TIM8)可以产生触发输出(TRGO),连接到 MDMA 的请求输入。当定时器计数达到设定值(如更新事件),会生成触发信号,MDMA 收到后自动启动一次传输。这种硬件联动无需 CPU 干预,实现了真正的零中断搬运。

传输流程

  1. 定时器配置为 PWM 模式或更新事件,产生固定频率的触发信号。
  2. MDMA 配置为外设到内存模式,外设地址指向 ADC 数据寄存器,内存地址指向缓冲区。
  3. 每次触发,MDMA 将外设数据搬运到内存,并自动递增内存地址。
  4. 当传输计数完成后,MDMA 可配置为循环模式,持续搬运。

硬件与软件准备

  • 硬件:STM32H743 开发板(如 NUCLEO-H743ZI2),外设使用 ADC1 的通道 0(PA0)模拟输入。
  • 软件:STM32CubeIDE 1.13+,STM32CubeH7 固件库 V1.11+。

配置步骤(基于 CubeMX)

1. 定时器配置

  • 选择 TIM1,时钟源为内部时钟(如 100MHz)。
  • 设置预分频器(PSC)和自动重载寄存器(ARR)以产生 1kHz 触发频率:
    • PSC = 99(100MHz/100 = 1MHz)
    • ARR = 999(1MHz/1000 = 1kHz)
  • 在“Trigger Output (TRGO)”中选择“Update Event”。

2. ADC 配置

  • 启用 ADC1,选择通道 0,采样时间设为 32.5 周期。
  • 将 ADC 设置为“Discontinuous Conversion Mode”或“Single Conversion”,并启用 DMA 请求(但实际由 MDMA 触发,ADC 的 DMA 请求可关闭)。
  • 注意:ADC 的触发源选择“Timer 1 Trigger Out”,确保 ADC 与定时器同步。

3. MDMA 配置

  • 在“Connectivity”中找到 MDMA,添加一个 MDMA 通道。
  • 请求源选择“TIM1_TRIG”,方向为“Peripheral to Memory”。
  • 外设地址:&ADC1->DR,内存地址:自定义缓冲区(如 adc_buffer[256])。
  • 传输大小:半字(16位,ADC 数据为 12 位)。
  • 突发大小:4 或 8 字节,提高效率。
  • 循环模式:启用,以便连续搬运。
  • 传输计数:256(缓冲区大小)。

4. 生成代码

在 CubeMX 中生成初始化代码,然后添加用户逻辑。

完整代码示例

以下代码基于生成的工程,在 main.c 中添加关键部分。

/* 缓冲区定义 */
#define BUFFER_SIZE 256
uint16_t adc_buffer[BUFFER_SIZE];

/* 用户代码区(在 main 函数中) */
int main(void)
{
  HAL_Init();
  SystemClock_Config();
  MX_GPIO_Init();
  MX_ADC1_Init();
  MX_TIM1_Init();
  MX_MDMA_Init();

  /* 启动 ADC 和定时器 */
  HAL_ADC_Start(&hadc1);
  HAL_TIM_Base_Start(&htim1);

  /* 启动 MDMA 传输,循环模式 */
  HAL_MDMA_Start(&hmdma_mdma_channel0, (uint32_t)&ADC1->DR, (uint32_t)adc_buffer, BUFFER_SIZE);

  while (1)
  {
    // 主循环无需处理数据搬运,可执行其他任务
    // 当需要处理数据时,检查缓冲区是否更新(例如通过标志)
    // 此处简单演示:每 100ms 打印缓冲区前 10 个值
    HAL_Delay(100);
    for (int i = 0; i < 10; i++) {
      printf("%d ", adc_buffer[i]);
    }
    printf("\n");
  }
}

MDMA 初始化代码(由 CubeMX 生成,但需确认参数):

void MX_MDMA_Init(void)
{
  hmdma_mdma_channel0.Instance = MDMA_Channel0;
  hmdma_mdma_channel0.Init.Request = MDMA_REQUEST_TIM1_TRIG;
  hmdma_mdma_channel0.Init.TransferTriggerMode = MDMA_REPEAT_BLOCK_TRIGGER; // 每次触发传输一个块
  hmdma_mdma_channel0.Init.Priority = MDMA_PRIORITY_HIGH;
  hmdma_mdma_channel0.Init.Endianness = MDMA_LITTLE_ENDIAN;
  hmdma_mdma_channel0.Init.SourceInc = MDMA_SRC_INC_FIXED; // 外设地址固定
  hmdma_mdma_channel0.Init.DestinationInc = MDMA_DEST_INC_WORD; // 内存地址递增
  hmdma_mdma_channel0.Init.SourceDataSize = MDMA_SRC_DATASIZE_HALFWORD;
  hmdma_mdma_channel0.Init.DestDataSize = MDMA_DEST_DATASIZE_HALFWORD;
  hmdma_mdma_channel0.Init.DataAlignment = MDMA_DATAALIGN_PACK;
  hmdma_mdma_channel0.Init.BufferTransferLength = BUFFER_SIZE;
  hmdma_mdma_channel0.Init.SourceBurst = MDMA_SOURCE_BURST_4BEATS;
  hmdma_mdma_channel0.Init.DestBurst = MDMA_DEST_BURST_4BEATS;
  hmdma_mdma_channel0.Init.SourceBlockAddressOffset = 0;
  hmdma_mdma_channel0.Init.DestBlockAddressOffset = 0;
  HAL_MDMA_Init(&hmdma_mdma_channel0);

  /* 关联 MDMA 到 ADC 和定时器 */
  HAL_MDMA_ConfigTransfer(&hmdma_mdma_channel0, &sConfig);
}

注意:实际代码中,sConfig 需要根据需求配置,但 CubeMX 会生成完整结构。

注意事项

  • 内存一致性:MDMA 写入内存后,CPU 读取时需确保缓存一致性。在 STM32H7 上,若缓冲区位于可缓存区域(如 DTCM),需使用 __DMB() 或配置 MPU 为非缓存。推荐将缓冲区放在 AXI SRAM 并配置为强序。
  • 触发频率与传输时间:确保定时器触发频率低于 MDMA 完成一次传输的时间,否则数据可能丢失。例如,传输 256 个半字,突发 4 次,约需 64 个周期,远低于 1ms。
  • ADC 与定时器同步:ADC 的触发源必须与 MDMA 的触发源一致,否则数据可能错位。本例中,ADC 由 TIM1_TRGO 触发,MDMA 也由 TIM1_TRGO 触发,但需注意时序:ADC 转换完成后,数据寄存器更新,MDMA 触发时读取到的是最新值。
  • 循环模式下的缓冲区管理:当缓冲区写满后,MDMA 自动回卷,可能覆盖旧数据。开发者需在合适时机处理数据,如使用双缓冲或半传输中断(但中断会破坏零中断特性,可考虑使用 DMA 的传输完成标志轮询)。
  • 调试建议:使用逻辑分析仪观察定时器触发信号和 MDMA 请求,确保时序正确。

总结

通过定时器与 MDMA 联动,STM32H7 实现了外设到内存的零中断搬运,极大降低了 CPU 负载。本文提供的配置和代码可直接应用于 ADC 连续采样等场景。开发者可根据实际需求调整触发源、缓冲区大小和传输模式,充分发挥 MDMA 的高效性能。