
一、引言在 STM32 的开发中有一个常见的误区我开了 DMA数据就能自动传输。结果发现 DMA 只工作了一次就停了或者数据覆盖了还没读走或者在高负载时丢数据。DMADirect Memory Access直接存储器访问是嵌入式系统中最重要的性能倍增器之一。它让数据在外设和内存之间自动流动CPU 可以专注于更有价值的计算任务。本文从 DMA 的工作原理出发深入分析DMA 控制器的内部架构从单次传输到循环模式双缓冲与乒乓缓冲的实战设计链式传输Scatter-Gather的高级用法ADC 多通道 DMA 采样的常见陷阱DMA 仲裁与总线矩阵平台STM32F103单 DMA7 通道和 STM32F407双 DMA16 流 对比 F1 和 F4 两代 DMA 架构的差异二、DMA 控制器架构2.1 STM32F103 的 DMA 架构┌────────────────────────────────────────────────┐ │ DMA1 (7 通道) │ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │ │ CH1 │ │ CH2 │ │ CH3 │ ... │ CH7 │ │ │ │ ADC1 │ │ SPI1 │ │ TIM1 │ │ USART│ │ │ │ │ │ _RX │ │ │ │ 2_TX │ │ │ └──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘ │ │ │ │ │ │ │ │ └────────┴────┬───┴────────────┘ │ │ ▼ │ │ ┌────────────┐ │ │ │ 仲裁器 │ ← 通道优先级管理 │ │ └──────┬─────┘ │ │ ▼ │ │ ┌────────────┐ │ │ │ AHB 主接口 │ → 总线矩阵 → 存储器 │ │ └────────────┘ │ ├────────────────────────────────────────────────┤ │ DMA2 (5 通道) │ │ 仅存在于大容量/互联型产品通道映射不同 │ └────────────────────────────────────────────────┘ 关键限制F103 - 每个通道只能绑定一个外设请求 - 同一时刻只有一个通道在传输分时复用 - 不支持双缓冲和链式传输2.2 STM32F407 的 DMA 架构Stream 模型┌──────────────────────────────────────────────────────┐ │ DMA1 (8 个 Stream × 8 个 Channel) │ │ │ │ Stream0 ─┬─ Ch0 ── SPI3_RX │ │ ├─ Ch1 ── TIM7_UP │ │ ├─ Ch2 ── ... │ │ └─ Ch7 ── ... │ │ │ │ Stream1 ─┬─ Ch0 ── USART3_RX │ │ ├─ ... │ │ │ │ ...每个 Stream 可以选 8 个 Channel 之一... │ │ │ │ ★ 关键升级 │ │ - 双缓冲模式Double Buffer │ │ - 可编程数据宽度字节/半字/字 │ │ - FIFO4 级深度可做字节重组 │ │ - 突发传输Burst最多 16 拍 │ │ - 链式传输通过描述符链表 │ └──────────────────────────────────────────────────────┘三、DMA 基本配置——从入门到精通3.1 USART TX DMA最简单的 DMA 应用// DMA 发送——解放 CPU让 printf 不阻塞 uint8_t tx_buffer[128]; void USART1_DMA_TX_Init(void) { DMA_InitTypeDef dma; // 1. 使能 DMA 时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 2. 配置 DMA 通道USART1_TX DMA1_Channel4 DMA_DeInit(DMA1_Channel4); dma.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; // ★ 外设地址 USART 数据寄存器 dma.DMA_MemoryBaseAddr (uint32_t)tx_buffer; // ★ 内存地址 发送缓冲区 dma.DMA_DIR DMA_DIR_PeripheralDST; // 方向内存 → 外设 dma.DMA_BufferSize 0; // 传输数量待定 dma.DMA_PeripheralInc DMA_PeripheralInc_Disable; // 外设地址不变 dma.DMA_MemoryInc DMA_MemoryInc_Enable; // 内存地址递增 dma.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; dma.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; dma.DMA_Mode DMA_Mode_Normal; // 单次模式 dma.DMA_Priority DMA_Priority_Medium; dma.DMA_M2M DMA_M2M_Disable; // 非存储器到存储器 DMA_Init(DMA1_Channel4, dma); // 3. 使能 USART 的 DMA 发送请求 USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); } // 发送数据非阻塞 void USART1_DMA_Send(const uint8_t *data, uint16_t len) { // 等待上次 DMA 传输完成 while (DMA_GetFlagStatus(DMA1_FLAG_TC4) RESET); DMA_ClearFlag(DMA1_FLAG_TC4); // 配置新的传输 DMA_Cmd(DMA1_Channel4, DISABLE); DMA1_Channel4-CMAR (uint32_t)data; DMA1_Channel4-CNDTR len; DMA_Cmd(DMA1_Channel4, ENABLE); // 函数立即返回数据在后台通过 DMA 发送 // CPU 可以继续做别的事情 }3.2 ADC 多通道 DMA——最常用的采样模式// ADC1 的三通道规则组 DMA 自动搬运 #define ADC_CHANNEL_COUNT 3 volatile uint16_t adc_values[ADC_CHANNEL_COUNT]; // DMA 自动填充 void ADC1_DMA_Init(void) { ADC_InitTypeDef adc; DMA_InitTypeDef dma; // DMA 配置 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel1); // ADC1 DMA1_Channel1 dma.DMA_PeripheralBaseAddr (uint32_t)ADC1-DR; // ADC 数据寄存器 dma.DMA_MemoryBaseAddr (uint32_t)adc_values; // 目标数组 dma.DMA_DIR DMA_DIR_PeripheralSRC; // 外设 → 内存 dma.DMA_BufferSize ADC_CHANNEL_COUNT; // 每次传输 3 个数据 dma.DMA_PeripheralInc DMA_PeripheralInc_Disable; // ADC_DR 地址固定 dma.DMA_MemoryInc DMA_MemoryInc_Enable; // 数组地址递增 dma.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; // 16bit dma.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; dma.DMA_Mode DMA_Mode_Circular; // ★ 循环模式 dma.DMA_Priority DMA_Priority_High; dma.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel1, dma); DMA_Cmd(DMA1_Channel1, ENABLE); // ADC 配置 // ... GPIO、RCC 配置 ... ADC_DeInit(ADC1); adc.ADC_Mode ADC_Mode_Independent; adc.ADC_ScanConvMode ENABLE; // ★ 扫描模式多通道 adc.ADC_ContinuousConvMode ENABLE; // ★ 连续转换模式 adc.ADC_NbrOfChannel ADC_CHANNEL_COUNT; // ... 其他参数 ... ADC_Init(ADC1, adc); // 配置规则组通道顺序很重要DMA 按此顺序填充数组 ADC_RegularChannelConfig(ADC1, ADC_Channel_0, 1, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_1, 2, ADC_SampleTime_55Cycles5); ADC_RegularChannelConfig(ADC1, ADC_Channel_2, 3, ADC_SampleTime_55Cycles5); // ★ 使能 ADC DMA ADC_DMACmd(ADC1, ENABLE); // 启动 ADC ADC_Cmd(ADC1, ENABLE); ADC_ResetCalibration(ADC1); while (ADC_GetResetCalibrationStatus(ADC1)); ADC_StartCalibration(ADC1); while (ADC_GetCalibrationStatus(ADC1)); ADC_SoftwareStartConvCmd(ADC1, ENABLE); } // 使用时直接读取数组——无需任何中断 // adc_values[0] 通道 0 的最新值 // adc_values[1] 通道 1 的最新值 // adc_values[2] 通道 2 的最新值 // 数据被 DMA 自动更新CPU 零开销四、双缓冲与乒乓缓冲4.1 为什么需要双缓冲单缓冲的问题 DMA 持续填充 Buffer A │ ▼ CPU 在某个时刻读取 Buffer A │ ▼ 问题DMA 正在覆盖 Buffer A → CPU 读到的数据可能一半是旧的一半是新的 → 数据不一致 双缓冲的解决 DMA 填充 Buffer 0 ──► 填满后触发中断 ──► 自动切换到 Buffer 1 CPU 处理 Buffer 0 数据 DMA 继续填充 Buffer 1 │ │ └────────────────────────────────────────┘ 互不干扰的数据区域4.2 F407 双缓冲模式硬件支持// STM32F407 的硬件双缓冲模式 uint16_t adc_buf0[100]; uint16_t adc_buf1[100]; void ADC_DoubleBuffer_Init(void) { DMA_InitTypeDef dma; // ... 基本配置 ... dma.DMA_Mode DMA_Mode_Circular; dma.DMA_Memory0BaseAddr (uint32_t)adc_buf0; // ★ 缓冲区 0 // ★ 不需要配置 Memory1BaseAddr用下面的函数 DMA_Init(DMA2_Stream0, dma); // ★ 启用双缓冲模式设置缓冲区 1 地址 DMA_DoubleBufferModeConfig(DMA2_Stream0, (uint32_t)adc_buf1, DMA_Memory_0); DMA_DoubleBufferModeCmd(DMA2_Stream0, ENABLE); // 使能传输完成中断用于切换缓冲区时通知 CPU DMA_ITConfig(DMA2_Stream0, DMA_IT_TC, ENABLE); NVIC_EnableIRQ(DMA2_Stream0_IRQn); } // 中断服务函数 void DMA2_Stream0_IRQHandler(void) { if (DMA_GetITStatus(DMA2_Stream0, DMA_IT_TCIF0)) { DMA_ClearITPendingBit(DMA2_Stream0, DMA_IT_TCIF0); // ★ 检查当前使用的是哪个缓冲区 if (DMA_GetCurrentMemoryTarget(DMA2_Stream0) DMA_Memory_0) { // 当前 DMA 正在写 buf0 → 上一轮完成的是 buf1 → 处理 buf1 ProcessData(adc_buf1, 100); } else { // 当前 DMA 正在写 buf1 → 上一轮完成的是 buf0 → 处理 buf0 ProcessData(adc_buf0, 100); } } }4.3 F103 软件乒乓缓冲模拟双缓冲// F103 没有硬件双缓冲用半传输中断模拟 uint16_t adc_buf[200]; // 总缓冲区 200 个数据 // 逻辑上分为两半[0..99] 和 [100..199] void ADC_PingPong_Init(void) { // ... DMA 配置 ... dma.DMA_BufferSize 200; // 总大小 dma.DMA_Mode DMA_Mode_Circular; DMA_Init(DMA1_Channel1, dma); // ★ 使能半传输中断和传输完成中断 DMA_ITConfig(DMA1_Channel1, DMA_IT_HT, ENABLE); // 半传输 一半完成 DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE); // 传输完成 全部完成 NVIC_EnableIRQ(DMA1_Channel1_IRQn); } void DMA1_Channel1_IRQHandler(void) { // ★ 半传输中断前一半0..99填满 if (DMA_GetITStatus(DMA1_IT_HT1)) { DMA_ClearITPendingBit(DMA1_IT_HT1); ProcessData(adc_buf[0], 100); // 处理前一半 } // ★ 传输完成中断后一半100..199填满 if (DMA_GetITStatus(DMA1_IT_TC1)) { DMA_ClearITPendingBit(DMA1_IT_TC1); ProcessData(adc_buf[100], 100); // 处理后一半 } } // 这就是乒乓缓冲的精髓DMA 轮流填充两半CPU 处理另一半五、DMA 的高级话题5.1 DMA 仲裁与总线矩阵STM32F407 的总线矩阵Bus Matrix ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ Cortex │ │ DMA1 │ │ DMA2 │ │ USB │ │ -M4 │ │ (8流) │ │ (8流) │ │ OTG │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ │ │ │ └────────────┴─────┬──────┴────────────┘ │ ┌──────┴──────┐ │ 总线矩阵 │ ← 仲裁当多方同时访问同一目标时 └──────┬──────┘ │ ┌───────────────┼───────────────┐ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ Flash │ │ SRAM1 │ │ AHB/APB │ │ (代码) │ │ (112KB) │ │ 外设 │ └──────────┘ └──────────┘ └──────────┘ 关键规则 - CPU 访问 Flash 的同时DMA 可以访问 SRAM不冲突 - CPU 和 DMA 同时访问 SRAM → 仲裁可能引入等待周期 - 多个 DMA 流同时请求 → 按优先级仲裁软件可配5.2 DMA 传输带宽分析// 计算理论最大 DMA 带宽 // F407 168MHz, AHB 168MHz // 单次 AHB 传输 1 个 AHB 时钟周期 // 每传输 1 个字 (32bit) 1 cycle // 理论带宽 168MHz × 4 bytes 672 MB/s // 实际情况 // - 与 CPU 竞争总线仲裁开销 // - APB 外设速度限制APB142MHz, APB284MHz // - Flash 访问等待周期 // - 外设数据宽度限制如 ADC 只产生 16bit 数据 // 实测DMA2 从 SRAM 到 SRAM168MHz // 约 300 MB/s考虑仲裁和开销5.3 常见 DMA 问题与调试// 问题 1DMA 不工作 // → 检查 ① 时钟是否使能 ② 通道映射是否正确 ③ 外设是否发出 DMA 请求 // 问题 2DMA 只传输了一次就停了 // → 检查 DMA_Mode 是否为 Circular循环模式 // → 每次传输后手动重设 CNDTR 和 CMAR // 问题 3数据覆盖新数据覆盖了 CPU 未读的旧数据 // → 使用双缓冲/乒乓缓冲 // 问题 4ADC DMA 的数据顺序与通道顺序不一致 // → 确认 ADC_RegularChannelConfig 的第三个参数顺序号是对的 // 问题 5DMA 和 Cache 一致性问题Cortex-M7 // → M7 有数据 CacheDMA 写 SRAM 后 CPU 读到的 Cache 是旧的 // → 解决方法 // ① 使用 non-cacheable 的 SRAM 区域 // ② 手动 Clean/Invalidate CacheSCB_CleanInvalidateDCache() // F103/F407 没有这个问题没有数据 Cache六、DMA 实战案例全双工音频采集与播放// 完整实例使用 DMA 实现 I2S 音频的采集播放 // 双 DMA 通道一个采集麦克风一个播放扬声器 #define AUDIO_BUF_SIZE 512 int16_t rx_buf[AUDIO_BUF_SIZE]; // 麦克风数据 int16_t tx_buf[AUDIO_BUF_SIZE]; // 扬声器数据 void Audio_DMA_Init(void) { // I2S2 接收麦克风→ DMA1_Stream3 // I2S2 发送扬声器→ DMA1_Stream4 // ... I2S 配置省略... // RX DMA (I2S → rx_buf) DMA_InitTypeDef dma_rx; dma_rx.DMA_Channel DMA_Channel_0; // I2S2_RX dma_rx.DMA_PeripheralBaseAddr (uint32_t)SPI2-DR; dma_rx.DMA_Memory0BaseAddr (uint32_t)rx_buf; dma_rx.DMA_DIR DMA_DIR_PeripheralToMemory; dma_rx.DMA_BufferSize AUDIO_BUF_SIZE; dma_rx.DMA_PeripheralInc DMA_PeripheralInc_Disable; dma_rx.DMA_MemoryInc DMA_MemoryInc_Enable; dma_rx.DMA_PeripheralDataSize DMA_PeripheralDataSize_HalfWord; dma_rx.DMA_MemoryDataSize DMA_MemoryDataSize_HalfWord; dma_rx.DMA_Mode DMA_Mode_Circular; dma_rx.DMA_Priority DMA_Priority_High; // ★ 注意RX 和 TX 可以用同一个 buf只要保证半传输/全传输时机不重叠 DMA_Init(DMA1_Stream3, dma_rx); // TX DMA (tx_buf → I2S) DMA_InitTypeDef dma_tx; dma_tx.DMA_Channel DMA_Channel_0; // I2S2_TX同 Channel 但不同 Stream dma_tx.DMA_PeripheralBaseAddr (uint32_t)SPI2-DR; dma_tx.DMA_Memory0BaseAddr (uint32_t)tx_buf; dma_tx.DMA_DIR DMA_DIR_MemoryToPeripheral; // ... 其余类似 ... DMA_Init(DMA1_Stream4, dma_tx); // 半传输中断 → 填充 tx_buf 的前半/后半做音频处理 DMA_ITConfig(DMA1_Stream4, DMA_IT_HT | DMA_IT_TC, ENABLE); } // 在半传输/全传输中断中处理音频 // 如果 rx_buf 的数据需要处理后再放到 tx_buf void DMA1_Stream4_IRQHandler(void) { if (DMA_GetITStatus(DMA1_Stream4, DMA_IT_HTIF4)) { DMA_ClearITPendingBit(DMA1_Stream4, DMA_IT_HTIF4); // 处理 rx_buf[0..255] → tx_buf[0..255] AudioProcess(rx_buf, tx_buf, 256); } if (DMA_GetITStatus(DMA1_Stream4, DMA_IT_TCIF4)) { DMA_ClearITPendingBit(DMA1_Stream4, DMA_IT_TCIF4); // 处理 rx_buf[256..511] → tx_buf[256..511] AudioProcess(rx_buf[256], tx_buf[256], 256); } }七、总结概念关键点DMA 的本质在外设和内存之间搬运数据绕过 CPUF103 vs F407F103 用半数中断模拟乒乓F407 硬件双缓冲循环模式DMA_Mode_Circular——永不停止适合 ADC双缓冲一半填满时处理数据互不干扰仲裁多个 DMA 同时访问要先看优先级Cache 一致性M7 需手动维护M3/M4 不需要调试建议先确认外设是否产生 DMA 请求 通道映射是否正确一句话总结DMA 是嵌入式系统性能的关键——学会用 DMA 搬运 ADC、USART、SPI 数据是你从能用到用好的分水岭。