Skip to content

feat: Add USB-MIDI 2.0 Device and Host class drivers#3571

Open
sauloverissimo wants to merge 7 commits intohathach:masterfrom
sauloverissimo:feat/midi2-device-host-driver
Open

feat: Add USB-MIDI 2.0 Device and Host class drivers#3571
sauloverissimo wants to merge 7 commits intohathach:masterfrom
sauloverissimo:feat/midi2-device-host-driver

Conversation

@sauloverissimo
Copy link
Copy Markdown
Contributor

@sauloverissimo sauloverissimo commented Mar 25, 2026

I have been following TinyUSB for a while and have deep respect for the work @hathach built here over the years. The care with static memory, the clarity of APIs, the callback patterns -- all of that directly influenced how I approached this implementation.

What I tried to do was bring MIDI 2.0 to TinyUSB following the same patterns as MIDI 1.0, as a natural extension of the code and architecture. Anyone familiar with midi_device.c and midi_host.c will recognize the structure. Same naming conventions (midi2d_*, midih2_*, tud_midi2_*, tuh_midi2_*), same endpoint stream patterns, same TU_VERIFY/TU_ASSERT usage, same init/open/xfer_cb/close logic.

Device driver

Implements USB-MIDI 2.0 with both Alt Settings: Alt 0 (MIDI 1.0 fallback) and Alt 1 (native UMP). Protocol negotiation (Endpoint Discovery, Config Request/Notify, Function Block Discovery) is embedded in the driver, responding automatically when the host requests it. Group Terminal Block descriptor is served via GET_DESCRIPTOR. UMP read/write does atomic framing by message type.

Host driver

Follows a reactive architecture: enumerate, detect MIDI 2.0 via bcdMSC and Alt Setting 1 presence, inform the application via callbacks. No protocol decisions are made by the driver. The application controls everything. Five weak callbacks: descriptor, mount, unmount, rx, tx.

Zero impact on existing code

Everything is guarded by #if CFG_TUD_MIDI2 / #if CFG_TUH_MIDI2 (default 0). If nobody enables these flags, the MIDI 2.0 code does not exist in the binary. I confirmed by running the existing unit tests: FIFO 26/26 PASS, USBD 5/5 PASS.

Examples

midi2_device plays Twinkle Twinkle Little Star using all MIDI 2.0 Channel Voice messages: genuine 16-bit velocity (values with no 7-bit equivalent), 32-bit CC, 32-bit pitch bend, channel pressure, per-note poly pressure, per-note management, program change with bank select, JR timestamps. The USB descriptor exposes both Alt Settings per the USB-MIDI 2.0 specification.

midi2_host receives UMP via PIO-USB and shows on a SSD1306 OLED display a boot checklist (PWR, TinyUSB init, USB bus, Device connected, Descriptor parsed, Alt Setting, Mount, Receiving) followed by decoded notes in real time.

Test results

  • Raspberry Pi Pico (RP2040) as Device -- Linux ALSA recognizes as "RP2040 MIDI 2.0", aseqdump shows correct notes
  • Raspberry Pi Pico (RP2040) as Device -- Windows MIDI Services enumerates device, shows native UMP packets
  • Waveshare RP2350-USB-A as Host -- PIO-USB GP12/GP13, SSD1306 display
  • Board-to-board: RP2040 Device sending UMP to RP2350 Host, notes on display
  • Unit tests pass (FIFO 26/26, USBD 5/5)
  • Full example build (3035 targets) zero errors

Note about the Waveshare RP2350-USB-A

