Microcontrollers live in a world of ones and zeros, but the things we control — LED brightness, motor speed, audio, a reference voltage — want something in between. There are two common ways to get "in between" out of a digital chip: switch a pin on and off very fast (PWM), or produce a real intermediate voltage (DAC). They are not interchangeable, and picking the wrong one is a classic source of noise, flicker and frustration.
Two ways to make "half a volt"
- PWM — pulse-width modulation. The pin is only ever fully on or fully off, but the proportion of time it spends on — the duty cycle — sets the average.
- DAC — digital-to-analog converter. A circuit inside (or next to) the chip turns a number into an actual steady voltage between 0 and its reference.
Measure both with a slow multimeter and they may show the same voltage. Look with an oscilloscope and they are completely different signals.
How PWM works
A hardware timer counts up from zero to a top value, then restarts. The pin is high while the count is below a compare value and low after it. The ratio of compare to top is the duty cycle, D, and the average voltage is simply:
At 3.3 V, 25 % duty averages 0.825 V; 50 % averages 1.65 V.
The frequency–resolution trade-off
Because the duty step is one timer tick, resolution and frequency fight each other. For a counter clocked at fclk with N bits:
The ESP32's LEDC peripheral, clocked from 80 MHz, can do 13-bit resolution (8192 steps) only up to about 80 MHz ÷ 8192 ≈ 9.8 kHz. Ask for 20 kHz and the maximum drops to 11 bits; 1 MHz leaves only 6 bits. An Arduino Uno's analogWrite() is 8-bit at about 490 Hz (980 Hz on pins 5 and 6).
How a DAC works
A DAC divides its reference voltage into 2N steps and outputs the one you ask for. The step size is:
An 8-bit DAC at 3.3 V has steps of about 12.9 mV; a 12-bit DAC at the same reference, about 0.8 mV. The output is a genuine DC level that updates as fast as the DAC and your code can write new values — so it can draw a sine wave, an audio signal or a slow ramp.
The catch is availability. Many popular chips have no DAC at all:
| Board / chip | Built-in DAC |
|---|---|
| Arduino Uno (ATmega328P) | None — PWM only |
| Arduino Uno R4 | 12-bit on A0 |
| ESP32 (original) | Two 8-bit channels on GPIO25 and GPIO26 |
| ESP32-S3, ESP32-C3 | None |
| Raspberry Pi Pico (RP2040) | None |
| External: MCP4725 | 12-bit over I²C, one channel |
Turning PWM into a real voltage
If you need a steady voltage but only have PWM, a low-pass RC filter averages the pulses. The resistor and capacitor set a cutoff frequency:
Put the cutoff far below the PWM frequency. What remains is a small ripple riding on the average. For a simple RC with R·C much longer than one PWM period, the peak-to-peak ripple is approximately:
A worked example
ESP32 at 3.3 V, PWM at 20 kHz, 50 % duty, R = 10 kΩ, C = 1 µF:
- R·C = 10 ms, so fc ≈ 16 Hz — over a thousand times below 20 kHz.
- Ripple ≈ 3.3 × 0.5 × 0.5 ÷ (20 000 × 0.01) ≈ 4 mV peak-to-peak. Fine for a reference or a slow control signal.
- The price is speed: the output takes about 5·R·C ≈ 50 ms to settle after you change the duty. It cannot follow anything faster than a few hertz.
That is the fundamental trade-off of filtered PWM: less ripple means slower response. A second RC stage (or a higher PWM frequency) improves ripple without slowing things down as much. Also remember the output has the resistor's impedance — buffer it with an op-amp follower before driving any real load.
When PWM is the right choice
PWM shines whenever the load itself does the averaging:
- LEDs. Your eye averages fast flicker. Use a few kHz or more to avoid visible flicker and camera banding.
- DC motors. The winding inductance smooths the current. Higher frequencies (around 20 kHz) also move the whine above hearing.
- Heaters and thermal loads. Thermal mass averages over seconds; even very slow PWM works.
- Servos. Here the pulse width is the information: typically a 50 Hz frame with a 1–2 ms pulse. No filtering — the servo reads the pulse directly.
- Efficiency. A switch that is fully on or fully off dissipates very little. That is why power electronics — including buck converters — are built on PWM.
When you really need a DAC
- Waveforms and audio — anything that changes quickly and must be smooth.
- Reference or bias voltages for analog circuits, where ripple would show up as noise or error.
- Driving sensitive analog inputs such as a comparator threshold or an op-amp setpoint.
- Fast settling — a DAC moves to a new level in microseconds, not tens of milliseconds.
ESP32 code for both
With the ESP32 Arduino core 3.x:
const int LED_PIN = 18; // any output-capable GPIO
const int DAC_PIN = 25; // DAC1 on the original ESP32 (GPIO25 or GPIO26)
void setup() {
// PWM: 5 kHz, 13-bit resolution (0 … 8191)
ledcAttach(LED_PIN, 5000, 13);
}
void loop() {
// PWM: sweep LED brightness
for (int duty = 0; duty <= 8191; duty += 64) {
ledcWrite(LED_PIN, duty);
delay(5);
}
// DAC: 8-bit value 0 … 255 → 0 … ~3.3 V
dacWrite(DAC_PIN, 128); // ≈ 1.65 V, steady
delay(500);
}
ledcSetup(channel, freq, bits), ledcAttachPin(pin, channel) and ledcWrite(channel, duty). Check which version your board package uses.Side by side
| PWM | DAC | |
|---|---|---|
| Output | Square wave; average = D × V | Steady voltage level |
| Availability | Almost every MCU, many pins | Few MCUs, 1–2 pins, or external chip |
| Resolution | Trades against frequency | Fixed (8, 10, 12 bits…) |
| Speed | Slow if filtered | Fast (µs settling) |
| Drive power | Can switch a MOSFET for amps | Weak; needs a buffer |
| Best for | LEDs, motors, heaters, servos, power | Audio, waveforms, references |
Quick decision checklist
- Does the load average on its own (light, motor, heat)? → PWM.
- Does the signal need to change faster than a few hertz and stay smooth? → DAC.
- Need a slow, steady voltage and have no DAC? → PWM + RC filter, buffered.
- Need clean 12-bit levels on a chip without a DAC? → external DAC such as the MCP4725.