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 | 可任意映射 |
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);
为什么是两倍?你想想看,如果数据刚好填满缓冲区,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;
}
}
}
}
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;
}
实际项目中,我会把环形缓冲区和中断接收结合起来:
// 在中断任务中
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稍微复杂一点,但掌握了套路,其实也差不多。