第四十五章 ESP32S3 Flash 模拟 U 盘实验 - 教程
        本章学习 ESP32S3 的 USB HOST 应用,即通过 USB HOST 功能,将某个分区表实现模拟 U 盘/读卡器等大容量 USB 存储设备。
         本章分为如下几个小节:
 45.1 Flash 模拟 U 盘简介
 45.2 硬件设计
 45.3 程序设计
 45.4 下载验证
45.1 Flash 模拟 U 盘简介
        所谓 Flash 模拟 U 盘,就类似于我们平常使用的 U 盘, 我们只不过是将单片机与电脑通过USB 数据线进行连接,从而进行数据传输。 电脑能够识别出单片机通过外部 Flash 模拟出的 U盘,在电脑上能够对该 U 盘进行文件的相互拷贝,并且重新上电后数据不丢失。通过对 USB 的了解, USB 分设备(Device)模式和主机(Host)模式,使用单片机模拟 U 盘是让 USB 工作在设备(Device)模式下。
         可以利用 ESP32 自带的 USB 功能,来实现一个 Flash 模拟 U 盘,从而通过 USB,实现电脑与 ESP32 的数据互传。上位机无需编写专门的 USB 程序,只需要一个串口调试助手即可调试,非常实用。
45.2 硬件设计
45.2.1 例程功能
        本实验利用 ESP32自带的 USB功能,通过 USB连接电脑后,子分区会在电脑上进行加载,并显示该子分区的容量,可测试子分区数据的读写了。
         LED 闪烁,提示程序运行, USB 和电脑连接成功。
45.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-IO45
         6. USB
45.2.3 原理图
本章实验使用 USB 接口与 PC 进行连接,开发板板载了一个 USB 接口,用于连接其他 USB设备, USB 接口与 MCU 的连接原理图,如下图所示:

图 45.2.3.1 USB 接口与 MCU 的连接原理图
45.3 程序设计
45.3.1 程序流程图
本实验的程序流程图:

图 45.3.1.1 Flash 模拟 U 盘实验程序流程图
45.3.2 Flash 模拟 U 盘函数解析
功能实现前提要设置分区表,如下所示,大小为4M:

图45.3.2.1 分区表设置
ESP-IDF 提供了一套 API 来配置 Flash。要使用此功能,需要导入必要的头文件:
#include "ff.h"
#include "diskio.h"
#include "esp_vfs_fat.h"
#include "tinyusb.h"
接下来,介绍一些常用的 ESP32-S3 中的 Flash 函数,这些函数的描述及其作用如下:
(1)初始化磨损均衡层
wl_mount的核心功能:初始化磨损均衡层,并将其绑定到一个特定的 SPI Flash 分区上。        作用:是提供一个抽象层,让上层应用(如FAT文件系统)可以将 SPI Flash 视为一个可靠的、耐用的块设备来读写,而无需关心 Flash 存储器的物理特性和磨损问题。
         该函数原型如下所示:
esp_err_t wl_mount(const esp_partition_t *partition, wl_handle_t *out_handle);
该函数的形参描述如下表所示:
| 参数 | 类型 | 描述 | 
|---|---|---|
| partition (输入参数) | const esp_partition_t * | 指向一个 SPI Flash 分区的指针。 使用  注意: 该分区通常应该在   | 
| out_handle(出参) | wl_handle_t * | 函数成功执行后,会通过这个指针返回一个磨损均衡句柄。 这个句柄的作用: 它是一个不透明的标识符,后续所有与这个磨损均衡块设备的交互(如读取、写入、卸载)都需要使用这个句柄。可以将其理解为打开了这个“虚拟块设备”后的一个“文件描述符”。  | 
表45.3.2.1 wl_mount()函数形参描述
| 返回值 | 描述 | 
|---|---|
| ESP_OK | 挂载成功。 | 
| 其他错误码 | 挂载失败,可能的原因包括:分区无效、初始化失败、内存不足等。 | 
表45.3.2.2 wl_mount()函数返回值描述
- 函数实现流程和底层原理:
 
        1)识别分区:函数接收一个指定的 Flash 分区。
         2)检查元数据:它读取该分区起始位置的元数据。这些元数据是磨损均衡层自己写入的,用于记录块映射表、状态等信息。
         3)初始化映射表:
              如果分区是全新的(没有元数据),它会初始化一套新的元数据,建立逻辑块地址 (LBA) 到物理块地址 (PBA) 的映射。
              如果分区是已使用过的,它会加载现有的元数据到内存中,重建映射关系。
         4)返回句柄:提供一个 wl_handle_t,后续的 wl_read、wl_write、wl_unmount等函数都依赖这个句柄来操作正确的磨损均衡实例。
