
如果你翻过任何一个单片机项目的代码大概率最先见到的外设就是UART串口。程序员和硬件之间的第一句对话多半也是靠串口完成的。我打算开一个通信协议系列第一期就写UART——不是因为简单而是因为它最诚实没有时钟线没有应答机制一帧数据怎么走的全在引脚电平上摆着。这篇文章适合两类人一是刚接触STM32、Arduino、ESP32的嵌入式新手想弄明白串口到底是什么二是已经能编译能点灯但一遇乱码就抓瞎、想系统摸清原理的同学。我会把UART通信协议的时序、参数、硬件标准、STM32 HAL库下的实际写法以及我自己踩过的坑一次性讲透。看完之后你至少能回答那个困扰很多人的问题为什么我和设备明明连上了收到的却是乱码1. 为什么第一站是UART从串口开始认识通信协议1.1 通信协议到底在“约定”什么提到通信协议很多人第一反应是HTTP、TCP/IP那套互联网大厂的东西其实在硬件层面每一次数据交换都是协议在起作用。所谓协议本质上就是双方提前说好的“规矩”第一电平怎么代表逻辑0和逻辑1第二按什么节奏去采样第三数据怎么分组、怎么定帧第四出错之后怎么办。小到两个传感器之间的TTL串口大到整个网络的协议栈都能按这套思路拆解。我用一个土办法帮你记UART就是两个人隔空喊话。你得先约定声调一样高电平标准语速一样快波特率一句话从第几个字开始算真正要说的内容起始位和停止位万一听错了怎么补救校验位。这些约定在通信开始前就写好了发送方按规矩喊接收方按规矩听这就是一套完整的通信协议。1.2 UART、串口、COM口为什么总被混着叫UART是Universal Asynchronous Receiver/Transmitter的缩写指的是MCU或PC里的硬件收发控制器。严格来说UART本身不算通信协议真正被大家口头称为“UART协议”的东西是以TTL电平和帧格式为载体的异步串行通信。串口是个很泛的名词可能指UART串口也可能指USB枚举出来的虚拟串口而COM口是Windows给串口设备分配的逻辑编号比如COM3、COM7。很多初学者被这几个词绕晕我一句话给你归纳UART是硬件实体串口是功能接口COM口是操作系统视角里的门牌号。写代码时你把这几个词混着用功能上没错但排查问题时心里得清楚你正在查的到底是硬件、是驱动还是应用层配置。1.3 哪些场景绕不开UARTUART是少有的从调试板到工业现场都能见到的接口。单片机日志输出、Wi-Fi/蓝牙/4G模块的AT指令、GPS模块吐出的NMEA报文、LoRa/ZigBee透传、RS-485上的MODBUS、老式PLC编程口、甚至很多带I2C/SPI屏的设备都会留一个TTL串口当调试后门。这也是我把它放第一篇的原因认识UART等于打通嵌入式里最常见的一条路。它的优点是显而易见的最少两根线TX、RX加一个共地GND就能通信实现成本低调试工具又多。缺点也明显异步没有时钟默认只能点对点速率受电平和布线质量限制。但正是这种“够用”让它在几十年里一直没被淘汰反而成了单片机世界里的默认配置。2. UART通讯时序拆解一帧数据怎么在人眼皮底下跑完2.1 异步发送的本质没有时钟线靠约定的时间采样UART最反直觉的地方在于它没有时钟线。SPI和I2C这类同步通信会单独拉一根CLK数据线怎么变化都由时钟沿去采样。UART不干这事发送方和接收方各自拿自己的振荡器当节拍器然后靠一帧开头的一个特殊边沿来对齐时间。换句更直白的话UART的信息就是引脚电平随时间的变化。接收端一直在采样这根引脚平时看到高电平就认为总线空闲一旦发现一个从高到低的下降沿就当成“开工信号”从这一刻起按照约定好的波特率每隔一个位时间读一次引脚电平读满一帧再停下来等待下一位开始。所以它命名为“异步”收发双方不需要共享同一根时钟只需要共享同一个时间流速——波特率的精确度。2.2 一帧数据的完整结构空闲、起始、数据、校验、停止在TTL电平下UART的一帧长这样空闲态TX引脚保持高电平逻辑1。高是为了抗噪声也为了给下降沿留出明显的触发空间。起始位发送方把电平拉低一个位时间逻辑0。这是整帧唯一的同步信号接收方的下降沿检测全靠它。数据位紧接着是5到9个位常见是8位从最低位LSB开始逐位发送。每个位的时间长度是1/波特率。校验位可选用来统计数据位中1的个数是奇数还是偶数算一种最简单的错误检查。停止位拉高至少1个位时间表示这一帧结束也保证下一帧的起始位一定是一个清晰的下降沿。很多人第一次看逻辑分析仪抓出来的UART时序会惊讶这不就是把一个字节拆成一小段一小段的方波吗没错UART就是最朴素的串行化。以8N1格式为例8位数据加上起始位和停止位一共10个位时间在115200波特率下一帧只要86.8微秒理论上每秒能传约11520字节。这里补充一个有意思的细节停止位的长度不是只能选1。常见还有1.5位和2位选择2位后接收方会有更充足的时间去同步下一个帧代价是有效吞吐下降。大多数MCU外设对1、1.5、2位都支持但工程上最省事的还是1位。2.3 波特率发送与接收的“节拍器”波特率就是每秒传输的符号数。在UART里每个位就是一个符号所以波特率和比特率在这里是一个意思。我们最常挂在嘴边的是9600和115200前者是低速调试的经典值后者是很多Wi-Fi模块和蓝牙模块的默认值数值上正好是前者的12倍。两边波特率不一致是乱码的最大来源。为什么因为接收端根本不知道你的位周期它只能按自己的周期去猜。以8N1为例一帧一共10个位如果两边各自时钟有一点偏差前面几位还能蒙对越往后的位采样点偏移越大最终会掉进错误的位置。打个比方一个人按每字90秒的速度念另一个人按80秒的速度听开头几个字还能对上念完一整句话必然对不上账。所以硬件的计时精度很重要。标准PC串口时代16550这颗经典芯片用1.8432MHz晶振因为1843200这个数能被一堆标准波特率整除。目标波特率等于时钟除以16再除以除数寄存器里的值。比如9600波特率除数就是1843200/(16×9600)12。STM32的USART也是类似的思路从总线时钟经过预分频再通过USARTDIV得到最终波特率还提供过采样8倍或16倍的选项。过采样倍数越高对噪声的容忍能力相对越好代价是需要更高的外设时钟。工程上我的习惯是把两端时钟误差控制在1%以内。误差再大波形开始变形逻辑分析仪上能直观看到采样点越来越偏人眼都觉得别扭。2.4 数据位、校验位、停止位怎么选8N1是事实标准8位数据、无校验、1位停止位。单片机的普通串口、USB转串口工具、绝大多数传感器模块默认都是8N1。但工控现场你还会碰到8E1偶校验、8O1奇校验、7E1这类老配置MODBUS的不少终端也支持这些。校验位的作用很有限它只能检测出一帧里奇数个位出错的情况偶数个位错它照样认不出来更别说纠正错误了。所以真正要求可靠的协议比如MODBUS帧会在UART之上再加一帧CRC校验靠报文尾部的2个字节来确认数据完整性。奇偶校验在如今更像是一个快速筛错的门槛而不是可靠的保障。我的建议很简单没有特殊要求全部用8N1。当你外接一块新模块时去手册里查默认帧格式很多模块出厂是“115200-8N1”但总有不按套路出牌的尤其工控仪表可能要求你改成8E1才能握手成功。3. 从16550到USB转串口UART的硬件江湖3.1 16550标准UART为什么它是“行业标准”如果你翻过PC串口的老资料会频繁看到16550这个名字。1987年National Semiconductor推出的16550 UART芯片继承了8250/16450的寄存器接口最大的改进是加入16字节的收发FIFO。PC主板上曾经的COM口就是插着这颗芯片或它的兼容型号直到主板逐步取消串口。为什么叫行业标准因为后辈写UART驱动、做上层系统都以16550的寄存器风格为参照波特率除数寄存器、线路控制寄存器、FIFO控制寄存器、中断使能寄存器……这一整套寄存器地址和位定义成了无数UART兼容设备的蓝本。现代MCU内部的UART硬件虽然做了很多扩展但基本思想仍然是那套东西只是寄存器布局和FIFO深度变了。了解这段历史不是为了考古而是为了明白一件事UART的波形、时序和寄存器规则已经被几十年工业界反复验证过稳定性极佳。它不是某个厂商一拍脑袋定的私有协议而是一套所有人都承认、都能兼容的基线。你在今天任何一颗单片机芯片上写串口驱动基本上都能用当年16550的经验来理解。3.2 电平标准TTL、RS-232、RS-485别接错把UART波形挂在电线上之前先确认电平和协议匹配。芯片内部跑的是TTL电平0到0.3V左右表示逻辑02.4V以上表示逻辑1实际电路中常见3.3V或5V。但PC老式串口用的是RS-232电平逻辑0是正压逻辑1是负压幅值还动辄±12V逻辑极性也和TTL相反。你把TTL信号直接怼RS-232口轻则读不出重则烧坏引脚。RS-485则是另一种物理层用A、B两根线之间的电压差来表示0和1属于差分信号抗共模干扰能力强传输距离可以到几百上千米还能挂多个节点。但从UART帧结构看RS-485几乎没有改变什么——起始位、数据位、停止位依旧只是波形的物理表达方式换了。设计上我一般的选型是板内调试用TTL要和PC通信用USB转TTL或者通过MAX3232这类芯片转RS-232工业现场则用RS-485加SP3485/MAX485收发器。电平转换这活不难但忘接共地、接反高压线、或者拿5V电平怼3.3V引脚往往是UART踩坑的重灾区。3.3 USB转UART芯片FT232R/FT231X与驱动安装现在笔记本上已经没有原生串口大家普遍靠USB转UART小板来调试。最常见的方案里FT232R是FTDI家的经典款Windows和Linux都有成熟驱动FT231X是更新型号体积更小、速度更高最高能到3Mbps级别而且内置晶振外围电路非常简单。这类芯片在电脑上会枚举成一个虚拟COM口但要正常工作驱动必须对。Windows 10/11多数时候能自动装好FTDI的VCP驱动可我也遇到过设备管理器显示COM3打开串口助手却报错或者设备出现在“USB Serial Converter”带黄色感叹号的情况。遇到这种问题我的处理顺序是先到FTDI官网下载CDM组合驱动右键设备选择更新驱动程序手动指定驱动目录如果失败就拔掉USB线在设备管理器里卸载残留设备并勾选“删除驱动程序软件”然后重新插入最后尽量别把小板插在USB Hub上供电和信号都会影响枚举和稳定性。顺带说一句国产CH340、CP2102模块也很常见驱动安装思路类似只是驱动厂商和签名不同。只要芯片本身没问题、驱动干净实际使用效果区别不大。真正容易忽略的是小板上的电平标准同样叫USB转TTL有的输出3.3V有的是5V买时看仔细接错了我烧过电平转换器那不是代码能救回来的。4. STM32上用HAL库调通UART从初始化到收发数据4.1 从CubeMX配置看UART被抽象掉了什么STM32的HAL库把UART封装成一个UART_HandleTypeDef结构体你用CubeMX点点鼠标就能生成初始化代码。表面上看只是选引脚、填波特率但实际上它替你完成了三件事配置GPIO为复用功能连接到USART外设计算并写入波特率分频、字长、校验位、停止位、过采样使能USART外设和对应的中断。很多新手跳过了原理直接调函数项目能跑但一旦出问题就没头绪。我的建议是至少把结构体里那6个字段和参考手册对应着看一遍它们就是你在软件层面能看见的“协议设置”。另外有个加分细节USART里的US是Universal Synchronous/Asynchronous的意思说明它除了异步还能做同步模式。但绝大多数工程只用异步模式你可以先不用纠结同步那部分。4.2 最直观的APIUART发送与接收最简单的初始化和收发长这样UART_HandleTypeDef huart2; // 初始化1152008N1 huart2.Instance USART2; huart2.Init.BaudRate 115200; huart2.Init.WordLength UART_WORDLENGTH_8B; huart2.Init.StopBits UART_STOPBITS_1; huart2.Init.Parity UART_PARITY_NONE; huart2.Init.Mode UART_MODE_TX_RX; huart2.Init.HwFlowCtl UART_HWCONTROL_NONE; huart2.Init.OverSampling UART_OVERSAMPLING_16; HAL_UART_Init(huart2); uint8_t txBuf[] hello\r\n; HAL_UART_Transmit(huart2, txBuf, sizeof(txBuf) - 1, 1000); uint8_t rxBuf[32]; HAL_UART_Receive(huart2, rxBuf, 4, 1000);两个函数的最后一个参数是超时时间单位毫秒。阻塞发送的意思就是如果发送没完成或者接收没凑够字节数函数就一直等下去直到超时返回。在主循环里这么用没问题但在中断回调或者定时器中断里就要小心了——对方如果一直不发数据整个系统会卡死。还有一个人人都踩过的坑HAL_UART_Receive指定收N个字节可对方只发了2个字节然后停住函数会一直卡到超时才返回。所以阻塞式接收适合“一问一答”的场合不适合做流式不定长解析。4.3 不定长数据接收中断、空闲中断、DMA思路项目里十有七八接收端不知道一帧到底有多长。最简单可靠的办法是配合空闲中断总线上超过一个字节时间没有新数据就认为一帧结束。STM32的HAL库里单字节中断收包是这样uint8_t rxByte; uint8_t frame[64]; uint16_t frameLen 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART2) { if (frameLen sizeof(frame)) { frame[frameLen] rxByte; } HAL_UART_Receive_IT(huart2, rxByte, 1); } }每次收到一个字节回调函数把它存入缓冲区然后立刻重新开启下一次中断接收。你在上层再定义一个自己的超时判断比如在定时器里隔几十毫秒看一次frameLen有没有变化或者直接用空闲中断来定帧边界就能把frame当作完整的一包来处理。更高性能的方案是DMA加空闲中断DMA把数据连续搬进buffer空闲中断一进来用buffer长度减去DMA剩余计数__HAL_DMA_GET_COUNTER就是这一包的实际长度。这套方案在大流量日志下发时非常稳但理解曲线比较陡。我的建议是先拿单字节中断把协议跑通确认帧结构没问题再按需优化成DMA一步到位反而容易出玄学问题。4.4 把printf重定向到串口调试串口的尽头是printf。GCC工具链下等于是把标准库的底层写函数重定向到串口int _write(int file, char *ptr, int len) { HAL_UART_Transmit(huart2, (uint8_t *)ptr, len, 0xFFFF); return len; }在Keil MDK里则是重写fputc。处理好半主机模式后就能舒服地打印温度、电压、状态信息了。但我提醒一句串口打印不是免费的115200波特率下每秒约11.5KB如果日志刷太狠既吃掉CPU时间也可能把接收端的缓冲区打爆。我做项目的习惯是日志分级平时只开info级别debug级别用一个编译期宏关掉需要排查问题时再打开重编一版。5. 和I2C、SPI、CAN、SDIO同框比较UART到底弱在哪、强在哪5.1 常见通信协议一表看清芯片内部和外设之间常见的总线我把它们拉出来对比一下。这里不追求参数上的极限值只给一个工程上的直觉感受协议时钟线方向最少数据线速率参考多设备支持时序特点典型场景UART无全双工TXRX不计GND常见9600~12Mbps默认点对点自由电平、定时采样调试口、透传模块、RS-485I2CSCL半双工SDASCL100K/400K/1M多设备、地址寻址开漏、时钟同步、ACK传感器、EEPROM、低速总线SPISCK全双工MOSIMISO几十MHz靠片选CS做多设备强时钟同步Flash、SD卡、高速外设I2SBCLK/LRCLK全双工2~3根常见1~几十Mbps点对点为主左右声道同步音频采集与播放CAN无内部位同步半双工CANH/CANL差分常用250K~1M多节点、报文仲裁差分、ID仲裁、CRC汽车、工业控制SDIOCLK半双工CMD1/4根DAT几十Mbps级别一主机多卡命令数据阶段SD/TF卡、Wi-Fi模块GPIO无单向1根/位软件决定无纯电平逻辑按键、LED、软协议模拟在这张表里UART的技术含量不算高。它是唯一一个“不用关心总线时序细节也能用”的接口写代码的人只要往寄存器里丢一个字节剩下的位移、采样、停止位判定全部由硬件完成。这大概就是它几十年来稳坐调试口第一把交椅的原因。5.2 UART的生态位调试、长线、多电平转换从协议角度看UART确实弱没有I2C那样的地址寻址默认只能点对点对时钟误差敏感双方都需要精确校准想提高速率加FIFO、加DMA只能缓解最终还得看物理层信号质量。但它的生态位非常稳。调试口这一块几乎所有MCU都至少有一个UART外设电源板、工控板、路由器都留了console口。电平转换方面TTL、RS-232、RS-485、USB转串口四个方向都有成熟又廉价的方案。模块互连方面Wi-Fi、蓝牙、GPS、LoRa模块原生就给你UART透传接口那是它们的母语。再加上MODBUS、NMEA 0183、AT指令、SBUS、MAVLink这些上层协议都建立在UART之上它的生命力比你想象中强得多。我经常形容UART是底层最朴实的通信协议它不需要花哨只需要稳定地做那条“最后一百米”的接线员。5.3 什么时候用软件模拟UART什么时候老老实实用硬件硬件UART不够用是最常见的痛点一颗MCU只有2个UART你要接蓝牙、GPS、调试三个口这时候很多人会想“用GPIO模拟一个串口”。软件模拟UART就是拿GPIO手动控制电平翻转和采样。简单demo可以跑一个位时间用定时器翻转一次但要同时接收、还要处理中断嵌套CPU基本被吃干榨净极易丢字节。我的经验是串口数量不够时先看能不能复用——很多MCU的USART支持引脚重映射或者芯片本身带LPUART这种低功耗串口实在不行才用软件串口而且只给低速、只发不接收的场景用比如单纯打印状态。能用硬件解决的问题别用软件硬扛。6. 我调UART踩过的坑排查链路和防坑经验6.1 乱码的第一现场第一只逻辑分析仪第一个下降沿出现乱码我第一件事不是改代码是接逻辑分析仪。几十块钱的八通道逻辑分析仪就够用采样率设2M起步触发条件选下降沿。打开抓包你会看到清晰的波形空闲高电平、一个下降沿、若干位宽一致的高低方波。然后当场就能判断三件事波特率对不对把光标卡在一个位宽上1除以位宽就是实际波特率。极性反没反正常起始位是下降沿如果看到空闲电平是低、起始是上升沿就是极性反了。帧格式对不对数一下一帧里有多少位是8位数据还是7位停止位是1格还是2格一目了然。这一步能省下后面至少一个小时的瞎猜时间我强烈建议每个人手边都放一个。6.2 一步一步的故障排查清单手头没有逻辑分析仪的话按这个链路排查命中率很高接线顺序TX接对方的RX、RX接对方的TXGND必须共地。初学者把TX接TX、只接两根线、忘了共地是乱码和没反应的第一大来源。电平匹配TTL对TTL、RS-232对RS-232、RS-485对RS-485。3.3V和5V之间最好加电平转换或者选支持3.3V的USB转TTL小板。参数核对先固定115200-8N1双方逐项对照。模块默认波特率一定要去数据手册查别想当然不少传感器模块出厂是9600。串口工具设置确认选择了正确的COM口关闭流控。很多开发板的自动下载电路把DTR或RTS接在复位脚上你在串口助手里勾选了DTR/RTS板子可能一直被复位数据自然收不到。线材和供电劣质杜邦线、一米以上的飞线都会让波形恶化USB转串口板供电不足模块也可能半死不活。驱动层面设备管理器里没有COM口或者有黄色感叹号跳到下一节的回环测试。这张清单看着基础但我自己每年都会被其中至少一条坑一两次。尤其是DTR/RTS自动复位这种坑手册里不会白纸黑字写出来全靠实际踩过才知道。6.3 USB转串口驱动装不上回环测试帮你看清是芯片还是线的问题排查USB转串口板最快的方法是回环把小板的TX接到RX不接任何外部设备打开串口助手发一串字符如果接收区能看见自己发出去的内容说明USB芯片、驱动、内部通路都正常。回环通过了问题就在USB板引脚之外回环都收不到重点查驱动和COM口选择。很多自称“FT232R驱动安装失败”的案例其实就是Windows缓存了旧设备签名或者USB口供电不稳导致枚举异常。所以我不建议一开始就怀疑芯片坏了。先回环能排除一大堆干扰因素。回环测试时如果收到乱码优先怀疑波特率没选对其次怀疑杜邦线有干扰把它接到真实MCU之前先用这个最笨也最有效的方法确认链路是干净的。6.4 串口调试架构的小习惯最后分享一个我自己多年形成的习惯不要把串口当成一个临时的联调工具它值得进入你的架构设计里。我的做法是日志分级release版本关掉debug每一帧数据都自定义帧头、长度和CRCUART只负责搬运应用层可靠性由自己保障接收端用环形缓冲区要么DMA要么中断往里塞字节主循环再按帧解析绝不在中断回调里做复杂处理调试路径固定化回环测试和逻辑分析仪永远是验证串口健康度的第一手段。这套东西看似简单但能救命。多少次半夜调不通最后发现不是协议问题而是线松了、串口助手多勾了一个选项、或者板子供电不足。这时候一套标准的排查路径比任何高级技巧都管用。写完这篇我又想起刚开始做板子的那个晚上拿着USB转TTL折腾了一整晚最后发现只是忘了把GND连上去。UART就像这样明明很简单却总能以最朴素的方式教你敬畏物理层。这个系列的第一篇就这样吧下一篇我们顺着协议栈往上走看看更热闹的总线。