Skip to content

重要事项!

www.sovereignEngine.cn

门主引擎

一、PWM 乐鑫ESP-IDF开发技术文档

LED PWM 控制器 - ESP32 - — ESP-IDF 编程指南 latest 文档

https://docs.espressif.com/projects/esp-idf/zh_CN/latest/esp32/api-reference/peripherals/ledc.html#led-pwm

脉宽调制(Pulse-Width Modulation,PWM)是利用微处理器的数字输出,来对模拟电路进行控制的一种非常有效的技术,通过对一系列脉冲的宽度进行调制,来等效的获得所需要的波形(含形状和幅值),即通过改变导通时间占总时间的比例,也就是占空比,达到调整电压和频率的目的。

二、PWM是什么?(LEDC,控制电机的是MCPWM)

画板

LED 控制器 (LEDC) 主要用于控制 LED,也可产生 PWM 信号用于其他设备的控制。该控制器有 8 路通道,可以产生独立的波形,驱动 RGB LED 等设备。

LEDC 通道共有两组,分别为 8 路高速通道和 8 路低速通道。高速通道模式在硬件中实现,可以自动且无干扰地改变 PWM 占空比。低速通道模式下,PWM 占空比需要由软件中的驱动器改变。每组通道都可以使用不同的时钟源。

LED PWM 控制器可在无需 CPU 干预的情况下自动改变占空比,实现亮度和颜色渐变。

三、如何配置ESP-IDF PWM?

LED PWM 控制器 API 的关键配置 API参考

LED PWM 控制器 技术规格书Page 55

3.1 LED PWM配置流程(LEDC)

      1. 速度模式 ledc_mode_t
      2. ESP32 LEDC 时钟源特性

3.2 备注

首次 LEDC 配置时,建议先配置定时器(调用函数 ledc_timer_config()),再配置通道(调用函数 ledc_channel_config())。这样可以确保 IO 脚上的 PWM 信号自有输出开始其频率就是正确的。确保配置顺序遵循ledc_timer_config()ledc_channel_config() → (可选)设置占空比/渐变

4.0 三个PWM(LEDC)项目对比

4.1 怎么做简单的PWM亮度渐变(硬件控制)

INFO

核心特点:

流程:

按键按住 → 渐亮1000ms(阻塞等待)→ 渐灭1000ms(阻塞等待)→ 循环

按键松开 → 直接设占空比为0,熄灭LED

把时钟源想象成一根水管,水流速度固定:

  1. 时钟源(f_时钟源):水管的原始水流速度,例如 APB_CLK = 80 MHz,就是每秒 8000 万个脉冲。这是"原材料"。
  2. 分频系数(LEDC_CLK_DIV):相当于给水管加了一个节流阀,把水流速度降低。比如分频系数为 5000,水流速度就变成 16000Hz。
  3. 分辨率(2^N):计数器从 0 数到 2^N - 1 才算完成一个 PWM 周期。N 越大,数的数越多,PWM 频率就越低,但占空比的调节精度越高。

image-3

  1. ****
  2. ****
  3. ****

假设你想要:1 kHz 的 PWM,50% 占空比,使用 APB_CLK (80 MHz):

  1. ****

这就是为什么代码里写 对应 50% 占空比的原因。

总结一句话:时钟源是原料,分辨率决定精细度,频率决定快慢,三者由公式绑定,鱼和熊掌不可兼得——频率越高,精度越低。

c
#include <stdio.h>
#include "freertos/FreeRTOS.h"   // FreeRTOS 实时操作系统核心头文件
#include "freertos/task.h"       // FreeRTOS 任务管理(xTaskCreate、vTaskDelay 等)
#include "driver/gpio.h"         // GPIO 驱动,用于配置和读取按键引脚
#include "driver/ledc.h"         // LEDC(LED PWM 控制器)驱动
#include "esp_err.h"             // ESP-IDF 错误码定义(ESP_OK 等)
#include "esp_log.h"             // 日志打印(ESP_LOGI、ESP_LOGD 等)

