UART串口通信详解:从协议原理到STM32实战调试
2026/9/12 9:02:11 网站建设 项目流程

入行做嵌入式第一件事儿,不是点灯,是把串口调通。UART串口作为最基础的通信协议,几乎所有MCU、传感器模组、4G模块、蓝牙模块都带这个接口。哪怕你后面做USB、做以太网,调试阶段大概率还是要靠串口输出日志。这篇“通信协议详解(一)”就把UART从头讲透:协议层怎么定义的、一帧数据长什么样、从硬件到代码怎么落地,再附上我这些年联调踩过的坑。做单片机、物联网设备、上位机开发的朋友,这篇对你有用。

1. 先把UART这件事说透:它到底是个啥

1.1 串口、UART、RS-232这一堆名字,账得算清楚

刚入行那阵子,我经常被“串口”、“UART”、“RS-232”这几个词搞混,后来发现其实它们是三个层面的事。

UART(Universal Asynchronous Receiver/Transmitter)是通用异步收发器。它既是协议,也是芯片内部的一个硬件外设模块。说它是协议,因为它规定了数据怎么按位发出去;说它是硬件模块,是因为MCU里专门有一块电路负责这件事,你只需要往寄存器里写数据,它自己就把电平翻转、移位这些脏活干了。

串口(Serial Port)是一个更口语化的泛称,指所有串行通信接口。大家平时说“把这个串口接出来”、“串口调试助手上能看到数据”,指的就是UART串口。

RS-232是另一个层面的东西,它是EIA(电子工业联盟)定义的电气标准,管的是电压多少伏、用哪种接头。UART决定了数据怎么排布,RS-232决定了信号怎么在线缆上传输,一个是“格式”,一个是“物理通道”。

实际开发中,MCU引脚直接输出的是TTL电平的UART信号,电平范围是0到3.3V(或5V)。而老式电脑的DB9串口走的是RS-232电平,正负电压表示高低。两边直接怼上去肯定不行,所以要靠MAX232、CH340、FT232这类电平转换芯片桥接。现在大家用的USB转串口小模块,本质就是把USB信号转成TTL电平的UART,方便电脑和板子通信。

提示:买USB转串口模块时,注意看输出电压是3.3V还是5V,最好选带跳线切换的。电平选错了,轻则收不到数据,重则烧芯片。

1.2 三根线打天下:TX、RX、GND怎么接

UART通信最少只需要三根线:TX(发送)、RX(接收)、GND(地)。连接规则一句话:TX接对端的RX,RX接对端的TX,GND对接

这里很多人第一次会绕晕,其实想想就通了:A要向B发数据,A的TX必须连B的RX,这样B才能“听见”A在说什么。同理B的TX连A的RX。所以两个设备直接连的时候,TX和RX是交叉的。

GND为什么必须连?因为UART是单端信号,接收端判断电平高低是拿信号线和GND比出来的。如果不共地,A的3.3V高电平在B眼里可能是2.8V,可能判成不确定状态,通信直接乱套。板子之间距离近时,一根杜邦线就能解决;距离远了,还得考虑信号完整性和隔离问题。

由于TX和RX互相独立,UART天生就是全双工的:发数据和收数据可以同时进行。这一点比后面要讲的I2C、SPI更灵活。实际项目里,UART还经常做半双工应用,比如RS-485总线,那就需要额外控制收发方向了。

1.3 TTL电平、RS-232电平、RS-485:别把电压搞混

我见过不少朋友把设备连上却没反应,最后发现是电平标准不匹配。

TTL电平下的UART:

  • 逻辑1:2.4V到VCC(通常3.3V或5V)
  • 逻辑0:0V到0.4V
  • 注意TTL是正逻辑,高电平表示1

RS-232电平下:

  • 逻辑1:-15V到-3V
  • 逻辑0:+3V到+15V

