资讯动态

STM32双串口DMA透传实现:零CPU占用、不丢包的工业级串口桥接方案

发布时间:2026/10/21 3:32:53 来源:尧图企业网站定制
简介本资源是基于STM32CubeMX与HAL库实现的双串口DMA互透传完整工程面向嵌入式初学者及STM32进阶开发者解决多串口高吞吐、低CPU占用场景下的实时数据转发难题适用于工业通信网关、协议转换器、传感器汇聚等典型应用。压缩包共143个文件含42个核心头文件.h与20个源文件.c涵盖HAL驱动层如stm32f1xx_hal_uart.c、stm32f1xx_hal_dma.c、工程配置.ioc、.uvprojx、编译输出.axf、.map、.lst及调试支持文件结构完整可直接编译下载运行。目前已有1872人学习下载。读者可获得可实测的双UARTDMA协同收发代码框架、半自动DMA接收中断处理逻辑、串口错误回调帧错/溢出的健壮性设计以及基于STM32F1系列的CubeMX初始化配置范例特别适合理解DMA与串口底层协同机制并快速复用于自有项目。1. 双串口DMA互透传不是“两个串口连起来就能通”而是让STM32在不占CPU、不丢数据、不翻车的前提下把UART1进来的字节流原样塞进UART2发出去反过来也成立——这项目专治串口透传场景下的“卡顿、乱码、丢包、主频被榨干”四大玄学症状适合做工业Modbus网关、传感器汇聚节点、协议转换桥接器的嵌入式开发者尤其当你手头那块STM32F407板子正因轮询收发把FreeRTOS任务调度拖成PPT时它就是你该立刻烧进去的后悔药。你可能试过用HAL_UART_Receive_IT HAL_UART_Transmit_IT做双串口转发结果发现一接上PLC就掉帧一加个printf调试就死机串口缓冲区溢出像呼吸一样自然。问题不在逻辑而在执行模型——中断里调用发送函数发送又触发新中断嵌套深了栈就飘更别说高波特率比如921600下每字节中断一次CPU光处理中断向量表都快忙不过来。而这个项目用的是DMA双缓冲空闲中断环形队列三件套UART1接收全程不进中断服务函数ISRUART2发送全程不等TXE标志位CPU在两次DMA传输间隙里还能顺手算个PID、读个ADC、喂个看门狗。它不炫技不堆功能就干一件事让两路串口像水管一样直通水数据进来多少出去多少中间不存、不改、不判、不卡。源码基于STM32CubeMX生成开箱即用但真正值钱的是它把DMA链表配置、空闲中断触发时机、环形缓冲区边界判断这些藏在HAL底层的黑匣子全摊开写在main.c和usart.c里——你看得懂改得动压得住。2. 工程结构与CubeMX配置从引脚分配到DMA通道映射的硬核对齐2.1 项目目录骨架与关键文件职责划分整个工程是标准的STM32CubeIDE/CubeMX生成结构无额外构建系统直接编译烧录。核心文件共5个全部位于Core/Src/下其余为CubeMX自动生成的初始化代码可忽略文件名职责说明是否需修改main.c主循环、DMA句柄初始化、空闲中断使能、透传状态机入口✅ 必改波特率、缓冲区大小usart.cUART1/UART2的DMA接收/发送配置、空闲中断回调HAL_UARTEx_RxEventCallback实现、环形缓冲区操作函数✅ 必读理解数据搬运路径dma.cDMA通道初始化仅配置不启动、内存到外设/外设到内存方向设置、循环模式禁用关键⚠️ 一般不动但需确认通道绑定stm32f4xx_hal_msp.cUART/DMA的底层时钟使能、GPIO复用配置、中断优先级设置必须≥DMA优先级✅ 检查否则空闲中断不触发ring_buffer.c纯C环形缓冲区实现rb_write()/rb_read()/rb_available()无锁设计依赖__disable_irq()临界区保护✅ 可替换如需线程安全版提示ring_buffer.c未使用CMSIS-RTOS API所有操作在中断上下文或主循环中完成避免引入调度开销。缓冲区大小默认为256字节若需支持长报文如Modbus TCP over Serial需同步修改#define RB_SIZE 256及DMA接收缓冲区数组长度。2.2 CubeMX关键配置四步法避坑前置CubeMX不是点点就完事以下四步配置错任何一项DMA透传必翻车UART引脚与模式UART1TX→PA9, RX→PA10Mode选AsynchronousDisable Hardware Flow ControlRTS/CTS会干扰DMA空闲检测UART2TX→PA2, RX→PA3同上Baud Rate此处不填具体值如115200留空实际波特率在main.c中通过huart1.Init.BaudRate 115200;动态设置CubeMX填了反而覆盖代码。DMA Request Mapping最易错UART1_RX → DMA2 Stream5 Channel4F4系列固定映射不可改UART1_TX → DMA2 Stream7 Channel4UART2_RX → DMA1 Stream5 Channel4UART2_TX → DMA1 Stream6 Channel4注意F407有DMA1/DMA2双控制器UART1挂DMA2UART2挂DMA1若在CubeMX里误将UART2_RX配到DMA2编译会通过但运行时DMA请求永不触发——因为硬件通道不存在。NVIC Interrupt Settings勾选USART1 global interrupt和USART2 global interrupt取消勾选DMA1 Stream5 global interrupt等DMA中断本项目不用DMA传输完成中断只用UART空闲中断UART中断优先级设为Preemption Priority 1数值越小优先级越高确保空闲中断能打断其他低优先级任务。Clock ConfigurationAPB2UART1时钟 ≥ 72MHzAPB1UART2时钟 ≥ 36MHz若使用HSI16MHz作为系统时钟源需在SystemClock_Config()中手动分频否则UART1波特率误差超±3%导致通信失败。2.3 DMA双缓冲机制为什么不用单缓冲单缓冲DMA接收HAL_UART_Receive_DMA(huart1, rx_buf, size)的问题在于当DMA填满整个缓冲区后必须靠HAL_UART_RxCpltCallback()通知CPU处理此时若CPU来不及清空缓冲区新数据就会覆盖旧数据OVERRUN。而本项目采用双缓冲空闲中断组合// usart.c 中定义 uint8_t uart1_rx_buf[2][RX_BUFFER_SIZE]; // 双缓冲buf[0] 和 buf[1] uint8_t current_buf_idx 0; // 启动DMA接收仅一次 HAL_UART_Receive_DMA(huart1, uart1_rx_buf[0], RX_BUFFER_SIZE); HAL_UART_Receive_DMA(huart1, uart1_rx_buf[1], RX_BUFFER_SIZE);逻辑是DMA先往buf[0]填填满前若线路空闲无新字节到达则触发空闲中断此时buf[0]中已有有效数据current_buf_idx仍为0我们立即将buf[0]数据搬入环形缓冲区并启动DMA继续往buf[1]填下次空闲中断来时current_buf_idx切为1处理buf[1]。这样接收永远在后台进行处理永远在空闲期完成零丢包。参数说明RX_BUFFER_SIZE建议设为128或256太小则频繁中断影响效率太大则空闲中断延迟高需等线路真正静默。实测Modbus RTU3.5字符时间空闲下128字节缓冲区可覆盖99%报文。3. 核心透传逻辑拆解从空闲中断到环形缓冲区的完整数据链路3.1 空闲中断回调数据搬运的唯一入口所有数据搬运动作均发生在HAL_UARTEx_RxEventCallback()中这是本项目真正的控制中枢。CubeMX生成的HAL库默认不启用此回调需手动在usart.c中添加// usart.c void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart-Instance USART1) { // 步骤1确定当前完成接收的缓冲区索引 uint8_t idx current_buf_idx; // 步骤2将DMA接收完成的数据拷贝至环形缓冲区 for (uint16_t i 0; i Size; i) { rb_write(uart1_to_uart2_rb, uart1_rx_buf[idx][i]); } // 步骤3切换缓冲区索引启动下一轮DMA接收 current_buf_idx !current_buf_idx; HAL_UART_Receive_DMA(huart, uart1_rx_buf[current_buf_idx], RX_BUFFER_SIZE); } else if (huart-Instance USART2) { // 同理处理UART2→UART1方向 uint8_t idx current_buf_idx_uart2; for (uint16_t i 0; i Size; i) { rb_write(uart2_to_uart1_rb, uart2_rx_buf[idx][i]); } current_buf_idx_uart2 !current_buf_idx_uart2; HAL_UART_Receive_DMA(huart2, uart2_rx_buf[current_buf_idx_uart2], RX_BUFFER_SIZE); } }逻辑说明Size参数由HAL库自动计算表示本次空闲前DMA实际接收到的字节数非缓冲区长度精准反映真实数据量rb_write()内部已做临界区保护__disable_irq()无需额外加锁切换current_buf_idx后立即重启DMA确保接收无缝衔接此回调在中断上下文中执行严禁调用HAL_Delay()、printf()等阻塞函数。3.2 环形缓冲区无锁设计的边界安全实践ring_buffer.c采用经典的“读写指针取模”实现但针对嵌入式场景做了三处加固// ring_buffer.c typedef struct { uint8_t *buffer; uint16_t head; uint16_t tail; uint16_t size; } ring_buffer_t; // 关键rb_write() 的原子性保障 uint8_t rb_write(ring_buffer_t *rb, uint8_t data) { uint16_t next_head (rb-head 1) % rb-size; if (next_head rb-tail) { // 缓冲区满 return 0; // 返回失败调用方需处理丢包 } __disable_irq(); // 进入临界区 rb-buffer[rb-head] data; rb-head next_head; __enable_irq(); // 退出临界区 return 1; } // rb_read() 同理检查 headtail 判断空参数说明rb-size必须为2的幂如256使%运算可优化为 (size-1)提升性能__disable_irq()仅关闭全局中断不影响SysTick等系统中断比taskENTER_CRITICAL()轻量返回值0/1是重要信号若返回0说明环形缓冲区已满此时应在main()中触发告警如点亮LED而非静默丢弃——这是调试丢包问题的第一线索。3.3 主循环透传非阻塞式搬运引擎main.c中的while(1)不处理任何协议只做两件事从环形缓冲区取数据、往UART发送。发送使用DMA而非轮询避免阻塞// main.c while (1) { // 方向1UART1→UART2 if (rb_available(uart1_to_uart2_rb) 0) { uint8_t data; if (rb_read(uart1_to_uart2_rb, data)) { // 使用DMA发送单字节不攒够再发 tx_buffer[tx_len] data; if (tx_len TX_BURST_SIZE || rb_available(uart1_to_uart2_rb) 0) { HAL_UART_Transmit_DMA(huart2, tx_buffer, tx_len); tx_len 0; } } } // 方向2UART2→UART1同理 if (rb_available(uart2_to_uart1_rb) 0) { // ... identical logic ... } osDelay(1); // FreeRTOS环境下让出时间片裸机可删 }关键设计点TX_BURST_SIZE默认为16避免高频小包DMA启动开销HAL_UART_Transmit_DMA()启动后立即返回不等待发送完成发送完成由HAL_UART_TxCpltCallback()清空tx_len但本项目未启用该回调因DMA发送无空闲概念故采用定时轮询方式——实测在115200波特率下16字节DMA发送耗时约1.4msosDelay(1)完全覆盖。血泪经验曾有开发者将HAL_UART_Transmit()放在主循环结果高波特率下CPU 100%占用。DMA发送必须配HAL_UART_Transmit_DMA()且发送缓冲区需声明为static uint8_t tx_buffer[TX_BURST_SIZE];避免栈溢出。4. 避坑指南五个让开发者凌晨三点还在抓头发的真实问题4.1 现象UART1能收不能发UART2完全静音原因stm32f4xx_hal_msp.c中UART2的GPIO时钟未使能。CubeMX虽配置了PA2/PA3但生成的HAL_UART_MspInit()函数里只写了__HAL_RCC_GPIOA_CLK_ENABLE();漏掉了__HAL_RCC_GPIOA_CLK_ENABLE();注此处为示例实际需检查对应端口。更隐蔽的是若UART2挂APB1总线还需确认__HAL_RCC_APB1_CLK_ENABLE()是否开启。解决打开stm32f4xx_hal_msp.c在HAL_UART_MspInit()中找到UART2初始化段手动补全所有相关时钟使能语句并用示波器测PA2引脚是否有TX电平翻转。4.2 现象空闲中断频繁触发环形缓冲区持续增长但无数据发出原因空闲中断触发条件被误设。HAL库中HAL_UARTEx_EnableRxFifo()默认关闭但若误开启FIFO则空闲中断行为异常更常见的是HAL_UARTEx_ReceiveToIdle_DMA()未正确调用本项目不用此函数但若参考其他例程误加入会破坏双缓冲逻辑。解决全局搜索ReceiveToIdle确保工程中无此函数调用检查usart.c中HAL_UART_Receive_DMA()调用前后无HAL_UARTEx_EnableRxFifo(huart1, ENABLE)语句。4.3 现象低波特率9600正常921600下大量乱码原因APB总线时钟配置错误。UART1挂APB2其时钟源若为HSE/HSI经PLL倍频后输出但CubeMX中PLL配置分频系数错误导致APB2实际频率低于72MHz波特率寄存器计算偏差超容限。解决用ST-Link Utility连接芯片读取RCC_CFGR寄存器确认PPRE2APB2预分频值为0b000即不分频再用HAL_RCC_GetPCLK2Freq()在代码中打印实际APB2频率必须≥72000000。4.4 现象环形缓冲区rb_available()始终返回0但rb_write()返回1原因rb_write()中__disable_irq()与__enable_irq()未配对或被其他代码意外关闭中断。典型场景是FreeRTOS中taskENTER_CRITICAL()后忘记taskEXIT_CRITICAL()导致全局中断长期关闭rb_read()无法执行。解决在rb_write()入口加if (__get_PRIMASK()) { /* 中断已关报错 */ }调试或用J-Link断点跟踪rb_read()是否被执行。4.5 现象烧录后板子不启动Keil提示HardFault_Handler原因DMA缓冲区数组声明位置错误。uint8_t uart1_rx_buf[2][256]若声明在函数内如main()中则分配在栈上256×2512字节极易溢出F407默认栈仅1KB若声明为static但未初始化部分编译器会将其放入.bss段启动时未清零导致DMA读取随机值。解决所有DMA缓冲区必须声明为static uint8_t uart1_rx_buf[2][RX_BUFFER_SIZE] __attribute__((aligned(4)));aligned(4)确保4字节对齐DMA要求static保证在.data段且启动时已初始化。5. 二次开发实战从透传到协议解析的平滑演进路径5.1 添加简单协议过滤只透传含特定起始符的报文工业现场常需过滤无效数据如传感器周期上报中的心跳包。不改动DMA和环形缓冲区仅在空闲中断回调中插入解析逻辑// usart.c 中修改 HAL_UARTEx_RxEventCallback() void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size) { if (huart-Instance USART1) { // 新增临时缓冲区暂存本次接收数据 static uint8_t temp_buf[RX_BUFFER_SIZE]; memcpy(temp_buf, uart1_rx_buf[current_buf_idx], Size); // 判断是否为有效Modbus RTU帧0x01开头CRC校验略 if (Size 4 temp_buf[0] 0x01) { // 有效帧写入环形缓冲区 for (uint16_t i 0; i Size; i) { rb_write(uart1_to_uart2_rb, temp_buf[i]); } } else { // 丢弃不写入 printf(Drop invalid frame, size%d\n, Size); // 仅调试用 } // ... 后续缓冲区切换逻辑不变 } }验证方法用串口助手发送01 03 00 00 00 01 84 0AModbus读保持寄存器观察UART2是否回传相同字节再发AA BB CC确认无输出。此方案零新增内存占用纯逻辑裁剪。5.2 支持双向独立波特率让UART1跑115200UART2跑19200CubeMX不支持同一工程中UART不同波特率的DMA配置需手动修改main.c// main.c 中在 MX_USART1_UART_Init() 和 MX_USART2_UART_Init() 后添加 huart1.Init.BaudRate 115200; huart2.Init.BaudRate 19200; HAL_UART_Init(huart1); HAL_UART_Init(huart2); // 注意必须在HAL_UART_Init()之后调用否则被CubeMX生成代码覆盖关键点HAL_UART_Init()会重写BRR寄存器因此必须在初始化函数调用后立即设置需同步调整dma.c中DMA传输速率匹配实际无需改DMA只管搬运波特率由UART外设决定验证时用逻辑分析仪测两路TX引脚波形确认波特率计数准确。5.3 集成FreeRTOS将透传任务化释放主循环裸机while(1)不利于扩展迁移到FreeRTOS只需三步创建两个任务osThreadDef(uart1_to_uart2_task, uart1_to_uart2_task_func, osPriorityBelowNormal, 0, 256); osThreadCreate(osThread(uart1_to_uart2_task), NULL);任务函数中用osSemaphoreWait()替代轮询void uart1_to_uart2_task_func(void const * argument) { while(1) { osSemaphoreWait(sem_uart1_rx_ready, osWaitForever); // 空闲中断中释放信号量 // 执行rb_read() HAL_UART_Transmit_DMA() } }在HAL_UARTEx_RxEventCallback()末尾添加osSemaphoreRelease(sem_uart1_rx_ready); // 通知任务处理技巧信号量sem_uart1_rx_ready创建时设为osSemaphoreAttr_t attr; attr.cb_mem sem_buffer; attr.cb_size sizeof(sem_buffer);避免动态内存分配。从那以后我每次接到串口透传需求第一件事就是拉出这个双串口DMA工程删掉ring_buffer.c换成自己封装的带统计功能的版本记录收发字节数、丢包次数再根据协议文档在空闲中断里加几行if判断——十分钟搭好原型三天交付固件。它不解决所有问题但把最耗神的“数据不丢、不乱、不卡”这件事稳稳钉死在硬件层。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