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,否则会出现数据传输错误。这个坑让我们调试了整整两天才找到原因。
