控制 GPIO 输入 - 按键实验
前面几节课介绍的都是 IO 口输出的使用,本节课我们通过按键实验来介绍 IO 口作为输入的使用并通过按键电路实现开关灯的效果。
实验原理
按键是一种电子开关,使用时轻轻按开关按钮就可使开关接通,当松开手时,开关断开。
按钮有两组引脚(触点)。当按下按钮时,它会连接这两个触点,从而关闭电路。
一般来说 4 脚开关(轻触按键)相距较远的是相通的,离得较近的是一组开关,最好是测量一下,如果懒得测,接对角肯定是可以的。
下图说明了按钮内部的连接:
使用按键的时候,通常情况下需要进行消抖。
什么是按键消抖?
该实验中所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的抖动,为了不产生这种现象而作的措施就是按键消抖。
按键的抖动对于人类来说是感觉不到的,但对单片机来说,则是完全可以感应到的,而且还是一个很漫长的过程,因为单片机处理的速度在微秒级,而按键抖动的时间至少在毫秒级。
一次按键动作的电平波形如下图。存在抖动现象,其前后沿抖动时间一般在 5ms~10ms 之间。由于单片机运行速度非常快,刚按下的时候会检测到低电平判断按键被按下。但是由于按键存在抖动,单片机在此时也会检测到高电平,误以为松开按键,紧接着又检测到低电平,判断到按键被按下。周而复始,在 5-10ms 内可能会出现很多次按下的动作,每一次按键的动作判断的次数都不相同。
这种抖动可能会影响程序误判,造成严重后果,一般我们采用两种方式对按键进行消抖:
- 硬件消抖,硬件消抖的典型做法是:采用 R-S 触发器或 RC 积分电路。
- 软件消抖,通常我们会使用软件延时 10ms 来消抖。例如,当按键按下后,引脚为低电平;所以首先读取引脚电平,若引脚为低电平,则延时 10ms 后再次读取引脚电平,若为低电平,则证明按键已按下。
硬件方法一般用在对按键操作过程比较严格,且按键数量较少的场合,而按键数量较多时,通常采用软件消抖。值得一提的是,对于复杂且多任务的单片机系统来说,若简单地采用循环指令来实现软件延时,则会浪费CPU宝贵的时间资源,大大降低系统的实时性,所以,更好的做法是利用定时中断服务程序或利用标志位的方法来实现软件消抖。
硬件电路设计
使用按键电路实现开关灯的效果。
物料清单(BOM 表):
材料名称 | 数量 |
---|---|
直插式 LED | 1 |
1kΩ 电阻 | 1 |
4 脚按键 | 1 |
杜邦线(跳线) | 若干 |
面包板 | 1 |
将材料按照下图相连:
按键一端接 3V3 引脚,一端接 D14,LED 接 D2。
软件程序设计
这里我们在使用 pinMode
方法的时候,第二个参数就不能传递 OUTPUT
了,并且我们需要使用 digitalRead
方法来获取输入值。
与输出不同的是,设置输入引脚时,我们需要配置上拉或下拉电阻,目的是确定某个状态电路中的高电平或低电平。
上、下拉电阻的作用是提高电路稳定性,避免引起误动作。按键如果不通过电阻上拉到高电平,那么在上电瞬间可能就发生误动作,因为在上电瞬间单片机的引脚电平是不确定的,上拉电阻的存在保证了其引脚处于高电平状态,而不会发生误动作。
在 Arduino 中,设置引脚的上拉电阻或下拉电阻需要使用 pinMode
函数和 INPUT_PULLUP
或 INPUT_PULLDOWN
常量。
如果要设置引脚为上拉电阻,需要将引脚设置为输入模式,并调用 pinMode(pin, INPUT_PULLUP)
函数,其中 pin
为引脚号。
提示
如果你不认识上拉电阻和下拉电阻,在这个阶段是无所谓的,你只需要了解他们的存在是为了确定初始电平状态。 选择上拉电阻,GPIO 引脚默认位高电平,那我们想要改变信号,就需要传递一个低电平,接地。 选择下拉电阻,GPIO 引脚默认为低电平,那我们想要改变信号,就需要传递一个高电平,接电源。
因此,我们的代码需要这么写:
// 定义 LED 与 按键引脚
int led_pin = 2;
int button_pin = 14;
// 定义 LED 逻辑值
int led_logic = 0;
// 判断 LED 的状态是否改变过
bool status = false;
void setup() {
pinMode(led_pin, OUTPUT);
pinMode(button_pin, INPUT_PULLDOWN);
}
void loop() {
// 按键消抖
if (digitalRead(button_pin)) {
// 睡眠 10ms,如果依然为高电平,说明抖动已消失。
delay(10);
if (digitalRead(button_pin) && !status) {
led_logic = !led_logic;
digitalWrite(led_pin, led_logic);
// led 的状态发生了变化,即使我持续按着按键,LED 的状态也不应该改变。
status = !status;
}else if (!digitalRead(button_pin)) {
status = false;
}
}
}
我们也可以不在 setup
前定义变量,而是选择 宏定义
的方法。
在 Arduino 中,宏定义是一种预处理器指令,它被用于创建常量和替代符号。通过使用宏定义,可以让代码更易读,更易于维护和修改,同时也可以避免在代码中出现重复的代码块。
宏定义以 #define
关键字开头,后面跟着宏名称和宏值。宏值可以是数字、字符或者表达式。一旦定义了宏,它将会被整个程序使用。
例如,下面的代码创建了一个宏定义 LED_PIN,将其值设置为 13:
#define LED_PIN 13
然后,在程序中可以使用宏定义来指定要使用的引脚,而不是写出具体的数字,从而使代码更具可读性:
#define LED_PIN 2
#define BUTTON_PIN 14
// 定义 LED 逻辑值
int led_logic = 0;
// 判断 LED 的状态是否改变过
bool status = false;
void setup() {
pinMode(LED_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLDOWN);
}
void loop() {
// 按键消抖
if (digitalRead(BUTTON_PIN)) {
// 睡眠 10ms,如果依然为高电平,说明抖动已消失。
delay(10);
if (digitalRead(BUTTON_PIN) && !status) {
led_logic = !led_logic;
digitalWrite(LED_PIN, led_logic);
// led 的状态发生了变化,即使我持续按着按键,LED 的状态也不应该改变。
status = !status;
}else if (!digitalRead(BUTTON_PIN)) {
status = false;
}
}
}
宏定义和变量定义都是定义标识符的方式,但二者有以下区别:
- 宏定义在预处理时进行处理,而变量定义在编译时进行处理。宏定义是一种文本替换机制,会在编译前被展开为其定义的文本,而变量则需要在编译时分配内存空间;
- 宏定义的作用域是从定义到文件结束,而变量定义的作用域则是在定义处到包含该定义的代码块结束;
- 宏定义可以定义函数或代码块,而变量定义只能定义变量;
- 宏定义不会占用内存空间,而变量需要分配内存空间;
- 宏定义不需要类型声明,而变量定义需要类型声明。
总之,宏定义主要是一种代码替换的机制,能够提高代码的可读性和可维护性,而变量定义则是用于在程序中存储和管理数据的标识符。