// ===== 宏定义:硬件引脚和 LEDC 参数配置 =====
#define BUTTON_GPIO         20          // 按键连接的 GPIO 引脚号
#define LED_GPIO            2           // LED 连接的 GPIO 引脚号
#define LEDC_TIMER          LEDC_TIMER_0        // 使用 LEDC 定时器 0
#define LEDC_MODE           LEDC_LOW_SPEED_MODE // 使用低速模式(ESP32 支持高速/低速两种模式)
#define LEDC_CHANNEL        LEDC_CHANNEL_0      // 使用 LEDC 通道 0
#define LEDC_DUTY_RES       LEDC_TIMER_13_BIT   // 占空比分辨率为 13 位,范围 0~8191
#define LEDC_FREQUENCY      4000                // PWM 信号频率为 4000 Hz

static const char *TAG = "PWM_Breath"; // 日志标签,打印日志时会显示此前缀

// ===== LEDC 初始化函数 =====
static void ledc_init(void)
{
    // 第一步:配置定时器
    // 定时器决定 PWM 信号的频率和占空比分辨率
    ledc_timer_config_t ledc_timer = {
        .speed_mode       = LEDC_MODE,      // 速度模式:低速
        .timer_num        = LEDC_TIMER,     // 定时器编号:0
        .duty_resolution  = LEDC_DUTY_RES,  // 分辨率:13 位(占空比范围 0~8191)
        .freq_hz          = LEDC_FREQUENCY, // PWM 频率:4000 Hz
        .clk_cfg          = LEDC_AUTO_CLK,  // 自动选择时钟源
    };
    ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer)); // 应用定时器配置,出错则终止程序

    // 第二步:配置通道
    // 通道将定时器产生的 PWM 信号绑定到具体的 GPIO 引脚
    ledc_channel_config_t ledc_channel = {
        .speed_mode     = LEDC_MODE,      // 速度模式:低速
        .channel        = LEDC_CHANNEL,   // 通道编号:0
        .timer_sel      = LEDC_TIMER,     // 绑定到定时器 0
        .intr_type      = LEDC_INTR_DISABLE, // 不使用渐变完成中断
        .gpio_num       = LED_GPIO,       // 输出到 GPIO2(LED 引脚)
        .duty           = 0,              // 初始占空比为 0(LED 初始状态为熄灭)
        .hpoint         = 0,              // 高电平起始点为 0(不做相位偏移)
    };
    ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel)); // 应用通道配置,出错则终止程序
}

// ===== 呼吸灯任务函数 =====
static void breathing_led_task(void *arg)
{
    // 安装 LEDC 渐变功能(硬件渐变需要先调用此函数,它会占用 LEDC 模块的中断)
    ESP_ERROR_CHECK(ledc_fade_func_install(0));

    for (;;) {  // 无限循环,持续检测按键状态
        
        if (gpio_get_level(BUTTON_PIN) == 0) {	// 检测到低电平(也就是按键被按下)时往下执行
            vTaskDelay(pdMS_TO_TICKS(30));	// 按键消抖
            
            if (gpio_get_level(BUTTON_PIN) == 0) {	// 确认是按下,非误触

                // 按键按下时,GPIO 电平为低(因为配置了上拉电阻,按下接地变为低电平)
                ESP_LOGD(TAG, "Button pressed, start breathing");
                
                // 渐亮:阻塞等待完成,从当前占空比渐变到 8191(最亮),耗时 1000ms
                ledc_set_fade_time_and_start(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0,
                                             8191, 1000, LEDC_FADE_WAIT_DONE);

                // 渐灭:非阻塞,立即返回,从 8191 渐变到 0(熄灭),耗时 1000ms
                ledc_set_fade_time_and_start(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0,
                                             0, 1000, LEDC_FADE_NO_WAIT);
            }
        }
        vTaskDelay(pdMS_TO_TICKS(10));	// 延时 50ms,避免 CPU 空转
    }
}

