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

STC89C52RC学习笔记

前言

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

学习资源

参考资料:
【51单片机入门教程-2020版 程序全程纯手打 从零开始入门】https://www.bilibili.com/video/BV1Mb411e7re?p=3&vd_source=c7e5b42e6c1438daf4523dc34387738c

单片机型号

STC89C52RC单片机开发板

环境搭建

keil5
stc-isp
文字取模软件


1.基础知识

1.1 单片机的概念

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

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

1.2 单片机的功能

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

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

1.3 STC89C52单片机

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

为什么STC89C52叫做51单片机?
51单片机是指80年代Intel开发的8051单片机内核的统称,就是因为这个“8051”有个51,所以凡是与“8051”内核一样的单片机都统称为51系列单片机。

参考资料详看:
http://www.51hei.com/bbs/dpj-40878-1.html

1.3.1 命名规则

请添加图片描述

STC:公司名称
89:类型为STC 12T/6T 8051
C:工作电压为5.5V-3.8V
5:固定不变
1:程序空间为8K字节
RC:RAM空间为512字节
40:表示芯片外部晶振最高可接入40MHz
I:工作温度范围为工业级 -40℃-80℃
PDIP:封装类型为PDIP
40:管脚数为40

1.3.2 内部结构

在这里插入图片描述

1.3.3 管脚

在这里插入图片描述

一些重要的管脚:
Vcc:电源正极
Gnd:电源负极
XTAL1/XTAL2:外接晶振

2.51单片机开发板

在这里插入图片描述

3.LED相关

3.1 LED模块原理图

在这里插入图片描述

从右往左看:
Vcc:电源
RP7和RP9:电阻,用于限流,保护LED灯
D1-8:发光二极管

LED灯发光原理:
电致发光效应,即发光二极管(LED)的发光原理基于半导体物理中的电致发光效应。当通过PN结施加正向电压时,电子和空穴在PN结处结合,释放出能量,以光的形式发射出来,产生发光现象。

  • 简单来讲,发光二极管,正极接正,负极接负,即可发光。
  • 在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
	}
}

实现上述代码,我们看到的LED点亮状态是全部点亮。不是没有实现闪烁,而是闪烁速度太快,我们观察不到,所以要在中间加入延时代码。
延时代码可以用stc-isp里的软件延时计算器生成实现延时功能的自定义函数。
请添加图片描述

系统频率:STC89C52RC选11.0592赫兹
定时长度:建议选择毫秒

代码呈现如下:

#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 独立按键原理图

在这里插入图片描述

从右往左:
GND:负极
K1-4:轻触开关

独立按键的工作原理:
轻触开关按下时,寄存器为低电平(0),反之为高电平(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;
}

但是,机械开关断开和闭合时,由于机械触电的弹性作用,不会立刻进入稳定状态,而是会伴随一系列的抖动状态(如下图)。
在这里插入图片描述
所以,需要减小抖动的影响,使得LED灯一直不会因为手拿开而改变状态。

  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 数码管原理图

数码管工作不仅依靠数码管模块,还需要调用138译码器。
在这里插入图片描述

138译码器和数码模块相连接。
138译码器是三线—八线译码器,由ABC输入量决定了Y端的有效性。使用时,将输入量以CBA的顺序排列,分别对应的是二进制的第三位、第二位、第一位,将输入量对应的数转换为十进制后,即可得出有效的Y口。
例如,当C、B、A输入分别为0、0、1时,对应的二进制为001,转化为十进制为1,即有效的Y口为Y1。
需要注意的是,Y口的有效情况为低电平(0)有效,即有效时输出为0。

在这里插入图片描述

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

5.1.2 数码管驱动方式

数码管的驱动方式有两种,分为单片机直接扫描和专用驱动芯片扫描。
单片机直接扫描:硬件设备简单,但是会耗费大量的单片机时间。
专用驱动芯片扫描:内部自带显存、扫描电路,单片机只需要告诉它显示什么即可。带有此功能的芯片例如TM1640。

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 模块化编程的作用

