Trinket M0 USB host to MIDI converter

In the old days, you bought a keyboard, you bought an external synth module, you connected the two with a very simple MIDI cable and lo and behold - the external module would play whatever you played on the keyboard. As long as you remembered to set the MIDI channel right on both ends AND your local control on the keyboard would not mess up the sent MIDI AND you plugged the cables in the right connectors AND your keyboard would not send too much MIDI clock or MIDI sense so that the very old external synth would not crash with too many MIDI messages and.... Oh well, as long as you understood what you were doing, it just worked.

Early 2000 saw the advent of USB, the ubiquitous communication mechanism between computers, mice, keyboards, toasters and self-watering flower pots. And of course they were included in the musical instruments as well. MIDI, as an almost 40 years old protocol has some drastic limitations compared to any version of USB, so the inclusion of high-speed, easy to configure and cheap communications were not a surprise to anyone.

USB, as wonderful as it is in general, has some architectural differences compared to the way MIDI works. MIDI devices could (and still can) talk to each other directly - connect a cable from one device's MIDI out to another one's MIDI in, you're golden. Chain as many devices as you like (given there are connectors available on the hardware). USB has a host-device architecture which means there is no direct communication between devices - there has to be a host device (usually a computer) present. This, of course, makes the task of connecting two devices way harder (and more expensive), as you have to introduce a computer to the mix.

But not to worry. The computer which provides the host interface does not have to be a very substantial one.

The idea of this project is to build a circuit capable of hosting the USB keyboard/controller on a small device and output standard MIDI for external gear. The original project on github is a bit vague, so I decided to document it a bit more comprehensively.

Credit where credit is due

The original project (and the code) is from https://github.com/gdsports/midiuartusbh

Trinket M0 and getting it to work

If you have already done something with Trinket M0 and your development environment is up to date, skip ahead to "Project ahoy!".

Arduino platform is the choice of tinkerers everywhere. Inexpensive Atmel-based boards, easy to install (and easy to understand) integrated development environment (IDE) and hundreds if not thousands of ready-made libraries for every hardware bell and whistle imaginable. Connect your buttons, displays, connectors and relays to your board, connect the board to a computer with USB, write a few lines of code and press upload - and there you have it.

Using the Adafruit Trinket as a development board has it caveats, though. The standard Arduino installation does not have the drivers and the necessary tool chain to support it, you need to install some additional software first, and this is not as clear-cut as it seems. Well, at least it wasn't for me, but after 15 minutes of cursing and mucking about, I got everything working. The basic procedure - if you do not have anything installed on your computer - is as follows:

(Obviously this section is for Windows OS only. Linux/Mac users, go find your own. A hint: Mac/Linux owners do not need to install any drivers.)

Drivers

Board support in Arduino IDE 

Time to manage some boards. But nothing is that easy, we need to change the Arduino environment a bit so that the poor IDE will find the correct board definitions (and install the necessary tools and libraries).

Project ahoy!

Now that we have the tedious part squared away, we can actually get into building our little project - a USB host-MIDI converter (later we still need to add two libraries into Arduino IDE, but that is relatively easy).

The required components are:

The schematic for the converter is as follows:


 

Schematic for the board. Thank you Google Drawings, but could we have a circuit shape library available the next time?

 

Not that complicated, is it? Basically it boils down to few things:

One important note - The USB-pin on the Trinket board is an input while the Trinket is not connected to a computer for programming purposes. When you connect the Trinket through USB while programming it, IT WILL BE AN OUTPUT.  So when you build this project, BE SURE NOT TO SUPPLY THE ELECTRONICS FROM TWO SOURCES AT ONCE! Disconnect you external power while programming the Trinket through computer USB connection. Since I personally added row connectors to the Trinket, I did not have to deal with this - Trinket is removed from the PCB while programming it.



Testing the board with external power. Ugly as sin construction, but hey, it works.

