8. PWM
8.1 PWM介绍
PWM(Pulse Width Modulation 脉宽调制)是利用微处理器的数字输出来对模拟电路进行控制的一种非常有效的技术。它是一种对模拟信号电平进行数字编码的方法。是指在一定时间内波形的高电平(即 1 状态)所占用的时间比例。通过高分辨率计数器的使用,方波占空比被调制用来对一个模拟信号的电平进行编码。PWM 信号任然是数字的,因为在给定的任何时刻,满幅值的直流供电要么完全有,要么完全无。比如我们的电压输出是 5V的,那么经过改变 PWM 的占空比,可以达到在一定时间内输出 3.3V 或者 1.3V 的效果。
举个例子
想象你有一个LED和一个开关,你以肉眼看不清的速度,快速反复地开关一次,这样LED灯就会亮一半时间,暗一半时间。如果你快速地进行这个操作,对于观察者来说,LED就像是以半亮度持续亮着。这就是PWM的基本原理。
如果你把大部分时间保持开关为关闭状态,那么LED会显得更暗;相反,如果你把开关大部分时间保持为打开状态,LED会显得更亮。这就是PWM调节占空比来控制亮度的过程。
8.2 PWM的基本参数
PWM 是脉冲宽度调制,具有两个非常重要的参数:频率和占空比。
- 频率:PWM 信号的周期长度,通常使用赫兹 (Hz) 表示,表示每秒钟有多少个脉冲。PWM 的频率是整个周期的倒数。指 1 秒钟内信号从高电平到低电平再回到高电平的次数(一个周期)
- 占空比:占空比是指一个周期内高电平所占的比例。
- 分辨率:ESP32 支持的 PWM 信号分辨率是指设备可输出的不同占空比级别的数量。例如,8 位分辨率就表示设备可以输出 2^8 个不同占空比级别,即 0%、1/256、2/256 … 直到 100%。
8.3 ESP32S3上的PWM
在ESP32-S3中有两个硬件外设可以输出PWM信号,分别是LED PWM 控制器 (LEDC) 和 电机控制脉宽调制器 (MCPWM)。它们各有其特点和用途:
- LED PWM 控制器 (LEDC):这个模块的主要设计目标是产生高精度的 PWM 波形,用以控制 LED 灯的亮度或者产生声音。LEDC 的分辨率可以达到 16 位,能够产生准确且平滑的变化,适用于控制 LED 灯的亮度和产生声音。并且,LEDC 支持多达 8 个通道的 PWM 输出,且支持任意的GPIO引脚。用户可以配置每个通道的频率和占空比。
- 电机控制脉宽调制器 (MCPWM):这个模块主要用于马达控制,包括伺服马达、步进马达和普通电机。MCPWM 支持更加复杂的控制模式,如电机的向前/向后驱动、断电刹车等,并且支持闭环控制模式,能满足更复杂的电机控制需求。MCPWM 支持高达 6 个通道的独立 PWM 输出,并且支持死区控制和外部信号捕获。
总的来说,它们两者在处理 PWM 方面有所不同,并被应用于不同的场景。LEDC 更加适合控制灯光、声音等线性设备,而 MCPWM 包含更高级的功能,适合电机控制。本章我们以LED PWM 控制器作为案例输出PWM,后面简称LEDC。
8.4 PWM的操作流程
- 定时器配置:指定 PWM 信号的频率和占空比分辨率。
- 通道配置: 绑定定时器和输出 PWM 信号的 GPIO。
- 改变PWM信号:输出 PWM 信号来驱动 LED。可通过软件控制或使用硬件渐变功能来改变 LED 的亮度。
8.4.1 定时器配置
要设置定时器,可调用函数 ledc_timer_config(),并将包括如下配置参数的数据结构ledc_timer_config_t 传递给该函数。关于ledc_timer_config_t 的相关参数说明:
speed_mode
:速度模式。注意,与 ESP32 不同,ESP32-S3 仅支持设置通道为低速模式,即LEDC_LOW_SPEED_MODE
。timer_num
: 通道的定时器源。定时器索引 ledc_timer_t。可选参数如下:LEDC_TIMER_0
LEDC_TIMER_1
LEDC_TIMER_2
LEDC_TIMER_3
LEDC_TIMER_MAX
freq_hz
: PWM 信号频率,表示LEDC模块的定时器时钟频率设置,单位为Hz。duty_resolution
: PWM 占空比分辨率。占空比分辨率通常用ledc_timer_bit_t设置,范围是 10 至 15 位。如需较低的占空比分辨率(上至 10,下至 1),可直接输入相应数值。相关参数请参考 ledc_timer_bit_t。clk_cfg
: LEDPWM的时钟来源。可选以下参数:LEDC_AUTO_CLK
:启动定时器时,将根据给定的分辨率和占空率参数自动选择ledc源时钟;LEDC_USE_APB_CLK
:选择APB作为源时钟;LEDC_USE_RC_FAST_CLK
:选择“RC_FAST”作为源时钟;LEDC_USE_XTAL_CLK
:选择XTAL作为源时钟;LEDC_USE_RTC8M_CLK
:”LEDC_USE_RC_FAST_CLK” 的别名;
频率和占空比分辨率相互关联。PWM 频率越高,占空比分辨率越低,反之亦然。如果 API 不是用来改变 LED 亮度,而是用于其它目的,这种相互关系可能会很重要。更多信息详见 频率和占空比分辨率支持范围 一节。
时钟源同样可以限制PWM频率。选择的时钟源频率越高,可以配置的PWM频率上限就越高。
备注
- 如果 ESP32-S3 的定时器选用了RC_FAST_CLK作为其时钟源,驱动会通过内部校准来得知这个时钟源的实际频率。这样确保了输出PWM信号频率的精准性。
- ESP32-S3 的所有定时器共用一个时钟源。因此 ESP32-S3 不支持给不同的定时器配置不同的时钟源。
8.4.2 通道配置
定时器设置好后,需要配置所需的通道(ledc_channel_t 之一)。配置通道需调用函数 ledc_channel_config()。 通道的配置与定时器设置类似,需向通道配置函数传递包括通道配置参数的结构体。
ledc_channel_config_t
此时,通道会按照 ledc_channel_config_t 的配置开始运作,并在选定的 GPIO 上生成由定时器设置指定的频率和占空比的 PWM 信号。在通道运作过程中,可以随时通过调用函数 ledc_stop() 将其暂停。
ledc_channel_config_t 的参数说明如下: gpio_num
:
配置输出引脚;例如使用GPIO6引脚,则gpio_num = 6;
speed_mode
:
LEDC速度模式选择,可选参数有高速模式(LEDC_HIGH_SPEED_MODE)或低速模式(LEDC_LOW_SPEED_MODE)
channel
:
LEDC的输出通道(PWM的输出通道),可选参数有0~7;
intr_type
:
配置中断。可选参数有使能中断(LEDC_INTR_FADE_END)和失能中断(LEDC_INTR_DISABLE)
timer_sel
:
选择通道的定时器源。定时器索引 ledc_timer_t。可选参数如下:
LEDC_TIMER_0
LEDC_TIMER_1
LEDC_TIMER_2
LEDC_TIMER_3
LEDC_TIMER_MAX
duty
:
LEDC通道的占空比设置。占空比设定范围为 0 到 2的duty_resolution次方。
hpoint
:
led通道 hpoint 值。hpoint 叫做占空比作用点。它表示占空比对应的时钟计数值。所谓占空比作用点,表示LEDC模块在输出PWM信号的过程中,会根据计数器和占空比值比较得出一个结果,将其与hpoint进行比较,然后输出PWM信号,具体操作如下:
- 如果计数器值小于hpoint,LEDC模块的PWM信号输出为逻辑高电平。
- 如果计数器值大于等于hpoint,LEDC模块的PWM信号输出为逻辑低电平。
- 当计数器达到最大值(0xfffff)时,LEDC模块会将计数值清零并将PWM信号输出为逻辑低电平。
因此,hpoint值越小,占空比越小,输出PWM信号时占空比也就越小。hpoint值越大,占空比越大,输出PWM信号时占空比也就越大。
output_invert
:
启用(1)或禁用(0)gpio输出反相。默认启动。
示例:
// 准备并应用LEDC PWM通道配置
ledc_channel_config_t ledc_channel = {
.speed_mode = LEDC_LOW_SPEED_MODE, //LED模式 低速模式
.channel = LEDC_CHANNEL_0, //通道0
.timer_sel = LEDC_TIMER_0, //定时器源 定时器0
.intr_type = LEDC_INTR_DISABLE, //关闭中断
.gpio_num = 5, //输出引脚 GPIO5
.duty = 0, // 设置占空比为0
.hpoint = 0
};
ledc_channel_config(&ledc_channel);
2
3
4
5
6
7
8
9
10
11
8.4.3 改变 PWM 信号
通道开始运行、生成具有恒定占空比和频率的 PWM 信号之后,有几种方式可以改变该信号。驱动 LED 时,主要通过改变占空比来变化光线亮度。
8.4.3.1 改变 PWM 占空比
调用函数 ledc_set_duty() 可以设置新的占空比。之后,调用函数 ledc_update_duty() 使新配置生效。要查看当前设置的占空比,可使用 get
函数 ledc_get_duty()。
- 设置占空比
esp_err_t ledc_set_duty(ledc_mode_t speed_mode, ledc_channel_t channel, uint32_t duty)
参数说明: speed_mode
: LEDC速度模式选择,可选参数有高速模式(LEDC_HIGH_SPEED_MODE
)或低速模式(LEDC_LOW_SPEED_MODE
) channel
- LEDC通道(0 - LEDC_CHANNEL_MAX-1),从 ledc_channel_t
中选择; duty
-设置led的负载,负载设置范围为0 到 (2 的 duty_resolution 次方) - 1;
- 更新占空比
esp_err_t ledc_update_duty(ledc_mode_t speed_mode, ledc_channel_t channel)
speed_mode
- LEDC速度模式选择,可选参数有高速模式(LEDC_HIGH_SPEED_MODE
)或低速模式(LEDC_LOW_SPEED_MODE
)channel
- LEDC通道(0 - LEDC_CHANNEL_MAX-1),从ledc_channel_t
中选择;
示例:假设分辨率设置为 13位。 则50%的占空比= ((2的13次方) - 1) _ 50% = (8,192-1)_ 0.5 = 4095.5;
// 设置占空比为50%
ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 4095);
// 更新通道占空比
ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0);
2
3
4
另外一种设置占空比和其他通道参数的方式是调用 通道配置 一节提到的函数 ledc_channel_config()。
传递给函数的占空比数值范围取决于选定的 duty_resolution
,应为 0 至 (2 的 duty_resolution 次方
) - 1。例如,如选定的占空比分辨率为 10,则占空比的数值范围为 0 至 1023。此时分辨率为 ~0.1%。
8.4.3.2 改变 PWM 频率
LED PWM 控制器 API 有多种方式即时改变 PWM 频率:
- 通过调用函数 ledc_set_freq() 设置频率。可用函数 ledc_get_freq() 查看当前频率。
esp_err_t ledc_set_freq(ledc_mode_t speed_mode, ledc_timer_t timer_num, uint32_t freq_hz)
参数 - speed_mode
- LEDC速度模式选择,可选参数有高速模式(LEDC_HIGH_SPEED_MODE
)或低速模式(LEDC_LOW_SPEED_MODE
)- timer_num
– LEDC定时器(0-3),从ledc_timer_t中选择; - freq_hz
– 设置led频率; 2. 通过调用函数 ledc_bind_channel_timer() 将其他定时器绑定到该通道来改变频率和占空比分辨率。
esp_err_t ledc_bind_channel_timer(ledc_mode_t speed_mode, ledc_channel_t channel, ledc_timer_t timer_sel)
参数
speed_mode
- LEDC速度模式选择,可选参数有高速模式(LEDC_HIGH_SPEED_MODE
)或低速模式(LEDC_LOW_SPEED_MODE
)channel
- LEDC通道(0 - LEDC_CHANNEL_MAX-1),从ledc_channel_t
中选择;timer_sel
– LEDC定时器(0-3),从ledc_timer_t中选择;
- 通过调用函数 ledc_channel_config() 改变通道的定时器。
8.5 硬件连接与准备
本案例使用板载的LED进行呼吸灯测试。一般人眼睛对于 80HZ 以上刷新频率则完全没有闪烁感,由于频率很高时看不到闪烁,占空比越大 LED 越亮,占空比越小 LED 越暗。所以在频率一定时,可以用不同占空比改变 LED 灯的亮度,使其达到一个呼吸灯的效果(逐渐亮在逐渐灭,如此反复)。
板载的LED接到的引脚是GPIO48,所以我们在初始化时,需要将LEDC功能绑定到GPIO48。
8.6 PWM呼吸灯验证
在main/hardware/pwm目录下(如果没有该目录则新建),新建两个文件,bsp_pwm.c 和 bsp_pwm.h。
记得配置头文件的路径
在bsp_pwm.h中编写如下代码。
#ifndef _BSP_PWM_H_
#define _BSP_PWM_H_
#include "driver/ledc.h"
#define LEDC_TIMER LEDC_TIMER_0 //定时器0
#define LEDC_MODE LEDC_LOW_SPEED_MODE //低速模式
#define LEDC_OUTPUT_IO (48) // 定义输出GPIO为GPIO48
#define LEDC_CHANNEL LEDC_CHANNEL_0 // 使用LEDC的通道0
#define LEDC_DUTY_RES LEDC_TIMER_13_BIT // LEDC分辨率设置为13位
#define LEDC_DUTY (4095) // 设置占空比为50%。 ((2的13次方) - 1) * 50% = 4095
#define LEDC_FREQUENCY (100) // 频率单位是Hz。设置频率为5000 Hz
/**
* @函数说明 LEDC功能初始化
* @传入参数 无
* @函数返回 无
* @备 注 PWM频率越高,可用的占空比分辨率越低
*/
void LedcInitConfig(void);
#endif
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
在bsp_pwm.c中编写如下代码。
#include "bsp_pwm.h"
/**
* @函数说明 LEDC功能初始化
* @传入参数 无
* @函数返回 无
* @备 注 PWM频率越高,可用的占空比分辨率越低
*/
void LedcInitConfig(void)
{
// 准备并应用led PWM定时器配置
ledc_timer_config_t ledc_timer = {
.speed_mode = LEDC_MODE, //LED模式 低速模式
.timer_num = LEDC_TIMER, //通道的定时器源 定时器0
.duty_resolution = LEDC_DUTY_RES, //将占空比分辨率设置为13位
.freq_hz = LEDC_FREQUENCY, // 设置输出频率为5 kHz
.clk_cfg = LEDC_AUTO_CLK //设置LEDPWM的时钟来源 为自动
//LEDC_AUTO_CLK = 启动定时器时,将根据给定的分辨率和占空率参数自动选择led源时钟
};
ledc_timer_config(&ledc_timer);
// 准备并应用LEDC PWM通道配置
ledc_channel_config_t ledc_channel = {
.speed_mode = LEDC_MODE, //LED模式 低速模式
.channel = LEDC_CHANNEL, //通道0
.timer_sel = LEDC_TIMER, //定时器源 定时器0
.intr_type = LEDC_INTR_DISABLE, //关闭中断
.gpio_num = LEDC_OUTPUT_IO, //输出引脚 GPIO5
.duty = 0, // 设置占空比为0
.hpoint = 0
};
ledc_channel_config(&ledc_channel);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
在main.c中编写如下代码。
#include <stdio.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "bsp_pwm.h"
void app_main(void)
{
int duty_value = 0;
// 设置led外设配置
LedcInitConfig();
// 设置占空比为50
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, LEDC_DUTY);
// 更新通道占空比
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
while(1)
{
// 实现渐亮效果
for(duty_value=0;duty_value<8191;duty_value+=100)
{
// 设置亮度模拟值,占空比不断加大
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty_value);
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
// 延时 10ms
vTaskDelay(10 / portTICK_PERIOD_MS);
}
// 实现渐灭效果
for(duty_value=8191;duty_value>=0;duty_value-=100)
{
// 设置亮度模拟值,占空比不断减少
ledc_set_duty(LEDC_MODE, LEDC_CHANNEL, duty_value);
ledc_update_duty(LEDC_MODE, LEDC_CHANNEL);
// 延时 10ms
vTaskDelay(10 / portTICK_PERIOD_MS);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37