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.h
andWire.h
for SPI and I2C communication. -
Adafruit_GFX.h
andAdafruit_SSD1306.h
for OLED display control. -
MIDI.h
for MIDI communication. -
pio_usb.h
andEZ_USB_MIDI_HOST.h
for 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:
onNoteOff
onNoteOn
onPolyphonicAftertouch
onControlChange
onProgramChange
onAftertouch
onPitchBend
onSysEx
onSMPTEqf
onSongPosition
onSongSelect
onTuneRequest
onMidiClock
onMidiStart
onMidiContinue
onMidiStop
onActiveSense
onSystemReset
onMidiTick
onMidiInWriteFail
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.
Text editor powered by tinymce.