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 知识体系总览
下面这张图把本章的核心知识点串起来了,你可以对照着回顾一下:
3.6 避坑与经验总结
最后,分享几个我这些年踩过的坑:
- 波特率偏差:两个设备波特率必须一致,且误差要小。用示波器看波形最准。
- 电平匹配:ESP32是3.3V电平,如果接5V的传感器,需要加电平转换芯片,否则会烧引脚。
- 共地问题:两个设备通信时,GND必须连在一起。我见过有人只连TX/RX,结果通信时好时坏。
- 缓冲区溢出:接收数据要快,尤其是高速率下。如果处理不过来,就加大缓冲区或者用DMA。
- 协议健壮性:帧头帧尾要选好,校验不能省。我曾经省了校验,结果现场电磁干扰导致数据错乱,排查了三天。