// ===== 程序入口 =====
void app_main(void)
{
    // 配置按键 GPIO
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << BUTTON_GPIO), // 选择 GPIO20
        .mode         = GPIO_MODE_INPUT,        // 设置为输入模式
        .pull_up_en   = GPIO_PULLUP_ENABLE,     // 使能内部上拉电阻(未按下时为高电平)
        .pull_down_en = GPIO_PULLDOWN_DISABLE,  // 不使用下拉
        .intr_type    = GPIO_INTR_DISABLE,      // 不使用 GPIO 中断
    };
    gpio_config(&io_conf); // 应用 GPIO 配置

    ledc_init(); // 初始化 LEDC 定时器和通道

    ESP_LOGI(TAG, "Program started. Press button on GPIO %d for breathing LED.", BUTTON_GPIO);
    // 打印启动日志,提示用户按下按键

    // 创建呼吸灯任务
    // 参数依次为:任务函数、任务名称、栈大小(字节)、参数、优先级、任务句柄
    xTaskCreate(breathing_led_task, "breathing_led", 4096, NULL, 5, NULL);
}

4.2 如何实现ESP-IDF框架下PWM结合GPIO上拉输入的短按切换模式(呼吸灯与闪烁)?

INFO

核心特点:

流程:

**button_task:检测下降沿 → led_mode = (led_mode % 2) + 1(在1和2之间切换)
**led_task:
**** mode=1 → 非阻塞渐亮 → 每10ms检查模式 → 非阻塞渐灭 → 循环
**** mode=2 → 亮100ms → 灭100ms → 循环
**** mode=0 → 熄灭,等待50ms

c
#include <stdio.h>
#include "freertos/FreeRTOS.h"   // FreeRTOS 实时操作系统核心头文件
#include "freertos/task.h"       // FreeRTOS 任务管理(创建任务、延时等)
#include "driver/gpio.h"         // GPIO 驱动(配置引脚、读取电平)
#include "driver/ledc.h"         // LEDC 驱动(LED PWM 控制器)
#include "esp_err.h"             // ESP-IDF 错误码处理
#include "esp_log.h"             // ESP-IDF 日志打印

// ===== 宏定义:硬件引脚和 LEDC 参数 =====
#define BUTTON_GPIO         20           // 按键连接的 GPIO 引脚编号
#define LED_GPIO            2            // LED 连接的 GPIO 引脚编号
#define LEDC_TIMER          LEDC_TIMER_0 // 使用 LEDC 定时器 0(共 4 个可选)
#define LEDC_MODE           LEDC_LOW_SPEED_MODE // 低速模式(大多数 ESP32 芯片支持)
#define LEDC_CHANNEL        LEDC_CHANNEL_0      // 使用 LEDC 通道 0
#define LEDC_DUTY_RES       LEDC_TIMER_13_BIT   // PWM 分辨率:13 位(占空比范围 0~8191)
#define LEDC_FREQUENCY      4000                // PWM 频率:4000 Hz

static const char *TAG = "PWM_Breath"; // 日志标签,打印日志时会显示此名称

// 全局模式变量(volatile 防止编译器优化,保证多任务访问时读到最新值)
// 0 = 关闭,1 = 呼吸灯,2 = 闪烁
static volatile int led_mode = 0;

// ===== LEDC 初始化函数 =====
static void ledc_init(void)
{
    // 第一步:配置定时器,决定 PWM 的频率和分辨率
    ledc_timer_config_t ledc_timer = {
        .speed_mode       = LEDC_MODE,       // 低速模式
        .timer_num        = LEDC_TIMER,      // 使用定时器 0
        .duty_resolution  = LEDC_DUTY_RES,   // 13 位分辨率
        .freq_hz          = LEDC_FREQUENCY,  // 频率 4000 Hz
        .clk_cfg          = LEDC_AUTO_CLK,   // 自动选择时钟源
    };
    ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer)); // 应用配置,出错则打印并停止

    // 第二步:配置通道,将 PWM 信号绑定到具体 GPIO 引脚
    ledc_channel_config_t ledc_channel = {
        .speed_mode     = LEDC_MODE,         // 低速模式
        .channel        = LEDC_CHANNEL,      // 通道 0
        .timer_sel      = LEDC_TIMER,        // 绑定定时器 0
        .intr_type      = LEDC_INTR_DISABLE, // 不使用中断
        .gpio_num       = LED_GPIO,          // 输出到 LED 引脚(GPIO2)
        .duty           = 0,                 // 初始占空比为 0(LED 熄灭)
        .hpoint         = 0,                 // 高电平起始点为 0(无相位偏移)
    };
    ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel)); // 应用通道配置
}

