ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32双串口DMA透传:架构设计、配置与踩坑实录

2026/9/1 17:02:14 拓冰建站 浏览量
STM32双串口DMA透传:架构设计、配置与踩坑实录 简介面向嵌入式开发者的STM32双串口DMA透传项目源码聚焦工业控制、智能网关、嵌入式调试中常见的串口数据透明转发场景利用直接内存访问与空闲中断机制构建接近零CPU占用的双向桥接方案。深入讲解USART与DMA的组合配置、双缓冲机制、IDLE空闲中断处理帧边界的方法以及实际项目中的内存优化和异常处理对策并在STM32F407平台实测获得2Mbps波特率双向透传、CPU占用低于百分之三、数据完整率接近百分之百的成绩。压缩包共3个文件以HTML说明文档、inscode工程配置和gitignore版本管理文件为主体积仅7KB结构精简既适合快速阅读原理讲解也方便导入IDE查看配置细节。目前已有102人学习下载对于希望掌握低CPU占用串口透传、优化数据转发链路的开发者而言是一份轻量而实用的学习样本尤其有助于理解DMA通道分配、双缓冲切换和帧边界识别等落地难点。同时这套源码中的HTML文档可配合双缓冲与IDLE中断的代码片段快速理解接收缓冲区自动切换与帧边界识别机制帮助在真实项目中减少丢包、降低中断频率。 串口透传在嵌入式项目里是特别高频的需求。前阵子翻到一个旧项目的源码当时给一套老设备做数据对接设备只有串口输出主机那边也指着串口通信中间必须加一块STM32做桥接串口1收到的数据原封不动从串口2出去串口2收到的数据也原封不动从串口1出去一来一回全双工CPU还不许在字节搬运上耗时间。最后定下来的方案就是STM32双串口DMA透传。整套代码加起来不到两百行实现过程中倒是踩了几个坑这篇把设计思路、DMA配置、空闲中断判帧、踩坑记录都写出来。适合刚入手STM32 DMA的同学参考也适合想直接复用一套双串口中继代码的工程师。1. 为什么需要双串口DMA透传解决哪类现场问题1.1 双串口透传是干什么的先明确“透传”的意思。它不是一个协议而是一种桥接策略一头进来一头出去数据不解析、不改动、不做应答仅仅原样搬运。这类需求在工程现场很常见我随便举几个场景老设备没有网络模块外接一块带WiFi模组的STM32板卡把设备的串口数据透传到服务器服务器下发指令时再透传回设备两个模块之间通信电平、协议格式互不兼容中间用STM32做物理层桥接比如把UART转成另一路UART中间加隔离或电平转换调试阶段让单板当“调试桥”串口2接PC上位机串口1接目标设备PC上的配置工具通过这个桥直接操作目标设备。这些需求的共同点是数据双向流动而且两侧波特率可能不同。双串口DMA透传的核心价值在于CPU只在“一帧开始/结束时”做少量处理字节级别的搬运全程由DMA控制器完成这样主循环还能腾出手干别的。1.2 为什么是DMA而不是普通中断串口数据接收经典做法有三种轮询、串口中断、DMA。轮询适合CPU闲着、数据量又少的场合缺点是会卡死主循环串口中断每收到一个字节就跳一次中断115200bps下每秒大约1.15万字节CPU要反复进出中断高负载时很容易丢数据DMA就完全不同串口数据到了之后DMA控制器直接按配置把数据搬到内存CPU不参与等DMA计数满了或者串口空闲CPU再过来处理一次。方式CPU开销实时性适合场景轮询高持续占CPU一般低频、小数据量普通中断中每字节进一次高数据量适中、帧短DMA空闲中断极低每帧进一次高不定长数据帧、高波特率、长数据透传本质上就是字节搬家如果把CPU按在数据搬运上那其他任务就没时间跑了。DMA让“搬家”不再是CPU的负担这是它在透传场景里成为首选的根本原因。2. 透传架构与数据流设计2.1 单向透传还是全双工透传如果只做“串口1收→串口2发”的单向转发代码很简单一个DMA通道就够。但实际操作里几乎没有设备只收不发。主机要发指令给设备设备要回数据给主机两条数据流是并行的所以必须做全双工透传。全双工方案在STM32上需要4个DMA通道串口1的RX、串口1的TX、串口2的RX、串口2的TX。每条RX通路独立接收收到一帧后交给对端串口的TX通路发送。这4个通道之间互不干扰真正需要设计的是“同一时刻串口2还在发上一帧串口1又收到了新帧”这种重入问题。2.2 缓冲方案帧转发 vs 环形缓冲双串口透传的缓冲方案业界常用两种做法帧转发串口空闲中断判定一帧结束直接把这一帧的数据交给另一路串口的DMA发送。优点是实现简单、实时性高适合命令/响应式交互、帧间有自然间隔的场景。缺点是两侧都是高速连续数据流时处理不过来会丢帧。环形缓冲区DMA不断把数据写入循环缓冲区CPU在空闲时维护读写指针满了就覆盖或置溢出标志。适合连续大数据流但代码复杂还要处理指针同步和内存屏障问题。我这个项目选的是帧转发。原因很现实绝大多数串口透传场景是主机发一条命令、设备回一条应答数据流本身是断续的。帧转发代码量小最容易理解和移植。如果以后遇到大流量连续数据只需要把“发送前等待完成”那段改成“写入环形缓冲后台任务读取发送”架构不用大改。3. 基于STM32CubeMXHAL库的完整实现3.1 CubeMX配置要点代码基于STM32F103C8T6HAL库是F1标准HAL其他芯片型号的配置逻辑基本一致。工程先用CubeMX生成基础配置主要设置项如下打开USART1和USART2均为异步模式波特率根据实际设备定比如115200USART1的RX配置为DMA1_Channel5TX配置为DMA1_Channel4USART2的RX配置为DMA1_Channel6TX配置为DMA1_Channel7。不同芯片型号的通道号会不同以CubeMX自动分配为准两个串口的DMA模式都选择Circular循环模式数据宽度都是ByteNVIC里使能USART1和USART2全局中断DMA中断按需开启。这套方案里接收靠串口空闲中断完成RX DMA可以不进中断发送完成如果需要精确通知就开TX DMA中断时钟树要保证USART1挂在APB2、USART2挂在APB1重点是波特率要算准不然数据全乱。这里的细节是DMA Mode参数RX和TX都建议选Circular。RX用Circular是因为我们接收完一帧后通过复位计数器或重新启动继续接收TX用Circular不是让它无限发而是让该通道可以反复被DMA占用配合HAL_UART_Transmit_DMA调用时发送行为更可控。3.2 接收启动与全局变量在main函数初始化完外设后调用一个启动函数让两路串口都进入DMA接收状态#define RX_BUF_LEN 256 uint8_t uart1_rx_buf[RX_BUF_LEN]; uint8_t uart2_rx_buf[RX_BUF_LEN]; volatile uint8_t uart1_frame_ready 0; volatile uint16_t uart1_frame_len 0; volatile uint8_t uart2_frame_ready 0; volatile uint16_t uart2_frame_len 0; void Transparent_Start(void) { HAL_UART_Receive_DMA(huart1, uart1_rx_buf, RX_BUF_LEN); HAL_UART_Receive_DMA(huart2, uart2_rx_buf, RX_BUF_LEN); }两个串口的接收缓冲尽量定义成全局数组不要用局部变量或malloc。在嵌入式环境下DMA对内存地址有对齐要求全局数组编译器会放到合适的区域局部变量可能被优化到栈上或寄存器中位置不稳定很容易出现“接收地址错误”这种排查半天都找不到原因的诡异问题。3.3 串口空闲中断处理逻辑HAL库里接收DMA的“接收满”回调是HAL_UART_RxCpltCallback它只有在收满RX_BUF_LEN时才会触发。实际串口帧通常是不定长的我在项目里用的是“串口空闲中断”来判定一帧结束当串口在一段时间内没有新字节到来硬件会置IDLE标志此时一帧数据就收完了。在串口中断服务函数里加入IDLE判断void USART1_IRQHandler(void) { if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); HAL_UART_DMAStop(huart1); uart1_frame_len RX_BUF_LEN - __HAL_DMA_GET_COUNTER(huart1.hdmarx); uart1_frame_ready 1; // 重新启动接收前先解锁 __HAL_UNLOCK(huart1); HAL_UART_Receive_DMA(huart1, uart1_rx_buf, RX_BUF_LEN); } }这里有几个关键点__HAL_DMA_GET_COUNTER读的是DMA当前剩余传输个数配置为Byte宽度时剩余个数就是剩余字节数用缓冲区长度减去它得到本次实际收到的字节数HAL_UART_DMAStop会停止串口的DMA接收并把串口句柄状态置为ready。之后重新调用HAL_UART_Receive_DMA之前必须加__HAL_UNLOCK否则可能因为句柄被锁导致启动失败空闲中断处理函数尽量短不要在这里做耗时的转发逻辑只记录长度和标志把数据搬运放到主循环或回调里去完成避免阻塞其他中断。注意DMAStop之后立刻启动Receive_DMA存在缓冲区指针回到起点的问题。如果上一帧数据还没有被主循环处理完新数据就可能覆盖旧数据。稳妥的做法是“先标记ready主循环处理完再重新启动接收”或者直接使用双缓冲切换把接收缓冲分成两个区交替使用。3.4 数据转发与发送完成保护在main主循环里检测到某个串口有帧数据后把数据交给另一个串口发送while (1) { if (uart1_frame_ready) { uart1_frame_ready 0; Transmit_To_UART2(uart1_rx_buf, uart1_frame_len); } if (uart2_frame_ready) { uart2_frame_ready 0; Transmit_To_UART1(uart2_rx_buf, uart2_frame_len); } }发送函数的实现需要考虑重入问题。比如串口1刚收到一帧准备通过串口2发送但串口2此时可能正在发送上一帧若直接调用HAL_UART_Transmit_DMA会造成DMA缓冲区被覆盖、数据乱序。我用的处理是void Transmit_To_UART2(uint8_t *buf, uint16_t len) { while (HAL_UART_GetState(huart2) ! HAL_UART_STATE_READY) ; HAL_UART_Transmit_DMA(huart2, buf, len); }while等待发送完成的写法简单直接但如果主循环里任务很多建议改成发送完成标志加事件队列。例如在HAL_UART_TxCpltCallback回调中置一个“串口2发送完成”标志主循环发送前先检查标志是否置位。这样不会阻塞主循环也能避免覆盖未发完的数据。4. 常见问题与排查技巧实录4.1 高频问题速查表这些问题的排查顺序建议按“配置→时序→中断优先级”来走大部分故障都出在这三个环节。现象可能原因排查与解决只收到一帧就再也不进中断DMA停止后未重新启动或启动前串口句柄被锁检查是否重新调用了HAL_UART_Receive_DMA是否先执行了__HAL_UNLOCK接收长度不对经常固定为缓冲区大小错误依赖接收满回调或计数器读取时机不对改用IDLE中断在空闲标志出现后再读取DMA计数器数据乱码两侧波特率不一致、DMA数据宽度配置错误、时钟树不准核对CubeMX时钟树DMA宽度改成Byte两端波特率统一数据偶发丢失串口中断和DMA中断优先级相同IDLE中断被抢占设置串口全局中断优先级高于DMA中断程序卡死在HAL_UART_Transmit_DMA上一次DMA发送未完成又触发了新发送发送前检查状态或使用发送完成事件队列4.2 深入排查DMA句柄与串口句柄的关联HAL库中串口句柄内部保存了DMA句柄指针具体是huart1.hdmarx和huart1.hdmatx。DMA中断服务函数里拿到的DMA句柄到底对应哪个串口可以通过句柄地址来判断。我在调试时经常在HAL_DMA_IRQHandler里打断点看传入的hdma参数指向哪里再和huart1.hdmarx的地址比对基本就能确定是不是CubeMX里通道绑定错了。有朋友问过“DMA句柄和DAC句柄怎么关联”这类问题道理其实一样DMA句柄本质是DMA通道的配置结构体串口、DAC、ADC这些外设只是请求发起方各自通过句柄里的通道号、外设基址、传输方向绑定到DMA控制器。只要在CubeMX里把对应外设的请求通道选对HAL库会自动把DMA句柄挂到外设句柄上不需要手动关联。理解这一点很多“DMA不工作”的诡异问题都能迎刃而解。4.3 实测效果与优化建议实际测试时波特率115200、一帧数据最长256字节从串口1收完一帧到串口2开始发送时间开销在微秒级整条路径几乎就是DMA在搬数据。CPU主循环只是查两个标志位占用率可以忽略不计。想继续优化的话有几个方向把RX_DMA缓冲区和TX发送缓冲区设计成同一个收到帧后不需要拷贝直接让另一个串口的TX DMA指向同一个缓冲区省掉一次memcpy把主循环里的等待发送改成发送完成中断加队列提升多任务场景下的响应速度如果数据量大且帧间没有明显间隔切换成环形缓冲方案避免IDLE判定延迟导致丢包RX_BUF_LEN最好设置成“最大协议帧长度”设小了长帧被截断设大了内存浪费。做数据对接前先确认对端协议里最长的一帧是多少字节照着设就行。这套代码后来我在好几个项目里复用结构上基本没怎么改。最深的体会是串口DMA透传的难点不在DMA本身而在帧边界判定和DMA重启时序只要把这两个地方想明白代码就能跑得很稳。最后分享一个习惯调试时先用串口工具发固定报文例如AA 55 01 02 03观察转发出来的字节数和内容是否正确把基础行为验证牢了再上真实业务协议会省掉大量排查时间。本文还有配套的精品资源点击获取