一、PWM简介
PWM(Pulse Width Modulation),简称脉宽调制,是一种将模拟信号变为脉冲信号的计数。PWM 可以控制 LED 亮度、直流电机的转速等。
PWM 的主要参数如下:
- PWM 频率。PWM 频率是 PWM 信号在 1s 内从高电平到低电平再回到高电平的次数,也就是说 1s 内有多少个 PWM 周期,单位为 Hz。
- PWM 周期。PWM 周期是 PWM 频率的倒数,即 \(T = \frac{1}{f}\),T 是 PWM 周期,f 是 PWM 频率。
- PWM 占空比。PWM 占空比是指在一个 PWM 周期内,高电平的时间与整个周期时间的比例,取值范围为 0%~100%。
二、LED PWM控制器介绍
ESP32 S3 的 LED PWM 控制器,简写为 LEDC,用于生成控制 LED 的脉冲宽度调制信号。LED PWM 控制器具有八个独立的 PWM 生成器(即八个通道)。每个 PWM 生成器会从四个通用定时器中选择一个,以该定时器的计数值作为基准生成 PWM 信号。
LED PWM 定时器如下图所示。
为了实现 PWM 输出,先需要设置指定通道的 PWM 参数:频率、分辨率、占空比,然后将该通道映射到指定引脚,该引脚输出对应通道的 PWM 信号,通道和引脚的关系所下图所示。
LED PWM 控制器可在没有 CPU 干预的情况下自动改变占空比,实现亮度以及颜色渐变。
三、LED PWM常用的函数
ESP-IDF 提供了一套 API 来配置 PWM。要使用此功能,需要导入必要的头文件:
#include "driver/ledc.h"
3.1、配置LEDC使用的定时器
我们可以使用 ledc_timer_config()
函数 配置 LEDC使用的定时器,其函数原型如下:
/*** @brief 配置LEDC使用的定时器* * @param timer_conf 配置 LEDC 定时器的结构体指针* @return esp_err_t ESP_OK表示配置成功,其它配置失败*/
esp_err_t ledc_timer_config(const ledc_timer_config_t *timer_conf);
形参 timer_conf
指向配置 LEDC 定时器的结构体指针,它的定义如下:
typedef struct
{ledc_mode_t speed_mode; // 速度模式ledc_timer_bit_t duty_resolution; // PWM占空比分辨率ledc_timer_t timer_num; // 通道的定时器源uint32_t freq_hz; // PWM信号频率,单位为Hzledc_clk_cfg_t clk_cfg; // LED PWM的时钟来源bool deconfigure;
} ledc_timer_config_t;
成员 speed_mode
用来 设置速度模式,它的可选值如下:
typedef enum
{
#if SOC_LEDC_SUPPORT_HS_MODELEDC_HIGH_SPEED_MODE = 0, /*!< LEDC high speed speed_mode */
#endifLEDC_LOW_SPEED_MODE, /*!< LEDC low speed speed_mode */LEDC_SPEED_MODE_MAX, /*!< LEDC speed limit */
} ledc_mode_t;
ESP32 S3 仅支持低速模式。
成员 duty_resolution
用来 设置 PWM 占空比分辨率。
typedef enum
{LEDC_TIMER_1_BIT = 1, /*!< LEDC PWM duty resolution of 1 bits */LEDC_TIMER_2_BIT, /*!< LEDC PWM duty resolution of 2 bits */LEDC_TIMER_3_BIT, /*!< LEDC PWM duty resolution of 3 bits */LEDC_TIMER_4_BIT, /*!< LEDC PWM duty resolution of 4 bits */LEDC_TIMER_5_BIT, /*!< LEDC PWM duty resolution of 5 bits */LEDC_TIMER_6_BIT, /*!< LEDC PWM duty resolution of 6 bits */LEDC_TIMER_7_BIT, /*!< LEDC PWM duty resolution of 7 bits */LEDC_TIMER_8_BIT, /*!< LEDC PWM duty resolution of 8 bits */LEDC_TIMER_9_BIT, /*!< LEDC PWM duty resolution of 9 bits */LEDC_TIMER_10_BIT, /*!< LEDC PWM duty resolution of 10 bits */LEDC_TIMER_11_BIT, /*!< LEDC PWM duty resolution of 11 bits */LEDC_TIMER_12_BIT, /*!< LEDC PWM duty resolution of 12 bits */LEDC_TIMER_13_BIT, /*!< LEDC PWM duty resolution of 13 bits */LEDC_TIMER_14_BIT, /*!< LEDC PWM duty resolution of 14 bits */
#if SOC_LEDC_TIMER_BIT_WIDTH > 14LEDC_TIMER_15_BIT, /*!< LEDC PWM duty resolution of 15 bits */LEDC_TIMER_16_BIT, /*!< LEDC PWM duty resolution of 16 bits */LEDC_TIMER_17_BIT, /*!< LEDC PWM duty resolution of 17 bits */LEDC_TIMER_18_BIT, /*!< LEDC PWM duty resolution of 18 bits */LEDC_TIMER_19_BIT, /*!< LEDC PWM duty resolution of 19 bits */LEDC_TIMER_20_BIT, /*!< LEDC PWM duty resolution of 20 bits */
#endifLEDC_TIMER_BIT_MAX,
} ledc_timer_bit_t;
成员 timer_num
用来 设置通道的定时器源,它的可选值如下:
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;
成员 clk_cfg
用来 设置 LED PWM 的时钟来源,它的可选值如下:
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_REF_TICK = SOC_MOD_CLK_REF_TICK, /*!< Select REF_TICK 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;
typedef soc_periph_ledc_clk_src_legacy_t ledc_clk_cfg_t;
LED PWM 控制器主要用于驱动 LED。该控制器 PWM 占空比设置的分辨率范围较广。比如,PWM 频率为 5 kHz 时,占空比分辨率最大可为 13 位。这意味着占空比可为 0 至 100% 之间的任意值,分辨率为 ~0.012%(\(2^{13} = 8192\) LED 亮度的离散电平)。然而,这些参数取决于为 LED PWM 控制器定时器计时的时钟信号,LED PWM 控制器为通道提供时钟。
PWM 频率越高,占空比分辨率越低,反之亦然。
3.2、通道配置函数
我们可以使用 ledc_channel_config()
函数 配置 LEDC 的通道,其函数原型如下:
/*** @brief 配置LEDC通道* * @param ledc_conf 指向配置LEDC通道的结构体指针* @return esp_err_t ESP_OK表示配置成功,其它配置失败*/
esp_err_t ledc_channel_config(const ledc_channel_config_t *ledc_conf);
成员 ledc_conf
是 指向配置LEDC通道的结构体指针,它的定义如下:
typedef struct
{int gpio_num; // 配置输出引脚ledc_mode_t speed_mode; // 速度模式ledc_channel_t channel; // LEDC的输出通道ledc_intr_type_t intr_type; // 配置中断ledc_timer_t timer_sel; // 选择通道的定时器源uint32_t duty; // LEDC 通道的占空比设置int hpoint; // 表示占空比对应的时钟计数值struct {unsigned int output_invert: 1; // 启用(1)或禁用(0)gpio输出反相} flags;
} ledc_channel_config_t;
成员 channel
用来 设置 LEDC 的输出通道,它的可选值如下:
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 > 6LEDC_CHANNEL_6, /*!< LEDC channel 6 */LEDC_CHANNEL_7, /*!< LEDC channel 7 */
#endifLEDC_CHANNEL_MAX,
} ledc_channel_t;
成员 intr_type
用来 配置中断,它的可选值如下:
typedef enum
{LEDC_INTR_DISABLE = 0, /*!< Disable LEDC interrupt */LEDC_INTR_FADE_END, /*!< Enable LEDC interrupt */LEDC_INTR_MAX,
} ledc_intr_type_t;
成员 timer_sel
用来 选择通道的定时器源,它的可选值如下:
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;
成员 duty
用来 设置 LEDC 通道的占空比设置,占空比设定范围为 0 ~ \(2^{duty\_resolution}\)。
3.3、改变PWM占空比
我们可以调用函数 ledc_set_duty()
来 设置新的占空比。之后,调用函数 ledc_update_duty()
使 新配置的占空比生效。要 查看当前设置的占空比,可使用 ledc_get_duty()
函数进行查看,该函数原型如下所示:
/*** @brief 设置LEDC的占空比* * @param speed_mode 速度模式* @param channel LEDC通道* @param duty 占空比* @return esp_err_t ESP_OK表示配置成功,其它配置失败*/
esp_err_t ledc_set_duty(ledc_mode_t speed_mode, ledc_channel_t channel, uint32_t duty);
/*** @brief 更新LEDC的占空比* * @param speed_mode 速度模式* @param channel LEDC通道* @return esp_err_t ESP_OK表示更新成功,其它更新失败*/
esp_err_t ledc_update_duty(ledc_mode_t speed_mode, ledc_channel_t channel);
/*** @brief 获取LEDC的占空比* * @param speed_mode 速度模式* @param channel LEDC通道* @return uint32_t 占空比*/
uint32_t ledc_get_duty(ledc_mode_t speed_mode, ledc_channel_t channel);
3.4、使能渐变
LED PWM 控制器硬件可逐渐改变占空比的数值。开启此功能,需要用函数 ledc_fade_func_install()
来 使能渐变,该函数原型如下所示:
/*** @brief 使能渐变* * @param intr_alloc_flags 用于分配中断的标志* @return esp_err_t ESP_OK表示配置成功,其它表示配置失败*/
esp_err_t ledc_fade_func_install(int intr_alloc_flags);
3.5、设置LEDC渐变功能
经过上一步渐变功能的配置后,我们还需要使用 函数 设置占空比以及渐变时长,该函数原型如下所示:
/*** @brief 设置LEDC渐变功能* * @param speed_mode 速度模式选择* @param channel LEDC通道* @param target_duty 目标占空比* @param max_fade_time_ms 最大渐变时间* @return esp_err_t ESP_OK表示配置成功,其它表示配置失败*/
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);
3.6、开启渐变
设置占空比以及渐变时长后,便可以使用 `` 函数开启渐变功能,该函数原型如下所示:
/*** @brief 开启渐变* * @param speed_mode 速度模式选择* @param channel LEDC通道* @param fade_mode 渐变模式* @return esp_err_t ESP_OK表示配置成功,其它表示配置失败*/
esp_err_t ledc_fade_start(ledc_mode_t speed_mode, ledc_channel_t channel, ledc_fade_mode_t fade_mode);
形参 fade_mode
表示 渐变模式,它的可选值如下:
typedef enum
{LEDC_FADE_NO_WAIT = 0, /*!< LEDC fade function will return immediately */LEDC_FADE_WAIT_DONE, /*!< LEDC fade function will block until fading to the target duty */LEDC_FADE_MAX,
} ledc_fade_mode_t;
四、实验例程
这里,我们在【components】文件夹下的【peripheral】文件夹下的【inc】文件夹(用来存放头文件)新建一个 bsp_ledc.h
文件,在【components】文件夹下的【peripheral】文件夹下的【src】文件夹(用来存放源文件)新建一个 bsp_ledc.c
文件。
#ifndef __BSP_LEDC_H__
#define __BSP_LEDC_H__#include "driver/ledc.h"void bsp_ledc_init(ledc_timer_t time_num, ledc_channel_t ledc_channel, int gpio_num, ledc_timer_bit_t resolution, uint32_t freq, uint32_t duty);
void bsp_ledc_set_pwm_duty(ledc_channel_t ledc_channel, uint32_t duty);
void bsp_ledc_set_gradient_range(ledc_channel_t ledc_channel, uint32_t start_value, uint32_t end_value, int gradient_time);#endif // !__BSP_LEDC_H__
#include "bsp_ledc.h"/*** @brief LEDC初始化* * @param time_num 定时器编号* @param ledc_channel LEDC通道编号* @param gpio_num GPIO引脚编号* @param resolution LEDC占空比分辨率* @param freq LEDC频率* @param duty LEDC占空比*/
void bsp_ledc_init(ledc_timer_t time_num, ledc_channel_t ledc_channel, int gpio_num, ledc_timer_bit_t resolution, uint32_t freq, uint32_t duty)
{ledc_timer_config_t ledc_timer_config_struct = {0};ledc_channel_config_t ledc_channel_config_struct = {0};// 配置LEDC定时器ledc_timer_config_struct.clk_cfg = LEDC_AUTO_CLK; // LEDC时钟源选择ledc_timer_config_struct.timer_num = time_num; // PWM定时器编号ledc_timer_config_struct.duty_resolution = resolution; // PWM占空比分辨率ledc_timer_config_struct.freq_hz = freq; // PWM频率ledc_timer_config_struct.speed_mode = LEDC_LOW_SPEED_MODE; // LEDC控制器工作模式ledc_timer_config(&ledc_timer_config_struct); // 配置定时器// 配置LEDC通道ledc_channel_config_struct.timer_sel = time_num; // LEDC控制器通道对应定时器编号ledc_channel_config_struct.gpio_num = gpio_num; // LEDC控制器通道对应引脚ledc_channel_config_struct.channel = ledc_channel; // LEDC控制器通道编号ledc_channel_config_struct.speed_mode = LEDC_LOW_SPEED_MODE; // LEDC控制器通道工作模式ledc_channel_config_struct.intr_type = LEDC_INTR_DISABLE; // LEDC失能中断ledc_channel_config_struct.duty = duty; // LEDC通道占空比ledc_channel_config(&ledc_channel_config_struct); // 配置通道
}/*** @brief LEDC设置占空比* * @param ledc_channel LEDC通道编号* @param duty LEDC占空比*/
void bsp_ledc_set_pwm_duty(ledc_channel_t ledc_channel, uint32_t duty)
{ledc_set_duty(LEDC_LOW_SPEED_MODE, ledc_channel, duty);ledc_update_duty(LEDC_LOW_SPEED_MODE, ledc_channel);
}/*** @brief LEDC设置渐变占空比* * @param ledc_channel LEDC通道编号* @param start_value 起始占空比* @param end_value 结束占空比* @param gradient_time 渐变时长*/
void bsp_ledc_set_gradient_range(ledc_channel_t ledc_channel, uint32_t start_value, uint32_t end_value, int gradient_time)
{// 设置占空比以及渐变时长ledc_set_fade_with_time(LEDC_LOW_SPEED_MODE, ledc_channel, start_value, gradient_time);// 开始渐变ledc_fade_start(LEDC_LOW_SPEED_MODE, ledc_channel, LEDC_FADE_NO_WAIT);ledc_set_fade_with_time(LEDC_LOW_SPEED_MODE, ledc_channel, end_value, gradient_time);ledc_fade_start(LEDC_LOW_SPEED_MODE, ledc_channel, LEDC_FADE_NO_WAIT);
}
修改【main】文件夹下的 main.c
文件。
#include "freertos/FreeRTOS.h"#include "bsp_ledc.h"#include "led.h"// app_main()函数是ESP32的入口函数,它是FreRTOS的一个任务,任务优先级是1
// main()函数是C语言入口函数,它会在编译过程中插入到二进制文件中的
void app_main(void)
{led_init(GPIO_NUM_0);bsp_ledc_init(LEDC_TIMER_0, LEDC_CHANNEL_0, GPIO_NUM_0, LEDC_TIMER_10_BIT, 1000, 0);ledc_fade_func_install(0); // 使能渐变while (1){bsp_ledc_set_gradient_range(LEDC_CHANNEL_0, 0, 1000, 3000);// 将一个任务延迟给定的滴答数,IDF中提供pdMS_TO_TICKS可以将指定的ms转换为对应的tick数vTaskDelay(pdMS_TO_TICKS(10));}
}
在 首次配置 LEDC 时,建议先配置定时器(调用函数
ledc_timer_config()
),再配置通道(调用函数ledc_channel_config()
)。这样可以确保 IO 引脚上的 PWM 信号自输出开始那一刻起,其频率就是正确的。PWM 频率越高,占空比分辨率越低,反之亦然。
LED PWM 控制器 API 会在设定的频率和占空比分辨率超过 LED PWM 控制器硬件范围时,ED PWM 驱动器会报错,例如:
E (196) ledc: requested frequency and duty resolution cannot be achieved, try reducing freq_hz or duty_resolution. div_param=128
。LED PWM 控制器 API 会在设定的频率和占空比分辨率低于 LED PWM 控制器硬件范围时,LED PWM 驱动器也会报错,例如:
E (196) ledc: requested frequency and duty resolution cannot be achieved, try increasing freq_hz or duty_resolution. div_param=128000000
。