当前位置: 首页 > news >正文

基于ESP32和TinyUSB实现虚拟U盘功能的完整指南

基于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) 选项。
在这里插入图片描述

自定义主机电脑卷标

  1. 使用 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());
  1. 使用 autorun.inf 配置文件

在这里插入图片描述

使用 autorun.inf 自定义驱动器的卷标(Volume Label)和驱动器图标(Drive Icon)

[AutoRun]
label=My Custom USB
icon=usb.ico

以下是一些关于 AutoRun.inf 文件的权威资源,供进一步参考:

  1. Microsoft Documentation (Legacy): AutoRun.inf Entries
    该文档详细介绍了 AutoRun.inf 文件中的各个条目及其用途,适用于早期版本的 Windows。

  2. Microsoft Documentation (Modern): AutoRun Commands
    该文档提供了关于 AutoRun.inf 文件中命令的现代解释,适用于当前版本的 Windows。

  3. 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_diskmount() 方法中,首先调用 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库中的 opendirreaddir 接口,并结合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);
}

该函数不仅提高了代码的可读性,同时也为开发者提供了一个简单的示例,如何在嵌入式系统中实现目录遍历功能,这对于调试和日志记录非常有用。


完整代码

完整代码包括:

  1. 包含头文件部分
  2. USB描述符和MSC配置部分
  3. virtual_u_disk类的定义和方法实现
  4. SD卡初始化和文件浏览函数
  5. 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
}
#endif

static constexpr char TAG[] = "usb";

/* TinyUSB descriptors
   goto 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 mA
    TUD_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 size
    TUD_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 mA
    TUD_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 size
    TUD_MSC_DESCRIPTOR(ITF_NUM_MSC, 0, EDPT_MSC_OUT, EDPT_MSC_IN, 512),
};
#endif // TUD_OPT_HIGH_SPEED

static 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.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());

    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大容量存储功能的开发者而言,这篇文章提供了一个从头到尾的实现思路。希望通过本文的学习,读者能够灵活运用这些技术,开发出符合实际需求的产品。

相关文章:

  • Android实践开发制作小猴子摘桃小游戏
  • Qt 线程和 QObjects
  • [新闻.AI]国产大模型新突破:阿里开源 Qwen2.5-VL-32B 与 DeepSeek 升级 V3 模型
  • rosbag|ROS中.bag数据包转换为matlab中.mat数据类型
  • [250325] Claude AI 现已支持网络搜索功能!| ReactOS 0.4.15 发布!
  • libc2.27版本下的tcache
  • 【CXX-Qt】2.2 生成的 QObject
  • java项目之基于ssm的高校共享单车管理系统(源码+稳定)
  • 数据结构与算法-数据结构-线段树1(单点修改,区间查询):最大数,你能回答这些问题吗
  • XSS漏洞靶场
  • C++对象数组的使用
  • Web前端之UniApp、Taro、ReactNative和Flutter的区别
  • Python Django系列—多数据库
  • 信息集译码算法(ISD)
  • C++11QT复习 (五)
  • Lambda 和 Stream 从 0 到 1,从基础到实战
  • Go 语言当中的反射机制
  • 数据结构--二叉树--其一
  • 搭建k8s集群的可观测体系(log和metric)(已踩完坑)
  • 常用的几种思维方式
  • 网站服务器时间在哪里查询/如何优化关键词搜索排名
  • 做网站平方根怎么表示/seo快速软件
  • 成都建设网站哪家好/医院网络销售要做什么
  • html商务网站模板/上海宝山网站制作
  • 龙岗网站建设服务/如何做好线上推广
  • 网站设计学习/前端seo是什么意思