看出来了吗?RS-232是反着的,而且电压范围大得多,所以能传更远的距离(十五米左右),代价是电路复杂、需要专门的收发芯片。单片机开发板上引出的串口,绝大部分是TTL电平,接模块、接传感器时用的就是TTL UART。接电脑的DB9串口、接老式工控机时才需要RS-232电平转换。

还有一种是RS-485,用差分信号传输,A、B两根线,抗干扰能力强,能传上千米,常出现在工业现场总线里。RS-485本质上也可以看作UART的一个“物理层变种”,数据格式照旧,只是由两根差分线代替了单端TX/RX。前面提到的热词里就有CAN、EtherCAT这些工业总线,它们跟UART不冲突,各有各的用武之地。

2. 数据格式拆解:一个字节是怎么飞出去的

2.1 一帧数据的长相:起始位、数据位、校验位、停止位

UART是异步通信,收发双方没有一个独立的时钟线。那接收端怎么知道对方什么时候开始发数据?靠的就是帧结构里的起始位

空闲状态下,TX线保持高电平。发送端要发数据时,先拉低一个位时间,这个下降沿就是给接收方的“注意,我开始发了”信号,叫起始位。接收方检测到起始位后,按约定的波特率每隔一个位时间采样一次,依次读出数据位。

标准的一帧UART数据由这几部分组成:

帧的部分位长度电平状态作用
起始位1位低电平告知接收方开始传输
数据位5~9位,常用8位按数据内容变化实际有效载荷
校验位0或1位视校验方式而定简单的错误检测
停止位0.5~2位,常用1位高电平告知接收方一帧结束

数据位低位先发,也就是LSB First。比如发送十六进制0x55(二进制01010101),线上先出现的是最低位1,然后依次是0、1、0、1、0、1、0。很多初学者拿逻辑分析仪抓波形,一看数据跟心里想的不一样,多半是没注意LSB先行这个规矩。

常见的参数组合是“8-N-1”,意思是8位数据、无校验、1位停止位。此外还有8-E-1(偶校验)、8-O-1(奇校验)、7-E-1(7位数据+偶校验)等。如果没有特别原因,用8-N-1就行。

2.2 波特率是怎么算出来的,误差多少会翻车

波特率定义为一秒钟传输的码元数,单位是Baud(波特)。在UART里,一个码元就是一个位,所以波特率数值上等于比特率(bps)。常见的有9600、19200、38400、57600、115200。115200是很多模块默认值,速度快,且大多数时钟频率下计算误差小。

波特率直接决定每一位的“时间宽度”。比如9600波特率下,一个位的时间是:

位时间 = 1 / 9600 ≈ 104.17微秒

双方约定好波特率后,接收方就在起始位之后的每个位时间中点采样一次,这样能最大程度避开信号翻转边沿的抖动区间。

但有个现实问题:MCU的时钟频率不是随便就能整除目标波特率的。以STM32F103C8T6为例,USART1挂在APB2总线上,时钟72MHz。波特率寄存器BRR的值这样算:

根据参考手册,USARTDIV = 时钟频率 / (16 × 目标波特率)

代入72MHz和115200:

USARTDIV = 72000000 / (16 × 115200) = 39.0625

写入BRR时要分成整数部分和小数部分。整数部分39(0x27),小数部分用0.0625 × 16 = 1。所以BRR = 0x271。此时实际波特率:

实际波特率 = 72000000 / (16 × 39.0625) = 115200,误差为0%

完美的整除。但很多频率组合没那么幸运,比如8MHz的时钟跑9600波特率,USARTDIV = 8000000 / (16 × 9600) = 52.08左右,舍入后会有千分之几的误差。只要误差在±2%以内,通常都没问题。

注意:比波特率计算更坑的是时钟源配置错误。比如STM32F103内部RC振荡器(HSI)精度约±2%,如果软件里没切换外部晶振就跑串口,波特率会整体偏移。单片机刚上电用HSI跑时,电脑端可能收到一堆乱码,一旦换成HSE就恢复正常,别问我是怎么知道的。

