尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
嵌入式USART串口通信从入门到精通:原理、配置与实战避坑指南
1. 从点灯到通信为什么USART是绕不开的第一道坎刚接触嵌入式开发那会儿我总觉得点个LED、读个按键就已经算是入门了。直到某天导师扔过来一块板子说“把传感器数据传到电脑上看看”我才发现事情没那么简单。串口也就是USART成了横在面前的第一座山。你可能会问都什么年代了以太网、无线通信满天飞为什么还要死磕这个看起来有点“古老”的接口答案很简单它是调试的命根子是设备对话的普通话更是理解所有通信协议的活教材。USART全称Universal Synchronous/Asynchronous Receiver/Transmitter翻译过来叫通用同步/异步收发器。名字听着唬人拆开看就三件事通用意味着它不挑食什么数据都能传同步/异步说明它有两种工作模式但九成九的场景我们只用异步模式收发器就是它负责把并行数据变成串行比特流发出去再把收到的比特流拼回并行数据。你可以把它想象成一个特别靠谱的翻译官MCU内部是并行世界外部线路是串行世界USART就在中间做同声传译。那它到底解决了什么问题最直接的场景就是调试信息输出。代码跑飞了你总得知道飞到哪一步了吧在USART出现之前有人用LED闪烁次数来编码错误码闪三下代表这个错闪五下代表那个错效率低得令人发指。有了USART直接printf往电脑一扔变量值、函数调用栈、时间戳全出来了。另一个核心场景是设备间通信比如主控MCU和蓝牙模块、GPS模块、指纹模块之间的对话基本都是USART打头阵。这些模块往往只需要简单的指令交互USART的简洁性反而成了优势——不需要复杂的协议栈几根线一接就能通。这篇文章适合谁看如果你是刚学STM32或者ESP32的新手正在被“波特率”“中断”“DMA”这些词搞得头晕那这篇就是为你写的。如果你已经用过串口但总是遇到乱码、丢数据的问题这里也有排查思路。甚至如果你是从Arduino转过来的想搞清楚底层到底发生了什么我也会把寄存器层面的逻辑讲清楚。我的目标很简单让你看完就能动手配一个稳定的串口并且知道每一步为什么要这么做。2. 异步通信的底层逻辑没有时钟线怎么对齐2.1 起始位、数据位、校验位、停止位到底怎么排的异步通信最反直觉的地方在于收发双方没有共享时钟线。那接收方怎么知道每一位持续多久答案是事先约定好波特率。波特率就是每秒传输的码元数单位是bps。比如9600波特率意味着每一位持续1/9600秒大约104微秒。发送方按照这个节奏往外吐比特接收方按照同样的节奏去采样。这就像两个人约好每隔一秒说一个字虽然没有节拍器但各自心里默数只要手表走得一样准就能对上。但光有波特率还不够因为数据不是连续不断的总得有个开头和结尾。于是就有了帧格式。一个典型的USART帧长这样空闲时线路是高电平突然拉低一个位的时间这就是起始位告诉接收方“注意我要开始发了”。接收方检测到这个下降沿就知道接下来要按约定节奏采样了。紧接着是数据位通常是8位从最低位开始发。你可以选择5到9位但8位是绝对主流因为一个字节刚好8位。数据位之后是校验位可选奇校验、偶校验或者无校验。校验位的逻辑很简单如果选偶校验那么数据位加校验位中1的个数必须是偶数奇校验则必须是奇数。接收方一算发现不对就知道传输过程中有比特翻转了。最后是停止位把线路拉回高电平长度可以是1位、1.5位或2位通常用1位。把这些串起来一个最常见的配置就是9600-8-N-1波特率96008位数据无校验1位停止位。总共10位传一个字节实际占用10位的时间。如果你要传1000个字节那就是10000位9600波特率下大约需要1.04秒。这个计算在评估通信耗时的时候非常有用。2.2 波特率误差为什么会导致乱码理论上收发双方波特率一致就能通但实际中晶振有误差分频系数有取整误差累积起来就会导致采样点偏移。接收方一般在每一位的中间时刻采样这样容错空间最大。如果波特率偏差太大采样点就会逐渐偏离位中心最终采到相邻位上去数据就全乱了。那误差允许多少通常要求总误差小于3%到5%具体取决于数据位长度和采样机制。很多MCU采用16倍过采样也就是每个位采样16次取中间第8、9、10次的三取二判决这样能容忍的误差更大一些。但如果你用内部RC振荡器做时钟源温漂可能就有±2%再加上分频误差很容易超标。所以用USART时尽量选外部晶振这是血泪教训。我见过太多人用内部HSI跑串口常温下勉强能通一到冬天或者夏天就疯狂出乱码查半天以为是代码问题其实是时钟在飘。计算波特率分频值有个公式USARTDIV fCK / (16 * 波特率)。假设系统时钟72MHz要9600波特率那USARTDIV 72000000 / (16 * 9600) 468.75。整数部分是468小数部分0.75乘以16等于12所以BRR寄存器写0x1D4C。实际波特率就是72000000 / (16 * 468.75) 9600误差为零。但如果系统时钟是25MHz算出来USARTDIV 162.76取整后实际波特率会有约0.15%的误差这完全可以接受。关键是要养成算一下误差的习惯别等到出问题了才回头查。2.3 全双工、半双工和单工到底怎么选USART支持全双工意味着收发可以同时进行因为TX和RX是两根独立的线。全双工的好处是效率高你发指令的同时可以收数据不需要切换方向。但有些场景为了省线或者省引脚会把它配成半双工也就是TX和RX合并成一根线分时复用。半双工需要软件控制方向切换麻烦不说还容易冲突。除非引脚实在不够否则一律用全双工。单工就更少见了要么只发要么只收。比如某些传感器只往外吐数据那MCU这边就只需要配RX。这种场景下把TX引脚省出来做GPIO用也算是一种资源优化。但要注意即使你只用RXUSART的发送部分也得初始化否则某些MCU会进不了接收状态这是硬件设计上的坑。3. 从寄存器到库函数手把手配一个能用的串口3.1 引脚、时钟、波特率初始化三件套不管你用标准库、HAL库还是直接怼寄存器初始化USART都绕不开三件事开时钟、配引脚、设参数。以常见的STM32F103为例假设我们用USART1TX是PA9RX是PA10。第一步开时钟。USART1挂在APB2总线上GPIOA也挂在APB2上。所以RCC-APB2ENR | RCC_APB2ENR_USART1EN | RCC_APB2ENR_IOPAEN;。这一步经常被忘记结果就是寄存器写不进去读回来全是零。我刚开始学的时候对着手册查了半天为什么BRR写不进去最后发现是时钟没开那种感觉真是想砸键盘。第二步配引脚。PA9要配成复用推挽输出因为TX是MCU往外驱动PA10要配成浮空输入或者上拉输入因为RX是接收外部信号。在F1系列里CRL寄存器控制低8位引脚PA9和PA10在CRH里。具体操作是GPIOA-CRH ~(0xFF 4); GPIOA-CRH | (0x0B 4) | (0x04 8);。0x0B表示复用推挽输出50MHz0x04表示浮空输入。如果你用的是F4或者H7系列引脚配置在MODER、OTYPER、OSPEEDR、PUPDR这几个寄存器里逻辑类似但更清晰。第三步设参数。先算好BRR的值写进去然后配置CR1寄存器使能USART、使能发送、使能接收、使能接收中断如果需要的话。USART1-CR1 USART_CR1_UE | USART_CR1_TE | USART_CR1_RE | USART_CR1_RXNEIE;。注意UE位要最后置1否则配置可能不生效。这个顺序问题在参考手册里有明确说明但很多人不看直接一把梭结果就是时好时坏。3.2 发送一个字节为什么要等TXE标志发送数据看起来简单把字节写进DR寄存器就完事了。但实际代码里总能看到while(!(USART1-SR USART_SR_TXE));这样的等待。TXE是“发送数据寄存器空”标志置1表示DR里的数据已经被搬到移位寄存器了你可以写下一个字节了。如果不等待直接写就会覆盖掉上一个还没发完的数据导致丢字节。这里有个细节TXE置1只代表数据从DR搬到了移位寄存器并不代表移位寄存器已经发完。真正发完要看TC标志。如果你在发送完最后一个字节后立刻关闭USART或者进入低功耗必须等TC置1否则最后一个字节可能没发出去。我踩过这个坑发完AT指令立刻切电源模块根本没收到因为最后一个字节还在移位寄存器里没出去。用中断发送的话流程是使能TXEIE在中断服务函数里判断TXE然后写下一个字节。当所有字节发完关闭TXEIE。这样CPU不用死等效率高很多。但要注意中断优先级和临界区保护如果发送缓冲区是全局的主循环和中断都在操作就得加锁或者用环形缓冲区。3.3 接收中断和空闲中断的配合使用接收比发送麻烦因为你不知道对方什么时候发、发多少。最简单的办法是轮询RXNE标志收到一个读一个。但这样CPU利用率极低而且如果对方发得快你处理得慢就会溢出。所以实际项目里基本都用接收中断每收到一个字节进一次中断把数据存到缓冲区里。但问题来了你怎么知道一帧数据收完了如果协议是固定长度还好收够N个字节就处理。但很多协议是变长的比如AT指令的响应以\r\n结尾长度不固定。这时候空闲中断就派上用场了。空闲中断的触发条件是总线在接收完一个字节后保持空闲状态超过一个字节的时间。利用这个特性你可以在接收中断里往缓冲区塞数据在空闲中断里置一个标志告诉主循环“一帧收完了可以处理了”。这个组合拳我用了无数遍稳定得不行。具体配置是使能RXNEIE和IDLEIE在USART中断服务函数里判断这两个标志。收到数据就存检测到空闲就置标志并清IDLE标志。注意清IDLE标志的顺序先读SR再读DR这是手册里明确要求的顺序反了清不掉。4. 那些年我们踩过的串口坑问题排查实录4.1 乱码问题排查表乱码是串口调试中最常见的问题没有之一。原因五花八门我整理了一个排查表按可能性从高到低排列现象可能原因排查方法全是乱码一个可读字符都没有波特率不匹配检查双方波特率是否一致用示波器测位宽偶尔出现乱码大部分正常时钟误差偏大或干扰换外部晶振检查地线是否接好前几个字节正常后面全乱接收溢出或缓冲区越界检查接收中断优先级增大缓冲区特定字符总是错比如0x00校验位或停止位配置错误确认双方帧格式完全一致上电初期乱码过一会正常晶振起振慢或电源不稳增加上电延时检查电源纹波波特率不匹配是最常见的。很多人用CH340或者CP2102这类USB转串口芯片电脑端驱动默认波特率可能和代码里设的不一样。我习惯在代码开头先发一串\r\n\r\n如果电脑收到的是乱码那基本就是波特率问题。另一个隐蔽的坑是数据位和停止位有些模块默认是7位数据、2位停止位你按8-N-1去配收到的数据就总是差一位。4.2 数据丢失的三种典型场景数据丢失比乱码更隐蔽因为收到的数据看起来是对的只是少了几个字节。第一种场景是接收溢出中断服务函数执行时间太长下一个字节已经来了还没读走DRORE标志置1数据就丢了。解决办法是缩短中断服务函数只做存缓冲区的操作复杂处理放到主循环。如果波特率很高比如115200以上可以考虑用DMA接收。第二种场景是发送时没等TXE。前面说过连续写DR会覆盖。有些人用printf重定向到串口printf内部是循环调用发送函数的如果发送函数没等TXE打印长字符串时就会丢字符。标准库的printf重定向通常写成while(!(USART1-SR USART_SR_TXE)); USART1-DR ch;这个等待不能省。第三种场景是中断优先级冲突。如果USART中断优先级低于某个频繁触发的定时器中断USART中断就会被延迟响应导致溢出。我一般把USART接收中断设为次高优先级仅次于系统滴答定时器这样既能保证实时性又不会打断关键时序。4.3 电平不匹配导致的通信失败USART是TTL电平高电平通常是3.3V或5V。如果你直接把它接到RS232接口上那肯定通不了因为RS232用负逻辑-3V到-15V表示13V到15V表示0。中间需要电平转换芯片比如MAX3232。反过来如果你把两个不同电压域的MCU直接连USART比如一个3.3V一个5V3.3V的TX接5V的RX5V那边可能识别不了3.3V的高电平导致通信失败。这时候要么加电平转换要么选容忍5V输入的引脚。还有一种情况是共地问题。两个设备只连了TX和RX没连GND信号就没有参考点通信时好时坏。我见过有人用电池供电的模块和电脑通信忘了接地线结果数据全是乱的。记住任何通信地线都是第一根要接的线。5. 进阶玩法DMA、流控和协议设计5.1 DMA接收为什么能解放CPU当波特率达到115200甚至更高时每个字节间隔只有几十微秒中断方式虽然能用但CPU频繁进出中断也是不小的开销。这时候DMA就派上用场了。配置DMA接收的思路是让DMA在后台把DR寄存器的数据搬到内存缓冲区CPU完全不用管。你只需要在空闲中断里检查DMA还剩多少个没搬就能算出收到多少字节。具体配置步骤使能DMA时钟配置DMA通道的源地址为USART1-DR目标地址为接收缓冲区传输方向为外设到内存循环模式或者普通模式。然后在USART的CR3寄存器里使能DMAR位。这样每收到一个字节DMA自动搬走完全不占CPU。空闲中断触发时用缓冲区总大小 - DMA剩余传输数量就是实际收到的字节数。这个方案我用来接收GPS模块的数据波特率9600每秒几十个字节CPU占用率几乎为零。但要注意DMA传输完成中断和空闲中断的配合以及缓冲区对齐问题。如果缓冲区是uint8_t数组DMA配置成字节传输就行如果是uint32_t数组就得配成字传输但USART的DR是8位的所以还是用字节传输最省心。5.2 硬件流控RTS/CTS什么时候必须用硬件流控是防止接收溢出的终极手段。原理很简单接收方准备好接收时把RTS拉低发送方检测到CTS为低才发送。这样接收方缓冲区快满的时候拉高RTS发送方就暂停。听起来很美好但实际项目中用的人不多因为要多接两根线而且很多USB转串口模块不支持流控。那什么时候必须用当发送方发得飞快接收方处理不过来而且软件流控XON/XOFF又不能用的时候。比如两个MCU之间高速传输大量数据接收方还要做Flash写入写入期间不能接收这时候硬件流控就是救命的。但如果你只是调试打印或者和传感器模块通信数据量小那完全没必要。配置流控需要设置CR3寄存器的RTSE和CTSE位并且把对应的引脚配成复用功能。注意RTS和CTS的极性不同MCU可能不一样查手册确认。5.3 自定义帧格式的四个设计要点当你用USART传输自定义协议时帧格式设计得好不好直接决定了通信的可靠性和解析的难易度。我总结四个要点第一帧头要足够独特。用0xAA、0x55这种交替模式做帧头不容易和数据混淆。如果数据里也可能出现0xAA那就用双字节帧头比如0xAA 0x55概率就低多了。第二长度字段要明确。帧头之后跟一个长度字节表示后续数据有多少字节。这样接收方先收帧头再收长度然后按长度收数据逻辑清晰。长度字段本身要包含校验防止长度字节出错导致整个帧解析错位。第三校验方式要匹配场景。简单的和校验所有字节相加取低8位够用但检错能力弱。CRC16检错能力强但计算量大。如果MCU资源紧张和校验也能凑合如果对可靠性要求高老老实实上CRC。第四超时机制不能少。即使有长度字段如果传输过程中丢了一个字节接收方会一直等下去。所以需要一个超时定时器比如50ms没收到新字节就重置接收状态机等待下一个帧头。6. 从USART到其他协议一通百通的底层思维6.1 USART和I2C、SPI的本质区别学完USART再看I2C和SPI你会发现它们解决的是不同维度的问题。USART是异步的没有时钟线靠波特率对齐I2C和SPI是同步的有时钟线收发双方严格按时钟节拍走。同步的好处是速率可以很高SPI随便上几十MHzI2C标准模式100kHz快速模式400kHz高速模式3.4MHz。异步的好处是省线两根线就能全双工。另一个区别是拓扑结构。USART是点对点的两个设备之间直连。I2C是总线式的一根线上可以挂多个从设备靠地址区分。SPI是主从式的每个从设备一根片选线硬件上比I2C多几根线但协议更简单没有应答机制全速跑就行。理解了这些区别你就能根据场景选协议点对点、中低速、省线选USART多设备、中速、省引脚选I2C高速、短距离、多设备选SPI。没有哪个最好只有哪个最合适。6.2 同步模式下的USART能干什么USART的同步模式经常被忽略因为它默认是异步的。但同步模式其实很有用它把TX引脚变成时钟输出RX变成数据线相当于一个简化的SPI。你可以用它来驱动一些需要时钟同步的芯片比如某些ADC或者移位寄存器。同步模式的配置比异步多一步设置CR2寄存器的CLKEN位然后配置CPOL和CPHA决定时钟极性和相位。但说实话同步模式用的人很少因为SPI硬件更专业配置也更灵活。USART的同步模式更像是一个“附赠功能”在引脚紧张的时候可以顶一下。如果你真的需要同步通信优先选SPI除非那个MCU没有SPI外设。6.3 用USART理解所有通信协议的共性最后我想说USART是理解通信协议的最佳切入点因为它足够简单又包含了所有通信协议的核心要素帧格式、同步机制、差错控制、流量控制。你把这四个概念在USART上搞明白了再看Modbus、CAN、TCP会发现它们只是在这些要素上做了不同的取舍和扩展。比如Modbus RTU就是基于USART的帧格式里加了地址、功能码、CRC本质上还是串口那一套。CAN总线虽然物理层不同但帧结构里的仲裁段、控制段、数据段、CRC段逻辑上和USART帧是相通的。TCP的滑动窗口、重传机制对应到USART就是流控和校验重发。底层思维一通上层协议百通这是我做了这么多年嵌入式最深的体会。所以别小看USART它不只是个调试工具更是你理解整个通信世界的钥匙。把它的每一个细节都吃透后面学什么协议都会觉得似曾相识。我在实际项目中用USART最频繁的场景还是调试输出和模块通信但每次遇到新协议我都会先想想它在USART的基础上加了什么、减了什么这样理解起来特别快。如果你正在学通信协议我的建议是先把USART玩到滚瓜烂熟再去看别的你会发现一切都顺理成章。
RELATED