When you get to the final construction, remember that the USB port on the Trinket needs an USB OTG (On The Go) -adapter. You can buy one as a connector module or as a short cable. This is needed because the USB connector on the trinket is standard USB Micro port and thus can not connect to a another USB device (or host other devices) - we need to present a Type A Host connector to the world (and to the connected USB keyboard/controller/what ever it is you're using this for).


USB OTG Cable.

The code and the required libraries

The code for the converter is from https://github.com/gdsports/midiuartusbh

Please check the site for the latest code. At the time of writing this guide (December 2019) the code was as it appears on later this page.

The code needs two libraries to work:

The first library is available directly from Arduino IDE - just open the Sketch / Include Library / Manage Libraries and search for "Forty" and click the install button. When that is done, the manager will show that the library is installed:

USB Host Library SAMD is not available from IDE Library Manager, you need to download the library as a ZIP file and import it manually. The library can be found at https://github.com/gdsports/USB_Host_Library_SAMD

From the web page, select the "Clone or download" button, and click the "Download ZIP":

After you have saved the library somewhere on your computer, add the library manually to Arduino IDE (there is no need to extract the ZIP file, just point the IDE to it):

Browse to the ZIP you downloaded and the IDE will take care of the rest.

And there you are! Now you can just copy and paste the code below to your Arduino editor, connect your Trinket M0 and press "Upload" - if all went well, the code will compile and IDE will flash it to the Trinket. Disconnect the Trinket, attach USB OTG adapter to the Micro USB port and connect your keyboard. If all went well, the activity LED will blink when you mash your keys. And if all worked perfectly, when you connect a MIDI synth to the MIDI connector it will play.

/*
  MIT License

  Copyright (c) 2018 gdsports625@gmail.com

  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.
*/

/*
   MIDI UART to MIDI USB host converter for SAMD21 and SAMD51 Arduino and
   Arduino compatible boards.
*/

#include <MIDI.h>       // MIDI Library by Forty Seven Effects
#include <usbh_midi.h>  // https://github.com/gdsports/USB_Host_Library_SAMD
#include <usbhub.h>

// 1 turns on debug, 0 off
#define DBGSERIAL if (0) SERIAL_PORT_MONITOR

USBHost UsbH;
USBH_MIDI MIDIUSBH(&UsbH);

#define MIDI_SERIAL_PORT Serial1

struct MySettings : public midi::DefaultSettings
{
  static const bool Use1ByteParsing = false;
  static const unsigned SysExMaxSize = 1026; // Accept SysEx messages up to 1024 bytes long.
  static const long BaudRate = 31250;
};

MIDI_CREATE_CUSTOM_INSTANCE(HardwareSerial, MIDI_SERIAL_PORT, MIDIUART, MySettings);

inline uint8_t writeUARTwait(uint8_t *p, uint16_t size)
{
  // Apparently, not needed. write blocks, if needed
  //  while (MIDI_SERIAL_PORT.availableForWrite() < size) {
  //    delay(1);
  //  }
  return MIDI_SERIAL_PORT.write(p, size);
}

uint16_t sysexSize = 0;

void sysex_end(uint8_t i)
{
  sysexSize += i;
  DBGSERIAL.print(F("sysexSize="));
  DBGSERIAL.println(sysexSize);
  sysexSize = 0;
}


void setup() {
  DBGSERIAL.begin(115200);

  MIDIUART.begin(MIDI_CHANNEL_OMNI);
  //MIDIUART.turnThruOff();

  if (UsbH.Init()) {
    DBGSERIAL.println(F("USB host failed to start"));
    while (1) delay(1); // halt
  }
}

void USBHost_to_UART()
{
  uint8_t recvBuf[MIDI_EVENT_PACKET_SIZE];
  uint8_t rcode = 0;     //return code
  uint16_t rcvd;
  uint8_t readCount = 0;

  rcode = MIDIUSBH.RecvData( &rcvd, recvBuf);

  //data check
  if (rcode != 0 || rcvd == 0) return;
  if ( recvBuf[0] == 0 && recvBuf[1] == 0 && recvBuf[2] == 0 && recvBuf[3] == 0 ) {
    return;
  }

  uint8_t *p = recvBuf;
  while (readCount < rcvd)  {
    if (*p == 0 && *(p + 1) == 0) break; //data end
    DBGSERIAL.print(F("USB "));
    DBGSERIAL.print(p[0], DEC);
    DBGSERIAL.print(' ');
    DBGSERIAL.print(p[1], DEC);
    DBGSERIAL.print(' ');
    DBGSERIAL.print(p[2], DEC);
    DBGSERIAL.print(' ');
    DBGSERIAL.println(p[3], DEC);
    uint8_t header = *p & 0x0F;
    p++;
    switch (header) {
      case 0x00:  // Misc. Reserved for future extensions.
        break;
      case 0x01:  // Cable events. Reserved for future expansion.
        break;
      case 0x02:  // Two-byte System Common messages
      case 0x0C:  // Program Change
      case 0x0D:  // Channel Pressure
        writeUARTwait(p, 2);
        break;
      case 0x03:  // Three-byte System Common messages
      case 0x08:  // Note-off
      case 0x09:  // Note-on
      case 0x0A:  // Poly-KeyPress
      case 0x0B:  // Control Change
      case 0x0E:  // PitchBend Change
        writeUARTwait(p, 3);
        break;

      case 0x04:  // SysEx starts or continues
        sysexSize += 3;
        writeUARTwait(p, 3);
        break;
      case 0x05:  // Single-byte System Common Message or SysEx ends with the following single byte
        sysex_end(1);
        writeUARTwait(p, 1);
        break;
      case 0x06:  // SysEx ends with the following two bytes
        sysex_end(2);
        writeUARTwait(p, 2);
        break;
      case 0x07:  // SysEx ends with the following three bytes
        sysex_end(3);
        writeUARTwait(p, 3);
        break;
      case 0x0F:  // Single Byte, TuneRequest, Clock, Start, Continue, Stop, etc.
        writeUARTwait(p, 1);
        break;
    }
    p += 3;
    readCount += 4;
  }
}

void UART_to_USBHost()
{
  if (MIDIUART.read()) {
    midi::MidiType msgType = MIDIUART.getType();
    DBGSERIAL.print(F("UART "));
    DBGSERIAL.print(msgType, HEX);
    DBGSERIAL.print(' ');
    DBGSERIAL.print(MIDIUART.getData1(), HEX);
    DBGSERIAL.print(' ');
    DBGSERIAL.println(MIDIUART.getData2(), HEX);
    switch (msgType) {
      case midi::InvalidType:
        break;
      case midi::NoteOff:
      case midi::NoteOn:
      case midi::AfterTouchPoly:
      case midi::ControlChange:
      case midi::ProgramChange:
      case midi::AfterTouchChannel:
      case midi::PitchBend:
        {
          uint8_t tx[4] = {
            (byte)(msgType >> 4),
            (byte)((msgType & 0xF0) | ((MIDIUART.getChannel() - 1) & 0x0F)), /* getChannel() returns values from 1 to 16 */
            MIDIUART.getData1(),
            MIDIUART.getData2()
          };
          MIDIUSBH.SendRawData(sizeof(tx), tx);
          break;
        }
      case midi::SystemExclusive:
        MIDIUSBH.SendSysEx((uint8_t *)MIDIUART.getSysExArray(),
            MIDIUART.getSysExArrayLength(), 0);
        DBGSERIAL.print("sysex size ");
        DBGSERIAL.println(MIDIUART.getSysExArrayLength());
        break;
      case midi::TuneRequest:
      case midi::Clock:
      case midi::Start:
      case midi::Continue:
      case midi::Stop:
      case midi::ActiveSensing:
      case midi::SystemReset:
        {
          uint8_t tx[4] = { 0x0F, (byte)(msgType), 0, 0 };
          MIDIUSBH.SendRawData(sizeof(tx), tx);
          break;
        }
      case midi::TimeCodeQuarterFrame:
      case midi::SongSelect:
        {
          uint8_t tx[4] = { 0x02, (byte)(msgType), MIDIUART.getData1(), 0 };
          MIDIUSBH.SendRawData(sizeof(tx), tx);
          break;
        }
      case midi::SongPosition:
        {
          uint8_t tx[4] = { 0x03, (byte)(msgType), MIDIUART.getData1(), MIDIUART.getData2() };
          MIDIUSBH.SendRawData(sizeof(tx), tx);
          break;
        }
      default:
        break;
    }
  }
}

void loop()
{
  UsbH.Task();

  if (MIDIUSBH) {
    /* MIDI UART -> MIDI USB Host */
    UART_to_USBHost();
    
    /* MIDI USB Host -> MIDI UART */
    USBHost_to_UART();
  }
}