The ESP32-WROOM-32 features 16 independent hardware PWM channels assignable to almost any of its 34 usable GPIOs. However, you must strictly avoid input-only pins (GPIO 34-39) and strapping pins (GPIO 0, 2, 12, 15) to prevent boot failures. More importantly, if you are writing code in 2026, the Arduino ESP32 Core 3.x has completely deprecated the old ledcSetup and ledcAttachPin functions in favor of the unified ledcAttach API. Using outdated tutorials will result in immediate compilation errors.
This guide provides the exact pin mapping, a decision matrix for selecting the right ESP32 variant, and fully compilable Core 3.x code to get your PWM project running without a second trip to the workbench.
The 2026 ESP32 PWM Pin Decision Matrix
Not all PWM tasks are created equal. The ESP32 actually has two different PWM peripherals: the LEDC (LED Controller) for general-purpose signals, and the MCPWM (Motor Control PWM) for high-power motor driving. Use this decision tree to pick the right hardware and chip variant for your specific application.
| If your project requires... | Then choose this peripheral... | Recommended Board Variant | Why? |
|---|---|---|---|
| Standard LED dimming or RC Servos (50Hz - 5kHz) | LEDC (ledcAttach) | Classic ESP32-WROOM-32 (DevKit V1) | 16 channels is plenty; classic chip is cheapest (~$4). |
| High-frequency LED multiplexing (>20kHz) | LEDC (8-bit resolution) | ESP32-S3-WROOM-1 | S3 has a more flexible LEDC matrix and handles high-speed timers without audio whine. |
| Audio tone generation (Piezo/Speaker) | LEDC (ledcWriteTone) | Any ESP32 variant | Built-in tone API handles frequency math automatically. |
| Brushed DC Motor speed control | MCPWM (mcpwm_*) | ESP32-S3 or ESP32-C6 | MCPWM includes hardware dead-time insertion and fault handling to prevent shoot-through. |
| Brushless (BLDC) or Stepper motors | MCPWM + Pulse Count | ESP32-S3 | S3 adds hardware pulse counting (PCNT) for closed-loop encoder feedback. |
Hardware Limits and Safe GPIO Mapping
A common mistake is assigning PWM to a pin that is physically incapable of outputting a signal, or worse, a pin that controls the ESP32's boot sequence. The classic ESP32 has 40 physical pins, but only 34 are exposed on standard DevKit boards, and only a subset are safe for PWM output.
The Math: Resolution vs. Frequency
The ESP32's LEDC peripheral runs off an 80 MHz APB clock. You cannot independently maximize both frequency and resolution. The formula is:
Max Frequency = 80,000,000 / (2 ^ Resolution)
- 12-bit resolution (0-4095): Max frequency is ~19.5 kHz. (Ideal for smooth LED fading).
- 10-bit resolution (0-1023): Max frequency is ~78 kHz.
- 8-bit resolution (0-255): Max frequency is ~312 kHz.
Safe vs. Unsafe Pin Table (Classic ESP32-WROOM-32)
| GPIO Pin | PWM Status | Notes & Warnings |
|---|---|---|
| GPIO 0, 2, 12, 15 | AVOID | Strapping pins. Pulling these high/low via PWM circuits at boot will change flash voltage or enter download mode, causing a boot loop. |
| GPIO 34, 35, 36, 39 | FORBIDDEN | Input-only pins. They physically lack output circuitry. ledcAttach will fail silently or throw a runtime error. |
| GPIO 1, 3 | CAUTION | Default UART TX/RX. Using PWM here will kill your Serial Monitor debugging. |
| GPIO 6-11 | FORBIDDEN | Connected to the integrated SPI flash memory. Using these will crash the chip. |
| GPIO 4, 5, 13, 14, 16-33 | SAFE | Excellent for PWM. GPIO 16-19 and 21-23 are the most reliable for breadboarding. |
Project Build: 4-Channel Precision LED Fader
We will build a 4-channel independent LED fader. This project targets the ESP32 DevKit V1 (ESP32-WROOM-32) and uses the modern Core 3.x API.
Parts List
- 1x ESP32 DevKit V1 (ESP32-WROOM-32 module, 30-pin or 38-pin variant)
- 4x 5mm Diffused LEDs (Any color)
- 4x 330Ω or 470Ω Through-hole resistors (1/4W)
- 1x Standard 830-point breadboard
- Male-to-Male jumper wires
Pin Mapping Table
| Component | ESP32 GPIO | Why this pin? |
|---|---|---|
| LED 1 (Anode via Resistor) | GPIO 16 | Safe output, no boot conflicts, easy physical access on 30-pin boards. |
| LED 2 (Anode via Resistor) | GPIO 17 | Safe output, adjacent to 16 for clean wiring. |
| LED 3 (Anode via Resistor) | GPIO 18 | Safe output, also doubles as VSPI SCK if you add SPI later. |
| LED 4 (Anode via Resistor) | GPIO 19 | Safe output, also doubles as VSPI MISO. |
| All LED Cathodes | GND | Common ground required for current return. |
Wiring Steps
- De-energize: Ensure the ESP32 is unplugged from your PC before wiring.
- Place Resistors: Insert one leg of each 330Ω resistor into GPIO 16, 17, 18, and 19 on the breadboard.
- Connect LEDs: Connect the Anode (long leg) of each LED to the other leg of the corresponding resistor.
- Ground the LEDs: Connect the Cathode (short leg, flat side) of all four LEDs to the negative rail on the breadboard.
- Complete Circuit: Run a jumper wire from any ESP32
GNDpin to the breadboard's negative rail. - Verify: Use a multimeter in continuity mode to verify there are no shorts between the GPIO pins and the ground rail before applying power.
Compilable Code (Arduino Core 3.x API)
This code is written for Arduino ESP32 Core 3.0.0 or newer (based on ESP-IDF 5.1). It uses the modern ledcAttach() function, which automatically allocates hardware channels behind the scenes. It also includes error handling to verify pin attachment.
#include <Arduino.h>
// Target Board: ESP32 DevKit V1 (ESP32-WROOM-32)
// Core Version: Arduino ESP32 Core 3.x+
const int pwmPins[] = {16, 17, 18, 19};
const int numPins = sizeof(pwmPins) / sizeof(pwmPins[0]);
// 5000 Hz is ideal for LEDs (no visible flicker, no audible whine)
const int pwmFreq = 5000;
// 12-bit resolution gives 0-4095 duty cycle steps
const int pwmResolution = 12;
void setup() {
Serial.begin(115200);
delay(1000); // Allow serial monitor to connect
Serial.println("Initializing ESP32 PWM (Core 3.x API)...");
// 1. Attach and configure PWM pins with error handling
for (int i = 0; i < numPins; i++) {
// ledcAttach returns true on success, false if channel allocation fails
bool success = ledcAttach(pwmPins[i], pwmFreq, pwmResolution);
if (!success) {
Serial.printf("[ERROR] Failed to attach PWM to GPIO %d. Check pin mapping.\n", pwmPins[i]);
// Halt execution if a critical pin fails
while (true) { delay(1000); }
} else {
Serial.printf("[OK] GPIO %d configured for %d Hz at %d-bit resolution.\n", pwmPins[i], pwmFreq, pwmResolution);
}
}
}
void loop() {
// Fade all LEDs up together
for (int duty = 0; duty <= 4095; duty += 16) {
for (int i = 0; i < numPins; i++) {
ledcWrite(pwmPins[i], duty);
}
delay(10);
}
// Fade all LEDs down together
for (int duty = 4095; duty >= 0; duty -= 16) {
for (int i = 0; i < numPins; i++) {
ledcWrite(pwmPins[i], duty);
}
delay(10);
}
delay(500); // Pause at off state
}
Debugging: "ledcSetup was not declared in this scope"
If you copied code from a tutorial written before 2024, you will likely hit this exact compilation error:
or
error: 'ledcAttachPin' was not declared in this scope
Ranked Causes and Fixes
- Cause: Using Arduino Core 3.x (Most Likely). Espressif overhauled the LEDC API in Core 3.0.0 to align with ESP-IDF v5.
ledcSetupandledcAttachPinwere permanently removed.
Fix: ReplaceledcSetup(channel, freq, res)andledcAttachPin(pin, channel)with the singleledcAttach(pin, freq, res)command. Remove all manual channel variables. - Cause: Typo in Function Name. You might have typed
ledcAttachpin(lowercase 'p').
Fix: Ensure exact capitalization:ledcAttachPin(for Core 2.x) orledcAttach(for Core 3.x). - Cause: Wrong Board Selected in IDE. If your IDE is set to an Arduino AVR board (like Uno) instead of an ESP32, the ESP32-specific libraries won't load.
Fix: Go to Tools > Board and selectESP32 Dev Module.
The First Three Things to Check When PWM Fails Silently
If the code compiles and uploads, but the LED stays dark or stays fully on, check these three things in order:
- Check the Resolution Math: Did you set a 12-bit resolution (4095 max) but write a duty cycle of
255? A duty of 255 on a 4095 scale is only 6% brightness. It will look completely off in a lit room. Change your max duty to 4095. - Check for Strapping Pin Conflicts: Did you use GPIO 12? If your PWM circuit pulls GPIO 12 high during the exact millisecond the ESP32 boots, the chip will switch its internal flash voltage regulator to 1.8V, cause a brownout, and reboot endlessly. Move to GPIO 16-19.
- Check the Ground Connection: The ESP32's 3.3V logic is weak. Ensure your LED cathode is tied to the ESP32's GND, not just floating or tied to an external power supply's ground without a common ground reference.
Extending and Simplifying the Build
How to Simplify (The analogWrite Fallback)
If you are migrating from an Arduino Uno and don't want to rewrite your entire codebase, Arduino ESP32 Core 3.x now natively supports analogWrite(pin, duty). By default, it maps 0-255 to a 5kHz PWM signal behind the scenes.
Trade-off: You lose the ability to precisely control frequency and resolution, and it uses more memory overhead than direct ledcAttach calls. Use it only for quick prototyping.
How to Extend (Scaling Beyond 16 Channels)
The classic ESP32-WROOM-32 physically maxes out at 16 LEDC channels (8 high-speed, 8 low-speed). If your project requires 32+ independent PWM channels (e.g., a massive LED matrix or complex robotics):
- Upgrade to ESP32-S3: The S3 variant expands the LEDC peripheral capabilities and handles memory allocation much more gracefully.
- Use I2C PWM Drivers: Add a PCA9685 16-channel PWM driver board (~$3 on Amazon). You can daisy-chain up to 62 of these on a single I2C bus, giving you nearly 1,000 hardware PWM channels controlled by just two ESP32 GPIO pins (SDA/SCL).
For authoritative documentation on the latest API changes, always refer to the official Espressif Arduino LEDC API Docs and the underlying ESP-IDF LEDC Peripheral Guide.






