示例:UART 串口通信
本教程的核心逻辑适用于所有 ESP32 开发板,但所有操作步骤均以 微雪 ESP32-S3-Zero 迷你开发板 为例进行讲解。如果您使用其他型号的开发板,请根据实际情况修改相应设置。
本教程介绍如何使用乐鑫 ESP-IDF 框架,通过 UART 外设实现串口数据收发。用杜邦线将开发板的 TX 与 RX 短接,构成"自发自收"的环回(loopback)回路,演示 UART 驱动的初始化流程、数据收发以及阻塞读取的超时处理。
1. UART 外设
UART(Universal Asynchronous Receiver/Transmitter,通用异步收发器)是最常见的串行通信接口之一。它异步、串行、全双工,仅需 TX/RX 两根信号线加共地即可在两个设备之间双向传输字节流。
ESP32-S3 芯片内置 3 个独立的 UART 控制器:UART_NUM_0、UART_NUM_1、UART_NUM_2,每个控制器都可以独立配置波特率、数据位、停止位、奇偶校验等参数,TX/RX 引脚可通过 GPIO 矩阵分配到几乎任意空闲 GPIO。
1.1 日志输出与 UART 控制器
在传统单片机上,printf 通常被重定向到某个 UART——日志输出 = UART 输出。但在 ESP32 上不一定走 UART:printf() 和 ESP_LOGI() 的输出目标由开发板硬件决定,可能是 UART,也可能是 USB Serial/JTAG。
- 传统 ESP32 / ESP32 + USB-TTL 桥芯片的开发板:日志默认走 UART0(GPIO1 TX / GPIO3 RX),由板载的 CH340/CP2102 等桥芯片转成 USB 输出到电脑。这种情况下,UART0 已经被日志占用,应用层应改用 UART1/UART2。
- 微雪 ESP32-S3-Zero 等带原生 USB 的开发板:日志默认走 USB Serial/JTAG——这是 ESP32-S3 芯片内置的 USB-CDC 外设,独立于所有 UART 控制器,因此 UART0、UART1、UART2 均处于空闲状态,可由应用层自由使用。
| 开发板类型 | 日志(ESP_LOGI)走哪里 | UART0 状态 | 应用层应使用 |
|---|---|---|---|
| ESP32 经典 + USB-TTL | UART0(被占用) | 已占用 | UART1 / UART2 |
| ESP32-S3-Zero(原生 USB) | USB Serial/JTAG | 空闲 | UART0/1/2 任选 |
1.2 通用步骤
ESP-IDF 的 UART 驱动遵循"配置参数 → 设置引脚 → 安装驱动 → 收发数据"的固定流程:
-
包含头文件
#include "driver/uart.h"并在
main/CMakeLists.txt中声明依赖:REQUIRES esp_driver_uart。 -
配置通信参数:填一个
uart_config_t(波特率、数据位、停止位、奇偶校验、流控、时钟源),调用uart_param_config()。 -
设置 TX/RX 引脚:调用
uart_set_pin(port, tx, rx, rts, cts),未使用的 RTS/CTS 传UART_PIN_NO_CHANGE。备注参数配置与引脚设置的先后顺序没有硬性要求,但两者必须都在
uart_driver_install()之前完成。 -
安装驱动:调用
uart_driver_install(port, rx_buf_size, tx_buf_size, queue_size, &queue, intr_flags),分配收发环形缓冲区。 -
收发数据:
- 发送:
uart_write_bytes() - 接收:
uart_read_bytes(),带 RTOS tick 超时
- 发送:
-
(可选)卸载驱动:
uart_driver_delete()。
2. 示例项目
本示例使用 UART1 进行自环回测试:将开发板的 TX 与 RX 引脚通过一根杜邦线短接,程序周期性地从 TX 发送一条字符串,随后立即从 RX 读回,并通过 USB 控制台(ESP_LOGI)打印发送与接收的内容。
2.1 电路
需要使用的器件有:
-
杜邦线(或跳线)
-
ESP32 开发板 * 1 (微雪 ESP32-S3-Zero 迷你开发板)
ESP32-S3-Zero 引脚图

