diff options
| author | HiFiPhile <[email protected]> | 2026-05-22 13:19:45 +0200 |
|---|---|---|
| committer | HiFiPhile <[email protected]> | 2026-05-22 13:19:45 +0200 |
| commit | c24016991f1b58544cf3bfe05815df637f466e0e (patch) | |
| tree | 8ba7e710d390b9c012327dec04c767f66d42a53a | |
| parent | 650e8a194fcfaa4b91203e9aedfecd614ea70105 (diff) | |
| parent | e1cb1b5b6fa183a1473f05b8d1ce4ab93a373ef4 (diff) | |
Merge remote-tracking branch 'tinyusb/master' into fix/Dwc2Stm32U5
131 files changed, 4716 insertions, 1578 deletions
diff --git a/.claude/commands/hil.md b/.claude/commands/hil.md deleted file mode 100644 index 07e1a865b..000000000 --- a/.claude/commands/hil.md +++ /dev/null @@ -1,58 +0,0 @@ -# hil - -Run Hardware-in-the-Loop (HIL) tests on physical boards. - -## Arguments -- $ARGUMENTS: Optional flags (e.g. board name, extra args). If empty, runs all boards with default config. - -## Instructions - -1. Parse $ARGUMENTS: - - If $ARGUMENTS contains `-b BOARD_NAME`, run for that specific board only. - - If $ARGUMENTS is empty or has no `-b`, run for all boards in the config. - - Pass through any other flags (e.g. `-v` for verbose, `-r N` for retry count) directly to the command. - -2. Determine whether to run **locally** or **remotely via SSH**: - - **Local**: boards are attached to this machine (default when `local.json` is used) - - **Remote (`ssh ci.lan`)**: boards are attached to the CI machine (when `tinyusb.json` is used) - -3. **Local execution** (boards attached to this machine): - ```bash - python test/hil/hil_test.py -b BOARD_NAME -B examples $HIL_CONFIG $EXTRA_ARGS - ``` - -4. **Remote execution** (boards attached to `ci.lan`): - Only copy the minimal files needed (firmware binaries + test script + config), then run remotely. - - ```bash - REMOTE=ci.lan - REMOTE_DIR=/tmp/tinyusb-hil - - # Create remote working directory - ssh $REMOTE "rm -rf $REMOTE_DIR && mkdir -p $REMOTE_DIR/test/hil" - - # Copy HIL test script and its dependency - scp test/hil/hil_test.py test/hil/pymtp.py test/hil/tinyusb.json $REMOTE:$REMOTE_DIR/test/hil/ - - # Copy only the firmware binaries for the target board(s) - # For a specific board: - scp -r examples/cmake-build-$BOARD_NAME $REMOTE:$REMOTE_DIR/examples/ - - # Or for all boards that have been built: - # for dir in examples/cmake-build-*/; do scp -r "$dir" $REMOTE:$REMOTE_DIR/examples/; done - - # Run the test remotely - ssh $REMOTE "cd $REMOTE_DIR && python3 test/hil/hil_test.py -b $BOARD_NAME -B examples tinyusb.json $EXTRA_ARGS" - ``` - - Note: The remote machine (`ci.lan`) must have: - - Python 3 with `pyserial` installed (`pip install pyserial`) - - Flasher tools: `JLinkExe`, `openocd`, etc. as needed by the board - - USB access to the boards (udev rules configured) - -5. Use a timeout of at least 20 minutes (600000ms). HIL tests take 2-5 minutes. NEVER cancel early. - -6. After the test completes: - - Show the test output to the user. - - Summarize pass/fail results per board. - - If there are failures, suggest re-running with `-v` flag for verbose output to help debug. diff --git a/.claude/skills/code-size/SKILL.md b/.claude/skills/code-size/SKILL.md new file mode 100644 index 000000000..f3c51ccfa --- /dev/null +++ b/.claude/skills/code-size/SKILL.md @@ -0,0 +1,76 @@ +--- +name: code-size +description: Use when comparing TinyUSB code size between a base ref (master by default) and the current branch to evaluate the size impact of changes. Three granularities — single example on one board (with optional bloaty), all examples on one board, or all examples across CI families combined. +--- + +# Code Size Comparison + +Compare TinyUSB code size between a base ref (default `master`) and the current branch using `tools/metrics_compare_base.py`. Three granularities — pick the narrowest one that exercises your change: + +| Granularity | When to use | Command | +|---|---|---| +| **single example, one board** | Focused change touching one feature | `-b BOARD -e device/cdc_msc` | +| **all examples, one board** | Per-board regression sweep | `-b BOARD` | +| **all examples, all CI families (combined)** | Pre-merge full check | `--ci` | + +The script handles the full base-vs-branch dance: +1. Creates a temporary git worktree of the base ref under `cmake-metrics/_worktree/`. +2. Builds the base in `cmake-metrics/<board>/base/`. +3. Builds the current tree in `cmake-metrics/<board>/build/`. +4. Runs `tools/metrics.py compare` and writes `cmake-metrics/<board>/metrics_compare.md`. +5. Removes the worktree on exit. + +`--combined` (auto-set by `--ci`) also produces `cmake-metrics/_combined/metrics_compare.md` aggregating across all boards. + +## Choosing arguments + +Infer from the user's request: + +- **Board(s):** named board → `-b BOARD` (repeatable). "All boards" / "CI" / "full sweep" → `--ci` (first board of each arm-gcc family). Default to a fast board (`raspberry_pi_pico`) if unspecified for an iterative check. +- **Example:** named example → `-e <group>/<name>` (e.g. `-e device/cdc_msc`). "All examples" → omit `-e`. +- **Bloaty:** only with `-e`. Use when the user wants a section/symbol-level breakdown for a single binary. +- **Base ref:** default `master`. Override with `--base-branch <ref>` (tag or commit also works). +- **Filter:** default is the absolute path of each side's `<checkout>/src/` directory, which uniquely identifies TinyUSB stack code without matching vendored deps that also have a `src/` (e.g. `pico-sdk/src/`). Override with one or more `-f SUBSTRING` flags to use repo-relative substrings instead. Change only if asked. + +## Common invocations + +```bash +# Single example, one board (linkermap, fastest): +python3 tools/metrics_compare_base.py -b raspberry_pi_pico -e device/cdc_msc + +# Same with bloaty for section/symbol breakdown: +python3 tools/metrics_compare_base.py -b raspberry_pi_pico -e device/cdc_msc --bloaty + +# All examples for one board: +python3 tools/metrics_compare_base.py -b raspberry_pi_pico + +# Multiple boards, one combined report: +python3 tools/metrics_compare_base.py -b raspberry_pi_pico -b raspberry_pi_pico2 --combined + +# Full CI sweep (first board per arm-gcc family, combined): +python3 tools/metrics_compare_base.py --ci + +# Compare against a tag/commit instead of master: +python3 tools/metrics_compare_base.py -b raspberry_pi_pico --base-branch v0.18.0 +``` + +## Outputs + +- **Per-board:** `cmake-metrics/<board>/metrics_compare.md` (and `_<example>.md` when `-e` is set) +- **Combined (with `--combined`/`--ci`):** `cmake-metrics/_combined/metrics_compare.md` +- **Bloaty:** printed to stdout as section + symbol diffs + +## Timing + +- Single example, single board: ~30 s +- All examples, single board: ~60-90 s +- `--ci` (all arm-gcc families, first board each): 4-8 minutes — sequential sweep across boards (Ninja parallelizes within each board, not across) + +Use timeouts ≥ 10 minutes (600000 ms) for `--ci`. + +## Reporting results + +After running: +- Show the markdown report's summary table to the user. +- Highlight any rows with non-zero `% diff` — under the default filter every row is a TinyUSB stack source file (e.g. `usbd.c`, `cdc_device.c`, `dcd_<port>.c`), so any non-zero delta is a real stack-size impact. +- If the diff is unexpected, follow up with a single-example `--bloaty` run to localize. diff --git a/.claude/skills/hil/SKILL.md b/.claude/skills/hil/SKILL.md new file mode 100644 index 000000000..1f3d7d072 --- /dev/null +++ b/.claude/skills/hil/SKILL.md @@ -0,0 +1,66 @@ +--- +name: hil +description: Use when running TinyUSB Hardware-in-the-Loop (HIL) tests on physical boards, debugging HIL failures, or copying firmware to the ci.lan test rig. Covers local execution and remote execution over SSH, config selection, and debugging tips. +--- + +# Hardware-in-the-Loop (HIL) Testing + +Run TinyUSB HIL tests against real boards. Two execution modes — **local** (boards attached to this machine) and **remote** (boards attached to `ci.lan`, reached over SSH). Default to **local** unless the user specifies `remote`. Do not auto-detect. + +## Prerequisites + +- Examples must already be built for the target board(s). See AGENTS.md "Build" → "All examples for a board", which produces `examples/cmake-build-<board>/`. +- `-B examples` tells `hil_test.py` that `examples/` is the parent folder containing the per-board build outputs. + +## Choosing arguments + +Infer from the user's request: + +- **Mode:** `local` (default) or `remote`. Only switch to `remote` if the user explicitly says so or names `ci.lan`. +- **Board:** if the user names a specific board, pass `-b BOARD_NAME`. Otherwise omit `-b` to run all boards in the config. +- **Pass-through flags:** `-v` (verbose), `-r N` (retry count), etc. — pass through unchanged. + +Config file follows from mode: +- **Local** → `test/hil/local.json` (user-supplied; not tracked in repo — describes boards attached locally) +- **Remote** → `test/hil/tinyusb.json` (tracked; describes the `ci.lan` test rig) + +If `local.json` is missing, fall back to `tinyusb.json` only when explicitly told to; otherwise stop and ask the user to supply one. + +## Local execution + +Boards attached to this machine: + +```bash +# Specific board: +python3 test/hil/hil_test.py -b BOARD_NAME -B examples test/hil/local.json $EXTRA_ARGS +# All boards in the config (no -b): +python3 test/hil/hil_test.py -B examples test/hil/local.json $EXTRA_ARGS +``` + +## Remote execution (ci.lan) + +Use `test/hil/hil_ci.sh` — it handles dir setup, scp of test scripts, rsync of firmware artifacts (`.elf` / `.bin` / `.hex` only), and running `hil_test.py` on `ci.lan`: + +```bash +# Specific board: +bash test/hil/hil_ci.sh -b raspberry_pi_pico2 +# All boards in tinyusb.json: +bash test/hil/hil_ci.sh +# Pass-through extra args (any non -b flag is forwarded to hil_test.py): +bash test/hil/hil_ci.sh -b raspberry_pi_pico2 -t host/cdc_msc_hid -r 1 +``` + +Overrides via env vars: `REMOTE=ci.lan`, `REMOTE_DIR=/tmp/tinyusb-hil`, `CONFIG=test/hil/tinyusb.json`. + +The script fails fast if the build dir or repo layout is missing. + +## Timing + +HIL runs take 2-5 minutes. Use a timeout of at least 20 minutes (600000 ms). NEVER cancel early. + +## Reporting results + +After the test completes: +- Show the test output to the user. +- Summarize pass/fail per board. +- On failure, suggest re-running with `-v` for verbose output. If `-v` isn't enough, temporarily add debug prints to `test/hil/hil_test.py` to pinpoint the issue. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 60f4c7ca1..af0191149 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,12 +9,9 @@ on: types: [ published ] concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }} cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} -env: - HIL_JSON: test/hil/tinyusb.json - jobs: # Check if the code changes and we need to run ci build # Cannot use paths filter in the on-event since we want this workflow to run even when there are no code changes, to register the commit chain @@ -59,11 +56,12 @@ jobs: id: set-matrix-json run: | # build matrix - MATRIX_JSON=$(python .github/workflows/ci_set_matrix.py)/ + MATRIX_JSON=$(python .github/workflows/ci_set_matrix.py) echo "matrix=$MATRIX_JSON" echo "matrix=$MATRIX_JSON" >> $GITHUB_OUTPUT - # hil matrix - HIL_MATRIX_JSON=$(python test/hil/hil_ci_set_matrix.py ${{ env.HIL_JSON }}) + + # HIL matrix (merged from tinyusb + hifiphile configs) + HIL_MATRIX_JSON=$(python test/hil/hil_ci_set_matrix.py test/hil/tinyusb.json test/hil/hfp.json) echo "hil_matrix=$HIL_MATRIX_JSON" echo "hil_matrix=$HIL_MATRIX_JSON" >> $GITHUB_OUTPUT @@ -239,7 +237,7 @@ jobs: # --------------------------------------- # Hardware in the loop (HIL) - # Run on PR only (hil-tinyusb), hil-hfp only run on non-forked PR + # Run on PR only (hil-tinyusb), hil-hfp-iar only run on non-forked PR # --------------------------------------- hil-build: needs: [ check-paths, set-matrix ] @@ -263,7 +261,20 @@ jobs: # --------------------------------------- hil-tinyusb: needs: hil-build - runs-on: [ self-hosted, X64, hathach, hardware-in-the-loop ] + name: hil-tinyusb (${{ matrix.display }}) + strategy: + fail-fast: false + matrix: + include: + - display: tinyusb.json + runner: [ self-hosted, X64, hathach, hardware-in-the-loop ] + hil_json: test/hil/tinyusb.json + - display: hfp.json + runner: [ self-hosted, Linux, X64, hifiphile ] + hil_json: test/hil/hfp.json + runs-on: ${{ matrix.runner }} + env: + HIL_JSON: ${{ matrix.hil_json }} steps: - name: Get Skip Boards from previous run if: github.run_attempt != '1' @@ -308,20 +319,22 @@ jobs: # self-hosted by HFP, build with IAR toolchain, for attached hardware checkout test/hil/hfp.json # Since IAR Token secret is not passed to forked PR, only build non-forked PR # --------------------------------------- - hil-hfp: + hil-hfp-iar: needs: [ check-paths ] if: | needs.check-paths.outputs.code_changed == 'true' && 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 @@ -345,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/.gitignore b/.gitignore index b833191f8..c11e51bb9 100644 --- a/.gitignore +++ b/.gitignore @@ -56,3 +56,34 @@ BrowseInfo .cmake_build README_processed.rst .worktrees +cmake-metrics/ +# Directories fetched by tools/get_deps.py - not to be committed +lib/CMSIS_5/ +lib/CMSIS_6/ +lib/FreeRTOS-Kernel/ +lib/fatfs/ +lib/lwip/ +lib/sct_neopixel/ +lib/threadx/ +tools/linkermap/ +tools/uf2/ +hw/mcu/allwinner/ +hw/mcu/analog/ +hw/mcu/artery/ +hw/mcu/broadcom/ +hw/mcu/bridgetek/ft9xx/ft90x-sdk/ +hw/mcu/gd/ +hw/mcu/hpmicro/ +hw/mcu/infineon/ +hw/mcu/microchip/ +hw/mcu/mindmotion/ +hw/mcu/nordic/nrfx/ +hw/mcu/nuvoton/ +hw/mcu/nxp/ +hw/mcu/raspberry_pi/ +hw/mcu/renesas/ +hw/mcu/silabs/ +hw/mcu/sony/cxd56/spresense-exported-sdk/ +hw/mcu/st/ +hw/mcu/ti/ +hw/mcu/wch/ @@ -1,428 +1,217 @@ # TinyUSB Agent Instructions -TinyUSB is an open-source cross-platform USB Host/Device stack for embedded systems, designed to be memory-safe with no -dynamic allocation and thread-safe with all interrupt events deferred to non-ISR task functions. +TinyUSB is a cross-platform USB Host/Device stack for embedded systems: memory-safe (no dynamic allocation) and thread-safe (ISR events deferred to task context). -Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected -information that does not match the info here. +Reference these instructions first; fall back to search/bash only when reality diverges. -## Shared Ground Rules -- Keep TinyUSB memory-safe: avoid dynamic allocation, defer ISR work to task context, and follow C99 with two-space indentation/no tabs. -- Match file organization: core stack under `src`, MCU/BSP support in `hw/{mcu,bsp}`, examples under `examples/{device,host,dual}`, docs in `docs`, tests under `test/{unit-test,fuzz,hil}`. -- Use descriptive snake_case for helpers, reserve `tud_`/`tuh_` for public APIs, `TU_` for macros, and keep headers self-contained with `#if CFG_TUSB_MCU` guards where needed. -- Prefer `.clang-format` for C/C++ formatting, run `pre-commit run --all-files` before submitting, and document board/HIL coverage when applicable. -- Commit in imperative mood, keep changes scoped, and supply PRs with linked issues plus test/build evidence. +## Behavioral Guidelines +Bias toward caution over speed. For trivial tasks, use judgment. -## Bootstrap and Build Setup +- **Think first** — state assumptions; ask if unclear; present alternatives instead of picking silently. +- **Simplicity** — no features, abstractions, flexibility, or error handling beyond what was asked. If 200 lines could be 50, rewrite. +- **Surgical changes** — touch only what the task requires; match existing style; don't refactor working code; mention unrelated dead code rather than deleting it. Remove only orphans *your* changes created. +- **Goal-driven** — turn tasks into verifiable goals ("write failing test, make it pass"). For multi-step work, state a brief `step → verify` plan. -- Install ARM GCC toolchain: `sudo apt-get update && sudo apt-get install -y gcc-arm-none-eabi` -- Fetch core dependencies: `python3 tools/get_deps.py` -- takes <1 second. NEVER CANCEL. -- For specific board families: `python3 tools/get_deps.py FAMILY_NAME` (e.g., rp2040, stm32f4), or - `python3 tools/get_deps.py -b BOARD_NAME` -- Dependencies are cached in `lib/` and `hw/mcu/` directories -- For **Espressif** boards, initialize the ESP-IDF environment before any build/flash/monitor command: - `. $HOME/code/esp-idf/export.sh` +## Ground Rules -## Build Examples +- **Language/style:** C99, 2-space indent (no tabs), snake_case helpers, `UPPER_CASE` macros. Public APIs use `tud_`/`tuh_`; macros use `TU_`. Headers self-contained with `#if CFG_TUSB_MCU` guards. +- **Safety:** no dynamic allocation; defer ISR work to task context; use `TU_ASSERT()` for error checks; always check return values; include order: C stdlib → tusb common → drivers → classes. +- **Layout:** `src/` core, `hw/{mcu,bsp}/` MCU+BSP, `examples/{device,host,dual}/`, `test/{unit-test,fuzz,hil}/`, `docs/`, `tools/`. +- **Commits/PRs:** imperative mood, scoped changes, link issues, include test/build evidence. +- **Formatting/lint:** `clang-format` (`.clang-format`), `codespell` (`.codespellrc`), run `pre-commit run --all-files` before submitting. -Choose ONE of these approaches: -**Option 1: Individual Example with CMake and Ninja (RECOMMENDED)** +## Bootstrap ```bash -cd examples/device/cdc_msc -mkdir -p build && cd build -cmake -DBOARD=raspberry_pi_pico -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel .. -cmake --build . +sudo apt-get install -y gcc-arm-none-eabi # ARM toolchain (2-5 min, one-time) +python3 tools/get_deps.py [FAMILY|-b BOARD] # fetch deps into lib/, hw/mcu/ (<1 s) +. $HOME/code/esp-idf/export.sh # Espressif only: before any build/flash/monitor ``` --- takes 1-2 seconds. NEVER CANCEL. Set timeout to 5+ minutes. - -**Option 2: All Examples for a Board** - -different folder than Option 1 +## Build +Single example (CMake+Ninja, recommended, 1-3 s): ```bash -cd examples/ -mkdir -p build && cd build +cd examples/device/cdc_msc && mkdir -p build && cd build cmake -DBOARD=raspberry_pi_pico -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel .. cmake --build . ``` --- takes 15-20 seconds, may have some objcopy failures that are non-critical. NEVER CANCEL. Set timeout to 30+ minutes. - -**Option 3: Individual Example with Make** - +All examples for a board (15-20 s; some objcopy failures are non-critical). Use `cmake-build-<board>` as the build dir — HIL tests expect that exact name: ```bash -cd examples/device/cdc_msc -make BOARD=raspberry_pi_pico all +cd examples +cmake -B cmake-build-raspberry_pi_pico -DBOARD=raspberry_pi_pico -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel . +cmake --build cmake-build-raspberry_pi_pico ``` --- takes 2-3 seconds. NEVER CANCEL. Set timeout to 5+ minutes. - -**Option 4: Espressif Example with ESP-IDF** - -Only ESP-IDF-enabled examples are supported for Espressif boards. Use FreeRTOS examples such as `examples/device/cdc_msc_freertos` -that contain `idf_component_register()` support. +Single example with Make: +```bash +cd examples/device/cdc_msc && make BOARD=raspberry_pi_pico all +``` +Espressif (only ESP-IDF examples like `cdc_msc_freertos`): ```bash . $HOME/code/esp-idf/export.sh cd examples/device/cdc_msc_freertos idf.py -DBOARD=espressif_s3_devkitc build ``` -Use `-DBOARD=...` with any supported board under `hw/bsp/espressif/boards/`. NEVER CANCEL. Set timeout to 10+ minutes. - - -## Build Options +**Build options** (CMake `-D…` / Make `…=…`): +- Debug: `CMAKE_BUILD_TYPE=Debug` / `DEBUG=1` +- Logging: `LOG=2` (add `LOGGER=rtt` for RTT) +- Root hub port: `RHPORT_DEVICE=1` +- Speed: `RHPORT_DEVICE_SPEED=OPT_MODE_FULL_SPEED` -- **Debug build**: - - CMake: `-DCMAKE_BUILD_TYPE=Debug` - - Make: `DEBUG=1` -- **With logging**: - - CMake: `-DLOG=2` - - Make: `LOG=2` -- **With RTT logger**: - - CMake: `-DLOG=2 -DLOGGER=rtt` - - Make: `LOG=2 LOGGER=rtt` -- **RootHub port selection**: - - CMake: `-DRHPORT_DEVICE=1` - - Make: `RHPORT_DEVICE=1` -- **Port speed**: - - CMake: `-DRHPORT_DEVICE_SPEED=OPT_MODE_FULL_SPEED` - - Make: `RHPORT_DEVICE_SPEED=OPT_MODE_FULL_SPEED` +## Flash -## Flashing and Deployment - -- **Flash with JLink**: - - CMake: `ninja cdc_msc-jlink` - - Make: `make BOARD=raspberry_pi_pico flash-jlink` -- **Flash with OpenOCD**: - - CMake: `ninja cdc_msc-openocd` - - Make: `make BOARD=raspberry_pi_pico flash-openocd` -- **Generate UF2**: - - CMake: `ninja cdc_msc-uf2` - - Make: `make BOARD=raspberry_pi_pico all uf2` -- **List all targets** (CMake/Ninja): `ninja -t targets` -- **Espressif flash**: - - Run `. $HOME/code/esp-idf/export.sh` - - `cd examples/device/cdc_msc_freertos` - - `idf.py -DBOARD=espressif_s3_devkitc flash` -- **Espressif serial monitor / chip log output**: - - Run `. $HOME/code/esp-idf/export.sh` - - `cd examples/device/cdc_msc_freertos` - - `idf.py -DBOARD=espressif_s3_devkitc monitor` +```bash +# JLink +ninja cdc_msc-jlink # CMake +make BOARD=<board> flash-jlink # Make -## GDB Debugging +# OpenOCD +ninja cdc_msc-openocd # CMake +make BOARD=<board> flash-openocd # Make -Look up the board's `JLINK_DEVICE` and `OPENOCD_OPTION` from `hw/bsp/*/boards/*/board.cmake` (or `board.mk`). +# UF2 +ninja cdc_msc-uf2 # CMake +make BOARD=<board> all uf2 # Make -### JLinkGDBServer +ninja -t targets # list CMake targets -**Terminal 1 – start the GDB server:** -```bash -JLinkGDBServer -device stm32h743xi -if SWD -speed 4000 \ - -port 2331 -swoport 2332 -telnetport 2333 -nogui +# Espressif (after . $HOME/code/esp-idf/export.sh) +idf.py -DBOARD=<board> flash +idf.py -DBOARD=<board> monitor ``` -**Terminal 2 – connect GDB:** -```bash -arm-none-eabi-gdb /tmp/build/firmware.elf -(gdb) target remote :2331 -(gdb) monitor reset halt -(gdb) load -(gdb) continue -``` +## GDB Debugging + +Look up `JLINK_DEVICE` / `OPENOCD_OPTION` in `hw/bsp/*/boards/*/board.cmake` (CMake builds) or `board.mk` (Make builds). -To break on entry instead of running immediately: +**JLink — Terminal 1:** ```bash -(gdb) monitor reset halt -(gdb) load -(gdb) break main -(gdb) continue +JLinkGDBServer -device stm32h743xi -if SWD -speed 4000 -port 2331 -swoport 2332 -telnetport 2333 -nogui ``` -### OpenOCD - -**Terminal 1 – start the GDB server:** +**OpenOCD — Terminal 1:** ```bash openocd -f interface/stlink.cfg -f target/stm32h7x.cfg -# or with J-Link probe: +# or with a J-Link interface: openocd -f interface/jlink.cfg -f target/stm32h7x.cfg -``` - -For **rp2040/rp2350** with a CMSIS-DAP probe (e.g. Picoprobe, debugprobe): -```bash +# rp2040/rp2350 via CMSIS-DAP: openocd -f interface/cmsis-dap.cfg -f target/rp2040.cfg -c "adapter speed 5000" -# or for rp2350: -openocd -f interface/cmsis-dap.cfg -f target/rp2350.cfg -c "adapter speed 5000" ``` -For boards that define `OPENOCD_OPTION` in `board.cmake`, use those options directly: -```bash -openocd $(cat hw/bsp/FAMILY/boards/BOARD/board.cmake | grep OPENOCD_OPTION | ...) -``` - -**Terminal 2 – connect GDB (OpenOCD default port is 3333):** +**Terminal 2 — connect GDB** (replace `<port>` with `2331` for JLinkGDBServer or `3333` for OpenOCD): ```bash arm-none-eabi-gdb /tmp/build/firmware.elf -(gdb) target remote :3333 +(gdb) target remote :<port> (gdb) monitor reset halt (gdb) load +(gdb) break main # optional, to stop at entry (gdb) continue ``` -### RTT Logging with JLinkGDBServer - -- Build with RTT logging enabled (example): - `cd examples/device/cdc_msc && make BOARD=stm32h743eval LOG=2 LOGGER=rtt all` -- Flash with J-Link: - `cd examples/device/cdc_msc && make BOARD=stm32h743eval LOG=2 LOGGER=rtt flash-jlink` -- Launch GDB server with RTT port (keep this running in terminal 1): - `JLinkGDBServer -device stm32h743xi -if SWD -speed 4000 -port 2331 -swoport 2332 -telnetport 2333 -RTTTelnetPort 19021 -nogui` -- Read RTT output (terminal 2): - `JLinkRTTClient` -- Capture RTT to file (optional): - `JLinkRTTClient | tee rtt.log` -- For non-interactive capture: - `timeout 20s JLinkRTTClient > rtt.log` - -## Unit Testing - -- Install Ceedling: `sudo gem install ceedling` -- Run all unit tests: `cd test/unit-test && ceedling` or `cd test/unit-test && ceedling test:all` -- takes 4 seconds. - NEVER CANCEL. Set timeout to 10+ minutes. -- Run specific test: `cd test/unit-test && ceedling test:test_fifo` -- Tests use Unity framework with CMock for mocking - -## Hardware-in-the-Loop (HIL) Testing - -- `-B examples` means `examples` is the parent folder that contains multi-board build outputs such as `examples/cmake-build-BOARD_NAME/...` -- Select config file before running HIL tests: - - if GitHub Actions self-hosted runner service is running, use `tinyusb.json` - - otherwise use `local.json` - - example: - `HIL_CONFIG=$( (systemctl list-units --type=service --state=running 2>/dev/null; systemctl --user list-units --type=service --state=running 2>/dev/null) | grep -q 'actions\.runner' && echo tinyusb.json || echo local.json )` -- Run tests on actual hardware, one of following ways: - - test a specific board `python test/hil/hil_test.py -b BOARD_NAME -B examples $HIL_CONFIG` - - test all boards in config `python test/hil/hil_test.py -B examples $HIL_CONFIG` -- In case of error, enabled verbose mode with `-v` flag for detailed logs. Also try to observe script output, and try to - modify hil_test.py (temporarily) to add more debug prints to pinpoint the issue. -- Requires pre-built (all) examples for target boards (see Build Examples section 2) - -take 2-5 minutes. NEVER CANCEL. Set timeout to 20+ minutes. - -## Documentation - -- Install requirements: `pip install -r docs/requirements.txt` -- Build docs: `cd docs && sphinx-build -b html . _build` -- takes 2-3 seconds. NEVER CANCEL. Set timeout to 10+ minutes. +**RTT logging:** build with `LOG=2 LOGGER=rtt`, flash, then run JLinkGDBServer with `-RTTTelnetPort 19021`, and in another terminal `JLinkRTTClient` (pipe to `tee rtt.log` or use `timeout 20s JLinkRTTClient > rtt.log` for non-interactive capture). -## Code Size Metrics - -Generate and compare code size metrics to evaluate the impact of changes. This is the most common workflow -when making code changes — use it to verify size impact before committing. - -**Quick single-board metrics (preferred for iterative development):** +## Testing +**Unit (Ceedling, Unity+CMock, ~4 s):** ```bash -rm -rf cmake-build -python3 tools/build.py -b raspberry_pi_pico --target all --target tinyusb_metrics -python3 tools/metrics.py combine -j -m -f tinyusb/src cmake-build/cmake-build-*/metrics.json +sudo gem install ceedling +cd test/unit-test && ceedling test:all # or ceedling test:test_fifo ``` -This builds all examples for one board and produces `metrics.json` + `metrics.md`. Takes ~30 seconds. -NEVER CANCEL. Set timeout to 10+ minutes. - -**Comparing with master (before/after workflow):** +**HIL (2-5 min):** invoke the `hil` skill (`.claude/skills/hil/SKILL.md`) for the full procedure (local vs remote mode, config selection, SSH copy steps, debugging tips). Requires pre-built examples — see Build → "All examples for a board". -1. On master: build and save baseline - ```bash - rm -rf cmake-build - python3 tools/build.py -b raspberry_pi_pico --target all --target tinyusb_metrics - python3 tools/metrics.py combine -j -m -f tinyusb/src cmake-build/cmake-build-*/metrics.json - mv metrics.json metrics_master.json - ``` -2. Switch to your branch: rebuild - ```bash - rm -rf cmake-build - python3 tools/build.py -b raspberry_pi_pico --target all --target tinyusb_metrics - python3 tools/metrics.py combine -j -m -f tinyusb/src cmake-build/cmake-build-*/metrics.json - ``` -3. Compare: `python3 tools/metrics.py compare -m -f tinyusb/src metrics_master.json metrics.json` - Produces `metrics_compare.md` showing size differences. - -**Full CI metrics (all arm-gcc families, for thorough validation):** +## Documentation ```bash -rm -rf cmake-build -FAMILIES=$(python3 .github/workflows/ci_set_matrix.py | python3 -c "import sys,json; d=json.load(sys.stdin); print(' '.join(d.get('arm-gcc',[])))") -python3 tools/build.py --one-first --target all --target tinyusb_metrics $FAMILIES -python3 tools/metrics.py combine -j -m -f tinyusb/src cmake-build/cmake-build-*/metrics.json +pip install -r docs/requirements.txt +cd docs && sphinx-build -b html . _build # ~2.5 s ``` -Builds the first board of each family. Takes 2-4 minutes. NEVER CANCEL. Set timeout to 10+ minutes. - -## Code Quality and Validation +## Code Size Metrics -- Format code: `clang-format -i path/to/file.c` (uses `.clang-format` config) -- Check spelling: `pip install codespell && codespell` (uses `.codespellrc` config) -- Pre-commit hooks validate unit tests and code quality automatically +Verify size impact before committing. Invoke the `code-size` skill (`.claude/skills/code-size/SKILL.md`) — it wraps `tools/metrics_compare_base.py` to handle the base-vs-branch worktree + build + compare flow. -## Static Analysis with PVS-Studio +Quick reference: +```bash +# Single example, one board: +python3 tools/metrics_compare_base.py -b raspberry_pi_pico -e device/cdc_msc +# Add --bloaty for section/symbol breakdown. -- **Analyze whole project**: - ```bash - pvs-studio-analyzer analyze -f examples/cmake-build-raspberry_pi_pico/compile_commands.json -R .PVS-Studio/.pvsconfig -o pvs-report.log -j12 --dump-files --misra-cpp-version 2008 --misra-c-version 2023 --use-old-parser - ``` -- **Analyze specific source files**: - ```bash - pvs-studio-analyzer analyze -f examples/cmake-build-raspberry_pi_pico/compile_commands.json -R .PVS-Studio/.pvsconfig -S path/to/file.c -o pvs-report.log -j12 --dump-files --misra-cpp-version 2008 --misra-c-version 2023 --use-old-parser - ``` -- **Multiple specific files**: - ```bash - pvs-studio-analyzer analyze -f examples/cmake-build-raspberry_pi_pico/compile_commands.json -R .PVS-Studio/.pvsconfig -S src/file1.c -S src/file2.c -o pvs-report.log -j12 --dump-files --misra-cpp-version 2008 --misra-c-version 2023 --use-old-parser - ``` -- Requires `compile_commands.json` in the build directory (generated by CMake with `-DCMAKE_EXPORT_COMPILE_COMMANDS=ON`) -- Use `-f` option to specify path to `compile_commands.json` -- Use `-R .PVS-Studio/.pvsconfig` to specify rule configuration file -- Use `-j12` for parallel analysis with 12 threads -- `--dump-files` saves preprocessed files for debugging -- `--misra-c-version 2023` enables MISRA C:2023 checks -- `--misra-cpp-version 2008` enables MISRA C++:2008 checks -- `--use-old-parser` uses legacy parser for compatibility -- Analysis takes ~10-30 seconds depending on project size. Set timeout to 5+ minutes. -- View results: `plog-converter -a GA:1,2 -t errorfile pvs-report.log` or open in PVS-Studio GUI +# All examples, one board: +python3 tools/metrics_compare_base.py -b raspberry_pi_pico -## Validation Checklist +# All arm-gcc CI families combined (pre-merge sweep, 4-8 min): +python3 tools/metrics_compare_base.py --ci +``` -### ALWAYS Run These After Making Changes +Reports land in `cmake-metrics/<board>/metrics_compare.md` (per-board) and `cmake-metrics/_combined/metrics_compare.md` (with `--combined`/`--ci`). -1. **Pre-commit validation** (RECOMMENDED): `pre-commit run --all-files` - - Install pre-commit: `pip install pre-commit && pre-commit install` - - Runs all quality checks, unit tests, spell checking, and formatting - - Takes 10-15 seconds. NEVER CANCEL. Set timeout to 15+ minutes. -2. **Build validation**: Build at least one board with all example that exercises your changes, see Build Examples - section (option 2) -3. Run unit tests relevant to touched modules; add fuzz/HIL coverage when modifying parsers or protocol state machines. +## Static Analysis (PVS-Studio) -### Manual Testing Scenarios -- **Device examples**: Cannot be fully tested without real hardware, but must build successfully -- **Unit tests**: Exercise core stack functionality - ALL tests must pass -- **Build system**: Must be able to build examples for multiple board families +Requires `compile_commands.json` (CMake `-DCMAKE_EXPORT_COMPILE_COMMANDS=ON`). -### Board Selection for Testing -- **STM32F4**: `stm32f407disco` - no external SDK required, good for testing -- **RP2040**: `raspberry_pi_pico` - requires Pico SDK, commonly used -- **Other families**: Check `hw/bsp/FAMILY/boards/` for available boards +```bash +# Whole project: +pvs-studio-analyzer analyze \ + -f examples/cmake-build-raspberry_pi_pico/compile_commands.json \ + -R .PVS-Studio/.pvsconfig \ + -o pvs-report.log -j12 --dump-files \ + --misra-c-version 2023 --misra-cpp-version 2008 --use-old-parser -## Release Instructions +# Specific files (add one or more `-S <file>`): +pvs-studio-analyzer analyze \ + -f examples/cmake-build-raspberry_pi_pico/compile_commands.json \ + -R .PVS-Studio/.pvsconfig \ + -S src/foo.c -S src/bar.c \ + -o pvs-report.log -j12 --dump-files \ + --misra-c-version 2023 --misra-cpp-version 2008 --use-old-parser -**DO NOT commit files automatically - only modify files and let the maintainer review before committing.** +plog-converter -a GA:1,2 -t errorfile pvs-report.log # view results +``` -1. Bump the release version variable at the top of `tools/make_release.py`. -2. Execute `python3 tools/make_release.py` to refresh: - - `src/tusb_option.h` (version defines) - - `repository.yml` (version mapping) - - `library.json` (PlatformIO version) - - `sonar-project.properties` (SonarQube version) - - `docs/reference/boards.rst` (generated board documentation) - - `hw/bsp/BoardPresets.json` (CMake presets) -3. Generate release notes for `docs/info/changelog.rst`: - - Get commit list: `git log <last-release-tag>..HEAD --oneline` - - **Visit GitHub PRs** for merged pull requests to understand context and gather details - - Use GitHub tools to search/read PRs: `github-mcp-server-list_pull_requests`, `github-mcp-server-pull_request_read` - - Extract key changes, API modifications, bug fixes, and new features from PR descriptions - - Add new changelog entry following the existing format: - - Version heading with equals underline (e.g., `0.20.0` followed by `======`) - - Release date in italics (e.g., `*November 19, 2024*`) - - Major sections: General, API Changes, Controller Driver (DCD & HCD), Device Stack, Host Stack, Testing - - Use bullet lists with descriptive categorization - - Reference function names, config macros, and file paths using RST inline code (double backticks) - - Include meaningful descriptions, not just commit messages -4. **Validation before commit**: - - Run unit tests: `cd test/unit-test && ceedling test:all` - - Build at least one example: `cd examples/device/cdc_msc && make BOARD=stm32f407disco all` - - Verify changed files look correct: `git diff --stat` -5. **Leave files unstaged** for maintainer to review, modify if needed, and commit with message: `Bump version to X.Y.Z` -6. **After maintainer commits**: Create annotated tag with `git tag -a vX.Y.Z -m "Release X.Y.Z"` -7. Push commit and tag: `git push origin <branch> && git push origin vX.Y.Z` -8. Create GitHub release from the tag with changelog content +Takes ~10-30 s. -## Repository Structure Quick Reference -``` -├── src/ # Core TinyUSB stack -│ ├── class/ # USB device classes (CDC, HID, MSC, Audio, etc.) -│ ├── portable/ # MCU-specific drivers (organized by vendor) -│ ├── device/ # USB device stack core -│ ├── host/ # USB host stack core -│ └── common/ # Shared utilities (FIFO, etc.) -├── examples/ # Example applications -│ ├── device/ # Device examples (cdc_msc, hid_generic, etc.) -│ ├── host/ # Host examples -│ └── dual/ # Dual-role examples -├── hw/bsp/ # Board Support Packages -│ └── FAMILY/boards/ # Board-specific configurations -├── test/unit-test/ # Unit tests using Ceedling -├── tools/ # Build and utility scripts -└── docs/ # Sphinx documentation -``` +## Validation After Changes -#### Build Time Reference -- **Dependency fetch**: <1 second -- **Single example build**: 1-3 seconds -- **Unit tests**: ~4 seconds -- **Documentation build**: ~2.5 seconds -- **Full board examples**: 15-20 seconds -- **Toolchain installation**: 2-5 minutes (one-time) +1. `pre-commit run --all-files` — format, spell, unit tests (10-15 s). +2. Build at least one board's full example set (Build → "All examples for a board") for modules you touched. +3. Run relevant unit tests; add fuzz/HIL coverage for parsers or protocol state machines. -#### Key Files to Know -- `tools/get_deps.py`: Manages dependencies for MCU families -- `tools/build.py`: Builds multiple examples, supports make/cmake -- `src/tusb.h`: Main TinyUSB header file -- `src/tusb_config.h`: Configuration template -- `examples/device/cdc_msc/`: Most commonly used example for testing -- `test/unit-test/project.yml`: Ceedling test configuration +**Boards good for local testing:** +- `stm32f407disco` — no external SDK +- `raspberry_pi_pico` — Pico SDK required +- Others: see `hw/bsp/FAMILY/boards/` -#### MCU Reference Manuals and Datasheets -- Look in `$HOME/Documents/Calibre Library` for all MCU reference manuals, datasheets and board schematics. +Device examples need real hardware to validate runtime behavior; must at least build. -#### Debugging Build Issues -- **Missing compiler**: Install `gcc-arm-none-eabi` package -- **Missing dependencies**: Run `python3 tools/get_deps.py FAMILY` -- **Board not found**: Check `hw/bsp/FAMILY/boards/` for valid board names -- **objcopy errors**: Often non-critical in full builds, try individual example builds +## Release -#### Working with USB Device Classes -- **CDC (Serial)**: `src/class/cdc/` - Virtual serial port -- **HID**: `src/class/hid/` - Human Interface Device (keyboard, mouse, etc.) -- **MSC**: `src/class/msc/` - Mass Storage Class (USB drive) -- **Audio**: `src/class/audio/` - USB Audio Class -- Each class has device (`*_device.c`) and host (`*_host.c`) implementations +**Do not commit automatically — leave changes for maintainer review.** -#### MCU Family Support -- **STM32**: Largest support (F0, F1, F2, F3, F4, F7, G0, G4, H7, L4, U5, etc.) -- **Raspberry Pi**: RP2040, RP2350 with PIO-USB host support -- **NXP**: iMXRT, Kinetis, LPC families -- **Microchip**: SAM D/E/G/L families -- Check `hw/bsp/` for complete list and `docs/reference/boards.rst` for details +1. Bump version at top of `tools/make_release.py`. +2. Run `python3 tools/make_release.py` to refresh: `src/tusb_option.h`, `repository.yml`, `library.json`, `sonar-project.properties`, `docs/reference/boards.rst`, `hw/bsp/BoardPresets.json`. +3. Changelog `docs/info/changelog.rst`: + - `git log <last-tag>..HEAD --oneline` for commit list. + - Read merged PRs for context (`gh pr view`, or github MCP tools). + - Follow existing format: version + `======` underline, italic date, sections (General, API Changes, DCD & HCD, Device Stack, Host Stack, Testing), RST inline code for symbols. +4. Validate: `ceedling test:all`, build `cdc_msc` for `stm32f407disco`, review `git diff --stat`. +5. Leave unstaged. Maintainer commits `Bump version to X.Y.Z`, then: `git tag -a vX.Y.Z -m "Release X.Y.Z" && git push origin <branch> vX.Y.Z`. Create GitHub release from tag. -### Code Style Guidelines +## References -#### General Coding Standards -- Use C99 standard -- Memory-safe: no dynamic allocation -- Thread-safe: defer all interrupt events to non-ISR task functions -- 2-space indentation, no tabs -- Use snake_case for variables/functions -- Use UPPER_CASE for macros and constants -- Follow existing variable naming patterns in files you're modifying -- Include proper header comments with MIT license -- Add descriptive comments for non-obvious functions +- MCU reference manuals, datasheets, schematics: `$HOME/Documents/Calibre Library`. +- Supported MCUs/boards: `hw/bsp/` and `docs/reference/boards.rst`. +- USB classes: `src/class/{cdc,hid,msc,audio,…}/` — each has `*_device.c` and `*_host.c`. +- Key files: `src/tusb.h`, `src/tusb_config.h`, `tools/get_deps.py`, `tools/build.py`, `test/unit-test/project.yml`. -#### Best Practices -- When including headers, group in order: C stdlib, tusb common, drivers, classes -- Always check return values from functions that can fail -- Use TU_ASSERT() for error checking with return statements -- Follow the existing code patterns in the files you're modifying +## Common Build Issues -Remember: TinyUSB is designed for embedded systems - builds are fast, tests are focused, and the codebase is optimized for resource-constrained environments. +- Missing compiler → install `gcc-arm-none-eabi`. +- Missing deps → `python3 tools/get_deps.py FAMILY`. +- Unknown board → check `hw/bsp/FAMILY/boards/`. +- `objcopy` errors in full builds are often non-critical; retry the single example. diff --git a/examples/device/CMakeLists.txt b/examples/device/CMakeLists.txt index 7173f455e..088872711 100644 --- a/examples/device/CMakeLists.txt +++ b/examples/device/CMakeLists.txt @@ -16,6 +16,7 @@ set(EXAMPLE_LIST cdc_dual_ports cdc_msc cdc_msc_freertos + cdc_msc_throughput cdc_uac2 dfu dfu_runtime diff --git a/examples/device/audio_4_channel_mic/src/usb_descriptors.c b/examples/device/audio_4_channel_mic/src/usb_descriptors.c index 00337eee7..2380ea0ae 100644 --- a/examples/device/audio_4_channel_mic/src/usb_descriptors.c +++ b/examples/device/audio_4_channel_mic/src/usb_descriptors.c @@ -91,6 +91,10 @@ enum // nRF5x ISO can only be endpoint 8 #define EPNUM_AUDIO 0x08 +#elif TU_CHECK_MCU(OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX32690, OPT_MCU_MAX78002) + // Put audio iso on EP>=8 so the 2048/4096-byte FIFOs can back double packet buffering + #define EPNUM_AUDIO 0x0A + #else #define EPNUM_AUDIO 0x01 #endif diff --git a/examples/device/audio_4_channel_mic_freertos/src/usb_descriptors.c b/examples/device/audio_4_channel_mic_freertos/src/usb_descriptors.c index 3bb93f67d..216cd062a 100644 --- a/examples/device/audio_4_channel_mic_freertos/src/usb_descriptors.c +++ b/examples/device/audio_4_channel_mic_freertos/src/usb_descriptors.c @@ -91,6 +91,10 @@ enum // nRF5x ISO can only be endpoint 8 #define EPNUM_AUDIO 0x08 +#elif TU_CHECK_MCU(OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX32690, OPT_MCU_MAX78002) + // Put audio iso on EP>=8 so the 2048/4096-byte FIFOs can back double packet buffering + #define EPNUM_AUDIO 0x0A + #else #define EPNUM_AUDIO 0x01 #endif diff --git a/examples/device/audio_test/src/usb_descriptors.c b/examples/device/audio_test/src/usb_descriptors.c index ad161939e..37ebf84d3 100644 --- a/examples/device/audio_test/src/usb_descriptors.c +++ b/examples/device/audio_test/src/usb_descriptors.c @@ -91,6 +91,10 @@ enum // nRF5x ISO can only be endpoint 8 #define EPNUM_AUDIO 0x08 +#elif TU_CHECK_MCU(OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX32690, OPT_MCU_MAX78002) + // Put audio iso on EP>=8 so the 2048/4096-byte FIFOs can back double packet buffering + #define EPNUM_AUDIO 0x0A + #else #define EPNUM_AUDIO 0x01 #endif diff --git a/examples/device/audio_test_freertos/src/usb_descriptors.c b/examples/device/audio_test_freertos/src/usb_descriptors.c index ad161939e..37ebf84d3 100644 --- a/examples/device/audio_test_freertos/src/usb_descriptors.c +++ b/examples/device/audio_test_freertos/src/usb_descriptors.c @@ -91,6 +91,10 @@ enum // nRF5x ISO can only be endpoint 8 #define EPNUM_AUDIO 0x08 +#elif TU_CHECK_MCU(OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX32690, OPT_MCU_MAX78002) + // Put audio iso on EP>=8 so the 2048/4096-byte FIFOs can back double packet buffering + #define EPNUM_AUDIO 0x0A + #else #define EPNUM_AUDIO 0x01 #endif diff --git a/examples/device/audio_test_multi_rate/src/usb_descriptors.c b/examples/device/audio_test_multi_rate/src/usb_descriptors.c index 505936fdb..31333dcd3 100644 --- a/examples/device/audio_test_multi_rate/src/usb_descriptors.c +++ b/examples/device/audio_test_multi_rate/src/usb_descriptors.c @@ -88,6 +88,10 @@ enum { // nRF5x ISO can only be endpoint 8 #define EPNUM_AUDIO 0x08 +#elif TU_CHECK_MCU(OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX32690, OPT_MCU_MAX78002) + // Put audio iso on EP>=8 so the 2048/4096-byte FIFOs can back double packet buffering + #define EPNUM_AUDIO 0x0A + #else #define EPNUM_AUDIO 0x01 #endif diff --git a/examples/device/cdc_dual_ports/src/usb_descriptors.c b/examples/device/cdc_dual_ports/src/usb_descriptors.c index e6011c35a..adfd8cf9d 100644 --- a/examples/device/cdc_dual_ports/src/usb_descriptors.c +++ b/examples/device/cdc_dual_ports/src/usb_descriptors.c @@ -106,16 +106,27 @@ enum { #define EPNUM_CDC_1_OUT 0x05 #define EPNUM_CDC_1_IN 0x84 -#elif defined(TUD_ENDPOINT_ONE_DIRECTION_ONLY) +#elif CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY // MCUs that don't support a same endpoint number with different direction IN and OUT defined in tusb_mcu.h // e.g EP1 OUT & EP1 IN cannot exist together - #define EPNUM_CDC_0_NOTIF 0x81 - #define EPNUM_CDC_0_OUT 0x02 - #define EPNUM_CDC_0_IN 0x83 + #if TU_CHECK_MCU(OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX32690, OPT_MCU_MAX78002) + // Put bulk on EP>=8 so the 2048/4096-byte FIFOs can back double packet buffering + #define EPNUM_CDC_0_NOTIF 0x81 + #define EPNUM_CDC_0_OUT 0x08 + #define EPNUM_CDC_0_IN 0x89 - #define EPNUM_CDC_1_NOTIF 0x84 - #define EPNUM_CDC_1_OUT 0x05 - #define EPNUM_CDC_1_IN 0x86 + #define EPNUM_CDC_1_NOTIF 0x82 + #define EPNUM_CDC_1_OUT 0x0A + #define EPNUM_CDC_1_IN 0x8B + #else + #define EPNUM_CDC_0_NOTIF 0x81 + #define EPNUM_CDC_0_OUT 0x02 + #define EPNUM_CDC_0_IN 0x83 + + #define EPNUM_CDC_1_NOTIF 0x84 + #define EPNUM_CDC_1_OUT 0x05 + #define EPNUM_CDC_1_IN 0x86 + #endif #else #define EPNUM_CDC_0_NOTIF 0x81 diff --git a/examples/device/cdc_msc/src/usb_descriptors.c b/examples/device/cdc_msc/src/usb_descriptors.c index c668ea3a7..5dc80dee3 100644 --- a/examples/device/cdc_msc/src/usb_descriptors.c +++ b/examples/device/cdc_msc/src/usb_descriptors.c @@ -102,15 +102,25 @@ enum { #define EPNUM_MSC_OUT 0x05 #define EPNUM_MSC_IN 0x84 -#elif defined(TUD_ENDPOINT_ONE_DIRECTION_ONLY) +#elif CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY // MCUs that don't support a same endpoint number with different direction IN and OUT defined in tusb_mcu.h // e.g EP1 OUT & EP1 IN cannot exist together - #define EPNUM_CDC_NOTIF 0x81 - #define EPNUM_CDC_OUT 0x02 - #define EPNUM_CDC_IN 0x83 + #if TU_CHECK_MCU(OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX32690, OPT_MCU_MAX78002) + // Put bulk on EP>=8 so the 2048/4096-byte FIFOs can back double packet buffering + #define EPNUM_CDC_NOTIF 0x81 + #define EPNUM_CDC_OUT 0x08 + #define EPNUM_CDC_IN 0x89 - #define EPNUM_MSC_OUT 0x04 - #define EPNUM_MSC_IN 0x85 + #define EPNUM_MSC_OUT 0x0A + #define EPNUM_MSC_IN 0x8B + #else + #define EPNUM_CDC_NOTIF 0x81 + #define EPNUM_CDC_OUT 0x02 + #define EPNUM_CDC_IN 0x83 + + #define EPNUM_MSC_OUT 0x04 + #define EPNUM_MSC_IN 0x85 + #endif #else #define EPNUM_CDC_NOTIF 0x81 diff --git a/examples/device/cdc_msc_freertos/src/main.c b/examples/device/cdc_msc_freertos/src/main.c index 4fb209fd0..f2f71d089 100644 --- a/examples/device/cdc_msc_freertos/src/main.c +++ b/examples/device/cdc_msc_freertos/src/main.c @@ -34,10 +34,10 @@ #define USBD_STACK_SIZE 4096 #else // Increase stack size when debug log is enabled - #define USBD_STACK_SIZE (3*configMINIMAL_STACK_SIZE/2) * (CFG_TUSB_DEBUG ? 2 : 1) + #define USBD_STACK_SIZE (configMINIMAL_STACK_SIZE * (CFG_TUSB_DEBUG ? 4 : 2)) #endif -#define CDC_STACK_SIZE (configMINIMAL_STACK_SIZE * (CFG_TUSB_DEBUG ? 2 : 1)) +#define CDC_STACK_SIZE (configMINIMAL_STACK_SIZE * (CFG_TUSB_DEBUG ? 3 : 2)) #define BLINKY_STACK_SIZE configMINIMAL_STACK_SIZE //--------------------------------------------------------------------+ diff --git a/examples/device/cdc_msc_freertos/src/usb_descriptors.c b/examples/device/cdc_msc_freertos/src/usb_descriptors.c index 4950f02e0..f5b015051 100644 --- a/examples/device/cdc_msc_freertos/src/usb_descriptors.c +++ b/examples/device/cdc_msc_freertos/src/usb_descriptors.c @@ -102,15 +102,25 @@ enum { #define EPNUM_MSC_OUT 0x05 #define EPNUM_MSC_IN 0x84 -#elif defined(TUD_ENDPOINT_ONE_DIRECTION_ONLY) +#elif CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY // MCUs that don't support a same endpoint number with different direction IN and OUT defined in tusb_mcu.h // e.g EP1 OUT & EP1 IN cannot exist together - #define EPNUM_CDC_NOTIF 0x81 - #define EPNUM_CDC_OUT 0x02 - #define EPNUM_CDC_IN 0x83 + #if TU_CHECK_MCU(OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX32690, OPT_MCU_MAX78002) + // Put bulk on EP>=8 so the 2048/4096-byte FIFOs can back double packet buffering + #define EPNUM_CDC_NOTIF 0x81 + #define EPNUM_CDC_OUT 0x08 + #define EPNUM_CDC_IN 0x89 - #define EPNUM_MSC_OUT 0x04 - #define EPNUM_MSC_IN 0x85 + #define EPNUM_MSC_OUT 0x0A + #define EPNUM_MSC_IN 0x8B + #else + #define EPNUM_CDC_NOTIF 0x81 + #define EPNUM_CDC_OUT 0x02 + #define EPNUM_CDC_IN 0x83 + + #define EPNUM_MSC_OUT 0x04 + #define EPNUM_MSC_IN 0x85 + #endif #else #define EPNUM_CDC_NOTIF 0x81 diff --git a/examples/device/cdc_msc_throughput/CMakeLists.txt b/examples/device/cdc_msc_throughput/CMakeLists.txt new file mode 100644 index 000000000..69c1caa6a --- /dev/null +++ b/examples/device/cdc_msc_throughput/CMakeLists.txt @@ -0,0 +1,35 @@ +cmake_minimum_required(VERSION 3.20) + +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake) + +project(cdc_msc_throughput 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() + +if (RTOS STREQUAL zephyr) + set(EXE_NAME app) +else() + set(EXE_NAME ${PROJECT_NAME}) + add_executable(${EXE_NAME}) +endif() + +# Example source +target_sources(${EXE_NAME} PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/main.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/usb_descriptors.c + ) + +# Example include +target_include_directories(${EXE_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src + ) + +# Configure compilation flags and libraries for the example without RTOS. +# See the corresponding function in hw/bsp/FAMILY/family.cmake for details. +family_configure_device_example(${EXE_NAME} ${RTOS}) diff --git a/examples/device/cdc_msc_throughput/Makefile b/examples/device/cdc_msc_throughput/Makefile new file mode 100644 index 000000000..035e90308 --- /dev/null +++ b/examples/device/cdc_msc_throughput/Makefile @@ -0,0 +1,11 @@ +include ../../../hw/bsp/family_support.mk + +INC += \ + src \ + + +# Example source +EXAMPLE_SOURCE += $(wildcard src/*.c) +SRC_C += $(addprefix $(EXAMPLE_PATH)/, $(EXAMPLE_SOURCE)) + +include ../../../hw/bsp/family_rules.mk diff --git a/examples/device/cdc_msc_throughput/src/main.c b/examples/device/cdc_msc_throughput/src/main.c new file mode 100644 index 000000000..116cbe13f --- /dev/null +++ b/examples/device/cdc_msc_throughput/src/main.c @@ -0,0 +1,151 @@ +/* + * 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 "bsp/board_api.h" +#include "tusb.h" + +// cdc_msc_throughput: minimal CDC+MSC device aimed at measuring pure USB bulk throughput. +// MSC read/write callbacks don't touch any backing storage - write discards the +// data and read only zero-fills the low LBAs the host scans during enumeration +// (partition table, GPT header). Higher LBAs return whatever is already in the +// transfer buffer, so `dd` numbers reflect the USB/driver ceiling, not any +// simulated storage or per-byte memset cost. +// CDC path drains RX in tud_cdc_rx_cb and sources TX from a static filler in the +// main loop so `dd` can target /dev/ttyACMx in either direction. + +static void cdc_throughput_task(void); + +//--------------------------------------------------------------------+ +// Main +//--------------------------------------------------------------------+ +int main(void) { + board_init(); + + tusb_rhport_init_t dev_init = {.role = TUSB_ROLE_DEVICE, .speed = TUSB_SPEED_AUTO}; + tusb_init(BOARD_TUD_RHPORT, &dev_init); + + board_init_after_tusb(); + + while (1) { + tud_task(); + cdc_throughput_task(); + } +} + +//--------------------------------------------------------------------+ +// CDC callbacks + tasks +//--------------------------------------------------------------------+ +void tud_cdc_rx_cb(uint8_t itf) { + (void) itf; + tud_cdc_read_flush(); // Drain RX +} + +static void cdc_throughput_task(void) { + if (!tud_cdc_connected()) return; + + // Source TX: fill whatever write room is free. + static uint8_t const filler[CFG_TUD_CDC_TX_EPSIZE] = {0}; + uint32_t room = tud_cdc_write_available(); + while (room > 0) { + uint32_t n = tud_cdc_write(filler, tu_min32(room, sizeof(filler))); + if (n == 0) { + break; + } + room -= n; + } + tud_cdc_write_flush(); +} + +//--------------------------------------------------------------------+ +// MSC callbacks +//--------------------------------------------------------------------+ + +// 1 GiB logical capacity so `dd` can run long enough for stable numbers. +// No real backing store - block content is synthesised on read, discarded on write. +enum { + DISK_BLOCK_SIZE = 512, + DISK_BLOCK_COUNT = 0x00200000u, // 2 Mi blocks = 1 GiB + // Kernel probes partition-table / filesystem-superblock locations near the + // start of the disk during enumeration. Zero-fill only this head range so the + // block layer sees "no partition, no filesystem" and leaves us alone; higher + // LBAs skip the memset so `dd` measures pure USB/driver throughput. + DISK_ZEROFILL_LBA = 64, // 32 KiB +}; + +void tud_msc_inquiry_cb(uint8_t lun, uint8_t vendor_id[8], uint8_t product_id[16], uint8_t product_rev[4]) { + (void) lun; + const char vid[] = "TinyUSB"; + const char pid[] = "Mass Storage"; + const char rev[] = "1.0"; + (void) strncpy((char*) vendor_id, vid, 8); + (void) strncpy((char*) product_id, pid, 16); + (void) strncpy((char*) product_rev, rev, 4); +} + +bool tud_msc_test_unit_ready_cb(uint8_t lun) { + (void) lun; + return true; +} + +void tud_msc_capacity_cb(uint8_t lun, uint32_t *block_count, uint16_t *block_size) { + (void) lun; + *block_count = DISK_BLOCK_COUNT; + *block_size = DISK_BLOCK_SIZE; +} + +bool tud_msc_start_stop_cb(uint8_t lun, uint8_t power_condition, bool start, bool load_eject) { + (void) lun; (void) power_condition; (void) start; (void) load_eject; + return true; +} + +bool tud_msc_is_writable_cb(uint8_t lun) { + (void) lun; + return true; +} + +// READ10: zero-fill only the head range the kernel inspects, skip memset everywhere +// else so we measure the USB / driver path rather than memset cost. +int32_t tud_msc_read10_cb(uint8_t lun, uint32_t lba, uint32_t offset, void *buffer, uint32_t bufsize) { + (void) lun; (void) offset; + if (lba < DISK_ZEROFILL_LBA) { + memset(buffer, 0, bufsize); + } else { + (void) buffer; + } + return (int32_t) bufsize; +} + +// WRITE10: discard the received data entirely - this is the pure USB-speed test. +int32_t tud_msc_write10_cb(uint8_t lun, uint32_t lba, uint32_t offset, uint8_t *buffer, uint32_t bufsize) { + (void) lun; (void) lba; (void) offset; (void) buffer; + return (int32_t) bufsize; +} + +// Unknown SCSI commands: stall with Invalid Command sense. +int32_t tud_msc_scsi_cb(uint8_t lun, uint8_t const scsi_cmd[16], void *buffer, uint16_t bufsize) { + (void) scsi_cmd; (void) buffer; (void) bufsize; + tud_msc_set_sense(lun, SCSI_SENSE_ILLEGAL_REQUEST, 0x20, 0x00); + return -1; +} diff --git a/examples/device/cdc_msc_throughput/src/tusb_config.h b/examples/device/cdc_msc_throughput/src/tusb_config.h new file mode 100644 index 000000000..6c8655719 --- /dev/null +++ b/examples/device/cdc_msc_throughput/src/tusb_config.h @@ -0,0 +1,103 @@ +/* + * 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 + +//-------------------------------------------------------------------- +// Board Specific Configuration +//-------------------------------------------------------------------- + +#ifndef BOARD_TUD_RHPORT + #define BOARD_TUD_RHPORT 0 +#endif + +#ifndef BOARD_TUD_MAX_SPEED + #define BOARD_TUD_MAX_SPEED OPT_MODE_DEFAULT_SPEED +#endif + +//-------------------------------------------------------------------- +// Common Configuration +//-------------------------------------------------------------------- + +#ifndef CFG_TUSB_MCU + #error CFG_TUSB_MCU must be defined +#endif + +#ifndef CFG_TUSB_OS + #define CFG_TUSB_OS OPT_OS_NONE +#endif + +#ifndef CFG_TUSB_DEBUG + #define CFG_TUSB_DEBUG 0 +#endif + +// Enable Device stack +#define CFG_TUD_ENABLED 1 +#define CFG_TUD_MAX_SPEED BOARD_TUD_MAX_SPEED + +#ifndef CFG_TUSB_MEM_SECTION + #define CFG_TUSB_MEM_SECTION +#endif + +#ifndef CFG_TUSB_MEM_ALIGN + #define CFG_TUSB_MEM_ALIGN __attribute__ ((aligned(4))) +#endif + +//-------------------------------------------------------------------- +// DEVICE CONFIGURATION +//-------------------------------------------------------------------- + +#ifndef CFG_TUD_ENDPOINT0_SIZE + #define CFG_TUD_ENDPOINT0_SIZE 64 +#endif + +//------------- CLASS -------------// +#define CFG_TUD_CDC 1 +#define CFG_TUD_MSC 1 + +// Large MSC bulk buffer: host transfers big CBW payloads (e.g. dd bs=1M does 64KiB +// chunks). A 4K per-bulk-IO buffer lets the class driver amortise the per-CBW +// overhead across many USB packets, approximating the maximum USB bulk throughput. +#define CFG_TUD_MSC_EP_BUFSIZE (TUD_OPT_HIGH_SPEED ? 4096 : 1024) + +// #define CFG_TUD_CDC_TX_PERSISTENT 1 + +// CDC throughput: size for HS; tinyusb will auto-scale for FS via TUD_OPT_HIGH_SPEED. +#define CFG_TUD_CDC_RX_EPSIZE (TUD_OPT_HIGH_SPEED ? 2*512 : 2*64) +#define CFG_TUD_CDC_TX_EPSIZE CFG_TUD_CDC_RX_EPSIZE + +#define CFG_TUD_CDC_RX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 2*512 : 2*64) +#define CFG_TUD_CDC_TX_BUFSIZE CFG_TUD_CDC_RX_BUFSIZE + +#ifdef __cplusplus +} +#endif + +#endif /* TUSB_CONFIG_H_ */ diff --git a/examples/device/cdc_msc_throughput/src/usb_descriptors.c b/examples/device/cdc_msc_throughput/src/usb_descriptors.c new file mode 100644 index 000000000..3b0ff6e17 --- /dev/null +++ b/examples/device/cdc_msc_throughput/src/usb_descriptors.c @@ -0,0 +1,186 @@ +/* + * 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 "bsp/board_api.h" +#include "tusb.h" + +#define USB_PID (0x4000 | ((CFG_TUD_CDC) ? (1 << 0) : 0) | ((CFG_TUD_MSC) ? (1 << 1) : 0)) +#define USB_VID 0xCafe +#define USB_BCD 0x0200 + +static tusb_desc_device_t const desc_device = { + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = USB_BCD, + + // IAD required for composite CDC + MSC + .bDeviceClass = TUSB_CLASS_MISC, + .bDeviceSubClass = MISC_SUBCLASS_COMMON, + .bDeviceProtocol = MISC_PROTOCOL_IAD, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + + .idVendor = USB_VID, + .idProduct = USB_PID, + .bcdDevice = 0x0100, + + .iManufacturer = 0x01, + .iProduct = 0x02, + .iSerialNumber = 0x03, + + .bNumConfigurations = 0x01, +}; + +uint8_t const *tud_descriptor_device_cb(void) { + return (uint8_t const *) &desc_device; +} + +enum { + ITF_NUM_CDC = 0, + ITF_NUM_CDC_DATA, + ITF_NUM_MSC, + ITF_NUM_TOTAL, +}; + +// Place bulk endpoints on EP>=8 for MAX32690 class parts (bigger FIFO, DPB-capable). +#if CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY + #if TU_CHECK_MCU(OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX32690, OPT_MCU_MAX78002) + // Put bulk on EP>=8 so the 2048/4096-byte FIFOs can back double packet buffering + #define EPNUM_CDC_NOTIF 0x81 + #define EPNUM_CDC_OUT 0x08 + #define EPNUM_CDC_IN 0x89 + #define EPNUM_MSC_OUT 0x0A + #define EPNUM_MSC_IN 0x8B + #else + #define EPNUM_CDC_NOTIF 0x81 + #define EPNUM_CDC_OUT 0x02 + #define EPNUM_CDC_IN 0x83 + #define EPNUM_MSC_OUT 0x04 + #define EPNUM_MSC_IN 0x85 + #endif +#else + #define EPNUM_CDC_NOTIF 0x81 + #define EPNUM_CDC_OUT 0x02 + #define EPNUM_CDC_IN 0x82 + #define EPNUM_MSC_OUT 0x03 + #define EPNUM_MSC_IN 0x83 +#endif + +#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_CDC_DESC_LEN + TUD_MSC_DESC_LEN) + +static uint8_t const desc_fs_configuration[] = { + TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100), + TUD_CDC_DESCRIPTOR(ITF_NUM_CDC, 4, EPNUM_CDC_NOTIF, 16, EPNUM_CDC_OUT, EPNUM_CDC_IN, 64), + TUD_MSC_DESCRIPTOR(ITF_NUM_MSC, 5, EPNUM_MSC_OUT, EPNUM_MSC_IN, 64), +}; + +#if TUD_OPT_HIGH_SPEED +static uint8_t const desc_hs_configuration[] = { + TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100), + TUD_CDC_DESCRIPTOR(ITF_NUM_CDC, 4, EPNUM_CDC_NOTIF, 16, EPNUM_CDC_OUT, EPNUM_CDC_IN, 512), + TUD_MSC_DESCRIPTOR(ITF_NUM_MSC, 5, EPNUM_MSC_OUT, EPNUM_MSC_IN, 512), +}; + +static uint8_t desc_other_speed_config[CONFIG_TOTAL_LEN]; + +static tusb_desc_device_qualifier_t const desc_device_qualifier = { + .bLength = sizeof(tusb_desc_device_qualifier_t), + .bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER, + .bcdUSB = USB_BCD, + .bDeviceClass = 0x00, + .bDeviceSubClass = 0x00, + .bDeviceProtocol = 0x00, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .bNumConfigurations = 0x01, + .bReserved = 0x00, +}; + +uint8_t const *tud_descriptor_device_qualifier_cb(void) { + return (uint8_t const *) &desc_device_qualifier; +} + +uint8_t const *tud_descriptor_other_speed_configuration_cb(uint8_t index) { + (void) index; + memcpy(desc_other_speed_config, + (tud_speed_get() == TUSB_SPEED_HIGH) ? desc_fs_configuration : desc_hs_configuration, + CONFIG_TOTAL_LEN); + desc_other_speed_config[1] = TUSB_DESC_OTHER_SPEED_CONFIG; + return desc_other_speed_config; +} +#endif + +uint8_t const *tud_descriptor_configuration_cb(uint8_t index) { + (void) index; +#if TUD_OPT_HIGH_SPEED + return (tud_speed_get() == TUSB_SPEED_HIGH) ? desc_hs_configuration : desc_fs_configuration; +#else + return desc_fs_configuration; +#endif +} + +enum { + STRID_LANGID = 0, + STRID_MANUFACTURER, + STRID_PRODUCT, + STRID_SERIAL, +}; + +static char const *string_desc_arr[] = { + (const char[]) { 0x09, 0x04 }, + "TinyUSB", + "Throughput", + NULL, + "TinyUSB CDC", + "TinyUSB MSC", +}; + +static uint16_t _desc_str[32 + 1]; + +uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) { + (void) langid; + size_t chr_count; + + switch (index) { + case STRID_LANGID: + memcpy(&_desc_str[1], string_desc_arr[0], 2); + chr_count = 1; + break; + + case STRID_SERIAL: + chr_count = board_usb_get_serial(_desc_str + 1, 32); + break; + + default: + if (!(index < sizeof(string_desc_arr) / sizeof(string_desc_arr[0]))) return NULL; + const char *str = string_desc_arr[index]; + chr_count = strlen(str); + size_t const max_count = sizeof(_desc_str) / sizeof(_desc_str[0]) - 1; + if (chr_count > max_count) chr_count = max_count; + for (size_t i = 0; i < chr_count; i++) _desc_str[1 + i] = str[i]; + break; + } + + _desc_str[0] = (uint16_t) ((TUSB_DESC_STRING << 8) | (2 * chr_count + 2)); + return _desc_str; +} diff --git a/examples/device/cdc_uac2/src/usb_descriptors.c b/examples/device/cdc_uac2/src/usb_descriptors.c index e6caaa971..fdffc761e 100644 --- a/examples/device/cdc_uac2/src/usb_descriptors.c +++ b/examples/device/cdc_uac2/src/usb_descriptors.c @@ -97,15 +97,25 @@ uint8_t const * tud_descriptor_device_cb(void) #define EPNUM_CDC_OUT 0x02 #define EPNUM_CDC_IN 0x82 -#elif defined(TUD_ENDPOINT_ONE_DIRECTION_ONLY) +#elif CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY // MCUs that don't support a same endpoint number with different direction IN and OUT defined in tusb_mcu.h // e.g EP1 OUT & EP1 IN cannot exist together - #define EPNUM_AUDIO_IN 0x01 - #define EPNUM_AUDIO_OUT 0x02 + #if TU_CHECK_MCU(OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX32690, OPT_MCU_MAX78002) + // Put CDC bulk on EP>=8 and audio iso on EP10/11 so the 2048/4096-byte FIFOs can back double packet buffering + #define EPNUM_AUDIO_OUT 0x0A + #define EPNUM_AUDIO_IN 0x0B - #define EPNUM_CDC_NOTIF 0x83 - #define EPNUM_CDC_OUT 0x04 - #define EPNUM_CDC_IN 0x85 + #define EPNUM_CDC_NOTIF 0x83 + #define EPNUM_CDC_OUT 0x08 + #define EPNUM_CDC_IN 0x89 + #else + #define EPNUM_AUDIO_IN 0x01 + #define EPNUM_AUDIO_OUT 0x02 + + #define EPNUM_CDC_NOTIF 0x83 + #define EPNUM_CDC_OUT 0x04 + #define EPNUM_CDC_IN 0x85 + #endif #else #define EPNUM_AUDIO_IN 0x01 diff --git a/examples/device/dfu/skip.txt b/examples/device/dfu/skip.txt index 79d3da9d2..ccff857ac 100644 --- a/examples/device/dfu/skip.txt +++ b/examples/device/dfu/skip.txt @@ -1,3 +1,2 @@ -mcu:TM4C mcu:BCM2835 family:espressif diff --git a/examples/device/dynamic_configuration/src/usb_descriptors.c b/examples/device/dynamic_configuration/src/usb_descriptors.c index 458b7c2a5..c4049414f 100644 --- a/examples/device/dynamic_configuration/src/usb_descriptors.c +++ b/examples/device/dynamic_configuration/src/usb_descriptors.c @@ -132,7 +132,7 @@ enum #define EPNUM_1_MSC_OUT 0x02 #define EPNUM_1_MSC_IN 0x82 -#elif defined(TUD_ENDPOINT_ONE_DIRECTION_ONLY) +#elif CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY // MCUs that don't support a same endpoint number with different direction IN and OUT defined in tusb_mcu.h // e.g EP1 OUT & EP1 IN cannot exist together #define EPNUM_0_CDC_NOTIF 0x81 diff --git a/examples/device/hid_generic_inout/src/usb_descriptors.c b/examples/device/hid_generic_inout/src/usb_descriptors.c index 929b2fd3a..93e718461 100644 --- a/examples/device/hid_generic_inout/src/usb_descriptors.c +++ b/examples/device/hid_generic_inout/src/usb_descriptors.c @@ -97,7 +97,7 @@ enum #define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_HID_INOUT_DESC_LEN) -#if defined(TUD_ENDPOINT_ONE_DIRECTION_ONLY) +#if CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY // MCUs that don't support a same endpoint number with different direction IN and OUT defined in tusb_mcu.h // e.g EP1 OUT & EP1 IN cannot exist together #define EPNUM_HID_OUT 0x01 diff --git a/examples/device/midi_test/src/usb_descriptors.c b/examples/device/midi_test/src/usb_descriptors.c index e969f33a3..99c798ce1 100644 --- a/examples/device/midi_test/src/usb_descriptors.c +++ b/examples/device/midi_test/src/usb_descriptors.c @@ -87,7 +87,7 @@ enum { #define EPNUM_MIDI_OUT 0x02 #define EPNUM_MIDI_IN 0x81 -#elif defined(TUD_ENDPOINT_ONE_DIRECTION_ONLY) +#elif CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY // MCUs that don't support a same endpoint number with different direction IN and OUT defined in tusb_mcu.h // e.g EP1 OUT & EP1 IN cannot exist together #define EPNUM_MIDI_OUT 0x01 diff --git a/examples/device/midi_test_freertos/src/usb_descriptors.c b/examples/device/midi_test_freertos/src/usb_descriptors.c index e969f33a3..99c798ce1 100644 --- a/examples/device/midi_test_freertos/src/usb_descriptors.c +++ b/examples/device/midi_test_freertos/src/usb_descriptors.c @@ -87,7 +87,7 @@ enum { #define EPNUM_MIDI_OUT 0x02 #define EPNUM_MIDI_IN 0x81 -#elif defined(TUD_ENDPOINT_ONE_DIRECTION_ONLY) +#elif CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY // MCUs that don't support a same endpoint number with different direction IN and OUT defined in tusb_mcu.h // e.g EP1 OUT & EP1 IN cannot exist together #define EPNUM_MIDI_OUT 0x01 diff --git a/examples/device/msc_dual_lun/src/usb_descriptors.c b/examples/device/msc_dual_lun/src/usb_descriptors.c index f73935ee0..b328cf17f 100644 --- a/examples/device/msc_dual_lun/src/usb_descriptors.c +++ b/examples/device/msc_dual_lun/src/usb_descriptors.c @@ -91,11 +91,17 @@ enum #define EPNUM_MSC_OUT 0x02 #define EPNUM_MSC_IN 0x81 -#elif defined(TUD_ENDPOINT_ONE_DIRECTION_ONLY) +#elif CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY // MCUs that don't support a same endpoint number with different direction IN and OUT defined in tusb_mcu.h // e.g EP1 OUT & EP1 IN cannot exist together - #define EPNUM_MSC_OUT 0x01 - #define EPNUM_MSC_IN 0x82 + #if TU_CHECK_MCU(OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX32690, OPT_MCU_MAX78002) + // Put bulk on EP>=8 so the 2048/4096-byte FIFOs can back double packet buffering + #define EPNUM_MSC_OUT 0x08 + #define EPNUM_MSC_IN 0x89 + #else + #define EPNUM_MSC_OUT 0x01 + #define EPNUM_MSC_IN 0x82 + #endif #else #define EPNUM_MSC_OUT 0x01 diff --git a/examples/device/mtp/src/usb_descriptors.c b/examples/device/mtp/src/usb_descriptors.c index f0aa3de6b..4c840560e 100644 --- a/examples/device/mtp/src/usb_descriptors.c +++ b/examples/device/mtp/src/usb_descriptors.c @@ -94,7 +94,7 @@ enum #define EPNUM_MTP_OUT 0x02 #define EPNUM_MTP_IN 0x81 -#elif defined(TUD_ENDPOINT_ONE_DIRECTION_ONLY) +#elif CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY // MCUs that don't support a same endpoint number with different direction IN and OUT defined in tusb_mcu.h // e.g EP1 OUT & EP1 IN cannot exist together #define EPNUM_MTP_EVT 0x81 diff --git a/examples/device/net_lwip_webserver/skip.txt b/examples/device/net_lwip_webserver/skip.txt index ecb9eb7ec..5e5562087 100644 --- a/examples/device/net_lwip_webserver/skip.txt +++ b/examples/device/net_lwip_webserver/skip.txt @@ -19,6 +19,6 @@ board:at_start_f425 board:curiosity_nano board:frdm_kl25z # lpc55 has weird error 'ncm_interface' causes a section type conflict with 'ntb_parameters' -family:lpc55 +#family:lpc55 family:nuc126 family:nuc100_120 diff --git a/examples/device/net_lwip_webserver/src/lwipopts.h b/examples/device/net_lwip_webserver/src/lwipopts.h index 11686ce2a..350120423 100644 --- a/examples/device/net_lwip_webserver/src/lwipopts.h +++ b/examples/device/net_lwip_webserver/src/lwipopts.h @@ -32,6 +32,14 @@ #ifndef LWIPOPTS_H__ #define LWIPOPTS_H__ +// Pulls in tusb_option.h → tusb_config.h, which defines LWIP_HIGH_THROUGHPUT +// based on the target MCU's SRAM tier. +#include "tusb_option.h" + +#ifndef LWIP_HIGH_THROUGHPUT + #define LWIP_HIGH_THROUGHPUT 0 +#endif + /* Prevent having to link sys_arch.c (we don't test the API layers in unit tests) */ #define NO_SYS 1 #define MEM_ALIGNMENT 4 @@ -49,7 +57,15 @@ #define TCP_MSS (1500 /*mtu*/ - 20 /*iphdr*/ - 20 /*tcphhr*/) #define TCP_SND_BUF (4 * TCP_MSS) -#define TCP_WND (4 * TCP_MSS) +#if LWIP_HIGH_THROUGHPUT + #define TCP_WND (8 * TCP_MSS) + #define PBUF_POOL_SIZE 8 + // Must grow in step with TCP_SND_BUF (default MEMP_NUM_TCP_SEG=16 caps TCP_SND_BUF at 4*MSS). + #define MEMP_NUM_TCP_SEG 16 +#else + #define TCP_WND (4 * TCP_MSS) + #define PBUF_POOL_SIZE 4 +#endif #define ETHARP_SUPPORT_STATIC_ENTRIES 1 @@ -60,8 +76,6 @@ #define LWIP_SINGLE_NETIF 1 #define LWIP_NETIF_LINK_CALLBACK 1 -#define PBUF_POOL_SIZE 4 - #define HTTPD_USE_CUSTOM_FSDATA 0 #define LWIP_MULTICAST_PING 1 diff --git a/examples/device/net_lwip_webserver/src/tusb_config.h b/examples/device/net_lwip_webserver/src/tusb_config.h index 3285ea52c..aff75866d 100644 --- a/examples/device/net_lwip_webserver/src/tusb_config.h +++ b/examples/device/net_lwip_webserver/src/tusb_config.h @@ -30,8 +30,6 @@ extern "C" { #endif -#include "lwipopts.h" - //--------------------------------------------------------------------+ // Board Specific Configuration //--------------------------------------------------------------------+ @@ -92,32 +90,50 @@ extern "C" { #define USE_ECM 1 #elif TU_CHECK_MCU(OPT_MCU_STM32F0, OPT_MCU_STM32F1) #define USE_ECM 1 -#elif TU_CHECK_MCU(OPT_MCU_MAX32690, OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX78002) - #define USE_ECM 1 #else #define USE_ECM 0 - #define INCLUDE_IPERF #endif #endif +// MCU SRAM tier — drives the bigger lwIP buffers in lwipopts.h, the larger +// NCM OUT NTB size below, and whether iperf is built. Small-RAM MCUs +// (stm32c0/f1/wb, lpc11/13, samd11) keep modest defaults to fit. +#ifndef LWIP_HIGH_THROUGHPUT + #if TU_CHECK_MCU(OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX32690, OPT_MCU_MAX78002) || \ + TU_CHECK_MCU(OPT_MCU_STM32F2, OPT_MCU_STM32F4, OPT_MCU_STM32F7) || \ + TU_CHECK_MCU(OPT_MCU_STM32H5, OPT_MCU_STM32H7, OPT_MCU_STM32H7RS) || \ + TU_CHECK_MCU(OPT_MCU_STM32U5, OPT_MCU_STM32N6) || \ + TU_CHECK_MCU(OPT_MCU_RP2040) || \ + TU_CHECK_MCU(OPT_MCU_MIMXRT1XXX) || \ + TU_CHECK_MCU(OPT_MCU_NRF5X) + #define LWIP_HIGH_THROUGHPUT 1 + #else + #define LWIP_HIGH_THROUGHPUT 0 + #endif +#endif + +#if LWIP_HIGH_THROUGHPUT && !defined(INCLUDE_IPERF) + #define INCLUDE_IPERF +#endif + //-------------------------------------------------------------------- // NCM CLASS CONFIGURATION, SEE "ncm.h" FOR PERFORMANCE TUNING //-------------------------------------------------------------------- -// Must be >> MTU -// Can be set to 2048 without impact -#define CFG_TUD_NCM_IN_NTB_MAX_SIZE (2 * TCP_MSS + 100) +// CDC-NCM 1.0 Table 6-4 defines 2048 as the minimum required NTB size +#define CFG_TUD_NCM_IN_NTB_MAX_SIZE 2048 -// Must be >> MTU -// Can be set to smaller values if wNtbOutMaxDatagrams==1 -#define CFG_TUD_NCM_OUT_NTB_MAX_SIZE (2 * TCP_MSS + 100) +#if LWIP_HIGH_THROUGHPUT + #define CFG_TUD_NCM_OUT_NTB_MAX_SIZE 4096 +#else + #define CFG_TUD_NCM_OUT_NTB_MAX_SIZE 2048 +#endif // Number of NCM transfer blocks for reception side #ifndef CFG_TUD_NCM_OUT_NTB_N #define CFG_TUD_NCM_OUT_NTB_N 1 #endif -// Number of NCM transfer blocks for transmission side #ifndef CFG_TUD_NCM_IN_NTB_N #define CFG_TUD_NCM_IN_NTB_N 1 #endif diff --git a/examples/device/net_lwip_webserver/src/usb_descriptors.c b/examples/device/net_lwip_webserver/src/usb_descriptors.c index c976cb62b..09090bb92 100644 --- a/examples/device/net_lwip_webserver/src/usb_descriptors.c +++ b/examples/device/net_lwip_webserver/src/usb_descriptors.c @@ -65,17 +65,19 @@ enum { CONFIG_ID_COUNT }; +#if CFG_TUD_NCM +#define USB_BCD 0x0201 +#else +#define USB_BCD 0x0200 +#endif + //--------------------------------------------------------------------+ // Device Descriptors //--------------------------------------------------------------------+ static const tusb_desc_device_t desc_device = { .bLength = sizeof(tusb_desc_device_t), .bDescriptorType = TUSB_DESC_DEVICE, -#if CFG_TUD_NCM - .bcdUSB = 0x0201, -#else - .bcdUSB = 0x0200, -#endif + .bcdUSB = USB_BCD, // Use Interface Association Descriptor (IAD) device class .bDeviceClass = TUSB_CLASS_MISC, .bDeviceSubClass = MISC_SUBCLASS_COMMON, @@ -121,12 +123,20 @@ const uint8_t *tud_descriptor_device_cb(void) { #define EPNUM_NET_OUT 0x02 #define EPNUM_NET_IN 0x81 -#elif defined(TUD_ENDPOINT_ONE_DIRECTION_ONLY) -// MCUs that don't support a same endpoint number with different direction IN and OUT defined in tusb_mcu.h +#elif CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY +// MCUs that don't support the same endpoint number with different direction IN and OUT defined in tusb_mcu.h // e.g EP1 OUT & EP1 IN cannot exist together + +#if TU_CHECK_MCU(OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX32690, OPT_MCU_MAX78002) +// endpoint 8,9 has FIFO of 2048 bytes +#define EPNUM_NET_NOTIF 0x81 +#define EPNUM_NET_OUT 0x08 +#define EPNUM_NET_IN 0x89 +#else #define EPNUM_NET_NOTIF 0x81 #define EPNUM_NET_OUT 0x02 #define EPNUM_NET_IN 0x83 +#endif #else #define EPNUM_NET_NOTIF 0x81 @@ -136,57 +146,184 @@ const uint8_t *tud_descriptor_device_cb(void) { #if CFG_TUD_ECM_RNDIS -static uint8_t const rndis_configuration[] = { +// full speed configuration +static uint8_t const rndis_fs_configuration[] = { // Config number (index+1), interface count, string index, total length, attribute, power in mA TUD_CONFIG_DESCRIPTOR(CONFIG_ID_RNDIS + 1, ITF_NUM_TOTAL, 0, MAIN_CONFIG_TOTAL_LEN, 0, 100), // Interface number, string index, EP notification address and size, EP data address (out, in) and size. TUD_RNDIS_DESCRIPTOR( - ITF_NUM_CDC, STRID_INTERFACE, EPNUM_NET_NOTIF, 8, EPNUM_NET_OUT, EPNUM_NET_IN, CFG_TUD_NET_ENDPOINT_SIZE), + ITF_NUM_CDC, STRID_INTERFACE, EPNUM_NET_NOTIF, 8, EPNUM_NET_OUT, EPNUM_NET_IN, 64), }; -static const uint8_t ecm_configuration[] = { +static const uint8_t ecm_fs_configuration[] = { // Config number (index+1), interface count, string index, total length, attribute, power in mA TUD_CONFIG_DESCRIPTOR(CONFIG_ID_ECM + 1, ITF_NUM_TOTAL, 0, ALT_CONFIG_TOTAL_LEN, 0, 100), // Interface number, description string index, MAC address string index, EP notification address and size, EP data address (out, in), and size, max segment size. TUD_CDC_ECM_DESCRIPTOR( ITF_NUM_CDC, STRID_INTERFACE, STRID_MAC, EPNUM_NET_NOTIF, 64, EPNUM_NET_OUT, EPNUM_NET_IN, - CFG_TUD_NET_ENDPOINT_SIZE, CFG_TUD_NET_MTU), + 64, CFG_TUD_NET_MTU), }; -#else +#if TUD_OPT_HIGH_SPEED +// Per USB specs: high speed capable device must report device_qualifier and other_speed_configuration -static uint8_t const ncm_configuration[] = { +// high speed configuration +static uint8_t const rndis_hs_configuration[] = { // Config number (index+1), interface count, string index, total length, attribute, power in mA - TUD_CONFIG_DESCRIPTOR(CONFIG_ID_NCM + 1, ITF_NUM_TOTAL, 0, NCM_CONFIG_TOTAL_LEN, 0, 100), + TUD_CONFIG_DESCRIPTOR(CONFIG_ID_RNDIS + 1, ITF_NUM_TOTAL, 0, MAIN_CONFIG_TOTAL_LEN, 0, 100), + + // Interface number, string index, EP notification address and size, EP data address (out, in) and size. + TUD_RNDIS_DESCRIPTOR( + ITF_NUM_CDC, STRID_INTERFACE, EPNUM_NET_NOTIF, 8, EPNUM_NET_OUT, EPNUM_NET_IN, 512), +}; + +static const uint8_t ecm_hs_configuration[] = { + // Config number (index+1), interface count, string index, total length, attribute, power in mA + TUD_CONFIG_DESCRIPTOR(CONFIG_ID_ECM + 1, ITF_NUM_TOTAL, 0, ALT_CONFIG_TOTAL_LEN, 0, 100), // Interface number, description string index, MAC address string index, EP notification address and size, EP data address (out, in), and size, max segment size. - TUD_CDC_NCM_DESCRIPTOR( + TUD_CDC_ECM_DESCRIPTOR( ITF_NUM_CDC, STRID_INTERFACE, STRID_MAC, EPNUM_NET_NOTIF, 64, EPNUM_NET_OUT, EPNUM_NET_IN, - CFG_TUD_NET_ENDPOINT_SIZE, CFG_TUD_NET_MTU), + 512, CFG_TUD_NET_MTU), +}; +#endif // highspeed + +#else + +// full speed configuration +static uint8_t const ncm_fs_configuration[] = { + // Config number (index+1), interface count, string index, total length, attribute, power in mA + TUD_CONFIG_DESCRIPTOR(CONFIG_ID_NCM + 1, ITF_NUM_TOTAL, 0, NCM_CONFIG_TOTAL_LEN, 0, 100), + + // Interface number, description string index, MAC address string index, EP notification address and size, EP data address (out, in), and size, max segment size, EP notification bInterval, NCM capabilities. + TUD_CDC_NCM_DESCRIPTOR(ITF_NUM_CDC, STRID_INTERFACE, STRID_MAC, EPNUM_NET_NOTIF, 64, EPNUM_NET_OUT, EPNUM_NET_IN, + 64, CFG_TUD_NET_MTU, 50, (uint8_t)((uint8_t)NCM_NETWORK_CAPS_ETH_FILTER | (uint8_t)NCM_NETWORK_CAPS_NTB_INPUT_SIZE)), +}; + +#if TUD_OPT_HIGH_SPEED +// Per USB specs: high speed capable device must report device_qualifier and other_speed_configuration + +// high speed configuration +// bInterval: FS=50 means 50ms; HS encodes as 2^(n-1) * 125us, so 9 = 2^8 * 125us = 32ms +static uint8_t const ncm_hs_configuration[] = { + // Config number (index+1), interface count, string index, total length, attribute, power in mA + TUD_CONFIG_DESCRIPTOR(CONFIG_ID_NCM + 1, ITF_NUM_TOTAL, 0, NCM_CONFIG_TOTAL_LEN, 0, 100), + + // Interface number, description string index, MAC address string index, EP notification address and size, EP data address (out, in), and size, max segment size, EP notification bInterval, NCM capabilities. + TUD_CDC_NCM_DESCRIPTOR(ITF_NUM_CDC, STRID_INTERFACE, STRID_MAC, EPNUM_NET_NOTIF, 64, EPNUM_NET_OUT, EPNUM_NET_IN, + 512, CFG_TUD_NET_MTU, 9, (uint8_t)((uint8_t)NCM_NETWORK_CAPS_ETH_FILTER | (uint8_t)NCM_NETWORK_CAPS_NTB_INPUT_SIZE)), }; +#endif // highspeed #endif -// Configuration array: RNDIS and CDC-ECM +// NCM work with all latest OS i.e macos 10.10+, windows 10+, and Linux. +// For older system Configuration array of RNDIS and CDC-ECM may be needed for better compatibility. // - Windows only works with RNDIS // - MacOS only works with CDC-ECM // - Linux will work on both -static const uint8_t *const configuration_arr[CONFIG_ID_COUNT] = { #if CFG_TUD_ECM_RNDIS - [CONFIG_ID_RNDIS] = rndis_configuration, - [CONFIG_ID_ECM] = ecm_configuration + +static const uint8_t *const configuration_fs_arr[CONFIG_ID_COUNT] = { + [CONFIG_ID_RNDIS] = rndis_fs_configuration, + [CONFIG_ID_ECM] = ecm_fs_configuration +}; + +#if TUD_OPT_HIGH_SPEED +static const uint8_t *const configuration_hs_arr[CONFIG_ID_COUNT] = { + [CONFIG_ID_RNDIS] = rndis_hs_configuration, + [CONFIG_ID_ECM] = ecm_hs_configuration +}; + +// Size array for each configuration +static const uint16_t configuration_sz_arr[CONFIG_ID_COUNT] = { + [CONFIG_ID_RNDIS] = MAIN_CONFIG_TOTAL_LEN, + [CONFIG_ID_ECM] = ALT_CONFIG_TOTAL_LEN +}; + +// Scratch buffer for other speed configuration (sized to hold the largest config) +#define MAX_CONFIG_TOTAL_LEN TU_MAX(MAIN_CONFIG_TOTAL_LEN, ALT_CONFIG_TOTAL_LEN) +#endif + #else - [CONFIG_ID_NCM] = ncm_configuration + +static const uint8_t *const configuration_fs_arr[CONFIG_ID_COUNT] = { + [CONFIG_ID_NCM] = ncm_fs_configuration +}; + +#if TUD_OPT_HIGH_SPEED +static const uint8_t *const configuration_hs_arr[CONFIG_ID_COUNT] = { + [CONFIG_ID_NCM] = ncm_hs_configuration +}; + +// Size array for each configuration +static const uint16_t configuration_sz_arr[CONFIG_ID_COUNT] = { + [CONFIG_ID_NCM] = NCM_CONFIG_TOTAL_LEN +}; + +// Scratch buffer for other speed configuration (sized to hold the largest config) +#define MAX_CONFIG_TOTAL_LEN NCM_CONFIG_TOTAL_LEN #endif + +#endif + +#if TUD_OPT_HIGH_SPEED +static uint8_t desc_other_speed_config[MAX_CONFIG_TOTAL_LEN]; + +// device qualifier: device descriptor fields that differ at other speed +static tusb_desc_device_qualifier_t const desc_device_qualifier = { + .bLength = sizeof(tusb_desc_device_qualifier_t), + .bDescriptorType = TUSB_DESC_DEVICE_QUALIFIER, + .bcdUSB = USB_BCD, + + .bDeviceClass = TUSB_CLASS_MISC, + .bDeviceSubClass = MISC_SUBCLASS_COMMON, + .bDeviceProtocol = MISC_PROTOCOL_IAD, + + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + .bNumConfigurations = CONFIG_ID_COUNT, + .bReserved = 0x00 }; +// Invoked when received GET DEVICE QUALIFIER DESCRIPTOR request +// Application return pointer to descriptor, whose contents must exist long enough for transfer to complete. +// device_qualifier descriptor describes information about a high-speed capable device that would +// change if the device were operating at the other speed. If not highspeed capable stall this request. +uint8_t const *tud_descriptor_device_qualifier_cb(void) { + return (uint8_t const *) &desc_device_qualifier; +} + +// Invoked when received GET OTHER SPEED CONFIGURATION DESCRIPTOR request +// Application return pointer to descriptor, whose contents must exist long enough for transfer to complete +// Configuration descriptor in the other speed e.g if high speed then this is for full speed and vice versa +uint8_t const *tud_descriptor_other_speed_configuration_cb(uint8_t index) { + if (index >= CONFIG_ID_COUNT) return NULL; + + // if link speed is high return fullspeed config, and vice versa + const uint8_t *const *arr = (tud_speed_get() == TUSB_SPEED_HIGH) ? configuration_fs_arr : configuration_hs_arr; + + // Note: the descriptor type is OTHER_SPEED_CONFIG instead of CONFIG + memcpy(desc_other_speed_config, arr[index], configuration_sz_arr[index]); + desc_other_speed_config[1] = TUSB_DESC_OTHER_SPEED_CONFIG; + + return desc_other_speed_config; +} + +#endif // highspeed + // Invoked when received GET CONFIGURATION DESCRIPTOR // Application return pointer to descriptor // Descriptor contents must exist long enough for transfer to complete const uint8_t *tud_descriptor_configuration_cb(uint8_t index) { - return (index < CONFIG_ID_COUNT) ? configuration_arr[index] : NULL; + if (index >= CONFIG_ID_COUNT) return NULL; +#if TUD_OPT_HIGH_SPEED + // Although we are highspeed, host may be fullspeed. + return (tud_speed_get() == TUSB_SPEED_HIGH) ? configuration_hs_arr[index] : configuration_fs_arr[index]; +#else + return configuration_fs_arr[index]; +#endif } #if CFG_TUD_NCM diff --git a/examples/device/printer_to_cdc/src/usb_descriptors.c b/examples/device/printer_to_cdc/src/usb_descriptors.c index 30d309ed4..db7bfe97a 100644 --- a/examples/device/printer_to_cdc/src/usb_descriptors.c +++ b/examples/device/printer_to_cdc/src/usb_descriptors.c @@ -67,12 +67,21 @@ uint8_t const *tud_descriptor_device_cb(void) { //--------------------------------------------------------------------+ // Endpoint numbers -#if defined(TUD_ENDPOINT_ONE_DIRECTION_ONLY) - #define EPNUM_CDC_NOTIF 0x81 - #define EPNUM_CDC_OUT 0x02 - #define EPNUM_CDC_IN 0x83 - #define EPNUM_PRINTER_OUT 0x04 - #define EPNUM_PRINTER_IN 0x85 +#if CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY + #if TU_CHECK_MCU(OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX32690, OPT_MCU_MAX78002) + // Put bulk on EP>=8 so the 2048/4096-byte FIFOs can back double packet buffering + #define EPNUM_CDC_NOTIF 0x81 + #define EPNUM_CDC_OUT 0x08 + #define EPNUM_CDC_IN 0x89 + #define EPNUM_PRINTER_OUT 0x0A + #define EPNUM_PRINTER_IN 0x8B + #else + #define EPNUM_CDC_NOTIF 0x81 + #define EPNUM_CDC_OUT 0x02 + #define EPNUM_CDC_IN 0x83 + #define EPNUM_PRINTER_OUT 0x04 + #define EPNUM_PRINTER_IN 0x85 + #endif #else #define EPNUM_CDC_NOTIF 0x81 #define EPNUM_CDC_OUT 0x02 diff --git a/examples/device/uac2_headset/src/usb_descriptors.c b/examples/device/uac2_headset/src/usb_descriptors.c index e4fbbf8a5..b554e7195 100644 --- a/examples/device/uac2_headset/src/usb_descriptors.c +++ b/examples/device/uac2_headset/src/usb_descriptors.c @@ -97,12 +97,19 @@ uint8_t const * tud_descriptor_device_cb(void) #define EPNUM_AUDIO_OUT 0x08 #define EPNUM_AUDIO_INT 0x01 -#elif defined(TUD_ENDPOINT_ONE_DIRECTION_ONLY) +#elif CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY // MCUs that don't support a same endpoint number with different direction IN and OUT defined in tusb_mcu.h // e.g EP1 OUT & EP1 IN cannot exist together - #define EPNUM_AUDIO_IN 0x01 - #define EPNUM_AUDIO_OUT 0x02 - #define EPNUM_AUDIO_INT 0x03 + #if TU_CHECK_MCU(OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX32690, OPT_MCU_MAX78002) + // Put audio iso on EP10/11 so the 4096-byte FIFOs can back double packet buffering + #define EPNUM_AUDIO_OUT 0x0A + #define EPNUM_AUDIO_IN 0x0B + #define EPNUM_AUDIO_INT 0x01 + #else + #define EPNUM_AUDIO_IN 0x01 + #define EPNUM_AUDIO_OUT 0x02 + #define EPNUM_AUDIO_INT 0x03 + #endif #else #define EPNUM_AUDIO_IN 0x01 diff --git a/examples/device/uac2_speaker_fb/src/usb_descriptors.c b/examples/device/uac2_speaker_fb/src/usb_descriptors.c index c5a161a1e..f0c780e38 100644 --- a/examples/device/uac2_speaker_fb/src/usb_descriptors.c +++ b/examples/device/uac2_speaker_fb/src/usb_descriptors.c @@ -115,12 +115,19 @@ uint8_t const * tud_hid_descriptor_report_cb(uint8_t itf) { #define EPNUM_AUDIO_FB 0x08 #define EPNUM_DEBUG 0x01 -#elif defined(TUD_ENDPOINT_ONE_DIRECTION_ONLY) +#elif CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY // MCUs that don't support a same endpoint number with different direction IN and OUT defined in tusb_mcu.h // e.g EP1 OUT & EP1 IN cannot exist together - #define EPNUM_AUDIO 0x02 - #define EPNUM_AUDIO_FB 0x01 - #define EPNUM_DEBUG 0x03 + #if TU_CHECK_MCU(OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX32690, OPT_MCU_MAX78002) + // Put audio iso on EP10/11 so the 4096-byte FIFOs can back double packet buffering + #define EPNUM_AUDIO 0x0A + #define EPNUM_AUDIO_FB 0x0B + #define EPNUM_DEBUG 0x01 + #else + #define EPNUM_AUDIO 0x02 + #define EPNUM_AUDIO_FB 0x01 + #define EPNUM_DEBUG 0x03 + #endif #else #define EPNUM_AUDIO 0x01 diff --git a/examples/device/webusb_serial/src/usb_descriptors.c b/examples/device/webusb_serial/src/usb_descriptors.c index 0ef41a68e..527837161 100644 --- a/examples/device/webusb_serial/src/usb_descriptors.c +++ b/examples/device/webusb_serial/src/usb_descriptors.c @@ -104,15 +104,25 @@ enum #define EPNUM_VENDOR_OUT 0x05 #define EPNUM_VENDOR_IN 0x84 -#elif defined(TUD_ENDPOINT_ONE_DIRECTION_ONLY) +#elif CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY // MCUs that don't support a same endpoint number with different direction IN and OUT defined in tusb_mcu.h // e.g EP1 OUT & EP1 IN cannot exist together - #define EPNUM_CDC_NOTIF 0x81 - #define EPNUM_CDC_OUT 0x02 - #define EPNUM_CDC_IN 0x83 + #if TU_CHECK_MCU(OPT_MCU_MAX32650, OPT_MCU_MAX32666, OPT_MCU_MAX32690, OPT_MCU_MAX78002) + // Put bulk on EP>=8 so the 2048/4096-byte FIFOs can back double packet buffering + #define EPNUM_CDC_NOTIF 0x81 + #define EPNUM_CDC_OUT 0x08 + #define EPNUM_CDC_IN 0x89 + + #define EPNUM_VENDOR_OUT 0x0A + #define EPNUM_VENDOR_IN 0x8B + #else + #define EPNUM_CDC_NOTIF 0x81 + #define EPNUM_CDC_OUT 0x02 + #define EPNUM_CDC_IN 0x83 - #define EPNUM_VENDOR_OUT 0x04 - #define EPNUM_VENDOR_IN 0x85 + #define EPNUM_VENDOR_OUT 0x04 + #define EPNUM_VENDOR_IN 0x85 + #endif #else #define EPNUM_CDC_NOTIF 0x81 diff --git a/examples/dual/dynamic_switch/src/usb_descriptors.c b/examples/dual/dynamic_switch/src/usb_descriptors.c index 54ffc2c18..ef6d795b7 100644 --- a/examples/dual/dynamic_switch/src/usb_descriptors.c +++ b/examples/dual/dynamic_switch/src/usb_descriptors.c @@ -86,7 +86,7 @@ enum { #define EPNUM_CDC_OUT 0x02 #define EPNUM_CDC_IN 0x82 -#elif defined(TUD_ENDPOINT_ONE_DIRECTION_ONLY) +#elif CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY // MCUs that don't support a same endpoint number with different direction IN and OUT defined in tusb_mcu.h // e.g EP1 OUT & EP1 IN cannot exist together #define EPNUM_CDC_NOTIF 0x81 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/hw/bsp/at32f402_405/family.c b/hw/bsp/at32f402_405/family.c index 56d4a7bea..aa1a5b484 100644 --- a/hw/bsp/at32f402_405/family.c +++ b/hw/bsp/at32f402_405/family.c @@ -220,7 +220,7 @@ int board_uart_write(void const *buf, int len) #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/at32f403a_407/family.c b/hw/bsp/at32f403a_407/family.c index 942e15872..cf15ba83a 100644 --- a/hw/bsp/at32f403a_407/family.c +++ b/hw/bsp/at32f403a_407/family.c @@ -238,7 +238,7 @@ int board_uart_write(void const *buf, int len) { #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/at32f413/family.c b/hw/bsp/at32f413/family.c index d9af0ae4d..69591b2ba 100644 --- a/hw/bsp/at32f413/family.c +++ b/hw/bsp/at32f413/family.c @@ -238,7 +238,7 @@ int board_uart_write(void const *buf, int len) { #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/at32f415/family.c b/hw/bsp/at32f415/family.c index ca205d480..132e0db31 100644 --- a/hw/bsp/at32f415/family.c +++ b/hw/bsp/at32f415/family.c @@ -212,7 +212,7 @@ int board_uart_write(void const *buf, int len) { #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/at32f423/family.c b/hw/bsp/at32f423/family.c index 9f13dba07..79ac3fcca 100644 --- a/hw/bsp/at32f423/family.c +++ b/hw/bsp/at32f423/family.c @@ -239,7 +239,7 @@ int board_uart_write(void const *buf, int len) { #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/at32f425/family.c b/hw/bsp/at32f425/family.c index 1629ad7c0..75e53b1f7 100644 --- a/hw/bsp/at32f425/family.c +++ b/hw/bsp/at32f425/family.c @@ -220,7 +220,7 @@ int board_uart_write(void const *buf, int len) { #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/at32f435_437/family.c b/hw/bsp/at32f435_437/family.c index 59a4fe120..80e200363 100644 --- a/hw/bsp/at32f435_437/family.c +++ b/hw/bsp/at32f435_437/family.c @@ -265,7 +265,7 @@ int board_uart_write(void const *buf, int len) { #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/at32f45x/family.c b/hw/bsp/at32f45x/family.c index 27eae861f..42b688b87 100644 --- a/hw/bsp/at32f45x/family.c +++ b/hw/bsp/at32f45x/family.c @@ -216,7 +216,7 @@ int board_uart_write(void const *buf, int len) { #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/board.c b/hw/bsp/board.c index 65b44e5f2..5093382d9 100644 --- a/hw/bsp/board.c +++ b/hw/bsp/board.c @@ -171,7 +171,7 @@ int board_getchar(void) { } int board_putchar(int c) { - if (board_uart_write((const char *)&c, 1)) { + if (board_uart_write((const char *)&c, 1) > 0) { return c; } else { return -1; diff --git a/hw/bsp/ch32v20x/family.c b/hw/bsp/ch32v20x/family.c index 221f62107..76024cfde 100644 --- a/hw/bsp/ch32v20x/family.c +++ b/hw/bsp/ch32v20x/family.c @@ -220,6 +220,6 @@ int board_uart_write(void const *buf, int len) { return count; #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/family_support.cmake b/hw/bsp/family_support.cmake index cb8ec6cf8..2468ac43c 100644 --- a/hw/bsp/family_support.cmake +++ b/hw/bsp/family_support.cmake @@ -326,10 +326,12 @@ ld_defs=\"$(echo \"$ld_defs\" | xargs)\"") "if [ -f \"${TARGET_ELF_PATH}\" ]; then \ ${MEMBROWSE_LD_SCRIPTS_CMD}; \ ${MEMBROWSE_LD_DEFS_CMD}; \ + map_arg=\"\"; \ + if [ -f \"${TARGET_ELF_PATH}.map\" ]; then map_arg=\"--map-file \\\"${TARGET_ELF_PATH}.map\\\"\"; fi; \ if [ \"$MEMBROWSE_UPLOAD\" = \"1\" ]; then \ - MEMBROWSE_CMD=\"${MEMBROWSE_EXE} report ${OPTION} \\\"${TARGET_ELF_PATH}\\\" \\\"$ld_scripts\\\" $ld_defs --upload --github --target-name ${BOARD}/${TARGET} --api-key $ENV{MEMBROWSE_API_KEY}\"; \ + MEMBROWSE_CMD=\"${MEMBROWSE_EXE} report ${OPTION} \\\"${TARGET_ELF_PATH}\\\" \\\"$ld_scripts\\\" $ld_defs $map_arg --upload --github --target-name ${BOARD}/${TARGET} --api-key $ENV{MEMBROWSE_API_KEY}\"; \ else \ - MEMBROWSE_CMD=\"${MEMBROWSE_EXE} report ${OPTION} \\\"${TARGET_ELF_PATH}\\\" \\\"$ld_scripts\\\" $ld_defs\"; \ + MEMBROWSE_CMD=\"${MEMBROWSE_EXE} report ${OPTION} \\\"${TARGET_ELF_PATH}\\\" \\\"$ld_scripts\\\" $ld_defs $map_arg\"; \ fi; \ else \ if [ \"$MEMBROWSE_UPLOAD\" = \"1\" ]; then \ diff --git a/hw/bsp/ft9xx/family.c b/hw/bsp/ft9xx/family.c index 5ee134eb0..0ca2663ac 100644 --- a/hw/bsp/ft9xx/family.c +++ b/hw/bsp/ft9xx/family.c @@ -221,8 +221,8 @@ int board_uart_read(uint8_t *buf, int len) // Send characters to UART int board_uart_write(void const *buf, int len) { - int count = 0; #ifdef BOARD_UART + int count = 0; uint8_t const *p = (uint8_t const *) buf; while (count < len) { if (BOARD_UART->LSR_ICR_XON2 & MASK_UART_LSR_THRE) { @@ -232,10 +232,11 @@ int board_uart_write(void const *buf, int len) break; } } + return count; #else (void) buf; (void) len; + return -1; #endif - return count; } // Get current milliseconds diff --git a/hw/bsp/gd32vf103/family.c b/hw/bsp/gd32vf103/family.c index c1dc82bda..8f82b3ada 100644 --- a/hw/bsp/gd32vf103/family.c +++ b/hw/bsp/gd32vf103/family.c @@ -174,7 +174,7 @@ int board_uart_write(void const* buf, int len) { #else (void)buf; (void)len; - return 0; + return -1; #endif } diff --git a/hw/bsp/imxrt/boards/metro_m7_1011/board.cmake b/hw/bsp/imxrt/boards/metro_m7_1011/board.cmake index 63b2a120a..5edf6cc91 100644 --- a/hw/bsp/imxrt/boards/metro_m7_1011/board.cmake +++ b/hw/bsp/imxrt/boards/metro_m7_1011/board.cmake @@ -12,5 +12,6 @@ function(update_board TARGET) target_compile_definitions(${TARGET} PUBLIC CPU_MIMXRT1011DAE5A CFG_EXAMPLE_VIDEO_READONLY + LWIP_HIGH_THROUGHPUT=0 ) endfunction() diff --git a/hw/bsp/imxrt/boards/metro_m7_1011/board.mk b/hw/bsp/imxrt/boards/metro_m7_1011/board.mk index 229b5c18c..fadb760b3 100644 --- a/hw/bsp/imxrt/boards/metro_m7_1011/board.mk +++ b/hw/bsp/imxrt/boards/metro_m7_1011/board.mk @@ -1,4 +1,4 @@ -CFLAGS += -DCPU_MIMXRT1011DAE5A -DCFG_EXAMPLE_VIDEO_READONLY +CFLAGS += -DCPU_MIMXRT1011DAE5A -DCFG_EXAMPLE_VIDEO_READONLY -DLWIP_HIGH_THROUGHPUT=0 MCU_FAMILY = RT1010 MCU_VARIANT = MIMXRT1011 diff --git a/hw/bsp/imxrt/boards/mimxrt1010_evk/board.cmake b/hw/bsp/imxrt/boards/mimxrt1010_evk/board.cmake index 63b2a120a..5edf6cc91 100644 --- a/hw/bsp/imxrt/boards/mimxrt1010_evk/board.cmake +++ b/hw/bsp/imxrt/boards/mimxrt1010_evk/board.cmake @@ -12,5 +12,6 @@ function(update_board TARGET) target_compile_definitions(${TARGET} PUBLIC CPU_MIMXRT1011DAE5A CFG_EXAMPLE_VIDEO_READONLY + LWIP_HIGH_THROUGHPUT=0 ) endfunction() diff --git a/hw/bsp/imxrt/boards/mimxrt1010_evk/board.mk b/hw/bsp/imxrt/boards/mimxrt1010_evk/board.mk index c5521faf5..81ec74d70 100644 --- a/hw/bsp/imxrt/boards/mimxrt1010_evk/board.mk +++ b/hw/bsp/imxrt/boards/mimxrt1010_evk/board.mk @@ -1,4 +1,4 @@ -CFLAGS += -DCPU_MIMXRT1011DAE5A -DCFG_EXAMPLE_VIDEO_READONLY +CFLAGS += -DCPU_MIMXRT1011DAE5A -DCFG_EXAMPLE_VIDEO_READONLY -DLWIP_HIGH_THROUGHPUT=0 MCU_FAMILY = RT1010 MCU_VARIANT = MIMXRT1011 diff --git a/hw/bsp/imxrt/boards/mimxrt1015_evk/board.cmake b/hw/bsp/imxrt/boards/mimxrt1015_evk/board.cmake index 5661bb3e6..fe74b8d33 100644 --- a/hw/bsp/imxrt/boards/mimxrt1015_evk/board.cmake +++ b/hw/bsp/imxrt/boards/mimxrt1015_evk/board.cmake @@ -12,5 +12,6 @@ function(update_board TARGET) target_compile_definitions(${TARGET} PUBLIC CPU_MIMXRT1015DAF5A CFG_EXAMPLE_VIDEO_READONLY + LWIP_HIGH_THROUGHPUT=0 ) endfunction() diff --git a/hw/bsp/imxrt/boards/mimxrt1015_evk/board.mk b/hw/bsp/imxrt/boards/mimxrt1015_evk/board.mk index 062929902..a84263a81 100644 --- a/hw/bsp/imxrt/boards/mimxrt1015_evk/board.mk +++ b/hw/bsp/imxrt/boards/mimxrt1015_evk/board.mk @@ -1,4 +1,4 @@ -CFLAGS += -DCPU_MIMXRT1015DAF5A -DCFG_EXAMPLE_VIDEO_READONLY +CFLAGS += -DCPU_MIMXRT1015DAF5A -DCFG_EXAMPLE_VIDEO_READONLY -DLWIP_HIGH_THROUGHPUT=0 MCU_FAMILY = RT1015 MCU_VARIANT = MIMXRT1015 diff --git a/hw/bsp/kinetis_k/family.c b/hw/bsp/kinetis_k/family.c index 8efab2762..a5af83931 100644 --- a/hw/bsp/kinetis_k/family.c +++ b/hw/bsp/kinetis_k/family.c @@ -140,7 +140,7 @@ int board_uart_write(void const *buf, int len) { #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/lpc55/boards/lpcxpresso55s69/board.cmake b/hw/bsp/lpc55/boards/lpcxpresso55s69/board.cmake index 59f7d6329..ef20d7601 100644 --- a/hw/bsp/lpc55/boards/lpcxpresso55s69/board.cmake +++ b/hw/bsp/lpc55/boards/lpcxpresso55s69/board.cmake @@ -2,7 +2,6 @@ set(MCU_VARIANT LPC55S69) set(MCU_CORE LPC55S69_cm33_core0) set(JLINK_DEVICE LPC55S69_M33_0) -set(JLINK_OPTION "-USB 000727648789") set(PYOCD_TARGET LPC55S69) set(NXPLINK_DEVICE LPC55S69:LPCXpresso55S69) diff --git a/hw/bsp/mm32/family.c b/hw/bsp/mm32/family.c index 14a17f6c5..1538181dc 100644 --- a/hw/bsp/mm32/family.c +++ b/hw/bsp/mm32/family.c @@ -167,7 +167,7 @@ int board_uart_write(void const* buf, int len) { #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/nrf/boards/nrf54h20dk/board.cmake b/hw/bsp/nrf/boards/nrf54h20dk/board.cmake index fca7a56d2..8095b59fe 100644 --- a/hw/bsp/nrf/boards/nrf54h20dk/board.cmake +++ b/hw/bsp/nrf/boards/nrf54h20dk/board.cmake @@ -1,7 +1,8 @@ set(MCU_VARIANT nrf54h20) function(update_board TARGET) - # temporarily, 54h20 has multiple sram sections + # 32 KB primary RAM is too tight for memory-heavy examples (e.g. video YUY2 + # framebuf). TODO: route static .bss to RAM00 (512 KB) and drop this. target_compile_definitions(${TARGET} PUBLIC CFG_EXAMPLE_VIDEO_READONLY ) diff --git a/hw/bsp/nrf/boards/nrf54h20dk/board.mk b/hw/bsp/nrf/boards/nrf54h20dk/board.mk index c49b605e8..3333a76f1 100644 --- a/hw/bsp/nrf/boards/nrf54h20dk/board.mk +++ b/hw/bsp/nrf/boards/nrf54h20dk/board.mk @@ -1,6 +1,11 @@ MCU_VARIANT = nrf54h20 CFLAGS += -DNRF54H20_XXAA +# 32 KB primary RAM is too tight for memory-heavy examples (e.g. video YUY2 +# framebuf). Match the CMake build (board.cmake) — TODO: route static .bss to +# RAM00 (512 KB) and drop this. +CFLAGS += -DCFG_EXAMPLE_VIDEO_READONLY + # enable max3421 host driver for this board MAX3421_HOST = 1 diff --git a/hw/bsp/nrf/boards/nrf54lm20dk/board.cmake b/hw/bsp/nrf/boards/nrf54lm20dk/board.cmake index 8c2b83346..e97b8822c 100644 --- a/hw/bsp/nrf/boards/nrf54lm20dk/board.cmake +++ b/hw/bsp/nrf/boards/nrf54lm20dk/board.cmake @@ -2,7 +2,6 @@ set(MCU_VARIANT nrf54lm20a_enga) set(JLINK_DEVICE NRF54LM20A_M33) function(update_board TARGET) - target_compile_definitions(${TARGET} PUBLIC - CFG_EXAMPLE_VIDEO_READONLY - ) + # No board-specific overrides needed — primary 256 KB RAM is plenty for + # memory-heavy examples (video YUY2 framebuf etc.). endfunction() diff --git a/hw/bsp/pic32mz/family.c b/hw/bsp/pic32mz/family.c index 5805e653f..98038b4f7 100644 --- a/hw/bsp/pic32mz/family.c +++ b/hw/bsp/pic32mz/family.c @@ -102,7 +102,9 @@ TU_ATTR_WEAK int board_uart_read(uint8_t * buf, int len) TU_ATTR_WEAK int board_uart_write(void const * buf, int len) { (void) buf; - return len; + (void) len; + + return -1; } #if CFG_TUSB_OS == OPT_OS_NONE diff --git a/hw/bsp/rp2040/family.c b/hw/bsp/rp2040/family.c index a4642face..55feec159 100644 --- a/hw/bsp/rp2040/family.c +++ b/hw/bsp/rp2040/family.c @@ -282,7 +282,7 @@ int board_uart_write(void const *buf, int len) { return count; #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/rp2040/family.cmake b/hw/bsp/rp2040/family.cmake index 6e88b9fa1..075582554 100644 --- a/hw/bsp/rp2040/family.cmake +++ b/hw/bsp/rp2040/family.cmake @@ -93,7 +93,6 @@ target_sources(tinyusb_device_base INTERFACE ${TOP}/src/portable/raspberrypi/rp2040/dcd_rp2040.c ${TOP}/src/portable/raspberrypi/rp2040/rp2040_usb.c ${TOP}/src/device/usbd.c - ${TOP}/src/device/usbd_control.c ${TOP}/src/class/audio/audio_device.c ${TOP}/src/class/cdc/cdc_device.c ${TOP}/src/class/dfu/dfu_device.c diff --git a/hw/bsp/stm32c0/family.c b/hw/bsp/stm32c0/family.c index 72af3ce7f..ac37045fb 100644 --- a/hw/bsp/stm32c0/family.c +++ b/hw/bsp/stm32c0/family.c @@ -193,7 +193,7 @@ int board_uart_write(void const *buf, int len) { return count; #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/stm32f0/family.c b/hw/bsp/stm32f0/family.c index f413163e5..c77ef4008 100644 --- a/hw/bsp/stm32f0/family.c +++ b/hw/bsp/stm32f0/family.c @@ -206,7 +206,7 @@ int board_uart_write(void const *buf, int len) { return count; #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/stm32f1/family.c b/hw/bsp/stm32f1/family.c index 74e0f53f2..abde44d21 100644 --- a/hw/bsp/stm32f1/family.c +++ b/hw/bsp/stm32f1/family.c @@ -246,7 +246,7 @@ int board_uart_write(void const *buf, int len) { return count; #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/stm32f2/family.c b/hw/bsp/stm32f2/family.c index 051a026c5..260e5e067 100644 --- a/hw/bsp/stm32f2/family.c +++ b/hw/bsp/stm32f2/family.c @@ -210,7 +210,7 @@ int board_uart_write(void const* buf, int len) { return count; #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/stm32f3/family.c b/hw/bsp/stm32f3/family.c index 0864d0fad..35e1852e8 100644 --- a/hw/bsp/stm32f3/family.c +++ b/hw/bsp/stm32f3/family.c @@ -222,7 +222,7 @@ int board_uart_write(void const* buf, int len) { return count; #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/stm32f4/family.c b/hw/bsp/stm32f4/family.c index f4ef99150..4eea5c7a8 100644 --- a/hw/bsp/stm32f4/family.c +++ b/hw/bsp/stm32f4/family.c @@ -295,7 +295,7 @@ int board_uart_write(void const *buf, int len) { return count; #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/stm32f7/family.c b/hw/bsp/stm32f7/family.c index 078e372d8..7a322591b 100644 --- a/hw/bsp/stm32f7/family.c +++ b/hw/bsp/stm32f7/family.c @@ -357,7 +357,7 @@ int board_uart_write(const void *buf, int len) { #else (void)buf; (void)len; - return 0; + return -1; #endif } diff --git a/hw/bsp/stm32g0/family.c b/hw/bsp/stm32g0/family.c index d0ff8bac2..cb674ccc9 100644 --- a/hw/bsp/stm32g0/family.c +++ b/hw/bsp/stm32g0/family.c @@ -206,7 +206,7 @@ int board_uart_write(void const *buf, int len) { return count; #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/stm32g4/family.c b/hw/bsp/stm32g4/family.c index 98739ffd5..433f74e2a 100644 --- a/hw/bsp/stm32g4/family.c +++ b/hw/bsp/stm32g4/family.c @@ -236,7 +236,7 @@ int board_uart_write(void const *buf, int len) { return count; #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/stm32h5/family.c b/hw/bsp/stm32h5/family.c index 8298dfaab..f52f94f32 100644 --- a/hw/bsp/stm32h5/family.c +++ b/hw/bsp/stm32h5/family.c @@ -238,7 +238,7 @@ int board_uart_write(void const* buf, int len) { return count; #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/stm32h7/family.c b/hw/bsp/stm32h7/family.c index b32f73754..5173c5401 100644 --- a/hw/bsp/stm32h7/family.c +++ b/hw/bsp/stm32h7/family.c @@ -328,7 +328,7 @@ int board_uart_write(void const *buf, int len) { return count; #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/stm32h7rs/family.c b/hw/bsp/stm32h7rs/family.c index 7ae9e5532..b0841c947 100644 --- a/hw/bsp/stm32h7rs/family.c +++ b/hw/bsp/stm32h7rs/family.c @@ -490,7 +490,7 @@ int board_uart_write(void const *buf, int len) { return count; #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/stm32l0/family.c b/hw/bsp/stm32l0/family.c index 930fa2d66..a4a0ebbfe 100644 --- a/hw/bsp/stm32l0/family.c +++ b/hw/bsp/stm32l0/family.c @@ -176,7 +176,7 @@ int board_uart_write(void const* buf, int len) { #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/stm32l4/family.c b/hw/bsp/stm32l4/family.c index 7a8acb3de..96de82bd5 100644 --- a/hw/bsp/stm32l4/family.c +++ b/hw/bsp/stm32l4/family.c @@ -264,7 +264,7 @@ int board_uart_write(void const *buf, int len) { return count; #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/stm32n6/family.c b/hw/bsp/stm32n6/family.c index 95578af04..80de20c6a 100644 --- a/hw/bsp/stm32n6/family.c +++ b/hw/bsp/stm32n6/family.c @@ -388,7 +388,7 @@ int board_uart_write(void const *buf, int len) { return count; #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/stm32u0/family.c b/hw/bsp/stm32u0/family.c index 5cf6e1eb2..7bd99fba6 100644 --- a/hw/bsp/stm32u0/family.c +++ b/hw/bsp/stm32u0/family.c @@ -199,7 +199,7 @@ int board_uart_write(void const *buf, int len) { return count; #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/stm32u5/family.c b/hw/bsp/stm32u5/family.c index 41e354351..7969b1c70 100644 --- a/hw/bsp/stm32u5/family.c +++ b/hw/bsp/stm32u5/family.c @@ -304,7 +304,7 @@ int board_uart_write(void const *buf, int len) { return count; #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/stm32wb/family.c b/hw/bsp/stm32wb/family.c index d97be2115..1f1da2271 100644 --- a/hw/bsp/stm32wb/family.c +++ b/hw/bsp/stm32wb/family.c @@ -196,7 +196,7 @@ int board_uart_write(void const* buf, int len) { return count; #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/hw/bsp/tm4c/family.c b/hw/bsp/tm4c/family.c index fe4737e50..4f85be220 100644 --- a/hw/bsp/tm4c/family.c +++ b/hw/bsp/tm4c/family.c @@ -58,6 +58,14 @@ static void board_button_init(GPIOA_Type* port, uint8_t PinMsk) { /* Set direction */ port->DIR &= ~PinMsk; + + /* Enable internal pull so the idle state is deterministic. LaunchPad buttons + * connect the pin to GND when pressed (active-low) and require a pull-up. */ +#if BUTTON_STATE_ACTIVE == 0 + port->PUR |= PinMsk; +#else + port->PDR |= PinMsk; +#endif } static void board_led_init(GPIOA_Type* port, uint8_t PinMsk, uint8_t dirmsk) { diff --git a/hw/bsp/xmc4000/family.c b/hw/bsp/xmc4000/family.c index 1325b784b..7e224d092 100644 --- a/hw/bsp/xmc4000/family.c +++ b/hw/bsp/xmc4000/family.c @@ -150,7 +150,7 @@ int board_uart_write(void const* buf, int len) { #else (void) buf; (void) len; - return 0; + return -1; #endif } diff --git a/lib/networking/rndis_reports.c b/lib/networking/rndis_reports.c index 5e824d5a5..f06bc5507 100644 --- a/lib/networking/rndis_reports.c +++ b/lib/networking/rndis_reports.c @@ -36,9 +36,13 @@ #include "rndis_protocol.h" #include "netif/ethernet.h" -#define RNDIS_LINK_SPEED 12000000 /* Link baudrate (12Mbit/s for USB-FS) */ #define RNDIS_VENDOR "TinyUSB" /* NIC vendor name */ +// USB link speed in bits/sec, reflected to host via OID_GEN_LINK_SPEED. +static inline uint32_t rndis_link_speed_bps(void) { + return (tud_speed_get() == TUSB_SPEED_HIGH) ? 480000000U : 12000000U; +} + static const uint8_t *const station_hwaddr = tud_network_mac_address; static const uint8_t *const permanent_hwaddr = tud_network_mac_address; @@ -127,7 +131,7 @@ static void rndis_query(void) case OID_GEN_MEDIA_IN_USE: rndis_query_cmplt32(RNDIS_STATUS_SUCCESS, NDIS_MEDIUM_802_3); return; case OID_GEN_PHYSICAL_MEDIUM: rndis_query_cmplt32(RNDIS_STATUS_SUCCESS, NDIS_MEDIUM_802_3); return; case OID_GEN_HARDWARE_STATUS: rndis_query_cmplt32(RNDIS_STATUS_SUCCESS, 0); return; - case OID_GEN_LINK_SPEED: rndis_query_cmplt32(RNDIS_STATUS_SUCCESS, RNDIS_LINK_SPEED / 100); return; + case OID_GEN_LINK_SPEED: rndis_query_cmplt32(RNDIS_STATUS_SUCCESS, rndis_link_speed_bps() / 100U); return; case OID_GEN_VENDOR_ID: rndis_query_cmplt32(RNDIS_STATUS_SUCCESS, 0x00FFFFFF); return; case OID_GEN_VENDOR_DESCRIPTION: rndis_query_cmplt(RNDIS_STATUS_SUCCESS, rndis_vendor, strlen(rndis_vendor) + 1); return; case OID_GEN_CURRENT_PACKET_FILTER: rndis_query_cmplt32(RNDIS_STATUS_SUCCESS, oid_packet_filter); return; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 00f466007..c7a5184c5 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,7 +8,6 @@ function(tinyusb_sources_get OUTPUT_VAR) ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/common/tusb_fifo.c # device ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/device/usbd.c - ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/device/usbd_control.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/audio/audio_device.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/cdc/cdc_device.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/dfu/dfu_device.c diff --git a/src/class/hid/hid_host.c b/src/class/hid/hid_host.c index 7935b84d3..fc7704258 100644 --- a/src/class/hid/hid_host.c +++ b/src/class/hid/hid_host.c @@ -74,44 +74,31 @@ static uint8_t _hidh_default_protocol = HID_PROTOCOL_BOOT; // Weak stubs: invoked if no strong implementation is available //--------------------------------------------------------------------+ TU_ATTR_WEAK void tuh_hid_mount_cb(uint8_t dev_addr, uint8_t idx, uint8_t const* report_desc, uint16_t desc_len) { - (void) dev_addr; - (void) idx; - (void) report_desc; - (void) desc_len; + (void) dev_addr; (void) idx; (void) report_desc; (void) desc_len; } TU_ATTR_WEAK void tuh_hid_umount_cb(uint8_t dev_addr, uint8_t idx) { - (void) dev_addr; - (void) idx; + (void) dev_addr; (void) idx; +} + +TU_ATTR_WEAK void tuh_hid_report_received_cb(uint8_t dev_addr, uint8_t idx, const uint8_t *report, uint16_t len) { + (void) dev_addr; (void) idx; (void) report; (void) len; } TU_ATTR_WEAK void tuh_hid_report_sent_cb(uint8_t dev_addr, uint8_t idx, uint8_t const* report, uint16_t len) { - (void) dev_addr; - (void) idx; - (void) report; - (void) len; + (void) dev_addr; (void) idx; (void) report; (void) len; } TU_ATTR_WEAK void tuh_hid_get_report_complete_cb(uint8_t dev_addr, uint8_t idx, uint8_t report_id, uint8_t report_type, uint16_t len) { - (void) dev_addr; - (void) idx; - (void) report_id; - (void) report_type; - (void) len; + (void) dev_addr; (void) idx; (void) report_id; (void) report_type; (void) len; } TU_ATTR_WEAK void tuh_hid_set_report_complete_cb(uint8_t dev_addr, uint8_t idx, uint8_t report_id, uint8_t report_type, uint16_t len) { - (void) dev_addr; - (void) idx; - (void) report_id; - (void) report_type; - (void) len; + (void) dev_addr; (void) idx; (void) report_id; (void) report_type; (void) len; } TU_ATTR_WEAK void tuh_hid_set_protocol_complete_cb(uint8_t dev_addr, uint8_t idx, uint8_t protocol) { - (void) dev_addr; - (void) idx; - (void) protocol; + (void) dev_addr; (void) idx; (void) protocol; } //--------------------------------------------------------------------+ diff --git a/src/class/hid/hid_host.h b/src/class/hid/hid_host.h index 922848fc2..95ba859ad 100644 --- a/src/class/hid/hid_host.h +++ b/src/class/hid/hid_host.h @@ -140,7 +140,7 @@ bool tuh_hid_send_ready(uint8_t dev_addr, uint8_t idx); bool tuh_hid_send_report(uint8_t dev_addr, uint8_t idx, uint8_t report_id, const void *report, uint16_t len); //--------------------------------------------------------------------+ -// Callbacks (Weak is optional) +// Callbacks (optional) //--------------------------------------------------------------------+ // Invoked when device with hid interface is mounted diff --git a/src/class/midi/midi_host.h b/src/class/midi/midi_host.h index b9ab0130d..8fdfd8966 100644 --- a/src/class/midi/midi_host.h +++ b/src/class/midi/midi_host.h @@ -150,6 +150,13 @@ uint32_t tuh_midi_stream_write(uint8_t idx, uint8_t cable_num, const uint8_t *p_ // Note that this function ignores the CIN field of the MIDI packet // because a number of commercial devices out there do not encode // it properly. +// +// NOTE: this function terminates when it encounters an event whose cable +// number differs from the one being returned. Applications should invoke +// it in a loop until it returns 0 (or until tuh_midi_read_available() +// returns 0) to guarantee the stream FIFO is fully drained per callback. +// Leaving bytes in the FIFO across callbacks can prevent subsequent bulk +// IN transfers from landing. uint32_t tuh_midi_stream_read(uint8_t idx, uint8_t *p_cable_num, uint8_t *p_buffer, uint16_t bufsize); #endif diff --git a/src/class/net/ecm_rndis_device.c b/src/class/net/ecm_rndis_device.c index eaa82c187..643bcfbcd 100644 --- a/src/class/net/ecm_rndis_device.c +++ b/src/class/net/ecm_rndis_device.c @@ -48,9 +48,10 @@ typedef struct { uint8_t itf_num; // Index number of Management Interface, +1 for Data Interface uint8_t itf_data_alt; // Alternate setting of Data Interface. 0 : inactive, 1 : active - uint8_t ep_notif; uint8_t ep_in; uint8_t ep_out; + uint16_t ep_size; // bulk endpoint max packet size (IN and OUT assumed equal) + uint8_t ep_notif; bool ecm_mode; @@ -81,6 +82,13 @@ CFG_TUD_MEM_SECTION static netd_epbuf_t _netd_epbuf; static bool can_xmit; static bool ecm_link_is_up = true; // Store link state for ECM mode +//--------------------------------------------------------------------+ +// Weak stubs: invoked if no strong implementation is available +//--------------------------------------------------------------------+ +TU_ATTR_WEAK void tud_network_set_packet_filter_cb(uint16_t packet_filter) { + (void) packet_filter; +} + void tud_network_recv_renew(void) { usbd_edpt_xfer(0, _netd_itf.ep_out, _netd_epbuf.rx, NETD_PACKET_SIZE, false); } @@ -176,6 +184,9 @@ uint16_t netd_open(uint8_t rhport, tusb_desc_interface_t const * itf_desc, uint1 // Pair of endpoints TU_ASSERT(TUSB_DESC_ENDPOINT == tu_desc_type(p_desc), 0); + // Save the actual bulk endpoint size (IN and OUT assumed equal) + _netd_itf.ep_size = tu_edpt_packet_size((tusb_desc_endpoint_t const *) p_desc); + if (_netd_itf.ecm_mode) { // ECM by default is in-active, save the endpoint attribute // to open later when received setInterface @@ -206,14 +217,15 @@ static void ecm_report(bool nc) { }, }; + const uint32_t link_bps = (tud_speed_get() == TUSB_SPEED_HIGH) ? 480000000U : 12000000U; const ecm_notify_t ecm_notify_csc = { .header = { .bmRequestType = 0xA1, .bRequest = 0x2A, /* CONNECTION_SPEED_CHANGE aka ConnectionSpeedChange */ .wLength = 8, }, - .downlink = 9728000, - .uplink = 9728000, + .downlink = link_bps, + .uplink = link_bps, }; ecm_notify_t notify = (nc) ? ecm_notify_nc : ecm_notify_csc; @@ -285,6 +297,7 @@ bool netd_control_xfer_cb (uint8_t rhport, uint8_t stage, tusb_control_request_t if (_netd_itf.ecm_mode) { /* the only required CDC-ECM Management Element Request is SetEthernetPacketFilter */ if (0x43 /* SET_ETHERNET_PACKET_FILTER */ == request->bRequest) { + tud_network_set_packet_filter_cb(request->wValue); tud_control_xfer(rhport, request, NULL, 0); // Only send connection notification if link is up if (ecm_link_is_up) { @@ -356,8 +369,7 @@ bool netd_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_ /* data transmission finished */ if (ep_addr == _netd_itf.ep_in) { /* TinyUSB requires the class driver to implement ZLP (since ZLP usage is class-specific) */ - - if (xferred_bytes && (0 == (xferred_bytes % CFG_TUD_NET_ENDPOINT_SIZE))) { + if (xferred_bytes > 0 && 0 == (xferred_bytes & (_netd_itf.ep_size-1))) { do_in_xfer(NULL, 0); /* a ZLP is needed */ } else { /* we're finally finished */ diff --git a/src/class/net/ncm.h b/src/class/net/ncm.h index 8989fe0b4..27ff89b72 100644 --- a/src/class/net/ncm.h +++ b/src/class/net/ncm.h @@ -161,4 +161,18 @@ typedef struct { uint32_t uplink; } ncm_notify_t; +typedef struct TU_ATTR_PACKED { + uint8_t bFunctionLength; + uint8_t bDescriptorType; + uint8_t bDescriptorSubType; + uint16_t bcdNcmVersion; + uint8_t bmCapabilities; +} tusb_desc_cdc_ncm_func_t; + +typedef struct TU_ATTR_PACKED { + uint32_t dwNtbInMaxSize; + uint16_t wNtbInMaxDatagrams; + uint16_t wReserved; +} ncm_ntb_input_size_t; + #endif diff --git a/src/class/net/ncm_device.c b/src/class/net/ncm_device.c index 405e4467b..a78e472c2 100644 --- a/src/class/net/ncm_device.c +++ b/src/class/net/ncm_device.c @@ -83,6 +83,7 @@ typedef struct { uint8_t itf_num; // interface number uint8_t itf_data_alt; // ==0 -> no endpoints, i.e. no network traffic, ==1 -> normal operation with two endpoints (spec, chapter 5.3) uint8_t rhport; // storage of \a rhport because some callbacks are done without it + uint16_t ep_size; // bulk endpoint max packet size (IN and OUT assumed equal) // recv handling recv_ntb_t *recv_free_ntb[RECV_NTB_N]; // free list of recv NTBs @@ -118,6 +119,12 @@ typedef struct { bool notification_xmit_is_running; // notification is currently transmitted bool link_is_up; // current link state + // host-configured transmit limits + uint8_t bm_capabilities; + uint16_t xmit_max_ntb_size; // maximum NTB size device may send + uint16_t xmit_max_datagrams; // maximum datagrams per NTB device may send + ncm_ntb_input_size_t ntb_input_size; + // misc bool tud_network_recv_renew_active; // tud_network_recv_renew() is active (avoid recursive invocations) bool tud_network_recv_renew_process_again; // tud_network_recv_renew() should process again @@ -138,6 +145,13 @@ typedef struct { static ncm_interface_t ncm_interface; CFG_TUD_MEM_SECTION static ncm_epbuf_t ncm_epbuf; +//--------------------------------------------------------------------+ +// Weak stubs: invoked if no strong implementation is available +//--------------------------------------------------------------------+ +TU_ATTR_WEAK void tud_network_set_packet_filter_cb(uint16_t packet_filter) { + (void) packet_filter; +} + /** * This is the NTB parameter structure * @@ -340,7 +354,8 @@ static xmit_ntb_t *xmit_get_next_ready_ntb(void) { static bool xmit_insert_required_zlp(uint8_t rhport, uint32_t xferred_bytes) { TU_LOG_DRV("xmit_insert_required_zlp(%d,%ld)\n", rhport, xferred_bytes); - if (xferred_bytes == 0 || xferred_bytes % CFG_TUD_NET_ENDPOINT_SIZE != 0) { + uint16_t const ep_size = ncm_interface.ep_size; + if (xferred_bytes == 0 || (xferred_bytes & (ep_size-1)) != 0) { return false; } @@ -408,10 +423,10 @@ static bool xmit_requested_datagram_fits_into_current_ntb(uint16_t datagram_size if (ncm_interface.xmit_glue_ntb == NULL) { return false; } - if (ncm_interface.xmit_glue_ntb_datagram_ndx >= CFG_TUD_NCM_IN_MAX_DATAGRAMS_PER_NTB) { + if (ncm_interface.xmit_glue_ntb_datagram_ndx >= ncm_interface.xmit_max_datagrams) { return false; } - if (ncm_interface.xmit_glue_ntb->nth.wBlockLength + datagram_size + XMIT_ALIGN_OFFSET(datagram_size) > CFG_TUD_NCM_IN_NTB_MAX_SIZE) { + if (ncm_interface.xmit_glue_ntb->nth.wBlockLength + datagram_size + (uint32_t)XMIT_ALIGN_OFFSET(datagram_size) > (uint32_t)ncm_interface.xmit_max_ntb_size) { return false; } return true; @@ -708,7 +723,7 @@ static void recv_transfer_datagram_to_glue_logic(void) { bool tud_network_can_xmit(uint16_t size) { TU_LOG_DRV("tud_network_can_xmit(%d)\n", size); - TU_ASSERT(size <= CFG_TUD_NCM_IN_NTB_MAX_SIZE - (sizeof(nth16_t) + sizeof(ndp16_t) + 2 * sizeof(ndp16_datagram_t)), false); + TU_ASSERT(size <= ncm_interface.xmit_max_ntb_size - (sizeof(nth16_t) + sizeof(ndp16_t) + 2 * sizeof(ndp16_datagram_t)), false); if (xmit_requested_datagram_fits_into_current_ntb(size) || xmit_setup_next_glue_ntb()) { // -> everything is fine @@ -828,6 +843,9 @@ void netd_init(void) { memset(&ncm_interface, 0, sizeof(ncm_interface)); + ncm_interface.xmit_max_ntb_size = CFG_TUD_NCM_IN_NTB_MAX_SIZE; + ncm_interface.xmit_max_datagrams = CFG_TUD_NCM_IN_MAX_DATAGRAMS_PER_NTB; + for (int i = 0; i < XMIT_NTB_N; ++i) { ncm_interface.xmit_free_ntb[i] = &ncm_epbuf.xmit[i].ntb; } @@ -878,10 +896,14 @@ uint16_t netd_open(uint8_t rhport, tusb_desc_interface_t const *itf_desc, uint16 ncm_interface.itf_num = itf_desc->bInterfaceNumber;// management interface - // skip the two first entries and the following TUSB_DESC_CS_INTERFACE entries uint16_t drv_len = sizeof(tusb_desc_interface_t); uint8_t const *p_desc = tu_desc_next(itf_desc); while (tu_desc_type(p_desc) == TUSB_DESC_CS_INTERFACE && drv_len <= max_len) { + if (tu_desc_subtype(p_desc) == CDC_FUNC_DESC_NCM) { + TU_ASSERT(tu_desc_len(p_desc) >= sizeof(tusb_desc_cdc_ncm_func_t), 0); + tusb_desc_cdc_ncm_func_t const *ncm_func = (tusb_desc_cdc_ncm_func_t const *) p_desc; + ncm_interface.bm_capabilities = ncm_func->bmCapabilities; + } drv_len += tu_desc_len(p_desc); p_desc = tu_desc_next(p_desc); } @@ -905,6 +927,7 @@ uint16_t netd_open(uint8_t rhport, tusb_desc_interface_t const *itf_desc, uint16 // a TUSB_DESC_ENDPOINT (actually two) must follow, open these endpoints TU_ASSERT(tu_desc_type(p_desc) == TUSB_DESC_ENDPOINT, 0); TU_ASSERT(usbd_open_edpt_pair(rhport, p_desc, 2, TUSB_XFER_BULK, &ncm_interface.ep_out, &ncm_interface.ep_in)); + ncm_interface.ep_size = tu_edpt_packet_size((tusb_desc_endpoint_t const *) p_desc); drv_len += 2 * sizeof(tusb_desc_endpoint_t); return drv_len; @@ -954,12 +977,12 @@ bool netd_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_ * At startup transmission of notification packets are done here. */ bool netd_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t const *request) { - if (stage != CONTROL_STAGE_SETUP) { - return true; - } switch (request->bmRequestType_bit.type) { case TUSB_REQ_TYPE_STANDARD: + if (stage != CONTROL_STAGE_SETUP) { + return true; + } switch (request->bRequest) { case TUSB_REQ_GET_INTERFACE: { @@ -993,16 +1016,75 @@ bool netd_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_request_t TU_VERIFY(ncm_interface.itf_num == request->wIndex, false); switch (request->bRequest) { case NCM_GET_NTB_PARAMETERS: { + if (stage != CONTROL_STAGE_SETUP) { + return true; + } // transfer NTB parameters to host. tud_control_xfer(rhport, request, (void *) (uintptr_t) &ntb_parameters, sizeof(ntb_parameters)); } break; - // unsupported request + case NCM_SET_ETHERNET_PACKET_FILTER: { + if (stage != CONTROL_STAGE_SETUP) { + return true; + } + + // Some hosts issue this request even if ETH_FILTER is not advertised, + // see https://bugzilla.kernel.org/show_bug.cgi?id=217290 + + tud_network_set_packet_filter_cb(request->wValue); + tud_control_xfer(rhport, request, NULL, 0); + } break; + + case NCM_GET_NTB_INPUT_SIZE: { + if (stage != CONTROL_STAGE_SETUP) { + return true; + } + + TU_VERIFY(request->wLength >=4, false); + + uint8_t resp_len = (request->wLength >= 8 && (ncm_interface.bm_capabilities & NCM_NETWORK_CAPS_NTB_INPUT_SIZE)) ? 8 : 4; + + ncm_ntb_input_size_t ntb_input_size = { + .dwNtbInMaxSize = ncm_interface.xmit_max_ntb_size, + .wNtbInMaxDatagrams = ncm_interface.xmit_max_datagrams + }; + tud_control_xfer(rhport, request, &ntb_input_size, resp_len); + } break; + + case NCM_SET_NTB_INPUT_SIZE: { + if (stage == CONTROL_STAGE_SETUP) { + /* wLength == 8 -> the NTB Input Size Structure (if NCM_NETWORK_CAPS_NTB_INPUT_SIZE is set) + wLength == 4 -> dwNtbInMaxSize field of the NTB Input Size Structure. */ + TU_VERIFY(request->wLength == 4 || request->wLength == 8, false); + if (request->wLength == 8) { + TU_VERIFY(ncm_interface.bm_capabilities & NCM_NETWORK_CAPS_NTB_INPUT_SIZE, false); + } + + tu_memclr(&ncm_interface.ntb_input_size, sizeof(ncm_interface.ntb_input_size)); + tud_control_xfer(rhport, request, &ncm_interface.ntb_input_size, request->wLength); + } else if (stage == CONTROL_STAGE_DATA) { + /* CDC-NCM 1.0 Table 6-4, up to NTB16 size */ + const uint32_t requested_size = ncm_interface.ntb_input_size.dwNtbInMaxSize; + if (requested_size < 2048u || requested_size > 65535u) { + return false; + } + ncm_interface.xmit_max_ntb_size = tu_min16(requested_size, CFG_TUD_NCM_IN_NTB_MAX_SIZE); + + if (ncm_interface.ntb_input_size.wNtbInMaxDatagrams == 0 || ncm_interface.ntb_input_size.wNtbInMaxDatagrams > CFG_TUD_NCM_IN_MAX_DATAGRAMS_PER_NTB) { + ncm_interface.xmit_max_datagrams = CFG_TUD_NCM_IN_MAX_DATAGRAMS_PER_NTB; + } else { + ncm_interface.xmit_max_datagrams = ncm_interface.ntb_input_size.wNtbInMaxDatagrams; + } + } + } break; + + // unsupported request default: return false; } break; - // unsupported request + + // unsupported request default: return false; } diff --git a/src/class/net/net_device.h b/src/class/net/net_device.h index 96c03fd61..332df09b3 100644 --- a/src/class/net/net_device.h +++ b/src/class/net/net_device.h @@ -35,9 +35,6 @@ #error "Cannot enable both ECM_RNDIS and NCM network drivers" #endif -/* declared here, NOT in usb_descriptors.c, so that the driver can intelligently ZLP as needed */ -#define CFG_TUD_NET_ENDPOINT_SIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) - /* Maximum Transmission Unit (in bytes) of the network, including Ethernet header */ #ifndef CFG_TUD_NET_MTU #define CFG_TUD_NET_MTU 1514 @@ -50,6 +47,16 @@ typedef enum NCM_DATA_PROTOCOL_NETWORK_TRANSFER_BLOCK = 0x01 } ncm_data_interface_protocol_code_t; +// Table 5.2 bmNetworkCapabilities bits +typedef enum { + NCM_NETWORK_CAPS_NONE = 0x00, + NCM_NETWORK_CAPS_ETH_FILTER = (1 << 0), + NCM_NETWORK_CAPS_NET_ADDRESS = (1 << 1), + NCM_NETWORK_CAPS_ENCAP_COMMAND = (1 << 2), + NCM_NETWORK_CAPS_MAX_DATAGRAM_SIZE = (1 << 3), + NCM_NETWORK_CAPS_CRC_MODE = (1 << 4), + NCM_NETWORK_CAPS_NTB_INPUT_SIZE = (1 << 5) +} ncm_network_capabilities_t; #ifdef __cplusplus extern "C" { @@ -96,6 +103,9 @@ extern uint8_t tud_network_mac_address[6]; //------------- NCM -------------// +// Optional callback: informs the application about host requested packet filter bits +void tud_network_set_packet_filter_cb(uint16_t packet_filter); + // Set the network link state (up/down) and notify the host void tud_network_link_state(uint8_t rhport, bool is_up); diff --git a/src/class/printer/printer_device.c b/src/class/printer/printer_device.c index d2dc9b163..158455fc9 100644 --- a/src/class/printer/printer_device.c +++ b/src/class/printer/printer_device.c @@ -41,7 +41,6 @@ typedef struct { uint8_t itf_num; /*------------- From this point, data is not cleared by bus reset -------------*/ - tu_edpt_stream_t rx_stream; tu_edpt_stream_t tx_stream; diff --git a/src/common/tusb_mcu.h b/src/common/tusb_mcu.h index a17c76a3b..c85ade4d0 100644 --- a/src/common/tusb_mcu.h +++ b/src/common/tusb_mcu.h @@ -177,12 +177,12 @@ #elif TU_CHECK_MCU(OPT_MCU_SAMG) #define TUP_DCD_ENDPOINT_MAX 6 - #define TUD_ENDPOINT_ONE_DIRECTION_ONLY + #define CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY 1 #elif TU_CHECK_MCU(OPT_MCU_SAMX7X) #define TUP_DCD_ENDPOINT_MAX 10 #define TUP_RHPORT_HIGHSPEED 1 - #define TUD_ENDPOINT_ONE_DIRECTION_ONLY + #define CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY 1 // Enable dcache if DMA is enabled #define CFG_TUD_MEM_DCACHE_ENABLE_DEFAULT CFG_TUD_SAMX7X_DMA_ENABLE @@ -190,11 +190,11 @@ #elif TU_CHECK_MCU(OPT_MCU_PIC32MZ) #define TUP_DCD_ENDPOINT_MAX 8 - #define TUD_ENDPOINT_ONE_DIRECTION_ONLY + #define CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY 1 #elif TU_CHECK_MCU(OPT_MCU_PIC32MX, OPT_MCU_PIC32MM, OPT_MCU_PIC32MK) || TU_CHECK_MCU(OPT_MCU_PIC24, OPT_MCU_DSPIC33) #define TUP_DCD_ENDPOINT_MAX 16 - #define TUD_ENDPOINT_ONE_DIRECTION_ONLY + #define CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY 1 #define TUP_DCD_EDPT_CLOSE_API //--------------------------------------------------------------------+ @@ -411,7 +411,7 @@ #elif TU_CHECK_MCU(OPT_MCU_CXD56) #define TUP_DCD_ENDPOINT_MAX 7 #define TUP_RHPORT_HIGHSPEED 1 - #define TUD_ENDPOINT_ONE_DIRECTION_ONLY + #define CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY 1 //--------------------------------------------------------------------+ // TI @@ -563,12 +563,12 @@ #elif TU_CHECK_MCU(OPT_MCU_FT90X) #define TUP_DCD_ENDPOINT_MAX 8 #define TUP_RHPORT_HIGHSPEED 1 - #define TUD_ENDPOINT_ONE_DIRECTION_ONLY + #define CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY 1 #elif TU_CHECK_MCU(OPT_MCU_FT93X) #define TUP_DCD_ENDPOINT_MAX 16 #define TUP_RHPORT_HIGHSPEED 1 - #define TUD_ENDPOINT_ONE_DIRECTION_ONLY + #define CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY 1 //--------------------------------------------------------------------+ // Allwinner @@ -659,7 +659,7 @@ #define TUP_USBIP_MUSB_ADI #define TUP_DCD_ENDPOINT_MAX 12 #define TUP_RHPORT_HIGHSPEED 1 - #define TUD_ENDPOINT_ONE_DIRECTION_ONLY + #define CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY 1 //--------------------------------------------------------------------+ // ArteryTek @@ -743,3 +743,14 @@ #ifndef TUP_DCD_EDPT_CLOSE_API #define TUP_DCD_EDPT_ISO_ALLOC #endif + +// Some USBIPs (SAMG, SAMX7X, PIC32, MAX3266x/MAX78002) cannot assign the same endpoint +// number to both IN and OUT. Default to 0 (same endpoint number may be used for IN and OUT). +#ifndef CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY + #define CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY 0 +#endif + +// Backward-compatible alias: legacy code only tests defined(TUD_ENDPOINT_ONE_DIRECTION_ONLY) +#if CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY && !defined(TUD_ENDPOINT_ONE_DIRECTION_ONLY) + #define TUD_ENDPOINT_ONE_DIRECTION_ONLY +#endif diff --git a/src/common/tusb_private.h b/src/common/tusb_private.h index 91d213755..a31bf7b03 100644 --- a/src/common/tusb_private.h +++ b/src/common/tusb_private.h @@ -46,17 +46,10 @@ extern tusb_role_t _tusb_rhport_role[TUP_USBIP_CONTROLLER_NUM]; // Endpoint //--------------------------------------------------------------------+ -enum { - TU_EDPT_STATE_BUSY = 0x01, - TU_EDPT_STATE_STALLED = 0x02, - TU_EDPT_STATE_CLAIMED = 0x04, -}; - -typedef struct TU_ATTR_PACKED { - volatile uint8_t busy : 1; - volatile uint8_t stalled : 1; - volatile uint8_t claimed : 1; -} tu_edpt_state_t; +// Endpoint state bits — manipulate the bare uint8_t with these masks. +#define TU_EDPT_STATE_BUSY 0x01u +#define TU_EDPT_STATE_STALLED 0x02u +#define TU_EDPT_STATE_CLAIMED 0x04u typedef struct { uint8_t hwid; // device: rhport, host: daddr @@ -92,10 +85,10 @@ bool tu_bind_driver_to_ep_itf(uint8_t driver_id, uint8_t ep2drv[][2], uint8_t it const uint8_t *p_desc, uint16_t desc_len); // Claim an endpoint with provided mutex -bool tu_edpt_claim(tu_edpt_state_t* ep_state, osal_mutex_t mutex); +bool tu_edpt_claim(volatile uint8_t* ep_state, osal_mutex_t mutex); // Release an endpoint with provided mutex -bool tu_edpt_release(tu_edpt_state_t* ep_state, osal_mutex_t mutex); +bool tu_edpt_release(volatile uint8_t* ep_state, osal_mutex_t mutex); //--------------------------------------------------------------------+ // Endpoint Stream diff --git a/src/common/tusb_types.h b/src/common/tusb_types.h index 8ae1407f8..959fc129a 100644 --- a/src/common/tusb_types.h +++ b/src/common/tusb_types.h @@ -102,10 +102,12 @@ typedef enum { typedef enum { TUSB_DIR_OUT = 0u, TUSB_DIR_IN = 1u, +} tusb_dir_t; - TUSB_EPNUM_MASK = 0x0F, +enum { + TUSB_EPNUM_MASK = 0x0F, TUSB_DIR_IN_MASK = 0x80 -} tusb_dir_t; +}; enum { TUSB_EPSIZE_BULK_FS = 64, @@ -319,6 +321,12 @@ enum { TUSB_INDEX_INVALID_8 = 0xFF }; +enum { + TU_EP0_OUT = 0x00, + TU_EP0_IN = 0x80 +}; + + //--------------------------------------------------------------------+ // //--------------------------------------------------------------------+ diff --git a/src/device/usbd.c b/src/device/usbd.c index da0ffb4c6..0e58f70bf 100644 --- a/src/device/usbd.c +++ b/src/device/usbd.c @@ -115,7 +115,20 @@ TU_ATTR_WEAK bool dcd_dcache_clean_invalidate(const void* addr, uint32_t data_si //--------------------------------------------------------------------+ // Device Data //--------------------------------------------------------------------+ + +// Per-control-transfer state: populated at process_setup_received() entry, +// consumed asynchronously by usbd_control_xfer_cb() when the EP0 transfer completes. +typedef struct { + tusb_control_request_t request; + uint8_t* buffer; + uint16_t data_len; + uint16_t total_xferred; + usbd_control_xfer_cb_t complete_cb; +} usbd_control_xfer_t; + typedef struct { + usbd_control_xfer_t ctrl_xfer; + // Note: these may share an enum state volatile uint8_t connected; volatile uint8_t addressed; @@ -136,12 +149,16 @@ typedef struct { uint8_t itf2drv[CFG_TUD_INTERFACE_MAX]; // map interface number to driver (0xff is invalid) uint8_t ep2drv[CFG_TUD_ENDPPOINT_MAX][2]; // map endpoint to driver ( 0xff is invalid ), can use only 4-bit each - tu_edpt_state_t ep_status[CFG_TUD_ENDPPOINT_MAX][2]; + volatile uint8_t ep_status[CFG_TUD_ENDPPOINT_MAX][2]; } usbd_device_t; static usbd_device_t _usbd_dev; static volatile uint8_t _usbd_queued_setup; +CFG_TUD_MEM_SECTION static struct { + TUD_EPBUF_DEF(buf, CFG_TUD_ENDPOINT0_BUFSIZE); +} _ctrl_epbuf; + //--------------------------------------------------------------------+ // Class Driver //--------------------------------------------------------------------+ @@ -405,7 +422,8 @@ TU_ATTR_ALWAYS_INLINE static inline bool queue_event(dcd_event_t const * event, //--------------------------------------------------------------------+ // Prototypes //--------------------------------------------------------------------+ -static bool process_control_request(uint8_t rhport, tusb_control_request_t const * p_request); +static bool usbd_control_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes); +static bool process_setup_received(uint8_t rhport, tusb_control_request_t const * p_request); static bool process_set_config(uint8_t rhport, uint8_t cfg_num); static bool process_get_descriptor(uint8_t rhport, tusb_control_request_t const * p_request); @@ -419,12 +437,6 @@ static bool process_test_mode_cb(uint8_t rhport, uint8_t stage, tusb_control_req } #endif -// from usbd_control.c -void usbd_control_reset(void); -void usbd_control_set_request(tusb_control_request_t const *request); -void usbd_control_set_complete_callback( usbd_control_xfer_cb_t fp ); -bool usbd_control_xfer_cb (uint8_t rhport, uint8_t ep_addr, xfer_result_t event, uint32_t xferred_bytes); - //--------------------------------------------------------------------+ // Weak stubs: invoked if no strong implementation is available //--------------------------------------------------------------------+ @@ -459,17 +471,6 @@ static char const *const _usbd_event_str[DCD_EVENT_COUNT] = { "Func Call" }; -// for usbd_control to print the name of control complete driver -void usbd_driver_print_control_complete_name(usbd_control_xfer_cb_t callback) { - for (uint8_t i = 0; i < TOTAL_DRIVER_COUNT; i++) { - usbd_class_driver_t const* driver = get_driver(i); - if (driver && driver->control_xfer_cb == callback) { - TU_LOG_USBD("%s control complete\r\n", driver->name); - return; - } - } -} - #endif //--------------------------------------------------------------------+ @@ -609,9 +610,7 @@ bool tud_deinit(uint8_t rhport) { } } - // Clear device data - tu_varclr(&_usbd_dev); - usbd_control_reset(); + tu_varclr(&_usbd_dev); // Clear device data // Deinit device queue & task osal_queue_delete(_usbd_q); @@ -646,7 +645,6 @@ static void configuration_reset(uint8_t rhport) { static void usbd_reset(uint8_t rhport) { configuration_reset(rhport); - usbd_control_reset(); } bool tud_task_event_ready(void) { @@ -713,7 +711,9 @@ void tud_task_ext(uint32_t timeout_ms, bool in_isr) { break; case DCD_EVENT_SETUP_RECEIVED: - TU_ASSERT(_usbd_queued_setup > 0,); + if (_usbd_queued_setup == 0) { + break; + } _usbd_queued_setup--; TU_LOG_BUF(CFG_TUD_LOG_LEVEL, &event.setup_received, 8); if (_usbd_queued_setup != 0) { @@ -725,18 +725,16 @@ void tud_task_ext(uint32_t timeout_ms, bool in_isr) { // But it is easier to set it every time instead of wasting time to check then set _usbd_dev.connected = 1; - // mark both in & out control as free - _usbd_dev.ep_status[0][TUSB_DIR_OUT].busy = 0; - _usbd_dev.ep_status[0][TUSB_DIR_OUT].claimed = 0; - _usbd_dev.ep_status[0][TUSB_DIR_IN].busy = 0; - _usbd_dev.ep_status[0][TUSB_DIR_IN].claimed = 0; + // reset ep state + _usbd_dev.ep_status[0][TUSB_DIR_OUT] = 0; + _usbd_dev.ep_status[0][TUSB_DIR_IN] = 0; // Process control request - if (!process_control_request(event.rhport, &event.setup_received)) { + if (!process_setup_received(event.rhport, &event.setup_received)) { TU_LOG_USBD(" Stall EP0\r\n"); // Failed -> stall both control endpoint IN and OUT - dcd_edpt_stall(event.rhport, 0); - dcd_edpt_stall(event.rhport, 0 | TUSB_DIR_IN_MASK); + dcd_edpt_stall(event.rhport, TU_EP0_OUT); + dcd_edpt_stall(event.rhport, TU_EP0_IN); } break; @@ -748,8 +746,8 @@ void tud_task_ext(uint32_t timeout_ms, bool in_isr) { TU_LOG_USBD("on EP %02X with %u bytes\r\n", ep_addr, (unsigned int) event.xfer_complete.len); - _usbd_dev.ep_status[epnum][ep_dir].busy = 0; - _usbd_dev.ep_status[epnum][ep_dir].claimed = 0; + // Clear busy + claimed + _usbd_dev.ep_status[epnum][ep_dir] &= (uint8_t) ~(TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED); if (0 == epnum) { usbd_control_xfer_cb(event.rhport, ep_addr, (xfer_result_t) event.xfer_complete.result, event.xfer_complete.len); @@ -809,25 +807,273 @@ void tud_task_ext(uint32_t timeout_ms, bool in_isr) { } //--------------------------------------------------------------------+ +// Control Endpoint +//--------------------------------------------------------------------+ + +// Weak hook: invoked when the control transfer's status stage completes +TU_ATTR_WEAK void dcd_edpt0_status_complete(uint8_t rhport, const tusb_control_request_t* request) { + (void) rhport; + (void) request; +} + +uint8_t* usbd_get_ctrl_buf(void) { + return _ctrl_epbuf.buf; +} + +// Endpoint used for the Status stage of a control transfer. +// Per USB 2.0 §9.3.1, when wLength == 0 the Direction bit is ignored and the Status +// stage is always IN. Otherwise the Status stage is opposite of the Data stage direction. +TU_ATTR_ALWAYS_INLINE static inline uint8_t status_stage_ep(const tusb_control_request_t* request) { + return (request->wLength != 0 && request->bmRequestType_bit.direction) ? TU_EP0_OUT : TU_EP0_IN; +} + +// Queue ZLP status transaction +TU_ATTR_ALWAYS_INLINE static inline bool status_stage_xact(uint8_t rhport, uint8_t ep_status) { + return usbd_edpt_xfer(rhport, ep_status, NULL, 0, false); +} + +// Queue a transaction in Data Stage. Each transaction has up to Endpoint0's max +// packet size. This function can also transfer a zero-length packet. +static bool data_stage_xact(uint8_t rhport) { + usbd_control_xfer_t* const ctrl_xfer = &_usbd_dev.ctrl_xfer; + const uint16_t xact_len = tu_min16(ctrl_xfer->data_len - ctrl_xfer->total_xferred, CFG_TUD_ENDPOINT0_BUFSIZE); + uint8_t ep_addr = TU_EP0_OUT; + + if (ctrl_xfer->request.bmRequestType_bit.direction == TUSB_DIR_IN) { + ep_addr = TU_EP0_IN; + if (0u != xact_len && ctrl_xfer->buffer != _ctrl_epbuf.buf) { + TU_VERIFY(0 == tu_memcpy_s(_ctrl_epbuf.buf, CFG_TUD_ENDPOINT0_BUFSIZE, ctrl_xfer->buffer, xact_len)); + } + } + + return usbd_edpt_xfer(rhport, ep_addr, xact_len ? _ctrl_epbuf.buf : NULL, xact_len, false); +} + +// Status phase +bool tud_control_status(uint8_t rhport, const tusb_control_request_t* request) { + // _usbd_dev.ctrl_xfer fields are pre-initialized at process_setup_received entry + (void) request; + return status_stage_xact(rhport, status_stage_ep(&_usbd_dev.ctrl_xfer.request)); +} + +// Transmit data to/from the control endpoint. If wLength is zero, a status packet is sent instead. +bool tud_control_xfer(uint8_t rhport, const tusb_control_request_t* request, void* buffer, uint16_t len) { + // _usbd_dev.ctrl_xfer.request and reset fields are pre-initialized at process_setup_received entry + (void) request; + usbd_control_xfer_t* const ctrl_xfer = &_usbd_dev.ctrl_xfer; + ctrl_xfer->buffer = (uint8_t*) buffer; + ctrl_xfer->data_len = tu_min16(len, ctrl_xfer->request.wLength); + + if (ctrl_xfer->request.wLength > 0U) { + if (ctrl_xfer->data_len > 0U) { + TU_ASSERT(buffer); + } + TU_ASSERT(data_stage_xact(rhport)); + } else { + // wLength == 0: Status stage is always IN per USB 2.0 §9.3.1 + TU_ASSERT(status_stage_xact(rhport, TU_EP0_IN)); + } + + return true; +} + +// Callback when a transaction completes on the DATA stage or Status stage of EP0 +static bool usbd_control_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { + (void) result; + usbd_control_xfer_t* const ctrl_xfer = &_usbd_dev.ctrl_xfer; + + // Status Stage complete: ep_addr matches the resolved Status stage endpoint + uint8_t const ep_status = status_stage_ep(&ctrl_xfer->request); + if (ep_addr == ep_status) { + TU_ASSERT(0 == xferred_bytes); + + // invoke optional dcd hook if available + dcd_edpt0_status_complete(rhport, &ctrl_xfer->request); + + if (NULL != ctrl_xfer->complete_cb) { + ctrl_xfer->complete_cb(rhport, CONTROL_STAGE_ACK, &ctrl_xfer->request); + } + + return true; + } + + // Data stage progress + if (ctrl_xfer->request.bmRequestType_bit.direction == TUSB_DIR_OUT) { + TU_VERIFY(ctrl_xfer->buffer); + if (ctrl_xfer->buffer != _ctrl_epbuf.buf) { + memcpy(ctrl_xfer->buffer, _ctrl_epbuf.buf, xferred_bytes); + } + TU_LOG_MEM(CFG_TUD_LOG_LEVEL, ctrl_xfer->buffer, xferred_bytes, 2); + } + + ctrl_xfer->total_xferred += (uint16_t) xferred_bytes; + ctrl_xfer->buffer += xferred_bytes; + + // Data Stage complete when wLength reached or short packet (incl. ZLP) seen + if ((ctrl_xfer->request.wLength == ctrl_xfer->total_xferred) || + (xferred_bytes < CFG_TUD_ENDPOINT0_BUFSIZE)) { + bool is_ok = true; + + if (NULL != ctrl_xfer->complete_cb) { + // Callback can still stall control in status phase, e.g. OUT data doesn't make sense + is_ok = ctrl_xfer->complete_cb(rhport, CONTROL_STAGE_DATA, &ctrl_xfer->request); + } + + if (is_ok) { + TU_ASSERT(status_stage_xact(rhport, ep_status)); + } else { + // Stall both IN and OUT control endpoint + dcd_edpt_stall(rhport, TU_EP0_OUT); + dcd_edpt_stall(rhport, TU_EP0_IN); + } + } else { + // More data to transfer + TU_ASSERT(data_stage_xact(rhport)); + } + + return true; +} + +//--------------------------------------------------------------------+ // Control Request Parser & Handling //--------------------------------------------------------------------+ // Helper to invoke class driver control request handler static bool invoke_class_control(uint8_t rhport, usbd_class_driver_t const * driver, tusb_control_request_t const * request) { - usbd_control_set_complete_callback(driver->control_xfer_cb); + _usbd_dev.ctrl_xfer.complete_cb = driver->control_xfer_cb; TU_LOG_USBD(" %s control request\r\n", driver->name); return driver->control_xfer_cb(rhport, CONTROL_STAGE_SETUP, request); } +// Process a standard request to the device recipient. +static bool process_std_device_request(uint8_t rhport, tusb_control_request_t const * p_request) { + switch (p_request->bRequest) { //-V2520 + case TUSB_REQ_SET_ADDRESS: + // Depending on mcu, status phase could be sent either before or after changing device address, + // or even require stack to not response with status at all + // Therefore DCD must take full responsibility to response and include zlp status packet if needed. + dcd_set_address(rhport, (uint8_t) p_request->wValue); + _usbd_dev.addressed = 1; + return true; + + case TUSB_REQ_GET_CONFIGURATION: { + uint8_t cfg_num = _usbd_dev.cfg_num; + tud_control_xfer(rhport, p_request, &cfg_num, 1); + return true; + } + + case TUSB_REQ_SET_CONFIGURATION: { + uint8_t const cfg_num = (uint8_t) p_request->wValue; + + // Only process if new configure is different + if (_usbd_dev.cfg_num != cfg_num) { + if (_usbd_dev.cfg_num != 0) { + // already configured: need to clear all endpoints and driver first + TU_LOG_USBD(" Clear current Configuration (%u) before switching\r\n", _usbd_dev.cfg_num); + + dcd_sof_enable(rhport, false); + dcd_edpt_close_all(rhport); + + // close all drivers and current configured state except bus speed + const uint8_t speed = _usbd_dev.speed; + configuration_reset(rhport); + + _usbd_dev.speed = speed; // restore speed + } + + _usbd_dev.cfg_num = cfg_num; + + // Handle the new configuration + if (cfg_num == 0) { + tud_umount_cb(); + } else { + if (!process_set_config(rhport, cfg_num)) { + _usbd_dev.cfg_num = 0; + TU_ASSERT(false); + } + tud_mount_cb(); + } + } + + tud_control_status(rhport, p_request); + return true; + } + + case TUSB_REQ_GET_DESCRIPTOR: + return process_get_descriptor(rhport, p_request); + + case TUSB_REQ_SET_FEATURE: + switch (p_request->wValue) { //-V2520 + case TUSB_REQ_FEATURE_REMOTE_WAKEUP: + TU_LOG_USBD(" Enable Remote Wakeup\r\n"); + // Host may enable remote wake up before suspending especially HID device + _usbd_dev.remote_wakeup_en = 1; + tud_control_status(rhport, p_request); + return true; + + #if CFG_TUD_TEST_MODE + case TUSB_REQ_FEATURE_TEST_MODE: { + // Only handle the test mode if supported and valid + TU_VERIFY(0 == tu_u16_low(p_request->wIndex)); + + uint8_t const selector = tu_u16_high(p_request->wIndex); + TU_VERIFY(TUSB_FEATURE_TEST_J <= selector && selector <= TUSB_FEATURE_TEST_FORCE_ENABLE); + + _usbd_dev.ctrl_xfer.complete_cb = process_test_mode_cb; + tud_control_status(rhport, p_request); + return true; + } + #endif + + // Stall unsupported feature selector + default: return false; + } + + case TUSB_REQ_CLEAR_FEATURE: + // Only support remote wakeup for device feature + TU_VERIFY(TUSB_REQ_FEATURE_REMOTE_WAKEUP == p_request->wValue); + TU_LOG_USBD(" Disable Remote Wakeup\r\n"); + + // Host may disable remote wake up after resuming + _usbd_dev.remote_wakeup_en = 0; + tud_control_status(rhport, p_request); + return true; + + case TUSB_REQ_GET_STATUS: { + // Device status bit mask + // - Bit 0: Self Powered TODO must invoke callback to get actual status + // - Bit 1: Remote Wakeup enabled + uint16_t status = (uint16_t) _usbd_dev.dev_state_bm; + tud_control_xfer(rhport, p_request, &status, 2); + return true; + } + + default: + TU_BREAKPOINT(); + return false; + } +} + + // This handles the actual request and its response. // Returns false if unable to complete the request, causing caller to stall control endpoints. -static bool process_control_request(uint8_t rhport, tusb_control_request_t const * p_request) { - usbd_control_set_complete_callback(NULL); +static bool process_setup_received(uint8_t rhport, tusb_control_request_t const * p_request) { + // Initialize control transfer state for this request. The request copy must be + // visible to usbd_control_xfer_cb when the (asynchronous) status ZLP completes, + // since the SETUP packet event has already gone out of scope by then. + usbd_control_xfer_t* const ctrl_xfer = &_usbd_dev.ctrl_xfer; + ctrl_xfer->request = *p_request; + ctrl_xfer->buffer = NULL; + ctrl_xfer->total_xferred = 0; + ctrl_xfer->data_len = 0; + ctrl_xfer->complete_cb = NULL; + + p_request = &ctrl_xfer->request; // re-direct request pointer to internal copy (modifiable for hacking) TU_ASSERT(p_request->bmRequestType_bit.type < TUSB_REQ_TYPE_INVALID); // Vendor request if ( p_request->bmRequestType_bit.type == TUSB_REQ_TYPE_VENDOR ) { - usbd_control_set_complete_callback(tud_vendor_control_xfer_cb); + ctrl_xfer->complete_cb = tud_vendor_control_xfer_cb; return tud_vendor_control_xfer_cb(rhport, CONTROL_STAGE_SETUP, p_request); } @@ -860,115 +1106,7 @@ static bool process_control_request(uint8_t rhport, tusb_control_request_t const return false; } - switch (p_request->bRequest) { //-V2520 - case TUSB_REQ_SET_ADDRESS: - // Depending on mcu, status phase could be sent either before or after changing device address, - // or even require stack to not response with status at all - // Therefore DCD must take full responsibility to response and include zlp status packet if needed. - usbd_control_set_request(p_request); // set request since DCD has no access to tud_control_status() API - dcd_set_address(rhport, (uint8_t) p_request->wValue); - // skip tud_control_status() - _usbd_dev.addressed = 1; - break; - - case TUSB_REQ_GET_CONFIGURATION: { - uint8_t cfg_num = _usbd_dev.cfg_num; - tud_control_xfer(rhport, p_request, &cfg_num, 1); - } - break; - - case TUSB_REQ_SET_CONFIGURATION: { - uint8_t const cfg_num = (uint8_t) p_request->wValue; - - // Only process if new configure is different - if (_usbd_dev.cfg_num != cfg_num) { - if (_usbd_dev.cfg_num != 0) { - // already configured: need to clear all endpoints and driver first - TU_LOG_USBD(" Clear current Configuration (%u) before switching\r\n", _usbd_dev.cfg_num); - - dcd_sof_enable(rhport, false); - dcd_edpt_close_all(rhport); - - // close all drivers and current configured state except bus speed - const uint8_t speed = _usbd_dev.speed; - configuration_reset(rhport); - - _usbd_dev.speed = speed; // restore speed - } - - _usbd_dev.cfg_num = cfg_num; - - // Handle the new configuration - if (cfg_num == 0) { - tud_umount_cb(); - } else { - if (!process_set_config(rhport, cfg_num)) { - _usbd_dev.cfg_num = 0; - TU_ASSERT(false); - } - tud_mount_cb(); - } - } - - tud_control_status(rhport, p_request); - } - break; - - case TUSB_REQ_GET_DESCRIPTOR: - TU_VERIFY(process_get_descriptor(rhport, p_request)); - break; - - case TUSB_REQ_SET_FEATURE: - switch(p_request->wValue) { //-V2520 - case TUSB_REQ_FEATURE_REMOTE_WAKEUP: - TU_LOG_USBD(" Enable Remote Wakeup\r\n"); - // Host may enable remote wake up before suspending especially HID device - _usbd_dev.remote_wakeup_en = 1; - tud_control_status(rhport, p_request); - break; - - #if CFG_TUD_TEST_MODE - case TUSB_REQ_FEATURE_TEST_MODE: { - // Only handle the test mode if supported and valid - TU_VERIFY(0 == tu_u16_low(p_request->wIndex)); - - uint8_t const selector = tu_u16_high(p_request->wIndex); - TU_VERIFY(TUSB_FEATURE_TEST_J <= selector && selector <= TUSB_FEATURE_TEST_FORCE_ENABLE); - - usbd_control_set_complete_callback(process_test_mode_cb); - tud_control_status(rhport, p_request); - break; - } - #endif - - // Stall unsupported feature selector - default: return false; - } - break; - - case TUSB_REQ_CLEAR_FEATURE: - // Only support remote wakeup for device feature - TU_VERIFY(TUSB_REQ_FEATURE_REMOTE_WAKEUP == p_request->wValue); - TU_LOG_USBD(" Disable Remote Wakeup\r\n"); - - // Host may disable remote wake up after resuming - _usbd_dev.remote_wakeup_en = 0; - tud_control_status(rhport, p_request); - break; - - case TUSB_REQ_GET_STATUS: { - // Device status bit mask - // - Bit 0: Self Powered TODO must invoke callback to get actual status - // - Bit 1: Remote Wakeup enabled - uint16_t status = (uint16_t)_usbd_dev.dev_state_bm; - tud_control_xfer(rhport, p_request, &status, 2); - break; - } - - // Unknown/Unsupported request - default: TU_BREAKPOINT(); return false; - } - break; + return process_std_device_request(rhport, p_request); //------------- Class/Interface Specific Request -------------// case TUSB_REQ_RCPT_INTERFACE: { @@ -1004,7 +1142,7 @@ static bool process_control_request(uint8_t rhport, tusb_control_request_t const TU_VERIFY(TUSB_REQ_TYPE_STANDARD == p_request->bmRequestType_bit.type); // Clear complete callback if driver set since it can also stall the request. - usbd_control_set_complete_callback(NULL); + ctrl_xfer->complete_cb = NULL; switch (p_request->bRequest) { //-V2520 case TUSB_REQ_GET_INTERFACE: { @@ -1062,10 +1200,10 @@ static bool process_control_request(uint8_t rhport, tusb_control_request_t const // STD request must always be ACKed regardless of driver returned value // Also clear complete callback if driver set since it can also stall the request. (void) invoke_class_control(rhport, driver, p_request); - usbd_control_set_complete_callback(NULL); + ctrl_xfer->complete_cb = NULL; // skip ZLP status if driver already did that - if (!_usbd_dev.ep_status[0][TUSB_DIR_IN].busy) { + if (!(_usbd_dev.ep_status[0][TUSB_DIR_IN] & TU_EDPT_STATE_BUSY)) { tud_control_status(rhport, p_request); } } @@ -1144,8 +1282,7 @@ static bool process_set_config(uint8_t rhport, uint8_t cfg_num) { } // return descriptor's buffer and update desc_len -static bool process_get_descriptor(uint8_t rhport, tusb_control_request_t const * p_request) -{ +static bool process_get_descriptor(uint8_t rhport, tusb_control_request_t const * p_request) { tusb_desc_type_t const desc_type = (tusb_desc_type_t) tu_u16_high(p_request->wValue); uint8_t const desc_index = tu_u16_low( p_request->wValue ); @@ -1153,20 +1290,18 @@ static bool process_get_descriptor(uint8_t rhport, tusb_control_request_t const case TUSB_DESC_DEVICE: { TU_LOG_USBD(" Device\r\n"); - void* desc_device = (void*) (uintptr_t) tud_descriptor_device_cb(); + void *desc_device = (void *)(uintptr_t)tud_descriptor_device_cb(); TU_ASSERT(desc_device); // Only response with exactly 1 Packet if: not addressed and host requested more data than device descriptor has. // This only happens with the very first get device descriptor and EP0 size = 8 or 16. if ((CFG_TUD_ENDPOINT0_SIZE < sizeof(tusb_desc_device_t)) && !_usbd_dev.addressed && - ((tusb_control_request_t const*) p_request)->wLength > sizeof(tusb_desc_device_t)) { + p_request->wLength > sizeof(tusb_desc_device_t)) { // Hack here: we modify the request length to prevent usbd_control response with zlp // since we are responding with 1 packet & less data than wLength. - tusb_control_request_t mod_request = *p_request; - mod_request.wLength = CFG_TUD_ENDPOINT0_SIZE; - - return tud_control_xfer(rhport, &mod_request, desc_device, CFG_TUD_ENDPOINT0_SIZE); - }else { + ((tusb_control_request_t *)(uintptr_t)p_request)->wLength = CFG_TUD_ENDPOINT0_SIZE; + return tud_control_xfer(rhport, p_request, desc_device, CFG_TUD_ENDPOINT0_SIZE); + } else { return tud_control_xfer(rhport, p_request, desc_device, sizeof(tusb_desc_device_t)); } } @@ -1305,15 +1440,15 @@ TU_ATTR_FAST_FUNC void dcd_event_handler(dcd_event_t const* event, bool in_isr) usbd_class_driver_t const* driver = get_driver(_usbd_dev.ep2drv[epnum][ep_dir]); if (driver && driver->xfer_isr) { - _usbd_dev.ep_status[epnum][ep_dir].busy = 0; - _usbd_dev.ep_status[epnum][ep_dir].claimed = 0; + // Clear busy + claimed + _usbd_dev.ep_status[epnum][ep_dir] &= (uint8_t) ~(TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED); send = !driver->xfer_isr(event->rhport, ep_addr, (xfer_result_t) event->xfer_complete.result, event->xfer_complete.len); // xfer_isr() is deferred to xfer_cb(), revert busy/claimed status if (send) { - _usbd_dev.ep_status[epnum][ep_dir].busy = 1; - _usbd_dev.ep_status[epnum][ep_dir].claimed = 1; + // set busy + claimed + _usbd_dev.ep_status[epnum][ep_dir] |= (TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED); } } } @@ -1403,9 +1538,7 @@ bool usbd_edpt_claim(uint8_t rhport, uint8_t ep_addr) { uint8_t const epnum = tu_edpt_number(ep_addr); uint8_t const dir = tu_edpt_dir(ep_addr); - tu_edpt_state_t* ep_state = &_usbd_dev.ep_status[epnum][dir]; - - return tu_edpt_claim(ep_state, _usbd_mutex); + return tu_edpt_claim(&_usbd_dev.ep_status[epnum][dir], _usbd_mutex); } bool usbd_edpt_release(uint8_t rhport, uint8_t ep_addr) { @@ -1413,9 +1546,7 @@ bool usbd_edpt_release(uint8_t rhport, uint8_t ep_addr) { uint8_t const epnum = tu_edpt_number(ep_addr); uint8_t const dir = tu_edpt_dir(ep_addr); - tu_edpt_state_t* ep_state = &_usbd_dev.ep_status[epnum][dir]; - - return tu_edpt_release(ep_state, _usbd_mutex); + return tu_edpt_release(&_usbd_dev.ep_status[epnum][dir], _usbd_mutex); } bool usbd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t* buffer, uint16_t total_bytes, bool is_isr) { @@ -1435,18 +1566,17 @@ bool usbd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t* buffer, uint16_t t #endif // Attempt to transfer on a busy endpoint, sound like an race condition ! - TU_ASSERT(_usbd_dev.ep_status[epnum][dir].busy == 0); + TU_ASSERT((_usbd_dev.ep_status[epnum][dir] & TU_EDPT_STATE_BUSY) == 0); // Set busy first since the actual transfer can be complete before dcd_edpt_xfer() // could return and USBD task can preempt and clear the busy - _usbd_dev.ep_status[epnum][dir].busy = 1; + _usbd_dev.ep_status[epnum][dir] |= TU_EDPT_STATE_BUSY; if (dcd_edpt_xfer(rhport, ep_addr, buffer, total_bytes, is_isr)) { return true; } else { // DCD error, mark endpoint as ready to allow next transfer - _usbd_dev.ep_status[epnum][dir].busy = 0; - _usbd_dev.ep_status[epnum][dir].claimed = 0; + _usbd_dev.ep_status[epnum][dir] &= (uint8_t) ~(TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED); TU_LOG_USBD("FAILED\r\n"); TU_BREAKPOINT(); return false; @@ -1467,19 +1597,18 @@ bool usbd_edpt_xfer_fifo(uint8_t rhport, uint8_t ep_addr, tu_fifo_t* ff, uint16_ TU_LOG_USBD(" Queue FIFO EP %02X with %u bytes ... ", ep_addr, total_bytes); // Attempt to transfer on a busy endpoint, sound like a race condition ! - TU_ASSERT(_usbd_dev.ep_status[epnum][dir].busy == 0); + TU_ASSERT((_usbd_dev.ep_status[epnum][dir] & TU_EDPT_STATE_BUSY) == 0); // Set busy first since the actual transfer can be complete before dcd_edpt_xfer() could return // and usbd task can preempt and clear the busy - _usbd_dev.ep_status[epnum][dir].busy = 1; + _usbd_dev.ep_status[epnum][dir] |= TU_EDPT_STATE_BUSY; if (dcd_edpt_xfer_fifo(rhport, ep_addr, ff, total_bytes, is_isr)) { TU_LOG_USBD("OK\r\n"); return true; } else { // DCD error, mark endpoint as ready to allow next transfer - _usbd_dev.ep_status[epnum][dir].busy = 0; - _usbd_dev.ep_status[epnum][dir].claimed = 0; + _usbd_dev.ep_status[epnum][dir] &= (uint8_t) ~(TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED); TU_LOG_USBD("failed\r\n"); TU_BREAKPOINT(); return false; @@ -1500,7 +1629,7 @@ bool usbd_edpt_busy(uint8_t rhport, uint8_t ep_addr) { uint8_t const epnum = tu_edpt_number(ep_addr); uint8_t const dir = tu_edpt_dir(ep_addr); - return _usbd_dev.ep_status[epnum][dir].busy; + return (_usbd_dev.ep_status[epnum][dir] & TU_EDPT_STATE_BUSY) != 0; } void usbd_edpt_stall(uint8_t rhport, uint8_t ep_addr) { @@ -1512,8 +1641,7 @@ void usbd_edpt_stall(uint8_t rhport, uint8_t ep_addr) { // only stalled if currently cleared TU_LOG_USBD(" Stall EP %02X\r\n", ep_addr); dcd_edpt_stall(rhport, ep_addr); - _usbd_dev.ep_status[epnum][dir].stalled = 1; - _usbd_dev.ep_status[epnum][dir].busy = 1; + _usbd_dev.ep_status[epnum][dir] |= (TU_EDPT_STATE_STALLED | TU_EDPT_STATE_BUSY); } void usbd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) { @@ -1525,8 +1653,7 @@ void usbd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) { // only clear if currently stalled TU_LOG_USBD(" Clear Stall EP %02X\r\n", ep_addr); dcd_edpt_clear_stall(rhport, ep_addr); - _usbd_dev.ep_status[epnum][dir].stalled = 0; - _usbd_dev.ep_status[epnum][dir].busy = 0; + _usbd_dev.ep_status[epnum][dir] &= (uint8_t) ~(TU_EDPT_STATE_STALLED | TU_EDPT_STATE_BUSY); } bool usbd_edpt_stalled(uint8_t rhport, uint8_t ep_addr) { @@ -1535,7 +1662,7 @@ bool usbd_edpt_stalled(uint8_t rhport, uint8_t ep_addr) { uint8_t const epnum = tu_edpt_number(ep_addr); uint8_t const dir = tu_edpt_dir(ep_addr); - return _usbd_dev.ep_status[epnum][dir].stalled; + return (_usbd_dev.ep_status[epnum][dir] & TU_EDPT_STATE_STALLED) != 0; } /** @@ -1555,9 +1682,7 @@ void usbd_edpt_close(uint8_t rhport, uint8_t ep_addr) { uint8_t const dir = tu_edpt_dir(ep_addr); dcd_edpt_close(rhport, ep_addr); - _usbd_dev.ep_status[epnum][dir].stalled = 0; - _usbd_dev.ep_status[epnum][dir].busy = 0; - _usbd_dev.ep_status[epnum][dir].claimed = 0; + _usbd_dev.ep_status[epnum][dir] = 0; #endif return; @@ -1602,9 +1727,7 @@ bool usbd_edpt_iso_activate(uint8_t rhport, tusb_desc_endpoint_t const* desc_ep) TU_ASSERT(epnum < CFG_TUD_ENDPPOINT_MAX); TU_ASSERT(tu_edpt_validate(desc_ep, (tusb_speed_t)_usbd_dev.speed)); - _usbd_dev.ep_status[epnum][dir].stalled = 0; - _usbd_dev.ep_status[epnum][dir].busy = 0; - _usbd_dev.ep_status[epnum][dir].claimed = 0; + _usbd_dev.ep_status[epnum][dir] = 0; return dcd_edpt_iso_activate(rhport, desc_ep); #else (void) rhport; (void) desc_ep; diff --git a/src/device/usbd.h b/src/device/usbd.h index d3a6dccbb..473e697ac 100644 --- a/src/device/usbd.h +++ b/src/device/usbd.h @@ -1026,23 +1026,23 @@ bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_requ // Length of template descriptor #define TUD_CDC_NCM_DESC_LEN (8+9+5+5+13+6+7+9+9+7+7) -// CDC-ECM Descriptor Template -// Interface number, description string index, MAC address string index, EP notification address and size, EP data address (out, in), and size, max segment size. -#define TUD_CDC_NCM_DESCRIPTOR(_itfnum, _desc_stridx, _mac_stridx, _ep_notif, _ep_notif_size, _epout, _epin, _epsize, _maxsegmentsize) \ +// CDC-NCM Descriptor Template +// Interface number, description string index, MAC address string index, EP notification address and size, EP data address (out, in), and size, max segment size, EP notification bInterval, capability. +#define TUD_CDC_NCM_DESCRIPTOR(_itfnum, _desc_stridx, _mac_stridx, _ep_notif, _ep_notif_size, _epout, _epin, _epsize, _maxsegmentsize, _ep_notif_interval, _capability) \ /* Interface Association */\ 8, TUSB_DESC_INTERFACE_ASSOCIATION, _itfnum, 2, TUSB_CLASS_CDC, CDC_COMM_SUBCLASS_NETWORK_CONTROL_MODEL, 0, 0,\ /* CDC Control Interface */\ 9, TUSB_DESC_INTERFACE, _itfnum, 0, 1, TUSB_CLASS_CDC, CDC_COMM_SUBCLASS_NETWORK_CONTROL_MODEL, 0, _desc_stridx,\ - /* CDC-NCM Header */\ + /* CDC Header */\ 5, TUSB_DESC_CS_INTERFACE, CDC_FUNC_DESC_HEADER, U16_TO_U8S_LE(0x0110),\ - /* CDC-NCM Union */\ + /* CDC Union */\ 5, TUSB_DESC_CS_INTERFACE, CDC_FUNC_DESC_UNION, _itfnum, (uint8_t)((_itfnum) + 1),\ - /* CDC-NCM Functional Descriptor */\ + /* CDC Ethernet Networking Descriptor */\ 13, TUSB_DESC_CS_INTERFACE, CDC_FUNC_DESC_ETHERNET_NETWORKING, _mac_stridx, 0, 0, 0, 0, U16_TO_U8S_LE(_maxsegmentsize), U16_TO_U8S_LE(0), 0, \ /* CDC-NCM Functional Descriptor */\ - 6, TUSB_DESC_CS_INTERFACE, CDC_FUNC_DESC_NCM, U16_TO_U8S_LE(0x0100), 0, \ + 6, TUSB_DESC_CS_INTERFACE, CDC_FUNC_DESC_NCM, U16_TO_U8S_LE(0x0100), _capability, \ /* Endpoint Notification */\ - 7, TUSB_DESC_ENDPOINT, _ep_notif, TUSB_XFER_INTERRUPT, U16_TO_U8S_LE(_ep_notif_size), 50,\ + 7, TUSB_DESC_ENDPOINT, _ep_notif, TUSB_XFER_INTERRUPT, U16_TO_U8S_LE(_ep_notif_size), _ep_notif_interval,\ /* CDC Data Interface (default inactive) */\ 9, TUSB_DESC_INTERFACE, (uint8_t)((_itfnum)+1), 0, 0, TUSB_CLASS_CDC_DATA, 0, NCM_DATA_PROTOCOL_NETWORK_TRANSFER_BLOCK, 0,\ /* CDC Data Interface (alternative active) */\ diff --git a/src/device/usbd_control.c b/src/device/usbd_control.c deleted file mode 100644 index 87593d4a7..000000000 --- a/src/device/usbd_control.c +++ /dev/null @@ -1,217 +0,0 @@ -/* - * 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. - * - * This file is part of the TinyUSB stack. - */ - -#include "tusb_option.h" - -#if CFG_TUD_ENABLED - -#include "dcd.h" -#include "tusb.h" -#include "device/usbd_pvt.h" - -//--------------------------------------------------------------------+ -// Callback weak stubs (called if application does not provide) -//--------------------------------------------------------------------+ -TU_ATTR_WEAK void dcd_edpt0_status_complete(uint8_t rhport, const tusb_control_request_t* request) { - (void) rhport; - (void) request; -} - -//--------------------------------------------------------------------+ -// MACRO CONSTANT TYPEDEF -//--------------------------------------------------------------------+ - -enum { - EDPT_CTRL_OUT = 0x00, - EDPT_CTRL_IN = 0x80 -}; - -typedef struct { - tusb_control_request_t request; - uint8_t* buffer; - uint16_t data_len; - uint16_t total_xferred; - usbd_control_xfer_cb_t complete_cb; -} usbd_control_xfer_t; - -static usbd_control_xfer_t _ctrl_xfer; - -CFG_TUD_MEM_SECTION static struct { - TUD_EPBUF_DEF(buf, CFG_TUD_ENDPOINT0_BUFSIZE); -} _ctrl_epbuf; - -uint8_t* usbd_get_ctrl_buf(void) { - return _ctrl_epbuf.buf; -} - -//--------------------------------------------------------------------+ -// Application API -//--------------------------------------------------------------------+ - -// Queue ZLP status transaction -static inline bool status_stage_xact(uint8_t rhport, const tusb_control_request_t* request) { - // Opposite to endpoint in Data Phase - const uint8_t ep_addr = request->bmRequestType_bit.direction ? EDPT_CTRL_OUT : EDPT_CTRL_IN; - return usbd_edpt_xfer(rhport, ep_addr, NULL, 0, false); -} - -// Status phase -bool tud_control_status(uint8_t rhport, const tusb_control_request_t* request) { - _ctrl_xfer.request = (*request); - _ctrl_xfer.buffer = NULL; - _ctrl_xfer.total_xferred = 0; - _ctrl_xfer.data_len = 0; - - return status_stage_xact(rhport, request); -} - -// Queue a transaction in Data Stage -// Each transaction has up to Endpoint0's max packet size. -// This function can also transfer an zero-length packet -static bool data_stage_xact(uint8_t rhport) { - const uint16_t xact_len = tu_min16(_ctrl_xfer.data_len - _ctrl_xfer.total_xferred, CFG_TUD_ENDPOINT0_BUFSIZE); - uint8_t ep_addr = EDPT_CTRL_OUT; - - if (_ctrl_xfer.request.bmRequestType_bit.direction == TUSB_DIR_IN) { - ep_addr = EDPT_CTRL_IN; - if (0u != xact_len && _ctrl_xfer.buffer != _ctrl_epbuf.buf) { - TU_VERIFY(0 == tu_memcpy_s(_ctrl_epbuf.buf, CFG_TUD_ENDPOINT0_BUFSIZE, _ctrl_xfer.buffer, xact_len)); - } - } - - return usbd_edpt_xfer(rhport, ep_addr, xact_len ? _ctrl_epbuf.buf : NULL, xact_len, false); -} - -// Transmit data to/from the control endpoint. -// If the request's wLength is zero, a status packet is sent instead. -bool tud_control_xfer(uint8_t rhport, const tusb_control_request_t* request, void* buffer, uint16_t len) { - _ctrl_xfer.request = (*request); - _ctrl_xfer.buffer = (uint8_t*) buffer; - _ctrl_xfer.total_xferred = 0U; - _ctrl_xfer.data_len = tu_min16(len, request->wLength); - - if (request->wLength > 0U) { - if (_ctrl_xfer.data_len > 0U) { - TU_ASSERT(buffer); - } - TU_ASSERT(data_stage_xact(rhport)); - } else { - TU_ASSERT(status_stage_xact(rhport, request)); - } - - return true; -} - -//--------------------------------------------------------------------+ -// USBD API -//--------------------------------------------------------------------+ -void usbd_control_reset(void); -void usbd_control_set_request(const tusb_control_request_t* request); -void usbd_control_set_complete_callback(usbd_control_xfer_cb_t fp); -bool usbd_control_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes); - -void usbd_control_reset(void) { - tu_varclr(&_ctrl_xfer); -} - -// Set complete callback -void usbd_control_set_complete_callback(usbd_control_xfer_cb_t fp) { - _ctrl_xfer.complete_cb = fp; -} - -// for dcd_set_address where DCD is responsible for status response -void usbd_control_set_request(const tusb_control_request_t* request) { - _ctrl_xfer.request = (*request); - _ctrl_xfer.buffer = NULL; - _ctrl_xfer.total_xferred = 0; - _ctrl_xfer.data_len = 0; -} - -// callback when a transaction complete on -// - DATA stage of control endpoint or -// - Status stage -bool usbd_control_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { - (void) result; - - // Endpoint Address is opposite to direction bit, this is Status Stage complete event - if (tu_edpt_dir(ep_addr) != _ctrl_xfer.request.bmRequestType_bit.direction) { - TU_ASSERT(0 == xferred_bytes); - - // invoke optional dcd hook if available - dcd_edpt0_status_complete(rhport, &_ctrl_xfer.request); - - if (NULL != _ctrl_xfer.complete_cb) { - // TODO refactor with usbd_driver_print_control_complete_name - _ctrl_xfer.complete_cb(rhport, CONTROL_STAGE_ACK, &_ctrl_xfer.request); - } - - return true; - } - - if (_ctrl_xfer.request.bmRequestType_bit.direction == TUSB_DIR_OUT) { - TU_VERIFY(_ctrl_xfer.buffer); - if (_ctrl_xfer.buffer != _ctrl_epbuf.buf) { - memcpy(_ctrl_xfer.buffer, _ctrl_epbuf.buf, xferred_bytes); - } - TU_LOG_MEM(CFG_TUD_LOG_LEVEL, _ctrl_xfer.buffer, xferred_bytes, 2); - } - - _ctrl_xfer.total_xferred += (uint16_t) xferred_bytes; - _ctrl_xfer.buffer += xferred_bytes; - - // Data Stage is complete when all request's length are transferred or - // a short packet is sent including zero-length packet. - if ((_ctrl_xfer.request.wLength == _ctrl_xfer.total_xferred) || - (xferred_bytes < CFG_TUD_ENDPOINT0_BUFSIZE)) { - // DATA stage is complete - bool is_ok = true; - - // invoke complete callback if set - // callback can still stall control in status phase e.g out data does not make sense - if (NULL != _ctrl_xfer.complete_cb) { - #if CFG_TUSB_DEBUG >= CFG_TUD_LOG_LEVEL - usbd_driver_print_control_complete_name(_ctrl_xfer.complete_cb); - #endif - - is_ok = _ctrl_xfer.complete_cb(rhport, CONTROL_STAGE_DATA, &_ctrl_xfer.request); - } - - if (is_ok) { - TU_ASSERT(status_stage_xact(rhport, &_ctrl_xfer.request)); - } else { - // Stall both IN and OUT control endpoint - dcd_edpt_stall(rhport, EDPT_CTRL_OUT); - dcd_edpt_stall(rhport, EDPT_CTRL_IN); - } - } else { - // More data to transfer - TU_ASSERT(data_stage_xact(rhport)); - } - - return true; -} - -#endif diff --git a/src/device/usbd_pvt.h b/src/device/usbd_pvt.h index 5f11ea481..be778f9af 100644 --- a/src/device/usbd_pvt.h +++ b/src/device/usbd_pvt.h @@ -130,10 +130,6 @@ void usbd_sof_enable(uint8_t rhport, sof_consumer_t consumer, bool en); bool usbd_open_edpt_pair(uint8_t rhport, uint8_t const* p_desc, uint8_t ep_count, uint8_t xfer_type, uint8_t* ep_out, uint8_t* ep_in); void usbd_defer_func(osal_task_func_t func, void *param, bool in_isr); -#if CFG_TUSB_DEBUG >= CFG_TUD_LOG_LEVEL -void usbd_driver_print_control_complete_name(usbd_control_xfer_cb_t callback); -#endif - #ifdef __cplusplus } #endif diff --git a/src/host/usbh.c b/src/host/usbh.c index 8f80800e9..490724b02 100644 --- a/src/host/usbh.c +++ b/src/host/usbh.c @@ -140,7 +140,7 @@ typedef struct { uint8_t itf2drv[CFG_TUH_INTERFACE_MAX]; // map interface number to driver (0xff is invalid) uint8_t ep2drv[CFG_TUH_ENDPOINT_MAX][2]; // map endpoint to driver ( 0xff is invalid ), can use only 4-bit each - tu_edpt_state_t ep_status[CFG_TUH_ENDPOINT_MAX][2]; + volatile uint8_t ep_status[CFG_TUH_ENDPOINT_MAX][2]; #if CFG_TUH_API_EDPT_XFER // TODO array can be CFG_TUH_ENDPOINT_MAX-1 @@ -744,8 +744,8 @@ void tuh_task_ext(uint32_t timeout_ms, bool in_isr) { usbh_device_t* dev = get_device(event.dev_addr); TU_VERIFY(dev && dev->connected,); - dev->ep_status[epnum][ep_dir].busy = 0; - dev->ep_status[epnum][ep_dir].claimed = 0; + // clear busy and claimed + dev->ep_status[epnum][ep_dir] &= (uint8_t) ~(TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED); if (0 == epnum) { usbh_control_xfer_cb(event.dev_addr, ep_addr, (xfer_result_t) event.xfer_complete.result, event.xfer_complete.len); @@ -1016,10 +1016,10 @@ bool tuh_edpt_abort_xfer(uint8_t daddr, uint8_t ep_addr) { usbh_device_t* dev = get_device(daddr); TU_VERIFY(dev); - TU_VERIFY(dev->ep_status[epnum][dir].busy); // non-control skip if not busy + TU_VERIFY(dev->ep_status[epnum][dir] & TU_EDPT_STATE_BUSY); // non-control skip if not busy // abort then mark as ready and release endpoint hcd_edpt_abort_xfer(dev->bus_info.rhport, daddr, ep_addr); - dev->ep_status[epnum][dir].busy = false; + dev->ep_status[epnum][dir] &= (uint8_t) ~TU_EDPT_STATE_BUSY; // clear busy tu_edpt_release(&dev->ep_status[epnum][dir], _usbh_mutex); } @@ -1110,16 +1110,16 @@ bool usbh_edpt_xfer_with_callback(uint8_t dev_addr, uint8_t ep_addr, uint8_t* bu uint8_t const epnum = tu_edpt_number(ep_addr); uint8_t const dir = tu_edpt_dir(ep_addr); - tu_edpt_state_t* ep_state = &dev->ep_status[epnum][dir]; + volatile uint8_t* ep_state = &dev->ep_status[epnum][dir]; TU_LOG_USBH(" Queue EP %02X with %u bytes ... \r\n", ep_addr, total_bytes); // Attempt to transfer on a busy endpoint, sound like an race condition ! - TU_ASSERT(ep_state->busy == 0); + TU_ASSERT((*ep_state & TU_EDPT_STATE_BUSY) == 0); // Set busy first since the actual transfer can be complete before hcd_edpt_xfer() // could return and USBH task can preempt and clear the busy - ep_state->busy = 1; + *ep_state |= TU_EDPT_STATE_BUSY; #if CFG_TUH_API_EDPT_XFER dev->ep_callback[epnum][dir].complete_cb = complete_cb; @@ -1130,9 +1130,8 @@ bool usbh_edpt_xfer_with_callback(uint8_t dev_addr, uint8_t ep_addr, uint8_t* bu TU_LOG_USBH("OK\r\n"); return true; } else { - // HCD error, mark endpoint as ready to allow next transfer - ep_state->busy = 0; - ep_state->claimed = 0; + // HCD error, clear busy and claimed to allow next transfer + *ep_state &= (uint8_t) ~(TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED); TU_LOG1("Failed\r\n"); // TU_BREAKPOINT(); return false; @@ -1178,7 +1177,7 @@ bool usbh_edpt_busy(uint8_t dev_addr, uint8_t ep_addr) { uint8_t const epnum = tu_edpt_number(ep_addr); uint8_t const dir = tu_edpt_dir(ep_addr); - return dev->ep_status[epnum][dir].busy; + return (dev->ep_status[epnum][dir] & TU_EDPT_STATE_BUSY) != 0; } //--------------------------------------------------------------------+ diff --git a/src/portable/mentor/musb/dcd_musb.c b/src/portable/mentor/musb/dcd_musb.c index 2f6f231ef..56429ac1f 100644 --- a/src/portable/mentor/musb/dcd_musb.c +++ b/src/portable/mentor/musb/dcd_musb.c @@ -50,36 +50,70 @@ * MACRO TYPEDEF CONSTANT ENUM DECLARATION *------------------------------------------------------------------*/ -#define REQUEST_TYPE_INVALID (0xFFu) - typedef union { volatile uint8_t u8; volatile uint16_t u16; volatile uint32_t u32; } hw_fifo_t; -typedef struct TU_ATTR_PACKED -{ - void *buf; /* the start address of a transfer data buffer */ +typedef struct { + union { + uint8_t *buf; /* the start address of a transfer data buffer */ + tu_fifo_t *fifo; + }; uint16_t length; /* the number of bytes in the buffer */ uint16_t remaining; /* the number of bytes remaining in the buffer */ + bool armed; /* true while a transfer is posted */ + bool use_fifo; /* true: buf is tu_fifo_t*; false: buf is plain byte pointer. */ } pipe_state_t; -typedef struct -{ - union { - tusb_control_request_t setup_packet; - uint32_t setup_buffer[2]; - }; - uint16_t remaining_ctrl; /* The number of bytes remaining in data stage of control transfer. */ - int8_t status_out; - pipe_state_t pipe0; - pipe_state_t pipe[2][TUP_DCD_ENDPOINT_MAX-1]; /* pipe[direction][endpoint number - 1] */ - uint16_t pipe_buf_is_fifo[2]; /* Bitmap. Each bit means whether 1:TU_FIFO or 0:POD. */ +// Pipe array layout (N = TUP_DCD_ENDPOINT_MAX). EP0 has its own scalars in +// dcd_data_t and does not occupy a pipe slot. +// One-direction-only IPs (CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY=1): +// [0..N-2] : EP1..N-1 (single slot per endpoint) +// Bidirectional-capable IPs: +// [0..N-2 ] : EP1..N-1 OUT +// [N-1..2*N-3 ] : EP1..N-1 IN +#if CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY + #define MUSB_PIPE_COUNT (TUP_DCD_ENDPOINT_MAX - 1u) +#else + #define MUSB_PIPE_COUNT (2u * (TUP_DCD_ENDPOINT_MAX - 1u)) +#endif + +enum { + PIPE0_STATE_IDLE = 0, // no active control transfer + PIPE0_STATE_DATA, // DATA stage (IN or OUT — direction implied by CSR/dir) + PIPE0_STATE_STATUS_IN, // STATUS IN — device sends IN-ZLP; awaits send-ACK IRQ + PIPE0_STATE_STATUS_OUT, // post-DATAEND, neither edpt0_xfer(STATUS OUT) nor confirmation IRQ has happened yet + PIPE0_STATE_STATUS_OUT_PENDING, // one of {edpt0_xfer(STATUS OUT), confirmation IRQ} has happened; the other fires xfer_complete +}; + +typedef struct { + struct { + uint8_t *buf; // DATA OUT drain target (only valid while EP0 is in DATA OUT stage) + uint16_t xact_len; // chunk length most recently armed via edpt0_xfer; reported in xfer_complete + uint16_t remain_wlength; // bytes remaining in the control transfer's DATA stage + uint8_t state; + uint8_t pending_addr; // new USB address latched by dcd_set_address; applied when STATUS IN completes + } pipe0; + pipe_state_t pipe[MUSB_PIPE_COUNT]; } dcd_data_t; static dcd_data_t _dcd; +// EP0 must not call this — it has its own scalars in dcd_data_t. +TU_ATTR_ALWAYS_INLINE static inline pipe_state_t* pipe_get(uint8_t epnum, tusb_dir_t epdir) { + size_t idx = epnum - 1u; +#if CFG_TUD_ENDPOINT_ONE_DIRECTION_ONLY + (void) epdir; +#else + if (epdir == TUSB_DIR_IN) { + idx += TUP_DCD_ENDPOINT_MAX - 1u; + } +#endif + return &_dcd.pipe[idx]; +} + //-------------------------------------------------------------------- // HW FIFO Helper // Note: Index register is already set by caller @@ -110,9 +144,8 @@ TU_ATTR_ALWAYS_INLINE static inline void hwfifo_reset(musb_regs_t* musb, unsigne TU_ATTR_ALWAYS_INLINE static inline bool hwfifo_config(musb_regs_t* musb, unsigned epnum, unsigned is_rx, unsigned mps, bool double_packet) { - (void) epnum; - uint8_t ffsize = hwfifo_byte2size((uint16_t)mps); - mps = 8u << ffsize; // round up to the next power of 2 + uint8_t ffsize = hwfifo_byte2size(mps); + mps = 8 << ffsize; // round up to the next power of 2 if (double_packet) { ffsize |= MUSB_FIFOSZ_DOUBLE_PACKET; @@ -120,9 +153,16 @@ TU_ATTR_ALWAYS_INLINE static inline bool hwfifo_config(musb_regs_t* musb, unsign } TU_ASSERT(alloced_fifo_bytes + mps <= MUSB_CFG_DYNAMIC_FIFO_SIZE); - musb->fifo_addr[is_rx] = (uint16_t)(alloced_fifo_bytes / 8); + musb->fifo_addr[is_rx] = alloced_fifo_bytes / 8; musb->fifo_size[is_rx] = ffsize; + volatile uint16_t* dp_disable = is_rx ? &musb->rx_doulbe_packet_disable : &musb->tx_double_packet_disable; + if (double_packet) { + *dp_disable &= ~(1u << epnum); + } else { + *dp_disable |= (1u << epnum); + } + alloced_fifo_bytes += mps; return true; } @@ -136,18 +176,29 @@ TU_ATTR_ALWAYS_INLINE static inline void hwfifo_reset(musb_regs_t* musb, unsigne TU_ATTR_ALWAYS_INLINE static inline bool hwfifo_config(musb_regs_t* musb, unsigned epnum, unsigned is_rx, unsigned mps, bool double_packet) { - (void) epnum; (void) mps; - if (!double_packet) { - #if defined(TUP_USBIP_MUSB_ADI) - musb->indexed_csr.maxp_csr[is_rx].csrh |= MUSB_CSRH_DISABLE_DOUBLE_PACKET(is_rx); - #else - if (is_rx) { - musb->rx_doulbe_packet_disable |= 1u << epnum; - } else { - musb->tx_double_packet_disable |= 1u << epnum; - } - #endif + (void) mps; + + #if defined(TUP_USBIP_MUSB_ADI) + // AnalogDevice FIFO sizes: EP1..7 = 512 B, EP8..9 = 2048 B, EP10..11 = 4096 B. + // DPB requires FIFO >= 2 * MPS. For HS bulk (MPS=512) only EP >= 8 qualifies. + // Force single-buffered on EP < 8 even if the caller requested DPB. + if (epnum < 8 && (musb->power & MUSB_POWER_HSMODE)) { + double_packet = false; + } + volatile uint8_t* csrh = &musb->indexed_csr.maxp_csr[is_rx].csrh; + if (double_packet) { + *csrh &= ~MUSB_CSRH_DISABLE_DOUBLE_PACKET; + } else { + *csrh |= MUSB_CSRH_DISABLE_DOUBLE_PACKET; + } + #else + volatile uint16_t* dp_disable = is_rx ? &musb->rx_doulbe_packet_disable : &musb->tx_double_packet_disable; + if (double_packet) { + *dp_disable &= ~(1u << epnum); + } else { + *dp_disable |= (1u << epnum); } + #endif return true; } @@ -157,315 +208,309 @@ TU_ATTR_ALWAYS_INLINE static inline bool hwfifo_config(musb_regs_t* musb, unsign // Flush FIFO and clear data toggle TU_ATTR_ALWAYS_INLINE static inline void hwfifo_flush(musb_regs_t* musb, unsigned epnum, unsigned is_rx, bool clear_dtog) { (void) epnum; - const uint8_t csrl_dtog = clear_dtog ? (uint8_t)MUSB_CSRL_CLEAR_DATA_TOGGLE(is_rx) : 0; + const uint8_t csrl_dtog = clear_dtog ? MUSB_CSRL_CLEAR_DATA_TOGGLE(is_rx) : 0; musb_ep_maxp_csr_t* maxp_csr = &musb->indexed_csr.maxp_csr[is_rx]; // may need to flush twice for double packet for (unsigned i=0; i<2; i++) { if (maxp_csr->csrl & MUSB_CSRL_PACKET_READY(is_rx)) { - maxp_csr->csrl = (uint8_t)(MUSB_CSRL_FLUSH_FIFO(is_rx) | csrl_dtog); + maxp_csr->csrl = MUSB_CSRL_FLUSH_FIFO(is_rx) | csrl_dtog; } } } -static void process_setup_packet(uint8_t rhport) { - musb_regs_t* musb_regs = MUSB_REGS(rhport); - - // Read setup packet - _dcd.setup_buffer[0] = musb_regs->fifo[0]; - _dcd.setup_buffer[1] = musb_regs->fifo[0]; - - _dcd.pipe0.buf = NULL; - _dcd.pipe0.length = 0; - _dcd.pipe0.remaining = 0; - dcd_event_setup_received(rhport, (const uint8_t*)(uintptr_t)&_dcd.setup_packet, true); - - const unsigned len = _dcd.setup_packet.wLength; - _dcd.remaining_ctrl = (uint16_t)len; - const unsigned dir_in = tu_edpt_dir(_dcd.setup_packet.bmRequestType); - /* Clear RX FIFO and reverse the transaction direction */ - if (len && dir_in) { - musb_ep_csr_t* ep_csr = get_ep_csr(musb_regs, 0); - ep_csr->csr0l = MUSB_CSRL0_RXRDYC; +// write to txfifo using pipe_state_t info +static void pipe_write(musb_regs_t* musb_regs, pipe_state_t* pipe, uint8_t epnum) { + musb_ep_csr_t* ep_csr = &musb_regs->indexed_csr; + const uint16_t mps = ep_csr->tx_maxp & MUSB_TXMAXP_PACKET_SIZE_M; + const uint16_t xact_len = tu_min16(mps, pipe->remaining); + volatile void *hwfifo = &musb_regs->fifo[epnum]; + if (xact_len) { + if (pipe->use_fifo) { + tu_hwfifo_write_from_fifo(hwfifo, pipe->fifo, xact_len, NULL); + } else { + tu_hwfifo_write(hwfifo, pipe->buf, xact_len, NULL); + pipe->buf += xact_len; + } + pipe->remaining -= xact_len; } + ep_csr->tx_csrl = MUSB_TXCSRL1_TXRDY; } -static bool handle_xfer_in(uint8_t rhport, uint_fast8_t ep_addr) { - unsigned epnum = tu_edpt_number(ep_addr); - unsigned epnum_minus1 = epnum - 1; - pipe_state_t *pipe = &_dcd.pipe[tu_edpt_dir(ep_addr)][epnum_minus1]; - const unsigned rem = pipe->remaining; +// Called from the TX interrupt. If the last queued packet finished the transfer, +// signal completion; otherwise queue the next packet. +static void process_epin(uint8_t rhport, musb_regs_t *musb_regs, uint8_t epnum) { + musb_ep_csr_t* ep_csr = get_ep_csr(musb_regs, epnum); + const uint_fast8_t csrl = ep_csr->tx_csrl; + if (csrl & MUSB_TXCSRL1_STALLED) { + ep_csr->tx_csrl &= ~(MUSB_TXCSRL1_STALLED | MUSB_TXCSRL1_UNDRN); + return; // sent STALL, do nothing + } - if (rem == 0 && pipe->length > 0) { + pipe_state_t* pipe = pipe_get(epnum, TUSB_DIR_IN); + if (pipe->remaining > 0) { + pipe_write(musb_regs, pipe, epnum); + } else { + // All bytes have been loaded into the FIFO. With double-packet buffering a + // second packet may still be waiting in the FIFO when this IRQ fires (the + // hardware signals TXRDY clear as soon as a slot frees, not when the wire + // transfer finishes). Defer completion until FIFONE == 0 so we don't emit + // a duplicate xfer_complete before the final packet has been sent. + if (csrl & MUSB_TXCSRL1_FIFONE) { + return; + } + const uint16_t xferred_len = pipe->length; pipe->buf = NULL; - return true; + pipe->armed = false; + dcd_event_xfer_complete(rhport, tu_edpt_addr(epnum, TUSB_DIR_IN), xferred_len, XFER_RESULT_SUCCESS, true); } +} - musb_regs_t* musb_regs = MUSB_REGS(rhport); - musb_ep_csr_t* ep_csr = get_ep_csr(musb_regs, epnum); - const unsigned mps = ep_csr->tx_maxp; - const unsigned len = TU_MIN(mps, rem); - void *buf = pipe->buf; - volatile void *fifo_ptr = &musb_regs->fifo[epnum]; - // TU_LOG1(" %p mps %d len %d rem %d\r\n", buf, mps, len, rem); - if (len) { - if (_dcd.pipe_buf_is_fifo[TUSB_DIR_IN] & TU_BIT(epnum_minus1)) { - tu_hwfifo_write_from_fifo(fifo_ptr, (tu_fifo_t *)buf, len, NULL); +// Drain one packet from the Rx FIFO into pipe->buf/fifo, update pipe state, and +// release the FIFO slot by clearing RXRDY. return true if short packet +static bool pipe_read(musb_regs_t* musb_regs, pipe_state_t* pipe, uint8_t epnum) { + musb_ep_csr_t* ep_csr = &musb_regs->indexed_csr; // index already set in process_epout() + const uint16_t mps = ep_csr->rx_maxp & MUSB_RXMAXP_PACKET_SIZE_M; + const uint16_t rx_count = ep_csr->rx_count; + const uint16_t xact_len = tu_min16(tu_min16(pipe->remaining, mps), rx_count); + volatile void *hwfifo = &musb_regs->fifo[epnum]; + if (xact_len) { + if (pipe->use_fifo) { + tu_hwfifo_read_to_fifo(hwfifo, pipe->fifo, xact_len, NULL); } else { - tu_hwfifo_write(fifo_ptr, buf, len, NULL); - pipe->buf = (uint8_t*)buf + len; + tu_hwfifo_read(hwfifo, pipe->buf, xact_len, NULL); + pipe->buf += xact_len; } - pipe->remaining = rem - len; + pipe->remaining -= xact_len; } - ep_csr->tx_csrl = MUSB_TXCSRL1_TXRDY; - // TU_LOG1(" TXCSRL%d = %x %d\r\n", epnum, ep_csr->tx_csrl, rem - len); - return false; + ep_csr->rx_csrl = 0; /* Clear RXRDY - release this FIFO slot */ + + return (xact_len < mps); } -static bool handle_xfer_out(uint8_t rhport, uint_fast8_t ep_addr) -{ - unsigned epnum = tu_edpt_number(ep_addr); - unsigned epnum_minus1 = epnum - 1; - pipe_state_t *pipe = &_dcd.pipe[tu_edpt_dir(ep_addr)][epnum_minus1]; - musb_regs_t* musb_regs = MUSB_REGS(rhport); +static void process_epout(uint8_t rhport, musb_regs_t *musb_regs, uint8_t epnum, bool is_isr) { musb_ep_csr_t* ep_csr = get_ep_csr(musb_regs, epnum); - // TU_LOG1(" RXCSRL%d = %x\r\n", epnum_minus1 + 1, ep_csr->rx_csrl); - - //Fail gracefully. Spurious interrupt. - if (!(ep_csr->rx_csrl & MUSB_RXCSRL1_RXRDY)) return false; + if (ep_csr->rx_csrl & MUSB_RXCSRL1_STALLED) { + ep_csr->rx_csrl &= ~(MUSB_RXCSRL1_STALLED | MUSB_RXCSRL1_OVER); + return; // sent STALL, do nothing + } - void *buf = pipe->buf; - if (buf == NULL) { - ep_csr->rx_csrl = MUSB_RXCSRL1_FLUSH; - return false; + // Fail gracefully. Spurious interrupt. + if (!(ep_csr->rx_csrl & MUSB_RXCSRL1_RXRDY)) { + return; } - const unsigned mps = ep_csr->rx_maxp; - const unsigned rem = pipe->remaining; - const unsigned vld = ep_csr->rx_count; - const unsigned len = TU_MIN(TU_MIN(rem, mps), vld); - volatile void *fifo_ptr = &musb_regs->fifo[epnum]; - if (len) { - if (_dcd.pipe_buf_is_fifo[TUSB_DIR_OUT] & TU_BIT(epnum_minus1)) { - tu_hwfifo_read_to_fifo(fifo_ptr, (tu_fifo_t *)buf, len, NULL); - } else { - tu_hwfifo_read(fifo_ptr, buf, len, NULL); - pipe->buf = (uint8_t*)buf + len; - } - pipe->remaining = rem - len; + pipe_state_t *pipe = pipe_get(epnum, TUSB_DIR_OUT); + if (!pipe->armed) { + // Packet is already ACK'd by hardware and sitting in the Rx FIFO, but no transfer is + // posted. Do NOT flush (per MUSB spec §3.3.11 FlushFIFO) - that would silently drop + // acknowledged data. Mask this endpoint's Rx interrupt so the ISR stops re-firing; + // the FIFO stays occupied so hardware NAKs further OUT tokens (natural backpressure). + // The next dcd_edpt_xfer() on this endpoint will drain the staged packet. + musb_regs->intr_rxen &= (uint16_t) ~TU_BIT(epnum); + return; } - ep_csr->rx_csrl = 0; /* Always Clear RXRDY bit */ - if ((len < mps) || (rem == len)) { + const bool is_short = pipe_read(musb_regs, pipe, epnum); + + // Transfer completes on a short packet or when the rx buffer is filled. + if (is_short || pipe->remaining == 0) { + const uint16_t xferred_len = pipe->length - pipe->remaining; pipe->buf = NULL; - return NULL != buf; + pipe->armed = false; + dcd_event_xfer_complete(rhport, epnum, xferred_len, XFER_RESULT_SUCCESS, is_isr); } - return false; } -static bool edpt_n_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t *buffer, uint16_t total_bytes) -{ - unsigned epnum = tu_edpt_number(ep_addr); - unsigned epnum_minus1 = epnum - 1; - unsigned dir_in = tu_edpt_dir(ep_addr); +static bool edpt_n_xfer(uint8_t rhport, uint8_t ep_addr, void *buffer, uint16_t total_bytes, bool use_fifo, bool is_isr) { + const uint8_t epnum = tu_edpt_number(ep_addr); + const unsigned dir_in = tu_edpt_dir(ep_addr); + + pipe_state_t *pipe = pipe_get(epnum, dir_in); + if (use_fifo) { + pipe->fifo = (tu_fifo_t *)buffer; + } else { + pipe->buf = (uint8_t *)buffer; + } + pipe->length = total_bytes; + pipe->remaining = total_bytes; + pipe->use_fifo = use_fifo; + pipe->armed = true; - pipe_state_t *pipe = &_dcd.pipe[dir_in][epnum_minus1]; - pipe->buf = buffer; - pipe->length = total_bytes; - pipe->remaining = total_bytes; + musb_regs_t *musb_regs = MUSB_REGS(rhport); + musb_ep_csr_t *ep_csr = get_ep_csr(musb_regs, epnum); if (dir_in) { - handle_xfer_in(rhport, ep_addr); + pipe_write(musb_regs, pipe, epnum); } else { - musb_regs_t* musb_regs = MUSB_REGS(rhport); - musb_ep_csr_t* ep_csr = get_ep_csr(musb_regs, epnum); - if (ep_csr->rx_csrl & MUSB_RXCSRL1_RXRDY) ep_csr->rx_csrl = 0; + // Re-enable Rx interrupt (may have been masked by the no-buffer path in process_epout) + musb_regs->intr_rxen |= (uint16_t)TU_BIT(epnum); + + // Drain any packet staged in the Rx FIFO from a prior no-buffer interrupt. + // process_epout() fires dcd_event_xfer_complete() itself if the drain completes. + if (ep_csr->rx_csrl & MUSB_RXCSRL1_RXRDY) { + process_epout(rhport, musb_regs, epnum, is_isr); + } } return true; } -static bool edpt0_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t *buffer, uint16_t total_bytes) -{ - (void)rhport; - TU_ASSERT(total_bytes <= 64); /* Current implementation supports for only up to 64 bytes. */ +static bool edpt0_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t *buffer, uint16_t total_bytes, bool is_isr) { + TU_ASSERT(total_bytes <= CFG_TUD_ENDPOINT0_SIZE); /* EP0 only supports 1 packet per dcd_edpt_xfer()*/ musb_regs_t* musb_regs = MUSB_REGS(rhport); musb_ep_csr_t* ep_csr = get_ep_csr(musb_regs, 0); - const unsigned req = _dcd.setup_packet.bmRequestType; - TU_ASSERT(req != REQUEST_TYPE_INVALID || total_bytes == 0); - - if (req == REQUEST_TYPE_INVALID || _dcd.status_out) { - /* STATUS OUT stage. - * MUSB controller automatically handles STATUS OUT packets without - * software helps. We do not have to do anything. And STATUS stage - * may have already finished and received the next setup packet - * without calling this function, so we have no choice but to - * invoke the callback function of status packet here. */ - // TU_LOG1(" STATUS OUT ep_csr->csr0l = %x\r\n", ep_csr->csr0l); - _dcd.status_out = 0; - if (req == REQUEST_TYPE_INVALID) { - dcd_event_xfer_complete(rhport, ep_addr, total_bytes, XFER_RESULT_SUCCESS, false); - } else { - /* The next setup packet has already been received, it aborts - * invoking callback function to avoid confusing TUSB stack. */ - TU_LOG1("Drop CONTROL_STAGE_ACK\r\n"); - } - return true; - } const unsigned dir_in = tu_edpt_dir(ep_addr); - if (tu_edpt_dir(req) == dir_in) { /* DATA stage */ - TU_ASSERT(total_bytes <= _dcd.remaining_ctrl); - const unsigned rem = _dcd.remaining_ctrl; - const unsigned len = TU_MIN(TU_MIN(rem, 64), total_bytes); - volatile void *fifo_ptr = &musb_regs->fifo[0]; - if (dir_in) { - tu_hwfifo_write(fifo_ptr, buffer, len, NULL); - _dcd.pipe0.buf = buffer + len; - _dcd.pipe0.length = len; - _dcd.pipe0.remaining = 0; - - _dcd.remaining_ctrl = rem - len; - if ((len < 64) || (rem == len)) { - _dcd.setup_packet.bmRequestType = REQUEST_TYPE_INVALID; /* Change to STATUS/SETUP stage */ - _dcd.status_out = 1; - /* Flush TX FIFO and reverse the transaction direction. */ - ep_csr->csr0l = MUSB_CSRL0_TXRDY | MUSB_CSRL0_DATAEND; + switch (_dcd.pipe0.state) { + case PIPE0_STATE_DATA: { + _dcd.pipe0.xact_len = total_bytes; + if (dir_in) { + // DATA IN: load FIFO, set TXRDY. Add DATAEND on the last chunk + // (ep0_remain_datalen == 0 after this load) to end the data stage. + tu_hwfifo_write(&musb_regs->fifo[0], buffer, total_bytes, NULL); + _dcd.pipe0.remain_wlength -= total_bytes; + if (_dcd.pipe0.remain_wlength == 0) { + ep_csr->csr0l = MUSB_CSRL0_TXRDY | MUSB_CSRL0_DATAEND; + } else { + ep_csr->csr0l = MUSB_CSRL0_TXRDY; + } } else { - ep_csr->csr0l = MUSB_CSRL0_TXRDY; /* Flush TX FIFO to return ACK. */ + // DATA OUT: arm drain target, ack RXRDY so host can send DATA OUT. + _dcd.pipe0.buf = buffer; + ep_csr->csr0l = MUSB_CSRL0_RXRDYC; } - // TU_LOG1(" IN ep_csr->csr0l = %x\r\n", ep_csr->csr0l); - } else { - // TU_LOG1(" OUT ep_csr->csr0l = %x\r\n", ep_csr->csr0l); - _dcd.pipe0.buf = buffer; - _dcd.pipe0.length = len; - _dcd.pipe0.remaining = len; - ep_csr->csr0l = MUSB_CSRL0_RXRDYC; /* Clear RX FIFO to return ACK. */ + break; } - } else if (dir_in) { - // TU_LOG1(" STATUS IN ep_csr->csr0l = %x\r\n", ep_csr->csr0l); - _dcd.pipe0.buf = NULL; - _dcd.pipe0.length = 0; - _dcd.pipe0.remaining = 0; - /* Clear RX FIFO and reverse the transaction direction */ - ep_csr->csr0l = MUSB_CSRL0_RXRDYC | MUSB_CSRL0_DATAEND; + + case PIPE0_STATE_STATUS_IN: + TU_ASSERT(dir_in && total_bytes == 0); // only STATUS IN allowed + ep_csr->csr0l = MUSB_CSRL0_RXRDYC | MUSB_CSRL0_DATAEND; + break; + + case PIPE0_STATE_STATUS_OUT: + TU_ASSERT(!dir_in && total_bytes == 0); // only STATUS OUT allowed + // First event of the STATUS OUT pair — wait for the IRQ to fire complete. + _dcd.pipe0.state = PIPE0_STATE_STATUS_OUT_PENDING; + break; + + case PIPE0_STATE_STATUS_OUT_PENDING: + // Second event — IRQ already arrived, fire complete now. + _dcd.pipe0.state = PIPE0_STATE_IDLE; + dcd_event_xfer_complete(rhport, ep_addr, 0, XFER_RESULT_SUCCESS, is_isr); + break; + + default: break; } + return true; } -static void process_ep0(uint8_t rhport) -{ +// 21.1.5: endpoint 0 service routine as peripheral +static void process_ep0(uint8_t rhport) { musb_regs_t* musb_regs = MUSB_REGS(rhport); musb_ep_csr_t* ep_csr = get_ep_csr(musb_regs, 0); uint_fast8_t csrl = ep_csr->csr0l; - // TU_LOG1(" EP0 ep_csr->csr0l = %x\r\n", csrl); - // 21.1.5: endpoint 0 service routine as peripheral - if (csrl & MUSB_CSRL0_STALLED) { - /* Returned STALL packet to HOST. */ - ep_csr->csr0l = 0; /* Clear STALL */ + ep_csr->csr0l = 0; + _dcd.pipe0.state = PIPE0_STATE_IDLE; return; } - unsigned req = _dcd.setup_packet.bmRequestType; if (csrl & MUSB_CSRL0_SETEND) { - TU_LOG1(" ABORT by the next packets\r\n"); + // Host aborted the current control transfer (new SETUP or premature STATUS). + // do nothing, it is probably another setup packet, usbd will reset its state. ep_csr->csr0l = MUSB_CSRL0_SETENDC; - if (req != REQUEST_TYPE_INVALID && _dcd.pipe0.buf) { - /* DATA stage was aborted by receiving STATUS or SETUP packet. */ - _dcd.pipe0.buf = NULL; - _dcd.setup_packet.bmRequestType = REQUEST_TYPE_INVALID; - dcd_event_xfer_complete(rhport, - req & TUSB_DIR_IN_MASK, - _dcd.pipe0.length - _dcd.pipe0.remaining, - XFER_RESULT_SUCCESS, true); + _dcd.pipe0.state = PIPE0_STATE_IDLE; + if (!(csrl & MUSB_CSRL0_RXRDY)) { + return; /* no SETUP waiting behind it */ } - req = REQUEST_TYPE_INVALID; - if (!(csrl & MUSB_CSRL0_RXRDY)) return; /* Received SETUP packet */ } + // Receive Data (Setup or OUT) if (csrl & MUSB_CSRL0_RXRDY) { - /* Received SETUP or DATA OUT packet */ - if (req == REQUEST_TYPE_INVALID) { - /* SETUP */ - TU_ASSERT(sizeof(tusb_control_request_t) == ep_csr->count0,); - process_setup_packet(rhport); - return; - } - if (_dcd.pipe0.buf) { - /* DATA OUT */ - const unsigned vld = ep_csr->count0; - const unsigned rem = _dcd.pipe0.remaining; - const unsigned len = TU_MIN(TU_MIN(rem, 64), vld); - volatile void *fifo_ptr = &musb_regs->fifo[0]; - tu_hwfifo_read(fifo_ptr, _dcd.pipe0.buf, len, NULL); + const uint16_t count0 = ep_csr->count0; + switch (_dcd.pipe0.state) { + case PIPE0_STATE_IDLE: + TU_ASSERT(sizeof(tusb_control_request_t) == count0, ); + union { + tusb_control_request_t req; + uint32_t u32[2]; + } setup_packet; + setup_packet.u32[0] = musb_regs->fifo[0]; + setup_packet.u32[1] = musb_regs->fifo[0]; - _dcd.pipe0.remaining = rem - len; - _dcd.remaining_ctrl -= len; + _dcd.pipe0.remain_wlength = setup_packet.req.wLength; - _dcd.pipe0.buf = NULL; - dcd_event_xfer_complete(rhport, - tu_edpt_addr(0, TUSB_DIR_OUT), - _dcd.pipe0.length - _dcd.pipe0.remaining, - XFER_RESULT_SUCCESS, true); - } - return; - } + if (setup_packet.req.wLength == 0) { + _dcd.pipe0.state = PIPE0_STATE_STATUS_IN; + } else { + _dcd.pipe0.state = PIPE0_STATE_DATA; + // If OUT (rx) direction, let edpt0_xfer() clear RXRDY when it's ready to receive data. + if (setup_packet.req.bmRequestType & TUSB_DIR_IN_MASK) { + ep_csr->csr0l = MUSB_CSRL0_RXRDYC; + } + } + dcd_event_setup_received(rhport, (const uint8_t *)&setup_packet.req, true); + break; - /* When CSRL0 is zero, it means that completion of sending a any length packet - * or receiving a zero length packet. */ - if (req != REQUEST_TYPE_INVALID && !tu_edpt_dir(req)) { - /* STATUS IN */ - if (*(const uint16_t*)(uintptr_t)&_dcd.setup_packet == 0x0500) { - /* The address must be changed on completion of the control transfer. */ - musb_regs->faddr = (uint8_t)_dcd.setup_packet.wValue; + case PIPE0_STATE_DATA: { + // EP0 OUT is single-packet (TU_ASSERT total_bytes <= EP0_SIZE in edpt0_xfer) + // so the whole packet drains in one shot. + if (count0) { + tu_hwfifo_read(&musb_regs->fifo[0], _dcd.pipe0.buf, count0, NULL); + _dcd.pipe0.remain_wlength -= count0; + } + if (_dcd.pipe0.remain_wlength == 0) { + // last packet: change state and leave RXRDY for edpt0_xfer(STATUS IN) to ack + _dcd.pipe0.state = PIPE0_STATE_STATUS_IN; + } else { + ep_csr->csr0l = MUSB_CSRL0_RXRDYC; + } + dcd_event_xfer_complete(rhport, TU_EP0_OUT, count0, XFER_RESULT_SUCCESS, true); + break; + } + + default: break; } - _dcd.setup_packet.bmRequestType = REQUEST_TYPE_INVALID; - dcd_event_xfer_complete(rhport, - tu_edpt_addr(0, TUSB_DIR_IN), - _dcd.pipe0.length - _dcd.pipe0.remaining, - XFER_RESULT_SUCCESS, true); + return; } - if (_dcd.pipe0.buf) { - /* DATA IN */ - _dcd.pipe0.buf = NULL; - dcd_event_xfer_complete(rhport, - tu_edpt_addr(0, TUSB_DIR_IN), - _dcd.pipe0.length - _dcd.pipe0.remaining, - XFER_RESULT_SUCCESS, true); - } -} -static void process_edpt_n(uint8_t rhport, uint_fast8_t ep_addr) -{ - bool completed; - const unsigned dir_in = tu_edpt_dir(ep_addr); - const unsigned epn = tu_edpt_number(ep_addr); - const unsigned epn_minus1 = epn - 1; + /* When CSRL0 is zero, it means that either + * - completion of sending any length packet TxPktRdy clear + * - or status stage is complete (ZLP) after DataEnd is set */ + switch (_dcd.pipe0.state) { + case PIPE0_STATE_DATA: + // csrl == 0 in DATA state = TXRDY just cleared, i.e. a DATA IN packet was successfully sent. If the just-sent + // packet was the last (DATAEND was set when ep0_remain_datalen hit zero), transition + // to STATUS_OUT to await the host's STATUS-OUT ZLP confirmation IRQ. + if (_dcd.pipe0.remain_wlength == 0) { + _dcd.pipe0.state = PIPE0_STATE_STATUS_OUT; + } + dcd_event_xfer_complete(rhport, TU_EP0_IN, _dcd.pipe0.xact_len, XFER_RESULT_SUCCESS, true); + break; - musb_regs_t* musb_regs = MUSB_REGS(rhport); - musb_ep_csr_t* ep_csr = get_ep_csr(musb_regs, epn); - if (dir_in) { - // TU_LOG1(" TX CSRL%d = %x\r\n", epn, ep_csr->tx_csrl); - if (ep_csr->tx_csrl & MUSB_TXCSRL1_STALLED) { - ep_csr->tx_csrl = (uint8_t)(ep_csr->tx_csrl & ~(MUSB_TXCSRL1_STALLED | MUSB_TXCSRL1_UNDRN)); - return; - } - completed = handle_xfer_in(rhport, ep_addr); - } else { - // TU_LOG1(" RX CSRL%d = %x\r\n", epn, ep_csr->rx_csrl); - if (ep_csr->rx_csrl & MUSB_RXCSRL1_STALLED) { - ep_csr->rx_csrl = (uint8_t)(ep_csr->rx_csrl & ~(MUSB_RXCSRL1_STALLED | MUSB_RXCSRL1_OVER)); - return; - } - completed = handle_xfer_out(rhport, ep_addr); - } + case PIPE0_STATE_STATUS_OUT: + // First event of the STATUS OUT pair — wait for edpt0_xfer(STATUS OUT) to fire complete. + _dcd.pipe0.state = PIPE0_STATE_STATUS_OUT_PENDING; + break; + + case PIPE0_STATE_STATUS_OUT_PENDING: + // Second event — edpt0_xfer(STATUS OUT) already called, fire complete now. + _dcd.pipe0.state = PIPE0_STATE_IDLE; + dcd_event_xfer_complete(rhport, TU_EP0_OUT, 0, XFER_RESULT_SUCCESS, true); + break; - if (completed) { - pipe_state_t *pipe = &_dcd.pipe[dir_in][epn_minus1]; - dcd_event_xfer_complete(rhport, ep_addr, - pipe->length - pipe->remaining, - XFER_RESULT_SUCCESS, true); + case PIPE0_STATE_STATUS_IN: + if (_dcd.pipe0.pending_addr) { + musb_regs->faddr = _dcd.pipe0.pending_addr; + _dcd.pipe0.pending_addr = 0; + } + _dcd.pipe0.state = PIPE0_STATE_IDLE; + dcd_event_xfer_complete(rhport, TU_EP0_IN, 0, XFER_RESULT_SUCCESS, true); + break; + + default: break; } } @@ -478,11 +523,10 @@ static void process_bus_reset(uint8_t rhport) { alloced_fifo_bytes = CFG_TUD_ENDPOINT0_SIZE; #endif - /* When bmRequestType is REQUEST_TYPE_INVALID(0xFF), a control transfer state is SETUP or STATUS stage. */ - _dcd.setup_packet.bmRequestType = REQUEST_TYPE_INVALID; - _dcd.status_out = 0; - /* When pipe0.buf has not NULL, DATA stage works in progress. */ + _dcd.pipe0.state = PIPE0_STATE_IDLE; _dcd.pipe0.buf = NULL; + _dcd.pipe0.xact_len = 0; + _dcd.pipe0.remain_wlength = 0; musb->intr_txen = 1; /* Enable only EP0 */ musb->intr_rxen = 0; @@ -544,17 +588,19 @@ void dcd_int_disable(uint8_t rhport) { musb_dcd_int_disable(rhport); } -// Receive Set Address request, mcu port must also include status IN response +// Receive Set Address request. Stash the new address here; hardware faddr is +// latched from pending_addr in process_ep0 once the STATUS IN completes (per +// USB spec, address must only take effect after the status stage). void dcd_set_address(uint8_t rhport, uint8_t dev_addr) { - (void)dev_addr; musb_regs_t* musb_regs = MUSB_REGS(rhport); musb_ep_csr_t* ep_csr = get_ep_csr(musb_regs, 0); - _dcd.pipe0.buf = NULL; - _dcd.pipe0.length = 0; - _dcd.pipe0.remaining = 0; - /* Clear RX FIFO to return ACK. */ + _dcd.pipe0.pending_addr = dev_addr; + _dcd.pipe0.buf = NULL; + _dcd.pipe0.xact_len = 0; + _dcd.pipe0.state = PIPE0_STATE_STATUS_IN; + /* Send STATUS IN ZLP with DATAEND; host ACK fires the confirmation IRQ. */ ep_csr->csr0l = MUSB_CSRL0_RXRDYC | MUSB_CSRL0_DATAEND; } @@ -595,39 +641,36 @@ void dcd_sof_enable(uint8_t rhport, bool en) //--------------------------------------------------------------------+ // Endpoint API //--------------------------------------------------------------------+ -// static void edpt_setup(musb_regs_t* musb, uint8_t ep_addr, uint8_t ep_type, uint16_t ep_size){ -// const unsigned epn = tu_edpt_number(ep_addr); -// const unsigned dir_in = tu_edpt_dir(ep_addr); -// } // Configure endpoint's registers according to descriptor bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const * ep_desc) { const unsigned ep_addr = ep_desc->bEndpointAddress; const unsigned epn = tu_edpt_number(ep_addr); - const unsigned dir_in = tu_edpt_dir(ep_addr); + const unsigned epdir = tu_edpt_dir(ep_addr); const unsigned mps = tu_edpt_packet_size(ep_desc); - pipe_state_t *pipe = &_dcd.pipe[dir_in][epn - 1]; + pipe_state_t *pipe = pipe_get(epn, epdir); pipe->buf = NULL; pipe->length = 0; pipe->remaining = 0; + pipe->armed = false; musb_regs_t* musb = MUSB_REGS(rhport); musb_ep_csr_t* ep_csr = get_ep_csr(musb, epn); - const uint8_t is_rx = 1 - dir_in; + const uint8_t is_rx = (1 - epdir); musb_ep_maxp_csr_t* maxp_csr = &ep_csr->maxp_csr[is_rx]; maxp_csr->maxp = mps; maxp_csr->csrh = 0; #if MUSB_CFG_SHARED_FIFO - if (dir_in) { + if (epdir) { maxp_csr->csrh |= MUSB_CSRH_TX_MODE; } #endif hwfifo_flush(musb, epn, is_rx, true); - TU_ASSERT(hwfifo_config(musb, epn, is_rx, mps, false)); + TU_ASSERT(hwfifo_config(musb, epn, is_rx, mps, ep_desc->bmAttributes.xfer == TUSB_XFER_BULK)); musb->intren_ep[is_rx] |= TU_BIT(epn); return true; @@ -652,10 +695,11 @@ bool dcd_edpt_iso_activate(uint8_t rhport, tusb_desc_endpoint_t const *ep_desc ) unsigned const ie = musb_dcd_get_int_enable(rhport); musb_dcd_int_disable(rhport); - pipe_state_t *pipe = &_dcd.pipe[dir_in][epn - 1]; + pipe_state_t *pipe = pipe_get(epn, dir_in); pipe->buf = NULL; pipe->length = 0; pipe->remaining = 0; + pipe->armed = false; musb_regs_t* musb = MUSB_REGS(rhport); musb_ep_csr_t* ep_csr = get_ep_csr(musb, epn); @@ -713,22 +757,22 @@ void dcd_edpt_close_all(uint8_t rhport) // Submit a transfer, When complete dcd_event_xfer_complete() is invoked to notify the stack bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t * buffer, uint16_t total_bytes, bool is_isr) { - (void) is_isr; (void)rhport; bool ret; - // TU_LOG1("X %x %d\r\n", ep_addr, total_bytes); unsigned const epnum = tu_edpt_number(ep_addr); unsigned const ie = musb_dcd_get_int_enable(rhport); musb_dcd_int_disable(rhport); if (epnum) { - _dcd.pipe_buf_is_fifo[tu_edpt_dir(ep_addr)] &= ~TU_BIT(epnum - 1); - ret = edpt_n_xfer(rhport, ep_addr, buffer, total_bytes); + ret = edpt_n_xfer(rhport, ep_addr, buffer, total_bytes, false, is_isr); } else { - ret = edpt0_xfer(rhport, ep_addr, buffer, total_bytes); + (void) is_isr; + ret = edpt0_xfer(rhport, ep_addr, buffer, total_bytes, is_isr); } - if (ie) musb_dcd_int_enable(rhport); + if (ie) { + musb_dcd_int_enable(rhport); + } return ret; } @@ -736,16 +780,13 @@ bool dcd_edpt_xfer(uint8_t rhport, uint8_t ep_addr, uint8_t * buffer, uint16_t t // - optional, however, must be listed in usbd.c bool dcd_edpt_xfer_fifo(uint8_t rhport, uint8_t ep_addr, tu_fifo_t * ff, uint16_t total_bytes, bool is_isr) { - (void) is_isr; (void)rhport; bool ret; - // TU_LOG1("X %x %d\r\n", ep_addr, total_bytes); unsigned const epnum = tu_edpt_number(ep_addr); TU_ASSERT(epnum); unsigned const ie = musb_dcd_get_int_enable(rhport); musb_dcd_int_disable(rhport); - _dcd.pipe_buf_is_fifo[tu_edpt_dir(ep_addr)] |= TU_BIT(epnum - 1); - ret = edpt_n_xfer(rhport, ep_addr, (uint8_t*)ff, total_bytes); + ret = edpt_n_xfer(rhport, ep_addr, ff, total_bytes, true, is_isr); if (ie) musb_dcd_int_enable(rhport); return ret; } @@ -760,14 +801,17 @@ void dcd_edpt_stall(uint8_t rhport, uint8_t ep_addr) { musb_ep_csr_t* ep_csr = get_ep_csr(musb_regs, epn); if (0 == epn) { - if (!ep_addr) { /* Ignore EP80 */ - _dcd.setup_packet.bmRequestType = REQUEST_TYPE_INVALID; + if (ep_addr == TU_EP0_OUT) { /* Ignore EP0 OUT */ + _dcd.pipe0.state = PIPE0_STATE_IDLE; _dcd.pipe0.buf = NULL; ep_csr->csr0l = MUSB_CSRL0_STALL; } } else { - const uint8_t is_rx = 1 - tu_edpt_dir(ep_addr); + const tusb_dir_t ep_dir = tu_edpt_dir(ep_addr); + const uint8_t is_rx = (ep_dir == TUSB_DIR_OUT ? 1u : 0u); ep_csr->maxp_csr[is_rx].csrl = MUSB_CSRL_SEND_STALL(is_rx); + pipe_state_t* pipe = pipe_get(epn, ep_dir); + pipe->armed = false; } if (ie) musb_dcd_int_enable(rhport); @@ -785,7 +829,7 @@ void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) musb_ep_csr_t* ep_csr = get_ep_csr(musb_regs, epn); const uint8_t is_rx = 1 - tu_edpt_dir(ep_addr); - ep_csr->maxp_csr[is_rx].csrl = (uint8_t)MUSB_CSRL_CLEAR_DATA_TOGGLE(is_rx); + ep_csr->maxp_csr[is_rx].csrl = MUSB_CSRL_CLEAR_DATA_TOGGLE(is_rx); if (ie) musb_dcd_int_enable(rhport); } @@ -801,8 +845,8 @@ void dcd_int_handler(uint8_t rhport) { musb_dcd_int_handler_enter(rhport); uint_fast8_t intr_usb = musb_regs->intr_usb; // a read will clear this interrupt status - uint_fast16_t intr_tx = musb_regs->intr_tx; // a read will clear this interrupt status - uint_fast16_t intr_rx = musb_regs->intr_rx; // a read will clear this interrupt status + uint_fast8_t intr_tx = musb_regs->intr_tx; // a read will clear this interrupt status + uint_fast8_t intr_rx = musb_regs->intr_rx; // a read will clear this interrupt status // TU_LOG1("D%2x T%2x R%2x\r\n", is, txis, rxis); intr_usb &= musb_regs->intr_usben; /* Clear disabled interrupts */ @@ -822,21 +866,35 @@ void dcd_int_handler(uint8_t rhport) { } intr_tx &= musb_regs->intr_txen; /* Clear disabled interrupts */ - if (intr_tx & TU_BIT(0)) { - process_ep0(rhport); - intr_tx &= ~TU_BIT(0); - } + while (intr_tx) { - unsigned const num = __builtin_ctz(intr_tx); - process_edpt_n(rhport, tu_edpt_addr(num, TUSB_DIR_IN)); - intr_tx &= ~TU_BIT(num); + const unsigned epnum = __builtin_ctz(intr_tx); + if (epnum == 0) { + process_ep0(rhport); // EP0 has its own state machine (control transfers) + } else { + process_epin(rhport, musb_regs, epnum); + } + intr_tx &= ~TU_BIT(epnum); + + // Double packet endpoint: TxPktRdy is clear, and interrupt is generated immediately when 1st packet is written. + // Also catches EP0 SETUP arriving during bulk processing. + uint_fast8_t new_intr_tx = musb_regs->intr_tx; + new_intr_tx &= musb_regs->intr_txen; + + intr_tx |= new_intr_tx; } intr_rx &= musb_regs->intr_rxen; /* Clear disabled interrupts */ while (intr_rx) { - unsigned const num = __builtin_ctz(intr_rx); - process_edpt_n(rhport, tu_edpt_addr(num, TUSB_DIR_OUT)); - intr_rx &= ~TU_BIT(num); + unsigned const epnum = __builtin_ctz(intr_rx); + process_epout(rhport, musb_regs, epnum, true); + intr_rx &= ~TU_BIT(epnum); + + // Double packet endpoint: RxPktRdy is set and interrupt is generated immediately if 2nd packet is received + uint_fast8_t new_intr_rx = musb_regs->intr_rx; + new_intr_rx &= musb_regs->intr_rxen; + + intr_rx |= new_intr_rx; } musb_regs->index = saved_index; // restore endpoint index diff --git a/src/portable/mentor/musb/musb_type.h b/src/portable/mentor/musb/musb_type.h index 176504a2f..3d3c3c834 100644 --- a/src/portable/mentor/musb/musb_type.h +++ b/src/portable/mentor/musb/musb_type.h @@ -336,7 +336,7 @@ TU_ATTR_ALWAYS_INLINE static inline musb_ep_csr_t* get_ep_csr(musb_regs_t* musb_ #define MUSB_CSRL_CLEAR_DATA_TOGGLE(_rx) (1u << ((_rx) ? 7 : 6)) // 0x13, 0x17: TX/RX CSRH -#define MUSB_CSRH_DISABLE_DOUBLE_PACKET(_rx) (1u << 1) +#define MUSB_CSRH_DISABLE_DOUBLE_PACKET (1u << 1) #define MUSB_CSRH_TX_MODE (1u << 5) // 1 = TX, 0 = RX. only relevant for SHARED FIFO #define MUSB_CSRH_ISO (1u << 6) @@ -568,6 +568,16 @@ TU_ATTR_ALWAYS_INLINE static inline musb_ep_csr_t* get_ep_csr(musb_regs_t* musb_ //***************************************************************************** // +// The following are defines for the bit fields in the MUSB_O_TXMAXP / MUSB_O_RXMAXP +// registers. Bits [10:0] carry the maximum packet size; bits [15:11] carry +// numpackminus1 (HB-iso / HS-bulk multiplier - 1). +// +//***************************************************************************** +#define MUSB_TXMAXP_PACKET_SIZE_M 0x07FFu +#define MUSB_RXMAXP_PACKET_SIZE_M 0x07FFu + +//***************************************************************************** +// // The following are defines for the bit fields in the MUSB_O_TXCSRL1 register. // //***************************************************************************** diff --git a/src/portable/nordic/nrf5x/dcd_nrf5x.c b/src/portable/nordic/nrf5x/dcd_nrf5x.c index 6ed5fde8e..befbaa338 100644 --- a/src/portable/nordic/nrf5x/dcd_nrf5x.c +++ b/src/portable/nordic/nrf5x/dcd_nrf5x.c @@ -1049,6 +1049,12 @@ void tusb_hal_nrf_power_event(uint32_t event) { NVIC_EnableIRQ(USBD_IRQn); } + // Ensure HFCLK is requested in the current context. The hfclk_enable() in + // USB_EVT_DETECTED may have been pre-SoftDevice. After Softdevice is + // enabled, HFXO is physically off again. So any caller that fires + // USB_EVT_READY post-SD would hang here. + hfclk_enable(); + // Wait for HFCLK while (!hfclk_running()) {} diff --git a/src/portable/synopsys/dwc2/dcd_dwc2.c b/src/portable/synopsys/dwc2/dcd_dwc2.c index bdab49f1b..8997394dd 100644 --- a/src/portable/synopsys/dwc2/dcd_dwc2.c +++ b/src/portable/synopsys/dwc2/dcd_dwc2.c @@ -1157,8 +1157,9 @@ static void handle_incomplete_iso_in(uint8_t rhport) { } epin->diepctl = depctl.value; } else { - // too many retries, give up + // too many retries, give up, but keep endpoint activated edpt_disable(rhport, epnum | TUSB_DIR_IN_MASK, false); + epin->diepctl |= DIEPCTL_USBAEP; dcd_event_xfer_complete(rhport, epnum | TUSB_DIR_IN_MASK, 0, XFER_RESULT_FAILED, true); } } 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/src/tinyusb.mk b/src/tinyusb.mk index 169098016..e3ef35dcf 100644 --- a/src/tinyusb.mk +++ b/src/tinyusb.mk @@ -3,7 +3,6 @@ TINYUSB_SRC_C += \ src/tusb.c \ src/common/tusb_fifo.c \ src/device/usbd.c \ - src/device/usbd_control.c \ src/typec/usbc.c \ src/class/audio/audio_device.c \ src/class/cdc/cdc_device.c \ diff --git a/src/tusb.c b/src/tusb.c index 5e4422e41..5d656fb8c 100644 --- a/src/tusb.c +++ b/src/tusb.c @@ -224,32 +224,31 @@ uint8_t const* tu_desc_find3(uint8_t const* desc, uint8_t const* end, uint8_t by // Endpoint Helper for both Host and Device stack //--------------------------------------------------------------------+ -bool tu_edpt_claim(tu_edpt_state_t* ep_state, osal_mutex_t mutex) { +bool tu_edpt_claim(volatile uint8_t* ep_state, osal_mutex_t mutex) { (void) mutex; // pre-check to help reducing mutex lock - TU_VERIFY(ep_state->busy == 0); - TU_VERIFY(ep_state->claimed == 0); + TU_VERIFY((*ep_state & (TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED)) == 0); (void) osal_mutex_lock(mutex, OSAL_TIMEOUT_WAIT_FOREVER); // can only claim the endpoint if it is not busy and not claimed yet. - bool const available = (ep_state->busy == 0) && (ep_state->claimed == 0); + bool const available = (*ep_state & (TU_EDPT_STATE_BUSY | TU_EDPT_STATE_CLAIMED)) == 0; if (available) { - ep_state->claimed = 1; + *ep_state |= TU_EDPT_STATE_CLAIMED; } (void) osal_mutex_unlock(mutex); return available; } -bool tu_edpt_release(tu_edpt_state_t* ep_state, osal_mutex_t mutex) { +bool tu_edpt_release(volatile uint8_t* ep_state, osal_mutex_t mutex) { (void) mutex; (void) osal_mutex_lock(mutex, OSAL_TIMEOUT_WAIT_FOREVER); // can only release the endpoint if it is claimed and not busy - bool const ret = (ep_state->claimed == 1) && (ep_state->busy == 0); + bool const ret = (*ep_state & (TU_EDPT_STATE_CLAIMED | TU_EDPT_STATE_BUSY)) == TU_EDPT_STATE_CLAIMED; if (ret) { - ep_state->claimed = 0; + *ep_state &= (uint8_t) ~TU_EDPT_STATE_CLAIMED; } (void) osal_mutex_unlock(mutex); diff --git a/test/fuzz/rules.mk b/test/fuzz/rules.mk index 329dcce11..c14330312 100644 --- a/test/fuzz/rules.mk +++ b/test/fuzz/rules.mk @@ -23,7 +23,6 @@ SRC_C += \ src/tusb.c \ src/common/tusb_fifo.c \ src/device/usbd.c \ - src/device/usbd_control.c \ src/class/audio/audio_device.c \ src/class/cdc/cdc_device.c \ src/class/dfu/dfu_device.c \ diff --git a/test/hil/hil_ci.sh b/test/hil/hil_ci.sh index fa8bb0245..96872e2e1 100644 --- a/test/hil/hil_ci.sh +++ b/test/hil/hil_ci.sh @@ -1,15 +1,24 @@ -#!/bin/bash +#!/usr/bin/env bash # Run HIL test remotely on ci.lan # Usage: test/hil/hil_ci.sh [-b BOARD] [-t TEST] [extra hil_test.py args...] # Example: # test/hil/hil_ci.sh -b stm32f723disco # test/hil/hil_ci.sh -b stm32f723disco -t host/cdc_msc_hid -r 1 +# +# Env overrides: REMOTE, REMOTE_DIR, CONFIG (path to HIL config json), +# ROOT_DIR (tinyusb checkout to test; defaults to the script's own checkout). -set -e +set -euo pipefail + +REMOTE=${REMOTE:-ci.lan} +REMOTE_DIR=${REMOTE_DIR:-/tmp/tinyusb-hil} +ROOT_DIR=${ROOT_DIR:-$(cd "$(dirname "$0")/../.." && pwd)} +CONFIG=${CONFIG:-$ROOT_DIR/test/hil/tinyusb.json} -REMOTE=ci.lan -REMOTE_DIR=/tmp/tinyusb-hil -SCRIPT_DIR="$(cd "$(dirname "$0")/../.." && pwd)" +[[ -f "$ROOT_DIR/test/hil/hil_test.py" && -d "$ROOT_DIR/examples" ]] || { + echo "error: $ROOT_DIR does not look like a tinyusb checkout" >&2 + exit 1 +} # Parse -b BOARD from arguments to know which build to copy BOARD="" @@ -17,6 +26,7 @@ ARGS=() while [[ $# -gt 0 ]]; do case "$1" in -b) + [[ $# -ge 2 ]] || { echo "error: -b requires a BOARD argument" >&2; exit 1; } BOARD="$2" ARGS+=("$1" "$2") shift 2 @@ -28,42 +38,52 @@ while [[ $# -gt 0 ]]; do esac done -# Setup remote directory +# Setup remote directory. Use `bash -s` + heredoc so REMOTE_DIR (user-overridable) +# is passed as a positional parameter and never reinterpreted by the remote shell. echo "==> Setting up remote $REMOTE:$REMOTE_DIR" -ssh "$REMOTE" "rm -rf $REMOTE_DIR && mkdir -p $REMOTE_DIR/test/hil $REMOTE_DIR/examples" +ssh "$REMOTE" bash -s -- "$REMOTE_DIR" <<'REMOTE' +set -e +rm -rf -- "$1" +mkdir -p -- "$1/test/hil" "$1/examples" +REMOTE # Copy HIL test script and config echo "==> Copying test scripts" -scp -q "$SCRIPT_DIR/test/hil/hil_test.py" \ - "$SCRIPT_DIR/test/hil/pymtp.py" \ - "$SCRIPT_DIR/test/hil/tinyusb.json" \ +scp -q "$ROOT_DIR/test/hil/hil_test.py" \ + "$ROOT_DIR/test/hil/pymtp.py" \ + "$CONFIG" \ "$REMOTE:$REMOTE_DIR/test/hil/" # Copy only firmware binaries (elf/bin/hex), preserving directory structure copy_board_binaries() { local src="$1" - local board_name - board_name=$(basename "$src") - rsync -a --include='*/' --include='*.elf' --include='*.bin' --include='*.hex' --exclude='*' \ + rsync -a --prune-empty-dirs \ + --include='*/' --include='*.elf' --include='*.bin' --include='*.hex' --exclude='*' \ "$src" "$REMOTE:$REMOTE_DIR/examples/" } if [ -n "$BOARD" ]; then - BUILD_DIR="$SCRIPT_DIR/examples/cmake-build-$BOARD" + BUILD_DIR="$ROOT_DIR/examples/cmake-build-$BOARD" if [ ! -d "$BUILD_DIR" ]; then echo "Error: build directory not found: $BUILD_DIR" - echo "Build first with: cd examples && cmake -DBOARD=$BOARD -G Ninja -B cmake-build-$BOARD .. && cmake --build cmake-build-$BOARD" + echo "Build first with: cd examples && cmake -DBOARD=$BOARD -G Ninja -B cmake-build-$BOARD . && cmake --build cmake-build-$BOARD" exit 1 fi echo "==> Copying binaries for $BOARD" copy_board_binaries "$BUILD_DIR" else echo "==> Copying all built binaries" - for dir in "$SCRIPT_DIR"/examples/cmake-build-*/; do + for dir in "$ROOT_DIR"/examples/cmake-build-*/; do [ -d "$dir" ] && copy_board_binaries "$dir" done fi -# Run test +# Run test. Use `bash -s` so REMOTE_DIR + ARGS reach the remote shell as positional +# parameters; quoting and metacharacters in args are preserved. +CONFIG_BASENAME="$(basename "$CONFIG")" echo "==> Running HIL test on $REMOTE" -ssh -t "$REMOTE" "cd $REMOTE_DIR && python3 -u test/hil/hil_test.py -B examples ${ARGS[*]} tinyusb.json" +ssh "$REMOTE" bash -s -- "$REMOTE_DIR" "${ARGS[@]}" "test/hil/$CONFIG_BASENAME" <<'REMOTE' +cd -- "$1" +shift +exec python3 -u test/hil/hil_test.py -B examples "$@" +REMOTE diff --git a/test/hil/hil_ci_set_matrix.py b/test/hil/hil_ci_set_matrix.py index ecd964d87..2cce35ae2 100644 --- a/test/hil/hil_ci_set_matrix.py +++ b/test/hil/hil_ci_set_matrix.py @@ -3,45 +3,60 @@ import json import os +def _resolve_config_path(config_file): + if os.path.exists(config_file): + return config_file + + script_relative = os.path.join(os.path.dirname(__file__), config_file) + if os.path.exists(script_relative): + return script_relative + + raise FileNotFoundError(f'Config file not found: {config_file}') + + def main(): parser = argparse.ArgumentParser() - parser.add_argument('config_file', help='Configuration JSON file') + parser.add_argument('config_files', nargs='+', help='Configuration JSON file(s)') args = parser.parse_args() - config_file = args.config_file - - # 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) - matrix = { 'arm-gcc': [], 'esp-idf': [] } - for board in config['boards']: - name = board['name'] - flasher = board['flasher'] - if flasher['name'] == 'esptool': - toolchain = 'esp-idf' - else: - toolchain = 'arm-gcc' - build_board = f'-b {name}' - if 'build' in board: - if 'args' in board['build']: - build_board += ' ' + ' '.join(f'-D{a}' for a in board['build']['args']) - if 'flags_on' in board['build']: - for f in board['build']['flags_on']: - if f == '': - matrix[toolchain].append(build_board) - else: - matrix[toolchain].append(f'{build_board} -f1 {f.replace(" ", " -f1 ")}') + seen = {toolchain: set() for toolchain in matrix} + + def append_build_arg(toolchain, build_arg): + if build_arg not in seen[toolchain]: + seen[toolchain].add(build_arg) + matrix[toolchain].append(build_arg) + + for config_file in args.config_files: + with open(_resolve_config_path(config_file)) as f: + config = json.load(f) + + for board in config['boards']: + name = board['name'] + flasher = board['flasher'] + if flasher['name'] == 'esptool': + toolchain = 'esp-idf' + else: + toolchain = 'arm-gcc' + + build_board = f'-b {name}' + if 'build' in board: + if 'args' in board['build']: + build_board += ' ' + ' '.join(f'-D{a}' for a in board['build']['args']) + if 'flags_on' in board['build']: + for f in board['build']['flags_on']: + if f == '': + append_build_arg(toolchain, build_board) + else: + append_build_arg(toolchain, f'{build_board} -f1 {f.replace(" ", " -f1 ")}') + else: + append_build_arg(toolchain, build_board) else: - matrix[toolchain].append(build_board) - else: - matrix[toolchain].append(build_board) + append_build_arg(toolchain, build_board) print(json.dumps(matrix)) diff --git a/test/hil/hil_test.py b/test/hil/hil_test.py index d50a60894..ed9ebbf1a 100755 --- a/test/hil/hil_test.py +++ b/test/hil/hil_test.py @@ -27,12 +27,18 @@ # ACTION=="add", SUBSYSTEM=="block", SUBSYSTEMS=="usb", ENV{ID_FS_USAGE}=="filesystem", MODE="0666", PROGRAM="/bin/sh -c 'echo $$ID_SERIAL_SHORT | rev | cut -c -8 | rev'", RUN{program}+="/usr/bin/systemd-mount --no-block --automount=yes --collect $devnode /media/blkUSB_%c.%s{bInterfaceNumber}" import argparse +import io import os import random import re +import select import sys import time import warnings +import signal +from contextlib import redirect_stdout +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") @@ -43,7 +49,9 @@ import serial import subprocess import json import glob -from multiprocessing import Pool +import shutil +from multiprocessing import Pool, Lock +from multiprocessing import TimeoutError as MpTimeoutError import fs import hashlib import ctypes @@ -58,7 +66,82 @@ STATUS_SKIPPED = "\033[33mSkipped\033[0m" verbose = False test_only = [] +board_test = {} build_dir = 'cmake-build' +skip_flash = False +print_lock = None + + +def init_worker(lock): + global print_lock + print_lock = lock + + +def log_line(msg: str) -> None: + out = sys.__stdout__ if sys.__stdout__ is not None else sys.stdout + if print_lock is not None: + with print_lock: + print(msg, file=out, flush=True) + else: + print(msg, file=out, flush=True) + + +def compact_output(raw: str) -> str: + if not raw: + return '' + lines = [ln.strip() for ln in raw.replace('\r', '\n').split('\n') if ln.strip()] + return ' | '.join(lines) + +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 @@ -88,8 +171,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): @@ -116,7 +199,20 @@ 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 get_alsa_capture_dev(id): + pattern = f'/dev/snd/by-id/usb-*_{id}-*' + for dev in glob.glob(pattern): + try: + link = os.path.basename(os.path.realpath(dev)) + except OSError: + continue + m = re.match(r'controlC(\d+)', link) + if m: + return f'hw:{m.group(1)},0' + return None + + +def open_serial_dev(port: str): timeout = ENUM_TIMEOUT ser = None while timeout > 0: @@ -131,10 +227,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 @@ -151,8 +248,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): @@ -174,7 +270,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 '' @@ -188,7 +284,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: @@ -203,41 +299,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 @@ -300,16 +432,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 @@ -328,17 +464,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 @@ -681,7 +817,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() @@ -737,56 +873,81 @@ def test_device_cdc_msc(board): data = read_disk_file(uid, 0, 'README.TXT') assert data == MSC_README_TXT, f'MSC wrong data in README.TXT\n expected: {MSC_README_TXT.decode()}\n received: {data.decode()}' - # MSC dd throughput test: read all sectors then write back same data + +def test_device_cdc_msc_freertos(board): + test_device_cdc_msc(board) + + +def test_device_cdc_msc_throughput(board): + uid = board['uid'] + + def parse_speed(dd_output): + for line in dd_output.splitlines(): + m = re.search(r'([\d.]+)\s+([kMG]?B)/s', line) + if m: + return f'{float(m.group(1)):.1f} {m.group(2)}ps' + return '?' + + # Wait for MSC disk enumeration dev = get_disk_dev(uid, 'TinyUSB', 0) timeout = ENUM_TIMEOUT while timeout > 0: if os.path.exists(dev): break - time.sleep(1) - timeout -= 1 - assert timeout > 0, f'Disk {dev} not found for dd test' + time.sleep(0.1); timeout -= 0.1 + assert timeout > 0, f'Disk {dev} not found' - block_count = 16 - block_size = 512 - tmp_file = f'/tmp/msc_dd_{uid}.bin' + # Wait for CDC tty enumeration + tty = get_serial_dev(uid, 'TinyUSB', 'Throughput', 0) + timeout = ENUM_TIMEOUT + while timeout > 0: + if os.path.exists(tty): + break + time.sleep(0.1); timeout -= 0.1 + assert timeout > 0, f'CDC tty {tty} not found' - # dd reports speed based on payload only. Each block also transfers 31-byte CBW + 13-byte CSW on USB. - scsi_ratio = (block_size + 31 + 13) / block_size + # Detect speed (12 Mbps FS / 480 Mbps HS) for payload scaling + is_fs = False + for f in glob.glob('/sys/bus/usb/devices/*/serial'): + try: + if open(f).read().strip() == uid: + is_fs = (open(os.path.join(os.path.dirname(f), 'speed')).read().strip() == '12') + break + except (OSError, ValueError): + pass - def parse_dd_speed(dd_output): - """Parse dd output, return USB-adjusted speed string""" - for line in dd_output.splitlines(): - m = re.search(r'([\d.]+)\s+([kMG]?B/s)', line) - if m: - speed_val = float(m.group(1)) * scsi_ratio - return f'{speed_val:.1f} {m.group(2)}' - return '' + # 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: {cmd_stdout_text(rs.stdout)}' - # Read: dd from device to file - ret = run_cmd(f'dd if={dev} of={tmp_file} bs={block_size} count={block_count} iflag=direct 2>&1') - assert ret.returncode == 0, f'dd read failed: {ret.stdout.decode()}' - read_speed = parse_dd_speed(ret.stdout.decode()) + # 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 + cdc_count = 16 if is_fs else 128 # bs=64K - # Write back the same data to avoid corrupting the disk (skip if read-only) - ret = run_cmd(f'dd if={tmp_file} of={dev} bs={block_size} count={block_count} oflag=direct 2>&1') - if ret.returncode != 0 and 'Read-only' in ret.stdout.decode(): - write_speed = 'skip (read-only)' - else: - assert ret.returncode == 0, f'dd write failed: {ret.stdout.decode()}' - write_speed = parse_dd_speed(ret.stdout.decode()) + 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: {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: {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: {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: {cmd_stdout_text(rmw.stdout)}' + msc_w = parse_speed(cmd_stdout_text(rmw.stdout)) try: os.remove(tmp_file) except OSError: pass - if read_speed and write_speed: - print(f' dd read: {read_speed}, write: {write_speed}', end='') - - -def test_device_cdc_msc_freertos(board): - test_device_cdc_msc(board) + print(f' CDC read {cdc_r} write {cdc_w}, MSC read {msc_r} write {msc_w} ', end='') def test_device_dfu(board): @@ -796,7 +957,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) @@ -836,7 +997,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) @@ -896,18 +1057,27 @@ def test_device_printer_to_cdc(board): ser.reset_input_buffer() # Test 1: Printer -> CDC with multiple sizes, write in random 1-64 byte chunks + LP_WRITE_TIMEOUT = 5.0 # seconds; firmware may stall draining the printer OUT endpoint for size in sizes: test_data = rand_ascii(size) ser.reset_input_buffer() rd = b'' offset = 0 - with open(lp_dev, 'wb') as lp: + lp_fd = os.open(lp_dev, os.O_WRONLY | os.O_NONBLOCK) + try: while offset < size: chunk_size = min(random.randint(1, 64), size - offset) - lp.write(test_data[offset:offset + chunk_size]) - lp.flush() + buf = test_data[offset:offset + chunk_size] + written = 0 + while written < len(buf): + _, wr, _ = select.select([], [lp_fd], [], LP_WRITE_TIMEOUT) + assert wr, f'Printer write timeout after {LP_WRITE_TIMEOUT}s (firmware not draining OUT endpoint)' + n = os.write(lp_fd, buf[written:]) + written += n rd += ser.read(chunk_size) offset += chunk_size + finally: + os.close(lp_fd) # read any remaining bytes (fullspeed devices may need extra time) while len(rd) < size: remaining = ser.read(size - len(rd)) @@ -1028,6 +1198,65 @@ def test_device_mtp(board): mtp.disconnect() +def test_device_net_lwip_webserver(board): + # MAC hard-coded in examples/device/net_lwip_webserver/src/main.c; Linux names the + # USB network interface enx<MAC_lowercase_no_colons>. Device IP is 192.168.7.1 and + # the example runs an iperf2 TCP server on port 5001 (INCLUDE_IPERF). + import socket + mac_no_colons = '0202846a9600' + iface = 'enx' + mac_no_colons + device_ip = '192.168.7.1' + iperf_port = 5001 + + # Wait for the host to get an IPv4 address in the device's subnet (DHCP served by the device). + # USB enum + DHCP serve can take longer on the CI HIL hardware than on local — give it 30s. + iface_timeout = 30 + deadline = time.time() + iface_timeout + host_ip = None + while time.time() < deadline: + ret = subprocess.run(['ip', '-o', '-4', 'addr', 'show', iface], + capture_output=True, text=True, timeout=2) + m = re.search(r'inet (192\.168\.7\.\d+)/', ret.stdout) if ret.returncode == 0 else None + if m: + host_ip = m.group(1) + break + time.sleep(0.5) + assert host_ip, f'USB net iface {iface} did not come up with 192.168.7.x within {iface_timeout}s' + + # Poll the iperf TCP port until the device is accepting. The net stack comes up a bit + # after DHCP completes; iperf server binding isn't instantaneous after reflash. + deadline = time.time() + ENUM_TIMEOUT + last_err = None + while time.time() < deadline: + try: + with socket.create_connection((device_ip, iperf_port), timeout=1): + last_err = None + break + except OSError as e: + last_err = e + time.sleep(0.3) + assert last_err is None, f'iperf TCP {device_ip}:{iperf_port} not accepting within {ENUM_TIMEOUT}s: {last_err}' + + # Throughput: 5-second iperf2 TCP test, CSV output for stable parsing. + # iperf2 CSV final summary line: timestamp,src_ip,src_port,dst_ip,dst_port,id,interval,bytes,bps + ret = subprocess.run(['iperf', '-c', device_ip, '-t', '5', '-y', 'C'], + capture_output=True, text=True, timeout=30) + stderr = ret.stderr.strip() + stdout = ret.stdout.strip() + assert ret.returncode == 0, f'iperf rc={ret.returncode}: stderr={stderr!r} stdout={stdout!r}' + lines = [l for l in stdout.splitlines() if l] + assert lines, f'iperf produced no output (rc={ret.returncode}, stderr={stderr!r})' + try: + bps = int(lines[-1].split(',')[-1]) + except (ValueError, IndexError) as e: + raise AssertionError(f'could not parse iperf output: {lines[-1]!r} ({e})') + mbps = bps / 1e6 + print(f' iperf {mbps:5.1f} Mbps', end='') + + # Reject implausibly low throughput - a working USB-net link should clear this easily. + assert mbps >= 1.0, f'iperf throughput too low: {mbps:.2f} Mbps' + + def test_device_msc_dual_lun(board): uid = board['uid'] @@ -1098,6 +1327,79 @@ def test_device_midi_test(board): assert n in note_sequence, f'Unexpected MIDI note {n}' +def test_device_audio_test_freertos(board): + uid = board['uid'] + + if os.name == 'nt': + return 'skipped' + + arecord = shutil.which('arecord') + if arecord is None: + return 'skipped' + + pcm = None + timeout = ENUM_TIMEOUT + while timeout > 0: + pcm = get_alsa_capture_dev(uid) + if pcm: + break + time.sleep(1) + timeout -= 1 + + assert pcm is not None, f'ALSA capture device not found for {uid}' + + raw_path = f'/tmp/tinyusb_audio_{uid}.raw' + cmd = [ + arecord, + '-D', pcm, + '-q', + '-f', 'S16_LE', + '-c', '1', + '-r', '48000', + '-d', '2', + '-t', 'raw', + raw_path, + ] + + ret = subprocess.run(cmd, capture_output=True, text=True, timeout=20) + assert ret.returncode == 0, f'arecord failed: {ret.stderr.strip() or ret.stdout.strip()}' + + try: + with open(raw_path, 'rb') as f: + raw = f.read() + finally: + try: + os.remove(raw_path) + except OSError: + pass + + assert len(raw) >= 48000, f'Captured too little audio: {len(raw)} bytes' + assert (len(raw) % 2) == 0, f'Invalid 16-bit audio length: {len(raw)}' + + sample_count = len(raw) // 2 + samples = [int.from_bytes(raw[i:i + 2], 'little', signed=False) for i in range(0, len(raw), 2)] + assert sample_count > 1024, f'Not enough samples captured: {sample_count}' + + # The firmware sends a continuous uint16 ramp. Using ALSA hw: capture bypasses + # PulseAudio processing, so most adjacent samples should differ by exactly 1. + total_diffs = sample_count - 1 + one_step = 0 + near_step = 0 + for i in range(total_diffs): + d = (samples[i + 1] - samples[i]) & 0xFFFF + if d == 1: + one_step += 1 + if d in (0, 1, 2, 47, 48, 49): + near_step += 1 + + one_ratio = one_step / total_diffs + near_ratio = near_step / total_diffs + assert one_ratio >= 0.85, f'Unexpected audio pattern (strict ratio={one_ratio:.3f})' + assert near_ratio >= 0.98, f'Unexpected audio pattern (relaxed ratio={near_ratio:.3f})' + + print(f' ALSA {pcm} strict={one_ratio:.3f} relaxed={near_ratio:.3f}', end='') + + def test_device_hid_generic_inout(board): uid = board['uid'] import hid @@ -1143,6 +1445,8 @@ device_tests = [ 'device/cdc_dual_ports', 'device/dfu', 'device/cdc_msc', + 'device/cdc_msc_throughput', + 'device/audio_test_freertos', 'device/dfu_runtime', 'device/cdc_msc_freertos', 'device/hid_boot_interface', @@ -1150,7 +1454,8 @@ device_tests = [ 'device/hid_generic_inout', 'device/printer_to_cdc', 'device/midi_test', - 'device/mtp' + 'device/mtp', + # 'device/net_lwip_webserver', # disabled for PR #3605: USB net iface enum is flaky on the CI HIL host ] dual_tests = [ @@ -1164,7 +1469,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 @@ -1179,66 +1484,127 @@ 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)}' - print(f'{name+f1_str:40} {example:30} ...', end='') + fw_dir = TINYUSB_ROOT / build_dir / f'cmake-build-{name}{f1_str}' / example + fw_name = fw_dir / Path(example).name + test_name = f'{name+f1_str:40} {example:30} ...' - if not os.path.exists(fw_dir) or not (os.path.exists(f'{fw_name}.elf') or os.path.exists(f'{fw_name}.bin')): - print('Skip (no binary)') + if not fw_dir.exists() or not ((fw_name.with_suffix('.elf')).exists() or (fw_name.with_suffix('.bin')).exists()): + log_line(f'{test_name} Skip (no binary)') return 0 if verbose: - print(f'Flashing {fw_name}.elf') + log_line(f'Flashing {fw_name}.elf') - # flash firmware. It may fail randomly, retry a few times + # flash firmware (unless --skip-flash), then run the test. Both may fail randomly, + # retry a few times. start_s = time.time() + flash_ok = True + last_err = '' + last_detail = '' for i in range(max_retry): - ret = globals()[f'flash_{board["flasher"]["name"].lower()}'](board, fw_name) - if ret.returncode == 0: - try: - tret = globals()[f'test_{example.replace("/", "_")}'](board) - if tret == 'skipped': - print(f' {STATUS_SKIPPED}', end='') - else: - print(' OK', end='') - break - except Exception as e: - if i == max_retry - 1: - err_count += 1 - print(f'{STATUS_FAILED}: {e}') - else: - print(f'\n Test failed: {e}, retry {i+2}/{max_retry}', end='') - time.sleep(0.5) - else: - print(f'\n Flash failed, retry {i+2}/{max_retry}', end='') - time.sleep(0.5) + attempt_out = io.StringIO() + with redirect_stdout(attempt_out): + if not skip_flash: + ret = globals()[f'flash_{board["flasher"]["name"].lower()}'](board, str(fw_name)) + flash_ok = (ret.returncode == 0) + if flash_ok: + try: + tret = globals()[f'test_{example.replace("/", "_")}'](board) + last_detail = compact_output(attempt_out.getvalue()) + if tret == 'skipped': + status = STATUS_SKIPPED + else: + status = STATUS_OK + msg = f'{test_name} {status}' + if last_detail: + msg += f' {last_detail}' + msg += f' in {time.time() - start_s:.1f}s' + log_line(msg) + break + except Exception as e: + last_err = str(e) + last_detail = compact_output(attempt_out.getvalue()) + if i == max_retry - 1: + err_count += 1 + msg = f'{test_name} {STATUS_FAILED}: {e}' + if last_detail: + msg += f' {last_detail}' + msg += f' in {time.time() - start_s:.1f}s' + log_line(msg) + else: + msg = f'{test_name} retry {i+2}/{max_retry}: test failed: {e}' + if last_detail: + msg += f' {last_detail}' + log_line(msg) + time.sleep(0.5) + else: + last_err = 'Flash failed' + last_detail = compact_output(attempt_out.getvalue()) + if i < max_retry - 1: + msg = f'{test_name} retry {i+2}/{max_retry}: flash failed' + if last_detail: + msg += f' {last_detail}' + log_line(msg) + time.sleep(0.5) - if ret.returncode != 0: + if not flash_ok: err_count += 1 - print(f' Flash {STATUS_FAILED}', end='') - - print(f' in {time.time() - start_s:.1f}s') + msg = f'{test_name} Flash {STATUS_FAILED}' + if last_err: + msg += f': {last_err}' + if last_detail: + msg += f' {last_detail}' + msg += f' in {time.time() - start_s:.1f}s' + log_line(msg) return err_count -def test_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 = 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, str(TINYUSB_ROOT / 'tools' / 'build.py'), '-b', name] + for d in extra_defs: + cmd += ['-D', d] + if f1: + for flag in f1.split(): + cmd += ['-f1', flag] + if verbose: + cmd.append('-v') + print(f' + {" ".join(cmd)}') + r = subprocess.run(cmd, cwd=TINYUSB_ROOT) + if r.returncode != 0: + failed += 1 + return name, failed + + +def test_board(board: Board) -> tuple[str, int, list[str]]: name = board['name'] flasher = board['flasher'] # default to all tests test_list = [] - if len(test_only) > 0: + if name in board_test: + test_list = board_test[name] + elif len(test_only) > 0: test_list = test_only 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'] @@ -1246,75 +1612,116 @@ def test_board(board): for skip in board_tests['skip']: if skip in test_list: test_list.remove(skip) - print(f'{name:25} {skip:30} ... Skip') + log_line(f'{name:25} {skip:30} ... Skip') err_count = 0 + failed_tests = [] flags_on_list = [""] if 'build' in board and 'flags_on' in board['build']: flags_on_list = board['build']['flags_on'] for f1 in flags_on_list: for test in test_list: - err_count += test_example(board, f1, test) + ec = test_example(board, f1, test) + err_count += ec + if ec > 0: + failed_tests.append(test) - # flash board_test last to disable board's usb - test_example(board, flags_on_list[0], 'device/board_test') + # flash board_test last to disable board's usb (skipped when --skip-flash is set) + if not skip_flash: + test_example(board, flags_on_list[0], 'device/board_test') - return name, err_count + return name, err_count, sorted(set(failed_tests)) -def main(): +def main() -> None: """ Hardware test on specified boards """ global verbose global test_only + global board_test global build_dir global max_retry + global skip_flash duration = time.time() parser = argparse.ArgumentParser() parser.add_argument('config_file', help='Configuration JSON file') parser.add_argument('-b', '--board', action='append', default=[], help='Boards to test, all if not specified') - parser.add_argument('-s', '--skip', action='append', default=[], help='Skip boards from test') + parser.add_argument('-s', '--skip-board', action='append', default=[], help='Skip boards from test') + parser.add_argument('-sf', '--skip-flash', action='store_true', help='Run tests without flashing firmware (use whatever is already on the board)') parser.add_argument('-t', '--test-only', action='append', default=[], help='Tests to run, all if not specified') - parser.add_argument('-B', '--build', default='cmake-build', help='Build folder name (default: cmake-build)') + parser.add_argument('-bt', '--board-test', action='append', default=[], + help='Per-board test list as BOARD:test1,test2 (overrides -t for that board); repeat for multiple boards') + parser.add_argument('-B', '--build-dir', default='cmake-build', help='Build folder name (default: cmake-build)') + parser.add_argument('--build', action='store_true', help='Build firmware for selected boards with cmake before running tests') parser.add_argument('-r', '--retry', type=int, default=3, help='Retry count for failed tests (default: 3)') 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 + skip_boards = args.skip_board verbose = args.verbose test_only = args.test_only - build_dir = args.build + for entry in args.board_test: + bname, _, tnames = entry.partition(':') + if not bname or not tnames: + parser.error(f'invalid --board-test value: {entry!r} (expected BOARD:test1,test2)') + board_test[bname] = [t for t in tnames.split(',') if t] + build_dir = args.build_dir max_retry = args.retry + 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] else: config_boards = [e for e in config['boards'] if e['name'] in boards] - err_count = 0 - with Pool(processes=os.cpu_count()) as pool: - mret = pool.map(test_board, config_boards) - err_count = sum(e[1] for e in mret) - # generate skip list for next re-run if failed - skip_fname = f'{config_file}.skip' + build_err = 0 + if args.build: + if build_dir != 'cmake-build': + print(f'warning: --build writes into cmake-build/, but -B is {build_dir!r}; ' + f'tests will not find the freshly built firmware') + print('-' * 30) + print(f'Build phase: {len(config_boards)} board(s)') + print('-' * 30) + for board in config_boards: + _, nfail = build_board(board) + build_err += nfail + print('-' * 30) + print(f'Build phase done: {build_err} failed') + print('-' * 30) + + with Pool(processes=os.cpu_count() or 1, initializer=init_worker, initargs=(Lock(),)) 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 = config_file.with_suffix(config_file.suffix + '.skip') if err_count > 0: - skip_boards += [name for name, err in mret if err == 0] - with open(skip_fname, 'w') as f: - f.write(' '.join(f'-s {i}' for i in skip_boards)) - elif os.path.exists(skip_fname): - os.remove(skip_fname) + 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 skip_fname.open('w') as f: + f.write(' '.join(parts)) + elif skip_fname.exists(): + skip_fname.unlink() duration = time.time() - duration print() diff --git a/test/hil/tinyusb.json b/test/hil/tinyusb.json index 92b7b21b0..cba7677cf 100644 --- a/test/hil/tinyusb.json +++ b/test/hil/tinyusb.json @@ -7,7 +7,7 @@ "flags_on": ["", "CFG_TUD_DWC2_DMA_ENABLE CFG_TUH_DWC2_DMA_ENABLE"] }, "tests": { - "only": ["device/cdc_msc_freertos", "device/hid_composite_freertos", "host/device_info"], + "only": ["device/cdc_msc_freertos", "device/hid_composite_freertos", "device/audio_test_freertos", "host/device_info"], "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2002427", "is_cdc": true}] }, "flasher": { @@ -25,7 +25,7 @@ "flags_on": ["", "CFG_TUD_DWC2_DMA_ENABLE CFG_TUH_DWC2_DMA_ENABLE"] }, "tests": { - "only": ["device/cdc_msc_freertos", "device/hid_composite_freertos", "host/device_info"], + "only": ["device/cdc_msc_freertos", "device/hid_composite_freertos", "device/audio_test_freertos", "host/device_info"], "dev_attached": [{"vid_pid": "1a86_55d4", "serial": "52D2005402", "is_cdc": true}] }, "flasher": { @@ -43,7 +43,7 @@ }, "flasher": { "name": "jlink", - "uid": "000682804350", + "uid": "681295394", "args": "-device nrf52840_xxaa" } }, @@ -156,11 +156,6 @@ "device": false, "host": true, "dual": false, "dev_attached": [ { - "vid_pid": "1a86_55d4", - "serial": "52D2023934", - "is_cdc": true - }, - { "vid_pid": "2008_2018", "serial": "O20070925A002746", "is_msc": true, @@ -184,11 +179,6 @@ "device": false, "host": true, "dual": false, "dev_attached": [ { - "vid_pid": "1a86_55d4", - "serial": "52D2002694", - "is_cdc": true - }, - { "vid_pid": "0951_1603", "serial": "820000000000000045B46338", "is_msc": true, @@ -226,6 +216,9 @@ { "name": "stm32f072disco", "uid": "3A001A001357364230353532", + "tests": { + "device": true, "host": false, "dual": false + }, "flasher": { "name": "jlink", "uid": "779541626", diff --git a/test/unit-test/CMakeLists.txt b/test/unit-test/CMakeLists.txt index b44a91d57..a33af4563 100644 --- a/test/unit-test/CMakeLists.txt +++ b/test/unit-test/CMakeLists.txt @@ -117,14 +117,14 @@ add_ceedling_test( add_ceedling_test( test_usbd ${CEEDLING_WORKDIR}/test/device/usbd/test_usbd.c - "${CEEDLING_WORKDIR}/../../src/tusb.c;${CEEDLING_WORKDIR}/../../src/device/usbd.c;${CEEDLING_WORKDIR}/../../src/device/usbd_control.c;${CEEDLING_WORKDIR}/../../src/common/tusb_fifo.c" + "${CEEDLING_WORKDIR}/../../src/tusb.c;${CEEDLING_WORKDIR}/../../src/device/usbd.c;${CEEDLING_WORKDIR}/../../src/common/tusb_fifo.c" "${CEEDLING_BUILD_DIR}/test/mocks/test_usbd/mock_dcd.c;${CEEDLING_BUILD_DIR}/test/mocks/test_usbd/mock_msc_device.c" ) add_ceedling_test( test_msc_device ${CEEDLING_WORKDIR}/test/device/msc/test_msc_device.c - "${CEEDLING_WORKDIR}/../../src/tusb.c;${CEEDLING_WORKDIR}/../../src/device/usbd.c;${CEEDLING_WORKDIR}/../../src/device/usbd_control.c;${CEEDLING_WORKDIR}/../../src/class/msc/msc_device.c;${CEEDLING_WORKDIR}/../../src/common/tusb_fifo.c" + "${CEEDLING_WORKDIR}/../../src/tusb.c;${CEEDLING_WORKDIR}/../../src/device/usbd.c;${CEEDLING_WORKDIR}/../../src/class/msc/msc_device.c;${CEEDLING_WORKDIR}/../../src/common/tusb_fifo.c" "${CEEDLING_BUILD_DIR}/test/mocks/test_msc_device/mock_dcd.c" ) diff --git a/tools/metrics_compare_base.py b/tools/metrics_compare_base.py new file mode 100644 index 000000000..799a96800 --- /dev/null +++ b/tools/metrics_compare_base.py @@ -0,0 +1,350 @@ +#!/usr/bin/env python3 +"""Build base branch (master) and current tree, then compare code size metrics. + +Creates cmake-metrics/<board>/{base,build} directories for each board. +With --combined, also writes cmake-metrics/_combined/metrics_compare.md aggregating +all boards into a single comparison. + +Usage: + python tools/metrics_compare_base.py -b raspberry_pi_pico + python tools/metrics_compare_base.py -b raspberry_pi_pico -b raspberry_pi_pico2 + python tools/metrics_compare_base.py -b raspberry_pi_pico -f portable/raspberrypi + python tools/metrics_compare_base.py -b raspberry_pi_pico -e device/cdc_msc + python tools/metrics_compare_base.py -b raspberry_pi_pico -e device/cdc_msc --bloaty + python tools/metrics_compare_base.py --ci # first board of each arm-gcc family, combined + python tools/metrics_compare_base.py -b pico -b pico2 --combined # aggregate listed boards +""" +import argparse +import glob +import json +import os +import re +import shlex +import subprocess +import sys + +TINYUSB_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +METRICS_DIR = os.path.join(TINYUSB_ROOT, 'cmake-metrics') + +def tinyusb_src_filter(checkout_dir): + """Return a path-substring filter that uniquely matches TinyUSB stack source files + in `checkout_dir`. The substring is the absolute path to the checkout's `src/` + dir — collision-free with vendored deps (pico-sdk, lwip, FreeRTOS, etc.) which + live at unrelated paths.""" + return os.path.realpath(os.path.join(checkout_dir, 'src')) + os.sep + +verbose = False + + +def run(cmd, **kwargs): + """Run a command. cmd must be a list (no shell=True). On `timeout=`-induced + TimeoutExpired, return a CompletedProcess with rc=124 instead of letting the + exception propagate, so the caller can fall through to error reporting and + worktree cleanup rather than crashing with a traceback.""" + if not isinstance(cmd, list): + raise TypeError('run() requires a list, got str — fix the caller') + if verbose: + print(f' $ {" ".join(shlex.quote(str(c)) for c in cmd)}') + try: + return subprocess.run(cmd, capture_output=True, text=True, **kwargs) + except subprocess.TimeoutExpired as e: + msg = f'Command timed out after {e.timeout}s: {" ".join(shlex.quote(str(c)) for c in cmd)}' + stderr = (e.stderr or '') + ('\n' if e.stderr else '') + msg + return subprocess.CompletedProcess(cmd, 124, stdout=(e.stdout or ''), stderr=stderr) + + +def symlink_deps(main_root, worktree_dir): + """Symlink dependency directories (fetched by tools/get_deps.py) from the main + checkout into the temporary worktree. Without this, the base build fails because + the worktree doesn't have the untracked deps.""" + def link_subdirs(rel_parent): + src_parent = os.path.join(main_root, rel_parent) + dst_parent = os.path.join(worktree_dir, rel_parent) + if not os.path.isdir(src_parent): + return + os.makedirs(dst_parent, exist_ok=True) + for entry in os.listdir(src_parent): + src = os.path.join(src_parent, entry) + dst = os.path.join(dst_parent, entry) + if os.path.isdir(src) and not os.path.exists(dst): + os.symlink(src, dst) + + # lib/* and tools/* deps (e.g. lib/lwip, tools/linkermap) + link_subdirs('lib') + link_subdirs('tools') + # hw/mcu/<vendor>/<dep> (e.g. hw/mcu/raspberry_pi/Pico-PIO-USB) + hw_mcu = os.path.join(main_root, 'hw', 'mcu') + if os.path.isdir(hw_mcu): + for vendor in os.listdir(hw_mcu): + link_subdirs(os.path.join('hw', 'mcu', vendor)) + + +def ci_first_boards(): + """Return the first board (alphabetical) of each arm-gcc CI family.""" + matrix_py = os.path.join(TINYUSB_ROOT, '.github', 'workflows', 'ci_set_matrix.py') + if not os.path.isfile(matrix_py): + return [] + ret = run([sys.executable, matrix_py]) + if ret.returncode != 0: + return [] + try: + data = json.loads(ret.stdout) + except json.JSONDecodeError: + return [] + families = data.get('arm-gcc', []) + boards = [] + bsp_root = os.path.join(TINYUSB_ROOT, 'hw', 'bsp') + for family in families: + family_boards = sorted( + d for d in os.listdir(os.path.join(bsp_root, family, 'boards')) + if os.path.isdir(os.path.join(bsp_root, family, 'boards', d)) + ) if os.path.isdir(os.path.join(bsp_root, family, 'boards')) else [] + if family_boards: + boards.append(family_boards[0]) + return boards + + +def build_board(src_dir, build_dir, board, example=None): + """Configure and build examples for a board. Returns True on success. + + When `example` is given, only that target is built (`cmake --build --target NAME`), + keeping single-example workflows fast. + """ + os.makedirs(build_dir, exist_ok=True) + ret = run(['cmake', '-B', build_dir, '-G', 'Ninja', + f'-DBOARD={board}', '-DCMAKE_BUILD_TYPE=MinSizeRel', + os.path.join(src_dir, 'examples')]) + if ret.returncode != 0: + print(f' Error configuring {board}: {ret.stderr}') + return False + cmd = ['cmake', '--build', build_dir] + if example: + cmd += ['--target', os.path.basename(example)] + ret = run(cmd, timeout=600) + if ret.returncode != 0: + print(f' Error building {board}: {ret.stderr}') + return False + return True + + +def generate_metrics(build_dir, out_basename, filters, example=None): + """Run metrics.py combine on .map.json files. Returns metrics json path or None. + + `filters` is a list of substrings; metrics.py keeps a compile unit if its path + contains any of them. + """ + if example: + patterns = glob.glob(f'{build_dir}/{example}/*.map.json') + else: + patterns = glob.glob(f'{build_dir}/**/*.map.json', recursive=True) + if not patterns: + print(f' Error: no .map.json files in {build_dir}' + (f' for {example}' if example else '')) + return None + + metrics_py = os.path.join(TINYUSB_ROOT, 'tools', 'metrics.py') + cmd = [sys.executable, metrics_py, 'combine'] + for f in filters: + cmd += ['-f', f] + cmd += ['-j', '-q', '-o', out_basename, *patterns] + ret = run(cmd) + if ret.returncode != 0: + print(f' Error: {ret.stderr}') + return None + return f'{out_basename}.json' + + +def main(): + global verbose + + parser = argparse.ArgumentParser(description='Compare code size metrics with base branch') + parser.add_argument('-b', '--board', action='append', default=[], + help='Board name (repeatable). Required unless --ci is given.') + parser.add_argument('-f', '--filter', action='append', default=None, + help='Path-substring filter (repeatable). When given, ' + 'overrides the default and is applied to BOTH base and ' + 'current builds. Default: each side\'s own absolute ' + '<checkout>/src/ path, which uniquely matches TinyUSB ' + 'stack code without colliding with vendored deps.') + parser.add_argument('--base-branch', default='master', + help='Base branch to compare against (default: master)') + parser.add_argument('-e', '--example', action='append', default=None, + help='Compare specific example (repeatable, e.g. -e device/cdc_msc -e host/cdc_msc_hid)') + parser.add_argument('--bloaty', action='store_true', + help='Use bloaty for detailed section/symbol diff (requires -e)') + parser.add_argument('--ci', action='store_true', + help='Add the first board of every arm-gcc CI family. Implies --combined.') + parser.add_argument('--combined', action='store_true', + help='Aggregate map.json files across all boards into one comparison ' + '(in cmake-metrics/_combined/), instead of (or in addition to) per-board.') + parser.add_argument('-v', '--verbose', action='store_true', + help='Print build commands') + args = parser.parse_args() + verbose = args.verbose + + if args.bloaty and not args.example: + parser.error('--bloaty requires -e/--example') + + if args.ci: + args.combined = True + ci_boards = ci_first_boards() + if not ci_boards: + parser.error('--ci: failed to derive boards from .github/workflows/ci_set_matrix.py') + # Append, dedup, preserve order + seen = set(args.board) + for b in ci_boards: + if b not in seen: + args.board.append(b) + seen.add(b) + + if not args.board: + parser.error('at least one -b BOARD is required (or pass --ci)') + + metrics_py = os.path.join(TINYUSB_ROOT, 'tools', 'metrics.py') + worktree_dir = os.path.join(METRICS_DIR, '_worktree') + + # Per-side filters: when no override is given, each build uses its own + # absolute <checkout>/src/ path so we only match TinyUSB stack code from that + # checkout (and never vendored-dep `src/` like pico-sdk/src/...). + if args.filter: + base_filters = cur_filters = list(args.filter) + else: + base_filters = [tinyusb_src_filter(worktree_dir)] + cur_filters = [tinyusb_src_filter(TINYUSB_ROOT)] + + # Step 1: Create worktree for base branch + print(f'[1/5] Setting up {args.base_branch} worktree...') + if os.path.isdir(worktree_dir): + run(['git', '-C', TINYUSB_ROOT, 'worktree', 'remove', '--force', worktree_dir]) + # --detach: check out the ref at a detached HEAD instead of trying to claim the + # branch. Lets us add a worktree of `master` even if master is already checked + # out elsewhere (main repo, another worktree). + ret = run(['git', '-C', TINYUSB_ROOT, 'worktree', 'add', '--detach', + worktree_dir, args.base_branch]) + if ret.returncode != 0: + print(f'Error creating worktree: {ret.stderr}') + sys.exit(1) + + # Symlink dependency dirs (lib/*, hw/mcu/*/*, tools/*) so the worktree builds. + symlink_deps(TINYUSB_ROOT, worktree_dir) + + try: + examples = args.example or [None] + # For --combined: track every (base_build, cur_build) pair so we can aggregate at the end. + built_pairs = [] + + for board in args.board: + print(f'\n=== {board} ===') + board_dir = os.path.join(METRICS_DIR, board) + base_build = os.path.join(board_dir, 'base') + cur_build = os.path.join(board_dir, 'build') + + # Build only the requested examples (or all if -e not given). Single-example + # mode used to build everything and filter at metric time — that was wasted work. + board_failed = False + for example in examples: + build_label = f' --target {os.path.basename(example)}' if example else '' + print(f'[2/5] Building {args.base_branch} for {board}{build_label}...') + if not build_board(worktree_dir, base_build, board, example): + board_failed = True + break + print(f'[3/5] Building current for {board}{build_label}...') + if not build_board(TINYUSB_ROOT, cur_build, board, example): + board_failed = True + break + if board_failed: + continue + + built_pairs.append((board, base_build, cur_build)) + + for example in examples: + suffix = f'_{example.replace("/", "_")}' if example else '' + label = f' ({example})' if example else '' + + # Step 4: Generate metrics + print(f'[4/5] Generating metrics for {board}{label}...') + base_json = generate_metrics(base_build, os.path.join(board_dir, f'base_metrics{suffix}'), + base_filters, example) + cur_json = generate_metrics(cur_build, os.path.join(board_dir, f'build_metrics{suffix}'), + cur_filters, example) + if not base_json or not cur_json: + continue + + # Step 5: Compare + out_base = os.path.join(board_dir, f'metrics_compare{suffix}') + print(f'[5/5] Comparing {board}{label}...') + ret = run([sys.executable, metrics_py, 'compare', '-m', '-o', out_base, base_json, cur_json]) + print(ret.stdout) + + # Optional: bloaty diff + if args.bloaty and example: + elf_name = os.path.basename(example) + base_elf = os.path.join(base_build, example, f'{elf_name}.elf') + cur_elf = os.path.join(cur_build, example, f'{elf_name}.elf') + if os.path.exists(base_elf) and os.path.exists(cur_elf): + # Bloaty expects one regex; OR-join all filters (current side + # for the new ELF, base side for the base ELF). + bloaty_regex = '(' + '|'.join( + re.escape(f) for f in (cur_filters + base_filters) + ) + ')' + bloaty_common = ['bloaty', '--domain=vm', f'--source-filter={bloaty_regex}'] + print(f'--- bloaty sections ---') + ret = run(bloaty_common + ['-d', 'compileunits,sections', cur_elf, '--', base_elf]) + print(ret.stdout) + print(f'--- bloaty symbols ---') + ret = run(bloaty_common + ['-d', 'compileunits,symbols', '-s', 'vm', + cur_elf, '--', base_elf]) + print(ret.stdout) + else: + print(f' bloaty: ELF not found') + + # Optional combined comparison across all boards. + # Aggregates the per-board metrics JSONs (not raw map.json globs) so the argv + # stays small even with --ci spanning many boards. + if args.combined and built_pairs: + combined_dir = os.path.join(METRICS_DIR, '_combined') + os.makedirs(combined_dir, exist_ok=True) + + # Use the no-suffix per-board JSONs (whole-board metrics). Combined mode + # is meant for board-level sweeps; -e/--example combinations skip combined. + base_jsons, cur_jsons = [], [] + for board, _, _ in built_pairs: + bj = os.path.join(METRICS_DIR, board, 'base_metrics.json') + cj = os.path.join(METRICS_DIR, board, 'build_metrics.json') + if os.path.isfile(bj) and os.path.isfile(cj): + base_jsons.append(bj) + cur_jsons.append(cj) + + if not base_jsons or not cur_jsons: + print(' combined: no per-board metrics found (did you pass -e? skip --combined with -e)') + else: + print(f'\n=== combined ({len(base_jsons)} boards) ===') + base_out = os.path.join(combined_dir, 'base_metrics') + cur_out = os.path.join(combined_dir, 'build_metrics') + + # Per-board JSONs are already filtered to TinyUSB-only files; combine + # without re-filtering so we don't accidentally drop entries. + def _combine(out_basename, inputs): + cmd = [sys.executable, metrics_py, 'combine', + '-j', '-q', '-o', out_basename, *inputs] + return run(cmd) + + ret = _combine(base_out, base_jsons) + if ret.returncode != 0: + print(f' combined base error: {ret.stderr}') + else: + ret = _combine(cur_out, cur_jsons) + if ret.returncode != 0: + print(f' combined current error: {ret.stderr}') + else: + out_combined = os.path.join(combined_dir, 'metrics_compare') + ret = run([sys.executable, metrics_py, 'compare', '-m', + '-o', out_combined, f'{base_out}.json', f'{cur_out}.json']) + print(ret.stdout) + print(f' combined report: {out_combined}.md') + finally: + print(f'\nCleaning up worktree...') + run(['git', '-C', TINYUSB_ROOT, 'worktree', 'remove', '--force', worktree_dir]) + + +if __name__ == '__main__': + main() |
