18 bit DAC: Precision Voltage Source
The Blog for this project

The Schematics, PCB files, and Simulation models are here
Youtube Video on this project (Coming soon)

Introduction

At Analogic, I did some work on the AN8200 precision 20bit DAC. This was a very accurate 6-digit voltage source with voltage ranges from 100mV to 1000V full scale. In the early 2000s it was the only product from the old Data Precision product line that was still shipping at Analogic. Why? Because it had amazing performance and a reasonable price. As a result, ATE test systems all over the world used it. In fact it was cloned by at least 2 other companies. I wanted to build a similar device that was simpler, much cheaper, and much smaller. Along came Linear Tech with the LTC275x series 18 bit DACs. Perfect.

It should have the following capabilities:

18b dac box

Design

I pretty much decided to use the LTC2756A single 18b DAC. I only plan to build a few of these, and will buy the best A grade grade parts. These have a lovely 0.5LSB INL (linearity) specs at 18 bits vs. the 2LSB B-grade parts. The cost is about $45 vs. $35 for B-grade. On the 10V range, one LSB is 40uV, 0.5LSB is 20uV, which is 2ppm of 10V. Very nice!

The Linear Tech basic example circuit for the LT2756 uses LT1468 precision opamps for the reference and the output current-to-voltage converter. Turns out I have a tube of
LT1469's, the dual version of LT1468.  I decided to use the duals; free is a great motivator for me. The compromises of using a dual opamp for precision work is that here are some minor disadvantages over singles.
But.... the increased baseline power and therefore the temperature is fairly constant and so doesn't contribute too much to drift.  Also I think there generally will be a low load on this circuit, like micro amps. If I load it to much ( > 1mA) then I expect a bit more drift.

For 10.00V, should I use the full 18bit range of 262,144 codes? If I use 250,000 codes instead, then a single count is a more convenient 40.00uV. If I use the full 18b, an LSB is an inconvenient 38.147uV. I came up with a simple fix. Instead of a 5V reference, use 5.0 * 262,144 / 250,000 = 5.234V. This has the added advantage of allowing slightly more than 10.00V: 10.486V, But how to increase the gain by a factor of 1.0486? Turns out increasing the reference by a small amount is not hard. Use a non-inverting, low-drift opamp with 0.1% resistors, with 25ppm tempco. To get  a x 1.0486 amplifier, use a 10.0K and a 487 ohm feedback resistor. Because the 10K and 487 ohm resistors contribute only about 5% to the gain, each 25ppm/C resistor only contributes about 5% of its 25ppm or 1.25ppm of drift, so pretty good. I use a low-cost, precision OP-07 for this stage. For even lower drift, 10ppm resistors are available for about $.50.

For a 5V reference, I chose the ADR431 which is 3ppm/C.

To calibrate the output for near-perfect 0.0000V and 10.0000V, it is necessary to have gain and offset calibration. Should I use a software cal or hardware trimpots? With software you can't get much closer than about 1/2 LSB of offset error. And the gain calibration affects the DAC LSB step size. And as long as the trimpots aren't contributing much change, they also don't contribute much signal change (or drift).  The LTC2756 DAC has convenient gain and offset adjust pins. Just feed in a low correction voltage to these pins.

Here is the schematic diagram for Rev 1. J2 is
the 5V power and SPI in connector, driven by LeoLed, my small front-panel Arduino board.

Sch1

Here is the box insides. The top board is the rear of LeoLed, the Arduino Leonardo with OLED display and controls. The bottom is the Rev0 18b DAC board.

guts

Control

I have a handful of my older LeoLed digital panel meter boards lying around. These have a 128 x 64 graphical OLED and a Leonardo style Arduino processor. They also have a 16b ADC and 12b DACs which I don't load for this project. They have a 10 pin expansion connector with +5V and a handful of IOs. Perfect to drive a precision SPI DAC. Originally intended as a smart panel meter and controller, I have used them for several projects including my electronic load and SMU. LeoLed features:
* Not used for 18b DAC project

