Trinket FM Synthesis with Mozzi

Having played around a little with the Adafruit Trinket M0 I’m now starting to spend some time learning about the SAMD21 processor on board as part of an investigation into 32-bit ARM processors and their possibilities for synthesis.

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

  • Adafruit Trinket M0 (or probably any other Adafruit M0 board)
  • 1x 10uF non-polar capacitor (optional)
  • 2x 1kΩ resistors (optional)
  • 2x 10kΩ potentiometers
  • 3.5mm jack socket (optional) and amplification
  • Breadboard and jumper wires

The Circuit

TrinketSynth_bb

This has two potentiometers connected up to the Trinket’s GPIO 3 and 4 and a basic output stage hooked up to the Trinket’s DAC output on GPIO 1.  One of the interesting things for me about the SAMD21 based boards is the fact they have a true digital to analog converter output (DAC), so this is partly to test that.

The native output would be a 0 to 3.3V biased signal, which in principle could be plugged directly into some kind of audio output, but it is much better to include a simple output stage consisting of two things:

  • A capacitor to remove the DC offset bias to turn it onto a +/- signal rather than a 0/VCC signal.
  • A potential divider to drop the voltage to more line-level friendly output levels.

I’ve used two 1kΩ resistors for the potential divider which will turn the 0 to 3.3V signal into a 0 to 1.65V signal (or thereabouts).  With the DC offset removed by the capacitor, this turns into a +/- 800mV signal max (again or thereabouts).

In practice once plugged into my usual “sacrificial” amp, I’m seeing a signal of around +/- 650mV which is fine.

Everything is powered via the micro-USB.

2021-03-20 16.26.44

The Code

I’ve gone back to experimenting with Mozzi.  But before I got into that, I wanted to check that the basic DAC was working and I understood what was going on.

One of the advantages of using a more powerful processor is the extensive support Adafruit provide for their boards with their CircuitPython distribution.  It includes an “audioio” set of libraries.  But unfortunately it isn’t supported on the Trinket M0 – you need one of the slightly more powerful boards that includes some additional flash memory for storage before you can use that, so until I get hold of a Feather or ItsyBitsy or a Metro, M0, CircuitPython is not an option for this one right now.

But then part of the reason for 32-bit processors was extra performance so native Arduino IDE based 32-bit code is more appealing for me anyway.  And if I want to play with Mozzi again, this is the way to go anyway.

Initial SAMD DAC Test

This is my first simple Arduino “test code” to see if everything was working.

#define DAC_PIN A0
#define POT A4

uint16_t wave=0;
uint16_t wavestep=8;

void setup() {
analogWriteResolution(10);
}

void loop() {
#ifdef POT
int pot = analogRead(POT);
int pot_delay = 1024-pot+5;
#else
int pot_delay = 18;
#endif

delayMicroseconds(pot_delay);
wave += wavestep;
wave &= 0x3FF; // Auto wrap at 10 bits
analogWrite(DAC_PIN, wave);
}

This creates a saw-tooth wave moving from 0 to 1023 (the highest 10-bit value) with the value sent straight out to the DAC.  Optionally the delay between writing values can be linked to a potentiometer on A4.

I’ve used analogWriteResolution to set the output resolution of the DAC to 10 bits.  By default the input resolution is already 10 bits as that was the default for the original Arduino.  The SAMD21E processor used on the Trinket actually has an ADC resolution of 12-bits on the input side, so I could have used analogReadReasolution(12) to change it to read a value between 0 and 4095, although it still corresponds to the same voltage range (0 to 3.3V).

Mozzi FM Synthesis

So with everything working so far, we now come to Mozzi.  As described in my previous projects this is an Arduino-based synthesis library initially for 8-bit microcontrollers, but these days supporting so much more.  I was encouraged to see mention of the Adafruit Circuit Playground Express and the Gemma M0 on the “getting started” page, so guessed the Trinket M0 should work fine too as it uses the same processor as the Gemma M0.

If you haven’t already got hold of Mozzi, then you need to download the ZIP file from the main repository via the “get code” button and then use the Arduino’s Sketch -> Include Library -> Add .ZIP Library function to add it to your installation.

My initial try used some of the example code and I did get some sounds out of it but it wasn’t that great!  On looking at it with an oscilloscope, I did wonder if it was trying to do PWM rather than use the DAC directly, but it might just have been excessive clipping of the signal.

The short version is that I had an old version of the Mozzi library which wasn’t setting the right output resolution for the DAC on SAMD21 based boards.  On updating to the latest version, all was good.

