SAMD51 USB MIDI Multi-Pot Mozzi Synthesis

When I left my Trinket USB MIDI Multi-Pot Mozzi Synthesis – Part 3 I had the following:

  • Mozzi synthesis on the Trinket M0 board (that uses the SAMD21 Cortex M0+ processor core at 48MHz).
  • Four note polyphony.
  • USB device functionality – it can be controlled via MIDI over USB from a PC.
  • USB host functionality – you can plug in a USB MIDI device and control it.

But as you may recall if you were following along, I got the feeling I was pushing the Trinket M0 to its limits supporting Mozzi polyphonic synthesis and USB host functionality, so was wondering about trying it on a more powerful board.

Then I got an Adafruit ItsyBitsy in the post, which is a slighter bigger version of the Trinket, but it comes in two versions – an M0 with the same SAMD21 core and a M4 with a SAMD51 Cortex M4 running at 120MHz. I now have an IstyBitsy M4 so wanted to get Mozzi running.  But there is a problem – at the time of writing, Mozzi doesn’t support SAMD51 processors.

This post describes what I did to get it (apparently) working.

Warning! I strongly recommend using old or second hand equipment for your experiments.  I am not responsible for any damage to expensive instruments!

This is an advanced post requiring detailed low-level updating of an Arduino library.  If you are new to Arduino, see the Getting Started pages.

Parts list

  • Adafruit ItsyBitsy M4
  • Adafruit Trinket M0 (optional)
  • 2x 1kΩ resistors (optional)
  • 1x 10uF non-polar capacitor (optional)
  • 1x 3.5mm jack socket
  • Amplification
  • Breadboard and jumper wires
  • Either USB MIDI controller device or a USB MIDI link to a PC

The Circuit

ItsyBitsyUSBMIDIMultiPotSynth_bb

This is an ItsyBitsy version of the circuit used with Trinket USB MIDI Multi-Pot Mozzi Synthesis – Part 3.  The connections to the ItsyBitsy are as follows:

  • A0 – DAC output to the optional filter circuit (2x resistors and non-polar cap) then to the jack socket.
  • A2 – Modulation ratio pot
  • A3 – Modulation rate pot
  • A4 – FM intensity pot
  • A5 – Wave table pot

Once again if using a USB MIDI device that doesn’t have its own power supply and is relying on being powered from the host (the ItsyBitsy) the I recommend adding a grounding connection to the USB outer connector.  When in host mode the system needs powering via the ItsyBitsy USB pin with a 5V connector, not from the USB connection.

DO NOT CONNECT 5V TO THE BREADBOARD POWER RAILS.

If used in USB device mode, the ItsyBitsy can be powered via the micro-USB connector from the PC.

In both cases that the solderless breadboard’s power rails are running at 3V.

Here are the Trinket M0 and ItsyBitsy M4 versions of the circuit side by side.

2021-04-16 20.08.34

The Code

Mozzi does not at present support the SAMD51 processor.  The main issue is that Mozzi uses timers to write samples out to the DAC at the AUDIO_RATE.  For the Arduino it uses the built-in timer/counters and for the SAMD21 it is no different, but each processor architecture has a different way to configure the timers.  And the SAMD51 is different again to the SAMD21.

Thankfully, everything we need to know can be found in this blog post.  So here are the changes required to convert your Mozzi library over to support the SAMD51 based ItsyBitsy M4.

Note: These changes are applicable to the version of the Mozzi library available in March 2021. It has already moved on slightly since these changes were made, but hopefully there is enough information here to be able to apply the changes on future versions of the library until the SAMD51 architecture is officially supported.

Also note that if you update the library you will have to re-apply these patches.

Mozzi is downloaded from here: https://github.com/sensorium/Mozzi.

In general the approach was: look for any mention of SAMD21 in the code or SAMD in general and work out what the new code should look like for the SAMD51.

Oh and this is assuming the Adafruit version of the SAMD core.  The original Arduino version doesn’t support the SAMD51 at all at the time of writing, but the Adafruit version includes definitions and code for all of their SAMD boards.

There is a macro defined called IS_SAMD21(). Wherever this appears in the code there will be something to be done.

AudioConfigSAMD51.h

