From fac23b90ae1ae4f144701124f1ec79aa53e7c8f2 Mon Sep 17 00:00:00 2001 From: Saurabh Kumar Bansal Date: Tue, 1 Nov 2022 01:12:05 +0530 Subject: [PATCH] USB: Add MSC example. --- .../api-reference/peripherals/usb_device.rst | 64 ++- .../usb/device/tusb_msc/CMakeLists.txt | 8 + .../peripherals/usb/device/tusb_msc/README.md | 180 +++++++ .../usb/device/tusb_msc/main/CMakeLists.txt | 5 + .../device/tusb_msc/main/idf_component.yml | 4 + .../usb/device/tusb_msc/main/tusb_msc_main.c | 455 ++++++++++++++++++ .../device/tusb_msc/main/tusb_msc_storage.c | 201 ++++++++ .../device/tusb_msc/main/tusb_msc_storage.h | 118 +++++ .../usb/device/tusb_msc/partitions.csv | 6 + .../device/tusb_msc/pytest_usb_device_msc.py | 20 + .../usb/device/tusb_msc/sdkconfig.defaults | 14 + 11 files changed, 1074 insertions(+), 1 deletion(-) create mode 100644 examples/peripherals/usb/device/tusb_msc/CMakeLists.txt create mode 100644 examples/peripherals/usb/device/tusb_msc/README.md create mode 100644 examples/peripherals/usb/device/tusb_msc/main/CMakeLists.txt create mode 100644 examples/peripherals/usb/device/tusb_msc/main/idf_component.yml create mode 100644 examples/peripherals/usb/device/tusb_msc/main/tusb_msc_main.c create mode 100644 examples/peripherals/usb/device/tusb_msc/main/tusb_msc_storage.c create mode 100644 examples/peripherals/usb/device/tusb_msc/main/tusb_msc_storage.h create mode 100644 examples/peripherals/usb/device/tusb_msc/partitions.csv create mode 100644 examples/peripherals/usb/device/tusb_msc/pytest_usb_device_msc.py create mode 100644 examples/peripherals/usb/device/tusb_msc/sdkconfig.defaults diff --git a/docs/en/api-reference/peripherals/usb_device.rst b/docs/en/api-reference/peripherals/usb_device.rst index c89e8848c98..600ce2b3587 100644 --- a/docs/en/api-reference/peripherals/usb_device.rst +++ b/docs/en/api-reference/peripherals/usb_device.rst @@ -77,7 +77,7 @@ However, the driver also provides default descriptors. You can install the drive - bcdDevice - Manufacturer - Product name -- Name of CDC device if it is On +- Name of CDC or MSC device if it is On - Serial number If you want to use your own descriptors with extended modification, you can define them during the driver installation process. @@ -141,6 +141,66 @@ USB Serial Console The driver allows to redirect all standard application streams (stdinm stdout, stderr) to the USB Serial Device and return them to UART using :cpp:func:`esp_tusb_init_console`/:cpp:func:`esp_tusb_deinit_console` functions. +USB Mass Storage Device (MSC) +----------------------------- + +If the MSC CONFIG_TINYUSB_MSC_ENABLED option is enabled and SPI Flash Wear Levelling WL_SECTOR_SIZE is set to 512 and WL_SECTOR_MODE is set to PERF in Menuconfig, the USB MSC Device can be initialized as shown below (see example below). + +.. code-block:: c + + static uint8_t const desc_configuration[] = { + // 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, TUD_OPT_HIGH_SPEED ? 512 : 64), + }; + + 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, + .idProduct = 0x4002, + .bcdDevice = 0x100, + .iManufacturer = 0x01, + .iProduct = 0x02, + .iSerialNumber = 0x03, + .bNumConfigurations = 0x01 + }; + + 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 + }; + + const tinyusb_config_t tusb_cfg = { + .device_descriptor = &descriptor_config, + .string_descriptor = string_desc_arr, + .external_phy = false, + .configuration_descriptor = desc_configuration, + }; + tinyusb_driver_install(&tusb_cfg); + +The mandatory callbacks that are required to be implemented are + +.. code-block:: c + + void tud_msc_inquiry_cb(uint8_t lun, uint8_t vendor_id[8], uint8_t product_id[16], uint8_t product_rev[4]) + bool tud_msc_test_unit_ready_cb(uint8_t lun) + void tud_msc_capacity_cb(uint8_t lun, uint32_t *block_count, uint16_t *block_size) + bool tud_msc_start_stop_cb(uint8_t lun, uint8_t power_condition, bool start, bool load_eject) + int32_t tud_msc_read10_cb(uint8_t lun, uint32_t lba, uint32_t offset, void *buffer, uint32_t bufsize) + int32_t tud_msc_write10_cb(uint8_t lun, uint32_t lba, uint32_t offset, uint8_t *buffer, uint32_t bufsize) + int32_t tud_msc_scsi_cb(uint8_t lun, uint8_t const scsi_cmd[16], void *buffer, uint16_t bufsize) + Application Examples -------------------- @@ -160,3 +220,5 @@ The table below describes the code examples available in the directory :example: - How to set up {IDF_TARGET_NAME} chip to work as a USB MIDI Device * - :example:`peripherals/usb/device/tusb_hid` - How to set up {IDF_TARGET_NAME} chip to work as a USB Human Interface Device + * - :example:`peripherals/usb/device/tusb_msc` + - How to set up {IDF_TARGET_NAME} chip to work as a USB Mass Storage Device diff --git a/examples/peripherals/usb/device/tusb_msc/CMakeLists.txt b/examples/peripherals/usb/device/tusb_msc/CMakeLists.txt new file mode 100644 index 00000000000..a0abd7ebbdd --- /dev/null +++ b/examples/peripherals/usb/device/tusb_msc/CMakeLists.txt @@ -0,0 +1,8 @@ +# For more information about build system see +# https://docs.espressif.com/projects/esp-idf/en/latest/api-guides/build-system.html +# The following five lines of boilerplate have to be in your project's +# CMakeLists in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.16) + +include($ENV{IDF_PATH}/tools/cmake/project.cmake) +project(tusb_msc) diff --git a/examples/peripherals/usb/device/tusb_msc/README.md b/examples/peripherals/usb/device/tusb_msc/README.md new file mode 100644 index 00000000000..496da82f091 --- /dev/null +++ b/examples/peripherals/usb/device/tusb_msc/README.md @@ -0,0 +1,180 @@ +| Supported Targets | ESP32-S2 | ESP32-S3 | +| ----------------- | -------- | -------- | + +# TinyUSB Mass Storage Device Example + +(See the README.md file in the upper level 'examples' directory for more information about examples.) + +Mass Storage Devices are one of the most common USB devices. It use Mass Storage Class (MSC) that allow access to their internal data storage. +This example contains code to make ESP based device recognizable by USB-hosts as a USB Mass Storage Device. +It either allows the embedded application ie example to access the partition or Host PC accesses the partition over USB MSC. +They can't be allowed to access the partition at the same time. +The access to the underlying block device is provided by functions in tusb_msc_storage.c + +In this example, data is read/written from/to SPI Flash through wear-levelling APIs. Wear leveling is a technique that helps to distribute wear and tear among sectors more evenly without requiring any attention from the user. As a result, it helps in extending the life of each sector of the Flash memory. + +As a USB stack, a TinyUSB component is used. + +## How to use example + +### Scenarios +1. USB which accesses the ESP MSC Partition is unplugged initially and the board is powered-on. + - Result: Host PC can't access the partition over USB MSC. Application example can perform operations (read, write) on partition. +2. USB which accesses the ESP MSC Partition is already plugged-in at boot time. + - Result: Host PC recongnize it as removable device and can access the partition over USB MSC. Application example can't perform any operation on partition. +3. USB which accesses the ESP MSC Partition is plugged-in at boo-up. After boot-up, it is ejected on Host PC manually by user. + - Result: Host PC can't access the partition over USB MSC. Application example can perform operations (read, write) on partition. +4. USB which accesses the ESP MSC Partition is plugged-in at boot-up. It is then unplugged(removed) from Host PC manually by user. + - Result: The behaviour is different for bus-powered devices and self-powered devices + - (a) Bus-Powered devices - Both Host PC as well as application example can't access the partition over USB MSC. Here, the device will be Powered-off. + - (b) Self-Powered devices - Here, the device can be powered-on even after unplugging the device from Host PC. These behaviour can be further categorize in two ways: + - (i) Self-Powered Devices without VBUS monitoring - Both Host PC as well as application example can't access the partition over USB MSC. + - (ii) Self-Powered Devices with VBUS monitoring - Host PC can't access the partition over USB MSC. Application example can perform operations (read, write) on partition. Here, in ``tinyusb_config_t`` user must set ``self_powered`` to ``true`` and ``vbus_monitor_io`` to GPIO number (``VBUS_MONITORING_GPIO_NUM``) that will be used for VBUS monitoring. + +### Hardware Required + +Any ESP board that have USB-OTG supported. + +### Pin Assignment + +_Note:_ In case your board doesn't have micro-USB connector connected to USB-OTG peripheral, you may have to DIY a cable and connect **D+** and **D-** to the pins listed below. + +See common pin assignments for USB Device examples from [upper level](../../README.md#common-pin-assignments). + +Next, for Self-Powered Devices with VBUS monitoring, user must set ``self_powered`` to ``true`` and ``vbus_monitor_io`` to GPIO number (``VBUS_MONITORING_GPIO_NUM``) that will be used for VBUS monitoring. + +### Build and Flash + +Build the project and flash it to the board, then run monitor tool to view serial output: + +```bash +idf.py -p PORT flash monitor +``` + +(Replace PORT with the name of the serial port to use.) + +(To exit the serial monitor, type ``Ctrl-]``.) + +See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. + +## Example Output + +After the flashing you should see the output at idf monitor: + +``` +I (311) cpu_start: Starting scheduler on PRO CPU. +I (0) cpu_start: Starting scheduler on APP CPU. +I (332) gpio: GPIO[4]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:0 +I (332) example_msc_main: Initializing storage... +I (342) example_msc_storage: Initializing wear levelling +I (372) example_msc_main: USB MSC initialization +I (372) tusb_desc: +┌─────────────────────────────────┐ +│ USB Device Descriptor Summary │ +├───────────────────┬─────────────┤ +│bDeviceClass │ 239 │ +├───────────────────┼─────────────┤ +│bDeviceSubClass │ 2 │ +├───────────────────┼─────────────┤ +│bDeviceProtocol │ 1 │ +├───────────────────┼─────────────┤ +│bMaxPacketSize0 │ 64 │ +├───────────────────┼─────────────┤ +│idVendor │ 0x303a │ +├───────────────────┼─────────────┤ +│idProduct │ 0x4002 │ +├───────────────────┼─────────────┤ +│bcdDevice │ 0x100 │ +├───────────────────┼─────────────┤ +│iManufacturer │ 0x1 │ +├───────────────────┼─────────────┤ +│iProduct │ 0x2 │ +├───────────────────┼─────────────┤ +│iSerialNumber │ 0x3 │ +├───────────────────┼─────────────┤ +│bNumConfigurations │ 0x1 │ +└───────────────────┴─────────────┘ +I (532) TinyUSB: TinyUSB Driver installed +I (532) example_msc_main: USB MSC initialization DONE +I (542) example_msc_main: Mount storage... +I (542) example_msc_storage: Initializing FAT +I (552) example_msc_main: +ls command output: +README.MD +.fseventsd + +Type 'help' to get the list of commands. +Use UP/DOWN arrows to navigate through command history. +Press TAB when typing command name to auto-complete. +esp32s3> I (912) example_msc_main: tud_mount_cb MSC START: Expose Over USB +I (912) example_msc_main: Unmount storage... +I (2032) example_msc_main: tud_msc_scsi_cb() invoked: SCSI_CMD_PREVENT_ALLOW_MEDIUM_REMOVAL +I (2032) example_msc_main: tud_msc_capacity_cb() size(1024000), sec_size(512) +esp32s3> +esp32s3> +esp32s3> help +help + Print the list of registered commands + +read + read BASE_PATH/README.MD and print its contents + +write + create file BASE_PATH/README.MD if it does not exist + +size + show storage size and sector size + +expose + Expose Storage to Host + +status + Status of storage exposure over USB + +exit + exit from application + +esp32s3> +esp32s3> read +E (19102) example_msc_main: storage exposed over USB. Application can't read from storage. +Command returned non-zero error code: 0xffffffff (ESP_FAIL) +esp32s3> write +E (22412) example_msc_main: storage exposed over USB. Application can't write to storage. +Command returned non-zero error code: 0xffffffff (ESP_FAIL) +esp32s3> size +E (24962) example_msc_main: storage exposed over USB. Application can't access storage +Command returned non-zero error code: 0xffffffff (ESP_FAIL) +esp32s3> status +storage exposed over USB: Yes +esp32s3> +esp32s3> +esp32s3> I (49692) example_msc_main: tud_msc_scsi_cb() invoked: SCSI_CMD_PREVENT_ALLOW_MEDIUM_REMOVAL +I (49692) example_msc_main: tud_msc_start_stop_cb() invoked, power_condition=0, start=0, load_eject=1 +I (49702) example_msc_main: tud_msc_start_stop_cb: MSC EJECT: Mount on Example +I (49712) example_msc_main: Mount storage... +I (49712) example_msc_storage: Initializing FAT +I (49712) example_msc_main: +ls command output: +README.MD +esp32s3> +esp32s3> +esp32s3> status +storage exposed over USB: No +esp32s3> read +Mass Storage Devices are one of the most common USB devices. It use Mass Storage Class (MSC) that allow access to their internal data storage. +In this example, ESP chip will be recognised by host (PC) as Mass Storage Device. +Upon connection to USB host (PC), the example application will initialize the storage module and then the storage will be seen as removable device on PC. +esp32s3> write +esp32s3> size +storage size(1024000), sec_size(512) +esp32s3> +esp32s3> expose +I (76402) example_msc_main: Unmount storage... +esp32s3> I (76772) example_msc_main: tud_msc_scsi_cb() invoked: SCSI_CMD_PREVENT_ALLOW_MEDIUM_REMOVAL +I (76772) example_msc_main: tud_msc_capacity_cb() size(1024000), sec_size(512) +esp32s3> +esp32s3> status +storage exposed over USB: Yes +esp32s3> +esp32s3> +``` diff --git a/examples/peripherals/usb/device/tusb_msc/main/CMakeLists.txt b/examples/peripherals/usb/device/tusb_msc/main/CMakeLists.txt new file mode 100644 index 00000000000..67074f3083f --- /dev/null +++ b/examples/peripherals/usb/device/tusb_msc/main/CMakeLists.txt @@ -0,0 +1,5 @@ +idf_component_register( + SRCS "tusb_msc_storage.c" "tusb_msc_main.c" + INCLUDE_DIRS . + REQUIRES fatfs wear_levelling console + ) diff --git a/examples/peripherals/usb/device/tusb_msc/main/idf_component.yml b/examples/peripherals/usb/device/tusb_msc/main/idf_component.yml new file mode 100644 index 00000000000..daf1db8fbf0 --- /dev/null +++ b/examples/peripherals/usb/device/tusb_msc/main/idf_component.yml @@ -0,0 +1,4 @@ +## IDF Component Manager Manifest File +dependencies: + espressif/esp_tinyusb: "0.0.1" + idf: "^5.1" diff --git a/examples/peripherals/usb/device/tusb_msc/main/tusb_msc_main.c b/examples/peripherals/usb/device/tusb_msc/main/tusb_msc_main.c new file mode 100644 index 00000000000..12be62712be --- /dev/null +++ b/examples/peripherals/usb/device/tusb_msc/main/tusb_msc_main.c @@ -0,0 +1,455 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +/* DESCRIPTION: + * This example contains code to make ESP32-S3 based device recognizable by USB-hosts as a USB Mass Storage Device. + * It either allows the embedded application ie example to access the partition or Host PC accesses the partition over USB MSC. + * They can't be allowed to access the partition at the same time. + * The access to the underlying block device is provided by functions in tusb_msc_storage.c + * For different scenarios and behaviour, Refer to README of this example. + */ + +#include +#include +#include +#include "esp_log.h" +#include "esp_console.h" +#include "tinyusb.h" +#include "class/msc/msc_device.h" +#include "tusb_msc_storage.h" +#include "driver/gpio.h" + +static const char *TAG = "example_msc_main"; +#define PROMPT_STR CONFIG_IDF_TARGET + +/********* TinyUSB MSC callbacks ***************/ + +/** SCSI ASC/ASCQ codes. **/ +/** User can add and use more codes as per the need of the application **/ +#define SCSI_CODE_ASC_MEDIUM_NOT_PRESENT 0x3A /** SCSI ASC code for 'MEDIUM NOT PRESENT' **/ +#define SCSI_CODE_ASC_INVALID_COMMAND_OPERATION_CODE 0x20 /** SCSI ASC code for 'INVALID COMMAND OPERATION CODE' **/ +#define SCSI_CODE_ASCQ 0x00 + +static void _mount(void); +static void _unmount(void); +static bool is_eject = false; + +// Invoked when received SCSI_CMD_INQUIRY +// Application fill vendor id, product id and revision with string up to 8, 16, 4 characters respectively +void tud_msc_inquiry_cb(uint8_t lun, uint8_t vendor_id[8], uint8_t product_id[16], uint8_t product_rev[4]) +{ + (void) lun; + ESP_LOGD(TAG, "tud_msc_inquiry_cb() invoked"); + + const char vid[] = "TinyUSB"; + const char pid[] = "Flash Storage"; + const char rev[] = "0.1"; + + memcpy(vendor_id, vid, strlen(vid)); + memcpy(product_id, pid, strlen(pid)); + memcpy(product_rev, rev, strlen(rev)); +} + +// Invoked when received Test Unit Ready command. +// return true allowing host to read/write this LUN e.g SD card inserted +bool tud_msc_test_unit_ready_cb(uint8_t lun) +{ + (void) lun; + ESP_LOGD(TAG, "tud_msc_test_unit_ready_cb() invoked"); + + if (is_eject) { + tud_msc_set_sense(lun, SCSI_SENSE_NOT_READY, SCSI_CODE_ASC_MEDIUM_NOT_PRESENT, SCSI_CODE_ASCQ); + return false; + } else { + ESP_LOGD(TAG, "tud_msc_test_unit_ready_cb: MSC START: Expose Over USB"); + _unmount(); + return true; + } +} + +// Invoked when received SCSI_CMD_READ_CAPACITY_10 and SCSI_CMD_READ_FORMAT_CAPACITY to determine the disk size +// Application update block count and block size +void tud_msc_capacity_cb(uint8_t lun, uint32_t *block_count, uint16_t *block_size) +{ + (void) lun; + + size_t size = storage_get_size(); + size_t sec_size = storage_get_sector_size(); + ESP_LOGI(TAG, "tud_msc_capacity_cb() size(%d), sec_size(%d)", size, sec_size); + *block_count = size / sec_size; + *block_size = sec_size; +} + +// Invoked when received Start Stop Unit command +// - Start = 0 : stopped power mode, if load_eject = 1 : unload disk storage +// - Start = 1 : active mode, if load_eject = 1 : load disk storage +bool tud_msc_start_stop_cb(uint8_t lun, uint8_t power_condition, bool start, bool load_eject) +{ + (void) lun; + (void) power_condition; + ESP_LOGI(TAG, "tud_msc_start_stop_cb() invoked, power_condition=%d, start=%d, load_eject=%d", power_condition, start, load_eject); + + if (load_eject && !start) { + is_eject = true; + ESP_LOGI(TAG, "tud_msc_start_stop_cb: MSC EJECT: Mount on Example"); + _mount(); + } + return true; +} + +// Invoked when received SCSI READ10 command +// - Address = lba * BLOCK_SIZE + offset +// - Application fill the buffer (up to bufsize) with address contents and return number of read byte. +int32_t tud_msc_read10_cb(uint8_t lun, uint32_t lba, uint32_t offset, void *buffer, uint32_t bufsize) +{ + ESP_LOGD(TAG, "tud_msc_read10_cb() invoked, lun=%d, lba=%lu, offset=%lu, bufsize=%lu", lun, lba, offset, bufsize); + + size_t addr = lba * storage_get_sector_size() + offset; + esp_err_t err = storage_read_sector(addr, bufsize, buffer); + if (err != ESP_OK) { + ESP_LOGE(TAG, "storage_read_sector failed: 0x%x", err); + return 0; + } + return bufsize; +} + +// Invoked when received SCSI WRITE10 command +// - Address = lba * BLOCK_SIZE + offset +// - Application write data from buffer to address contents (up to bufsize) and return number of written byte. +int32_t tud_msc_write10_cb(uint8_t lun, uint32_t lba, uint32_t offset, uint8_t *buffer, uint32_t bufsize) +{ + ESP_LOGD(TAG, "tud_msc_write10_cb() invoked, lun=%d, lba=%lu, offset=%lu", lun, lba, offset); + + size_t addr = lba * storage_get_sector_size() + offset; + esp_err_t err = storage_write_sector(addr, bufsize, buffer); + if (err != ESP_OK) { + ESP_LOGE(TAG, "storage_write_sector failed: 0x%x", err); + return 0; + } + return bufsize; +} + +/** + * Invoked when received an SCSI command not in built-in list below. + * - READ_CAPACITY10, READ_FORMAT_CAPACITY, INQUIRY, TEST_UNIT_READY, START_STOP_UNIT, MODE_SENSE6, REQUEST_SENSE + * - READ10 and WRITE10 has their own callbacks + * + * \param[in] lun Logical unit number + * \param[in] scsi_cmd SCSI command contents which application must examine to response accordingly + * \param[out] buffer Buffer for SCSI Data Stage. + * - For INPUT: application must fill this with response. + * - For OUTPUT it holds the Data from host + * \param[in] bufsize Buffer's length. + * + * \return Actual bytes processed, can be zero for no-data command. + * \retval negative Indicate error e.g unsupported command, tinyusb will \b STALL the corresponding + * endpoint and return failed status in command status wrapper phase. + */ +int32_t tud_msc_scsi_cb(uint8_t lun, uint8_t const scsi_cmd[16], void *buffer, uint16_t bufsize) +{ + int32_t ret; + + ESP_LOGD(TAG, "tud_msc_scsi_cb() invoked. bufsize=%d", bufsize); + + switch (scsi_cmd[0]) { + case SCSI_CMD_PREVENT_ALLOW_MEDIUM_REMOVAL: + /* SCSI_CMD_PREVENT_ALLOW_MEDIUM_REMOVAL is the Prevent/Allow Medium Removal + command (1Eh) that requests the library to enable or disable user access to + the storage media/partition. */ + ESP_LOGI(TAG, "tud_msc_scsi_cb() invoked: SCSI_CMD_PREVENT_ALLOW_MEDIUM_REMOVAL"); + ret = 0; + break; + default: + ESP_LOGW(TAG, "tud_msc_scsi_cb() invoked: %d", scsi_cmd[0]); + tud_msc_set_sense(lun, SCSI_SENSE_ILLEGAL_REQUEST, SCSI_CODE_ASC_INVALID_COMMAND_OPERATION_CODE, SCSI_CODE_ASCQ); + ret = -1; + break; + } + return ret; +} + +// Invoked when device is unmounted +void tud_umount_cb(void) +{ + is_eject = true; + ESP_LOGI(TAG, "tud_umount_cb: Mount on Example"); + _mount(); +} + +// Invoked when device is mounted (configured) +void tud_mount_cb(void) +{ + ESP_LOGI(TAG, "tud_mount_cb MSC START: Expose Over USB"); + _unmount(); +} + +/************* Application Code *******************/ + +/************* TinyUSB descriptors ****************/ +#define EPNUM_MSC 1 +#define TUSB_DESC_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_MSC_DESC_LEN) +#define VBUS_MONITORING_GPIO_NUM GPIO_NUM_4 + +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 uint8_t const desc_configuration[] = { + // 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, TUD_OPT_HIGH_SPEED ? 512 : 64), +}; + +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 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 +}; + +#define BASE_PATH "/data" // base path to mount the partition +static bool is_mount = false; + +// mount the partition and show all the files in BASE_PATH +static void _mount(void) +{ + ESP_LOGI(TAG, "Mount storage..."); + if (!is_mount) { + ESP_ERROR_CHECK(storage_mount(BASE_PATH)); + is_mount = true; + } + + // List all the files in this directory + ESP_LOGI(TAG, "\nls command output:"); + struct dirent *d; + DIR *dh = opendir(BASE_PATH); + if (!dh) { + if (errno = ENOENT) { + //If the directory is not found + ESP_LOGE(TAG, "Directory doesn't exist %s", BASE_PATH); + } else { + //If the directory is not readable then throw error and exit + ESP_LOGE(TAG, "Unable to read directory %s", BASE_PATH); + } + return; + } + //While the next entry is not readable we will print directory files + while ((d = readdir(dh)) != NULL) { + printf("%s\n", d->d_name); + } + return; +} + +// unmount the partition +static void _unmount(void) +{ + if (!is_mount) { + ESP_LOGD(TAG, "storage exposed over USB..."); + return; + } + ESP_LOGI(TAG, "Unmount storage..."); + ESP_ERROR_CHECK(storage_unmount()); + is_mount = false; + is_eject = false; +} + +static int f_unmount(int argc, char **argv) +{ + _unmount(); + return 0; +} + +// read BASE_PATH/README.MD and print its contents +static int f_read(int argc, char **argv) +{ + if (!is_mount) { + ESP_LOGE(TAG, "storage exposed over USB. Application can't read from storage."); + return -1; + } + ESP_LOGD(TAG, "read from storage:"); + const char *filename = BASE_PATH "/README.MD"; + FILE *ptr = fopen(filename, "r"); + if (ptr == NULL) { + ESP_LOGE(TAG, "Filename not present - %s", filename); + return -1; + } + char buf[1024]; + while (fgets(buf, 1000, ptr) != NULL) { + printf("%s", buf); + } + fclose(ptr); + return 0; +} + +// create file BASE_PATH/README.MD if it does not exist +static int f_write(int argc, char **argv) +{ + if (!is_mount) { + ESP_LOGE(TAG, "storage exposed over USB. Application can't write to storage."); + return -1; + } + ESP_LOGD(TAG, "write to storage:"); + const char *filename = BASE_PATH "/README.MD"; + FILE *fd = fopen(filename, "r"); + if (!fd) { + ESP_LOGW(TAG, "README.MD doesn't exist yet, creating"); + fd = fopen(filename, "w"); + fprintf(fd, "Mass Storage Devices are one of the most common USB devices. It use Mass Storage Class (MSC) that allow access to their internal data storage.\n"); + fprintf(fd, "In this example, ESP chip will be recognised by host (PC) as Mass Storage Device.\n"); + fprintf(fd, "Upon connection to USB host (PC), the example application will initialize the storage module and then the storage will be seen as removable device on PC.\n"); + fclose(fd); + } + return 0; +} + +// Show storage size and sector size +static int f_size(int argc, char **argv) +{ + if (!is_mount) { + ESP_LOGE(TAG, "storage exposed over USB. Application can't access storage"); + return -1; + } + size_t size = storage_get_size(); + size_t sec_size = storage_get_sector_size(); + printf("storage size(%d), sec_size(%d)\n", size, sec_size); + return 0; +} + +// exit from application +static int f_status(int argc, char **argv) +{ + printf("storage exposed over USB: %s\n", is_mount ? "No" : "Yes"); + return 0; +} + +// exit from application +static int f_exit(int argc, char **argv) +{ + printf("Application Exiting\n"); + exit(0); + return 0; +} + +void app_main(void) +{ + // Configure GPIO Pin for vbus monitorung + const gpio_config_t vbus_gpio_config = { + .pin_bit_mask = BIT64(VBUS_MONITORING_GPIO_NUM), + .mode = GPIO_MODE_INPUT, + .intr_type = GPIO_INTR_DISABLE, + .pull_up_en = true, + .pull_down_en = false, + }; + ESP_ERROR_CHECK(gpio_config(&vbus_gpio_config)); + + ESP_LOGI(TAG, "Initializing storage..."); + ESP_ERROR_CHECK(storage_init()); + + ESP_LOGI(TAG, "USB MSC initialization"); + const tinyusb_config_t tusb_cfg = { + .device_descriptor = &descriptor_config, + .string_descriptor = string_desc_arr, + .external_phy = false, + .configuration_descriptor = desc_configuration, + .self_powered = true, + .vbus_monitor_io = VBUS_MONITORING_GPIO_NUM, + }; + ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg)); + ESP_LOGI(TAG, "USB MSC initialization DONE"); + + //mounted in the app by default + _mount(); + + esp_console_repl_t *repl = NULL; + esp_console_repl_config_t repl_config = ESP_CONSOLE_REPL_CONFIG_DEFAULT(); + /* Prompt to be printed before each line. + * This can be customized, made dynamic, etc. + */ + repl_config.prompt = PROMPT_STR ">"; + repl_config.max_cmdline_length = 64; + esp_console_register_help_command(); + esp_console_dev_uart_config_t hw_config = ESP_CONSOLE_DEV_UART_CONFIG_DEFAULT(); + ESP_ERROR_CHECK(esp_console_new_repl_uart(&hw_config, &repl_config, &repl)); + + const esp_console_cmd_t cmd_read = { + .command = "read", + .help = "read BASE_PATH/README.MD and print its contents", + .hint = NULL, + .func = &f_read, + }; + ESP_ERROR_CHECK( esp_console_cmd_register(&cmd_read) ); + + const esp_console_cmd_t cmd_write = { + .command = "write", + .help = "create file BASE_PATH/README.MD if it does not exist", + .hint = NULL, + .func = &f_write, + }; + ESP_ERROR_CHECK( esp_console_cmd_register(&cmd_write) ); + + const esp_console_cmd_t cmd_size = { + .command = "size", + .help = "show storage size and sector size", + .hint = NULL, + .func = &f_size, + }; + ESP_ERROR_CHECK( esp_console_cmd_register(&cmd_size) ); + + const esp_console_cmd_t cmd_umount = { + .command = "expose", + .help = "Expose Storage to Host", + .hint = NULL, + .func = &f_unmount, + }; + ESP_ERROR_CHECK( esp_console_cmd_register(&cmd_umount) ); + + const esp_console_cmd_t cmd_status = { + .command = "status", + .help = "Status of storage exposure over USB", + .hint = NULL, + .func = &f_status, + }; + ESP_ERROR_CHECK( esp_console_cmd_register(&cmd_status) ); + + const esp_console_cmd_t cmd_exit = { + .command = "exit", + .help = "exit from application", + .hint = NULL, + .func = &f_exit, + }; + ESP_ERROR_CHECK( esp_console_cmd_register(&cmd_exit) ); + + ESP_ERROR_CHECK(esp_console_start_repl(repl)); +} diff --git a/examples/peripherals/usb/device/tusb_msc/main/tusb_msc_storage.c b/examples/peripherals/usb/device/tusb_msc/main/tusb_msc_storage.c new file mode 100644 index 00000000000..ddb7b230438 --- /dev/null +++ b/examples/peripherals/usb/device/tusb_msc/main/tusb_msc_storage.c @@ -0,0 +1,201 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +// DESCRIPTION: +// This file contains the code for accessing the storage medium ie SPI Flash. + +#include "esp_log.h" +#include "esp_err.h" +#include "esp_vfs_fat.h" +#include "diskio_impl.h" +#include "diskio_wl.h" +#include "wear_levelling.h" +#include "esp_partition.h" + +static wl_handle_t s_wl_handle = WL_INVALID_HANDLE; +static bool s_fat_mounted; +static const char *s_base_path; + +static const char *TAG = "example_msc_storage"; + +esp_err_t storage_init(void) +{ + ESP_LOGI(TAG, "Initializing wear levelling"); + esp_err_t err; + + const esp_partition_t *data_partition = esp_partition_find_first(ESP_PARTITION_TYPE_DATA, ESP_PARTITION_SUBTYPE_DATA_FAT, NULL); + if (data_partition == NULL) { + ESP_LOGE(TAG, "Failed to find FATFS partition. Check the partition table."); + return ESP_ERR_NOT_FOUND; + } + + err = wl_mount(data_partition, &s_wl_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "failed to mount wear levelling layer (0x%x)", err); + return err; + } + + return ESP_OK; +} + +static inline size_t esp_vfs_fat_get_allocation_unit_size( + size_t sector_size, size_t requested_size) +{ + size_t alloc_unit_size = requested_size; + const size_t max_sectors_per_cylinder = 128; + const size_t max_size = sector_size * max_sectors_per_cylinder; + alloc_unit_size = MAX(alloc_unit_size, sector_size); + alloc_unit_size = MIN(alloc_unit_size, max_size); + return alloc_unit_size; +} + +esp_err_t storage_mount(const char *base_path) +{ + const size_t workbuf_size = 4096; + void *workbuf = NULL; + esp_err_t err; + + if (s_fat_mounted) { + return ESP_OK; + } + + ESP_LOGI(TAG, "Initializing FAT"); + + // connect driver to FATFS + BYTE pdrv = 0xFF; + if (ff_diskio_get_drive(&pdrv) != ESP_OK) { + ESP_LOGE(TAG, "the maximum count of volumes is already mounted"); + return ESP_ERR_NO_MEM; + } + ESP_LOGD(TAG, "using pdrv=%i", pdrv); + char drv[3] = {(char)('0' + pdrv), ':', 0}; + + err = ff_diskio_register_wl_partition(pdrv, s_wl_handle); + if (err != ESP_OK) { + ESP_LOGE(TAG, "ff_diskio_register_wl_partition failed pdrv=%d (0x%x)", pdrv, err); + goto fail; + } + FATFS *fs; + err = esp_vfs_fat_register(base_path, drv, 2, &fs); + if (err == ESP_ERR_INVALID_STATE) { + // it's okay, already registered with VFS + } else if (err != ESP_OK) { + ESP_LOGE(TAG, "esp_vfs_fat_register failed (0x%x)", err); + goto fail; + } + + // Try to mount partition + FRESULT fresult = f_mount(fs, drv, 1); + if (fresult != FR_OK) { + ESP_LOGW(TAG, "f_mount failed (%d)", fresult); + if (!((fresult == FR_NO_FILESYSTEM || fresult == FR_INT_ERR))) { + err = ESP_FAIL; + goto fail; + } + workbuf = ff_memalloc(workbuf_size); + if (workbuf == NULL) { + err = ESP_ERR_NO_MEM; + goto fail; + } + size_t alloc_unit_size = esp_vfs_fat_get_allocation_unit_size( + CONFIG_WL_SECTOR_SIZE, + 4096); + + ESP_LOGI(TAG, "Formatting FATFS partition, allocation unit size=%d", alloc_unit_size); + const MKFS_PARM opt = {(BYTE)FM_FAT, 0, 0, 0, 0}; + fresult = f_mkfs("", &opt, workbuf, workbuf_size); // Use default volume + if (fresult != FR_OK) { + err = ESP_FAIL; + ESP_LOGE(TAG, "f_mkfs failed (%d)", fresult); + goto fail; + } + free(workbuf); + workbuf = NULL; + ESP_LOGI(TAG, "Mounting again"); + fresult = f_mount(fs, drv, 0); + if (fresult != FR_OK) { + err = ESP_FAIL; + ESP_LOGE(TAG, "f_mount failed after formatting (%d)", fresult); + goto fail; + } + } + s_fat_mounted = true; + s_base_path = base_path; + + return ESP_OK; + +fail: + free(workbuf); + esp_vfs_fat_unregister_path(base_path); + ff_diskio_unregister(pdrv); + s_fat_mounted = false; + ESP_LOGW(TAG, "Failed to mount storage (0x%x)", err); + return err; +} + +esp_err_t storage_unmount(void) +{ + if (!s_fat_mounted) { + return ESP_OK; + } + + BYTE pdrv = ff_diskio_get_pdrv_wl(s_wl_handle); + if (pdrv == 0xff) { + return ESP_ERR_INVALID_STATE; + } + char drv[3] = {(char)('0' + pdrv), ':', 0}; + + f_mount(0, drv, 0); + ff_diskio_unregister(pdrv); + ff_diskio_clear_pdrv_wl(s_wl_handle); + esp_err_t err = esp_vfs_fat_unregister_path(s_base_path); + s_base_path = NULL; + s_fat_mounted = false; + + return err; + +} + +size_t storage_get_size(void) +{ + assert(s_wl_handle != WL_INVALID_HANDLE); + + return wl_size(s_wl_handle); +} + +size_t storage_get_sector_size(void) +{ + assert(s_wl_handle != WL_INVALID_HANDLE); + + return wl_sector_size(s_wl_handle); +} + +esp_err_t storage_read_sector(size_t addr, size_t size, void *dest) +{ + assert(s_wl_handle != WL_INVALID_HANDLE); + + return wl_read(s_wl_handle, addr, dest, size); +} + +esp_err_t storage_write_sector(size_t addr, size_t size, const void *src) +{ + assert(s_wl_handle != WL_INVALID_HANDLE); + + if (s_fat_mounted) { + ESP_LOGE(TAG, "can't write, FAT mounted"); + return ESP_ERR_INVALID_STATE; + } + size_t sector_size = wl_sector_size(s_wl_handle); + if (addr % sector_size != 0 || size % sector_size != 0) { + return ESP_ERR_INVALID_ARG; + } + esp_err_t err = wl_erase_range(s_wl_handle, addr, size); + if (err != ESP_OK) { + ESP_LOGE(TAG, "wl_erase_range failed (0x%x)", err); + return err; + } + return wl_write(s_wl_handle, addr, src, size); +} diff --git a/examples/peripherals/usb/device/tusb_msc/main/tusb_msc_storage.h b/examples/peripherals/usb/device/tusb_msc/main/tusb_msc_storage.h new file mode 100644 index 00000000000..3f76bd696ef --- /dev/null +++ b/examples/peripherals/usb/device/tusb_msc/main/tusb_msc_storage.h @@ -0,0 +1,118 @@ +/* + * SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Unlicense OR CC0-1.0 + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include "esp_err.h" + +/* Public functions + ********************************************************************* */ + +/** + * @brief Initialize the Storage. + * + * First find 'first partition' based on one or more parameters (ie DATA, FAT). + * Mount WL for defined partition. + * Initialize the instance of WL instance. + * Once the storage is initialized, other storage functions can be used. + * + * @return esp_err_t + * - ESP_OK, if success; + * - ESP_ERR_NOT_FOUND, if Failed to find FATFS partition; + * - ESP_ERR_INVALID_ARG, if WL allocation was unsuccessful; + * - ESP_ERR_NO_MEM, if there was no memory to allocate WL components; + */ +esp_err_t storage_init(void); + +/** + * @brief Mount the storage partition locally on the firmware application. + * + * Get the available drive number. Register spi flash partition. + * Connect POSIX and C standard library IO function with FATFS. + * Mounts the partition. + * This API is used by the firmware application. If the storage partition is + * mounted by this API, host (PC) can't access the storage via MSC. + * + * @param base_path path prefix where FATFS should be registered + * @return esp_err_t + * - ESP_OK, if success; + * - ESP_ERR_NO_MEM if not enough memory or too many VFSes already registered; + */ +esp_err_t storage_mount(const char *base_path); + +/** + * @brief Unmount the storage partition from the firmware application. + * + * Unmount the partition. Unregister diskio driver. + * Unregister the SPI flash partition. + * Finally, Un-register FATFS from VFS. + * After this function is called, storage device can be seen (recongnized) by host (PC). + * + * @return esp_err_t + * - ESP_OK on success + * - ESP_ERR_INVALID_STATE if FATFS is not registered in VFS + */ +esp_err_t storage_unmount(void); + +/** + * @brief Get size of the WL storage + * + * @return usable size, in bytes + */ +size_t storage_get_size(void); + +/** + * @brief Get sector size of the WL instance + * + * @return sector size, in bytes + */ +size_t storage_get_sector_size(void); + +/** + * @brief Read data from the WL storage + * + * @param addr Address of the data to be read, relative to the + * beginning of the partition. + * @param size Size of data to be read, in bytes. + * @param dest Pointer to the buffer where data should be stored. + * Pointer must be non-NULL and buffer must be at least 'size' bytes long. + * @return esp_err_t + * - ESP_OK, if data was read successfully; + * - ESP_ERR_INVALID_ARG, if src_offset exceeds partition size; + * - ESP_ERR_INVALID_SIZE, if read would go out of bounds of the partition; + * - or one of error codes from lower-level flash driver. + */ +esp_err_t storage_read_sector(size_t addr, size_t size, void *dest); + +/** + * @brief Write data to the WL storage + * + * Before writing data to flash, corresponding region of flash needs to be erased. + * This is done internally using wl_erase_range function. + * + * @param addr Address where the data should be written, relative to the + * beginning of the partition. + * @param size Size of data to be written, in bytes. + * @param src Pointer to the source buffer. Pointer must be non-NULL and + * buffer must be at least 'size' bytes long. + * @return esp_err_t + * - ESP_OK, if data was written successfully; + * - ESP_ERR_INVALID_ARG, if dst_offset exceeds partition size; + * - ESP_ERR_INVALID_SIZE, if write would go out of bounds of the partition; + * - or one of error codes from lower-level flash driver. + */ +esp_err_t storage_write_sector(size_t addr, size_t size, const void *src); + +/*********************************************************************** Public functions*/ + +#ifdef __cplusplus +} +#endif diff --git a/examples/peripherals/usb/device/tusb_msc/partitions.csv b/examples/peripherals/usb/device/tusb_msc/partitions.csv new file mode 100644 index 00000000000..1c79321a107 --- /dev/null +++ b/examples/peripherals/usb/device/tusb_msc/partitions.csv @@ -0,0 +1,6 @@ +# Name, Type, SubType, Offset, Size, Flags +# Note: if you have increased the bootloader size, make sure to update the offsets to avoid overlap +nvs, data, nvs, 0x9000, 0x6000, +phy_init, data, phy, 0xf000, 0x1000, +factory, app, factory, 0x10000, 1M, +storage, data, fat, , 1M, diff --git a/examples/peripherals/usb/device/tusb_msc/pytest_usb_device_msc.py b/examples/peripherals/usb/device/tusb_msc/pytest_usb_device_msc.py new file mode 100644 index 00000000000..b59aaea7332 --- /dev/null +++ b/examples/peripherals/usb/device/tusb_msc/pytest_usb_device_msc.py @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2022 Espressif Systems (Shanghai) CO LTD +# SPDX-License-Identifier: CC0-1.0 +import pytest +from pytest_embedded import Dut + + +@pytest.mark.esp32s2 +@pytest.mark.usb_device +def test_usb_device_msc_example(dut: Dut) -> None: + dut.expect('USB MSC initialization DONE') + dut.expect('Mount storage') + dut.expect('Initializing FAT') + dut.write(' help') + dut.expect('read') + dut.expect('write') + dut.expect('size') + dut.expect('expose') + dut.expect('status') + dut.write(' status') + dut.expect('storage exposed over USB') diff --git a/examples/peripherals/usb/device/tusb_msc/sdkconfig.defaults b/examples/peripherals/usb/device/tusb_msc/sdkconfig.defaults new file mode 100644 index 00000000000..74355e7a93b --- /dev/null +++ b/examples/peripherals/usb/device/tusb_msc/sdkconfig.defaults @@ -0,0 +1,14 @@ +# This file was generated using idf.py save-defconfig. It can be edited manually. +# Espressif IoT Development Framework (ESP-IDF) Project Minimal Configuration +# +CONFIG_TINYUSB=y +CONFIG_TINYUSB_MSC_ENABLED=y + +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" +CONFIG_PARTITION_TABLE_FILENAME="partitions.csv" +CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y +CONFIG_WL_SECTOR_SIZE_512=y +CONFIG_WL_SECTOR_MODE_PERF=y + +CONFIG_FATFS_LFN_HEAP=y