ESP32‑S3 串口:从“能打印日志”到真正用在业务里
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结构 |
|---|---|
![]() |
![]() |
二、ESP‑IDF 中使用 UART 的“官方正确姿势”
在 ESP‑IDF 里,官方把 UART 的使用拆成了非常清晰的三步:
- 配置串口参数
- 设置串口管脚
- 安装驱动程序
这个顺序不是随便定的,而是踩坑踩出来的。
我一开始也试过:
- 不装驱动直接
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 和业务
你会发现:
串口已经不只是调试工具,而是一个标准、可靠的通信通道。
openvela 操作系统专为 AIoT 领域量身定制,以轻量化、标准兼容、安全性和高度可扩展性为核心特点。openvela 以其卓越的技术优势,已成为众多物联网设备和 AI 硬件的技术首选,涵盖了智能手表、运动手环、智能音箱、耳机、智能家居设备以及机器人等多个领域。
更多推荐



所有评论(0)