Sunday, November 17, 2013

Arduino and DAC

My quest to implement a version of the analogWrite() function on the Arduino that actually behaves as the name advertises continues.

In my previous post, I built a 4-stage Sallen-Key low pass filter to convert the PWM output to a flat signal, but it had shortcomings:
  • Lots of components to get a flat signal
  • Only 8 bit of resolution means only 256 voltage levels (although this could probably be improved using PWM tricks).
  • Unbuffered output meant I'd have to add a couple of op-amps behind it to get it to behave like a stable voltage source.
Today, I am trying to do the same thing with a real digital to analogue converter (a DAC) : the Analog Device DAC8412FPZ.

I just got my hands on one of these chip, and this is what it looks like:


Here's the data sheet for this little guy.

Advantage of this chip:
  • 12 bit output resolution gives you 4096 possible output voltage levels.
  • 28-pin DIP packaging makes for easy breadboarding and experimenting.
  • 4 channels (effectively, the chip contains 4 quasi independent DACs) so you can send 4 independent analogue signals to 4 separate devices.
  • Works with both unipolar or bipolar power supplies in [5V, 30V] range means you can power it from an Arduino +5V supply for experiments but still move the final project to a proper bipolar +/- 12V supply later on.
  • Outputs are buffered and can source or sink +/- 5 mA at 10V, which means you don't have to add a power amplification stage after the DAC if all you want is to drive small loads.
  • Data transfers to the chip is done with a parallel bus, which is annoying to wire up, but super simple to manipulate in software.
Disadvantages of this chip:
  • Chip is kinda bulky (4cm x 1.5cm)
  • Unless you manage to get your hands on a sample, chip is expensive (~ $35)
  • The parallel bus is a bit of a pain to wire up (12 wires) and you need a lot of pins on the micro controller unit side. 
This last part is actually the most annoying: you are going to need an Arduino Mega if you want enough digital ports to control this thing directly.

Here's  what it looks like all wired up on the breadboard:



A couple of things to note:
  • The big bundle of green wires on pin 30 to 41 of the Arduino (12 of them) is the data bus and it's the reason why you need a Mega to makes this work :(.
  • The two blue wire are the "address bus", that you use to select which of the 4 DACs you're fiddling with.
  • In case you're wondering what they're for, some patch cables are only in there to mechanically anchor the Mega to the breadboard.
  • The Arduino's digital pins 7, 6, 5, 4, 3, 2 are used to manipulate the control pins of the DAC.
  • The Arduino, via its A0 analog pin, is also used to read back the voltage output of the DAC to check that it all works (we only get a resolution of 1024 levels since the embedded ADC of the Arduino only does 10bits).
  • In the pictures, the DAC is powered by an external +/- 12V power supply. For this, I use this excellent little breakout board from the good folks at dangerous prototypes which neatly converts the output of an old PC ATX power supply I had lying around.
  • In this example, the VRefHigh and VRefLow pins of the DAC are hooked respectively to the +5V and GND of the Arduino, which means the DAC will output 4096 levels in the [0, 5V] interval. This makes it easy to read back the DAC's output voltage directly with the Arduino. However, you could hook these two pins to any other external reference (as long as VRefLow is less than VRefHigh and they're smaller than the power voltages), including making VRefLow negative to get the DAC to crank out negative voltages.
On the software side, here is a simple Arduino sketch that will:
  • Setup the Arduino pins to control the DAC
  • Reset the DAC
  • Control the DAC to repeatedly output all possible values in [VRefLow, VRefHigh] on all 4 channels at once
  • Use pin A0 to read back one of the DAC's voltage output
  • Dump the value read on the serial port.

