0.briefly speaking

本文的目的旨在学习和深入USB总线协议,并深入分析TinyUSB这个著名的USB协议栈的相关源码,TinyUSB协议栈对于设备端(device)的支持更为全面,这里我们也将更多精力放在设备端的协议实现上

本文以一个ESP-IDF中的例子(tusb_serial_device)为核心展开,并逐步深入到TinyUSB的源码层面。目前ESP-IDF中提供了对TinyUSB开源代码库的官方支持,详情请见TinyUSB 应用指南。目前提供支持的目标只有ESP32-S2/S3/P4,可以在淘宝购买一块对应的开发板,我手头的一块开发板是ESP32S3。

这个例子本质上执行了一个CDC_ACM设备的环回通信,所谓CDC_ACM可以简单理解为一个使用USB总线来模拟经典串口(UART)行为的协议规范。在这个示例中:上位机(PC)首先设备枚举检测到S3的插入动作,随后向S3虚拟出来的ttyACM口发送数据,S3通过USB-Serial-JTAG接收USB总线信号并将其保留在本地缓冲区中,随后再次发回给上位机,上位机会将其回显出来

在开始阅读之前,首先对USB整体的协议栈进行一个简单的概括,这样在深入到具体的代码细节时才不会丢失整体的方向:

一般来说,我们的应用程序通过调用TinyUSB协议栈对外暴露的接口来完成一个USB设备的软件逻辑。而TinyUSB协议栈还可以进一步从上到下分为三层:

  • Class Driver功能类的驱动代码,例如本文的CDC_ACM(虚拟串口)就是一个功能类,U盘(MSD)/键鼠(HID)等也是常见的USB功能,简单可以理解为USB对外展示出的用途和功能,这些功能的实现依赖于USBH/USBD层向上提供的接口。
  • USBD/USBH Core:这一层本质上可以被认为是一个硬件抽象层(Hardware Abstraction Layer, HAL),它主要负责描述一个通用USB设备的通用工作流程,包括枚举相关逻辑、端点(Endpoint)管理、事件分发等。但它不参与具体逻辑功能的实现(Class Driver),也不直接操作特定USB IP的寄存器(DCD/HCD)。
  • DCD/HCD:这一层可以认为是与每个USB IP设计厂商紧耦合的一层驱动代码,它会直接向具体IP的特定寄存器进行读写操作,完成上层发送而来的需求到特定USB IP寄存器操作的映射

请注意这三层之间的关系,它们是可以相互关联和调用的。一方面,从上到下,Class Driver的实现依赖于USBD/USBH提供的USB通用功能接口,而USBD/USBH同样也依赖于DCD/HCD提供的对具体IP寄存器的操作。另一方面,反过来,DCD/HCD层会向上汇报底层USB事件到USBD/USBH层,同时USBD/USBH也会请求Class Driver来完成对底层事件的处理
在这里插入图片描述


Review本文时补充:

一次USB传输过程也可以细分为多个层次,这些知识写在前面有助于提纲挈领:

  1. 传输(transfer): 可以分为Control/Bulk/Interrupt/Isochronous四大类,Control Transfer可以简单理解为传递的是USB的管理平面信息,它独占EP0传递一些有关设备配置、状态管理的信息,Bulk Transfer就可以认为是我们正常情况下说的含确认机制的大批量数据传输。关于这4种传输,可以参考:USB的4种传输
  2. 阶段(stage):对于控制传输而言,一次传输还可以分为SETUP/DATA/STATUS三个阶段,SETUP一般是主机告知从机即将要执行的的控制命令,DATA阶段完成数据的交换,STATUS阶段完成信息的确认和握手对于其他三种类型的传输,不存在阶段这一级分类。
  3. 事务(transactions):最常见可以分类为SETUP(控制传输)/IN(Host读数据)/OUT(Host写数据)三大类,还有SOF/PING/SPLIT事务,一个传输/阶段可以包含一个或多个传输事务
  4. 包(packet):这是传输的最小单位,可以分为Token Packet(SETUP/IN/OUT/SOF), Data Packet(DATA0/DATA1/DATA2/MDATA), Handshake Pakcet(ACK/NAK /STALL/NYET)三大类,一个事务又可以由多个包组成。

举个简单例子,对于GET_DESCRIPTOR命令来说,它的整个通信过程可以分为如下阶段:

Control Transfer: GET_DESCRIPTOR
  ├── SETUP stage
  │   └── SETUP transaction
  │       ├── SETUP token packet
  │       ├── DATA0 packet
  │       └── ACK handshake packet
  │
  ├── DATA stage
  │   └── IN transaction(s)
  │       ├── IN token packet
  │       ├── DATAx packet
  │       └── ACK handshake packet
  │
  └── STATUS stage
      └── OUT transaction
          ├── OUT token packet
          ├── DATA1 ZLP packet
          └── ACK handshake packet

这一段上来看不懂,请先跳过,看完全文后再回来 😃


以下是一些好用的学习资料:

1.主任务及其调用函数详解

/-------------------------------------------------------------------/
// CDC_ACM环回通信主函数
app_main
   // 安装TinyUSB驱动
   tinyusb_driver_install
     // 初始化USB PHY(从略)
     usb_new_phy
     // 初始化tinyusb协议栈任务
     tinyusb_task_start
       // USB设备端任务初始化
       tinyusb_device_task
         // 设置USB设备描述符
         tinyusb_descriptors_set
         // 初始化USB端口为特定角色
         tusb_rhport_init
           // 初始化USB设备端口
           tud_rhport_init
           // Class Driver和USBD Core层初始化
           // DCD层初始化
           dcd_init
         // USBD Core层设备任务:死循环接收DCD层事件并处理
         tud_task_ext
   // 初始化CDC_ACM设备,注册回调函数
   tinyusb_cdcacm_init
  // 接收数据并回显到上位机
/-------------------------------------------------------------------/

void app_main(void)
{
    // Create FreeRTOS primitives
    // 创建一个FreeRTOS队列,专门用来存储回调函数中的消息
    // typedef struct {
    	//	 uint8_t buf[CONFIG_TINYUSB_CDC_RX_BUFSIZE + 1];     // Data buffer
    	//	 size_t buf_len;                                     // Number of bytes received
    	//	 uint8_t itf;                                        // Index of CDC device interface
		// } app_message_t;
    app_queue = xQueueCreate(5, sizeof(app_message_t));
    assert(app_queue);
    app_message_t msg;

    ESP_LOGI(TAG, "USB initialization");
    // 定义并初始化一个默认配置项,这里加载的是TINYUSB_CONFIG_NO_ARG配置
    const tinyusb_config_t tusb_cfg = TINYUSB_DEFAULT_CONFIG();
		
		// [!]: 安装TinyUSB设备驱动并初始化端口
    ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg));

    tinyusb_config_cdcacm_t acm_cfg = {
        .cdc_port = TINYUSB_CDC_ACM_0,
        .callback_rx = &tinyusb_cdc_rx_callback, // the first way to register a callback
        .callback_rx_wanted_char = NULL,
        .callback_line_state_changed = NULL,
        .callback_line_coding_changed = NULL
    };
		
		// 为CDC_ACM设备分配空间并注册回调函数
    ESP_ERROR_CHECK(tinyusb_cdcacm_init(&acm_cfg));
    /* the second way to register a callback */
    ESP_ERROR_CHECK(tinyusb_cdcacm_register_callback(
                        TINYUSB_CDC_ACM_0,
                        CDC_EVENT_LINE_STATE_CHANGED,
                        &tinyusb_cdc_line_state_changed_callback));

