
目录
1. 按键
都是一些基础的知识,主要了解一下什么是按键?以及什么是按键抖动?
按键:常见的输入设备,按下导通,松手断开;

按键抖动:由于按键内部使用的是机械式弹簧片来进行通断的,所以在按下和松手的瞬间会伴随有一连串的抖动。

按键抖动会导致按键按下一次,结果出现多次连续触发,不利于逻辑梳理,因此按键的消抖非常有必要,按键的消抖常见的方法有软件消抖和硬件消抖。
1.1 软件消抖
软件消抖的原理一般是跳过按键抖动状态,读取按键稳定时IO口的电平作为按键状态。常用的方法:延时消抖法、计数消抖法和状态机法。
- 延时消抖法:就是在检测到按键按下时延时一段时间(比如10ms)再次读取按键IO的电平,检测两次读取的结果是否相同,相同则认为按键被按下。
- 计数消抖法:周期执行按键扫描程序在检测到按键变化时备份输入状态,清零计数器并开始计数,直到检测到按键弹起,如果计数值大于某个设定值则认为按键动作有效,否则按键动作无效。
- 状态机法:利用状态机的思想,通过定义按键的不同状态(如按下、释放、抖动等),根据输入信号和当前状态决定下一个状态,从而实现消抖。
1.2 硬件消抖
硬件消抖通常通过RC电路实现。在RC消抖电路中,电阻起到限流的作用,电容则用来储存电荷。当输入信号发生变化时,电容会通过电阻进行充放电,从而实现对信号的平滑处理。通过合理选择电阻和电容的数值,可以达到最佳的消抖效果。
常见电路如下:

2. 中断
2.1 中断简介
中断:在主程序运行过程中,出现了特定的中断触发条件(中断源),使得CPU暂停当前正在运行的程序,转而去处理中断程序,处理完成后又返回原来被暂停的位置继续运行;

中断优先级:当有多个中断源同时申请中断时,CPU会根据中断源的轻重缓急进行裁决,优先响应更加紧急的中断源;
中断嵌套:当一个中断程序正在运行时,又有新的更高优先级的中断源申请中断,CPU再次暂停当前中断程序,转而去处理新的中断程序,处理完成后依次进行返回。