2.3 校验位是防呆不防傻:奇偶校验到底有没有用

奇偶校验的原理很简单:发送方统计数据位里1的个数,如果是偶校验,就设置校验位让“数据位+校验位”中1的总数为偶数;奇校验则让总数为奇数。接收方收到后同样统计,如果对不上,就说明这一帧有误。

但这里必须说句实话:奇偶校验只能检测奇数个位的翻转,偶数个位翻转时校验结果不变,检查不出来。而且它不能定位是哪一位错了,没有纠错能力。所以别把它当成网络安全协议里的CRC。

实际项目里,TTL短距离通信(板内、板间几十厘米),噪声干扰小,一般直接不开校验,参数就是8-N-1。长线传输、工业环境,有条件上CRC更好,比如Modbus RTU用CRC16。如果协议强制要求带校验位,那就按协议来,比如很多老式仪器会用到7-E-1。

3. 从原理到落地:一套能直接用的收发设计

3.1 硬件侧要留意的几个点

设计阶段就把下面几个事确认好,能省掉后面大量联调时间。

第一,MCU的UART外设数量够不够用。有些项目要同时挂4G模块、GPS模块、蓝牙模块,每个模块各占一路UART,选型时就要数清楚。MCU引脚的复用功能(AF)也要查仔细,比如STM32的USART1可以用PA9/PA10,也可以用PB6/PB7,但两者不能同时启用。

第二,串口线上的上拉电阻。UART空闲电平是高电平,如果MCU引脚内部有上拉,通常够用。但对外接模块的板子,建议外部再加4.7kΩ到10kΩ上拉,增强抗干扰能力。如果走RS-485,A/B线还要加终端匹配电阻(通常120Ω),那是另一个话题了。

第三,USB转串口模块的选型。日常调试用CH340就够了,便宜大碗,Windows驱动也好装。FT232更稳定一些,适合固件下载失败率高或者需要长期高负载传输的场景。热词里有人搜FT231X、FT232R驱动装不上,多半是驱动版本和系统不匹配,去原厂官网下载最新驱动基本能解决。

第四,串口保护。如果板子要引出到外部,比如接工控设备、接户外传感器,最好加TVS管或ESD保护器件。我有一块板子就是没加保护,插拔几次串口线之后UART引脚就挂了。

3.2 STM32上跑通一轮收发:HAL库实操

现在用STM32CubeMX生成工程的打法很主流,我以STM32F103C8T6加HAL库为例,讲一下UART初始化和收发。使用CubeMX时需要先配置RCC、SYS的Debug Serial Wire,给USART1选择异步模式(Asynchronous),设置波特率115200、8位数据、无校验、1位停止位。

生成代码后,核心的初始化函数是这样:

static void MX_USART1_UART_Init(void) { huart1.Instance = USART1; huart1.Init.BaudRate = 115200; huart1.Init.WordLength = UART_WORDLENGTH_8B; huart1.Init.StopBits = UART_STOPBITS_1; huart1.Init.Parity = UART_PARITY_NONE; huart1.Init.Mode = UART_MODE_TX_RX; huart1.Init.HwFlowCtl = UART_HWCONTROL_NONE; huart1.Init.OverSampling = UART_OVERSAMPLING_16; if (HAL_UART_Init(&huart1) != HAL_OK) { Error_Handler(); } }

发送数据最简单的方式是阻塞轮询:

HAL_UART_Transmit(&huart1, (uint8_t *)"Hello UART\r\n", 13, 1000);

最后一个参数是超时时间,单位毫秒。阻塞模式下函数会一直等发送完成,CPU被占着,尤其在大批量日志输出时会有性能问题。

接收数据如果也用阻塞模式,在RTOS里会把任务卡死,所以实战中更常用中断方式。每次收一个字节,进一次回调:

uint8_t rx_byte; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart->Instance == USART1) { /* 把数据塞入环形缓冲区 */ ring_buf_put(&rx_buf, rx_byte); /* 重新开启下一次接收 */ HAL_UART_Receive_IT(&huart1, &rx_byte, 1); } }

注意HAL_UART_Receive_IT每次只能触发一次接收,收到一个字节就会进回调,所以必须在回调里再次调用,否则收了一个字节后就停了。

有了环形缓冲区,应用层就能随时取数据,不用担心中断里面处理太多事情影响实时性。环形缓冲区实现不复杂,我用的是这个结构体:

typedef struct { uint8_t buffer[512]; volatile uint16_t head; volatile uint16_t tail; } ring_buf_t;

head是写入位置,tail是读取位置,当两者相等时缓冲区为空。读的时候检查是否为空,写的时候检查是否满了,满了就把旧数据丢掉或者置一个溢出标志。

3.3 阻塞、中断、DMA到底怎么选

这是UART设计里最实际的一个问题。我把三种模式放在一起对比:

模式CPU占用实时性适合场景
阻塞轮询初始化日志、简单指令应答
中断收发较好一般业务交互、不定长协议
DMA收发极低大数据量传输、高波特率日志

阻塞模式在裸机项目里最省事,但遇到需要同时处理多个任务时就不行了。中断模式是大多数项目的默认选择,配合环形缓冲区足够应付日常通信。DMA模式适合高速率大数据,比如一次发几KB的数据,用中断模式CPU要频繁进出中断,效率低,而DMA可以让外设直接读写内存,CPU完全不用管。

DMA还有一招很实用:DMA加串口空闲中断实现不定长接收。很多模块的数据帧长度不固定,比如AT指令的响应有长有短。做法是开启UART的DMA接收和IDLE中断,当总线空闲时触发一次空闲中断,此时从DMA缓冲区里读走当前收到的全部数据。HAL库里可以直接用HAL_UARTEx_ReceiveToIdle_DMA,省去自己写寄存器。

我在一个4G模块透传项目里用这套方案,代码逻辑大概是:

HAL_UARTEx_ReceiveToIdle_DMA(&huart1, dma_buf, sizeof(dma_buf)); /* 在空闲中断回调里处理收到的不定长数据 */

这个用法的关键是从DMA缓冲区判断实际收到多少字节,不要在空闲中断里做耗时操作,只做数据搬移和标志置位。

4. 实战中一定会遇到的坑和排查套路

4.1 乱码先别急,按这个顺序查

串口调试最头大的就是打开串口助手收到一堆“烫烫烫”或者乱码。我的排查顺序始终是固定的。

第一步查波特率。双方是不是都配置成115200了。有的模块默认是9600,有的默认115200,接上之前先看数据手册。

第二步查时钟源。板子用的是外部晶振还是内部RC振荡器?外部晶振频率跟配置是否一致?有些STM32最小系统板板载了8MHz晶振,但CubeMX里如果配置成其他频率,UART波特率也会跟着偏。

第三步查电平匹配。是不是把TTL设备接到了RS-232设备上,中间少了转换芯片?或者USB转串口模块输出是5V,直接接到了3.3V的MCU引脚上,长期看有风险甚至烧坏引脚。

第四步查GND。我之前接一块传感器板,怎么调都是乱码,最后发现是只接了两根信号线,忘了共地。两个设备电位基准都不同,数据自然对不上。

如果按这个顺序查完还乱码,掏出逻辑分析仪抓一下波形,对比实际位宽和理论值。这一招能直接定位是发送端的问题还是接收端的问题。

4.2 丢数据、卡死,多半是缓冲区的问题

数据丢失和程序卡在UART中断里,是两块最让人头疼的问题。

先说丢数据。中断接收模式下,如果中断里处理时间太长,下一个字节到达时就没法及时响应,硬件接收寄存器会被覆盖。解决办法就是前面提到的环形缓冲区:中断里只做最简单的字节入队,解析和业务处理放到主循环或RTOS任务里。

