51单片机(STC89C52RC)系统性学习笔记

51单片机(STC89C52RC)系统性学习笔记本文是关于 51 单片机学习的笔记 不包含红外模块的相关内容

大家好,欢迎来到IT知识分享网。

STC89C52RC学习笔记

前言

本文是关于51单片机学习的笔记,不包含红外模块的相关内容。

学习资源

单片机型号

STC89C52RC单片机开发板

环境搭建


1.基础知识

1.1 单片机的概念

单片机(Micro Controller Unit,简称MCU),它是集成了CPU、RAM、ROM、定时器、中断系统、通讯接口等硬件的小而完善的微型计算机系统。

单片机相关概念可以参考相关的百度百科:https://baike.baidu.com/item/%E5%8D%95%E7%89%87%E6%9C%BA/

1.2 单片机的功能

单片机的主要功能是依靠传感器进行信息采集,依靠CPU进行信息处理和依靠硬件设备进行控制。

  • 与计算机相比,单片机相当于一个袖珍版计算机,它能独立构成一个完整的计算机系统。但是在性能上和计算机还是相差甚远的。
  • 单片机的使用范围非常广泛,从红绿灯到家用电器都有应用。

1.3 STC89C52单片机

STC89C52单片机属于51单片机系列,由STC公司生产,位数为8位,RAM为512字节,ROM为8K,工作频率为12MHz。

1.3.1 命名规则

请添加图片描述

1.3.2 内部结构

在这里插入图片描述

1.3.3 管脚

在这里插入图片描述

2.51单片机开发板

在这里插入图片描述

3.LED相关

3.1 LED模块原理图

在这里插入图片描述

  • 简单来讲,发光二极管,正极接正,负极接负,即可发光。
  • 在LED模块中,P20-27若是输出低电平即可实现LED的发光。

如何实现代码向实际功能的转变?
MCU中代码通过CPU来更改寄存器的值,再通过驱动器传输给硬件与MCU相连的引脚,如若符合硬件电路工作的条件,则实现功能。

3.2 实现单个LED点亮

参考步骤:

  1. 在Keil中建立新项目。

在这里插入图片描述

  1. 找到Microchip下的AT89C51RC2,出现对话框后点是。

目录里的这些都是生产公司的名字

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  1. 右键Source Group 1文件夹,点击Add New Item Group ‘Source Group 1’…
    请添加图片描述
  2. 选择C File,命名文件为main
    在这里插入图片描述
  3. 参考3.1原理,找到控制LED灯的引脚为P20-P27,寄存器以八个为一组,所以P20-P27可以直接以P2的形式呈现。
  4. 例如要使P27相连的LED灯点亮,则P27要输出低电平,即输出0。
  5. 在P2中以二进制的方式表示为 P2 = 1111 1110 。
  6. 需要注意的是,在单片机程序中需要将二进制转换为十六进制。转换后得 P2 = 0xFE。

0x是一个前缀,表示后面是十六进制。

  1. 将代码写入keil
    代码呈现:
#include <at89c51RC2.h> void main() { 
     P2 = 0xFE; } 
  1. 点击Options for Target,点击Output,选中Create HEX File,点击OK后,返回点击Build
    请添加图片描述
    请添加图片描述
    在这里插入图片描述


  2. 打开stc-isp,在单片机型号中选择自己的单片机型号。
    请添加图片描述
  3. 串口号选择如下:
    在这里插入图片描述
  4. 点击打开程序文件,找到建立的文件,点击打开。

在这里插入图片描述
请添加图片描述

  1. 点击下载,出现右框所示操作成功,查看你的单片机,此时LED应当处于点亮状态。请添加图片描述

3.3 LED闪烁

  1. 参考3.2实现LED隔一个点亮
#include <at89c51RC2.h> void main() { 
     P2 = 0xAA; //1010 1010 } 
  1. 实现与上一步相反的点亮状态
#include <at89c51RC2.h> void main() { 
     P2 = 0x55; //0101 0101 } 
  1. 设置循环实现交替闪烁
#include <at89c51RC2.h> void main() { 
     while(1) { 
     P2 = 0xAA; //1010 1010 P2 = 0x55; //0101 0101 } } 

代码呈现如下:

#include <at89c51RC2.h> #include <INTRINS.H> void Delay500ms() //@11.0592MHz { 
     unsigned char i, j, k; _nop_(); i = 4; j = 129; k = 119; do { 
     do { 
     while (--k); } while (--j); } while (--i); } void main() { 
     while(1) { 
     P2 = 0xAA; //1010 1010 Delay500ms(); P2 = 0x55; //0101 0101 Delay500ms(); } } 

3.4 LED流水灯

  1. 参考3.2实现一个灯亮
#include <at89c51RC2.h> void main() { 
     P2 = 0x7F; //0111 1111 } 
  1. 参考3.2实现上一步中灯灭后与它相邻的灯亮
#include <at89c51RC2.h> void main() { 
     P2 = 0xBF; //1011 1111 } 
  1. 根据上述两步以及3.3的思路,实现每次只亮一个灯

代码呈现如下:

#include <at89c51RC2.h> #include <INTRINS.H> void Delay500ms() //@11.0592MHz { 
     unsigned char i, j, k; _nop_(); i = 4; j = 129; k = 119; do { 
     do { 
     while (--k); } while (--j); } while (--i); } void main() { 
     while(1) { 
     P2 = 0x7F; //0111 1111 Delay500ms(); P2 = 0xBF; //1011 1111 Delay500ms(); P2 = 0xDF; //1101 1111 Delay500ms(); P2 = 0xEF; //1110 1111 Delay500ms(); P2 = 0xF7; //1111 0111 Delay500ms(); P2 = 0xFB; //1111 1011 Delay500ms(); P2 = 0xFD; //1111 1101 Delay500ms(); P2 = 0xFE; //1111 1110 Delay500ms(); } } 

上述代码实现的是以500ms为一个间隔的流水灯,对代码进行优化,使得间隔时间由给定量决定

  1. 生成一个以1ms为单位间隔的自定义函数,给定一个参数表示自定义的间隔时间,修改延时函数
#include <INTRINS.H> void Delay(unsigned int xms) //@11.0592MHz { 
     unsigned char i, j; _nop_(); while(xms) { 
     i = 2; j = 199; do { 
     while (--j); } while (--i); xms--; } } 
  1. 参考以上代码得到优化代码
#include <at89c51RC2.h> #include <INTRINS.H> void Delay(unsigned int xms) //@11.0592MHz { 
     unsigned char i, j; _nop_(); while(xms) { 
     i = 2; j = 199; do { 
     while (--j); } while (--i); xms--; } } void main() { 
     while(1) { 
     P2 = 0x7F; //0111 1111 Delay(100); P2 = 0xBF; //1011 1111 Delay(100); P2 = 0xDF; //1101 1111 Delay(100); P2 = 0xEF; //1110 1111 Delay(100); P2 = 0xF7; //1111 0111 Delay(100); P2 = 0xFB; //1111 1011 Delay(100); P2 = 0xFD; //1111 1101 Delay(100); P2 = 0xFE; //1111 1110 Delay(100); } } 

4.独立按键

4.1 独立按键原理图

在这里插入图片描述

4.2 独立按键控制LED的亮灭

  1. 通过P2_0 – P2_7实现对单独的LED的控制,例如 P2_0=0 单独对D1灯的处理,使得只控制它亮,而其他的LED保持原始状态不改变。
    与P2相比,P2可以看成批量处理。
#include <at89c51RC2.h> void main() { 
     P2_0=0; } 
  1. 参考4.1可知,当 P3_0==0状态时即开关闭合。
  2. 结合上述两步,引入if语句判断开关状态,实现开关闭合时,D1号灯亮。
#include <at89c51RC2.h> void main() { 
     if(P3_0==0) P2_0=0; else P2_0=1; } 
  1. 用if语句判断按键当前处于闭合还是断开状态。
    以闭合为例,若是闭合,则延时20毫秒,消除闭合时的抖动。
    while循环判断是否松开手,若是没有松开,则不跳出循环,不执行下方语句;若是松开,先延时20毫秒,消除断开时的抖动,如果不做处理,P2_0此时为1,将变为灭的状态。如果想要松开手后,灯仍然保持亮的状态,那么要使得P2_0为0,所以要用 P2_0=~P2_0 将P2_0的状态取反。

为什么不直接将P2_0赋值为0?
因为,LED的状态变化的条件为,按键按下松开,所以不论后续要保持亮的状态还是灭的状态,都要进过按下的过程,即要进入if循环进行取反。

代码呈现如下:

#include <at89c51RC2.h> #include <INTRINS.H> void Delay(unsigned int xms) //@11.0592MHz { 
     unsigned char i, j; _nop_(); while(xms) { 
     i = 2; j = 199; do { 
     while (--j); } while (--i); xms--; } } void main() { 
     while(1) { 
     if(P3_0==0) { 
     Delay(20); while(P3_0==0); Delay(20); P2_0=~P2_0; } } } 

4.3 独立按键控制LED显示二进制

  1. 参考4.1,判断独立按键的通断状况。
#include <at89c51RC2.h> #include <INTRINS.H> void Delay(unsigned int xms) //@11.0592MHz { 
     unsigned char i, j; _nop_(); while(xms) { 
     i = 2; j = 199; do { 
     while (--j); } while (--i); xms--; } } void main() { 
     while(1) { 
     if(P3_1==0) { 
     Delay(20); while(P3_1==0); Delay(20); } } 
  1. LED显示二进制,即P2在0~256之间变化,LED灯的亮灭状态也发生变化,例如当P2为0时,LED全亮,P2为1时,LED中D1灭了,其他保持亮的状态。P2从0到1的变化,由 P2++ 实现。
#include <at89c51RC2.h> #include <INTRINS.H> void Delay(unsigned int xms) //@11.0592MHz { 
     unsigned char i, j; _nop_(); while(xms) { 
     i = 2; j = 199; do { 
     while (--j); } while (--i); xms--; } } void main() { 
     while(1) { 
     if(P3_1==0) { 
     Delay(20); while(P3_1==0); Delay(20); P2++; } } } 
  1. 上面一步实现的是灭了的灯表示二进制,如果要实现亮着的灯表示的是二进制。
    如果直接在 P2++; 加入语句 P2=~P2; LED将一直保持熄灭状态,因为初始时P2的二进制表示为1111 1111,当进行了加法操作后,由于溢出P2的二进制表示将变为0000 0000,由于程序运行很快,着段时间可以忽略不记,此时若是取反,P2又会变回1111 1111的状态,而训话一直在进行,所以P2可以看成保持在1111 1111的状态,所以LED将一直保持熄灭状态。
    由上述可知,如果直接用P2的话,只能取到十进制中的0和256,且0的状态可以忽略不记,所以如果要取中间值的话,我们可以引入一个变量LEDNum。使 P2=LEDNum++ 时,所得结果还是熄灭的灯表示二进制,此时只要将取反后的LEDNum赋值给P2即可实现亮着的灯表示的是二进制。

代码呈现如下:

#include <at89c51RC2.h> #include <INTRINS.H> void Delay(unsigned int xms) //@11.0592MHz { 
     unsigned char i, j; _nop_(); while(xms) { 
     i = 2; j = 199; do { 
     while (--j); } while (--i); xms--; } } void main() { 
     unsigned char LEDNum=0; while(1) { 
     if(P3_1==0) { 
     Delay(20); while(P3_1==0); Delay(20); LEDNum++; P2=~LEDNum; } } } 

4.4 独立按键控制LED移位

  1. 参考4.1,判断两个独立按键的通断状况。
#include <at89c51RC2.h> #include <INTRINS.H> void Delay(unsigned int xms) //@11.0592MHz { 
     unsigned char i, j; _nop_(); while(xms) { 
     i = 2; j = 199; do { 
     while (--j); } while (--i); xms--; } } void main() { 
     while(1) { 
     if(P3_1==0) { 
     Delay(20); while(P3_1==0); Delay(20); } if(P3_0==0) { 
     Delay(20); while(P3_0==0); Delay(20); } } } 
  1. K1按键(P3_1)控制LED点亮的状态左移,K2(P3_0)控制LED点亮的状态右移,实现移送可以用位运算符中的<<和>>。
    由于我的开发板的LED模块是从右往左分别为二进制的第一位、第二位、第三位…和我们二进制书写的方式正好相反,所以我在实现LED点亮状态的左移时,其实是在二进制表示为0000 0001的基础上实现数字1的右移动
    因为1的初始状态在第一位,如果直接用>>向右移动的话会产生溢出,所以可以借助<<以达到相同的功能。
    引入一个变量LEDNum标记移动的位数。
    因为向右移动一位与向左移动七位等价,所以在初始状态下按第一下,发光状态向左移动一位,即1向左移动7位,所以给初始值为0的LEDNum赋值为7,那么按动的第一下用代码表示为P2=~(0x01<<LEDNum)。
    此后,每按一次,发光状态都左移动一位,即1向左移动的位数在LEDNum为7的基础向减一位。
    当LEDNum减到0时,在下一次循环要将它重新赋值为7,避免出现LEDNum为-1的状况。
    此时得到的结果为LED中我们需要点亮的那盏是熄灭的,其他的LED都是点亮状态。此时只要进行取反操作即可。






unsigned char LEDNum=0; while(1) { 
     if(P3_1==0) { 
     Delay(20); while(P3_1==0); Delay(20); if(LEDNum==0) LEDNum=7; else LEDNum--; P2 =~ (0x01<<LEDNum); } } 
  1. 参考上一步,为实现LED点亮状态的右移时,在二进制表示为0000 0001的基础上,按动一次K1,1向左移动一位,进行LEDNum++的操作。
    需要注意的是LEDNum最多向左移动7位,所以当LEDNum超出值时要重新进行赋值操作。
