
Programmable Voltage Reference – Part 1
First part of a project series for a “precision” programmable voltage reference based on the DAC1220 20-bit DAC. The goal is a voltage range of +/- 10V and a low temperature coefficient at max. 1ppm/°C for the whole circuit. This would result in a voltage change of 100µV for 5°C of temperature deviation.
Possible usage is the calibration of up to 4-digit multimeters and providing accurate and stable ref-voltages to ADCs and DACs.
The voltage reference IC for the DAC1220 is a MAX6325 2.5V with a tempco of 1ppm/°C, which I soldered on a breakout board for easy prototyping and later usage. The same applies to the DAC.
Prototype soldered on perfboard.
The prototype involves only the reference IC, the DAC and the essential voltage regulators. The circuit is powered of a 5V-to-12V step-up converter (which is isolating but this feature is ignored for now) and subsequent linear regulation to 5V and 9V for the DAC and the MAX6325. This also filters most of the noise generated by the switching converter and coupled-in noise by USB.
The DAC1220 is controlled via SPI and set to it’s maximum resolution of 20 bits. Although the output drivers are rail-to-rail types, the actual output voltage can’t go lower than about 1-2mV, but that’s not a big deal. Such low voltages are barely needed. The upper end doesn’t reach exact 5V too.
The theoretical resolution is 5V / 220 -> 4.768µV/bit. At a later time I’ve planned to add a positive amplifying op-amp stage, that will scale it to 10µV/bit in order to achieve a voltage span of approx. 0V to about 10.486V (minus the upper offset of course!).
Circuit
The circuit is pretty simple. In the left corner there is the input connector with the 5V from the Arduino or any other microcontroller board. The digital logic lines are pulled up by three 10k resistors, the additional 2k resistors are optional and provide a crude protection to the DAC in case of overvoltage.
SIM1-0512s is a DIP-8 package DC-DC converter, which boosts the incoming 5V to 12V-14V. The following linear regulators (U1 and U2) provide the necessary voltages for the DAC (5V for analog and digital section) and the Reference IC (9V). U4 could be powered directly by U3, but I wanted to keep the noise as low as possible.
Although there is a model provided for the DAC, I used 1×8 pin headers to mount it via a breakout board.
Download above schematic (pdf):
Code
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 |
#define PIN_CS 7 //enable #define PIN_CLK 6 //spi clock #define PIN_DAT 5 //spi data uint32_t Millis = 0; uint8_t Buf[10]; uint8_t Len = 0; float cal = 1.00015; //linear distortion float offset = 0.0004; //offset (due to output drivers of DAC1220) -> depends on YOUR circuit! (ground level) float voltageLimit = 4.99; //upper voltage limit (due to output drivers of DAC1220) void setup() { Serial.begin(9600); pinMode(PIN_CS, OUTPUT); pinMode(PIN_CLK, OUTPUT); pinMode(PIN_DAT, OUTPUT); pinMode(13, OUTPUT); digitalWrite(PIN_CS, HIGH); //reset(); } void loop() { float v; if (millis() - Millis > 10) { if (Len) { if (parseFloat(Buf, Len, &v)) { Serial.print("set voltage: "); Serial.print(v, 4); Serial.print(" set bits: "); uint32_t bitCode1 = (uint32_t)(v / 5 * 0xFFFFF + 0.5); Serial.println(bitCode1); //reset(); sendData(v); } } Len = 0; } while (Serial.available()) { if (Len < 10) { Buf[Len++] = Serial.read(); if (Buf[0] == 'r') reset(); } else { Serial.read(); } Millis = millis(); } } void sendData (float setVoltage) { if (setVoltage < offset) { setVoltage = offset; } else if (setVoltage > voltageLimit) { setVoltage = voltageLimit; } uint32_t bitCode = voltageToBits(setVoltage * cal - offset); if (bitCode > 0xFFFFF) { bitCode = 0xFFFFF; Serial.println("input value clipped."); } Serial.print("Voltage: "); Serial.print(setVoltage, 4); Serial.print(" Bits: "); Serial.println(bitCode); Serial.println("-----------------------"); Serial.println(""); bitCode = bitCode << 4; digitalWrite(13, HIGH); digitalWrite(PIN_CS, LOW); shiftOut(PIN_DAT, PIN_CLK, MSBFIRST, 0x40); shiftOut(PIN_DAT, PIN_CLK, MSBFIRST, (bitCode & 0x00FF0000) >> 16); shiftOut(PIN_DAT, PIN_CLK, MSBFIRST, (bitCode & 0x0000FF00) >> 8); shiftOut(PIN_DAT, PIN_CLK, MSBFIRST, (bitCode & 0x000000FF)); digitalWrite(PIN_CS, HIGH); digitalWrite(13, LOW); } uint32_t voltageToBits(float voltage) { return (voltage / 5.0) * pow(2, 20); } void reset () { digitalWrite(13, HIGH); digitalWrite(PIN_CS, LOW); //Reset DAC pinMode(PIN_CLK, OUTPUT); digitalWrite(PIN_CLK, LOW); delay(1); digitalWrite(PIN_CLK, HIGH); delayMicroseconds(240); //First high period (600 clocks) digitalWrite(PIN_CLK, LOW); delayMicroseconds(5); digitalWrite(PIN_CLK, HIGH); delayMicroseconds(480); //Second high period (1200 clocks) digitalWrite(PIN_CLK, LOW); delayMicroseconds(5); digitalWrite(PIN_CLK, HIGH); delayMicroseconds(960); //Second high period (2400 clocks) digitalWrite(PIN_CLK, LOW); delay(1); //Start Self-Calibration shiftOut(PIN_DAT, PIN_CLK, MSBFIRST, 0x05); //20-bit resolution shiftOut(PIN_DAT, PIN_CLK, MSBFIRST, 0xA1); //20-bit resolution digitalWrite(PIN_CS, HIGH); delay(600); digitalWrite(13, LOW); Serial.println("DAC reset sucessful"); sendData(2.5); } void shiftOut(uint8_t dataPin, uint8_t clockPin, uint8_t bitOrder, int val, uint8_t bits = 8, uint8_t del = 10) { uint8_t i; for (i = 0; i < bits; i++) { if (bitOrder == LSBFIRST) digitalWrite(dataPin, !!(val & (1 << i))); else digitalWrite(dataPin, !!(val & (1 << ((bits - 1 - i))))); digitalWrite(clockPin, HIGH); delayMicroseconds(del); digitalWrite(clockPin, LOW); } } uint8_t parseFloat(uint8_t *pbuf, uint8_t len, float *f) { uint8_t i; uint8_t c; uint8_t period = false; uint8_t n = 0; uint32_t m = 0; uint32_t divider = 1; for (i = 0; i < len; i++) { c = pbuf[i]; if (c == '.') { if (period == false) { period = true; n = len - i - 1; } else { return false; } } else if (pbuf[i] >= 0x30 && pbuf[i] <= 0x39) { m = m * 10 + (c - 0x30); } else { return false; } } for (i = 0; i < n; i++) { divider *= 10; } *f = (float)m / divider; return true; } |
The Arduino code uses software-based SPI, because the DAC1220 runs only with a 2.5MHz crystal clock and it’s maximum serial clock rate is less than 1/10 of the master clock.
So the code above is versatile and not limited to the arduino platform, just adapt the pinouts and you’re ready to go. Some parts are extracted from GarageProto’s GitHub code, especially the serial input to float conversion. Thanks for that!
https://github.com/GarageProto/DAC1220
Testing
The current consumption is approx. 50mA@5V, but the majority flows through the switching and the linear converters. The DAC and the ref-IC draw only about 2mA.
Following some test voltages with relatively good software trimming.
The first 3 voltages are set to 1V, 2V and 3V. The last one to the maximum limited by the software (4.990V). The Multimeter has an offset of -0.0001 in the 10V range, which can be added. The values would then be only -100µV out. The last reading shows a bit non-linear distortion, which is expected at the upper end of the DAC (datasheet page 7) and could be corrected in software later.