This is a new file that needs to be added, but you can start with a copy of AudioConfigSAMD21.h.  Here are the complete contents of this new file:

#ifndef AUDIOCONFIGSAMD51_H
#define AUDIOCONFIGSAMD51_H

/* SAMD51 has 12 bits DAC */
#define AUDIO_BITS 12

/** @ingroup core
*/
/* Used internally to put the 0-biased generated audio into the centre of the output range (12 bits) */
#define AUDIO_BIAS ((uint16_t) 1 << (AUDIO_BITS - 1))

#define AUDIO_CHANNEL_1_PIN DAC0

#endif // #ifndef AUDIOCONFIGSAMD51_H

hardware_defines.h

This file contains a number of “helper” macros that are used to work out what architecture Mozzi is being built for.  The critical section for us is around line 16 as follows:

#define IS_AVR() (defined(__AVR__)) // "Classic" Arduino boards
#define IS_SAMD21() (defined(ARDUINO_ARCH_SAMD))
#define IS_TEENSY3() (defined(__MK20DX128__) || defined(__MK20DX256__) || defined(__MK64FX512__) || defined(__MK66FX1M0__) || defined(__MKL26Z64__) ) // 32bit arm-based Teensy
#define IS_STM32() (defined(__arm__) && !IS_TEENSY3() && !IS_SAMD21()) 
#define IS_ESP8266() (defined(ESP8266))
#define IS_ESP32() (defined(ESP32))

#if !(IS_AVR() || IS_TEENSY3() || IS_STM32() || IS_ESP8266() || IS_SAMD21() || IS_ESP32())
#error Your hardware is not supported by Mozzi or not recognized. Edit hardware_defines.h to proceed.
#endif

We need to do three things:

  • Add in an IS_SAMD51() test.
  • Make sure the IS_SAMD21() test isn’t triggered by mistake when building for the SAMD51.
  • Add our IS_SAMD51 to the last test to show we are now “supported hardware”.
  • Oh, and it looks like we’ll also need to add a !IS_SAMD51() part to the IS_STM32() test too.

Here is the new version.

#define IS_AVR() (defined(__AVR__)) // "Classic" Arduino boards
#define IS_SAMD51() (defined(__SAMD51__)) // Adafruit SAMD51 based boards
#define IS_SAMD21() (defined(ARDUINO_ARCH_SAMD) && !IS_SAMD51())
#define IS_TEENSY3() (defined(__MK20DX128__) || defined(__MK20DX256__) || defined(__MK64FX512__) || defined(__MK66FX1M0__) || defined(__MKL26Z64__) ) // 32bit arm-based Teensy
#define IS_STM32() (defined(__arm__) && !IS_TEENSY3() && !IS_SAMD21() && !IS_SAMD51())
#define IS_ESP8266() (defined(ESP8266))
#define IS_ESP32() (defined(ESP32))

#if !(IS_AVR() || IS_TEENSY3() || IS_STM32() || IS_ESP8266() || IS_SAMD21() || IS_ESP32() || IS_SAMD51())
#error Your hardware is not supported by Mozzi or not recognized. Edit hardware_defines.h to proceed.
#endif

The Adafruit SAMD core defines __SAMD51__ for any of the SAMD51 based boards in addition to ARDUINO_ARCH_SAMD, so I take that as my cue.

But note how I now have to tell the IS_SAMD21 test to check it isn’t a SAMD51, as there doesn’t seem to be an equivalent __SAMD21__ definition.  It can only rely on the more generic ARDUINO_ARCH_SAMD unfortunately.

AudioOutput.h

This is where the board specific audioOutput() routine is provided.  There is an “#if IS_SAMD21()”/ “#endif” pair of instructions around line 217 so an equivalent can be added just after for the SAMD51 as follows.

///////////////////// SAMD51
#if IS_SAMD51()
#include "AudioConfigSAMD51.h"
inline void audioOutput(const AudioOutput f)
{
  analogWrite(AUDIO_CHANNEL_1_PIN, f.l()+AUDIO_BIAS);
}
#endif

MozziGuts.h

Around line 180 there are a series of “#if board / #elif other board / #elif other board / ” statements that work out which of the AudioConfig files to include, so I just add in the IS_SAMD51() entry here too as follows:

...
#elif IS_SAMD21()
#include "AudioConfigSAMD21.h"
// New code starts
#elif IS_SAMD51()
#include "AudioConfigSAMD51.h"
// New code ends
#elif IS_AVR() && (AUDIO_MODE == STANDARD)
...

MozziGuts.cpp

Ok, as you might imagine from the name, this is where most of the detail comes in. There are the following key changes required in this file:

  • We need to add in code to configure and run the SAMD51 timers.  This is where the majority of that blog post comes into play and that involves taking the code for the SAMD21 and duplicating it, then re-writing it for the SAMD51.
  • We need to add in a reference to the new timer’s interrupt handler.
  • We need to provide the samd51AudioOutput() function, which is based on the existing samd21AudioOutput() function.
  • in startAudioStandard we need set up the DAC output resolution and initialise the timers for the SAMD51.
  • in stopMozzi we need an empty case to skip the AVR specific “clean up the timers” code

Taking these in order:

Around line 235 in the original file there is a large “#if IS_SAMD21 / ” block that contains the following functions:

  • tcIsSyncing()
  • tcReset()
  • tcEnd() – which is commented out
  • tcConfigure()

These need to be duplicated and re-written for the SAMD51, replacing the use of TC5 in the original with TC0, giving the following code:

#if IS_SAMD51()
// These are ARM SAMD51 Timer 0 routines to establish a sample rate interrupt
// (Based on the code for the ARM SAMD21 Timer 5 routines above)
static bool tcIsSyncing() {
  return (TC0->COUNT16.SYNCBUSY.reg > 0);
}

static void tcReset() {
  // Disable TCx
  TC0->COUNT16.CTRLA.bit.ENABLE = 0;
  while (TC0->COUNT16.SYNCBUSY.bit.ENABLE)
    ;
  // Reset TCx
  TC0->COUNT16.CTRLA.reg = TC_CTRLA_SWRST;
  while (TC0->COUNT16.SYNCBUSY.bit.SWRST)
    ;
}

static void tcConfigure(uint32_t sampleRate) {
  // Enable GCLK0 for TC0 (timer counter input clock)
  GCLK->PCHCTRL[TC0_GCLK_ID].reg = (GCLK_PCHCTRL_CHEN | GCLK_PCHCTRL_GEN_GCLK0);

  tcReset();

  // Set Timer counter Mode to 16 bits
  TC0->COUNT16.CTRLA.reg |= TC_CTRLA_MODE_COUNT16;

  // Set TC0 mode as match frequency
  TC0->COUNT16.WAVE.reg |= TC_WAVE_WAVEGEN_MFRQ;

  TC0->COUNT16.CTRLA.reg |= TC_CTRLA_PRESCALER_DIV1 | TC_CTRLA_ENABLE;
  TC0->COUNT16.CC[0].reg = (uint16_t)(SystemCoreClock / sampleRate - 1);
  while (tcIsSyncing())
    ;

  // Configure interrupt request
  NVIC_DisableIRQ(TC0_IRQn);
  NVIC_ClearPendingIRQ(TC0_IRQn);
  NVIC_SetPriority(TC0_IRQn, 0);
  NVIC_EnableIRQ(TC0_IRQn);

  // Enable the TC0 interrupt request
  TC0->COUNT16.INTENSET.bit.MC0 = 1;
  while (tcIsSyncing())
    ;
}
#endif

Then around about line 350 in the original, or approx line 405 in our updated file with the above code added, there is another “#IS_SAMD21() / ” block defining TC5_Handler.  We need the equivalent for TC0 and the SAMD51.

#if IS_SAMD51()
void TC0_Handler(void) __attribute__((weak, alias("samd51AudioOutput")));
#endif

Around line 380 of the original (445 of the modified) file there is another “#if IS_SAMD21() / ” block defining samd21AudioOutput(). Once again we need to follow it with a SAMD51 version.

#if IS_SAMD51()
#ifdef __cplusplus
extern "C" {
#endif
void samd51AudioOutput() {
  defaultAudioOutput();
  TC0->COUNT16.INTFLAG.bit.MC0 = 1;
}
#ifdef __cplusplus
}
#endif
#endif