For SPI data isolation. I like the Silicon Labs Si86xx series parts. For this design the 6 channel part with 5 signals in one direction and one return signal will do: Si8661.
2020 update: Al the SI86xx parts are pretty much unavailable. I have a few on hand, but have been specifying the
ADUM261 instead, which is a wide package, but otherwise pin-compatible with SI8661. It requires a footprint change to the PCB. For SMU I designed a mutated SOIC16 footprint that can take a wide or narrow SOIC16. Another difference, the ADUM261 uses magnetic isolation vs. capacitive isolation. That shouldn't matter.
2023 update:
The SI86xx parts are now available both in wide and narrow SOIC.
 
The power needs of this project are minimal. A few tens of milliamps of +/- 15V, and +5V. A small 1 watt, +5V to +/-15V DC-DC provides the +/-15V as well as the required isolation. A small +5V regulator makes the +5V. I have been bitten by the common-mode switching noise of DC-DC converters so to keep noise low, a common mode filter and a .01uF safety cap should help.

Here is a view of the LeoLED board.  It and the 18b DAC board mount nicely in a Hammond plastic box. 

leoled

Firmware

The firmware runs on an Arduino Leonardo-style (ATMEGA32U4) processor. The SPI graphics for the OLED uses the U8G2 library, which I like a lot. The hardware interface uses bit-bang SPI to drive the DAC. I do this mainly because the hardware SPI is busy doing the OLED graphics, and also because the SPI requirements for the DAC are pretty slow. For slow SPI devices, I generally just bit-bang SPI. Assert the SS/ pin low, set up a data bit (MOSI or SDI), and toggle the clock (SCK). Then left-shift (data <<1) out however many bits are needed in a FOR loop.  For a READ, shift in the read data. It only takes a few instructions. Depending on the slave, you need to control the clock polarity. Analog Devices parts often use a low-going clock. LTC and TI parts generally use a high, going clock.  Something to watch out for.

timing

Here is my bit-bang SPI routine that performs a 32 bit WRITE operation to the LTC2756.

/*  Low-level Bit-bang 32 bit SPI Write for LTC2756 DAC.
    Data is clocked in on + edge of CK\
    Assert SS/, clock out 32 bits, de-assert /SS
*/
void spi32W(long data)
{
  int i;
  digitalWrite(SPI_CK, 0);    // CK Should already be in this state
  digitalWrite(SPI_MOSI, 0);
  digitalWrite(SPI_SS, 1);

  digitalWrite(SPI_SS, 0);
  for (i = 0; i < 32; i++)
  {
    if (data & 0x80000000L) digitalWrite(SPI_MOSI, 1); // MOSI = 1
    else digitalWrite(SPI_MOSI, 0);                 // MOSI = 0
    digitalWrite(SPI_CK, 1);                        // Clock HI then LO
    digitalWrite(SPI_CK, 0);
    data = data << 1;                               // Shift up
  }
  digitalWrite(SPI_SS, 1);
}

The initial firmware uses an underline cursor to select the digit to be adjusted. Then the encoder is used to increase or decrease the value. The two buttons move the cursor right and left. Unfortunately there is no automatic cursor in U8G2 or most LCD graphics libraries. So I draw a short line under the appropriate digit. And skip over the decimal point. Crude but effective.

For more details on my firmware, see the
LeoLed digital panel meter page.

This project needs a proper USB control interface, so ATE tests can set the voltage and range. I plan to implement and test the Arduino SCPI library, and then port the code to the DIY-SMU. If there is interest, I'll order Rev2 boards to clean up a few rework cuts and jumps. Otherwise I'll just build a few. For me. 

Ranges and Calibration

My plan is to use hardware only calibration on the 10V range. This requires only multi-turn trimpots on the offset and gain trim pins. So if you want the highest accuracy, use 10V range. But hardware calibration takes, well, hardware, and is impractical to use on all the ranges. I'm not sure how important the other ranges are. The lower voltage ranges provide 1/2 the LSB voltage, so 20uV resolution. The higher voltage ranges provide bipolar voltages such as +/- 10V and +/- 5V. If you really need negative output, you could simply reverse the output terminals. Negative output may be more important in an ATE environment.

As far as calibration of the other ranges, EEPROM corrections for offset and gain would need to be implemented. These would provide very good cal, +/- 1/2 LSB.

Rev2 Plan

Since I am looking at adding SCPI software control, I also am looking at a Rev2 PC board. I originally built only one unit based on the Rev1 board. It did not have the x 1.0486 amplifier so used 2^18 codes full scale for 10.00V. But it worked very well, providing me with a very convenient, precision voltage to set the DIY-SMU project with.

I was having guilty second thoughts about my decision to use a dual op-amp. It should be the builders decision, right? So I made provisions for either a dual or two single op-amps. Here are the changes for the Rev2 board.

More Stable Reference Voltage: LM399

The Rev1 board had a few minor PCB errors that needed correcting. And there was a bit of output voltage gain drift, which I traced back to the 5V voltage reference. After I built this project, I played with LM399 temperature controlled references. Particularly, looking for ways to get better (1PPM) accuracy from these crude 7V +/- 3% devices. For a perfect 5V, you can use an LT5400 matched resistor network which has < .5ppm tempco match. They build a version with two 1K and two 5K resistors. The two 1K's can be wired in series to get 2K. So it's ideal for building a 2K/5K divider for converting 7.0V to 5.0V. But how to deal with the +/- 3% tolerance? Turns out since 18bDAC wants a 5.243V reference to achieve 40uV / LSB, adding a x1.0486 gain stage will do it, then just pick the value of R4 (shown as 486 ohms) as a function of the LM399 voltage. The gain will be nominally +4.86% (x1.0486), but can be easily adjusted from 1.000 to about 1.08  just by changing one resistor: R4. It means you need to measure your LM399(s) first, then buy the correct resistor(s).  This is my current working schematic, done in KiCad.  It has the latest changes including the heater current limit and the reference driver op-amp, but has not been built yet. I decided to use an LC filter for the +/- 15V supplies.

There are 2 options for the 2K/5K resistor network, LT5400, and my (free) 16 pin SOIC resistor networks.

sch
Here is the new board, so far. I have not worked the output grounding yet.
diptrace board

Since the X1.048 stage only contributes 3-7% of the output voltage, it also only contributes 3-7% of the tempco drift. So a readily available 25ppm 0.1% resistor only contributes about 5% of 25ppm or 1.25ppm. Not bad for a $0.25 resistor. Here is a plot from a spreadsheet showing the R4 value to correct for a range of the LM399 Voltages. The bumps in the curve are because I use actual resistor values. The spreadsheet is in the project files above.

resistor vs lm399 V

The LM399 heater draws significantly more power than the ADR435 reference. I measured about 1W, but the power draw peaks at about 2.5W during a cold-start of the LM399 heater. 2.5W is the maximum power to draw from a USB 5V connector: 5V at 0.5A. Would a USB C connector help? A solution is to add a current-limiting circuit to the LM399 heater. I can also use +/- 15V or 30V to power the heater, drawing equal and lower current from both +/- 15V supplies,

On my LM399 page I talk about some surplus (so free) precision resistor networks that I have. These have 12 resistors, two sets of 6. Each has 2x10K, 2x1K, 3340 and 3224ohms. It didn't occur to me at the time that I can put the two 10Ks in parallel to get 5K, and two 1Ks in series to get 2K. These are in a nice 16SOIC ceramic package, more convenient than the LT5400 in a SSOP-8 with heat tab. And free!

I have not yet ordered these PCBs. In fact I recently converted the design from Diptrace to KiCad. Let me know if you want the files. Copy this design at your own risk. Here is the Kicad Schematic and PCB.

SCPI Control

For SCPI control, I looked at 3 libraries I found: https://github.com/LachlanGunn/oic and https://github.com/Vrekrer/Vrekrer_scpi_parserand https://www.jaybee.cz/scpi-parser. Thanks to Vini from the OSMU project for the pointers. One of OIC's examples implements a simple Source-Measure unit using Arduino ADC and PWM pins: nice. I tested the code with various number formats for the voltage settings: Integer, fixed point, and scientific notation. Now I'm changing the example to implement ranging, current measure, etc. I tried all 3 libraries. Here is a quick review:

LachlanGunn/OIC
This one was easy to install and to run the examples. I like that the basic example uses Arduino analog I/O. I found it to be a bit touchy: It seems to hang if you send it bad SCPI commands. Interprets floating point numbers fine. To specify commands is slightly cumbersome, requiring you to specify both the long and short forms as well as the string lengths.

  source = scpi_register_command(ctx.command_tree, SCPI_CL_CHILD, "SOURCE", 6, "SOUR", 4, NULL);
  scpi_register_command(source, SCPI_CL_CHILD, "VOLTAGE",  7, "VOLT",   4, set_voltage);


Vrekrer SCPI Parser
Also easy to install this library and run the basic example. The basic example is a dimmer, and controls an LED brightness. Command register format is more convenient. Just call out the name once, and no need to specify string lengths. Interprets Floating point fine. So far so good, I'll stick with this one for now. BTW F("stuff") causes the string to live in Flash memory. I did not know that.

  my_instrument.SetCommandTreeBase(F("SOURce"));
    my_instrument.RegisterCommand(F(":VOLTage"), &SetVoltage);
    my_instrument.RegisterCommand(F(":VOLTage?"), &GetSourceVoltage);

Jaybee.cz/scpi-parser
This one is not a native Arduino library. It it quite large, about 10 .h files and another 8 .c files. I spent a day trying unsuccessfully to coax it to compile on a Teensy, and then gave up.  A real programmer could probably do it.
I began with the OIC library. I got it working on Teensy and on ATMEGA, and was happy with how quickly it came up. 18b DAC was crying for a proper way to control it via USB. And unlike DIY-SMU, the 18b DAC is a simple project: Set the DAC to a voltage, done! I moved the code to the LeoLed I use to control the 18b DAC. The code fits, but is a tight squeeze on the ATMEGA32U4 processor. Between the U8G2 library (with large fonts) and the SCPI Parser with floating point math, it's up to about 23K of the 28K available flash memory. For RAM, I use ~2K of the 2.5K available. As long as I avoid too much feature-creep, it should be OK. I'd like to implement SCPI on my E-Load project soon, as well as on the upcoming Switch (MUX) project. Both of these projects plan to use use LeoLed on a 'MEGA32U4. The Vrekrer and OIC libraries are about the same size.

If I run short of RAM or Flash, I could redesign LeoLed to use the newer Arduino NanoEvery processor, an upgrade for the standard 32K Arduinos: ATMEGA4809, 20MHz, 48K Flash, 6K RAM, handy little module, $12.  It does not have native USB like the ATMEGA32U4, but instead uses a small AVR processor as a USB to serial. I have played with this processor a bit, so far so good. Another CPU upgrade option is Teensy LC, a low-cost ARM processor: 48MHz, 32bit ARM M0, 62K Flash, 8K RAM, lots of I/O, $12. But as of 2023 Teensy LC is NRND.
I considered using the Raspberry Pi Pico, but I have had problems with it: I can't get a simple timed interrupt working! Also it's ADC isn't very good.

For the basic set of SCPI commands on 18b DAC, I use the following:
  /*
   *  *IDN?         -> identify
   *  *RST          -> Reset to known idle state: 10V range, 0.00V
   *  :SOURce
   *    :VOLTage    -> set_voltage
   *    :OUTput:    -> <OFF/ON> (does nothing for now)
   *    :VOLTage?   -> get_source_voltage
   *  :MEASure
   *    :VOLTage?   -> get_voltage from an 10b ADC channel
   */


The :MEASure:VOLTage? command reads the Arduino 10b ADC. It's just there to test some other SCPI commands. I am happy that both parsers deal with all kinds of number formats: 3, 3.14159, 3141.59e-3 all work fine.


The Blog for this project
  Dave's Home Page

Last Updated: 6/24/2023