STM32开发关于DMA 核心概念与工程选型(附完整代码配置)
DMA 核心概念与工程选型
从“CPU逐字节搬运”到“硬件自动传输”——STM32 DMA 完整学习笔记
一、DMA 核心概念
DMA(Direct Memory Access,直接内存访问)是 STM32 内置的硬件搬运外设。
核心作用:不占用 CPU,由硬件自动搬运数据,大幅解放内核,提升系统实时性。
CPU 只需要:初始化配置 → 启动 DMA → 坐等完成中断,搬运全程无需干预。
DMA 三大优势
无需 CPU 逐字节读取,零占用内核
硬件自动触发、自动搬运、速度极快
适合高速、连续、大批量数据传输
二、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) | 内存→内存 |