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

STM32F103实战指南(11):DMA+串口空闲中断实现高效数据接收

1. 为什么需要DMA+串口空闲中断?

在嵌入式开发中,串口通信是最基础也最常用的功能之一。但传统的串口接收方式存在两个明显的痛点:一是需要CPU频繁参与数据搬运,二是处理不定长数据时逻辑复杂。我曾经在一个智能家居网关项目中使用普通串口中断接收传感器数据,结果发现当数据量增大时,CPU使用率直接飙升到70%以上,严重影响了其他任务的实时性。

DMA(直接内存访问)就像你请了个专职快递员。当串口收到数据时,DMA会自动把数据搬运到指定内存区域,完全不需要CPU插手。而串口空闲中断则像是个聪明的门卫,当检测到总线空闲超过1个字符时间时,就会触发中断通知我们:"这批货已经收完了"。实测下来,这种组合方案能让CPU占用率降低90%以上。

2. 硬件配置实战

2.1 引脚与时钟配置

以STM32F103C8T6为例,我们需要先初始化USART1的硬件资源。记得有次调试时忘了开启GPIO时钟,结果折腾了半天才发现是这么低级的问题:

// 使能GPIO和USART1时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, ENABLE); // 配置PA9(TX)为推挽复用输出 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &GPIO_InitStructure); // 配置PA10(RX)为浮空输入 GPIO_InitStructure.GPIO_Pin = GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, &GPIO_InitStructure);

2.2 串口参数设置

波特率建议使用115200,这个速率在大多数场景下都够用。注意数据位要设为8位,这是最常见的情况:

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(USART1, &USART_InitStructure);

3. DMA配置详解

3.1 DMA通道选择

STM32F103的DMA1有7个通道,不同通道对应不同外设。USART1_RX对应的是DMA1通道5,这个映射关系一定要记准。我曾经因为搞错通道号导致数据死活收不到:

// 使能DMA1时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 配置DMA1通道5(USART1_RX) DMA_DeInit(DMA1_Channel5); DMA_InitStructure.DMA_PeripheralBaseAddr = (uint32_t)&USART1->DR; DMA_InitStructure.DMA_MemoryBaseAddr = (uint32_t)rx_buffer; DMA_InitStructure.DMA_DIR = DMA_DIR_PeripheralSRC; DMA_InitStructure.DMA_BufferSize = 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_Init(DMA1_Channel5, &DMA_InitStructure);

3.2 双缓冲技巧

为了防止数据覆盖,我通常会使用双缓冲机制。定义两个缓冲区交替使用:

uint8_t rx_buf1[BUF_SIZE]; uint8_t rx_buf2[BUF_SIZE]; uint8_t *current_buf = rx_buf1;

当DMA完成一个缓冲区的填充后,立即切换到另一个缓冲区,同时处理已满的数据。这个技巧在高速数据传输时特别管用。

4. 空闲中断实现

4.1 中断配置

串口空闲中断需要配合DMA使用,配置顺序很重要。先开DMA再开空闲中断,这个坑我踩过:

// 使能DMA接收 USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE); DMA_Cmd(DMA1_Channel5, ENABLE); // 配置空闲中断 USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); // 配置NVIC NVIC_InitStructure.NVIC_IRQChannel = USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 0; NVIC_InitStructure.NVIC_IRQChannelSubPriority = 0; NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE; NVIC_Init(&NVIC_InitStructure);

4.2 中断服务函数

在中断服务函数中,我们需要做三件事:清除空闲标志、计算接收长度、重启DMA。特别注意要先读SR再读DR才能清除空闲中断标志:

void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_IDLE) != RESET) { // 必须读取SR和DR来清除空闲中断标志 USART1->SR; USART1->DR; // 停止DMA并计算接收长度 DMA_Cmd(DMA1_Channel5, DISABLE); uint16_t len = BUF_SIZE - DMA_GetCurrDataCounter(DMA1_Channel5); // 处理数据 if(len > 0) { process_data(current_buf, len); } // 切换缓冲区并重启DMA current_buf = (current_buf == rx_buf1) ? rx_buf2 : rx_buf1; DMA_SetCurrDataCounter(DMA1_Channel5, BUF_SIZE); DMA1_Channel5->CMAR = (uint32_t)current_buf; DMA_Cmd(DMA1_Channel5, ENABLE); } }

5. 常见问题排查

5.1 数据接收不全

遇到数据截断时,首先检查DMA缓冲区大小是否足够。其次确认波特率是否匹配,可以用示波器测量实际波特率。我遇到过因为晶振精度不够导致115200波特率实际是111k左右的情况。

5.2 中断不触发

如果空闲中断不触发,按这个顺序检查:

  1. 确认USART_ITConfig正确调用了
  2. 检查NVIC优先级配置
  3. 确保DMA没有关闭
  4. 用逻辑分析仪看总线是否真的出现了空闲状态

5.3 数据错位

当出现数据错位时,通常是DMA缓冲区切换时机不对导致的。建议在切换缓冲区前先检查DMA传输是否完成,必要时加入简单的校验机制如CRC校验。

6. 性能优化技巧

6.1 内存对齐

将DMA缓冲区按4字节对齐可以提升传输效率:

__align(4) uint8_t rx_buffer[BUF_SIZE];

6.2 合理设置优先级

DMA通道优先级要高于串口中断优先级,否则可能出现数据覆盖。我的经验值是:

  • DMA优先级:最高
  • 串口空闲中断:中
  • 其他外设中断:低

6.3 动态调整缓冲区

对于数据量变化大的场景,可以实现动态缓冲区大小。当检测到频繁的数据溢出时,自动增大缓冲区尺寸:

if(overflow_count > 3) { BUF_SIZE *= 2; // 重新初始化DMA... }

7. 实际应用案例

在工业传感器网络中,我使用这套方案实现了同时处理8路串口数据。每路串口配置独立的DMA通道和双缓冲,主循环只需要处理业务逻辑,完全不用操心数据接收。实测即使在所有串口全速工作时,CPU占用率也不到10%。

有个特别实用的技巧是在数据包头加入时间戳。当在中断服务函数中获取到数据后,立即记录当前SysTick值,这样后续处理时就能知道数据的精确接收时间:

typedef struct { uint32_t timestamp; uint8_t data[PAYLOAD_SIZE]; } sensor_packet_t;

这套方案经过多个项目验证,稳定性非常好。即使在强电磁干扰的工业环境下,配合简单的校验机制也能保证数据可靠传输。关键是要做好错误恢复机制,当检测到异常时能自动重置DMA和串口状态。

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

相关文章:

  • 树莓派4B与STM32 RT1064串口通信实战:从硬件连线上位机调试全流程
  • 20254111周笑凡 2025-2026-2 《Python程序设计》实验1报告
  • 探索Bayes-HKELM多输出回归:MATLAB实战
  • Windows 7 SP2焕新体验:让经典系统重获现代硬件适配能力
  • 模拟IC设计避坑:手把手教你用Cadence Virtuoso仿真时钟馈通效应(附减小误差的3个实用技巧)
  • MiniMax Token Plan 邀请码
  • MySQL 多表连接查询实战:内连接 + 外连接
  • 从零开始:Ubuntu 18.04上HBase 2.1.1伪分布式环境搭建全流程(含常见错误修复)
  • 【忍者算法】394 字符串解码:遇到嵌套时,栈最像“现场保存器”
  • ESXi主机添加必看:解决vCenter Server版本不兼容和HA报警的5个技巧
  • LVGL+FreeRTOS实战项目:智能健康助手(GUI设计与数据可视化篇)
  • 单片机例程之电子琴
  • 保姆级教程:用FreeRTOS在ESP32上管理DHT22和MQ-135,实现多传感器稳定采集与低功耗
  • 数字孪生:工业4.0的智能引擎,如何驱动制造业高效转型
  • React Native Material Design 最佳实践:避免常见陷阱的10个技巧
  • AIGC内容创作流水线:Qwen3-ASR-0.6B赋能语音素材自动化文本化
  • day10-数据结构力扣
  • Fugu14越狱指南:如何在iOS 14设备上实现完美越狱体验 [特殊字符]
  • 回顾方法
  • Presenton:如何用本地AI重新定义演示文稿创作的三重革命?
  • 2025版等离子体期刊分区解析:从PRL到PPAP的投稿指南
  • DeepSeek总结的 pg_duckpipe:2026年3月新特性
  • 3款PCB文件查看工具深度解析:OpenBoardView如何突破电路可视化行业痛点
  • 如何让OpenClaw多Agent协作架构更高效?
  • 计算机组成原理实战解析:CPU与存储器的连接及Cache设计关键问题
  • Java基础篇
  • 【由浅入深探究langchain】第十七集-构建你的首个 RAG 知识库助手(从文档索引到检索增强生成)
  • Joy-Con Toolkit:重新定义任天堂手柄的技术边界
  • 2026年教室灯市场新宠:这些品牌你了解吗?行业内教室灯有哪些推荐企业引领行业技术新高度 - 品牌推荐师
  • RexUniNLU效果展示:短视频弹幕‘求资源’‘打假’‘催更’等社区意图零样本识别