GPIO口的输入功能-机械按键状态的识别
开发环境:MDK(keil 5) + STM32CubeMX
1.1 GPIO口的输入的作用
输入,其意是指将处理器外部的逻辑信号0或者1输入到处理器的内部。输入是每一个处理器的IO引脚的基本功能。利用处理器的输入功能我们可以获取外部电路的状态,进而做出进一步的判断。GPIO的输入功能的典型应用是获取机械按键的状态—判断按键是按下还是弹起。
1.2 机械按键状态的识别
1.2.1 机械按键电路的设计
按键有两个状态,一个是按下一个是弹起。通过巧妙的电路设计,会使得按键的按下与弹起时IO引脚的逻辑电平不一样。通过GPIO引脚的输入功能将这些逻辑电平输入到内部供处理器识别,由此可知按键是按下还是弹起,并做出进一步的判断。
下面我们先来讨论按键电路的设计。常用的按键电路设计如图1的(a)和(b)所示。
(a)
(b)
图1 按键电路设计
先来看图1的(a)图,在(a)图中,按键的一端接地,另一端接IO引脚,接IO引脚这一端通过一个电阻连接到高电平VCC(这种电阻叫上拉电阻)。在没有按键按下时,由于处理器吸取的电流非常非常小,R1两端可以认为没有电流流动,所以它们两边电位一样,也即IO引脚的电平跟VCC基本一样,此时IO引脚端为高电平。当按键按下后,IO引脚和地端相连,IO引脚直接变为了低电平。通过这个分析,我们得出图1(a)按键电路的特点如下:
(1)没有按键按下,IO引脚为高电平;
(2)有按键按下,IO引脚为低电平。
所以,如果处理器某个时候读到这个引脚信号为0,说明此时按键按下了,如果读到为1,说明按键没有按下。
再来看图1的(b)图,图1(b)中,按键一端接高电平,另一端接IO引脚,其中接IO引脚这一端通过一个电阻接到地(这种电阻叫下拉电阻)。图1(b)按键电路的特点如下:
(1)没有按键按下时,IO引脚为低电平;
(2)有按键按下时,IO引脚为高电平。
所以,如果处理器某个时候读到这个引脚信号为1,说明此时按键按下了,如果读到为0,说明按键没有按下。
暴风开发板的按键电路如图2所示,可以看到,在标航的暴风开发板中特意将两个按键分别按图1的(a)和(b)来连接,以便读者学习这两种按键电路按键状态的识别,这个设计比较人性化,可以照顾不同开发者的不同应用场合。
图2 暴风开发板按键电路图
图2中要注意,此时电路设计没有在PE2和PA0两个引脚分别连一个电阻到VCC和地,所以我们得在GD32的内部这两个引脚这里分别使能这两个电阻(GD32和STM32的每个IO引脚内部都配有一对受控的上下拉电阻)。
1.2.2 机械按键状态识别的思路设计
通常,我们都是设计一个函数来单独判断按键是否按下,这个按键函数的思路设计如下:
uint8_t Key_Scan(void)
{
if(KEY0_Status == 0) || (WK_UP1_Status == 1) //说明有按键按下了
{
if(KEY0_Status == 0) return KEY0_Value;
if(WK_UP1_Status == 1) return WK_UP1_Value;
}
return KEY0_NO;
}
在函数Key_Scan中,我们先判断KEY0的状态是不是0或者WK_UP1的状态是不是为1,如果KEY0的状态是0或者WK_UP1的状态是1,说明按键按下了,接下来进行细分,看看是KEY0按下还是WK_UP1按下,并返回对应的按键值。
对于Key_Scan函数的调用,我们可以在主函数中这样调用
int main(void)
{
uint8 keyvalue = 0;
系统初始化;
while(1)
{
keyvalue = Key_Scan();
if(keyvalue == KEY0_Value) LED0 = ~LED0; // LED0状态反转
if(keyvalue == WK_UP1_Value) LED1 = ~LED1;// LED1状态反转
}
}
在主函数中,我们循环执行按键扫描,如果发现按键扫描函数返回的是KEY0_Value,则将LED0的状态反转,如果返回的是WK_UP1_Value,则将LED1的状态反转。总的来说,我们是希望按下一次按键,对应的LED的状态就反转。
上面这两个函数的配合是否有问题呢?表面看来好像没有问题,但是当你用这个思路去完善程序并下载到开发板执行的时候,你会发现按键按下时,灯的状态是不受控的,这个不受控的原因是什么呢?我们看一下整个执行过程。
假设有按键KEY0按下,则整个过程为
①执行语句“keyvalue = Key_Scan();”此时返回KEY0_Value,接着执行判断并使得LED0状态反转一次,这个过程持续时间非常短,1ms内估计就能执行完。
②又回来执行语句“keyvalue = Key_Scan();”,此时由于按键仍然处于按下状态(人为按下时,按键的按下状态通常会超过100ms,典型的是600ms左右),所以又会返回KEY0_Value,接着执行判断并使得LED0状态又反转一次。
注意,此时按键的状态已经变化两次了,但是我们只是执行一次按下而已!!!!!
继续往下分析,你会发现按键按下一次时,这个判断系统会执行多次返回,这是错误的。错误的原因在哪里呢?在Key_Scan这个函数中,这个函数里面只要KEY0_Status等于0,它就会返回一次KEY0_Value,所以我们需要加一个变量,用于描述按键的当前状态,如果当前按键已经按下了,则这里就不需要再次判断了。由于这个变量描述按键的按下与弹起状态,在Key_Scan执行完后也不能释放它的存储空间,所以我们需要用static修饰它,此时的Key_Scan函数需要修改如下:
uint8_t Key_Scan(void)
{
static uint8_t flag = 0; //flag =0说明当前是弹起,=1说明是按下
/*如果刚才是弹起但现在有按键按下则判断是那个按键按下,同时将按键状态置为1*/
if((flag == 0)&&((KEY0_Status == 0) || (WK_UP1_Status == 1))) {
flag = 1;
if(KEY0_Status == 0) return KEY0_Value;
if(WK_UP1_Status == 1) return WK_UP1_Value;
}
return KEY0_NO;
}
将Key_Scan函数修改为上面的样子后,解决了按下一次就执行一次返回,避免了按下一次则返回多次的问题。但是它仍然是有重大缺陷的,因为我们按下一次按键后,flag被设置为1了,当按键再次被按下时里面的按下判断再也得不到执行,也即刚刚修改后的函数只能判断一次按键按下。要想将flag恢复为0,我们要在Key_Scan中增加弹起的语句,如果弹起了,将flag设置为0,则就可以解决多次按下后都能触发判断的问题了。增加判断后的Key_Scan函数如下:
uint8_t Key_Scan(void)
{
static uint8_t flag = 0; //flag =0说明当前是弹起,=1说明是按下
/*如果刚才是弹起但现在有按键按下则判断是那个按键按下,同时将按键状态置为1*/
if((flag == 0)&&((KEY0_Status == 0) || (WK_UP1_Status == 1)))
{
flag = 1;
if(KEY0_Status == 0) return KEY0_Value;
if(WK_UP1_Status == 1) return WK_UP1_Value;
}
/*如果刚才按键按下,现在弹起了,则设置flag=0*/
if((flag == 1)&&((KEY0_Status == 1) && (WK_UP1_Status == 0))) {
flag = 0;
}
return KEY0_NO;
}
注意,按键弹起指的是所有按键的弹起,所以(KEY0_Status == 1) && (WK_UP1_Status == 0)这里要用逻辑与,体现出“而且”之意。
至此,机械按键状态识别的关键问题就解决了。
1.2.3 机械按键的抖动及其消除
虽然关键问题解决了,但还有一些细节要注意,这个细节就是按键的抖动。
以图3的按键电路为例,当按键按下时,PE2引脚的电平状态如图4所示。
图3 按键电路图
图4 按键按下过程
由图4可见,按键按下时,PE2并不是马上变为低电平,而是有一个渐变过程,而在弹起时也不是马上变为高电平,它也有一个渐变过程。这些渐变过程我们叫做抖动。在进行按键的按下与弹起时,我们都要进行抖动的消除。消除的方法非常简单,就是延时。通常,抖动持续的时间在10ms之内,所有,我们只要进行10ms的延时可以解决掉绝大部分机械按键的抖动—如果解决不了,此时就要用示波器测一下你的按键按下与放开时的信号,看看具体的抖动是多少,然后增加延时消除它,笔者就曾遇到过需要延时20ms的情况……
这个消抖的过程如下:
1、按下的消抖
if(按键按下)
{
Delay(10ms);
if(按键按下)
{
按键是真的按下,执行相应的动作;
}
}
2、弹起的消抖
if(按键弹起)
{
Delay(10ms);
if(按键弹起)
{
按键是真的弹起了,执行相应的动作;
}
}
1.2.4 完整的按键判断程序
加入消抖后,整个按键判断的函数可以修改如下
uint8_t KEY_Scan(void)
{
static uint8_t flag=0; //按键弹起为0,按下为1
if(( flag == 0) && ((KEY0_Status == 0)||(WK_UP1_Status == 1)))
{
/*按键刚刚处于弹起状态,但现在有按下*/
HAL_Delay(10); //延时10ms,消除抖动
if((KEY0_Status == 0)||(WK_UP1_Status == 1))
{
/*确实有按键按下*/
flag = 1; //按键为按下状态
if(KEY0_Status == 0) return KEY0_Value;
if(WK_UP1_Status == 1) return WK_UP1_Value;
}
}
if((flag == 1) && ((KEY0_Status == 1) && (WK_UP1_Status == 0)))
{
/*按键处于弹起状态而且刚才是按下状态*/
HAL_Delay(10); //消除弹起抖动
if((KEY0_Status == 1) && (WK_UP1_Status == 0))
{
flag = 0; //按键弹起了
}
}
return KEY_NO; //没有按键按下返回KEY_NO
}
1.3 按键状态判断实验
下面我们通过一个例子来验证按键状态的识别。
【例1】已知按键电路和LED电路如图5所示,编写程序实现以下功能:
按下按键KEY0, LED0的状态反转;按下按键WK_UP1,LED1的状态反转。
图5 按键电路和LED电路示意图
【实现过程】
1.配置RCC的高速时钟来自于外部晶体陶瓷晶振,并且设置HCLK的频率为72Mhz。
2.设置调式方式为Serial Wire。
以上两步不懂如何设置的可以回头看一下【模块一 GPIO口的输出功能-LED的闪烁实验】这一部分的步骤介绍。
3.设置PE12、PE13引脚的工作模式为输出,PE12的User Label选项为LED0,PE13的User Label设置为LED1,以使能更加直观方便。另外,一开始我们将这两盏LED灯都点亮,以方便观察结果。PE12和PE13的设置结果如图6所示。
图6 PE12和PE13的设置过程
4.设置PE2和PA0为输入。PE2引脚上拉电阻使能,PA0引脚下拉电阻使能。同时设置PE2的用户标号为KEY0,设置PA0引脚的标号为WK_UP1,。PE2引脚的设置如图7所示,PA0引脚的设置如图8所示。
图7 PE2引脚的设置结果示意图
图8 PA0引脚的设置
5.设置好后,给工程取名,同时选择IDE,并生成工程代码。
6.添加代码。
①编写按键识别的C语言文件,其内容如图9所示。
图9 key.c中内容示意图
②在key.h中定义KEY0_Value等宏名,如图10所示。
图10 KEY0_Value等的定义示意图
③修改主函数,其内容如图11所示。
图11 主函数的内容示意图
在主函数中,注意要将头文件key.h包含进工程,如图12所示。
至此,工程的代码添加完毕,编译后下载到开发板,按复位键,然后按KEY0或者WK_UP0,可以看到对应的LED灯的状态反转,任务目标完成。
1.4 按键识别实验用到的HAL库的函数解读
1.引脚电平反转函数 HAL_GPIO_TogglePin()
在主函数main的while循环中,我们使用到了函数HAL_GPIO_TogglePin,这个函数的相关信息为
●作用:将某个IO引脚的输出电平反转。比如要反转PE12引脚的电平,我们可以采用如下的方式来调用该函数:
HAL_GPIO_TogglePin(GPIOE, GPIO_PIN_12);
●函数参数,有两个,第一个用于指明要反转信号的引脚位于第几组GPIO口,第二个用于指明要反转的是哪一个引脚的信号。
是不是很方便呢?
最后要注意,函数HAL_GPIO_TogglePin要使用于将IO引脚已经配置为输出的场合。
2.读引脚信号函数HAL_GPIO_ReadPin()
在key.c函数中,有一个宏定义
#define KEY0_Status HAL_GPIO_ReadPin(KEY0_GPIO_Port, KEY0_Pin)
里面用到了一个函数HAL_GPIO_ReadPin,这个函数相关的信息如下:
●作用:读取某个引脚的状态。比如要读取PA0的状态,我们可以采用如下的方式来调用该函数
HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_0);
●函数参数,有两个,第一个用于指明要读取信号的引脚位于那组GPIO口,第二个参数用于指明是那个引脚。
注意,函数HAL_GPIO_ReadPin使用于IO引脚已经设置为输入的场合
1.5 GPIO输入功能总结
在使用GD32/STM32的IO引脚时要注意以下两点:
1.如果引脚外部没有上拉电阻或者下拉电阻,则可能需要在引脚内部使能上拉电阻或者下拉电阻。
2.对按键状态进行识别时,一定要注意防止按下一次时有多次返回值。
评论
查看更多