精通 ESP32 呼吸燈 Breathing 設計|漸亮漸暗的 LEDC PWM 效果教學


在物聯網裝置設計中,漸亮漸暗的 LED 呼吸燈(Breathing Light)效果不僅常見,還能為裝置增添生命感。無論是待機顯示、睡眠模式提示,還是氛圍燈設計,呼吸燈都是一個實用又美觀的功能。

我將帶你從零開始,深入掌握如何使用 ESP32 與 LEDC 和 PWM 實現呼吸燈效果,並使用 ESP-IDF 開發環境,完整示範如何控制 LED 漸亮漸暗,達到視覺上平滑且穩定的呼吸感。

Breathing

什麼是「呼吸燈」?

呼吸燈(Breathing Light)是一種 LED 亮度隨時間緩慢變化、看起來像「呼吸」一樣的燈光效果。它不像一般閃爍那樣突兀,而是 漸亮→漸暗→重複循環,產生平滑而具有節奏感的視覺體驗。

這種燈光效果經常應用在:

  • 裝置待機狀態顯示(如筆電電源燈)
  • 睡眠輔助燈、夜燈
  • 智慧家居氛圍照明
  • 視覺 UI 回饋效果(UX)

呼吸燈不只能讓裝置看起來更有生命感,也能提供使用者更溫和的視覺提示。

PWM 與呼吸燈原理快速理解

呼吸燈的核心原理,是透過 PWM 技術改變 LED 的亮度。

PWM 是一種讓數位腳位「看起來像類比輸出」的方式,控制的是每個週期中高電位的時間佔比(即佔空比)。當我們讓佔空比在 0%~100% 間平滑變化時,LED 的亮度也會隨之逐漸變化,產生呼吸燈效果。

ESP32 的 LEDC(LED Control)模組支援高解析度的 PWM 輸出,最多支援 16 個通道,能同時控制多顆 LED 或馬達。

開發環境

在開始編程之前,請確保已完成以下準備工作:

使用 ESP-IDF 撰寫呼吸燈控制程式

建立一個 ESP-IDF 專案,並將下列程式碼放入 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 */
    }
}

呼吸效果控制技巧

Fade IN (漸亮)

  • ledc_set_fade_with_time():這個函數用來設定漸變的目標佔空比以及時間。在這裡,目標佔空比是 8191,這對應於 100% 亮度(13 位解析度,範圍是 0~8191)。BREATHE_TIME_MS / 2 表示漸變的時間是總週期的一半。
  • ledc_fade_start():開始執行設定的漸變,LEDC_FADE_NO_WAIT 表示這個操作不會阻塞程式執行,會讓硬體自動處理漸變,並且程式會繼續執行後續代碼。
  • vTaskDelay():這個延遲函數讓程式在漸變時間內進行延遲,保證在漸變過程中不會進入下一步。這裡的延遲時間是 BREATHE_TIME_MS / 2,也就是漸變所需時間的一半。

Fade OUT (漸暗)

  • ledc_set_fade_with_time():與漸亮相似,這裡的目標佔空比設為 0,表示要從 100% 亮度漸變回 0% 亮度,漸變時間同樣是週期的一半。
  • ledc_fade_start():開始漸變,LEDC_FADE_NO_WAIT 參數的使用方式與前面相同。
  • vTaskDelay():這裡的延遲時間同樣是 BREATHE_TIME_MS / 2,保證漸變時間的同步。

關鍵概念

  • 漸變時間BREATHE_TIME_MS):這個變數控制了整個呼吸燈週期的長度,包括從最暗到最亮的過渡,以及從最亮到最暗的過渡。你可以根據需求調整這個變數來改變呼吸燈的速度。
  • ledc_set_fade_with_time():這個函數會根據設定的時間,在指定的時間內完成漸變,讓亮度平滑過渡。BREATHE_TIME_MS / 2 的時間設定確保了漸變的順暢性和效果。
  • LEDC_FADE_NO_WAIT:表示 ledc_fade_start() 不會等待漸變完成,而是直接讓硬體處理漸變。這樣程式不會被阻塞,可以繼續執行其他的邏輯。
  • vTaskDelay():這個延遲確保了程式在等待漸變完成的同時,保持適當的時間同步,不會提前進入下一輪漸變。

編譯和燒錄

完成程式碼後,您可以使用 ESP-IDF 提供的命令進行編譯、燒錄和監控。

結論

呼吸燈看似簡單,其實涵蓋了 PWM 控制、時間延遲、輸出平滑調變 等多個關鍵概念,是非常實用的入門範例。當你能靈活運用這些基礎,未來在馬達控制、燈光特效或 UI 回饋設計中都能派上用場。

下一篇教學我會帶你做出 RGB 呼吸燈 或是 根據聲音或感測器數據動態變化的燈效,敬請期待!