[关闭]
@yifan 2017-01-02T04:42:55.000000Z 字数 5916 阅读 1898

单片机编程与实际应用-独立按键专题分析

吴坚鸿


引言:

本文主要介绍了在单片机编程的实际应用中,我们是以一个怎样的思路去编写按键的驱动程序。此外,需要注意的是由于实际的代码是比较长的,所以我只截取其中核心的部分进行分析,具体的代码请访问21IC吴坚鸿的单片机帖子: 点我访问


1.独立按键的识别原理:


识别的流程图

具体实现的代码:

enum {
    key_unlock = 0 ; 
    key_lock   = 1 ;
};
enum {
    over  = 0 ;
    start = 1 ;
};
#define TIMES_10MS  (4)
#define KEY_NULL   (0)
#define key = key_1 ;
sbit    key_pin = P3^0 ; 
uint8_t key_counter = 0 ;
uint8_t key_locker  = key_unlock ;
uint8_t key_startflag = over ;
uint8_t key_value = KEY_NULL ;
...
void key_scan()
{
//当IO口为高电平时,清零计数器和locker标志
    if ( key == 1 )
    {
        key_counter = 0 ;
        key_locker  = key_unlock ;
        key_startflag = over ;         // 开启定时器按键计数
    } 
    else if ( key_locker == key_unlock )   // IO为低电平,且第一次按下
    {
        key_startflag = start ;
        if( key_counter >= TIMES_10MS )
        {
            key_counter = 0 ;           // 清零计数器的值
            key_locker  = key_lock ;  // 上锁,防止一直触发
            key_startflag = over ;      // 关闭定时器按键计数
            key_value = key_1 ;         // 返回键值
        }
    } // end of unloker 
} // end of key scan
...
void timer1_isr() interrupt 3 
{
    ET1 = 0 ; // 先关定时器1中断
    ...
    // 
    if ( key_startflag == start )  // 开始计数
    {
         key_counter++ ; 
    } // end of key_cnt++

    ET1 = 1 ; // 打开定时器1中断
    ...
}

void key_servier()
{
    switch ( key_value )
    {
    case key_1 :
        ... // dosmething
        key_value = KEY_NULL ;
        break;
    ...  // other case 
    }
}

代码如上:当我们按照流程图的顺序走,其实发现并没有多大困难,只是在理解上面要注意if/else if 的应用。这里还有可以优化的地方,可以用一个结构体对按键进行封装如下:

typedef struct KEY{ // 没有加入IO口,可能要重新写reg52.h
uint8_t startflag ;
uint8_t locker ;
uint8_t counter ;
uint8_t value ;
}KEY;

独立按键扫描的详细过程:
第一步:平时没有按键被触发时,按键的自锁标志,去抖动延时计数器一直被清零。
第二步:一旦有按键被按下,去抖动延时计数器开始在定时中断函数里累加,在还没累加到 阀值TIMES_10MS时,如果在这期间由于受外界干扰或者按键抖动,而使IO口突然瞬间触发成高电平,这个时候马上把延时计数器key_counter 清零了,这个过程非常巧妙,非常有效地去除瞬间的杂波干扰。这是我实战中摸索出来的。 以后凡是用到开关感应器的时候,都可以用类似这样的方法去干扰。
第三步:如果按键按下的时间超过了阀值TIMES_10MS,则触发按键,把编号key_value赋值。 同时,马上把自锁标志key_locker置位,防止按住按键不松手后一直触发。
第四步:等按键松开后,自锁标志key_locker及时清零,为下一次自锁做准备。
第五步:以上整个过程,就是识别按键IO口下降沿触发的过程。

需要调用的时候,先申请一个按键的空间,即"KEY key1;"这样就可以比较方便的引用了。在while(1)主循环里面直接调用key_scan和key_service即可

2.组合按键的识别


其实组合按键的识别比较简单,即两个按键同时按下就触发,然后转换为相应的键值。只不过我们需要注意的是使用的场合:比如在矩阵中的组合按键识别就需要小心,以免烧坏IO口。
以下附上代码:

...
if(key_sr1==1||key_sr2==1)//IO是高电平,说明两个按键没有全部被按下,这时要及时清零一些标志位
{
ucKeyLock12=0; //按键自锁标志清零
uiKeyTimeCnt12=0;//按键去抖动延时计数器清零
}
else if(ucKeyLock12==0)//有按键按下,且是第一次被按下
{
uiKeyTimeCnt12++; //累加定时中断次数
if(uiKeyTimeCnt12>const_key_time12)
{
uiKeyTimeCnt12=0;
ucKeyLock12=1; //自锁按键置位,避免一直触发
key_value=1; //触发1号键
}
}
...


和上面的独立按键感觉没有什么区别,只不过在key_sr1==1||key_sr2==1不同罢了。在此就不详细说了。

寻呼机流行的时候,寻呼机往往只有一个设置按键,它要求用一个按键来设置不同的参数,这个时候就要用到同一个按键来实现短按和长按的区别触发功能。要现实这种功能,我们该怎么写程序?

3.同一按键的短按和长按


区别短按和长按的要点在于:两者按下的时间长短不同,所以可以设定两个阈值short_press_time和long_press_time,当按下的时间超过short_press_time,短按标志位置1;如果还被按下,时间超过
long_press_time,会把短按标志位清0,触发长按功能,并且将按键上锁;如果按键松开发现短按标志位置1,则表示短按有效触发短按,并且上锁。代码如下:

#define short_press_time (10)
#define long_press_time  (20)
sbit key_pin = P2^0;
uint8_t key_short_press_flag = 0 ;
uint8_t key_locker = key_unlock ;
uint8_t key_time_counter = 0 ;
...
if ( key_pin == 1 )
{
   key_locker = key_unlock ;
   key_time_counter = 0 ;
   if ( key_short_press_flag == 1)
   {
   key_short_press_flag = 0 ;
   key_type = short_press;  // 触发短按
   }
}
else if ( key_locker == key_unlock )
{
    key_time_counter++ ;
    if ( key_time_counter >  short_press_time )
    {
     key_short_press_flag = 1 ;
    }// end fo short press 识别

    if ( key_time_counter > long_press_time )
    {
        key_short_press_flag = 0 ;
        key_time_counter = 0 ;
        key_locker = key_lock ;
        key_type = long_press ; // 触发长按
    } // end of long press 识别
} // end of 按键识别短按和长按

在很多需要人机交互的项目中,需要用按键来快速加减某个数值,这个时候如果按住一个按键不松手,这个数值要有节奏地快速往上加或者快速往下减。要现实这种功能,我们该怎么写程序?

4.按键的连发模式实现快速加和快速减


先引入一些基本的知识点:

在单片机的C语言编译器中,当无符号数据0减去1时,就会溢出,变成这个类型数据的最大值。比如是unsigned int类型的0减去1就等于65535(0xffff),unsigned char类型的0减去1就等于255(0xff)。这个常识经常要用在判断数据临界点的地方。比如一个数最大值是20,最小值是0。这个数据一直往下减,当我们发现它突然大于20的时候,就知道它溢出了,这个时候要及时把它赋值成0就达到我们的目的。
通过判断是否大于临界值就可以断定是否加到某个值或者减到0了没有。


先观察连续触发模式有什么特点:当触发一次按键后不松手,如果超过一秒钟,就会进入连发模式,进入连发模式后,需要一个连发模式的计数器,一直累加,并且按照规定的时间累加(例如每0.25秒触发一次连发,被设置的值就会+1)

放一段具体的实现代码:

#define time_1s (444)
#define time_0_25s (111)
#define TIME_DELAY (3)
sbit key_pin = P2^0 ;
uint8_t key_time_counter = 0 ;
uint8_t key_locker = key_unlock ;
uint8_t key_continue_counter = 0 ;
uint8_t set_value = 0 ; // 按键连发后待加减设置值
...
if ( key_pin == 1 )  // 没有按下,或者干扰
{
   key_time_counter = 0 ;
   key_locker = key_unlock ;
   key_continue_counter = 0 ;
}
else if ( key_locker == key_unlock ) // 第一次按下
{
    key_time_counter++ ; 
    if ( key_time_counter > TIME_DELAY )
    {
      key_time_counter = 0 ;
      key_locker = key_lock ;
      key_value = key_1;   //触发按键
    }
}
else if ( key_time_counter < time_1s ) // 按下之后有没有长1s
{
    key_time_counter++ ;
}
else // 超过一秒就会触发连发模式
{
    key_continue_counter++ ;
    if ( key_continue_counter >= time_0_25s )  //每0.25s,触发连发,设置值+1或者按照规定的值加
    {
        key_continue_counter = 0 ;
        //
        key_value = 1 ; // 触发按键
    }
}
...
switch ( key_value )
{
case 1 : set_value++ ;  // 按键值为1,表示计数值加
         if ( set_value > 20 ) // 最大为20 
         {
            set_value = 20 ; 
         }
        key_value = 0 ; // 防止一直触发
        break;
case 2 : set_value-- ; // 按键值为2,表示计数值减
        if ( set_value >20)//最小是0.为什么这里用20?因为0减去1溢出了,65535
         {
            set_value = 0 ; 
         }
        key_value = 0 ; // 防止一直触发
        break;
}