- 它如何实现“磨损均衡”?
 
        写操作转移:当您请求写入某个逻辑扇区时,wl_write函数(需要配合 wl_handle使用)并不会直接擦除并写入旧的物理位置。而是会寻找一个新的、空闲的物理扇区进行写入,并更新元数据中的映射关系,将您的逻辑扇区指向这个新的物理位置;
         平均磨损:通过这种方式,对同一逻辑地址的多次写操作会被分散到Flash的不同物理地址上,从而避免了某个特定物理扇区被频繁擦写而过早损坏。
        总结:wl_mount是为 Flash 穿上了一件“防弹衣”,让它可以承受频繁的数据写入而不至于很快“受伤”(损坏)。而返回的 wl_handle就是操作这件“防弹衣”的遥控器。
        在ESP-IDF (v5.0+) 中,更推荐使用 esp_vfs_fat_spiflash_mount 这个高级函数(将查找目标分区、挂载磨损均衡层灯步骤整合),这里不讨论。本次使用wl_mount()函数实现,代码实现封装了一层,上层调用storage_init_spiflash()函数。
(2)将 SPI Flash 分区注册为 TinyUSB MSC 的存储单元
        函数功能告诉 TinyUSB MSC 驱动程序,使用哪个 SPI Flash 分区作为“U盘”的存储介质。调用后,TinyUSB 就知道了从哪里读取数据、向哪里写入数据。
它的主要作用是:
- 提供访问底层存储的句柄:告诉 MSC 驱动使用哪个已经初始化好的磨损均衡(Wear Levelling)区域;
 - 设置文件系统行为:指定如果磁盘被主机(电脑)格式化后,ESP32 这边重新挂载文件系统时的参数;
 - 注册事件回调:允许应用程序在磁盘被主机挂载或卸载时得到通知。
 
函数原型 (注意非IDF标准库函数):
esp_err_t tinyusb_msc_storage_init_spiflash(
    const tinyusb_msc_spiflash_config_t *config
)
该函数的形参结构体如下所示:
typedef struct {
    wl_handle_t wl_handle;                          /*!< Pointer to spiflash wera-levelling handle */
    tusb_msc_callback_t callback_mount_changed;     /*!< Pointer to the function callback that will be delivered AFTER mount/unmount operation is successfully finished */
    tusb_msc_callback_t callback_premount_changed;  /*!< Pointer to the function callback that will be delivered BEFORE mount/unmount operation is started */
    const esp_vfs_fat_mount_config_t mount_config; /*!< FATFS mount config */
} tinyusb_msc_spiflash_config_t;
具体解释如下:
        1)wl_handle_t(通常定义为 int)
         作用:这是最重要的参数。它是一个磨损均衡层(Wear Levelling)的句柄,标识了 MSC 驱动应该使用哪一块 SPI Flash 区域作为它的存储空间。
如何获取:这个句柄需要您事先通过 wl_mount()或更高级的 esp_vfs_fat_spiflash_mount()函数初始化好并获取到。它代表了已经成功挂载、准备好进行块读写操作的一个虚拟磁盘。
         要求:必须是一个有效的、已挂载的 WL 句柄。传递 WL_INVALID_HANDLE会导致初始化失败。
        2)tusb_msc_callback_t callback_mount_changed
类型:函数指针 typedef void (*tusb_msc_callback_t)(tinyusb_msc_event_t *event)
作用:挂载状态变化回调函数。当 USB 主机(电脑)挂载或卸载该磁盘时,TinyUSB MSC 驱动会调用这个函数。
         使用场景:非常适合用于在主机连接时暂停某些本地文件操作,或在主机断开后恢复本地日志记录等功能。
         事件参数:回调函数会接收一个 tinyusb_msc_event_t指针,其 type成员为 TINYUSB_MSC_EVENT_MOUNT_CHANGED,并通过 mount_changed_data.is_mounted告知当前是挂载(true)还是卸载(false)。
