STM32开发关于DMA 核心概念与工程选型(附完整代码配置)

📅 2026/7/31 1:45:15 👁️ 阅读次数 📝 编程学习
STM32开发关于DMA 核心概念与工程选型(附完整代码配置)

DMA 核心概念与工程选型

从“CPU逐字节搬运”到“硬件自动传输”——STM32 DMA 完整学习笔记

一、DMA 核心概念

DMA(Direct Memory Access,直接内存访问)是 STM32 内置的硬件搬运外设。

核心作用:不占用 CPU,由硬件自动搬运数据,大幅解放内核,提升系统实时性。

CPU 只需要:初始化配置 → 启动 DMA → 坐等完成中断,搬运全程无需干预。

DMA 三大优势

  1. 无需 CPU 逐字节读取,零占用内核

  2. 硬件自动触发、自动搬运、速度极快

  3. 适合高速、连续、大批量数据传输

二、DMA 三大工作模式

1. 普通模式(Normal)

工作机制:设定传输长度 N,DMA 搬运 N 个数据后自动停止、通道关闭。如需再次传输,必须软件重新使能、重载计数。

特点:一次性任务、自动停机、不会覆盖数据。

缺点:无法持续接收数据流,停机后新数据直接丢包。

2. 循环模式(Circular)

工作机制:数据搬运到末尾后,硬件自动重载计数器,DMA 永久运行、永不停止。数据写满缓冲区后自动从头覆盖,形成环形缓冲区。

特点:7×24 不间断传输、无停机、适合流式数据。

风险:CPU 处理不及时会覆盖旧数据,必须搭配中断分包。

3. 内存到内存模式(Mem2Mem)

唯一纯内存搬运模式,无任何外设参与。只能普通模式,不支持循环模式。由软件启动 DMA,不需要硬件触发信号。

用途:大块内存快速拷贝,替代memcpy

三、DMA 四种传输方向(工程最核心)

DMA 所有项目应用,全部来自下面 4 种组合。

方向一:外设 → 内存(接收方向)

本质:外部设备数据自动流入 MCU 内存。

固定配置:外设地址不递增(DMA_PeripheralInc_Disable)、内存地址递增(DMA_MemoryInc_Enable)。

① 外设→内存 + 普通模式(Normal)

适用场景:一次性、有限长度、触发式采集。

真实项目应用:

  • 仪器仪表:按键触发单次 ADC 采样、均值滤波采集

  • 传感器设备:I2C/SPI 单次读取温湿度、压力数据

  • 物联网设备:上电一次性读取 Flash 配置参数

  • 工业检测:单次采集一组电压电流数据

核心逻辑:采完一组就停机,不需要一直运行。

c

/** * @brief 外设→内存 + 普通模式:单次ADC采样 * @note ADC1_Channel0 → 内存数组,采样完成后DMA自动停止 */ void ADC_DMA_Normal_Config(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&ADC1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)g_adc_buffer; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 外设→内存 DMA_InitStructure.DMA_BufferSize = 10; // 采集10个点后停止 DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 普通模式 DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel1, &DMA_InitStructure); // 使能DMA传输完成中断(可选) DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE); DMA_Cmd(DMA1_Channel1, ENABLE); }

② 外设→内存 + 循环模式(Circular)

适用场景:不间断流式数据、永久实时采集。

真实项目应用(工业最常用):

  • AGV 小车、机器人:RS485/USART 持续接收运动控制指令

  • 充电桩、光伏逆变器:不间断 ADC 采集电压电流

  • 音频设备:麦克风 I2S 持续录音

  • 运动控制:SPI 编码器实时位置持续采集

  • 智能家居:串口持续接收遥控、蓝牙数据流

工业标准方案:循环 DMA + 串口 IDLE 空闲中断(实现不定长帧接收)

c

/** * @brief 外设→内存 + 循环模式:串口DMA持续接收 * @note DMA自动将USART1收到的数据循环存入缓冲区 * 配合IDLE空闲中断实现不定长帧接收 */ #define USART_DMA_BUF_SIZE 256 uint8_t g_usart_rx_buffer[USART_DMA_BUF_SIZE]; void USART1_DMA_RX_Config(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel5); // USART1_RX 对应 DMA1_Channel5 DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)g_usart_rx_buffer; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 外设→内存 DMA_InitStructure.DMA_BufferSize = USART_DMA_BUF_SIZE; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环模式[reference:8] DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel5, &DMA_InitStructure); // 使能DMA半满中断 + 完成中断(防覆盖)[reference:9] DMA_ITConfig(DMA1_Channel5, DMA_IT_HT | DMA_IT_TC, ENABLE); DMA_Cmd(DMA1_Channel5, ENABLE); // 开启串口空闲中断 USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE); }

方向二:内存 → 外设(发送方向)

本质:MCU 内存数据自动推送到外设,向外输出信号。

固定配置:外设地址固定(DMA_PeripheralInc_Disable)、内存自增(DMA_MemoryInc_Enable)。

① 内存→外设 + 普通模式(Normal)

