ESP32-S3 是 QFN56 封装,GPIO 引脚一共有 45 个,从 GPIO0 到 GPIO21,再从 GPIO26 到 GPIO48。理论上,所有的 IO 都可以作为普通 GPIO 使用,或者复用为任何外设功能使用,但有些引脚用作连接 FLASH 和 PSRAM 后,就不能再做其它用途了。
我们开发板上使用的模组型号是 ESP32-S3-WROOM-1-N16R8,它的 FLASH 为 16MB,与 ESP32 采用 4 线制 SPI 连接,它的 PSRAM 为 8MB,与 ESP32 采用 8 线制 SPI 连接。FLASH 与 PSRAM 一共占用了 12 个 IO 引脚。除去这些引脚,IO 就剩下 33 个了。
在这里插入图片描述

通过开发板的原理图,可以看到开发板上的 ESP32 引脚连接情况。
在这里插入图片描述

在开发板上,实际上引出了 3 个我们可以用户自定义的 IO,一个是 BOOT 按键连接的 IO,另外两个是外扩接口引出的 GPIO10 和 11。
这里我们使用 BOOT 按键,来学习一下 GPIO 功能。
ESP32 的 GPIO,可以用作输入、输出,可以配置内部上拉、下拉,可以配置为中断引脚。
这里我们把连接 BOOT 按键的 IO0 引脚,设置为 GPIO 中断,接收按键请求。

独立按键原理

独立按键的原理主要依赖于机械触点和电气触点之间的相互作用。在未被按下时,触点保
持分离状态,电路处于断开状态。然而,当用户按下按键时,在弹簧和导电片的共同作用下,
触点会闭合,从而使电路连通。此时,微控制器能够检测到按键触发的信号,进而执行相应的
操作。这种基于物理触点的设计使得独立按键既稳定又可靠,广泛应用于各种电子设备中。

消抖措施
械按键在闭合与分开的过程中,由于机械振动(类似于弹簧效应)的存在,可能导致开
关状态在短时间内频繁切换,这种现象被称为按键抖动。
在这里插入图片描述
中的按下抖动和释放抖动的时间一般为 5~10ms,如果在抖动阶段采样,其不稳定状态可
能出现一次按键动作被认为是多次按下的情况。为了避免抖动可能带来的误操作,我们要做的
措施就是给按键消抖(即采样稳定闭合阶段)。
为了消除这种抖动,我们通常采用软件消抖和硬件消抖两种主要方法:
(1)软件消抖:主要是通过编程的方法,设定一个延迟或计时器,确保在一定的时间内只
读取一次按键状态,避免抖动对程序的影响。
(2)硬件消抖:在按键电路中加入元器件如电阻、电容组成的 RC 低通滤波器,对按键信
号进行平滑处理,降低抖动的影响。
我们例程中使用最简单的延时消抖。检测到按键按下后,一般进行 10ms 延时,用于跳过抖
动的时间段,如果消抖效果不好可以调整这个 10ms 延时,因为不同类型的按键抖动时间可能有偏差。待延时过后再检测按键状态,如果没有按下,那我们就判断这是抖动或者干扰造成的。
如果还是按下,那么我们就认为这是按键真的按下了。对按键释放的判断同理。

例程现象

该例程比较简单,只有一个 c 文件。我们点击打开 main.c 文件,可以看到在这个文件中也只有 40 多行代码。
我们从 app_main 函数开始看起。(当你看别人写的单片机代码时,先找到主函数,然后按照从上到下的顺序,看主函数中的语句,就能快速的读懂别人写的程序。)
app_main 函数内容如下所示:
在这里插入图片描述

#include <stdio.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"
static QueueHandle_t gpio_evt_queue = NULL;  // 定义队列句柄

// GPIO中断服务函数
static void IRAM_ATTR gpio_isr_handler(void* arg)
{
    uint32_t gpio_num = (uint32_t) arg;  // 获取入口参数
    xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL); // 把入口参数值发送到队列
}

