尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
STM32双串口DMA互透传:从能跑到跑得稳的实战指南
简介这份资源面向STM32嵌入式开发者尤其是需要提升串口通信效率的初学者与中级工程师核心解决传统中断或轮询方式在连续、不定长数据收发时CPU占用高、实时性差的问题。资源围绕UART1与UART2双串口通过DMA实现数据互透传涵盖串口初始化、DMA通道配置、中断服务函数编写、HAL_UART_Transmit_DMA与HAL_UART_Receive_DMA调用以及HAL_UART_ErrorCallback错误处理等关键环节帮助读者掌握外设与内存直接交换数据、降低CPU负担的实用技巧。压缩包共143个文件约3.62MB以c与h源码、o与d编译中间文件、crf与axf等工程产物为主另含ioc、uvprojx等CubeMX与Keil工程配置便于直接打开复现与二次开发。目前已有1872人学习下载适合作为双串口DMA透传的参考工程与排错对照。1. 双串口 DMA 互透传为什么“能跑”和“跑得稳”是两码事很多人第一次做双串口透传思路都很直接串口1中断收一个字节往串口2的发送寄存器里塞一个字节反过来也一样。在波特率9600、数据量小的时候这套逻辑确实能跑甚至跑得挺欢。但只要把波特率拉到115200以上或者两边同时开始收发你就会发现丢包、卡死、数据错位全来了。问题不在代码逻辑在于你把CPU当成了搬运工而搬运速度根本跟不上串口的节奏。STM32CUBE 双串口 DMA 互透传要解决的就是这件事让DMA当搬运工CPU只负责配置和异常处理。串口1收到的数据通过DMA自动搬到内存缓冲区内存缓冲区的数据再通过DMA自动搬到串口2的发送寄存器反方向同理。整个过程CPU几乎不参与只在半满/全满中断里做一次缓冲区切换。这套方案适合做协议转换网关、双机通信中继、调试口转发或者任何需要两个串口之间透明传输数据的场景。如果你正在用中断方式硬扛高波特率透传这篇文章就是写给你的。2. 先把DMA和串口的配合关系理清楚从外设请求到缓冲区切换2.1 串口DMA请求是怎么产生的STM32的USART外设内部有两个关键标志位RXNE接收数据寄存器非空和TXE发送数据寄存器空。当DMA使能后这两个标志不再触发中断而是直接产生DMA请求。接收方向上每收到一个完整字节RXNE置位DMA控制器自动从USART_DR寄存器读走数据写到你指定的内存地址然后地址指针自增。发送方向上当TXE置位DMA控制器自动从内存读一个字节写入USART_DR地址指针同样自增。这里有个容易忽略的细节DMA的传输完成中断和串口的空闲中断是两回事。DMA传输完成中断只在DMA搬完你设定的数据量时触发一次而串口空闲中断IDLE是在总线空闲一个字节时间后触发。做透传时如果只靠DMA传输完成中断你无法知道对方什么时候发完了一帧数据。常见做法是开启串口空闲中断在IDLE中断里读取DMA剩余传输数量算出这一帧实际收了多少字节然后启动发送DMA。这个组合是双串口透传的核心。2.2 缓冲区设计双缓冲还是环形缓冲透传场景下缓冲区设计直接决定稳定性。最简单的做法是给每个方向分配一个固定大小的数组DMA循环模式收收到一半触发半满中断收满触发全满中断。但循环模式下DMA会一直覆盖缓冲区如果CPU来不及处理旧数据就被冲掉了。更稳妥的方案是双缓冲准备两个缓冲区A和BDMA填A的时候CPU处理BDMA填B的时候CPU处理A。STM32的DMA支持双缓冲模式DBM在CUBE里配置DMA为循环模式开启双缓冲设置两个内存地址。但双缓冲模式对透传来说有个问题两个缓冲区大小必须一致且切换时机由硬件控制不够灵活。我一般会用环形缓冲加DMA半满/全满中断的方案。具体做法是定义一个大小为2的幂次的环形缓冲区DMA以循环模式往里面写半满中断时处理前半段全满中断时处理后半段。这样CPU有充足的时间处理数据而且缓冲区大小可以根据波特率和处理速度灵活调整。下面是一个典型的配置结构// 串口1接收DMA缓冲区大小256字节2的幂次便于取模 #define UART1_RX_BUF_SIZE 256 uint8_t uart1_rx_buf[UART1_RX_BUF_SIZE]; // 串口2接收DMA缓冲区 #define UART2_RX_BUF_SIZE 256 uint8_t uart2_rx_buf[UART2_RX_BUF_SIZE]; // 透传方向标志用于区分数据来源 typedef enum { DIR_UART1_TO_UART2 0, DIR_UART2_TO_UART1 } transfer_dir_t;参数说明缓冲区大小选256是因为它既是2的幂次又能容纳115200波特率下约22毫秒的数据量。在22毫秒内即使主频72MHz的STM32F103也能执行上百万条指令处理时间绰绰有余。如果你的主频更低或者波特率更高把缓冲区加大到512或1024即可。2.3 CUBE里的DMA配置要点在STM32CUBE里配置双串口DMA有几个地方容易翻车。第一DMA请求要选对USART1_RX对应DMA1_Channel5USART1_TX对应DMA1_Channel4USART2_RX对应DMA1_Channel6USART2_TX对应DMA1_Channel7。这是F103系列的固定映射选错了DMA根本不工作。第二DMA模式要选循环模式Circular不是普通模式。普通模式下DMA搬完设定数量就停了透传需要持续搬运必须用循环。第三优先级设置如果两个串口同时收发建议把接收DMA优先级设为高发送设为中避免接收数据被发送操作阻塞导致溢出。第四中断使能接收DMA要开半满中断和全满中断发送DMA开传输完成中断。串口本身要开空闲中断IDLE这个在CUBE的NVIC里找不到需要在代码里手动使能。下面是初始化后的手动配置代码// 使能串口1空闲中断用于检测一帧数据结束 __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE); // 使能串口2空闲中断 __HAL_UART_ENABLE_IT(huart2, UART_IT_IDLE); // 启动串口1接收DMA循环模式 HAL_UART_Receive_DMA(huart1, uart1_rx_buf, UART1_RX_BUF_SIZE); // 启动串口2接收DMA HAL_UART_Receive_DMA(huart2, uart2_rx_buf, UART2_RX_BUF_SIZE);逻辑说明HAL_UART_Receive_DMA会配置DMA并启动接收但不会自动开启空闲中断所以需要手动调用__HAL_UART_ENABLE_IT。空闲中断的使能位在USART_CR1寄存器的IDLEIE位HAL库没有提供直接的宏用这个宏可以设置。启动DMA接收后数据会自动填入缓冲区CPU不需要干预。3. 透传逻辑实现空闲中断里算长度DMA负责搬运3.1 空闲中断的处理流程空闲中断是透传的“帧边界检测器”。当串口总线空闲一个字节时间后IDLE标志置位触发中断。在中断服务函数里你需要做三件事清除IDLE标志、计算本次接收了多少字节、启动发送DMA。计算接收字节数的公式是已接收字节数 缓冲区总大小 - DMA剩余传输数量。DMA的CNDTR寄存器保存着剩余要传输的数量用__HAL_DMA_GET_COUNTER宏可以读取。下面是一个完整的空闲中断处理函数void USART1_IRQHandler(void) { // 检查空闲中断标志 if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { // 清除空闲中断标志先读SR再读DR __HAL_UART_CLEAR_IDLEFLAG(huart1); // 停止当前接收DMA准备重新配置 HAL_UART_DMAStop(huart1); // 计算本次接收到的数据长度 uint16_t recv_len UART1_RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(huart1.hdmarx); // 如果收到数据启动串口2的发送DMA if (recv_len 0) { HAL_UART_Transmit_DMA(huart2, uart1_rx_buf, recv_len); } // 重新启动串口1接收DMA准备下一帧 HAL_UART_Receive_DMA(huart1, uart1_rx_buf, UART1_RX_BUF_SIZE); } // 调用HAL库的默认中断处理 HAL_UART_IRQHandler(huart1); }逻辑说明先判断IDLE标志清除它然后停止DMA。停止DMA是为了读取准确的剩余传输数量因为DMA在运行中CNDTR会不断变化。算出recv_len后把uart1_rx_buf里的数据通过串口2的DMA发出去。最后重新启动串口1的接收DMA等待下一帧。注意HAL_UART_DMAStop会同时停止接收和发送DMA如果串口1同时在发送数据这个操作会打断发送。更精细的做法是只停止接收DMA用HAL_UART_AbortReceive代替。3.2 发送DMA的启动与完成处理发送DMA启动后数据会自动从内存搬到串口发送寄存器。发送完成后DMA传输完成中断触发在中断里你需要做两件事清除标志、通知上层发送完成。对于透传来说发送完成中断里通常不需要做额外操作因为数据已经发出去了。但如果你需要统计发送字节数或者做流控这里是个合适的切入点。void DMA1_Channel7_IRQHandler(void) { // 检查串口2发送DMA传输完成标志 if (__HAL_DMA_GET_FLAG(huart2.hdmatx, DMA_FLAG_TC7)) { // 清除标志 __HAL_DMA_CLEAR_FLAG(huart2.hdmatx, DMA_FLAG_TC7); // 发送完成可以在这里做统计或状态切换 // 例如g_uart2_tx_done 1; } HAL_DMA_IRQHandler(huart2.hdmatx); }参数说明DMA_FLAG_TC7对应DMA1通道7的传输完成标志具体通道号根据你用的串口查参考手册。HAL_DMA_IRQHandler会处理HAL库内部的状态机必须调用否则HAL_UART_Transmit_DMA的后续调用会出错。3.3 双向透传的完整数据流把两个方向拼起来完整的数据流是这样的串口1收到数据DMA自动填入uart1_rx_buf空闲中断触发算出长度启动串口2发送DMA数据从uart1_rx_buf搬到串口2的DR寄存器发送完成中断触发。反方向同理串口2收到数据填入uart2_rx_buf空闲中断触发启动串口1发送DMA。这里有个资源冲突问题如果串口1正在发送数据DMA占用中同时串口1又收到了数据空闲中断里调用HAL_UART_Transmit_DMA会返回HAL_BUSY导致这一帧数据丢失。解决办法是给每个方向加一个发送队列或者用双缓冲让接收和发送使用不同的缓冲区。我一般会在空闲中断里判断发送DMA是否忙如果忙就把数据暂存到另一个缓冲区等发送完成中断里再启动下一次发送。// 发送忙标志 volatile uint8_t uart2_tx_busy 0; // 在空闲中断里判断 if (recv_len 0) { if (uart2_tx_busy 0) { uart2_tx_busy 1; HAL_UART_Transmit_DMA(huart2, uart1_rx_buf, recv_len); } else { // 发送忙数据暂存到备用缓冲区 memcpy(uart2_tx_backup, uart1_rx_buf, recv_len); uart2_tx_backup_len recv_len; } } // 在发送完成中断里检查备用缓冲区 if (uart2_tx_backup_len 0) { HAL_UART_Transmit_DMA(huart2, uart2_tx_backup, uart2_tx_backup_len); uart2_tx_backup_len 0; } else { uart2_tx_busy 0; }这段代码解决了发送冲突问题但引入了新的复杂度备用缓冲区也可能被覆盖。如果数据量特别大建议用环形队列管理发送数据而不是简单的备用缓冲区。环形队列的实现这里不展开核心思路是生产者空闲中断往队列尾写消费者发送完成中断从队列头读。4. 避坑排查透传丢包、卡死、数据错位的五个血泪经验4.1 现象低速能跑高速丢包原因中断方式下CPU处理速度跟不上串口接收速度。115200波特率下每个字节间隔约87微秒如果中断服务函数执行时间超过87微秒下一个字节就会覆盖上一个字节。解决改用DMA接收CPU只在空闲中断里处理整帧数据。同时检查中断优先级串口中断优先级要高于其他非关键中断。如果用了RTOS把串口中断优先级设到configMAX_SYSCALL_INTERRUPT_PRIORITY以上。4.2 现象第一帧正常后续帧数据错位原因空闲中断里没有重新启动接收DMA或者DMA停止后没有正确复位。HAL_UART_DMAStop会复位DMA的CNDTR寄存器但如果你手动操作寄存器而没有重新配置DMA会从错误的位置开始写。解决每次空闲中断处理后必须重新调用HAL_UART_Receive_DMA确保DMA指针回到缓冲区起始位置。如果用的是HAL_UART_AbortReceive也要重新启动接收。4.3 现象串口发送一半卡死DMA传输完成中断不触发原因DMA传输完成标志没有清除或者HAL_DMA_IRQHandler没有被调用。HAL库的DMA状态机依赖中断里的标志清除操作如果中断服务函数里只清了自己的标志而没调用HAL_DMA_IRQHandlerHAL库内部状态会卡在BUSY。解决在DMA中断服务函数里先清除标志再调用HAL_DMA_IRQHandler。顺序不能反否则HAL库可能重复处理同一个标志。4.4 现象两个串口同时收发时其中一个串口无响应原因DMA通道优先级配置不当高优先级通道一直占用总线低优先级通道得不到服务。或者两个串口共用了同一个DMA控制器而DMA控制器同一时间只能服务一个通道。解决在CUBE里把接收DMA优先级设为High发送设为Medium。如果两个串口的数据量都很大考虑用DMA1和DMA2分别服务两个串口如果芯片支持。F103系列DMA1有7个通道DMA2有5个通道合理分配可以避免冲突。4.5 现象空闲中断触发过于频繁CPU占用率高原因空闲中断的触发条件是总线空闲一个字节时间。如果对方发送的数据帧之间间隔很短空闲中断会频繁触发每次都要停止/启动DMA开销不小。解决适当增大DMA缓冲区让DMA在半满或全满中断里处理数据而不是依赖空闲中断。或者设置一个超时阈值空闲中断触发后延时一小段时间再处理合并短帧。但延时处理会引入延迟需要根据实际场景权衡。5. 进阶技巧用DMA双缓冲和空闲中断超时做零丢包透传5.1 DMA双缓冲模式在透传中的正确用法STM32的DMA双缓冲模式DBM允许你设置两个内存地址DMA在填满第一个缓冲区后自动切换到第二个同时触发传输完成中断。在透传场景下这个特性可以用来实现“接收不停止”的效果DMA一直在两个缓冲区之间交替填充CPU在中断里处理已经填满的那个缓冲区另一个缓冲区继续接收。配置双缓冲需要在CUBE里把DMA模式设为循环然后在代码里手动设置第二个内存地址// 启用DMA双缓冲模式 huart1.hdmarx-Instance-CR | DMA_CCR_DBM; // 设置第二个内存地址 huart1.hdmarx-Instance-CMAR2 (uint32_t)uart1_rx_buf2; // 重新启动DMA HAL_UART_Receive_DMA(huart1, uart1_rx_buf, UART1_RX_BUF_SIZE);注意HAL库对双缓冲模式的支持不完整HAL_UART_Receive_DMA只会设置第一个内存地址。上面的代码直接操作寄存器来启用双缓冲但HAL库的中断处理函数可能无法正确识别双缓冲的传输完成事件。更稳妥的做法是绕过HAL库直接配置DMA寄存器自己写中断处理。这超出了本文范围但思路是在DMA传输完成中断里读取当前DMA正在使用的内存地址通过CT位判断处理另一个缓冲区。5.2 空闲中断超时机制解决短帧频繁触发空闲中断在数据帧间隔很短时会频繁触发每次触发都要停止/启动DMA开销不小。一个优化方案是在空闲中断里启动一个定时器延时一段时间后再处理数据。如果在这段时间内又有新数据到达重置定时器。这样可以把多个短帧合并成一个长帧处理。// 在空闲中断里启动超时定时器 void USART1_IRQHandler(void) { if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); // 启动定时器10ms后处理 HAL_TIM_Base_Start_IT(htim6); } HAL_UART_IRQHandler(huart1); } // 定时器中断里处理数据 void TIM6_IRQHandler(void) { HAL_TIM_IRQHandler(htim6); // 停止定时器 HAL_TIM_Base_Stop_IT(htim6); // 处理接收数据 process_uart1_data(); }参数说明超时时间根据实际数据帧间隔设置。如果帧间隔在1ms以内超时设5-10ms可以合并大部分短帧。但超时时间越长透传延迟越大。对于实时性要求高的场景不建议用超时合并。5.3 验证透传稳定性的方法透传做完了怎么验证它稳不稳我一般用三个测试第一用串口助手以最高波特率连续发送随机数据持续10分钟检查接收端数据是否完全一致。第二两个串口同时以最高波特率对发观察是否丢包。第三发送特定模式的数据如0x00到0xFF循环用逻辑分析仪抓取两个串口的波形对比时序和内容。如果测试中发现丢包先检查DMA缓冲区大小是否足够再检查中断优先级是否合理最后检查是否有其他地方禁用了全局中断导致DMA请求被延迟响应。透传的稳定性取决于最慢的那个环节任何一个细节没处理好都会成为瓶颈。从那以后我每次做双串口透传都会先把DMA缓冲区和中断优先级表画出来确认每个环节的余量再开始写代码。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

