第8章 函数进阶与按键8.4

张开发
2026/4/9 20:53:25 15 分钟阅读

分享文章

第8章 函数进阶与按键8.4
8.4按键8.4.1独立按键常用的按键电路有两种形式独立式按键和矩阵式按键独立式按键比较简单它们各自与独立的输入线相连接如图8-6所示。图8-6 独立式按键原理图4条输入线接到单片机的I/O口当按键K1按下时5V通过电阻R1再通过按键K1最终进入GND形成一条通路这条线路的电压都加到R1这个电阻上KeyIn1这个引脚就是个低电平。当松开按键线路断开就不会有电流通过KeyIn1和5V就应该是等电位是一个高电平。就可以通过KeyIn1这个I/O口的高低电平来判断是否有按键按下。实际上在单片机I/O口内部也有一个上拉电阻的存在。Kingst51开发板的按键接到P2口上P2口上电默认是准双向I/O口来了解一下这个准双向I/O口的电路如图8-7所示。图8-7 准双向I/O口结构图首先说明一点现在绝大多数单片机的I/O口都是使用MOS管而非三极管但用在这里的MOS管其原理和三极管是一样的因此在这里用三极管来进行原理讲解把前面讲过的三极管的知识搬过来一切都是适用的有助于理解。图8-7方框内的电路都是指单片机内部部分方框外的就是外接的上拉电阻和按键。这个地方要注意一下就是当单片机要读取外部按键信号的时候必须先给该引脚写“1”也就是高电平这样才能正确读取到外部按键信号。当“内部输出”是高电平经过一个反向器变成低电平NPN三极管不会导通那么单片机I/O口从内部来看由于上拉电阻R的存在所以是一个高电平。当外部没有按键按下将电平拉低的话VCC也是5V它们之间虽然有2个电阻但是没有压差就不会有电流线上所有的位置都是高电平这个时候就可以正常读取到按键的状态了。当“内部输出”是个低电平经过一个反相器变成高电平NPN三极管导通那么单片机的内部I/O口就是个低电平这个时候外部虽然也有上拉电阻的存在但是两个电阻是并联关系不管按键是否按下单片机的I/O口上输入到单片机内部的状态都是低电平就无法正常读取到按键的状态了。和水流类似内部和外部只要有一个点是低电位电流就会顺流而下由于只有上拉电阻没有下拉电阻分压直接到GND上线路上就是低电平了。可以得出一个结论这种具有上拉的准双向I/O口如果要正常读取外部信号的状态得保证内部输出1如果内部输出0无论外部信号是1还是0这个引脚读进来的都是0。8.4.2矩阵按键在某一个系统设计中如果需要使用很多的按键时做成独立按键会大量占用I/O口因此引入了矩阵按键的设计。如图8-8所示是Kingst51开发板上的矩阵按键电路原理图使用8个I/O口来实现了16个按键。图8-8 矩阵按键原理图独立按键理解了矩阵按键也不难理解。图8-8中一共有4组按键如果只看其中一组如图8-9所示。KeyOut1输出一个低电平KeyOut1就相当于是GND是否相当于4个独立按键呢。当然这时候KeyOut2、KeyOut3、KeyOut4都必须输出高电平才能保证与它们相连的三路按键不会对这一路产生干扰可以对照两张原理图分析一下。图8-9 矩阵按键变独立按键示意图8.4.3独立按键的扫描原理搞清楚了下面就先编写一个独立按键的程序把最基本的功能验证一下。#include reg52.hsbit ADDR0 P1^0;sbit ADDR1 P1^1;sbit ADDR2 P1^2;sbit ADDR3 P1^3;sbit ENLED P1^4;sbit LED9 P0^7;sbit LED8 P0^6;sbit LED7 P0^5;sbit LED6 P0^4;sbit KEY1 P2^4;sbit KEY2 P2^5;sbit KEY3 P2^6;sbit KEY4 P2^7;void main(){ENLED 0; //选择独立LED进行显示ADDR3 1;ADDR2 1;ADDR1 1;ADDR0 0;P2 0xF7; //P2.3置0即KeyOut1输出低电平while (1){//将按键扫描引脚的值传递到LED上LED9 KEY1; //按下时为0对应的LED点亮LED8 KEY2;LED7 KEY3;LED6 KEY4;}}本程序在KeyOut1上输出低电平而KeyOut24保持高电平就相当于是把矩阵按键的第一行即K1K4作为4个独立按键来处理然后把这4个按键的状态直接送给LED96这4个LED小灯。当按键按下时对应按键的输入引脚是0对应小灯控制信号是低电平于是灯就亮了这说明上述关于按键检测的理论都是可实现的。绝大多数情况下按键是不会一直按住的所以通常检测按键的动作并不是检测一个固定的电平值而是检测电平值的变化即按键在按下和弹起这两种状态之间的变化只要发生了这种变化就说明现在按键产生动作了。如何判断按键被按下事件发生假设代表按键的IO口的状态xx为0代表按下x为1代表弹起。某一t时刻读取一次按键IO口的状态x为1而t1时刻再次读取一次按键IO口状态x为0通过x的变化得知按键状态发生了变化即按键被按下的事件发生。同理如果t时刻读到的x为0而t1时刻读取到IO口状态x为1则按键弹起的事件发生。每次按键动作都会包含一次“按下”和一次“弹起”可以任选其一来执行程序或者两个都用以执行不同的功能程序。把每次t时刻扫描到的按键状态都保存起来当t1时刻按键状态扫描进来的时候与前一次t时刻扫描的状态进行比较如果这两次按键状态不一致就说明按键产生动作了。下面用程序实现这个功能程序只取按键K4为例。#include reg52.hsbit ADDR0 P1^0;sbit ADDR1 P1^1;sbit ADDR2 P1^2;sbit ADDR3 P1^3;sbit ENLED P1^4;sbit KEY1 P2^4;sbit KEY2 P2^5;sbit KEY3 P2^6;sbit KEY4 P2^7;unsigned char code LedChar[] { //数码管显示字符转换表0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E};void main(){bit backup 1; //定义位变量保存t时刻扫描的按键值默认按键状态为弹起unsigned char cnt 0; //定义一个计数变量记录按键按下的次数ENLED 0; //选择数码管DS1进行显示ADDR3 1;ADDR2 0;ADDR1 0;ADDR0 0;P2 0xF7; //P2.3置0即KeyOut1输出低电平P0 LedChar[cnt]; //显示按键次数初值while (1){if (KEY4 ! backup) //t1时刻KEY4与t时刻不相等说明此时按键有动作{if (backup 0) //如果t时刻值为0则说明当前是由0变1即按键弹起{cnt; //按键次数1if (cnt 10){ //只用1个数码管显示所以加到10就清零重新开始cnt 0;}P0 LedChar[cnt]; //计数值显示到数码管上}backup KEY4; //更新备份按键状态以备进行下次比较}}}先来介绍出现在程序中的一个新知识点变量类型——bit这个在标准C语言是没有的。51单片机有一种特殊的变量类型就是bit型。unsigned char型是定义了一个无符号的8位的数据它占用一个字节(Byte)的内存而bit型是1位数据只占用1个位(bit)的内存用法和标准C中其他的基本数据类型是一致的。它的优点就是节省内存空间8个bit型变量才相当于1个char型变量所占用的空间。虽然它只有0和1两个值但也已经可以表示很多东西了比如按键的按下和弹起、LED灯的亮和灭、三极管的导通与关断等等。在这个程序中以K4为例按一次按键就会产生“按下”和“弹起”两个动态的动作程序选择在“弹起”时对数码管进行加1操作。理论虽是如此但是经过多次实验是否发现了这样一种现象有的时候明明只按了一下按键但数字却加了不止1而是2或者更多但是程序并没有逻辑上的错误这是怎么回事呢这是一个按键抖动和消抖的问题。8.4.4按键消抖通常按键所用的开关都是机械弹性开关当机械触点断开、闭合时由于机械触点的弹性作用一个按键在闭合时不会马上就稳定的接通在断开时也不会一下子彻底断开而是在闭合和断开的瞬间伴随了一连串的抖动如图8-10所示。图8-10 按键抖动状态图按键稳定闭合时间长短是由操作人员决定的通常都会在100ms以上刻意快速按的话能达到40-50ms左右很难再低了。抖动时间是由按键的机械特性决定的一般都会在10ms以内为了确保程序对按键的一次闭合或者一次断开只响应一次必须进行按键的消抖处理。当检测到按键状态变化时不是立即去响应动作而是先等待闭合或断开稳定后再进行处理。最简单的消抖就是当检测到按键状态变化后先等待一个10ms左右的延时时间让抖动消失后再进行一次按键状态检测如果与刚才检测到的状态相同就可以确认按键已经稳定的动作了。将上一个的程序稍加改动得到新的带消抖功能的程序如下。#include reg52.hsbit ADDR0 P1^0;sbit ADDR1 P1^1;sbit ADDR2 P1^2;sbit ADDR3 P1^3;sbit ENLED P1^4;sbit KEY1 P2^4;sbit KEY2 P2^5;sbit KEY3 P2^6;sbit KEY4 P2^7;unsigned char code LedChar[] { //数码管显示字符转换表0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E};void delay();void main(){bit keybuf 1; //按键值暂存临时保存按键的扫描值bit backup 1; //按键值备份保存前一次的扫描值unsigned char cnt 0; //按键计数记录按键按下的次数ENLED 0; //选择数码管DS1进行显示ADDR3 1;ADDR2 0;ADDR1 0;ADDR0 0;P2 0xF7; //P2.3置0即KeyOut1输出低电平P0 LedChar[cnt]; //显示按键次数初值while (1){keybuf KEY4; //把当前扫描值暂存if (keybuf ! backup) //当前值与前次值不相等说明此时按键有动作{delay(); //延时大约10msif (keybuf KEY4) //判断扫描值有没有发生改变即按键抖动{if (backup 0) //如果前次值为0则说明当前是弹起动作{cnt; //按键次数1if (cnt 10){ //只用1个数码管显示所以加到10就清零重新开始cnt 0;}P0 LedChar[cnt]; //计数值显示到数码管上}backup keybuf; //更新备份为当前值以备进行下次比较}}}}/* 软件延时函数延时约10ms */void delay(){unsigned int i 1000;while (i--);}作为消抖的功能演示程序可以采用延时的办法但是实际做开发的时候程序量往往很大各种状态值也很多while(1)这个主循环要不停的扫描各种状态值是否有发生变化及时的进行任务调度如果程序中间加了这种delay延时操作后很可能某一事件发生了但是程序还在进行delay延时操作中当这个事件发生完了程序还在delay操作中当delay完事再去检查的时候已经检测不到那个事件了。为了避免这种情况的发生要尽量缩短while(1)循环一次所用的时间而需要进行长时间延时的操作必须想其它的办法来处理。那么消抖该采用什么办法呢介绍一种作者在实际工程中常常采用的一种办法启用一个定时中断每2ms进一次中断扫描一次按键状态并且存储起来连续扫描8次后看看这连续8次的按键状态是否是一致的。8次按键的时间大概是16ms这16ms内如果按键状态一直保持一致那就可以确定现在按键处于稳定的阶段而非处于抖动的阶段如图8-12。图8-12 按键连续扫描判断假如8-12图中t时刻检测到了某一个按键[1,2,3,4,5,6,7,8]这8个状态t1t2ms时刻检测[2,3,4,5,6,7,8,9]这8个状态t2t4ms时刻检测[3,4,5,6,7,8,9,10]这8个状态... ...随着时间的推移检测按键也不断更新连续的8次按键状态。按键状态分为弹起、抖动和按下三种状态当程序检测到连续8次按键状态全为1时则代表按键状态为弹起当程序检测到连续8次按键状态全为0时则代表按键状态为按下当检测连续8次按键状态是0和1交错则代表按键为抖动。利用这种方法就可以避免通过延时消抖占用单片机执行时间而是转化成了一种按键状态判定而非按键过程判定只对当前按键的连续16ms的8次状态进行判断而不再关心它在这16ms内都做了什么下面就按照这种思路用程序实现出来同样只以K4为例。#include reg52.hsbit ADDR0 P1^0;sbit ADDR1 P1^1;sbit ADDR2 P1^2;sbit ADDR3 P1^3;sbit ENLED P1^4;sbit KEY1 P2^4;sbit KEY2 P2^5;sbit KEY3 P2^6;sbit KEY4 P2^7;unsigned char code LedChar[] { //数码管显示字符转换表0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E};bit KeySta 1; //当前按键状态初始默认为弹起void main(){bit backup 1; //按键值备份保存前一次的扫描值默认弹起unsigned char cnt 0; //按键计数记录按键按下的次数EA 1; //使能总中断ENLED 0; //选择数码管DS1进行显示ADDR3 1;ADDR2 0;ADDR1 0;ADDR0 0;TMOD 0x01; //设置T0为模式1TH0 0xF8; //为T0赋初值0xF8CD定时2msTL0 0xCD;ET0 1; //使能T0中断TR0 1; //启动T0P2 0xF7; //P2.3置0即KeyOut1输出低电平P0 LedChar[cnt]; //显示按键次数初值while (1){if (KeySta ! backup) //当前值与前次值不相等说明此时按键有动作{if (backup 0) //如果前次值为0则说明当前是弹起动作{cnt; //按键次数1if (cnt 10){ //只用1个数码管显示所以加到10就清零重新开始cnt 0;}P0 LedChar[cnt]; //计数值显示到数码管上}backup KeySta; //更新备份为当前值以备进行下次比较}}}/* T0中断服务函数用于按键状态的扫描并消抖 */void InterruptTimer0() interrupt 1{static unsigned char keybuf 0xFF;//扫描缓冲区保存一段时间内的扫描值默认按键弹起为全‘1’TH0 0xF8; //重新加载初值定时2msTL0 0xCD;keybuf (keybuf1) | KEY4; //缓冲区左移一位将当前按键状态更新至最低位if (keybuf 0x00){ //连续8次扫描值都为0即16ms内都只检测到按下状态时可认为按键已按下KeySta 0;}else if (keybuf 0xFF){ //连续8次扫描值都为1即16ms内都只检测到弹起状态时可认为按键已弹起KeySta 1;}else{} //其它情况则说明按键状态尚未稳定则不对KeySta变量值进行更新}8.4.5矩阵按键的扫描介绍独立按键扫描的时候已经简单认识了矩阵按键是什么样子了。矩阵按键相当于4组每组各4个独立按键一共是16个按键。那如何区分这些按键呢前边讲过按键按下通常都会保持100ms以上。如果在按键扫描中断程序中每次让矩阵按键的一个KeyOut输出低电平其它三个输出高电平判断当前4个KeyIn的状态下次中断时再让下一个KeyOut输出低电平其它三个输出高电平再次判断所有KeyIn通过快速的中断不停的循环进行判断就可以最终确定哪个按键按下了扫描原理是不是跟数码管动态扫描有点类似数码管在动态赋值而按键这里在动态读取状态。至于扫描间隔时间和消抖时间由于现在有4个KeyOut输出要中断4次才能完成一次全部按键的扫描显然再采用2ms中断判断8次扫描值的方式时间就太长了2*4*864ms那么就改用1ms中断判断4次采样值这样消抖时间还是16ms(1*4*4)。下面就用程序实现程序循环扫描板子上的K1K16这16个矩阵按键在按键按下时把当前按键的编号显示在一位数码管上用0~F表示显示值按键编号-1。#include reg52.hsbit ADDR0 P1^0;sbit ADDR1 P1^1;sbit ADDR2 P1^2;sbit ADDR3 P1^3;sbit ENLED P1^4;sbit KEY_IN_1 P2^4;sbit KEY_IN_2 P2^5;sbit KEY_IN_3 P2^6;sbit KEY_IN_4 P2^7;sbit KEY_OUT_1 P2^3;sbit KEY_OUT_2 P2^2;sbit KEY_OUT_3 P2^1;sbit KEY_OUT_4 P2^0;unsigned char code LedChar[] { //数码管显示字符转换表0xC0, 0xF9, 0xA4, 0xB0, 0x99, 0x92, 0x82, 0xF8,0x80, 0x90, 0x88, 0x83, 0xC6, 0xA1, 0x86, 0x8E};unsigned char KeySta[4][4] { //全部矩阵按键的当前状态1代表弹起0表示按下{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}};void main(){unsigned char i, j;unsigned char backup[4][4] { //按键值备份保存前一次的值{1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}, {1, 1, 1, 1}};EA 1; //使能总中断ENLED 0; //选择数码管DS1进行显示ADDR3 1;ADDR2 0;ADDR1 0;ADDR0 0;TMOD 0x01; //设置T0为模式1TH0 0xFC; //为T0赋初值0xFC67定时1msTL0 0x67;ET0 1; //使能T0中断TR0 1; //启动T0P0 LedChar[0]; //默认显示0while (1){for (i0; i4; i) //循环检测4*4的矩阵按键比较16个按键状态是否发生变化{for (j0; j4; j){if (backup[i][j] ! KeySta[i][j]) //检测按键动作{if (backup[i][j] ! 0) //按键按下时执行动作{P0 LedChar[i*4j]; //将编号显示到数码管}backup[i][j] KeySta[i][j]; //更新前一次的备份值}}}}}/* T0中断服务函数扫描矩阵按键状态并消抖 */void InterruptTimer0() interrupt 1{unsigned char i 0;static unsigned char keyout 0; //矩阵按键扫描输出索引static unsigned char keybuf[4][4] { //矩阵按键扫描缓冲区{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF},{0xFF, 0xFF, 0xFF, 0xFF}, {0xFF, 0xFF, 0xFF, 0xFF}};TH0 0xFC; //重新加载初值TL0 0x67;//将keyout x(x为1,2,3,4中其中一个)时对应一行的4个按键值移入缓冲区keybuf[keyout][0] (keybuf[keyout][0] 1) | KEY_IN_1;keybuf[keyout][1] (keybuf[keyout][1] 1) | KEY_IN_2;keybuf[keyout][2] (keybuf[keyout][2] 1) | KEY_IN_3;keybuf[keyout][3] (keybuf[keyout][3] 1) | KEY_IN_4;//消抖后更新按键状态for (i0; i4; i) //每行4个按键所以循环4次{if ((keybuf[keyout][i] 0x0F) 0x00){ //连续4次扫描值为0即4*4ms内都是按下状态时可认为按键已稳定的按下KeySta[keyout][i] 0;}else if ((keybuf[keyout][i] 0x0F) 0x0F){ //连续4次扫描值为1即4*4ms内都是弹起状态时可认为按键已稳定的弹起KeySta[keyout][i] 1;}}//执行下一次的扫描输出keyout; //输出索引递增keyout keyout 0x03; //索引值加到4即归零switch (keyout) //根据索引释放当前输出引脚拉低下次的输出引脚{case 0: KEY_OUT_4 1; KEY_OUT_1 0; break;case 1: KEY_OUT_1 1; KEY_OUT_2 0; break;case 2: KEY_OUT_2 1; KEY_OUT_3 0; break;case 3: KEY_OUT_3 1; KEY_OUT_4 0; break;default: break;}}这个程序完成了矩阵按键的扫描、消抖、动作分离的全部内容还有两点值得说明。首先中断函数中扫描KeyIn输入和切换KeyOut输出的顺序与前面提到的不同程序中先对所有的KeyIn输入做了扫描、消抖然后才切换到了下一次的KeyOut输出也就是说中断每次扫描的实际是上一次KeyOut输出选择的那行按键这是为什么呢任何信号从输出到稳定都需要时间有时它足够快而有时却不够快这取决于具体的电路设计如果keyout为0时立即读取与其对应的四个按键的KeyIn值虽然程序上已经让keyout为0了但是电路反应并没有那么快电路中对应的keyout引脚还没有完全拉成低电平的话读取到的值就有可能出现错误。这里的输入输出顺序的颠倒就是为了让输出信号有足够的时间一次中断间隔来稳定并有足够的时间来完成它对输入的影响虽然这样使得程序理解起来有点绕但它的适应性是最好的换个说法就是这段程序足够“健壮”足以应对各种恶劣情况。其次是一点小小的编程技巧。注意看keyout keyout 0x03;这一行这里是要让keyout在03之间变化加到4就自动归零按照常规可以用前面讲过的if语句轻松实现但是现在看一下这样程序是不是同样可以做到这一点呢因为0、1、2、3这四个数值正好占用两个二进制的位所以把一个字节的高6位一直清零的话这个字节的值自然就是一种到4归零的效果了。看一下这样一句代码比if语句要更为简洁吧而效果完全一样。

更多文章