学习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中声明依赖:
## IDF Component Manager Manifest Filedependencies: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 配置项:
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=5123. 工程目录结构
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中注册源文件路径和头文件路径:
idf_component_register( SRC_DIRS "." "APP" "APP/USB_UART" INCLUDE_DIRS "." "APP" "APP/USB_UART")4.2 USB CDC 头文件 (tud_usart.h)
#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"/* 函数声明 */voidtud_usb_usart(void);/* USB 初始化入口 */voidusb_send_data(void);/* 循环发送数据 */#endif关键头文件说明:
| 头文件 | 作用 |
|---|---|
tinyusb.h | TinyUSB 驱动安装、配置结构体定义 |
tusb_cdc_acm.h | CDC ACM 类初始化、读写、回调注册 |
4.3 USB CDC 实现文件 (tud_usart.c)
#include"tud_usart.h"#include<string.h>staticconstchar*TAG="usb_cdc";/** * @brief 循环发送的数据内容 */staticconstchar*send_msg="12345678";/** * @brief CDC 接收回调函数 * @param itf : CDC 端口号 * @param event : CDC 事件结构体指针 * @retval 无 * * 当 PC 通过虚拟串口向设备发送数据时,此回调被触发。 * 函数内部读取收到的数据并打印到日志。 */voidtinyusb_cdc_rx_callback(intitf,cdcacm_event_t*event){size_trx_size=0;uint8_tbuf[CONFIG_TINYUSB_CDC_RX_BUFSIZE+1];/* 读取 PC 端发来的串口数据 */esp_err_tret=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 信号时触发。 */voidtinyusb_cdc_line_state_changed_callback(intitf,cdcacm_event_t*event){intdtr=event->line_state_changed_data.dtr;intrts=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"。 */voidusb_send_task(void*pvParameter){while(1){/* 向 CDC 端口 0 发送数据 */tinyusb_cdcacm_write_queue(TINYUSB_CDC_ACM_0,(constuint8_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) */voidtud_usb_usart(void){ESP_LOGI(TAG,"USB initialization start");/* ===== 第1步:USB 设备登记 ===== */consttinyusb_config_ttusb_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_tacm_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)
#include"freertos/FreeRTOS.h"#include"freertos/task.h"#include"nvs_flash.h"#include"tud_usart.h"voidapp_main(void){esp_err_tret;/* 初始化 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 有新设备接入
consttinyusb_config_ttusb_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 通信端口
- 设置接收缓冲区大小
- 绑定接收回调函数(收到数据时自动触发)
tinyusb_config_cdcacm_tacm_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注册 |
tinyusb_cdcacm_register_callback(TINYUSB_CDC_ACM_0,CDC_EVENT_LINE_STATE_CHANGED,&tinyusb_cdc_line_state_changed_callback);5.5 数据收发
发送数据到 PC
/* 将数据放入发送队列 */tinyusb_cdcacm_write_queue(TINYUSB_CDC_ACM_0,(constuint8_t*)"12345678",8);/* 刷新发送缓冲区,立即发送 */tinyusb_cdcacm_write_flush(TINYUSB_CDC_ACM_0,0);write_queue: 将数据放入内部 FIFO 队列,非阻塞write_flush: 将队列中的数据立即发送出去,第二个参数为超时时间(tick),0 表示不等待
从 PC 接收数据
uint8_tbuf[512];size_trx_size=0;tinyusb_cdcacm_read(itf,buf,sizeof(buf),&rx_size);接收数据有两种方式:
- 回调方式(推荐):在
callback_rx中处理收到的数据 - 轮询方式:主动调用
tinyusb_cdcacm_read()读取
6. 编译与烧录
# 1. 进入工程目录cd33_usb_uart# 2. 设置目标芯片idf.py set-target esp32s3# 3. 配置 menuconfig(按第 2 节配置)idf.py menuconfig# 4. 编译idf.py build# 5. 烧录idf.py-pCOMx flash monitor7. 测试验证
- 使用 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中传入自定义的描述符指针:
consttinyusb_config_ttusb_cfg={.device_descriptor=&my_device_desc,// 自定义设备描述符.string_descriptor=&my_string_desc,// 自定义字符串描述符.configuration_descriptor=NULL,.external_phy=false,};