ESP32 外设驱动开发指南 (ESP-IDF框架)——GPIO篇:基础配置、外部中断与PWM(LEDC模块)应用

发布于:2025-08-01 ⋅ 阅读:(15) ⋅ 点赞:(0)

一、前言

        博主最近也是找到实习了,实习项目用的是 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上)
高速模式
LEDC_LOW_SPEED_MODE
低速模式
LEDC_SPEED_MODE_MAX
模式上限(用于检查模式有效性,不可作为实际的模式配置)

.duty_resolution
PWM占空比分辨率。由 ledc_timer_bit_t 枚举类型定义,ESP32-S3支持的范围是1~14位的分辨率。

LEDC_TIMER_X_BIT(X=1~14)

.timer_num

PWM通道的定时器源,由 ledc_timer_t 枚举类型定义。

LEDC_TIMER_0
LEDC_TIMER_1
LEDC_TIMER_2
LEDC_TIMER_3
LEDC_TIMER_MAX
(同样用于检查模式有效性,不可作为实际的模式配置)

.freq_hz
PWM脉冲的频率,表示LEDC模块的定时器时钟频率,单位为Hz

uint32_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的别名

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

.intr_type

中断配置

LEDC_INTR_DISABLE
失能
LEDC_INTR_FADE_END

使能渐变结束中断
LEDC_INTR_MAX

(用于检查模式有效性,不可作为实际的模式配置)

.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)],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);
param:系统传入的事件参数(通道号、状态等)
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));

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


网站公告

今日签到

点亮在社区的每一天
去签到