// ===== 按键检测任务 =====
// 功能:检测按键下降沿(按下),短按切换模式(1→2→1 循环)
static void button_task(void *arg)
{
    int last_level = 1; // 上一次按键电平(1=未按下,因为启用了上拉电阻)

    while (1) {
        int level = gpio_get_level(BUTTON_GPIO); // 读取当前按键电平

        // 检测下降沿:上次高电平,现在低电平 → 按键刚被按下
        if (last_level == 1 && level == 0) {
            vTaskDelay(pdMS_TO_TICKS(20)); // 延时 20ms 去抖动(消除机械抖动干扰)
            if (gpio_get_level(BUTTON_GPIO) == 0) { // 再次确认确实是按下状态
                // 在模式 1 和 2 之间切换:1→2→1→...
                led_mode = (led_mode % 2) + 1;
                ESP_LOGI(TAG, "Button pressed, mode -> %d", led_mode);
            }
        }

        last_level = level;             // 更新上一次电平状态
        vTaskDelay(pdMS_TO_TICKS(10));  // 每 10ms 检测一次,释放 CPU 给其他任务
    }
}

// ===== LED 控制任务 =====
static void led_task(void *arg)
{
    // 安装 LEDC 渐变功能(呼吸灯效果必须先调用此函数)
    ESP_ERROR_CHECK(ledc_fade_func_install(0));

    int current_mode = 0; // 记录当前正在执行的模式,用于检测模式切换

    while (1) {
        int mode = led_mode; // 读取全局模式变量

        if (mode == 1) {
            // ===== 呼吸灯模式 =====
            if (current_mode != 1) {
                current_mode = 1;
                ESP_LOGI(TAG, "Breathing mode"); // 首次进入时打印日志
            }

            // 渐亮:1000ms 内将占空比渐变到 8191(最亮)
            ledc_set_fade_with_time(LEDC_MODE, LEDC_CHANNEL, 8191, 1000);
            ledc_fade_start(LEDC_MODE, LEDC_CHANNEL, LEDC_FADE_NO_WAIT); // 非阻塞启动渐变

            // 等待 1000ms,每 10ms 检查一次模式是否被切换
            for (int i = 0; i < 100; i++) {
                vTaskDelay(pdMS_TO_TICKS(10));
                if (led_mode != 1) goto mode_changed; // 模式变了,跳转到清理逻辑
            }

            // 渐灭:1000ms 内将占空比渐变到 0(最暗)
            ledc_set_fade_with_time(LEDC_MODE, LEDC_CHANNEL, 0, 1000);
            ledc_fade_start(LEDC_MODE, LEDC_CHANNEL, LEDC_FADE_NO_WAIT);

            for (int i = 0; i < 100; i++) {
                vTaskDelay(pdMS_TO_TICKS(10));
                if (led_mode != 1) goto mode_changed;
            }

        } else if (mode == 2) {
            // ===== 闪烁模式 =====
            if (current_mode != 2) {
                current_mode = 2;
                ESP_LOGI(TAG, "Blink mode");
            }

            // 点亮 LED:占空比设为 8191(最大亮度),调用 update 立即生效
            ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, 8191);
            ledc_update_duty(LEDC_MODE, LEDC_CHANNEL); // 注意:set_duty 后必须调用 update_duty 才能生效
            vTaskDelay(pdMS_TO_TICKS(100)); // 亮 100ms
            if (led_mode != 2) goto mode_changed; // 检查模式是否切换

            // 熄灭 LED:占空比设为 0
            ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, 0);
            ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
            vTaskDelay(pdMS_TO_TICKS(100)); // 灭 100ms

        } else {
            // ===== 关闭模式(mode == 0)=====
            ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, 0); // 占空比设为 0
            ledc_update_duty(LEDC_MODE, LEDC_CHANNEL); // 立即更新
            vTaskDelay(pdMS_TO_TICKS(50));              // 等待 50ms 再检查
        }
        continue; // 继续下一次循环

