4、UART串口通信:从硬件到环形缓冲区的实战指南

UART,说白了就是串口。这玩意儿在嵌入式开发里,就像空气一样无处不在。我刚开始做ESP32项目时,第一个调通的接口就是它。你想想看,没有串口,你怎么看打印信息?怎么跟传感器聊天?

这一章,咱们就把UART从里到外扒个干净。从硬件引脚到驱动配置,从收发数据到环形缓冲区,一步到位。

4.1 UART硬件介绍:不只是TX和RX

ESP32的UART,其实挺强大的。它有三个独立的UART控制器,每个都能独立配置。我习惯把UART0留给日志打印,UART1接外设,UART2留着备用。

先看硬件引脚:

UART编号 默认TX 默认RX 备注
UART0 GPIO1 GPIO3 通常接USB转串口芯片
UART1 GPIO10 GPIO9 可任意映射
UART2 GPIO17 GPIO16 可任意映射
我的经验:ESP32的UART引脚其实可以任意映射到任何GPIO。但要注意,有些GPIO在启动时有特殊功能,比如GPIO12是电压选择脚。我曾经踩过这个坑,焊好板子发现串口死活不通,查了半天才发现是GPIO12被拉高了。

UART的硬件结构,核心就三部分:

  • 波特率发生器:决定通信速度,常见的有9600、115200
  • 收发FIFO:硬件缓冲区,ESP32每个UART有128字节的FIFO
  • 控制逻辑:处理起始位、停止位、校验位

嗯,这里要注意。FIFO虽然只有128字节,但配合中断,已经能处理大部分场景了。我见过有人一上来就搞DMA,其实没必要。先搞清楚FIFO怎么用,再说别的。

4.2 UART驱动配置:三步走

配置UART驱动,我总结了三步:参数设置、引脚分配、安装驱动。咱们直接看代码:

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

// 第一步:配置参数
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_NUM_1, &uart_config);
uart_set_pin(UART_NUM_1, TX_PIN, RX_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
uart_driver_install(UART_NUM_1, BUF_SIZE * 2, BUF_SIZE * 2, 20, &uart_queue, 0);
关键点:uart_driver_install的第二个参数是接收缓冲区大小,第三个是发送缓冲区大小。我建议设成你预期数据量的两倍,防止溢出。

为什么是两倍?你想想看,如果数据刚好填满缓冲区,CPU还没来得及处理,下一波数据就来了。留点余量,心里踏实。

4.3 串口收发数据:中断还是轮询?

收发数据,有两种方式:轮询和中断。我个人的建议是:

  • 调试阶段:用轮询,简单直接
  • 正式产品:用中断,不阻塞CPU

先看轮询发送:

// 发送数据
const char *data = "Hello, ESP32!\r\n";
uart_write_bytes(UART_NUM_1, data, strlen(data));

// 接收数据
uint8_t rx_buf[128];
int len = uart_read_bytes(UART_NUM_1, rx_buf, sizeof(rx_buf), pdMS_TO_TICKS(100));
if (len > 0) {
    // 处理接收到的数据
}

再看中断接收。这才是实际项目里常用的方式:

// 中断接收任务
void uart_event_task(void *pvParameters) {
    uart_event_t event;
    uint8_t *data = (uint8_t *)malloc(BUF_SIZE);
    
    while (1) {
        // 等待UART事件
        if (xQueueReceive(uart_queue, &event, portMAX_DELAY)) {
            switch (event.type) {
                case UART_DATA:
                    // 有数据来了
                    uart_read_bytes(UART_NUM_1, data, event.size, portMAX_DELAY);
                    // 处理数据...
                    break;
                    
                case UART_FIFO_OVF:
                    // FIFO溢出了,赶紧清空
                    uart_flush_input(UART_NUM_1);
                    break;
                    
                case UART_BUFFER_FULL:
                    // 缓冲区满了
                    uart_flush_input(UART_NUM_1);
                    break;
                    
                default:
                    break;
            }
        }
    }
}
避坑指南:我曾经在中断处理函数里直接调用printf,结果系统直接死机。记住,中断服务函数里不要做耗时操作,更不要调用会阻塞的函数。把数据扔到队列里,让任务去处理。

4.4 环形缓冲区应用:解决数据粘包

说到环形缓冲区,我得先讲个故事。有一次我做GPS数据解析,串口一包数据还没收完,下一包就来了。用普通数组存,数据全乱套了。后来用了环形缓冲区,问题迎刃而解。

环形缓冲区,说白了就是一个循环数组。有读指针和写指针,写满了就从头开始。这样就不会有数据覆盖的问题。

我写了一个轻量级的环形缓冲区,你们可以直接用:

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

// 初始化
void ring_buffer_init(ring_buffer_t *rb, uint8_t *buf, int size) {
    rb->buffer = buf;
    rb->head = 0;
    rb->tail = 0;
    rb->size = size;
}

// 写入数据
int ring_buffer_write(ring_buffer_t *rb, uint8_t *data, int len) {
    int written = 0;
    for (int i = 0; i < len; i++) {
        int next = (rb->tail + 1) % rb->size;
        if (next == rb->head) {
            // 缓冲区满了
            break;
        }
        rb->buffer[rb->tail] = data[i];
        rb->tail = next;
        written++;
    }
    return written;
}

// 读取数据
int ring_buffer_read(ring_buffer_t *rb, uint8_t *data, int len) {
    int read = 0;
    while (rb->head != rb->tail && read < len) {
        data[read++] = rb->buffer[rb->head];
        rb->head = (rb->head + 1) % rb->size;
    }
    return read;
}
使用技巧:环形缓冲区的大小,我建议设成最大数据包长度的2-4倍。比如你的GPS数据包最长100字节,那就设256或512。太小容易丢数据,太大浪费内存。

实际项目中,我会把环形缓冲区和中断接收结合起来:

// 在中断任务中
case UART_DATA:
    uart_read_bytes(UART_NUM_1, temp_buf, event.size, portMAX_DELAY);
    ring_buffer_write(&rx_ring, temp_buf, event.size);
    break;

// 在主循环中解析
void parse_data_task(void *pvParameters) {
    uint8_t buf[256];
    while (1) {
        int len = ring_buffer_read(&rx_ring, buf, sizeof(buf));
        if (len > 0) {
            // 在这里解析数据,比如找帧头帧尾
            for (int i = 0; i < len; i++) {
                // 解析逻辑...
            }
        }
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

嗯,这里要注意。环形缓冲区虽然好用,但也不是万能的。如果数据量特别大,或者实时性要求特别高,还是得上DMA。不过对于大部分物联网项目,这个方案已经够用了。

小结

这一章咱们聊了UART的硬件结构、驱动配置、收发方式,还有环形缓冲区的实战应用。说白了,串口通信就是三个字:配、收、发。配好参数,收对数据,发准指令,剩下的就是经验积累了。

下一章,咱们聊聊I2C通信。那个比UART稍微复杂一点,但掌握了套路,其实也差不多。