按键扫描,我想应该是比较简单的单片机应用了,但是有时候看起来简单的东西反而不好写。
本文拿大部分人觉得简单的按键扫描聊聊我工作至今对于软件结构的理解。嗯,对的,是结构,不是架构,暂时不敢提架构这个词。
按键扫描,我当时入门的时候是看的郭天祥的51单片机入门的,视频里面讲的是循环扫描io引脚,一旦有电平变化就利用软件延时消抖,模拟延时就是让单片机空转,什么也不做,等待个几十毫秒之后再检测一次如果电平没有变化就认为按键按下。这种方法也能实现按键检测,好处是简单,缺点是占用太多的软件资源,CPU空转这一点我觉得挺不好的。
下面说说我个人对于一个按键检测的代码理解。
按键检测需要做什么事情呢?一个是按键按下的这个物理事件的检测,一个是按下时候的消抖。能想到这两个已经可以写一段代码来实现功能了。
#define DEBOUNCE 10//延时消抖时间
uint8 key_scan( uint8 keycur )
{
static uint8 KeyLast = KEY_NULL;
static uint8 KeyCountdowm = 0;
uint8 keyret = KEY_NULL;
if( keycur != KEY_NULL ) //如果检测到按键按下,开始进行延时消抖
{
if( KeyLast == keycur || KEY_NULL == KeyLast)
{
KeyCountdowm++;
}
}
else
{
if( KeyCountdowm >= DEBOUNCE)//按键抬起
{
keyret = KeyLast;
}
else
{
keyret = KEY_NULL;
}
KeyCountdowm = 0;
}
KeyLast = keycur;//按键备份值更新
return keyret;
}
上面是一个简单的按键扫描函数,函数需要放置在一个10ms的定时函数里面,注意是定时函数不是定时中断函数,需要传递按键信息,这个信息可以是从通信函数获取的,也可以是直接读取IO端口获得,返回一个消抖过后的键值,键值不做逻辑判断。
然后我们来聊聊这个消抖函数,这个消抖函数只实现了一个功能,按下消抖,那如果使用环境或者硬件设计缺陷,导致抬起的时候也有抖动呢?所以需要添加抬起消抖。
下面对函数进行优化一下
#define DEBOUNCE 10//延时消抖时间
uint8 key_scan( uint8 keycur )
{
static uint8 KeyLast = 0;
static uint8 KeyCountdowm = 0;
static uint8 KeyCountup = 0;
unsigned uint8 keyret = 0;
keyret = KEY_NULL;
if( keycur != KEY_NULL )
{
if( KeyLast == keycur || KEY_NULL == KeyLast)
{
KeyCountdowm++;
KeyCountup = 0;
}
}
else
{
KeyCountup++;
if( KeyCountup > 1 )
{
KeyCountup = 0;
if( KeyCountdowm >= DEBOUNCE )
{
keyret = KeyLast;
KeyCountdowm = 0;
}
}
}
KeyLast = keycur;
return keyret;
}
添加了20ms的抬起消抖,这段代码添加了两个消抖检测,一个是在抬起的时候有20ms的消抖,一个是在按键按下过程的一个过程消抖。
上面的代码实现的是抬起有效,那么如果需要做到按下有效呢?
#define DEBOUNCE 10//延时消抖时间
uint8 key_scan( uint8 keycur )
{
static uint8 KeyLast = 0;
static uint8 KeyCountdowm = 0;
static uint8 KeyCountup = 0;
unsigned uint8 keyret = 0;
keyret = KEY_NULL;
if( keycur != KEY_NULL )
{
if( KeyLast == keycur || KEY_NULL == KeyLast)
{
KeyCountdowm++;
KeyCountup = 0;
if( KeyCountdowm >= DEBOUNCE )
{
keyret = KeyLast;
}
}
}
else
{
KeyCountup++;
if( KeyCountup > 1 )
{
KeyCountup = 0;
KeyCountdowm = 0;
}
}
KeyLast = keycur;
return keyret;
}
挪动按键的延时检测判断语句,在按下超过延时消抖时间的时候,返回按键按下值。
然后一个简单按键检测函数就实现了,下面再给这个函数添加长按键判断和连续按键。
#define DEBOUNCE 10//延时消抖时间
#define LONGPRESS 100//长按键判断函数
uint16 key_scan( uint8 keycur )
{
static uint8 KeyLast = 0;
static uint8 KeyCountdowm = 0;
static uint8 KeyCountup = 0;
unsigned uint16 keyret = 0;
keyret = KEY_NULL;
if( keycur != KEY_NULL )
{
if( KeyLast == keycur || KEY_NULL == KeyLast)
{
KeyCountdowm++;
KeyCountup = 0;
if( KeyCountdowm >= DEBOUNCE && KeyCountdowm < LONGPRESS)//短按键判断
{
keyret = KeyLast;
}
else if( KeyCountdowm == LONGPRESS )//长按键判断
{
keyret = KeyLast;
keyret |= 0x0100;
}
else if(KeyCountdowm > LONGPRESS+DEBOUNCE)//连续按键判断
{
KeyCountdowm -= DEBOUNCE;
keyret = KeyLast;
keyret |= 0x0200;
}
}
}
else
{
KeyCountup++;
if( KeyCountup > 1 )
{
KeyCountup = 0;
KeyCountdowm = 0;
}
}
KeyLast = keycur;
return keyret;
}
当有多个设备的时候,可以将静态局部变量修改为结构体指针的形式,如下
type struct key
{
uint8 Last;
uint8 CountDowm;
uint8 CountUp;
}KEY_TYPE;
#define DEBOUNCE 10//延时消抖时间
#define LONGPRESS 100//长按键判断函数
uint16 key_scan( KEY_TYPE *Key ,uint8 keycur)
{
unsigned uint16 keyret = 0;
keyret = KEY_NULL;
if( keycur != KEY_NULL )
{
if( Key->Last == keycur || KEY_NULL == Key->Last)
{
Key->CountDowm++;
Key->CountUp = 0;
if( Key->CountDowm >= DEBOUNCE && Key->CountDowm < LONGPRESS)//短按键判断
{
keyret = KeyLast;
}
else if( Key->CountDowm == LONGPRESS )//长按键判断
{
keyret = Key->KeyLast;
keyret |= 0x0100;
}
else if(KeyCKey->CountDowmountdowm > LONGPRESS+DEBOUNCE)//连续按键判断
{
Key->CountDowm -= DEBOUNCE;
keyret = Key->KeyLast;
keyret |= 0x0200;
}
}
}
else
{
Key->CountUp++;
if( Key->CountUp > 1 )
{
Key->CountUp = 0;
Key->CountDowm = 0;
}
}
Key->KeyLast = keycur;
return keyret;
}
最后说说这个功能的实现,按键检测分为三个部分,一个是按键获取函数,一个是消抖,一个是按键筛选函数,先把代码贴上来。
type struct key
{
uint8 Last;
uint8 CountDowm;
uint8 CountUp;
}KEY_TYPE;
#define DEBOUNCE 10//延时消抖时间
#define LONGPRESS 100//长按键判断函数
/*
* description: 按键消抖函数
* intput:按键结构体,键值
* output:键值
*
*/
uint16 key_scan( KEY_TYPE *Key ,uint8 keycur)
{
unsigned uint16 keyret = 0;
keyret = KEY_NULL;
if( keycur != KEY_NULL )
{
if( Key->Last == keycur || KEY_NULL == Key->Last)
{
Key->CountDowm++;
Key->CountUp = 0;
if( Key->CountDowm >= DEBOUNCE && Key->CountDowm < LONGPRESS)//短按键判断
{
keyret = KeyLast;
}
else if( Key->CountDowm == LONGPRESS )//长按键判断
{
keyret = Key->KeyLast;
keyret |= 0x0100;
}
else if(KeyCKey->CountDowmountdowm > LONGPRESS+DEBOUNCE)//连续按键判断
{
Key->CountDowm -= DEBOUNCE;
keyret = Key->KeyLast;
keyret |= 0x0200;
}
}
}
else
{
Key->CountUp++;
if( Key->CountUp > 1 )
{
Key->CountUp = 0;
Key->CountDowm = 0;
}
}
Key->KeyLast = keycur;
return keyret;
}
/*
* description: 获取按键的函数
* intput:none
* output:键值
*
*/
uint8 GetKeyValue(void)
{
uint8 ret=NULL;
/*
这个部分的代码需要自己实现,这里可以从通信函数如iic,spi获得键值,也可以从gpio端口获得键值
*/
return ret;
}
#define KEY_NULL 0
#define KEY_UP 1
#define KEY_DOWN 2
#define KEY_ON 3
#define KEY_UP_L 4
#define KEY_DOEN_L 5
#define KEY_ON_L 6
/*
* description: 筛选按键的函数,只筛选需要的键值
* intput:键值
* output:键值
*
*/
uint8 GetKeyValue(uint8 key)
{
uint8 ret=KEYNULL;
switch(key)
{
0x01:
{
ret = KEY_UP;
}
break;
0x0101:
{
ret = KEY_UP_L;
}
break;
default:
break;
}
return ret;
}
函数的三个部分的按键获取函数部分需要自己去编写,不同的键值来源不同,我试过从IIC中读取键值,也试过从GPIO中读取键值。
然后是消抖函数,消抖函数需要如果有多个按键来源的话需要定义多个结构体,如果只有一个键值来源可以替换成静态局部变量版的函数。
分成三个部分的好处是,函数间各干各事情,互不影响,更容易读,我在接手别人的代码时候,如果涉及到按键部分出现问题的话,一般先问上一个维护的人做没做过程消抖,如果回答说有做,好的,接着问怎么做的,一般把对方的过程消抖搞懂了,基本就懂了按键这部分的代码。如果对方说没有做或者很茫然的看着我,好吧,我会直接把对方的这部分代码删掉,重写。
写于2017年7月30日 一个没有空调的夏天
深圳