// 如果有第二个虚拟串口CDC_ACM,则同样初始化并注册回调函数
#if (CONFIG_TINYUSB_CDC_COUNT > 1)
   // 从略
#endif

    ESP_LOGI(TAG, "USB initialization DONE");
    while (1) {
    	// 阻塞等待,直到收到一条消息
        if (xQueueReceive(app_queue, &msg, portMAX_DELAY)) {
            if (msg.buf_len) {

                /* Print received data*/
                ESP_LOGI(TAG, "Data from channel %d:", msg.itf);
                ESP_LOG_BUFFER_HEXDUMP(TAG, msg.buf, msg.buf_len, ESP_LOG_INFO);

                /* write back */
                // 将得到的信息再直接写回,并直接刷新
                tinyusb_cdcacm_write_queue(msg.itf, msg.buf, msg.buf_len);
                esp_err_t err = tinyusb_cdcacm_write_flush(msg.itf, 0);
                if (err != ESP_OK) {
                    ESP_LOGE(TAG, "CDC ACM write flush error: %s", esp_err_to_name(err));
                }
            }
        }
    }
}

[1] TINYUSB_DEFAULT_CONFIG——加载USB默认配置

写下关于这个宏的解读,一方面是我们得以据此窥见S3上对于USB设备的初始化配置,另一方面是这里面有一个非常精妙的宏使用,值得作为一个USB学习之余的额外收获。我们首先看一下TINYUSB_DEFAULT_CONFIG这个宏的定义:

#define TINYUSB_DEFAULT_CONFIG(...)      GET_CONFIG_MACRO(, ##__VA_ARGS__, \
                                                            TINYUSB_CONFIG_INVALID,    \
                                                            TINYUSB_CONFIG_EVENT_ARG,  \
                                                            TINYUSB_CONFIG_EVENT,      \
                                                            TINYUSB_CONFIG_NO_ARG      \
                                                        )(__VA_ARGS__)
// 无论如何,GET_CONFIG_MACRO都返回第五个入口参数                                                       
#define GET_CONFIG_MACRO(dummy, arg1, arg2, arg3, name, ...)    name               

可以看到在上述的定义中,TINYUSB_DEFAULT_CONFIG本质上是GET_CONFIG_MACRO这个宏的封装,而GET_CONFIG_MACRO雷打不动地返回传入的第五个参数。里面关键之处在于一个特殊的编译器扩展:##__VA_ARGS__,其用法和含义解释如下,也可以参考这篇文章

  • __VA_ARGS__表示传入的所有参数(可变个数)
  • 当__VA_ARGS__不为空时,保留其前面的逗号(comma),并将参数列表原封不动拼在其后
  • 当__VA_ARGS__为空时,连同其前方的逗号(如果存在)一并删除

那么,我们就可以展开一下不同情况下的TINYUSB_DEFAULT_CONFIG宏了:

  1. 当我们不传入任何参数时,展开结果正好是:
// 1.第一层展开
GET_CONFIG_MACRO( ,	TINYUSB_CONFIG_INVALID, TINYUSB_CONFIG_EVENT_ARG, TINYUSB_CONFIG_EVENT, TINYUSB_CONFIG_NO_ARG)()
// 2.第二层展开
TINYUSB_CONFIG_NO_ARG()

很有趣,这里自动对应到了没有参数情况下的默认配置宏TINYUSB_CONFIG_NO_ARG

  1. 当我们传入1个参数arg0时,展开结果就是:
// 1.第一层展开
GET_CONFIG_MACRO( , arg0, TINYUSB_CONFIG_INVALID, TINYUSB_CONFIG_EVENT_ARG, TINYUSB_CONFIG_EVENT, TINYUSB_CONFIG_NO_ARG)(arg0)
// 2.第二层展开
TINYUSB_CONFIG_EVENT(arg0)

可见,随着传入参数个数的变化,这个宏可以灵活地调整应该调用的配置宏,并传入正常的入口参数。更多参数的情况不再展开,整体上来说GET_CONFIG_MACRO这个宏实现了向多种USB配置的自动转发和入口参数传递,代码非常简洁干净,这是##__VA_ARGS__的妙用。

回到正题,在主函数的调用中,对于ESP32S3作为芯片目标的情况,TINYUSB_DEFAULT_CONFIG会最终被展开为TINYUSB_CONFIG_NO_ARG,并最终对应到TINYUSB_CONFIG_FULL_SPEED(NULL, NULL)

// 对于ESP32S3,使用的USB无参默认配置如下:
#define TINYUSB_CONFIG_NO_ARG()                  TINYUSB_CONFIG_FULL_SPEED(NULL, NULL)

// 展开结果如下,其中包含了对于以下字段的配置:
// 1.port: tinyusb协议栈要绑定到的硬件端口
// 2.phy: 对USB PHY的配置
// 3.task: TinyUSB协议栈运行的FreeRTOS任务配置
// 4.descriptor: 描述符配置
// 5.event_cb & event_arg: 回调函数及其调用参数
#define TINYUSB_CONFIG_FULL_SPEED(event_hdl, arg)       \
    (tinyusb_config_t) {                                \
        .port = TINYUSB_PORT_FULL_SPEED_0,              \
        .phy = {                                        \
            .skip_setup = false,                        \
            .self_powered = false,                      \
            .vbus_monitor_io = -1,                      \
        },                                              \
        .task = TINYUSB_TASK_DEFAULT(),                 \
        .descriptor = {                                 \
            .device = NULL,                             \
            .qualifier = NULL,                          \
            .string = NULL,                             \
            .string_count = 0,                          \
            .full_speed_config = NULL,                  \
            .high_speed_config = NULL,                  \
        },                                              \
        .event_cb = (event_hdl),                        \
        .event_arg = (arg),                             \
    }

[1] tinyusb_driver_install——安装TinyUSB驱动

这个函数是esp芯片原厂对TinyUSB协议栈封装得到的,我们在这里将更多注意力放在TinyUSB协议栈本身,而不过多关注PHY的相关初始化动作。这个函数完成的动作可以按顺序归纳如下:

  1. 指定PHY的基础配置,并初始化USB PHY
  2. 启动TinyUSB的软件任务,此后TinyUSB协议栈开始正常执行
  3. 将驱动信息设置到全局
