@yifan
2017-01-02T04:42:55.000000Z
字数 5916
阅读 1898
吴坚鸿
引言:
本文主要介绍了在单片机编程的实际应用中,我们是以一个怎样的思路去编写按键的驱动程序。此外,需要注意的是由于实际的代码是比较长的,所以我只截取其中核心的部分进行分析,具体的代码请访问21IC吴坚鸿的单片机帖子: 点我访问
具体实现的代码:
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即可
其实组合按键的识别比较简单,即两个按键同时按下就触发,然后转换为相应的键值。只不过我们需要注意的是使用的场合:比如在矩阵中的组合按键识别就需要小心,以免烧坏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不同罢了。在此就不详细说了。
寻呼机流行的时候,寻呼机往往只有一个设置按键,它要求用一个按键来设置不同的参数,这个时候就要用到同一个按键来实现短按和长按的区别触发功能。要现实这种功能,我们该怎么写程序?
区别短按和长按的要点在于:两者按下的时间长短不同,所以可以设定两个阈值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 按键识别短按和长按
在很多需要人机交互的项目中,需要用按键来快速加减某个数值,这个时候如果按住一个按键不松手,这个数值要有节奏地快速往上加或者快速往下减。要现实这种功能,我们该怎么写程序?
先引入一些基本的知识点:
在单片机的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极限就会匀速运动。这是一个运动的模式,也即运动控制思想,即通过几个变量判断就可以完成。
通过本节的分析,我们主要列了按键的几种运动状态,通过分析其时间和电平的变化情况,来判断按键的几种触发模式,将有助于提高我们对于运动模型的理解。最后发表一下个人的感悟:
- 在单片机编程中有两类状态机,一类由if/else if/...实现,另外一类由switch来实现。两类不同的状态机可以用于不同的场合:if/else 更加适合运动控制模型和顺序模型,即我们需要判断的值是随时间变化且未知其具体时间的;而switch更加适合消息机制,同时也可以用于顺序模型。两者的共性基本相同,只不过if/else这种情况更加适合于抗干扰,如上面的例子所示,if的优先级最高,所以一旦出现干扰项就会立即停止或者做相应的处理。而switch如果实现抗干扰,可能要多重嵌套switch,从逻辑上就显得别扭,代码上显得累赘。那么,我们以后需要根据自己的需要去选择不同的结构以适应不同的环境。
- 要建立起时间与运动的关系,通过分析其规律的运动模型,并在不同的场合描述其变化是要点,同时要学会合理使用变量、标志来做合理的判断。
那么做完了独立按键的运动分析,接下来我们会分析在矩阵键盘中如何触发。请看下节。