ESP32入门开发·三万字详解LEDC产生PWM信号实现呼吸灯效果(内含源码可直接移植)

C盘清理技巧分享 10w+人浏览 308人参与

目录

1.  简介

2.  系统架构

2.1  定时器

2.1.1  时钟源选择

2.1.2  预分频器配置

2.1.3  计数器

2.2  PWM生成器

3.  功能概述

3.1  定时器配置

3.2  通道配置

3.3  软件改变PWM占空比

3.4  硬件改变PWM占空比

4.  程序设计

4.1  点亮一颗LED灯

4.1.1  头文件包含

4.1.2  GPIO口初始化

4.1.3  LED任务函数

4.1.4  主函数

4.2  PWM调节LED灯亮度

4.2.1  定时器配置

4.2.2  通道配置

4.2.3  软件改变PWM占空比

4.2.4  LED任务函数

4.2.5  主函数

4.3  呼吸灯效果实现

4.3.1  定时器配置

4.3.2  通道配置

4.3.3  硬件改变PWM占空比

4.3.4  回调函数

4.3.5  LED任务函数

4.3.6  主函数


1.  简介

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

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

其主要特性有:

• 八个独立的PWM生成器(即八个通道)
• 四个独立定时器,可实现小数分频
• 占空比自动渐变(即PWM信号占空比可逐渐增加或减小,无须处理器干预),渐变完成时产生中断
• 输出PWM信号相位可调
• 低功耗模式(Light-sleep mode) 下可输出PWM信号
• PWM最大精度为14位

注意:这里我们需要搞清楚一点就是,ESP32-S3 的 GPTimer(通用定时器)和 LEDC(LED PWM 控制器)使用的是不同的定时器硬件,它们不是同一个定时器,而是两个独立的模块。

虽然LEDC是专门为 LED 调光或 PWM 信号生成所设,GPTimer虽然也能完成PWM的功能,但是其主要功能是高精度定时或复杂计时。

想要了解GPTimer的可以跳转:

ESP32入门开发·通用硬件定时器 (GPTimer)-优快云博客

2.  系统架构

        LED PWM 控制器的架构,四个定时器可独立配置(时钟分频器和计数器最大值),每个定时器内部有一个时基计数器(即基于基准时钟周期计数的计数器)。每个PWM生成器会在四个定时器中择一,以该定时器的计数值为基准生成PWM信号,定时器和PWM生成器的主要功能模块:

2.1  定时器

        我们先对图左侧部分进行一个讲解,大概整理了一下:

2.1.1  时钟源选择

        LED PWM控制器的定时器有三个时钟源信号可以选择:APB_CLK、RC_FAST_CLK和XTAL_CLK,为LEDC_CLKx选择时钟源信号的配置如下:

LEDC_APB_CLK_SEL[1:0]

时钟源

说明

1

APB_CLK

默认选择,通常为 80 MHz(ESP32-S3 的 APB 总线时钟)。

2

RTC8M_CLK

即 RC_FAST_CLK(内部 ~20 MHz RC 振荡器),适合低功耗场景。

3

XTAL_CLK

外部晶振时钟(通常 40 MHz),精度高但依赖外部硬件。

2.1.2  预分频器配置

        LEDC_CLKx 信号传输到时钟分频器,产生ref_pulsex信号供计数器使用。ref_pulsex的频率等于LEDC_CLKx 的频率经分频系数LEDC_CLK_DIV分频后的结果,分频系数LEDC_CLK_DIV为小数,因此其值可为非整数。分频系数LEDC_CLK_DIV可根据下列等式配置:

LEDC_CLK_DIV = A + B/256

//整数部分A为LEDC_CLK_DIV_TIMERx字段的高10位(即LEDC_TIMERx_CONF_REG[21:12])
//小数部分B为LEDC_CLK_DIV_TIMERx字段的低8位(即LEDC_TIMERx_CONF_REG[11:4])

        小数部分B为0时,LEDC_CLK_DIV的值为整数(整数分频)。也就是说,每A个LEDC_CLKx时钟周期产生一个ref_pulsex 时钟脉冲。

        小数部分B不为0时,LEDC_CLK_DIV的值非整数。时钟分频器按照A个LEDC_CLKx时钟周期和(A+1)个LEDC_CLKx 时钟周期轮流进行非整数分频。这样一来,ref_pulsex时钟脉冲的平均频率便会是理想值(非整数分频的频率)。每256个ref_pulsex时钟脉冲中:

  • 有B个以(A+1)个LEDC_CLKx时钟周期分频
  • 有(256-B) 个以A个LEDC_CLKx时钟周期分频
  • 以(A+1) 个LEDC_CLKx 时钟周期分频的时钟脉冲均匀分布在以A分频的时钟脉冲中

        官方给的一个图示:

2.1.3  计数器

        每个定时器有一个以ref_pulsex为基准时钟的14位时基计数器。LEDC_TIMERx_DUTY_RES字段用于配置14位计数器的最大值。因此,PWM信号的最大精确度为14位。计数器最大可计数至 2LEDC_TIMERx_DUTY_RES −1,然后溢出重新从0开始计数。软件可以读取、复位、暂停计数器。

2.2  PWM生成器

        而后我们观察右侧图示,这里主要寄存器的配置,原理比较简单:

        要生成PWM信号,PWM生成器(PWMn)需选择一个定时器(Timerx)。每个PWM生成器均可通过置位 LEDC_TIMER_SEL_CHn 单独配置,在四个定时器中选择一个输出PWM信号。每个PWM生成器主要包括一个高低电平比较器和两个选择器。PWM生成器将定时器的14 位计数值(Timerx_cnt) 与高低电平比较器的值Hpointn和Lpointn比较。如果定时器的计数值等于 Hpointn 或 Lpointn,PWM 信号可以输出高低电平:

  • 如果Timerx_cnt == Hpointn,则 sig_outn 为 1。
  • 如果Timerx_cnt == Lpointn,则 sig_outn 为 0。

3.  功能概述

        设置 LEDC 通道分三步完成:

  • 定时器配置:指定 PWM 信号的频率和占空比分辨率。
  • 通道配置:绑定定时器和输出 PWM 信号的 GPIO。
  • 改变 PWM 信号:输出 PWM 信号来驱动 LED。可通过软件控制或使用硬件渐变功能来改变 LED 的亮度。

注意,与 ESP32 不同,ESP32-S3 仅支持设置通道为低速模式。

3.1  定时器配置

        要设置定时器,可调用函数 ledc_timer_config(),并将包括如下配置参数的数据结构 ledc_timer_config_t 传递给该函数:

/**
 * @brief Configuration parameters of LEDC timer for ledc_timer_config function
 */
typedef struct {
    ledc_mode_t speed_mode;                /*!< LEDC speed speed_mode, high-speed mode (only exists on esp32) or low-speed mode */
    ledc_timer_bit_t duty_resolution;      /*!< LEDC channel duty resolution */
    ledc_timer_t  timer_num;               /*!< The timer source of channel (0 - LEDC_TIMER_MAX-1) */
    uint32_t freq_hz;                      /*!< LEDC timer frequency (Hz) */
    ledc_clk_cfg_t clk_cfg;                /*!< Configure LEDC source clock from ledc_clk_cfg_t.
                                                Note that LEDC_USE_RC_FAST_CLK and LEDC_USE_XTAL_CLK are
                                                non-timer-specific clock sources. You can not have one LEDC timer uses
                                                RC_FAST_CLK as the clock source and have another LEDC timer uses XTAL_CLK
                                                as its clock source. All chips except esp32 and esp32s2 do not have
                                                timer-specific clock sources, which means clock source for all timers
                                                must be the same one. */
    bool deconfigure;                      /*!< Set this field to de-configure a LEDC timer which has been configured before
                                                Note that it will not check whether the timer wants to be de-configured
                                                is binded to any channel. Also, the timer has to be paused first before
                                                it can be de-configured.
                                                When this field is set, duty_resolution, freq_hz, clk_cfg fields are ignored. */
} ledc_timer_config_t;

        简单翻译了一下:

/**
 * @brief 用于“ledc_timer_config”函数的 LEDC 定时器配置参数
 */