There were two examples I was keen to have a proper go with:

  • Examples -> Mozzi -> 06.Synthesis -> FMSynth
  • Examples -> Mozzi -> 03.Sensors -> Knob_LightLevel_FMSynth

The first will run “as is” and is a good way to check that Mozzi has built and is running successfully. I had a few compiler warnings pop up, but nothing that caused me concern.  This is a good way to hear what can be done with Mozzi.

The second requires a few minor updates for the Trinket:

  • KNOB_PIN and LDR_PIN need to be set to the two pins used for the potentiometers, in this case 3 and 4 respectively.
  • mozziAnalogRead doesn’t work for the Trinket M0, and possibly not for any SAMD21 boards, so it needs to be changed to the standard analogRead function. I’m not sure what the difference is, other than mozziAnalogRead is described as a “fast analog read function”.

You can see and hear the second example in action in the video.

Advanced Discussion

These are a few “field notes” from digging around in the MozziGuts.cpp file to try to work out why I wasn’t get very good sound quality initially. I wanted to make sure it was really using the native DAC for the SAMD21.

After some quite extensive following through of various definitions, I worked out there is an IS_SAMD21() test that looks for ARDUINO_ARCH_SAMD, but I wasn’t sure if this was being picked up or not for the Trinket.  Looking at the “boards.txt” file for the Adafruit Trinket M0 shows that there are several defines being set:

adafruit_trinket_m0.build.extra_flags=-DCRYSTALLESS -DADAFRUIT_TRINKET_M0 -D__SAMD21E18A__ -DARM_MATH_CM0PLUS {build.usb_flags}

So I enabled the “verbose output” for compilation within the IDE and had a look at what was actually being set.  Turns out it was all fine after all – I could see a -DARDUINO_ARCH_SAMD on the command line, so I had to look elsewhere.

The parts of MozziGuts.cpp relevant to audio output have a number of definitions to work out which architectures are in use: IS_SAMD21(), IS_TEENSY3(), IS_STM32(), IS_AVR() so you have to work quite hard to find the code that is relevant to what is going on for your specific board.

In the end the output chain for the SAMD21 looks something like the following:

In AudioConfigSAMD21.h:

#define AUDIO_CHANNEL_1_PIN DAC0

Where DAC0 comes from the Adafruit Arduino-SAMD core file variants/trinket_m0/variant.h:

#define PIN_A0 (14ul)
...
#define PIN_DAC0 PIN_A0
...
static const uint8_t DAC0 = PIN_DAC0;

So that was all fine.

In MozziGuts.cpp there are a number of routines that setup the TC5 timer  on the SAMD21 to drive the interrupt for use as the AUDIO_RATE output in Mozzi.  Then we get to the output stage itself.  Here are the relevant parts of code within the IS_SAMD21() sections.

static void CACHED_FUNCTION_ATTR defaultAudioOutput() {
audioOutput(output_buffer.read());
}

void TC5_Handler(void) __attribute__((weak, alias("samd21AudioOutput")));

void samd21AudioOutput() {
defaultAudioOutput();
}

So the interrupt handler for the TC5 interrupt is tied to samd21AudioOutput, which calls defaultAudioOutput, which calls audioOutput… which gets us to AudioOutput.h:

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

So that all connects up fine.  But then I noticed the following in startAudioStandard:

#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);

On further poking around in the GitHub repository, I found a reference in the history of MozziGuts.cpp to a change from January this year, where the initial audio resolution for the SAMD21 wasn’t set correctly and had been fixed.

Sure enough, if my own version of MozziGuts.cpp this line is calling analogWriteResolution(12).  So after all this, simply updating the Mozzi library to the most recent one has fixed the issue.

Closing Thoughts

I love this little board.  I’d like to know why the CircuitPython audioio library isn’t available for it though, as I would like some demonstrations of synthesis (or at least proper DAC audio out) in CircuitPython, but the main thread of exploration right now is seeing how far I can push on with with Mozzi.

I might spend a bit of time re-creating some of my previous Mozzi projects now, especially the MIDI ones as one thing I’d really like to know is if the board can handle a MIDI host USB stack at the same time as performing direct digital synthesis… watch this space!

I should also try it on the Circuit Playground Express now I know that is supported too and I might succumb to the temptation to get some of the Trinket’s bigger siblings – maybe an ItsyBitsy or a Feather or even a Metro, M0.

It would also be interesting to know how much further it is possible to go with one of the M4 (SAMD51) based boards, but I’m not sure they are supported in Mozzi yet, but that is an investigation for another day.

Kevin

Leave a comment