diff --git a/Middlewares/Patched/ST/STM32_USB_Device_Library/Class/CDC/Inc/usbd_cdc.h b/Middlewares/Patched/ST/STM32_USB_Device_Library/Class/CDC/Inc/usbd_cdc.h index d1b8efc18..53d801efb 100644 --- a/Middlewares/Patched/ST/STM32_USB_Device_Library/Class/CDC/Inc/usbd_cdc.h +++ b/Middlewares/Patched/ST/STM32_USB_Device_Library/Class/CDC/Inc/usbd_cdc.h @@ -65,6 +65,8 @@ extern "C" { #define USB_CDC_CONFIG_DESC_SIZ 67U #define USB_MIDI_CONFIG_DESC_SIZ 101U +#define USB_MIDI2_CONFIG_DESC_SIZ 141U +#define USB_MIDI2_GTB_DESC_SIZ 18U #define CDC_DATA_HS_IN_PACKET_SIZE CDC_DATA_HS_MAX_PACKET_SIZE #define CDC_DATA_HS_OUT_PACKET_SIZE CDC_DATA_HS_MAX_PACKET_SIZE @@ -147,9 +149,11 @@ extern USBD_ClassTypeDef USBD_CDC; #define USBD_CDC_CLASS &USBD_CDC // these are used to hack in an optional MIDI mode -#define USBD_MODE_CDC 0 -#define USBD_MODE_MIDI 1 +#define USBD_MODE_CDC 0 +#define USBD_MODE_MIDI 1 +#define USBD_MODE_MIDI2 2 extern uint8_t usbd_mode; +extern uint8_t usbd_midi2_alt_setting; /** * @} diff --git a/Middlewares/Patched/ST/STM32_USB_Device_Library/Class/CDC/Src/usbd_cdc.c b/Middlewares/Patched/ST/STM32_USB_Device_Library/Class/CDC/Src/usbd_cdc.c index 900d2ffa7..5300cb791 100644 --- a/Middlewares/Patched/ST/STM32_USB_Device_Library/Class/CDC/Src/usbd_cdc.c +++ b/Middlewares/Patched/ST/STM32_USB_Device_Library/Class/CDC/Src/usbd_cdc.c @@ -196,6 +196,74 @@ __ALIGN_BEGIN uint8_t USBD_MIDI_CfgDesc[USB_MIDI_CONFIG_DESC_SIZ] __ALIGN_END = 0x05, 0x25, 0x01, 0x01, 0x03 }; +// USB MIDI 2.0 Configuration Descriptor: Alt 0 (MIDI 1.0) + Alt 1 (UMP) +__ALIGN_BEGIN uint8_t USBD_MIDI2_CfgDesc[USB_MIDI2_CONFIG_DESC_SIZ] __ALIGN_END = +{ + // Configuration Descriptor (9) + 0x09, 0x02, + LOBYTE(USB_MIDI2_CONFIG_DESC_SIZ), HIBYTE(USB_MIDI2_CONFIG_DESC_SIZ), + 0x02, 0x01, 0x00, 0xC0, 0x50, + + // Audio Control Interface (9) + 0x09, 0x04, 0x00, 0x00, 0x00, 0x01, 0x01, 0x00, 0x00, + // CS AC Interface Header (9) + 0x09, 0x24, 0x01, 0x00, 0x01, 0x09, 0x00, 0x01, 0x01, + + // ==== Alt 0: MIDIStreaming (MIDI 1.0 compat) ==== + // Standard MS Interface (9) -- bAlternateSetting=0 + 0x09, 0x04, 0x01, 0x00, 0x02, 0x01, 0x03, 0x00, 0x00, + // CS MS Header (7) -- bcdMSC=0x0100 + 0x07, 0x24, 0x01, 0x00, 0x01, 0x41, 0x00, + // MIDI IN Jack Embedded (6) + 0x06, 0x24, 0x02, 0x01, 0x01, 0x00, + // MIDI IN Jack External (6) + 0x06, 0x24, 0x02, 0x02, 0x02, 0x00, + // MIDI OUT Jack Embedded (9) + 0x09, 0x24, 0x03, 0x01, 0x03, 0x01, 0x02, 0x01, 0x00, + // MIDI OUT Jack External (9) + 0x09, 0x24, 0x03, 0x02, 0x06, 0x01, 0x01, 0x01, 0x00, + // Bulk OUT Endpoint (9) + 0x09, 0x05, CDC_OUT_EP, 0x02, 0x40, 0x00, 0x00, 0x00, 0x00, + // CS Endpoint General (5) + 0x05, 0x25, 0x01, 0x01, 0x01, + // Bulk IN Endpoint (9) + 0x09, 0x05, CDC_IN_EP, 0x02, 0x40, 0x00, 0x00, 0x00, 0x00, + // CS Endpoint General (5) + 0x05, 0x25, 0x01, 0x01, 0x03, + + // ==== Alt 1: MIDIStreaming (MIDI 2.0 UMP) ==== + // Standard MS Interface (9) -- bAlternateSetting=1 + 0x09, 0x04, 0x01, 0x01, 0x02, 0x01, 0x03, 0x00, 0x00, + // CS MS Header (7) -- bcdMSC=0x0200 + 0x07, 0x24, 0x01, 0x00, 0x02, 0x07, 0x00, + // Bulk OUT Endpoint (7) -- 7 bytes on Alt 1 (USB MIDI 2.0 Table B-17) + 0x07, 0x05, CDC_OUT_EP, 0x02, 0x40, 0x00, 0x00, + // CS Endpoint General 2.0 (5) -- subtype=0x02, 1 GTB, ID=1 + 0x05, 0x25, 0x02, 0x01, 0x01, + // Bulk IN Endpoint (7) + 0x07, 0x05, CDC_IN_EP, 0x02, 0x40, 0x00, 0x00, + // CS Endpoint General 2.0 (5) -- subtype=0x02, 1 GTB, ID=1 + 0x05, 0x25, 0x02, 0x01, 0x01, +}; + +// Group Terminal Block Descriptor -- served via GET_DESCRIPTOR(0x26) +__ALIGN_BEGIN uint8_t USBD_MIDI2_GTB_Desc[USB_MIDI2_GTB_DESC_SIZ] __ALIGN_END = +{ + // GTB Header (5) + 0x05, 0x26, 0x01, + LOBYTE(USB_MIDI2_GTB_DESC_SIZ), HIBYTE(USB_MIDI2_GTB_DESC_SIZ), + // GTB Entry (13) -- 1 group, bidirectional, native MIDI 2.0 + 0x0D, 0x26, 0x02, + 0x01, // bGrpTrmBlkID + 0x00, // bGrpTrmBlkType: bidirectional + 0x00, // nGroupTrm: group 0 + 0x01, // nNumGroupTrm: 1 + 0x00, // iBlockItem: no string + 0x11, // bMIDIProtocol: MIDI_2_0 (no MIDI-CI negotiation) + 0x00, 0x00, // wMaxInputBandwidth: unknown + 0x00, 0x00, // wMaxOutputBandwidth: unknown +}; + /* USB CDC device Configuration Descriptor */ __ALIGN_BEGIN uint8_t USBD_CDC_CfgHSDesc[USB_CDC_CONFIG_DESC_SIZ] __ALIGN_END = { @@ -700,6 +768,10 @@ static uint8_t USBD_CDC_Setup(USBD_HandleTypeDef *pdev, case USB_REQ_GET_INTERFACE: if (pdev->dev_state == USBD_STATE_CONFIGURED) { + if (usbd_mode == USBD_MODE_MIDI2) + { + ifalt = usbd_midi2_alt_setting; + } (void)USBD_CtlSendData(pdev, &ifalt, 1U); } else @@ -710,13 +782,36 @@ static uint8_t USBD_CDC_Setup(USBD_HandleTypeDef *pdev, break; case USB_REQ_SET_INTERFACE: - if (pdev->dev_state != USBD_STATE_CONFIGURED) + if (pdev->dev_state == USBD_STATE_CONFIGURED) + { + if (usbd_mode == USBD_MODE_MIDI2) + { + usbd_midi2_alt_setting = (uint8_t)LOBYTE(req->wValue); + /* Re-arm OUT endpoint for new packet format */ + (void)USBD_LL_PrepareReceive(pdev, CDC_OUT_EP, + hcdc->RxBuffer, CDC_DATA_FS_OUT_PACKET_SIZE); + } + } + else { USBD_CtlError(pdev, req); ret = USBD_FAIL; } break; + case USB_REQ_GET_DESCRIPTOR: + if (usbd_mode == USBD_MODE_MIDI2 + && ((req->wValue >> 8) == 0x26U)) + { + uint16_t gtb_len = sizeof(USBD_MIDI2_GTB_Desc); + if (gtb_len > req->wLength) + { + gtb_len = req->wLength; + } + (void)USBD_CtlSendData(pdev, USBD_MIDI2_GTB_Desc, gtb_len); + } + break; + case USB_REQ_CLEAR_FEATURE: break; @@ -851,6 +946,11 @@ static uint8_t *USBD_CDC_GetFSCfgDesc(uint16_t *length) *length = sizeof (USBD_MIDI_CfgDesc); return USBD_MIDI_CfgDesc; } + case USBD_MODE_MIDI2: + { + *length = sizeof (USBD_MIDI2_CfgDesc); + return USBD_MIDI2_CfgDesc; + } } return USBD_CDC_CfgFSDesc; @@ -877,6 +977,11 @@ static uint8_t *USBD_CDC_GetHSCfgDesc(uint16_t *length) *length = sizeof (USBD_MIDI_CfgDesc); return USBD_MIDI_CfgDesc; } + case USBD_MODE_MIDI2: + { + *length = sizeof (USBD_MIDI2_CfgDesc); + return USBD_MIDI2_CfgDesc; + } } return USBD_CDC_CfgHSDesc; @@ -903,6 +1008,11 @@ static uint8_t *USBD_CDC_GetOtherSpeedCfgDesc(uint16_t *length) *length = sizeof (USBD_MIDI_CfgDesc); return USBD_MIDI_CfgDesc; } + case USBD_MODE_MIDI2: + { + *length = sizeof (USBD_MIDI2_CfgDesc); + return USBD_MIDI2_CfgDesc; + } } return USBD_CDC_OtherSpeedCfgDesc; diff --git a/Middlewares/ST/STM32_USB_Host_Library/Class/MIDI/Inc/usbh_midi.h b/Middlewares/ST/STM32_USB_Host_Library/Class/MIDI/Inc/usbh_midi.h index 22d7d0d10..d74663936 100644 --- a/Middlewares/ST/STM32_USB_Host_Library/Class/MIDI/Inc/usbh_midi.h +++ b/Middlewares/ST/STM32_USB_Host_Library/Class/MIDI/Inc/usbh_midi.h @@ -47,6 +47,7 @@ typedef enum { typedef void (*USBH_MIDI_RxCallback)(uint8_t* buff, size_t len, void* pUser); #define USBH_MIDI_RX_BUF_SIZE 64 +#define USBH_MIDI_TX_BUF_SIZE 64 /* Structure for MIDI process */ typedef struct _MIDI_Process { @@ -61,6 +62,15 @@ typedef struct _MIDI_Process { USBH_MIDI_RxCallback callback; void* pUser; uint8_t rxBuffer[USBH_MIDI_RX_BUF_SIZE]; + /* Received bytes are copied here and the RX pipe re-armed before the + class callback runs, so the pipe is never idle while a packet is + processed (the device would otherwise NAK and drop under load). */ + uint8_t procBuffer[USBH_MIDI_RX_BUF_SIZE]; + /* DMA-coherent Tx staging. The handle is transferred by DMA with no + cache maintenance, so Tx data (which may come from cached memory) + is copied here before submitting, as rxBuffer is for the receive. */ + uint8_t txBuffer[USBH_MIDI_TX_BUF_SIZE]; + uint8_t altSetting; /* 0 = MIDI 1.0, 1 = MIDI 2.0 UMP */ } MIDI_HandleTypeDef; /* MIDI Class Codes */ @@ -71,6 +81,7 @@ extern USBH_ClassTypeDef USBH_midi; #define USBH_MIDI_CLASS &USBH_midi uint8_t USBH_MIDI_IsReady(USBH_HandleTypeDef *phost); +uint8_t USBH_MIDI_GetAltSetting(USBH_HandleTypeDef *phost); MIDI_ErrorTypeDef USBH_MIDI_Transmit(USBH_HandleTypeDef *phost, uint8_t* data, size_t len); diff --git a/Middlewares/ST/STM32_USB_Host_Library/Class/MIDI/Src/usbh_midi.c b/Middlewares/ST/STM32_USB_Host_Library/Class/MIDI/Src/usbh_midi.c index e678fc5ab..f68dde591 100644 --- a/Middlewares/ST/STM32_USB_Host_Library/Class/MIDI/Src/usbh_midi.c +++ b/Middlewares/ST/STM32_USB_Host_Library/Class/MIDI/Src/usbh_midi.c @@ -54,6 +54,7 @@ static USBH_StatusTypeDef USBH_MIDI_InterfaceInit(USBH_HandleTypeDef *phost) MIDI_Handle = (MIDI_HandleTypeDef*)phost->pActiveClass->pData; USBH_memset(MIDI_Handle, 0, sizeof(MIDI_HandleTypeDef)); + /* Find MIDI Streaming interface (Alt 0 first) */ uint8_t interface = USBH_FindInterface(phost, phost->pActiveClass->ClassCode, USB_MIDI_STREAMING_SUBCLASS, 0xFFU); @@ -61,11 +62,53 @@ static USBH_StatusTypeDef USBH_MIDI_InterfaceInit(USBH_HandleTypeDef *phost) USBH_DbgLog("Cannot find interface for %s class.", phost->pActiveClass->Name); return USBH_FAIL; } - status = USBH_SelectInterface(phost, interface); + + /* Check if device has Alt 1 (MIDI 2.0 UMP). USBH_SelectInterface + * bounds-checks against CfgDesc.bNumInterfaces, which counts unique + * interface numbers (NOT entries in Itf_Desc[]), so the Alt 1 index + * would always fail that check. Pass the Alt 0 index to satisfy the + * bounds check; the Alt 1 index drives the endpoint scan + SET_INTERFACE + * below. */ + uint8_t alt0_idx = interface; + uint8_t ifnum = phost->device.CfgDesc.Itf_Desc[interface].bInterfaceNumber; + uint8_t alt1_idx = USBH_FindInterfaceIndex(phost, ifnum, 1U); + uint8_t use_alt = 0U; + + if (alt1_idx != 0xFFU) { + /* Device supports MIDI 2.0 -- the endpoint scan will use Alt 1 */ + interface = alt1_idx; + use_alt = 1U; + USBH_UsrLog("MIDI 2.0 Alt 1 found, selecting UMP mode"); + } + + status = USBH_SelectInterface(phost, alt0_idx); if (status != USBH_OK) { return USBH_FAIL; } + /* Send SET_INTERFACE if using Alt 1. USBH_SetInterface is async via + * USBH_CtlReq; calling it once just queues the transfer. We must + * drive it to completion BEFORE opening the IN/OUT pipes below, + * otherwise the host opens Alt 1 pipes while the device is still + * on Alt 0 and the device sends nothing on those pipes. */ + if (use_alt == 1U) { + USBH_StatusTypeDef setif_status; + uint32_t setif_start = HAL_GetTick(); + do { + setif_status = USBH_SetInterface(phost, ifnum, 1U); + USBH_Delay(1U); + if ((HAL_GetTick() - setif_start) > 100U) { + USBH_ErrLog("SET_INTERFACE Alt 1 timeout"); + break; + } + } while (setif_status == USBH_BUSY); + if (setif_status != USBH_OK) { + USBH_ErrLog("SET_INTERFACE Alt 1 failed"); + } + } + + MIDI_Handle->altSetting = use_alt; + /* Find the endpoints */ for (int ep = 0; ep < phost->device.CfgDesc.Itf_Desc[interface].bNumEndpoints; ++ep) { if (phost->device.CfgDesc.Itf_Desc[interface].Ep_Desc[ep].bEndpointAddress & EP_IN) { @@ -174,11 +217,23 @@ static USBH_StatusTypeDef USBH_MIDI_Process(USBH_HandleTypeDef *phost) hMidi->state = MIDI_RX_POLL; } else if (rxStatus == USBH_URB_DONE) { size_t sz = USBH_LL_GetLastXferSize(phost, hMidi->InPipe); - hMidi->state = MIDI_RX; - if (hMidi->callback) { - hMidi->callback(hMidi->rxBuffer, sz, hMidi->pUser); + if (sz > USBH_MIDI_RX_BUF_SIZE) { + sz = USBH_MIDI_RX_BUF_SIZE; + } + /* Copy out and re-arm before the callback so the pipe is + never idle while a packet is processed (the device would + otherwise NAK and drop under load). */ + if (sz > 0U) { + USBH_memcpy(hMidi->procBuffer, hMidi->rxBuffer, sz); + } + USBH_BulkReceiveData(phost, hMidi->rxBuffer, hMidi->InEpSize, + hMidi->InPipe); + hMidi->state = MIDI_RX_POLL; + if (sz > 0U && hMidi->callback) { + hMidi->callback(hMidi->procBuffer, sz, hMidi->pUser); } } else { + USBH_ErrLog("MIDI URB unexpected state=%u", (unsigned)rxStatus); hMidi->state = MIDI_RX_ERROR; error = USBH_FAIL; } @@ -211,6 +266,15 @@ static USBH_StatusTypeDef USBH_MIDI_SOFProcess(USBH_HandleTypeDef *phost) return USBH_OK; } +uint8_t USBH_MIDI_GetAltSetting(USBH_HandleTypeDef *phost) +{ + MIDI_HandleTypeDef *hMidi = (MIDI_HandleTypeDef*)phost->pActiveClass->pData; + if (hMidi) { + return hMidi->altSetting; + } + return 0U; +} + void USBH_MIDI_SetReceiveCallback(USBH_HandleTypeDef *phost, USBH_MIDI_RxCallback cb, void* pUser) { USBH_UsrLog(__FUNCTION__); @@ -222,28 +286,41 @@ void USBH_MIDI_SetReceiveCallback(USBH_HandleTypeDef *phost, USBH_MIDI_RxCallbac MIDI_ErrorTypeDef USBH_MIDI_Transmit(USBH_HandleTypeDef *phost, uint8_t* data, size_t len) { MIDI_HandleTypeDef *hMidi = (MIDI_HandleTypeDef*)phost->pActiveClass->pData; - int numUrbs = 0; - // This only blocks if data won't fit into one URB while(len) { - USBH_URBStateTypeDef txStatus = USBH_LL_GetURBState(phost, hMidi->OutPipe); - while(txStatus != USBH_URB_IDLE && txStatus != USBH_URB_DONE) + size_t cap = (hMidi->OutEpSize < USBH_MIDI_TX_BUF_SIZE) + ? hMidi->OutEpSize : USBH_MIDI_TX_BUF_SIZE; + size_t sz = (len <= cap) ? len : cap; + + // Stage into the DMA-coherent handle buffer: the caller's buffer + // may be cached (AXI SRAM) and the OTG DMA does no cache + // maintenance, so it must read from the DMA section. + USBH_memcpy(hMidi->txBuffer, data, sz); + USBH_BulkSendData(phost, hMidi->txBuffer, (uint16_t)sz, hMidi->OutPipe, 1); + + // Drive the OUT URB to completion: poll the URB state and resubmit + // on NOTREADY (device NAK). Without this the URB is submitted and + // abandoned, so a NAK is never retried and data never arrives. + uint32_t start = HAL_GetTick(); + for(;;) { - if(txStatus == USBH_URB_ERROR || txStatus == USBH_URB_STALL) + USBH_URBStateTypeDef st + = USBH_LL_GetURBState(phost, hMidi->OutPipe); + if(st == USBH_URB_DONE) + break; + if(st == USBH_URB_ERROR || st == USBH_URB_STALL) { USBH_ClrFeature(phost, hMidi->OutEp); return MIDI_ERROR; } - if(numUrbs == 0) + if(st == USBH_URB_NOTREADY) + USBH_BulkSendData(phost, hMidi->txBuffer, (uint16_t)sz, hMidi->OutPipe, 1); + if((HAL_GetTick() - start) > 50U) return MIDI_BUSY; - - // Give previous URB time to complete - USBH_Delay(2); + USBH_Delay(1); } - size_t sz = (len <= hMidi->OutEpSize) ? len : hMidi->OutEpSize; - USBH_BulkSendData(phost, data, sz, hMidi->OutPipe, 1); + data += sz; len -= sz; - ++numUrbs; } return MIDI_OK; } diff --git a/examples/MIDI2_Host/CMakeLists.txt b/examples/MIDI2_Host/CMakeLists.txt new file mode 100644 index 000000000..6e4f4370e --- /dev/null +++ b/examples/MIDI2_Host/CMakeLists.txt @@ -0,0 +1,3 @@ +set(FIRMWARE_NAME MIDI2_Host) +set(FIRMWARE_SOURCES MIDI2_Host.cpp) +include(DaisyProject) diff --git a/examples/MIDI2_Host/MIDI2_Host.cpp b/examples/MIDI2_Host/MIDI2_Host.cpp new file mode 100644 index 000000000..5de2f83d1 --- /dev/null +++ b/examples/MIDI2_Host/MIDI2_Host.cpp @@ -0,0 +1,333 @@ +/** + * @author sauloverissimo + * @brief USB MIDI 2.0 host stress validator (pairs with MIDI2_USB_Device) + * @date 2026-06-10 + * + * USB MIDI 2.0 Host: stress validator for the paired MIDI2_USB_Device emitter + * + * Daisy Seed enumerates the device plugged into its USB-A jack as a + * USB MIDI 2.0 host, selects Alt 1 (UMP), and validates the incoming + * stream against the emitter contract: + * + * per burst the device emits 4001 UMP packets: + * 2000 NoteOn (Group 0 Ch 0, note = 48 + i%24, + * velocity high-16 = seq, seq runs 0..1999) + * 2000 NoteOff (same note, velocity 0) + * 1 Marker (CC 119 Group 0 Ch 0, value = burst_id) + * 400 ms quiet between bursts. + * + * Reporting model: every received UMP word is counted in cumulative + * monotonic counters inside the RX callback. The main loop reads the + * counters and prints a one-line HEARTBEAT every second, plus a final + * RESULT line when the stream goes idle (emitter unplugged or paused). + * The host usually attaches mid-burst, so the verdict measures the + * span BETWEEN the first and last marker. Pass criteria: + * + * received between markers == (last_id - first_id) * 4001 + * gaps == 0 + * unexpected == 0 + * + * Counters are 32-bit monotonics incremented in the RX callback, so no + * data can ever be lost to a reporting race; the main loop only reads. + */ +#include "daisy_seed.h" +#include "hid/usb_host.h" +#include "hid/usb_midi.h" +#include "usbh_midi.h" + +using namespace daisy; + +/** Global hardware access */ +DaisySeed hw; +USBHostHandle usbHost; +MidiUsbTransport midi; + +/** UMP constants (M2-104-UM, matches the MIDI2_USB_Device emitter) */ +static constexpr uint32_t kMT4Mask = 0xF0000000U; +static constexpr uint32_t kMT4Marker = 0x40000000U; +static constexpr uint32_t kStatusMask = 0x00F00000U; +static constexpr uint32_t kStatusNoteOn = 0x00900000U; +static constexpr uint32_t kStatusNoteOff = 0x00800000U; +static constexpr uint32_t kStatusCC = 0x00B00000U; +static constexpr uint8_t kMarkerCC = 119U; +static constexpr uint16_t kExpectedPerBurst = 4001U; +static constexpr uint32_t kHeartbeatMs = 1000U; +static constexpr uint32_t kIdleMs = 1500U; + +/** Cumulative counters. Every increment happens in the RX callback; + * the main loop only reads. Volatile because read/write cross the + * callback / main boundary; on Cortex-M7 32-bit aligned word loads + * are atomic so torn reads of individual fields are not a concern. + */ +volatile uint32_t g_total_messages = 0; +volatile uint32_t g_note_on = 0; +volatile uint32_t g_note_off = 0; +volatile uint32_t g_marker_count = 0; +volatile uint32_t g_unexpected = 0; +volatile uint32_t g_gaps = 0; +volatile uint32_t g_first_burst_id = 0; +volatile uint32_t g_last_burst_id = 0; +volatile bool g_first_marker_seen = false; +volatile uint32_t g_last_rx_ms = 0; + +/** Totals latched at marker boundaries. The host usually attaches in + * the middle of a burst, so the verdict measures the span BETWEEN the + * first and last marker: that window must hold an exact multiple of + * the burst size. + */ +volatile uint32_t g_total_at_first_marker = 0; +volatile uint32_t g_total_at_last_marker = 0; + +/** Sequence tracking for NoteOn contiguity. Lives across the whole + * run; the emitter's seq rolls 0..1999 within each burst so a delta + * of +1 is expected inside a burst and a roll-back to 0 is expected + * at the burst boundary (right after the marker). + */ +static uint16_t g_last_seq = 0; +static bool g_last_seq_valid = false; + +static inline void HandleWord(uint32_t w0, uint32_t w1) +{ + g_total_messages++; + if((w0 & kMT4Mask) != kMT4Marker) + { + g_unexpected++; + return; + } + const uint32_t status = w0 & kStatusMask; + if(status == kStatusNoteOn) + { + g_note_on++; + const uint16_t seq = static_cast(w1 >> 16); + if(g_last_seq_valid) + { + const uint16_t expected = g_last_seq + 1U; + if(seq != expected) + g_gaps++; + } + g_last_seq = seq; + g_last_seq_valid = true; + } + else if(status == kStatusNoteOff) + { + g_note_off++; + } + else if(status == kStatusCC && ((w0 >> 8) & 0xFFU) == kMarkerCC) + { + g_marker_count++; + const uint32_t burst_id = w1 & 0xFFFFU; + if(!g_first_marker_seen) + { + g_first_burst_id = burst_id; + g_first_marker_seen = true; + g_total_at_first_marker = g_total_messages; + } + g_last_burst_id = burst_id; + g_total_at_last_marker = g_total_messages; + /** Sequence resets at the burst boundary; arm for next burst. */ + g_last_seq_valid = false; + } + else + { + g_unexpected++; + } +} + +static void UmpRxCb(const uint32_t* words, uint8_t count, void* /*ctx*/) +{ + g_last_rx_ms = System::GetNow(); + for(uint8_t i = 0; i + 1 < count; i += 2) + HandleWord(words[i], words[i + 1]); +} + +/** MIDI 1.0 byte path is not used by the emitter, but the transport + * needs a parse callback registered to activate StartRx. + */ +static void NoopParseCb(uint8_t*, size_t, void*) {} + +static void USBH_Connect(void* /*data*/) +{ + hw.PrintLine("device connected"); +} +static void USBH_Disconnect(void* /*data*/) +{ + hw.PrintLine("device disconnected"); +} +static void USBH_Error(void* /*data*/) +{ + hw.PrintLine("USB error"); +} + +static void USBH_ClassActive(void* /*data*/) +{ + if(usbHost.IsActiveClass(USBH_MIDI_CLASS)) + { + hw.PrintLine("MIDI class active"); + MidiUsbTransport::Config cfg; + cfg.periph = MidiUsbTransport::Config::HOST; + midi.Init(cfg); + midi.SetUmpCallback(UmpRxCb, nullptr); + midi.StartRx(NoopParseCb, nullptr); + } +} + +struct Snapshot +{ + uint32_t total; + uint32_t note_on; + uint32_t note_off; + uint32_t marker; + uint32_t unexpected; + uint32_t gaps; + uint32_t first_burst_id; + uint32_t last_burst_id; + uint32_t total_at_first_marker; + uint32_t total_at_last_marker; + bool first_marker_seen; +}; + +static inline Snapshot ReadCounters() +{ + /** Single read per field. Torn reads across fields are tolerated: + * cumulative counters can only grow, so a heartbeat may briefly + * under-report by a few words while the RX callback runs between + * field reads; the next heartbeat corrects. + */ + Snapshot s; + s.total = g_total_messages; + s.note_on = g_note_on; + s.note_off = g_note_off; + s.marker = g_marker_count; + s.unexpected = g_unexpected; + s.gaps = g_gaps; + s.first_burst_id = g_first_burst_id; + s.last_burst_id = g_last_burst_id; + s.total_at_first_marker = g_total_at_first_marker; + s.total_at_last_marker = g_total_at_last_marker; + s.first_marker_seen = g_first_marker_seen; + return s; +} + +/** Bursts COMPLETED between the first and last marker (the partial + * burst received before the first marker is excluded by design). + */ +static inline uint32_t ExpectedFromMarkers(const Snapshot& s) +{ + if(!s.first_marker_seen) + return 0; + const uint32_t bursts = (s.last_burst_id - s.first_burst_id) & 0xFFFFU; + return bursts * kExpectedPerBurst; +} + +static void PrintLineTagged(const char* tag, const Snapshot& s) +{ + char buf[200]; + const uint32_t expected = ExpectedFromMarkers(s); + const uint32_t received = s.total_at_last_marker - s.total_at_first_marker; + const bool counters_clean + = (s.total > 0 && s.gaps == 0 && s.unexpected == 0); + /** Marker mode: the paired emitter brackets bursts with CC 119, so + * the span between the first and last marker must hold an exact + * multiple of the burst size. Without markers (any emitter that + * embeds a monotonic sequence in NoteOn velocity), the verdict + * falls back to the running gap / unexpected counters. + */ + const bool marker_exact = (s.marker == 0) || (received == expected); + const char* verdict = (counters_clean && marker_exact) ? "OK" : "FAIL"; + /** Compact field names: the logger truncates lines at 128 chars + * and the verdict must stay visible on long runs. + */ + sprintf(buf, + "%s bursts=%lu span=%lu..%lu recv=%lu exp=%lu " + "on=%lu off=%lu gaps=%lu unexp=%lu => %s", + tag, + static_cast(s.marker), + static_cast(s.first_burst_id), + static_cast(s.last_burst_id), + static_cast(received), + static_cast(expected), + static_cast(s.note_on), + static_cast(s.note_off), + static_cast(s.gaps), + static_cast(s.unexpected), + verdict); + hw.PrintLine(buf); +} + +int main(void) +{ + hw.Init(); + /** StartLog(true) blocks until the PC opens the USB CDC virtual COM + * port. Without this wait, the firmware can buffer log lines before + * the cdc_acm driver subscribes, which blocks the main loop on the + * first flush and stalls usbHost.Process() during early enumeration, + * causing the RX path to miss URBs. + */ + hw.StartLog(true); + hw.PrintLine("MIDI2_Host start"); + + USBHostHandle::Config usbhConfig; + usbhConfig.connect_callback = USBH_Connect; + usbhConfig.disconnect_callback = USBH_Disconnect; + usbhConfig.class_active_callback = USBH_ClassActive; + usbhConfig.error_callback = USBH_Error; + usbHost.Init(usbhConfig); + usbHost.RegisterClass(USBH_MIDI_CLASS); + + hw.PrintLine("host ready, waiting for emitter on USB-A"); + + uint32_t blink_time = 0; + bool led_state = false; + uint32_t next_hb_ms = kHeartbeatMs; + bool idle_reported = true; + + while(1) + { + usbHost.Process(); + + const uint32_t now = System::GetNow(); + if(now >= blink_time) + { + hw.SetLed(led_state); + led_state = !led_state; + blink_time = now + (usbHost.GetPresent() ? 400U : 80U); + } + + /** Heartbeat: print running tally every second while data is + * flowing. The cumulative counters can only grow, so this is + * a strictly-monotonic progress line. + */ + if(now >= next_hb_ms) + { + const Snapshot s = ReadCounters(); + /** Print whenever any UMP word has been received. The + * paired emitter's verdict logic kicks in once markers + * start arriving; against arbitrary MIDI 2.0 devices + * (no marker contract), the line still shows the running + * tally so the operator can verify the host is alive + * and the stack is draining URBs. + */ + if(s.total > 0) + PrintLineTagged("HEARTBEAT", s); + next_hb_ms = now + kHeartbeatMs; + } + + /** Idle detection: print one final RESULT line when the stream + * stops (emitter unplugged or paused). Fires once per quiet + * window; rearms automatically when data resumes. + */ + if(g_last_rx_ms != 0U && (now - g_last_rx_ms) > kIdleMs) + { + if(!idle_reported) + { + const Snapshot s = ReadCounters(); + PrintLineTagged("RESULT", s); + idle_reported = true; + } + } + else + { + idle_reported = false; + } + } +} diff --git a/examples/MIDI2_Host/Makefile b/examples/MIDI2_Host/Makefile new file mode 100644 index 000000000..a0fe9a93f --- /dev/null +++ b/examples/MIDI2_Host/Makefile @@ -0,0 +1,12 @@ +# Project Name +TARGET = MIDI2_Host + +# Sources +CPP_SOURCES = MIDI2_Host.cpp + +# Library Locations +LIBDAISY_DIR = ../.. + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core +include $(SYSTEM_FILES_DIR)/Makefile diff --git a/examples/MIDI2_USB_Device/CMakeLists.txt b/examples/MIDI2_USB_Device/CMakeLists.txt new file mode 100644 index 000000000..303a42a74 --- /dev/null +++ b/examples/MIDI2_USB_Device/CMakeLists.txt @@ -0,0 +1,3 @@ +set(FIRMWARE_NAME MIDI2_USB_Device) +set(FIRMWARE_SOURCES MIDI2_USB_Device.cpp) +include(DaisyProject) diff --git a/examples/MIDI2_USB_Device/MIDI2_USB_Device.cpp b/examples/MIDI2_USB_Device/MIDI2_USB_Device.cpp new file mode 100644 index 000000000..8e2c56005 --- /dev/null +++ b/examples/MIDI2_USB_Device/MIDI2_USB_Device.cpp @@ -0,0 +1,132 @@ +/** + * @author sauloverissimo + * @brief USB MIDI 2.0 deterministic stress emitter (pairs with MIDI2_Host) + * @date 2026-06-10 + * + * USB MIDI 2.0 Device: deterministic UMP reference emitter + * + * Daisy Seed enumerates as a USB MIDI 2.0 device on Alt 1 (UMP). + * The main loop emits a deterministic stress catalog at the bus rate + * the host can drain: + * + * per burst: + * 2000 iterations of (NoteOn note, NoteOff note) + * note = 48 + (i % 24) (chromatic walk, notes 48..71) + * vel32 = (seq << 16) (seq number embedded so the + * host can verify contiguity) + * seq increments per NoteOn inside the burst (0..1999) + * 1 marker CC 119, value = burst_id (rolling 16-bit) + * hold 400 ms (quiet, observable as a cycle boundary) + * repeat + * + * Total per burst: 4001 UMP packets (2000 NoteOn + 2000 NoteOff + 1 CC). + * Paired with the MIDI2_Host example, the host validator checks that + * every marker-to-marker span holds exactly 4001 packets with zero + * sequence gaps. + */ +#include "daisy_seed.h" +#include "hid/usb_midi.h" + +using namespace daisy; + +/** Global hardware access */ +DaisySeed hw; +MidiUsbTransport midi; + +/** UMP constants (M2-104-UM) + * + * Word 0 layout for MT 0x4 (MIDI 2.0 Channel Voice): + * bits 31..28 MT (0x4) + * bits 27..24 Group (0x0, the only group the group terminal + * block declares) + * bits 23..20 Status (0x9 NoteOn, 0x8 NoteOff, 0xB CC) + * bits 19..16 Channel (0x0) + * bits 15..08 Note / Index + * bits 07..00 Reserved (0x00) + */ +static constexpr uint32_t kMT4NoteOn = 0x40900000U; +static constexpr uint32_t kMT4NoteOff = 0x40800000U; +static constexpr uint32_t kMT4CC = 0x40B00000U; +static constexpr uint8_t kMarkerCC = 119U; + +static constexpr uint16_t kBurstCount = 2000U; +static constexpr uint8_t kBurstLowNote = 48U; +static constexpr uint8_t kBurstHighNote = 71U; +static constexpr uint32_t kBurstPauseMs = 400U; + +/** Receive callbacks: this example does not consume incoming UMP, but + * the transport requires both callbacks to be registered for StartRx + * to activate the receive pipe. + */ +static void NoopParseCb(uint8_t*, size_t, void*) {} +static void NoopUmpCb(const uint32_t*, uint8_t, void*) {} + +/** Send one UMP packet (2 words = 8 bytes). MidiUsbTransport::Tx blocks + * until the bulk endpoint has accepted the data (subject to the + * tx_retry_count in Config; default 3), so the caller does not need + * to throttle. + */ +static inline void SendPacket(uint32_t w0, uint32_t w1) +{ + uint32_t words[2] = {w0, w1}; + midi.Tx(reinterpret_cast(words), sizeof(words)); +} + +static void EmitBurst(uint16_t burst_id) +{ + const uint8_t note_span = kBurstHighNote - kBurstLowNote + 1U; + for(uint16_t i = 0; i < kBurstCount; ++i) + { + const uint8_t note = kBurstLowNote + (i % note_span); + const uint32_t seq = static_cast(i); + const uint32_t w0_on + = kMT4NoteOn | (static_cast(note) << 8); + const uint32_t w0_off + = kMT4NoteOff | (static_cast(note) << 8); + SendPacket(w0_on, seq << 16); + SendPacket(w0_off, 0U); + } + SendPacket(kMT4CC | (static_cast(kMarkerCC) << 8), + static_cast(burst_id)); +} + +static void Hold(uint32_t ms, uint32_t& blink_time, bool& led_state) +{ + const uint32_t start = System::GetNow(); + while(System::GetNow() - start < ms) + { + const uint32_t now = System::GetNow(); + if(now >= blink_time) + { + hw.SetLed(led_state); + led_state = !led_state; + blink_time = now + 100U; + } + } +} + +int main(void) +{ + hw.Init(); + hw.StartLog(false); + hw.PrintLine("MIDI2_USB_Device start"); + + MidiUsbTransport::Config cfg; + cfg.periph = MidiUsbTransport::Config::INTERNAL; + midi.Init(cfg); + midi.SetUmpCallback(NoopUmpCb, nullptr); + midi.StartRx(NoopParseCb, nullptr); + + hw.PrintLine("transport up, emitting reference stream"); + + uint32_t blink_time = 0; + bool led_state = false; + uint16_t burst_id = 0; + + while(1) + { + burst_id++; + EmitBurst(burst_id); + Hold(kBurstPauseMs, blink_time, led_state); + } +} diff --git a/examples/MIDI2_USB_Device/Makefile b/examples/MIDI2_USB_Device/Makefile new file mode 100644 index 000000000..250956b48 --- /dev/null +++ b/examples/MIDI2_USB_Device/Makefile @@ -0,0 +1,12 @@ +# Project Name +TARGET = MIDI2_USB_Device + +# Sources +CPP_SOURCES = MIDI2_USB_Device.cpp + +# Library Locations +LIBDAISY_DIR = ../.. + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core +include $(SYSTEM_FILES_DIR)/Makefile diff --git a/src/hid/usb.cpp b/src/hid/usb.cpp index a8e32c94e..c4c2683e2 100644 --- a/src/hid/usb.cpp +++ b/src/hid/usb.cpp @@ -24,7 +24,8 @@ extern "C" CDC_ReceiveCallback rxcallback; - uint8_t usbd_mode = USBD_MODE_CDC; + uint8_t usbd_mode = USBD_MODE_CDC; + uint8_t usbd_midi2_alt_setting = 0; } UsbHandle::ReceiveCallback rx_callback; diff --git a/src/hid/usb_midi.cpp b/src/hid/usb_midi.cpp index 539d0ff56..ba431d020 100644 --- a/src/hid/usb_midi.cpp +++ b/src/hid/usb_midi.cpp @@ -3,6 +3,7 @@ #include "usbh_midi.h" #include "hid/usb_midi.h" #include +#include extern "C" { @@ -30,8 +31,16 @@ class MidiUsbTransport::Impl void FlushRx() { rx_buffer_.Flush(); } void Tx(uint8_t* buffer, size_t size); + void SetUmpCallback(MidiRxUmpCallback cb, void* ctx) + { + ump_callback_ = cb; + ump_context_ = ctx; + } + void UsbToMidi(uint8_t* buffer, uint8_t length); + void UsbToUmp(uint8_t* buffer, uint32_t length); void MidiToUsb(uint8_t* buffer, size_t length); + void UmpToUsb(uint8_t* buffer, size_t size); void Parse(); private: @@ -49,6 +58,10 @@ class MidiUsbTransport::Impl MidiRxParseCallback parse_callback_; void* parse_context_; + // UMP callback for MIDI 2.0 Alt 1 + MidiRxUmpCallback ump_callback_; + void* ump_context_; + // simple, self-managed buffer uint8_t tx_buffer_[kBufferSize]; size_t tx_ptr_; @@ -79,20 +92,48 @@ void ReceiveCallback(uint8_t* buffer, uint32_t* length) { if(midi_usb_handle.RxActive()) { - for(uint16_t i = 0; i < *length; i += 4) + if(usbd_midi2_alt_setting == 0) { - size_t remaining_bytes = *length - i; - uint8_t packet_length = remaining_bytes > 4 ? 4 : remaining_bytes; - midi_usb_handle.UsbToMidi(buffer + i, packet_length); - midi_usb_handle.Parse(); + // Alt 0: MIDI 1.0 CIN packets + for(uint16_t i = 0; i < *length; i += 4) + { + size_t remaining_bytes = *length - i; + uint8_t packet_length + = remaining_bytes > 4 ? 4 : remaining_bytes; + midi_usb_handle.UsbToMidi(buffer + i, packet_length); + midi_usb_handle.Parse(); + } + } + else + { + // Alt 1: MIDI 2.0 UMP words + midi_usb_handle.UsbToUmp(buffer, *length); } } } static void HostReceiveCallback(uint8_t* buffer, size_t sz, void* pUser) { - uint32_t len = sz; - ReceiveCallback(buffer, &len); + if(midi_usb_handle.RxActive()) + { + uint8_t host_alt = USBH_MIDI_GetAltSetting(pUSB_Host); + if(host_alt == 0) + { + // Alt 0: MIDI 1.0 CIN packets + for(size_t i = 0; i < sz; i += 4) + { + size_t remaining = sz - i; + uint8_t pkt_len = remaining > 4 ? 4 : remaining; + midi_usb_handle.UsbToMidi(buffer + i, pkt_len); + midi_usb_handle.Parse(); + } + } + else + { + // Alt 1: MIDI 2.0 UMP words + midi_usb_handle.UsbToUmp(buffer, (uint32_t)sz); + } + } } void MidiUsbTransport::Impl::Init(Config config) @@ -103,8 +144,10 @@ void MidiUsbTransport::Impl::Init(Config config) */ // static_assert(1u == sizeof(MidiUsbTransport::Impl::usb_handle_), "UsbHandle is not static"); - config_ = config; - rx_active_ = false; + config_ = config; + rx_active_ = false; + ump_callback_ = nullptr; + ump_context_ = nullptr; if(config_.periph == Config::HOST) { @@ -113,8 +156,8 @@ void MidiUsbTransport::Impl::Init(Config config) } else { - // This tells the USB middleware to send out MIDI descriptors instead of CDC - usbd_mode = USBD_MODE_MIDI; + // MIDI 2.0 descriptors include Alt 0 (MIDI 1.0 compat) + Alt 1 (UMP) + usbd_mode = USBD_MODE_MIDI2; UsbHandle::UsbPeriph periph = UsbHandle::FS_INTERNAL; if(config_.periph == Config::EXTERNAL) @@ -127,12 +170,77 @@ void MidiUsbTransport::Impl::Init(Config config) } } +void MidiUsbTransport::Impl::UsbToUmp(uint8_t* buffer, uint32_t length) +{ + uint32_t offset = 0; + while(offset + 4 <= length) + { + uint32_t w0; + memcpy(&w0, buffer + offset, 4); + + // Word count by message type (M2-104-UM table 4). Kept local + // so the transport layer has no dependency on a MIDI 2.0 + // message library. + uint8_t mt = (w0 >> 28) & 0x0F; + uint8_t wc; + switch(mt) + { + case 0x0: + case 0x1: + case 0x2: + case 0x6: + case 0x7: wc = 1; break; + case 0x3: + case 0x4: + case 0x8: + case 0x9: + case 0xA: wc = 2; break; + case 0xB: + case 0xC: wc = 3; break; + default: wc = 4; break; + } + + if(offset + wc * 4 > length) + break; + + uint32_t words[4]; + for(uint8_t i = 0; i < wc; i++) + memcpy(&words[i], buffer + offset + i * 4, 4); + + if(ump_callback_) + ump_callback_(words, wc, ump_context_); + + offset += wc * 4; + } +} + +void MidiUsbTransport::Impl::UmpToUsb(uint8_t* buffer, size_t size) +{ + // UMP words go directly to endpoint, no CIN encapsulation + tx_ptr_ = 0; + size_t copy = (size <= kBufferSize) ? size : kBufferSize; + memcpy(tx_buffer_, buffer, copy); + tx_ptr_ = copy; +} + void MidiUsbTransport::Impl::Tx(uint8_t* buffer, size_t size) { int attempt_count = config_.tx_retry_count; bool should_retry; - MidiToUsb(buffer, size); + // UMP framing applies when the active MIDIStreaming interface is + // Alt 1: the connected device's alt setting in host mode, our own + // device alt setting otherwise. + bool ump_mode; + if(config_.periph == Config::HOST) + ump_mode = (USBH_MIDI_GetAltSetting(pUSB_Host) == 1); + else + ump_mode = (usbd_midi2_alt_setting == 1); + + if(ump_mode) + UmpToUsb(buffer, size); + else + MidiToUsb(buffer, size); do { if(config_.periph == Config::HOST) @@ -346,6 +454,11 @@ void MidiUsbTransport::StartRx(MidiRxParseCallback callback, void* context) pimpl_->StartRx(callback, context); } +void MidiUsbTransport::SetUmpCallback(MidiRxUmpCallback cb, void* context) +{ + pimpl_->SetUmpCallback(cb, context); +} + bool MidiUsbTransport::RxActive() { return pimpl_->RxActive(); diff --git a/src/hid/usb_midi.h b/src/hid/usb_midi.h index 900d5afcd..60e28659c 100644 --- a/src/hid/usb_midi.h +++ b/src/hid/usb_midi.h @@ -44,9 +44,24 @@ class MidiUsbTransport Config() : periph(INTERNAL), tx_retry_count(3) {} }; + /** Called once per complete UMP message received on Alt 1. + * \param words Message words, most significant word first + * \param word_count Number of valid words (1..4, per message type) + * \param context User context passed to SetUmpCallback + */ + typedef void (*MidiRxUmpCallback)(const uint32_t* words, + uint8_t word_count, + void* context); + void Init(Config config); void StartRx(MidiRxParseCallback callback, void* context); + + /** Registers the receive callback for UMP traffic (MIDI 2.0 Alt 1). + * MIDI 1.0 byte traffic keeps flowing through the StartRx parse + * callback; both can be registered at the same time. + */ + void SetUmpCallback(MidiRxUmpCallback cb, void* context); bool RxActive(); void FlushRx(); void Tx(uint8_t* buffer, size_t size); diff --git a/src/usbd/usbd_desc.c b/src/usbd/usbd_desc.c index 3a9941c2d..8ca124772 100644 --- a/src/usbd/usbd_desc.c +++ b/src/usbd/usbd_desc.c @@ -25,7 +25,7 @@ #include "usbd_conf.h" /* USER CODE BEGIN INCLUDE */ - +#include "usbd_cdc.h" /* USER CODE END INCLUDE */ /* Private typedef -----------------------------------------------------------*/ @@ -75,6 +75,7 @@ // FS #define USBD_PID_FS 22336 // replace with our PID when we have one. #define USBD_PRODUCT_STRING_FS "Daisy Seed Built In" +#define USBD_PRODUCT_STRING_FS_MIDI2 "Daisy Seed MIDI 2.0" #define USBD_CONFIGURATION_STRING_FS "CDC Config" #define USBD_INTERFACE_STRING_FS "CDC Interface" @@ -474,14 +475,10 @@ uint8_t *USBD_FS_LangIDStrDescriptor(USBD_SpeedTypeDef speed, uint16_t *length) */ uint8_t *USBD_FS_ProductStrDescriptor(USBD_SpeedTypeDef speed, uint16_t *length) { - if(speed == 0) - { - USBD_GetString((uint8_t *)USBD_PRODUCT_STRING_FS, USBD_StrDesc, length); - } - else - { - USBD_GetString((uint8_t *)USBD_PRODUCT_STRING_FS, USBD_StrDesc, length); - } + const char *name = (usbd_mode == USBD_MODE_MIDI2) + ? USBD_PRODUCT_STRING_FS_MIDI2 + : USBD_PRODUCT_STRING_FS; + USBD_GetString((uint8_t *)name, USBD_StrDesc, length); return USBD_StrDesc; } diff --git a/src/usbh/usbh_conf.h b/src/usbh/usbh_conf.h index ab71b4387..562b61576 100644 --- a/src/usbh/usbh_conf.h +++ b/src/usbh/usbh_conf.h @@ -67,7 +67,7 @@ extern "C" #define USBH_MAX_NUM_ENDPOINTS 2U /*---------- -----------*/ -#define USBH_MAX_NUM_INTERFACES 2U +#define USBH_MAX_NUM_INTERFACES 4U /*---------- -----------*/ #define USBH_MAX_NUM_CONFIGURATION 1U