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

2020-01-12 17:19发布

本帖最后由 51kingst 于 2014-5-26 16:43 编辑

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

      
友情提示: 此问题已得到解决,问题已经关闭,关闭后问题禁止继续编辑,回答。