相关推荐

嵌入式项目深度比数量更重要:从STM32+Linux看系统级能力构建

嵌入式项目深度比数量更重要:从STM32+Linux看系统级能力构建

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/18 1:02:01
基于仓颉语言与Tauri的Modbus调试工具

基于仓颉语言与Tauri的Modbus调试工具

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📅 2026/10/18 1:02:01
MySQL面经核心考点全解析:索引、事务与锁的深度原理与实战

MySQL面经核心考点全解析:索引、事务与锁的深度原理与实战

MySQL面经这块儿,说穿了就三件事:索引、事务、锁。不管你面的是初级还是高级岗位,十家公司里有八家都围着这几个点打转,剩下的两成在问你分库分表和主从复制。我早几年准备面试的时候也走过弯路,抱着《高性能MySQL》啃…

📅 2026/10/18 1:02:01
MORE NEWS

更多资讯

📰

Breeze TTS 2部署指南:Docker镜像、A100/H100适配与显存需求全解析

【免费下载链接】Breeze-TTS-2 项目地址: https://ai.gitcode.com/hf_mirrors/BreezeBlue/Breeze-TTS-2 点击查看 免费下载 Breeze TTS 2 是一款面向实时交互的开源权重文本转语音(Text-to-Speech)模型。本指南带你完成 Breeze TTS 2 部署全…

