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通信分三层:应用层调用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转串口模块就能看到效果。