
简介这份资源面向使用STM32F407进行多串口开发的嵌入式工程师与学生提供UART1至UART6六路串口同时工作的完整工程代码解决多串口并行通信、不定长度数据接收等常见开发难题。工程基于CubeMx生成六个串口均已启用DMA与空闲中断可高效接收变长数据帧并支持通过CubeMx裁剪串口数量裁剪后仅需删除少量与硬件资源相关的代码即可移植默认使用片外8M晶振也可在CubeMx中修改。压缩包共67个文件约581KB以41个h头文件与19个c源文件为主体涵盖HAL库配置、中断服务、MSP初始化与主程序逻辑另含ioc工程文件、MDK-ARM工程文件及启动汇编文件方便直接编译与二次开发。目前已有7802人学习下载适合需要快速搭建多串口通信框架、研究DMA加空闲中断接收机制的开发者参考借鉴。1. 六路串口一起上STM32F407 串口1~6 同时使用的环境底账很多人第一次在 STM32F407 上把 USART1 到 UART6 全部打开都会经历一个反直觉的瞬间代码编译通过、下载成功但串口助手里只有一两个口有输出其余全是空白。这不是玄学是时钟、引脚、中断优先级三件事没对齐。STM32F407 的六个串口分布在 APB1 和 APB2 两条总线上USART1 和 USART6 挂 APB2USART2/3 和 UART4/5 挂 APB1总线频率不同波特率寄存器算错一位就全盘乱码。这套环境适合正在做多路数据采集、工控网关、多传感器汇聚的开发者尤其是需要同时接 GPS、蓝牙、上位机、调试口、RS485 和预留通道的场景。装不好不会报错只会让你在示波器前怀疑人生。2. 环境准备与版本选择时钟、引脚、库的三条硬线2.1 芯片型号与封装确认STM32F407 家族里不是每个型号都引出全部六个串口。LQFP100 封装的 F407VET6、F407VGT6 通常能完整引出 USART1~3 和 UART4/5USART6 要看具体引脚复用表。LQFP64 的 F407ZE 之类UART5 和 USART6 可能被其他功能占用。我一般先翻对应型号的 datasheet 第 3 章引脚定义表把六个串口的 TX/RX 候选引脚列出来再对照自己板子的原理图确认哪些脚已经接了晶振、SWD、USB 或外部 Flash。常见做法是优先选默认复用引脚避免重映射带来的额外配置。比如 USART1 默认 PA9/PA10USART2 默认 PA2/PA3USART3 默认 PB10/PB11UART4 默认 PC10/PC11UART5 默认 PC12/PD2USART6 默认 PC6/PC7。如果板子已经把这些脚占用了再考虑部分重映射或完全重映射但重映射会牵扯 AFIO 时钟和重映射寄存器多一层出错点。2.2 标准外设库还是 HAL 库网上关于 STM32F407 串口配置的代码标准外设库和 HAL 库各占一半。标准外设库直接操作寄存器代码量小、执行效率高但 ST 已经停止维护新项目用得少。HAL 库配合 CubeMX 生成初始化代码六个串口的配置可以图形化勾选适合快速搭建。我自己的取舍是如果项目周期紧、团队里有人不熟悉寄存器用 HAL 库加 CubeMX如果对中断响应时间和代码体积敏感用标准外设库手动配。版本上标准外设库选 V1.8.0 或 V1.9.0 都行差别不大。HAL 库建议用 STM32CubeF4 包里较新的版本比如 V1.27.x 之后的早期版本在 UART 中断接收上有已知的溢出标志清除问题。注意HAL 库版本和 CubeMX 版本要匹配否则生成的代码可能编译不过。2.3 时钟树与总线频率这是六串口同时使用最容易翻车的地方。STM32F407 最高主频 168MHzAPB1 最高 42MHzAPB2 最高 84MHz。USART1 和 USART6 在 APB2 上USART2/3 和 UART4/5 在 APB1 上。如果 APB1 分频系数设成 4APB1 时钟就是 42MHzAPB2 分频系数设成 2APB2 时钟就是 84MHz。波特率计算公式是USARTDIV fCK / (16 × 波特率)。以 115200 为例APB2 上 USART1 的 USARTDIV 84000000 / (16 × 115200) ≈ 45.5729整数部分 45小数部分 0.5729 × 16 ≈ 9.17取 9。APB1 上 USART2 的 USARTDIV 42000000 / (16 × 115200) ≈ 22.7865整数部分 22小数部分 0.7865 × 16 ≈ 12.58取 13。如果两个口用了同一个波特率寄存器值必然有一个是乱码。提示用 CubeMX 配置时钟树时把 APB1 和 APB2 的分频系数截图保存后面手动算波特率或者排查乱码时直接对照。3. 六串口初始化从 GPIO 到 NVIC 的完整配置3.1 GPIO 与复用功能配置六个串口同时使用第一步是把所有 TX/RX 引脚配成复用推挽输出和浮空输入或上拉输入。标准外设库的写法如下// 使能 GPIO 和 USART 时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA | RCC_AHB1Periph_GPIOB | RCC_AHB1Periph_GPIOC | RCC_AHB1Periph_GPIOD, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_USART6, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2 | RCC_APB1Periph_USART3 | RCC_APB1Periph_UART4 | RCC_APB1Periph_UART5, ENABLE); // USART1 PA9 TX, PA10 RX GPIO_InitStructure.GPIO_Pin GPIO_Pin_9 | GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_OType GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1); // USART2 PA2 TX, PA3 RX GPIO_InitStructure.GPIO_Pin GPIO_Pin_2 | GPIO_Pin_3; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_PinAFConfig(GPIOA, GPIO_PinSource2, GPIO_AF_USART2); GPIO_PinAFConfig(GPIOA, GPIO_PinSource3, GPIO_AF_USART2); // USART3 PB10 TX, PB11 RX GPIO_InitStructure.GPIO_Pin GPIO_Pin_10 | GPIO_Pin_11; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_PinAFConfig(GPIOB, GPIO_PinSource10, GPIO_AF_USART3); GPIO_PinAFConfig(GPIOB, GPIO_PinSource11, GPIO_AF_USART3); // UART4 PC10 TX, PC11 RX GPIO_InitStructure.GPIO_Pin GPIO_Pin_10 | GPIO_Pin_11; GPIO_Init(GPIOC, GPIO_InitStructure); GPIO_PinAFConfig(GPIOC, GPIO_PinSource10, GPIO_AF_UART4); GPIO_PinAFConfig(GPIOC, GPIO_PinSource11, GPIO_AF_UART4); // UART5 PC12 TX, PD2 RX GPIO_InitStructure.GPIO_Pin GPIO_Pin_12; GPIO_Init(GPIOC, GPIO_InitStructure); GPIO_PinAFConfig(GPIOC, GPIO_PinSource12, GPIO_AF_UART5); GPIO_InitStructure.GPIO_Pin GPIO_Pin_2; GPIO_Init(GPIOD, GPIO_InitStructure); GPIO_PinAFConfig(GPIOD, GPIO_PinSource2, GPIO_AF_UART5); // USART6 PC6 TX, PC7 RX GPIO_InitStructure.GPIO_Pin GPIO_Pin_6 | GPIO_Pin_7; GPIO_Init(GPIOC, GPIO_InitStructure); GPIO_PinAFConfig(GPIOC, GPIO_PinSource6, GPIO_AF_USART6); GPIO_PinAFConfig(GPIOC, GPIO_PinSource7, GPIO_AF_USART6);这段代码的关键点有三个。第一时钟使能要分总线APB1 和 APB2 分开写漏一个对应的串口就完全没反应。第二GPIO_PinAFConfig 的复用功能号必须和引脚对应USART1 到 USART6 的 AF 编号不全是同一个F407 上 USART1/2/3 是 AF7UART4/5 和 USART6 也是 AF8 或 AF7具体查数据手册的复用功能映射表。第三UART5 的 TX 在 PC12RX 在 PD2跨了两个 GPIO 端口初始化时别只配一个。3.2 串口参数与中断优先级GPIO 配好后逐个初始化串口参数。六个串口的波特率可以不同但同一个串口的 TX 和 RX 必须一致。下面以 USART1 和 USART2 为例// USART1 初始化波特率 115200 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_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStructure.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, USART_InitStructure); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE); // USART2 初始化波特率 9600 USART_InitStructure.USART_BaudRate 9600; USART_Init(USART2, USART_InitStructure); USART_ITConfig(USART2, USART_IT_RXNE, ENABLE); USART_Cmd(USART2, ENABLE); // 其余 USART3、UART4、UART5、USART6 按同样结构逐个初始化参数说明USART_WordLength_8b 是数据位 8 位USART_StopBits_1 是停止位 1 位USART_Parity_No 是无校验。如果某个口接的是 Modbus RTU 设备通常用 8 数据位、1 停止位、无校验或偶校验按从机手册改。USART_Mode 要同时开 Rx 和 Tx只开一个的话另一个方向没数据。中断优先级是六串口同时使用时最容易被忽略的。NVIC 里每个串口的中断通道不同USART1 是 USART1_IRQnUSART2 是 USART2_IRQn依此类推。如果六个口都开接收中断优先级分组和抢占优先级要提前规划。我一般把调试口 USART1 设成最高抢占优先级数据采集口设成中等预留口设成最低。用 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2) 分组2 位抢占优先级、2 位子优先级够用。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); NVIC_InitStructure.NVIC_IRQChannel USART2_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_Init(NVIC_InitStructure); // 其余串口依次降低抢占优先级3.3 中断服务函数与数据缓冲六个串口的中断服务函数名是固定的写错一个字母就进不了中断。标准外设库下分别是 USART1_IRQHandler、USART2_IRQHandler、USART3_IRQHandler、UART4_IRQHandler、UART5_IRQHandler、USART6_IRQHandler。每个函数里先判断 RXNE 标志再读数据寄存器最后清标志。void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART1); // 存入 USART1 接收缓冲区 USART_ClearITPendingBit(USART1, USART_IT_RXNE); } } void UART4_IRQHandler(void) { if (USART_GetITStatus(UART4, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(UART4); // 存入 UART4 接收缓冲区 USART_ClearITPendingBit(UART4, USART_IT_RXNE); } }注意 UART4 和 UART5 的中断函数名里没有 S是 UART4_IRQHandler 而不是 USART4_IRQHandler这是很多人编译报错的直接原因。另外如果开了多个中断源比如同时开 RXNE 和 IDLE判断顺序要先 RXNE 后 IDLE否则可能丢数据。4. 避坑与排查六串口同时使用的五条血泪记录4.1 现象只有 USART1 有输出其余全静默原因APB1 时钟没使能或者使能了但波特率算错。USART2/3 和 UART4/5 挂 APB1如果 RCC_APB1PeriphClockCmd 里漏了某个串口对应口完全不工作。另一种可能是 GPIO 复用功能号写错引脚没有真正切换到串口模式。解决用调试器读 RCC_APB1ENR 寄存器确认对应位是 1。再用万用表测 TX 引脚空闲时应该是高电平如果一直是低电平说明 GPIO 没配成复用推挽。逐个口单独测试先保证一个口能收发再叠加下一个。4.2 现象数据能发但全是乱码原因波特率不匹配。APB1 和 APB2 频率不同如果两个口用了同一个 USARTDIV 值挂在不同总线上的口必然有一个乱码。另外如果外部晶振频率和代码里 HSE_VALUE 定义不一致比如板子是 8MHz 但代码写 25MHz系统时钟整体偏移所有串口波特率都会偏。解决用示波器测 TX 引脚的一个字节波形算实际波特率。或者用串口助手逐个口试不同波特率能正常收发的就是实际值。检查 system_stm32f4xx.c 里的 HSE_VALUE 和 PLL 配置确保和板子晶振一致。4.3 现象同时收发时丢数据或死机原因中断优先级冲突或缓冲区溢出。六个口都开接收中断如果某个口数据量大、中断服务函数里处理时间过长高优先级中断会打断低优先级导致低优先级口的数据寄存器溢出。另外如果中断函数里调用了 printf 之类的阻塞函数直接卡死。解决中断服务函数里只做最少的操作把数据存入环形缓冲区主循环里再处理。提高数据量大的串口抢占优先级或者用 DMA 接收。检查 ORE 溢出标志如果置位了先读 SR 再读 DR 清除。4.4 现象UART5 完全没反应原因UART5 的 TX 在 PC12RX 在 PD2跨了 GPIOC 和 GPIOD 两个端口。如果只使能了 GPIOC 时钟PD2 就没法配成复用功能。另外UART5 没有重映射选项引脚是固定的。解决确认 RCC_AHB1Periph_GPIOD 时钟已使能PD2 配成复用功能并选对 AF 号。用示波器直接测 PC12如果发送时没有波形检查 UART5 的时钟使能和 USART_Cmd 是否调用。4.5 现象CubeMX 生成的代码编译报错提示重复定义原因CubeMX 在生成代码时如果手动改过 main.c 或 stm32f4xx_it.c重新生成会覆盖或产生重复的中断服务函数。另外HAL 库和标准外设库混用时函数名冲突。解决在 CubeMX 里把用户代码写在 /* USER CODE BEGIN/ 和 /USER CODE END */ 之间重新生成不会丢。如果已经混用了两个库把标准外设库的初始化代码全部移到 HAL 之外或者干脆统一用一个库。5. 装完怎么验证六串口回环与压力测试六个串口全部初始化完成后不要急着接外部设备先做回环测试。把每个串口的 TX 和 RX 用杜邦线短接发送一个字节看能否原样收到。这一步能排除 GPIO、波特率、中断配置的大部分问题。// 六串口回环测试逐个发送并等待接收 uint8_t test_data 0x5A; uint8_t recv_data 0; // USART1 回环 USART_SendData(USART1, test_data); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) RESET); recv_data USART_ReceiveData(USART1); if (recv_data ! test_data) { /* 记录 USART1 失败 */ } // 其余五个口按同样逻辑逐个测试回环通过后再做并发压力测试。用一个定时器每 10ms 向六个口同时发送递增数据接收端用串口助手分别打开六个 COM 口观察是否有丢包或乱码。我一般会连续跑 10 分钟统计每个口的接收字节数和错误数。如果某个口错误率明显偏高优先查该口的时钟使能和波特率寄存器值。验证时还有一个容易忽略的点六个串口同时工作时电源电流会上升。如果板子供电不足可能出现某个口时好时坏。用万用表测 3.3V 轨电压发送时如果跌到 3.0V 以下就要检查 LDO 或电源走线。从那以后我每次配多串口都强制先跑一遍六口回环再跑十分钟并发压力测试确认每个口的错误计数为零才接外部设备。希望帮到你。本文还有配套的精品资源点击获取