Merge branch 'feat/usb-msc-host-multi' into 'master'

feat(usb_host/msc): Add support for multiple MSC devices

See merge request espressif/esp-idf!37336
This commit is contained in:
Igor Masar
2025-03-20 17:40:47 +08:00
2 changed files with 384 additions and 102 deletions

View File

@@ -7,29 +7,48 @@
This example demonstrates usage of the MSC (Mass Storage Class) to access storage on a USB flash drive. Upon connection of the flash drive, it is mounted to the Virtual filesystem. The following example operations are then performed:
1. Print device info (capacity, sectors size, and count...)
2. List all folders and files in the root directory of the USB flash drive
3. Create `ESP` subdirectory (if not present already), as well as a `text.txt` file
4. Run read/write benchmarks by transferring 1 MB of data to a `dummy` file
1. Print device info (capacity, sectors size, and count...).
2. List all folders and files in the root directory of the USB flash drive.
3. Create `esp` subdirectory (if not present already), as well as a `test.txt` file.
4. Run read/write benchmarks by transferring 1 MB of data to a `dummy` file.
> Note: This example currently supports only FAT-formatted drives. Other file systems, such as exFAT or NTFS, are not compatible with this example. Please ensure that your USB drive is formatted as FAT to avoid compatibility issues.
### USB Reconnections
The example is run in a loop so that it can demonstrate USB connection and reconnection handling. If you want to deinitialize the entire USB Host Stack, you can short GPIO0 to GND. GPIO0 is usually mapped to a BOOT button, thus pressing the button will deinitialize the stack.
The example is run in a loop so that it can demonstrate USB connection and reconnection handling. If you want to deinitialize the entire USB Host Stack, you can short GPIO0 to GND. GPIO0 is usually mapped to a BOOT button, thus pressing the button will deinitialize the stack.
### Hardware Required
* Development board with USB-OTG support
* A USB cable for Power supply and programming
* A USB cable for power supply and programming
* A USB flash drive
### USB Host Limitations
#### ESP32-S2 & ESP32-S3
The USB OTG peripheral on ESP32-S2 and ESP32-S3 in host mode supports **up to 8 bidirectional endpoints**. Each of these endpoints can be configured as either IN or OUT.
Since each USB Mass Storage Class (MSC) device typically requires **3 endpoints** (Control, BULK IN, and BULK OUT), and USB hubs also consume endpoints (Control, INTERRUPT IN), this limits the theoretical maximum number of connected MSC devices to **2**.
#### ESP32-P4
ESP32-P4 features a more advanced USB 2.0 OTG controller with **up to 16 bidirectional endpoints** in host mode. Given the same endpoint requirements per device, the theoretical maximum number of connected MSC devices is **4**.
### Example Limitations
The number of simultaneously connected MSC devices is now **determined by `CONFIG_FATFS_VOLUME_COUNT`**. This ensures consistency with the FAT filesystem configuration and prevents misalignment between the MSC device count and the available FAT volumes.
By default, `CONFIG_FATFS_VOLUME_COUNT` is set to **2**, meaning that up to **2 MSC devices** can be connected at the same time. If your application requires more devices, you can increase `CONFIG_FATFS_VOLUME_COUNT` accordingly, while keeping in mind **endpoint and memory constraints**.
#### How to configure `CONFIG_FATFS_VOLUME_COUNT` in menuconfig:
To change the maximum number of MSC devices, adjust `CONFIG_FATFS_VOLUME_COUNT` in **menuconfig**:
idf.py menuconfig → Component config → FAT Filesystem support → Number of FATFS volumes
Set this value to the desired number of MSC devices, ensuring it aligns with USB endpoint availability.
### Common Pin Assignments
Follow instructions in [examples/usb/README.md](../../README.md) for specific hardware setup.
Additionally, GPIO0 can be shorted to ground in order to deinitialize USB stack.
Additionally, GPIO0 can be shorted to ground in order to deinitialize the USB stack.
### Build and Flash
@@ -47,27 +66,74 @@ See the Getting Started Guide for full steps to configure and use ESP-IDF to bui
```
...
I (380) example: Waiting for USB flash drive to be connected
I (790) example: MSC device connected
...
I (323) example: Waiting for USB flash drive to be connected
I (3353) example: MSC device connected (usb_addr=3)
*** Device descriptor ***
bLength 18
bDescriptorType 1
bcdUSB 2.00
bDeviceClass 0x0
bDeviceSubClass 0x0
bDeviceProtocol 0x0
bMaxPacketSize0 64
idVendor 0xabcd
idProduct 0x1234
bcdDevice 1.00
iManufacturer 1
iProduct 2
iSerialNumber 3
bNumConfigurations 1
*** Configuration descriptor ***
bLength 9
bDescriptorType 2
wTotalLength 32
bNumInterfaces 1
bConfigurationValue 1
iConfiguration 0
bmAttributes 0x80
bMaxPower 100mA
*** Interface descriptor ***
bLength 9
bDescriptorType 4
bInterfaceNumber 0
bAlternateSetting 0
bNumEndpoints 2
bInterfaceClass 0x8
bInterfaceSubClass 0x6
bInterfaceProtocol 0x50
iInterface 0
*** Endpoint descriptor ***
bLength 7
bDescriptorType 5
bEndpointAddress 0x1 EP 1 OUT
bmAttributes 0x2 BULK
wMaxPacketSize 512
bInterval 0
*** Endpoint descriptor ***
bLength 7
bDescriptorType 5
bEndpointAddress 0x81 EP 1 IN
bmAttributes 0x2 BULK
wMaxPacketSize 512
bInterval 0
Device info:
Capacity: 29339 MB
Capacity: 3839 MB
Sector size: 512
Sector count: 60088319
PID: 0x5595
VID: 0x0781
iProduct: SanDisk 3.2Gen1
iManufacturer: USB
iSerialNumber: 0401545df64623a907abf299bae54c9
I (990) example: ls command output:
SYSTEM~1
ESP
DUMMY
I (1000) example: Reading file
I (1010) example: Read from file '/usb/esp/test.txt': 'Hello World!'
I (1030) example: Writing to file /usb/esp/dummy
I (2160) example: Write speed 0.93 MiB/s
I (2160) example: Reading from file /usb/esp/dummy
I (3110) example: Read speed 1.10 MiB/s
I (3140) example: Example finished, you can disconnect the USB flash drive
Sector count: 7864319
PID: 0x1234
VID: 0xABCD
iProduct: UDisk
iManufacturer: General
iSerialNumber:
I (3763) example: ls command output for all connected devices:
I (3763) example: Listing contents of /usb0
/usb0/SYSTEM~1
/usb0/ESP
I (3773) example: Reading file
I (3773) example: Read from file '/usb0/esp/test.txt': 'Hello World!'
I (3803) example: Writing to file /usb0/esp/dummy
I (3943) example: Write speed 7.16 MiB/s
I (3953) example: Reading from file /usb0/esp/dummy
I (4093) example: Read speed 7.16 MiB/s
I (4103) example: Example finished, you can disconnect the USB flash drive (or connect another USB flash drive)
```

