diff options
| author | copilot-swe-agent[bot] <[email protected]> | 2026-05-25 19:15:45 +0000 |
|---|---|---|
| committer | GitHub <[email protected]> | 2026-05-25 19:15:45 +0000 |
| commit | 072c02e3684e886a93681ecb37f830be5b8c6b53 (patch) | |
| tree | 0b7cd9e7d287fa563726cfb0cc1f70e418ce499a | |
| parent | e8baf2e0102660d51eb515d1b0651d8298178dc3 (diff) | |
| parent | 7b9f6eabd8623c2bbe188c8d53072b16a087cdda (diff) | |
chore: resolve .gitignore merge conflict with master
Co-authored-by: HiFiPhile <[email protected]>
177 files changed, 9351 insertions, 1923 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/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..e9930a25a --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: hathach 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 641283476..c11e51bb9 100644 --- a/.gitignore +++ b/.gitignore @@ -56,12 +56,34 @@ BrowseInfo .cmake_build README_processed.rst .worktrees -# Fetched dependencies (get_deps.py) -hw/mcu/*/ +cmake-metrics/ +# Directories fetched by tools/get_deps.py - not to be committed lib/CMSIS_5/ +lib/CMSIS_6/ lib/FreeRTOS-Kernel/ lib/fatfs/ lib/lwip/ +lib/sct_neopixel/ lib/threadx/ tools/linkermap/ tools/uf2/ +hw/mcu/allwinner/ +hw/mcu/analog/ +hw/mcu/artery/ +hw/mcu/broadcom/ +hw/mcu/bridgetek/ft9xx/ft90x-sdk/ +hw/mcu/gd/ +hw/mcu/hpmicro/ +hw/mcu/infineon/ +hw/mcu/microchip/ +hw/mcu/mindmotion/ +hw/mcu/nordic/nrfx/ +hw/mcu/nuvoton/ +hw/mcu/nxp/ +hw/mcu/raspberry_pi/ +hw/mcu/renesas/ +hw/mcu/silabs/ +hw/mcu/sony/cxd56/spresense-exported-sdk/ +hw/mcu/st/ +hw/mcu/ti/ +hw/mcu/wch/ @@ -1,428 +1,217 @@ # TinyUSB Agent Instructions -TinyUSB is an open-source cross-platform USB Host/Device stack for embedded systems, designed to be memory-safe with no -dynamic allocation and thread-safe with all interrupt events deferred to non-ISR task functions. +TinyUSB is a cross-platform USB Host/Device stack for embedded systems: memory-safe (no dynamic allocation) and thread-safe (ISR events deferred to task context). -Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected -information that does not match the info here. +Reference these instructions first; fall back to search/bash only when reality diverges. -## Shared Ground Rules -- Keep TinyUSB memory-safe: avoid dynamic allocation, defer ISR work to task context, and follow C99 with two-space indentation/no tabs. -- Match file organization: core stack under `src`, MCU/BSP support in `hw/{mcu,bsp}`, examples under `examples/{device,host,dual}`, docs in `docs`, tests under `test/{unit-test,fuzz,hil}`. -- Use descriptive snake_case for helpers, reserve `tud_`/`tuh_` for public APIs, `TU_` for macros, and keep headers self-contained with `#if CFG_TUSB_MCU` guards where needed. -- Prefer `.clang-format` for C/C++ formatting, run `pre-commit run --all-files` before submitting, and document board/HIL coverage when applicable. -- Commit in imperative mood, keep changes scoped, and supply PRs with linked issues plus test/build evidence. +## Behavioral Guidelines +Bias toward caution over speed. For trivial tasks, use judgment. -## Bootstrap and Build Setup +- **Think first** — state assumptions; ask if unclear; present alternatives instead of picking silently. +- **Simplicity** — no features, abstractions, flexibility, or error handling beyond what was asked. If 200 lines could be 50, rewrite. +- **Surgical changes** — touch only what the task requires; match existing style; don't refactor working code; mention unrelated dead code rather than deleting it. Remove only orphans *your* changes created. +- **Goal-driven** — turn tasks into verifiable goals ("write failing test, make it pass"). For multi-step work, state a brief `step → verify` plan. -- Install ARM GCC toolchain: `sudo apt-get update && sudo apt-get install -y gcc-arm-none-eabi` -- Fetch core dependencies: `python3 tools/get_deps.py` -- takes <1 second. NEVER CANCEL. -- For specific board families: `python3 tools/get_deps.py FAMILY_NAME` (e.g., rp2040, stm32f4), or - `python3 tools/get_deps.py -b BOARD_NAME` -- Dependencies are cached in `lib/` and `hw/mcu/` directories -- For **Espressif** boards, initialize the ESP-IDF environment before any build/flash/monitor command: - `. $HOME/code/esp-idf/export.sh` +## Ground Rules -## Build Examples +- **Language/style:** C99, 2-space indent (no tabs), snake_case helpers, `UPPER_CASE` macros. Public APIs use `tud_`/`tuh_`; macros use `TU_`. Headers self-contained with `#if CFG_TUSB_MCU` guards. +- **Safety:** no dynamic allocation; defer ISR work to task context; use `TU_ASSERT()` for error checks; always check return values; include order: C stdlib → tusb common → drivers → classes. +- **Layout:** `src/` core, `hw/{mcu,bsp}/` MCU+BSP, `examples/{device,host,dual}/`, `test/{unit-test,fuzz,hil}/`, `docs/`, `tools/`. +- **Commits/PRs:** imperative mood, scoped changes, link issues, include test/build evidence. +- **Formatting/lint:** `clang-format` (`.clang-format`), `codespell` (`.codespellrc`), run `pre-commit run --all-files` before submitting. -Choose ONE of these approaches: -**Option 1: Individual Example with CMake and Ninja (RECOMMENDED)** +## Bootstrap ```bash -cd examples/device/cdc_msc -mkdir -p build && cd build -cmake -DBOARD=raspberry_pi_pico -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel .. -cmake --build . +sudo apt-get install -y gcc-arm-none-eabi # ARM toolchain (2-5 min, one-time) +python3 tools/get_deps.py [FAMILY|-b BOARD] # fetch deps into lib/, hw/mcu/ (<1 s) +. $HOME/code/esp-idf/export.sh # Espressif only: before any build/flash/monitor ``` --- takes 1-2 seconds. NEVER CANCEL. Set timeout to 5+ minutes. - -**Option 2: All Examples for a Board** - -different folder than Option 1 +## Build +Single example (CMake+Ninja, recommended, 1-3 s): ```bash -cd examples/ -mkdir -p build && cd build +cd examples/device/cdc_msc && mkdir -p build && cd build cmake -DBOARD=raspberry_pi_pico -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel .. cmake --build . ``` --- takes 15-20 seconds, may have some objcopy failures that are non-critical. NEVER CANCEL. Set timeout to 30+ minutes. - -**Option 3: Individual Example with Make** - +All examples for a board (15-20 s; some objcopy failures are non-critical). Use `cmake-build-<board>` as the build dir — HIL tests expect that exact name: ```bash -cd examples/device/cdc_msc -make BOARD=raspberry_pi_pico all +cd examples +cmake -B cmake-build-raspberry_pi_pico -DBOARD=raspberry_pi_pico -G Ninja -DCMAKE_BUILD_TYPE=MinSizeRel . +cmake --build cmake-build-raspberry_pi_pico ``` --- takes 2-3 seconds. NEVER CANCEL. Set timeout to 5+ minutes. - -**Option 4: Espressif Example with ESP-IDF** - -Only ESP-IDF-enabled examples are supported for Espressif boards. Use FreeRTOS examples such as `examples/device/cdc_msc_freertos` -that contain `idf_component_register()` support. +Single example with Make: +```bash +cd examples/device/cdc_msc && make BOARD=raspberry_pi_pico all +``` +Espressif (only ESP-IDF examples like `cdc_msc_freertos`): ```bash . $HOME/code/esp-idf/export.sh cd examples/device/cdc_msc_freertos idf.py -DBOARD=espressif_s3_devkitc build ``` -Use `-DBOARD=...` with any supported board under `hw/bsp/espressif/boards/`. NEVER CANCEL. Set timeout to 10+ minutes. - - -## Build Options +**Build options** (CMake `-D…` / Make `…=…`): +- Debug: `CMAKE_BUILD_TYPE=Debug` / `DEBUG=1` +- Logging: `LOG=2` (add `LOGGER=rtt` for RTT) +- Root hub port: `RHPORT_DEVICE=1` +- Speed: `RHPORT_DEVICE_SPEED=OPT_MODE_FULL_SPEED` -- **Debug build**: - - CMake: `-DCMAKE_BUILD_TYPE=Debug` - - Make: `DEBUG=1` -- **With logging**: - - CMake: `-DLOG=2` - - Make: `LOG=2` -- **With RTT logger**: - - CMake: `-DLOG=2 -DLOGGER=rtt` - - Make: `LOG=2 LOGGER=rtt` -- **RootHub port selection**: - - CMake: `-DRHPORT_DEVICE=1` - - Make: `RHPORT_DEVICE=1` -- **Port speed**: - - CMake: `-DRHPORT_DEVICE_SPEED=OPT_MODE_FULL_SPEED` - - Make: `RHPORT_DEVICE_SPEED=OPT_MODE_FULL_SPEED` +## Flash -## Flashing and Deployment - -- **Flash with JLink**: - - CMake: `ninja cdc_msc-jlink` - - Make: `make BOARD=raspberry_pi_pico flash-jlink` -- **Flash with OpenOCD**: - - CMake: `ninja cdc_msc-openocd` - - Make: `make BOARD=raspberry_pi_pico flash-openocd` -- **Generate UF2**: - - CMake: `ninja cdc_msc-uf2` - - Make: `make BOARD=raspberry_pi_pico all uf2` -- **List all targets** (CMake/Ninja): `ninja -t targets` -- **Espressif flash**: - - Run `. $HOME/code/esp-idf/export.sh` - - `cd examples/device/cdc_msc_freertos` - - `idf.py -DBOARD=espressif_s3_devkitc flash` -- **Espressif serial monitor / chip log output**: - - Run `. $HOME/code/esp-idf/export.sh` - - `cd examples/device/cdc_msc_freertos` - - `idf.py -DBOARD=espressif_s3_devkitc monitor` +```bash +# JLink +ninja cdc_msc-jlink # CMake +make BOARD=<board> flash-jlink # Make -## GDB Debugging +# OpenOCD +ninja cdc_msc-openocd # CMake +make BOARD=<board> flash-openocd # Make -Look up the board's `JLINK_DEVICE` and `OPENOCD_OPTION` from `hw/bsp/*/boards/*/board.cmake` (or `board.mk`). +# UF2 +ninja cdc_msc-uf2 # CMake +make BOARD=<board> all uf2 # Make -### JLinkGDBServer +ninja -t targets # list CMake targets -**Terminal 1 – start the GDB server:** -```bash -JLinkGDBServer -device stm32h743xi -if SWD -speed 4000 \ - -port 2331 -swoport 2332 -telnetport 2333 -nogui +# Espressif (after . $HOME/code/esp-idf/export.sh) +idf.py -DBOARD=<board> flash +idf.py -DBOARD=<board> monitor ``` -**Terminal 2 – connect GDB:** -```bash -arm-none-eabi-gdb /tmp/build/firmware.elf -(gdb) target remote :2331 -(gdb) monitor reset halt -(gdb) load -(gdb) continue -``` +## GDB Debugging + +Look up `JLINK_DEVICE` / `OPENOCD_OPTION` in `hw/bsp/*/boards/*/board.cmake` (CMake builds) or `board.mk` (Make builds). -To break on entry instead of running immediately: +**JLink — Terminal 1:** ```bash -(gdb) monitor reset halt -(gdb) load -(gdb) break main -(gdb) continue +JLinkGDBServer -device stm32h743xi -if SWD -speed 4000 -port 2331 -swoport 2332 -telnetport 2333 -nogui ``` -### OpenOCD - -**Terminal 1 – start the GDB server:** +**OpenOCD — Terminal 1:** ```bash openocd -f interface/stlink.cfg -f target/stm32h7x.cfg -# or with J-Link probe: +# or with a J-Link interface: openocd -f interface/jlink.cfg -f target/stm32h7x.cfg -``` - -For **rp2040/rp2350** with a CMSIS-DAP probe (e.g. Picoprobe, debugprobe): -```bash +# rp2040/rp2350 via CMSIS-DAP: openocd -f interface/cmsis-dap.cfg -f target/rp2040.cfg -c "adapter speed 5000" -# or for rp2350: -openocd -f interface/cmsis-dap.cfg -f target/rp2350.cfg -c "adapter speed 5000" ``` -For boards that define `OPENOCD_OPTION` in `board.cmake`, use those options directly: -```bash -openocd $(cat hw/bsp/FAMILY/boards/BOARD/board.cmake | grep OPENOCD_OPTION | ...) -``` - -**Terminal 2 – connect GDB (OpenOCD default port is 3333):** +**Terminal 2 — connect GDB** (replace `<port>` with `2331` for JLinkGDBServer or `3333` for OpenOCD): ```bash arm-none-eabi-gdb /tmp/build/firmware.elf -(gdb) target remote :3333 +(gdb) target remote :<port> (gdb) monitor reset halt (gdb) load +(gdb) break main # optional, to stop at entry (gdb) continue ``` -### RTT Logging with JLinkGDBServer - -- Build with RTT logging enabled (example): - `cd examples/device/cdc_msc && make BOARD=stm32h743eval LOG=2 LOGGER=rtt all` -- Flash with J-Link: - `cd examples/device/cdc_msc && make BOARD=stm32h743eval LOG=2 LOGGER=rtt flash-jlink` -- Launch GDB server with RTT port (keep this running in terminal 1): - `JLinkGDBServer -device stm32h743xi -if SWD -speed 4000 -port 2331 -swoport 2332 -telnetport 2333 -RTTTelnetPort 19021 -nogui` -- Read RTT output (terminal 2): - `JLinkRTTClient` -- Capture RTT to file (optional): - `JLinkRTTClient | tee rtt.log` -- For non-interactive capture: - `timeout 20s JLinkRTTClient > rtt.log` - -## Unit Testing - -- Install Ceedling: `sudo gem install ceedling` -- Run all unit tests: `cd test/unit-test && ceedling` or `cd test/unit-test && ceedling test:all` -- takes 4 seconds. - NEVER CANCEL. Set timeout to 10+ minutes. -- Run specific test: `cd test/unit-test && ceedling test:test_fifo` -- Tests use Unity framework with CMock for mocking - -## Hardware-in-the-Loop (HIL) Testing - -- `-B examples` means `examples` is the parent folder that contains multi-board build outputs such as `examples/cmake-build-BOARD_NAME/...` -- Select config file before running HIL tests: - - if GitHub Actions self-hosted runner service is running, use `tinyusb.json` - - otherwise use `local.json` - - example: - `HIL_CONFIG=$( (systemctl list-units --type=service --state=running 2>/dev/null; systemctl --user list-units --type=service --state=running 2>/dev/null) | grep -q 'actions\.runner' && echo tinyusb.json || echo local.json )` -- Run tests on actual hardware, one of following ways: - - test a specific board `python test/hil/hil_test.py -b BOARD_NAME -B examples $HIL_CONFIG` - - test all boards in config `python test/hil/hil_test.py -B examples $HIL_CONFIG` -- In case of error, enabled verbose mode with `-v` flag for detailed logs. Also try to observe script output, and try to - modify hil_test.py (temporarily) to add more debug prints to pinpoint the issue. -- Requires pre-built (all) examples for target boards (see Build Examples section 2) - -take 2-5 minutes. NEVER CANCEL. Set timeout to 20+ minutes. - -## Documentation - -- Install requirements: `pip install -r docs/requirements.txt` -- Build docs: `cd docs && sphinx-build -b html . _build` -- takes 2-3 seconds. NEVER CANCEL. Set timeout to 10+ minutes. +**RTT logging:** build with `LOG=2 LOGGER=rtt`, flash, then run JLinkGDBServer with `-RTTTelnetPort 19021`, and in another terminal `JLinkRTTClient` (pipe to `tee rtt.log` or use `timeout 20s JLinkRTTClient > rtt.log` for non-interactive capture). -## Code Size Metrics - -Generate and compare code size metrics to evaluate the impact of changes. This is the most common workflow -when making code changes — use it to verify size impact before committing. - -**Quick single-board metrics (preferred for iterative development):** +## Testing +**Unit (Ceedling, Unity+CMock, ~4 s):** ```bash -rm -rf cmake-build -python3 tools/build.py -b raspberry_pi_pico --target all --target tinyusb_metrics -python3 tools/metrics.py combine -j -m -f tinyusb/src cmake-build/cmake-build-*/metrics.json +sudo gem install ceedling +cd test/unit-test && ceedling test:all # or ceedling test:test_fifo ``` -This builds all examples for one board and produces `metrics.json` + `metrics.md`. Takes ~30 seconds. -NEVER CANCEL. Set timeout to 10+ minutes. - -**Comparing with master (before/after workflow):** +**HIL (2-5 min):** invoke the `hil` skill (`.claude/skills/hil/SKILL.md`) for the full procedure (local vs remote mode, config selection, SSH copy steps, debugging tips). Requires pre-built examples — see Build → "All examples for a board". -1. On master: build and save baseline - ```bash - rm -rf cmake-build - python3 tools/build.py -b raspberry_pi_pico --target all --target tinyusb_metrics - python3 tools/metrics.py combine -j -m -f tinyusb/src cmake-build/cmake-build-*/metrics.json - mv metrics.json metrics_master.json - ``` -2. Switch to your branch: rebuild - ```bash - rm -rf cmake-build - python3 tools/build.py -b raspberry_pi_pico --target all --target tinyusb_metrics - python3 tools/metrics.py combine -j -m -f tinyusb/src cmake-build/cmake-build-*/metrics.json - ``` -3. Compare: `python3 tools/metrics.py compare -m -f tinyusb/src metrics_master.json metrics.json` - Produces `metrics_compare.md` showing size differences. - -**Full CI metrics (all arm-gcc families, for thorough validation):** +## Documentation ```bash -rm -rf cmake-build -FAMILIES=$(python3 .github/workflows/ci_set_matrix.py | python3 -c "import sys,json; d=json.load(sys.stdin); print(' '.join(d.get('arm-gcc',[])))") -python3 tools/build.py --one-first --target all --target tinyusb_metrics $FAMILIES -python3 tools/metrics.py combine -j -m -f tinyusb/src cmake-build/cmake-build-*/metrics.json +pip install -r docs/requirements.txt +cd docs && sphinx-build -b html . _build # ~2.5 s ``` -Builds the first board of each family. Takes 2-4 minutes. NEVER CANCEL. Set timeout to 10+ minutes. - -## Code Quality and Validation +## Code Size Metrics -- Format code: `clang-format -i path/to/file.c` (uses `.clang-format` config) -- Check spelling: `pip install codespell && codespell` (uses `.codespellrc` config) -- Pre-commit hooks validate unit tests and code quality automatically +Verify size impact before committing. Invoke the `code-size` skill (`.claude/skills/code-size/SKILL.md`) — it wraps `tools/metrics_compare_base.py` to handle the base-vs-branch worktree + build + compare flow. -## Static Analysis with PVS-Studio +Quick reference: +```bash +# Single example, one board: +python3 tools/metrics_compare_base.py -b raspberry_pi_pico -e device/cdc_msc +# Add --bloaty for section/symbol breakdown. -- **Analyze whole project**: - ```bash - pvs-studio-analyzer analyze -f examples/cmake-build-raspberry_pi_pico/compile_commands.json -R .PVS-Studio/.pvsconfig -o pvs-report.log -j12 --dump-files --misra-cpp-version 2008 --misra-c-version 2023 --use-old-parser - ``` -- **Analyze specific source files**: - ```bash - pvs-studio-analyzer analyze -f examples/cmake-build-raspberry_pi_pico/compile_commands.json -R .PVS-Studio/.pvsconfig -S path/to/file.c -o pvs-report.log -j12 --dump-files --misra-cpp-version 2008 --misra-c-version 2023 --use-old-parser - ``` -- **Multiple specific files**: - ```bash - pvs-studio-analyzer analyze -f examples/cmake-build-raspberry_pi_pico/compile_commands.json -R .PVS-Studio/.pvsconfig -S src/file1.c -S src/file2.c -o pvs-report.log -j12 --dump-files --misra-cpp-version 2008 --misra-c-version 2023 --use-old-parser - ``` -- Requires `compile_commands.json` in the build directory (generated by CMake with `-DCMAKE_EXPORT_COMPILE_COMMANDS=ON`) -- Use `-f` option to specify path to `compile_commands.json` -- Use `-R .PVS-Studio/.pvsconfig` to specify rule configuration file -- Use `-j12` for parallel analysis with 12 threads -- `--dump-files` saves preprocessed files for debugging -- `--misra-c-version 2023` enables MISRA C:2023 checks -- `--misra-cpp-version 2008` enables MISRA C++:2008 checks -- `--use-old-parser` uses legacy parser for compatibility -- Analysis takes ~10-30 seconds depending on project size. Set timeout to 5+ minutes. -- View results: `plog-converter -a GA:1,2 -t errorfile pvs-report.log` or open in PVS-Studio GUI +# All examples, one board: +python3 tools/metrics_compare_base.py -b raspberry_pi_pico -## Validation Checklist +# All arm-gcc CI families combined (pre-merge sweep, 4-8 min): +python3 tools/metrics_compare_base.py --ci +``` -### ALWAYS Run These After Making Changes +Reports land in `cmake-metrics/<board>/metrics_compare.md` (per-board) and `cmake-metrics/_combined/metrics_compare.md` (with `--combined`/`--ci`). -1. **Pre-commit validation** (RECOMMENDED): `pre-commit run --all-files` - - Install pre-commit: `pip install pre-commit && pre-commit install` - - Runs all quality checks, unit tests, spell checking, and formatting - - Takes 10-15 seconds. NEVER CANCEL. Set timeout to 15+ minutes. -2. **Build validation**: Build at least one board with all example that exercises your changes, see Build Examples - section (option 2) -3. Run unit tests relevant to touched modules; add fuzz/HIL coverage when modifying parsers or protocol state machines. +## Static Analysis (PVS-Studio) -### Manual Testing Scenarios -- **Device examples**: Cannot be fully tested without real hardware, but must build successfully -- **Unit tests**: Exercise core stack functionality - ALL tests must pass -- **Build system**: Must be able to build examples for multiple board families +Requires `compile_commands.json` (CMake `-DCMAKE_EXPORT_COMPILE_COMMANDS=ON`). -### Board Selection for Testing -- **STM32F4**: `stm32f407disco` - no external SDK required, good for testing -- **RP2040**: `raspberry_pi_pico` - requires Pico SDK, commonly used -- **Other families**: Check `hw/bsp/FAMILY/boards/` for available boards +```bash +# Whole project: +pvs-studio-analyzer analyze \ + -f examples/cmake-build-raspberry_pi_pico/compile_commands.json \ + -R .PVS-Studio/.pvsconfig \ + -o pvs-report.log -j12 --dump-files \ + --misra-c-version 2023 --misra-cpp-version 2008 --use-old-parser -## Release Instructions +# Specific files (add one or more `-S <file>`): +pvs-studio-analyzer analyze \ + -f examples/cmake-build-raspberry_pi_pico/compile_commands.json \ + -R .PVS-Studio/.pvsconfig \ + -S src/foo.c -S src/bar.c \ + -o pvs-report.log -j12 --dump-files \ + --misra-c-version 2023 --misra-cpp-version 2008 --use-old-parser -**DO NOT commit files automatically - only modify files and let the maintainer review before committing.** +plog-converter -a GA:1,2 -t errorfile pvs-report.log # view results +``` -1. Bump the release version variable at the top of `tools/make_release.py`. -2. Execute `python3 tools/make_release.py` to refresh: - - `src/tusb_option.h` (version defines) - - `repository.yml` (version mapping) - - `library.json` (PlatformIO version) - - `sonar-project.properties` (SonarQube version) - - `docs/reference/boards.rst` (generated board documentation) - - `hw/bsp/BoardPresets.json` (CMake presets) -3. Generate release notes for `docs/info/changelog.rst`: - - Get commit list: `git log <last-release-tag>..HEAD --oneline` - - **Visit GitHub PRs** for merged pull requests to understand context and gather details - - Use GitHub tools to search/read PRs: `github-mcp-server-list_pull_requests`, `github-mcp-server-pull_request_read` - - Extract key changes, API modifications, bug fixes, and new features from PR descriptions - - Add new changelog entry following the existing format: - - Version heading with equals underline (e.g., `0.20.0` followed by `======`) - - Release date in italics (e.g., `*November 19, 2024*`) - - Major sections: General, API Changes, Controller Driver (DCD & HCD), Device Stack, Host Stack, Testing - - Use bullet lists with descriptive categorization - - Reference function names, config macros, and file paths using RST inline code (double backticks) - - Include meaningful descriptions, not just commit messages -4. **Validation before commit**: - - Run unit tests: `cd test/unit-test && ceedling test:all` - - Build at least one example: `cd examples/device/cdc_msc && make BOARD=stm32f407disco all` - - Verify changed files look correct: `git diff --stat` -5. **Leave files unstaged** for maintainer to review, modify if needed, and commit with message: `Bump version to X.Y.Z` -6. **After maintainer commits**: Create annotated tag with `git tag -a vX.Y.Z -m "Release X.Y.Z"` -7. Push commit and tag: `git push origin <branch> && git push origin vX.Y.Z` -8. Create GitHub release from the tag with changelog content +Takes ~10-30 s. -## Repository Structure Quick Reference -``` -├── src/ # Core TinyUSB stack -│ ├── class/ # USB device classes (CDC, HID, MSC, Audio, etc.) -│ ├── portable/ # MCU-specific drivers (organized by vendor) -│ ├── device/ # USB device stack core -│ ├── host/ # USB host stack core -│ └── common/ # Shared utilities (FIFO, etc.) -├── examples/ # Example applications -│ ├── device/ # Device examples (cdc_msc, hid_generic, etc.) -│ ├── host/ # Host examples -│ └── dual/ # Dual-role examples -├── hw/bsp/ # Board Support Packages -│ └── FAMILY/boards/ # Board-specific configurations -├── test/unit-test/ # Unit tests using Ceedling -├── tools/ # Build and utility scripts -└── docs/ # Sphinx documentation -``` +## Validation After Changes -#### Build Time Reference -- **Dependency fetch**: <1 second -- **Single example build**: 1-3 seconds -- **Unit tests**: ~4 seconds -- **Documentation build**: ~2.5 seconds -- **Full board examples**: 15-20 seconds -- **Toolchain installation**: 2-5 minutes (one-time) +1. `pre-commit run --all-files` — format, spell, unit tests (10-15 s). +2. Build at least one board's full example set (Build → "All examples for a board") for modules you touched. +3. Run relevant unit tests; add fuzz/HIL coverage for parsers or protocol state machines. -#### Key Files to Know -- `tools/get_deps.py`: Manages dependencies for MCU families -- `tools/build.py`: Builds multiple examples, supports make/cmake -- `src/tusb.h`: Main TinyUSB header file -- `src/tusb_config.h`: Configuration template -- `examples/device/cdc_msc/`: Most commonly used example for testing -- `test/unit-test/project.yml`: Ceedling test configuration +**Boards good for local testing:** +- `stm32f407disco` — no external SDK +- `raspberry_pi_pico` — Pico SDK required +- Others: see `hw/bsp/FAMILY/boards/` -#### MCU Reference Manuals and Datasheets -- Look in `$HOME/Documents/Calibre Library` for all MCU reference manuals, datasheets and board schematics. +Device examples need real hardware to validate runtime behavior; must at least build. -#### Debugging Build Issues -- **Missing compiler**: Install `gcc-arm-none-eabi` package -- **Missing dependencies**: Run `python3 tools/get_deps.py FAMILY` -- **Board not found**: Check `hw/bsp/FAMILY/boards/` for valid board names -- **objcopy errors**: Often non-critical in full builds, try individual example builds +## Release -#### Working with USB Device Classes -- **CDC (Serial)**: `src/class/cdc/` - Virtual serial port -- **HID**: `src/class/hid/` - Human Interface Device (keyboard, mouse, etc.) -- **MSC**: `src/class/msc/` - Mass Storage Class (USB drive) -- **Audio**: `src/class/audio/` - USB Audio Class -- Each class has device (`*_device.c`) and host (`*_host.c`) implementations +**Do not commit automatically — leave changes for maintainer review.** -#### MCU Family Support -- **STM32**: Largest support (F0, F1, F2, F3, F4, F7, G0, G4, H7, L4, U5, etc.) -- **Raspberry Pi**: RP2040, RP2350 with PIO-USB host support -- **NXP**: iMXRT, Kinetis, LPC families -- **Microchip**: SAM D/E/G/L families -- Check `hw/bsp/` for complete list and `docs/reference/boards.rst` for details +1. Bump version at top of `tools/make_release.py`. +2. Run `python3 tools/make_release.py` to refresh: `src/tusb_option.h`, `repository.yml`, `library.json`, `sonar-project.properties`, `docs/reference/boards.rst`, `hw/bsp/BoardPresets.json`. +3. Changelog `docs/info/changelog.rst`: + - `git log <last-tag>..HEAD --oneline` for commit list. + - Read merged PRs for context (`gh pr view`, or github MCP tools). + - Follow existing format: version + `======` underline, italic date, sections (General, API Changes, DCD & HCD, Device Stack, Host Stack, Testing), RST inline code for symbols. +4. Validate: `ceedling test:all`, build `cdc_msc` for `stm32f407disco`, review `git diff --stat`. +5. Leave unstaged. Maintainer commits `Bump version to X.Y.Z`, then: `git tag -a vX.Y.Z -m "Release X.Y.Z" && git push origin <branch> vX.Y.Z`. Create GitHub release from tag. -### Code Style Guidelines +## References -#### General Coding Standards -- Use C99 standard -- Memory-safe: no dynamic allocation -- Thread-safe: defer all interrupt events to non-ISR task functions -- 2-space indentation, no tabs -- Use snake_case for variables/functions -- Use UPPER_CASE for macros and constants -- Follow existing variable naming patterns in files you're modifying -- Include proper header comments with MIT license -- Add descriptive comments for non-obvious functions +- MCU reference manuals, datasheets, schematics: `$HOME/Documents/Calibre Library`. +- Supported MCUs/boards: `hw/bsp/` and `docs/reference/boards.rst`. +- USB classes: `src/class/{cdc,hid,msc,audio,…}/` — each has `*_device.c` and `*_host.c`. +- Key files: `src/tusb.h`, `src/tusb_config.h`, `tools/get_deps.py`, `tools/build.py`, `test/unit-test/project.yml`. -#### Best Practices -- When including headers, group in order: C stdlib, tusb common, drivers, classes -- Always check return values from functions that can fail -- Use TU_ASSERT() for error checking with return statements -- Follow the existing code patterns in the files you're modifying +## Common Build Issues -Remember: TinyUSB is designed for embedded systems - builds are fast, tests are focused, and the codebase is optimized for resource-constrained environments. +- Missing compiler → install `gcc-arm-none-eabi`. +- Missing deps → `python3 tools/get_deps.py FAMILY`. +- Unknown board → check `hw/bsp/FAMILY/boards/`. +- `objcopy` errors in full builds are often non-critical; retry the single example. diff --git a/docs/reference/class_drivers.rst b/docs/reference/class_drivers.rst new file mode 100644 index 000000000..4a101fabc --- /dev/null +++ b/docs/reference/class_drivers.rst @@ -0,0 +1,316 @@ +*************** +Class Drivers +*************** + +USB Class Drivers implement specific USB device classes (CDC, HID, MSC, MIDI, Audio, etc.) and are the main interface between the USB core and application code. + +MIDI 2.0 Device Driver +======================= + +Overview +-------- + +The MIDI 2.0 Device driver enables TinyUSB to act as a USB MIDI 2.0 device. It implements both Alt Setting 0 (MIDI 1.0 fallback) and Alt Setting 1 (native UMP) as required by the USB-MIDI 2.0 specification. + +**Key Features:** + +- **Dual Alt Settings**: Alt 0 (MIDI 1.0) and Alt 1 (UMP native) per USB-MIDI 2.0 spec +- **Protocol Negotiation**: Endpoint Discovery, Config Request/Notify, Function Block Discovery +- **Group Terminal Block**: Served via GET_DESCRIPTOR automatically +- **Atomic UMP Framing**: Read/write with correct message boundaries +- **Memory Safe**: No dynamic allocation, static instances + +Configuration +------------- + +Enable MIDI 2.0 Device support in ``tusb_config.h``: + +.. code-block:: c + + #define CFG_TUD_ENABLED 1 + #define CFG_TUD_MIDI2 1 + +Optional configuration: + +.. code-block:: c + + #define CFG_TUD_MIDI2_TX_BUFSIZE 256 + #define CFG_TUD_MIDI2_RX_BUFSIZE 256 + #define CFG_TUD_MIDI2_TX_EPSIZE 64 + #define CFG_TUD_MIDI2_RX_EPSIZE 64 + #define CFG_TUD_MIDI2_NUM_GROUPS 1 // 1..16 + #define CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS 1 // 1..32 + #define CFG_TUD_MIDI2_EP_NAME "TinyUSB MIDI 2.0" + #define CFG_TUD_MIDI2_PRODUCT_ID "TinyUSB-MIDI2" + +Public API +---------- + +Query Functions +^^^^^^^^^^^^^^^ + +.. code-block:: c + + bool tud_midi2_mounted(void); + uint32_t tud_midi2_available(void); + uint8_t tud_midi2_alt_setting(void); + bool tud_midi2_negotiated(void); + uint8_t tud_midi2_protocol(void); + +I/O Functions +^^^^^^^^^^^^^ + +.. code-block:: c + + uint32_t tud_midi2_ump_read(uint32_t* words, uint32_t max_words); + uint32_t tud_midi2_ump_write(const uint32_t* words, uint32_t count); + uint32_t tud_midi2_packet_read(uint8_t packets[], uint32_t max_packets); + uint32_t tud_midi2_packet_write(const uint8_t packets[], uint32_t count); + +Callbacks +^^^^^^^^^ + +.. code-block:: c + + void tud_midi2_rx_cb(uint8_t itf); + void tud_midi2_set_itf_cb(uint8_t itf, uint8_t alt); + bool tud_midi2_get_req_itf_cb(uint8_t rhport, const tusb_control_request_t* request); + +MIDI 2.0 Host Driver +===================== + +Overview +-------- + +The MIDI 2.0 Host driver enables TinyUSB to enumerate and communicate with USB MIDI 2.0 devices. It implements the USB MIDI 2.0 specification, supporting both MIDI 1.0 legacy devices and modern MIDI 2.0 devices with UMP (Universal MIDI Packet) protocol. + +**Key Features:** + +- **Reactive Architecture**: Auto-detects Alt Setting 1 (MIDI 2.0) capability during enumeration +- **Auto-Selection**: Automatically selects the highest available protocol and issues SET_INTERFACE to activate Alt Setting 1 when MIDI 2.0 is detected +- **Transparent Stream Messages**: All data (UMP packets + Stream Messages) flow through callbacks +- **Memory Safe**: No dynamic allocation, fixed-size instances per device + +Configuration +------------- + +Enable MIDI 2.0 Host support in ``tusb_config.h``: + +.. code-block:: c + + #define CFG_TUH_ENABLED 1 + #define CFG_TUH_MIDI2 4 // Number of MIDI 2.0 devices to support + +Optional buffer configuration: + +.. code-block:: c + + #define CFG_TUH_MIDI2_RX_BUFSIZE (4 * TUH_EPSIZE_BULK_MAX) + #define CFG_TUH_MIDI2_TX_BUFSIZE (4 * TUH_EPSIZE_BULK_MAX) + +Enumeration Lifecycle +--------------------- + +When a MIDI 2.0 device is connected, the host stack invokes callbacks in this order: + +.. code-block:: none + + Device Connected + | + [Host detects Alt 0 and Alt 1 descriptors] + | + tuh_midi2_descriptor_cb() <- Device detected, NOT yet ready + | + [Auto-select highest protocol] + | + tuh_midi2_mount_cb() <- Device ready to use + | + [Application can read/write data] + | + tuh_midi2_rx_cb() <- Data arrived + tuh_midi2_tx_cb() <- TX buffer space available + | + [Device disconnects] + | + tuh_midi2_umount_cb() <- Device removed + +Public API +---------- + +Query Functions +^^^^^^^^^^^^^^^ + +.. code-block:: c + + bool tuh_midi2_mounted(uint8_t idx); + uint8_t tuh_midi2_get_protocol_version(uint8_t idx); // 0=MIDI 1.0, 1=MIDI 2.0 + uint8_t tuh_midi2_get_alt_setting_active(uint8_t idx); // 0 or 1 + uint8_t tuh_midi2_get_cable_count(uint8_t idx); + +I/O Functions +^^^^^^^^^^^^^ + +Read and write UMP (Universal MIDI Packet) data: + +.. code-block:: c + + uint32_t tuh_midi2_ump_read(uint8_t idx, uint32_t* words, uint32_t max_words); + uint32_t tuh_midi2_ump_write(uint8_t idx, const uint32_t* words, uint32_t count); + uint32_t tuh_midi2_write_flush(uint8_t idx); + +Callbacks +--------- + +Application can define weak callback implementations to respond to device events. + +Descriptor Callback +^^^^^^^^^^^^^^^^^^^ + +Invoked when device is detected but not yet ready for I/O: + +.. code-block:: c + + void tuh_midi2_descriptor_cb(uint8_t idx, const tuh_midi2_descriptor_cb_t *desc_cb_data) { + printf("MIDI %s device detected\r\n", + desc_cb_data->protocol_version == 0 ? "1.0" : "2.0"); + } + +Mount Callback +^^^^^^^^^^^^^^ + +Invoked when device is ready for I/O: + +.. code-block:: c + + void tuh_midi2_mount_cb(uint8_t idx, const tuh_midi2_mount_cb_t *mount_cb_data) { + printf("Device mounted at idx=%u, protocol=%u, alt_setting=%u\r\n", + idx, mount_cb_data->protocol_version, mount_cb_data->alt_setting_active); + } + +RX Callback +^^^^^^^^^^^ + +Invoked when data arrives from device (both UMP packets and Stream Messages): + +.. code-block:: c + + void tuh_midi2_rx_cb(uint8_t idx, uint32_t xferred_bytes) { + uint32_t words[4]; + uint32_t n = tuh_midi2_ump_read(idx, words, 4); + + for (uint32_t i = 0; i < n; i++) { + uint8_t mt = (words[i] >> 28) & 0x0F; + if (mt == 0x0F) { + // Stream Message - app handles discovery, negotiation, etc. + } else { + // Regular MIDI UMP packet + } + } + } + +TX Callback +^^^^^^^^^^^ + +Invoked when TX buffer space becomes available: + +.. code-block:: c + + void tuh_midi2_tx_cb(uint8_t idx, uint32_t xferred_bytes) { + // Buffer space available for writing + } + +Unmount Callback +^^^^^^^^^^^^^^^^ + +Invoked when device is disconnected: + +.. code-block:: c + + void tuh_midi2_umount_cb(uint8_t idx) { + printf("Device at idx=%u disconnected\r\n", idx); + } + +Complete Example +---------------- + +.. code-block:: c + + #include "tusb.h" + + void tuh_midi2_mount_cb(uint8_t idx, const tuh_midi2_mount_cb_t *mount_cb_data) { + printf("MIDI 2.0 device mounted\r\n"); + } + + void tuh_midi2_rx_cb(uint8_t idx, uint32_t xferred_bytes) { + uint32_t words[4]; + uint32_t n = tuh_midi2_ump_read(idx, words, 4); + + for (uint32_t i = 0; i < n; i++) { + printf("RX: 0x%08lx\r\n", words[i]); + } + } + + void tuh_midi2_umount_cb(uint8_t idx) { + printf("MIDI 2.0 device disconnected\r\n"); + } + + int main(void) { + board_init(); + + tusb_rhport_init_t host_init = {.role = TUSB_ROLE_HOST, .speed = TUSB_SPEED_AUTO}; + tusb_init(BOARD_TUH_RHPORT, &host_init); + + while (1) { + tuh_task(); + } + } + +Architecture +------------ + +The MIDI 2.0 Host driver uses a **reactive, callback-driven architecture** that mirrors the proven patterns in TinyUSB's existing device drivers (CDC, HID, etc.): + +- **Auto-Detection**: Host automatically detects Alt Setting 1 capability +- **Auto-Selection**: Selects highest protocol available and issues SET_INTERFACE +- **Transparent I/O**: Stream Messages and UMP packets flow through callbacks +- **Callback-Driven**: App receives events via callbacks (descriptor, mount, rx, tx, unmount) + +Differences from MIDI 1.0 Host +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + + * - Aspect + - MIDI 1.0 Host + - MIDI 2.0 Host + * - Alt Settings + - Parses only Alt 0 + - Parses Alt 0 + Alt 1 + * - Data Format + - 4-byte MIDI packets + - UMP (32/64/128-bit) + * - Version Detection + - None + - bcdMSC from descriptor + * - GTB + - N/A + - Presence detection + * - Stream Messages + - N/A + - Transparent passthrough + * - Callbacks + - descriptor_cb, mount_cb, rx_cb, umount_cb + - descriptor_cb, mount_cb, rx_cb, tx_cb, umount_cb + * - Public API + - tuh_midi_* + - tuh_midi2_* + +Implementation Notes +-------------------- + +- All internal state is statically allocated (no dynamic allocation) +- Endpoint streams use TinyUSB's tu_edpt_stream_t for buffered I/O +- Protocol version detection via bcdMSC field +- Alt Setting is automatically selected during mount +- Compatible with all TinyUSB-supported MCU families diff --git a/docs/reference/index.rst b/docs/reference/index.rst index d3c96eeee..148e8a63b 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -9,6 +9,7 @@ Complete reference documentation for TinyUSB APIs, configuration, and supported architecture usb_concepts + class_drivers boards dependencies concurrency diff --git a/examples/device/CMakeLists.txt b/examples/device/CMakeLists.txt index 7173f455e..1432b36bb 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 @@ -27,6 +28,7 @@ set(EXAMPLE_LIST hid_multiple_interface midi_test midi_test_freertos + midi2_device msc_dual_lun mtp net_lwip_webserver diff --git a/examples/device/audio_4_channel_mic/src/plot_audio_samples.py b/examples/device/audio_4_channel_mic/src/plot_audio_samples.py index 4d61e7f5e..618745934 100755 --- a/examples/device/audio_4_channel_mic/src/plot_audio_samples.py +++ b/examples/device/audio_4_channel_mic/src/plot_audio_samples.py @@ -4,6 +4,51 @@ import matplotlib.pyplot as plt import numpy as np import platform + +def find_windows_input_device(name_hint, channels, preferred_apis=None): + """Pick a Windows input device index from query_devices() output.""" + preferred_apis = preferred_apis or [] + name_hint = name_hint.lower() + candidates = [] + + for index in range(len(sd.query_devices())): + device_info = sd.query_devices(index) + max_input_channels = int(device_info.get('max_input_channels', 0)) + + if max_input_channels < channels: + continue + + device_name = str(device_info.get('name', '')).lower() + if name_hint not in device_name: + continue + + score = 0 + + # Prefer exact channel matches to avoid selecting a different stream layout. + if max_input_channels == channels: + score += 100 + else: + score += 10 + + hostapi_index = int(device_info.get('hostapi', -1)) + api_name = '' + if hostapi_index >= 0: + api_name = str(sd.query_hostapis(hostapi_index)).lower() + + for priority, api_hint in enumerate(preferred_apis): + if api_hint in api_name: + score += 50 - priority + break + + candidates.append((score, index)) + + if not candidates: + raise ValueError( + 'No input device matching hint="{}" with at least {} channels'.format(name_hint, channels) + ) + + return max(candidates, key=lambda item: item[0])[1] + if __name__ == '__main__': # If you got "ValueError: No input device matching", that is because your PC name example device @@ -14,8 +59,8 @@ if __name__ == '__main__': duration = 1 # Duration of recording if platform.system() == 'Windows': - # WDM-KS is needed since there are more than one MicNode device APIs (at least in Windows) - device = 'Microphone (MicNode_4_Ch), Windows WASAPI' + # Match by substring to support names like "Microphone (2- MicNode_4_Ch)". + device = find_windows_input_device('micnode_4_ch', channels=4, preferred_apis=['wasapi', 'wdm-ks', 'mme']) elif platform.system() == 'Darwin': device = 'MicNode_4_Ch' else: 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..6b9a9bbae 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,14 @@ 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 + +#elif TU_CHECK_MCU(OPT_MCU_CH32V20X, OPT_MCU_CH32V307) + // Only EP3 is available for ISO + #define EPNUM_AUDIO 0x03 + #else #define EPNUM_AUDIO 0x01 #endif diff --git a/examples/device/audio_4_channel_mic_freertos/src/plot_audio_samples.py b/examples/device/audio_4_channel_mic_freertos/src/plot_audio_samples.py index 4d5ca28d6..3b3cd0d83 100755 --- a/examples/device/audio_4_channel_mic_freertos/src/plot_audio_samples.py +++ b/examples/device/audio_4_channel_mic_freertos/src/plot_audio_samples.py @@ -4,6 +4,52 @@ import matplotlib.pyplot as plt import numpy as np import platform + +def find_windows_input_device(name_hint, channels, preferred_apis=None): + """Pick a Windows input device index from query_devices() output.""" + preferred_apis = preferred_apis or [] + name_hint = name_hint.lower() + candidates = [] + + for index in range(len(sd.query_devices())): + device_info = sd.query_devices(index) + max_input_channels = int(device_info.get('max_input_channels', 0)) + + if max_input_channels < channels: + continue + + device_name = str(device_info.get('name', '')).lower() + if name_hint not in device_name: + continue + + score = 0 + + # Prefer exact channel matches to avoid selecting a different stream layout. + if max_input_channels == channels: + score += 100 + else: + score += 10 + + hostapi_index = int(device_info.get('hostapi', -1)) + api_name = '' + if hostapi_index >= 0: + hostapi_info = sd.query_hostapis(hostapi_index) + api_name = str(hostapi_info.get('name', '')).lower() + + for priority, api_hint in enumerate(preferred_apis): + if api_hint in api_name: + score += 50 - priority + break + + candidates.append((score, index)) + + if not candidates: + raise ValueError( + 'No input device matching hint="{}" with at least {} channels'.format(name_hint, channels) + ) + + return max(candidates, key=lambda item: item[0])[1] + if __name__ == '__main__': # If you got "ValueError: No input device matching", that is because your PC name example device @@ -14,8 +60,8 @@ if __name__ == '__main__': duration = 100e-3 # Duration of recording if platform.system() == 'Windows': - # WDM-KS is needed since there are more than one MicNode device APIs (at least in Windows) - device = 'Microphone (MicNode_4_Ch), Windows WDM-KS' + # Match by substring to support names like "Microphone (2- MicNode_4_Ch)". + device = find_windows_input_device('micnode_4_ch', channels=4, preferred_apis=['wdm-ks', 'wasapi', 'mme']) elif platform.system() == 'Darwin': device = 'MicNode_4_Ch' else: 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/plot_audio_samples.py b/examples/device/audio_test/src/plot_audio_samples.py index 2be8948ea..af01b7b3e 100755 --- a/examples/device/audio_test/src/plot_audio_samples.py +++ b/examples/device/audio_test/src/plot_audio_samples.py @@ -5,6 +5,52 @@ import numpy as np import platform import csv + +def find_windows_input_device(name_hint, channels, preferred_apis=None): + """Pick a Windows input device index from query_devices() output.""" + preferred_apis = preferred_apis or [] + name_hint = name_hint.lower() + candidates = [] + + for index in range(len(sd.query_devices())): + device_info = sd.query_devices(index) + max_input_channels = int(device_info.get('max_input_channels', 0)) + + if max_input_channels < channels: + continue + + device_name = str(device_info.get('name', '')).lower() + if name_hint not in device_name: + continue + + score = 0 + + # Prefer exact channel matches (for example 1ch source over a 4ch source). + if max_input_channels == channels: + score += 100 + else: + score += 10 + + hostapi_index = int(device_info.get('hostapi', -1)) + api_name = '' + if hostapi_index >= 0: + hostapi_info = sd.query_hostapis(hostapi_index) + api_name = str(hostapi_info.get('name', '')).lower() + + for priority, api_hint in enumerate(preferred_apis): + if api_hint in api_name: + score += 50 - priority + break + + candidates.append((score, index)) + + if not candidates: + raise ValueError( + 'No input device matching hint="{}" with at least {} channels'.format(name_hint, channels) + ) + + return max(candidates, key=lambda item: item[0])[1] + if __name__ == '__main__': # If you got "ValueError: No input device matching", that is because your PC name example device @@ -15,8 +61,8 @@ if __name__ == '__main__': duration = 3 # Duration of recording if platform.system() == 'Windows': - # MME is needed since there are more than one MicNode device APIs (at least in Windows) - device = 'Microphone (MicNode), Windows WASAPI' + # Match by substring to support names like "Microphone (2- MicNode)". + device = find_windows_input_device('micnode', channels=1, preferred_apis=['wasapi', 'mme', 'wdm-ks']) elif platform.system() == 'Darwin': device = 'MicNode' else: diff --git a/examples/device/audio_test/src/usb_descriptors.c b/examples/device/audio_test/src/usb_descriptors.c index ad161939e..cea4eb8d1 100644 --- a/examples/device/audio_test/src/usb_descriptors.c +++ b/examples/device/audio_test/src/usb_descriptors.c @@ -91,6 +91,14 @@ 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 + +#elif TU_CHECK_MCU(OPT_MCU_CH32V20X, OPT_MCU_CH32V307) + // Only EP3 is available for ISO + #define EPNUM_AUDIO 0x03 + #else #define EPNUM_AUDIO 0x01 #endif diff --git a/examples/device/audio_test_freertos/src/plot_audio_samples.py b/examples/device/audio_test_freertos/src/plot_audio_samples.py index b6d8e824b..b6a916be4 100755 --- a/examples/device/audio_test_freertos/src/plot_audio_samples.py +++ b/examples/device/audio_test_freertos/src/plot_audio_samples.py @@ -4,6 +4,52 @@ import matplotlib.pyplot as plt import numpy as np import platform + +def find_windows_input_device(name_hint, channels, preferred_apis=None): + """Pick a Windows input device index from query_devices() output.""" + preferred_apis = preferred_apis or [] + name_hint = name_hint.lower() + candidates = [] + + for index in range(len(sd.query_devices())): + device_info = sd.query_devices(index) + max_input_channels = int(device_info.get('max_input_channels', 0)) + + if max_input_channels < channels: + continue + + device_name = str(device_info.get('name', '')).lower() + if name_hint not in device_name: + continue + + score = 0 + + # Prefer exact channel matches (for example 1ch source over a 4ch source). + if max_input_channels == channels: + score += 100 + else: + score += 10 + + hostapi_index = int(device_info.get('hostapi', -1)) + api_name = '' + if hostapi_index >= 0: + hostapi_info = sd.query_hostapis(hostapi_index) + api_name = str(hostapi_info.get('name', '')).lower() + + for priority, api_hint in enumerate(preferred_apis): + if api_hint in api_name: + score += 50 - priority + break + + candidates.append((score, index)) + + if not candidates: + raise ValueError( + 'No input device matching hint="{}" with at least {} channels'.format(name_hint, channels) + ) + + return max(candidates, key=lambda item: item[0])[1] + if __name__ == '__main__': # If you got "ValueError: No input device matching", that is because your PC name example device @@ -14,8 +60,8 @@ if __name__ == '__main__': duration = 3 # Duration of recording if platform.system() == 'Windows': - # MME is needed since there are more than one MicNode device APIs (at least in Windows) - device = 'Microphone (MicNode), Windows WASAPI' + # Match by substring to support names like "Microphone (2- MicNode)". + device = find_windows_input_device('micnode', channels=1, preferred_apis=['wasapi', 'mme', 'wdm-ks']) elif platform.system() == 'Darwin': device = 'MicNode' else: 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/plot_audio_samples.py b/examples/device/audio_test_multi_rate/src/plot_audio_samples.py index 1f33a003e..f35cfa311 100755 --- a/examples/device/audio_test_multi_rate/src/plot_audio_samples.py +++ b/examples/device/audio_test_multi_rate/src/plot_audio_samples.py @@ -5,6 +5,52 @@ import numpy as np import platform import csv + +def find_windows_input_device(name_hint, channels, preferred_apis=None): + """Pick a Windows input device index from query_devices() output.""" + preferred_apis = preferred_apis or [] + name_hint = name_hint.lower() + candidates = [] + + for index in range(len(sd.query_devices())): + device_info = sd.query_devices(index) + max_input_channels = int(device_info.get('max_input_channels', 0)) + + if max_input_channels < channels: + continue + + device_name = str(device_info.get('name', '')).lower() + if name_hint not in device_name: + continue + + score = 0 + + # Prefer exact channel matches (for example 1ch source over a 4ch source). + if max_input_channels == channels: + score += 100 + else: + score += 10 + + hostapi_index = int(device_info.get('hostapi', -1)) + api_name = '' + if hostapi_index >= 0: + hostapi_info = sd.query_hostapis(hostapi_index) + api_name = str(hostapi_info.get('name', '')).lower() + + for priority, api_hint in enumerate(preferred_apis): + if api_hint in api_name: + score += 50 - priority + break + + candidates.append((score, index)) + + if not candidates: + raise ValueError( + 'No input device matching hint="{}" with at least {} channels'.format(name_hint, channels) + ) + + return max(candidates, key=lambda item: item[0])[1] + if __name__ == '__main__': # If you got "ValueError: No input device matching", that is because your PC name example device @@ -15,8 +61,8 @@ if __name__ == '__main__': duration = 100e-3 # Duration of recording if platform.system() == 'Windows': - # MME is needed since there are more than one MicNode device APIs (at least in Windows) - device = 'Microphone (MicNode) MME' + # Match by substring to support names like "Microphone (2- MicNode)". + device = find_windows_input_device('micnode', channels=1, preferred_apis=['mme', 'wasapi', 'wdm-ks']) elif platform.system() == 'Darwin': device = 'MicNode' else: 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..b1f60dd10 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,14 @@ 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 + +#elif TU_CHECK_MCU(OPT_MCU_CH32V20X, OPT_MCU_CH32V307) + // Only EP3 is available for ISO + #define EPNUM_AUDIO 0x03 + #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/CMakePresets.json b/examples/device/cdc_msc_throughput/CMakePresets.json new file mode 100644 index 000000000..5cd8971e9 --- /dev/null +++ b/examples/device/cdc_msc_throughput/CMakePresets.json @@ -0,0 +1,6 @@ +{ + "version": 6, + "include": [ + "../../../hw/bsp/BoardPresets.json" + ] +} 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/midi2_device/CMakeLists.txt b/examples/device/midi2_device/CMakeLists.txt new file mode 100644 index 000000000..295af6550 --- /dev/null +++ b/examples/device/midi2_device/CMakeLists.txt @@ -0,0 +1,33 @@ +cmake_minimum_required(VERSION 3.20) + +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake) + +project(midi2_device 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/usb_descriptors.c + ) + +# Example include +target_include_directories(${PROJECT_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(${PROJECT_NAME} noos) + +# Suppress pre-existing warning in usbd.c (uint8_t comparison always true/false) +target_compile_options(${PROJECT_NAME} PRIVATE -Wno-type-limits) diff --git a/examples/device/midi2_device/CMakePresets.json b/examples/device/midi2_device/CMakePresets.json new file mode 100644 index 000000000..5cd8971e9 --- /dev/null +++ b/examples/device/midi2_device/CMakePresets.json @@ -0,0 +1,6 @@ +{ + "version": 6, + "include": [ + "../../../hw/bsp/BoardPresets.json" + ] +} diff --git a/examples/device/midi2_device/Makefile b/examples/device/midi2_device/Makefile new file mode 100644 index 000000000..829d9da59 --- /dev/null +++ b/examples/device/midi2_device/Makefile @@ -0,0 +1,16 @@ +include ../../../hw/bsp/family_support.mk + +INC += \ + src \ + +# Example source +EXAMPLE_SOURCE += \ + src/main.c \ + src/usb_descriptors.c \ + +SRC_C += $(addprefix $(EXAMPLE_PATH)/, $(EXAMPLE_SOURCE)) + +# Suppress pre-existing warning in usbd.c +CFLAGS_GCC += -Wno-type-limits + +include ../../../hw/bsp/family_rules.mk diff --git a/examples/device/midi2_device/README.md b/examples/device/midi2_device/README.md new file mode 100644 index 000000000..1731dba57 --- /dev/null +++ b/examples/device/midi2_device/README.md @@ -0,0 +1,59 @@ +# MIDI 2.0 Song Sender + +USB MIDI 2.0 Device example that plays "Twinkle Twinkle Little Star" using +native UMP (Universal MIDI Packet) format with full MIDI 2.0 expression. + +## MIDI 2.0 Features Demonstrated + +- 16-bit Velocity (vs 7-bit MIDI 1.0) +- 32-bit Control Change values +- 32-bit Pitch Bend (vs 14-bit MIDI 1.0) +- 32-bit Channel Pressure (Aftertouch) +- 32-bit Poly Pressure (Per-Note Aftertouch) +- Per-Note Management (MIDI 2.0 exclusive) +- Program Change with Bank Select +- JR Timestamps + +## USB Descriptor + +The device exposes both USB-MIDI 1.0 (Alt Setting 0) and USB-MIDI 2.0 (Alt Setting 1) +as required by the USB-MIDI 2.0 specification. A MIDI 2.0 capable host (e.g. Windows +MIDI Services) will select Alt Setting 1 for native UMP transport. Legacy hosts use +Alt Setting 0 with automatic MIDI 1.0 fallback. + +## Hardware + +- Any RP2040 board with USB (e.g. Raspberry Pi Pico) +- LED on GPIO 25: steady = playing, slow blink = waiting for host + +## Building + +```bash +mkdir build && cd build +cmake -DBOARD=raspberry_pi_pico -DPICO_SDK_FETCH_FROM_GIT=on -G Ninja .. +cmake --build . +``` + +## Flashing + +Hold BOOTSEL, connect USB, drag `midi2_device.uf2` to the RPI-RP2 drive. + +## Testing + +**Linux:** +```bash +aseqdump -p "MIDI 2.0 Device" +``` + +**Windows (MIDI 2.0 native):** +```powershell +midi endpoint list +midi endpoint monitor +``` + +## Song Data + +Twinkle Twinkle Little Star in C major, 120 BPM. Six phrases with dynamic +shaping (pp to ff crescendo and back), pitch bend vibrato on sustained notes, +and channel/poly pressure for expression. All values use genuine MIDI 2.0 +resolution with no 7-bit equivalent. diff --git a/examples/device/midi2_device/skip.txt b/examples/device/midi2_device/skip.txt new file mode 100644 index 000000000..eadb6e74a --- /dev/null +++ b/examples/device/midi2_device/skip.txt @@ -0,0 +1 @@ +mcu:SAMD11 diff --git a/examples/device/midi2_device/src/main.c b/examples/device/midi2_device/src/main.c new file mode 100644 index 000000000..62741ac41 --- /dev/null +++ b/examples/device/midi2_device/src/main.c @@ -0,0 +1,720 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * 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 <stdio.h> +#include <string.h> +#include "bsp/board_api.h" +#include "tusb.h" +#include "class/midi/midi2_device.h" + +//--------------------------------------------------------------------+ +// MIDI 2.0 UMP Message Type Constants (M2-104-UM, Section 4) +//--------------------------------------------------------------------+ +// Message Type (MT) occupies bits 31-28 of Word 0 +#define UMP_MT_UTILITY 0x00000000 // 32-bit: Utility (NOOP, JR Clock, JR Timestamp) +#define UMP_MT_SYSTEM 0x10000000 // 32-bit: System Common / Real Time +#define UMP_MT_MIDI1_CV 0x20000000 // 32-bit: MIDI 1.0 Channel Voice +#define UMP_MT_DATA64 0x30000000 // 64-bit: Data (SysEx 7-bit) +#define UMP_MT_MIDI2_CV 0x40000000 // 64-bit: MIDI 2.0 Channel Voice +#define UMP_MT_DATA128 0x50000000 // 128-bit: Data (SysEx 8-bit) + +// MIDI 2.0 Channel Voice status (bits 23-20 of Word 0) +#define UMP_STATUS_NOTE_OFF 0x00800000 +#define UMP_STATUS_NOTE_ON 0x00900000 +#define UMP_STATUS_POLY_PRESSURE 0x00A00000 +#define UMP_STATUS_CC 0x00B00000 +#define UMP_STATUS_PROGRAM 0x00C00000 +#define UMP_STATUS_CHAN_PRESSURE 0x00D00000 +#define UMP_STATUS_PITCH_BEND 0x00E00000 +#define UMP_STATUS_PN_MGMT 0x00F00000 // Per-Note Management + +// Note Attribute Types (MIDI 2.0 spec, Section 4.2.6) +#define UMP_ATTR_NONE 0x00 +#define UMP_ATTR_MANUFACTURER 0x01 +#define UMP_ATTR_PROFILE 0x02 +#define UMP_ATTR_PITCH_7_9 0x03 // Pitch 7.9 format + +//--------------------------------------------------------------------+ +// MIDI 2.0 UMP Builders - Full Spec Coverage +//--------------------------------------------------------------------+ + +// Helper: send a 64-bit UMP (2 words) +static inline void ump_send_64(uint32_t w0, uint32_t w1) { + uint32_t words[2] = { w0, w1 }; + tud_midi2_ump_write(words, 2); +} + +// Helper: send a 32-bit UMP (1 word) +static inline void ump_send_32(uint32_t w0) { + tud_midi2_ump_write(&w0, 1); +} + +// -- Utility Messages (MT=0x0, 32-bit) -- + +static inline void ump_jr_timestamp(uint16_t timestamp) { + // Word 0: [MT(0x0) | Group(0) | Status(0x0020) | Timestamp(16-bit)] + ump_send_32(UMP_MT_UTILITY | 0x00200000 | (uint32_t)timestamp); +} + +// -- MIDI 2.0 Channel Voice: Note On (MT=0x4, 64-bit) -- +// Word 0: [MT(4):Group(4):Status(4):Channel(4):NoteNumber(8):AttrType(8)] +// Word 1: [Velocity(16):Attribute(16)] +static inline void ump_note_on(uint8_t group, uint8_t channel, + uint8_t pitch, uint16_t velocity, + uint8_t attr_type, uint16_t attr_val) { + uint32_t w0 = UMP_MT_MIDI2_CV | ((uint32_t)(group & 0x0F) << 24) + | UMP_STATUS_NOTE_ON | ((uint32_t)(channel & 0x0F) << 16) + | ((uint32_t)(pitch & 0x7F) << 8) + | (uint32_t)(attr_type & 0xFF); + uint32_t w1 = ((uint32_t)(velocity & 0xFFFF) << 16) + | (uint32_t)(attr_val & 0xFFFF); + ump_send_64(w0, w1); +} + +// -- MIDI 2.0 Channel Voice: Note Off (MT=0x4, 64-bit) -- +static inline void ump_note_off(uint8_t group, uint8_t channel, + uint8_t pitch, uint16_t velocity, + uint8_t attr_type, uint16_t attr_val) { + uint32_t w0 = UMP_MT_MIDI2_CV | ((uint32_t)(group & 0x0F) << 24) + | UMP_STATUS_NOTE_OFF | ((uint32_t)(channel & 0x0F) << 16) + | ((uint32_t)(pitch & 0x7F) << 8) + | (uint32_t)(attr_type & 0xFF); + uint32_t w1 = ((uint32_t)(velocity & 0xFFFF) << 16) + | (uint32_t)(attr_val & 0xFFFF); + ump_send_64(w0, w1); +} + +// -- MIDI 2.0 Channel Voice: Control Change (MT=0x4, 64-bit) -- +// Word 0: [MT(4):Group(4):Status(0xB):Channel(4):Index(8):Reserved(8)] +// Word 1: [Data(32)] -- full 32-bit CC resolution (vs 7-bit MIDI 1.0) +static inline void ump_cc(uint8_t group, uint8_t channel, + uint8_t index, uint32_t value) { + uint32_t w0 = UMP_MT_MIDI2_CV | ((uint32_t)(group & 0x0F) << 24) + | UMP_STATUS_CC | ((uint32_t)(channel & 0x0F) << 16) + | ((uint32_t)(index & 0x7F) << 8); + ump_send_64(w0, value); +} + +// -- MIDI 2.0 Channel Voice: Program Change (MT=0x4, 64-bit) -- +// Word 0: [MT(4):Group(4):Status(0xC):Channel(4):Reserved(8):OptionFlags(8)] +// Word 1: [Program(8):Reserved(8):BankMSB(8):BankLSB(8)] +// OptionFlags bit 0 = Bank Valid +static inline void ump_program_change(uint8_t group, uint8_t channel, + uint8_t program, + bool bank_valid, uint8_t bank_msb, + uint8_t bank_lsb) { + uint8_t flags = bank_valid ? 0x01 : 0x00; + uint32_t w0 = UMP_MT_MIDI2_CV | ((uint32_t)(group & 0x0F) << 24) + | UMP_STATUS_PROGRAM | ((uint32_t)(channel & 0x0F) << 16) + | (uint32_t)flags; + uint32_t w1 = ((uint32_t)program << 24) + | ((uint32_t)bank_msb << 8) + | (uint32_t)bank_lsb; + ump_send_64(w0, w1); +} + +// -- MIDI 2.0 Channel Voice: Pitch Bend (MT=0x4, 64-bit) -- +// Word 0: [MT(4):Group(4):Status(0xE):Channel(4):Reserved(16)] +// Word 1: [PitchBend(32)] -- full 32-bit (vs 14-bit MIDI 1.0!) +// 0x80000000 = center, 0x00000000 = min, 0xFFFFFFFF = max +static inline void ump_pitch_bend(uint8_t group, uint8_t channel, + uint32_t value) { + uint32_t w0 = UMP_MT_MIDI2_CV | ((uint32_t)(group & 0x0F) << 24) + | UMP_STATUS_PITCH_BEND | ((uint32_t)(channel & 0x0F) << 16); + ump_send_64(w0, value); +} + +// -- MIDI 2.0 Channel Voice: Channel Pressure / Aftertouch (MT=0x4, 64-bit) -- +// Word 0: [MT(4):Group(4):Status(0xD):Channel(4):Reserved(16)] +// Word 1: [Pressure(32)] -- full 32-bit (vs 7-bit MIDI 1.0) +static inline void ump_channel_pressure(uint8_t group, uint8_t channel, + uint32_t pressure) { + uint32_t w0 = UMP_MT_MIDI2_CV | ((uint32_t)(group & 0x0F) << 24) + | UMP_STATUS_CHAN_PRESSURE | ((uint32_t)(channel & 0x0F) << 16); + ump_send_64(w0, pressure); +} + +// -- MIDI 2.0 Channel Voice: Poly Pressure / Per-Note Aftertouch -- +// Word 0: [MT(4):Group(4):Status(0xA):Channel(4):NoteNumber(8):Reserved(8)] +// Word 1: [Pressure(32)] +static inline void ump_poly_pressure(uint8_t group, uint8_t channel, + uint8_t pitch, uint32_t pressure) { + uint32_t w0 = UMP_MT_MIDI2_CV | ((uint32_t)(group & 0x0F) << 24) + | UMP_STATUS_POLY_PRESSURE | ((uint32_t)(channel & 0x0F) << 16) + | ((uint32_t)(pitch & 0x7F) << 8); + ump_send_64(w0, pressure); +} + +// -- MIDI 2.0 Channel Voice: Per-Note Management (MT=0x4, 64-bit) -- +// Exclusive to MIDI 2.0: controls per-note behavior +// Word 0: [MT(4):Group(4):Status(0xF):Channel(4):NoteNumber(8):Flags(8)] +// Word 1: Reserved +// Flags bit 1 = Reset (S), bit 0 = Detach (D) +static inline void ump_per_note_mgmt(uint8_t group, uint8_t channel, + uint8_t pitch, bool detach, + bool reset) { + uint8_t flags = (reset ? 0x02 : 0x00) | (detach ? 0x01 : 0x00); + uint32_t w0 = UMP_MT_MIDI2_CV | ((uint32_t)(group & 0x0F) << 24) + | UMP_STATUS_PN_MGMT | ((uint32_t)(channel & 0x0F) << 16) + | ((uint32_t)(pitch & 0x7F) << 8) + | (uint32_t)flags; + ump_send_64(w0, 0x00000000); +} + +//--------------------------------------------------------------------+ +// MIDI 1.0 Channel Voice Builders (UMP MT 0x2, 32-bit) +//--------------------------------------------------------------------+ +// Used on Alt 1 when negotiated protocol is MIDI 1.0. +// Word layout: [MT(0x2) | Group(4b) | Status(8b) | Data1(8b) | Data2(8b)] +// Status nibbles: 0x8=NoteOff, 0x9=NoteOn, 0xA=PolyPress, 0xB=CC, +// 0xC=ProgChg, 0xD=ChanPress, 0xE=PitchBend. + +static inline void ump_midi1_send(uint8_t group, uint8_t status, + uint8_t data1, uint8_t data2) { + uint32_t w = UMP_MT_MIDI1_CV + | ((uint32_t)(group & 0x0F) << 24) + | ((uint32_t)status << 16) + | ((uint32_t)data1 << 8) + | (uint32_t)data2; + ump_send_32(w); +} + +static inline void ump_midi1_note_on(uint8_t group, uint8_t channel, + uint8_t note, uint8_t vel7) { + ump_midi1_send(group, 0x90 | (channel & 0x0F), note & 0x7F, vel7 & 0x7F); +} + +static inline void ump_midi1_note_off(uint8_t group, uint8_t channel, + uint8_t note, uint8_t vel7) { + ump_midi1_send(group, 0x80 | (channel & 0x0F), note & 0x7F, vel7 & 0x7F); +} + +static inline void ump_midi1_cc(uint8_t group, uint8_t channel, + uint8_t cc, uint8_t val7) { + ump_midi1_send(group, 0xB0 | (channel & 0x0F), cc & 0x7F, val7 & 0x7F); +} + +static inline void ump_midi1_program(uint8_t group, uint8_t channel, + uint8_t program) { + ump_midi1_send(group, 0xC0 | (channel & 0x0F), program & 0x7F, 0); +} + +static inline void ump_midi1_pitch_bend(uint8_t group, uint8_t channel, + uint16_t value14) { + // 14-bit pitch bend: LSB first, then MSB. Center = 0x2000. + ump_midi1_send(group, 0xE0 | (channel & 0x0F), + (uint8_t)(value14 & 0x7F), + (uint8_t)((value14 >> 7) & 0x7F)); +} + +static inline void ump_midi1_channel_pressure(uint8_t group, uint8_t channel, + uint8_t val7) { + ump_midi1_send(group, 0xD0 | (channel & 0x0F), val7 & 0x7F, 0); +} + +static inline void ump_midi1_poly_pressure(uint8_t group, uint8_t channel, + uint8_t note, uint8_t val7) { + ump_midi1_send(group, 0xA0 | (channel & 0x0F), note & 0x7F, val7 & 0x7F); +} + +//--------------------------------------------------------------------+ +// USB-MIDI 1.0 32-bit Event Packet Builders (Alt 0 transport) +//--------------------------------------------------------------------+ +// Used on Alt 0 (USB-MIDI 1.0). Each packet is 4 raw bytes: +// [(Cable << 4) | CIN] [Status] [Data1] [Data2] +// CIN = Code Index Number. See USB-MIDI 1.0 spec Section 4. + +static inline void midi1_pkt_send(uint8_t cable, uint8_t cin, + uint8_t status, uint8_t data1, + uint8_t data2) { + uint8_t packet[4] = { + (uint8_t)(((cable & 0x0F) << 4) | (cin & 0x0F)), + status, data1, data2 + }; + (void) tud_midi2_packet_write(packet, 1); +} + +static inline void midi1_pkt_note_on(uint8_t cable, uint8_t channel, + uint8_t note, uint8_t vel7) { + midi1_pkt_send(cable, 0x9, 0x90 | (channel & 0x0F), + note & 0x7F, vel7 & 0x7F); +} + +static inline void midi1_pkt_note_off(uint8_t cable, uint8_t channel, + uint8_t note, uint8_t vel7) { + midi1_pkt_send(cable, 0x8, 0x80 | (channel & 0x0F), + note & 0x7F, vel7 & 0x7F); +} + +static inline void midi1_pkt_cc(uint8_t cable, uint8_t channel, + uint8_t cc, uint8_t val7) { + midi1_pkt_send(cable, 0xB, 0xB0 | (channel & 0x0F), + cc & 0x7F, val7 & 0x7F); +} + +static inline void midi1_pkt_program(uint8_t cable, uint8_t channel, + uint8_t program) { + midi1_pkt_send(cable, 0xC, 0xC0 | (channel & 0x0F), + program & 0x7F, 0); +} + +static inline void midi1_pkt_pitch_bend(uint8_t cable, uint8_t channel, + uint16_t value14) { + midi1_pkt_send(cable, 0xE, 0xE0 | (channel & 0x0F), + (uint8_t)(value14 & 0x7F), + (uint8_t)((value14 >> 7) & 0x7F)); +} + +static inline void midi1_pkt_channel_pressure(uint8_t cable, uint8_t channel, + uint8_t val7) { + midi1_pkt_send(cable, 0xD, 0xD0 | (channel & 0x0F), val7 & 0x7F, 0); +} + +static inline void midi1_pkt_poly_pressure(uint8_t cable, uint8_t channel, + uint8_t note, uint8_t val7) { + midi1_pkt_send(cable, 0xA, 0xA0 | (channel & 0x0F), + note & 0x7F, val7 & 0x7F); +} + +//--------------------------------------------------------------------+ +// Scaling Helpers (MIDI 2.0 ↔ MIDI 1.0) +//--------------------------------------------------------------------+ + +static inline uint8_t scale_vel16_to_vel7(uint16_t v16) { return (uint8_t)(v16 >> 9); } +static inline uint8_t scale_val32_to_val7(uint32_t v32) { return (uint8_t)(v32 >> 25); } +static inline uint16_t scale_pb32_to_pb14(uint32_t pb32) { return (uint16_t)(pb32 >> 18); } + +//--------------------------------------------------------------------+ +// Dispatch Layer - Transport + Protocol Fallback +//--------------------------------------------------------------------+ +// Follows the same idea as the UAC examples (`tud_descriptor_configuration_cb` +// returning UAC1 or UAC2 based on bus speed): pick the path that matches the +// state the host put us in. Here the decision is made per message because +// MIDI 2.0 advertises both alts in a single config descriptor; the host +// selects via SetInterface and UMP Stream protocol negotiation. + +static void send_note_on(uint8_t grp, uint8_t ch, uint8_t note, uint16_t vel16) { + uint8_t alt = tud_midi2_alt_setting(); + if (alt == 0) { + midi1_pkt_note_on(grp, ch, note, scale_vel16_to_vel7(vel16)); + } else if (tud_midi2_protocol() == MIDI_PROTOCOL_MIDI1) { + ump_midi1_note_on(grp, ch, note, scale_vel16_to_vel7(vel16)); + } else { + ump_note_on(grp, ch, note, vel16, UMP_ATTR_NONE, 0); + } +} + +static void send_note_off(uint8_t grp, uint8_t ch, uint8_t note, uint16_t vel16) { + uint8_t alt = tud_midi2_alt_setting(); + if (alt == 0) { + midi1_pkt_note_off(grp, ch, note, scale_vel16_to_vel7(vel16)); + } else if (tud_midi2_protocol() == MIDI_PROTOCOL_MIDI1) { + ump_midi1_note_off(grp, ch, note, scale_vel16_to_vel7(vel16)); + } else { + ump_note_off(grp, ch, note, vel16, UMP_ATTR_NONE, 0); + } +} + +static void send_cc(uint8_t grp, uint8_t ch, uint8_t cc, uint32_t val32) { + uint8_t alt = tud_midi2_alt_setting(); + if (alt == 0) { + midi1_pkt_cc(grp, ch, cc, scale_val32_to_val7(val32)); + } else if (tud_midi2_protocol() == MIDI_PROTOCOL_MIDI1) { + ump_midi1_cc(grp, ch, cc, scale_val32_to_val7(val32)); + } else { + ump_cc(grp, ch, cc, val32); + } +} + +static void send_program_change(uint8_t grp, uint8_t ch, uint8_t program, + bool with_bank, uint8_t bank_msb, + uint8_t bank_lsb) { + uint8_t alt = tud_midi2_alt_setting(); + if (alt == 0) { + if (with_bank) { + midi1_pkt_cc(grp, ch, 0x00, bank_msb); + midi1_pkt_cc(grp, ch, 0x20, bank_lsb); + } + midi1_pkt_program(grp, ch, program); + } else if (tud_midi2_protocol() == MIDI_PROTOCOL_MIDI1) { + if (with_bank) { + ump_midi1_cc(grp, ch, 0x00, bank_msb); + ump_midi1_cc(grp, ch, 0x20, bank_lsb); + } + ump_midi1_program(grp, ch, program); + } else { + ump_program_change(grp, ch, program, with_bank, bank_msb, bank_lsb); + } +} + +static void send_pitch_bend(uint8_t grp, uint8_t ch, uint32_t pb32) { + uint8_t alt = tud_midi2_alt_setting(); + if (alt == 0) { + midi1_pkt_pitch_bend(grp, ch, scale_pb32_to_pb14(pb32)); + } else if (tud_midi2_protocol() == MIDI_PROTOCOL_MIDI1) { + ump_midi1_pitch_bend(grp, ch, scale_pb32_to_pb14(pb32)); + } else { + ump_pitch_bend(grp, ch, pb32); + } +} + +static void send_channel_pressure(uint8_t grp, uint8_t ch, uint32_t val32) { + uint8_t alt = tud_midi2_alt_setting(); + if (alt == 0) { + midi1_pkt_channel_pressure(grp, ch, scale_val32_to_val7(val32)); + } else if (tud_midi2_protocol() == MIDI_PROTOCOL_MIDI1) { + ump_midi1_channel_pressure(grp, ch, scale_val32_to_val7(val32)); + } else { + ump_channel_pressure(grp, ch, val32); + } +} + +static void send_poly_pressure(uint8_t grp, uint8_t ch, uint8_t note, + uint32_t val32) { + uint8_t alt = tud_midi2_alt_setting(); + if (alt == 0) { + midi1_pkt_poly_pressure(grp, ch, note, scale_val32_to_val7(val32)); + } else if (tud_midi2_protocol() == MIDI_PROTOCOL_MIDI1) { + ump_midi1_poly_pressure(grp, ch, note, scale_val32_to_val7(val32)); + } else { + ump_poly_pressure(grp, ch, note, val32); + } +} + +//--------------------------------------------------------------------+ +// Song Data +//--------------------------------------------------------------------+ + +// Extended note event with MIDI 2.0 expression data +typedef struct { + uint8_t pitch; // MIDI pitch (0-127, 0=rest) + uint16_t duration_ms; // Duration in ms + uint16_t velocity; // 16-bit velocity (MIDI 2.0) + uint32_t pressure; // 32-bit aftertouch (0 = none) + int16_t bend_cents; // Pitch bend in cents (0 = none, for vibrato/ornaments) +} midi2_note_t; + +// 16-bit velocity (MIDI 2.0): values that have NO 7-bit equivalent. +// MIDI 1.0 can only express 128 levels (0x0000, 0x0200, 0x0400 ... 0xFE00). +// These use the full 16-bit range to prove genuine MIDI 2.0 resolution. +#define V_PPP 0x0A3D // 2621 - between MIDI1 vel 5 and 6 +#define V_PP 0x1C71 // 7281 - between MIDI1 vel 14 and 15 +#define V_P 0x3219 // 12825 - between MIDI1 vel 24 and 25 +#define V_MP 0x4F5C // 20316 - between MIDI1 vel 39 and 40 +#define V_MF 0x6E93 // 28307 - between MIDI1 vel 55 and 56 +#define V_F 0x8DA5 // 36261 - between MIDI1 vel 70 and 71 +#define V_FF 0xAC37 // 44087 - between MIDI1 vel 85 and 86 +#define V_FFF 0xDEB8 // 57016 - between MIDI1 vel 111 and 112 + +// Twinkle Twinkle Little Star - Traditional +// Tempo: 120 BPM (500ms per quarter note) +// Key: C major, 4/4 +// Demonstrates all MIDI 2.0 Channel Voice features: +// 16-bit velocity, 32-bit CC, 32-bit pitch bend, +// 32-bit channel pressure, per-note poly pressure, +// per-note management, program change with bank select, +// JR timestamps +static const midi2_note_t song_data[] = { + // Phrase 1: "Twin-kle twin-kle lit-tle star" (C C G G A A G-) + // Crescendo pp -> mp, gentle entry + { .pitch = 60, .duration_ms = 500, .velocity = V_PP, .pressure = 0, .bend_cents = 0 }, // C4 + { .pitch = 60, .duration_ms = 500, .velocity = V_P, .pressure = 0, .bend_cents = 0 }, // C4 + { .pitch = 67, .duration_ms = 500, .velocity = V_MP, .pressure = 0, .bend_cents = 0 }, // G4 + { .pitch = 67, .duration_ms = 500, .velocity = V_MP, .pressure = 0, .bend_cents = 0 }, // G4 + { .pitch = 69, .duration_ms = 500, .velocity = V_MF, .pressure = 0x1A3D7E5F, .bend_cents = 0 }, // A4 (32-bit pressure) + { .pitch = 69, .duration_ms = 500, .velocity = V_MF, .pressure = 0x2B851EB9, .bend_cents = 0 }, // A4 (pressure swell) + { .pitch = 67, .duration_ms = 1000,.velocity = V_MF, .pressure = 0x3C6EF373, .bend_cents = 7 }, // G4 (half, bend 7 cents) + + // Phrase 2: "How I won-der what you are" (F F E E D D C-) + // mf, sustained + { .pitch = 65, .duration_ms = 500, .velocity = V_MF, .pressure = 0, .bend_cents = 0 }, // F4 + { .pitch = 65, .duration_ms = 500, .velocity = V_MF, .pressure = 0, .bend_cents = 0 }, // F4 + { .pitch = 64, .duration_ms = 500, .velocity = V_MF, .pressure = 0x1E4C2B7A, .bend_cents = 0 }, // E4 + { .pitch = 64, .duration_ms = 500, .velocity = V_MF, .pressure = 0x2D5A8FC1, .bend_cents = 0 }, // E4 (aftertouch swell) + { .pitch = 62, .duration_ms = 500, .velocity = V_MP, .pressure = 0, .bend_cents = 0 }, // D4 + { .pitch = 62, .duration_ms = 500, .velocity = V_MP, .pressure = 0, .bend_cents = 0 }, // D4 + { .pitch = 60, .duration_ms = 1000,.velocity = V_MP, .pressure = 0, .bend_cents = 0 }, // C4 (half, resolve) + + // Phrase 3: "Up a-bove the world so high" (G G F F E E D-) + // f, building intensity + { .pitch = 67, .duration_ms = 500, .velocity = V_F, .pressure = 0, .bend_cents = 0 }, // G4 + { .pitch = 67, .duration_ms = 500, .velocity = V_F, .pressure = 0, .bend_cents = 0 }, // G4 + { .pitch = 65, .duration_ms = 500, .velocity = V_F, .pressure = 0x2F8A4E13, .bend_cents = 0 }, // F4 + { .pitch = 65, .duration_ms = 500, .velocity = V_MF, .pressure = 0x41B2C9D7, .bend_cents = 0 }, // F4 (triggers poly pressure) + { .pitch = 64, .duration_ms = 500, .velocity = V_MF, .pressure = 0, .bend_cents = 0 }, // E4 + { .pitch = 64, .duration_ms = 500, .velocity = V_MF, .pressure = 0, .bend_cents = 0 }, // E4 + { .pitch = 62, .duration_ms = 1000,.velocity = V_MF, .pressure = 0x537DC2A6, .bend_cents = 13 }, // D4 (half, vibrato 13 cents) + + // Phrase 4: "Like a dia-mond in the sky" (G G F F E E D-) + // ff, expressive peak + { .pitch = 67, .duration_ms = 500, .velocity = V_FF, .pressure = 0, .bend_cents = 0 }, // G4 + { .pitch = 67, .duration_ms = 500, .velocity = V_FF, .pressure = 0, .bend_cents = 0 }, // G4 + { .pitch = 65, .duration_ms = 500, .velocity = V_F, .pressure = 0x44E7B8D2, .bend_cents = 0 }, // F4 (triggers poly pressure) + { .pitch = 65, .duration_ms = 500, .velocity = V_F, .pressure = 0x56A3F14B, .bend_cents = 0 }, // F4 (triggers poly pressure) + { .pitch = 64, .duration_ms = 500, .velocity = V_MF, .pressure = 0x2C8E1F5A, .bend_cents = 0 }, // E4 + { .pitch = 64, .duration_ms = 500, .velocity = V_MF, .pressure = 0x1D73A4E8, .bend_cents = 0 }, // E4 + { .pitch = 62, .duration_ms = 1000,.velocity = V_MF, .pressure = 0x63F5B17D, .bend_cents = 19 }, // D4 (half, vibrato 19 cents) + + // Phrase 5: "Twin-kle twin-kle lit-tle star" (C C G G A A G-) + // Diminuendo mf -> mp + { .pitch = 60, .duration_ms = 500, .velocity = V_MF, .pressure = 0, .bend_cents = 0 }, // C4 + { .pitch = 60, .duration_ms = 500, .velocity = V_MF, .pressure = 0, .bend_cents = 0 }, // C4 + { .pitch = 67, .duration_ms = 500, .velocity = V_MP, .pressure = 0, .bend_cents = 0 }, // G4 + { .pitch = 67, .duration_ms = 500, .velocity = V_MP, .pressure = 0, .bend_cents = 0 }, // G4 + { .pitch = 69, .duration_ms = 500, .velocity = V_MP, .pressure = 0x1B4F6D83, .bend_cents = 0 }, // A4 + { .pitch = 69, .duration_ms = 500, .velocity = V_P, .pressure = 0x0E29C5A1, .bend_cents = 0 }, // A4 + { .pitch = 67, .duration_ms = 1000,.velocity = V_P, .pressure = 0x2A6D3B9E, .bend_cents = 5 }, // G4 (half, gentle bend 5 cents) + + // Phrase 6: "How I won-der what you are" (F F E E D D C-) + // Dying away mp -> ppp + { .pitch = 65, .duration_ms = 500, .velocity = V_MP, .pressure = 0, .bend_cents = 0 }, // F4 + { .pitch = 65, .duration_ms = 500, .velocity = V_P, .pressure = 0, .bend_cents = 0 }, // F4 + { .pitch = 64, .duration_ms = 500, .velocity = V_P, .pressure = 0, .bend_cents = 0 }, // E4 + { .pitch = 64, .duration_ms = 500, .velocity = V_PP, .pressure = 0, .bend_cents = 0 }, // E4 + { .pitch = 62, .duration_ms = 500, .velocity = V_PP, .pressure = 0, .bend_cents = 0 }, // D4 + { .pitch = 62, .duration_ms = 500, .velocity = V_PPP, .pressure = 0, .bend_cents = 0 }, // D4 + { .pitch = 60, .duration_ms = 2000,.velocity = V_PPP, .pressure = 0x07A1E3C9, .bend_cents = 0 }, // C4 (fermata) + + // Silence before loop + { .pitch = 0, .duration_ms = 1000,.velocity = 0, .pressure = 0, .bend_cents = 0 }, + + // End marker + { .pitch = 0, .duration_ms = 0, .velocity = 0, .pressure = 0, .bend_cents = 0 }, +}; + +#define SONG_LENGTH (sizeof(song_data) / sizeof(midi2_note_t)) + +//--------------------------------------------------------------------+ +// Song Playback State Machine +//--------------------------------------------------------------------+ + +typedef struct { + uint32_t current_note_idx; + uint32_t note_start_ms; + uint8_t active_pitch; + bool note_is_active; + bool setup_sent; // Initial setup (Program Change, CC) sent? + uint32_t loop_count; +} song_state_t; + +static song_state_t song = { 0 }; + +// Forward declarations +void update_song_playback(uint32_t now_ms); +void send_initial_setup(void); + +//--------------------------------------------------------------------+ +// MIDI 2.0 Device Callbacks (override weak stubs from middleware) +//--------------------------------------------------------------------+ + +void tud_midi2_rx_cb(uint8_t itf) { + // Drain the RX FIFO in a loop until empty. Leaving words in the FIFO + // across callbacks can prevent subsequent bulk OUT transfers from landing. + uint32_t words[8]; + uint32_t n; + while ((n = tud_midi2_n_ump_read(itf, words, TU_ARRAY_SIZE(words))) > 0) { + (void) n; + } +} + +// Reset playback state and re-send setup when host switches alt setting or +// renegotiates the UMP Stream protocol. +void tud_midi2_set_itf_cb(uint8_t itf, uint8_t alt) { + (void)itf; + song.setup_sent = false; + printf("[ALT] Host selected alt=%u\r\n", (unsigned)alt); +} + +//--------------------------------------------------------------------+ +// Initial Setup - Program Change, CC, Per-Note Management +//--------------------------------------------------------------------+ + +void send_initial_setup(void) { + // JR Timestamp is UMP-only (Utility MT 0x0); skipped on Alt 0 transport. + if (tud_midi2_alt_setting() == 1) ump_jr_timestamp(0x0001); + + send_program_change(0, 0, 0, true, 0, 0); + send_cc(0, 0, 7, 0xCCCCCCCC); // Volume 80% + send_cc(0, 0, 11, 0xFFFFFFFF); // Expression 100% + send_cc(0, 0, 64, 0x00000000); // Sustain off + send_cc(0, 0, 1, 0x20000000); // Modulation + send_cc(0, 0, 10, 0x80000000); // Pan center + + // Per-Note Management is MIDI 2.0 exclusive (MT 0x4 status 0xF). + // Skipped when the active path falls back to MIDI 1.0 in any form. + if (tud_midi2_alt_setting() == 1 && + tud_midi2_protocol() == MIDI_PROTOCOL_MIDI2) { + ump_per_note_mgmt(0, 0, 0, false, true); + } + + send_pitch_bend(0, 0, 0x80000000); // Center + send_channel_pressure(0, 0, 0x00000000); + + printf("[SETUP] Piano | Vol 80%% | alt=%u proto=%u\r\n", + (unsigned)tud_midi2_alt_setting(), + (unsigned)tud_midi2_protocol()); +} + +//--------------------------------------------------------------------+ +// Pitch Bend Conversion: cents to 32-bit value +//--------------------------------------------------------------------+ + +// Convert pitch bend in cents (-200 to +200) to 32-bit UMP value +// Center = 0x80000000, range = +/- 2 semitones (200 cents) +static inline uint32_t cents_to_pitch_bend(int16_t cents) { + if (cents == 0) return 0x80000000; + // Scale: 200 cents = full range (0x7FFFFFFF deviation from center) + int32_t offset = (int32_t)(((int64_t)cents * 0x7FFFFFFF) / 200); + return (uint32_t)((int32_t)0x80000000 + offset); +} + +//--------------------------------------------------------------------+ +// Song Playback Logic - Full MIDI 2.0 Expression +//--------------------------------------------------------------------+ + +void update_song_playback(uint32_t now_ms) { + const midi2_note_t *current = &song_data[song.current_note_idx]; + + if (!song.setup_sent) { + send_initial_setup(); + song.setup_sent = true; + song.note_start_ms = now_ms; + } + + // Note duration elapsed: send Note Off, advance + if (song.note_is_active && (now_ms - song.note_start_ms) >= current->duration_ms) { + if (song.active_pitch > 0) { + if (current->bend_cents != 0) send_pitch_bend(0, 0, 0x80000000); + if (current->pressure > 0) send_channel_pressure(0, 0, 0x00000000); + send_note_off(0, 0, song.active_pitch, V_P); + } + + song.note_is_active = false; + song.current_note_idx++; + + if (song.current_note_idx >= SONG_LENGTH) { + song.current_note_idx = 0; + song.setup_sent = false; + song.loop_count++; + printf("\r\n=== Loop %lu ===\r\n", (unsigned long)song.loop_count); + } + + song.note_start_ms = now_ms; + } + + // Start next note + if (!song.note_is_active && song.current_note_idx < SONG_LENGTH) { + const midi2_note_t *next = &song_data[song.current_note_idx]; + + if (next->duration_ms == 0) { + song.current_note_idx = 0; + song.setup_sent = false; + song.loop_count++; + printf("\r\n=== Loop %lu ===\r\n", (unsigned long)song.loop_count); + return; + } + + if (next->pitch > 0) { + // JR Timestamp is UMP-only; skip on Alt 0 transport. + if (tud_midi2_alt_setting() == 1) { + ump_jr_timestamp((uint16_t)(now_ms & 0xFFFF)); + } + if (next->bend_cents != 0) { + send_pitch_bend(0, 0, cents_to_pitch_bend(next->bend_cents)); + } + send_note_on(0, 0, next->pitch, next->velocity); + if (next->pressure > 0) { + send_channel_pressure(0, 0, next->pressure); + } + if (next->pressure > 0x40000000 && next->duration_ms > 500) { + send_poly_pressure(0, 0, next->pitch, next->pressure); + } + + song.active_pitch = next->pitch; + song.note_is_active = true; + } + + // Rest: honor duration + if (!song.note_is_active) { + song.active_pitch = 0; + song.note_is_active = true; + song.note_start_ms = now_ms; + } + } +} + +//--------------------------------------------------------------------+ +// Main +//--------------------------------------------------------------------+ + +int main(void) { + board_init(); + printf("\r\n"); + printf("===========================================\r\n"); + printf(" TinyUSB MIDI 2.0 Device\r\n"); + printf("===========================================\r\n"); + printf("Tempo: 120 BPM | Song: %u notes\r\n", (unsigned)SONG_LENGTH); + printf("Transport + protocol fallback:\r\n"); + printf(" Alt 0 -> USB-MIDI 1.0 32-bit Event Packets (packet_write)\r\n"); + printf(" Alt 1 + MIDI1 -> UMP MT 0x2 MIDI 1.0 Channel Voice (ump_write)\r\n"); + printf(" Alt 1 + MIDI2 -> UMP MT 0x4 MIDI 2.0 Channel Voice (ump_write)\r\n"); + printf("Status: Initializing...\r\n"); + + 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(); + board_led_write(true); + + uint32_t last_report_ms = 0; + + while (1) { + tud_task(); + + uint32_t now_ms = tusb_time_millis_api(); + + if (tud_midi2_mounted()) { + update_song_playback(now_ms); + board_led_write(song.active_pitch > 0); + } else { + board_led_write((now_ms / 500) & 1); + } + + // Status report every 10 seconds + if (now_ms - last_report_ms > 10000) { + last_report_ms = now_ms; + if (tud_midi2_mounted()) { + printf("[%lums] Playing idx %lu/%u loop %lu\r\n", + (unsigned long)now_ms, + (unsigned long)song.current_note_idx, + (unsigned)SONG_LENGTH, + (unsigned long)song.loop_count); + } else { + printf("[%lums] Waiting for host...\r\n", (unsigned long)now_ms); + } + } + } + + return 0; +} diff --git a/examples/device/midi2_device/src/tusb_config.h b/examples/device/midi2_device/src/tusb_config.h new file mode 100644 index 000000000..1ada0015f --- /dev/null +++ b/examples/device/midi2_device/src/tusb_config.h @@ -0,0 +1,88 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * 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_MIDI2 1 + +#ifdef __cplusplus +} +#endif + +#endif /* TUSB_CONFIG_H_ */ diff --git a/examples/device/midi2_device/src/usb_descriptors.c b/examples/device/midi2_device/src/usb_descriptors.c new file mode 100644 index 000000000..19a43f2d8 --- /dev/null +++ b/examples/device/midi2_device/src/usb_descriptors.c @@ -0,0 +1,142 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * 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" +#include "class/audio/audio.h" +#include "class/midi/midi.h" + +//--------------------------------------------------------------------+ +// Device Descriptors +//--------------------------------------------------------------------+ + +static tusb_desc_device_t const desc_device = { + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = 0x0200, + .bDeviceClass = 0x00, + .bDeviceSubClass = 0x00, + .bDeviceProtocol = 0x00, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + + .idVendor = 0xcafe, + .idProduct = 0x4062, // MIDI 2.0 Device + .bcdDevice = 0x0100, + + .iManufacturer = 0x01, + .iProduct = 0x02, + .iSerialNumber = 0x03, + + .bNumConfigurations = 0x01 +}; + +uint8_t const * tud_descriptor_device_cb(void) { + return (uint8_t const *) &desc_device; +} + +//--------------------------------------------------------------------+ +// Configuration Descriptor - MIDI 2.0 +//--------------------------------------------------------------------+ + +enum { + ITF_NUM_MIDI2 = 0, // Audio Control interface + ITF_NUM_MIDI2_STREAMING, // MIDI Streaming interface (auto-created by TUD_MIDI2_DESCRIPTOR) + ITF_NUM_TOTAL +}; + +#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_MIDI2_DESC_LEN) + +// Endpoint addresses +#define EPNUM_MIDI2_OUT 0x01 +#define EPNUM_MIDI2_IN 0x81 + +static uint8_t const desc_fs_configuration[] = { + // Config number, interface count, string index, total length, attribute, power in mA + TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100), + + // MIDI 2.0 Interface + TUD_MIDI2_DESCRIPTOR(ITF_NUM_MIDI2, 0, EPNUM_MIDI2_OUT, EPNUM_MIDI2_IN, 64) +}; + +uint8_t const * tud_descriptor_configuration_cb(uint8_t index) { + (void) index; + return desc_fs_configuration; +} + +//--------------------------------------------------------------------+ +// String Descriptors +//--------------------------------------------------------------------+ + +enum { + STRID_LANGID = 0, + STRID_MANUFACTURER = 1, + STRID_PRODUCT = 2, + STRID_SERIAL = 3, +}; + +static char const *string_desc_arr[] = { + (const char[]) { 0x09, 0x04 }, // 0: Language + "TinyUSB", // 1: Manufacturer + "TinyUSB MIDI 2.0", // 2: Product + NULL, // 3: Serial +}; + +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); + const size_t 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/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/mtp_fs_example.c b/examples/device/mtp/src/mtp_fs_example.c index 09697693e..b7d062c64 100644 --- a/examples/device/mtp/src/mtp_fs_example.c +++ b/examples/device/mtp/src/mtp_fs_example.c @@ -143,6 +143,7 @@ static int32_t fs_get_device_properties(tud_mtp_cb_data_t* cb_data); static int32_t fs_get_object_handles(tud_mtp_cb_data_t* cb_data); static int32_t fs_get_object_info(tud_mtp_cb_data_t* cb_data); static int32_t fs_get_object(tud_mtp_cb_data_t* cb_data); +static int32_t fs_get_partial_object(tud_mtp_cb_data_t* cb_data); static int32_t fs_delete_object(tud_mtp_cb_data_t* cb_data); static int32_t fs_send_object_info(tud_mtp_cb_data_t* cb_data); static int32_t fs_send_object(tud_mtp_cb_data_t* cb_data); @@ -164,6 +165,7 @@ fs_op_handler_dict_t fs_op_handler_dict[] = { { MTP_OP_GET_OBJECT_HANDLES, fs_get_object_handles }, { MTP_OP_GET_OBJECT_INFO, fs_get_object_info }, { MTP_OP_GET_OBJECT, fs_get_object }, + { MTP_OP_GET_PARTIAL_OBJECT, fs_get_partial_object }, { MTP_OP_DELETE_OBJECT, fs_delete_object }, { MTP_OP_SEND_OBJECT_INFO, fs_send_object_info }, { MTP_OP_SEND_OBJECT, fs_send_object }, @@ -330,6 +332,14 @@ int32_t tud_mtp_data_complete_cb(tud_mtp_cb_data_t* cb_data) { break; } + case MTP_OP_GET_PARTIAL_OBJECT: { + // response parameter: actual length of data sent excluding container header + const uint32_t len = cb_data->total_xferred_bytes - sizeof(mtp_container_header_t); + (void) mtp_container_add_uint32(resp, len); + resp->header->code = MTP_RESP_OK; + break; + } + default: resp->header->code = (cb_data->xfer_result == XFER_RESULT_SUCCESS) ? MTP_RESP_OK : MTP_RESP_GENERAL_ERROR; break; @@ -535,6 +545,40 @@ static int32_t fs_get_object(tud_mtp_cb_data_t* cb_data) { return 0; } +static int32_t fs_get_partial_object(tud_mtp_cb_data_t* cb_data) { + const mtp_container_command_t* command = cb_data->command_container; + mtp_container_info_t* io_container = &cb_data->io_container; + const uint32_t obj_handle = command->params[0]; + const uint32_t req_offset = command->params[1]; + const uint32_t req_max = command->params[2]; + const fs_file_t* f = fs_get_file(obj_handle); + if (f == NULL) { + return MTP_RESP_INVALID_OBJECT_HANDLE; + } + + const uint32_t avail = (req_offset >= f->size) ? 0u : (f->size - req_offset); + const uint32_t to_send = tu_min32(avail, req_max); + + if (cb_data->phase == MTP_PHASE_COMMAND) { + // If file contents is larger than CFG_TUD_MTP_EP_BUFSIZE, data may only partially be added here + // the rest will be sent in tud_mtp_data_more_cb + (void) mtp_container_add_raw(io_container, f->data + req_offset, to_send); + tud_mtp_data_send(io_container); + } else if (cb_data->phase == MTP_PHASE_DATA) { + // continue sending remaining data: file contents offset is xferred byte minus header size + const uint32_t offset = cb_data->total_xferred_bytes - sizeof(mtp_container_header_t); + const uint32_t xact_len = tu_min32(to_send - offset, io_container->payload_bytes); + if (xact_len > 0) { + memcpy(io_container->payload, f->data + offset + req_offset, xact_len); + tud_mtp_data_send(io_container); + } + } else { + // nothing to do + } + + return 0; +} + static int32_t fs_send_object_info(tud_mtp_cb_data_t* cb_data) { const mtp_container_command_t* command = cb_data->command_container; mtp_container_info_t* io_container = &cb_data->io_container; diff --git a/examples/device/mtp/src/tusb_config.h b/examples/device/mtp/src/tusb_config.h index 95cc048ee..5224cd79b 100644 --- a/examples/device/mtp/src/tusb_config.h +++ b/examples/device/mtp/src/tusb_config.h @@ -106,6 +106,7 @@ MTP_OP_GET_OBJECT_HANDLES, \ MTP_OP_GET_OBJECT_INFO, \ MTP_OP_GET_OBJECT, \ + MTP_OP_GET_PARTIAL_OBJECT, \ MTP_OP_DELETE_OBJECT, \ MTP_OP_SEND_OBJECT_INFO, \ MTP_OP_SEND_OBJECT, \ 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..c594d1ebd 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, OPT_MCU_CH32V307) || \ + 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..7c74e3c73 100644 --- a/examples/host/CMakeLists.txt +++ b/examples/host/CMakeLists.txt @@ -13,7 +13,9 @@ set(EXAMPLE_LIST device_info hid_controller midi_rx + midi2_host msc_file_explorer + msc_file_explorer_freertos ) foreach (example ${EXAMPLE_LIST}) diff --git a/examples/host/midi2_host/CMakeLists.txt b/examples/host/midi2_host/CMakeLists.txt new file mode 100644 index 000000000..0ec03bf5f --- /dev/null +++ b/examples/host/midi2_host/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.20) + +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake) + +project(midi2_host 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 + ) + +# Example include +target_include_directories(${PROJECT_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_host_example(${PROJECT_NAME} noos) diff --git a/examples/host/midi2_host/CMakePresets.json b/examples/host/midi2_host/CMakePresets.json new file mode 100644 index 000000000..5cd8971e9 --- /dev/null +++ b/examples/host/midi2_host/CMakePresets.json @@ -0,0 +1,6 @@ +{ + "version": 6, + "include": [ + "../../../hw/bsp/BoardPresets.json" + ] +} diff --git a/examples/host/midi2_host/Makefile b/examples/host/midi2_host/Makefile new file mode 100644 index 000000000..f8292385e --- /dev/null +++ b/examples/host/midi2_host/Makefile @@ -0,0 +1,13 @@ +include ../../../hw/bsp/family_support.mk + +INC += \ + src \ + + +# Example source +EXAMPLE_SOURCE += \ + src/main.c + +SRC_C += $(addprefix $(EXAMPLE_PATH)/, $(EXAMPLE_SOURCE)) + +include ../../../hw/bsp/family_rules.mk diff --git a/examples/host/midi2_host/only.txt b/examples/host/midi2_host/only.txt new file mode 100644 index 000000000..c71aacd87 --- /dev/null +++ b/examples/host/midi2_host/only.txt @@ -0,0 +1,34 @@ +family:hpmicro +family:samd21 +family:samd5x_e5x +mcu:CH32V20X +mcu:ESP32P4 +mcu:ESP32S2 +mcu:ESP32S3 +mcu:KINETIS_KL +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:RAXXX +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/midi2_host/skip.txt b/examples/host/midi2_host/skip.txt new file mode 100644 index 000000000..308796869 --- /dev/null +++ b/examples/host/midi2_host/skip.txt @@ -0,0 +1 @@ +board:lpcxpresso54114 diff --git a/examples/host/midi2_host/src/main.c b/examples/host/midi2_host/src/main.c new file mode 100644 index 000000000..63b08318c --- /dev/null +++ b/examples/host/midi2_host/src/main.c @@ -0,0 +1,165 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * 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. + */ + +// Minimal USB-MIDI 2.0 host example. +// Receives UMP from any MIDI 2.0 device, prints each packet to stdout. + +#include <stdio.h> +#include <string.h> +#include "bsp/board_api.h" +#include "tusb.h" +#include "class/midi/midi2_host.h" + +//--------------------------------------------------------------------+ +// State +//--------------------------------------------------------------------+ + +static uint8_t midi2_idx = 0xFF; + +//--------------------------------------------------------------------+ +// UMP printer - shows MT and word(s) in hex; decodes Channel Voice +//--------------------------------------------------------------------+ + +static void print_ump(const uint32_t* words, uint8_t wc) { + uint8_t mt = (uint8_t)((words[0] >> 28) & 0x0F); + uint8_t group = (uint8_t)((words[0] >> 24) & 0x0F); + + if (mt == 0x4 && wc >= 2) { + // MIDI 2.0 Channel Voice + uint8_t status = (uint8_t)((words[0] >> 20) & 0x0F); + uint8_t channel = (uint8_t)((words[0] >> 16) & 0x0F); + uint8_t data1 = (uint8_t)((words[0] >> 8) & 0x7F); + switch (status) { + case 0x9: + printf("[g%u ch%u] M2 NoteOn n=%u vel=%04X attr=%04X\r\n", + group, channel, data1, + (unsigned)((words[1] >> 16) & 0xFFFF), + (unsigned)(words[1] & 0xFFFF)); + break; + case 0x8: + printf("[g%u ch%u] M2 NoteOff n=%u vel=%04X\r\n", + group, channel, data1, + (unsigned)((words[1] >> 16) & 0xFFFF)); + break; + case 0xB: + printf("[g%u ch%u] M2 CC#%u = %08lX\r\n", + group, channel, data1, (unsigned long)words[1]); + break; + case 0xC: + printf("[g%u ch%u] M2 ProgChg %u\r\n", + group, channel, (unsigned)((words[1] >> 24) & 0x7F)); + break; + case 0xD: + printf("[g%u ch%u] M2 ChanPress %08lX\r\n", + group, channel, (unsigned long)words[1]); + break; + case 0xE: + printf("[g%u ch%u] M2 PitchBend %08lX\r\n", + group, channel, (unsigned long)words[1]); + break; + default: + printf("[g%u ch%u] M2 status=0x%X w0=%08lX w1=%08lX\r\n", + group, channel, status, + (unsigned long)words[0], (unsigned long)words[1]); + break; + } + } else if (mt == 0x2 && wc == 1) { + // MIDI 1.0 Channel Voice + uint8_t status = (uint8_t)((words[0] >> 16) & 0xFF); + uint8_t data1 = (uint8_t)((words[0] >> 8) & 0x7F); + uint8_t data2 = (uint8_t)(words[0] & 0x7F); + printf("[g%u] M1 %02X %02X %02X\r\n", group, status, data1, data2); + } else { + printf("UMP MT=0x%X wc=%u w0=%08lX\r\n", + mt, wc, (unsigned long)words[0]); + } +} + +//--------------------------------------------------------------------+ +// MIDI 2.0 Host Callbacks +//--------------------------------------------------------------------+ + +void tuh_midi2_descriptor_cb(uint8_t idx, const tuh_midi2_descriptor_cb_t* d) { + (void)idx; + printf("MIDI2 descriptor: %s, RX cables=%u TX cables=%u\r\n", + d->protocol_version ? "MIDI 2.0" : "MIDI 1.0", + d->rx_cable_count, d->tx_cable_count); +} + +void tuh_midi2_mount_cb(uint8_t idx, const tuh_midi2_mount_cb_t* m) { + midi2_idx = idx; + printf("MIDI2 mounted: idx=%u protocol=%s\r\n", + idx, m->protocol_version ? "MIDI 2.0" : "MIDI 1.0"); +} + +void tuh_midi2_rx_cb(uint8_t idx, uint32_t xferred_bytes) { + (void)xferred_bytes; + + uint32_t words[16]; + while (1) { + uint32_t n = tuh_midi2_ump_read(idx, words, 16); + if (n == 0) break; + + uint32_t i = 0; + while (i < n) { + uint8_t mt = (uint8_t)((words[i] >> 28) & 0x0F); + uint8_t wc = midi2_ump_word_count(mt); + if (i + wc > n) break; + print_ump(&words[i], wc); + i += wc; + } + } +} + +void tuh_midi2_tx_cb(uint8_t idx, uint32_t xferred_bytes) { + (void)idx; (void)xferred_bytes; +} + +void tuh_midi2_umount_cb(uint8_t idx) { + (void)idx; + midi2_idx = 0xFF; + printf("MIDI2 unmounted\r\n"); +} + +//--------------------------------------------------------------------+ +// Main +//--------------------------------------------------------------------+ + +int main(void) { + board_init(); + + printf("\r\nTinyUSB Host MIDI 2.0 Example\r\n"); + + tusb_rhport_init_t host_init = { + .role = TUSB_ROLE_HOST, + .speed = TUSB_SPEED_AUTO, + }; + tusb_init(BOARD_TUH_RHPORT, &host_init); + + while (1) { + tuh_task(); + } + + return 0; +} diff --git a/examples/host/midi2_host/src/tusb_config.h b/examples/host/midi2_host/src/tusb_config.h new file mode 100644 index 000000000..4bd6ef2ea --- /dev/null +++ b/examples/host/midi2_host/src/tusb_config.h @@ -0,0 +1,106 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * 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 +//--------------------------------------------------------------------+ + +#ifndef CFG_TUSB_MCU +#error CFG_TUSB_MCU must be defined +#endif + +#ifdef ESP_PLATFORM +#define CFG_TUSB_OS_INC_PATH freertos/ +#endif + +#ifndef CFG_TUSB_OS +#define CFG_TUSB_OS OPT_OS_NONE +#endif + +#ifndef CFG_TUSB_DEBUG +#define CFG_TUSB_DEBUG 0 +#endif + +#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 +//--------------------------------------------------------------------+ + +#define CFG_TUH_ENABLED 1 + +#if CFG_TUSB_MCU == OPT_MCU_RP2040 + // #define CFG_TUH_RPI_PIO_USB 1 // use pio-usb as host controller + // #define CFG_TUH_MAX3421 1 // use max3421 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 + +#define CFG_TUH_MAX_SPEED BOARD_TUH_MAX_SPEED + +//------------------------- Board Specific --------------------------+ + +#ifndef BOARD_TUH_RHPORT +#define BOARD_TUH_RHPORT 0 +#endif + +#ifndef BOARD_TUH_MAX_SPEED +#define BOARD_TUH_MAX_SPEED OPT_MODE_DEFAULT_SPEED +#endif + +//--------------------------------------------------------------------+ +// Driver Configuration +//--------------------------------------------------------------------+ + +#define CFG_TUH_ENUMERATION_BUFSIZE 256 + +#define CFG_TUH_HUB 1 +#define CFG_TUH_DEVICE_MAX (3*CFG_TUH_HUB + 1) + +// USB-MIDI 2.0 Host +#define CFG_TUH_MIDI2 CFG_TUH_DEVICE_MAX +#define CFG_TUH_MIDI2_RX_BUFSIZE 512 +#define CFG_TUH_MIDI2_TX_BUFSIZE 512 + +#ifdef __cplusplus +} +#endif + +#endif 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/ch32v10x/linker/ch32v10x.ld b/hw/bsp/ch32v10x/linker/ch32v10x.ld index cd5c8dc17..a2de3c25f 100644 --- a/hw/bsp/ch32v10x/linker/ch32v10x.ld +++ b/hw/bsp/ch32v10x/linker/ch32v10x.ld @@ -13,6 +13,7 @@ ENTRY( _start ) __stack_size = 2048; PROVIDE( _stack_size = __stack_size ); +_estack = ORIGIN(RAM) + LENGTH(RAM); SECTIONS { @@ -150,16 +151,16 @@ SECTIONS PROVIDE( _ebss = .); } >RAM AT>FLASH - PROVIDE( _end = _ebss); - PROVIDE( end = . ); - - .stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size : - { - PROVIDE( _heap_end = . ); - . = ALIGN(4); - PROVIDE(_susrstack = . ); - . = . + __stack_size; - PROVIDE( _eusrstack = .); - } >RAM + ._user_heap_stack : + { + . = ALIGN(8); + PROVIDE( end = . ); + PROVIDE( _end = . ); + . = . + __stack_size; + . = ALIGN(8); + } >RAM + PROVIDE( _heap_end = ORIGIN(RAM) + LENGTH(RAM) - __stack_size ); + PROVIDE( _susrstack = _heap_end ); + PROVIDE( _eusrstack = ORIGIN(RAM) + LENGTH(RAM) ); } 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/ch32v20x/family.cmake b/hw/bsp/ch32v20x/family.cmake index 59a96f70d..785f5ee35 100644 --- a/hw/bsp/ch32v20x/family.cmake +++ b/hw/bsp/ch32v20x/family.cmake @@ -61,8 +61,9 @@ function(family_add_board BOARD_TARGET) if (RHPORT_DEVICE EQUAL 0) target_compile_definitions(${BOARD_TARGET} PUBLIC CFG_TUD_WCH_USBIP_FSDEV=1) + target_compile_definitions(${BOARD_TARGET} PUBLIC CFG_TUH_WCH_USBIP_FSDEV=1) elseif (RHPORT_DEVICE EQUAL 1) - target_compile_definitions(${BOARD_TARGET} PUBLIC CFG_TUH_WCH_USBIP_USBFS=1) + target_compile_definitions(${BOARD_TARGET} PUBLIC CFG_TUD_WCH_USBIP_USBFS=1) else() message(FATAL_ERROR "Invalid RHPORT_DEVICE ${RHPORT_DEVICE}") endif() diff --git a/hw/bsp/ch32v20x/linker/ch32v20x.ld b/hw/bsp/ch32v20x/linker/ch32v20x.ld index f84808b0d..bfa9a8436 100644 --- a/hw/bsp/ch32v20x/linker/ch32v20x.ld +++ b/hw/bsp/ch32v20x/linker/ch32v20x.ld @@ -12,6 +12,7 @@ MEMORY ENTRY( _start ) PROVIDE( _stack_size = __stack_size ); +_estack = ORIGIN(RAM) + LENGTH(RAM); SECTIONS { @@ -149,16 +150,16 @@ SECTIONS PROVIDE( _ebss = .); } >RAM AT>FLASH - PROVIDE( _end = _ebss); - PROVIDE( end = . ); - - .stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size : - { - PROVIDE( _heap_end = . ); - . = ALIGN(4); - PROVIDE(_susrstack = . ); - . = . + __stack_size; - PROVIDE( _eusrstack = .); - } >RAM + ._user_heap_stack : + { + . = ALIGN(8); + PROVIDE( end = . ); + PROVIDE( _end = . ); + . = . + __stack_size; + . = ALIGN(8); + } >RAM + PROVIDE( _heap_end = ORIGIN(RAM) + LENGTH(RAM) - __stack_size ); + PROVIDE( _susrstack = _heap_end ); + PROVIDE( _eusrstack = ORIGIN(RAM) + LENGTH(RAM) ); } diff --git a/hw/bsp/ch32v30x/linker/ch32v30x.ld b/hw/bsp/ch32v30x/linker/ch32v30x.ld index 6dd5d344a..2941c41a0 100644 --- a/hw/bsp/ch32v30x/linker/ch32v30x.ld +++ b/hw/bsp/ch32v30x/linker/ch32v30x.ld @@ -12,6 +12,7 @@ MEMORY ENTRY( _start ) PROVIDE( _stack_size = __stack_size ); +_estack = ORIGIN(RAM) + LENGTH(RAM); SECTIONS { @@ -151,17 +152,16 @@ SECTIONS PROVIDE( _ebss = .); } >RAM AT>FLASH - PROVIDE( _end = _ebss); - PROVIDE( end = . ); - - .stack ORIGIN(RAM) + LENGTH(RAM) - __stack_size : - { - PROVIDE( _heap_end = . ); - . = ALIGN(4); - PROVIDE(_susrstack = . ); - . = . + __stack_size; - PROVIDE( _eusrstack = .); - __freertos_irq_stack_top = .; - } >RAM + ._user_heap_stack : + { + . = ALIGN(8); + PROVIDE( end = . ); + PROVIDE( _end = . ); + . = . + __stack_size; + . = ALIGN(8); + } >RAM + PROVIDE( _heap_end = ORIGIN(RAM) + LENGTH(RAM) - __stack_size ); + PROVIDE( _susrstack = _heap_end ); + PROVIDE( _eusrstack = ORIGIN(RAM) + LENGTH(RAM) ); } 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..aab9a4fae 100644 --- a/hw/bsp/rp2040/family.cmake +++ b/hw/bsp/rp2040/family.cmake @@ -93,13 +93,13 @@ 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 ${TOP}/src/class/dfu/dfu_rt_device.c ${TOP}/src/class/hid/hid_device.c ${TOP}/src/class/midi/midi_device.c + ${TOP}/src/class/midi/midi2_device.c ${TOP}/src/class/msc/msc_device.c ${TOP}/src/class/mtp/mtp_device.c ${TOP}/src/class/net/ecm_rndis_device.c @@ -122,6 +122,7 @@ target_sources(tinyusb_host_base INTERFACE ${TOP}/src/class/cdc/cdc_host.c ${TOP}/src/class/hid/hid_host.c ${TOP}/src/class/midi/midi_host.c + ${TOP}/src/class/midi/midi2_host.c ${TOP}/src/class/msc/msc_host.c ${TOP}/src/class/vendor/vendor_host.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/boards/stm32h573i_dk/board.h b/hw/bsp/stm32h5/boards/stm32h573i_dk/board.h index 5788837ab..e9c406486 100644 --- a/hw/bsp/stm32h5/boards/stm32h573i_dk/board.h +++ b/hw/bsp/stm32h5/boards/stm32h573i_dk/board.h @@ -204,6 +204,10 @@ int32_t i2c_writereg(uint16_t DevAddr, uint16_t Reg, uint8_t *pData, uint16_t Le return 0; } +static int32_t i2c_get_tick(void) { + return (int32_t) HAL_GetTick(); +} + static inline void board_init2(void) { TCPP0203_IO_t io_ctx; @@ -212,6 +216,7 @@ static inline void board_init2(void) { io_ctx.DeInit = board_tcpp0203_deinit; io_ctx.ReadReg = i2c_readreg; io_ctx.WriteReg = i2c_writereg; + io_ctx.GetTick = i2c_get_tick; TU_ASSERT(TCPP0203_RegisterBusIO(&tcpp0203_obj, &io_ctx) == TCPP0203_OK, ); 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/boards/stm32h7s3nucleo/board.h b/hw/bsp/stm32h7rs/boards/stm32h7s3nucleo/board.h index b1446414e..996eb1515 100644 --- a/hw/bsp/stm32h7rs/boards/stm32h7s3nucleo/board.h +++ b/hw/bsp/stm32h7rs/boards/stm32h7s3nucleo/board.h @@ -223,6 +223,10 @@ int32_t i2c_writereg(uint16_t DevAddr, uint16_t Reg, uint8_t *pData, uint16_t Le return 0; } +static int32_t i2c_get_tick(void) { + return (int32_t) HAL_GetTick(); +} + static inline void board_init2(void) { TCPP0203_IO_t io_ctx; @@ -231,6 +235,7 @@ static inline void board_init2(void) { io_ctx.DeInit = board_tcpp0203_deinit; io_ctx.ReadReg = i2c_readreg; io_ctx.WriteReg = i2c_writereg; + io_ctx.GetTick = i2c_get_tick; TU_ASSERT(TCPP0203_RegisterBusIO(&tcpp0203_obj, &io_ctx) == TCPP0203_OK, ); 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/boards/stm32n6570dk/board.h b/hw/bsp/stm32n6/boards/stm32n6570dk/board.h index 4d162bbca..0cba5e1f9 100644 --- a/hw/bsp/stm32n6/boards/stm32n6570dk/board.h +++ b/hw/bsp/stm32n6/boards/stm32n6570dk/board.h @@ -244,6 +244,10 @@ static int32_t i2c_writereg(uint16_t DevAddr, uint16_t Reg, uint8_t *pData, uint return 0; } +static int32_t i2c_get_tick(void) { + return (int32_t) HAL_GetTick(); +} + static inline void board_init2(void) { TCPP0203_IO_t io_ctx; @@ -252,6 +256,7 @@ static inline void board_init2(void) { io_ctx.DeInit = board_tcpp0203_deinit; io_ctx.ReadReg = i2c_readreg; io_ctx.WriteReg = i2c_writereg; + io_ctx.GetTick = i2c_get_tick; TU_ASSERT(TCPP0203_RegisterBusIO(&tcpp0203_obj, &io_ctx) == TCPP0203_OK, ); diff --git a/hw/bsp/stm32n6/boards/stm32n657nucleo/board.h b/hw/bsp/stm32n6/boards/stm32n657nucleo/board.h index c26367af6..be9ea7a31 100644 --- a/hw/bsp/stm32n6/boards/stm32n657nucleo/board.h +++ b/hw/bsp/stm32n6/boards/stm32n657nucleo/board.h @@ -239,6 +239,10 @@ int32_t i2c_writereg(uint16_t DevAddr, uint16_t Reg, uint8_t *pData, uint16_t Le return 0; } +static int32_t i2c_get_tick(void) { + return (int32_t) HAL_GetTick(); +} + static inline void board_init2(void) { TCPP0203_IO_t io_ctx; @@ -247,6 +251,7 @@ static inline void board_init2(void) { io_ctx.DeInit = board_tcpp0203_deinit; io_ctx.ReadReg = i2c_readreg; io_ctx.WriteReg = i2c_writereg; + io_ctx.GetTick = i2c_get_tick; TU_ASSERT(TCPP0203_RegisterBusIO(&tcpp0203_obj, &io_ctx) == TCPP0203_OK, ); 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..b3e05f60f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -8,13 +8,13 @@ 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 ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/dfu/dfu_rt_device.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/hid/hid_device.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/midi/midi_device.c + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/midi/midi2_device.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/msc/msc_device.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/mtp/mtp_device.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/net/ecm_rndis_device.c @@ -29,6 +29,7 @@ function(tinyusb_sources_get OUTPUT_VAR) ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/cdc/cdc_host.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/hid/hid_host.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/midi/midi_host.c + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/midi/midi2_host.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/msc/msc_host.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/vendor/vendor_host.c # typec 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.h b/src/class/midi/midi.h index cd67640e4..8121ec016 100644 --- a/src/class/midi/midi.h +++ b/src/class/midi/midi.h @@ -186,6 +186,24 @@ typedef midi_desc_cs_endpoint_n_t(1) midi_desc_cs_endpoint_1jack_t; TU_VERIFY_STATIC(sizeof(midi_desc_cs_endpoint_1jack_t) == 4+1, "size is not correct"); //--------------------------------------------------------------------+ +// MIDI 2.0 UMP Helpers +//--------------------------------------------------------------------+ + +// Return the number of 32-bit words for a UMP message given its Message Type +static inline uint8_t midi2_ump_word_count(uint8_t mt) { + switch (mt) { + case 0x0: case 0x1: case 0x2: case 0x6: case 0x7: + return 1; + case 0x3: case 0x4: case 0x8: case 0x9: case 0xA: + return 2; + case 0xB: case 0xC: + return 3; + default: // 0x5, 0xD, 0xE, 0xF + return 4; + } +} + +//--------------------------------------------------------------------+ // For Internal Driver Use //--------------------------------------------------------------------+ typedef struct { diff --git a/src/class/midi/midi2_device.c b/src/class/midi/midi2_device.c new file mode 100644 index 000000000..b14f439f8 --- /dev/null +++ b/src/class/midi/midi2_device.c @@ -0,0 +1,769 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * 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 && CFG_TUD_MIDI2 + +#include <string.h> + +#include "device/usbd.h" +#include "device/usbd_pvt.h" +#include "midi2_device.h" + +//--------------------------------------------------------------------+ +// Weak stubs +//--------------------------------------------------------------------+ +TU_ATTR_WEAK void tud_midi2_rx_cb(uint8_t itf) { (void) itf; } +TU_ATTR_WEAK void tud_midi2_set_itf_cb(uint8_t itf, uint8_t alt) { (void) itf; (void) alt; } +TU_ATTR_WEAK bool tud_midi2_get_req_itf_cb(uint8_t rhport, const tusb_control_request_t* request) { + (void) rhport; (void) request; return false; +} +TU_ATTR_WEAK uint8_t tud_midi2_num_groups_cb(uint8_t itf) { + (void) itf; return CFG_TUD_MIDI2_NUM_GROUPS; +} +TU_ATTR_WEAK uint8_t tud_midi2_num_function_blocks_cb(uint8_t itf) { + (void) itf; return CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS; +} +TU_ATTR_WEAK const char* tud_midi2_ep_name_cb(uint8_t itf) { + (void) itf; return CFG_TUD_MIDI2_EP_NAME; +} +TU_ATTR_WEAK const char* tud_midi2_product_id_cb(uint8_t itf) { + (void) itf; return CFG_TUD_MIDI2_PRODUCT_ID; +} + +//--------------------------------------------------------------------+ +// Byte order note +//--------------------------------------------------------------------+ +// Per USB-MIDI 2.0 Section 3.2.2, each 32-bit UMP word is transmitted with the +// least significant byte first. This driver reads and writes UMP words as +// native uint32_t through tu_edpt_stream_read/write. All TinyUSB targets are +// little-endian, so the in-memory layout already matches the wire order and no +// swap is needed. If a big-endian target is ever supported, wrap access with +// tu_htole32 / tu_le32toh at the buffer boundary. + +//--------------------------------------------------------------------+ +// UMP Stream Message Constants +//--------------------------------------------------------------------+ +// UMP Message Type for Stream messages (bits 31:28) +enum { + MT_STREAM = 0x0F, +}; + +// UMP Stream Status values (10-bit, bits 25:16) +enum { + STREAM_ENDPOINT_DISCOVERY = 0x000, + STREAM_ENDPOINT_INFO = 0x001, + STREAM_EP_NAME = 0x003, + STREAM_PROD_INSTANCE_ID = 0x004, + STREAM_CONFIG_REQUEST = 0x005, + STREAM_CONFIG_NOTIFY = 0x006, + STREAM_FB_DISCOVERY = 0x010, + STREAM_FB_INFO = 0x011, +}; + +enum { + UMP_VER_MAJOR = 1, + UMP_VER_MINOR = 1, +}; + +// Group Terminal Block descriptor types (USB-MIDI 2.0) +enum { + MIDI2_CS_GRP_TRM_BLOCK = 0x26, + MIDI2_GRP_TRM_BLOCK_HEADER = 0x01, + MIDI2_GRP_TRM_BLOCK_ENTRY = 0x02, +}; + +//--------------------------------------------------------------------+ +// MACRO CONSTANT TYPEDEF +//--------------------------------------------------------------------+ +typedef struct { + uint8_t ep_addr; + uint16_t mps; + tu_fifo_t ff; + +#if CFG_TUD_EDPT_DEDICATED_HWFIFO == 0 + uint8_t* ep_buf; +#endif +} midi2d_tx_t; + +typedef struct { + uint8_t rhport; + uint8_t itf_num; + uint8_t alt_setting; + uint8_t protocol; + bool negotiated; + + /*------------- From this point, data is not cleared by bus reset -------------*/ + struct { + midi2d_tx_t tx; + tu_edpt_stream_t rx; + + uint8_t rx_ff_buf[CFG_TUD_MIDI2_RX_BUFSIZE]; + uint8_t tx_ff_buf[CFG_TUD_MIDI2_TX_BUFSIZE]; + } ep_stream; +} midi2d_interface_t; + +// Skip local EP buffer if dedicated hw FIFO is supported +#if CFG_TUD_EDPT_DEDICATED_HWFIFO == 0 +typedef struct { + TUD_EPBUF_DEF(epin, CFG_TUD_MIDI2_TX_EPSIZE); + TUD_EPBUF_DEF(epout, CFG_TUD_MIDI2_RX_EPSIZE); +} midi2d_epbuf_t; + +CFG_TUD_MEM_SECTION static midi2d_epbuf_t _midi2d_epbuf[CFG_TUD_MIDI2]; +#endif + +TU_VERIFY_STATIC(CFG_TUD_MIDI2_NUM_GROUPS >= 1 && CFG_TUD_MIDI2_NUM_GROUPS <= 16, + "CFG_TUD_MIDI2_NUM_GROUPS must be 1..16"); +TU_VERIFY_STATIC(CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS >= 1 && CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS <= 32, + "CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS must be 1..32"); + +#define ITF_MEM_RESET_SIZE offsetof(midi2d_interface_t, ep_stream) + +static midi2d_interface_t _midi2d_itf[CFG_TUD_MIDI2]; + +// Default Group Terminal Block descriptor (USB-MIDI 2.0 spec, Table 5-5/5-6) +static const uint8_t _default_gtb_desc[] = { + // GTB Header (5 bytes) + 5, // bLength + MIDI2_CS_GRP_TRM_BLOCK, // bDescriptorType + MIDI2_GRP_TRM_BLOCK_HEADER, // bDescriptorSubtype + U16_TO_U8S_LE(18), // wTotalLength (5 + 13 = 18) + + // GTB Entry (13 bytes) + 13, // bLength + MIDI2_CS_GRP_TRM_BLOCK, // bDescriptorType + MIDI2_GRP_TRM_BLOCK_ENTRY, // bDescriptorSubtype + 1, // bGrpTrmBlkID + 0x00, // bGrpTrmBlkType: bidirectional + 0x00, // nGroupTrm: first group (0) + CFG_TUD_MIDI2_NUM_GROUPS, // nNumGroupTrm + CFG_TUD_MIDI2_BLOCK_STRIDX, // iBlockItem: string descriptor index (0 = none) + 0x00, // bMIDIProtocol: unknown/not fixed + 0, 0, // wMaxInputBandwidth: unknown + 0, 0 // wMaxOutputBandwidth: unknown +}; + +//--------------------------------------------------------------------+ +// Common utility functions +//--------------------------------------------------------------------+ + +static inline uint8_t _itf_idx(const midi2d_interface_t* p_midi) { + return (uint8_t)(p_midi - _midi2d_itf); +} + +static inline bool _tx_opened(const midi2d_interface_t* p_midi) { + return p_midi->ep_stream.tx.ep_addr != 0; +} + +static uint8_t _tx_byte_at(const tu_fifo_buffer_info_t* info, uint16_t offset) { + if (offset < info->linear.len) { + return info->linear.ptr[offset]; + } + + offset = (uint16_t) (offset - info->linear.len); + if (offset < info->wrapped.len) { + return info->wrapped.ptr[offset]; + } + + return 0; +} + +// Calculate the largest byte count that contains only whole UMP packets and +// fits in one USB transfer (<= mps). +static uint16_t _tx_nonseg_len_to_mps(midi2d_tx_t* tx) { + tu_fifo_buffer_info_t info; + tu_fifo_get_read_info(&tx->ff, &info); + + const uint16_t available = (uint16_t) (info.linear.len + info.wrapped.len); + uint16_t bytes = 0; + + while (bytes < tx->mps) { + if ((uint16_t) (available - bytes) < 4) break; + + uint8_t mt = (uint8_t)((_tx_byte_at(&info, (uint16_t) (bytes + 3)) >> 4) & 0x0F); + uint8_t pkt_words = midi2_ump_word_count(mt); + uint16_t pkt_bytes = (uint16_t) pkt_words * 4; + + if (pkt_bytes == 0) break; + if ((uint16_t) (available - bytes) < pkt_bytes) break; + if ((uint16_t) (bytes + pkt_bytes) > tx->mps) break; + + bytes = (uint16_t) (bytes + pkt_bytes); + } + + return bytes; +} + +// Start one IN transfer capped at mps, return number of bytes queued to the controller, or 0 if nothing was queued. +static uint16_t _tx_start_xfer(midi2d_interface_t* p_midi) { + midi2d_tx_t* tx = &p_midi->ep_stream.tx; + uint16_t ff_count = tu_fifo_count(&tx->ff); + + if (ff_count == 0) return 0; + + if (!usbd_edpt_claim(p_midi->rhport, tx->ep_addr)) return 0; + + uint16_t bytes; + if (p_midi->alt_setting == 1) { + bytes = _tx_nonseg_len_to_mps(tx); + } else { + bytes = tu_min16(tu_fifo_count(&tx->ff), tx->mps); + } + if (bytes == 0) { + usbd_edpt_release(p_midi->rhport, tx->ep_addr); + return 0; + } + +#if CFG_TUD_EDPT_DEDICATED_HWFIFO + TU_ASSERT(usbd_edpt_xfer_fifo(p_midi->rhport, tx->ep_addr, &tx->ff, bytes, false), 0); +#else + tu_fifo_read_n(&tx->ff, tx->ep_buf, bytes); + TU_ASSERT(usbd_edpt_xfer(p_midi->rhport, tx->ep_addr, tx->ep_buf, bytes, false), 0); +#endif + + return bytes; +} + +static uint32_t _tx_ump_write(midi2d_interface_t* p_midi, const uint32_t* words, uint32_t count) { + uint32_t written = 0; + while (written < count) { + uint8_t mt = (uint8_t)((words[written] >> 28) & 0x0F); + uint8_t pkt_words = midi2_ump_word_count(mt); + uint16_t pkt_bytes = (uint16_t) pkt_words * 4; + + if (written + pkt_words > count) break; + if (tu_fifo_remaining(&p_midi->ep_stream.tx.ff) < pkt_bytes) break; + + if (tu_fifo_write_n(&p_midi->ep_stream.tx.ff, &words[written], pkt_bytes) != pkt_bytes) break; + written += pkt_words; + } + + (void) _tx_start_xfer(p_midi); + return written; +} + +//--------------------------------------------------------------------+ +// Protocol Negotiation +//--------------------------------------------------------------------+ +static void _nego_send_ump(midi2d_interface_t* p_midi, const uint32_t* words, uint8_t count) { + if (!_tx_opened(p_midi)) return; + if (tu_fifo_remaining(&p_midi->ep_stream.tx.ff) < (uint32_t) count * 4) return; + (void) _tx_ump_write(p_midi, words, count); +} + +static void _nego_send_endpoint_info(midi2d_interface_t* p_midi) { + uint32_t msg[4] = {0}; + msg[0] = ((uint32_t) MT_STREAM << 28) + | ((uint32_t) STREAM_ENDPOINT_INFO << 16) + | ((uint32_t) UMP_VER_MAJOR << 8) + | (uint32_t) UMP_VER_MINOR; + msg[1] = (UINT32_C(1) << 31) // Static Function Blocks flag + | ((uint32_t)(tud_midi2_num_function_blocks_cb(_itf_idx(p_midi)) & 0x7F) << 24) + | (UINT32_C(1) << 9) // MIDI 2.0 Protocol capability + | (UINT32_C(1) << 8); // MIDI 1.0 Protocol capability + _nego_send_ump(p_midi, msg, 4); +} + +static void _nego_send_stream_text(midi2d_interface_t* p_midi, uint16_t status, const char* str) { + if (!str || str[0] == '\0') return; + + uint16_t total_len = (uint16_t) strlen(str); + uint16_t offset = 0; + + while (offset < total_len) { + uint16_t remaining = total_len - offset; + uint8_t n = (uint8_t)((remaining > 14) ? 14 : remaining); + bool is_first = (offset == 0); + bool is_last = (remaining <= 14); + + uint8_t form; + if (is_first && is_last) form = 0; + else if (is_first) form = 1; + else if (is_last) form = 3; + else form = 2; + + uint32_t msg[4] = {0}; + msg[0] = ((uint32_t) MT_STREAM << 28) + | ((uint32_t) form << 26) + | ((uint32_t) status << 16); + + const char* p = str + offset; + if (n > 0) msg[0] |= ((uint32_t)(uint8_t) p[0] << 8); + if (n > 1) msg[0] |= (uint32_t)(uint8_t) p[1]; + for (uint8_t i = 2; i < n; i++) { + uint8_t word_idx = (uint8_t)(1 + (i - 2) / 4); + uint8_t shift = (uint8_t)(24 - ((i - 2) % 4) * 8); + msg[word_idx] |= ((uint32_t)(uint8_t) p[i] << shift); + } + + _nego_send_ump(p_midi, msg, 4); + offset += n; + } +} + +static void _nego_send_config_notify(midi2d_interface_t* p_midi, uint8_t protocol) { + uint32_t msg[4] = {0}; + msg[0] = ((uint32_t) MT_STREAM << 28) + | ((uint32_t) STREAM_CONFIG_NOTIFY << 16) + | ((uint32_t) protocol << 8); + _nego_send_ump(p_midi, msg, 4); +} + +static void _nego_send_fb_info(midi2d_interface_t* p_midi, uint8_t fb_idx) { + uint32_t msg[4] = {0}; + msg[0] = ((uint32_t) MT_STREAM << 28) + | ((uint32_t) STREAM_FB_INFO << 16) + | (UINT32_C(1) << 15) + | ((uint32_t) fb_idx << 8) + | 0x02; // bDirection: bidirectional + msg[1] = ((uint32_t) 0 << 24) // bFirstGroup + | ((uint32_t) tud_midi2_num_groups_cb(_itf_idx(p_midi)) << 16); + _nego_send_ump(p_midi, msg, 4); +} + +static void _nego_handle_stream_msg(midi2d_interface_t* p_midi, const uint32_t* words) { + uint16_t status = (words[0] >> 16) & 0x3FF; + + switch (status) { + case STREAM_ENDPOINT_DISCOVERY: + _nego_send_endpoint_info(p_midi); + _nego_send_stream_text(p_midi, STREAM_EP_NAME, tud_midi2_ep_name_cb(_itf_idx(p_midi))); + _nego_send_stream_text(p_midi, STREAM_PROD_INSTANCE_ID, tud_midi2_product_id_cb(_itf_idx(p_midi))); + break; + + case STREAM_CONFIG_REQUEST: { + uint8_t req_proto = (words[0] >> 8) & 0xFF; + if (req_proto == MIDI_PROTOCOL_MIDI1 || req_proto == MIDI_PROTOCOL_MIDI2) { + p_midi->protocol = req_proto; + } + _nego_send_config_notify(p_midi, p_midi->protocol); + p_midi->negotiated = true; + break; + } + + case STREAM_FB_DISCOVERY: { + uint8_t fb_idx = (words[0] >> 8) & 0xFF; + uint8_t fb_count = tud_midi2_num_function_blocks_cb(_itf_idx(p_midi)); + if (fb_idx == 0xFF) { + for (uint8_t f = 0; f < fb_count; f++) { + _nego_send_fb_info(p_midi, f); + } + } else if (fb_idx < fb_count) { + _nego_send_fb_info(p_midi, fb_idx); + } + break; + } + + default: + break; + } +} + +static void _nego_process_rx(midi2d_interface_t* p_midi) { + tu_edpt_stream_t* ep_rx = &p_midi->ep_stream.rx; + uint8_t word_bytes[4]; + + while (tu_fifo_peek_n(&ep_rx->ff, word_bytes, 4) == 4) { + // UMP words travel LSB-first on the wire and in LE memory, so MT is in + // the high nibble of byte 3, not byte 0. + uint8_t mt = (word_bytes[3] >> 4) & 0x0F; + uint8_t pkt_words = midi2_ump_word_count(mt); + uint32_t pkt_bytes = (uint32_t)pkt_words * 4; + + if (mt != MT_STREAM) break; + if (tu_edpt_stream_read_available(ep_rx) < pkt_bytes) break; + + uint32_t buf[4] = {0}; + tu_edpt_stream_read(ep_rx, buf, pkt_bytes); + _nego_handle_stream_msg(p_midi, buf); + } +} + +//--------------------------------------------------------------------+ +// READ API +//--------------------------------------------------------------------+ +bool tud_midi2_n_mounted(uint8_t itf) { + TU_VERIFY(itf < CFG_TUD_MIDI2, false); + midi2d_interface_t* p_midi = &_midi2d_itf[itf]; + return _tx_opened(p_midi) && + tu_edpt_stream_is_opened(&p_midi->ep_stream.rx); +} + +uint32_t tud_midi2_n_available(uint8_t itf) { + TU_VERIFY(itf < CFG_TUD_MIDI2, 0); + midi2d_interface_t* p_midi = &_midi2d_itf[itf]; + return tu_edpt_stream_read_available(&p_midi->ep_stream.rx) / 4; +} + +uint32_t tud_midi2_n_ump_read(uint8_t itf, uint32_t* words, uint32_t max_words) { + TU_VERIFY(itf < CFG_TUD_MIDI2 && words != NULL && max_words > 0, 0); + midi2d_interface_t* p_midi = &_midi2d_itf[itf]; + + // UMP API is only valid on Alt Setting 1 (USB-MIDI 2.0). + // Alt 0 carries USB-MIDI 1.0 32-bit Event Packets, not UMP words. + if (p_midi->alt_setting != 1) { return 0; } + + tu_edpt_stream_t* ep_rx = &p_midi->ep_stream.rx; + + uint32_t total_read = 0; + while (total_read < max_words) { + uint8_t word_bytes[4]; + if (tu_fifo_peek_n(&ep_rx->ff, word_bytes, 4) < 4) break; + + // UMP words travel LSB-first; MT is the high nibble of byte 3, not byte 0. + uint8_t mt = (word_bytes[3] >> 4) & 0x0F; + uint8_t pkt_words = midi2_ump_word_count(mt); + + if (total_read + pkt_words > max_words) break; + if (tu_edpt_stream_read_available(ep_rx) < (uint32_t)pkt_words * 4) break; + + tu_edpt_stream_read(ep_rx, &words[total_read], pkt_words * 4); + total_read += pkt_words; + } + + return total_read; +} + +uint32_t tud_midi2_n_packet_read(uint8_t itf, uint8_t packets[], uint32_t max_packets) { + TU_VERIFY(itf < CFG_TUD_MIDI2 && packets != NULL && max_packets > 0, 0); + midi2d_interface_t* p_midi = &_midi2d_itf[itf]; + return tu_edpt_stream_read(&p_midi->ep_stream.rx, packets, max_packets * 4u) >> 2u; +} + +//--------------------------------------------------------------------+ +// WRITE API +//--------------------------------------------------------------------+ +uint32_t tud_midi2_n_ump_write(uint8_t itf, const uint32_t* words, uint32_t count) { + TU_VERIFY(itf < CFG_TUD_MIDI2 && words != NULL && count > 0, 0); + midi2d_interface_t* p_midi = &_midi2d_itf[itf]; + + // UMP API is only valid on Alt Setting 1 (USB-MIDI 2.0). + // Alt 0 carries USB-MIDI 1.0 32-bit Event Packets, not UMP words. + if (p_midi->alt_setting != 1) { return 0; } + TU_VERIFY(_tx_opened(p_midi), 0); + + return _tx_ump_write(p_midi, words, count); +} + +uint32_t tud_midi2_n_packet_write(uint8_t itf, const uint8_t packets[], uint32_t count) { + TU_VERIFY(itf < CFG_TUD_MIDI2 && packets != NULL && count > 0, 0); + midi2d_interface_t* p_midi = &_midi2d_itf[itf]; + midi2d_tx_t* tx = &p_midi->ep_stream.tx; + + // Packet API is for Alt Setting 0 (USB-MIDI 1.0) event packets. + TU_VERIFY(p_midi->alt_setting == 0, 0); + TU_VERIFY(_tx_opened(p_midi), 0); + + uint32_t written = 0; + while (written < count) { + if (tu_fifo_remaining(&tx->ff) < 4) break; + + if (tu_fifo_write_n(&tx->ff, packets + written * 4u, 4) != 4) break; + written++; + } + + (void) _tx_start_xfer(p_midi); + + return written; +} + +//--------------------------------------------------------------------+ +// STATE GETTERS +//--------------------------------------------------------------------+ +uint8_t tud_midi2_n_alt_setting(uint8_t itf) { + TU_VERIFY(itf < CFG_TUD_MIDI2, 0); + return _midi2d_itf[itf].alt_setting; +} + +bool tud_midi2_n_negotiated(uint8_t itf) { + TU_VERIFY(itf < CFG_TUD_MIDI2, false); + return _midi2d_itf[itf].negotiated; +} + +uint8_t tud_midi2_n_protocol(uint8_t itf) { + TU_VERIFY(itf < CFG_TUD_MIDI2, 0); + return _midi2d_itf[itf].protocol; +} + +//--------------------------------------------------------------------+ +// USBD Driver API +//--------------------------------------------------------------------+ +void midi2d_init(void) { + tu_memclr(_midi2d_itf, sizeof(_midi2d_itf)); + for (uint8_t i = 0; i < CFG_TUD_MIDI2; i++) { + midi2d_interface_t* p_midi = &_midi2d_itf[i]; + p_midi->protocol = MIDI_PROTOCOL_MIDI2; + + #if CFG_TUD_EDPT_DEDICATED_HWFIFO + uint8_t *epout_buf = NULL; + uint8_t *epin_buf = NULL; + #else + uint8_t *epout_buf = _midi2d_epbuf[i].epout; + uint8_t *epin_buf = _midi2d_epbuf[i].epin; + #endif + + tu_edpt_stream_init(&p_midi->ep_stream.rx, false, false, false, + p_midi->ep_stream.rx_ff_buf, CFG_TUD_MIDI2_RX_BUFSIZE, epout_buf); + + midi2d_tx_t* tx = &p_midi->ep_stream.tx; + (void) tu_fifo_config(&tx->ff, p_midi->ep_stream.tx_ff_buf, CFG_TUD_MIDI2_TX_BUFSIZE, false); +#if CFG_TUD_EDPT_DEDICATED_HWFIFO == 0 + tx->ep_buf = epin_buf; +#else + (void) epin_buf; +#endif + } +} + +bool midi2d_deinit(void) { + for (uint8_t i = 0; i < CFG_TUD_MIDI2; i++) { + midi2d_interface_t* p_midi = &_midi2d_itf[i]; + tu_edpt_stream_deinit(&p_midi->ep_stream.rx); + } + return true; +} + +void midi2d_reset(uint8_t rhport) { + (void) rhport; + for (uint8_t i = 0; i < CFG_TUD_MIDI2; i++) { + midi2d_interface_t* p_midi = &_midi2d_itf[i]; + tu_memclr(p_midi, ITF_MEM_RESET_SIZE); + + tu_edpt_stream_clear(&p_midi->ep_stream.rx); + tu_edpt_stream_close(&p_midi->ep_stream.rx); + + tu_fifo_clear(&p_midi->ep_stream.tx.ff); + p_midi->ep_stream.tx.ep_addr = 0; + } +} + +TU_ATTR_ALWAYS_INLINE static inline uint8_t find_midi2_itf(uint8_t ep_addr) { + for (uint8_t idx = 0; idx < CFG_TUD_MIDI2; idx++) { + const midi2d_interface_t* p_midi = &_midi2d_itf[idx]; + if (ep_addr == p_midi->ep_stream.rx.ep_addr || ep_addr == p_midi->ep_stream.tx.ep_addr) { + return idx; + } + } + return TUSB_INDEX_INVALID_8; +} + +static uint8_t find_midi2_itf_by_num(uint8_t itf_num) { + for (uint8_t idx = 0; idx < CFG_TUD_MIDI2; idx++) { + if (_midi2d_itf[idx].itf_num == itf_num) return idx; + } + return TUSB_INDEX_INVALID_8; +} + +uint16_t midi2d_open(uint8_t rhport, const tusb_desc_interface_t* desc_itf, uint16_t max_len) { + const uint8_t* p_desc = (const uint8_t*) desc_itf; + const uint8_t* desc_end = p_desc + max_len; + + // 1st Interface: Audio Control v1 (optional) + if (TUSB_CLASS_AUDIO == desc_itf->bInterfaceClass && + AUDIO_SUBCLASS_CONTROL == desc_itf->bInterfaceSubClass && + AUDIO_FUNC_PROTOCOL_CODE_UNDEF == desc_itf->bInterfaceProtocol) { + p_desc = tu_desc_next(desc_itf); + while (tu_desc_in_bounds(p_desc, desc_end) && TUSB_DESC_CS_INTERFACE == tu_desc_type(p_desc)) { + p_desc = tu_desc_next(p_desc); + } + } + + // 2nd Interface: MIDI Streaming + TU_VERIFY(TUSB_DESC_INTERFACE == tu_desc_type(p_desc), 0); + const tusb_desc_interface_t* desc_midi = (const tusb_desc_interface_t*) p_desc; + + TU_VERIFY(TUSB_CLASS_AUDIO == desc_midi->bInterfaceClass && + AUDIO_SUBCLASS_MIDI_STREAMING == desc_midi->bInterfaceSubClass && + AUDIO_FUNC_PROTOCOL_CODE_UNDEF == desc_midi->bInterfaceProtocol, + 0); + + uint8_t idx = find_midi2_itf(0); + TU_ASSERT(idx < CFG_TUD_MIDI2, 0); + midi2d_interface_t* p_midi = &_midi2d_itf[idx]; + + p_midi->rhport = rhport; + p_midi->itf_num = desc_midi->bInterfaceNumber; + p_midi->alt_setting = 0; + p_midi->protocol = MIDI_PROTOCOL_MIDI2; + p_midi->negotiated = false; + + p_desc = tu_desc_next(p_desc); + + // Skip class-specific descriptors + while (tu_desc_in_bounds(p_desc, desc_end) && TUSB_DESC_CS_INTERFACE == tu_desc_type(p_desc)) { + p_desc = tu_desc_next(p_desc); + } + + // Find and open endpoint descriptors + uint8_t found_ep = 0; + while ((found_ep < desc_midi->bNumEndpoints) && tu_desc_in_bounds(p_desc, desc_end)) { + if (TUSB_DESC_ENDPOINT == tu_desc_type(p_desc)) { + const tusb_desc_endpoint_t* desc_ep = (const tusb_desc_endpoint_t*) p_desc; + TU_ASSERT(usbd_edpt_open(rhport, desc_ep), 0); + const uint8_t ep_addr = desc_ep->bEndpointAddress; + + if (tu_edpt_dir(ep_addr) == TUSB_DIR_IN) { + p_midi->ep_stream.tx.ep_addr = ep_addr; + p_midi->ep_stream.tx.mps = tu_edpt_packet_size(desc_ep); + tu_fifo_clear(&p_midi->ep_stream.tx.ff); + } else { + tu_edpt_stream_open(&p_midi->ep_stream.rx, rhport, desc_ep, tu_edpt_packet_size(desc_ep)); + tu_edpt_stream_clear(&p_midi->ep_stream.rx); + TU_ASSERT(tu_edpt_stream_read_xfer(&p_midi->ep_stream.rx) > 0, 0); + } + + found_ep++; + } + + p_desc = tu_desc_next(p_desc); + } + + // Skip remaining descriptors (alt setting 1, CS endpoints, GTB) + // Stop at any interface descriptor that is not our MIDI Streaming alt setting + while (tu_desc_in_bounds(p_desc, desc_end)) { + uint8_t dtype = tu_desc_type(p_desc); + + if (dtype == TUSB_DESC_INTERFACE) { + const tusb_desc_interface_t* next_itf = (const tusb_desc_interface_t*) p_desc; + // Continue only if this is an alternate setting of our own interface + if (next_itf->bInterfaceNumber != desc_midi->bInterfaceNumber) break; + } else if (dtype != TUSB_DESC_CS_INTERFACE && dtype != TUSB_DESC_CS_ENDPOINT && + dtype != TUSB_DESC_ENDPOINT) { + break; + } + + p_desc = tu_desc_next(p_desc); + } + + return (uint16_t)(p_desc - (const uint8_t*) desc_itf); +} + +bool midi2d_control_xfer_cb(uint8_t rhport, uint8_t stage, const tusb_control_request_t* request) { + TU_LOG2("MIDI2 ctrl: stage=%u bRequest=0x%02X wValue=0x%04X wIndex=0x%04X wLength=%u\r\n", + stage, request->bRequest, request->wValue, request->wIndex, request->wLength); + + if (stage != CONTROL_STAGE_SETUP) return true; + + switch (request->bRequest) { + case TUSB_REQ_SET_INTERFACE: { + uint8_t itf_num = tu_u16_low(request->wIndex); + uint8_t alt = tu_u16_low(request->wValue); + + // Only Alt Setting 0 (MIDI 1.0) and 1 (UMP) are valid + if (alt > 1) return false; + + uint8_t idx = find_midi2_itf_by_num(itf_num); + if (idx >= CFG_TUD_MIDI2) return false; + + midi2d_interface_t* p_midi = &_midi2d_itf[idx]; + p_midi->alt_setting = alt; + + tu_edpt_stream_clear(&p_midi->ep_stream.rx); + tu_fifo_clear(&p_midi->ep_stream.tx.ff); + + if (alt == 1) { + p_midi->negotiated = false; + p_midi->protocol = MIDI_PROTOCOL_MIDI2; + } + + // Re-arm RX endpoint for receiving data after alt setting change + tu_edpt_stream_read_xfer(&p_midi->ep_stream.rx); + + tud_midi2_set_itf_cb(idx, alt); + tud_control_status(rhport, request); + return true; + } + + case TUSB_REQ_GET_DESCRIPTOR: { + // USB-MIDI 2.0 Section 6: GTB descriptor retrieval + // bmRequestType = 0x81 (Device-to-Host, Standard, Interface) + // wValue = CS_GR_TRM_BLOCK (0x26) in high byte, alt setting in low byte + // wIndex = interface number + if (request->bmRequestType_bit.direction != TUSB_DIR_IN) return false; + if (request->bmRequestType_bit.type != TUSB_REQ_TYPE_STANDARD) return false; + if (request->bmRequestType_bit.recipient != TUSB_REQ_RCPT_INTERFACE) return false; + if (tu_u16_high(request->wValue) != MIDI2_CS_GRP_TRM_BLOCK) return false; + + uint8_t itf_num = tu_u16_low(request->wIndex); + uint8_t idx = find_midi2_itf_by_num(itf_num); + if (idx >= CFG_TUD_MIDI2) return false; + + // Only Alt Setting 1 exposes Group Terminal Block descriptors. + if (tu_u16_low(request->wValue) != 0x01) return false; + + if (tud_midi2_get_req_itf_cb(rhport, request)) return true; + + uint16_t len = request->wLength; + if (len > sizeof(_default_gtb_desc)) { + len = sizeof(_default_gtb_desc); + } + tud_control_xfer(rhport, request, (void*)(uintptr_t) _default_gtb_desc, len); + return true; + } + + default: + return false; + } +} + +bool midi2d_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { + (void) rhport; + + uint8_t idx = find_midi2_itf(ep_addr); + TU_ASSERT(idx < CFG_TUD_MIDI2); + midi2d_interface_t* p_midi = &_midi2d_itf[idx]; + + tu_edpt_stream_t* ep_rx = &p_midi->ep_stream.rx; + midi2d_tx_t* ep_tx = &p_midi->ep_stream.tx; + + if (ep_addr == ep_rx->ep_addr) { + if (result == XFER_RESULT_SUCCESS) { + tu_edpt_stream_read_xfer_complete(ep_rx, xferred_bytes); + if (p_midi->alt_setting == 1) { + _nego_process_rx(p_midi); + } + tud_midi2_rx_cb(idx); + } + tu_edpt_stream_read_xfer(ep_rx); + } else if (ep_addr == ep_tx->ep_addr && result == XFER_RESULT_SUCCESS) { + uint16_t queued = _tx_start_xfer(p_midi); + // Send ZLP if no more data is queued but the last transfer was exactly mps + if (queued == 0 && tu_fifo_count(&ep_tx->ff) == 0 && xferred_bytes > 0 && + (0 == (xferred_bytes & (ep_tx->mps - 1)))) { + if (usbd_edpt_claim(rhport, ep_tx->ep_addr)) { + usbd_edpt_xfer(rhport, ep_tx->ep_addr, NULL, 0, false); + } + } + } else { + return false; + } + + return true; +} + +#endif diff --git a/src/class/midi/midi2_device.h b/src/class/midi/midi2_device.h new file mode 100644 index 000000000..e53535693 --- /dev/null +++ b/src/class/midi/midi2_device.h @@ -0,0 +1,193 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * 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 TUSB_MIDI2_DEVICE_H_ +#define TUSB_MIDI2_DEVICE_H_ + +#include "class/audio/audio.h" +#include "midi.h" + +//--------------------------------------------------------------------+ +// Class Driver Configuration +//--------------------------------------------------------------------+ + +// Config defaults are in tusb_option.h: +// CFG_TUD_MIDI2_RX_EPSIZE, CFG_TUD_MIDI2_TX_EPSIZE, +// CFG_TUD_MIDI2_RX_BUFSIZE, CFG_TUD_MIDI2_TX_BUFSIZE, +// CFG_TUD_MIDI2_NUM_GROUPS, CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS, +// CFG_TUD_MIDI2_EP_NAME, CFG_TUD_MIDI2_PRODUCT_ID + +#ifdef __cplusplus +extern "C" { +#endif + +//--------------------------------------------------------------------+ +// Class Driver Configuration +//--------------------------------------------------------------------+ + +#ifndef CFG_TUD_MIDI2_TX_EPSIZE + #define CFG_TUD_MIDI2_TX_EPSIZE TUD_EPSIZE_BULK_MAX +#endif + +#ifndef CFG_TUD_MIDI2_RX_EPSIZE + #define CFG_TUD_MIDI2_RX_EPSIZE TUD_EPSIZE_BULK_MAX +#endif + +#ifndef CFG_TUD_MIDI2_TX_BUFSIZE + #define CFG_TUD_MIDI2_TX_BUFSIZE CFG_TUD_MIDI2_TX_EPSIZE +#endif + +#ifndef CFG_TUD_MIDI2_RX_BUFSIZE + #define CFG_TUD_MIDI2_RX_BUFSIZE CFG_TUD_MIDI2_RX_EPSIZE +#endif + +#ifndef CFG_TUD_MIDI2_NUM_GROUPS + #define CFG_TUD_MIDI2_NUM_GROUPS 1 +#endif + +#ifndef CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS + #define CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS 1 +#endif + +#ifndef CFG_TUD_MIDI2_EP_NAME + #define CFG_TUD_MIDI2_EP_NAME "TinyUSB MIDI 2.0" +#endif + +#ifndef CFG_TUD_MIDI2_PRODUCT_ID + #define CFG_TUD_MIDI2_PRODUCT_ID "TinyUSB-MIDI2" +#endif + +// String descriptor index for the Group Terminal Block (iBlockItem, Table 5-6). +// 0 = no string descriptor (default, spec-allowed). +#ifndef CFG_TUD_MIDI2_BLOCK_STRIDX + #define CFG_TUD_MIDI2_BLOCK_STRIDX 0 +#endif + +//--------------------------------------------------------------------+ +// MIDI Protocol Values (returned by tud_midi2_n_protocol) +//--------------------------------------------------------------------+ + +// Per USB-MIDI 2.0 spec, UMP Stream Configuration messages. +enum { + MIDI_PROTOCOL_MIDI1 = 0x01, + MIDI_PROTOCOL_MIDI2 = 0x02, +}; + +//--------------------------------------------------------------------+ +// Application Callback API (weak, optional) +//--------------------------------------------------------------------+ +void tud_midi2_rx_cb(uint8_t itf); +void tud_midi2_set_itf_cb(uint8_t itf, uint8_t alt); +bool tud_midi2_get_req_itf_cb(uint8_t rhport, const tusb_control_request_t* request); + +// Per-interface UMP Stream config (override for per-itf values). +uint8_t tud_midi2_num_groups_cb(uint8_t itf); +uint8_t tud_midi2_num_function_blocks_cb(uint8_t itf); +const char* tud_midi2_ep_name_cb(uint8_t itf); +const char* tud_midi2_product_id_cb(uint8_t itf); + +//--------------------------------------------------------------------+ +// Application API (Multiple Interfaces) +//--------------------------------------------------------------------+ + +bool tud_midi2_n_mounted(uint8_t itf); +uint32_t tud_midi2_n_available(uint8_t itf); +uint8_t tud_midi2_n_alt_setting(uint8_t itf); +bool tud_midi2_n_negotiated(uint8_t itf); +uint8_t tud_midi2_n_protocol(uint8_t itf); + +// Read up to max_words UMP words from the RX FIFO. Returns the number of +// words actually read (0 if FIFO is empty). +// +// NOTE: this function returns when max_words is reached or when the FIFO is +// empty, whichever comes first. Applications should invoke it in a loop +// until it returns 0 to guarantee the RX FIFO is fully drained per +// tud_midi2_rx_cb callback. Leaving words in the FIFO across callbacks can +// prevent subsequent bulk OUT transfers from landing. +uint32_t tud_midi2_n_ump_read(uint8_t itf, uint32_t* words, uint32_t max_words); +uint32_t tud_midi2_n_ump_write(uint8_t itf, const uint32_t* words, uint32_t count); + +uint32_t tud_midi2_n_packet_read(uint8_t itf, uint8_t packets[], uint32_t max_packets); +uint32_t tud_midi2_n_packet_write(uint8_t itf, const uint8_t packets[], uint32_t count); + +//--------------------------------------------------------------------+ +// Application API (Single Interface) +//--------------------------------------------------------------------+ +TU_ATTR_ALWAYS_INLINE static inline bool tud_midi2_mounted(void) { + return tud_midi2_n_mounted(0); +} + +TU_ATTR_ALWAYS_INLINE static inline uint32_t tud_midi2_available(void) { + return tud_midi2_n_available(0); +} + +TU_ATTR_ALWAYS_INLINE static inline uint8_t tud_midi2_alt_setting(void) { + return tud_midi2_n_alt_setting(0); +} + +TU_ATTR_ALWAYS_INLINE static inline bool tud_midi2_negotiated(void) { + return tud_midi2_n_negotiated(0); +} + +TU_ATTR_ALWAYS_INLINE static inline uint8_t tud_midi2_protocol(void) { + return tud_midi2_n_protocol(0); +} + +TU_ATTR_ALWAYS_INLINE static inline uint32_t +tud_midi2_ump_read(uint32_t* words, uint32_t max_words) { + return tud_midi2_n_ump_read(0, words, max_words); +} + +TU_ATTR_ALWAYS_INLINE static inline uint32_t +tud_midi2_ump_write(const uint32_t* words, uint32_t count) { + return tud_midi2_n_ump_write(0, words, count); +} + +TU_ATTR_ALWAYS_INLINE static inline uint32_t +tud_midi2_packet_read(uint8_t packets[], uint32_t max_packets) { + return tud_midi2_n_packet_read(0, packets, max_packets); +} + +TU_ATTR_ALWAYS_INLINE static inline uint32_t +tud_midi2_packet_write(const uint8_t packets[], uint32_t count) { + return tud_midi2_n_packet_write(0, packets, count); +} + +//--------------------------------------------------------------------+ +// Internal Class Driver API +//--------------------------------------------------------------------+ +void midi2d_init(void); +bool midi2d_deinit(void); +void midi2d_reset(uint8_t rhport); +uint16_t midi2d_open(uint8_t rhport, const tusb_desc_interface_t* itf_desc, uint16_t max_len); +bool midi2d_control_xfer_cb(uint8_t rhport, uint8_t stage, const tusb_control_request_t* request); +bool midi2d_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/class/midi/midi2_host.c b/src/class/midi/midi2_host.c new file mode 100644 index 000000000..6d8861f4b --- /dev/null +++ b/src/class/midi/midi2_host.c @@ -0,0 +1,635 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * 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_TUH_ENABLED && CFG_TUH_MIDI2) + +#include "host/usbh.h" +#include "host/usbh_pvt.h" +#include "midi2_host.h" + +#define TU_LOG_DRV(...) TU_LOG(CFG_TUH_MIDI2_LOG_LEVEL, __VA_ARGS__) + +//--------------------------------------------------------------------+ +// Weak stubs for application callbacks +//--------------------------------------------------------------------+ + +TU_ATTR_WEAK void tuh_midi2_descriptor_cb(uint8_t idx, const tuh_midi2_descriptor_cb_t *desc_cb_data) { + (void) idx; (void) desc_cb_data; +} + +TU_ATTR_WEAK void tuh_midi2_mount_cb(uint8_t idx, const tuh_midi2_mount_cb_t *mount_cb_data) { + (void) idx; (void) mount_cb_data; +} + +TU_ATTR_WEAK void tuh_midi2_rx_cb(uint8_t idx, uint32_t xferred_bytes) { + (void) idx; (void) xferred_bytes; +} + +TU_ATTR_WEAK void tuh_midi2_tx_cb(uint8_t idx, uint32_t xferred_bytes) { + (void) idx; (void) xferred_bytes; +} + +TU_ATTR_WEAK void tuh_midi2_umount_cb(uint8_t idx) { + (void) idx; +} + +//--------------------------------------------------------------------+ +// Internal structure and state +//--------------------------------------------------------------------+ + +typedef struct { + uint8_t ep_addr; + uint16_t mps; + tu_fifo_t ff; + uint8_t* ep_buf; +} midih2_tx_t; + +typedef struct { + uint8_t daddr; + uint8_t bInterfaceNumber; + + uint8_t alt_setting_current; + + uint8_t protocol_version; + uint8_t bcdMSC_hi, bcdMSC_lo; + uint8_t rx_cable_count_alt0; + uint8_t tx_cable_count_alt0; + uint8_t rx_cable_count_alt1; + uint8_t tx_cable_count_alt1; + + struct { + midih2_tx_t tx; + tu_edpt_stream_t rx; + + uint8_t rx_ff_buf[CFG_TUH_MIDI2_RX_BUFSIZE]; + uint8_t tx_ff_buf[CFG_TUH_MIDI2_TX_BUFSIZE]; + } ep_stream; + + bool mounted; +} midih2_interface_t; + +static midih2_interface_t _midi2_host[CFG_TUH_MIDI2]; + +typedef struct { + TUH_EPBUF_DEF(tx, TUH_EPSIZE_BULK_MAX); + TUH_EPBUF_DEF(rx, TUH_EPSIZE_BULK_MAX); +} midih2_epbuf_t; + +CFG_TUH_MEM_SECTION static midih2_epbuf_t _midi2_epbuf[CFG_TUH_MIDI2]; + +//--------------------------------------------------------------------+ +// Helper functions +//--------------------------------------------------------------------+ + +static inline uint8_t find_new_midi2_index(void) { + for (uint8_t idx = 0; idx < CFG_TUH_MIDI2; idx++) { + if (_midi2_host[idx].daddr == 0) { + return idx; + } + } + return TUSB_INDEX_INVALID_8; +} + +static inline uint8_t get_idx_by_ep_addr(uint8_t daddr, uint8_t ep_addr) { + for (uint8_t idx = 0; idx < CFG_TUH_MIDI2; idx++) { + const midih2_interface_t *p_midi = &_midi2_host[idx]; + if ((p_midi->daddr == daddr) && + (ep_addr == p_midi->ep_stream.rx.ep_addr || ep_addr == p_midi->ep_stream.tx.ep_addr)) { + return idx; + } + } + return TUSB_INDEX_INVALID_8; +} + +static inline bool _tuh_tx_opened(const midih2_interface_t* p_midi) { + return p_midi->ep_stream.tx.ep_addr != 0; +} + +static uint8_t _tuh_tx_byte_at(const tu_fifo_buffer_info_t* info, uint16_t offset) { + if (offset < info->linear.len) { + return info->linear.ptr[offset]; + } + offset = (uint16_t)(offset - info->linear.len); + if (offset < info->wrapped.len) { + return info->wrapped.ptr[offset]; + } + return 0; +} + +// Largest byte count containing only whole UMP packets and fitting one xfer. +static uint16_t _tuh_tx_nonseg_len_to_mps(midih2_tx_t* tx) { + tu_fifo_buffer_info_t info; + tu_fifo_get_read_info(&tx->ff, &info); + + const uint16_t available = (uint16_t)(info.linear.len + info.wrapped.len); + uint16_t bytes = 0; + + while (bytes < tx->mps) { + if ((uint16_t)(available - bytes) < 4) break; + + uint8_t mt = (uint8_t)((_tuh_tx_byte_at(&info, (uint16_t)(bytes + 3)) >> 4) & 0x0F); + uint8_t pkt_words = midi2_ump_word_count(mt); + uint16_t pkt_bytes = (uint16_t)(pkt_words * 4); + + if (pkt_bytes == 0) break; + if ((uint16_t)(available - bytes) < pkt_bytes) break; + if ((uint16_t)(bytes + pkt_bytes) > tx->mps) break; + + bytes = (uint16_t)(bytes + pkt_bytes); + } + + return bytes; +} + +// Start one OUT transfer capped at mps. Returns bytes queued, or 0 if nothing. +static uint16_t _tuh_tx_start_xfer(midih2_interface_t* p_midi) { + midih2_tx_t* tx = &p_midi->ep_stream.tx; + uint16_t ff_count = tu_fifo_count(&tx->ff); + if (ff_count == 0) return 0; + if (!usbh_edpt_claim(p_midi->daddr, tx->ep_addr)) return 0; + + uint16_t bytes; + if (p_midi->alt_setting_current == 1) { + bytes = _tuh_tx_nonseg_len_to_mps(tx); + } else { + bytes = tu_min16(tu_fifo_count(&tx->ff), tx->mps); + } + if (bytes == 0) { + usbh_edpt_release(p_midi->daddr, tx->ep_addr); + return 0; + } + + tu_fifo_read_n(&tx->ff, tx->ep_buf, bytes); + TU_ASSERT(usbh_edpt_xfer(p_midi->daddr, tx->ep_addr, tx->ep_buf, bytes), 0); + return bytes; +} + +static uint32_t _tuh_tx_ump_write(midih2_interface_t* p_midi, const uint32_t* words, uint32_t count) { + uint32_t written = 0; + while (written < count) { + uint8_t mt = (uint8_t)((words[written] >> 28) & 0x0F); + uint8_t pkt_words = midi2_ump_word_count(mt); + uint16_t pkt_bytes = (uint16_t)(pkt_words * 4); + + if (written + pkt_words > count) break; + if (tu_fifo_remaining(&p_midi->ep_stream.tx.ff) < pkt_bytes) break; + if (tu_fifo_write_n(&p_midi->ep_stream.tx.ff, &words[written], pkt_bytes) != pkt_bytes) break; + written += pkt_words; + } + + (void) _tuh_tx_start_xfer(p_midi); + return written; +} + +//--------------------------------------------------------------------+ +// Descriptor parsing +//--------------------------------------------------------------------+ + +// Parse Alt Setting 0 (MIDI 1.0) descriptors. Returns pointer past last consumed descriptor. +static const uint8_t* midih2_parse_descriptors_alt0(midih2_interface_t *p_midi, + const tusb_desc_interface_t *desc_itf, const uint8_t *desc_end) { + TU_VERIFY(AUDIO_SUBCLASS_MIDI_STREAMING == desc_itf->bInterfaceSubClass, NULL); + + p_midi->bInterfaceNumber = desc_itf->bInterfaceNumber; + + const uint8_t *p_desc = (const uint8_t *) desc_itf; + p_desc = tu_desc_next(p_desc); + + uint8_t rx_cable_count = 0; + uint8_t tx_cable_count = 0; + bool found_new_interface = false; + + while (tu_desc_in_bounds(p_desc, desc_end) && !found_new_interface) { + switch (tu_desc_type(p_desc)) { + case TUSB_DESC_INTERFACE: + found_new_interface = true; + break; + + case TUSB_DESC_ENDPOINT: { + const tusb_desc_endpoint_t *p_ep = (const tusb_desc_endpoint_t *) p_desc; + + TU_ASSERT(tuh_edpt_open(p_midi->daddr, p_ep), NULL); + if (tu_edpt_dir(p_ep->bEndpointAddress) == TUSB_DIR_IN) { + tu_edpt_stream_open(&p_midi->ep_stream.rx, p_midi->daddr, p_ep, tu_edpt_packet_size(p_ep)); + tu_edpt_stream_clear(&p_midi->ep_stream.rx); + } else { + p_midi->ep_stream.tx.ep_addr = p_ep->bEndpointAddress; + p_midi->ep_stream.tx.mps = tu_edpt_packet_size(p_ep); + tu_fifo_clear(&p_midi->ep_stream.tx.ff); + } + + p_desc = tu_desc_next(p_desc); + if (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) == TUSB_DESC_CS_ENDPOINT) { + const midi_desc_cs_endpoint_t *p_csep = (const midi_desc_cs_endpoint_t *) p_desc; + if (tu_edpt_dir(p_ep->bEndpointAddress) == TUSB_DIR_OUT) { + tx_cable_count = p_csep->bNumEmbMIDIJack; + } else { + rx_cable_count = p_csep->bNumEmbMIDIJack; + } + } + break; + } + + default: + break; + } + + if (!found_new_interface) { + p_desc = tu_desc_next(p_desc); + } + } + + p_midi->rx_cable_count_alt0 = rx_cable_count; + p_midi->tx_cable_count_alt0 = tx_cable_count; + return p_desc; +} + +// Parse Alt Setting 1 (MIDI 2.0 UMP) descriptors. Returns pointer past last consumed descriptor. +static const uint8_t* midih2_parse_descriptors_alt1(midih2_interface_t *p_midi, + const tusb_desc_interface_t *desc_itf, const uint8_t *desc_end) { + TU_VERIFY(AUDIO_SUBCLASS_MIDI_STREAMING == desc_itf->bInterfaceSubClass, NULL); + TU_VERIFY(desc_itf->bAlternateSetting == 1, NULL); + + const uint8_t *p_desc = (const uint8_t *) desc_itf; + p_desc = tu_desc_next(p_desc); + + uint8_t rx_cable_count = 0; + uint8_t tx_cable_count = 0; + bool found_new_interface = false; + + while (tu_desc_in_bounds(p_desc, desc_end) && !found_new_interface) { + switch (tu_desc_type(p_desc)) { + case TUSB_DESC_INTERFACE: + found_new_interface = true; + break; + + case TUSB_DESC_CS_INTERFACE: + if (tu_desc_subtype(p_desc) == MIDI_CS_INTERFACE_HEADER) { + // bcdMSC at offset 3-4 in CS Interface Header + const uint8_t *bcd_ptr = p_desc + 3; + p_midi->bcdMSC_lo = bcd_ptr[0]; + p_midi->bcdMSC_hi = bcd_ptr[1]; + if (p_midi->bcdMSC_hi == 0x02) { // bcdMSC 0x0200 = USB-MIDI 2.0 + p_midi->protocol_version = 1; + } + } + break; + + case TUSB_DESC_ENDPOINT: { + const tusb_desc_endpoint_t *p_ep = (const tusb_desc_endpoint_t *) p_desc; + p_desc = tu_desc_next(p_desc); + + if (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) == TUSB_DESC_CS_ENDPOINT) { + // MIDI 2.0 CS Endpoint General 2.0: bNumGrpTrmBlk at offset 3 + if (p_desc[0] >= 4 && p_desc[2] == MIDI_CS_ENDPOINT_GENERAL_2_0) { + uint8_t num_grp_trm_blk = p_desc[3]; + if (tu_edpt_dir(p_ep->bEndpointAddress) == TUSB_DIR_OUT) { + tx_cable_count = num_grp_trm_blk; + } else { + rx_cable_count = num_grp_trm_blk; + } + } + } + break; + } + + default: + break; + } + + if (!found_new_interface) { + p_desc = tu_desc_next(p_desc); + } + } + + p_midi->rx_cable_count_alt1 = rx_cable_count; + p_midi->tx_cable_count_alt1 = tx_cable_count; + return p_desc; +} + +//--------------------------------------------------------------------+ +// Auto-selection logic +//--------------------------------------------------------------------+ + +static void midih2_auto_select_alt_setting(midih2_interface_t *p_midi) { + p_midi->alt_setting_current = 0; + if (p_midi->protocol_version == 1) { + p_midi->alt_setting_current = 1; + } +} + +//--------------------------------------------------------------------+ +// Init/Deinit +//--------------------------------------------------------------------+ + +bool midih2_init(void) { + tu_memclr(&_midi2_host, sizeof(_midi2_host)); + for (int inst = 0; inst < CFG_TUH_MIDI2; inst++) { + midih2_interface_t *p_midi = &_midi2_host[inst]; + + tu_edpt_stream_init(&p_midi->ep_stream.rx, true, false, false, + p_midi->ep_stream.rx_ff_buf, CFG_TUH_MIDI2_RX_BUFSIZE, _midi2_epbuf[inst].rx); + + // TX uses raw tu_fifo + direct usbh_edpt_xfer (no FIFO wrapper) to preserve + // UMP packet boundaries across USB transfers. + midih2_tx_t* tx = &p_midi->ep_stream.tx; + (void) tu_fifo_config(&tx->ff, p_midi->ep_stream.tx_ff_buf, CFG_TUH_MIDI2_TX_BUFSIZE, false); + tx->ep_buf = _midi2_epbuf[inst].tx; + } + return true; +} + +bool midih2_deinit(void) { + for (size_t i = 0; i < CFG_TUH_MIDI2; i++) { + midih2_interface_t* p_midi = &_midi2_host[i]; + tu_edpt_stream_deinit(&p_midi->ep_stream.rx); + } + return true; +} + +//--------------------------------------------------------------------+ +// Class driver callbacks +//--------------------------------------------------------------------+ + +uint16_t midih2_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface_t *desc_itf, uint16_t max_len) { + (void) rhport; + + TU_VERIFY(TUSB_CLASS_AUDIO == desc_itf->bInterfaceClass, 0); + + // For Alt Setting 1, reuse existing slot for same device+interface + uint8_t idx = TUSB_INDEX_INVALID_8; + if (desc_itf->bAlternateSetting > 0) { + for (uint8_t i = 0; i < CFG_TUH_MIDI2; i++) { + if (_midi2_host[i].daddr == dev_addr && + _midi2_host[i].bInterfaceNumber == desc_itf->bInterfaceNumber) { + idx = i; + break; + } + } + } + if (idx == TUSB_INDEX_INVALID_8) { + idx = find_new_midi2_index(); + } + TU_VERIFY(idx < CFG_TUH_MIDI2, 0); + + midih2_interface_t *p_midi = &_midi2_host[idx]; + p_midi->daddr = dev_addr; + + const uint8_t *desc_start = (const uint8_t *) desc_itf; + const uint8_t *desc_end = desc_start + max_len; + + // Skip Audio Control interface and any non-MIDI-Streaming descriptors + // (following midi_host.c pattern from Ha Thach) + if (AUDIO_SUBCLASS_CONTROL == desc_itf->bInterfaceSubClass) { + const uint8_t *p_desc = tu_desc_next((const uint8_t *)desc_itf); + // Skip CS_INTERFACE header + TU_VERIFY(tu_desc_type(p_desc) == TUSB_DESC_CS_INTERFACE, 0); + p_desc = tu_desc_next(p_desc); + desc_itf = (const tusb_desc_interface_t *) p_desc; + // Skip until we find MIDI Streaming interface + while (tu_desc_in_bounds(p_desc, desc_end) && + (desc_itf->bDescriptorType != TUSB_DESC_INTERFACE || + (desc_itf->bInterfaceClass == TUSB_CLASS_AUDIO && + desc_itf->bInterfaceSubClass != AUDIO_SUBCLASS_MIDI_STREAMING))) { + p_desc = tu_desc_next(p_desc); + desc_itf = (const tusb_desc_interface_t *) p_desc; + } + TU_VERIFY(tu_desc_in_bounds(p_desc, desc_end), 0); + TU_VERIFY(TUSB_CLASS_AUDIO == desc_itf->bInterfaceClass, 0); + } + + TU_VERIFY(AUDIO_SUBCLASS_MIDI_STREAMING == desc_itf->bInterfaceSubClass, 0); + + TU_LOG_DRV("MIDI2 opening Interface %u Alt %u (addr = %u)\r\n", + desc_itf->bInterfaceNumber, desc_itf->bAlternateSetting, dev_addr); + + // Dispatch to appropriate parser based on Alt Setting + const uint8_t *p_end = NULL; + if (desc_itf->bAlternateSetting == 0) { + p_end = midih2_parse_descriptors_alt0(p_midi, desc_itf, desc_end); + } else if (desc_itf->bAlternateSetting == 1) { + p_end = midih2_parse_descriptors_alt1(p_midi, desc_itf, desc_end); + } + + // Return number of bytes consumed (following midi_host.c pattern) + uint16_t const parsed_len = (p_end != NULL) ? (uint16_t)(p_end - desc_start) : 0; + return parsed_len; +} + +static void midih2_set_config_complete(midih2_interface_t *p_midi, uint8_t idx) { + uint8_t dev_addr = p_midi->daddr; + + // Invoke descriptor_cb + tuh_midi2_descriptor_cb_t desc_cb = { + .protocol_version = p_midi->protocol_version, + .bcdMSC_hi = p_midi->bcdMSC_hi, + .bcdMSC_lo = p_midi->bcdMSC_lo, + .rx_cable_count = (p_midi->alt_setting_current == 0) ? + p_midi->rx_cable_count_alt0 : p_midi->rx_cable_count_alt1, + .tx_cable_count = (p_midi->alt_setting_current == 0) ? + p_midi->tx_cable_count_alt0 : p_midi->tx_cable_count_alt1, + }; + tuh_midi2_descriptor_cb(idx, &desc_cb); + + // Mark as mounted + TU_LOG_DRV("MIDI2 mounted addr = %u, alt = %u, protocol = %u\r\n", + dev_addr, p_midi->alt_setting_current, p_midi->protocol_version); + p_midi->mounted = true; + + // Invoke mount_cb + tuh_midi2_mount_cb_t mount_cb = { + .daddr = dev_addr, + .bInterfaceNumber = p_midi->bInterfaceNumber, + .protocol_version = p_midi->protocol_version, + .alt_setting_active = p_midi->alt_setting_current, + .rx_cable_count = desc_cb.rx_cable_count, + .tx_cable_count = desc_cb.tx_cable_count, + }; + tuh_midi2_mount_cb(idx, &mount_cb); + + // Prepare RX transfer + tu_edpt_stream_read_xfer(&p_midi->ep_stream.rx); + + // Signal USBH that configuration is complete + usbh_driver_set_config_complete(dev_addr, p_midi->bInterfaceNumber); +} + +static void midih2_set_interface_cb(tuh_xfer_t *xfer) { + uint8_t const dev_addr = xfer->daddr; + uint8_t const itf_num = (uint8_t) tu_le16toh(xfer->setup->wIndex); + + // Find our interface + for (uint8_t idx = 0; idx < CFG_TUH_MIDI2; idx++) { + if (_midi2_host[idx].daddr == dev_addr && _midi2_host[idx].bInterfaceNumber == itf_num) { + if (xfer->result == XFER_RESULT_SUCCESS) { + midih2_set_config_complete(&_midi2_host[idx], idx); + } else { + // SET_INTERFACE failed, fall back to alt 0 + TU_LOG_DRV("MIDI2 SET_INTERFACE failed, falling back to alt 0\r\n"); + _midi2_host[idx].alt_setting_current = 0; + midih2_set_config_complete(&_midi2_host[idx], idx); + } + return; + } + } +} + +bool midih2_set_config(uint8_t dev_addr, uint8_t itf_num) { + uint8_t idx = 0; + for (idx = 0; idx < CFG_TUH_MIDI2; idx++) { + if (_midi2_host[idx].daddr == dev_addr && _midi2_host[idx].bInterfaceNumber == itf_num) { + break; + } + } + + if (idx >= CFG_TUH_MIDI2) { + // Not our interface (e.g. Audio Control) - pass through to next + usbh_driver_set_config_complete(dev_addr, itf_num); + return true; + } + + midih2_interface_t *p_midi = &_midi2_host[idx]; + + // Auto-select alt setting + midih2_auto_select_alt_setting(p_midi); + + // If MIDI 2.0 detected, issue SET_INTERFACE to activate Alt Setting 1 + if (p_midi->alt_setting_current == 1) { + TU_LOG_DRV("MIDI2 requesting SET_INTERFACE alt 1 for itf %u\r\n", itf_num); + TU_ASSERT(tuh_interface_set(dev_addr, itf_num, 1, midih2_set_interface_cb, 0)); + } else { + // MIDI 1.0 only, complete immediately + midih2_set_config_complete(p_midi, idx); + } + + return true; +} + +void midih2_close(uint8_t dev_addr) { + for (uint8_t idx = 0; idx < CFG_TUH_MIDI2; idx++) { + midih2_interface_t *p_midi = &_midi2_host[idx]; + if (p_midi->daddr == dev_addr) { + TU_LOG_DRV(" MIDI2 close addr = %u index = %u\r\n", dev_addr, idx); + tu_edpt_stream_close(&p_midi->ep_stream.rx); + tu_fifo_clear(&p_midi->ep_stream.tx.ff); + p_midi->ep_stream.tx.ep_addr = 0; + tuh_midi2_umount_cb(idx); + tu_memclr(p_midi, sizeof(midih2_interface_t)); + } + } +} + +bool midih2_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { + uint8_t idx = get_idx_by_ep_addr(dev_addr, ep_addr); + TU_VERIFY(idx < CFG_TUH_MIDI2); + + midih2_interface_t *p_midi = &_midi2_host[idx]; + midih2_tx_t* ep_tx = &p_midi->ep_stream.tx; + + if (ep_addr == p_midi->ep_stream.rx.ep_addr) { + if (result == XFER_RESULT_SUCCESS && xferred_bytes > 0) { + tu_edpt_stream_read_xfer_complete(&p_midi->ep_stream.rx, xferred_bytes); + tuh_midi2_rx_cb(idx, xferred_bytes); + } + tu_edpt_stream_read_xfer(&p_midi->ep_stream.rx); + } else if (ep_addr == ep_tx->ep_addr) { + tuh_midi2_tx_cb(idx, xferred_bytes); + if (result == XFER_RESULT_SUCCESS) { + uint16_t queued = _tuh_tx_start_xfer(p_midi); + // Send ZLP if no more data is queued but the last transfer was exactly mps + if (queued == 0 && tu_fifo_count(&ep_tx->ff) == 0 && xferred_bytes > 0 && + (0 == (xferred_bytes & (ep_tx->mps - 1)))) { + if (usbh_edpt_claim(dev_addr, ep_tx->ep_addr)) { + usbh_edpt_xfer(dev_addr, ep_tx->ep_addr, NULL, 0); + } + } + } + } + + return true; +} + +//--------------------------------------------------------------------+ +// Public API +//--------------------------------------------------------------------+ + +bool tuh_midi2_mounted(uint8_t idx) { + TU_VERIFY(idx < CFG_TUH_MIDI2); + return _midi2_host[idx].mounted; +} + +uint8_t tuh_midi2_get_protocol_version(uint8_t idx) { + TU_VERIFY(idx < CFG_TUH_MIDI2); + return _midi2_host[idx].protocol_version; +} + +uint8_t tuh_midi2_get_alt_setting_active(uint8_t idx) { + TU_VERIFY(idx < CFG_TUH_MIDI2); + return _midi2_host[idx].alt_setting_current; +} + +uint8_t tuh_midi2_get_cable_count(uint8_t idx) { + TU_VERIFY(idx < CFG_TUH_MIDI2); + return (_midi2_host[idx].alt_setting_current == 0) ? + _midi2_host[idx].rx_cable_count_alt0 : _midi2_host[idx].rx_cable_count_alt1; +} + +uint32_t tuh_midi2_ump_read(uint8_t idx, uint32_t* words, uint32_t max_words) { + TU_VERIFY(idx < CFG_TUH_MIDI2 && words && max_words); + + midih2_interface_t *p_midi = &_midi2_host[idx]; + tu_edpt_stream_t *ep_rx = &p_midi->ep_stream.rx; + + uint32_t n_words = 0; + for (uint32_t i = 0; i < max_words; i++) { + if (tu_edpt_stream_read_available(ep_rx) >= 4) { + tu_edpt_stream_read(ep_rx, (uint8_t *) &words[i], 4); + n_words++; + } else { + break; + } + } + + return n_words; +} + +uint32_t tuh_midi2_ump_write(uint8_t idx, const uint32_t* words, uint32_t count) { + TU_VERIFY(idx < CFG_TUH_MIDI2 && words && count); + + midih2_interface_t *p_midi = &_midi2_host[idx]; + TU_VERIFY(_tuh_tx_opened(p_midi), 0); + + return _tuh_tx_ump_write(p_midi, words, count); +} + +uint32_t tuh_midi2_write_flush(uint8_t idx) { + TU_VERIFY(idx < CFG_TUH_MIDI2); + return _tuh_tx_start_xfer(&_midi2_host[idx]); +} + +#endif // CFG_TUH_ENABLED && CFG_TUH_MIDI2 diff --git a/src/class/midi/midi2_host.h b/src/class/midi/midi2_host.h new file mode 100644 index 000000000..47039eb85 --- /dev/null +++ b/src/class/midi/midi2_host.h @@ -0,0 +1,107 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * 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 TUSB_MIDI2_HOST_H_ +#define TUSB_MIDI2_HOST_H_ + +#include "class/audio/audio.h" +#include "midi.h" + +#ifdef __cplusplus +extern "C" { +#endif + +//--------------------------------------------------------------------+ +// Callback Type Definitions +//--------------------------------------------------------------------+ + +typedef struct { + uint8_t protocol_version; // 0 = MIDI 1.0 only, 1 = MIDI 2.0 + uint8_t bcdMSC_hi, bcdMSC_lo; // MIDI version from descriptor + uint8_t rx_cable_count; // For both alt settings (same for Alt 0 and Alt 1) + uint8_t tx_cable_count; +} tuh_midi2_descriptor_cb_t; + +typedef struct { + uint8_t daddr; + uint8_t bInterfaceNumber; + uint8_t protocol_version; // 0 = MIDI 1.0, 1 = MIDI 2.0 + uint8_t alt_setting_active; // 0 or 1 + uint8_t rx_cable_count; + uint8_t tx_cable_count; +} tuh_midi2_mount_cb_t; + +//--------------------------------------------------------------------+ +// Application Callback API (weak, optional) +//--------------------------------------------------------------------+ + +void tuh_midi2_descriptor_cb(uint8_t idx, const tuh_midi2_descriptor_cb_t *desc_cb_data); +void tuh_midi2_mount_cb(uint8_t idx, const tuh_midi2_mount_cb_t *mount_cb_data); +void tuh_midi2_rx_cb(uint8_t idx, uint32_t xferred_bytes); +void tuh_midi2_tx_cb(uint8_t idx, uint32_t xferred_bytes); +void tuh_midi2_umount_cb(uint8_t idx); + +//--------------------------------------------------------------------+ +// Application API - Query +//--------------------------------------------------------------------+ + +bool tuh_midi2_mounted(uint8_t idx); +uint8_t tuh_midi2_get_protocol_version(uint8_t idx); +uint8_t tuh_midi2_get_alt_setting_active(uint8_t idx); +uint8_t tuh_midi2_get_cable_count(uint8_t idx); + +//--------------------------------------------------------------------+ +// Application API - I/O +//--------------------------------------------------------------------+ + +// Read up to max_words UMP words from the RX FIFO. Returns the number of +// words actually read (0 if FIFO is empty). +// +// NOTE: this function returns when max_words is reached or when the FIFO is +// empty, whichever comes first. Applications should invoke it in a loop +// until it returns 0 to guarantee the RX FIFO is fully drained per +// tuh_midi2_rx_cb callback. Leaving words in the FIFO across callbacks can +// prevent subsequent bulk IN transfers from landing. +uint32_t tuh_midi2_ump_read(uint8_t idx, uint32_t* words, uint32_t max_words); +uint32_t tuh_midi2_ump_write(uint8_t idx, const uint32_t* words, uint32_t count); +uint32_t tuh_midi2_write_flush(uint8_t idx); + +//--------------------------------------------------------------------+ +// Internal Class Driver API +//--------------------------------------------------------------------+ + +bool midih2_init(void); +bool midih2_deinit(void); +bool midih2_set_config(uint8_t dev_addr, uint8_t itf_num); +void midih2_close(uint8_t dev_addr); +uint16_t midih2_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface_t *desc_itf, uint16_t max_len); +bool midih2_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes); + +#ifdef __cplusplus +} +#endif + +#endif 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..413ac4850 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 @@ -616,10 +616,6 @@ #define CFG_TUH_WCH_USBIP_USBFS 1 #endif - #define TUP_USBIP_FSDEV - #define TUP_USBIP_FSDEV_CH32 - #define CFG_TUSB_FSDEV_PMA_SIZE 512u - // default to FSDEV for device #if !defined(CFG_TUD_WCH_USBIP_USBFS) #define CFG_TUD_WCH_USBIP_USBFS 0 @@ -629,6 +625,12 @@ #define CFG_TUD_WCH_USBIP_FSDEV (CFG_TUD_WCH_USBIP_USBFS ? 0 : 1) #endif + #if CFG_TUD_WCH_USBIP_FSDEV + #define TUP_USBIP_FSDEV + #define TUP_USBIP_FSDEV_CH32 + #define CFG_TUSB_FSDEV_PMA_SIZE 512u + #endif + #define TUP_DCD_ENDPOINT_MAX 8 #elif TU_CHECK_MCU(OPT_MCU_CH32V307) @@ -659,7 +661,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 +745,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..55ad330c1 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 //--------------------------------------------------------------------+ @@ -237,6 +254,20 @@ static const usbd_class_driver_t _usbd_driver[] = { }, #endif + #if CFG_TUD_MIDI2 + { + .name = DRIVER_NAME("MIDI2"), + .init = midi2d_init, + .deinit = midi2d_deinit, + .open = midi2d_open, + .reset = midi2d_reset, + .control_xfer_cb = midi2d_control_xfer_cb, + .xfer_cb = midi2d_xfer_cb, + .xfer_isr = NULL, + .sof = NULL + }, + #endif + #if CFG_TUD_VENDOR { .name = DRIVER_NAME("VENDOR"), @@ -405,7 +436,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 +451,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 +485,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 +624,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 +659,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 +725,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 +739,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 +760,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 +821,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 +1120,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 +1156,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 +1214,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 +1296,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 +1304,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 +1454,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 +1552,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 +1560,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 +1580,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 +1611,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 +1643,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 +1655,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 +1667,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 +1676,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 +1696,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 +1741,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..abce5a887 100644 --- a/src/device/usbd.h +++ b/src/device/usbd.h @@ -425,6 +425,43 @@ bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_requ TUD_MIDI_JACKID_OUT_EMB(1) //--------------------------------------------------------------------+ +// MIDI 2.0 Descriptor Templates (USB-MIDI 2.0) +//--------------------------------------------------------------------+ + +// Alt Setting 1: MS Interface + MS Header (bcdMSC=0x0200) +// Per USB-MIDI 2.0 Table 5-2: wTotalLength in the MS Header is not used in 2.0 +// and shall be set to match bLength (= 0x0007) for conformity with USB-MIDI 1.0. +#define TUD_MIDI2_DESC_ALT1_HEAD_LEN (9 + 7) +#define TUD_MIDI2_DESC_ALT1_HEAD(_itfnum, _stridx) \ + /* MIDI Streaming Interface, Alt Setting 1 */\ + 9, TUSB_DESC_INTERFACE, (uint8_t)((_itfnum) + 1), 1, 2, TUSB_CLASS_AUDIO, AUDIO_SUBCLASS_MIDI_STREAMING, AUDIO_FUNC_PROTOCOL_CODE_UNDEF, 0,\ + /* MS Header (MIDI 2.0): wTotalLength = bLength per spec */\ + 7, TUSB_DESC_CS_INTERFACE, MIDI_CS_INTERFACE_HEADER, U16_TO_U8S_LE(0x0200), U16_TO_U8S_LE(0x0007) + +// Alt Setting 1: Standard USB Endpoint (7 bytes) + CS Endpoint General 2.0 +#define TUD_MIDI2_DESC_ALT1_EP_LEN(_numgtbs) (7 + 4 + (_numgtbs)) +#define TUD_MIDI2_DESC_ALT1_EP(_ep, _epsize, _numgtbs, ...) \ + 7, TUSB_DESC_ENDPOINT, _ep, TUSB_XFER_BULK, U16_TO_U8S_LE(_epsize), 0, \ + (uint8_t)(4 + (_numgtbs)), TUSB_DESC_CS_ENDPOINT, MIDI_CS_ENDPOINT_GENERAL_2_0, _numgtbs, ## __VA_ARGS__ + +// Total length: Alt 0 (MIDI 1.0) + Alt 1 (UMP) +#define TUD_MIDI2_DESC_LEN (TUD_MIDI_DESC_LEN + TUD_MIDI2_DESC_ALT1_HEAD_LEN + TUD_MIDI2_DESC_ALT1_EP_LEN(1) * 2) + +// Complete MIDI 2.0 descriptor with both alternate settings (single cable/GTB) +#define TUD_MIDI2_DESCRIPTOR(_itfnum, _stridx, _epout, _epin, _epsize) \ + /* Alt Setting 0 (MIDI 1.0) */\ + TUD_MIDI_DESC_HEAD(_itfnum, _stridx, 1),\ + TUD_MIDI_DESC_JACK_DESC(1, 0),\ + TUD_MIDI_DESC_EP(_epout, _epsize, 1),\ + TUD_MIDI_JACKID_IN_EMB(1),\ + TUD_MIDI_DESC_EP(_epin, _epsize, 1),\ + TUD_MIDI_JACKID_OUT_EMB(1),\ + /* Alt Setting 1 (UMP) */\ + TUD_MIDI2_DESC_ALT1_HEAD(_itfnum, _stridx),\ + TUD_MIDI2_DESC_ALT1_EP(_epout, _epsize, 1, 1 /* bAssoGrpTrmBlkID */),\ + TUD_MIDI2_DESC_ALT1_EP(_epin, _epsize, 1, 1 /* bAssoGrpTrmBlkID */) + +//--------------------------------------------------------------------+ // Audio Descriptor Templates //--------------------------------------------------------------------+ @@ -1026,23 +1063,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..2e3c93c5e 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 @@ -278,6 +278,18 @@ static usbh_class_driver_t const usbh_class_drivers[] = { }, #endif + #if CFG_TUH_MIDI2 + { + .name = DRIVER_NAME("MIDI2"), + .init = midih2_init, + .deinit = midih2_deinit, + .open = midih2_open, + .set_config = midih2_set_config, + .xfer_cb = midih2_xfer_cb, + .close = midih2_close + }, + #endif + #if CFG_TUH_HUB { .name = DRIVER_NAME("HUB"), @@ -744,8 +756,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 +1028,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 +1122,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 +1142,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 +1189,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/raspberrypi/rp2040/hcd_rp2040.c b/src/portable/raspberrypi/rp2040/hcd_rp2040.c index 02a4e055e..064834efb 100644 --- a/src/portable/raspberrypi/rp2040/hcd_rp2040.c +++ b/src/portable/raspberrypi/rp2040/hcd_rp2040.c @@ -617,10 +617,16 @@ bool hcd_edpt_xfer(uint8_t rhport, uint8_t dev_addr, uint8_t ep_addr, uint8_t *b io_rw_32 *buf_reg = dpram_int_ep_buffer_ctrl(ep->interrupt_num); rp2usb_xfer_start(ep, ep_reg, buf_reg, buffer, NULL, buflen); } else { - // Control endpoint can change direction 0x00 <-> 0x80 when changing stages - if (ep_addr != ep->ep_addr) { + // Control transfer data and status stages always start with DATA1, regardless of + // whether the direction changed since the previous stage. SET_REPORT (and any other + // host-to-device class request with an OUT data stage) keeps the same direction + // across SETUP -> DATA, so we cannot key off "direction changed" -- we must reset + // next_pid every time hcd_edpt_xfer is invoked on ep 0. Without this, the data stage + // of SET_REPORT goes out as DATA0 because ep->next_pid is still 0 from hcd_edpt_open(), + // which strict devices treat as a protocol violation and disconnect. + if (tu_edpt_number(ep_addr) == 0) { ep->ep_addr = ep_addr; - ep->next_pid = 1; // data and status stage start with DATA1 + ep->next_pid = 1; } // If EPX is busy with another transfer, mark as pending diff --git a/src/portable/synopsys/dwc2/dcd_dwc2.c b/src/portable/synopsys/dwc2/dcd_dwc2.c index 30e24a9ad..1d0ef45d2 100644 --- a/src/portable/synopsys/dwc2/dcd_dwc2.c +++ b/src/portable/synopsys/dwc2/dcd_dwc2.c @@ -1136,8 +1136,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/portable/wch/dcd_ch32_usbfs.c b/src/portable/wch/dcd_ch32_usbfs.c index 5cd25e33e..af0f17785 100644 --- a/src/portable/wch/dcd_ch32_usbfs.c +++ b/src/portable/wch/dcd_ch32_usbfs.c @@ -29,29 +29,29 @@ #if CFG_TUD_ENABLED && defined(TUP_USBIP_WCH_USBFS) && CFG_TUD_WCH_USBIP_USBFS -#include "device/dcd.h" -#include "ch32_usbfs_reg.h" + #include "device/dcd.h" + #include "ch32_usbfs_reg.h" -/* private defines */ -#define EP_MAX (8) + /* private defines */ + #define EP_MAX (8) -#define EP_DMA(ep) ((&USBOTG_FS->UEP0_DMA)[ep]) -#define EP_TX_LEN(ep) ((&USBOTG_FS->UEP0_TX_LEN)[2 * ep]) -#define EP_TX_CTRL(ep) ((&USBOTG_FS->UEP0_TX_CTRL)[4 * ep]) -#define EP_RX_CTRL(ep) ((&USBOTG_FS->UEP0_RX_CTRL)[4 * ep]) + #define EP_DMA(ep) ((&USBOTG_FS->UEP0_DMA)[ep]) + #define EP_TX_LEN(ep) ((&USBOTG_FS->UEP0_TX_LEN)[2 * ep]) + #define EP_TX_CTRL(ep) ((&USBOTG_FS->UEP0_TX_CTRL)[4 * ep]) + #define EP_RX_CTRL(ep) ((&USBOTG_FS->UEP0_RX_CTRL)[4 * ep]) /* private data */ struct usb_xfer { - bool valid; - uint8_t* buffer; - size_t len; - size_t processed_len; - size_t max_size; + bool valid; + uint8_t *buffer; + size_t len; + size_t processed_len; + size_t max_size; }; static struct { - bool ep0_tog; - bool isochronous[EP_MAX]; + bool ep0_tog; + bool isochronous[EP_MAX]; struct usb_xfer xfer[EP_MAX][2]; TU_ATTR_ALIGNED(4) uint8_t buffer[EP_MAX][2][64]; TU_ATTR_ALIGNED(4) struct { @@ -64,7 +64,7 @@ static struct { /* private helpers */ static void update_in(uint8_t rhport, uint8_t ep, bool force) { - struct usb_xfer* xfer = &data.xfer[ep][TUSB_DIR_IN]; + struct usb_xfer *xfer = &data.xfer[ep][TUSB_DIR_IN]; if (xfer->valid) { if (force || xfer->len) { size_t len = TU_MIN(xfer->max_size, xfer->len); @@ -82,7 +82,7 @@ static void update_in(uint8_t rhport, uint8_t ep, bool force) { EP_TX_LEN(ep) = len; if (ep == 0) { EP_TX_CTRL(0) = USBFS_EP_T_RES_ACK | (data.ep0_tog ? USBFS_EP_T_TOG : 0); - data.ep0_tog = !data.ep0_tog; + data.ep0_tog = !data.ep0_tog; } else if (data.isochronous[ep]) { EP_TX_CTRL(ep) = (EP_TX_CTRL(ep) & ~(USBFS_EP_T_RES_MASK)) | USBFS_EP_T_RES_NYET; } else { @@ -90,16 +90,18 @@ static void update_in(uint8_t rhport, uint8_t ep, bool force) { } } else { xfer->valid = false; - EP_TX_CTRL(ep) = (EP_TX_CTRL(ep) & ~(USBFS_EP_T_RES_MASK)) | USBFS_EP_T_RES_NAK; - dcd_event_xfer_complete( - rhport, ep | TUSB_DIR_IN_MASK, xfer->processed_len, - XFER_RESULT_SUCCESS, true); + if (ep == 0) { + EP_TX_CTRL(0) = USBFS_EP_T_RES_NAK | (data.ep0_tog ? USBFS_EP_T_TOG : 0); + } else if (!data.isochronous[ep]) { + EP_TX_CTRL(ep) = (EP_TX_CTRL(ep) & ~(USBFS_EP_T_RES_MASK)) | USBFS_EP_T_RES_NAK; + } + dcd_event_xfer_complete(rhport, ep | TUSB_DIR_IN_MASK, xfer->processed_len, XFER_RESULT_SUCCESS, true); } } } static void update_out(uint8_t rhport, uint8_t ep, size_t rx_len) { - struct usb_xfer* xfer = &data.xfer[ep][TUSB_DIR_OUT]; + struct usb_xfer *xfer = &data.xfer[ep][TUSB_DIR_OUT]; if (xfer->valid) { size_t len = TU_MIN(xfer->max_size, TU_MIN(xfer->len, rx_len)); if (ep == 3) { @@ -117,25 +119,39 @@ static void update_out(uint8_t rhport, uint8_t ep, size_t rx_len) { } if (ep == 0) { - EP_RX_CTRL(0) = USBFS_EP_R_RES_ACK; + EP_RX_CTRL(0) = USBFS_EP_R_RES_NAK; + } else { + uint8_t rx_res = + data.isochronous[ep] ? USBFS_EP_R_RES_NYET : (xfer->valid ? USBFS_EP_R_RES_ACK : USBFS_EP_R_RES_NAK); + EP_RX_CTRL(ep) = (EP_RX_CTRL(ep) & ~USBFS_EP_R_RES_MASK) | rx_res; } } } +static void reset_ep_ctrls(void) { + for (uint8_t ep = 1; ep < EP_MAX; ep++) { + EP_DMA(ep) = (uint32_t)&data.buffer[ep][0]; + EP_TX_LEN(ep) = 0; + EP_TX_CTRL(ep) = USBFS_EP_T_AUTO_TOG | USBFS_EP_T_RES_NYET; + EP_RX_CTRL(ep) = USBFS_EP_R_AUTO_TOG | USBFS_EP_R_RES_NYET; + } + EP_DMA(3) = (uint32_t)&data.ep3_buffer.out[0]; +} + /* public functions */ -bool dcd_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) { - (void) rh_init; +bool dcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) { + (void)rh_init; // init registers USBOTG_FS->BASE_CTRL = USBFS_CTRL_SYS_CTRL | USBFS_CTRL_INT_BUSY | USBFS_CTRL_DMA_EN; USBOTG_FS->UDEV_CTRL = USBFS_UDEV_CTRL_PD_DIS | USBFS_UDEV_CTRL_PORT_EN; - USBOTG_FS->DEV_ADDR = 0x00; + USBOTG_FS->DEV_ADDR = 0x00; USBOTG_FS->INT_FG = 0xFF; USBOTG_FS->INT_EN = USBFS_INT_EN_BUS_RST | USBFS_INT_EN_TRANSFER | USBFS_INT_EN_SUSPEND; // setup endpoint 0 - EP_DMA(0) = (uint32_t) &data.buffer[0][0]; - EP_TX_LEN(0) = 0; + EP_DMA(0) = (uint32_t)&data.buffer[0][0]; + EP_TX_LEN(0) = 0; EP_TX_CTRL(0) = USBFS_EP_T_RES_NAK; EP_RX_CTRL(0) = USBFS_EP_R_RES_ACK; @@ -143,15 +159,9 @@ bool dcd_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) { USBOTG_FS->UEP4_1_MOD = 0xCC; USBOTG_FS->UEP2_3_MOD = 0xCC; USBOTG_FS->UEP5_6_MOD = 0xCC; - USBOTG_FS->UEP7_MOD = 0x0C; + USBOTG_FS->UEP7_MOD = 0x0C; - for (uint8_t ep = 1; ep < EP_MAX; ep++) { - EP_DMA(ep) = (uint32_t) &data.buffer[ep][0]; - EP_TX_LEN(ep) = 0; - EP_TX_CTRL(ep) = USBFS_EP_T_AUTO_TOG | USBFS_EP_T_RES_NAK; - EP_RX_CTRL(ep) = USBFS_EP_R_AUTO_TOG | USBFS_EP_R_RES_NAK; - } - EP_DMA(3) = (uint32_t) &data.ep3_buffer.out[0]; + reset_ep_ctrls(); dcd_connect(rhport); @@ -159,15 +169,15 @@ bool dcd_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) { } void dcd_int_handler(uint8_t rhport) { - (void) rhport; + (void)rhport; uint8_t status = USBOTG_FS->INT_FG; if (status & USBFS_INT_FG_TRANSFER) { - uint8_t ep = USBFS_INT_ST_MASK_UIS_ENDP(USBOTG_FS->INT_ST); - uint8_t token = USBFS_INT_ST_MASK_UIS_TOKEN(USBOTG_FS->INT_ST); + uint8_t ep = USBFS_INT_ST_MASK_UIS_ENDP(USBOTG_FS->INT_ST); + uint8_t token = USBFS_INT_ST_MASK_UIS_TOKEN(USBOTG_FS->INT_ST); + uint16_t rx_len = USBOTG_FS->RX_LEN; switch (token) { case PID_OUT: { - uint16_t rx_len = USBOTG_FS->RX_LEN; update_out(rhport, ep, rx_len); break; } @@ -179,24 +189,30 @@ void dcd_int_handler(uint8_t rhport) { case PID_SETUP: // setup clears stall EP_TX_CTRL(0) = USBFS_EP_T_RES_NAK; - EP_RX_CTRL(0) = USBFS_EP_R_RES_ACK; + data.ep0_tog = true; + + const tusb_control_request_t *setup = (const tusb_control_request_t *)&data.buffer[0][TUSB_DIR_OUT][0]; + EP_RX_CTRL(0) = (setup->wLength == 0) ? USBFS_EP_R_RES_ACK : USBFS_EP_R_RES_NAK; - data.ep0_tog = true; dcd_event_setup_received(rhport, &data.buffer[0][TUSB_DIR_OUT][0], true); break; } USBOTG_FS->INT_FG = USBFS_INT_FG_TRANSFER; } else if (status & USBFS_INT_FG_BUS_RST) { - data.ep0_tog = true; + data.ep0_tog = true; data.xfer[0][TUSB_DIR_OUT].max_size = 64; - data.xfer[0][TUSB_DIR_IN].max_size = 64; + data.xfer[0][TUSB_DIR_IN].max_size = 64; - //dcd_event_bus_reset(rhport, (USBOTG_FS->BASE_CTRL & USBFS_CTRL_LOW_SPEED) ? TUSB_SPEED_LOW : TUSB_SPEED_FULL, true); - dcd_event_bus_reset(rhport, (USBOTG_FS->UDEV_CTRL & USBFS_UDEV_CTRL_LOW_SPEED) ? TUSB_SPEED_LOW : TUSB_SPEED_FULL, true); + // dcd_event_bus_reset(rhport, (USBOTG_FS->BASE_CTRL & USBFS_CTRL_LOW_SPEED) ? TUSB_SPEED_LOW : TUSB_SPEED_FULL, + // true); + dcd_event_bus_reset(rhport, (USBOTG_FS->UDEV_CTRL & USBFS_UDEV_CTRL_LOW_SPEED) ? TUSB_SPEED_LOW : TUSB_SPEED_FULL, + true); USBOTG_FS->DEV_ADDR = 0x00; - EP_RX_CTRL(0) = USBFS_EP_R_RES_ACK; + EP_RX_CTRL(0) = USBFS_EP_R_RES_ACK; + + reset_ep_ctrls(); USBOTG_FS->INT_FG = USBFS_INT_FG_BUS_RST; } else if (status & USBFS_INT_FG_SUSPEND) { @@ -207,71 +223,62 @@ void dcd_int_handler(uint8_t rhport) { } void dcd_int_enable(uint8_t rhport) { - (void) rhport; + (void)rhport; NVIC_EnableIRQ(USBHD_IRQn); } void dcd_int_disable(uint8_t rhport) { - (void) rhport; + (void)rhport; NVIC_DisableIRQ(USBHD_IRQn); } void dcd_set_address(uint8_t rhport, uint8_t dev_addr) { - (void) dev_addr; + (void)dev_addr; dcd_edpt_xfer(rhport, 0x80, NULL, 0, false); // zlp status response } void dcd_remote_wakeup(uint8_t rhport) { - (void) rhport; + (void)rhport; // TODO optional } void dcd_connect(uint8_t rhport) { - (void) rhport; + (void)rhport; USBOTG_FS->BASE_CTRL |= USBFS_CTRL_DEV_PUEN; } void dcd_disconnect(uint8_t rhport) { - (void) rhport; + (void)rhport; USBOTG_FS->BASE_CTRL &= ~USBFS_CTRL_DEV_PUEN; } void dcd_sof_enable(uint8_t rhport, bool en) { - (void) rhport; - (void) en; + (void)rhport; + (void)en; // TODO implement later } -void dcd_edpt0_status_complete(uint8_t rhport, tusb_control_request_t const* request) { - (void) rhport; +void dcd_edpt0_status_complete(uint8_t rhport, const tusb_control_request_t *request) { + (void)rhport; if (request->bmRequestType_bit.recipient == TUSB_REQ_RCPT_DEVICE && - request->bmRequestType_bit.type == TUSB_REQ_TYPE_STANDARD && - request->bRequest == TUSB_REQ_SET_ADDRESS) { - USBOTG_FS->DEV_ADDR = (uint8_t) request->wValue; + request->bmRequestType_bit.type == TUSB_REQ_TYPE_STANDARD && request->bRequest == TUSB_REQ_SET_ADDRESS) { + USBOTG_FS->DEV_ADDR = (uint8_t)request->wValue; } - EP_TX_CTRL(0) = USBFS_EP_T_RES_NAK; - EP_RX_CTRL(0) = USBFS_EP_R_RES_ACK; } -bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const* desc_ep) { - (void) rhport; - uint8_t ep = tu_edpt_number(desc_ep->bEndpointAddress); +bool dcd_edpt_open(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) { + (void)rhport; + uint8_t ep = tu_edpt_number(desc_ep->bEndpointAddress); uint8_t dir = tu_edpt_dir(desc_ep->bEndpointAddress); TU_ASSERT(ep < EP_MAX); - data.isochronous[ep] = desc_ep->bmAttributes.xfer == TUSB_XFER_ISOCHRONOUS; data.xfer[ep][dir].max_size = tu_edpt_packet_size(desc_ep); if (ep != 0) { if (dir == TUSB_DIR_OUT) { - if (data.isochronous[ep]) { - EP_RX_CTRL(ep) = USBFS_EP_R_AUTO_TOG | USBFS_EP_R_RES_NYET; - } else { - EP_RX_CTRL(ep) = USBFS_EP_R_AUTO_TOG | USBFS_EP_R_RES_ACK; - } + EP_RX_CTRL(ep) = USBFS_EP_R_AUTO_TOG | USBFS_EP_T_RES_NAK; } else { - EP_TX_LEN(ep) = 0; EP_TX_CTRL(ep) = USBFS_EP_T_AUTO_TOG | USBFS_EP_T_RES_NAK; } } @@ -279,52 +286,60 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const* desc_ep) { } void dcd_edpt_close_all(uint8_t rhport) { - (void) rhport; + (void)rhport; // TODO optional } bool dcd_edpt_iso_alloc(uint8_t rhport, uint8_t ep_addr, uint16_t largest_packet_size) { - (void) rhport; - (void) ep_addr; + (void)rhport; + (void)ep_addr; (void)largest_packet_size; - return false; + uint8_t ep = tu_edpt_number(ep_addr); + uint8_t dir = tu_edpt_dir(ep_addr); + + data.isochronous[ep] = true; + data.xfer[ep][dir].max_size = largest_packet_size; + return true; } bool dcd_edpt_iso_activate(uint8_t rhport, const tusb_desc_endpoint_t *desc_ep) { (void)rhport; (void)desc_ep; - return false; + return true; } -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; - uint8_t ep = tu_edpt_number(ep_addr); +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; + uint8_t ep = tu_edpt_number(ep_addr); uint8_t dir = tu_edpt_dir(ep_addr); - struct usb_xfer* xfer = &data.xfer[ep][dir]; + struct usb_xfer *xfer = &data.xfer[ep][dir]; dcd_int_disable(rhport); - xfer->valid = true; - xfer->buffer = buffer; - xfer->len = total_bytes; + xfer->valid = true; + xfer->buffer = buffer; + xfer->len = total_bytes; xfer->processed_len = 0; dcd_int_enable(rhport); if (dir == TUSB_DIR_IN) { update_in(rhport, ep, true); + } else { + uint8_t rx_res = data.isochronous[ep] ? USBFS_EP_R_RES_NYET : USBFS_EP_R_RES_ACK; + EP_RX_CTRL(ep) = (EP_RX_CTRL(ep) & ~USBFS_EP_R_RES_MASK) | rx_res; } return true; } void dcd_edpt_stall(uint8_t rhport, uint8_t ep_addr) { - (void) rhport; - uint8_t ep = tu_edpt_number(ep_addr); + (void)rhport; + uint8_t ep = tu_edpt_number(ep_addr); uint8_t dir = tu_edpt_dir(ep_addr); if (ep == 0) { if (dir == TUSB_DIR_OUT) { EP_RX_CTRL(0) = USBFS_EP_R_RES_STALL; } else { - EP_TX_LEN(0) = 0; + EP_TX_LEN(0) = 0; EP_TX_CTRL(0) = USBFS_EP_T_RES_STALL; } } else { @@ -337,8 +352,8 @@ void dcd_edpt_stall(uint8_t rhport, uint8_t ep_addr) { } void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) { - (void) rhport; - uint8_t ep = tu_edpt_number(ep_addr); + (void)rhport; + uint8_t ep = tu_edpt_number(ep_addr); uint8_t dir = tu_edpt_dir(ep_addr); if (ep == 0) { if (dir == TUSB_DIR_OUT) { @@ -346,9 +361,9 @@ void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) { } } else { if (dir == TUSB_DIR_OUT) { - EP_RX_CTRL(ep) = (EP_RX_CTRL(ep) & ~(USBFS_EP_R_RES_MASK | USBFS_EP_R_TOG)) | USBFS_EP_R_RES_ACK; + EP_RX_CTRL(ep) = USBFS_EP_R_AUTO_TOG | USBFS_EP_R_RES_NAK; } else { - EP_TX_CTRL(ep) = (EP_TX_CTRL(ep) & ~(USBFS_EP_T_RES_MASK | USBFS_EP_T_TOG)) | USBFS_EP_T_RES_NAK; + EP_TX_CTRL(ep) = USBFS_EP_T_AUTO_TOG | USBFS_EP_T_RES_NAK; } } } diff --git a/src/portable/wch/dcd_ch32_usbhs.c b/src/portable/wch/dcd_ch32_usbhs.c index 11734de37..a6dd5bb79 100644 --- a/src/portable/wch/dcd_ch32_usbhs.c +++ b/src/portable/wch/dcd_ch32_usbhs.c @@ -24,7 +24,6 @@ * * This file is part of the TinyUSB stack. */ - #include "tusb_option.h" #if CFG_TUD_ENABLED && defined(TUP_USBIP_WCH_USBHS) && defined(CFG_TUD_WCH_USBIP_USBHS) && \ @@ -37,138 +36,182 @@ #define EP_MAX 16 typedef struct { - uint8_t* buffer; + uint8_t *buffer; uint16_t total_len; uint16_t queued_len; uint16_t max_size; - bool is_last_packet; - bool is_iso; + bool is_iso; + bool valid; } xfer_ctl_t; -typedef enum { - EP_RESPONSE_ACK, - EP_RESPONSE_NAK, -} ep_response_list_t; - -#define XFER_CTL_BASE(_ep, _dir) &xfer_status[_ep][_dir] + #define XFER_CTL_BASE(_ep, _dir) &xfer_status[_ep][_dir] static xfer_ctl_t xfer_status[EP_MAX][2]; -#define EP_TX_LEN(ep) *(volatile uint16_t *)((volatile uint16_t *)&(USBHSD->UEP0_TX_LEN) + (ep) * 2) -#define EP_TX_CTRL(ep) *(volatile uint8_t *)((volatile uint8_t *)&(USBHSD->UEP0_TX_CTRL) + (ep) * 4) -#define EP_RX_CTRL(ep) *(volatile uint8_t *)((volatile uint8_t *)&(USBHSD->UEP0_RX_CTRL) + (ep) * 4) -#define EP_RX_MAX_LEN(ep) *(volatile uint16_t *)((volatile uint16_t *)&(USBHSD->UEP0_MAX_LEN) + (ep) * 2) + #define EP_TX_LEN(ep) *(volatile uint16_t *)((volatile uint16_t *)&(USBHSD->UEP0_TX_LEN) + (ep) * 2) + #define EP_TX_CTRL(ep) *(volatile uint8_t *)((volatile uint8_t *)&(USBHSD->UEP0_TX_CTRL) + (ep) * 4) + #define EP_RX_CTRL(ep) *(volatile uint8_t *)((volatile uint8_t *)&(USBHSD->UEP0_RX_CTRL) + (ep) * 4) + #define EP_RX_MAX_LEN(ep) *(volatile uint16_t *)((volatile uint16_t *)&(USBHSD->UEP0_MAX_LEN) + (ep) * 2) -#define EP_TX_DMA_ADDR(ep) *(volatile uint32_t *)((volatile uint32_t *)&(USBHSD->UEP1_TX_DMA) + (ep - 1)) -#define EP_RX_DMA_ADDR(ep) *(volatile uint32_t *)((volatile uint32_t *)&(USBHSD->UEP1_RX_DMA) + (ep - 1)) + #define EP_TX_DMA_ADDR(ep) *(volatile uint32_t *)((volatile uint32_t *)&(USBHSD->UEP1_TX_DMA) + (ep - 1)) + #define EP_RX_DMA_ADDR(ep) *(volatile uint32_t *)((volatile uint32_t *)&(USBHSD->UEP1_RX_DMA) + (ep - 1)) /* Endpoint Buffer */ TU_ATTR_ALIGNED(4) static uint8_t ep0_buffer[CFG_TUD_ENDPOINT0_SIZE]; +static bool ep0_tog; +static bool ep_data_tog[EP_MAX][2]; -static void ep_set_response_and_toggle(uint8_t ep_num, tusb_dir_t ep_dir, ep_response_list_t response_type) { +static void set_ep_toggle(uint8_t ep_num, tusb_dir_t ep_dir, bool data1) { if (ep_dir == TUSB_DIR_IN) { - uint8_t response = (response_type == EP_RESPONSE_ACK) ? USBHS_EP_T_RES_ACK : USBHS_EP_T_RES_NAK; - if (ep_num == 0) { - if (response_type == EP_RESPONSE_ACK) { - if (EP_TX_LEN(ep_num) == 0) { - EP_TX_CTRL(ep_num) |= USBHS_EP_T_TOG_1; - } else { - EP_TX_CTRL(ep_num) ^= USBHS_EP_T_TOG_1; - } - } - } - if (xfer_status[ep_num][TUSB_DIR_IN].is_iso == true) { - EP_TX_CTRL(ep_num) = USBHS_EP_T_AUTOTOG; - } else { - EP_TX_CTRL(ep_num) = (EP_TX_CTRL(ep_num) & ~(USBHS_EP_T_RES_MASK)) | response; - } + EP_TX_CTRL(ep_num) = (EP_TX_CTRL(ep_num) & ~(USBHS_EP_T_TOG_MASK)) | + (data1 ? USBHS_EP_T_TOG_1 : USBHS_EP_T_TOG_0); } else { - uint8_t response = (response_type == EP_RESPONSE_ACK) ? USBHS_EP_R_RES_ACK : USBHS_EP_R_RES_NAK; - if (ep_num == 0) { - if (response_type == EP_RESPONSE_ACK) { - if (xfer_status[ep_num][TUSB_DIR_OUT].queued_len == 0) { - EP_RX_CTRL(ep_num) |= USBHS_EP_R_TOG_1; - } - } else { - EP_RX_CTRL(ep_num) ^= USBHS_EP_R_TOG_1; - } - } - EP_RX_CTRL(ep_num) = (EP_RX_CTRL(ep_num) & ~(USBHS_EP_R_RES_MASK)) | response; + EP_RX_CTRL(ep_num) = (EP_RX_CTRL(ep_num) & ~(USBHS_EP_R_TOG_MASK)) | + (data1 ? USBHS_EP_R_TOG_1 : USBHS_EP_R_TOG_0); } } -static void xfer_data_packet(uint8_t ep_num, tusb_dir_t ep_dir, xfer_ctl_t* xfer) { - if (ep_dir == TUSB_DIR_IN) { - uint16_t remaining = xfer->total_len - xfer->queued_len; - uint16_t next_tx_size = TU_MIN(remaining, xfer->max_size); +static void queue_in_packet(uint8_t ep_num, xfer_ctl_t* xfer) { + uint16_t remaining = xfer->total_len - xfer->queued_len; + uint16_t tx_len = TU_MIN(remaining, xfer->max_size); + + if (ep_num == 0) { + memcpy(ep0_buffer, &xfer->buffer[xfer->queued_len], tx_len); + } else { + EP_TX_DMA_ADDR(ep_num) = (uint32_t) &xfer->buffer[xfer->queued_len]; + } + + EP_TX_LEN(ep_num) = tx_len; + xfer->queued_len += tx_len; + + if (ep_num == 0) { + EP_TX_CTRL(0) = USBHS_EP_T_RES_ACK | (ep0_tog ? USBHS_EP_T_TOG_1 : USBHS_EP_T_TOG_0); + ep0_tog = !ep0_tog; + } else if (xfer->is_iso) { + EP_TX_CTRL(ep_num) = (EP_TX_CTRL(ep_num) & ~(USBHS_EP_T_RES_MASK)) | USBHS_EP_T_RES_NYET; + } else { + set_ep_toggle(ep_num, TUSB_DIR_IN, ep_data_tog[ep_num][TUSB_DIR_IN]); + EP_TX_CTRL(ep_num) = (EP_TX_CTRL(ep_num) & ~(USBHS_EP_T_RES_MASK)) | USBHS_EP_T_RES_ACK; + } +} + +static void queue_out_packet(uint8_t ep_num, xfer_ctl_t* xfer) { + uint16_t remaining = xfer->total_len - xfer->queued_len; + uint16_t rx_len = TU_MIN(remaining, xfer->max_size); + if (ep_num > 0) { + EP_RX_DMA_ADDR(ep_num) = (uint32_t) &xfer->buffer[xfer->queued_len]; + EP_RX_MAX_LEN(ep_num) = rx_len; + } + + if (ep_num == 0) { + EP_RX_CTRL(0) = (EP_RX_CTRL(0) & ~(USBHS_EP_R_RES_MASK)) | USBHS_EP_R_RES_ACK; + } else if (xfer->is_iso) { + EP_RX_CTRL(ep_num) = (EP_RX_CTRL(ep_num) & ~(USBHS_EP_R_RES_MASK)) | USBHS_EP_R_RES_NYET; + } else { + set_ep_toggle(ep_num, TUSB_DIR_OUT, ep_data_tog[ep_num][TUSB_DIR_OUT]); + EP_RX_CTRL(ep_num) = (EP_RX_CTRL(ep_num) & ~(USBHS_EP_R_RES_MASK)) | USBHS_EP_R_RES_ACK; + } +} + +static void update_in(uint8_t rhport, uint8_t ep_num, bool force) { + xfer_ctl_t* xfer = XFER_CTL_BASE(ep_num, TUSB_DIR_IN); + if (!xfer->valid) { + return; + } + + if (!force && ep_num != 0 && !xfer->is_iso) { + ep_data_tog[ep_num][TUSB_DIR_IN] = !ep_data_tog[ep_num][TUSB_DIR_IN]; + } + + if (force || (xfer->total_len > xfer->queued_len)) { + queue_in_packet(ep_num, xfer); + } else { + xfer->valid = false; if (ep_num == 0) { - memcpy(ep0_buffer, &xfer->buffer[xfer->queued_len], next_tx_size); + EP_TX_CTRL(0) = USBHS_EP_T_RES_NAK | (ep0_tog ? USBHS_EP_T_TOG_1 : USBHS_EP_T_TOG_0); } else { - EP_TX_DMA_ADDR(ep_num) = (uint32_t) &xfer->buffer[xfer->queued_len]; + EP_TX_CTRL(ep_num) = (EP_TX_CTRL(ep_num) & ~(USBHS_EP_T_RES_MASK)) | USBHS_EP_T_RES_NAK; } + dcd_event_xfer_complete(rhport, ep_num | TUSB_DIR_IN_MASK, xfer->queued_len, XFER_RESULT_SUCCESS, true); + } +} - EP_TX_LEN(ep_num) = next_tx_size; - xfer->queued_len += next_tx_size; - if (xfer->queued_len == xfer->total_len) { - xfer->is_last_packet = true; - } - if (xfer->is_iso == true) { - /* Enable EP to generate ISA_ACT interrupt */ - USBHSD->ENDP_CONFIG |= (USBHS_EP0_T_EN << ep_num); - } - } else { /* TUSB_DIR_OUT */ - uint16_t left_to_receive = xfer->total_len - xfer->queued_len; - uint16_t max_possible_rx_size = TU_MIN(xfer->max_size, left_to_receive); +static void update_out(uint8_t rhport, uint8_t ep_num, uint16_t rx_len) { + xfer_ctl_t* xfer = XFER_CTL_BASE(ep_num, TUSB_DIR_OUT); + if (!xfer->valid) { + return; + } + + uint16_t remaining = xfer->total_len - xfer->queued_len; + uint16_t len = TU_MIN(rx_len, TU_MIN(remaining, xfer->max_size)); + + if (ep_num == 0) { + memcpy(&xfer->buffer[xfer->queued_len], ep0_buffer, len); + } + + xfer->queued_len += len; + + if (ep_num != 0 && !xfer->is_iso) { + ep_data_tog[ep_num][TUSB_DIR_OUT] = !ep_data_tog[ep_num][TUSB_DIR_OUT]; + } - if (max_possible_rx_size == left_to_receive) { - xfer->is_last_packet = true; + if ((xfer->queued_len == xfer->total_len) || (len < xfer->max_size)) { + xfer->valid = false; + if (ep_num == 0) { + EP_RX_CTRL(0) = (EP_RX_CTRL(0) & ~(USBHS_EP_R_RES_MASK)) | USBHS_EP_R_RES_NAK; } + dcd_event_xfer_complete(rhport, ep_num, xfer->queued_len, XFER_RESULT_SUCCESS, true); + } - if (ep_num > 0) { - EP_RX_DMA_ADDR(ep_num) = (uint32_t) &xfer->buffer[xfer->queued_len]; - EP_RX_MAX_LEN(ep_num) = max_possible_rx_size; + if (ep_num != 0) { + if (xfer->valid) { + queue_out_packet(ep_num, xfer); + } else { + uint8_t rx_res = xfer->is_iso ? USBHS_EP_R_RES_NYET : USBHS_EP_R_RES_NAK; + EP_RX_CTRL(ep_num) = (EP_RX_CTRL(ep_num) & ~(USBHS_EP_R_RES_MASK)) | rx_res; } } - ep_set_response_and_toggle(ep_num, ep_dir, USBHS_EP_R_RES_ACK); } -bool dcd_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) { - (void) rhport; - (void) rh_init; +bool dcd_init(uint8_t rhport, const tusb_rhport_init_t *rh_init) { + (void)rhport; + (void)rh_init; memset(&xfer_status, 0, sizeof(xfer_status)); + memset(ep_data_tog, 0, sizeof(ep_data_tog)); + ep0_tog = true; USBHSD->HOST_CTRL = 0x00; USBHSD->HOST_CTRL = USBHS_PHY_SUSPENDM; USBHSD->CONTROL = 0; -#if TUD_OPT_HIGH_SPEED + #if TUD_OPT_HIGH_SPEED USBHSD->CONTROL = USBHS_DMA_EN | USBHS_INT_BUSY_EN | USBHS_HIGH_SPEED; -#else - #error OPT_MODE_FULL_SPEED not currently supported on CH32 + #else + #error OPT_MODE_FULL_SPEED not currently supported on CH32 USBHSD->CONTROL = USBHS_DMA_EN | USBHS_INT_BUSY_EN | USBHS_FULL_SPEED; -#endif + #endif USBHSD->INT_EN = 0; - USBHSD->INT_EN = USBHS_SETUP_ACT_EN | USBHS_TRANSFER_EN | USBHS_BUS_RST_EN | USBHS_SUSPEND_EN | USBHS_ISO_ACT_EN; + USBHSD->INT_EN = USBHS_SETUP_ACT_EN | USBHS_TRANSFER_EN | USBHS_BUS_RST_EN | USBHS_SUSPEND_EN; USBHSD->ENDP_CONFIG = USBHS_EP0_T_EN | USBHS_EP0_R_EN; - USBHSD->ENDP_TYPE = 0x00; - USBHSD->BUF_MODE = 0x00; + USBHSD->ENDP_TYPE = 0x00; + USBHSD->BUF_MODE = 0x00; for (int ep = 0; ep < EP_MAX; ep++) { - EP_TX_LEN(ep) = 0; - EP_TX_CTRL(ep) = USBHS_EP_T_AUTOTOG | USBHS_EP_T_RES_NAK; - EP_RX_CTRL(ep) = USBHS_EP_R_AUTOTOG | USBHS_EP_R_RES_NAK; + EP_TX_LEN(ep) = 0; + EP_TX_CTRL(ep) = USBHS_EP_T_RES_NAK | USBHS_EP_T_TOG_0; + EP_RX_CTRL(ep) = USBHS_EP_R_RES_NAK | USBHS_EP_R_TOG_0; EP_RX_MAX_LEN(ep) = 0; } - USBHSD->UEP0_DMA = (uint32_t) ep0_buffer; - USBHSD->UEP0_MAX_LEN = CFG_TUD_ENDPOINT0_SIZE; + USBHSD->UEP0_DMA = (uint32_t)ep0_buffer; + USBHSD->UEP0_MAX_LEN = CFG_TUD_ENDPOINT0_SIZE; xfer_status[0][TUSB_DIR_OUT].max_size = CFG_TUD_ENDPOINT0_SIZE; - xfer_status[0][TUSB_DIR_IN].max_size = CFG_TUD_ENDPOINT0_SIZE; + xfer_status[0][TUSB_DIR_IN].max_size = CFG_TUD_ENDPOINT0_SIZE; USBHSD->DEV_AD = 0; USBHSD->CONTROL |= USBHS_DEV_PU_EN; @@ -177,22 +220,24 @@ bool dcd_init(uint8_t rhport, const tusb_rhport_init_t* rh_init) { } void dcd_int_enable(uint8_t rhport) { - (void) rhport; + (void)rhport; NVIC_EnableIRQ(USBHS_IRQn); } void dcd_int_disable(uint8_t rhport) { - (void) rhport; + (void)rhport; NVIC_DisableIRQ(USBHS_IRQn); } void dcd_edpt_close_all(uint8_t rhport) { - (void) rhport; + (void)rhport; + + memset(ep_data_tog, 0, sizeof(ep_data_tog)); for (size_t ep = 1; ep < EP_MAX; ep++) { - EP_TX_LEN(ep) = 0; - EP_TX_CTRL(ep) = USBHS_EP_T_AUTOTOG | USBHS_EP_T_RES_NAK; - EP_RX_CTRL(ep) = USBHS_EP_R_AUTOTOG | USBHS_EP_R_RES_NAK; + EP_TX_LEN(ep) = 0; + EP_TX_CTRL(ep) = USBHS_EP_T_RES_NAK | USBHS_EP_T_TOG_0; + EP_RX_CTRL(ep) = USBHS_EP_R_RES_NAK | USBHS_EP_R_TOG_0; EP_RX_MAX_LEN(ep) = 0; } @@ -201,18 +246,18 @@ void dcd_edpt_close_all(uint8_t rhport) { } void dcd_set_address(uint8_t rhport, uint8_t dev_addr) { - (void) dev_addr; + (void)dev_addr; // Response with zlp status dcd_edpt_xfer(rhport, 0x80, NULL, 0, false); } void dcd_remote_wakeup(uint8_t rhport) { - (void) rhport; + (void)rhport; } void dcd_sof_enable(uint8_t rhport, bool en) { - (void) rhport; + (void)rhport; if (en) { USBHSD->INT_EN |= USBHS_SOF_ACT_EN; } else { @@ -220,24 +265,19 @@ void dcd_sof_enable(uint8_t rhport, bool en) { } } -void dcd_edpt0_status_complete(uint8_t rhport, tusb_control_request_t const* request) { - (void) rhport; - +void dcd_edpt0_status_complete(uint8_t rhport, const tusb_control_request_t *request) { + (void)rhport; if (request->bmRequestType_bit.recipient == TUSB_REQ_RCPT_DEVICE && - request->bmRequestType_bit.type == TUSB_REQ_TYPE_STANDARD && - request->bRequest == TUSB_REQ_SET_ADDRESS) { - USBHSD->DEV_AD = (uint8_t) request->wValue; + request->bmRequestType_bit.type == TUSB_REQ_TYPE_STANDARD && request->bRequest == TUSB_REQ_SET_ADDRESS) { + USBHSD->DEV_AD = (uint8_t)request->wValue; } - - EP_TX_CTRL(0) = USBHS_EP_T_RES_NAK | USBHS_EP_T_TOG_0; - EP_RX_CTRL(0) = USBHS_EP_R_RES_NAK | USBHS_EP_R_TOG_0; } -bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const* desc_edpt) { - (void) rhport; +bool dcd_edpt_open(uint8_t rhport, const tusb_desc_endpoint_t *desc_edpt) { + (void)rhport; - uint8_t const ep_num = tu_edpt_number(desc_edpt->bEndpointAddress); - tusb_dir_t const dir = tu_edpt_dir(desc_edpt->bEndpointAddress); + const uint8_t ep_num = tu_edpt_number(desc_edpt->bEndpointAddress); + const tusb_dir_t dir = tu_edpt_dir(desc_edpt->bEndpointAddress); TU_ASSERT(ep_num < EP_MAX); @@ -245,13 +285,14 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const* desc_edpt) { return true; } - xfer_ctl_t* xfer = XFER_CTL_BASE(ep_num, dir); - xfer->max_size = tu_edpt_packet_size(desc_edpt); + xfer_ctl_t *xfer = XFER_CTL_BASE(ep_num, dir); + xfer->max_size = tu_edpt_packet_size(desc_edpt); + ep_data_tog[ep_num][dir] = false; xfer->is_iso = (desc_edpt->bmAttributes.xfer == TUSB_XFER_ISOCHRONOUS); if (dir == TUSB_DIR_OUT) { USBHSD->ENDP_CONFIG |= (USBHS_EP0_R_EN << ep_num); - EP_RX_CTRL(ep_num) = USBHS_EP_R_AUTOTOG | USBHS_EP_R_RES_NAK; + EP_RX_CTRL(ep_num) = USBHS_EP_R_RES_NAK | USBHS_EP_R_TOG_0; if (xfer->is_iso == true) { USBHSD->ENDP_TYPE |= (USBHS_EP0_R_TYP << ep_num); } @@ -259,31 +300,31 @@ bool dcd_edpt_open(uint8_t rhport, tusb_desc_endpoint_t const* desc_edpt) { } else { if (xfer->is_iso == true) { USBHSD->ENDP_TYPE |= (USBHS_EP0_T_TYP << ep_num); - } else { - /* Enable all types except Isochronous to avoid ISO_ACT interrupt generation */ - USBHSD->ENDP_CONFIG |= (USBHS_EP0_T_EN << ep_num); } - EP_TX_LEN(ep_num) = 0; - EP_TX_CTRL(ep_num) = USBHS_EP_T_AUTOTOG | USBHS_EP_T_RES_NAK | USBHS_EP_T_TOG_0; + USBHSD->ENDP_CONFIG |= (USBHS_EP0_T_EN << ep_num); + EP_TX_LEN(ep_num) = 0; + EP_TX_CTRL(ep_num) = USBHS_EP_T_RES_NAK | USBHS_EP_T_TOG_0; } return true; } void dcd_edpt_close(uint8_t rhport, uint8_t ep_addr) { - (void) rhport; + (void)rhport; - uint8_t const ep_num = tu_edpt_number(ep_addr); - tusb_dir_t const dir = tu_edpt_dir(ep_addr); + const uint8_t ep_num = tu_edpt_number(ep_addr); + const tusb_dir_t dir = tu_edpt_dir(ep_addr); if (dir == TUSB_DIR_OUT) { - EP_RX_CTRL(ep_num) = USBHS_EP_R_AUTOTOG | USBHS_EP_R_RES_NAK; + EP_RX_CTRL(ep_num) = USBHS_EP_R_RES_NAK | USBHS_EP_R_TOG_0; EP_RX_MAX_LEN(ep_num) = 0; + ep_data_tog[ep_num][TUSB_DIR_OUT] = false; USBHSD->ENDP_TYPE &= ~(USBHS_EP0_R_TYP << ep_num); USBHSD->ENDP_CONFIG &= ~(USBHS_EP0_R_EN << ep_num); - } else { // TUSB_DIR_IN - EP_TX_CTRL(ep_num) = USBHS_EP_T_AUTOTOG | USBHS_EP_T_RES_NAK | USBHS_EP_T_TOG_0; - EP_TX_LEN(ep_num) = 0; + } else { // TUSB_DIR_IN + EP_TX_CTRL(ep_num) = USBHS_EP_T_RES_NAK | USBHS_EP_T_TOG_0; + EP_TX_LEN(ep_num) = 0; + ep_data_tog[ep_num][TUSB_DIR_IN] = false; USBHSD->ENDP_TYPE &= ~(USBHS_EP0_T_TYP << ep_num); USBHSD->ENDP_CONFIG &= ~(USBHS_EP0_T_EN << ep_num); } @@ -305,128 +346,120 @@ bool dcd_edpt_iso_activate(uint8_t rhport, tusb_desc_endpoint_t const * desc_ep) #endif void dcd_edpt_stall(uint8_t rhport, uint8_t ep_addr) { - (void) rhport; + (void)rhport; - uint8_t const ep_num = tu_edpt_number(ep_addr); - tusb_dir_t const dir = tu_edpt_dir(ep_addr); + const uint8_t ep_num = tu_edpt_number(ep_addr); + const tusb_dir_t dir = tu_edpt_dir(ep_addr); if (dir == TUSB_DIR_OUT) { EP_RX_CTRL(ep_num) = USBHS_EP_R_RES_STALL; } else { - EP_TX_LEN(0) = 0; + EP_TX_LEN(0) = 0; EP_TX_CTRL(ep_num) = USBHS_EP_T_RES_STALL; } } void dcd_edpt_clear_stall(uint8_t rhport, uint8_t ep_addr) { - (void) rhport; + (void)rhport; - uint8_t const ep_num = tu_edpt_number(ep_addr); - tusb_dir_t const dir = tu_edpt_dir(ep_addr); + const uint8_t ep_num = tu_edpt_number(ep_addr); + const tusb_dir_t dir = tu_edpt_dir(ep_addr); if (dir == TUSB_DIR_OUT) { - EP_RX_CTRL(ep_num) = USBHS_EP_R_AUTOTOG | USBHS_EP_R_RES_NAK; + EP_RX_CTRL(ep_num) = USBHS_EP_R_RES_NAK | USBHS_EP_R_TOG_0; + ep_data_tog[ep_num][TUSB_DIR_OUT] = false; } else { - EP_TX_CTRL(ep_num) = USBHS_EP_T_AUTOTOG | USBHS_EP_R_RES_NAK; + EP_TX_CTRL(ep_num) = USBHS_EP_T_RES_NAK | USBHS_EP_T_TOG_0; + ep_data_tog[ep_num][TUSB_DIR_IN] = false; } } -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; - uint8_t const ep_num = tu_edpt_number(ep_addr); - tusb_dir_t const dir = tu_edpt_dir(ep_addr); +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; + const uint8_t ep_num = tu_edpt_number(ep_addr); + const tusb_dir_t dir = tu_edpt_dir(ep_addr); + + xfer_ctl_t *xfer = XFER_CTL_BASE(ep_num, dir); + xfer->buffer = buffer; + xfer->total_len = total_bytes; + xfer->queued_len = 0; + xfer->valid = true; - xfer_ctl_t* xfer = XFER_CTL_BASE(ep_num, dir); - xfer->buffer = buffer; - xfer->total_len = total_bytes; - xfer->queued_len = 0; - xfer->is_last_packet = false; + if (ep_num == 0 && dir == TUSB_DIR_OUT) { + if (total_bytes == 0) { + EP_RX_CTRL(0) = (EP_RX_CTRL(0) & ~(USBHS_EP_R_TOG_MASK)) | USBHS_EP_R_TOG_1; + } else { + EP_RX_CTRL(0) ^= USBHS_EP_R_TOG_1; + } + } - xfer_data_packet(ep_num, dir, xfer); + if (dir == TUSB_DIR_IN) { + update_in(rhport, ep_num, true); + } else { + queue_out_packet(ep_num, xfer); + } return true; } void dcd_int_handler(uint8_t rhport) { - (void) rhport; + (void)rhport; - uint8_t int_flag = USBHSD->INT_FG; + uint8_t int_flag = USBHSD->INT_FG; uint8_t int_status = USBHSD->INT_ST; - if (int_flag & (USBHS_ISO_ACT_FLAG | USBHS_TRANSFER_FLAG)) { - uint8_t const token = int_status & MASK_UIS_TOKEN; + if (int_flag & USBHS_TRANSFER_FLAG) { + const uint8_t token = int_status & MASK_UIS_TOKEN; + const uint8_t ep_num = int_status & MASK_UIS_ENDP; + const uint16_t len = USBHSD->RX_LEN; if (token == USBHS_TOKEN_PID_SOF) { uint32_t frame_count = USBHSD->FRAME_NO & USBHS_FRAME_NO_NUM_MASK; dcd_event_sof(rhport, frame_count, true); - }else { - uint8_t const ep_num = int_status & MASK_UIS_ENDP; - tusb_dir_t const ep_dir = (token == USBHS_TOKEN_PID_IN) ? TUSB_DIR_IN : TUSB_DIR_OUT; - uint8_t const ep_addr = tu_edpt_addr(ep_num, ep_dir); - xfer_ctl_t* xfer = XFER_CTL_BASE(ep_num, ep_dir); - - if (token == USBHS_TOKEN_PID_OUT) { - uint16_t rx_len = USBHSD->RX_LEN; - - if (ep_num == 0) { - memcpy(&xfer->buffer[xfer->queued_len], ep0_buffer, rx_len); - } - - xfer->queued_len += rx_len; - if (rx_len < xfer->max_size) { - xfer->is_last_packet = true; - } - } else if (token == USBHS_TOKEN_PID_IN) { - if (xfer->is_iso && xfer->is_last_packet) { - /* Disable EP to avoid ISO_ACT interrupt generation */ - USBHSD->ENDP_CONFIG &= ~(USBHS_EP0_T_EN << ep_num); - } else { - // Do nothing, no need to update xfer->is_last_packet, it is already updated in xfer_data_packet - } - } - - if (xfer->is_last_packet == true) { - ep_set_response_and_toggle(ep_num, ep_dir, EP_RESPONSE_NAK); - dcd_event_xfer_complete(0, ep_addr, xfer->queued_len, XFER_RESULT_SUCCESS, true); - } else { - /* prepare next part of packet to xref */ - xfer_data_packet(ep_num, ep_dir, xfer); - } + } else if (token == USBHS_TOKEN_PID_OUT) { + update_out(rhport, ep_num, len); + } else if (token == USBHS_TOKEN_PID_IN) { + update_in(rhport, ep_num, false); } - - USBHSD->INT_FG = (int_flag & (USBHS_ISO_ACT_FLAG | USBHS_TRANSFER_FLAG)); /* Clear flag */ + USBHSD->INT_FG = (int_flag & USBHS_TRANSFER_FLAG); /* Clear flag */ } else if (int_flag & USBHS_SETUP_FLAG) { - ep_set_response_and_toggle(0, TUSB_DIR_IN, EP_RESPONSE_NAK); - ep_set_response_and_toggle(0, TUSB_DIR_OUT, EP_RESPONSE_NAK); + tusb_control_request_t const* setup = + (tusb_control_request_t const*) ep0_buffer; + ep0_tog = true; + EP_RX_CTRL(0) = (setup->wLength == 0) ? USBHS_EP_R_RES_ACK : USBHS_EP_R_RES_NAK; + EP_TX_CTRL(0) = USBHS_EP_T_RES_NAK; + dcd_event_setup_received(0, ep0_buffer, true); USBHSD->INT_FG = USBHS_SETUP_FLAG; /* Clear flag */ } else if (int_flag & USBHS_BUS_RST_FLAG) { // TODO CH32 does not detect actual speed at this time (should be known at end of reset) // This interrupt probably triggered at start of bus reset -// tusb_speed_t actual_speed; -// switch(USBHSD->SPEED_TYPE & USBHS_SPEED_TYPE_MASK){ -// case USBHS_SPEED_TYPE_HIGH: -// actual_speed = TUSB_SPEED_HIGH; -// break; -// case USBHS_SPEED_TYPE_FULL: -// actual_speed = TUSB_SPEED_FULL; -// break; -// case USBHS_SPEED_TYPE_LOW: -// actual_speed = TUSB_SPEED_LOW; -// break; -// default: -// TU_ASSERT(0,); -// break; -// } -// dcd_event_bus_reset(0, actual_speed, true); + // tusb_speed_t actual_speed; + // switch(USBHSD->SPEED_TYPE & USBHS_SPEED_TYPE_MASK){ + // case USBHS_SPEED_TYPE_HIGH: + // actual_speed = TUSB_SPEED_HIGH; + // break; + // case USBHS_SPEED_TYPE_FULL: + // actual_speed = TUSB_SPEED_FULL; + // break; + // case USBHS_SPEED_TYPE_LOW: + // actual_speed = TUSB_SPEED_LOW; + // break; + // default: + // TU_ASSERT(0,); + // break; + // } + // dcd_event_bus_reset(0, actual_speed, true); dcd_event_bus_reset(0, TUSB_SPEED_HIGH, true); USBHSD->DEV_AD = 0; - EP_RX_CTRL(0) = USBHS_EP_R_RES_ACK | USBHS_EP_R_TOG_0; - EP_TX_CTRL(0) = USBHS_EP_T_RES_NAK | USBHS_EP_T_TOG_0; + memset(ep_data_tog, 0, sizeof(ep_data_tog)); + ep0_tog = true; + EP_RX_CTRL(0) = USBHS_EP_R_RES_ACK | USBHS_EP_R_TOG_0; + EP_TX_CTRL(0) = USBHS_EP_T_RES_NAK | USBHS_EP_T_TOG_0; USBHSD->INT_FG = USBHS_BUS_RST_FLAG; /* Clear flag */ } else if (int_flag & USBHS_SUSPEND_FLAG) { @@ -434,6 +467,9 @@ void dcd_int_handler(uint8_t rhport) { dcd_event_handler(&event, true); USBHSD->INT_FG = USBHS_SUSPEND_FLAG; /* Clear flag */ + } else { + // Unhandled interrupt + USBHSD->INT_FG = int_flag; /* Clear all flags */ } } #endif diff --git a/src/tinyusb.mk b/src/tinyusb.mk index 169098016..365043927 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 \ @@ -11,6 +10,7 @@ TINYUSB_SRC_C += \ src/class/dfu/dfu_rt_device.c \ src/class/hid/hid_device.c \ src/class/midi/midi_device.c \ + src/class/midi/midi2_device.c \ src/class/msc/msc_device.c \ src/class/mtp/mtp_device.c \ src/class/net/ecm_rndis_device.c \ @@ -24,5 +24,6 @@ TINYUSB_SRC_C += \ src/class/cdc/cdc_host.c \ src/class/hid/hid_host.c \ src/class/midi/midi_host.c \ + src/class/midi/midi2_host.c \ src/class/msc/msc_host.c \ src/class/vendor/vendor_host.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/src/tusb.h b/src/tusb.h index c80c8433c..aa6b461e5 100644 --- a/src/tusb.h +++ b/src/tusb.h @@ -63,6 +63,10 @@ #include "class/midi/midi_host.h" #endif + #if CFG_TUH_MIDI2 + #include "class/midi/midi2_host.h" + #endif + #if CFG_TUH_VENDOR #include "class/vendor/vendor_host.h" #endif @@ -108,6 +112,10 @@ #include "class/midi/midi_device.h" #endif + #if CFG_TUD_MIDI2 + #include "class/midi/midi2_device.h" + #endif + #if CFG_TUD_VENDOR #include "class/vendor/vendor_device.h" #endif diff --git a/src/tusb_option.h b/src/tusb_option.h index 154f8e2a4..74eb8cc06 100644 --- a/src/tusb_option.h +++ b/src/tusb_option.h @@ -646,6 +646,10 @@ #define CFG_TUD_MIDI 0 #endif +#ifndef CFG_TUD_MIDI2 + #define CFG_TUD_MIDI2 0 +#endif + #ifndef CFG_TUD_VENDOR #define CFG_TUD_VENDOR 0 #endif @@ -815,6 +819,22 @@ #define CFG_TUH_MIDI 0 #endif +#ifndef CFG_TUH_MIDI2 + #define CFG_TUH_MIDI2 0 +#endif + +#ifndef CFG_TUH_MIDI2_RX_BUFSIZE + #define CFG_TUH_MIDI2_RX_BUFSIZE TUH_EPSIZE_BULK_MAX +#endif + +#ifndef CFG_TUH_MIDI2_TX_BUFSIZE + #define CFG_TUH_MIDI2_TX_BUFSIZE TUH_EPSIZE_BULK_MAX +#endif + +#ifndef CFG_TUH_MIDI2_LOG_LEVEL + #define CFG_TUH_MIDI2_LOG_LEVEL CFG_TUH_LOG_LEVEL +#endif + #ifndef CFG_TUH_MSC #define CFG_TUH_MSC 0 #endif 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/test/unit-test/test/device/midi2/test_midi2_device.c b/test/unit-test/test/device/midi2/test_midi2_device.c new file mode 100644 index 000000000..1314c2585 --- /dev/null +++ b/test/unit-test/test/device/midi2/test_midi2_device.c @@ -0,0 +1,266 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * 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 "unity.h" +#include "tusb_types.h" +#include "class/audio/audio.h" +#include "class/midi/midi.h" +#include "device/usbd.h" + +void setUp(void) {} +void tearDown(void) {} + +//--------------------------------------------------------------------+ +// UMP Word Count: all 16 message types +//--------------------------------------------------------------------+ + +void test_ump_word_count_1word_types(void) { + uint8_t types[] = {0x0, 0x1, 0x2, 0x6, 0x7}; + for (int i = 0; i < 5; i++) { + TEST_ASSERT_EQUAL(1, midi2_ump_word_count(types[i])); + } +} + +void test_ump_word_count_2word_types(void) { + uint8_t types[] = {0x3, 0x4, 0x8, 0x9, 0xA}; + for (int i = 0; i < 5; i++) { + TEST_ASSERT_EQUAL(2, midi2_ump_word_count(types[i])); + } +} + +void test_ump_word_count_3word_types(void) { + TEST_ASSERT_EQUAL(3, midi2_ump_word_count(0xB)); + TEST_ASSERT_EQUAL(3, midi2_ump_word_count(0xC)); +} + +void test_ump_word_count_4word_types(void) { + uint8_t types[] = {0x5, 0xD, 0xE, 0xF}; + for (int i = 0; i < 4; i++) { + TEST_ASSERT_EQUAL(4, midi2_ump_word_count(types[i])); + } +} + +void test_ump_word_count_covers_all_16(void) { + for (uint8_t mt = 0; mt <= 0xF; mt++) { + uint8_t wc = midi2_ump_word_count(mt); + TEST_ASSERT_TRUE(wc >= 1 && wc <= 4); + } +} + +//--------------------------------------------------------------------+ +// CS Endpoint subtypes (defined in midi.h) +//--------------------------------------------------------------------+ + +void test_cs_endpoint_subtypes(void) { + TEST_ASSERT_EQUAL(0x01, MIDI_CS_ENDPOINT_GENERAL); + TEST_ASSERT_EQUAL(0x02, MIDI_CS_ENDPOINT_GENERAL_2_0); +} + +//--------------------------------------------------------------------+ +// Descriptor macro length calculations +//--------------------------------------------------------------------+ + +void test_midi1_desc_len(void) { + TEST_ASSERT_EQUAL(TUD_MIDI_DESC_HEAD_LEN + TUD_MIDI_DESC_JACK_LEN + TUD_MIDI_DESC_EP_LEN(1) * 2, + TUD_MIDI_DESC_LEN); +} + +void test_midi2_alt1_head_len(void) { + TEST_ASSERT_EQUAL(16, TUD_MIDI2_DESC_ALT1_HEAD_LEN); +} + +void test_midi2_alt1_ep_len(void) { + // EP(7) + CS base(4) + numgtbs + TEST_ASSERT_EQUAL(12, TUD_MIDI2_DESC_ALT1_EP_LEN(1)); + TEST_ASSERT_EQUAL(13, TUD_MIDI2_DESC_ALT1_EP_LEN(2)); + TEST_ASSERT_EQUAL(18, TUD_MIDI2_DESC_ALT1_EP_LEN(7)); +} + +void test_midi2_desc_len(void) { + int expected = TUD_MIDI_DESC_LEN + TUD_MIDI2_DESC_ALT1_HEAD_LEN + TUD_MIDI2_DESC_ALT1_EP_LEN(1) * 2; + TEST_ASSERT_EQUAL(expected, TUD_MIDI2_DESC_LEN); +} + +void test_midi2_desc_len_greater_than_midi1(void) { + TEST_ASSERT_TRUE(TUD_MIDI2_DESC_LEN > TUD_MIDI_DESC_LEN); +} + +//--------------------------------------------------------------------+ +// Descriptor macro byte validation +//--------------------------------------------------------------------+ + +void test_midi2_descriptor_bytes(void) { + uint8_t desc[] = { TUD_MIDI2_DESCRIPTOR(0, 0, 0x01, 0x81, 64) }; + + TEST_ASSERT_EQUAL(TUD_MIDI2_DESC_LEN, sizeof(desc)); + + // First byte: Audio Control Interface descriptor length = 9 + TEST_ASSERT_EQUAL(9, desc[0]); + TEST_ASSERT_EQUAL(TUSB_DESC_INTERFACE, desc[1]); + TEST_ASSERT_EQUAL(0, desc[2]); + + // Find Alt Setting 1 by scanning + int alt1_offset = -1; + int pos = 0; + while (pos < (int)sizeof(desc)) { + if (desc[pos + 1] == TUSB_DESC_INTERFACE && desc[pos + 3] == 1) { + alt1_offset = pos; + break; + } + pos += desc[pos]; + } + + TEST_ASSERT_TRUE_MESSAGE(alt1_offset >= 0, "Alt Setting 1 interface not found"); + + TEST_ASSERT_EQUAL(9, desc[alt1_offset]); + TEST_ASSERT_EQUAL(TUSB_DESC_INTERFACE, desc[alt1_offset + 1]); + TEST_ASSERT_EQUAL(1, desc[alt1_offset + 2]); // bInterfaceNumber + TEST_ASSERT_EQUAL(1, desc[alt1_offset + 3]); // bAlternateSetting + TEST_ASSERT_EQUAL(2, desc[alt1_offset + 4]); // bNumEndpoints + TEST_ASSERT_EQUAL(TUSB_CLASS_AUDIO, desc[alt1_offset + 5]); + + // MS Header after Alt Setting 1 interface: bcdMSC = 0x0200 + int ms2_offset = alt1_offset + 9; + TEST_ASSERT_EQUAL(7, desc[ms2_offset]); + TEST_ASSERT_EQUAL(TUSB_DESC_CS_INTERFACE, desc[ms2_offset + 1]); + TEST_ASSERT_EQUAL(MIDI_CS_INTERFACE_HEADER, desc[ms2_offset + 2]); + TEST_ASSERT_EQUAL(0x00, desc[ms2_offset + 3]); + TEST_ASSERT_EQUAL(0x02, desc[ms2_offset + 4]); + // USB-MIDI 2.0 Table 5-2: wTotalLength shall match bLength (= 0x0007) + TEST_ASSERT_EQUAL(0x07, desc[ms2_offset + 5]); + TEST_ASSERT_EQUAL(0x00, desc[ms2_offset + 6]); +} + +void test_midi2_descriptor_alt1_cs_endpoint_subtype(void) { + uint8_t desc[] = { TUD_MIDI2_DESCRIPTOR(0, 0, 0x01, 0x81, 64) }; + + int cs_ep_count = 0; + int pos = 0; + while (pos < (int)sizeof(desc)) { + if (desc[pos + 1] == TUSB_DESC_CS_ENDPOINT && + desc[pos + 2] == MIDI_CS_ENDPOINT_GENERAL_2_0) { + cs_ep_count++; + TEST_ASSERT_EQUAL(1, desc[pos + 3]); + } + pos += desc[pos]; + } + TEST_ASSERT_EQUAL(2, cs_ep_count); +} + +void test_midi2_descriptor_has_both_alt_settings(void) { + uint8_t desc[] = { TUD_MIDI2_DESCRIPTOR(0, 0, 0x01, 0x81, 64) }; + + int alt0_count = 0; + int alt1_count = 0; + int pos = 0; + while (pos < (int)sizeof(desc)) { + if (desc[pos + 1] == TUSB_DESC_INTERFACE) { + if (desc[pos + 3] == 0) alt0_count++; + if (desc[pos + 3] == 1) alt1_count++; + } + pos += desc[pos]; + } + TEST_ASSERT_TRUE(alt0_count >= 2); + TEST_ASSERT_EQUAL(1, alt1_count); +} + +void test_midi2_descriptor_endpoint_addresses(void) { + uint8_t desc[] = { TUD_MIDI2_DESCRIPTOR(0, 0, 0x02, 0x82, 64) }; + + int ep_out_count = 0; + int ep_in_count = 0; + int pos = 0; + while (pos < (int)sizeof(desc)) { + if (desc[pos + 1] == TUSB_DESC_ENDPOINT) { + uint8_t ep_addr = desc[pos + 2]; + if (ep_addr == 0x02) ep_out_count++; + if (ep_addr == 0x82) ep_in_count++; + TEST_ASSERT_EQUAL(TUSB_XFER_BULK, desc[pos + 3]); + TEST_ASSERT_EQUAL(64, desc[pos + 4]); + TEST_ASSERT_EQUAL(0, desc[pos + 5]); + } + pos += desc[pos]; + } + TEST_ASSERT_EQUAL(2, ep_out_count); + TEST_ASSERT_EQUAL(2, ep_in_count); +} + +void test_midi2_descriptor_nonzero_itfnum(void) { + uint8_t desc[] = { TUD_MIDI2_DESCRIPTOR(2, 0, 0x03, 0x83, 64) }; + + TEST_ASSERT_EQUAL(2, desc[2]); + + int pos = desc[0]; + while (pos < (int)sizeof(desc)) { + if (desc[pos + 1] == TUSB_DESC_INTERFACE) { + TEST_ASSERT_EQUAL(3, desc[pos + 2]); + break; + } + pos += desc[pos]; + } +} + +//--------------------------------------------------------------------+ +// Descriptor traversal integrity +//--------------------------------------------------------------------+ + +void test_midi2_descriptor_no_zero_length(void) { + uint8_t desc[] = { TUD_MIDI2_DESCRIPTOR(0, 0, 0x01, 0x81, 64) }; + + int pos = 0; + int desc_count = 0; + while (pos < (int)sizeof(desc)) { + TEST_ASSERT_TRUE_MESSAGE(desc[pos] > 0, "Zero-length descriptor found"); + TEST_ASSERT_TRUE_MESSAGE(desc[pos] <= (int)sizeof(desc) - pos, + "Descriptor length exceeds remaining bytes"); + pos += desc[pos]; + desc_count++; + } + TEST_ASSERT_EQUAL((int)sizeof(desc), pos); + TEST_ASSERT_TRUE(desc_count > 5); +} + +void test_midi2_descriptor_valid_types(void) { + uint8_t desc[] = { TUD_MIDI2_DESCRIPTOR(0, 0, 0x01, 0x81, 64) }; + + int pos = 0; + while (pos < (int)sizeof(desc)) { + uint8_t dtype = desc[pos + 1]; + bool valid = (dtype == TUSB_DESC_INTERFACE || + dtype == TUSB_DESC_ENDPOINT || + dtype == TUSB_DESC_CS_INTERFACE || + dtype == TUSB_DESC_CS_ENDPOINT); + TEST_ASSERT_TRUE_MESSAGE(valid, "Invalid descriptor type found"); + pos += desc[pos]; + } +} + +//--------------------------------------------------------------------+ +// Edge cases +//--------------------------------------------------------------------+ + +void test_ump_word_count_with_values_beyond_0xf(void) { + TEST_ASSERT_EQUAL(4, midi2_ump_word_count(0x10)); + TEST_ASSERT_EQUAL(4, midi2_ump_word_count(0xFF)); +} diff --git a/test/unit-test/test/host/midi2/test_midi2_host.c b/test/unit-test/test/host/midi2/test_midi2_host.c new file mode 100644 index 000000000..8ad77c14e --- /dev/null +++ b/test/unit-test/test/host/midi2/test_midi2_host.c @@ -0,0 +1,101 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * 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 "unity.h" +#include "tusb_option.h" +#include "class/midi/midi.h" +#include "class/midi/midi2_host.h" + +void setUp(void) {} +void tearDown(void) {} + +//--------------------------------------------------------------------+ +// UMP Word Count (shared helper, defined in midi.h) +//--------------------------------------------------------------------+ + +void test_midi2_host_ump_word_count_1word(void) { + uint8_t types[] = {0x0, 0x1, 0x2, 0x6, 0x7}; + for (int i = 0; i < 5; i++) { + TEST_ASSERT_EQUAL(1, midi2_ump_word_count(types[i])); + } +} + +void test_midi2_host_ump_word_count_2word(void) { + uint8_t types[] = {0x3, 0x4, 0x8, 0x9, 0xA}; + for (int i = 0; i < 5; i++) { + TEST_ASSERT_EQUAL(2, midi2_ump_word_count(types[i])); + } +} + +void test_midi2_host_ump_word_count_4word(void) { + uint8_t types[] = {0x5, 0xD, 0xE, 0xF}; + for (int i = 0; i < 4; i++) { + TEST_ASSERT_EQUAL(4, midi2_ump_word_count(types[i])); + } +} + +//--------------------------------------------------------------------+ +// Callback struct field validation +//--------------------------------------------------------------------+ + +void test_midi2_descriptor_cb_struct_fields(void) { + tuh_midi2_descriptor_cb_t desc = { + .protocol_version = 1, + .bcdMSC_hi = 0x02, + .bcdMSC_lo = 0x00, + .rx_cable_count = 1, + .tx_cable_count = 1 + }; + TEST_ASSERT_EQUAL(1, desc.protocol_version); + TEST_ASSERT_EQUAL(0x02, desc.bcdMSC_hi); + TEST_ASSERT_EQUAL(0x00, desc.bcdMSC_lo); + TEST_ASSERT_EQUAL(1, desc.rx_cable_count); + TEST_ASSERT_EQUAL(1, desc.tx_cable_count); +} + +void test_midi2_mount_cb_struct_fields(void) { + tuh_midi2_mount_cb_t mount = { + .daddr = 1, + .bInterfaceNumber = 0, + .protocol_version = 1, + .alt_setting_active = 1, + .rx_cable_count = 2, + .tx_cable_count = 2 + }; + TEST_ASSERT_EQUAL(1, mount.daddr); + TEST_ASSERT_EQUAL(0, mount.bInterfaceNumber); + TEST_ASSERT_EQUAL(1, mount.protocol_version); + TEST_ASSERT_EQUAL(1, mount.alt_setting_active); + TEST_ASSERT_EQUAL(2, mount.rx_cable_count); + TEST_ASSERT_EQUAL(2, mount.tx_cable_count); +} + +//--------------------------------------------------------------------+ +// CS Endpoint subtypes +//--------------------------------------------------------------------+ + +void test_midi2_host_cs_endpoint_subtypes(void) { + TEST_ASSERT_EQUAL(0x01, MIDI_CS_ENDPOINT_GENERAL); + TEST_ASSERT_EQUAL(0x02, MIDI_CS_ENDPOINT_GENERAL_2_0); +} 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() |
