diff --git a/examples/host/CMakeLists.txt b/examples/host/CMakeLists.txt index 5c63ec0c06..ce679815d4 100644 --- a/examples/host/CMakeLists.txt +++ b/examples/host/CMakeLists.txt @@ -8,3 +8,4 @@ family_initialize_project(tinyusb_host_examples ${CMAKE_CURRENT_LIST_DIR}) # family_add_subdirectory will filter what to actually add based on selected FAMILY family_add_subdirectory(cdc_msc_hid) family_add_subdirectory(hid_controller) +family_add_subdirectory(midi) diff --git a/examples/host/midi/.only.MCU_RP2040 b/examples/host/midi/.only.MCU_RP2040 new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/host/midi/CMakeLists.txt b/examples/host/midi/CMakeLists.txt new file mode 100644 index 0000000000..15a818bac6 --- /dev/null +++ b/examples/host/midi/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.5) + +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake) + +# gets PROJECT name for the example (e.g. -) +family_get_project_name(PROJECT ${CMAKE_CURRENT_LIST_DIR}) + +project(${PROJECT}) + +# Checks this example is valid for the family and initializes the project +family_initialize_project(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}) + +add_executable(${PROJECT}) + +# Example source +target_sources(${PROJECT} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src/midi_app.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/main.c + ) + +# Example include +target_include_directories(${PROJECT} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src + ) + +# Configure compilation flags and libraries for the example... see the corresponding function +# in hw/bsp/FAMILY/family.cmake for details. +family_configure_host_example(${PROJECT}) \ No newline at end of file diff --git a/examples/host/midi/Makefile b/examples/host/midi/Makefile new file mode 100644 index 0000000000..1012759d7a --- /dev/null +++ b/examples/host/midi/Makefile @@ -0,0 +1,29 @@ +include ../../../tools/top.mk +include ../../make.mk + +INC += \ + src \ + $(TOP)/hw \ + +# Example source +EXAMPLE_SOURCE += $(wildcard src/*.c) +SRC_C += $(addprefix $(CURRENT_PATH)/, $(EXAMPLE_SOURCE)) + +# TODO: suppress warning caused by host stack +CFLAGS += -Wno-error=cast-align -Wno-error=null-dereference + +# TinyUSB Host Stack source +SRC_C += \ + src/class/cdc/cdc_host.c \ + src/class/hid/hid_host.c \ + src/class/midi/midi_host.c \ + src/class/msc/msc_host.c \ + src/host/hub.c \ + src/host/usbh.c \ + src/host/usbh_control.c \ + src/portable/ehci/ehci.c \ + src/portable/ohci/ohci.c \ + src/portable/nxp/transdimension/hcd_transdimension.c \ + src/portable/nxp/lpc17_40/hcd_lpc17_40.c + +include ../../rules.mk diff --git a/examples/host/midi/src/CMakeLists.txt b/examples/host/midi/src/CMakeLists.txt new file mode 100644 index 0000000000..15a818bac6 --- /dev/null +++ b/examples/host/midi/src/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.5) + +include(${CMAKE_CURRENT_SOURCE_DIR}/../../../hw/bsp/family_support.cmake) + +# gets PROJECT name for the example (e.g. -) +family_get_project_name(PROJECT ${CMAKE_CURRENT_LIST_DIR}) + +project(${PROJECT}) + +# Checks this example is valid for the family and initializes the project +family_initialize_project(${PROJECT} ${CMAKE_CURRENT_LIST_DIR}) + +add_executable(${PROJECT}) + +# Example source +target_sources(${PROJECT} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src/midi_app.c + ${CMAKE_CURRENT_SOURCE_DIR}/src/main.c + ) + +# Example include +target_include_directories(${PROJECT} PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/src + ) + +# Configure compilation flags and libraries for the example... see the corresponding function +# in hw/bsp/FAMILY/family.cmake for details. +family_configure_host_example(${PROJECT}) \ No newline at end of file diff --git a/examples/host/midi/src/Makefile b/examples/host/midi/src/Makefile new file mode 100644 index 0000000000..a50783ccd0 --- /dev/null +++ b/examples/host/midi/src/Makefile @@ -0,0 +1,27 @@ +include ../../../tools/top.mk +include ../../make.mk + +INC += \ + src \ + $(TOP)/hw \ + +# Example source +EXAMPLE_SOURCE += $(wildcard src/*.c) +SRC_C += $(addprefix $(CURRENT_PATH)/, $(EXAMPLE_SOURCE)) + +# TODO: suppress warning caused by host stack +CFLAGS += -Wno-error=cast-align -Wno-error=null-dereference + +# TinyUSB Host Stack source +SRC_C += \ + src/class/midi/midi_host.c \ + src/host/hub.c \ + src/host/usbh.c \ + src/host/usbh_control.c \ + src/portable/ehci/ehci.c \ + src/portable/ohci/ohci.c \ + src/portable/nxp/transdimension/hcd_transdimension.c \ + src/portable/nxp/lpc17_40/hcd_lpc17_40.c \ + src/portable/raspberrypi/rp2040/hcd_rp2040.c + +include ../../rules.mk diff --git a/examples/host/midi/src/main.c b/examples/host/midi/src/main.c new file mode 100644 index 0000000000..1cbae755f6 --- /dev/null +++ b/examples/host/midi/src/main.c @@ -0,0 +1,85 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#include +#include +#include + +#include "bsp/board.h" +#include "tusb.h" +#if CFG_TUH_MIDI + +//--------------------------------------------------------------------+ +// MACRO CONSTANT TYPEDEF PROTYPES +//--------------------------------------------------------------------+ +void led_blinking_task(void); + +extern void midi_host_app_task(void); + +/*------------- MAIN -------------*/ +int main(void) +{ + board_init(); + + printf("TinyUSB Host MIDI Example\r\n"); + + tusb_init(); + + while (1) + { + // tinyusb host task + tuh_task(); + led_blinking_task(); + + midi_host_app_task(); + + } + + return 0; +} + +#endif + +//--------------------------------------------------------------------+ +// TinyUSB Callbacks +//--------------------------------------------------------------------+ + +//--------------------------------------------------------------------+ +// Blinking Task +//--------------------------------------------------------------------+ +void led_blinking_task(void) +{ + const uint32_t interval_ms = 1000; + static uint32_t start_ms = 0; + + static bool led_state = false; + + // Blink every interval ms + if ( board_millis() - start_ms < interval_ms) return; // not enough time + start_ms += interval_ms; + + board_led_write(led_state); + led_state = 1 - led_state; // toggle +} diff --git a/examples/host/midi/src/midi_app.c b/examples/host/midi/src/midi_app.c new file mode 100644 index 0000000000..c2fc4132a4 --- /dev/null +++ b/examples/host/midi/src/midi_app.c @@ -0,0 +1,205 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2021, Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +/** + * This test program is designed to send and receive MIDI data concurrently + * It send G, G#, A, A#, B over and over again on the highest cable number + * of the MIDI device. The notes correspond to the Mackie Control transport + * button LEDs, so most keyboards and controllers will react to these note + * messages from the host. The host will print on the serial port console + * any incoming messages along with the cable number that sent them. + * + * If you define MIDIH_TEST_KORG_NANOKONTROL2 to 1 and connect a Korg + * nanoKONTROL2 controller to the host, then the host will request a dump + * of the current scene, and the nanoKONTROL2 will respond with a long + * sysex message (used to prove that sysex decoding appears to work) + */ +#include "bsp/board.h" +#include "tusb.h" +#include "class/midi/midi_host.h" +//--------------------------------------------------------------------+ +// MACRO TYPEDEF CONSTANT ENUM DECLARATION +//--------------------------------------------------------------------+ +#ifndef MIDIH_TEST_KORG_NANOKONTROL2 +#define MIDIH_TEST_KORG_NANOKONTROL2 0 +#endif + +#define MIDIH_TEST_KORG_NANOKONTROL2_COUNT 5 +//--------------------------------------------------------------------+ +// STATIC GLOBALS DECLARATION +//--------------------------------------------------------------------+ +static uint8_t midi_dev_addr = 0; +static uint8_t first_note = 0x5b; // Mackie Control rewind +static uint8_t last_note = 0x5f; // Mackie Control stop +static uint8_t message[6] = +{ + 0x90, 0x5f, 0x00, + 0x90, 0x5b, 0x7f, +}; +#if MIDIH_TEST_KORG_NANOKONTROL2 +// This command will dump a scene from a Kork nanoKONTROL2 control surface +// Use it to test receiving long sysex messages +static uint8_t nanoKONTROL2_dump_req[] = +{ + 0xf0, 0x42, 0x40, 0x00, 0x01, 0x13, 0x00, 0x1f, 0x10, 0x00, 0xf7 +}; +// wait MIDIH_TEST_KORG_NANOKONTROL2_COUNT seconds before requesting a dump +static uint8_t dump_req_countdown = MIDIH_TEST_KORG_NANOKONTROL2_COUNT; +#endif + +static void test_tx(void) +{ + // toggle NOTE On, Note Off for the Mackie Control channels 1-8 REC LED + const uint32_t interval_ms = 1000; + static uint32_t start_ms = 0; + + // device must be attached and have at least one endpoint ready to receive a message + if (!midi_dev_addr || !tuh_midi_configured(midi_dev_addr)) + { + return; + } + if (tuh_midih_get_num_tx_cables(midi_dev_addr) < 1) + { + return; + } + // transmit any previously queued bytes + tuh_midi_stream_flush(midi_dev_addr); + // Blink every interval ms + if ( board_millis() - start_ms < interval_ms) + { + return; // not enough time + } + start_ms += interval_ms; + + uint32_t nwritten = 0; +#if MIDIH_TEST_KORG_NANOKONTROL2 + if (dump_req_countdown == 0) + { + nwritten = tuh_midi_stream_write(midi_dev_addr, 0, nanoKONTROL2_dump_req, sizeof(nanoKONTROL2_dump_req)); + if (nwritten != sizeof(nanoKONTROL2_dump_req)) + { + _MESS_FAILED(); + TU_BREAKPOINT(); + } + dump_req_countdown = MIDIH_TEST_KORG_NANOKONTROL2_COUNT; + } + else + { + nwritten = 0; + nwritten += tuh_midi_stream_write(midi_dev_addr, 0, message, sizeof(message)); + --dump_req_countdown; + } +#else + uint8_t cable = tuh_midih_get_num_tx_cables(midi_dev_addr) - 1; + nwritten = 0; + /// @bug For some reason, Arturia Keylab-88 ignores these messages if + /// they are transimitted on cable 0 first + //for (cable = 0; cable < tuh_midih_get_num_tx_cables(midi_dev_addr); cable++) + nwritten += tuh_midi_stream_write(midi_dev_addr, cable, message, sizeof(message)); +#endif + +#if MIDIH_TEST_KORG_NANOKONTROL2 + if (nwritten != 0 && dump_req_countdown != MIDIH_TEST_KORG_NANOKONTROL2_COUNT) +#else + if (nwritten != 0) +#endif + { + ++message[1]; + ++message[4]; + if (message[1] > last_note) + message[1] = first_note; + if (message[4] > last_note) + message[4] = first_note; + } +} + +static void test_rx(void) +{ + // device must be attached and have at least one endpoint ready to receive a message + if (!midi_dev_addr || !tuh_midi_configured(midi_dev_addr)) + { + return; + } + if (tuh_midih_get_num_rx_cables(midi_dev_addr) < 1) + { + return; + } + tuh_midi_read_poll(midi_dev_addr); +} + +void midi_host_app_task(void) +{ + test_tx(); + + test_rx(); +} + +//--------------------------------------------------------------------+ +// TinyUSB Callbacks +//--------------------------------------------------------------------+ + +// Invoked when device with hid interface is mounted +// Report descriptor is also available for use. tuh_hid_parse_report_descriptor() +// can be used to parse common/simple enough descriptor. +// Note: if report descriptor length > CFG_TUH_ENUMERATION_BUFSIZE, it will be skipped +// therefore report_desc = NULL, desc_len = 0 +void tuh_midi_mount_cb(uint8_t dev_addr, uint8_t in_ep, uint8_t out_ep, uint8_t num_cables_rx, uint16_t num_cables_tx) +{ + printf("MIDI device address = %u, IN endpoint %u has %u cables, OUT endpoint %u has %u cables\r\n", + dev_addr, in_ep & 0xf, num_cables_rx, out_ep & 0xf, num_cables_tx); + message[1] = last_note; + message[4] = first_note; +#if MIDIH_TEST_KORG_NANOKONTROL2 + dump_req_countdown = MIDIH_TEST_KORG_NANOKONTROL2_COUNT; +#endif + midi_dev_addr = dev_addr; +} + +// Invoked when device with hid interface is un-mounted +void tuh_midi_umount_cb(uint8_t dev_addr, uint8_t instance) +{ + midi_dev_addr = 0; + printf("MIDI device address = %d, instance = %d is unmounted\r\n", dev_addr, instance); +} + +void tuh_midi_rx_cb(uint8_t dev_addr, uint32_t num_packets) +{ + if (midi_dev_addr == dev_addr) + { + if (num_packets != 0) + { + uint8_t cable_num; + uint8_t buffer[48]; + uint32_t bytes_read = tuh_midi_stream_read(dev_addr, &cable_num, buffer, sizeof(buffer)); + TU_LOG1("Read bytes %u cable %u", bytes_read, cable_num); + TU_LOG1_MEM(buffer, bytes_read, 2); + } + } +} + +void tuh_midi_tx_cb(uint8_t dev_addr) +{ + +} diff --git a/examples/host/midi/src/tusb_config.h b/examples/host/midi/src/tusb_config.h new file mode 100644 index 0000000000..f1bd045976 --- /dev/null +++ b/examples/host/midi/src/tusb_config.h @@ -0,0 +1,95 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + */ + +#ifndef _TUSB_CONFIG_H_ +#define _TUSB_CONFIG_H_ + +#ifdef __cplusplus + extern "C" { +#endif + +//-------------------------------------------------------------------- +// COMMON CONFIGURATION +//-------------------------------------------------------------------- + +// defined by compiler flags for flexibility +#ifndef CFG_TUSB_MCU + #error CFG_TUSB_MCU must be defined +#endif + +#if CFG_TUSB_MCU == OPT_MCU_LPC43XX || CFG_TUSB_MCU == OPT_MCU_LPC18XX || CFG_TUSB_MCU == OPT_MCU_MIMXRT10XX + #define CFG_TUSB_RHPORT0_MODE (OPT_MODE_HOST | OPT_MODE_HIGH_SPEED) +#else + #define CFG_TUSB_RHPORT0_MODE OPT_MODE_HOST +#endif + +#ifndef CFG_TUSB_OS +#define CFG_TUSB_OS OPT_OS_NONE +#endif + +// CFG_TUSB_DEBUG is defined by compiler in DEBUG build +// #define CFG_TUSB_DEBUG 0 + +/* USB DMA on some MCUs can only access a specific SRAM region with restriction on alignment. + * Tinyusb use follows macros to declare transferring memory so that they can be put + * into those specific section. + * e.g + * - CFG_TUSB_MEM SECTION : __attribute__ (( section(".usb_ram") )) + * - CFG_TUSB_MEM_ALIGN : __attribute__ ((aligned(4))) + */ +#ifndef CFG_TUSB_MEM_SECTION +#define CFG_TUSB_MEM_SECTION +#endif + +#ifndef CFG_TUSB_MEM_ALIGN +#define CFG_TUSB_MEM_ALIGN __attribute__ ((aligned(4))) +#endif + +//-------------------------------------------------------------------- +// CONFIGURATION +//-------------------------------------------------------------------- + +// Size of buffer to hold descriptors and other data used for enumeration +#define CFG_TUH_ENUMERATION_BUFSIZE 256 + +#define CFG_TUH_HUB 1 +#define CFG_TUH_CDC 0 +#define CFG_TUH_HID 0 // typical keyboard + mouse device can have 3-4 HID interfaces +#define CFG_TUH_MIDI 1 // there will be at most one MIDIStreaming Interface descriptor +#define CFG_TUH_MSC 0 +#define CFG_TUH_VENDOR 0 + +// max device support (excluding hub device) +#define CFG_TUH_DEVICE_MAX (CFG_TUH_HUB ? 4 : 1) // hub typically has 4 ports + +//------------- HID -------------// +#define CFG_TUH_HID_EPIN_BUFSIZE 64 +#define CFG_TUH_HID_EPOUT_BUFSIZE 64 + +#ifdef __cplusplus + } +#endif + +#endif /* _TUSB_CONFIG_H_ */ diff --git a/hw/bsp/rp2040/family.cmake b/hw/bsp/rp2040/family.cmake index 1aa180ef82..6576138e00 100644 --- a/hw/bsp/rp2040/family.cmake +++ b/hw/bsp/rp2040/family.cmake @@ -87,6 +87,7 @@ if (NOT TARGET _rp2040_family_inclusion_marker) ${TOP}/src/host/hub.c ${TOP}/src/class/cdc/cdc_host.c ${TOP}/src/class/hid/hid_host.c + ${TOP}/src/class/midi/midi_host.c ${TOP}/src/class/msc/msc_host.c ${TOP}/src/class/vendor/vendor_host.c ) diff --git a/src/class/midi/README_midi_host.md b/src/class/midi/README_midi_host.md new file mode 100644 index 0000000000..9df2249b39 --- /dev/null +++ b/src/class/midi/README_midi_host.md @@ -0,0 +1,111 @@ +# MIDI HOST DRIVER +This README file contains the design notes and limitations of the +MIDI host driver. + +# MAXIMUM NUMBER OF MIDI DEVICES ATTACHED TO HOST +In this version of the driver, only one MIDI device is supported. This +constraint may change in the future. + +# MAXIMUM NUMBER OF ENDPOINTS +Although the USB MIDI 1.0 Class specification allows an arbitrary number +of endpoints, this driver supports at most one USB BULK DATA IN endpoint +and one USB BULK DATA OUT endpoint. Each endpoint can support up to 16 +virtual cables. If a device has multiple IN endpoints or multiple OUT +endpoints, it will fail to enumerate. + +Most USB MIDI devices contain both an IN endpoint and an OUT endpoint, +but not all do. For example, some USB pedals only support an OUT endpoint. +This driver allows that. + +# PUBLIC API +Applications interact with this driver via 8-bit buffers of MIDI messages +formed using the rules for sending bytes on a 5-pin DIN cable per the +original MIDI 1.0 specification. + +To send a message to a device, the Host application composes a sequence +of status and data bytes in a byte array and calls the API function. +The arguments of the function are a pointer to the byte array, the number +of bytes in the array, and the target virtual cable number 0-15. + +When the host driver receives a message from the device, the host driver +will call a callback function that the host application registers. This +callback function contains a pointer to a message buffer, a message length, +and the virtual cable number of the message buffer. One complete bulk IN +endpoint transfer might contain multiple messages targeted to different +virtual cables. + +# SUBCLASS AUDIO CONTROL +A MIDI device does not absolutely need to have an Audio Control Interface, +unless it adheres to the USB Audio Class 2 spec, but many devices +have them even if the devices do not have an audio streaming interface. +Because this driver does not support audio streaming, the descriptor parser +will skip past any audio control interface and audio streaming interface +and open only the MIDI interface. + +An audio streaming host driver can use this driver by passing a pointer +to the MIDI interface descriptor that is found after the audio streaming +interface to the midih_open() function. That is, an audio streaming host +driver would parse the audio control interface descriptor and then the +audio streaming interface and endpoint descriptors. When the next descriptor +pointer points to a MIDI interface descriptor, call midih_open() with that +descriptor pointer. + +# CLASS SPECIFIC INTERFACE AND REQUESTS +The host driver does not make use of the informaton in the class specific +interface descriptors. In the future, a public API could be created to +retrieve the string descriptors for the names of each ELEMENT, +IN JACK and OUT JACK, and how the device describes the connections. + +This driver also does not support class specific requests to control +ELEMENT items, nor does it support non-MIDI Streaming bulk endpoints. + +# MIDI CLASS SPECIFIC DESCRIPTOR TOTAL LENGTH FIELD IGNORED +I have observed at least one keyboard by a leading manufacturer that +sets the wTotalLength field of the Class-Specific MS Interface Header +Descriptor to include the length of the MIDIStreaming Endpoint +Descriptors. This is wrong per my reading of the specification. + +# MESSAGE BUFFER DETAILS +Messages buffers composed from USB data received on the IN endpoint will never contain +running status because USB MIDI 1.0 class does not support that. Messages +buffers to be sent to the device on the OUT endpont may contain running status +(the message might come from a UART data stream from a 5-pin DIN MIDI IN +cable on the host, for example). The driver may in the future correctly compose +4-byte USB MIDI Class packets using the running status if need be. However, +it does not currently do that. Also, use of running status is not a good idea +overall because a single byte error can really mess up the data stream with no +way to recover until the next non-real time status byte is in the message buffer. + +Message buffers to be sent to the device may contain Real time messages +such as MIDI clock. Real time messages may be inserted in the message +byte stream between status and data bytes of another message without disrupting +the running status. However, because MIDI 1.0 class messages are sent +as four byte packets, a real-time message so inserted will be re-ordered +to be sent to the device in a new 4-byte packet immediately before the +interrupted data stream. + +Real time messages the device sends to the host can only appear between +the status byte and data bytes of the message in System Exclusive messages +that are longer than 3 bytes. + +# POORLY FORMED USB MIDI DATA PACKETS FROM THE DEVICE +Some devices do not properly encode the code index number (CIN) for the +MIDI message status byte even though the 3-byte data payload correctly encodes +the MIDI message. This driver looks to the byte after the CIN byte to decide +how many bytes to place in the message buffer. + +Some devices do not properly encode the virtual cable number. If the virtual +cable number in the CIN data byte of the packet is not less than bNumEmbMIDIJack +for that endpoint, then the host driver assumes virtual cable 0 and does not +report an error. + +Some MIDI devices will always send back exactly wMaxPacketSize bytes on +every endpoint even if only one 4-byte packet is required (e.g., NOTE ON). +These devices send packets with 4 packet bytes 0. This driver ignores all +zero packets without reporting an error. + +# ENUMERATION FAILURES +The host may fail to enumerate a device if it has too many endpoints, if it has +if it has a Standard MS Transfer Bulk Data Endpoint Descriptor (not supported), +if it has a poorly formed descriptor, or if the descriptor is too long for +the host to read the whole thing. diff --git a/src/class/midi/midi.h b/src/class/midi/midi.h index 74dc41749b..e4b2b7bfdb 100644 --- a/src/class/midi/midi.h +++ b/src/class/midi/midi.h @@ -52,8 +52,8 @@ typedef enum typedef enum { - MIDI_CS_ENDPOINT_GENERAL = 0x01 -} midi_cs_endpoint_subtype_t; + MIDI_MS_ENDPOINT_GENERAL = 0x01 +} midi_ms_endpoint_subtype_t; typedef enum { @@ -71,8 +71,8 @@ typedef enum MIDI_CIN_SYSEX_END_1BYTE = 5, // SysEx ends with 1 data, or 1 byte system common message MIDI_CIN_SYSEX_END_2BYTE = 6, // SysEx ends with 2 data MIDI_CIN_SYSEX_END_3BYTE = 7, // SysEx ends with 3 data - MIDI_CIN_NOTE_ON = 8, - MIDI_CIN_NOTE_OFF = 9, + MIDI_CIN_NOTE_ON = 9, + MIDI_CIN_NOTE_OFF = 8, MIDI_CIN_POLY_KEYPRESS = 10, MIDI_CIN_CONTROL_CHANGE = 11, MIDI_CIN_PROGRAM_CHANGE = 12, @@ -106,6 +106,11 @@ enum MIDI_STATUS_SYSREAL_SYSTEM_RESET = 0xFF, }; +enum +{ + MIDI_MAX_DATA_VAL = 0x7F, +}; + /// MIDI Interface Header Descriptor typedef struct TU_ATTR_PACKED { @@ -201,6 +206,17 @@ typedef struct TU_ATTR_PACKED uint8_t iElement; \ } +// This descriptor follows the standard bulk data endpoint descriptor +typedef struct +{ + uint8_t bLength ; ///< Size of this descriptor in bytes (4+bNumEmbMIDIJack) + uint8_t bDescriptorType ; ///< Descriptor Type, must be CS_ENDPOINT + uint8_t bDescriptorSubType ; ///< Descriptor SubType, must be MS_GENERAL + uint8_t bNumEmbMIDIJack; ; ///< Number of embedded MIDI jacks associated with this endpoint + uint8_t baAssocJackID[]; ; ///< A list of associated jacks +} midi_cs_desc_endpoint_t; + + /** @} */ #ifdef __cplusplus diff --git a/src/class/midi/midi_host.c b/src/class/midi/midi_host.c new file mode 100644 index 0000000000..c0f0599bd7 --- /dev/null +++ b/src/class/midi/midi_host.c @@ -0,0 +1,717 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * This file is part of the TinyUSB stack. + */ + +#include "tusb_option.h" + +#if (TUSB_OPT_HOST_ENABLED && CFG_TUH_MIDI) + +#include "host/usbh.h" +#include "host/usbh_classdriver.h" + +#include "midi_host.h" + +//--------------------------------------------------------------------+ +// MACRO CONSTANT TYPEDEF +//--------------------------------------------------------------------+ +#ifndef CFG_TUH_MAX_CABLES + #define CFG_TUH_MAX_CABLES 16 +#endif +#define CFG_TUH_MIDI_RX_BUFSIZE 64 +#define CFG_TUH_MIDI_TX_BUFSIZE 64 +#ifndef CFG_TUH_MIDI_EP_BUFSIZE + #define CFG_TUH_MIDI_EP_BUFSIZE 64 +#endif + +// TODO: refactor to share code with the MIDI Device driver +typedef struct +{ + uint8_t buffer[4]; + uint8_t index; + uint8_t total; +}midi_stream_t; + +typedef struct +{ + uint8_t dev_addr; + uint8_t itf_num; + + uint8_t ep_in; // IN endpoint address + uint8_t ep_out; // OUT endpoint address + uint16_t ep_in_max; // min( CFG_TUH_MIDI_RX_BUFSIZE, wMaxPacketSize of the IN endpoint) + uint16_t ep_out_max; // min( CFG_TUH_MIDI_TX_BUFSIZE, wMaxPacketSize of the OUT endpoint) + + uint8_t num_cables_rx; // IN endpoint CS descriptor bNumEmbMIDIJack value + uint8_t num_cables_tx; // OUT endpoint CS descriptor bNumEmbMIDIJack value + + // For Stream read()/write() API + // Messages are always 4 bytes long, queue them for reading and writing so the + // callers can use the Stream interface with single-byte read/write calls. + midi_stream_t stream_write; + midi_stream_t stream_read; + + /*------------- From this point, data is not cleared by bus reset -------------*/ + // Endpoint FIFOs + tu_fifo_t rx_ff; + tu_fifo_t tx_ff; + + + uint8_t rx_ff_buf[CFG_TUH_MIDI_RX_BUFSIZE]; + uint8_t tx_ff_buf[CFG_TUH_MIDI_TX_BUFSIZE]; + + #if CFG_FIFO_MUTEX + osal_mutex_def_t rx_ff_mutex; + osal_mutex_def_t tx_ff_mutex; + #endif + + // Endpoint Transfer buffer + CFG_TUSB_MEM_ALIGN uint8_t epout_buf[CFG_TUH_MIDI_EP_BUFSIZE]; + CFG_TUSB_MEM_ALIGN uint8_t epin_buf[CFG_TUH_MIDI_EP_BUFSIZE]; + + bool configured; +}midih_interface_t; + +static midih_interface_t _midi_host[CFG_TUH_DEVICE_MAX]; + +static midih_interface_t *get_midi_host(uint8_t dev_addr) +{ + TU_ASSERT(dev_addr >0 && dev_addr <= CFG_TUH_DEVICE_MAX); + return (_midi_host + dev_addr - 1); +} + +//------------- Internal prototypes -------------// +static uint32_t write_flush(uint8_t dev_addr, midih_interface_t* midi); + +//--------------------------------------------------------------------+ +// USBH API +//--------------------------------------------------------------------+ +void midih_init(void) +{ + tu_memclr(&_midi_host, sizeof(_midi_host)); + + // config fifos + for (int inst = 0; inst < CFG_TUH_DEVICE_MAX; inst++) + { + midih_interface_t *p_midi_host = &_midi_host[inst]; + tu_fifo_config(&p_midi_host->rx_ff, p_midi_host->rx_ff_buf, CFG_TUH_MIDI_RX_BUFSIZE, 1, false); // true, true + tu_fifo_config(&p_midi_host->tx_ff, p_midi_host->tx_ff_buf, CFG_TUH_MIDI_TX_BUFSIZE, 1, false); // OBVS. + + #if CFG_FIFO_MUTEX + tu_fifo_config_mutex(&p_midi_host->rx_ff, NULL, osal_mutex_create(&p_midi_host->rx_ff_mutex)); + tu_fifo_config_mutex(&p_midi_host->tx_ff, osal_mutex_create(&p_midi_host->tx_ff_mutex), NULL); + #endif + } +} + +bool midih_xfer_cb(uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes) +{ + midih_interface_t *p_midi_host = get_midi_host(dev_addr); + if ( ep_addr == p_midi_host->ep_in) + { + if (0 == xferred_bytes) + { + return true; // No data to handle + } + + // receive new data if available + uint32_t packets_queued = 0; + if (xferred_bytes) + { + // put in the RX FIFO only non-zero MIDI IN 4-byte packets + uint8_t* buf = p_midi_host->epin_buf; + uint32_t npackets = xferred_bytes / 4; + uint32_t packet_num; + for (packet_num = 0; packet_num < npackets; packet_num++) + { + // some devices send back all zero packets even if there is no data ready + uint32_t packet = (uint32_t)((*buf)<<24) | ((uint32_t)(*(buf+1))<<16) | ((uint32_t)(*(buf+2))<<8) | ((uint32_t)(*(buf+3))); + if (packet != 0) + { + tu_fifo_write_n(&p_midi_host->rx_ff, buf, 4); + ++packets_queued; + TU_LOG3("MIDI RX=%08x\r\n", packet); + } + buf += 4; + } + } + // invoke receive callback if available + if (tuh_midi_rx_cb) + { + tuh_midi_rx_cb(dev_addr, packets_queued); + } + } + else if ( ep_addr == p_midi_host->ep_out ) + { + if (0 == write_flush(dev_addr, p_midi_host)) + { + // If there is no data left, a ZLP should be sent if + // xferred_bytes is multiple of EP size and not zero + if ( !tu_fifo_count(&p_midi_host->tx_ff) && xferred_bytes && (0 == (xferred_bytes % p_midi_host->ep_out_max)) ) + { + if ( usbh_edpt_claim(dev_addr, p_midi_host->ep_out) ) + { + TU_ASSERT(usbh_edpt_xfer(dev_addr, p_midi_host->ep_out, XFER_RESULT_SUCCESS, 0)); + } + } + } + if (tuh_midi_tx_cb) + { + tuh_midi_tx_cb(dev_addr); + } + } + + return true; +} + +void midih_close(uint8_t dev_addr) +{ + midih_interface_t *p_midi_host = get_midi_host(dev_addr); + if (tuh_midi_umount_cb) + tuh_midi_umount_cb(dev_addr, 0); + tu_fifo_clear(&p_midi_host->rx_ff); + tu_fifo_clear(&p_midi_host->tx_ff); + p_midi_host->ep_in = 0; + p_midi_host->ep_in_max = 0; + p_midi_host->ep_out = 0; + p_midi_host->ep_out_max = 0; + p_midi_host->itf_num = 0; + p_midi_host->num_cables_rx = 0; + p_midi_host->num_cables_tx = 0; + p_midi_host->dev_addr = 255; // invalid + p_midi_host->configured = false; + tu_memclr(&p_midi_host->stream_read, sizeof(p_midi_host->stream_read)); + tu_memclr(&p_midi_host->stream_write, sizeof(p_midi_host->stream_write)); +} + +//--------------------------------------------------------------------+ +// Enumeration +//--------------------------------------------------------------------+ +bool midih_open(uint8_t rhport, uint8_t dev_addr, tusb_desc_interface_t const *desc_itf, uint16_t max_len) +{ + (void) rhport; + midih_interface_t *p_midi_host = get_midi_host(dev_addr); + TU_VERIFY(TUSB_CLASS_AUDIO == desc_itf->bInterfaceClass); + // There can be just a MIDI interface or an audio and a MIDI interface. Only open the MIDI interface + uint8_t const *p_desc = (uint8_t const *) desc_itf; + uint16_t len_parsed = 0; + if (AUDIO_SUBCLASS_CONTROL == desc_itf->bInterfaceSubClass) + { + // This driver does not support audio streaming. However, if this is the audio control interface + // there might be a MIDI interface following it. Search through every descriptor until a MIDI + // interface is found or the end of the descriptor is found + while (len_parsed < max_len && (desc_itf->bInterfaceClass != TUSB_CLASS_AUDIO || desc_itf->bInterfaceSubClass != AUDIO_SUBCLASS_MIDI_STREAMING)) + { + len_parsed += desc_itf->bLength; + p_desc = tu_desc_next(p_desc); + desc_itf = (tusb_desc_interface_t const *)p_desc; + } + + TU_VERIFY(TUSB_CLASS_AUDIO == desc_itf->bInterfaceClass); + } + TU_VERIFY(AUDIO_SUBCLASS_MIDI_STREAMING == desc_itf->bInterfaceSubClass); + len_parsed += desc_itf->bLength; + + p_desc = tu_desc_next(p_desc); + TU_LOG1("MIDI opening Interface %u (addr = %u)\r\n", desc_itf->bInterfaceNumber, dev_addr); + // Find out if getting the MIDI class specific interface header or an endpoint descriptor + // or a class-specific endpoint descriptor + // Jack descriptors or element descriptors must follow the cs interface header, + // but this driver does not support devices that contain element descriptors + + // assume it is an interface header + midi_desc_header_t const *p_mdh = (midi_desc_header_t const *)p_desc; + TU_VERIFY((p_mdh->bDescriptorType == TUSB_DESC_CS_INTERFACE && p_mdh->bDescriptorSubType == MIDI_CS_INTERFACE_HEADER) || + (p_mdh->bDescriptorType == TUSB_DESC_CS_ENDPOINT && p_mdh->bDescriptorSubType == MIDI_MS_ENDPOINT_GENERAL) || + p_mdh->bDescriptorType == TUSB_DESC_ENDPOINT); + + uint8_t prev_ep_addr = 0; // the CS endpoint descriptor is associated with the previous endpoint descrptor + p_midi_host->itf_num = desc_itf->bInterfaceNumber; + tusb_desc_endpoint_t const* in_desc = NULL; + tusb_desc_endpoint_t const* out_desc = NULL; + while (len_parsed < max_len) + { + TU_VERIFY((p_mdh->bDescriptorType == TUSB_DESC_CS_INTERFACE) || + (p_mdh->bDescriptorType == TUSB_DESC_CS_ENDPOINT && p_mdh->bDescriptorSubType == MIDI_MS_ENDPOINT_GENERAL) || + p_mdh->bDescriptorType == TUSB_DESC_ENDPOINT); + + if (p_mdh->bDescriptorType == TUSB_DESC_CS_INTERFACE) { + // The USB host doesn't really need this information unless it uses + // the string descriptor for a jack or Element + + // assume it is an input jack + midi_desc_in_jack_t const *p_mdij = (midi_desc_in_jack_t const *)p_desc; + if (p_mdij->bDescriptorSubType == MIDI_CS_INTERFACE_HEADER) + { + TU_LOG2("Found MIDI Interface Header\r\b"); + } + else if (p_mdij->bDescriptorSubType == MIDI_CS_INTERFACE_IN_JACK) + { + // Then it is an in jack. + TU_LOG2("Found in jack\r\n"); + } + else if (p_mdij->bDescriptorSubType == MIDI_CS_INTERFACE_OUT_JACK) + { + // then it is an out jack + TU_LOG2("Found out jack\r\n"); + } + else if (p_mdij->bDescriptorSubType == MIDI_CS_INTERFACE_ELEMENT) + { + // the it is an element; + TU_LOG2("Found element\r\n"); + } + else + { + TU_LOG2("Unknown CS Interface sub-type %u\r\n", p_mdij->bDescriptorSubType); + TU_VERIFY(false); // unknown CS Interface sub-type + } + len_parsed += p_mdij->bLength; + } + else if (p_mdh->bDescriptorType == TUSB_DESC_CS_ENDPOINT) + { + TU_LOG2("found CS_ENDPOINT Descriptor for %u\r\n", prev_ep_addr); + TU_VERIFY(prev_ep_addr != 0); + // parse out the mapping between the device's embedded jacks and the endpoints + // Each embedded IN jack is assocated with an OUT endpoint + midi_cs_desc_endpoint_t const* p_csep = (midi_cs_desc_endpoint_t const*)p_mdh; + if (tu_edpt_dir(prev_ep_addr) == TUSB_DIR_OUT) + { + TU_VERIFY(p_midi_host->ep_out == prev_ep_addr); + TU_VERIFY(p_midi_host->num_cables_tx == 0); + p_midi_host->num_cables_tx = p_csep->bNumEmbMIDIJack; + } + else + { + TU_VERIFY(p_midi_host->ep_in == prev_ep_addr); + TU_VERIFY(p_midi_host->num_cables_rx == 0); + p_midi_host->num_cables_rx = p_csep->bNumEmbMIDIJack; + } + len_parsed += p_csep->bLength; + prev_ep_addr = 0; + } + else if (p_mdh->bDescriptorType == TUSB_DESC_ENDPOINT) { + // parse out the bulk endpoint info + tusb_desc_endpoint_t const *p_ep = (tusb_desc_endpoint_t const *)p_mdh; + TU_LOG2("found ENDPOINT Descriptor for %u\r\n", p_ep->bEndpointAddress); + if (tu_edpt_dir(p_ep->bEndpointAddress) == TUSB_DIR_OUT) + { + TU_VERIFY(p_midi_host->ep_out == 0); + TU_VERIFY(p_midi_host->num_cables_tx == 0); + p_midi_host->ep_out = p_ep->bEndpointAddress; + p_midi_host->ep_out_max = p_ep->wMaxPacketSize; + if (p_midi_host->ep_out_max > CFG_TUH_MIDI_TX_BUFSIZE) + p_midi_host->ep_out_max = CFG_TUH_MIDI_TX_BUFSIZE; + prev_ep_addr = p_midi_host->ep_out; + out_desc = p_ep; + } + else + { + TU_VERIFY(p_midi_host->ep_in == 0); + TU_VERIFY(p_midi_host->num_cables_rx == 0); + p_midi_host->ep_in = p_ep->bEndpointAddress; + p_midi_host->ep_in_max = p_ep->wMaxPacketSize; + if (p_midi_host->ep_in_max > CFG_TUH_MIDI_RX_BUFSIZE) + p_midi_host->ep_in_max = CFG_TUH_MIDI_RX_BUFSIZE; + prev_ep_addr = p_midi_host->ep_in; + in_desc = p_ep; + } + len_parsed += p_mdh->bLength; + } + p_desc = tu_desc_next(p_desc); + p_mdh = (midi_desc_header_t const *)p_desc; + } + TU_VERIFY((p_midi_host->ep_out != 0 && p_midi_host->num_cables_tx != 0) || + (p_midi_host->ep_in != 0 && p_midi_host->num_cables_rx != 0)); + TU_LOG1("MIDI descriptor parsed successfully\r\n"); + + if (in_desc) + { + TU_ASSERT(usbh_edpt_open(rhport, dev_addr, in_desc)); + // Some devices always return exactly the request length so transfers won't complete + // unless you assume every transfer is the last one. + usbh_edpt_force_last_buffer(dev_addr, p_midi_host->ep_in, true); + } + if (out_desc) + { + TU_ASSERT(usbh_edpt_open(rhport, dev_addr, out_desc)); + } + p_midi_host->dev_addr = dev_addr; + + if (tuh_midi_mount_cb) + { + tuh_midi_mount_cb(dev_addr, p_midi_host->ep_in, p_midi_host->ep_out, p_midi_host->num_cables_rx, p_midi_host->num_cables_tx); + } + return true; +} + +bool tuh_midi_configured(uint8_t dev_addr) +{ + midih_interface_t *p_midi_host = get_midi_host(dev_addr); + return p_midi_host->configured; +} + +bool midih_set_config(uint8_t dev_addr, uint8_t itf_num) +{ + (void) itf_num; + midih_interface_t *p_midi_host = get_midi_host(dev_addr); + p_midi_host->configured = true; + + // TODO I don't think there are any special config things to do for MIDI + + return true; +} + +//--------------------------------------------------------------------+ +// Stream API +//--------------------------------------------------------------------+ +static uint32_t write_flush(uint8_t dev_addr, midih_interface_t* midi) +{ + // No data to send + if ( !tu_fifo_count(&midi->tx_ff) ) return 0; + + // skip if previous transfer not complete + TU_VERIFY( usbh_edpt_claim(dev_addr, midi->ep_out) ); + + uint16_t count = tu_fifo_read_n(&midi->tx_ff, midi->epout_buf, midi->ep_out_max); + + if (count) + { + TU_ASSERT( usbh_edpt_xfer(dev_addr, midi->ep_out, midi->epout_buf, count), 0 ); + return count; + }else + { + // Release endpoint since we don't make any transfer + usbh_edpt_release(dev_addr, midi->ep_out); + return 0; + } +} + +bool tuh_midi_read_poll( uint8_t dev_addr ) +{ + midih_interface_t *p_midi_host = get_midi_host(dev_addr); + // MIDI bulk endpoints are shared with the control endpoints. None can be busy before we start a transfer + bool control_edpt_not_busy = !usbh_edpt_busy(dev_addr,0) && !usbh_edpt_busy(dev_addr,0x80); + bool out_edpt_not_busy = true; + bool result = false; + if (p_midi_host->num_cables_tx > 0) + out_edpt_not_busy = !usbh_edpt_busy(p_midi_host->dev_addr, p_midi_host->ep_out); + if (control_edpt_not_busy && out_edpt_not_busy) + { + bool in_edpt_not_busy = !usbh_edpt_busy(dev_addr, p_midi_host->ep_in); + if (in_edpt_not_busy) + { + TU_LOG2("Requesting poll IN endpoint %d\r\n", p_midi_host->ep_in); + TU_ASSERT(usbh_edpt_xfer(p_midi_host->dev_addr, p_midi_host->ep_in, _midi_host->epin_buf, _midi_host->ep_in_max), 0); + result = true; + } + else + { + // Maybe the IN endpoint is only busy because the RP2040 host hardware + // is retrying a NAK'd IN transfer forever. Try aborting the NAK'd + // transfer to allow other transfers to happen on the one shared + // epx endpoint. + usbh_edpt_clear_in_on_nak(p_midi_host->dev_addr, p_midi_host->ep_in); + } + } + return result; +} + +uint32_t tuh_midi_stream_write (uint8_t dev_addr, uint8_t cable_num, uint8_t const* buffer, uint32_t bufsize) +{ + midih_interface_t *p_midi_host = get_midi_host(dev_addr); + TU_VERIFY(cable_num < p_midi_host->num_cables_tx); + midi_stream_t *stream = &p_midi_host->stream_write; + + uint32_t i = 0; + while ( (i < bufsize) && (tu_fifo_remaining(&p_midi_host->tx_ff) >= 4) ) + { + uint8_t const data = buffer[i]; + i++; + if (data >= MIDI_STATUS_SYSREAL_TIMING_CLOCK) + { + // real-time messages need to be sent right away + midi_stream_t streamrt; + streamrt.buffer[0] = MIDI_CIN_SYSEX_END_1BYTE; + streamrt.buffer[1] = data; + streamrt.index = 2; + streamrt.total = 2; + uint16_t const count = tu_fifo_write_n(&p_midi_host->tx_ff, streamrt.buffer, 4); + // FIFO overflown, since we already check fifo remaining. It is probably race condition + TU_ASSERT(count == 4, i); + } + else if ( stream->index == 0 ) + { + //------------- New event packet -------------// + + uint8_t const msg = data >> 4; + + stream->index = 2; + stream->buffer[1] = data; + + // Check to see if we're still in a SysEx transmit. + if ( stream->buffer[0] == MIDI_CIN_SYSEX_START ) + { + if ( data == MIDI_STATUS_SYSEX_END ) + { + stream->buffer[0] = MIDI_CIN_SYSEX_END_1BYTE; + stream->total = 2; + } + else + { + stream->total = 4; + } + } + else if ( (msg >= 0x8 && msg <= 0xB) || msg == 0xE ) + { + // Channel Voice Messages + stream->buffer[0] = (cable_num << 4) | msg; + stream->total = 4; + } + else if ( msg == 0xC || msg == 0xD) + { + // Channel Voice Messages, two-byte variants (Program Change and Channel Pressure) + stream->buffer[0] = (cable_num << 4) | msg; + stream->total = 3; + } + else if ( msg == 0xf ) + { + // System message + if ( data == MIDI_STATUS_SYSEX_START ) + { + stream->buffer[0] = MIDI_CIN_SYSEX_START; + stream->total = 4; + } + else if ( data == MIDI_STATUS_SYSCOM_TIME_CODE_QUARTER_FRAME || data == MIDI_STATUS_SYSCOM_SONG_SELECT ) + { + stream->buffer[0] = MIDI_CIN_SYSCOM_2BYTE; + stream->total = 3; + } + else if ( data == MIDI_STATUS_SYSCOM_SONG_POSITION_POINTER ) + { + stream->buffer[0] = MIDI_CIN_SYSCOM_3BYTE; + stream->total = 4; + } + else + { + stream->buffer[0] = MIDI_CIN_SYSEX_END_1BYTE; + stream->total = 2; + } + } + else + { + // Pack individual bytes if we don't support packing them into words. + stream->buffer[0] = cable_num << 4 | 0xf; + stream->buffer[2] = 0; + stream->buffer[3] = 0; + stream->index = 2; + stream->total = 2; + } + } + else + { + //------------- On-going (buffering) packet -------------// + + TU_ASSERT(stream->index < 4, i); + stream->buffer[stream->index] = data; + stream->index++; + // See if this byte ends a SysEx. + if ( stream->buffer[0] == MIDI_CIN_SYSEX_START && data == MIDI_STATUS_SYSEX_END ) + { + stream->buffer[0] = MIDI_CIN_SYSEX_START + (stream->index - 1); + stream->total = stream->index; + } + } + + // Send out packet + if ( stream->index >= 2 && stream->index == stream->total ) + { + // zeroes unused bytes + for(uint8_t idx = stream->total; idx < 4; idx++) stream->buffer[idx] = 0; + TU_LOG3_MEM(stream->buffer, 4, 2); + + uint16_t const count = tu_fifo_write_n(&p_midi_host->tx_ff, stream->buffer, 4); + + // complete current event packet, reset stream + stream->index = 0; + stream->total = 0; + + // FIFO overflown, since we already check fifo remaining. It is probably race condition + TU_ASSERT(count == 4, i); + } + } + return i; +} + +uint32_t tuh_midi_stream_flush( uint8_t dev_addr ) +{ + midih_interface_t *p_midi_host = get_midi_host(dev_addr); + bool control_edpt_not_busy = !usbh_edpt_busy(dev_addr,0) && !usbh_edpt_busy(dev_addr,0x80); + bool in_edpt_not_busy = true; + + uint32_t bytes_flushed = 0; + if (p_midi_host->num_cables_rx > 0) + in_edpt_not_busy = !usbh_edpt_busy(dev_addr, p_midi_host->ep_in); + if (control_edpt_not_busy && in_edpt_not_busy && !usbh_edpt_busy(p_midi_host->dev_addr, p_midi_host->ep_out)) + { + bytes_flushed = write_flush(dev_addr, p_midi_host); + } + return bytes_flushed; +} +//--------------------------------------------------------------------+ +// Helper +//--------------------------------------------------------------------+ +uint8_t tuh_midih_get_num_tx_cables (uint8_t dev_addr) +{ + midih_interface_t *p_midi_host = get_midi_host(dev_addr); + TU_VERIFY(p_midi_host->ep_out != 0); // returns 0 if fails + return p_midi_host->num_cables_tx; +} + +uint8_t tuh_midih_get_num_rx_cables (uint8_t dev_addr) +{ + midih_interface_t *p_midi_host = get_midi_host(dev_addr); + TU_VERIFY(p_midi_host->ep_in != 0); // returns 0 if fails + return p_midi_host->num_cables_rx; +} + +uint32_t tuh_midi_stream_read (uint8_t dev_addr, uint8_t *p_cable_num, uint8_t *p_buffer, uint16_t bufsize) +{ + midih_interface_t *p_midi_host = get_midi_host(dev_addr); + uint32_t bytes_buffered = 0; + TU_ASSERT(p_cable_num); + TU_ASSERT(p_buffer); + TU_ASSERT(bufsize); + uint8_t one_byte; + if (!tu_fifo_peek(&p_midi_host->rx_ff, &one_byte)) + { + return 0; + } + *p_cable_num = (one_byte >> 4) & 0xf; + uint32_t nread = tu_fifo_read_n(&p_midi_host->rx_ff, p_midi_host->stream_read.buffer, 4); + static uint16_t cable_sysex_in_progress; // bit i is set if received MIDI_STATUS_SYSEX_START but not MIDI_STATUS_SYSEX_END + while (nread == 4 && bytes_buffered < bufsize) + { + *p_cable_num=(p_midi_host->stream_read.buffer[0] >> 4) & 0x0f; + uint8_t bytes_to_add_to_stream = 0; + if (*p_cable_num < p_midi_host->num_cables_rx) + { + // ignore the CIN field; too many devices out there encode this wrong + uint8_t status = p_midi_host->stream_read.buffer[1]; + uint16_t cable_mask = 1 << *p_cable_num; + if (status <= MIDI_MAX_DATA_VAL || status == MIDI_STATUS_SYSEX_START) + { + if (status == MIDI_STATUS_SYSEX_START) + { + cable_sysex_in_progress |= cable_mask; + } + // only add the packet if a sysex message is in progress + if (cable_sysex_in_progress & cable_mask) + { + ++bytes_to_add_to_stream; + uint8_t idx; + for (idx = 2; idx < 4; idx++) + { + if (p_midi_host->stream_read.buffer[idx] <= MIDI_MAX_DATA_VAL) + { + ++bytes_to_add_to_stream; + } + else if (p_midi_host->stream_read.buffer[idx] == MIDI_STATUS_SYSEX_END) + { + ++bytes_to_add_to_stream; + cable_sysex_in_progress &= ~cable_mask; + idx = 4; // force the loop to exit; I hate break statements in loops + } + } + } + } + else if (status < MIDI_STATUS_SYSEX_START) + { + // then it is a channel message either three bytes or two + uint8_t fake_cin = (status & 0xf0) >> 4; + switch (fake_cin) + { + case MIDI_CIN_NOTE_OFF: + case MIDI_CIN_NOTE_ON: + case MIDI_CIN_POLY_KEYPRESS: + case MIDI_CIN_CONTROL_CHANGE: + case MIDI_CIN_PITCH_BEND_CHANGE: + bytes_to_add_to_stream = 3; + break; + case MIDI_CIN_PROGRAM_CHANGE: + case MIDI_CIN_CHANNEL_PRESSURE: + bytes_to_add_to_stream = 2; + break; + default: + break; // Should not get this + } + cable_sysex_in_progress &= ~cable_mask; + } + else if (status < MIDI_STATUS_SYSREAL_TIMING_CLOCK) + { + switch (status) + { + case MIDI_STATUS_SYSCOM_TIME_CODE_QUARTER_FRAME: + case MIDI_STATUS_SYSCOM_SONG_SELECT: + bytes_to_add_to_stream = 2; + break; + case MIDI_STATUS_SYSCOM_SONG_POSITION_POINTER: + bytes_to_add_to_stream = 3; + break; + case MIDI_STATUS_SYSCOM_TUNE_REQUEST: + case MIDI_STATUS_SYSEX_END: + bytes_to_add_to_stream = 1; + break; + default: + break; + cable_sysex_in_progress &= ~cable_mask; + } + } + else + { + // Real-time message: can be inserted into a sysex message, + // so do don't clear cable_sysex_in_progress bit + bytes_to_add_to_stream = 1; + } + } + uint8_t idx; + for (idx = 1; idx <= bytes_to_add_to_stream; idx++) + { + *p_buffer++ = p_midi_host->stream_read.buffer[idx]; + } + bytes_buffered += bytes_to_add_to_stream; + nread = 0; + if (tu_fifo_peek(&p_midi_host->rx_ff, &one_byte)) + { + uint8_t new_cable = (one_byte >> 4) & 0xf; + if (new_cable == *p_cable_num) + { + // still on the same cable. Continue reading the stream + nread = tu_fifo_read_n(&p_midi_host->rx_ff, p_midi_host->stream_read.buffer, 4); + } + } + } + + return bytes_buffered; +} +#endif diff --git a/src/class/midi/midi_host.h b/src/class/midi/midi_host.h new file mode 100644 index 0000000000..870e27c6f6 --- /dev/null +++ b/src/class/midi/midi_host.h @@ -0,0 +1,118 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Ha Thach (tinyusb.org) + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + * + * This file is part of the TinyUSB stack. + */ + +#ifndef _TUSB_MIDI_HOST_H_ +#define _TUSB_MIDI_HOST_H_ + +#include "class/audio/audio.h" +#include "midi.h" + +#ifdef __cplusplus + extern "C" { +#endif + +//--------------------------------------------------------------------+ +// Class Driver Configuration +//--------------------------------------------------------------------+ + +// TODO Highspeed bulk transfer can be up to 512 bytes +#ifndef CFG_TUH_HID_EPIN_BUFSIZE +#define CFG_TUH_HID_EPIN_BUFSIZE 64 +#endif + +#ifndef CFG_TUH_HID_EPOUT_BUFSIZE +#define CFG_TUH_HID_EPOUT_BUFSIZE 64 +#endif + + +//--------------------------------------------------------------------+ +// Application API (Single Interface) +//--------------------------------------------------------------------+ +bool tuh_midi_configured (uint8_t dev_addr); +uint32_t tuh_midi_available (uint8_t dev_addr); + +// return the number of virtual midi cables on the device's OUT endpoint +uint8_t tuh_midih_get_num_tx_cables (uint8_t dev_addr); + +// return the number of virtual midi cables on the device's IN endpoint +uint8_t tuh_midih_get_num_rx_cables (uint8_t dev_addr); + +// request available data from the device. tuh_midi_message_received_cb() will +// be called if the device has any data to send. Otherwise, the device will +// respond NAK. This function blocks until the transfer completes or the +// devices sends NAK. +// This function will return false if the hardware is busy. +bool tuh_midi_read_poll( uint8_t dev_addr ); + +// Queue a message to the device. The application +// must call tuh_midi_stream_flush to actually have the +// data go out. +uint32_t tuh_midi_stream_write (uint8_t dev_addr, uint8_t cable_num, uint8_t const* p_buffer, uint32_t bufsize); + +// Send any queued packets to the device if the host hardware is able to do it +// Returns the number of bytes flushed to the host hardware or 0 if +// the host hardware is busy or there is nothing in queue to send. +uint32_t tuh_midi_stream_flush( uint8_t dev_addr); + +// Get the MIDI stream from the device. Set the value pointed +// to by p_cable_num to the MIDI cable number intended to receive it. +// The MIDI stream will be stored in the buffer pointed to by p_buffer. +// Return the number of bytes added to the buffer. +// Note that this function ignores the CIN field of the MIDI packet +// because a number of commercial devices out there do not encode +// it properly. +uint32_t tuh_midi_stream_read (uint8_t dev_addr, uint8_t *p_cable_num, uint8_t *p_buffer, uint16_t bufsize); + +//--------------------------------------------------------------------+ +// Internal Class Driver API +//--------------------------------------------------------------------+ +void midih_init (void); +bool midih_open (uint8_t rhport, uint8_t dev_addr, tusb_desc_interface_t const *desc_itf, uint16_t max_len); +bool midih_set_config (uint8_t dev_addr, uint8_t itf_num); +bool midih_xfer_cb (uint8_t dev_addr, uint8_t ep_addr, xfer_result_t result, uint32_t xferred_bytes); +void midih_close (uint8_t dev_addr); + +//--------------------------------------------------------------------+ +// Callbacks (Weak is optional) +//--------------------------------------------------------------------+ + +// Invoked when device with MIDI interface is mounted. +// If the MIDI host application requires MIDI IN, it should requst an +// IN transfer here. The device will likely NAK this transfer. How the driver +// handles the NAK is hardware dependent. +TU_ATTR_WEAK void tuh_midi_mount_cb(uint8_t dev_addr, uint8_t in_ep, uint8_t out_ep, uint8_t num_cables_rx, uint16_t num_cables_tx); + +// Invoked when device with MIDI interface is un-mounted +// For now, the instance parameter is always 0 and can be ignored +TU_ATTR_WEAK void tuh_midi_umount_cb(uint8_t dev_addr, uint8_t instance); + +TU_ATTR_WEAK void tuh_midi_rx_cb(uint8_t dev_addr, uint32_t num_packets); +TU_ATTR_WEAK void tuh_midi_tx_cb(uint8_t dev_addr); +#ifdef __cplusplus +} +#endif + +#endif /* _TUSB_MIDI_HOST_H_ */ diff --git a/src/device/usbd.h b/src/device/usbd.h index ec34817fa4..99c3ce1818 100644 --- a/src/device/usbd.h +++ b/src/device/usbd.h @@ -311,7 +311,7 @@ TU_ATTR_WEAK bool tud_vendor_control_xfer_cb(uint8_t rhport, uint8_t stage, tusb /* Endpoint: Note Audio v1.0's endpoint has 9 bytes instead of 7 */\ 9, TUSB_DESC_ENDPOINT, _epout, TUSB_XFER_BULK, U16_TO_U8S_LE(_epsize), 0, 0, 0, \ /* MS Endpoint (connected to embedded jack) */\ - (uint8_t)(4 + (_numcables)), TUSB_DESC_CS_ENDPOINT, MIDI_CS_ENDPOINT_GENERAL, _numcables + (uint8_t)(4 + (_numcables)), TUSB_DESC_CS_ENDPOINT, MIDI_MS_ENDPOINT_GENERAL, _numcables // Length of template descriptor (88 bytes) #define TUD_MIDI_DESC_LEN (TUD_MIDI_DESC_HEAD_LEN + TUD_MIDI_DESC_JACK_LEN + TUD_MIDI_DESC_EP_LEN(1) * 2) diff --git a/src/host/hcd.h b/src/host/hcd.h index eb53d2e80e..70b1ea15e6 100644 --- a/src/host/hcd.h +++ b/src/host/hcd.h @@ -148,7 +148,8 @@ bool hcd_setup_send(uint8_t rhport, uint8_t dev_addr, uint8_t const setup_packet bool hcd_edpt_open(uint8_t rhport, uint8_t dev_addr, tusb_desc_endpoint_t const * ep_desc); bool hcd_edpt_xfer(uint8_t rhport, uint8_t dev_addr, uint8_t ep_addr, uint8_t * buffer, uint16_t buflen); bool hcd_edpt_clear_stall(uint8_t dev_addr, uint8_t ep_addr); - +TU_ATTR_WEAK void hcd_edpt_force_last_buffer(uint8_t dev_addr, uint8_t ep_addr, bool force); +TU_ATTR_WEAK void hcd_edpt_clear_in_on_nak(uint8_t dev_addr, uint8_t ep_addr); //--------------------------------------------------------------------+ // USBH implemented API //--------------------------------------------------------------------+ diff --git a/src/host/usbh.c b/src/host/usbh.c index b8439addce..05a9d3397a 100644 --- a/src/host/usbh.c +++ b/src/host/usbh.c @@ -171,6 +171,17 @@ static usbh_class_driver_t const usbh_class_drivers[] = }, #endif + #if CFG_TUH_MIDI + { + DRIVER_NAME("MIDI") + .init = midih_init, + .open = midih_open, + .set_config = midih_set_config, + .xfer_cb = midih_xfer_cb, + .close = midih_close + }, + #endif + #if CFG_TUH_HUB { DRIVER_NAME("HUB") @@ -1199,6 +1210,15 @@ bool usbh_edpt_busy(uint8_t dev_addr, uint8_t ep_addr) return dev->ep_status[epnum][dir].busy; } +void usbh_edpt_force_last_buffer(uint8_t dev_addr, uint8_t ep_addr, bool force) +{ + if (hcd_edpt_force_last_buffer) + hcd_edpt_force_last_buffer(dev_addr, ep_addr, force); +} - +void usbh_edpt_clear_in_on_nak(uint8_t dev_addr, uint8_t ep_addr) +{ + if (hcd_edpt_clear_in_on_nak) + hcd_edpt_clear_in_on_nak(dev_addr, ep_addr); +} #endif diff --git a/src/host/usbh.h b/src/host/usbh.h index 8411cad283..2156b2597c 100644 --- a/src/host/usbh.h +++ b/src/host/usbh.h @@ -81,6 +81,13 @@ static inline bool tuh_ready(uint8_t dev_addr) // Carry out control transfer bool tuh_control_xfer (uint8_t dev_addr, tusb_control_request_t const* request, void* buffer, tuh_control_complete_cb_t complete_cb); +// For bulk transfer devices that might not properly signal the end of a transfer +void usbh_edpt_force_last_buffer(uint8_t dev_addr, uint8_t ep_addr, bool force); + +// Clear IN endpoint busy due to NAK if possible by sending +// a transfer complete message with 0 length data +void usbh_edpt_clear_in_on_nak(uint8_t dev_addr, uint8_t ep_addr); + //--------------------------------------------------------------------+ // APPLICATION CALLBACK //--------------------------------------------------------------------+ diff --git a/src/portable/raspberrypi/rp2040/hcd_rp2040.c b/src/portable/raspberrypi/rp2040/hcd_rp2040.c index 3e80dd8720..f9418be08d 100644 --- a/src/portable/raspberrypi/rp2040/hcd_rp2040.c +++ b/src/portable/raspberrypi/rp2040/hcd_rp2040.c @@ -55,6 +55,9 @@ static_assert(PICO_USB_HOST_INTERRUPT_ENDPOINTS <= USB_MAX_ENDPOINTS, ""); static struct hw_endpoint ep_pool[1 + PICO_USB_HOST_INTERRUPT_ENDPOINTS]; #define epx (ep_pool[0]) +// The epx endpoint is shared among control, bulk and isochronous endpoints +struct hw_endpoint *_current_epx_endpoint = &epx; + #define usb_hw_set hw_set_alias(usb_hw) #define usb_hw_clear hw_clear_alias(usb_hw) @@ -64,6 +67,10 @@ enum { USB_SIE_CTRL_PULLDOWN_EN_BITS | USB_SIE_CTRL_EP0_INT_1BUF_BITS }; +// See hcd_edpt_clear_in_on_nak() for an explanation of this hack on top of a hack +#define SKIP_CLEAR_IN_ON_NAK_COUNT 20 +static uint8_t skip_hcd_edpt_clear_in_on_nak = SKIP_CLEAR_IN_ON_NAK_COUNT; // for some reason doing this algorithm right away doesn't work + static struct hw_endpoint *get_dev_ep(uint8_t dev_addr, uint8_t ep_addr) { uint8_t num = tu_edpt_number(ep_addr); @@ -83,6 +90,16 @@ static inline uint8_t dev_speed(void) return (usb_hw->sie_status & USB_SIE_STATUS_SPEED_BITS) >> USB_SIE_STATUS_SPEED_LSB; } +static inline void clear_nak_received(void) +{ + usb_hw_clear->sie_status = USB_SIE_STATUS_NAK_REC_BITS; +} + +static inline bool nak_received() +{ + return ((usb_hw->sie_status & USB_SIE_STATUS_NAK_REC_BITS) != 0); +} + static bool need_pre(uint8_t dev_addr) { // If this device is different to the speed of the root device @@ -97,6 +114,7 @@ static void hw_xfer_complete(struct hw_endpoint *ep, xfer_result_t xfer_result) uint8_t ep_addr = ep->ep_addr; uint xferred_len = ep->xferred_len; hw_endpoint_reset_transfer(ep); + hcd_event_xfer_complete(dev_addr, ep_addr, xferred_len, xfer_result, true); } @@ -120,7 +138,7 @@ static void hw_handle_buff_status(void) if (remaining_buffers & bit) { remaining_buffers &= ~bit; - struct hw_endpoint *ep = &epx; + struct hw_endpoint *ep = _current_epx_endpoint; uint32_t ep_ctrl = *ep->endpoint_control; if (ep_ctrl & EP_CTRL_DOUBLE_BUFFERED_BITS) @@ -213,7 +231,7 @@ static void hcd_rp2040_irq(void) if (status & USB_INTS_STALL_BITS) { - // We have rx'd a stall from the device + // We have rx'd a stall or a NAK from the device pico_trace("Stall REC\n"); handled |= USB_INTS_STALL_BITS; usb_hw_clear->sie_status = USB_SIE_STATUS_STALL_REC_BITS; @@ -255,6 +273,21 @@ static struct hw_endpoint *_next_free_interrupt_ep(void) return ep; } +static struct hw_endpoint *_next_free_non_interrupt_ep(void) +{ + struct hw_endpoint *ep = NULL; + for (uint i = TU_ARRAY_SIZE(ep_pool)-1; i > 0 ; i--) + { + ep = &ep_pool[i]; + if (!ep->configured) + { + // Will be configured by _hw_endpoint_init / _hw_endpoint_allocate + return ep; + } + } + return ep; +} + static struct hw_endpoint *_hw_endpoint_allocate(uint8_t transfer_type) { struct hw_endpoint *ep = NULL; @@ -272,6 +305,15 @@ static struct hw_endpoint *_hw_endpoint_allocate(uint8_t transfer_type) // etc ep->hw_data_buf = &usbh_dpram->epx_data[64 * (ep->interrupt_num + 2)]; } + else if (transfer_type == TUSB_XFER_BULK || transfer_type == TUSB_XFER_ISOCHRONOUS) + { + ep = _next_free_non_interrupt_ep(); + pico_info("Allocated non-interrupt ep\n"); + assert(ep); + ep->buffer_control = &usbh_dpram->epx_buf_ctrl; + ep->endpoint_control = &usbh_dpram->epx_ctrl; + ep->hw_data_buf = &usbh_dpram->epx_data[0]; + } else { ep = &epx; @@ -399,6 +441,9 @@ tusb_speed_t hcd_port_speed_get(uint8_t rhport) // TODO: Should enumval this register switch (dev_speed()) { + case 0: + // device is disconnected; we shouldn't panic + return TUSB_SPEED_INVALID; case 1: return TUSB_SPEED_LOW; case 2: @@ -416,7 +461,7 @@ void hcd_device_close(uint8_t rhport, uint8_t dev_addr) (void) rhport; if (dev_addr == 0) return; - + skip_hcd_edpt_clear_in_on_nak = SKIP_CLEAR_IN_ON_NAK_COUNT; for (size_t i = 1; i < TU_ARRAY_SIZE(ep_pool); i++) { hw_endpoint_t* ep = &ep_pool[i]; @@ -478,12 +523,43 @@ bool hcd_edpt_open(uint8_t rhport, uint8_t dev_addr, tusb_desc_endpoint_t const return true; } +// The epx register is shared among endpoints. Set it up with the parameters stored in epx register +static void _hw_endpoint_reinit_epx(struct hw_endpoint *ep) +{ + // Already has data buffer, endpoint control, and buffer control allocated at this point + assert(ep->endpoint_control); + assert(ep->buffer_control); + assert(ep->hw_data_buf); + assert(ep->configured); + pico_trace("hw_endpoint_init dev %d ep %d %s xfer %d\n", ep->dev_addr, tu_edpt_number(ep->ep_addr), ep_dir_string[tu_edpt_dir(ep->ep_addr)], ep->transfer_type); + pico_trace("dev %d ep %d %s setup buffer @ 0x%p\n", ep->dev_addr, tu_edpt_number(ep->ep_addr), ep_dir_string[tu_edpt_dir(ep->ep_addr)], ep->hw_data_buf); + uint dpram_offset = hw_data_offset(ep->hw_data_buf); + // Bits 0-5 should be 0 + assert(!(dpram_offset & 0b111111)); + + // Fill in endpoint control register with buffer offset + uint32_t ep_reg = EP_CTRL_ENABLE_BITS + | EP_CTRL_INTERRUPT_PER_BUFFER + | (ep->transfer_type << EP_CTRL_BUFFER_TYPE_LSB) + | dpram_offset; + + *ep->endpoint_control = ep_reg; + pico_trace("endpoint control (0x%p) <- 0x%x\n", ep->endpoint_control, ep_reg); + if (nak_received()) + clear_nak_received(); + if (tu_edpt_dir(ep->ep_addr) == TUSB_DIR_IN) + { + uint32_t nak_poll_delay = 100; // increase the spacing between NAK auto-retries + usb_hw->nak_poll = (nak_poll_delay << USB_NAK_POLL_DELAY_FS_LSB) | (nak_poll_delay << USB_NAK_POLL_DELAY_LS_LSB); + } +} + bool hcd_edpt_xfer(uint8_t rhport, uint8_t dev_addr, uint8_t ep_addr, uint8_t * buffer, uint16_t buflen) { (void) rhport; pico_trace("hcd_edpt_xfer dev_addr %d, ep_addr 0x%x, len %d\n", dev_addr, ep_addr, buflen); - + uint8_t const ep_num = tu_edpt_number(ep_addr); tusb_dir_t const ep_dir = tu_edpt_dir(ep_addr); @@ -491,15 +567,44 @@ bool hcd_edpt_xfer(uint8_t rhport, uint8_t dev_addr, uint8_t ep_addr, uint8_t * struct hw_endpoint *ep = get_dev_ep(dev_addr, ep_addr); assert(ep); - // Control endpoint can change direction 0x00 <-> 0x80 - if ( ep_addr != ep->ep_addr ) + if ( ep_num == 0 ) { - assert(ep_num == 0); + // exp buffers might not have been used for endpoint 0 or control endpoint might have changed direction 0x00 <-> 0x80 + if ( _current_epx_endpoint != &epx || ep_addr != ep->ep_addr ) + { + // Re-init endpoint + _hw_endpoint_init(ep, dev_addr, ep_addr, ep->wMaxPacketSize, ep->transfer_type, 0); + } + _current_epx_endpoint = &epx; + } - // Direction has flipped on endpoint control so re init it but with same properties - _hw_endpoint_init(ep, dev_addr, ep_addr, ep->wMaxPacketSize, ep->transfer_type, 0); - } + bool is_bulk_or_isochronous = (ep != &epx && (ep->transfer_type == TUSB_XFER_BULK || ep->transfer_type == TUSB_XFER_ISOCHRONOUS)); + // the epx endpoint is shared among control, bulk and isochronous endpoints + if ( is_bulk_or_isochronous ) + { + if (ep != _current_epx_endpoint) + { + _hw_endpoint_reinit_epx(ep); + } + _current_epx_endpoint = ep; + hw_endpoint_xfer_start(ep, buffer, buflen); + // That has set up buffer control, endpoint control etc + // for host we have to initiate the transfer + usb_hw->dev_addr_ctrl = dev_addr | (ep_num << USB_ADDR_ENDP_ENDPOINT_LSB); + + uint32_t flags = SIE_CTRL_BASE | + (ep_dir ? USB_SIE_CTRL_RECEIVE_DATA_BITS : USB_SIE_CTRL_SEND_DATA_BITS); + // Set pre if we are a low speed device on full speed hub + flags |= need_pre(dev_addr) ? USB_SIE_CTRL_PREAMBLE_EN_BITS : 0; + // Set up the hardware with all flags except the start bit + usb_hw->sie_ctrl = flags; + // Now start the transaction; if you don't do this in two parts, + // and the host has switched direction, sometimes the transaction + // does not start + flags |= USB_SIE_CTRL_START_TRANS_BITS; + usb_hw->sie_ctrl = flags; + }else // If a normal transfer (non-interrupt) then initiate using // sie ctrl registers. Otherwise interrupt ep registers should // already be configured @@ -577,4 +682,62 @@ bool hcd_edpt_clear_stall(uint8_t dev_addr, uint8_t ep_addr) return true; } +void hcd_edpt_force_last_buffer(uint8_t dev_addr, uint8_t ep_addr, bool force) +{ + struct hw_endpoint *ep = get_dev_ep(dev_addr, ep_addr); + assert(ep); + ep->force_last_buff = force; +} + +// This function is a hack. It appears that the RP2040 host +// controller will keep retrying a NAK'd bulk transfer forever +// until the transfer completes. This function detects that +// the sie status register NAK bit is set, stops the +// current transaction, clears the NAK status, and fixes +// up the endpoint so we can safely start a new transaction +void hcd_edpt_clear_in_on_nak(uint8_t dev_addr, uint8_t ep_addr) +{ + struct hw_endpoint *ep = get_dev_ep(dev_addr, ep_addr); + if (ep == _current_epx_endpoint) + { + if (nak_received() && (usb_hw->ints & (USB_INTS_BUFF_STATUS_BITS | USB_INTS_TRANS_COMPLETE_BITS)) == 0) + { + if (skip_hcd_edpt_clear_in_on_nak > 0) + { + --skip_hcd_edpt_clear_in_on_nak; + clear_nak_received(); + return; + } + // Disable USB interrupts in case there is an in-flight transaction + uint32_t temp_inte = usb_hw->inte; + usb_hw->inte = 0; + // clear the NAK status bit + clear_nak_received(); + // Wait for either the next nak or any raw irq that would interrupt the USB + // The amount of time between host NAK retries is set in the nak_poll register + uint32_t masked_ints = (usb_hw->intr & temp_inte); + while (!nak_received() && masked_ints == 0) + masked_ints = (usb_hw->intr & temp_inte); + if (masked_ints == 0 && nak_received()) + { + // stop the current transaction to free up the host epx HW + usb_hw_set->sie_ctrl = USB_SIE_CTRL_STOP_TRANS_BITS; + // clear the NAK status bit + clear_nak_received(); + // There was no transfer, but the logic that sets up the buffer + // control register needs to see USB_BUF_CTRL_AVAIL bit clear + // or else it will assert. + _hw_endpoint_buffer_control_clear_mask32(ep, USB_BUF_CTRL_AVAIL); + // Fixup next PID: prepare_ep_buffer() toggles the PID, but there was + // no successful data transfer, so we need to toggle it here so that + // it is back where we started. + ep->next_pid ^= 1u; + // Notify host stack that the transfer is done (clears busy) + hw_xfer_complete(ep, XFER_RESULT_SUCCESS); + } + // restore the USB interrupts; a transaction is in process + usb_hw->inte = temp_inte; + } + } +} #endif diff --git a/src/portable/raspberrypi/rp2040/rp2040_usb.c b/src/portable/raspberrypi/rp2040/rp2040_usb.c index c9e2f6b266..0e12359b9d 100644 --- a/src/portable/raspberrypi/rp2040/rp2040_usb.c +++ b/src/portable/raspberrypi/rp2040/rp2040_usb.c @@ -127,8 +127,12 @@ static uint32_t prepare_ep_buffer(struct hw_endpoint *ep, uint8_t buf_id) // Is this the last buffer? Only really matters for host mode. Will trigger // the trans complete irq but also stop it polling. We only really care about - // trans complete for setup packets being sent + // trans complete for setup packets being sent and for bulk transfers + #if TUSB_OPT_HOST_ENABLED + if (ep->remaining_len == 0 || ep->force_last_buff) + #else if (ep->remaining_len == 0) + #endif { buf_ctrl |= USB_BUF_CTRL_LAST; } diff --git a/src/portable/raspberrypi/rp2040/rp2040_usb.h b/src/portable/raspberrypi/rp2040/rp2040_usb.h index a9cf1dd07f..43e9272e8d 100644 --- a/src/portable/raspberrypi/rp2040/rp2040_usb.h +++ b/src/portable/raspberrypi/rp2040/rp2040_usb.h @@ -62,6 +62,9 @@ typedef struct hw_endpoint // If interrupt endpoint uint8_t interrupt_num; + + // Set to true to force the LAST_BUFF flag to true in the next xfer request + bool force_last_buff; #endif } hw_endpoint_t; diff --git a/src/tusb.h b/src/tusb.h index 0d29e106c9..2f68147142 100644 --- a/src/tusb.h +++ b/src/tusb.h @@ -54,6 +54,10 @@ #include "class/cdc/cdc_host.h" #endif + #if CFG_TUH_MIDI + #include "class/midi/midi_host.h" + #endif + #if CFG_TUH_VENDOR #include "class/vendor/vendor_host.h" #endif