// GPIO任务函数
static void gpio_task_example(void* arg)
{
    uint32_t io_num; // 定义变量 表示哪个GPIO
    for(;;) {
        if(xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {  // 死等队列消息
            printf("GPIO[%"PRIu32"] intr, val: %d\n", io_num, gpio_get_level(io_num)); // 打印相关内容
        }
    }
}
void app_main(void)
{
    gpio_config_t io0_conf = {
        .intr_type = GPIO_INTR_NEGEDGE, // 下降沿中断
        .mode = GPIO_MODE_INPUT, // 输入模式
        .pin_bit_mask = 1<<GPIO_NUM_0, // 选择GPIO0
        .pull_down_en = 0, // 禁能内部下拉
        .pull_up_en = 1 // 使能内部上拉
    };
    // 根据上面的配置 设置GPIO
    gpio_config(&io0_conf);

    // 创建一个队列处理GPIO事件
    gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
    // 开启GPIO任务
    xTaskCreate(gpio_task_example, "gpio_task_example", 2048, NULL, 10, NULL);
    // 创建GPIO中断服务
    gpio_install_isr_service(0);
    // 给GPIO0添加中断处理
    gpio_isr_handler_add(GPIO_NUM_0, gpio_isr_handler, (void*) GPIO_NUM_0);
}

连接开发板到电脑,在 VSCode 上选择串口号,选择目标芯片为 esp32s3,串口下载方式,然后点击“一键三联”按钮,等待编译下载打开终端。终端打开后,按 BOOT 按键,就可以在终端检测到按键按下,并输出按键的电平,如下所示,截取了最后几行。

I (305) main_task: Calling app_main()
I (305) gpio: GPIO[0]| InputEn: 1| OutputEn: 0| OpenDrain: 0| Pullup: 1| Pulldown: 0| Intr:2
I (315) main_task: Returned from app_main()
GPIO[0] intr, val: 0
GPIO[0] intr, val: 0
GPIO[0] intr, val: 0
GPIO[0] intr, val: 0

例程制作过程

总的来说,该例程是使用 IDF 例程中的 sample_project 工程(看工程名字就可以知道,这是一个“样例工程”)作为模板,参考例程是 IDF 例程中的 generic_gpio 例程,都是从这个例程中复制粘贴修改而来。
我们复制官方例程中的 sample_project 工程,工程路径为 examples\get-started\sample_project。把这个文件夹的名称修改为 01-boot_key,或者 boot_key。01 表示这是第 1 个例程,加 01 是为了工程例程多了以后看起来整洁,加不加都可以。修改后我的工程路径为 D:\esp32s3\01-boot_key。
使用 VSCode 打开 boot_key 这个文件夹。单击打开工程一级目录下的 CMakeLists.txt 文件(注意不是 main 目录下的),然后我们把工程名字修改为 gpio_key,保存后关闭此文件。

project(boot_key)

我们现在需要实现按键中断,比较简单,所以在这个工程上写就可以了。

现在再打开一个 VSCode 软件,然后打开 esp-idf 整个工程文件夹,然后我们依次找到 examples\peripherals\gpio\generic_gpio 这个工程作为参考,注意不要修改这个工程中的内容和配置,只是作为参考。
我们单击 gpio_example_main.c 打开这个文件,找到 app_main 函数。
复制它的前几行语句(第 80~93 行)到我们自己的 gpio_key 工程中,这几行语句如下所示:

#include <stdio.h>

void app_main(void)
{
    //zero-initialize the config structure.
    gpio_config_t io_conf = {};
    //disable interrupt
    io_conf.intr_type = GPIO_INTR_DISABLE;
    //set as output mode
    io_conf.mode = GPIO_MODE_OUTPUT;
    //bit mask of the pins that you want to set,e.g.GPIO18/19
    io_conf.pin_bit_mask = GPIO_OUTPUT_PIN_SEL;
    //disable pull-down mode
    io_conf.pull_down_en = 0;
    //disable pull-up mode
    io_conf.pull_up_en = 0;
    //configure GPIO with the given settings
    gpio_config(&io_conf);
}

然后按照开发板上 BOOT 按键连接的是 GPIO0 进行修改。
第 1 条语句,定义了一个 gpio_config_t 结构体变量。
第 2 条语句,定义引脚中断类型。开发板上的按键没有按下的时候是高电平,按下去以后是低电平,我们定义成下降沿中断。这里原来是 GPIO_INTR_DISABLE,表示中断关闭,这里我们修改为 GPIO_INTR_NEGEDGE,即下降沿中断。这些宏定义在 gpio_types.h 文件中被定义,我们在 gpio_example_main.c 文件中的 GPIO_INTR_DISABLE 上单击右键,然后选择“转到定义”,就可以找到这几个宏定义,如下所示:

typedef enum {
    GPIO_INTR_DISABLE = 0,     /*!< Disable GPIO interrupt                             */
    GPIO_INTR_POSEDGE = 1,     /*!< GPIO interrupt type : rising edge                  */
    GPIO_INTR_NEGEDGE = 2,     /*!< GPIO interrupt type : falling edge                 */
    GPIO_INTR_ANYEDGE = 3,     /*!< GPIO interrupt type : both rising and falling edge */
    GPIO_INTR_LOW_LEVEL = 4,   /*!< GPIO interrupt type : input low level trigger      */
    GPIO_INTR_HIGH_LEVEL = 5,  /*!< GPIO interrupt type : input high level trigger     */
    GPIO_INTR_MAX,
} gpio_int_type_t;

第 3 条语句是配置模式,这里的模式是 GPIO_MODE_OUTPUT,我们修改为 GPIO_MODE_INPUT 输入模式。
第 4 条语句是配置选择哪个引脚,这里我们把 GPIO_OUTPUT_PIN_SEL 修改为 1<<GPIO_NUM_0,因为 BOOT 按键连接到了 GPIO0。
第 5、6 条语句配置是否打开上下拉电阻,0 是关闭,1 是打开,我们把上拉打开。
前面都是给结构体成员变量赋值,最后一句使用 gpio_config 函数进行配置。

void app_main(void)
{
    //zero-initialize the config structure.
    gpio_config_t io_conf = {};
    //falling edge interrupt
    io_conf.intr_type = GPIO_INTR_NEGEDGE;
    //set as input mode
    io_conf.mode = GPIO_MODE_INPUT;
    //bit mask of the pins GPIO0
    io_conf.pin_bit_mask = 1<<GPIO_NUM_0;
    //disable pull-down mode
    io_conf.pull_down_en = 0;
    //enable pull-up mode
    io_conf.pull_up_en = 1;
    //configure GPIO with the given settings
    gpio_config(&io_conf);
}

上面的代码,总结来说一下,就是先定义一个 GPIO 结构体,然后给 GPIO 结构体成员变量赋值,然后使用 GPIO 配置函数配置 GPIO。给结构体成员变量赋值,也可以在定义的时候直接赋值,也就是可以把前面的代码改成如下所示代码。

void app_main(void)
{
    gpio_config_t io_conf = {
        .intr_type = GPIO_INTR_NEGEDGE, //falling edge interrupt
        .mode = GPIO_MODE_INPUT, //set as input mode
        .pin_bit_mask = 1<<GPIO_NUM_0, //bit mask of the pins GPIO0
        .pull_down_en = 0, //disable pull-down mode
        .pull_up_en = 1 //enable pull-up mode
    };
    //configure GPIO with the given settings
    gpio_config(&io_conf);
}

接下来,我们再复制 gpio_example_main.c 文件中的第 108~116 行代码到我们的 main.c 文件中,放到刚才复制的代码后面就行。

//create a queue to handle gpio event from isr
    gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
    //start gpio task
    xTaskCreate(gpio_task_example, "gpio_task_example", 2048, NULL, 10, NULL);

    //install gpio isr service
    gpio_install_isr_service(ESP_INTR_FLAG_DEFAULT);
    //hook isr handler for specific gpio pin
    gpio_isr_handler_add(GPIO_INPUT_IO_0, gpio_isr_handler, (void*) GPIO_INPUT_IO_0);

接下来我们修改这几行语句。

第 1 条代码,创建了一个队列,队列消息数量为 10,gpio_evt_queue 是队列句柄,一会儿需要我们在 main 函数外面定义。
第 2 条代码,创建了一个任务,任务名称为 gpio_task_example。
第 3 条代码,启动 GPIO 中断服务,其中 ESP_INTR_FLAG_DEFAULT 的值是 0,这个宏定义是在 gpio_example_main.c 文件中定义的,我们可以直接把这里改成 0,也可以把这个宏定义复制到我们的 main.c 文件中。
第 4 条代码,添加某个 GPIO 的中断,这里我们添加 GPIO0,第 1 个和第 3 个参数,都修改为 GPIO_NUM_0。第 1 个参数指定哪个 GPIO 产生中断。第 2 个参数是中断服务函数的名称,我们之后会以这个名称定义函数。第 3 个参数是中断服务函数的参数,我们定义了 GPIO_NUM_0,发生中断时,这个值将作为参数进入中断服务函数。

void app_main(void)
{
    gpio_config_t io_conf = {
        .intr_type = GPIO_INTR_NEGEDGE, //falling edge interrupt
        .mode = GPIO_MODE_INPUT, //set as input mode
        .pin_bit_mask = 1<<GPIO_NUM_0, //bit mask of the pins GPIO0
        .pull_down_en = 0, //disable pull-down mode
        .pull_up_en = 1 //enable pull-up mode
    };
    //configure GPIO with the given settings
    gpio_config(&io_conf);

    //create a queue to handle gpio event from isr
    gpio_evt_queue = xQueueCreate(10, sizeof(uint32_t));
    //start gpio task
    xTaskCreate(gpio_task_example, "gpio_task_example", 2048, NULL, 10, NULL);

    //install gpio isr service
    gpio_install_isr_service(0);
    //hook isr handler for specific gpio pin
    gpio_isr_handler_add(GPIO_NUM_0, gpio_isr_handler, (void*) GPIO_NUM_0);
}

接下来,我们在 app_main 函数的上方添加队列、中断等相关的代码。复制 gpio_example_main.c 中的第 60~76 行到我们的 main.c 文件中,放到 app_main 函数的上方。

static QueueHandle_t gpio_evt_queue = NULL;

static void IRAM_ATTR gpio_isr_handler(void* arg)
{
    uint32_t gpio_num = (uint32_t) arg;
    xQueueSendFromISR(gpio_evt_queue, &gpio_num, NULL);
}

static void gpio_task_example(void* arg)
{
    uint32_t io_num;
    for(;;) {
        if(xQueueReceive(gpio_evt_queue, &io_num, portMAX_DELAY)) {
            printf("GPIO[%"PRIu32"] intr, val: %d\n", io_num, gpio_get_level(io_num));
        }
    }
}

接下来,我们再把需要的头文件添加到我们的 main.c 文件就可以了。
我们复制 gpio_example_main.c 中的第 9~16 行到我们的 main.c 文件中,放到 main.c 文件的最上方。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <inttypes.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"
#include "driver/gpio.h"

使用 printf 函数,需要添加 stdio.h 头文件。string.h 和 stdlib.h 我们这里用不着,可以去掉。接下来是 3 个 freeRTOS 的头文件,最后一个头文件是用于 gpio 的配置。
操作完上面的代码,就可以编译下载看结果了。
这里需要注意的是,menuconfig 里面,需要把 FLASH 大小设置为 16MB,默认是 2MB,其它不用修改。
编译下载后,结果没有问题的话,使用 idf.py save-defconfig 命令生成 sdkconfig.defaults 文件。这个命令要打开“命令终端”执行,看结果的“串口终端”不行。打开“命令终端”的按钮如下所示:
在这里插入图片描述

打开终端后,输入 idf.py save-defconfig 命令。
回车执行命令后,会看到工程中多了一个 sdkconfig.defaults 文件。

点击打开 sdkconfig.defaults 文件,会看到里面的内容。这个文件里面包含了你对 menuconfig 的修改。
这时候,你可以把工程中配置和编译生成的文件夹全部去掉,最后的文件如下所示:
在这里插入图片描述
使用 VSCode 重新打开工程,在选择目标芯片后,sdkconfig.defaults 文件里面的配置就配置到 menuconfig 里面了,省去了手动配置 menucofig。本例程只配置了 FLASH 大小,后面的例程中,menuconfig 里面配置的内容会越来越多,到那时,这个文件就显得很有必要了。

按键控制LED实验

在 IDF 版的 02_key 例程中,作者在 01_key\components\BSP 路径下新增了一个 KEY 文件夹,用于存放key.c和key.h这两个文件。其中,key.h文件负责声明KEY相关的函数和变量,而key.c文件则实现了 KEY 的驱动代码。下面,我们将详细解析这两个文件的实现内容。
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 宏,用于获取 IO0 的
状态,并声明了 key_init 和 key_scan 函数,以便外部文件能够调用这些函数。通过这些声明和定义,该文件为其他部分的代码提供了必要的接口和功能支持。

key.c 文件
此文件中定义了两个函数,分别为 key_init和 key_scan。接下来,我将对这两个函数进行详
细的解析。
key_init 函数

* @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); /* 配置使能 */
}