View File

@@ -1,5 +1,5 @@
/*
* SPDX-FileCopyrightText: 2022-2024 Espressif Systems (Shanghai) CO LTD
* SPDX-FileCopyrightText: 2022-2025 Espressif Systems (Shanghai) CO LTD
*
* SPDX-License-Identifier: Unlicense OR CC0-1.0
*/
@@ -18,21 +18,32 @@
#include "esp_err.h"
#include "esp_log.h"
#include "usb/usb_host.h"
#include "usb/msc_host.h"
#include "usb/msc_host_vfs.h"
#include "ffconf.h"
#include "errno.h"
#include "driver/gpio.h"
static const char *TAG = "example";
#define MNT_PATH "/usb" // Path in the Virtual File System, where the USB flash drive is going to be mounted
#define MNT_PATH "/usb" // Base mount path prefix, devices will be mounted as /usb0, /usb1, /usb2...
#define APP_QUIT_PIN GPIO_NUM_0 // BOOT button on most boards
#define BUFFER_SIZE 4096 // The read/write performance can be improved with larger buffer for the cost of RAM, 4kB is enough for most usecases
#define MAX_MSC_DEVICES CONFIG_FATFS_VOLUME_COUNT /*!< Maximum number of simultaneously connected MSC (Mass Storage Class) devices.
Adjust this value based on available system resources and expected use cases. */
// IMPORTANT NOTE
// MSC Class Driver is not fully support connecting devices through external Hub.
// TODO: Remove this line after MSC Class Driver will support it
static bool dev_present = false;
/**
* @brief MSC Device Entry
*
* This structure holds information about a connected MSC device,
* including the USB address, MSC device handle, VFS handle, and assigned mount point.
*/
typedef struct {
uint8_t usb_addr; /*!< USB device address */
msc_host_device_handle_t msc_device; /*!< Handle of the MSC device */
msc_host_vfs_handle_t vfs_handle; /*!< VFS handle assigned to the MSC device */
} msc_dev_entry_t;
static msc_dev_entry_t *msc_devices[MAX_MSC_DEVICES] = {0};
/**
* @brief Application Queue and its messages ID
@@ -45,10 +56,146 @@ typedef struct {
APP_DEVICE_DISCONNECTED, // USB device disconnect event
} id;
union {
uint8_t new_dev_address; // Address of new USB device for APP_DEVICE_CONNECTED event if
uint8_t new_dev_address; // Address of new USB device for APP_DEVICE_CONNECTED event
msc_host_device_handle_t device_handle; // Handle of removed USB device for APP_DEVICE_DISCONNECTED event
} data;
} app_message_t;
/**
* @brief Find a free slot in the device table.
*
* @return Index of the free slot, or -1 if no free slot is available.
*/
static inline int find_free_slot(void)
{
for (int i = 0; i < MAX_MSC_DEVICES; i++) {
if (msc_devices[i] == NULL) {
return i;
}
}
return -1;
}
/**
* @brief Allocates a new MSC device entry and mounts it to VFS.
*
* This function finds a free slot for a new MSC device, allocates memory for the device entry,
* installs the MSC device, and mounts it to the virtual file system (VFS).
*
* If any step fails, the function ensures proper cleanup of allocated resources before returning an error.
*
* @param[in] msg Message containing the address of the new USB device.
* @param[out] out_slot Pointer to store the allocated slot index on success.
*
* @return
* - ESP_OK on success.
* - ESP_ERR_NOT_FOUND if no free slot is available.
* - ESP_ERR_NO_MEM if memory allocation fails.
* - Other esp_err_t codes if device installation or VFS registration fails.
*/
static esp_err_t allocate_new_msc_device(const app_message_t *msg, int *out_slot)
{
int slot = find_free_slot();
if (slot < 0) {
ESP_LOGW(TAG, "No free slots for new MSC device (max %d)", MAX_MSC_DEVICES);
return ESP_ERR_NOT_FOUND;
}
msc_devices[slot] = calloc(1, sizeof(msc_dev_entry_t));
if (!msc_devices[slot]) {
ESP_LOGE(TAG, "Failed to allocate memory for new MSC device entry");
return ESP_ERR_NO_MEM;
}
esp_err_t err = msc_host_install_device(msg->data.new_dev_address, &msc_devices[slot]->msc_device);
if (err != ESP_OK) {
ESP_LOGE(TAG, "msc_host_install_device failed: %s", esp_err_to_name(err));
free(msc_devices[slot]);
msc_devices[slot] = NULL;
return err;
}
msc_devices[slot]->usb_addr = msg->data.new_dev_address;
const esp_vfs_fat_mount_config_t mount_config = {
.format_if_mount_failed = false,
.max_files = 3,
.allocation_unit_size = 8192,
};
char mount_path[16];
snprintf(mount_path, sizeof(mount_path), MNT_PATH "%d", slot);
err = msc_host_vfs_register(msc_devices[slot]->msc_device, mount_path, &mount_config, &msc_devices[slot]->vfs_handle);
if (err != ESP_OK) {
ESP_LOGE(TAG, "msc_host_vfs_register failed: %s", esp_err_to_name(err));
ESP_ERROR_CHECK(msc_host_uninstall_device(msc_devices[slot]->msc_device));
free(msc_devices[slot]);
msc_devices[slot] = NULL;
return err;
}
*out_slot = slot;
return ESP_OK;
}
/**
* @brief Find a slot by MSC device handle.
*
* This function searches for the slot corresponding to a given MSC device handle.
*
* @param handle MSC device handle to search for.
* @return Index of the slot if found, otherwise -1.
*/
static int find_slot_by_handle(msc_host_device_handle_t handle)
{
for (int i = 0; i < MAX_MSC_DEVICES; i++) {
if (msc_devices[i] && msc_devices[i]->msc_device == handle) {
return i;
}
}
return -1;
}
/**
* @brief Free resources associated with a specific MSC device by slot index.
*
* This function releases all resources associated with a device identified by its slot index.
* It unmounts the VFS, uninstalls the MSC device, and frees the allocated memory.
*
* @param slot Index of the MSC device in the device array.
*/
static void free_msc_device(int slot)
{
if (slot < 0 || slot >= MAX_MSC_DEVICES || !msc_devices[slot]) {
ESP_LOGE(TAG, "Invalid slot index for MSC device deallocation");
return;
}
if (msc_devices[slot]->vfs_handle) {
ESP_ERROR_CHECK(msc_host_vfs_unregister(msc_devices[slot]->vfs_handle));
}
if (msc_devices[slot]->msc_device) {
ESP_ERROR_CHECK(msc_host_uninstall_device(msc_devices[slot]->msc_device));
}
free(msc_devices[slot]);
msc_devices[slot] = NULL;
}
/**
* @brief Free all connected MSC devices.
*
* Iterates over all allocated MSC devices, unmounts them from VFS, and frees their memory.
*/
static void free_all_msc_devices(void)
{
for (int i = 0; i < MAX_MSC_DEVICES; i++) {
if (msc_devices[i]) {
free_msc_device(i);
}
}
}
/**
* @brief BOOT button pressed callback
*
@@ -72,6 +219,22 @@ static void gpio_cb(void *arg)
}
}
/**
* @brief Find a USB addr by MSC device handle.
*
* @param handle MSC device handle
* @return USB addr, or -1 if not found.
*/
static inline int8_t find_usb_addr_by_handle(msc_host_device_handle_t handle)
{
for (int8_t i = 0; i < MAX_MSC_DEVICES; i++) {
if (msc_devices[i] && msc_devices[i]->msc_device == handle) {
return msc_devices[i]->usb_addr;
}
}
return -1;
}
/**
* @brief MSC driver callback
*
@@ -90,14 +253,28 @@ static void msc_event_cb(const msc_host_event_t *event, void *arg)
};
xQueueSend(app_queue, &message, portMAX_DELAY);
} else if (event->event == MSC_DEVICE_DISCONNECTED) {
ESP_LOGI(TAG, "MSC device disconnected");
int usb_addr = find_usb_addr_by_handle(event->device.handle);
if (usb_addr >= 0) {
ESP_LOGI(TAG, "MSC device disconnected (usb_addr=%d)", usb_addr);
} else {
ESP_LOGW(TAG, "MSC device disconnected, but failed to retrieve USB address");
}
app_message_t message = {
.id = APP_DEVICE_DISCONNECTED,
.data.device_handle = event->device.handle,
};
xQueueSend(app_queue, &message, portMAX_DELAY);
}
}
/**
* @brief Print information about the connected MSC device.
*
* This function prints detailed information about the connected USB MSC device,
* such as capacity, sector size, PID, VID, and string descriptors.
*
* @param[in] info Pointer to MSC device information structure.
*/
static void print_device_info(msc_host_device_info_t *info)
{
const size_t megabyte = 1024 * 1024;
@@ -116,12 +293,24 @@ static void print_device_info(msc_host_device_info_t *info)
#endif
}
static void file_operations(void)
/**
* @brief Perform basic file operations on the mounted USB storage device.
*
* This function demonstrates basic file I/O operations:
* - Create a directory `/usb<slot>/esp` if it does not exist.
* - Create a file `test.txt` in the directory with sample content if it does not exist.
* - Read the content of the `test.txt` file and print it to the console.
*
* @param[in] slot Index of the mounted USB device (0 to MAX_MSC_DEVICES-1).
*/
static void file_operations(int slot)
{
const char *directory = "/usb/esp";
const char *file_path = "/usb/esp/test.txt";
char directory[32];
char file_path[32];
snprintf(directory, sizeof(directory), MNT_PATH "%d/esp", slot);
snprintf(file_path, sizeof(file_path), MNT_PATH "%d/esp/test.txt", slot);
// Create /usb/esp directory
// Create /usb<slot>/esp directory
struct stat s = {0};
bool directory_exists = stat(directory, &s) == 0;
if (!directory_exists) {
@@ -130,7 +319,7 @@ static void file_operations(void)
}
}
// Create /usb/esp/test.txt file, if it doesn't exist
// Create /usb<slot>/esp/test.txt file, if it doesn't exist
if (stat(file_path, &s) != 0) {
ESP_LOGI(TAG, "Creating file");
FILE *f = fopen(file_path, "w");
@@ -161,13 +350,25 @@ static void file_operations(void)
ESP_LOGI(TAG, "Read from file '%s': '%s'", file_path, line);
}
void speed_test(void)
/**
* @brief Perform sequential write and read speed test on the mounted USB storage device.
*
* This function performs:
* - A write speed test by writing a series of 4KB blocks to a test file.
* - A read speed test by reading the same number of 4KB blocks from the file.
*
* The results (in MiB/s) are printed to the console.
*
* @param[in] slot Index of the mounted USB device (0 to MAX_MSC_DEVICES-1).
*/
static void speed_test(int slot)
{
#define TEST_FILE "/usb/esp/dummy"
#define ITERATIONS 256 // 256 * 4kb = 1MB
int64_t test_start, test_end;
char test_file[32];
snprintf(test_file, sizeof(test_file), MNT_PATH "%d/esp/dummy", slot);
FILE *f = fopen(TEST_FILE, "wb+");
FILE *f = fopen(test_file, "wb+");
if (f == NULL) {
ESP_LOGE(TAG, "Failed to open file for writing");
return;
@@ -179,10 +380,13 @@ void speed_test(void)
uint8_t *data = malloc(BUFFER_SIZE);
assert(data);
ESP_LOGI(TAG, "Writing to file %s", TEST_FILE);
ESP_LOGI(TAG, "Writing to file %s", test_file);
test_start = esp_timer_get_time();
for (int i = 0; i < ITERATIONS; i++) {
if (fwrite(data, BUFFER_SIZE, 1, f) == 0) {
ESP_LOGE(TAG, "Write error");
fclose(f);
free(data);
return;
}
}
@@ -190,10 +394,13 @@ void speed_test(void)
ESP_LOGI(TAG, "Write speed %1.2f MiB/s", (BUFFER_SIZE * ITERATIONS) / (float)(test_end - test_start));
rewind(f);
ESP_LOGI(TAG, "Reading from file %s", TEST_FILE);
ESP_LOGI(TAG, "Reading from file %s", test_file);
test_start = esp_timer_get_time();
for (int i = 0; i < ITERATIONS; i++) {
if (0 == fread(data, BUFFER_SIZE, 1, f)) {
ESP_LOGE(TAG, "Read error");
fclose(f);
free(data);
return;
}
}
@@ -248,6 +455,41 @@ static void usb_task(void *args)
vTaskDelete(NULL);
}
/**
* @brief List contents of the root directories of all mounted USB storage devices.
*
* This function iterates over all mounted MSC devices and lists the contents
* of their root directories. It is intended for debugging and demonstration purposes.
*
* For each connected and mounted device, the function attempts to open the root directory
* `/usb<slot>` and print the names of all files and directories contained within.
*
* If opening the directory fails, an error is logged.
*/
static inline void show_list_files_all_devices(void)
{
ESP_LOGI(TAG, "ls command output for all connected devices:");
for (int i = 0; i < MAX_MSC_DEVICES; i++) {
if (msc_devices[i]) {
char mount_path[16];
snprintf(mount_path, sizeof(mount_path), MNT_PATH "%d", i);
ESP_LOGI(TAG, "Listing contents of %s", mount_path);
struct dirent *d;
DIR *dh = opendir(mount_path);
if (!dh) {
ESP_LOGE(TAG, "Failed to open directory: %s", mount_path);
continue;
}
while ((d = readdir(dh)) != NULL) {
printf("%s/%s\n", mount_path, d->d_name);
}
closedir(dh);
}
}
}
void app_main(void)
{
// Create FreeRTOS primitives
@@ -258,7 +500,7 @@ void app_main(void)
assert(task_created);
// Init BOOT button: Pressing the button simulates app request to exit
// It will disconnect the USB device and uninstall the MSC driver and USB Host Lib
// It will disconnect the USB device and uninstall the MSC drivers and USB Host Lib
const gpio_config_t input_pin = {
.pin_bit_mask = BIT64(APP_QUIT_PIN),
.mode = GPIO_MODE_INPUT,
@@ -270,8 +512,6 @@ void app_main(void)
ESP_ERROR_CHECK(gpio_isr_handler_add(APP_QUIT_PIN, gpio_cb, NULL));
ESP_LOGI(TAG, "Waiting for USB flash drive to be connected");
msc_host_device_handle_t msc_device = NULL;
msc_host_vfs_handle_t vfs_handle = NULL;
// Perform all example operations in a loop to allow USB reconnections
while (1) {
@@ -279,66 +519,42 @@ void app_main(void)
xQueueReceive(app_queue, &msg, portMAX_DELAY);
if (msg.id == APP_DEVICE_CONNECTED) {
if (dev_present) {
ESP_LOGW(TAG, "MSC Example handles only one device at a time");
} else {
// 0. Change flag
dev_present = true;
// 1. MSC flash drive connected. Open it and map it to Virtual File System
ESP_ERROR_CHECK(msc_host_install_device(msg.data.new_dev_address, &msc_device));
const esp_vfs_fat_mount_config_t mount_config = {
.format_if_mount_failed = false,
.max_files = 3,
.allocation_unit_size = 8192,
};
ESP_ERROR_CHECK(msc_host_vfs_register(msc_device, MNT_PATH, &mount_config, &vfs_handle));
int slot;
esp_err_t res = allocate_new_msc_device(&msg, &slot);
if (res != ESP_OK) {
continue;
}
// 2. Print information about the connected disk
msc_host_device_info_t info;
ESP_ERROR_CHECK(msc_host_get_device_info(msc_devices[slot]->msc_device, &info));
msc_host_print_descriptors(msc_devices[slot]->msc_device);
print_device_info(&info);
// 2. Print information about the connected disk
msc_host_device_info_t info;
ESP_ERROR_CHECK(msc_host_get_device_info(msc_device, &info));
msc_host_print_descriptors(msc_device);
print_device_info(&info);
// 3. List all the files in root directory from all connected msc devices
show_list_files_all_devices();
// 3. List all the files in root directory
ESP_LOGI(TAG, "ls command output:");
struct dirent *d;
DIR *dh = opendir(MNT_PATH);
assert(dh);
while ((d = readdir(dh)) != NULL) {
printf("%s\n", d->d_name);
}
closedir(dh);
// 4. The disk is mounted to Virtual File System, perform some basic demo file operation
file_operations(slot);
// 5. Perform speed test
speed_test(slot);
// 4. The disk is mounted to Virtual File System, perform some basic demo file operation
file_operations();
// 5. Perform speed test
speed_test();
ESP_LOGI(TAG, "Example finished, you can disconnect the USB flash drive");
ESP_LOGI(TAG, "Example finished, you can disconnect the USB flash drive (or connect another USB flash drive)");
}
if (msg.id == APP_DEVICE_DISCONNECTED) {
int slot = find_slot_by_handle(msg.data.device_handle);
if (slot >= 0) {
free_msc_device(slot);
}
}
if ((msg.id == APP_DEVICE_DISCONNECTED) || (msg.id == APP_QUIT)) {
if (dev_present) {
dev_present = false;
if (vfs_handle) {
ESP_ERROR_CHECK(msc_host_vfs_unregister(vfs_handle));
vfs_handle = NULL;
}
if (msc_device) {
ESP_ERROR_CHECK(msc_host_uninstall_device(msc_device));
msc_device = NULL;
}
}
if (msg.id == APP_QUIT) {
// This will cause the usb_task to exit
ESP_ERROR_CHECK(msc_host_uninstall());
break;
}
if (msg.id == APP_QUIT) {
free_all_msc_devices();
msc_host_uninstall();
break;
}
}
ESP_LOGI(TAG, "Done");
gpio_isr_handler_remove(APP_QUIT_PIN);
gpio_uninstall_isr_service();
vQueueDelete(app_queue);
}