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

📅 2026/6/25 16:29:51
学习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-S3USB 接口板载 USB OTG (GPIO19/GPIO20)开发框架ESP-IDF 5.3.x2. 软件依赖配置2.1 添加 TinyUSB 依赖在main/idf_component.yml中声明依赖## IDF Component Manager Manifest Filedependencies:espressif/esp_tinyusb:^1idf:^5.0esp_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_DMAy CONFIG_TINYUSB_TASK_PRIORITY5 CONFIG_TINYUSB_TASK_STACK_SIZE4096 CONFIG_TINYUSB_CDC_ENABLEDy CONFIG_TINYUSB_CDC_COUNT1 CONFIG_TINYUSB_CDC_RX_BUFSIZE512 CONFIG_TINYUSB_CDC_TX_BUFSIZE5123. 工程目录结构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.txtmain/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#includeinttypes.h#includetinyusb.h#includetusb_cdc_acm.h#includesdkconfig.h#includeesp_log.h/* 函数声明 */voidtud_usb_usart(void);/* USB 初始化入口 */voidusb_send_data(void);/* 循环发送数据 */#endif关键头文件说明头文件作用tinyusb.hTinyUSB 驱动安装、配置结构体定义tusb_cdc_acm.hCDC ACM 类初始化、读写、回调注册4.3 USB CDC 实现文件 (tud_usart.c)#includetud_usart.h#includestring.hstaticconstchar*TAGusb_cdc;/** * brief 循环发送的数据内容 */staticconstchar*send_msg12345678;/** * brief CDC 接收回调函数 * param itf : CDC 端口号 * param event : CDC 事件结构体指针 * retval 无 * * 当 PC 通过虚拟串口向设备发送数据时此回调被触发。 * 函数内部读取收到的数据并打印到日志。 */voidtinyusb_cdc_rx_callback(intitf,cdcacm_event_t*event){size_trx_size0;uint8_tbuf[CONFIG_TINYUSB_CDC_RX_BUFSIZE1];/* 读取 PC 端发来的串口数据 */esp_err_trettinyusb_cdcacm_read(itf,buf,CONFIG_TINYUSB_CDC_RX_BUFSIZE,rx_size);if(retESP_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){intdtrevent-line_state_changed_data.dtr;intrtsevent-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_descriptorNULL,/* NULL使用默认设备描述符 */.string_descriptorNULL,/* NULL使用默认字符串描述符 */.external_phyfalse,/* 使用内部 USB PHY */.configuration_descriptorNULL,/* NULL使用默认配置描述符 */};ESP_ERROR_CHECK(tinyusb_driver_install(tusb_cfg));/* 第2步CDC ACM 初始化 */tinyusb_config_cdcacm_tacm_cfg{.usb_devTINYUSB_USBDEV_0,/* USB 设备实例 */.cdc_portTINYUSB_CDC_ACM_0,/* CDC 端口号 */.rx_unread_buf_sz64,/* RX 未读缓冲区大小 */.callback_rxtinyusb_cdc_rx_callback,/* 接收数据回调 */.callback_rx_wanted_charNULL,/* 特殊字符回调(未用) */.callback_line_state_changedNULL,/* 线路状态回调(在此初始化中置空) */.callback_line_coding_changedNULL/* 线路编码回调(未用) */};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)#includefreertos/FreeRTOS.h#includefreertos/task.h#includenvs_flash.h#includetud_usart.hvoidapp_main(void){esp_err_tret;/* 初始化 NVS (TinyUSB 依赖 NVS 存储) */retnvs_flash_init();if(retESP_ERR_NVS_NO_FREE_PAGES||retESP_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_descriptorNULL,// 使用默认描述符.string_descriptorNULL,// 使用默认字符串.external_phyfalse,// 内部 PHY.configuration_descriptorNULL,// 使用默认配置};tinyusb_driver_install(tusb_cfg);各字段为NULL时TinyUSB 会使用menuconfig中配置的默认值VID: 0x303A (Espressif)PID: 0x4002Manufacturer: “Espressif Systems”Product: “Espressif Device”5.3 CDC ACM 初始化 (Step 2)tusb_cdc_acm_init()的作用配置 CDC 通信端口设置接收缓冲区大小绑定接收回调函数收到数据时自动触发tinyusb_config_cdcacm_tacm_cfg{.usb_devTINYUSB_USBDEV_0,// USB 设备 0.cdc_portTINYUSB_CDC_ACM_0,// CDC 端口 0.rx_unread_buf_sz64,// RX 缓冲 64 字节.callback_rxtinyusb_cdc_rx_callback,// 接收回调.callback_rx_wanted_charNULL,.callback_line_state_changedNULL,.callback_line_coding_changedNULL};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_CHANGEDDTR/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: 将队列中的数据立即发送出去第二个参数为超时时间tick0 表示不等待从 PC 接收数据uint8_tbuf[512];size_trx_size0;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 口到 PCPC 端打开串口调试助手选择识别到的串口如 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_ENABLEDy已配置检查 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_descriptormy_device_desc,// 自定义设备描述符.string_descriptormy_string_desc,// 自定义字符串描述符.configuration_descriptorNULL,.external_phyfalse,};