3、UART串口通信:从协议基础到ESP32实战

UART串口,说白了就是嵌入式开发者的「生命线」。你想想看,从最初的调试信息输出,到与传感器、GPS模块、蓝牙模块通信,哪样离得开它?我个人习惯,拿到一块新板子,第一件事就是先把串口调通——串口通了,心就定了。

这一章,咱们就彻底把UART在ESP32上的玩法讲透。从协议基础到多串口配置,再到环形缓冲区的实现,最后和PC上位机联调。嗯,都是实战中每天都要用的东西。

3.1 UART协议基础:你真的懂串口吗?

UART(Universal Asynchronous Receiver/Transmitter),通用异步收发器。异步,意味着没有时钟线,靠双方约定好的波特率来同步。

一个完整的UART数据帧长这样:

起始位 数据位(5~8位) 校验位(可选) 停止位(1/1.5/2位)
1位,低电平 LSB先发 奇/偶/无 高电平

最常用的配置是:115200波特率、8数据位、无校验、1停止位,简写为115200-8-N-1。我在项目中遇到过有人把波特率配错成9600,结果上位机死活收不到正确数据,排查了半天才发现是波特率不匹配——这种低级错误,犯过一次就记住了。

关键参数速查:
波特率:双方必须一致,误差不超过±2%
数据位:通常8位,老设备可能用7位
停止位:1位最常用,2位用于抗干扰差的场景

3.2 ESP32多串口配置:三个UART怎么用?

ESP32一共有3个UART控制器:UART0、UART1、UART2。但要注意,UART0默认连接到了板载的USB转串口芯片(CP2102或CH340),所以它通常被用作日志输出和程序下载。

UART编号 默认TX引脚 默认RX引脚 常用场景
UART0 GPIO1 GPIO3 日志输出、程序下载
UART1 GPIO9 GPIO10 外接传感器/模块
UART2 GPIO16 GPIO17 外接设备
避坑指南: 我曾经在项目里直接用UART1的默认引脚GPIO9和GPIO10,结果发现程序死活下载不进去。后来才想起来——这两个引脚在ESP32上连接了内部Flash!所以,如果你要用UART1,记得重新映射引脚。

下面是一个标准的UART2初始化代码,我习惯把引脚映射到GPIO16和GPIO17:

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

#define UART2_TX_PIN  GPIO_NUM_16
#define UART2_RX_PIN  GPIO_NUM_17
#define UART2_BAUD    115200
#define UART2_BUF_SIZE 1024

