This README file contains the design notes and limitations of the usb_midi_host application driver for TinyUSB. This driver supports both C/C++ development and Arduino development. The code in this project should run on any TinyUSB supported processor with USB Host Bulk endpoint support, but the driver and example code have only been tested on a RP2040 in a Raspberry Pi Pico board.
By default, this driver supports up to 4 USB MIDI devices connected through a USB hub, or a single device that may or may not be connected through a hub.
The application driver code is based on code that rppicomidi submitted
to TinyUSB as pull request #1219. The pull request was never merged and
got stale. TinyUSB pull request #1627 by atoktoto started with pull request
#1219, but used simpler RP2040 bulk endpoint support from TinyUSB pull request
#1434. Pull request #1627 was reviewed by todbot, AndrewCapon, PaulHamsh,
and rppicomidi and was substantially functional. This driver copied
the midi_host.c/h
files from pull request #1627 and renamed them
usb_midi_host.c/h
. It also fixed some minor issues in the driver
API and added the application driver wrapper usb_midi_host_app_driver.c
.
The driver C example code code is adapted from TinyUSB pull request #1219.
Although it is possible in the future for the TinyUSB stack to
incorporate this driver into its builtin driver support, right now
it is used as an application driver external to the stack. Installing
the driver to the TinyUSB stack requires adding it to the array
of application drivers returned by the usbh_app_driver_get_cb()
function.
Before you attempt to build any C/C++ applications, be sure
you have the toolchain properly installed and the pico-sdk
installed. Please make sure you can build and
run the blink example found in the Getting Started Guide.
Version 2.0 or later of the pico-sdk
offers the best support
for this project
You will also need to make sure that the the TinyUSB library is installed.
If there is nothing in the directory pico-sdk/lib/tinyusb
, or
the directory does not exist, please run the following commands
cd ${PICO_SDK_PATH}/pico-sdk
git submodule update --recursive --init
You will need a version of the TinyUSB library that supports
USB Host Application drivers. This feature was introduced to TinyUSB on
15-Aug-2023 with commit 7537985c080e439f6f97a021ce49f5ef48979c78
which is release 0.16.0 or later. Version 2.0 of the pico-sdk
is
compatible with this. Older versions are not.
If you must use an older version of the of the pico-sdk
, it ships
configured to use TinyUSB 0.14 or earlier. You will need to update the
TinyUSB library. Please make sure you have a current TinyUSB code
library in your pico-sdk by using the following commands:
cd ${PICO_SDK_PATH}/lib/tinyusb
git fetch origin
git checkout 525406597627fb9307425539b86dddf10278eca8
If you are using the Pico-PIO-USB
Library to implement the
USB Host hardware (see the HARDWARE section, below), install
the Pico-PIO-USB
Library. You must have the Python interpreter
installed to run these commands:
cd ${PICO_SDK_PATH}/lib/tinyusb/tools
python3 get_deps.py rp2040
See the TinyUSB Dependencies documentation for more information.
If you are using the 2.0 version of pico-sdk
, then you need to run the
following commands. Version 2.0 of the pico-sdk
broke the build of the
Pico-PIO-USB
library and there is no update yet to the pico-sdk
to
fix this.
cd ${PICO_SDK_PATH}/lib/tinyusb/hw/mcu/raspberry_pi/Pico-PIO-USB
git fetch origin
get checkout 7902e9fa8ed4a271d8d1d5e7e50516c2292b7bc2
The CMakeLists.txt
file contains two INTERFACE
libraries.
If this driver is your only application USB host driver external
to the TinyUSB stack, you should install this driver by
adding the usb_midi_host_app_driver
library to your main
application's CMakeLists.txt
file's target_link_libraries
.
If you want to add multiple host drivers, you must implement
your own usbh_app_driver_get_cb()
function and you should
add the usb_midi_host
library to your main application's
CMakeLists.txt
file's target_link_libraries
instead.
See the files in examples/C-code/usb_midi_host_example
or examples/C-code/usb_midi_host_pio_example
for examples.
Include this library, the Adafruit TinyUSB Arduino Library,
and, if your host port hardware requires it, the Pico_PIO_USB
Library using the Arduino IDE Library Manager. If this library
is not available in the Arduino IDE Library Manager yet,
please copy this library code to a usb_midi_host
directory
under your sketech folder libraries
directory.
To build any Arduino application on the RP2040, you should install the Earle Philhower arduino-pico core for the Arduino IDE.
You need to set up the board under the Tools Menu.
If you are using the Pico-PIO-USB
Library to implement your
USB Host hardware:
Tools->CPU Speed->120 MHz (or 240 MHz (Overclock))
Tools->USB Stack->"Adafruit TinyUSB"
Tools->Debug Port->Serial
Tools->Optimize: You can choose any option except Small (-Os) (Standard). I generally use Optimize Even More (-O3)
If you are using the native RP2040 USB hardware to implement your USB Host hardware, please configure the core as follows:
Tools->CPU Speed->133 MHz (or faster, if you wish)
Tools->USB Stack->"Adafruit TinyUSB Host"
Tools->Debug Port->Serial1
Tools->Optimize: (Choose anything)
NOTE: The USB Stack option "Adafruit TinyUSB Host" is not
available in the arduino-pico
package 3.6.2 and earlier.
You must install version 3.6.3 or later to make this option
available.
The example programs have been tested on a Raspberry Pi Pico board, a Pico W board, and an Adafruit RP2040 Feather with USB A Host board. The Pico boards, like most RP2040-based boards, do not ship with a USB Host friendly connector, and the development environments generally assume you are using the USB connector in Device mode to provide power to the board, and to allow software update, to provide a serial port interface for a serial console monitor, etc. You will likely have to modify your board to add a USB Host interface connector. The RP2040-based boards offer two approaches.
The Pico-PIO-USB
library, which works for both C/C++ and Arduino,
uses the RP2040 PIO 0 and CPU core 1 to to efficiently
bit-bang a full-speed USB host port on 2 GPIO pins. Adafruit makes
a RP2040 board that uses
this method for USB Host.
If you are not using the Adafruit or similar board, you need to wire up something yourself. Wire a USB A jack to the GPIO and power pins on the Pico board as follows:
Pico/Pico W board pin USB A connector pin
23 (or any GND pin) -> GND
21 (GP16) -> D+ (via a 22 ohm resistor should improve things)
22 (GP17) -> D- (via a 22 ohm resistor should improve things)
24 (GP18) -> TinyUSB drives this pin high to enable VBus on the Adafruit Feather board
40 (VBus) -> VBus (safer if it has current limiting on the pin)
I use a low-cost USB breakout board and solder the 22 ohm resistors to cut traces on the D+ and D- lines. I leave GP18 unconnected.
TODO Insert photos of my setup here.
The main advantages of this approach are:
- Your board will have both a USB Device port and
a USB Host port, which, in an Arduino environment
especially, is convenient. For example, the Arduino
Serial
object will work with the Serial Monitor console, and firmware update via the Arduino IDE is supported. - If you need both a USB MIDI Host port and a USB MIDI Device port at the same time, you can do it. See the pico-usb-midi-filter, pico-usb-midi-processor, and midi2piousbhub projects for examples of this.
- You can buy off-the-shelf hardware already wired to support a Host port using this method.
The disadvantages of this approach are:
- The RP2040 clock must run at a multiple of 120MHz. This is a bit slower than the default of 133MHz.
- It consumes 2 GPIO pins
- It consumes the PIO 0 module
- It consumes CPU 1
- It takes a bit more code storage space and RAM space.
- The Pico_PIO_USB library can conflict with the drivers for the Pico W WiFi/Bluetooth module. To prevent the conflicts, please initialze the TinyUSB library before the WiFi/Bluetooth module lbiraries.
The RP2040 USB core natively supports a host mode that is good enough for MIDI. The minimum modification to a Pico board is to connect a USB OTG adapter to the Micro USB B connector and add an external 5V power supply between the VBus pin (pin 40 on the Pico board) and any ground pin on the Pico board. As long as your 5V power supply is clean and protected against short circuit, it should be OK. It is how I test native hardware USB Host.
TODO Insert photo of my setup here.
The main advantages of this approach are
- It does not consume 3 GPIO pins
- It does not consume PIO 0
- It does not restrict CPU operating speed
- It does not consume CPU 1
- It does not need the memory the Pico_PIO_USB library uses
The disadvantages of this approach are
- Serial port console now has to use a UART; you have to
provide external hardware to interface that UART to your
computer terminal software (e.g., a Picoprobe). In Arduino,
you have to use
Serial1
orSerial2
UARTs for serial monitor to work. - Software update either requires you to unplug the OTG connector and connect the RP2040 in flash drive mode, or you have to use a Picoprobe or similar debug interface.
- Depending on how you want to mount the development board, adapting the board's native USB connector to USB A may be harder than just soldering to a few GPIO pins.
- There appears to be a bug in the RP2040 USB controller hardware that prevents connection with the Arturia Beatstep Pro. Other MIDI hardware may have the same problem.
NOTE: If you are using native USB hardware in USB Host mode for an Arduino project, please look at the note in the Building Arduino Applications section.
There are two connection management functions every application must implement:
tuh_midi_mount_cb()
tuh_midi_unmount_cb()
Each device connected to the USB Host, either directly, or through
a hub, is identified by its device address, which is an 8-bit number.
In addition, during enumeration, the host discovers how many virtual
MIDI IN cables and how many virtual MIDI OUT cables the device has.
When someone plugs a MIDI device to the USB Host, this driver will
call the tuh_midi_mount_cb()
function so the application can save
the device address and virtual cable information.
When someone unplugs a MIDI device from the USB Host, this driver
will call the tuh_midi_unmount_cb()
function so the application
can mark the previous device address as invalid.
There is one function that every application that supports MIDI IN must implement
tuh_midi_rx_cb()
When the USB Host receives MIDI IN packets from the MIDI device,
this driver calls tuh_midi_rx_cb()
to notify the application
that MIDI data is available. The application should read that
MIDI data as soon as possible.
There are two ways to handle USB MIDI messages:
-
As 4-byte raw USB MIDI 1.0 packets
tuh_midi_packet_read()
tuh_midi_packet_write()
-
As serial MIDI 1.0 byte streams
tuh_midi_stream_read()
tuh_midi_stream_write()
Both tuh_midi_packet_write()
and tuh_midi_stream_write()
only write MIDI data to a queue. Once you are done writing
all MIDI messages that you want to send in a single
USB Bulk transfer (usually 64 bytes but sometimes only
8 bytes), you must call tuh_midi_stream_flush()
.
The examples
folder contains both C-Code and Arduino
code examples of how to use the API.
A USB MIDI device can attach a string descriptor to any or all virtual MIDI cables. This driver can retrieve the indices to the strings using these functions:
tuh_midi_get_rx_cable_istrings();
tuh_midi_get_tx_cable_istrings();
tuh_midi_get_all_istrings()
You can then use the TinyUSB API for retrieving a device string by string index to get the UTF-16LE string.
NOTE: For non-Arduino builds, to save space and to speed up
enumeration, by default, CFG_MIDI_HOST_DEVSTRINGS
is set to 0,
so the MIDI Device String API is disabled by default. If you want
to use the MIDI Device String API, please add, either before you
include this library, or to your C/C++ application project tusb_config.h
#define CFG_MIDI_HOST_DEVSTRINGS 1
For Arduino builds, because the Arduino IDE does not allow letting library files include files from the sketch directory, the default is to enable the MIDI Device String API. Disabling it requires adding
#define CFG_MIDI_HOST_DEVSTRINGS 0
in the library file usb_midi_host.h
.
This library API is designed to be relatively low level and is well suited for applications that require the application to touch all MIDI messages (for example, bridging and filtering). If you want your application to use the Arduino MIDI library API to access the devices attached to your USB MIDI host, install the EZ_USB_MIDI_HOST wrapper library in addition to this library. See that repository for more information.
Each example program does the same thing:
- play a 5 note sequence on MIDI cable 0
- print out every MIDI message it receives on cable 0.
The only difference among them is whether they are C/C++
examples or Arduino examples, and whether they use native
rp2040 hardware (in directory with name usb_midi_host_example
)
or the Pico_PIO_USB software USB Host (in directory with name
usb_midi_host_pio_example
).
First, set up your environment for command line pico-sdk program builds. If you are new to this, please see the Getting Started Guide and build the blink example before you try this.
Next, install the libraries as described in the Building C/C++ Applications section.
To build via command line:
cd examples/C-code/[example program directory name]
mkdir build
cd build
cmake ..
make
Version 2.0 of the pico-sdk
seems to prefer using VS Code for builds.
See the Getting Started Guide for information on building using VS Code.
To test, first prepare your development board as described in the Hardware section of this document. Next, copy the UF2 file to the Pico board using whatever method you prefer. Make sure the board powers up and displays the message
Pico MIDI Host Example
before you attach your USB MIDI device.
Attach your USB MIDI device to the USB A connector your hardware provides. You should see a message similar to
MIDI device address = 1, IN endpoint 2 has 1 cables, OUT endpoint 1 has 1 cables
If your MIDI device can generate sound, you should start hearing a pattern of notes from B-flat to D. If your device is Mackie Control compatible, then the transport LEDs should sequence. If you use a control on your MIDI device, you should see the message traffic displayed on the serial console.
NOTE: Unfortunately, the pico-sdk
does not currently allow you to use the
USB device port for console I/O and the PIO USB host port at the same time.
You can install a library
that lets you do this for you own projects. In these examples,
all printf() output goes to the UART 0 serial port. If you want an
example that uses this library and uses the native USB port for both
MIDI device and console output, see the midi2piousbhub project.
To build and run the Arduino examples, in the Arduino IDE, use the Library Manager to install this library and accept all of its dependencies. If your hardware requires it, install the Pico PIO USB library too. Next, in the IDE, select File->Examples->usb_midi_host->arduino->[your example program name]
Use the Arduino IDE to build and run the code. Make sure to start a serial monitor or else the code will appear to lock up.
Attach a MIDI device to the USB A port. You should see something like this in the Serial Port Monitor (of course, your connected MIDI device will likely be different).
MIDI device address = 1, IN endpoint 1 has 1 cables, OUT endpoint 2 has 1 cables
Device attached, address = 1
iManufacturer 1 KORG INC.
iProduct 2 nanoKONTROL2
iSerialNumber 0
If your MIDI device can generate sound, you should start hearing a pattern of notes from B-flat to D. If your device is Mackie Control compatible, then the transport LEDs should sequence. If you use a control on your MIDI device, you should see the message traffic displayed on the Serial Port Monitor.
In addition to this section, you might find this guide helpful.
In C/C++, the config file for your project is called tusb_config.h
.
It should be in the include path of your project.
In Arduino code,the config file is stored in the libraries
directory of
your sketch directory as the file
Adafruit_TinyUSB/src/arduino/ports/${target}/tusb_config_${target}.h
,
where ${target}
is the processor name of the processor on the target
hardware. For example, for a Rapsberry Pi Pico board, the file is
libraries/Adafruit_TinyUSB/src/arduino/ports/rp2040/tusb_config_rp2040.h
.
Sadly, any changes you make to the config file will disappear if you update
Adafruit_TinyUSB_Library
.
To make configuring parameters specific to the USB MIDI Host a bit simpler
for Arduino IDE users, see the function tuh_midih_define_limits()
.
When the USB Host driver tries to enumerate a device, it reads the
USB descriptors into a byte buffer. By default, that buffer is 256 bytes
long. Complete MIDI devices, or device that also have audio interfaces,
tend to have much longer USB descriptors. If a device fails to enumerate,
locate the line in your config file that contains #define CFG_TUH_ENUMERATION_BUFSIZE
and change the default 256 to something larger
(for example, 512).
If a device fails to enumerate, a debug log printout may be helpful.
Debug log levels go from 0 (no debug logging) to 3 (very verbose). The
default log level is 0. The most direct way to set the debug level is to
define CFG_TUSB_DEBUG
in your config file. For example, to set the log
level to 2, make sure your config file contains the lines
#ifndef CFG_TUSB_DEBUG
#define CFG_TUSB_DEBUG 2
#endif
The conditional is in case you choose to change the debug level by setting an environment variable.
For Arduino, the Adafruit TinyUSB Host
option seems to require you to define
the function log_printf
if you use a debug log level other than 0. Adding
the following function to your program sketch should suffice as long as none
of the debug log output lines is longer than 256 bytes.
// Debugging
int log_printf(const char * format, ...)
{
char outstr[256];
va_list va;
va_start(va, format);
int ret = vsprintf(outstr, format, va);
// Uncomment the next line to send the debug log to the Serial1 output
return Serial1.print(outstr);
}
You should define the value CFG_TUH_DEVICE_MAX
in the tuh_config.h file to
match the number of USB hub ports attached to the USB host port. For
example
// max device support (excluding hub device)
#define CFG_TUH_DEVICE_MAX (CFG_TUH_HUB ? 4 : 1) // hub typically has 4 ports
The Arduino IDE makes configuring this is difficult because tuh_config.h is part of the processor core (for RP2040, anyway)
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 IN endpoint. This driver allows that.
A USB MIDI 1.0 Class message can support up to 16 virtual cables. The function
tuh_midi_stream_write()
uses 6 bytes of data stored in an array in
an internal data structure to deserialize a MIDI byte stream to a
particular virtual cable. To properly handle all 16 possible virtual cables,
CFG_TUH_DEVICE_MAX*16*6
data bytes are required. If the application
needs to save memory, in file tusb_cfg.h
set CFG_TUH_CABLE_MAX
to
something less than 16 as long as it is at least 1.
For Arduino builds, you can configure this parameter at runtime by calling tuh_midih_define_limits().
A MIDI device is supposed to have an Audio Control Interface and it may have an Audio Streaming Interface before the the MIDI Streaming Interface, that this driver supports. Many commercial devices do not have even the Audio Control Interface. To support these devices, the descriptor parser in this driver will skip past Audio Control Interface and Audio Streaming Interface descriptors 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.
The host driver only makes use of the informaton in the class specific
interface descriptors to extract string descriptors from each IN JACK and
OUT JACK. To use these, you must set CFG_MIDI_HOST_DEVSTRINGS
to 1 in
your application's tusb_config.h file. It does not parse ELEMENT items
for string descriptors.
This driver does not support class specific requests to control ELEMENT items, nor does it support non-MIDI Streaming bulk endpoints.
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.
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 can 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), and thanks to pull request#3 from @moseltronics, this driver should correctly parse or compose 4-byte USB MIDI Class packets from streams encoded with running status.
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.
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.
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 configuration descriptor is too long for the host to read the whole thing. The most common failure, though, is the descriptor is too long. See Size of the Enumeration Buffer for how to address that.