Python 3.13 中文离线文档:本地部署与搜索配置指南

Python 3.13 中文离线文档:本地部署与搜索配置指南

简介:这份《python 3.13版本中文文档资料》面向希望系统学习或查阅 Python 3.13 的中文开发者,尤其适合需要离线环境学习的新手与需要跟进新特性的进阶程序员。资源以 HTML 离线文档为主体,无需联网即可在浏览器中打开,覆盖基础语…

📅 2026/10/17 3:08:59
OpenCV与YOLOv8目标检测实战:从图像处理到模型部署的完整笔记

OpenCV与YOLOv8目标检测实战:从图像处理到模型部署的完整笔记

剩余课程笔记终于趁周末补完了,连带OpenCV课程笔记和YOLOv8目标检测实战任务的总结,今天一起整理出来发掉。这门课是某高校的《计算机视觉》,前半程讲OpenCV图像处理,后半程讲深度学习目标检测,最后一组实战任务是训练…

📅 2026/10/17 3:08:59
世界国家GeoJSON大全:Python校验与ECharts地图可视化实战

世界国家GeoJSON大全:Python校验与ECharts地图可视化实战

简介:面向地理信息系统开发者和地图可视化场景的世界国家地理边界数据包,整合了来自自然地球的全球国家与地区边界多边形,适用于网页地图、桌面地理信息系统、数据分析和可视化大屏等应用。资源以JSON格式文本为主体,共含三千四百…

