The STM32F4 ARM microcontroller (as used on the cheap-as-chips Discovery board from ST, who make the microcontroller) includes two 12-bit DAC. If this isn’t enough outputs, you can either pile on the external SPI DACs, or use simple sample-and-hold buffers for multiplexing. Basically, instead of outputting a single stream of values on each DAC, you send out the samples for each channel sequentially, so first a sample from channel 1, then a sample from channel 2, etc., etc. These are then sent through a 1-to-n-channel switch whose state is changed in time with the stream so that the samples from channel 1 go to the switch’s first output, channel 2 to the second output, etc. After each output, the demultiplexed voltage goes over the top of a capacitor with its other pin tied to ground through to a normal non-inverting opamp buffer:
I’ve used smallish ceramic disc capacitors of in the nanofarad range for this. If you know about such things you can probably calculate an optimal value for the capacitor; the crucial thing is that capacitors take some time to charge and lose some charge over time, so for the output voltage to remain stable, you have to pick the right value.
The number of channels you can get out of a single DAC in this manner depends on a number of factors: primarily, the desired sampling rate and how long it takes for your DAC to settle. Say you want a 50khz signal, for one channel that’s a sample every 20 microseconds. If you want four channels, your system needs to be able to output four samples in that timeframe. Note that the voltage needs to be steady for some time (depending, no doubt, on the capacitor) for the capacitor to charge.
The 4051, ’52, and ’53 CMOS switch ICs have a number of inputs and ouputs, as well as up to three address pins for selecting which output is connected to the input, and an enable pin to select whether the switch is open or closed. The crucial element, then, is outputting a set of control signals in time with the DAC output.
DACs don’t output a voltage instantaneously (stupid physics making everything difficult as usual), so between two consecutive samples there’s a phase where the output voltage is in some indeterminate, intermediate state. You use the enable pin on the switch to make sure its closed while the DAC settles to avoid glitches on your final output.
This basic technique works for any system (whether you have a microcontroller or not). You can obviously drive the DAC and switch with logic ICs if that’s what makes sense in your context. The only important thing is that you’re able to maintain the correct timing relations between the DAC output and the control signals sent to the switch; note that this might make working with serial input DACs difficult. I doubt this technique works at all with I2S (audio) DACs or codecs that expect a steady stereo stream at a fixed rate and have integrated digital filtering and such.
This technique was often used for both control and audio signals in early synthesizers and samplers, because DACs were so expensive — the Emu SP12, for instance, has only two DACs in the entire system, one for converting the audio signal itself and the other used as a digital volume control, and the eight audio channels were derived by a scheme like this one. The D50 uses a similar technique.
Here I’ll describe how I’ve done this with the STM32F4 microcontroller in particular. This references code that’ll be on Github shortly. I started out wanting to port the Sonic Potions LXR code to a board with six outputs, but when I got the output code working I decided to try getting some synth code of my own design I wrote last summer to run on the microcontroller. It’s basically a simple box of 4-op FM percussion sounds, and I’ve only looked at the output on a scope, not listened to it. But the principle stands, I can get six outputs out of two DACs.
The built-in DACs of the STM32F4 have a settling time of 3µs. I’ve tested with three output channels per DAC channel at approximately 50khz, and this wasn’t a problem, since this gives several microseconds for the capacitor to charge.
For my application, I wanted a steadily changing voltage (an audio stream to be output at 12 bits), so I set up a timer for running at the desired sample rate multiplied by the number of channels. I’m using TIM4, since this is able to trigger the DACs (not all timers are; consult the godawful documentation, namely the STM32F4 reference manual page 314). That is, whenever TIM4 is set to some value that divides the main clock down to the desired sampling rate. When it overflows and triggers the Update event, the DACs output a value. Also, in the interrupt service routine for this event (TIM4_IRQHandler), I set the address pins on the chip by writing the value of a simple software counter to a GPIO block (this is basically some stored number of ticks modulo the number of outputs for each DAC). I’m using DMA streams to fill the DAC, but I could easily pick the value out of a buffer and trigger the DACs from software in the interrupt service routine.
A second timer, TIM5, is also triggered by the main timer. This runs in one-shot mode, outputting a single, slightly delayed, pulse that on the pin hooked to the enable-pin on the switch IC. It is triggered by TIM4, the same that triggers the DAC.
To recap, every time TIM4 triggers an update event, a few things happen:
- The DACs start converting the value held in their output register
- The software counter that controls the pin adressing is updated and its value put to the adress pins
- TIM5 is started, first holding its output pin high, disabling the switch IC while the DAC settles, then pulling it low to enabling the switch to let the sample and hold capacitor charge.
Only the setting of adress pins needs to be explicitly handled in the ISR, the other two things happen simply by setting the correct triggering for the given peripherals.
I’ll add another post describing the DMA code that fills the buffers.