unsigned char LEDNum=0; while(1) { 
     if(P3_0==0) { 
     Delay(20); while(P3_0==0); Delay(20); LEDNum++; if(LEDNum>=8) LEDNum=0; P2 =~ (0x01<<LEDNum); } 
  1. 执行上述程序时,最开始灯都不亮,且操作后显示的是移位后的点亮状态,并没有初始的D1点亮的状态,是因为P2的默认的初始状态的二进制表示为1111 1111,而进行独立按键的操作后P2被赋予的是0x01移动相应位置取反后的状态,所以初始的D1点亮的状态直接被忽略了。
    为解决这个问腿,只要给用 P2=~0x01 给P2一个初始值即可。

代码呈现:

#include <at89c51RC2.h> #include <INTRINS.H> void Delay(unsigned int xms) //@11.0592MHz { 
     unsigned char i, j; _nop_(); while(xms) { 
     i = 2; j = 199; do { 
     while (--j); } while (--i); xms--; } } void main() { 
     unsigned char LEDNum=0; P2=~0x01; while(1) { 
     if(P3_1==0) { 
     Delay(20); while(P3_1==0); Delay(20); if(LEDNum==0) LEDNum=7; else LEDNum--; P2 =~ (0x01<<LEDNum); } if(P3_0==0) { 
     Delay(20); while(P3_0==0); Delay(20); LEDNum++; if(LEDNum>=8) LEDNum=0; P2 =~ (0x01<<LEDNum); } } } 

5.数码管

5.1 数码管工作原理

5.1.1 数码管原理图

在这里插入图片描述

COM:共阴极,要选中哪个数码管,就给这个COM端输入低电平(0)。
a、b、c、d、e、f、g、dp:共阳极,输入高电平(1)有效。
左边的芯片是双向数据缓冲器。

5.1.2 数码管驱动方式

5.2 数码管的静态显示

  1. 选中要用的数码管,在此以选中LED7为例。因为数码管模块的COM口与138译码器相连,所以,此端口的输入值由138译码器的输出值决定。由138译码器可知,LED7对应的是Y6端口,所以要Y6端有效,则CBA的输入为110。
#include <at89c51RC2.h> void main() { 
     while(1) { 
     P2_4=1; P2_3=1; P2_2=0; P0=0x7D; } } 
  1. 用子函数优化代码。
    自定义一个函数,用以判断需要点亮的数码管和需要显示的数字。
    需要注意的是,a、b、c、d、e、f、g、dp以二进制表示时顺序为dp、g、f、e、d、c、b、a。例如要选中b和c,那么二进制表示则为0000 0110,转换为二进制则为0x06。

代码呈现:

#include <at89c51RC2.h> unsigned char NixieTable[]={ 
    0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71,0x00}; void Nixie(unsigned char LED,Number) { 
     switch(LED) { 
     case 1: P2_4=0;P2_3=0;P2_2=0;break; case 2: P2_4=0;P2_3=0;P2_2=1;break; case 3: P2_4=0;P2_3=1;P2_2=0;break; case 4: P2_4=0;P2_3=1;P2_2=1;break; case 5: P2_4=1;P2_3=0;P2_2=0;break; case 6: P2_4=1;P2_3=0;P2_2=1;break; case 7: P2_4=1;P2_3=1;P2_2=0;break; case 8: P2_4=1;P2_3=1;P2_2=1;break; } P0=NixieTable[Number]; } void main() { 
     while(1) { 
     Nixie(1,5); } } 

5.3数码管动态显示

  1. 参考5.1,由于单片机执行代码的速度很快,间隔时间可以忽略不计。思考是否可以直接多次调用函数实现动态显示?
#include <at89c51RC2.h> unsigned char NixieTable[]={ 
    0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71,0x00}; void Nixie(unsigned char LED,Number) { 
     switch(LED) { 
     case 1: P2_4=0;P2_3=0;P2_2=0;break; case 2: P2_4=0;P2_3=0;P2_2=1;break; case 3: P2_4=0;P2_3=1;P2_2=0;break; case 4: P2_4=0;P2_3=1;P2_2=1;break; case 5: P2_4=1;P2_3=0;P2_2=0;break; case 6: P2_4=1;P2_3=0;P2_2=1;break; case 7: P2_4=1;P2_3=1;P2_2=0;break; case 8: P2_4=1;P2_3=1;P2_2=1;break; } P0=NixieTable[Number]; } void main() { 
     while(1) { 
     Nixie(8,1); Nixie(7,2); Nixie(6,3); } } 
  1. 执行上述程序,我们发现数码管的显示出现错位。
    这是因为,数码管在显示时一直在进行 位选(选择让哪个数码管亮) 段选(选择让这个数码管显示显示什么数字) 位选 段选 位选 段选… 的过程。还是因为执行速度太快,段选过后单片机上面的针脚有几毫秒还处于该段选的显示状态,然后在这几毫秒内单片机又进行了下一位位选,所以导致了在选择了下一位位选时还保留了上一条段选的显示状态,所以会出现错位的情况。
    为解决这个问题,我们要进行消影处理,即在段选和位选之间进行清零。
    段选的结尾在Nixie()函数,所以应在Nixie()函数中进行处理。清零即对于针脚全都不选择,用 P0=0x00 。


#include <at89c51RC2.h> #include <INTRINS.H> void Delay(unsigned int xms) //@11.0592MHz { 
     unsigned char i, j; _nop_(); while(xms) { 
     i = 2; j = 199; do { 
     while (--j); } while (--i); xms--; } } unsigned char NixieTable[]={ 
    0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71,0x00}; void Nixie(unsigned char LED,Number) { 
     switch(LED) { 
     case 1: P2_4=0;P2_3=0;P2_2=0;break; case 2: P2_4=0;P2_3=0;P2_2=1;break; case 3: P2_4=0;P2_3=1;P2_2=0;break; case 4: P2_4=0;P2_3=1;P2_2=1;break; case 5: P2_4=1;P2_3=0;P2_2=0;break; case 6: P2_4=1;P2_3=0;P2_2=1;break; case 7: P2_4=1;P2_3=1;P2_2=0;break; case 8: P2_4=1;P2_3=1;P2_2=1;break; } P0=NixieTable[Number]; P0=0x00; } void main() { 
     while(1) { 
     Nixie(8,1); Nixie(7,2); Nixie(6,3); } } 
  1. 按照上述操作,数码管的显示会比较暗。
    为解决此问题,可以在清零前加1ms的延迟。

代码呈现:

#include <at89c51RC2.h> #include <INTRINS.H> void Delay(unsigned int xms) //@11.0592MHz { 
     unsigned char i, j; _nop_(); while(xms) { 
     i = 2; j = 199; do { 
     while (--j); } while (--i); xms--; } } unsigned char NixieTable[]={ 
    0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71,0x00}; void Nixie(unsigned char LED,Number) { 
     switch(LED) { 
     case 1: P2_4=0;P2_3=0;P2_2=0;break; case 2: P2_4=0;P2_3=0;P2_2=1;break; case 3: P2_4=0;P2_3=1;P2_2=0;break; case 4: P2_4=0;P2_3=1;P2_2=1;break; case 5: P2_4=1;P2_3=0;P2_2=0;break; case 6: P2_4=1;P2_3=0;P2_2=1;break; case 7: P2_4=1;P2_3=1;P2_2=0;break; case 8: P2_4=1;P2_3=1;P2_2=1;break; } P0=NixieTable[Number]; Delay(1); P0=0x00; } void main() { 
     while(1) { 
     Nixie(8,1); Nixie(7,2); Nixie(6,3); } } 

6.模块化编程

6.1 模块化编程的作用

6.2 模块化编程的步骤

在此以延时函数为例。

  1. 将延时函数的代码放在.c文件里。
    在这里插入图片描述
  2. 在.h文件里提供外部可调用函数的声明。
    创建.h文件,在预处理框架里进行函数声明。
    在这里插入图片描述
    在这里插入图片描述


#ifndef __DELAY_H__ #define __DELAY_H__ void Delay(unsigned int xms); #endif 
  1. 在main.c函数中调用调试。
    在这里插入图片描述

7.LCD1602

7.1 基础介绍

7.1.1 LCD1602

7.1.2 LCD1602原理图

在这里插入图片描述

GND:接地。
VCC:电源正极(4.5V~5.5V)。
VO:对比度调节电压。
RS:数据/指令选择,1为数据,0为指令。
RW:读/写选择,1为读,0为写。
E:使能,1为数据有效,下降沿执行指令。
D0~D7:数据输入/输出。
A:背光灯电源正极。
K:背光灯电源负极。







7.1.2 LCD1602内部结构框图

在这里插入图片描述
在这个框图里,实现的步骤是:将要显示的数据写入数据显示区(DDRAM)光标指示的位置,然后通过字模库(CGRAM+CGROM)找出要现实的字符,再在屏幕中显示。

7.1.2.1 DDRAM

在这里插入图片描述

7.1.2.2 CGRAM+CGROM

7.1.3 时序结构

7.1.4 显示模块指令

7.1.4.1 清屏指令

在这里插入图片描述

7.1.4.2 光标归位指令

在这里插入图片描述

7.1.4.3 进入模式指令

在这里插入图片描述
在这里插入图片描述

7.1.4.4 显示开关控制指令

在这里插入图片描述
在这里插入图片描述

7.1.4.5 功能设定指令

在这里插入图片描述

其他指令可以参看文章末的手册。

7.2 显示一个字符

  1. 最开始调用存储了LCD1602相关程序的头文件。
  1. 对于LCD1602的使用,要先进行初始化。
/ * @brief LCD1602写命令 * @param Command 要写入的命令 * @retval 无 */ void LCD_WriteCommand(unsigned char Command) { 
     LCD_RS=0; LCD_RW=0; LCD_DataPort=Command; LCD_EN=1; LCD_Delay(); LCD_EN=0; LCD_Delay(); } void LCD_Init() { 
     LCD_WriteCommand(0x38);//八位数据接口,两行显示,5*7点阵 LCD_WriteCommand(0x0c);//显示开,光标关,闪烁关 LCD_WriteCommand(0x06);//数据读写操作后,光标自动加一,画面不动 LCD_WriteCommand(0x01);//光标复位,清屏 } 
  1. 然后调用单个字符显示函数。
/ * @brief LCD1602设置光标位置 * @param Line 行位置,范围:1~2 * @param Column 列位置,范围:1~16 * @retval 无 */ void LCD_SetCursor(unsigned char Line,unsigned char Column) { 
     if(Line==1) { 
     LCD_WriteCommand(0x80|(Column-1)); } else if(Line==2) { 
     LCD_WriteCommand(0x80|(Column-1+0x40)); } } / * @brief 在LCD1602指定位置上显示一个字符 * @param Line 行位置,范围:1~2 * @param Column 列位置,范围:1~16 * @param Char 要显示的字符 * @retval 无 */ void LCD_ShowChar(unsigned char Line,unsigned char Column,char Char) { 
     LCD_SetCursor(Line,Column); LCD_WriteData(Char); } 
  1. 需要注意的是,在STC89C52RC中,单个字符和多个字符一样要用双引号。

代码呈现:

#include <at89c51RC2.h> #include <LCD1602.h> void main() { 
     LCD_Init(); LCD_ShowChar(1,1,"A"); while(1) { 
     } } 

8.矩阵键盘

8.1 矩阵键盘原理图

在这里插入图片描述
可以参考独立按键。
但是相对于独立键盘而言,矩阵键盘的连接方式可以减少I/O口的占用。
矩阵键盘的扫描是输入扫描,以行或者列为单位,逐个扫描,快速循环这个过程,最终实现所有按键同时扫描。


  • 按行扫描会与单片机上的蜂鸣器功能相冲突,所以一般选择按列扫描。

8.2 矩阵键盘

  1. 用模块化编程的思维,就矩阵键盘建立新的文件MatrixKey.c和MatrixKey.h。
    在这里插入图片描述
  2. 初始状态将P1针脚全部置为1。
    选中的那一列对应的针脚输入0。
    判断按键按下以及消抖参考4.2。
    根据原理图,判断当前按下的按键,引入变量KeyNumber用来标记该按键的返回值。
    在MatrixKey.c文件中,写入以下代码。



#include <at89c51RC2.h> #include <Delay.h> unsigned char MatrixKey() { 
     unsigned char KeyNumber=0; P1=0xFF; P1_3=0; if(P1_7==0){ 
    Delay(20);while(P1_7==0);Delay(20);KeyNumber=1;} if(P1_6==0){ 
    Delay(20);while(P1_6==0);Delay(20);KeyNumber=6;} if(P1_5==0){ 
    Delay(20);while(P1_5==0);Delay(20);KeyNumber=7;} P1=0xFF; P1_2=0; if(P1_7==0){ 
    Delay(20);while(P1_7==0);Delay(20);KeyNumber=2;} if(P1_6==0){ 
    Delay(20);while(P1_6==0);Delay(20);KeyNumber=5;} if(P1_5==0){ 
    Delay(20);while(P1_5==0);Delay(20);KeyNumber=8;} if(P1_4==0){ 
    Delay(20);while(P1_5==0);Delay(20);KeyNumber=0;} P1=0xFF; P1_1=0; if(P1_7==0){ 
    Delay(20);while(P1_7==0);Delay(20);KeyNumber=3;} if(P1_6==0){ 
    Delay(20);while(P1_6==0);Delay(20);KeyNumber=4;} if(P1_5==0){ 
    Delay(20);while(P1_5==0);Delay(20);KeyNumber=9;} return KeyNumber; } 
  1. 在MatrixKey.h文件中完成上一步函数的声明。
    在这里插入图片描述
  2. 在main.c文件中加入#include <MatriKey.h>用于调用MatriKey.h文件。
    调用LCD前要进行初始化。
    设置一个变量用于存储MatrixKey函数的返回值。
    当MatrixKey函数有返回值,即KeyNum有定义且在0-9时,调用LCD_ShowNum函数实现按键对应的函数在LCD上的显示。


#include <at89c51RC2.h> #include <Delay.h> #include <LCD1602.h> #include <MatriKey.h> unsigned char KeyNum; void main() { 
     LCD_Init(); LCD_ShowString(1,1,"MatrixKey:"); while(1) { 
     KeyNum=MatrixKey(); if(KeyNum>=0 && KeyNum<=9) { 
     LCD_ShowNum(2,1,KeyNum,1); } } } 

