Continuing my experiments with my Raspberry Pi Pico I2C MIDI Interface, one comment on Twitter asked about latency, so I thought I’d try to see if it was possible to measure how long it took for a complete round trip of a MIDI message from an Arduino through the Pico, back out over I2C to the Arduino.
- In part 1, I tested the concept of a Pico I2C MIDI interface.
- In part 2, I wrote an I2C MIDI “driver” for the Adafruit MIDI library.
- In this part, I’m measuring the latency of the links.
- In the final part, I’ll be routing messages to several Arduino peripherals.
Warning! I strongly recommend using old or second hand equipment for your experiments. I am not responsible for any damage to expensive instruments!
These are the key tutorials for the main concepts used in this project:
- Getting Started with Raspberry Pi Pico and CircuitPython
- CircuitPython Essentials: CircuitPython I2C
If you are new to microcontrollers, see the Getting Started pages.
Parts list
- Arduino Uno, Nano, or similar
- Raspberry Pi Pico
- 3.3V to 5V level shifter
- 2x 10kΩ resistors
- An Arduino MIDI OUT circuit (e.g. one of Arduino MIDI Interfaces or Ready-Made MIDI Modules)
- A Pico MIDI IN circuit (e.g. one of DIY MIDI Interfaces for 3.3V operation)
The Circuit
The Pico needs a 3.3V compatible MIDI IN interface and the Arduino needs a 5V MIDI OUT. The boards are all connected as follows:
- 3.3V MIDI IN to Pico GP1 (UART 0 RX).
- 5V MIDI OUT to Arduino D3 (for use with SoftwareSerial – see later).
- Pico I2C to level shifter LOW side.
- Arduino I2C to level shifter HIGH side.
The operating principle is that a MIDI message originates within the Arduino, gets sent out over MIDI and received by the Pico. The Pico then routes it out over I2C back to the Arduino. The Arduino can measure the time it took between sending and receiving the message.
The Code
The Pico will use my I2C MIDI code for Adafruit MIDI on CircuitPython. The Arduino will use my I2C MIDI transport for the Arduino MIDI Library. The Pico is the I2C CONTROLLER and the Arduino is (naturally therefore) the PERIPHERAL.
Here is the Pico “Relay” code.
import board import busio import adafruit_midi import i2cmidi from adafruit_midi.midi_message import MIDIUnknownEvent addr = 0x40 i2c = busio.I2C(board.GP13, board.GP12, frequency=400000) uart = busio.UART(tx=board.GP0, rx=board.GP1, baudrate=31250, timeout=0.001) uartmidi = adafruit_midi.MIDI(midi_in=uart) imidi = i2cmidi.I2CMIDI(i2c, addr, i2cmidi.I2CMIDI_CTRL) midi = adafruit_midi.MIDI(midi_out=imidi) while True: msg = uartmidi.receive() if (isinstance(msg, MIDIUnknownEvent)): # Ignore unknown MIDI events pass elif msg is not None: midi.send(msg) print("Sent: ", msg) else: # Ignore "None" pass
This sets up an I2C MIDI CONTROLLER wanting to talk to the PERIPHERAL on address 0x40 and a serial MIDI on UART 0 (GP0 and GP1). Any messages received over the serial port are re-sent back out over the I2C MIDI port.
Note that I’ve filtered out any unknown MIDI events, just to keep things simple.
On the Arduino side, things are a little more complicated, in that I have set it up as follows:
- It will use SoftwareSerial for the MIDI link so that I can print diagnostics to the hardware serial port.
- It is configured as an I2C PERIPHERAL on address 0x40.
- The code has three states:
- Send out a MIDI message over the serial port.
- Waiting for the MIDI message to return over I2C.
- Waiting once a test is complete for the next test to begin.
- I’m using the Arduino micros() function to measure the time between sending and receiving.
- I’m just sending a single NoteOff message for note 60 on MIDI channel 1, which I check on receive too, to make sure I’m measuring the right thing!
- The results of each test are printed to the serial port.
Results
I’ve found a reasonable variation in response time! From my simple experiments, it would appear that a MIDI message could take between 2.5mS to 12.5mS to complete a full cycle! A sample of some of the results can be seen below.
Is this a good result? To be honest – I’ve no idea! By way of comparison, I guess that to play a run of demi-semi-quavers at a tempo of 120 beats per minute, would require:
- 2 beats every second.
- so 16 demi-semi-quavers every second.
- two MIDI messages per note, so 32 MIDI messages every second (each three bytes each).
- so approximately one MIDI message every 30 mS.
On this basis, the worst latency for the entire trip is equivalent to a demi-semi-quaver at a tempo of twice that – i.e. 240 beats per minute. That seems pretty small in “human ear” terms to me. If I was playing a keyboard and notes were sounding that far behind, I doubt I’d notice.
I don’t think I’m worried about through-put either. MIDI, when all said and done is a 31.25kbps serial protocol, but I’m running over a hardware serial bus that is running at 100kbps (standard) or 400kbps (if the code/hardware is configured properly for “fast mode”).
What about the fact that the Pico is running python? Well the Arduino is an 8-bit microcontroller (ATmega328, AVR) running at 16MHz but programmed in C; compared to the Pico which is a 32-bit (dual core, but that is irrelevant in this case) microcontroller (RP2040, ARM M0+) running at 300MHz, so I’d hope they were at least comparable in performance 🙂
No the real limitation of this system is that it isn’t really “plug and play”. The I2C devices and their addresses really have to be pre-configured upfront and all turned on for it to work. It might be possible for it to be a bit more dynamic, but for me the value (if there is one, I’m still deciding) is perhaps as an intra-instrument MIDI bus, not as an inter-instrument MIDI connection. I could imagine this being used (for example) for routing between modules in a rack.
Closing Thoughts
I’ve one more experiment I want to perform. I’d like to try playing one of my Lo-Fi Orchestra pieces via a Pico as a router, sending MIDI out to several Arduino “instruments” to see how it goes.
That will probably be my next, and possibly last for the time being, experiment with I2C MIDI I think.
Kevin