mode_changed:
        // 模式切换时的清理:先关闭 LED,再重置当前模式标记
        ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, 0);
        ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
        current_mode = 0; // 重置,下次进入新模式时会重新打印日志
    }
}

// ===== 程序入口函数 =====
void app_main(void)
{
    // 配置按键 GPIO
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << BUTTON_GPIO), // 选择 GPIO20 作为按键引脚
        .mode         = GPIO_MODE_INPUT,        // 设置为输入模式
        .pull_up_en   = GPIO_PULLUP_ENABLE,     // 启用内部上拉(未按下时为高电平)
        .pull_down_en = GPIO_PULLDOWN_DISABLE,  // 不启用下拉
        .intr_type    = GPIO_INTR_DISABLE,      // 不使用中断(用轮询方式检测按键)
    };
    gpio_config(&io_conf); // 应用 GPIO 配置

    ledc_init(); // 初始化 LEDC(PWM 控制器)

    // 打印启动日志
    ESP_LOGI(TAG, "Program started. Press button on GPIO %d to switch mode.", BUTTON_GPIO);

    // 创建按键检测任务(优先级 6,高于 led_task,确保按键响应及时)
    xTaskCreate(button_task, "button_task", 2048, NULL, 6, NULL);

        // 创建 LED 控制任务(优先级 5,栈大小 4096 字节,因呼吸灯逻辑较复杂)
    xTaskCreate(led_task, "led_task", 4096, NULL, 5, NULL);

    // app_main 函数返回后,主任务会被自动删除
    // 但 button_task 和 led_task 会继续在后台运行
}

4.3 如何实现ESP-IDF框架下PWM结合GPIO上拉输入的长短按模式(状态控制开关、呼吸与闪烁)?

INFO

核心特点:

流程:

button_task:

** 检测下降沿 → 记录 press_start 时刻**

** 检测上升沿 → 计算 held_time**

** held_time >= 1000ms → led_mode = 0(关闭)**

** held_time < 1000ms → led_mode 在1和2之间切换**

led_task:与代码二完全相同

c
#include <stdio.h>
#include "freertos/FreeRTOS.h"   // FreeRTOS 实时操作系统核心头文件
#include "freertos/task.h"       // FreeRTOS 任务管理(创建任务、延时等)
#include "driver/gpio.h"         // GPIO 驱动(配置引脚输入输出、读取电平)
#include "driver/ledc.h"         // LEDC 驱动(LED PWM 控制器,用于生成 PWM 信号)
#include "esp_err.h"             // ESP-IDF 错误码处理
#include "esp_log.h"             // ESP-IDF 日志打印

// ===== 宏定义:硬件引脚和 LEDC 参数 =====
#define BUTTON_GPIO         20           // 按键连接的 GPIO 引脚编号
#define LED_GPIO            2            // LED 连接的 GPIO 引脚编号
#define LEDC_TIMER          LEDC_TIMER_0 // 使用 LEDC 定时器 0
#define LEDC_MODE           LEDC_LOW_SPEED_MODE // 使用低速模式(大多数 ESP32 芯片支持)
#define LEDC_CHANNEL        LEDC_CHANNEL_0      // 使用 LEDC 通道 0
#define LEDC_DUTY_RES       LEDC_TIMER_13_BIT   // PWM 分辨率:13 位(占空比范围 0~8191)
#define LEDC_FREQUENCY      4000                // PWM 频率:4000 Hz

static const char *TAG = "PWM_Breath"; // 日志标签,打印日志时会显示此名称

// 全局变量:当前 LED 模式(volatile 保证多任务访问时不被编译器优化掉)
// 0 = 关闭,1 = 呼吸灯,2 = 闪烁
static volatile int led_mode = 0;