8.3 密码锁

  1. 密码锁程序只要在8.2上进行改动即可。可以对8.2程序文件建立副本,进行修改。
  2. 设置一个变量Password用于存储输入的四位密码的值。
//以下为在main.c文件下的更改 #include <Delay.h> #include <LCD1602.h> #include <MatriKey.h> unsigned char KeyNum; unsigned int Password=0; void main() { 
     LCD_Init(); LCD_ShowString(1,1,"Password:"); while(1) { 
     KeyNum=MatrixKey(); if(KeyNum>=0 && KeyNum<=9) { 
     Password*=10; //密码左移一位 Password+=KeyNum%10; //获取一位密码 LCD_ShowNum(2,1,Password,4); //设置四位密码 } } } 
  1. 已经输入四位后,再继续输入,显示值会出错,为了避免过多的输入影响显示的情况,我们可以引入变量Count记录按键按下的次数,如果已经达到四次,则不再获取输入值。
//以下为在main.c文件下的更改 #include <at89c51RC2.h> #include <Delay.h> #include <LCD1602.h> #include <MatriKey.h> unsigned char KeyNum; unsigned int Password=0,Count=0; void main() { 
     LCD_Init(); LCD_ShowString(1,1,"Password:"); while(1) { 
     KeyNum=MatrixKey(); if(KeyNum>=0 && KeyNum<=9 && Count<4) { 
     Password*=10; //密码左移一位 Password+=KeyNum%10; //获取一位密码 Count++; LCD_ShowNum(2,1,Password,4); //设置四位密码 } } } 
  1. 在8.2的基础上,对MatrixKey函数进行更改,添加确认按键,赋值为10。
#include <at89c51RC2.h> #include <Delay.h> / * @brief 矩阵键盘读取键盘键码 * @param 无 * @retval KeyNumber本人 按下按键的键码值 如果按键按下不放,程序会停留在子函数,松手的一瞬间,返回按键的键码值,没有按键按下时,返回W */ unsigned char MatrixKey() { 
     unsigned char KeyNumber="W"; P1=0xFF; P1_3=0; if(P1_7==0){ 
    Delay(20);while(P1_7==0);Delay(20);KeyNumber=1;} if(P1_6==0){ 
    Delay(20);while(P1_6==0);Delay(20);KeyNumber=4;} if(P1_5==0){ 
    Delay(20);while(P1_5==0);Delay(20);KeyNumber=7;} if(P1_4==0){ 
    Delay(20);while(P1_5==0);Delay(20);KeyNumber=10;} P1=0xFF; P1_2=0; if(P1_7==0){ 
    Delay(20);while(P1_7==0);Delay(20);KeyNumber=2;} if(P1_6==0){ 
    Delay(20);while(P1_6==0);Delay(20);KeyNumber=5;} if(P1_5==0){ 
    Delay(20);while(P1_5==0);Delay(20);KeyNumber=8;} if(P1_4==0){ 
    Delay(20);while(P1_5==0);Delay(20);KeyNumber=0;} P1=0xFF; P1_1=0; if(P1_7==0){ 
    Delay(20);while(P1_7==0);Delay(20);KeyNumber=3;} if(P1_6==0){ 
    Delay(20);while(P1_6==0);Delay(20);KeyNumber=6;} if(P1_5==0){ 
    Delay(20);while(P1_5==0);Delay(20);KeyNumber=9;} return KeyNumber; } 
  1. 判断确认按键(赋值为10的按键)按下时,再进行输入密码和正确密码的比较,所以在此前要设置好输入密码。
    如果密码正确,输出RIGHT,如果密码错误,则清空密码和计次,显示重新输入。
unsigned int True_Password=2470,Password=0,Count=0; if(KeyNum==10) { 
     if(Password==True_Password) LCD_ShowString(1,11,"RIGHT"); else { 
     LCD_ShowString(1,11,"ERROR"); Password=0; //密码清零 Count=0; //计次清零 LCD_ShowNum(2,1,Password,4); LCD_ShowString(1,11,"AGAIN"); //清空显示ERROR } } 
  1. 在上一步中,判断输入密码错误后,显示直接变成了AGAIN,不是ERROR没显示,而是代码运行太快,显示了但是我们看不见,所以可以在中间加一个延时函数。
unsigned int True_Password=2470,Password=0,Count=0; if(KeyNum==10) { 
     if(Password==True_Password) LCD_ShowString(1,11,"RIGHT"); else { 
     LCD_ShowString(1,11,"ERROR"); Delay(1000); Password=0; //密码清零 Count=0; //计次清零 LCD_ShowNum(2,1,Password,4); LCD_ShowString(1,11,"AGAIN"); //清空显示ERROR } } 
  1. 参考4、5、6步,我们可以再添加一个取消键,赋值为11。
if(KeyNum==11) { 
     Password=0; //密码清零 Count=0; //计次清零 LCD_ShowNum(2,1,Password,4); } 

代码呈现(仅main.c文件)

#include <at89c51RC2.h> #include <Delay.h> #include <LCD1602.h> #include <MatriKey.h> unsigned char KeyNum; unsigned int True_Password=2470,Password=0,Count=0; void main() { 
     LCD_Init(); LCD_ShowString(1,1,"Password:"); while(1) { 
     KeyNum=MatrixKey(); if(KeyNum>=0 && KeyNum<=9 && Count<4) { 
     Password*=10; //密码左移一位 Password+=KeyNum%10; //获取一位密码 Count++; LCD_ShowNum(2,1,Password,4); //设置四位密码 } if(KeyNum==10) { 
     if(Password==True_Password) LCD_ShowString(1,11,"RIGHT"); else { 
     LCD_ShowString(1,11,"ERROR"); Delay(1000); Password=0; //密码清零 Count=0; //计次清零 LCD_ShowNum(2,1,Password,4); LCD_ShowString(1,11,"AGAIN"); //清空显示ERROR } } if(KeyNum==11) { 
     Password=0; //密码清零 Count=0; //计次清零 LCD_ShowNum(2,1,Password,4); } } } 

9.定时器

9.1 定时器的介绍

定时器是51单片机的内部资源,其电路的连接和运转均在单片机内部完成。

9.1.1 定时器资源

STC89C52RC中含有的定时器个数为3个,即T0、T1、T2,其中T0和T1与传统的51单片机兼容,T2则是该型号的单片机增加的资源。

9.1.2 定时器工作原理

9.1.3 中断系统

9.1.3.1 中断资源
9.1.3.2 中断系统原理图

在这里插入图片描述

9.1.4 定时器的工作模式

9.1.5 模式1状态下的定时器

9.1.5.1 模式1时定时器的连接
9.1.5.2 模式1工作原理

在这里插入图片描述

sysclk:单片机内置时钟。它从晶振中获取脉冲(即晶振周期,看开发板上晶振上的标识)来进行计时。
MCU in 12T/6T mode:分频操作。即输入若是12Hz,经此会经行12/12或者12/6的操作。
C/一横T:选择开关。C/一横T赋值为0,和MCU in 12T/6T mode口相连,进入timer模式,即定时器模式;C/一横T赋值为1,和T0 Pin口相连,进入counter模式,即计数器模式。

sysclk获取脉冲信号,经过MCU in 12T/6T mode进行分频操作,获得处理后的脉冲信号,发送到计数单元,每接收到一个脉冲信号技术单元的数值就加一,当计数单元的数值到达了设定的阈值时,就会通过TF0口,向中断系统发出中断申请,在中断系统中实现中断操作。

9.1.6 定时器相关寄存器

单片机通过配置寄存器来控制内部线路的连接。

9.1.6.1 定时0和1的相关寄存器

在这里插入图片描述

TCON(定时器中断控制寄存器)在这里插入图片描述
TF0:定时器T0溢出中断标志。T0被允许允许计数以后,从初值开始加1计数,当最高位产生溢出时,由硬件置“1”TF0,向CPU发送中断请求,一直保持CPU响应该中断时,才由硬件清“0”TF0。该口只需要检测它的值。
TR0:定时器T0的运行控制位。当TR0=1时允许T0开始计数,TR0=0时禁止T0计数。

定时器工作模式寄存器TMOD
在这里插入图片描述
GATA:控制中断。可以由GATA单独控制,也可以由它和一横INTO共同控制。参看原理图可知,当GATA为0时,为GATA单独控制。(单看电路图的话,这部分涉及与门、或门和非门的知识)
C/一横T:控制定时器用作定时器或计数器,清零则用作定时器 (从内部系统时钟输入),置1用作计数器(从T0/P3.4脚输入)。
M1、M0:用于模式选择。当M1配置为0,M0配置为1时,定时器工作模式为模式1。



需要注意的是,TMOD为不可位寻址,配置时需进行整体赋值。

TL0和TH0、TL1和TH1:计数单元。后面写的是1即为定时器1,后面写的是0即为定时器0。TL0是低八位,TH0是高八位,当低八位计数记满了之后,向高八位进一位。

9.1.6.2 中断寄存器

在这里插入图片描述
在这里插入图片描述

这里是引用
EA:CPU的总中断允许控制位,EA=1,CPU开放中断,EA=0,CPU屏蔽所有的中断申请。
ET和EX:定时器的溢出中断允许位。1为打开

在这里插入图片描述
参考下方:在这里插入图片描述

9.2 按键控制LED流水灯模式

  1. 配置定时0和1的相关寄存器,参考9.1.6.1。
    配置TMOD,调用定时器0。M0给1,M1给0,C/一横T给0,GATE给0。定时器1全部置0。
    配置TCON,TR0给1,TF进行清零操作。
    配置TH0和TL0,STR89C52RC的晶振为11.0952MHZ,即机器周期为1.085微秒,设定计时时间为1ms,则需要计11.0592MHz的机器周期是1.085微秒,所以1ms需要计约为921.个,则计数器溢出差值为约为921,即从64614开始计数。则TH0=64614/256=0xFC,TL0=64614%256=0x66。
    因为两个寄存器TH0、TL0为二进制八位,单独可计256次所以除以256可得高八位得次数,取余就是低八位的次数,合并在一起就是所赋的初始值。




补充
时钟周期:一个时钟脉冲所需要的时间。
机器周期:通常用从内存中读取一个指令字的最短时间来规定CPU周期(机器周期),也即CPU完成一个基本操作所需的时间。通常一个机器周期包含12个时钟周期。


逻辑与运算:全1为1,有0为0。
逻辑或运算:全0为0,有1为1


参考资料
https://blog.csdn.net/weixin_/article/details/?ops_request_misc=%257B%2522request%255Fid%2522%253A%080932%2522%252C%2522scm%2522%253A%3.…%2522%257D&request_id=32&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_click~default-2–null-null.142v100pc_search_result_base8&utm_term=%E9%80%BB%E8%BE%91%E4%B8%8E%E8%BF%90%E7%AE%97&spm=1018.2226.3001.4187

  1. 配置中断寄存器,参考9.1.6.2。
    ET0给1,EA给1,开关闭合。
void Timer0_Init() { 
     // TOMD=0x01; //0000 0001,选中定时器1,置为计时器状态 TOMD=TOMD&0xF0; //把TMOD的低四位清零,高四位保持不变 TOMD=TOMD|0x01; //把TMOD最低位置1,高四位保持不变 TF0=0; //将溢出中断标志清0 TR0=1; //开始计时 TL0 = 0x66; //设置定时初值 TH0 = 0xFC; //设置定时初值  ET0=1; EA=1; //闭合两个总开关 PT0=0; //设置优先级为最低 } 
  1. 编写main函数,打开定时器。
void main() { 
     Timer0_Init(); while(1) { 
     } } 
  1. 当计数溢出时,程序中断,编写中断函数,实现此时要产生的功能。
    当发生中断时TH0和TL0溢出,下一次计时时将从1ms开始,不再是计1ms,所以要对TH0和TL0进行重新赋值,即要将他们的和重新赋值为64614。赋值操作参考第一步。
    引入变量T0Count,每中断一次,即每计满1ms使得T0Count加1,因为1m=1000ms,所以当T0Count为1000时,即为1ms的时间到了。可以设置一个判断语句来判断当前时间是否到了1ms。

void Timer0_Routine() interrupt 1 //溢出时中断 { 
     static int T0Count; TL0 = 0x66; TH0 = 0xFC; T0Count++; if(T0Count>=1000) { 
     T0Count=0; } } 
  1. 按键控制流水灯闪亮方向。对独立按键进行模块化编程,以获取按下按键的键码值。
//此处仅展示Key.c文件 #include <at89c51RC2.h> #include <Delay.h> / * @brief 获取独立按键键码 * @param 无 * @retval 按下按键的键码,范围0~4,无按键按下时返回值为0 */ unsigned char Key() { 
     unsigned int KeyNumber=0; if(P3_1==0){ 
    Delay(20);while(P3_1==0);Dealy(20);KeyNumber=1;} if(P3_0==0){ 
    Delay(20);while(P3_0==0);Dealy(20);KeyNumber=2;} if(P3_2==0){ 
    Delay(20);while(P3_2==0);Dealy(20);KeyNumber=3;} if(P3_3==0){ 
    Delay(20);while(P3_3==0);Dealy(20);KeyNumber=4;} return KeyNumber; } 
  1. 在主函数中调用上一步的函数。
unsigned char KeyNum; void main() { 
     Timer0_Init(); while(1) { 
     KeyNum=Key(); } } 
  1. 实现流水灯功能,可以参考3.3,也可以看下方的新方法。
    调用INTRINS函数库,借助循环左移函数_crol_()和循环右移函数_cror()_实现流水灯。
    在实现移动前,要给LED赋初值,因为LED给0实现点亮,在此的初始状态实现第一个处于点亮状态。

P2=0xFE; P2=_crol_(P2,1); //循环左移一位 P2=_cror_(P2,1); //循环右移一位 
  1. 为实现按键每一次按下一次,流水灯变向一次,可以引入一个变量LEDMod来标记按键按下次数,使得中断时根据LEDMod的值判断流水的进入哪种闪亮模式。

代码呈现:

#include <REGX52.h> #include <Timer0.h> #include <Key.h> #include <INTRINS.h> unsigned char KeyNum,LEDMod; void main() { 
     P2=0xFE; Timer0_Init(); while(1) { 
     KeyNum=Key(); if(KeyNum) { 
     LEDMod++; if(LEDMod>=2) LEDMod=0; } } } void Timer0_Routine() interrupt 1 //溢出时中断 { 
     static int T0Count; TL0 = 0x66; TH0 = 0xFC; T0Count++; if(T0Count>=1000) { 
     T0Count=0; if(LEDMod==1) P2=_crol_(P2,1); //循环左移一位 if(LEDMod==0) P2=_cror_(P2,1); //循环右移一位 } } 

9.3 时钟

  1. 调用LCD1602,添加LCD1602相关头文件,可以参考7.2。
    调用第一步进行初始化。
    调用相关函数,在LCD1602上显示提示信息Clock:。

#include <LCD1602.h> void main() { 
     LCD_Init(); LCD_ShowString(1,1,"Clock:"); while(1) { 
     } } 
  1. 调用定时器,添加定时器相关头文件,参考9.2。
    第一步对定时器进行初始化,设置中断函数,参考9.2。
    引入一个变量sec,用于记录秒数。程序每中断一次sec加1。
    秒数增加后,更新显示值。相关函数不要放在中断函数中,因为中断函数适合处理时间较短的任务,显示值在整个时钟程序中都在发生变化,任务时间很长,建议放在主函数中处理。


#include <at89c51RC2.h> #include <LCD1602.h> #include <Timer0.h> unsigned char sec; void main() { 
     LCD_Init(); Timer0_Init(); LCD_ShowString(1,1,"Clock:"); while(1) { 
     LCD_ShowNum(2,1,sec,2); } } void Timer0_Routine() interrupt 1 //溢出时中断 { 
     static int T0Count; TL0 = 0x66; TH0 = 0xFC; T0Count++; if(T0Count>=1000) { 
     T0Count=0; sec++; //秒数加1 } } 
  1. 引入变量min和hour来记录分钟和小时。以min为例,对sce的大小进行判断,当sec等于60时,min+1,sec清零。hour类似。
if(sec>=60) { 
     sec=0; min++; if(min>=60) { 
     min=0; hour++; if(hour>=24) { 
     hour=0; } } } 
//此处只呈现main.c文件 #include <at89c51RC2.h> #include <LCD1602.h> #include <Timer0.h> unsigned char sec,min,hour; void main() { 
     LCD_Init(); Timer0_Init(); LCD_ShowString(1,1,"Clock:"); LCD_ShowString(2,1," : :"); while(1) { 
     LCD_ShowNum(2,1,hour,2); LCD_ShowNum(2,4,min,2); LCD_ShowNum(2,7,sec,2); } } void Timer0_Routine() interrupt 1 //溢出时中断 { 
     static int T0Count; TL0 = 0x66; TH0 = 0xFC; T0Count++; if(T0Count>=1000) { 
     T0Count=0; sec++; //秒数加1 if(sec>=60) { 
     sec=0; min++; if(min>=60) { 
     min=0; hour++; if(hour>=24) { 
     hour=0; } } } } } 

10.串口通信

10.1 串口介绍

10.1.1 硬件电路

  • TXD和RXD要交叉连接。因为从一个设备的发送端发送数据,对于另一个设备而言是输入值,要从接收端进入。
  • 当电平标准不一致时,需要加电平转换芯片。

电平标准:数据1和数据0的表达方式,是传输线缆中认为规定的电压与数据的对应关系。

串口常用的电平标准:

  1. TTL电平(单片机用):+5V 表示1,0V 表示0。
  2. RS232电平:-3 ~ -15V 表示1,+3 ~ +15V 表示0。
  3. RS485电平:两线压差+2 ~ +6V表示1, -2 ~ -6V 表示0。

10.1.2 常见的通信接口比较

名称 引脚定义 通信方式 特点
UART TXD、RXD 全双工、异步 点对点通信
I^2C SCL、SDA 半双工、同步 可挂载多个设备
SPI SCLK、MOSI、MISO、CS 全双工、同步 可挂载多个设备
1-Wire DQ 半双工、异步 可挂载多个设备

此外还有:CAN(多用于汽车)、USB等

相关术语

  1. 全双工:通信双方可以同一时刻互相传输数据。例如,手机。
    半双工:通信双方可以互相传输数据,但必须分时复用同一根数据线。例如,对讲机。
    单工:通信双方只能有一方发送到另一方,不能反向传输。例如,遥控器。

  2. 异步:通信双方各自约定通信速率。
    同步:通信双方靠一根时钟线来约定通信速率。
  3. 总线:连接各个设备的数据传输线路。

10.1.3 UART的四种工作模式

10.1.4 串口通信原理图

请添加图片描述

10.1.4 串口参数以及时序图

波特率:串口通信的速率(发送和接收各数据位的间隔时间)
检验位:用于数据验证
停止位:用于数据帧间隔
在这里插入图片描述


10.1.5 串口模式图

在这里插入图片描述
左边为总线,数据只要到了总线,CPU才能进行相关操作。
数据通过TXD发出;数据通过RXD进入。

SBUF:串口数据寄存器,物理上是两个单独的寄存器,但占用相同的地址。写操作时,写入的是发送寄存器,读操作时,独处的是接收寄存器。

10.1.6 串口相关寄存器

在这里插入图片描述
在这里插入图片描述

SCON:串行控制寄存器这里是引用
SM0/SM1:SM0/FE:当PCON寄存器中的SMOD0/PCON.6位为1时,该位用于帧错误检测。当检测到一个无效停止位时,通过UART接收器设置该位。它必须由软件清零。
当PCON寄存器中的SMOD0/PCON.6位为0时,该位和SM1一起指定串行通信的工作 方式,如下表所示:在这里插入图片描述
SM2:允许方式2或方式3多机通信控制位。在方式1和方式0中,SM2配置为0。
REN:允许/禁止串行接收控制位。由软件置位REN,即REN=1为允许串行接收状态,可启动串行接收器RxD,开始接收信息。软件复位REN,即REN=0,则禁止接收。
TB8:在方式2或方式3,它为要发送的第9位数据,按需要由软件置位或清0。
RB8:在方式2或方式3,是接收到的第9位数据。
TI:发送中断标志。发送结束,内部硬件自动将T1置1,向主机请求中断,相应结束后,必须用软件复位T1,使T1置0。
RI:接收中断请求标志。和T1类似。







PCON:电源控制寄存器这里是引用
SMOD:波特率选择位。当SMOD为1时,波特率加倍。
SMOD0:帧错误检测有效控制位。当SMOD0为1时,开启真错误检测。

10.2 串口向电脑发送数据

  1. 参考10.1.6,给串口进行初始化配置,配置为模式1。
    注意,在配置波特率时,需要调用定时器,此处对于计时器只能调用计时器1,而且要配置为八位重装模式,参考9和手册。也可以直接用stc-isp中生成。但是需要注意的是STC89C52系列单片机中无AUXR,所以对于生成代码需要删除AUXR。
    在这里插入图片描述

void UartInit(void) //4800bps@11.0592MHz { 
     PCON &= 0x7F; //波特率不倍速 SCON = 0x50; //8位数据,可变波特率 TMOD &= 0x0F; //清除定时器1模式位 TMOD |= 0x20; //设定定时器1为8位自动重装方式 TL1 = 0xFA; //设定定时初值 TH1 = 0xFA; //设定定时器重装值 ET1 = 0; //禁止定时器1中断 TR1 = 1; //启动定时器1 } 
  1. 写数据发送的函数。
    在SBUF中写入需要发送的数据。
    通过发送完成标志位检测是否完成发送,若是已经完成发送,则重新置0。

void Uart_SendByte(unsigned char Byte) { 
     SBUF=Byte; while(TI==0); TI=0; } 
  1. 发送不断增加的数。
    引入变量sec,每循环循环一次加一。
unsigned char sec; void main() { 
     UartInit(); while(1) { 
     Uart_SendByte(sec); sec++; } } 
  1. 用sct-isp自带的串口助手可以接收数据。
    使用时,需要确保串口是扫描出来的,需要调整波特率与代码中保持一致,检查确定无误后,烧录代码,打开串口。
    在这里插入图片描述

  2. 显示速度太快,且有误差。
    在每一次输出之后都进行一下延迟。
void main() { 
     UartInit(); while(1) { 
     Uart_SendByte(sec); sec++; Delay(100); } } 

显示最开始是02,而不是00?
解决方案:按一下复位。

代码呈现:

#include <at89c51RC2.h> #include <Delay.h> unsigned char sec; void UartInit(void) //4800bps@11.0592MHz { 
     PCON &= 0x7F; //波特率不倍速 SCON = 0x50; //8位数据,可变波特率 TMOD &= 0x0F; //清除定时器1模式位 TMOD |= 0x20; //设定定时器1为8位自动重装方式 TL1 = 0xFA; //设定定时初值 TH1 = 0xFA; //设定定时器重装值 ET1 = 0; //禁止定时器1中断 TR1 = 1; //启动定时器1 } void Uart_SendByte(unsigned char Byte) { 
     SBUF=Byte; while(TI==0); TI=0; } void main() { 
     UartInit(); while(1) { 
     Uart_SendByte(sec); sec++; Delay(100); } } 

10.3 电脑通过串口控制LED

  1. 参考10.2,与10.2不同的是,当电脑电脑通过串口向单片机发送数据时,我们需要它有一个中断可以处理传输过来的数据,所以在初始化时,需要对初始化函数添加一个对于串口的中断。
void UartInit_Device(void) //4800bps@11.0592MHz { 
     PCON &= 0x7F; //波特率不倍速 SCON = 0x50; //8位数据,可变波特率 TMOD &= 0x0F; //清除定时器1模式位 TMOD |= 0x20; //设定定时器1为8位自动重装方式 TL1 = 0xFA; //设定定时初值 TH1 = 0xFA; //设定定时器重装值 ET1 = 0; //禁止定时器1中断 TR1 = 1; //启动定时器1 EA=1; ES=1; //设置串口开放中断 } 
  1. 查询中断号,编写中断服务子函数。
    判断当前状态是否为电脑发送完成,如果电脑发送完成了,那么单片机接收中断,可以进行后续操作。
    电脑通过串口控制LED,即电脑传输至CPU的数据控制LED,数据传输完成后,要对LED灯进行操作,LED灯对应P2,根据需要的点亮状态,对P2进行赋值。
    LED灯点亮的同时,单片机给电脑一个返回值,通过Uart_SendByte函数实现。参考10.1.6,操作完成后要对RI进行重置。


void UART_Routine(void) interrupt 4 { 
     if(RI==1) //单片机接收中断 { 
     P2=SBUF; Uart_SendByte(SBUF); RI=0; } } 

代码呈现:

//在此只呈现main.c文件的内容 #include <at89c51RC2.h> #include <Delay.h> #include <UART.h> unsigned char sec; void main() { 
     UartInit_Device(); while(1) { 
     } } void UART_Routine(void) interrupt 4 { 
     if(RI==1) //单片机接收中断 { 
     P2=SBUF; Uart_SendByte(SBUF); RI=0; } } 

11.LED点阵屏

11.1 LED点阵屏原理图

在这里插入图片描述
选中列P0?给0。
选中行,要调用74HC595模块,实现DP?输出为1。
在这里插入图片描述



11.2 LED点阵显示图形

  1. 编写函数,先对74HC595进行操作。
    引入输入值变量Byte,一次性输入八位,对sec进行赋值。参考11.1,SER先取最高位,可以用逻辑与。因为SER只存储一位数据,所以当Byte进行逻辑与运算时,SER最后得到的值非0即1,则SER实际得到的值是Byte与给出的数进行逻辑与运算后是否相同的判断值,若是相同则为1,若是不同则为0,以此取得Byte最高位。
    数据存入后,要用SERCLK使得数据下移,为下一位数据的存入腾出空间。
    如此循环八次,可以用for循环进行优化。
    八位数字全部获取后,用SRCLK实现输出。
    需要注意的是,为了避免针脚在调用前已经有非0值,需要在调用该针脚前进行清零操作,可以在主函数中进行。
    实现以上操作可以选中行。





void _74HC595_WriteByte(unsigned char Byte) //写入数据 { 
     unsigned char i; for(i=0;i<8;i++) { 
     P3_4=Byte&(0x80>>i); //依次取Byte各位值赋给SER P3_6=1; //一横SRCLR,实现数据下行 P3_6=0; } P3_5=1; //SRCLK,实现输出 P3_5=0; } 
  1. 实现选中列,对P0进行操作,可以参考5.2,也可以用移位进行优化。
void MatrixLED_ShowColumn(unsigned char Column) //选中行 { 
     P0=~(0x80>>Column); } 

代码呈现

#include <at89c51RC2.h> #include <Delay.h> void _74HC595_WriteByte(unsigned char Byte) //写入数据 { 
     unsigned char i; for(i=0;i<8;i++) { 
     P3_4=Byte&(0x80>>i); //依次取Byte各位值赋给SER P3_6=1; //一横SRCLR,实现数据下行 P3_6=0; } P3_5=1; //SRCLK,实现输出 P3_5=0; } void MatrixLED_ShowColumn(unsigned char Column,Data) //选中行,列 { 
     _74HC595_WriteByte(Data); P0=~(0x80>>Column); Delay(1); P0=0xFF; } void main() { 
     P3_6=0; P3_5=0; while(1) { 
     MatrixLED_ShowColumn(0,0x3C); MatrixLED_ShowColumn(1,0x42); MatrixLED_ShowColumn(2,0xA9); MatrixLED_ShowColumn(3,0x85); MatrixLED_ShowColumn(4,0x85); MatrixLED_ShowColumn(5,0xA9); MatrixLED_ShowColumn(6,0x42); MatrixLED_ShowColumn(7,0x3C); } } 

没亮
调一下gnd oe vcc的一个插头

11.3 LED点阵显示动画

  1. 引入数组来存放需要显示的数据。
    可以用文字取模软件得到数据。
    定义for循环实现逐个获取。

unsigned char Animatio[]={ 
    0xFF,0x08,0x08,0x08,0xFF,0x00,0x5F,0x00,0xFD}; void main() { 
     unsigned int i; MatrixLED_Init(); while(1) { 
     for(i=0;i<8;i++) MatrixLED_ShowColumn(i,Animatio[i+offset]); } } 
  1. 实现字符移动,引入一个变量offset记录偏移量。
#include <at89c51RC2.h> #include <MatrixLED.h> unsigned char Animatio[]={ 
    0xFF,0x08,0x08,0x08,0xFF,0x00,0x5F,0x00,0xFD}; void main() { 
     unsigned int i,offset=0,count=0; MatrixLED_Init(); while(1) { 
     for(i=0;i<8;i++) MatrixLED_ShowColumn(i,Animatio[i+offset]); offset++; } } 
  1. 上一步的显示移动太快看不清楚,加入Dealy实现延时。
#include <at89c51RC2.h> #include <MatrixLED.h> #include <Delay.h> unsigned char Animatio[]={ 
    0xFF,0x08,0x08,0x08,0xFF,0x00,0x5F,0x00,0xFD}; void main() { 
     unsigned int i,offset=0,count=0; MatrixLED_Init(); while(1) { 
     for(i=0;i<8;i++) MatrixLED_ShowColumn(i,Animatio[i+offset]); Delay(100); offset++; } } 
  1. 上一步的显示,字符闪亮。因为,扫描一遍全部字符之后,下一次的扫描在延迟之后,中间处于无显示状态,所以,我们可以引入一个变量count记录扫描次数,使得持续扫描显示点阵的一个状态。