2.2 外部中断简介
EXTI可以监测指定GPIO口的电平信号,当其指定的GPIO口产生电平变化时,EXTI将立即向NVIC发出中断申请,经过NVIC裁决后即可中断CPU主程序,使CPU执行EXTI对应的中断程序;
支持的触发方式:上升沿/下降沿/双边沿/软件触发。
3. ESP32-S3中断分配
ESP32-S3 有两个核,每个核有 32 个中断。每个中断都有一个确定的优先级别,大多数中断(但不是全部)都连接到中断矩阵。
由于中断源数量多于中断,有时多个驱动程序可以共用一个中断。中断分配器提供两种不同的中断类型:共享中断和非共享中断,这两种中断需要不同处理方式。
- 非共享中断在每次调用 esp_intr_alloc() 时,都会分配一个单独的中断,该中断仅用于与其相连的外设,只调用一个 ISR。
- 共享中断则可以由多个外设触发,当其中一个外设发出中断信号时,会调用多个 ISR。因此,针对共享中断的 ISR 应检查对应外设的中断状态,以确定是否需要采取任何操作。
注意:非共享中断可由电平或边缘触发。共享中断只能由电平触发,因为使用边缘触发可能会错过中断。
至于原因,为什么共享中断只能由电平触发?我们举个例子,以外设 A 和外设 B 共用一个边缘触发中断为例,当外设 B 触发中断时,会将其中断信号设置为高电平,产生一个从低到高的边缘,进而锁存 CPU 中断位,并触发 ISR。接着,ISR 开始执行,检查到此时外设 A 没有触发中断,于是继续处理外设 B 的中断信号,最后将外设 B 的中断状态清除。最后,CPU 会在 ISR 返回之前清除中断位锁存器。因此在整个中断处理过程中,如果外设 A 触发了中断,该中断会因 CPU 清除中断位锁存器而丢失。
对于优先级的分配,在 ESP32-S3 上,中断优先级由 FreeRTOS 和 硬件中断控制器(XTensa LX7 处理器) 共同管理。
每个 Xtensa CPU 核都有六个内部外设:
- 三个定时器比较器
- 一个性能监视器
- 两个软件中断
/** @addtogroup Intr_Alloc_Pseudo_Src
* @{
*/
/**
* The esp_intr_alloc* functions can allocate an int for all ETS_*_INTR_SOURCE interrupt sources that
* are routed through the interrupt mux. Apart from these sources, each core also has some internal
* sources that do not pass through the interrupt mux. To allocate an interrupt for these sources,
* pass these pseudo-sources to the functions.
*/
#define ETS_INTERNAL_TIMER0_INTR_SOURCE -1 ///< Platform timer 0 interrupt source
#define ETS_INTERNAL_TIMER1_INTR_SOURCE -2 ///< Platform timer 1 interrupt source
#define ETS_INTERNAL_TIMER2_INTR_SOURCE -3 ///< Platform timer 2 interrupt source
#define ETS_INTERNAL_SW0_INTR_SOURCE -4 ///< Software int source 1
#define ETS_INTERNAL_SW1_INTR_SOURCE -5 ///< Software int source 2
#define ETS_INTERNAL_PROFILING_INTR_SOURCE -6 ///< Int source for profiling
#define ETS_INTERNAL_UNUSED_INTR_SOURCE -99 ///< Interrupt is not assigned to any source
//翻译一下
/**
esp_intr_alloc* 系列函数可以为所有通过中断复用器(interrupt mux)路由的 ETS_*_INTR_SOURCE 中断源分配中断。
除了这些中断源外,每个内核还有一些不经过中断复用器的内部中断源。若要为这些内部中断源分配中断,
需将这些伪中断源(pseudo-sources)作为参数传递给函数。
*/
#define ETS_INTERNAL_TIMER0_INTR_SOURCE -1 ///< 平台定时器 0 中断源
#define ETS_INTERNAL_TIMER1_INTR_SOURCE -2 ///< 平台定时器 1 中断源
#define ETS_INTERNAL_TIMER2_INTR_SOURCE -3 ///< 平台定时器 2 中断源
#define ETS_INTERNAL_SW0_INTR_SOURCE -4 ///< 软件中断源 1
#define ETS_INTERNAL_SW1_INTR_SOURCE -5 ///< 软件中断源 2
#define ETS_INTERNAL_PROFILING_INTR_SOURCE -6 ///< 性能分析专用中断源
#define ETS_INTERNAL_UNUSED_INTR_SOURCE -99 ///< 未分配给任何源的中断(保留未使用)