该函数主要配置 IO0 管脚为输入模式,这样就可以获取 IO0 的电平状态了。

key_scan 函数

**
* @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 时,表示
按键不支持连续按下;反之,则支持连续按下。值得注意的是,该函数内部已经对按键进行了
消抖延时处理,因此,在其他地方调用此函数时,无需再进行额外的按键消抖操作。

CMakeLists.txt 文件
打开本实验 BSP 下的 CMakeLists.txt 文件,其内容如下所示:

set(src_dirs
 KEY
 LED)
set(include_dirs
 KEY
 LED)
set(requires
 driver)
idf_component_register(SRC_DIRS ${src_dirs}
INCLUDE_DIRS ${include_dirs} REQUIRES ${requires})
component_compile_options(-ffast-math -O3 -Wno-error=format=-Wno-format)

打开 main/main.c 文件,该文件定义了工程入口函数,名为 app_main。该函数代码如下

**
* @brief 程序入口
* @param 无
* @retval 无
*/
void app_main(void)
{
 uint8_t key;
 esp_err_t ret;
 
 ret = nvs_flash_init(); /* 初始化 NVS */
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)
 {
  ESP_ERROR_CHECK(nvs_flash_erase());
  ret = nvs_flash_init();
 }
 led_init(); /* 初始化 LED */
 key_init(); /* KEY 初始化 */
 while(1)
 {
  key = key_scan(0); /* 获取键值 */
 switch (key)
 {
 case BOOT_PRES: /* BOOT 被按下 */
 {
 LED_TOGGLE(); /* LED 状态翻转 */
 break;
 }
 default:
 {
 break;
 }
 }
 vTaskDelay(10);
 }
}

在完成编译和烧录操作后,可以看到板子上的 LED 是处于亮起的状态,若此时按下并释放
一次 BOOT 按键,则能够看到 LED 的亮灭状态发生了一次翻转,与预期的实验现象效果相符。

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