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 的模式、上下拉、中断等功能,函数原型如下:

cpp 复制代码
esp_err_t gpio_config(const gpio_config_t *pGPIOConfig);

该函数的形参描述如下表所示:

参数 描述
pGPIOConfig GPIO结构体

返回值:ESP_OK 表示配置成功,ESP_FAIL 表示配置失败。

pGPIOConfig 为 GPIO 配置结构体指针,下面来看一下 gpio_config_t 结构体中的变量。

cpp 复制代码
/* 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 失能中断 上升沿触发 下降沿触发 上升沿和下降沿触发 输入低电平触发 输入高电平触发

设置管脚输出电平

该函数用于配置某个管脚输出电平,该函数原型如下所示:

cpp 复制代码
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 表示设置失败。

获取管脚电平

该函数用于获取某个管脚的电平,该函数原型如下所示:

cpp 复制代码
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

cpp 复制代码
/* 引脚定义 */
#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

cpp 复制代码
// 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

cpp 复制代码
/* 引脚定义 */
#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

cpp 复制代码
#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函数解析

注册中断函数

该函数用于注册中断服务,原型如下:

cpp 复制代码
esp_err_t gpio_install_isr_service(int intr_alloc_flags);

该函数的形参描述如下表所示:

参数 中断标志位 描述
intr_alloc_flags ESP_INTR_FLAG_LEVEL1 使用Level 1中断级别。在中断服务程序执行期间禁用同级别中断。
intr_alloc_flags ESP_INTR_FLAG_LEVEL2 使用Level 2中断级别。在中断服务程序执行期间禁用同级别和Level 1的中断。
intr_alloc_flags ESP_INTR_FLAG_LEVEL3 同理。
intr_alloc_flags ESP_INTR_FLAG_LEVEL4 同理。
intr_alloc_flags ESP_INTR_FLAG_LEVEL5 同理。
intr_alloc_flags ESP_INTR_FLAG_LEVEL6 同理。
intr_alloc_flags ESP_INTR_FLAG_NMI Level 7中断级别(最高优先级)
intr_alloc_flags ESP_INTR_FLAG_SHARED 中断可以在ISRs之间共享
intr_alloc_flags ESP_INTR_FLAG_EDGE 使用边沿触发方式。使能GPIO边沿触发中断。
intr_alloc_flags ESP_INTR_FLAG_IRAM 如果缓存被禁用,ISR可以被调用
intr_alloc_flags ESP_INTR_FLAG_INTRDISABLED 返回时禁用此中断

返回值:ESP_OK,成功;

ESP_ERR_NO_MEM,没有内存来安装此服务;

ESP_ERR_INVALID_STATE,ISR服务已经安装;

ESP_ERR_NOT_FOUND,没有找到具有指定标志的空闲中断;

ESP_ERR_INVALID_ARG,GPIO错误。

分配中断函数

该函数设置某个管脚的中断服务函数,该函数原型如下所示:

cpp 复制代码
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,以确保其运行在内存中的可执行区域。

cpp 复制代码
void IRAM_ATTR gpio_isr_handler(void *arg)
{
    /* 处理中断响应 */
}

开启外部中断函数

该函数用来配置某个管脚开启外部中断,该函数原型如下所示:

cpp 复制代码
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

cpp 复制代码
/*引脚定义*/
#define BOOT_INT_GPIO_PIN GPIO_NUM_0

/*IO 操作*/
#define BOOT gpio_get_level(BOOT_INT_GPIO_PIN)

/* 函数声明 */
void exit_init(void); /* 外部中断初始化程序 */

exit.c

