基于ESP32和TinyUSB实现虚拟U盘功能的完整指南
- 基于ESP32和TinyUSB实现虚拟U盘功能的完整指南
- 摘要
- 引言
- 安装 esp_tinyusb 依赖
- 自定义主机电脑卷标
- USB描述符配置与初始化
- MSC配置与SD卡初始化
- SD卡初始化代码解析
- 文件目录浏览与任务调度
- 完整代码
- 结论
摘要
本文详细介绍了如何利用ESP32与TinyUSB库实现虚拟U盘(Mass Storage Class, MSC)功能。通过对代码示例的解析,读者将了解到如何配置USB描述符、初始化SD卡、实现挂载与卸载操作,并学习如何将代码片段嵌入文章中。本文同时提供完整代码的占位符,方便开发者根据自身需求进行修改和调试。
引言
随着物联网和嵌入式设备的普及,利用ESP32实现多种外设功能已成为开发者的热门选择。本文重点介绍利用TinyUSB库在ESP32平台上构建虚拟U盘的实现方法。通过深入解析代码示例,我们将展示如何将SD卡内容以USB大容量存储设备的形式提供给主机系统。
在这篇文章中,我们将着重讨论以下几个关键点:
- tinyusb依赖的安装:如何安装 esp-tinyusb 依赖。
- U盘卷标的设置:如何自定义U盘驱动器卷标。
- USB描述符的配置:如何为虚拟U盘定义设备、配置和接口描述符。
- SD卡初始化和挂载:如何使用ESP-IDF的SDMMC驱动实现SD卡的初始化,并借助TinyUSB完成挂载。
- 文件目录浏览:如何利用FreeRTOS任务和文件系统接口浏览SD卡中的文件。
这些内容不仅能帮助初学者快速上手,同时对于有经验的开发者来说也能提供优化思路。接下来我们将对代码进行详细剖析。
安装 esp_tinyusb 依赖
打开 esp32 组件仓库 https://components.espressif.com/
搜索 esp_tinyusb
关键字,然后安装 esp_tinyusb 组件
idf.py add-dependency "espressif/esp_tinyusb^1.7.2"
重启 vscode,刷新 sdkconfig 配置项,开启 MSC (Mass Storage Class) 选项。
自定义主机电脑卷标
- 使用
f_setlabel
函数
ESP_ERROR_CHECK(tinyusb_msc_storage_mount(mount_point()));
// f_setlabel label max len less than 11 byte
// ref http://elm-chan.org/fsw/ff/doc/setlabel.html
if (auto ret = f_setlabel("XIONGWEI"); FR_OK != ret) {ESP_LOGI(TAG, "USB setlabel Failed code:%d", ret);
}
ESP_ERROR_CHECK(tinyusb_msc_storage_unmount());
- 使用 autorun.inf 配置文件
使用 autorun.inf
自定义驱动器的卷标(Volume Label)和驱动器图标(Drive Icon)
[AutoRun]
label=My Custom USB
icon=usb.ico
以下是一些关于 AutoRun.inf
文件的权威资源,供进一步参考:
-
Microsoft Documentation (Legacy): AutoRun.inf Entries
该文档详细介绍了AutoRun.inf
文件中的各个条目及其用途,适用于早期版本的 Windows。 -
Microsoft Documentation (Modern): AutoRun Commands
该文档提供了关于AutoRun.inf
文件中命令的现代解释,适用于当前版本的 Windows。 -
Wikipedia: AutoRun.inf
Wikipedia 上的文章提供了AutoRun.inf
文件的概述,包括其历史、用途以及安全方面的考虑。
USB描述符配置与初始化
在代码的开头部分,我们定义了USB设备的描述符信息。描述符是USB设备与主机通信时所必须的元数据。下面代码片段展示了设备描述符的配置:
static tusb_desc_device_t descriptor_config = {.bLength = sizeof(descriptor_config),.bDescriptorType = TUSB_DESC_DEVICE,.bcdUSB = 0x0200,.bDeviceClass = TUSB_CLASS_MISC,.bDeviceSubClass = MISC_SUBCLASS_COMMON,.bDeviceProtocol = MISC_PROTOCOL_IAD,.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,.idVendor = 0x303A, // Espressif VID,用户可根据需求修改.idProduct = 0x4002,.bcdDevice = 0x100,.iManufacturer = 0x01,.iProduct = 0x02,.iSerialNumber = 0x03,.bNumConfigurations = 0x01
};
这段代码设置了ESP32设备的基本信息,包括厂商ID、产品ID以及USB标准版本。
MSC配置与SD卡初始化
为了实现虚拟U盘功能,必须配置MSC(Mass Storage Class)接口和初始化SD卡。以下代码片段展示了如何定义MSC配置描述符:
static uint8_t const msc_fs_configuration_desc[] = {TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, TUSB_DESC_TOTAL_LEN,TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),TUD_MSC_DESCRIPTOR(ITF_NUM_MSC, 0, EDPT_MSC_OUT, EDPT_MSC_IN, 64),
};
在虚拟U盘类 virtual_u_disk
的 mount()
方法中,首先调用 storage_init_sdmmc(handle())
来初始化SD卡,然后配置TinyUSB MSC存储参数。通过注册回调函数 storage_mount_changed_cb
,系统能够实时反馈挂载状态变化。初始化成功后,TinyUSB驱动被安装,并通过 f_setlabel("XI")
设置存储卷标。
SD卡初始化代码解析
esp_err_t storage_init_sdmmc(sdmmc_card_t **card) {sdmmc_host_t host = SDMMC_HOST_DEFAULT();host.slot = SDMMC_HOST_SLOT_0;host.max_freq_khz = SDMMC_FREQ_HIGHSPEED;sd_pwr_ctrl_ldo_config_t ldo_config = {.ldo_chan_id = 4,};sd_pwr_ctrl_handle_t pwr_ctrl_handle = NULL;esp_err_t ret = sd_pwr_ctrl_new_on_chip_ldo(&ldo_config, &pwr_ctrl_handle);if (ret != ESP_OK) {ESP_LOGE(TAG, "Failed to create power control handle");return ret;}host.pwr_ctrl_handle = pwr_ctrl_handle;sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();slot_config.width = 4;slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;sdmmc_card_t *sd_card = (sdmmc_card_t *)malloc(sizeof(sdmmc_card_t));(*host.init)();ESP_ERROR_CHECK(sdmmc_host_init_slot(host.slot, (const sdmmc_slot_config_t *)&slot_config));while (sdmmc_card_init(&host, sd_card)) {ESP_LOGE(TAG, "The detection pin of the slot is disconnected (Insert uSD card). Retrying...");vTaskDelay(pdMS_TO_TICKS(1000));}sdmmc_card_print_info(stdout, sd_card);*card = sd_card;return ESP_OK;
}
这段代码展示了如何配置SDMMC主机、初始化电源控制以及反复尝试初始化SD卡,确保系统在检测不到SD卡时能够持续重试。对于嵌入式系统开发者而言,这种防护机制尤为重要。
文件目录浏览与任务调度
代码中还提供了一个 list_files
方法,用于遍历指定目录中的文件和子目录。该方法利用了标准C库中的 opendir
和 readdir
接口,并结合ESP32的日志打印机制输出结果:
static void list_files(const char *path) {DIR *dir = opendir(path);if (!dir) {ESP_LOGE(TAG, "Failed to open directory: %s", path);return;}struct dirent *entry;while ((entry = readdir(dir)) != NULL) {if (entry->d_type == DT_DIR) {// 处理目录文件ESP_LOGI(TAG, "DIR: %s", entry->d_name);} else if (entry->d_type == DT_REG) {// 处理常规文件ESP_LOGI(TAG, "FILE: %s", entry->d_name);} else {ESP_LOGI(TAG, "UNKNOWN: %s", entry->d_name);}}closedir(dir);
}
该函数不仅提高了代码的可读性,同时也为开发者提供了一个简单的示例,如何在嵌入式系统中实现目录遍历功能,这对于调试和日志记录非常有用。
完整代码
完整代码包括:
- 包含头文件部分
- USB描述符和MSC配置部分
- virtual_u_disk类的定义和方法实现
- SD卡初始化和文件浏览函数
- app_main函数,作为程序入口
#ifdef __cplusplus
extern "C" {
#endif#include "esp_log.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h"#include "tinyusb.h"
#include "tusb_msc_storage.h"#include "diskio_sdmmc.h"
#include "sd_pwr_ctrl_by_on_chip_ldo.h"#include <dirent.h>
#include <string.h>#ifdef __cplusplus
}
#endifstatic constexpr char TAG[] = "usb";/* TinyUSB descriptorsgoto sdkconfig Enable Massive Storage Class (MSC) Option
********************************************************************* */
#define EPNUM_MSC 1
#define TUSB_DESC_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_MSC_DESC_LEN)enum { ITF_NUM_MSC = 0, ITF_NUM_TOTAL };enum {EDPT_CTRL_OUT = 0x00,EDPT_CTRL_IN = 0x80,EDPT_MSC_OUT = 0x01,EDPT_MSC_IN = 0x81,
};static tusb_desc_device_t descriptor_config = {.bLength = sizeof(descriptor_config),.bDescriptorType = TUSB_DESC_DEVICE,.bcdUSB = 0x0200,.bDeviceClass = TUSB_CLASS_MISC,.bDeviceSubClass = MISC_SUBCLASS_COMMON,.bDeviceProtocol = MISC_PROTOCOL_IAD,.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,.idVendor = 0x303A, // This is Espressif VID. This needs to be changed// according to Users / Customers.idProduct = 0x4002,.bcdDevice = 0x100,.iManufacturer = 0x01,.iProduct = 0x02,.iSerialNumber = 0x03,.bNumConfigurations = 0x01};static uint8_t const msc_fs_configuration_desc[] = {// Config number, interface count, string index, total length, attribute,// power in mATUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, TUSB_DESC_TOTAL_LEN,TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),// Interface number, string index, EP Out & EP In address, EP sizeTUD_MSC_DESCRIPTOR(ITF_NUM_MSC, 0, EDPT_MSC_OUT, EDPT_MSC_IN, 64),
};#if (TUD_OPT_HIGH_SPEED)
static const tusb_desc_device_qualifier_t device_qualifier = {.bLength = sizeof(tusb_desc_device_qualifier_t),.bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER,.bcdUSB = 0x0200,.bDeviceClass = TUSB_CLASS_MISC,.bDeviceSubClass = MISC_SUBCLASS_COMMON,.bDeviceProtocol = MISC_PROTOCOL_IAD,.bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE,.bNumConfigurations = 0x01,.bReserved = 0};static uint8_t const msc_hs_configuration_desc[] = {// Config number, interface count, string index, total length, attribute,// power in mATUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, TUSB_DESC_TOTAL_LEN,TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100),// Interface number, string index, EP Out & EP In address, EP sizeTUD_MSC_DESCRIPTOR(ITF_NUM_MSC, 0, EDPT_MSC_OUT, EDPT_MSC_IN, 512),
};
#endif // TUD_OPT_HIGH_SPEEDstatic char const *string_desc_arr[] = {(const char[]){0x09, 0x04}, // 0: is supported language is English (0x0409)"TinyUSB", // 1: Manufacturer"TinyUSB Device", // 2: Product"123456", // 3: Serials"Example MSC", // 4. MSC
};
/*********************************************************************** TinyUSB* descriptors*/class virtual_u_disk {
public:esp_err_t mount() {ESP_ERROR_CHECK(storage_init_sdmmc(handle()));tinyusb_msc_sdmmc_config_t config_sdmmc = {.card = *handle(),.callback_mount_changed =storage_mount_changed_cb,.mount_config{.max_files = 5}};ESP_ERROR_CHECK(tinyusb_msc_storage_init_sdmmc(&config_sdmmc));ESP_ERROR_CHECK(tinyusb_msc_register_callback(TINYUSB_MSC_EVENT_MOUNT_CHANGED, storage_mount_changed_cb));ESP_ERROR_CHECK(tinyusb_msc_storage_mount(mount_point()));// f_setlabel label max len less than 11 byte// ref http://elm-chan.org/fsw/ff/doc/setlabel.htmlif (auto ret = f_setlabel("XIONGWEI"); FR_OK != ret) {ESP_LOGI(TAG, "USB setlabel Failed code:%d", ret);}ESP_ERROR_CHECK(tinyusb_msc_storage_unmount());const tinyusb_config_t tusb_cfg = {.device_descriptor = &descriptor_config,.string_descriptor = string_desc_arr,.string_descriptor_count =sizeof(string_desc_arr) / sizeof(string_desc_arr[0]),.external_phy = false,
#if (TUD_OPT_HIGH_SPEED).fs_configuration_descriptor = msc_fs_configuration_desc,.hs_configuration_descriptor = msc_hs_configuration_desc,.qualifier_descriptor = &device_qualifier,
#else.configuration_descriptor = msc_fs_configuration_desc,
#endif // TUD_OPT_HIGH_SPEED};ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg));ESP_LOGI(TAG, "USB MSC initialization DONE");return ESP_OK;}esp_err_t unmount() {ESP_ERROR_CHECK(tinyusb_msc_storage_unmount());return ESP_OK;}sdmmc_card_t **handle() {static sdmmc_card_t *sdcard_handle{nullptr};return &sdcard_handle;}static void list_files(const char *path) {DIR *dir = opendir(path);if (!dir) {ESP_LOGE(TAG, "Failed to open directory: %s", path);return;}struct dirent *entry;while ((entry = readdir(dir)) != NULL) {if (entry->d_type == DT_DIR) {DIR *subdir = opendir(path);int file_count = 0;if (subdir) {while (readdir(subdir) != NULL) {file_count++;}closedir(subdir);}ESP_LOGI(TAG, "DIR (%d): %s", file_count, entry->d_name);} else if (entry->d_type == DT_REG) {ESP_LOGI(TAG, "FILE: %s", entry->d_name);} else {ESP_LOGI(TAG, "UNKNOWN: %s", entry->d_name);}}closedir(dir);}private:static void storage_mount_changed_cb(tinyusb_msc_event_t *event) {ESP_LOGI(TAG, "Storage mounted to application: %s",event->mount_changed_data.is_mounted ? "Yes" : "No");}esp_err_t storage_init_sdmmc(sdmmc_card_t **card) {sdmmc_host_t host = SDMMC_HOST_DEFAULT();host.slot = SDMMC_HOST_SLOT_0;host.max_freq_khz = SDMMC_FREQ_HIGHSPEED;sd_pwr_ctrl_ldo_config_t ldo_config = {.ldo_chan_id = 4,};sd_pwr_ctrl_handle_t pwr_ctrl_handle = NULL;esp_err_t ret = sd_pwr_ctrl_new_on_chip_ldo(&ldo_config, &pwr_ctrl_handle);if (ret != ESP_OK) {ESP_LOGE(TAG, "Failed to create power control handle");return ret;}host.pwr_ctrl_handle = pwr_ctrl_handle;sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();slot_config.width = 4;slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;sdmmc_card_t *sd_card = (sdmmc_card_t *)malloc(sizeof(sdmmc_card_t));(*host.init)();ESP_ERROR_CHECK(sdmmc_host_init_slot(host.slot, (const sdmmc_slot_config_t *)&slot_config));while (sdmmc_card_init(&host, sd_card)) {ESP_LOGE(TAG, "The detection pin of the slot is disconnected(Insert uSD ""card). Retrying...");vTaskDelay(pdMS_TO_TICKS(1000));}sdmmc_card_print_info(stdout, sd_card);*card = sd_card;return ESP_OK;}const char *mount_point() { return MOUNT_POINT; }static constexpr char MOUNT_POINT[] = "/usb";
};// sdkconfig Enable Massive Storage Class (MSC) Option
// sdkconfig Enable Use FATFS volume label option to call f_setlabel function
extern "C" void app_main(void) {virtual_u_disk usb;usb.mount();
}
结论
本文详细讲解了如何基于ESP32和TinyUSB实现虚拟U盘功能,从USB描述符配置、MSC接口设置,到SD卡的初始化及文件系统的操作,都进行了全面的说明和代码解析。对于希望在嵌入式项目中实现USB大容量存储功能的开发者而言,这篇文章提供了一个从头到尾的实现思路。希望通过本文的学习,读者能够灵活运用这些技术,开发出符合实际需求的产品。