3. UART串口通信:从协议到实战
UART串口,说白了就是嵌入式世界里最基础的“对话通道”。不管你是调试、刷固件,还是跟传感器、GPS模块、蓝牙模块打交道,十有八九都得靠它。我做了这么多年嵌入式,可以负责任地告诉你——串口搞不定,后面基本没法玩。
这一章,咱们就把UART从协议到配置再到收发数据,一次讲透。
3.1 UART协议基础:别被“异步”吓到
UART全称是Universal Asynchronous Receiver/Transmitter,通用异步收发器。异步是什么意思?就是收发双方没有共同的时钟线,各用各的时钟。
你想想看,没有时钟怎么对齐数据?靠的是约定好的波特率。就像两个人约好“每秒说一个字”,你按这个节奏说,我按这个节奏听,只要节奏一致,就能听懂。
UART一帧数据的标准结构:
- 起始位:1位,逻辑低电平,告诉接收方“我要开始发了”
- 数据位:5~9位,常用8位,就是你要传的字节
- 校验位:可选,奇校验或偶校验,用来检错
- 停止位:1位或2位,逻辑高电平,表示这一帧结束
举个例子,你发一个字节0x55(二进制01010101),用8N1配置(8数据位、无校验、1停止位),波形就是:先拉低(起始位),然后依次送出0、1、0、1、0、1、0、1,最后拉高(停止位)。
我在项目中遇到过一个问题:两个设备波特率都设成了115200,但就是收不到正确数据。折腾了半天,发现一个设备用的是16MHz晶振,另一个是12MHz,分频后实际波特率差了3%左右。嗯,串口通信对时钟精度是有要求的,一般误差要小于2%。
| 参数 | 常见值 | 说明 |
|---|---|---|
| 波特率 | 9600, 115200, 921600 | 越高越快,但受限于线路和时钟精度 |
| 数据位 | 8 | 最常用,兼容ASCII和二进制数据 |
| 校验位 | 无(None) | 大多数场景不用,省一个bit |
| 停止位 | 1 | 够用,2位用于低速或噪声环境 |
3.2 ESP32的UART资源:三个硬件串口够用吗?
ESP32内置了3个UART控制器,编号UART0、UART1、UART2。每个都是独立的硬件模块,可以同时工作,互不干扰。
我个人的习惯是:
- UART0:留给日志输出和固件下载,默认引脚是GPIO1(TX)和GPIO3(RX)
- UART1:接外部设备,比如GPS模块、指纹模块
- UART2:接蓝牙模块或另一个传感器
但要注意,UART1的默认引脚(GPIO9和GPIO10)在大多数ESP32开发板上被占用了(连接了Flash)。所以用UART1时,一定要重新映射引脚。
引脚映射的坑:
ESP32的UART引脚可以通过GPIO矩阵任意映射,但不是所有引脚都能随便用。比如GPIO6~GPIO11是Flash接口引脚,别去占用。我曾经有个同事把UART2的TX接到了GPIO6上,结果程序死活跑不起来——Flash被干扰了。
3.3 ESP-IDF中UART配置:三步走
在ESP-IDF里配置UART,其实就三步:
- 配置参数:波特率、数据位、校验位、停止位、流控
- 设置引脚:把UART的TX、RX映射到具体的GPIO
- 安装驱动:分配缓冲区,启动UART
来看代码,我习惯用这种写法:
#include "driver/uart.h"
#include "driver/gpio.h"
// 定义UART参数
#define UART_PORT_NUM UART_NUM_2
#define UART_BAUD_RATE 115200
#define UART_TX_GPIO 17
#define UART_RX_GPIO 16
#define BUF_SIZE 1024
void uart_init(void)
{
// 第一步:配置参数
uart_config_t uart_config = {
.baud_rate = UART_BAUD_RATE,
.data_bits = UART_DATA_8_BITS,
.parity = UART_PARITY_DISABLE,
.stop_bits = UART_STOP_BITS_1,
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE,
.source_clk = UART_SCLK_APB,
};
uart_param_config(UART_PORT_NUM, &uart_config);
// 第二步:设置引脚
uart_set_pin(UART_PORT_NUM, UART_TX_GPIO, UART_RX_GPIO,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
// 第三步:安装驱动,分配收发缓冲区
uart_driver_install(UART_PORT_NUM, BUF_SIZE, BUF_SIZE, 0, NULL, 0);
}
这里有个细节:source_clk我用了UART_SCLK_APB,这是默认时钟源,频率80MHz。如果你对波特率精度要求特别高,可以考虑用UART_SCLK_RTC,但那个频率低,不支持高速率。
3.4 串口收发数据实战:别用阻塞方式
配置好了,怎么收发?很多人上来就用uart_write_bytes()和uart_read_bytes(),但这两个函数默认是阻塞的。你想想看,如果对方没发数据,你的程序就卡死在读取那里了。
我建议的做法是:
- 发送:用
uart_write_bytes()没问题,发送缓冲区满了才会阻塞 - 接收:用事件驱动方式,或者设置超时
来看一个实际收发示例:
// 发送数据
const char *data = "Hello ESP32!\r\n";
uart_write_bytes(UART_PORT_NUM, data, strlen(data));
// 接收数据(带超时)
uint8_t rx_buf[128];
int len = uart_read_bytes(UART_PORT_NUM, rx_buf, sizeof(rx_buf),
pdMS_TO_TICKS(100));
if (len > 0) {
// 处理收到的数据
rx_buf[len] = '\0';
printf("Received: %s", rx_buf);
}
但说实话,这种轮询方式在复杂项目里不够用。更好的做法是用UART事件队列:
// 安装驱动时传入事件队列句柄
QueueHandle_t uart_queue;
uart_driver_install(UART_PORT_NUM, BUF_SIZE, BUF_SIZE, 10, &uart_queue, 0);
// 在任务中循环处理事件
uart_event_t event;
while (1) {
if (xQueueReceive(uart_queue, &event, portMAX_DELAY)) {
switch (event.type) {
case UART_DATA:
// 有数据到达
uint8_t data[event.size];
uart_read_bytes(UART_PORT_NUM, data, event.size, 0);
// 处理数据...
break;
case UART_FIFO_OVF:
// FIFO溢出,清空缓冲区
uart_flush_input(UART_PORT_NUM);
break;
case UART_BUFFER_FULL:
// 缓冲区满了
uart_flush_input(UART_PORT_NUM);
break;
default:
break;
}
}
}
我的经验:
事件驱动方式虽然代码多几行,但可靠性高很多。我曾经在一个项目中用轮询方式接收GPS数据,结果偶尔丢包,换成事件队列后问题就解决了。说白了,中断+队列才是嵌入式收发的正确姿势。
3.5 UART通信知识体系
下面这张图把UART通信的核心流程串起来了,从协议到配置再到收发,一目了然:
3.6 常见问题与避坑指南
做UART通信,有几个坑我踩过不止一次,分享给你:
坑一:波特率误差
我曾经用ESP32的UART2去接一个GPS模块,波特率设成9600,但GPS模块用的是内部RC振荡器,精度一般。结果就是——能收到数据,但偶尔有乱码。后来我把ESP32的时钟源改成外部晶振,问题就解决了。
坑二:引脚冲突
UART1的默认引脚GPIO9和GPIO10,在大多数ESP32模组上被Flash占用了。如果你不重新映射,程序要么跑不起来,要么Flash读写异常。我建议UART1的TX/RX分别用GPIO4和GPIO5,这两个引脚比较安全。
坑三:缓冲区溢出
如果你用轮询方式接收,而对方发送速度很快,缓冲区很容易溢出。我习惯把接收缓冲区设大一点(至少256字节),同时用事件队列来通知数据到达。这样既不会丢数据,也不会占用CPU。
嗯,UART串口这块内容就这些。配置不难,但细节不少。你只要记住:参数配置要准、引脚映射要查、收发用事件驱动,基本就不会出大问题。