4. UART串口通信:从原理到实战
各位同学,今天我们来聊聊嵌入式通信里最基础、也最常用的一个接口——UART。说白了,就是串口。我做了这么多年物联网项目,几乎每个设备调试都离不开它。你想想看,没有屏幕、没有键盘的单片机,怎么跟它交流?串口就是那个最直接的“对话窗口”。
4.1 UART协议原理:其实没那么复杂
UART的全称是Universal Asynchronous Receiver/Transmitter,通用异步收发器。注意“异步”这两个字——它没有单独的时钟线,全靠双方约定好速度来通信。
基本通信方式
- 全双工:可以同时发送和接收,用两根数据线(TX和RX)
- 异步:不需要时钟线,靠起始位和停止位同步
- 帧格式:起始位(1位) + 数据位(5~9位) + 校验位(可选) + 停止位(1~2位)
我记得刚入行时,有个老工程师跟我说:“串口通信就像两个人打电话,你先说‘喂’(起始位),然后说内容(数据位),最后说‘再见’(停止位)。”这个比喻我一直记到现在。
波特率是什么?
波特率就是每秒传输的符号数。常用的有9600、115200等。双方必须设置成一样的,否则收到的全是乱码。我在项目中遇到过好几次,明明代码没问题,但串口助手显示乱码——最后发现是波特率设错了。嗯,这种低级错误,犯过一次就记住了。
关键参数总结:
| 参数 | 常见值 | 说明 |
|---|---|---|
| 波特率 | 9600, 115200 | 速度越快,抗干扰能力越弱 |
| 数据位 | 8 | 最常用,一帧传一个字节 |
| 校验位 | 无(None) | 简单场景不用,重要数据用奇/偶校验 |
| 停止位 | 1 | 1位足够,2位用于低速或噪声环境 |
4.2 ESP32的UART配置:三步搞定
ESP32有3个UART接口,编号从0到2。其中UART0默认连接到了USB转串口芯片,所以我们用电脑连ESP32开发板时,用的就是UART0。
配置步骤:
- 初始化UART驱动:设置波特率、数据位、停止位等
- 设置引脚:把UART的TX/RX映射到GPIO
- 安装驱动:启动UART,准备收发数据
我个人习惯用ESP-IDF的UART驱动库,代码清晰,不容易出错。下面是一个典型的配置示例:
#include "driver/uart.h"
#include "driver/gpio.h"
// 定义UART参数
#define UART_PORT_NUM UART_NUM_1
#define UART_BAUD_RATE 115200
#define UART_TX_GPIO 17
#define UART_RX_GPIO 16
#define BUF_SIZE 1024
void uart_init(void) {
// 1. 配置UART参数
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,
};
// 2. 应用配置
uart_param_config(UART_PORT_NUM, &uart_config);
// 3. 设置引脚
uart_set_pin(UART_PORT_NUM, UART_TX_GPIO, UART_RX_GPIO,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
// 4. 安装驱动
uart_driver_install(UART_PORT_NUM, BUF_SIZE, 0, 0, NULL, 0);
printf("UART初始化完成!波特率:%d\n", UART_BAUD_RATE);
}
小技巧:调试阶段建议把波特率设成115200,速度快,响应及时。量产时如果线路较长,可以降到9600,更稳定。
4.3 与PC串口助手通信:让数据“看得见”
配置好UART后,第一件事就是跟电脑上的串口助手“握手”。我常用的工具有SSCOM、Putty,还有ESP-IDF自带的monitor。
发送数据到PC:
// 发送字符串
const char *msg = "Hello from ESP32!\r\n";
uart_write_bytes(UART_PORT_NUM, msg, strlen(msg));
// 发送二进制数据
uint8_t data[] = {0x01, 0x02, 0x03, 0xAA, 0xBB};
uart_write_bytes(UART_PORT_NUM, (const char *)data, sizeof(data));
接收PC发来的数据:
uint8_t rx_buf[BUF_SIZE];
int len = uart_read_bytes(UART_PORT_NUM, rx_buf, BUF_SIZE, pdMS_TO_TICKS(100));
if (len > 0) {
printf("收到 %d 字节:", len);
for (int i = 0; i < len; i++) {
printf("0x%02X ", rx_buf[i]);
}
printf("\n");
}
我曾经调试一个智能插座项目,发现设备偶尔会死机。用串口助手一抓,发现是上位机发来的数据包长度不对,导致缓冲区溢出。从那以后,我每次写UART接收代码,都会加长度校验和超时处理。
注意:ESP32的UART接收是异步的,不要在主循环里死等数据。用FreeRTOS的任务或事件组来处理,效率高得多。
4.4 自定义通信协议帧格式设计:别让数据“裸奔”
直接发原始数据,就像在大街上喊电话号码——谁都能听。实际项目中,我们需要一套“暗号”,也就是自定义协议帧。
为什么需要协议帧?
- 数据完整性:防止丢包、错位
- 多命令支持:一个串口处理多种指令
- 错误检测:发现并丢弃损坏的数据
我常用的帧格式:
| 帧头(2字节) | 长度(1字节) | 命令(1字节) | 数据(N字节) | 校验(1字节) | 帧尾(1字节) |
| 0xAA 0x55 | N+1 | CMD | payload | XOR校验 | 0x0D |
解释一下各字段的作用:
- 帧头:0xAA 0x55,用来同步,检测一帧的开始
- 长度:从命令到校验的总字节数,方便接收方知道要收多少
- 命令:比如0x01表示读电表,0x02表示写参数
- 数据:实际要传的内容
- 校验:我用简单的XOR校验,把所有字段(不含帧头帧尾)异或一遍
- 帧尾:0x0D(回车符),表示帧结束
发送示例代码:
void send_frame(uint8_t cmd, uint8_t *data, uint8_t data_len) {
uint8_t frame[256];
uint8_t index = 0;
uint8_t checksum = 0;
// 帧头
frame[index++] = 0xAA;
frame[index++] = 0x55;
// 长度(命令+数据+校验)
frame[index++] = 1 + data_len + 1;
// 命令
frame[index++] = cmd;
checksum ^= cmd;
// 数据
for (int i = 0; i < data_len; i++) {
frame[index++] = data[i];
checksum ^= data[i];
}
// 校验
frame[index++] = checksum;
// 帧尾
frame[index++] = 0x0D;
// 发送
uart_write_bytes(UART_PORT_NUM, (const char *)frame, index);
}
接收解析示例:
// 状态机解析帧
typedef enum {
WAIT_HEADER1,
WAIT_HEADER2,
WAIT_LENGTH,
WAIT_CMD,
WAIT_DATA,
WAIT_CHECKSUM,
WAIT_FOOTER
} frame_state_t;
frame_state_t state = WAIT_HEADER1;
uint8_t rx_frame[256];
uint8_t rx_index = 0;
uint8_t expected_len = 0;
void parse_byte(uint8_t byte) {
switch (state) {
case WAIT_HEADER1:
if (byte == 0xAA) state = WAIT_HEADER2;
break;
case WAIT_HEADER2:
if (byte == 0x55) {
state = WAIT_LENGTH;
rx_index = 0;
} else {
state = WAIT_HEADER1; // 重新同步
}
break;
case WAIT_LENGTH:
expected_len = byte;
state = WAIT_CMD;
break;
case WAIT_CMD:
rx_frame[rx_index++] = byte;
state = WAIT_DATA;
break;
case WAIT_DATA:
rx_frame[rx_index++] = byte;
if (rx_index >= expected_len - 1) { // 减掉校验字节
state = WAIT_CHECKSUM;
}
break;
case WAIT_CHECKSUM:
// 校验计算(略)
state = WAIT_FOOTER;
break;
case WAIT_FOOTER:
if (byte == 0x0D) {
// 一帧完整接收,处理数据
process_frame(rx_frame, rx_index);
}
state = WAIT_HEADER1;
break;
}
}
避坑指南:我曾经在协议里没加帧尾,结果数据里偶然出现0xAA 0x55就导致误同步。后来加了帧尾和长度校验,问题彻底解决。记住:协议设计时,宁可多花几个字节,也要保证可靠性。
小结
UART串口通信,说难不难,说简单也不简单。核心就三点:参数配好、收发对路、协议可靠。ESP32的UART驱动很成熟,你只要照着上面的代码改改引脚和波特率,几分钟就能跑起来。
下一章我们会讲蓝牙通信,到时候UART会作为蓝牙模块的控制接口。所以这一章的基础一定要打牢。有问题随时在群里问我,我看到就会回。
课后练习:
- 用ESP32向串口助手每秒发送一次“Hello UART!”
- 设计一个协议帧,包含温度、湿度两个数据(各2字节),并实现收发解析
- 测试不同波特率下(9600 vs 115200)的通信稳定性
公众号:蓝海资料掘金营,微信deep3321