📰

EmulatorJS:浏览器里的网页版模拟器,3 分钟搭好自己的复古游戏库

EmulatorJS:浏览器里的网页版模拟器,3 分钟搭好自己的复古游戏库 【免费下载链接】EmulatorJS A web-based frontend for RetroArch 项目地址: https://gitcode.com/GitHub_Trending/em/EmulatorJS EmulatorJS 是一个基于 JavaScript 的 RetroArc…

📰

能商用吗?Medical-Reasoning-SFT-GPT-OSS-120B许可证Apache-2.0解读与医疗数据合规清单

【免费下载链接】Medical-Reasoning-SFT-GPT-OSS-120B 项目地址: https://ai.gitcode.com/hf_mirrors/OpenMed/Medical-Reasoning-SFT-GPT-OSS-120B 点击查看 免费下载 结论先行:Medical-Reasoning-SFT-GPT-OSS-120B 是一个 Apache-2.0 许可证 的开源医…

📰

变频器EMC测试与整改实战:传导辐射抗扰度全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

📰

明日方舟日常自动化:MAA 一键全日常长草,4 步跑通

明日方舟日常自动化:MAA 一键全日常长草,4 步跑通 【免费下载链接】MaaAssistantArknights 《明日方舟》小助手,全日常一键长草!| A one-click tool for the daily tasks of Arknights, supporting all clients. 项目地址: http…

📰

用Python+Django开发网吧会员上机计费系统实战指南

接网吧管理系统这类活儿,十年前跟朋友合伙做网咖维护的时候,我就没少折腾过。现在很多年轻人想接这种单子练手,或者给亲戚开的网吧做套会员计费系统,聊得最多的就是“Python Django 能不能搞定”。说实话,完全可以&am…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