…now for the digital-to-analogue converter

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!

  1. 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.
  2. 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!
The circuit schematic that I used to test out the MCP4822 DAC. Note that I used a digital voltmeter connected to pin 8 (Va output) to monitor the output and I sent commands over SPI using an Arduino UNO.

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…

Pinout diagram for the MCP4822 DAC

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:

  1. Setup the SPI interface first given the settings in the chip datasheet.
  2. Take user input from the serial connection and store it as text.
  3. Separate the channel address, voltage and gain from the users command and store as char, int and int respectively.
  4. Convert our int representation of voltage to two binary bytes.
  5. 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).
  6. Take the CS pin LOW and transfer two bytes via SPI (MSB first). Then return the CS pin to HIGH again.
  7. Print the result to the serial window.
  8. Repeat
SPI communication protocol for the MCP4822 DAC.

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…

 

Extract from the MCP4822 datasheet showing the write command register. I’ve also given you an example of the kind of thing you would send to the unit and what that actually means.

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();
}

One Reply to “…now for the digital-to-analogue converter”

Leave a Reply

Your email address will not be published. Required fields are marked *