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);
接收数据有两种方式:
- 回调方式 (推荐):在
callback_rx中处理收到的数据 - 轮询方式 :主动调用
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. 测试验证
-
使用 USB 线连接 ESP32-S3 的 USB OTG 口到 PC
-
PC 端打开串口调试助手,选择识别到的串口(如 COM3)
-
设置波特率(CDC 虚拟串口忽略波特率设置,任意值均可)
-
观察接收窗口,每 1 秒收到一次
12345678[16:00:00.123] 12345678
[16:00:01.123] 12345678
[16:00:02.123] 12345678
... -
从 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_rx在acm_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,
};