其上有 7 个可编程优先级级别(0~6),其中 0 是最低优先级,6 是最高优先级。
/** @brief Interrupt allocation flags
*
* These flags can be used to specify which interrupt qualities the
* code calling esp_intr_alloc* needs.
*
*/
//Keep the LEVELx values as they are here; they match up with (1<<level)
#define ESP_INTR_FLAG_LEVEL1 (1<<1) ///< Accept a Level 1 interrupt vector (lowest priority)
#define ESP_INTR_FLAG_LEVEL2 (1<<2) ///< Accept a Level 2 interrupt vector
#define ESP_INTR_FLAG_LEVEL3 (1<<3) ///< Accept a Level 3 interrupt vector
#define ESP_INTR_FLAG_LEVEL4 (1<<4) ///< Accept a Level 4 interrupt vector
#define ESP_INTR_FLAG_LEVEL5 (1<<5) ///< Accept a Level 5 interrupt vector
#define ESP_INTR_FLAG_LEVEL6 (1<<6) ///< Accept a Level 6 interrupt vector
#define ESP_INTR_FLAG_NMI (1<<7) ///< Accept a Level 7 interrupt vector (highest priority)
#define ESP_INTR_FLAG_SHARED (1<<8) ///< Interrupt can be shared between ISRs
#define ESP_INTR_FLAG_EDGE (1<<9) ///< Edge-triggered interrupt
#define ESP_INTR_FLAG_IRAM (1<<10) ///< ISR can be called if cache is disabled
#define ESP_INTR_FLAG_INTRDISABLED (1<<11) ///< Return with this interrupt disabled
#define ESP_INTR_FLAG_LOWMED (ESP_INTR_FLAG_LEVEL1|ESP_INTR_FLAG_LEVEL2|ESP_INTR_FLAG_LEVEL3) ///< Low and medium prio interrupts. These can be handled in C.
#define ESP_INTR_FLAG_HIGH (ESP_INTR_FLAG_LEVEL4|ESP_INTR_FLAG_LEVEL5|ESP_INTR_FLAG_LEVEL6|ESP_INTR_FLAG_NMI) ///< High level interrupts. Need to be handled in assembly.
/** Mask for all level flags */
#define ESP_INTR_FLAG_LEVELMASK (ESP_INTR_FLAG_LEVEL1|ESP_INTR_FLAG_LEVEL2|ESP_INTR_FLAG_LEVEL3| \
ESP_INTR_FLAG_LEVEL4|ESP_INTR_FLAG_LEVEL5|ESP_INTR_FLAG_LEVEL6| \
ESP_INTR_FLAG_NMI)
//翻译一下
/** @brief 中断分配标志位
这些标志用于指定调用 esp_intr_alloc* 的代码所需的中断特性。
*/
// 保持 LEVELx 的当前值不变,它们与 (1<<level) 对应
#define ESP_INTR_FLAG_LEVEL1 (1<<1) ///< 接受优先级 1 的中断向量(最低优先级)
#define ESP_INTR_FLAG_LEVEL2 (1<<2) ///< 接受优先级 2 的中断向量
#define ESP_INTR_FLAG_LEVEL3 (1<<3) ///< 接受优先级 3 的中断向量
#define ESP_INTR_FLAG_LEVEL4 (1<<4) ///< 接受优先级 4 的中断向量
#define ESP_INTR_FLAG_LEVEL5 (1<<5) ///< 接受优先级 5 的中断向量
#define ESP_INTR_FLAG_LEVEL6 (1<<6) ///< 接受优先级 6 的中断向量
#define ESP_INTR_FLAG_NMI (1<<7) ///< 接受优先级 7 的中断向量(最高优先级,不可屏蔽中断 NMI)
#define ESP_INTR_FLAG_SHARED (1<<8) ///< 中断可在多个 ISR 之间共享
#define ESP_INTR_FLAG_EDGE (1<<9) ///< 边沿触发中断
#define ESP_INTR_FLAG_IRAM (1<<10) ///< 即使缓存禁用,仍可调用此 ISR(必须位于 IRAM 中)
#define ESP_INTR_FLAG_INTRDISABLED (1<<11) ///< 分配时默认禁用此中断(需手动启用)
#define ESP_INTR_FLAG_LOWMED (ESP_INTR_FLAG_LEVEL1|ESP_INTR_FLAG_LEVEL2|ESP_INTR_FLAG_LEVEL3) ///< 低/中优先级中断。这些中断可在 C 代码中处理。
#define ESP_INTR_FLAG_HIGH (ESP_INTR_FLAG_LEVEL4|ESP_INTR_FLAG_LEVEL5|ESP_INTR_FLAG_LEVEL6|ESP_INTR_FLAG_NMI) ///< 高优先级中断。需在汇编中处理(响应时间敏感)。
/** 所有优先级标志的掩码 */
#define ESP_INTR_FLAG_LEVELMASK (ESP_INTR_FLAG_LEVEL1|ESP_INTR_FLAG_LEVEL2|ESP_INTR_FLAG_LEVEL3|
ESP_INTR_FLAG_LEVEL4|ESP_INTR_FLAG_LEVEL5|ESP_INTR_FLAG_LEVEL6|
ESP_INTR_FLAG_NMI)

