第四十四章 ESP32S3 USB 虚拟串口(Slave)实验
本章,学习如何利用 USB 在开发板实现一个 USB 虚拟串口,通过 USB 与电脑数据数据交互。
本章分为如下几个小节:
44.1 USB 虚拟串口简介
44.2 硬件设计
44.3 程序设计
44.4 下载验证
44.1 USB 虚拟串口简介
USB 虚拟串口,简称 VCP,是 Virtual COM Port 的简写,它是利用 USB 的 CDC 类来实现的一种通信接口。 CDC(Communication Device Class)类是 USB2.0 标准下的一个设备类,定义了通信相关设备的抽象集合。
利用 ESP32 自带的 USB 功能,来实现一个 USB 虚拟串口,从而通过 USB,实现电脑与 ESP32 的数据互传。上位机无需编写专门的 USB 程序,只需要一个串口调试助手即可调试。
44.2 硬件设计
44.2.1 例程功能
本实验利用 ESP32 自带的 USB 功能,连接电脑 USB,虚拟出一个 USB 串口,实现电脑和开发板的数据通信。本例程功能完全 第十三章 ESP32S3 UART 实验,只不过串口变成了 ESP32 的USB 虚拟串口。当 USB 连接电脑(USB 线插入 USB_SLAVE 接口),开发板将通过 USB 和电脑建立连接,并虚拟出一个串口。
LED 闪烁,提示程序运行。 USB 和电脑连接成功后。
44.2.2 硬件资源
1. LED 灯
LED -IO0
2.独立按键
KEY0(XL9555) - IO1_7
KEY1(XL9555) - IO1_6
KEY2(XL9555) - IO1_5
KEY3(XL9555) - IO1_4
3. XL9555
IIC_SDA-IO41
IIC_SCL-IO42
4. SPILCD
CS-IO21
SCK-IO12
SDA-IO11
DC-IO40(在 P5 端口,使用跳线帽将 IO_SET 和 LCD_DC 相连)
PWR- IO1_3(XL9555)
RST- IO1_2(XL9555)
5. UART_NUM_0(U0TX、 U0RX 连接至板载 USB 转串口芯片上)
U0TXD-IO43
U0RXD-IO44
6. USB
44.2.3 原理图
本章实验使用 USB 接口与 PC 进行连接,开发板板载了一个 USB 接口,用于连接其他 USB
设备, USB 接口与 MCU 的连接原理图,如下图所示:
图 44.2.3.1 USB 接口与 MCU 的连接原理图
44.3 程序设计
44.3.1 程序流程图
本实验的程序流程图:
图 44.3.1.1 USB 虚拟串口实验程序流程图
44.3.2 USB 虚拟串口函数解析
ESP-IDF 提供了一套 API 来配置 USB。要使用此功能,需要导入必要的头文件:
#include "tinyusb.h"
#include "tusb_cdc_acm.h"
接下来,看一些常用的ESP32-S3中的USB函数,这些函数的描述及其作用如下:
(1)USB 设备登记
该函数用给定的配置,来配置 USB 设备,整个功能的初始化入口,必须在任何其他函数之前调用。该函数原型如下所示:
esp_err_t tinyusb_driver_install(const tinyusb_config_t *config);
该函数的形参描述如下表所示:
参数 | 描述 |
---|---|
config | Tinyusb 堆栈特定配置 |
表 44.3.2.1 tinyusb_driver_install ()函数形参描述
该函数使用 tinyusb_config_t 类型的结构体变量传入,该结构体的定义如下所示:
结构体 | 成员变量 | 可选参数 |
---|---|---|
tinyusb_config_t | device_descriptor | 指向设备描述符的指针,例程设置为NULL |
string_descriptor | 指向字符串描述符数组的指针,例程设置为 NULL | |
external_phy | USB 应该使用外部 PHY,例程设置为false | |
configuration_descriptor | 指向配置描述符的指针,例程设置为NULL |
表 44.3.2.2 i2s_pin_config_t 结构体参数值描述
完成上述结构体参数配置之后,可以将结构传递给 tinyusb_driver_install () 函数,用以安装USB 驱动。
注意:此函数只需调用一次,通常放在 app_main
的开头。它不专门针对 CDC ACM,而是为所有 TinyUSB 功能(CDC、MSC、HID 等)提供基础环境。
(2)USB 设备初始化
该函数用给定的配置,来初始化 USB 设备,设置 CDC ACM(虚拟串口)的特定参数,如 RX/TX 缓冲区大小、端口号等,该函数原型如下所示:
esp_err_t tusb_cdc_acm_init(const tinyusb_config_cdcacm_t *cfg);
该函数的形参描述如下表所示:
参数 | 描述 |
---|---|
cfg | 指向 USB 设备初始化配置结构体 |
表 44.3.2.3 tusb_cdc_acm_init ()函数形参描述
该函数使用 tinyusb_config_cdcacm_t 类型的结构体变量传入,该结构体的定义如下所示:
结构体 | 成员变量 | 可选参数 |
---|---|---|
tinyusb_config_cdcacm_t | usb_dev | USB 设备 |
cdc_port | CDC 端口 | |
rx_unread_buf_sz | 配置 RX 缓冲区大小 | |
callback_rx | 接收回调函数 | |
callback_rx_wanted_char | 指向' tusb_cdcacm_callback_t '类型 的函数的指针,该函数将作为回调处理,例程中配置为 NULL | |
callback_line_state_changed | 同上,例程中配置为 NULL | |
callback_line_coding_changed | 同上,例程中配置为 NULL |
表 44.3.2.4 tinyusb_config_cdcacm_t 结构体参数值描述
完成上述结构体参数配置之后,可以将结构传递给 tinyusb_driver_install () 函数,用以安装USB 驱动。
(3)注册回调函数
用于为 CDC-ACM(通信设备类-抽象控制模型)设备注册事件回调函数。当特定的 CDC-ACM 事件(如接收到数据、线路状态改变等)发生时,注册的回调函数会被自动调用,从而实现事件驱动的编程模型。该步骤用以注册回调函数,该函数原型如下所示:
esp_err_t tinyusb_cdcacm_register_callback(tinyusb_cdcacm_t *hdl,cdcacm_event_type_t event,tusb_cdcacm_callback_t callback
);
该函数的形参描述如下表所示:
参数 | 类型 | 说明 |
---|---|---|
|
| CDC-ACM 设备的句柄,用于指定操作哪个 CDC 端口(在多端口设备中尤为重要)。 |
|
| 要监听的事件类型。这是一个枚举值,指定哪种事件会触发回调。 |
|
| 事件回调函数指针。当指定事件发生时,这个函数会被调用。 |
表 44.3.2.5 tinyusb_cdcacm_register_callback ()函数形参描述
event
参数决定回调函数将对哪些事件做出响应。以下是常见的事件类型:
事件类型 | 枚举值(示例) | 触发条件与用途 |
---|---|---|
数据接收事件 |
| 当主机(如电脑)通过虚拟串口发送数据到此设备时触发。这是最常用的事件,用于接收数据。 |
线路状态变化事件 |
| 当主机的串口软件状态改变时触发,例如 DTR(数据终端就绪) 或 RTS(请求发送) 信号变化。常用于检测主机是否打开串口,以决定是否开始通信或进入低功耗模式。 |
线路编码变化事件 |
| 当主机更改串口参数时触发,如波特率、数据位、停止位、校验位发生改变。你的设备应据此调整内部串口配置。 |
发送完成事件 |
| 当一批数据成功发送给主机后触发。可用于流控,确保不会发生数据覆盖。 |
表 44.3.2.6 tinyusb_cdcacm_register_callback ()函数event
形参描述
使用注意事项:
- 中断上下文:回调函数通常在中断服务程序上下文中被调用。因此,在回调函数内部应避免执行耗时的操作(如长时间循环、阻塞式延迟),也不要调用如 printf这类非线程安全的函数。最佳实践是将数据存入队列,然后在主循环中处理;
- 多端口支持:如果设备配置了多个CDC端口(例如 CFG_TUD_CDC = 2),注册回调时需要指定正确的端口句柄(如 TINYUSB_CDC_ACM_0或 TINYUSB_CDC_ACM_1)。回调函数中的 itf参数也会指明事件来自哪个端口;
- 函数名称差异:请注意,不同平台或TinyUSB的封装层可能使用略微不同的函数名,例如 tinyusb_cdcacm_register_callback是ESP-IDF中常见的封装,而在更底层的TinyUSB API中,可能会直接使用像 tud_cdc_rx_cb这样的全局回调。请以你所用的具体开发环境和库文档为准。
(4)发送数据 1
将数据放入内部的发送 FIFO 队列,准备发送给主机。此函数不会立即触发数据发送,它只是将数据缓存起来。该函数原型如下所示:
size_t tinyusb_cdcacm_write_queue(tinyusb_cdcacm_itf_t itf,const uint8_t *in_buf,size_t in_size);
该函数的形参描述如下表所示:
参数 | 描述 |
---|---|
itf | CDC 对象的编号 |
in_buf | 源数组 |
in_size | 从 SRC 数组写入的大小 |
表 44.3.2.7 tinyusb_cdcacm_write_queue ()函数形参描述
使用注意事项:采用“队列”机制,高效且非阻塞,调用会立即返回,适合在中断或高速循环中调用。
该函数的返回值描述,如下表所示:
返回值 | 描述 |
---|---|
ESP_OK | 成功 |
ESP_ERR_NO_MEM | 表示队列已满,无法放入更多数据 |
表 44.3.2.8 函数 tinyusb_cdcacm_write_queue ()返回值描述
(5)发送数据 2
该函数从写缓冲区发送所有数据,此函数与 write_queue
配合使用。立即尝试将发送队列中的所有数据推送到 USB 总线,发送给主机。它会阻塞等待,直到所有排队的数据都被底层 USB 栈取走(注意:这不等于主机已经接收,只是送到了 USB 控制器)。该函数原型如下所示:
esp_err_t tinyusb_cdcacm_write_flush(tinyusb_cdcacm_itf_t itf,uint32_t timeout_ticks);
该函数的形参描述如下表所示:
参数 | 描述 |
---|---|
itf | CDC 对象的编号 |
timeout_ticks | 等待刷新将被视为失败 |
表 44.3.2.9 tinyusb_cdcacm_write_flush ()函数形参描述
使用注意事项:
- 此函数是阻塞的,会等待一段时间,因此需要谨慎选择超时值,避免长时间阻塞其他任务;
- 调用 write_queue后,必须调用 write_flush才能确保数据被真正发送出去。如果不调用flush,数据可能会在队列中停留很久,直到缓冲区满或后续有其他操作触发发送。
最佳实践:对于数据发送,总是成对使用 write_queue
和 write_flush
。为了提高效率,可以多次调用 queue
累积数据,然后一次调用 flush
统一发送,以减少频繁 USB 传输的开销。
44.3.3 USB 虚拟串口驱动解析
在 IDF 版的StandardExampleIDF(v5.3.x)\33_usb_uart,在33_usb_uart\main\APP\USB_UART路径下新增了tud_usart.c和tud_usart.h两个USB 驱动文件。主要实现函数为void tud_usb_usart(void)函数,函数实现比较简单,可以自行研究。
图 44.3.3.1 USB 虚拟串口工程分组
上图中位于 components 文件夹下的是我们编写的一些外设驱动, main 文件夹下包含了一个 APP 文件与一个后缀为.yml 的文件。 APP 文件夹下包含的是 USB 模拟串口代码,而后缀为.yml 的文件其主要作用是将项目中各组件的依赖项定义在单独的清单文件中,并以上图所示的方式进行命名。在例程中提现出的作用就是简化了整个工程结构。在编译的过程中 , 系统便会帮我们自动生成USB外设所需要的依赖库 :espressif_esp_tinyusb 以及 espressif_tinyusb。 做到了即能简化项目工程,又能有效规避了在编译中遇到的错误,但前提是运行时得确保个人的电脑处于联网状态。
44.3.4 CMakeLists.txt 文件
打开本实验 main 文件下的 CMakeLists.txt 文件,其内容如下所示:
idf_component_register(SRC_DIRS ".""APP""APP/USB_UART"INCLUDE_DIRS ".""APP""APP/USB_UART")
44.3.5 实验应用代码
打开 main/main.c 文件,该文件定义了工程入口函数,名为 app_main。该函数代码如下。
/*** @brief 程序入口* @param 无* @retval 无*/
void app_main(void)
{esp_err_t ret;uint8_t x = 0;ret = nvs_flash_init(); /* 初始化NVS */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());}led_init(); /* LED初始化 */my_spi_init(); /* SPI初始化 */myiic_init(); /* MYIIC初始化 */xl9555_init(); /* XL9555初始化 */spilcd_init(); /* SPILCD初始化 *//* 显示实验信息 */spilcd_show_string(30, 50, 200, 16, 16, "ESP32-S3", RED);spilcd_show_string(30, 70, 200, 16, 16, "USB USART TEST", RED);spilcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);tud_usb_usart(); /* USB初始化 */while(1){LED0_TOGGLE();vTaskDelay(pdMS_TO_TICKS(500));}
}
代码比较简单,通过 tud_usb_usart()等函数初始化USB,在该函数中需要注册一个调用 CDC事件的回调函数。此时,如果回调已经注册,那么它将会被覆盖。同时, LCD显示实验信息, LED 闪烁以示程序正在运行。
44.4 下载验证
本例程的测试,不需要安装特定的 USB 驱动,只需用数据线将 USB 接口( 不是UART接口) 与 PC端连接起来即可,并打开串口助手,选择对应的端口号进行数据发送操作。
打开设备管理器(我用的是 WIN10),在端口(COM 和 LPT)里面可以发现多出了一个COM6 的设备,这就是 USB 虚拟的串口设备端口,如图 44.3.1 所示:
图 44.4.1 通过设备管理器查看 USB 虚拟的串口设备端口
如图 44.4.1, ESP32 通过USB虚拟的串口,被电脑识别,端口号为:COM6(可变),字符串名字为:USB 串行设备(COM8)。此时,开发板的 LED 闪烁,提示程序运行,如下图所示:
图 44.4.2 USB 虚拟串口连接成功
打开 XCOM,选择 COM8(需根据自己的电脑识别到的串口号选择),并打开串口(注意:波特率可以随意设置),就可以进行测试了,如图 44.4.3 所示:
图 44.4.3 ESP32 虚拟串口通信测试
可以看到,按发送按钮,可以收到电脑发送给 ESP32 的数据(原样返回),说明实验是成功的。