
简介这套 STM32F429 标准库串口通信示例工程面向嵌入式初学者及需要快速上手 USART 开发的工程师尤其适合刚接触 STM32F4 系列的开发者适用于调试、传感器数据采集、上位机通信等典型场景帮助掌握串口初始化、数据收发与异常处理的关键方法。压缩包共 295 个文件约 17.11MB以 .c/.h 源码和 .uvprojx/.uvoptx 工程文件为主并包含 .o/.d/.crf 等编译过程文件以及 .map/.axf 链接输出便于对照源代码检查编译与链接信息可在 Keil 中直接打开学习或二次开发整体结构清晰。当前已有 1029 人下载学习说明它在同类 STM32 串口资源中具有实用参考价值。工程展示了 USART1 在 PA9/PA10 上的 GPIO 复用配置、波特率与帧结构初始化、阻塞/中断收发调用方式并提供 DMA 传输和错误回调处理思路代码中还集成了 stm32f4xx 系列多数外设驱动源文件可作为后续项目开发的基础模板。1. 串口通信F429 标准库拿到手先把 USART 干通F429 开发板到手后的第一件事往往不是点灯而是把调试信息从串口打出来。很多人的第一课就在这里翻车明明照着例程抄printf 出来的全是乱码或者干脆一点反应都没有。这份「STM32F429 标准库串口通信」例程目标就是用标准库把 USART 的初始化、中断收发、printf 重定向一次讲透让你拿到就能把串口当作调试和对外通信的“眼睛”。它适合两类人一类是刚接触 STM32 标准库、想快速跑通串口的新手另一类是要在项目中真正用 F429 的 USART 与上位机、传感器模块通信的工程师。串口通信从来不是 HAL 库的专利标准库这套实现反而能让你更清晰地看到底层链路。2. 选型与工程骨架F429 串口资源、StdPeriph 标准库与目录分工2.1 F429 的串口资源USART1/2/3/6 加 UART4/5/7/8先认清你手上到底有几个串口可用。STM32F429 这级芯片上串口总数是 8 个USART1、USART2、USART3、USART6以及 UART4、UART5、UART7、UART8。USART 和 UART 的区别很简单USART 多了同步时钟信号可以做类似 SPI 的同步收发、智能卡这类扩展功能如果只做异步串口两者用法几乎没有差别。这 8 个串口挂在两条总线上USART1 和 USART6 挂在 APB2USART2、USART3、UART4、UART5、UART7、UART8 挂在 APB1。APB2 的时钟上限更高所以追求极限速率时大家习惯优先使用 USART1。日常场景下的 115200、460800、921600 波特率APB1 的时钟也完全够用不必过度纠结总线选择。引脚复用关系也是重点。同一个 USART 可能有多个引脚映射库函数只负责按你给的参数配置不会帮你校验引脚是否被其他外设占用。最常用的映射如下串口时钟总线常见 TX / RX 引脚USART1APB2PA9 / PA10也支持 PB6 / PB7USART2APB1PA2 / PA3USART3APB1PB10 / PB11也支持 PD8 / PD9 等USART6APB2PC6 / PC7也支持 PG14 / PG9我拿到工程后最先确认的就是引脚选好 PA9/PA10再决定是给 GPIOA 开时钟还是给 GPIOC 开时钟。引脚选错后面配置再对也是白搭。2.2 为什么这套例程坚持用标准库而不是 HAL 库CubeMX 生成 HAL 工程已经成为主流但真要处理「中断里收数据、主循环里解析」这类场景时HAL 的 UART 接收回调机制会牵扯出不少隐含状态。标准库的思路是薄封装每个寄存器对应一个配置函数参数结构体一目了然中断服务函数直接就是全局中断入口没有多余的回调注册环节。更实际的一点是资料匹配。F429 的很多老代码、参考设计都是基于标准库写的把这些代码移植过来时标准库 API 能跟三年前的项目直接对上换成 HAL 之后光是改 HAL_UART_Transmit 这一层函数名就要动不少业务逻辑。HAL 的优势在于开发速度和工具链集成但对想读懂串口底层时序的开发者来说标准库更像一张“透明图纸”。不过要提醒一句标准库官方已经停止更新新产品线整体转向 HAL。所以这套工程的学习价值大于长期维护价值。我的建议是把标准库当作理解工具生产环境如果时间允许可以逐渐往 HAL 迁移但如果要维护老设备标准库这套代码绝对还能再战很多年。2.3 工程目录结构谁负责启动谁负责外设谁负责用户代码标准库工程的骨架非常典型解压后你会看到清晰的分层目录。我按实际使用频率排一下Project/ ├── User/ │ ├── main.c # 主循环与板级初始化 │ ├── stm32f4xx_it.c # 中断服务函数 │ ├── uart.c / uart.h # 串口配置与收发逻辑 ├── Libraries/ │ ├── CMSIS/ │ │ ├── Include/ # core_cm4.h 等内核头文件 │ │ └── Device/STM32F4xx/ # 设备头文件、系统时钟文件 │ └── STM32F4xx_StdPeriph_Driver/ │ ├── inc/ # 所有外设的头文件 │ └── src/ # 外设驱动源码gpio、usart 等 └── MDK-ARM/ # Keil 工程文件与启动文件标准库工程的规则是CMSIS 层除了系统时钟配置其余基本不动StdPeriph_Driver 的 src 目录是整个外设驱动实现正常情况下不需要修改用户代码集中在 User 目录。main.c 负责三件事时钟配置、外设初始化、主循环。stm32f4xx_it.c 里放的是 USART1_IRQHandler 这样的中断服务函数注意每个中断函数的名字是固定的不能随意改名这个细节我在早期的串口调试里踩过坑。这套分层的好处是要新加 Timer、I2C 外设时只需要在 StdPeriph_Driver 源码已经包含的前提下在 User 目录新增一个模块文件复制初始化模板就行。跟着这套工程的结构走后续加外设不会把代码搅成一团。3. 核心代码拆解一个字节从 TX 引脚发出去的完整链路这一章是整个工程的“解题过程”按数据流向拆开先管好引脚再配好串口然后发送路径走轮询和 printf接收路径走中断加环形缓冲。下面的代码统一以 USART1 PA9/PA10 为例。3.1 初始化 GPIO复用功能映射是关键STM32F4 系列每个引脚在没配置复用功能之前只能当普通 GPIO 用。想让 PA9 变成 USART1_TX、PA10 变成 USART1_RX必须做两步打开 GPIOA 时钟再用 GPIO_PinAFConfig 把引脚映射到 USART1 的复用功能编号。void UART_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOA, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE); 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_OType GPIO_OType_PP; GPIO_InitStructure.GPIO_PuPd GPIO_PuPd_UP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); }这几行里最关键的是 GPIO_PinAFConfig(GPIOA, GPIO_PinSource9, GPIO_AF_USART1)。GPIO_PinSource9 表示第 9 号引脚GPIO_AF_USART1 是复用功能编号告诉芯片“这个引脚要交给 USART1 控制”。同一个引脚可以对应多种复用功能比如 PA9 也能做 USB OTG、CAN 等相关信号所以配置前最好查一下数据手册的复用功能映射表。GPIO_Mode_AF 表示引脚工作在复用功能模式GPIO_OType_PP 指推挽输出。串口 TX 是电平输出推挽最合适RX 是输入配上拉没有问题。GPIO_Speed_50MHz 在串口场景下完全够用不用刻意调到 100MHz速度越高引脚翻转带来的噪声越明显。这里的配置同时作用于 PA9、PA10AF 模式下引脚方向由片上外设决定代码里不需要再单独控制方向。3.2 USART 外设初始化波特率、字长、停止位与中断开关串口外设的配置由 USART_InitTypeDef 结构体完成。库内部的 USART_Init 会根据传入的波特率计算分频因子 USARTDIV再写入 BRR 寄存器。注意计算依赖的是串口时钟源的频率USART1 挂在 APB2 上配置前必须保证 APB2 时钟与系统时钟分频关系正确否则波特率会系统性偏移。void UART_Config(uint32_t baud) { USART_InitTypeDef usart; NVIC_InitTypeDef nvic; usart.USART_BaudRate baud; usart.USART_WordLength USART_WordLength_8b; usart.USART_StopBits USART_StopBits_1; usart.USART_Parity USART_Parity_No; usart.USART_HardwareFlowControl USART_HardwareFlowControl_None; usart.USART_Mode USART_Mode_Rx | USART_Mode_Tx; USART_Init(USART1, usart); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); USART_Cmd(USART1, ENABLE); nvic.NVIC_IRQChannel USART1_IRQn; nvic.NVIC_IRQChannelPreemptionPriority 1; nvic.NVIC_IRQChannelSubPriority 0; nvic.NVIC_IRQChannelCmd ENABLE; NVIC_Init(nvic); }USART_WordLength_8b 配合 USART_Parity_No表示一帧数据是 8 个数据位、无校验位也就是常说的 8N1 格式。USART_Mode_Rx | USART_Mode_Tx 打开双向收发只写 Rx 会导致只收不发只写 Tx 会导致只发不收。USART_HardwareFlowControl_None 关闭硬件流控RTS/CTS 引脚可以挪作他用。NVIC 优先级分组要注意需要在 main 最开始调用 NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2)再初始化串口否则中断配置不会生效。我习惯把 USART1 的抢占优先级设为 1把更高优先级留给 Systick 等时基外设。USART1_IRQHandler 这个名字不能改改了编译不会报错但中断向量表里找不到入口中断永远不会触发。3.3 发送路径轮询发送加 printf 重定向要让 printf 正常输出底层只需要实现一个 fputc。标准库的 printf 最终会调用这个函数把单个字符送到串口。轮询发送的写法很简单等待 TXE 标志置位再把数据写进数据寄存器 DR。int fputc(int ch, FILE *f) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }轮询等待的是 TXE发送数据寄存器空不是 TC发送完成。TXE 置位表示数据寄存器还能再写入一个字节写入后立刻返回效率最高如果等 TC每发一个字节都要等移位寄存器把当前帧彻底送完速度损耗接近一倍。在 MDK 环境下fputc 有两个容易踩的坑。第一printf 调用后一直卡在 BKPT 指令附近多半是半主机模式没关需要在 Target 选项里勾选 Use MicroLIB或者自己实现 __use_no_semihosting 的处理桩。第二fputc 里不要做阻塞延时和复杂计算否则发送一个完整数据帧时可能被其他中断打断实测中偶发少字节。我平时还会单独封装一个 UART_PutChar 给 main 里直接用与 printf 互不影响。3.4 接收路径RXNE 中断与环形缓冲区接收中断和处理逻辑分离是串口工程的通用做法。中断服务函数里只负责把收到的字节塞进缓冲区主循环再从缓冲区取走解析。如果不加缓冲区主循环处理一帧数据期间后续字节就会覆盖前一个字节这就是串口调试里最常见的“丢字节”。#define RX_BUF_SIZE 256 static volatile uint8_t rx_buf[RX_BUF_SIZE]; static volatile uint16_t rx_head 0; static volatile uint16_t rx_tail 0; void USART1_IRQHandler(void) { uint8_t data; if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { data USART_ReceiveData(USART1); uint16_t next (rx_head 1) % RX_BUF_SIZE; if (next ! rx_tail) { rx_buf[rx_head] data; rx_head next; } } }RXNE 位在 USART1 收到一个完整字节时由硬件置 1进入中断后调用 USART_ReceiveData 读取 DR 寄存器RXNE 自动清 0。缓冲区写指针 rx_head 往前推进当 next 等于 rx_tail 时表示缓冲区满此时只能丢弃新数据保证旧数据不被覆盖。环形缓冲容量取 2 的幂时取模可以用位运算优化但 256 字节的数组用 % 性能上也没有明显压力。主循环取数据时只操作头尾指针uint8_t UART_GetChar(uint8_t *byte) { if (rx_head rx_tail) { return 0; } *byte rx_buf[rx_tail]; rx_tail (rx_tail 1) % RX_BUF_SIZE; return 1; }主循环里把读到的字节做回显或者送入解析逻辑。这里要特别留意 ORE过载错误标志如果中断触发太快主循环来不及取走数据旧数据被新数据覆盖后 ORE 会置位并阻塞后续接收。标准库的接收中断里要额外判断 ORE读 SR 再读 DR 把它清掉否则你会遇到“间隔几秒后串口再也不进中断”的诡异现象。4. 把工程跑起来MDK 配置、接线与串口助手验证4.1 MDK 工程搭建与关键编译选项实际跑这个工程时你通常不用从零新建直接打开 MDK-ARM 目录下的工程文件即可。我拿到这种模板后第一件事是检查几个关键设置而不是立刻编译。首先是 Target 选项里的 Device确认芯片型号是 STM32F429不同型号对应的启动文件不同。然后是 C/C 选项卡Define 里必须出现这两个宏STM32F429xx, USE_STDPERIPH_DRIVERSTM32F429xx 是让库源码知道“当前芯片是 F429”很多外设驱动靠这个宏裁剪代码。USE_STDPERIPH_DRIVER 会引入 stm32f4xx_conf.h 配置文件这个文件里开关了要用到的外设模块USART 和 GPIO 必须保持启用。删掉其中一个宏最典型的症状就是编译报一堆类型未定义或者 SystemInit 函数根本没有被调用。Include Paths 需要手动检查一遍标准库工程最常见的报错是找不到 stm32f4xx.hLibraries/CMSIS/Include Libraries/CMSIS/Device/STM32F4xx/Include Libraries/STM32F4xx_StdPeriph_Driver/inc User路径顺序不影响编译结果但建议把 User 放在最后避免你自己的头文件覆盖库头文件。配置完成后全编译第一次编译时间一般几十秒到两三分钟不等。如果报错提示缺少 core_cm4.h就是 CMSIS/Include 路径没有加进工程。4.2 接线PA9/PA10 与 USB 转 TTL 小板的电平匹配USART1 的收发引脚是 PA9 和 PA10PA9 是发送端 TXPA10 是接收端 RX。和 USB 转 TTL 小板连接时必须交叉开发板 PA9TX接小板的 RX开发板 PA10RX接小板的 TX。很多初学者在这里绕晕交叉连接后如果仍然没有数据优先怀疑是不是接成了同号引脚。这里有一个电平的关键细节F429 的 IO 电平是 3.3V所以 USB 转 TTL 小板也必须是 3.3V 逻辑不能用 RS232 电平转换板。接线可以按下面这张表对照检查开发板引脚功能连接目标PA9USART1_TXUSB 转 TTL 小板 RXPA10USART1_RXUSB 转 TTL 小板 TXGND地USB 转 TTL 小板 GND共地是这条链路里最基础也最关键的一步。开发板和小板之间如果只接 TX、RX 而不接地信号波形没有公共参考点现象就是偶尔收到一两个乱码字节其余时间完全没反应。共地之后不要接 VCC开发板用自身供电即可避免两套电源互相灌电流。4.3 编译烧录与串口助手验证编译通过后用 ST-Link 或 J-Link 通过 SWD 接口连接开发板。烧录前确认 Debug 选项卡里的下载器型号和接口速度ST-Link 默认速度足够。配置无误后点击 Download下载完成按复位键程序从 main 开始执行。打开串口助手时端口号选择 USB 转 TTL 小板被电脑识别后的 COM 口波特率设为 115200数据位 8停止位 1无校验无流控。在发送区输入一个测试字符串比如 hello点击发送开发板收到后通过回显逻辑把同样内容送回串口助手这是最简单也最可靠的连通性验证手段。如果发送后串口助手没有任何输出先量一下 PA9 引脚的电平确认代码确实在跑、引脚确实在翻转。没有示波器的话最简单的办法是在 main 初始化后加一个 LED 翻转循环确认程序没死在某个 while 等待里。这个排查顺序能帮你快速区分问题出在软件初始化、中断配置还是硬件接线上。5. 避坑串口通信阶段我反复折腾过的 5 个问题5.1 收发无反应先怀疑 RX/TX 接反现象发送端调用 printf 或 USART_SendData 后串口助手没有任何字符用万用表量 TX 引脚却有电平翻转。原因RX 和 TX 交叉连接时接反或者使用了非 3.3V 电平的转换板。USB 转 TTL 小板上标注的 TX、RX 是相对于小板自身的开发板 PA9 是 TX必须接小板的 RX很多折腾半天的问题最后都出在这根杜邦线上。解决先按第 4 章的接线表逐脚检查再确认开发板和小板共地。有一个快速自检方法把 PA9 和 PA10 直接用杜邦线短接配置成自收自发模式如果串口助手能收到自己发出的数据说明代码链路正常问题一定在外部接线上。5.2 乱码不是波特率问题晶振参数也得查现象串口助手能收到数据但内容全是乱码波特率从 9600 一路切到 921600 依然无效。原因波特率本身配置没错但 USARTDIV 分频因子计算依赖 HSE_VALUE。F429 核心板常见晶振是 25MHz 或 8MHzstm32f4xx.h 中 HSE_VALUE 如果与实际晶振不一致整个时钟树都会偏移串口波特率自然不准。解决打开 stm32f4xx.h找到 HSE_VALUE 宏核对开发板原理图上的晶振频率改成实际值后重新编译。用内部 HSI 时钟可以临时绕过这个问题但 HSI 精度远低于外部晶振长时间运行仍会漂移不能当作长期方案。5.3 printf 卡死fputc 和半主机模式现象程序初始化后执行到 printf 就卡住调试器全速运行时会停在 BKPT 指令附近复位后依然如此。原因MDK 默认启用半主机模式printf 需要走底层 fputc而标准库在没有连接调试器时无法完成半主机调用程序陷入死循环等待。解决在 MDK Target 选项里勾选 Use MicroLIBMicroLIB 的重定向实现更轻量能直接让 printf 走你封装的 fputc。如果不想用 MicroLIB就要实现 __use_no_semihosting并补上 _ttywrch 等半主机辅助函数的空实现两条路选一条就行。5.4 中断里放 printf一调就乱套现象在 USART1_IRQHandler 里加 printf 打印接收字节结果输出重叠、数据错乱甚至程序无故卡死。原因printf 本身耗时很长fputc 里又有 TXE 轮询等待如果发送期间被其他中断抢占数据帧会被切碎输出自然错乱。更严重的是某些标准库实现里 printf 内部会用到动态内存分配在中断上下文里做分配容易死锁。解决不要在中断服务函数里调用 printf。中断里只做“把数据搬进环形缓冲”这一件事打印放到主循环。这也是我第 3 章坚持把中断写短的原因——中断服务函数越短系统越稳定。5.5 高速率下 ORE 溢出串口接收彻底停止现象把波特率提高到 921600 后程序刚跑几分钟就再也不进接收中断主循环读缓冲区也一直为空。原因当 RXNE 中断触发后主循环来不及读取数据DR 寄存器中的旧数据被新数据覆盖ORE 标志置位USART 停掉后续接收。标准库的 USART_ReceiveData 在连续高速接收的场景下没有彻底清掉 ORE 状态导致中断被锁死。解决在 USART1_IRQHandler 里主动判断 ORE 标志读 SR 再读 DR 清掉溢出位或者像第 3 章那样在中断里循环读取 DR 直到 RXNE 清零。更彻底的做法是把接收放到 DMA让数据不经过 CPU 中断搬运这部分放到下一章展开。6. 二次开发把“能回显的串口”升级成“能解析指令的串口”回显验证只是起点。实际项目中串口通常要接收上位机下发的指令帧或者向上位机上报采集到的数据。这里我给出一个最实用的改造思路定义一个简短的通信协议帧在主循环里用状态机逐个字节解析。帧格式可以自己定推荐一个通用结构帧头长度命令字数据校验0xAA 0x551 字节1 字节N 字节1 字节帧头用于同步长度字段告诉解析器这一帧需要读多少数据校验字段用累加和就能满足大多数场景。收到完整一帧后先校验再分发命令。状态机解析的代码骨架switch (rx_state) { case WAIT_HEAD1: if (data 0xAA) rx_state WAIT_HEAD2; break; case WAIT_HEAD2: if (data 0x55) rx_state WAIT_LEN; break; case WAIT_LEN: frame_len data; rx_index 0; rx_state WAIT_DATA; break; // 之后逐字节填充 data 区校验通过后做命令分发 }这套改动的核心收益是让通信链路与业务逻辑解耦。环形缓冲负责接收原始字节状态机负责提取完整帧命令分发函数负责执行具体逻辑。我再加一个调试习惯每条指令处理完后强制回发一帧“已收到并执行”的应答包这样上位机和下位机能立刻判断通信是否正常。从标准库迁移到 HAL 时这套状态机逻辑可以原样保留只需要替换最底层的接收通道。如果未来要在 F429 上接收高频串口数据再考虑开 DMA 模式传输。但我建议先把中断版本跑熟再叠加 DMA否则中断和 DMA 同时出问题时排查难度会翻倍。代码都在下载包里建议拿到后先按第 4 章跑一遍回显再对照这章改协议。先跑通基础收发再加业务逻辑是我这几年一直保留下来的习惯。从那以后我每次做串口例程都会强制走一遍“配置检查、接线检查、回显验证、协议验证”的流程希望帮到你。本文还有配套的精品资源点击获取