定义 FreeRTOS 可以管理的中断的最高优先级(数值越小,优先级越高),优先级高于此值的中断(4~6)不会受 FreeRTOS 影响(不可调用 FreeRTOS API)。优先级低于或等于此值的中断(0~3)可以与 FreeRTOS 交互(如调用 xQueueSendFromISR)。
| 特性 | ESP32-S3 支持情况 |
|---|---|
| 硬件优先级级别 | 7 级(0~6) |
| FreeRTOS 管理优先级 | 默认 0~3(可配置) |
| 高优先级不可抢占中断 | 4~6(避免调用 FreeRTOS API) |
| 推荐配置 | Wi-Fi/BT 用 5~6,普通任务用 1~3 |
4. 代码编写
功能设计,每当按键按下一次,LED灯电平翻转一次。
4.1 LED初始化配置
调用 gpio_config_t 函数定义结构体变量,对所要配置的引脚,所使用的模式,是否开启下拉电阻,是否开启上拉电阻,使用那种中断模式等进行配置:
void LED_Init(void)
{
gpio_config_t io_conf;// 配置GPIO引脚
io_conf.pin_bit_mask = (1ULL << GPIO_NUM_1); // 设置要配置的引脚
io_conf.mode = GPIO_MODE_INPUT_OUTPUT;// 输入 + 输出模式
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;// 禁用下拉电阻
io_conf.pull_up_en = GPIO_PULLUP_DISABLE;// 禁用上拉电阻
io_conf.intr_type = GPIO_INTR_DISABLE;// 中断模式不使用
// 配置GPIO
gpio_config(&io_conf);
gpio_set_level(GPIO_NUM_1, 1); // 设置GPIO0为高电平
}
这里对于模式的配置,开始的时候只配置成输出模式(GPIO_MODE_OUTPUT),但是在后来使用的时候发下无法进行电平翻转,后来找了一下原因,发现因为我们通过 gpio_get_level() 函数读取当前电平的状态,进行电平翻转,因此需要GPIO口支持输入模式,所以这里配置为输入+输出模式:
gpio_set_level(GPIO_NUM_1, !gpio_get_level(GPIO_NUM_1));
另一种解决方法是在纯输出模式下,通过一个变量用来存储状态,通过翻转变量进行翻转电平:
static bool led_state = false;
led_state = !led_state;
gpio_set_level(LED_GPIO, led_state);
4.2 KEY初始化配置
按键的初始化配置也是调用 gpio_config_t 对于其上参数进行一些配置,对于按键我们只需要输入模式,并且根据上方介绍我们要实现硬件消抖,需要加一个上拉电阻,并联一个100nF电容,因此这里我们启用上拉电阻,禁用下拉电阻,触发方式为下降沿触发:
gpio_config_t io_config;// 配置GPIO引脚
io_config.pin_bit_mask = (1ULL << GPIO_NUM_21); // 设置要配置的引脚
io_config.mode = GPIO_MODE_INPUT;// 设置为输入模式
io_config.pull_down_en = GPIO_PULLDOWN_DISABLE;// 禁用下拉电阻
io_config.pull_up_en = GPIO_PULLUP_ENABLE;// 启用上拉
io_config.intr_type = GPIO_INTR_NEGEDGE;// 下降沿触发
gpio_config(&io_config);// 配置GPIO
4.3 中断触发配置
4.3.1 触发方式
首先是中断的触发方式,其实这个函数和上面下降沿触发有些重复了,两者都是配置触发方式,把这个注释掉对函数没有影响,这里介绍一下其使用:
gpio_set_intr_type(GPIO_NUM_21,GPIO_INTR_NEGEDGE);
我们跳转到其定义,可以看到其作用就是判断我们是否在中断类型是否在有效范围内,若是不在则返回错误类型,若是在,则进入临界区域,配置触发方式,清除标志位:
esp_err_t gpio_set_intr_type(gpio_num_t gpio_num, gpio_int_type_t intr_type)
{
GPIO_CHECK(GPIO_IS_VALID_GPIO(gpio_num), "GPIO number error", ESP_ERR_INVALID_ARG);
GPIO_CHECK(intr_type < GPIO_INTR_MAX, "GPIO interrupt type error", ESP_ERR_INVALID_ARG);
portENTER_CRITICAL(&gpio_context.gpio_spinlock);
gpio_hal_set_intr_type(gpio_context.gpio_hal, gpio_num, intr_type);
if (intr_type == GPIO_INTR_POSEDGE || intr_type == GPIO_INTR_NEGEDGE || intr_type == GPIO_INTR_ANYEDGE) {
gpio_context.isr_clr_on_entry_mask |= (1ULL << (gpio_num));
} else {
gpio_context.isr_clr_on_entry_mask &= ~(1ULL << (gpio_num));
}
portEXIT_CRITICAL(&gpio_context.gpio_spinlock);
return ESP_OK;
}
其一些触发方式:
typedef enum {
GPIO_INTR_DISABLE = 0, /*!< 禁用GPIO中断 */
GPIO_INTR_POSEDGE = 1, /*!< GPIO中断类型:上升沿触发 */
GPIO_INTR_NEGEDGE = 2, /*!< GPIO中断类型:下降沿触发 */
GPIO_INTR_ANYEDGE = 3, /*!< GPIO中断类型:双边沿触发(上升沿和下降沿都触发)*/
GPIO_INTR_LOW_LEVEL = 4, /*!< GPIO中断类型:低电平触发 */
GPIO_INTR_HIGH_LEVEL = 5, /*!< GPIO中断类型:高电平触发 */
GPIO_INTR_MAX, /*!< 中断类型最大值(用于参数校验)*/
} gpio_int_type_t;
4.3.2 中断服务函数
这里通过调用 gpio_install_isr_service() 函数,为后续的中断处理函数注册做准备,其内参数就是我们上面提到的几种优先级,这里我们使用边沿触发中断:
gpio_install_isr_service(ESP_INTR_FLAG_EDGE);
注意该函数只需调用 一次(全局初始化),后续所有 GPIO 中断共享此服务。也就是我们刚刚提到的共享中断,类似STM32的中断分组,全局就只能一个中断分组。
4.3.3 中断处理函数
我们触发中断后,需要处理中断,这里就翻转一下电平,因此创建一个中断处理函数:
static void button_isr_handler(void *arg)
{
gpio_set_level(GPIO_NUM_1, !gpio_get_level(GPIO_NUM_1));
}
通过调用 gpio_isr_handler_add 函数绑定中断处理函数:
gpio_isr_handler_add(GPIO_NUM_21,button_isr_handler,NULL);
4.4 完整代码
综上我们的代码编写完成:
#include <stdio.h>
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define configTICK_RATE_HZ 1000 // 例如,1kHz 的 tick 频率
static void button_isr_handler(void *arg)
{
gpio_set_level(GPIO_NUM_1, !gpio_get_level(GPIO_NUM_1));
}
void LED_Init(void)
{
gpio_config_t io_conf;// 配置GPIO引脚
io_conf.pin_bit_mask = (1ULL << GPIO_NUM_1); // 设置要配置的引脚
io_conf.mode = GPIO_MODE_INPUT_OUTPUT;// 输入 + 输出模式
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;// 禁用下拉电阻
io_conf.pull_up_en = GPIO_PULLUP_DISABLE;// 禁用上拉电阻
io_conf.intr_type = GPIO_INTR_DISABLE;// 中断模式不使用
// 配置GPIO
gpio_config(&io_conf);
gpio_set_level(GPIO_NUM_1, 1); // 设置GPIO0为高电平
}
void Button_Init(void)
{
gpio_config_t io_config;// 配置GPIO引脚
io_config.pin_bit_mask = (1ULL << GPIO_NUM_21); // 设置要配置的引脚
io_config.mode = GPIO_MODE_INPUT;// 设置为输入模式
io_config.pull_down_en = GPIO_PULLDOWN_DISABLE;// 禁用下拉电阻
io_config.pull_up_en = GPIO_PULLUP_ENABLE;// 启用上拉
io_config.intr_type = GPIO_INTR_NEGEDGE;// 下降沿触发
gpio_config(&io_config);// 配置GPIO
gpio_set_intr_type(GPIO_NUM_21,GPIO_INTR_NEGEDGE);
gpio_install_isr_service(ESP_INTR_FLAG_EDGE);
gpio_isr_handler_add(GPIO_NUM_21,button_isr_handler,NULL);
}
void app_main(void)
{
LED_Init();
Button_Init();
while(1)
{
vTaskDelay(500/portTICK_PERIOD_MS);
}
}
我们来编译运行看一下,时间可能比较久等一下:

点击按键可以发现电平翻转,代码可以使用。
4.5 代码优化
我们在点击代码的时候会发现,随然我们加了硬件消抖但是还是会出现抖动现象,我们在加一个软件消抖,顺便优化一下代码,对于一个GPIO口增加一些宏定义,首先是宏定义方面,就是两个GPIO口改一下宏定义,方便调整,增加一个20系统节拍数的宏定义,用作后续消抖使用:
#define LED_GPIO_PIN GPIO_NUM_1
#define BUTTON_GPIO_PIN GPIO_NUM_21
#define DEBOUNCE_DELAY_MS 20 // 消抖延时
调用 xTaskGetTickCount() 函数统计当前系统节拍数,通过 last_isr_time 记录上一次中断发生的时间,二者相减大于20ms的,才会触发电平翻转,减少抖动:
static void button_isr_handler(void *arg)
{
static uint32_t last_isr_time = 0;
uint32_t now = xTaskGetTickCount();
// 简单的消抖处理
if ((now - last_isr_time) > pdMS_TO_TICKS(DEBOUNCE_DELAY_MS)) {
gpio_set_level(LED_GPIO_PIN, !gpio_get_level(LED_GPIO_PIN));
last_isr_time = now;
}
}
完整代码:
#include <stdio.h>
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#define CONFIG_TICK_RATE_HZ 1000 // 1kHz tick frequency
#define LED_GPIO_PIN GPIO_NUM_1
#define BUTTON_GPIO_PIN GPIO_NUM_21
#define DEBOUNCE_DELAY_MS 20 // 消抖延时
static void button_isr_handler(void *arg)
{
static uint32_t last_isr_time = 0;
uint32_t now = xTaskGetTickCount();
// 简单的消抖处理
if ((now - last_isr_time) > pdMS_TO_TICKS(DEBOUNCE_DELAY_MS)) {
gpio_set_level(LED_GPIO_PIN, !gpio_get_level(LED_GPIO_PIN));
last_isr_time = now;
}
}
void LED_Init(void)
{
gpio_config_t io_conf;
io_conf.pin_bit_mask = (1ULL << LED_GPIO_PIN);
io_conf.mode = GPIO_MODE_INPUT_OUTPUT;
io_conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
io_conf.pull_up_en = GPIO_PULLUP_DISABLE;
io_conf.intr_type = GPIO_INTR_DISABLE;
gpio_config(&io_conf);
gpio_set_level(LED_GPIO_PIN, 1); // 初始化为高电平
}
void Button_Init(void)
{
gpio_config_t io_config;
io_config.pin_bit_mask = (1ULL << BUTTON_GPIO_PIN);
io_config.mode = GPIO_MODE_INPUT;
io_config.pull_down_en = GPIO_PULLDOWN_DISABLE;
io_config.pull_up_en = GPIO_PULLUP_ENABLE;
io_config.intr_type = GPIO_INTR_NEGEDGE;
gpio_config(&io_config);
gpio_set_intr_type(BUTTON_GPIO_PIN, GPIO_INTR_NEGEDGE);
gpio_install_isr_service(ESP_INTR_FLAG_EDGE);
gpio_isr_handler_add(BUTTON_GPIO_PIN, button_isr_handler, NULL);
}
void app_main(void)
{
LED_Init();
Button_Init();
while(1) {
vTaskDelay(500 / portTICK_PERIOD_MS);
}
}
这里可以自行下载测试一下。


2318

被折叠的 条评论
为什么被折叠?



