学习ESP32—USB CDC 虚拟串口开发指南

ESP32-S3 USB CDC 虚拟串口开发指南

概述

ESP32-S3 内置 USB OTG 外设,配合 ESP-IDF 的 TinyUSB 协议栈,可以轻松实现 USB CDC (Communication Device Class) 虚拟串口功能。PC 通过 USB 连接 ESP32-S3 后,设备会被识别为一个串口,实现双向数据传输。

本文档详细介绍如何使用 ESP32-S3 实现 USB 虚拟串口,并循环发送数据 "12345678"


1. 硬件环境

项目 说明
主控芯片 ESP32-S3
USB 接口 板载 USB OTG (GPIO19/GPIO20)
开发框架 ESP-IDF 5.3.x

2. 软件依赖配置

2.1 添加 TinyUSB 依赖

main/idf_component.yml 中声明依赖:

yaml 复制代码
## IDF Component Manager Manifest File
dependencies:
  espressif/esp_tinyusb: "^1"
  idf: "^5.0"
  • esp_tinyusb 是 Espressif 官方封装的 TinyUSB 组件,简化了 USB 设备开发流程。

2.2 SDK Configuration (menuconfig) 配置

运行 idf.py menuconfig,进入 Component config → TinyUSB Stack,按以下配置:

复制代码
TinyUSB Stack
├── TinyUSB DCD
│   └── [*] Enable DMA mode (CONFIG_TINYUSB_MODE_DMA)
├── TinyUSB task configuration
│   ├── Task Priority: 5
│   └── Task Stack Size: 4096
├── Descriptor configuration
│   └── (使用默认 Espressif VID/PID 或自定义)
└── Communication Device Class (CDC)
    ├── [*] Enable CDC (CONFIG_TINYUSB_CDC_ENABLED)
    ├── CDC Port Count: 1
    ├── RX Buffer Size: 512
    └── TX Buffer Size: 512

核心 sdkconfig 配置项:

ini 复制代码
CONFIG_TINYUSB_MODE_DMA=y
CONFIG_TINYUSB_TASK_PRIORITY=5
CONFIG_TINYUSB_TASK_STACK_SIZE=4096
CONFIG_TINYUSB_CDC_ENABLED=y
CONFIG_TINYUSB_CDC_COUNT=1
CONFIG_TINYUSB_CDC_RX_BUFSIZE=512
CONFIG_TINYUSB_CDC_TX_BUFSIZE=512

3. 工程目录结构

复制代码
33_usb_uart/
├── CMakeLists.txt                 # 顶层 CMake
├── main/
│   ├── CMakeLists.txt             # main 组件 CMake
│   ├── idf_component.yml          # 组件依赖声明
│   ├── main.c                     # 程序入口
│   └── APP/
│       └── USB_UART/
│           ├── tud_usart.h        # USB CDC 头文件
│           └── tud_usart.c        # USB CDC 实现文件
└── sdkconfig                      # 项目配置

4. 代码实现

4.1 main 组件 CMakeLists.txt

main/CMakeLists.txt 中注册源文件路径和头文件路径:

cmake 复制代码
idf_component_register(
    SRC_DIRS
        "."
        "APP"
        "APP/USB_UART"
    INCLUDE_DIRS
        "."
        "APP"
        "APP/USB_UART")

4.2 USB CDC 头文件 (tud_usart.h)

c 复制代码
#ifndef __TUD_USART_H
#define __TUD_USART_H

#include <inttypes.h>
#include "tinyusb.h"
#include "tusb_cdc_acm.h"
#include "sdkconfig.h"
#include "esp_log.h"

/* 函数声明 */
void tud_usb_usart(void);     /* USB 初始化入口 */
void usb_send_data(void);     /* 循环发送数据 */

#endif

关键头文件说明:

头文件 作用
tinyusb.h TinyUSB 驱动安装、配置结构体定义
tusb_cdc_acm.h CDC ACM 类初始化、读写、回调注册

4.3 USB CDC 实现文件 (tud_usart.c)

c 复制代码
#include "tud_usart.h"
#include <string.h>

static const char *TAG = "usb_cdc";

/**
 * @brief   循环发送的数据内容
 */
static const char *send_msg = "12345678";

