Simple 10 bit DAC for the Arduino ATmega328 (1/2)

Intro
For the development of a solar cell curve tracer with the Arduino I needed a DAC. The Arduino processor, the ATmega328 / Atmega168, has ADC inputs but no DAC outputs. Although the internal ADC contains a 10 bit DAC, this DAC can't be used stand alone.
Therefore I developed an external 10 bit DAC, which is build with an integrator. For the schematic see Dac.cpp.

See for a picture here: http://www.esnips.com/doc/a42c5892-08f3-4892-8ad1-a12c5d400dd4/Scoop

Details
Resolution and accuracy are identically to the ADC (10 bit)
Output voltage 0 ? 10V
Uses 2 pins
Settling time 20ms max.

Hardware
The output voltage range is 5V * (R4 + R5) / R5 and can be changed by R4 and R5.
The TLC272CN is a high input impedance precision opamp.
Because the opamp output isn't rail to rail, the supply voltage is 12V. We can use a single 5V power supply when it is no problem that the maximum DAC output voltage is limited.

Installation
Place the files Dac.cpp and Dac.h in a library subfolder ?\hardware\libraries\Dac.

Library
Dac.cpp

/* Simple 10 bit DAC for the Arduino
Version 1.0
write(): Write a value to the DAC. 0 = 0V, 1023 = 10V.
read():  Read the actual output voltage.
refresh(): Because of leakage current, refresh the DAC periodically (10 sec. for 1 LSB error).
The settling time is max. 20ms.
Don't touch C1 / R1 during run.

               C1 100nF 10% MKT  
                _____||____
               |     ||    |  TLC272CN (VDD=12V, GND=0V)
           R1  |  |\       |
   I/O 2--56k-----|- \     |
                  |    \ __|____ DAC out 0 ... 10V
           R2     |    /  |     
     5V --10k-----|+ /    |
               |  |/  R4 10k
               |          |_____
           R3 10k         |     |
               |      R5 10k    |
               |          |     |
              GND        GND    |
    ADC 0-----------------------
         
            5V |      _       _
               |     | |     | |
  I/O 2        |     | |     | |
          2.5V |_____| |_____| |______
               |
               |
            0V |______________________
          
               |______
               |      \_______
 DAC out       |              \_______ 
 (not to scale)| 
               |______________________
*/

#include <WProgram.h>
#include "Dac.h"

Dac::Dac():
dacUpdownPin(2), UDacPin(0), overshoot(5)
{ write(512); // set to 2,5V 
  write(512); // the first conversion can be wrong 
}

bool Dac::write(int val)
{ targetVal = val;
  if(targetVal > 1023) targetVal = 1023;
  if(targetVal < 0) targetVal = 0;   
  
  if(abs(read() - targetVal) > overshoot) // avoid overshoot from setDac() for small value changes
    if(!setDac()) return false; 
  if(!fineTune()) return false;
  if(abs(read() - targetVal) > 1) return false; // final error check
  return true;
} 

bool Dac::refresh()
{ if(!fineTune()) return false;
  return true;
} 

int Dac::read() const// not inline
{ return analogRead(UDacPin);
}

inline int Dac::fastRead() const
{ return analogRead(UDacPin);
}
  
bool Dac::setDac()  
{ const byte timeout1 (255); // maxSettlingTime1 = 195
  int targetCorr; 

  if(read() == targetVal) return true; 
  if(read() < targetVal) 
  { targetCorr = targetVal - overshoot; // reduce overshoot caused by adc delay
    dacUp();
    for(settlingTime1=0; settlingTime1 < timeout1; settlingTime1++) 
    { if(fastRead() >= targetCorr) 
      { dacHold(); 
        break;
      }
    }
  } else  
  { targetCorr = targetVal + overshoot;
    dacDown();
    for(settlingTime1=0; settlingTime1 < timeout1; settlingTime1++)
    { if(fastRead() <= targetCorr)  
      { dacHold(); 
        break;
      }
    }
  }
  dacHold(); // end always with hold, in case of timeout
  if(settlingTime1 >= timeout1) return false;
  else return true;
}

