这个实验的功能是使用 PWM 去控制板子上的彩灯实现渐变效果。 这个实验的代码为工程“3_8_pwm”目录。

3.8.1. 实验内容

(1) 学习如何控制 LED 灯及硬件原理 (2) 学习 ESP32 的 PWM(ledc)功能的配置 (3) 掌握 PWM(ledc)控制 LED 彩灯渐变程序

3.8.2. 硬件设计和原理

LED 彩灯硬件设计在 3.3 节已经讲解过了,我们知道通过 IO15、IO16 和 IO32 输出高低电平就可以控制 彩灯亮灭了。 在这个实验里,我们是通过 IO15、IO16 和 IO32 输出 PWM 信号去控制彩灯的亮度。要注意的是我们 的彩灯是共阳极的,输出 100%占空比的时候,彩灯是灭的,输出 0%占空比的时候,彩灯处于最亮状态。 ESP32 的占空比支持手动和自动两种模块,我们的这个实验里启动了两个任务,用于实现手动设置占 空比和自动设置占空比,通过板上的按键 key1 去控制切换。

3.8.3. 函数介绍

ESP32 的 ESP-IDF 编程指南可以从官网上查询: https://docs.espressif.com/projects/esp-idf/zh_CN/latest/index.html

 PWM(LEDC)定时器配置 函数定义如下: esp_err_t ledc_timer_config(const ledc_timer_config_t* timer_conf); 参数说明: ledc_timer_config_t* timer_conf:定时器的参数,这是一个结构体,具体的定义请往下看 返回值:ESP_OK(成功)和 ESP_ERR_INVALID_ARG(参数错误) 。

typedef struct {
ledc_mode_t speed_mode; union {

ledc_timer_bit_t duty_resolution;  //pwm 的分辨率,也就是最大的占空比
ledc_timer_bit_t bit_num   attribute  ((deprecated)); //
};
ledc_timer_t timer_num;	//定时器选择,支持 0 ̄3 

 	} ledc_timer_config_t;

PWM(LEDC)配置函数 函数定义如下: esp_err_t ledc_channel_config(const ledc_channel_config_t* ledc_conf); 参数说明: ledc_channel_config_t* ledc_conf:pwm 配置参数,这是一个结构体,具体的定义请往下看 返回值:ESP_OK(成功)和 ESP_ERR_INVALID_ARG(参数错误) 。

typedef struct {
int gpio_num;	//具体的 IO 口定义 ledc_mode_t speed_mode;		//速度选择,高速或者低速 ledc_channel_t channel;		//通道选择,取值 0~7 ledc_intr_type_t intr_type;	//中断类型 ledc_timer_t timer_sel;			//定时器通道选择 uint32_t duty;		//占空比
int hpoint;	//LEDC channel hpoint value, the max value is 0xfffff
} ledc_channel_config_t;

 PWM(LEDC)渐变安装函数 这个函数在系统 PWM 占空比自己变化的时候会使用到,函数定义如下: esp_err_t ledc_fade_func_install(int intr_alloc_flags); 参数说明: int intr_alloc_flags:是否分配中断标志 返回值:ESP_OK(成功)和 ESP_ERR_INVALID_ARG(参数错误) 。

 PWM(LEDC)自动渐变 要实现 PWM 自动渐变,先要调用自动渐变设置函数 ledc_set_fade_with_time,接着调用自动渐变启 动函数 ledc_fade_start,分别说明。 函数 1: 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_mode_t speed_mode://速度选择,高速或者低速 ledc_channel_t channel://PWM 通道 uint32_t target_duty://目标占空比 int max_fade_time_ms://变化到目标占空比使用的时间 返回值:ESP_OK(成功)和 ESP_ERR_INVALID_ARG(参数错误) 。

函数 2: esp_err_t ledc_fade_start(ledc_mode_t speed_mode, ledc_channel_t channel,ledc_fade_mode_t fade_mode); 功能:启动自动渐变功能 参数说明: ledc_mode_t speed_mode://速度选择,高速或者低速 ledc_channel_t channel://PWM 通道 ledc_fade_mode_t fade_mode://wait_done:是否等待 返回值:ESP_OK(成功)和 ESP_ERR_INVALID_ARG(参数错误) 。

 PWM(LEDC)手动修改占空比 手动修改占空比涉及到 2 个函数,使用函数 ledc_set_duty 设置占空比后,还要使用 ledc_update_duty函数让新占空比生效,分别说明。 函数 1: esp_err_t ledc_set_duty(ledc_mode_t speed_mode, ledc_channel_t channel, uint32_t duty); 功能:设置新的占空比 参数说明: ledc_mode_t speed_mode://速度选择,高速或者低速 ledc_channel_t channel://PWM 通道 uint32_t duty://要设置的占空比 返回值:ESP_OK(成功)和 ESP_ERR_INVALID_ARG(参数错误) 。

函数 2: esp_err_t ledc_update_duty(ledc_mode_t speed_mode, ledc_channel_t channel); 功能:启用新的占空比 参数说明: ledc_mode_t speed_mode://速度选择,高速或者低速 ledc_channel_t channel://PWM 通道 返回值:ESP_OK(成功)和 ESP_ERR_INVALID_ARG(参数错误) 。

