3. ESP32硬件基础
UART串口,嵌入式开发里最基础也最常用的通信方式。说它基础,是因为几乎所有MCU都自带UART外设;说它常用,是因为调试、传感器、GPS模块、蓝牙模块……哪哪都离不开它。
ESP32有3个UART控制器,但很多人只用默认的Serial.print()。其实用好双UART,能同时处理调试信息和业务数据。今天我就把UART协议、双UART配置、环形缓冲区这些硬骨头,掰开了讲清楚。
3.1 UART协议:你真的懂它吗?
UART全称是Universal Asynchronous Receiver/Transmitter。异步,意味着没有时钟线。收发双方靠约定的波特率来同步。
一个完整的数据帧长这样:
| 起始位 | 数据位(5~8位) | 校验位(可选) | 停止位(1/1.5/2位) |
|---|---|---|---|
| 0(低电平) | LSB先发 | 奇/偶/无 | 1(高电平) |
空闲时,TX线保持高电平。起始位拉低,告诉对方“我要发数据了”。然后按位发送数据,最后停止位拉高,结束一帧。
ESP32的UART支持最高5Mbps。但实际项目中,超过921600就得小心走线长度和电平匹配了。
3.2 ESP32双UART配置:不只是Serial1和Serial2
ESP32的3个UART控制器,编号UART0、UART1、UART2。默认UART0连到USB转串口芯片,用于烧录和Serial输出。UART1和UART2的引脚可以任意映射到GPIO。
我个人习惯这样分配:
- UART0:调试日志,固定接USB口
- UART1:与主控或传感器通信
- UART2:与无线模块(如GPS、LoRa)通信
配置代码其实不复杂,但有几个细节容易翻车:
// 初始化UART1,波特率115200,8N1
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, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
uart_driver_install(UART_NUM_1, BUF_SIZE * 2, BUF_SIZE * 2, 10, &uart_queue, 0);
双UART同时收发时,中断优先级和任务调度要小心。我曾经在UART1中断里调用了延时函数,结果UART2的数据全丢了。后来改成用队列传递数据,中断里只做标记。
3.3 环形缓冲区:为什么不用数组?
很多人处理串口数据,直接用一个数组接收。数据来了就覆盖,或者用memcpy搬走。这样做的问题很明显:
- 数据覆盖:处理不及时,新数据把旧数据冲了
- 内存碎片:频繁申请释放,堆容易乱
- 实时性差:必须等一帧收完才能处理
环形缓冲区(Ring Buffer)就是解决这些问题的。它用一块固定内存,通过头尾指针循环读写。生产者(中断)写数据,消费者(任务)读数据,互不干扰。
我常用的环形缓冲区实现:
typedef struct {
uint8_t *buffer;
uint32_t head;
uint32_t tail;
uint32_t size;
} ring_buf_t;
// 写一个字节
bool ring_buf_write(ring_buf_t *rb, uint8_t data) {
uint32_t next = (rb->head + 1) % rb->size;
if (next == rb->tail) {
return false; // 缓冲区满
}
rb->buffer[rb->head] = data;
rb->head = next;
return true;
}
// 读一个字节
bool ring_buf_read(ring_buf_t *rb, uint8_t *data) {
if (rb->head == rb->tail) {
return false; // 缓冲区空
}
*data = rb->buffer[rb->tail];
rb->tail = (rb->tail + 1) % rb->size;
return true;
}
在UART中断里,每收到一个字节就调用ring_buf_write。主循环或任务里,用ring_buf_read取出数据解析。这样即使中断频率很高,也不会阻塞主流程。
3.4 数据收发实战:一个完整的例子
下面是一个双UART同时收发的完整流程。UART1接收传感器数据,UART2发送到无线模块。
// 定义两个环形缓冲区
#define BUF_SIZE 256
ring_buf_t uart1_rb, uart2_rb;
uint8_t uart1_buf[BUF_SIZE];
uint8_t uart2_buf[BUF_SIZE];
// UART1中断处理
static void uart1_isr_handler(void *arg) {
uint8_t data;
while (uart_read_bytes(UART_NUM_1, &data, 1, 0)) {
ring_buf_write(&uart1_rb, data);
}
}
// 主任务
void uart_task(void *pvParameters) {
uint8_t data;
while (1) {
// 从UART1缓冲区读数据
if (ring_buf_read(&uart1_rb, &data)) {
// 处理数据,比如协议解析
process_data(data);
// 转发到UART2
uart_write_bytes(UART_NUM_2, &data, 1);
}
vTaskDelay(1); // 让出CPU
}
}
这个结构的好处是:中断只做一件事——把数据塞进缓冲区。主任务按自己的节奏处理数据。即使UART1以1Mbps狂发数据,只要缓冲区够大,就不会丢。
3.5 知识结构图
下面这张图展示了UART通信的完整数据流:
3.6 性能调优要点
双UART同时跑高速时,有几个地方容易成为瓶颈:
| 瓶颈点 | 表现 | 解决方案 |
|---|---|---|
| 中断频率过高 | CPU占用率飙升 | 使用FIFO中断,一次收多个字节再触发 |
| 缓冲区太小 | 数据丢失 | 根据最大帧长×2设置缓冲区 |
| 任务优先级低 | 数据堆积 | 提高处理任务优先级,或使用DMA |
| 波特率误差 | 乱码 | 用逻辑分析仪校准,或选REF_TICK时钟 |
嗯,UART这块内容就这些。从协议到配置,从环形缓冲区到实战,每一步都有坑,但也都有解法。用好双UART和环形缓冲区,你的ESP32串口通信就能稳如老狗。