From 89d685991a27efc48eadb7d3eb7be0611ca3d542 Mon Sep 17 00:00:00 2001 From: Martin Valik Date: Tue, 19 Jul 2022 09:30:32 +0200 Subject: [PATCH] usb: Add MSC class driver --- .github/workflows/upload_component.yml | 2 +- usb/test_app/CMakeLists.txt | 4 +- usb/usb_host_msc/CMakeLists.txt | 9 + usb/usb_host_msc/LICENCE | 202 +++++++ usb/usb_host_msc/README.md | 36 ++ usb/usb_host_msc/idf_component.yml | 10 + usb/usb_host_msc/include/msc_host.h | 169 ++++++ usb/usb_host_msc/include/msc_host_vfs.h | 44 ++ usb/usb_host_msc/private_include/diskio_usb.h | 39 ++ usb/usb_host_msc/private_include/msc_common.h | 61 ++ .../private_include/msc_scsi_bot.h | 56 ++ usb/usb_host_msc/src/diskio_usb.c | 118 ++++ usb/usb_host_msc/src/msc_host.c | 559 ++++++++++++++++++ usb/usb_host_msc/src/msc_host_vfs.c | 124 ++++ usb/usb_host_msc/src/msc_scsi_bot.c | 434 ++++++++++++++ usb/usb_host_msc/test/CMakeLists.txt | 3 + usb/usb_host_msc/test/msc_device.c | 302 ++++++++++ usb/usb_host_msc/test/test_common.h | 19 + usb/usb_host_msc/test/test_msc.c | 324 ++++++++++ 19 files changed, 2512 insertions(+), 3 deletions(-) create mode 100644 usb/usb_host_msc/CMakeLists.txt create mode 100644 usb/usb_host_msc/LICENCE create mode 100644 usb/usb_host_msc/README.md create mode 100644 usb/usb_host_msc/idf_component.yml create mode 100644 usb/usb_host_msc/include/msc_host.h create mode 100644 usb/usb_host_msc/include/msc_host_vfs.h create mode 100644 usb/usb_host_msc/private_include/diskio_usb.h create mode 100644 usb/usb_host_msc/private_include/msc_common.h create mode 100644 usb/usb_host_msc/private_include/msc_scsi_bot.h create mode 100644 usb/usb_host_msc/src/diskio_usb.c create mode 100644 usb/usb_host_msc/src/msc_host.c create mode 100644 usb/usb_host_msc/src/msc_host_vfs.c create mode 100644 usb/usb_host_msc/src/msc_scsi_bot.c create mode 100644 usb/usb_host_msc/test/CMakeLists.txt create mode 100644 usb/usb_host_msc/test/msc_device.c create mode 100644 usb/usb_host_msc/test/test_common.h create mode 100644 usb/usb_host_msc/test/test_msc.c diff --git a/.github/workflows/upload_component.yml b/.github/workflows/upload_component.yml index b198b3b..b8348b8 100644 --- a/.github/workflows/upload_component.yml +++ b/.github/workflows/upload_component.yml @@ -15,6 +15,6 @@ jobs: - name: Upload components to component service uses: espressif/upload-components-ci-action@v1 with: - directories: "cbor;jsmn;libsodium;pid_ctrl;qrcode;nghttp;sh2lib;expat;esp_encrypted_img;coap;pcap;json_generator;json_parser;usb/usb_host_cdc_acm" + directories: "cbor;jsmn;libsodium;pid_ctrl;qrcode;nghttp;sh2lib;expat;esp_encrypted_img;coap;pcap;json_generator;json_parser;usb/usb_host_cdc_acm;usb/usb_host_msc" namespace: "espressif" api_token: ${{ secrets.IDF_COMPONENT_API_TOKEN }} diff --git a/usb/test_app/CMakeLists.txt b/usb/test_app/CMakeLists.txt index e11974c..8af765c 100644 --- a/usb/test_app/CMakeLists.txt +++ b/usb/test_app/CMakeLists.txt @@ -3,9 +3,9 @@ cmake_minimum_required(VERSION 3.16) set(EXTRA_COMPONENT_DIRS ../usb_host_cdc_acm - $ENV{IDF_PATH}/examples/peripherals/usb/host/msc/components/) + ../usb_host_msc) # Set the components to include the tests for. -set(TEST_COMPONENTS "usb_host_cdc_acm" "msc" CACHE STRING "List of components to test") +set(TEST_COMPONENTS "usb_host_cdc_acm" "usb_host_msc" CACHE STRING "List of components to test") include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(usb_test_app) diff --git a/usb/usb_host_msc/CMakeLists.txt b/usb/usb_host_msc/CMakeLists.txt new file mode 100644 index 0000000..36848f1 --- /dev/null +++ b/usb/usb_host_msc/CMakeLists.txt @@ -0,0 +1,9 @@ +set(sources src/msc_scsi_bot.c + src/diskio_usb.c + src/msc_host.c + src/msc_host_vfs.c) + +idf_component_register( SRCS ${sources} + INCLUDE_DIRS include + PRIV_INCLUDE_DIRS private_include + REQUIRES usb fatfs ) diff --git a/usb/usb_host_msc/LICENCE b/usb/usb_host_msc/LICENCE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/usb/usb_host_msc/LICENCE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/usb/usb_host_msc/README.md b/usb/usb_host_msc/README.md new file mode 100644 index 0000000..716b262 --- /dev/null +++ b/usb/usb_host_msc/README.md @@ -0,0 +1,36 @@ +# USB Host MSC (Mass Storage Class) Driver + +This directory contains an implementation of a USB Mass Storage Class Driver implemented on top of the [USB Host Library](https://docs.espressif.com/projects/esp-idf/en/latest/esp32s2/api-reference/peripherals/usb_host.html). + +MSC driver allows access to USB flash drivers using the BOT “Bulk-Only Transport” protocol and the Transparent SCSI command set. + +## Usage + +- First, usb host library has to be initialized by calling `usb_host_install` +- USB Host Library events have to be handled by invoking `usb_host_lib_handle_events` periodically. + In general, an application should spawn a dedicated task handle USB Host Library events. + However, in order to save RAM, an already existing task can also be used to call `usb_host_lib_handle_events`. +- Mass Storage Class driver is installed by calling `usb_msc_install` function along side with configuration. +- Supplied configuration contains user provided callback function invoked whenever MSC device is connected/disconnected + and optional parameters for creating background task handling MSC related events. + Alternatively, user can call `usb_msc_handle_events` function from already existing task. +- After receiving `MSC_DEVICE_CONNECTED` event, user has to install device with `usb_msc_install_device` function, + obtaining MSC device handle. +- USB descriptors can be printed out with `usb_msc_print_descriptors` and general information about MSC device retrieved + with `from usb_msc_get_device_info` function. +- Obtained device handle is then used in helper function `usb_msc_vfs_register` mounting USB Disk to Virtual filesystem. +- At this point, standard C functions for accessing storage (`fopen`, `fwrite`, `fread`, `mkdir` etc.) can be carried out. +- In order to uninstall the whole USB stack, deinitializing counterparts to functions above has to be called in reverse order. + +## Known issues + +- Driver only supports USB 2.0 flash drives using the BOT “Bulk-Only Transport” protocol and the Transparent SCSI command set +- Composite USB devices are not supported + +## Examples + +- For an example, refer to [msc_host_example](https://github.com/espressif/esp-idf/tree/master/examples/peripherals/usb/host/msc) + +## Troubleshooting + +After connecting composite USB device, driver prints `COMPOSITE DEVICES UNSUPPORTED` diff --git a/usb/usb_host_msc/idf_component.yml b/usb/usb_host_msc/idf_component.yml new file mode 100644 index 0000000..f4256b6 --- /dev/null +++ b/usb/usb_host_msc/idf_component.yml @@ -0,0 +1,10 @@ +version: "1.0.0" +description: USB Host MSC driver +url: https://github.com/espressif/idf-extra-components/tree/master/usb/usb_host_msc + +dependencies: + idf: ">=4.4" + +targets: + - esp32s2 + - esp32s3 diff --git a/usb/usb_host_msc/include/msc_host.h b/usb/usb_host_msc/include/msc_host.h new file mode 100644 index 0000000..0580376 --- /dev/null +++ b/usb/usb_host_msc/include/msc_host.h @@ -0,0 +1,169 @@ +/* + * SPDX-FileCopyrightText: 2015-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include "esp_err.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define ESP_ERR_MSC_HOST_BASE 0x1700 /*!< MSC host error code base */ +#define ESP_ERR_MSC_MOUNT_FAILED (ESP_ERR_MSC_HOST_BASE + 1) /*!< Failed to mount storage */ +#define ESP_ERR_MSC_FORMAT_FAILED (ESP_ERR_MSC_HOST_BASE + 2) /*!< Failed to format storage */ +#define ESP_ERR_MSC_INTERNAL (ESP_ERR_MSC_HOST_BASE + 3) /*!< MSC host internal error */ + +#define MSC_STR_DESC_SIZE 32 + +typedef struct msc_host_device *msc_host_device_handle_t; /**< Handle to a Mass Storage Device */ + +/** + * @brief USB Mass Storage event containing event type and associated device handle. +*/ +typedef struct { + enum { + MSC_DEVICE_CONNECTED, /**< MSC device has been connected to the system.*/ + MSC_DEVICE_DISCONNECTED, /**< MSC device has been disconnected from the system.*/ + } event; + union { + uint8_t address; /**< Address of connected MSC device.*/ + msc_host_device_handle_t handle; /**< MSC device handle to disconnected device.*/ + } device; +} msc_host_event_t; + +/** + * @brief USB Mass Storage event callback. + * + * @param[in] event mass storage event +*/ +typedef void (*msc_host_event_cb_t)(const msc_host_event_t *event, void *arg); + +/** + * @brief MSC configuration structure. +*/ +typedef struct { + bool create_backround_task; /**< When set to true, background task handling usb events is created. + Otherwise user has to periodically call msc_host_handle_events function */ + size_t task_priority; /**< Task priority of crated background task */ + size_t stack_size; /**< Stack size of crated background task */ + BaseType_t core_id; /**< Select core on which background task will run or tskNO_AFFINITY */ + msc_host_event_cb_t callback; /**< Callback invoked when MSC event occurs. Must not be NULL. */ + void *callback_arg; /**< User provided argument passed to callback */ +} msc_host_driver_config_t; + +/** + * @brief MSC device info. +*/ +typedef struct { + uint32_t sector_count; + uint32_t sector_size; + uint16_t idProduct; + uint16_t idVendor; + wchar_t iManufacturer[MSC_STR_DESC_SIZE]; + wchar_t iProduct[MSC_STR_DESC_SIZE]; + wchar_t iSerialNumber[MSC_STR_DESC_SIZE]; +} msc_host_device_info_t; + +/** + * @brief Install USB Host Mass Storage Class driver + * + * @param[in] config configuration structure MSC to create + * @return esp_err_r + */ +esp_err_t msc_host_install(const msc_host_driver_config_t *config); + +/** + * @brief Uninstall Mass Storage Class driver + * @return esp_err_t + */ +esp_err_t msc_host_uninstall(void); + +/** + * @brief Initialization of MSC device. + * + * @param[in] device_address Device address obtained from MSC callback provided upon connection and enumeration + * @param[out] device Mass storage device handle to be used for subsequent calls. + * @return esp_err_t + */ +esp_err_t msc_host_install_device(uint8_t device_address, msc_host_device_handle_t *device); + +/** + * @brief Deinitialization of MSC device. + * + * @param[in] device Device handle obtained from msc_host_install_device function + * @return esp_err_t + */ +esp_err_t msc_host_uninstall_device(msc_host_device_handle_t device); + +/** + * @brief Helper function for reading sector from mass storage device. + * + * @warning This call is not thread safe and should not be combined + * with accesses to storage through file system. + * + * @note Provided sector and size cannot exceed + * sector_count and sector_size obtained from msc_host_device_info_t + * + * @param[in] device Device handle + * @param[in] sector Number of sector to be read + * @param[out] data Buffer into which data will be written + * @param[in] size Number of bytes to be read + * @return esp_err_t + */ +esp_err_t msc_host_read_sector(msc_host_device_handle_t device, size_t sector, void *data, size_t size); + +/** + * @brief Helper function for writing sector to mass storage device. + * + * @warning This call is not thread safe and should not be combined + * with accesses to storare through file system. + * + * @note Provided sector and size cannot exceed + * sector_count and sector_size obtained from msc_host_device_info_t + * + * @param[in] device Device handle + * @param[in] sector Number of sector to be read + * @param[in] data Data to be written to the sector + * @param[in] size Number of bytes to be written + * @return esp_err_t + */ +esp_err_t msc_host_write_sector(msc_host_device_handle_t device, size_t sector, const void *data, size_t size); + +/** + * @brief Handle MSC HOST events. + * + * @param[in] timeout_ms Timeout in miliseconds + * @return esp_err_t + */ +esp_err_t msc_host_handle_events(uint32_t timeout_ms); + +/** + * @brief Gets devices information. + * + * @warning This call is not thread safe and should not be combined + * with accesses to storare through file system. + * + * @param[in] device Handle to device + * @param[out] info Structure to be populated with device info + * @return esp_err_t + */ +esp_err_t msc_host_get_device_info(msc_host_device_handle_t device, msc_host_device_info_t *info); + +/** + * @brief Print configuration descriptor. + * + * @param[in] device Handle of MSC device + * @return esp_err_t + */ +esp_err_t msc_host_print_descriptors(msc_host_device_handle_t device); + +#ifdef __cplusplus +} +#endif //__cplusplus diff --git a/usb/usb_host_msc/include/msc_host_vfs.h b/usb/usb_host_msc/include/msc_host_vfs.h new file mode 100644 index 0000000..af9137f --- /dev/null +++ b/usb/usb_host_msc/include/msc_host_vfs.h @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: 2015-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "esp_vfs_fat.h" +#include "msc_host.h" +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct msc_host_vfs *msc_host_vfs_handle_t; /**< VFS handle to attached Mass Storage device */ + +/** + * @brief Register MSC device to Virtual filesystem. + * + * @param[in] device Device handle obtained from MSC callback provided upon initialization + * @param[in] base_path Base VFS path to be used to access file storage + * @param[in] mount_config Mount configuration. + * @param[out] vfs_handle Handle to MSC device associated with registered VFS + * @return esp_err_t + */ +esp_err_t msc_host_vfs_register(msc_host_device_handle_t device, + const char *base_path, + const esp_vfs_fat_mount_config_t *mount_config, + msc_host_vfs_handle_t *vfs_handle); + + +/** + * @brief Unregister MSC device from Virtual filesystem. + * + * @param[in] vfs_handle VFS handle obtained from MSC callback provided upon initialization + * @return esp_err_t + */ +esp_err_t msc_host_vfs_unregister(msc_host_vfs_handle_t vfs_handle); + +#ifdef __cplusplus +} +#endif diff --git a/usb/usb_host_msc/private_include/diskio_usb.h b/usb/usb_host_msc/private_include/diskio_usb.h new file mode 100644 index 0000000..6327d6e --- /dev/null +++ b/usb/usb_host_msc/private_include/diskio_usb.h @@ -0,0 +1,39 @@ +/* + * SPDX-FileCopyrightText: 2015-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Mass storage disk initialization structure + */ +typedef struct { + uint32_t block_size; /**< Block size */ + uint32_t block_count; /**< Block count */ +} usb_disk_t; + +/** + * @brief Register mass storage disk to fat file system + * + * @param[in] pdrv Number of free drive obtained from ff_diskio_get_drive() function + * @param[in] disk usb_disk_t structure + */ +void ff_diskio_register_msc(uint8_t pdrv, usb_disk_t *disk); + +/** + * @brief Obtains number of drive assigned to usb disk upon calling ff_diskio_register_msc() + * + * @param[in] disk usb_disk_t structure + * @return Drive number + */ +uint8_t ff_diskio_get_pdrv_disk(const usb_disk_t *disk); + +#ifdef __cplusplus +} +#endif //__cplusplus diff --git a/usb/usb_host_msc/private_include/msc_common.h b/usb/usb_host_msc/private_include/msc_common.h new file mode 100644 index 0000000..a5bed31 --- /dev/null +++ b/usb/usb_host_msc/private_include/msc_common.h @@ -0,0 +1,61 @@ +/* + * SPDX-FileCopyrightText: 2015-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include +#include "esp_err.h" +#include "esp_check.h" +#include "diskio_usb.h" +#include "usb/usb_host.h" +#include "usb/usb_types_stack.h" +#include "freertos/semphr.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +typedef enum { + MSC_EP_OUT, + MSC_EP_IN +} msc_endpoint_t; + +typedef struct { + uint16_t bulk_in_mps; + uint8_t bulk_in_ep; + uint8_t bulk_out_ep; + uint8_t iface_num; +} msc_config_t; + +typedef struct msc_host_device { + STAILQ_ENTRY(msc_host_device) tailq_entry; + usb_transfer_status_t transfer_status; + SemaphoreHandle_t transfer_done; + usb_device_handle_t handle; + usb_transfer_t *xfer; + msc_config_t config; + usb_disk_t disk; +} msc_device_t; + +esp_err_t msc_bulk_transfer(msc_device_t *device_handle, uint8_t *data, size_t size, msc_endpoint_t ep); + +esp_err_t msc_control_transfer(msc_device_t *device_handle, usb_transfer_t *xfer, size_t len); + +#define MSC_GOTO_ON_ERROR(exp) ESP_GOTO_ON_ERROR(exp, fail, TAG, "") + +#define MSC_GOTO_ON_FALSE(exp, err) ESP_GOTO_ON_FALSE( (exp), err, fail, TAG, "" ) + +#define MSC_RETURN_ON_ERROR(exp) ESP_RETURN_ON_ERROR((exp), TAG, "") + +#define MSC_RETURN_ON_FALSE(exp, err) ESP_RETURN_ON_FALSE( (exp), (err), TAG, "") + +#define MSC_RETURN_ON_INVALID_ARG(exp) ESP_RETURN_ON_FALSE((exp) != NULL, ESP_ERR_INVALID_ARG, TAG, "") + +#ifdef __cplusplus +} +#endif diff --git a/usb/usb_host_msc/private_include/msc_scsi_bot.h b/usb/usb_host_msc/private_include/msc_scsi_bot.h new file mode 100644 index 0000000..d4ee210 --- /dev/null +++ b/usb/usb_host_msc/private_include/msc_scsi_bot.h @@ -0,0 +1,56 @@ +/* + * SPDX-FileCopyrightText: 2015-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include +#include "esp_err.h" +#include "msc_common.h" + +#ifdef __cplusplus +extern "C" +{ +#endif + +typedef struct { + uint8_t key; + uint8_t code; + uint8_t code_q; +} scsi_sense_data_t; + +esp_err_t scsi_cmd_read10(msc_device_t *device, + uint8_t *data, + uint32_t sector_address, + uint32_t num_sectors, + uint32_t sector_size); + +esp_err_t scsi_cmd_write10(msc_device_t *device, + const uint8_t *data, + uint32_t sector_address, + uint32_t num_sectors, + uint32_t sector_size); + +esp_err_t scsi_cmd_read_capacity(msc_device_t *device, + uint32_t *block_size, + uint32_t *block_count); + +esp_err_t scsi_cmd_sense(msc_device_t *device, scsi_sense_data_t *sense); + +esp_err_t scsi_cmd_unit_ready(msc_device_t *device); + +esp_err_t scsi_cmd_inquiry(msc_device_t *device); + +esp_err_t scsi_cmd_prevent_removal(msc_device_t *device, bool prevent); + +esp_err_t scsi_cmd_mode_sense(msc_device_t *device); + +esp_err_t msc_mass_reset(msc_device_t *device); + +esp_err_t msc_get_max_lun(msc_device_t *device, uint8_t *lun); + +#ifdef __cplusplus +} +#endif diff --git a/usb/usb_host_msc/src/diskio_usb.c b/usb/usb_host_msc/src/diskio_usb.c new file mode 100644 index 0000000..0191f28 --- /dev/null +++ b/usb/usb_host_msc/src/diskio_usb.c @@ -0,0 +1,118 @@ +/* + * SPDX-FileCopyrightText: 2015-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "diskio_impl.h" +#include "ffconf.h" +#include "ff.h" +#include "esp_log.h" +#include "diskio_usb.h" +#include "msc_scsi_bot.h" +#include "msc_common.h" +#include "usb/usb_types_stack.h" + +static usb_disk_t *s_disks[FF_VOLUMES] = { NULL }; + +static const char *TAG = "diskio_usb"; + +static DSTATUS usb_disk_initialize (BYTE pdrv) +{ + return RES_OK; +} + +static DSTATUS usb_disk_status (BYTE pdrv) +{ + return RES_OK; +} + +static DRESULT usb_disk_read (BYTE pdrv, BYTE *buff, DWORD sector, UINT count) +{ + assert(pdrv < FF_VOLUMES); + assert(s_disks[pdrv]); + + esp_err_t err; + usb_disk_t *disk = s_disks[pdrv]; + size_t sector_size = disk->block_size; + msc_device_t *dev = __containerof(disk, msc_device_t, disk); + + for (int i = 0; i < count; i++) { + err = scsi_cmd_read10(dev, &buff[i * sector_size], sector + i, 1, sector_size); + if (err != ESP_OK) { + ESP_LOGE(TAG, "scsi_cmd_read10 failed (%d)", err); + return RES_ERROR; + } + + } + + return RES_OK; +} + +static DRESULT usb_disk_write (BYTE pdrv, const BYTE *buff, DWORD sector, UINT count) +{ + assert(pdrv < FF_VOLUMES); + assert(s_disks[pdrv]); + + esp_err_t err; + usb_disk_t *disk = s_disks[pdrv]; + size_t sector_size = disk->block_size; + msc_device_t *dev = __containerof(disk, msc_device_t, disk); + + for (int i = 0; i < count; i++) { + err = scsi_cmd_write10(dev, &buff[i * sector_size], sector + i, 1, sector_size); + if (err != ESP_OK) { + ESP_LOGE(TAG, "scsi_cmd_write10 failed (%d)", err); + return RES_ERROR; + } + + } + return RES_OK; +} + +static DRESULT usb_disk_ioctl (BYTE pdrv, BYTE cmd, void *buff) +{ + assert(pdrv < FF_VOLUMES); + assert(s_disks[pdrv]); + + usb_disk_t *disk = s_disks[pdrv]; + + switch (cmd) { + case CTRL_SYNC: + return RES_OK; + case GET_SECTOR_COUNT: + *((DWORD *) buff) = disk->block_count; + return RES_OK; + case GET_SECTOR_SIZE: + *((WORD *) buff) = disk->block_size; + return RES_OK; + case GET_BLOCK_SIZE: + return RES_ERROR; + } + return RES_ERROR; +} + +void ff_diskio_register_msc(BYTE pdrv, usb_disk_t *disk) +{ + 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_disks[pdrv] = disk; + ff_diskio_register(pdrv, &usb_disk_impl); +} + +BYTE ff_diskio_get_pdrv_disk(const usb_disk_t *disk) +{ + for (int i = 0; i < FF_VOLUMES; i++) { + if (disk == s_disks[i]) { + return i; + } + } + return 0xff; +} diff --git a/usb/usb_host_msc/src/msc_host.c b/usb/usb_host_msc/src/msc_host.c new file mode 100644 index 0000000..f63f4f8 --- /dev/null +++ b/usb/usb_host_msc/src/msc_host.c @@ -0,0 +1,559 @@ +/* + * SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/semphr.h" +#include "usb/usb_host.h" +#include "diskio_usb.h" +#include "msc_common.h" +#include "msc_host.h" +#include "msc_scsi_bot.h" +#include "usb/usb_types_ch9.h" +#include "usb/usb_helpers.h" + +static portMUX_TYPE msc_lock = portMUX_INITIALIZER_UNLOCKED; + +#define MSC_ENTER_CRITICAL() portENTER_CRITICAL(&msc_lock) +#define MSC_EXIT_CRITICAL() portEXIT_CRITICAL(&msc_lock) + +#define MSC_GOTO_ON_FALSE_CRITICAL(exp, err) \ + do { \ + if(!(exp)) { \ + MSC_EXIT_CRITICAL(); \ + ret = err; \ + goto fail; \ + } \ + } while(0) + +#define MSC_RETURN_ON_FALSE_CRITICAL(exp, err) \ + do { \ + if(!(exp)) { \ + MSC_EXIT_CRITICAL(); \ + return err; \ + } \ + } while(0) + +#define WAIT_FOR_READY_TIMEOUT_MS 3000 +#define TAG "USB_MSC" + +#define SCSI_COMMAND_SET 0x06 +#define BULK_ONLY_TRANSFER 0x50 +#define MSC_NO_SENSE 0x00 +#define MSC_NOT_READY 0x02 +#define MSC_UNIT_ATTENTION 0x06 + +typedef struct { + usb_host_client_handle_t client_handle; + msc_host_event_cb_t user_cb; + void *user_arg; + SemaphoreHandle_t all_events_handled; + volatile bool end_client_event_handling; +} msc_driver_t; + +static msc_driver_t *s_msc_driver; + +STAILQ_HEAD(devices, msc_host_device) devices_tailq; + +static const usb_standard_desc_t *next_interface_desc(const usb_standard_desc_t *desc, size_t len, size_t *offset) +{ + return usb_parse_next_descriptor_of_type(desc, len, USB_W_VALUE_DT_INTERFACE, (int *)offset); +} + +static const usb_standard_desc_t *next_endpoint_desc(const usb_standard_desc_t *desc, size_t len, size_t *offset) +{ + return usb_parse_next_descriptor_of_type(desc, len, USB_B_DESCRIPTOR_TYPE_ENDPOINT, (int *)offset); +} + +static const usb_intf_desc_t *find_msc_interface(const usb_config_desc_t *config_desc, size_t *offset) +{ + size_t total_length = config_desc->wTotalLength; + const usb_standard_desc_t *next_desc = (const usb_standard_desc_t *)config_desc; + + next_desc = next_interface_desc(next_desc, total_length, offset); + + while ( next_desc ) { + + const usb_intf_desc_t *ifc_desc = (const usb_intf_desc_t *)next_desc; + + if ( ifc_desc->bInterfaceClass == USB_CLASS_MASS_STORAGE && + ifc_desc->bInterfaceSubClass == SCSI_COMMAND_SET && + ifc_desc->bInterfaceProtocol == BULK_ONLY_TRANSFER ) { + return ifc_desc; + } + + next_desc = next_interface_desc(next_desc, total_length, offset); + }; + return NULL; +} + +/** + * @brief Extracts configuration from configuration descriptor. + * + * @note Passes interface and endpoint descriptors to obtain: + + * - interface number, IN endpoint, OUT endpoint, max. packet size + * + * @param[in] cfg_desc Configuration descriptor + * @param[out] cfg Obtained configuration + * @return esp_err_t + */ +static esp_err_t extract_config_from_descriptor(const usb_config_desc_t *cfg_desc, msc_config_t *cfg) +{ + size_t offset = 0; + size_t total_len = cfg_desc->wTotalLength; + const usb_intf_desc_t *ifc_desc = find_msc_interface(cfg_desc, &offset); + assert(ifc_desc); + const usb_standard_desc_t *next_desc = (const usb_standard_desc_t *)ifc_desc; + const usb_ep_desc_t *ep_desc = NULL; + + cfg->iface_num = ifc_desc->bInterfaceNumber; + + next_desc = next_endpoint_desc(next_desc, total_len, &offset); + MSC_RETURN_ON_FALSE(next_desc, ESP_ERR_NOT_SUPPORTED); + ep_desc = (const usb_ep_desc_t *)next_desc; + + if (ep_desc->bEndpointAddress & 0x80) { + cfg->bulk_in_ep = ep_desc->bEndpointAddress; + cfg->bulk_in_mps = ep_desc->wMaxPacketSize; + } else { + cfg->bulk_out_ep = ep_desc->bEndpointAddress; + } + + next_desc = next_endpoint_desc(next_desc, total_len, &offset); + MSC_RETURN_ON_FALSE(next_desc, ESP_ERR_NOT_SUPPORTED); + ep_desc = (const usb_ep_desc_t *)next_desc; + + if (ep_desc->bEndpointAddress & 0x80) { + cfg->bulk_in_ep = ep_desc->bEndpointAddress; + cfg->bulk_in_mps = ep_desc->wMaxPacketSize; + } else { + cfg->bulk_out_ep = ep_desc->bEndpointAddress; + } + + return ESP_OK; +} + +static esp_err_t msc_deinit_device(msc_device_t *dev, bool install_failed) +{ + MSC_ENTER_CRITICAL(); + MSC_RETURN_ON_FALSE_CRITICAL( dev, ESP_ERR_INVALID_STATE ); + STAILQ_REMOVE(&devices_tailq, dev, msc_host_device, tailq_entry); + MSC_EXIT_CRITICAL(); + + if (dev->transfer_done) { + vSemaphoreDelete(dev->transfer_done); + } + if (install_failed) { + // Error code is unchecked, as it's unknown at what point installation failed. + usb_host_interface_release(s_msc_driver->client_handle, dev->handle, dev->config.iface_num); + usb_host_device_close(s_msc_driver->client_handle, dev->handle); + usb_host_transfer_free(dev->xfer); + } else { + MSC_RETURN_ON_ERROR( usb_host_interface_release(s_msc_driver->client_handle, dev->handle, dev->config.iface_num) ); + MSC_RETURN_ON_ERROR( usb_host_device_close(s_msc_driver->client_handle, dev->handle) ); + MSC_RETURN_ON_ERROR( usb_host_transfer_free(dev->xfer) ); + } + + free(dev); + return ESP_OK; +} + +// Some MSC devices requires to change its internal state from non-ready to ready +static esp_err_t msc_wait_for_ready_state(msc_device_t *dev, size_t timeout_ms) +{ + esp_err_t err; + scsi_sense_data_t sense; + uint32_t trials = MAX(1, timeout_ms / 100); + + do { + err = scsi_cmd_unit_ready(dev); + if (err != ESP_OK) { + MSC_RETURN_ON_ERROR( scsi_cmd_sense(dev, &sense) ); + if (sense.key != MSC_NOT_READY && + sense.key != MSC_UNIT_ATTENTION && + sense.key != MSC_NO_SENSE) { + return ESP_ERR_MSC_INTERNAL; + } + } + vTaskDelay( pdMS_TO_TICKS(100) ); + } while (trials-- && err); + + return err; +} + +static bool is_mass_storage_device(uint8_t dev_addr) +{ + size_t dummy = 0; + bool is_msc_device = false; + usb_device_handle_t device; + const usb_config_desc_t *config_desc; + + if ( usb_host_device_open(s_msc_driver->client_handle, dev_addr, &device) == ESP_OK) { + if ( usb_host_get_active_config_descriptor(device, &config_desc) == ESP_OK ) { + if ( find_msc_interface(config_desc, &dummy) ) { + is_msc_device = true; + } else { + ESP_LOGD(TAG, "Connected USB device is not MSC"); + } + } + usb_host_device_close(s_msc_driver->client_handle, device); + } + + return is_msc_device; +} + +static void event_handler_task(void *arg) +{ + while (1) { + usb_host_client_handle_events(s_msc_driver->client_handle, pdMS_TO_TICKS(50)); + + if (s_msc_driver->end_client_event_handling) { + break; + } + } + usb_host_client_unblock(s_msc_driver->client_handle); + ESP_ERROR_CHECK( usb_host_client_deregister(s_msc_driver->client_handle) ); + xSemaphoreGive(s_msc_driver->all_events_handled); + vTaskDelete(NULL); +} + +static msc_device_t *find_msc_device(usb_device_handle_t device_handle) +{ + msc_host_device_handle_t device; + + STAILQ_FOREACH(device, &devices_tailq, tailq_entry) { + if (device_handle == device->handle) { + return device; + } + } + + return NULL; +} + +static void client_event_cb(const usb_host_client_event_msg_t *event, void *arg) +{ + if (event->event == USB_HOST_CLIENT_EVENT_NEW_DEV) { + if (is_mass_storage_device(event->new_dev.address)) { + const msc_host_event_t msc_event = { + .event = MSC_DEVICE_CONNECTED, + .device.address = event->new_dev.address, + }; + s_msc_driver->user_cb(&msc_event, s_msc_driver->user_arg); + } + } else if (event->event == USB_HOST_CLIENT_EVENT_DEV_GONE) { + msc_device_t *msc_device = find_msc_device(event->dev_gone.dev_hdl); + if (msc_device) { + const msc_host_event_t msc_event = { + .event = MSC_DEVICE_DISCONNECTED, + .device.handle = msc_device, + }; + s_msc_driver->user_cb(&msc_event, s_msc_driver->user_arg); + } + } +} + +esp_err_t msc_host_install(const msc_host_driver_config_t *config) +{ + esp_err_t ret; + + MSC_RETURN_ON_INVALID_ARG(config); + MSC_RETURN_ON_INVALID_ARG(config->callback); + if ( config->create_backround_task ) { + MSC_RETURN_ON_FALSE(config->stack_size != 0, ESP_ERR_INVALID_ARG); + MSC_RETURN_ON_FALSE(config->task_priority != 0, ESP_ERR_INVALID_ARG); + } + MSC_RETURN_ON_FALSE(!s_msc_driver, ESP_ERR_INVALID_STATE); + + msc_driver_t *driver = calloc(1, sizeof(msc_driver_t)); + MSC_RETURN_ON_FALSE(driver, ESP_ERR_NO_MEM); + driver->user_cb = config->callback; + driver->user_arg = config->callback_arg; + + usb_host_client_config_t client_config = { + .async.client_event_callback = client_event_cb, + .async.callback_arg = NULL, + .max_num_event_msg = 10, + }; + + driver->end_client_event_handling = false; + driver->all_events_handled = xSemaphoreCreateBinary(); + MSC_GOTO_ON_FALSE(driver->all_events_handled, ESP_ERR_NO_MEM); + + MSC_GOTO_ON_ERROR( usb_host_client_register(&client_config, &driver->client_handle) ); + + MSC_ENTER_CRITICAL(); + MSC_GOTO_ON_FALSE_CRITICAL(!s_msc_driver, ESP_ERR_INVALID_STATE); + s_msc_driver = driver; + STAILQ_INIT(&devices_tailq); + MSC_EXIT_CRITICAL(); + + if (config->create_backround_task) { + BaseType_t task_created = xTaskCreatePinnedToCore( + event_handler_task, + "USB MSC", + config->stack_size, + NULL, + config->task_priority, + NULL, + config->core_id); + MSC_GOTO_ON_FALSE(task_created, ESP_ERR_NO_MEM); + } + + return ESP_OK; + +fail: + s_msc_driver = NULL; + usb_host_client_deregister(driver->client_handle); + if (driver->all_events_handled) { + vSemaphoreDelete(driver->all_events_handled); + } + free(driver); + return ret; +} + +esp_err_t msc_host_uninstall(void) +{ + // Make sure msc driver is installed, + // not being uninstalled from other task + // and no msc device is registered + MSC_ENTER_CRITICAL(); + MSC_RETURN_ON_FALSE_CRITICAL( s_msc_driver != NULL, ESP_ERR_INVALID_STATE ); + MSC_RETURN_ON_FALSE_CRITICAL( !s_msc_driver->end_client_event_handling, ESP_ERR_INVALID_STATE ); + MSC_RETURN_ON_FALSE_CRITICAL( STAILQ_EMPTY(&devices_tailq), ESP_ERR_INVALID_STATE ); + s_msc_driver->end_client_event_handling = true; + MSC_EXIT_CRITICAL(); + + xSemaphoreTake(s_msc_driver->all_events_handled, portMAX_DELAY); + vSemaphoreDelete(s_msc_driver->all_events_handled); + free(s_msc_driver); + s_msc_driver = NULL; + return ESP_OK; +} + +esp_err_t msc_host_install_device(uint8_t device_address, msc_host_device_handle_t *msc_device_handle) +{ + esp_err_t ret; + uint32_t block_size, block_count; + const usb_config_desc_t *config_desc; + msc_device_t *msc_device; + uint8_t lun; + size_t transfer_size = 512; // Normally the smallest block size + + MSC_GOTO_ON_FALSE( msc_device = calloc(1, sizeof(msc_device_t)), ESP_ERR_NO_MEM ); + + MSC_ENTER_CRITICAL(); + MSC_GOTO_ON_FALSE_CRITICAL( s_msc_driver, ESP_ERR_INVALID_STATE ); + MSC_GOTO_ON_FALSE_CRITICAL( s_msc_driver->client_handle, ESP_ERR_INVALID_STATE ); + STAILQ_INSERT_TAIL(&devices_tailq, msc_device, tailq_entry); + MSC_EXIT_CRITICAL(); + + MSC_GOTO_ON_FALSE( msc_device->transfer_done = xSemaphoreCreateBinary(), ESP_ERR_NO_MEM); + MSC_GOTO_ON_ERROR( usb_host_device_open(s_msc_driver->client_handle, device_address, &msc_device->handle) ); + MSC_GOTO_ON_ERROR( usb_host_get_active_config_descriptor(msc_device->handle, &config_desc) ); + MSC_GOTO_ON_ERROR( extract_config_from_descriptor(config_desc, &msc_device->config) ); + MSC_GOTO_ON_ERROR( usb_host_transfer_alloc(transfer_size, 0, &msc_device->xfer) ); + MSC_GOTO_ON_ERROR( usb_host_interface_claim( + s_msc_driver->client_handle, + msc_device->handle, + msc_device->config.iface_num, 0) ); + + MSC_GOTO_ON_ERROR( msc_get_max_lun(msc_device, &lun) ); + MSC_GOTO_ON_ERROR( scsi_cmd_inquiry(msc_device) ); + MSC_GOTO_ON_ERROR( msc_wait_for_ready_state(msc_device, WAIT_FOR_READY_TIMEOUT_MS) ); + MSC_GOTO_ON_ERROR( scsi_cmd_read_capacity(msc_device, &block_size, &block_count) ); + + // Configuration descriptor size of simple MSC device is 32 bytes. + if (config_desc->wTotalLength != 32) { + ESP_LOGE(TAG, "COMPOSITE DEVICES UNSUPPORTED"); + } + + msc_device->disk.block_size = block_size; + msc_device->disk.block_count = block_count; + + if (block_size > transfer_size) { + usb_transfer_t *larger_xfer; + MSC_GOTO_ON_ERROR( usb_host_transfer_alloc(block_size, 0, &larger_xfer) ); + usb_host_transfer_free(msc_device->xfer); + msc_device->xfer = larger_xfer; + } + + *msc_device_handle = msc_device; + + return ESP_OK; + +fail: + msc_deinit_device(msc_device, true); + return ret; +} + +esp_err_t msc_host_uninstall_device(msc_host_device_handle_t device) +{ + MSC_RETURN_ON_INVALID_ARG(device); + return msc_deinit_device((msc_device_t *)device, false); +} + + +esp_err_t msc_host_read_sector(msc_host_device_handle_t device, size_t sector, void *data, size_t size) +{ + MSC_RETURN_ON_INVALID_ARG(device); + msc_device_t *dev = (msc_device_t *)device; + + return scsi_cmd_read10(dev, data, sector, 1, dev->disk.block_size); +} + +esp_err_t msc_host_write_sector(msc_host_device_handle_t device, size_t sector, const void *data, size_t size) +{ + MSC_RETURN_ON_INVALID_ARG(device); + msc_device_t *dev = (msc_device_t *)device; + + return scsi_cmd_write10(dev, data, sector, 1, dev->disk.block_size); +} + +esp_err_t msc_host_handle_events(uint32_t timeout_ms) +{ + MSC_RETURN_ON_FALSE(s_msc_driver != NULL, ESP_ERR_INVALID_STATE); + + return usb_host_client_handle_events(s_msc_driver->client_handle, timeout_ms); +} + +static esp_err_t msc_read_string_desc(msc_device_t *dev, uint8_t index, wchar_t *str) +{ + if (index == 0) { + // String descriptor not available + str[0] = 0; + return ESP_OK; + } + + usb_transfer_t *xfer = dev->xfer; + USB_SETUP_PACKET_INIT_GET_STR_DESC((usb_setup_packet_t *)xfer->data_buffer, index, 0x409, 64); + MSC_RETURN_ON_ERROR( msc_control_transfer(dev, xfer, USB_SETUP_PACKET_SIZE + 64) ); + + usb_standard_desc_t *desc = (usb_standard_desc_t *)(xfer->data_buffer + USB_SETUP_PACKET_SIZE); + wchar_t *data = (wchar_t *)(xfer->data_buffer + USB_SETUP_PACKET_SIZE + 2); + size_t len = MIN((desc->bLength - USB_STANDARD_DESC_SIZE) / 2, MSC_STR_DESC_SIZE - 1); + + wcsncpy(str, data, len); + str[len] = 0; + + return ESP_OK; +} + +esp_err_t msc_host_get_device_info(msc_host_device_handle_t device, msc_host_device_info_t *info) +{ + MSC_RETURN_ON_INVALID_ARG(device); + MSC_RETURN_ON_INVALID_ARG(info); + + msc_device_t *dev = (msc_device_t *)device; + const usb_device_desc_t *desc; + + MSC_RETURN_ON_ERROR( usb_host_get_device_descriptor(dev->handle, &desc) ); + + info->idProduct = desc->idProduct; + info->idVendor = desc->idVendor; + info->sector_size = dev->disk.block_size; + info->sector_count = dev->disk.block_count; + + MSC_RETURN_ON_ERROR( msc_read_string_desc(dev, desc->iManufacturer, info->iManufacturer) ); + MSC_RETURN_ON_ERROR( msc_read_string_desc(dev, desc->iProduct, info->iProduct) ); + MSC_RETURN_ON_ERROR( msc_read_string_desc(dev, desc->iSerialNumber, info->iSerialNumber) ); + + return ESP_OK; +} + +esp_err_t msc_host_print_descriptors(msc_host_device_handle_t device) +{ + msc_device_t *dev = (msc_device_t *)device; + const usb_device_desc_t *device_desc; + const usb_config_desc_t *config_desc; + MSC_RETURN_ON_ERROR( usb_host_get_device_descriptor(dev->handle, &device_desc) ); + MSC_RETURN_ON_ERROR( usb_host_get_active_config_descriptor(dev->handle, &config_desc) ); + usb_print_device_descriptor(device_desc); + usb_print_config_descriptor(config_desc, NULL); + return ESP_OK; +} + +static void transfer_callback(usb_transfer_t *transfer) +{ + msc_device_t *device = (msc_device_t *)transfer->context; + + if (transfer->status != USB_TRANSFER_STATUS_COMPLETED) { + ESP_LOGE("Transfer failed", "Status %d", transfer->status); + } + + device->transfer_status = transfer->status; + xSemaphoreGive(device->transfer_done); +} + +static esp_err_t wait_for_transfer_done(usb_transfer_t *xfer) +{ + msc_device_t *device = (msc_device_t *)xfer->context; + BaseType_t received = xSemaphoreTake(device->transfer_done, pdMS_TO_TICKS(xfer->timeout_ms)); + + if (received != pdTRUE) { + usb_host_endpoint_halt(xfer->device_handle, xfer->bEndpointAddress); + usb_host_endpoint_flush(xfer->device_handle, xfer->bEndpointAddress); + xSemaphoreTake(device->transfer_done, portMAX_DELAY); + return ESP_ERR_TIMEOUT; + } + + return (device->transfer_status == USB_TRANSFER_STATUS_COMPLETED) ? ESP_OK : ESP_FAIL; +} + +static inline bool is_in_endpoint(uint8_t endpoint) +{ + return endpoint & USB_B_ENDPOINT_ADDRESS_EP_DIR_MASK ? true : false; +} + +esp_err_t msc_bulk_transfer(msc_device_t *device, uint8_t *data, size_t size, msc_endpoint_t ep) +{ + usb_transfer_t *xfer = device->xfer; + MSC_RETURN_ON_FALSE(size <= xfer->data_buffer_size, ESP_ERR_INVALID_SIZE); + uint8_t endpoint = (ep == MSC_EP_IN) ? device->config.bulk_in_ep : device->config.bulk_out_ep; + + if (is_in_endpoint(endpoint)) { + xfer->num_bytes = usb_round_up_to_mps(size, device->config.bulk_in_mps); + } else { + memcpy(xfer->data_buffer, data, size); + xfer->num_bytes = size; + } + + xfer->device_handle = device->handle; + xfer->bEndpointAddress = endpoint; + xfer->callback = transfer_callback; + xfer->timeout_ms = 1000; + xfer->context = device; + + MSC_RETURN_ON_ERROR( usb_host_transfer_submit(xfer) ); + MSC_RETURN_ON_ERROR( wait_for_transfer_done(xfer) ); + + if (is_in_endpoint(endpoint)) { + memcpy(data, xfer->data_buffer, size); + } + + return ESP_OK; +} + +esp_err_t msc_control_transfer(msc_device_t *device, usb_transfer_t *xfer, size_t len) +{ + xfer->device_handle = device->handle; + xfer->bEndpointAddress = 0; + xfer->callback = transfer_callback; + xfer->timeout_ms = 1000; + xfer->num_bytes = len; + xfer->context = device; + + MSC_RETURN_ON_ERROR( usb_host_transfer_submit_control(s_msc_driver->client_handle, xfer)); + return wait_for_transfer_done(xfer); +} diff --git a/usb/usb_host_msc/src/msc_host_vfs.c b/usb/usb_host_msc/src/msc_host_vfs.c new file mode 100644 index 0000000..50b058d --- /dev/null +++ b/usb/usb_host_msc/src/msc_host_vfs.c @@ -0,0 +1,124 @@ +/* + * SPDX-FileCopyrightText: 2015-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include "msc_common.h" +#include "msc_host_vfs.h" +#include "diskio_impl.h" +#include "ffconf.h" +#include "ff.h" + +#define DRIVE_STR_LEN 3 + +typedef struct msc_host_vfs { + char drive[DRIVE_STR_LEN]; + char *base_path; + uint8_t pdrv; +} msc_host_vfs_t; + +static const char *TAG = "MSC VFS"; + +static esp_err_t msc_format_storage(size_t block_size, size_t allocation_size, const char *drv) +{ + void *workbuf = NULL; + const size_t workbuf_size = 4096; + + MSC_RETURN_ON_FALSE( workbuf = ff_memalloc(workbuf_size), ESP_ERR_NO_MEM ); + + // Valid value of cluster size is between sector_size and 128 * sector_size. + size_t cluster_size = MIN(MAX(allocation_size, block_size), 128 * block_size); + const MKFS_PARM opt = {(BYTE)(FM_ANY | FM_SFD), 0, 0, 0, cluster_size}; + FRESULT err = f_mkfs(drv, &opt, workbuf, workbuf_size); + if (err) { + ESP_LOGE(TAG, "Formatting failed with error: %d", err); + free(workbuf); + return ESP_ERR_MSC_FORMAT_FAILED; + } + + free(workbuf); + return ESP_OK; +} + +static void dealloc_msc_vfs(msc_host_vfs_t *vfs) +{ + free(vfs->base_path); + free(vfs); +} + +esp_err_t msc_host_vfs_register(msc_host_device_handle_t device, + const char *base_path, + const esp_vfs_fat_mount_config_t *mount_config, + msc_host_vfs_handle_t *vfs_handle) +{ + MSC_RETURN_ON_INVALID_ARG(device); + MSC_RETURN_ON_INVALID_ARG(base_path); + MSC_RETURN_ON_INVALID_ARG(mount_config); + MSC_RETURN_ON_INVALID_ARG(vfs_handle); + + FATFS *fs = NULL; + BYTE pdrv; + bool diskio_registered = false; + esp_err_t ret = ESP_ERR_MSC_MOUNT_FAILED; + msc_device_t *dev = (msc_device_t *)device; + size_t block_size = dev->disk.block_size; + size_t alloc_size = mount_config->allocation_unit_size; + + msc_host_vfs_t *vfs = calloc(1, sizeof(msc_host_vfs_t)); + MSC_RETURN_ON_FALSE(vfs != NULL, ESP_ERR_NO_MEM); + + MSC_GOTO_ON_ERROR( ff_diskio_get_drive(&pdrv) ); + + ff_diskio_register_msc(pdrv, &dev->disk); + char drive[DRIVE_STR_LEN] = {(char)('0' + pdrv), ':', 0}; + diskio_registered = true; + + strncpy(vfs->drive, drive, DRIVE_STR_LEN); + MSC_GOTO_ON_FALSE( vfs->base_path = strdup(base_path), ESP_ERR_NO_MEM ); + vfs->pdrv = pdrv; + + MSC_GOTO_ON_ERROR( esp_vfs_fat_register(base_path, drive, mount_config->max_files, &fs) ); + + FRESULT fresult = f_mount(fs, drive, 1); + + if ( fresult != FR_OK) { + if (mount_config->format_if_mount_failed && + (fresult == FR_NO_FILESYSTEM || fresult == FR_INT_ERR)) { + MSC_GOTO_ON_ERROR( msc_format_storage(block_size, alloc_size, drive) ); + MSC_GOTO_ON_FALSE( f_mount(fs, drive, 0) == FR_OK, ESP_ERR_MSC_MOUNT_FAILED ); + } else { + goto fail; + } + } + + *vfs_handle = vfs; + return ESP_OK; + +fail: + if (diskio_registered) { + ff_diskio_unregister(pdrv); + } + esp_vfs_fat_unregister_path(base_path); + if (fs) { + f_mount(NULL, drive, 0); + } + dealloc_msc_vfs(vfs); + return ret; +} + +esp_err_t msc_host_vfs_unregister(msc_host_vfs_handle_t vfs_handle) +{ + MSC_RETURN_ON_INVALID_ARG(vfs_handle); + msc_host_vfs_t *vfs = (msc_host_vfs_t *)vfs_handle; + + f_mount(NULL, vfs->drive, 0); + ff_diskio_unregister(vfs->pdrv); + esp_vfs_fat_unregister_path(vfs->base_path); + dealloc_msc_vfs(vfs); + return ESP_OK; +} diff --git a/usb/usb_host_msc/src/msc_scsi_bot.c b/usb/usb_host_msc/src/msc_scsi_bot.c new file mode 100644 index 0000000..2e548f0 --- /dev/null +++ b/usb/usb_host_msc/src/msc_scsi_bot.c @@ -0,0 +1,434 @@ +/* + * SPDX-FileCopyrightText: 2015-2021 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include "esp_log.h" +#include +#include +#include +#include +#include "esp_check.h" +#include "esp_log.h" +#include "msc_common.h" +#include "msc_scsi_bot.h" + +#define TAG "USB_MSC_SCSI" + +/* --------------------------- SCSI Definitions ----------------------------- */ +#define CMD_SENSE_VALID_BIT (1 << 7) +#define SCSI_FLAG_DPO (1<<4) +#define SCSI_FLAG_FUA (1<<3) + +#define SCSI_CMD_FORMAT_UNIT 0x04 +#define SCSI_CMD_INQUIRY 0x12 +#define SCSI_CMD_MODE_SELECT 0x55 +#define SCSI_CMD_MODE_SENSE 0x5A +#define SCSI_CMD_PREVENT_ALLOW_MEDIUM_REMOVAL 0x1E +#define SCSI_CMD_READ10 0x28 +#define SCSI_CMD_READ12 0xA8 +#define SCSI_CMD_READ_CAPACITY 0x25 +#define SCSI_CMD_READ_FORMAT_CAPACITIES 0x23 +#define SCSI_CMD_REQUEST_SENSE 0x03 +#define SCSI_CMD_REZERO 0x01 +#define SCSI_CMD_SEEK10 0x2B +#define SCSI_CMD_SEND_DIAGNOSTIC 0x1D +#define SCSI_CMD_START_STOP Unit 0x1B +#define SCSI_CMD_TEST_UNIT_READY 0x00 +#define SCSI_CMD_VERIFY 0x2F +#define SCSI_CMD_WRITE10 0x2A +#define SCSI_CMD_WRITE12 0xAA +#define SCSI_CMD_WRITE_AND_VERIFY 0x2E + +#define IN_DIR CWB_FLAG_DIRECTION_IN +#define OUT_DIR 0 + +#define INQUIRY_VID_SIZE 8 +#define INQUIRY_PID_SIZE 16 +#define INQUIRY_REV_SIZE 4 + +#define CBW_CMD_SIZE(cmd) (sizeof(cmd) - sizeof(msc_cbw_t)) + +#define CBW_BASE_INIT(dir, cbw_len, data_len) \ + .base = { \ + .signature = 0x43425355, \ + .tag = ++cbw_tag, \ + .flags = dir, \ + .lun = 0, \ + .data_length = data_len, \ + .cbw_length = cbw_len, \ + } + +#define FEATURE_SELECTOR_ENDPOINT 0 +#define CSW_SIGNATURE 0x53425355 +#define CBW_SIZE 31 + +#define USB_MASS_REQ_INIT_RESET(ctrl_req_ptr, intf_num) ({ \ + (ctrl_req_ptr)->bmRequestType = USB_BM_REQUEST_TYPE_DIR_OUT | \ + USB_BM_REQUEST_TYPE_TYPE_CLASS | \ + USB_BM_REQUEST_TYPE_RECIP_INTERFACE; \ + (ctrl_req_ptr)->bRequest = 0xFF; \ + (ctrl_req_ptr)->wValue = 0; \ + (ctrl_req_ptr)->wIndex = (intf_num); \ + (ctrl_req_ptr)->wLength = 0; \ +}) + +#define USB_MASS_REQ_INIT_GET_MAX_LUN(ctrl_req_ptr, intf_num) ({ \ + (ctrl_req_ptr)->bmRequestType = USB_BM_REQUEST_TYPE_DIR_IN | \ + USB_BM_REQUEST_TYPE_TYPE_CLASS | \ + USB_BM_REQUEST_TYPE_RECIP_INTERFACE; \ + (ctrl_req_ptr)->bRequest = 0xFE; \ + (ctrl_req_ptr)->wValue = 0; \ + (ctrl_req_ptr)->wIndex = (intf_num); \ + (ctrl_req_ptr)->wLength = 1; \ +}) + +#define USB_SETUP_PACKET_INIT_CLEAR_FEATURE_EP(ctrl_req_ptr, ep_num) ({ \ + (ctrl_req_ptr)->bmRequestType = USB_BM_REQUEST_TYPE_DIR_OUT | \ + USB_BM_REQUEST_TYPE_TYPE_STANDARD | \ + USB_BM_REQUEST_TYPE_RECIP_ENDPOINT; \ + (ctrl_req_ptr)->bRequest = USB_B_REQUEST_CLEAR_FEATURE; \ + (ctrl_req_ptr)->wValue = FEATURE_SELECTOR_ENDPOINT; \ + (ctrl_req_ptr)->wIndex = (ep_num); \ + (ctrl_req_ptr)->wLength = 0; \ +}) + +#define CWB_FLAG_DIRECTION_IN (1<<7) // device -> host + +/** + * @brief Command Block Wrapper structure + */ +typedef struct __attribute__((packed)) +{ + uint32_t signature; + uint32_t tag; + uint32_t data_length; + uint8_t flags; + uint8_t lun; + uint8_t cbw_length; +} msc_cbw_t; + +/** + * @brief Command Status Wrapper structure + */ +typedef struct __attribute__((packed)) +{ + uint32_t signature; + uint32_t tag; + uint32_t dataResidue; + uint8_t status; +} msc_csw_t; + +typedef struct __attribute__((packed)) +{ + msc_cbw_t base; + uint8_t opcode; + uint8_t flags; + uint32_t address; + uint8_t reserved1; + uint16_t length; + uint8_t reserved2[3]; +} cbw_read10_t; + +typedef struct __attribute__((packed)) +{ + msc_cbw_t base; + uint8_t opcode; + uint8_t flags; + uint32_t address; + uint8_t reserved1; + uint16_t length; + uint8_t reserved2[1]; +} cbw_write10_t; + +typedef struct __attribute__((packed)) +{ + msc_cbw_t base; + uint8_t opcode; + uint8_t flags; + uint32_t address; + uint8_t reserved[6]; +} cbw_read_capacity_t; + +typedef struct __attribute__((packed)) +{ + uint32_t block_count; + uint32_t block_size; +} cbw_read_capacity_response_t; + +typedef struct __attribute__((packed)) +{ + msc_cbw_t base; + uint8_t opcode; + uint8_t flags; + uint8_t reserved[10]; +} cbw_unit_ready_t; + +typedef struct __attribute__((packed)) +{ + msc_cbw_t base; + uint8_t opcode; + uint8_t flags; + uint8_t reserved_0[2]; + uint8_t allocation_length; + uint8_t reserved_1[7]; +} cbw_sense_t; + +typedef struct __attribute__((packed)) +{ + uint8_t error_code; + uint8_t reserved_0; + uint8_t sense_key; + uint32_t info; + uint8_t sense_len; + uint32_t reserved_1; + uint8_t sense_code; + uint8_t sense_code_qualifier; + uint32_t reserved_2; +} cbw_sense_response_t; + +typedef struct __attribute__((packed)) +{ + msc_cbw_t base; + uint8_t opcode; + uint8_t flags; + uint8_t page_code; + uint8_t reserved_0; + uint8_t allocation_length; + uint8_t reserved_1[7]; +} cbw_inquiry_t; + +typedef struct __attribute__((packed)) +{ + msc_cbw_t base; + uint8_t opcode; + uint8_t flags; + uint8_t pc_page_code; + uint8_t reserved_1[4]; + uint16_t parameter_list_length; + uint8_t reserved_2[3]; +} mode_sense_t; + +typedef struct __attribute__((packed)) +{ + uint8_t data[8]; +} mode_sense_response_t; + +typedef struct __attribute__((packed)) +{ + msc_cbw_t base; + uint8_t opcode; + uint8_t flags; + uint8_t reserved_1[2]; + uint8_t prevent; + uint8_t reserved_2[7]; +} prevent_allow_medium_removal_t; + +typedef struct __attribute__((packed)) +{ + uint8_t data[36]; +} cbw_inquiry_response_t; + +// Unique number based on which MSC protocol pairs request and response +static uint32_t cbw_tag; + +static esp_err_t check_csw(msc_csw_t *csw, uint32_t tag) +{ + bool csw_ok = csw->signature == CSW_SIGNATURE && csw->tag == tag && + csw->dataResidue == 0 && csw->status == 0; + + if (!csw_ok) { + ESP_LOGD(TAG, "CSW failed: status %d", csw->status); + } + + return csw_ok ? ESP_OK : ESP_FAIL; +} + +static esp_err_t clear_feature(msc_device_t *device, uint8_t endpoint) +{ + usb_device_handle_t dev = device->handle; + usb_transfer_t *xfer = device->xfer; + + MSC_RETURN_ON_ERROR( usb_host_endpoint_clear(dev, endpoint) ); + USB_SETUP_PACKET_INIT_CLEAR_FEATURE_EP((usb_setup_packet_t *)xfer->data_buffer, endpoint); + MSC_RETURN_ON_ERROR( msc_control_transfer(device, xfer, USB_SETUP_PACKET_SIZE) ); + + return ESP_OK; +} + +esp_err_t msc_mass_reset(msc_device_t *device) +{ + usb_transfer_t *xfer = device->xfer; + + USB_MASS_REQ_INIT_RESET((usb_setup_packet_t *)xfer->data_buffer, 0); + MSC_RETURN_ON_ERROR( msc_control_transfer(device, xfer, USB_SETUP_PACKET_SIZE) ); + + return ESP_OK; +} + +esp_err_t msc_get_max_lun(msc_device_t *device, uint8_t *lun) +{ + usb_transfer_t *xfer = device->xfer; + + USB_MASS_REQ_INIT_GET_MAX_LUN((usb_setup_packet_t *)xfer->data_buffer, 0); + MSC_RETURN_ON_ERROR( msc_control_transfer(device, xfer, USB_SETUP_PACKET_SIZE + 1) ); + + *lun = xfer->data_buffer[USB_SETUP_PACKET_SIZE]; + + return ESP_OK; +} + +static esp_err_t bot_execute_command(msc_device_t *device, msc_cbw_t *cbw, void *data, size_t size) +{ + msc_csw_t csw; + msc_endpoint_t ep = (cbw->flags & CWB_FLAG_DIRECTION_IN) ? MSC_EP_IN : MSC_EP_OUT; + + MSC_RETURN_ON_ERROR( msc_bulk_transfer(device, (uint8_t *)cbw, CBW_SIZE, MSC_EP_OUT) ); + + if (data) { + MSC_RETURN_ON_ERROR( msc_bulk_transfer(device, (uint8_t *)data, size, ep) ); + } + + esp_err_t err = msc_bulk_transfer(device, (uint8_t *)&csw, sizeof(msc_csw_t), MSC_EP_IN); + + if (err == ESP_FAIL && device->transfer_status == USB_TRANSFER_STATUS_STALL) { + ESP_RETURN_ON_ERROR( clear_feature(device, MSC_EP_IN), TAG, "Clear feature failed" ); + // Try to read csw again after clearing feature + err = msc_bulk_transfer(device, (uint8_t *)&csw, sizeof(msc_csw_t), MSC_EP_IN); + if (err) { + ESP_RETURN_ON_ERROR( clear_feature(device, MSC_EP_IN), TAG, "Clear feature failed" ); + ESP_RETURN_ON_ERROR( msc_mass_reset(device), TAG, "Mass reset failed" ); + return ESP_FAIL; + } + } + + MSC_RETURN_ON_ERROR(err); + + return check_csw(&csw, cbw->tag); +} + + +esp_err_t scsi_cmd_read10(msc_device_t *device, + uint8_t *data, + uint32_t sector_address, + uint32_t num_sectors, + uint32_t sector_size) +{ + cbw_read10_t cbw = { + CBW_BASE_INIT(IN_DIR, CBW_CMD_SIZE(cbw_read10_t), num_sectors * sector_size), + .opcode = SCSI_CMD_READ10, + .flags = 0, // lun + .address = __builtin_bswap32(sector_address), + .length = __builtin_bswap16(num_sectors), + }; + + return bot_execute_command(device, &cbw.base, data, num_sectors * sector_size); +} + +esp_err_t scsi_cmd_write10(msc_device_t *device, + const uint8_t *data, + uint32_t sector_address, + uint32_t num_sectors, + uint32_t sector_size) +{ + cbw_write10_t cbw = { + CBW_BASE_INIT(OUT_DIR, CBW_CMD_SIZE(cbw_write10_t), num_sectors * sector_size), + .opcode = SCSI_CMD_WRITE10, + .address = __builtin_bswap32(sector_address), + .length = __builtin_bswap16(num_sectors), + }; + + return bot_execute_command(device, &cbw.base, (void *)data, num_sectors * sector_size); +} + +esp_err_t scsi_cmd_read_capacity(msc_device_t *device, uint32_t *block_size, uint32_t *block_count) +{ + cbw_read_capacity_response_t response; + + cbw_read_capacity_t cbw = { + CBW_BASE_INIT(IN_DIR, CBW_CMD_SIZE(cbw_read_capacity_t), sizeof(response)), + .opcode = SCSI_CMD_READ_CAPACITY, + }; + + MSC_RETURN_ON_ERROR( bot_execute_command(device, &cbw.base, &response, sizeof(response)) ); + + *block_count = __builtin_bswap32(response.block_count); + *block_size = __builtin_bswap32(response.block_size); + + return ESP_OK; +} + +esp_err_t scsi_cmd_unit_ready(msc_device_t *device) +{ + cbw_unit_ready_t cbw = { + CBW_BASE_INIT(IN_DIR, CBW_CMD_SIZE(cbw_unit_ready_t), 0), + .opcode = SCSI_CMD_TEST_UNIT_READY, + }; + + return bot_execute_command(device, &cbw.base, NULL, 0); +} + +esp_err_t scsi_cmd_sense(msc_device_t *device, scsi_sense_data_t *sense) +{ + cbw_sense_response_t response; + + cbw_sense_t cbw = { + CBW_BASE_INIT(IN_DIR, CBW_CMD_SIZE(cbw_sense_t), sizeof(response)), + .opcode = SCSI_CMD_REQUEST_SENSE, + .allocation_length = sizeof(response), + }; + + MSC_RETURN_ON_ERROR( bot_execute_command(device, &cbw.base, &response, sizeof(response)) ); + + if (sense->key) { + ESP_LOGD(TAG, "sense_key: 0x%02X, code: 0x%02X, qualifier: 0x%02X", + response.sense_key, response.sense_code, response.sense_code_qualifier); + } + + sense->key = response.sense_key; + sense->code = response.sense_code; + sense->code_q = response.sense_code_qualifier; + + return ESP_OK; +} + +esp_err_t scsi_cmd_inquiry(msc_device_t *device) +{ + cbw_inquiry_response_t response = { 0 }; + + cbw_inquiry_t cbw = { + CBW_BASE_INIT(IN_DIR, CBW_CMD_SIZE(cbw_inquiry_t), sizeof(response)), + .opcode = SCSI_CMD_INQUIRY, + .allocation_length = sizeof(response), + }; + + return bot_execute_command(device, &cbw.base, &response, sizeof(response) ); +} + +esp_err_t scsi_cmd_mode_sense(msc_device_t *device) +{ + mode_sense_response_t response = { 0 }; + + mode_sense_t cbw = { + CBW_BASE_INIT(IN_DIR, CBW_CMD_SIZE(mode_sense_t), sizeof(response)), + .opcode = SCSI_CMD_MODE_SENSE, + .pc_page_code = 0x3F, + .parameter_list_length = sizeof(response), + }; + + return bot_execute_command(device, &cbw.base, &response, sizeof(response) ); +} + +esp_err_t scsi_cmd_prevent_removal(msc_device_t *device, bool prevent) +{ + prevent_allow_medium_removal_t cbw = { + CBW_BASE_INIT(OUT_DIR, CBW_CMD_SIZE(prevent_allow_medium_removal_t), 0), + .opcode = SCSI_CMD_PREVENT_ALLOW_MEDIUM_REMOVAL, + .prevent = 1, + }; + + return bot_execute_command(device, &cbw.base, NULL, 0); +} diff --git a/usb/usb_host_msc/test/CMakeLists.txt b/usb/usb_host_msc/test/CMakeLists.txt new file mode 100644 index 0000000..1dd7e53 --- /dev/null +++ b/usb/usb_host_msc/test/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRC_DIRS . + INCLUDE_DIRS . + REQUIRES unity usb usb_host_msc tinyusb) diff --git a/usb/usb_host_msc/test/msc_device.c b/usb/usb_host_msc/test/msc_device.c new file mode 100644 index 0000000..03dd210 --- /dev/null +++ b/usb/usb_host_msc/test/msc_device.c @@ -0,0 +1,302 @@ +/* + * SPDX-FileCopyrightText: 2019 Ha Thach (tinyusb.org) + * + * SPDX-License-Identifier: MIT + * + * SPDX-FileContributor: 2019-2021 Espressif Systems (Shanghai) CO LTD + * + */ + +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "tinyusb.h" +#include "test_common.h" +#include "soc/soc_caps.h" + +#if SOC_USB_OTG_SUPPORTED + +#define MASS_STORAGE_CLASS 0x08 +#define SCSI_COMMAND_SET 0x06 +#define BULK_ONLY_TRANSFER 0x50 + +static const char *TAG = "msc_example"; + + +/**** Kconfig driven Descriptor ****/ +static const tusb_desc_device_t device_descriptor = { + .bLength = sizeof(device_descriptor), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0200, + .bDeviceClass = MASS_STORAGE_CLASS, + .bDeviceSubClass = SCSI_COMMAND_SET, + .bDeviceProtocol = BULK_ONLY_TRANSFER, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .idVendor = USB_ESPRESSIF_VID, + .idProduct = 0x1234, + .bcdDevice = 0x0100, + .iManufacturer = 0x01, + .iProduct = 0x02, + .iSerialNumber = 0x03, + .bNumConfigurations = 0x01 +}; + +const uint16_t msc_desc_config_len = TUD_CONFIG_DESC_LEN + CFG_TUD_MSC * TUD_MSC_DESC_LEN; +static const uint8_t msc_desc_configuration[] = { + TUD_CONFIG_DESCRIPTOR(1, 4, 0, msc_desc_config_len, TUSB_DESC_CONFIG_ATT_REMOTE_WAKEUP, 100), + TUD_MSC_DESCRIPTOR(0, 5, 1, 0x80 | 1, 64), +}; + + +void device_app(void) +{ + ESP_LOGI(TAG, "USB initialization"); + + tinyusb_config_t tusb_cfg = { + .device_descriptor = &device_descriptor, + .configuration_descriptor = msc_desc_configuration + }; + + ESP_ERROR_CHECK(tinyusb_driver_install(&tusb_cfg)); + ESP_LOGI(TAG, "USB initialization DONE"); + + while (1) { + vTaskDelay(100); + } +} + +// whether host does safe-eject +static bool ejected = false; + +// Some MCU doesn't have enough 8KB SRAM to store the whole disk +// We will use Flash as read-only disk with board that has +// CFG_EXAMPLE_MSC_READONLY defined + +uint8_t msc_disk[DISK_BLOCK_NUM][DISK_BLOCK_SIZE] = { + //------------- Block0: Boot Sector -------------// + // byte_per_sector = DISK_BLOCK_SIZE; fat12_sector_num_16 = DISK_BLOCK_NUM; + // sector_per_cluster = 1; reserved_sectors = 1; + // fat_num = 1; fat12_root_entry_num = 16; + // sector_per_fat = 1; sector_per_track = 1; head_num = 1; hidden_sectors = 0; + // drive_number = 0x80; media_type = 0xf8; extended_boot_signature = 0x29; + // filesystem_type = "FAT12 "; volume_serial_number = 0x1234; volume_label = "TinyUSB MSC"; + // FAT magic code at offset 510-511 + { + 0xEB, 0x3C, 0x90, 0x4D, 0x53, 0x44, 0x4F, 0x53, 0x35, 0x2E, 0x30, 0x00, 0x02, 0x01, 0x01, 0x00, + 0x01, 0x10, 0x00, 0x10, 0x00, 0xF8, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x29, 0x34, 0x12, 0x00, 0x00, 'T', 'i', 'n', 'y', 'U', + 'S', 'B', ' ', 'M', 'S', 'C', 0x46, 0x41, 0x54, 0x31, 0x32, 0x20, 0x20, 0x20, 0x00, 0x00, + + // Zero up to 2 last bytes of FAT magic code + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 'F', 'A', 'T', '3', '2', ' ', ' ', ' ', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55, 0xAA + }, + + //------------- Block1: FAT12 Table -------------// + { + 0xF8, 0xFF, 0xFF, 0xFF, 0x0F // // first 2 entries must be F8FF, third entry is cluster end of readme file + }, + + //------------- Block2: Root Directory -------------// + { + // first entry is volume label + 'T', 'i', 'n', 'y', 'U', 'S', 'B', ' ', 'M', 'S', 'C', 0x08, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0x6D, 0x65, 0x43, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + // second entry is readme file + 'R', 'E', 'A', 'D', 'M', 'E', ' ', ' ', 'T', 'X', 'T', 0x20, 0x00, 0xC6, 0x52, 0x6D, + 0x65, 0x43, 0x65, 0x43, 0x00, 0x00, 0x88, 0x6D, 0x65, 0x43, 0x02, 0x00, + sizeof(README_CONTENTS) - 1, 0x00, 0x00, 0x00 // readme's files size (4 Bytes) + }, + + //------------- Block3: Readme Content -------------// + README_CONTENTS +}; + +// 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; + + const char vid[] = "TinyUSB"; + const char pid[] = "Mass Storage"; + const char rev[] = "1.0"; + + 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; + + // RAM disk is ready until ejected + if (ejected) { + tud_msc_set_sense(lun, SCSI_SENSE_NOT_READY, 0x3a, 0x00); + return false; + } + + 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; + + *block_count = DISK_BLOCK_NUM; + *block_size = DISK_BLOCK_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; + + if ( load_eject ) { + if (start) { + // load disk storage + } else { + // unload disk storage + ejected = true; + } + } + + return true; +} + +// Callback invoked when received READ10 command. +// Copy disk's data to buffer (up to bufsize) and return number of copied bytes. +int32_t tud_msc_read10_cb(uint8_t lun, uint32_t lba, uint32_t offset, void *buffer, uint32_t bufsize) +{ + (void) lun; + + uint8_t const *addr = msc_disk[lba] + offset; + memcpy(buffer, addr, bufsize); + + return bufsize; +} + +// Callback invoked when received WRITE10 command. +// Process data in buffer to disk's storage and return number of written bytes +int32_t tud_msc_write10_cb(uint8_t lun, uint32_t lba, uint32_t offset, uint8_t *buffer, uint32_t bufsize) +{ + (void) lun; + +#ifndef CFG_EXAMPLE_MSC_READONLY + uint8_t *addr = msc_disk[lba] + offset; + memcpy(addr, buffer, bufsize); +#else + (void) lba; (void) offset; (void) buffer; +#endif + + return bufsize; +} + +// Callback invoked when received an SCSI command not in built-in list below +// - READ_CAPACITY10, READ_FORMAT_CAPACITY, INQUIRY, MODE_SENSE6, REQUEST_SENSE +// - READ10 and WRITE10 has their own callbacks +int32_t tud_msc_scsi_cb (uint8_t lun, uint8_t const scsi_cmd[16], void *buffer, uint16_t bufsize) +{ + // read10 & write10 has their own callback and MUST not be handled here + + void const *response = NULL; + uint16_t resplen = 0; + + // most scsi handled is input + bool in_xfer = true; + + switch (scsi_cmd[0]) { + case SCSI_CMD_PREVENT_ALLOW_MEDIUM_REMOVAL: + // Host is about to read/write etc ... better not to disconnect disk + resplen = 0; + break; + + default: + // Set Sense = Invalid Command Operation + tud_msc_set_sense(lun, SCSI_SENSE_ILLEGAL_REQUEST, 0x20, 0x00); + + // negative means error -> tinyusb could stall and/or response with failed status + resplen = -1; + break; + } + + // return resplen must not larger than bufsize + if ( resplen > bufsize ) { + resplen = bufsize; + } + + if ( response && (resplen > 0) ) { + if (in_xfer) { + memcpy(buffer, response, resplen); + } else { + // SCSI output + } + } + + return resplen; +} + +#endif /* SOC_USB_OTG_SUPPORTED */ diff --git a/usb/usb_host_msc/test/test_common.h b/usb/usb_host_msc/test/test_common.h new file mode 100644 index 0000000..63eb63d --- /dev/null +++ b/usb/usb_host_msc/test/test_common.h @@ -0,0 +1,19 @@ +/* + * SPDX-FileCopyrightText: 2021-2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ +#pragma once + +enum { + // FatFS only allows to format disks with number of blocks greater than 128 + DISK_BLOCK_NUM = 128 + 1, + DISK_BLOCK_SIZE = 512 +}; + +#define README_CONTENTS \ +"This is tinyusb's MassStorage Class demo.\r\n\r\n\ +If you find any bugs or get any questions, feel free to file an\r\n\ +issue at github.com/hathach/tinyusb" + +void device_app(void); diff --git a/usb/usb_host_msc/test/test_msc.c b/usb/usb_host_msc/test/test_msc.c new file mode 100644 index 0000000..5fb6cba --- /dev/null +++ b/usb/usb_host_msc/test/test_msc.c @@ -0,0 +1,324 @@ + +/* + * SPDX-FileCopyrightText: 2015-2022 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: CC0-1.0 + */ + +#include "unity.h" +#include +#include +#include +#include +#include +#include +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include "freertos/queue.h" +#include "freertos/semphr.h" +#include "esp_err.h" +#include "esp_log.h" +#include "esp_private/usb_phy.h" +#include "usb/usb_host.h" +#include "msc_host.h" +#include "msc_host_vfs.h" +#include "ffconf.h" +#include "ff.h" +#include "test_common.h" +#include "soc/usb_wrap_struct.h" +#include "soc/soc_caps.h" + +#if SOC_USB_OTG_SUPPORTED + +static const char *TAG = "APP"; + +#define ESP_OK_ASSERT(exp) TEST_ASSERT_EQUAL(ESP_OK, exp) + +static esp_vfs_fat_mount_config_t mount_config = { + .format_if_mount_failed = false, + .max_files = 3, + .allocation_unit_size = 1024, +}; + +static QueueHandle_t app_queue; +static SemaphoreHandle_t ready_to_deinit_usb; +static msc_host_device_handle_t device; +static msc_host_vfs_handle_t vfs_handle; +static volatile bool waiting_for_sudden_disconnect; +static usb_phy_handle_t phy_hdl = NULL; + +static void force_conn_state(bool connected, TickType_t delay_ticks) +{ + TEST_ASSERT(phy_hdl); + if (delay_ticks > 0) { + //Delay of 0 ticks causes a yield. So skip if delay_ticks is 0. + vTaskDelay(delay_ticks); + } + ESP_ERROR_CHECK(usb_phy_action(phy_hdl, (connected) ? USB_PHY_ACTION_HOST_ALLOW_CONN : USB_PHY_ACTION_HOST_FORCE_DISCONN)); +} + +static void msc_event_cb(const msc_host_event_t *event, void *arg) +{ + if (waiting_for_sudden_disconnect) { + waiting_for_sudden_disconnect = false; + TEST_ASSERT_EQUAL(MSC_DEVICE_DISCONNECTED, event->event); + } + + if (event->event == MSC_DEVICE_CONNECTED) { + printf("MSC_DEVICE_CONNECTED\n"); + } else { + printf("MSC_DEVICE_DISCONNECTED\n"); + } + + xQueueSend(app_queue, event, 10); +} + +static const char *TEST_STRING = "Hello World!"; +static const char *FILE_NAME = "/usb/ESP32.txt"; + +static void write_read_file(const char *file_path) +{ + char line[64]; + + ESP_LOGI(TAG, "Writing file"); + FILE *f = fopen(file_path, "w"); + TEST_ASSERT(f); + fprintf(f, TEST_STRING); + fclose(f); + + ESP_LOGI(TAG, "Reading file"); + TEST_ASSERT(fopen(file_path, "r")); + fgets(line, sizeof(line), f); + fclose(f); + // strip newline + char *pos = strchr(line, '\n'); + if (pos) { + *pos = '\0'; + } + TEST_ASSERT_EQUAL_STRING(line, TEST_STRING); + ESP_LOGI(TAG, "Done"); +} + +static bool file_exists(const char *file_path) +{ + return ( access(file_path, F_OK) == 0 ); +} + +// Handles common USB host library events +static void handle_usb_events(void *args) +{ + uint32_t end_flags = 0; + + while (1) { + uint32_t event_flags; + usb_host_lib_handle_events(portMAX_DELAY, &event_flags); + // Release devices once all clients has deregistered + if (event_flags & USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS) { + printf("USB_HOST_LIB_EVENT_FLAGS_NO_CLIENTS\n"); + usb_host_device_free_all(); + end_flags |= 1; + } + // Give ready_to_deinit_usb semaphore to indicate that USB Host library + // can be deinitialized, and terminate this task. + if (event_flags & USB_HOST_LIB_EVENT_FLAGS_ALL_FREE) { + printf("USB_HOST_LIB_EVENT_FLAGS_ALL_FREE\n"); + end_flags |= 2; + } + + if (end_flags == 3) { + xSemaphoreGive(ready_to_deinit_usb); + break; + } + } + vTaskDelete(NULL); +} + +static void check_file_content(const char *file_path, const char *expected) +{ + ESP_LOGI(TAG, "Reading %s:", file_path); + FILE *file = fopen(file_path, "r"); + TEST_ASSERT(file) + + char content[200]; + fread(content, 1, sizeof(content), file); + TEST_ASSERT_EQUAL_STRING(content, expected); + fclose(file); +} + +static void check_sudden_disconnect(void) +{ + uint8_t data[512]; + const size_t DATA_SIZE = sizeof(data); + + ESP_LOGI(TAG, "Creating test.tx"); + FILE *file = fopen("/usb/test.txt", "w"); + TEST_ASSERT(file); + + ESP_LOGI(TAG, "Write data"); + TEST_ASSERT_EQUAL(DATA_SIZE, fwrite(data, 1, DATA_SIZE, file)); + TEST_ASSERT_EQUAL(DATA_SIZE, fwrite(data, 1, DATA_SIZE, file)); + TEST_ASSERT_EQUAL(0, fflush(file)); + + ESP_LOGI(TAG, "Trigger a disconnect"); + //Trigger a disconnect + waiting_for_sudden_disconnect = true; + force_conn_state(false, 0); + + // Make sure flag was leared in callback + vTaskDelay( pdMS_TO_TICKS(100) ); + TEST_ASSERT_FALSE(waiting_for_sudden_disconnect); + + ESP_LOGI(TAG, "Write data after disconnect"); + TEST_ASSERT_NOT_EQUAL( DATA_SIZE, fwrite(data, 1, DATA_SIZE, file)); + + fclose(file); +} + +static void msc_setup(void) +{ + BaseType_t task_created; + + ready_to_deinit_usb = xSemaphoreCreateBinary(); + + TEST_ASSERT( app_queue = xQueueCreate(5, sizeof(msc_host_event_t)) ); + + //Initialize the internal USB PHY to connect to the USB OTG peripheral. We manually install the USB PHY for testing + usb_phy_config_t phy_config = { + .controller = USB_PHY_CTRL_OTG, + .target = USB_PHY_TARGET_INT, + .otg_mode = USB_OTG_MODE_HOST, + .otg_speed = USB_PHY_SPEED_UNDEFINED, //In Host mode, the speed is determined by the connected device + .gpio_conf = NULL, + }; + ESP_OK_ASSERT(usb_new_phy(&phy_config, &phy_hdl)); + const usb_host_config_t host_config = { + .skip_phy_setup = true, + .intr_flags = ESP_INTR_FLAG_LEVEL1, + }; + ESP_OK_ASSERT( usb_host_install(&host_config) ); + + task_created = xTaskCreatePinnedToCore(handle_usb_events, "usb_events", 2048, NULL, 2, NULL, 0); + TEST_ASSERT(task_created); + + const msc_host_driver_config_t msc_config = { + .create_backround_task = true, + .callback = msc_event_cb, + .stack_size = 4096, + .task_priority = 5, + }; + ESP_OK_ASSERT( msc_host_install(&msc_config) ); + + ESP_LOGI(TAG, "Waiting for USB stick to be connected"); + msc_host_event_t app_event; + xQueueReceive(app_queue, &app_event, portMAX_DELAY); + TEST_ASSERT_EQUAL(MSC_DEVICE_CONNECTED, app_event.event); + uint8_t device_addr = app_event.device.address; + + ESP_OK_ASSERT( msc_host_install_device(device_addr, &device) ); + ESP_OK_ASSERT( msc_host_vfs_register(device, "/usb", &mount_config, &vfs_handle) ); +} + +static void msc_teardown(void) +{ + // Wait to finish any ongoing USB operations + vTaskDelay(100); + + ESP_OK_ASSERT( msc_host_vfs_unregister(vfs_handle) ); + ESP_OK_ASSERT( msc_host_uninstall_device(device) ); + ESP_OK_ASSERT( msc_host_uninstall() ); + + xSemaphoreTake(ready_to_deinit_usb, portMAX_DELAY); + vSemaphoreDelete(ready_to_deinit_usb); + ESP_OK_ASSERT( usb_host_uninstall() ); + //Tear down USB PHY + ESP_OK_ASSERT(usb_del_phy(phy_hdl)); + phy_hdl = NULL; + + vQueueDelete(app_queue); +} + +static void write_read_sectors(void) +{ + uint8_t write_data[DISK_BLOCK_SIZE]; + uint8_t read_data[DISK_BLOCK_SIZE]; + + memset(write_data, 0x55, DISK_BLOCK_SIZE); + memset(read_data, 0, DISK_BLOCK_SIZE); + + msc_host_write_sector(device, 10, write_data, DISK_BLOCK_SIZE); + msc_host_read_sector(device, 10, read_data, DISK_BLOCK_SIZE); + + TEST_ASSERT_EQUAL_MEMORY(write_data, read_data, DISK_BLOCK_SIZE); +} + +static void erase_storage(void) +{ + uint8_t data[DISK_BLOCK_SIZE]; + memset(data, 0xFF, DISK_BLOCK_SIZE); + + for (int block = 0; block < DISK_BLOCK_NUM; block++) { + msc_host_write_sector(device, block, data, DISK_BLOCK_SIZE); + } +} + +TEST_CASE("write_and_read_file", "[usb_msc]") +{ + msc_setup(); + write_read_file(FILE_NAME); + msc_teardown(); +} + +TEST_CASE("sudden_disconnect", "[usb_msc]") +{ + msc_setup(); + check_sudden_disconnect(); + msc_teardown(); +} + +TEST_CASE("sectors_can_be_written_and_read", "[usb_msc]") +{ + msc_setup(); + write_read_sectors(); + msc_teardown(); +} + +TEST_CASE("check_README_content", "[usb_msc]") +{ + msc_setup(); + check_file_content("/usb/README.TXT", README_CONTENTS); + msc_teardown(); +} + +/** + * @brief USB MSC format testcase + * @attention This testcase deletes all content on the USB MSC device. + * The device must be reset in order to contain the FILE_NAME again. + */ +TEST_CASE("can_be_formated", "[usb_msc]") +{ + printf("Create file\n"); + msc_setup(); + write_read_file(FILE_NAME); + msc_teardown(); + + printf("File exists after mounting again\n"); + msc_setup(); + TEST_ASSERT(file_exists(FILE_NAME)); + printf("Erase storage device\n"); + erase_storage(); + msc_teardown(); + + printf("Check file does not exist after formatting\n"); + mount_config.format_if_mount_failed = true; + msc_setup(); + TEST_ASSERT_FALSE(file_exists(FILE_NAME)); + msc_teardown(); + mount_config.format_if_mount_failed = false; +} + +TEST_CASE("mock_device_app", "[usb_msc_device][ignore]") +{ + device_app(); +} + +#endif