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

各位同学,今天我们来聊聊嵌入式通信里最基础、也最常用的一个接口——UART。说白了,就是串口。我做了这么多年物联网项目,几乎每个设备调试都离不开它。你想想看,没有屏幕、没有键盘的单片机,怎么跟它交流?串口就是那个最直接的“对话窗口”。

4.1 UART协议原理:其实没那么复杂

UART的全称是Universal Asynchronous Receiver/Transmitter,通用异步收发器。注意“异步”这两个字——它没有单独的时钟线,全靠双方约定好速度来通信。

基本通信方式

  • 全双工:可以同时发送和接收,用两根数据线(TX和RX)
  • 异步:不需要时钟线,靠起始位和停止位同步
  • 帧格式:起始位(1位) + 数据位(5~9位) + 校验位(可选) + 停止位(1~2位)

我记得刚入行时,有个老工程师跟我说:“串口通信就像两个人打电话,你先说‘喂’(起始位),然后说内容(数据位),最后说‘再见’(停止位)。”这个比喻我一直记到现在。

波特率是什么?

波特率就是每秒传输的符号数。常用的有9600、115200等。双方必须设置成一样的,否则收到的全是乱码。我在项目中遇到过好几次,明明代码没问题,但串口助手显示乱码——最后发现是波特率设错了。嗯,这种低级错误,犯过一次就记住了。

关键参数总结:

参数常见值说明
波特率9600, 115200速度越快,抗干扰能力越弱
数据位8最常用,一帧传一个字节
校验位无(None)简单场景不用,重要数据用奇/偶校验
停止位11位足够,2位用于低速或噪声环境

4.2 ESP32的UART配置:三步搞定

ESP32有3个UART接口,编号从0到2。其中UART0默认连接到了USB转串口芯片,所以我们用电脑连ESP32开发板时,用的就是UART0。

配置步骤:

  1. 初始化UART驱动:设置波特率、数据位、停止位等
  2. 设置引脚:把UART的TX/RX映射到GPIO
  3. 安装驱动:启动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会作为蓝牙模块的控制接口。所以这一章的基础一定要打牢。有问题随时在群里问我,我看到就会回。

课后练习:

  1. 用ESP32向串口助手每秒发送一次“Hello UART!”
  2. 设计一个协议帧,包含温度、湿度两个数据(各2字节),并实现收发解析
  3. 测试不同波特率下(9600 vs 115200)的通信稳定性

公众号:蓝海资料掘金营,微信deep3321