资讯动态

STM32F407六串口并发配置实战:时钟树、GPIO复用与中断优先级避坑指南

发布时间:2026/10/21 17:52:46 来源:尧图企业网站定制
简介这份资源面向使用STM32F407进行多串口通信开发的嵌入式工程师与学生提供基于CubeMX生成的UART1至UART6六路串口完整工程代码解决多串口同时工作、不定长度数据接收的配置难题。工程中六个串口均已启用DMA与空闲中断可高效接收变长数据帧适合工业控制、多传感器采集、通信网关等需要并行串口交互的场景。压缩包共67个文件约581KB以41个h头文件与19个c源文件为核心另含ioc配置、uvprojx与uvoptx工程文件、启动汇编及调试配置结构完整可直接编译。资源支持通过CubeMX裁剪串口数量裁剪后仅需删除极少量与硬件资源相关的代码即可移植默认使用片外8M晶振且可灵活修改。目前已有7802人学习下载读者可借此快速掌握多串口DMA加空闲中断的接收框架省去从零配置的调试成本并作为项目移植与二次开发的可靠起点。1. STM32F407 六串口同时跑通从时钟树到中断优先级的一次性配置很多人第一次在 STM32F407 上开串口都是先点亮 UART1收发正常以为剩下的 UART2~UART6 照抄就行。结果真把六个串口同时打开要么只有一两个能收发要么跑几分钟就进 HardFault要么波特率莫名其妙对不上。STM32F407 的串口资源其实很宽裕UART1 到 UART6 分布在 APB1 和 APB2 两条总线上只要时钟、引脚、中断三件事对齐六路并发收发是稳的。这篇记录的是我在一块 F407 开发板上把 UART1~UART6 全部跑起来的过程包含时钟使能、GPIO 复用、波特率计算、中断分组和常见翻车点。适合刚上手 F407、准备做多路串口通信的开发者也适合从 F103 迁移过来、发现配置方式变了的人。2. 环境与版本先对齐F407 六串口要用到的库和工具2.1 硬件与固件库版本选择F407 的串口配置在不同固件库版本里写法差别不小先把版本定死后面才不会出现「照着旧教程写、编译报错」的情况。我这次用的是 STM32F407ZGT6 核心板外部 8MHz 晶振系统主频拉到 168MHz。固件库选的是 STM32F4xx 标准外设库 V1.8.0这个版本对 F407 的 UART 支持完整寄存器定义和 V1.4.0 之前的版本有差异尤其是USART_TypeDef结构体成员和时钟使能宏。如果你用的是 HAL 库思路一样但函数名和句柄结构不同本文以标准库为主线HAL 的差异我会在关键处点出来。工具链方面Keil MDK 5.38 加 ARM Compiler 6或者 STM32CubeIDE 1.14 都可以。调试器用 ST-Link V2串口助手用任意一款能同时开六个窗口的工具。这里要提醒一句六个串口同时收发串口助手如果只开一个窗口来回切很容易误判「某个串口没数据」最好用支持多标签的终端工具或者干脆用另一块板子做回环测试。项目选型说明MCUSTM32F407ZGT6100 脚以上才引出全部 UART 引脚晶振8MHz 外部晶振决定 PLL 和最终波特率精度固件库STM32F4xx 标准外设库 V1.8.0寄存器定义与旧版有差异工具链Keil MDK 5.38 / STM32CubeIDE 1.14两者均可注意编译器版本调试器ST-Link V2支持 SWD 在线调试2.2 六个串口的引脚与总线归属F407 的 UART1 和 UART6 挂在 APB2 总线上UART2、UART3、UART4、UART5 挂在 APB1 上。这个归属直接决定时钟使能函数用哪个也决定波特率计算时的时钟频率。引脚方面F407ZGT6 的默认复用如下UART1 用 PA9/PA10UART2 用 PA2/PA3UART3 用 PB10/PB11UART4 用 PC10/PC11UART5 用 PC12/PD2UART6 用 PC6/PC7。注意 UART5 的 RX 在 PD2这个脚在很多最小系统板上没引出选板子时要确认。我一般会在配置前先把这张表画在纸上因为后面写 GPIO 初始化时每个串口对应哪个 GPIO 端口、哪个引脚、哪个复用功能号都要一一对上。F407 的 GPIO 复用功能号在数据手册里查UART1 是 AF7UART2 和 UART3 也是 AF7UART4、UART5、UART6 同样是 AF7这一点比 F103 统一不容易记混。// 串口引脚与总线归属对照配置时逐项核对 // UART1: PA9(TX) PA10(RX) APB2 AF7 // UART2: PA2(TX) PA3(RX) APB1 AF7 // UART3: PB10(TX) PB11(RX) APB1 AF7 // UART4: PC10(TX) PC11(RX) APB1 AF7 // UART5: PC12(TX) PD2(RX) APB1 AF7 // UART6: PC6(TX) PC7(RX) APB2 AF7这段注释不是代码逻辑而是配置前的核对清单。每写一个串口的 GPIO 初始化就回来对一遍能避免「TX 配成 RX」这种低级但难查的错误。尤其是 UART5 的 RX 在 PD2如果板子上这个脚被其他外设占用就得改引脚重映射但 F407 的 UART5 重映射选项有限选型阶段就要确认。3. 时钟与 GPIO 配置六路串口能同时工作的前提3.1 使能时钟APB1 和 APB2 别使能错时钟使能是六串口配置里最容易埋雷的一步。F407 的标准库用RCC_APB1PeriphClockCmd和RCC_APB2PeriphClockCmd两个函数分别使能两条总线上的外设。UART1 和 UART6 走 APB2其余四个走 APB1。如果你把 UART1 的时钟写成 APB1编译能过但串口就是不动这种问题查起来很费时间。// 使能 UART1 和 UART6 时钟APB2 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_USART6, ENABLE); // 使能 UART2~UART5 时钟APB1 RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2 | RCC_APB1Periph_USART3 | RCC_APB1Periph_UART4 | RCC_APB1Periph_UART5, ENABLE); // 使能对应 GPIO 时钟A/B/C/D 四个端口都要开 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA | RCC_AHB1Periph_GPIOB | RCC_AHB1Periph_GPIOC | RCC_AHB1Periph_GPIOD, ENABLE);这里RCC_AHB1PeriphClockCmd是 F4 系列和 F1 系列的一个明显区别F1 用RCC_APB2Periph_GPIOAF4 把 GPIO 时钟挪到了 AHB1 总线上。如果你从 F103 迁移过来这一步不改GPIO 配置全部无效串口自然没反应。参数说明RCC_APB2Periph_USART1和RCC_APB2Periph_USART6是 APB2 上的串口宏RCC_APB1Periph_USART2到RCC_APB1Periph_UART5是 APB1 上的宏注意 UART4 和 UART5 的宏名里没有 S写错编译会报未定义。3.2 GPIO 复用配置AF7 和推挽输出的细节GPIO 配置要同时设置模式、输出类型、速度和复用功能号。F407 的标准库用GPIO_Init结构体一次性配好。TX 脚配成复用推挽输出RX 脚配成复用输入两者都要选对复用功能号。下面以 UART1 为例其余串口照此结构替换端口和引脚。GPIO_InitTypeDef GPIO_InitStruct; // UART1 TX: PA9 GPIO_InitStruct.GPIO_Pin GPIO_Pin_9; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF; // 复用模式 GPIO_InitStruct.GPIO_OType GPIO_OType_PP; // 推挽输出 GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; // 高速 GPIO_InitStruct.GPIO_PuPd GPIO_PuPd_UP; // 上拉 GPIO_Init(GPIOA, GPIO_InitStruct); GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1); // UART1 RX: PA10 GPIO_InitStruct.GPIO_Pin GPIO_Pin_10; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF; GPIO_InitStruct.GPIO_OType GPIO_OType_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStruct.GPIO_PuPd GPIO_PuPd_UP; GPIO_Init(GPIOA, GPIO_InitStruct); GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1);关键点是GPIO_PinAFConfig这一句它把引脚和复用功能号绑定。F407 的 UART 复用功能号统一是GPIO_AF_USART1到GPIO_AF_USART6但注意 UART4 和 UART5 的宏名是GPIO_AF_UART4和GPIO_AF_UART5没有 S。这个细节在标准库头文件里能查到写错不会报错但引脚不会切换到串口功能表现就是「配置都对了就是没波形」。RX 脚虽然配成复用模式但GPIO_OType和GPIO_Speed对输入影响不大保持和 TX 一致即可GPIO_PuPd建议上拉避免悬空时收到乱码。3.3 串口参数初始化波特率、字长、停止位六个串口的参数结构体可以复用但每个串口要单独调用USART_Init。波特率在 168MHz 主频下APB1 和 APB2 的分频系数不同标准库的USART_Init会根据你传入的USART_BaudRate自动算分频但前提是RCC_GetClocksFreq能拿到正确的总线频率。如果你改过系统时钟配置要确认 APB1 和 APB2 的实际频率否则波特率会偏。USART_InitTypeDef USART_InitStruct; USART_InitStruct.USART_BaudRate 115200; USART_InitStruct.USART_WordLength USART_WordLength_8b; USART_InitStruct.USART_StopBits USART_StopBits_1; USART_InitStruct.USART_Parity USART_Parity_No; USART_InitStruct.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_InitStruct.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, USART_InitStruct); USART_Cmd(USART1, ENABLE);这段配置对 UART1 到 UART6 都适用逐个替换USART1为USART2到UART5、USART6即可。注意 UART4 和 UART5 的宏名是UART4和UART5没有 S。USART_Mode要同时开收发只开 TX 的话接收中断不会触发。USART_Cmd是最后一步使能之前串口不会工作。如果你用 HAL 库对应的是HAL_UART_Init句柄里配BaudRate、WordLength等思路一致但 HAL 会在HAL_UART_MspInit里回调 GPIO 和时钟配置结构更分散。4. 中断与优先级六串口并发收发的调度安排4.1 NVIC 中断分组与优先级分配六个串口如果都用中断接收就有六个中断向量要处理。F407 的 NVIC 支持中断分组我一般用NVIC_PriorityGroup_2即 2 位抢占优先级加 2 位响应优先级。六个串口的中断优先级不能全设成一样否则高波特率下容易丢数据。我的分配原则是波特率越高、数据量越大的串口抢占优先级越高。比如 UART1 接调试终端优先级设 1UART2 和 UART3 接传感器设 2UART4 到 UART6 接低速模块设 3。NVIC_InitTypeDef NVIC_InitStruct; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitStruct.NVIC_IRQChannel USART1_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStruct.NVIC_IRQChannelSubPriority 0; NVIC_InitStruct.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStruct); // UART2~UART6 照此结构替换 IRQn 和优先级NVIC_PriorityGroupConfig只需在初始化开头调用一次后面所有中断都按这个分组来。NVIC_IRQChannel的取值在stm32f4xx.h里能查到UART1 是USART1_IRQnUART2 是USART2_IRQnUART3 是USART3_IRQnUART4 是UART4_IRQnUART5 是UART5_IRQnUART6 是USART6_IRQn。注意 UART4 和 UART5 的 IRQn 宏名里没有 S和前面时钟宏的命名规律一致。4.2 接收中断服务函数六个入口别写混每个串口有独立的 IRQHandler函数名必须和启动文件里的向量表一致。标准库的启动文件startup_stm32f40_41xxx.s里已经定义了这些名字你只要在stm32f4xx_it.c里实现即可。六个中断服务函数的逻辑可以一样判断接收中断标志读数据清标志。void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART1); // 把 data 存入 UART1 的接收缓冲区 USART_ClearITPendingBit(USART1, USART_IT_RXNE); } } void UART4_IRQHandler(void) { if (USART_GetITStatus(UART4, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(UART4); // 把 data 存入 UART4 的接收缓冲区 USART_ClearITPendingBit(UART4, USART_IT_RXNE); } }这里只列了两个其余四个结构相同。注意 UART4 和 UART5 的中断函数名是UART4_IRQHandler和UART5_IRQHandler没有 S写错的话中断会进默认死循环。USART_GetITStatus和USART_ClearITPendingBit的参数要用对应的串口宏。接收数据后要尽快存入缓冲区不要在中断里做耗时处理否则高波特率下会丢字节。我一般给每个串口配一个环形缓冲区中断里只做入队主循环里再解析。4.3 发送处理轮询还是中断六个串口如果都用来发数据发送方式要选好。低速、少量数据用轮询USART_SendData加等待USART_FLAG_TXE就行简单可靠。高速、连续发送建议用中断或 DMA但六个串口同时开 DMA 会占用较多通道F407 的 DMA 资源虽然够但配置复杂度上升。我的做法是调试口 UART1 用轮询其余五个用中断接收加轮询发送先保证功能跑通再按需优化。void UART_SendByte(USART_TypeDef* USARTx, uint8_t data) { while (USART_GetFlagStatus(USARTx, USART_FLAG_TXE) RESET); USART_SendData(USARTx, data); }这个函数对六个串口通用传入不同的USARTx即可。USART_FLAG_TXE表示发送数据寄存器空等待它置位再写下一个字节能避免覆盖未发送的数据。如果发送数据量大这个等待会阻塞主循环此时要考虑中断发送或 DMA。但在六串口并发的场景下先跑通再优化比一上来就上 DMA 更稳妥。5. 避坑与排查六串口配置里最容易翻车的五件事5.1 现象只有 UART1 能收发其余无反应原因通常是时钟使能写错总线。UART1 在 APB2如果你把 UART2 也写成RCC_APB2Periph_USART2编译能过但 UART2 的时钟根本没开。解决方法是逐个核对UART1 和 UART6 用 APB2UART2 到 UART5 用 APB1。另外 GPIO 时钟在 AHB1别写成 APB2。5.2 现象串口能发不能收或收乱码先查 GPIO 复用功能号。UART4 和 UART5 的复用宏是GPIO_AF_UART4和GPIO_AF_UART5没有 S写成GPIO_AF_USART4编译报错但写成GPIO_AF_USART1给 UART4 用就不会报错只是引脚功能不对。再查波特率APB1 和 APB2 频率不同如果系统时钟改过USART_Init算出的分频可能偏用示波器量一下实际波特率最直接。5.3 现象跑一段时间进 HardFault六串口中断同时开如果中断服务函数里用了同一个全局缓冲区且没做保护主循环和中断同时读写会踩内存。解决方法是每个串口独立缓冲区中断里只入队主循环里出队队列操作加临界区保护。另外检查中断优先级分组是否只设置了一次重复设置会导致优先级混乱。5.4 现象UART5 完全没波形UART5 的 RX 在 PD2很多开发板这个脚没引出或接了其他外设。先确认板子原理图如果 PD2 被占用UART5 就只能发不能收或者换用其他串口。另外 UART5 的时钟在 APB1中断向量是UART5_IRQn函数名UART5_IRQHandler这些命名里都没有 S容易和 UART1 到 UART3 混淆。5.5 现象编译报未定义USART4标准库里没有USART4和USART5这两个宏只有UART4和UART5。所有函数调用、时钟宏、中断宏都要用UART4和UART5。如果你从网上抄了一段用USART4的代码编译会报一堆未定义逐个替换成UART4即可。这个坑在 F1 系列不存在因为 F1 没有 UART4 和 UART5迁移过来的人容易想当然。6. 装完怎么验证六串口回环与并发收发测试配置写完、编译通过只是第一步。六串口能不能同时稳定工作要用回环和并发测试来验证。我的验证分三步先单串口回环再六串口同时回环最后六串口并发收发不同数据。第一步把每个串口的 TX 和 RX 短接发一个字节看能不能收到。这一步能排除 GPIO 和波特率问题。第二步六个串口同时做回环每个串口发不同的字符串比如 UART1 发 A1UART2 发 A2依次类推看是否各收各的不串数据。第三步用另一块板子或串口助手同时向六个串口发数据观察接收缓冲区和主循环解析是否正常。// 六串口回环测试片段逐个串口发送并等待接收 void UART_LoopbackTest(USART_TypeDef* USARTx, uint8_t testByte) { UART_SendByte(USARTx, testByte); // 等待接收中断把数据存入缓冲区主循环里检查 // 如果超时未收到说明该串口回环失败 }这个测试函数对六个串口逐个调用testByte用不同值区分。实际运行时接收中断会把回环数据存入对应缓冲区主循环里比对发送值和接收值是否一致。如果某个串口收不到回到第 5 章排查。六串口同时回环时注意观察是否有串口丢数据如果丢检查中断优先级和缓冲区大小。验证通过后我习惯把六个串口的配置封装成UART_InitAll()函数里面按顺序调用时钟、GPIO、串口参数、中断四步主函数里只调一次。这样以后换板子或改引脚只改这一个函数。从那以后我每次配多串口都先把时钟总线和引脚复用列成表配完逐个回环不再相信「照抄就能跑」。希望帮到你。本文还有配套的精品资源点击获取

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

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

免费获取报价 →
↑