STC89C52RC学习笔记
前言
本文是关于51单片机学习的笔记,不包含红外模块的相关内容。
学习资源
参考资料:
【51单片机入门教程-2020版 程序全程纯手打 从零开始入门】https://www.bilibili.com/video/BV1Mb411e7re?p=3&vd_source=c7e5b42e6c1438daf4523dc34387738c
单片机型号
STC89C52RC单片机开发板
环境搭建
Keil5 uVision5
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点亮
参考步骤:
- 在Keil中建立新项目。
- 找到Microchip下的AT89C51RC2,出现对话框后点是。
目录里的这些都是生产公司的名字
-
右键Source Group 1文件夹,点击Add New Item Group ‘Source Group 1’…
-
选择C File,命名文件为main
-
参考3.1原理,找到控制LED灯的引脚为P20-P27,寄存器以八个为一组,所以P20-P27可以直接以P2的形式呈现。
-
例如要使P27相连的LED灯点亮,则P27要输出低电平,即输出0。
-
在P2中以二进制的方式表示为 P2 = 1111 1110 。
-
需要注意的是,在单片机程序中需要将二进制转换为十六进制。转换后得 P2 = 0xFE。
0x是一个前缀,表示后面是十六进制。
- 将代码写入keil
代码呈现:
#include <at89c51RC2.h>
void main()
{
P2 = 0xFE;
}
-
点击Options for Target,点击Output,选中Create HEX File,点击OK后,返回点击Build
-
打开stc-isp,在单片机型号中选择自己的单片机型号。
-
串口号选择如下:
-
点击打开程序文件,找到建立的文件,点击打开。
- 点击下载,出现右框所示操作成功,查看你的单片机,此时LED应当处于点亮状态。
3.3 LED闪烁
- 参考3.2实现LED隔一个点亮
#include <at89c51RC2.h>
void main()
{
P2 = 0xAA; //1010 1010
}
- 实现与上一步相反的点亮状态
#include <at89c51RC2.h>
void main()
{
P2 = 0x55; //0101 0101
}
- 设置循环实现交替闪烁
#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流水灯
- 参考3.2实现一个灯亮
#include <at89c51RC2.h>
void main()
{
P2 = 0x7F; //0111 1111
}
- 参考3.2实现上一步中灯灭后与它相邻的灯亮
#include <at89c51RC2.h>
void main()
{
P2 = 0xBF; //1011 1111
}
- 根据上述两步以及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为一个间隔的流水灯,对代码进行优化,使得间隔时间由给定量决定
- 生成一个以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--;
}
}
- 参考以上代码得到优化代码
#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的亮灭
- 通过P2_0 - P2_7实现对单独的LED的控制,例如 P2_0=0 单独对D1灯的处理,使得只控制它亮,而其他的LED保持原始状态不改变。
与P2相比,P2可以看成批量处理。
#include <at89c51RC2.h>
void main()
{
P2_0=0;
}
- 参考4.1可知,当 P3_0==0状态时即开关闭合。
- 结合上述两步,引入if语句判断开关状态,实现开关闭合时,D1号灯亮。
#include <at89c51RC2.h>
void main()
{
if(P3_0==0) P2_0=0;
else P2_0=1;
}
但是,机械开关断开和闭合时,由于机械触电的弹性作用,不会立刻进入稳定状态,而是会伴随一系列的抖动状态(如下图)。
所以,需要减小抖动的影响,使得LED灯一直不会因为手拿开而改变状态。
- 用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显示二进制
- 参考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);
}
}
- 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++;
}
}
}
- 上面一步实现的是灭了的灯表示二进制,如果要实现亮着的灯表示的是二进制。
如果直接在 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移位
- 参考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);
}
}
}
- 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);
}
}
- 参考上一步,为实现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);
}
- 执行上述程序时,最开始灯都不亮,且操作后显示的是移位后的点亮状态,并没有初始的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 数码管的静态显示
- 选中要用的数码管,在此以选中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;
}
}
- 用子函数优化代码。
自定义一个函数,用以判断需要点亮的数码管和需要显示的数字。
需要注意的是,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数码管动态显示
- 参考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);
}
}
- 执行上述程序,我们发现数码管的显示出现错位。
这是因为,数码管在显示时一直在进行 位选(选择让哪个数码管亮) 段选(选择让这个数码管显示显示什么数字) 位选 段选 位选 段选… 的过程。还是因为执行速度太快,段选过后单片机上面的针脚有几毫秒还处于该段选的显示状态,然后在这几毫秒内单片机又进行了下一位位选,所以导致了在选择了下一位位选时还保留了上一条段选的显示状态,所以会出现错位的情况。
为解决这个问题,我们要进行消影处理,即在段选和位选之间进行清零。
段选的结尾在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);
}
}
- 按照上述操作,数码管的显示会比较暗。
为解决此问题,可以在清零前加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 模块化编程的步骤
在此以延时函数为例。
- 将延时函数的代码放在.c文件里。
- 在.h文件里提供外部可调用函数的声明。
创建.h文件,在预处理框架里进行函数声明。
#ifndef __DELAY_H__
#define __DELAY_H__
void Delay(unsigned int xms);
#endif
- 在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 显示一个字符
- 最开始调用存储了LCD1602相关程序的头文件。
此头文件资源来源于江协科技:
https://jiangxiekeji.com/download.html
- 对于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);//光标复位,清屏
}
- 然后调用单个字符显示函数。
/**
* @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);
}
- 需要注意的是,在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 矩阵键盘
- 用模块化编程的思维,就矩阵键盘建立新的文件MatrixKey.c和MatrixKey.h。
- 初始状态将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;
}
- 在MatrixKey.h文件中完成上一步函数的声明。
- 在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 密码锁
- 密码锁程序只要在8.2上进行改动即可。可以对8.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); //设置四位密码
}
}
}
- 已经输入四位后,再继续输入,显示值会出错,为了避免过多的输入影响显示的情况,我们可以引入变量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); //设置四位密码
}
}
}
- 在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;
}
- 判断确认按键(赋值为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
}
}
- 在上一步中,判断输入密码错误后,显示直接变成了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
}
}
- 参考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流水灯模式
- 配置定时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
- 配置中断寄存器,参考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; //设置优先级为最低
}
- 编写main函数,打开定时器。
void main()
{
Timer0_Init();
while(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;
}
}
- 按键控制流水灯闪亮方向。对独立按键进行模块化编程,以获取按下按键的键码值。
//此处仅展示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;
}
- 在主函数中调用上一步的函数。
unsigned char KeyNum;
void main()
{
Timer0_Init();
while(1)
{
KeyNum=Key();
}
}
- 实现流水灯功能,可以参考3.3,也可以看下方的新方法。
调用INTRINS函数库,借助循环左移函数_crol_()和循环右移函数_cror()_实现流水灯。
在实现移动前,要给LED赋初值,因为LED给0实现点亮,在此的初始状态实现第一个处于点亮状态。
P2=0xFE;
P2=_crol_(P2,1); //循环左移一位
P2=_cror_(P2,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 时钟
- 调用LCD1602,添加LCD1602相关头文件,可以参考7.2。
调用第一步进行初始化。
调用相关函数,在LCD1602上显示提示信息Clock:。
#include <LCD1602.h>
void main()
{
LCD_Init();
LCD_ShowString(1,1,"Clock:");
while(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
}
}
- 引入变量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的表达方式,是传输线缆中认为规定的电压与数据的对应关系。
串口常用的电平标准:
- TTL电平(单片机用):+5V 表示1,0V 表示0。
- RS232电平:-3 ~ -15V 表示1,+3 ~ +15V 表示0。
- 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等
相关术语:
- 全双工:通信双方可以同一时刻互相传输数据。例如,手机。
半双工:通信双方可以互相传输数据,但必须分时复用同一根数据线。例如,对讲机。
单工:通信双方只能有一方发送到另一方,不能反向传输。例如,遥控器。- 异步:通信双方各自约定通信速率。
同步:通信双方靠一根时钟线来约定通信速率。- 总线:连接各个设备的数据传输线路。
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 串口向电脑发送数据
- 参考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
}
- 写数据发送的函数。
在SBUF中写入需要发送的数据。
通过发送完成标志位检测是否完成发送,若是已经完成发送,则重新置0。
void Uart_SendByte(unsigned char Byte)
{
SBUF=Byte;
while(TI==0);
TI=0;
}
- 发送不断增加的数。
引入变量sec,每循环循环一次加一。
unsigned char sec;
void main()
{
UartInit();
while(1)
{
Uart_SendByte(sec);
sec++;
}
}
- 用sct-isp自带的串口助手可以接收数据。
使用时,需要确保串口是扫描出来的,需要调整波特率与代码中保持一致,检查确定无误后,烧录代码,打开串口。
- 显示速度太快,且有误差。
在每一次输出之后都进行一下延迟。
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
- 参考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; //设置串口开放中断
}
- 查询中断号,编写中断服务子函数。
判断当前状态是否为电脑发送完成,如果电脑发送完成了,那么单片机接收中断,可以进行后续操作。
电脑通过串口控制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点阵显示图形
- 编写函数,先对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;
}
- 实现选中列,对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点阵显示动画
- 引入数组来存放需要显示的数据。
可以用文字取模软件得到数据。
定义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]);
}
}
- 实现字符移动,引入一个变量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++;
}
}
- 上一步的显示移动太快看不清楚,加入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++;
}
}
- 上一步的显示,字符闪亮。因为,扫描一遍全部字符之后,下一次的扫描在延迟之后,中间处于无显示状态,所以,我们可以引入一个变量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;
}
}
}
- 上一步完成了显示的移动,但是在显示的字符中我们可以看到,末尾移动的是乱码,这是因为,偏移量后取的位置超出了数组包含字符的数量。
此时数组里一共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;
}
}
}
}
- 字符显示完直接就跳回了首位,显示状态没有衔接,更改数组中元素。
如果数组中数据太多,可以将其定义为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 时钟显示
- 依靠LCD1602进行显示,参考7,调用相关代码。
- 为使对引脚的调用更加明确,可以对相关引脚经行重新的定义。
sbit DS1302_SCLK=P3^6;
sbit DS1302_IO=P3^4;
sbit DS1302_CE=P3^5;
- 对DS1302进行初始化,参考12.1.5可知,在没运行的时候,CE和SCLK处于0状态。
void DS1302_Init(void)
{
DS1302_CE=0;
DS1302_SCLK=0;
}
- 对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;
}
- 完成输入后,要进行读取才能呈现。
参考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);
}
- 在主函数中调用函数。
先对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 蜂鸣器播放提示音
- 通过独立按键控制数码管的显示。引入独立按键和数码管显示的相关头文件,参考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);
}
}
}
- 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);
}
}
}
- 蜂鸣器发声时间太短,用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 蜂鸣器播放音乐
- 借助定时器的中断实现音符间的切换。
引入相关定时器函数,参考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# 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};
- 引入变量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;
}
- 上一步写出的代码经过调试发现,字符都连载一起了,为了使得每个字符之间有停顿,可以暂时关闭定时器再打开。
TR0=0;
Delay(5);
TR0=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显示
- 模块化编程建立I2C和AT24C02相关的.c和.h文件。
- 在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;
}
- 在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;
}
- 在主函数中,调用相关函数经行写入和读取。
引入变量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)
{
}
}
- 按照上一步,并不能实现显示,这是因为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)
{
}
}
- 通过独立按键控制数字的加减,并且实现写入和读出。
引入独立按键模块,参考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 秒表(定时器扫描按键数码管)
- 定时器定时时间到了会进入中断函数,在进入中断后进行扫描。
在中断函数中调用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();
}
}
- 在跳转到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;
}
}
- 建立函数Key,用于输出的调用。
因为Key_KeyNumber不会自动清零,所以引入一个中间变量Temp用来存储Key_KeyNumber的值。
输出Temp的值。
unsigned char Key(void)
{
unsigned char Temp;
Temp=Key_KeyNumber;
Key_KeyNumber=0;
return Temp;
}
- 在主函数中引入变量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();
}
}
- 接下来编写定时器扫描数码管线管代码。参考第一步,在中断函数中再建立一个中断中的中断。
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();
}
}
- 对于中断中的中断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;
}
- 接下来开始用数码管显示秒表。
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 温度读数
- 建立单总线模块,参考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;
}
- 参考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;
}
- 参考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;
}
- 发送一个字节与接收一个字节。
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;
}
- 建立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;
}
- 在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 温度报警
- 参考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);
}
}
- 设置温度的上下限。
#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);
}
}
- 用按键实现最高温度设定值的加减。
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);
}
}
- 设置报警提示。
if(T>THigh)
{
LCD_ShowString(1,13,"OV:H");
}
else if(T<TLow)
{
LCD_ShowString(1,13,"OV:L");
}
else
{
LCD_ShowString(1,13," ");
}
- 参考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 呼吸灯
- 控制第一个LED灯的亮灭,可以给该灯对应的引脚赋0和1。
#include <at89c51RC2.h>
void main()
{
while(1)
{
P2_0=0;
P2_0=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);
}
}
- 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);
}
}
- 实现亮度的变化,可以通过延迟时间的动态变化来实现。
引入变量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);
}
}
}
- 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 直流电机调速
- 参考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;
}
- 引入变量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;
}
}
- 用独立按键控制电机档位,并且用数码管显示当前档位。
引入独立按键模块和数码管模块。
第一个按键实现档位加功能。
引入变量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;
}
}
- 当档位不同时,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);
}
}
- 一直到上面都是控制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模数转换
- 引入LCD1602模块,用LCD1602进行显示。
#include <at89c51RC2.h>
#include <LCD1602.h>
void main()
{
LCD_Init();
LCD_ShowString(1,1,"ADJ");
while(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;
}
- 参考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开始学习,所以笔记里面难免会有些错误。此后笔者也会不断学习,不定期地修改笔记。