During development I needed CFG_TUSB_DEBUG=2 for this specific board because it has a known hardware issue (R13 pull-up on D+ that interferes with hot-plug detection, ref: https://qsantos.fr/2025/11/21/fixing-the-rp2350-usb-a-not-working-as-usb-host/). This is board-specific, not driver-related. The Host example ships with CFG_TUSB_DEBUG=0 (default). I built this with what I had in the lab while my Adafruit Feather RP2040 USB Host is still on the way. If anyone has a Feather available and could test, I would appreciate the feedback. I believe it should work out of the box since the Feather's USB Host circuit (GP16, VBUSEN GP18) is what TinyUSB uses as reference.

Files

New files:

  • src/class/midi/midi2_device.c/h -- Device driver (604+125 lines)
  • src/class/midi/midi2_host.c/h -- Host driver (502+99 lines)
  • examples/device/midi2_device/ -- Device example
  • examples/host/midi2_host/ -- Host example with display
  • hw/bsp/rp2040/boards/waveshare_rp2350_usb_a/ -- board definition
  • test/unit-test/test/device/midi2/test_midi2_device.c
  • test/unit-test/test/host/midi2/test_midi2_host.c
  • docs/reference/class_drivers.rst

Modified files (all within #if guards):

  • src/device/usbd.c -- driver table registration
  • src/device/usbd.h -- TUD_MIDI2_DESCRIPTOR macros
  • src/host/usbh.c -- driver table registration
  • src/tusb.h -- includes
  • src/tusb_option.h -- config defaults
  • src/class/midi/midi.h -- midi2_ump_word_count helper
  • hw/bsp/rp2040/family.cmake -- sources
  • src/CMakeLists.txt -- sources

What is next

I already have some ideas for a possible next step, but I am still studying the best approach before proposing anything. When it is more mature, I will bring it in a separate PR.

For context on my MIDI 2.0 work: I have been contributing to the MIDI 2.0 ecosystem including the official MIDI Association library (AM_MIDI2.0Lib -- merged, Flex Data + per-note expression + bug fixes) and collaborating on USB-MIDI 2.0 topics in the tusb_ump project. This PR is a natural continuation of that work, bringing native MIDI 2.0 directly into TinyUSB.

Demo

Device test video:

Device test

Board-to-board test video (Device + Host):

Board-to-board test

Linux: ALSA enumeration + aseqdump
linux

Windows: Device Manager
device_manager

Windows: MIDI Services - Devices and Endpoints
windows_midi_devices_endpoints

Windows: MIDI Services - UMP Monitor
windows_midi_service

Add native USB-MIDI 2.0 Device class driver to TinyUSB. Implements the
USB-MIDI 2.0 specification with both Alt Setting 0 (MIDI 1.0 fallback)
and Alt Setting 1 (UMP native) descriptor support.

Driver features:
- UMP (Universal MIDI Packet) read/write with atomic message framing
- Protocol negotiation: Endpoint Discovery, Config Request/Notify,
  Function Block Discovery (embedded in driver)
- Group Terminal Block descriptor via GET_DESCRIPTOR
- Alt Setting switch handler with endpoint re-arm
- Static allocation, no dynamic memory, ISR-safe

Build system:
- Register midi2d_* in usbd.c driver table
- Add TUD_MIDI2_DESCRIPTOR macros to usbd.h
- Add config defaults (CFG_TUD_MIDI2_*) to tusb_option.h
- Add midi2_ump_word_count() to midi.h (shared by Device and Host)
- Add midi2_device.c/h to family.cmake and CMakeLists.txt
- Add midi2_device.h include to tusb.h

All changes guarded by #if CFG_TUD_MIDI2 (default 0). Zero impact on
existing drivers and examples.

Tested: Raspberry Pi Pico (RP2040), Linux ALSA, Windows MIDI Services
Add native USB-MIDI 2.0 Host class driver to TinyUSB. Implements
reactive architecture: enumerate, detect MIDI 2.0 capability, inform
application via callbacks.

Driver features:
- Parse both Alt Setting 0 (MIDI 1.0) and Alt Setting 1 (UMP)
- Detect bcdMSC version from descriptor
- Auto-select highest protocol (Alt 1 preferred if available)
- UMP read/write via endpoint streams
- Proper Audio Control interface skip (loop-based, following
  midi_host.c pattern)
- Endpoint open with tuh_edpt_open/tu_edpt_stream_open/clear
- usbh_driver_set_config_complete for USBH state machine
- Handle Audio Control itf_num in set_config gracefully
- 5 weak callback stubs (descriptor, mount, unmount, rx, tx)

Build system:
- Register midih2_* in usbh.c driver table
- Add midi2_host.h include to tusb.h
- Add CFG_TUH_MIDI2_LOG_LEVEL to tusb_option.h

All changes guarded by #if CFG_TUH_MIDI2 (default 0). Zero impact on
existing drivers and examples.

Tested: Waveshare RP2350-USB-A (Host) receiving UMP from Raspberry Pi
Pico (Device) via PIO-USB, board-to-board
Add unit tests for MIDI 2.0 drivers:
- Device: UMP word count (all 16 message types), descriptor macro
  validation (length, byte layout, alt settings, endpoints),
  CS endpoint subtypes, traversal integrity
- Host: UMP word count, callback struct validation, CS endpoint
  subtypes

Also add Sphinx documentation for MIDI 2.0 class drivers (Device
and Host API reference, lifecycle, configuration, examples).

Tests: 60/60 PASS (FIFO 26/26, USBD 5/5, MIDI2 Device 18/18,
MIDI2 Host 6/6, USBD internal 5/5)
Add Device example that plays Twinkle Twinkle Little Star using native
UMP format. Demonstrates all MIDI 2.0 Channel Voice message types:
16-bit velocity, 32-bit CC, 32-bit pitch bend, 32-bit channel/poly
pressure, per-note management, program change with bank select, and
JR timestamps.

USB descriptor exposes both Alt Setting 0 (MIDI 1.0) and Alt Setting 1
(UMP) per USB-MIDI 2.0 specification.

Tested on: Raspberry Pi Pico (RP2040), Linux (ALSA), Windows (MIDI
Services)
Add Host example that receives UMP from a MIDI 2.0 Device via PIO-USB
and displays received messages on a SSD1306 OLED (I2C, 128x64).

Features:
- Boot checklist on display (PWR, TinyUSB, USB bus, Device, Descriptor,
  Alt Setting UMP, Mount, Receiving)
- Decode and display all MIDI 2.0 Channel Voice messages
- PIO-USB Host on rhport 1 (GP12/GP13 for Waveshare RP2350-USB-A)
- SSD1306 display via I2C0 (GP4=SDA, GP5=SCL)

Also add board definition for Waveshare RP2350-USB-A.

Tested: Waveshare RP2350-USB-A (Host) receiving UMP from Raspberry Pi
Pico (Device) via USB cable, notes displayed on SSD1306 OLED
Copilot AI review requested due to automatic review settings March 25, 2026 03:06
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5d5c50a274

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

midih2_parse_descriptors_alt1(p_midi, desc_itf, desc_end);
}

return max_len;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Return parsed descriptor span instead of max_len

midih2_open() currently returns max_len unconditionally, but USB host enumeration expects each class driver to return only the number of descriptor bytes it consumed. In enum_parse_configuration_desc() (src/host/usbh.c), this return value is used to bind interfaces/endpoints to the class and advance parsing; returning the full remaining length makes MIDI2 claim all following interfaces in the configuration, breaking composite devices that have additional functions after MIDI2.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Fixed in 9f1001e -- midih2_open() now returns the actual parsed length. Both parsers (alt0/alt1) were refactored to return a const uint8_t* end pointer, and open() computes the delta. Follows the same pattern as midih_open() in midi_host.c.

Comment on lines +510 to +512
if (dtype != TUSB_DESC_CS_INTERFACE && dtype != TUSB_DESC_CS_ENDPOINT &&
dtype != TUSB_DESC_INTERFACE && dtype != TUSB_DESC_ENDPOINT) {
break;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Stop scanning at the next interface in midi2d_open

The descriptor-skip loop in midi2d_open() continues through any TUSB_DESC_INTERFACE, not just MIDI2 alternate-setting descriptors. During process_set_config() (src/device/usbd.c), the returned length is used to bind all enclosed interfaces/endpoints to this driver, so this logic can consume unrelated interfaces that follow MIDI2 in a composite device and prevent their own class drivers from being opened.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9f1001e. The skip loop now checks bInterfaceNumber and stops at any interface that doesn't belong to our MIDI Streaming interface, preventing consumption of unrelated interfaces in composite devices.

Comment on lines +389 to +390
// Signal USBH that configuration is complete
usbh_driver_set_config_complete(dev_addr, itf_num);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Send SET_INTERFACE before completing MIDI2 config

midih2_set_config() selects alt_setting_current and immediately completes configuration without issuing a standard SET_INTERFACE control request for alternate setting 1. USB interfaces default to alt 0, so MIDI2-capable devices can be reported as mounted while still running alt 0 semantics, which prevents proper MIDI2/UMP operation on devices that require alt 1 activation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9f1001e. midih2_set_config() now issues tuh_interface_set() to activate Alt Setting 1, with an async callback (midih2_set_interface_cb) that falls back to Alt 0 if the request fails. Mount is only reported after the control transfer completes.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds USB-MIDI 2.0 support to TinyUSB by introducing new device/host class drivers, descriptor templates, configuration defaults, documentation, examples, and unit tests—while keeping the feature opt-in via CFG_TUD_MIDI2 / CFG_TUH_MIDI2.

Changes:

  • Introduce new USB-MIDI 2.0 device and host class drivers (midi2_device.*, midi2_host.*) and register them with USBD/USBH.
  • Add USB-MIDI 2.0 descriptor templates/macros and new configuration defaults for buffers/EP sizing.
  • Add examples (device + host) plus unit tests and reference documentation for the new drivers.

Reviewed changes

Copilot reviewed 31 out of 31 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
test/unit-test/test/host/midi2/test_midi2_host.c Adds unit tests for shared UMP helper and callback struct field layout.
test/unit-test/test/device/midi2/test_midi2_device.c Adds unit tests for UMP helper and descriptor macro composition/bytes.
src/tusb_option.h Adds default config knobs for MIDI 2.0 device/host.
src/tusb.h Exposes new MIDI2 headers behind CFG_TUD_MIDI2 / CFG_TUH_MIDI2.
src/host/usbh.c Registers the new MIDI2 host class driver.
src/device/usbd.h Adds TUD_MIDI2_DESCRIPTOR and related MIDI2 descriptor templates.
src/device/usbd.c Registers the new MIDI2 device class driver.
src/class/midi/midi2_host.h Declares the MIDI2 host public API and callback structs.
src/class/midi/midi2_host.c Implements MIDI2 host enumeration + stream I/O API.
src/class/midi/midi2_device.h Declares the MIDI2 device public API and callbacks.
src/class/midi/midi2_device.c Implements MIDI2 device streaming + negotiation + GTB descriptor handling.
src/class/midi/midi.h Adds shared midi2_ump_word_count() helper.
src/CMakeLists.txt Adds MIDI2 sources to the core TinyUSB build.
hw/bsp/rp2040/family.cmake Adds MIDI2 sources to the RP2040 family build targets.
hw/bsp/rp2040/boards/waveshare_rp2350_usb_a/board.h Adds a new RP2350 USB-A host board definition stub.
hw/bsp/rp2040/boards/waveshare_rp2350_usb_a/board.cmake Adds board CMake metadata for Waveshare RP2350-USB-A.
examples/host/midi2_host/src/tusb_config.h Host example configuration enabling CFG_TUH_MIDI2.
examples/host/midi2_host/src/main.c Host example that receives/decodes UMP and displays status/logs.
examples/host/midi2_host/src/font5x7.h Adds a minimal font for the OLED example UI.
examples/host/midi2_host/src/display.h Declares SSD1306 helper API for the host example.
examples/host/midi2_host/src/display.c Implements SSD1306 text UI (checklist/log/status).
examples/host/midi2_host/CMakeLists.txt Builds the host example (RP2350 PIO-USB + I2C display).
examples/host/CMakeLists.txt Registers the new midi2_host example.
examples/device/midi2_device/src/usb_descriptors.c Device example descriptors using TUD_MIDI2_DESCRIPTOR.
examples/device/midi2_device/src/tusb_config.h Device example configuration enabling CFG_TUD_MIDI2.
examples/device/midi2_device/src/main.c Device example sending MIDI 2.0 UMP “Twinkle Twinkle” sequence.
examples/device/midi2_device/README.md Documents the new device example and testing steps.
examples/device/midi2_device/CMakeLists.txt Builds the device example.
examples/device/CMakeLists.txt Registers the new midi2_device example.
docs/reference/index.rst Adds class_drivers to the reference docs TOC.
docs/reference/class_drivers.rst Adds reference documentation for MIDI 2.0 device + host drivers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +211 to +216
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;
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the Alt 1 descriptor parser, the CS_ENDPOINT subtype is MIDI_CS_ENDPOINT_GENERAL_2_0, but the code casts it to midi_desc_cs_endpoint_t and reads bNumEmbMIDIJack. For USB-MIDI 2.0 this field represents Group Terminal Block association (bNumGrpTrmBlk and following block IDs), not embedded jack/cable count, so the parsed rx/tx counts (and callback fields) are likely incorrect. Define/parse the correct USB-MIDI 2.0 CS endpoint descriptor layout and expose the appropriate GTB/group information (or rename fields if you intend to surface GTB count).

Suggested change
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;
// For MIDI 2.0, the CS Endpoint General 2.0 descriptor uses bNumGrpTrmBlk
// at offset 4 instead of the MIDI 1.0 bNumEmbMIDIJack. Use protocol_version
// to decide which layout to apply.
if (p_midi->protocol_version == 1) {
const uint8_t *p_csep = p_desc;
// Ensure descriptor is long enough to contain bNumGrpTrmBlk (offset 4).
if (p_csep[0] >= 5) {
uint8_t num_grp_trm_blk = p_csep[4];
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;
}
}
} else {
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;
}

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9f1001e. Alt 1 parser now reads bNumGrpTrmBlk at offset 3 with a MIDI_CS_ENDPOINT_GENERAL_2_0 subtype check, instead of casting to the MIDI 1.0 midi_desc_cs_endpoint_t layout.

#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, _stridx,\
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TUD_MIDI2_DESC_ALT1_HEAD sets the Alt 1 MS Header wTotalLength to 7 (header-only) and uses _stridx as iInterface for the streaming interface. In the existing MIDI 1.0 template, wTotalLength covers the full MS descriptor set for the interface and iInterface for the streaming interface is 0. Consider updating wTotalLength to include the Alt 1 class-specific descriptors (and keep iInterface consistent with Alt 0), or splitting the parameters so Audio Control and MIDI Streaming string indices are not conflated.

Suggested change
9, TUSB_DESC_INTERFACE, (uint8_t)((_itfnum) + 1), 1, 2, TUSB_CLASS_AUDIO, AUDIO_SUBCLASS_MIDI_STREAMING, AUDIO_FUNC_PROTOCOL_CODE_UNDEF, _stridx,\
9, TUSB_DESC_INTERFACE, (uint8_t)((_itfnum) + 1), 1, 2, TUSB_CLASS_AUDIO, AUDIO_SUBCLASS_MIDI_STREAMING, AUDIO_FUNC_PROTOCOL_CODE_UNDEF, 0,\

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9f1001e. wTotalLength now uses TUD_MIDI2_DESC_ALT1_CS_LEN(_numgtbs) which accounts for the MS Header plus both CS Endpoint descriptors. iInterface for the streaming interface is set to 0, consistent with the MIDI 1.0 template.

Comment on lines +440 to +443
#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
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TUD_MIDI2_DESC_ALT1_EP(_numgtbs) sets bLength to (4 + _numgtbs) but the macro itself only emits the 4-byte CS endpoint header; the required _numgtbs bytes of bAssoGrpTrmBlkID must be appended manually. Since the macro signature suggests it is self-contained, it is easy to generate malformed descriptors when _numgtbs != 1. Consider either (a) hard-coding the template to 1 GTB and removing the parameter, or (b) extending the macro to accept/emit the GTB ID list so the output size matches the declared bLength.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9f1001e. The macro now accepts GTB ID bytes via variadic arguments and emits them inline, so bLength matches the actual output. Usage: TUD_MIDI2_DESC_ALT1_EP(_ep, _epsize, 1, 1 /* bAssoGrpTrmBlkID */).

Comment on lines +527 to +551
case TUSB_REQ_SET_INTERFACE: {
uint8_t itf_num = tu_u16_low(request->wIndex);
uint8_t alt = tu_u16_low(request->wValue);

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_edpt_stream_clear(&p_midi->ep_stream.tx);

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;
}
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

midi2d_control_xfer_cb handles SET_INTERFACE but accepts any alt value and updates p_midi->alt_setting without validation. Since this interface only defines Alt 0 and Alt 1, unsupported alternate settings should be rejected (stall/return false) to avoid leaving the driver in an undefined state. Also consider explicitly resetting negotiated/protocol state when switching back to Alt 0 if the driver relies on those fields elsewhere.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9f1001e. midi2d_control_xfer_cb() now rejects alt > 1 with return false (STALL). State reset on switch back to Alt 0 is handled by the existing alt_setting update path.


if (log_count >= LOG_LINES) {
for (int i = 0; i < LOG_LINES - 1; i++) {
strncpy(log_lines[i], log_lines[i + 1], CHARS_PER_LINE);
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When the log buffer scrolls, the strncpy() copies CHARS_PER_LINE bytes but does not ensure a trailing '\0' on the destination lines. fb_string() relies on NUL-terminated strings, so this can lead to reading past the end of log_lines[i]. After shifting, explicitly set log_lines[i][CHARS_PER_LINE] = '\0' (or use memcpy + manual terminator) for each moved line.

Suggested change
strncpy(log_lines[i], log_lines[i + 1], CHARS_PER_LINE);
strncpy(log_lines[i], log_lines[i + 1], CHARS_PER_LINE);
log_lines[i][CHARS_PER_LINE] = '\0';

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9f1001e. Added explicit NUL termination (log_lines[i][CHARS_PER_LINE] = '\0') after each strncpy() in the scroll loop.

Comment on lines +89 to +91
- **Reactive Architecture**: Auto-detects Alt Setting 1 (MIDI 2.0) capability during enumeration
- **Auto-Selection**: Automatically selects the highest available protocol (MIDI 2.0 preferred)
- **Transparent Stream Messages**: All data (UMP packets + Stream Messages) flow through callbacks
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The host driver documentation states it will "Auto-Select" the highest available protocol, which matches the current implementation (midih2_auto_select_alt_setting). However, the PR description says the driver makes no protocol decisions and the application controls everything. Please align the PR description and public API/docs with the actual behavior (either document the auto-selection as the supported behavior, or add an application-controlled selection mechanism and update the code/docs accordingly).

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9f1001e. Documentation now reflects the actual behavior: auto-selects highest protocol and issues SET_INTERFACE to activate Alt Setting 1 when MIDI 2.0 is detected. PR description aligned accordingly.

midih2_parse_descriptors_alt1(p_midi, desc_itf, desc_end);
}

return max_len;
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

midih2_open() returns max_len, which will cause the USBH configuration parser to skip the rest of the configuration descriptor (and potentially prevent other interfaces/alt settings from being opened). The host class driver open() callback is expected to return the number of bytes consumed for this interface (see midih_open()). Compute and return the parsed length (e.g., advance a local descriptor pointer until the next TUSB_DESC_INTERFACE and return the delta from desc_start).

Suggested change
return max_len;
// Compute and return number of bytes consumed for this interface.
// Walk descriptors from desc_itf until the next interface descriptor
// or the end of the provided buffer.
uint8_t const *desc_start = (uint8_t const *) desc_itf;
uint8_t const *p = desc_start;
uint8_t const *end = desc_start + max_len;
while (p + 2 <= end) {
uint8_t const len = p[0]; // bLength
uint8_t const type = p[1]; // bDescriptorType
// Stop at the next interface descriptor (but skip the first one,
// which is desc_itf itself).
if (type == TUSB_DESC_INTERFACE && p != desc_start) {
break;
}
if (len == 0 || p + len > end) {
// Malformed descriptor or would run past end; stop here.
break;
}
p += len;
}
uint16_t const parsed_len = (uint16_t) (p - desc_start);
return parsed_len;

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9f1001e -- same fix as noted in the other thread. midih2_open() now returns the actual parsed length computed from descriptor end pointers, following the same pattern as midih_open() in midi_host.c.

Comment on lines +355 to +392
// Auto-select alt setting
midih2_auto_select_alt_setting(p_midi);

// 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 = p_midi->daddr,
.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, itf_num);

return true;
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

midih2_set_config() sets alt_setting_current to 1 when a MIDI 2.0-capable device is detected, but the driver never issues a SET_INTERFACE request to actually activate alternate setting 1. As a result the device will remain in Alt 0 while callbacks report Alt 1, and UMP read/write will interpret legacy MIDI 1.0 event packets as UMP words. Consider calling tuh_interface_set(dev_addr, p_midi->bInterfaceNumber, desired_alt, ...) and only marking mounted / invoking mount_cb (and usbh_driver_set_config_complete) after that control transfer completes successfully.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 9f1001e. midih2_set_config() now calls tuh_interface_set() and only marks mounted after the control transfer completes successfully in the async callback. Falls back to Alt 0 on failure.

@sauloverissimo
Copy link
Copy Markdown
Contributor Author

OK. Working on these adjustments. 🔧

Host driver (midi2_host.c):
- midih2_open() now returns actual parsed length instead of max_len,
  preventing composite device interface conflicts
- Parsers (alt0/alt1) refactored to return const uint8_t* end pointer
  following midi_host.c switch/case pattern
- Alt 1 CS Endpoint now parses MIDI 2.0 layout (bNumGrpTrmBlk at
  offset 3 with MIDI_CS_ENDPOINT_GENERAL_2_0 subtype check) instead
  of reusing MIDI 1.0 struct (bNumEmbMIDIJack)
- midih2_set_config() now issues SET_INTERFACE control request via
  tuh_interface_set() before completing configuration. Falls back to
  alt 0 if SET_INTERFACE fails
- Extracted midih2_set_config_complete() and midih2_set_interface_cb()
  for async SET_INTERFACE handling

Device driver (midi2_device.c):
- midi2d_open() skip loop now checks bInterfaceNumber, stopping at
  interfaces that belong to other functions in composite devices
- SET_INTERFACE handler now rejects alt > 1 (returns false/stall)
- Named constants for GTB descriptor types and MIDI protocol values

Descriptor macros (usbd.h):
- TUD_MIDI2_DESC_ALT1_HEAD: iInterface set to 0 (consistent with
  Alt 0), wTotalLength now uses TUD_MIDI2_DESC_ALT1_CS_LEN to cover
  all Alt 1 class-specific descriptors
- TUD_MIDI2_DESC_ALT1_EP: now accepts GTB ID list via variadic args,
  emitting complete CS endpoint descriptor

Host example:
- CMakeLists.txt restricted to rp2040 family (display.c requires
  Pico SDK headers)
- display.c: null terminator after strncpy in log scroll

Documentation:
- class_drivers.rst updated to reflect SET_INTERFACE behavior and
  auto-select with fallback

Addresses: Codex P1 (hathach#1, hathach#2, hathach#3), Copilot (hathach#4-hathach#9)

// -- Utility Messages (MT=0x0, 32-bit) --

static inline void ump_noop(void) {

Check notice

Code scanning / CodeQL

Unused static function Note

Static function ump_noop is unreachable
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed -- this function no longer exists in the current code.

- Use UINT32_C(1) instead of 1u for bit shifts >= 16 in
  midi2_device.c to avoid shift-count-overflow on 16-bit
  platforms (MSP430)
- Add Makefiles for midi2_device and midi2_host examples with
  family guard (skip if FAMILY != rp2040). These examples
  require Pico SDK and board-specific hardware
- Restrict midi2_device CMakeLists.txt to rp2040 family
  (matching midi2_host)
@github-actions
Copy link
Copy Markdown

Size Difference Report

Because TinyUSB code size varies by port and configuration, the metrics below represent the averaged totals across all example builds.

Note: If there is no change, only one value is shown.

Changes >1% in size

file .text .rodata .bss size % diff
midi2_device.c 0 ➙ 1510 (+1510) 0 ➙ 49 (+49) 0 ➙ 728 (+728) 0 ➙ 2287 (+2287) n/a
midi2_host.c 0 ➙ 1040 (+1040) 0 0 ➙ 1248 (+1248) 0 ➙ 2288 (+2288) n/a
TOTAL 0 ➙ 2550 (+2550) 0 ➙ 49 (+49) 0 ➙ 1976 (+1976) 0 ➙ 4575 (+4575) n/a

Changes <1% in size

file .text .rodata .data .bss size % diff
dcd_rp2040.c 836 ➙ 835 (-1) 20 604 655 ➙ 654 (-1) 2115 ➙ 2113 (-2) -0.1%
rp2040_usb.c 120 75 669 ➙ 668 (-1) 4 868 ➙ 867 (-1) -0.1%
usbh.c 4649 ➙ 4646 (-3) 55 99 961 ➙ 959 (-2) 5731 ➙ 5725 (-6) -0.1%
TOTAL 5605 ➙ 5601 (-4) 150 1372 ➙ 1371 (-1) 1620 ➙ 1617 (-3) 8714 ➙ 8705 (-9) -0.1%
No changes
file .text .rodata .data .bss size % diff
audio_device.c 2897 0 1260 1627 4518 +0.0%
cdc_device.c 1252 16 1106 684 1935 +0.0%
cdc_host.c 6617 487 15 1498 8327 +0.0%
dcd_ch32_usbfs.c 1473 0 0 2444 3917 +0.0%
dcd_ch32_usbhs.c 1469 0 0 448 1917 +0.0%
dcd_ci_fs.c 1925 0 0 1290 3215 +0.0%
dcd_ci_hs.c 1759 0 0 1344 2538 +0.0%
dcd_da146xx.c 3067 0 0 144 3211 +0.0%
dcd_dwc2.c 4176 25 0 265 4465 +0.0%
dcd_eptri.c 2271 0 0 259 2530 +0.0%
dcd_ft9xx.c 3276 0 0 172 3448 +0.0%
dcd_khci.c 1953 0 0 1290 3243 +0.0%
dcd_lpc17_40.c 1474 0 0 648 1798 +0.0%
dcd_lpc_ip3511.c 1463 0 0 264 1683 +0.0%
dcd_mm32f327x_otg.c 1478 0 0 1290 2768 +0.0%
dcd_msp430x5xx.c 1798 0 0 176 1974 +0.0%
dcd_musb.c 2445 0 0 160 2605 +0.0%
dcd_nrf5x.c 2918 0 0 292 3210 +0.0%
dcd_nuc120.c 1094 0 0 78 1172 +0.0%
dcd_nuc121.c 1168 0 0 101 1269 +0.0%
dcd_nuc505.c 0 0 1531 157 1688 +0.0%
dcd_rusb2.c 2919 0 0 156 3075 +0.0%
dcd_samd.c 1034 0 0 266 1300 +0.0%
dcd_samg.c 1320 0 0 72 1392 +0.0%
dcd_stm32_fsdev.c 2558 0 0 291 2849 +0.0%
dfu_device.c 777 28 712 140 916 +0.0%
dfu_rt_device.c 157 0 134 0 157 +0.0%
dwc2_common.c 602 30 0 0 618 +0.0%
ecm_rndis_device.c 1037 0 1 2858 3896 +0.0%
ehci.c 2763 0 0 6043 7597 +0.0%
fsdev_common.c 180 0 0 0 180 +0.0%
hcd_ch32_usbfs.c 2484 0 0 498 2982 +0.0%
hcd_ci_hs.c 184 0 0 0 184 +0.0%
hcd_dwc2.c 4994 33 1 513 5540 +0.0%
hcd_khci.c 2442 0 0 449 2891 +0.0%
hcd_musb.c 3073 0 0 157 3230 +0.0%
hcd_pio_usb.c 262 0 240 0 502 +0.0%
hcd_rp2040.c 976 73 416 384 1849 +0.0%
hcd_rusb2.c 2923 0 0 245 3168 +0.0%
hcd_samd.c 2220 0 0 324 2544 +0.0%
hcd_stm32_fsdev.c 3287 0 1 420 3708 +0.0%
hid_device.c 1125 44 997 119 1244 +0.0%
hid_host.c 1240 0 0 1251 2491 +0.0%
hub.c 1384 8 8 30 1418 +0.0%
midi_device.c 1151 0 1007 623 1772 +0.0%
midi_host.c 1341 7 7 3635 4979 +0.0%
msc_device.c 2525 108 2286 547 3071 +0.0%
msc_host.c 1587 0 0 394 1982 +0.0%
mtp_device.c 1696 22 735 588 2292 +0.0%
ncm_device.c 1538 28 718 5843 7395 +0.0%
ohci.c 1940 0 0 2414 4353 +0.0%
printer_device.c 830 0 706 566 1394 +0.0%
rusb2_common.c 160 0 16 0 176 +0.0%
tusb.c 451 0 383 3 453 +0.0%
tusb_fifo.c 841 0 480 0 836 +0.0%
typec_stm32.c 820 8 2 12 842 +0.0%
usbc.c 420 2 20 166 608 +0.0%
usbd.c 3225 57 88 275 3564 +0.0%
usbd_control.c 538 0 484 79 616 +0.0%
usbtmc_device.c 2196 24 68 316 2544 +0.0%
vendor_device.c 641 0 534 565 1204 +0.0%
video_device.c 4443 5 1235 479 4914 +0.0%
TOTAL 112257 1005 15191 45352 158157 +0.0%

@sauloverissimo
Copy link
Copy Markdown
Contributor Author

Thank you for the automated review feedback. I pushed two commits addressing all of it:

9f1001e - PR review feedback:

  • midih2_open() now returns actual parsed length instead of max_len (composite device safety)
  • midi2d_open() skip loop stops at interfaces that belong to other functions
  • midih2_set_config() now issues SET_INTERFACE via tuh_interface_set() before completing mount, with fallback to alt 0 on failure
  • Alt 1 CS Endpoint parser uses MIDI 2.0 layout (bNumGrpTrmBlk) instead of reusing MIDI 1.0 struct
  • Descriptor macros fixed: wTotalLength covers all CS descriptors, EP macro emits GTB IDs
  • SET_INTERFACE handler rejects alt > 1
  • strncpy null terminator fix in display scroll
  • Docs aligned with actual behavior

b8fa96a - CI build fixes:

  • UINT32_C(1) instead of 1u for shifts on 16-bit platforms (MSP430)
  • Added Makefiles with family guard for both examples (they require Pico SDK)
  • Restricted CMakeLists to rp2040 family

All CI checks passing now.

@hathach
Copy link
Copy Markdown
Owner

hathach commented Apr 3, 2026

superb! Thank you very much, I am in the middle of reworking rp2 driver, give me a bit of time to catch up. Will review this asap.

@sauloverissimo
Copy link
Copy Markdown
Contributor Author

sauloverissimo commented Apr 3, 2026

@hathach thank you for your response! I'm available to explain and adjust anything that's needed. 🌟

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants