3. UART串口通信:从协议到实战

UART串口,可以说是嵌入式开发里最基础、也最常用的通信接口了。我刚开始做嵌入式那会儿,调试代码全靠串口打印,那时候还没有现在的JTAG/SWD这么方便。说白了,UART就是两根线——TX和RX,一收一发,简单粗暴。

但你别看它简单,用不好照样翻车。我记得有一次量产阶段,发现一批板子串口通信偶尔丢字节,查了两天才找到原因——波特率误差累积导致的采样错位。嗯,这节课我们就从协议基础讲起,把ESP-IDF的UART驱动API捋一遍,最后带大家做个收发实战。

3.1 UART协议基础

UART全称是Universal Asynchronous Receiver/Transmitter,异步串行通信。什么叫异步?就是没有单独的时钟线,收发双方靠约定的波特率来同步。

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

起始位(1bit) + 数据位(5~8bit) + 校验位(可选) + 停止位(1/1.5/2bit)

举个最常见的配置:8N1,意思是8位数据、无校验、1位停止位。加上1位起始位,一帧总共10个bit。

波特率就是每秒传输的bit数。115200波特率,每秒传115200个bit,除以10就是一帧,实际每秒传11520字节。够你打印日志了。

这里有个坑——波特率误差。收发双方的波特率偏差不能超过±2%,否则采样点会偏移。我建议你尽量用整数分频的波特率,比如115200、57600、38400这些,别用奇怪的数字。

3.2 ESP-IDF UART驱动API

ESP-IDF的UART驱动封装得挺完善的。核心API就几个,我按使用顺序列出来:

函数 作用 注意事项
uart_driver_install() 安装驱动,分配缓冲区 队列大小要合理,别太大也别太小
uart_param_config() 配置波特率、数据位、停止位等 一次搞定所有参数
uart_set_pin() 映射TX/RX/RTS/CTS引脚 ESP32支持任意GPIO映射
uart_write_bytes() 发送数据 返回实际写入的字节数
uart_read_bytes() 读取数据 可以设置超时
uart_flush() 清空接收缓冲区 上电初始化时建议调用一次

我个人习惯把配置封装成一个函数,这样项目里多个串口复用起来方便。你想想看,每次都要写一遍uart_param_config,多烦人。

小技巧:ESP32的UART1和UART2默认引脚和Flash有冲突,用之前一定要查引脚复用表。我吃过这个亏,焊好板子发现UART2死活不通,最后发现引脚被Flash占用了。

3.3 串口收发数据实战

光说不练假把式。我们写一个完整的UART收发例程:UART2接收数据,然后原样返回(回环测试)。

先看代码:

#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_BUF_SIZE      1024

void uart_init(void)
{
    // 1. 配置参数
    uart_config_t uart_config = {
        .baud_rate = 115200,
        .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);

    // 2. 设置引脚
    uart_set_pin(UART_PORT_NUM, UART_TX_GPIO, UART_RX_GPIO, 
                 UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);

    // 3. 安装驱动
    uart_driver_install(UART_PORT_NUM, UART_BUF_SIZE, 0, 0, NULL, 0);
}

void uart_echo_task(void *arg)
{
    uint8_t *data = malloc(UART_BUF_SIZE);
    
    while (1) {
        // 读取数据,超时20ms
        int len = uart_read_bytes(UART_PORT_NUM, data, UART_BUF_SIZE, 
                                  pdMS_TO_TICKS(20));
        if (len > 0) {
            // 原样返回
            uart_write_bytes(UART_PORT_NUM, (const char *)data, len);
        }
        vTaskDelay(10 / portTICK_PERIOD_MS);
    }
    
    free(data);
}

void app_main(void)
{
    uart_init();
    xTaskCreate(uart_echo_task, "uart_echo", 4096, NULL, 10, NULL);
}

这段代码逻辑很清晰:初始化UART2,然后创建一个任务循环收数据、发数据。注意我用了pdMS_TO_TICKS(20)作为超时,这样不会死等。

注意:uart_read_bytes的timeout参数是FreeRTOS的tick数,不是毫秒。用pdMS_TO_TICKS()做转换,别直接写数字。我曾经在某个项目里直接写了20,结果超时时间完全不对,调试了一下午才发现。

下面这张图展示了UART收发的完整流程:

UART收发数据流程图 应用层调用 uart_driver_install() UART硬件收发 uart_write_bytes() uart_read_bytes() 注意:发送和接收可以同时进行,UART硬件是全双工的 但软件层面要注意缓冲区管理,避免数据覆盖

从流程图能看出来,UART通信分三层:应用层调用API、驱动层管理缓冲区、硬件层负责实际电平转换。每一层都有各自的注意事项。

3.4 常见问题与避坑指南

做UART开发,有几个坑我几乎每次都会遇到:

  • 波特率不准:ESP32的APB时钟是80MHz,有些波特率分频后误差大。建议用uart_get_baudrate()检查实际值。
  • 缓冲区溢出:接收缓冲区满了之后,新数据会丢失。我建议把缓冲区设大一点,或者用中断方式及时读取。
  • 引脚冲突:UART0默认是下载口,别乱动。UART1和UART2的默认引脚可能和Flash/PSRAM冲突。
  • 流控问题:如果对方设备有流控,记得配置RTS/CTS引脚,否则高速通信会丢包。

我的经验:调试UART时,先用逻辑分析仪看波形。看一眼起始位、数据位、停止位对不对,比猜代码快多了。我包里常备一个8通道的逻辑分析仪,几十块钱那种就够用。

3.5 总结

UART串口通信,说白了就是两根线、一个波特率、几个配置参数。ESP-IDF的驱动API封装得很好,你只需要记住安装→配置→收发的流程就行。

嗯,这节课的内容就到这里。代码我已经在ESP32-S3上跑过,没问题。你拿过去直接编译烧录,接上USB转串口模块就能看到效果。