ESP32 外设驱动开发指南 (ESP-IDF框架)——GPIO篇:基础配置、外部中断与PWM(LEDC模块)应用
一、前言
博主最近也是找到实习了,实习项目用的是 ESP32-S3,基于 esp-idf 开发,因此想写博客记录一下学习笔记。
esp-idf 是基于 freeRTOS 的框架,里面用到的组件,以及我们的应用程序都是基于 freeRTOS 来开发的,因此我们必须掌握 freeRTOS 的用法。如果我们不深究原理,只关注于 freeRTOS 的接口使用,我们很快就能掌握。另外,因为 freeRTOS 开源免费的特性,目前大部分芯片产商做的 SDK 都是基于 freeRTOS 系统开发的,因此我们就更有理由要学习 RTOS 了。freeRTOS 可以去看我的专栏:FreeRTOS专栏,也可以去看韦东山老师的课程,尤其是内部原理,看完大有收获。
二、GPIO
2.1 GPIO简介
GPIO 是负责控制或采集外部器件信息的外设,主要负责输入输出功能。ESP32-S3 芯片具有 45 个物理 GPIO 管脚,涵盖 GPIO0 至 GPIO21 以及 GPIO26 至 GPIO48 的广泛范围。
2.2 GPIO函数解析
ESP-IDF 提供了丰富的 GPIO 操作函数,在 v5.x.x\esp-idf\components\esp_driver_gpio 路径下找到相关的 gpio.c 和 gpio.h 文件。在 gpio.h 头文件中,你可以找到 ESP32-S3 的所有 GPIO 函数定义。
● GPIO配置函数
该函数用于配置 GPIO 的模式、上下拉、中断等功能,函数原型如下:
esp_err_t gpio_config(const gpio_config_t *pGPIOConfig);
该函数的形参描述如下表所示:
参数 | 描述 |
---|---|
pGPIOConfig | GPIO结构体 |
返回值:ESP_OK 表示配置成功,ESP_FAIL 表示配置失败。
pGPIOConfig 为 GPIO 配置结构体指针,下面来看一下 gpio_config_t 结构体中的变量。
/* GPIO配置参数 */
typedef struct {uint64_t pin_bit_mask; /* 配置引脚位 */gpio_mode_t mode; /* 设置引脚模式 */gpio_pullup_t pull_up_en; /* 设置上拉 */gpio_pulldown_t pull_down_en; /* 设置下拉 */gpio_int_type_t intr_type; /* 中断配置 */
} gpio_config_t;
各个参数有哪些见下表:
类型 | 类型说明 | 可填参数 | 参数说明 |
---|---|---|---|
.pin_bit_mask | 引脚位 | (1<<x)其中 x 为 ESP32S3 中可用 GPIO | 要用哪个引脚,比如 IO1 引脚,则写为:(1ull << GPIO NUM 1) |
.mode | 引脚模式 | GPIO_MODE_DISABLE GPIO_MODE_INPUT GPIO_MODE_OUTPUT GPIO_MODE_OUTPUT_OD GPIO_MODE_INPUT_OUTPUT_OD GPIO_MODE_INPUT_OUTPUT | 失能输入输出模式 仅输入模式 仅输出模式 输出开漏模式 输入输出开漏模式 输入输出模式 |
.pull_up_en | 配置上拉 | GPIO_PULLUP_DISABLE GPIO_PULLUP_ENABLE | 失能上拉 |
.pull_down_en | 配置下拉 | GPIO_PULLDOWN_DISABLE GPIO_PULLDOWN_ENABLE | 失能下拉 使能下拉 |
.intr_type | 中断配置 | GPIO_INTR_DISABLE GPIO_INTR_POSEDGE GPIO_INTR_NEGEDGE GPIO_INTR_ANYEDGE GPIO_INTR_LOW_LEVEL GPIO_INTR_HIGH_LEVEL | 失能中断 |
● 设置管脚输出电平
该函数用于配置某个管脚输出电平,该函数原型如下所示:
esp_err_t gpio_set_level(gpio_num_t gpio_num, uint32_t level);
该函数的形参描述如下:
参数 | 描述 |
---|---|
gpio_num | GPIO 引脚号。(在 gpio_types.h 文件中枚举 gpio_num_t 有定义) |
level | GPIO 引脚输出电平。0:低电平,1:高电平 |
返回值:ESP_OK 表示设置成功,ESP_FAIL 表示设置失败。
● 获取管脚电平
该函数用于获取某个管脚的电平,该函数原型如下所示:
int gpio_get_level(gpio_num_t gpio_num);
该函数的形参描述如下:
参数 | 描述 |
---|---|
gpio_num | GPIO 引脚号。(在 gpio_types.h 文件中枚举 gpio_num_t 有定义) |
返回值:0 GPIO 输入电平为低电平,1 GPIO 输入电平为高电平。
2.3 LED驱动
万物先从点灯开始,下面实现led.c及led.h两个文件。led.h负责声明LED相关的函数和变量,led.c实现LED的驱动代码。
● led.h
/* 引脚定义 */
#define LED_GPIO_PIN GPIO_NUM_1 /* LED 连接的 GPIO 端口 *//* 引脚的输出的电平状态 */
enum GPIO_OUTPUT_STATE{PIN_RESET,PIN_SET
};#define LED(x) do { x ? \gpio_set_level(LED_GPIO_PIN, PIN_SET) : \gpio_set_level(LED_GPIO_PIN, PIN_RESET); \
} while(0)#define LED_TOGGLE() do { gpio_set_level(LED_GPIO_PIN, !gpio_get_level(LED_GPIO_PIN));
} while(0) /* LED 翻转 *//* 函数声明*/
void led_init(void); /* 初始化 LED */
LED(x) 宏用于控制 IO1 管脚的电平状态,使用三元运算符,传入 1 设置引脚为高电平;反之,输出低电平。LED_TOGGLE() 宏,实现管脚电平翻转。
● led.c
// esp封装的库
#include "driver/gpio.h"
#include "led.h"/*** @brief 初始化 LED* @param 无* @retval 无*/
void led_init(void)
{gpio_config_t gpio_init_struct = {.pin_bit_mask = 1ull << LED_GPIO_PIN, //指定GPIO.mode = GPIO_MODE_OUTPUT, //设置为输出模式.pull_up_en = GPIO_PULLUP_DISABLE, //禁止上拉.pull_down_en = GPIO_PULLDOWN_DISABLE, //禁止下拉.intr_type = GPIO_INTR_DISABLE, //禁止中断};gpio_config(&gpio_init_struct); /* 配置 GPIO */LED(1);
}
2.4 KEY驱动
配置 GPIO 为输入模式,通常按键直接连接到芯片的引脚,没有加上拉电阻,因此需要将 GPIO 配置成上拉输入模式。本次按键驱动使用的是最简单的延时消抖,下面实现 key.c 及 key.h 两个文件。
● key.h
/* 引脚定义 */
#define BOOT_GPIO_PIN GPIO_NUM_0/*IO 操作*/
#define BOOT gpio_get_level(BOOT_GPIO_PIN)/* 按键按下定义 */
#define BOOT_PRES 1 /* BOOT 按键按下 *//* 函数声明 */
void key_init(void); /* 初始化按键 */
uint8_t key_scan(uint8_t mode); /* 按键扫描函数 */
通过 BOOT 宏来读取连接按键引脚的电平。
● key.c
#include "driver/gpio.h"
#include "key.h"/*** @brief 初始化按键引脚* @param 无* @retval 无*/
void key_init(void)
{gpio_config_t gpio_init_struct;gpio_init_struct.intr_type = GPIO_INTR_DISABLE; /* 失能引脚中断 */gpio_init_struct.mode = GPIO_MODE_INPUT; /* 输入模式 */gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; /* 使能上拉 */gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; /* 失能下拉 */gpio_init_struct.pin_bit_mask = 1ull << BOOT_GPIO_PIN; /* BOOT 按键引脚 */gpio_config(&gpio_init_struct); /* 配置使能 */
}/*** @brief 按键扫描函数* @param mode:0 / 1, 具体含义如下:* 0, 不支持连续按(当按键按下不放时, 只有第一次调用会返回键值,* 必须松开以后, 再次按下才会返回其他键值)* 1, 支持连续按(当按键按下不放时, 每次调用该函数都会返回键值)* @retval 键值, 定义如下:* BOOT_PRES, 1, BOOT 按下*/
uint8_t key_scan(uint8_t mode)
{uint8_t keyval = 0;static uint8_t key_boot = 1; /* 按键松开标志 */if(mode){key_boot = 1;}if (key_boot && (BOOT == 0)) /* 按键松开标志为 1,且有任意一个按键按下了 */{vTaskDelay(10); /* 去抖动 */key_boot = 0;if (BOOT == 0){keyval = BOOT_PRES;}}else if (BOOT == 1){key_boot = 1;}return keyval; /* 返回键值 */
}
此函数只有一个形参 mode,用于设置按键是否支持连续按下模式。当 mode 为 0 时,表示按键不支持连续按下;反之,则支持连续按下。
三、EXIT
3.1 EXIT简介
外部中断属于硬件中断,由微控制器外部事件触发。微控制器的特定引脚被设计为对特定事件(如按钮按压、传感器信号变化等)作出响应,这些引脚通常称为“外部中断引脚”。一旦外部中断事件发生,当前程序执行将立即暂停,并跳转到相应的中断服务程序(ISR)进行处理。处理完毕后,程序会恢复执行,从被中断的地方继续。下图是 CPU 中断处理过程。
ESP32-S3 的外部中断具备两种触发类型:
(1)电平触发:高、低电平触发,要求保持中断的电平状态直到 CPU 响应。
(2)边沿触发:上升沿和下降沿触发,这类型的中断一旦触发,CPU 即可响应。
ESP32-S3 的外部中断功能能够以非常精确的方式捕捉外部事件的触发。开发者可以通过配置中断触发方式(如上升沿、下降沿、任意电平、低电平保持、高电平保持等)来适应不同的外部事件,并在事件发生时立即中断当前程序的执行,转而执行中断服务函数。ESP32-S3 支持六级中断,同时支持中断嵌套,也就是低优先级中断可以被高优先级中断打断。数字越大表明该中断的优先级越高。其中,NMI 中断拥有最高优先级,此类中断已经触发,CPU 必须处理。
3.2 EXIT函数解析
● 注册中断函数
该函数用于注册中断服务,原型如下:
esp_err_t gpio_install_isr_service(int intr_alloc_flags);
该函数的形参描述如下表所示:
参数 | 中断标志位 | 描述 |
---|---|---|
intr_alloc_flags | ESP_INTR_FLAG_LEVEL1 | 使用Level 1中断级别。在中断服务程序执行期间禁用同级别中断。 |
ESP_INTR_FLAG_LEVEL2 | 使用Level 2中断级别。在中断服务程序执行期间禁用同级别和Level 1的中断。 | |
ESP_INTR_FLAG_LEVEL3 | 同理。 | |
ESP_INTR_FLAG_LEVEL4 | ||
ESP_INTR_FLAG_LEVEL5 | ||
ESP_INTR_FLAG_LEVEL6 | ||
ESP_INTR_FLAG_NMI | Level 7中断级别(最高优先级) | |
ESP_INTR_FLAG_SHARED | 中断可以在ISRs之间共享 | |
ESP_INTR_FLAG_EDGE | 使用边沿触发方式。使能GPIO边沿触发中断。 | |
ESP_INTR_FLAG_IRAM | 如果缓存被禁用,ISR可以被调用 | |
ESP_INTR_FLAG_INTRDISABLED | 返回时禁用此中断 |
返回值:ESP_OK,成功;
ESP_ERR_NO_MEM,没有内存来安装此服务;
ESP_ERR_INVALID_STATE,ISR服务已经安装;
ESP_ERR_NOT_FOUND,没有找到具有指定标志的空闲中断;
ESP_ERR_INVALID_ARG,GPIO错误。
● 分配中断函数
该函数设置某个管脚的中断服务函数,该函数原型如下所示:
esp_err_t gpio_isr_handler_add(gpio_num_t gpio_num, gpio_isr_t isr_handler, void *args);
该函数的形参描述如下表所示:
参数 | 描述 |
---|---|
gpio_num | GPIO引脚号,指定要分配中断处理程序的GPIO引脚 |
isr_handler | 指向中断处理函数的函数指针。中断处理函数是一个用户定义的回调函数,将在中断发生时执行 |
args | 传递给中断处理程序的参数。这是一个指向用户特定数据的指针,可以在中断处理程序中使用 |
返回值:ESP_OK,成功;
ESP_ERR_INVALID_STATE,状态错误,ISR服务没有初始化;
ESP_ERR_INVALID_ARG,参数错误。
下面是中断处理函数的模板,中断处理函数需要声明为 IRAM_ATTR,以确保其运行在内存中的可执行区域。
void IRAM_ATTR gpio_isr_handler(void *arg)
{/* 处理中断响应 */
}
● 开启外部中断函数
该函数用来配置某个管脚开启外部中断,该函数原型如下所示:
esp_err_t gpio_intr_enable(gpio_num_t gpio_num);
参数就是要使能哪个GPIO引脚,传入引脚号。
返回值:ESP_OK,成功;
ESP_ERR_INVALID_ARG,参数错误。
注意:在使用 gpio_intr_enable() 函数之前,开发者需要先通过 gpio_install_isr_service() 函数和 gpio_isr_handler_add() 函数来安装和注册中断处理程序。
3.3 EXIT驱动
● exit.h
/*引脚定义*/
#define BOOT_INT_GPIO_PIN GPIO_NUM_0/*IO 操作*/
#define BOOT gpio_get_level(BOOT_INT_GPIO_PIN)/* 函数声明 */
void exit_init(void); /* 外部中断初始化程序 */
● exit.c
/*** @brief 外部中断服务函数* @param arg:中断引脚号* @note IRAM_ATTR: 这里的IRAM_ATTR属性用于将中断处理函数存储在内部RAM中,目的在于减少延迟* @retval 无*/
static void IRAM_ATTR exit_gpio_isr_handler(void *arg)
{uint32_t gpio_num = (uint32_t) arg;if (gpio_num == BOOT_INT_GPIO_PIN){/* 消抖 */esp_rom_delay_us(20000);//注意://这里的延时函数通过空循环消耗CPU时间,不会主动释放CPU控制权//但是在ISR中,不允许使用可能阻塞的函数如vTaskDelay(会触发上下文切换)//总的来说,还是不希望在中断里进行耗时的操纵,这里的20ms勉强能接受if (BOOT == 0){LED0_TOGGLE();}}
}/*** @brief 外部中断初始化程序* @param 无* @retval 无*/
void exit_init(void)
{gpio_config_t gpio_init_struct;/* 配置BOOT引脚和外部中断 */gpio_init_struct.mode = GPIO_MODE_INPUT; /* 选择为输入模式 */gpio_init_struct.pull_up_en = GPIO_PULLUP_ENABLE; /* 上拉使能 */gpio_init_struct.pull_down_en = GPIO_PULLDOWN_DISABLE; /* 下拉失能 */gpio_init_struct.intr_type = GPIO_INTR_NEGEDGE; /* 下降沿触发 */gpio_init_struct.pin_bit_mask = 1ull << BOOT_INT_GPIO_PIN; /* 设置的引脚的位掩码 */ESP_ERROR_CHECK(gpio_config(&gpio_init_struct)); /* 配置使能 *//* 注册中断服务 */ESP_ERROR_CHECK(gpio_install_isr_service(0));/* 设置BOOT的中断回调函数 */ESP_ERROR_CHECK(gpio_isr_handler_add(BOOT_INT_GPIO_PIN, exit_gpio_isr_handler, (void*) BOOT_INT_GPIO_PIN));/* 使能GPIO模块中断信号 */ESP_ERROR_CHECK(gpio_intr_enable(BOOT_INT_GPIO_PIN));
}
开启管脚的外部中断操作相对简便。首先,需要将管脚配置为下降沿触发(GPIO_INTR_NEGEDGE)和输入模式(GPIO_MODE_INPUT)。完成配置后,需要调用 gpio_install_isr_service 函数来注册中断服务,并调用 gpio_isr_handler_add 函数来注册外部中断的回调函数。最后,调用 gpio_intr_enable 函数启用外部中断功能。其中,exit_gpio_isr_handler 回调函数负责实现 LED 灯状态的切换。
四、LEDC
4.1 PWM原理解析
PWM(Pulse Width Modulation),简称脉宽调制,是一种将模拟信号变为脉冲信号的技术。PWM可以控制LED亮度、直流电机的转速等。
PWM的主要参数如下:
① 频率:1s内有多少个PWM周期(一个高电平加一个低电平为一个周期),单位Hz。
② 周期:频率倒数,T=1/f。
③ 占空比:在一个周期内,高电平的时间与整个周期时间的比例,范围0%~100%。
使用PWM控制LED时,一个PWM周期持续时间比较长,人眼就可以看出LED在闪烁。只要缩小周期,直到一个临界值使得人眼无法分辨LED在闪烁,改变占空比,就改变了LED的亮度。这就是PWM的原理。
4.2 ESP32的LED PWM控制器介绍
ESP32-S3的LED PWM控制器,简写为LEDC,用于生成脉冲宽度调制信号。
LEDC具有八个独立的PWM生成器(八个通道)。每个PWM生成器会从四个通用定时器中选择一个,以该定时器的计数值作为基准生成PWM信号。LEDC定时器如下图所示:
想要实现PWM输出,需要先指定PWM通道的参数:频率、分辨率、占空比,然后将通道映射到指定的引脚,该引脚输出对应通道的PWM信号,如下图所示:
LEDC可以在没有CPU干预的情况下自动改变占空比(硬件PWM)。
4.3 LEDC函数解析
4.3.1 SW_PWM
ESP-IDF提供了一套API来配置PWM。要使用此功能,需要包含以下头文件:
#include "driver/ledc.h"
● 配置LEDC使用的定时器的函数
注意:在首次配置LEDC时,建议先配置定时器,再配置通道。这样可以确保IO引脚上的PWM信号自输出开始那一刻起,其频率就是正确的。
设置定时器函数原型如下:
esp_err_t ledc_timer_config(const ledc_timer_config_t *timer_conf);
该函数的形参描述如下表所示:
形参 | 描述 |
---|---|
timer_conf | 指向配置LEDC定时器的结构体指针 |
返回值:ESP_OK,成功;
ESP_ERR_INVALID_ARG,参数错误;
ESP_FAIL,无法根据给定的频率和当前的 PWM 分辨率找到一个合适的分频系数;
ESP_ERR_INVALID_STATE,无法取消定时器配置,因为定时器未配置或未处于暂停状态。
该函数使用 ledc_timer_config_t 类型的结构体变量传入,该结构体的定义如下所示:
结构体 | 成员变量 | 可选参数 |
---|---|---|
ledc_timer_config_t | .speed_mode | LEDC_HIGH_SPEED_MODE(仅存在于ESP32上) |
.duty_resolution | LEDC_TIMER_X_BIT(X=1~14) | |
.timer_num PWM通道的定时器源,由 ledc_timer_t 枚举类型定义。 | LEDC_TIMER_0 | |
.freq_hz | uint32_t大小的值 | |
.clk_cfg | LEDC_AUTO_CLK 选择外部晶体时钟作为时钟源 LEDC_USE_RC_FAST_CLK的别名 | |
.deconfigure 执行硬件定时器的反初始化:停止定时器计数、释放占用的硬件资源、复位内部状态机、使定时器回归未配置状态。(需要完全改变定时器参数时使用) | bool值 |
deconfigure 成员变量的使用流程如下:
// 1. 暂停定时器(必须步骤!)
ledc_timer_pause(LEDC_LOW_SPEED_MODE, LEDC_TIMER_0);// 2. 准备反配置结构体
ledc_timer_config_t timer_cfg = {.speed_mode = LEDC_LOW_SPEED_MODE, // 必须匹配原配置.timer_num = LEDC_TIMER_0, // 指定要反配置的定时器.deconfigure = true // 核心开关// 其他参数自动忽略
};// 3. 执行反配置
ledc_timer_config(&timer_cfg);
注意:ESP32-S3 不支持定时器专属时钟,所有定时器必须共享同一时钟源。禁止混合配置(如 TIMER0 用 RC_FAST + TIMER1 用 XTAL)!!!
● 通道配置函数
函数原型如下:
esp_err_t ledc_channel_config(const ledc_channel_config_t *ledc_conf);
形参就是指向LEDC通道的结构体指针,来看一下返回值和结构体的具体定义。
返回值:ESP_OK,成功;
ESP_ERR_INVALID_ARG,参数错误。
结构体 | 成员变量 | 可选参数 |
---|---|---|
ledc_channel_config_t | .gpio_num 配置输出引脚 | if you want to use gpio16, gpio_num = 16 |
.speed_mode 速度模式 | LEDC_HIGH_SPEED_MODE(仅存在于ESP32上) 高速模式 LEDC_LOW_SPEED_MODE 低速模式 LEDC_SPEED_MODE_MAX 模式上限(用于检查模式有效性,不可作为实际的模式配置) | |
.channel LEDC的输出通道(PWM输出通道) | LEDC_CHANNEL_X(X=0~7) | |
.intr_type 中断配置 | LEDC_INTR_DISABLE 使能渐变结束中断 (用于检查模式有效性,不可作为实际的模式配置) | |
.timer_sel 选择通道的定时器源。由 ledc_timer_t 枚举类型定义,和之前配置定时器一样 | LEDC_TIMER_0 LEDC_TIMER_1 LEDC_TIMER_2 LEDC_TIMER_3 LEDC_TIMER_MAX (同样用于检查模式有效性,不可作为实际的模式配置) | |
.duty | 范围为[0, (2**duty_resolution)],duty_resolution为定时器配置时的PWM占空比分辨率 | |
.hpoint led通道hpoint值。一个周期中上升沿开始的时间点,一般不太关系,给0即可。 | int类型的大小 | |
.output_invert 启用或禁用gpio输出反相 | 1(启用);0(禁用) |
● 设置PWM占空比
调用函数 ledc_set_duty() 可以设置新的占空比,之后调用函数 ledc_update_duty() 使新配置生效。要查看当前设置的占空比,可以使用 ledc_get_duty() 函数。设置PWM占空比的函数原型如下:
esp_err_t ledc_set_duty(ledc_mode_t speed_mode, ledc_channel_t channel, uint32_t duty);
该函数的形参描述如下表所示:
形参 | 描述 |
---|---|
speed_mode | 速度模式选择: LEDC_HIGH_SPEED_MODE(仅存在于ESP32上) 高速模式 LEDC_LOW_SPEED_MODE 低速模式 LEDC_SPEED_MODE_MAX 模式上限(用于检查模式有效性,不可作为实际的模式配置) |
channel | LEDC通道: (0~LEDC_CHANNEL_MAX-1),从 ledc_channel_t 中选择 |
duty | 占空比,范围为[0, (2**duty_resolution)],duty_resolution为定时器配置时的PWM占空比分辨率 |
返回值:ESP_OK,成功;
ESP_ERR_INVALID_ARG,参数错误。
● 更新PWM占空比
上一步调用 ledc_set_duty() 后,调用 ledc_update_duty() 使得新配置生效,函数原型如下:
esp_err_t ledc_update_duty(ledc_mode_t speed_mode, ledc_channel_t channel);
该函数的形参描述见上一个函数,返回值也一样。
到这里就属于ESP32-S3的软件PWM部分,配置好定时器、LEDC通道后,就可以搭配使用上面两个改变PWM占空比的函数,在指定引脚输出想要的PWM脉冲。之所以叫做软件PWM,是因为:如果想要实现呼吸灯的效果,需要我们不断判断当前的占空比为多少,然后手动改变占空比的递增或递减,这些操作都需要消耗CPU资源。下面来介绍硬件PWM的功能和用法,它可在无需CPU干预的情况下自动改变占空比。
4.3.2 HW_PWM
LEDC控制器硬件可逐渐改变占空比的数值,要使用此功能,可用 ledc_fade_func_install() 使能渐变,然后使用下列渐变函数之一进行配置:
ledc_set_fade_with_time()
ledc_set_fade_with_step()
ledc_set_fade()
最后调用 ledc_fade_start() 开启渐变。还记得配置LEDC通道的时候有个参数是使能中断吗,可选的只有两项,使能渐变结束中断和失能。我查了一下,发现这个即使不使能也不影响硬件PWM,至于软件PWM中使能这个中断有没有用,暂时没找到很明确的说明,如果有大佬懂得可以在评论区讨论一下。硬件PWM可以注册一个回调函数,在渐变完成之后就会调用回调函数,这个回调函数由中断调用,但这个中断是我们调用 ledc_fade_func_install() 函数时,内部会初始化LEDC的渐变中断,和通道配置中的 intr_type 无关。
● 开启硬件PWM,使能渐变
安装LEDC渐变功能。该功能将占用LEDC模块的中断资源。
esp_err_t ledc_fade_func_install(int intr_alloc_flags);
该函数的形参见 esp_intr_alloc.h 里,带 ESP_INTR_FLAG_ 前缀的宏定义。但是,很多例程里调用这个函数直接传入0即可,表示默认的中断优先级。
返回值:ESP_OK,成功;
ESP_ERR_INVALID_ARG,参数错误;
ESP_ERR_NOT_FOUND,找不到可用的中断源;
ESP_ERR_INVALID_STATE,渐变服务已经安装。
● 设置LEDC渐变功能
接下来要设置占空比以及渐变时长,函数原型如下:
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);
该函数的形参描述如下表所示:
形参 | 描述 |
---|---|
speed_mode | LEDC_HIGH_SPEED_MODE(仅存在于ESP32上) 高速模式 LEDC_LOW_SPEED_MODE 低速模式 LEDC_SPEED_MODE_MAX 模式上限(用于检查模式有效性,不可作为实际的模式配置) |
channel | LEDC通道: (0~LEDC_CHANNEL_MAX-1),从 ledc_channel_t 中选择 |
target_duty | 目标占空比 范围为[0, (2**duty_resolution)],duty_resolution为定时器配置时的PWM占空比分辨率 |
max_fade_time_ms | 最大渐变时间(毫秒) |
返回值: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 | 渐变模式,由 ledc_fade_mode_t 枚举类型定义,有以下模式可选: LEDC_FADE_NO_WAIT LEDC_FADE_WAIT_DONE LEDC_FADE_MAX (用于检查模式有效性,不可作为实际的模式配置) |
这个参数就是设置是否阻塞,不论选择哪个模式,都可以绑定渐变完成回调函数,不过阻塞模式下使用回调函数意义不太大,因为当阻塞模式下的函数返回时,回调函数一定已经执行完毕了(回调函数是在渐变结束时、函数返回前由内部驱动调用的)。
● 设置渐变完成回调函数
在非阻塞模式下,函数调用之后立即返回,想要知道什么时候渐变完成,需要绑定一个回调函数,当回调函数被调用时,在回调函数里设置某些标志位,不能调用任何可能导致阻塞的函数。函数原型如下:
esp_err_t ledc_cb_register(ledc_mode_t speed_mode, ledc_channel_t channel, ledc_cbs_t *cbs, void *user_arg);
主要看后两个参数:
后两个形参 | 描述 |
---|---|
*cbs | 指向ledc_cbs_t结构体的指针。ledc_cbs_t里面只有一个成员变量:fade_cb。它是指向回调函数的指针,回调函数的类型为ledc_cb_t,定义如下: typedef bool (*ledc_cb_t)(const ledc_cb_param_t *param, void *user_arg); |
*user_arg | 传给回调函数的参数 |
4.4 LEDC驱动
使用硬件PWM、非阻塞模式,在回调函数里使用事件组,实现呼吸灯的效果
#include <freertos/FreeRTOS.h> // FreeRTOS基础功能
#include <freertos/task.h> // 任务相关API(xTaskCreatePinnedToCore)
#include <freertos/event_groups.h> // 事件组(EventGroupHandle_t, xEventGroup*)
#include "driver/gpio.h" // GPIO定义(LED_GPIO)
#include "driver/ledc.h" // LEDC PWM驱动(所有ledc_*函数和结构体)
#include <esp_log.h> // 日志系统(ESP_ERROR_CHECK)//定义LED的GPIO口
#define LED_GPIO GPIO_NUM_1#define TAG "LEDC"#define LEDC_TIMER LEDC_TIMER_0 //定时器0
#define LEDC_MODE LEDC_LOW_SPEED_MODE //低速模式
#define LEDC_OUTPUT_IO (LED_GPIO) //选择GPIO端口
#define LEDC_CHANNEL LEDC_CHANNEL_0 //PWM通道
#define LEDC_DUTY_RES LEDC_TIMER_13_BIT //分辨率
#define LEDC_DUTY (4095) //最大占空比值,这里是2^13-1
#define LEDC_FREQUENCY (5000) //PWM周期//用于通知渐变完成
static EventGroupHandle_t s_ledc_ev = NULL;//关灯完成事件标志
#define LEDC_OFF_EV (1<<0)//开灯完成事件标志
#define LEDC_ON_EV (1<<1)//渐变完成回调函数
bool IRAM_ATTR ledc_finish_cb(const ledc_cb_param_t *param, void *user_arg)
{BaseType_t xHigherPriorityTaskWoken;if(param->duty){xEventGroupSetBitsFromISR(s_ledc_ev,LEDC_ON_EV,&xHigherPriorityTaskWoken);}else{xEventGroupSetBitsFromISR(s_ledc_ev,LEDC_OFF_EV,&xHigherPriorityTaskWoken);}return xHigherPriorityTaskWoken;
}//ledc 渐变任务
void ledc_breath_task(void* param)
{EventBits_t ev;while(1){ev = xEventGroupWaitBits(s_ledc_ev,LEDC_ON_EV|LEDC_OFF_EV,pdTRUE,pdFALSE,pdMS_TO_TICKS(5000));if(ev){//设置LEDC开灯渐变if(ev & LEDC_OFF_EV){ledc_set_fade_with_time(LEDC_MODE,LEDC_CHANNEL,LEDC_DUTY,2000);ledc_fade_start(LEDC_MODE,LEDC_CHANNEL,LEDC_FADE_NO_WAIT);}else if(ev & LEDC_ON_EV) //设置LEDC关灯渐变{ledc_set_fade_with_time(LEDC_MODE,LEDC_CHANNEL,0,2000);ledc_fade_start(LEDC_MODE,LEDC_CHANNEL,LEDC_FADE_NO_WAIT);}}}
}//LED呼吸灯初始化
void led_breath_init(void)
{//初始化一个定时器ledc_timer_config_t ledc_timer = {.speed_mode = LEDC_MODE, //低速模式.timer_num = LEDC_TIMER, //定时器ID.duty_resolution = LEDC_DUTY_RES, //占空比分辨率,这里是13位,2^13-1.freq_hz = LEDC_FREQUENCY, // PWM频率,这里是5KHZ.clk_cfg = LEDC_AUTO_CLK // 时钟};ESP_ERROR_CHECK(ledc_timer_config(&ledc_timer));//ledc通道初始化ledc_channel_config_t ledc_channel = {.speed_mode = LEDC_MODE, //低速模式.channel = LEDC_CHANNEL, //PWM 通道0-7.timer_sel = LEDC_TIMER, //关联定时器,也就是上面初始化好的那个定时器.intr_type = LEDC_INTR_DISABLE,//不使能中断.gpio_num = LEDC_OUTPUT_IO, //设置输出PWM方波的GPIO管脚.duty = 0, // 设置默认占空比为0.hpoint = 0};ESP_ERROR_CHECK(ledc_channel_config(&ledc_channel));//开启硬件PWMledc_fade_func_install(0);//创建一个事件组,用于通知任务渐变完成s_ledc_ev = xEventGroupCreate();//配置LEDC渐变ledc_set_fade_with_time(LEDC_MODE,LEDC_CHANNEL,LEDC_DUTY,2000);//启动渐变ledc_fade_start(LEDC_MODE,LEDC_CHANNEL,LEDC_FADE_NO_WAIT);//设置渐变完成回调函数ledc_cbs_t cbs = {.fade_cb=ledc_finish_cb,};ledc_cb_register(LEDC_MODE,LEDC_CHANNEL,&cbs,NULL);xTaskCreatePinnedToCore(ledc_breath_task,"ledc",2048,NULL,3,NULL,1);
}// 主函数
void app_main(void)
{led_breath_init(); //呼吸灯
}
结语
该系列会持续更新,后续可能会更新实习用到的技术栈如JSON、OTA、http和UDP等。