esp_err_t tinyusb_driver_install(const tinyusb_config_t *config)
{
		// TinyUSB和任务配置合法性检查
    ESP_RETURN_ON_ERROR(tinyusb_check_config(config), TAG, "TinyUSB configuration check failed");
    ESP_RETURN_ON_ERROR(tinyusb_task_check_config(&config->task), TAG, "TinyUSB task configuration check failed");

    esp_err_t ret;
    usb_phy_handle_t phy_hdl = NULL;
    // 如果使用ESP32S3的内部USB PHY
    if (!config->phy.skip_setup) {
        // Configure USB PHY
        // 对ESP32S3内部PHY的基础配置
        usb_phy_config_t phy_conf = {
            .controller = USB_PHY_CTRL_OTG, // PHY控制器类型(OTG/USJ)
            .target = USB_PHY_TARGET_INT,	 // PHY的具体型号(FSLS/UTMI/外部 PHY)
            .otg_mode = USB_OTG_MODE_DEVICE, // 模式:设备端(device)
            .otg_speed = USB_PHY_SPEED_FULL, // 速度:全速模式(SPEED_FULL)
        };

// 高速模式下的PHY配置
#if (SOC_USB_OTG_PERIPH_NUM > 1)
        if (config->port == TINYUSB_PORT_HIGH_SPEED_0) {
            // Default PHY for OTG2.0 is UTMI
            phy_conf.target = USB_PHY_TARGET_UTMI;
            phy_conf.otg_speed = USB_PHY_SPEED_HIGH;
        }
#endif // (SOC_USB_OTG_PERIPH_NUM > 1)

        // OTG IOs config
        // 如果是自供电状态,在这里设置vbus_monitor_io
        // vbus_monitor在自供电模式下常用来热插拔的连接检测,这里仅介绍它的基本含义
        // 细节参考:https://docs.espressif.com/projects/esp-iot-solution/zh_CN/release-v2.0/usb/usb_overview/usb_device_self_power.html
        const usb_phy_otg_io_conf_t otg_io_conf = USB_PHY_SELF_POWERED_DEVICE(config->phy.vbus_monitor_io);
        if (config->phy.self_powered) {
            phy_conf.otg_io_conf = &otg_io_conf;
        }
			// 设置并初始化USB的内部PHY,这里不再展开
        ESP_RETURN_ON_ERROR(usb_new_phy(&phy_conf, &phy_hdl), TAG, "Install USB PHY failed");
    }
    // Init TinyUSB stack in task
    // [!]初始化TinyUSB任务,自此TinyUSB就运行在这个FreeRTOS任务中
    ESP_GOTO_ON_ERROR(tinyusb_task_start(config->port, &config->task, &config->descriptor), del_phy, TAG, "Init TinyUSB task failed");

// ESP32P4硬件补丁,专门用来处理vbus_monitor_io的
#if (CONFIG_IDF_TARGET_ESP32P4)
       /* 
    			Due to hardware limitations on ESP32-P4, 
    			VBUS cannot be monitored automatically by the High-Speed USB-OTG peripheral, 
    			so we need to initialize VBUS GPIO monitoring manually.
    			Initialize VBUS monitoring only for High-Speed ports and self-powered devices
 		*/
#endif // CONFIG_IDF_TARGET_ESP32P4
		
		// 设置USB驱动的全局变量
    s_ctx.port = config->port;              // Save the port number
    s_ctx.phy_hdl = phy_hdl;                // Save the PHY handle for uninstallation
    s_ctx.event_cb = config->event_cb;      // Save the event callback
    s_ctx.event_arg = config->event_arg;    // Save the event callback argument
    s_ctx.remote_wakeup_en = false;         // Remote wakeup is disabled by default

    ESP_LOGI(TAG, "TinyUSB Driver installed on port %d", config->port);
    return ESP_OK;

del_phy:
    if (!config->phy.skip_setup) {
        usb_del_phy(phy_hdl);
    }
    return ret;
}

[2] tinyusb_task_start——启动tusb协议栈任务

这个函数本身创建了一个FreeRTOS任务,这个任务上承载着整个TinyUSB协议栈的运行,此函数首先初始化了任务上下文结构体,随后此结构体被作为参数传入任务tinyusb_device_task的创建,这个任务将会初始化TinyUSB协议栈中的USB设备协议

esp_err_t tinyusb_task_start(tinyusb_port_t port, const tinyusb_task_config_t *config, const tinyusb_desc_config_t *desc_cfg)
{
	// tusb描述符合法性检查
    ESP_RETURN_ON_ERROR(tinyusb_descriptors_check(port, desc_cfg), TAG, "TinyUSB descriptors check failed");
		
	// 任务状态合法性检查
    TINYUSB_TASK_ENTER_CRITICAL();
    // Task shouldn't started
    TINYUSB_TASK_CHECK_FROM_CRIT(p_tusb_task_ctx == NULL, ESP_ERR_INVALID_STATE);  
    // Task shouldn't be running   
    TINYUSB_TASK_CHECK_FROM_CRIT(!_task_is_running, ESP_ERR_INVALID_STATE); 
    // Task is running flag, will be cleared in task in case of the error          
    _task_is_running = true;                                                          
    TINYUSB_TASK_EXIT_CRITICAL();

    esp_err_t ret;
    // 分配TinyUSB任务上下文内存,这是ESP引入的自定义结构体
    // 功能上分为
    // 1.TinyUSB协议栈相关的配置
    // 2.任务本身相关的内容
    // /** TinyUSB task context */
		// typedef struct {
		//     /* TinyUSB协议栈相关的配置 */
		// 		// USB Peripheral hardware port number; several peripherals on chip
		//     uint8_t rhport;
		// 		// USB Device RH port init parameters  
		//     tusb_rhport_init_t rhport_init;
		//     // USB Device descriptors configuration pointer 
		//     const tinyusb_desc_config_t *desc_cfg;  
		//     /* 任务本身相关的内容 */
		//     // This TinyUSB task handle
		//     TaskHandle_t handle;
		//     // Parent task notified after stack started  
		//     volatile TaskHandle_t awaiting_handle;
		// } tinyusb_task_ctx_t;
    tinyusb_task_ctx_t *task_ctx = heap_caps_calloc(1, sizeof(tinyusb_task_ctx_t), MALLOC_CAP_DEFAULT);
    if (task_ctx == NULL) {
        return ESP_ERR_NO_MEM;
    }
		
	// 初始化任务上下文结构体内容
	// 当前任务的FreeRTOS任务句柄
	// 后续将作为唤醒对象,等待TinyUSB协议栈初始化完成
    task_ctx->awaiting_handle = xTaskGetCurrentTaskHandle();    
    // TinyUSB 任务尚未启动,先置空任务句柄
    task_ctx->handle = NULL;
    // 外设端口号(roothub port)
    task_ctx->rhport = port;
    // USB角色为设备端(esp_tinyusb 固定工作在 Device 模式)
    task_ctx->rhport_init.role = TUSB_ROLE_DEVICE;   
    // USB接口速度选择(高速/全速)
    task_ctx->rhport_init.speed = (port == TINYUSB_PORT_FULL_SPEED_0) ? 
    								TUSB_SPEED_FULL : TUSB_SPEED_HIGH; 
    // USB描述符设置
    task_ctx->desc_cfg = desc_cfg;
		
	// 将任务上下文作为参数,创建USB device任务
    TaskHandle_t task_hdl = NULL;
    ESP_LOGD(TAG, "Creating TinyUSB main task on CPU%d", config->xCoreID);
		
    // Create a task for tinyusb device stack
    // [!] 创建TinyUSB设备协议栈在指定核上(单核放在核0上,双核放在核1上)
    xTaskCreatePinnedToCore(tinyusb_device_task,
                            "TinyUSB",
                            config->size,
                            (void *) task_ctx,
                            config->priority,
                            &task_hdl,
                            config->xCoreID);
    if (task_hdl == NULL) {
        ESP_LOGE(TAG, "Create TinyUSB main task failed");
        ret = ESP_ERR_NOT_FINISHED;
        goto err;
    }

    // Wait until the Task notify that port is active, 5 sec is more than enough
    // 至多等待5秒,等待TinyUSB协议栈初始化完毕
    if (ulTaskNotifyTake(pdTRUE, pdMS_TO_TICKS(5000)) == 0) {
        ESP_LOGE(TAG, "Task wasn't able to start TinyUSB stack");
        ret = ESP_ERR_TIMEOUT;
        goto err;
    }

    return ESP_OK;

err:
    heap_caps_free(task_ctx);
    return ret;
}

[3] tinyusb_device_task——USB设备端任务

这个函数,顾名思义就是USB Device要执行的任务,首先它完成了USB描述符的设置,随后初始化对应的USB设备端口

/**
 * @brief This top level thread processes all usb events and invokes callbacks
 * 这个顶层线程处理所有USB事件并调用回调函数
 */
