你可能会想:按键吗这个很简单啊,按下一个电平抬起一个电平根据io状态进入不同的处理就行了有什么复杂的?
不是这样的我问问你,你的按键灵敏有效吗?运行资源占用多少?能支持复合按键长按键功能吗?复杂的按键逻辑该怎么实现呢?其实按键是一个看似简单做好不容易的东西,话说回来什么东西做好也要花些心思啊.网上经常见到初学者的按键程序不是delay就是中断,实在看着别扭.今天做个教程分析一下我以前写的按键驱动程序,这个程序开销很小,功能却很强估计我下半辈子都会用它了,51下编的,如果在其它U上稍微改改就行了,原理是一样的.另外借助这个驱动讲解一下单片机程序的一般结构.
首先我们来分析一下按键程序的特点,第一点按键程序的实时性要求不高,这一点决定了按键驱动放到大循环里比较好.所谓实时性就是cpu对于按键状态变化是否要及时处理的程度,你可以想象的出来键按下去了晚个几十毫秒反应的话你根本感觉不出来,况且按键是一定要消抖的,消抖最少也会产生100ms左右的时间滞后.这里你可能会想了当然是越及时处理越好了,但是事情是有两面性的把有限的资源按排到最需要的地方才是合理的.
另起一段再啰嗦一整段哈,这段和按键无关.我们可以把事件的处理的及时性分成三类1:while(事件){处理}这是实时性处理最强的,但是这个时候你的单片机不能干别的了,什么时候需要这么做呢?举个例子吧比如你要做一个红外遥控编码的获取程序,实际就是测量脉冲的上下沿的宽度,测量的精度高对于分析编码和发射编码是有好处的,你的程序可以这么写if(检测到红外编码开始) {关闭中断;while(编码结束){测量记录上升沿宽;测量记录下降沿宽;}开启中断;}.因为红外编码时间也就是几个毫秒因此让程序专注于检测对整体影响不会很大.那么类似的if(事件){处理}else{做别的事}可不可以兼顾呢?这个时候你在"做别的事"里时间长短就是对实时性的影响了,平均响应时间最坏情况不说了很好理解吧.特别提及一点就是大循环里的语句会受中断影响,因为中断的到来是不可预知的,严格的时间控制是要关闭中断的.2:安排到中断里处理,但是注意的一点就是中断服务程序不能拖太长的时间,复杂的工作是不能安排到服务里完成的.3:安排到大循环里执行,这是实时性最差的,随着功能越来越多大循环的执行时间会变得越来越长.另外再提及一点,不要只会把程序写成if(事件) {处理;}的结构,当程序复杂了你会陷入绝境的.通过全局变量或位变量可以把事件和处理分割开,把程序按一定的组件分块编写通过函数完成功能这才是重要的,面向对象是个思想,结构混乱自然就没有逻辑了.以后再听到谁说面向对象就是C++和java我们一起鄙视他好吧.
按键程序的第二个特点是功能复合,这一点决定了按键驱动程序最好分成两块:按键扫描和按键获取函数.下面会说扫描函数,所谓功能复合是指一个状态可能对应多种处理方式,一个键按下去了但是在程序的不同部分需要不同的处理方式,比如置数时某个键表示数值加,翻屏时这个键表示显示往上翻等等.从对象的角度讲按键的处理应该属于按键对象之外的东西,不应包含在对象内部,很多初学者的按键程序简单的没有问题,但是程序复杂了以后按键的逻辑再也理不清了.
按键程序的第三个特点是一定要消抖,考虑到这一点你的扫描程序必须要定时运转.消抖就是过一段时间再看取按键状态作比较然后决定按键扫描结果,否则会引起多个按键事件.初学者一看到延时马上想起delay()其实这是个很菜的方法,全都是死循环的延时方法,更有甚者居然能delay(500)ms,真是没话说了.扫描函数的全部内容就是根据按键扫描结果的比较切换按键扫描状态,按键状态只有三种1无按键2有按键的消抖期3消抖结束待取.如果能保证每间隔一定的时间调用一次扫描函数那么扫描函数对某一个变量的计数就可以实现计时的目的,不再需要其他额外的定时了.
这里又来一个啰嗦段,程序里面用计数来定时是个很常用的办法,这也是初学者要学会的.程序里一般都会有很多个需要周期性的执行的函数调用或者需要定时的时间,怎么解决呢?其实你可以安排一个全局量TimeBase每隔固定时间++,也没必要非得要用一个定时器实现,你的程序中肯定有很多个这种周期性的地方,加后来个判断(避免条件重复成立) if ((TimeBase & (2N-1))==0) {调用函数;} 2N是2的n次方等于2,4,8,16...这就相当于分频了,不需要额外的变量.对于不相干的几个定时时间可以加几个全局量timer1,timer2....就解决了,TimeBase++;后执行if (timer1!=0) timer1--;if (timer2!=0) timer2--;...用到的时候如果timerx==0定时时间到否则就是没到定时,要多少有多少啊.另外说明一点这里因为& (2N-1)所以才变得简单的,否则你势必要做一次除法取模数运算才能实现,比如你的时基是1ms你想要一个10ms的分频时间就不得不(TimeBase%10)==0了,10ms是必须的吗?你可以让它不是的.作为一个程序高手一遇到数字首先会想到的是2的N次方行不行,这样处理起来简单,机器最喜欢二进制记住这点省很多事.所以如果非特殊情况真的应该忘记十进制,特殊的情况就是要人机交互的地方你不得不显示成十进制给人家.如果人类天生就是8个手指头这个事情就好理解了.按键程序中的扫描函数调用不需要很快,因为100ms消抖还是105ms消抖其实都无所谓,这个能理解吧?间隔越小精度越高.能接受的最慢速度开销最小,实际调用频率设定为128HZ.
接下来看程序了...
程序中的书写时给变量起个切实有意义的名字;保持标准的语言书写规范;必要的地方使用宏定义;添加必要的注释;这些花不了多少时间但是初学者没有这个习惯.其实良好的书写习惯不仅是为了方便别人阅读和理解,更多的是为了你自己的积累.对于一个程序员来说最有价值的程序莫过于自己亲自调试编写过的代码了,自己写的程序过不了多长时间就会不记得了,即使大致的框架是有的但很多细节肯定是记不清了,程序中的注释能帮你很快的回忆起来,而且通过宏定义可以大大减少程序修改的工作量,这些可能的修改情况你之前就已经考虑过了.特别的是当你拿到一个项目或者和别人讨论一个项目时因为你好有这一部分代码,能实现那些功能你的心里是很有数的,稍微改改调试一下就能用了这是件再愉快都没有的事情了.如果你真的不想让别人看到你的源代码你可以采用编译成库文件的方式只把库文件给他,没必要难为自己.
/********************key.h**************************/
#define KEY_PORTX P3 //按键io口
//键盘扫描码,此数值等于此按键按抬起时port口上的数值(抬起高电平,按下拉低)
#define KEY_1 C_Bit7
#define KEY_2 C_Bit6
#define KEY_3 C_Bit5
#define KEY_4 C_Bit4
#define KEY_5 C_Bit3
#define KEY_6 C_Bit2
#define KEY_CODE (KEY_1+KEY_2+KEY_3+KEY_4+KEY_5+KEY_6)
//说明:以下定义的所指时间均以Key_Scan调用的时间间隔为单位
//按键去抖时间和长按键计时单位 此数值不能大于127
#define KEY_DEJITTER_TIME 16
//长时间按连发功能使用
#define PRESS_KEY_LONGTIME_MESSAGE //如果不用注释掉本行即可
//长按键计时以消抖以后开始计算//长按键时,按下按键每超过一次(下值*按建消抖时间)时就返回一个带长按标记的Key_ID值
#define PRESS_KEY_LONGTIME_VALUE 16 //必须为2的n次整数
#define PRESS_KEY_LONGTIME_FLAG ~KEY_CODE //所有其它未使用的按键位均为标记
extern void Key_Scan(void); //按键扫描
extern uint8 Key_Get(void); //按键获取
//下面两个变量只在程序调试时引出
extern uint8 Press_Key_ID; //按键值
extern int8 Press_Key_Time; //持续按键时间
/********************key.c**************************/
#include "51.h"
#include "Key.h"
#ifdef PRESS_KEY_LONGTIME_MESSAGE
uint8 KeyTimeBase;
#endif
uint8 Press_Key_ID=0; //按键扫描码
int8 Press_Key_Time=-KEY_DEJITTER_TIME;
//键盘扫描
void Key_Scan(void)
{
uint8 tmp=0;
#ifdef PRESS_KEY_LONGTIME_MESSAGE
KeyTimeBase++;
#endif
//非消抖时间键盘扫描,置建值
if (Press_Key_Time==-KEY_DEJITTER_TIME || Press_Key_Time>=0)
{
//单行按键,就一句,如果是4*4按键,用扫描代码替换这句,总之这里tmp就是新的扫描结果
tmp = ~KEY_PORTX & KEY_CODE;
if (tmp==0)
{//无键按下
Press_Key_ID=0;
Press_Key_Time=-KEY_DEJITTER_TIME;
}
else
//有键按下
if (Press_Key_ID != tmp) //按键状态有改变
{
Press_Key_ID = tmp;
Press_Key_Time=-KEY_DEJITTER_TIME+1; //进入消抖期
}
//常有状态保持不变Press_Key_Time=0待取
}
// 消抖期键码不变 keytime++
else
Press_Key_Time++;
}
//获取键值,如果有按键返回键码(已消抖)
//如果长按按键每隔一段时间返回PRESS_KEY_LONGTIME_FLAG标记的扫描码
uint8 Key_Get(void)
{
uint8 ret=0;
if (Press_Key_ID && Press_Key_Time>=0)
if (Press_Key_Time==0) //有待取按键
{
ret=Press_Key_ID;
Press_Key_Time++;
#ifdef PRESS_KEY_LONGTIME_MESSAGE
KeyTimeBase=0;
}
else //>0 有长按键
{
//每个消抖时间Press_Key_Time+1
if (KeyTimeBase >= KEY_DEJITTER_TIME)
{
KeyTimeBase=0;
Press_Key_Time++;
//长按
if ((Press_Key_Time & (PRESS_KEY_LONGTIME_VALUE-1)) ==0)
ret=Press_Key_ID|PRESS_KEY_LONGTIME_FLAG;
}
#endif
}
return ret;
}
/********************main.c*************************/
//128HZ调用,驱动扫描
Key_Scan();
//头文件中重新定义按键名称比如 #define KEY_LEFT KEY_1其它的省略了...
//需要按键处理的地方
switch (Key_Get()) {
case KEY_LEFT:
break;
case KEY_RIGHT:
break;
case KEY_UP:
break;
case KEY_DOWN:
break;
case PRESS_KEY_LONGTIME_FLAG|KEY_UP: //长按温度设置快加
C1Temp_Setting+=10;
break;
case PRESS_KEY_LONGTIME_FLAG|KEY_DOWN: //长按温度设置快减
C1Temp_Setting-=10;
break;
case KEY_SHIFT:
break;
case KEY_ENTER:
break;
case PRESS_KEY_LONGTIME_FLAG|KEY_ENTER:
break;
case KEY_SHIFT|KEY_LEFT: //组合键置温度设置
break;
case KEY_SHIFT|KEY_RIGHT: //组合键置温度报警范围
break;
case KEY_SHIFT|KEY_UP: //组合键置延时
break;
case KEY_SHIFT|KEY_DOWN: //组合键置
break;
}//end switch
教程就到这里了,一点经验之谈希望对大家有用.
[ 本帖最后由 huo_hu 于 2013-4-15 08:32 编辑 ]
|