#include <at89c51RC2.h> #include <MatrixLED.h> unsigned char Animatio[]={ 
    0xFF,0x08,0x08,0x08,0xFF,0x00,0x5F,0x00,0xFD}; void main() { 
     unsigned int i,offset=0,count=0; MatrixLED_Init(); while(1) { 
     for(i=0;i<8;i++) MatrixLED_ShowColumn(i,Animatio[i+offset]); count++; if(count==10) { 
     offset++; count=0; } } } 
  1. 上一步完成了显示的移动,但是在显示的字符中我们可以看到,末尾移动的是乱码,这是因为,偏移量后取的位置超出了数组包含字符的数量。
    此时数组里一共10个数据,点阵屏一次可以显示8个数据,可以当偏移值为10-8=2时,不再偏移。
#include <at89c51RC2.h> #include <MatrixLED.h> unsigned char Animatio[]={ 
     0xFF,0x08,0x08,0x08,0xFF,0x00,0x5F, 0x00,0xFD }; void main() { 
     unsigned int i,offset=0,count=0; MatrixLED_Init(); while(1) { 
     for(i=0;i<8;i++) MatrixLED_ShowColumn(i,Animatio[i+offset]); count++; if(count==10) { 
     offset++; count=0; if(offset>=2) { 
     offset=0; } } } } 
  1. 字符显示完直接就跳回了首位,显示状态没有衔接,更改数组中元素。
    如果数组中数据太多,可以将其定义为code形式。
    代码呈现

