4. 按键点灯
4.1 独立按键基础知识
独立按键是一种简单的输入设备,广泛应用于各种电子设备中,用于实现基本的用户交互。它们的工作原理通常基于一个简单的机械开关,当按下按键时触发某些操作。独立按键可以有多种尺寸、形状和颜色,便于用户辨识和使用。
4.2 独立按键结构组成
独立按键的主要结构组成包括:按钮、外壳、弹簧、触点、导电片和引脚。当按下按键时,导电片触碰到触点,从而形成一个闭合电路。
4.3 独立按键原理
独立按键原理主要是基于机械触点和电气触点之间的关系。当按键未被按下时,通常触点是分开的,电路是断开的。当用户按下按键时,触点在弹簧和导电片的作用下形成闭合,这时电路连通,微控制器能够读取到该按键触发的信号。
4.4 消抖措施
由于机械按键在闭合和分开时可能产生的机械振动(相当于弹簧),导致开关状态在短时间内多次变换,这就是按键抖动现象。消抖措施主要分为软件消抖和硬件消抖:
- 软件消抖:主要是通过编程的方法,设定一个延迟或计时器,确保在一定的时间内只读取一次按键状态,避免抖动对程序的影响。
- 硬件消抖:在按键电路中加入元器件如电阻、电容组成的RC低通滤波器,对按键信号进行平滑处理,降低抖动的影响。
4.5 独立按键驱动原理
独立按键驱动是为了让微控制器能识别按键的状态,而微控制器正好可以识别高电平和低电平,所以大多数的按键都是通过给按键的一端接入高电平,一端接入GPIO;或者是给按键的一端接入低电平,一端接入GPIO。通过检测连接按键的引脚有没有发生电平变化,就可以知道按键是否按下。
4.6 独立按键原理图
开发板原理图中,将按键一端通过上拉电阻R14接到的3.3V的高电平和引脚GPIO0上,另一端接到了GND(低电平)。将开发板的按键电路简化,得到下图。
开发板一上电,GPIO0引脚就会因为R14这个上拉电阻变为高电平,所以当按键没有按下的时候,GPIO0引脚默认为高电平;当按键按下时,因为按键闭合,GPIO0通过按键接到了GND上,所以GPIO0变为了低电平;
4.7 独立按键驱动流程
使用按键可以分为以下几个步骤:
- 配置 GPIO 为输入模式
在 ESP32 上所有的 IO 口都是通过 GPIO 来进行控制的,因此我们需要将需要使用的 GPIO 配置为输入模式,以检测按键的状态。
例如,如果我们需要使用GPIO0
来检测按键状态,可以通过以下方式来将其配置为输入模式:
gpio_config_t io_conf; //定义GPIO初始化结构体
io_conf.mode = GPIO_MODE_INPUT; //输入模式
io_conf.pull_up_en = GPIO_PULLUP_ENABLE; //使能上拉
io_conf.pin_bit_mask = (1ULL<<GPIO_NUM_0); //设置引脚为GPIO0
gpio_config(&io_conf); //将以上配置写入寄存器
2
3
4
5
这里的 pin_bit_mask
表示开启的 GPIO 引脚 Bit mask,因为 ESP32 支持一次配置多个 GPIO 引脚,使用 Bit mask 来指定需要配置的 GPIO 引脚。
主要修改的地方就是 .mode = GPIO_MODE_INPUT;
将GPIO模式修改为输入模式,其余与LED章节一致;
2. 轮询检测按键状态
在配置好 GPIO 之后,我们需要不断轮询检测按键状态,以获取按键事件并执行相应的操作。
我们可以通过如下代码来获取 GPIO0
引脚的状态:
if(gpio_get_level(0) == 0)
{
// 如果 GPIO0 对应的引脚逻辑为 0,则表示按键被按下
// 执行相应操作
}
2
3
4
5
gpio_get_level()
是一个 ESP-IDF 中的函数,用于获取指定 GPIO 引脚的逻辑电平。
函数原型如下:
int gpio_get_level(gpio_num_t gpio_num);
参数 gpio_num
表示需要获取电平的 GPIO 引脚编号,类型为 gpio_num_t
。
函数返回值为 int
类型,表示指定 GPIO 引脚的逻辑电平。返回值为 0 表示引脚为低电平 (logic level 0),返回值为 1 表示引脚为高电平 (logic level 1)。
注意事项:
- 使用
gpio_get_level()
函数之前,需要先配置对应的 GPIO 引脚为输入模式,否则可能会得到错误的电平值。 - 在读取 GPIO 电平之前,需要知道GPIO是否有设置上下拉电阻,以确保在未连接按键时能够正确读取电平状态。例如,假设设置为了下拉电阻,再使用语句
if(gpio_get_level(0) == 0)
则无法判断,因为该因为下拉电阻的存在,导致引脚一直是低电平。
- 如有按键抖动现象,应用消抖措施
按键需要消抖是因为它可能会在按下或松开时产生抖动现象。当我们按下一个机械按键时,机械部件的接触可能会因为各种因素导致颤动,这就会导致按键连接的电路在短时间内产生多次开关状态的切换。这种切换会导致系统错误地认为按键被按下了多次,造成按键操作的重复执行或干扰其他命令的执行。消抖的作用是在按键信号产生时,瞬间过滤掉抖动信号,确保只有一个有效的按键信号被识别响应,避免误操作。
为了解决按键抖动问题,我们可以使用软件或硬件方案实现按键消抖。在软件方案中,通过设定一定的抖动消除时间和处理逻辑,使得按键信号被按下和松开的变化稳定处理为一个有效信号。
//如果按键按下使GPIO0为低电平
if( gpio_get_level(0) == 0 )
{
//延时100ms,等待按键抖动的时间过去
vTaskDelay(100 / portTICK_PERIOD_MS);
//点亮LED,LED引脚为GPIO48
gpio_set_level(48, 0);
}
2
3
4
5
6
7
8
以上是最简单的一种延时消抖方法,还有很多种消抖方式。
- 状态判断消抖:在按键信号改变时,记录按键状态,并在一段时间内持续检测按键的状态。只有在状态持续稳定一段时间后,才认定为有效的按键触发。这可以通过比较多次采样的结果来实现。
- 滑动窗口消抖:使用一个滑动窗口来记录按键触发的历史状态。在每次检测按键状态时,计算窗口内按键状态的平均值或多数票决,只有当平均值或多数票决结果表示按键稳定时,才认定为有效的按键触发。
- 边缘触发消抖:检测按键信号的上升沿和下降沿(也称为边缘触发)。只有在检测到一个边缘触发后,在一段时间内不再检测到其他边缘触发,才认定为有效的按键触发。 在硬件消抖方案中,通过在电路中添加RC电路、滤波器等元件进行抖动滤波,也可以实现减少抖动信号的作用。注意是减少,不是消除。
4.8 按键点灯验证
设置LED引脚为输出模式,设置按键引脚为输入模式。当按键按下时,先进行按键消抖,再判断一次是否按下按键。确定按下按键后将执行LED状态变化的操作。(关于LED的代码请参考LED章节。)
创建两个文件,bsp_key.c和bsp_key.h。加入头文件路径。
在 bsp_key.c
中编写以下代码:
#include "bsp_key.h"
//配置引脚寄存器
#define GPIO_INPUT_PIN_SEL (1ULL<<KEY_PIN)
/**
* @函数说明 按键引脚初始化
* @传入参数 无
* @函数返回 无
*/
void KeyGpioConfig(void)
{
//初始化GPIO配置结构 为 零
gpio_config_t io_conf = {};
//失能中断
io_conf.intr_type = GPIO_INTR_DISABLE;
//设置输入引脚
io_conf.pin_bit_mask = GPIO_INPUT_PIN_SEL;
//设置输入模式
io_conf.mode = GPIO_MODE_INPUT;
//使能上拉电阻
io_conf.pull_up_en = 1;
//失能下拉电阻
io_conf.pull_down_en = 0;
////用给定的设置配置GPIO
gpio_config(&io_conf);
}
/**
* @函数说明 获取按键引脚上的电平状态
* @传入参数 无
* @函数返回 0=按键被按下 1=按键没有被按下
*/
bool GetKeyValue(void)
{
//如果按键状态为0
if( gpio_get_level(KEY_PIN) == 0 )
{
//延时消抖,使用该延时需要加入对应头文件
vTaskDelay(100 / portTICK_PERIOD_MS);
// 如果按键状态还是0,说明按键真的按下
if( gpio_get_level(KEY_PIN) == 0 )
{
return 0;
}
}
return 1;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
在 bsp_key.h
中编写以下代码:
#ifndef _BSP_KEY_H_
#define _BSP_KEY_H_
#include "driver/gpio.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
//设置按键的引脚
#define KEY_PIN 0
/**
* @函数说明 按键引脚初始化
* @传入参数
* @函数返回
*/
void KeyGpioConfig(void);
/**
* @函数说明 获取按键引脚上的电平状态
* @传入参数 无
* @函数返回 0=按键被按下 1=按键没有被按下
*/
bool GetKeyValue(void);
#endif
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
在 main.c
中编写以下代码:
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "bsp_led.h"
#include "bsp_key.h"
void app_main(void)
{
int cnt = 0;
//LED初始化
LedGpioConfig();
//按键初始化
KeyGpioConfig();
while(1) {
//因ESP32S3采用的是RTOS方式运行,必须在死循环中加入延时让其能够正常运转
vTaskDelay(20 / portTICK_PERIOD_MS);
//如果按键有按下
if( GetKeyValue() == 0 )
{
//使LED状态取反。LED引脚为GPIO48
gpio_set_level(LED_PIN, cnt = !cnt);
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
WARNING
📌 为什么要在死循环中加入延时?
在 ESP32 的 ESP-IDF 环境下,死循环本身并不需要加上延时。死循环是指在程序中出现了一个循环结构,其中的条件永远为真,导致程序一直在该循环中执行,而无法继续往后执行其他代码。
然而,为了避免死循环对系统产生过大的负荷,核心代码部分启动了看门狗,用于监视程序的运行状态。如果程序在某个任务或死循环中占用 CPU 时间过长,没有及时喂狗(即复位 Watchdog 定时器),就会导致 Watchdog 定时器超时,进而触发系统重启。故一般使用时需要加入延时,释放CPU。这是为了让系统有一定的空闲时间去处理其他任务,也可以说是通过添加延时来减轻系统的负荷。
在死循环中加入延时可以避免 CPU 过度占用,在单核系统上尤为重要。即使当前任务无法继续执行,通过加入延时可以给其他任务腾出时间片来执行,从而保证系统的相对平滑运行。
示例代码如下,展示了一个带有延时的死循环:
while(1) {
// 执行某些任务
vTaskDelay(10 / portTICK_PERIOD_MS); // 延时 10 毫秒
}
2
3
4
5
这段代码中,使用了 FreeRTOS 的 vTaskDelay() 函数,它会暂停当前任务一段指定的时间,让出 CPU 的执行权。通过适当调整延时的时间,可以控制死循环中的任务执行速率,避免过度占用 CPU。
4.9 按键点灯效果
按下按键灯亮,再按下按键灯灭,如此反复。