适用场景:一次性整块数据发送、刷屏、烧录、上报。

真实项目应用:

  • 工业 HMI:LCD/OLED/SPI 屏幕一次性刷屏

  • 存储设备:SPI Flash 一页数据烧录

  • 物联网设备:定时上报日志、上报传感器数据

  • 串口设备:一次性发送数据包、指令帧

c

/** * @brief 内存→外设 + 普通模式:串口DMA发送 * @note 将内存中的字符串通过DMA发送到USART1 */ void USART1_DMA_TX_Config(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel4); // USART1_TX 对应 DMA1_Channel4 DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)g_tx_buffer; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // 内存→外设 DMA_InitStructure.DMA_BufferSize = strlen((char*)g_tx_buffer); DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 普通模式[reference:12] DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel4, &DMA_InitStructure); // 使能传输完成中断 DMA_ITConfig(DMA1_Channel4, DMA_IT_TC, ENABLE); DMA_Cmd(DMA1_Channel4, ENABLE); USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); } // 启动一次DMA发送 void USART1_DMA_Send(uint8_t *data, uint16_t len) { DMA_Cmd(DMA1_Channel4, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel4, len); DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)data; DMA_Init(DMA1_Channel4, &DMA_InitStructure); DMA_Cmd(DMA1_Channel4, ENABLE); }

② 内存→外设 + 循环模式(Circular)

适用场景:持续周期性输出固定波形/音频。

真实项目应用:

  • 信号发生器:DAC 循环输出正弦波、三角波、方波

  • 变频器、电机驱动:DAC 模拟波形持续输出

  • 音频设备:循环播放提示音、报警音

  • 灯光控制器:SPI 持续输出时序信号

重点坑点:运行中修改缓存必须用双缓冲,否则波形撕裂、数据错乱。

c

/** * @brief 内存→外设 + 循环模式:DAC输出正弦波 * @note DMA循环从正弦波表中搬运数据到DAC寄存器 * 配合定时器触发,实现连续波形输出[reference:16] */ #define SINE_WAVE_POINTS 32 uint16_t g_sine_wave[SINE_WAVE_POINTS]; // 预计算的正弦波表 void DAC_DMA_Circular_Config(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 预计算正弦波表(12位DAC,0~4095) for (int i = 0; i < SINE_WAVE_POINTS; i++) { g_sine_wave[i] = (uint16_t)(2048 + 2047 * sin(2 * PI * i / SINE_WAVE_POINTS)); } DMA_DeInit(DMA1_Channel3); // DAC1 对应 DMA1_Channel3 DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&DAC->DHR12R1; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)g_sine_wave; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralDST; // 内存→外设 DMA_InitStructure.DMA_BufferSize = SINE_WAVE_POINTS; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_HalfWord; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_HalfWord; DMA_InitStructure.DMA_Mode = DMA_Mode_Circular; // 循环模式[reference:17] DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Disable; DMA_Init(DMA1_Channel3, &DMA_InitStructure); DMA_Cmd(DMA1_Channel3, ENABLE); // 使能DAC的DMA请求 DAC_DMACmd(DAC_Channel_1, ENABLE); DAC_Cmd(DAC_Channel_1, ENABLE); }

方向三:内存 → 内存(Mem2Mem)

无外设、无硬件触发、纯内存搬运。只能普通模式,禁止循环模式。

真实项目应用:

  • 液晶屏 UI:Flash 字库/图片搬运到 SRAM 显存

  • 图像设备:摄像头大数据块内存平移、拷贝

  • 系统初始化:批量拷贝配置参数

  • 替代库函数memcpy,大幅节省 CPU 资源

绝对不能用:串口、ADC、DAC、SPI 等所有外设传输。

c

/** * @brief 内存→内存模式:大块数据快速拷贝 * @note 纯内存搬运,无外设参与,只能普通模式 */ #define MEM2MEM_BUF_SIZE 1024 uint32_t g_src_buffer[MEM2MEM_BUF_SIZE]; uint32_t g_dst_buffer[MEM2MEM_BUF_SIZE]; void DMA_Mem2Mem_Config(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 注意:Mem2Mem只能使用DMA2(大容量芯片)或特定通道[reference:23] // STM32F103ZET6 使用 DMA1_Channel1 ~ 7 均可支持 Mem2Mem DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)g_src_buffer; // 源地址 DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)g_dst_buffer; // 目标地址 DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; // 从源读(此处把外设当内存用) DMA_InitStructure.DMA_BufferSize = MEM2MEM_BUF_SIZE; DMA_InitStructure.DMA_PeripheralInc = DMA_PeripheralInc_Enable; // 源地址递增 DMA_InitStructure.DMA_MemoryInc = DMA_MemoryInc_Enable; // 目标地址递增 DMA_InitStructure.DMA_PeripheralDataSize = DMA_PeripheralDataSize_Word; DMA_InitStructure.DMA_MemoryDataSize = DMA_MemoryDataSize_Word; DMA_InitStructure.DMA_Mode = DMA_Mode_Normal; // 只能普通模式 DMA_InitStructure.DMA_Priority = DMA_Priority_High; DMA_InitStructure.DMA_M2M = DMA_M2M_Enable; // 使能内存到内存模式[reference:25] DMA_Init(DMA1_Channel1, &DMA_InitStructure); // 使能传输完成中断 DMA_ITConfig(DMA1_Channel1, DMA_IT_TC, ENABLE); } // 启动一次内存拷贝(软件触发,无需外设信号) void DMA_Mem2Mem_Start(void) { DMA_Cmd(DMA1_Channel1, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel1, MEM2MEM_BUF_SIZE); DMA_Cmd(DMA1_Channel1, ENABLE); // 软件启动,立即开始搬运 }