本程序可以有节奏地快速往上加或者快速往下减。假如被设置数据的范围不是20,而是1000。如果按0.25秒的节奏往上加,那不是累死人了?如果直接把0.25秒的节奏调快到0.01秒,那么到达999的时候,还来不及松手就很容易超过头,不好微调。下面介绍一种方法,可以实现按键不松手的加速匀速触发。

好了,放一段程序:

...
if ( key_pin == 1 )  // 没有按下,或者干扰
{
   key_time_counter = 0 ;
   key_locker = key_unlock ;
   key_continue_counter = 0 ;
   key_continueTimeSet1=initial_set; //按键每次触发的时间间隔初始值,这数值不断变小,导致速度不断加快
}
else if ( key_locker == key_unlock ) // 第一次按下
{
    key_time_counter++ ; 
    if ( key_time_counter > TIME_DELAY )
    {
      key_time_counter = 0 ;
      key_locker = key_lock ;
      key_value = key_1;   //触发按键
    }
}
else if ( key_time_counter < time_1s ) // 按下之后有没有长1s
{
    key_time_counter++ ;
}
else // 超过一秒就会触发连发模式
{
    key_continue_counter++ ;
    if ( key_continue_counter > key_continueTimeSet1 )  //一旦超过规定的时间,连发加速模式
    {
        key_continue_counter = 0 ;
        if ( key_continueTimeSet1 > min_level )
        {
            key_continueTimeSet1 -= deta_speed ; //加速运动
        }
        else 
        {
            key_continueTimeSet1= min_speed ; //速度达到极限值
        } // end of 
        countine_flag = 1 ; // 连发模式置1
        key_value = 1 ; // 触发按键
    }
}

其实上面的关键点在于:

如果按下去不松手的时间超过1秒,则进入连续加速触发模式。按键触发节奏不断加快。直到它们都到达一个极限值,然后以此极限值间隔匀速触发。它是通过key_continueTimeSet1来控制加速的快慢的,由于key_continueTimeSet1一直在减deta_speed,直到min_speed极限就会匀速运动。这是一个运动的模式,也即运动控制思想,即通过几个变量判断就可以完成。

5.个人总结


通过本节的分析,我们主要列了按键的几种运动状态,通过分析其时间和电平的变化情况,来判断按键的几种触发模式,将有助于提高我们对于运动模型的理解。最后发表一下个人的感悟:

  • 在单片机编程中有两类状态机,一类由if/else if/...实现,另外一类由switch来实现。两类不同的状态机可以用于不同的场合:if/else 更加适合运动控制模型和顺序模型,即我们需要判断的值是随时间变化且未知其具体时间的;而switch更加适合消息机制,同时也可以用于顺序模型。两者的共性基本相同,只不过if/else这种情况更加适合于抗干扰,如上面的例子所示,if的优先级最高,所以一旦出现干扰项就会立即停止或者做相应的处理。而switch如果实现抗干扰,可能要多重嵌套switch,从逻辑上就显得别扭,代码上显得累赘。那么,我们以后需要根据自己的需要去选择不同的结构以适应不同的环境。
  • 要建立起时间与运动的关系,通过分析其规律的运动模型,并在不同的场合描述其变化是要点,同时要学会合理使用变量、标志来做合理的判断。

那么做完了独立按键的运动分析,接下来我们会分析在矩阵键盘中如何触发。请看下节。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注