// ===== LEDC 初始化函数 =====
static void ledc_init(void)
{
    // 配置 LEDC 定时器:决定 PWM 的频率和分辨率
    ledc_timer_config_t ledc_timer = {
        .speed_mode       = LEDC_MODE,       // 低速模式
        .timer_num        = LEDC_TIMER,      // 使用定时器 0
        .duty_resolution  = LEDC_DUTY_RES,   // 13 位分辨率
        .freq_hz          = LEDC_FREQUENCY,  // 频率 4000 Hz
        .clk_cfg          = LEDC_AUTO_CLK,   // 自动选择时钟源
    };
    ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer)); // 应用定时器配置,出错则打印并停止

    // 配置 LEDC 通道:将 PWM 信号绑定到具体的 GPIO 引脚
    ledc_channel_config_t ledc_channel = {
        .speed_mode     = LEDC_MODE,         // 低速模式
        .channel        = LEDC_CHANNEL,      // 通道 0
        .timer_sel      = LEDC_TIMER,        // 绑定定时器 0
        .intr_type      = LEDC_INTR_DISABLE, // 不使用中断
        .gpio_num       = LED_GPIO,          // 输出到 LED 引脚(GPIO2)
        .duty           = 0,                 // 初始占空比为 0(LED 熄灭)
        .hpoint         = 0,                 // 高电平起始点为 0(无相位偏移)
    };
    ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel)); // 应用通道配置
}

// ===== 按键检测任务 =====
// 功能:短按切换模式(1→2→1 循环),长按超过 1 秒松开后关闭 LED
static void button_task(void *arg)
{
    int last_level = 1;       // 上一次按键电平(1=未按下,因为启用了上拉电阻)
    TickType_t press_start = 0; // 记录按键按下的时刻(FreeRTOS 系统 tick)

    while (1) {
        int level = gpio_get_level(BUTTON_GPIO); // 读取当前按键电平

        // 检测下降沿:上次是高电平,现在是低电平 → 按键刚被按下
        if (last_level == 1 && level == 0) {
            vTaskDelay(pdMS_TO_TICKS(20)); // 延时 20ms 去抖动(消除机械抖动干扰)
            if (gpio_get_level(BUTTON_GPIO) == 0) { // 再次确认确实是按下状态
                press_start = xTaskGetTickCount(); // 记录按下时刻(单位:tick)
                ESP_LOGD(TAG, "Button pressed");   // 调试日志
            }
        }

        // 检测上升沿:上次是低电平,现在是高电平 → 按键刚被松开
        if (last_level == 0 && level == 1) {
            vTaskDelay(pdMS_TO_TICKS(20)); // 延时 20ms 去抖动
            if (gpio_get_level(BUTTON_GPIO) == 1) { // 再次确认确实是松开状态
                // 计算按键持续时间(tick 差值 × 每 tick 毫秒数)
                TickType_t held_time = (xTaskGetTickCount() - press_start) * portTICK_PERIOD_MS;
                if (held_time >= 1000) {
                    // 长按(超过 1 秒):关闭 LED
                    led_mode = 0;
                    ESP_LOGI(TAG, "Long press released (>1s), LED off");
                } else {
                    // 短按:在模式 1 和 2 之间切换(1→2→1→...)
                    led_mode = (led_mode % 2) + 1;
                    ESP_LOGI(TAG, "Short press, mode -> %d", led_mode);
                }
            }
        }

        last_level = level;              // 更新上一次电平状态
        vTaskDelay(pdMS_TO_TICKS(10));   // 每 10ms 检测一次按键,释放 CPU 给其他任务
    }
}