bool Dac::fineTune() // produces no overshoot 
{ const byte timeout2 (80); // maxSettlingTime2 ~ 20
  const byte halfLsbCorrection (1);
  
  if(read() == targetVal) return true; // avoid ripple at refresh()
  if(read() < targetVal) 
  { for(settlingTime2=0; settlingTime2 < timeout2; settlingTime2++)
    { dacUp(); dacHold(); // finetuning with short pulse 
      if(fastRead() >= targetVal) 
      { for(int i=0; i<halfLsbCorrection; i++) dacUp(); // reduce error to 0
        break;
      }
    }     
  } else  
  { for(settlingTime2=0; settlingTime2 < timeout2; settlingTime2++)
    { dacDown(); dacHold(); // finetuning with short pulse 
      if(fastRead() <= targetVal) 
      { for(int i=0; i<halfLsbCorrection; i++) dacDown(); // reduce error to 0
        break;
      }
    }
  }
  dacHold(); // end always with hold, in case of timeout
  if(settlingTime2 >= timeout2) return false;
  else return true;
}

void Dac::dacUp() const
{ digitalWrite(dacUpdownPin, LOW);
  pinMode(dacUpdownPin, OUTPUT); 
}
void Dac::dacDown() const
{ digitalWrite(dacUpdownPin, HIGH);
  pinMode(dacUpdownPin, OUTPUT);
}

void Dac::dacHold() const
{ pinMode(dacUpdownPin, INPUT); // high impedance tristate 
  digitalWrite(dacUpdownPin, LOW); // disable pull up resistor 1*)
}

Dac.h

#ifndef DAC_H
#define DAC_H

// Version 1.0

class Dac
{ 
public:
  Dac();
  bool write(int val); 
  bool refresh();
  int read() const;
  
  int targetVal;
  byte settlingTime1, settlingTime2;
  
private:
  inline int fastRead() const; 
  inline bool setDac();
  inline bool fineTune();
  inline void dacUp() const; 
  inline void dacDown() const;
  inline void dacHold() const;
  
  const int overshoot;
  const int dacUpdownPin; // Digital
  const int UDacPin; // Analog in
};

#endif

Test software
DacDemo.pde is the test program which is used to test the application. As a good practise, the libraries Streaming.h and Flash.h should always be used. Download these libraries from Mikal Hart here: http://arduiniana.org.

To-do
To increase DAC speed, the overshoot from function setDac() is reduced by an overshoot value (5). However, this mechanism has a small influence on the accuracy. The DAC error is 0 or 1. When the overshoot value is changed to 0 the DAC error is mostly 0. The accuracy with an overshoot value (5) should be improved.

Although the DAC works fine it should be possible to further improve it in size and speed. Do you have any questions and improvement ideas, please put a reply! ::slight_smile:

Part 2

DAC test software
DacDemo.pde is the test program which is used to test the application. As a good practise, the libraries Streaming.h and Flash.h should always be used. Download these libraries from Mikal Hart here: http://arduiniana.org.

DacDemo.pde

#include <WProgram.h>
#include <Streaming.h> // use this library always!
#include <Flash.h> // use this library always!
#include <Dac.h>
#include "TestDac.h"

Dac dac;
TestDac testDac; 

void setup() 
{ Serial.begin(9600);
  randomSeed(0);
  Dac(); // run constructor here
}

void loop()
{ testDac.testAll();
}

TestDac.pde

#include <assert.h>

void TestDac::testAll()
{ //Select here what you want to test:
  //triangleWave(0, 1000, 100); 
  //triangleWave(500, 505, 1); 
  //triangleWave(-5, 10, 1); 
  //triangleWave(0, 1023, 1023); // measure settlingTime1
  //triangleWave(0, 30, 5);
  //triangleWave(0, 100, 15); // measure overshoot 
  //triangleWave(0, 1000, 200, 200); // test leakagecurrent 
  rand(); // measure settlingTime2 
  //minMax();
  //refresh(); // test if the output is ripple free
}

