第十七章 按键输入实验
上一章,我们介绍了STM32MP157的IO口作为输出的使用。本章,我们向大家介绍IO口作为输入使用的操作方法,我们将利用板载的3个按键来控制LED灯亮和灭以及和蜂鸣器的开和关。
本章将分为如下几个小节:
12.1、按键输入简介;
12.2、硬件设计;
12.3、程序设计;
12.4、章节小结;
17.1 按键输入简介
17.1.1 按键检测原理
几乎每个开发板都会板载有独立按键,因为按键用处很多。常态下,独立按键是断开的,按下的时候才闭合。每个独立按键会单独占用一个IO口,通过查询IO口的高低电平来判断按键的状态。按键在闭合和断开的时候,都存在抖动现象,即按键在闭合时不会马上就稳定的连接,断开时也不会马上断开,这是机械触点,无法避免。独立按键抖动波形图如下:
图17.1.1.1独立按键抖动波形图
图中的按下抖动和释放抖动的时间大概都为10ms。为了避免抖动可能带来的误操作,我们要做的措施就是给按键消抖。消抖方法分为硬件消抖和软件消抖,我们常用软件的方法消抖。
软件消抖:检测到按键按下后,一般进行10ms延时,用于跳过抖动的时间段,如果消抖效果不好可以调整这个10ms延时,因为不同类型的按键抖动时间可能有偏差。按键按下以后,待延时过后再检测按键状态,如果测试到按键没有按下,那我们就判断这是抖动或者干扰造成的;如果测试到还是按下的状态,那么我们就认为这是按键真的按下了。对按键释放的判断同理。
硬件消抖:利用R-S触发器或者单稳态触发器构成消抖电路,或者利用RC积分电路吸收振荡脉冲的特点来达到消抖的效果。
17.1.2 配置上下拉的原则
结合前面的实验以及本章的实验,相信会有部分小伙伴对什么时候软件要配置上拉什么时候软件要配置下拉有疑惑,下面我们来分析一下。
随着技术的迅速发展,芯片的集成度和复杂度也越来越高,功能也越来越强大,现在的芯片内部一般都集成了电阻和电容,为PCB设计极大地节省了空间。如下图所示,STM32的GPIO口内部自带了上拉和下拉电阻,电阻上有一个开关,可通过软件配置来设置开或者关。
图17.1.2.1 GPIO的基本结构图
上拉就是将一个不确定的信号通过一个电阻嵌位在高电平,电阻同时起限流作用;下拉就是将一个不确定的信号通过一个电阻嵌位在低电平,电阻同时起限流作用。STM32的这个上下拉电阻阻值一般是30KΩ~50KΩ,上拉电阻,一般称为弱上拉,下拉电阻,一般称为弱下拉。这里的弱是指电阻阻值比较大,电流就比较小,充电就比较慢,从而上下拉的速度就比较慢。
例如,芯片内部开启了弱上拉(例如阻值是50KΩ),外部再加一个强上拉电阻10KΩ,电路中这两个电阻相当于并联,IO口的上拉电阻就小于10KΩ,电流就变大了,也就变“强”了。此电流可以供开漏电路使用,开漏输出最主要的特性就是高电平没有驱动能力,需要借助外部上拉电阻才能真正输出高电平。
芯片复位以后,端口上拉下拉寄存器(PUPDR)的复位值为0x00000000,即默认不开启内部上拉和下拉,IO口相当于浮空状态,电平状态是高还是低不确定。那么问题来了,如果此IO口接的是一个按键,如果外部不做上下拉会怎样?
图17.1.2.2按键原理图
我们是通过读取按键对应的IO电平状态来判断按键是否有按下的,按键WKUP一端接的高电平,另一端接的PA0,按键按下以后IO口是高电平,所以认为此按键是高电平有效。如果PA0内部不开启下拉,那么IO口电平是未知的,当按键程序去读取IO口电平时可能读到值为0也可能为1,如果此时恰好没有按下按键,而程序读取IO口电平为1,程序就认为按键是已经按下了,这就错了。在按键未按下前,我们当然希望PA0电平为0,所以对于按键WKUP一定要开启弱下拉。对于按键KEY0和KEY1是低电平有效,IO口的一端实际上是已经接了10KΩ的上拉电阻,IO口电平已经默认为高电平了,所以内部上拉不开启也是不影响的。
上下拉电阻的目的是给IO口设定一个确定的电平状态,如果按键按下是高电平有效,我们就做下拉,让该IO口在默认状态下处于低电平状态,即没有按键按下时,IO口检测到的总是低电平,只有按键按下的时候IO口才会检测到高电平;如果按键按下是低电平有效的话,我们就上拉,让该IO口在默认状态下处于高电平,即没有按键按下时,IO口检测到的总是高电平,只有按键按下的时候IO口才会检测到低电平。
17.2 硬件设计
1. 例程功能
通过开发板上的三个独立按键控制LED灯:WKUP控制LED0翻转,KEY1控制LED1翻转,KEY0控制蜂鸣器翻转。
2. 硬件资源
LED0 | LED1 | WK_UP | KEY0 | KEY1 | BEEP |
PI0 | PF3 | PA0 | PG3 | PH7 | PC7 |
表17.2. 1硬件资源
3. 原理图
LED和BEEP的原理图我们前面已经涉及,独立按键硬件部分的原理图如下图所示:
图17.2. 1独立按键与STM32MP157连接原理图
这里需要注意的是:KEY0和KEY1是低电平有效的,而WK_UP是高电平有效的,并且KEY0和KEY2接了一个10k的上拉电阻,而WK_UP外部没有下拉电阻,所以WK_UP的配置中,一定要通过软件来开启STM32MP157内部下拉。
17.3 程序设计
本实验配置好的实验工程已经放到了开发板光盘中,路径为:开发板光盘A-基础资料\1、程序源码\ 3、M4裸机驱动例程\库V1.2\实验6 按键输入实验。
17.3.1 程序设计流程
前面实验我们学习了GPIO口作为输出的使用方法,本节,我们来学习GPIO作为输入的使用方法。实验中,我们通过HAL库的HAL_GPIO_ReadPin函数(实际上是操作IDR寄存器)来读取按键对应GPIO引脚电平状态,从而判断按键是否有按下。KEY0和KEY1按下后引脚是低电平(低电平有效),WKUP按键按下后引脚是高电平。
程序设计中,我们要注意以下两点:
1)按键程序要做消抖处理,消抖时间约为10ms;
2)除了按键WKUP配置为下拉以外,其它引脚均配置为上拉。
图17.3.1. 1程序设计流程图
17.3.2 添加驱动文件
在上一章工程的Drivers\BSP\KEY下新建key.c和key.h文件,并将key.c关联到工程中:
图17.3.2.1 新建key.c和key.h文件
17.3.3 添加驱动代码
1. 添加key.h代码
key.h文件代码如下,这段代码主要定义了3个按键引脚、使能按键引脚对应的IO口的时钟、读取三个按键引脚的电平。其中,通过HAL_GPIO_ReadPin函数读取GPIO口的电平值,也就是读取按键引脚电平值。最后定义了3个宏KEY0_PRES、KEY1_PRES和WKUP_PRES,这3个宏将用于按键扫描程序中,用于表示按键按下。
#ifndef __KEY_H
#define __KEY_H
#include "./SYSTEM/sys/sys.h"
/* 按键KEY0引脚定义 */
#define KEY0_GPIO_PORT GPIOG
#define KEY0_GPIO_PIN GPIO_PIN_3
/* 使能PG3时钟使能 */
#define KEY0_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOG_CLK_ENABLE(); }while(0)
/* 按键KEY1引脚定义 */
#define KEY1_GPIO_PORT GPIOH
#define KEY1_GPIO_PIN GPIO_PIN_7
/* 使能PH7时钟使能 */
#define KEY1_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOH_CLK_ENABLE(); }while(0)
/* 按键WK_UP引脚定义 */
#define WKUP_GPIO_PORT GPIOA
#define WKUP_GPIO_PIN GPIO_PIN_0
/* 使能PA0时钟使能 */
#define WKUP_GPIO_CLK_ENABLE() do{ __HAL_RCC_GPIOA_CLK_ENABLE(); }while(0)
/* 读取KEY0引脚 */
#define KEY0 HAL_GPIO_ReadPin(KEY0_GPIO_PORT, KEY0_GPIO_PIN)
/* 读取KEY1引脚 */
#define KEY1 HAL_GPIO_ReadPin(KEY1_GPIO_PORT, KEY1_GPIO_PIN)
/* 读取WKUP引脚 */
#define WK_UP HAL_GPIO_ReadPin(WKUP_GPIO_PORT, WKUP_GPIO_PIN)
#define KEY0_PRES 1 /* KEY0按下 */
#define KEY1_PRES 2 /* KEY1按下 */
#define WKUP_PRES 3 /* KEY_UP按下 */
void key_init(void); /* 按键初始化函数 */
uint8_t key_scan(uint8_t mode); /* 按键扫描函数 */
#endif
2. 添加key.c文件代码
key.c文件主要有两部分内容:按键初始化和按键扫描。
(1)按键初始化
/**
* @brief按键初始化函数
* @param无
* @retval无
*/
void key_init(void)
{
GPIO_InitTypeDef gpio_init_struct;
KEY0_GPIO_CLK_ENABLE(); /* KEY0时钟使能 */
KEY1_GPIO_CLK_ENABLE(); /* KEY1时钟使能 */
WKUP_GPIO_CLK_ENABLE(); /* KEY_UP时钟使能 */
gpio_init_struct.Pin = KEY0_GPIO_PIN; /* KEY0引脚 */
gpio_init_struct.Mode = GPIO_MODE_INPUT; /* 输入 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_init_struct.Speed = GPIO_SPEED_FREQ_VERY_HIGH; /* 高速 */
HAL_GPIO_Init(KEY0_GPIO_PORT, &gpio_init_struct); /* KEY0引脚初始化 */
gpio_init_struct.Pin = KEY1_GPIO_PIN; /* KEY1引脚 */
gpio_init_struct.Pull = GPIO_PULLUP; /* 上拉 */
HAL_GPIO_Init(KEY1_GPIO_PORT, &gpio_init_struct); /* KEY1引脚初始化 */
gpio_init_struct.Pin = WKUP_GPIO_PIN; /* KEY_UP引脚 */
gpio_init_struct.Pull = GPIO_PULLDOWN; /* 下拉 */
HAL_GPIO_Init(WKUP_GPIO_PORT, &gpio_init_struct); /* KEY_UP引脚初始化 */
}
按键初始化函数中,先使能按键的时钟,再配置按键的模式,其中:
配置KEY0和KEY1为输入、上拉、高速模式,配置WK_UP为输入、下拉、高速模式。
(2)按键扫描函数
key.c文件中记得添加如下代码:
#include "./BSP/KEY/key.h"
#include "./SYSTEM/delay/delay.h"
按键扫描函数如下:
1 /**
2 * @brief按键扫描函数
3 * @note该函数响应优先级为(同时按下多个按键): WK_UP > KEY1 > KEY0!!
4 * @param可取 0 / 1, 具体含义如下:
5 * @arg不支持连续按(当按键按下不放时, 只有第一次调用会返回键值,
6必须松开按键以后, 再次按下才会返回其他键值)
7 * @arg支持连续按(当按键按下不放时, 每次调用该函数都会返回键值)
8 * @retval键值, 定义如下:
9按下
10按下
11按下
12 */
13 uint8_t key_scan(uint8_t mode)
14 {
15 static uint8_t key_up = 1; /* 按键按松开标志 */
16 uint8_t keyval = 0;
17 if (mode) key_up = 1; /* 支持连按 */
18 /* 按键松开标志为1, 且有任意一个按键按下了 */
19 if (key_up && (KEY0 == 0 || KEY1 == 0 || WK_UP == 1))
20 {
21 delay(2); /* 去抖动,后面会换成高精度延时函数! */
22 key_up = 0;
23 if (KEY0 == 0) keyval = KEY0_PRES;
24 if (KEY1 == 0) keyval = KEY1_PRES;
25 if (WK_UP == 1) keyval = WKUP_PRES;
26 }
27 /* 没有任何按键按下, 标记按键松开 */
28 else if (KEY0 == 1 && KEY1 == 1 && WK_UP == 0)
29 {
30 key_up = 1;
31 }
32 return keyval; /* 返回键值 */
33 }
key.c文件主要是处理按键扫描函数,程序设计按键支持连续按和不支持连续按两种情况。我们来看按键处理的程序设计过程:
第15行,key_up为按键松开标志,如果key_up等于1,表示按键已经松开。
第16行,定义程序的返回值keyval。
第17行,如果函数的参数mode等于1,则key_up等于1,表示支持按键可连续按模式,也就是当按键按下不放时,每次调用该函数都会返回键值。如果函数的参数mode等于0,表示不支持连续按,也就是当按键按下不放时,只有第一次调用会返回键值,必须松开按键以后, 再次按下才会返回其键值。
第19行,如果标志位key_up等于1(表示按键是松开的,没有按下),当KEY0等于0或者KEY1等于0或者WKUP等于1的时候,表示有按键按下了,具体是哪一个按键按下了,需经过后面的程序进行判断。
第21行,程序延时约10ms,因为按键按下过程产生抖动,抖动周期大概10ms,所以我们先延时10ms以后再去读取按键对应的IO口的状态。这里的delay函数是前面的实验中我们自己定义的函数,如果想实现比较精确的10ms延时,我们可以直接调用HAL库的HAL_Delay函数来实现,该函数是实现毫秒的延时,可以将第21行的代码替换成HAL_Delay(10);
第22行,将标志位key_up置0,方便程序用于判断下次按键是否有按下。
第23~25行,通过读取到的按键对应的IO口电平来判断是哪一个按键按下了。
我们前面通过原理图分析知道,当按键WK_UP按下以后,对应的IO口为高电平,当KEY0或KEY1按键按下以后,对应的IO口为低电平。
第28行,如果读取到KEY0和KEY1的电平为1,WKUP的电平为0,说明此时没有按键按下, key_up值等于1,表示按键是松开的。
3. 添加main.c文件代码
main.c文件函数如下:
1 #include "./SYSTEM/sys/sys.h"
2 #include "./SYSTEM/delay/delay.h"
3 #include "./BSP/LED/led.h"
4 #include "./BSP/BEEP/beep.h"
5 #include "./BSP/KEY/key.h"
6
7 /**
8 * @brief主函数
9 * @param无
10 * @retval无
11 */
12 int main(void)
13 {
14 uint8_t key;
15
16 HAL_Init(); /* 初始化HAL库 */
17 led_init(); /* 初始化LED */
18 beep_init(); /* 初始化蜂鸣器 */
19 key_init(); /* 初始化按键 */
20 LED0(0); /* 先点亮LED0 */
21
22 while(1)
23 {
24 key = key_scan(0); /* 得到键值 */
25 if (key)
26 {
27 switch (key)
28 {
29 case WKUP_PRES: /* 控制LED0(RED)翻转 */
30 LED0_TOGGLE(); /* LED0状态取反 */
31 break;
32 case KEY1_PRES: /* 控制LED1(GREEN)翻转 */
33 LED1_TOGGLE(); /* LED1状态取反 */
34 break;
35 case KEY0_PRES: /* 控制蜂鸣器开关 */
36 BEEP_TOGGLE(); /* 蜂鸣器状态取反 */
37 break;
38 }
39 }
40 else
41 {
42 delay(10);
43 //HAL_Delay(10); /* 也可以使用HAL库自带的毫秒级别延时函数 */
44 }
45 }
46 }
main.c文件中也是先初始化HAL库,我们前面多次提到,因为我们还没手动添加时钟配置的代码,所以此时MCU的时钟为64MHz。另外,延时函数的话,我们也可以使用HAL_Delay函数来代替。
第16~20行,初始化HAL库、LED、蜂鸣器、按键,逛进入main函数的时候,LED0是亮的状态;
第24行,获取按键值,这里key_scan(0)函数的参数是0,表示不支持连续按模式,如果大家想测试连续按模式,只需要把函数的参数0改为1即可。
第25行,key_scan(0)的返回值key可以是0、1、2或者3中的某一个,如果key的值是1、2和3的话,则表示有按键按下,通过第30~43行的case语句判断按键的按下情况:
第一次WKUP按键按下,则蜂鸣器响,再次按下,蜂鸣器不响,再次按下蜂鸣器又响,如此循环,实现蜂鸣器翻转。
第一次按下KEY1,LED1灭,再次按下KEY1,LED1又亮,如此循环。
第一次按下KEY0,LED0亮,再次按下KEY0,LED0灭,如此循环。
如果key的值是0,表示没有按键按下,程序执行delay(10)函数延时一定时间以后再返回到第24行往下执行,即在while中循环。
17.4 编译和测试
保存修改,编译工程不报错,进入仿真模式进行测试,实验现象为:
点击运行后,LED0是常亮的状态;按下KEY0后蜂鸣器响,松开KEY0后蜂鸣器不响;按下KEY1后LED1翻转;按下WK_UP后LED0翻转。实验现象结果和我们程序设计一致。