关于按键
按键相当于是一种电子开关,按下时开关接通,松开时开关断开,实现原理是通过轻触按键内部的金属弹片受力弹动来实现接通和断开。
最基础的就是使用死循环来对按键动作做轮询判断。
比如,监测如果某个按键按下,某个LED就会亮。
对于机械开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开,所以在开关闭合及断开的瞬间会伴随一连串的抖动:
可以通过延时来做消抖。
上拉电阻
上拉电阻:
将一个不确定的信号(高或低电平),通过一个电阻与电源VCC相连,固定在高电平。
下拉电阻:
将一个不确定的信号(高或低电平),通过一个电阻与地GND相连,固定在低电平。
当上拉电阻和下拉电阻共同作用时,表现为接地端的低电平状态。
查看原理图
独立按键:
代码实现
此处,我将JP5引脚接到P1端口,将LED灯接到P0端口。
1、按一个按键,就亮对应的LED灯。
/** *@file key.c *@author Timi *@date 2022.07.21 */ #include <reg51.h> #define uchar unsigned char void KeyLightLed(); //函数入口 void main(void) { KeyLightLed(); } /** *@brief *@param[in] *@param[out] *@return */ void KeyLightLed() { uchar portData[] = {0xFE, 0xFD, 0xFB, 0xF7, 0xEF, 0xDF, 0xBF, 0x7F}; while(1) { int i = 0; for (i; i < 8; i++) { if(P1 == portData[i]) { P0 = ~portData[i]; } } } }
2、点击一个按钮,控制其中一个LED灯,再次点击该按钮,则又熄灭,再按又亮起,如此循环往复。即每个按钮都会改变对应LED灯的状态。
/** *@file key.c *@author Timi *@date 2022.07.21 */ #include <reg51.h> #define uchar unsigned char sbit P00 = P0^0; sbit P01 = P0^1; sbit P02 = P0^2; sbit P03 = P0^3; sbit P04 = P0^4; sbit P05 = P0^5; sbit P06 = P0^6; sbit P07 = P0^7; sbit P10 = P1^0; sbit P11 = P1^1; sbit P12 = P1^2; sbit P13 = P1^3; sbit P14 = P1^4; sbit P15 = P1^5; sbit P16 = P1^6; sbit P17 = P1^7; void KeyLightLed(); //函数入口 void main(void) { KeyLightLed(); } /** *@brief *@param[in] *@param[out] *@return */ void KeyLightLed() { P0 = 0x0; while(1) { if(P10 == 0) { P00 = ~P00; } if(P11 == 0) { P01 = ~P01; } if(P12 == 0) { P02 = ~P02; } if(P13 == 0) { P03 = ~P03; } if(P14 == 0) { P04 = ~P04; } if(P15 == 0) { P05 = ~P05; } if(P16 == 0) { P06 = ~P06; } if(P17 == 0) { P07 = ~P07; } } }
关于这种方式的灵敏度问题。
当按键数量少的时候,其实不太明显,如果按键数量太多,就会导致在按下某个按键的时候,CPU还在执行其他部分的代码,因此检测按键不及时。
3、按键消抖
上面提到过按键在按下和弹起的过程中会有抖动,这会引起一些问题,比如按键在按下和弹起时会有闪烁,更明显的是,比如按键控制数码管数字依次增加,如果不消抖,就有可能按下时因为抖动,导致检测到多次的低电平,所以明明只按下一次按键,数字却是无规律地增长,比如有时增加1,有时又增加不止1。
所以常常都需要消抖,消抖有两方面,一方面是硬件消抖(比如利用电容器来使抖动变得更平滑),另一方面是软件消抖,即在检测到一次某种电平时,不急着进行某种操作,而是延时一段时间,因为这种电平有可能是抖动期间引起的,并不稳定。
通常,消抖的延时时间在5—20毫秒之间,一般选择10毫秒就行。示例代码如下:
/** *@file key.c *@author Timi *@date 2022.07.21 */ #include <reg51.h> #define uchar unsigned char sbit P00 = P0^0; sbit P10 = P1^0; void KeyLightLed(void); void Delay10ms(void); //函数入口 void main(void) { KeyLightLed(); } /** *@brief *@param[in] *@param[out] *@return */ void KeyLightLed(void) { P0 = 0x0; while(1) { if(P10 == 0) { //先不着急点亮LED,延时10ms后再判断,如果还是一样的状态则点亮 Delay10ms(); if (P10 == 0) { P00 = 1; } } else { //先不着急点亮LED,延时10ms后再判断,如果还是一样的状态则点亮 Delay10ms(); if (P10 == 1) { P00 = 0; } } } } /** *@brief *@param[in] *@param[out] *@return */ void Delay10ms(void) //误差 0us { uchar a, b, c; for(c=5; c > 0; c--) { for(b=4;b>0;b--) { for(a=248;a>0;a--); } } }
中断的引入
任务:独立数码管循环显示0-F,同时按键控制LED亮灭。
要想实现这两个功能,都需要CPU执行死循环,怎么办呢?最简单的就是将这两个程序都放在同一个死循环中。可以在一定程度上解决这个问题。
但是这样也存在一个问题,那就是按键检测可能不及时,导致按键不灵敏。
为什么呢?因为只有在执行完t1时间后,来到t2时间才能执行按键检测代码,所以如果按键一直按着等待检测,还能起作用;要是快速按下按键,就有大概率处于t1时间,那么就检测不到了。
这两个任务需要实现宏观上的并行执行。其中,t1时间远大于t2的时间,t1可以是秒级,t2几乎是微秒级,差距还是较大的。
此时,数码管循环显示就可以看做是主线任务。“主线任务”为常规任务,默认运行。
可以将类似按键检测的这种任务绑定到中断。中断发生后CPU暂停主线任务转去处理中断任务,完成后再回来接着执行主线任务。由于中断处理程序所耗费的时间较短,所以几乎不会对主线任务产生影响。
中断流程:
中断处理能力让CPU可以全力处理主线任务而不用担心会错过中断任务(举例:看电影和收快递)
中断式比轮询式更适合处理异步事件,效率更高。
中断中处理的事件的特点是:无法预料、处理时间短、响应要求急。中断能力是CPU本身设计时支持的,并不是编程制造出来的
程序员只要负责2件事即可:主程序中初始化中断、定义中断处理程序。
当中断条件发生时,硬件会自动检测到并且通知CPU,CPU会自动去执行中断处理程序,这一切都是CPU设计时定下的,不需要编程干预。
51单片机的中断
内容较多,具体内容阅读数据手册,中断系统相关章节。
外部中断0使用示例如下:
我们这里就用INT0来进行简单的按键处理。
在电路连接上,需要将按键的一端连接到中断引脚上,INT0的引脚为P3.2
如果中断没有打开并使能,那么P3.2就是一个普通的GPIO。
这里我有个疑惑,那就是,INT0只有一个引脚,那么不就只有1个按键能触发中断了?这也太少了吧。
暂且不管,后面再说。~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
中断代码实现
实现功能如下:按键K1触发外部中断0,然后就执行相应的代码,点亮LED。
/** *@file key.c *@author Timi *@date 2022.07.21 */ #include <reg51.h> #define uchar unsigned char sbit P00 = P0^0; sbit P10 = P1^0; void KeyLightLed(void); void Delay10ms(void); //函数入口 void main(void) { IT0 = 1; //选择下降沿触发 EX0 = 1; //使能INT0 EA = 1; //打开中断开关 while(1); } /** *@brief *@param[in] *@param[out] *@return */ void KeyLightLed(void) interrupt 0 { P0 = 0xAA; }
矩阵键盘
按键作为最基本的输入设备,有独立按键和矩阵按键两种。
和LED类似的理念,独立按键就是两个引脚都有单独的线连接。矩阵按键则是将许多按键的同一端连接在一起,然后通过扫描来检测哪个按键被按下。
独立按键与单片机连接时, 每一个按键都需要单片机的一个 I/O 口, 若某单片机系统需较多按键, 用独立按键便会占用过多的 I/O 口资源。 单片机系统中 I/O 口资源往往比较宝贵, 当用到多个按键时为了减少 I/O 口引脚, 引入了矩阵按键,一般叫做矩阵键盘。
可以看到是将16个按键排成4行4列,前面的四行分别连接io口的每一行,后面的四行分别连接io口的每一列,这样就实现了每个io口都连接四个按键,同样通过这样的方式也可以实现3X3,5X5等这样的布局。
那么在检测的时候又是如何实现的呢,这种按键的检测一般是通过扫描来实现的。
具体原理解析如下:
首先,假定p10-p13接地,p14-p17接电源,则可以通过检测p14-p17是否有电平被拉低,因为扫描的速度够快,所以能检测到按键按下,但这种情况只能确定有哪一行被按下了,并不能定位到具体的按键。
要想定位到具体的按键,那么就需要用到二维检测。
首先,让p10置低电平,p11-p13置高电平,同时检测p14-p17是否有电平被拉低,假如此时检测到p14有电平变化,则肯定是s1按键被按下了。同理,可以检测到任意一个按键是否被按下。
矩阵键盘:
优点:省单片机IO
缺点:不能同时按下多个按键数码管扫描(输出扫描)
原理:显示第1位→显示第2位→显示第3位→……,然后快速循环这个过程,最终实现所有数码管同时显示的效果;
矩阵键盘扫描(输入扫描)
原理:读取第1行(列)→读取第2行(列) →读取第3行(列) → ……,然后快速循环这个过程,最终实现所有按键同时检测的效果。
查看原理图
代码实现
将JP4连接到P1端口,P1端口全部为高电平。
/** *@file key.c *@author Timi *@date 2022.07.21 */ #include <reg51.h> #define uchar unsigned char sbit P10 = P1^0; sbit P11 = P1^1; sbit P12 = P1^2; sbit P13 = P1^3; sbit P14 = P1^4; sbit P15 = P1^5; sbit P16 = P1^6; sbit P17 = P1^7; void KeyLightLed(void); void FeelKey(); //函数入口 void main(void) { KeyLightLed(); } /** *@brief *@param[in] *@param[out] *@return */ void KeyLightLed(void) { while(1) { P1 = 0xFE; //经测试,最后一列是P1.7控制的 FeelKey(); P1 = 0xFD; //经测试,最后一列是P1.7控制的 FeelKey(); P1 = 0xFB; //经测试,最后一列是P1.7控制的 FeelKey(); P1 = 0xF7; //经测试,最后一列是P1.7控制的 FeelKey(); } } /** *@brief *@param[in] *@param[out] *@return */ void FeelKey() { if(P14 == 0) { P0 = 0x01; } if(P15 == 0) { P0 = 0x02; } if(P16 == 0) { P0 = 0x04; } if(P17 == 0) { P0 = 0x08; } }
按键消抖再补充
按键的抖动发生在按下或者松开的瞬间,比如低电平触发,需要等其稳定再判断低电平,要不然会由于电平波动,导致可能被判断为高电平。
如果是边沿触发,比如下降沿,只要出现了下降沿就会触发,这里面可能会因为抖动出现多次下降沿,那么中断处理函数就有可能被多次触发。所以,进去之后,先延时,再写处理函数。
单击和长按