We now need an entry in the startAudioStandard function, but there are several versions of this function, so beware.  We need to update the version within the “#if !IS_AVR” section, which in the original file is around line 395 (473 in my updated file).  This function as a series of “#if / #elif / #elif / ” for the different non-AVR board options, so we can look for the “#if IS_SAMD21() / #elif” block and add in one for the SAMD51.

The main difference from the SAMD21 version is that we want to set the output resolution of the DAC to 12 for the SAMD51, compared to 10 for the SAMD21.  This is done with the analogWriteResolution() call.

It is a bit complicated as there are a lot of nested #if statements in this section – so keeping track is tricky!  But here is the new code to add after the IS_SAMD21() block and before the “#elif IS_ESP32()…” block.

#elif IS_SAMD21()
#ifdef ARDUINO_SAMD_CIRCUITPLAYGROUND_EXPRESS
{
  static const int CPLAY_SPEAKER_SHUTDOWN = 11;
  pinMode(CPLAY_SPEAKER_SHUTDOWN, OUTPUT);
  digitalWrite(CPLAY_SPEAKER_SHUTDOWN, HIGH);
}

#endif
analogWriteResolution(10);
#if (EXTERNAL_AUDIO_OUTPUT != true)
analogWrite(AUDIO_CHANNEL_1_PIN, 0);
#endif
tcConfigure(AUDIO_RATE);

// New code starts
#elif IS_SAMD51()
analogWriteResolution(12);
#if (EXTERNAL_AUDIO_OUTPUT != true)
analogWrite(AUDIO_CHANNEL_1_PIN, 0);
#endif
tcConfigure(AUDIO_RATE);
// New code ends

#elif IS_ESP32() && (BYPASS_MOZZI_OUTPUT_BUFFER != true) // for external audio output, set up a timer running a audio rate

Finally, we need to add a line in stopMozzi (around line 715 in the original or 800 in the modified file):

#elif IS_SAMD21()
#elif IS_SAMD51()
#elif IS_ESP32()
#else

If all has gone to plan, and you’ve managed to keep track of the different nested compilation options, that should now successfully build for a SAMD51 board using the Adafruit SAMD Arduino core.

USB MIDI Mozzi Synthesis

To use the new library, you should be able to pretty much take any previous code and re-build it.  I’ve taken the code from Trinket USB MIDI Multi-Pot Mozzi Synthesis – Part 3 but it did need a few tweaks as follows:

  • I had to remove the analogFastRead library and revert back to the old built-in analogRead as the fast library doesn’t support the SAMD51. I might take a look at this another time, but for now, the old one is fine.
  • I added in some code to turn off the built-in dot-star LED from the ItsyBitsy.  This code is dependent on the Adafruit board in use though as the pins used for the dotstar are different for different boards, but the basic idea is to have the following at the top somewhere:
#include <Adafruit_DotStar.h>
Adafruit_DotStar strip = Adafruit_DotStar(1,8,6,DOTSTAR_BRG); // ItsyBitsy M4 Data=8; Clock=6
void dotStarOff () {strip.begin(); strip.clear(); strip.show();}

And then call dotStarOff() from your setup() code.  If you want to make it conditional, then Adafruit include a board-specific definition for each build, so I could hide this within a “#if (ADAFRUIT_ITSYBITSY_M4_EXPRESS) / ” block and add in other versions for the Trinket M0 or ItsyBitsy M0 too.

  • The pin numbers for the potentiometers need setting up for the ItsyBitsy circuit too (as listed above).

Find my updated application on GitHub here.

I’ve not included the USB host files, analogReadFast (if using the Trinket) or the updated Mozzi library though so to get it building, you’ll have to follow the above instructions and details in the previous tutorials.

Closing Thoughts

This is hopefully a short-lived post. I wanted to document what was necessary to get Mozzi running on the SAMD51 and have a video of it working, but I hope that at some point these changes can be added to the official Mozzi library.  Watch this space.

But for now, in the video you can see first the Trinket M0 version and then the ItsyBitsy M4 version of the same code running and providing four-note polyphonic Mozzi synthesis while being driven by a USB MIDI keyboard.

This should now give me a platform for some detailed investigations of the performance of the USB MIDI stack and synthesis at the same time.

Kevin

Leave a comment