与把所有函数都放在main.c 里的传统方式编程不同的是,模块化编程将各个模块的代码放在不同的.c文件里。
这样做的好处在于,当调用模块较多时,避免了过多的代码在同一个.c文件里而影响编码者的思路。所以相对于传统方式编程,模块化编程更有利于代码的组织和管理,提高了代码的可阅读性、可维护性、可移植性。

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

LCD1602液晶显示屏是一种字符型液晶显示模块,可以显示ASCII的标准字符和其他的一些内置特殊字符,还可以有8个自定义字符。
显示容量为16 * 2个字符,每个字符为5 * 7点阵。

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

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相关程序的头文件。

此头文件资源来源于江协科技:
https://jiangxiekeji.com/download.html

  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口的占用。
矩阵键盘的扫描是输入扫描,以行或者列为单位,逐个扫描,快速循环这个过程,最终实现所有按键同时扫描。

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

按列扫描选中过程
位选:P10-P13为输入部分,针脚默认状态1,所以要选择哪一列,就要给这一列对应的阵脚输入0。例如选择第一列,则P13为0,P12、P11、P10都为1。
段选:P14-P17为输出部分,针脚默认状态1,当按键按下时,作为输出的针脚和低电平相接,输出为0。例如按键S1按下时,P17口输出值为0。

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 中断资源

中断源的个数为8个(外部中断0、定时器0中断、外部中断1、定时器1中断、串口中断、外部中断2、外部中断3)
中断级个数4个。

9.1.3.2 中断系统原理图

在这里插入图片描述

9.1.4 定时器的工作模式

