Skip to content

What is PWM?

PWM (Pulse Width Modulation) rapidly switches a pin between HIGH (3.3V) and LOW (0V) to control the average power output. By changing what percentage of time the pin stays on, you can dim LEDs, control motor speed, set servo positions, and generate tones on a buzzer — all from a digital pin that can only be fully on or fully off.

PWM is one of the most useful tools on the tinyCore. Every time you dim an LED, control a motor’s speed, position a servo, or play a tone on a buzzer, you’re using PWM. The Blink an LED and Buzz a Buzzer tutorials both use it. Understanding how it works will help you get more out of these projects and build new ones.

A digital GPIO pin can only output two voltages: 3.3V (HIGH) or 0V (LOW). It can’t output 1.5V or 2.0V directly. PWM gets around this by switching between HIGH and LOW thousands of times per second — fast enough that the connected device experiences a smooth average.

Two parameters control the output:

The percentage of each cycle that the pin stays HIGH. A 50% duty cycle means the pin is HIGH half the time and LOW half the time — the effective average is ~1.65V. A 25% duty cycle means HIGH for a quarter of each cycle — average of ~0.8V.

Duty CycleAverage VoltageEffect on LED
0%0VOff
25%~0.8VDim
50%~1.65VMedium
75%~2.5VBright
100%3.3VFull brightness

How many times per second the pin completes a full HIGH→LOW cycle. Measured in Hertz (Hz). For LED dimming, anything above ~1 kHz is fast enough that your eyes can’t see flickering. For buzzers, the frequency determines the pitch — 440 Hz plays the note A4, 523 Hz plays C5, etc.

The ESP32-S3 has a dedicated PWM peripheral called LEDC (LED Control). Despite the name, it works for any PWM application — not just LEDs. It provides 8 independent PWM channels, each configurable with its own frequency and duty cycle.

// Step 1: Configure a PWM channel
ledcSetup(channel, frequency, resolution);
// channel: 0–7 (which PWM channel to configure)
// frequency: in Hz (e.g., 5000 for LEDs, 50 for servos)
// resolution: bit depth (e.g., 8 = 256 steps, 13 = 8192 steps)
// Step 2: Attach the channel to a GPIO pin
ledcAttachPin(pin, channel);
// Step 3: Set the duty cycle
ledcWrite(channel, dutyCycle);
// dutyCycle: 0 to (2^resolution - 1)
// For 8-bit: 0–255. For 13-bit: 0–8191.
const int ledPin = 21; // LED_BOOT on tinyCore
const int channel = 0;
const int freq = 5000; // 5 kHz — no visible flicker
const int resolution = 8; // 8-bit: 0–255
void setup() {
ledcSetup(channel, freq, resolution);
ledcAttachPin(ledPin, channel);
}
void loop() {
// Fade up
for (int duty = 0; duty <= 255; duty++) {
ledcWrite(channel, duty);
delay(10);
}
// Fade down
for (int duty = 255; duty >= 0; duty--) {
ledcWrite(channel, duty);
delay(10);
}
}

For buzzers, ledcWriteTone() sets the frequency directly:

const int buzzerPin = 2;
const int channel = 0;
void setup() {
ledcSetup(channel, 1000, 8);
ledcAttachPin(buzzerPin, channel);
}
void loop() {
ledcWriteTone(channel, 440); // play A4 (440 Hz)
delay(500);
ledcWriteTone(channel, 523); // play C5 (523 Hz)
delay(500);
ledcWriteTone(channel, 0); // silence
delay(500);
}

Higher resolution means more steps of control (smoother LED fading), but the maximum achievable frequency decreases. Higher frequency means less visible flicker, but fewer resolution steps are available.

ResolutionStepsMax Frequency
8-bit256~312 kHz
10-bit1,024~78 kHz
13-bit8,192~9.7 kHz

For LED dimming, 8-bit at 5 kHz is a great default — 256 brightness levels with no visible flicker. For servos, use 16-bit at 50 Hz for fine position control.

ApplicationTypical FrequencyHow Duty Cycle Is Used
LED brightness1–5 kHz0% = off, 100% = full bright
Motor speed5–25 kHz0% = stop, 100% = full speed
Buzzer tone20 Hz–20 kHzFrequency = pitch; 50% duty = loudest
Servo position50 HzPulse width (1–2 ms) controls angle
RGB color mixing1–5 kHzOne channel per color (R, G, B)

PWM produces a pulsed square wave that averages to a target voltage. A true DAC produces a clean, steady voltage. For LEDs, motors, buzzers, and servos, PWM is perfect — these devices either don’t care about the pulsing or their physical inertia smooths it out. For applications that need a real steady voltage (audio output, precision analog circuits), you need an actual DAC.

FeatureESP32-S3
PWM channels8
Max resolutionUp to 14-bit (at low frequencies)
Typical setup8-bit at 5 kHz
Key functionsledcSetup(), ledcAttachPin(), ledcWrite()
Tone functionledcWriteTone(channel, frequency)
analogWrite()Not available on ESP32 — use LEDC
tone()Not available on ESP32 — use ledcWriteTone()