summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZixun LI <[email protected]>2026-05-11 16:02:50 +0200
committerGitHub <[email protected]>2026-05-11 16:02:50 +0200
commit4e64f3e91cefab0993a837ac03476173eeeda854 (patch)
tree0843841795f9260bea7da7564b61037810e4476a
parentdcd2a4bd0f005835d3edc193309ef3484cd9c8b3 (diff)
parent3188ed4fd6ccab395b6618d913ebdb44d94c3dcc (diff)
Merge pull request #3632 from hathach/msc_os
hcd/dwc2: fix txfifo full check
-rw-r--r--.github/workflows/build.yml7
-rw-r--r--examples/host/CMakeLists.txt1
-rw-r--r--examples/host/msc_file_explorer_freertos/CMakeLists.txt42
-rw-r--r--examples/host/msc_file_explorer_freertos/CMakePresets.json6
-rw-r--r--examples/host/msc_file_explorer_freertos/Makefile27
-rw-r--r--examples/host/msc_file_explorer_freertos/README.md105
-rw-r--r--examples/host/msc_file_explorer_freertos/only.txt28
-rw-r--r--examples/host/msc_file_explorer_freertos/skip.txt3
-rw-r--r--examples/host/msc_file_explorer_freertos/src/CMakeLists.txt13
-rw-r--r--examples/host/msc_file_explorer_freertos/src/ffconf.h313
-rw-r--r--examples/host/msc_file_explorer_freertos/src/main.c158
-rw-r--r--examples/host/msc_file_explorer_freertos/src/msc_app.c709
-rw-r--r--examples/host/msc_file_explorer_freertos/src/msc_app.h34
-rw-r--r--examples/host/msc_file_explorer_freertos/src/tusb_config.h126
-rw-r--r--src/portable/synopsys/dwc2/hcd_dwc2.c5
-rwxr-xr-xtest/hil/hil_test.py233
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()