cpp 复制代码
/**
 * @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。要使用此功能,需要包含以下头文件:

cpp 复制代码
#include "driver/ledc.h"

配置LEDC使用的定时器的函数

注意:在首次配置LEDC时,建议先配置定时器,再配置通道。这样可以确保IO引脚上的PWM信号自输出开始那一刻起,其频率就是正确的。

设置定时器函数原型如下:

cpp 复制代码
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上)** 高速模式 LEDC_LOW_SPEED_MODE 低速模式 LEDC_SPEED_MODE_MAX 模式上限(用于检查模式有效性,不可作为实际的模式配置)
ledc_timer_config_t .duty_resolution PWM占空比分辨率。由 ledc_timer_bit_t 枚举类型定义,ESP32-S3支持的范围是1~14位的分辨率。 LEDC_TIMER_X_BIT(X=1~14)
ledc_timer_config_t .timer_num PWM通道的定时器源,由 ledc_timer_t 枚举类型定义。 LEDC_TIMER_0 LEDC_TIMER_1 LEDC_TIMER_2 LEDC_TIMER_3 LEDC_TIMER_MAX (同样用于检查模式有效性,不可作为实际的模式配置)
ledc_timer_config_t .freq_hz PWM脉冲的频率,表示LEDC模块的定时器时钟频率,单位为Hz uint32_t大小的值
ledc_timer_config_t .clk_cfg 时钟源 LEDC_AUTO_CLK 在初始化计时器时,LEDC源时钟会根据给定的分辨率和占空比被自动选定。 LEDC_USE_APB_CLK 选择APB作为时钟源。 LEDC_USE_RC_FAST_CLK 选择内部快速RC时钟作为时钟源 LEDC_USE_XTAL_CLK 选择外部晶体时钟作为时钟源 LEDC_USE_RTC8M_CLK LEDC_USE_RC_FAST_CLK的别名
ledc_timer_config_t .deconfigure 执行硬件定时器的反初始化:停止定时器计数、释放占用的硬件资源、复位内部状态机、使定时器回归未配置状态。(需要完全改变定时器参数时使用) bool值

deconfigure成员变量的使用流程如下:

cpp 复制代码
// 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)!!!

通道配置函数

函数原型如下:

cpp 复制代码
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
ledc_channel_config_t .speed_mode 速度模式 LEDC_HIGH_SPEED_MODE**(仅存在于ESP32上)** 高速模式 LEDC_LOW_SPEED_MODE 低速模式 LEDC_SPEED_MODE_MAX 模式上限(用于检查模式有效性,不可作为实际的模式配置)
ledc_channel_config_t .channel LEDC的输出通道(PWM输出通道) LEDC_CHANNEL_X(X=0~7) LEDC_CHANNEL_MAX (用于检查模式有效性,不可作为实际的模式配置)
ledc_channel_config_t .intr_type 中断配置 LEDC_INTR_DISABLE 失能 LEDC_INTR_FADE_END 使能渐变结束中断 LEDC_INTR_MAX (用于检查模式有效性,不可作为实际的模式配置)
ledc_channel_config_t .timer_sel 选择通道的定时器源。由 ledc_timer_t 枚举类型定义,和之前配置定时器一样 LEDC_TIMER_0 LEDC_TIMER_1 LEDC_TIMER_2 LEDC_TIMER_3 LEDC_TIMER_MAX (同样用于检查模式有效性,不可作为实际的模式配置)
ledc_channel_config_t .duty LEDC通道的占空比设置 范围为[0, (2**duty_resolution)],duty_resolution为定时器配置时的PWM占空比分辨率
ledc_channel_config_t .hpoint led通道hpoint值。一个周期中上升沿开始的时间点,一般不太关系,给0即可。 int类型的大小
ledc_channel_config_t .output_invert 启用或禁用gpio输出反相 1(启用);0(禁用)

设置PWM占空比

调用函数 ledc_set_duty() 可以设置新的占空比,之后调用函数 ledc_update_duty() 使新配置生效。要查看当前设置的占空比,可以使用 ledc_get_duty() 函数。设置PWM占空比的函数原型如下:

cpp 复制代码
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() 使得新配置生效,函数原型如下:

cpp 复制代码
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模块的中断资源。

cpp 复制代码
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渐变功能

接下来要设置占空比以及渐变时长,函数原型如下:

cpp 复制代码
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表示成功,其他表示配置失败。

开启渐变函数原型如下:

cpp 复制代码
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 (用于检查模式有效性,不可作为实际的模式配置)

这个参数就是设置是否阻塞,不论选择哪个模式,都可以绑定渐变完成回调函数,不过阻塞模式下使用回调函数意义不太大,因为当阻塞模式下的函数返回时,回调函数一定已经执行完毕了(回调函数是在渐变结束时、函数返回前由内部驱动调用的)。

设置渐变完成回调函数

在非阻塞模式下,函数调用之后立即返回,想要知道什么时候渐变完成,需要绑定一个回调函数,当回调函数被调用时,在回调函数里设置某些标志位,不能调用任何可能导致阻塞的函数。函数原型如下:

cpp 复制代码
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); param:系统传入的事件参数(通道号、状态等) user_arg:用户自定义的透传参数,初始化时传入
*user_arg 传给回调函数的参数

4.4 LEDC驱动

使用硬件PWM、非阻塞模式,在回调函数里使用事件组,实现呼吸灯的效果

cpp 复制代码
#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));

    //开启硬件PWM
    ledc_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等。

相关推荐
知识分享小能手7 分钟前
Vue3 学习教程,从入门到精通,Vue3 中使用 Axios 进行 Ajax 请求的语法知识点与案例代码(23)
前端·javascript·vue.js·学习·ajax·vue·vue3
凤年徐22 分钟前
【数据结构与算法】21.合并两个有序链表(LeetCode)
c语言·数据结构·c++·笔记·算法·链表
玄酒40 分钟前
51单片机入门:模块化编程
单片机·嵌入式硬件·51单片机
嵌入式仿真实验教学平台1 小时前
嵌入式系统教学范式演进:云端仿真平台如何重构温湿度监测实验教学
单片机·重构·proteus·产教融合·温湿度传感器·嵌入式仿真
小一亿1 小时前
【0基础PS】PS工具详解--仿制图章工具
学习·平面·adobe·信息可视化·媒体·photoshop
淮北4944 小时前
STL学习(十一、常用的算数算法和集合算法)
c++·vscode·学习·算法
_Kayo_8 小时前
VUE2 学习笔记14 nextTick、过渡与动画
javascript·笔记·学习
Ronin-Lotus9 小时前
嵌入式硬件篇---ESP32稳压板
嵌入式硬件·esp32·稳压板
哪 吒9 小时前
OpenAI放大招:ChatGPT学习模式上线,免费AI智能家教
人工智能·学习·ai·chatgpt·gemini·deepseek
AI视觉网奇10 小时前
语音识别dolphin 学习笔记
笔记·学习