简介:这套 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 挂在 APB2,USART2、USART3、UART4、UART5、UART7、UART8 挂在 APB1。APB2 的时钟上限更高,所以追求极限速率时大家习惯优先使用 USART1。日常场景下的 115200、460800、921600 波特率,APB1 的时钟也完全够用,不必过度纠结总线选择。引脚复用关系也是重点。同一个 USART 可能有多个引脚映射,库函数只负责按你给的参数配置,不会帮你校验引脚是否被其他外设占用。最常用的映射如下:串口时钟总线常见 TX / RX 引脚USART1APB2PA9 / PA10(也支持 PB6 / PB7)USART2APB1PA2 / 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、PA10,AF 模式下引脚方向由片上外设决定,代码里不需要再单独控制方向。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.h:Libraries/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 和 PA10,PA9 是发送端 TX,PA10 是接收端 RX。和 USB 转 TTL 小板连接时必须交叉:开发板 PA9(TX)接小板的 RX,开发板 PA10(RX)接小板的 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 或 8MHz,stm32f4xx.h 中 HSE_VALUE 如果与实际晶振不一致,整个时钟树都会偏移,串口波特率自然不准。解决:打开 stm32f4xx.h,找到 HSE_VALUE 宏,核对开发板原理图上的晶振频率,改成实际值后重新编译。用内部 HSI 时钟可以临时绕过这个问题,但 HSI 精度远低于外部晶振,长时间运行仍会漂移,不能当作长期方案。5.3 printf 卡死:fputc 和半主机模式现象:程序初始化后执行到 printf 就卡住,调试器全速运行时会停在 BKPT 指令附近,复位后依然如此。原因:MDK 默认启用半主机模式,printf 需要走底层 fputc,而标准库在没有连接调试器时无法完成半主机调用,程序陷入死循环等待。解决:在 MDK Target 选项里勾选 Use MicroLIB,MicroLIB 的重定向实现更轻量,能直接让 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 章跑一遍回显,再对照这章改协议。先跑通基础收发,再加业务逻辑,是我这几年一直保留下来的习惯。从那以后,我每次做串口例程都会强制走一遍“配置检查、接线检查、回显验证、协议验证”的流程,希望帮到你。本文还有配套的精品资源,点击获取
简介:这套 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 挂在 APB2,USART2、USART3、UART4、UART5、UART7、UART8 挂在 APB1。APB2 的时钟上限更高,所以追求极限速率时大家习惯优先使用 USART1。日常场景下的 115200、460800、921600 波特率,APB1 的时钟也完全够用,不必过度纠结总线选择。引脚复用关系也是重点。同一个 USART 可能有多个引脚映射,库函数只负责按你给的参数配置,不会帮你校验引脚是否被其他外设占用。最常用的映射如下:串口时钟总线常见 TX / RX 引脚USART1APB2PA9 / PA10(也支持 PB6 / PB7)USART2APB1PA2 / 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、PA10,AF 模式下引脚方向由片上外设决定,代码里不需要再单独控制方向。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.h:Libraries/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 和 PA10,PA9 是发送端 TX,PA10 是接收端 RX。和 USB 转 TTL 小板连接时必须交叉:开发板 PA9(TX)接小板的 RX,开发板 PA10(RX)接小板的 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 或 8MHz,stm32f4xx.h 中 HSE_VALUE 如果与实际晶振不一致,整个时钟树都会偏移,串口波特率自然不准。解决:打开 stm32f4xx.h,找到 HSE_VALUE 宏,核对开发板原理图上的晶振频率,改成实际值后重新编译。用内部 HSI 时钟可以临时绕过这个问题,但 HSI 精度远低于外部晶振,长时间运行仍会漂移,不能当作长期方案。5.3 printf 卡死:fputc 和半主机模式现象:程序初始化后执行到 printf 就卡住,调试器全速运行时会停在 BKPT 指令附近,复位后依然如此。原因:MDK 默认启用半主机模式,printf 需要走底层 fputc,而标准库在没有连接调试器时无法完成半主机调用,程序陷入死循环等待。解决:在 MDK Target 选项里勾选 Use MicroLIB,MicroLIB 的重定向实现更轻量,能直接让 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 章跑一遍回显,再对照这章改协议。先跑通基础收发,再加业务逻辑,是我这几年一直保留下来的习惯。从那以后,我每次做串口例程都会强制走一遍“配置检查、接线检查、回显验证、协议验证”的流程,希望帮到你。本文还有配套的精品资源,点击获取