/**
 * @brief   CDC 接收回调函数
 * @param   itf   : CDC 端口号
 * @param   event : CDC 事件结构体指针
 * @retval  无
 *
 * 当 PC 通过虚拟串口向设备发送数据时,此回调被触发。
 * 函数内部读取收到的数据并打印到日志。
 */
void tinyusb_cdc_rx_callback(int itf, cdcacm_event_t *event)
{
    size_t rx_size = 0;
    uint8_t buf[CONFIG_TINYUSB_CDC_RX_BUFSIZE + 1];

    /* 读取 PC 端发来的串口数据 */
    esp_err_t ret = tinyusb_cdcacm_read(itf, buf,
                    CONFIG_TINYUSB_CDC_RX_BUFSIZE, &rx_size);

    if (ret == ESP_OK)
    {
        ESP_LOGI(TAG, "Received %d bytes from channel %d:", rx_size, itf);
        ESP_LOG_BUFFER_HEXDUMP(TAG, buf, rx_size, ESP_LOG_INFO);

        /* 回显:将收到的数据原样发送回 PC */
        tinyusb_cdcacm_write_queue(itf, buf, rx_size);
        tinyusb_cdcacm_write_flush(itf, 0);
    }
    else
    {
        ESP_LOGE(TAG, "Read error");
    }
}

/**
 * @brief   线路状态变化回调函数
 * @param   itf   : CDC 端口号
 * @param   event : CDC 事件结构体指针
 * @retval  无
 *
 * 当 PC 端打开/关闭串口或改变 DTR/RTS 信号时触发。
 */
void tinyusb_cdc_line_state_changed_callback(int itf, cdcacm_event_t *event)
{
    int dtr = event->line_state_changed_data.dtr;
    int rts = event->line_state_changed_data.rts;
    ESP_LOGI(TAG, "Line state changed: DTR=%d, RTS=%d", dtr, rts);
}

/**
 * @brief   循环发送数据 "12345678" 的任务函数
 * @param   pvParameter : 任务参数(未使用)
 * @retval  无
 *
 * 每 1 秒向 PC 端发送一次 "12345678"。
 */
void usb_send_task(void *pvParameter)
{
    while (1)
    {
        /* 向 CDC 端口 0 发送数据 */
        tinyusb_cdcacm_write_queue(TINYUSB_CDC_ACM_0,
                    (const uint8_t *)send_msg, strlen(send_msg));
        tinyusb_cdcacm_write_flush(TINYUSB_CDC_ACM_0, 0);

        ESP_LOGI(TAG, "Sent: %s", send_msg);

        /* 延时 1 秒 */
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}

/**
 * @brief   USB 设备登记、CDC 初始化和回调注册
 * @param   无
 * @retval  无
 *
 * 此函数完成以下三件事:
 * 1. USB 设备登记 (tinyusb_driver_install)
 * 2. CDC ACM 初始化 (tusb_cdc_acm_init)
 * 3. 回调函数注册 (tinyusb_cdcacm_register_callback)
 */
void tud_usb_usart(void)
{
    ESP_LOGI(TAG, "USB initialization start");

    /* ===== 第1步:USB 设备登记 ===== */
    const tinyusb_config_t tusb_cfg = {
        .device_descriptor = NULL,          /* NULL=使用默认设备描述符 */
        .string_descriptor = NULL,          /* NULL=使用默认字符串描述符 */
        .external_phy = false,              /* 使用内部 USB PHY */
        .configuration_descriptor = NULL,   /* NULL=使用默认配置描述符 */
    };
    ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg));

    /* ===== 第2步:CDC ACM 初始化 ===== */
    tinyusb_config_cdcacm_t acm_cfg = {
        .usb_dev = TINYUSB_USBDEV_0,                    /* USB 设备实例 */
        .cdc_port = TINYUSB_CDC_ACM_0,                  /* CDC 端口号 */
        .rx_unread_buf_sz = 64,                         /* RX 未读缓冲区大小 */
        .callback_rx = &tinyusb_cdc_rx_callback,        /* 接收数据回调 */
        .callback_rx_wanted_char = NULL,                 /* 特殊字符回调(未用) */
        .callback_line_state_changed = NULL,             /* 线路状态回调(在此初始化中置空) */
        .callback_line_coding_changed = NULL             /* 线路编码回调(未用) */
    };
    ESP_ERROR_CHECK(tusb_cdc_acm_init(&acm_cfg));

    /* ===== 第3步:注册线路状态变化回调 ===== */
    /* 注意:line_state_changed 回调需要在 acm_init 之后单独注册 */
    ESP_ERROR_CHECK(tinyusb_cdcacm_register_callback(
                        TINYUSB_CDC_ACM_0,                          /* CDC 端口 */
                        CDC_EVENT_LINE_STATE_CHANGED,                /* 事件类型 */
                        &tinyusb_cdc_line_state_changed_callback));  /* 回调函数 */

    ESP_LOGI(TAG, "USB initialization done");
}

