资讯动态

STM32串口通信入门:USART标准库实战与避坑指南

发布时间:2026/10/20 9:33:05 来源:尧图企业网站定制
学到第五章终于要碰串口了。说实话串口这个章节在我心里一直是分水岭——前面的GPIO、定时器再怎么说都是“芯片自己跟自己玩”顶多带个按键、LED而一到串口MCU开始跟外部设备“说话”这就完全不一样了。这篇文章是我跟着一套很受欢迎的STM32标准库视频教程学习到第五章时整理的笔记内容包括USART的接线方式、底层通信原理、标准库初始化流程、收发实测代码以及我在调试时踩过的一堆坑。适合刚学完GPIO和定时器、正准备进入通信外设的初学者。1. 第五章为什么从“点灯”跳到“通信”在STM32的学习路线里第五章通常是一个明显的分界点。前几章无论怎么说对象都是自己内部的东西GPIO高低电平、定时器计数程序行为都可以用逻辑分析仪甚至LED肉眼观察。而从这一章开始芯片要和外设沟通沟通的对象可能是电脑、蓝牙模块、GPS模块也可能只是另一块单片机。“通信”这两个字一进来学习方式就不一样了你不能再只盯着一根引脚的状态而要开始关注时序、速率、协议、缓冲这些概念。1.1 串口在课程中的位置调试口的“基础属性”串口几乎是每一款单片机开发板上默认存在的接口甚至很多板子上只有两个调试接口一个SWD用于下载程序另一个就是串口。学好串口首先得到的就是“眼睛”——程序跑没跑、变量对不对、中断有没有触发只需要printf一条消息发到电脑上就全看见了。没有串口之前我调试基本靠一个LED翻转有了串口之后所有信息监控都简单了一个量级。所以很多有经验的开发者会把串口当成标配外设在写任何项目之前先把串口初始化好一旦程序跑飞了、逻辑不对了第一反应就是“串口打点”。这章学完你会多一个特别趁手的调试工具往后的ADC、DMA、I2C等章节都可以依赖它输出内部状态。从整体课程结构看第五章正是把“只会执行”的单片机变成“会交流”的单片机的转折点。1.2 学这一章之前建议先补齐的三块基础第一个基础是时钟系统。串口作为APB2外设挂在高速外设总线上配置之前必须先通过RCC打开USART1的时钟和引脚GPIOA的时钟。很多人初始化之后串口完全没反应排查老半天最后发现时钟忘开了。第二个基础是GPIO的复用模式。PA9、PA10这两个引脚默认是普通IO口要作为USART1的发送、接收引脚必须把PA9配置成复用推挽输出PA10配置成浮空输入。如果你对复用模式没有概念可以先回头翻翻GPIO章节。第三个基础是中断处理流程。实际使用串口不可能只靠轮询接收数据几乎总是要用中断否则CPU会被卡死在等待数据上。中断优先级分组、中断服务函数怎么写前几章学定时器时应该已经接触过如果忘了建议重新敲一遍外部中断的例程再往下走。提示串口这一章会涉及不少寄存器层面的概念但标准库已经把寄存器都封装好了。学习时不要一开始就扣每个寄存器而是先把“配置外设→数据收发→中断处理”这条主线跑通再回头研究寄存器细节。2. 串口通信的硬件本质IC之间交流的“共同语言”串口通信的本质其实就是双方约定好一种“共同语言”然后在一个引脚上按时间顺序把8个bit或者更多位传过去。这句话听起来简单但其中有几个概念必须先搞清楚否则后面写代码时你会觉得所有参数都是死的根本不知道为什么要填那些值。2.1 从电平和二进制说起TTL串口的物理层在STM32板上串口直接输出的是TTL电平高电平表示1低电平表示0。发送端通过控制引脚的高低电平变化就能把数据按bit发出去。比如要发送一个字节0x55二进制是01010101发送端就会在TX引脚上依次拉低、拉高、拉低、拉高……一共八个变化的电平。接收端在同一个时刻去采样RX引脚上的电平再把它们还原成一个字节。听起来像摩斯电码对吧不过串口的每个bit时间长度是固定的这个固定长度就是由波特率决定的。收发双方必须使用相同的波特率否则接收端采样到的电平就会“错位”出来的字节就是乱码。你调试串口时最常见的乱码问题十有八九是波特率不一致造成的。当时我为了弄懂这件事特意用逻辑分析仪抓了一次波形先抓一个正确的0x55再故意把波特率调高一点抓同一个字节两帧波形肉眼可见地对不上从此对“波特率必须一致”这句话有了直觉记忆。2.2 一个字节的完整旅程起始位、数据位、校验位与停止位串口发送一个字节并不是直接把8个电平丢到线上就完事。为了告诉接收端“我要开始传了”“哪一个是数据”“传完了没有”还需要在数据前后加上固定的标识位。标准的一帧串口数据格式是默认空闲状态时为高电平发送方先拉低一个bit时间作为“起始位”然后按顺序发送数据位通常是低位在前最后再拉高至少一个bit时间作为“停止位”。可选地还会加一位校验位用来简单判断这一帧数据有没有传坏。参数常见取值作用起始位1 bit标志一帧数据开始从高电平跳变到低电平数据位8 bit 或 9 bit真正要传的数据通常8位即一个字节校验位无校验/奇校验/偶校验统计数据位中1的个数用于简单检错停止位1 bit 或 2 bit拉高电平给接收端一个“这一帧结束了”的信号这里有个初学者很容易犯迷糊的点为什么起始位一定是“下降沿”因为平时串口引脚空闲时是高电平接收端一直在等待高电平。一旦它检测到一个下降沿就知道这是起始位然后按波特率定时采样后面的电平。这正是为什么如果波特率不对接收端从错误的时间点开始采样整帧数据全乱套。2.3 波特率到底怎么算不是随便填的标准库里使用USART_InitStructure.USART_BaudRate 115200;但你知道这个值是怎么映射到硬件上的吗STM32内部有一个波特率寄存器USART_BRR它把一个时钟频率分频成想要的波特率。比如USART1挂在APB2总线上时钟是72MHz。对于USART过采样率默认是16那么要得到115200的波特率分频系数就是72000000 / 115200 ≈ 625把这个值拆成整数和小数部分写入BRR寄存器硬件就能在每次电平切换时持续稳定地产生115200个码元/s的时序。标准库把这些运算封装好了你只需要填波特率数值但理解这件事很重要。当你用8MHz外部晶振而不是72MHz时如果没把PLL配置对实际波特率会差非常多表现同样是乱码。所以别再以为串口只管填个数就能用了它背后依赖的是整个时钟树的准确性。在第五章的练习里我建议你故意把外部晶振配置改错一次亲眼看一次乱码之后再遇到类似问题就会条件反射地去查时钟和波特率。3. 硬件接线实测USB转TTL模块与最小系统板的连接看完原理下一件事就是动手接线。很多人在写代码之前就死在硬件上线插反了、没共地、电平不对各种问题。这一节我把我自己用过的接线流程完整写出来按这个顺序来基本能一次通过。3.1 硬件准备清单我用的是一块STM32F103C8T6最小系统板配合一个常见的CH340 USB转TTL模块再准备几条杜邦线。如果你用的是带板载串口芯片的开发板一般不用外接模块但也要先看板子原理图确认串口是否已经接到了USB口。硬件说明STM32F103C8T6最小系统板3.3V供电引出PA9/PA10USB转TTL模块CH340将USB信号转为TTL电平引出TX/RX/GND/VCC杜邦线公对母或母对母根据板子接口选择串口调试助手PC端工具常见的有“串口助手”等软件另外建议准备一个万用表用来测量模块输出的电平是不是3.3V。没有万用表的话至少要看模块丝印很多模块上会标注“3.3V/5V”或“Level”字样。我一开始偷懒没测结果模块输出的是5V电平差点把单片机引脚搞坏。3.2 接线步骤与最容易忽略的“共地”接线核心就三根线板子的PA9USART1_TX接模块的RXD板子的PA10USART1_RX接模块的TXD板子的GND接模块的GND。注意是“交叉连接”发送接接收接收接发送不要一根根全接同名端子否则两端都在发数据永远互相听不见。当时我第一次接的时候就忘了接GND。结果模块上电后电脑能识别到但发的数据一头都收不到。原因是收发双方的电平基准不一样TX引脚上的高电平对于模块来说可能是悬空的完全没法判断是1还是0。后来把GND连上问题立刻消失。这一条请务必写在自己的学习笔记第一行串口模块必须共地否则数据一定乱飞。3.3 电压等级匹配为什么不能随便用5V模块USB转TTL模块通常有两种一种板上有电平转换芯片兼容3.3V另一种是简单的5V TTL输出。STM32的IO耐压虽然可以承受5V输入但很多最小系统板的串口引脚并没有加5V容忍配置而且模块的TX输出5V高电平直接怼到PA10上长期使用并不稳妥。安全做法是购买模块时直接买带3.3V/5V电平切换或明确标注“3.3V兼容”的型号如果没有切换开关也可以在模块和STM32之间串一个电阻分压或者用单向电平转换模块。我踩过一次坑用了不兼容5V的模块结果PA10长时间被5V电平冲击虽然没有立刻烧毁但后来这个脚就不好用了。所以接线前一定要看模块的丝印确定有3.3V标称值再接。4. 标准库配置USART1从外设时钟到NVIC的完整链路硬件接好之后开始写代码。串口的初始化代码不像点灯那样一两行就完事它是一条完整的链路时钟→GPIO→USART参数→中断→使能。容易漏步骤但每一步都有明确的原因。我习惯先把这段代码从main函数里抽出来单独封装成USART_Init函数以后每个项目都能复用。4.1 开启时钟与引脚复用两步缺一不可我用的标准库初始化第一行就是RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE);这条语句同时打开USART1外设时钟和GPIOA的时钟。之后配置PA9、PA10时必须明确告诉芯片这两个引脚不是普通IO口而是被外设复用。PA9要作为USART1的发送输出标准库的配置是GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure);PA10则配置为浮空输入因为接收引脚的任务是把外部电平采样进来不需要输出能力GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOA, GPIO_InitStructure);这里有个容易忽略的点PA9复用推挽输出里的“复用”到底复用的是什么其实是说这个引脚的电平不再由GPIO输出数据寄存器控制而是交给USART外设内部的发送移位寄存器控制。如果你错误地把PA9配成了普通推挽输出那它永远只能输出自己设置的0或1串口发的数据根本出不来。4.2 USART_Init结构体每个字段到底在说什么接下来是USART1的参数配置标准库代码一般是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_BaudRate波特率选115200这个数值在绝大多数串口助手下都能稳定支持。有人喜欢用4800、9600不用纠结只要两边一致就行。USART_WordLength数据位长度选8位也就是一个字节。USART_StopBits停止位选1位这也是默认主流配置。USART_Parity校验位初学者一律选无校验避免理解额外的校验逻辑。USART_HardwareFlowControl硬件流控我们只接三根线没接RTS/CTS所以必须选None。USART_Mode收发模式这里同时使能接收和发送等于告诉外设“你既要能发也要能收。”到这里你可能会问为什么不需要配置“先发高位还是先发低位”因为串口协议规定好了数据位低位在前你配置8位数据硬件就会自动按低位到高位发送不用软件干预。标准库把这些细节都处理好了。如果你有兴趣深挖可以直接读寄存器版本对比一下标准库帮你做了哪些位运算会更佩服库函数的封装思路。4.3 使能串口与NVIC中断别漏掉最后的关键一步参数配置完很多人直接开始调收发结果发现一点反应都没有。原因往往是忘了使能串口。标准库中必须显式调用USART_Cmd(USART1, ENABLE);库函数并不是在USART_Init内部帮你自动使能外设的这是一个常见的疏漏。使能之后串口才开始真正工作。接下来如果要用中断接收还要配置NVIC。这玩意儿的本质是串口收到数据后会产生一个中断请求但CPU怎么响应这个请求取决于你在NVIC里有没有把串口中断“打开”。配置代码NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_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);同时还要在USART的配置中开启接收中断USART_ITConfig(USART1, USART_IT_RXNE, ENABLE);RXNE就是“接收数据寄存器非空”标志位。一旦串口收到一个完整字节RXNE硬件置位中断控制器就会去调用USART1_IRQHandler。这个流程想清楚以后串口中断就一点都不神秘了。4.4 main函数里的初始化顺序为什么不能乱很多人习惯把外设初始化都堆在main函数开头这个习惯本身没错但顺序经常有问题。以串口为例正确的顺序是优先配置时钟系统如果是标准库默认启动文件SystemInit已经配好72MHz然后NVIC分组再初始化GPIO再初始化USART参数最后使能串口和中断。顺序错一个可能会导致中断请求来的时候NVIC还没配置好第一次数据就丢了。具体的代码结构可以这样int main(void) { NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); USART_Init(); while (1) { // 主循环... } }把所有串口初始化都封装在USART_Init()中main函数看起来非常干净。我一般还会在USART_Init最后调用一次发送测试字节比如“AT”之类的字符这样上电就能立刻看到串口有没有通不用等程序跑到后面逻辑。这个小技巧帮我省下了大量定位时间。5. 代码实战让数据在MCU和电脑之间“跑一趟”初始化完成下面进入最有意思的环节实际收发。我先从最底层的单字节收发写起再封装成常用函数最后用回环测试验证链路。5.1 发送一个字节、发送字符串的底层实现标准库提供了发送函数USART_SendData(USART1, byte)但这里有个坑它只是把字节写入发送数据寄存器并不代表这个字节已经发完了。如果你紧接着就发下一个字节前一个字还没移出去新数据就会覆盖它导致数据丢失。正确的发送方式是写一个等待函数检查TXE标志void USART_SendOneByte(uint8_t byte) { USART_SendData(USART1, byte); while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); }TXE清零表示“发送数据寄存器已空”即当前字节已经被移入移位寄存器可以继续丢入下一个字节。基于这个函数再封装字符串发送void USART_SendString(char *str) { while (*str) { USART_SendOneByte((uint8_t)*str); } }注意字符串要以\0结尾否则这个函数会一直读下去。我在早期就因为没有在字符串常量末尾加结束符导致发送完预期内容后又吐出一堆乱码。5.2 重定向printf让调试信息随手可得没有串口之前想看到变量的值我的做法是加个断点或者把值跟某个LED关联起来怎么看都别扭。有了串口后自然想用printf直接打印。标准库的printf默认是往显示器输出的对于嵌入式需要重定向fputc函数int fputc(int ch, FILE *f) { USART_SendOneByte((uint8_t)ch); return ch; }这里其实只用到了前面封装的发送函数。注意要包含stdio.h并且在项目设置里勾选“使用微库”MicroLIB否则Keil工程里链接器找不到printf的实现或者会占用大量Flash。同样为了接收scanf可以重定向fgetc不过初学阶段先搞定输出就够了。重定向之后你的调试方式会彻底改变printf(USART1 init ok, baud%d\r\n, 115200);在串口助手里会立刻看到一行日志。这里的\r\n要特别注意很多串口助手默认按窗口模式显示必须加上换行回车才会另起一行否则所有日志会挤成一团。5.3 接收中断怎么安全地处理一次收到的数据串口接收最忌讳的就是在主循环里轮询等待数据。那样不仅CPU效率低而且容易漏数据。标准库推荐的接收方式是中断。先写中断服务函数void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t received USART_ReceiveData(USART1); USART_SendOneByte(received); // 回显 } }这个最简单的“回显”逻辑非常适合验证串口通路无论你在电脑端发什么板子收到后都会原样发回来。如果电脑上能看到你发的字符原封不动弹回来说明硬件接线、波特率、初始化、中断全部OK。需要注意USART_ReceiveData(USART1)这个函数本身会清除RXNE标志位。不要在中断里既调用这个函数又手动去清标志容易造成标志误清。标准库封装的顺序是先判断标志再读数据读数据操作会自然清除RXNE这个流程就够了。5.4 回环测试一个字节都不改的最稳自检法回环测试是所有串口调试的第一步。硬件上有两个层面的回环第一种把单片机TX和RX直接短接PA9和PA10之间接一根杜邦线然后程序调用发送函数再打开接收中断看能不能收到自己发出去的数据。这个测试完全不需要外部模块能快速确认芯片内部USART收发链路是否正常。第二种接好USB转TTL模块后在串口助手里发送什么板子原样返回什么。这个验证的是完整链路电脑→USB模块→单片机→USB模块→电脑。只要这一步通了后面接GPS模块、蓝牙模块基本原理都是一样。我第一次做回环测试时发现发送“ABC”只回来“A”查了很久才发现是自己发送字符串的循环里没有等待TXE连续发第二个字符时把第一个覆盖了。这种问题在回环测试里暴露得特别快所以千万不要跳过这步直接写复杂协议。6. 避坑记录串口学习中的六个“幺蛾子”串口的坑非常多很多坑不亲自踩一遍光靠看视频根本意识不到。下面这六个问题是我在学习第五章时真实遇到过的我把排查链路写在这里如果你也碰到相同现象可以照着检查。6.1 乱码先查波特率再查时钟树如果电脑端收到一串完全看不懂的字符比如“#%”或者“烫烫烫”第一反应不要怀疑程序逻辑先确认波特率。在串口助手里把波特率值改成9600、4800试试如果换到某个数值后数据忽然变正常说明双方波特率不一致。还有一种情况是外部晶振频率不对我用过一块板子贴的是8MHz晶振但代码里按12MHz配置了PLL导致实际系统时钟偏差很大最后串口输出的波特率自然也对不上。这种需要先用逻辑分析仪或示波器测量TX引脚的波形周期或者直接检查RCC配置代码里PLL倍频数是否正确。6.2 数据出不来看看是不是忘开时钟或者没使能外设发送函数执行了但电脑端一个字节都收不到这种问题最常见的原因有三个第一串口外设时钟没开第二GPIO复用模式配错第三没调用USART_Cmd(USART1, ENABLE)。我建议在USART_Init函数的最后加一句USART_SendOneByte(U)上电后如果能看到“U”这三个原因就全部排除了。如果看不到再检查接线、共地、串口助手的COM口号是否选对。6.3 中断不触发很大概率是全局中断没开USART_ITConfig和NVIC配置都写了但主程序一进while(1)之后你发数据它就是不进中断服务函数。这种在初学阶段出现频率极高十有八九是NVIC_PriorityGroupConfig没调用或者在启动文件里没有使能全局中断。如果用的是标准库一般启动文件已经默认开了全局中断但某些移植的工程模板可能漏掉了。可以在main开头显式调用__enable_irq()CMSIS函数测试一下。另外别忘了在NVIC_Init之前先设置优先级分组否则分组不一致会导致中断响应异常。6.4 TXE和TC一个管“能发”一个管“发完”刚开始我以为只要检查TXE就够了后来发现发送完数据立刻关闭串口或者进入低功耗模式时最后几个字节会丢掉。原因是TXE表示数据已经进入移位寄存器并开始发送但移位寄存器里的这8个bit还没完全输出完。要确保一帧数据完全发送完毕应该检查TC发送完成标志。USART_SendData(USART1, byte); while (USART_GetFlagStatus(USART1, USART_FLAG_TC) RESET);在普通连续发送场景下用TXE就够了它能获得更高的发送速率。但如果你需要在发送完之后立刻改变引脚状态或进入低功耗模式就必须等待TC。这个概念需要分清楚否则遇到“最后一个字节丢失”时会很困惑。6.5 接收粘包没有协议导致的“边界”问题接收中断写好后你会发现如果电脑端一次性发来“ABC”USART1_IRQHandler会被连续调用三次每次收到一个字符。但如果你在中断里直接把这些字符拼进一个数组怎么知道一帧数据什么时候结束这就是“粘包/拆包”问题。最简单的做法是约定结束符比如用\r\n作为一帧数据的结束标志收到\r时认为一帧结束然后开始处理缓存里的内容。这个方法不完美但对于学习阶段足够用。更规范的方法是设计一个简单的帧格式帧头长度数据校验。不过这是后面学习通信协议的内容了。在第五章我建议先学会用结束符处理至少不会再把所有数据挤成一坨。6.6 串口助手设置三个不起眼的选项最后说一个和代码无关但经常卡住人的点。串口助手打开后至少有三个选项需要检查波特率选择是否和代码一致数据位/停止位/校验位是否匹配有没有勾选“按十六进制显示/发送”。很多新手在发送纯文本时不小心开了十六进制模式输入“FF”发出去的是两个字符FF不是0xFF。我在调试中也常因为显示模式没切换把正常ASCII码当成十六进制看半天结果代码根本没问题。经验把串口助手的参数模板保存好以后每次新建调试任务先加载同一套配置就不会在这种琐碎环节耗时间。写到这里串口这一章的核心内容基本就覆盖完了。我个人学下来的感受是串口是所有通信外设里最“朴素”但最实用的一种没有复杂的时钟分频协议没有频繁的数据仲裁牵涉到的知识反而更接近通信的基础逻辑。第五章学完之后后面的DMA、ADC、I2C章节大概率都要依赖串口来观察内部状态所以强烈建议在离开这一章之前亲手把回环测试跑通、把printf重定向配好、把接收中断捋顺。把这三种能力变成肌肉记忆后面会省下很多力气。最后再分享一个小技巧在你的串口初始化函数里保留一个上电自检字符串比如“USART1 Ready”每次烧录完成一打开串口助手就能确认板子还活着——这个小习惯帮我节省了大量因为硬件没连接导致的无谓排查时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