再说Linux下的情况。热词里就有“linux从串口接收数据丢失”,这个坑我也踩过。Linux下使用串口时,如果不做配置,默认的termios参数下串口驱动有自己的一套行规则,比如会对输入做回显、按行缓冲、特殊字符处理。实际开发中需要设置原始模式(raw mode),关掉ICANON、ECHO、ISIG这些选项。关键的两个参数:

newtio.c_cc[VMIN] = 1; /* 最少读取1个字节才返回 */ newtio.c_cc[VTIME] = 0; /* 不设超时 */

VMIN设置一次read()操作至少返回所需的字节数,VTIME设置等待时间。如果VMIN为0,read会立即返回当前缓冲区里的数据,可能造成粘包;如果只设VTIME不设VMIN,在高波特率下没等数据来齐就超时,就会出现丢数据的假象。把这些配置对了,Linux下串口收发就稳定了。

嵌入式端还有一个常见的卡死原因:中断里调用了printf或其他库函数。printf本身耗时且可能不可重入,一旦在中断上下文里调用,轻则数据错乱,重则直接进HardFault。我的习惯是所有日志输出都通过一个专用的日志发送函数,内部用DMA异步发送,never在中断里直接printf。

4.3 驱动装不上、烧写失败这些破事

热词里很多人搜CH340串口驱动、FT232R驱动下载,还有串口烧写失败。驱动问题有个通用解法:不要用驱动精灵这种第三方工具,直接去芯片官网下载对应系统的驱动,Windows和macOS的安装包不一样。CH340芯片的驱动在沁恒官网,FT232在FTDI官网。

装好驱动之后,设备管理器里能看到COM口号。如果插上没反应,依次检查:

  • USB线是不是只能充电不能传数据
  • 是不是用了USB集线器导致的供电不足,直接插电脑USB口试试
  • 电脑上是否同时装了几个USB转串口驱动,导致设备识别到了错误驱动上

烧写失败除了驱动问题,还有一个经常被忽略的点:BOOT0引脚的电平状态。STM32上电时BOOT0为低电平时从Flash启动,正常跑程序;如果你想用串口烧录,需要把BOOT0拉高再复位,烧完再拉回来。很多“串口烧写失败”其实是忘了这个步骤。

还有一点,烧写软件和串口助手不要同时占用同一个串口。哪怕只是打开串口助手挂着不操作,也会独占串口导致烧写工具打不开。

4.4 一个能救命的小工具:逻辑分析仪

排查UART问题时,我最推荐的工具不是示波器,而是USB逻辑分析仪。便宜的几十块钱,采样率24MHz够用,能看到完整的帧结构。

抓波形时怎么定位问题?先看起始位下降沿是否存在,再看每个位的时间宽度是否符合波特率要求。比如9600波特率下,一个位应该是104微秒左右,如果你抓到的位宽是130微秒,说明发送端的时钟偏了,问题在单片机时钟配置上。这种方式比看十六进制数据直观多了,能一眼分清是“数据错”还是“时序错”。

写在后面

做嵌入式这些年,UART是我打交道最多的接口,也是让我栽过最多跟头的接口。最初的一块板子,就是因为晶振虚焊导致串口一直乱码,查了整整两天才找到原因。后来我学乖了,凡是串口问题,一定先抓波形,而不是盲目改波特率。

一个小经验分享给各位:新板子第一次上电时,先用串口发一个0x55或者0xAA这样的固定字节,逻辑分析仪抓一下波形,确认位宽和帧格式都对,再跑正式的业务代码。这就像打地基,地基稳了,后面再复杂的协议栈都不慌。

UART理解透了,后面再聊I2C、SPI会顺畅很多,因为很多底层思维是相通的,比如时钟极性和相位、从机地址、总线仲裁这些概念都是对比着学。下一篇通信协议详解,我会结合I2C和实际项目来讲,到时候再聊。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询