搜索
bottom↓
回复: 49

<手把手教你学51单片机 - C语言版> 文字教程 + 视频教程 下载

  [复制链接]

出0入0汤圆

发表于 2014-5-26 16:33:22 | 显示全部楼层 |阅读模式
本帖最后由 51kingst 于 2014-5-26 16:43 编辑

        经过去年一整年的撰写、排版、校对工作,《手把手教你学51单片机-C语言版》正式由清华大学出版社出版。在签合同之前,我已经跟出版社提出我要开放电子版的想法,经由出版社同意,电子版是完全开放的。(之前也和几个出版社谈过,出版社都要求不开源电子版,要么部分开源,最终清华大学出版社同意开源电子版并且合作出版此书)。我把PDF的电子版本上传上来,大家可以了解学习一下,此外书籍完全配套的20课视频教程外加一课altium designer的画图教程,下载链接都提供在里边。
                           
       出版社不建议开源电子版无非是担心读者有了电子版就不购买纸质书籍了,我想这个担心完全没必要。电子版提供更容易让大家通过阅读电子版来了解书写的怎么样,到底有没有真材实料,更有利于知识的传播。通过电子版如果觉得书写的还可以,考虑用来做教材的教师可以跟出版社联系索取样书,并且可以通过出版社获取免费教学实验板,助力中国教育,联系方式都在电子书籍首页。
       当前的单片机教程来说,大多很简单的入门,而很多同学反馈入门点小灯容易,深入成为工程师很难,很多同学学了51单片机后,感觉和实际开发的距离还是非常遥远。基于这些反馈信息,做教程的时候更加注重深入实际开发技术和技巧。尤其注重把51单片机当“单片机”来讲解,而不仅仅当“51”来教,尤其是C语言的基础,指针,结构体这些实际项目所常用的内容,全部做了详细深入讲解。目标是学会了这个51单片机,再做任何一款8位单片机,通过一个周熟悉编程软件和新寄存器就能够用起来。任何一款32位的单片机,只要不做嵌入操作系统,只要熟悉手册和开发平台一个月就可以上手。书中重点介绍电路,编程,尤其是对于C语言的深入讲解,是几乎之前单片机教程所没有的。对于大学生来说,目标就是学会后,进入公司能够在其他工程师带领下参与项目开发,通过几个项目磨练一下,差不多就可以独立开发了。
      视频教程目录:
      书籍的目录截图:


      写书之前,我专门买了很多同类书籍专门看了一下,大多还是仅限于入门级别。所以这本书除了注重入门之外,还注重深度的介绍,尽可能的把平时项目开发的小技巧算法什么的提供出来,尽可能的让书中的内容和实际项目开发贴近起来。这一点大家可以通过书籍的目录看出来。我把其中按键章节摘出来,大家看下独立按键和矩阵按键的用法,以及实际开发消抖的方法。一般实际开发不会使用delay来消抖的,用定时器的方法最为普遍。看过其他单片机书籍的,可以看看按键我怎么处理的,看看方法是不是巧妙。如果觉得还行,可以把整体下载下来看看,看完对比后可评论评论。

8.4按键
〖*2〗8.4.1独立按键
      常用的按键电路有两种形式: 独立式按键和矩阵式按键。独立式按键比较简单,它们各自与独立的输入线相连接,如图86所示。      
      4条输入线接到单片机的IO口上,当按键K1按下时,+5V通过电阻R1然后再通过按键K1最终进入GND形成一条通路,这条线路的全部电压都加到了R1这个电阻上,KeyIn1这个引脚就是个低电平。当松开按键后,线路断开,就不会有电流通过,KeyIn1和+5V就应该是等电位,是一个高电平。我们就可以通过KeyIn1这个IO口的高低电平来判断是否有按键按下。
      这个电路中按键的原理讲清楚了,但是实际上在单片机IO口内部,也有一个上拉电阻的存在。按键是接到了P2口上,P2口上电默认是准双向IO口,下面来简单了解一下准双向IO口的电路,如图8-7所示。
     首先说明一点,就是现在绝大多数单片机的IO口都是使用MOS管而非三极管,但用在这里的MOS管其原理和三极管是一样的,因此用三极管替代它来进行原理讲解,把前面讲过的三极管的知识搬过来,一切都是适用的,有助于理解。
