(二)ESP32-S3之BOOT-KEY 按键
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 的亮灭状态发生了一次翻转,与预期的实验现象效果相符。
更多推荐



所有评论(0)