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
- Download and install Arduino IDE. https://www.arduino.cc/en/Main/Software
- Install the drivers for Adafruit SAMD boards.
https://github.com/adafruit/Adafruit_Windows_Drivers/releases/latest
- The driver package has drivers for may boards, but the critical
one is of course the Trinket M0 driver. Default options are probably
a-ok - the first listed driver actually contains the driver for
Trinket M0, not the one that says "Trinket" (that one is for the
older Trinket):
- Installnextnext and so on, now you have the correct drivers installed.
- The driver package has drivers for may boards, but the critical
one is of course the Trinket M0 driver. Default options are probably
a-ok - the first listed driver actually contains the driver for
Trinket M0, not the one that says "Trinket" (that one is for the
older Trinket):
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).
- Open the Arduino IDE and select File/Prefreneces.
- Add the
https://adafruit.github.io/arduino-board-index/package_adafruit_index.json
...to the "Additional Board Manager URLs" -field, so that the IDE will find configuration and tool chain stuff for the Adafruit boards. Like in the picture below.
- Now you can add the Adafruit boards from Arduino IDE Board Manager
(Tools/Board/Boards Manager). I found out that you really,
really need to add TWO board packages - first the "Arduino SAMD
Boards (32-bit ARM Cortex-M0+)" and the "Adafruit SAMD Boards".
Otherwise something in the tool chain is missing and the compiler won't
work. But, just install both and be happy.
- And that's it! Now you have the support for your Trinket M0 board in Arduino IDE.
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:
- Adafruit Trinket M0 board ( https://www.digikey.fi/product-detail/en/adafruit-industries-llc/3500/1528-2361-ND/7623049 )
- 74125 tri-state buffer (74HC125, 74LS125)
- 2 pcs 330 Ohm resistors for current limiting the LEDs
- 2 pcs 220 Ohm resistors for protecting the MIDI circuit
- 2 LEDs (I had a few tower LEDs available, 2 LEDs stacked on a single plastic angled connector, but use whatever you want)
- 0.1 uF ceramic capacitor for 74125 power decoupling
- Through-hole 180 degree 5-pin DIN connector (for MIDI out)
- USB OTG cable or adapter for connecting the USB keyboard
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:
- Trinket M0 will work on 5 volts, since it has an internal 3.3V regulator.
- The chip itself on the board is 3.3 volts. So the pins that output stuff are 3.3 volts.
- That MIGHT be enough, but the easy cop-out is to use TTL level buffer from 74 series, the 74125, which will boost the signal output to 5 volts and adhere to the MIDI standard. HC version of the chip is preferred (74HC125) but the old LS (74LS125) will work just fine. HC is a bit more tolerant to all kind of things, but who cares. Also the 74125 is cheaper than the Trinket, so if anything goes amiss it is probably easier to replace the buffer circuit than the fancy computing part.
- Since we have four buffers on the 74125, let's use one of them to drive activity LED to show that we're actually sending MIDI data. Can be omitted, just skip installing the current limiting resistor, LED and the pins 11, 12 and 13 on the 74125.
- Power LED (and the corresponding current limiting resistor) is also not necessary if you are that cheap.
- Decoupling capacitor (0.1 uF) needs to be as near as possible the VCC and GND pins of the 74125. Any ceramic cap will do nicely. For the uninitiated: TTL chips eat a lot of current while changing state. The transient current will make the supply power fluctuate, so you need to have a little local reservoir for power, otherwise you will end up with some really weird effects when the voltage drops suddenly (probably the Trinket will reset and so on).
- I soldered a 2.54mm Arduino-style row connectors to the Trinket board and matching pin connectors on the circuit board, so that I could easily remove the Trinket for programming and put it back again. I did not try to program the Trinket while it was connected to the circuit board - I wanted the things to be as clear as possible (trinket connected only to the computer while programming, trinket on the PCB while testing it).
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:
- MIDI Library by Forty Seven Effects
- USB Host Library SAMD
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(); } }