
目录
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的可以跳转:
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灯
对于文件的创建这些,这里不在做过多赘述,想要了解如何创建一个新工程的可以查看:
下面的点亮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的但是基本原理一样:
主函数代码:
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的时候,大概原理是一样的:
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双核特性)
);
}
看一下效果:

完成,结束。


896

被折叠的 条评论
为什么被折叠?