3.8.4. 代码讲解

使用 vs code 展开本实验的工程目录,如下图:

esp32pwm输出频率 esp32的pwm_嵌入式

我们的这个实验,只有一个源文件 app_main.c,按键、任务和 PWM 控制都在这个文件里。在讲解代 码的前,先把这个实验的框图发出来,如下图:

esp32pwm输出频率 esp32的pwm_参数说明_02

通过框图可以看出,一共有三个任务,功能分别如下:  主任务,检测按键 key1 按下,然后修改 pwm_mode 值。,任务 1 和任务 2 根据 pwm_mode 的值去 决定是否执行渐变功能,  任务 1,pwm_mode 等于 1 时,红绿蓝三种灯各渐变 2 秒,一个周期约是 9 秒时间。  任务 2,pwm_mode 等于 0 时,根据 pwm_index 决定当前是设置红绿蓝三种的其中一种,每一个占 空比停留时间是 50ms。

对这个框图有了一定的认识之后,我们看程序的时候应该比较容易理解了。程序里关于按键初始化和 按键扫描部分不讲解了,如果不理解请往回看实验 3.5。下面开始讲解程序里的各个片段。 在源文件 app_main.c 的最前面,对灯的硬件 IO 口、占空比最大值、自动渐变的时间和 PWM 使用的 通道有定义,代码如下图:

//定义 LED 灯的 IO 
口                  32//对应红灯的 LED,绿灯为 15,
  #define             蓝灯为 16
   LED_RED_IO      15	//对应红灯的 LED,绿灯为 15,蓝灯为 16



#define LEDC_MAX_DUTY
#define        (8191)	//2 的 13 次方-1(13 位 PWM)
              (1000)	//渐变时间(ms)
   
#define 
#define  PWM_RED_CHANNEL	LEDC_CHANNEL_0	//定义红灯通道
#define  PWM_GREEN_CHANNEL	LEDC_CHANNEL_1	//定义绿灯通道
#define  PWM_BLUE_CHANNEL	LEDC_CHANNEL_2	//定义蓝灯通道
unsigned  char     //PWM 模块,如果为 1 表示通过库函数实现渐变

首先我们讲解 PWM 初始化,是对 PWM 使用的定时器配置,三路 PWM 参数配置,以及是否启动渐变功能,代码里都是注释,用到的每一个函数在前面都有讲解,代码如下:

void PWM_init(void)
{
//定时器配置结构体 ledc_timer_config_t
ledc_timer.duty_resolution  = LEDC_TIMER_13_BIT;
ledc_timer.freq_hz =  5000; ledc_timer.speed_mode = LEDC_HIGH_SPEED_MODE; ledc_timer.timer_num = LEDC_TIMER_0; ledc_timer_config(&ledc_timer);
//PWM 通道 0 配置->IO32->红色灯
g_ledc_red.channel		=  PWM_RED_CHANNEL; g_ledc_red.duty			=  LEDC_MAX_DUTY; g_ledc_red.gpio_num		=  LED_RED_IO; g_ledc_red.speed_mode = LEDC_HIGH_SPEED_MODE; g_ledc_red.timer_sel	=  LEDC_TIMER_0; ledc_channel_config(&g_ledc_red);
//PWM 通道 0 配置->IO15->绿
g_ledc_green.channel
g_ledc_green.duty g_ledc_green.gpio_num g_ledc_green.speed_mode g_ledc_green.timer_sel      
//PWM 通道 0 配置->IO32->蓝
g_ledc_blue.channel
g_ledc_blue.duty g_ledc_blue.gpio_num g_ledc_blue.speed_mode g_ledc_blue.timer_sel
ledc_channel_config(&g_ledc_bl
//PWM 模式为 1 的时候,使能 ledc 渐变功能
if(pwm_mode==1)
{
ledc_fade_func_install(0);

接着我们看主任务,任务里选是初始化按键和 PWM,接着创建了任务 1 和任务 2,最后通过 while(1) 检测 key1 从而改变 pwm_mode,代码如下:

//用户函数入口,相当于 main 函数 void app_main()
{
initKey();//按键初始化 PWM_init();//PWM 初始化

//创建两个任务,用于执行不同的 PWM 模式
xTaskCreate(&task_pwm1,  "task_pwm1",  4096,  NULL,  9, NULL);
xTaskCreate(&task_pwm2,  "task_pwm2",  4096,  NULL,  9, NULL);

while(1)
{
//通过按键改变 PWM 模式
if(1==key_read_key1())
{
pwm_mode=(pwm_mode==1)?0:1; PWM_init();//重新初始化 ledc 初始化
}

vTaskDelay(10);//延时一下
}
}

任务 1 入口函数,如果 pwm_mode 等于 1 时,红绿蓝三种灯自动轮流渐变,代码里都是注释,用到的 每一个函数在前面都有讲解。代码如下,绿灯和蓝灯的代码和红灯的类似,固省略,完整的代码请看源文件:

//通过渐变功能演示 PWM 任务
void task_pwm1(void *pvParameter)
{
while(1)
{
if(1==pwm_mode)
{
printf("pwm mode1.\r\n");
//渐变功能演示 PWM

///
//红灯占空比 100%-->0%-->100%,时间 2*LEDC_FADE_TIME
//红灯:灭-->亮-->灭,的过程
///

//红灯占空比 100% 渐变至 0%,时间 LEDC_FADE_TIME ledc_set_fade_with_time(g_ledc_red.speed_mode,
g_ledc_red.channel, 0,  LEDC_FADE_TIME);
//渐变开始 ledc_fade_start(g_ledc_red.speed_mode,
g_ledc_red.channel, LEDC_FADE_NO_WAIT);
vTaskDelay(LEDC_FADE_TIME / portTICK_PERIOD_MS);

//红灯占空比 0%渐变至 100%,时间 LEDC_FADE_TIME ledc_set_fade_with_time(g_ledc_red.speed_mode,
g_ledc_red.channel, LEDC_MAX_DUTY, LEDC_FADE_TIME);
//渐变开始 ledc_fade_start(g_ledc_red.speed_mode,
g_ledc_red.channel, LEDC_FADE_NO_WAIT);
vaskDelay(LEDCT_FADE_TIME / portTICK_PERIOD_MS); vTaskDelay(LEDC_FADE_TIME / portTICK_PERIOD_MS);

///

///
//蓝灯占空比 100%-->0%-->100%,时间 2*LEDC_FADE_TIME
//蓝灯:灭-->亮-->灭,的过程
//
代码相似///略/
}
else{
vTaskDelay(5);//延时一下
}
}
}