typedef struct {
    ledc_mode_t speed_mode;                /*!< LEDC 速度模式(speed_mode),可选择高速模式(仅 ESP32 支持)或低速模式 */
    ledc_timer_bit_t duty_resolution;      /*!< LEDC 通道占空比分辨率 */
    ledc_timer_t  timer_num;               /*!< 通道(0 - LEDC_TIMER_MAX - 1)的定时器源 */
    uint32_t freq_hz;                      /*!< LED 时钟频率 (Hz) */
    ledc_clk_cfg_t clk_cfg;                /*!< 从 ledc_clk_cfg_t 配置 LEDC 源时钟。
                                                请注意,LEDC_USE_RC_FAST_CLK 和 LEDC_USE_XTAL_CLK 并非特定于定时器的时钟源。
                                                您不能让一个 LEDC 定时器使用 RC_FAST_CLK 作为时钟源,而让另一个 LEDC 定时器使用 XTAL_CLK 作为其时钟源。
                                                除了 esp32 和 esp32s2 外,所有芯片都不具备特定于定时器的时钟源,这意味着所有定时器的时钟源必须是相同的。 */
    bool deconfigure;                      /*!< 将此字段设置为“取消配置”可取消之前已配置的 LEDC 定时器
                                                请注意,它不会检查该定时器是否需要取消配置并且它未绑定到任何通道。
                                                此外,必须先暂停该定时器才能取消配置。当设置此字段时,duty_resolution、freq_hz、clk_cfg 字段将被忽略。 */
} ledc_timer_config_t;

        频率和占空比分辨率相互关联。PWM 频率越高,占空比分辨率越低,反之亦然。

3.2  通道配置

        定时器设置好后,请配置所需的通道(ledc_channel_t 之一)。配置通道需调用函数ledc_channel_config()。

        通道的配置与定时器设置类似,需向通道配置函数传递包括通道配置参数的结构体 ledc_channel_config_t。

        此时,通道会按照 ledc_channel_config_t 的配置开始运作,并在选定的 GPIO 上生成由定时器设置指定的频率和占空比的 PWM 信号。在通道运作过程中,可以随时通过调用函数 ledc_stop() 将其暂停。

3.3  软件改变PWM占空比

        调用函数 ledc_set_duty() 可以设置新的占空比:

/**
* @简述:设置 LEDC 脉冲占空比
* 该函数不会更改此通道的 hpoint 值。如需更改,请调用 ledc_set_duty_with_hpoint 函数。
* 只有在调用 ledc_update_duty 之后,占空比才会更新。*
* @注意:ledc_set_duty、ledc_set_duty_with_hpoint 以及 ledc_update_duty 这些函数并非线程安全的,请勿在同一任务中同时调用这些函数来控制同一个 LEDC 通道。
*        请使用线程安全版本的 API ledc_set_duty_and_update 来进行控制。
* @注意:对于 ESP32 芯片,当某个通道上的渐变操作正在进行时,硬件不会支持任何占空比的更改。
*        其他占空比操作必须等到该渐变操作完成之后才能进行。*
* @参数 speed_mode:选择具有指定速度模式的 LEDC 通道组。请注意,并非所有目标都支持高速模式。
* @参数 channel:LEDC 通道(0 - LEDC_CHANNEL_MAX - 1),从 ledc_channel_t 中选择。
* @参数 duty:设置 LEDC 脉冲宽度,脉冲宽度设置的范围为 [0, (2**duty_resolution)] 。*
* @返回值
*     - ESP_OK 表示成功
*     - ESP_ERR_INVALID_ARG 表示参数错误*/
esp_err_t ledc_set_duty(ledc_mode_t speed_mode, ledc_channel_t channel, uint32_t duty);

        传递给函数的占空比数值范围取决于选定的 duty_resolution,应为 0 至 (2 ** duty_resolution)。例如,如选定的占空比分辨率为 10,则占空比的数值范围为 0 至 1024。此时分辨率为 ~ 0.1%。

在 ESP32-S3 上,当通道绑定的定时器配置了其最大 PWM 占空比分辨率( MAX_DUTY_RES ),通道的占空比不能被设置到 (2 ** MAX_DUTY_RES) 。否则,硬件内部占空比计数器会溢出,并导致占空比计算错误。

        之后,调用函数 ledc_update_duty() 使新配置生效:

/**
* 【简要说明】更新 LEDC 更新通道参数*
* @注意:调用此函数可激活 LEDC 更新的参数。
* 在调用 ledc_set_duty 之后,我们需要调用此函数来更新设置。
* 新的 LEDC 参数要到下一个 PWM 周期才会生效。
* @注意:ledc_set_duty、ledc_set_duty_with_hpoint 和 ledc_update_duty 并非线程安全,不要在同一任务的不同线程中同时调用这些函数来控制一个 LEDC 通道。
* 一个线程安全的 API 版本是 ledc_set_duty_and_update。
* @注意:如果启用了 CONFIG_LEDC_CTRL_FUNC_IN_IRAM,此函数将由链接器放置在 IRAM 中,
* 这使得即使在缓存禁用的情况下也能执行。
* @注意:此函数可以在中断服务程序(ISR)上下文中运行。*
* @参数 speed_mode:选择具有指定速度模式的 LEDC 通道组。请注意,并非所有目标都支持高速模式。
* @参数 channel:LEDC 通道(0 - LEDC_CHANNEL_MAX - 1),从 ledc_channel_t 中选择。*
* @返回值
*     - ESP_OK 表示成功
*     - ESP_ERR_INVALID_ARG 表示参数错误*/

esp_err_t ledc_update_duty(ledc_mode_t speed_mode, ledc_channel_t channel);

3.4  硬件改变PWM占空比

        LED PWM 控制器硬件可逐渐改变占空比的数值。要使用此功能,需用函数 ledc_fade_func_install() 使能渐变,之后用下列可用渐变函数之一配置:

  • ledc_set_fade_with_time()
  • ledc_set_fade_with_step()
  • ledc_set_fade()

        最后需要调用 ledc_fade_start() 开启渐变。此外,在使能渐变后,每个通道都可以额外通过调用 ledc_cb_register() 注册一个回调函数用以获得渐变完成的事件通知。回调函数的原型被定义在 ledc_cb_t。每个回调函数都应当返回一个布尔值给驱动的中断处理函数,用以表示是否有高优先级任务被其唤醒。

        如不需要渐变和渐变中断,可用函数 ledc_fade_func_uninstall() 关闭。

        下面使用的时候,和例题一起进行详细介绍其用法(4.3  呼吸灯效果实现)。

4.  程序设计

        我们从点亮一颗LED灯,逐步演变最终实现呼吸灯的效果。

4.1  点亮一颗LED灯

        对于文件的创建这些,这里不在做过多赘述,想要了解如何创建一个新工程的可以查看:

ESP32入门开发·VScode空白项目搭建·点亮一颗LED灯-优快云博客

        下面的点亮LED灯是在上述基础上,增加一些FreeRTOS的任务调用,为方便后续实现呼吸灯做准备。

4.1.1  头文件包含

        为方便后续调用,把所有后续要使用到的头文件都包含进来了:

#include <stdio.h>
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/ledc.h"

4.1.2  GPIO口初始化

        这里就是一些初始化参数,选择要配置的引脚,引脚的工作模式,是否开启上下拉以及中断功能:

/* LED控制引脚定义 */
#define LED_GPIO_PIN      GPIO_NUM_1  // 使用GPIO1作为LED控制引脚

//LED初始化
void LED_Init(void)
{
    gpio_config_t io_conf;// 定义 GPIO 配置结构体
    
    io_conf.pin_bit_mask = (1ULL << LED_GPIO_PIN); // 设置要配置的 GPIO 引脚(使用位掩码形式)
    io_conf.mode = GPIO_MODE_OUTPUT;               // 设置 GPIO 模式为输出(既能读取也能控制电平)
    io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;  // 禁用下拉电阻
    io_conf.pull_up_en = GPIO_PULLUP_DISABLE;      // 禁用上拉电阻  
    io_conf.intr_type = GPIO_INTR_DISABLE;         // 禁用 GPIO 中断功能
    
    gpio_config(&io_conf);// 应用 GPIO 配置
}

4.1.3  LED任务函数

        创建一个任务函数,用于创建任务调用,这里就实现每500ms实现以下电平翻转:

