ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

STM32F103标准库UART串口通信实验详解:从初始化到中断接收

STM32F103标准库UART串口通信实验详解:从初始化到中断接收 简介面向嵌入式开发初学者这是一份基于STM32F103C8T6芯片的UART串口通信标准库实验工程通过串口输入1、2或3触发不同输出直观演示串口初始化、参数配置以及数据收发等完整流程。工程在Keil环境下可直接编译下载适合课堂实验、毕业设计或自学练手也可作为移植到其他STM32F1系列项目的参考模板。资源包共175个文件约5.78MB按标准库工程结构组织包括29个C源文件、30个头文件以及启动文件、链接脚本、Keil工程配置和编译中间文件、hex烧录文件等便于源码阅读与直接烧录验证。目前已有2645人学习下载通过对照代码与实际运行可掌握USART的GPIO复用配置、波特率设置、中断或轮询收发方式并了解标准库工程的文件组织与构建流程为串口类应用开发打下基础。整个实验体量适中便于反复调试和扩展也可作为后续学习其他外设驱动的参考骨架。1. STM32F103 标准库 Uart 实验开篇串口是嵌入式联调的地基刚把 stm32f103 最小系统点亮一颗 LED下一个实验几乎必然是 Uart 串口通信。串口这跟线承担着调试打印、传感器读取、上位机指令交互这些最基础的通信任务STM32 标准库 V3.5 下配置串口的套路在 F1 全系列里高度统一学会一次就能迁移到 F407、F429 上。很多人以为串口实验就是把波特率配对真正上手才发现乱码、丢字节、收不到数据问题往往不在波特率而在 GPIO 复用模式配错、RCC 时钟总线使能错或者 PC 端的 USB 转串口驱动没装对。下面按“硬件准备 → 参数初始化 → 轮询收发 → 中断接收 → 排错验证”的实际开发顺序展开所有代码基于 STM32 标准库可以直接抄进自己的工程。适合刚建好标准库工程的新手也适合从寄存器或 HAL 库转过来想对照差异的工程师。2. STM32F103 最小系统与标准库工程的串口硬件准备2.1 USART1 引脚映射与交叉接线STM32F103 系列最常用的调试串口是 USART1默认引脚映射在 PA9TX和 PA10RX。绝大多数最小系统板把这组引脚单独引出接 USB 转 TTL 模块时有一个最高频的接错点模块的 TXD 必须接板子的 RXDPA10模块的 RXD 接板子的 TXDPA9也就是“发对收、收对发”。接成同名的 TX 对 TX用示波器量电平完全正常数据就是出不去。F103 的串口资源不止 USART1 一个做多串口项目前先对照一下外设挂载和引脚映射避免初始化时张冠李戴。串口外设挂载总线默认 TX/RX 引脚重映射 TX/RX时钟使能函数USART1APB272MHzPA9 / PA10PB6 / PB7RCC_APB2PeriphClockCmdUSART2APB136MHzPA2 / PA3PD5 / PD6RCC_APB1PeriphClockCmdUSART3APB136MHzPB10 / PB11PD8 / PD9RCC_APB1PeriphClockCmd在这个表里能看到 stm32f103 串口1和串口3使用差异的根源USART1 挂在 APB2 总线上主频 72MHzUSART2 和 USART3 挂在 APB1 总线上主频最大 36MHz。同样配置 115200 波特率USART1 和 USART3 在 BRR 寄存器里写入的分频值完全不同。初始化时如果把 USART1 的时钟使能误写成 RCC_APB1PeriphClockCmd向时钟位写值会失败后续所有 USART 寄存器操作都无效表现就是串口完全没反应。2.2 标准库 V3.5 的工程结构与系统时钟标准库 V3.5网上常搜到的 stm32f103 库 v3.50 下载就是这个版本新建工程的骨架由几个固定文件组成main.c 写应用逻辑stm32f10x_it.c 放中断服务函数stm32f10x_conf.h 做外设头文件包含system_stm32f10x.c 负责时钟初始化启动文件根据芯片容量选择F103ZET6 这类高密度芯片用 startup_stm32f10x_hd.s。main 函数里由 SystemInit() 把系统时钟配置到 72MHzSystemCoreClock 全局变量等于 72000000。分频计算直接依赖这个 72MHz。USART 的 BRR 寄存器计算公式是 USARTDIV PCLK / (16 * 波特率)当 PCLK72MHz、波特率115200 时USARTDIV39.0625BRR 写成 0x271。如果外部晶振不是 8MHz或者板子用了内部 HSI 时钟PCLK 就不是 72MHz同样的 BRR 值实际产生的波特率会偏移对端自然收出乱码。排查串口乱码时先确认板子晶振频率再看 system_stm32f10x.c 里的 PLL 倍频配置。2.3 GPIO 复用推挽与浮空输入的初始化代码#include stm32f10x.h void UART1_GPIO_Config(void) { GPIO_InitTypeDef GPIO_InitStructure; /* 使能 GPIOA 与 USART1 时钟。 串口1挂在 APB2 总线上主频 72MHz所以用 APB2 的时钟使能函数。 如果换成 USART3这行要改成 RCC_APB1PeriphClockCmd(...) */ RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_USART1, 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); }TX 引脚用 GPIO_Mode_AF_PP 而不是 GPIO_Mode_Out_PP原因是复用推挽模式把引脚控制权交给片上外设USART 移位寄存器输出的 TX 信号才能接到引脚上普通推挽输出由 ODR 寄存器人工控制配成这种模式后 USART 发送的数据根本到不了 PA9。RX 配浮空输入是官方例程的默认做法如果外部设备输出端是开漏结构也可以改用上拉输入但会轻微影响电平边沿质量调试期建议先保持浮空。提示如果最小系统的串口引脚不是 PA9/PA10而是 PB6/PB7除了 GPIO 时钟外还要单独使能 AFIO 时钟并调用 GPIO_PinRemapConfig(GPIO_Remap_USART1, ENABLE)否则重映射后的引脚同样不工作。3. 标准库 Uart 通信实验USART 初始化的参数与轮询收发3.1 USART_Init 的六个结构体成员拆解void UART1_Init(uint32_t baudrate) { USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate baudrate; 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); } int main(void) { SystemInit(); UART1_GPIO_Config(); UART1_Init(115200); while (1) { /* 主循环里做轮询收发 */ } }每个成员单独说清楚USART_BaudRate 115200。库内部根据 PCLK 自动计算 BRR 寄存器值但 PCLK 取值来自 SystemCoreClock 全局变量。如果工程里手动改过时钟初始化比如把外部晶振倍频数从 9 改成 18SystemCoreClock 没有同步更新算出来的 BRR 就是错的。手动改时钟后要检查 system_stm32f10x.c 里的 SystemCoreClock 更新逻辑。USART_WordLength 8b指 8 个数据位。开启奇偶校验时这里必须改成 9b原因见 3.2 的参数表。USART_StopBits 1常规配置。对端停止位设 2F103 这边仍按 1 位停止位发送也能通信但反过来不行。USART_Parity No。串口帧的校验位会占用数据位长度这一点最容易踩。USART_HardwareFlowControl None。只有板子引出 RTS/CTS 引脚时才需要打开普通三线制串口直接关闭。USART_Mode Rx | Tx收发都开。只调发送可以只写 USART_Mode_Tx但实际调试中几乎没有只发不收的场景。3.2 数据位、停止位、校验位的参数组合应用场景WordLengthParityStopBits实际帧结构常规调试8N18bNo11起始 8数据 1停止Modbus RTU8E19bEven11起始 8数据 1校验 1停止与老式设备对接8bNo21起始 8数据 2停止9 位多机通信9bNo11起始 8数据 1地址/标志位USART_Parity 配成 Even 或 Odd 时标准库不会自动调整 WordLength得手动把 WordLength 改成 9b。9 位里低 8 位是数据第 9 位是校验位硬件自动计算和插入。如果只改 Parity 不改 WordLength发送端会产生 10 位帧接收端按 9 位解析帧边界全部错位表现就是偶发乱码且无法通过调整波特率解决。上电联调第一个动作是确认 PC 端串口助手的校验位和停止位设置和单片机完全一致后再谈乱码问题。3.3 轮询发送判断 TXE 还是 TCvoid UART1_SendByte(uint8_t byte) { /* 等待发送数据寄存器空。TXE1 说明 TDR 已空可以写入下一字节 */ while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, byte); }USART 有两个发送相关标志位最容易混。TXE 表示 TDR 里的数据已经搬到移位寄存器TDR 空出来了TC 表示移位寄存器把这一帧的停止位也发送完毕。发单个字节等 TXE 就够因为 TDR 空出后立刻写入下一字节不会覆盖当前正在移位的数据。但半双工总线场景必须等 TC典型例子是 RS485 方向控制void UART1_SendByte_RS485(uint8_t byte) { while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, byte); /* RS485 方向引脚必须在整帧发送完后才能释放总线 只等 TXE 会导致最后一位还在移位寄存器里时总线已被切换 */ while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET); GPIO_SetBits(GPIOC, GPIO_Pin_4); /* 释放总线 */ }这里的 GPIO_SetBits 只是示意实际方向控制引脚的拉高拉低电平取决于收发器芯片。核心逻辑是TXE 置位时数据还没发完TC 置位才代表物理层发送完毕。需要精确控制总线切换时间的场景一律等 TC。3.4 轮询接收与 ORE 溢出丢帧uint8_t UART1_ReceiveByte(void) { /* 等待接收数据寄存器非空。RXNE1 表示 RDR 已收到一帧数据 */ while (USART_GetFlagStatus(USART1, USART_FLAG_RXNE) RESET); return (uint8_t)USART_ReceiveData(USART1); }轮询接收的硬伤是等待期间 CPU 被完全占住。上位机连续发来两帧第一帧被读走之后软件还没回到 while 循环开头第二帧已经到达 RDR此时 RXNE 尚未清除新的数据把旧数据覆盖硬件置 ORE 溢出标志。读 DR 读到的还是上一帧数据新帧直接丢失。这一点决定了轮询方式只适合一问一答式的短交互比如 Modbus 主站查询后从站应答。连续高速数据流必须换中断接收。提示排查丢帧时先看 ORE 标志是否被置位在接收到预期数据后执行 if (USART_GetFlagStatus(USART1, USART_FLAG_ORE) ! RESET) 并计数连续出现 ORE 说明数据到达速率超过消费速率调高主循环处理效率或改中断接收才是正路。4. STM32F103 Uart 通信实验进阶中断接收与不定长帧解析4.1 NVIC 初始化与 USART_ITConfig 的先后顺序NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); /* 2位抢占优先级 2位子优先级 */ 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); /* 必须在 NVIC 配置完成后使能串口中断 */ USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);NVIC_PriorityGroupConfig 必须在任何 NVIC_Init 之前调用否则分组设置无效。抢占优先级和子优先级的分配取决于整个工程的中断负载如果同时有定时器中断和 DMA 中断在跑串口中断的抢占优先级建议放高抢占优先级数值越小优先级越高给 0 或 1 都可以定时器放后面。中断里如果还做了浮点运算或长耗时操作抢占优先级再高也会拖慢主循环所以中断里只做数据搬运业务处理放主循环。中断源选择看具体需求。RXNE 负责收数据TC 负责发送完成回调ORE 负责溢出恢复。对帧丢失敏感的应用建议把 USART_IT_ORE 也打开溢出中断里做状态复位。4.2 中断服务函数的标志位清理规则uint8_t rx_buf[128]; volatile uint16_t rx_len 0; void USART1_IRQHandler(void) { uint8_t byte; if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { byte (uint8_t)USART_ReceiveData(USART1); /* 读 DR 自动清 RXNE */ if (rx_len sizeof(rx_buf)) { rx_buf[rx_len] byte; } } if (USART_GetITStatus(USART1, USART_IT_ORE) ! RESET) { /* 溢出后先读 SR 再读 DR把残留数据取走清除 ORE */ (void)USART_GetFlagStatus(USART1, USART_FLAG_ORE); (void)USART_ReceiveData(USART1); rx_len 0; } }标准库里 USART_GetITStatus 和 USART_GetFlagStatus 的差别在于IT 版本同时检查 SR 标志位和 CR 寄存器里的中断使能位FLAG 版本只查 SR。中断服务里统一用 IT 版本避免外设中断根本没开启时误入处理分支。RXNE 标志的清除方式是读 DR读完自动清。不要在中断里再调 USART_ClearITPendingBit(USART1, USART_IT_RXNE)标准库的这个清除操作对 RXNE 是无效的还会多一次写 CR 操作。ORE 清除比较特殊必须先读 SR 再读 DR两者顺序反了清不掉。中断源触发条件清除方式典型用途RXNE接收数据寄存器非空读 DR 自动清除收数据TC一帧发送完成读 SR 后写 DR或 ClearFlag发送完成钩子ORE接收溢出先读 SR 再读 DR溢出恢复IDLE总线空闲超过一帧时间先读 SR 再读 DR不定长帧收尾4.3 用状态机解析不定长数据帧中断接收解决了不丢字节的问题但字节流本身没有边界。上位机发来长度不定的命令帧时常见做法是用状态机逐字节推进帧头 0xAA 0x55接下来是数据长度然后数据区最后是累加校验和。typedef enum { FRAME_WAIT_HEAD1, FRAME_WAIT_HEAD2, FRAME_WAIT_LEN, FRAME_WAIT_DATA, FRAME_WAIT_CHECK } FrameState; FrameState state FRAME_WAIT_HEAD1; uint8_t frame_buf[64]; uint8_t frame_cnt 0; uint8_t frame_len 0; uint8_t check_sum 0; void process_byte(uint8_t b) { switch (state) { case FRAME_WAIT_HEAD1: if (b 0xAA) state FRAME_WAIT_HEAD2; break; case FRAME_WAIT_HEAD2: if (b 0x55) state FRAME_WAIT_LEN; else if (b 0xAA) state FRAME_WAIT_HEAD2; /* 连续两个 0xAA 时保持等待 */ else state FRAME_WAIT_HEAD1; break; case FRAME_WAIT_LEN: if (b 0 b 60) { frame_len b; frame_cnt 0; check_sum b; state FRAME_WAIT_DATA; } else { state FRAME_WAIT_HEAD1; /* 长度非法重新找帧头 */ } break; case FRAME_WAIT_DATA: frame_buf[frame_cnt] b; check_sum b; if (frame_cnt frame_len) state FRAME_WAIT_CHECK; break; case FRAME_WAIT_CHECK: if (check_sum b) { /* 帧校验通过在 frame_buf 里解析业务数据 */ } state FRAME_WAIT_HEAD1; break; } }把 process_byte 挂到中断服务里RXNE 分支收到字节后直接调用。状态机的核心收益是不管上位机以什么速度发帧只要字节不丢、不乱序状态就能正确推进。帧头出现错误字节时通过状态回退保证重新同步这是最基础但最实用的容错设计。如果上位机发的是 ASCII 字符串命令比如 ATSTATUS\r\n也可以用 strcmp 直接比较。要注意 stm32f103 里 strcmp 依赖字符串结尾的 \0而串口收到的原始数据没有结尾必须在接收缓冲区末尾手动补 \0 再比较否则 strcmp 会越界读到不确定内容。比较之前先做 \r\n 定界处理比直接在整个缓冲区上 strcmp 更可靠。4.4 环形缓冲区替代简单数组简单数组接收有个边界问题不定长帧什么时候算收完。一个常见做法是环形缓冲区加 IDLE 中断空闲超时代表一帧结束。环形缓冲的实现用 256 字节配合掩码取模比取模运算快很多。#define RX_RING_SIZE 256 #define RX_RING_MASK 0xFF volatile uint8_t rx_ring[RX_RING_SIZE]; volatile uint16_t rx_head 0; volatile uint16_t rx_tail 0; /* 中断里写入缓冲区被写满时覆盖最旧数据 */ void ring_push(uint8_t b) { rx_ring[rx_head] b; rx_head (rx_head 1) RX_RING_MASK; } /* 主循环里读取返回 0 表示没有新数据 */ uint8_t ring_pop(uint8_t *byte) { if (rx_tail rx_head) { return 0; } *byte rx_ring[rx_tail]; rx_tail (rx_tail 1) RX_RING_MASK; return 1; }head 和 tail 相等代表缓冲区为空。这个实现不做防覆盖适合帧速率不高的调试场景。要防覆盖在 push 里判断 ((rx_head 1) RX_RING_MASK) rx_tail相等说明缓冲区已满这时丢弃新字节并置一个溢出计数供主循环查询。注意 RX_RING_SIZE 必须是 2 的整数次幂掩码才能用 来代替 %改成 512 或 1024 时掩码要同步改成 0x1FF、0x3FF。5. Uart 串口通信实验的故障排查与波形验证技巧5.1 回环测试定位是哪一端的问题把 PA9 和 PA10 用杜邦线短接程序上电后发送 0x5A随后轮询接收把收到的字节再发回 PC。短接时能自收自发说明单片机侧的 GPIO 配置、USART 初始化、波特率都是对的问题在单片机与 PC 之间的链路。接着去掉短接接上 USB 转 TTL 模块。USB 转 TTL 模块常用 FT232R 或 CP2102 芯片这两类芯片在 Windows 下如果设备管理器看不到 COM 口先装芯片厂商的官方驱动Windows 10 以上系统通常能自动识别但部分精简版系统需要手动指定驱动路径。CH340 芯片的驱动名和 FT232R 不一样不要装错。设备管理器出现 COM 口后再打开串口助手波特率、停止位、校验位全部与单片机一致发送 0x5A接收区能看到同样字节就说明整条链路通。回环测试有个陷阱PA9 和 PA10 短接后发送端和接收端是同一个芯片的同一个 USART哪怕两边波特率配置不一致也能收到数据因为收发共用同一个 BRR。所以回环测试通过不能完全证明波特率准确只能证明外设本身没坏。5.2 用逻辑分析仪看波特率误差逻辑分析仪夹在 PA9 上抓一段实际发送波形。一帧的标准结构是 1 位起始位低电平、8 位数据位、1 位停止位高电平共 10 个位时间。量出起始位下降沿到停止位结束的总时间除以 10 得到单个位时间做 1000000 / 位时间 换算就能得到实际波特率。实际波特率与设定值偏差在 ±2% 以内基本能正常通信超过 ±3% 就会出现偶发误码。自制最小系统的晶振不是 8MHz或晶振负载电容不匹配导致频率偏移在逻辑分析仪上一目了然。检查时把示波器探头或逻辑分析仪的地线夹在板子地不要夹在 USB 转 TTL 模块的地上两边参考地不一致时测出的波形是浮动的数值没有参考意义。5.3 十六进制打印对比字节序调试多字节协议时按 ASCII 字符串打印会把 0x00 显示成空字符0xA0 这类数据被当成扩展字符帧边界完全看不清。写一个十六进制打印函数把缓冲区内容逐字节转成两个十六进制字符输出void uart_print_hex(uint8_t *buf, uint16_t len) { uint8_t hex[] 0123456789ABCDEF; uint16_t i; for (i 0; i len; i) { UART1_SendByte((uint8_t)hex[(buf[i] 4) 0x0F]); UART1_SendByte((uint8_t)hex[buf[i] 0x0F]); UART1_SendByte( ); } UART1_SendByte(\r); UART1_SendByte(\n); }用这个方法观察收到的帧头、长度字段和校验和能快速定位帧格式错误。另一个高频问题现象是串口收到的整数高低字节颠倒F103 是 Cortex-M3 小端核上位机按大端发送 0x12 0x34单片机里按 uint16_t 读出来却是 0x3412。把发送端和接收端的打印都切到十六进制两端对比字节顺序这类问题几秒钟就能确认。串口协议里多字节整数建议统一按大端传输也就是高字节先发MCU 端解析时手动拼装避免不同平台字节序不一致带来的移植问题。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表