当前位置: 首页 > news >正文

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 缓冲区管理策略

根据项目经验,我总结了三种缓冲区方案:

  1. 单缓冲区:简单但存在数据覆盖风险
  2. 双缓冲区:接收和处理可以并行
  3. 环形缓冲区:适合高频小数据量传输

对于大多数应用,双缓冲区是最佳选择。下面是实现示例:

#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 异常情况处理

在实际产品中,必须考虑以下异常情况:

  1. 数据溢出:DMA缓冲区不够时如何处理
  2. 通信超时:长时间未收到完整数据帧
  3. 校验错误:数据完整性检查

我的经验是增加超时检测和缓冲区监控:

// 在初始化时启用定时器 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%以下。具体优化点包括:

  1. 将DMA缓冲区对齐到4字节边界,提升搬运效率
  2. 根据数据特性调整DMA突发传输模式
  3. 在FreeRTOS中使用信号量通知任务,而非在中断中直接处理数据

有个特别值得注意的问题:当系统时钟配置改变时(如进入低功耗模式),必须重新初始化DMA,否则会出现数据传输错误。这个坑让我们调试了整整两天才找到原因。

http://www.jsqmd.com/news/824680/

相关文章:

  • 从ERROR 1062到MySQL主键约束:一次“Duplicate entry”的深度排查与修复实战
  • 2026届最火的十大降AI率方案横评
  • 告别XDMA限制:用开源Riffa框架在Linux下轻松实现多通道PCIE DMA通信(Kintex-7实测)
  • 基于MCP协议构建DeFi智能体:降低链上操作门槛的实践指南
  • Windows-build-tools终极指南:一键安装C++构建工具和Python的完整解决方案
  • 初次使用Taotoken从注册到发出第一个请求的全流程记录
  • DeepSeek MATH实测得分暴跌37%?揭秘模型在组合数学与形式化证明中的3个致命盲区
  • Kubuntu 22.04 LTS 新手指南:从零到一,在VMware中轻松部署你的KDE桌面
  • Java架构面试参考指南全网首次公开!
  • Heat静态站点生成器:极简Python工具构建个人博客与文档站
  • WandEnhancer:解锁游戏修改器的完整本地增强体验
  • QKeyMapper:免费开源的Windows全能按键映射工具终极指南
  • STM32H743以太网实战:基于CubeMX 6.8.0与LAN8720的LWIP移植避坑指南
  • 开源安全工具集openclaw-safe:自动化安全检查的模块化实践
  • Nginx Server Configs配置验证工具:确保配置正确性的终极指南
  • 阿里Java面试核心讲(终极版)全网首次公开!
  • 华为USG6000防火墙Web界面实战:从零配置到安全策略部署
  • 小微团队如何利用Taotoken的Token Plan套餐控制AI开发成本
  • 打造现代化Vue 3侧边栏导航:从零到一的专业实践
  • 小红书二面:Function Calling 的可靠性怎么保证?
  • Jetson Linux 系统刷写常见依赖缺失报错排查指南
  • 模型选择的罗盘:AIC、BIC、FPE、LILC四大信息准则深度解析
  • 编译原理实战:从正则表达式到最小化DFA的完整构建与可视化
  • Wwise音频处理完整指南:从游戏音效解包到自定义替换的终极解决方案
  • 基于机器学习的智能告警分流系统:从特征工程到实战部署
  • 从MC1496乘法器到DSB调制:一个经典电路的设计实践与参数解析
  • 创业团队如何借助Taotoken统一管理多个AI项目的API成本
  • SpringBoot Actuator端点安全:从信息泄露到RCE的攻防实战
  • JoyCon-Driver深度解析:Windows平台任天堂Switch手柄驱动的完整实现方案
  • 告别PuTTY!用MobaXterm搞定Ubuntu远程连接与文件互传(保姆级图文教程)