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

UART,说白了就是串口。这东西在嵌入式开发里太常见了——调试打印、传感器通信、GPS模块、蓝牙模块,几乎每个项目都离不开它。我最早接触ESP32时,第一个跑通的例程就是UART回环测试。嗯,今天我们就把它彻底搞明白。

UART协议基础

UART是异步串行通信,没有时钟线。收发双方靠约定的波特率来同步。数据格式很简单:

  • 起始位:1位,逻辑低电平
  • 数据位:5~9位,常用8位
  • 校验位:可选,奇校验/偶校验/无校验
  • 停止位:1位或2位,逻辑高电平

举个例子,8N1表示8数据位、无校验、1停止位。这是最常用的配置。我在项目中遇到过一个问题:两个设备波特率都设成了115200,但通信就是乱码。查了半天,发现一个设备用的是16MHz晶振,另一个是12MHz,实际波特率误差超过了2%。UART对时钟精度有要求,误差超过3%就容易丢数据。

经验之谈:调试串口时,先用示波器看波形。起始位下降沿一抓,量一下位宽,1/位宽就是实际波特率。这比猜配置靠谱多了。

ESP-IDF UART驱动API

ESP-IDF的UART驱动封装得不错。核心API就几个,我列个表方便你对照:

函数 作用 我常用的参数
uart_driver_install() 安装驱动,分配缓冲区 队列大小、环形缓冲区大小
uart_param_config() 配置波特率、数据位等 UART_NUM_1, 115200, 8N1
uart_set_pin() 映射GPIO引脚 TX、RX、RTS、CTS
uart_write_bytes() 发送数据 超时时间设为0
uart_read_bytes() 读取数据 配合环形缓冲区使用

安装驱动的顺序很重要。我习惯这样写:

// 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
};
uart_param_config(UART_NUM_1, &uart_config);

// 2. 设置引脚
uart_set_pin(UART_NUM_1, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);

// 3. 安装驱动,分配环形缓冲区
uart_driver_install(UART_NUM_1, RX_BUF_SIZE, TX_BUF_SIZE, 20, &uart_queue, 0);
注意:uart_driver_install()的第三个参数是TX缓冲区大小。如果你只发不收,可以设成0。但RX缓冲区建议至少256字节,否则高频数据容易溢出。

串口收发数据实战

收发数据其实就两个函数:uart_write_bytes()和uart_read_bytes()。但实际项目中,收发逻辑要复杂得多。

发送很简单:

const char *data = "Hello ESP32\r\n";
uart_write_bytes(UART_NUM_1, data, strlen(data));

接收就有讲究了。你不能一直轮询,那样CPU就废了。我推荐用事件驱动方式:

uart_event_t event;
while (1) {
    if (xQueueReceive(uart_queue, (void *)&event, portMAX_DELAY)) {
        switch (event.type) {
            case UART_DATA:
                // 有数据来了
                uint8_t *buf = malloc(event.size);
                uart_read_bytes(UART_NUM_1, buf, event.size, 0);
                // 处理数据...
                free(buf);
                break;
            case UART_FIFO_OVF:
                // FIFO溢出,清空缓冲区
                uart_flush_input(UART_NUM_1);
                break;
            default:
                break;
        }
    }
}

我曾经在一个项目中用轮询方式接收GPS数据,结果CPU占用率飙到40%。改成事件驱动后,降到5%以下。你想想看,这差距多大。

环形缓冲区应用

环形缓冲区是UART驱动的核心。ESP-IDF内部已经帮你实现了,但理解它的原理很重要。

说白了,环形缓冲区就是一个固定大小的数组,加上读指针和写指针。写指针追着读指针跑,读指针追着写指针跑。当写指针追上读指针时,就说明缓冲区满了——要么覆盖旧数据,要么丢弃新数据。

我画个图帮你理解:

环形缓冲区工作原理 BUF[0] BUF[1] BUF[2] BUF[3] BUF[4] BUF[5] 写指针 读指针 绿色:已写入但未读取的数据 蓝色:空闲空间 橙色:已读取或未使用的空间

实际使用中,你不需要自己实现环形缓冲区。ESP-IDF的uart_driver_install()已经帮你分配好了。但有几个坑要注意:

  • 缓冲区大小要匹配数据量:如果一帧数据1024字节,缓冲区至少2048,留余量
  • 读取要及时:否则写指针追上读指针,数据就被覆盖了
  • 中断服务程序要快:不要在ISR里做复杂处理,只把数据丢进队列

核心要点:UART通信的可靠性,一半靠硬件配置,一半靠软件缓冲区管理。环形缓冲区用好了,数据流再大也不怕。

我记得有一次调试一个4G模块,数据量特别大,每秒好几KB。一开始缓冲区设了512字节,结果一直丢数据。后来改成2048字节,配合事件驱动读取,问题就解决了。说白了,缓冲区大小和读取策略要匹配数据速率。

嗯,UART这块内容就这些。从协议到API,再到实战和环形缓冲区,都是我在项目中反复踩过的坑。你把这些搞明白,串口通信这块基本就稳了。