Introduction: OpenDAC Seekat: 8-channel, 16-bit, Low-cost Laboratory DAC
The openDAC Seekat is an 8 channel, +/- 10 V, 16 bit digital-to-analog converter (DAC) based on the AD5764 chip from Analog Devices. This DAC was designed with the needs of a physics research laboratory in mind, but we hope a broader community will find this simple yet versatile instrument useful as well. Please visit openDACs.com for more information and future low-cost, easy to assemble, precision laboratory electronics. The website also includes performance characterization of the Seekat and an output noise comparison of several widely used laboratory DACs, including Seekat.
Step 1: Hardware Overview and Part List
Seekat is based on evaluation boards for the AD5764 chip from Analog Devices. Communication with the boards over SPI is done with an Arduino, controlled by an isolated USB connection to a PC. To simplify communication wiring and power distribution, we use a custom Arduino shield. A complete electrical schematic is shown above.
Here is the part list for Seekat. We assume you have hook-up wire and chip resistors on hand. If not, buy those too (see schematic for resistor values, size 1206).
Part | Supplier | Part No. (link) | Price (9/14/2014) | Qty. | Subtotal |
---|---|---|---|---|---|
AD5764 Eval board | Digikey | EVAL-AD5764EBZ-ND | $98.75 | 2 | $197.50 |
ADUM4160 USB isolator | Digikey | EVAL-ADUM4160EBZ-ND | $66.50 | 1 | $66.50 |
D-SUB cable (for power) | Digikey | AE9869-ND | $3.00 | 1 | $3.00 |
D-SUB/ terminal block bulkhead adaptor | Digikey | 277-2668-ND | $22.07 | 1 | $22.07 |
Indicator lights - bicolor | Digikey | 558-3001-007F | $2.86 | 4 | $11.44 |
3 position 0.1" terminal block | Digikey | A98334-ND | $1.99 | 3 | $5.97 |
2 position 0.1" terminal block | Digikey | A98333-ND | $1.55 | 3 | $4.65 |
Right angle header | Digikey | S1112EC-40-ND | $0.74 | 1 | $0.74 |
Straight header | Digikey | S1012EC-40-ND | $0.56 | 1 | $0.56 |
Enclosure | Par-metal | 14-19101B | $151.00 | 1 | $151.00 |
6" BNC/SMB bulkhead cable | Amazon | (link) | $6.00 | 8 | $48.00 |
Jumper wire | Amazon | (link) | $9.45 | 1 | $9.45 |
Mushroom fastener | Amazon | (link) | $16.44 | 1 | $16.44 |
Arduino UNO | Amazon | (link) | $23.11 | 1 | $23.11 |
6" USB-A to USB-B cable | Amazon | (link) | $4.67 | 2 | $9.34 |
USB-A to USB-B feedthrough | L-com | ECF504-UAB | $6.25 | 1 | $6.25 |
5/15/15 Triple output power supply | Acopian | 51515T6A | $315.00 | 1 | $315.00 |
Seekat Arduino Shield | OSHPark | (link) | $23.30 | 1/3 | $7.76 |
Total | $898.78 |
A significant fraction of the cost comes from the power supply, the enclosure, and the USB isolator. If cost is a more important concern that noise (and the differences may be minor) and appearance (the differences will not be minor), almost $500 can be saved by eliminating the USB isolator and finding lower cost alternatives for the enclosure and power supply, such as this very inexpensive switching power supply: Mean Well RT-65C.
Step 2: Prepare the AD5764 Boards and Machine the Enclosure
Solder a 14-pin header to the AD5764 eval board so the Arduino can communicate with the AD5764 via SPI. You may want to tape the top side to make it easier to solder the bottom side.
The AD5764 eval board has 14 jumpers that set various options on the board. Refer to the datasheet for a complete description. For the Seekat, set the jumpers like this (refer to the picture above):
LK1: B LK2: B LK3, 5A, 5B, 6, 12, 13, 14: leave in place LK4: B LK7: B LK8: A LK9: B LK10: remove LK11: A
Machine holes in the enclosure front panel for BNC feedthroughs and indicator LEDs, and in the back panel for the D-sub power and USB feedthroughs. Attach all the feedthroughs to the enclosure and attach the front and back panels. For machining simplicity, we used circular BNC feedthroughs. Use lock-washers for the BNCs and make the nuts nice and tight; alternatively, machine D-holes and get appropriate feedthroughs.
Step 3: Solder Components Onto the Seekat Arduino Shield
Now solder right-angle headers, screw terminals, and resistors to the OpenDAC Seekat Arduino shield (see above). You can ensure a good fit to the Arduino if you solder the headers with the shield in place, just make sure not to overheat the Arduino (be quick and minimize soldering iron heat). Resistor values are printed on the shield and can also be found in the schematic in Step 1.
Step 4: Connect PCBs and Install in Enclosure
Connect the Arduino, USB Isolator, and USB feedthrough using the two 6-inch USB A-B cables, and connect the SMB-BNC cables to the chassis and the AD5764 eval boards. Then attach the four PCBs (two AD5764 eval boards, Arduino, and USB isolation board) to the enclosure with mushroom-head fasteners (unisex Velcro). Refer to the picture in the next step for a suggested layout. If you use half as much Velcro as shown above, it may be easier to remove and reposition the PCBs.
Step 5: Make Power Connections
Make power connections as shown below. Twisted pairs for power likely have no benefit, but they keep things tidy. You will have an easier time with the screw terminals if you use solid conductor hook-up wire. The SMB cables to the AD5764 boards are shown connected below, but other wiring may be easier if you save those until the end.
Connect a 9-pin D-sub cable to your power supply according to the pinout shown above.
Don't blow up the eval board: -15 V goes to Vss, +15 V goes to Vdd.
Step 6: Wire the Power and Communication Indicator LEDs
Wire the power and communication indicator LEDs as shown above. If the LED doesn’t light up when you plug in the power cable, try reversing the leads.
Step 7: Add the Communication Wiring
Make all the connections for communication using jumper wires between the Seekat Arduino shield and the J21 pins on the EVAL-AD5764 boards. See above for a communication wiring schematic and a picture of the completed Seekat. Labels (LDAC, MISO, etc.) refer to those printed on the Seekat Arduino shield which differ in some cases from the labels on the AD5764 boards.
Put the lid on your enclosure and the hardware is done! Plug in the power and the +/- 15 V and +5 V indicator LEDs will turn on, but the communication LED will not turn on until the Seekat firmware is uploaded to the Arduino (next step).
Step 8: Software
Install the Arduino IDE on the computer that will be controlling the DAC and upload the Seekat Firmware to the Arduino.
Seekat Firmware
//Ardunio code for controlling Seekat DC voltage box from openDACS.com. Designed for Arduino UNO. #include “SPI.h” // necessary library int sync=10; // using digital pin 10 for SPI slave select int ldac=9; // Load dac (not implemented). You need to change some jumpers on the boards if you want to use synchronous updating and modify arduino code. int clr=8; // DAC Clear (not implemented). You need to change some jumpers on the AD58764 boards if you want to use this. void setup() { Serial.begin(9600); pinMode(7, OUTPUT); // we use this for SS pin pinMode(sync, OUTPUT); // we use this for SS pin pinMode(ldac, OUTPUT); // we use this for SS pin digitalWrite(7, HIGH); digitalWrite(sync, HIGH); SPI.begin(); // wake up the SPI bus. SPI.setBitOrder(MSBFIRST); //correct order for AD5764. SPI.setClockDivider(SPI_CLOCK_DIV32); SPI.setDataMode(SPI_MODE1); //1 and 3 communicate with DAC. 1 is the only one that works with no clock divider. } void setValue(byte DB[10]) { if (DB[0] == 255&&DB[1]==254&&DB[2]==253) // These bytes serve as a control that communication is working, and are reserved for future functionality such as synchronous updating, clear, and native arduino autoramp. { digitalWrite(sync, LOW); //assert sync-bar int o1 = SPI.transfer(DB[3]); // send command byte to DAC2 in the daisy chain. Serial.flush(); int o2 = SPI.transfer(DB[4]); // MS data bits, DAC2 Serial.flush(); int o3 = SPI.transfer(DB[5]);//LS 8 data bits, DAC2 Serial.flush(); int o4 = SPI.transfer(DB[6]);// send command byte to DAC1 in the daisy chain. Serial.flush(); int o5 = SPI.transfer(DB[7]);// MS data bits, DAC1 Serial.flush(); int o6 = SPI.transfer(DB[8]);//LS 8 data bits, DAC1 Serial.flush(); digitalWrite(sync, HIGH);//raise sync-bar to change the dac voltage. Must have LDAC-bar tied low. Serial.println(o1); Serial.println(o2); Serial.println(o3); Serial.println(o4); Serial.println(o5); Serial.println(o6); } else //This allows you to check on the scope what has been received by the Arduino for trouble shooting. Use pin 7 to trigger, then look at output of pins 13 (sclk) and 11 on the arduino to readout the bytes the arduino is getting. { digitalWrite(7, LOW); SPI.transfer(DB[0]); Serial.flush(); SPI.transfer(DB[1]); Serial.flush(); SPI.transfer(DB[2]); Serial.flush(); SPI.transfer(DB[3]); Serial.flush(); SPI.transfer(DB[4]); Serial.flush(); SPI.transfer(DB[5]); Serial.flush(); SPI.transfer(DB[6]); Serial.flush(); SPI.transfer(DB[7]); Serial.flush(); SPI.transfer(DB[8]); Serial.flush(); digitalWrite(7, HIGH); } } void loop() { if ( Serial.available()) // wait until all data bytes are avaialable { byte bytes[9]; for (int i=0; i<9; i++) { bytes[i] = Serial.read(); delay(2); //2mS pause to make sure bytes don’t run into each other. } setValue(bytes); } }
Communication with the Seekat can be done with any program capable of serial communication. We provide Matlab drivers to write and read voltages, and also to calibrate the offset and gain of each channel using a DMM.
Those functions are also included below:
Write voltage
%Find out which port the Arduino is using. One way is to use the Arduino IDE. % These are the commands you must execute in matlab to initialize for a virtual serial port COM5 on a PC. % global a; a = serial(‘COM5′,’baudrate’,115200);fopen(a) %Mac, for example: global a; a = serial(‘/dev/tty.usbmodem1431′,’BaudRate’,115200); fopen(a) function[] = setvoltageDC(channel,voltage) global a if voltage > 10 voltage = 10.0; elseif voltage < -10 voltage = -10.0; end switch channel case 1 n1 = 19; n2=0; m1=1; m2=0; case 2 n1 = 18; n2=0; m1=1; m2=0; case 3 n1 = 17; n2=0; m1=1; m2=0; case 4 n1 = 16; n2=0; m1=1; m2=0; case 5 n1 = 0; n2=19; m1=0; m2=1; case 6 n1 = 0; n2=18; m1=0; m2=1; case 7 n1 = 0; n2=17; m1=0; m2=1; case 8 n1 = 0; n2=16; m1=0; m2=1; otherwise disp(‘INVALID CHANNEL’) end if voltage >= 0 dec16 = round((2^15-1)*voltage/10); %Decimal equivalent of 16 bit data else dec16 = round(2^16 – abs(voltage)/10 * 2^15); %Decimal equivalent of 16 bit data end bin16 = de2bi(dec16,16,2,‘left-msb’); %16 bit binary d1=bi2de(fliplr(bin16(1:8))); %first 8 bits d2=bi2de(fliplr(bin16(9:16))); %second 8 bits %disp([255,254,253,n1,d1*m1,d2*m1,n2,d1*m2,d2*m2]); %uncomment to check what’s being sent to the Arduino pause(.005); fwrite(a,[255,254,253,n1,d1*m1,d2*m1,n2,d1*m2,d2*m2]); while a.BytesAvailable fscanf(a,‘%e’); end end
Read voltage
%Find out which port the Arduino is using. One way is to use the Arduino IDE. % These are the commands you must execute in matlab to initialize for a virtual serial port COM5 on a PC. % global a; a = serial(‘COM5′,’baudrate’,115200);fopen(a) %Mac, for example: global a; a = serial(‘/dev/tty.usbmodem1431′,’BaudRate’,115200); fopen(a) function v = getvoltageDC(channel) global a switch channel case 1 n1 = 19+128; n2=0; m1=1; m2=0; case 2 n1 = 18+128; n2=0; m1=1; m2=0; case 3 n1 = 17+128; n2=0; m1=1; m2=0; case 4 n1 = 16+128; n2=0; m1=1; m2=0; case 5 n1 = 0; n2=19+128; m1=0; m2=1; case 6 n1 = 0; n2=18+128; m1=0; m2=1; case 7 n1 = 0; n2=17+128; m1=0; m2=1; case 8 n1 = 0; n2=16+128; m1=0; m2=1; otherwise disp(‘INVALID CHANNEL’) end pause(.02); fwrite(a,[255,254,253,n1,0,0,n2,0,0]); pause(.02); fwrite(a,[255,254,253,n1,0,0,n2,0,0]); pause(.02); while a.BytesAvailable fscanf(a,‘%e’); % clear the buffer end fwrite(a,[255,254,253,0,0,0,0,0,0]); pause(.01); bdata=zeros([1,6]); for i=0:5; i=i+1; r=fscanf(a,‘%e’); bdata(i)=r; end bdata2=max(bdata(2)*2^8+bdata(3),bdata(5)*2^8+bdata(6)); if bdata2 < 2^15 %disp(10*bdata2/(2^15-1)) %bdata3=sprintf(‘%20f’,10.0*bdata2/(2^15-1)); bdata3=10.0*bdata2/(2^15-1); else %bdata3=sprintf(‘%20f’,-10.0*(2^16-bdata2)/2^15); bdata3=-10.0*(2^16-bdata2)/2^15; %disp(-10*(2^16-bdata2)/2^15) end v = bdata3; end
Calibrate gain and offset
%plug the channel to be calibrated into a DMM. You will need to have some function of your own to read the DMM voltage. function[] = calibrateSeekat(channel) global a switch channel case 1 n1 = 19; n2=0; m1=1; m2=0; case 2 n1 = 18; n2=0; m1=1; m2=0; case 3 n1 = 17; n2=0; m1=1; m2=0; case 4 n1 = 16; n2=0; m1=1; m2=0; case 5 n1 = 0; n2=19; m1=0; m2=1; case 6 n1 = 0; n2=18; m1=0; m2=1; case 7 n1 = 0; n2=17; m1=0; m2=1; case 8 n1 = 0; n2=16; m1=0; m2=1; otherwise disp(‘INVALID CHANNEL’) end fwrite(a,[255,254,253,n1+24,0,0,n2+24,0,0]); %zero the offset register fwrite(a,[255,254,253,n1+16,0,0,n2+16,0,0]); %zero the gain register measuredValues = zeros(2,1); voltage = 0; if voltage >= 0 dec16 = round((2^15-1)*voltage/10); %Decimal equivalent of 16 bit data else dec16 = round(2^16 – abs(voltage)/10 * 2^15); %Decimal equivalent of 16 bit data end bin16 = de2bi(dec16,16,2,‘left-msb’); %16 bit binary d1=bi2de(fliplr(bin16(1:8))); %first 8 bits d2=bi2de(fliplr(bin16(9:16))); %second 8 bits %disp([255,254,253,n1,d1*m1,d2*m1,n2,d1*m2,d2*m2]); pause(.005); fwrite(a,[255,254,253,n1,d1*m1,d2*m1,n2,d1*m2,d2*m2]); while a.BytesAvailable fscanf(a,‘%e’); end pause(2) blah = smget(‘V’); %your code here to read voltage offset = -blah{1}; offsetsteps = round(offset/(38.14e-6)); offset8 = de2bi(mod((offsetsteps),2^8),8,‘left-msb’); d1=0;%bi2de(fliplr(bin16(1:8))); %first 8 bits d2=bi2de(fliplr(offset8));%bi2de(fliplr(bin16(9:16))); %second 8 bits %disp([255,254,253,n1,d1*m1,d2*m1,n2,d1*m2,d2*m2]) pause(.005); fwrite(a,[255,254,253,n1+24,d1*m1,d2*m1,n2+24,d1*m2,d2*m2]); % +24 to access offset register while a.BytesAvailable fscanf(a,‘%e’); end pause(1) voltage = -10; if voltage >= 0 dec16 = round((2^15-1)*voltage/10); %Decimal equivalent of 16 bit data else dec16 = round(2^16 – abs(voltage)/10 * 2^15); %Decimal equivalent of 16 bit data end bin16 = de2bi(dec16,16,2,‘left-msb’); %16 bit binary d1=bi2de(fliplr(bin16(1:8))); %first 8 bits d2=bi2de(fliplr(bin16(9:16))); %second 8 bits %disp([255,254,253,n1,d1*m1,d2*m1,n2,d1*m2,d2*m2]); pause(.005); fwrite(a,[255,254,253,n1,d1*m1,d2*m1,n2,d1*m2,d2*m2]); while a.BytesAvailable fscanf(a,‘%e’); end pause(2) blah = smget(‘V’); %your code here to read voltage gainerror = blah{1}+10; gainsteps = round(gainerror/(152.59e-6)); gain8 = de2bi(mod((gainsteps),2^8),8,‘left-msb’); %bin16 = de2bi(gain16,16,2,’left-msb’); %16 bit binary d1=0;%bi2de(fliplr(gain16(1:8))) %first 8 bits d2=bi2de(fliplr(gain8));%bi2de(fliplr(gain8(1:8))) %second 8 bits pause(.005); fwrite(a,[255,254,253,n1+16,d1*m1,d2*m1,n2+16,d1*m2,d2*m2]); % +16 to access gain register instead of data register while a.BytesAvailable fscanf(a,‘%e’); end pause(1) voltage = 0; if voltage >= 0 dec16 = round((2^15-1)*voltage/10); %Decimal equivalent of 16 bit data else dec16 = round(2^16 – abs(voltage)/10 * 2^15); %Decimal equivalent of 16 bit data end bin16 = de2bi(dec16,16,2,‘left-msb’); %16 bit binary d1=bi2de(fliplr(bin16(1:8))); %first 8 bits d2=bi2de(fliplr(bin16(9:16))); %second 8 bits %disp([255,254,253,n1,d1*m1,d2*m1,n2,d1*m2,d2*m2]); pause(.005); fwrite(a,[255,254,253,n1,d1*m1,d2*m1,n2,d1*m2,d2*m2]); while a.BytesAvailable fscanf(a,‘%e’); end end