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