ESP32‑S3 串口:从“能打印日志”到真正用在业务里

刚开始用 ESP32‑S3 的时候,我对串口的理解其实非常“初级”——

能连上串口助手、能看到启动日志,就算会用了。

但真正开始写业务代码、接外设、跑 FreeRTOS 任务之后,我才发现:UART 这东西,看起来简单,用起来细节非常多

这篇文章不是教科书式的 API 罗列,而是我在 ESP32‑S3 上一步一步把串口用“顺”的过程

  • UART 在 ESP32‑S3 里到底是什么
  • ESP‑IDF 推荐的使用顺序
  • 为什么一定要装驱动、用缓冲区
  • 串口 + FreeRTOS 任务,才是“正确打开方式”

一、ESP32‑S3 里的 UART 是什么

UART,全称 Universal Asynchronous Receiver / Transmitter(通用异步收发器)

在 ESP32‑S3 中:

  • 一共有 3 个 UART 控制器

  • 每个 UART 都可以独立配置

    • 波特率
    • 数据位
    • 校验位
    • 停止位
    • 流控方式

换句话说:

UART 并不是“固定焊死在某几个 IO 上”的外设,而是一套可以灵活映射到 GPIO 的控制器。

这也是 ESP32 相比很多传统 MCU 非常舒服的一点。
以下有两张图

UART架构概述图 UART结构
UART架构概述图 UART结构

二、ESP‑IDF 中使用 UART 的“官方正确姿势”

在 ESP‑IDF 里,官方把 UART 的使用拆成了非常清晰的三步

  1. 配置串口参数
  2. 设置串口管脚
  3. 安装驱动程序

这个顺序不是随便定的,而是踩坑踩出来的。

我一开始也试过:

  • 不装驱动直接 uart_write_bytes
  • 不开缓冲区就想收数据

结果就是:

能跑,但非常不稳定,也完全不适合上业务。

下面按我自己的理解,把这三步重新拆一遍。


三、第一步:配置串口参数(一次性 or 分步)

1. 一次性配置(最常用)

这是我现在最推荐、也是最常用的方式。

核心 API:

esp_err_t uart_param_config(uart_port_t uart_num,
                            const uart_config_t *uart_config);

通过 uart_config_t 结构体,把所有参数一次性说明白:

const 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_DEFAULT,
};

uart_param_config(UART_NUM_0, &uart_config);

这种方式的好处是:

  • 配置是“原子的”,一眼就能看清
  • 非常适合博客、示例和工程初始化阶段

2. 分步配置(了解即可)

ESP‑IDF 也提供了:

  • 单独设置波特率
  • 单独设置数据位
  • 单独设置校验方式
配置参数 函数
波特率 uart_set_baudrate()
传输位 调用 uart_set_word_length() 设置 uart_word_length_t
奇偶控制 调用 uart_parity_t 设置 uart_set_parity()
停止位 调用 uart_set_stop_bits() 设置 uart_stop_bits_t
硬件流控模式 调用 uart_set_hw_flow_ctrl() 设置 uart_hw_flowcontrol_t
通信模式 调用 uart_set_mode() 设置 uart_mode_t

这在动态修改参数时会比较有用,但在绝大多数应用里并不常见。

具体可参考官方文档链接:https://docs.espressif.com/projects/esp-idf/zh_CN/v5.4/esp32s3/api-reference/peripherals/uart.html


四、第二步:设置串口管脚(ESP32 的灵魂所在)

这是 ESP32‑S3 非常“爽”的一个地方。

在 STM32 里,UART IO 基本是固定的;
但在 ESP32‑S3 中:

UART 的 TX / RX 可以通过软件映射到几乎任意 GPIO。

我自己的板子上:

  • TX → GPIO43
  • RX → GPIO44

在这里插入图片描述

API 原型

esp_err_t uart_set_pin(uart_port_t uart_num,
                       int tx_io_num,
                       int rx_io_num,
                       int rts_io_num,
                       int cts_io_num);

使用示例:

uart_set_pin(UART_NUM_0,
             43,
             44,
             UART_PIN_NO_CHANGE,
             UART_PIN_NO_CHANGE);

如果你不用硬件流控,RTS / CTS 直接 UART_PIN_NO_CHANGE 即可。