static void tinyusb_device_task(void *arg)
{
    tinyusb_task_ctx_t *task_ctx = (tinyusb_task_ctx_t *)arg;

    // Sanity check
    // 合法性检查
    assert(task_ctx != NULL);
    assert(task_ctx->awaiting_handle != NULL);

    ESP_LOGD(TAG, "TinyUSB task started");
		
		// 检查设备是否已经初始化过,是则直接返回
    if (tud_inited()) {
        ESP_LOGE(TAG, "TinyUSB stack is already initialized");
        goto del;
    }
		
		// 设置当前USB设备的多层次(设备、配置、功能、接口、端点)描述符
		// 这一步完成后,枚举阶段Host可以检测到设备的信息和功能
    if (tinyusb_descriptors_set(task_ctx->rhport, task_ctx->desc_cfg) != ESP_OK) {
        ESP_LOGE(TAG, "TinyUSB descriptors set failed");
        goto del;
    }
		
		// 初始化USB设备的硬件端口
		// 这一步完成后Host就可以得知有新设备插入USB总线,进而触发枚举
    if (!tusb_rhport_init(task_ctx->rhport, &task_ctx->rhport_init)) {
        ESP_LOGE(TAG, "Init TinyUSB stack failed");
        goto desc_free;
    }
		
		// 更新任务上下文任务句柄之后将其保留在全局变量中
    TINYUSB_TASK_ENTER_CRITICAL();
    task_ctx->handle = xTaskGetCurrentTaskHandle(); // Save task handle
    p_tusb_task_ctx = task_ctx;                     // Save global task context pointer
    TINYUSB_TASK_EXIT_CRITICAL();
		
		// 向父任务发送信号量,提醒TinyUSB协议栈初始化已完成
    xTaskNotifyGive(task_ctx->awaiting_handle);
		
		// 陷入TinyUSB设备任务的无限循环
    while (1) { // RTOS forever loop
        tud_task();
    }

// 错误处理函数
desc_free:
    tinyusb_descriptors_free();
del:
    TINYUSB_TASK_ENTER_CRITICAL();
    _task_is_running = false;       // Task is not running anymore
    TINYUSB_TASK_EXIT_CRITICAL();
    vTaskDelete(NULL);
    // No return needed here: vTaskDelete(NULL) does not return
}

[4] tinyusb_descriptors_set——设置不同层次描述符

这个函数完成的任务就是设置设备描述符和配置描述符,这两个描述符所处的层次不同。对于一个USB设备(USB Device)来说,从上到下来说可以分为逐级包含的几层

  • 设备(Device):一个连接到USB总线上的设备实体
  • 配置(Configuration):描述USB设备的一组工作模式(供电模式、最大功耗)
  • 接口(Interface):代表设备的一个逻辑功能(例如键盘、摄像头)
  • 端点(Endpoint):设备中的一个数据源和信道,通信的真正落脚点

以上概念从大到小相互包含:端点 ∈ 接口 ∈ 配置 ∈设备,每一个层次都有自己的描述符,某一个配置描述符内部可能还有若干class-specific描述符,这和具体的USB设备功能类别有关。在下面会看到具体的实例:

在接下来的代码中,它依次完成了以下描述符的配置:

  • Device Descriptor(设备描述符)
  • Full-speed Configuration Descriptor(全速配置描述符,这里实际上也包含了interface和endpoint信息)
  • High-speed Configuration Descriptor(高速配置描述符,如果存在)
  • Device Qualifier Descriptor(设备限定描述符,描述当一个高速设备运行在其他模式下的工作模式)
  • String Descriptors(字符串描述符,用于将数字索引映射到人类可读的字符串信息)
esp_err_t tinyusb_descriptors_set(tinyusb_port_t port, const tinyusb_desc_config_t *config)
{
    esp_err_t ret;
    const char **pstr_desc;
    // Flush descriptors control struct
    // 清空描述符结构体
    memset(&s_desc_cfg, 0x00, sizeof(tinyusb_descriptors_map_t));

    // Device Descriptor
    // 设备描述符的配置
    // 设定设备描述符到全局变量s_desc_cfg
    if (config->device == NULL) {
        ESP_LOGW(TAG, "No Device descriptor provided, using default.");
        s_desc_cfg.dev = &descriptor_dev_default;
    } else {
        s_desc_cfg.dev = config->device;
    }

    // Full-speed configuration descriptor
    // 全速模式配置(configuration)描述符的配置
    // 如果没有传入全速描述符
    if (config->full_speed_config == NULL) {
// 但是配置了以下之一,则可以使用默认全速配置:
// 1.CDC(Communication Device Class), USB串口
// 2.MSC(Mass Storage Class), USB硬盘
// 3.NCM(Network Control Model), USB网卡
// 在本示例中,CFG_TUD_CDC == 1
#if (CFG_TUD_CDC > 0 || CFG_TUD_MSC > 0 || CFG_TUD_NCM > 0)
        // We provide default config descriptors only for CDC, MSC and NCM classes
        ESP_LOGW(TAG, "No Full-speed configuration descriptor provided, using default.");
        s_desc_cfg.fs_cfg = descriptor_fs_cfg_default;
#else
        // Default configuration descriptor must be provided via config structure
        ESP_GOTO_ON_FALSE(config->full_speed_config, ESP_ERR_INVALID_ARG, fail, TAG, "Full-speed configuration descriptor must be provided for this device");
#endif
		// 传入全速描述符的情况下,将其设置进全局变量
    } else {
        s_desc_cfg.fs_cfg = config->full_speed_config;
    }


// 当USB_OTG外设的个数大于1时,才需要处理高速模式(High Speed, HS)的情况
// 这里不展开
#if (SOC_USB_OTG_PERIPH_NUM > 1)
    // 这里相关代码逻辑从略
#endif // (SOC_USB_OTG_PERIPH_NUM > 1)

    // Select String Descriptors and count them
    // 这里加载描述符的字符串版本
    // 在USB描述符中记录的都是数字索引,这张表将索引映射到字符串,因此人类易读
    if (config->string == NULL) {
        ESP_LOGW(TAG, "No String descriptors provided, using default.");
        pstr_desc = descriptor_str_default;
        while (descriptor_str_default[++s_desc_cfg.str_count] != NULL);
    } else {
        pstr_desc = config->string;
        s_desc_cfg.str_count = config->string_count;
    }

    ESP_GOTO_ON_FALSE(s_desc_cfg.str_count <= USB_STRING_DESCRIPTOR_ARRAY_SIZE, ESP_ERR_NOT_SUPPORTED, fail, TAG, "String descriptors exceed limit");
    // 将字符串描述符拷贝到全局变量
    memcpy(s_desc_cfg.str, pstr_desc, s_desc_cfg.str_count * sizeof(pstr_desc[0]));
		
		// 打印描述符信息
    ESP_LOGI(...);

    return ESP_OK;

fail:
#if (TUD_OPT_HIGH_SPEED)
    free(s_desc_cfg.other_speed);
#endif // TUD_OPT_HIGH_SPEED
    return ret;
}

A.descriptor_dev_default——设备描述符

这个结构体声明了ESP32S3上的USB设备描述符,标准描述符字段及其含义可以在这里看到:Device Descriptor。这里值得关注的一个地方就是IAD机制(Interface Associate Descriptor),简单来说,IAD可以支持将多个编号连续的接口打包在一起以实现一个完整的逻辑功能。

