STM32F407标准库六路串口配置与DMA发送避坑指南 简介这份资源面向使用STM32F407标准库进行多串口开发的嵌入式工程师与学生聚焦6路USART的完整配置方案解决多设备通信、模块化设计及并发数据流处理中的串口资源管理问题。包内共115个文件以50个.c与50个.h源码文件为核心涵盖USART、TIM、RCC、ADC、CAN等外设驱动另含2个hex烧录文件、Keil工程文件uvprojx、uvopt及bat批处理脚本压缩包约552KB结构完整可直接编译。已有3893人学习下载说明该方案在同类多串口应用中具有较高参考价值。读者可从中获取6路串口初始化、DMA发送通道分配、中断接收回调及多串口并行运行的实现思路理解GPIO、时钟与DMA资源不冲突的配置方法并借助工程文件快速搭建调试环境适合需要提升串口通信效率与系统稳定性的中高级开发者参考。1. 六路串口一起上STM32F407 标准库环境到底怎么搭第一次在 STM32F407 上同时开六路串口是在一个多传感器采集的项目里。六路 UART 要同时跑每路波特率还不一样有的接上位机、有的接模块、有的做调试口。当时想当然地以为“标准库嘛复制粘贴改个 USART 编号就行”结果第一版代码烧进去串口要么一个都收不到数据要么 DMA 发到一半就卡死调试口打印出来的全是乱码。后来才发现标准库下多串口配置远不是改个宏定义那么简单——时钟使能、GPIO 复用、中断优先级、DMA 通道映射每一项都有坑。这套环境适合两类人一是刚接触 STM32F407 标准库、想从单串口过渡到多串口的开发者二是项目里确实需要多路串口并发、又不想上 HAL 库的团队。标准库虽然老但在 F407 这种经典芯片上资料多、代码量小、执行效率高很多工业现场还在用。装不好会怎样轻则某一路串口不工作重则 DMA 和中断打架整个系统跑飞。下面按我实际搭环境的顺序从工程创建到六路串口全部跑通一步步来。2. 工程骨架与时钟配置标准库版本选择和基础环境搭建2.1 标准库版本怎么选别拿 F1 的库硬套 F4STM32F407 属于 F4 系列标准库要用STM32F4xx_DSP_StdPeriph_Lib_V1.8.0或更高版本。我见过有人拿 F1 的stm32f10x.h改改就往上套编译能过但时钟树配置完全对不上串口波特率算出来全是错的。F4 的库文件结构是这样的STM32F4xx_DSP_StdPeriph_Lib_V1.8.0/ ├── Libraries/ │ ├── CMSIS/ │ │ ├── Device/ST/STM32F4xx/ │ │ │ ├── Include/ # stm32f4xx.h 等头文件 │ │ │ └── Source/Templates/ # system_stm32f4xx.c 启动文件 │ │ └── Include/ # core_cm4.h 等内核头文件 │ └── STM32F4xx_StdPeriph_Driver/ │ ├── inc/ # 外设驱动头文件 │ └── src/ # 外设驱动源文件 ├── Project/ │ └── STM32F4xx_StdPeriph_Templates/ │ ├── main.c │ ├── stm32f4xx_conf.h # 外设配置裁剪头 │ └── stm32f4xx_it.c/h # 中断服务函数 └── Utilities/我一般会把Libraries整个拷到工程目录下然后在 IDE 里添加头文件搜索路径。关键一步是stm32f4xx_conf.h这个文件决定哪些外设驱动参与编译。六路串口要用到USART、GPIO、DMA、RCC、NVIC对应的宏都要打开/* stm32f4xx_conf.h 中确保以下模块被使能 */ #include stm32f4xx_rcc.h #include stm32f4xx_gpio.h #include stm32f4xx_usart.h #include stm32f4xx_dma.h #include misc.h /* NVIC 优先级分组配置 */这里有个细节misc.h不在STM32F4xx_StdPeriph_Driver里它在Libraries/CMSIS/Device/ST/STM32F4xx/下面路径别加错。少了它NVIC_Init和NVIC_PriorityGroupConfig都找不到。2.2 系统时钟树配置168MHz 下串口波特率才准F407 标准库默认的系统时钟是 16MHz 内部 RC直接拿来跑串口波特率误差大得离谱。我一般用外部 8MHz 晶振PLL 倍频到 168MHz。这段代码在system_stm32f4xx.c里改或者自己写一个SystemInit覆盖/* 系统时钟配置HSE 8MHz - PLL - 168MHz */ void SystemClock_Config(void) { RCC_DeInit(); RCC_HSEConfig(RCC_HSE_ON); /* 等待 HSE 就绪超时则停在这里方便排查 */ if (RCC_WaitForHSEStartUp() ! SUCCESS) { while (1); } RCC_HCLKConfig(RCC_SYSCLK_Div1); /* AHB 168MHz */ RCC_PCLK1Config(RCC_HCLK_Div4); /* APB1 42MHzUSART2~5 挂这里 */ RCC_PCLK2Config(RCC_HCLK_Div2); /* APB2 84MHzUSART1/6 挂这里 */ RCC_PLLConfig(RCC_PLLSource_HSE, 8, 336, 2, 7); RCC_PLLCmd(ENABLE); while (RCC_GetFlagStatus(RCC_FLAG_PLLRDY) RESET); RCC_SYSCLKConfig(RCC_SYSCLKSource_PLLCLK); while (RCC_GetSYSCLKSource() ! 0x08); }参数说明RCC_PLLConfig的五个参数分别是 PLL 源、M、N、P、Q。M8 表示 8MHz 先分频到 1MHzN336 倍频到 336MHzP2 再分频得到 168MHz。Q7 是给 USB 用的这里不影响串口。APB1 分频 4 得到 42MHzAPB2 分频 2 得到 84MHz这两个频率直接决定串口波特率的计算基准。如果 APB 频率设错USART_Init里算出来的 BRR 值就是错的现象就是能发不能收或者收全是乱码。提示改完时钟后先用调试口打印一下RCC_GetClocksFreq的结果确认 PCLK1 和 PCLK2 跟预期一致再往下配串口。3. 六路串口逐个配GPIO 复用、USART 参数与中断优先级3.1 六路串口的引脚分配和时钟使能F407 有 6 个 USART/UARTUSART1、USART2、USART3、UART4、UART5、USART6。我用的引脚分配如下这套分配避开了 JTAG 和 SDIO 的冲突引脚串口TX 引脚RX 引脚总线时钟宏USART1PA9PA10APB2RCC_APB2Periph_USART1USART2PA2PA3APB1RCC_APB1Periph_USART2USART3PB10PB11APB1RCC_APB1Periph_USART3UART4PC10PC11APB1RCC_APB1Periph_UART4UART5PC12PD2APB1RCC_APB1Periph_UART5USART6PC6PC7APB2RCC_APB2Periph_USART6时钟使能要分两条总线写别一股脑全塞进 APB2/* 使能 GPIO 和串口时钟 */ 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);GPIO 初始化时模式要设成GPIO_Mode_AF输出类型GPIO_OType_PushPull复用功能用GPIO_PinAFConfig指定。以 USART1 为例GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Pin GPIO_Pin_9 | GPIO_Pin_10; GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF; GPIO_InitStruct.GPIO_Speed GPIO_Speed_100MHz; GPIO_InitStruct.GPIO_OType GPIO_OType_PushPull; GPIO_InitStruct.GPIO_PuPd GPIO_PuPd_UP; GPIO_Init(GPIOA, GPIO_InitStruct); GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1);其他五路照这个模板改引脚和复用宏就行。注意 UART4 和 UART5 的复用宏是GPIO_AF_UART4和GPIO_AF_UART5不是GPIO_AF_USART4写错了编译不报错但引脚没复用上。3.2 USART 参数配置波特率、字长和校验位六路串口波特率我设成三组USART1 和 USART6 用 115200USART2 和 USART3 用 9600UART4 和 UART5 用 38400。配置函数统一写一个传参进去void USART_Config(USART_TypeDef* USARTx, uint32_t baudrate) { USART_InitTypeDef USART_InitStruct; USART_InitStruct.USART_BaudRate baudrate; 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(USARTx, USART_InitStruct); USART_Cmd(USARTx, ENABLE); }USART_BaudRate填的是实际波特率数值标准库内部会根据 PCLK 频率自动算 BRR。如果 PCLK 配置错了这里填 115200 实际输出可能是 9600 的波形。USART_WordLength_8b配合USART_Parity_No就是最常见的 8N1 格式。如果接的模块需要偶校验把USART_Parity改成USART_Parity_Even同时字长要改成USART_WordLength_9b否则校验位会挤掉数据位。3.3 中断接收配置NVIC 优先级分组和使能六路串口都要中断接收NVIC 优先级分组必须先定好。我一般用NVIC_PriorityGroup_22 位抢占优先级、2 位响应优先级够用且不容易乱NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitTypeDef NVIC_InitStruct; 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);六路串口的中断通道分别是USART1_IRQn、USART2_IRQn、USART3_IRQn、UART4_IRQn、UART5_IRQn、USART6_IRQn。抢占优先级我按串口重要性排USART1 最高设 1USART6 设 2其余设 3。响应优先级全设 0。这样高优先级串口的中断可以打断低优先级串口的处理适合调试口和关键指令口。使能接收中断用USART_ITConfig(USARTx, USART_IT_RXNE, ENABLE)然后在对应的stm32f4xx_it.c里写中断服务函数。每个串口的中断函数名不能写错比如 UART4 的是UART4_IRQHandler不是USART4_IRQHandler写错了编译能过但中断永远不进。4. DMA 发送配置通道映射、传输模式和发送完成判断4.1 DMA 通道映射表六路串口分别走哪个流F407 的 DMA 有两个控制器DMA1 有 7 个流DMA2 有 7 个流。串口发送的 DMA 请求映射是固定的不能随便选串口DMA 控制器流通道USART1_TXDMA2Stream7Channel4USART2_TXDMA1Stream6Channel4USART3_TXDMA1Stream3Channel4UART4_TXDMA1Stream4Channel4UART5_TXDMA1Stream7Channel4USART6_TXDMA2Stream6Channel5这张表我建议贴在显示器边上配错一个流对应的串口就发不出数据而且不会报错只是 DMA 传输完成标志永远不置位。4.2 DMA 初始化内存到外设模式的关键参数以 USART1 的 DMA 发送为例DMA_InitTypeDef DMA_InitStruct; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); DMA_DeInit(DMA2_Stream7); DMA_InitStruct.DMA_Channel DMA_Channel_4; DMA_InitStruct.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; DMA_InitStruct.DMA_Memory0BaseAddr (uint32_t)tx_buffer; DMA_InitStruct.DMA_DIR DMA_DIR_MemoryToPeripheral; DMA_InitStruct.DMA_BufferSize tx_len; DMA_InitStruct.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStruct.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStruct.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStruct.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStruct.DMA_Mode DMA_Mode_Normal; DMA_InitStruct.DMA_Priority DMA_Priority_Medium; DMA_InitStruct.DMA_FIFOMode DMA_FIFOMode_Disable; DMA_Init(DMA2_Stream7, DMA_InitStruct);DMA_DIR_MemoryToPeripheral表示从内存搬到外设这是发送方向。DMA_PeripheralInc_Disable是因为外设地址固定是USART1-DR不能自增。DMA_Mode_Normal表示传完指定长度就停适合不定长发送如果要循环发送固定数据改成DMA_Mode_Circular。DMA_FIFOMode_Disable在直接模式下用开了 FIFO 反而容易出玄学问题我一般关掉。使能 DMA 发送前要先使能串口的 DMA 发送请求USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); DMA_Cmd(DMA2_Stream7, ENABLE);4.3 发送完成判断别用延时用标志位DMA 发送最忌讳发完就delay_ms等。正确做法是查DMA_FLAG_TCIF7或者开传输完成中断/* 等待 DMA 发送完成 */ while (DMA_GetFlagStatus(DMA2, DMA_FLAG_TCIF7) RESET); DMA_ClearFlag(DMA2, DMA_FLAG_TCIF7);如果是六路同时发每路的标志位不一样要分别判断。我一般给每路串口维护一个发送状态标志DMA 传输完成中断里置位主循环里检查。这样六路 DMA 可以并发启动互不阻塞。注意DMA 传输完成标志在DMA_Mode_Normal下不会自动清除必须手动DMA_ClearFlag否则第二次发送时标志一直是置位的判断逻辑就失效了。5. 避坑与排查六路串口跑不起来时先查这五处5.1 现象某一路串口完全无输出其他五路正常原因DMA 流或通道映射错了。F407 的 DMA 请求映射是硬件固定的比如 UART5_TX 只能用 DMA1_Stream7_Channel4换成别的流DMA 控制器收不到串口的请求信号数据永远搬不出去。解决对照 4.1 的映射表逐路核对DMA_InitStruct.DMA_Channel和DMA_DeInit的流编号。改完后先单独测这一路确认能发再合入六路。5.2 现象串口能发不能收或者收几个字节就卡死原因中断优先级分组没设或者NVIC_PriorityGroupConfig在NVIC_Init之后才调用。F407 的优先级分组是全局的必须在所有NVIC_Init之前设好。另外如果中断服务函数里没有清USART_IT_RXNE标志会反复进中断主循环被拖死。解决把NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2)放到main开头所有外设初始化之前。中断服务函数里第一句就USART_ClearITPendingBit(USARTx, USART_IT_RXNE)再读USART_ReceiveData。5.3 现象DMA 发送的数据只有第一个字节对后面全是错位原因DMA_MemoryInc设成了Disable内存地址不自增DMA 每次都从同一个地址搬数据发出去的全是第一个字节。或者DMA_PeripheralDataSize和DMA_MemoryDataSize不一致比如外设设 Byte、内存设 HalfWord数据就乱了。解决发送方向必须DMA_MemoryInc_Enable外设地址DMA_PeripheralInc_Disable。两边数据宽度统一设DMA_PeripheralDataSize_Byte和DMA_MemoryDataSize_Byte除非你确定要传半字。5.4 现象六路串口同时收发时系统偶尔死机原因DMA 传输完成中断和串口接收中断的优先级冲突或者两个中断里操作了同一个全局变量没有保护。标准库没有自动的临界区保护六路并发时共享的发送缓冲区索引、接收环形缓冲区指针都可能被踩。解决给每路串口独立的发送缓冲区和接收缓冲区不要共用。DMA 传输完成中断的抢占优先级设得比串口接收中断低让接收先处理。共享的计数器用__disable_irq()和__enable_irq()包住或者用原子操作。5.5 现象波特率高的串口115200误码率高低的正常原因APB 时钟分频不对导致高波特率下 BRR 误差超过 3%。F407 的 USART1 和 USART6 挂 APB2最高 84MHzUSART2~5 挂 APB1最高 42MHz。如果 APB1 分频设成 2 得到 84MHz超了 42MHz 上限时钟树虽然能跑但串口时序会飘。解决确认RCC_PCLK1Config是RCC_HCLK_Div4RCC_PCLK2Config是RCC_HCLK_Div2。用示波器量一下 TX 引脚波形115200 波特率下位宽应该是 8.68us 左右偏差超过 0.3us 就要回头查时钟。6. 装完怎么验证六路串口回环测试和 DMA 发送计数环境搭完别急着接实际模块先做回环测试。把每路串口的 TX 和 RX 用杜邦线短接发什么收什么六路同时跑看有没有丢包。我一般写一个简单的测试函数/* 六路串口回环测试发送递增数据接收比对 */ void USART_LoopbackTest(void) { uint8_t tx_data[6] {0x01, 0x02, 0x03, 0x04, 0x05, 0x06}; uint8_t rx_data[6] {0}; USART_TypeDef* usart_list[6] {USART1, USART2, USART3, UART4, UART5, USART6}; DMA_Stream_TypeDef* dma_stream[6] {DMA2_Stream7, DMA1_Stream6, DMA1_Stream3, DMA1_Stream4, DMA1_Stream7, DMA2_Stream6}; uint32_t dma_flag[6] {DMA_FLAG_TCIF7, DMA_FLAG_TCIF6, DMA_FLAG_TCIF3, DMA_FLAG_TCIF4, DMA_FLAG_TCIF7, DMA_FLAG_TCIF6}; DMA_TypeDef* dma_ctrl[6] {DMA2, DMA1, DMA1, DMA1, DMA1, DMA2}; for (int i 0; i 6; i) { /* 启动 DMA 发送 */ DMA_SetCurrDataCounter(dma_stream[i], 1); DMA_Cmd(dma_stream[i], ENABLE); /* 等待发送完成 */ while (DMA_GetFlagStatus(dma_ctrl[i], dma_flag[i]) RESET); DMA_ClearFlag(dma_ctrl[i], dma_flag[i]); /* 等待接收 */ while (USART_GetFlagStatus(usart_list[i], USART_FLAG_RXNE) RESET); rx_data[i] USART_ReceiveData(usart_list[i]); /* 比对 */ if (rx_data[i] ! tx_data[i]) { /* 记录哪一路出错方便定位 */ printf(USART%d loopback fail: tx0x%02X rx0x%02X\r\n, i 1, tx_data[i], rx_data[i]); } } }这段代码一次测六路每路发一个字节收一个字节。如果某一路rx_data和tx_data对不上先查这一路的 GPIO 复用和 DMA 映射再查波特率。六路全过之后再把数据长度加到 64 字节、256 字节测 DMA 的BufferSize和内存自增有没有问题。验证 DMA 发送是否真的搬完了数据不能只看DMA_Cmd使能了要看DMA_GetCurrDataCounter是不是减到 0。我习惯在发送完成后读一次计数器/* 确认 DMA 实际传输的字节数 */ uint16_t remaining DMA_GetCurrDataCounter(DMA2_Stream7); if (remaining ! 0) { /* 没传完可能是流被意外关闭或请求没使能 */ printf(DMA not finished, remaining%d\r\n, remaining); }还有一个容易忽略的点六路串口同时做回环时如果某一路的接收中断优先级设得太低被其他串口的中断连续打断USART_FLAG_RXNE可能溢出。现象是偶尔收到 0x00 或者上一次的残留数据。解决办法是把接收中断的抢占优先级统一设成一样响应优先级按串口编号排保证每个中断都能及时进。从那以后我每次配多路串口都强制走一遍「时钟树确认 → 引脚复用核对 → DMA 映射表对照 → 回环测试」这四步少一步后面就要花两倍时间查玄学问题。希望帮到你。本文还有配套的精品资源点击获取