4.4 主函数 (main.c)

c 复制代码
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "nvs_flash.h"
#include "tud_usart.h"

void app_main(void)
{
    esp_err_t ret;

    /* 初始化 NVS (TinyUSB 依赖 NVS 存储) */
    ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES ||
        ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
    {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ESP_ERROR_CHECK(nvs_flash_init());
    }

    /* USB CDC 初始化(登记 + 初始化 + 回调注册) */
    tud_usb_usart();

    /* 创建循环发送任务 */
    xTaskCreate(usb_send_task, "usb_send", 4096, NULL, 5, NULL);
}

5. 工作流程详解

5.1 整体流程图

复制代码
┌─────────────────────────────────────────────────────────┐
│                     app_main()                          │
│                         │                               │
│                   nvs_flash_init()                      │
│                         │                               │
│                   tud_usb_usart()                       │
│                   ┌─────┴─────┐                         │
│                   │           │                         │
│             ① USB设备登记   ② CDC初始化                 │
│           (driver_install)  (acm_init)                  │
│                   │           │                         │
│                   └─────┬─────┘                         │
│                         │                               │
│                   ③ 回调注册                            │
│              (register_callback)                        │
│                         │                               │
│                xTaskCreate()                            │
│              (创建发送任务)                              │
│                         │                               │
│              usb_send_task()                            │
│              ┌─────┴─────┐                              │
│              │  循环发送  │                              │
│              │ "12345678"│                              │
│              │  每1秒1次  │                              │
│              └───────────┘                              │
└─────────────────────────────────────────────────────────┘

5.2 USB 设备登记 (Step 1)

tinyusb_driver_install() 的作用:

  • 初始化 USB 硬件(OTG 控制器、内部 PHY)
  • 注册设备描述符、字符串描述符、配置描述符
  • 创建 TinyUSB 后台处理任务
  • 启用 USB D+ 上拉电阻,通知 Host 有新设备接入
c 复制代码
const tinyusb_config_t tusb_cfg = {
    .device_descriptor = NULL,          // 使用默认描述符
    .string_descriptor = NULL,          // 使用默认字符串
    .external_phy = false,              // 内部 PHY
    .configuration_descriptor = NULL,   // 使用默认配置
};
tinyusb_driver_install(&tusb_cfg);

各字段为 NULL 时,TinyUSB 会使用 menuconfig 中配置的默认值:

  • VID: 0x303A (Espressif)
  • PID: 0x4002
  • Manufacturer: "Espressif Systems"
  • Product: "Espressif Device"

5.3 CDC ACM 初始化 (Step 2)

tusb_cdc_acm_init() 的作用:

  • 配置 CDC 通信端口
  • 设置接收缓冲区大小
  • 绑定接收回调函数(收到数据时自动触发)
c 复制代码
tinyusb_config_cdcacm_t acm_cfg = {
    .usb_dev = TINYUSB_USBDEV_0,                // USB 设备 0
    .cdc_port = TINYUSB_CDC_ACM_0,              // CDC 端口 0
    .rx_unread_buf_sz = 64,                     // RX 缓冲 64 字节
    .callback_rx = &tinyusb_cdc_rx_callback,    // 接收回调
    .callback_rx_wanted_char = NULL,
    .callback_line_state_changed = NULL,
    .callback_line_coding_changed = NULL
};
tusb_cdc_acm_init(&acm_cfg);

5.4 回调注册 (Step 3)