//------------- Device Descriptor -------------//
const tusb_desc_device_t descriptor_dev_default = {
		// 描述符总长度
    .bLength = sizeof(descriptor_dev_default),
    // 描述符类似: 设备(Device)
    .bDescriptorType = TUSB_DESC_DEVICE,
    // USB协议版本号(USB 2.0.0)
    .bcdUSB = 0x0200,

#if CFG_TUD_CDC
	// 对于CDC这种功能,这里启用了IAD描述符
	// IAD描述符描述了如何将连续几个接口打包成一个功能
	// IAD对应的class/subclass/protocal编号见:https://www.usb.org/defined-class-codes
    // Use Interface Association Descriptor (IAD) for CDC
    // As required by USB Specs IAD's subclass must be common class (2) and protocol must be IAD (1)
    .bDeviceClass = TUSB_CLASS_MISC,
    .bDeviceSubClass = MISC_SUBCLASS_COMMON,
    .bDeviceProtocol = MISC_PROTOCOL_IAD,
#else
		// CDC以外的其他情况则指定为普通设备类,此时类信息从接口描述符中解析
    .bDeviceClass = 0x00,
    .bDeviceSubClass = 0x00,
    .bDeviceProtocol = 0x00,
#endif
		// 控制端点EP0的最大包长度
		// 控制端点专门用来传递一些重要的握手和控制信息
    .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,

// USB厂商ID,可选是不是用ESPRESSIF的
#if CONFIG_TINYUSB_DESC_USE_ESPRESSIF_VID
    .idVendor = TINYUSB_ESPRESSIF_VID,
#else
    .idVendor = CONFIG_TINYUSB_DESC_CUSTOM_VID,
#endif

// 产品ID
#if CONFIG_TINYUSB_DESC_USE_DEFAULT_PID
    .idProduct = USB_TUSB_PID,
#else
    .idProduct = CONFIG_TINYUSB_DESC_CUSTOM_PID,
#endif
		
		// 设备版本号
    .bcdDevice = CONFIG_TINYUSB_DESC_BCD_DEVICE,
		
		// 厂商、产品、序列号索引
		// 据此可以索引对应的字符串描述符
    .iManufacturer = 0x01,
    .iProduct = 0x02,
    .iSerialNumber = 0x03,
		
		// 配置个数:1
    .bNumConfigurations = 0x01
};

B.descriptor_fs_cfg_default——全速配置描述符

这个数组声明了多个描述符,包括配置描述符和一系列不同功能的接口描述符。值得注意的是,一个配置描述符后往往紧跟着多个接口描述符和端点描述符,它们在主机读取时是作为整体一次返回的,而不是多次逐个返回,详见下图的说明:

在这里插入图片描述

uint8_t const descriptor_fs_cfg_default[] = {
    // Configuration number, interface count, string index, total length, attribute, power in mA
    // 配置描述符
    TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, TUSB_DESC_TOTAL_LEN, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),

#if CFG_TUD_CDC
    // Interface number, string index, EP notification address and size, EP data address (out, in) and size.
    TUD_CDC_DESCRIPTOR(ITF_NUM_CDC, STRID_CDC_INTERFACE, 0x80 | EPNUM_0_CDC_NOTIF, 8, EPNUM_0_CDC, 0x80 | EPNUM_0_CDC, 64),
#endif

// 更多不同接口的描述符...
#if CFG_TUD_CDC > 1
   // 从略
#endif

#if CFG_TUD_MSC
   // 从略
#endif

#if CFG_TUD_NCM
   // 从略
#endif

#if CFG_TUD_VENDOR
   // 从略
#endif

#if CFG_TUD_VENDOR > 1
   // 从略
#endif
};

将上述TUD_CONFIG_DESCRIPTOR宏展开之后如下,具体字段含义可以对照Configuration Descriptorsz字段含义

/* bLength:头部长度为9字节 */
9,
/* bDescriptorType: 配置描述符(0x2) */
TUSB_DESC_CONFIGURATION,
/* wTotalLength:整个描述符的数据总长,也就是主机会得到的一包数据的总长 */
U16_TO_U8S_LE(TUSB_DESC_TOTAL_LEN),
/* bNumInterfaces:该配置下包含的接口数量 */
ITF_NUM_TOTAL,
/* bConfigurationValue:配置编号,主机要使用此配置时用这个号来索引当前配置 */
1,
/* iConfiguration:配置字符串描述符索引,0表示没有配置字符串 */
0,
/* bmAttributes:指明USB设备的属性,bit7必须为1,同时声明支持Remote Wakeup */
TU_BIT(7) | TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP,
/* bMaxPower:最大取电电流,单位是2mA*/
100 / 2,

C.TUD_CDC_DESCRIPTOR——接口与端点描述符

当启用CDC功能时,TUD_CDC_DESCRIPTOR宏自动展开为如下若干描述符集合,这其中不仅包含interface和endpoint描述符,还包含大量CDC_ACM设备类的功能描述符,它们都紧跟在配置描述符之后。这是TinyUSB写好的CDC_ACM描述符模板:

// 定义 CDC ACM 描述符模板宏,调用时传入接口号、字符串索引、通知端点、数据端点和包长等参数。
#define TUD_CDC_DESCRIPTOR(_itfnum, _stridx, _ep_notif, _ep_notif_size, _epout, _epin, _epsize) \

	// IAD描述符
  /* Interface Associate */
  8, TUSB_DESC_INTERFACE_ASSOCIATION, _itfnum, 2, TUSB_CLASS_CDC, CDC_COMM_SUBCLASS_ABSTRACT_CONTROL_MODEL, CDC_COMM_PROTOCOL_NONE, 0,\

	// CDC控制接口
  /* CDC Control Interface */
  9, TUSB_DESC_INTERFACE, _itfnum, 0, 1, TUSB_CLASS_CDC, CDC_COMM_SUBCLASS_ABSTRACT_CONTROL_MODEL, CDC_COMM_PROTOCOL_NONE, _stridx,\

	// CDC Header功能描述符
  /* CDC Header */
  5, TUSB_DESC_CS_INTERFACE, CDC_FUNC_DESC_HEADER, U16_TO_U8S_LE(0x0120),\

  // CDC Call Management功能描述符,声明呼叫管理能力及对应数据接口。
  /* CDC Call */
  5, TUSB_DESC_CS_INTERFACE, CDC_FUNC_DESC_CALL_MANAGEMENT, 0, (uint8_t)((_itfnum) + 1),\

  // CDC_ACM功能描述符,声明ACM能力。
  /* CDC ACM: support line request + send break */
  4, TUSB_DESC_CS_INTERFACE, CDC_FUNC_DESC_ABSTRACT_CONTROL_MANAGEMENT, 6,\

  // CDC Union功能描述符,把控制接口和数据接口绑定起来。
  /* CDC Union */
  5, TUSB_DESC_CS_INTERFACE, CDC_FUNC_DESC_UNION, _itfnum, (uint8_t)((_itfnum) + 1),\

  // CDC通知端点描述符,供控制接口向主机发送状态通知。
  /* Endpoint Notification */
  7, TUSB_DESC_ENDPOINT, _ep_notif, TUSB_XFER_INTERRUPT, U16_TO_U8S_LE(_ep_notif_size), 1,\

  // CDC数据接口描述符,声明用于实际串口数据传输的接口。
  /* CDC Data Interface */
  9, TUSB_DESC_INTERFACE, (uint8_t)((_itfnum)+1), 0, 2, TUSB_CLASS_CDC_DATA, 0, 0, 0,\

  // CDC数据OUT端点描述符,主机通过该端点发送数据给设备。
  /* Endpoint Out */
  7, TUSB_DESC_ENDPOINT, _epout, TUSB_XFER_BULK, U16_TO_U8S_LE(_epsize), 0,\

  // 生成 CDC 数据 IN 端点描述符,设备通过该端点发送数据给主机。
  /* Endpoint In */
  7, TUSB_DESC_ENDPOINT, _epin, TUSB_XFER_BULK, U16_TO_U8S_LE(_epsize), 0