3)tusb_msc_callback_t callback_premount_changed
类型:函数指针(同上)
         作用:预挂载状态变化回调函数。这个回调在挂载状态即将发生变化之前被调用。
         使用场景:比 callback_mount_changed更早地获知状态变化,可以用来进行一些准备工作,例如确保所有本地文件都已关闭,以避免主机和 ESP32 同时访问文件系统造成冲突。
         事件参数:同样接收 tinyusb_msc_event_t事件,类型为 TINYUSB_MSC_EVENT_PREMOUNT_CHANGED。
4)const esp_vfs_fat_mount_config_t mount_config
类型:esp_vfs_fat_mount_config_t结构体,本次只关注max_files成员。
         作用:这个配置决定了当 USB 主机(例如您的电脑)卸载磁盘后,ESP32 是否可以以及如何将同一块 SPI Flash 区域作为 FAT 文件系统重新挂载到自己的 VFS(虚拟文件系统)上以供自己访问。
         成员:int max_files,当 ESP32 本地重新挂载 FATFS 时,允许同时打开的最大文件数量。这会影响 FATFS 驱动分配的内存大小。
特别注意:不能让 ESP32 的 VFS 和 USB 主机同时挂载并访问同一个 Flash 分区,这会导致数据损坏。通常的做法是初始化 MSC 前,先卸载本地的 VFS 挂载(调用esp_vfs_fat_unmount函数),本次实验暂时先不修改。
(3)动态注册事件回调函数
        用于动态注册事件回调函数的接口,它允许你在 TinyUSB MSC(大容量存储设备)初始化之后,灵活地为其各种事件(如挂载状态改变)绑定或更换处理函数。
         这与在初始化配置结构体 tinyusb_msc_spiflash_config_t中静态设置回调函数是互为补充的两种方式。
         静态设置:在调用 tinyusb_msc_storage_init_spiflash时通过配置结构体传入。适合在初始化阶段就确定好的回调。
         动态注册:在初始化之后的任意时刻,调用 tinyusb_msc_register_callback来注册、更换或移除回调。适合根据程序运行状态动态管理事件响应。
函数原型:
esp_err_t tinyusb_msc_register_callback(tinyusb_msc_event_type_t event_type,
                                        tusb_msc_callback_t callback)
函数参数说明:
| 参数 | 含义 | 取值 | 
|---|---|---|
event_type  | 指定要为哪种类型的 事件注册回调函数。  | TINYUSB_MSC_EVENT_MOUNT_CHANGED: USB主机(如电脑)挂载或卸载了磁盘。这是最常用的事件。 挂载状态即将发生改变之前的事件。用于执行一些准备工作。  | 
callback  | 指向事件发生时被调用的函数。 如果传入 NULL,则效果是注销 之前为该事件注册的任何回调函数  | 一个  (类型、挂载状态、用户上下文等)。  | 
表45.3.2.3 tinyusb_msc_register_callback()函数形参描述
| 返回值 | 描述 | 
|---|---|
| ESP_OK | 注册成功 | 
| ESP_ERR_INVALID_ARG | 参数错误,例如传入了无效的 event_type | 
| 其他错误码 | 注册过程中发生内部错误 | 
表45.3.2.4 tinyusb_msc_register_callback()函数返回值描述
        典型使用场景与代码示例(具体实现可以百度):
         1)场景:动态响应USB磁盘的插拔
         设备平时会将日志写入SPI Flash。当用户把设备当作U盘插入电脑时,你希望暂停日志记录,防止电脑和MCU同时写文件导致冲突。当用户安全弹出磁盘后,再恢复日志记录。
         2)场景:在运行时改变回调行为
         设备可能有不同的“模式”。在“配置模式”下,希望当USB磁盘被挂载时弹出配置页面;在“数据记录模式”下,只希望暂停记录。
        重要注意事项:
         1)回调函数的执行上下文:这些回调函数通常在TinyUSB的任务上下文中被调用。意味着:
              不要在其中执行冗长或阻塞的操作,否则会妨碍其他USB事件的及时处理。
              如果需要执行复杂操作,建议使用队列(Queue)或任务通知(Task Notification)来唤醒另一个任务去处理。
         2)共享回调函数:一个回调函数可以处理多种事件类型(如示例中所示),通过 event->type字段进行区分。
         3)与初始化配置的关系:通过 tinyusb_msc_register_callback设置的回调会覆盖之前在 tinyusb_msc_spiflash_config_t中为同一事件类型设置的回调。
         4)线程安全:该函数本身是线程安全的,可以在任务中调用。
     总结:简单来说,tinyusb_msc_register_callback是你与USB主机“拔插”动作进行交互的开关。通过它,你可以让你的应用程序感知到外部世界对存储设备的访问,并做出相应的反应,这对于构建稳定、可靠的产品至关重要。
