The Arduino code for the project is available as a pre-compiled .UF2 file that you can drag and drop onto your USB Host Feather RP2040 board.
To enter the bootloader, hold down the BOOT button, and while continuing to hold it (don't let go!), press and release the reset button. Continue to hold the BOOT button until the RPI-RP2 drive appears!
Once you see RPI-RP2 drive, release the BOOT button. You should longer be holding down any buttons (reset or boot0 button).
You will see a new disk drive appear called RPI-RP2
Drag the USB_MIDI_Host_Messenger_Arduino.uf2 file to RPI-RP2
This will take a few moments and then the board will restart -- the RPI-RP2 drive will disappear from your computer's list of drives and then the code will run on the Feather.
Customize
If you'd like to look at the code or customize it further, you can download the Arduino sketch file below.
// SPDX-FileCopyrightText: 2024 john park for Adafruit Industries
//
// SPDX-License-Identifier: MIT
/**
* For USB MIDI Host Feather RP2040 with mini OLED FeatherWing and MIDI FeatherWing
* Modified 12 Jun 2024 - @todbot -- added USB MIDI forwarding
* Modified by @johnedgarpark -- added UART MIDI forwarding and display/message filtering
* originally from: https://github.com/rppicomidi/EZ_USB_MIDI_HOST/blob/main/examples/arduino/EZ_USB_MIDI_HOST_PIO_example/EZ_USB_MIDI_HOST_PIO_example.ino
*/
/*
* The MIT License (MIT)
*
* Copyright (c) 2023 rppicomidi
*
* 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 demo program is designed to test the USB MIDI Host driver for a single USB
* MIDI device connected to the USB Host port. It also
* forwards MIDI received from the USB MIDI device to USB and UART MIDI devices.
*
* This program works with a single USB MIDI device connected via a USB hub, but it
* does not handle multiple USB MIDI devices connected at the same time.
*
* Libraries (all available via library manager):
* - MIDI -- https://github.com/FortySevenEffects/arduino_midi_library
*/
// Be sure to set the CPU clock to 120MHz or 240MHz before uploading to board
// USB Stack is TinyUSB
// Press A to change output MIDI channel
// Press B to change Program Change banks in groups of 8
// Press C for MIDI panic
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
//#define WIRE Wire1 //only if display needs it.
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 32 // OLED display height, in pixels
#define OLED_RESET -1 // Reset pin # (or -1 if sharing Arduino reset pin)
#define SCREEN_ADDRESS 0x3C ///< See datasheet for Address; 0x3D for 128x64, 0x3C for OLED FeatherWing 128x32
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);
//Screen buttons
const int buttonAPin = 9;
const int buttonBPin = 6;
const int buttonCPin = 5;
int buttonAState;
int buttonBState;
int buttonCState;
int lastButtonAState;
int lastButtonBState;
int lastButtonCState;
unsigned long lastDebounceATime = 0;
unsigned long lastDebounceBTime = 0;
unsigned long lastDebounceCTime = 0;
unsigned long debounceDelay = 50;
int userChannel = 1; //1-16
int userProgOffset = 0;
#include <MIDI.h>
#if defined(USE_TINYUSB_HOST) || !defined(USE_TINYUSB)
#error "Please use the Menu to select Tools->USB Stack: Adafruit TinyUSB"
#endif
#include "pio_usb.h"
#define HOST_PIN_DP 16 // Pin used as D+ for host, D- = D+ + 1
#include "EZ_USB_MIDI_HOST.h"
// USB Host object
Adafruit_USBH_Host USBHost;
USING_NAMESPACE_MIDI
USING_NAMESPACE_EZ_USB_MIDI_HOST
RPPICOMIDI_EZ_USB_MIDI_HOST_INSTANCE(usbhMIDI, MidiHostSettingsDefault)
Adafruit_USBD_MIDI usb_midi; // USB MIDI object
MIDI_CREATE_INSTANCE(Adafruit_USBD_MIDI, usb_midi, MIDIusb); // USB MIDI
MIDI_CREATE_INSTANCE(HardwareSerial, Serial1, MIDIuart); // Serial MIDI over MIDI FeatherWing
static uint8_t midiDevAddr = 0;
static bool core0_booting = true;
static bool core1_booting = true;
/* MIDI IN MESSAGE REPORTING */
static void onMidiError(int8_t errCode)
{
Serial.printf("MIDI Errors: %s %s %s\r\n", (errCode & (1UL << ErrorParse)) ? "Parse":"",
(errCode & (1UL << ErrorActiveSensingTimeout)) ? "Active Sensing Timeout" : "",
(errCode & (1UL << WarningSplitSysEx)) ? "Split SysEx":"");
}
int last_cc_cntrl = 1;
static void midiPanic()
{
for (int i=0; i<128; i++)
{
MIDIusb.sendNoteOff(i, 0, userChannel);
MIDIuart.sendNoteOff(i, 0, userChannel);
Serial.printf("note %u off\r\n", i);
last_cc_cntrl = 0; // dirty this
}
}
static void onNoteOff(Channel channel, byte note, byte velocity)
{
MIDIusb.sendNoteOff(note, velocity, userChannel);
MIDIuart.sendNoteOff(note, velocity, userChannel);
Serial.printf("ch%u: Note off#%u v=%u\r\n", userChannel, note, velocity);
display.setCursor(0,12);
display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
display.setTextWrap(false);
display.printf("Ch %u > %u Note %u \r\n", channel, userChannel, note);
display.display();
last_cc_cntrl = 0;
}
static void onNoteOn(Channel channel, byte note, byte velocity)
{
MIDIusb.sendNoteOn(note, velocity, userChannel);
MIDIuart.sendNoteOn(note, velocity, userChannel);
Serial.printf("ch%u: Note on#%u v=%u\r\n", userChannel, note, velocity);
display.setCursor(0,12);
display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
display.setTextWrap(false);
display.printf("Ch %u > %u Note %u o\r\n", channel, userChannel, note);
display.display();
last_cc_cntrl = 0;
}
static void onPolyphonicAftertouch(Channel channel, byte note, byte amount)
{
Serial.printf("ch%u: PAT#%u=%u\r\n", userChannel, note, amount);
MIDIusb.sendAfterTouch(note, amount, userChannel);
MIDIuart.sendAfterTouch(note, amount, userChannel);
}
static void onControlChange(Channel channel, byte controller, byte value)
{
MIDIusb.sendControlChange(controller, value, userChannel);
MIDIuart.sendControlChange(controller, value, userChannel);
Serial.printf("Ch %u CC#%u=%u\r\n", userChannel, controller, value);
if (last_cc_cntrl != controller){
display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
display.setTextWrap(false);
display.setCursor(0,12);
display.printf("CC# %u \r\n", controller);
display.display();
last_cc_cntrl = controller;
}
}
static void onProgramChange(Channel channel, byte program)
{
Serial.printf("ch%u: Prog=%u\r\n", userChannel, program);
MIDIusb.sendProgramChange(program + userProgOffset, userChannel);
MIDIuart.sendProgramChange(program + userProgOffset, userChannel);
display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
display.setCursor(0,24);
display.printf("Progs %u-%u [%u] \r\n", userProgOffset, (userProgOffset + 7), (program + userProgOffset));
display.display();
last_cc_cntrl = 0; // dirty this
}
static void onAftertouch(Channel channel, byte value)
{
Serial.printf("ch%u: AT=%u\r\n", userChannel, value);
MIDIusb.sendAfterTouch(value, userChannel);
MIDIuart.sendAfterTouch(value, userChannel);
}
static void onPitchBend(Channel channel, int value)
{
Serial.printf("ch%u: PB=%d\r\n", userChannel, value);
MIDIusb.sendPitchBend(value, userChannel);
MIDIuart.sendPitchBend(value, userChannel);
}
static void onSysEx(byte * array, unsigned size)
{
Serial.printf("SysEx:\r\n");
unsigned multipleOf8 = size/8;
unsigned remOf8 = size % 8;
for (unsigned idx=0; idx < multipleOf8; idx++) {
for (unsigned jdx = 0; jdx < 8; jdx++) {
Serial.printf("%02x ", *array++);
}
Serial.printf("\r\n");
}
for (unsigned idx = 0; idx < remOf8; idx++) {
Serial.printf("%02x ", *array++);
}
Serial.printf("\r\n");
}
static void onSMPTEqf(byte data)
{
uint8_t type = (data >> 4) & 0xF;
data &= 0xF;
static const char* fps[4] = {"24", "25", "30DF", "30ND"};
switch (type) {
case 0: Serial.printf("SMPTE FRM LS %u \r\n", data); break;
case 1: Serial.printf("SMPTE FRM MS %u \r\n", data); break;
case 2: Serial.printf("SMPTE SEC LS %u \r\n", data); break;
case 3: Serial.printf("SMPTE SEC MS %u \r\n", data); break;
case 4: Serial.printf("SMPTE MIN LS %u \r\n", data); break;
case 5: Serial.printf("SMPTE MIN MS %u \r\n", data); break;
case 6: Serial.printf("SMPTE HR LS %u \r\n", data); break;
case 7:
Serial.printf("SMPTE HR MS %u FPS:%s\r\n", data & 0x1, fps[(data >> 1) & 3]);
break;
default:
Serial.printf("invalid SMPTE data byte %u\r\n", data);
break;
}
}
static void onSongPosition(unsigned beats)
{
Serial.printf("SongP=%u\r\n", beats);
MIDIusb.sendSongPosition(beats);
MIDIuart.sendSongPosition(beats);
}
static void onSongSelect(byte songnumber)
{
Serial.printf("SongS#%u\r\n", songnumber);
MIDIusb.sendSongSelect(songnumber);
MIDIuart.sendSongSelect(songnumber);
}
static void onTuneRequest()
{
Serial.printf("Tune\r\n");
MIDIusb.sendTuneRequest();
MIDIuart.sendTuneRequest();
}
static void onMidiClock()
{
Serial.printf("Clock\r\n");
MIDIusb.sendClock();
MIDIuart.sendClock();
}
static void onMidiStart()
{
Serial.printf("Start\r\n");
MIDIusb.sendStart();
MIDIuart.sendStart();
}
static void onMidiContinue()
{
Serial.printf("Cont\r\n");
MIDIusb.sendContinue();
MIDIuart.sendContinue();
}
static void onMidiStop()
{
Serial.printf("Stop\r\n");
MIDIusb.sendStop();
MIDIuart.sendStop();
}
static void onActiveSense()
{
Serial.printf("ASen\r\n");
}
static void onSystemReset()
{
Serial.printf("SysRst\r\n");
}
static void onMidiTick()
{
Serial.printf("Tick\r\n");
}
static void onMidiInWriteFail(uint8_t devAddr, uint8_t cable, bool fifoOverflow)
{
if (fifoOverflow)
Serial.printf("Dev %u cable %u: MIDI IN FIFO overflow\r\n", devAddr, cable);
else
Serial.printf("Dev %u cable %u: MIDI IN FIFO error\r\n", devAddr, cable);
}
static void registerMidiInCallbacks()
{
auto intf = usbhMIDI.getInterfaceFromDeviceAndCable(midiDevAddr, 0);
if (intf == nullptr)
return;
intf->setHandleNoteOff(onNoteOff); // 0x80
intf->setHandleNoteOn(onNoteOn); // 0x90
intf->setHandleAfterTouchPoly(onPolyphonicAftertouch); // 0xA0
intf->setHandleControlChange(onControlChange); // 0xB0
intf->setHandleProgramChange(onProgramChange); // 0xC0
intf->setHandleAfterTouchChannel(onAftertouch); // 0xD0
intf->setHandlePitchBend(onPitchBend); // 0xE0
intf->setHandleSystemExclusive(onSysEx); // 0xF0, 0xF7
intf->setHandleTimeCodeQuarterFrame(onSMPTEqf); // 0xF1
intf->setHandleSongPosition(onSongPosition); // 0xF2
intf->setHandleSongSelect(onSongSelect); // 0xF3
intf->setHandleTuneRequest(onTuneRequest); // 0xF6
intf->setHandleClock(onMidiClock); // 0xF8
// 0xF9 as 10ms Tick is not MIDI 1.0 standard but implemented in the Arduino MIDI Library
intf->setHandleTick(onMidiTick); // 0xF9
intf->setHandleStart(onMidiStart); // 0xFA
intf->setHandleContinue(onMidiContinue); // 0xFB
intf->setHandleStop(onMidiStop); // 0xFC
intf->setHandleActiveSensing(onActiveSense); // 0xFE
intf->setHandleSystemReset(onSystemReset); // 0xFF
intf->setHandleError(onMidiError);
auto dev = usbhMIDI.getDevFromDevAddr(midiDevAddr);
if (dev == nullptr)
return;
dev->setOnMidiInWriteFail(onMidiInWriteFail);
}
/* CONNECTION MANAGEMENT */
static void onMIDIconnect(uint8_t devAddr, uint8_t nInCables, uint8_t nOutCables)
{
Serial.printf("MIDI device at address %u has %u IN cables and %u OUT cables\r\n", devAddr, nInCables, nOutCables);
midiDevAddr = devAddr;
registerMidiInCallbacks();
}
static void onMIDIdisconnect(uint8_t devAddr)
{
Serial.printf("MIDI device at address %u unplugged\r\n", devAddr);
midiDevAddr = 0;
}
/* MAIN LOOP FUNCTIONS */
static void blinkLED(void)
{
const uint32_t intervalMs = 1000;
static uint32_t startMs = 0;
static bool ledState = false;
if ( millis() - startMs < intervalMs)
return;
startMs += intervalMs;
ledState = !ledState;
digitalWrite(LED_BUILTIN, ledState ? HIGH:LOW);
}
// core1's setup
void setup1() {
#if ARDUINO_ADAFRUIT_FEATHER_RP2040_USB_HOST
pinMode(18, OUTPUT); // Sets pin USB_HOST_5V_POWER to HIGH to enable USB power
digitalWrite(18, HIGH);
#endif
//while(!Serial); // wait for native usb
Serial.println("Core1 setup to run TinyUSB host with pio-usb\r\n");
// Check for CPU frequency, must be multiple of 120Mhz for bit-banging USB
uint32_t cpu_hz = clock_get_hz(clk_sys);
if ( cpu_hz != 120000000UL && cpu_hz != 240000000UL ) {
delay(2000); // wait for native usb
Serial.printf("Error: CPU Clock = %lu, PIO USB require CPU clock must be multiple of 120 Mhz\r\n", cpu_hz);
Serial.printf("Change your CPU Clock to either 120 or 240 Mhz in Menu->CPU Speed \r\n");
while(1) delay(1);
}
pio_usb_configuration_t pio_cfg = PIO_USB_DEFAULT_CONFIG;
pio_cfg.pin_dp = HOST_PIN_DP;
USBHost.configure_pio_usb(1, &pio_cfg);
// run host stack on controller (rhport) 1
// Note: For rp2040 pico-pio-usb, calling USBHost.begin() on core1 will have most of the
// host bit-banging processing work done in core1 to free up core0 for other work
usbhMIDI.begin(&USBHost, 1, onMIDIconnect, onMIDIdisconnect);
core1_booting = false;
while(core0_booting) ;
}
// core1's loop
void loop1()
{
USBHost.task();
}
void setup()
{
TinyUSBDevice.setManufacturerDescriptor("LarsCo");
TinyUSBDevice.setProductDescriptor("MIDI Masseuse");
Serial.begin(115200);
if(!display.begin(SSD1306_SWITCHCAPVCC, SCREEN_ADDRESS)) {
Serial.println(F("SSD1306 allocation failed"));
for(;;); // Don't proceed, loop forever
}
// Show initial display buffer contents on the screen --
// the library initializes this with an Adafruit splash screen.
display.display();
delay(2000); // Pause for 2 seconds
// Clear the buffer
display.clearDisplay();
display.display();
display.setTextSize(1);
display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
display.setCursor(0,0);
display.print("USB MIDI Messenger");
display.setCursor(0,12);
display.print("Ch x > 1 Note_ CC#_\r\n");
display.setCursor(0,24);
display.printf("Progs %u-%u \r\n", userProgOffset, (userProgOffset + 7));
display.display();
pinMode(buttonAPin, INPUT_PULLUP);
pinMode(buttonBPin, INPUT); // OLED button B as a 100k pullup on it on 128x32 FW
pinMode(buttonCPin, INPUT_PULLUP);
MIDIusb.begin();
MIDIusb.turnThruOff(); // turn off echo
MIDIuart.begin(MIDI_CHANNEL_OMNI); // don't forget OMNI
// while(!Serial); // wait for serial port
pinMode(LED_BUILTIN, OUTPUT);
Serial.println("USB Host to MIDI Messenger\r\n");
core0_booting = false;
while(core1_booting) ;
}
void loop() {
// Handle any incoming data; triggers MIDI IN callbacks
usbhMIDI.readAll();
// Do other processing that might generate pending MIDI OUT data
// Tell the USB Host to send as much pending MIDI OUT data as possible
usbhMIDI.writeFlushAll();
// Do other non-USB host processing
blinkLED();
int readingA = digitalRead(buttonAPin);
int readingB = digitalRead(buttonBPin);
int readingC = digitalRead(buttonCPin);
if (readingA != lastButtonAState) {
lastDebounceATime = millis();
}
if (readingB != lastButtonBState) {
lastDebounceBTime = millis();
}
if (readingC != lastButtonCState) {
lastDebounceCTime = millis();
}
if ((millis() - lastDebounceATime) > debounceDelay) {
if (readingA != buttonAState) {
buttonAState = readingA;
if (buttonAState == LOW) {
userChannel = (userChannel % 16) + 1 ; // increment from 1-16
Serial.printf("Ch%u\r\n", userChannel);
display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
display.setCursor(0,12);
display.printf("Ch out %u \r\n", userChannel);
display.display();
}
}
}
if ((millis() - lastDebounceBTime) > debounceDelay) {
if (readingB != buttonBState) {
buttonBState = readingB;
if (buttonBState == LOW) {
userProgOffset = (userProgOffset + 8) % 128 ;
Serial.printf("Prog Progs %u through %u\r\n", userProgOffset, (userProgOffset + 7));
display.setTextColor(SSD1306_WHITE, SSD1306_BLACK);
display.setCursor(0,24);
display.printf("Progs %u-%u \r\n", userProgOffset, (userProgOffset + 7));
display.display();
}
}
}
if ((millis() - lastDebounceCTime) > debounceDelay) {
if (readingC != buttonCState) {
buttonCState = readingC;
if (buttonCState == LOW) {
midiPanic();
}
}
}
lastButtonAState = readingA;
lastButtonBState = readingB;
lastButtonCState = readingC;
}
How It Works
This sketch uses the EZ_USB_MIDI_HOST library by rppicomidi. It was derived from the PIO example code here.
Tod Kurt modified it to add USB MIDI forwarding (so incoming MIDI messages are received and then re-sent to a computer over the Feather's primary (non-Host) USB port.
John Park added the classic MIDI messaging output over UART, as well as message filtering to re-map MIDI channels, and OLED display/UI functionality.
Initial Setup and Libraries
Libraries
-
SPI.handWire.hfor SPI and I2C communication. -
Adafruit_GFX.handAdafruit_SSD1306.hfor OLED display control. -
MIDI.hfor MIDI communication. -
pio_usb.handEZ_USB_MIDI_HOST.hfor USB host support.
OLED Display Configuration
- Configures the OLED display with specified width, height, reset pin, and I2C address.
- Initializes the display and sets initial text.
Button Configuration
- Configures three buttons (A, B, C) with debounce logic to ensure reliable button presses.
MIDI Configuration
- Defines USB and UART MIDI interfaces.
- Sets up USB host and MIDI handling with callbacks for various MIDI messages.
MIDI Message Handling
The code includes a series of callback functions to handle various MIDI messages, including:
onNoteOffonNoteOnonPolyphonicAftertouchonControlChangeonProgramChangeonAftertouchonPitchBendonSysExonSMPTEqfonSongPositiononSongSelectonTuneRequestonMidiClockonMidiStartonMidiContinueonMidiStoponActiveSenseonSystemResetonMidiTickonMidiInWriteFail
Each of these functions forwards the MIDI message to both the USB and UART MIDI interfaces and updates the OLED display as needed. The midiPanic function is designed to send Note Off messages for all notes, effectively stopping all sound.
Core1 Setup and Loop
The Feather RP2040 has dual cores. The code sets up Core1 to handle the USB host stack:
- Initializes USB host configuration for MIDI devices.
- Runs the USB host task in a loop to manage MIDI device connections and data transfer.
Core0 Setup and Loop
Core0 handles the main program logic:
- Initializes Serial communication and the OLED display.
- Sets up the buttons and their debounce logic.
- Begins the MIDI interfaces and configures them to not echo messages (
turnThruOff).
The main loop on Core0 handles:
- Reading and processing button states to change the output MIDI channel, program change banks, and to send a MIDI panic message.
- Blinking an LED to indicate the program is running.
- Handling USB MIDI input and output.
Button Functionality
- Button A: Changes the output MIDI channel (1-16).
- Button B: Changes the program change bank in groups of 8.
- Button C: Sends a MIDI panic message, turning off all notes.
How it Works Together
Initialization
- Both cores are initialized, setting up USB host and MIDI interfaces.
- OLED display shows the initial message and sets up text properties.
Main Loop
- USB MIDI messages are read and processed, triggering appropriate callbacks.
- Button states are read and debounced. Button presses change MIDI settings or trigger panic.
- The OLED display is updated with the current MIDI channel, program bank, or control change as messages are received or settings are changed.
MIDI Message Handling
- Incoming MIDI messages are forwarded to both USB and UART MIDI interfaces.
- Messages are also displayed on the OLED screen and printed to the Serial monitor for debugging.
Page last edited January 18, 2025
Text editor powered by tinymce.