使用原生的 tinyusb 进行开发

[English]

本指南包含以下内容:

本篇将会介绍如何使用 tinyusb 的组件进行开发。

工程目录

首先需要建立以下目录结构:

project_name
|
|-- main
|   |-- CMakeLists.txt
|   |-- idf_component.yml
|   |-- main.c
|
|-- tusb
    |-- tusb_config.h
    |-- usb_descriptors.c
    |-- usb_descriptors.h

在该工程的 main 组件中添加组件依赖,espressif/tinyusb

tusb 文件夹主要放置用于反向提供给 tinyusb 的文件,通过单独放置一个文件夹中,保证依赖关系的简单。然后需要在 main/CMakeLists.txt 文件中添加以下语句(放置于 idf_component_register 之后)

# espressif__tinyusb 应匹配当前依赖的 tinyusb 名称
idf_component_get_property(tusb_lib espressif__tinyusb COMPONENT_LIB)

target_include_directories(${tusb_lib} PUBLIC "${COMPONENT_DIR}/tusb")
target_sources(${tusb_lib} PUBLIC "${COMPONENT_DIR}/tusb/usb_descriptors.c")

备注

关于反向依赖问题,因为工程需要依赖 tinyusb,有需要向 tinyusb 提供头文件,所以不可避免的会导致反向依赖的问题。目前可以通过将 tinyusb 的全部关键文件作为源码直接编译到 main 组件中来解决。

tusb_config.h 文件

tinyusb 大部分的功能的启用和关闭都是通过宏来控制,所以需要在 tusb_config.h 文件中声明所需要的功能。下面列出一些关键的宏:

系统设置的宏

  • CFG_TUSB_RHPORT0_MODE:用于定义连接到 USB Phy 的方式和速率,下面的方式表示是 USB device 设备,并且速率为USB 全速。

    #define CFG_TUSB_RHPORT0_MODE    (OPT_MODE_DEVICE | OPT_MODE_FULL_SPEED)
    
  • ESP_PLATFORM:使用 esp-idf 平台进行编译,需启用该宏。

  • CFG_TUSB_OS:用于定义 tinyusb 的操作系统,如果使用的是 FreeRTOS,需要启用该宏。也可以不启用操作系统

    #define CFG_TUSB_OS           OPT_OS_FREERTOS
    
  • CFG_TUSB_OS_INC_PATH:在 ESP-IDF 中要求需要添加 “freertos/” 前缀在 include 路径中。

    #define CFG_TUSB_OS_INC_PATH   freertos/
    
  • CFG_TUSB_DEBUG:用于启用 tinyusb 的 LOG 打印等级。总共三级

    #define CFG_TUSB_DEBUG         0
    
  • CFG_TUD_ENABLED:设为 1 启用 tinyusb device 功能。

  • CFG_TUSB_MEM_SECTION:通过启用该宏,可以将 tinyusb 的内存分配到特定的内存段中。

  • CFG_TUSB_MEM_ALIGN:用于定义内存对齐方式。

    #define CFG_TUSB_MEM_ALIGN      __attribute__ ((aligned(4)))
    

USB 设备的宏

  • CFG_TUD_ENDPOINT0_SIZE:用于定义端点 0 的最大包大小。

USB Class 的宏

这里以 UVC Class 举例,每一个 USB Class 都有单独的宏定义:

  • CFG_TUD_VIDEO:配置视频控制接口(video control interface)的数量

  • CFG_TUD_VIDEO_STREAMING:配置视频流接口(video streaming interface)的数量

可以参考以下文件示例:

usb_descriptors.h 文件(可选)

该文件主要用来放置自定义的 USB 描述符。tinyusb 提供了很多描述符的模板,如果不满足需求,就需要自己定义一套 USB 描述符。需要注意的是尽量使用 tinyusb 中预定义好的一些描述符,这样可以很方便的进行描述符组装和计算长度。

可以参考以下文件示例:

usb_descriptors.c 文件

该文件主要实现了几个获取描述符的弱函数,分别是获取设备描述符,或者配置描述符和获取字符串描述符。

uint8_t const *tud_descriptor_device_cb(void);

uint8_t const *tud_descriptor_configuration_cb(uint8_t index);

uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid);

注意点:

  • 配置描述符的长度一定要等于实际的长度

  • 配置描述符使用的各个端点描述符的端点号要避免重复

可以参考以下文件示例:

初始化 USB Phy

初始化内部 USB Phy:

static void usb_phy_init(void)
{
    // Configure USB PHY
    usb_phy_config_t phy_conf = {
        .controller = USB_PHY_CTRL_OTG,
        .otg_mode = USB_OTG_MODE_DEVICE,
        .target = USB_PHY_TARGET_INT,
    };
    usb_new_phy(&phy_conf, &s_uvc_device.phy_hdl);
}

如果使用外部 USB Phy, 参考 使用外部 PHY

初始化 TinyUSB 协议栈

使用以下的代码

static void tusb_device_task(void *arg)
{
    while (1) {
        tud_task();
    }
}

int main(void) {
    usb_phy_init();
    bool usb_init = tusb_init();
    if (!usb_init) {
        ESP_LOGE(TAG, "USB Device Stack Init Fail");
        return ESP_FAIL;
    }
    xTaskCreatePinnedToCore(tusb_device_task, "TinyUSB", 4096, NULL, 5, NULL, 0);
}

实现设备层的弱函数

可以获取设备的插入,拔出,暂停,恢复等事件。

// Invoked when device is mounted
void tud_mount_cb(void)
{
}

// Invoked when device is unmounted
void tud_umount_cb(void)
{
}

// Invoked when device is suspended
void tud_suspend_cb(bool remote_wakeup_en)
{
}

// Invoked when usb bus is resumed
void tud_resume_cb(void)
{
}

实现 USB Class 的特殊的回调函数。

USB Class 提供了一些弱函数来完成基本的功能,接下来会以 UVC 驱动为例。源码文件 video device

通过观察 API 可以发现 UVC Class 提供了两个函数和一个回调函数,

bool tud_video_n_streaming(uint_fast8_t ctl_idx, uint_fast8_t stm_idx);

bool tud_video_n_frame_xfer(uint_fast8_t ctl_idx, uint_fast8_t stm_idx, void *buffer, size_t bufsize);

TU_ATTR_WEAK void tud_video_frame_xfer_complete_cb(uint_fast8_t ctl_idx, uint_fast8_t stm_idx);

通过调用 tud_video_n_frame_xfer 函数来传输一帧图像,并通过 tud_video_frame_xfer_complete_cb 来检查是否传输完成。

此外不同的 USB Class 还会有一些特殊的宏定义,用于定义软件 fifo 大小或者启用一些功能。比如 UVC Class 中的宏 CFG_TUD_VIDEO_STREAMING_EP_BUFSIZE 用于定义视频传输流(video streaming interface)端点的 buffer 的大小。