STM32F103 USART2串口DMA接收不定长数据与中断发送的实战配置与性能优化
1. STM32F103 USART2串口DMA通信基础在嵌入式开发中串口通信是最常用的外设之一。传统的中断方式虽然简单但在处理大量数据时会导致CPU频繁中断严重影响系统性能。我在实际项目中就遇到过这种情况当串口需要连续收发几百字节数据时CPU几乎被中断服务程序占满其他任务根本无法正常运行。DMA直接内存访问技术就像是一个专职的快递员它能在不打扰CPU的情况下自动完成外设和内存之间的数据传输。以STM32F103的USART2为例使用DMA后接收数据时DMA会自动将串口接收到的数据搬运到指定缓冲区仅在数据接收完成时通知CPU一次发送数据时CPU只需准备好数据并启动DMA发送过程完全由DMA接管具体到硬件连接USART2的TX(PA2)和RX(PA3)引脚对应DMA1的通道7发送和通道6接收。这种硬件映射关系是固定的不能随意更改。我在调试时曾经尝试用其他通道结果数据根本无法传输后来查手册才发现这个问题。2. DMA接收不定长数据的实战配置2.1 硬件初始化关键步骤先来看USART2和DMA的初始化代码。这里有个坑我踩过如果不按正确顺序初始化会导致第一个字节丢失。// GPIO初始化 GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_2; // TX GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_3; // RX GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); // USART2初始化 USART_InitTypeDef USART_InitStructure; RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART2, USART_InitStructure); USART_Cmd(USART2, ENABLE);2.2 DMA接收配置技巧不定长数据接收的关键在于利用串口的IDLE中断。当串口总线空闲时会产生中断此时通过查询DMA剩余计数器值就能计算出接收到的数据长度。#define RX_BUF_SIZE 256 uint8_t rx_buf[RX_BUF_SIZE]; void DMA_RX_Config(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel6); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART2-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)rx_buf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize RX_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_Normal; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel6, DMA_InitStructure); USART_DMACmd(USART2, USART_DMAReq_Rx, ENABLE); DMA_Cmd(DMA1_Channel6, ENABLE); // 启用IDLE中断 USART_ITConfig(USART2, USART_IT_IDLE, ENABLE); }3. DMA中断发送的实现与优化3.1 发送初始化配置发送配置与接收类似但方向相反。这里有个性能优化点使用VeryHigh优先级可以减少数据传输延迟。uint8_t tx_buf[256]; void DMA_TX_Config(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); DMA_DeInit(DMA1_Channel7); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART2-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)tx_buf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize 0; 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_VeryHigh; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel7, DMA_InitStructure); USART_DMACmd(USART2, USART_DMAReq_Tx, ENABLE); }3.2 发送数据函数实现发送函数需要考虑缓冲区管理和DMA状态检查。我在项目中遇到过DMA忙状态判断不准确的问题后来增加了状态标志才解决。void USART2_SendData(uint8_t *data, uint16_t len) { while(DMA_GetCmdStatus(DMA1_Channel7) ENABLE); // 等待DMA空闲 memcpy(tx_buf, data, len); DMA_SetCurrDataCounter(DMA1_Channel7, len); DMA_Cmd(DMA1_Channel7, ENABLE); // 启用TC中断以便知道发送完成 USART_ITConfig(USART2, USART_IT_TC, ENABLE); }4. 中断服务程序编写要点4.1 接收中断处理IDLE中断处理是接收不定长数据的核心。这里有个关键细节必须先读SR再读DR才能正确清除IDLE标志。void USART2_IRQHandler(void) { if(USART_GetITStatus(USART2, USART_IT_IDLE) ! RESET) { USART_ReceiveData(USART2); // 必须读DR清除标志 uint16_t len RX_BUF_SIZE - DMA_GetCurrDataCounter(DMA1_Channel6); // 处理接收到的数据 if(len 0) { ProcessData(rx_buf, len); } // 重新配置DMA DMA_Cmd(DMA1_Channel6, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel6, RX_BUF_SIZE); DMA_Cmd(DMA1_Channel6, ENABLE); } }4.2 发送完成中断处理TC中断用于通知发送完成。在实际项目中我通常会在这里释放发送缓冲区或触发后续操作。void USART2_IRQHandler(void) { // ... 其他中断处理 if(USART_GetITStatus(USART2, USART_IT_TC) ! RESET) { USART_ClearITPendingBit(USART2, USART_IT_TC); // 发送完成处理 OnSendComplete(); } }5. 性能优化与异常处理5.1 缓冲区管理策略根据项目经验我总结了三种缓冲区方案单缓冲区简单但存在数据覆盖风险双缓冲区接收和处理可以并行环形缓冲区适合高频小数据量传输对于大多数应用双缓冲区是最佳选择。下面是实现示例#define BUF_SIZE 256 uint8_t rx_buf1[BUF_SIZE], rx_buf2[BUF_SIZE]; uint8_t *active_buf rx_buf1; void USART2_IRQHandler(void) { if(USART_GetITStatus(USART2, USART_IT_IDLE) ! RESET) { USART_ReceiveData(USART2); uint16_t len BUF_SIZE - DMA_GetCurrDataCounter(DMA1_Channel6); // 切换缓冲区 uint8_t *process_buf active_buf; active_buf (active_buf rx_buf1) ? rx_buf2 : rx_buf1; DMA_Cmd(DMA1_Channel6, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel6, BUF_SIZE); DMA_SetCurrDataCounter(DMA1_Channel6, (uint32_t)active_buf); DMA_Cmd(DMA1_Channel6, ENABLE); if(len 0) { ProcessData(process_buf, len); } } }5.2 异常情况处理在实际产品中必须考虑以下异常情况数据溢出DMA缓冲区不够时如何处理通信超时长时间未收到完整数据帧校验错误数据完整性检查我的经验是增加超时检测和缓冲区监控// 在初始化时启用定时器 TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE); // 定时器中断中检测超时 void TIM2_IRQHandler(void) { if(TIM_GetITStatus(TIM2, TIM_IT_Update) ! RESET) { TIM_ClearITPendingBit(TIM2, TIM_IT_Update); uint16_t remain DMA_GetCurrDataCounter(DMA1_Channel6); if(remain ! RX_BUF_SIZE) { // 触发超时处理 HandleTimeout(); } } }6. 实际项目经验分享在工业控制器项目中我们使用这套方案实现了115200波特率下稳定传输。对比传统中断方式CPU负载从70%降低到5%以下。具体优化点包括将DMA缓冲区对齐到4字节边界提升搬运效率根据数据特性调整DMA突发传输模式在FreeRTOS中使用信号量通知任务而非在中断中直接处理数据有个特别值得注意的问题当系统时钟配置改变时如进入低功耗模式必须重新初始化DMA否则会出现数据传输错误。这个坑让我们调试了整整两天才找到原因。