#include <at89c51RC2.h> #include <MatrixLED.h> unsigned char code Animatio[]={ 
     0x00,0x00,0x00,0x00,0x00,0x00,0x00, 0xFF,0x08,0x08,0x08,0xFF,0x00,0x5F, 0x00,0xFD,0x00,0x00,0x00,0x00,0x00 }; void main() { 
     unsigned int i,offset=0,count=0; MatrixLED_Init(); while(1) { 
     for(i=0;i<8;i++) MatrixLED_ShowColumn(i,Animatio[i+offset]); count++; if(count==10) { 
     offset++; count=0; if(offset>=16) { 
     offset=0; } } } } 

12.DS1302

12.1 DS1302介绍

12.1.1 DS1302应用电路

在这里插入图片描述

VCC2:主电源
VCC1:备用电池
GND:电源地
X1、X2:32.768KHz晶振
CE:芯片使能
IO:数据输入/输出
SCLK:串行时钟





12.1.2 DS1302原理图

在这里插入图片描述

12.1.3 DS1302中时钟相关寄存器

在这里插入图片描述

12.1.4 DS1302中命令字

在这里插入图片描述

位 7:必须是逻辑 1. 如果是 0, 则禁止对 DS1302写入。
位 6: 在逻辑 0时规定为时钟/日历数据,逻辑 1时为 RAM数据。
位 1 至 位 5: 表示了输入输出的指定寄存器。
位 0: 在逻辑0时为写操作,逻辑1时为读操作.命令字以 LSB (位 0)开始总是输入。


12.1.5 DS1302时序定义

在这里插入图片描述

12.2 时钟显示

  1. 依靠LCD1602进行显示,参考7,调用相关代码。
  2. 为使对引脚的调用更加明确,可以对相关引脚经行重新的定义。
sbit DS1302_SCLK=P3^6; sbit DS1302_IO=P3^4; sbit DS1302_CE=P3^5; 
  1. 对DS1302进行初始化,参考12.1.5可知,在没运行的时候,CE和SCLK处于0状态。
void DS1302_Init(void) { 
     DS1302_CE=0; DS1302_SCLK=0; } 
  1. 对DS1302进行写入操作。
    引入变量Command存储命令字,Data存储输入值。
    参考12.1.5,DS1302运行时,CE处于1。
    对于第一位操作时,IO口获取输入的最小位,SCLK进行了从0到1到0的变化,此后SCLK变化相同,IO口获取输入的位置逐级往前一位。
    写入完成后,将CE重新置于0。



void DS1302_WriteByte(unsigned char Command,Data) { 
     unsigned char i; DS1302_CE=1; for(i=0;i<8;i++) { 
     DS1302_IO=Command&(0x01<<i); DS1302_SCLK=1; DS1302_SCLK=0; } for(i=0;i<8;i++) { 
     DS1302_IO=Data&(0x01<<i); DS1302_SCLK=1; DS1302_SCLK=0; } DS1302_CE=0; } 
  1. 完成输入后,要进行读取才能呈现。
    参考12.1.5,写入比读取多以一个脉冲,当运行到第八位时,到对应脉冲的下降沿立即输出,进入第二个数据的第一位(即输出的第一位),但是此时的输出值我们并没有获取,所以实际输出时跳过了这个值 。所以,在进入输出的第一位数据时,要及时暂停,获取要输出的内容。对此可以调整SCLK。
unsigned char DS1302_ReadByte(unsigned char Command) { 
     unsigned char i,Data=0x00; DS1302_CE=1; for(i=0;i<8;i++) { 
     DS1302_IO=Command&(0x01<<i); DS1302_SCLK=0; DS1302_SCLK=1; } for(i=0;i<8;i++) { 
     DS1302_SCLK=1; DS1302_SCLK=0; if(DS1302_IO){ 
    Data|=(0x01<<i)}; } DS1302_CE=0; DS1302_IO=0; return(Data); } 
  1. 在主函数中调用函数。
    先对LCD1602进行操作,输出提示字。
    参考12.1.4,进行写操作时,位0为0,位6为0,位7为1。进行读操作时位0为1,位6为0,位7为1。
    参考12.1.3,对秒、分、时进行设定。
    用LCD1602呈现结果。



显示结果到9之后直接跳转到16,因为在DS1302中采用BCD码(用四位二进制数来表示1位十进制数,可以简单的理解为取去除了abcd等字母的十六进制)存储,所以我们还需要了解BCD码和十进制之间的转换关系:
1.BCD码转十进制
DEC=BCD/16*10+BCD%16(2位)

2.十进制换BCD码
BCD=DEC/10*16+DEC%10(2位)

代码显示:

#include <at89c51RC2.h> #include <LCD1602.h> #include <DS1302.h> void main() { 
     LCD_Init(); DS1302_Init(); LCD_ShowString(1,1," - - "); LCD_ShowString(2,1," : : "); DS1302_SetTime(); while(1) { 
     DS1302_ReadTime(); LCD_ShowNum(1,1,DS1302_Time[0],2); LCD_ShowNum(1,4,DS1302_Time[1],2); LCD_ShowNum(1,7,DS1302_Time[2],2); LCD_ShowNum(2,1,DS1302_Time[3],2); LCD_ShowNum(2,4,DS1302_Time[4],2); LCD_ShowNum(2,7,DS1302_Time[5],2); LCD_ShowNum(1,10,DS1302_Time[6],2); } } 

13.蜂鸣器

13.1 基础介绍

13.1.1 蜂鸣器

P25控制BEEP的输出。

在这里插入图片描述

BEEP输出为低电平时,蜂鸣器工作。

13.1.2 乐理

要用蜂鸣器实现音乐的输出,必须要了解相关乐理中对于时长和音高的知识。

13.1.2.1 音高
13.1.2.2 时长

在这里插入图片描述
全音符占2000s,二分音符占1000s,四分音符占500ms,以此类推。

音符后加了一个附点是延长该音符时长的一半。

13.1.2.3 对照表

13.2 蜂鸣器播放提示音

  1. 通过独立按键控制数码管的显示。引入独立按键和数码管显示的相关头文件,参考4和5。
    引入变量KeyNum记录按下的独立按键的编号。
    调用Nixie函数实现数码管显示。

#include <at89c51RC2.h> #include <Key.h> #include <Nixie.h> unsigned char KeyNum; void main() { 
     Nixie(1,0); while(1) { 
     KeyNum=Key(); if(KeyNum) { 
     Nixie(1,KeyNum); } } } 
  1. BEEP取反蜂鸣器发声。
#include <at89c51RC2.h> #include <Key.h> #include <Nixie.h> #include <Delay.h> sbit BEEP=P2^5; unsigned char KeyNum,i; void main() { 
     Nixie(1,0); while(1) { 
     KeyNum=Key(); if(KeyNum) { 
     BEEP=!BEEP; Nixie(1,KeyNum); } } } 
  1. 蜂鸣器发声时间太短,用for循环和delay实现延长发声时间。

代码呈现

#include <at89c51RC2.h> #include <Key.h> #include <Nixie.h> #include <Delay.h> sbit BEEP=P2^5; unsigned char KeyNum,i; void main() { 
     Nixie(1,0); while(1) { 
     KeyNum=Key(); if(KeyNum) { 
     for(i=0;i<100;i++) { 
     BEEP=!BEEP; Delay(1); } Nixie(1,KeyNum); } } } 

13.3 蜂鸣器播放音乐

  1. 借助定时器的中断实现音符间的切换。
    引入相关定时器函数,参考9。调用定时器第一步进行初始化,后设置中断函数。
    在中断函数中,BEEP取反实现响应。

#include <at89c51RC2.h> #include <Timer0.h> #include <Delay.h> sbit BEEP=P2^5; void main() { 
     Timer0_Init(); while(1) { 
     } } void Timer0_Routine() interrupt 1 //溢出时中断 { 
     TL0 = 0x66; TH0 = 0xFC; BEEP=!BEEP; } 
  1. 建立数组,加入字符对应的重装载值。
//三组 1 1# 2 2# 3 4 4# 5 5# 6 6# 7 一行12个 unsigned int FreqTable[]={ 
     63777,63872,63969,64054,64140,61216,64291,64360,64426,64489,64547,64607, 64655,64704,64751,64795,64837,64876,64913,64948,64981,65012,65042,65070, 65095,65120,65144,65166,65186,65206,65255,65242,65259,65274,65289,65303 } ; 
  1. 参考乐谱,建立数组存储音符对应的重装载值的位置。
    在这里插入图片描述
unsigned int Music[]={ 
    12,12,19,19,21,21,19,17,17,16,16,14,12}; 
  1. 引入变量FreqSelect用来获取Music[]中的数。
    引入变量MusicSelect标记当前获取的Music的位置。
FreqSelect=Music[MusicSelect]; MusicSelect++; 
#include <at89c51RC2.h> #include <Timer0.h> #include <Delay.h> sbit BEEP=P2^5; //三组 1 1# 2 2# 3 4 4# 5 5# 6 6# 7 一行12个 unsigned int FreqTable[]={ 
     63777,63872,63969,64054,64140,61216,64291,64360,64426,64489,64547,64607, 64655,64704,64751,64795,64837,64876,64913,64948,64981,65012,65042,65070, 65095,65120,65144,65166,65186,65206,65255,65242,65259,65274,65289,65303 } ; unsigned int Music[]={ 
    12,12,19,19,21,21,19,17,17,16,16,14,12}; unsigned int FreqSelect,MusicSelect; void main() { 
     Timer0_Init(); while(1) { 
     FreqSelect=Music[MusicSelect]; MusicSelect++; Delay(500); } } void Timer0_Routine() interrupt 1 //溢出时中断 { 
     TL0 = FreqTable[FreqSelect]%256; TH0 = FreqTable[FreqSelect]/256; BEEP=!BEEP; } 
  1. 上一步写出的代码经过调试发现,字符都连载一起了,为了使得每个字符之间有停顿,可以暂时关闭定时器再打开。
TR0=0; Delay(5); TR0=1; 
  1. 对于延长的处理。
    引入数组Music_Time存储字符响的时间。
    引入新的变量Music_TimeSelect标记当前获取的Music_Time的位置。

unsigned char Music_Time[]={ 
    500,500,500,500,500,500,1000,500,500,500,500,500,500,1000}; unsigned int Music_TimeSelect; 

代码呈现:

#include <at89c51RC2.h> #include <Timer0.h> #include <Delay.h> sbit BEEP=P2^5; //三组 1 1# 2 2# 3 4 4# 5 5# 6 6# 7 一行12个 unsigned int FreqTable[]={ 
     63777,63872,63969,64054,64140,61216,64291,64360,64426,64489,64547,64607, 64655,64704,64751,64795,64837,64876,64913,64948,64981,65012,65042,65070, 65095,65120,65144,65166,65186,65206,65255,65242,65259,65274,65289,65303 } ; unsigned int Music[]={ 
    12,12,19,19,21,21,19,17,17,16,16,14,14,12}; unsigned char Music_Time[]={ 
    500,500,500,500,500,500,1000,500,500,500,500,500,500,1000}; unsigned int FreqSelect,MusicSelect,Music_TimeSelect; void main() { 
     Timer0_Init(); while(1) { 
     if(Music[MusicSelect]!=0xFF && Music_Time[Music_TimeSelect]!=0xFF) { 
     FreqSelect=Music[MusicSelect]; MusicSelect++; Delay(Music_Time[Music_TimeSelect]); Music_TimeSelect++; TR0=0; Delay(5); TR0=1; } else { 
     TR0=0; while(1); } } } void Timer0_Routine() interrupt 1 //溢出时中断 { 
     TL0 = FreqTable[FreqSelect]%256; TH0 = FreqTable[FreqSelect]/256; BEEP=!BEEP; } 

14.AT24C02(I2C总线)

14.1 介绍

14.1.1 存储器的介绍

在这里插入图片描述

RAM:易失性存储器,存储速度快,断电丢失。
SRAM(静态RAM):D触发器,用电路储存。存储速度最快。
DRAM(动态RAM):用电容存储数据,需要搭配扫描电路。
ROM:非易失性存储器,存储速度慢,断电不丢失。
Mask ROM(掩膜ROM):最早的ROM,只能读不能写。
PROM(可编程ROM):基于Mask ROM升级后的ROM,解决了只能读不能写的问题,但是只能编写一次。
EPROM(可擦除可编程ROM):基于PROM升级后的ROM,紫外线照射30分钟可以实现擦除。
E2PROM(电可擦除可编程ROM):基于EPROM升级后的ROM,可以依靠电信号擦除,相对于EPROM更加方便。
Flash(闪存)
硬盘、软盘、光盘








地址总线:横向的线。
数据总线:纵向的线。
从地址总线输入数据,根据结点的连接情况不同,数据总线得到的数据不同。

14.1.2 AT24C02

14.1.2.1 介绍

AT24C02是掉电不丢失的存储器,它的存储介质是E2PROM,通讯接口是I2C总线,容量为256字节。

14.1.2.2 引脚及应用电路

在这里插入图片描述

在这里插入图片描述

VCC、GND(8号和4号):电源(1.8V~5.5V)
WP(7号):写保护。但是在此电路中直接接到了GND上,可以不用再配置。
SCL、SDA:I2C接口。
A0、A1、A2(1号2号3号):I2C地址。


14.1.3 I2C总线

14.1.3.1 介绍
14.1.3.2 I2C电路规范
  • 所有I2C设备的SCL连载一起,SDA连接在一起。
  • 设备的SCL和SDA均要配置成开漏输出模式。
  • SCL和SDA各添加一个上拉电阻,阻值一般为千欧级。
  • 开漏输出和上拉电阻的共同作用实现了“线与”的功能,此设计主要是为了解决多机通信互相干扰的问题。

在这里插入图片描述

14.1.3.3 I2C时序结构

在这里插入图片描述

起始条件:SCL高电平期间,SDA从高电平切换到低电平。SDA切换完成后,把SCL也切换到低电平状态,可以避免时序的混乱。
终止条件:SCL高电平期间,SDA从低电平切换到高电平。SCL先从低电平切换到高电平,切换完成后,再切换SDA,可以避免时序的混乱。

在这里插入图片描述

发送一个字节:在SCL低电平期间,主机将数据位依次放到SDA线上(高位在前),然后拉高SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。


简单来说,SCL会变化,高电位读取SDA数据一位,此时SDA保持稳定。SCL低位时,不读取,此时SDA需要改变数据,以供下轮读取。

在这里插入图片描述

接收一个字节:在SCL低电平期间,从机将数据位依次放到SDA线上(高位在前),然后拉高SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节(主机在接收之前,需要释放SDA)。

在这里插入图片描述

发送应答:在接收完一个字节后,主机在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答。
接收应答:在发送完一个字节之后,主机在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)

14.1.3.4 I2C数据帧

在这里插入图片描述

发送一帧数据:由初始状态开始。再发送一个字节数据,这个字节数据必须是从机地址(前四位固定,STC89C52前四位是1010)+读写位(1是读,0是写)。每发送一个字节都要跟一个接收应答。最后是终止条件。

在这里插入图片描述

接收一帧数据:由初始状态开始。再发送一个字节数据,这个字节数据必须是从机地址(前四位固定,STC89C52前四位是1010)+读写位(1是读,0是写)。每接收一个字节都要跟一个发送应答,最后一个应答一般为非应答(SA:1)。最后是终止条件。

在这里插入图片描述

复合格式:由初始状态开始。再发送一个字节数据,这个字节数据必须是从机地址(前四位固定,STC89C52前四位是1010)+读写位(1是读,0是写)。每发送一个字节都要跟一个接收应答。再发送一个字节数据,这个字节数据必须是从机地址(前四位固定,STC89C52前四位是1010)+读写位(1是读,0是写)。每接收一个字节都要跟一个发送应答,最后一个应答一般为非应答(SA:1)。最后是终止条件。

14.1.4 AT24C02数据帧

字节写:在WORD ADDRESS处写入数据DATA。
在这里插入图片描述

在这里插入图片描述

随机读:读出在WORD ADDRESS处的数据DATA。
在这里插入图片描述

AT24C02的固定地址为1010,可配置地址本开发板上为000,所以SLAVE ADDRESS+W为0xA0,SLAVE ADDRESS+R为0xA1。

14.2 AT24C02显示

  1. 模块化编程建立I2C和AT24C02相关的.c和.h文件。
  2. 在I2C中
#include <REGX52.H> sbit I2C_SCL=P2^1; sbit I2C_SDA=P2^0; / * @brief I2C开始 * @param 无 * @retval 无 */ void I2C_Start(void) { 
     I2C_SDA=1; I2C_SCL=1; I2C_SDA=0; I2C_SCL=0; } / * @brief I2C停止 * @param 无 * @retval 无 */ void I2C_Stop(void) { 
     I2C_SDA=0; I2C_SCL=1; I2C_SDA=1; } / * @brief I2C发送一个字节 * @param Byte 要发送的字节 * @retval 无 */ void I2C_SendByte(unsigned char Byte) { 
     unsigned char i; for(i=0;i<8;i++) { 
     I2C_SDA=Byte&(0x80>>i); I2C_SCL=1; I2C_SCL=0; } } / * @brief I2C接收一个字节 * @param 无 * @retval 接收到的一个字节数据 */ unsigned char I2C_ReceiveByte(void) { 
     unsigned char i,Byte=0x00; I2C_SDA=1; for(i=0;i<8;i++) { 
     I2C_SCL=1; if(I2C_SDA){ 
    Byte|=(0x80>>i);} I2C_SCL=0; } return Byte; } / * @brief I2C发送应答 * @param AckBit 应答位,0为应答,1为非应答 * @retval 无 */ void I2C_SendAck(unsigned char AckBit) { 
     I2C_SDA=AckBit; I2C_SCL=1; I2C_SCL=0; } / * @brief I2C接收应答位 * @param 无 * @retval 接收到的应答位,0为应答,1为非应答 */ unsigned char I2C_ReceiveAck(void) { 
     unsigned char AckBit; I2C_SDA=1; I2C_SCL=1; AckBit=I2C_SDA; I2C_SCL=0; return AckBit; } 
  1. 在AT24C02中
#include <at89c51RC2.h> #include <I2C.h> #define AT24C02_ADDRESS_W 0xA0  #define AT24C02_ADDRESS_R 0xA1  / * @brief AT24C02写入一个字节 * @param WordAddress 字节写入的地址 * @param Data 要写入的数据 * @retval 无 */ void AT24C02_WriteByte(unsigned char WordAddress,Data) { 
     I2C_Start(); I2C_SendByte(AT24C02_ADDRESS_W); I2C_ReceiveAck(); I2C_SendByte(WordAddress); I2C_ReceiveAck(); I2C_SendByte(Data); I2C_ReceiveAck(); I2C_Stop(); } / * @brief AT24C02读取一个字节 * @param WordAdress 要读出的字节的地址 * @retval Data 读出的数据 */ unsigned char AT24C02_ReadByte(unsigned char WordAddress) { 
     unsigned char Data; I2C_Start(); I2C_SendByte(AT24C02_ADDRESS_W); I2C_ReceiveAck(); I2C_SendByte(WordAddress); I2C_ReceiveAck(); I2C_Start(); I2C_SendByte(AT24C02_ADDRESS_R); I2C_ReceiveAck(); Data=I2C_ReceiveByte(); I2C_SendAck(1); I2C_Stop(); return Data; } 
  1. 在主函数中,调用相关函数经行写入和读取。
    引入变量Data,存储读出的值。
    用LCD1602显示在AT2402中存储的值。

#include <at89c51RC2.h> #include <LCD1602.h> #include <AT24C02.h> unsigned char Data; void main() { 
     LCD_Init(); AT24C02_WriteByte(0,66); Data=AT24C02_ReadByte(0); LCD_ShowNum(1,1,Data,3); while(1) { 
     } } 
  1. 按照上一步,并不能实现显示,这是因为AT24C02的写周期为5ms,但是按照上一步直接就读出了,所以读不到,应该写完后加一个延迟。
#include <at89c51RC2.h> #include <LCD1602.h> #include <AT24C02.h> #include <Delay.h> unsigned char Data; void main() { 
     LCD_Init(); AT24C02_WriteByte(0,66); Delay(5); Data=AT24C02_ReadByte(0); LCD_ShowNum(1,1,Data,3); while(1) { 
     } } 
  1. 通过独立按键控制数字的加减,并且实现写入和读出。
    引入独立按键模块,参考4。
    引入变量KeyNum标记按下的按键的键码值,引入变量Num记录实时的数据大小。
    第一个独立按键控制数字加。当
    第二个独立按键控制数字减。
    第三个按键控制数据写入。因为Num是16位数据,而AT24C02中的寄存器是8位的,所以要把Num的高八位和第八位拆开来存储。
    第四个独立按键控制数据读取。





代码呈现:

#include <at89c51RC2.h> #include <LCD1602.h> #include <Key.h> #include <AT24C02.h> #include <Delay.h> unsigned char KeyNum; unsigned int Num; void main() { 
     LCD_Init(); LCD_ShowNum(1,1,Num,5); while(1) { 
     KeyNum=Key(); if(KeyNum==1) { 
     Num++; LCD_ShowNum(1,1,Num,5); } if(KeyNum==2) { 
     Num--; LCD_ShowNum(1,1,Num,5); } if(KeyNum==3) { 
     AT24C02_WriteByte(0,Num%256); //取出Num的低八位 Delay(5); AT24C02_WriteByte(1,Num/256); //取出Num的高八位 Delay(5); LCD_ShowString(2,1,"Write success!"); Delay(1000); LCD_ShowString(2,1," "); } if(KeyNum==4) { 
     Num=AT24C02_ReadByte(0); //获取低八位 Num|=AT24C02_ReadByte(1)<<8; //获取高八位 LCD_ShowNum(1,1,Num,5); LCD_ShowString(2,1,"Read success!"); Delay(1000); LCD_ShowString(2,1," "); } } } 

14.3 秒表(定时器扫描按键数码管)

  1. 定时器定时时间到了会进入中断函数,在进入中断后进行扫描。
    在中断函数中调用Key相关的函数(在此代码中用了自定义的Key_Loop函数)建立进入扫描按键的中断。简单来讲就是在中断的中断里进行按键的扫描。
#include <at89c51RC2.h> #include <Timer0.h> void main() { 
     Timer0_Init(); while(1) { 
     } } void Timer0_Routine() interrupt 1 //溢出时中断 { 
     static int T0Count; TL0 = 0x66; TH0 = 0xFC; T0Count++; if(T0Count>=20) { 
     T0Count=0; Key_Loop(); } } 
  1. 在跳转到Key_Loop后进行按键扫描。
    引入变量NowsState和LastState分别存储当前状态和前一步的状态。
    每一次进入Key_Loop都进行依次按键扫描,更新状态,即将LastState赋值给NowsState。
    NowState获取当前按键的状态,建立函数Key_GetState()判断各按键的状态。
    当前一步的状态按下了一个按键,现在的状态为无按键按下时,引入变量Key_KeyNumber来记录上一步按下的按键的键码值。在这里,因为中断中设置是20sm进入一次,所以在此的上一步和当前状态的时间可以忽略不计,不存在上一步为很久远之前的操作的情况。



#include <at89c51RC2.h> #include <Delay.h> unsigned char Key_KeyNumber; unsigned char Key_GetState() { 
     unsigned int KeyNum=0; if(P3_1==0){ 
    KeyNum=1;} if(P3_0==0){ 
    KeyNum=2;} if(P3_2==0){ 
    KeyNum=3;} if(P3_3==0){ 
    KeyNum=4;} return KeyNum; } / * @brief 循环调用(驱动) * @param 无 * @retval 无 */ void Key_Loop(void) { 
     static unsigned NowsState,LastState; LastState=NowsState; NowsState=Key_GetState(); if(LastState==1 && NowsState==0) { 
     Key_KeyNumber=1; } if(LastState==2 && NowsState==0) { 
     Key_KeyNumber=2; } if(LastState==3 && NowsState==0) { 
     Key_KeyNumber=3; } if(LastState==4 && NowsState==0) { 
     Key_KeyNumber=4; } } 
  1. 建立函数Key,用于输出的调用。
    因为Key_KeyNumber不会自动清零,所以引入一个中间变量Temp用来存储Key_KeyNumber的值。
    输出Temp的值。

unsigned char Key(void) { 
     unsigned char Temp; Temp=Key_KeyNumber; Key_KeyNumber=0; return Temp; } 
  1. 在主函数中引入变量KeyNum存储Key函数的输出值。
    调用Nixie函数实现数码管的输出。
    但是此时,实现的数码管的显示时间过短,显示不明显。可以引入一个中间变量Temp,把KeyNum存入Temp中,对Temp进行Nixie函数的调用。

