If you are looking for a reliable ESP32-S3 PWM example, the direct answer is to use the LEDC (LED Control) peripheral via the modern ledcAttach() API introduced in ESP32 Arduino Core v3.x. Unlike the original ESP32, the S3 variant features 8 high-speed LEDC channels (no low-speed channels) and a completely different GPIO matrix. Attempting to use legacy ledcSetup() functions or routing PWM to internal SPI flash pins will result in silent failures or boot loops.
This guide provides a production-ready ESP32-S3 PWM implementation, explicit safe-pin mapping, and a debugging framework for when your signals fail to route.
ESp32-S3 PWM Capabilities and Safe Pin Mapping
The ESP32-S3 derives its PWM clock from the 80 MHz APB clock. The maximum achievable frequency is inversely proportional to the timer resolution. Before wiring your prototype, you must select GPIOs that are not reserved for boot strapping or internal flash routing.
| Resolution (Bits) | Max Duty Value | Max Frequency (Hz) | Safe GPIOs for PWM Output | GPIOs to AVOID (Hardware Reserved) |
|---|---|---|---|---|
| 8-bit | 255 | 312,500 | 1, 2, 4-18, 21, 33-44, 47-48 | 0, 3, 19-20, 22-32, 45, 46 |
| 10-bit | 1023 | 78,125 | 1, 2, 4-18, 21, 33-44, 47-48 | 0, 3, 19-20, 22-32, 45, 46 |
| 12-bit | 4095 | 19,531 | 1, 2, 4-18, 21, 33-44, 47-48 | 0, 3, 19-20, 22-32, 45, 46 |
| 14-bit | 16383 | 4,882 | 1, 2, 4-18, 21, 33-44, 47-48 | 0, 3, 19-20, 22-32, 45, 46 |
Hardware Build and Parts List
This build targets the ESP32-S3-DevKitC-1 (N8R8) variant, which includes 8MB of Quad SPI Flash and 8MB of Octal PSRAM. This specific board variant is chosen because its GPIO breakout is fully documented, avoiding the hidden pinout traps of generic unbranded S3 clones.
Required Components
- Microcontroller: ESP32-S3-DevKitC-1 (N8R8) - Approx. $9.00 - $12.00
- Driver: IRLZ44N Logic-Level N-Channel MOSFET (Vgs threshold 1-2V, ideal for 3.3V S3 GPIO) - $1.50
- Gate Protection: 100Ω gate resistor and 10kΩ gate-to-source pulldown resistor
- Load: 12V 50W LED COB module or 12V DC brush motor
- Power: 12V 5A DC switching power supply
Pin Mapping Table
| ESP32-S3 Pin | Component Pin | Function / Notes |
|---|---|---|
| GPIO 18 | 100Ω Resistor -> MOSFET Gate | PWM Output (LEDC Channel auto-assigned) |
| GND | MOSFET Source & 10kΩ Pulldown | Common ground reference |
| 3V3 | Voltage Divider (if monitoring) | Do NOT use to power high-current loads |
Safety Caveat: When driving inductive loads like DC motors, you must place a flyback diode (e.g., 1N5819 Schottky) in reverse parallel across the motor terminals. Failing to do this will induce voltage spikes that will destroy the MOSFET and back-feed the ESP32-S3, permanently bricking the silicon.
Complete ESP32-S3 PWM Example Code
The following C++ sketch is written for ESP32 Arduino Core v3.0.0 or newer. It utilizes the modern ledcAttach() function, which automatically allocates an available LEDC channel and timer, eliminating the manual channel math required in older core versions.
#include <Arduino.h>
// --- Pin Definitions ---
#define PWM_PIN 18 // Safe GPIO for ESP32-S3
#define PWM_FREQ 5000 // 5 kHz frequency (avoids audible motor whine)
#define PWM_RESOLUTION 10 // 10-bit resolution (0-1023 duty cycle)
// Function prototypes
void fadeLedSmoothly();
void beepPiezo();
void setup() {
Serial.begin(115200);
delay(1000); // Allow serial monitor to connect
Serial.println("ESP32-S3 PWM Initialization...");
// Modern ESP32 Core v3.x API: ledcAttach(pin, freq, resolution)
// Returns true if successful, false if no channels are available
bool attachSuccess = ledcAttach(PWM_PIN, PWM_FREQ, PWM_RESOLUTION);
if (!attachSuccess) {
Serial.println("[FATAL] LEDC Attach failed. Check if GPIO is valid or all 8 channels are in use.");
while(1) { delay(1000); } // Halt execution
}
Serial.printf("[OK] PWM attached to GPIO %d | Freq: %d Hz | Res: %d-bit\n",
PWM_PIN, PWM_FREQ, PWM_RESOLUTION);
}
void loop() {
fadeLedSmoothly();
delay(1000);
beepPiezo();
delay(2000);
}
void fadeLedSmoothly() {
Serial.println("Fading sequence started...");
// Fade up
for (int duty = 0; duty <= 1023; duty += 16) {
ledcWrite(PWM_PIN, duty);
delay(15);
}
// Fade down
for (int duty = 1023; duty >= 0; duty -= 16) {
ledcWrite(PWM_PIN, duty);
delay(15);
}
ledcWrite(PWM_PIN, 0); // Ensure fully off
}
void beepPiezo() {
Serial.println("Generating 2kHz tone...");
// ledcWriteTone temporarily overrides the setup frequency for audio tones
ledcWriteTone(PWM_PIN, 2000);
delay(500);
ledcWriteTone(PWM_PIN, 0); // Silence the tone
}
Debugging: When the PWM Output Fails
If your logic analyzer shows a flatline on GPIO 18, or your serial monitor throws an exception, work through these ranked causes. These are the first three things to check when an ESP32-S3 PWM build fails.
1. The Exact Error String: Channel Argument Invalid
If you see this exact string in your serial monitor:
E (142) ledc: ledc_channel_config(431): channel argument is invalid
Cause: You are mixing legacy ESP32 Arduino Core v2.x syntax (like ledcSetup(channel, freq, res)) with v3.x auto-allocation, or you have manually specified a channel index greater than 7. The ESP32-S3 only has 8 LEDC channels (0 through 7).
Fix: Remove all ledcSetup and ledcAttachPin calls. Replace them entirely with ledcAttach(PIN, FREQ, RES) as shown in the code block above.
2. Silent Failure: Code Compiles but GPIO Stays Low
Cause: You routed the PWM to a strapping pin or an internal SPI pin. On the S3, GPIO 19, 20, 22-32 are often routed to internal Octal SPI flash/PSRAM on N8R8 boards. GPIO 0, 3, 45, and 46 are boot strapping pins.
Fix: Move your physical wire to GPIO 18, 17, 16, or 15. Refer to Table 1 for the verified safe list. Use a multimeter in continuity mode to verify the physical trace from the header pin to the silicon pad if using a custom PCB.
3. Visual/Audio Glitching: Flickering LEDs or Whining Motors
Cause: Frequency and resolution mismatch. If you request a 20-bit resolution (1,048,576 steps) at 20,000 Hz, the 80 MHz APB clock cannot mathematically support it. The hardware silently truncates the resolution or drops pulses.
Fix: Apply the rule of thumb: $Frequency \times 2^{Resolution} \le 80,000,000$. For 5 kHz, your absolute maximum resolution is 14-bit. For 20 kHz, drop to 11-bit.
Extending and Simplifying the Build
Depending on your project timeline and end-goal, you can either strip this build down to its bare essentials or scale it up for industrial motor control.
How to Simplify: The analogWrite() Wrapper
If you are porting legacy Arduino Uno code and do not need explicit control over the PWM frequency, you can delete the LEDC configuration entirely. In ESP32 Core v3.x, calling analogWrite(PWM_PIN, 128) automatically invokes the LEDC peripheral under the hood at a default 5 kHz frequency and 8-bit resolution. This sacrifices fine-grained timing control but cuts setup code to zero.
How to Extend: Migrating to MCPWM for Motor Control
The LEDC peripheral is excellent for LEDs, servos, and simple heaters. It is not suitable for driving H-bridges or half-bridge motor drivers. If your project requires dead-time insertion (preventing shoot-through currents in MOSFET bridges) or hardware fault-tripping (shutting down PWM in microseconds if an overcurrent pin goes high), you must abandon LEDC and use the MCPWM (Motor Control PWM) peripheral.
Migrating to MCPWM requires using the ESP-IDF API directly (mcpwm_new_timer, mcpwm_new_operator), as the Arduino wrapper for MCPWM remains incomplete. For authoritative register-level details on the S3's MCPWM peripheral, consult the Espressif ESP32-S3 Technical Reference Manual. For standard LEDC API updates and edge cases, the official Arduino ESP32 Core LEDC documentation remains the definitive source.






