Sep 21, 2013

Analog Output from a Microcontroller: DAC vs. PWM

To generate an analog or variable voltage from a microcontroller, the normal way is to use a digital to analog converter (DAC). It produces an output voltage proportional to its numerical input. They are fast and accurate and simple to program. A numerical value from your program is written to the address of the DAC and a voltage proportional to the number appears at the DAC output pin.
For example, a byte variable [0 - 255] written to an 8-bit DAC might produce a voltage ranging from 0.0 to 5.0 Volts. A value of 51 would therefore put ~1.0V at output.
 51/255 * 5.0 = 1.0
If the DAC had a 0 - 3.3V range, then output would be 51/255 * 3.3 = 0.66 V  
A DAC IC may be loaded parallel or serial, with 8-bit resolution or higher. 10 and 12 bits are very common and inexpensive. There are others which operate at 16 bits and higher.

Some microcontrollers have DACs built in (Silicon Labs parts are an excellent example), but the majority do not. (In particular, none of the Atmel processor families used on Arduinos include one). It is easy enough to add one or several if you need them. In general, for all but the simplest low speed cases, a bit of additional hardware gives 10 - 1000 times better performance, and with a lot less software.

A common work-around is to use a readily available digital output. By switching the output rapidly and controlling the ratio of high/low time, a PWM (pulse-width modulation) signal is created. If the output is smoothed or averaged, the resulting signal is a variable voltage. In theory, any resolution can be created; in practice, there are several limitations to performance including relatively slow speed, high noise generation, software overhead, quality of smoothing circuit (a low pass filter). (PWM speed and filter operation are beyond the scope of this post. [See PWM and Low Pass Filter.]


The Arduino's programming language (Wiring) uses this strategy with a command called analogWrite(). It can be misleading, and might better have been named pwm(). When driving an LED, it looks smooth, because the switching is faster than the human eye's response, so the eye/brain does the smoothing. When used on small motors, the mechanical inertia smoothes the movement. For these and other applications, it is often good enough. For some applications, it simply won't work as a substitute.

To illustrate the difference, I have the following example of DAC and PWM signals of the same signal viewed on an oscilloscope.


Here is a view of a sine wave calculated and generated from an arduino with the values output simultaneously to an external 12-bit DAC (MCP4725) and to a digital pin using analogWrite (8-bit PWM).

 The yellow and blue traces show the direct DAC output and the direct PWM output.



 In this example, a more complex wave is generated (noise on the traces is from missing scope ground, not the circuit). Notice how the width of the pulses changes, with the skinniest pulses when signal is low, and fattest when signal is high.

-----
Here is the code to generate these waveforms and a code link:

// generate a sine wave with the MCP4725 12-bit DAC. I2C interface (Adafruit MCP4725 breakout board)
// also make a PWM version of wave using analogWrite()
// view both on oscilloscope
#include 
#include 
#include 

Adafruit_MCP4725 dac;
#define DAC_RESOLUTION    (12)
// global variables
static char xbuf[20];    // for string formatting

// -----------------------------------------------------------------------
void setup(void) {
  Serial.begin(57600);
  dac.begin(0x62);    // initialize dac. For Adafruit MCP4725A1 the address is 0x62 (default) 
  Serial.println("\nGenerating a wave");
}

void loop(void) {
  unsigned int i;
  static float tstep = 0.005;    // small time increment for higher resolution wave
  static float t = 0;
  float v;
  int v_scaled;
  v = 2048*(1.0 + 0.5*sin(6.2832*1.0*t));  // plain sine wave, adjusted for 0 to 4095 range
  // v = 2048*(1.0 + .5*sin(6.2832*1.0*t)+0.2*sin(13*t+7)-.3*cos(23*t)); // more complex wave
  dac.setVoltage(v, 0);  // output to DAC

  analogWrite(3,map(v,0,4096,1,255));  //output as PWM to compare
  t = t + tstep;
  delay(10);      // adjust output rate for viewing
}