//LED运行测试任务函数
void LED_run_test(void* param)
{
    bool gpio_level = false;

    while(1)
    {
        gpio_level = gpio_level?0:1;//三目运算符实现数据翻转
        gpio_set_level(LED_GPIO_PIN,gpio_level);
        vTaskDelay(pdMS_TO_TICKS(500));
    }
}

4.1.4  主函数

        这里先来熟悉一下ESP32上任务的创建函数:

BaseType_t xTaskCreatePinnedToCore(
    TaskFunction_t pvTaskCode,      // 任务函数指针
    const char * const pcName,      // 任务名称(字符串标识)
    const uint32_t usStackDepth,    // 任务栈大小(单位:字)
    void * const pvParameters,      // 任务参数(传递给函数的void指针)
    UBaseType_t uxPriority,         // 任务优先级(0~configMAX_PRIORITIES-1)
    TaskHandle_t * const pvCreatedTask, // 任务句柄(可选,用于管理任务)
    const BaseType_t xCoreID        // 指定运行的核心(0或1,或tskNO_AFFINITY)
);

        观察我们可以发现其和FreeRTOS的任务创建函数 xTaskCreate() 类似,只是多了一个指定运行核心,其中:

  • 核心0:处理高实时性任务(WiFi/BT 协议栈、中断服务)。

  • 核心1:运行用户应用(传感器数据处理、PWM 控制等)。

  • tskNO_AFFINITY:不绑定核心(由调度器分配)。

这里默认对FreeRTOS有一定的了解,如果详细了解,可以查看下方链接,虽然是STM32的但是基本原理一样:

FreeRTOS菜鸟入门系列_时光の尘的博客-优快云博客

        主函数代码:

void app_main(void)
{
    LED_Init();

    xTaskCreatePinnedToCore(
        LED_run_test,    // 任务函数指针(LED控制主逻辑)
        "led",          // 任务名称(用于调试)
        2048,           // 任务栈大小(字节)
        NULL,           // 任务参数(未使用)
        3,              // 任务优先级(数字越大优先级越高)
        NULL,           // 任务句柄(不保存)
        1               // 指定运行在核心1(ESP32双核特性)
    );
}

        可以看到LED每隔500ms闪烁一次(这里可能相机拍摄的原因,感觉慢慢灭的,实际直接亮灭的):

注意,对于下面两个小节没有前后关系,其中 4.2 小节主要是调用软件PWM实现LED灯的亮度调节,4.3 小节主要是使用硬件定时器实现呼吸灯的效果,可以直接跳过4.2去看4.3,这两个小节,没有前后关系。

4.2  PWM调节LED灯亮度

        我先使用软件更改占空比看一下。

4.2.1  定时器配置

        根据我们上面介绍,定时器配置需要调用 ledc_timer_config_t 函数,配置其参数,然后调用 ledc_timer_config 应用定时器配置:

    ledc_timer_config_t ledc_timer = 
    {
        .speed_mode = LEDC_LOW_SPEED_MODE,     // 使用低速模式
        .timer_num = LEDC_TIMER_0,             // 选择定时器0
        .clk_cfg = LEDC_AUTO_CLK,              // 自动选择时钟源
        .freq_hz = 5000,                       // 设置PWM频率为5kHz
        .duty_resolution = LEDC_TIMER_13_BIT,  // 13位分辨率(8192级亮度控制)
    };
    ledc_timer_config(&ledc_timer);            // 应用定时器配置

        对于模式上面也提到了ESP32-S3只能选择低速模式,定时器四个定时选择一个,这里我们就选择0:

typedef enum {
    LEDC_TIMER_0 = 0, /*!< LEDC timer 0 */
    LEDC_TIMER_1,     /*!< LEDC timer 1 */
    LEDC_TIMER_2,     /*!< LEDC timer 2 */
    LEDC_TIMER_3,     /*!< LEDC timer 3 */
    LEDC_TIMER_MAX,
} ledc_timer_t;

        时钟源自动选择:

/**
 * @brief Type of LEDC clock source, reserved for the legacy LEDC driver
 */
typedef enum {
    LEDC_AUTO_CLK = 0,                              /*!< LEDC source clock will be automatically selected based on the giving resolution and duty parameter when init the timer*/
    LEDC_USE_APB_CLK = SOC_MOD_CLK_APB,             /*!< Select APB as the source clock */
    LEDC_USE_RC_FAST_CLK = SOC_MOD_CLK_RC_FAST,     /*!< Select RC_FAST as the source clock */
    LEDC_USE_XTAL_CLK = SOC_MOD_CLK_XTAL,           /*!< Select XTAL as the source clock */

    LEDC_USE_RTC8M_CLK __attribute__((deprecated("please use 'LEDC_USE_RC_FAST_CLK' instead"))) = LEDC_USE_RC_FAST_CLK,   /*!< Alias of 'LEDC_USE_RC_FAST_CLK' */
} soc_periph_ledc_clk_src_legacy_t;

        对于频率和分辨率注意参考这张图:

4.2.2  通道配置

        也是调用API函数,对其参数配置,使用什么模式,使用哪个PWM通道,通道需要绑定在哪个定时器上,指定哪个引脚,初始值是多少,是否开启中断:

    ledc_channel_config_t ledc_channel = 
    {
        .speed_mode = LEDC_LOW_SPEED_MODE,     // 低速模式
        .channel = LEDC_CHANNEL_0,             // 使用通道0
        .timer_sel = LEDC_TIMER_0,             // 绑定到定时器0
        .gpio_num = LED_GPIO_PIN,              // 指定GPIO引脚
        .duty = 0,                             // 初始占空比为0(全暗)
        .intr_type = LEDC_INTR_DISABLE,        // 禁用中断
    };
    ledc_channel_config(&ledc_channel);        // 应用通道配置   

typedef enum {
    LEDC_CHANNEL_0 = 0, /*!< LEDC channel 0 */
    LEDC_CHANNEL_1,     /*!< LEDC channel 1 */
    LEDC_CHANNEL_2,     /*!< LEDC channel 2 */
    LEDC_CHANNEL_3,     /*!< LEDC channel 3 */
    LEDC_CHANNEL_4,     /*!< LEDC channel 4 */
    LEDC_CHANNEL_5,     /*!< LEDC channel 5 */
#if SOC_LEDC_CHANNEL_NUM > 6
    LEDC_CHANNEL_6,     /*!< LEDC channel 6 */
    LEDC_CHANNEL_7,     /*!< LEDC channel 7 */
#endif
    LEDC_CHANNEL_MAX,
} ledc_channel_t;

4.2.3  软件改变PWM占空比

        这里需要调用 ledc_set_duty 和 ledc_update_duty 这两个函数,对于 ledc_set_duty 的 duty 取值范围需要考虑到我们分辨率的取值,我们上面分辨率给的13,那么其最大值为 8191 也就是2的13次方减1,这里我取其819,也就是十分之一亮度:

    // 更新 PWM 占空比
    ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 819);
    ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);

        不过这样我们想象更改其他值不好更改,我们创建一个函数,限制一下取值范围,亮度的范围就是0~100,这样之后直接更改函数即可:

void set_led_brightness(uint8_t brightness)
{
    if (brightness > 100) 
    {
        brightness = 100;  // 限制范围
    }

    // 计算占空比(13 位分辨率,最大值 8191)
    uint32_t duty = (8191 * brightness) / 100;

    // 更新 PWM 占空比
    ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, duty);
    ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
}

4.2.4  LED任务函数

        将上面LED闪烁的任务函数做一个简单的修改,调用上面改变占空比函数:

void LED_run_test(void* param)
{
    while(1)
    {
        set_led_brightness(10);
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

        注意这里必须要有一个延时阻塞一下任务,否则终端看门狗一直打印数据,运行看一下效果:

        不过这里只有一个灯看不出对比效果,在声明一个LED灯,对比看一下:

#define LED_GPIO_PIN_C      GPIO_NUM_4

void LED_Init_comparison(void)
{
    gpio_config_t io_conf;// 定义 GPIO 配置结构体
    
    io_conf.pin_bit_mask = (1ULL << LED_GPIO_PIN_C); // 设置要配置的 GPIO 引脚(使用位掩码形式)
    io_conf.mode = GPIO_MODE_OUTPUT;               // 设置 GPIO 模式为输出(既能读取也能控制电平)
    io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;  // 禁用下拉电阻
    io_conf.pull_up_en = GPIO_PULLUP_DISABLE;      // 禁用上拉电阻  
    io_conf.intr_type = GPIO_INTR_DISABLE;         // 禁用 GPIO 中断功能
    
    gpio_config(&io_conf);// 应用 GPIO 配置

    gpio_set_level(LED_GPIO_PIN_C,1);
}

        注意主函数初始化一下,看一下效果:

        可以看到下方没有加占空比的灯珠更亮,不过还是手机拍摄的原因,对比不明显,可以自己下载看一下。

4.2.5  主函数

        完整代码:

#include <stdio.h>
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/ledc.h"

/* LED控制引脚定义 */
#define LED_GPIO_PIN        GPIO_NUM_1  // 使用GPIO1作为LED控制引脚
#define LED_GPIO_PIN_C      GPIO_NUM_4

//LED初始化
void LED_Init(void)
{
    gpio_config_t io_conf;// 定义 GPIO 配置结构体
    
    io_conf.pin_bit_mask = (1ULL << LED_GPIO_PIN); // 设置要配置的 GPIO 引脚(使用位掩码形式)
    io_conf.mode = GPIO_MODE_OUTPUT;               // 设置 GPIO 模式为输出(既能读取也能控制电平)
    io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;  // 禁用下拉电阻
    io_conf.pull_up_en = GPIO_PULLUP_DISABLE;      // 禁用上拉电阻  
    io_conf.intr_type = GPIO_INTR_DISABLE;         // 禁用 GPIO 中断功能
    
    gpio_config(&io_conf);// 应用 GPIO 配置
}

void LED_Init_comparison(void)
{
    gpio_config_t io_conf;// 定义 GPIO 配置结构体
    
    io_conf.pin_bit_mask = (1ULL << LED_GPIO_PIN_C); // 设置要配置的 GPIO 引脚(使用位掩码形式)
    io_conf.mode = GPIO_MODE_OUTPUT;               // 设置 GPIO 模式为输出(既能读取也能控制电平)
    io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;  // 禁用下拉电阻
    io_conf.pull_up_en = GPIO_PULLUP_DISABLE;      // 禁用上拉电阻  
    io_conf.intr_type = GPIO_INTR_DISABLE;         // 禁用 GPIO 中断功能
    
    gpio_config(&io_conf);// 应用 GPIO 配置

    gpio_set_level(LED_GPIO_PIN_C,1);
}

void LEDC_PWM_Init(void)
{
    ledc_timer_config_t ledc_timer = 
    {
        .speed_mode = LEDC_LOW_SPEED_MODE,     // 使用低速模式
        .timer_num = LEDC_TIMER_0,             // 选择定时器0
        .clk_cfg = LEDC_AUTO_CLK,              // 自动选择时钟源
        .freq_hz = 5000,                       // 设置PWM频率为5kHz
        .duty_resolution = LEDC_TIMER_13_BIT,  // 13位分辨率(8192级亮度控制)
    };
    ledc_timer_config(&ledc_timer);            // 应用定时器配置
    
    ledc_channel_config_t ledc_channel = 
    {
        .speed_mode = LEDC_LOW_SPEED_MODE,     // 低速模式
        .channel = LEDC_CHANNEL_0,             // 使用通道0
        .timer_sel = LEDC_TIMER_0,             // 绑定到定时器0
        .gpio_num = LED_GPIO_PIN,              // 指定GPIO引脚
        .duty = 0,                             // 初始占空比为0(全暗)
        .intr_type = LEDC_INTR_DISABLE,        // 禁用中断
    };
    ledc_channel_config(&ledc_channel);        // 应用通道配置    
}

void set_led_brightness(uint8_t brightness)
{
    if (brightness > 100) 
    {
        brightness = 100;  // 限制范围
    }

    // 计算占空比(13 位分辨率,最大值 8191)
    uint32_t duty = (8191 * brightness) / 100;

    // 更新 PWM 占空比
    ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, duty);
    ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
}

//LED运行测试任务函数
void LED_run_test(void* param)
{
    while(1)
    {
        set_led_brightness(10);
        vTaskDelay(pdMS_TO_TICKS(10));
    }
}

void app_main(void)
{
    LED_Init();
    LEDC_PWM_Init();
    LED_Init_comparison();
    
    xTaskCreatePinnedToCore(
        LED_run_test,    // 任务函数指针(LED控制主逻辑)
        "led",          // 任务名称(用于调试)
        2048,           // 任务栈大小(字节)
        NULL,           // 任务参数(未使用)
        3,              // 任务优先级(数字越大优先级越高)
        NULL,           // 任务句柄(不保存)
        1               // 指定运行在核心1(ESP32双核特性)
    );
}

        这里软件PWM的使用,主要是为了控制灯的亮度,当然也能实现逐步增加亮度,只是没有调用硬件流程,想要软件实现逐步增加亮度,只需要在LED任务函数进行一个简单的修改,每隔一秒对亮度累加20,这样就有一种逐步电亮的效果:

//LED运行测试任务函数
void LED_run_test(void* param)
{
    uint8_t brightness = 0;

    while(1)
    {
        // 设置亮度(0%~100%)
        set_led_brightness(brightness);
        printf("LED Brightness: %d%%\n", brightness);

        brightness += 20;      // 每次增加 20%
        if (brightness > 100) brightness = 0;

        vTaskDelay(1000/ portTICK_PERIOD_MS);  // 延时 1 秒
    }

        看一下效果:

4.3  呼吸灯效果实现

4.3.1  定时器配置

        直接沿用了4.2的介绍,跳过去看了。

         根据我们上面介绍,定时器配置需要调用 ledc_timer_config_t 函数,配置其参数,然后调用 ledc_timer_config 应用定时器配置:

    ledc_timer_config_t ledc_timer = 
    {
        .speed_mode = LEDC_LOW_SPEED_MODE,     // 使用低速模式
        .timer_num = LEDC_TIMER_0,             // 选择定时器0
        .clk_cfg = LEDC_AUTO_CLK,              // 自动选择时钟源
        .freq_hz = 5000,                       // 设置PWM频率为5kHz
        .duty_resolution = LEDC_TIMER_13_BIT,  // 13位分辨率(8192级亮度控制)
    };
    ledc_timer_config(&ledc_timer);            // 应用定时器配置

        对于模式上面也提到了ESP32-S3只能选择低速模式,定时器四个定时选择一个,这里我们就选择0:

typedef enum {
    LEDC_TIMER_0 = 0, /*!< LEDC timer 0 */
    LEDC_TIMER_1,     /*!< LEDC timer 1 */
    LEDC_TIMER_2,     /*!< LEDC timer 2 */
    LEDC_TIMER_3,     /*!< LEDC timer 3 */
    LEDC_TIMER_MAX,
} ledc_timer_t;

        时钟源自动选择:

/**
 * @brief Type of LEDC clock source, reserved for the legacy LEDC driver
 */
typedef enum {
    LEDC_AUTO_CLK = 0,                              /*!< LEDC source clock will be automatically selected based on the giving resolution and duty parameter when init the timer*/
    LEDC_USE_APB_CLK = SOC_MOD_CLK_APB,             /*!< Select APB as the source clock */
    LEDC_USE_RC_FAST_CLK = SOC_MOD_CLK_RC_FAST,     /*!< Select RC_FAST as the source clock */
    LEDC_USE_XTAL_CLK = SOC_MOD_CLK_XTAL,           /*!< Select XTAL as the source clock */

    LEDC_USE_RTC8M_CLK __attribute__((deprecated("please use 'LEDC_USE_RC_FAST_CLK' instead"))) = LEDC_USE_RC_FAST_CLK,   /*!< Alias of 'LEDC_USE_RC_FAST_CLK' */
} soc_periph_ledc_clk_src_legacy_t;

        对于频率和分辨率注意参考这张图:

4.3.2  通道配置

        也是沿用。

        也是调用API函数,对其参数配置,使用什么模式,使用哪个PWM通道,通道需要绑定在哪个定时器上,指定哪个引脚,初始值是多少,是否开启中断:

    ledc_channel_config_t ledc_channel = 
    {
        .speed_mode = LEDC_LOW_SPEED_MODE,     // 低速模式
        .channel = LEDC_CHANNEL_0,             // 使用通道0
        .timer_sel = LEDC_TIMER_0,             // 绑定到定时器0
        .gpio_num = LED_GPIO_PIN,              // 指定GPIO引脚
        .duty = 0,                             // 初始占空比为0(全暗)
        .intr_type = LEDC_INTR_DISABLE,        // 禁用中断
    };
    ledc_channel_config(&ledc_channel);        // 应用通道配置   

typedef enum {
    LEDC_CHANNEL_0 = 0, /*!< LEDC channel 0 */
    LEDC_CHANNEL_1,     /*!< LEDC channel 1 */
    LEDC_CHANNEL_2,     /*!< LEDC channel 2 */
    LEDC_CHANNEL_3,     /*!< LEDC channel 3 */
    LEDC_CHANNEL_4,     /*!< LEDC channel 4 */
    LEDC_CHANNEL_5,     /*!< LEDC channel 5 */
#if SOC_LEDC_CHANNEL_NUM > 6
    LEDC_CHANNEL_6,     /*!< LEDC channel 6 */
    LEDC_CHANNEL_7,     /*!< LEDC channel 7 */
#endif
    LEDC_CHANNEL_MAX,
} ledc_channel_t;

4.3.3  硬件改变PWM占空比

        前面我们也提到了,要想实现渐变效果,需用先使能函数 ledc_fade_func_install() 实现渐变:

    ledc_fade_func_install(0);                // 安装渐变服务(参数0表示不分配中断优先级)

        然后调用介绍中的三个函数中的一个进行设置渐变效果,这里我们使用 ledc_set_fade_with_time ,其函数原型为:

/**
 * @brief Set LEDC fade function, with a limited time.
 *
 * @note  Call ledc_fade_func_install() once before calling this function.
 *        Call ledc_fade_start() after this to start fading.
 * @note  ledc_set_fade_with_step, ledc_set_fade_with_time and ledc_fade_start are not thread-safe, do not call these functions to
 *        control one LEDC channel in different tasks at the same time.
 *        A thread-safe version of API is ledc_set_fade_step_and_start
 * @note  For ESP32, hardware does not support any duty change while a fade operation is running in progress on that channel.
 *        Other duty operations will have to wait until the fade operation has finished.
 *
 * @param speed_mode Select the LEDC channel group with specified speed mode. Note that not all targets support high speed mode.
 * @param channel LEDC channel index (0 - LEDC_CHANNEL_MAX-1), select from ledc_channel_t
 * @param target_duty Target duty of fading [0, (2**duty_resolution)]
 * @param max_fade_time_ms The maximum time of the fading ( ms ).
 *
 * @return
 *     - ESP_OK Success
 *     - ESP_ERR_INVALID_ARG Parameter error
 *     - ESP_ERR_INVALID_STATE Channel not initialized
 *     - ESP_FAIL Fade function init error
 */
esp_err_t ledc_set_fade_with_time(ledc_mode_t speed_mode, ledc_channel_t channel, uint32_t target_duty, int max_fade_time_ms);

        翻译一下:

/**
* @简要说明:设置 LEDC 褪色功能,限时操作。*
* @注意:在调用此函数之前,需先调用 ledc_fade_func_install() 函数。
* 调用此函数之后,需再调用 ledc_fade_start() 函数以启动渐变过程。
* @注意:ledc_set_fade_with_step、ledc_set_fade_with_time 和 ledc_fade_start 这些函数并非线程安全的,请勿在同一任务的不同线程中同时调用这些函数来控制同一个 LEDC 通道。
* 有线程安全版本的 API 是 ledc_set_fade_step_and_start。
* @注意:对于 ESP32,当在该通道上进行渐变操作时,硬件不会支持任何占空比的变化。其他占空比操作必须等到渐变操作完成后再进行。*
* @参数 speed_mode:选择具有指定速度模式的 LEDC 通道组。请注意,并非所有目标都支持高速模式。
* @参数 channel:LEDC 通道索引(0 - LEDC_CHANNEL_MAX - 1),从 ledc_channel_t 类型中选择。
* @参数 target_duty:渐变目标的占空比 [0,(2的duty_resolution次幂)]。
* @参数 max_fade_time_ms:渐变的最大时间(毫秒)。*
* @返回
*   - ESP_OK 成功
*   - ESP_ERR_INVALID_ARG 参数错误
*   - ESP_ERR_INVALID_STATE 通道未初始化
*   - ESP_FAIL 调光功能初始化失败*/

esp_err_t ledc_set_fade_with_time(ledc_mode_t speed_mode, ledc_channel_t channel, uint32_t target_duty, int max_fade_time_ms);

        这里我们使用低速模式,通道0,由于之前配置的计数器是13,这里我们给一个8192,渐变时间设置为3000ms,也就是3s:

    ledc_set_fade_with_time(
        LEDC_LOW_SPEED_MODE,   // 低速模式
        LEDC_CHANNEL_0,        // 通道0
        8192,                  // 目标占空比(对应13bit最大值的100%)
        3000                   // 渐变时间3000ms(3秒)
    );

        而后调用 ledc_fade_start 启动渐变,其函数原型:

/**
 * @brief Start LEDC fading.
 *
 * @note  Call ledc_fade_func_install() once before calling this function.
 *        Call this API right after ledc_set_fade_with_time or ledc_set_fade_with_step before to start fading.
 * @note  Starting fade operation with this API is not thread-safe, use with care.
 * @note  For ESP32, hardware does not support any duty change while a fade operation is running in progress on that channel.
 *        Other duty operations will have to wait until the fade operation has finished.
 *
 * @param speed_mode Select the LEDC channel group with specified speed mode. Note that not all targets support high speed mode.
 * @param channel LEDC channel number
 * @param fade_mode Whether to block until fading done. See ledc_types.h ledc_fade_mode_t for more info.
 *        Note that this function will not return until fading to the target duty if LEDC_FADE_WAIT_DONE mode is selected.
 *
 * @return
 *     - ESP_OK Success
 *     - ESP_ERR_INVALID_STATE Channel not initialized or fade function not installed.
 *     - ESP_ERR_INVALID_ARG Parameter error.
 */
esp_err_t ledc_fade_start(ledc_mode_t speed_mode, ledc_channel_t channel, ledc_fade_mode_t fade_mode);

        翻译一下:

/**
* 【简要说明】启动 LEDC 艳彩渐变功能。*
* @注意:在调用此函数之前,需先调用 ledc_fade_func_install() 函数。
* 调用此 API 时应在调用 ledc_set_fade_with_time 或 ledc_set_fade_with_step 之后进行,以便开始渐变操作。
* 注意:使用此 API 开始渐变操作并非线程安全的,请谨慎使用。
* 注意:对于 ESP32,当在该通道上进行渐变操作时,硬件不会支持任何占空比的更改。其他占空比操作必须等到渐变操作完成后再进行。*
* @参数 speed_mode:选择具有指定速度模式的 LEDC 通道组。请注意,并非所有目标都支持高速模式。
* @参数 channel:LEDC 通道编号
* @参数 fade_mode:是否要等待褪色完成后再继续执行。有关更多信息,请参阅 ledc_types.h 中的 ledc_fade_mode_t。
        注意:如果选择了 LEDC_FADE_WAIT_DONE 模式,此函数将不会返回,直到褪色达到目标占空比为止。*
* @返回值
*     - ESP_OK 表示成功
*     - ESP_ERR_INVALID_STATE 表示通道未初始化或衰减功能未安装
*     - ESP_ERR_INVALID_ARG 表示参数错误*/

esp_err_t ledc_fade_start(ledc_mode_t speed_mode, ledc_channel_t channel, ledc_fade_mode_t fade_mode);

        还是低速,通道零,非阻塞:

    ledc_fade_start(
        LEDC_LOW_SPEED_MODE,
        LEDC_CHANNEL_0,
        LEDC_FADE_NO_WAIT      // 非阻塞模式(不等待渐变完成)
    );

        这里是一些初始化的配置,之后回调函数和任务函数一起讲,方便理清思路,后面可能会感觉有点绕,并且涉及到事件,最好有一些基础,想要了解可以跳转,之前写STM32的时候,大概原理是一样的:

FreeRTOS菜鸟入门(十四)·事件_freertos 事件-优快云博客

4.3.4  回调函数

        在函数开始前我们先看一下官方文档,我们知道,在我们使能完渐变后,每个通道都需要调用 ledc_cb_register() 注册一个回调函数用以获得渐变完成的事件通知。回调函数的原型被定义在 ledc_cb_t。每个回调函数都应当返回一个布尔值给驱动的中断处理函数,用以表示是否有高优先级任务被其唤醒。此外,值得注意的是,由于驱动的中断处理函数被放在了 IRAM 中, 回调函数和其调用的函数也需要被放在 IRAM 中。 ledc_cb_register() 会检查回调函数及函数上下文的指针地址是否在正确的存储区域。

        基于此,我们想要完成LED灯循环亮灭的呼吸效果,就需要创建两个事件标志位,用于判断LED灯是由灭转亮,还是由亮转灭,先完善两个宏定义,顺便创建事件组句柄方便调用:

/* 事件标志位定义 */
#define FULL_EV_BIT BIT0      // 全亮事件标志位(二进制01)
#define EMPTY_EV_BIT  BIT1    // 全暗事件标志位(二进制10)

/* 事件组句柄声明 */
static EventGroupHandle_t ledc_event_handle;  // 用于PWM渐变事件通知的FreeRTOS事件组

        首先创建FreeRTOS事件组:

    ledc_event_handle = xEventGroupCreate();  // 创建FreeRTOS事件组

        我们想要注册回调函数,需要找到其原型,回调函数的原型被定义在 ledc_cb_t 当中:

/**
 * @brief Type of LEDC event callback
 * @param param LEDC callback parameter
 * @param user_arg User registered data
 * @return Whether a high priority task has been waken up by this function
 */
typedef bool (*ledc_cb_t)(const ledc_cb_param_t *param, void *user_arg);

        翻译一下:

/**
* @brief LEDC 事件回调的类型
* @param param LEDC 回调参数
* @param user_arg 用户注册的数据
* @return 该函数是否唤醒了高优先级任务*/

typedef bool (*ledc_cb_t)(const ledc_cb_param_t *param, void *user_arg);

        把这段复制下来,来到主函数声明,注意改一下函数名:

bool ledc_finish_cb(const ledc_cb_param_t *param, void *user_arg)
{
}

        不过这里需要注意的是,由于驱动的中断处理函数被放在了 IRAM 中, 回调函数和其调用的函数也需要被放在 IRAM 中,其作用是指定函数或数据应放置在 内部 RAM (IRAM) 中,而不是默认的 Flash 中,因此最终声明为(内容等下填充):

bool IRAM_ATTR ledc_finish_cb(const ledc_cb_param_t *param, void *user_arg)
{
}

        我们上面也看到了 ledc_cb_t 是在 typedef 结构体当中,找到定义:

/**
 * @brief Group of supported LEDC callbacks
 * @note The callbacks are all running under ISR environment
 */
typedef struct {
    ledc_cb_t fade_cb;                  /**< LEDC fade_end callback function */
} ledc_cbs_t;

        因此我们想要注册回调函数需要调用 ledc_cbs_t ,其内参数指定我们刚刚创建的回调函数,如下:

    ledc_cbs_t cbs = 
    {
        .fade_cb = ledc_finish_cb,  // 指定渐变完成时的回调函数
    };

        然后再调用 ledc_cb_register 完成回调函数的注册,其函数原型:

/**
 * @brief LEDC callback registration function
 *
 * @note  The callback is called from an ISR, it must never attempt to block, and any FreeRTOS API called must be ISR capable.
 *
 * @param speed_mode Select the LEDC channel group with specified speed mode. Note that not all targets support high speed mode.
 * @param channel LEDC channel index (0 - LEDC_CHANNEL_MAX-1), select from ledc_channel_t
 * @param cbs Group of LEDC callback functions
 * @param user_arg user registered data for the callback function
 *
 * @return
 *     - ESP_OK Success
 *     - ESP_ERR_INVALID_ARG Parameter error
 *     - ESP_ERR_INVALID_STATE Channel not initialized
 *     - ESP_FAIL Fade function init error
 */
esp_err_t ledc_cb_register(ledc_mode_t speed_mode, ledc_channel_t channel, ledc_cbs_t *cbs, void *user_arg);

        翻译一下:

/**
* @brief LEDC 回调注册函数*
* @note 回调函数是从中断服务例程(ISR)中调用的,它绝不能尝试阻塞,并且调用的任何 FreeRTOS API 都必须是中断安全的。*
* @param speed_mode 选择具有指定速度模式的 LEDC 通道组。请注意,并非所有目标都支持高速模式。
* @param channel LEDC 通道索引(0 - LEDC_CHANNEL_MAX - 1),从 ledc_channel_t 中选择
* @param cbs LEDC 回调函数组
* @param user_arg 回调函数的用户注册数据*
* 返回值
* - ESP_OK 成功
* - ESP_ERR_INVALID_ARG 参数错误
* - ESP_ERR_INVALID_STATE 通道未初始化
* - ESP_FAIL 淡入淡出功能初始化错误*/

esp_err_t ledc_cb_register(ledc_mode_t speed_mode, ledc_channel_t channel, ledc_cbs_t *cbs, void *user_arg);

        这里我们使用低速,通道0,绑定我们上面声明的结构体 cbs ,无数据传输:

    ledc_cb_register(
        LEDC_LOW_SPEED_MODE,
        LEDC_CHANNEL_0,
        &cbs,                      // 回调结构体
        NULL                       // 无额外参数
    );

        至此我们PWM初始化配置相关的已经完成,此时的完整函数:

/**
 * @brief LED PWM控制器(LEDC)初始化函数
 * 
 * 功能描述:
 * 1. 配置LEDC定时器参数
 * 2. 设置LEDC通道参数
 * 3. 启用PWM渐变功能
 * 4. 配置渐变结束回调
 */
void LEDC_PWM_Init(void)
{
    /* 第一步:配置LEDC定时器 */
    ledc_timer_config_t ledc_timer = 
    {
        .speed_mode = LEDC_LOW_SPEED_MODE,     // 使用低速模式
        .timer_num = LEDC_TIMER_0,             // 选择定时器0
        .clk_cfg = LEDC_AUTO_CLK,              // 自动选择时钟源
        .freq_hz = 5000,                       // 设置PWM频率为5kHz
        .duty_resolution = LEDC_TIMER_13_BIT,  // 13位分辨率(8192级亮度控制)
    };
    ledc_timer_config(&ledc_timer);            // 应用定时器配置

    /* 第二步:配置LEDC通道 */
    ledc_channel_config_t ledc_channel = 
    {
        .speed_mode = LEDC_LOW_SPEED_MODE,     // 低速模式
        .channel = LEDC_CHANNEL_0,             // 使用通道0
        .timer_sel = LEDC_TIMER_0,             // 绑定到定时器0
        .gpio_num = LED_GPIO_PIN,              // 指定GPIO引脚
        .duty = 0,                            // 初始占空比为0(全暗)
        .intr_type = LEDC_INTR_DISABLE,       // 禁用中断
    };
    ledc_channel_config(&ledc_channel);        // 应用通道配置

    /* 第三步:初始化渐变功能 */
    ledc_fade_func_install(0);                // 安装渐变服务(参数0表示不分配中断优先级)

    /* 第四步:设置渐变效果 */
    ledc_set_fade_with_time(
        LEDC_LOW_SPEED_MODE,   // 低速模式
        LEDC_CHANNEL_0,        // 通道0
        8192,                  // 目标占空比(对应13bit最大值的100%)
        3000                   // 渐变时间3000ms(3秒)
    );

    /* 第五步:启动渐变 */
    ledc_fade_start(
        LEDC_LOW_SPEED_MODE,
        LEDC_CHANNEL_0,
        LEDC_FADE_NO_WAIT      // 非阻塞模式(不等待渐变完成)
    );

    /* 第六步:创建事件组用于回调通知 */
    ledc_event_handle = xEventGroupCreate();  // 创建FreeRTOS事件组

    /* 第七步:注册渐变完成回调函数 */
    ledc_cbs_t cbs = 
    {
        .fade_cb = ledc_finish_cb,  // 指定渐变完成时的回调函数
    };
    ledc_cb_register(
        LEDC_LOW_SPEED_MODE,
        LEDC_CHANNEL_0,
        &cbs,                      // 回调结构体
        NULL                       // 无额外参数
    );
}

        然后我们来到我们刚刚创建的回调函数的位置,将其空白填充,首先我们要考虑这个回调函数我们是想要用来干什么,我们来回顾一下我们功能,我们想要实现呼吸灯的效果,也就是LED灯由灭转亮,在由亮转灭,那么我们就需要判断我们什么时候完成由灭转亮,什么时候在完成由亮转灭,这里我们可能会想直接判断 duty(通道计数器的值)是否在最高位或者在最低位不就行了,当最高位触发事件亮标志位,当最低位触发事件暗标志位,然后我们在根据事件标志位在做后续判断:

bool IRAM_ATTR ledc_finish_cb(const ledc_cb_param_t *param, void *user_arg)
{
    BaseType_t taskWoken = pdFALSE;  // 初始化为不需要上下文切换
    
    /* 判断当前占空比状态 */
    if(param->duty == 8192)  // 如果占空比不为0(灯亮状态)
    {
        // 设置全亮事件标志(中断安全版本)
        xEventGroupSetBitsFromISR(
            ledc_event_handle,
            FULL_EV_BIT,
            &taskWoken  // 输出参数,指示是否需要任务切换
        );
    }
    else  // 占空比为0(灯灭状态)
    {
        // 设置全暗事件标志(中断安全版本)
        xEventGroupSetBitsFromISR(
            ledc_event_handle,
            EMPTY_EV_BIT,
            &taskWoken
        );
    }

    /* 返回调度需求标志:
     * - pdTRUE: 需要立即进行任务切换
     * - pdFALSE: 不需要切换
     */
    return taskWoken;
}

        不过这样做有些不方便,因为此时我们设的分辨率对应的最高值是8192,那么我们如果该分辨率了,这里也需要重复修改,并且如果我们不想达到最高这里的值还需要修改,因此我们换个思路,因为我们的渐变是靠 ledc_set_fade_with_time 函数实现的,和我们这里的值没有多大关系,这里的值只是判断是否在此事件,那么我们是不是可以发散一下思维,只要大于0都属于亮事件,需要调用量事件任务函数,简单改一下(当然上面的代码也能完成,只是不方便,可最后自行修改验证):

/**
 * @brief LEDC渐变完成回调函数(在中断上下文中执行)
 * @param param 回调参数结构体指针,包含事件详情
 * @param user_arg 用户自定义参数(未使用)
 * @return 是否需要任务切换(FreeRTOS调度标志)
 * 
 * 功能特性:
 * 1. IRAM_ATTR修饰确保函数放在内部RAM中执行(满足中断响应时间要求)
 * 2. 根据当前占空比状态触发不同事件:
 *    - 占空比>0时触发FULL_EV_BIT(全亮)
 *    - 占空比=0时触发EMPTY_EV_BIT(全暗)
 * 3. 使用FromISR版本的事件组操作函数(中断安全)
 */
bool IRAM_ATTR ledc_finish_cb(const ledc_cb_param_t *param, void *user_arg)
{
    BaseType_t taskWoken = pdFALSE;  // 初始化为不需要上下文切换
    
    /* 判断当前占空比状态 */
    if(param->duty > 0)  // 如果占空比不为0(灯亮状态)
    {
        // 设置全亮事件标志(中断安全版本)
        xEventGroupSetBitsFromISR(
            ledc_event_handle,
            FULL_EV_BIT,
            &taskWoken  // 输出参数,指示是否需要任务切换
        );
    }
    else  // 占空比为0(灯灭状态)
    {
        // 设置全暗事件标志(中断安全版本)
        xEventGroupSetBitsFromISR(
            ledc_event_handle,
            EMPTY_EV_BIT,
            &taskWoken
        );
    }

    /* 返回调度需求标志:
     * - pdTRUE: 需要立即进行任务切换
     * - pdFALSE: 不需要切换
     */
    return taskWoken;
}

        当然我们也可以直接声明宏定义,直接对宏定义修改,方法皆可,可自己验证一下。

4.3.5  LED任务函数

        上面的初始化的一些配置已经完成了,下面该具体任务了,首先我们需要等待时间的发生,调用事件等待函数,注意声明一个变量用来存储事件组返回值:

    EventBits_t ev;  // 用于存储事件组返回的事件位

        /* 等待事件发生(最多阻塞5秒) */
        ev = xEventGroupWaitBits(
            ledc_event_handle,        // 事件组句柄
            FULL_EV_BIT | EMPTY_EV_BIT, // 监听两个事件位
            pdTRUE,                   // 清除已触发的事件位
            pdFALSE,                  // 不需要所有事件位都触发
            pdMS_TO_TICKS(5000)       // 超时时间5秒
        );

        然后就是处理亮事件和暗事件了,具体思路:

        也就是通过if语句判断,如果到达亮或者暗事件,通过调用 ledc_set_fade_with_time 和 ledc_fade_start 改变渐变状态,然后重新将参数写入,函数如下:

/**
 * @brief LED运行测试任务函数
 * @param param FreeRTOS任务参数(未使用)
 * 
 * 功能描述:
 * 1. 通过事件组监听LED状态变化事件
 * 2. 根据事件触发LED亮度渐变效果
 * 3. 实现LED在"全亮"和"全暗"状态间的自动切换
 */
void LED_run_test(void* param)
{
    EventBits_t ev;  // 用于存储事件组返回的事件位
    
    while(1)  // 无限循环
    {
        /* 等待事件发生(最多阻塞5秒) */
        ev = xEventGroupWaitBits(
            ledc_event_handle,        // 事件组句柄
            FULL_EV_BIT | EMPTY_EV_BIT, // 监听两个事件位
            pdTRUE,                   // 清除已触发的事件位
            pdFALSE,                  // 不需要所有事件位都触发
            pdMS_TO_TICKS(5000)       // 超时时间5秒
        );
        
        /* 处理全亮事件 */
        if(ev & FULL_EV_BIT)  // 如果收到全亮事件
        {
            // 设置3秒内渐变到全暗(占空比0)
            ledc_set_fade_with_time(
                LEDC_LOW_SPEED_MODE,  // 低速模式
                LEDC_CHANNEL_0,        // 通道0
                0,                    // 目标占空比(全暗)
                3000                   // 渐变时间3000ms
            );
            
            // 启动渐变(非阻塞模式)
            ledc_fade_start(
                LEDC_LOW_SPEED_MODE,
                LEDC_CHANNEL_0,
                LEDC_FADE_NO_WAIT      // 不等待渐变完成
            );
        }
        
        /* 处理全暗事件 */
        if(ev & EMPTY_EV_BIT)  // 如果收到全暗事件
        {
            // 设置3秒内渐变到全亮(占空比8192对应13bit最大值)
            ledc_set_fade_with_time(
                LEDC_LOW_SPEED_MODE,
                LEDC_CHANNEL_0,
                8192,                 // 目标占空比(全亮)
                3000                   // 渐变时间3000ms
            );
            
            // 启动渐变(非阻塞模式)
            ledc_fade_start(
                LEDC_LOW_SPEED_MODE,
                LEDC_CHANNEL_0,
                LEDC_FADE_NO_WAIT
            );
        }

        /* 每次循环重新注册回调函数 */
        ledc_cbs_t cbs = 
        {
            .fade_cb = ledc_finish_cb,  // 指定渐变完成回调
        };
        ledc_cb_register(
            LEDC_LOW_SPEED_MODE,
            LEDC_CHANNEL_0,
            &cbs,      // 回调结构体
            NULL       // 无额外参数
        );
    }
}

4.3.6  主函数

        让我们来看看完整代码:

#include <stdio.h>
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "driver/ledc.h"

/* LED控制引脚定义 */
#define LED_GPIO_PIN        GPIO_NUM_1  // 使用GPIO1作为LED控制引脚

/* 事件标志位定义 */
#define FULL_EV_BIT BIT0      // 全亮事件标志位(二进制01)
#define EMPTY_EV_BIT  BIT1    // 全暗事件标志位(二进制10)

/* 事件组句柄声明 */
static EventGroupHandle_t ledc_event_handle;  // 用于PWM渐变事件通知的FreeRTOS事件组

/**
 * @brief LEDC渐变完成回调函数(在中断上下文中执行)
 * @param param 回调参数结构体指针,包含事件详情
 * @param user_arg 用户自定义参数(未使用)
 * @return 是否需要任务切换(FreeRTOS调度标志)
 * 
 * 功能特性:
 * 1. IRAM_ATTR修饰确保函数放在内部RAM中执行(满足中断响应时间要求)
 * 2. 根据当前占空比状态触发不同事件:
 *    - 占空比>0时触发FULL_EV_BIT(全亮)
 *    - 占空比=0时触发EMPTY_EV_BIT(全暗)
 * 3. 使用FromISR版本的事件组操作函数(中断安全)
 */
bool IRAM_ATTR ledc_finish_cb(const ledc_cb_param_t *param, void *user_arg)
{
    BaseType_t taskWoken = pdFALSE;  // 初始化为不需要上下文切换
    
    /* 判断当前占空比状态 */
    if(param->duty > 0)  // 如果占空比不为0(灯亮状态)
    {
        // 设置全亮事件标志(中断安全版本)
        xEventGroupSetBitsFromISR(
            ledc_event_handle,
            FULL_EV_BIT,
            &taskWoken  // 输出参数,指示是否需要任务切换
        );
    }
    else  // 占空比为0(灯灭状态)
    {
        // 设置全暗事件标志(中断安全版本)
        xEventGroupSetBitsFromISR(
            ledc_event_handle,
            EMPTY_EV_BIT,
            &taskWoken
        );
    }

    /* 返回调度需求标志:
     * - pdTRUE: 需要立即进行任务切换
     * - pdFALSE: 不需要切换
     */
    return taskWoken;
}

//LED初始化
void LED_Init(void)
{
    gpio_config_t io_conf;// 定义 GPIO 配置结构体
    
    io_conf.pin_bit_mask = (1ULL << LED_GPIO_PIN); // 设置要配置的 GPIO 引脚(使用位掩码形式)
    io_conf.mode = GPIO_MODE_OUTPUT;               // 设置 GPIO 模式为输出(既能读取也能控制电平)
    io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;  // 禁用下拉电阻
    io_conf.pull_up_en = GPIO_PULLUP_DISABLE;      // 禁用上拉电阻  
    io_conf.intr_type = GPIO_INTR_DISABLE;         // 禁用 GPIO 中断功能
    
    gpio_config(&io_conf);// 应用 GPIO 配置
}

/**
 * @brief LED运行测试任务函数
 * @param param FreeRTOS任务参数(未使用)
 * 
 * 功能描述:
 * 1. 通过事件组监听LED状态变化事件
 * 2. 根据事件触发LED亮度渐变效果
 * 3. 实现LED在"全亮"和"全暗"状态间的自动切换
 */
void LED_run_test(void* param)
{
    EventBits_t ev;  // 用于存储事件组返回的事件位
    
    while(1)  // 无限循环
    {
        /* 等待事件发生(最多阻塞5秒) */
        ev = xEventGroupWaitBits(
            ledc_event_handle,        // 事件组句柄
            FULL_EV_BIT | EMPTY_EV_BIT, // 监听两个事件位
            pdTRUE,                   // 清除已触发的事件位
            pdFALSE,                  // 不需要所有事件位都触发
            pdMS_TO_TICKS(5000)       // 超时时间5秒
        );
        
        /* 处理全亮事件 */
        if(ev & FULL_EV_BIT)  // 如果收到全亮事件
        {
            // 设置3秒内渐变到全暗(占空比0)
            ledc_set_fade_with_time(
                LEDC_LOW_SPEED_MODE,  // 低速模式
                LEDC_CHANNEL_0,        // 通道0
                0,                    // 目标占空比(全暗)
                3000                   // 渐变时间3000ms
            );
            
            // 启动渐变(非阻塞模式)
            ledc_fade_start(
                LEDC_LOW_SPEED_MODE,
                LEDC_CHANNEL_0,
                LEDC_FADE_NO_WAIT      // 不等待渐变完成
            );
        }
        
        /* 处理全暗事件 */
        if(ev & EMPTY_EV_BIT)  // 如果收到全暗事件
        {
            // 设置3秒内渐变到全亮(占空比8192对应13bit最大值)
            ledc_set_fade_with_time(
                LEDC_LOW_SPEED_MODE,
                LEDC_CHANNEL_0,
                8192,                 // 目标占空比(全亮)
                3000                   // 渐变时间3000ms
            );
            
            // 启动渐变(非阻塞模式)
            ledc_fade_start(
                LEDC_LOW_SPEED_MODE,
                LEDC_CHANNEL_0,
                LEDC_FADE_NO_WAIT
            );
        }

        /* 每次循环重新注册回调函数 */
        ledc_cbs_t cbs = 
        {
            .fade_cb = ledc_finish_cb,  // 指定渐变完成回调
        };
        ledc_cb_register(
            LEDC_LOW_SPEED_MODE,
            LEDC_CHANNEL_0,
            &cbs,      // 回调结构体
            NULL       // 无额外参数
        );
    }
}

/**
 * @brief LED PWM控制器(LEDC)初始化函数
 * 
 * 功能描述:
 * 1. 配置LEDC定时器参数
 * 2. 设置LEDC通道参数
 * 3. 启用PWM渐变功能
 * 4. 配置渐变结束回调
 */
void LEDC_PWM_Init(void)
{
    /* 第一步:配置LEDC定时器 */
    ledc_timer_config_t ledc_timer = 
    {
        .speed_mode = LEDC_LOW_SPEED_MODE,     // 使用低速模式
        .timer_num = LEDC_TIMER_0,             // 选择定时器0
        .clk_cfg = LEDC_AUTO_CLK,              // 自动选择时钟源
        .freq_hz = 5000,                       // 设置PWM频率为5kHz
        .duty_resolution = LEDC_TIMER_13_BIT,  // 13位分辨率(8192级亮度控制)
    };
    ledc_timer_config(&ledc_timer);            // 应用定时器配置

    /* 第二步:配置LEDC通道 */
    ledc_channel_config_t ledc_channel = 
    {
        .speed_mode = LEDC_LOW_SPEED_MODE,     // 低速模式
        .channel = LEDC_CHANNEL_0,             // 使用通道0
        .timer_sel = LEDC_TIMER_0,             // 绑定到定时器0
        .gpio_num = LED_GPIO_PIN,              // 指定GPIO引脚
        .duty = 0,                            // 初始占空比为0(全暗)
        .intr_type = LEDC_INTR_DISABLE,       // 禁用中断
    };
    ledc_channel_config(&ledc_channel);        // 应用通道配置

    /* 第三步:初始化渐变功能 */
    ledc_fade_func_install(0);                // 安装渐变服务(参数0表示不分配中断优先级)

    /* 第四步:设置渐变效果 */
    ledc_set_fade_with_time(
        LEDC_LOW_SPEED_MODE,   // 低速模式
        LEDC_CHANNEL_0,        // 通道0
        8192,                  // 目标占空比(对应13bit最大值的100%)
        3000                   // 渐变时间3000ms(3秒)
    );

    /* 第五步:启动渐变 */
    ledc_fade_start(
        LEDC_LOW_SPEED_MODE,
        LEDC_CHANNEL_0,
        LEDC_FADE_NO_WAIT      // 非阻塞模式(不等待渐变完成)
    );

    /* 第六步:创建事件组用于回调通知 */
    ledc_event_handle = xEventGroupCreate();  // 创建FreeRTOS事件组

    /* 第七步:注册渐变完成回调函数 */
    ledc_cbs_t cbs = 
    {
        .fade_cb = ledc_finish_cb,  // 指定渐变完成时的回调函数
    };
    ledc_cb_register(
        LEDC_LOW_SPEED_MODE,
        LEDC_CHANNEL_0,
        &cbs,                      // 回调结构体
        NULL                       // 无额外参数
    );
}

void app_main(void)
{
    LED_Init();
    LEDC_PWM_Init();

    xTaskCreatePinnedToCore(
        LED_run_test,    // 任务函数指针(LED控制主逻辑)
        "led",          // 任务名称(用于调试)
        2048,           // 任务栈大小(字节)
        NULL,           // 任务参数(未使用)
        3,              // 任务优先级(数字越大优先级越高)
        NULL,           // 任务句柄(不保存)
        1               // 指定运行在核心1(ESP32双核特性)
    );
}

        看一下效果:

        完成,结束。

FreeRTOS实时操作系统_时光の尘的博客-优快云博客

ESP32学习笔记_时光の尘的博客-优快云博客

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

时光の尘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值