10. PWM输出
一、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%。
二、ESP32的LEDC控制器
ESP32 S3 的 LED PWM 控制器,简写为 LEDC,用于生成控制 LED 的脉冲宽度调制信号。LED PWM 控制器具有八个独立的 PWM 生成器(即八个通道)。每个 PWM 控制器内置比较器,可以将计数器的值与设定的占空比进行比较,从而输出所需的 PWM 波形。每个 PWM 生成器会从四个通用定时器中选择一个,以该定时器的计数值作为基准生成 PWM 信号。
LEDC 具有以下几个主要特性:
- 多通道支持:LEDC 支持多达八个独立的 PWM 通道,每个通道可以独立配置,实现对多个输出设备的控制。
- 高分辨率占空比:LEDC 可提供 14 位的占空比分辨率,能够精确调节输出信号的高低电平,从而实现更细腻的亮度控制。
- 渐变功能:通过自动占空比渐变功能,LEDC 能够在不干预处理器的情况下,实现信号输出的平滑过渡。
- 低功耗模式:在低功耗下,LEDC 依然可以稳定输出 PWM 信号,有效降低整体能耗。
- 丰富的配置选项:LEDC 支持多个时钟源和小数分频配置,允许用户根据具体需求灵活调整输出频率。
LED PWM 定时器如下图所示。
LED PWM(脉宽调制)模块的架构由四个定时器(Timer0 到 Timer3)和多个 PWM 生成器(PWM0 到 PWM7)组成。这种设计允许每个 PWM 生成器独立配置和使用其对应的定时器,从而实现灵活的 PWM 信号控制。该架构还包含事件和任务信号输入,支持对 PWM 信号进行更复杂的控制。例如,事件输入可以用来触发特定的 PWM 操作,而任务信号则用于控制 PWM 的开启、停止或其他功能。
为了实现 PWM 输出,先需要设置指定通道的 PWM 参数:频率、分辨率、占空比,然后将该通道映射到指定引脚,该引脚输出对应通道的 PWM 信号,通道和引脚的关系所下图所示。
LEDC 模块主要由三个部分组成:时钟源选择、定时器计数和 PWM 输出生成。
【1】、时钟源选择
LEDC 控制器中的四个计时器可以选择以下三种时钟源之一作为时钟源,它们分别为 APB_CLK
、RC_FAST_CLK
和 XTAL_CLK
时钟源,从上图所示,我们可通过 LEDC_CONF_REG
寄存器中的 LEDC_APB_CLK_SE
字段来控制。
完成上述设置后,LEDC_CLK 信号将传递至时钟分频器(18 位分频器),为计数器提供所需的时钟频率。分频器的分频系数可通过 LEDC_TIMERx_CONF_REG
寄存器中的 LEDC_CLK_DIV_TIMERx
字段进行配置。该字段由 18 位组成,其中高 10 位表示整数分频系数(22:13 位),低 8 位表示小数分频系数(12:5 位)。
分频值 LEDC_CLK_DIV 的计算方式如下:
配置完成后,结合 LEDC_CLK 时钟和 LEDC_CLK_DIV 分频系数,可以得到以下公式:
【2】、定时器计数
四个定时器包含一个 14 位可调位宽的基准计数器,使用计数器的时钟频率作为参考时钟。我们可通过操作 LEDC_TIMERx_CONF_REG
寄存器中的 LEDC_TIMERx_DUTY_RES
字段配置了实际使用的计数器位宽。因此,PWM 信号的最大分辨率为 14 位。计数器从 0 开始递增,最大计数到 \(2^{LEDC\_TIMERx\_DUTY_RES - 1}\),然后溢出并重新开始计数。计数器的值可以通过软件读取、重置和暂停。
要控制计数器的暂停和重置,可以通过配置 LEDC_TIMERx_CONF_REG
中的 LEDC_TIMERx_PAUSE
和 LEDC_TIMERx_RST
字段来实现。具体而言,LEDC_TIMERx_PAUSE
字段可用于暂停计数器的运行,将其设置为 1 时,计数器将停止计数;而 LEDC_TIMERx_RST
字段则用于重置计数器,将其设置为 1 时,计数器会被重置。
最后,可以通过配置 LEDC_CHn_CONF0_REG
寄存器中的 LEDC_TIMER_SEL_CHn
字段来选择用于 PWM 生成器的计数器。具体而言,该字段允许用户指定所需的计数器,以便为 PWM 信号提供计数值。通过灵活配置该字段,可以实现不同计数器与 PWM 生成器之间的关联,从而满足各种应用需求,该字段描述如下所示。
LED PWM 控制器可在没有 CPU 干预的情况下自动改变占空比,实现亮度以及颜色渐变。
三、输出PWM
为了生成 PWM 信号,需要一个 PWM 生成器(PWMn)和一个 定时器(Timerx)。每个 PWM 生成器可以通过设置 LEDC_TIMER_SEL_CHn
来单独配置,以使用四个定时器中的一个来生成 PWM 输出。PWM 信号生成如下图所示。
每个 PWM 生成器都包含一个比较器和两个多路复用器。PWM 生成器将定时器的 14 位计数器值(Timerx_cnt)与两个触发值 Hpointn 和 Lpointn 进行比较。当定时器的计数器值等于 Hpointn 或 Lpointn 时,PWM 信号分别为高或低,具体如下:
- 如果 Timerx_cnt == Hpointn,则 sig_outn 为 1。
- 如果 Timerx_cnt == Lpointn,则 sig_outn 为 0。
上图展示了 Hpointn 和 Lpointn 如何用于生成固定占空比的 PWM 输出信号,其中 Hpointn 数值由 LEDC_CHn_HPOINT_REG
寄存器中的 LEDC_HPOINT_CHn
字段配置,该字段描述如下:
而 Lpointn 的数值由 LEDC_CHn_DUTY_REG
寄存器中的 LEDC_DUTY_CHn[18:0]
字段和 LEDC_HPOINT_CHn
字段的和计算得出。LEDC_DUTY_CHn[18:0]
字段描述如下。
最后,我们通过 LEDC_CHn_CONF0_REG
寄存器中的 LEDC_SIG_OUT_EN_CHn
和 LEDC_IDLE_LV_CHn
字段来配置 PWM 在空闲状态下的电平以及开启 PWM 通道输出。
四、LED PWM常用的函数
ESP-IDF 提供了一套 API 来配置 PWM。要使用此功能,需要导入必要的头文件:
#include "driver/ledc.h"
4.1、配置LEDC使用的定时器
我们可以使用 ledc_timer_config()
函数 配置 LEDC使用的定时器,其函数原型如下:
/**
* @brief 配置LEDC使用的定时器
*
* @param timer_conf 配置 LEDC 定时器的结构体指针
* @return esp_err_t ESP_OK 表示配置成功
* ESP_ERR_INVALID_ARG 表示参数错误
* ESP_FAIL 表示基于给定的频率和当前的占空比分辨率无法找到合适的预分频器数字
* ESP_ERR_INVALID_STATE 表示因定时器尚未配置或未暂停,定时器无法被去配置
*/
esp_err_t ledc_timer_config(const ledc_timer_config_t *timer_conf);
形参 timer_conf
指向配置 LEDC 定时器的结构体指针,它的定义如下:
typedef struct
{
ledc_mode_t speed_mode; // LEDC速度模式
ledc_timer_bit_t duty_resolution; // LEDC通道的占空比分辨率
ledc_timer_t timer_num; // 通道的定时器源
uint32_t freq_hz; // LEDC定时器频率,单位为Hz
ledc_clk_cfg_t clk_cfg; // LEDC的时钟来源
bool deconfigure; // 是否去配置之前已配置的LEDC定时器
} ledc_timer_config_t;
成员 speed_mode
用来 设置速度模式,它的可选值如下:
typedef enum
{
#if SOC_LEDC_SUPPORT_HS_MODE
LEDC_HIGH_SPEED_MODE = 0, // 高速模式
#endif
LEDC_LOW_SPEED_MODE, // 低速模式
LEDC_SPEED_MODE_MAX, // 速度临界
} ledc_mode_t;
ESP32 S3 仅支持低速模式。
成员 duty_resolution
用来 设置 PWM 占空比分辨率。
typedef enum
{
LEDC_TIMER_1_BIT = 1, // LEDC PWM 占空比分辨率为 1 bits
LEDC_TIMER_2_BIT, // LEDC PWM 占空比分辨率为 2 bits
LEDC_TIMER_3_BIT, // LEDC PWM 占空比分辨率为 3 bits
LEDC_TIMER_4_BIT, // LEDC PWM 占空比分辨率为 4 bits
LEDC_TIMER_5_BIT, // LEDC PWM 占空比分辨率为 5 bits
LEDC_TIMER_6_BIT, // LEDC PWM 占空比分辨率为 6 bits
LEDC_TIMER_7_BIT, // LEDC PWM 占空比分辨率为 7 bits
LEDC_TIMER_8_BIT, // LEDC PWM 占空比分辨率为 8 bits
LEDC_TIMER_9_BIT, // LEDC PWM 占空比分辨率为 9 bits
LEDC_TIMER_10_BIT, // LEDC PWM 占空比分辨率为 10 bits
LEDC_TIMER_11_BIT, // LEDC PWM 占空比分辨率为 11 bits
LEDC_TIMER_12_BIT, // LEDC PWM 占空比分辨率为 12 bits
LEDC_TIMER_13_BIT, // LEDC PWM 占空比分辨率为 13 bits
LEDC_TIMER_14_BIT, // LEDC PWM 占空比分辨率为 14 bits
#if SOC_LEDC_TIMER_BIT_WIDTH > 14
LEDC_TIMER_15_BIT, // LEDC PWM 占空比分辨率为 15 bits
LEDC_TIMER_16_BIT, // LEDC PWM 占空比分辨率为 16 bits
LEDC_TIMER_17_BIT, // LEDC PWM 占空比分辨率为 17 bits
LEDC_TIMER_18_BIT, // LEDC PWM 占空比分辨率为 18 bits
LEDC_TIMER_19_BIT, // LEDC PWM 占空比分辨率为 19 bits
LEDC_TIMER_20_BIT, // LEDC PWM 占空比分辨率为 20 bits
#endif
LEDC_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, // 将根据给定的分辨率和占空率参数自动选择led源时钟
LEDC_USE_APB_CLK = SOC_MOD_CLK_APB, // 选择APB作为时钟源
LEDC_USE_RC_FAST_CLK = SOC_MOD_CLK_RC_FAST, // 选择RC_FAST作为时钟源
LEDC_USE_REF_TICK = SOC_MOD_CLK_REF_TICK, // 选择REF_TICK作为时钟源
LEDC_USE_RTC8M_CLK __attribute__((deprecated("please use 'LEDC_USE_RC_FAST_CLK' instead"))) = 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 频率越高,占空比分辨率越低,反之亦然。
4.2、通道配置函数
我们可以使用 ledc_channel_config()
函数 配置 LEDC 的通道,其函数原型如下:
/**
* @brief 配置LEDC通道
*
* @param ledc_conf 指向配置LEDC通道的结构体指针
* @return esp_err_t ESP_OK 表示配置成功
* ESP_ERR_INVALID_ARG 表示参数错误
*/
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 > 6
LEDC_CHANNEL_6, // LEDC channel 6
LEDC_CHANNEL_7, // LEDC channel 7
#endif
LEDC_CHANNEL_MAX,
} ledc_channel_t;
成员 intr_type
用来 配置中断,它的可选值如下:
typedef enum
{
LEDC_INTR_DISABLE = 0, // 使能LEDC中断
LEDC_INTR_FADE_END, // 使能LEDC中断
LEDC_INTR_MAX,
} ledc_intr_type_t;
成员 duty
用来 设置 LEDC 通道的占空比设置,占空比设定范围为 0 ~ \(2^{duty\_resolution}\)。
4.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_INVALID_ARG 表示参数错误
*/
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_INVALID_ARG 表示参数错误
*/
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);
4.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);
4.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);
4.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
。