(4)将已初始化的存储介质挂载到ESP32的虚拟文件系统(VFS)上
        将已初始化的存储介质(如SPI Flash)挂载到ESP32的虚拟文件系统(VFS)上,以便ESP32本地代码可以读写其中的文件。
它的核心作用是实现一种 “访问权切换”:
         当 USB主机(如你的电脑) 没有访问存储设备时,你可以调用此函数,让 ESP32本地代码 访问存储设备上的文件系统(如FATFS),进行文件读写、日志记录等操作。
         当 USB主机要访问 时(例如你插上USB线),TinyUSB驱动会自动处理卸载,此时ESP32本地应停止文件访问,否则会导致数据损坏。
         重要警告:绝不能让ESP32本地和USB主机同时挂载并访问同一个存储介质。tinyusb_msc_storage_mount和 USB主机的访问是互斥的。
函数原型和参数:
esp_err_t tinyusb_msc_storage_mount(const char *base_path)
        函数参数说明:
类型:const char*
         含义:一个字符串,指定挂载点(Mount Point)。这是ESP32本地应用程序访问文件系统时使用的路径前缀。
         要求:必须是绝对路径(以"/"开头),例如 "/spiflash"或 "/fatfs";该路径在VFS中必须是唯一的,不能与其他存储介质的挂载点冲突。
         特殊值:可以传入 NULL。函数将使用组件内部的默认配置路径CONFIG_TINYUSB_MSC_MOUNT_PATH(通常在 menuconfig中设置)。
| 返回值 | 描述 | 
|---|---|
| ESP_OK | 挂载成功。现在ESP32本地代码可以访问该路径下的文件了。 | 
ESP_ERR_INVALID_STATE | 挂载失败。最常见的原因是USB主机当前正在访问该存储设备。必须等待主机卸载(安全弹出)后才能成功调用此函数。 | 
| 其他错误码 | 挂载失败,可能的原因包括文件系统损坏、内存分配失败等。 | 
表45.3.2.5 tinyusb_msc_storage_mount()函数返回值描述
        工作流程和典型用法:
         理解这个函数的最佳方式是看它在一个完整的应用生命周期中如何被使用。
场景:设备作为USB磁盘+本地数据记录器
         你的设备使用SPI Flash。大部分时间它自己记录传感器数据到文件中。偶尔用户会通过USB线连接电脑,将记录的数据文件拷贝走。简单实现如下:
