别再让CPU干杂活了手把手教你用STM32的DMA给串口发送数据提速在嵌入式开发中串口通信是最基础也最常用的功能之一。但当我们需要发送大量数据时传统的轮询或中断方式往往会成为系统性能的瓶颈。想象一下这样的场景你的设备需要每秒发送数百KB的传感器数据同时还要处理复杂的算法和用户交互——这时如果CPU被串口发送数据这种杂活拖累整个系统的实时性就会大打折扣。这就是DMA技术大显身手的时候。DMA直接内存访问就像是一个专门负责数据搬运的小助手它可以在不占用CPU资源的情况下高效地完成内存与外设之间的数据传输。本文将带你深入理解DMA的工作原理并通过一个完整的STM32 USART DMA发送实例展示如何将串口发送速度提升数倍同时大幅降低CPU占用率。1. 为什么你的串口发送需要DMA在开始技术细节之前让我们先看看三种常见的串口数据发送方式及其性能对比发送方式CPU占用率最大吞吐量实时性影响代码复杂度轮询发送100%中等严重低中断发送30-70%较低中等中等DMA发送5%最高极小较高表三种串口发送方式性能对比传统轮询方式就像是你亲自一趟趟搬运货物——CPU必须等待每个字节发送完成才能处理下一个效率低下且完全阻塞了其他任务。中断方式稍好一些像是雇佣了一个临时工但每次搬运少量货物就要通知你一次频繁的上下文切换仍然消耗大量资源。而DMA方式则像是雇佣了一个专业的物流团队你只需要告诉他们货物的位置和目的地剩下的工作完全由他们自主完成期间你可以专心处理其他重要事务。这种方式的优势在以下场景尤为明显高频传感器数据采集与传输大容量日志记录实时音频/视频流传输需要低延迟响应的控制系统2. DMA核心机制深度解析要充分利用DMA我们需要理解它的几个关键特性2.1 DMA通道与仲裁机制STM32的DMA控制器采用多通道设计以F1系列为例DMA17个通道所有型号都有DMA25个通道仅大容量产品具备每个通道可以独立配置服务于特定的外设。当多个通道同时请求时仲裁器会根据以下优先级决定处理顺序软件优先级通过DMA_CCRx寄存器配置很高高中等低硬件优先级当软件优先级相同时通道编号越小优先级越高2.2 数据传输模式DMA支持多种灵活的数据传输方式typedef enum { DMA_Mode_Normal 0, // 普通模式传输完成后停止 DMA_Mode_Circular 1 // 循环模式自动重装计数器 } DMA_Mode_TypeDef;循环模式特别适合以下场景ADC连续采样环形缓冲区管理实时数据流处理2.3 地址增量与数据宽度DMA传输中的地址管理非常灵活DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; // 外设地址固定 DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; // 内存地址递增 DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; // 8位 DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; // 8位这种配置组合可以实现固定外设寄存器地址如USART-DR连续的内存区域访问不同位宽的数据自动打包/解包3. 实战USART DMA发送完整配置让我们以STM32F103的USART1为例详细讲解DMA发送配置步骤。3.1 硬件连接与时钟配置首先确保硬件连接正确USART1_TX → PA9串口终端设备如USB转TTL正确连接时钟配置代码// 使能DMA1时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 使能USART1时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); // GPIO时钟使能 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);3.2 DMA通道初始化USART1_TX对应DMA1通道4配置代码如下void USART1_DMA_Config(void) { DMA_InitTypeDef DMA_InitStructure; // 复位DMA通道 DMA_DeInit(DMA1_Channel4); // 配置DMA参数 DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)SendBuffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; // 内存到外设 DMA_InitStructure.DMA_BufferSize BUFFER_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_Normal; // 普通模式 DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; // 非内存到内存 DMA_Init(DMA1_Channel4, DMA_InitStructure); // 使能USART1 DMA发送请求 USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); }3.3 数据传输控制启动DMA传输的典型流程// 准备发送数据 void PrepareData(uint8_t *data, uint16_t length) { memcpy(SendBuffer, data, length); DMA_SetCurrDataCounter(DMA1_Channel4, length); // 设置传输数量 } // 开始DMA传输 void StartDMATransfer(void) { DMA_Cmd(DMA1_Channel4, DISABLE); // 先禁用通道 DMA_SetCurrDataCounter(DMA1_Channel4, BUFFER_SIZE); // 重新设置计数器 DMA_Cmd(DMA1_Channel4, ENABLE); // 启用通道 } // 检查传输状态 uint8_t IsTransferComplete(void) { return DMA_GetFlagStatus(DMA1_FLAG_TC4) SET; }4. 高级技巧与性能优化掌握了基本配置后我们来看几个提升DMA使用效率的高级技巧。4.1 双缓冲技术对于连续数据流可以采用双缓冲机制#define BUF_SIZE 512 uint8_t BufferA[BUF_SIZE]; uint8_t BufferB[BUF_SIZE]; volatile uint8_t ActiveBuffer 0; // 0:BufferA, 1:BufferB void DMA1_Channel4_IRQHandler(void) { if(DMA_GetITStatus(DMA1_IT_TC4)) { DMA_ClearITPendingBit(DMA1_IT_TC4); // 切换缓冲区 if(ActiveBuffer 0) { DMA_SetCurrDataCounter(DMA1_Channel4, BUF_SIZE); DMA_SetMemoryAddress(DMA1_Channel4, (uint32_t)BufferB); ActiveBuffer 1; } else { DMA_SetCurrDataCounter(DMA1_Channel4, BUF_SIZE); DMA_SetMemoryAddress(DMA1_Channel4, (uint32_t)BufferA); ActiveBuffer 0; } // 处理新数据填充... } }4.2 内存到内存传输DMA不仅可以用于外设通信还能加速内存间数据传输void MemoryCopy_DMA(uint32_t *src, uint32_t *dst, uint16_t count) { DMA_InitTypeDef DMA_InitStructure; DMA_DeInit(DMA1_Channel1); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)src; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)dst; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize count; 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; DMA_Init(DMA1_Channel1, DMA_InitStructure); DMA_Cmd(DMA1_Channel1, ENABLE); while(DMA_GetFlagStatus(DMA1_FLAG_TC1) RESET); DMA_ClearFlag(DMA1_FLAG_TC1); }4.3 动态调整传输速率在某些应用中我们需要根据系统负载动态调整DMA传输速率void AdjustDMARate(uint16_t new_rate) { // 先停止当前传输 USART_DMACmd(USART1, USART_DMAReq_Tx, DISABLE); DMA_Cmd(DMA1_Channel4, DISABLE); // 调整USART波特率 USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate new_rate; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); // 重新配置DMA DMA_SetCurrDataCounter(DMA1_Channel4, BUFFER_SIZE); USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); DMA_Cmd(DMA1_Channel4, ENABLE); }5. 常见问题与调试技巧即使正确配置了DMA实际开发中仍可能遇到各种问题。以下是几个常见问题及解决方法5.1 DMA传输不启动检查清单确认所有相关时钟已使能DMA、USART、GPIO验证DMA通道与外设的对应关系是否正确检查DMA和外设的使能顺序先配置DMA参数然后使能外设DMA请求最后使能DMA通道5.2 数据传输不完整可能原因缓冲区大小设置错误内存地址未正确递增传输过程中被高优先级任务打断调试方法// 在传输过程中监控剩余数据量 uint16_t remaining DMA_GetCurrDataCounter(DMA1_Channel4); printf(Remaining data: %d\n, remaining);5.3 性能优化检查点要获得最佳性能注意以下几点将DMA缓冲区放在高速内存区域如CCM RAM合理设置DMA通道优先级对于大数据传输使用循环缓冲避免频繁重配置对齐内存地址到4字节边界32位系统提示使用DMA时合理的内存布局能显著提升性能。考虑使用__attribute__((aligned(4)))确保缓冲区对齐。在实际项目中我遇到过一个典型的性能问题设备在发送大量数据时其他任务的响应变得迟缓。通过将串口发送改为DMA方式CPU占用率从70%降到了不到5%同时系统吞吐量提升了3倍。关键是要确保DMA缓冲区足够大避免频繁启动小数据量传输带来的开销。