定时器有四种工作模式:
模式0:13位定时器/计数器
模式1:16位定时器/计数器(常用
模式2:8位自动重装模式
模式3:两个8位计数器

9.1.5 模式1状态下的定时器

9.1.5.1 模式1时定时器的连接

在模式1时定时器的连接方式如下
在这里插入图片描述

分为三部分:
1.时钟:请添加图片描述
2.中断:
请添加图片描述
中断系统是为使CPU具有对外界紧急事件的实时处理能力而设置的。
中断系统结构见下图:在这里插入图片描述
3.计数单元:余下部分为计数系统。

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.6589862个,则计数器溢出差值为约为921,即从64614开始计数。则TH0=64614/256=0xFC,TL0=64614%256=0x66。
    因为两个寄存器TH0、TL0为二进制八位,单独可计256次所以除以256可得高八位得次数,取余就是低八位的次数,合并在一起就是所赋的初始值。

关于机器周期可参考以下文章:
https://blog.csdn.net/weixin_48839038/article/details/126311620?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522172113180216800227481013%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=172113180216800227481013&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2allfirst_rank_ecpm_v1~rank_v31_ecpm-2-126311620-null-null.142v100pc_search_result_base8&utm_term=11.0952MHZ%E9%85%8D%E7%BD%AE%E5%AE%9A%E6%97%B6%E5%99%A8&spm=1018.2226.3001.4187


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

对于配置TMOD的配置的优化
如果同时调用定时器0和定时器1时,用上述方式赋值将对定时器1产生影响。
如果只对定时器0进行更改,则要保持属于定时器1的高四位不变,可以用逻辑与运算。
TOMD=TOMD&0xF0;,其他位保持不变,可以用逻辑或运算。
如果要把最低位置1
TOMD=TOMD|0x01;


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


参考资料
https://blog.csdn.net/weixin_37909391/article/details/131441253?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522172119612016800207080932%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id=172119612016800207080932&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2alltop_click~default-2-131441253-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 串口介绍

串口是一种应用十分广泛的通讯接口,串口成本低、容易使用、通信线路简单,可实现两个设备的互相通信。
51单片机内部自带的UART(通用异步收发器),可以实现串口通信。

10.1.1 硬件电路

简单的双向串口通信有两根通信线(发送端TXD和接受端RXD)。
在这里插入图片描述

  • 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 常见的通信接口比较

名称引脚定义通信方式特点
UARTTXD、RXD全双工、异步点对点通信
I^2CSCL、SDA半双工、同步可挂载多个设备
SPISCLK、MOSI、MISO、CS全双工、同步可挂载多个设备
1-WireDQ半双工、异步可挂载多个设备

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

相关术语

  1. 全双工:通信双方可以同一时刻互相传输数据。例如,手机。
    半双工:通信双方可以互相传输数据,但必须分时复用同一根数据线。例如,对讲机。
    单工:通信双方只能有一方发送到另一方,不能反向传输。例如,遥控器。
  2. 异步:通信双方各自约定通信速率。
    同步:通信双方靠一根时钟线来约定通信速率。
  3. 总线:连接各个设备的数据传输线路。

10.1.3 UART的四种工作模式

模式0:同步移位寄存器
模式1:8位UART,波特率可变(常用)
模式2:9位UART,波特率固定
模式3:9位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。
在这里插入图片描述

74HC595是串行输入并行输出的移位寄存器,可用3根线输入串行数据,8根线输出并行数据,多片级联后,可输出16位、24位、32位等,常用于IO口扩展。
在这里插入图片描述


相关引脚
一横SRCLR:串行清零端。给高电平(1)实现数据移位。
SRCLR:串行时钟
SER:串行数据
用SER输入一个字符,SERCLK(一横SRCLK)使得数据下移(可以参考栈),当数据达到八位时,RCLK(SRCLK)将数据移动到输出各引脚。

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介绍

DS1302是由DALLAS公司推出的具有涓细电流充电能力的低功耗实时时钟芯片。它可以对年、月、日、周、时、分、秒进行计时,且具有闰年补偿等多种功能。
RTC:实时时钟,是一种集成电路,通常称为时钟芯片。

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呈现结果。

读出时间为一个大于59并且不动的数时,芯片可能处于读写保护状态,要加
DS1302_WriteByte(0x8E,0x00);
解除保护。

显示结果到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 蜂鸣器

单片机中蜂鸣器采用集成电路驱动,蜂鸣器连接了ULN2003,ULN2003的BEEP口的输出值决定了蜂鸣器的工作状态。
在这里插入图片描述

P25控制BEEP的输出。

在这里插入图片描述

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

13.1.2 乐理

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

13.1.2.1 音高

音高之间的关系如下图所示:
两个白键是全音关系,白键和黑键之间是半音关系。
在这里插入图片描述

#为升高半音
b为减低半音

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 介绍

I2C总线是由Philips公司开发的一种通用数据总线。
它的两根通信线为,SCL、SDA。

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数据帧

AT24C02数据帧与I2C数据帧相似,但有所简化。
在这里插入图片描述

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

测温范围:-55℃到+125℃
通信接口:1-Wire(单总线)

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公司开发的一种通用数据总线。

一根通信线:DQ
异步、半双工

单总线只需要一根通信线即可实现数据的双向传输,当采用寄生供电时,还可以省去设备的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原理

T型电阻网络转换器:
在这里插入图片描述

17.1.4 AD原理

PWM型DA转换器:
在这里插入图片描述

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将调制后的信号发出,由专门的红外接收头解调输出。

通信方式:单工,异步
红外LED波长:940nm
通信协议标准:NEC标准

18.1.2 基本发送与接收

在这里插入图片描述

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

18.1.3 NEC编码

在这里插入图片描述

18.1.4 外部中断

在这里插入图片描述

STC89C52由4个外部中断,传统的只有2个外部中断。
外部中断的触发方式分别为:下降沿触发、低电平触发。

18.1.4.1 中断号

在这里插入图片描述

18.1.4.2 外部中断寄存器

在这里插入图片描述

在这里插入图片描述

18.1.5 原理图

请添加图片描述

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


结语

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

  • 26
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值