ESP32-S3开发教程8:UART串口通信
·
本文代码已上传开源仓库:ESP32-S3教学资产:包括了一些在ESP32-S3上的简单工程,基于ESP-IDF框架 - AtomGit | GitCode
本文将基于一段 UART 回显代码,从硬件连接、宏定义、初始化函数、任务逻辑到避坑指南,逐字逐句拆解,帮你彻底搞懂 ESP32-S3 的 UART 串口通信,不仅能 “复制代码跑通”,更能 “灵活修改实现自己的需求”。
一、前置说明
1.1 什么是 UART?
UART(Universal Asynchronous Receiver/Transmitter,通用异步收发传输器)是 ESP32-S3 最常用的串口通信协议,核心特点:
- 异步通信:不需要时钟线,仅用 TX(发送)、RX(接收)、GND(地线)三根线就能通信;
- 全双工:可以同时发送和接收数据;
- 广泛兼容:几乎所有单片机、USB 转 TTL 模块、蓝牙模块都支持 UART。
1.2 这段代码能实现什么功能?
- 回显功能:通过 UART 收到什么数据,就立刻发回什么数据(比如你发 “Hello ESP32”,它就回显 “Hello ESP32”);
- 日志打印:通过
printf把收到的数据打印到 ESP-IDF 的串口监视器(默认 UART0)。
二、接线说明
这段代码用的是UART_NUM_0,而 ESP32-S3 的 UART0 默认通过 USB Type-C 接口连接电脑,无需额外接线!
- 直接用 USB 数据线把 ESP32-S3 开发板和电脑连接即可;
- 电脑会自动识别为串口设备。
三、完善CMakeList.txt
idf_component_register(SRCS "main.c"
PRIV_REQUIRES driver freertos
INCLUDE_DIRS ".")
四、完善main.c
#include "driver/uart.h"
#include "driver/gpio.h" // 必须添加,否则GPIO_NUM_43/44会报错
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "string.h"
#include "stdlib.h" // 必须添加,否则malloc/free会警告
#define UART_PORT_NUM UART_NUM_0 // 选择UART端口(0/1/2)
#define TXD_PIN GPIO_NUM_43 // UART TX引脚
#define RXD_PIN GPIO_NUM_44 // UART RX引脚
#define BUF_SIZE 1024 // 数据缓冲区大小(字节)
// ===================== UART初始化函数 =====================
void uart_init(void) {
// 1. 定义UART配置结构体
uart_config_t uart_config = {
.baud_rate = 115200, // 波特率:115200(最常用)
.data_bits = UART_DATA_8_BITS, // 数据位:8位
.parity = UART_PARITY_DISABLE, // 校验位:无
.stop_bits = UART_STOP_BITS_1, // 停止位:1位
.flow_ctrl = UART_HW_FLOWCTRL_DISABLE, // 硬件流控:无
.source_clk = UART_SCLK_APB, // 时钟源:APB(80MHz)
};
// 2. 应用UART参数配置
uart_param_config(UART_PORT_NUM, &uart_config);
// 3. 设置UART引脚
uart_set_pin(UART_PORT_NUM, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
// 4. 安装UART驱动(必须!否则无法读写数据)
uart_driver_install(UART_PORT_NUM, BUF_SIZE * 2, 0, 0, NULL, 0);
}
// ===================== UART数据处理任务 =====================
void uart_task(void *arg) {
// 1. 分配数据缓冲区内存
uint8_t* data = (uint8_t*)malloc(BUF_SIZE);
if (data == NULL) { // 检查内存分配是否成功
printf("内存分配失败!\n");
vTaskDelete(NULL);
return;
}
while(1) {
// 2. 读取串口数据(阻塞100ms,超时返回0)
int len = uart_read_bytes(UART_PORT_NUM, data, BUF_SIZE, pdMS_TO_TICKS(100));
if(len > 0) {
// 3. 回显数据:把收到的数据发回去
uart_write_bytes(UART_PORT_NUM, (const char*)data, len);
// 4. 打印日志:通过UART0打印收到的数据
printf("收到串口数据:");
for(int i=0; i<len; i++) {
printf("%c", data[i]);
}
printf("\n");
// 5. 清空缓冲区:避免下次读取残留数据
memset(data, 0, BUF_SIZE);
}
// 6. 任务延时10ms:释放CPU资源
vTaskDelay(pdMS_TO_TICKS(10));
}
// 7. 任务退出时释放内存(虽然是无限循环,但规范写法)
free(data);
vTaskDelete(NULL);
}
// ===================== 主函数 =====================
void app_main(void) {
// 1. 初始化UART
uart_init();
// 2. 创建UART数据处理任务
xTaskCreate(
uart_task, // 任务函数
"uart_task", // 任务名称
4096, // 任务栈大小(字节,从2048改为4096,避免栈溢出)
NULL, // 任务参数(无)
10, // 任务优先级(较高,优先处理串口数据)
NULL // 任务句柄(无)
);
}
4.1 头文件说明
#include "driver/uart.h" // UART驱动头文件,必须添加
#include "driver/gpio.h" // GPIO头文件,定义了GPIO_NUM_43/44等宏,必须添加
#include "freertos/FreeRTOS.h" // FreeRTOS核心头文件
#include "freertos/task.h" // FreeRTOS任务头文件
#include "string.h" // 字符串操作头文件,用于memset
#include "stdlib.h" // 标准库头文件,用于malloc/free

4.2 宏定义部分
#define UART_PORT_NUM UART_NUM_0 // 选择UART端口
#define TXD_PIN GPIO_NUM_43 // UART TX引脚
#define RXD_PIN GPIO_NUM_44 // UART RX引脚
#define BUF_SIZE 1024 // 数据缓冲区大小

4.3 UART 初始化函数:uart_init()
这是 UART 能正常工作的核心,分为 4 步:定义配置结构体 → 应用参数 → 设置引脚 → 安装驱动。
步骤 1:定义 UART 配置结构体
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, // 时钟源
};

步骤 2:应用 UART 参数配置
uart_param_config(UART_PORT_NUM, &uart_config);
- 作用:将配置结构体的参数应用到指定的 UART 端口;
- 参数:1.
UART_PORT_NUM:UART 端口号(复用宏定义);2.&uart_config:配置结构体的指针(传地址,加&); - 新手注意:必须在
uart_set_pin()和uart_driver_install()之前调用。
步骤 3:设置 UART 引脚
uart_set_pin(UART_PORT_NUM, TXD_PIN, RXD_PIN, UART_PIN_NO_CHANGE, UART_PIN_NO_CHANGE);
- 作用:将 UART 的 TX、RX、RTS、CTS 引脚绑定到指定的 GPIO;
- 参数:
UART_PORT_NUM:UART 端口号;TXD_PIN:TX 引脚(复用宏定义);RXD_PIN:RX 引脚(复用宏定义);UART_PIN_NO_CHANGE:RTS 引脚(不使用,设为UART_PIN_NO_CHANGE);UART_PIN_NO_CHANGE:CTS 引脚(不使用,设为UART_PIN_NO_CHANGE);- 新手注意:如果用 UART0,且开发板默认通过 USB 连接电脑,
TXD_PIN和RXD_PIN可以设为UART_PIN_NO_CHANGE(使用默认引脚)。
步骤 4:安装 UART 驱动(最容易漏写!)
uart_driver_install(UART_PORT_NUM, BUF_SIZE * 2, 0, 0, NULL, 0);
- 作用:安装 UART 驱动,分配接收和发送缓冲区,必须调用!否则无法读写数据;
- 参数:
UART_PORT_NUM:UART 端口号;BUF_SIZE * 2:接收缓冲区大小(设为数据缓冲区的 2 倍,避免溢出);0:发送缓冲区大小(设为 0,使用默认发送缓冲区);0:队列长度(设为 0,不使用队列);NULL:队列句柄(设为 NULL,不使用队列);0:中断优先级(设为 0,自动分配);- 新手必记:这是最容易漏写的函数!如果漏写,
uart_read_bytes()和uart_write_bytes()会无法正常工作。
4.4 UART数据处理任务
这是任务的核心逻辑,分为 7 步:分配内存 → 读取数据 → 回显数据 → 打印日志 → 清空缓冲区 → 任务延时 → 释放内存。
4.4.1 分配数据缓冲区内存
uint8_t* data = (uint8_t*)malloc(BUF_SIZE);
if (data == NULL) { // 检查内存分配是否成功
printf("内存分配失败!\n");
vTaskDelete(NULL);
return;
}
- 作用:在堆上分配一块内存,用于存储收到的串口数据;
- 参数:
BUF_SIZE:缓冲区大小(复用宏定义); - 返回值:
- 成功:返回指向缓冲区的指针;
- 失败:返回
NULL;
- 新手注意:
- 必须检查
malloc是否成功,避免内存分配失败导致系统崩溃; - 任务退出时必须用
free释放内存(虽然是无限循环,但规范写法)。
- 必须检查
4.4.2 读取串口数据
int len = uart_read_bytes(UART_PORT_NUM, data, BUF_SIZE, pdMS_TO_TICKS(100));
- 作用:从 UART 接收缓冲区读取数据;
- 参数:
UART_PORT_NUM:UART 端口号;data:指向数据缓冲区的指针;BUF_SIZE:最多读取的字节数;pdMS_TO_TICKS(100):超时时间(100ms,阻塞等待 100ms,超时返回 0);
- 返回值:实际读取到的字节数(0 表示超时未收到数据);
- 通俗解释:
- 这个函数会阻塞等待 100ms;
- 如果 100ms 内收到数据,就把数据存到
data缓冲区,返回实际收到的字节数; - 如果 100ms 内没收到数据,就返回 0,继续执行后面的代码。
4.4.3 回显函数
uart_write_bytes(UART_PORT_NUM, (const char*)data, len);
- 作用:把收到的数据通过 UART 发回去(回显);
- 参数:
UART_PORT_NUM:UART 端口号;(const char*)data:指向要发送的数据的指针(强制转换为const char*,因为函数要求);len:要发送的字节数(实际读取到的字节数);
- 返回值:实际发送的字节数;
- 新手注意:必须在
uart_driver_install()之后调用,否则无法发送数据。
4.4.4 打印日志
printf("收到串口数据:");
for(int i=0; i<len; i++) {
printf("%c", data[i]);
}
printf("\n");
- 作用:通过
printf把收到的数据打印到 ESP-IDF 的串口监视器(默认 UART0); - 通俗解释:
- 先打印 “收到串口数据:”;
- 再用循环逐个打印收到的字符;
- 最后打印换行符,方便阅读。
4.4.5 清空缓冲区
memset(data, 0, BUF_SIZE);
- 作用:把数据缓冲区的所有字节设为 0,避免下次读取残留数据;
- 参数:
data:指向缓冲区的指针;0:要设置的值(0);BUF_SIZE:要设置的字节数;
- 新手注意:必须清空缓冲区,否则下次读取时可能会残留上次的数据。
4.4.6 任务延时
vTaskDelay(pdMS_TO_TICKS(10));
- 作用:任务延时 10ms,释放 CPU 资源,让其他任务有机会执行;
- 参数:
pdMS_TO_TICKS(10):延时时间(10ms,转换为 FreeRTOS 的时钟节拍); - 新手注意:必须加任务延时,否则这个任务会一直占用 CPU,导致其他任务无法执行。
4.4.7 任务退出时释放内存
free(data);
vTaskDelete(NULL);
- 作用:释放之前分配的内存,删除任务;
- 新手注意:虽然这个任务是无限循环,不会执行到这里,但这是规范写法,养成好习惯。
4.5 主函数
void app_main(void) {
// 1. 初始化UART
uart_init();
// 2. 创建UART数据处理任务
xTaskCreate(
uart_task, // 任务函数
"uart_task", // 任务名称
4096, // 任务栈大小(字节,从2048改为4096,避免栈溢出)
NULL, // 任务参数(无)
10, // 任务优先级(较高,优先处理串口数据)
NULL // 任务句柄(无)
);
}

五、烧录查看现象
编译烧录后,无需额外接线,仅需要按下键盘上的某一按键,打开终端,即可看到先输出了按下什么键,之后输出一个字符串“收到串口数据:”


更多推荐


所有评论(0)