#include <at89c51RC2.h> #include <Timer0.h> #include <Nixie.h> unsigned char KeyNum; void main() { 
     Timer0_Init(); while(1) { 
     KeyNum=Key(); if(KeyNum) Temp=KeyNum; Nixie(1,Temp); } } void Timer0_Routine() interrupt 1 //溢出时中断 { 
     static int T0Count; TL0 = 0x66; TH0 = 0xFC; T0Count++; if(T0Count>=20) { 
     T0Count=0; Key_Loop(); } } 
  1. 接下来编写定时器扫描数码管线管代码。参考第一步,在中断函数中再建立一个中断中的中断。
void Timer0_Routine() interrupt 1 //溢出时中断 { 
     static int T0Count1,T0Count2; TL0 = 0x66; TH0 = 0xFC; T0Count1++; if(T0Count1>=20) { 
     T0Count1=0; Key_Loop(); } if(T0Count2>=2) { 
     T0Count2=0; Nixie_Loop(); } } 
  1. 对于中断中的中断Nixie_Loop。
    引入数组Nixie_Buf存放数码管各个位置的数值对应的在NixieTable中的位置。
    引入变量i标记读取的位置。

#include <at89c51RC2.h> unsigned char NixieTable[]={ 
    0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71,0x00}; unsigned char Nixie_Buf[8]={ 
    10,10,10,10,10,10,10,10}; void Nixie_SetBuf(unsigned char Location,Number) { 
     Nixie_Buf[Location]=Number; } / * @brief 数码管的静态显示 * @param LED,哪个数码管亮 范围为8~1 * @param Number,要显示的数字 * @retval 无 */ void Nixie(unsigned char LED,Number) { 
     P0=0x00; switch(LED) { 
     case 8: P2_4=0;P2_3=0;P2_2=0;break; case 7: P2_4=0;P2_3=0;P2_2=1;break; case 6: P2_4=0;P2_3=1;P2_2=0;break; case 5: P2_4=0;P2_3=1;P2_2=1;break; case 4: P2_4=1;P2_3=0;P2_2=0;break; case 3: P2_4=1;P2_3=0;P2_2=1;break; case 2: P2_4=1;P2_3=1;P2_2=0;break; case 1: P2_4=1;P2_3=1;P2_2=1;break; } P0=NixieTable[Number]; } void Nixie_Loop(void) { 
     static unsigned char i=1; Nixie(i,Nixie_Buf[i-1]); i++; if(i>=9) i=1; } 
  1. 接下来开始用数码管显示秒表。
unsigned char Min,Sec,MiniSec; void main() { 
     Timer0_Init(); while(1) { 
     Nixie_SetBuf(1,Min/10); Nixie_SetBuf(2,Min%10); Nixie_SetBuf(3,17); Nixie_SetBuf(4,Sec/10); Nixie_SetBuf(5,Sec%10); Nixie_SetBuf(6,17); Nixie_SetBuf(7,MiniSec/10); Nixie_SetBuf(8,MiniSec%10); } } void Sec_Loop(void) { 
     MiniSec++; if(MiniSec>=100) { 
     MiniSec=0; Sec++; if(Sec>=60) { 
     Sec=0; Min++; if(Min>=60) { 
     Min=0; } } } } void Timer0_Routine() interrupt 1 //溢出时中断 { 
     static int T0Count1,T0Count2,T0Count3; TL0 = 0x66; TH0 = 0xFC; T0Count1++; if(T0Count1>=20) { 
     T0Count1=0; Key_Loop(); } T0Count2++; if(T0Count2>=3) { 
     T0Count2=0; Nixie_Loop(); } T0Count3++; if(T0Count3>=10) { 
     T0Count3=0; Sec_Loop(); } } 

15.DS18B20(单总线)

15.1 介绍

15.1.1 DS18B20

DS18B20是一种常见的数字温度传感器,其控制命令和数据都是以数字信号的方式输出的,相比较与模拟温度传感器,具有功能强大、硬件简单、易扩展、抗干扰性强等特点。

15.1.1.1 引脚及应用电路

在这里插入图片描述

VDD:电源(3.0V ~ 5.5V)
GND:电源地
I/O:单总线接口

15.1.1.2 内部结构框图

在这里插入图片描述

15.1.1.3 存储器结构

在这里插入图片描述

15.1.2 单总线

单总线(1-Wire BUS)是由Dallas公司开发的一种通用数据总线。

单总线只需要一根通信线即可实现数据的双向传输,当采用寄生供电时,还可以省去设备的VDD线路,此时,供电加通信只需要DQ和GND两根线。

15.1.2.1 电路规范
  • 设备的DQ均要配置成开漏输出模式。
  • DQ添加一个上拉电阻,阻值一般为4.7KΩ左右
  • 若此总线的从机采取寄生供电,则主机还应配一个强上拉输出电阻。
    在这里插入图片描述
15.1.2.2 时序结构

在这里插入图片描述

初始化:主机将总线拉低至少480us,然后释放总线,等待15 ~ 60us后,存在的从机拉低总线60 ~ 240us以响应主机,之后从机将释放总线。

在这里插入图片描述

发送一位:主机将总线拉低60 ~ 120us,然后释放总线,表示发送0;主机将总线拉低1 ~ 15us,然后释放总线,表示发送1。从机将再总线拉低30us后读取电平,整个时间片大于60us。

在这里插入图片描述

接收一位:主机将总线拉低1 ~ 15us,然后释放总线,并且在拉低后15us内读为高电平则为接收1,整个时间片应大于60us。

15.1.2.3 时序结构

在这里插入图片描述

发送一个字节:连续调用8次发送一位的时序,依次发送一个字节的8位(低位在前)。

在这里插入图片描述

接收一个字节:连续调用8次接收一位的时序,依次接收一个字节的8位(低位在前)。

15.1.3 DS18B20操作流程

先进行初始化,再进行ROM操作,最后是功能操作

初始化:从机复位,主机判断从机是否响应。
ROM操作:RO指令+本指令需要的读写操作。
功能操作:功能指令+本指令需要的读写操作。
在这里插入图片描述


15.1.4 DS18B20数据帧

在这里插入图片描述

温度变换:先初始化,再跳过ROM,最后开始温度变换。

在这里插入图片描述

温度读取:先初始化,再跳过ROM,然后读暂存器,最后是连续的读操作。

15.1.5 温度存储格式

在这里插入图片描述

LS BYTE:低八位
MS BYTE:高八位

将MS BYTE移动到LS BYTE前,构成16位二进制。

BIT0 ~ BIT3:表示小数部分。
BIT11 ~ BIT15:表示数字的正负性。当数值为负时,这几位全为1;当数值为正时,这几位全为0。

15.2 温度读数

  1. 建立单总线模块,参考15.1.1.1和15.1.2。
    参考15.1.2.2进行初始化,将总线先拉高(相关引脚赋值为1)再拉低(相关引脚赋值为0),用stc-isp软件生成延时超过480us的延时代码。
    在这里插入图片描述
    相关引脚赋值为1,释放总线。延时足够长的时间,设置采样点。
    引入变量AckBit获取应答。
    继续延时直到初始化完成。




unsigned char OneWire_Init(void) { 
     unsigned char i; unsigned char AckBit; OneWire_DQ=1; OneWire_DQ=0; i = 227;while (--i); //Delay 500us OneWire_DQ=1; i = 29;while (--i); //Delay 70us AckBit=OneWire_DQ; i = 227;while (--i); //Delay 500us return AckBit; } 
  1. 参考15.1.2.2进行发送一位。
    将总线拉低,延时10us后,将要发送的高电平(低电平)赋值给总线。当要发送的是高电平时,总线被拉高,发送1;当要发送的是低电平时,总线任然保持拉低状态,发送0。
    继续延时直到发送结束。
    将总线重新拉高。


void() OneWire_SendBit(unsigned char Bit) { 
     unsigned char i; OneWire_DQ=0; i = 4;while (--i); //Delay 10us OneWire_DQ=Bit; i = 22;while (--i); //Delay 50us OneWire_DQ=1; } 
  1. 参考15.1.2.2进行接收一位。
    将总线拉低,延时5us后,释放总线,再延时5us后,采样。
    继续延时直到接收结束。

unsigned char OneWire_ReciveBit(void) { 
     unsigned char i; unsigned char Bit; OneWire_DQ=0; i = 2;while (--i); //Delay 5us OneWire_DQ=1; i = 2;while (--i); //Delay 5us Bit=OneWire_DQ; i = 22;while (--i); //Delay 50us return Bit; } 
  1. 发送一个字节与接收一个字节。
void OneWire_SendByte(unsigned char Byte) { 
     unsigned char i; for(i=0;i<8;i++) { 
     OneWire_SendBit(Byte&(0x01<<i)); } } unsigned char OneWire_ReceiveByte(void) { 
     unsigned char i; unsigned char Byte; for(i=0;i<8;i++) { 
     if(OneWire_ReciveBit()){ 
    Byte|=(0x01<<i);} } return Byte; } 
  1. 建立DS17B20模块,参考15.1.3、15.1.4和15.1.5。
#include <at89c51RC2.h> #include <OneWire.h> #define DS18B02_SKIP_ROM 0xCC #define DS18B02_CONVERT_T 0x44 #define DS18B02_READ_SCRATCHPAD 0xBE / * @brief 温度变换 * @param 无 * @retval 无 */ void DS18B20_ConvertT(void) { 
     OneWire_Init(); OneWire_SendByte(DS18B02_SKIP_ROM); OneWire_SendByte(DS18B02_CONVERT_T); } / * @brief 温度读取 * @param 无 * @retval T 当前温度 */ float DS18B20_ReadT(void) { 
     unsigned char TLSB,TMSB; int Temp; float T; OneWire_Init(); OneWire_SendByte(DS18B02_SKIP_ROM); OneWire_SendByte(DS18B02_READ_SCRATCHPAD); TLSB=OneWire_ReceiveByte(); TMSB=OneWire_ReceiveByte(); Temp=(TMSB<<8)|TLSB; T=Temp/16.0; return T; } 
  1. 在main函数中。
    调用LCD模块用于显示,在我的LCD函数模块中,并不能显示float类型的值,所以需要将小数点前后的部分分开来表示。

代码呈现:

#include <at89c51RC2.h> #include <LCD1602.h> #include <DS18B20.h> float T; void main() { 
     LCD_Init(); LCD_ShowString(1,1,"Temperature:"); while(1) { 
     DS18B20_ConvertT(); T=DS18B20_ReadT(); if(T<0) { 
     LCD_ShowChar(2,1,'-'); T=-T; } else { 
     LCD_ShowChar(2,1,'+'); } LCD_ShowNum(2,2,T,3); LCD_ShowChar(2,5,'.'); LCD_ShowNum(2,6,(unsigned long)(T*10000)%10000,3); } } 

15.3 温度报警

  1. 参考15.2,显示实时温度。
#include <at89c51RC2.h> #include <LCD1602.h> #include <DS18B20.h> float T; void main() { 
     LCD_Init(); LCD_ShowString(1,1,"T:"); while(1) { 
     DS18B20_ConvertT(); T=DS18B20_ReadT(); if(T<0) { 
     LCD_ShowChar(1,3,'-'); } else { 
     LCD_ShowChar(1,3,'+'); } LCD_ShowNum(1,4,T,3); LCD_ShowChar(1,7,'.'); LCD_ShowNum(1,8,(unsigned long)(T*10000)%10000,3); } } 
  1. 设置温度的上下限。
#include <at89c51RC2.h> #include <LCD1602.h> #include <DS18B20.h> float T; char TLow,THigh; void main() { 
     LCD_Init(); LCD_ShowString(1,1,"T:"); LCD_ShowString(2,1,"TH:"); LCD_ShowString(2,9,"TL:"); while(1) { 
     /*温度读取及显示*/ DS18B20_ConvertT(); T=DS18B20_ReadT(); if(T<0) { 
     LCD_ShowChar(1,3,'-'); } else { 
     LCD_ShowChar(1,3,'+'); } LCD_ShowNum(1,4,T,3); LCD_ShowChar(1,7,'.'); LCD_ShowNum(1,8,(unsigned long)(T*10000)%10000,3); /*阈值判断及显示*/ LCD_ShowSignedNum(2,4,THigh,3); LCD_ShowSignedNum(2,12,TLow,3); } } 
  1. 用按键实现最高温度设定值的加减。
    DS18B20的测温范围为-55℃ ~ +125℃。
    高温阈值应当一直大于低温阈值。

#include <at89c51RC2.h> #include <LCD1602.h> #include <DS18B20.h> #include <Key.h> float T; char TLow,THigh; unsigned char KeyNum; void main() { 
     LCD_Init(); LCD_ShowString(1,1,"T:"); LCD_ShowString(2,1,"TH:"); LCD_ShowString(2,9,"TL:"); while(1) { 
     KeyNum=Key(); /*温度读取及显示*/ DS18B20_ConvertT(); T=DS18B20_ReadT(); if(T<0) { 
     LCD_ShowChar(1,3,'-'); } else { 
     LCD_ShowChar(1,3,'+'); } LCD_ShowNum(1,4,T,3); LCD_ShowChar(1,7,'.'); LCD_ShowNum(1,8,(unsigned long)(T*10000)%10000,3); /*阈值判断及显示*/ if(KeyNum) { 
     if(KeyNum==1) { 
     THigh++; if(THigh>125){ 
    THigh=125;} } if(KeyNum==2) { 
     THigh--; if(THigh<=TLow){ 
    THigh++;} } if(KeyNum==3) { 
     TLow++; if(THigh<=TLow){ 
    TLow--;} } if(KeyNum==4) { 
     TLow--; if(THigh<-55){ 
    TLow=-55;} } } LCD_ShowSignedNum(2,4,THigh,3); LCD_ShowSignedNum(2,12,TLow,3); } } 
  1. 设置报警提示。
