# 引言 在嵌入式系统中,外设数据(如 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 中添加关键部分。 ```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 生成,但需确认参数): ```c 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 的高效性能。