|
用过单片机的人应该都熟悉矩阵键盘扫描程序。矩阵键盘一般是依次扫描输出管脚,需要N(N为输出管脚的数目)次扫描才能完成整个键盘的一次完整扫描。不过,你见过一次就可以扫描一个完整键盘的程序吗? 呵呵,这个你应该见过的,这就是键盘扫描中很有特色的线反转法,在不少教科书中都介绍过。 所以我今天要介绍的不是线反转法这个扫描算法,而是将定时器和线反转法结合在一起的一种扫描算法。引入定时器后,由于整个扫描过程都是在中断中进行的,所以我给他起了个名字:全中断矩阵键盘。 不知道大家还记不记得我以前写的那个全中断键盘,那是针对独立按键的,当将其引入到矩阵键盘中后,带来的一系列问题更加的有意思。 下面我就详细介绍一下这个“全中断矩阵键盘”。 首先回忆一下全中断键盘这个算法,在键盘中,作为按键输入的GPIO管脚开启中断,在GPIO中断到来时,关闭GPIO中断,同时打开定时器开始计时(消抖)。在定时器中断中读取按键的值,通俗关闭自己的终端,打开GPIO中断,准备迎接下一次中断的到来。两个中断就是这样接力完成整个扫描过程,所有的扫描过程都是在中断中的一个简短运算,对软件资源消耗极少。 好了,秀完了我所谓的中断键盘,再来介绍一下线反转法这个经典的矩阵键盘扫描算法吧。 线反转法,故名思义,在其扫描过程中有一次输入输出反向的过程。其扫描算法是(假设输入管脚上拉,低电平按键有效):扫描时,所有的输入管脚都上拉,所有的输出管脚都输出低电平,然后检测输入管脚。当输入管脚检测到低电平时(不论在哪个管脚上的),读入作为行号。然后将输入和输出管脚反过来,原来的输出脚做输入,原来的输入脚做输出,将在它上面读取的整组值原封不动的输出。如果只有一个按键按下,则此时的输出管脚中,只有一只管脚是低电平,(相当与直接取有键按下的行来进行扫描,效率当然高了)再从现在的输入管脚读取数据作为列号,然后根据行号和列号来解码。 最基本的键盘扫描程序如下:
-
void IntGPIOb(void) { IntDisable(INT_GPIOB); GPIOPinIntClear(GPIO_PORTB_BASE,0x0f); SysTickEnable(); }
void IntSysTick(void) { SysTickDisable(); ReadData=GPIOPinRead(GPIO_PORTB_BASE,0x0f); if(ReadData!=0x0f); { GPIODirModeSet(GPIO_PORTB_BASE,0xf0,GPIO_DIR_MODE_IN); GPIODirModeSet(GPIO_PORTB_BASE,0x0f,GPIO_DIR_MODE_OUT); GPIOPinWrite(GPIO_PORTB_BASE,0x0f,ReadData); KeyCoder=(ReadData & 0x0f)|(GPIOPinRead(GPIO_PORTB_BASE,0xf0) & 0xf0); Key_Change=1; } GPIODirModeSet(GPIO_PORTB_BASE,0x0f,GPIO_DIR_MODE_IN); GPIODirModeSet(GPIO_PORTB_BASE,0xf0,GPIO_DIR_MODE_OUT); GPIOPinWrite(GPIO_PORTB_BASE,0xf0,0x0f); IntEnable(INT_GPIOB); }
复制代码
GPIO中断根原来的独立按键模式一样,都是打开定时器中断即可。这里采用的是M3内核处理器特有的Systick定时器,比通用定时器操作起来更加的简便。 在定时器中断中,首先再次读取产生中断的端口,看按键是否依然按下。如果没有按键按下,则说明当前是抖动,此时恢复各个端口及中断的状态到待命状态,然后退出。当有按键按下时,将数据反转输出,然后计算按键键值编码并设置按键更改标志。 在这里因为是4×4键盘,每组读取的数据有四位有效位,所以直接将两组数据拼接起来即可完成按键编码。 这就结束了吗?当然没有了,这只是一个最基本的,偷工减料的键盘扫描程序。 普通的矩阵键盘是不支持多个按键同时按下的,普通的逐行扫描的程序也可以检测到按键按下的数量,从而避免无效的键值出现。但是这个程序不具备这项功能。 为了检测无效键值,可以在每次读取时采用移位的方法对每次读取的值进行计数,如果0的个数超过一个则放弃本次结果。但这不是我喜欢的风格,我不喜欢循环好多次之为了检测一个0,所以就有了后面的文章。 我不喜欢在运算上花费时间,但是对于内存的消耗还是可以忍受的,在FPGA设计中常常会碰到两个折中的原则,一是用面积换取速度,另一个则是用速度(牺牲速度)换取面积(即资源数量)。在单片机中也同样有类似的问题,现在就到了用面积换取速度的时候了。 最简单的方法,就是定义一个数组,在下标号为有效键值的存储单元中,定义为我们想要的键值,其他区域,可以定义为一个无效键值,程序如下:
-
void IntSysTick(void) { SysTickDisable(); ReadData=GPIOPinRead(GPIO_PORTB_BASE,0x0f); if(ReadData!=0x0f); { GPIODirModeSet(GPIO_PORTB_BASE,0xf0,GPIO_DIR_MODE_IN); GPIODirModeSet(GPIO_PORTB_BASE,0x0f,GPIO_DIR_MODE_OUT); GPIOPinWrite(GPIO_PORTB_BASE,0x0f,ReadData); ReadData=(ReadData & 0x0f)|(GPIOPinRead(GPIO_PORTB_BASE,0xf0) & 0xf0); ReadData=Key_Num_Tabel[ReadData]; if(ReadData!=Err_Num) { KeyCoder=ReadData; Key_Change=1; } } GPIODirModeSet(GPIO_PORTB_BASE,0x0f,GPIO_DIR_MODE_IN); GPIODirModeSet(GPIO_PORTB_BASE,0xf0,GPIO_DIR_MODE_OUT); GPIOPinWrite(GPIO_PORTB_BASE,0xf0,0x0f); IntEnable(INT_GPIOB); }
复制代码
这样写的好处是,按键的值可以任意定义,不存在运算公式的束缚。比如我曾经在公司想采用AVR单片机代替掉ZLG的7290芯片,这时候这样定义就有好处了,可以完全模仿7290芯片输出的值,而且完全不用动主处理器(ARM)的程序。再者,有时候一个键值的定义往往也会对程序的执行效率起到很大的作用。比如说我们要根据按键的值来选择执行的程序,这时候最好的键值是什么?当然是其功能程序的入口地址了,这样定义后,就完全不用再进行按键判断了。 不过有些朋友可能不买这一套,你自己写的程序占用内存多你就说占内存多好?典型的忽悠人嘛。我就不要那么多好处,内存还我! 呵呵好吧,虽然用面积换取速度有时候是必然的,但是内存用量这个问题则是我们要考虑是否采取这种兑换方式的首要问题,我们要考虑考虑这桩买卖是否划算。不过还好,这个问题也不是完全的铁公鸡,一毛不拔。在内存用量上其实还是有一些讨价还价的余地的。 前面这种方法要占用256字节以上的内存,不过这个按键的初始编码本来就是由两个编码组合起来的,最终编码也可以采用组合的方式来实现嘛。这样,定义一个数组,只要其长度大于半个字节的计数范围即可,典型的是16个字节,然后高低字节分别用初始编码的查出最终编码后再组合在一起即可。这样数组占用内存一下子从256字节减小到了16字节,我想大多数“客户”的脸上应该浮出笑意了吧。 其具体方法是:该键盘为四行四列,无论是行号还是列号都可以用两位来表示。在最终键值中用00、01、02和03分别表示按下的行号和列号。这也是和逐行扫描得出的紧密型键值一样了,对于无效键值,则统一赋成0xFF。程序如下:
-
void IntSysTick(void) { SysTickDisable(); ReadData=GPIOPinRead(GPIO_PORTB_BASE,0x0f); if(ReadData!=0x0f); { GPIODirModeSet(GPIO_PORTB_BASE,0xf0,GPIO_DIR_MODE_IN); GPIODirModeSet(GPIO_PORTB_BASE,0x0f,GPIO_DIR_MODE_OUT); GPIOPinWrite(GPIO_PORTB_BASE,0x0f,ReadData); //ReadData=(ReadData & 0x0f)|(GPIOPinRead(GPIO_PORTB_BASE,0xf0) & 0xf0); ReadData=Key_Num_Tab[ReadData & 0x0f]; ReadData=(ReadData & 0xfc)|(Key_Num_Tab[(GPIOPinRead(GPIO_PORTB_BASE,0xf0)>>4)]<<2); if(ReadData<=0x0f) { Key_Read=ReadData; Key_Change=1; } } GPIODirModeSet(GPIO_PORTB_BASE,0x0f,GPIO_DIR_MODE_IN); GPIODirModeSet(GPIO_PORTB_BASE,0xf0,GPIO_DIR_MODE_OUT); GPIOPinWrite(GPIO_PORTB_BASE,0xf0,0x0f); IntEnable(INT_GPIOB); }
复制代码
两次查表计算中,第一次最终键值中没有需要保护的单元,所以采用直接赋值。而第二次只需要保护最低2位即可,高位虽然不用,但是要保护好每次写入高位的1,以避免清除掉错误标志,所以高位从不与0进行与运算。 好了,现在开始对这个程序感兴趣了吧?在推销中有一个很重要的方法就是趁热打铁。那我现在就要在这块铁上再砸一锤!将数组大小减小到8字节! 这可能吗? 当然可能,横列扫描读取的初始码中只有一位是与其他三位不同的,如果这一位取的是低电平,那似乎看不出什么,但是如果这一位变成了高电平,那就有意思了。一个四位二进制串中,只有一位是1,这个数字只能是1、2、4、8。将其减一,其范围正好在0—7中,此时定义一个8字节的数组就已经足够存储按键编码了。只是不同的是,还需要对大于8的初始值编码进行一次筛选。程序代码如下(GPIO中断代码不变,就不给出了,只是输入GPIO设置中要使能下拉电阻,采用上升沿中断):
-
void IntSysTick(void) { SysTickDisable(); ReadData=GPIOPinRead(GPIO_PORTB_BASE,0x0f); if(ReadData!=0x00); { GPIODirModeSet(GPIO_PORTB_BASE,0xf0,GPIO_DIR_MODE_IN); GPIODirModeSet(GPIO_PORTB_BASE,0x0f,GPIO_DIR_MODE_OUT); GPIOPinWrite(GPIO_PORTB_BASE,0x0f,ReadData); //ReadData=(ReadData & 0x0f)|(GPIOPinRead(GPIO_PORTB_BASE,0xf0) & 0xf0); if((ReadData<=8)&&(GPIOPinRead(GPIO_PORTB_BASE,0xf0)<=0x80)) { ReadData=Key_Num_Tab[ReadData - 1]; ReadData=(ReadData & 0xfc)|(Key_Num_Tab[(GPIOPinRead(GPIO_PORTB_BASE,0xf0)>>4)-1]<<2); if(ReadData<=0x0f) { Key_Read=ReadData; Key_Change=1; } } } GPIODirModeSet(GPIO_PORTB_BASE,0x0f,GPIO_DIR_MODE_IN); GPIODirModeSet(GPIO_PORTB_BASE,0xf0,GPIO_DIR_MODE_OUT); GPIOPinWrite(GPIO_PORTB_BASE,0xf0,0xf0); IntEnable(INT_GPIOB); }
复制代码
好了,铁打完了,我也累了,休息一下~~
|
|