Raspberry Pi Pico I2C MIDI Interface – Part 3

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.

IMG_5714

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:

If you are new to microcontrollers, see the Getting Started pages.

Parts list

The Circuit

RaspberryPiPicoI2CMIDITest3_bb

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.

Find it on GitHub here.

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.

I2CMIDItoSerialLatency

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

Leave a comment