任务 2 入口函数,如果 pwm_mode 等于 0 时,每 50ms 通过程序修改一次占空比,红绿蓝三种灯轮变 化:

//用户修改占空比,改变 LED 灯亮度任务 void  task_pwm2(void *pvParameter)
{
int pwm_level=0;//0~255 占空比 int pwm_index=1;//红 1 绿 2 蓝 3

while(1)
{
if(0==pwm_mode)
{
printf("pwm mode2.\r\n"); if(pwm_index==1)
{
//修改红灯占空比 CtrRBG_R(pwm_level); pwm_level+=10;//占空比递增

if(pwm_level>255)
{
pwm_level=0; pwm_index=2; CtrRBG_R(255);
}
}
else if(pwm_index==2)
{
//修改绿灯占空比 CtrRBG_G(pwm_level); pwm_level+=10;//占空比递增

if(pwm_level>255)
{
pwm_level=0; pwm_index=3; CtrRBG_G(255);
}
}
else

if(pwm_level>255)
{
pwm_level=0; pwm_index=1; CtrRBG_B(255);
}
}
}
vTaskDelay(5);//延时 50ms
}

}

在上面的代码里,修改红绿蓝占空比是通过三个函数 CtrRBG_R()、CtrRBG_G()和 CtrRBG_B()进行的, 这三个函数类似。 我们选其中一个讲解,这类函数先是把最大的占空比 LEDC_MAX_DUTY 平均分为 255 等分,根据参数计 算出当前的占空比,然后通过 ledc_set_duty()和 ledc_update_duty()函数更新占空比。为什么我们使 用 255 等分呢?因为我们说的真彩色是 24 位色,他是由红绿蓝三种色混合起来的,每一种颜色就是 8 位 色,2 的 8 次方就是 256,而 0~255 就是 256 级颜色,关于这方面的知识,可以百度查询理解一下。

//设置红灯的 PWM 级别
//输入 level 取值 0~255
void  CtrRBG_R(unsigned  char level)
{
int duty=0;

if(level==255)
{
duty=LEDC_MAX_DUTY;
}
else if(level==0)
{
duty=0;
}
else
{
//计算占空比 duty=(level*LEDC_MAX_DUTY)/255;
}

ledc_set_duty(LEDC_HIGH_SPEED_MODE, PWM_RED_CHANNEL, duty);//修改占空比 ledc_update_duty(LEDC_HIGH_SPEED_MODE, PWM_RED_CHANNEL);//新的占空比生效
}

3.8.5. 实验过程

配置下载串口、波特率、编绎和程序下载的详细过程请往回看 3.1.4,在这个实验里都是一笔带过。 (1) 把开发板通过 USB 线接到电脑上,通过设备管理器查看生成的串口。开发板在我们演示电脑上生成的是 COM3。 (2) 在 menuconfig 菜单里配置下载程序串口。提供的例程配置的串口是 COM3,波特率为 921600。 (3) 通过 make all 编绎工程。 (4) 当编绎通过之后,使用命令 make flash 把程序下载到开发板上。或者参考 2.3.2 节,使用工具下载。 (5) 按下开发板的复位键,让程序运行起来,观察彩灯的渐变过程。

(6) 按下 key1 键,观察彩灯的渐变过程,然后和按键前进行对比,也就是彩灯的两种模式变化区别。 (7) 我们认为自动变化过程比较适合做呼吸灯。