From 290470bda38ce75bbadcce8939435953ca483269 Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Tue, 24 Mar 2026 23:38:24 -0300 Subject: example: add MIDI 2.0 Device example (midi2_device) 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) --- examples/device/midi2_device/CMakeLists.txt | 33 ++ examples/device/midi2_device/README.md | 59 +++ examples/device/midi2_device/src/main.c | 483 +++++++++++++++++++++ examples/device/midi2_device/src/tusb_config.h | 88 ++++ examples/device/midi2_device/src/usb_descriptors.c | 142 ++++++ 5 files changed, 805 insertions(+) create mode 100644 examples/device/midi2_device/CMakeLists.txt create mode 100644 examples/device/midi2_device/README.md create mode 100644 examples/device/midi2_device/src/main.c create mode 100644 examples/device/midi2_device/src/tusb_config.h create mode 100644 examples/device/midi2_device/src/usb_descriptors.c (limited to 'examples/device/midi2_device') 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/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/src/main.c b/examples/device/midi2_device/src/main.c new file mode 100644 index 000000000..515efe07b --- /dev/null +++ b/examples/device/midi2_device/src/main.c @@ -0,0 +1,483 @@ +/* + * 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 +#include +#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_noop(void) { + ump_send_32(UMP_MT_UTILITY); +} + +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); +} + +//--------------------------------------------------------------------+ +// 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) { + (void)itf; +} + +//--------------------------------------------------------------------+ +// Initial Setup - Program Change, CC, Per-Note Management +//--------------------------------------------------------------------+ + +void send_initial_setup(void) { + ump_jr_timestamp(0x0001); + ump_program_change(0, 0, 0, true, 0, 0); + ump_cc(0, 0, 7, 0xCCCCCCCC); // Volume 80% (32-bit) + ump_cc(0, 0, 11, 0xFFFFFFFF); // Expression 100% + ump_cc(0, 0, 64, 0x00000000); // Sustain off + ump_cc(0, 0, 1, 0x20000000); // Modulation + ump_cc(0, 0, 10, 0x80000000); // Pan center + ump_per_note_mgmt(0, 0, 0, false, true); // Per-Note reset + ump_pitch_bend(0, 0, 0x80000000); // Pitch Bend center + ump_channel_pressure(0, 0, 0x00000000); + + printf("[SETUP] Piano | Vol 80%% | UMP\r\n"); +} + +//--------------------------------------------------------------------+ +// 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) ump_pitch_bend(0, 0, 0x80000000); + if (current->pressure > 0) ump_channel_pressure(0, 0, 0x00000000); + ump_note_off(0, 0, song.active_pitch, V_P, UMP_ATTR_NONE, 0); + } + + 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) { + ump_jr_timestamp((uint16_t)(now_ms & 0xFFFF)); + if (next->bend_cents != 0) { + ump_pitch_bend(0, 0, cents_to_pitch_bend(next->bend_cents)); + } + ump_note_on(0, 0, next->pitch, next->velocity, UMP_ATTR_NONE, 0); + if (next->pressure > 0) { + ump_channel_pressure(0, 0, next->pressure); + } + if (next->pressure > 0x40000000 && next->duration_ms > 500) { + ump_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(" RP2040 MIDI 2.0 Device\r\n"); + printf("===========================================\r\n"); + printf("Tempo: 120 BPM | Format: UMP 64-bit\r\n"); + printf("Song: %u notes with full MIDI 2.0 expression\r\n", + (unsigned)SONG_LENGTH); + printf("Features:\r\n"); + printf(" - 16-bit Velocity (vs 7-bit MIDI 1.0)\r\n"); + printf(" - 32-bit Control Change\r\n"); + printf(" - 32-bit Pitch Bend (vs 14-bit MIDI 1.0)\r\n"); + printf(" - 32-bit Channel Pressure\r\n"); + printf(" - 32-bit Poly Pressure (per-note)\r\n"); + printf(" - Per-Note Management (MIDI 2.0 exclusive)\r\n"); + printf(" - Program Change with Bank Select\r\n"); + printf(" - JR Timestamps\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..ce289bd4d --- /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 +#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 + "RP2040 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; +} -- cgit v1.3.1 From f8a5fc37f3472cb2e3eb584d58e9cf4588dc4329 Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Wed, 25 Mar 2026 07:29:10 -0300 Subject: fix: CI build failures on non-RP2040 platforms - 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) --- examples/device/midi2_device/CMakeLists.txt | 4 ++-- examples/device/midi2_device/Makefile | 27 +++++++++++++++++++++++++++ examples/host/midi2_host/Makefile | 27 +++++++++++++++++++++++++++ src/class/midi/midi2_device.c | 8 ++++---- 4 files changed, 60 insertions(+), 6 deletions(-) create mode 100644 examples/device/midi2_device/Makefile create mode 100644 examples/host/midi2_host/Makefile (limited to 'examples/device/midi2_device') diff --git a/examples/device/midi2_device/CMakeLists.txt b/examples/device/midi2_device/CMakeLists.txt index 295af6550..3f4a876c9 100644 --- a/examples/device/midi2_device/CMakeLists.txt +++ b/examples/device/midi2_device/CMakeLists.txt @@ -7,8 +7,8 @@ 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") +# This example requires RP2040/RP2350 (USB descriptors and config are board-specific) +if(NOT FAMILY STREQUAL "rp2040") return() endif() diff --git a/examples/device/midi2_device/Makefile b/examples/device/midi2_device/Makefile new file mode 100644 index 000000000..09f069c4f --- /dev/null +++ b/examples/device/midi2_device/Makefile @@ -0,0 +1,27 @@ +# This example requires RP2040/RP2350 (USB descriptors and config are board-specific) +ifeq (,$(findstring rp2040,$(FAMILY))) +$(info Skipping midi2_device: requires FAMILY=rp2040) +all: + @: +.DEFAULT: + @: +else + +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 + +endif diff --git a/examples/host/midi2_host/Makefile b/examples/host/midi2_host/Makefile new file mode 100644 index 000000000..2b4660516 --- /dev/null +++ b/examples/host/midi2_host/Makefile @@ -0,0 +1,27 @@ +# This example requires RP2040/RP2350 (PIO-USB, Pico SDK I2C, SSD1306 display) +ifeq (,$(findstring rp2040,$(FAMILY))) +$(info Skipping midi2_host: requires FAMILY=rp2040) +all: + @: +.DEFAULT: + @: +else + +include ../../../hw/bsp/family_support.mk + +INC += \ + src \ + +# Example source +EXAMPLE_SOURCE += \ + src/main.c \ + src/display.c \ + +SRC_C += $(addprefix $(EXAMPLE_PATH)/, $(EXAMPLE_SOURCE)) + +# Suppress pre-existing warning +CFLAGS_GCC += -Wno-type-limits + +include ../../../hw/bsp/family_rules.mk + +endif diff --git a/src/class/midi/midi2_device.c b/src/class/midi/midi2_device.c index aecbda4c5..0029737ce 100644 --- a/src/class/midi/midi2_device.c +++ b/src/class/midi/midi2_device.c @@ -158,10 +158,10 @@ static void _nego_send_endpoint_info(midi2d_interface_t* p_midi) { | ((uint32_t) STREAM_ENDPOINT_INFO << 16) | ((uint32_t) UMP_VER_MAJOR << 8) | (uint32_t) UMP_VER_MINOR; - msg[1] = (1u << 31) // Static Function Blocks flag + msg[1] = (UINT32_C(1) << 31) // Static Function Blocks flag | ((uint32_t)(CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS & 0x7F) << 24) - | (1u << 9) // MIDI 2.0 Protocol capability - | (1u << 8); // MIDI 1.0 Protocol capability + | (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); } @@ -214,7 +214,7 @@ 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) - | (1u << 15) + | (UINT32_C(1) << 15) | ((uint32_t) fb_idx << 8) | 0x02; // bDirection: bidirectional msg[1] = ((uint32_t) 0 << 24) // bFirstGroup -- cgit v1.3.1 From 43a72cb614e2fdd27788057a6f9e843d458f1ca5 Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Sat, 16 May 2026 13:00:05 -0300 Subject: midi2: device example - portable, add protocol fallback Remove the rp2040-only guard; the example now follows the audio_test pattern (only Espressif uses its own build system). Add CMakePresets.json and generic product strings. The runtime fallback picks the path that matches the host state, per message, mirroring the idea behind the UAC examples: Alt 0 USB-MIDI 1.0 32-bit Event Packets (packet_write) Alt 1 + MIDI 1.0 negotiated UMP MT 0x2 (ump_write) Alt 1 + MIDI 2.0 negotiated UMP MT 0x4 (ump_write) Build-tested on rp2040, rp2350, stm32f0/f1/f2/f3/f4/f7/g0/g4/h5/h7/l0/l4/u0/u5/wb/c0, samd2x_l2x, samd5x_e5x, same7x, nrf, imxrt, lpc17, lpc55, mcx, ra, da1469x, efm32. Ref #3571 --- examples/device/midi2_device/CMakeLists.txt | 4 +- examples/device/midi2_device/CMakePresets.json | 6 + examples/device/midi2_device/Makefile | 11 - examples/device/midi2_device/src/main.c | 299 ++++++++++++++++++--- examples/device/midi2_device/src/usb_descriptors.c | 2 +- 5 files changed, 276 insertions(+), 46 deletions(-) create mode 100644 examples/device/midi2_device/CMakePresets.json (limited to 'examples/device/midi2_device') diff --git a/examples/device/midi2_device/CMakeLists.txt b/examples/device/midi2_device/CMakeLists.txt index 3f4a876c9..295af6550 100644 --- a/examples/device/midi2_device/CMakeLists.txt +++ b/examples/device/midi2_device/CMakeLists.txt @@ -7,8 +7,8 @@ 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}) -# This example requires RP2040/RP2350 (USB descriptors and config are board-specific) -if(NOT FAMILY STREQUAL "rp2040") +# Espressif has its own cmake build system +if(FAMILY STREQUAL "espressif") return() endif() 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 index 09f069c4f..829d9da59 100644 --- a/examples/device/midi2_device/Makefile +++ b/examples/device/midi2_device/Makefile @@ -1,12 +1,3 @@ -# This example requires RP2040/RP2350 (USB descriptors and config are board-specific) -ifeq (,$(findstring rp2040,$(FAMILY))) -$(info Skipping midi2_device: requires FAMILY=rp2040) -all: - @: -.DEFAULT: - @: -else - include ../../../hw/bsp/family_support.mk INC += \ @@ -23,5 +14,3 @@ SRC_C += $(addprefix $(EXAMPLE_PATH)/, $(EXAMPLE_SOURCE)) CFLAGS_GCC += -Wno-type-limits include ../../../hw/bsp/family_rules.mk - -endif diff --git a/examples/device/midi2_device/src/main.c b/examples/device/midi2_device/src/main.c index 515efe07b..2e2488752 100644 --- a/examples/device/midi2_device/src/main.c +++ b/examples/device/midi2_device/src/main.c @@ -186,6 +186,226 @@ static inline void ump_per_note_mgmt(uint8_t group, uint8_t channel, 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 + }; + tud_midi2_packet_write(packet); +} + +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 //--------------------------------------------------------------------+ @@ -316,23 +536,42 @@ void tud_midi2_rx_cb(uint8_t itf) { (void)itf; } +// 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) { - ump_jr_timestamp(0x0001); - ump_program_change(0, 0, 0, true, 0, 0); - ump_cc(0, 0, 7, 0xCCCCCCCC); // Volume 80% (32-bit) - ump_cc(0, 0, 11, 0xFFFFFFFF); // Expression 100% - ump_cc(0, 0, 64, 0x00000000); // Sustain off - ump_cc(0, 0, 1, 0x20000000); // Modulation - ump_cc(0, 0, 10, 0x80000000); // Pan center - ump_per_note_mgmt(0, 0, 0, false, true); // Per-Note reset - ump_pitch_bend(0, 0, 0x80000000); // Pitch Bend center - ump_channel_pressure(0, 0, 0x00000000); + // 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); + } - printf("[SETUP] Piano | Vol 80%% | UMP\r\n"); + 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()); } //--------------------------------------------------------------------+ @@ -364,9 +603,9 @@ void update_song_playback(uint32_t 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) ump_pitch_bend(0, 0, 0x80000000); - if (current->pressure > 0) ump_channel_pressure(0, 0, 0x00000000); - ump_note_off(0, 0, song.active_pitch, V_P, UMP_ATTR_NONE, 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; @@ -395,16 +634,19 @@ void update_song_playback(uint32_t now_ms) { } if (next->pitch > 0) { - ump_jr_timestamp((uint16_t)(now_ms & 0xFFFF)); + // 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) { - ump_pitch_bend(0, 0, cents_to_pitch_bend(next->bend_cents)); + send_pitch_bend(0, 0, cents_to_pitch_bend(next->bend_cents)); } - ump_note_on(0, 0, next->pitch, next->velocity, UMP_ATTR_NONE, 0); + send_note_on(0, 0, next->pitch, next->velocity); if (next->pressure > 0) { - ump_channel_pressure(0, 0, next->pressure); + send_channel_pressure(0, 0, next->pressure); } if (next->pressure > 0x40000000 && next->duration_ms > 500) { - ump_poly_pressure(0, 0, next->pitch, next->pressure); + send_poly_pressure(0, 0, next->pitch, next->pressure); } song.active_pitch = next->pitch; @@ -428,20 +670,13 @@ int main(void) { board_init(); printf("\r\n"); printf("===========================================\r\n"); - printf(" RP2040 MIDI 2.0 Device\r\n"); + printf(" TinyUSB MIDI 2.0 Device\r\n"); printf("===========================================\r\n"); - printf("Tempo: 120 BPM | Format: UMP 64-bit\r\n"); - printf("Song: %u notes with full MIDI 2.0 expression\r\n", - (unsigned)SONG_LENGTH); - printf("Features:\r\n"); - printf(" - 16-bit Velocity (vs 7-bit MIDI 1.0)\r\n"); - printf(" - 32-bit Control Change\r\n"); - printf(" - 32-bit Pitch Bend (vs 14-bit MIDI 1.0)\r\n"); - printf(" - 32-bit Channel Pressure\r\n"); - printf(" - 32-bit Poly Pressure (per-note)\r\n"); - printf(" - Per-Note Management (MIDI 2.0 exclusive)\r\n"); - printf(" - Program Change with Bank Select\r\n"); - printf(" - JR Timestamps\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}; diff --git a/examples/device/midi2_device/src/usb_descriptors.c b/examples/device/midi2_device/src/usb_descriptors.c index ce289bd4d..19a43f2d8 100644 --- a/examples/device/midi2_device/src/usb_descriptors.c +++ b/examples/device/midi2_device/src/usb_descriptors.c @@ -99,7 +99,7 @@ enum { static char const *string_desc_arr[] = { (const char[]) { 0x09, 0x04 }, // 0: Language "TinyUSB", // 1: Manufacturer - "RP2040 MIDI 2.0", // 2: Product + "TinyUSB MIDI 2.0", // 2: Product NULL, // 3: Serial }; -- cgit v1.3.1 From 214de5181c7d011035f0071b3bb99c1c79206876 Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Sat, 16 May 2026 20:39:20 -0300 Subject: midi2: fix CI matrix with only.txt and skip.txt Restrict midi2_host build to MCUs with USB host support (cloned from midi_rx) and skip SAMD11 from midi2_device (ROM overflow on link). --- examples/device/midi2_device/skip.txt | 1 + examples/host/midi2_host/only.txt | 34 ++++++++++++++++++++++++++++++++++ examples/host/midi2_host/skip.txt | 1 + 3 files changed, 36 insertions(+) create mode 100644 examples/device/midi2_device/skip.txt create mode 100644 examples/host/midi2_host/only.txt create mode 100644 examples/host/midi2_host/skip.txt (limited to 'examples/device/midi2_device') 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/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 -- cgit v1.3.1 From 7c214f9d4f47cfadf18fc7e67d15cc22b1bc6f99 Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Sat, 16 May 2026 21:35:21 -0300 Subject: midi2: add sources to TINYUSB_SRC_C, skip family Make build was missing midi2_device.c/midi2_host.c from src/tinyusb.mk. Skip stm32h7s3nucleo board which exposes a pre-existing uninitialized warning in its board.h (board_init2) under Make+LTO. --- examples/device/midi2_device/skip.txt | 1 + examples/host/midi2_host/skip.txt | 1 + src/tinyusb.mk | 2 ++ 3 files changed, 4 insertions(+) (limited to 'examples/device/midi2_device') diff --git a/examples/device/midi2_device/skip.txt b/examples/device/midi2_device/skip.txt index eadb6e74a..3bc342bff 100644 --- a/examples/device/midi2_device/skip.txt +++ b/examples/device/midi2_device/skip.txt @@ -1 +1,2 @@ mcu:SAMD11 +board:stm32h7s3nucleo diff --git a/examples/host/midi2_host/skip.txt b/examples/host/midi2_host/skip.txt index 308796869..ee7ed0fc0 100644 --- a/examples/host/midi2_host/skip.txt +++ b/examples/host/midi2_host/skip.txt @@ -1 +1,2 @@ board:lpcxpresso54114 +board:stm32h7s3nucleo diff --git a/src/tinyusb.mk b/src/tinyusb.mk index e3ef35dcf..365043927 100644 --- a/src/tinyusb.mk +++ b/src/tinyusb.mk @@ -10,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 \ @@ -23,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 \ -- cgit v1.3.1 From 478e04778b8674b5ef4bf2557d305a06871ac473 Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Sat, 16 May 2026 21:44:22 -0300 Subject: midi2: remove unused ump_noop helper arm-clang fails with -Werror=unused-function; arm-gcc was silently omitting it via LTO. The helper was never called. --- examples/device/midi2_device/src/main.c | 4 ---- 1 file changed, 4 deletions(-) (limited to 'examples/device/midi2_device') diff --git a/examples/device/midi2_device/src/main.c b/examples/device/midi2_device/src/main.c index 2e2488752..cc918e198 100644 --- a/examples/device/midi2_device/src/main.c +++ b/examples/device/midi2_device/src/main.c @@ -72,10 +72,6 @@ static inline void ump_send_32(uint32_t w0) { // -- Utility Messages (MT=0x0, 32-bit) -- -static inline void ump_noop(void) { - ump_send_32(UMP_MT_UTILITY); -} - 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); -- cgit v1.3.1 From 7cf579dd49ab84163497ebc4348d208794a7e635 Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Sat, 16 May 2026 22:37:45 -0300 Subject: midi2: skip 3 more boards with same tcpp0203 BSP bug stm32h573i_dk, stm32n657nucleo and stm32n6570dk have the same pre-existing uninitialized io_ctx.GetTick in their board.h that we already skip on stm32h7s3nucleo. Trips with Make+LTO; CMake passes. --- examples/device/midi2_device/skip.txt | 6 ++++++ examples/host/midi2_host/skip.txt | 6 ++++++ 2 files changed, 12 insertions(+) (limited to 'examples/device/midi2_device') diff --git a/examples/device/midi2_device/skip.txt b/examples/device/midi2_device/skip.txt index 3bc342bff..76d694958 100644 --- a/examples/device/midi2_device/skip.txt +++ b/examples/device/midi2_device/skip.txt @@ -1,2 +1,8 @@ mcu:SAMD11 +# Skip boards exposing a pre-existing uninitialized io_ctx.GetTick in +# their board.h (called from board_init2 before TCPP0203_RegisterBusIO). +# Trips with Make+LTO; CMake passes. board:stm32h7s3nucleo +board:stm32h573i_dk +board:stm32n657nucleo +board:stm32n6570dk diff --git a/examples/host/midi2_host/skip.txt b/examples/host/midi2_host/skip.txt index ee7ed0fc0..2232e513a 100644 --- a/examples/host/midi2_host/skip.txt +++ b/examples/host/midi2_host/skip.txt @@ -1,2 +1,8 @@ board:lpcxpresso54114 +# Skip boards exposing a pre-existing uninitialized io_ctx.GetTick in +# their board.h (called from board_init2 before TCPP0203_RegisterBusIO). +# Trips with Make+LTO; CMake passes. board:stm32h7s3nucleo +board:stm32h573i_dk +board:stm32n657nucleo +board:stm32n6570dk -- cgit v1.3.1 From 2a58e8256ebe683106ef3070b5b418c8ba88c8f0 Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Sat, 16 May 2026 22:54:25 -0300 Subject: midi2: skip.txt comments --- examples/device/midi2_device/skip.txt | 3 --- examples/host/midi2_host/skip.txt | 3 --- 2 files changed, 6 deletions(-) (limited to 'examples/device/midi2_device') diff --git a/examples/device/midi2_device/skip.txt b/examples/device/midi2_device/skip.txt index 76d694958..4cabba2fb 100644 --- a/examples/device/midi2_device/skip.txt +++ b/examples/device/midi2_device/skip.txt @@ -1,7 +1,4 @@ mcu:SAMD11 -# Skip boards exposing a pre-existing uninitialized io_ctx.GetTick in -# their board.h (called from board_init2 before TCPP0203_RegisterBusIO). -# Trips with Make+LTO; CMake passes. board:stm32h7s3nucleo board:stm32h573i_dk board:stm32n657nucleo diff --git a/examples/host/midi2_host/skip.txt b/examples/host/midi2_host/skip.txt index 2232e513a..0f96db3e9 100644 --- a/examples/host/midi2_host/skip.txt +++ b/examples/host/midi2_host/skip.txt @@ -1,7 +1,4 @@ board:lpcxpresso54114 -# Skip boards exposing a pre-existing uninitialized io_ctx.GetTick in -# their board.h (called from board_init2 before TCPP0203_RegisterBusIO). -# Trips with Make+LTO; CMake passes. board:stm32h7s3nucleo board:stm32h573i_dk board:stm32n657nucleo -- cgit v1.3.1 From 116395a1b24f2cb1787b4110a8c3749a0435f1ee Mon Sep 17 00:00:00 2001 From: HiFiPhile Date: Sun, 17 May 2026 14:05:03 +0200 Subject: bsp: fix tcpp0203 lto uninit error Signed-off-by: HiFiPhile --- examples/device/midi2_device/skip.txt | 4 ---- examples/host/midi2_host/skip.txt | 4 ---- hw/bsp/stm32h5/boards/stm32h573i_dk/board.h | 5 +++++ hw/bsp/stm32h7rs/boards/stm32h7s3nucleo/board.h | 5 +++++ hw/bsp/stm32n6/boards/stm32n6570dk/board.h | 5 +++++ hw/bsp/stm32n6/boards/stm32n657nucleo/board.h | 5 +++++ 6 files changed, 20 insertions(+), 8 deletions(-) (limited to 'examples/device/midi2_device') diff --git a/examples/device/midi2_device/skip.txt b/examples/device/midi2_device/skip.txt index 4cabba2fb..eadb6e74a 100644 --- a/examples/device/midi2_device/skip.txt +++ b/examples/device/midi2_device/skip.txt @@ -1,5 +1 @@ mcu:SAMD11 -board:stm32h7s3nucleo -board:stm32h573i_dk -board:stm32n657nucleo -board:stm32n6570dk diff --git a/examples/host/midi2_host/skip.txt b/examples/host/midi2_host/skip.txt index 0f96db3e9..308796869 100644 --- a/examples/host/midi2_host/skip.txt +++ b/examples/host/midi2_host/skip.txt @@ -1,5 +1 @@ board:lpcxpresso54114 -board:stm32h7s3nucleo -board:stm32h573i_dk -board:stm32n657nucleo -board:stm32n6570dk 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/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/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, ); -- cgit v1.3.1 From b31e7cdbbedec4a3286700f3ec24bb63fd7c224f Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Sun, 17 May 2026 19:56:04 -0300 Subject: midi2: drain pattern for RX FIFO Default RX/TX buffers to EPSIZE for both device and host. Document drain-in-loop on ump_read; example device callback drains until empty. --- examples/device/midi2_device/src/main.c | 8 +++++++- src/class/midi/midi2_device.h | 12 ++++++++++-- src/class/midi/midi2_host.h | 8 ++++++++ src/tusb_option.h | 4 ++-- 4 files changed, 27 insertions(+), 5 deletions(-) (limited to 'examples/device/midi2_device') diff --git a/examples/device/midi2_device/src/main.c b/examples/device/midi2_device/src/main.c index cc918e198..d5f9bae24 100644 --- a/examples/device/midi2_device/src/main.c +++ b/examples/device/midi2_device/src/main.c @@ -529,7 +529,13 @@ void send_initial_setup(void); //--------------------------------------------------------------------+ void tud_midi2_rx_cb(uint8_t itf) { - (void)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 diff --git a/src/class/midi/midi2_device.h b/src/class/midi/midi2_device.h index b0bbd7572..6c29b8ddf 100644 --- a/src/class/midi/midi2_device.h +++ b/src/class/midi/midi2_device.h @@ -57,11 +57,11 @@ extern "C" { #endif #ifndef CFG_TUD_MIDI2_TX_BUFSIZE - #define CFG_TUD_MIDI2_TX_BUFSIZE (4 * CFG_TUD_MIDI2_TX_EPSIZE) + #define CFG_TUD_MIDI2_TX_BUFSIZE CFG_TUD_MIDI2_TX_EPSIZE #endif #ifndef CFG_TUD_MIDI2_RX_BUFSIZE - #define CFG_TUD_MIDI2_RX_BUFSIZE (4 * CFG_TUD_MIDI2_RX_EPSIZE) + #define CFG_TUD_MIDI2_RX_BUFSIZE CFG_TUD_MIDI2_RX_EPSIZE #endif #ifndef CFG_TUD_MIDI2_NUM_GROUPS @@ -119,6 +119,14 @@ 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); diff --git a/src/class/midi/midi2_host.h b/src/class/midi/midi2_host.h index d2de55270..47039eb85 100644 --- a/src/class/midi/midi2_host.h +++ b/src/class/midi/midi2_host.h @@ -77,6 +77,14 @@ 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); diff --git a/src/tusb_option.h b/src/tusb_option.h index 0114b86ee..74eb8cc06 100644 --- a/src/tusb_option.h +++ b/src/tusb_option.h @@ -824,11 +824,11 @@ #endif #ifndef CFG_TUH_MIDI2_RX_BUFSIZE - #define CFG_TUH_MIDI2_RX_BUFSIZE (4 * TUH_EPSIZE_BULK_MAX) + #define CFG_TUH_MIDI2_RX_BUFSIZE TUH_EPSIZE_BULK_MAX #endif #ifndef CFG_TUH_MIDI2_TX_BUFSIZE - #define CFG_TUH_MIDI2_TX_BUFSIZE (4 * TUH_EPSIZE_BULK_MAX) + #define CFG_TUH_MIDI2_TX_BUFSIZE TUH_EPSIZE_BULK_MAX #endif #ifndef CFG_TUH_MIDI2_LOG_LEVEL -- cgit v1.3.1 From b54c8666ec35efb48305d943c87bc48800dd7d15 Mon Sep 17 00:00:00 2001 From: HiFiPhile Date: Mon, 18 May 2026 23:08:03 +0200 Subject: update example Signed-off-by: HiFiPhile --- examples/device/midi2_device/src/main.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'examples/device/midi2_device') diff --git a/examples/device/midi2_device/src/main.c b/examples/device/midi2_device/src/main.c index d5f9bae24..62741ac41 100644 --- a/examples/device/midi2_device/src/main.c +++ b/examples/device/midi2_device/src/main.c @@ -252,7 +252,7 @@ static inline void midi1_pkt_send(uint8_t cable, uint8_t cin, (uint8_t)(((cable & 0x0F) << 4) | (cin & 0x0F)), status, data1, data2 }; - tud_midi2_packet_write(packet); + (void) tud_midi2_packet_write(packet, 1); } static inline void midi1_pkt_note_on(uint8_t cable, uint8_t channel, -- cgit v1.3.1