Now I’m dealing with the mirror case to the ADC that I just posted on. Instead of us reading an analogue voltage and converting that to a digital representation, we want an analogue voltage from a digital signal (DAC). In other words, I want the Arduino to give me a voltage from an int. But this is just like the AnalogWrite() function right?… Wrong!
- AnalogWrite() does not give us an analogue voltage. It gives us a pulse width modulated (PWM) digital signal ie. the output is either 0 or 5V and we change the ratio of on and off time to give us an analogue-like signal for driving motors for example. This is not truly analogue in that what we want is to be able to select any DC voltage between 0 and 5V that we want. We could apply a filter to the PWM signal to convert it to DC and this would be a cheap and fast trick but we would lose speed and voltage. Speed is a key figure of merit in a digital-to-analogue converter (DAC) systems as it determines the sampling rate.
- Resolution – if we were to go with option 1 plus filter, with the Arduino, we’re limited to a resolution of 8 bits ie. the output voltage would be scaled between 0-255*Vout. On the other hand with an external DAC, we could choose our resolution up to 32 bits ie. 0-4294967296!
So being able to use a DAC is going to be an important tool in our kit for attacking lots of projects. These things are commonplace in A/V equipment and mobile phones but I have another project in mind for this. More on that later. First, let’s discuss how to use a DAC – in this case the 12bit MCP4822. Se pinout here…
If you’ve not done so already, have a look back at my previous post on using an ADC which covers the basics of SPI connections, bit math and relevant code.
Same as before, we’re going to setup an SPI connection between the DAC and Arduino but this time we transfer data over the master-out-slave-in (MOSI) line. The process will look like this:
- Setup the SPI interface first given the settings in the chip datasheet.
- Take user input from the serial connection and store it as text.
- Separate the channel address, voltage and gain from the users command and store as char, int and int respectively.
- Convert our int representation of voltage to two binary bytes.
- Change the state of some of the leading bits to denote which channel of the DAC we’re addressing (A or B) and what gain we would like (1 or 2).
- Take the CS pin LOW and transfer two bytes via SPI (MSB first). Then return the CS pin to HIGH again.
- Print the result to the serial window.
- Repeat
Above you can see an extract from the datasheet that illustrates the SPI protocol. So how do we actually set the voltage that we want? Well, the MCP4822 has two channels, two gain settings and a 12-bit DAC register meaning and the voltage is calculated as follows…
Vout = Gain*Vref*(D/4095)
…where the gain is either 1 or 2, Vref=2.04V and is the internal voltage reference of the DAC, and D is the binary input that we send to the unit over SPI. Having an internal reference voltage means that even though the supply voltage may change slightly, the output is going to be stable since Vref is a stabilised internal reference…Nice! So this means we can choose whether we want to scale our voltage over 0.00-2.04V or 0.00-4.08V by the gain setting, either large/small range low/high resolution depending on our application, by using the gain setting.
Here is an extract from the MCP4822 datasheet for further information. It shows how the output is calculated and the writing process with example input. You can see that the before our binary input (left of the most significant bit) there are some extra bits referred to as config bits which in my code are inside the most significant byte (named MSB in the following code). We need to concentrate on bit 15, the channel selector, bit 13, the gain selector and bit 12, the shutdown bit…
Now let’s discuss how the code could look. The basic process is given in the numbered list above but let’s elaborate here (You’ll find a complete copy of the code at the bottom of the post):
Setup the SPI interface
// set the CS as an output: pinMode (DAC_CS, OUTPUT); Serial.begin(9600); // opens serial port, sets data rate to 9600 bps inputString.reserve(200); // reserve 200 bytes for the inputString: SPI.begin();
Take user input from the serial connection
// read the incoming string: inputString = Serial.readString(); // say what you got: Serial.print("I received: "); Serial.println(inputString);
Separate the channel address, voltage and gain from the user’s command and store as char, int and char respectively
//extract channel address channel = inputString.charAt(0); //and gain gain = inputString.charAt(1); //convert string to an int - binary voltage n = inputString.substring(2).toInt(); //set DAC state DAC_set(n, channel, gain, DAC_CS, errmsg); //print errors if there are any Serial.println(errmsg); //clear input string ready for the next command inputString = "";
Convert our int representation of voltage to two binary bytes
//convert decimal input to binary stored in two bytes MSB = (input >> 8) & 0xFF; //most sig byte LSB = input & 0xFF; //least sig byte
Set the config bits
You will see that setting config bits is done with bit manipulation. In essence, if you want to set bit 7 to a 1, you need to do an OR operation on your data with 10000000 (which is 0x80 in hex) – this is useful in setting the channel bit. If instead, you want to set the same bit to 0, you would do an AND operation with 01111111 (0x7F) so you would end up setting bit 7 to 0 and keeping any data that you already had. If you did, AND 00000000, all your bits would go to 0 of course.
//apply config bits to the front of MSB if (DAC_sel=='a' || DAC_sel=='A') MSB &= 0x7F; //writing a 0 to bit 7. else if (DAC_sel=='b' || DAC_sel=='B') MSB |= 0x80; //writing a 1 to bit 7. else errmsg += "DAC selection out of range. input A or B."; if (Gain_sel=='l' || Gain_sel=='L') MSB |= 0x20; else if (Gain_sel=='h' || Gain_sel=='H') MSB &= 0x1F; else errmsg += "Gain selection out of range. input H or L."; //get out of shutdown mode to active state MSB |= 0x10;
Take the CS pin LOW and transfer two bytes via SPI (MSB first). Then return the CS pin to HIGH again.
//now write to DAC // take the CS pin low to select the chip: digitalWrite(CS_pin,LOW); delay(10); // send in the address and value via SPI: SPI.transfer(MSB); SPI.transfer(LSB); delay(10); // take the CS pin high to de-select the chip: digitalWrite(CS_pin,HIGH);
Print the result to the serial window.
Serial.println("binary input to DAC: "); Serial.print(MSB,BIN); Serial.print(" "); Serial.println(LSB,BIN);
Repeat
In the serial monitor, you should see something like this…
I received: AL500 binary input to DAC: 110001 11110100 I received: AL0 binary input to DAC: 110000 0 I received: al1000 binary input to DAC: 110011 11101000 I received: al0 binary input to DAC: 110000 0 I received: AL1000 binary input to DAC: 110011 11101000
You get the command back that you wrote in then the binary input to the DAC and any errors. To check that the code and hardware were working, I measured the output from the DAC as a function of binary input. Here is what I got…
What follows is the final code that I used to check my DAC. It contains separate functions for setting the state of the DAC and reading user input from the Arduino Serial monitor. I hope this post is enough to give you the basics of DAC implementation using an Arduino. If you have any questions, please comment and I’ll do my best to answer. Note that this code is limited in terms of speed. I’ve included some short (10ms) delays in the code which would really slow the application of the DAC if we wanted to sample at high data rates. One to return to at a later date.
/* D. Mohamad 29/03/17 code to commumincate with MCP4822 12-bit DAC via SPI pin assignments as follows... Uno (master) MCP4822 (slave) CS 8 2 MOSI 11 4 SCK 13 3 Read output voltage with a multimeter Va/Vb on pin 8/6. send commands via serial interface eg. AL1000 would mean... channel=A gain=low D=1000 Va = gain*Vref*D/4095 = 1*2.04*1000/4095 = 0.498V see www.theonlineshed.com */ #include <SPI.h> const int DAC_CS = 8; //Chip select pin for the DAC String inputString = ""; //holds serial commands void setup() { // set the CS as an output: pinMode (DAC_CS, OUTPUT); Serial.begin(9600); // opens serial port, sets data rate to 9600 bps inputString.reserve(200); // reserve 200 bytes for the inputString: SPI.begin(); } //function to set state of DAC - input value between 0-4095 void DAC_set(unsigned int input, char DAC_sel, char Gain_sel, int CS_pin, String &errmsg) { //DAC_sel choose which DAC channel you want to write to A or B //Gain_sel choose your gain: H=2xVref and L=1xVref byte MSB,LSB;//most sig, least sig bytes and config info //clear error messages errmsg=""; Serial.flush(); // in case of garbage serial data, flush the buffer delay(10); //only run the rest of the code if binary is in range. if (input<0 || input >4095) errmsg += "input out of range. 0-4095."; else { //convert decimal input to binary stored in two bytes MSB = (input >> 8) & 0xFF; //most sig byte LSB = input & 0xFF; //least sig byte //apply config bits to the front of MSB if (DAC_sel=='a' || DAC_sel=='A') MSB &= 0x7F; //writing a 0 to bit 7. else if (DAC_sel=='b' || DAC_sel=='B') MSB |= 0x80; //writing a 1 to bit 7. else errmsg += "DAC selection out of range. input A or B."; if (Gain_sel=='l' || Gain_sel=='L') MSB |= 0x20; else if (Gain_sel=='h' || Gain_sel=='H') MSB &= 0x1F; else errmsg += "Gain selection out of range. input H or L."; //get out of shutdown mode to active state MSB |= 0x10; Serial.println("binary input to DAC: "); Serial.print(MSB,BIN); Serial.print(" "); Serial.println(LSB,BIN); //now write to DAC // take the CS pin low to select the chip: digitalWrite(CS_pin,LOW); delay(10); // send in the address and value via SPI: SPI.transfer(MSB); SPI.transfer(LSB); delay(10); // take the CS pin high to de-select the chip: digitalWrite(CS_pin,HIGH); } } //function to read user command from the serial command window and set the DAC output void serial_DAC_set(void) { unsigned int n; //number 2 bytes long unsigned char channel, gain; String errmsg; //errors returned from DAC_set // read the incoming string: inputString = Serial.readString(); // say what you got: Serial.print("I received: "); Serial.println(inputString); //extract channel address channel = inputString.charAt(0); //and gain gain = inputString.charAt(1); //convert string to an int - binary voltage n = inputString.substring(2).toInt(); //set DAC state DAC_set(n, channel, gain, DAC_CS, errmsg); //print errors if there are any Serial.println(errmsg); //clear input string ready for the next command inputString = ""; } void loop() { //only run when data is available if (Serial.available() > 0) serial_DAC_set(); }