STM32F405五路串口通信实战:从初始化到DMA收发完整指南
简介为stm32f405rgt6开发者提供的串口1~5通信代码包完整覆盖USART1至USART5的数据接收、缓存计数与标志管理主要面向嵌入式硬件、单片机及ARM相关开发者尤其适合需要快速搭建多串口通信框架的工程人员。压缩包共145个文件以C源码和头文件为主体42个h、36个c同时包含Keil工程配置文件uvprojx、uvopt、编译生成文件hex、axf、map以及STM32F4标准外设库驱动如stm32f4xx_usart.c、stm32f4xx_rcc.c等资源整体仅2.56MB便于下载与解压。目前已有1214人学习代码中为每个串口独立定义了接收缓冲区、接收计数及接收完成标志变量和宏定义清晰便于读者理解串口收发流程并快速移植到自身项目中也可在此基础上进一步扩展DMA或中断接收逻辑具有较好的参考与复用价值。1. 上手前的硬件盘点5路串口到底怎么分配用STM32F405RGT6做多串口通信这个需求在实际项目里太常见了。一颗芯片同时接GPS模块、传感器、上位机、蓝牙模块再加上一路调试串口正好5路。F405这颗料在串口资源上确实厚道片上集成了USART1、USART2、USART3、UART4、UART5一共5路独立的串口外设基本能覆盖绝大多数多设备通信场景。这里写的就是基于标准外设库的直接可用的代码梳理包含初始化、发送、接收、中断处理这些核心内容。在动手写代码之前先得把芯片的串口资源分布搞清楚否则代码写完了跑起来才发现引脚映射不对排查起来非常痛苦。1.1 先分清USART和UART的区别很多朋友从STM32F103转过来看到F405上既有USART又有UART型号名字里带“UART4”“UART5”的容易误以为跟USART不一样。实际上差异很小USART支持同步模式多一个SCLK时钟引脚输出可以外接需要时钟同步的设备UART只支持异步模式就是最常用的TX/RX两根线收发。我们的日常串口通信基本都是异步模式所以USART1/2/3和UART4/5在用法上没有本质区别初始化时配置的参数也完全一样。F405上这5路串口总线挂载情况不一样这个非常重要。USART1挂在APB2上时钟频率84MHzUSART2、USART3、UART4、UART5挂在APB1上时钟频率42MHz。很多从F103迁移过来的工程沿用72MHz/36MHz的时钟参数在F405上就会出问题——波特率算出来的误差很大115200这种常用波特率实测会乱码。这一点在后面初始化代码里会具体体现。1.2 引脚映射与复用功能表F4系列和F103有一个非常大的区别F103的串口引脚是固定的F4系列则是通过复用功能Alternate Function映射的。也就是说串口外设的信号要想从引脚输出必须先把这个引脚配置成对应的AF模式否则引脚就是个普通GPIO数据根本出不去。常用的默认引脚映射如下串口TX引脚RX引脚复用编号总线时钟USART1PA9PA10AF7APB2 84MHzUSART2PA2PA3AF7APB1 42MHzUSART3PB10PB11AF7APB1 42MHzUART4PA0PA1AF8APB1 42MHzUART5PC12PD2AF8APB1 42MHz这5组引脚之间互相不冲突做5路串口同时通信非常舒服不用抢引脚。当然F405的串口引脚不止这一套映射比如USART2可以重映射到PD5/PD6USART3可以到PC10/PC11但默认这组是最好用的原理图设计也最顺我建议没有特殊原因别去重映射省得看数据手册的AF表看得头晕。2. 核心代码基于标准库的串口初始化这里以标准外设库STM32F4xx_DSP_StdPeriph_Lib为例因为现在网上能找到的例程和参考代码大部分还是标准库风格而且标准库的寄存器操作思路更直观理解起来比HAL库的层层封装容易得多。HAL库版本的差异我在后面单列一小节说明。2.1 时钟使能这一步别搞错F405的时钟树和F103不一样初始化时先分清APB1和APB2。USART1的时钟使能位在RCC_APB2PeriphClockCmd里USART2/3/UART4/UART5的时钟使能位在RCC_APB1PeriphClockCmd里。同时还要使能对应GPIO端口的AHB1时钟。// 使能串口时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); // USART1挂APB2 RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART2, ENABLE); // USART2挂APB1 RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE); // USART3挂APB1 RCC_APB1PeriphClockCmd(RCC_APB1Periph_UART4, ENABLE); // UART4挂APB1 RCC_APB1PeriphClockCmd(RCC_APB1Periph_UART5, ENABLE); // UART5挂APB1 // 使能GPIO时钟 RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA | RCC_AHB1Periph_GPIOB | RCC_AHB1Periph_GPIOC | RCC_AHB1Periph_GPIOD, ENABLE);这里有个经验串口时钟和GPIO时钟分开使能如果只使能了GPIO时钟忘了串口时钟初始化USART时写寄存器没反应如果只使能串口时钟忘了GPIO时钟GPIO配置写不进去。两个都开是基本前提。2.2 GPIO复用模式配置F4系列的GPIO配置比F103多两个参数GPIO_OType和GPIO_PuRd。串口的TX/RX引脚要配置成复用功能GPIO_Mode_AF输出类型推挽上拉。GPIO_InitTypeDef GPIO_InitStructure; // USART1 引脚 PA9(TX) PA10(RX) GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1); GPIO_PinAFConfig(GPIOA, GPIO_PinSource10, GPIO_AF_USART1); 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这个函数是F4系列特有的初始化步骤漏掉它是最常见的“串口不工作”原因。F103的代码里没有这一步直接从F103移植过来的朋友最容易在这个地方翻车。2.3 USART外设初始化串口参数配置比较常规8位数据位、1位停止位、无校验、无硬件流控、收发都开。关键是波特率。USART1挂APB284MHzUSART2/3/UART4/5挂APB142MHz初始化结构体里只要把波特率数值填进去库函数会根据外设挂载的总线频率自动计算分频系数所以不用手工算BRR寄存器前提是RCC时钟配置正确。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);USART2/3/UART4/5的初始化代码结构完全一样只是把USART1替换成对应的外设名引脚按1.2节的表格换成对应引脚。这里不重复贴5遍代码了后面第4节会给一个统一管理多串口的模板代码复用性更好。2.4 HAL库版本怎么写如果你用的是STM32CubeMX生成的HAL库工程初始化代码会被CubeMX自动生成核心差异在于UART_HandleTypeDef huart1; void MX_USART1_UART_Init(void) { huart1.Instance USART1; huart1.Init.BaudRate 115200; huart1.Init.WordLength UART_WORDLENGTH_8B; huart1.Init.StopBits UART_STOPBITS_1; huart1.Init.Parity UART_PARITY_NONE; huart1.Init.Mode UART_MODE_TX_RX; huart1.Init.HwFlowCtl UART_HWCONTROL_NONE; huart1.Init.OverSampling UART_OVERSAMPLING_16; HAL_UART_Init(huart1); }注意HAL库的引脚复用配置在HAL_UART_MspInit函数里由CubeMX自动生成这一点比标准库省心。但HAL库的中断接收不是直接在中断服务函数里读数据而是要用HAL_UART_Receive_IT启动一轮接收数据到了之后进回调函数HAL_UART_RxCpltCallback处理完后需要再次调用HAL_UART_Receive_IT才能继续接收下一字节。这个机制刚到HAL的人经常忘了重新启动接收导致只能收到一帧数据。3. 数据收发从轮询到中断再到空闲DMA有了串口基础配置接下来是收发逻辑。不同场景用不同的收发方式下面按从简到繁的顺序说。3.1 最简单的轮询发送和接收发送用TXE标志判断发送数据寄存器是否为空void USART_SendByte(USART_TypeDef* USARTx, uint8_t data) { while (USART_GetFlagStatus(USARTx, USART_FLAG_TXE) RESET); USART_SendData(USARTx, data); } void USART_SendString(USART_TypeDef* USARTx, uint8_t* str) { while (*str) { USART_SendByte(USARTx, *str); } }接收用RXNE标志判断是否收到数据uint8_t USART_ReceiveByte(USART_TypeDef* USARTx) { while (USART_GetFlagStatus(USARTx, USART_FLAG_RXNE) RESET); return USART_ReceiveData(USARTx); }轮询方式适合主循环里周期性查询接收比如每10ms查一次有没有数据。缺点很明显如果数据来得又快又急主循环忙别的事情时可能丢数据。对实时性要求高的场景得用中断。3.2 中断接收的标准写法中断接收的核心是在初始化时打开接收中断在中断服务函数里取数据。以USART1为例// 初始化时加这两行 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); NVIC_InitTypeDef NVIC_InitStructure; 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); // 中断服务函数 void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART1); // 这里把data存入缓冲区或者做帧处理 USART_ClearITPendingBit(USART1, USART_IT_RXNE); } }注意USART_ReceiveData函数内部会清除RXNE标志位所以中断里不主动清问题也不大但保险起见加上USART_ClearITPendingBit更规范防止某些边界情况下标志位状态异常。五个串口的IRQHandler分别是USART1_IRQHandler、USART2_IRQHandler、USART3_IRQHandler、UART4_IRQHandler、UART5_IRQHandler。每个串口一个中断服务函数函数名是启动文件里定死的不能自己改名。3.3 空闲中断DMA接收不定长数据如果协议帧是不定长的用固定字节中断接收往往要配合超时判断或者帧头帧尾解析稍微有点繁琐。F405的串口支持空闲IDLE中断一帧数据发完总线空闲了就触发一次中断。配合DMA可以实现“收完一整帧再处理”的效果CPU几乎不干预数据搬运。思路是串口RX用DMA接收每收到一个字节DMA自动搬运到缓冲区。同时开启串口空闲中断当一帧数据发送完毕、总线进入空闲状态触发IDLE中断此时在中断里读取DMA当前剩余计数就能算出这一帧收到了多少字节。#define RX_BUF_SIZE 256 uint8_t rx_buf[RX_BUF_SIZE]; uint16_t rx_len 0; void UART_DMA_Init(void) { DMA_InitTypeDef DMA_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_DMA2, ENABLE); // USART1_RX 对应 DMA2_Stream2 Channel4 DMA_InitStructure.DMA_Channel DMA_Channel_4; DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)(USART1-DR); DMA_InitStructure.DMA_Memory0BaseAddr (uint32_t)rx_buf; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralToMemory; DMA_InitStructure.DMA_BufferSize 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_InitStructure.DMA_FIFOThreshold DMA_FIFOThreshold_1QuarterFull; DMA_InitStructure.DMA_MemoryBurst DMA_MemoryBurst_Single; DMA_InitStructure.DMA_PeripheralBurst DMA_PeripheralBurst_Single; DMA_Init(DMA2_Stream2, DMA_InitStructure); DMA_Cmd(DMA2_Stream2, ENABLE); USART_DMACmd(USART1, USART_DMAReq_RX, ENABLE); USART_ITConfig(USART1, USART_IT_IDLE, ENABLE); } void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_IDLE) ! RESET) { USART_ReceiveData(USART1); // 读DR清IDLE标志 DMA_Cmd(DMA2_Stream2, DISABLE); rx_len RX_BUF_SIZE - DMA_GetCurrDataCounter(DMA2_Stream2); DMA_SetCurrDataCounter(DMA2_Stream2, RX_BUF_SIZE); DMA_Cmd(DMA2_Stream2, ENABLE); // 此时rx_buf里有rx_len个字节的数据可以交给协议栈处理 } }这个模式实测在115200波特率下每帧不超过256字节跑得非常稳。需要注意F405的USART1_RX对应DMA2_Stream2_Channel4USART2_RX对应DMA1_Stream5_Channel4每个串口的DMA通道和Stream号都不同用的时候要对着参考手册查这个表在STM32F405的数据手册DMA章节里有网上也能查到我就不贴全表格了你们实际配的时候一定要核对。4. 五路串口工程模板统一管理不迷糊实际工程里5路串口的协议和处理逻辑各不相同如果每个串口的初始化、发送、接收都单独写一坨代码工程文件会非常乱。我习惯用一个结构体把每个串口的信息封装起来统一索引、统一操作。4.1 用结构体统一管理多串口typedef struct { USART_TypeDef* USARTx; // 外设基地址 uint32_t RCC_APBxPeriph_USARTx; // 外设时钟 uint32_t RCC_AHB1Periph_GPIOx; // GPIO时钟 uint16_t TX_Pin; uint16_t RX_Pin; uint8_t TX_Source; uint8_t RX_Source; uint8_t AF_Value; GPIO_TypeDef* GPIOx; uint8_t IRQn; } USART_Config_t; USART_Config_t usart_config[5] { {USART1, RCC_APB2Periph_USART1, RCC_AHB1Periph_GPIOA, GPIO_Pin_9, GPIO_Pin_10, GPIO_PinSource9, GPIO_PinSource10, GPIO_AF_USART1, GPIOA, USART1_IRQn}, {USART2, RCC_APB1Periph_USART2, RCC_AHB1Periph_GPIOA, GPIO_Pin_2, GPIO_Pin_3, GPIO_PinSource2, GPIO_PinSource3, GPIO_AF_USART2, GPIOA, USART2_IRQn}, // 以此类推USART3/UART4/UART5按表格填充 };有了这张配置表初始化函数就是循环调一遍收发函数只传外设基地址就行代码量能省一大半。更重要的是后面如果换了引脚只需要改配置表里的Pin和Source不用动任何逻辑代码。4.2 一个实际的调用示例举例一个典型场景串口1接上位机串口2接GPS串口3接传感器串口4接蓝牙串口5做调试日志输出。那么主循环里就可以while (1) { // 串口1 收到上位机指令回显应答 if (rx_flag[0]) { USART_SendString(USART1, (uint8_t*)cmd ok\r\n); rx_flag[0] 0; } // 串口5 打印调试日志 USART_SendString(USART5, (uint8_t*)system running\r\n); delay_ms(100); }每个串口的中断服务函数只做一件事把收到的数据放到对应的环形缓冲区或者置一个标志位具体协议解析放到主循环里做。这样中断里的处理时间很短不容易丢数据主循环里又可以根据标志位判断数据到达逻辑非常清晰。5. 调试工具与常见问题排查5.1 串口调试助手和USB转串口芯片调试5路串口工具很重要。上位机推荐用支持多串口同时打开、带波形显示、支持定时发送的调试助手比如正点原子的XCOM、野火的串口调试助手或者串口猎人、SSCOM。这些工具在Gitee和CSDN上都能找到下载基本都免费。USB转串口芯片方面市面上最常见的是CH340和FTDI的FT232。CH340便宜几块钱一根线Windows 10/11插上一般能自动识别驱动个别精简版系统需要去官网手动装一下驱动。FT232稳定但贵适合工业现场。如果有硬件条件还可以用逻辑分析仪直接抓TX/RX引脚的波形排查波特率不匹配、接线错误这类问题非常直观。5.2 常见问题速查表现象可能原因排查方法完全没有输出GPIO没配成AF模式检查是否调用了GPIO_PinAFConfig完全没有输出串口时钟没使能检查RCC_APB1/APB2PeriphClockCmd乱码波特率不匹配检查双方波特率检查总线频率配置乱码时钟树配置不对F405主频168MHzAPB142MHzAPB284MHz只能收到第一个字节HAL库中断接收未重新启动回调函数里重新调用HAL_UART_Receive_IT接收一段时间后死机缓冲区溢出加大缓冲区或用环形缓冲区数据都是0xFFRX引脚悬空/虚焊检查硬件连接示波器量波形还有一个坑值得单独提F405复位后串口TX引脚默认是高电平如果板子上TX/RX接反了通信完全不通但示波器上能看到波形。这个时候先别急着改代码拿万用表量一下两边引脚有没有接反。我排查过好几个“串口死活不通”的板子最后都发现是杜邦线插反了。6. 一点实操体会串口是嵌入式开发最常用的调试和通信手段F405这颗芯片5路串口全部启用后做一个多设备网关绰绰有余。如果手上同时有F103和F405的工程要注意它们最大的区别就是GPIO复用配置和时钟树参数这两个地方搞定了代码迁移非常快。另外建议从一开始就把串口驱动封装成独立的模块发送函数、接收回调、缓冲区管理分开写。5路串口的工程如果不封装代码会越来越难维护封装好后加一个第6路串口虽然F405没有或者换一个外设主逻辑都不用动。最后分享一个排查串口问题时的小技巧先用串口1做最简单的回环测试就是初始化好之后把TX和RX用杜邦线短接自己给自己发数据。如果回环通了说明串口外设、引脚、中断逻辑都没问题。这时候再接外部设备问题范围就缩小到外部设备的协议、波特率、接线上了。一步一步排查比拿到板子就闷头写代码要高效得多。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →