本文代码已上传开源仓库: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_PINRXD_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 接收缓冲区读取数据;
  • 参数
    1. UART_PORT_NUM:UART 端口号;
    2. data:指向数据缓冲区的指针;
    3. BUF_SIZE:最多读取的字节数;
    4. 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 发回去(回显);
  • 参数
    1. UART_PORT_NUM:UART 端口号;
    2. (const char*)data:指向要发送的数据的指针(强制转换为const char*,因为函数要求);
    3. 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,避免下次读取残留数据;
  • 参数
    1. data:指向缓冲区的指针;
    2. 0:要设置的值(0);
    3. 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          // 任务句柄(无)
    );
}

五、烧录查看现象

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

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