diff options
| author | Zixun LI <[email protected]> | 2026-05-11 16:02:50 +0200 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-05-11 16:02:50 +0200 |
| commit | 4e64f3e91cefab0993a837ac03476173eeeda854 (patch) | |
| tree | 0843841795f9260bea7da7564b61037810e4476a | |
| parent | dcd2a4bd0f005835d3edc193309ef3484cd9c8b3 (diff) | |
| parent | 3188ed4fd6ccab395b6618d913ebdb44d94c3dcc (diff) | |
Merge pull request #3632 from hathach/msc_os
hcd/dwc2: fix txfifo full check
| -rw-r--r-- | .github/workflows/build.yml | 7 | ||||
| -rw-r--r-- | examples/host/CMakeLists.txt | 1 | ||||
| -rw-r--r-- | examples/host/msc_file_explorer_freertos/CMakeLists.txt | 42 | ||||
| -rw-r--r-- | examples/host/msc_file_explorer_freertos/CMakePresets.json | 6 | ||||
| -rw-r--r-- | examples/host/msc_file_explorer_freertos/Makefile | 27 | ||||
| -rw-r--r-- | examples/host/msc_file_explorer_freertos/README.md | 105 | ||||
| -rw-r--r-- | examples/host/msc_file_explorer_freertos/only.txt | 28 | ||||
| -rw-r--r-- | examples/host/msc_file_explorer_freertos/skip.txt | 3 | ||||
| -rw-r--r-- | examples/host/msc_file_explorer_freertos/src/CMakeLists.txt | 13 | ||||
| -rw-r--r-- | examples/host/msc_file_explorer_freertos/src/ffconf.h | 313 | ||||
| -rw-r--r-- | examples/host/msc_file_explorer_freertos/src/main.c | 158 | ||||
| -rw-r--r-- | examples/host/msc_file_explorer_freertos/src/msc_app.c | 709 | ||||
| -rw-r--r-- | examples/host/msc_file_explorer_freertos/src/msc_app.h | 34 | ||||
| -rw-r--r-- | examples/host/msc_file_explorer_freertos/src/tusb_config.h | 126 | ||||
| -rw-r--r-- | src/portable/synopsys/dwc2/hcd_dwc2.c | 5 | ||||
| -rwxr-xr-x | test/hil/hil_test.py | 233 |
16 files changed, 1739 insertions, 71 deletions
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a88b8ffba..a83a997c2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -326,13 +326,15 @@ jobs: github.repository_owner == 'hathach' && !(github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) runs-on: [ self-hosted, Linux, X64, hifiphile ] + timeout-minutes: 30 env: IAR_LMS_BEARER_TOKEN: ${{ secrets.IAR_LMS_BEARER_TOKEN }} + PYTHONUNBUFFERED: '1' steps: - name: Clean workspace run: | echo "Cleaning up previous run" - rm -rf "${{ github.workspace }}"3 + rm -rf "${{ github.workspace }}" mkdir -p "${{ github.workspace }}" - name: Toolchain version @@ -356,4 +358,5 @@ jobs: run: python3 tools/build.py --toolchain iar $BUILD_ARGS - name: Test on actual hardware (hardware in the loop) - run: python3 test/hil/hil_test.py hfp.json + run: | + python3 test/hil/hil_test.py hfp.json diff --git a/examples/host/CMakeLists.txt b/examples/host/CMakeLists.txt index f8e0ce692..70e0427ab 100644 --- a/examples/host/CMakeLists.txt +++ b/examples/host/CMakeLists.txt @@ -14,6 +14,7 @@ set(EXAMPLE_LIST hid_controller midi_rx msc_file_explorer + msc_file_explorer_freertos ) foreach (example ${EXAMPLE_LIST}) diff --git a/examples/host/msc_file_explorer_freertos/CMakeLists.txt b/examples/host/msc_file_explorer_freertos/CMakeLists.txt new file mode 100644 index 000000000..4893dd1fb --- /dev/null +++ b/examples/host/msc_file_explorer_freertos/CMakeLists.txt @@ -0,0 +1,42 @@ +cmake_minimum_required(VERSION 3.20) + +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake) + +project(msc_file_explorer_freertos C CXX ASM) + +# Checks this example is valid for the family and initializes the project +family_initialize_project(${PROJECT_NAME} ${CMAKE_CURRENT_LIST_DIR}) + +# Espressif has its own cmake build system +if(FAMILY STREQUAL "espressif") + return() +endif() + +add_executable(${PROJECT_NAME}) + +# Example source +target_sources(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src/main.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/msc_app.c + ${TOP}/lib/fatfs/source/ff.c + ${TOP}/lib/fatfs/source/ffsystem.c + ${TOP}/lib/fatfs/source/ffunicode.c + ) + +# Example include +target_include_directories(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src + ${TOP}/lib/fatfs/source + ${TOP}/lib/embedded-cli + ) + +# Configure compilation flags and libraries for the example with FreeRTOS. +# See the corresponding function in hw/bsp/FAMILY/family.cmake for details. +family_configure_host_example(${PROJECT_NAME} freertos) + +# Suppress warnings on fatfs +if (CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID STREQUAL "Clang") + set_source_files_properties(${TOP}/lib/fatfs/source/ff.c PROPERTIES + COMPILE_OPTIONS "-Wno-conversion;-Wno-cast-qual" + ) +endif () diff --git a/examples/host/msc_file_explorer_freertos/CMakePresets.json b/examples/host/msc_file_explorer_freertos/CMakePresets.json new file mode 100644 index 000000000..5cd8971e9 --- /dev/null +++ b/examples/host/msc_file_explorer_freertos/CMakePresets.json @@ -0,0 +1,6 @@ +{ + "version": 6, + "include": [ + "../../../hw/bsp/BoardPresets.json" + ] +} diff --git a/examples/host/msc_file_explorer_freertos/Makefile b/examples/host/msc_file_explorer_freertos/Makefile new file mode 100644 index 000000000..15c7420d4 --- /dev/null +++ b/examples/host/msc_file_explorer_freertos/Makefile @@ -0,0 +1,27 @@ +RTOS = freertos +include ../../../hw/bsp/family_support.mk + +FATFS_PATH = lib/fatfs/source + +INC += \ + src \ + $(TOP)/$(FATFS_PATH) \ + $(TOP)/lib/embedded-cli \ + +# Example source +EXAMPLE_SOURCE = \ + src/main.c \ + src/msc_app.c \ + +SRC_C += $(addprefix $(EXAMPLE_PATH)/, $(EXAMPLE_SOURCE)) + +# FatFS source +SRC_C += \ + $(FATFS_PATH)/ff.c \ + $(FATFS_PATH)/ffsystem.c \ + $(FATFS_PATH)/ffunicode.c \ + +# suppress warning caused by fatfs +CFLAGS += -Wno-error=cast-qual + +include ../../../hw/bsp/family_rules.mk diff --git a/examples/host/msc_file_explorer_freertos/README.md b/examples/host/msc_file_explorer_freertos/README.md new file mode 100644 index 000000000..4ee6b96fa --- /dev/null +++ b/examples/host/msc_file_explorer_freertos/README.md @@ -0,0 +1,105 @@ +# MSC File Explorer (FreeRTOS) + +This host example implements an interactive command-line file browser for USB Mass Storage devices. +When a USB flash drive is connected, the device is automatically mounted using FatFS and a shell-like +CLI is presented over the board's serial console. + +## Features + +- Automatic mount/unmount of USB storage devices +- FAT12/16/32 filesystem support via FatFS +- Interactive CLI with command history +- Read speed benchmarking with `dd` +- Support for up to 4 simultaneous USB storage devices (via hub) + +## Supported Commands + +| Command | Usage | Description | +|---------|--------------------|------------------------------------------------------| +| help | `help` | Print list of available commands | +| cat | `cat <file>` | Print file contents to the console | +| cd | `cd <dir>` | Change current working directory | +| cp | `cp <src> <dest>` | Copy a file | +| dd | `dd [count]` | Read sectors and report speed (default 1024 sectors) | +| ls | `ls [dir]` | List directory contents | +| pwd | `pwd` | Print current working directory | +| mkdir | `mkdir <dir>` | Create a directory | +| mv | `mv <src> <dest>` | Rename/move a file or directory | +| rm | `rm <file>` | Remove a file | + +## Build + +Build for a specific board using CMake (see [Getting Started](https://docs.tinyusb.org/en/latest/getting_started.html)): + +```bash +# Example: build for STM32F407 Discovery board +cmake -B build -DBOARD=stm32f407disco -GNinja examples/host/msc_file_explorer_freertos +cmake --build build +``` + +## Usage + +1. Flash the firmware to your board. +2. Open a serial terminal (e.g. `minicom`, `screen`, `PuTTY`) at 115200 baud. +3. Plug a USB flash drive into the board's USB host port. +4. The device is auto-mounted and the prompt appears: + +``` +TinyUSB MSC File Explorer Example + +Device connected + Vendor : Kingston + Product : DataTraveler 2.0 + Rev : 1.0 + Capacity: 1.9 GB + +0:/> _ +``` + +### Browsing Files + +``` +0:/> ls +----a 1234 readme.txt +d---- 0 photos +d---- 0 docs + +0:/> cd photos +0:/photos> ls +----a 520432 vacation.jpg +----a 312088 family.png + +0:/> cat readme.txt +Hello from USB drive! +``` + +### Copying and Moving Files + +``` +0:/> cp readme.txt backup.txt +0:/> mv backup.txt docs/backup.txt +``` + +### Measuring Read Speed + +``` +0:/> dd +Reading 1024 sectors... + Data speed: 823 KB/s +``` + +### Multiple Devices + +When using a USB hub, multiple drives are mounted as `0:`, `1:`, etc. Use the drive prefix to +navigate between them: + +``` +0:/> cd 1: +1:/> ls +``` + +## Testing + +Build-time validation follows the standard TinyUSB host example flow. Runtime behavior should be +verified on hardware by attaching an MSC device and exercising CLI commands such as `ls`, `pwd`, +and `dd`. diff --git a/examples/host/msc_file_explorer_freertos/only.txt b/examples/host/msc_file_explorer_freertos/only.txt new file mode 100644 index 000000000..519ac2ebd --- /dev/null +++ b/examples/host/msc_file_explorer_freertos/only.txt @@ -0,0 +1,28 @@ +family:espressif +family:samd21 +family:samd5x_e5x +mcu:LPC175X_6X +mcu:LPC177X_8X +mcu:LPC18XX +mcu:LPC40XX +mcu:LPC43XX +mcu:LPC54 +mcu:LPC55 +mcu:MAX3421 +mcu:MIMXRT10XX +mcu:MIMXRT11XX +mcu:MIMXRT1XXX +mcu:MSP432E4 +mcu:RP2040 +mcu:RW61X +mcu:RX65X +mcu:STM32C0 +mcu:STM32F4 +mcu:STM32F7 +mcu:STM32G0 +mcu:STM32H5 +mcu:STM32H7 +mcu:STM32H7RS +mcu:STM32N6 +mcu:STM32U3 +mcu:STM32U5 diff --git a/examples/host/msc_file_explorer_freertos/skip.txt b/examples/host/msc_file_explorer_freertos/skip.txt new file mode 100644 index 000000000..f0be07d25 --- /dev/null +++ b/examples/host/msc_file_explorer_freertos/skip.txt @@ -0,0 +1,3 @@ +mcu:CH32F20X +board:lpcxpresso54114 +mcu:FT90X diff --git a/examples/host/msc_file_explorer_freertos/src/CMakeLists.txt b/examples/host/msc_file_explorer_freertos/src/CMakeLists.txt new file mode 100644 index 000000000..c3fb35607 --- /dev/null +++ b/examples/host/msc_file_explorer_freertos/src/CMakeLists.txt @@ -0,0 +1,13 @@ +# This file is for ESP-IDF only +set(FATFS_DIR ${CMAKE_CURRENT_LIST_DIR}/../../../../lib/fatfs/source) +set(EMBEDDED_CLI_DIR ${CMAKE_CURRENT_LIST_DIR}/../../../../lib/embedded-cli) + +idf_component_register( + SRCS "main.c" "msc_app.c" + ${FATFS_DIR}/ff.c + ${FATFS_DIR}/ffsystem.c + ${FATFS_DIR}/ffunicode.c + INCLUDE_DIRS "." ${FATFS_DIR} ${EMBEDDED_CLI_DIR} + REQUIRES boards tinyusb_src) + +target_compile_options(${COMPONENT_LIB} PRIVATE -Wno-error=format) diff --git a/examples/host/msc_file_explorer_freertos/src/ffconf.h b/examples/host/msc_file_explorer_freertos/src/ffconf.h new file mode 100644 index 000000000..5c89136fe --- /dev/null +++ b/examples/host/msc_file_explorer_freertos/src/ffconf.h @@ -0,0 +1,313 @@ +/*---------------------------------------------------------------------------/ +/ Configurations of FatFs Module +/---------------------------------------------------------------------------*/ + +#define FFCONF_DEF 80386 /* Revision ID */ + +/*---------------------------------------------------------------------------/ +/ Function Configurations +/---------------------------------------------------------------------------*/ + +#define FF_FS_READONLY 0 +/* This option switches read-only configuration. (0:Read/Write or 1:Read-only) +/ Read-only configuration removes writing API functions, f_write(), f_sync(), +/ f_unlink(), f_mkdir(), f_chmod(), f_rename(), f_truncate(), f_getfree() +/ and optional writing functions as well. */ + + +#define FF_FS_MINIMIZE 0 +/* This option defines minimization level to remove some basic API functions. +/ +/ 0: Basic functions are fully enabled. +/ 1: f_stat(), f_getfree(), f_unlink(), f_mkdir(), f_truncate() and f_rename() +/ are removed. +/ 2: f_opendir(), f_readdir() and f_closedir() are removed in addition to 1. +/ 3: f_lseek() function is removed in addition to 2. */ + + +#define FF_USE_FIND 0 +/* This option switches filtered directory read functions, f_findfirst() and +/ f_findnext(). (0:Disable, 1:Enable 2:Enable with matching altname[] too) */ + + +#define FF_USE_MKFS 0 +/* This option switches f_mkfs(). (0:Disable or 1:Enable) */ + + +#define FF_USE_FASTSEEK 0 +/* This option switches fast seek feature. (0:Disable or 1:Enable) */ + + +#define FF_USE_EXPAND 0 +/* This option switches f_expand(). (0:Disable or 1:Enable) */ + + +#define FF_USE_CHMOD 0 +/* This option switches attribute control API functions, f_chmod() and f_utime(). +/ (0:Disable or 1:Enable) Also FF_FS_READONLY needs to be 0 to enable this option. */ + + +#define FF_USE_LABEL 0 +/* This option switches volume label API functions, f_getlabel() and f_setlabel(). +/ (0:Disable or 1:Enable) */ + + +#define FF_USE_FORWARD 0 +/* This option switches f_forward(). (0:Disable or 1:Enable) */ + + +#define FF_USE_STRFUNC 0 +#define FF_PRINT_LLI 0 +#define FF_PRINT_FLOAT 0 +#define FF_STRF_ENCODE 0 +/* FF_USE_STRFUNC switches string API functions, f_gets(), f_putc(), f_puts() and +/ f_printf(). +/ +/ 0: Disable. FF_PRINT_LLI, FF_PRINT_FLOAT and FF_STRF_ENCODE have no effect. +/ 1: Enable without LF-CRLF conversion. +/ 2: Enable with LF-CRLF conversion. +/ +/ FF_PRINT_LLI = 1 makes f_printf() support long long argument and FF_PRINT_FLOAT = 1/2 +/ makes f_printf() support floating point argument. These features want C99 or later. +/ When FF_LFN_UNICODE >= 1 with LFN enabled, string API functions convert the character +/ encoding in it. FF_STRF_ENCODE selects assumption of character encoding ON THE FILE +/ to be read/written via those functions. +/ +/ 0: ANSI/OEM in current CP +/ 1: Unicode in UTF-16LE +/ 2: Unicode in UTF-16BE +/ 3: Unicode in UTF-8 +*/ + + +/*---------------------------------------------------------------------------/ +/ Locale and Namespace Configurations +/---------------------------------------------------------------------------*/ + +#define FF_CODE_PAGE 437 +/* This option specifies the OEM code page to be used on the target system. +/ Incorrect code page setting can cause a file open failure. +/ +/ 437 - U.S. +/ 720 - Arabic +/ 737 - Greek +/ 771 - KBL +/ 775 - Baltic +/ 850 - Latin 1 +/ 852 - Latin 2 +/ 855 - Cyrillic +/ 857 - Turkish +/ 860 - Portuguese +/ 861 - Icelandic +/ 862 - Hebrew +/ 863 - Canadian French +/ 864 - Arabic +/ 865 - Nordic +/ 866 - Russian +/ 869 - Greek 2 +/ 932 - Japanese (DBCS) +/ 936 - Simplified Chinese (DBCS) +/ 949 - Korean (DBCS) +/ 950 - Traditional Chinese (DBCS) +/ 0 - Include all code pages above and configured by f_setcp() +*/ + + +#define FF_USE_LFN 1 +#define FF_MAX_LFN 255 +/* The FF_USE_LFN switches the support for LFN (long file name). +/ +/ 0: Disable LFN. FF_MAX_LFN has no effect. +/ 1: Enable LFN with static working buffer on the BSS. Always NOT thread-safe. +/ 2: Enable LFN with dynamic working buffer on the STACK. +/ 3: Enable LFN with dynamic working buffer on the HEAP. +/ +/ To enable the LFN, ffunicode.c needs to be added to the project. The LFN feature +/ requiers certain internal working buffer occupies (FF_MAX_LFN + 1) * 2 bytes and +/ additional (FF_MAX_LFN + 44) / 15 * 32 bytes when exFAT is enabled. +/ The FF_MAX_LFN defines size of the working buffer in UTF-16 code unit and it can +/ be in range of 12 to 255. It is recommended to be set 255 to fully support the LFN +/ specification. +/ When use stack for the working buffer, take care on stack overflow. When use heap +/ memory for the working buffer, memory management functions, ff_memalloc() and +/ ff_memfree() exemplified in ffsystem.c, need to be added to the project. */ + + +#define FF_LFN_UNICODE 0 +/* This option switches the character encoding on the API when LFN is enabled. +/ +/ 0: ANSI/OEM in current CP (TCHAR = char) +/ 1: Unicode in UTF-16 (TCHAR = WCHAR) +/ 2: Unicode in UTF-8 (TCHAR = char) +/ 3: Unicode in UTF-32 (TCHAR = DWORD) +/ +/ Also behavior of string I/O functions will be affected by this option. +/ When LFN is not enabled, this option has no effect. */ + + +#define FF_LFN_BUF 255 +#define FF_SFN_BUF 12 +/* This set of options defines size of file name members in the FILINFO structure +/ which is used to read out directory items. These values should be sufficient for +/ the file names to read. The maximum possible length of the read file name depends +/ on character encoding. When LFN is not enabled, these options have no effect. */ + + +#define FF_FS_RPATH 2 +/* This option configures support for relative path feature. +/ +/ 0: Disable relative path and remove related API functions. +/ 1: Enable relative path and dot names. f_chdir() and f_chdrive() are available. +/ 2: f_getcwd() is available in addition to 1. +*/ + + +#define FF_PATH_DEPTH 10 +/* This option defines maximum depth of directory in the exFAT volume. It is NOT +/ relevant to FAT/FAT32 volume. +/ For example, FF_PATH_DEPTH = 3 will able to follow a path "/dir1/dir2/dir3/file" +/ but a sub-directory in the dir3 will not able to be followed and set current +/ directory. +/ The size of filesystem object (FATFS) increases FF_PATH_DEPTH * 24 bytes. +/ When FF_FS_EXFAT == 0 or FF_FS_RPATH == 0, this option has no effect. +*/ + + + +/*---------------------------------------------------------------------------/ +/ Drive/Volume Configurations +/---------------------------------------------------------------------------*/ + +#define FF_VOLUMES 4 +/* Number of volumes (logical drives) to be used. (1-10) */ + + +#define FF_STR_VOLUME_ID 0 +#define FF_VOLUME_STRS "RAM","NAND","CF","SD","SD2","USB","USB2","USB3" +/* FF_STR_VOLUME_ID switches support for volume ID in arbitrary strings. +/ When FF_STR_VOLUME_ID is set to 1 or 2, arbitrary strings can be used as drive +/ number in the path name. FF_VOLUME_STRS defines the volume ID strings for each +/ logical drive. Number of items must not be less than FF_VOLUMES. Valid +/ characters for the volume ID strings are A-Z, a-z and 0-9, however, they are +/ compared in case-insensitive. If FF_STR_VOLUME_ID >= 1 and FF_VOLUME_STRS is +/ not defined, a user defined volume string table is needed as: +/ +/ const char* VolumeStr[FF_VOLUMES] = {"ram","flash","sd","usb",... +*/ + + +#define FF_MULTI_PARTITION 0 +/* This option switches support for multiple volumes on the physical drive. +/ By default (0), each logical drive number is bound to the same physical drive +/ number and only an FAT volume found on the physical drive will be mounted. +/ When this feature is enabled (1), each logical drive number can be bound to +/ arbitrary physical drive and partition listed in the VolToPart[]. Also f_fdisk() +/ will be available. */ + + +#define FF_MIN_SS 512 +#define FF_MAX_SS 512 +/* This set of options configures the range of sector size to be supported. (512, +/ 1024, 2048 or 4096) Always set both 512 for most systems, generic memory card and +/ harddisk, but a larger value may be required for on-board flash memory and some +/ type of optical media. When FF_MAX_SS is larger than FF_MIN_SS, FatFs is +/ configured for variable sector size mode and disk_ioctl() needs to implement +/ GET_SECTOR_SIZE command. */ + + +#define FF_LBA64 0 +/* This option switches support for 64-bit LBA. (0:Disable or 1:Enable) +/ To enable the 64-bit LBA, also exFAT needs to be enabled. (FF_FS_EXFAT == 1) */ + + +#define FF_MIN_GPT 0x10000000 +/* Minimum number of sectors to switch GPT as partitioning format in f_mkfs() and +/ f_fdisk(). 2^32 sectors maximum. This option has no effect when FF_LBA64 == 0. */ + + +#define FF_USE_TRIM 0 +/* This option switches support for ATA-TRIM. (0:Disable or 1:Enable) +/ To enable this feature, also CTRL_TRIM command should be implemented to +/ the disk_ioctl(). */ + + + +/*---------------------------------------------------------------------------/ +/ System Configurations +/---------------------------------------------------------------------------*/ + +#define FF_FS_TINY 0 +/* This option switches tiny buffer configuration. (0:Normal or 1:Tiny) +/ At the tiny configuration, size of file object (FIL) is reduced FF_MAX_SS bytes. +/ Instead of private sector buffer eliminated from the file object, common sector +/ buffer in the filesystem object (FATFS) is used for the file data transfer. */ + + +#define FF_FS_EXFAT 0 +/* This option switches support for exFAT filesystem. (0:Disable or 1:Enable) +/ To enable exFAT, also LFN needs to be enabled. (FF_USE_LFN >= 1) +/ Note that enabling exFAT discards ANSI C (C89) compatibility. */ + + +#define FF_FS_NORTC 1 +#define FF_NORTC_MON 1 +#define FF_NORTC_MDAY 1 +#define FF_NORTC_YEAR 2025 +/* The option FF_FS_NORTC switches timestamp feature. If the system does not have +/ an RTC or valid timestamp is not needed, set FF_FS_NORTC = 1 to disable the +/ timestamp feature. Every object modified by FatFs will have a fixed timestamp +/ defined by FF_NORTC_MON, FF_NORTC_MDAY and FF_NORTC_YEAR in local time. +/ To enable timestamp function (FF_FS_NORTC = 0), get_fattime() need to be added +/ to the project to read current time form real-time clock. FF_NORTC_MON, +/ FF_NORTC_MDAY and FF_NORTC_YEAR have no effect. +/ These options have no effect in read-only configuration (FF_FS_READONLY = 1). */ + + +#define FF_FS_CRTIME 0 +/* This option enables(1)/disables(0) the timestamp of the file created. When +/ set 1, the file created time is available in FILINFO structure. */ + + +#define FF_FS_NOFSINFO 0 +/* If you need to know the correct free space on the FAT32 volume, set bit 0 of +/ this option, and f_getfree() on the first time after volume mount will force +/ a full FAT scan. Bit 1 controls the use of last allocated cluster number. +/ +/ bit0=0: Use free cluster count in the FSINFO if available. +/ bit0=1: Do not trust free cluster count in the FSINFO. +/ bit1=0: Use last allocated cluster number in the FSINFO if available. +/ bit1=1: Do not trust last allocated cluster number in the FSINFO. +*/ + + +#define FF_FS_LOCK 0 +/* The option FF_FS_LOCK switches file lock function to control duplicated file open +/ and illegal operation to open objects. This option must be 0 when FF_FS_READONLY +/ is 1. +/ +/ 0: Disable file lock function. To avoid volume corruption, application program +/ should avoid illegal open, remove and rename to the open objects. +/ >0: Enable file lock function. The value defines how many files/sub-directories +/ can be opened simultaneously under file lock control. Note that the file +/ lock control is independent of re-entrancy. */ + + +#define FF_FS_REENTRANT 0 +#define FF_FS_TIMEOUT 1000 +/* The option FF_FS_REENTRANT switches the re-entrancy (thread safe) of the FatFs +/ module itself. Note that regardless of this option, file access to different +/ volume is always re-entrant and volume control functions, f_mount(), f_mkfs() +/ and f_fdisk(), are always not re-entrant. Only file/directory access to +/ the same volume is under control of this featuer. +/ +/ 0: Disable re-entrancy. FF_FS_TIMEOUT have no effect. +/ 1: Enable re-entrancy. Also user provided synchronization handlers, +/ ff_mutex_create(), ff_mutex_delete(), ff_mutex_take() and ff_mutex_give(), +/ must be added to the project. Samples are available in ffsystem.c. +/ +/ The FF_FS_TIMEOUT defines timeout period in unit of O/S time tick. +*/ + + + +/*--- End of configuration options ---*/ diff --git a/examples/host/msc_file_explorer_freertos/src/main.c b/examples/host/msc_file_explorer_freertos/src/main.c new file mode 100644 index 000000000..d1e627f4f --- /dev/null +++ b/examples/host/msc_file_explorer_freertos/src/main.c @@ -0,0 +1,158 @@ +/* + * 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 <string.h> + +#include "bsp/board_api.h" +#include "tusb.h" +#ifdef ESP_PLATFORM + // ESP-IDF need "freertos/" prefix in include path. + // CFG_TUSB_OS_INC_PATH should be defined accordingly. + #include "freertos/FreeRTOS.h" + #include "freertos/task.h" + #include "freertos/timers.h" +#else + #include "FreeRTOS.h" + #include "task.h" + #include "timers.h" +#endif + +#include "msc_app.h" + +//--------------------------------------------------------------------+ +// MACRO CONSTANT TYPEDEF PROTYPES +//--------------------------------------------------------------------+ +#ifdef ESP_PLATFORM + #define USBH_STACK_SIZE 4096 +#else + // Increase stack size when debug log is enabled. + #define USBH_STACK_SIZE (configMINIMAL_STACK_SIZE * (CFG_TUSB_DEBUG ? 4 : 3)) +#endif + +enum { + BLINK_MOUNTED = 1000, +}; + +#if configSUPPORT_STATIC_ALLOCATION +StaticTimer_t blinky_tmdef; + +StackType_t usb_host_stack[USBH_STACK_SIZE]; +StaticTask_t usb_host_taskdef; +#endif + +TimerHandle_t blinky_tm; + +static void led_blinky_cb(TimerHandle_t xTimer); +static void usb_host_task(void* param); + +/*------------- MAIN -------------*/ +int main(void) { + board_init(); + + printf("TinyUSB Host MassStorage Explorer FreeRTOS Example\r\n"); + + // Create soft timer for blinky and task for TinyUSB host stack. +#if configSUPPORT_STATIC_ALLOCATION + blinky_tm = xTimerCreateStatic(NULL, pdMS_TO_TICKS(BLINK_MOUNTED), true, NULL, led_blinky_cb, &blinky_tmdef); + xTaskCreateStatic(usb_host_task, "usbh", USBH_STACK_SIZE, NULL, configMAX_PRIORITIES - 1, usb_host_stack, + &usb_host_taskdef); +#else + blinky_tm = xTimerCreate(NULL, pdMS_TO_TICKS(BLINK_MOUNTED), true, NULL, led_blinky_cb); + xTaskCreate(usb_host_task, "usbh", USBH_STACK_SIZE, NULL, configMAX_PRIORITIES - 1, NULL); +#endif + + xTimerStart(blinky_tm, 0); + + // only start scheduler for non-espressif mcu +#ifndef ESP_PLATFORM + vTaskStartScheduler(); +#endif + + return 0; +} + +#ifdef ESP_PLATFORM +void app_main(void) { + main(); +} +#endif + +// USB Host task +// This top-level thread processes all USB events and invokes callbacks. +static void usb_host_task(void* param) { + (void) param; + + // init host stack on configured roothub port + tusb_rhport_init_t host_init = { + .role = TUSB_ROLE_HOST, + .speed = TUSB_SPEED_AUTO + }; + + if (!tusb_init(BOARD_TUH_RHPORT, &host_init)) { + printf("Failed to init USB Host Stack\r\n"); + vTaskSuspend(NULL); + } + + board_init_after_tusb(); + +#if CFG_TUH_ENABLED && CFG_TUH_MAX3421 + // FeatherWing MAX3421E uses MAX3421E GPIO0 for VBUS enable. + enum { IOPINS1_ADDR = 20u << 3 }; + tuh_max3421_reg_write(BOARD_TUH_RHPORT, IOPINS1_ADDR, 0x01, false); +#endif + + if (!msc_app_init()) { + printf("Failed to init MSC app\r\n"); + vTaskSuspend(NULL); + } + + while (1) { + // TinyUSB host task. + tuh_task(); + } +} + +//--------------------------------------------------------------------+ +// TinyUSB Callbacks +//--------------------------------------------------------------------+ + +void tuh_mount_cb(uint8_t dev_addr) { + (void) dev_addr; +} + +void tuh_umount_cb(uint8_t dev_addr) { + (void) dev_addr; +} + +//--------------------------------------------------------------------+ +// Blinking Task +//--------------------------------------------------------------------+ +static void led_blinky_cb(TimerHandle_t xTimer) { + (void) xTimer; + static bool led_state = false; + + board_led_write(led_state); + led_state = 1 - led_state; // toggle +} diff --git a/examples/host/msc_file_explorer_freertos/src/msc_app.c b/examples/host/msc_file_explorer_freertos/src/msc_app.c new file mode 100644 index 000000000..c7e00e52a --- /dev/null +++ b/examples/host/msc_file_explorer_freertos/src/msc_app.c @@ -0,0 +1,709 @@ +/* + * 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 <ctype.h> +#include "tusb.h" +#include "bsp/board_api.h" +#ifdef ESP_PLATFORM + // ESP-IDF need "freertos/" prefix in include path. + // CFG_TUSB_OS_INC_PATH should be defined accordingly. + #include "freertos/FreeRTOS.h" + #include "freertos/task.h" + #include "freertos/timers.h" +#else + #include "FreeRTOS.h" + #include "task.h" + #include "timers.h" +#endif + +#include "ff.h" +#include "diskio.h" + +// lib/embedded-cli +#define EMBEDDED_CLI_IMPL +#include "embedded_cli.h" + +#include "msc_app.h" + + +//--------------------------------------------------------------------+ +// MACRO TYPEDEF CONSTANT ENUM DECLARATION +//--------------------------------------------------------------------+ + +//------------- embedded-cli -------------// +#define CLI_BUFFER_SIZE 512 +#define CLI_RX_BUFFER_SIZE 16 +#define CLI_CMD_BUFFER_SIZE 64 +#define CLI_HISTORY_SIZE 32 +#define CLI_BINDING_COUNT 9 + +#ifdef ESP_PLATFORM + #define MSC_APP_STACK_SIZE 4096 +#else + #define MSC_APP_STACK_SIZE (configMINIMAL_STACK_SIZE * (CFG_TUSB_DEBUG ? 3 : 2)) +#endif + +static EmbeddedCli *_cli; +static CLI_UINT cli_buffer[BYTES_TO_CLI_UINTS(CLI_BUFFER_SIZE)]; + +#if configSUPPORT_STATIC_ALLOCATION +StackType_t msc_app_stack[MSC_APP_STACK_SIZE]; +StaticTask_t msc_app_taskdef; +#endif + +//------------- Elm Chan FatFS -------------// +static CFG_TUH_MEM_SECTION FATFS fatfs[CFG_TUH_DEVICE_MAX]; // for simplicity only support 1 LUN per device +static volatile bool _disk_busy[CFG_TUH_DEVICE_MAX]; +static volatile bool _mount_pending[CFG_TUH_DEVICE_MAX]; + +static CFG_TUH_MEM_SECTION FIL file1, file2; + +#ifndef CFG_EXAMPLE_MSC_FILE_EXPLORER_RW_BUFSIZE +#define CFG_EXAMPLE_MSC_FILE_EXPLORER_RW_BUFSIZE 4096 +#endif +static CFG_TUH_MEM_SECTION uint8_t rw_buf[CFG_EXAMPLE_MSC_FILE_EXPLORER_RW_BUFSIZE]; + +// define the buffer to be place in USB/DMA memory with correct alignment/cache line size +CFG_TUH_MEM_SECTION static struct { + TUH_EPBUF_TYPE_DEF(scsi_inquiry_resp_t, inquiry); +} scsi_resp; + + +//--------------------------------------------------------------------+ +// +//--------------------------------------------------------------------+ + +static bool cli_init(void); +static void msc_app_task(void* param); +static void process_pending_mount(void); + +bool msc_app_init(void) { + for (size_t i = 0; i < CFG_TUH_DEVICE_MAX; i++) { + _disk_busy[i] = false; + _mount_pending[i] = false; + } + +// disable stdout buffered for echoing typing command +#ifndef __ICCARM__ // TODO IAR doesn't support stream control ? + setbuf(stdout, NULL); +#endif + + cli_init(); + +#if configSUPPORT_STATIC_ALLOCATION + TaskHandle_t task_hdl = xTaskCreateStatic(msc_app_task, "msc", MSC_APP_STACK_SIZE, NULL, + configMAX_PRIORITIES - 2, msc_app_stack, &msc_app_taskdef); + TU_ASSERT(task_hdl != NULL); +#else + TU_ASSERT(xTaskCreate(msc_app_task, "msc", MSC_APP_STACK_SIZE, NULL, configMAX_PRIORITIES - 2, NULL) == pdPASS); +#endif + + return true; +} + +static void msc_app_task(void* param) { + (void) param; + + while (1) { + process_pending_mount(); + + if (!_cli) { + vTaskDelay(1); + continue; + } + + int ch = board_getchar(); + if (ch > 0) { + while (ch > 0) { + embeddedCliReceiveChar(_cli, (char) ch); + ch = board_getchar(); + } + embeddedCliProcess(_cli); + } + + vTaskDelay(1); + } +} + +static void process_pending_mount(void) { + for (uint8_t drive_num = 0; drive_num < CFG_TUH_DEVICE_MAX; drive_num++) { + if (!_mount_pending[drive_num]) { + continue; + } + + _mount_pending[drive_num] = false; + + const uint8_t dev_addr = drive_num + 1; + if (!tuh_msc_mounted(dev_addr)) { + continue; + } + + char drive_path[3] = "0:"; + drive_path[0] += drive_num; + + if (f_mount(&fatfs[drive_num], drive_path, 1) != FR_OK) { + printf("mount failed\r\n"); + continue; + } + + f_chdrive(drive_path); + FRESULT rc = f_chdir("/"); + if (rc != FR_OK) { + printf("chdir failed: %d\r\n", rc); + } + } +} + +//--------------------------------------------------------------------+ +// +//--------------------------------------------------------------------+ + +static bool inquiry_complete_cb(uint8_t dev_addr, const tuh_msc_complete_data_t *cb_data) { + const msc_cbw_t *cbw = cb_data->cbw; + const msc_csw_t *csw = cb_data->csw; + + if (csw->status != 0) { + printf("Inquiry failed\r\n"); + return false; + } + + // Print out Vendor ID, Product ID and Rev + printf("%.8s %.16s %.4s\r\n", scsi_resp.inquiry.vendor_id, scsi_resp.inquiry.product_id, + scsi_resp.inquiry.product_rev); + + // Get capacity of device + const uint32_t block_count = tuh_msc_get_block_count(dev_addr, cbw->lun); + const uint32_t block_size = tuh_msc_get_block_size(dev_addr, cbw->lun); + + printf("Disk Size: %" PRIu32 " %" PRIu32 "-byte blocks: %" PRIu32 " MB\r\n", + block_count, block_size, block_count / ((1024 * 1024) / block_size)); + + // For simplicity: we only mount 1 LUN per device + const uint8_t drive_num = dev_addr - 1; + _mount_pending[drive_num] = true; + + // print the drive label + // char label[34]; + // if ( FR_OK == f_getlabel(drive_path, label, NULL) ) + // { + // puts(label); + // } + + return true; +} + +//------------- IMPLEMENTATION -------------// +void tuh_msc_mount_cb(uint8_t dev_addr) { + printf("A MassStorage device (addr = %u) is mounted\r\n", dev_addr); + + const uint8_t lun = 0; + tuh_msc_inquiry(dev_addr, lun, &scsi_resp.inquiry, inquiry_complete_cb, 0); +} + +void tuh_msc_umount_cb(uint8_t dev_addr) { + printf("A MassStorage device is unmounted\r\n"); + + const uint8_t drive_num = dev_addr - 1; + char drive_path[3] = "0:"; + drive_path[0] += drive_num; + + _mount_pending[drive_num] = false; + + f_unmount(drive_path); + + // if ( phy_disk == f_get_current_drive() ) + // { // active drive is unplugged --> change to other drive + // for(uint8_t i=0; i<CFG_TUH_DEVICE_MAX; i++) + // { + // if ( disk_is_ready(i) ) + // { + // f_chdrive(i); + // cli_init(); // refractor, rename + // } + // } + // } +} + +//--------------------------------------------------------------------+ +// DiskIO +//--------------------------------------------------------------------+ + +static void wait_for_disk_io(BYTE pdrv) { + while (_disk_busy[pdrv]) { + vTaskDelay(1); + } +} + +static bool disk_io_complete(uint8_t dev_addr, const tuh_msc_complete_data_t *cb_data) { + (void)dev_addr; + (void)cb_data; + _disk_busy[dev_addr - 1] = false; + return true; +} + +DSTATUS disk_status(BYTE pdrv /* Physical drive nmuber to identify the drive */ +) { + uint8_t dev_addr = pdrv + 1; + return tuh_msc_mounted(dev_addr) ? 0 : STA_NODISK; +} + +DSTATUS disk_initialize(BYTE pdrv /* Physical drive nmuber to identify the drive */ +) { + (void)pdrv; + return 0; // nothing to do +} + +DRESULT disk_read(BYTE pdrv, /* Physical drive nmuber to identify the drive */ + BYTE *buff, /* Data buffer to store read data */ + LBA_t sector, /* Start sector in LBA */ + UINT count /* Number of sectors to read */ +) { + const uint8_t dev_addr = pdrv + 1; + const uint8_t lun = 0; + + _disk_busy[pdrv] = true; + tuh_msc_read10(dev_addr, lun, buff, sector, (uint16_t)count, disk_io_complete, 0); + wait_for_disk_io(pdrv); + + return RES_OK; +} + +#if FF_FS_READONLY == 0 + +DRESULT disk_write(BYTE pdrv, /* Physical drive nmuber to identify the drive */ + const BYTE *buff, /* Data to be written */ + LBA_t sector, /* Start sector in LBA */ + UINT count /* Number of sectors to write */ +) { + const uint8_t dev_addr = pdrv + 1; + const uint8_t lun = 0; + + _disk_busy[pdrv] = true; + tuh_msc_write10(dev_addr, lun, buff, sector, (uint16_t)count, disk_io_complete, 0); + wait_for_disk_io(pdrv); + + return RES_OK; +} + +#endif + +DRESULT disk_ioctl(BYTE pdrv, /* Physical drive nmuber (0..) */ + BYTE cmd, /* Control code */ + void *buff /* Buffer to send/receive control data */ +) { + const uint8_t dev_addr = pdrv + 1; + const uint8_t lun = 0; + switch (cmd) { + case CTRL_SYNC: + // nothing to do since we do blocking + return RES_OK; + + case GET_SECTOR_COUNT: + *((DWORD *)buff) = (DWORD)tuh_msc_get_block_count(dev_addr, lun); + return RES_OK; + + case GET_SECTOR_SIZE: + *((WORD *)buff) = (WORD)tuh_msc_get_block_size(dev_addr, lun); + return RES_OK; + + case GET_BLOCK_SIZE: + *((DWORD *)buff) = 1; // erase block size in units of sector size + return RES_OK; + + default: + return RES_PARERR; + } +} + +//--------------------------------------------------------------------+ +// CLI Commands +//--------------------------------------------------------------------+ + +void cli_cmd_cat(EmbeddedCli *cli, char *args, void *context); +void cli_cmd_cd(EmbeddedCli *cli, char *args, void *context); +void cli_cmd_cp(EmbeddedCli *cli, char *args, void *context); +void cli_cmd_dd(EmbeddedCli *cli, char *args, void *context); +void cli_cmd_ls(EmbeddedCli *cli, char *args, void *context); +void cli_cmd_pwd(EmbeddedCli *cli, char *args, void *context); +void cli_cmd_mkdir(EmbeddedCli *cli, char *args, void *context); +void cli_cmd_mv(EmbeddedCli *cli, char *args, void *context); +void cli_cmd_rm(EmbeddedCli *cli, char *args, void *context); + +static void cli_write_char(EmbeddedCli *cli, char c) { + (void)cli; + putchar((int)c); +} + +bool cli_init(void) { + EmbeddedCliConfig *config = embeddedCliDefaultConfig(); + config->cliBuffer = cli_buffer; + config->cliBufferSize = CLI_BUFFER_SIZE; + config->rxBufferSize = CLI_RX_BUFFER_SIZE; + config->cmdBufferSize = CLI_CMD_BUFFER_SIZE; + config->historyBufferSize = CLI_HISTORY_SIZE; + config->maxBindingCount = CLI_BINDING_COUNT; + + TU_ASSERT(embeddedCliRequiredSize(config) <= CLI_BUFFER_SIZE); + + _cli = embeddedCliNew(config); + TU_ASSERT(_cli != NULL); + + _cli->writeChar = cli_write_char; + + embeddedCliAddBinding(_cli, + (CliCommandBinding){"cat", "Usage: cat [FILE]...\r\n\tConcatenate FILE(s) to standard output..", + true, NULL, cli_cmd_cat}); + + embeddedCliAddBinding(_cli, (CliCommandBinding){"cd", "Usage: cd [DIR]...\r\n\tChange the current directory to DIR.", + true, NULL, cli_cmd_cd}); + + embeddedCliAddBinding(_cli, (CliCommandBinding){"cp", "Usage: cp SOURCE DEST\r\n\tCopy SOURCE to DEST.", true, NULL, + cli_cmd_cp}); + + embeddedCliAddBinding(_cli, (CliCommandBinding){"dd", "Usage: dd [COUNT]\r\n\t" "Read COUNT sectors (default 1024) and report speed.", true, NULL, + cli_cmd_dd}); + + embeddedCliAddBinding(_cli, (CliCommandBinding){"ls", + "Usage: ls [DIR]...\r\n\tList information about the FILEs (the " + "current directory by default).", + true, NULL, cli_cmd_ls}); + + embeddedCliAddBinding(_cli, + (CliCommandBinding){"pwd", "Usage: pwd\r\n\tPrint the name of the current working directory.", + true, NULL, cli_cmd_pwd}); + + embeddedCliAddBinding(_cli, (CliCommandBinding){"mkdir", + "Usage: mkdir DIR...\r\n\tCreate the DIRECTORY(ies), if they do not " + "already exist..", + true, NULL, cli_cmd_mkdir}); + + embeddedCliAddBinding(_cli, (CliCommandBinding){"mv", "Usage: mv SOURCE DEST...\r\n\tRename SOURCE to DEST.", true, + NULL, cli_cmd_mv}); + + embeddedCliAddBinding(_cli, (CliCommandBinding){"rm", "Usage: rm [FILE]...\r\n\tRemove (unlink) the FILE(s).", true, + NULL, cli_cmd_rm}); + + return true; +} + +void cli_cmd_dd(EmbeddedCli *cli, char *args, void *context) { + (void)cli; + (void)context; + + uint32_t count = 1024; // default sectors to read + if (embeddedCliGetTokenCount(args) >= 1) { + count = (uint32_t)atoi(embeddedCliGetToken(args, 1)); + if (count == 0) { + count = 1024; + } + } + + // find first mounted MSC device + uint8_t dev_addr = 0; + for (uint8_t i = 1; i <= CFG_TUH_DEVICE_MAX; i++) { + if (tuh_msc_mounted(i)) { + dev_addr = i; + break; + } + } + if (dev_addr == 0) { + printf("no MSC device mounted\r\n"); + return; + } + + const uint8_t lun = 0; + const uint32_t block_size = tuh_msc_get_block_size(dev_addr, lun); + const uint32_t block_count = tuh_msc_get_block_count(dev_addr, lun); + if (count > block_count) { + count = block_count; + } + + const uint16_t sectors_per_xfer = (uint16_t)(sizeof(rw_buf) / block_size); + const uint32_t xfer_count = (count + sectors_per_xfer - 1) / sectors_per_xfer; + + printf("dd: reading %" PRIu32 " sectors (%" PRIu32 " bytes), %u sectors/xfer ...\r\n", + count, count * block_size, sectors_per_xfer); + + const uint32_t start_ms = tusb_time_millis_api(); + const uint8_t pdrv = dev_addr - 1; + bool submit_failed = false; + + for (uint32_t i = 0; i < count; i += sectors_per_xfer) { + const uint16_t n = (uint16_t)((count - i < sectors_per_xfer) ? (count - i) : sectors_per_xfer); + _disk_busy[pdrv] = true; + + if (!tuh_msc_read10(dev_addr, lun, rw_buf, i, n, disk_io_complete, 0)) { + _disk_busy[pdrv] = false; + printf("dd: failed to submit read at sector %" PRIu32 " (%u sectors)\r\n", i, n); + submit_failed = true; + break; + } + + wait_for_disk_io(pdrv); + } + + if (submit_failed) { + return; + } + + const uint32_t elapsed_ms = tusb_time_millis_api() - start_ms; + const uint32_t total_data = count * block_size; + // each SCSI transaction has 31-byte CBW + data + 13-byte CSW + const uint32_t total_bus = total_data + xfer_count * (31 + 13); + + if (elapsed_ms > 0) { + const uint32_t data_kbs = total_data / elapsed_ms; // KB/s (bytes/ms = KB/s) + const uint32_t bus_kbs = total_bus / elapsed_ms; + printf("dd: %" PRIu32 " bytes in %" PRIu32 " ms = %" PRIu32 " KB/s (bus %" PRIu32 " KB/s)\r\n", + total_data, elapsed_ms, data_kbs, bus_kbs); + } else { + printf("dd: %" PRIu32 " bytes in <1 ms\r\n", total_data); + } +} + +void cli_cmd_cat(EmbeddedCli *cli, char *args, void *context) { + (void)cli; + (void)context; + + uint16_t argc = embeddedCliGetTokenCount(args); + + // need at least 1 argument + if (argc == 0) { + printf("invalid arguments\r\n"); + return; + } + + for (uint16_t i = 0; i < argc; i++) { + FIL *fi = &file1; + const char *fpath = embeddedCliGetToken(args, i + 1); // token count from 1 + + if (FR_OK != f_open(fi, fpath, FA_READ)) { + printf("%s: No such file or directory\r\n", fpath); + } else { + UINT count = 0; + while ((FR_OK == f_read(fi, rw_buf, sizeof(rw_buf), &count)) && (count > 0)) { + for (UINT c = 0; c < count; c++) { + const uint8_t ch = rw_buf[c]; + if (isprint(ch) || iscntrl(ch)) { + putchar(ch); + } else { + putchar('.'); + } + } + } + } + + f_close(fi); + } +} + +void cli_cmd_cd(EmbeddedCli *cli, char *args, void *context) { + (void)cli; + (void)context; + + uint16_t argc = embeddedCliGetTokenCount(args); + + // only support 1 argument + if (argc != 1) { + printf("invalid arguments\r\n"); + return; + } + + // default is current directory + const char *dpath = args; + + if (FR_OK != f_chdir(dpath)) { + printf("%s: No such file or directory\r\n", dpath); + return; + } +} + +void cli_cmd_cp(EmbeddedCli *cli, char *args, void *context) { + (void)cli; + (void)context; + + uint16_t argc = embeddedCliGetTokenCount(args); + if (argc != 2) { + printf("invalid arguments\r\n"); + return; + } + + // default is current directory + const char *src = embeddedCliGetToken(args, 1); + const char *dst = embeddedCliGetToken(args, 2); + + FIL *f_src = &file1; + FIL *f_dst = &file2; + + if (FR_OK != f_open(f_src, src, FA_READ)) { + printf("cannot stat '%s': No such file or directory\r\n", src); + return; + } + + if (FR_OK != f_open(f_dst, dst, FA_WRITE | FA_CREATE_ALWAYS)) { + printf("cannot create '%s'\r\n", dst); + f_close(f_src); + return; + } else { + UINT rd_count = 0; + while ((FR_OK == f_read(f_src, rw_buf, sizeof(rw_buf), &rd_count)) && (rd_count > 0)) { + UINT wr_count = 0; + + if (FR_OK != f_write(f_dst, rw_buf, rd_count, &wr_count)) { + printf("cannot write to '%s'\r\n", dst); + break; + } + } + } + + f_close(f_src); + f_close(f_dst); +} + +void cli_cmd_ls(EmbeddedCli *cli, char *args, void *context) { + (void)cli; + (void)context; + + uint16_t argc = embeddedCliGetTokenCount(args); + + // only support 1 argument + if (argc > 1) { + printf("invalid arguments\r\n"); + return; + } + + // default is current directory + const char *dpath = "."; + if (argc) { + dpath = args; + } + + DIR dir; + if (FR_OK != f_opendir(&dir, dpath)) { + printf("cannot access '%s': No such file or directory\r\n", dpath); + return; + } + + FILINFO fno; + while ((f_readdir(&dir, &fno) == FR_OK) && (fno.fname[0] != 0)) { + if (fno.fname[0] != '.') // ignore . and .. entry + { + if (fno.fattrib & AM_DIR) { + // directory + printf("/%s\r\n", fno.fname); + } else { + printf("%-40s", fno.fname); + if (fno.fsize < 1024) { + printf("%" PRIu32 " B\r\n", fno.fsize); + } else { + printf("%" PRIu32 " KB\r\n", fno.fsize / 1024); + } + } + } + } + + f_closedir(&dir); +} + +void cli_cmd_pwd(EmbeddedCli *cli, char *args, void *context) { + (void)cli; + (void)context; + uint16_t argc = embeddedCliGetTokenCount(args); + + if (argc != 0) { + printf("invalid arguments\r\n"); + return; + } + + char path[256]; + if (FR_OK != f_getcwd(path, sizeof(path))) { + printf("cannot get current working directory\r\n"); + return; + } + + puts(path); +} + +void cli_cmd_mkdir(EmbeddedCli *cli, char *args, void *context) { + (void)cli; + (void)context; + + uint16_t argc = embeddedCliGetTokenCount(args); + + // only support 1 argument + if (argc != 1) { + printf("invalid arguments\r\n"); + return; + } + + // default is current directory + const char *dpath = args; + + if (FR_OK != f_mkdir(dpath)) { + printf("%s: cannot create this directory\r\n", dpath); + return; + } +} + +void cli_cmd_mv(EmbeddedCli *cli, char *args, void *context) { + (void)cli; + (void)context; + + uint16_t argc = embeddedCliGetTokenCount(args); + if (argc != 2) { + printf("invalid arguments\r\n"); + return; + } + + // default is current directory + const char *src = embeddedCliGetToken(args, 1); + const char *dst = embeddedCliGetToken(args, 2); + + if (FR_OK != f_rename(src, dst)) { + printf("cannot mv %s to %s\r\n", src, dst); + return; + } +} + +void cli_cmd_rm(EmbeddedCli *cli, char *args, void *context) { + (void)cli; + (void)context; + + uint16_t argc = embeddedCliGetTokenCount(args); + + // need at least 1 argument + if (argc == 0) { + printf("invalid arguments\r\n"); + return; + } + + for (uint16_t i = 0; i < argc; i++) { + const char *fpath = embeddedCliGetToken(args, i + 1); // token count from 1 + + if (FR_OK != f_unlink(fpath)) { + printf("cannot remove '%s': No such file or directory\r\n", fpath); + } + } +} diff --git a/examples/host/msc_file_explorer_freertos/src/msc_app.h b/examples/host/msc_file_explorer_freertos/src/msc_app.h new file mode 100644 index 000000000..eff195b1a --- /dev/null +++ b/examples/host/msc_file_explorer_freertos/src/msc_app.h @@ -0,0 +1,34 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2025 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. + * + * This file is part of the TinyUSB stack. + */ +#ifndef MSC_APP_H +#define MSC_APP_H + +#include <stdbool.h> +#include <stdio.h> + +bool msc_app_init(void); + +#endif diff --git a/examples/host/msc_file_explorer_freertos/src/tusb_config.h b/examples/host/msc_file_explorer_freertos/src/tusb_config.h new file mode 100644 index 000000000..c3fc4624f --- /dev/null +++ b/examples/host/msc_file_explorer_freertos/src/tusb_config.h @@ -0,0 +1,126 @@ +/* + * 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. + * + */ + +#ifndef TUSB_CONFIG_H_ +#define TUSB_CONFIG_H_ + +#ifdef __cplusplus + extern "C" { +#endif + +//-------------------------------------------------------------------- +// Common Configuration +//-------------------------------------------------------------------- + +// defined by compiler flags for flexibility +#ifndef CFG_TUSB_MCU +#error CFG_TUSB_MCU must be defined +#endif + +#ifndef CFG_TUSB_OS +#define CFG_TUSB_OS OPT_OS_FREERTOS +#endif + +// Espressif IDF requires "freertos/" prefix in include path +#ifdef ESP_PLATFORM +#define CFG_TUSB_OS_INC_PATH freertos/ +#endif + +#ifndef CFG_TUSB_DEBUG +#define CFG_TUSB_DEBUG 0 +#endif + +/* USB DMA on some MCUs can only access a specific SRAM region with restriction on alignment. + * Tinyusb use follows macros to declare transferring memory so that they can be put + * into those specific section. + * e.g + * - CFG_TUSB_MEM SECTION : __attribute__ (( section(".usb_ram") )) + * - CFG_TUSB_MEM_ALIGN : __attribute__ ((aligned(4))) + */ +#ifndef CFG_TUH_MEM_SECTION +#define CFG_TUH_MEM_SECTION +#endif + +#ifndef CFG_TUH_MEM_ALIGN +#define CFG_TUH_MEM_ALIGN __attribute__ ((aligned(4))) +#endif + +//-------------------------------------------------------------------- +// Host Configuration +//-------------------------------------------------------------------- + +// Enable Host stack +#define CFG_TUH_ENABLED 1 + +// #define CFG_TUH_MAX3421 1 // use max3421 as host controller + +#if CFG_TUSB_MCU == OPT_MCU_RP2040 + // #define CFG_TUH_RPI_PIO_USB 1 // use pio-usb as host controller + + // host roothub port is 1 if using either pio-usb or max3421 + #if (defined(CFG_TUH_RPI_PIO_USB) && CFG_TUH_RPI_PIO_USB) || (defined(CFG_TUH_MAX3421) && CFG_TUH_MAX3421) + #define BOARD_TUH_RHPORT 1 + #endif +#endif + +// Default is max speed that hardware controller could support with on-chip PHY +#define CFG_TUH_MAX_SPEED BOARD_TUH_MAX_SPEED + +//------------------------- Board Specific -------------------------- + +// RHPort number used for host can be defined by board.mk, default to port 0 +#ifndef BOARD_TUH_RHPORT +#define BOARD_TUH_RHPORT 0 +#endif + +// RHPort max operational speed can defined by board.mk +#ifndef BOARD_TUH_MAX_SPEED +#define BOARD_TUH_MAX_SPEED OPT_MODE_DEFAULT_SPEED +#endif + +//-------------------------------------------------------------------- +// Driver Configuration +//-------------------------------------------------------------------- + +// Size of buffer to hold descriptors and other data used for enumeration +#define CFG_TUH_ENUMERATION_BUFSIZE 256 + +#define CFG_TUH_HUB 1 // number of supported hubs +#define CFG_TUH_MSC 1 +#define CFG_TUH_CDC 0 +#define CFG_TUH_HID 0 // typical keyboard + mouse device can have 3-4 HID interfaces +#define CFG_TUH_VENDOR 0 + +// max device support (excluding hub device): 1 hub typically has 4 ports +#define CFG_TUH_DEVICE_MAX (3*CFG_TUH_HUB + 1) + +//------------- MSC -------------// +#define CFG_TUH_MSC_MAXLUN 4 // typical for most card reader + +#ifdef __cplusplus + } +#endif + +#endif /* TUSB_CONFIG_H_ */ diff --git a/src/portable/synopsys/dwc2/hcd_dwc2.c b/src/portable/synopsys/dwc2/hcd_dwc2.c index 6098d6eaa..9ea5f33c5 100644 --- a/src/portable/synopsys/dwc2/hcd_dwc2.c +++ b/src/portable/synopsys/dwc2/hcd_dwc2.c @@ -903,9 +903,6 @@ static void handle_rxflvl_irq(uint8_t rhport) { // return true if there is still pending data and need more ISR static bool handle_txfifo_empty(dwc2_regs_t* dwc2, bool is_periodic) { - // Use period txsts for both p/np to get request queue space available (1-bit difference, it is small enough) - const dwc2_hptxsts_t txsts = {.value = (is_periodic ? dwc2->hptxsts : dwc2->hnptxsts)}; - const uint8_t max_channel = dwc2_channel_count(dwc2); for (uint8_t ch_id = 0; ch_id < max_channel; ch_id++) { dwc2_channel_t* channel = &dwc2->channel[ch_id]; @@ -923,6 +920,8 @@ static bool handle_txfifo_empty(dwc2_regs_t* dwc2, bool is_periodic) { // skip if there is not enough space in FIFO and RequestQueue. // Packet's last word written to FIFO will trigger a request queue + // Use period txsts for both p/np to get request queue space available (1-bit difference, it is small enough) + const dwc2_hptxsts_t txsts = {.value = (is_periodic ? dwc2->hptxsts : dwc2->hnptxsts)}; if ((xact_bytes > (txsts.fifo_available << 2)) || (txsts.req_queue_available == 0)) { return true; } diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index f96d0d90a..5d9aa8431 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -34,6 +34,9 @@ import select import sys import time import warnings +import signal +from pathlib import Path +from typing import Any, TypedDict, NotRequired, cast # Suppress pkg_resources deprecation warning from fs module warnings.filterwarnings("ignore", message="pkg_resources is deprecated") @@ -45,6 +48,7 @@ import subprocess import json import glob from multiprocessing import Pool +from multiprocessing import TimeoutError as MpTimeoutError import fs import hashlib import ctypes @@ -63,6 +67,57 @@ board_test = {} build_dir = 'cmake-build' skip_flash = False +class FlasherCfg(TypedDict): + name: str + uid: str + args: str + + +class AttachedDevCfg(TypedDict, total=False): + vid_pid: str + serial: str + is_cdc: bool + is_msc: bool + block_count: int + block_size: int + + +class TestsCfg(TypedDict, total=False): + device: bool + dual: bool + host: bool + only: list[str] + skip: list[str] + dev_attached: list[AttachedDevCfg] + + +class BuildCfg(TypedDict, total=False): + flags_on: list[str] + args: list[str] + + +class Board(TypedDict): + name: str + uid: str + tests: TestsCfg + flasher: FlasherCfg + build: NotRequired[BuildCfg] + + +class HilConfig(TypedDict): + boards: list[Board] + +CMD_TIMEOUT = int(os.getenv('HIL_CMD_TIMEOUT', '180')) +POOL_TIMEOUT = int(os.getenv('HIL_POOL_TIMEOUT', '3000')) + + +def cmd_stdout_text(out: Any) -> str: + if out is None: + return '' + if isinstance(out, bytes): + return out.decode('utf-8', errors='ignore') + return str(out) + WCH_RISCV_CONTENT = """ adapter driver wlinke adapter speed 6000 @@ -91,8 +146,8 @@ issue at github.com/hathach/tinyusb" # ------------------------------------------------------------- # Path # ------------------------------------------------------------- -OPENCOD_ADI_PATH = f'{os.getenv("HOME")}/app/openocd_adi' -TINYUSB_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +OPENCOD_ADI_PATH = Path.home() / 'app' / 'openocd_adi' +TINYUSB_ROOT = Path(__file__).resolve().parents[2] # get usb serial by id def get_serial_dev(id, vendor_str, product_str, ifnum): @@ -119,7 +174,7 @@ def get_hid_dev(id, vendor_str, product_str, event): return f'/dev/input/by-id/usb-{vendor_str}_{product_str}_{id}-{event}' -def open_serial_dev(port): +def open_serial_dev(port: str): timeout = ENUM_TIMEOUT ser = None while timeout > 0: @@ -134,10 +189,11 @@ def open_serial_dev(port): timeout -= 0.1 assert timeout > 0, f'Cannot open port f{port}' if os.path.exists(port) else f'Port {port} not existed' + assert ser is not None return ser -def read_disk_file(uid, lun, fname): +def read_disk_file(uid: str, lun: int, fname: str) -> bytes: # open_fs("fat://{dev}) require 'pip install pyfatfs' dev = get_disk_dev(uid, 'TinyUSB', lun) timeout = ENUM_TIMEOUT @@ -154,8 +210,7 @@ def read_disk_file(uid, lun, fname): time.sleep(1) timeout -= 1 - assert timeout > 0, f'Storage {dev} not existed' - return None + raise AssertionError(f'Storage {dev} not existed') def open_mtp_dev(uid): @@ -177,7 +232,7 @@ def open_mtp_dev(uid): return None -def get_printer_dev(id, vendor_str, product_str, ifnum): +def get_printer_dev(id: str, vendor_str, product_str, ifnum: int): """Find /dev/usb/lpX by matching USB serial, vendor, product, and interface number via sysfs""" vendor_str = vendor_str.replace(' ', '_') if vendor_str else '' product_str = product_str.replace(' ', '_') if product_str else '' @@ -191,7 +246,7 @@ def get_printer_dev(id, vendor_str, product_str, ifnum): return None -def open_printer_dev(id, vendor_str, product_str, ifnum): +def open_printer_dev(id: str, vendor_str, product_str, ifnum: int) -> str: """Wait for printer device to enumerate and return its path""" timeout = ENUM_TIMEOUT while timeout > 0: @@ -206,41 +261,77 @@ def open_printer_dev(id, vendor_str, product_str, ifnum): # ------------------------------------------------------------- # Flashing firmware # ------------------------------------------------------------- -def run_cmd(cmd, cwd=None): - r = subprocess.run(cmd, cwd=cwd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) +def run_cmd(cmd: str, cwd: str | None = None, timeout: int = CMD_TIMEOUT) -> subprocess.CompletedProcess: + popen_kwargs = { + 'cwd': cwd, + 'shell': True, + 'stdout': subprocess.PIPE, + 'stderr': subprocess.STDOUT, + 'text': True, + 'encoding': 'utf-8', + 'errors': 'replace', + } + if os.name != 'nt': + popen_kwargs['preexec_fn'] = os.setsid + + p = subprocess.Popen(cmd, **popen_kwargs) + try: + out, _ = p.communicate(timeout=timeout) + r = subprocess.CompletedProcess(args=cmd, returncode=p.returncode, stdout=out) + except subprocess.TimeoutExpired as ex: + if os.name != 'nt': + try: + os.killpg(p.pid, signal.SIGKILL) + except ProcessLookupError: + pass + else: + p.kill() + out, _ = p.communicate() + timeout_out = ex.stdout or out or b'' + title = f'COMMAND TIMEOUT ({timeout}s): {cmd}' + print() + if os.getenv('CI'): + print(f"::group::{title}") + print(cmd_stdout_text(timeout_out)) + print(f"::endgroup::") + else: + print(title) + print(cmd_stdout_text(timeout_out)) + return subprocess.CompletedProcess(args=cmd, returncode=124, stdout=timeout_out) + if r.returncode != 0: title = f'COMMAND FAILED: {cmd}' print() if os.getenv('CI'): print(f"::group::{title}") - print(r.stdout.decode("utf-8")) + print(cmd_stdout_text(r.stdout)) print(f"::endgroup::") else: print(title) - print(r.stdout.decode("utf-8")) + print(cmd_stdout_text(r.stdout)) elif verbose: print(cmd) - print(r.stdout.decode("utf-8")) + print(cmd_stdout_text(r.stdout)) return r -def flash_jlink(board, firmware): +def flash_jlink(board: Board, firmware: str) -> subprocess.CompletedProcess: flasher = board['flasher'] script = ['halt', 'r', f'loadfile {firmware}.elf', 'r', 'go', 'exit'] - f_jlink = f'{board["name"]}_{os.path.basename(firmware)}.jlink' - with open(f_jlink, 'w') as f: + f_jlink = Path(f'{board["name"]}_{Path(firmware).name}.jlink') + with f_jlink.open('w') as f: f.writelines(f'{s}\n' for s in script) ret = run_cmd(f'JLinkExe -USB {flasher["uid"]} {flasher["args"]} -if swd -JTAGConf -1,-1 -speed auto -NoGui 1 -ExitOnError 1 -CommandFile {f_jlink}') - os.remove(f_jlink) + f_jlink.unlink(missing_ok=True) return ret -def reset_jlink(board): +def reset_jlink(board: Board) -> subprocess.CompletedProcess: flasher = board['flasher'] script = ['halt', 'r', 'go', 'exit'] - f_jlink = f'{board["name"]}_reset.jlink' - if not os.path.exists(f_jlink): - with open(f_jlink, 'w') as f: + f_jlink = Path(f'{board["name"]}_reset.jlink') + if not f_jlink.exists(): + with f_jlink.open('w') as f: f.writelines(f'{s}\n' for s in script) ret = run_cmd(f'JLinkExe -USB {flasher["uid"]} {flasher["args"]} -if swd -JTAGConf -1,-1 -speed auto -NoGui 1 -ExitOnError 1 -CommandFile {f_jlink}') return ret @@ -303,16 +394,20 @@ def reset_openocd_wch(board): return ret -def flash_openocd_adi(board, firmware): +def flash_openocd_adi(board: Board, firmware: str) -> subprocess.CompletedProcess: flasher = board['flasher'] - ret = run_cmd(f'{OPENCOD_ADI_PATH}/src/openocd -c "adapter serial {flasher["uid"]}" -s {OPENCOD_ADI_PATH}/tcl ' + openocd = OPENCOD_ADI_PATH / 'src' / 'openocd' + tcl_dir = OPENCOD_ADI_PATH / 'tcl' + ret = run_cmd(f'{openocd} -c "adapter serial {flasher["uid"]}" -s {tcl_dir} ' f'{flasher["args"]} -c "program {firmware}.elf reset exit"') return ret -def reset_openocd_adi(board): +def reset_openocd_adi(board: Board) -> subprocess.CompletedProcess: flasher = board['flasher'] - ret = run_cmd(f'{OPENCOD_ADI_PATH}/src/openocd -c "adapter serial {flasher["uid"]}" -s {OPENCOD_ADI_PATH}/tcl ' + openocd = OPENCOD_ADI_PATH / 'src' / 'openocd' + tcl_dir = OPENCOD_ADI_PATH / 'tcl' + ret = run_cmd(f'{openocd} -c "adapter serial {flasher["uid"]}" -s {tcl_dir} ' f'{flasher["args"]} -c "program reset exit"') return ret @@ -331,17 +426,17 @@ def reset_wlink_rs(board): return ret -def flash_esptool(board, firmware): +def flash_esptool(board: Board, firmware: str) -> subprocess.CompletedProcess: flasher = board['flasher'] port = get_serial_dev(flasher["uid"], None, None, 0) - fw_dir = os.path.dirname(f'{firmware}.bin') - with open(f'{fw_dir}/config.env') as f: + fw_dir = Path(f'{firmware}.bin').parent + with (fw_dir / 'config.env').open() as f: idf_target = json.load(f)['IDF_TARGET'] - with open(f'{fw_dir}/flash_args') as f: + with (fw_dir / 'flash_args').open() as f: flash_args = f.read().strip().replace('\n', ' ') command = (f'esptool --chip {idf_target} -p {port} {flasher["args"]} ' f'--before=default_reset --after=hard_reset write_flash {flash_args}') - ret = run_cmd(command, cwd=fw_dir) + ret = run_cmd(command, cwd=str(fw_dir)) return ret @@ -684,7 +779,7 @@ def test_device_cdc_dual_ports(board): sizes = [32, 64, 128, 256, 512, random.randint(2000, 5000)] - def write_and_check(writer, payload): + def write_and_check(writer, payload : bytes): payload_len = len(payload) for s in ser: s.reset_input_buffer() @@ -785,7 +880,7 @@ def test_device_cdc_msc_throughput(board): # Put tty in raw mode so dd sees pure binary throughput. rs = run_cmd(f'timeout 30 stty -F {tty} raw -echo') - assert rs.returncode == 0, f'stty failed: {rs.stdout.decode()}' + assert rs.returncode == 0, f'stty failed: {cmd_stdout_text(rs.stdout)}' # Payload aim: ~5 s per direction at FS (~830 kB/s), much less at HS. msc_count = 2 if is_fs else 16 # bs=1M @@ -794,20 +889,20 @@ def test_device_cdc_msc_throughput(board): tmp_file = f'/tmp/cdc_msc_tp_{uid}.bin' rw = run_cmd(f'timeout 30 dd if=/dev/zero of={tty} bs=64K count={cdc_count} 2>&1') - assert rw.returncode == 0, f'CDC dd write failed: {rw.stdout.decode()}' - cdc_w = parse_speed(rw.stdout.decode()) + assert rw.returncode == 0, f'CDC dd write failed: {cmd_stdout_text(rw.stdout)}' + cdc_w = parse_speed(cmd_stdout_text(rw.stdout)) rr = run_cmd(f'timeout 30 dd if={tty} of=/dev/null bs=64K count={cdc_count} iflag=fullblock 2>&1') - assert rr.returncode == 0, f'CDC dd read failed: {rr.stdout.decode()}' - cdc_r = parse_speed(rr.stdout.decode()) + assert rr.returncode == 0, f'CDC dd read failed: {cmd_stdout_text(rr.stdout)}' + cdc_r = parse_speed(cmd_stdout_text(rr.stdout)) rmr = run_cmd(f'dd if={dev} of={tmp_file} bs=1M count={msc_count} iflag=direct 2>&1') - assert rmr.returncode == 0, f'MSC dd read failed: {rmr.stdout.decode()}' - msc_r = parse_speed(rmr.stdout.decode()) + assert rmr.returncode == 0, f'MSC dd read failed: {cmd_stdout_text(rmr.stdout)}' + msc_r = parse_speed(cmd_stdout_text(rmr.stdout)) rmw = run_cmd(f'dd if={tmp_file} of={dev} bs=1M count={msc_count} oflag=direct 2>&1') - assert rmw.returncode == 0, f'MSC dd write failed: {rmw.stdout.decode()}' - msc_w = parse_speed(rmw.stdout.decode()) + assert rmw.returncode == 0, f'MSC dd write failed: {cmd_stdout_text(rmw.stdout)}' + msc_w = parse_speed(cmd_stdout_text(rmw.stdout)) try: os.remove(tmp_file) @@ -824,7 +919,7 @@ def test_device_dfu(board): timeout = ENUM_TIMEOUT while timeout > 0: ret = run_cmd(f'dfu-util -l') - stdout = ret.stdout.decode() + stdout = cmd_stdout_text(ret.stdout) if f'serial="{uid}"' in stdout and 'Found DFU: [cafe:4000]' in stdout: break time.sleep(1) @@ -864,7 +959,7 @@ def test_device_dfu_runtime(board): timeout = ENUM_TIMEOUT while timeout > 0: ret = run_cmd(f'dfu-util -l') - stdout = ret.stdout.decode() + stdout = cmd_stdout_text(ret.stdout) if f'serial="{uid}"' in stdout and 'Found Runtime: [cafe:4000]' in stdout: break time.sleep(1) @@ -1262,7 +1357,7 @@ host_test = [ ] -def test_example(board, f1, example): +def test_example(board: Board, f1: str, example: str) -> int: """ Test example firmware :param board: board dict @@ -1277,11 +1372,11 @@ def test_example(board, f1, example): if f1 != "": f1_str = '-f1_' + f1.replace(' ', '_') - fw_dir = f'{TINYUSB_ROOT}/{build_dir}/cmake-build-{name}{f1_str}/{example}' - fw_name = f'{fw_dir}/{os.path.basename(example)}' + fw_dir = TINYUSB_ROOT / build_dir / f'cmake-build-{name}{f1_str}' / example + fw_name = fw_dir / Path(example).name print(f'{name+f1_str:40} {example:30} ...', end='') - if not os.path.exists(fw_dir) or not (os.path.exists(f'{fw_name}.elf') or os.path.exists(f'{fw_name}.bin')): + if not fw_dir.exists() or not ((fw_name.with_suffix('.elf')).exists() or (fw_name.with_suffix('.bin')).exists()): print('Skip (no binary)') return 0 @@ -1294,7 +1389,7 @@ def test_example(board, f1, example): flash_ok = True for i in range(max_retry): if not skip_flash: - ret = globals()[f'flash_{board["flasher"]["name"].lower()}'](board, fw_name) + ret = globals()[f'flash_{board["flasher"]["name"].lower()}'](board, str(fw_name)) flash_ok = (ret.returncode == 0) if flash_ok: try: @@ -1324,18 +1419,18 @@ def test_example(board, f1, example): return err_count -def build_board(board): +def build_board(board: Board) -> tuple[str, int]: """Build firmware for this board via tools/build.py. Honors board config's build.flags_on variants and build.args defines. Output goes to cmake-build/cmake-build-BOARD[-f1_...]/ (tools/build.py layout).""" name = board['name'] - bcfg = board.get('build', {}) + bcfg = cast(BuildCfg, board.get('build', {})) flags_on_list = bcfg.get('flags_on', ['']) extra_defs = bcfg.get('args', []) failed = 0 for f1 in flags_on_list: - cmd = [sys.executable, f'{TINYUSB_ROOT}/tools/build.py', '-b', name] + cmd = [sys.executable, str(TINYUSB_ROOT / 'tools' / 'build.py'), '-b', name] for d in extra_defs: cmd += ['-D', d] if f1: @@ -1350,7 +1445,7 @@ def build_board(board): return name, failed -def test_board(board): +def test_board(board: Board) -> tuple[str, int, list[str]]: name = board['name'] flasher = board['flasher'] @@ -1364,11 +1459,11 @@ def test_board(board): else: if 'tests' in board: board_tests = board['tests'] - if 'device' in board_tests and board_tests['device'] == True: + if board_tests.get('device') is True: test_list += list(device_tests) - if 'dual' in board_tests and board_tests['dual'] == True: + if board_tests.get('dual') is True: test_list += dual_tests - if 'host' in board_tests and board_tests['host'] == True: + if board_tests.get('host') is True: test_list += host_test if 'only' in board_tests: test_list = board_tests['only'] @@ -1398,7 +1493,7 @@ def test_board(board): return name, err_count, sorted(set(failed_tests)) -def main(): +def main() -> None: """ Hardware test on specified boards """ @@ -1425,7 +1520,7 @@ def main(): parser.add_argument('-v', '--verbose', action='store_true', help='Verbose output') args = parser.parse_args() - config_file = args.config_file + config_file = Path(args.config_file) boards = args.board skip_boards = args.skip_board verbose = args.verbose @@ -1440,10 +1535,10 @@ def main(): skip_flash = args.skip_flash # if config file is not found, try to find it in the same directory as this script - if not os.path.exists(config_file): - config_file = os.path.join(os.path.dirname(__file__), config_file) - with open(config_file) as f: - config = json.load(f) + if not config_file.exists(): + config_file = Path(__file__).resolve().parent / config_file + with config_file.open() as f: + config = cast(HilConfig, json.load(f)) if len(boards) == 0: config_boards = [e for e in config['boards'] if e['name'] not in skip_boards] @@ -1465,20 +1560,26 @@ def main(): print(f'Build phase done: {build_err} failed') print('-' * 30) - with Pool(processes=os.cpu_count()) as pool: - mret = pool.map(test_board, config_boards) + with Pool(processes=os.cpu_count() or 1) as pool: + async_ret = pool.map_async(test_board, config_boards) + try: + mret = async_ret.get(timeout=POOL_TIMEOUT) + except MpTimeoutError: + pool.terminate() + pool.join() + raise RuntimeError(f'HIL worker pool timed out after {POOL_TIMEOUT}s') err_count = build_err + sum(e[1] for e in mret) # generate skip list for next re-run if failed: skip boards that fully passed, # and emit -bt BOARD:t1,t2 so each failed board only re-runs its own failed tests. - skip_fname = f'{config_file}.skip' + skip_fname = config_file.with_suffix(config_file.suffix + '.skip') if err_count > 0: skip_boards += [name for name, err, _ in mret if err == 0] parts = [f'--skip-board {i}' for i in skip_boards] parts += [f'-bt {name}:{",".join(fts)}' for name, err, fts in mret if err > 0 and fts] - with open(skip_fname, 'w') as f: + with skip_fname.open('w') as f: f.write(' '.join(parts)) - elif os.path.exists(skip_fname): - os.remove(skip_fname) + elif skip_fname.exists(): + skip_fname.unlink() duration = time.time() - duration print() |
