跳到主要内容

示例: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_0UART_NUM_1UART_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-TTLUART0(被占用)已占用UART1 / UART2
ESP32-S3-Zero(原生 USB)USB Serial/JTAG空闲UART0/1/2 任选

1.2 通用步骤

ESP-IDF 的 UART 驱动遵循"配置参数 → 设置引脚 → 安装驱动 → 收发数据"的固定流程:

  1. 包含头文件

    #include "driver/uart.h"

    并在 main/CMakeLists.txt 中声明依赖:REQUIRES esp_driver_uart

  2. 配置通信参数:填一个 uart_config_t(波特率、数据位、停止位、奇偶校验、流控、时钟源),调用 uart_param_config()

  3. 设置 TX/RX 引脚:调用 uart_set_pin(port, tx, rx, rts, cts),未使用的 RTS/CTS 传 UART_PIN_NO_CHANGE

    备注

    参数配置与引脚设置的先后顺序没有硬性要求,但两者必须都在 uart_driver_install() 之前完成。

  4. 安装驱动:调用 uart_driver_install(port, rx_buf_size, tx_buf_size, queue_size, &queue, intr_flags),分配收发环形缓冲区。

  5. 收发数据

    • 发送:uart_write_bytes()
    • 接收:uart_read_bytes(),带 RTOS tick 超时
  6. (可选)卸载驱动uart_driver_delete()

2. 示例项目

本示例使用 UART1 进行自环回测试:将开发板的 TX 与 RX 引脚通过一根杜邦线短接,程序周期性地从 TX 发送一条字符串,随后立即从 RX 读回,并通过 USB 控制台(ESP_LOGI)打印发送与接收的内容。

2.1 电路

需要使用的器件有:

按照下面接线图连接电路:用一根杜邦线把 GPIO5(用于 UART1 TX)GPIO13(用于 UART1 RX) 短接。

信息

本示例只用一块开发板,TX 与 RX 自然共享同一参考地,不需要额外连线。

接线图

2.2 创建项目

  1. 创建一个项目。如果不清楚如何操作,请参考 从模板创建项目

  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 构建并烧录

  1. 配置烧录选项

    首先,在构建和烧录之前,请务必检查并设置正确的目标设备、串口和烧录方式。参考 第 2 节 运行示例 - 1.3 配置项目

    VS Code 工具栏

  2. 点击 VS Code 一键构建烧录监视图标 一键自动依次执行构建、烧录和监视。

  3. 烧录完成后,确保 GPIO5 和 GPIO13 已用杜邦线短接,串口监视器中应该看到一发一收成对出现:

    I (266) main_task: Calling app_main()
    I (276) example: TX (15 bytes): Hello UART #0
    I (286) example: RX (15 bytes): Hello UART #0

    I (1286) example: TX (15 bytes): Hello UART #1
    I (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_tuart_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 的输出不受影响。

3. 进一步阅读