Arduino Direct Digital (Additive) Synthesis

This is actually a collection of previous projects with the code tidied up a little and combined to show a single sketch that can be configured for either PWM, an R2R ladder or the MCP4725 I2C DAC.

There isn’t really anything here that hasn’t been talked about before somewhere, but hopefully this can act as a single reference point for a range of direct digital synthesis techniques from now on.

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 Arduino tutorials for the main concepts used in this project:

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

Parts list

The Circuit

There is no specific circuit dedicated to this post, but it works well with the two audio experimenter PCBs listed above, which each contain options for the following:

  • PWM audio output on either D3 or D9.
  • R2R resistor DAC (in the case of the Uno experimenter PCB).
  • MPC4725 I2C DAC.

Alternatively, the main elements for PWM operation can be put together on a solderless breadboard as follows.

The Code

As already mentioned the core elements of the code have largely been met before, but the main sections are described below.

The general theory of direct digital synthesis operation is described fully in Arduino R2R Digital Audio – Part 3 so I won’t go over that again.

This code implements simple additive synthesis by using potentiometers to set the amplitudes for a set of sine wave harmonics. The performance of the Arduino largely limits this to being a maximum of six sine waves to be added up, but that is enough for some simple experimentation.

The code configures the first six harmonics: fundamental (f), f*2, f*3, f*4, f*5, f*6. There are some default ranges that can be used for testing without pots:

#define SC  32
int sine[MAXPOTS] = {SC*2,0,0,0,0,0};
int saw[MAXPOTS] = {SC,SC/2,SC/4,SC/8,SC/16,SC/32};
int squ[MAXPOTS] = {SC,0,SC/2,0,SC/4,0};

There is no way to set the fundamental frequency – it is fixed at 440Hz. Making that controllable is left as an exercise for another day! The natural options are MIDI triggering or another pot.

The basic properties of the synthesis code are as follows:

  • The sample rate depends on the technique, but in principle it could support 4096Hz, 8192Hz, 16384Hz or 32768Hz.
  • It uses a 256 entry, 8-bit wavetable to define the basic sine wave.
  • It uses 8.8 fixed point accumulators with the top 8-bits as the index into the wave table.
  • It uses a 16-bit sample value which is scaled down as required by the audio output method.

The general pattern used in this code is as follows:

// Audio output specific functions:
dacSetup ()
dacWrite (value)
dacScan ()

// Generic audio functions:
dacPlayer ()
Call dacWrite (last calculated sample value)
For each potentiometer:
Update accumulator for the DDS
Add potval * sinetable[accumulator>>8] to the total

setup ()
Call dacSetup ()

loop ()
Call dacScan ()
Every 10 loops update the pots

Each audio output option will implement the three functions dacSetup, dacWrite and dacScan, but not all need to be used. Conditional compilation is used to select between audio output options by defining one of PWM_OUTPUT, DAC_OUTPUT or R2R_OUTPUT.

Here are some notes for each option.

PWM:

  • Output scaled to 8-bits for use with PWM.
  • PWM is configured to run at 65536Hz.
  • The TIMERn_OVF_vect interrupt is used to trigger sample updating via dacPlayer().
  • All four sample rates are possible so samples are not written out on every interrupt. For example, for a sample rate of 16384Hz a sample is written out on every fourth interrupt.
  • Can support either D9 (Timer 1) or D3 (Timer 2).
  • As updates are interrupt driven, dacScan () is empty.

R2R:

  • Uses D8-D9, D2-D7 as bits 0 to 7 for the DAC output.
  • Output scaled to 8-bits for use.
  • PORT I/O is used to write to the data lines.
  • Code takes into account the fact that D0/D1 might be in use as the UART.
  • Updates are interrupt driven using the TimerOne library, calling dacPlayer() directly.
  • As updates are interrupt driven, dacScan () is empty.

MCP4725:

  • The I2C address for the DAC is configured by defining MCP4725ADDR. It defaults to 0x60.
  • As the DAC can’t be written to from an interrupt routine, the output is set during dacScan() so the loop() has to run as fast as possible.
  • The sample rate is set by monitoring the micros() tick (note on an Arduino the resolution is 4uS at best).
  • Uses the non-blocking I2C library and fast analog read from Mozzi.
  • Uses the MCP4725 fast write mode, which only requires two bytes to be sent to the DAC.
  • The maximum sample rate is 8192Hz and even then it runs a little slow (i.e. the 440Hz tone is flat by around a semitone).

General comments:

  • There is an optional timing pin that is configured by defining TIMING_TEST. This is toggled in dacPlayer().
  • There is an optional fixed set of amplitudes that can be used instead of potentiometers. These are set up in setDefaultAmplitudes() when DAC_TEST is defined.
  • The maximum number of pots supported is 6. The code skips using A4/A5 as these map onto I2C if the DAC is used. The number of pots to scan (and hence sine waves to add up) can be reduced by setting NUMPOTS to a number less than MAXPOTS (which is 6).
  • If the number of pots is reduced, then the scaling factors used to calculating the totals can be adjusted by changing SC and PSC accordingly. For 6 pots/waves they are set to 32 and 4 respectively. This means that analogReads have a maximum range of 0..31 which is set by
val = analogRead(pot) >> PSC;

Find it on GitHub here.

Closing Thoughts

This has been interesting to revisit. After all my experiments this is starting to finally make some sense. It has been interesting to contrast the three output methods both in terms of their computational performance and in terms of output waveform quality.

The photo at the start shows the R2R output of the test sine wave. The photo below is the PWM output for a potentiometer-driven saw.

This has also prompted me to revisit my Arduino PWM Output Filter Circuit and finally work out how to properly combine a low-pass filter and potential divider and still get something approximating the filter characteristics I wanted. I feel I understand quite a bit more about what is going on now.

Now if I could just get a bit of a handle on impedance I might actually start to feel like I know a little about what I’d be talking about….

Kevin

Leave a comment