按照下面接线图连接电路:用一根杜邦线把 GPIO5(用于 UART1 TX) 和 GPIO13(用于 UART1 RX) 短接。
本示例只用一块开发板,TX 与 RX 自然共享同一参考地,不需要额外连线。

2.2 创建项目
-
创建一个项目。如果不清楚如何操作,请参考 从模板创建项目。
-
查看 UART API 参考。根据文档中的指引完成以下步骤。
首先在 main.c 中包含头文件:
#include "driver/uart.h"然后在 main/CMakeLists.txt 中声明
esp_driver_uart组件:idf_component_register(SRCS "main.c"INCLUDE_DIRS "."REQUIRES esp_driver_uart)
2.3 示例代码
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "driver/gpio.h"
#include "driver/uart.h"
static const char *TAG = "example";
#define UART_PORT UART_NUM_1 // 使用 UART1
#define UART_TX_PIN GPIO_NUM_5 // UART1 TX → 接到 RX 即可自环
#define UART_RX_PIN GPIO_NUM_13 // UART1 RX
#define UART_BAUD_RATE 115200
#define UART_BUF_SIZE 1024 // 收发环形缓冲区大小
static void uart_init(void)
{
// 1. 配置通信参数:115200 8N1,无流控
uart_config_t uart_cfg = {
.baud_rate = UART_BAUD_RATE,
.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_DEFAULT,
};
ESP_ERROR_CHECK(uart_param_config(UART_PORT, &uart_cfg));
// 2. 绑定 TX/RX 引脚,RTS/CTS 不用
ESP_ERROR_CHECK(uart_set_pin(UART_PORT,
UART_TX_PIN, UART_RX_PIN,
UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE));
// 3. 安装驱动:分配 RX/TX 环形缓冲区,不使用事件队列
ESP_ERROR_CHECK(uart_driver_install(UART_PORT,
UART_BUF_SIZE, UART_BUF_SIZE,
0, NULL, 0));
}
void app_main(void)
{
uart_init();
uint8_t rx_buf[128];
int counter = 0;
while (1) {
// 组装一条消息:例如 "Hello UART #3"
char tx_msg[32];
int tx_len = snprintf(tx_msg, sizeof(tx_msg), "Hello UART #%d\n", counter++);
// 发送
uart_write_bytes(UART_PORT, tx_msg, tx_len);
ESP_LOGI(TAG, "TX (%d bytes): %.*s", tx_len, tx_len - 1, tx_msg); // -1 去掉末尾的 '\n'
// 接收:等最多 200 ms
int rx_len = uart_read_bytes(UART_PORT, rx_buf, sizeof(rx_buf) - 1,
pdMS_TO_TICKS(200));
if (rx_len > 0) {
rx_buf[rx_len] = '\0';
ESP_LOGI(TAG, "RX (%d bytes): %s", rx_len, (char *)rx_buf);
} else {
ESP_LOGW(TAG, "RX timeout (check the jumper between GPIO%d and GPIO%d)",
UART_TX_PIN, UART_RX_PIN);
}
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
2.4 构建并烧录
-
配置烧录选项
首先,在构建和烧录之前,请务必检查并设置正确的目标设备、串口和烧录方式。参考 第 2 节 运行示例 - 1.3 配置项目。
-
点击
一键自动依次执行构建、烧录和监视。
-
烧录完成后,确保 GPIO5 和 GPIO13 已用杜邦线短接,串口监视器中应该看到一发一收成对出现:
I (266) main_task: Calling app_main()I (276) example: TX (15 bytes): Hello UART #0I (286) example: RX (15 bytes): Hello UART #0I (1286) example: TX (15 bytes): Hello UART #1I (1296) example: RX (15 bytes): Hello UART #1...若仅看到
TX行和RX timeout警告,表示跳线未接好或接错引脚,请核对实际接线与串口监视器输出的 GPIO 号是否一致。你也可以主动断开跳线,串口监视器会出现
RX timeout警告。重新接好后,RX行随即恢复。
2.5 代码解析
1. 包含头文件
#include "driver/gpio.h"
#include "driver/uart.h"
driver/uart.h:ESP-IDF UART 驱动的统一入口,包含uart_config_t、uart_param_config()、uart_set_pin()、uart_driver_install()、uart_write_bytes()、uart_read_bytes()等所有 API。driver/gpio.h:提供GPIO_NUM_x枚举,用于以可读方式书写 TX/RX 引脚号(如GPIO_NUM_5)。
2. 定义常量
#define UART_PORT UART_NUM_1
#define UART_TX_PIN GPIO_NUM_5
#define UART_RX_PIN GPIO_NUM_13
#define UART_BAUD_RATE 115200
#define UART_BUF_SIZE 1024
UART_NUM_1:使用 1 号 UART 控制器。参见 1.1 节,避开 UART0 以保证代码的可移植性。UART_TX_PIN/UART_RX_PIN:通过 GPIO 矩阵将 UART1 的 TX/RX 映射到 GPIO5、GPIO13。任意空闲 GPIO 均可,无需固定引脚。UART_BAUD_RATE:115200 是 ESP32 控制台以及多数外部模块(GPS、传感器、蓝牙模组等)的常用波特率。UART_BUF_SIZE:分配给收发的环形缓冲区大小,单位为字节。1024 字节对本示例足够;实际项目应根据吞吐量调整。
3. UART 初始化(uart_init)
按"参数 → 引脚 → 安装"三步完成:
-
参数配置 (
uart_param_config)uart_config_t uart_cfg = {.baud_rate = UART_BAUD_RATE,.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_DEFAULT,};这是常见的 "115200 8N1,无流控" 配置:8 位数据位、无奇偶校验、1 位停止位,适用于绝大多数应用场景。
source_clk = UART_SCLK_DEFAULT由驱动自动选择合适的时钟源(通常是 APB 时钟)。 -
引脚绑定 (
uart_set_pin)uart_set_pin(UART_PORT, UART_TX_PIN, UART_RX_PIN,UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);四个引脚参数依次是 TX / RX / RTS / CTS。本示例仅使用 TX、RX 两根线;RTS、CTS 传入
UART_PIN_NO_CHANGE,表示保留对应引脚的当前状态不做修改。 -
驱动安装 (
uart_driver_install)uart_driver_install(UART_PORT, UART_BUF_SIZE, UART_BUF_SIZE, 0, NULL, 0);六个参数依次为:UART 端口、RX 环形缓冲区大小、TX 环形缓冲区大小、事件队列深度、事件队列句柄输出指针、中断分配标志。本示例不使用事件队列,因此队列深度传
0、句柄指针传NULL。备注TX 缓冲区传
0合法,表示不使用 TX 环形缓冲,uart_write_bytes()将阻塞直至数据全部写出。传入非零值(如本例的 1024)则使发送函数立即返回,由 ISR 在后台搬运数据,吞吐更高。本示例 RX/TX 均使用相同大小的非零缓冲区。
4. 主循环:发送与接收
-
发送
uart_write_bytes(UART_PORT, tx_msg, tx_len);将
tx_len字节从tx_msg写入 UART1 的 TX 环形缓冲区。由于启用了 TX 缓冲,函数将立即返回,实际数据由驱动 ISR 异步发出。 -
接收
int rx_len = uart_read_bytes(UART_PORT, rx_buf, sizeof(rx_buf) - 1,pdMS_TO_TICKS(200));最多读
sizeof(rx_buf) - 1字节(保留一位用于'\0'终止符),最长等待 200 ms。返回值为实际读到的字节数:> 0:成功读到对应字节数;0:超时,未读到数据;-1:参数错误。
200 ms 的超时对 115200 波特率下传输数十字节(耗时约 1 ms 量级)足够宽裕,同时不会让主循环长时间停顿。实际项目中,该超时应根据通信对端的响应延迟设定。
备注uart_read_bytes()为阻塞式调用,等待期间当前 FreeRTOS 任务会让出 CPU,不会进行忙等。即使将超时设为portMAX_DELAY(永久等待)也不会占用 CPU。 -
日志走 USB Serial/JTAG,与 UART1 无关
代码中的
ESP_LOGI(TAG, ...)通过 USB Serial/JTAG 输出到 VS Code 串口监视器,这条通路独立于 UART1。若 UART1 的 TX/RX 未正确连接,仅会触发 RX 超时分支;ESP_LOGI的输出不受影响。