Create a Breathing LED with ESP32 | Smooth Fade Effect Tutorial


In IoT device design, the gradual dimming and brightening of an LED (Breathing Light effect) is not only common but also adds a sense of life to the device. Whether it’s for standby indication, sleep mode signals, or ambient lighting design, the breathing light is a practical and aesthetically pleasing feature.

I will guide you step by step on how to use ESP32 with LEDC and PWM to achieve the breathing light effect, and demonstrate how to control the LED’s gradual dimming and brightening to create a smooth and stable breathing effect visually, using the ESP-IDF development environment.

Breathing

What is “Breathing Light”?

A Breathing Light is a type of LED light where its brightness slowly changes over time, creating a visual effect that looks like “breathing.” Unlike typical blinking lights, which are abrupt, breathing lights gradually brighten → dim → repeat, producing a smooth, rhythmic visual experience.

This lighting effect is commonly used for:

  • Device standby status indicators (e.g., laptop power lights)
  • Sleep aid lights, night lights
  • Smart home ambient lighting
  • Visual UI feedback effects (UX)

Breathing lights not only make devices appear more lively, but they also provide a gentler visual cue for users.

PWM and Breathing Light Principles

The core principle of a breathing light is to control the LED’s brightness by using PWM (Pulse Width Modulation) technology.

PWM is a method of making a digital pin “appear” as if it’s outputting analog, by controlling the proportion of high voltage time in a cycle (called duty cycle). When the duty cycle gradually changes between 0% and 100%, the LED’s brightness changes correspondingly, creating the breathing light effect.

The ESP32’s LEDC (LED Control) module supports high-resolution PWM output, with up to 16 channels, which allows simultaneous control of multiple LEDs or motors.

Development Environment

Before starting your programming, make sure to complete the following preparations:

Program

Create an ESP-IDF project and place the following code in main/main.c:

/**
 * ESP32 Breathing LED Example using ESP-IDF
 * This code demonstrates a smooth breathing effect on an LED using PWM hardware fading
 */

#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/ledc.h"        // LED Control (PWM) driver
#include "esp_err.h"           // ESP32 error codes

/* Hardware Configuration */
#define LED_GPIO        GPIO_NUM_2     // GPIO pin connected to LED (adjust according to your setup)
#define LEDC_CHANNEL    LEDC_CHANNEL_0 // PWM channel (ESP32 has 0-7 high-speed channels)
#define LEDC_TIMER      LEDC_TIMER_0   // PWM timer (ESP32 has 0-3 timers)
#define LEDC_MODE       LEDC_HIGH_SPEED_MODE  // PWM operating mode
#define LEDC_FREQ_HZ    5000           // PWM frequency in Hz (5kHz works well for LEDs)
#define LEDC_RES        LEDC_TIMER_13_BIT  // PWM resolution (13-bit = 0-8191 duty values)
#define BREATHE_TIME_MS 3000           // Total time for one complete breathe cycle (in milliseconds)

/**
 * @brief Main application entry point
 */
void app_main(void)
{
    /* Step 1: Configure PWM Timer */
    ledc_timer_config_t ledc_timer = {
        .speed_mode = LEDC_MODE,       // PWM mode (high-speed or low-speed)
        .timer_num = LEDC_TIMER,       // Timer number (0-3)
        .duty_resolution = LEDC_RES,   // PWM resolution (bit depth)
        .freq_hz = LEDC_FREQ_HZ,      // PWM frequency
        .clk_cfg = LEDC_AUTO_CLK       // Automatic clock source selection
    };
    // Apply timer configuration - this sets up the base PWM signal characteristics
    ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));

    /* Step 2: Configure PWM Channel */
    ledc_channel_config_t ledc_channel = {
        .gpio_num = LED_GPIO,          // GPIO pin connected to LED
        .speed_mode = LEDC_MODE,       // Must match timer mode
        .channel = LEDC_CHANNEL,       // PWM channel (0-7)
        .intr_type = LEDC_INTR_DISABLE,// Disable interrupts (not needed for fading)
        .timer_sel = LEDC_TIMER,       // Timer to be attached to this channel
        .duty = 0,                     // Initial duty cycle (0 = LED off)
        .hpoint = 0                    // Phase point (0 for standard PWM)
    };
    // Apply channel configuration - links the timer to a specific GPIO
    ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));

    /* Step 3: Initialize Hardware Fading Functionality */
    // This enables the ESP32's built-in PWM fading hardware acceleration
    // Parameter is interrupt flags (0 means no special interrupt handling)
    ESP_ERROR_CHECK(ledc_fade_func_install(0));

    /* Main loop to create breathing effect */
    while (1) {
        /* Fade IN (from 0% to 100% brightness) */
        // Configure fade: target duty = max (8191 for 13-bit), time = half of total cycle
        ESP_ERROR_CHECK(ledc_set_fade_with_time(LEDC_MODE, LEDC_CHANNEL, 
                            8191, BREATHE_TIME_MS/2));
        // Start fade (LEDC_FADE_NO_WAIT means don't block during fade)
        ESP_ERROR_CHECK(ledc_fade_start(LEDC_MODE, LEDC_CHANNEL, LEDC_FADE_NO_WAIT));
        // Delay for fade duration (while hardware handles the actual fading)
        vTaskDelay(pdMS_TO_TICKS(BREATHE_TIME_MS/2));

        /* Fade OUT (from 100% back to 0% brightness) */
        ESP_ERROR_CHECK(ledc_set_fade_with_time(LEDC_MODE, LEDC_CHANNEL, 
                            0, BREATHE_TIME_MS/2));
        ESP_ERROR_CHECK(ledc_fade_start(LEDC_MODE, LEDC_CHANNEL, LEDC_FADE_NO_WAIT));
        vTaskDelay(pdMS_TO_TICKS(BREATHE_TIME_MS/2));

        /* Note: The actual fading happens in hardware while the CPU is free to do other tasks */
    }
}

Effect Control Techniques

Fade IN (Brightening):

  • ledc_set_fade_with_time(): This function sets the target duty cycle and the time. Here, the target duty cycle is 8191, which corresponds to 100% brightness (13-bit resolution, range is 0–8191). BREATHE_TIME_MS / 2 sets the fade time to half of the total cycle.
  • ledc_fade_start(): Starts the fade effect, and LEDC_FADE_NO_WAIT ensures it doesn’t block program execution. The hardware handles the fade, and the program continues to execute.
  • vTaskDelay(): This delay ensures the program waits for the fade to complete, preventing it from moving to the next step prematurely.

Fade OUT (Dimming):

  • Similar to the fade-in process, the target duty cycle is set to 0, indicating the LED should fade out from 100% to 0% brightness. The fade time is the same as for fade-in.
  • ledc_fade_start(): Starts the fade, and LEDC_FADE_NO_WAIT is used again.
  • vTaskDelay(): This delay ensures synchronization of the fade-out time.

Key Concepts:

  • Breathing Time (BREATHE_TIME_MS): This variable controls the entire breathing light cycle, including the transition from dim to bright and vice versa. Adjust this variable to change the speed of the breathing effect.
  • ledc_set_fade_with_time(): This function ensures smooth transitions by adjusting the duty cycle over a specified time, providing the breathing effect.
  • LEDC_FADE_NO_WAIT: This means that the function ledc_fade_start() will not wait for the fade to complete. It allows the hardware to manage the fade process, while the program can continue running other logic.
  • vTaskDelay(): This delay ensures proper time synchronization during the fading process and prevents moving on to the next cycle too early.

Compiling and Flashing

Once the code is complete, use the ESP-IDF commands to compile, flash, and monitor the output.

Conclusion

Breathing lights may seem simple, but they involve multiple key concepts like PWM control, timing delays, and smooth output modulation. This makes it a very practical example for beginners. Once you’re comfortable with these basics, you can apply the knowledge to control motors, create light effects, or design UI feedback in various projects.

In the next tutorial, I’ll show you how to create an RGB breathing light or dynamic light effects based on sound or sensor data. Stay tuned!