恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
STM32F405RGT6串口例程:从点灯到可靠收发调试
首页
资讯中心
/
STM32F405RGT6串口例程:从点灯到可靠收发调试
STM32F405RGT6串口例程:从点灯到可靠收发调试
发布时间:2026/10/16 4:42:30
简介这份压缩包提供了一套可直接参考的 STM32F405RGT6 串口通信例程面向正在学习 STM32 HAL 库、需要快速实现 USART 收发与调试的嵌入式开发者。例程以串口一为主线覆盖引脚复用配置、波特率与数据位停止位设置、阻塞式与非阻塞式收发、接收中断回显、DMA 传输以及 HAL 库底层初始化逻辑适合作为工程模板迁移到实际项目中。压缩包共 460 个文件主要内容包括 C 语言源文件、头文件、Keil 工程文件、编译生成的 axf 与 hex 文件、批处理辅助脚本等总大小约 20.24MB。已有 3748 人学习下载。通过对照例程能够理解串口发送接收函数在中断和 DMA 模式下的调用方式掌握中断服务程序与 MSP 初始化写法并借鉴工程内的调试技巧与代码组织方式为后续传感器通信、上位机交互等开发提供直接素材。1. STM32F405RGT6串口例程从会点灯到敢用串口调试中间差一个可靠收发调过F405板子的人都有这种经历屏幕不亮、传感器没反应、示波器又不在手边这时候唯一能指望的就是一根USB转串口线。串口成了嵌入式调试里最后一道真相通道。这套STM32F4串口例程的核心价值就是把单片机串口调试从「对着寄存器手册硬搓」变成「拿HAL库例程直接改」——只要理清GPIO复用、USART外设时钟、中断接收这三件事printf和收发回调就能稳定跑起来。适合刚入门F4、想从点灯跨到串口通信的开发者也适合做产测或设备调试框架时想抄一份串口底层的熟手。2. 硬件底子与技术选型F405RGT6的USART资源、时钟约束与引脚复用2.1 串口资源盘点F405RGT6上能用的6个串口与引脚分布STM32F405RGT6是LQFP64封装1MB Flash加192KB RAM裸机或跑RTOS都够用。串口外设一共给了6个USART1、USART2、USART3再加UART4、UART5、USART6。前三个支持同步模式和硬件流控后三个是精简收发。例程默认走USART1原因很直接它挂在APB2总线上外设时钟84MHz以后想把波特率拉到460800甚至921600时余量更足。引脚分布这张表值得背下来它直接决定了你画板子时怎么绕开引脚冲突串口TX可用引脚RX可用引脚复用功能USART1PA9 / PB6PA10 / PB7AF7USART2PA2 / PD5PA3 / PD6AF7USART3PB10 / PC10 / PD8PB11 / PC11 / PD9AF7UART4PA0 / PC10PA1 / PC11AF8UART5PC12PD2AF8USART6PC6PC7AF8注意USART6在F405RGT6这颗64脚芯片上只有PC6/PC7一组可用PG9/PG14在封装里没有引出来想再多配一组就是白搭。UART4的PA0/PA1和USART1的PA9/PA10在同一个GPIOA上复用功能不同如果板子上PA0接了按键、PA1接了LED那UART4基本就废了。选串口之前先打开数据手册的引脚复用对照表过一遍能省后面一晚上的查线时间。2.2 时钟树与波特率计算为什么USART1能跑更高波特率芯片上电默认跑HSI16MHz这个状态下直接配串口波特率误差会大到乱码。例程里的SystemClock_Config会把系统时钟拉到168MHzAHB总线168MHzAPB1分频后42MHzAPB2分频后84MHz。USART1和USART6挂在APB2上USART2/3和UART4/5/6挂在APB1上。不同外设的基准时钟不一样同样的波特率写进BRR寄存器实际误差完全可能不同。HAL库配波特率时内部做的事情是过采样16倍的前提下把外设时钟除以16再除以目标波特率得到分频系数再拆成整数部分和小数部分写进BRR。以USART1跑115200为例84MHz除以16再除以115200结果是45.5729。小数部分能不能被硬件精确表示直接决定收发误码率。USART1在这个组合下误差极小但同一份配置挪到APB1上的USART2基准时钟变成42MHz分频结果是22.7864误差通常比USART1大一些。调试时发现串口助手收到乱码或偶发丢字节先别怀疑杜邦线按复位让SystemClock_Config完整跑一遍再看。2.3 技术选型为什么串口调试至今没被SWD取代有人觉得F405有SWD接口用调试器看寄存器多方便为什么还要折腾串口。SWD的软肋在于现场要带调试器、要IDE在线、要目标板供电且复位时序正常。设备已经跑起来或者装进机箱里SWD未必连得上但串口三根线就能接。更关键的是串口天然双向PC能收数据也能下命令批量产测时发指令控制设备自检、读序列号、标定参数这套例程可以直接复用。所以就算做了很多年嵌入式串口依然是单片机串口调试的首选通道它走的是最少依赖、最快出真相那条路。Debug口留给复杂问题单步串口留给日常通信和日志。3. 工程结构与核心代码走读一次串口回显从进入到返回的完整链路3.1 工程目录CubeMX生成的每一层文件在干什么以STM32CubeMX生成的MDK工程为例目录结构长这样stm32f405_usart_demo/ ├─ Core/ │ ├─ Inc/ main.h, usart.h, gpio.h │ └─ Src/ main.c, usart.c, gpio.c, stm32f4xx_it.c ├─ Drivers/ │ ├─ CMSIS/ 启动文件和系统初始化 │ └─ STM32F4xx_HAL_Driver/ HAL库全部源码 ├─ MDK-ARM/ 工程文件、启动汇编、链接脚本 └─ .mxproject CubeMX工程描述Core里是用户代码Drivers里是库MDK-ARM里是工具链文件。实际改代码只动Core这一点很重要。usart.c管串口外设初始化gpio.c管引脚复用stm32f4xx_it.c是中断入口。新手最容易犯的错是HAL库不满足需求时直接改Drivers里的库函数编译能过但下次CubeMX重新生成或升级固件包改动全被覆盖。我一般只在Core里做二次封装库能力不够就在Core/Src下自己加一层wrapper升级库不心疼。3.2 串口初始化完整代码GPIO复用、USART参数、NVIC三件事例程的初始化函数把外设时钟、引脚复用、串口参数、中断使能串在一起。核心是这两段void MX_USART1_UART_Init(void) { // USART1挂在APB2上外设时钟84MHz huart1.Instance USART1; huart1.Init.BaudRate 115200; // 波特率PC串口助手必须一致 huart1.Init.WordLength UART_WORDLENGTH_8B; // 8位数据位 huart1.Init.StopBits UART_STOPBITS_1; // 1位停止位 huart1.Init.Parity UART_PARITY_NONE; // 无校验 huart1.Init.Mode UART_MODE_TX_RX; // 收发都要用 huart1.Init.HwFlowCtl UART_HWCONTROL_NONE; // 不使能RTS/CTS huart1.Init.OverSampling UART_OVERSAMPLING_16; // 16倍过采样默认 if (HAL_UART_Init(huart1) ! HAL_OK) { Error_Handler(); // 初始化失败进这里调试时在此打断点 } }HAL_UART_Init内部会回调HAL_UART_MspInit引脚复用和NVIC都放这里。这是CubeMX的固定套路把外设参数和底层引脚分离void HAL_UART_MspInit(UART_HandleTypeDef *huart) { GPIO_InitTypeDef GPIO_InitStruct {0}; if (huart-Instance USART1) { __HAL_RCC_USART1_CLK_ENABLE(); // 打开USART1时钟 __HAL_RCC_GPIOA_CLK_ENABLE(); // 打开GPIOA时钟 // PA9复用为USART1_TXPA10复用为USART1_RX GPIO_InitStruct.Pin GPIO_PIN_9 | GPIO_PIN_10; GPIO_InitStruct.Mode GPIO_MODE_AF_PP; // 复用推挽输出 GPIO_InitStruct.Pull GPIO_PULLUP; // 空闲拉高防误触发 GPIO_InitStruct.Speed GPIO_SPEED_FREQ_VERY_HIGH; // 高速应用 GPIO_InitStruct.Alternate GPIO_AF7_USART1; // F4的USART1复用AF7 HAL_GPIO_Init(GPIOA, GPIO_InitStruct); HAL_NVIC_SetPriority(USART1_IRQn, 0, 0); // 优先级可抢占 HAL_NVIC_EnableIRQ(USART1_IRQn); } }逻辑说明HAL_UART_Init先做参数校验随后调用MspInit完成时钟、GPIO、NVIC三件事。GPIO的Alternate必须查芯片复用表USART1/2/3是AF7UART4/5/6和USART6是AF8写错编译照样通过但引脚完全不工作。NVIC优先级在裸机里0到15都能跑但如果后面移植FreeRTOS串口中断优先级必须高于可屏蔽的最高优先级阈值否则中断进不来数据全丢。参数说明BaudRate要和PC端串口助手完全一致WordLength、StopBits、Parity组合起来就是常见的8-N-1。OverSampling默认16只有跑921600这种极高波特率时才考虑切到8倍换来更高的分频精度但抗干扰变差。Mode只写TX的话RX引脚不会被初始化后面接收必然失败。3.3 数据收发链路printf重定向、中断接收、回显主循环串口例程真正跑起来看的是数据链路。先把printf挂到串口上以后所有打印都走USART1// 重定向printf到USART1MDK工程需开启MicroLIB int fputc(int ch, FILE *f) { // 等待发送数据寄存器为空满就死等 while (__HAL_UART_GET_FLAG(huart1, UART_FLAG_TXE) RESET); // 字符写入数据寄存器硬件自动移出 huart1.Instance-TDR (uint8_t)ch; return ch; }中断接收这段是例程的灵魂。HAL库的做法是每次只收1字节完成回调里再注册下一次形成连续接收uint8_t rx_byte 0; uint8_t rx_buffer[64]; uint16_t rx_index 0; // 串口1中断服务函数由向量表自动跳入 void USART1_IRQHandler(void) { HAL_UART_IRQHandler(huart1); // HAL库统一分发中断标志 } // 每次完成1字节接收后自动触发 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { if (rx_index sizeof(rx_buffer)) // 防缓冲区溢出 { rx_buffer[rx_index] rx_byte; // 存入自定义缓冲区 } // 立即注册下一次1字节接收否则中断只触发一次 HAL_UART_Receive_IT(huart1, rx_byte, 1); } }主循环把收到的内容原样发回PC形成回显Demo用来验证双向通路int main(void) { HAL_Init(); SystemClock_Config(); // 168MHz系统时钟 MX_USART1_UART_Init(); // 串口初始化 HAL_UART_Receive_IT(huart1, rx_byte, 1); // 启动第一次中断接收 while (1) { if (rx_index 0) // 有数据就发回PC { HAL_UART_Transmit(huart1, rx_buffer, rx_index, 100); rx_index 0; // 清空计数等下一批 } } }逻辑说明HAL_UART_Receive_IT把接收缓冲指针、接收字节数存进huart结构体然后使能RXNE中断。中断进来后HAL_UART_IRQHandler判断RXNE标志读到数据、清标志、回调一次性做完。所以回调末尾必须重新调用HAL_UART_Receive_IT否则只收第一批就再也不进回调这是最高频的中断收发翻车点。参数说明HAL_UART_Transmit最后那个100是超时时间单位毫秒指发送被占用时最多等多久返回HAL_TIMEOUT说明总线异常或时钟停了。rx_index归零建议放在发送完成后我先拷贝再清零避免回调在发送间隙又写入导致丢数。实际项目里主循环的发送逻辑我会抽成函数回调只负责收主循环只负责发。4. 编译烧录与串口调试从下载到验证回显的完整过程4.1 准备工具链最少需要板子、调试器、电脑三样东西跑这套例程硬件上需要三样一块F405RGT6核心板板上带USB转串口最省事、一个ST-Link调试器、一台装了MDK-ARM和STM32CubeMX的电脑。ST-Link走SWD只需要四根线SWDIO、SWCLK、GND、3.3V别接反接反烧不坏但会连不上。打开工程之前先在MDK的Run-Time Environment里确认HAL库版本不同版本API基本一致但如果拿旧工程强行配新版固件包偶发出现HAL_UART_Receive_IT函数签名不一致的编译报错先检查这里别一上来就怀疑代码。如果手头没有ST-Link用USB转串口小板也能烧把BOOT0拉高复位后芯片进入系统存储器里的bootloader原厂工具通过串口把程序写进Flash写完复位一次BOOT0拉低程序从用户Flash启动。注意串口ISP用的是芯片的USART1还是USART2不同板子设计不一样我遇到过一张板子BOOT0引脚默认被上拉到3.3V程序永远跑不起来后来才发现要先复位再等bootloader握手顺序反了就会卡在连接阶段。4.2 编译与烧录从打开工程到看见LED闪烁MDK打开工程后第一步是检查Target里选的芯片型号是不是STM32F405RGT6型号选错会导致flash容量和地址映射不对烧录时校验失败。第二步编译正常情况0 Error有Error先看是不是没有添加HAL库源文件或者头文件路径缺失。第三步配置调试器Options for Target里的Debug选ST-LinkSettings确认能读到芯片ID烧录算法选STM32F4xx Flash。最后点Load烧录进度条走完按一下复位。烧录成功那一刻串口助手里应该能看到例程的启动打印这是整个调试流程里最有成就感的瞬间。烧录失败先看三处SWDIO和SWCLK是不是接反了目标板供电是不是稳定调试器驱动是不是被杀毒软件拦了。前两者占九成原因。4.3 串口调试三步验证从上电打印到回显数据流验证分三步走。第一步开串口助手选对COM口波特率1152008-N-1无校验关闭硬件流控。第二步按复位键串口助手窗口里能看到例程在main开头打印的启动信息这说明TX链路是通的。第三步在下发输入框里敲一串字符点发送例程把同样的内容原样回显这说明RX中断、缓冲区、TX发送三段链路全部打通。如果回显只有一部分或者字符面目全非先检查串口助手右下角的DTR和RTS是不是自动勾上了。很多USB转串口板子的DTR/RTS引脚连着MCU的复位和BOOT0勾选后会触发芯片不停复位表现为发送后没有任何响应或打印到一半重启。这个问题在调试例程时极其常见我一开始也以为是代码问题查了半天才发现是串口助手的默认勾选项在捣乱。回显动作本身同时验证了中断接收和发送链路链路分开排查不要在GPIO和中断里来回猜。5. 避坑指南串口例程调试中5个真实翻车现场与排查步骤5.1 现象一PC收到的全是乱码现象板子复位后串口助手收到一串乱码或者中文文本彻底错乱。原因最常见是波特率不匹配PC端设了115200而板子实际跑9600或921600。第二常见是SystemClock_Config没跑或者配错APB1、APB2分频值和预期不一致分频基准就歪了。第三是USB转串口小板质量差板上电平噪音大接收端频繁误码。解决先用9600、115200这几个常见档位轮换测试锁定范围再确认SystemClock_Config里APB1分频2、APB2不分频外设时钟分别是42MHz和84MHz最后查硬件链路杜邦线超过20cm就换短一点的或者降到38400验证。还有一种隐蔽情况是XTAL晶振没起振系统时钟悄悄回退到HSI打印内容会偏但不会全乱排查时看串口波特率漂移方向能辅助判断。5.2 现象二板子能发不能收现象PC能看到板子的启动打印但发任何命令板子都没反应像是接收功能被砍了。原因RX引脚没有正确复用GPIO初始化里只配了TX那半。PA10没被设置成复用模式时引脚是默认的浮空输入电平飘忽不定根本触发不了RXNE标志。另一种情况是中断使能了但没调HAL_UART_Receive_IT相当于接收通道没挂上钩子硬件能收到但软件不处理。解决断点打在HAL_UART_RxCpltCallback里能看到回调说明中断通路没断。检查GPIO初始化结构体里Pin是否同时包含GPIO_PIN_9和GPIO_PIN_10Mode是否为GPIO_MODE_AF_PP。再查main里有没有在初始化后调用HAL_UART_Receive_IT以及回调里有没有再次调用。把两个调用点都打上断点能清楚看到调用链在哪里断了。5.3 现象三串口中断进不去现象数据明明从PC发过来了调试器里甚至能看到RXNE标志置位但回调函数一次都不执行。原因三选一。中断服务函数USART1_IRQHandler没有被跳到向量表被别的东西覆盖或者HAL_UART_IRQHandler没被调用又或者NVIC优先级被屏蔽了。最隐蔽的是后续移植了RTOSFreeRTOS在临界区里屏蔽了所有中断串口中断优先级如果低于可屏蔽阈值就会一直等不到响应。解决先在USART1_IRQHandler入口打断点进不来就是向量表或启动文件问题检查启动文件里USART1_IRQHandler符号是否与中断向量表一致。入口能进但回调不进查HAL_UART_IRQHandler里对接收状态机的判断是不是上一次接收还没完成。如果跑了RTOS把串口中断优先级调到最高抢占级别再配合portMAX_DELAY的消息队列基本能根治。5.4 现象四printf输出卡死现象程序执行到某条printf后整体卡住LED不闪了整个主循环像是被定住。原因fputc里用了while等TXE标志这个循环没有超时保护。如果串口初始化失败、时钟没开、发送引脚被占用TXE永远不会置位程序就死在那里。另外高频打印也是罪魁祸首TDR还没清空又来一条虽然不会真死但主循环耗时爆炸看起来像死机。解决fputc加超时判断比如等了一个计数周期还没空就强制返回保证即使串口坏了主逻辑还能跑。高频日志场景加一条简单的打印节流限制每100ms最多打一条。想彻底解决就用中断或DMA发送把发送从主循环里摘出去这是后面二次开发章节重点讲的方向。5.5 现象五高波特率下总是丢字符现象115200一切正常升到460800或921600就吞字节收十条丢两三条。原因F405主频168MHz处理1字节中断绰绰有余但瓶颈在PC端。USB转串口芯片内部缓冲只有几十字节PC端串口助手接收线程响应不及时数据就溢出了。另一方面HAL库中断接收模式每收1字节进一次中断高波特率下CPU频繁出入中断USART1的RXNE还没被读走下一字节已经到了overrun错误标志一置位后面数据全丢。解决业务逻辑正常收发优先跑115200除非传输量真的支撑不住。非上不可的场合把过采样降到8倍提高分频精度接收方式改用DMA加IDLE空闲中断让DMA连续搬运只有一行帧结束时才进中断处理。这套组合才能在高波特率下保住数据完整性具体代码就是二次开发章节给出的方向。6. 二次开发思路把回显例程改造成自己的串口调试框架6.1 第一步改造用环形缓冲区替换固定数组回显例程的rx_buffer是线性数组主循环读走再清零收发一快就覆盖。换成环形缓冲区是性价比最高的一步typedef struct { uint8_t buf[256]; volatile uint16_t head; // 写指针中断里更新 volatile uint16_t tail; // 读指针主循环里更新 } ringbuf_t; int ringbuf_write(ringbuf_t *rb, uint8_t byte) { uint16_t next (rb-head 1) % sizeof(rb-buf); if (next rb-tail) return -1; // 缓冲区满 rb-buf[rb-head] byte; rb-head next; return 0; }逻辑说明head指向下一个空位tail指向待读数据。判断满的条件是next等于tail而不是head等于tail否则空和满会混淆。回调里把rx_byte写入环形缓冲区主循环查非空再处理接收与业务彻底解耦。参数说明缓冲区256字节够大多数命令帧用如果业务帧超过256就扩到512或1024192KB RAM完全扛得住但裸机程序别贪大按最长帧加余量定最合理。6.2 第二步改造把裸数据变成可交互命令回显只是验证链路实际调试更想要的是命令交互。约定帧格式为$CMD param1 param2\n主循环读到\n时触发解析按命令名匹配执行。这样用串口助手就能输入指令翻转GPIO、读传感器、改参数逐步积累成一个微型命令调试框架。解析放主循环不要放中断里中断只负责收字节组帧。6.3 第三步改造对接RTOS消息队列如果板子跑了FreeRTOS串口可以做成独立任务中断里把数据投递到消息队列或直接给信号量任务里阻塞等待并处理。发送侧也从阻塞式HAL_UART_Transmit换成HAL_UART_Transmit_DMA配合完成回调释放缓冲区。这套结构在量产设备里是通用做法串口驱动与业务彻底隔离不会再互相拖累。从那次因为AF7和AF8搞混、在串口无声面前浪费了半天的经历之后我每次拿到新板子第一件事就是用回显例程验证串口链路通了再往上叠业务。这个习惯在后面好几个项目里帮我省下大把查线时间。希望帮到你。本文还有配套的精品资源点击获取