// ===== LED 控制任务 =====
static void led_task(void *arg)
{
    ESP_ERROR_CHECK(ledc_fade_func_install(0)); // 安装 LEDC 渐变功能(用于呼吸灯效果)

    int current_mode = 0; // 记录当前正在执行的模式,用于检测模式切换

    while (1) {
        int mode = led_mode; // 读取全局模式变量

        if (mode == 1) {
            // ===== 呼吸灯模式 =====
            if (current_mode != 1) {
                current_mode = 1;
                ESP_LOGI(TAG, "Breathing mode"); // 首次进入时打印日志
            }

            // 渐亮:在 1000ms 内将占空比从当前值渐变到 8191(最亮)
            ledc_set_fade_with_time(LEDC_MODE, LEDC_CHANNEL, 8191, 1000);
            ledc_fade_start(LEDC_MODE, LEDC_CHANNEL, LEDC_FADE_NO_WAIT); // 非阻塞启动渐变

            // 等待 1000ms,每 10ms 检查一次模式是否被切换
            for (int i = 0; i < 100; i++) {
                vTaskDelay(pdMS_TO_TICKS(10));
                if (led_mode != 1) goto mode_changed; // 模式变了,跳转到清理逻辑
            }

            // 渐灭:在 1000ms 内将占空比从当前值渐变到 0(最暗)
            ledc_set_fade_with_time(LEDC_MODE, LEDC_CHANNEL, 0, 1000);
            ledc_fade_start(LEDC_MODE, LEDC_CHANNEL, LEDC_FADE_NO_WAIT);

            for (int i = 0; i < 100; i++) {
                vTaskDelay(pdMS_TO_TICKS(10));
                if (led_mode != 1) goto mode_changed;
            }

        } else if (mode == 2) {
            // ===== 闪烁模式 =====
            if (current_mode != 2) {
                current_mode = 2;
                ESP_LOGI(TAG, "Blink mode");
            }

            // 点亮 LED:设置占空比为 8191(最大亮度)并立即更新
            ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, 8191);
            ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
            vTaskDelay(pdMS_TO_TICKS(100)); // 亮 100ms
            if (led_mode != 2) goto mode_changed; // 检查模式是否切换

            // 熄灭 LED:设置占空比为 0
            ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, 0);
            ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
            vTaskDelay(pdMS_TO_TICKS(100)); // 灭 100ms

        } else {
            // ===== 关闭模式(mode == 0)=====
            ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, 0); // 占空比设为 0
            ledc_update_duty(LEDC_MODE, LEDC_CHANNEL); // 立即更新
            vTaskDelay(pdMS_TO_TICKS(50));              // 等待 50ms 再检查
        }
        continue; // 继续下一次循环

mode_changed:
        // 模式切换时的清理:先关闭 LED,再重置当前模式标记
        ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, 0);
        ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
        current_mode = 0; // 重置,下次进入
    }
}

// ===== 程序入口函数 =====
void app_main(void)
{
    // 配置按键 GPIO
    gpio_config_t io_conf = {
        .pin_bit_mask = (1ULL << BUTTON_GPIO), // 选择 GPIO20 作为按键引脚
        .mode         = GPIO_MODE_INPUT,        // 设置为输入模式
        .pull_up_en   = GPIO_PULLUP_ENABLE,     // 启用内部上拉电阻(未按下时为高电平)
        .pull_down_en = GPIO_PULLDOWN_DISABLE,  // 不启用下拉
        .intr_type    = GPIO_INTR_DISABLE,      // 不使用中断(用轮询方式检测按键)
    };
    gpio_config(&io_conf); // 应用 GPIO 配置

    ledc_init(); // 初始化 LEDC(PWM 控制器)

    ESP_LOGI(TAG, "Program started. Press button on GPIO %d to switch mode.", BUTTON_GPIO);
    // 打印启动日志,提示用户按下按键切换模式

    // 创建按键检测任务
    // 参数依次:任务函数、任务名称、栈大小(字节)、参数、优先级、任务句柄
    xTaskCreate(button_task, "button_task", 2048, NULL, 6, NULL);
    // 优先级 6,高于 led_task,确保按键响应及时

    // 创建 LED 控制任务
    xTaskCreate(led_task, "led_task", 4096, NULL, 5, NULL);
    // 优先级 5,栈稍大(4096字节),因为呼吸灯逻辑更复杂
}

觉醒,然后燎原。 © 2026 门主引擎