Go Down

Topic: Simple 10 bit DAC for the Arduino ATmega328 (1/2) (Read 28135 times) previous topic - next topic

avandalen

May 05, 2010, 12:35 pm Last Edit: May 05, 2010, 12:46 pm by avandalen Reason: 1
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
Code: [Select]

/* 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
Code: [Select]

#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! ::)

avandalen

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
Code: [Select]

#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
Code: [Select]

#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
Code: [Select]

#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

MarkT

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?
[ I will NOT respond to personal messages, I WILL delete them, use the forum please ]

avandalen

Quote
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.

avandalen


Go Up