资讯详情

STM32F103标准库UART串口通信实战:配置、中断与调试

📅 2026/9/16 1:23:44 | 华诺云谱 👁 阅读
STM32F103标准库UART串口通信实战:配置、中断与调试
简介面向STM32初学者的串口通信实验工程基于STM32F103C8T6芯片与标准外设库通过串口输入1、2、3演示不同输出反馈覆盖UART初始化、中断接收、数据处理与发送等关键编程环节。压缩包为ZIP格式共175个文件不仅包含C/H源文件和标准库驱动如stm32f10x_usart.c、stm32f10x_rcc.c还保留了定时器、ADC、I2C、CAN等外设库文件同时提供AXF/HEX可执行文件、链接映射表、调试日志及uvprojx工程配置大小仅5.78MB便于快速下载与整体阅读。工程目录结构清晰源文件、库函数与输出产物分层存放方便按模块检索。已有2645人学习下载资源内附完整程序源码、备份文件与依赖配置可辅助理解STM32串口外设配置、时钟使能和MDK工程结构。通过动手修改代码并实际运行初学者能逐步掌握标准库开发流程为后续扩展更多通信接口打下基础。1. UART 串口实验为什么是标准库入门的第一站先泼一盆冷水很多人第一次在 STM32F103 上跑串口实验不是死在代码逻辑上而是死在时钟树上。标准库把 USART_Init() 封装好了但如果你没在 RCC_APB2PeriphClockCmd() 里把 GPIO 和 USART 的时钟一块打开或者忘了开 AFIO程序就会在轮询循环里干等 TXE 标志连一个字节都吐不出来。什么是 UART 串口通信实验说白了就是把芯片里一个叫 USART 的外设配合 GPIO 复用引脚按既定的波特率和帧格式把数据发出去、收进来。这个实验是 STM32F103 标准库开发的敲门砖它同时用到 GPIO、RCC、中断、外设库文件四个基础模块。以标准库 v3.5 为底色下面就把 UART 配置、收发实现和调试验证一次讲清楚适合刚建好最小系统、想用串口当调试后门的开发者。2. UART 帧格式与 STM32F103 串口资源映射2.1 UART 帧格式一条完整的字节就是一段波形UART 是异步串行协议通信双方不需要时钟线前提是约定帧格式。不发送数据时 TX 线保持高电平要发一个字节时先拉低一个位宽作为起始位然后从最低位开始依次发送 8 个数据位最后拉高至少一个位宽作为停止位。接收端用自己的时钟采样以起始位下降沿为同步点之后按波特率规定的每个 bit 时长逐位采样。STM32F103 的 USART 硬件会自动完成这些移位动作标准库里你只需要告诉它波特率和帧参数。波特率是每秒发送的 bit 数常用 9600、115200。115200 时每个 bit 约 8.68us如果收发双方波特率误差超过约 2%采样点逐渐偏离数据位中心最终收错字节。标准库初始化时只需把数值填进 USART_BaudRate库函数会计算 USARTDIV 分频系数并写入 USART_BRR 寄存器。字长、停止位、校验位这三项也必须和对端一致8 位数据位对应 USART_WordLength_8b停止位默认 1 位校验多数选无。上位机配成“115200,8,E,1”而单片机配成 8N1出来的数据就会错位。2.2 STM32F103 的串口资源与标准库文件对应关系STM32F103 中容量和高容量芯片一般引出 3 个 USART 和 2 个 UART。UART4、UART5 没有同步时钟输出多数串口实验从 USART1、USART2、USART3 里选。标准库在 stm32f10x_usart.c 里用 USART_TypeDef 统一管理这些外设初始化代码几乎一致差异集中在时钟总线、引脚和中断向量。做串口 1 和串口 3 使用差异排查时下面这张表比源码有用参数USART1USART2USART3所在总线APB272MHzAPB136MHzAPB136MHz默认 TX/RXPA9/PA10PA2/PA3PB10/PB11是否可重映射可映射到 PB6/PB7不支持可映射到 PD8/PD9IRQ 向量USART1_IRQnUSART2_IRQnUSART3_IRQn时钟使能函数RCC_APB2PeriphClockCmdRCC_APB1PeriphClockCmdRCC_APB1PeriphClockCmd注意表格里 APB1 固定 36MHz 的前提是系统时钟 72MHz、APB1 预分频 2。标准库的 SystemInit 默认已经把主频拉到 72MHz但如果你改了 PLL 配置波特率计算就要重新按实际 PCLK 算。2.3 标准库在串口实验里的封装颗粒度标准库 v3.5 的外设操作分两层结构体配置层和寄存器访问层。配置层包含 GPIO_Init、USART_Init你填一个结构体函数把它拆成寄存器位写入寄存器访问层提供 USART_SendData、USART_ReceiveData、USART_GetFlagStatus。相比寄存器开发它省去查位域的时间相比 HAL 库它没有 HAL_UART_Receive 那种带超时的重封装每条 API 几乎都能对应到数据手册的寄存器说明。调试串口问题时去翻 stm32f10x_usart.c 源码比翻 HAL 的 usart.c 更省力。标准库新建工程的核心文件包括startup_stm32f10x_hd.s、stm32f10x.h、system_stm32f10x.c、core_cm3.h再加上 stm32f10x_gpio.c、stm32f10x_usart.c、stm32f10x_rcc.c。stm32f10x_conf.h 里没有打开对应外设头文件时编译器会报 USART_TypeDef 找不到这是新建工程最常见的编译错误。标准库 v3.5 的下载包解压后目录结构清晰把这几个文件按路径加入工程就行。2.4 波特率寄存器怎么算USART_BRR 的映射关系是 16 倍过采样USARTDIV PCLK / (16 * BaudRate)。USART1 在 APB2 上PCLK 为 72MHzUSART3 在 APB1 上PCLK 为 36MHz。以 USART1、115200 为例USARTDIV 72000000 / (16 * 115200) ≈ 39.06标准库把整数部分写进 DIV_Mantissa小数部分写进 DIV_Fraction最终真实波特率和目标值的误差约 0.02%在可接受范围内。真正导致乱码的不是这个小数取整而是系统主频配置错误。常见情况是外部晶振没起振或 PLL 配置不对SystemCoreClock 停在 8MHzUSART1 按 72MHz 去算波特率实际跑出来的频率完全对不上。调试时可以在代码里调用 RCC_GetClocksFreq 把 SYSCLK、PCLK1、PCLK2 打印出来先确认时钟再调串口。检查代码如下RCC_ClocksTypeDef clocks; RCC_GetClocksFreq(clocks); printf(SYSCLK%u PCLK1%u PCLK2%u\r\n, (unsigned int)clocks.SYSCLK_Frequency, (unsigned int)clocks.PCLK1_Frequency, (unsigned int)clocks.PCLK2_Frequency);说明RCC_GetClocksFreq 会读取当前时钟配置并换算成 Hz。如果在串口助手里看到 SYSCLK 不是 72000000说明时钟树配置有问题波特率再怎么调都白费。PCLK1 对应 APB1 预分频后的时钟PCLK2 对应 APB2 预分频后的时钟这两个数值直接决定 USART1 和 USART3 的波特率基准。3. 标准库串口初始化的最小代码GPIO 与 USART 结构体参数3.1 GPIO 复用配置TX 推挽、RX 浮空串口信号由 USART 外设控制但引脚本身属于 GPIO所以要把 PA9、PA10 配置为复用功能。TX 由单片机驱动外部电平使用复用推挽输出 GPIO_Mode_AF_PPRX 由外部设备驱动、单片机负责读取使用浮空输入或上拉输入。采用上拉输入时要注意外部电平避免在空闲时读到不确定信号。GPIO_Speed 对输入模式无效但保留该字段不会出错。void Uart1_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; /* 开 USART1、GPIOA、AFIO 时钟三个时钟缺一个都不行 */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1 | RCC_APB2Periph_AFIO, ENABLE); /* TX - PA9复用推挽输出 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOA, GPIO_InitStructure); /* RX - PA10浮空输入 */ GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure); }说明GPIO_Mode_AF_PP 是复用推挽模式USART 的 TX 信号要通过这个模式才能驱动到引脚上。RCC_APB2Periph_AFIO 负责复用功能时钟标准库中即使不使用重映射开上它也能避免部分库版本在切换复用功能时的时序问题。GPIO_Speed 取 50MHz 是 F103 的常见选择低速串口用 2MHz 也可以但建议统一填 50MHz后续改功能时不用回头补参数。3.2 USART_InitTypeDef 结构体的六个参数USART_InitTypeDef 是标准库串口初始化最核心的结构体六个成员分别映射到 USART_BRR、USART_CR1、USART_CR2、USART_CR3 寄存器。优先级最高的还是波特率参数填错后面全白搭。六个参数的取值直接参考对端设备的配置收发双方必须完全一致。结构体成员常用取值说明USART_BaudRate9600 / 115200必须与对端一致串口工具里也要选同样数值USART_WordLengthUSART_WordLength_8b数据位长度9 位时配合校验位使用USART_StopBitsUSART_StopBits_1停止位长度常用 1 位抗干扰场景用 2 位USART_ParityUSART_Parity_No无校验/偶校验/奇校验要和对端一致USART_HardwareFlowControlUSART_HardwareFlowControl_None多数板卡不用 RTS/CTS 硬件流控USART_ModeUSART_Mode_Rx | USART_Mode_Tx收发模式按需使能void Uart1_Init(void) { 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); }说明USART_Init 负责把参数换算后写进寄存器其中波特率写入 USART_BRRM 位写入 USART_CR1停止位写入 USART_CR2硬件流控写入 USART_CR3。USART_Cmd(USART1, ENABLE) 是把 USART_CR1 的 UE 位置 1不使能外设前面所有配置都不生效。常见失误是配置完忘记使能寄存器看起来都对但引脚上没有信号输出。3.3 发送字节与字符串的轮询实现标准库发送一个字节的常用写法是等 TXE 标志。USART_SendData 只是把数据放进发送数据寄存器硬件移位输出完成后 TXE 会被置 1表示发送数据寄存器已空可以写入下一个字节。要注意 TXE 置位不代表这一字节已经从引脚发完需要立即切换电平或拉控制线时应该等 TC 标志。void Uart1_SendByte(uint8_t ch) { USART_SendData(USART1, ch); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); } void Uart1_SendString(char *str) { while (*str ! \0) { Uart1_SendByte((uint8_t)(*str)); str; } }说明Uart1_SendString 用字符串的 \0 结尾判断结束。如果你的数据源是没有结尾的二进制帧比如 Modbus 报文就不能用这个函数需要改成带长度参数的发送函数。逐字节发送大量数据时每发送一个字节判断一次 TXE低速场景没问题吞吐量要求高时改用 DMA 发送。3.4 轮询接收与 RXNE 标志位接收同样有标志位。RXNE 置 1 表示接收移位寄存器已经把数据完整移入 USART_DR等待软件读取。读取 USART_DR 后 RXNE 自动清零。uint8_t Uart1_ReceiveByte(void) { while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) RESET); return (uint8_t)USART_ReceiveData(USART1); } uint8_t ch; if (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) ! RESET) { ch (uint8_t)USART_ReceiveData(USART1); }说明第一段是阻塞等待适合主循环里专门轮询的场合第二段是查询式读取适合放在主循环里配合定时器扫描。USART_ReceiveData 读取 USART_DR 之后 RXNE 自动清零但要注意如果先读 SR、没读 DR而下一个字节已经到达会造成对同一字节的重复判断。标准库里 GetFlagStatus 只读 SRGetITStatus 读 SR 后还会判断中断使能位中断服务函数中优先用后者。4. 中断接收、printf 重定向与串口 1/3 差异4.1 中断接收从轮询到事件驱动轮询把 CPU 锁死在等待循环里主循环除了等串口什么都干不了。产品的常见做法是把接收放到中断里主循环只处理缓冲区已经完整收到的帧。F103 上 USART1、USART2、USART3 都有独立的中断向量使用 NVIC 配置优先级。void Uart1_NVIC_Config(void) { NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); }中断服务函数写在 stm32f10x_it.c通过启动文件里的向量表跳到 USART1_IRQHandlervoid USART1_IRQHandler(void) { uint8_t data 0; if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { data (uint8_t)USART_ReceiveData(USART1); /* 回显验证通路 */ while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, data); } }说明USART_IT_RXNE 是接收缓冲区非空中断每收到一个字节进一次中断。中断里先读 IT 状态再读 DR这一步同时清除了 RXNE 标志。USART1_IRQHandler 内部要区分 RXNE、IDLE、ORE 等多个中断源时不要在判断完 RXNE 后直接 return而是让所有 if 都独立执行避免漏掉同批次到达的其他事件。4.2 printf 重定向与半主机陷阱串口实验最常见的调试手段是 printf。标准库的 printf 依赖 fputc而 fputc 默认走半主机模式会在硬件上产生一个断点等待调试器响应不接调试器程序就卡死。常见做法是勾选 MicroLIB再重定向 fputc#include stdio.h int fputc(int ch, FILE *f) { (void)f; while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, (uint8_t)ch); return ch; }说明fputc 返回 int但实际发送的是低 8 位。重定向之后 printf 发出的每个字符都走 USART1。如果你不用 MicroLIB需要在 Keil 里加入禁用半主机的代码块二选一即可。printf 只适合格式化文本输出不适合二进制帧。Modbus 那种由设备地址和 CRC 组成的 16 进制报文还是要写专门的发送函数否则遇到 0x0A、0x00 这类字符会被 printf 的字符串处理逻辑干扰。4.3 串口 1 和串口 3 的使用差异串口 1 和串口 3 的使用差异是 F103 开发中的高频问题也是代码移植时最需要小心的地方。二者在标准库里的差异集中在三处时钟总线、引脚、中断向量。USART1 挂 APB2USART3 挂 APB1所以时钟使能函数一个是 RCC_APB2PeriphClockCmd一个是 RCC_APB1PeriphClockCmd。引脚一组在 PA9/PA10一组在 PB10/PB11。void Uart3_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; /* USART3 时钟在 APB1GPIOB 时钟在 APB2 */ RCC_APB1PeriphClockCmd(RCC_APB1Periph_USART3, ENABLE); RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB | RCC_APB2Periph_AFIO, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; /* TX */ GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOB, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_11; /* RX */ GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, GPIO_InitStructure); }说明USART3 配置完成后遇到要使用中断时IRQChannel 要改成 USART3_IRQn中断服务函数名改成 USART3_IRQHandler。USART1 和 USART3 的波特率计算基准确实不同USART1 按 72MHz 算USART3 按 36MHz 算。但标准库的 USART_Init 内部会自动根据外设所属总线读取正确的 PCLK只要时钟使能正确这部分不用手动干预。如果使用重映射USART1 可以映射到 PB6/PB7USART3 可以映射到 PD8/PD9。重映射后需要调用 GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE)。PA9/PA10 除了串口功能外部分开发板上还挂了其他器件会影响串口电平。串口 1 做调试输出、串口 3 接 RS485 转 Modbus 是 F103 项目里很常见的分工Modbus 调试时配合 ModbusPoll 一类的上位机工具可以快速看到传感器返回的寄存器值。4.4 初始化调用顺序先时钟后外设再中断串口实验的初始化顺序固定为配置 NVIC 优先级分组、配置 GPIO、配置 USART、使能外设、使能中断。NVIC_PriorityGroupConfig 建议在 main 函数最开始调用一次不要每个外设初始化时重复设置否则会改变所有中断的优先级分组正在运行的中断行为会跟着变。USART_Init 内部会读取当前时钟频率并计算波特率如果把 UART 初始化放到 SystemInit 之前读到的时钟还是原始值输出就会乱码。标准库会在进入 main 前自动调用 SystemInit但如果你修改过启动文件或者手动关过时钟就要先确认 SystemCoreClock 的值。5. 回环验证、DMA 起步与三个串口实验的坑5.1 用回环测试确认串口通路程序跑起来后先别急着接外部设备。把 PA9 和 PA10 用杜邦线短接发一个字节看能否在接收端收到这是最直接的验证方式。收发都在单片机上只要回显正常说明 GPIO、USART、中断链路都是通的。接 USB 转串口线时注意交叉本机 TX 接对端 RX本机 RX 接对端 TX共地不能少。Windows 下设备管理器里能看到枚举出的 COM 口FT232R、FT231X、CP2104 这类常见 USB 转 UART 芯片要先装好驱动识别不到端口时优先查驱动而不查单片机。如果回环测试失败可以拿逻辑分析仪直接夹在 PA9 上看波形。115200 波特率下一个 bit 宽度约 8.68us能看到起始位下降沿、8 个数据位和停止位高电平说明发送路径没问题问题出在接收或上位机配置。5.2 三个常踩的坑现象常见原因排查方向printf 后程序卡死半主机模式勾选 MicroLIB 或禁用半主机串口助手一直乱码系统主频不是 72MHz打印 RCC_GetClocksFreq 结果DAP 下载失败提示找不到目标BOOT1 悬空或串口线干扰确认 BOOT00、BOOT10拔掉 PA9/PA10 外接线这三个坑按出现频率排基本覆盖了标准库串口实验的绝大多数翻车现场。printf 卡死是最隐蔽的因为单步调试时程序在走全速运行却停在 fputc 里原因就是半主机调用需要调试器响应。串口助手乱码要先确认时钟再怀疑波特率主频不对所有串口的波特率都跟着错。DAP 下载失败时如果发现 SWD 引脚和串口引脚有复用或者 USB 转串口模块和目标板同时供电断电后重新插拔通常能解决。5.3 下一步从轮询与中断迈向 DMA收发频率上来后轮询占 CPU、中断频繁进出也有开销。F103 的 USART1 在 DMA1 上有独立通道USART1_TX 对应 DMA1_Channel4USART1_RX 对应 DMA1_Channel5。第一步别做双缓冲先做“收到一帧后由 DMA 搬运到内存主循环只查计数”的简单实现。DMA 配置的核心在 DMA_InitTypeDef 里的外设基址、内存基址、传输方向、数据宽度和缓冲区大小初始化完成后用 USART_DMACmd(USART1, USART_DMAReq_Rx, ENABLE) 使能接收请求。DMA 搬运过程中的数据一致性靠内存访问顺序保证配置时不要同时开 Bufferable 位否则可能出现数据还没写完就被串口发出去的情况。先把轮询和中断收发在回环里跑稳再动 DMA这是定位问题最快的路径。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。