Built-in project · Arduino Uno R3

DAC sine wave on an Arduino Uno

An MCP4725 on I2C plays a 32-point sine onto a scope: a real voltage, not PWM, and A0 reads it back to check. The whole build, an Arduino Uno and 2 more parts, runs here in your browser on the firmware below; open it in the editor to change the wiring or the code and run it again.

Teaching? Assign it to your class in one click.

Intermediate About 20 minutes to build and run. Runs in your browser. Free, and no account needed.

The parts retail for about $29. On Mokxi they're free.

The circuit runs here once the page has loaded.

What you'll learn

  • A true DAC versus PWM
  • Lookup tables for a waveform
  • Reading a device over I2C at address 0x60
  • Checking an output by reading it back with an ADC
  • Difficulty: Intermediate
  • Time: About 20 minutes
  • Board: Arduino Uno R3
  • Parts: 2

How it works

An MCP4725 DAC on the I2C bus (SDA on A4, SCL on A5) produces a real analog voltage: 5 V times the code divided by 4096. The sketch steps through a 32-point sine table, writing one point every 20 milliseconds.

The DAC's output also goes to A0, so the Uno can read back what it made. Every eighth point it prints the code, the expected volts and what A0 measured. An oscilloscope shows the DAC on channel 1.

Pin 9 plays the same sine with analogWrite, which is PWM. On channel 2 it shows as a square wave whose width follows the sine, while the DAC trace is a smooth staircase.

Parts list

2 parts, plus the jumper wires and a bench instrument. The parts retail for about $29; on Mokxi they're free, and so are the instruments.

Wiring table

Every connection, pin by pin, read from the circuit itself. Each row joins the pin on the left to the pin on the right, by a jumper wire or a breadboard strip.

Part and pinConnects to
Oscilloscope pin GNDArduino Uno R3 pin GND
Oscilloscope pin CH2Arduino Uno R3 pin 9
DAC, MCP4725 pin VCCArduino Uno R3 pin 5V
DAC, MCP4725 pin GNDArduino Uno R3 pin GND
DAC, MCP4725 pin VOUTArduino Uno R3 pin A0
Oscilloscope pin CH1Arduino Uno R3 pin A0
DAC, MCP4725 pin SDAArduino Uno R3 pin A4
DAC, MCP4725 pin SCLArduino Uno R3 pin A5

The code

The firmware exactly as the editor opens it. Change a line there and press Run: it compiles in the browser.

sketch.ino · Arduino Uno R3
// An MCP4725 DAC: a real analog voltage out of an I2C bus.
//
// analogWrite() on an Uno is PWM, a square wave that only averages to a
// voltage. The MCP4725 puts out the voltage itself: VOUT = 5 V x code / 4096,
// in 4096 steps. Here it plays a slow sine, and its output also goes to A0,
// so the sketch can read back what it made.
//
//   MCP4725  SDA, SCL -> SDA, SCL (A4, A5)   VCC -> 5V, GND -> GND
//            VOUT -> A0, and to the scope's channel 1
//   pin 9 -> the scope's channel 2: the same sine as PWM, for comparison
//
// Every 20 ms the sketch writes the next point of a 32-point sine. Every
// eighth point it prints the code, the volts that should come out, and what
// A0 reads. Pin 9 plays the same sine with analogWrite(): on the scope,
// channel 1 is a smooth staircase and channel 2 a square wave whose width
// follows it.
//
// THINGS TO TRY. Change STEP_MS to 1 and the sine is 30 Hz. Or wire A0 on
// the DAC to VCC: it moves to 0x61 and begin() at 0x60 finds nothing.

#include <Wire.h>
#include <Adafruit_MCP4725.h>

const uint8_t DAC_ADDRESS = 0x60;
const unsigned long STEP_MS = 20;

Adafruit_MCP4725 dac;

// One cycle of a sine, 32 points, centered on 2048.
const uint16_t SINE[32] = {
  2048, 2447, 2831, 3185, 3495, 3750, 3939, 4056,
  4095, 4056, 3939, 3750, 3495, 3185, 2831, 2447,
  2048, 1648, 1264, 910, 600, 345, 156, 39,
  0, 39, 156, 345, 600, 910, 1264, 1648,
};

uint8_t i = 0;

