3. UART串口通信:从原理到实战

UART串口,说实在的,是嵌入式开发里最基础也最常用的通信方式。我做了十几年硬件,几乎每个项目都离不开它。调试时打印日志、跟传感器交换数据、甚至给模块升级固件,串口都是那个最可靠的“老伙计”。

这一章,咱们就把UART从原理到ESP32上的实战操作,掰开揉碎了讲清楚。

3.1 UART协议原理:为什么它这么简单又这么实用?

UART的全称是Universal Asynchronous Receiver/Transmitter,通用异步收发器。它只需要两根数据线——TX(发送)和RX(接收),就能实现两个设备之间的全双工通信。

你想想看,两根线就能双向传数据,多省引脚。这也是为什么几乎所有MCU、传感器模块、GPS模块、蓝牙模块都标配UART接口。

核心要点:UART是异步通信,没有时钟线。收发双方靠约定的波特率来同步。波特率就是每秒传输的比特数,常见的有9600、115200等。

UART一帧数据的格式是这样的:

  • 起始位(1位):拉低电平,告诉接收方“我要开始发了”
  • 数据位(5~9位):实际传输的数据,通常用8位
  • 校验位(可选):奇校验或偶校验,用于简单检错
  • 停止位(1或2位):拉高电平,表示一帧结束

举个例子,发送一个字节0x55(二进制01010101),用8N1格式(8数据位、无校验、1停止位),波形就是:先拉低1位起始位,然后依次发送8位数据(从LSB开始),最后拉高1位停止位。

我在项目中遇到过一个问题:两个设备波特率都设成了115200,但通信就是乱码。查了半天才发现,其中一个设备的晶振精度不够,实际波特率偏差超过了2%。UART对时钟精度是有要求的,偏差太大就会丢数据。

避坑指南:我曾经因为忽略波特率误差,导致一批产品在现场频繁通信失败。后来强制要求所有串口通信的波特率误差必须控制在±1.5%以内。尤其是用内部RC振荡器的芯片,一定要实测校准。

3.2 ESP32的多串口配置:不止一个UART

ESP32一共有3个UART控制器,分别对应UART0、UART1、UART2。每个控制器都可以独立配置波特率、数据位、停止位、校验位,还能任意映射到GPIO引脚。

这一点非常实用。比如UART0默认的TX/RX是GPIO1/GPIO3,但如果你要用这些引脚做其他功能,完全可以把UART0映射到别的引脚上。

UART控制器 默认TX引脚 默认RX引脚 备注
UART0 GPIO1 GPIO3 通常用于下载和调试
UART1 GPIO10 GPIO9 可自由映射
UART2 GPIO17 GPIO16 可自由映射

配置UART的步骤其实很简单,我习惯用ESP-IDF的UART驱动库,几行代码就能搞定:

#include "driver/uart.h"
#include "driver/gpio.h"

// 配置参数
uart_config_t uart_config = {
    .baud_rate = 115200,
    .data_bits = UART_DATA_8_BITS,
    .parity = UART_PARITY_DISABLE,
    .stop_bits = UART_STOP_BITS_1,
    .flow_ctrl = UART_HW_FLOWCTRL_DISABLE
};

// 安装驱动
uart_param_config(UART_NUM_2, &uart_config);
uart_set_pin(UART_NUM_2, GPIO_NUM_17, GPIO_NUM_16, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
uart_driver_install(UART_NUM_2, 1024, 0, 0, NULL, 0);

嗯,这里要注意:uart_driver_install的第二个参数是接收缓冲区大小。如果数据量大,建议设大一点,比如2048或4096,防止丢包。

3.3 与PC/传感器串口通信:收发数据

配置好UART之后,收发数据就简单了。发送用uart_write_bytes,接收用uart_read_bytes。

跟PC通信时,我一般用115200波特率,8N1格式。PC端用串口助手或者我自己写的Python脚本,调试起来很方便。

跟传感器通信时,情况就复杂一些。很多传感器模块(比如GPS、温湿度、PM2.5)都是主动上报数据,你需要不断读取。我建议用中断或任务方式接收,不要在主循环里轮询,否则容易丢数据。

// 发送数据
const char *data = "Hello ESP32!\r\n";
uart_write_bytes(UART_NUM_2, data, strlen(data));

// 接收数据
uint8_t buf[128];
int len = uart_read_bytes(UART_NUM_2, buf, sizeof(buf), pdMS_TO_TICKS(100));
if (len > 0) {
    // 处理接收到的数据
}

个人经验:接收数据时,我习惯用环形缓冲区(ring buffer)来缓存。ESP-IDF的UART驱动内部已经做了缓冲,但如果你自己实现协议解析,建议再加一层应用层缓冲区,防止数据被覆盖。

3.4 自定义通信协议设计:让数据“说人话”

裸的UART传输只是字节流,怎么让这些字节有意义?这就需要通信协议了。

我设计过很多种自定义协议,最常用的是“帧头+长度+数据+校验+帧尾”的结构。举个例子:

字段 长度(字节) 说明
帧头 2 固定值0xAA 0x55
长度 1 数据域的长度
命令 1 0x01读数据,0x02写数据等
数据 N 实际数据内容
校验 1 累加和或CRC
帧尾 1 固定值0x0D

为什么要有帧头和帧尾?说白了就是为了“找边界”。接收方收到字节流后,先找帧头,再根据长度字段提取完整的一帧,最后校验。如果校验通过,就解析数据;如果失败,就丢弃这一帧,继续找下一帧。

我曾经在一个项目中,用这种协议跟20多个传感器节点通信。每个节点地址不同,主机轮询时带上地址,只有地址匹配的节点才响应。这样一根串口线就能挂多个设备,省了不少线缆成本。

协议设计原则:

  • 帧头要选不常用的字节,减少误判
  • 长度字段要包含自身,方便解析
  • 校验算法要简单高效,累加和适合资源受限的MCU,CRC适合对可靠性要求高的场景
  • 超时机制必须有,防止接收卡死

3.5 知识体系总览

下面这张图把本章的核心知识点串起来了,你可以对照着回顾一下:

UART串口通信知识体系 UART串口通信知识体系 UART串口通信 协议原理 异步通信,无时钟线 波特率同步 起始位+数据位+停止位 ESP32多串口 3个UART控制器 引脚可任意映射 独立配置参数 通信实战 与PC串口助手通信 与传感器模块通信 中断/任务方式接收 自定义通信协议设计 帧头+长度+命令+数据+校验+帧尾 多节点轮询通信 超时机制与容错处理 核心:理解异步通信原理 → 掌握ESP32多串口配置 → 实战收发数据 → 设计可靠的自定义协议

3.6 避坑与经验总结

最后,分享几个我这些年踩过的坑:

  • 波特率偏差:两个设备波特率必须一致,且误差要小。用示波器看波形最准。
  • 电平匹配:ESP32是3.3V电平,如果接5V的传感器,需要加电平转换芯片,否则会烧引脚。
  • 共地问题:两个设备通信时,GND必须连在一起。我见过有人只连TX/RX,结果通信时好时坏。
  • 缓冲区溢出:接收数据要快,尤其是高速率下。如果处理不过来,就加大缓冲区或者用DMA。
  • 协议健壮性:帧头帧尾要选好,校验不能省。我曾经省了校验,结果现场电磁干扰导致数据错乱,排查了三天。