PWM: faking analog with fast switching

Beginner14 min readUpdated September 2026

A microcontroller pin can only be on or off, yet it can dim an LED, set a motor's speed and play a tune. The trick is switching very fast, and choosing how long to stay on.

A microcontroller pin is a blunt instrument. It's either on, at 3.3 V, or off, at 0 V. There's nothing in between.

And yet the same pin can fade an LED smoothly from dark to full brightness, run a motor at any speed you like, and play a tune on a buzzer. None of those are on-or-off jobs.

The trick is called pulse-width modulation, or PWM. The pin still only switches fully on and fully off, but it does it so fast that whatever's connected only notices the average. By choosing how long each pulse stays on, you choose that average.

On, off, and the average

If you've read Getting the voltage you need, this will sound familiar. A buck converter switches fully on and off because a switch that's either wide open or firmly shut wastes almost nothing. Remember the transistor valve from Electricity, explained with water: it's the part-open valve that gets hot.

PWM is the same idea, without the smoothing. The pin switches on and off in a steady rhythm. The fraction of each cycle it spends on is the duty cycle:

average voltage = duty cycle × 3.3 V

On 25% of the time gives an average of about 0.8 V. On 75% of the time gives about 2.5 V. On all the time is just 3.3 V.

A pin can only be fully on or fully off. Set how much of each cycle it's on, then slow the pulses down until you can see the trick.

The pin, over two cycles

3.3 V0 Vaverage 0.99 Vtwo cycles = 2 s

LED

on

Motor

0% speed

30%

How much of each cycle the pin is on

Frequency (cycles per second)

At 1 Hz you can watch every pulse. The LED blinks, and the motor lurches: its heavy rotor speeds up during each pulse and coasts down in between.

Duty cycle
30%
time spent on
Average voltage
0.99 V
3.3 V × duty
Frequency
1 Hz
cycles per second
On for
300 ms
each cycle

Slow it down to 1 Hz and the trick falls apart: the LED just blinks. Speed it up and the blinks blur together, until at 1 kHz the LED simply looks dimmer.

Who does the averaging?

A buck converter uses an inductor and a capacitor to smooth the pulses into a steady voltage. With PWM, nothing in the circuit smooths anything. The pin really is switching fully on and off, 3.3 V then 0 V. The averaging happens in whatever is on the receiving end.

Your eye averages light. Your vision can't follow flashes much faster than about 60 to 90 a second. Anything quicker blurs into a steady glow, whose brightness depends on how much of the time the light is on. That's persistence of vision, and it's also how films and screens work.

A motor averages with its weight. A motor's rotor is the heavy water wheel from Electricity, explained with water. One short push doesn't get it spinning, and it doesn't stop the instant the push ends. Fast enough pulses feel to the rotor like a steady, gentler push. The motor's coils also act as an inductor, which smooths the current itself.

A buzzer doesn't average at all, and that's the point. You want to hear the switching. More on that at the end.

PWM saves power as well as faking voltages. An LED at 25% duty cycle draws full current a quarter of the time, so it uses a quarter of the power. Dimming it doesn't need an extra resistor wasting energy as heat.

Choosing a frequency

How fast should the pin switch? Fast enough that whatever's on the end only sees the average, but no faster than it needs to be.

LEDs need a few hundred hertz at least. Below about 100 Hz, many people see flicker, especially out of the corner of their eye or when the LED is moving. Phone cameras are even fussier and show dark bands across a PWM-dimmed LED unless it runs at a kilohertz or more.

Motors are awkward, because their coils vibrate at the PWM frequency. Run a motor at 1 kHz and you'll hear it whine at 1 kHz. Most motor drivers run at 20 kHz or more, just above the range of human hearing.

Higher isn't free. Every time the transistor switches, it spends a brief moment part open, and a part-open valve gets hot. The more switches per second, the more of those moments, and the more heat. So you pick the lowest frequency that does the job.

Hobby servos also use pulses, but they're not PWM power. A servo reads the width of a pulse sent 50 times a second, roughly 1 to 2 ms, as a message saying which angle to hold. Feeding one ordinary LED-style PWM doesn't set its speed. Use a servo library instead.

How many steps?

PWM duty cycles aren't infinitely fine. They come in steps, set by the resolution. At 8-bit resolution there are 256 steps, numbered 0 to 255, and that's what analogWrite() uses:

analogWrite(LED_PIN, 0);     // off
analogWrite(LED_PIN, 64);    // about 25%
analogWrite(LED_PIN, 255);   // fully on

On the ESP32-S3, you can choose the frequency and resolution yourself. In version 3 of the ESP32 Arduino core, that's ledcAttach() and ledcWrite():

ledcAttach(MOTOR_PIN, 20000, 10);   // 20 kHz, 10-bit: steps 0 to 1023
ledcWrite(MOTOR_PIN, 512);          // about 50%

