summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHiFiPhile <[email protected]>2026-05-22 13:19:45 +0200
committerHiFiPhile <[email protected]>2026-05-22 13:19:45 +0200
commitc24016991f1b58544cf3bfe05815df637f466e0e (patch)
tree8ba7e710d390b9c012327dec04c767f66d42a53a
parent650e8a194fcfaa4b91203e9aedfecd614ea70105 (diff)
parente1cb1b5b6fa183a1473f05b8d1ce4ab93a373ef4 (diff)
Merge remote-tracking branch 'tinyusb/master' into fix/Dwc2Stm32U5
-rw-r--r--.claude/commands/hil.md58
-rw-r--r--.claude/skills/code-size/SKILL.md76
-rw-r--r--.claude/skills/hil/SKILL.md66
-rw-r--r--.github/workflows/build.yml38
-rw-r--r--.gitignore31
-rw-r--r--AGENTS.md473
-rw-r--r--examples/device/CMakeLists.txt1
-rw-r--r--examples/device/audio_4_channel_mic/src/usb_descriptors.c4
-rw-r--r--examples/device/audio_4_channel_mic_freertos/src/usb_descriptors.c4
-rw-r--r--examples/device/audio_test/src/usb_descriptors.c4
-rw-r--r--examples/device/audio_test_freertos/src/usb_descriptors.c4
-rw-r--r--examples/device/audio_test_multi_rate/src/usb_descriptors.c4
-rw-r--r--examples/device/cdc_dual_ports/src/usb_descriptors.c25
-rw-r--r--examples/device/cdc_msc/src/usb_descriptors.c22
-rw-r--r--examples/device/cdc_msc_freertos/src/main.c4
-rw-r--r--examples/device/cdc_msc_freertos/src/usb_descriptors.c22
-rw-r--r--examples/device/cdc_msc_throughput/CMakeLists.txt35
-rw-r--r--examples/device/cdc_msc_throughput/Makefile11
-rw-r--r--examples/device/cdc_msc_throughput/src/main.c151
-rw-r--r--examples/device/cdc_msc_throughput/src/tusb_config.h103
-rw-r--r--examples/device/cdc_msc_throughput/src/usb_descriptors.c186
-rw-r--r--examples/device/cdc_uac2/src/usb_descriptors.c22
-rw-r--r--examples/device/dfu/skip.txt1
-rw-r--r--examples/device/dynamic_configuration/src/usb_descriptors.c2
-rw-r--r--examples/device/hid_generic_inout/src/usb_descriptors.c2
-rw-r--r--examples/device/midi_test/src/usb_descriptors.c2
-rw-r--r--examples/device/midi_test_freertos/src/usb_descriptors.c2
-rw-r--r--examples/device/msc_dual_lun/src/usb_descriptors.c12
-rw-r--r--examples/device/mtp/src/usb_descriptors.c2
-rw-r--r--examples/device/net_lwip_webserver/skip.txt2
-rw-r--r--examples/device/net_lwip_webserver/src/lwipopts.h20
-rw-r--r--examples/device/net_lwip_webserver/src/tusb_config.h40
-rw-r--r--examples/device/net_lwip_webserver/src/usb_descriptors.c181
-rw-r--r--examples/device/printer_to_cdc/src/usb_descriptors.c21
-rw-r--r--examples/device/uac2_headset/src/usb_descriptors.c15
-rw-r--r--examples/device/uac2_speaker_fb/src/usb_descriptors.c15
-rw-r--r--examples/device/webusb_serial/src/usb_descriptors.c22
-rw-r--r--examples/dual/dynamic_switch/src/usb_descriptors.c2
-rw-r--r--examples/host/CMakeLists.txt1
-rw-r--r--examples/host/msc_file_explorer_freertos/CMakeLists.txt42
-rw-r--r--examples/host/msc_file_explorer_freertos/CMakePresets.json6
-rw-r--r--examples/host/msc_file_explorer_freertos/Makefile27
-rw-r--r--examples/host/msc_file_explorer_freertos/README.md105
-rw-r--r--examples/host/msc_file_explorer_freertos/only.txt28
-rw-r--r--examples/host/msc_file_explorer_freertos/skip.txt3
-rw-r--r--examples/host/msc_file_explorer_freertos/src/CMakeLists.txt13
-rw-r--r--examples/host/msc_file_explorer_freertos/src/ffconf.h313
-rw-r--r--examples/host/msc_file_explorer_freertos/src/main.c158
-rw-r--r--examples/host/msc_file_explorer_freertos/src/msc_app.c709
-rw-r--r--examples/host/msc_file_explorer_freertos/src/msc_app.h34
-rw-r--r--examples/host/msc_file_explorer_freertos/src/tusb_config.h126
-rw-r--r--hw/bsp/at32f402_405/family.c2
-rw-r--r--hw/bsp/at32f403a_407/family.c2
-rw-r--r--hw/bsp/at32f413/family.c2
-rw-r--r--hw/bsp/at32f415/family.c2
-rw-r--r--hw/bsp/at32f423/family.c2
-rw-r--r--hw/bsp/at32f425/family.c2
-rw-r--r--hw/bsp/at32f435_437/family.c2
-rw-r--r--hw/bsp/at32f45x/family.c2
-rw-r--r--hw/bsp/board.c2
-rw-r--r--hw/bsp/ch32v20x/family.c2
-rw-r--r--hw/bsp/family_support.cmake6
-rw-r--r--hw/bsp/ft9xx/family.c5
-rw-r--r--hw/bsp/gd32vf103/family.c2
-rw-r--r--hw/bsp/imxrt/boards/metro_m7_1011/board.cmake1
-rw-r--r--hw/bsp/imxrt/boards/metro_m7_1011/board.mk2
-rw-r--r--hw/bsp/imxrt/boards/mimxrt1010_evk/board.cmake1
-rw-r--r--hw/bsp/imxrt/boards/mimxrt1010_evk/board.mk2
-rw-r--r--hw/bsp/imxrt/boards/mimxrt1015_evk/board.cmake1
-rw-r--r--hw/bsp/imxrt/boards/mimxrt1015_evk/board.mk2
-rw-r--r--hw/bsp/kinetis_k/family.c2
-rw-r--r--hw/bsp/lpc55/boards/lpcxpresso55s69/board.cmake1
-rw-r--r--hw/bsp/mm32/family.c2
-rw-r--r--hw/bsp/nrf/boards/nrf54h20dk/board.cmake3
-rw-r--r--hw/bsp/nrf/boards/nrf54h20dk/board.mk5
-rw-r--r--hw/bsp/nrf/boards/nrf54lm20dk/board.cmake5
-rw-r--r--hw/bsp/pic32mz/family.c4
-rw-r--r--hw/bsp/rp2040/family.c2
-rw-r--r--hw/bsp/rp2040/family.cmake1
-rw-r--r--hw/bsp/stm32c0/family.c2
-rw-r--r--hw/bsp/stm32f0/family.c2
-rw-r--r--hw/bsp/stm32f1/family.c2
-rw-r--r--hw/bsp/stm32f2/family.c2
-rw-r--r--hw/bsp/stm32f3/family.c2
-rw-r--r--hw/bsp/stm32f4/family.c2
-rw-r--r--hw/bsp/stm32f7/family.c2
-rw-r--r--hw/bsp/stm32g0/family.c2
-rw-r--r--hw/bsp/stm32g4/family.c2
-rw-r--r--hw/bsp/stm32h5/family.c2
-rw-r--r--hw/bsp/stm32h7/family.c2
-rw-r--r--hw/bsp/stm32h7rs/family.c2
-rw-r--r--hw/bsp/stm32l0/family.c2
-rw-r--r--hw/bsp/stm32l4/family.c2
-rw-r--r--hw/bsp/stm32n6/family.c2
-rw-r--r--hw/bsp/stm32u0/family.c2
-rw-r--r--hw/bsp/stm32u5/family.c2
-rw-r--r--hw/bsp/stm32wb/family.c2
-rw-r--r--hw/bsp/tm4c/family.c8
-rw-r--r--hw/bsp/xmc4000/family.c2
-rw-r--r--lib/networking/rndis_reports.c8
-rw-r--r--src/CMakeLists.txt1
-rw-r--r--src/class/hid/hid_host.c33
-rw-r--r--src/class/hid/hid_host.h2
-rw-r--r--src/class/midi/midi_host.h7
-rw-r--r--src/class/net/ecm_rndis_device.c22
-rw-r--r--src/class/net/ncm.h14
-rw-r--r--src/class/net/ncm_device.c102
-rw-r--r--src/class/net/net_device.h16
-rw-r--r--src/class/printer/printer_device.c1
-rw-r--r--src/common/tusb_mcu.h27
-rw-r--r--src/common/tusb_private.h19
-rw-r--r--src/common/tusb_types.h12
-rw-r--r--src/device/usbd.c501
-rw-r--r--src/device/usbd.h16
-rw-r--r--src/device/usbd_control.c217
-rw-r--r--src/device/usbd_pvt.h4
-rw-r--r--src/host/usbh.c23
-rw-r--r--src/portable/mentor/musb/dcd_musb.c686
-rw-r--r--src/portable/mentor/musb/musb_type.h12
-rw-r--r--src/portable/nordic/nrf5x/dcd_nrf5x.c6
-rw-r--r--src/portable/synopsys/dwc2/dcd_dwc2.c3
-rw-r--r--src/portable/synopsys/dwc2/hcd_dwc2.c5
-rw-r--r--src/tinyusb.mk1
-rw-r--r--src/tusb.c15
-rw-r--r--test/fuzz/rules.mk1
-rw-r--r--test/hil/hil_ci.sh56
-rw-r--r--test/hil/hil_ci_set_matrix.py73
-rwxr-xr-xtest/hil/hil_test.py675
-rw-r--r--test/hil/tinyusb.json19
-rw-r--r--test/unit-test/CMakeLists.txt4
-rw-r--r--tools/metrics_compare_base.py350
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/
diff --git a/AGENTS.md b/AGENTS.md
index eb6b737c3..5c9908d19 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -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()