void uart2_init(void) {
    uart_config_t uart_config = {
        .baud_rate = UART2_BAUD,
        .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_NUM_2, &uart_config);
    uart_set_pin(UART_NUM_2, UART2_TX_PIN, UART2_RX_PIN, 
                 UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
    uart_driver_install(UART_NUM_2, UART2_BUF_SIZE, 
                        UART2_BUF_SIZE, 0, NULL, 0);
}

这段代码里,uart_driver_install的第二个参数是接收缓冲区大小,第三个是发送缓冲区大小。我建议至少设到1024字节,不然数据稍微多点就容易丢包。

3.3 环形缓冲区实现:解决数据粘包和丢失

为什么要用环形缓冲区?你想想看,如果串口数据是一段一段来的,每次中断里只读一个字节,那主循环里怎么知道数据有没有收完?

环形缓冲区(Ring Buffer)就是解决这个问题的。它本质上是一个固定大小的数组,用两个指针(读指针和写指针)来管理数据。写指针追着读指针跑,永远不会溢出——除非你写入速度远大于读取速度。

我的经验: 在ESP32上,我一般把环形缓冲区大小设为256字节或512字节。太小了容易丢数据,太大了浪费RAM——ESP32的RAM总共才520KB,省着点用。

下面是一个轻量级的环形缓冲区实现:

typedef struct {
    uint8_t *buffer;
    uint32_t head;      // 写指针
    uint32_t tail;      // 读指针
    uint32_t size;      // 缓冲区大小
} ring_buf_t;

bool ring_buf_init(ring_buf_t *rb, uint32_t size) {
    rb->buffer = malloc(size);
    if (!rb->buffer) return false;
    rb->head = 0;
    rb->tail = 0;
    rb->size = size;
    return true;
}

bool ring_buf_write(ring_buf_t *rb, uint8_t data) {
    uint32_t next = (rb->head + 1) % rb->size;
    if (next == rb->tail) return false;  // 缓冲区满
    rb->buffer[rb->head] = data;
    rb->head = next;
    return true;
}

bool ring_buf_read(ring_buf_t *rb, uint8_t *data) {
    if (rb->head == rb->tail) return false;  // 缓冲区空
    *data = rb->buffer[rb->tail];
    rb->tail = (rb->tail + 1) % rb->size;
    return true;
}

注意看,这里判断「满」和「空」的方式:head == tail表示空,(head + 1) % size == tail表示满。这意味着缓冲区实际能存的数据是size - 1个字节——牺牲一个位置来区分满和空状态。

在UART中断服务函数里,我们只需要把收到的字节塞进环形缓冲区:

static ring_buf_t uart_rb;

void uart2_isr_handler(void *arg) {
    uint8_t data;
    while (uart_read_bytes(UART_NUM_2, &data, 1, 0) > 0) {
        if (!ring_buf_write(&uart_rb, data)) {
            // 缓冲区满了,丢掉旧数据
            // 我这里选择丢弃最旧的数据
            ring_buf_read(&uart_rb, NULL);
            ring_buf_write(&uart_rb, data);
        }
    }
}
注意: 中断服务函数里不要做耗时操作,比如打印日志、动态内存分配。我曾经见过有人在ISR里调用printf,结果系统直接卡死——中断里调用printf会触发另一个中断,形成递归,栈溢出。

3.4 与PC上位机联调:让数据流动起来

硬件和驱动都准备好了,怎么和PC通信?最常用的工具是串口调试助手,比如SSCOM、Putty、或者VSCode的串口插件。

联调时,我一般遵循这个流程:

  1. 硬件连接: ESP32的UART0通过USB线连到PC,驱动自动识别为COM口
  2. 参数匹配: 上位机设置115200-8-N-1,和ESP32保持一致
  3. 发送测试: 上位机发送"AT\r\n"或任意字符串,看ESP32能否回复
  4. 数据解析: 如果数据乱码,检查波特率;如果丢数据,检查缓冲区大小

下面是一个简单的ESP32回显程序,收到什么就返回什么:

void app_main(void) {
    uart2_init();
    ring_buf_init(&uart_rb, 256);
    
    uint8_t data;
    while (1) {
        if (ring_buf_read(&uart_rb, &data)) {
            uart_write_bytes(UART_NUM_2, &data, 1, portMAX_DELAY);
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

这个程序虽然简单,但已经构成了一个完整的串口通信链路。你可以用上位机发送"Hello ESP32",然后看到同样的字符串返回。

联调常见问题排查:
• 收不到数据:检查引脚映射、波特率、驱动是否安装
• 数据乱码:波特率不匹配,或者双方数据位/停止位不一致
• 数据丢包:环形缓冲区太小,或者主循环读取速度不够快
• 程序下载失败:按住BOOT键再按RESET,或者检查UART0是否被占用

3.5 知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以把它当作一个快速索引:

UART串口通信知识体系 UART协议基础 ESP32多串口配置 环形缓冲区实现 数据帧结构 波特率匹配 UART0/1/2 引脚映射 读写指针 满/空判断 PC上位机联调 硬件连接 参数匹配 发送测试 数据解析 核心原则:协议是基础,配置要灵活,缓冲区防丢包,联调靠耐心 —— 串口通了,嵌入式开发就成功了一半

嗯,这一章的内容就到这里。UART串口看似简单,但真正用好它,需要理解协议、熟悉硬件、掌握缓冲区管理。我建议你动手把代码跑一遍,然后试着用环形缓冲区接收一帧完整的数据——比如GPS的NMEA语句。相信我,当你看到数据完整无误地出现在串口助手上的那一刻,你会觉得一切都值了。