作者:水瓶星人
座右铭:记录每一分痛点,分享每一点收获
突然看到之前写的一篇博客,发现其按键处理程序和最近使用到的一个按键处理程序不大一样,拿过来综合比较一下。
首先,我们在判断按键按下时,最简单的处理方法是直接比较按键对应引口的高低电平。
比如 按键按下点亮LED,我们可以直接写
//假设button1代表按键1,led低电平点亮
if(button1==0)
{
led=0; //亮
}
else if(button1 == 1)
{
led=1; //灭
}
但是,如果使用这种方法去处理时钟 时分秒 的加减设置,一旦按下由于程序运行速度极快,我们按一下,相应的检测程序实际上已经执行了很多遍了,在实际使用中就是,只按一次 时/分/秒数却增加好多。
比如代码:
while(1)
{
//button123对应三个按键 shi:时 fen:分 miao:秒
if(button1==0) shi++; break;
if(button2==0) fen++; break;
if(button3==0) miao++; break;
}
解决方法一
在最开始的时候我的处理是在按键按下时,我让程序进入死循环,等待按键松开后再执行相应的时分秒加减。
比如代码:
if(button2 == 0)
{
switch(button_num1)
{
case 0: break;
case 1: delay_us(10); while(~button2); num3++; break; //等待按键释放 时加一
case 2: delay_us(10); while(~button2); num2++; break; //等待按键释放 分加一
case 3: delay_us(10); while(~button2); num1++; break; //等待按键释放 秒加一
default: break;
}
if(num3 == 24) num3 = 0; //超出归零
if(num2 == 60) num2 = 0;
if(num1 == 60) num1 = 0;
}
这种方法的优点是:简单粗暴,易于理解。
但是缺点也很明显:就是我在按下按键后如果我不松开按键,那么程序将一直卡在死循环里无法去做其他的事,对应明显表现就是液晶屏此时不显示数值了或者显示数值缺失。
注:这种方法也是我在做单片机显示时间时看到使用最多的一种方法,但是我并不推荐
解决方法二
这个时候我们就需要对按键进行长按判断处理,让程序知道我 按下——松开 才是完成了 一次 按键按下动作。而不是让程序忙等。
接下来我假设我要用KEY1和KEY2去修改时间的秒数,当KEY1按下时秒数加一,KEY2按下时秒数减一,我们可以这么处理
//按键扫描函数,返回值确定按键是否按下
u8 KEY_Scan()
{
static u8 key_up=1;//按键按松开标志
if(key_up&&(KEY1==0||KEY2==0))
{
delay_ms(10);//去抖动
key_up=0; //进入一次按键就将按键按下标志置0,防止重复进入
if(KEY1 == 0)
{
return '1'; //KEY1按下
}
else if(KEY2 == 0)
{
return '2'; //KEY2按下
}
}else if(KEY1==1&&KEY2==1) key_up=1; //当无按键按下或着按键松开后,按键标志位 重新置1,为下次按键按下做准备
return '0';// 无按键按下
}
//设置秒数
void Miao_Setting()
{
u8 key; //定义一个接收变量
key=KEY_Scan(); //key等于 ‘0’/‘1’/‘2’
switch(key)
{
case '0': break; //没有按下直接跳出
case '1': miao++; break;
case '2': miao--; break;
}
}
通过这个函数,我们就可以实现每按一下,对应修改数值 加一/减一。
这个函数实现起来比较简单,但是我们也可以看到,如果我们要判断好多按键按下,就比如说要判断8个按键其中某个按键是否按下,那么我们就需要在 if 条件判断中加入8个按键状态判断,判断语句也将由两条变为8条,代码就显得很长。
解决方法三
现在我们有一种更加高大上的方法来做这项处理。
先花几分钟看下代码:
/***********************************************************
*函数名 :KeyScan
*功能 : 按键扫描,返回按下的按键对应的位,并防止连按
*返回值 :返回按键按下的位置
*参数 :无
************************************************************/
char KeyScan()
{
static unsigned char klast = 0; //记录上一次的按键值
unsigned char trg = 0,key = 0; //trg:得到的返回值,返回值中8位只有一位为1,为1的位代表按下的位,其余位为零
key = P2 & 0x0F; //将按下的位转换为0 没按下的位依然为1
key ^= 0x0f; //异或之后没按下的位转换为1,按下的位转换为0
trg = key & (key ^ klast); //这句是最关键的一句,需要自己理解
klast = key;
return trg; //最终返回值是按下哪个按键,返回值对应就是1,没按对应的就是0 eg:按下button3 则返回0x01 0000 0100
}
如果没有理解也没关系,我们先举个例子
假设:P2是51单片机的P2口,对应有 P0_0——P2_7 8个引口,现在我们把低4个引口接入按键,高4个引口作为其他用途不得改变,检测低四位中哪个被按下
我画了一张表,取了六种情况,得到六种情况的返回值。
如下:
通过这张表我们也可以看出,
- 按键都没按下时,返回值的低四位都为0
- 低四位某个按键被按下时,其相应位返回值为1,其余位为0
- 某个按键长按时,其对应位返回值变回0
- 在上一次的按键未松开时按住另一个按键,未松开的按键对应返回值为0,新按下的按键对应返回值为1
- 同时按住多个按键,新按下的按键对应位返回值为1,已经按过的按键对应位返回值为0
结论:此函数能够指出新按下的按键位置,以1标明按下的是哪一位,并支持多按键触发
我们在使用的时候只需要比较对应按键位的值是否为1即可,为1则表示此按键按下,为0表示按键未按下或未松开。
使用举例:
void button_setting()
{
if((button_flag & 0x0f) == 0x01 ) //此button_flag即trg 判断最低位是否被按下
{
button_num1++; //按下就加一
}
}
效果对比(注意看我按键按下的动作)
方案一:
方案二、三
总结:
方案三 优于 方案二 优于 方案一
在以后的开发中应该尽量使用更少的代码去实现相应的功能,在单片机开发中更是如此。
一一一一一一一一一2021年6月10日更新一一一一一一一一一
找到一篇针对第三种方法更加生动形象的解释,收获良多,帖子地址:点击进入
第三种方法的简单写法
unsigned char Trg;
unsigned char Cont;
void KeyRead( void )
{
unsigned char ReadData = PINB^0xff; // 1
Trg = ReadData & (ReadData ^ Cont); // 2
Cont = ReadData; // 3
}
Trg(triger) 代表的是触发,Cont(continue)代表的是连续按下。
一一一一一一一一一2023年10月24日更新一一一一一一一一一
针对第三种方法最近找到一篇相关专利介绍,专利号:CN202110678156.5
在专利介绍中针对那段代码有一些专业的描述,这里我复制粘贴一下。
原文代码
Read_Data = (GPIO_Read&X)^X
Trag = Read_Data&(Read_Data^Count)
Rel = Read_Data^Trag^Count
Count = Read_Data
描述:GPIO_Read表示所述目标数据,X为预设值且X的第1bit至第Nbit均为1,Trag的第1bit至第Nbit依次表示第1按键至第N按键各自的触发变量值,Rel的第1bit至第Nbit依次表示第1按键至第N按键各自的释放变量值,Count的第1bit至第Nbit依次表示第1按键至第N按键各自的长按变量值,Read_Data为预设的中间变量;&表示与操作,∧表示异或操作;
(PS:这里的释放变量值就像klast一样,记录上一回按下的按键)
代码转换一下就是
char KeyScan()
{
static unsigned char klast = 0;
unsigned char trg = 0,key = 0;
key = (P2 & 0x0F)^0x0f
trg = key & (key ^ klast);
Rel = key^Trag^klast
klast = key;
return trg;
}
感兴趣的可以下载那篇专利看一下,这里附上下载链接:点击进入 提取码:crnf
希望本文能对你有所帮助 谢谢!