tinyusb_cdcacm_register_callback() 用于注册 CDC 事件回调:

事件类型 触发时机 回调函数
CDC_EVENT_RX 收到数据 callback_rx(在 acm_init 中注册)
CDC_EVENT_LINE_STATE_CHANGED DTR/RTS 状态变化 通过 register_callback 注册
CDC_EVENT_LINE_CODING_CHANGED 波特率等参数变化 通过 register_callback 注册
c 复制代码
tinyusb_cdcacm_register_callback(
    TINYUSB_CDC_ACM_0,
    CDC_EVENT_LINE_STATE_CHANGED,
    &tinyusb_cdc_line_state_changed_callback);

5.5 数据收发

发送数据到 PC
c 复制代码
/* 将数据放入发送队列 */
tinyusb_cdcacm_write_queue(TINYUSB_CDC_ACM_0,
            (const uint8_t *)"12345678", 8);

/* 刷新发送缓冲区,立即发送 */
tinyusb_cdcacm_write_flush(TINYUSB_CDC_ACM_0, 0);
  • write_queue: 将数据放入内部 FIFO 队列,非阻塞
  • write_flush: 将队列中的数据立即发送出去,第二个参数为超时时间(tick),0 表示不等待
从 PC 接收数据
c 复制代码
uint8_t buf[512];
size_t rx_size = 0;

tinyusb_cdcacm_read(itf, buf, sizeof(buf), &rx_size);

接收数据有两种方式:

  1. 回调方式 (推荐):在 callback_rx 中处理收到的数据
  2. 轮询方式 :主动调用 tinyusb_cdcacm_read() 读取

6. 编译与烧录

bash 复制代码
# 1. 进入工程目录
cd 33_usb_uart

# 2. 设置目标芯片
idf.py set-target esp32s3

# 3. 配置 menuconfig(按第 2 节配置)
idf.py menuconfig

# 4. 编译
idf.py build

# 5. 烧录
idf.py -p COMx flash monitor

7. 测试验证

  1. 使用 USB 线连接 ESP32-S3 的 USB OTG 口到 PC

  2. PC 端打开串口调试助手,选择识别到的串口(如 COM3)

  3. 设置波特率(CDC 虚拟串口忽略波特率设置,任意值均可)

  4. 观察接收窗口,每 1 秒收到一次 12345678

    [16:00:00.123] 12345678
    [16:00:01.123] 12345678
    [16:00:02.123] 12345678
    ...

  5. 从 PC 端发送任意数据,ESP32-S3 会将收到的数据回显(在 callback_rx 中实现)


8. API 参考速查

API 函数 功能 说明
tinyusb_driver_install() USB 设备登记 初始化 USB 硬件和协议栈
tusb_cdc_acm_init() CDC ACM 初始化 配置 CDC 端口和接收回调
tinyusb_cdcacm_register_callback() 注册事件回调 注册线路状态/编码变化等回调
tinyusb_cdcacm_read() 读取接收数据 从指定 CDC 端口读取数据
tinyusb_cdcacm_write_queue() 发送数据入队 将数据放入发送队列
tinyusb_cdcacm_write_flush() 刷新发送队列 立即发送队列中的数据

9. 常见问题

Q1: 设备插入后 PC 无法识别?

  • 检查 USB 线是否支持数据传输(非仅充电线)
  • 确认 CONFIG_TINYUSB_CDC_ENABLED=y 已配置
  • 检查 GPIO19/GPIO20 是否被其他外设占用

Q2: 发送数据丢失或乱码?

  • 增大 CONFIG_TINYUSB_CDC_TX_BUFSIZE
  • 确保 write_flush()write_queue() 之后调用

Q3: 接收数据时回调不触发?

  • 确认 callback_rxacm_cfg 中正确设置
  • 检查 PC 端串口是否已打开(DTR 为高电平时回调才会生效)

Q4: 如何自定义 VID/PID?

tinyusb_config_t 中传入自定义的描述符指针:

c 复制代码
const tinyusb_config_t tusb_cfg = {
    .device_descriptor = &my_device_desc,     // 自定义设备描述符
    .string_descriptor = &my_string_desc,     // 自定义字符串描述符
    .configuration_descriptor = NULL,
    .external_phy = false,
};