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.

Table 1: ESP32-S3 LEDC Frequency vs. Resolution Limits & Safe GPIO Matrix
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
Bench Note: While the hardware supports up to 20-bit resolution, pushing beyond 14 bits drops your maximum frequency below 5 kHz, which causes audible whine in motor drivers and visible flicker in high-end LED strips. Stick to 10-bit or 12-bit for 95% of hobbyist and industrial prototyping tasks.

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.

Final Verification Step: Before connecting any high-current load, always probe the ESP32-S3 GPIO with an oscilloscope or a logic analyzer. Verify the baseline voltage is exactly 0V when duty is 0, and peaks at 3.3V (not 3.1V, which indicates a weak internal pull-up conflict). Confirming the clean 3.3V square wave on the bench saves hours of troubleshooting MOSFET gate drive issues later.