(Older versions of the core used ledcSetup() and ledcAttachPin() instead. If an example online won't compile, that's usually why.)

There's a trade-off between the two. The PWM hardware counts through every step once per cycle, off a fixed clock, so more steps means fewer cycles per second. Very roughly, each extra bit of resolution halves the fastest frequency you can reach. 8 bits at 20 kHz is easy. 16 bits at 20 kHz isn't possible.

Brightness isn't what it seems

Set an LED to 50% duty cycle and you'd expect it to look half as bright. It doesn't. It looks about three-quarters as bright.

Your eyes don't respond to light in a straight line. They're very sensitive to changes in dim light and much less sensitive to changes in bright light. It's what lets you see both in a dark room and on a sunny beach. So equal steps in duty cycle don't look like equal steps in brightness. Most of the visible change happens in the first quarter of the range, and the top half barely seems to move.

The fix is gamma correction: bending the numbers so that equal steps in what you ask for become equal steps in what you see.

Both rows step the brightness from 0 to 100% in equal steps. The top row sets the PWM duty cycle to match. The bottom row corrects for how eyes actually work. Which one looks like even steps?

Linear: duty = brightness you asked for

PWM value

0
26
51
77
102
128
153
179
204
230
255

Gamma corrected: duty = brightness^2.2

PWM value

0
2
7
18
34
55
83
116
156
202
255

In the top row, the right-hand half all looks about the same, and the big changes are squashed into the first few steps. The bottom row spends most of its range at the dim end, where your eye is most sensitive, so each step looks like the same jump.

50%

The brightness you want to see

LinearPWM 128
CorrectedPWM 55

Knob position → PWM value

012825550%brightness knob
LinearGamma corrected
Knob
50%
what you want to see
Linear PWM
128
of 255
Corrected PWM
55
of 255
Light out
22%
corrected, looks like the knob

In code, gamma correction is one line of maths. The exponent is usually somewhere between 2.2 and 2.8. Tune it by eye:

uint8_t gammaCorrect(uint8_t level) {
  return (uint8_t)(pow(level / 255.0, 2.2) * 255.0 + 0.5);
}
 
analogWrite(LED_PIN, gammaCorrect(brightness));

There's a catch at the dim end. Correction squeezes the bottom of the range into very few PWM values: at 8 bits, the dimmest settings all round to 0, 1 or 2. That's why slow fades to darkness look steppy. If you need silky-smooth fades, use a higher resolution, like 10 or 12 bits, and scale the formula to match.

Making sound

The Vector S3 has a small magnetic buzzer on GPIO48. A buzzer is a coil and a thin metal disc: each pulse pulls the disc in, and between pulses it springs back. Push it a few hundred times a second and it vibrates the air. That's sound.

For sound, the duty cycle stays fixed at 50%, and what you change is the frequency. Frequency is pitch: 523 pulses a second is the note C, and double it to 1,047 and you get the C an octave higher. Arduino's tone() function does exactly this.

tone() is PWM with the duty cycle fixed at 50%. What changes is the frequency, and frequency is pitch. Tap the keys (sound is quiet, and you can mute it) and watch the wave squeeze together as the notes go up.

5 ms of the signal on GPIO48

3.3 V0 V2.6 cycles in 5 ms
tone(48, 523, 300);   // 523 Hz for 300 ms
delay(300);
noTone(48);
digitalWrite(48, LOW);       // always leave the buzzer off
Frequency
523 Hz
pitch
Period
1.91 ms
one on + one off
Pin flips
1,046/s
on or off
Duty cycle
50%
fixed by tone()

A few things are specific to the Vector S3's buzzer:

  • It's driven through a transistor. The pin can't supply the current the coil needs, so, just as in the transistor section of Electricity, explained with water, the pin works a valve and the valve switches the real current.
  • It's loudest at about 4 kHz. That's the disc's natural resonance, the frequency it most wants to vibrate at. Notes further away from it sound quieter.
  • Always leave the pin LOW when you're done. A steady HIGH makes no sound, but it holds full current through the coil, which heats the buzzer and drains the battery. It's the one PWM output where "on" is never a sensible resting state.
tone(48, 4000, 150);    // a short, loud beep
delay(150);
noTone(48);
digitalWrite(48, LOW);  // required

Cheat sheet

DrivingTypical frequencyWhat matters
LEDs500 Hz to a few kHzFast enough to avoid flicker and camera banding; gamma-correct the brightness
DC motors, through a driver20 kHz or moreAbove hearing, so the motor doesn't whine
Buzzers and speakers100 Hz to 5 kHzThe frequency is the pitch; 50% duty
Servos50 HzNot PWM power at all: the pulse width is a position command

One pin, two states, and a stopwatch. That's all PWM is. And it's behind a surprising share of everything a microcontroller does to the physical world.