void TestDac::test(int val, int _delay, bool refresh)
{ int actualVal, error, absError;
  if(!refresh) 
  { if(!dac.write(val)) Serial << F("DAC fault "); // Serial and F, see Streaming.h and Flash.h
  }
  else if(!dac.refresh())  Serial << F("DAC refresh fault "); 
  delay(_delay);  
  actualVal = dac.read();
  error = actualVal - dac.targetVal;
  absError = abs(error);
  if(absError > maxError) maxError = absError; 
  if(dac.settlingTime2 > maxSettlingTime2) maxSettlingTime2 = dac.settlingTime2; 
  
  //Select here what you want to log:
  //Serial << val << " ";
  //Serial << actualVal << " ";  
  //Serial << (int) dac.settlingTime1 << " "; 
  //Serial << (int) dac.settlingTime2 << " ";
  //Serial << error << " "; 
  //Serial << "/ ";
}

void TestDac::triangleWave(int begin, int end, int step, int _delay)   
{ maxError = 0, maxSettlingTime2 = 0;
  Serial << F("\nTest triangle wave\n");
  scopeTrigger();
  int i = begin;
  while(i >= begin)
  { test(i, _delay);
    i += step;
    if(i >= end) step = step * -1;    
  }
  printMaxError();
  Serial << F(" Max time2: ") << maxSettlingTime2;
}

void TestDac::rand()
{ maxError = 0, maxSettlingTime2 = 0;
  Serial << F("\nTest random\n");
  scopeTrigger();
  for(int i=0; i<25; i++) test(random(1024)); 
  printMaxError();   
  Serial << F(" Max time2: ") << maxSettlingTime2;
}

void TestDac::minMax()   
{ maxError = 0, maxSettlingTime2 = 0;
  Serial << F("\nTest min max\n");
  dac.write(-1);
  assert(dac.targetVal == 0);
  dac.write(0);
  assert(dac.targetVal == 0);
  dac.write(1023);
  assert(dac.targetVal == 1023);
  dac.write(1024);
  assert(dac.targetVal == 1023);
  Serial << F("test OK");
}

void TestDac::refresh() 
{ maxError = 0, maxSettlingTime2 = 0;
  Serial << F("\ntest refresh\n");
  test(50); // dac.write()  
  while(1) 
  { scopeTrigger();
    test(9999,100,1); // dac.refresh()
  }
}
 
void TestDac::printMaxError()
{ Serial << F("Max err: ") << maxError;
}
  
void TestDac::scopeTrigger()
{ const int triggerPin = 4;
  pinMode(triggerPin, OUTPUT);
  digitalWrite(triggerPin, HIGH); 
  digitalWrite(triggerPin, LOW); 
}

TestDac.h

#ifndef TESTDAC_H
#define TESTDAC_H

class TestDac
{
public:
  void testAll();

private:  
  void triangleWave(int begin, int end, int step, int _delay=0);
  void rand();
  void minMax(); 
  void refresh();
  void test(int val, int _delay=0, bool refresh=0);
  void printMaxError();
  void scopeTrigger();
  
  int maxError, maxSettlingTime2;
};

#endif

By coincidence I am just about to order some DACs for a curve-tracer, I'm thinking of using a DAC chip (not as cheap as an opamp though...).

Did you also consider trying a good multi-pole low-pass filter on a PWM signal? Or a resistor ladder?

By coincidence I am just about to order some DACs for a curve-tracer, I'm thinking of using a DAC chip (not as cheap as an opamp though...).

Did you also consider trying a good multi-pole low-pass filter on a PWM signal? Or a resistor ladder?

For a data logging and curve tracers this DAC is excellent. I didn't tried other DAC's because my DAC is accurate and very simple. A resistor ladder uses to many pins and is not accurate.

Now you can also find this article on my website here: