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.
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.
- 1 × Arduino Uno R3In Starter Kit, Claw Machine Kit, Vending Machine Kit.
- 1 × DAC, MCP4725
- 1 × Oscilloscope
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 pin | Connects to |
|---|---|
| Oscilloscope pin GND | Arduino Uno R3 pin GND |
| Oscilloscope pin CH2 | Arduino Uno R3 pin 9 |
| DAC, MCP4725 pin VCC | Arduino Uno R3 pin 5V |
| DAC, MCP4725 pin GND | Arduino Uno R3 pin GND |
| DAC, MCP4725 pin VOUT | Arduino Uno R3 pin A0 |
| Oscilloscope pin CH1 | Arduino Uno R3 pin A0 |
| DAC, MCP4725 pin SDA | Arduino Uno R3 pin A4 |
| DAC, MCP4725 pin SCL | Arduino 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.
// 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_MSsets 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.