17. Libraries, DAC, and I2C
We already mentioned that the Arduino Uno board has built-in analog-to-digital conversion (ADC). A voltage between zero and five volts is converted to an integer between zero and 1023. But it does not have digital-to-analog conversion (DAC). DAC is useful in many applications for biodevices. As an example consider measurement of a cyclic voltammogram. In these assays, you need to supply a varying voltage and then measure a current. The voltage supply cannot be delivered with pulse-width modulation; it must be analog. For this purpose, DAC is necessary.
To deliver an analog voltage, you need a DAC that is separate from the Arduino Uno. We have an MCP4725 DAC breakout board from Adafruit. The breakout board communicates with Arduino using I2C communication. In order to use the communication, you need to use the Wire library. Libraries contain extra bits of code that can be very useful for specific tasks. Many libraries, including Wire, are included in the Arduino IDE.
This lesson covers three topics that are not necessarily related: DAC, I2C, and libraries. However, to properly execute DAC, you need to learn about latter two. You will learn about these three topics by working through a follow-along exercise.
Follow-along exercise 12: DAC with MCP4725
To demonstrate digital-to-analog conversion, we will vary the brightness of an LED without using pulse width modulation. Instead, we will deliver an LED an analog voltage that varies sinusoidally. We will use the MCP4725 DAC. This is a 12-bit, single-channel DAC. This means that it takes a 12-bit integer and converts it to a voltage referenced against the input and ground voltage (in this case, zero to five volts). So, the range of integer inputs is zero to 4095. It is single-channel, meaning that it can output only one analog voltage.
Wiring up the DAC
Wire up the DAC/LED according to the schematic below.
A few things to take note of in the schematic:
When lines in the schematic cross, they are not connected unless a dot (a node) is present at the crossing.
You can use pins A4 and A5 or pins SDA and SCL, respectively. The respective pins go to the same place on the microcontroller. I always use the SDA and SCL pins just to remind myself that I am using them for I2C.
The labels on the MCP4725 on the schematic are different than printed on the board. Without going into too much discussion behind the nomenclature, you can consider Vcc and Vdd to be the same thing: this is input voltage. Similarly, you can functionally consider Vee and Vss to be the same thing: these go to ground. On the MCP4725 breakout boards we have, VSS is labeled GND and VOUT is labeled VOU.
The DAC is powered through the Vdd/ground pins. Communication via I2C is accomplished via the SDA and SCL pins, which are connected to the appropriate pins on the Arduino board. Recall from our tour of Arduino that I2C works by switching the voltage on SCL (the clock) on and off at a given frequency. A comparison is made between the clock voltage and the data voltage (from the SDA pin); if both are high, the transmitted bit is one, and if the SDA voltage is low, the transmitted bit is zero.
Using the Wire library
In order to conveniently communicate using I2C, we will use the Wire library. In order to use it, or any other library, it needs to be included in your sketch. The syntax for including an installed library is # include <Wire.h>
, where you can substitute whatever library you are using for Wire.h
. It is easiest to see how the library is used by example. Load the following sketch onto Arduino.
// Wire is library to talk with I2C
#include <Wire.h>
//This is the I2C Address of the MCP4725, by default (A0 pulled to GND).
//For devices with A0 pulled HIGH, use 0x63
#define MCP4725_ADDR 0x62
const float freq = 1.0;
const unsigned long sampleDelay = 20;
unsigned long lastSampleTime = 0;
void write12BitI2C(int x) {
/*
* Write a 12-bit integer out to I2C.
*/
Wire.beginTransmission(MCP4725_ADDR);
Wire.write(64); // cmd to update the DAC
Wire.write(x >> 4); // the 8 most significant bits...
Wire.write((x % 16) << 4); // the 4 least significant bits...
Wire.endTransmission();
}
void setup() {
Wire.begin();
}
void loop() {
unsigned long currTime = millis();
if (currTime - lastSampleTime > sampleDelay) {
int x = (int)(4095 * (1 + sin(2 * PI * freq * millis() / 1000.0)) / 2.0);
write12BitI2C(x);
lastSampleTime = currTime;
}
}
We #include
the library at the top. In the setup()
function, we call Wire.begin()
to open communication via I2C. When we transmit data to the MCP4725, we need to know its address. If the A0 pin of the MCP4725 (completely different from the A0 pin on the Arduino Uno) is connected to ground (or not connected), the address is 0x62
, which is hexidecimal for 98. In the A0 pin is connected to a high voltage, then the address is 0x63
. This enables you to have two DACs controlled
by the same Arduino Uno. We only have one here, so we will leave the A0 pin unconnected and use address 0x62
. We define the address #define MCP4725_ADDR 0x62
. A #define
directive basically tells the compiler to substitute 0x62
wherever MCP4725_ADDR
appears in the code.
Now that we have the address, we can write data to the DAC. The function write12BitI2C()
is used to send a 12-bit integer (since our DAC is 12-bit). First, we need to begin a transmission using the (you guessed it) Wire.beginTransmission()
function, with the address of the device we are communicating with as the argument. To tell the MCP4725 to expect to receive data to convert to analog voltage, we send 0x40
, which corresponds to the decimal number 64. This is done using
Wire.write(64)
. Since we can only write a byte (8 bits) at a time, we have to write the 12-bit number in two steps. First, we write the 8 most significant bits by shifting the 12-bit integer rightward 4 bits. We get the remaining four bits as our input modulo 2⁴ = 16. We then shift it four bits leftward so that the next four bits received by the MCP4725 at the last four bits of the input. Finally, we end the transmission with Wire.endTransmission()
. (If this all seems confusing, don’t
worry; soon you won’t need to think about it.)
In the loop, we generate a sine wave that oscillates between zero and 4095 with a frequency of 1 Hz. We send that to the DAC over I2C, sending data every 20 ms. The LED will then smoothly turn on and off.
Using an external library
Libraries are there to make things easier for you. In the above example, we had to do some bit-twiddling to get our number sent across to the DAC. It would be nice if we had a library that took care of that for us. It turns out there is one! The library is made by Adafruit to go along with the breakout board. It is called Adafruit_MCP4725
. The Arduino IDE is aware of many third-party libraries, and you can install them into the IDE. To install this library, within the Arduino IDE, click on
Tools
→ Manage Libraries...
. The Libary Manager window will pop up. You can type MCP4725
in the search window, and you will have the option to install the Adafruit MCP4725 library. Go ahead and do this. While you are at it, find the Adafruit ADS1X15 library, which we will use in a future lesson.
Now that the library is installed, you can include it as we did with the built-in Wire library. The sketch below accomplishes the same result as the sketch above, but uses the convenient features of the Adafruit MCP4725 library.
// Adafruit provides a convenient library!
#include <Adafruit_MCP4725.h>
//This is the I2C Address of the MCP4725, by default (A0 pulled to GND).
//For devices with A0 pulled HIGH, use 0x63
#define MCP4725_ADDR 0x62
const int freq = 1;
const unsigned long sampleDelay = 20;
unsigned long lastSampleTime = 0;
// Instantiate the convenient class
Adafruit_MCP4725 dac;
void setup() {
dac.begin(MCP4725_ADDR);
}
void loop() {
unsigned long currTime = millis();
if (currTime - lastSampleTime > sampleDelay) {
uint16_t x = (uint16_t)(4095 * (1 + sin(2 * PI * freq * millis() / 1000.0)) / 2.0);
dac.setVoltage(x, false);
lastSampleTime = currTime;
}
}
The sketch warrants some comments. The library defines a useful Adafruit_MCP4725
class. This class has convenient begin()
and setVoltage()
functions. The begin()
function is called in the setup()
function to open communication. The address is given here, and is stored in the instance of the class, so you do not need to use the address in the loop()
. The setVoltage()
function sends an integer to the MCP4725 for conversion to an analog voltage. The function expects the
integer to be of type uint16_t
, which is a 16-bit unsigned integer. It also takes a second argument, which, if true
, the integer is stored in the EEPROM on-board the MCP4725 breakout board. If the second argument is false
, the input is simply converted to an analog voltage and not stored.
The setVoltage()
function is also clever in that it speeds up the rate of data transfer by I2C. As we mentioned when introducing I2C in lesson 2, the Arduino Uno can communicate over I2C at about 400 kbaud, but defaults to 100 kbaud. When you use setVoltage()
, it automatically works at 400 kbaud.