Here is the code:

 // DAC8412FPZ  
   
 #define ADDR_BIT0 7 // Control pin got LSB of address bus  
 #define ADDR_BIT1 6 // Control pin got MSB of address bus  
 #define DATA_BIT0 30 // Control pin for LSB of data bus  
   
 #define RW 5 // Pin for read-write command : High -> read, Low -> write  
 #define CSB 4 // Pin for chip select : High -> Off, Low -> On. Chip ignores commands unless this is low  
 #define RSTB 3 // Pin to Reset all channels to midscale : High -> do nothing, Low -> do reset  
 #define LDACB 2 // Ping to control "Load DAC"  
   
 #define analogPin A0  
 static uint32_t count = 0;  
   
 // Prep the address bus to talk to one DAC  
 static inline void setAddrBus(  
   int dacId  
 )  
 {  
   digitalWrite(ADDR_BIT0, (dacId & 0x1) ? HIGH : LOW);  
   digitalWrite(ADDR_BIT1, (dacId >> 0x1) ? HIGH : LOW);  
 }  
   
 // Prep the data bus with a given value  
 static inline void setDataBus(  
   int16_t value  
 )  
 {  
   for(int i=0; i<12; ++i) {  
     digitalWrite(  
       i + DATA_BIT0,  
       (value & (1<<i)) ? HIGH : LOW  
     );  
   }  
 }  
   
 // Manipulate the "chip select" signal  
 static inline void ioState(  
   bool on  
 )  
 {  
   digitalWrite(CSB, on ? LOW : HIGH);  
 }  
   
 // Load a value on all 4 DACs  
 static inline void loadValue(  
   int16_t value  
 )  
 {  
   for(int i=0; i<4; ++i) {  
     ioState(false); delay(10);  
       setAddrBus(i); delay(10);  
       setDataBus(value); delay(10);  
     ioState(true); delay(10);  
     ioState(false); delay(10);  
   }  
 }  
   
 // Read a value from A0, output on serial  
 static inline void output()  
 {  
   uint16_t value = analogRead(analogPin);  
   Serial.print("count:");  
   Serial.print(count);  
   Serial.print(" millis:");  
   Serial.print(millis());  
   Serial.print(" value:");  
   Serial.println(value);  
 }  
   
 void setup()  
 {  
   // Prep control pins  
   pinMode( RW, OUTPUT);  
   pinMode( CSB, OUTPUT);  
   pinMode( RSTB, OUTPUT);  
   pinMode( LDACB, OUTPUT);  
   pinMode(ADDR_BIT0, OUTPUT);  
   pinMode(ADDR_BIT1, OUTPUT);  
   for(int i=0; i<12; ++i) {  
     pinMode(i + DATA_BIT0, OUTPUT);  
   }  
   
   // Reset DAC  
   digitalWrite(RSTB, LOW); delay(10); // Turn reset on  
   digitalWrite(RSTB, HIGH); delay(10); // Turn reset off  
   
   // Prep serial for output  
   Serial.begin(230400);  
 }  
   
 void loop()  
 {  
   loadValue(count*10);  
   output();  
   ++count;  
 }  
Here is the code on github.
And finally, here's the oscilloscope output:
Looks neat :)

Saturday, November 9, 2013

Active low-pass filtering of Arduino's pwm output for a clean, flat, software adjustable signal.

The rather ill-named Arduino's analogWrite() function absolutely does not do what its name suggests: you'd expect it to set the voltage one of the output pins to the given value.

To do this you'd typically need something called a "digital to analog converter", or DAC in short, something the Arduino unfortunately does not come equipped with.

Instead, analogWrite() generates a PWM signal,  i.e. a square wave with "ON" and "OFF" timings designed such that the *average* voltage value is what you requested (the so-called duty cycle of the signal).

This is fine to - say - drive a motor, but certainly doesn't help if you want a clean, flat, noiseless signal.

In fact, the whole premise of PWM is the assumption that the loads it's ever going to drive have a built-in filter. In the case of a motor, the slow response times of the physical device is the filter in question.

But for some applications, PWM just does not cut it: sometimes, you'd like your Arduino to produce a clean, flat, noiseless constant voltage signal that can be controlled in software.

The standard RC low pass filter (such as the one described in this article) kinda works for this and is super easy to build, but it generates a fairly noisy output signal.

For example, below is the standard (490Hz / 50% duty cycle) PWM output of the Arduino fed through such a RC low pass filter:




Bottom line, the output is better than the PWM wave, but still awful choppy.

A larger cap, or maybe a multi-stage RC low-pass filter would of course help smooth out the noise further, but:
  • you might not have larger caps handy
  • you'll still get fairly fat ripple
Here's for example a 4 stage RC low-pass filter.
The output still has ripples:


Looking at the components I have lying around, I tried to put something together to produce a flatter, software adjustable voltage level from my Arduino.

Turns out I had a LM324N quad op-amp, a bunch of caps and resistors, generally enough to put together a four stage active low pass filter, with each stage based on the Sallen-Key Low Pass topology.

I didn't really spend much time doing theoretical analysis of the circuit to figure our caps values, but rather played with what I had lying around until I found a combo that worked well with the signal produced but the Arduino.

Here's what the circuit looks like:



Note: LTSpice does not come standard with models for the LM324, and if you want to play with the circuit above, you'll need to add two files to your LTSpice lib directory (mine happens to be in ~/.wine/drive_c/Program Files (x86)/LTC/LTspiceIV/lib/sym/Opamps , but YMMV).

Put this SYM file in lib/sym/OpAmps
Put this MOD file in lib/sub

This last circuit has a number of advantages:
  • The output is a lot cleaner than the RC filter, and cleanly maps the [0 - 255] range of the analogWrite() function to the full [0-5V] range.
  • Because it is a 12V powered, active circuit, it can easily be hacked  to produce 255 clean levels in [0- X volts] where X is the max output voltage of your OpAmps. All that's needed is a gain bridge on the feedback loop of the 4th stage, and if you are using an OpAmp that can goes rail to rail (which the 324N isn't), you can sweep the full 12V range.
  • I've tried to measure the theoretical output noise in LTSpice, and looks like it is in the 10µV range ... well below the noise level generated by my 12V power supply.
Here's what the thing looks like on a breadboard:



Note that:
  • the quad op-amp is the IC in the center
  • each of the stages is laid out vertically around the op-amp
  • I've added a 330Ω + LED load at the output.
Here's an oscilloscope screenshot of the signal output:



Good enough for me.