void setup() {
  pinMode(9, OUTPUT);
  Serial.begin(9600);
  if (!dac.begin(DAC_ADDRESS)) {
    Serial.println("No MCP4725 at 0x60: check SDA, SCL and A0.");
    while (1) {}
  }
  Serial.println("MCP4725 found at 0x60");
}

void loop() {
  uint16_t code = SINE[i];
  dac.setVoltage(code, false);
  analogWrite(9, code >> 4);    // 0 to 4095 down to 0 to 255
  if (i % 8 == 0) {
    delay(2);
    int a0 = analogRead(A0);
    Serial.print("code ");
    Serial.print(code);
    Serial.print(" = ");
    Serial.print(5.0 * code / 4096, 2);
    Serial.print(" V, A0 reads ");
    Serial.print(a0 * 5.0 / 1023, 2);
    Serial.println(" V");
  }
  i = (i + 1) % 32;
  delay(STEP_MS);
}

Code walkthrough

The key functions of the sketch, and what each one does.

setup sketch.ino

Sets pin 9 as an output, starts the serial port and calls dac.begin with address 0x60. If the chip does not answer, it prints a message about SDA, SCL and A0 and stops in an endless loop, so a wiring problem is obvious.

void setup() {
  pinMode(9, OUTPUT);
  Serial.begin(9600);
  if (!dac.begin(DAC_ADDRESS)) {
    Serial.println("No MCP4725 at 0x60: check SDA, SCL and A0.");
    while (1) {}
  }
  Serial.println("MCP4725 found at 0x60");
}

loop sketch.ino

Takes the next value from SINE, sends it to the DAC and writes the same value, shifted right by four, to pin 9 as a PWM duty. Every eighth step it waits 2 ms, reads A0 and prints the code, expected volts and measured volts. Then it advances the index and waits STEP_MS.

void loop() {
  uint16_t code = SINE[i];
  dac.setVoltage(code, false);
  analogWrite(9, code >> 4);    // 0 to 4095 down to 0 to 255
  if (i % 8 == 0) {
    delay(2);
    int a0 = analogRead(A0);
    Serial.print("code ");
    Serial.print(code);
    Serial.print(" = ");
    Serial.print(5.0 * code / 4096, 2);
    Serial.print(" V, A0 reads ");
    Serial.print(a0 * 5.0 / 1023, 2);
    Serial.println(" V");
  }
  i = (i + 1) % 32;
  delay(STEP_MS);
}

Troubleshooting

The usual mistakes with this circuit, and the fix for each.

  • The serial monitor says no MCP4725 at 0x60 This project
    Check SDA on A4, SCL on A5, and power. Wiring the DAC's A0 address pin to VCC moves it to 0x61.
  • The sine looks too fast to follow This project
    STEP_MS sets the time per point. At 20 ms the 32 points make a slow sine, and at 1 ms it runs about 30 Hz.
  • The device is not found on the bus. DAC
    The I2C address is 0x60 by default, or 0x61 with A0 high. On some boards it is 0x62 or 0x63. Run an I2C scanner. Pull SDA and SCL up to VCC.
  • The output voltage does not match the code. DAC
    The output is VCC times code divided by 4096, so the supply sets the range. Use a stable supply. Codes run 0 to 4095. The output cannot drive heavy loads, so buffer it with an op-amp.
  • The trace is flat or off the screen. The channel probes the wrong node, or the volts per division and time base do not suit the signal. Oscilloscope
    Pick the nodes you really mean and a common ground. Set volts per division so the signal fits, and the time base so you see a few cycles. Adjust the trigger level.
  • A fast or noisy signal looks like a different frequency or a mess because the time base is too slow. Oscilloscope
    Zoom the time base until you see single cycles. A signal faster than the sample rate can alias, so slow the test signal or raise the sample rate.
  • Upload fails or nothing runs after wiring pins 0 and 1 to a module. Arduino Uno R3
    Pins 0 and 1 are the USB serial port. Move the module to other pins, or unplug it while uploading. Also check the right board is selected.
  • Pins misbehave or the board resets when you power motors or servos from it. Arduino Uno R3
    Each I/O pin gives about 20 mA, 40 mA max. Power motors and servos from their own supply and join its ground to the Uno's GND.

Change it and keep it

Open it in the editor, change the circuit or the code, and keep your version in a free account.