图8-7方框内的电路都是指单片机内部部分,方框外的就是外接的上拉电阻和按键。这个地方大家要注意一下,就是当要读取外部按键信号的时候,单片机必须先给该引脚写“1”,也就是高电平,这样才能正确读取到外部按键信号,下面来分析一下缘由。
     当内部输出是高电平,经过一个反向器变成低电平,NPN三极管不会导通,单片机IO口从内部来看,由于上拉电阻R的存在,所以是一个高电平。当外部没有按键按下将电平拉低的话,VCC也是+5V,它们之间虽然有两个电阻,但是没有压差,就不会有电流,线上所有的位置都是高电平,这个时候就可以正常读取到按键的状态了。
     当内部输出是个低电平,经过一个反相器变成高电平,NPN三极管导通,单片机的内部IO口就是个低电平,这个时候,外部虽然也有上拉电阻的存在,但是两个电阻是并联关系,不管按键是否按下,单片机的IO口上输入到单片机内部的状态都是低电平,就无法正常读取到按键的状态了。
     这与水流其实很类似的,内部和外部,只要有一边是低电位,电流就会顺流而下,由于只有上拉电阻,下边没有电阻分压,直接到GND上了,所以不管另外一边是高还是低,那电平肯定就是低电平了。
     从上面的分析可以得出一个结论,这种具有上拉的准双向IO口,如果要正常读取外部信号的状态,必须首先得保证自己内部输出的是1,如果内部输出0,则无论外部信号是1还是0,这个引脚读进来的都是0。

8.4.2矩阵按键
    在某一个系统设计中,如果需要使用很多的按键时,做成独立按键会大量占用IO口,因此引入了矩阵按键的设计。如图8-8所示是KST-51开发板上的矩阵按键电路原理图,使用8个IO口来实现了16个按键。
    如果独立按键理解了,矩阵按键也不难理解,下面一起来分析一下。图88中,一共有4组按键,我们只看其中一组,如图89所示。大家认真看一下,如果KeyOut1输出一个低电平,KeyOut1就相当于是GND,是否相当于4个独立按键呢。当然这时候KeyOut2、KeyOut3、KeyOut4都必须输出高电平,它们都输出高电平才能保证与它们相连的3路按键不会对这一路产生干扰,大家可以对照两张原理图分析一下。
    8.4.3独立按键的扫描
原理搞清楚了,那么下面就先编写一个独立按键的程序,把最基本的功能验证一下。
#include reg52.h

