3、UART串口通信:硬件串口配置、自定义波特率、串口缓冲区管理、与PC通信实战

UART串口,说白了就是嵌入式设备与外界对话的“嘴巴”。你想想看,一个ESP32芯片,没有屏幕没有键盘,怎么知道它跑没跑起来?怎么调试代码?怎么跟PC交换数据?全靠串口。

我个人习惯,拿到一块新的ESP32开发板,第一件事就是点亮LED,第二件事就是打通串口。串口通了,后面所有事情才有底。这一章,咱们就把ESP32的UART串口彻底搞明白。

3.1 ESP32的硬件串口资源

ESP32一共有3个硬件UART控制器:UART0、UART1、UART2。每个都是独立的硬件外设,可以同时工作,互不干扰。

串口编号 默认引脚(TX / RX) 常用场景 备注
UART0 GPIO1 / GPIO3 固件下载、调试日志输出 默认连接USB转串口芯片
UART1 GPIO9 / GPIO10 外接传感器、GPS模块 注意避开Flash占用引脚
UART2 GPIO16 / GPIO17 外接蓝牙模块、RS485 引脚自由度高,推荐使用
⚠ 避坑提醒: 我曾经在项目里直接用UART1的默认引脚GPIO9/GPIO10去接一个4G模块,结果死活不通。查了半天才发现,这两个引脚在ESP32启动时被Flash占用。后来我改用UART2,问题立刻解决。所以,外接设备优先用UART2,省心。

3.2 硬件串口配置实战

ESP-IDF里配置串口,核心就三步:参数设置 → 安装驱动 → 引脚绑定。我直接上代码,边看边讲。

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

// 定义串口参数
#define UART_PORT_NUM      UART_NUM_2
#define UART_TX_GPIO       17
#define UART_RX_GPIO       16
#define UART_BAUD_RATE     115200
#define UART_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, UART_BUF_SIZE * 2,
                        UART_BUF_SIZE * 2, 0, NULL, 0);
}

这里有个细节:uart_driver_install的第二个参数是接收缓冲区大小,第三个是发送缓冲区大小。我建议接收缓冲区至少设成你预期最大数据包的2倍,防止丢包。

3.3 自定义波特率:不按套路出牌

标准波特率大家都熟:9600、115200、921600。但有时候,项目要求就是不走寻常路。比如我做过一个工业项目,对方设备非要跑一个19200的波特率——等等,19200不是标准吗?嗯,但那次对方要的是14400,这就有点偏了。

ESP32的UART时钟源是APB(默认80MHz),通过分频器可以生成任意波特率。公式很简单:

实际波特率 = APB时钟频率 / (分频系数 × 8)

ESP-IDF的uart_param_config会自动计算最接近的分频值。你只管填目标波特率,驱动会帮你搞定。但要注意,波特率越高,对时钟精度要求也越高。我曾经在14400波特率下通信偶尔乱码,排查后发现是APB时钟被其他外设动态调整了。解决办法:固定source_clk为UART_SCLK_APB,别用默认的UART_SCLK_DEFAULT。

💡 小技巧: 如果你需要非标准波特率(比如250000、500000),直接在baud_rate字段填数字就行。ESP32的UART硬件支持连续可调,不是只能选那几个固定值。

3.4 串口缓冲区管理:别让数据丢了

串口通信最怕什么?数据来了,CPU没空处理,结果被新数据覆盖。这就是缓冲区溢出。ESP32的UART驱动内部维护了一个环形缓冲区(Ring Buffer),接收到的数据先存进去,应用程序再从里面读。

我画了一张图,帮你理解数据流动的过程:

UART串口数据流与缓冲区管理 PC / 外部设备 TX UART硬件FIFO 环形缓冲区 应用程序 uart_read_bytes() 应用缓冲区 数据解析 / 业务逻辑 ⚠ 关键点:环形缓冲区大小要大于单次最大数据量。如果应用来不及读,数据会被新数据覆盖。 建议:接收缓冲区设为最大数据包长度的2~4倍,并开启UART中断模式。

实际开发中,我建议你这样管理缓冲区:

  • 接收缓冲区:至少1024字节起步。如果数据量大(比如GPS NMEA语句,一条最长82字节,但每秒10条),建议设到2048以上。
  • 发送缓冲区:如果只是发短命令,256字节就够了。如果要发文件或固件,设到4096。
  • 应用层缓冲区:自己再开一个环形队列或链表,从驱动层读出来后,放到应用层慢慢处理。
📌 我的经验: 曾经做一个多串口数据采集器,3个UART同时接收,每个波特率921600。一开始缓冲区设小了,数据频繁丢失。后来我把每个串口的接收缓冲区都扩到4096字节,并且用FreeRTOS任务独立处理每个串口,问题彻底解决。记住:缓冲区宁可大,不要小。

3.5 与PC通信实战:收发数据

打通串口之后,怎么跟PC收发数据?我直接给一个完整的收发示例,你拿去就能用。

// 发送数据到PC
void uart_send_data(const char *data, size_t len)
{
    uart_write_bytes(UART_PORT_NUM, data, len);
}

// 从PC接收数据(非阻塞)
int uart_receive_data(uint8_t *buf, size_t max_len)
{
    size_t available = 0;
    uart_get_buffered_data_len(UART_PORT_NUM, &available);
    if (available == 0) {
        return 0;  // 没有数据
    }
    size_t len = (available > max_len) ? max_len : available;
    int ret = uart_read_bytes(UART_PORT_NUM, buf, len, 0);
    return ret;
}

// 主循环中调用
void app_main(void)
{
    uart_init();

    uint8_t rx_buf[256];
    while (1) {
        int len = uart_receive_data(rx_buf, sizeof(rx_buf));
        if (len > 0) {
            // 把收到的数据原样发回去(回环测试)
            uart_send_data((const char *)rx_buf, len);
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

用串口助手(比如Putty、sscom)连上ESP32,波特率设115200,发什么就回什么。这就是最简单的串口回环测试,用来验证硬件和驱动是否正常。

⚠ 注意: uart_read_bytes的最后一个参数是超时时间(毫秒)。设为0表示非阻塞,没数据立刻返回。设为portMAX_DELAY会一直等。我建议在任务循环里用非阻塞方式,别把CPU卡死。

3.6 常见问题与排查思路

串口出问题,别慌。我总结了一个排查清单,按顺序查,90%的问题都能解决:

现象 可能原因 排查方法
完全收不到数据 TX/RX接反、波特率不匹配 用示波器或逻辑分析仪看波形
数据乱码 波特率误差大、电平不匹配 检查时钟源、加电平转换芯片
丢数据 缓冲区太小、中断优先级低 加大缓冲区、提高中断优先级
偶尔卡死 中断服务函数里做了耗时操作 ISR里只做标志位,处理放任务里

嗯,这一章的内容就到这里。串口是嵌入式开发的基本功,看似简单,但坑不少。把缓冲区管理好、波特率配准、引脚选对,后面用MQTT跟云平台通信时,串口就是你最可靠的调试通道。