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.
Why This Matters for Your Projects
Section titled “Why This Matters for Your Projects”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.
How PWM Actually Works
Section titled “How PWM Actually Works”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:
Duty Cycle
Section titled “Duty Cycle”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 Cycle | Average Voltage | Effect on LED |
|---|---|---|
| 0% | 0V | Off |
| 25% | ~0.8V | Dim |
| 50% | ~1.65V | Medium |
| 75% | ~2.5V | Bright |
| 100% | 3.3V | Full brightness |
Frequency
Section titled “Frequency”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.
On the tinyCore (ESP32-S3)
Section titled “On the tinyCore (ESP32-S3)”LEDC: The ESP32’s PWM Hardware
Section titled “LEDC: The ESP32’s PWM Hardware”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.
Key Functions
Section titled “Key Functions”// Step 1: Configure a PWM channelledcSetup(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 pinledcAttachPin(pin, channel);
// Step 3: Set the duty cycleledcWrite(channel, dutyCycle);// dutyCycle: 0 to (2^resolution - 1)// For 8-bit: 0–255. For 13-bit: 0–8191.Example: Fade an LED
Section titled “Example: Fade an LED”const int ledPin = 21; // LED_BOOT on tinyCoreconst int channel = 0;const int freq = 5000; // 5 kHz — no visible flickerconst 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); }}Example: Play a Tone on a Buzzer
Section titled “Example: Play a Tone on a Buzzer”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);}Resolution vs Frequency Tradeoff
Section titled “Resolution vs Frequency Tradeoff”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.
| Resolution | Steps | Max Frequency |
|---|---|---|
| 8-bit | 256 | ~312 kHz |
| 10-bit | 1,024 | ~78 kHz |
| 13-bit | 8,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.
What PWM Controls
Section titled “What PWM Controls”| Application | Typical Frequency | How Duty Cycle Is Used |
|---|---|---|
| LED brightness | 1–5 kHz | 0% = off, 100% = full bright |
| Motor speed | 5–25 kHz | 0% = stop, 100% = full speed |
| Buzzer tone | 20 Hz–20 kHz | Frequency = pitch; 50% duty = loudest |
| Servo position | 50 Hz | Pulse width (1–2 ms) controls angle |
| RGB color mixing | 1–5 kHz | One channel per color (R, G, B) |
PWM vs DAC
Section titled “PWM vs DAC”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.
Quick Reference
Section titled “Quick Reference”| Feature | ESP32-S3 |
|---|---|
| PWM channels | 8 |
| Max resolution | Up to 14-bit (at low frequencies) |
| Typical setup | 8-bit at 5 kHz |
| Key functions | ledcSetup(), ledcAttachPin(), ledcWrite() |
| Tone function | ledcWriteTone(channel, frequency) |
analogWrite() | Not available on ESP32 — use LEDC |
tone() | Not available on ESP32 — use ledcWriteTone() |
Learn More
Section titled “Learn More”- Blink an LED — your first PWM project (LED fading)
- Buzz a Buzzer — PWM for sound output
- What is GPIO? — the pins that PWM outputs on
- What is a DAC? — when you need real analog output instead of PWM