if(T>THigh) { 
     LCD_ShowString(1,13,"OV:H"); } else if(T<TLow) { 
     LCD_ShowString(1,13,"OV:L"); } else { 
     LCD_ShowString(1,13," "); } 
  1. 参考15.1.1.3,存储温度阈值。
 DS18B20_ConvertT(); Delay(1000); THigh=AT24C02_ReadByte(0); TLow=AT24C02_ReadByte(1); if(THigh>125 || TLow<-55 || THigh<=TLow) { 
     THigh=30; TLow=-10; } 

代码呈现:

#include <at89c51RC2.h> #include <LCD1602.h> #include <DS18B20.h> #include <Key.h> #include <AT24C02.h> #include <Delay.h> float T,TShow; char TLow,THigh; unsigned char KeyNum; void main() { 
     DS18B20_ConvertT(); Delay(1000); THigh=AT24C02_ReadByte(0); TLow=AT24C02_ReadByte(1); if(THigh>125 || TLow<-55 || THigh<=TLow) { 
     THigh=30; TLow=-10; } LCD_Init(); LCD_ShowString(1,1,"T:"); LCD_ShowString(2,1,"TH:"); LCD_ShowString(2,9,"TL:"); while(1) { 
     KeyNum=Key(); /*温度读取及显示*/ DS18B20_ConvertT(); T=DS18B20_ReadT(); if(T<0) { 
     LCD_ShowChar(1,3,'-'); TShow=-T; } else { 
     LCD_ShowChar(1,3,'+'); TShow=T; } LCD_ShowNum(1,4,TShow,3); LCD_ShowChar(1,7,'.'); LCD_ShowNum(1,8,(unsigned long)(TShow*10000)%10000,3); /*阈值判断及显示*/ if(KeyNum) { 
     if(KeyNum==1) { 
     THigh++; if(THigh>125){ 
    THigh=125;} } if(KeyNum==2) { 
     THigh--; if(THigh<=TLow){ 
    THigh++;} } if(KeyNum==3) { 
     TLow++; if(THigh<=TLow){ 
    TLow--;} } if(KeyNum==4) { 
     TLow--; if(THigh<-55){ 
    TLow=-55;} } } LCD_ShowSignedNum(2,4,THigh,3); LCD_ShowSignedNum(2,12,TLow,3); if(T>THigh) { 
     LCD_ShowString(1,13,"OV:H"); } else if(T<TLow) { 
     LCD_ShowString(1,13,"OV:L"); } else { 
     LCD_ShowString(1,13," "); } } } 

16.直流电机驱动(PWM)

16.1 介绍

16.1.1 直流电机介绍

16.1.2 电机驱动电路

16.1.2.1 大功率器件直接驱动

在这里插入图片描述

三极管基极低电平导通

16.1.2.2 H桥驱动

在这里插入图片描述

16.1.3 PWM

PWM(Pulae Width Modulation)即脉冲宽度调制,在惯性的系统中,可以通过对一系列脉冲的宽度进行调制,来等效地获得所需要地模拟参量,常应用于电机控速、开关电源等领域。

PWM重要参数
在这里插入图片描述

16.1.3.1 产生PWM的方法

在这里插入图片描述

计数器定时自增和用户设置的比较值对比,对比结果不同,输出不同,产生方波,实现PWM输出。

16.1.4 步进电机模块原理图

请添加图片描述

16.2 呼吸灯

  1. 控制第一个LED灯的亮灭,可以给该灯对应的引脚赋0和1。
#include <at89c51RC2.h> void main() { 
     while(1) { 
     P2_0=0; P2_0=1; } } 
  1. LED的亮灭并不明显,可以在亮和灭之后加一个延迟。
#include <at89c51RC2.h> void Delay(unsigned int i) { 
     while(i--); } void main() { 
     while(1) { 
     P2_0=0; Delay(95); P2_0=1; Delay(5); } } 
  1. LED闪亮,且亮度很高。调换延迟时间,LED亮度变暗。
#include <at89c51RC2.h> void Delay(unsigned int i) { 
     while(i--); } void main() { 
     while(1) { 
     P2_0=0; Delay(5); P2_0=1; Delay(95); } } 
  1. 实现亮度的变化,可以通过延迟时间的动态变化来实现。
    引入变量Time,记录当前延迟时间,并且要保持两个延迟时间相加后时间保持不变。
#include <at89c51RC2.h> void Delay(unsigned int i) { 
     while(i--); } void main() { 
     unsigned char Time,i; while(1) { 
     for(Time=0;Time<100;Time++) { 
     P2_0=0; Delay(Time); P2_0=1; Delay(100-Time); } } } 
  1. LED变化太快,可以再套一个for循环,使得每次灯变化停留的时间都变长。
#include <at89c51RC2.h> void Delay(unsigned int i) { 
     while(i--); } void main() { 
     unsigned char Time,i; while(1) { 
     for(Time=0;Time<100;Time++) { 
     for(i=0;i<20;i++) { 
     P2_0=0; Delay(Time); P2_0=1; Delay(100-Time); } } } } 

代码呈现:

#include <at89c51RC2.h> void Delay(unsigned int i) { 
     while(i--); } void main() { 
     unsigned char Time,i; while(1) { 
     for(Time=0;Time<100;Time++) { 
     for(i=0;i<20;i++) { 
     P2_0=0; Delay(Time); P2_0=1; Delay(100-Time); } } for(Time=100;Time>0;Time--) { 
     for(i=0;i<20;i++) { 
     P2_0=0; Delay(Time); P2_0=1; Delay(100-Time); } } } } 

16.3 直流电机调速

  1. 参考16.1.3.1,要实现PWM输出,先要调用定时器。
    在这里插入图片描述
#include <at89c51RC2.h> #include <Timer0.h> void main() { 
     Timer0_Init(); while(1) { 
     } } void Timer0_Routine() interrupt 1 //溢出时中断 { 
     TL0 = 0xAE; TH0 = 0xFB; } 
  1. 引入变量Counter和Compare记录计时器和比较值。
    在中断中对Counter和Compare进行比较。
#include <at89c51RC2.h> #include <Timer0.h> unsigned char Counter,Compare; void main() { 
     Timer0_Init(); Counter=50; while(1) { 
     } } void Timer0_Routine() interrupt 1 //溢出时中断 { 
     TL0 = 0xAE; TH0 = 0xFB; Counter++; Counter%=100; //Counter到100了 if(Counter<Compare) { 
     P2_0=0; } else { 
     P2_0=1; } } 
  1. 用独立按键控制电机档位,并且用数码管显示当前档位。
    引入独立按键模块和数码管模块。
    第一个按键实现档位加功能。
    引入变量Speed存储当前档位信息,一共三个档位。


#include <at89c51RC2.h> #include <Timer0.h> #include <Key.h> #include <Nixie.h> unsigned char Counter,Compare; unsigned char KeyNum,Speed; void main() { 
     Timer0_Init(); Counter=50; while(1) { 
     KeyNum=Key(); if(KeyNum==1) { 
     Speed++; Speed%=4; } Nixie(1,Speed); } } void Timer0_Routine() interrupt 1 //溢出时中断 { 
     TL0 = 0xAE; TH0 = 0xFB; Counter++; Counter%=100; //Counter到100了 if(Counter<Compare) { 
     P2_0=0; } else { 
     P2_0=1; } } 
  1. 当档位不同时,PWM输出也不同,所以当档位不同时更改Compare的值。
void main() { 
     Timer0_Init(); while(1) { 
     KeyNum=Key(); if(KeyNum==1) { 
     Speed++; Speed%=4; if(Speed==0) Compare=0; if(Speed==1) Compare=5; if(Speed==2) Compare=50; if(Speed==3) Compare=100; } Nixie(1,Speed); } } 
  1. 一直到上面都是控制LED的,现在要控制电机。
    参考16.1.4把原来对LED的控制转为对步进电机模块的控制。
    还有在最开始要将引脚置0,不然电机插入后可能是处于旋转状态。

代码呈现:

#include <at89c51RC2.h> #include <Timer0.h> #include <Key.h> #include <Nixie.h> sbit Motor=P1^0; unsigned char Counter,Compare; unsigned char KeyNum,Speed; void main() { 
     Motor=0; Timer0_Init(); while(1) { 
     KeyNum=Key(); if(KeyNum==1) { 
     Speed++; Speed%=4; if(Speed==0) { 
    Compare=0;} if(Speed==1) { 
    Compare=50;} if(Speed==2) { 
    Compare=75;} if(Speed==3) { 
    Compare=100;} } Nixie(1,Speed); } } void Timer0_Routine() interrupt 1 //溢出时中断 { 
     TL0 = 0xAE; TH0 = 0xFB; Counter++; Counter%=100; //Counter到100了 if(Counter<Compare) { 
     Motor=1; } else { 
     Motor=0; } } 

17.AD/DA

17.1 介绍

AD(Analog to Digital):模拟-数字转换,将模拟信号转换为计算机可操作的数字信号。
DA(Digital to Analog):数字-模拟转换,将计算机输出的数字信号转换为模拟信号。

AD/DA转换打开了计算机与模拟信号的大门,极大的提高了计算机系统的应用范围,也为模拟信号数字化处理提供了可能。

17.1.1 硬件电路模型

在这里插入图片描述

  • AD转换通常有多个输入通道,用多路选择开关链接至AD转换器,以实现AD多路复用的目的,提高硬件利用率。
  • AD/DA与单片机数据传送可使用并口(速度快、原理简单),也可以使用串口(接线少、使用方便)。
  • 可将AD/DA模块直接集成在单片机内,这样直接写入/读出寄存器就可进行AD/DA转换,单片机的IO口可直接复用为AD/DA的通道。

17.1.2 运算放大器

运算放大器(简称“运放”)是具有很高放大倍数的放大电路单元。内部集成了差分放大器、电压放大器、功率放大器三级放大电路,是一个性能完备、功能强大的通用放大电路单元。

在这里插入图片描述

运算放大器可构成的电路有:电压比较器、反相放大器、同相放大器、电压跟随器、加法器、积分器、微分器等。


运算放大器电路的分析方法:虚短、虚断(负反馈条件下)

17.1.3 DA原理

17.1.4 AD原理

17.1.5 AD/DA性能指标

性能指标:AD/DA数字量的精细程度,通常用位数表示。AD/DA位数越高,分辨率就越高。
转换速度:表示AD/DA的最大采样/建立频率,通常用转换频率或者转换时间来表示,对于采样/输出高速信号,应注意AD/DA的转换速度

17.1.6 XPT2046

XPT2046是4线制电阻式触摸屏控制器,内含12位分辨率25KHz转换速率逐步逼近型A/D转换器。

17.1.6.1 时序

在这里插入图片描述

一横CS:低电平片选。
DCLK:上升沿输入,下降沿输出。
DIN:输入。
DOUT:输出。


在这里插入图片描述

17.1.7 原理图

在这里插入图片描述

在这里插入图片描述

17.1.8 命令字

在这里插入图片描述
在这里插入图片描述

17.2 AD模数转换

  1. 引入LCD1602模块,用LCD1602进行显示。
#include <at89c51RC2.h> #include <LCD1602.h> void main() { 
     LCD_Init(); LCD_ShowString(1,1,"ADJ"); while(1) { 
     } } 
  1. 建立XPT2046模块。
    参考17.1.7,对引脚进行定义。
    参考17.1.6.1,建立函数读取数据函数。

#include <at89c51RC2.h> sbit XPT2046_CS=P3^5; sbit XPT2046_DCLK=P3^6; sbit XPT2046_DIN=P3^4; sbit XPT2046_DOUT=P3^7; unsigned int XPT2046_ReadAD(unsigned char Command) { 
     unsigned char i; unsigned int ADVAlue=0; XPT2046_DCLK=0; XPT2046_CS=0; for(i=0;i<8;i++) { 
     XPT2046_DIN=Command&(0x80>>i); XPT2046_DCLK=1; XPT2046_DCLK=0; } for(i=0;i<16;i++) { 
     XPT2046_DCLK=1; XPT2046_DCLK=0; if(XPT2046_DOUT){ 
    ADVAlue|=(0x8000>>i);} } XPT2046_CS=1; return ADVAlue>>8; } 
  1. 参考17.1.8,对命令字进行定义。
#define XPT2046_XP 0x9C //0x8c #define XPT2046_YP 0xDC #define XPT2046_YBAT 0xAC #define XPT2046_AUX 0xEC 

代码呈现:

#include <at89c51RC2.h> #include <LCD1602.h> #include <XPT2046.h> #include <Delay.h> unsigned int ADValue; void main() { 
     LCD_Init(); LCD_ShowString(1,1,"ADJ NTC RG"); while(1) { 
     ADValue=XPT2046_ReadAD(XPT2046_XP); LCD_ShowNum(2,1,ADValue,4); ADValue=XPT2046_ReadAD(XPT2046_YP); LCD_ShowNum(2,6,ADValue,4); ADValue=XPT2046_ReadAD(XPT2046_VBAT); LCD_ShowNum(2,11,ADValue,4); Delay(10); } } 

17.3 DA模数转换

参考16.3,直接进行更改。

18.红外遥控

18.1 介绍

18.1.1 红外遥控器

红外遥控是利用红外光进行通信的设备,由红外LED将调制后的信号发出,由专门的红外接收头解调输出。

18.1.2 基本发送与接收

在这里插入图片描述

空闲状态:红外LED不亮,输出头输出高电平。
发送低电平:红外LED以38KHz频率闪烁发光,接收头输出低电平。
发送高电平:红外LED不亮,输出头输出高电平。

18.1.3 NEC编码

在这里插入图片描述

18.1.4 外部中断

在这里插入图片描述

18.1.4.1 中断号

在这里插入图片描述

18.1.4.2 外部中断寄存器

在这里插入图片描述

在这里插入图片描述

18.1.5 原理图

请添加图片描述

关于红外的测试,因为我的红外模块坏了,所以就暂且不进行了。


结语

到此就完成了单片机的学习,因为笔者也是从0开始学习,所以笔记里面难免会有些错误。此后笔者也会不断学习,不定期地修改笔记。

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/124430.html

(0)
上一篇 2025-10-07 17:20
下一篇 2025-10-07 17:33

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信