四、DMA 中断类型详解

DMA 支持三种中断事件,通过“线或”方式共享一个中断向量:

中断类型标志位触发条件典型用途
传输完成(TC)TCIF全部数据搬运完毕通知CPU数据已就绪
半传输完成(HT)HTIF搬运到缓冲区一半实现“软双缓冲”,防止覆盖
传输错误(TE)TEIF总线错误、地址非法异常处理

c

/** * @brief DMA中断服务函数(以DMA1_Channel5为例) * @note 需在中断中判断具体中断源[reference:31] */ void DMA1_Channel5_IRQHandler(void) { // 半传输完成中断(HT)—— 前半段数据已就绪[reference:32] if (DMA_GetITStatus(DMA1_IT_HT5) != RESET) { DMA_ClearITPendingBit(DMA1_IT_HT5); // 处理缓冲区前半部分数据[reference:33] Process_Half_Buffer(0, USART_DMA_BUF_SIZE / 2); } // 传输完成中断(TC)—— 全部数据已就绪[reference:34] if (DMA_GetITStatus(DMA1_IT_TC5) != RESET) { DMA_ClearITPendingBit(DMA1_IT_TC5); // 处理缓冲区后半部分数据[reference:35] Process_Half_Buffer(USART_DMA_BUF_SIZE / 2, USART_DMA_BUF_SIZE); } // 传输错误中断(TE) if (DMA_GetITStatus(DMA1_IT_TE5) != RESET) { DMA_ClearITPendingBit(DMA1_IT_TE5); // 错误处理 Error_Handler(); } }

半满中断 + 完成中断 = 软双缓冲:将单个大缓冲区逻辑上分成两半,DMA 写入前半段时触发 HT 中断,CPU 处理前半段数据;DMA 继续写入后半段,两者互不干扰。这是 STM32F1 系列没有硬件双缓冲时的替代方案。

五、DMA 优先级仲裁

当多个 DMA 通道同时请求传输时,需通过优先级仲裁决定响应顺序:

  • 软件优先级(4级):DMA_Priority_VeryHigh>High>Medium>Low

  • 硬件优先级:软件优先级相同时,通道号越小优先级越高

c

// 高优先级通道配置示例 DMA_InitStructure.DMA_Priority = DMA_Priority_VeryHigh;

六、面试高频标准答案

1. 为什么串口接收要用循环 DMA,不用普通 DMA?

普通 DMA 收满长度会自动停机,串口数据流是不定长、随时到来,停机后会丢包。循环 DMA 永久运行,配合 IDLE 空闲中断可实现工业级不定长帧接收。

2. DAC 输出波形为什么必须循环模式?

普通模式发送完一次数据就停止,电平固定不变,无法形成连续波形。循环模式可以重复循环波形表,输出连续正弦/三角波。

3. Mem2Mem 为什么不能循环?

硬件设计禁止,循环内存拷贝会长期占用总线带宽,抢占外设实时数据流,造成通讯、采样异常。

4. 循环 DMA 最大风险是什么?

缓冲区覆盖。CPU 处理速度跟不上 DMA 写入速度,旧数据被新数据直接覆盖。解决方案:HT/TC 中断 + 双缓冲(软双缓冲)。

5. DMA 的优先级是如何分配的?

DMA 优先级分为软件优先级(4级:VeryHigh/High/Medium/Low)和硬件优先级。当软件优先级相同时,通道号越小的优先级越高。多个 DMA 请求同时发生时,优先级高的先响应。

6. DMA 的“半满中断”有什么用?

在循环 DMA 模式下,半满中断(HT)在 DMA 写到缓冲区一半时触发,配合完成中断(TC)可以将缓冲区拆分为两半,实现“软双缓冲”。当 CPU 在处理前半段数据时,DMA 继续向后半段写入,防止数据覆盖。这是 STM32F1 系列没有硬件双缓冲时的标准替代方案。

七、全文总结(一句话记住所有 DMA)

单次任务用普通,流式任务用循环,外设收发分方向,内存拷贝用 MEM2MEM。

场景模式方向
单次ADC采样Normal外设→内存
串口持续接收Circular外设→内存
一次性发送数据包Normal内存→外设
DAC连续输出波形Circular内存→外设
大块内存拷贝Normal (Mem2Mem)内存→内存