STM32F407六路串口配置避坑指南:时钟、DMA与中断全解析
简介这是一份面向嵌入式开发者的STM32F407六串口通信工程源码基于CubeMX生成UART1至UART6全部启用DMA与空闲中断可稳定接收不定长度数据适合需要多路串口并发处理、协议解析或工业数据采集的场景对熟悉HAL库的中级开发者尤为实用。压缩包共67个文件约581KB以41个h头文件与19个c源文件为主体涵盖HAL配置、中断服务、MSP初始化及主逻辑另含ioc工程文件、MDK-ARM工程与启动汇编方便直接编译与二次裁剪。资源默认使用片外8M晶振可在CubeMX中修改若需减少串口数量裁剪后仅需删除极少量与硬件资源相关的代码即可移植。目前已有7802人学习下载读者可借此掌握多串口DMA加空闲中断的完整配置思路、中断与回调组织方式以及工程裁剪方法快速搭建自己的多串口通信框架。1. 六路串口同时跑STM32F407 的 UART 配置到底卡在哪去年帮一个做工业网关的朋友调板子STM32F407ZGT6 上要同时接六路串口设备两路 RS485 传感器、一路 GPS 模组、一路 4G 通信模块、一路调试口、一路预留。他一开始只跑通了 UART1加到第三路就开始丢数据第六路直接进 HardFault。翻了两天代码才发现问题根本不在串口本身而是时钟树没配对、中断优先级乱成一锅粥、DMA 通道撞车。STM32F407 有 6 个 USART/UART 外设但它们的时钟源、引脚复用、DMA 请求映射各不相同不是复制粘贴就能全部跑起来的。这份资源把 UART1 到 UART6 的完整配置流程拆开了从时钟使能、GPIO 复用、波特率计算到中断和 DMA 分配每一路都单独讲清楚。适合正在用 F407 做多串口通信的嵌入式开发者尤其是那些卡在“单路能跑、多路就崩”阶段的同学。2. 六路串口硬件资源分配先搞清楚谁挂在哪条总线上2.1 六个 UART 外设的时钟归属与引脚映射STM32F407 的六个串口不是平均分布在同一条 APB 总线上的。UART1 和 UART6 挂在 APB2UART2、UART3、UART4、UART5 挂在 APB1。APB2 最高 84MHzAPB1 最高 42MHz这个差异直接影响波特率寄存器的计算。很多人用 CubeMX 生成代码后不去看时钟树结果 UART2 的波特率误差偏大高速通信时偶尔丢帧查半天以为是硬件问题。引脚方面每个 UART 都有多组复用可选。以 UART1 为例默认是 PA9/PA10重映射后是 PB6/PB7。UART2 默认 PA2/PA3UART3 默认 PB10/PB11UART4 默认 PC10/PC11UART5 默认 PC12/PD2UART6 默认 PC6/PC7。实际布线时经常需要根据 PCB 走线调整所以配置代码里必须显式调用 GPIO_PinAFConfig 来指定复用功能不能只靠默认值。串口总线默认TX默认RXDMA控制器常用场景UART1APB2PA9PA10DMA2调试/高速通信UART2APB1PA2PA3DMA1传感器UART3APB1PB10PB11DMA14G模块UART4APB1PC10PC11DMA1RS485UART5APB1PC12PD2DMA1预留UART6APB2PC6PC7DMA2GPS这张表建议贴在显示器旁边配引脚的时候对照着看能省不少翻手册的时间。2.2 中断优先级分组策略六路串口如果全开接收中断NVIC 的优先级分配就是个必须提前规划的事。STM32F407 的 NVIC 支持 4 位优先级分组方式决定了抢占优先级和响应优先级的位数分配。我一般用 NVIC_PriorityGroup_22 位抢占、2 位响应这样每个串口可以有不同的抢占级别。实际分配时UART1 作为调试口给最高抢占优先级 0UART3 接 4G 模块给 1UART2 接传感器给 2UART4 到 UART6 依次降低。这样当 4G 模块收到数据时可以打断传感器的中断处理保证通信实时性。如果全部设成一样的优先级就会出现某个串口长时间占用 CPU其他串口的数据寄存器溢出。// 中断优先级配置示例 NVIC_InitTypeDef NVIC_InitStructure; // UART1 最高优先级 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); // UART3 次高优先级 NVIC_InitStructure.NVIC_IRQChannel USART3_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 0; NVIC_Init(NVIC_InitStructure);这段代码的关键在于抢占优先级的数值越小级别越高。UART1 设为 0 意味着任何其他串口的中断都能被它打断。参数 NVIC_IRQChannel 要填对应的中断向量F407 的 UART4 是 UART4_IRQnUART5 是 UART5_IRQnUART6 是 USART6_IRQn注意 UART6 的名字带 USART 前缀写错了编译能过但中断进不去。3. 从零配置六路 UART时钟、GPIO、波特率一个都不能少3.1 时钟使能与 GPIO 复用配置配置任何外设的第一步都是开时钟。六个串口涉及 APB1 和 APB2 两条总线GPIO 又涉及 A、B、C、D 四个端口时钟使能代码要写全。// 使能 UART 时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_USART6, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2 | RCC_APB1Periph_USART3 | RCC_APB1Periph_UART4 | RCC_APB1Periph_UART5, ENABLE); // 使能 GPIO 时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA | RCC_AHB1Periph_GPIOB | RCC_AHB1Periph_GPIOC | RCC_AHB1Periph_GPIOD, ENABLE);这里有个容易翻车的点UART4 和 UART5 的时钟使能宏是 RCC_APB1Periph_UART4 和 RCC_APB1Periph_UART5不带 S。而 UART1 和 UART6 是 RCC_APB2Periph_USART1 和 RCC_APB2Periph_USART6带 S。这个命名不一致是 ST 的历史遗留问题写错了编译器不报错但外设就是不工作。GPIO 配置要区分 TX 和 RX。TX 配成复用推挽输出RX 配成浮空输入或上拉输入。以 UART1 为例GPIO_InitTypeDef GPIO_InitStructure; // UART1 TX - PA9 GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF; // 复用模式 GPIO_InitStructure.GPIO_OType GPIO_OType_PP; // 推挽输出 GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; // 上拉 GPIO_InitStructure.GPIO_Speed GPIO_Speed_100MHz; GPIO_Init(GPIOA, GPIO_InitStructure); // UART1 RX - PA10 GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF; 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);GPIO_PinAFConfig 这行是必须的很多人只配了 GPIO_Init 忘了这步结果引脚一直是普通 IO 状态串口发不出数据。参数 GPIO_AF_USART1 对应 UART1UART2 和 UART3 是 GPIO_AF_USART2/3UART4 到 UART6 是 GPIO_AF_UART4/5/6同样注意 UART6 用的是 GPIO_AF_USART6。3.2 波特率计算与初始化结构体F407 的波特率计算公式是USARTDIV fCK / (16 × 波特率)。fCK 是挂载总线的时钟频率APB2 是 84MHzAPB1 是 42MHz。以 115200 为例UART1 的 USARTDIV 84000000 / (16 × 115200) ≈ 45.5729。整数部分 45 写入 BRR 的 [15:4] 位小数部分 0.5729 × 16 ≈ 9.17 取整为 9 写入 [3:0] 位最终 BRR 0x2D9。标准库的 USART_Init 函数会自动算这个值但前提是 RCC_GetClocksFreq 能正确获取时钟。如果时钟树配错了算出来的波特率就是错的。USART_InitTypeDef USART_InitStructure; 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_Cmd(USART1, ENABLE);六个串口用同一套参数只是把 USART1 换成 USART2 到 UART6。但要注意 UART4 和 UART5 的库函数是 UART_Init 而不是 USART_Init标准库对这两个外设单独做了函数。如果统一用 USART_Init 编译会报错这是新手最容易卡住的地方之一。3.3 中断接收与 DMA 通道分配六路串口如果都用中断接收CPU 负载会很高。我的做法是UART1 用中断做调试输出UART2 和 UART3 用 DMA 接收UART4 到 UART6 用中断加环形缓冲区。DMA 分配上UART2 的 RX 用 DMA1_Stream5_Channel4UART3 的 RX 用 DMA1_Stream1_Channel4UART1 的 TX 用 DMA2_Stream7_Channel4。这些映射关系在参考手册的 DMA 请求映射表里有选错了通道数据不会丢但会跑到别的外设去。// UART2 DMA 接收配置 DMA_InitTypeDef DMA_InitStructure; DMA_InitStructure.DMA_Channel DMA_Channel_4; DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART2-DR; DMA_InitStructure.DMA_Memory0BaseAddr (uint32_t)uart2_rx_buf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralToMemory; DMA_InitStructure.DMA_BufferSize UART2_RX_BUF_SIZE; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Circular; // 循环模式 DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_FIFOMode DMA_FIFOMode_Disable; DMA_Init(DMA1_Stream5, DMA_InitStructure); USART_DMACmd(USART2, USART_DMAReq_Rx, ENABLE); DMA_Cmd(DMA1_Stream5, ENABLE);DMA_Mode_Circular 让缓冲区收满后自动从头覆盖适合持续接收的场景。如果是一次性接收用 DMA_Mode_Normal收完后要手动重新使能。DMA_Priority 设成 High 是因为串口数据不能等优先级低了会被其他 DMA 请求挤掉。USART_DMACmd 这行是告诉串口把接收事件映射到 DMA 请求不调用的话 DMA 收不到触发信号。4. 多串口同时工作的避坑清单五个真实踩过的坑4.1 坑一UART4/UART5 库函数名不统一导致编译报错现象是编译时提示 USART_Init 未定义或者参数类型不匹配。原因是标准库对 UART4 和 UART5 单独定义了 UART_Init、UART_Cmd 等函数和 USART1 到 USART3、USART6 不共用。解决方法是条件编译或者直接写两套调用UART4 用 UART_Init(UART4, USART_InitStructure)UART5 同理。这个坑在 CubeMX 生成的 HAL 库里不存在但用标准库开发时必踩。4.2 坑二中断服务函数名写错导致进 HardFault现象是串口收到数据后程序跑飞调试器显示停在 HardFault_Handler。原因是中断向量表里的函数名必须和启动文件里定义的一致UART4 的中断函数是 UART4_IRQHandler不是 USART4_IRQHandler。UART5 是 UART5_IRQHandlerUART6 是 USART6_IRQHandler。写错了链接器不会报错因为启动文件里的弱定义会被覆盖但实际中断触发时跳转到了默认死循环。解决方法是核对 startup_stm32f407xx.s 里的向量表名称。4.3 坑三DMA 通道冲突导致数据错乱现象是两路串口同时收数据时其中一路的数据出现在另一路的缓冲区里。原因是两个串口用了同一个 DMA 流或通道。F407 的 DMA1 有 8 个流每个流有 8 个通道可选但同一个流同一时间只能被一个外设占用。UART2 和 UART3 的 RX 都在 DMA1 上必须分配不同的流。解决方法是查参考手册的 DMA 请求映射表确保每个外设的每个方向都独占一个流。4.4 坑四APB1 时钟分频导致波特率偏差过大现象是 UART2 到 UART5 在 115200 下通信不稳定偶尔丢字节。原因是 APB1 的时钟被分频到了 21MHz 或更低但代码里按 42MHz 算波特率。解决方法是确认 RCC 配置里 APB1 的分频系数保证 SystemCoreClock 和实际总线频率一致。用示波器量一下 TX 引脚的实际波特率偏差超过 3% 就要查时钟树。4.5 坑五接收中断里做耗时操作导致数据溢出现象是某个串口数据量大时其他串口的数据丢失。原因是在中断服务函数里调用了 printf 或者做了复杂的协议解析导致中断执行时间过长其他串口的 RX 寄存器溢出。解决方法是中断里只做数据搬运把解析放到主循环或者用 DMA 加空闲中断的方式。我一般会在中断里只往环形缓冲区写一个字节然后置个标志位就退出。5. 二次开发改造思路从能跑到好用的几个关键调整5.1 用空闲中断加 DMA 替代纯中断接收纯中断接收每个字节都要进一次中断115200 波特率下每秒最多 11520 次中断六路同时跑 CPU 基本干不了别的。改成 DMA 加空闲中断后一帧数据收完才进一次中断CPU 负载能降一个数量级。具体做法是开 DMA 循环接收同时使能 USART_IT_IDLE 空闲中断在空闲中断里计算收到的数据长度。// 空闲中断处理 void USART2_IRQHandler(void) { if(USART_GetITStatus(USART2, USART_IT_IDLE) ! RESET) { // 清除空闲中断标志 USART_ReceiveData(USART2); // 计算本帧数据长度 uint16_t recv_len UART2_RX_BUF_SIZE - DMA_GetCurrDataCounter(DMA1_Stream5); // 置标志位主循环处理 uart2_frame_ready 1; uart2_frame_len recv_len; // 重启 DMA DMA_Cmd(DMA1_Stream5, DISABLE); DMA_SetCurrDataCounter(DMA1_Stream5, UART2_RX_BUF_SIZE); DMA_Cmd(DMA1_Stream5, ENABLE); } }这段代码的关键是 DMA_GetCurrDataCounter 返回的是剩余未传输的数据量用缓冲区总大小减去它就是已接收的字节数。清除空闲中断标志必须读一次 DR 寄存器这是 F4 系列的硬件要求只调 USART_ClearITPendingBit 不够。5.2 统一串口设备管理结构体六路串口如果每路都写一套收发函数代码会非常臃肿。我一般会定义一个结构体把串口句柄、缓冲区、回调函数打包然后用一个数组管理六路设备。typedef struct { USART_TypeDef* instance; // 串口实例 uint8_t* rx_buf; // 接收缓冲区 uint16_t rx_len; // 接收长度 void (*on_frame)(uint8_t* data, uint16_t len); // 帧回调 } uart_device_t; uart_device_t uart_devices[6] { {USART1, uart1_buf, 0, uart1_handler}, {USART2, uart2_buf, 0, uart2_handler}, {USART3, uart3_buf, 0, uart3_handler}, {UART4, uart4_buf, 0, uart4_handler}, {UART5, uart5_buf, 0, uart5_handler}, {USART6, uart6_buf, 0, uart6_handler}, };这样主循环里只需要遍历数组检查标志位新增串口或者改回调函数只动一个地方。回调函数指针让每路串口可以有不同的协议解析逻辑传感器用 Modbus4G 模块用 AT 指令互不干扰。5.3 验证方法与调试技巧改完之后怎么确认六路都正常我的做法是写一个回环测试每路串口短接 TX 和 RX发一串递增数据看收到的和发出去的是否一致。六路同时跑每路发 1000 帧统计误码率。另外用逻辑分析仪抓 UART4 和 UART5 的波形确认波特率和数据位宽对得上。如果某一路误码率高先查时钟配置再查 DMA 通道最后查中断优先级。从那以后我每次配多串口都强制走一遍时钟树核对、DMA 映射表对照、中断向量表检查这三步再也没出现过六路跑不起来的情况。希望帮到你。本文还有配套的精品资源点击获取