From bc8ce76ae800b312b4d3ef591cc42e8726e68d7f Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Tue, 24 Mar 2026 23:37:48 -0300 Subject: feat: add MIDI 2.0 Device class driver (USB-MIDI 2.0) 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 --- hw/bsp/rp2040/family.cmake | 2 + src/CMakeLists.txt | 2 + src/class/midi/midi.h | 18 ++ src/class/midi/midi2_device.c | 604 ++++++++++++++++++++++++++++++++++++++++++ src/class/midi/midi2_device.h | 125 +++++++++ src/device/usbd.c | 14 + src/device/usbd.h | 37 +++ src/tusb.h | 8 + src/tusb_option.h | 52 ++++ 9 files changed, 862 insertions(+) create mode 100644 src/class/midi/midi2_device.c create mode 100644 src/class/midi/midi2_device.h diff --git a/hw/bsp/rp2040/family.cmake b/hw/bsp/rp2040/family.cmake index 075582554..aab9a4fae 100644 --- a/hw/bsp/rp2040/family.cmake +++ b/hw/bsp/rp2040/family.cmake @@ -99,6 +99,7 @@ target_sources(tinyusb_device_base INTERFACE ${TOP}/src/class/dfu/dfu_rt_device.c ${TOP}/src/class/hid/hid_device.c ${TOP}/src/class/midi/midi_device.c + ${TOP}/src/class/midi/midi2_device.c ${TOP}/src/class/msc/msc_device.c ${TOP}/src/class/mtp/mtp_device.c ${TOP}/src/class/net/ecm_rndis_device.c @@ -121,6 +122,7 @@ target_sources(tinyusb_host_base INTERFACE ${TOP}/src/class/cdc/cdc_host.c ${TOP}/src/class/hid/hid_host.c ${TOP}/src/class/midi/midi_host.c + ${TOP}/src/class/midi/midi2_host.c ${TOP}/src/class/msc/msc_host.c ${TOP}/src/class/vendor/vendor_host.c ) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c7a5184c5..b3e05f60f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -14,6 +14,7 @@ function(tinyusb_sources_get OUTPUT_VAR) ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/dfu/dfu_rt_device.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/hid/hid_device.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/midi/midi_device.c + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/midi/midi2_device.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/msc/msc_device.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/mtp/mtp_device.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/net/ecm_rndis_device.c @@ -28,6 +29,7 @@ function(tinyusb_sources_get OUTPUT_VAR) ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/cdc/cdc_host.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/hid/hid_host.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/midi/midi_host.c + ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/midi/midi2_host.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/msc/msc_host.c ${CMAKE_CURRENT_FUNCTION_LIST_DIR}/class/vendor/vendor_host.c # typec diff --git a/src/class/midi/midi.h b/src/class/midi/midi.h index cd67640e4..8121ec016 100644 --- a/src/class/midi/midi.h +++ b/src/class/midi/midi.h @@ -185,6 +185,24 @@ typedef midi_desc_cs_endpoint_n_t(1) midi_desc_cs_endpoint_1jack_t; TU_VERIFY_STATIC(sizeof(midi_desc_cs_endpoint_1jack_t) == 4+1, "size is not correct"); +//--------------------------------------------------------------------+ +// MIDI 2.0 UMP Helpers +//--------------------------------------------------------------------+ + +// Return the number of 32-bit words for a UMP message given its Message Type +static inline uint8_t midi2_ump_word_count(uint8_t mt) { + switch (mt) { + case 0x0: case 0x1: case 0x2: case 0x6: case 0x7: + return 1; + case 0x3: case 0x4: case 0x8: case 0x9: case 0xA: + return 2; + case 0xB: case 0xC: + return 3; + default: // 0x5, 0xD, 0xE, 0xF + return 4; + } +} + //--------------------------------------------------------------------+ // For Internal Driver Use //--------------------------------------------------------------------+ diff --git a/src/class/midi/midi2_device.c b/src/class/midi/midi2_device.c new file mode 100644 index 000000000..9363aac11 --- /dev/null +++ b/src/class/midi/midi2_device.c @@ -0,0 +1,604 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * This file is part of the TinyUSB stack. + */ + +#include "tusb_option.h" + +#if CFG_TUD_ENABLED && CFG_TUD_MIDI2 + +#include + +#include "device/usbd.h" +#include "device/usbd_pvt.h" +#include "midi2_device.h" + +//--------------------------------------------------------------------+ +// Weak stubs +//--------------------------------------------------------------------+ +TU_ATTR_WEAK void tud_midi2_rx_cb(uint8_t itf) { (void) itf; } +TU_ATTR_WEAK void tud_midi2_set_itf_cb(uint8_t itf, uint8_t alt) { (void) itf; (void) alt; } +TU_ATTR_WEAK bool tud_midi2_get_req_itf_cb(uint8_t rhport, const tusb_control_request_t* request) { + (void) rhport; (void) request; return false; +} + +//--------------------------------------------------------------------+ +// UMP Stream Message Constants +//--------------------------------------------------------------------+ +// UMP Message Type for Stream messages (bits 31:28) +enum { + MT_STREAM = 0x0F, +}; + +// UMP Stream Status values (10-bit, bits 25:16) +enum { + STREAM_ENDPOINT_DISCOVERY = 0x000, + STREAM_ENDPOINT_INFO = 0x001, + STREAM_EP_NAME = 0x003, + STREAM_PROD_INSTANCE_ID = 0x004, + STREAM_CONFIG_REQUEST = 0x005, + STREAM_CONFIG_NOTIFY = 0x006, + STREAM_FB_DISCOVERY = 0x010, + STREAM_FB_INFO = 0x011, +}; + +// MIDI Protocol values (per USB-MIDI 2.0 spec) +enum { + MIDI_PROTOCOL_MIDI1 = 0x01, + MIDI_PROTOCOL_MIDI2 = 0x02, +}; + +enum { + UMP_VER_MAJOR = 1, + UMP_VER_MINOR = 1, +}; + +// Group Terminal Block descriptor types (USB-MIDI 2.0) +enum { + MIDI2_CS_GRP_TRM_BLOCK = 0x26, + MIDI2_GRP_TRM_BLOCK_HEADER = 0x01, + MIDI2_GRP_TRM_BLOCK_ENTRY = 0x02, +}; + +//--------------------------------------------------------------------+ +// MACRO CONSTANT TYPEDEF +//--------------------------------------------------------------------+ +typedef struct { + uint8_t rhport; + uint8_t itf_num; + uint8_t alt_setting; + uint8_t protocol; + bool negotiated; + + /*------------- From this point, data is not cleared by bus reset -------------*/ + struct { + tu_edpt_stream_t tx; + tu_edpt_stream_t rx; + + uint8_t rx_ff_buf[CFG_TUD_MIDI2_RX_BUFSIZE]; + uint8_t tx_ff_buf[CFG_TUD_MIDI2_TX_BUFSIZE]; + } ep_stream; +} midi2d_interface_t; + +TU_VERIFY_STATIC(CFG_TUD_MIDI2_NUM_GROUPS >= 1 && CFG_TUD_MIDI2_NUM_GROUPS <= 16, + "CFG_TUD_MIDI2_NUM_GROUPS must be 1..16"); +TU_VERIFY_STATIC(CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS >= 1 && CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS <= 32, + "CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS must be 1..32"); + +#define ITF_MEM_RESET_SIZE offsetof(midi2d_interface_t, ep_stream) + +static midi2d_interface_t _midi2d_itf[CFG_TUD_MIDI2]; + +#if CFG_TUD_EDPT_DEDICATED_HWFIFO == 0 +typedef struct { + TUD_EPBUF_DEF(epin, CFG_TUD_MIDI2_TX_EPSIZE); + TUD_EPBUF_DEF(epout, CFG_TUD_MIDI2_RX_EPSIZE); +} midi2d_epbuf_t; + +CFG_TUD_MEM_SECTION static midi2d_epbuf_t _midi2d_epbuf[CFG_TUD_MIDI2]; +#endif + +// Default Group Terminal Block descriptor (USB-MIDI 2.0 spec, Table 5-5/5-6) +static const uint8_t _default_gtb_desc[] = { + // GTB Header (5 bytes) + 5, // bLength + MIDI2_CS_GRP_TRM_BLOCK, // bDescriptorType + MIDI2_GRP_TRM_BLOCK_HEADER, // bDescriptorSubtype + U16_TO_U8S_LE(18), // wTotalLength (5 + 13 = 18) + + // GTB Entry (13 bytes) + 13, // bLength + MIDI2_CS_GRP_TRM_BLOCK, // bDescriptorType + MIDI2_GRP_TRM_BLOCK_ENTRY, // bDescriptorSubtype + 1, // bGrpTrmBlkID + 0x00, // bGrpTrmBlkType: bidirectional + 0x00, // nGroupTrm: first group (0) + CFG_TUD_MIDI2_NUM_GROUPS, // nNumGroupTrm + 0, // iBlockItem: no string + 0x00, // bMIDIProtocol: unknown/not fixed + 0, 0, // wMaxInputBandwidth: unknown + 0, 0 // wMaxOutputBandwidth: unknown +}; + +//--------------------------------------------------------------------+ +// Protocol Negotiation +//--------------------------------------------------------------------+ +static void _nego_send_ump(midi2d_interface_t* p_midi, const uint32_t* words, uint8_t count) { + tu_edpt_stream_t* ep_tx = &p_midi->ep_stream.tx; + if (!tu_edpt_stream_is_opened(ep_tx)) return; + if (tu_edpt_stream_write_available(ep_tx) < count * 4) return; + tu_edpt_stream_write(ep_tx, words, count * 4); + tu_edpt_stream_write_xfer(ep_tx); +} + +static void _nego_send_endpoint_info(midi2d_interface_t* p_midi) { + uint32_t msg[4] = {0}; + msg[0] = ((uint32_t) MT_STREAM << 28) + | ((uint32_t) STREAM_ENDPOINT_INFO << 16) + | ((uint32_t) UMP_VER_MAJOR << 8) + | (uint32_t) UMP_VER_MINOR; + msg[1] = (1u << 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 + _nego_send_ump(p_midi, msg, 4); +} + +static void _nego_send_stream_text(midi2d_interface_t* p_midi, uint16_t status, const char* str) { + if (!str || str[0] == '\0') return; + + uint16_t total_len = (uint16_t) strlen(str); + uint16_t offset = 0; + + while (offset < total_len) { + uint16_t remaining = total_len - offset; + uint8_t n = (uint8_t)((remaining > 14) ? 14 : remaining); + bool is_first = (offset == 0); + bool is_last = (remaining <= 14); + + uint8_t form; + if (is_first && is_last) form = 0; + else if (is_first) form = 1; + else if (is_last) form = 3; + else form = 2; + + uint32_t msg[4] = {0}; + msg[0] = ((uint32_t) MT_STREAM << 28) + | ((uint32_t) form << 26) + | ((uint32_t) status << 16); + + const char* p = str + offset; + if (n > 0) msg[0] |= ((uint32_t)(uint8_t) p[0] << 8); + if (n > 1) msg[0] |= (uint32_t)(uint8_t) p[1]; + for (uint8_t i = 2; i < n; i++) { + uint8_t word_idx = (uint8_t)(1 + (i - 2) / 4); + uint8_t shift = (uint8_t)(24 - ((i - 2) % 4) * 8); + msg[word_idx] |= ((uint32_t)(uint8_t) p[i] << shift); + } + + _nego_send_ump(p_midi, msg, 4); + offset += n; + } +} + +static void _nego_send_config_notify(midi2d_interface_t* p_midi, uint8_t protocol) { + uint32_t msg[4] = {0}; + msg[0] = ((uint32_t) MT_STREAM << 28) + | ((uint32_t) STREAM_CONFIG_NOTIFY << 16) + | ((uint32_t) protocol << 8); + _nego_send_ump(p_midi, msg, 4); +} + +static void _nego_send_fb_info(midi2d_interface_t* p_midi, uint8_t fb_idx) { + uint32_t msg[4] = {0}; + msg[0] = ((uint32_t) MT_STREAM << 28) + | ((uint32_t) STREAM_FB_INFO << 16) + | (1u << 15) + | ((uint32_t) fb_idx << 8) + | 0x02; // bDirection: bidirectional + msg[1] = ((uint32_t) 0 << 24) // bFirstGroup + | ((uint32_t) CFG_TUD_MIDI2_NUM_GROUPS << 16); + _nego_send_ump(p_midi, msg, 4); +} + +static void _nego_handle_stream_msg(midi2d_interface_t* p_midi, const uint32_t* words) { + uint16_t status = (words[0] >> 16) & 0x3FF; + + switch (status) { + case STREAM_ENDPOINT_DISCOVERY: + _nego_send_endpoint_info(p_midi); + _nego_send_stream_text(p_midi, STREAM_EP_NAME, CFG_TUD_MIDI2_EP_NAME); + _nego_send_stream_text(p_midi, STREAM_PROD_INSTANCE_ID, CFG_TUD_MIDI2_PRODUCT_ID); + break; + + case STREAM_CONFIG_REQUEST: { + uint8_t req_proto = (words[0] >> 8) & 0xFF; + if (req_proto == MIDI_PROTOCOL_MIDI1 || req_proto == MIDI_PROTOCOL_MIDI2) { + p_midi->protocol = req_proto; + } + _nego_send_config_notify(p_midi, p_midi->protocol); + p_midi->negotiated = true; + break; + } + + case STREAM_FB_DISCOVERY: { + uint8_t fb_idx = (words[0] >> 8) & 0xFF; + if (fb_idx == 0xFF) { + for (uint8_t f = 0; f < CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS; f++) { + _nego_send_fb_info(p_midi, f); + } + } else if (fb_idx < CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS) { + _nego_send_fb_info(p_midi, fb_idx); + } + break; + } + + default: + break; + } +} + +static void _nego_process_rx(midi2d_interface_t* p_midi) { + tu_edpt_stream_t* ep_rx = &p_midi->ep_stream.rx; + uint8_t first_byte; + + while (tu_edpt_stream_peek(ep_rx, &first_byte)) { + uint8_t mt = (first_byte >> 4) & 0x0F; + uint8_t pkt_words = midi2_ump_word_count(mt); + uint32_t pkt_bytes = (uint32_t)pkt_words * 4; + + if (mt != MT_STREAM) break; + if (tu_edpt_stream_read_available(ep_rx) < pkt_bytes) break; + + uint32_t buf[4] = {0}; + tu_edpt_stream_read(ep_rx, buf, pkt_bytes); + _nego_handle_stream_msg(p_midi, buf); + } +} + +//--------------------------------------------------------------------+ +// READ API +//--------------------------------------------------------------------+ +bool tud_midi2_n_mounted(uint8_t itf) { + TU_VERIFY(itf < CFG_TUD_MIDI2, false); + midi2d_interface_t* p_midi = &_midi2d_itf[itf]; + return tu_edpt_stream_is_opened(&p_midi->ep_stream.tx) && + tu_edpt_stream_is_opened(&p_midi->ep_stream.rx); +} + +uint32_t tud_midi2_n_available(uint8_t itf) { + TU_VERIFY(itf < CFG_TUD_MIDI2, 0); + midi2d_interface_t* p_midi = &_midi2d_itf[itf]; + return tu_edpt_stream_read_available(&p_midi->ep_stream.rx) / 4; +} + +uint32_t tud_midi2_n_ump_read(uint8_t itf, uint32_t* words, uint32_t max_words) { + TU_VERIFY(itf < CFG_TUD_MIDI2 && words != NULL && max_words > 0, 0); + midi2d_interface_t* p_midi = &_midi2d_itf[itf]; + tu_edpt_stream_t* ep_rx = &p_midi->ep_stream.rx; + + uint32_t total_read = 0; + while (total_read < max_words) { + uint8_t first_byte; + if (!tu_edpt_stream_peek(ep_rx, &first_byte)) break; + + uint8_t mt = (first_byte >> 4) & 0x0F; + uint8_t pkt_words = midi2_ump_word_count(mt); + + if (total_read + pkt_words > max_words) break; + if (tu_edpt_stream_read_available(ep_rx) < (uint32_t)pkt_words * 4) break; + + tu_edpt_stream_read(ep_rx, &words[total_read], pkt_words * 4); + total_read += pkt_words; + } + + return total_read; +} + +bool tud_midi2_n_packet_read(uint8_t itf, uint8_t packet[4]) { + TU_VERIFY(itf < CFG_TUD_MIDI2, false); + midi2d_interface_t* p_midi = &_midi2d_itf[itf]; + return 4 == tu_edpt_stream_read(&p_midi->ep_stream.rx, packet, 4); +} + +//--------------------------------------------------------------------+ +// WRITE API +//--------------------------------------------------------------------+ +uint32_t tud_midi2_n_ump_write(uint8_t itf, const uint32_t* words, uint32_t count) { + TU_VERIFY(itf < CFG_TUD_MIDI2 && words != NULL && count > 0, 0); + midi2d_interface_t* p_midi = &_midi2d_itf[itf]; + tu_edpt_stream_t* ep_tx = &p_midi->ep_stream.tx; + TU_VERIFY(tu_edpt_stream_is_opened(ep_tx), 0); + + uint32_t written = 0; + while (written < count) { + uint8_t mt = (uint8_t)((words[written] >> 28) & 0x0F); + uint8_t pkt_words = midi2_ump_word_count(mt); + + if (written + pkt_words > count) break; + if (tu_edpt_stream_write_available(ep_tx) < pkt_words * 4) break; + + tu_edpt_stream_write(ep_tx, &words[written], pkt_words * 4); + written += pkt_words; + } + + (void) tu_edpt_stream_write_xfer(ep_tx); + return written; +} + +bool tud_midi2_n_packet_write(uint8_t itf, const uint8_t packet[4]) { + TU_VERIFY(itf < CFG_TUD_MIDI2, false); + midi2d_interface_t* p_midi = &_midi2d_itf[itf]; + tu_edpt_stream_t* ep_tx = &p_midi->ep_stream.tx; + TU_VERIFY(tu_edpt_stream_is_opened(ep_tx), false); + TU_VERIFY(tu_edpt_stream_write_available(ep_tx) >= 4, false); + TU_VERIFY(tu_edpt_stream_write(ep_tx, packet, 4) > 0, false); + (void) tu_edpt_stream_write_xfer(ep_tx); + return true; +} + +//--------------------------------------------------------------------+ +// STATE GETTERS +//--------------------------------------------------------------------+ +uint8_t tud_midi2_n_alt_setting(uint8_t itf) { + TU_VERIFY(itf < CFG_TUD_MIDI2, 0); + return _midi2d_itf[itf].alt_setting; +} + +bool tud_midi2_n_negotiated(uint8_t itf) { + TU_VERIFY(itf < CFG_TUD_MIDI2, false); + return _midi2d_itf[itf].negotiated; +} + +uint8_t tud_midi2_n_protocol(uint8_t itf) { + TU_VERIFY(itf < CFG_TUD_MIDI2, 0); + return _midi2d_itf[itf].protocol; +} + +//--------------------------------------------------------------------+ +// USBD Driver API +//--------------------------------------------------------------------+ +void midi2d_init(void) { + tu_memclr(_midi2d_itf, sizeof(_midi2d_itf)); + for (uint8_t i = 0; i < CFG_TUD_MIDI2; i++) { + midi2d_interface_t* p_midi = &_midi2d_itf[i]; + p_midi->protocol = MIDI_PROTOCOL_MIDI2; + + #if CFG_TUD_EDPT_DEDICATED_HWFIFO + uint8_t* epout_buf = NULL; + uint8_t* epin_buf = NULL; + #else + midi2d_epbuf_t* p_epbuf = &_midi2d_epbuf[i]; + uint8_t* epout_buf = p_epbuf->epout; + uint8_t* epin_buf = p_epbuf->epin; + #endif + + tu_edpt_stream_init(&p_midi->ep_stream.rx, false, false, false, + p_midi->ep_stream.rx_ff_buf, CFG_TUD_MIDI2_RX_BUFSIZE, epout_buf); + tu_edpt_stream_init(&p_midi->ep_stream.tx, false, true, false, + p_midi->ep_stream.tx_ff_buf, CFG_TUD_MIDI2_TX_BUFSIZE, epin_buf); + } +} + +bool midi2d_deinit(void) { + for (uint8_t i = 0; i < CFG_TUD_MIDI2; i++) { + midi2d_interface_t* p_midi = &_midi2d_itf[i]; + tu_edpt_stream_deinit(&p_midi->ep_stream.rx); + tu_edpt_stream_deinit(&p_midi->ep_stream.tx); + } + return true; +} + +void midi2d_reset(uint8_t rhport) { + (void) rhport; + for (uint8_t i = 0; i < CFG_TUD_MIDI2; i++) { + midi2d_interface_t* p_midi = &_midi2d_itf[i]; + tu_memclr(p_midi, ITF_MEM_RESET_SIZE); + + tu_edpt_stream_clear(&p_midi->ep_stream.rx); + tu_edpt_stream_close(&p_midi->ep_stream.rx); + + tu_edpt_stream_clear(&p_midi->ep_stream.tx); + tu_edpt_stream_close(&p_midi->ep_stream.tx); + } +} + +TU_ATTR_ALWAYS_INLINE static inline uint8_t find_midi2_itf(uint8_t ep_addr) { + for (uint8_t idx = 0; idx < CFG_TUD_MIDI2; idx++) { + const midi2d_interface_t* p_midi = &_midi2d_itf[idx]; + if (ep_addr == p_midi->ep_stream.rx.ep_addr || ep_addr == p_midi->ep_stream.tx.ep_addr) { + return idx; + } + } + return TUSB_INDEX_INVALID_8; +} + +static uint8_t find_midi2_itf_by_num(uint8_t itf_num) { + for (uint8_t idx = 0; idx < CFG_TUD_MIDI2; idx++) { + if (_midi2d_itf[idx].itf_num == itf_num) return idx; + } + return TUSB_INDEX_INVALID_8; +} + +uint16_t midi2d_open(uint8_t rhport, const tusb_desc_interface_t* desc_itf, uint16_t max_len) { + const uint8_t* p_desc = (const uint8_t*) desc_itf; + const uint8_t* desc_end = p_desc + max_len; + + // 1st Interface: Audio Control v1 (optional) + if (TUSB_CLASS_AUDIO == desc_itf->bInterfaceClass && + AUDIO_SUBCLASS_CONTROL == desc_itf->bInterfaceSubClass && + AUDIO_FUNC_PROTOCOL_CODE_UNDEF == desc_itf->bInterfaceProtocol) { + p_desc = tu_desc_next(desc_itf); + while (tu_desc_in_bounds(p_desc, desc_end) && TUSB_DESC_CS_INTERFACE == tu_desc_type(p_desc)) { + p_desc = tu_desc_next(p_desc); + } + } + + // 2nd Interface: MIDI Streaming + TU_VERIFY(TUSB_DESC_INTERFACE == tu_desc_type(p_desc), 0); + const tusb_desc_interface_t* desc_midi = (const tusb_desc_interface_t*) p_desc; + + TU_VERIFY(TUSB_CLASS_AUDIO == desc_midi->bInterfaceClass && + AUDIO_SUBCLASS_MIDI_STREAMING == desc_midi->bInterfaceSubClass && + AUDIO_FUNC_PROTOCOL_CODE_UNDEF == desc_midi->bInterfaceProtocol, + 0); + + uint8_t idx = find_midi2_itf(0); + TU_ASSERT(idx < CFG_TUD_MIDI2, 0); + midi2d_interface_t* p_midi = &_midi2d_itf[idx]; + + p_midi->rhport = rhport; + p_midi->itf_num = desc_midi->bInterfaceNumber; + p_midi->alt_setting = 0; + p_midi->protocol = MIDI_PROTOCOL_MIDI2; + p_midi->negotiated = false; + + p_desc = tu_desc_next(p_desc); + + // Skip class-specific descriptors + while (tu_desc_in_bounds(p_desc, desc_end) && TUSB_DESC_CS_INTERFACE == tu_desc_type(p_desc)) { + p_desc = tu_desc_next(p_desc); + } + + // Find and open endpoint descriptors + uint8_t found_ep = 0; + while ((found_ep < desc_midi->bNumEndpoints) && tu_desc_in_bounds(p_desc, desc_end)) { + if (TUSB_DESC_ENDPOINT == tu_desc_type(p_desc)) { + const tusb_desc_endpoint_t* desc_ep = (const tusb_desc_endpoint_t*) p_desc; + TU_ASSERT(usbd_edpt_open(rhport, desc_ep), 0); + const uint8_t ep_addr = desc_ep->bEndpointAddress; + + if (tu_edpt_dir(ep_addr) == TUSB_DIR_IN) { + tu_edpt_stream_open(&p_midi->ep_stream.tx, rhport, desc_ep, CFG_TUD_MIDI2_TX_EPSIZE); + tu_edpt_stream_clear(&p_midi->ep_stream.tx); + } else { + tu_edpt_stream_open(&p_midi->ep_stream.rx, rhport, desc_ep, tu_edpt_packet_size(desc_ep)); + tu_edpt_stream_clear(&p_midi->ep_stream.rx); + TU_ASSERT(tu_edpt_stream_read_xfer(&p_midi->ep_stream.rx) > 0, 0); + } + + found_ep++; + } + + p_desc = tu_desc_next(p_desc); + } + + // Skip remaining descriptors (alt setting 1, CS endpoints, GTB) + while (tu_desc_in_bounds(p_desc, desc_end)) { + uint8_t dtype = tu_desc_type(p_desc); + if (dtype != TUSB_DESC_CS_INTERFACE && dtype != TUSB_DESC_CS_ENDPOINT && + dtype != TUSB_DESC_INTERFACE && dtype != TUSB_DESC_ENDPOINT) { + break; + } + p_desc = tu_desc_next(p_desc); + } + + return (uint16_t)(p_desc - (const uint8_t*) desc_itf); +} + +bool midi2d_control_xfer_cb(uint8_t rhport, uint8_t stage, const tusb_control_request_t* request) { + TU_LOG2("MIDI2 ctrl: stage=%u bRequest=0x%02X wValue=0x%04X wIndex=0x%04X wLength=%u\r\n", + stage, request->bRequest, request->wValue, request->wIndex, request->wLength); + + if (stage != CONTROL_STAGE_SETUP) return true; + + switch (request->bRequest) { + case TUSB_REQ_SET_INTERFACE: { + uint8_t itf_num = tu_u16_low(request->wIndex); + uint8_t alt = tu_u16_low(request->wValue); + + 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; + } + + case TUSB_REQ_GET_DESCRIPTOR: { + // wValue: descriptor type (high) | index (low) + // 0x26 = CS_GRP_TRM_BLOCK, index 0x01 + if (request->wValue == ((uint16_t)MIDI2_CS_GRP_TRM_BLOCK << 8 | 0x01)) { + if (tud_midi2_get_req_itf_cb(rhport, request)) return true; + + uint16_t len = request->wLength; + if (len > sizeof(_default_gtb_desc)) { + len = sizeof(_default_gtb_desc); + } + tud_control_xfer(rhport, request, (void*)(uintptr_t) _default_gtb_desc, len); + return true; + } + return false; + } + + default: + return false; + } +} + +bool midi2d_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { + (void) rhport; + + uint8_t idx = find_midi2_itf(ep_addr); + TU_ASSERT(idx < CFG_TUD_MIDI2); + midi2d_interface_t* p_midi = &_midi2d_itf[idx]; + + tu_edpt_stream_t* ep_rx = &p_midi->ep_stream.rx; + tu_edpt_stream_t* ep_tx = &p_midi->ep_stream.tx; + + if (ep_addr == ep_rx->ep_addr) { + if (result == XFER_RESULT_SUCCESS) { + tu_edpt_stream_read_xfer_complete(ep_rx, xferred_bytes); + if (p_midi->alt_setting == 1) { + _nego_process_rx(p_midi); + } + tud_midi2_rx_cb(idx); + } + tu_edpt_stream_read_xfer(ep_rx); + } else if (ep_addr == ep_tx->ep_addr && result == XFER_RESULT_SUCCESS) { + if (0 == tu_edpt_stream_write_xfer(ep_tx)) { + (void) tu_edpt_stream_write_zlp_if_needed(ep_tx, xferred_bytes); + } + } else { + return false; + } + + return true; +} + +#endif diff --git a/src/class/midi/midi2_device.h b/src/class/midi/midi2_device.h new file mode 100644 index 000000000..dac1f0124 --- /dev/null +++ b/src/class/midi/midi2_device.h @@ -0,0 +1,125 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * This file is part of the TinyUSB stack. + */ + +#ifndef TUSB_MIDI2_DEVICE_H_ +#define TUSB_MIDI2_DEVICE_H_ + +#include "class/audio/audio.h" +#include "midi.h" + +//--------------------------------------------------------------------+ +// Class Driver Configuration +//--------------------------------------------------------------------+ + +// Config defaults are in tusb_option.h: +// CFG_TUD_MIDI2_RX_EPSIZE, CFG_TUD_MIDI2_TX_EPSIZE, +// CFG_TUD_MIDI2_RX_BUFSIZE, CFG_TUD_MIDI2_TX_BUFSIZE, +// CFG_TUD_MIDI2_NUM_GROUPS, CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS, +// CFG_TUD_MIDI2_EP_NAME, CFG_TUD_MIDI2_PRODUCT_ID + +#ifdef __cplusplus +extern "C" { +#endif + +//--------------------------------------------------------------------+ +// Application Callback API (weak, optional) +//--------------------------------------------------------------------+ +void tud_midi2_rx_cb(uint8_t itf); +void tud_midi2_set_itf_cb(uint8_t itf, uint8_t alt); +bool tud_midi2_get_req_itf_cb(uint8_t rhport, const tusb_control_request_t* request); + +//--------------------------------------------------------------------+ +// Application API (Multiple Interfaces) +//--------------------------------------------------------------------+ + +bool tud_midi2_n_mounted(uint8_t itf); +uint32_t tud_midi2_n_available(uint8_t itf); +uint8_t tud_midi2_n_alt_setting(uint8_t itf); +bool tud_midi2_n_negotiated(uint8_t itf); +uint8_t tud_midi2_n_protocol(uint8_t itf); + +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); + +bool tud_midi2_n_packet_read(uint8_t itf, uint8_t packet[4]); +bool tud_midi2_n_packet_write(uint8_t itf, const uint8_t packet[4]); + +//--------------------------------------------------------------------+ +// Application API (Single Interface) +//--------------------------------------------------------------------+ +TU_ATTR_ALWAYS_INLINE static inline bool tud_midi2_mounted(void) { + return tud_midi2_n_mounted(0); +} + +TU_ATTR_ALWAYS_INLINE static inline uint32_t tud_midi2_available(void) { + return tud_midi2_n_available(0); +} + +TU_ATTR_ALWAYS_INLINE static inline uint8_t tud_midi2_alt_setting(void) { + return tud_midi2_n_alt_setting(0); +} + +TU_ATTR_ALWAYS_INLINE static inline bool tud_midi2_negotiated(void) { + return tud_midi2_n_negotiated(0); +} + +TU_ATTR_ALWAYS_INLINE static inline uint8_t tud_midi2_protocol(void) { + return tud_midi2_n_protocol(0); +} + +TU_ATTR_ALWAYS_INLINE static inline uint32_t +tud_midi2_ump_read(uint32_t* words, uint32_t max_words) { + return tud_midi2_n_ump_read(0, words, max_words); +} + +TU_ATTR_ALWAYS_INLINE static inline uint32_t +tud_midi2_ump_write(const uint32_t* words, uint32_t count) { + return tud_midi2_n_ump_write(0, words, count); +} + +TU_ATTR_ALWAYS_INLINE static inline bool tud_midi2_packet_read(uint8_t packet[4]) { + return tud_midi2_n_packet_read(0, packet); +} + +TU_ATTR_ALWAYS_INLINE static inline bool tud_midi2_packet_write(const uint8_t packet[4]) { + return tud_midi2_n_packet_write(0, packet); +} + +//--------------------------------------------------------------------+ +// Internal Class Driver API +//--------------------------------------------------------------------+ +void midi2d_init(void); +bool midi2d_deinit(void); +void midi2d_reset(uint8_t rhport); +uint16_t midi2d_open(uint8_t rhport, const tusb_desc_interface_t* itf_desc, uint16_t max_len); +bool midi2d_control_xfer_cb(uint8_t rhport, uint8_t stage, const tusb_control_request_t* request); +bool midi2d_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/device/usbd.c b/src/device/usbd.c index 0e58f70bf..55ad330c1 100644 --- a/src/device/usbd.c +++ b/src/device/usbd.c @@ -254,6 +254,20 @@ static const usbd_class_driver_t _usbd_driver[] = { }, #endif + #if CFG_TUD_MIDI2 + { + .name = DRIVER_NAME("MIDI2"), + .init = midi2d_init, + .deinit = midi2d_deinit, + .open = midi2d_open, + .reset = midi2d_reset, + .control_xfer_cb = midi2d_control_xfer_cb, + .xfer_cb = midi2d_xfer_cb, + .xfer_isr = NULL, + .sof = NULL + }, + #endif + #if CFG_TUD_VENDOR { .name = DRIVER_NAME("VENDOR"), diff --git a/src/device/usbd.h b/src/device/usbd.h index 473e697ac..af37eff56 100644 --- a/src/device/usbd.h +++ b/src/device/usbd.h @@ -424,6 +424,43 @@ bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_requ TUD_MIDI_DESC_EP(_epin, _epsize, 1),\ TUD_MIDI_JACKID_OUT_EMB(1) +//--------------------------------------------------------------------+ +// MIDI 2.0 Descriptor Templates (USB-MIDI 2.0) +//--------------------------------------------------------------------+ + +// Alt Setting 1: MS Interface + MS Header (bcdMSC=0x0200) +#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,\ + /* MS Header (MIDI 2.0) */\ + 7, TUSB_DESC_CS_INTERFACE, MIDI_CS_INTERFACE_HEADER, U16_TO_U8S_LE(0x0200), U16_TO_U8S_LE(7) + +// Alt Setting 1: Standard USB Endpoint (7 bytes) + CS Endpoint (subtype 0x02) +#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 + +// Total length: Alt 0 (MIDI 1.0) + Alt 1 (UMP) +#define TUD_MIDI2_DESC_LEN (TUD_MIDI_DESC_LEN + TUD_MIDI2_DESC_ALT1_HEAD_LEN + TUD_MIDI2_DESC_ALT1_EP_LEN(1) * 2) + +// Complete MIDI 2.0 descriptor with both alternate settings (single cable/GTB) +#define TUD_MIDI2_DESCRIPTOR(_itfnum, _stridx, _epout, _epin, _epsize) \ + /* Alt Setting 0 (MIDI 1.0) */\ + TUD_MIDI_DESC_HEAD(_itfnum, _stridx, 1),\ + TUD_MIDI_DESC_JACK_DESC(1, 0),\ + TUD_MIDI_DESC_EP(_epout, _epsize, 1),\ + TUD_MIDI_JACKID_IN_EMB(1),\ + TUD_MIDI_DESC_EP(_epin, _epsize, 1),\ + TUD_MIDI_JACKID_OUT_EMB(1),\ + /* Alt Setting 1 (UMP) */\ + TUD_MIDI2_DESC_ALT1_HEAD(_itfnum, _stridx),\ + TUD_MIDI2_DESC_ALT1_EP(_epout, _epsize, 1),\ + 1, /* bAssoGrpTrmBlkID = 1 */\ + TUD_MIDI2_DESC_ALT1_EP(_epin, _epsize, 1),\ + 1 /* bAssoGrpTrmBlkID = 1 */ + //--------------------------------------------------------------------+ // Audio Descriptor Templates //--------------------------------------------------------------------+ diff --git a/src/tusb.h b/src/tusb.h index c80c8433c..aa6b461e5 100644 --- a/src/tusb.h +++ b/src/tusb.h @@ -63,6 +63,10 @@ #include "class/midi/midi_host.h" #endif + #if CFG_TUH_MIDI2 + #include "class/midi/midi2_host.h" + #endif + #if CFG_TUH_VENDOR #include "class/vendor/vendor_host.h" #endif @@ -108,6 +112,10 @@ #include "class/midi/midi_device.h" #endif + #if CFG_TUD_MIDI2 + #include "class/midi/midi2_device.h" + #endif + #if CFG_TUD_VENDOR #include "class/vendor/vendor_device.h" #endif diff --git a/src/tusb_option.h b/src/tusb_option.h index 154f8e2a4..4483c2200 100644 --- a/src/tusb_option.h +++ b/src/tusb_option.h @@ -646,6 +646,42 @@ #define CFG_TUD_MIDI 0 #endif +#ifndef CFG_TUD_MIDI2 + #define CFG_TUD_MIDI2 0 +#endif + +#ifndef CFG_TUD_MIDI2_TX_BUFSIZE + #define CFG_TUD_MIDI2_TX_BUFSIZE 256 +#endif + +#ifndef CFG_TUD_MIDI2_RX_BUFSIZE + #define CFG_TUD_MIDI2_RX_BUFSIZE 256 +#endif + +#ifndef CFG_TUD_MIDI2_TX_EPSIZE + #define CFG_TUD_MIDI2_TX_EPSIZE 64 +#endif + +#ifndef CFG_TUD_MIDI2_RX_EPSIZE + #define CFG_TUD_MIDI2_RX_EPSIZE 64 +#endif + +#ifndef CFG_TUD_MIDI2_NUM_GROUPS + #define CFG_TUD_MIDI2_NUM_GROUPS 1 +#endif + +#ifndef CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS + #define CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS 1 +#endif + +#ifndef CFG_TUD_MIDI2_EP_NAME + #define CFG_TUD_MIDI2_EP_NAME "TinyUSB MIDI 2.0" +#endif + +#ifndef CFG_TUD_MIDI2_PRODUCT_ID + #define CFG_TUD_MIDI2_PRODUCT_ID "TinyUSB-MIDI2" +#endif + #ifndef CFG_TUD_VENDOR #define CFG_TUD_VENDOR 0 #endif @@ -815,6 +851,22 @@ #define CFG_TUH_MIDI 0 #endif +#ifndef CFG_TUH_MIDI2 + #define CFG_TUH_MIDI2 0 +#endif + +#ifndef CFG_TUH_MIDI2_RX_BUFSIZE + #define CFG_TUH_MIDI2_RX_BUFSIZE (4 * TUH_EPSIZE_BULK_MAX) +#endif + +#ifndef CFG_TUH_MIDI2_TX_BUFSIZE + #define CFG_TUH_MIDI2_TX_BUFSIZE (4 * TUH_EPSIZE_BULK_MAX) +#endif + +#ifndef CFG_TUH_MIDI2_LOG_LEVEL + #define CFG_TUH_MIDI2_LOG_LEVEL CFG_TUH_LOG_LEVEL +#endif + #ifndef CFG_TUH_MSC #define CFG_TUH_MSC 0 #endif -- cgit v1.3.1 From 7750e51ce1b64bd4d2f0ba6d9671303b96ba80ce Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Tue, 24 Mar 2026 23:38:02 -0300 Subject: feat: add MIDI 2.0 Host class driver (USB-MIDI 2.0) 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 --- src/class/midi/midi2_host.c | 502 ++++++++++++++++++++++++++++++++++++++++++++ src/class/midi/midi2_host.h | 99 +++++++++ src/host/usbh.c | 12 ++ 3 files changed, 613 insertions(+) create mode 100644 src/class/midi/midi2_host.c create mode 100644 src/class/midi/midi2_host.h diff --git a/src/class/midi/midi2_host.c b/src/class/midi/midi2_host.c new file mode 100644 index 000000000..beb441b56 --- /dev/null +++ b/src/class/midi/midi2_host.c @@ -0,0 +1,502 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * This file is part of the TinyUSB stack. + */ + +#include "tusb_option.h" + +#if (CFG_TUH_ENABLED && CFG_TUH_MIDI2) + +#include "host/usbh.h" +#include "host/usbh_pvt.h" +#include "midi2_host.h" + +#define TU_LOG_DRV(...) TU_LOG(CFG_TUH_MIDI2_LOG_LEVEL, __VA_ARGS__) + +//--------------------------------------------------------------------+ +// Weak stubs for application callbacks +//--------------------------------------------------------------------+ + +TU_ATTR_WEAK void tuh_midi2_descriptor_cb(uint8_t idx, const tuh_midi2_descriptor_cb_t *desc_cb_data) { + (void) idx; (void) desc_cb_data; +} + +TU_ATTR_WEAK void tuh_midi2_mount_cb(uint8_t idx, const tuh_midi2_mount_cb_t *mount_cb_data) { + (void) idx; (void) mount_cb_data; +} + +TU_ATTR_WEAK void tuh_midi2_rx_cb(uint8_t idx, uint32_t xferred_bytes) { + (void) idx; (void) xferred_bytes; +} + +TU_ATTR_WEAK void tuh_midi2_tx_cb(uint8_t idx, uint32_t xferred_bytes) { + (void) idx; (void) xferred_bytes; +} + +TU_ATTR_WEAK void tuh_midi2_umount_cb(uint8_t idx) { + (void) idx; +} + +//--------------------------------------------------------------------+ +// Internal structure and state +//--------------------------------------------------------------------+ + +typedef struct { + uint8_t daddr; + uint8_t bInterfaceNumber; + + uint8_t alt_setting_current; + + uint8_t protocol_version; + uint8_t bcdMSC_hi, bcdMSC_lo; + uint8_t rx_cable_count_alt0; + uint8_t tx_cable_count_alt0; + uint8_t rx_cable_count_alt1; + uint8_t tx_cable_count_alt1; + + struct { + tu_edpt_stream_t tx; + tu_edpt_stream_t rx; + + uint8_t rx_ff_buf[CFG_TUH_MIDI2_RX_BUFSIZE]; + uint8_t tx_ff_buf[CFG_TUH_MIDI2_TX_BUFSIZE]; + } ep_stream; + + bool mounted; +} midih2_interface_t; + +static midih2_interface_t _midi2_host[CFG_TUH_MIDI2]; + +#if CFG_TUH_EDPT_DEDICATED_HWFIFO == 0 +typedef struct { + TUH_EPBUF_DEF(tx, TUH_EPSIZE_BULK_MAX); + TUH_EPBUF_DEF(rx, TUH_EPSIZE_BULK_MAX); +} midih2_epbuf_t; + +CFG_TUH_MEM_SECTION static midih2_epbuf_t _midi2_epbuf[CFG_TUH_MIDI2]; +#endif + +//--------------------------------------------------------------------+ +// Helper functions +//--------------------------------------------------------------------+ + +static inline uint8_t find_new_midi2_index(void) { + for (uint8_t idx = 0; idx < CFG_TUH_MIDI2; idx++) { + if (_midi2_host[idx].daddr == 0) { + return idx; + } + } + return TUSB_INDEX_INVALID_8; +} + +static inline uint8_t get_idx_by_ep_addr(uint8_t daddr, uint8_t ep_addr) { + for (uint8_t idx = 0; idx < CFG_TUH_MIDI2; idx++) { + const midih2_interface_t *p_midi = &_midi2_host[idx]; + if ((p_midi->daddr == daddr) && + (ep_addr == p_midi->ep_stream.rx.ep_addr || ep_addr == p_midi->ep_stream.tx.ep_addr)) { + return idx; + } + } + return TUSB_INDEX_INVALID_8; +} + +//--------------------------------------------------------------------+ +// Descriptor parsing +//--------------------------------------------------------------------+ + +static void midih2_parse_descriptors_alt0(midih2_interface_t *p_midi, + const tusb_desc_interface_t *desc_itf, const uint8_t *desc_end) { + TU_VERIFY(AUDIO_SUBCLASS_MIDI_STREAMING == desc_itf->bInterfaceSubClass,); + + p_midi->bInterfaceNumber = desc_itf->bInterfaceNumber; + + const uint8_t *p_desc = (const uint8_t *) desc_itf; + p_desc = tu_desc_next(p_desc); + + uint8_t rx_cable_count = 0; + uint8_t tx_cable_count = 0; + + while (tu_desc_in_bounds(p_desc, desc_end)) { + if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { + break; + } + + if (tu_desc_type(p_desc) == TUSB_DESC_ENDPOINT) { + const tusb_desc_endpoint_t *p_ep = (const tusb_desc_endpoint_t *) p_desc; + + // Open endpoint and stream + TU_ASSERT(tuh_edpt_open(p_midi->daddr, p_ep),); + if (tu_edpt_dir(p_ep->bEndpointAddress) == TUSB_DIR_IN) { + tu_edpt_stream_open(&p_midi->ep_stream.rx, p_midi->daddr, p_ep, tu_edpt_packet_size(p_ep)); + tu_edpt_stream_clear(&p_midi->ep_stream.rx); + } else { + tu_edpt_stream_open(&p_midi->ep_stream.tx, p_midi->daddr, p_ep, tu_edpt_packet_size(p_ep)); + tu_edpt_stream_clear(&p_midi->ep_stream.tx); + } + + p_desc = tu_desc_next(p_desc); + + if (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) == TUSB_DESC_CS_ENDPOINT) { + const midi_desc_cs_endpoint_t *p_csep = (const midi_desc_cs_endpoint_t *) p_desc; + + if (tu_edpt_dir(p_ep->bEndpointAddress) == TUSB_DIR_OUT) { + tx_cable_count = p_csep->bNumEmbMIDIJack; + } else { + rx_cable_count = p_csep->bNumEmbMIDIJack; + } + } + } + + p_desc = tu_desc_next(p_desc); + } + + p_midi->rx_cable_count_alt0 = rx_cable_count; + p_midi->tx_cable_count_alt0 = tx_cable_count; +} + +static void midih2_parse_descriptors_alt1(midih2_interface_t *p_midi, + const tusb_desc_interface_t *desc_itf, const uint8_t *desc_end) { + TU_VERIFY(AUDIO_SUBCLASS_MIDI_STREAMING == desc_itf->bInterfaceSubClass,); + TU_VERIFY(desc_itf->bAlternateSetting == 1,); + + const uint8_t *p_desc = (const uint8_t *) desc_itf; + p_desc = tu_desc_next(p_desc); + + uint8_t rx_cable_count = 0; + uint8_t tx_cable_count = 0; + + while (tu_desc_in_bounds(p_desc, desc_end)) { + if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { + break; + } + + if (tu_desc_type(p_desc) == TUSB_DESC_CS_INTERFACE) { + if (tu_desc_subtype(p_desc) == MIDI_CS_INTERFACE_HEADER) { + const uint8_t *bcd_ptr = p_desc + 3; + p_midi->bcdMSC_lo = bcd_ptr[0]; + p_midi->bcdMSC_hi = bcd_ptr[1]; + + if (p_midi->bcdMSC_hi == 0x02) { + p_midi->protocol_version = 1; + } + } + } + + if (tu_desc_type(p_desc) == TUSB_DESC_ENDPOINT) { + const tusb_desc_endpoint_t *p_ep = (const tusb_desc_endpoint_t *) p_desc; + p_desc = tu_desc_next(p_desc); + + if (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) == TUSB_DESC_CS_ENDPOINT) { + 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; + } + } + } + + p_desc = tu_desc_next(p_desc); + } + + p_midi->rx_cable_count_alt1 = rx_cable_count; + p_midi->tx_cable_count_alt1 = tx_cable_count; +} + +//--------------------------------------------------------------------+ +// Auto-selection logic +//--------------------------------------------------------------------+ + +static void midih2_auto_select_alt_setting(midih2_interface_t *p_midi) { + p_midi->alt_setting_current = 0; + if (p_midi->protocol_version == 1) { + p_midi->alt_setting_current = 1; + } +} + +//--------------------------------------------------------------------+ +// Init/Deinit +//--------------------------------------------------------------------+ + +bool midih2_init(void) { + tu_memclr(&_midi2_host, sizeof(_midi2_host)); + for (int inst = 0; inst < CFG_TUH_MIDI2; inst++) { + midih2_interface_t *p_midi = &_midi2_host[inst]; + + #if CFG_TUH_EDPT_DEDICATED_HWFIFO + uint8_t* rx_buf = NULL; + uint8_t* tx_buf = NULL; + #else + uint8_t* rx_buf = _midi2_epbuf[inst].rx; + uint8_t* tx_buf = _midi2_epbuf[inst].tx; + #endif + + tu_edpt_stream_init(&p_midi->ep_stream.rx, true, false, false, + p_midi->ep_stream.rx_ff_buf, CFG_TUH_MIDI2_RX_BUFSIZE, rx_buf); + tu_edpt_stream_init(&p_midi->ep_stream.tx, true, true, false, + p_midi->ep_stream.tx_ff_buf, CFG_TUH_MIDI2_TX_BUFSIZE, tx_buf); + } + return true; +} + +bool midih2_deinit(void) { + for (size_t i = 0; i < CFG_TUH_MIDI2; i++) { + midih2_interface_t* p_midi = &_midi2_host[i]; + tu_edpt_stream_deinit(&p_midi->ep_stream.rx); + tu_edpt_stream_deinit(&p_midi->ep_stream.tx); + } + return true; +} + +//--------------------------------------------------------------------+ +// Class driver callbacks +//--------------------------------------------------------------------+ + +uint16_t midih2_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface_t *desc_itf, uint16_t max_len) { + (void) rhport; + + TU_VERIFY(TUSB_CLASS_AUDIO == desc_itf->bInterfaceClass, 0); + + // For Alt Setting 1, reuse existing slot for same device+interface + uint8_t idx = TUSB_INDEX_INVALID_8; + if (desc_itf->bAlternateSetting > 0) { + for (uint8_t i = 0; i < CFG_TUH_MIDI2; i++) { + if (_midi2_host[i].daddr == dev_addr && + _midi2_host[i].bInterfaceNumber == desc_itf->bInterfaceNumber) { + idx = i; + break; + } + } + } + if (idx == TUSB_INDEX_INVALID_8) { + idx = find_new_midi2_index(); + } + TU_VERIFY(idx < CFG_TUH_MIDI2, 0); + + midih2_interface_t *p_midi = &_midi2_host[idx]; + p_midi->daddr = dev_addr; + + const uint8_t *desc_start = (const uint8_t *) desc_itf; + const uint8_t *desc_end = desc_start + max_len; + + // Skip Audio Control interface and any non-MIDI-Streaming descriptors + // (following midi_host.c pattern from Ha Thach) + if (AUDIO_SUBCLASS_CONTROL == desc_itf->bInterfaceSubClass) { + const uint8_t *p_desc = tu_desc_next((const uint8_t *)desc_itf); + // Skip CS_INTERFACE header + TU_VERIFY(tu_desc_type(p_desc) == TUSB_DESC_CS_INTERFACE, 0); + p_desc = tu_desc_next(p_desc); + desc_itf = (const tusb_desc_interface_t *) p_desc; + // Skip until we find MIDI Streaming interface + while (tu_desc_in_bounds(p_desc, desc_end) && + (desc_itf->bDescriptorType != TUSB_DESC_INTERFACE || + (desc_itf->bInterfaceClass == TUSB_CLASS_AUDIO && + desc_itf->bInterfaceSubClass != AUDIO_SUBCLASS_MIDI_STREAMING))) { + p_desc = tu_desc_next(p_desc); + desc_itf = (const tusb_desc_interface_t *) p_desc; + } + TU_VERIFY(tu_desc_in_bounds(p_desc, desc_end), 0); + TU_VERIFY(TUSB_CLASS_AUDIO == desc_itf->bInterfaceClass, 0); + } + + TU_VERIFY(AUDIO_SUBCLASS_MIDI_STREAMING == desc_itf->bInterfaceSubClass, 0); + + TU_LOG_DRV("MIDI2 opening Interface %u Alt %u (addr = %u)\r\n", + desc_itf->bInterfaceNumber, desc_itf->bAlternateSetting, dev_addr); + + // Dispatch to appropriate parser based on Alt Setting + if (desc_itf->bAlternateSetting == 0) { + midih2_parse_descriptors_alt0(p_midi, desc_itf, desc_end); + } else if (desc_itf->bAlternateSetting == 1) { + midih2_parse_descriptors_alt1(p_midi, desc_itf, desc_end); + } + + return max_len; +} + +bool midih2_set_config(uint8_t dev_addr, uint8_t itf_num) { + uint8_t idx = 0; + for (idx = 0; idx < CFG_TUH_MIDI2; idx++) { + if (_midi2_host[idx].daddr == dev_addr && _midi2_host[idx].bInterfaceNumber == itf_num) { + break; + } + } + + if (idx >= CFG_TUH_MIDI2) { + // Not our interface (e.g. Audio Control) - pass through to next + usbh_driver_set_config_complete(dev_addr, itf_num); + return true; + } + + midih2_interface_t *p_midi = &_midi2_host[idx]; + + // Auto-select alt setting + midih2_auto_select_alt_setting(p_midi); + + // 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; +} + +void midih2_close(uint8_t dev_addr) { + for (uint8_t idx = 0; idx < CFG_TUH_MIDI2; idx++) { + midih2_interface_t *p_midi = &_midi2_host[idx]; + if (p_midi->daddr == dev_addr) { + TU_LOG_DRV(" MIDI2 close addr = %u index = %u\r\n", dev_addr, idx); + tu_edpt_stream_close(&p_midi->ep_stream.rx); + tu_edpt_stream_close(&p_midi->ep_stream.tx); + tuh_midi2_umount_cb(idx); + tu_memclr(p_midi, sizeof(midih2_interface_t)); + } + } +} + +bool midih2_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { + uint8_t idx = get_idx_by_ep_addr(dev_addr, ep_addr); + TU_VERIFY(idx < CFG_TUH_MIDI2); + + midih2_interface_t *p_midi = &_midi2_host[idx]; + + if (ep_addr == p_midi->ep_stream.rx.ep_addr) { + if (result == XFER_RESULT_SUCCESS && xferred_bytes > 0) { + tu_edpt_stream_read_xfer_complete(&p_midi->ep_stream.rx, xferred_bytes); + tuh_midi2_rx_cb(idx, xferred_bytes); + } + tu_edpt_stream_read_xfer(&p_midi->ep_stream.rx); + } else if (ep_addr == p_midi->ep_stream.tx.ep_addr) { + tuh_midi2_tx_cb(idx, xferred_bytes); + if (0 == tu_edpt_stream_write_xfer(&p_midi->ep_stream.tx)) { + tu_edpt_stream_write_zlp_if_needed(&p_midi->ep_stream.tx, xferred_bytes); + } + } + + return true; +} + +//--------------------------------------------------------------------+ +// Public API +//--------------------------------------------------------------------+ + +bool tuh_midi2_mounted(uint8_t idx) { + TU_VERIFY(idx < CFG_TUH_MIDI2); + return _midi2_host[idx].mounted; +} + +uint8_t tuh_midi2_get_protocol_version(uint8_t idx) { + TU_VERIFY(idx < CFG_TUH_MIDI2); + return _midi2_host[idx].protocol_version; +} + +uint8_t tuh_midi2_get_alt_setting_active(uint8_t idx) { + TU_VERIFY(idx < CFG_TUH_MIDI2); + return _midi2_host[idx].alt_setting_current; +} + +uint8_t tuh_midi2_get_cable_count(uint8_t idx) { + TU_VERIFY(idx < CFG_TUH_MIDI2); + return (_midi2_host[idx].alt_setting_current == 0) ? + _midi2_host[idx].rx_cable_count_alt0 : _midi2_host[idx].rx_cable_count_alt1; +} + +uint32_t tuh_midi2_ump_read(uint8_t idx, uint32_t* words, uint32_t max_words) { + TU_VERIFY(idx < CFG_TUH_MIDI2 && words && max_words); + + midih2_interface_t *p_midi = &_midi2_host[idx]; + tu_edpt_stream_t *ep_rx = &p_midi->ep_stream.rx; + + uint32_t n_words = 0; + for (uint32_t i = 0; i < max_words; i++) { + if (tu_edpt_stream_read_available(ep_rx) >= 4) { + tu_edpt_stream_read(ep_rx, (uint8_t *) &words[i], 4); + n_words++; + } else { + break; + } + } + + return n_words; +} + +uint32_t tuh_midi2_ump_write(uint8_t idx, const uint32_t* words, uint32_t count) { + TU_VERIFY(idx < CFG_TUH_MIDI2 && words && count); + + midih2_interface_t *p_midi = &_midi2_host[idx]; + tu_edpt_stream_t *ep_tx = &p_midi->ep_stream.tx; + + uint32_t n_words = 0; + for (uint32_t i = 0; i < count; i++) { + if (tu_edpt_stream_write_available(ep_tx) >= 4) { + tu_edpt_stream_write(ep_tx, (const uint8_t *) &words[i], 4); + n_words++; + } else { + break; + } + } + + return n_words; +} + +uint32_t tuh_midi2_write_flush(uint8_t idx) { + TU_VERIFY(idx < CFG_TUH_MIDI2); + + midih2_interface_t *p_midi = &_midi2_host[idx]; + tu_edpt_stream_t *ep_tx = &p_midi->ep_stream.tx; + + return tu_edpt_stream_write_xfer(ep_tx); +} + +#endif // CFG_TUH_ENABLED && CFG_TUH_MIDI2 diff --git a/src/class/midi/midi2_host.h b/src/class/midi/midi2_host.h new file mode 100644 index 000000000..d2de55270 --- /dev/null +++ b/src/class/midi/midi2_host.h @@ -0,0 +1,99 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * This file is part of the TinyUSB stack. + */ + +#ifndef TUSB_MIDI2_HOST_H_ +#define TUSB_MIDI2_HOST_H_ + +#include "class/audio/audio.h" +#include "midi.h" + +#ifdef __cplusplus +extern "C" { +#endif + +//--------------------------------------------------------------------+ +// Callback Type Definitions +//--------------------------------------------------------------------+ + +typedef struct { + uint8_t protocol_version; // 0 = MIDI 1.0 only, 1 = MIDI 2.0 + uint8_t bcdMSC_hi, bcdMSC_lo; // MIDI version from descriptor + uint8_t rx_cable_count; // For both alt settings (same for Alt 0 and Alt 1) + uint8_t tx_cable_count; +} tuh_midi2_descriptor_cb_t; + +typedef struct { + uint8_t daddr; + uint8_t bInterfaceNumber; + uint8_t protocol_version; // 0 = MIDI 1.0, 1 = MIDI 2.0 + uint8_t alt_setting_active; // 0 or 1 + uint8_t rx_cable_count; + uint8_t tx_cable_count; +} tuh_midi2_mount_cb_t; + +//--------------------------------------------------------------------+ +// Application Callback API (weak, optional) +//--------------------------------------------------------------------+ + +void tuh_midi2_descriptor_cb(uint8_t idx, const tuh_midi2_descriptor_cb_t *desc_cb_data); +void tuh_midi2_mount_cb(uint8_t idx, const tuh_midi2_mount_cb_t *mount_cb_data); +void tuh_midi2_rx_cb(uint8_t idx, uint32_t xferred_bytes); +void tuh_midi2_tx_cb(uint8_t idx, uint32_t xferred_bytes); +void tuh_midi2_umount_cb(uint8_t idx); + +//--------------------------------------------------------------------+ +// Application API - Query +//--------------------------------------------------------------------+ + +bool tuh_midi2_mounted(uint8_t idx); +uint8_t tuh_midi2_get_protocol_version(uint8_t idx); +uint8_t tuh_midi2_get_alt_setting_active(uint8_t idx); +uint8_t tuh_midi2_get_cable_count(uint8_t idx); + +//--------------------------------------------------------------------+ +// Application API - I/O +//--------------------------------------------------------------------+ + +uint32_t tuh_midi2_ump_read(uint8_t idx, uint32_t* words, uint32_t max_words); +uint32_t tuh_midi2_ump_write(uint8_t idx, const uint32_t* words, uint32_t count); +uint32_t tuh_midi2_write_flush(uint8_t idx); + +//--------------------------------------------------------------------+ +// Internal Class Driver API +//--------------------------------------------------------------------+ + +bool midih2_init(void); +bool midih2_deinit(void); +bool midih2_set_config(uint8_t dev_addr, uint8_t itf_num); +void midih2_close(uint8_t dev_addr); +uint16_t midih2_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface_t *desc_itf, uint16_t max_len); +bool midih2_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/host/usbh.c b/src/host/usbh.c index 490724b02..2e3c93c5e 100644 --- a/src/host/usbh.c +++ b/src/host/usbh.c @@ -278,6 +278,18 @@ static usbh_class_driver_t const usbh_class_drivers[] = { }, #endif + #if CFG_TUH_MIDI2 + { + .name = DRIVER_NAME("MIDI2"), + .init = midih2_init, + .deinit = midih2_deinit, + .open = midih2_open, + .set_config = midih2_set_config, + .xfer_cb = midih2_xfer_cb, + .close = midih2_close + }, + #endif + #if CFG_TUH_HUB { .name = DRIVER_NAME("HUB"), -- cgit v1.3.1 From 77cef83304c92402a133fc936c44d182fc99ecf0 Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Tue, 24 Mar 2026 23:38:14 -0300 Subject: test: add MIDI 2.0 Device and Host unit tests 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) --- docs/reference/class_drivers.rst | 316 +++++++++++++++++++++ docs/reference/index.rst | 1 + .../test/device/midi2/test_midi2_device.c | 263 +++++++++++++++++ test/unit-test/test/host/midi2/test_midi2_host.c | 101 +++++++ 4 files changed, 681 insertions(+) create mode 100644 docs/reference/class_drivers.rst create mode 100644 test/unit-test/test/device/midi2/test_midi2_device.c create mode 100644 test/unit-test/test/host/midi2/test_midi2_host.c diff --git a/docs/reference/class_drivers.rst b/docs/reference/class_drivers.rst new file mode 100644 index 000000000..9ed332acb --- /dev/null +++ b/docs/reference/class_drivers.rst @@ -0,0 +1,316 @@ +*************** +Class Drivers +*************** + +USB Class Drivers implement specific USB device classes (CDC, HID, MSC, MIDI, Audio, etc.) and are the main interface between the USB core and application code. + +MIDI 2.0 Device Driver +======================= + +Overview +-------- + +The MIDI 2.0 Device driver enables TinyUSB to act as a USB MIDI 2.0 device. It implements both Alt Setting 0 (MIDI 1.0 fallback) and Alt Setting 1 (native UMP) as required by the USB-MIDI 2.0 specification. + +**Key Features:** + +- **Dual Alt Settings**: Alt 0 (MIDI 1.0) and Alt 1 (UMP native) per USB-MIDI 2.0 spec +- **Protocol Negotiation**: Endpoint Discovery, Config Request/Notify, Function Block Discovery +- **Group Terminal Block**: Served via GET_DESCRIPTOR automatically +- **Atomic UMP Framing**: Read/write with correct message boundaries +- **Memory Safe**: No dynamic allocation, static instances + +Configuration +------------- + +Enable MIDI 2.0 Device support in ``tusb_config.h``: + +.. code-block:: c + + #define CFG_TUD_ENABLED 1 + #define CFG_TUD_MIDI2 1 + +Optional configuration: + +.. code-block:: c + + #define CFG_TUD_MIDI2_TX_BUFSIZE 256 + #define CFG_TUD_MIDI2_RX_BUFSIZE 256 + #define CFG_TUD_MIDI2_TX_EPSIZE 64 + #define CFG_TUD_MIDI2_RX_EPSIZE 64 + #define CFG_TUD_MIDI2_NUM_GROUPS 1 // 1..16 + #define CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS 1 // 1..32 + #define CFG_TUD_MIDI2_EP_NAME "TinyUSB MIDI 2.0" + #define CFG_TUD_MIDI2_PRODUCT_ID "TinyUSB-MIDI2" + +Public API +---------- + +Query Functions +^^^^^^^^^^^^^^^ + +.. code-block:: c + + bool tud_midi2_mounted(void); + uint32_t tud_midi2_available(void); + uint8_t tud_midi2_alt_setting(void); + bool tud_midi2_negotiated(void); + uint8_t tud_midi2_protocol(void); + +I/O Functions +^^^^^^^^^^^^^ + +.. code-block:: c + + uint32_t tud_midi2_ump_read(uint32_t* words, uint32_t max_words); + uint32_t tud_midi2_ump_write(const uint32_t* words, uint32_t count); + bool tud_midi2_packet_read(uint8_t packet[4]); + bool tud_midi2_packet_write(const uint8_t packet[4]); + +Callbacks +^^^^^^^^^ + +.. code-block:: c + + void tud_midi2_rx_cb(uint8_t itf); + void tud_midi2_set_itf_cb(uint8_t itf, uint8_t alt); + bool tud_midi2_get_req_itf_cb(uint8_t rhport, const tusb_control_request_t* request); + +MIDI 2.0 Host Driver +===================== + +Overview +-------- + +The MIDI 2.0 Host driver enables TinyUSB to enumerate and communicate with USB MIDI 2.0 devices. It implements the USB MIDI 2.0 specification, supporting both MIDI 1.0 legacy devices and modern MIDI 2.0 devices with UMP (Universal MIDI Packet) protocol. + +**Key Features:** + +- **Reactive Architecture**: Auto-detects Alt Setting 1 (MIDI 2.0) capability during enumeration +- **Auto-Selection**: Automatically selects the highest available protocol (MIDI 2.0 preferred) +- **Transparent Stream Messages**: All data (UMP packets + Stream Messages) flow through callbacks +- **Memory Safe**: No dynamic allocation, fixed-size instances per device + +Configuration +------------- + +Enable MIDI 2.0 Host support in ``tusb_config.h``: + +.. code-block:: c + + #define CFG_TUH_ENABLED 1 + #define CFG_TUH_MIDI2 4 // Number of MIDI 2.0 devices to support + +Optional buffer configuration: + +.. code-block:: c + + #define CFG_TUH_MIDI2_RX_BUFSIZE (4 * TUH_EPSIZE_BULK_MAX) + #define CFG_TUH_MIDI2_TX_BUFSIZE (4 * TUH_EPSIZE_BULK_MAX) + +Enumeration Lifecycle +--------------------- + +When a MIDI 2.0 device is connected, the host stack invokes callbacks in this order: + +.. code-block:: none + + Device Connected + | + [Host detects Alt 0 and Alt 1 descriptors] + | + tuh_midi2_descriptor_cb() <- Device detected, NOT yet ready + | + [Auto-select highest protocol] + | + tuh_midi2_mount_cb() <- Device ready to use + | + [Application can read/write data] + | + tuh_midi2_rx_cb() <- Data arrived + tuh_midi2_tx_cb() <- TX buffer space available + | + [Device disconnects] + | + tuh_midi2_umount_cb() <- Device removed + +Public API +---------- + +Query Functions +^^^^^^^^^^^^^^^ + +.. code-block:: c + + bool tuh_midi2_mounted(uint8_t idx); + uint8_t tuh_midi2_get_protocol_version(uint8_t idx); // 0=MIDI 1.0, 1=MIDI 2.0 + uint8_t tuh_midi2_get_alt_setting_active(uint8_t idx); // 0 or 1 + uint8_t tuh_midi2_get_cable_count(uint8_t idx); + +I/O Functions +^^^^^^^^^^^^^ + +Read and write UMP (Universal MIDI Packet) data: + +.. code-block:: c + + uint32_t tuh_midi2_ump_read(uint8_t idx, uint32_t* words, uint32_t max_words); + uint32_t tuh_midi2_ump_write(uint8_t idx, const uint32_t* words, uint32_t count); + uint32_t tuh_midi2_write_flush(uint8_t idx); + +Callbacks +--------- + +Application can define weak callback implementations to respond to device events. + +Descriptor Callback +^^^^^^^^^^^^^^^^^^^ + +Invoked when device is detected but not yet ready for I/O: + +.. code-block:: c + + void tuh_midi2_descriptor_cb(uint8_t idx, const tuh_midi2_descriptor_cb_t *desc_cb_data) { + printf("MIDI %s device detected\r\n", + desc_cb_data->protocol_version == 0 ? "1.0" : "2.0"); + } + +Mount Callback +^^^^^^^^^^^^^^ + +Invoked when device is ready for I/O: + +.. code-block:: c + + void tuh_midi2_mount_cb(uint8_t idx, const tuh_midi2_mount_cb_t *mount_cb_data) { + printf("Device mounted at idx=%u, protocol=%u, alt_setting=%u\r\n", + idx, mount_cb_data->protocol_version, mount_cb_data->alt_setting_active); + } + +RX Callback +^^^^^^^^^^^ + +Invoked when data arrives from device (both UMP packets and Stream Messages): + +.. code-block:: c + + void tuh_midi2_rx_cb(uint8_t idx, uint32_t xferred_bytes) { + uint32_t words[4]; + uint32_t n = tuh_midi2_ump_read(idx, words, 4); + + for (uint32_t i = 0; i < n; i++) { + uint8_t mt = (words[i] >> 28) & 0x0F; + if (mt == 0x0F) { + // Stream Message - app handles discovery, negotiation, etc. + } else { + // Regular MIDI UMP packet + } + } + } + +TX Callback +^^^^^^^^^^^ + +Invoked when TX buffer space becomes available: + +.. code-block:: c + + void tuh_midi2_tx_cb(uint8_t idx, uint32_t xferred_bytes) { + // Buffer space available for writing + } + +Unmount Callback +^^^^^^^^^^^^^^^^ + +Invoked when device is disconnected: + +.. code-block:: c + + void tuh_midi2_umount_cb(uint8_t idx) { + printf("Device at idx=%u disconnected\r\n", idx); + } + +Complete Example +---------------- + +.. code-block:: c + + #include "tusb.h" + + void tuh_midi2_mount_cb(uint8_t idx, const tuh_midi2_mount_cb_t *mount_cb_data) { + printf("MIDI 2.0 device mounted\r\n"); + } + + void tuh_midi2_rx_cb(uint8_t idx, uint32_t xferred_bytes) { + uint32_t words[4]; + uint32_t n = tuh_midi2_ump_read(idx, words, 4); + + for (uint32_t i = 0; i < n; i++) { + printf("RX: 0x%08lx\r\n", words[i]); + } + } + + void tuh_midi2_umount_cb(uint8_t idx) { + printf("MIDI 2.0 device disconnected\r\n"); + } + + int main(void) { + board_init(); + + tusb_rhport_init_t host_init = {.role = TUSB_ROLE_HOST, .speed = TUSB_SPEED_AUTO}; + tusb_init(BOARD_TUH_RHPORT, &host_init); + + while (1) { + tuh_task(); + } + } + +Architecture +------------ + +The MIDI 2.0 Host driver uses a **reactive, callback-driven architecture** that mirrors the proven patterns in TinyUSB's existing device drivers (CDC, HID, etc.): + +- **Auto-Detection**: Host automatically detects Alt Setting 1 capability +- **Auto-Selection**: Selects highest protocol available (MIDI 2.0 preferred) +- **Application Control**: App makes protocol behavior decisions via callbacks +- **Transparent I/O**: Stream Messages and UMP packets flow transparently + +Differences from MIDI 1.0 Host +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. list-table:: + :header-rows: 1 + + * - Aspect + - MIDI 1.0 Host + - MIDI 2.0 Host + * - Alt Settings + - Parses only Alt 0 + - Parses Alt 0 + Alt 1 + * - Data Format + - 4-byte MIDI packets + - UMP (32/64/128-bit) + * - Version Detection + - None + - bcdMSC from descriptor + * - GTB + - N/A + - Presence detection + * - Stream Messages + - N/A + - Transparent passthrough + * - Callbacks + - descriptor_cb, mount_cb, rx_cb, umount_cb + - descriptor_cb, mount_cb, rx_cb, tx_cb, umount_cb + * - Public API + - tuh_midi_* + - tuh_midi2_* + +Implementation Notes +-------------------- + +- All internal state is statically allocated (no dynamic allocation) +- Endpoint streams use TinyUSB's tu_edpt_stream_t for buffered I/O +- Protocol version detection via bcdMSC field +- Alt Setting is automatically selected during mount +- Compatible with all TinyUSB-supported MCU families diff --git a/docs/reference/index.rst b/docs/reference/index.rst index d3c96eeee..148e8a63b 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -9,6 +9,7 @@ Complete reference documentation for TinyUSB APIs, configuration, and supported architecture usb_concepts + class_drivers boards dependencies concurrency diff --git a/test/unit-test/test/device/midi2/test_midi2_device.c b/test/unit-test/test/device/midi2/test_midi2_device.c new file mode 100644 index 000000000..9d716d93b --- /dev/null +++ b/test/unit-test/test/device/midi2/test_midi2_device.c @@ -0,0 +1,263 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "unity.h" +#include "tusb_types.h" +#include "class/audio/audio.h" +#include "class/midi/midi.h" +#include "device/usbd.h" + +void setUp(void) {} +void tearDown(void) {} + +//--------------------------------------------------------------------+ +// UMP Word Count: all 16 message types +//--------------------------------------------------------------------+ + +void test_ump_word_count_1word_types(void) { + uint8_t types[] = {0x0, 0x1, 0x2, 0x6, 0x7}; + for (int i = 0; i < 5; i++) { + TEST_ASSERT_EQUAL(1, midi2_ump_word_count(types[i])); + } +} + +void test_ump_word_count_2word_types(void) { + uint8_t types[] = {0x3, 0x4, 0x8, 0x9, 0xA}; + for (int i = 0; i < 5; i++) { + TEST_ASSERT_EQUAL(2, midi2_ump_word_count(types[i])); + } +} + +void test_ump_word_count_3word_types(void) { + TEST_ASSERT_EQUAL(3, midi2_ump_word_count(0xB)); + TEST_ASSERT_EQUAL(3, midi2_ump_word_count(0xC)); +} + +void test_ump_word_count_4word_types(void) { + uint8_t types[] = {0x5, 0xD, 0xE, 0xF}; + for (int i = 0; i < 4; i++) { + TEST_ASSERT_EQUAL(4, midi2_ump_word_count(types[i])); + } +} + +void test_ump_word_count_covers_all_16(void) { + for (uint8_t mt = 0; mt <= 0xF; mt++) { + uint8_t wc = midi2_ump_word_count(mt); + TEST_ASSERT_TRUE(wc >= 1 && wc <= 4); + } +} + +//--------------------------------------------------------------------+ +// CS Endpoint subtypes (defined in midi.h) +//--------------------------------------------------------------------+ + +void test_cs_endpoint_subtypes(void) { + TEST_ASSERT_EQUAL(0x01, MIDI_CS_ENDPOINT_GENERAL); + TEST_ASSERT_EQUAL(0x02, MIDI_CS_ENDPOINT_GENERAL_2_0); +} + +//--------------------------------------------------------------------+ +// Descriptor macro length calculations +//--------------------------------------------------------------------+ + +void test_midi1_desc_len(void) { + TEST_ASSERT_EQUAL(TUD_MIDI_DESC_HEAD_LEN + TUD_MIDI_DESC_JACK_LEN + TUD_MIDI_DESC_EP_LEN(1) * 2, + TUD_MIDI_DESC_LEN); +} + +void test_midi2_alt1_head_len(void) { + TEST_ASSERT_EQUAL(16, TUD_MIDI2_DESC_ALT1_HEAD_LEN); +} + +void test_midi2_alt1_ep_len(void) { + // EP(7) + CS base(4) + numgtbs + TEST_ASSERT_EQUAL(12, TUD_MIDI2_DESC_ALT1_EP_LEN(1)); + TEST_ASSERT_EQUAL(13, TUD_MIDI2_DESC_ALT1_EP_LEN(2)); + TEST_ASSERT_EQUAL(18, TUD_MIDI2_DESC_ALT1_EP_LEN(7)); +} + +void test_midi2_desc_len(void) { + int expected = TUD_MIDI_DESC_LEN + TUD_MIDI2_DESC_ALT1_HEAD_LEN + TUD_MIDI2_DESC_ALT1_EP_LEN(1) * 2; + TEST_ASSERT_EQUAL(expected, TUD_MIDI2_DESC_LEN); +} + +void test_midi2_desc_len_greater_than_midi1(void) { + TEST_ASSERT_TRUE(TUD_MIDI2_DESC_LEN > TUD_MIDI_DESC_LEN); +} + +//--------------------------------------------------------------------+ +// Descriptor macro byte validation +//--------------------------------------------------------------------+ + +void test_midi2_descriptor_bytes(void) { + uint8_t desc[] = { TUD_MIDI2_DESCRIPTOR(0, 0, 0x01, 0x81, 64) }; + + TEST_ASSERT_EQUAL(TUD_MIDI2_DESC_LEN, sizeof(desc)); + + // First byte: Audio Control Interface descriptor length = 9 + TEST_ASSERT_EQUAL(9, desc[0]); + TEST_ASSERT_EQUAL(TUSB_DESC_INTERFACE, desc[1]); + TEST_ASSERT_EQUAL(0, desc[2]); + + // Find Alt Setting 1 by scanning + int alt1_offset = -1; + int pos = 0; + while (pos < (int)sizeof(desc)) { + if (desc[pos + 1] == TUSB_DESC_INTERFACE && desc[pos + 3] == 1) { + alt1_offset = pos; + break; + } + pos += desc[pos]; + } + + TEST_ASSERT_TRUE_MESSAGE(alt1_offset >= 0, "Alt Setting 1 interface not found"); + + TEST_ASSERT_EQUAL(9, desc[alt1_offset]); + TEST_ASSERT_EQUAL(TUSB_DESC_INTERFACE, desc[alt1_offset + 1]); + TEST_ASSERT_EQUAL(1, desc[alt1_offset + 2]); // bInterfaceNumber + TEST_ASSERT_EQUAL(1, desc[alt1_offset + 3]); // bAlternateSetting + TEST_ASSERT_EQUAL(2, desc[alt1_offset + 4]); // bNumEndpoints + TEST_ASSERT_EQUAL(TUSB_CLASS_AUDIO, desc[alt1_offset + 5]); + + // MS Header after Alt Setting 1 interface: bcdMSC = 0x0200 + int ms2_offset = alt1_offset + 9; + TEST_ASSERT_EQUAL(7, desc[ms2_offset]); + TEST_ASSERT_EQUAL(TUSB_DESC_CS_INTERFACE, desc[ms2_offset + 1]); + TEST_ASSERT_EQUAL(MIDI_CS_INTERFACE_HEADER, desc[ms2_offset + 2]); + TEST_ASSERT_EQUAL(0x00, desc[ms2_offset + 3]); + TEST_ASSERT_EQUAL(0x02, desc[ms2_offset + 4]); +} + +void test_midi2_descriptor_alt1_cs_endpoint_subtype(void) { + uint8_t desc[] = { TUD_MIDI2_DESCRIPTOR(0, 0, 0x01, 0x81, 64) }; + + int cs_ep_count = 0; + int pos = 0; + while (pos < (int)sizeof(desc)) { + if (desc[pos + 1] == TUSB_DESC_CS_ENDPOINT && + desc[pos + 2] == MIDI_CS_ENDPOINT_GENERAL_2_0) { + cs_ep_count++; + TEST_ASSERT_EQUAL(1, desc[pos + 3]); + } + pos += desc[pos]; + } + TEST_ASSERT_EQUAL(2, cs_ep_count); +} + +void test_midi2_descriptor_has_both_alt_settings(void) { + uint8_t desc[] = { TUD_MIDI2_DESCRIPTOR(0, 0, 0x01, 0x81, 64) }; + + int alt0_count = 0; + int alt1_count = 0; + int pos = 0; + while (pos < (int)sizeof(desc)) { + if (desc[pos + 1] == TUSB_DESC_INTERFACE) { + if (desc[pos + 3] == 0) alt0_count++; + if (desc[pos + 3] == 1) alt1_count++; + } + pos += desc[pos]; + } + TEST_ASSERT_TRUE(alt0_count >= 2); + TEST_ASSERT_EQUAL(1, alt1_count); +} + +void test_midi2_descriptor_endpoint_addresses(void) { + uint8_t desc[] = { TUD_MIDI2_DESCRIPTOR(0, 0, 0x02, 0x82, 64) }; + + int ep_out_count = 0; + int ep_in_count = 0; + int pos = 0; + while (pos < (int)sizeof(desc)) { + if (desc[pos + 1] == TUSB_DESC_ENDPOINT) { + uint8_t ep_addr = desc[pos + 2]; + if (ep_addr == 0x02) ep_out_count++; + if (ep_addr == 0x82) ep_in_count++; + TEST_ASSERT_EQUAL(TUSB_XFER_BULK, desc[pos + 3]); + TEST_ASSERT_EQUAL(64, desc[pos + 4]); + TEST_ASSERT_EQUAL(0, desc[pos + 5]); + } + pos += desc[pos]; + } + TEST_ASSERT_EQUAL(2, ep_out_count); + TEST_ASSERT_EQUAL(2, ep_in_count); +} + +void test_midi2_descriptor_nonzero_itfnum(void) { + uint8_t desc[] = { TUD_MIDI2_DESCRIPTOR(2, 0, 0x03, 0x83, 64) }; + + TEST_ASSERT_EQUAL(2, desc[2]); + + int pos = desc[0]; + while (pos < (int)sizeof(desc)) { + if (desc[pos + 1] == TUSB_DESC_INTERFACE) { + TEST_ASSERT_EQUAL(3, desc[pos + 2]); + break; + } + pos += desc[pos]; + } +} + +//--------------------------------------------------------------------+ +// Descriptor traversal integrity +//--------------------------------------------------------------------+ + +void test_midi2_descriptor_no_zero_length(void) { + uint8_t desc[] = { TUD_MIDI2_DESCRIPTOR(0, 0, 0x01, 0x81, 64) }; + + int pos = 0; + int desc_count = 0; + while (pos < (int)sizeof(desc)) { + TEST_ASSERT_TRUE_MESSAGE(desc[pos] > 0, "Zero-length descriptor found"); + TEST_ASSERT_TRUE_MESSAGE(desc[pos] <= (int)sizeof(desc) - pos, + "Descriptor length exceeds remaining bytes"); + pos += desc[pos]; + desc_count++; + } + TEST_ASSERT_EQUAL((int)sizeof(desc), pos); + TEST_ASSERT_TRUE(desc_count > 5); +} + +void test_midi2_descriptor_valid_types(void) { + uint8_t desc[] = { TUD_MIDI2_DESCRIPTOR(0, 0, 0x01, 0x81, 64) }; + + int pos = 0; + while (pos < (int)sizeof(desc)) { + uint8_t dtype = desc[pos + 1]; + bool valid = (dtype == TUSB_DESC_INTERFACE || + dtype == TUSB_DESC_ENDPOINT || + dtype == TUSB_DESC_CS_INTERFACE || + dtype == TUSB_DESC_CS_ENDPOINT); + TEST_ASSERT_TRUE_MESSAGE(valid, "Invalid descriptor type found"); + pos += desc[pos]; + } +} + +//--------------------------------------------------------------------+ +// Edge cases +//--------------------------------------------------------------------+ + +void test_ump_word_count_with_values_beyond_0xf(void) { + TEST_ASSERT_EQUAL(4, midi2_ump_word_count(0x10)); + TEST_ASSERT_EQUAL(4, midi2_ump_word_count(0xFF)); +} diff --git a/test/unit-test/test/host/midi2/test_midi2_host.c b/test/unit-test/test/host/midi2/test_midi2_host.c new file mode 100644 index 000000000..8ad77c14e --- /dev/null +++ b/test/unit-test/test/host/midi2/test_midi2_host.c @@ -0,0 +1,101 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "unity.h" +#include "tusb_option.h" +#include "class/midi/midi.h" +#include "class/midi/midi2_host.h" + +void setUp(void) {} +void tearDown(void) {} + +//--------------------------------------------------------------------+ +// UMP Word Count (shared helper, defined in midi.h) +//--------------------------------------------------------------------+ + +void test_midi2_host_ump_word_count_1word(void) { + uint8_t types[] = {0x0, 0x1, 0x2, 0x6, 0x7}; + for (int i = 0; i < 5; i++) { + TEST_ASSERT_EQUAL(1, midi2_ump_word_count(types[i])); + } +} + +void test_midi2_host_ump_word_count_2word(void) { + uint8_t types[] = {0x3, 0x4, 0x8, 0x9, 0xA}; + for (int i = 0; i < 5; i++) { + TEST_ASSERT_EQUAL(2, midi2_ump_word_count(types[i])); + } +} + +void test_midi2_host_ump_word_count_4word(void) { + uint8_t types[] = {0x5, 0xD, 0xE, 0xF}; + for (int i = 0; i < 4; i++) { + TEST_ASSERT_EQUAL(4, midi2_ump_word_count(types[i])); + } +} + +//--------------------------------------------------------------------+ +// Callback struct field validation +//--------------------------------------------------------------------+ + +void test_midi2_descriptor_cb_struct_fields(void) { + tuh_midi2_descriptor_cb_t desc = { + .protocol_version = 1, + .bcdMSC_hi = 0x02, + .bcdMSC_lo = 0x00, + .rx_cable_count = 1, + .tx_cable_count = 1 + }; + TEST_ASSERT_EQUAL(1, desc.protocol_version); + TEST_ASSERT_EQUAL(0x02, desc.bcdMSC_hi); + TEST_ASSERT_EQUAL(0x00, desc.bcdMSC_lo); + TEST_ASSERT_EQUAL(1, desc.rx_cable_count); + TEST_ASSERT_EQUAL(1, desc.tx_cable_count); +} + +void test_midi2_mount_cb_struct_fields(void) { + tuh_midi2_mount_cb_t mount = { + .daddr = 1, + .bInterfaceNumber = 0, + .protocol_version = 1, + .alt_setting_active = 1, + .rx_cable_count = 2, + .tx_cable_count = 2 + }; + TEST_ASSERT_EQUAL(1, mount.daddr); + TEST_ASSERT_EQUAL(0, mount.bInterfaceNumber); + TEST_ASSERT_EQUAL(1, mount.protocol_version); + TEST_ASSERT_EQUAL(1, mount.alt_setting_active); + TEST_ASSERT_EQUAL(2, mount.rx_cable_count); + TEST_ASSERT_EQUAL(2, mount.tx_cable_count); +} + +//--------------------------------------------------------------------+ +// CS Endpoint subtypes +//--------------------------------------------------------------------+ + +void test_midi2_host_cs_endpoint_subtypes(void) { + TEST_ASSERT_EQUAL(0x01, MIDI_CS_ENDPOINT_GENERAL); + TEST_ASSERT_EQUAL(0x02, MIDI_CS_ENDPOINT_GENERAL_2_0); +} -- cgit v1.3.1 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/CMakeLists.txt | 1 + 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 ++++++ 6 files changed, 806 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 diff --git a/examples/device/CMakeLists.txt b/examples/device/CMakeLists.txt index 088872711..1432b36bb 100644 --- a/examples/device/CMakeLists.txt +++ b/examples/device/CMakeLists.txt @@ -28,6 +28,7 @@ set(EXAMPLE_LIST hid_multiple_interface midi_test midi_test_freertos + midi2_device msc_dual_lun mtp net_lwip_webserver diff --git a/examples/device/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 d5c5ac586bfdbf53e18a8cdbb11b978f53b6d059 Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Tue, 24 Mar 2026 23:38:36 -0300 Subject: example: add MIDI 2.0 Host example with display (midi2_host) 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 --- examples/host/CMakeLists.txt | 1 + examples/host/midi2_host/CMakeLists.txt | 37 +++ examples/host/midi2_host/src/display.c | 220 ++++++++++++++ examples/host/midi2_host/src/display.h | 54 ++++ examples/host/midi2_host/src/font5x7.h | 107 +++++++ examples/host/midi2_host/src/main.c | 326 +++++++++++++++++++++ examples/host/midi2_host/src/tusb_config.h | 91 ++++++ .../boards/waveshare_rp2350_usb_a/board.cmake | 2 + .../rp2040/boards/waveshare_rp2350_usb_a/board.h | 49 ++++ 9 files changed, 887 insertions(+) create mode 100644 examples/host/midi2_host/CMakeLists.txt create mode 100644 examples/host/midi2_host/src/display.c create mode 100644 examples/host/midi2_host/src/display.h create mode 100644 examples/host/midi2_host/src/font5x7.h create mode 100644 examples/host/midi2_host/src/main.c create mode 100644 examples/host/midi2_host/src/tusb_config.h create mode 100644 hw/bsp/rp2040/boards/waveshare_rp2350_usb_a/board.cmake create mode 100644 hw/bsp/rp2040/boards/waveshare_rp2350_usb_a/board.h diff --git a/examples/host/CMakeLists.txt b/examples/host/CMakeLists.txt index 70e0427ab..7c74e3c73 100644 --- a/examples/host/CMakeLists.txt +++ b/examples/host/CMakeLists.txt @@ -13,6 +13,7 @@ set(EXAMPLE_LIST device_info hid_controller midi_rx + midi2_host msc_file_explorer msc_file_explorer_freertos ) diff --git a/examples/host/midi2_host/CMakeLists.txt b/examples/host/midi2_host/CMakeLists.txt new file mode 100644 index 000000000..221de7adf --- /dev/null +++ b/examples/host/midi2_host/CMakeLists.txt @@ -0,0 +1,37 @@ +cmake_minimum_required(VERSION 3.20) + +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake) + +project(midi2_host C CXX ASM) + +family_initialize_project(${PROJECT_NAME} ${CMAKE_CURRENT_LIST_DIR}) + +if(FAMILY STREQUAL "espressif") + return() +endif() + +add_executable(${PROJECT_NAME}) + +target_sources(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src/main.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/display.c +) + +target_include_directories(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + +family_configure_host_example(${PROJECT_NAME} noos) + +# Waveshare RP2350-USB-A: PIO-USB on GP12/GP13 +target_compile_definitions(${PROJECT_NAME} PRIVATE + PIO_USB_DP_PIN_DEFAULT=12 +) +target_compile_options(${PROJECT_NAME} PRIVATE + -Wno-type-limits +) + +# SSD1306 display (I2C) +target_link_libraries(${PROJECT_NAME} PUBLIC + hardware_i2c +) diff --git a/examples/host/midi2_host/src/display.c b/examples/host/midi2_host/src/display.c new file mode 100644 index 000000000..6d81bcd5c --- /dev/null +++ b/examples/host/midi2_host/src/display.c @@ -0,0 +1,220 @@ +/* + * 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. + */ + +// SSD1306 OLED display driver (128x64, I2C) for MIDI 2.0 Host example. +// Minimal text-only implementation, no graphics library. +// I2C0: SDA = GP4, SCL = GP5, Address = 0x3C + +#include "display.h" +#include +#include +#include "pico/stdlib.h" +#include "hardware/i2c.h" +#include "hardware/gpio.h" + +#define I2C_PORT i2c0 +#define I2C_SDA 4 +#define I2C_SCL 5 +#define I2C_FREQ 400000 +#define SSD1306_ADDR 0x3C + +#define SCR_W 128 +#define SCR_H 64 +#define PAGES (SCR_H / 8) // 8 pages +#define CHARS_PER_LINE 21 // 128 / 6 = 21 chars + +// Framebuffer +static uint8_t fb[SCR_W * PAGES]; + +// Log buffer +#define LOG_LINES 3 +static char log_lines[LOG_LINES][CHARS_PER_LINE + 1]; +static int log_count = 0; + +// Status line +static char status_text[CHARS_PER_LINE + 1] = ""; + +//--------------------------------------------------------------------+ +// Minimal 5x7 font (ASCII 32-126) +//--------------------------------------------------------------------+ +#include "font5x7.h" + +//--------------------------------------------------------------------+ +// SSD1306 I2C commands +//--------------------------------------------------------------------+ + +static void ssd_cmd(uint8_t cmd) { + uint8_t buf[2] = { 0x00, cmd }; // Co=0, D/C=0 + i2c_write_blocking(I2C_PORT, SSD1306_ADDR, buf, 2, false); +} + +static void ssd_data(const uint8_t* data, size_t len) { + uint8_t buf[SCR_W + 1]; + buf[0] = 0x40; // Co=0, D/C=1 + size_t chunk = (len > SCR_W) ? SCR_W : len; + memcpy(buf + 1, data, chunk); + i2c_write_blocking(I2C_PORT, SSD1306_ADDR, buf, chunk + 1, false); +} + +static void ssd_flush(void) { + ssd_cmd(0x21); ssd_cmd(0); ssd_cmd(127); // Column range + ssd_cmd(0x22); ssd_cmd(0); ssd_cmd(7); // Page range + for (int page = 0; page < PAGES; page++) { + ssd_data(&fb[page * SCR_W], SCR_W); + } +} + +//--------------------------------------------------------------------+ +// Framebuffer drawing +//--------------------------------------------------------------------+ + +static void fb_clear(void) { + memset(fb, 0, sizeof(fb)); +} + +static void fb_char(int x, int y, char c) { + if (c < 32 || c > 126) c = '?'; + const uint8_t* glyph = font5x7 + (c - 32) * 5; + int page = y / 8; + int bit_offset = y % 8; + + if (page >= PAGES || x + 5 > SCR_W) return; + + for (int col = 0; col < 5; col++) { + uint8_t column_data = glyph[col]; + fb[(page * SCR_W) + x + col] |= (uint8_t)(column_data << bit_offset); + if (bit_offset > 0 && page + 1 < PAGES) { + fb[((page + 1) * SCR_W) + x + col] |= (uint8_t)(column_data >> (8 - bit_offset)); + } + } +} + +static void fb_string(int x, int y, const char* str) { + while (*str) { + fb_char(x, y, *str); + x += 6; + if (x + 6 > SCR_W) break; + str++; + } +} + +//--------------------------------------------------------------------+ +// Display API +//--------------------------------------------------------------------+ + +void display_init(void) { + i2c_init(I2C_PORT, I2C_FREQ); + gpio_set_function(I2C_SDA, GPIO_FUNC_I2C); + gpio_set_function(I2C_SCL, GPIO_FUNC_I2C); + gpio_pull_up(I2C_SDA); + gpio_pull_up(I2C_SCL); + + sleep_ms(100); + + // SSD1306 init sequence + ssd_cmd(0xAE); // Display off + ssd_cmd(0xD5); ssd_cmd(0x80); // Clock div + ssd_cmd(0xA8); ssd_cmd(0x3F); // Multiplex 64 + ssd_cmd(0xD3); ssd_cmd(0x00); // Display offset + ssd_cmd(0x40); // Start line 0 + ssd_cmd(0x8D); ssd_cmd(0x14); // Charge pump on + ssd_cmd(0x20); ssd_cmd(0x00); // Horizontal addressing + ssd_cmd(0xA1); // Segment remap + ssd_cmd(0xC8); // COM scan direction + ssd_cmd(0xDA); ssd_cmd(0x12); // COM pins + ssd_cmd(0x81); ssd_cmd(0xCF); // Contrast + ssd_cmd(0xD9); ssd_cmd(0xF1); // Pre-charge + ssd_cmd(0xDB); ssd_cmd(0x40); // VCOMH deselect + ssd_cmd(0xA4); // Display from RAM + ssd_cmd(0xA6); // Normal display + ssd_cmd(0xAF); // Display on + + fb_clear(); + fb_string(0, 0, "MIDI 2.0 Host"); + ssd_flush(); +} + +void display_checklist_update(const checklist_t* ck) { + struct { const char* label; bool ok; } items[] = { + { "PWR", ck->pwr_on }, + { "TinyUSB", ck->tusb_init }, + { "USB bus", ck->bus_active }, + { "Device", ck->device_connected }, + { "Descript", ck->descriptor_parsed }, + { "Alt1 UMP", ck->alt_setting_ok }, + { "Mount", ck->mounted }, + { "RX UMP", ck->receiving }, + }; + + // Clear checklist area (lines 1-4, two columns) + for (int p = 1; p <= 4; p++) { + memset(&fb[p * SCR_W], 0, SCR_W); + } + + for (int i = 0; i < 8; i++) { + int col = (i < 4) ? 0 : 64; + int row = (i < 4) ? i : i - 4; + int y = 9 + row * 8; + char line[12]; + snprintf(line, sizeof(line), "%s %s", items[i].ok ? "OK" : "..", items[i].label); + fb_string(col, y, line); + } + + ssd_flush(); +} + +void display_log(const char* text, uint16_t color) { + (void)color; // SSD1306 is monochrome + + 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); + } + log_count = LOG_LINES - 1; + } + + strncpy(log_lines[log_count], text, CHARS_PER_LINE); + log_lines[log_count][CHARS_PER_LINE] = '\0'; + log_count++; + + // Draw log area (pages 5-6, y=40-55) + memset(&fb[5 * SCR_W], 0, SCR_W); + memset(&fb[6 * SCR_W], 0, SCR_W); + + for (int i = 0; i < log_count && i < LOG_LINES; i++) { + fb_string(0, 41 + i * 8, log_lines[i]); + } + + ssd_flush(); +} + +void display_status(const char* text) { + strncpy(status_text, text, CHARS_PER_LINE); + status_text[CHARS_PER_LINE] = '\0'; + + // Status on last page (y=56) + memset(&fb[7 * SCR_W], 0, SCR_W); + fb_string(0, 56, status_text); + ssd_flush(); +} diff --git a/examples/host/midi2_host/src/display.h b/examples/host/midi2_host/src/display.h new file mode 100644 index 000000000..a8b0a4b5e --- /dev/null +++ b/examples/host/midi2_host/src/display.h @@ -0,0 +1,54 @@ +/* + * 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 DISPLAY_H_ +#define DISPLAY_H_ + +#include +#include + +typedef struct { + bool pwr_on; + bool tusb_init; + bool bus_active; + bool device_connected; + bool descriptor_parsed; + bool alt_setting_ok; + bool mounted; + bool receiving; +} checklist_t; + +// Initialize SSD1306 display (I2C on GP4/GP5) +void display_init(void); + +// Redraw the checklist area (top portion) +void display_checklist_update(const checklist_t* ck); + +// Add a line to the scrolling log area +void display_log(const char* text, uint16_t color); + +// Update the status bar (bottom line) +void display_status(const char* text); + +#endif diff --git a/examples/host/midi2_host/src/font5x7.h b/examples/host/midi2_host/src/font5x7.h new file mode 100644 index 000000000..e515faac1 --- /dev/null +++ b/examples/host/midi2_host/src/font5x7.h @@ -0,0 +1,107 @@ +// Minimal 5x7 font, ASCII 32-126. Public domain. +// 5 bytes per character (5 columns, 7 rows, LSB=top row) + +#ifndef FONT5X7_H_ +#define FONT5X7_H_ + +#include + +static const uint8_t font5x7[] = { + 0x00,0x00,0x00,0x00,0x00, // 32 (space) + 0x00,0x00,0x5F,0x00,0x00, // 33 ! + 0x00,0x07,0x00,0x07,0x00, // 34 " + 0x14,0x7F,0x14,0x7F,0x14, // 35 # + 0x24,0x2A,0x7F,0x2A,0x12, // 36 $ + 0x23,0x13,0x08,0x64,0x62, // 37 % + 0x36,0x49,0x55,0x22,0x50, // 38 & + 0x00,0x05,0x03,0x00,0x00, // 39 ' + 0x00,0x1C,0x22,0x41,0x00, // 40 ( + 0x00,0x41,0x22,0x1C,0x00, // 41 ) + 0x08,0x2A,0x1C,0x2A,0x08, // 42 * + 0x08,0x08,0x3E,0x08,0x08, // 43 + + 0x00,0x50,0x30,0x00,0x00, // 44 , + 0x08,0x08,0x08,0x08,0x08, // 45 - + 0x00,0x60,0x60,0x00,0x00, // 46 . + 0x20,0x10,0x08,0x04,0x02, // 47 / + 0x3E,0x51,0x49,0x45,0x3E, // 48 0 + 0x00,0x42,0x7F,0x40,0x00, // 49 1 + 0x42,0x61,0x51,0x49,0x46, // 50 2 + 0x21,0x41,0x45,0x4B,0x31, // 51 3 + 0x18,0x14,0x12,0x7F,0x10, // 52 4 + 0x27,0x45,0x45,0x45,0x39, // 53 5 + 0x3C,0x4A,0x49,0x49,0x30, // 54 6 + 0x01,0x71,0x09,0x05,0x03, // 55 7 + 0x36,0x49,0x49,0x49,0x36, // 56 8 + 0x06,0x49,0x49,0x29,0x1E, // 57 9 + 0x00,0x36,0x36,0x00,0x00, // 58 : + 0x00,0x56,0x36,0x00,0x00, // 59 ; + 0x00,0x08,0x14,0x22,0x41, // 60 < + 0x14,0x14,0x14,0x14,0x14, // 61 = + 0x41,0x22,0x14,0x08,0x00, // 62 > + 0x02,0x01,0x51,0x09,0x06, // 63 ? + 0x32,0x49,0x79,0x41,0x3E, // 64 @ + 0x7E,0x11,0x11,0x11,0x7E, // 65 A + 0x7F,0x49,0x49,0x49,0x36, // 66 B + 0x3E,0x41,0x41,0x41,0x22, // 67 C + 0x7F,0x41,0x41,0x22,0x1C, // 68 D + 0x7F,0x49,0x49,0x49,0x41, // 69 E + 0x7F,0x09,0x09,0x01,0x01, // 70 F + 0x3E,0x41,0x41,0x51,0x32, // 71 G + 0x7F,0x08,0x08,0x08,0x7F, // 72 H + 0x00,0x41,0x7F,0x41,0x00, // 73 I + 0x20,0x40,0x41,0x3F,0x01, // 74 J + 0x7F,0x08,0x14,0x22,0x41, // 75 K + 0x7F,0x40,0x40,0x40,0x40, // 76 L + 0x7F,0x02,0x04,0x02,0x7F, // 77 M + 0x7F,0x04,0x08,0x10,0x7F, // 78 N + 0x3E,0x41,0x41,0x41,0x3E, // 79 O + 0x7F,0x09,0x09,0x09,0x06, // 80 P + 0x3E,0x41,0x51,0x21,0x5E, // 81 Q + 0x7F,0x09,0x19,0x29,0x46, // 82 R + 0x46,0x49,0x49,0x49,0x31, // 83 S + 0x01,0x01,0x7F,0x01,0x01, // 84 T + 0x3F,0x40,0x40,0x40,0x3F, // 85 U + 0x1F,0x20,0x40,0x20,0x1F, // 86 V + 0x7F,0x20,0x18,0x20,0x7F, // 87 W + 0x63,0x14,0x08,0x14,0x63, // 88 X + 0x03,0x04,0x78,0x04,0x03, // 89 Y + 0x61,0x51,0x49,0x45,0x43, // 90 Z + 0x00,0x00,0x7F,0x41,0x41, // 91 [ + 0x02,0x04,0x08,0x10,0x20, // 92 backslash + 0x41,0x41,0x7F,0x00,0x00, // 93 ] + 0x04,0x02,0x01,0x02,0x04, // 94 ^ + 0x40,0x40,0x40,0x40,0x40, // 95 _ + 0x00,0x01,0x02,0x04,0x00, // 96 ` + 0x20,0x54,0x54,0x54,0x78, // 97 a + 0x7F,0x48,0x44,0x44,0x38, // 98 b + 0x38,0x44,0x44,0x44,0x20, // 99 c + 0x38,0x44,0x44,0x48,0x7F, // 100 d + 0x38,0x54,0x54,0x54,0x18, // 101 e + 0x08,0x7E,0x09,0x01,0x02, // 102 f + 0x08,0x14,0x54,0x54,0x3C, // 103 g + 0x7F,0x08,0x04,0x04,0x78, // 104 h + 0x00,0x44,0x7D,0x40,0x00, // 105 i + 0x20,0x40,0x44,0x3D,0x00, // 106 j + 0x00,0x7F,0x10,0x28,0x44, // 107 k + 0x00,0x41,0x7F,0x40,0x00, // 108 l + 0x7C,0x04,0x18,0x04,0x78, // 109 m + 0x7C,0x08,0x04,0x04,0x78, // 110 n + 0x38,0x44,0x44,0x44,0x38, // 111 o + 0x7C,0x14,0x14,0x14,0x08, // 112 p + 0x08,0x14,0x14,0x18,0x7C, // 113 q + 0x7C,0x08,0x04,0x04,0x08, // 114 r + 0x48,0x54,0x54,0x54,0x20, // 115 s + 0x04,0x3F,0x44,0x40,0x20, // 116 t + 0x3C,0x40,0x40,0x20,0x7C, // 117 u + 0x1C,0x20,0x40,0x20,0x1C, // 118 v + 0x3C,0x40,0x30,0x40,0x3C, // 119 w + 0x44,0x28,0x10,0x28,0x44, // 120 x + 0x0C,0x50,0x50,0x50,0x3C, // 121 y + 0x44,0x64,0x54,0x4C,0x44, // 122 z + 0x00,0x08,0x36,0x41,0x00, // 123 { + 0x00,0x00,0x7F,0x00,0x00, // 124 | + 0x00,0x41,0x36,0x08,0x00, // 125 } + 0x10,0x08,0x08,0x10,0x08, // 126 ~ +}; + +#endif diff --git a/examples/host/midi2_host/src/main.c b/examples/host/midi2_host/src/main.c new file mode 100644 index 000000000..e23fbfc87 --- /dev/null +++ b/examples/host/midi2_host/src/main.c @@ -0,0 +1,326 @@ +/* + * 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. + */ + +// MIDI 2.0 Host Receiver Example +// +// Receives UMP from a MIDI 2.0 Device via PIO-USB Host. +// SSD1306 OLED (I2C, 128x64) shows boot checklist then received UMP messages. + +#include +#include +#include "bsp/board_api.h" +#include "tusb.h" +#include "class/midi/midi2_host.h" +#include "class/midi/midi.h" +#include "display.h" + +//--------------------------------------------------------------------+ +// Checklist state +//--------------------------------------------------------------------+ + +static checklist_t ck = { 0 }; +static uint32_t note_count = 0; +static uint8_t midi2_idx = 0xFF; // invalid until mount + +//--------------------------------------------------------------------+ +// Note name helper +//--------------------------------------------------------------------+ + +static const char* NOTE_NAMES[] = { + "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" +}; + +static void note_name(uint8_t pitch, char* buf, size_t len) { + int octave = (pitch / 12) - 1; + snprintf(buf, len, "%s%d", NOTE_NAMES[pitch % 12], octave); +} + +//--------------------------------------------------------------------+ +// UMP Message Type names +//--------------------------------------------------------------------+ + +static const char* mt_name(uint8_t mt) { + switch (mt) { + case 0x0: return "Utility"; + case 0x1: return "System"; + case 0x2: return "M1 CVM"; + case 0x3: return "SysEx7"; + case 0x4: return "M2 CVM"; + case 0x5: return "SysEx8"; + case 0xD: return "Flex"; + case 0xF: return "Stream"; + default: return "?"; + } +} + +//--------------------------------------------------------------------+ +// UMP decoder - extract and display MIDI 2.0 messages +//--------------------------------------------------------------------+ + +static void decode_ump(const uint32_t* words, uint8_t word_count) { + uint8_t mt = (uint8_t)((words[0] >> 28) & 0x0F); + char line[64]; + + if (mt == 0x4 && word_count >= 2) { + // MIDI 2.0 Channel Voice Message + uint8_t status = (uint8_t)((words[0] >> 20) & 0x0F); + uint8_t channel = (uint8_t)((words[0] >> 16) & 0x0F); + + switch (status) { + case 0x9: { // Note On + uint8_t pitch = (uint8_t)((words[0] >> 8) & 0x7F); + uint16_t vel = (uint16_t)((words[1] >> 16) & 0xFFFF); + char nn[6]; + note_name(pitch, nn, sizeof(nn)); + snprintf(line, sizeof(line), "NoteOn %s ch%u vel=0x%04X", nn, channel, vel); + display_log(line, 0x07E0); // green + note_count++; + break; + } + case 0x8: { // Note Off + uint8_t pitch = (uint8_t)((words[0] >> 8) & 0x7F); + char nn[6]; + note_name(pitch, nn, sizeof(nn)); + snprintf(line, sizeof(line), "NoteOff %s ch%u", nn, channel); + display_log(line, 0x8410); // grey + break; + } + case 0xB: { // CC + uint8_t idx = (uint8_t)((words[0] >> 8) & 0x7F); + snprintf(line, sizeof(line), "CC%-3u = 0x%08lX", idx, (unsigned long)words[1]); + display_log(line, 0x001F); // blue + break; + } + case 0xC: { // Program Change + uint8_t prog = (uint8_t)((words[1] >> 24) & 0x7F); + snprintf(line, sizeof(line), "ProgChg %u", prog); + display_log(line, 0xFFE0); // yellow + break; + } + case 0xE: { // Pitch Bend + snprintf(line, sizeof(line), "PBend = 0x%08lX", (unsigned long)words[1]); + display_log(line, 0xF81F); // magenta + break; + } + case 0xD: { // Channel Pressure + snprintf(line, sizeof(line), "CPress = 0x%08lX", (unsigned long)words[1]); + display_log(line, 0xFC10); // orange + break; + } + case 0xA: { // Poly Pressure + uint8_t pitch = (uint8_t)((words[0] >> 8) & 0x7F); + char nn[6]; + note_name(pitch, nn, sizeof(nn)); + snprintf(line, sizeof(line), "PolyP %s = 0x%08lX", nn, (unsigned long)words[1]); + display_log(line, 0xFC10); + break; + } + case 0xF: { // Per-Note Management + uint8_t pitch = (uint8_t)((words[0] >> 8) & 0x7F); + uint8_t flags = (uint8_t)(words[0] & 0xFF); + snprintf(line, sizeof(line), "PN-Mgmt note=%u flags=0x%02X", pitch, flags); + display_log(line, 0x07FF); // cyan + break; + } + default: { + snprintf(line, sizeof(line), "M2CVM status=0x%X", status); + display_log(line, 0xFFFF); + break; + } + } + } else if (mt == 0x0) { + // Utility (JR Timestamp, NOOP) + uint8_t status = (uint8_t)((words[0] >> 20) & 0x0F); + if (status == 0x2) { + uint16_t ts = words[0] & 0xFFFF; + snprintf(line, sizeof(line), "JR-TS = 0x%04X", ts); + display_log(line, 0x8410); + } + } else { + snprintf(line, sizeof(line), "MT=0x%X (%s) w0=0x%08lX", + mt, mt_name(mt), (unsigned long)words[0]); + display_log(line, 0xFFFF); + } +} + +//--------------------------------------------------------------------+ +// MIDI 2.0 Host Callbacks +//--------------------------------------------------------------------+ + +void tuh_midi2_descriptor_cb(uint8_t idx, const tuh_midi2_descriptor_cb_t* d) { + (void)idx; + ck.descriptor_parsed = true; + char line[48]; + + snprintf(line, sizeof(line), "bcdMSC=0x%02X%02X proto=%s", + d->bcdMSC_hi, d->bcdMSC_lo, + d->protocol_version ? "MIDI2" : "MIDI1"); + display_log(line, 0x07E0); + + snprintf(line, sizeof(line), "Cables: RX=%u TX=%u", + d->rx_cable_count, d->tx_cable_count); + display_log(line, 0x07E0); + + display_checklist_update(&ck); +} + +void tuh_midi2_mount_cb(uint8_t idx, const tuh_midi2_mount_cb_t* m) { + midi2_idx = idx; + ck.alt_setting_ok = true; + ck.mounted = true; + + char line[48]; + snprintf(line, sizeof(line), "Mounted addr=%u alt=%u", + m->daddr, m->alt_setting_active); + display_log(line, 0x07E0); + + if (m->protocol_version) { + display_log("MIDI 2.0 ready", 0x07E0); + } else { + display_log("MIDI 1.0 only", 0xFFE0); + } + + display_checklist_update(&ck); +} + +void tuh_midi2_rx_cb(uint8_t idx, uint32_t xferred_bytes) { + (void)xferred_bytes; + if (!ck.receiving) { + ck.receiving = true; + display_checklist_update(&ck); + } + + uint32_t words[16]; + while (1) { + uint32_t n = tuh_midi2_ump_read(idx, words, 16); + if (n == 0) break; + + // Decode complete UMP messages + uint32_t i = 0; + while (i < n) { + uint8_t mt = (uint8_t)((words[i] >> 28) & 0x0F); + uint8_t wc = midi2_ump_word_count(mt); + if (i + wc > n) break; + decode_ump(&words[i], wc); + i += wc; + } + } +} + +void tuh_midi2_tx_cb(uint8_t idx, uint32_t xferred_bytes) { + (void)idx; (void)xferred_bytes; +} + +void tuh_midi2_umount_cb(uint8_t idx) { + (void)idx; + midi2_idx = 0xFF; + ck.device_connected = false; + ck.descriptor_parsed = false; + ck.alt_setting_ok = false; + ck.mounted = false; + ck.receiving = false; + note_count = 0; + + display_log("Device disconnected", 0xF800); + display_checklist_update(&ck); +} + +//--------------------------------------------------------------------+ +// Main +//--------------------------------------------------------------------+ + +int main(void) { + board_init(); + + display_init(); + + ck.pwr_on = true; + display_checklist_update(&ck); + + // Init TinyUSB Host (BSP handles PIO-USB configuration via tuh_configure) + tusb_rhport_init_t host_init = { + .role = TUSB_ROLE_HOST, + .speed = TUSB_SPEED_FULL, + }; + tusb_init(BOARD_TUH_RHPORT, &host_init); + + ck.tusb_init = true; + ck.bus_active = true; + display_checklist_update(&ck); + + char dbg[40]; + snprintf(dbg, sizeof(dbg), "RHPORT=%u PIO_USB=%u", + BOARD_TUH_RHPORT, CFG_TUH_RPI_PIO_USB); + display_log(dbg, 0xFFE0); // yellow + display_status("Waiting for device..."); + + uint32_t last_status_ms = 0; + static uint32_t loop_count = 0; + + while (1) { + tuh_task(); + loop_count++; + + // Update status every 2 seconds + uint32_t now = tusb_time_millis_api(); + if (now - last_status_ms > 2000) { + last_status_ms = now; + char line[22]; + if (ck.receiving) { + snprintf(line, sizeof(line), "Notes:%lu", (unsigned long)note_count); + } else if (ck.mounted) { + snprintf(line, sizeof(line), "Mounted OK!"); + } else if (ck.device_connected) { + snprintf(line, sizeof(line), "Dev found, mounting.."); + } else { + snprintf(line, sizeof(line), "Wait.. t=%lu", (unsigned long)(now/1000)); + } + display_status(line); + } + } + + return 0; +} + +// Generic USB device mount/unmount (any class) +void tuh_mount_cb(uint8_t daddr) { + char line[32]; + uint16_t vid, pid; + tuh_vid_pid_get(daddr, &vid, &pid); + snprintf(line, sizeof(line), "USB %04X:%04X a%u", vid, pid, daddr); + display_log(line, 0x07E0); + ck.device_connected = true; + display_checklist_update(&ck); +} + +void tuh_umount_cb(uint8_t daddr) { + (void)daddr; + display_log("USB disconnected", 0xF800); + ck.device_connected = false; + ck.descriptor_parsed = false; + ck.alt_setting_ok = false; + ck.mounted = false; + ck.receiving = false; + display_checklist_update(&ck); +} diff --git a/examples/host/midi2_host/src/tusb_config.h b/examples/host/midi2_host/src/tusb_config.h new file mode 100644 index 000000000..650df97e1 --- /dev/null +++ b/examples/host/midi2_host/src/tusb_config.h @@ -0,0 +1,91 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TUSB_CONFIG_H_ +#define TUSB_CONFIG_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +//-------------------------------------------------------------------- +// Common Configuration +//-------------------------------------------------------------------- + +#ifndef CFG_TUSB_MCU +#error CFG_TUSB_MCU must be defined +#endif + +#ifndef CFG_TUSB_OS +#define CFG_TUSB_OS OPT_OS_NONE +#endif + +#ifndef CFG_TUSB_DEBUG +#define CFG_TUSB_DEBUG 0 +#endif + +#ifndef CFG_TUH_MEM_SECTION +#define CFG_TUH_MEM_SECTION +#endif + +#ifndef CFG_TUH_MEM_ALIGN +#define CFG_TUH_MEM_ALIGN __attribute__ ((aligned(4))) +#endif + +//-------------------------------------------------------------------- +// Host Configuration +// Waveshare RP2350-USB-A: PIO-USB on GP12/GP13 (USB-A Host port) +//-------------------------------------------------------------------- + +#define CFG_TUH_ENABLED 1 + +// PIO-USB Host on rhport 1 (USB-A connector on GP12/GP13) +#define CFG_TUH_RPI_PIO_USB 1 +#define BOARD_TUH_RHPORT 1 + +#ifndef BOARD_TUH_MAX_SPEED +#define BOARD_TUH_MAX_SPEED OPT_MODE_FULL_SPEED +#endif + +#define CFG_TUH_MAX_SPEED BOARD_TUH_MAX_SPEED + +//-------------------------------------------------------------------- +// Driver Configuration +//-------------------------------------------------------------------- + +#define CFG_TUH_ENUMERATION_BUFSIZE 256 + +#define CFG_TUH_HUB 0 +#define CFG_TUH_DEVICE_MAX 1 + +// MIDI 2.0 Host +#define CFG_TUH_MIDI2 1 +#define CFG_TUH_MIDI2_RX_BUFSIZE 512 +#define CFG_TUH_MIDI2_TX_BUFSIZE 512 + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/hw/bsp/rp2040/boards/waveshare_rp2350_usb_a/board.cmake b/hw/bsp/rp2040/boards/waveshare_rp2350_usb_a/board.cmake new file mode 100644 index 000000000..e888c7fb3 --- /dev/null +++ b/hw/bsp/rp2040/boards/waveshare_rp2350_usb_a/board.cmake @@ -0,0 +1,2 @@ +set(PICO_PLATFORM rp2350-arm-s) +set(PICO_BOARD waveshare_rp2350_usb_a) diff --git a/hw/bsp/rp2040/boards/waveshare_rp2350_usb_a/board.h b/hw/bsp/rp2040/boards/waveshare_rp2350_usb_a/board.h new file mode 100644 index 000000000..0af8a695f --- /dev/null +++ b/hw/bsp/rp2040/boards/waveshare_rp2350_usb_a/board.h @@ -0,0 +1,49 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * This file is part of the TinyUSB stack. + */ + +/* metadata: + name: Waveshare RP2350-USB-A + url: https://www.waveshare.com/wiki/RP2350-USB-A +*/ + +#ifndef TUSB_BOARD_H +#define TUSB_BOARD_H + +#ifdef __cplusplus + extern "C" { +#endif + +//--------------------------------------------------------------------+ +// PIO_USB +//--------------------------------------------------------------------+ +// Waveshare RP2350-USB-A: PIO-USB on GP12 (D+) / GP13 (D-) +// PICO_DEFAULT_PIO_USB_DP_PIN already defined in SDK board header + +#ifdef __cplusplus + } +#endif + +#endif -- cgit v1.3.1 From fa9edeff9c00ee1fc4ee7ab9938b1a5955a6281a Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Wed, 25 Mar 2026 07:05:56 -0300 Subject: fix: address PR review feedback for MIDI 2.0 drivers 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 (#1, #2, #3), Copilot (#4-#9) --- docs/reference/class_drivers.rst | 8 +- examples/host/midi2_host/CMakeLists.txt | 3 +- examples/host/midi2_host/src/display.c | 1 + src/class/midi/midi2_device.c | 14 ++- src/class/midi/midi2_host.c | 213 ++++++++++++++++++++------------ src/device/usbd.h | 24 ++-- 6 files changed, 166 insertions(+), 97 deletions(-) diff --git a/docs/reference/class_drivers.rst b/docs/reference/class_drivers.rst index 9ed332acb..3ac0d8d4e 100644 --- a/docs/reference/class_drivers.rst +++ b/docs/reference/class_drivers.rst @@ -87,7 +87,7 @@ The MIDI 2.0 Host driver enables TinyUSB to enumerate and communicate with USB M **Key Features:** - **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) +- **Auto-Selection**: Automatically selects the highest available protocol and issues SET_INTERFACE to activate Alt Setting 1 when MIDI 2.0 is detected - **Transparent Stream Messages**: All data (UMP packets + Stream Messages) flow through callbacks - **Memory Safe**: No dynamic allocation, fixed-size instances per device @@ -271,9 +271,9 @@ Architecture The MIDI 2.0 Host driver uses a **reactive, callback-driven architecture** that mirrors the proven patterns in TinyUSB's existing device drivers (CDC, HID, etc.): - **Auto-Detection**: Host automatically detects Alt Setting 1 capability -- **Auto-Selection**: Selects highest protocol available (MIDI 2.0 preferred) -- **Application Control**: App makes protocol behavior decisions via callbacks -- **Transparent I/O**: Stream Messages and UMP packets flow transparently +- **Auto-Selection**: Selects highest protocol available and issues SET_INTERFACE +- **Transparent I/O**: Stream Messages and UMP packets flow through callbacks +- **Callback-Driven**: App receives events via callbacks (descriptor, mount, rx, tx, unmount) Differences from MIDI 1.0 Host ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/examples/host/midi2_host/CMakeLists.txt b/examples/host/midi2_host/CMakeLists.txt index 221de7adf..cd70d122a 100644 --- a/examples/host/midi2_host/CMakeLists.txt +++ b/examples/host/midi2_host/CMakeLists.txt @@ -6,7 +6,8 @@ project(midi2_host C CXX ASM) family_initialize_project(${PROJECT_NAME} ${CMAKE_CURRENT_LIST_DIR}) -if(FAMILY STREQUAL "espressif") +# This example requires PIO-USB and Pico SDK (I2C, SSD1306 display) +if(NOT FAMILY STREQUAL "rp2040") return() endif() diff --git a/examples/host/midi2_host/src/display.c b/examples/host/midi2_host/src/display.c index 6d81bcd5c..4745c2961 100644 --- a/examples/host/midi2_host/src/display.c +++ b/examples/host/midi2_host/src/display.c @@ -190,6 +190,7 @@ void display_log(const char* text, uint16_t color) { 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); + log_lines[i][CHARS_PER_LINE] = '\0'; } log_count = LOG_LINES - 1; } diff --git a/src/class/midi/midi2_device.c b/src/class/midi/midi2_device.c index 9363aac11..aecbda4c5 100644 --- a/src/class/midi/midi2_device.c +++ b/src/class/midi/midi2_device.c @@ -505,12 +505,19 @@ uint16_t midi2d_open(uint8_t rhport, const tusb_desc_interface_t* desc_itf, uint } // Skip remaining descriptors (alt setting 1, CS endpoints, GTB) + // Stop at any interface descriptor that is not our MIDI Streaming alt setting while (tu_desc_in_bounds(p_desc, desc_end)) { uint8_t dtype = tu_desc_type(p_desc); - if (dtype != TUSB_DESC_CS_INTERFACE && dtype != TUSB_DESC_CS_ENDPOINT && - dtype != TUSB_DESC_INTERFACE && dtype != TUSB_DESC_ENDPOINT) { + + if (dtype == TUSB_DESC_INTERFACE) { + const tusb_desc_interface_t* next_itf = (const tusb_desc_interface_t*) p_desc; + // Continue only if this is an alternate setting of our own interface + if (next_itf->bInterfaceNumber != desc_midi->bInterfaceNumber) break; + } else if (dtype != TUSB_DESC_CS_INTERFACE && dtype != TUSB_DESC_CS_ENDPOINT && + dtype != TUSB_DESC_ENDPOINT) { break; } + p_desc = tu_desc_next(p_desc); } @@ -528,6 +535,9 @@ bool midi2d_control_xfer_cb(uint8_t rhport, uint8_t stage, const tusb_control_re uint8_t itf_num = tu_u16_low(request->wIndex); uint8_t alt = tu_u16_low(request->wValue); + // Only Alt Setting 0 (MIDI 1.0) and 1 (UMP) are valid + if (alt > 1) return false; + uint8_t idx = find_midi2_itf_by_num(itf_num); if (idx >= CFG_TUD_MIDI2) return false; diff --git a/src/class/midi/midi2_host.c b/src/class/midi/midi2_host.c index beb441b56..5b9f98f8b 100644 --- a/src/class/midi/midi2_host.c +++ b/src/class/midi/midi2_host.c @@ -125,9 +125,10 @@ static inline uint8_t get_idx_by_ep_addr(uint8_t daddr, uint8_t ep_addr) { // Descriptor parsing //--------------------------------------------------------------------+ -static void midih2_parse_descriptors_alt0(midih2_interface_t *p_midi, +// Parse Alt Setting 0 (MIDI 1.0) descriptors. Returns pointer past last consumed descriptor. +static const uint8_t* midih2_parse_descriptors_alt0(midih2_interface_t *p_midi, const tusb_desc_interface_t *desc_itf, const uint8_t *desc_end) { - TU_VERIFY(AUDIO_SUBCLASS_MIDI_STREAMING == desc_itf->bInterfaceSubClass,); + TU_VERIFY(AUDIO_SUBCLASS_MIDI_STREAMING == desc_itf->bInterfaceSubClass, NULL); p_midi->bInterfaceNumber = desc_itf->bInterfaceNumber; @@ -136,93 +137,113 @@ static void midih2_parse_descriptors_alt0(midih2_interface_t *p_midi, uint8_t rx_cable_count = 0; uint8_t tx_cable_count = 0; + bool found_new_interface = false; - while (tu_desc_in_bounds(p_desc, desc_end)) { - if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { - break; - } - - if (tu_desc_type(p_desc) == TUSB_DESC_ENDPOINT) { - const tusb_desc_endpoint_t *p_ep = (const tusb_desc_endpoint_t *) p_desc; - - // Open endpoint and stream - TU_ASSERT(tuh_edpt_open(p_midi->daddr, p_ep),); - if (tu_edpt_dir(p_ep->bEndpointAddress) == TUSB_DIR_IN) { - tu_edpt_stream_open(&p_midi->ep_stream.rx, p_midi->daddr, p_ep, tu_edpt_packet_size(p_ep)); - tu_edpt_stream_clear(&p_midi->ep_stream.rx); - } else { - tu_edpt_stream_open(&p_midi->ep_stream.tx, p_midi->daddr, p_ep, tu_edpt_packet_size(p_ep)); - tu_edpt_stream_clear(&p_midi->ep_stream.tx); - } - - p_desc = tu_desc_next(p_desc); + while (tu_desc_in_bounds(p_desc, desc_end) && !found_new_interface) { + switch (tu_desc_type(p_desc)) { + case TUSB_DESC_INTERFACE: + found_new_interface = true; + break; - if (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) == TUSB_DESC_CS_ENDPOINT) { - const midi_desc_cs_endpoint_t *p_csep = (const midi_desc_cs_endpoint_t *) p_desc; + case TUSB_DESC_ENDPOINT: { + const tusb_desc_endpoint_t *p_ep = (const tusb_desc_endpoint_t *) p_desc; - if (tu_edpt_dir(p_ep->bEndpointAddress) == TUSB_DIR_OUT) { - tx_cable_count = p_csep->bNumEmbMIDIJack; + TU_ASSERT(tuh_edpt_open(p_midi->daddr, p_ep), NULL); + if (tu_edpt_dir(p_ep->bEndpointAddress) == TUSB_DIR_IN) { + tu_edpt_stream_open(&p_midi->ep_stream.rx, p_midi->daddr, p_ep, tu_edpt_packet_size(p_ep)); + tu_edpt_stream_clear(&p_midi->ep_stream.rx); } else { - rx_cable_count = p_csep->bNumEmbMIDIJack; + tu_edpt_stream_open(&p_midi->ep_stream.tx, p_midi->daddr, p_ep, tu_edpt_packet_size(p_ep)); + tu_edpt_stream_clear(&p_midi->ep_stream.tx); + } + + p_desc = tu_desc_next(p_desc); + if (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) == TUSB_DESC_CS_ENDPOINT) { + const midi_desc_cs_endpoint_t *p_csep = (const midi_desc_cs_endpoint_t *) p_desc; + if (tu_edpt_dir(p_ep->bEndpointAddress) == TUSB_DIR_OUT) { + tx_cable_count = p_csep->bNumEmbMIDIJack; + } else { + rx_cable_count = p_csep->bNumEmbMIDIJack; + } } + break; } + + default: + break; } - p_desc = tu_desc_next(p_desc); + if (!found_new_interface) { + p_desc = tu_desc_next(p_desc); + } } p_midi->rx_cable_count_alt0 = rx_cable_count; p_midi->tx_cable_count_alt0 = tx_cable_count; + return p_desc; } -static void midih2_parse_descriptors_alt1(midih2_interface_t *p_midi, +// Parse Alt Setting 1 (MIDI 2.0 UMP) descriptors. Returns pointer past last consumed descriptor. +static const uint8_t* midih2_parse_descriptors_alt1(midih2_interface_t *p_midi, const tusb_desc_interface_t *desc_itf, const uint8_t *desc_end) { - TU_VERIFY(AUDIO_SUBCLASS_MIDI_STREAMING == desc_itf->bInterfaceSubClass,); - TU_VERIFY(desc_itf->bAlternateSetting == 1,); + TU_VERIFY(AUDIO_SUBCLASS_MIDI_STREAMING == desc_itf->bInterfaceSubClass, NULL); + TU_VERIFY(desc_itf->bAlternateSetting == 1, NULL); const uint8_t *p_desc = (const uint8_t *) desc_itf; p_desc = tu_desc_next(p_desc); uint8_t rx_cable_count = 0; uint8_t tx_cable_count = 0; + bool found_new_interface = false; - while (tu_desc_in_bounds(p_desc, desc_end)) { - if (tu_desc_type(p_desc) == TUSB_DESC_INTERFACE) { - break; - } + while (tu_desc_in_bounds(p_desc, desc_end) && !found_new_interface) { + switch (tu_desc_type(p_desc)) { + case TUSB_DESC_INTERFACE: + found_new_interface = true; + break; - if (tu_desc_type(p_desc) == TUSB_DESC_CS_INTERFACE) { - if (tu_desc_subtype(p_desc) == MIDI_CS_INTERFACE_HEADER) { - const uint8_t *bcd_ptr = p_desc + 3; - p_midi->bcdMSC_lo = bcd_ptr[0]; - p_midi->bcdMSC_hi = bcd_ptr[1]; + case TUSB_DESC_CS_INTERFACE: + if (tu_desc_subtype(p_desc) == MIDI_CS_INTERFACE_HEADER) { + // bcdMSC at offset 3-4 in CS Interface Header + const uint8_t *bcd_ptr = p_desc + 3; + p_midi->bcdMSC_lo = bcd_ptr[0]; + p_midi->bcdMSC_hi = bcd_ptr[1]; + if (p_midi->bcdMSC_hi == 0x02) { // bcdMSC 0x0200 = USB-MIDI 2.0 + p_midi->protocol_version = 1; + } + } + break; - if (p_midi->bcdMSC_hi == 0x02) { - p_midi->protocol_version = 1; + case TUSB_DESC_ENDPOINT: { + const tusb_desc_endpoint_t *p_ep = (const tusb_desc_endpoint_t *) p_desc; + p_desc = tu_desc_next(p_desc); + + if (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) == TUSB_DESC_CS_ENDPOINT) { + // MIDI 2.0 CS Endpoint General 2.0: bNumGrpTrmBlk at offset 3 + if (p_desc[0] >= 4 && p_desc[2] == MIDI_CS_ENDPOINT_GENERAL_2_0) { + uint8_t num_grp_trm_blk = p_desc[3]; + if (tu_edpt_dir(p_ep->bEndpointAddress) == TUSB_DIR_OUT) { + tx_cable_count = num_grp_trm_blk; + } else { + rx_cable_count = num_grp_trm_blk; + } + } } + break; } + + default: + break; } - if (tu_desc_type(p_desc) == TUSB_DESC_ENDPOINT) { - const tusb_desc_endpoint_t *p_ep = (const tusb_desc_endpoint_t *) p_desc; + if (!found_new_interface) { p_desc = tu_desc_next(p_desc); - - if (tu_desc_in_bounds(p_desc, desc_end) && tu_desc_type(p_desc) == TUSB_DESC_CS_ENDPOINT) { - const midi_desc_cs_endpoint_t *p_csep = (const midi_desc_cs_endpoint_t *) p_desc; - - if (tu_edpt_dir(p_ep->bEndpointAddress) == TUSB_DIR_OUT) { - tx_cable_count = p_csep->bNumEmbMIDIJack; - } else { - rx_cable_count = p_csep->bNumEmbMIDIJack; - } - } } - - p_desc = tu_desc_next(p_desc); } p_midi->rx_cable_count_alt1 = rx_cable_count; p_midi->tx_cable_count_alt1 = tx_cable_count; + return p_desc; } //--------------------------------------------------------------------+ @@ -327,33 +348,20 @@ uint16_t midih2_open(uint8_t rhport, uint8_t dev_addr, const tusb_desc_interface desc_itf->bInterfaceNumber, desc_itf->bAlternateSetting, dev_addr); // Dispatch to appropriate parser based on Alt Setting + const uint8_t *p_end = NULL; if (desc_itf->bAlternateSetting == 0) { - midih2_parse_descriptors_alt0(p_midi, desc_itf, desc_end); + p_end = midih2_parse_descriptors_alt0(p_midi, desc_itf, desc_end); } else if (desc_itf->bAlternateSetting == 1) { - midih2_parse_descriptors_alt1(p_midi, desc_itf, desc_end); + p_end = midih2_parse_descriptors_alt1(p_midi, desc_itf, desc_end); } - return max_len; + // Return number of bytes consumed (following midi_host.c pattern) + uint16_t const parsed_len = (p_end != NULL) ? (uint16_t)(p_end - desc_start) : 0; + return parsed_len; } -bool midih2_set_config(uint8_t dev_addr, uint8_t itf_num) { - uint8_t idx = 0; - for (idx = 0; idx < CFG_TUH_MIDI2; idx++) { - if (_midi2_host[idx].daddr == dev_addr && _midi2_host[idx].bInterfaceNumber == itf_num) { - break; - } - } - - if (idx >= CFG_TUH_MIDI2) { - // Not our interface (e.g. Audio Control) - pass through to next - usbh_driver_set_config_complete(dev_addr, itf_num); - return true; - } - - midih2_interface_t *p_midi = &_midi2_host[idx]; - - // Auto-select alt setting - midih2_auto_select_alt_setting(p_midi); +static void midih2_set_config_complete(midih2_interface_t *p_midi, uint8_t idx) { + uint8_t dev_addr = p_midi->daddr; // Invoke descriptor_cb tuh_midi2_descriptor_cb_t desc_cb = { @@ -374,7 +382,7 @@ bool midih2_set_config(uint8_t dev_addr, uint8_t itf_num) { // Invoke mount_cb tuh_midi2_mount_cb_t mount_cb = { - .daddr = p_midi->daddr, + .daddr = dev_addr, .bInterfaceNumber = p_midi->bInterfaceNumber, .protocol_version = p_midi->protocol_version, .alt_setting_active = p_midi->alt_setting_current, @@ -387,7 +395,56 @@ bool midih2_set_config(uint8_t dev_addr, uint8_t itf_num) { 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); + usbh_driver_set_config_complete(dev_addr, p_midi->bInterfaceNumber); +} + +static void midih2_set_interface_cb(tuh_xfer_t *xfer) { + uint8_t const dev_addr = xfer->daddr; + uint8_t const itf_num = (uint8_t) tu_le16toh(xfer->setup->wIndex); + + // Find our interface + for (uint8_t idx = 0; idx < CFG_TUH_MIDI2; idx++) { + if (_midi2_host[idx].daddr == dev_addr && _midi2_host[idx].bInterfaceNumber == itf_num) { + if (xfer->result == XFER_RESULT_SUCCESS) { + midih2_set_config_complete(&_midi2_host[idx], idx); + } else { + // SET_INTERFACE failed, fall back to alt 0 + TU_LOG_DRV("MIDI2 SET_INTERFACE failed, falling back to alt 0\r\n"); + _midi2_host[idx].alt_setting_current = 0; + midih2_set_config_complete(&_midi2_host[idx], idx); + } + return; + } + } +} + +bool midih2_set_config(uint8_t dev_addr, uint8_t itf_num) { + uint8_t idx = 0; + for (idx = 0; idx < CFG_TUH_MIDI2; idx++) { + if (_midi2_host[idx].daddr == dev_addr && _midi2_host[idx].bInterfaceNumber == itf_num) { + break; + } + } + + if (idx >= CFG_TUH_MIDI2) { + // Not our interface (e.g. Audio Control) - pass through to next + usbh_driver_set_config_complete(dev_addr, itf_num); + return true; + } + + midih2_interface_t *p_midi = &_midi2_host[idx]; + + // Auto-select alt setting + midih2_auto_select_alt_setting(p_midi); + + // If MIDI 2.0 detected, issue SET_INTERFACE to activate Alt Setting 1 + if (p_midi->alt_setting_current == 1) { + TU_LOG_DRV("MIDI2 requesting SET_INTERFACE alt 1 for itf %u\r\n", itf_num); + TU_ASSERT(tuh_interface_set(dev_addr, itf_num, 1, midih2_set_interface_cb, 0)); + } else { + // MIDI 1.0 only, complete immediately + midih2_set_config_complete(p_midi, idx); + } return true; } diff --git a/src/device/usbd.h b/src/device/usbd.h index af37eff56..a9f4c5f08 100644 --- a/src/device/usbd.h +++ b/src/device/usbd.h @@ -429,18 +429,20 @@ bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_requ //--------------------------------------------------------------------+ // Alt Setting 1: MS Interface + MS Header (bcdMSC=0x0200) +// wTotalLength covers MS Header + all CS Endpoint descriptors +#define TUD_MIDI2_DESC_ALT1_CS_LEN(_numgtbs) (7 + (4 + (_numgtbs)) * 2) #define TUD_MIDI2_DESC_ALT1_HEAD_LEN (9 + 7) -#define TUD_MIDI2_DESC_ALT1_HEAD(_itfnum, _stridx) \ +#define TUD_MIDI2_DESC_ALT1_HEAD(_itfnum, _stridx, _numgtbs) \ /* 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,\ - /* MS Header (MIDI 2.0) */\ - 7, TUSB_DESC_CS_INTERFACE, MIDI_CS_INTERFACE_HEADER, U16_TO_U8S_LE(0x0200), U16_TO_U8S_LE(7) + 9, TUSB_DESC_INTERFACE, (uint8_t)((_itfnum) + 1), 1, 2, TUSB_CLASS_AUDIO, AUDIO_SUBCLASS_MIDI_STREAMING, AUDIO_FUNC_PROTOCOL_CODE_UNDEF, 0,\ + /* MS Header (MIDI 2.0): wTotalLength = header + 2x CS Endpoint */\ + 7, TUSB_DESC_CS_INTERFACE, MIDI_CS_INTERFACE_HEADER, U16_TO_U8S_LE(0x0200), U16_TO_U8S_LE(TUD_MIDI2_DESC_ALT1_CS_LEN(_numgtbs)) -// Alt Setting 1: Standard USB Endpoint (7 bytes) + CS Endpoint (subtype 0x02) +// Alt Setting 1: Standard USB Endpoint (7 bytes) + CS Endpoint General 2.0 #define TUD_MIDI2_DESC_ALT1_EP_LEN(_numgtbs) (7 + 4 + (_numgtbs)) -#define TUD_MIDI2_DESC_ALT1_EP(_ep, _epsize, _numgtbs) \ +#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 + (uint8_t)(4 + (_numgtbs)), TUSB_DESC_CS_ENDPOINT, MIDI_CS_ENDPOINT_GENERAL_2_0, _numgtbs, ## __VA_ARGS__ // Total length: Alt 0 (MIDI 1.0) + Alt 1 (UMP) #define TUD_MIDI2_DESC_LEN (TUD_MIDI_DESC_LEN + TUD_MIDI2_DESC_ALT1_HEAD_LEN + TUD_MIDI2_DESC_ALT1_EP_LEN(1) * 2) @@ -455,11 +457,9 @@ bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_requ TUD_MIDI_DESC_EP(_epin, _epsize, 1),\ TUD_MIDI_JACKID_OUT_EMB(1),\ /* Alt Setting 1 (UMP) */\ - TUD_MIDI2_DESC_ALT1_HEAD(_itfnum, _stridx),\ - TUD_MIDI2_DESC_ALT1_EP(_epout, _epsize, 1),\ - 1, /* bAssoGrpTrmBlkID = 1 */\ - TUD_MIDI2_DESC_ALT1_EP(_epin, _epsize, 1),\ - 1 /* bAssoGrpTrmBlkID = 1 */ + TUD_MIDI2_DESC_ALT1_HEAD(_itfnum, _stridx, 1),\ + TUD_MIDI2_DESC_ALT1_EP(_epout, _epsize, 1, 1 /* bAssoGrpTrmBlkID */),\ + TUD_MIDI2_DESC_ALT1_EP(_epin, _epsize, 1, 1 /* bAssoGrpTrmBlkID */) //--------------------------------------------------------------------+ // Audio Descriptor Templates -- 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 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 13c6f6a2c7aca7165161d9f32d10087c91aa61aa Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Tue, 7 Apr 2026 22:50:29 -0300 Subject: example: add MIDI 2.0 Host example for Adafruit Feather RP2040 USB Host Board-specific variant of midi2_host targeting the Adafruit Feather RP2040 with USB Type A Host (product 5723). Hardware differences from the Waveshare RP2350-USB-A example: - PIO-USB on GP16/GP17 (vs GP12/GP13) - I2C1 via STEMMA QT on GP2/GP3 (vs I2C0 GP4/GP5) - USB Host 5V power enable on GP18 Display uses three phases: splash screen, spinner while waiting for a device, then live scrolling UMP message view with 6 visible lines. Tested board-to-board: RP2040 Pico (Device) to Feather RP2040 (Host), SSD1306 128x64 OLED showing decoded MIDI 2.0 messages in real time. --- examples/host/midi2_host_feather/CMakeLists.txt | 38 +++ examples/host/midi2_host_feather/Makefile | 1 + examples/host/midi2_host_feather/src/display.c | 276 +++++++++++++++++++++ examples/host/midi2_host_feather/src/display.h | 46 ++++ examples/host/midi2_host_feather/src/font5x7.h | 107 ++++++++ examples/host/midi2_host_feather/src/main.c | 267 ++++++++++++++++++++ examples/host/midi2_host_feather/src/tusb_config.h | 91 +++++++ 7 files changed, 826 insertions(+) create mode 100644 examples/host/midi2_host_feather/CMakeLists.txt create mode 100644 examples/host/midi2_host_feather/Makefile create mode 100644 examples/host/midi2_host_feather/src/display.c create mode 100644 examples/host/midi2_host_feather/src/display.h create mode 100644 examples/host/midi2_host_feather/src/font5x7.h create mode 100644 examples/host/midi2_host_feather/src/main.c create mode 100644 examples/host/midi2_host_feather/src/tusb_config.h diff --git a/examples/host/midi2_host_feather/CMakeLists.txt b/examples/host/midi2_host_feather/CMakeLists.txt new file mode 100644 index 000000000..5d34b8170 --- /dev/null +++ b/examples/host/midi2_host_feather/CMakeLists.txt @@ -0,0 +1,38 @@ +cmake_minimum_required(VERSION 3.20) + +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake) + +project(midi2_host_feather C CXX ASM) + +family_initialize_project(${PROJECT_NAME} ${CMAKE_CURRENT_LIST_DIR}) + +# This example requires PIO-USB and Pico SDK (I2C, SSD1306 display) +if(NOT FAMILY STREQUAL "rp2040") + return() +endif() + +add_executable(${PROJECT_NAME}) + +target_sources(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src/main.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/display.c +) + +target_include_directories(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src +) + +family_configure_host_example(${PROJECT_NAME} noos) + +# Adafruit Feather RP2040 USB Host: PIO-USB on GP16/GP17 +target_compile_definitions(${PROJECT_NAME} PRIVATE + PIO_USB_DP_PIN_DEFAULT=16 +) +target_compile_options(${PROJECT_NAME} PRIVATE + -Wno-type-limits +) + +# SSD1306 display (I2C via STEMMA QT) +target_link_libraries(${PROJECT_NAME} PUBLIC + hardware_i2c +) diff --git a/examples/host/midi2_host_feather/Makefile b/examples/host/midi2_host_feather/Makefile new file mode 100644 index 000000000..09172c37f --- /dev/null +++ b/examples/host/midi2_host_feather/Makefile @@ -0,0 +1 @@ +include ../../../examples/build_system/make/make.mk diff --git a/examples/host/midi2_host_feather/src/display.c b/examples/host/midi2_host_feather/src/display.c new file mode 100644 index 000000000..91039fa16 --- /dev/null +++ b/examples/host/midi2_host_feather/src/display.c @@ -0,0 +1,276 @@ +/* + * 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. + */ + +// SSD1306 OLED display driver (128x64, I2C) for MIDI 2.0 Host example. +// Adafruit Feather RP2040 USB Host: I2C1 via STEMMA QT (SDA = GP2, SCL = GP3) +// +// Three display phases: +// 1. Splash : title + credits (shown during init) +// 2. Connecting : spinner animation while waiting for device +// 3. Live : header + 6 scrolling log lines + status bar + +#include "display.h" +#include +#include +#include "pico/stdlib.h" +#include "hardware/i2c.h" +#include "hardware/gpio.h" + +#define I2C_PORT i2c1 +#define I2C_SDA 2 +#define I2C_SCL 3 +#define I2C_FREQ 400000 +#define SSD1306_ADDR 0x3C + +#define SCR_W 128 +#define SCR_H 64 +#define PAGES (SCR_H / 8) +#define CHARS_PER_LINE 21 + +//--------------------------------------------------------------------+ +// Framebuffer +//--------------------------------------------------------------------+ + +static uint8_t fb[SCR_W * PAGES]; + +//--------------------------------------------------------------------+ +// Log buffer (6 lines for live view) +//--------------------------------------------------------------------+ + +#define LOG_LINES 6 +static char log_lines[LOG_LINES][CHARS_PER_LINE + 1]; +static int log_count = 0; + +//--------------------------------------------------------------------+ +// Minimal 5x7 font (ASCII 32-126) +//--------------------------------------------------------------------+ + +#include "font5x7.h" + +//--------------------------------------------------------------------+ +// SSD1306 I2C low-level +//--------------------------------------------------------------------+ + +static void ssd_cmd(uint8_t cmd) { + uint8_t buf[2] = { 0x00, cmd }; + i2c_write_blocking(I2C_PORT, SSD1306_ADDR, buf, 2, false); +} + +static void ssd_data(const uint8_t* data, size_t len) { + uint8_t buf[SCR_W + 1]; + buf[0] = 0x40; + size_t chunk = (len > SCR_W) ? SCR_W : len; + memcpy(buf + 1, data, chunk); + i2c_write_blocking(I2C_PORT, SSD1306_ADDR, buf, chunk + 1, false); +} + +static void ssd_flush(void) { + ssd_cmd(0x21); ssd_cmd(0); ssd_cmd(127); + ssd_cmd(0x22); ssd_cmd(0); ssd_cmd(7); + for (int page = 0; page < PAGES; page++) { + ssd_data(&fb[page * SCR_W], SCR_W); + } +} + +//--------------------------------------------------------------------+ +// Framebuffer drawing +//--------------------------------------------------------------------+ + +static void fb_clear(void) { + memset(fb, 0, sizeof(fb)); +} + +static void fb_char(int x, int y, char c) { + if (c < 32 || c > 126) c = '?'; + const uint8_t* glyph = font5x7 + (c - 32) * 5; + int page = y / 8; + int bit_offset = y % 8; + + if (page >= PAGES || x + 5 > SCR_W) return; + + for (int col = 0; col < 5; col++) { + uint8_t column_data = glyph[col]; + fb[(page * SCR_W) + x + col] |= (uint8_t)(column_data << bit_offset); + if (bit_offset > 0 && page + 1 < PAGES) { + fb[((page + 1) * SCR_W) + x + col] |= (uint8_t)(column_data >> (8 - bit_offset)); + } + } +} + +static void fb_string(int x, int y, const char* str) { + while (*str) { + fb_char(x, y, *str); + x += 6; + if (x + 6 > SCR_W) break; + str++; + } +} + +// Draw a horizontal line (1px) +static void fb_hline(int x0, int x1, int y) { + int page = y / 8; + uint8_t mask = (uint8_t)(1 << (y % 8)); + if (page >= PAGES) return; + for (int x = x0; x <= x1 && x < SCR_W; x++) { + fb[page * SCR_W + x] |= mask; + } +} + +//--------------------------------------------------------------------+ +// Phase 1: Splash +//--------------------------------------------------------------------+ + +void display_init(void) { + i2c_init(I2C_PORT, I2C_FREQ); + gpio_set_function(I2C_SDA, GPIO_FUNC_I2C); + gpio_set_function(I2C_SCL, GPIO_FUNC_I2C); + gpio_pull_up(I2C_SDA); + gpio_pull_up(I2C_SCL); + + sleep_ms(100); + + // SSD1306 init sequence + ssd_cmd(0xAE); + ssd_cmd(0xD5); ssd_cmd(0x80); + ssd_cmd(0xA8); ssd_cmd(0x3F); + ssd_cmd(0xD3); ssd_cmd(0x00); + ssd_cmd(0x40); + ssd_cmd(0x8D); ssd_cmd(0x14); + ssd_cmd(0x20); ssd_cmd(0x00); + ssd_cmd(0xA1); + ssd_cmd(0xC8); + ssd_cmd(0xDA); ssd_cmd(0x12); + ssd_cmd(0x81); ssd_cmd(0xCF); + ssd_cmd(0xD9); ssd_cmd(0xF1); + ssd_cmd(0xDB); ssd_cmd(0x40); + ssd_cmd(0xA4); + ssd_cmd(0xA6); + ssd_cmd(0xAF); + + // Splash screen + fb_clear(); + fb_string(4, 8, "TinyUSB MIDI 2.0"); + fb_hline(4, 123, 18); + fb_string(16, 24, "USB Host Demo"); + fb_string(4, 40, "Feather RP2040"); + fb_string(4, 52, "PIO-USB + SSD1306"); + ssd_flush(); +} + +//--------------------------------------------------------------------+ +// Phase 2: Connecting (spinner) +//--------------------------------------------------------------------+ + +static const char SPINNER[] = "|/-\\"; + +void display_connecting(uint32_t elapsed_ms) { + int idx = (int)((elapsed_ms / 200) % 4); + + // Clear bottom 2 pages for spinner area + memset(&fb[6 * SCR_W], 0, SCR_W); + memset(&fb[7 * SCR_W], 0, SCR_W); + + char line[CHARS_PER_LINE + 1]; + snprintf(line, sizeof(line), "%c Waiting device...", SPINNER[idx]); + fb_string(4, 52, line); + ssd_flush(); +} + +//--------------------------------------------------------------------+ +// Phase 3: Live view +//--------------------------------------------------------------------+ + +// Layout: +// y=0 : "TinyUSB MIDI 2.0 Host" (header, fixed) +// y=9 : separator line +// y=10 : log line 0 +// y=18 : log line 1 +// y=26 : log line 2 +// y=34 : log line 3 +// y=42 : log line 4 +// y=50 : log line 5 +// y=57 : separator line +// y=58 : status bar + +static bool live_mode = false; + +static void draw_live_frame(void) { + fb_clear(); + fb_string(0, 0, "TinyUSB MIDI 2.0 Host"); + fb_hline(0, 127, 9); + fb_hline(0, 127, 57); +} + +void display_live_begin(void) { + live_mode = true; + log_count = 0; + memset(log_lines, 0, sizeof(log_lines)); + + draw_live_frame(); + fb_string(0, 58, "Connected"); + ssd_flush(); +} + +void display_log(const char* text, uint16_t color) { + (void)color; + + if (!live_mode) { + display_live_begin(); + } + + // Scroll up if full + 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); + log_lines[i][CHARS_PER_LINE] = '\0'; + } + log_count = LOG_LINES - 1; + } + + strncpy(log_lines[log_count], text, CHARS_PER_LINE); + log_lines[log_count][CHARS_PER_LINE] = '\0'; + log_count++; + + // Redraw: header + log + status + draw_live_frame(); + for (int i = 0; i < log_count && i < LOG_LINES; i++) { + fb_string(0, 10 + i * 8, log_lines[i]); + } + ssd_flush(); +} + +void display_status(const char* text) { + if (!live_mode) return; + + // Clear status area (page 7) + memset(&fb[7 * SCR_W], 0, SCR_W); + // Redraw separator (might have been cleared) + fb_hline(0, 127, 57); + + char status[CHARS_PER_LINE + 1]; + strncpy(status, text, CHARS_PER_LINE); + status[CHARS_PER_LINE] = '\0'; + fb_string(0, 58, status); + ssd_flush(); +} diff --git a/examples/host/midi2_host_feather/src/display.h b/examples/host/midi2_host_feather/src/display.h new file mode 100644 index 000000000..ad8ead81d --- /dev/null +++ b/examples/host/midi2_host_feather/src/display.h @@ -0,0 +1,46 @@ +/* + * 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 DISPLAY_H_ +#define DISPLAY_H_ + +#include +#include + +// Initialize SSD1306 display and show splash screen +void display_init(void); + +// Show waiting/connecting animation (call periodically) +void display_connecting(uint32_t elapsed_ms); + +// Transition to live message view +void display_live_begin(void); + +// Add a line to the scrolling log area (6 visible lines) +void display_log(const char* text, uint16_t color); + +// Update the status bar (bottom line) +void display_status(const char* text); + +#endif diff --git a/examples/host/midi2_host_feather/src/font5x7.h b/examples/host/midi2_host_feather/src/font5x7.h new file mode 100644 index 000000000..e515faac1 --- /dev/null +++ b/examples/host/midi2_host_feather/src/font5x7.h @@ -0,0 +1,107 @@ +// Minimal 5x7 font, ASCII 32-126. Public domain. +// 5 bytes per character (5 columns, 7 rows, LSB=top row) + +#ifndef FONT5X7_H_ +#define FONT5X7_H_ + +#include + +static const uint8_t font5x7[] = { + 0x00,0x00,0x00,0x00,0x00, // 32 (space) + 0x00,0x00,0x5F,0x00,0x00, // 33 ! + 0x00,0x07,0x00,0x07,0x00, // 34 " + 0x14,0x7F,0x14,0x7F,0x14, // 35 # + 0x24,0x2A,0x7F,0x2A,0x12, // 36 $ + 0x23,0x13,0x08,0x64,0x62, // 37 % + 0x36,0x49,0x55,0x22,0x50, // 38 & + 0x00,0x05,0x03,0x00,0x00, // 39 ' + 0x00,0x1C,0x22,0x41,0x00, // 40 ( + 0x00,0x41,0x22,0x1C,0x00, // 41 ) + 0x08,0x2A,0x1C,0x2A,0x08, // 42 * + 0x08,0x08,0x3E,0x08,0x08, // 43 + + 0x00,0x50,0x30,0x00,0x00, // 44 , + 0x08,0x08,0x08,0x08,0x08, // 45 - + 0x00,0x60,0x60,0x00,0x00, // 46 . + 0x20,0x10,0x08,0x04,0x02, // 47 / + 0x3E,0x51,0x49,0x45,0x3E, // 48 0 + 0x00,0x42,0x7F,0x40,0x00, // 49 1 + 0x42,0x61,0x51,0x49,0x46, // 50 2 + 0x21,0x41,0x45,0x4B,0x31, // 51 3 + 0x18,0x14,0x12,0x7F,0x10, // 52 4 + 0x27,0x45,0x45,0x45,0x39, // 53 5 + 0x3C,0x4A,0x49,0x49,0x30, // 54 6 + 0x01,0x71,0x09,0x05,0x03, // 55 7 + 0x36,0x49,0x49,0x49,0x36, // 56 8 + 0x06,0x49,0x49,0x29,0x1E, // 57 9 + 0x00,0x36,0x36,0x00,0x00, // 58 : + 0x00,0x56,0x36,0x00,0x00, // 59 ; + 0x00,0x08,0x14,0x22,0x41, // 60 < + 0x14,0x14,0x14,0x14,0x14, // 61 = + 0x41,0x22,0x14,0x08,0x00, // 62 > + 0x02,0x01,0x51,0x09,0x06, // 63 ? + 0x32,0x49,0x79,0x41,0x3E, // 64 @ + 0x7E,0x11,0x11,0x11,0x7E, // 65 A + 0x7F,0x49,0x49,0x49,0x36, // 66 B + 0x3E,0x41,0x41,0x41,0x22, // 67 C + 0x7F,0x41,0x41,0x22,0x1C, // 68 D + 0x7F,0x49,0x49,0x49,0x41, // 69 E + 0x7F,0x09,0x09,0x01,0x01, // 70 F + 0x3E,0x41,0x41,0x51,0x32, // 71 G + 0x7F,0x08,0x08,0x08,0x7F, // 72 H + 0x00,0x41,0x7F,0x41,0x00, // 73 I + 0x20,0x40,0x41,0x3F,0x01, // 74 J + 0x7F,0x08,0x14,0x22,0x41, // 75 K + 0x7F,0x40,0x40,0x40,0x40, // 76 L + 0x7F,0x02,0x04,0x02,0x7F, // 77 M + 0x7F,0x04,0x08,0x10,0x7F, // 78 N + 0x3E,0x41,0x41,0x41,0x3E, // 79 O + 0x7F,0x09,0x09,0x09,0x06, // 80 P + 0x3E,0x41,0x51,0x21,0x5E, // 81 Q + 0x7F,0x09,0x19,0x29,0x46, // 82 R + 0x46,0x49,0x49,0x49,0x31, // 83 S + 0x01,0x01,0x7F,0x01,0x01, // 84 T + 0x3F,0x40,0x40,0x40,0x3F, // 85 U + 0x1F,0x20,0x40,0x20,0x1F, // 86 V + 0x7F,0x20,0x18,0x20,0x7F, // 87 W + 0x63,0x14,0x08,0x14,0x63, // 88 X + 0x03,0x04,0x78,0x04,0x03, // 89 Y + 0x61,0x51,0x49,0x45,0x43, // 90 Z + 0x00,0x00,0x7F,0x41,0x41, // 91 [ + 0x02,0x04,0x08,0x10,0x20, // 92 backslash + 0x41,0x41,0x7F,0x00,0x00, // 93 ] + 0x04,0x02,0x01,0x02,0x04, // 94 ^ + 0x40,0x40,0x40,0x40,0x40, // 95 _ + 0x00,0x01,0x02,0x04,0x00, // 96 ` + 0x20,0x54,0x54,0x54,0x78, // 97 a + 0x7F,0x48,0x44,0x44,0x38, // 98 b + 0x38,0x44,0x44,0x44,0x20, // 99 c + 0x38,0x44,0x44,0x48,0x7F, // 100 d + 0x38,0x54,0x54,0x54,0x18, // 101 e + 0x08,0x7E,0x09,0x01,0x02, // 102 f + 0x08,0x14,0x54,0x54,0x3C, // 103 g + 0x7F,0x08,0x04,0x04,0x78, // 104 h + 0x00,0x44,0x7D,0x40,0x00, // 105 i + 0x20,0x40,0x44,0x3D,0x00, // 106 j + 0x00,0x7F,0x10,0x28,0x44, // 107 k + 0x00,0x41,0x7F,0x40,0x00, // 108 l + 0x7C,0x04,0x18,0x04,0x78, // 109 m + 0x7C,0x08,0x04,0x04,0x78, // 110 n + 0x38,0x44,0x44,0x44,0x38, // 111 o + 0x7C,0x14,0x14,0x14,0x08, // 112 p + 0x08,0x14,0x14,0x18,0x7C, // 113 q + 0x7C,0x08,0x04,0x04,0x08, // 114 r + 0x48,0x54,0x54,0x54,0x20, // 115 s + 0x04,0x3F,0x44,0x40,0x20, // 116 t + 0x3C,0x40,0x40,0x20,0x7C, // 117 u + 0x1C,0x20,0x40,0x20,0x1C, // 118 v + 0x3C,0x40,0x30,0x40,0x3C, // 119 w + 0x44,0x28,0x10,0x28,0x44, // 120 x + 0x0C,0x50,0x50,0x50,0x3C, // 121 y + 0x44,0x64,0x54,0x4C,0x44, // 122 z + 0x00,0x08,0x36,0x41,0x00, // 123 { + 0x00,0x00,0x7F,0x00,0x00, // 124 | + 0x00,0x41,0x36,0x08,0x00, // 125 } + 0x10,0x08,0x08,0x10,0x08, // 126 ~ +}; + +#endif diff --git a/examples/host/midi2_host_feather/src/main.c b/examples/host/midi2_host_feather/src/main.c new file mode 100644 index 000000000..7c996355d --- /dev/null +++ b/examples/host/midi2_host_feather/src/main.c @@ -0,0 +1,267 @@ +/* + * 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. + */ + +// MIDI 2.0 Host Example: Adafruit Feather RP2040 USB Host +// +// Receives UMP from a MIDI 2.0 Device via PIO-USB (GP16/GP17). +// SSD1306 OLED (I2C1, GP2/GP3) shows splash, spinner, then live UMP messages. + +#include +#include +#include "bsp/board_api.h" +#include "tusb.h" +#include "hardware/gpio.h" +#include "class/midi/midi2_host.h" +#include "class/midi/midi.h" +#include "display.h" + +//--------------------------------------------------------------------+ +// State +//--------------------------------------------------------------------+ + +static uint32_t note_count = 0; +static uint8_t midi2_idx = 0xFF; +static bool device_connected = false; +static bool mounted = false; + +//--------------------------------------------------------------------+ +// Note name helper +//--------------------------------------------------------------------+ + +static const char* NOTE_NAMES[] = { + "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" +}; + +static void note_name(uint8_t pitch, char* buf, size_t len) { + int octave = (pitch / 12) - 1; + snprintf(buf, len, "%s%d", NOTE_NAMES[pitch % 12], octave); +} + +//--------------------------------------------------------------------+ +// UMP decoder +//--------------------------------------------------------------------+ + +static void decode_ump(const uint32_t* words, uint8_t word_count) { + uint8_t mt = (uint8_t)((words[0] >> 28) & 0x0F); + char line[64]; + + if (mt == 0x4 && word_count >= 2) { + uint8_t status = (uint8_t)((words[0] >> 20) & 0x0F); + uint8_t channel = (uint8_t)((words[0] >> 16) & 0x0F); + + switch (status) { + case 0x9: { + uint8_t pitch = (uint8_t)((words[0] >> 8) & 0x7F); + uint16_t vel = (uint16_t)((words[1] >> 16) & 0xFFFF); + char nn[6]; + note_name(pitch, nn, sizeof(nn)); + snprintf(line, sizeof(line), "NoteOn %s ch%u v%04X", nn, channel, vel); + display_log(line, 0x07E0); + note_count++; + break; + } + case 0x8: { + uint8_t pitch = (uint8_t)((words[0] >> 8) & 0x7F); + char nn[6]; + note_name(pitch, nn, sizeof(nn)); + snprintf(line, sizeof(line), "NoteOff %s ch%u", nn, channel); + display_log(line, 0x8410); + break; + } + case 0xB: { + uint8_t idx = (uint8_t)((words[0] >> 8) & 0x7F); + snprintf(line, sizeof(line), "CC%-3u %08lX", idx, (unsigned long)words[1]); + display_log(line, 0x001F); + break; + } + case 0xC: { + uint8_t prog = (uint8_t)((words[1] >> 24) & 0x7F); + snprintf(line, sizeof(line), "ProgChg %u", prog); + display_log(line, 0xFFE0); + break; + } + case 0xE: { + snprintf(line, sizeof(line), "PBend %08lX", (unsigned long)words[1]); + display_log(line, 0xF81F); + break; + } + case 0xD: { + snprintf(line, sizeof(line), "CPress %08lX", (unsigned long)words[1]); + display_log(line, 0xFC10); + break; + } + case 0xA: { + uint8_t pitch = (uint8_t)((words[0] >> 8) & 0x7F); + char nn[6]; + note_name(pitch, nn, sizeof(nn)); + snprintf(line, sizeof(line), "PolyP %s %08lX", nn, (unsigned long)words[1]); + display_log(line, 0xFC10); + break; + } + case 0xF: { + uint8_t pitch = (uint8_t)((words[0] >> 8) & 0x7F); + uint8_t flags = (uint8_t)(words[0] & 0xFF); + snprintf(line, sizeof(line), "PN-Mgmt n%u f%02X", pitch, flags); + display_log(line, 0x07FF); + break; + } + default: { + snprintf(line, sizeof(line), "M2CVM s=0x%X", status); + display_log(line, 0xFFFF); + break; + } + } + } else if (mt == 0x0) { + uint8_t status = (uint8_t)((words[0] >> 20) & 0x0F); + if (status == 0x2) { + uint16_t ts = words[0] & 0xFFFF; + snprintf(line, sizeof(line), "JR-TS %04X", ts); + display_log(line, 0x8410); + } + } else { + snprintf(line, sizeof(line), "MT=0x%X w0=%08lX", + mt, (unsigned long)words[0]); + display_log(line, 0xFFFF); + } +} + +//--------------------------------------------------------------------+ +// MIDI 2.0 Host Callbacks +//--------------------------------------------------------------------+ + +void tuh_midi2_descriptor_cb(uint8_t idx, const tuh_midi2_descriptor_cb_t* d) { + (void)idx; + char line[48]; + snprintf(line, sizeof(line), "%s RX=%u TX=%u", + d->protocol_version ? "MIDI2" : "MIDI1", + d->rx_cable_count, d->tx_cable_count); + display_log(line, 0x07E0); +} + +void tuh_midi2_mount_cb(uint8_t idx, const tuh_midi2_mount_cb_t* m) { + midi2_idx = idx; + mounted = true; + + display_live_begin(); + + if (m->protocol_version) { + display_log("MIDI 2.0 ready", 0x07E0); + } else { + display_log("MIDI 1.0 device", 0xFFE0); + } + display_status("Receiving..."); +} + +void tuh_midi2_rx_cb(uint8_t idx, uint32_t xferred_bytes) { + (void)xferred_bytes; + + uint32_t words[16]; + while (1) { + uint32_t n = tuh_midi2_ump_read(idx, words, 16); + if (n == 0) break; + + uint32_t i = 0; + while (i < n) { + uint8_t mt = (uint8_t)((words[i] >> 28) & 0x0F); + uint8_t wc = midi2_ump_word_count(mt); + if (i + wc > n) break; + decode_ump(&words[i], wc); + i += wc; + } + } +} + +void tuh_midi2_tx_cb(uint8_t idx, uint32_t xferred_bytes) { + (void)idx; (void)xferred_bytes; +} + +void tuh_midi2_umount_cb(uint8_t idx) { + (void)idx; + midi2_idx = 0xFF; + device_connected = false; + mounted = false; + note_count = 0; + display_log("Disconnected", 0xF800); + display_status("Waiting..."); +} + +//--------------------------------------------------------------------+ +// Main +//--------------------------------------------------------------------+ + +int main(void) { + board_init(); + + // Enable 5V to USB-A port (Feather RP2040 USB Host: GP18) + gpio_init(18); + gpio_set_dir(18, GPIO_OUT); + gpio_put(18, 1); + + display_init(); + sleep_ms(1500); + + tusb_rhport_init_t host_init = { + .role = TUSB_ROLE_HOST, + .speed = TUSB_SPEED_FULL, + }; + tusb_init(BOARD_TUH_RHPORT, &host_init); + + uint32_t last_status_ms = 0; + + while (1) { + tuh_task(); + + uint32_t now = tusb_time_millis_api(); + + if (!mounted) { + // Spinner while waiting for device + if (now - last_status_ms > 200) { + last_status_ms = now; + display_connecting(now); + } + } else { + // Update note count every 2 seconds + if (now - last_status_ms > 2000) { + last_status_ms = now; + char line[22]; + snprintf(line, sizeof(line), "Notes: %lu", (unsigned long)note_count); + display_status(line); + } + } + } + + return 0; +} + +// Generic USB device mount/unmount +void tuh_mount_cb(uint8_t daddr) { + (void)daddr; + device_connected = true; +} + +void tuh_umount_cb(uint8_t daddr) { + (void)daddr; + device_connected = false; + mounted = false; +} diff --git a/examples/host/midi2_host_feather/src/tusb_config.h b/examples/host/midi2_host_feather/src/tusb_config.h new file mode 100644 index 000000000..362d0e2d0 --- /dev/null +++ b/examples/host/midi2_host_feather/src/tusb_config.h @@ -0,0 +1,91 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TUSB_CONFIG_H_ +#define TUSB_CONFIG_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +//--------------------------------------------------------------------+ +// Common Configuration +//--------------------------------------------------------------------+ + +#ifndef CFG_TUSB_MCU +#error CFG_TUSB_MCU must be defined +#endif + +#ifndef CFG_TUSB_OS +#define CFG_TUSB_OS OPT_OS_NONE +#endif + +#ifndef CFG_TUSB_DEBUG +#define CFG_TUSB_DEBUG 0 +#endif + +#ifndef CFG_TUH_MEM_SECTION +#define CFG_TUH_MEM_SECTION +#endif + +#ifndef CFG_TUH_MEM_ALIGN +#define CFG_TUH_MEM_ALIGN __attribute__ ((aligned(4))) +#endif + +//--------------------------------------------------------------------+ +// Host Configuration +// Adafruit Feather RP2040 USB Host: PIO-USB on GP16/GP17 (USB-A Host port) +//--------------------------------------------------------------------+ + +#define CFG_TUH_ENABLED 1 + +// PIO-USB Host on rhport 1 (USB-A connector on GP16/GP17) +#define CFG_TUH_RPI_PIO_USB 1 +#define BOARD_TUH_RHPORT 1 + +#ifndef BOARD_TUH_MAX_SPEED +#define BOARD_TUH_MAX_SPEED OPT_MODE_FULL_SPEED +#endif + +#define CFG_TUH_MAX_SPEED BOARD_TUH_MAX_SPEED + +//--------------------------------------------------------------------+ +// Driver Configuration +//--------------------------------------------------------------------+ + +#define CFG_TUH_ENUMERATION_BUFSIZE 256 + +#define CFG_TUH_HUB 0 +#define CFG_TUH_DEVICE_MAX 1 + +// MIDI 2.0 Host +#define CFG_TUH_MIDI2 1 +#define CFG_TUH_MIDI2_RX_BUFSIZE 512 +#define CFG_TUH_MIDI2_TX_BUFSIZE 512 + +#ifdef __cplusplus +} +#endif + +#endif -- cgit v1.3.1 From c4847006dbb7565b71055837103ec788c4cb6fee Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Tue, 7 Apr 2026 23:04:03 -0300 Subject: fix: skip midi2_host_feather build on non-RP2040 platforms The Makefile was missing the FAMILY=rp2040 guard, causing CI failures on stm32h7rs and other non-RP2040 targets. Matches the approach used by midi2_host. --- examples/host/midi2_host_feather/Makefile | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/examples/host/midi2_host_feather/Makefile b/examples/host/midi2_host_feather/Makefile index 09172c37f..933d19434 100644 --- a/examples/host/midi2_host_feather/Makefile +++ b/examples/host/midi2_host_feather/Makefile @@ -1 +1,27 @@ -include ../../../examples/build_system/make/make.mk +# This example requires RP2040 (PIO-USB, Pico SDK I2C, SSD1306 display) +ifeq (,$(findstring rp2040,$(FAMILY))) +$(info Skipping midi2_host_feather: 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 -- cgit v1.3.1 From b4880761bcadbee86d047e0a3b329859926caad5 Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Tue, 7 Apr 2026 23:46:10 -0300 Subject: refactor: replace midi2_host (Waveshare) with midi2_host_feather Remove the Waveshare RP2350-USB-A host example and board definition. The Feather RP2040 USB Host is the TinyUSB reference board for PIO-USB host and does not require the CFG_TUSB_DEBUG workaround needed by the Waveshare (R13 pull-up issue). One host example is sufficient for the PR. The Feather variant has an improved display with splash, spinner, and 6-line live view. --- examples/host/CMakeLists.txt | 2 +- examples/host/midi2_host/CMakeLists.txt | 38 --- examples/host/midi2_host/Makefile | 27 -- examples/host/midi2_host/src/display.c | 221 -------------- examples/host/midi2_host/src/display.h | 54 ---- examples/host/midi2_host/src/font5x7.h | 107 ------- examples/host/midi2_host/src/main.c | 326 --------------------- examples/host/midi2_host/src/tusb_config.h | 91 ------ .../boards/waveshare_rp2350_usb_a/board.cmake | 2 - .../rp2040/boards/waveshare_rp2350_usb_a/board.h | 49 ---- 10 files changed, 1 insertion(+), 916 deletions(-) delete mode 100644 examples/host/midi2_host/CMakeLists.txt delete mode 100644 examples/host/midi2_host/Makefile delete mode 100644 examples/host/midi2_host/src/display.c delete mode 100644 examples/host/midi2_host/src/display.h delete mode 100644 examples/host/midi2_host/src/font5x7.h delete mode 100644 examples/host/midi2_host/src/main.c delete mode 100644 examples/host/midi2_host/src/tusb_config.h delete mode 100644 hw/bsp/rp2040/boards/waveshare_rp2350_usb_a/board.cmake delete mode 100644 hw/bsp/rp2040/boards/waveshare_rp2350_usb_a/board.h diff --git a/examples/host/CMakeLists.txt b/examples/host/CMakeLists.txt index 7c74e3c73..2cf2c10af 100644 --- a/examples/host/CMakeLists.txt +++ b/examples/host/CMakeLists.txt @@ -13,7 +13,7 @@ set(EXAMPLE_LIST device_info hid_controller midi_rx - midi2_host + midi2_host_feather msc_file_explorer msc_file_explorer_freertos ) diff --git a/examples/host/midi2_host/CMakeLists.txt b/examples/host/midi2_host/CMakeLists.txt deleted file mode 100644 index cd70d122a..000000000 --- a/examples/host/midi2_host/CMakeLists.txt +++ /dev/null @@ -1,38 +0,0 @@ -cmake_minimum_required(VERSION 3.20) - -include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake) - -project(midi2_host C CXX ASM) - -family_initialize_project(${PROJECT_NAME} ${CMAKE_CURRENT_LIST_DIR}) - -# This example requires PIO-USB and Pico SDK (I2C, SSD1306 display) -if(NOT FAMILY STREQUAL "rp2040") - return() -endif() - -add_executable(${PROJECT_NAME}) - -target_sources(${PROJECT_NAME} PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/src/main.c - ${CMAKE_CURRENT_SOURCE_DIR}/src/display.c -) - -target_include_directories(${PROJECT_NAME} PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/src -) - -family_configure_host_example(${PROJECT_NAME} noos) - -# Waveshare RP2350-USB-A: PIO-USB on GP12/GP13 -target_compile_definitions(${PROJECT_NAME} PRIVATE - PIO_USB_DP_PIN_DEFAULT=12 -) -target_compile_options(${PROJECT_NAME} PRIVATE - -Wno-type-limits -) - -# SSD1306 display (I2C) -target_link_libraries(${PROJECT_NAME} PUBLIC - hardware_i2c -) diff --git a/examples/host/midi2_host/Makefile b/examples/host/midi2_host/Makefile deleted file mode 100644 index 2b4660516..000000000 --- a/examples/host/midi2_host/Makefile +++ /dev/null @@ -1,27 +0,0 @@ -# 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/examples/host/midi2_host/src/display.c b/examples/host/midi2_host/src/display.c deleted file mode 100644 index 4745c2961..000000000 --- a/examples/host/midi2_host/src/display.c +++ /dev/null @@ -1,221 +0,0 @@ -/* - * 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. - */ - -// SSD1306 OLED display driver (128x64, I2C) for MIDI 2.0 Host example. -// Minimal text-only implementation, no graphics library. -// I2C0: SDA = GP4, SCL = GP5, Address = 0x3C - -#include "display.h" -#include -#include -#include "pico/stdlib.h" -#include "hardware/i2c.h" -#include "hardware/gpio.h" - -#define I2C_PORT i2c0 -#define I2C_SDA 4 -#define I2C_SCL 5 -#define I2C_FREQ 400000 -#define SSD1306_ADDR 0x3C - -#define SCR_W 128 -#define SCR_H 64 -#define PAGES (SCR_H / 8) // 8 pages -#define CHARS_PER_LINE 21 // 128 / 6 = 21 chars - -// Framebuffer -static uint8_t fb[SCR_W * PAGES]; - -// Log buffer -#define LOG_LINES 3 -static char log_lines[LOG_LINES][CHARS_PER_LINE + 1]; -static int log_count = 0; - -// Status line -static char status_text[CHARS_PER_LINE + 1] = ""; - -//--------------------------------------------------------------------+ -// Minimal 5x7 font (ASCII 32-126) -//--------------------------------------------------------------------+ -#include "font5x7.h" - -//--------------------------------------------------------------------+ -// SSD1306 I2C commands -//--------------------------------------------------------------------+ - -static void ssd_cmd(uint8_t cmd) { - uint8_t buf[2] = { 0x00, cmd }; // Co=0, D/C=0 - i2c_write_blocking(I2C_PORT, SSD1306_ADDR, buf, 2, false); -} - -static void ssd_data(const uint8_t* data, size_t len) { - uint8_t buf[SCR_W + 1]; - buf[0] = 0x40; // Co=0, D/C=1 - size_t chunk = (len > SCR_W) ? SCR_W : len; - memcpy(buf + 1, data, chunk); - i2c_write_blocking(I2C_PORT, SSD1306_ADDR, buf, chunk + 1, false); -} - -static void ssd_flush(void) { - ssd_cmd(0x21); ssd_cmd(0); ssd_cmd(127); // Column range - ssd_cmd(0x22); ssd_cmd(0); ssd_cmd(7); // Page range - for (int page = 0; page < PAGES; page++) { - ssd_data(&fb[page * SCR_W], SCR_W); - } -} - -//--------------------------------------------------------------------+ -// Framebuffer drawing -//--------------------------------------------------------------------+ - -static void fb_clear(void) { - memset(fb, 0, sizeof(fb)); -} - -static void fb_char(int x, int y, char c) { - if (c < 32 || c > 126) c = '?'; - const uint8_t* glyph = font5x7 + (c - 32) * 5; - int page = y / 8; - int bit_offset = y % 8; - - if (page >= PAGES || x + 5 > SCR_W) return; - - for (int col = 0; col < 5; col++) { - uint8_t column_data = glyph[col]; - fb[(page * SCR_W) + x + col] |= (uint8_t)(column_data << bit_offset); - if (bit_offset > 0 && page + 1 < PAGES) { - fb[((page + 1) * SCR_W) + x + col] |= (uint8_t)(column_data >> (8 - bit_offset)); - } - } -} - -static void fb_string(int x, int y, const char* str) { - while (*str) { - fb_char(x, y, *str); - x += 6; - if (x + 6 > SCR_W) break; - str++; - } -} - -//--------------------------------------------------------------------+ -// Display API -//--------------------------------------------------------------------+ - -void display_init(void) { - i2c_init(I2C_PORT, I2C_FREQ); - gpio_set_function(I2C_SDA, GPIO_FUNC_I2C); - gpio_set_function(I2C_SCL, GPIO_FUNC_I2C); - gpio_pull_up(I2C_SDA); - gpio_pull_up(I2C_SCL); - - sleep_ms(100); - - // SSD1306 init sequence - ssd_cmd(0xAE); // Display off - ssd_cmd(0xD5); ssd_cmd(0x80); // Clock div - ssd_cmd(0xA8); ssd_cmd(0x3F); // Multiplex 64 - ssd_cmd(0xD3); ssd_cmd(0x00); // Display offset - ssd_cmd(0x40); // Start line 0 - ssd_cmd(0x8D); ssd_cmd(0x14); // Charge pump on - ssd_cmd(0x20); ssd_cmd(0x00); // Horizontal addressing - ssd_cmd(0xA1); // Segment remap - ssd_cmd(0xC8); // COM scan direction - ssd_cmd(0xDA); ssd_cmd(0x12); // COM pins - ssd_cmd(0x81); ssd_cmd(0xCF); // Contrast - ssd_cmd(0xD9); ssd_cmd(0xF1); // Pre-charge - ssd_cmd(0xDB); ssd_cmd(0x40); // VCOMH deselect - ssd_cmd(0xA4); // Display from RAM - ssd_cmd(0xA6); // Normal display - ssd_cmd(0xAF); // Display on - - fb_clear(); - fb_string(0, 0, "MIDI 2.0 Host"); - ssd_flush(); -} - -void display_checklist_update(const checklist_t* ck) { - struct { const char* label; bool ok; } items[] = { - { "PWR", ck->pwr_on }, - { "TinyUSB", ck->tusb_init }, - { "USB bus", ck->bus_active }, - { "Device", ck->device_connected }, - { "Descript", ck->descriptor_parsed }, - { "Alt1 UMP", ck->alt_setting_ok }, - { "Mount", ck->mounted }, - { "RX UMP", ck->receiving }, - }; - - // Clear checklist area (lines 1-4, two columns) - for (int p = 1; p <= 4; p++) { - memset(&fb[p * SCR_W], 0, SCR_W); - } - - for (int i = 0; i < 8; i++) { - int col = (i < 4) ? 0 : 64; - int row = (i < 4) ? i : i - 4; - int y = 9 + row * 8; - char line[12]; - snprintf(line, sizeof(line), "%s %s", items[i].ok ? "OK" : "..", items[i].label); - fb_string(col, y, line); - } - - ssd_flush(); -} - -void display_log(const char* text, uint16_t color) { - (void)color; // SSD1306 is monochrome - - 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); - log_lines[i][CHARS_PER_LINE] = '\0'; - } - log_count = LOG_LINES - 1; - } - - strncpy(log_lines[log_count], text, CHARS_PER_LINE); - log_lines[log_count][CHARS_PER_LINE] = '\0'; - log_count++; - - // Draw log area (pages 5-6, y=40-55) - memset(&fb[5 * SCR_W], 0, SCR_W); - memset(&fb[6 * SCR_W], 0, SCR_W); - - for (int i = 0; i < log_count && i < LOG_LINES; i++) { - fb_string(0, 41 + i * 8, log_lines[i]); - } - - ssd_flush(); -} - -void display_status(const char* text) { - strncpy(status_text, text, CHARS_PER_LINE); - status_text[CHARS_PER_LINE] = '\0'; - - // Status on last page (y=56) - memset(&fb[7 * SCR_W], 0, SCR_W); - fb_string(0, 56, status_text); - ssd_flush(); -} diff --git a/examples/host/midi2_host/src/display.h b/examples/host/midi2_host/src/display.h deleted file mode 100644 index a8b0a4b5e..000000000 --- a/examples/host/midi2_host/src/display.h +++ /dev/null @@ -1,54 +0,0 @@ -/* - * 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 DISPLAY_H_ -#define DISPLAY_H_ - -#include -#include - -typedef struct { - bool pwr_on; - bool tusb_init; - bool bus_active; - bool device_connected; - bool descriptor_parsed; - bool alt_setting_ok; - bool mounted; - bool receiving; -} checklist_t; - -// Initialize SSD1306 display (I2C on GP4/GP5) -void display_init(void); - -// Redraw the checklist area (top portion) -void display_checklist_update(const checklist_t* ck); - -// Add a line to the scrolling log area -void display_log(const char* text, uint16_t color); - -// Update the status bar (bottom line) -void display_status(const char* text); - -#endif diff --git a/examples/host/midi2_host/src/font5x7.h b/examples/host/midi2_host/src/font5x7.h deleted file mode 100644 index e515faac1..000000000 --- a/examples/host/midi2_host/src/font5x7.h +++ /dev/null @@ -1,107 +0,0 @@ -// Minimal 5x7 font, ASCII 32-126. Public domain. -// 5 bytes per character (5 columns, 7 rows, LSB=top row) - -#ifndef FONT5X7_H_ -#define FONT5X7_H_ - -#include - -static const uint8_t font5x7[] = { - 0x00,0x00,0x00,0x00,0x00, // 32 (space) - 0x00,0x00,0x5F,0x00,0x00, // 33 ! - 0x00,0x07,0x00,0x07,0x00, // 34 " - 0x14,0x7F,0x14,0x7F,0x14, // 35 # - 0x24,0x2A,0x7F,0x2A,0x12, // 36 $ - 0x23,0x13,0x08,0x64,0x62, // 37 % - 0x36,0x49,0x55,0x22,0x50, // 38 & - 0x00,0x05,0x03,0x00,0x00, // 39 ' - 0x00,0x1C,0x22,0x41,0x00, // 40 ( - 0x00,0x41,0x22,0x1C,0x00, // 41 ) - 0x08,0x2A,0x1C,0x2A,0x08, // 42 * - 0x08,0x08,0x3E,0x08,0x08, // 43 + - 0x00,0x50,0x30,0x00,0x00, // 44 , - 0x08,0x08,0x08,0x08,0x08, // 45 - - 0x00,0x60,0x60,0x00,0x00, // 46 . - 0x20,0x10,0x08,0x04,0x02, // 47 / - 0x3E,0x51,0x49,0x45,0x3E, // 48 0 - 0x00,0x42,0x7F,0x40,0x00, // 49 1 - 0x42,0x61,0x51,0x49,0x46, // 50 2 - 0x21,0x41,0x45,0x4B,0x31, // 51 3 - 0x18,0x14,0x12,0x7F,0x10, // 52 4 - 0x27,0x45,0x45,0x45,0x39, // 53 5 - 0x3C,0x4A,0x49,0x49,0x30, // 54 6 - 0x01,0x71,0x09,0x05,0x03, // 55 7 - 0x36,0x49,0x49,0x49,0x36, // 56 8 - 0x06,0x49,0x49,0x29,0x1E, // 57 9 - 0x00,0x36,0x36,0x00,0x00, // 58 : - 0x00,0x56,0x36,0x00,0x00, // 59 ; - 0x00,0x08,0x14,0x22,0x41, // 60 < - 0x14,0x14,0x14,0x14,0x14, // 61 = - 0x41,0x22,0x14,0x08,0x00, // 62 > - 0x02,0x01,0x51,0x09,0x06, // 63 ? - 0x32,0x49,0x79,0x41,0x3E, // 64 @ - 0x7E,0x11,0x11,0x11,0x7E, // 65 A - 0x7F,0x49,0x49,0x49,0x36, // 66 B - 0x3E,0x41,0x41,0x41,0x22, // 67 C - 0x7F,0x41,0x41,0x22,0x1C, // 68 D - 0x7F,0x49,0x49,0x49,0x41, // 69 E - 0x7F,0x09,0x09,0x01,0x01, // 70 F - 0x3E,0x41,0x41,0x51,0x32, // 71 G - 0x7F,0x08,0x08,0x08,0x7F, // 72 H - 0x00,0x41,0x7F,0x41,0x00, // 73 I - 0x20,0x40,0x41,0x3F,0x01, // 74 J - 0x7F,0x08,0x14,0x22,0x41, // 75 K - 0x7F,0x40,0x40,0x40,0x40, // 76 L - 0x7F,0x02,0x04,0x02,0x7F, // 77 M - 0x7F,0x04,0x08,0x10,0x7F, // 78 N - 0x3E,0x41,0x41,0x41,0x3E, // 79 O - 0x7F,0x09,0x09,0x09,0x06, // 80 P - 0x3E,0x41,0x51,0x21,0x5E, // 81 Q - 0x7F,0x09,0x19,0x29,0x46, // 82 R - 0x46,0x49,0x49,0x49,0x31, // 83 S - 0x01,0x01,0x7F,0x01,0x01, // 84 T - 0x3F,0x40,0x40,0x40,0x3F, // 85 U - 0x1F,0x20,0x40,0x20,0x1F, // 86 V - 0x7F,0x20,0x18,0x20,0x7F, // 87 W - 0x63,0x14,0x08,0x14,0x63, // 88 X - 0x03,0x04,0x78,0x04,0x03, // 89 Y - 0x61,0x51,0x49,0x45,0x43, // 90 Z - 0x00,0x00,0x7F,0x41,0x41, // 91 [ - 0x02,0x04,0x08,0x10,0x20, // 92 backslash - 0x41,0x41,0x7F,0x00,0x00, // 93 ] - 0x04,0x02,0x01,0x02,0x04, // 94 ^ - 0x40,0x40,0x40,0x40,0x40, // 95 _ - 0x00,0x01,0x02,0x04,0x00, // 96 ` - 0x20,0x54,0x54,0x54,0x78, // 97 a - 0x7F,0x48,0x44,0x44,0x38, // 98 b - 0x38,0x44,0x44,0x44,0x20, // 99 c - 0x38,0x44,0x44,0x48,0x7F, // 100 d - 0x38,0x54,0x54,0x54,0x18, // 101 e - 0x08,0x7E,0x09,0x01,0x02, // 102 f - 0x08,0x14,0x54,0x54,0x3C, // 103 g - 0x7F,0x08,0x04,0x04,0x78, // 104 h - 0x00,0x44,0x7D,0x40,0x00, // 105 i - 0x20,0x40,0x44,0x3D,0x00, // 106 j - 0x00,0x7F,0x10,0x28,0x44, // 107 k - 0x00,0x41,0x7F,0x40,0x00, // 108 l - 0x7C,0x04,0x18,0x04,0x78, // 109 m - 0x7C,0x08,0x04,0x04,0x78, // 110 n - 0x38,0x44,0x44,0x44,0x38, // 111 o - 0x7C,0x14,0x14,0x14,0x08, // 112 p - 0x08,0x14,0x14,0x18,0x7C, // 113 q - 0x7C,0x08,0x04,0x04,0x08, // 114 r - 0x48,0x54,0x54,0x54,0x20, // 115 s - 0x04,0x3F,0x44,0x40,0x20, // 116 t - 0x3C,0x40,0x40,0x20,0x7C, // 117 u - 0x1C,0x20,0x40,0x20,0x1C, // 118 v - 0x3C,0x40,0x30,0x40,0x3C, // 119 w - 0x44,0x28,0x10,0x28,0x44, // 120 x - 0x0C,0x50,0x50,0x50,0x3C, // 121 y - 0x44,0x64,0x54,0x4C,0x44, // 122 z - 0x00,0x08,0x36,0x41,0x00, // 123 { - 0x00,0x00,0x7F,0x00,0x00, // 124 | - 0x00,0x41,0x36,0x08,0x00, // 125 } - 0x10,0x08,0x08,0x10,0x08, // 126 ~ -}; - -#endif diff --git a/examples/host/midi2_host/src/main.c b/examples/host/midi2_host/src/main.c deleted file mode 100644 index e23fbfc87..000000000 --- a/examples/host/midi2_host/src/main.c +++ /dev/null @@ -1,326 +0,0 @@ -/* - * 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. - */ - -// MIDI 2.0 Host Receiver Example -// -// Receives UMP from a MIDI 2.0 Device via PIO-USB Host. -// SSD1306 OLED (I2C, 128x64) shows boot checklist then received UMP messages. - -#include -#include -#include "bsp/board_api.h" -#include "tusb.h" -#include "class/midi/midi2_host.h" -#include "class/midi/midi.h" -#include "display.h" - -//--------------------------------------------------------------------+ -// Checklist state -//--------------------------------------------------------------------+ - -static checklist_t ck = { 0 }; -static uint32_t note_count = 0; -static uint8_t midi2_idx = 0xFF; // invalid until mount - -//--------------------------------------------------------------------+ -// Note name helper -//--------------------------------------------------------------------+ - -static const char* NOTE_NAMES[] = { - "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" -}; - -static void note_name(uint8_t pitch, char* buf, size_t len) { - int octave = (pitch / 12) - 1; - snprintf(buf, len, "%s%d", NOTE_NAMES[pitch % 12], octave); -} - -//--------------------------------------------------------------------+ -// UMP Message Type names -//--------------------------------------------------------------------+ - -static const char* mt_name(uint8_t mt) { - switch (mt) { - case 0x0: return "Utility"; - case 0x1: return "System"; - case 0x2: return "M1 CVM"; - case 0x3: return "SysEx7"; - case 0x4: return "M2 CVM"; - case 0x5: return "SysEx8"; - case 0xD: return "Flex"; - case 0xF: return "Stream"; - default: return "?"; - } -} - -//--------------------------------------------------------------------+ -// UMP decoder - extract and display MIDI 2.0 messages -//--------------------------------------------------------------------+ - -static void decode_ump(const uint32_t* words, uint8_t word_count) { - uint8_t mt = (uint8_t)((words[0] >> 28) & 0x0F); - char line[64]; - - if (mt == 0x4 && word_count >= 2) { - // MIDI 2.0 Channel Voice Message - uint8_t status = (uint8_t)((words[0] >> 20) & 0x0F); - uint8_t channel = (uint8_t)((words[0] >> 16) & 0x0F); - - switch (status) { - case 0x9: { // Note On - uint8_t pitch = (uint8_t)((words[0] >> 8) & 0x7F); - uint16_t vel = (uint16_t)((words[1] >> 16) & 0xFFFF); - char nn[6]; - note_name(pitch, nn, sizeof(nn)); - snprintf(line, sizeof(line), "NoteOn %s ch%u vel=0x%04X", nn, channel, vel); - display_log(line, 0x07E0); // green - note_count++; - break; - } - case 0x8: { // Note Off - uint8_t pitch = (uint8_t)((words[0] >> 8) & 0x7F); - char nn[6]; - note_name(pitch, nn, sizeof(nn)); - snprintf(line, sizeof(line), "NoteOff %s ch%u", nn, channel); - display_log(line, 0x8410); // grey - break; - } - case 0xB: { // CC - uint8_t idx = (uint8_t)((words[0] >> 8) & 0x7F); - snprintf(line, sizeof(line), "CC%-3u = 0x%08lX", idx, (unsigned long)words[1]); - display_log(line, 0x001F); // blue - break; - } - case 0xC: { // Program Change - uint8_t prog = (uint8_t)((words[1] >> 24) & 0x7F); - snprintf(line, sizeof(line), "ProgChg %u", prog); - display_log(line, 0xFFE0); // yellow - break; - } - case 0xE: { // Pitch Bend - snprintf(line, sizeof(line), "PBend = 0x%08lX", (unsigned long)words[1]); - display_log(line, 0xF81F); // magenta - break; - } - case 0xD: { // Channel Pressure - snprintf(line, sizeof(line), "CPress = 0x%08lX", (unsigned long)words[1]); - display_log(line, 0xFC10); // orange - break; - } - case 0xA: { // Poly Pressure - uint8_t pitch = (uint8_t)((words[0] >> 8) & 0x7F); - char nn[6]; - note_name(pitch, nn, sizeof(nn)); - snprintf(line, sizeof(line), "PolyP %s = 0x%08lX", nn, (unsigned long)words[1]); - display_log(line, 0xFC10); - break; - } - case 0xF: { // Per-Note Management - uint8_t pitch = (uint8_t)((words[0] >> 8) & 0x7F); - uint8_t flags = (uint8_t)(words[0] & 0xFF); - snprintf(line, sizeof(line), "PN-Mgmt note=%u flags=0x%02X", pitch, flags); - display_log(line, 0x07FF); // cyan - break; - } - default: { - snprintf(line, sizeof(line), "M2CVM status=0x%X", status); - display_log(line, 0xFFFF); - break; - } - } - } else if (mt == 0x0) { - // Utility (JR Timestamp, NOOP) - uint8_t status = (uint8_t)((words[0] >> 20) & 0x0F); - if (status == 0x2) { - uint16_t ts = words[0] & 0xFFFF; - snprintf(line, sizeof(line), "JR-TS = 0x%04X", ts); - display_log(line, 0x8410); - } - } else { - snprintf(line, sizeof(line), "MT=0x%X (%s) w0=0x%08lX", - mt, mt_name(mt), (unsigned long)words[0]); - display_log(line, 0xFFFF); - } -} - -//--------------------------------------------------------------------+ -// MIDI 2.0 Host Callbacks -//--------------------------------------------------------------------+ - -void tuh_midi2_descriptor_cb(uint8_t idx, const tuh_midi2_descriptor_cb_t* d) { - (void)idx; - ck.descriptor_parsed = true; - char line[48]; - - snprintf(line, sizeof(line), "bcdMSC=0x%02X%02X proto=%s", - d->bcdMSC_hi, d->bcdMSC_lo, - d->protocol_version ? "MIDI2" : "MIDI1"); - display_log(line, 0x07E0); - - snprintf(line, sizeof(line), "Cables: RX=%u TX=%u", - d->rx_cable_count, d->tx_cable_count); - display_log(line, 0x07E0); - - display_checklist_update(&ck); -} - -void tuh_midi2_mount_cb(uint8_t idx, const tuh_midi2_mount_cb_t* m) { - midi2_idx = idx; - ck.alt_setting_ok = true; - ck.mounted = true; - - char line[48]; - snprintf(line, sizeof(line), "Mounted addr=%u alt=%u", - m->daddr, m->alt_setting_active); - display_log(line, 0x07E0); - - if (m->protocol_version) { - display_log("MIDI 2.0 ready", 0x07E0); - } else { - display_log("MIDI 1.0 only", 0xFFE0); - } - - display_checklist_update(&ck); -} - -void tuh_midi2_rx_cb(uint8_t idx, uint32_t xferred_bytes) { - (void)xferred_bytes; - if (!ck.receiving) { - ck.receiving = true; - display_checklist_update(&ck); - } - - uint32_t words[16]; - while (1) { - uint32_t n = tuh_midi2_ump_read(idx, words, 16); - if (n == 0) break; - - // Decode complete UMP messages - uint32_t i = 0; - while (i < n) { - uint8_t mt = (uint8_t)((words[i] >> 28) & 0x0F); - uint8_t wc = midi2_ump_word_count(mt); - if (i + wc > n) break; - decode_ump(&words[i], wc); - i += wc; - } - } -} - -void tuh_midi2_tx_cb(uint8_t idx, uint32_t xferred_bytes) { - (void)idx; (void)xferred_bytes; -} - -void tuh_midi2_umount_cb(uint8_t idx) { - (void)idx; - midi2_idx = 0xFF; - ck.device_connected = false; - ck.descriptor_parsed = false; - ck.alt_setting_ok = false; - ck.mounted = false; - ck.receiving = false; - note_count = 0; - - display_log("Device disconnected", 0xF800); - display_checklist_update(&ck); -} - -//--------------------------------------------------------------------+ -// Main -//--------------------------------------------------------------------+ - -int main(void) { - board_init(); - - display_init(); - - ck.pwr_on = true; - display_checklist_update(&ck); - - // Init TinyUSB Host (BSP handles PIO-USB configuration via tuh_configure) - tusb_rhport_init_t host_init = { - .role = TUSB_ROLE_HOST, - .speed = TUSB_SPEED_FULL, - }; - tusb_init(BOARD_TUH_RHPORT, &host_init); - - ck.tusb_init = true; - ck.bus_active = true; - display_checklist_update(&ck); - - char dbg[40]; - snprintf(dbg, sizeof(dbg), "RHPORT=%u PIO_USB=%u", - BOARD_TUH_RHPORT, CFG_TUH_RPI_PIO_USB); - display_log(dbg, 0xFFE0); // yellow - display_status("Waiting for device..."); - - uint32_t last_status_ms = 0; - static uint32_t loop_count = 0; - - while (1) { - tuh_task(); - loop_count++; - - // Update status every 2 seconds - uint32_t now = tusb_time_millis_api(); - if (now - last_status_ms > 2000) { - last_status_ms = now; - char line[22]; - if (ck.receiving) { - snprintf(line, sizeof(line), "Notes:%lu", (unsigned long)note_count); - } else if (ck.mounted) { - snprintf(line, sizeof(line), "Mounted OK!"); - } else if (ck.device_connected) { - snprintf(line, sizeof(line), "Dev found, mounting.."); - } else { - snprintf(line, sizeof(line), "Wait.. t=%lu", (unsigned long)(now/1000)); - } - display_status(line); - } - } - - return 0; -} - -// Generic USB device mount/unmount (any class) -void tuh_mount_cb(uint8_t daddr) { - char line[32]; - uint16_t vid, pid; - tuh_vid_pid_get(daddr, &vid, &pid); - snprintf(line, sizeof(line), "USB %04X:%04X a%u", vid, pid, daddr); - display_log(line, 0x07E0); - ck.device_connected = true; - display_checklist_update(&ck); -} - -void tuh_umount_cb(uint8_t daddr) { - (void)daddr; - display_log("USB disconnected", 0xF800); - ck.device_connected = false; - ck.descriptor_parsed = false; - ck.alt_setting_ok = false; - ck.mounted = false; - ck.receiving = false; - display_checklist_update(&ck); -} diff --git a/examples/host/midi2_host/src/tusb_config.h b/examples/host/midi2_host/src/tusb_config.h deleted file mode 100644 index 650df97e1..000000000 --- a/examples/host/midi2_host/src/tusb_config.h +++ /dev/null @@ -1,91 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2026 Saulo Verissimo - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -#ifndef TUSB_CONFIG_H_ -#define TUSB_CONFIG_H_ - -#ifdef __cplusplus -extern "C" { -#endif - -//-------------------------------------------------------------------- -// Common Configuration -//-------------------------------------------------------------------- - -#ifndef CFG_TUSB_MCU -#error CFG_TUSB_MCU must be defined -#endif - -#ifndef CFG_TUSB_OS -#define CFG_TUSB_OS OPT_OS_NONE -#endif - -#ifndef CFG_TUSB_DEBUG -#define CFG_TUSB_DEBUG 0 -#endif - -#ifndef CFG_TUH_MEM_SECTION -#define CFG_TUH_MEM_SECTION -#endif - -#ifndef CFG_TUH_MEM_ALIGN -#define CFG_TUH_MEM_ALIGN __attribute__ ((aligned(4))) -#endif - -//-------------------------------------------------------------------- -// Host Configuration -// Waveshare RP2350-USB-A: PIO-USB on GP12/GP13 (USB-A Host port) -//-------------------------------------------------------------------- - -#define CFG_TUH_ENABLED 1 - -// PIO-USB Host on rhport 1 (USB-A connector on GP12/GP13) -#define CFG_TUH_RPI_PIO_USB 1 -#define BOARD_TUH_RHPORT 1 - -#ifndef BOARD_TUH_MAX_SPEED -#define BOARD_TUH_MAX_SPEED OPT_MODE_FULL_SPEED -#endif - -#define CFG_TUH_MAX_SPEED BOARD_TUH_MAX_SPEED - -//-------------------------------------------------------------------- -// Driver Configuration -//-------------------------------------------------------------------- - -#define CFG_TUH_ENUMERATION_BUFSIZE 256 - -#define CFG_TUH_HUB 0 -#define CFG_TUH_DEVICE_MAX 1 - -// MIDI 2.0 Host -#define CFG_TUH_MIDI2 1 -#define CFG_TUH_MIDI2_RX_BUFSIZE 512 -#define CFG_TUH_MIDI2_TX_BUFSIZE 512 - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/hw/bsp/rp2040/boards/waveshare_rp2350_usb_a/board.cmake b/hw/bsp/rp2040/boards/waveshare_rp2350_usb_a/board.cmake deleted file mode 100644 index e888c7fb3..000000000 --- a/hw/bsp/rp2040/boards/waveshare_rp2350_usb_a/board.cmake +++ /dev/null @@ -1,2 +0,0 @@ -set(PICO_PLATFORM rp2350-arm-s) -set(PICO_BOARD waveshare_rp2350_usb_a) diff --git a/hw/bsp/rp2040/boards/waveshare_rp2350_usb_a/board.h b/hw/bsp/rp2040/boards/waveshare_rp2350_usb_a/board.h deleted file mode 100644 index 0af8a695f..000000000 --- a/hw/bsp/rp2040/boards/waveshare_rp2350_usb_a/board.h +++ /dev/null @@ -1,49 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2026 Saulo Verissimo - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - * - * This file is part of the TinyUSB stack. - */ - -/* metadata: - name: Waveshare RP2350-USB-A - url: https://www.waveshare.com/wiki/RP2350-USB-A -*/ - -#ifndef TUSB_BOARD_H -#define TUSB_BOARD_H - -#ifdef __cplusplus - extern "C" { -#endif - -//--------------------------------------------------------------------+ -// PIO_USB -//--------------------------------------------------------------------+ -// Waveshare RP2350-USB-A: PIO-USB on GP12 (D+) / GP13 (D-) -// PICO_DEFAULT_PIO_USB_DP_PIN already defined in SDK board header - -#ifdef __cplusplus - } -#endif - -#endif -- cgit v1.3.1 From 0c68ca8c1de45e24ae022ab58934c6930bad7b4b Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Tue, 21 Apr 2026 20:42:24 -0300 Subject: midi2: align ep_buf allocation with other stream-based classes Keep the MIDI 2.0 drivers fully self-contained, with no changes to shared or generic stack code. Allocate the per-endpoint buffer in both midi2_host and midi2_device, following the convention used by cdc, midi, vendor, and printer. The class buffer struct is declared unconditionally and passed to tu_edpt_stream_init on every init, so the streaming helpers operate on the same shape across all classes. --- src/class/midi/midi2_device.c | 7 ------- src/class/midi/midi2_host.c | 7 ------- 2 files changed, 14 deletions(-) diff --git a/src/class/midi/midi2_device.c b/src/class/midi/midi2_device.c index 0029737ce..34e467fc6 100644 --- a/src/class/midi/midi2_device.c +++ b/src/class/midi/midi2_device.c @@ -110,14 +110,12 @@ TU_VERIFY_STATIC(CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS >= 1 && CFG_TUD_MIDI2_NUM_FUN static midi2d_interface_t _midi2d_itf[CFG_TUD_MIDI2]; -#if CFG_TUD_EDPT_DEDICATED_HWFIFO == 0 typedef struct { TUD_EPBUF_DEF(epin, CFG_TUD_MIDI2_TX_EPSIZE); TUD_EPBUF_DEF(epout, CFG_TUD_MIDI2_RX_EPSIZE); } midi2d_epbuf_t; CFG_TUD_MEM_SECTION static midi2d_epbuf_t _midi2d_epbuf[CFG_TUD_MIDI2]; -#endif // Default Group Terminal Block descriptor (USB-MIDI 2.0 spec, Table 5-5/5-6) static const uint8_t _default_gtb_desc[] = { @@ -385,14 +383,9 @@ void midi2d_init(void) { midi2d_interface_t* p_midi = &_midi2d_itf[i]; p_midi->protocol = MIDI_PROTOCOL_MIDI2; - #if CFG_TUD_EDPT_DEDICATED_HWFIFO - uint8_t* epout_buf = NULL; - uint8_t* epin_buf = NULL; - #else midi2d_epbuf_t* p_epbuf = &_midi2d_epbuf[i]; uint8_t* epout_buf = p_epbuf->epout; uint8_t* epin_buf = p_epbuf->epin; - #endif tu_edpt_stream_init(&p_midi->ep_stream.rx, false, false, false, p_midi->ep_stream.rx_ff_buf, CFG_TUD_MIDI2_RX_BUFSIZE, epout_buf); diff --git a/src/class/midi/midi2_host.c b/src/class/midi/midi2_host.c index 5b9f98f8b..90d632a8e 100644 --- a/src/class/midi/midi2_host.c +++ b/src/class/midi/midi2_host.c @@ -88,14 +88,12 @@ typedef struct { static midih2_interface_t _midi2_host[CFG_TUH_MIDI2]; -#if CFG_TUH_EDPT_DEDICATED_HWFIFO == 0 typedef struct { TUH_EPBUF_DEF(tx, TUH_EPSIZE_BULK_MAX); TUH_EPBUF_DEF(rx, TUH_EPSIZE_BULK_MAX); } midih2_epbuf_t; CFG_TUH_MEM_SECTION static midih2_epbuf_t _midi2_epbuf[CFG_TUH_MIDI2]; -#endif //--------------------------------------------------------------------+ // Helper functions @@ -266,13 +264,8 @@ bool midih2_init(void) { for (int inst = 0; inst < CFG_TUH_MIDI2; inst++) { midih2_interface_t *p_midi = &_midi2_host[inst]; - #if CFG_TUH_EDPT_DEDICATED_HWFIFO - uint8_t* rx_buf = NULL; - uint8_t* tx_buf = NULL; - #else uint8_t* rx_buf = _midi2_epbuf[inst].rx; uint8_t* tx_buf = _midi2_epbuf[inst].tx; - #endif tu_edpt_stream_init(&p_midi->ep_stream.rx, true, false, false, p_midi->ep_stream.rx_ff_buf, CFG_TUH_MIDI2_RX_BUFSIZE, rx_buf); -- cgit v1.3.1 From 97852816e873bf7f91f3a81f093ad08f96179656 Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Wed, 22 Apr 2026 18:04:56 -0300 Subject: midi2: align descriptors with USB-MIDI 2.0 spec Brings the MIDI 2.0 device driver into full conformance with USB Device Class Definition for MIDI Devices v2.0 (USB-IF, May 2020). - Alt 1 MS Interface Header wTotalLength now reports 0x0007 per Table 5-2 ("set to match bLength"), replacing the prior 0x0011 carried over from USB-MIDI 1.0 conventions. - GET_DESCRIPTOR class request now validates bmRequestType direction, type and recipient plus wIndex and wValue high byte per Section 6. - iBlockItem in the default Group Terminal Block is driven by CFG_TUD_MIDI2_BLOCK_STRIDX so applications can attach a UI string descriptor to the block per Table 5-6. - UMP word byte order assumption (little-endian host per Section 3.2.2) is documented inline so future big-endian ports know where to wrap access with tu_htole32 / tu_le32toh. Validated on RP2040 and ESP32-P4 under Linux kernel 6.17: lsusb -v reports wTotalLength = 0x0007 on Alt 1 MS Header (raw bytes 07 24 01 00 02 07 00). amidi -l enumerates Group Terminals exposed via the class-specific GET_DESCRIPTOR response. --- src/class/midi/midi2_device.c | 47 ++++++++++++++++------ src/device/usbd.h | 12 +++--- src/tusb_option.h | 6 +++ .../test/device/midi2/test_midi2_device.c | 3 ++ 4 files changed, 49 insertions(+), 19 deletions(-) diff --git a/src/class/midi/midi2_device.c b/src/class/midi/midi2_device.c index 34e467fc6..15daad096 100644 --- a/src/class/midi/midi2_device.c +++ b/src/class/midi/midi2_device.c @@ -43,6 +43,16 @@ TU_ATTR_WEAK bool tud_midi2_get_req_itf_cb(uint8_t rhport, const tusb_control_re (void) rhport; (void) request; return false; } +//--------------------------------------------------------------------+ +// Byte order note +//--------------------------------------------------------------------+ +// Per USB-MIDI 2.0 Section 3.2.2, each 32-bit UMP word is transmitted with the +// least significant byte first. This driver reads and writes UMP words as +// native uint32_t through tu_edpt_stream_read/write. All TinyUSB targets are +// little-endian, so the in-memory layout already matches the wire order and no +// swap is needed. If a big-endian target is ever supported, wrap access with +// tu_htole32 / tu_le32toh at the buffer boundary. + //--------------------------------------------------------------------+ // UMP Stream Message Constants //--------------------------------------------------------------------+ @@ -133,7 +143,7 @@ static const uint8_t _default_gtb_desc[] = { 0x00, // bGrpTrmBlkType: bidirectional 0x00, // nGroupTrm: first group (0) CFG_TUD_MIDI2_NUM_GROUPS, // nNumGroupTrm - 0, // iBlockItem: no string + CFG_TUD_MIDI2_BLOCK_STRIDX, // iBlockItem: string descriptor index (0 = none) 0x00, // bMIDIProtocol: unknown/not fixed 0, 0, // wMaxInputBandwidth: unknown 0, 0 // wMaxOutputBandwidth: unknown @@ -554,19 +564,30 @@ bool midi2d_control_xfer_cb(uint8_t rhport, uint8_t stage, const tusb_control_re } case TUSB_REQ_GET_DESCRIPTOR: { - // wValue: descriptor type (high) | index (low) - // 0x26 = CS_GRP_TRM_BLOCK, index 0x01 - if (request->wValue == ((uint16_t)MIDI2_CS_GRP_TRM_BLOCK << 8 | 0x01)) { - if (tud_midi2_get_req_itf_cb(rhport, request)) return true; - - uint16_t len = request->wLength; - if (len > sizeof(_default_gtb_desc)) { - len = sizeof(_default_gtb_desc); - } - tud_control_xfer(rhport, request, (void*)(uintptr_t) _default_gtb_desc, len); - return true; + // USB-MIDI 2.0 Section 6: GTB descriptor retrieval + // bmRequestType = 0x81 (Device-to-Host, Standard, Interface) + // wValue = CS_GR_TRM_BLOCK (0x26) in high byte, alt setting in low byte + // wIndex = interface number + if (request->bmRequestType_bit.direction != TUSB_DIR_IN) return false; + if (request->bmRequestType_bit.type != TUSB_REQ_TYPE_STANDARD) return false; + if (request->bmRequestType_bit.recipient != TUSB_REQ_RCPT_INTERFACE) return false; + if (tu_u16_high(request->wValue) != MIDI2_CS_GRP_TRM_BLOCK) return false; + + uint8_t itf_num = tu_u16_low(request->wIndex); + uint8_t idx = find_midi2_itf_by_num(itf_num); + if (idx >= CFG_TUD_MIDI2) return false; + + // Only Alt Setting 1 exposes Group Terminal Block descriptors. + if (tu_u16_low(request->wValue) != 0x01) return false; + + if (tud_midi2_get_req_itf_cb(rhport, request)) return true; + + uint16_t len = request->wLength; + if (len > sizeof(_default_gtb_desc)) { + len = sizeof(_default_gtb_desc); } - return false; + tud_control_xfer(rhport, request, (void*)(uintptr_t) _default_gtb_desc, len); + return true; } default: diff --git a/src/device/usbd.h b/src/device/usbd.h index a9f4c5f08..abce5a887 100644 --- a/src/device/usbd.h +++ b/src/device/usbd.h @@ -429,14 +429,14 @@ bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_requ //--------------------------------------------------------------------+ // Alt Setting 1: MS Interface + MS Header (bcdMSC=0x0200) -// wTotalLength covers MS Header + all CS Endpoint descriptors -#define TUD_MIDI2_DESC_ALT1_CS_LEN(_numgtbs) (7 + (4 + (_numgtbs)) * 2) +// Per USB-MIDI 2.0 Table 5-2: wTotalLength in the MS Header is not used in 2.0 +// and shall be set to match bLength (= 0x0007) for conformity with USB-MIDI 1.0. #define TUD_MIDI2_DESC_ALT1_HEAD_LEN (9 + 7) -#define TUD_MIDI2_DESC_ALT1_HEAD(_itfnum, _stridx, _numgtbs) \ +#define TUD_MIDI2_DESC_ALT1_HEAD(_itfnum, _stridx) \ /* MIDI Streaming Interface, Alt Setting 1 */\ 9, TUSB_DESC_INTERFACE, (uint8_t)((_itfnum) + 1), 1, 2, TUSB_CLASS_AUDIO, AUDIO_SUBCLASS_MIDI_STREAMING, AUDIO_FUNC_PROTOCOL_CODE_UNDEF, 0,\ - /* MS Header (MIDI 2.0): wTotalLength = header + 2x CS Endpoint */\ - 7, TUSB_DESC_CS_INTERFACE, MIDI_CS_INTERFACE_HEADER, U16_TO_U8S_LE(0x0200), U16_TO_U8S_LE(TUD_MIDI2_DESC_ALT1_CS_LEN(_numgtbs)) + /* MS Header (MIDI 2.0): wTotalLength = bLength per spec */\ + 7, TUSB_DESC_CS_INTERFACE, MIDI_CS_INTERFACE_HEADER, U16_TO_U8S_LE(0x0200), U16_TO_U8S_LE(0x0007) // Alt Setting 1: Standard USB Endpoint (7 bytes) + CS Endpoint General 2.0 #define TUD_MIDI2_DESC_ALT1_EP_LEN(_numgtbs) (7 + 4 + (_numgtbs)) @@ -457,7 +457,7 @@ bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb_control_requ TUD_MIDI_DESC_EP(_epin, _epsize, 1),\ TUD_MIDI_JACKID_OUT_EMB(1),\ /* Alt Setting 1 (UMP) */\ - TUD_MIDI2_DESC_ALT1_HEAD(_itfnum, _stridx, 1),\ + TUD_MIDI2_DESC_ALT1_HEAD(_itfnum, _stridx),\ TUD_MIDI2_DESC_ALT1_EP(_epout, _epsize, 1, 1 /* bAssoGrpTrmBlkID */),\ TUD_MIDI2_DESC_ALT1_EP(_epin, _epsize, 1, 1 /* bAssoGrpTrmBlkID */) diff --git a/src/tusb_option.h b/src/tusb_option.h index 4483c2200..2614110fc 100644 --- a/src/tusb_option.h +++ b/src/tusb_option.h @@ -682,6 +682,12 @@ #define CFG_TUD_MIDI2_PRODUCT_ID "TinyUSB-MIDI2" #endif +// String descriptor index for the Group Terminal Block (iBlockItem, Table 5-6). +// 0 = no string descriptor (default, spec-allowed). +#ifndef CFG_TUD_MIDI2_BLOCK_STRIDX + #define CFG_TUD_MIDI2_BLOCK_STRIDX 0 +#endif + #ifndef CFG_TUD_VENDOR #define CFG_TUD_VENDOR 0 #endif diff --git a/test/unit-test/test/device/midi2/test_midi2_device.c b/test/unit-test/test/device/midi2/test_midi2_device.c index 9d716d93b..1314c2585 100644 --- a/test/unit-test/test/device/midi2/test_midi2_device.c +++ b/test/unit-test/test/device/midi2/test_midi2_device.c @@ -147,6 +147,9 @@ void test_midi2_descriptor_bytes(void) { TEST_ASSERT_EQUAL(MIDI_CS_INTERFACE_HEADER, desc[ms2_offset + 2]); TEST_ASSERT_EQUAL(0x00, desc[ms2_offset + 3]); TEST_ASSERT_EQUAL(0x02, desc[ms2_offset + 4]); + // USB-MIDI 2.0 Table 5-2: wTotalLength shall match bLength (= 0x0007) + TEST_ASSERT_EQUAL(0x07, desc[ms2_offset + 5]); + TEST_ASSERT_EQUAL(0x00, desc[ms2_offset + 6]); } void test_midi2_descriptor_alt1_cs_endpoint_subtype(void) { -- cgit v1.3.1 From 0af0665ed65ef9574a4c3c2da83c1c502cb713de Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Sat, 16 May 2026 12:59:03 -0300 Subject: midi2: reject UMP read/write API on alt setting 0 Alt 0 carries USB-MIDI 1.0 32-bit Event Packets, not UMP words; calling the UMP API there would misinterpret the stream. Expose MIDI_PROTOCOL_MIDI1 and MIDI_PROTOCOL_MIDI2 in the public header so applications can branch on the negotiated protocol. Ref #3571 --- src/class/midi/midi2_device.c | 16 ++++++++++------ src/class/midi/midi2_device.h | 9 +++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/class/midi/midi2_device.c b/src/class/midi/midi2_device.c index 15daad096..f7d338ede 100644 --- a/src/class/midi/midi2_device.c +++ b/src/class/midi/midi2_device.c @@ -73,12 +73,6 @@ enum { STREAM_FB_INFO = 0x011, }; -// MIDI Protocol values (per USB-MIDI 2.0 spec) -enum { - MIDI_PROTOCOL_MIDI1 = 0x01, - MIDI_PROTOCOL_MIDI2 = 0x02, -}; - enum { UMP_VER_MAJOR = 1, UMP_VER_MINOR = 1, @@ -304,6 +298,11 @@ uint32_t tud_midi2_n_available(uint8_t itf) { uint32_t tud_midi2_n_ump_read(uint8_t itf, uint32_t* words, uint32_t max_words) { TU_VERIFY(itf < CFG_TUD_MIDI2 && words != NULL && max_words > 0, 0); midi2d_interface_t* p_midi = &_midi2d_itf[itf]; + + // UMP API is only valid on Alt Setting 1 (USB-MIDI 2.0). + // Alt 0 carries USB-MIDI 1.0 32-bit Event Packets, not UMP words. + if (p_midi->alt_setting != 1) { return 0; } + tu_edpt_stream_t* ep_rx = &p_midi->ep_stream.rx; uint32_t total_read = 0; @@ -336,6 +335,11 @@ bool tud_midi2_n_packet_read(uint8_t itf, uint8_t packet[4]) { uint32_t tud_midi2_n_ump_write(uint8_t itf, const uint32_t* words, uint32_t count) { TU_VERIFY(itf < CFG_TUD_MIDI2 && words != NULL && count > 0, 0); midi2d_interface_t* p_midi = &_midi2d_itf[itf]; + + // UMP API is only valid on Alt Setting 1 (USB-MIDI 2.0). + // Alt 0 carries USB-MIDI 1.0 32-bit Event Packets, not UMP words. + if (p_midi->alt_setting != 1) { return 0; } + tu_edpt_stream_t* ep_tx = &p_midi->ep_stream.tx; TU_VERIFY(tu_edpt_stream_is_opened(ep_tx), 0); diff --git a/src/class/midi/midi2_device.h b/src/class/midi/midi2_device.h index dac1f0124..8c64729c3 100644 --- a/src/class/midi/midi2_device.h +++ b/src/class/midi/midi2_device.h @@ -44,6 +44,15 @@ extern "C" { #endif +//--------------------------------------------------------------------+ +// MIDI Protocol Values (returned by tud_midi2_n_protocol) +//--------------------------------------------------------------------+ +// Per USB-MIDI 2.0 spec, UMP Stream Configuration messages. +enum { + MIDI_PROTOCOL_MIDI1 = 0x01, + MIDI_PROTOCOL_MIDI2 = 0x02, +}; + //--------------------------------------------------------------------+ // Application Callback API (weak, optional) //--------------------------------------------------------------------+ -- cgit v1.3.1 From 840f3e0a628e69a2b7fb4136b2dca8ab5b350f83 Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Sat, 16 May 2026 12:59:44 -0300 Subject: midi2: prevent UMP packet split across USB transfers The edpt_stream auto-flush is byte-oriented and could cut an UMP message in half when the FIFO reached wMaxPacketSize, corrupting the peer's RX context. Pre-flush whole packets before writing one that would cross the boundary on both device (tud_midi2_n_ump_write) and host (tuh_midi2_ump_write) paths. Host write also becomes packet-aware instead of word-by-word. Ref #3571 --- src/class/midi/midi2_device.c | 13 +++++++++++-- src/class/midi/midi2_host.c | 27 +++++++++++++++++++-------- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/src/class/midi/midi2_device.c b/src/class/midi/midi2_device.c index f7d338ede..a005c8e51 100644 --- a/src/class/midi/midi2_device.c +++ b/src/class/midi/midi2_device.c @@ -347,11 +347,20 @@ uint32_t tud_midi2_n_ump_write(uint8_t itf, const uint32_t* words, uint32_t coun while (written < count) { uint8_t mt = (uint8_t)((words[written] >> 28) & 0x0F); uint8_t pkt_words = midi2_ump_word_count(mt); + uint32_t pkt_bytes = (uint32_t)pkt_words * 4; if (written + pkt_words > count) break; - if (tu_edpt_stream_write_available(ep_tx) < pkt_words * 4) break; + if (tu_edpt_stream_write_available(ep_tx) < pkt_bytes) break; + + // Flush whole packets already queued before adding one that would cross + // the wMaxPacketSize boundary. Prevents an UMP message from being split + // across two USB transfers, which would corrupt the host RX context. + uint16_t ff_count = tu_fifo_count(&ep_tx->ff); + if (ff_count > 0 && ff_count + pkt_bytes > ep_tx->mps) { + tu_edpt_stream_write_xfer(ep_tx); + } - tu_edpt_stream_write(ep_tx, &words[written], pkt_words * 4); + tu_edpt_stream_write(ep_tx, &words[written], pkt_bytes); written += pkt_words; } diff --git a/src/class/midi/midi2_host.c b/src/class/midi/midi2_host.c index 90d632a8e..2046fdb67 100644 --- a/src/class/midi/midi2_host.c +++ b/src/class/midi/midi2_host.c @@ -527,17 +527,28 @@ uint32_t tuh_midi2_ump_write(uint8_t idx, const uint32_t* words, uint32_t count) midih2_interface_t *p_midi = &_midi2_host[idx]; tu_edpt_stream_t *ep_tx = &p_midi->ep_stream.tx; - uint32_t n_words = 0; - for (uint32_t i = 0; i < count; i++) { - if (tu_edpt_stream_write_available(ep_tx) >= 4) { - tu_edpt_stream_write(ep_tx, (const uint8_t *) &words[i], 4); - n_words++; - } else { - break; + uint32_t written = 0; + while (written < count) { + uint8_t mt = (uint8_t)((words[written] >> 28) & 0x0F); + uint8_t pkt_words = midi2_ump_word_count(mt); + uint32_t pkt_bytes = (uint32_t)pkt_words * 4; + + if (written + pkt_words > count) break; + if (tu_edpt_stream_write_available(ep_tx) < pkt_bytes) break; + + // Flush whole packets already queued before adding one that would cross + // the wMaxPacketSize boundary. Prevents an UMP message from being split + // across two USB transfers, which would corrupt the peer RX context. + uint16_t ff_count = tu_fifo_count(&ep_tx->ff); + if (ff_count > 0 && ff_count + pkt_bytes > ep_tx->mps) { + tu_edpt_stream_write_xfer(ep_tx); } + + tu_edpt_stream_write(ep_tx, (const uint8_t *) &words[written], pkt_bytes); + written += pkt_words; } - return n_words; + return written; } uint32_t tuh_midi2_write_flush(uint8_t idx) { -- 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 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 3efac1b28c4174b0734ef106ce8496fa39657b47 Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Sat, 16 May 2026 13:00:30 -0300 Subject: midi2: host example - simplify and rename to midi2_host Drop Feather-specific scaffolding (SSD1306 display, font, PIO-USB pin override) and rename the directory to midi2_host. The example is now a single main.c + tusb_config.h pair following the midi_rx pattern, with a printf-only UMP decoder for Channel Voice messages. Build-tested on adafruit_feather_rp2040_usb_host, daisyseed (stm32h7), mimxrt1010_evk, stm32f407disco, raspberry_pi_pico2, b_u585i_iot2a, portenta_c33, stm32h503nucleo, stlinkv3mini, feather_stm32f405. Ref #3571 --- examples/host/CMakeLists.txt | 2 +- examples/host/midi2_host/CMakeLists.txt | 29 +++ examples/host/midi2_host/CMakePresets.json | 6 + examples/host/midi2_host/Makefile | 13 + examples/host/midi2_host/src/main.c | 165 ++++++++++++ examples/host/midi2_host/src/tusb_config.h | 106 ++++++++ examples/host/midi2_host_feather/CMakeLists.txt | 38 --- examples/host/midi2_host_feather/Makefile | 27 -- examples/host/midi2_host_feather/src/display.c | 276 --------------------- examples/host/midi2_host_feather/src/display.h | 46 ---- examples/host/midi2_host_feather/src/font5x7.h | 107 -------- examples/host/midi2_host_feather/src/main.c | 267 -------------------- examples/host/midi2_host_feather/src/tusb_config.h | 91 ------- 13 files changed, 320 insertions(+), 853 deletions(-) create mode 100644 examples/host/midi2_host/CMakeLists.txt create mode 100644 examples/host/midi2_host/CMakePresets.json create mode 100644 examples/host/midi2_host/Makefile create mode 100644 examples/host/midi2_host/src/main.c create mode 100644 examples/host/midi2_host/src/tusb_config.h delete mode 100644 examples/host/midi2_host_feather/CMakeLists.txt delete mode 100644 examples/host/midi2_host_feather/Makefile delete mode 100644 examples/host/midi2_host_feather/src/display.c delete mode 100644 examples/host/midi2_host_feather/src/display.h delete mode 100644 examples/host/midi2_host_feather/src/font5x7.h delete mode 100644 examples/host/midi2_host_feather/src/main.c delete mode 100644 examples/host/midi2_host_feather/src/tusb_config.h diff --git a/examples/host/CMakeLists.txt b/examples/host/CMakeLists.txt index 2cf2c10af..7c74e3c73 100644 --- a/examples/host/CMakeLists.txt +++ b/examples/host/CMakeLists.txt @@ -13,7 +13,7 @@ set(EXAMPLE_LIST device_info hid_controller midi_rx - midi2_host_feather + midi2_host msc_file_explorer msc_file_explorer_freertos ) diff --git a/examples/host/midi2_host/CMakeLists.txt b/examples/host/midi2_host/CMakeLists.txt new file mode 100644 index 000000000..0ec03bf5f --- /dev/null +++ b/examples/host/midi2_host/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.20) + +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake) + +project(midi2_host C CXX ASM) + +# Checks this example is valid for the family and initializes the project +family_initialize_project(${PROJECT_NAME} ${CMAKE_CURRENT_LIST_DIR}) + +# Espressif has its own cmake build system +if(FAMILY STREQUAL "espressif") + return() +endif() + +add_executable(${PROJECT_NAME}) + +# Example source +target_sources(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src/main.c + ) + +# Example include +target_include_directories(${PROJECT_NAME} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src + ) + +# Configure compilation flags and libraries for the example without RTOS. +# See the corresponding function in hw/bsp/FAMILY/family.cmake for details. +family_configure_host_example(${PROJECT_NAME} noos) diff --git a/examples/host/midi2_host/CMakePresets.json b/examples/host/midi2_host/CMakePresets.json new file mode 100644 index 000000000..5cd8971e9 --- /dev/null +++ b/examples/host/midi2_host/CMakePresets.json @@ -0,0 +1,6 @@ +{ + "version": 6, + "include": [ + "../../../hw/bsp/BoardPresets.json" + ] +} diff --git a/examples/host/midi2_host/Makefile b/examples/host/midi2_host/Makefile new file mode 100644 index 000000000..f8292385e --- /dev/null +++ b/examples/host/midi2_host/Makefile @@ -0,0 +1,13 @@ +include ../../../hw/bsp/family_support.mk + +INC += \ + src \ + + +# Example source +EXAMPLE_SOURCE += \ + src/main.c + +SRC_C += $(addprefix $(EXAMPLE_PATH)/, $(EXAMPLE_SOURCE)) + +include ../../../hw/bsp/family_rules.mk diff --git a/examples/host/midi2_host/src/main.c b/examples/host/midi2_host/src/main.c new file mode 100644 index 000000000..63b08318c --- /dev/null +++ b/examples/host/midi2_host/src/main.c @@ -0,0 +1,165 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +// Minimal USB-MIDI 2.0 host example. +// Receives UMP from any MIDI 2.0 device, prints each packet to stdout. + +#include +#include +#include "bsp/board_api.h" +#include "tusb.h" +#include "class/midi/midi2_host.h" + +//--------------------------------------------------------------------+ +// State +//--------------------------------------------------------------------+ + +static uint8_t midi2_idx = 0xFF; + +//--------------------------------------------------------------------+ +// UMP printer - shows MT and word(s) in hex; decodes Channel Voice +//--------------------------------------------------------------------+ + +static void print_ump(const uint32_t* words, uint8_t wc) { + uint8_t mt = (uint8_t)((words[0] >> 28) & 0x0F); + uint8_t group = (uint8_t)((words[0] >> 24) & 0x0F); + + if (mt == 0x4 && wc >= 2) { + // MIDI 2.0 Channel Voice + uint8_t status = (uint8_t)((words[0] >> 20) & 0x0F); + uint8_t channel = (uint8_t)((words[0] >> 16) & 0x0F); + uint8_t data1 = (uint8_t)((words[0] >> 8) & 0x7F); + switch (status) { + case 0x9: + printf("[g%u ch%u] M2 NoteOn n=%u vel=%04X attr=%04X\r\n", + group, channel, data1, + (unsigned)((words[1] >> 16) & 0xFFFF), + (unsigned)(words[1] & 0xFFFF)); + break; + case 0x8: + printf("[g%u ch%u] M2 NoteOff n=%u vel=%04X\r\n", + group, channel, data1, + (unsigned)((words[1] >> 16) & 0xFFFF)); + break; + case 0xB: + printf("[g%u ch%u] M2 CC#%u = %08lX\r\n", + group, channel, data1, (unsigned long)words[1]); + break; + case 0xC: + printf("[g%u ch%u] M2 ProgChg %u\r\n", + group, channel, (unsigned)((words[1] >> 24) & 0x7F)); + break; + case 0xD: + printf("[g%u ch%u] M2 ChanPress %08lX\r\n", + group, channel, (unsigned long)words[1]); + break; + case 0xE: + printf("[g%u ch%u] M2 PitchBend %08lX\r\n", + group, channel, (unsigned long)words[1]); + break; + default: + printf("[g%u ch%u] M2 status=0x%X w0=%08lX w1=%08lX\r\n", + group, channel, status, + (unsigned long)words[0], (unsigned long)words[1]); + break; + } + } else if (mt == 0x2 && wc == 1) { + // MIDI 1.0 Channel Voice + uint8_t status = (uint8_t)((words[0] >> 16) & 0xFF); + uint8_t data1 = (uint8_t)((words[0] >> 8) & 0x7F); + uint8_t data2 = (uint8_t)(words[0] & 0x7F); + printf("[g%u] M1 %02X %02X %02X\r\n", group, status, data1, data2); + } else { + printf("UMP MT=0x%X wc=%u w0=%08lX\r\n", + mt, wc, (unsigned long)words[0]); + } +} + +//--------------------------------------------------------------------+ +// MIDI 2.0 Host Callbacks +//--------------------------------------------------------------------+ + +void tuh_midi2_descriptor_cb(uint8_t idx, const tuh_midi2_descriptor_cb_t* d) { + (void)idx; + printf("MIDI2 descriptor: %s, RX cables=%u TX cables=%u\r\n", + d->protocol_version ? "MIDI 2.0" : "MIDI 1.0", + d->rx_cable_count, d->tx_cable_count); +} + +void tuh_midi2_mount_cb(uint8_t idx, const tuh_midi2_mount_cb_t* m) { + midi2_idx = idx; + printf("MIDI2 mounted: idx=%u protocol=%s\r\n", + idx, m->protocol_version ? "MIDI 2.0" : "MIDI 1.0"); +} + +void tuh_midi2_rx_cb(uint8_t idx, uint32_t xferred_bytes) { + (void)xferred_bytes; + + uint32_t words[16]; + while (1) { + uint32_t n = tuh_midi2_ump_read(idx, words, 16); + if (n == 0) break; + + uint32_t i = 0; + while (i < n) { + uint8_t mt = (uint8_t)((words[i] >> 28) & 0x0F); + uint8_t wc = midi2_ump_word_count(mt); + if (i + wc > n) break; + print_ump(&words[i], wc); + i += wc; + } + } +} + +void tuh_midi2_tx_cb(uint8_t idx, uint32_t xferred_bytes) { + (void)idx; (void)xferred_bytes; +} + +void tuh_midi2_umount_cb(uint8_t idx) { + (void)idx; + midi2_idx = 0xFF; + printf("MIDI2 unmounted\r\n"); +} + +//--------------------------------------------------------------------+ +// Main +//--------------------------------------------------------------------+ + +int main(void) { + board_init(); + + printf("\r\nTinyUSB Host MIDI 2.0 Example\r\n"); + + tusb_rhport_init_t host_init = { + .role = TUSB_ROLE_HOST, + .speed = TUSB_SPEED_AUTO, + }; + tusb_init(BOARD_TUH_RHPORT, &host_init); + + while (1) { + tuh_task(); + } + + return 0; +} diff --git a/examples/host/midi2_host/src/tusb_config.h b/examples/host/midi2_host/src/tusb_config.h new file mode 100644 index 000000000..4bd6ef2ea --- /dev/null +++ b/examples/host/midi2_host/src/tusb_config.h @@ -0,0 +1,106 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2026 Saulo Verissimo + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef TUSB_CONFIG_H_ +#define TUSB_CONFIG_H_ + +#ifdef __cplusplus +extern "C" { +#endif + +//--------------------------------------------------------------------+ +// Common Configuration +//--------------------------------------------------------------------+ + +#ifndef CFG_TUSB_MCU +#error CFG_TUSB_MCU must be defined +#endif + +#ifdef ESP_PLATFORM +#define CFG_TUSB_OS_INC_PATH freertos/ +#endif + +#ifndef CFG_TUSB_OS +#define CFG_TUSB_OS OPT_OS_NONE +#endif + +#ifndef CFG_TUSB_DEBUG +#define CFG_TUSB_DEBUG 0 +#endif + +#ifndef CFG_TUH_MEM_SECTION +#define CFG_TUH_MEM_SECTION +#endif + +#ifndef CFG_TUH_MEM_ALIGN +#define CFG_TUH_MEM_ALIGN __attribute__ ((aligned(4))) +#endif + +//--------------------------------------------------------------------+ +// Host Configuration +//--------------------------------------------------------------------+ + +#define CFG_TUH_ENABLED 1 + +#if CFG_TUSB_MCU == OPT_MCU_RP2040 + // #define CFG_TUH_RPI_PIO_USB 1 // use pio-usb as host controller + // #define CFG_TUH_MAX3421 1 // use max3421 as host controller + + // host roothub port is 1 if using either pio-usb or max3421 + #if (defined(CFG_TUH_RPI_PIO_USB) && CFG_TUH_RPI_PIO_USB) || (defined(CFG_TUH_MAX3421) && CFG_TUH_MAX3421) + #define BOARD_TUH_RHPORT 1 + #endif +#endif + +#define CFG_TUH_MAX_SPEED BOARD_TUH_MAX_SPEED + +//------------------------- Board Specific --------------------------+ + +#ifndef BOARD_TUH_RHPORT +#define BOARD_TUH_RHPORT 0 +#endif + +#ifndef BOARD_TUH_MAX_SPEED +#define BOARD_TUH_MAX_SPEED OPT_MODE_DEFAULT_SPEED +#endif + +//--------------------------------------------------------------------+ +// Driver Configuration +//--------------------------------------------------------------------+ + +#define CFG_TUH_ENUMERATION_BUFSIZE 256 + +#define CFG_TUH_HUB 1 +#define CFG_TUH_DEVICE_MAX (3*CFG_TUH_HUB + 1) + +// USB-MIDI 2.0 Host +#define CFG_TUH_MIDI2 CFG_TUH_DEVICE_MAX +#define CFG_TUH_MIDI2_RX_BUFSIZE 512 +#define CFG_TUH_MIDI2_TX_BUFSIZE 512 + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/examples/host/midi2_host_feather/CMakeLists.txt b/examples/host/midi2_host_feather/CMakeLists.txt deleted file mode 100644 index 5d34b8170..000000000 --- a/examples/host/midi2_host_feather/CMakeLists.txt +++ /dev/null @@ -1,38 +0,0 @@ -cmake_minimum_required(VERSION 3.20) - -include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake) - -project(midi2_host_feather C CXX ASM) - -family_initialize_project(${PROJECT_NAME} ${CMAKE_CURRENT_LIST_DIR}) - -# This example requires PIO-USB and Pico SDK (I2C, SSD1306 display) -if(NOT FAMILY STREQUAL "rp2040") - return() -endif() - -add_executable(${PROJECT_NAME}) - -target_sources(${PROJECT_NAME} PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/src/main.c - ${CMAKE_CURRENT_SOURCE_DIR}/src/display.c -) - -target_include_directories(${PROJECT_NAME} PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/src -) - -family_configure_host_example(${PROJECT_NAME} noos) - -# Adafruit Feather RP2040 USB Host: PIO-USB on GP16/GP17 -target_compile_definitions(${PROJECT_NAME} PRIVATE - PIO_USB_DP_PIN_DEFAULT=16 -) -target_compile_options(${PROJECT_NAME} PRIVATE - -Wno-type-limits -) - -# SSD1306 display (I2C via STEMMA QT) -target_link_libraries(${PROJECT_NAME} PUBLIC - hardware_i2c -) diff --git a/examples/host/midi2_host_feather/Makefile b/examples/host/midi2_host_feather/Makefile deleted file mode 100644 index 933d19434..000000000 --- a/examples/host/midi2_host_feather/Makefile +++ /dev/null @@ -1,27 +0,0 @@ -# This example requires RP2040 (PIO-USB, Pico SDK I2C, SSD1306 display) -ifeq (,$(findstring rp2040,$(FAMILY))) -$(info Skipping midi2_host_feather: 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/examples/host/midi2_host_feather/src/display.c b/examples/host/midi2_host_feather/src/display.c deleted file mode 100644 index 91039fa16..000000000 --- a/examples/host/midi2_host_feather/src/display.c +++ /dev/null @@ -1,276 +0,0 @@ -/* - * 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. - */ - -// SSD1306 OLED display driver (128x64, I2C) for MIDI 2.0 Host example. -// Adafruit Feather RP2040 USB Host: I2C1 via STEMMA QT (SDA = GP2, SCL = GP3) -// -// Three display phases: -// 1. Splash : title + credits (shown during init) -// 2. Connecting : spinner animation while waiting for device -// 3. Live : header + 6 scrolling log lines + status bar - -#include "display.h" -#include -#include -#include "pico/stdlib.h" -#include "hardware/i2c.h" -#include "hardware/gpio.h" - -#define I2C_PORT i2c1 -#define I2C_SDA 2 -#define I2C_SCL 3 -#define I2C_FREQ 400000 -#define SSD1306_ADDR 0x3C - -#define SCR_W 128 -#define SCR_H 64 -#define PAGES (SCR_H / 8) -#define CHARS_PER_LINE 21 - -//--------------------------------------------------------------------+ -// Framebuffer -//--------------------------------------------------------------------+ - -static uint8_t fb[SCR_W * PAGES]; - -//--------------------------------------------------------------------+ -// Log buffer (6 lines for live view) -//--------------------------------------------------------------------+ - -#define LOG_LINES 6 -static char log_lines[LOG_LINES][CHARS_PER_LINE + 1]; -static int log_count = 0; - -//--------------------------------------------------------------------+ -// Minimal 5x7 font (ASCII 32-126) -//--------------------------------------------------------------------+ - -#include "font5x7.h" - -//--------------------------------------------------------------------+ -// SSD1306 I2C low-level -//--------------------------------------------------------------------+ - -static void ssd_cmd(uint8_t cmd) { - uint8_t buf[2] = { 0x00, cmd }; - i2c_write_blocking(I2C_PORT, SSD1306_ADDR, buf, 2, false); -} - -static void ssd_data(const uint8_t* data, size_t len) { - uint8_t buf[SCR_W + 1]; - buf[0] = 0x40; - size_t chunk = (len > SCR_W) ? SCR_W : len; - memcpy(buf + 1, data, chunk); - i2c_write_blocking(I2C_PORT, SSD1306_ADDR, buf, chunk + 1, false); -} - -static void ssd_flush(void) { - ssd_cmd(0x21); ssd_cmd(0); ssd_cmd(127); - ssd_cmd(0x22); ssd_cmd(0); ssd_cmd(7); - for (int page = 0; page < PAGES; page++) { - ssd_data(&fb[page * SCR_W], SCR_W); - } -} - -//--------------------------------------------------------------------+ -// Framebuffer drawing -//--------------------------------------------------------------------+ - -static void fb_clear(void) { - memset(fb, 0, sizeof(fb)); -} - -static void fb_char(int x, int y, char c) { - if (c < 32 || c > 126) c = '?'; - const uint8_t* glyph = font5x7 + (c - 32) * 5; - int page = y / 8; - int bit_offset = y % 8; - - if (page >= PAGES || x + 5 > SCR_W) return; - - for (int col = 0; col < 5; col++) { - uint8_t column_data = glyph[col]; - fb[(page * SCR_W) + x + col] |= (uint8_t)(column_data << bit_offset); - if (bit_offset > 0 && page + 1 < PAGES) { - fb[((page + 1) * SCR_W) + x + col] |= (uint8_t)(column_data >> (8 - bit_offset)); - } - } -} - -static void fb_string(int x, int y, const char* str) { - while (*str) { - fb_char(x, y, *str); - x += 6; - if (x + 6 > SCR_W) break; - str++; - } -} - -// Draw a horizontal line (1px) -static void fb_hline(int x0, int x1, int y) { - int page = y / 8; - uint8_t mask = (uint8_t)(1 << (y % 8)); - if (page >= PAGES) return; - for (int x = x0; x <= x1 && x < SCR_W; x++) { - fb[page * SCR_W + x] |= mask; - } -} - -//--------------------------------------------------------------------+ -// Phase 1: Splash -//--------------------------------------------------------------------+ - -void display_init(void) { - i2c_init(I2C_PORT, I2C_FREQ); - gpio_set_function(I2C_SDA, GPIO_FUNC_I2C); - gpio_set_function(I2C_SCL, GPIO_FUNC_I2C); - gpio_pull_up(I2C_SDA); - gpio_pull_up(I2C_SCL); - - sleep_ms(100); - - // SSD1306 init sequence - ssd_cmd(0xAE); - ssd_cmd(0xD5); ssd_cmd(0x80); - ssd_cmd(0xA8); ssd_cmd(0x3F); - ssd_cmd(0xD3); ssd_cmd(0x00); - ssd_cmd(0x40); - ssd_cmd(0x8D); ssd_cmd(0x14); - ssd_cmd(0x20); ssd_cmd(0x00); - ssd_cmd(0xA1); - ssd_cmd(0xC8); - ssd_cmd(0xDA); ssd_cmd(0x12); - ssd_cmd(0x81); ssd_cmd(0xCF); - ssd_cmd(0xD9); ssd_cmd(0xF1); - ssd_cmd(0xDB); ssd_cmd(0x40); - ssd_cmd(0xA4); - ssd_cmd(0xA6); - ssd_cmd(0xAF); - - // Splash screen - fb_clear(); - fb_string(4, 8, "TinyUSB MIDI 2.0"); - fb_hline(4, 123, 18); - fb_string(16, 24, "USB Host Demo"); - fb_string(4, 40, "Feather RP2040"); - fb_string(4, 52, "PIO-USB + SSD1306"); - ssd_flush(); -} - -//--------------------------------------------------------------------+ -// Phase 2: Connecting (spinner) -//--------------------------------------------------------------------+ - -static const char SPINNER[] = "|/-\\"; - -void display_connecting(uint32_t elapsed_ms) { - int idx = (int)((elapsed_ms / 200) % 4); - - // Clear bottom 2 pages for spinner area - memset(&fb[6 * SCR_W], 0, SCR_W); - memset(&fb[7 * SCR_W], 0, SCR_W); - - char line[CHARS_PER_LINE + 1]; - snprintf(line, sizeof(line), "%c Waiting device...", SPINNER[idx]); - fb_string(4, 52, line); - ssd_flush(); -} - -//--------------------------------------------------------------------+ -// Phase 3: Live view -//--------------------------------------------------------------------+ - -// Layout: -// y=0 : "TinyUSB MIDI 2.0 Host" (header, fixed) -// y=9 : separator line -// y=10 : log line 0 -// y=18 : log line 1 -// y=26 : log line 2 -// y=34 : log line 3 -// y=42 : log line 4 -// y=50 : log line 5 -// y=57 : separator line -// y=58 : status bar - -static bool live_mode = false; - -static void draw_live_frame(void) { - fb_clear(); - fb_string(0, 0, "TinyUSB MIDI 2.0 Host"); - fb_hline(0, 127, 9); - fb_hline(0, 127, 57); -} - -void display_live_begin(void) { - live_mode = true; - log_count = 0; - memset(log_lines, 0, sizeof(log_lines)); - - draw_live_frame(); - fb_string(0, 58, "Connected"); - ssd_flush(); -} - -void display_log(const char* text, uint16_t color) { - (void)color; - - if (!live_mode) { - display_live_begin(); - } - - // Scroll up if full - 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); - log_lines[i][CHARS_PER_LINE] = '\0'; - } - log_count = LOG_LINES - 1; - } - - strncpy(log_lines[log_count], text, CHARS_PER_LINE); - log_lines[log_count][CHARS_PER_LINE] = '\0'; - log_count++; - - // Redraw: header + log + status - draw_live_frame(); - for (int i = 0; i < log_count && i < LOG_LINES; i++) { - fb_string(0, 10 + i * 8, log_lines[i]); - } - ssd_flush(); -} - -void display_status(const char* text) { - if (!live_mode) return; - - // Clear status area (page 7) - memset(&fb[7 * SCR_W], 0, SCR_W); - // Redraw separator (might have been cleared) - fb_hline(0, 127, 57); - - char status[CHARS_PER_LINE + 1]; - strncpy(status, text, CHARS_PER_LINE); - status[CHARS_PER_LINE] = '\0'; - fb_string(0, 58, status); - ssd_flush(); -} diff --git a/examples/host/midi2_host_feather/src/display.h b/examples/host/midi2_host_feather/src/display.h deleted file mode 100644 index ad8ead81d..000000000 --- a/examples/host/midi2_host_feather/src/display.h +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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 DISPLAY_H_ -#define DISPLAY_H_ - -#include -#include - -// Initialize SSD1306 display and show splash screen -void display_init(void); - -// Show waiting/connecting animation (call periodically) -void display_connecting(uint32_t elapsed_ms); - -// Transition to live message view -void display_live_begin(void); - -// Add a line to the scrolling log area (6 visible lines) -void display_log(const char* text, uint16_t color); - -// Update the status bar (bottom line) -void display_status(const char* text); - -#endif diff --git a/examples/host/midi2_host_feather/src/font5x7.h b/examples/host/midi2_host_feather/src/font5x7.h deleted file mode 100644 index e515faac1..000000000 --- a/examples/host/midi2_host_feather/src/font5x7.h +++ /dev/null @@ -1,107 +0,0 @@ -// Minimal 5x7 font, ASCII 32-126. Public domain. -// 5 bytes per character (5 columns, 7 rows, LSB=top row) - -#ifndef FONT5X7_H_ -#define FONT5X7_H_ - -#include - -static const uint8_t font5x7[] = { - 0x00,0x00,0x00,0x00,0x00, // 32 (space) - 0x00,0x00,0x5F,0x00,0x00, // 33 ! - 0x00,0x07,0x00,0x07,0x00, // 34 " - 0x14,0x7F,0x14,0x7F,0x14, // 35 # - 0x24,0x2A,0x7F,0x2A,0x12, // 36 $ - 0x23,0x13,0x08,0x64,0x62, // 37 % - 0x36,0x49,0x55,0x22,0x50, // 38 & - 0x00,0x05,0x03,0x00,0x00, // 39 ' - 0x00,0x1C,0x22,0x41,0x00, // 40 ( - 0x00,0x41,0x22,0x1C,0x00, // 41 ) - 0x08,0x2A,0x1C,0x2A,0x08, // 42 * - 0x08,0x08,0x3E,0x08,0x08, // 43 + - 0x00,0x50,0x30,0x00,0x00, // 44 , - 0x08,0x08,0x08,0x08,0x08, // 45 - - 0x00,0x60,0x60,0x00,0x00, // 46 . - 0x20,0x10,0x08,0x04,0x02, // 47 / - 0x3E,0x51,0x49,0x45,0x3E, // 48 0 - 0x00,0x42,0x7F,0x40,0x00, // 49 1 - 0x42,0x61,0x51,0x49,0x46, // 50 2 - 0x21,0x41,0x45,0x4B,0x31, // 51 3 - 0x18,0x14,0x12,0x7F,0x10, // 52 4 - 0x27,0x45,0x45,0x45,0x39, // 53 5 - 0x3C,0x4A,0x49,0x49,0x30, // 54 6 - 0x01,0x71,0x09,0x05,0x03, // 55 7 - 0x36,0x49,0x49,0x49,0x36, // 56 8 - 0x06,0x49,0x49,0x29,0x1E, // 57 9 - 0x00,0x36,0x36,0x00,0x00, // 58 : - 0x00,0x56,0x36,0x00,0x00, // 59 ; - 0x00,0x08,0x14,0x22,0x41, // 60 < - 0x14,0x14,0x14,0x14,0x14, // 61 = - 0x41,0x22,0x14,0x08,0x00, // 62 > - 0x02,0x01,0x51,0x09,0x06, // 63 ? - 0x32,0x49,0x79,0x41,0x3E, // 64 @ - 0x7E,0x11,0x11,0x11,0x7E, // 65 A - 0x7F,0x49,0x49,0x49,0x36, // 66 B - 0x3E,0x41,0x41,0x41,0x22, // 67 C - 0x7F,0x41,0x41,0x22,0x1C, // 68 D - 0x7F,0x49,0x49,0x49,0x41, // 69 E - 0x7F,0x09,0x09,0x01,0x01, // 70 F - 0x3E,0x41,0x41,0x51,0x32, // 71 G - 0x7F,0x08,0x08,0x08,0x7F, // 72 H - 0x00,0x41,0x7F,0x41,0x00, // 73 I - 0x20,0x40,0x41,0x3F,0x01, // 74 J - 0x7F,0x08,0x14,0x22,0x41, // 75 K - 0x7F,0x40,0x40,0x40,0x40, // 76 L - 0x7F,0x02,0x04,0x02,0x7F, // 77 M - 0x7F,0x04,0x08,0x10,0x7F, // 78 N - 0x3E,0x41,0x41,0x41,0x3E, // 79 O - 0x7F,0x09,0x09,0x09,0x06, // 80 P - 0x3E,0x41,0x51,0x21,0x5E, // 81 Q - 0x7F,0x09,0x19,0x29,0x46, // 82 R - 0x46,0x49,0x49,0x49,0x31, // 83 S - 0x01,0x01,0x7F,0x01,0x01, // 84 T - 0x3F,0x40,0x40,0x40,0x3F, // 85 U - 0x1F,0x20,0x40,0x20,0x1F, // 86 V - 0x7F,0x20,0x18,0x20,0x7F, // 87 W - 0x63,0x14,0x08,0x14,0x63, // 88 X - 0x03,0x04,0x78,0x04,0x03, // 89 Y - 0x61,0x51,0x49,0x45,0x43, // 90 Z - 0x00,0x00,0x7F,0x41,0x41, // 91 [ - 0x02,0x04,0x08,0x10,0x20, // 92 backslash - 0x41,0x41,0x7F,0x00,0x00, // 93 ] - 0x04,0x02,0x01,0x02,0x04, // 94 ^ - 0x40,0x40,0x40,0x40,0x40, // 95 _ - 0x00,0x01,0x02,0x04,0x00, // 96 ` - 0x20,0x54,0x54,0x54,0x78, // 97 a - 0x7F,0x48,0x44,0x44,0x38, // 98 b - 0x38,0x44,0x44,0x44,0x20, // 99 c - 0x38,0x44,0x44,0x48,0x7F, // 100 d - 0x38,0x54,0x54,0x54,0x18, // 101 e - 0x08,0x7E,0x09,0x01,0x02, // 102 f - 0x08,0x14,0x54,0x54,0x3C, // 103 g - 0x7F,0x08,0x04,0x04,0x78, // 104 h - 0x00,0x44,0x7D,0x40,0x00, // 105 i - 0x20,0x40,0x44,0x3D,0x00, // 106 j - 0x00,0x7F,0x10,0x28,0x44, // 107 k - 0x00,0x41,0x7F,0x40,0x00, // 108 l - 0x7C,0x04,0x18,0x04,0x78, // 109 m - 0x7C,0x08,0x04,0x04,0x78, // 110 n - 0x38,0x44,0x44,0x44,0x38, // 111 o - 0x7C,0x14,0x14,0x14,0x08, // 112 p - 0x08,0x14,0x14,0x18,0x7C, // 113 q - 0x7C,0x08,0x04,0x04,0x08, // 114 r - 0x48,0x54,0x54,0x54,0x20, // 115 s - 0x04,0x3F,0x44,0x40,0x20, // 116 t - 0x3C,0x40,0x40,0x20,0x7C, // 117 u - 0x1C,0x20,0x40,0x20,0x1C, // 118 v - 0x3C,0x40,0x30,0x40,0x3C, // 119 w - 0x44,0x28,0x10,0x28,0x44, // 120 x - 0x0C,0x50,0x50,0x50,0x3C, // 121 y - 0x44,0x64,0x54,0x4C,0x44, // 122 z - 0x00,0x08,0x36,0x41,0x00, // 123 { - 0x00,0x00,0x7F,0x00,0x00, // 124 | - 0x00,0x41,0x36,0x08,0x00, // 125 } - 0x10,0x08,0x08,0x10,0x08, // 126 ~ -}; - -#endif diff --git a/examples/host/midi2_host_feather/src/main.c b/examples/host/midi2_host_feather/src/main.c deleted file mode 100644 index 7c996355d..000000000 --- a/examples/host/midi2_host_feather/src/main.c +++ /dev/null @@ -1,267 +0,0 @@ -/* - * 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. - */ - -// MIDI 2.0 Host Example: Adafruit Feather RP2040 USB Host -// -// Receives UMP from a MIDI 2.0 Device via PIO-USB (GP16/GP17). -// SSD1306 OLED (I2C1, GP2/GP3) shows splash, spinner, then live UMP messages. - -#include -#include -#include "bsp/board_api.h" -#include "tusb.h" -#include "hardware/gpio.h" -#include "class/midi/midi2_host.h" -#include "class/midi/midi.h" -#include "display.h" - -//--------------------------------------------------------------------+ -// State -//--------------------------------------------------------------------+ - -static uint32_t note_count = 0; -static uint8_t midi2_idx = 0xFF; -static bool device_connected = false; -static bool mounted = false; - -//--------------------------------------------------------------------+ -// Note name helper -//--------------------------------------------------------------------+ - -static const char* NOTE_NAMES[] = { - "C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B" -}; - -static void note_name(uint8_t pitch, char* buf, size_t len) { - int octave = (pitch / 12) - 1; - snprintf(buf, len, "%s%d", NOTE_NAMES[pitch % 12], octave); -} - -//--------------------------------------------------------------------+ -// UMP decoder -//--------------------------------------------------------------------+ - -static void decode_ump(const uint32_t* words, uint8_t word_count) { - uint8_t mt = (uint8_t)((words[0] >> 28) & 0x0F); - char line[64]; - - if (mt == 0x4 && word_count >= 2) { - uint8_t status = (uint8_t)((words[0] >> 20) & 0x0F); - uint8_t channel = (uint8_t)((words[0] >> 16) & 0x0F); - - switch (status) { - case 0x9: { - uint8_t pitch = (uint8_t)((words[0] >> 8) & 0x7F); - uint16_t vel = (uint16_t)((words[1] >> 16) & 0xFFFF); - char nn[6]; - note_name(pitch, nn, sizeof(nn)); - snprintf(line, sizeof(line), "NoteOn %s ch%u v%04X", nn, channel, vel); - display_log(line, 0x07E0); - note_count++; - break; - } - case 0x8: { - uint8_t pitch = (uint8_t)((words[0] >> 8) & 0x7F); - char nn[6]; - note_name(pitch, nn, sizeof(nn)); - snprintf(line, sizeof(line), "NoteOff %s ch%u", nn, channel); - display_log(line, 0x8410); - break; - } - case 0xB: { - uint8_t idx = (uint8_t)((words[0] >> 8) & 0x7F); - snprintf(line, sizeof(line), "CC%-3u %08lX", idx, (unsigned long)words[1]); - display_log(line, 0x001F); - break; - } - case 0xC: { - uint8_t prog = (uint8_t)((words[1] >> 24) & 0x7F); - snprintf(line, sizeof(line), "ProgChg %u", prog); - display_log(line, 0xFFE0); - break; - } - case 0xE: { - snprintf(line, sizeof(line), "PBend %08lX", (unsigned long)words[1]); - display_log(line, 0xF81F); - break; - } - case 0xD: { - snprintf(line, sizeof(line), "CPress %08lX", (unsigned long)words[1]); - display_log(line, 0xFC10); - break; - } - case 0xA: { - uint8_t pitch = (uint8_t)((words[0] >> 8) & 0x7F); - char nn[6]; - note_name(pitch, nn, sizeof(nn)); - snprintf(line, sizeof(line), "PolyP %s %08lX", nn, (unsigned long)words[1]); - display_log(line, 0xFC10); - break; - } - case 0xF: { - uint8_t pitch = (uint8_t)((words[0] >> 8) & 0x7F); - uint8_t flags = (uint8_t)(words[0] & 0xFF); - snprintf(line, sizeof(line), "PN-Mgmt n%u f%02X", pitch, flags); - display_log(line, 0x07FF); - break; - } - default: { - snprintf(line, sizeof(line), "M2CVM s=0x%X", status); - display_log(line, 0xFFFF); - break; - } - } - } else if (mt == 0x0) { - uint8_t status = (uint8_t)((words[0] >> 20) & 0x0F); - if (status == 0x2) { - uint16_t ts = words[0] & 0xFFFF; - snprintf(line, sizeof(line), "JR-TS %04X", ts); - display_log(line, 0x8410); - } - } else { - snprintf(line, sizeof(line), "MT=0x%X w0=%08lX", - mt, (unsigned long)words[0]); - display_log(line, 0xFFFF); - } -} - -//--------------------------------------------------------------------+ -// MIDI 2.0 Host Callbacks -//--------------------------------------------------------------------+ - -void tuh_midi2_descriptor_cb(uint8_t idx, const tuh_midi2_descriptor_cb_t* d) { - (void)idx; - char line[48]; - snprintf(line, sizeof(line), "%s RX=%u TX=%u", - d->protocol_version ? "MIDI2" : "MIDI1", - d->rx_cable_count, d->tx_cable_count); - display_log(line, 0x07E0); -} - -void tuh_midi2_mount_cb(uint8_t idx, const tuh_midi2_mount_cb_t* m) { - midi2_idx = idx; - mounted = true; - - display_live_begin(); - - if (m->protocol_version) { - display_log("MIDI 2.0 ready", 0x07E0); - } else { - display_log("MIDI 1.0 device", 0xFFE0); - } - display_status("Receiving..."); -} - -void tuh_midi2_rx_cb(uint8_t idx, uint32_t xferred_bytes) { - (void)xferred_bytes; - - uint32_t words[16]; - while (1) { - uint32_t n = tuh_midi2_ump_read(idx, words, 16); - if (n == 0) break; - - uint32_t i = 0; - while (i < n) { - uint8_t mt = (uint8_t)((words[i] >> 28) & 0x0F); - uint8_t wc = midi2_ump_word_count(mt); - if (i + wc > n) break; - decode_ump(&words[i], wc); - i += wc; - } - } -} - -void tuh_midi2_tx_cb(uint8_t idx, uint32_t xferred_bytes) { - (void)idx; (void)xferred_bytes; -} - -void tuh_midi2_umount_cb(uint8_t idx) { - (void)idx; - midi2_idx = 0xFF; - device_connected = false; - mounted = false; - note_count = 0; - display_log("Disconnected", 0xF800); - display_status("Waiting..."); -} - -//--------------------------------------------------------------------+ -// Main -//--------------------------------------------------------------------+ - -int main(void) { - board_init(); - - // Enable 5V to USB-A port (Feather RP2040 USB Host: GP18) - gpio_init(18); - gpio_set_dir(18, GPIO_OUT); - gpio_put(18, 1); - - display_init(); - sleep_ms(1500); - - tusb_rhport_init_t host_init = { - .role = TUSB_ROLE_HOST, - .speed = TUSB_SPEED_FULL, - }; - tusb_init(BOARD_TUH_RHPORT, &host_init); - - uint32_t last_status_ms = 0; - - while (1) { - tuh_task(); - - uint32_t now = tusb_time_millis_api(); - - if (!mounted) { - // Spinner while waiting for device - if (now - last_status_ms > 200) { - last_status_ms = now; - display_connecting(now); - } - } else { - // Update note count every 2 seconds - if (now - last_status_ms > 2000) { - last_status_ms = now; - char line[22]; - snprintf(line, sizeof(line), "Notes: %lu", (unsigned long)note_count); - display_status(line); - } - } - } - - return 0; -} - -// Generic USB device mount/unmount -void tuh_mount_cb(uint8_t daddr) { - (void)daddr; - device_connected = true; -} - -void tuh_umount_cb(uint8_t daddr) { - (void)daddr; - device_connected = false; - mounted = false; -} diff --git a/examples/host/midi2_host_feather/src/tusb_config.h b/examples/host/midi2_host_feather/src/tusb_config.h deleted file mode 100644 index 362d0e2d0..000000000 --- a/examples/host/midi2_host_feather/src/tusb_config.h +++ /dev/null @@ -1,91 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2026 Saulo Verissimo - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -#ifndef TUSB_CONFIG_H_ -#define TUSB_CONFIG_H_ - -#ifdef __cplusplus -extern "C" { -#endif - -//--------------------------------------------------------------------+ -// Common Configuration -//--------------------------------------------------------------------+ - -#ifndef CFG_TUSB_MCU -#error CFG_TUSB_MCU must be defined -#endif - -#ifndef CFG_TUSB_OS -#define CFG_TUSB_OS OPT_OS_NONE -#endif - -#ifndef CFG_TUSB_DEBUG -#define CFG_TUSB_DEBUG 0 -#endif - -#ifndef CFG_TUH_MEM_SECTION -#define CFG_TUH_MEM_SECTION -#endif - -#ifndef CFG_TUH_MEM_ALIGN -#define CFG_TUH_MEM_ALIGN __attribute__ ((aligned(4))) -#endif - -//--------------------------------------------------------------------+ -// Host Configuration -// Adafruit Feather RP2040 USB Host: PIO-USB on GP16/GP17 (USB-A Host port) -//--------------------------------------------------------------------+ - -#define CFG_TUH_ENABLED 1 - -// PIO-USB Host on rhport 1 (USB-A connector on GP16/GP17) -#define CFG_TUH_RPI_PIO_USB 1 -#define BOARD_TUH_RHPORT 1 - -#ifndef BOARD_TUH_MAX_SPEED -#define BOARD_TUH_MAX_SPEED OPT_MODE_FULL_SPEED -#endif - -#define CFG_TUH_MAX_SPEED BOARD_TUH_MAX_SPEED - -//--------------------------------------------------------------------+ -// Driver Configuration -//--------------------------------------------------------------------+ - -#define CFG_TUH_ENUMERATION_BUFSIZE 256 - -#define CFG_TUH_HUB 0 -#define CFG_TUH_DEVICE_MAX 1 - -// MIDI 2.0 Host -#define CFG_TUH_MIDI2 1 -#define CFG_TUH_MIDI2_RX_BUFSIZE 512 -#define CFG_TUH_MIDI2_TX_BUFSIZE 512 - -#ifdef __cplusplus -} -#endif - -#endif -- 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 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(+) 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(-) 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(+) 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(-) 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(-) 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 3f209e2a00538726992ba908fe905213c78a9f6e Mon Sep 17 00:00:00 2001 From: HiFiPhile Date: Sun, 17 May 2026 14:11:53 +0200 Subject: midi2: move config to class header Signed-off-by: HiFiPhile --- src/class/midi/midi2_device.h | 43 +++++++++++++++++++++++++++++++++++++++++++ src/tusb_option.h | 38 -------------------------------------- 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/src/class/midi/midi2_device.h b/src/class/midi/midi2_device.h index 8c64729c3..b7caf97a3 100644 --- a/src/class/midi/midi2_device.h +++ b/src/class/midi/midi2_device.h @@ -44,9 +44,52 @@ extern "C" { #endif +//--------------------------------------------------------------------+ +// Class Driver Configuration +//--------------------------------------------------------------------+ + +#ifndef CFG_TUD_MIDI2_TX_BUFSIZE + #define CFG_TUD_MIDI2_TX_BUFSIZE TUD_EPSIZE_BULK_MAX +#endif + +#ifndef CFG_TUD_MIDI2_RX_BUFSIZE + #define CFG_TUD_MIDI2_RX_BUFSIZE TUD_EPSIZE_BULK_MAX +#endif + +#ifndef CFG_TUD_MIDI2_TX_EPSIZE + #define CFG_TUD_MIDI2_TX_EPSIZE TUD_EPSIZE_BULK_MAX +#endif + +#ifndef CFG_TUD_MIDI2_RX_EPSIZE + #define CFG_TUD_MIDI2_RX_EPSIZE TUD_EPSIZE_BULK_MAX +#endif + +#ifndef CFG_TUD_MIDI2_NUM_GROUPS + #define CFG_TUD_MIDI2_NUM_GROUPS 1 +#endif + +#ifndef CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS + #define CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS 1 +#endif + +#ifndef CFG_TUD_MIDI2_EP_NAME + #define CFG_TUD_MIDI2_EP_NAME "TinyUSB MIDI 2.0" +#endif + +#ifndef CFG_TUD_MIDI2_PRODUCT_ID + #define CFG_TUD_MIDI2_PRODUCT_ID "TinyUSB-MIDI2" +#endif + +// String descriptor index for the Group Terminal Block (iBlockItem, Table 5-6). +// 0 = no string descriptor (default, spec-allowed). +#ifndef CFG_TUD_MIDI2_BLOCK_STRIDX + #define CFG_TUD_MIDI2_BLOCK_STRIDX 0 +#endif + //--------------------------------------------------------------------+ // MIDI Protocol Values (returned by tud_midi2_n_protocol) //--------------------------------------------------------------------+ + // Per USB-MIDI 2.0 spec, UMP Stream Configuration messages. enum { MIDI_PROTOCOL_MIDI1 = 0x01, diff --git a/src/tusb_option.h b/src/tusb_option.h index 2614110fc..0114b86ee 100644 --- a/src/tusb_option.h +++ b/src/tusb_option.h @@ -650,44 +650,6 @@ #define CFG_TUD_MIDI2 0 #endif -#ifndef CFG_TUD_MIDI2_TX_BUFSIZE - #define CFG_TUD_MIDI2_TX_BUFSIZE 256 -#endif - -#ifndef CFG_TUD_MIDI2_RX_BUFSIZE - #define CFG_TUD_MIDI2_RX_BUFSIZE 256 -#endif - -#ifndef CFG_TUD_MIDI2_TX_EPSIZE - #define CFG_TUD_MIDI2_TX_EPSIZE 64 -#endif - -#ifndef CFG_TUD_MIDI2_RX_EPSIZE - #define CFG_TUD_MIDI2_RX_EPSIZE 64 -#endif - -#ifndef CFG_TUD_MIDI2_NUM_GROUPS - #define CFG_TUD_MIDI2_NUM_GROUPS 1 -#endif - -#ifndef CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS - #define CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS 1 -#endif - -#ifndef CFG_TUD_MIDI2_EP_NAME - #define CFG_TUD_MIDI2_EP_NAME "TinyUSB MIDI 2.0" -#endif - -#ifndef CFG_TUD_MIDI2_PRODUCT_ID - #define CFG_TUD_MIDI2_PRODUCT_ID "TinyUSB-MIDI2" -#endif - -// String descriptor index for the Group Terminal Block (iBlockItem, Table 5-6). -// 0 = no string descriptor (default, spec-allowed). -#ifndef CFG_TUD_MIDI2_BLOCK_STRIDX - #define CFG_TUD_MIDI2_BLOCK_STRIDX 0 -#endif - #ifndef CFG_TUD_VENDOR #define CFG_TUD_VENDOR 0 #endif -- cgit v1.3.1 From e00ac15b7f31be28ff888a5220cbde93a61c17f4 Mon Sep 17 00:00:00 2001 From: HiFiPhile Date: Sun, 17 May 2026 14:35:34 +0200 Subject: midi2: use hwfifo if supported Signed-off-by: HiFiPhile --- src/class/midi/midi2_device.c | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/class/midi/midi2_device.c b/src/class/midi/midi2_device.c index a005c8e51..360d70967 100644 --- a/src/class/midi/midi2_device.c +++ b/src/class/midi/midi2_device.c @@ -114,12 +114,15 @@ TU_VERIFY_STATIC(CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS >= 1 && CFG_TUD_MIDI2_NUM_FUN static midi2d_interface_t _midi2d_itf[CFG_TUD_MIDI2]; +// Skip local EP buffer if dedicated hw FIFO is supported +#if CFG_TUD_EDPT_DEDICATED_HWFIFO == 0 typedef struct { TUD_EPBUF_DEF(epin, CFG_TUD_MIDI2_TX_EPSIZE); TUD_EPBUF_DEF(epout, CFG_TUD_MIDI2_RX_EPSIZE); } midi2d_epbuf_t; CFG_TUD_MEM_SECTION static midi2d_epbuf_t _midi2d_epbuf[CFG_TUD_MIDI2]; +#endif // Default Group Terminal Block descriptor (USB-MIDI 2.0 spec, Table 5-5/5-6) static const uint8_t _default_gtb_desc[] = { @@ -406,9 +409,13 @@ void midi2d_init(void) { midi2d_interface_t* p_midi = &_midi2d_itf[i]; p_midi->protocol = MIDI_PROTOCOL_MIDI2; - midi2d_epbuf_t* p_epbuf = &_midi2d_epbuf[i]; - uint8_t* epout_buf = p_epbuf->epout; - uint8_t* epin_buf = p_epbuf->epin; + #if CFG_TUD_EDPT_DEDICATED_HWFIFO + uint8_t *epout_buf = NULL; + uint8_t *epin_buf = NULL; + #else + uint8_t *epout_buf = _midi2d_epbuf[i].epout; + uint8_t *epin_buf = _midi2d_epbuf[i].epin; + #endif tu_edpt_stream_init(&p_midi->ep_stream.rx, false, false, false, p_midi->ep_stream.rx_ff_buf, CFG_TUD_MIDI2_RX_BUFSIZE, epout_buf); -- cgit v1.3.1 From 5ac6346dd4980af75973e53dd130e7ee2a9af32d Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Sun, 17 May 2026 11:31:12 -0300 Subject: midi2: peek 4 bytes for MT, restore 256 buffers MT is in byte 3 of the UMP word in LE memory, not byte 0. Buffers at mps blocked RX xfer re-arm on partial packets. --- src/class/midi/midi2_device.c | 15 +++++++++------ src/class/midi/midi2_device.h | 4 ++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/class/midi/midi2_device.c b/src/class/midi/midi2_device.c index 360d70967..48cb3d388 100644 --- a/src/class/midi/midi2_device.c +++ b/src/class/midi/midi2_device.c @@ -266,10 +266,12 @@ static void _nego_handle_stream_msg(midi2d_interface_t* p_midi, const uint32_t* static void _nego_process_rx(midi2d_interface_t* p_midi) { tu_edpt_stream_t* ep_rx = &p_midi->ep_stream.rx; - uint8_t first_byte; + uint8_t word_bytes[4]; - while (tu_edpt_stream_peek(ep_rx, &first_byte)) { - uint8_t mt = (first_byte >> 4) & 0x0F; + while (tu_fifo_peek_n(&ep_rx->ff, word_bytes, 4) == 4) { + // UMP words travel LSB-first on the wire and in LE memory, so MT is in + // the high nibble of byte 3, not byte 0. + uint8_t mt = (word_bytes[3] >> 4) & 0x0F; uint8_t pkt_words = midi2_ump_word_count(mt); uint32_t pkt_bytes = (uint32_t)pkt_words * 4; @@ -310,10 +312,11 @@ uint32_t tud_midi2_n_ump_read(uint8_t itf, uint32_t* words, uint32_t max_words) uint32_t total_read = 0; while (total_read < max_words) { - uint8_t first_byte; - if (!tu_edpt_stream_peek(ep_rx, &first_byte)) break; + uint8_t word_bytes[4]; + if (tu_fifo_peek_n(&ep_rx->ff, word_bytes, 4) < 4) break; - uint8_t mt = (first_byte >> 4) & 0x0F; + // UMP words travel LSB-first; MT is the high nibble of byte 3, not byte 0. + uint8_t mt = (word_bytes[3] >> 4) & 0x0F; uint8_t pkt_words = midi2_ump_word_count(mt); if (total_read + pkt_words > max_words) break; diff --git a/src/class/midi/midi2_device.h b/src/class/midi/midi2_device.h index b7caf97a3..a1c51f047 100644 --- a/src/class/midi/midi2_device.h +++ b/src/class/midi/midi2_device.h @@ -49,11 +49,11 @@ extern "C" { //--------------------------------------------------------------------+ #ifndef CFG_TUD_MIDI2_TX_BUFSIZE - #define CFG_TUD_MIDI2_TX_BUFSIZE TUD_EPSIZE_BULK_MAX + #define CFG_TUD_MIDI2_TX_BUFSIZE 256 #endif #ifndef CFG_TUD_MIDI2_RX_BUFSIZE - #define CFG_TUD_MIDI2_RX_BUFSIZE TUD_EPSIZE_BULK_MAX + #define CFG_TUD_MIDI2_RX_BUFSIZE 256 #endif #ifndef CFG_TUD_MIDI2_TX_EPSIZE -- cgit v1.3.1 From 4b729bfb9274af44f8edfba2134893a6889cfd03 Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Sun, 17 May 2026 12:19:45 -0300 Subject: midi2: per-interface weak callbacks for UMP Stream config Lets each instance return different NUM_GROUPS, NUM_FUNCTION_BLOCKS, EP_NAME and PRODUCT_ID. Defaults fall back to the macros. --- src/class/midi/midi2_device.c | 29 +++++++++++++++++++++++------ src/class/midi/midi2_device.h | 6 ++++++ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/class/midi/midi2_device.c b/src/class/midi/midi2_device.c index 48cb3d388..534458a24 100644 --- a/src/class/midi/midi2_device.c +++ b/src/class/midi/midi2_device.c @@ -42,6 +42,18 @@ TU_ATTR_WEAK void tud_midi2_set_itf_cb(uint8_t itf, uint8_t alt) { (void) itf; ( TU_ATTR_WEAK bool tud_midi2_get_req_itf_cb(uint8_t rhport, const tusb_control_request_t* request) { (void) rhport; (void) request; return false; } +TU_ATTR_WEAK uint8_t tud_midi2_num_groups_cb(uint8_t itf) { + (void) itf; return CFG_TUD_MIDI2_NUM_GROUPS; +} +TU_ATTR_WEAK uint8_t tud_midi2_num_function_blocks_cb(uint8_t itf) { + (void) itf; return CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS; +} +TU_ATTR_WEAK const char* tud_midi2_ep_name_cb(uint8_t itf) { + (void) itf; return CFG_TUD_MIDI2_EP_NAME; +} +TU_ATTR_WEAK const char* tud_midi2_product_id_cb(uint8_t itf) { + (void) itf; return CFG_TUD_MIDI2_PRODUCT_ID; +} //--------------------------------------------------------------------+ // Byte order note @@ -114,6 +126,10 @@ TU_VERIFY_STATIC(CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS >= 1 && CFG_TUD_MIDI2_NUM_FUN static midi2d_interface_t _midi2d_itf[CFG_TUD_MIDI2]; +static inline uint8_t _itf_idx(const midi2d_interface_t* p_midi) { + return (uint8_t)(p_midi - _midi2d_itf); +} + // Skip local EP buffer if dedicated hw FIFO is supported #if CFG_TUD_EDPT_DEDICATED_HWFIFO == 0 typedef struct { @@ -164,7 +180,7 @@ static void _nego_send_endpoint_info(midi2d_interface_t* p_midi) { | ((uint32_t) UMP_VER_MAJOR << 8) | (uint32_t) UMP_VER_MINOR; msg[1] = (UINT32_C(1) << 31) // Static Function Blocks flag - | ((uint32_t)(CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS & 0x7F) << 24) + | ((uint32_t)(tud_midi2_num_function_blocks_cb(_itf_idx(p_midi)) & 0x7F) << 24) | (UINT32_C(1) << 9) // MIDI 2.0 Protocol capability | (UINT32_C(1) << 8); // MIDI 1.0 Protocol capability _nego_send_ump(p_midi, msg, 4); @@ -223,7 +239,7 @@ static void _nego_send_fb_info(midi2d_interface_t* p_midi, uint8_t fb_idx) { | ((uint32_t) fb_idx << 8) | 0x02; // bDirection: bidirectional msg[1] = ((uint32_t) 0 << 24) // bFirstGroup - | ((uint32_t) CFG_TUD_MIDI2_NUM_GROUPS << 16); + | ((uint32_t) tud_midi2_num_groups_cb(_itf_idx(p_midi)) << 16); _nego_send_ump(p_midi, msg, 4); } @@ -233,8 +249,8 @@ static void _nego_handle_stream_msg(midi2d_interface_t* p_midi, const uint32_t* switch (status) { case STREAM_ENDPOINT_DISCOVERY: _nego_send_endpoint_info(p_midi); - _nego_send_stream_text(p_midi, STREAM_EP_NAME, CFG_TUD_MIDI2_EP_NAME); - _nego_send_stream_text(p_midi, STREAM_PROD_INSTANCE_ID, CFG_TUD_MIDI2_PRODUCT_ID); + _nego_send_stream_text(p_midi, STREAM_EP_NAME, tud_midi2_ep_name_cb(_itf_idx(p_midi))); + _nego_send_stream_text(p_midi, STREAM_PROD_INSTANCE_ID, tud_midi2_product_id_cb(_itf_idx(p_midi))); break; case STREAM_CONFIG_REQUEST: { @@ -249,11 +265,12 @@ static void _nego_handle_stream_msg(midi2d_interface_t* p_midi, const uint32_t* case STREAM_FB_DISCOVERY: { uint8_t fb_idx = (words[0] >> 8) & 0xFF; + uint8_t fb_count = tud_midi2_num_function_blocks_cb(_itf_idx(p_midi)); if (fb_idx == 0xFF) { - for (uint8_t f = 0; f < CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS; f++) { + for (uint8_t f = 0; f < fb_count; f++) { _nego_send_fb_info(p_midi, f); } - } else if (fb_idx < CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS) { + } else if (fb_idx < fb_count) { _nego_send_fb_info(p_midi, fb_idx); } break; diff --git a/src/class/midi/midi2_device.h b/src/class/midi/midi2_device.h index a1c51f047..cb47bdf3c 100644 --- a/src/class/midi/midi2_device.h +++ b/src/class/midi/midi2_device.h @@ -103,6 +103,12 @@ void tud_midi2_rx_cb(uint8_t itf); void tud_midi2_set_itf_cb(uint8_t itf, uint8_t alt); bool tud_midi2_get_req_itf_cb(uint8_t rhport, const tusb_control_request_t* request); +// Per-interface UMP Stream config (override for per-itf values). +uint8_t tud_midi2_num_groups_cb(uint8_t itf); +uint8_t tud_midi2_num_function_blocks_cb(uint8_t itf); +const char* tud_midi2_ep_name_cb(uint8_t itf); +const char* tud_midi2_product_id_cb(uint8_t itf); + //--------------------------------------------------------------------+ // Application API (Multiple Interfaces) //--------------------------------------------------------------------+ -- cgit v1.3.1 From 287096f7333aa10b6aaaf75337ccd3de9b06b95d Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Sun, 17 May 2026 13:07:21 -0300 Subject: midi2: scale device buffers with EPSIZE Match midi2_host pattern. Literal 256 underran HS endpoints (512B). --- src/class/midi/midi2_device.h | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/class/midi/midi2_device.h b/src/class/midi/midi2_device.h index cb47bdf3c..b0bbd7572 100644 --- a/src/class/midi/midi2_device.h +++ b/src/class/midi/midi2_device.h @@ -48,14 +48,6 @@ extern "C" { // Class Driver Configuration //--------------------------------------------------------------------+ -#ifndef CFG_TUD_MIDI2_TX_BUFSIZE - #define CFG_TUD_MIDI2_TX_BUFSIZE 256 -#endif - -#ifndef CFG_TUD_MIDI2_RX_BUFSIZE - #define CFG_TUD_MIDI2_RX_BUFSIZE 256 -#endif - #ifndef CFG_TUD_MIDI2_TX_EPSIZE #define CFG_TUD_MIDI2_TX_EPSIZE TUD_EPSIZE_BULK_MAX #endif @@ -64,6 +56,14 @@ extern "C" { #define CFG_TUD_MIDI2_RX_EPSIZE TUD_EPSIZE_BULK_MAX #endif +#ifndef CFG_TUD_MIDI2_TX_BUFSIZE + #define CFG_TUD_MIDI2_TX_BUFSIZE (4 * CFG_TUD_MIDI2_TX_EPSIZE) +#endif + +#ifndef CFG_TUD_MIDI2_RX_BUFSIZE + #define CFG_TUD_MIDI2_RX_BUFSIZE (4 * CFG_TUD_MIDI2_RX_EPSIZE) +#endif + #ifndef CFG_TUD_MIDI2_NUM_GROUPS #define CFG_TUD_MIDI2_NUM_GROUPS 1 #endif -- 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(-) 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 aaca323bd34945178e88de51acf67c4d0443a726 Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Mon, 18 May 2026 11:42:34 -0300 Subject: midi2: boundary-aware drain in xfer_cb to keep UMP packets intact --- src/class/midi/midi2_device.c | 51 ++++++++++++++++++++++++++++++++++++++++--- src/class/midi/midi2_host.c | 42 +++++++++++++++++++++++++++++++++-- 2 files changed, 88 insertions(+), 5 deletions(-) diff --git a/src/class/midi/midi2_device.c b/src/class/midi/midi2_device.c index 534458a24..c61884689 100644 --- a/src/class/midi/midi2_device.c +++ b/src/class/midi/midi2_device.c @@ -635,6 +635,53 @@ bool midi2d_control_xfer_cb(uint8_t rhport, uint8_t stage, const tusb_control_re } } +// Drain whole UMP packets from the TX FIFO into the EP buffer, capped at +// wMaxPacketSize. Needed when CFG_TUD_MIDI2_TX_BUFSIZE > mps: the FIFO can +// then hold more bytes than fit in a single USB transfer, and a blind +// tu_edpt_stream_write_xfer would split a UMP across two transfers. +static void midi2d_flush_tx_boundary_aware(midi2d_interface_t* p_midi, uint32_t last_xferred) { + tu_edpt_stream_t* ep_tx = &p_midi->ep_stream.tx; + const uint16_t mps = ep_tx->mps; + const uint16_t ff_count = tu_fifo_count(&ep_tx->ff); + + if (ff_count == 0) { + (void) tu_edpt_stream_write_zlp_if_needed(ep_tx, last_xferred); + return; + } + if (ff_count <= mps) { + // Whole FIFO fits in one transfer; the stream API drain is safe. + (void) tu_edpt_stream_write_xfer(ep_tx); + return; + } + if (ep_tx->ep_buf == NULL) { + // HWFIFO mode: relies on CFG_TUD_MIDI2_TX_BUFSIZE <= mps for UMP integrity. + (void) tu_edpt_stream_write_xfer(ep_tx); + return; + } + + // ff_count > mps and a local EP buffer is available: drain only whole UMP + // packets up to mps to preserve packet boundaries on the USB wire. + uint8_t word_bytes[4]; + uint8_t* buf = ep_tx->ep_buf; + uint16_t bytes = 0; + while (bytes < mps) { + if (tu_fifo_count(&ep_tx->ff) < 4) break; + if (4 != tu_fifo_peek_n(&ep_tx->ff, word_bytes, 4)) break; + uint8_t mt = (uint8_t)((word_bytes[3] >> 4) & 0x0F); + uint8_t pkt_words = midi2_ump_word_count(mt); + uint16_t pkt_bytes = (uint16_t)(pkt_words * 4); + if (tu_fifo_count(&ep_tx->ff) < pkt_bytes) break; + if (bytes + pkt_bytes > mps) break; + tu_fifo_read_n(&ep_tx->ff, buf + bytes, pkt_bytes); + bytes = (uint16_t)(bytes + pkt_bytes); + } + if (bytes == 0) return; + if (!usbd_edpt_claim(p_midi->rhport, ep_tx->ep_addr)) return; + if (!usbd_edpt_xfer(p_midi->rhport, ep_tx->ep_addr, buf, bytes, false)) { + usbd_edpt_release(p_midi->rhport, ep_tx->ep_addr); + } +} + bool midi2d_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { (void) rhport; @@ -655,9 +702,7 @@ bool midi2d_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint3 } tu_edpt_stream_read_xfer(ep_rx); } else if (ep_addr == ep_tx->ep_addr && result == XFER_RESULT_SUCCESS) { - if (0 == tu_edpt_stream_write_xfer(ep_tx)) { - (void) tu_edpt_stream_write_zlp_if_needed(ep_tx, xferred_bytes); - } + midi2d_flush_tx_boundary_aware(p_midi, xferred_bytes); } else { return false; } diff --git a/src/class/midi/midi2_host.c b/src/class/midi/midi2_host.c index 2046fdb67..5d77e5990 100644 --- a/src/class/midi/midi2_host.c +++ b/src/class/midi/midi2_host.c @@ -455,6 +455,44 @@ void midih2_close(uint8_t dev_addr) { } } +// Drain whole UMP packets from the TX FIFO into the EP buffer, capped at +// wMaxPacketSize. Needed when CFG_TUH_MIDI2_TX_BUFSIZE > mps to keep UMP +// packets from crossing USB transfer boundaries. +static void midih2_flush_tx_boundary_aware(midih2_interface_t* p_midi, uint32_t last_xferred) { + tu_edpt_stream_t* ep_tx = &p_midi->ep_stream.tx; + const uint16_t mps = ep_tx->mps; + const uint16_t ff_count = tu_fifo_count(&ep_tx->ff); + + if (ff_count == 0) { + (void) tu_edpt_stream_write_zlp_if_needed(ep_tx, last_xferred); + return; + } + if (ff_count <= mps || ep_tx->ep_buf == NULL) { + (void) tu_edpt_stream_write_xfer(ep_tx); + return; + } + + uint8_t word_bytes[4]; + uint8_t* buf = ep_tx->ep_buf; + uint16_t bytes = 0; + while (bytes < mps) { + if (tu_fifo_count(&ep_tx->ff) < 4) break; + if (4 != tu_fifo_peek_n(&ep_tx->ff, word_bytes, 4)) break; + uint8_t mt = (uint8_t)((word_bytes[3] >> 4) & 0x0F); + uint8_t pkt_words = midi2_ump_word_count(mt); + uint16_t pkt_bytes = (uint16_t)(pkt_words * 4); + if (tu_fifo_count(&ep_tx->ff) < pkt_bytes) break; + if (bytes + pkt_bytes > mps) break; + tu_fifo_read_n(&ep_tx->ff, buf + bytes, pkt_bytes); + bytes = (uint16_t)(bytes + pkt_bytes); + } + if (bytes == 0) return; + if (!usbh_edpt_claim(p_midi->daddr, ep_tx->ep_addr)) return; + if (!usbh_edpt_xfer(p_midi->daddr, ep_tx->ep_addr, buf, bytes)) { + usbh_edpt_release(p_midi->daddr, ep_tx->ep_addr); + } +} + bool midih2_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { uint8_t idx = get_idx_by_ep_addr(dev_addr, ep_addr); TU_VERIFY(idx < CFG_TUH_MIDI2); @@ -469,8 +507,8 @@ bool midih2_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uin tu_edpt_stream_read_xfer(&p_midi->ep_stream.rx); } else if (ep_addr == p_midi->ep_stream.tx.ep_addr) { tuh_midi2_tx_cb(idx, xferred_bytes); - if (0 == tu_edpt_stream_write_xfer(&p_midi->ep_stream.tx)) { - tu_edpt_stream_write_zlp_if_needed(&p_midi->ep_stream.tx, xferred_bytes); + if (result == XFER_RESULT_SUCCESS) { + midih2_flush_tx_boundary_aware(p_midi, xferred_bytes); } } -- cgit v1.3.1 From 4baf1883c194c2c4b6fa30cc0c443ad6c83e0b19 Mon Sep 17 00:00:00 2001 From: HiFiPhile Date: Mon, 18 May 2026 23:07:49 +0200 Subject: midi2: convert to raw Tx FIFO for better segmentation handling, add count to packet api Signed-off-by: HiFiPhile --- docs/reference/class_drivers.rst | 4 +- src/class/midi/midi2_device.c | 276 +++++++++++++++++++++++---------------- src/class/midi/midi2_device.h | 14 +- 3 files changed, 176 insertions(+), 118 deletions(-) diff --git a/docs/reference/class_drivers.rst b/docs/reference/class_drivers.rst index 3ac0d8d4e..4a101fabc 100644 --- a/docs/reference/class_drivers.rst +++ b/docs/reference/class_drivers.rst @@ -64,8 +64,8 @@ I/O Functions uint32_t tud_midi2_ump_read(uint32_t* words, uint32_t max_words); uint32_t tud_midi2_ump_write(const uint32_t* words, uint32_t count); - bool tud_midi2_packet_read(uint8_t packet[4]); - bool tud_midi2_packet_write(const uint8_t packet[4]); + uint32_t tud_midi2_packet_read(uint8_t packets[], uint32_t max_packets); + uint32_t tud_midi2_packet_write(const uint8_t packets[], uint32_t count); Callbacks ^^^^^^^^^ diff --git a/src/class/midi/midi2_device.c b/src/class/midi/midi2_device.c index c61884689..b14f439f8 100644 --- a/src/class/midi/midi2_device.c +++ b/src/class/midi/midi2_device.c @@ -100,6 +100,16 @@ enum { //--------------------------------------------------------------------+ // MACRO CONSTANT TYPEDEF //--------------------------------------------------------------------+ +typedef struct { + uint8_t ep_addr; + uint16_t mps; + tu_fifo_t ff; + +#if CFG_TUD_EDPT_DEDICATED_HWFIFO == 0 + uint8_t* ep_buf; +#endif +} midi2d_tx_t; + typedef struct { uint8_t rhport; uint8_t itf_num; @@ -109,7 +119,7 @@ typedef struct { /*------------- From this point, data is not cleared by bus reset -------------*/ struct { - tu_edpt_stream_t tx; + midi2d_tx_t tx; tu_edpt_stream_t rx; uint8_t rx_ff_buf[CFG_TUD_MIDI2_RX_BUFSIZE]; @@ -117,19 +127,6 @@ typedef struct { } ep_stream; } midi2d_interface_t; -TU_VERIFY_STATIC(CFG_TUD_MIDI2_NUM_GROUPS >= 1 && CFG_TUD_MIDI2_NUM_GROUPS <= 16, - "CFG_TUD_MIDI2_NUM_GROUPS must be 1..16"); -TU_VERIFY_STATIC(CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS >= 1 && CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS <= 32, - "CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS must be 1..32"); - -#define ITF_MEM_RESET_SIZE offsetof(midi2d_interface_t, ep_stream) - -static midi2d_interface_t _midi2d_itf[CFG_TUD_MIDI2]; - -static inline uint8_t _itf_idx(const midi2d_interface_t* p_midi) { - return (uint8_t)(p_midi - _midi2d_itf); -} - // Skip local EP buffer if dedicated hw FIFO is supported #if CFG_TUD_EDPT_DEDICATED_HWFIFO == 0 typedef struct { @@ -140,6 +137,15 @@ typedef struct { CFG_TUD_MEM_SECTION static midi2d_epbuf_t _midi2d_epbuf[CFG_TUD_MIDI2]; #endif +TU_VERIFY_STATIC(CFG_TUD_MIDI2_NUM_GROUPS >= 1 && CFG_TUD_MIDI2_NUM_GROUPS <= 16, + "CFG_TUD_MIDI2_NUM_GROUPS must be 1..16"); +TU_VERIFY_STATIC(CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS >= 1 && CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS <= 32, + "CFG_TUD_MIDI2_NUM_FUNCTION_BLOCKS must be 1..32"); + +#define ITF_MEM_RESET_SIZE offsetof(midi2d_interface_t, ep_stream) + +static midi2d_interface_t _midi2d_itf[CFG_TUD_MIDI2]; + // Default Group Terminal Block descriptor (USB-MIDI 2.0 spec, Table 5-5/5-6) static const uint8_t _default_gtb_desc[] = { // GTB Header (5 bytes) @@ -162,15 +168,112 @@ static const uint8_t _default_gtb_desc[] = { 0, 0 // wMaxOutputBandwidth: unknown }; +//--------------------------------------------------------------------+ +// Common utility functions +//--------------------------------------------------------------------+ + +static inline uint8_t _itf_idx(const midi2d_interface_t* p_midi) { + return (uint8_t)(p_midi - _midi2d_itf); +} + +static inline bool _tx_opened(const midi2d_interface_t* p_midi) { + return p_midi->ep_stream.tx.ep_addr != 0; +} + +static uint8_t _tx_byte_at(const tu_fifo_buffer_info_t* info, uint16_t offset) { + if (offset < info->linear.len) { + return info->linear.ptr[offset]; + } + + offset = (uint16_t) (offset - info->linear.len); + if (offset < info->wrapped.len) { + return info->wrapped.ptr[offset]; + } + + return 0; +} + +// Calculate the largest byte count that contains only whole UMP packets and +// fits in one USB transfer (<= mps). +static uint16_t _tx_nonseg_len_to_mps(midi2d_tx_t* tx) { + tu_fifo_buffer_info_t info; + tu_fifo_get_read_info(&tx->ff, &info); + + const uint16_t available = (uint16_t) (info.linear.len + info.wrapped.len); + uint16_t bytes = 0; + + while (bytes < tx->mps) { + if ((uint16_t) (available - bytes) < 4) break; + + uint8_t mt = (uint8_t)((_tx_byte_at(&info, (uint16_t) (bytes + 3)) >> 4) & 0x0F); + uint8_t pkt_words = midi2_ump_word_count(mt); + uint16_t pkt_bytes = (uint16_t) pkt_words * 4; + + if (pkt_bytes == 0) break; + if ((uint16_t) (available - bytes) < pkt_bytes) break; + if ((uint16_t) (bytes + pkt_bytes) > tx->mps) break; + + bytes = (uint16_t) (bytes + pkt_bytes); + } + + return bytes; +} + +// Start one IN transfer capped at mps, return number of bytes queued to the controller, or 0 if nothing was queued. +static uint16_t _tx_start_xfer(midi2d_interface_t* p_midi) { + midi2d_tx_t* tx = &p_midi->ep_stream.tx; + uint16_t ff_count = tu_fifo_count(&tx->ff); + + if (ff_count == 0) return 0; + + if (!usbd_edpt_claim(p_midi->rhport, tx->ep_addr)) return 0; + + uint16_t bytes; + if (p_midi->alt_setting == 1) { + bytes = _tx_nonseg_len_to_mps(tx); + } else { + bytes = tu_min16(tu_fifo_count(&tx->ff), tx->mps); + } + if (bytes == 0) { + usbd_edpt_release(p_midi->rhport, tx->ep_addr); + return 0; + } + +#if CFG_TUD_EDPT_DEDICATED_HWFIFO + TU_ASSERT(usbd_edpt_xfer_fifo(p_midi->rhport, tx->ep_addr, &tx->ff, bytes, false), 0); +#else + tu_fifo_read_n(&tx->ff, tx->ep_buf, bytes); + TU_ASSERT(usbd_edpt_xfer(p_midi->rhport, tx->ep_addr, tx->ep_buf, bytes, false), 0); +#endif + + return bytes; +} + +static uint32_t _tx_ump_write(midi2d_interface_t* p_midi, const uint32_t* words, uint32_t count) { + uint32_t written = 0; + while (written < count) { + uint8_t mt = (uint8_t)((words[written] >> 28) & 0x0F); + uint8_t pkt_words = midi2_ump_word_count(mt); + uint16_t pkt_bytes = (uint16_t) pkt_words * 4; + + if (written + pkt_words > count) break; + if (tu_fifo_remaining(&p_midi->ep_stream.tx.ff) < pkt_bytes) break; + + if (tu_fifo_write_n(&p_midi->ep_stream.tx.ff, &words[written], pkt_bytes) != pkt_bytes) break; + written += pkt_words; + } + + (void) _tx_start_xfer(p_midi); + return written; +} + //--------------------------------------------------------------------+ // Protocol Negotiation //--------------------------------------------------------------------+ static void _nego_send_ump(midi2d_interface_t* p_midi, const uint32_t* words, uint8_t count) { - tu_edpt_stream_t* ep_tx = &p_midi->ep_stream.tx; - if (!tu_edpt_stream_is_opened(ep_tx)) return; - if (tu_edpt_stream_write_available(ep_tx) < count * 4) return; - tu_edpt_stream_write(ep_tx, words, count * 4); - tu_edpt_stream_write_xfer(ep_tx); + if (!_tx_opened(p_midi)) return; + if (tu_fifo_remaining(&p_midi->ep_stream.tx.ff) < (uint32_t) count * 4) return; + (void) _tx_ump_write(p_midi, words, count); } static void _nego_send_endpoint_info(midi2d_interface_t* p_midi) { @@ -307,7 +410,7 @@ static void _nego_process_rx(midi2d_interface_t* p_midi) { bool tud_midi2_n_mounted(uint8_t itf) { TU_VERIFY(itf < CFG_TUD_MIDI2, false); midi2d_interface_t* p_midi = &_midi2d_itf[itf]; - return tu_edpt_stream_is_opened(&p_midi->ep_stream.tx) && + return _tx_opened(p_midi) && tu_edpt_stream_is_opened(&p_midi->ep_stream.rx); } @@ -346,10 +449,10 @@ uint32_t tud_midi2_n_ump_read(uint8_t itf, uint32_t* words, uint32_t max_words) return total_read; } -bool tud_midi2_n_packet_read(uint8_t itf, uint8_t packet[4]) { - TU_VERIFY(itf < CFG_TUD_MIDI2, false); +uint32_t tud_midi2_n_packet_read(uint8_t itf, uint8_t packets[], uint32_t max_packets) { + TU_VERIFY(itf < CFG_TUD_MIDI2 && packets != NULL && max_packets > 0, 0); midi2d_interface_t* p_midi = &_midi2d_itf[itf]; - return 4 == tu_edpt_stream_read(&p_midi->ep_stream.rx, packet, 4); + return tu_edpt_stream_read(&p_midi->ep_stream.rx, packets, max_packets * 4u) >> 2u; } //--------------------------------------------------------------------+ @@ -362,44 +465,31 @@ uint32_t tud_midi2_n_ump_write(uint8_t itf, const uint32_t* words, uint32_t coun // UMP API is only valid on Alt Setting 1 (USB-MIDI 2.0). // Alt 0 carries USB-MIDI 1.0 32-bit Event Packets, not UMP words. if (p_midi->alt_setting != 1) { return 0; } + TU_VERIFY(_tx_opened(p_midi), 0); - tu_edpt_stream_t* ep_tx = &p_midi->ep_stream.tx; - TU_VERIFY(tu_edpt_stream_is_opened(ep_tx), 0); + return _tx_ump_write(p_midi, words, count); +} + +uint32_t tud_midi2_n_packet_write(uint8_t itf, const uint8_t packets[], uint32_t count) { + TU_VERIFY(itf < CFG_TUD_MIDI2 && packets != NULL && count > 0, 0); + midi2d_interface_t* p_midi = &_midi2d_itf[itf]; + midi2d_tx_t* tx = &p_midi->ep_stream.tx; + + // Packet API is for Alt Setting 0 (USB-MIDI 1.0) event packets. + TU_VERIFY(p_midi->alt_setting == 0, 0); + TU_VERIFY(_tx_opened(p_midi), 0); uint32_t written = 0; while (written < count) { - uint8_t mt = (uint8_t)((words[written] >> 28) & 0x0F); - uint8_t pkt_words = midi2_ump_word_count(mt); - uint32_t pkt_bytes = (uint32_t)pkt_words * 4; + if (tu_fifo_remaining(&tx->ff) < 4) break; - if (written + pkt_words > count) break; - if (tu_edpt_stream_write_available(ep_tx) < pkt_bytes) break; - - // Flush whole packets already queued before adding one that would cross - // the wMaxPacketSize boundary. Prevents an UMP message from being split - // across two USB transfers, which would corrupt the host RX context. - uint16_t ff_count = tu_fifo_count(&ep_tx->ff); - if (ff_count > 0 && ff_count + pkt_bytes > ep_tx->mps) { - tu_edpt_stream_write_xfer(ep_tx); - } - - tu_edpt_stream_write(ep_tx, &words[written], pkt_bytes); - written += pkt_words; + if (tu_fifo_write_n(&tx->ff, packets + written * 4u, 4) != 4) break; + written++; } - (void) tu_edpt_stream_write_xfer(ep_tx); - return written; -} + (void) _tx_start_xfer(p_midi); -bool tud_midi2_n_packet_write(uint8_t itf, const uint8_t packet[4]) { - TU_VERIFY(itf < CFG_TUD_MIDI2, false); - midi2d_interface_t* p_midi = &_midi2d_itf[itf]; - tu_edpt_stream_t* ep_tx = &p_midi->ep_stream.tx; - TU_VERIFY(tu_edpt_stream_is_opened(ep_tx), false); - TU_VERIFY(tu_edpt_stream_write_available(ep_tx) >= 4, false); - TU_VERIFY(tu_edpt_stream_write(ep_tx, packet, 4) > 0, false); - (void) tu_edpt_stream_write_xfer(ep_tx); - return true; + return written; } //--------------------------------------------------------------------+ @@ -439,8 +529,14 @@ void midi2d_init(void) { tu_edpt_stream_init(&p_midi->ep_stream.rx, false, false, false, p_midi->ep_stream.rx_ff_buf, CFG_TUD_MIDI2_RX_BUFSIZE, epout_buf); - tu_edpt_stream_init(&p_midi->ep_stream.tx, false, true, false, - p_midi->ep_stream.tx_ff_buf, CFG_TUD_MIDI2_TX_BUFSIZE, epin_buf); + + midi2d_tx_t* tx = &p_midi->ep_stream.tx; + (void) tu_fifo_config(&tx->ff, p_midi->ep_stream.tx_ff_buf, CFG_TUD_MIDI2_TX_BUFSIZE, false); +#if CFG_TUD_EDPT_DEDICATED_HWFIFO == 0 + tx->ep_buf = epin_buf; +#else + (void) epin_buf; +#endif } } @@ -448,7 +544,6 @@ bool midi2d_deinit(void) { for (uint8_t i = 0; i < CFG_TUD_MIDI2; i++) { midi2d_interface_t* p_midi = &_midi2d_itf[i]; tu_edpt_stream_deinit(&p_midi->ep_stream.rx); - tu_edpt_stream_deinit(&p_midi->ep_stream.tx); } return true; } @@ -462,8 +557,8 @@ void midi2d_reset(uint8_t rhport) { tu_edpt_stream_clear(&p_midi->ep_stream.rx); tu_edpt_stream_close(&p_midi->ep_stream.rx); - tu_edpt_stream_clear(&p_midi->ep_stream.tx); - tu_edpt_stream_close(&p_midi->ep_stream.tx); + tu_fifo_clear(&p_midi->ep_stream.tx.ff); + p_midi->ep_stream.tx.ep_addr = 0; } } @@ -533,8 +628,9 @@ uint16_t midi2d_open(uint8_t rhport, const tusb_desc_interface_t* desc_itf, uint const uint8_t ep_addr = desc_ep->bEndpointAddress; if (tu_edpt_dir(ep_addr) == TUSB_DIR_IN) { - tu_edpt_stream_open(&p_midi->ep_stream.tx, rhport, desc_ep, CFG_TUD_MIDI2_TX_EPSIZE); - tu_edpt_stream_clear(&p_midi->ep_stream.tx); + p_midi->ep_stream.tx.ep_addr = ep_addr; + p_midi->ep_stream.tx.mps = tu_edpt_packet_size(desc_ep); + tu_fifo_clear(&p_midi->ep_stream.tx.ff); } else { tu_edpt_stream_open(&p_midi->ep_stream.rx, rhport, desc_ep, tu_edpt_packet_size(desc_ep)); tu_edpt_stream_clear(&p_midi->ep_stream.rx); @@ -588,7 +684,7 @@ bool midi2d_control_xfer_cb(uint8_t rhport, uint8_t stage, const tusb_control_re p_midi->alt_setting = alt; tu_edpt_stream_clear(&p_midi->ep_stream.rx); - tu_edpt_stream_clear(&p_midi->ep_stream.tx); + tu_fifo_clear(&p_midi->ep_stream.tx.ff); if (alt == 1) { p_midi->negotiated = false; @@ -635,53 +731,6 @@ bool midi2d_control_xfer_cb(uint8_t rhport, uint8_t stage, const tusb_control_re } } -// Drain whole UMP packets from the TX FIFO into the EP buffer, capped at -// wMaxPacketSize. Needed when CFG_TUD_MIDI2_TX_BUFSIZE > mps: the FIFO can -// then hold more bytes than fit in a single USB transfer, and a blind -// tu_edpt_stream_write_xfer would split a UMP across two transfers. -static void midi2d_flush_tx_boundary_aware(midi2d_interface_t* p_midi, uint32_t last_xferred) { - tu_edpt_stream_t* ep_tx = &p_midi->ep_stream.tx; - const uint16_t mps = ep_tx->mps; - const uint16_t ff_count = tu_fifo_count(&ep_tx->ff); - - if (ff_count == 0) { - (void) tu_edpt_stream_write_zlp_if_needed(ep_tx, last_xferred); - return; - } - if (ff_count <= mps) { - // Whole FIFO fits in one transfer; the stream API drain is safe. - (void) tu_edpt_stream_write_xfer(ep_tx); - return; - } - if (ep_tx->ep_buf == NULL) { - // HWFIFO mode: relies on CFG_TUD_MIDI2_TX_BUFSIZE <= mps for UMP integrity. - (void) tu_edpt_stream_write_xfer(ep_tx); - return; - } - - // ff_count > mps and a local EP buffer is available: drain only whole UMP - // packets up to mps to preserve packet boundaries on the USB wire. - uint8_t word_bytes[4]; - uint8_t* buf = ep_tx->ep_buf; - uint16_t bytes = 0; - while (bytes < mps) { - if (tu_fifo_count(&ep_tx->ff) < 4) break; - if (4 != tu_fifo_peek_n(&ep_tx->ff, word_bytes, 4)) break; - uint8_t mt = (uint8_t)((word_bytes[3] >> 4) & 0x0F); - uint8_t pkt_words = midi2_ump_word_count(mt); - uint16_t pkt_bytes = (uint16_t)(pkt_words * 4); - if (tu_fifo_count(&ep_tx->ff) < pkt_bytes) break; - if (bytes + pkt_bytes > mps) break; - tu_fifo_read_n(&ep_tx->ff, buf + bytes, pkt_bytes); - bytes = (uint16_t)(bytes + pkt_bytes); - } - if (bytes == 0) return; - if (!usbd_edpt_claim(p_midi->rhport, ep_tx->ep_addr)) return; - if (!usbd_edpt_xfer(p_midi->rhport, ep_tx->ep_addr, buf, bytes, false)) { - usbd_edpt_release(p_midi->rhport, ep_tx->ep_addr); - } -} - bool midi2d_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { (void) rhport; @@ -690,7 +739,7 @@ bool midi2d_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint3 midi2d_interface_t* p_midi = &_midi2d_itf[idx]; tu_edpt_stream_t* ep_rx = &p_midi->ep_stream.rx; - tu_edpt_stream_t* ep_tx = &p_midi->ep_stream.tx; + midi2d_tx_t* ep_tx = &p_midi->ep_stream.tx; if (ep_addr == ep_rx->ep_addr) { if (result == XFER_RESULT_SUCCESS) { @@ -702,7 +751,14 @@ bool midi2d_xfer_cb(uint8_t rhport, uint8_t ep_addr, xfer_result_t result, uint3 } tu_edpt_stream_read_xfer(ep_rx); } else if (ep_addr == ep_tx->ep_addr && result == XFER_RESULT_SUCCESS) { - midi2d_flush_tx_boundary_aware(p_midi, xferred_bytes); + uint16_t queued = _tx_start_xfer(p_midi); + // Send ZLP if no more data is queued but the last transfer was exactly mps + if (queued == 0 && tu_fifo_count(&ep_tx->ff) == 0 && xferred_bytes > 0 && + (0 == (xferred_bytes & (ep_tx->mps - 1)))) { + if (usbd_edpt_claim(rhport, ep_tx->ep_addr)) { + usbd_edpt_xfer(rhport, ep_tx->ep_addr, NULL, 0, false); + } + } } else { return false; } diff --git a/src/class/midi/midi2_device.h b/src/class/midi/midi2_device.h index 6c29b8ddf..e53535693 100644 --- a/src/class/midi/midi2_device.h +++ b/src/class/midi/midi2_device.h @@ -130,8 +130,8 @@ uint8_t tud_midi2_n_protocol(uint8_t itf); 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); -bool tud_midi2_n_packet_read(uint8_t itf, uint8_t packet[4]); -bool tud_midi2_n_packet_write(uint8_t itf, const uint8_t packet[4]); +uint32_t tud_midi2_n_packet_read(uint8_t itf, uint8_t packets[], uint32_t max_packets); +uint32_t tud_midi2_n_packet_write(uint8_t itf, const uint8_t packets[], uint32_t count); //--------------------------------------------------------------------+ // Application API (Single Interface) @@ -166,12 +166,14 @@ tud_midi2_ump_write(const uint32_t* words, uint32_t count) { return tud_midi2_n_ump_write(0, words, count); } -TU_ATTR_ALWAYS_INLINE static inline bool tud_midi2_packet_read(uint8_t packet[4]) { - return tud_midi2_n_packet_read(0, packet); +TU_ATTR_ALWAYS_INLINE static inline uint32_t +tud_midi2_packet_read(uint8_t packets[], uint32_t max_packets) { + return tud_midi2_n_packet_read(0, packets, max_packets); } -TU_ATTR_ALWAYS_INLINE static inline bool tud_midi2_packet_write(const uint8_t packet[4]) { - return tud_midi2_n_packet_write(0, packet); +TU_ATTR_ALWAYS_INLINE static inline uint32_t +tud_midi2_packet_write(const uint8_t packets[], uint32_t count) { + return tud_midi2_n_packet_write(0, packets, count); } //--------------------------------------------------------------------+ -- 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(-) 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 From dad8c0ae51d68fd5355a8a0de4e478e4149ec05e Mon Sep 17 00:00:00 2001 From: Saulo Veríssimo Date: Tue, 19 May 2026 16:34:42 -0300 Subject: midi2_host: convert TX to raw FIFO mirroring midi2_device refactor --- src/class/midi/midi2_host.c | 192 ++++++++++++++++++++++++++------------------ 1 file changed, 113 insertions(+), 79 deletions(-) diff --git a/src/class/midi/midi2_host.c b/src/class/midi/midi2_host.c index 5d77e5990..6d8861f4b 100644 --- a/src/class/midi/midi2_host.c +++ b/src/class/midi/midi2_host.c @@ -62,6 +62,13 @@ TU_ATTR_WEAK void tuh_midi2_umount_cb(uint8_t idx) { // Internal structure and state //--------------------------------------------------------------------+ +typedef struct { + uint8_t ep_addr; + uint16_t mps; + tu_fifo_t ff; + uint8_t* ep_buf; +} midih2_tx_t; + typedef struct { uint8_t daddr; uint8_t bInterfaceNumber; @@ -76,7 +83,7 @@ typedef struct { uint8_t tx_cable_count_alt1; struct { - tu_edpt_stream_t tx; + midih2_tx_t tx; tu_edpt_stream_t rx; uint8_t rx_ff_buf[CFG_TUH_MIDI2_RX_BUFSIZE]; @@ -119,6 +126,86 @@ static inline uint8_t get_idx_by_ep_addr(uint8_t daddr, uint8_t ep_addr) { return TUSB_INDEX_INVALID_8; } +static inline bool _tuh_tx_opened(const midih2_interface_t* p_midi) { + return p_midi->ep_stream.tx.ep_addr != 0; +} + +static uint8_t _tuh_tx_byte_at(const tu_fifo_buffer_info_t* info, uint16_t offset) { + if (offset < info->linear.len) { + return info->linear.ptr[offset]; + } + offset = (uint16_t)(offset - info->linear.len); + if (offset < info->wrapped.len) { + return info->wrapped.ptr[offset]; + } + return 0; +} + +// Largest byte count containing only whole UMP packets and fitting one xfer. +static uint16_t _tuh_tx_nonseg_len_to_mps(midih2_tx_t* tx) { + tu_fifo_buffer_info_t info; + tu_fifo_get_read_info(&tx->ff, &info); + + const uint16_t available = (uint16_t)(info.linear.len + info.wrapped.len); + uint16_t bytes = 0; + + while (bytes < tx->mps) { + if ((uint16_t)(available - bytes) < 4) break; + + uint8_t mt = (uint8_t)((_tuh_tx_byte_at(&info, (uint16_t)(bytes + 3)) >> 4) & 0x0F); + uint8_t pkt_words = midi2_ump_word_count(mt); + uint16_t pkt_bytes = (uint16_t)(pkt_words * 4); + + if (pkt_bytes == 0) break; + if ((uint16_t)(available - bytes) < pkt_bytes) break; + if ((uint16_t)(bytes + pkt_bytes) > tx->mps) break; + + bytes = (uint16_t)(bytes + pkt_bytes); + } + + return bytes; +} + +// Start one OUT transfer capped at mps. Returns bytes queued, or 0 if nothing. +static uint16_t _tuh_tx_start_xfer(midih2_interface_t* p_midi) { + midih2_tx_t* tx = &p_midi->ep_stream.tx; + uint16_t ff_count = tu_fifo_count(&tx->ff); + if (ff_count == 0) return 0; + if (!usbh_edpt_claim(p_midi->daddr, tx->ep_addr)) return 0; + + uint16_t bytes; + if (p_midi->alt_setting_current == 1) { + bytes = _tuh_tx_nonseg_len_to_mps(tx); + } else { + bytes = tu_min16(tu_fifo_count(&tx->ff), tx->mps); + } + if (bytes == 0) { + usbh_edpt_release(p_midi->daddr, tx->ep_addr); + return 0; + } + + tu_fifo_read_n(&tx->ff, tx->ep_buf, bytes); + TU_ASSERT(usbh_edpt_xfer(p_midi->daddr, tx->ep_addr, tx->ep_buf, bytes), 0); + return bytes; +} + +static uint32_t _tuh_tx_ump_write(midih2_interface_t* p_midi, const uint32_t* words, uint32_t count) { + uint32_t written = 0; + while (written < count) { + uint8_t mt = (uint8_t)((words[written] >> 28) & 0x0F); + uint8_t pkt_words = midi2_ump_word_count(mt); + uint16_t pkt_bytes = (uint16_t)(pkt_words * 4); + + if (written + pkt_words > count) break; + if (tu_fifo_remaining(&p_midi->ep_stream.tx.ff) < pkt_bytes) break; + if (tu_fifo_write_n(&p_midi->ep_stream.tx.ff, &words[written], pkt_bytes) != pkt_bytes) break; + written += pkt_words; + } + + (void) _tuh_tx_start_xfer(p_midi); + return written; +} + //--------------------------------------------------------------------+ // Descriptor parsing //--------------------------------------------------------------------+ @@ -151,8 +238,9 @@ static const uint8_t* midih2_parse_descriptors_alt0(midih2_interface_t *p_midi, tu_edpt_stream_open(&p_midi->ep_stream.rx, p_midi->daddr, p_ep, tu_edpt_packet_size(p_ep)); tu_edpt_stream_clear(&p_midi->ep_stream.rx); } else { - tu_edpt_stream_open(&p_midi->ep_stream.tx, p_midi->daddr, p_ep, tu_edpt_packet_size(p_ep)); - tu_edpt_stream_clear(&p_midi->ep_stream.tx); + p_midi->ep_stream.tx.ep_addr = p_ep->bEndpointAddress; + p_midi->ep_stream.tx.mps = tu_edpt_packet_size(p_ep); + tu_fifo_clear(&p_midi->ep_stream.tx.ff); } p_desc = tu_desc_next(p_desc); @@ -264,13 +352,14 @@ bool midih2_init(void) { for (int inst = 0; inst < CFG_TUH_MIDI2; inst++) { midih2_interface_t *p_midi = &_midi2_host[inst]; - uint8_t* rx_buf = _midi2_epbuf[inst].rx; - uint8_t* tx_buf = _midi2_epbuf[inst].tx; - tu_edpt_stream_init(&p_midi->ep_stream.rx, true, false, false, - p_midi->ep_stream.rx_ff_buf, CFG_TUH_MIDI2_RX_BUFSIZE, rx_buf); - tu_edpt_stream_init(&p_midi->ep_stream.tx, true, true, false, - p_midi->ep_stream.tx_ff_buf, CFG_TUH_MIDI2_TX_BUFSIZE, tx_buf); + p_midi->ep_stream.rx_ff_buf, CFG_TUH_MIDI2_RX_BUFSIZE, _midi2_epbuf[inst].rx); + + // TX uses raw tu_fifo + direct usbh_edpt_xfer (no FIFO wrapper) to preserve + // UMP packet boundaries across USB transfers. + midih2_tx_t* tx = &p_midi->ep_stream.tx; + (void) tu_fifo_config(&tx->ff, p_midi->ep_stream.tx_ff_buf, CFG_TUH_MIDI2_TX_BUFSIZE, false); + tx->ep_buf = _midi2_epbuf[inst].tx; } return true; } @@ -279,7 +368,6 @@ bool midih2_deinit(void) { for (size_t i = 0; i < CFG_TUH_MIDI2; i++) { midih2_interface_t* p_midi = &_midi2_host[i]; tu_edpt_stream_deinit(&p_midi->ep_stream.rx); - tu_edpt_stream_deinit(&p_midi->ep_stream.tx); } return true; } @@ -448,56 +536,20 @@ void midih2_close(uint8_t dev_addr) { if (p_midi->daddr == dev_addr) { TU_LOG_DRV(" MIDI2 close addr = %u index = %u\r\n", dev_addr, idx); tu_edpt_stream_close(&p_midi->ep_stream.rx); - tu_edpt_stream_close(&p_midi->ep_stream.tx); + tu_fifo_clear(&p_midi->ep_stream.tx.ff); + p_midi->ep_stream.tx.ep_addr = 0; tuh_midi2_umount_cb(idx); tu_memclr(p_midi, sizeof(midih2_interface_t)); } } } -// Drain whole UMP packets from the TX FIFO into the EP buffer, capped at -// wMaxPacketSize. Needed when CFG_TUH_MIDI2_TX_BUFSIZE > mps to keep UMP -// packets from crossing USB transfer boundaries. -static void midih2_flush_tx_boundary_aware(midih2_interface_t* p_midi, uint32_t last_xferred) { - tu_edpt_stream_t* ep_tx = &p_midi->ep_stream.tx; - const uint16_t mps = ep_tx->mps; - const uint16_t ff_count = tu_fifo_count(&ep_tx->ff); - - if (ff_count == 0) { - (void) tu_edpt_stream_write_zlp_if_needed(ep_tx, last_xferred); - return; - } - if (ff_count <= mps || ep_tx->ep_buf == NULL) { - (void) tu_edpt_stream_write_xfer(ep_tx); - return; - } - - uint8_t word_bytes[4]; - uint8_t* buf = ep_tx->ep_buf; - uint16_t bytes = 0; - while (bytes < mps) { - if (tu_fifo_count(&ep_tx->ff) < 4) break; - if (4 != tu_fifo_peek_n(&ep_tx->ff, word_bytes, 4)) break; - uint8_t mt = (uint8_t)((word_bytes[3] >> 4) & 0x0F); - uint8_t pkt_words = midi2_ump_word_count(mt); - uint16_t pkt_bytes = (uint16_t)(pkt_words * 4); - if (tu_fifo_count(&ep_tx->ff) < pkt_bytes) break; - if (bytes + pkt_bytes > mps) break; - tu_fifo_read_n(&ep_tx->ff, buf + bytes, pkt_bytes); - bytes = (uint16_t)(bytes + pkt_bytes); - } - if (bytes == 0) return; - if (!usbh_edpt_claim(p_midi->daddr, ep_tx->ep_addr)) return; - if (!usbh_edpt_xfer(p_midi->daddr, ep_tx->ep_addr, buf, bytes)) { - usbh_edpt_release(p_midi->daddr, ep_tx->ep_addr); - } -} - bool midih2_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) { uint8_t idx = get_idx_by_ep_addr(dev_addr, ep_addr); TU_VERIFY(idx < CFG_TUH_MIDI2); midih2_interface_t *p_midi = &_midi2_host[idx]; + midih2_tx_t* ep_tx = &p_midi->ep_stream.tx; if (ep_addr == p_midi->ep_stream.rx.ep_addr) { if (result == XFER_RESULT_SUCCESS && xferred_bytes > 0) { @@ -505,10 +557,17 @@ bool midih2_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uin tuh_midi2_rx_cb(idx, xferred_bytes); } tu_edpt_stream_read_xfer(&p_midi->ep_stream.rx); - } else if (ep_addr == p_midi->ep_stream.tx.ep_addr) { + } else if (ep_addr == ep_tx->ep_addr) { tuh_midi2_tx_cb(idx, xferred_bytes); if (result == XFER_RESULT_SUCCESS) { - midih2_flush_tx_boundary_aware(p_midi, xferred_bytes); + uint16_t queued = _tuh_tx_start_xfer(p_midi); + // Send ZLP if no more data is queued but the last transfer was exactly mps + if (queued == 0 && tu_fifo_count(&ep_tx->ff) == 0 && xferred_bytes > 0 && + (0 == (xferred_bytes & (ep_tx->mps - 1)))) { + if (usbh_edpt_claim(dev_addr, ep_tx->ep_addr)) { + usbh_edpt_xfer(dev_addr, ep_tx->ep_addr, NULL, 0); + } + } } } @@ -563,39 +622,14 @@ uint32_t tuh_midi2_ump_write(uint8_t idx, const uint32_t* words, uint32_t count) TU_VERIFY(idx < CFG_TUH_MIDI2 && words && count); midih2_interface_t *p_midi = &_midi2_host[idx]; - tu_edpt_stream_t *ep_tx = &p_midi->ep_stream.tx; + TU_VERIFY(_tuh_tx_opened(p_midi), 0); - uint32_t written = 0; - while (written < count) { - uint8_t mt = (uint8_t)((words[written] >> 28) & 0x0F); - uint8_t pkt_words = midi2_ump_word_count(mt); - uint32_t pkt_bytes = (uint32_t)pkt_words * 4; - - if (written + pkt_words > count) break; - if (tu_edpt_stream_write_available(ep_tx) < pkt_bytes) break; - - // Flush whole packets already queued before adding one that would cross - // the wMaxPacketSize boundary. Prevents an UMP message from being split - // across two USB transfers, which would corrupt the peer RX context. - uint16_t ff_count = tu_fifo_count(&ep_tx->ff); - if (ff_count > 0 && ff_count + pkt_bytes > ep_tx->mps) { - tu_edpt_stream_write_xfer(ep_tx); - } - - tu_edpt_stream_write(ep_tx, (const uint8_t *) &words[written], pkt_bytes); - written += pkt_words; - } - - return written; + return _tuh_tx_ump_write(p_midi, words, count); } uint32_t tuh_midi2_write_flush(uint8_t idx) { TU_VERIFY(idx < CFG_TUH_MIDI2); - - midih2_interface_t *p_midi = &_midi2_host[idx]; - tu_edpt_stream_t *ep_tx = &p_midi->ep_stream.tx; - - return tu_edpt_stream_write_xfer(ep_tx); + return _tuh_tx_start_xfer(&_midi2_host[idx]); } #endif // CFG_TUH_ENABLED && CFG_TUH_MIDI2 -- cgit v1.3.1