将上述的描述符结构简化一下,可以得到下面的结构图。这些描述符和之前的配置描述符打包在一起,一并在主机枚举设备阶段被主机获取到。

Configuration Descriptor
├── IAD Descriptor
├── Interface 0: CDC Control Interface
│   ├── CDC Header Functional Descriptor
│   ├── CDC Call Management Functional Descriptor
│   ├── CDC ACM Functional Descriptor
│   ├── CDC Union Functional Descriptor
│   └── Endpoint Descriptor: Interrupt IN
│       └── Notification Endpoint,设备通知主机串口状态变化
└── Interface 1: CDC Data Interface
    ├── Endpoint Descriptor: Bulk OUT
    │   └── 主机发送串口数据给设备
    └── Endpoint Descriptor: Bulk IN
        └── 设备发送串口数据给主机

[4] tusb_rhport_init——按特定配置初始化USB端口为特定角色

这个函数会根据传入的rh_init配置对特定的USB端口进行初始化,根据USB角色(host/device)的不同,分叉到两个不同的初始化路径

bool tusb_rhport_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) {
  //  backward compatible called with tusb_init(void)
  // 向后兼容无参数的旧式接口tusb_init(void)
  #if defined(TUD_OPT_RHPORT) || defined(TUH_OPT_RHPORT)
  if (rh_init == NULL) {
  		// 从略...并执行初始化
  		// 这里根据配置自动推导出应该初始化的USB端口
  }
  #endif

  // new API with explicit rhport and role
  // 以下是新版接口的调用,新接口里明确指定了要使用的端口及角色(role)
  TU_ASSERT(rhport < TUP_USBIP_CONTROLLER_NUM && rh_init->role != TUSB_ROLE_INVALID);
	// 在全局变量中记录特定端口的角色
  _tusb_rhport_role[rhport] = rh_init->role;
	
	// 设备模式:将特定端口初始化为USB设备
  #if CFG_TUD_ENABLED
  if (rh_init->role == TUSB_ROLE_DEVICE) {
    TU_ASSERT(tud_rhport_init(rhport, rh_init));
  }
  #endif
	
	// 主机模式:将特定端口初始化为USB主机
  #if CFG_TUH_ENABLED
  if (rh_init->role == TUSB_ROLE_HOST) {
    TU_ASSERT(tuh_rhport_init(rhport, rh_init));
  }
  #endif
  return true;
}

[5] tud_rhport_init——初始化USB设备端口

这个函数完成的任务就是对USB Device端的端口进行初始化,这里牵涉到多个层次(Class Driver/USBD/DCD)的相关初始化动作,包括对USBD层一些机制的初始化,对Class Driver的初始化,以及对DCD层的初始化,以下注释中会顺带指明初始化动作所在层次

bool tud_rhport_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) {
	// USBD: 如果USBD层已经初始化完成,则直接返回
  if (tud_inited()) {
    return true; // skip if already initialized
  }
  TU_ASSERT(rh_init);
#if CFG_TUSB_DEBUG >= CFG_TUD_LOG_LEVEL
  // debug的一些信息,从略
#endif
	
	// USBD: 清空整个USB设备状态结构体,防止状态残留
	// 这是一个记录全局状态的变量
	// typedef struct {
	//   struct TU_ATTR_PACKED {
	//     volatile uint8_t connected    : 1;
	//     volatile uint8_t addressed    : 1;
	//     volatile uint8_t suspended    : 1;

	//     uint8_t remote_wakeup_en      : 1; // enable/disable by host
	//     uint8_t remote_wakeup_support : 1; // configuration descriptor's attribute
	//     uint8_t self_powered          : 1; // configuration descriptor's attribute
	//   };
	//   volatile uint8_t cfg_num; // current active configuration (0x00 is not configured)
	//   uint8_t speed;
	//   volatile uint8_t sof_consumer;
	// map interface number to driver (0xff is invalid)
	//   uint8_t itf2drv[CFG_TUD_INTERFACE_MAX];   
	// map endpoint to driver ( 0xff is invalid ), can use only 4-bit each
	//   uint8_t ep2drv[CFG_TUD_ENDPPOINT_MAX][2]; 
	//   tu_edpt_state_t ep_status[CFG_TUD_ENDPPOINT_MAX][2];
	// }usbd_device_t;
  tu_varclr(&_usbd_dev);
  _usbd_queued_setup = 0;
	
	// USBD: 初始化自旋锁
  osal_spin_init(&_usbd_spin);

// USBD: 如果操作系统需要互斥量,则初始化
#if OSAL_MUTEX_REQUIRED
  // Init device mutex
  _usbd_mutex = osal_mutex_create(&_ubsd_mutexdef);
  TU_ASSERT(_usbd_mutex);
#endif

  // Init device queue & task
  // USBD: 初始化设备任务队列
  _usbd_q = osal_queue_create(&_usbd_qdef);
  TU_ASSERT(_usbd_q);

  // Get application driver if available
  // Class Driver: 获取自定义的应用驱动
  _app_driver = usbd_app_driver_get_cb(&_app_driver_count);
  TU_ASSERT(_app_driver_count + BUILTIN_DRIVER_COUNT <= UINT8_MAX);

  // Init class drivers
  // Class Driver: 初始化内置的class driver
  for (uint8_t i = 0; i < TOTAL_DRIVER_COUNT; i++) {
    usbd_class_driver_t const* driver = get_driver(i);
    TU_ASSERT(driver && driver->init);
    TU_LOG_USBD("%s init\r\n", driver->name);
    driver->init();
  }
	
	// 将设备端口号记录在全局变量
  _usbd_rhport = rhport;

  // Init device controller driver
  // DCD: 初始化DCD驱动层,这一层会涉及具体USB IP的初始化
  // 对于esp32s3,这里对应到synopsys的DWC2 USB控制器中配置寄存器的动作
  TU_ASSERT(dcd_init(rhport, rh_init));
	
	// [!]: 这一步很关键,它注册dcd_int_handler为ISR
	// 将DWC2控制器中断与esp32s3的CPU中断关联起来
  dcd_int_enable(rhport);

  return true;
}

[5] dcd_init——DCD层的初始化

这个函数完成了DCD层的初始化动作,对于esp32s3,这里使用的synopsys dwc2 USB控制器,以下的代码主要就是通过读写dwc2 IP中的相关寄存器来完成初始化配置的。以下的代码本质都是配置一些寄存器的动作,这里不再深入探讨寄存器字段的具体细节。

bool dcd_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) {
  (void) rh_init;
  // 根据端口号获取DWC2控制器寄存器基址
  dwc2_regs_t* dwc2 = DWC2_REG(rhport);
	
	// 清空dcd私有数据的状态
  tu_memclr(&_dcd_data, sizeof(_dcd_data));

  // Core Initialization
  // 判断是否为高速模式以及是否开启DMA
  const bool is_highspeed = dwc2_core_is_highspeed(dwc2, TUSB_ROLE_DEVICE);
  const bool is_dma = dma_device_enabled(dwc2);
	
	// 这里主要完成dwc2控制器的公有初始化配置(无关USB角色)
	// PHY的初始化、刷新TX/RX FIFO,关中断等
  TU_ASSERT(dwc2_core_init(rhport, is_highspeed, is_dma));

  //------------- 7.1 Device Initialization -------------//
  // Set device max speed
  //设置设备的最大工作速度(HS/FS)
  uint32_t dcfg = dwc2->dcfg & ~DCFG_DSPD_Msk;
	
	// 全速模式设置
	/* 
		对于一些特定的PHY,需要在从全速切换到高速模式之后延迟一段时间等待PHY稳定再发送数据
	*/
  if (is_highspeed) {
    dcfg |= DCFG_DSPD_HS << DCFG_DSPD_Pos;

    /*
    XCVRDLY: transceiver delay between xcvr_sel and txvalid during device chirp is required 
    when using with some PHYs such as USB334x (USB3341, USB3343, USB3346, USB3347)
    */
    const dwc2_ghwcfg2_t ghwcfg2 = {.value = dwc2->ghwcfg2};
    if (ghwcfg2.hs_phy_type == GHWCFG2_HSPHY_ULPI) {
      dcfg |= DCFG_XCVRDLY;
    }
  // 其他情况下配置FS模式
  } else {
    dcfg |= DCFG_DSPD_FS << DCFG_DSPD_Pos;
  }
	
	// send STALL back and discard if host send non-zlp during control status
	// 当主机在控制传输的status阶段发送非零长度(non-zlp)数据包时,返回STALL并丢弃这个non-zlp
  dcfg |= DCFG_NZLSOHSK; 
  dwc2->dcfg = dcfg;
	
	// 软断开USB设备到总线的连接,后续会重新连接并触发枚举
  dcd_disconnect(rhport);

  // Force device mode
  // 强制dwc2进入设备模式(device mode)
  dwc2->gusbcfg = (dwc2->gusbcfg & ~GUSBCFG_FHMOD) | GUSBCFG_FDMOD;

  // No overrides
  // OTG相关配置
  dwc2->gotgctl &= ~(GOTGCTL_BVALOEN | GOTGCTL_BVALOVAL | GOTGCTL_VBVALOVAL);


#if CFG_TUSB_MCU == OPT_MCU_STM32N6
  // No hardware detection of Vbus B-session is available on the STM32N6
  dwc2->stm32_gccfg |= STM32_GCCFG_VBVALOVAL;
#endif

  // Enable required interrupts
  // 打开一些必要的中断位
  dwc2->gintmsk |= GINTMSK_OTGINT | GINTMSK_USBRST | GINTMSK_ENUMDNEM | GINTMSK_WUIM;

  // TX FIFO empty level for interrupt is complete empty
  // 设置TX FIFO空的条件是完全空
  uint32_t gahbcfg = dwc2->gahbcfg;
  gahbcfg |= GAHBCFG_TX_FIFO_EPMTY_LVL;
  gahbcfg |= GAHBCFG_GINT; // Enable global interrupt
  dwc2->gahbcfg = gahbcfg;
	
	// 重新软连接USB总线,触发主机枚举过程
  dcd_connect(rhport);
  return true;
}

[4] tud_task_ext——USBD层接收DCD层事件并转发处理

这是USB设备端在完成端口初始化之后会陷入的一个永久执行的任务,在tinyusb_device_task它在一个死循环中被调用。这个函数完成的任务就是检查DCD层压入队列的事件,根据事件的类型(event_id)进行分门别类的处理(USB通用逻辑由USBD Core层直接代理,而抽象级别更高的功能层则转发给Class Driver)。而事件本身是当DCD层中断发生时由中断处理程序打包并压入队列的(详见dwc2_esp32.h:dcd_int_handler)

在下面的注释中,我们不展开具体事件的处理细节,而只列出处理的事件列表。

void tud_task_ext(uint32_t timeout_ms, bool in_isr) {
  // 暂未实现从ISR内执行函数的版本
  (void) in_isr;

  // 如果TinyUSB Device栈还没有初始化,则直接返回
  if (!tud_inited()) return;

  // 不断处理事件队列中的事件,直到队列为空或等待超时
  while (1) {
    // 用于接收从DCD层上报到USBD队列的事件。
    dcd_event_t event;

    // 从USBD事件队列中取一个事件,timeout_ms 表示最多等待多久
    if (!osal_queue_receive(_usbd_q, &event, timeout_ms)) return;

#if CFG_TUSB_DEBUG >= CFG_TUD_LOG_LEVEL
    // DEBUG 日志相关内容
#endif

    // 根据DCD上报的事件类型分发处理。
    switch (event.event_id) {
      // 1.RESET事件
      case DCD_EVENT_BUS_RESET:
        // 具体动作从略
        break;
        
      // 2.USB 设备被拔出/断开事件
      case DCD_EVENT_UNPLUGGED:
        // 具体动作从略
        break;
        
      // 3.收到EP0的SETUP包
      // 这通常意味着主机发起了一次控制传输,例如 GET_DESCRIPTOR、SET_ADDRESS、
      case DCD_EVENT_SETUP_RECEIVED:
      	  // 具体动作从略
        break;
        
      // 4.某个endpoint传输完成
      // 这个事件通常由DCD中断处理后投递到 USBD 队列。
      case DCD_EVENT_XFER_COMPLETE:
        // 具体动作从略
        break;
        
      // 5.USB SUSPEND事件。
      // 主机让总线进入挂起,或者总线空闲达到 suspend 条件。
      case DCD_EVENT_SUSPEND:
        // 具体动作从略
        break;
        
      // 6.USB RESUME事件
      case DCD_EVENT_RESUME:
        // 具体动作从略
        break;
      
      // 7.USBD内部延迟函数调用事件
      // 一些ISR中不方便直接执行的函数,可以包装成事件放到队列里
      // 然后在 tud_task_ext() 的任务上下文中执行
      case USBD_EVENT_FUNC_CALL:
        // 具体动作从略
        break;

      // 8.SOF(Start Of Frame)事件 
      case DCD_EVENT_SOF:
        // 具体动作从略
      break;

      // 未知事件,触发断点,说明事件类型异常
      default:
        TU_BREAKPOINT();
        break;
    }

#if CFG_TUSB_OS != OPT_OS_NONE && CFG_TUSB_OS != OPT_OS_PICO
    // 如果当前不是裸机轮询模式,并且事件队列已经空了,
    // 就返回给应用/RTOS,让其他任务有机会运行。
    if (osal_queue_empty(_usbd_q)) { return; }
#endif
  }
}

[2] tinyusb_cdcacm_init——初始化CDC_ACM模型,注册回调函数

这个函数也是由esp封装出来的函数,主要完成的任务也非常简单,就是在TinyUSB定义的基础设施之上为自定义的CDC_ACM设备分配内存空间,并注册回调函数,属于简单的二次封装,细节详见下面注释:

esp_err_t tinyusb_cdcacm_init(const tinyusb_config_cdcacm_t *cfg)
{
    esp_err_t ret = ESP_OK;
    int itf = (int)cfg->cdc_port;
    /* Creating a CDC object */
    // 创建一个CDC对象的配置,这里传入的就是类别码和子类别码
    const tinyusb_config_cdc_t cdc_cfg = {
        .cdc_class = TUSB_CLASS_CDC,
        .cdc_subclass.comm_subclass = CDC_COMM_SUBCLASS_ABSTRACT_CONTROL_MODEL
    };
		
		// 这个名字很唬人,其实它只是在给esp自己的结构体(esp_tusb_cdc_t)分配空间
		// typedef struct {
		//     // CDC 类型,也就是具体的 USB class code。
		//     tusb_class_code_t type;
		//     union {
		//         // 通信设备子类:ACM、ECM 等。
		//         cdc_comm_sublcass_type_t comm_subclass;
		//         // 数据设备只有一个子类,所以默认为0
		//         cdc_data_sublcass_type_t data_subclass;
		//     } cdc_subclass;
		//     // 动态分配的子类专用对象。
		//     void *subclass_obj;
		// } esp_tusb_cdc_t;
    ESP_RETURN_ON_ERROR(tinyusb_cdc_init(itf, &cdc_cfg), TAG, "tinyusb_cdc_init failed");
		
		// typedef struct {
    // 	tusb_cdcacm_callback_t callback_rx;
    // 	tusb_cdcacm_callback_t callback_rx_wanted_char;
    // 	tusb_cdcacm_callback_t callback_line_state_changed;
    // 	tusb_cdcacm_callback_t callback_line_coding_changed;
	  //	} esp_tusb_cdcacm_t; /*!< CDC_ACM object */
	  // 分配回调函数的指针
    ESP_GOTO_ON_ERROR(alloc_obj(itf), fail, TAG, "alloc_obj failed");

    /* Callbacks setting up*/
    // 如果用户提供了对应的回调函数,则在这里完成注册
    if (cfg->callback_rx) {
        tinyusb_cdcacm_register_callback(itf, CDC_EVENT_RX, cfg->callback_rx);
    }
    if (cfg->callback_rx_wanted_char) {
        tinyusb_cdcacm_register_callback(itf, CDC_EVENT_RX_WANTED_CHAR, cfg->callback_rx_wanted_char);
    }
    if (cfg->callback_line_state_changed) {
        tinyusb_cdcacm_register_callback(itf, CDC_EVENT_LINE_STATE_CHANGED, cfg->callback_line_state_changed);
    }
    if (cfg->callback_line_coding_changed) {
        tinyusb_cdcacm_register_callback( itf, CDC_EVENT_LINE_CODING_CHANGED, cfg->callback_line_coding_changed);
    }

    return ESP_OK;
fail:
    tinyusb_cdc_deinit(itf);
    return ret;
}

2.通信过程概述

在上面我们简单对整个CDC_ACM模型的回环通信代码进行了梳理,这一节对整个的执行过程进行总结,首先给出USB设备端的状态机和状态说明。
在这里插入图片描述

  1. 首先,将ESP32S3插入上位机之后,上电复位并启动进入固件,此时ESP32S3虽然已经插入上位机,但注意它并不算是Attached状态,因为此时ESP32S3的DWC2 USB控制器还没有有效上拉D+/D-两根线,所以是Not Attached状态

  2. 随后,ESP32S3开始执行上述流程,在任务的最开始,ESP32S3中的固件初始化了USB PHY,这为后续的USB正常通信提供了基础。

  3. 接下来,ESP32S3启动了一个FreeRTOS任务,这个任务将承载完整的TinyUSB USB设备端任务并连续执行下去。在这个任务中先后完成了:

    1. 描述符的设置: 这一步将设备描述符配置描述符接口描述符功能描述符(可选)端点描述符全部设置到一个全局变量s_desc_cfg中进行保存。
    2. USB设备端口的初始化:在这里完成了USBD Core层,Class Driver层以及DCD层的初始化。值得注意的是在这里DCD层初始化时,有一个先断开USB连接(dcd_disconnect)再重连(dcd_connect)的动作,这个动作完成之后,Host才可以正常识别到USB设备的插入,设备自此进入powered状态
    3. 进入USB Device任务并持续监控:ESP32S3的固件将会进入tud_task_ext任务,这个任务将会持续监控_usbd_q这个队列,并逐个处理队列中的事件。
  4. 主机检测到设备进入powered状态,随后会向ESP32S3发送一个RESET命令,这条命令会触发一个中断(GINTSTS_USBRST),并被dcd_int_handler(see dcd_dwc2.c:dcd_int_handler)接管并处理,dcd_int_handler是DCD层的USB控制器IP需要实现的定制化的中断处理函数,代码片段如下:

if (gintsts & GINTSTS_USBRST) {
    // USBRST is start of reset.
    dwc2->gintsts = GINTSTS_USBRST;

    usbd_spin_lock(true);
    handle_bus_reset(rhport);
    usbd_spin_unlock(true);
  }
  1. 之后,主机会尝试获取设备的描述符(GET_DESCRIPTOR),这是一个经由EP0端口发送而来的控制传输(Control Transfer)。这个命令会触发一个OUTPUT ENDPOINT INT。随后该中断同样被dcd_int_handler接管,并转发到下面的分支。
 // OUT endpoint interrupt handling.
  if (gintsts & GINTSTS_OEPINT) {
    // OEPINT is read-only, clear using DOEPINTn
    handle_ep_irq(rhport, TUSB_DIR_OUT);
  }

在函数handle_ep_irq中,TinyUSB会首先判断是哪个端点出现了中断,随后判断是否为SETUP中断。一般而言控制传输都会由SETUP/DATA/STATUS三个阶段组成,对于GET_DESCRIPTOR这样的命令自然也不例外。

最后,中断处理程序将GET_DESCRIPTOR请求封装成事件,转发到USBD层,详见下面的代码片段(for details, see dcd_dwc2.c:handle_epout_slave)。

if (doepint_bm.setup_phase_done) {
    // Cleanup previous pending EP0 IN transfer if any
    dwc2_dep_t* epin0 = &DWC2_REG(rhport)->epin[0];
    if (epin0->diepctl & DIEPCTL_EPENA) {
      edpt_disable(rhport, 0x80, false);
    }
    dcd_event_setup_received(rhport, _dcd_usbbuf.setup_packet, true);
    return;
  }

而从上述的初始化流程中,我们可以看到USBD层此时正运行在tud_task_ext这样一个任务中持续监测是否有DCD层转发而来的事件。GET_DESCRIPTOR在tud_task_ext的分类中属于DCD_EVENT_SETUP_RECEIVED类型的事件,进而接下来会在process_control_request这个函数中具体处理返回描述符的动作,这样我们之前设置在s_desc_cfg中的描述符就可以被返回给上位机,对应的代码片段如下:

case TUSB_REQ_GET_DESCRIPTOR:
          TU_VERIFY( process_get_descriptor(rhport, p_request) );
        break;
  1. 在获取完基本描述符信息后,host还会紧跟着执行SET_ADDRESS来为USB设备分配地址,以及执行SET_CONFIGURATION命令来选择USB设备运行所处的配置。这两个阶段也都是控制传输,TinyUSB对其的处理策略和GET_DESCRIPTOR很像,都是先经由dcd_int_handler率先处理中断,再注入USBD层的事件队列,最终它们都在process_control_request函数中完成处理。
  2. 完成SET_CONFIGURATION动作之后,收发数据缓冲区也已经就绪,此后DWC2 USB控制器就可以通过GINTSTS_RXFLVL和GINTSTS_OEPINT两个中断配合在dcd_int_handler持续收取数据了。

3.总结

本文以一个ESP32S3的USB设备端的CDC_ACM代码示例作为引入点,对USB设备端的初始化过程以及TinyUSB软件栈的构成进行了分析与学习。以下几个是本文需要关注的要点:

  • TinyUSB是一个分层结构(Class Driver/USBD Core/DCD),它们相互关联和影响,需要清楚每一层主要在做什么,之间如何相互调用。
  • USB设备本身也是一个多层结构,从其描述符的层次(Device/Configuration/Interface/(Function)/Endpoint)便可窥见一斑,需要理解每一个层次描述了USB设备的哪些特性。
  • 需要理解从上电启动开始,USB设备以及TinyUSB软件栈完成了哪些初始化动作,主机何时开始识别并枚举到这个新插入的USB设备
  • USBD Core层以常在任务形式(tud_task_ext)驻留下来,不断检测DCD层是否有事件上报,其中一部分可以由自己直接处理,另外一部分需要上传到抽象程度更高的Class Driver进行处理。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