📅 2026/10/17 3:03:59
MORE NEWS

更多资讯

📰

电商小程序h5st签名机制解析:原理、风险与合规联调指南

1. 从一次接口联调说起:购物小程序里的那串神秘签名做前端或客户端开发的同行,大概率都遇到过这样一种场景:你在调试一个电商购物小程序的接口时,明明参数都带齐了,请求却莫名其妙返回一串错误码,提示"…

📰

AI Agent任务调度实战:规划、执行与并发控制的完整链路设计

1. 先说清楚:Agent里的"调度"到底调度什么1.1 从普通程序到Agent的调度差异很多刚开始接触AI Agent的开发者,第一反应是把它当成一个复杂点的API调用流程:用户说一句话,系统调一次大模型,把结果返回。真去实…

📰

两串电芯被动均衡方案:MP2642GR-Z与MK51DN512CLQ10实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

危险报文识别实战:从告警日志到分层过滤与规则引擎

1. 从一条告警日志说起:危险报文识别到底在防什么凌晨两点,安全运营中心的告警群弹出一条消息:某业务接口在十分钟内出现了大量带有异常编码特征的请求体,WAF(Web应用防火墙)已经拦截了一部分,但…

📰

拼多多客服机器人开发实战:从Webhook接入到自动回复

简介:面向拼多多商家的智能客服机器人解决方案,基于官方平台插件接入,可稳定处理大量用户咨询,支持人机全程协作,并在语义理解、情感分析等AI能力辅助下提升回复质量与客户体验。资源包共18个文件,压缩后约…

📰

VS Code远程SSH开发C++环境搭建指南

1. 项目概述:为什么一个“VS Code SSH C编译环境”值得花两小时认真搭好你有没有过这种体验:手头有一台性能不错的远程服务器(可能是实验室集群、公司开发机,也可能是自己租的云主机),上面跑着Linux系统&…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