From 0ac445cbd6d34c531fe5e910494e3ab555b6cd8f Mon Sep 17 00:00:00 2001 From: LiPeng Date: Thu, 10 Jul 2025 13:13:50 +0800 Subject: [PATCH] feat(usb): add CherryUSB host msc example --- .../main/device_cdc_main.c | 4 +- .../usb/host/cherryusb_host/README.md | 2 +- .../host/cherryusb_host/main/CMakeLists.txt | 10 + .../cherryusb_host/main/Kconfig.projbuild | 8 + .../usb/host/cherryusb_host/main/cdc_acm.c | 5 +- .../usb/host/cherryusb_host/main/main.c | 8 +- .../usb/host/cherryusb_host/main/msc.c | 402 ++++++++++++++++++ .../host/cherryusb_host/sdkconfig.defaults | 3 + 8 files changed, 433 insertions(+), 9 deletions(-) create mode 100644 examples/peripherals/usb/host/cherryusb_host/main/msc.c diff --git a/examples/peripherals/usb/device/cherryusb_serial_device/main/device_cdc_main.c b/examples/peripherals/usb/device/cherryusb_serial_device/main/device_cdc_main.c index 6132154900..13663ec711 100644 --- a/examples/peripherals/usb/device/cherryusb_serial_device/main/device_cdc_main.c +++ b/examples/peripherals/usb/device/cherryusb_serial_device/main/device_cdc_main.c @@ -422,13 +422,13 @@ void cdc_acm_task(void *arg) void app_main(void) { -#if EXAMPLE_CHERRYUSB_INIT_DEINIT_LOOP +#if CONFIG_EXAMPLE_CHERRYUSB_INIT_DEINIT_LOOP reinit : #endif xTaskCreatePinnedToCore(&cdc_acm_task, "cdc_acm_task", 1024 * 3, NULL, 10, &s_task_handle, 0); -#if EXAMPLE_CHERRYUSB_INIT_DEINIT_LOOP +#if CONFIG_EXAMPLE_CHERRYUSB_INIT_DEINIT_LOOP for (int i = 10; i >= 0; i--) { ESP_LOGW(TAG, "Deinit usb after %d seconds...", i); vTaskDelay(1000 / portTICK_PERIOD_MS); diff --git a/examples/peripherals/usb/host/cherryusb_host/README.md b/examples/peripherals/usb/host/cherryusb_host/README.md index d682c6d74b..3c131210f1 100644 --- a/examples/peripherals/usb/host/cherryusb_host/README.md +++ b/examples/peripherals/usb/host/cherryusb_host/README.md @@ -5,7 +5,7 @@ (See the README.md file in the upper level 'examples' directory for more information about examples.) -This example demonstrates how to use the CherryUSB host driver. Currently, this example supports communication with HID devices (such as Keyboard and Mouse) and serial port devices (such as CDC_ACM, CH34x, CP210x, PL2303, FTDI FT23x/FT423x devices). +This example demonstrates how to use the CherryUSB host driver. Currently, this example supports communication with HID devices (such as Keyboard and Mouse), serial port devices (such as CDC_ACM, CH34x, CP210x, PL2303, FTDI FT23x/FT423x devices) and MSC (Mass Storage Class). ## How to use example diff --git a/examples/peripherals/usb/host/cherryusb_host/main/CMakeLists.txt b/examples/peripherals/usb/host/cherryusb_host/main/CMakeLists.txt index b55ad345ce..d2254d31ca 100644 --- a/examples/peripherals/usb/host/cherryusb_host/main/CMakeLists.txt +++ b/examples/peripherals/usb/host/cherryusb_host/main/CMakeLists.txt @@ -9,8 +9,13 @@ if(CONFIG_CHERRYUSB_HOST_CDC_ACM OR CONFIG_CHERRYUSB_HOST_FTDI OR CONFIG_CHERRYU list(APPEND srcs "cdc_acm.c") endif() +if(CONFIG_CHERRYUSB_HOST_MSC) + list(APPEND srcs "msc.c") +endif() + idf_component_register(SRCS ${srcs} INCLUDE_DIRS "." + PRIV_REQUIRES esp_timer fatfs ) if(CONFIG_CHERRYUSB_HOST_HID) @@ -42,3 +47,8 @@ if(CONFIG_CHERRYUSB_HOST_PL2303) # Make sure the definitions in hid.c are linked correctly target_link_libraries(${COMPONENT_LIB} INTERFACE "-u usbh_pl2303_run_real") endif() + +if(CONFIG_CHERRYUSB_HOST_MSC) + # Make sure the definitions in hid.c are linked correctly + target_link_libraries(${COMPONENT_LIB} INTERFACE "-u usbh_msc_run_real") +endif() diff --git a/examples/peripherals/usb/host/cherryusb_host/main/Kconfig.projbuild b/examples/peripherals/usb/host/cherryusb_host/main/Kconfig.projbuild index 17e0682741..98f1f01955 100644 --- a/examples/peripherals/usb/host/cherryusb_host/main/Kconfig.projbuild +++ b/examples/peripherals/usb/host/cherryusb_host/main/Kconfig.projbuild @@ -5,6 +5,14 @@ menu "Example Configuration" depends on IDF_TARGET_ESP32S3 default y + config EXAMPLE_FORMAT_IF_MOUNT_FAILED + bool "Format the card if mount failed" + depends on CHERRYUSB_HOST_MSC + default n + help + If this config item is set, format_if_mount_failed will be set to true and the card will be formatted if + the mount has failed. + config EXAMPLE_CHERRYUSB_INIT_DEINIT_LOOP bool "Perform init deinit of cherryusb stack in a loop" default n diff --git a/examples/peripherals/usb/host/cherryusb_host/main/cdc_acm.c b/examples/peripherals/usb/host/cherryusb_host/main/cdc_acm.c index f82ed6be4d..586ef17b6e 100644 --- a/examples/peripherals/usb/host/cherryusb_host/main/cdc_acm.c +++ b/examples/peripherals/usb/host/cherryusb_host/main/cdc_acm.c @@ -7,7 +7,6 @@ #include "sdkconfig.h" #include "freertos/FreeRTOS.h" #include "freertos/task.h" -#include "esp_timer.h" #include "esp_heap_caps.h" #include "esp_log.h" @@ -111,10 +110,12 @@ static void serial_in_cb(void *arg, int nbytes) esp_rom_printf("%c", data[i]); } else { esp_rom_printf("[%02x]", data[i]); + if (data[i] == '\n') { + esp_rom_printf("\n"); + } } } - esp_rom_printf("\r"); serial_start_in(serial); } diff --git a/examples/peripherals/usb/host/cherryusb_host/main/main.c b/examples/peripherals/usb/host/cherryusb_host/main/main.c index 4d8ab4adfb..9cad725279 100644 --- a/examples/peripherals/usb/host/cherryusb_host/main/main.c +++ b/examples/peripherals/usb/host/cherryusb_host/main/main.c @@ -10,7 +10,7 @@ #include "freertos/task.h" #include "esp_log.h" -#if EXAMPLE_HAL_USE_ESP32_S3_USB_OTG +#if CONFIG_EXAMPLE_HAL_USE_ESP32_S3_USB_OTG #include "driver/gpio.h" #define BOOST_EN 13 #define DEV_VBUS_EN 12 @@ -25,7 +25,7 @@ static char *TAG = "HOST"; void app_main(void) { -#if EXAMPLE_HAL_USE_ESP32_S3_USB_OTG +#if CONFIG_EXAMPLE_HAL_USE_ESP32_S3_USB_OTG gpio_config_t io_conf = { .intr_type = GPIO_INTR_DISABLE, .mode = GPIO_MODE_OUTPUT, @@ -40,13 +40,13 @@ void app_main(void) gpio_set_level(USB_SEL, 1); #endif -#if EXAMPLE_CHERRYUSB_INIT_DEINIT_LOOP +#if CONFIG_EXAMPLE_CHERRYUSB_INIT_DEINIT_LOOP reinit: #endif usbh_initialize(0, ESP_USBH_BASE); ESP_LOGI(TAG, "Init usb"); -#if EXAMPLE_CHERRYUSB_INIT_DEINIT_LOOP +#if CONFIG_EXAMPLE_CHERRYUSB_INIT_DEINIT_LOOP for (int i = 10; i >= 0; i--) { ESP_LOGW(TAG, "Deinit usb after %d seconds...", i); vTaskDelay(1000 / portTICK_PERIOD_MS); diff --git a/examples/peripherals/usb/host/cherryusb_host/main/msc.c b/examples/peripherals/usb/host/cherryusb_host/main/msc.c new file mode 100644 index 0000000000..ef888417fa --- /dev/null +++ b/examples/peripherals/usb/host/cherryusb_host/main/msc.c @@ -0,0 +1,402 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ +#include +#include +#include +#include "sdkconfig.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "esp_log.h" +#include "esp_check.h" +#include "diskio_impl.h" +#include "ffconf.h" +#include "ff.h" +#include "esp_vfs_fat.h" + +#include "usbh_core.h" +#include "usbh_msc.h" + +static char *TAG = "MSC"; + +#define DRIVE_STR_LEN 3 + +typedef struct msc_host_vfs { + uint8_t pdrv; + FATFS *fs; + char base_path[0]; +} msc_host_vfs_t; + +static struct usbh_msc *s_mscs[FF_VOLUMES] = { NULL }; + +#define WAIT_BUFFER_TIMEOUT_MS 8000 +static SemaphoreHandle_t s_buff_mux = NULL; +static size_t s_buff_size = 0; +static uint8_t *s_buff = NULL; + +static DSTATUS usb_disk_initialize(BYTE pdrv) +{ + return RES_OK; +} + +static DSTATUS usb_disk_status(BYTE pdrv) +{ + return RES_OK; +} + +static uint8_t *get_buffer(size_t size) +{ + if (xSemaphoreTake(s_buff_mux, WAIT_BUFFER_TIMEOUT_MS / portTICK_PERIOD_MS) != pdTRUE) { + ESP_LOGW(TAG, "wait buffer timeout"); + return NULL; + } + if (s_buff_size < size) { + if (s_buff) { + heap_caps_free(s_buff); + } + s_buff = heap_caps_aligned_alloc(CONFIG_USB_ALIGN_SIZE, size, MALLOC_CAP_DMA | MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT); + if (s_buff == NULL) { + s_buff_size = 0; + ESP_LOGW(TAG, "no mem"); + xSemaphoreGive(s_buff_mux); + return NULL; + } + s_buff_size = size; + } + return s_buff; +} + +static void free_buffer(void) +{ + xSemaphoreGive(s_buff_mux); +} + +static void check_free_buffer(void) +{ + uint8_t *buff; + if (s_buff == NULL || xSemaphoreTake(s_buff_mux, 0) != pdTRUE) { + return; + } + for (size_t i = 0; i < sizeof(s_mscs) / sizeof(s_mscs[0]); i++) { + if (s_mscs[i] != NULL) { + xSemaphoreGive(s_buff_mux); + return; + } + } + buff = s_buff; + s_buff = NULL; + s_buff_size = 0; + xSemaphoreGive(s_buff_mux); + if (buff) { + heap_caps_free(buff); + ESP_LOGI(TAG, "free msc buffer"); + } +} + +static DRESULT usb_disk_read(BYTE pdrv, BYTE *buff, DWORD sector, UINT count) +{ + struct usbh_msc *msc_class; + assert(pdrv < FF_VOLUMES); + + msc_class = s_mscs[pdrv]; + assert(msc_class); + if (sector >= msc_class->blocknum - count) { + ESP_LOGW(TAG, "%s: sector 0x%"PRIX32" out of range", __FUNCTION__, (uint32_t)sector); + return RES_PARERR; + } + + uint8_t *dma_buff = buff; + size_t len = msc_class->blocksize * count; + + if (((uint32_t)dma_buff & (CONFIG_USB_ALIGN_SIZE - 1)) || (len & (CONFIG_USB_ALIGN_SIZE - 1))) { + dma_buff = get_buffer(len); + if (dma_buff == NULL) { + return RES_ERROR; + } + } + + int ret = usbh_msc_scsi_read10(msc_class, sector, dma_buff, count); + if (dma_buff != buff) { + if (ret == 0) { + memcpy(buff, dma_buff, len); + } + free_buffer(); + } + if (ret != 0) { + ESP_LOGE(TAG, "usbh_msc_scsi_read10 failed (%d)", ret); + return RES_ERROR; + } + + return RES_OK; +} + +static DRESULT usb_disk_write(BYTE pdrv, const BYTE *buff, DWORD sector, UINT count) +{ + struct usbh_msc *msc_class; + assert(pdrv < FF_VOLUMES); + + msc_class = s_mscs[pdrv]; + assert(msc_class); + if (sector >= msc_class->blocknum - count) { + ESP_LOGW(TAG, "%s: sector 0x%"PRIX32" out of range", __FUNCTION__, (uint32_t)sector); + return RES_PARERR; + } + + const uint8_t *dma_buff = buff; + size_t len = msc_class->blocksize * count; + + if (((uint32_t)dma_buff & (CONFIG_USB_ALIGN_SIZE - 1)) || (len & (CONFIG_USB_ALIGN_SIZE - 1))) { + dma_buff = get_buffer(len); + if (dma_buff == NULL) { + return RES_ERROR; + } + memcpy((uint8_t *)dma_buff, buff, len); + } + + int ret = usbh_msc_scsi_write10(msc_class, sector, dma_buff, count); + if (dma_buff != buff) { + free_buffer(); + } + if (ret != ESP_OK) { + ESP_LOGE(TAG, "usbh_msc_scsi_write10 failed (%d)", ret); + return RES_ERROR; + } + return RES_OK; +} + +static DRESULT usb_disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) +{ + struct usbh_msc *msc_class; + assert(pdrv < FF_VOLUMES); + + msc_class = s_mscs[pdrv]; + assert(msc_class); + + switch (cmd) { + case CTRL_SYNC: + return RES_OK; + case GET_SECTOR_COUNT: + *((DWORD *) buff) = msc_class->blocknum; + return RES_OK; + case GET_SECTOR_SIZE: + *((WORD *) buff) = msc_class->blocksize; + return RES_OK; + case GET_BLOCK_SIZE: + return RES_ERROR; + } + return RES_ERROR; +} + +void ff_diskio_register_msc(BYTE pdrv, struct usbh_msc *msc_class) +{ + assert(pdrv < FF_VOLUMES); + + static const ff_diskio_impl_t usb_disk_impl = { + .init = &usb_disk_initialize, + .status = &usb_disk_status, + .read = &usb_disk_read, + .write = &usb_disk_write, + .ioctl = &usb_disk_ioctl + }; + s_mscs[pdrv] = msc_class; + ff_diskio_register(pdrv, &usb_disk_impl); +} + +BYTE ff_diskio_get_pdrv_disk(const struct usbh_msc *msc_class) +{ + for (int i = 0; i < FF_VOLUMES; i++) { + if (msc_class == s_mscs[i]) { + return i; + } + } + return 0xff; +} + +static esp_err_t msc_host_format(struct usbh_msc *msc_class, size_t allocation_size) +{ + ESP_RETURN_ON_FALSE((msc_class != NULL && msc_class->user_data != NULL), ESP_ERR_INVALID_ARG, TAG, ""); + void *workbuf = NULL; + const size_t workbuf_size = 4096; + msc_host_vfs_t *vfs = (msc_host_vfs_t *)msc_class->user_data; + + char drive[DRIVE_STR_LEN] = {(char)('0' + vfs->pdrv), ':', 0}; + ESP_RETURN_ON_FALSE((workbuf = ff_memalloc(workbuf_size)), ESP_ERR_NO_MEM, TAG, ""); + + // Valid value of cluster size is between sector_size and 128 * sector_size. + size_t cluster_size = MIN(MAX(allocation_size, msc_class->blocksize), 128 * msc_class->blocksize); + + ESP_LOGW(TAG, "Formatting card, allocation unit size=%d", cluster_size); + f_mount(0, drive, 0); + +#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 0, 0) + FRESULT err = f_mkfs(drive, FM_ANY | FM_SFD, cluster_size, workbuf, workbuf_size); +#else + const MKFS_PARM opt = {(BYTE)(FM_ANY | FM_SFD), 0, 0, 0, cluster_size}; + FRESULT err = f_mkfs(drive, &opt, workbuf, workbuf_size); +#endif + + free(workbuf); + if (err != FR_OK || (err = f_mount(vfs->fs, drive, 0)) != FR_OK) { + ESP_LOGE(TAG, "Formatting failed with error: %d", err); + return ESP_FAIL; + } + return ESP_OK; +} + +esp_err_t msc_host_vfs_register(struct usbh_msc *msc_class, + const char *base_path, + const esp_vfs_fat_mount_config_t *mount_config) +{ + ESP_RETURN_ON_FALSE((msc_class != NULL && msc_class->user_data == NULL && base_path != NULL && mount_config != NULL), ESP_ERR_INVALID_ARG, TAG, ""); + + FATFS *fs = NULL; + BYTE pdrv; + + if (ff_diskio_get_drive(&pdrv) != ESP_OK) { + ESP_LOGW(TAG, "the maximum count of volumes is already mounted"); + return ESP_ERR_NO_MEM; + } + + esp_err_t ret; + + msc_host_vfs_t *vfs = malloc(sizeof(msc_host_vfs_t) + strlen(base_path) + 1); + ESP_RETURN_ON_FALSE(vfs != NULL, ESP_ERR_NO_MEM, TAG, ""); + + ff_diskio_register_msc(pdrv, msc_class); + char drive[DRIVE_STR_LEN] = {(char)('0' + pdrv), ':', 0}; + strcpy(vfs->base_path, base_path); + vfs->pdrv = pdrv; + + ret = esp_vfs_fat_register(base_path, drive, mount_config->max_files, &fs); + ESP_GOTO_ON_ERROR(ret, fail, TAG, "Failed to register filesystem, error=%s", esp_err_to_name(ret)); + vfs->fs = fs; + + msc_class->user_data = vfs; + if (f_mount(fs, drive, 1) != FR_OK) { + if ((!mount_config->format_if_mount_failed) || msc_host_format(msc_class, mount_config->allocation_unit_size) != ESP_OK) { + ret = ESP_FAIL; + goto fail; + } + } + return ESP_OK; + +fail: + msc_class->user_data = NULL; + if (fs) { + f_mount(NULL, drive, 0); + } + esp_vfs_fat_unregister_path(base_path); + ff_diskio_unregister(pdrv); + s_mscs[pdrv] = NULL; + return ret; +} + +esp_err_t msc_host_vfs_unregister(struct usbh_msc *msc_class) +{ + ESP_RETURN_ON_FALSE((msc_class != NULL && ff_diskio_get_pdrv_disk(msc_class) != 0XFF), ESP_ERR_INVALID_ARG, TAG, ""); + msc_host_vfs_t *vfs = (msc_host_vfs_t *)msc_class->user_data; + msc_class->user_data = NULL; + + char drive[DRIVE_STR_LEN] = {(char)('0' + vfs->pdrv), ':', 0}; + f_mount(NULL, drive, 0); + ff_diskio_unregister(vfs->pdrv); + s_mscs[vfs->pdrv] = NULL; + esp_vfs_fat_unregister_path(vfs->base_path); + heap_caps_free(vfs); + check_free_buffer(); + return ESP_OK; +} + +static esp_err_t s_example_write_file(const char *path, const char *data) +{ + ESP_LOGI(TAG, "Opening file %s", path); + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC); + if (fd < 0) { + ESP_LOGE(TAG, "Failed to open file for writing"); + return ESP_FAIL; + } + write(fd, data, strlen(data)); + close(fd); + ESP_LOGI(TAG, "File written"); + + return ESP_OK; +} + +static esp_err_t s_example_read_file(const char *path) +{ + ESP_LOGI(TAG, "Reading file %s", path); + int fd = open(path, O_RDONLY); + if (fd < 0) { + ESP_LOGE(TAG, "Failed to open file for reading"); + return ESP_FAIL; + } + char line[64]; + size_t len; + + ESP_LOGI(TAG, "Read from file:"); + do { + len = read(fd, line, sizeof(line)); + ESP_LOG_BUFFER_HEXDUMP(TAG, line, len, ESP_LOG_WARN); + } while (len == sizeof(line)); + close(fd); + return ESP_OK; +} + +void usbh_msc_run(struct usbh_msc *msc_class) +{ + int ret; + + if (s_buff_mux == NULL) { + s_buff_mux = xSemaphoreCreateMutex(); + if (s_buff_mux == NULL) { + ESP_LOGE(TAG, "create mutex fail"); + return; + } + } + + ret = usbh_msc_scsi_init(msc_class); + if (ret < 0) { + ESP_LOGE(TAG, "scsi_init error,ret:%d", ret); + return; + } + + esp_vfs_fat_sdmmc_mount_config_t mount_config = { +#ifdef CONFIG_EXAMPLE_FORMAT_IF_MOUNT_FAILED + .format_if_mount_failed = true, +#else + .format_if_mount_failed = false, +#endif // EXAMPLE_FORMAT_IF_MOUNT_FAILED + .max_files = 5, + .allocation_unit_size = 4 * 1024 + }; + ESP_LOGI(TAG, "Mounting filesystem"); + + if (msc_host_vfs_register(msc_class, "/usb", &mount_config) != ESP_OK) { + ESP_LOGE(TAG, "msc_host_vfs_register fail"); + return; + } + ESP_LOGI(TAG, "Filesystem mounted"); + + const char *file_hello = "/usb/hello.txt"; + const char data[] = "Hello, world!\n"; + ret = s_example_write_file(file_hello, data); + if (ret != ESP_OK) { + return; + } + + ret = s_example_read_file(file_hello); + if (ret != ESP_OK) { + return; + } + + return; +} + +void usbh_msc_run_real(struct usbh_msc *) __attribute__((alias("usbh_msc_run"))); + +void usbh_msc_stop(struct usbh_msc *msc_class) +{ + msc_host_vfs_unregister(msc_class); +} diff --git a/examples/peripherals/usb/host/cherryusb_host/sdkconfig.defaults b/examples/peripherals/usb/host/cherryusb_host/sdkconfig.defaults index 39a33d32c7..7b98cfd2af 100644 --- a/examples/peripherals/usb/host/cherryusb_host/sdkconfig.defaults +++ b/examples/peripherals/usb/host/cherryusb_host/sdkconfig.defaults @@ -2,11 +2,14 @@ # Espressif IoT Development Framework (ESP-IDF) 6.0.0 Project Minimal Configuration # CONFIG_ESP_TIMER_SUPPORTS_ISR_DISPATCH_METHOD=y +CONFIG_FATFS_LFN_HEAP=y +CONFIG_FATFS_VFS_FSTAT_BLKSIZE=4096 CONFIG_CHERRYUSB=y CONFIG_CHERRYUSB_HOST=y CONFIG_CHERRYUSB_HOST_DWC2_ESP=y CONFIG_CHERRYUSB_HOST_CDC_ACM=y CONFIG_CHERRYUSB_HOST_HID=y +CONFIG_CHERRYUSB_HOST_MSC=y CONFIG_CHERRYUSB_HOST_CH34X=y CONFIG_CHERRYUSB_HOST_CP210X=y CONFIG_CHERRYUSB_HOST_PL2303=y