#include "tusb_msc_storage.h"
#include 
#include 
// 1. 初始化阶段
void initialize_storage(void)
{
    // ... (之前的代码: 初始化 wear levelling 和 TinyUSB MSC) ...
    // tinyusb_msc_storage_init_spiflash(...);
    // tinyusb_driver_install(...);
    // 设备启动后,默认由ESP32本地挂载并使用
    esp_err_t ret = tinyusb_msc_storage_mount("/local_data");
    if (ret == ESP_OK) {
        ESP_LOGI(TAG, "Storage mounted for local access.");
        // 开始本地数据记录任务
        start_data_logger_task();
    } else if (ret == ESP_ERR_INVALID_STATE) {
        // 如果一启动就失败,很可能是因为USB主机正在访问(例如开发时一直连着USB线)
        ESP_LOGW(TAG, "Cannot mount locally. USB host might be active.");
    }
}
// 2. 本地数据记录任务
void data_logger_task(void *arg)
{
    while (1) {
        // 在写入数据前,最好再次检查是否确实由本地挂载
        // 但更好的方法是通过回调事件(后面讲)来控制本任务的启停
        FILE *f = fopen("/local_data/sensor_log.txt", "a");
        if (f) {
            fprintf(f, "Sensor reading: %d\n", read_sensor());
            fclose(f);
        } else {
            ESP_LOGE(TAG, "Failed to open file! Is USB host active?");
        }
        vTaskDelay(pdMS_TO_TICKS(1000));
    }
}
// 3. 注册回调函数,响应USB主机的插拔事件
static void usb_event_callback(tinyusb_msc_event_t *event)
{
    if (event->type == TINYUSB_MSC_EVENT_MOUNT_CHANGED) {
        if (event->mount_changed_data.is_mounted) {
            // USB主机挂载了磁盘!立即停止本地访问。
            ESP_LOGI(TAG, "USB host mounted. Stopping local logger.");
            stop_data_logger_task(); // 自定义函数,用于停止记录任务
            // TinyUSB会自动为我们卸载VFS挂载
        } else {
            // USB主机卸载了磁盘!现在可以重新本地挂载和使用。
            ESP_LOGI(TAG, "USB host unmounted. Remounting for local access.");
            esp_err_t ret = tinyusb_msc_storage_mount("/local_data");
            if (ret == ESP_OK) {
                start_data_logger_task(); // 重启本地记录任务
            }
        }
    }
}
// 在初始化时注册这个回调
tinyusb_msc_register_callback(TINYUSB_MSC_EVENT_MOUNT_CHANGED, usb_event_callback);  
        与 esp_vfs_fat_spiflash_mount的区别:
特性  | 
  | 
  | 
|---|---|---|
目的  | 初始化并永久挂载一个FAT文件系统供ESP32独占使用。  | 在TinyUSB MSC管理下,临时切换存储介质的访问权给ESP32本地。  | 
底层  | 自己内部调用   | 依赖于预先初始化好的   | 
USB MSC  | 不涉及USB大容量存储设备功能。  | 专为与USB MSC功能配合使用而设计。  | 
访问冲突  | 如果同时启用USB MSC,会导致ESP32和主机同时访问,引发数据损坏。  | 安全地管理访问权切换,防止冲突。  | 
典型流程  | 
  | 
  | 
表45.3.2.6 tinyusb_msc_storage_mount()与esp_vfs_fat_spiflash_mount()函数区别
简单比喻:
- esp_vfs_fat_spiflash_mount:就像你买了一块硬盘,格式化成NTFS,装进你的电脑里专用;
 - tinyusb_msc_storage_mount:就像你把一个已经做好的U盘,从电脑A上安全弹出后,再插到电脑B上使用。TinyUSB MSC组件就是管理“安全弹出”和“插入”这个过程的管家。
 
(5)USB 设备登记
使用tinyusb_driver_install()函数,参见上一个章节已介绍。
45.3.3 Flash 模拟 U 盘驱动解析
在 IDF 版的StandardExampleIDF(v5.3.x)\34_usb_flash_u,在34_usb_flash_u\managed_components\espressif_esp_tinyusb文件夹下增加USB 驱动文件。主要实现函数为上面介绍的函数,函数实现比较简单,可以自行研究。

上图中位于 components 文件夹下的是自己编写的一些外设驱动, main 文件夹下包含了一个 一个后缀为.yml 的文件。 espressif_esp_tinyusb下包含的是 FLASH 模拟 U 盘(USB)代码,而后缀为.yml 的文件其主要作用是将项目中各组件的依赖项定义在单独的清单文件中,并以上图所示的方式进行命名。在我们的例程中提现出的作用就是简化了整个工程结构。在编译的过程中,系统便会帮我们自动生成 USB 外设所需要的依赖库: espressif_esp_tinyusb 以及espressif_tinyusb。做到了即能简化项目工程, 又能有效规避了在编译中遇到的错误,但前提是运行时得确保个人的电脑处于联网状态。
45.3.4 CMakeLists.txt 文件
打开本实验 managed_components 文件下的 CMakeLists.txt 文件,其内容如下所示:
set(srcs
    "descriptors_control.c"
    "tinyusb.c"
    "usb_descriptors.c"
    )
if(NOT CONFIG_TINYUSB_NO_DEFAULT_TASK)
    list(APPEND srcs "tusb_tasks.c")