RTS 和 CTS 是 UART 的硬件流控信号,用于在接收端处理能力不足时,自动通知发送端暂停发送数据,从而避免 RX FIFO 溢出和数据丢失。在 ESP32-S3 中,RTS / CTS 由 UART 硬件自动管理,并通过 GPIO Matrix 映射到任意 GPIO;在大多数低速 STM32 项目中由于引脚成本和速率需求较低,通常不会启用。


五、第三步:安装驱动(真正开始“工程化”)

这一点是我踩过坑之后才真正重视的

uart_driver_install 并不是“可选项”,而是:

一旦你想稳定收发数据,就必须装。

核心能力

  • 创建 RX / TX 环形缓冲区
  • 管理 UART 中断
  • 可选地提供 事件队列(和 FreeRTOS 深度结合)

API 原型

esp_err_t uart_driver_install(uart_port_t uart_num,
                              int rx_buffer_size,
                              int tx_buffer_size,
                              int queue_size,
                              QueueHandle_t *uart_queue,
                              int intr_alloc_flags);

示例用法:

例如:配置接收缓冲区大小为 1024,不使用发送缓冲区,不使用消息队列。
uart_driver_install(UART_NUM_0, 1024, 0, 0, NULL, 0);


例如:配置接收缓冲区大小为 1024,发送缓冲区大小为512,使用消息队列,消息队列大小为10。
QueueHandle_t uart_queue;
uart_driver_install(UART_NUM_0, 1024, 512, 10, &uart_queue, 0);

如果你后面要用 UART 事件队列,那这里就是入口。
使用队列参考:esp-idf/examples/peripherals/uart/uart_events


六、串口 + FreeRTOS:我真正开始“用明白”的地方

在 ESP32‑S3 上,我基本不会再用“裸循环”去玩串口了。

正确姿势是:任务化。

我的示例结构

  • 一个 发送任务
    • 每 2 秒往串口发一次数据
  • 一个 接收任务
    • 轮询读取 RX 缓冲区
    • 一旦有数据立即处理

初始化代码(精简版)

#define TXD_PIN (GPIO_NUM_43)
#define RXD_PIN (GPIO_NUM_44)
#define UART_NUM (UART_NUM_1)

void init(void)
{
    const 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_DEFAULT,
    };

    uart_param_config(UART_NUM, &uart_config);
    uart_set_pin(UART_NUM, TXD_PIN, RXD_PIN,
                 UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
    uart_driver_install(UART_NUM, 2048, 0, 0, NULL, 0);
}

发送任务的核心逻辑

int uart_write_bytes(uart_port_t uart_num, const void* src, size_t size);

从应用层看,这只是一个阻塞式发送 API。
但在 ESP-IDF 内部,它隐含了一套完整的发送路径:

  • 数据先被拷贝到 TX FIFO / 驱动缓冲区
  • UART 硬件负责逐字节移出
  • 必要时通过中断驱动继续发送

你在任务里调用它时,并不需要关心:

  • FIFO 是否满
  • 发送是否被打断
  • 是否需要手动等待发送完成
  • 这些复杂度,被 ESP-IDF 吃掉了。

接收任务的核心逻辑

const int rxBytes = uart_read_bytes(UART_NUM,
                                    data,
                                    RX_BUF_SIZE,
                                    1000 / portTICK_PERIOD_MS);

这行代码背后,其实是:

  • UART 中断
  • 环形缓冲区
  • FreeRTOS 时间片

这也是 ESP‑IDF 真正“值钱”的地方。


七、关于 UART0、日志口和“冲突”的一点经验

ESP32‑S3 默认:

  • UART0 = 日志口

所以你会看到:

  • 启动日志
  • ESP_LOGI 打印
  • 你自己发的数据

全部混在一起。

如果你希望:

  • 一个串口只干业务
  • 一个串口只打日志

那就果断换 UART1 或 UART2

#define UART_NUM (UART_NUM_1)

这一改,世界立刻清净。


八、写在最后

UART 是一个非常“基础”的外设,但在 ESP32‑S3 + FreeRTOS 的体系里,它绝对不是“玩具”。

当你:

  • 用任务拆收发
  • 用缓冲区抗抖动
  • 用队列解耦 ISR 和业务

你会发现:

串口已经不只是调试工具,而是一个标准、可靠的通信通道。

Logo

openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。

更多推荐