sbit 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上输出低电平,而KeyOut2~KeyOut4保持高电平,就相当于是把矩阵按键的第一行,即K1~K4作为4个独立按键来处理,然后把这4个按键的状态直接送给LED9~LED6这4个LED小灯,那么当按键按下时,对应按键的输入引脚是0,对应小灯控制信号也是0,于是灯就亮了,这说明上述关于按键检测的理论都是可实现的。
绝大多数情况下,按键是不会一直按住的,所以通常检测按键的动作并不是检测一个固定的电平值,而是检测电平值的变化,即按键在按下和弹起这两种状态之间的变化,只要发生了这种变化就说明现在按键产生动作了。
程序上,可以把每次扫描到的按键状态都保存起来,当一次按键状态扫描进来的时候,与前一次的状态做比较,如果发现这两次按键状态不一致,就说明按键产生动作了。当上一次的状态是未按下而现在是按下,此时按键的动作就是“按下”; 当上一次的状态是按下而现在是未按下,此时按键的动作就是“弹起”。显然,每次按键动作都会包含一次“按下”和一次“弹起”,可以任选其一来执行程序,或者两个都用,以执行不同的程序也是可以的。下面就用程序来实现这个功能,程序只取按键K4为例。
#include reg52.h
sbit 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;//定义一个位变量,保存前一次扫描的按键值
        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) //当前值与前次值不相等说明此时按键有动作
                {
                        if (backup == 0)//如果前次值为0,则说明当前是由0变1,即按键弹起
                        {
                                cnt++;//按键次数+1
                                if (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操作。理论是如此,大家可以在开发板上用K4按键做做实验试试,多按几次,是不是会发生这样一种现象: 有的时候明明只按了一下按键,但数字却加了不止1,而是2或者更多?但是程序并没有任何逻辑上的错误,这是怎么回事呢?于是就得来说说按键抖动和消抖的问题了。
8.4.4按键消抖
通常按键所用的开关都是机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上就稳定接通,在断开时也不会一下子彻底断开,而是在闭合和断开的瞬间伴随了一连串的抖动,如图810所示。
按键稳定闭合时间长短是由操作人员决定的,通常都会在100ms以上,刻意快速按的话能达到40~50ms左右,很难再低了。抖动时间是由按键的机械特性决定的,一般都会在10ms以内,为了确保程序对按键的一次闭合或者一次断开只响应一次,必须进行按键的消抖处理。当检测到按键状态变化时,不是立即去响应动作,而是先等待闭合或断开稳定后再进行处理。按键消抖可分为硬件消抖和软件消抖。
硬件消抖就是在按键上并联一个电容,如图811所示,利用电容的充放电特性来对抖动过程中产生的电压毛刺进行平滑处理,从而实现消抖。但实际应用中,这种方式的效果往往不是很好,而且还增加了成本和电路复杂度,所以实际中使用的并不多。
在绝大多数情况下是用软件即程序来实现消抖的。最简单的消抖原理,就是当检测到按键状态变化后,先等待一个10ms左右的延时时间,让抖动消失后再进行一次按键状态检测,如果与刚才检测到的状态相同,就可以确认按键已经稳定动作了。将上一个的程序稍加改动,得到新的带消抖功能的程序如下。
#include reg52.h

sbit 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();//延时大约10ms
                        if (keybuf == KEY4) //判断扫描值有没有发生改变,即按键抖动
                        {
                                if (backup == 0)//如果前次值为0,则说明当前是弹起动作
                                {
                                        cnt++;//按键次数+1
                                        if (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所示。
    假如左边时间是起始0时刻,每经过2ms左移一次,每移动一次,判断当前连续的8次按键状态是不是全1或者全0,如果是全1则判定为弹起,如果是全0则判定为按下,如果0和1交错,就认为是抖动,不做任何判定。想一下,这样是不是比简单的延时更加可靠?
利用这种方法,就可以避免通过延时消抖占用单片机执行时间,而是转化成了一种按键状态判定而非按键过程判定,我们只对当前按键的连续16ms的8次状态进行判断,而不再关心它在这16ms内都做了什么事情,下面就按照这种思路用程序实现出来,同样只以K4为例。
#include reg52.h
sbit 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为模式1
        TH0= 0xF8;//为T0赋初值0xF8CD,定时2ms
        TL0= 0xCD;
        ET0= 1;//使能T0中断
        TR0= 1;//启动T0
        P2 = 0xF7;//P2.3置0,即KeyOut1输出低电平
        P0 = LedChar[cnt]; //显示按键次数初值
        while (1)
        {
                if (KeySta != backup)//当前值与前次值不相等说明此时按键有动作
                {
                        if (backup == 0) //如果前次值为0,则说明当前是弹起动作
                        {
                                cnt++; //按键次数+1
                                if (cnt = 10)
                                {//只用1个数码管显示,所以加到10就清零,重新开始
                                        cnt = 0;
                                }
                                P0 = LedChar[cnt];//计数值显示到数码管上
                        }
                        backup = KeySta; //更新备份为当前值,以备进行下次比较
                }
        }
}

/* T0中断服务函数,用于按键状态的扫描并消抖 */
void InterruptTimer0() interrupt 1
{
        static unsigned char keybuf = 0xFF;//扫描缓冲区,保存一段时间内的扫描值
        TH0 = 0xF8;//重新加载初值
        TL0 = 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输出低电平,其他三个输出高电平,判断当前所有KeyIn的状态,下次中断时再让下一个KeyOut输出低电平,其他三个输出高电平,再次判断所有KeyIn,通过快速的中断不停地循环进行判断,就可以最终确定哪个按键按下了,这个原理是不是跟数码管动态扫描有点类似?数码管在动态赋值,而按键在动态读取状态。至于扫描间隔时间和消抖时间,因为现在有4个KeyOut输出,要中断4次才能完成一次全部按键的扫描,显然再采用2ms中断判断8次扫描值的方式时间就太长了(2×4×8=64ms),就改用1ms中断判断4次采样值,这样消抖时间还是16ms(1×4×4)。下面就用程序实现出来,程序循环扫描板子上的K1~K16这16个矩阵按键,分离出按键动作并在按键按下时把当前按键的编号显示在一位数码管上(用0~F表示,显示值=按键编号-1)。
#include reg52.h
sbit 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, 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为模式1
        TH0= 0xFC;//为T0赋初值0xFC67,定时1ms
        TL0= 0x67;
        ET0= 1; //使能T0中断
        TR0= 1; //启动T0
        P0 = LedChar[0];  //默认显示0

        while (1)
        {
                for (i=0; i4; i++)//循环检测4*4的矩阵按键
                {
                        for (j=0; j4; j++)
                        {
                                if (backup[i][j] != KeySta[i][j])//检测按键动作
                                {
                                        if (backup[i][j] != 0)//按键按下时执行动作
                                        {
                                                P0 = LedChar[i*4+j];//将编号显示到数码管
                                        }
                                        backup[i][j] = KeySta[i][j]; //更新前一次的备份值
                                }
                        }
                }
        }
}

/* T0中断服务函数,扫描矩阵按键状态并消抖 */

void InterruptTimer0() interrupt 1
{
        unsigned char i;
        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;
                                                                //将一行的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 (i=0; i<4; 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 & 0x03;这一行,在这里要让keyout在0~3之间变化,加到4就自动归零,按照常规可以用前面讲过的if语句轻松实现,但是现在看一下这样程序是不是同样可以做到这一点呢?因为0、1、2、3这四个数值正好占用两个二进制的位,所以把一个字节的高6位一直清零的话,这个字节的值自然就是一种到4归零的效果了。看一下,这样一句代码比if语句要更为简洁吧,而效果完全一样。
8.5简易加法计算器
学到这里,我们已经掌握了一种显示设备和一种输入设备的使用,那么是不是可以来做点综合性的实验了。好吧,下面就来做一个简易的加法计算器,用程序实现从板子上标有0~9数字的按键输入相应数字,该数字要实时显示到数码管上,用标有向上箭头的按键代替加号,按下加号后可以再输入一串数字,然后回车键计算加法结果,并同时显示到数码管上。虽然这远不是一个完善的计算器程序,但作为初学者也足够你研究一阵子了。
首先,本程序相对于之前的例程要复杂得多,需要完成的工作也多得多,所以把各个子功能都作成独立的函数,以使程序便于编写和维护。分析程序的时候就从主函数和中断函数入手,随着程序的流程进行就可以了。可以体会体会划分函数的好处,想想如果还是只有主函数和中断函数来实现的话程序会是什么样子。
其次,大家可以看到再把矩阵按键扫描分离出动作以后,并没有直接使用行列数所组成的数值作为分支判断执行动作的依据,而是把抽象的行列数转换为了一种叫作标准键盘键码(就是计算机键盘的编码)的数据,然后用得到的这个数据作为下一步分支判断执行动作的依据,为什么多此一举呢?有两层含义: 第一,尽量让自己设计的东西(包括硬件和软件)向已有的行业规范或标准看齐,这样有助于别人理解认可你的设计,也有助于你的设计与别人的设计相对接,毕竟标准就是为此而生的嘛。第二,有助于程序的层次化而方便维护与移植,比如用的按键是4×4,但如果后续又增加了一行成了4×5,那么由行列数组成的编号可能就变了,就要在程序的各个分支中查找修改,稍不留神就会出错,而采用这种转换后,则只需要维护KeyCodeMap这样一个数组表格就行了,看上去就像是把程序的底层驱动与应用层的功能实现函数分离开了,应用层不用关心底层的实现细节,底层改变后也无须在应用层中做相应修改,两层程序之间是一种标准化的接口。这就是程序的层次化,而层次化是构建复杂系统的必备条件,现在就先通过简单的示例来学习一下吧。
作为初学者针对这种程序的学习方式是,先从头到尾读一到三遍,边读边理解,然后边抄边理解,彻底理解透彻后,自己尝试独立写出来。完全采用记忆模式来学习这种例程,一两个例程你可能感觉不到什么提高,当这种例程背过上百八十个的时候,厚积薄发的感觉就来了。同时,在抄读的过程中也要注意学习编程规范,这些可都是无形的财富,可以为日后的研发工作加分。
#include reg52.h
sbit 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 LedBuff[6] = {//数码管显示缓冲区
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF
};

unsigned char code KeyCodeMap[4][4] = { //矩阵按键编号到标准键盘键码的映射表
{ 0x31, 0x32, 0x33, 0x26 }, //数字键1、数字键2、数字键3、向上键
{ 0x34, 0x35, 0x36, 0x25 }, //数字键4、数字键5、数字键6、向左键
{ 0x37, 0x38, 0x39, 0x28 }, //数字键7、数字键8、数字键9、向下键
{ 0x30, 0x1B, 0x0D, 0x27 }//数字键0、ESC键、回车键、 向右键
};

unsigned char KeySta[4][4] = {//全部矩阵按键的当前状态
{1, 1, 1, 1},{1, 1, 1, 1},{1, 1, 1, 1},{1, 1, 1, 1}
};

void KeyDriver();
void main()
{
        EA = 1;//使能总中断
        ENLED = 0;//选择数码管进行显示
        ADDR3 = 1;
        TMOD = 0x01;//设置T0为模式1
        TH0= 0xFC;//为T0赋初值0xFC67,定时1ms
        TL0= 0x67;
        ET0= 1; //使能T0中断
        TR0= 1; //启动T0
        LedBuff[0] = LedChar[0];//上电显示0
        while (1)
        {
                KeyDriver(); //调用按键驱动函数
        }
}

/* 将一个无符号长整型的数字显示到数码管上,num为待显示数字 */
void ShowNumber(unsigned long num)
{
        signed char i;
        unsigned char buf[6];
        for (i=0; i6; i++) //把长整型数转换为6位十进制的数组
        {
                buf[i] = num % 10;
                num = num / 10;
        }
        for (i=5; i=1; i--)//从最高位起,遇到0转换为空格,遇到非0则退出循环
        {
                if (buf[i] == 0)
                LedBuff[i] = 0xFF;
                else
                break;
        }
        for ( ; i=0; i--)//剩余低位都如实转换为数码管显示字符
        {
                LedBuff[i] = LedChar[buf[i]];
        }
}

/* 按键动作函数,根据键码执行相应的操作,keycode-按键键码 */
void KeyAction(unsigned char keycode)
{
        static unsigned long result = 0;//用于保存运算结果
        static unsigned long addend = 0;//用于保存输入的加数

        if ((keycode=0x30) && (keycode=0x39))//输入0~9的数字
        {
                addend = (addend*10)+(keycode-0x30); //整体十进制左移,新数字进入个位
                ShowNumber(addend); //运算结果显示到数码管
        }
        else if (keycode == 0x26)//向上键用作加号,执行加法或连加运算
        {
                result += addend; //进行加法运算
                addend = 0;
                ShowNumber(result);//运算结果显示到数码管
        }
        else if (keycode == 0x0D)//回车键执行加法运算(实际效果与加号相同)
        {
                result += addend;//进行加法运算
                addend = 0;
                ShowNumber(result); //运算结果显示到数码管
        }
        else if (keycode == 0x1B)//Esc键,清零结果
        {
                addend = 0;
                result = 0;
                ShowNumber(addend);//清零后的加数显示到数码管
        }
}

/* 按键驱动函数,检测按键动作,调度相应动作函数,需在主循环中调用 */
void KeyDriver()
{
        unsigned char i, j;
        static unsigned char backup[4][4] = {//按键值备份,保存前一次的值       
        {1, 1, 1, 1},{1, 1, 1, 1},{1, 1, 1, 1},{1, 1, 1, 1}
        };

        for (i=0; i4; i++)//循环检测4*4的矩阵按键
        {
                for (j=0; j4; j++)
                {
                        if (backup[i][j] != KeySta[i][j])//检测按键动作
                        {
                                if (backup[i][j] != 0) //按键按下时执行动作
                                {
                                        KeyAction(KeyCodeMap[i][j]); //调用按键动作函数
                                }
                                backup[i][j] = KeySta[i][j]; //刷新前一次的备份值
                        }
                }
        }
}
/* 按键扫描函数,需在定时中断中调用,推荐调用间隔1ms */
void KeyScan()
{
        unsigned char i;
        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}
                };
               
        //将一行的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 (i=0; 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;
        }
}
/* 数码管动态扫描刷新函数,需在定时中断中调用 */

void LedScan()
{
        static unsigned char i = 0;//动态扫描的索引
        P0 = 0xFF; //显示消隐
        switch (i)
        {
                case 0: ADDR2=0; ADDR1=0; ADDR0=0; i++; P0=LedBuff[0]; break;
                case 1: ADDR2=0; ADDR1=0; ADDR0=1; i++; P0=LedBuff[1]; break;
                case 2: ADDR2=0; ADDR1=1; ADDR0=0; i++; P0=LedBuff[2]; break;
                case 3: ADDR2=0; ADDR1=1; ADDR0=1; i++; P0=LedBuff[3]; break;
                case 4: ADDR2=1; ADDR1=0; ADDR0=0; i++; P0=LedBuff[4]; break;
                case 5: ADDR2=1; ADDR1=0; ADDR0=1; i=0; P0=LedBuff[5]; break;
                default: break;
        }
}

/* T0中断服务函数,用于数码管显示扫描与按键扫描 */
void InterruptTimer0() interrupt 1
{
        TH0 = 0xFC;//重新加载初值
        TL0 = 0x67;
        LedScan(); //调用数码管显示扫描函数
        KeyScan(); //调用按键扫描函数
}

      

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有帐号?注册

x

阿莫论坛20周年了!感谢大家的支持与爱护!!

月入3000的是反美的。收入3万是亲美的。收入30万是移民美国的。收入300万是取得绿卡后回国,教唆那些3000来反美的!

出0入0汤圆

发表于 2014-5-26 18:14:14 | 显示全部楼层
谢谢楼主,资料是极好的!

出0入0汤圆

发表于 2014-5-26 19:10:37 | 显示全部楼层
想不到小宋老师也在这里了

出0入0汤圆

发表于 2014-5-26 21:28:33 | 显示全部楼层
真心不错

出0入0汤圆

发表于 2014-5-26 22:37:04 | 显示全部楼层
感谢楼主分享 先顶后看

出0入0汤圆

发表于 2014-5-26 22:58:09 | 显示全部楼层
相当给力啊,顶楼主。

出0入0汤圆

发表于 2014-5-26 23:19:33 | 显示全部楼层
学习学习!!

出0入10汤圆

发表于 2014-5-26 23:34:37 来自手机 | 显示全部楼层
顶楼主,感谢无私分享

出0入0汤圆

发表于 2014-5-26 23:54:31 | 显示全部楼层
支持宋老师

出0入0汤圆

发表于 2014-6-3 20:51:37 | 显示全部楼层
谢谢老师分享,认真学习!!!!!

出0入0汤圆

发表于 2014-10-10 22:45:47 | 显示全部楼层
小宋老师的资料很有参考价值,谢谢分享!

出0入0汤圆

发表于 2014-10-11 08:16:49 | 显示全部楼层
谢谢分享!

出0入0汤圆

发表于 2014-10-11 13:03:46 | 显示全部楼层
先收下了,谢谢老师的无私奉献。

出0入0汤圆

发表于 2014-10-11 16:14:40 | 显示全部楼层
顶楼主,下来好好学习下

出0入95汤圆

发表于 2014-10-12 00:06:23 | 显示全部楼层
好东东,顶一下

出0入0汤圆

发表于 2014-10-12 14:39:21 | 显示全部楼层
蛮全的  以前学习的时候资历好少。。

出0入0汤圆

发表于 2014-10-12 21:23:56 | 显示全部楼层
为什么都喜欢在中断里处理按键扫描?有什么特别大优势?

出0入0汤圆

发表于 2014-10-12 22:54:24 | 显示全部楼层
感谢分享!

出0入0汤圆

发表于 2014-10-13 08:02:00 | 显示全部楼层
好资料,感谢楼主分享!

出0入0汤圆

发表于 2014-10-17 13:09:17 | 显示全部楼层
看过视频,挺好的

出0入0汤圆

发表于 2014-10-17 13:31:24 | 显示全部楼层
支持小宋老师

出0入0汤圆

发表于 2014-10-17 16:08:07 | 显示全部楼层
非常好的资料  谢谢老师

出0入0汤圆

发表于 2014-10-17 17:24:53 | 显示全部楼层
很好的资料,很适合学习。

出0入0汤圆

发表于 2014-10-17 18:13:13 | 显示全部楼层
顶一个,2年前看过几集宋老师的视频。

出0入0汤圆

发表于 2014-10-19 16:06:26 | 显示全部楼层
多谢楼主共享

出0入0汤圆

发表于 2014-11-14 00:10:24 | 显示全部楼层
不错,我也是看过楼主教程读者之一,真心不错!

出0入0汤圆

发表于 2014-12-1 23:19:15 | 显示全部楼层
好资源,要好好学习。

出0入0汤圆

发表于 2014-12-3 16:11:16 | 显示全部楼层
感谢分享!努力奋斗····!

出0入0汤圆

发表于 2014-12-3 17:15:23 | 显示全部楼层
感谢分享精神

出0入0汤圆

发表于 2014-12-4 09:56:57 | 显示全部楼层
"出版社不建议开源电子版无非是担心读者有了电子版就不购买纸质书籍了,我想这个担心完全没必要。电子版提供更容易让大家通过阅读电子版来了解书写的怎么样,到底有没有真材实料,更有利于知识的传播。通过电子版如果觉得书写的还可以,考虑用来做教材的教师可以跟出版社联系索取样书,并且可以通过出版社获取免费教学实验板,助力中国教育,联系方式都在电子书籍首页。"
思路相同,极力支持!!!

出0入0汤圆

发表于 2014-12-4 10:04:05 | 显示全部楼层
对初学者来说绝对是最佳的教程,比郭天祥那个专业太多了,推广重心放在大学生里,加油!金沙滩。

出0入0汤圆

发表于 2014-12-9 11:01:09 | 显示全部楼层
好资料,感谢楼主分享!

出0入0汤圆

发表于 2014-12-9 11:07:27 | 显示全部楼层
学了一段时间就断了,重新学学,,,

出0入0汤圆

发表于 2014-12-9 11:34:35 | 显示全部楼层
谢谢分享 很不错的资料

出0入0汤圆

发表于 2014-12-15 01:04:26 来自手机 | 显示全部楼层
几年前就看过小宋老师的教材,觉得不错,谢谢再次分享!

出0入0汤圆

发表于 2015-6-11 22:03:13 | 显示全部楼层
谢谢小宋老师。很好的 资料

出0入0汤圆

发表于 2015-12-18 11:54:42 | 显示全部楼层
入门资料不错,想当初我们大学教材都是51汇编的,实验课也不知道做了什么,用C语言开发单片机程序都是大学毕业后才自学的。

出0入0汤圆

发表于 2016-5-19 13:44:55 | 显示全部楼层
确实很好~

出0入0汤圆

发表于 2016-6-23 07:46:07 | 显示全部楼层
支持宋老师,6年前第一次接触单片机就是看的宋老师的视频。真的讲的非常不错。

出0入0汤圆

发表于 2018-10-11 07:56:09 | 显示全部楼层
单片机的入门老师,讲的真的很好。

出0入0汤圆

发表于 2018-10-11 08:54:27 | 显示全部楼层
这个必须赞一个!正在学习过程中!!!

出0入54汤圆

发表于 2018-10-13 13:56:46 | 显示全部楼层
我大51单片机屹立万年不倒啊

出0入0汤圆

发表于 2018-10-24 11:06:24 | 显示全部楼层
资料很全,很好,谢谢分享

出0入0汤圆

发表于 2018-11-27 09:20:55 | 显示全部楼层
这个按键程序方法很好,就是太耗RAM空间了,与本论坛的状态机的方法相比

出0入4汤圆

发表于 2018-11-27 11:59:30 | 显示全部楼层
这本书我在罗湖图书馆看过,是本好书!

出10入12汤圆

发表于 2018-11-27 16:24:19 | 显示全部楼层
下载看看呢!!!!!!!!!!

出0入0汤圆

发表于 2018-12-16 21:18:44 | 显示全部楼层
感谢楼主分享

出0入0汤圆

发表于 2019-8-20 09:16:56 | 显示全部楼层
没方法,开源了就一定要买一本

出0入0汤圆

发表于 2019-8-22 18:39:51 | 显示全部楼层
这本书好书,值得拥有!

出0入0汤圆

发表于 2019-10-6 10:48:07 | 显示全部楼层
不错,真的是本好书,值得拥有,谢谢分享,辛苦了
回帖提示: 反政府言论将被立即封锁ID 在按“提交”前,请自问一下:我这样表达会给举报吗,会给自己惹麻烦吗? 另外:尽量不要使用Mark、顶等没有意义的回复。不得大量使用大字体和彩色字。【本论坛不允许直接上传手机拍摄图片,浪费大家下载带宽和论坛服务器空间,请压缩后(图片小于1兆)才上传。压缩方法可以在微信里面发给自己(不要勾选“原图),然后下载,就能得到压缩后的图片】。另外,手机版只能上传图片,要上传附件需要切换到电脑版(不需要使用电脑,手机上切换到电脑版就行,页面底部)。
您需要登录后才可以回帖 登录 | 注册

本版积分规则

手机版|Archiver|amobbs.com 阿莫电子技术论坛 ( 粤ICP备2022115958号, 版权所有:东莞阿莫电子贸易商行 创办于2004年 (公安交互式论坛备案:44190002001997 ) )

GMT+8, 2024-4-27 17:27

© Since 2004 www.amobbs.com, 原www.ourdev.cn, 原www.ouravr.com

快速回复 返回顶部 返回列表