endif() # CONFIG_TINYUSB_NO_DEFAULT_TASK
if(CONFIG_TINYUSB_CDC_ENABLED)
    list(APPEND srcs
        "cdc.c"
        "tusb_cdc_acm.c"
        )
    if(CONFIG_VFS_SUPPORT_IO)
        list(APPEND srcs
            "tusb_console.c"
            "vfs_tinyusb.c"
            )
    endif() # CONFIG_VFS_SUPPORT_IO
endif() # CONFIG_TINYUSB_CDC_ENABLED
if(CONFIG_TINYUSB_MSC_ENABLED)
    list(APPEND srcs
        tusb_msc_storage.c
        )
endif() # CONFIG_TINYUSB_MSC_ENABLED
if(CONFIG_TINYUSB_NET_MODE_NCM)
    list(APPEND srcs
         tinyusb_net.c
         )
endif() # CONFIG_TINYUSB_NET_MODE_NCM
idf_component_register(SRCS ${srcs}
                       INCLUDE_DIRS "include"
                       PRIV_INCLUDE_DIRS "include_private"
                       PRIV_REQUIRES usb
                       REQUIRES fatfs vfs
                       )
# Determine whether tinyusb is fetched from component registry or from local path
idf_build_get_property(build_components BUILD_COMPONENTS)
if(tinyusb IN_LIST build_components)
    set(tinyusb_name tinyusb) # Local component
else()
    set(tinyusb_name espressif__tinyusb) # Managed component
endif()
# Pass tusb_config.h from this component to TinyUSB
idf_component_get_property(tusb_lib ${tinyusb_name} COMPONENT_LIB)
target_include_directories(${tusb_lib} PRIVATE "include")
45.3.5 实验应用代码
打开 main/main.c 文件,该文件定义了工程入口函数,名为 app_main。该函数代码如下。
/**
 * @brief       子分区初始化
 * @param       wl_handle:wear levelling handle
 * @retval      ESP_OK:初始化成功
 */
static esp_err_t storage_init_spiflash(wl_handle_t *wl_handle)
{
    ESP_LOGI(TAG, "Initializing wear levelling");
    const esp_partition_t *data_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_SPIFFS, "storage");
    if (data_partition == NULL)
    {
        ESP_LOGE(TAG, "Failed to find FATFS partition. Check the partition table.");
        return ESP_ERR_NOT_FOUND;
    }
    return wl_mount(data_partition, wl_handle);
}
/**
 * @brief       程序入口
 * @param       无
 * @retval      无
 */
void app_main(void)
{
    esp_err_t ret;
    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 Flash TEST", RED);
    spilcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);
     static wl_handle_t wl_handle = WL_INVALID_HANDLE;
    ESP_ERROR_CHECK(storage_init_spiflash(&wl_handle));
    const tinyusb_msc_spiflash_config_t config_spi = {
        .wl_handle = wl_handle,
        .callback_mount_changed = NULL,
        .mount_config.max_files = 5,
    };
    ESP_ERROR_CHECK(tinyusb_msc_storage_init_spiflash(&config_spi));
    ESP_ERROR_CHECK(tinyusb_msc_register_callback(TINYUSB_MSC_EVENT_MOUNT_CHANGED, NULL));
    /* 挂载设备 */
    ESP_ERROR_CHECK(tinyusb_msc_storage_mount(BASE_PATH));
    /* 配置USB */
    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,                      /* 使用内部USB PHY */
        .configuration_descriptor = msc_fs_configuration_desc,      /* 配置描述符 */
    };
    /* 初始化USB */
    ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg));
    while(1)
    {
        LED0_TOGGLE();
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}
45.4 下载验证
将程序下载到开发板后(注意:先插在UART 端口进行程序下载,程序下载完成后,然后要插在 USB 端口!),打开设备管理器,在通用串行总线控制器->出现 USB大容量存储设备,表示实验成功:

图 45.4.1 通过设备管理器查看 USB 虚拟的串口设备端口
        如图 45.4.1, ESP32 通过 Flash 模拟 U 盘,被电脑识别了,通用串行总线控制器显示的是:USB 大容量存储设备(设置4M,实际显示差不多4M,符合预期)。此时,开发板的 LED 在闪烁,提示程序运行。
         然后打开“我的电脑”,可以看见界面显示了通过 Flash 模拟 U 盘后的容量大小,如下图所示:

图 45.4.2 ESP32 Flash 模拟 U 盘测试
                    
                
                
            
        
浙公网安备 33010602011771号