一,单片机及开发板介绍
1,基本介绍
- 单片机,英文Micro Controller Unit,简称MCU
- 内部集成了CPU、RAM、ROM、定时器、中断系统、通讯接口等一系列电脑的常用硬件功能
- 单片机的任务是信息采集(依靠传感器)、处理(依靠CPU)和硬件设备(例如电机,LED等)的控制
- 单片机跟计算机相比,单片机算是一个袖珍版计算机,一个芯片就能构成完整的计算机系统。但在性能上,与计算机相差甚远,但单片机成本低、体积小、结构简单,在生活和工业控制领域大有所用同时,学习使用单片机是了解计算机原理与结构的最佳选择
- 单片机上电时所有I/O口默认都为高电平
2,命名规则
以下为STC89C52系列的命名规则,不同系列单片机命名规则可能不同,如STC32G12K128351-LQPF64与其命名规则不同,具体的查STC官网手册
3,内部结构
单片机基本采用8051微处理器为内核,不同系列单片机不同主要是下图除8051微处理器外其他外设的不同,具体系列单片机的不同可以在STC官网查手册
4,最小应用系统
要想让单片机运行起来,需要给外部的一些电路,单片机和能让它运行起来的基本电路叫做最小应用系统,如下图所示,具体系列单片机的不同可以在STC官网查手册
右上为电源电路;左下为晶振电路(有一些单片机有内置晶振,可以不要此电路),为CPU提供时钟,驱动程序一步一步往下走;左中为复位电路,可以让程序从头开始运行,高电平时复位(上电一瞬间电容充电相当于短路,电路只连接上半部分将RST接为高电平,当电容充满时相当于断路,电流流向下半电路R1,此时RST为低电平,达到一个上电复位的效果) 。
二,点亮LED
1,点亮一个LED
下图所示,LED右端接VCC,左端如果为负极则导通,正极则不导通(单片机引脚输出高电平不导通,低电平导通)。
- CPU控制引脚高低电平的原理
MCU(单片机),内有许多个寄存器,寄存器就是存储器。寄存器以8个为一组(也就对应了引脚8个为一组,下图所示为P2系列引脚,每个存储器对应一个引脚),每个存储器都连接了一根线,再通过驱动器来增大电流,然后连接到引脚。CPU通过软件程序直接访问寄存器,给他里面写值,如果值为1,则寄存器输出高电平,如果为0,则寄存器输出低电平
要想让引脚P20输出低电平,就要通过代码实现
#include <REGX52.H>
//其中定义了各个寄存器的地址,这样P2就有了定义
//每个芯片的库是不一样的,右键点击Insert就可以引入该芯片的库
void main(){
P2=0xFE; //要使P20所连接的灯亮,其他灯都不亮,就要使寄存器配置为1111 1110
//单片机软件编程不支持二进制,所以就要将二进制转换为十六进制
}
//上述是一次操作了八位寄存器,也可以直接操作一位寄存器,写为P2_0=0;
//注:P2_0这种1位寄存器只在REGX52.H中有定义,在REG52.H中没有定义
2,LED闪烁
- 创建延时函数
第一步在STC-ISP中找到软件延时计算器并选择
第二步,修改系统频率为晶振频率。选择定时长度(即延时时间)。选择8051指令集,可以看又边框你选择的指令集适用于什么系列。
第三步
点击生成C代码,复制代码,然后回到Keil软件将代码粘贴,这样在使用的时候直接调用即可。
- 代码演示
#include <REGX52.H>
#include <INTRINS.H> #此头文件中定义了很多函数,其中包括_nop_()
void Delay500ms(void) //@12.000MHz
{
unsigned char data i, j, k;
_nop_();
i = 4;
j = 205;
k = 187;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
void main(){
while(1){
P2=0xFE;
Delay500ms();
P2=0xFF;
Delay500ms();
}
}
扩展:对延时函数进行修改,可以指定延时多少毫秒
void Delay1ms(int x) //@12.000MHz
{
unsigned char data i, j;
while(x){
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
x=x-1;
}
}
//在延时1ms的函数上进行修改
3,LED流水灯
#include <REGX52.H>
#include <INTRINS.H> #此头文件中定义了很多函数,其中包括_nop_()
void Delay500ms(void) //@12.000MHz
{
unsigned char data i, j, k;
_nop_();
i = 4;
j = 205;
k = 187;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
void main(){
while(1){
P2=0xFE; //1111 1110亮第一个
Delay500ms();
P2=0xFD; //1111 1101亮第二个
Delay500ms();
P2=0xFB; //1111 1011亮第三个
Delay500ms();
P2=0xF7; //1111 0111亮第四个
Delay500ms();
P2=0xEF; //1110 1111
Delay500ms();
P2=0xDF; //1101 1111
Delay500ms();
P2=0xBF; //1011 1111
Delay500ms();
P2=0x7F; //0111 1111
Delay500ms();
}
}
三,独立按键控制LED
轻触按键相当于电子开关,按下时接通,松开时断开
1,按下开关亮,松开就灭
#include <REGX52.H>
void main(){
while(1){
if(P3_1==0){//开关连接的I/O口为P3_1,单片机接通时,所有I/O口都为高电平。由于开关另一端接
//地,所以开关按下时,P3_1输出低电平。
P2_0=0;//点亮第一个LED灯
}else{
P2_0=1;
}
}
}
2,按一下开关亮,松开后仍亮,再按一次灭
对于机械开关,当机械触电断开,闭合时,由于机械触点的弹性作用,一个开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开,所以在开关闭合及断开的瞬间会伴随一连串的抖动。为了解决这个问题,可以在按下时延时几十毫秒,松开时也延时几十毫秒。
#include <REGX52.H>
#include <INTRINS.H>
void Delay1ms(int x) //@12.000MHz
{
unsigned char data i, j;
while(x){
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
x=x-1;
}
}
void mian(){
while(1){
if(P3_1==0){
Delay1ms(20);
while(P3_1==0);
Delay1ms(20);
P2_0=~P2_0;//位运算取反,可以让P2_0亮或者灭
}
}
}
3,LED按照1~9的二进制形式依次闪烁
#include <REGX52.H>
#include <INTRINS.H>
void Delay1ms(int x) //@12.000MHz
{
unsigned char data i, j;
while(x){
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
x=x-1;
}
}
void mian(){
unsigned int LEDNum=0;//设置一个变量表示数字1~9,因为寄存器8个为一组,而char为8个字符,所
//以用char
while(1){
if(P3_1==0){
Delay1ms(20);
while(P3_1==0);
Delay1ms(20);
LEDNum++; //LEDNum不断增加,即依次表示1~9
P2=~LEDNum;//当单片机通电后所有引脚默认为高电平1,根据点亮LED工程中所说的
//二极管负极接引脚,所以当引脚输出低电平时二极管才能点亮
}
}
}
//P2++:因为所有引脚默认为高电平,所以为1111 1111,P2++超出范围变为0000 0000,
//再P2++,变为1111 1111
4,LED依次点亮(按一下开关移位一次)
#include <REGX52.H>
#include <INTRINS.H>
void Delay1ms(int x) //@12.000MHz
{
unsigned char data i, j;
while(x){
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
x=x-1;
}
}
void mian(){
unsigned int LEDNum=0;//设置一个变量表示数字1~9
while(1){
if(P3_1==0){
Delay1ms(20);
while(P3_1==0);
Delay1ms(20);
LEDNum++;
if(LEDNum>=8) LEDNum=0;
P2=~(0x00<<LEDNum);//每次左移一位
}
}
}
四,数码管
1,数码管介绍
(1)一位LED数码管
一位数码管共有8个LED。其内部连接如下图,为了减少数码管的引脚数,在数码管内将8个LED的正极或负极引脚连接起来,接成一个公共端(COM端),根据公共端是正极还是负极,可分为共阳极和共阴极。数码管共十个引脚,左下角的为1号引脚,逆时针递增,中间的两个引脚(即3号和8号引脚为两个公共端)。
(2)多位LED数码管
如图为4位数码管,他有两排12个引脚。内部电路如图所示,也分为共阳极和共阴极两种方式。控制方式:比如说要控制第二个数码管显示数字1(共阳极),我们就可以使9号引脚为高电平,再令11号引脚为低电平(假如11号引脚控制的是最右段LED),这样就可以使第二个数码管显示一,这叫做静态显示。但是这种方法只能让单个数码管显示或者多个数码管显示同一个数字,为了让不同数码管显示不同数字,我们就要采用动态显示的方法,利用人眼的暂留特性,先显示第一位,再快速显示第二位,这样就看起来好像两位在同时点亮。
2,数码管驱动器件
下图所示为单片机学习板数码管部分的原理图
- 138译码器
P22,P23,P24 接单片机I/O口,引脚Y0~Y7分别接8个数码管的8个公共端,此译码器的作用是减少单片机I/O口的占用,将8个引脚转为P22,P23,P24这3个引脚控制(所以叫138译码器)。原理:二进制转化。P24,P23,P22(C为最高位,也就是P24为最高位)可以表示二进制111,转化为十进制就是7,而P24,P23,P22为000时转为十进制就为0,所以P24,P23,P22可以表示0~7这8个数,也就对应了Y0~Y7。当CBA分别为001时就对应了Y1为0,其他都为1;当CBA为011时就代表Y3为0,其他都为1。
左下角的G1,G2A,G2B叫做使能端,相当于一种电源开关。G1接高电平,G2A,G2B接上低电平,这个芯片就能工作。除此之外,这个芯片还需要电源和接地。
- 双向数据缓冲器
VCC和GND为电源。OE是这个芯片的使能,如图所示它连接了低电平所以这个芯片可以工作(低电平有效)。DIR(direction)引脚,也就是方向的意思,主要用于控制将左边数据缓冲到右边还是右边数据缓冲到左边(如果DIR接高电平就是从左到右,如果接低电平就是从右往左),如图所示,它接LE引脚,LE为跳线帽,它插在哪个地方就把两个引脚给短路,实物图中将LE插到VCC,所以此缓冲器从左往右缓冲。A0连接B0,A1连接B1,A2连接B2,依次类推,它就是起一个数据缓冲的作用。
需要缓冲的原因:因为单片机高电平驱动能力有限,输出最大电流不能太大 ,低电平驱动能力强一些(所以LED通常采用低电平点亮)。所以如果没有缓冲器直接与单片机连接,它的电流会很小,灯会暗。加上缓冲器后可以提高驱动能力,单片机的高电平会作为信号(只需要微弱的信号即可被缓冲器接收到)进入缓冲器,然后缓冲器用自己的电源为数码管提供高电平
3,静态数码管显示
#include <REGX52.H>
unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x39,0x5E,0x79,0x71,0x00};//分别对应0~9,A,B,C,D,E,F,空
void Nixie(unsigned char Location, Number){
switch(Location)//Location代表哪个数码管显示
{
case 1:P2_4=1;P2_3=1;P2_2=1;break;
case 2:P2_4=1;P2_3=1;P2_2=0;break;
case 3:P2_4=1;P2_3=0;P2_2=1;break;
case 4:P2_4=1;P2_3=0;P2_2=0;break;
case 5:P2_4=0;P2_3=1;P2_2=1;break;
case 6:P2_4=0;P2_3=1;P2_2=0;break;
case 7:P2_4=0;P2_3=0;P2_2=1;break;
case 8:P2_4=0;P2_3=0;P2_2=0;break;
}
P0=NixieTable[Number];//要显示的数字
}
void main(){
while(1){
Nixie(3,2);//第3个晶体管显示2
}
}
4,动态数码管显示
在操作数码管时,如果仅是下方代码会出现下图所示的数码管重影现象,这是因为数码管显示时会有一个位选和段选的过程(一般地,操作数码管时,先执行段选再执行位选。位选是选择待操作的数码管,如开发板上的是8位数码管,位选就是选择8位数码管中的某一个。段选是选择数码管里面的LED灯,即通过选择点亮响应的LED灯以达到显示需要的数据的目的。)但是由于单片机速度很快,位选-段选-位选-段选————的过程就会变为段选-位选-段选-位选的过程(再选中下一位时上一位还没有完全消失直接串到下一位,导致重影)
#include <REGX52.H>
void Delay1ms(int x) //@12.000MHz
{
unsigned char data i, j;
while(x){
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
x=x-1;
}
}
unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x39,0x5E,0x79,0x71,0x00};//分别对应0~9,A,B,C,D,E,F,空
void Nixie(unsigned char Location, Number){
switch(Location)//Location代表哪个数码管显示
{
case 1:P2_4=1;P2_3=1;P2_2=1;break;
case 2:P2_4=1;P2_3=1;P2_2=0;break;
case 3:P2_4=1;P2_3=0;P2_2=1;break;
case 4:P2_4=1;P2_3=0;P2_2=0;break;
case 5:P2_4=0;P2_3=1;P2_2=1;break;
case 6:P2_4=0;P2_3=1;P2_2=0;break;
case 7:P2_4=0;P2_3=0;P2_2=1;break;
case 8:P2_4=0;P2_3=0;P2_2=0;break;
}
P0=NixieTable[Number];//要显示的数字
}
void main(){
while(1){
Nixie(3,2);//第3个晶体管显示2
Nixie(2,4);
Nixie(1,5);
}
}
为了避免这个问题,我们就需要在段选之后把它清零 ,以下为正确代码
#include <REGX52.H>
void Delay1ms(int x) //@12.000MHz
{
unsigned char data i, j;
while(x){
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
x=x-1;
}
}
unsigned char NixieTable[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x39,0x5E,0x79,0x71,0x00};//分别对应0~9,A,B,C,D,E,F,空
void Nixie(unsigned char Location, Number){
switch(Location)//Location代表哪个数码管显示
{
case 1:P2_4=1;P2_3=1;P2_2=1;break;
case 2:P2_4=1;P2_3=1;P2_2=0;break;
case 3:P2_4=1;P2_3=0;P2_2=1;break;
case 4:P2_4=1;P2_3=0;P2_2=0;break;
case 5:P2_4=0;P2_3=1;P2_2=1;break;
case 6:P2_4=0;P2_3=1;P2_2=0;break;
case 7:P2_4=0;P2_3=0;P2_2=1;break;
case 8:P2_4=0;P2_3=0;P2_2=0;break;
}
P0=NixieTable[Number];//要显示的数字
Delay(1);//使其稳定显示,否则数码管会变暗
P0=0x00;//清零
}
void main(){
while(1){
Nixie(3,2);//第3个晶体管显示2
Nixie(2,4);
Nixie(1,5);
}
}
- 补充
上述方法属于单片机直接扫描的方法,就是不断给单片机输入要显示的数据。这种方法对硬件设备要求简单,但会耗费大量CPU时间。
专用驱动芯片扫描的方法:其内部自带显存,扫描电路,单片机只需要按照通讯协议告诉它显示什么即可 (TM1640专用驱动芯片扫描)(74HC595三根线即可驱动)
五,LCD1602调试工具
使用LCD1602液晶屏作为调试窗口,提供类似printf函数的功能,可实时观察单片机内部数据的变换情况,便于调试和演示。也可使用串口进行调试
这里提供的LCD1602代码属于模块化的代码,只需要知道这个函数的作用和使用方法即可。这些代码是需要自己编写的,有具体的.c文件,.h文件
使用之前必须先初始化
#include <REGX52.H>
#include "LCD1602.H"
void main(){
LCD_Init();
LCD_ShowChar(1,1,'A');//在第一行第一列显示A
LCD_ShowString(1,3,"Hello");//在第一行第三列开始显示Hello
LCD_ShowNum(1,9,123,3);//在第一行第九咧显示123,显示位数为3
LCD_ShowSignedNum(1,13,-66);
}
六,矩阵键盘
在键盘中按键数量较多时,为了减少I/O口的占用,通常将按键排列成矩阵形式。采用逐行或逐列的扫描,就可以读出任何按键的状态。数码管的扫描也是矩阵的形式,它是输出扫描,矩阵键盘属于输入扫描。下图为矩阵键盘的原理图
矩阵按键读取过程:如上图,如果给P10低电平0,读取P17,P16,P15,P14的引脚状态,如果P17输入为低电平,则表示按键S4按下,如果P16输入为低电平,则表示S8按下。
单片机的I/O口为一种弱上拉模式,又叫准双向口,这使得一个I/O口既可以输入,又可以输出。为什么上述按键读取过程中,P17接触了低电平P10后读取为低电平,而不是他本身输出的高电平?这是因为单片机弱上拉模式的内部结构简单图为下图,当要输出高电平时就把开关接到VCC,要输出低电平就把开关接到地;读取时在图中节点引出的电路,然后经过施密特触发器等后续电路来读取。这样的话如果I/O口接地,那么读取的就是接地的低电平0,而不会受输出高电平的影响。
1,利用LCD1602显示按下的矩阵键盘的键码值
#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
int MatrixKey(){
int KeyNumber=0;
P1=0xFF;//将P1的I/O口全部置为0
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=5;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=9;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=13;}
P1=0xFF;//将P1的I/O口全部置为0
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=6;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=10;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=14;}
P1=0xFF;//将P1的I/O口全部置为0
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=7;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=11;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=15;}
P1=0xFF;//将P1的I/O口全部置为0
P1_0=0;
if(P1_7==0){Delay(20);while(P1_7==0);Delay(20);KeyNumber=4;}//按键检测
if(P1_6==0){Delay(20);while(P1_6==0);Delay(20);KeyNumber=8;}
if(P1_5==0){Delay(20);while(P1_5==0);Delay(20);KeyNumber=12;}
if(P1_4==0){Delay(20);while(P1_4==0);Delay(20);KeyNumber=16;}
return KeyNumber;
}
void main(){
LCD_Init();
while(1){
int a=MatrixKey();
if(a){
LCD_ShowNum(1,1,a,2);//在第一行第九咧显示123,显示位数为3
}
}
}
2,矩阵键盘密码锁
S1~S9表示数字1~9,S10表示数字0,S11为确定,S12为取消
七,定时器(计数器)
1,定时器介绍
定时器属于单片机内部资源,其电路的连接和运转均在单片机内部完成。
定时器作用:1,用于计时系统,可实现软件计时,或是程序每隔一段时间完成一项操作2,替代长时间的延时函数,提高CPU运行效率和处理速度(延时函数会使CPU进入等待,直到延时时间结束CPU才开始重新运行,这就说明CPU在延时的这段时间内无法完成其他事情。而利用定时器来延时的话,CPU就可以做其他事情。)
STC89C52中有3个定时器T0,T1,T2,T0和T1与传统的51单片机兼容,T2是此型号单片机增加的资源(不同型号的单片机定时器的个数和操作方式有所不同,但一般T0,T1的操作方式是51单片机所共有的)
2,定时器原理
(1)运行框图
定时器在单片机内部相当于一个闹钟,根据时钟的输出信号,每来一次脉冲计数单元就增加一,当计数单元数值增加到“设定的闹钟提醒时间”时,计数单元就会向中断系统发出中断申请,产生闹铃提醒,使程序跳转到中断服务函数中执行
(2)定时器内部模式及模式1原理
- 工作模式:
STC89C52的T0和T1均有四种工作模式:
模式0:13位定时器/计数器;模式1:16位定时器/计数器(最常用);模式2:8位自动重装模式;模式3:两个8位计数器
- 模式1原理:
如下图所示,其内部电路按照运行框图共分为三部分。
- 计数器
可以看到计数器中的TL1(timer low)和TH1(timer high),他就是一个16位的计数器(由两个字节组成,高字节为TH,低字节为TL。1表示为定时器T1),2个字节最大能计数65535。左边的时钟为计数器提供脉冲,每来一个脉冲计数器就加一。当加到最大的65535时,再来一个脉冲计数器就会归0(当计数器达到最大值时,给TF1一个标志位,然后传给中断系统产生中断)。计数器下方为它的控制位,可以控制计数器启动或者暂停
- 时钟
脉冲的时间由SYSclk来确定,它是系统时钟,即晶振周期(STC89C52晶振频率为12MHz)。时钟有两个来源,一个是SYSclk,一个是T0 Pin,T0 Pin是单片机上的一个引脚,也就是说单片机的时钟可以由单片机自己提供,也可以由引脚连接的外设提供,当由外设提供时,定时器就变成了一个计数器。SYSclk提供晶振频率后,会经过分频(如上图电路),+12表示12分频,输出的就为1MHz,表示一次为1微秒记一次数,当记到最大值就会产生中断;+6表示6分频,每个2微秒记一次数。电路之后的C/T表示开关,如果给高电平1,那么开关就会连接到T0 Pin,如果给低电平0,开关就会连接到SYSclk。
- 中断系统
中断资源适合单片机型号有关的,不同型号的单片机拥有的中断资源不同,例如中断源个数不同,中断优先级个数不同等。
STC89C52中有8个中断源(外部中断0,定时器0中断,外部中断1,定时器1中断,串口中断,外部中断2,外部中断3) ,4个中断优先级
3,定时器寄存器
(1)寄存器作用介绍
让中断按照我们想要的方式运行,就要依靠定时相关寄存器。单片机中寄存器就是一种特殊的RAM,一方面它可以存储和读取数据,另一方面,每个寄存器背后都连接了一根导线,控制着电路的连接方式。寄存器相当于一个复杂机器的操作按钮(单片机通过寄存器配置内部线路的连接)
寄存器就是用来控制下图电路中的开关,如C/T寄存器可以控制C/T开关拨到那个位置
(3)定时器/计数器控制寄存器TCON(timer control)
以下为官方手册上的内容
可以结合电路上的寄存器进行理解
(3)定时器/计数器工作模式寄存器 (timer mode)
- GATE寄存器
GATE:计数器的启动暂停可以由控制寄存器的TR1直接控制,也可以由TR0和外设INT1来联合控制(INT1为单片机引脚)。GATE就可以选择是TR1单独控制还是联合控制
控制原理:可以看下图电路GATE经过非门,然后经过或门,最后通过与门。如果GATE置0,那么经过非门信号就变为1,由于是或门,有1就为1,所以不管INT1是0还是1,或门输出的都为1,这时就不受INT1控制;如果GATE为1,经过非门为0,这时INT1为1则输出1,为0则输出0,也就是受INT1控制
- 其他寄存器
(4)TH,TL寄存器
前面说过,来计数的寄存器
4,中断寄存器
(1) 中断允许寄存器IE和XICON
EA为总中断允许控制位,EA=0所有的中断都会关闭,如下简化图,EA相当于总开关
其他的如ET2,ES,ET1,EX1,ET0,EX0就是控制单条路的开关
(2)中断优先级控制寄存器IP/XICON和IPH
5,程序实现定时器计时
- 配置定时器模式TMOD:
配置定时器模式TMOD(启动定时器0,并且定时器0处于16位定时器模式,根据前面的表格,可知M1=0,M0=1时定时器0处于16位定时器模式),即TMOD=0000 0001,换算为16进制为0x01。注意:这里的TMOD上写着不可位寻址表示的是只能整体赋值,而TCON上写着可位寻址则表示可以给单独一个寄存器为1或者0.
- 配置TCON:
然后根据下表配置TCON。着重说一下配置计数器的寄存器TL0和TH0,我们知道这个寄存器能记录0~65535的次数,当达到65535时就请求中断,在STC89C52中晶振频率12MHz,12分频后也就是每隔1微秒计数加1,总共可以定时65535微秒,也就65毫秒左右。如果想让它定时1s,我们可以先让他记满1毫秒产生中断,然后再记1毫秒,这样记1000次就可以达到计数1s的目的。为了让他一次能记1ms,需要将它初始化为64535
- 配置中断
按照电路图配置即可
- 中断函数
中断函数(即达到规定时间后,需要CPU做什么),这里我们用的定时器0,就需要使用C这个函数.中断函数中进行简短的任务,执行的任务时间不能太长
- 最终代码
#include <REGX52.H>
void Timer0_Init(){
TMOD=0x01;//配置模式0000 0001
//如果使用两个定时器时,配置第一个而不影响第二个,配置第二个而不影响第一个,
//就可以使用与或的方法,如配置定时器0而不影响定时器1 TMOD=TMOD&0xF0 TMOD=TMOD|0x01
//第一步与使得高四位不变,低四位清零,第二步或可以使高四位不变,
//低四位按要求改变(这里是将低四位置为0001)
TF0=0;//中断溢出标志位要先清0
TR0=1;//开启定时器0
TH0=64535/256;
TL0=64535%256;//或者用程序员计算器计算65532的十六进制
ET0=1;//中断配置
EA=1;
PT0=0;
}
void main(){
Timer0_Init();
while(1)
{
}
}
int T0_Count;//计数变量,当其为1000时说明记了1000个1ms,即1s
void Timer0_Rountine(void) interrupt 1
{
TH0=64535/256;
TL0=64535%256;//每次记1ms后都赋初值,使其从64535开始记
T0_Count++;
if(T0_Count>=1000){
T0_Count=0;
//加上需要执行的操作
}
}
也可以使用STC-ISP来自动配置定时器 如下图
再定时器计算器中选择好时钟频率,定时长度(不能太长),定时器模式(根据所需要的定时器模式选择),定时器时钟(根据定时器原理图可知为12T)
复制过来后要做修改,比如STC89C52没有AUXR配置12T模式,而是系统模式已经配置好了(新版本有AUXR配置模式),所以第一行删除。除此之外,此代码没有中断的配置,需要自己加上。
6,应用1:按键控制LED流水灯&定时器
介绍两个函数,这两个函数包含在函数库#Include <INTRINS.H>中
unsigned char _cror_(unsigned char, usigned char); 和 unsigned char _crol_(unsigned char, usigned char);分别指循环右移和循环左移。第一个形参表示要移位的数值,第二个参数表示移多少位 比如
unsigned char a=0x01;
a=_cror_(a,1);//这时a等于0x02
a=_cror_(a,2);//这时a等于0x04
//这样看它和<<左移没什么区别。
//但它们的区别是当a移到最高位0x80时他就会回到最开始的0x01,以此来循环移位
//_crol_同理
#include <REGX52.H>
#include "Timer0.h"
#include "Key.h"
#include <INTRINS.H>
unsigned char KeyNum,LEDMode;
void main()
{
P2=0xFE;//先将LED等的最后一位点亮
Timer0Init();//定时器初始化
while(1)
{
KeyNum=Key(); //获取独立按键键码
if(KeyNum) //如果按键按下
{
if(KeyNum==1) //如果K1按键按下
{
LEDMode++; //模式切换
if(LEDMode>=2)LEDMode=0;
}
}
}
}
void Timer0_Routine() interrupt 1
{
static unsigned int T0Count;
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count++; //T0Count计次,对中断频率进行分频
if(T0Count>=500)//分频500次,500ms
{
T0Count=0;
if(LEDMode==0) //模式判断
P2=_crol_(P2,1); //LED输出
if(LEDMode==1)
P2=_cror_(P2,1);
}
}
//自写版本
#include <REGX52.H>
#include "LCD1602.h"
#include "Key.h"
#include "Timer0.h"
unsigned char KeyNum=0,LEDMode=0;
int time_count=0;
int a=0x01;
void main(){
Timer0_Init();
while(1){
KeyNum=Key();
if(KeyNum==1)
{
LEDMode++;
if(LEDMode>=3)
{
LEDMode=1;
}
}
}
}
void Timer0_Routine() interrupt 1
{
TL0=0x17;
TH0=0xFC;
time_count++;
if(time_count>=500){
time_count=0;
if(LEDMode==1)
{ if(a>128) a=1;
P2=~a;
a=a<<1;
}
if(LEDMode==2)
{
if(a<1) a=128;
P2=~a;
a=a>>1;
}
}
}
7,应用2:定时器时钟
#include <REGX52.H>
#include "Delay.h"
#include "LCD1602.h"
#include "Timer0.h"
unsigned char Sec=55,Min=59,Hour=23;
void main()
{
LCD_Init();
Timer0Init();
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 unsigned int T0Count;
TL0 = 0x18; //设置定时初值
TH0 = 0xFC; //设置定时初值
T0Count++;
if(T0Count>=1000) //定时器分频,1s
{
T0Count=0;
Sec++; //1秒到,Sec自增
if(Sec>=60)
{
Sec=0; //60秒到,Sec清0,Min自增
Min++;
if(Min>=60)
{
Min=0; //60分钟到,Min清0,Hour自增
Hour++;
if(Hour>=24)
{
Hour=0; //24小时到,Hour清0
}
}
}
}
}
//自写版本
#include <REGX52.H>
#include "LCD1602.h"
#include "Key.h"
#include "Timer0.h"
#include "Delay.h"
unsigned char KeyNum=0,LEDMode=0;
int time_count=0;
int s,h,m;
void main(){
Timer0_Init();
LCD_Init();
LCD_ShowString(1,1,"CLOCK:");
LCD_ShowString(2,1," : : ");
while(1){
LCD_ShowNum(2,1,h,2);
LCD_ShowNum(2,4,m,2);
LCD_ShowNum(2,7,s,2);
}
}
void Timer0_Routine() interrupt 1
{
TL0=0x17;
TH0=0xFC;
time_count++;
if(time_count>=1000){
time_count=0;
s++;
if(s>=60){m++;s=0;}
if(m>=60){h++;m=0;}
}
}
八,串口通信
1,串口理论知识介绍
(1)串口基本介绍
串口是一种应用十分广泛的通讯接口,可实现两个设备的互相通信。单片机的串口可以实现单片机与单片机,单片机与电脑,单片机与各式各样的模块互相通信。
51单片机内部自带UART,可实现单片机的串口通信
(2)串口数据发送(STC-ISP)
用单片机给电脑发送数据时,可以用STC-ISP中的串口助手的接收缓冲区查看。也可以用电脑像单片机发送数据以实现某些功能,在发送缓冲区输入要发送的数据
(3)串口连接方式
简单的双向串口通信有两根通信线(发送端TXD和接收端RXD),TXD和RXD要交叉连接(如下图)。当只需要单向传输数据时,可以直接用一根通信线。此外,复杂的串口会有很多通讯接口,如D89母头
注意:当电平标准不一致时,需要使用电平转换芯片(电平标准如下)
- 电平标准:
电平标准是数据1和数据0的表达方式,是传输线缆中人为规定的电压和数据的对应关系,串口常用的电平标准有三种 :
TTL电平:+5V表示1,0V表示0
RS232电平:-15~-3V表示1,+3~+15V表示0
RS485电平:两线压差+2~+6V表示1,-2~-6V表示0(差分信号)
(4)常见串口比较
点对点通信:指两个设备之间进行通信。
CAN主要用于汽车领域,使用差分信号,传输距离远,稳定性好
异步,同步进一步解释:通信双方发送数据时,比如A发10(高低电平),B收到的是10(高低电平)。A再发1100,B如果接受的速率不同,就可能会接收成10(A发送1个A用时1ms,而B为2ms接收一次,这样B接受的就变为1),所以要保证接受速率相同,比如A每隔1ms发送一次那么B就要每隔1ms接受一次,这就是异步,双方规定一个相同的速率发送和接收。同步是通过一根时钟线,只检测这一根线,当A给一个上升沿(高电平),B就采样一次,这样来达到目的,同步用于对时间要求严格,它可以做到同时发送与接收。可以看一下上面不同的串口,有同步的引脚都需要SCL或SCLK这样的时钟线
总线进一步理解:可挂载多个设备的都用到总线(可以用下图理解)
(5)STC89C52内部串口
(6)串口参数和时序图
波特率:串口通信的速率(发送和接收数据位的间隔时间)
校验位:用于数据检验,比如STC89C52的8位UART和9位UART,8位UART表示一个字节,9位UART就是一个字节加上最后一位校验位,可以检验前面数据的正确性。常用的是奇偶校验,以奇校验为例,双方约定了使用奇校验,A发送0000 0011这个是1个数为偶数,所以再加一位1,发送数据为0000 0011 1,B接收时,如果1的个数变为偶数,则表示数据错误;如果A发送为0000 0001,则再加一位0,发送数据1为0000 0001 0,B接收时,如果1的个数变为偶数,则表示数据错误。但是如果两个数据同时反转,1还为奇数,这样就检测不出来,所以这种方法准确率并不高。
停止位:用于数据帧间隔(一个数据发送完后停止一小段时间)。数据是一个一个发送的,比如一个字节有8个位,数据就是1位1位的发送,先发低位,再发高位,接收也是一位一位的收
(7)串口模式图
下图所示。数据只有到达单片机的总线,单片机才能接收处理这些数据。红框内的电路是用来控制波特率,也就是说通信双方的速率主要是由定时器来约定的,TH1和TL1是定时器寄存器,通过溢出率经2分频,16分频等来控制收发器的采样时间,使用时配置T1定时器的TH1,TL1来控制收发速率
SBUF:串口数据缓存寄存器,物理上是两个独立的寄存器,但占用相同的地址。写操作时,写入的是发送寄存器,读操作时,读出的是接收寄存器。
发送时先将数据写入SBUF中,再通过发送控制器的控制将数据发送出去;接收时,从RXD接收数据回来经过移位寄存器一位位的移到SBUF中,然后需要数据时直接读缓存就好了。写程序时,当SBUF在等号左边,意味着给SBUF赋值,也就是要发送的数据写入SBUF;如果SBUF在等号右边,意味着取SBUF的值,也就是要读取SBUF存的数据。当收到一位数据时,接收控制器就会产生一个叫RI的接收中断,提示可以取数据了;发送数据时,数据发送完发送控制器会产生一个叫TI的中断,提示发送数据完毕
加上去中断逻辑的电路图如下
单片机通过如下电路通过USB与电脑连接
2,STC89C52的串行口寄存器
3,单片机向电脑发送数据
(1)配置寄存器(串口初始化)
- 配置SCON
SCON是串行控制寄存器,用于选择串行通信的工作方式和某些控制功能。PCON是波特率选择特殊功能寄存器
这里我们使用串口通讯的模式二,下面是软件实现方式。在PCON中对SMOD和SMOD0的描述(下图)可以得知SCON中的SM0/FE有两个作用,当SMOD0变化时,这两个作用切换,当SMOD0=0时,处于SM0模式,此时它和SM1搭配就可以配出四种模式。令SM0=0,SM1=1就可以使串口处于模式一。
接着配置SCON的其他寄存器。如下图,SM2用来控制方式2和3的,我们使用的是方式1,所以此位置为0;REN位串行接收控制位的开关,当需要单片机接收数据时我们置为1;TB8和RB8均为方式2和3的,置为0;TI和RI分别为发送和接收的中断请求标志位,我们在模式图中看过,注意,当数据发送或接收完毕后,它是由系统置为1,需要我们用软件重新置为0。
所以总结来说SCON应用二进制表示为0100 0000.由于SCON可以位寻址,所以可以直接用SM1=1等单独表示
- 配置PCON
波特率加倍可以减少误差(可以参照下方配置定时器内容),所以SMOD置为1.PCON配置为1000 0000.即PCON |=0x80
- 配置SBUF
SBUF用于往里面写数据或者从里面取数据,初始化阶段不需要配置
- 配置定时器
根据下图可以知道控制波特率需要先配置定时器,图中是定时器T1。串口定时器需要使用8位自动重装模式,原来的16位不自动重装模式并不精准,而波特率变化是很快的,所以我们需要更精准的8位自动重装模式
配置为模式8位自动重装,则TMOD=0010 0000.则写为TMOD &=0x0F; (前四位清零)TMOD |= 0x20;(配置前四位为0010)
利用STC-ISP配置定时器和波特率,注意勾选波特率倍速,勾选波特率倍速可以降低误差。这里的定时器只做溢出波特率发生器(只要有溢出即可)(参照模式图),而不进入中断,所以这里会关闭中断
(2)发送数据函数编写
#include <REGX.H>
#include "Dalay.h"
void Uart1_Init(void) //4800bps@12.000MHz
{
PCON |= 0x80; //使能波特率倍速位SMOD
SCON = 0x40; //8位数据,可变波特率
TMOD &= 0x0F; //设置定时器模式
TMOD |= 0x20; //设置定时器模式
TL1 = 0xF3; //设置定时初始值
TH1 = 0xF3; //设置定时重载值
ET1 = 0; //禁止定时器中断
TR1 = 1; //定时器1开始计时
}
void UART_SendByte(unsigned char Byte)
{
SBUF=Byte;
while(TI==0);
TI=0;//软件复位标志位
}
void main(){
Uart1_Init();
UART_SendByte(0x66);
while(1){
//如果把UART_SendByte(0x66);放到while里面,让单片机一直发数据
//电脑接收的数据就会出现问题,这是因为误差的原因(整数晶振频率存在误差)
//小数晶振频率如11.0592MHz误差就可以达到0。对于整数晶振频率如本例所用的
//12MHz就会存在误差,所以可以在每次UART_SendByte(0x66);完后加一个delay(10)
//还有一点就是可以降低波特率,波特率越低数据越稳定
}
}
接收数据时一定要把波特率调为发送数据设备的波特率,这里单片机发送数据为4800,接收时波特率也要调味4800. 校验位一般设置为无校验,停止位设为1位
文本模式,HEX模式:HEX模式是十六进制模式,比如接收缓冲区为HEX模式时,单片机发送0x23,则接收缓冲区接收为23.如果接收缓冲区为文本模式,则根据ASCⅡ表将0x23接收为C。当然,单片机发送的数据也可以是字符‘A’等。
4,电脑向单片机发送数据
电脑通过串口控制LED:
单片机接收数据时需要使用中断函数,因为不知道电脑何时会发数据,所以利用中断系统每当发过来数据就接收它。
(1)配置寄存器
在单片机向电脑发送数据时说过SCON寄存器,其中有一个REN,当要接收数据时就要将这个REN置为1。和前面一样的模式,SM0=0,SM1=1,其他位为0,所以SCON=0x50
配置中断:EA=1(打开中断总开关),ES=1(打开串口中断开关)
配置完成后,当中断来时跳转到中断函数 ,下面的UART_Rountine就是所需要的中断函数
(2)代码实现
以下代码可以实现电脑发送数据控制LED,而且还可以将单片机接收的数据再发给电脑
#include <REGX.H>
#include "Dalay.h"
void Uart1_Init(void) //4800bps@12.000MHz
{
PCON |= 0x80; //使能波特率倍速位SMOD
SCON = 0x50; //8位数据,可变波特率
TMOD &= 0x0F; //设置定时器模式
TMOD |= 0x20; //设置定时器模式
TL1 = 0x64; //设置定时初始值
TH1 = 0x64; //设置定时重载值
ET1 = 0; //禁止定时器中断
TR1 = 1; //定时器1开始计时
EA=1;
ES=1;
}
void UART_SendByte(unsigned char Byte)
{
SBUF=byte;
while(TI==0);
TI=0;//软件复位标志位
}
void UART_Routine() interrupt 4
{
if(RI==1)//因为接收,发送都会触发中断,这里保证是单片机接收数据中断
{
P2=SBUF;
UART_SendByte(SBUF);//注意要写在中断函数,不能再写在主函数
RI=0;
}
}
void main(){
Uart1_Init();
UART_SendByte(0x66);
while(1){
}
}
九,LED点阵屏
1,点阵屏介绍
LED点阵屏由若干个独立的LED组成,LED以矩阵的形式排列,以灯的亮灭来显示图像。
LED点阵屏按颜色分为单色,双色,全彩(每个LED中由红绿蓝三个LED)。大规模LED点阵通常由很多个小点阵拼接而成。
2,显示原理
LED点阵屏结构类似于数码管,有共阴和共阳两种接法。LED点阵屏需要进行逐行或逐列扫描才能使所有LED同时显示
3,单片机控制LED点阵屏原理
LED点阵屏有16个引脚接口,如果全部连接在单片机上就会浪费单片机引脚资源,所以使用74HC595来扩展引脚(如下图)
74HC595工作原理:
74HC595是串行输入并行输出的移位寄存器,可用三根线输入串行数据,8根线输出并行数据,多片级联后,还可输出16为,24位,32位等,常用于IO口扩展。
它的左端P35,P36,P34接单片机引脚,也就是通过3个单片机引脚控制LED点阵屏8个引脚D0~D7。 OE是输出使能,其上加了横线表示低电平有效,也就是OE引脚接低电平这个芯片才有效。SRCLR叫做串行清零端,会把数据进行清空,其上加了横线表示低电平时才会清空,原理图中SRCLR接VCC表示不清空。QH‘用于多片级联。SER表示串行数据(数据分为串行数据和并行数据,串行就是数据根据时钟一个一个的发送,并行就是同时给多个数据,比如同时给D0~D7八个引脚数据)
SRCLK为串行时钟,当时钟每来一个上升沿,SER串行数据就输入一个数据到移位寄存器(如下图左为移位寄存器,右为缓存器)。当RCLK来一个上升沿时,就会把移位寄存器内的八个数据同时送到缓存器。比如,要给QA~QH输出0000 0101,由于单片机上电后默认IO口为高电平,所以开始要给SERCLK初始化为低电平,将RCLK也初始化为低电平,SER串行口输入数据需要先填到QH的移位寄存器,也就是先给SER写1,当给SER写1时,把SERCLK设置为高电平,这时1就到了移位寄存器的第一个格,再对ERCLK清零和SER清零,下一个数据SER输入为0,再给SERCLK输入高电平,0就到了刚才1的位置,1往下移动一格。不断输入数据,当输入8个数据后,设置RCLK为高电平,数据就会从移位寄存器搬到缓存器再进行输出。如果想要多片联结输出多位,就再按相同的步骤给SER数据然后放到移位寄存器,这时移位寄存器的数据溢出,溢出的数据就会通过QH'到下一个移位寄存器
理论来说,如果将点阵屏的16个引脚全都接在单片机的IO口上是可行的,但是单片机的IO口是弱上拉模式,在驱动高频电路时,它的高电平输出电流很低,这会导致点阵屏很暗。如果在单片机IO口后接一个三极管开关再驱动点阵屏就可以实现,如下电路图,如果给I/O口低电平,三极管就会导通,电压VCC就会直接去驱动引脚,而不是I/O电压驱动引脚,这里I/O相当于一个控制信号。
4,应用1:显示静态图像
- 先介绍几个概念
•可位寻址/不可位寻址:在单片机系统中,操作任意寄存器或者某一位的数据时,必须给出其物理地址,又因为一个寄存器里有8位,所以位的数量是寄存器数量的8倍,单片机无法对所有位进行编码,故每8个寄存器中,只有一个是可以位寻址的。对不可位寻址的寄存器,若要只操作其中一位而不影响其它位时,可用“&=”、“|=”、“^=”的方法进行位操作
•sfr(special function register):特殊功能寄存器声明。例:sfr P0 = 0x80;声明P0口寄存器,物理地址为0x80
•sbit(special bit):特殊位声明。例:sbit P0_1 = 0x81; 或 sbit P0_1 = P0^1;声明P0寄存器的第1位
sfr ,sbit用在头文件的声明中,比如STC89C52的头文件<REGX52.H>
有了以上概念,我们在操作74HC595时为了方便,就可以把引脚P3_5重新定义为以RCLK为名的引脚,如下方
#include <REGX52.H>
sbit RCK=P3^5;//P3_5是P3系列引脚的第五位,重新定义时直接用P3^5
//就可以表示为P3_5的引脚地址0xB5,由于RCLK已经命名过了
//所以命名为RCK
sbit SRCLK=P3^6;
sbit SER=P3^4;
#include <REGX52.H>
#include <Delay.h>
sbit RCK=P3^5;//P3_5是P3系列引脚的第五位,重新定义时直接用P3^5
//就可以表示为P3_5的引脚地址0xB5,由于RCLK已经命名过了
//所以命名为RCK
sbit SRCLK=P3^6;
sbit SER=P3^4;
int i=0;
void _74HC595_WriteByte(unsigned char Byte){//此函数用于74HC595传输数据
for(i=0;i<8;i++){//移位8次
SER=Byte&(0x80>>i);//因为第一个数据传输的是最高位数据(先传最后一个寄存器数据)
//所以用逻辑&来提取最高位数据
//SER只能一位一位传输,这样一次传输8位可以的原因是:如果要传输的数据
//为0,则SER送到寄存器里的为0,如果要传输的数据不为0,则SER
//送到寄存器里的为1
SRCLK=1;//将最高位下移一位
SRCLK=0;//清零
}
SRCLK=1;//将八位数据送到缓存寄存器
RCK=0;//清零
}
void MatrixLED_ShowColumn(unsigned char Column,Data)
{
_74HC595_WriteByte(Data);
P0=~(0x80>>Column);
Delay(1);
P0=0xFF;//清零显示,防止段选位选出现重影
}
void main()
{
SRCLK=0;//初始化SCK
SRCLK=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);
}
}
5,应用2:显示动画
#include <REGX52.H>
#include "Delay.h"
#include "MatrixLED.h"
//用资料中的软件快速提取数据
//动画数据,动画数据如果很多的话,RAM内存可能会不够,而Flash的内存比RAM大得多,
//所以在此变量前加一个code,表示把它存在Flash中
//注意,放在Flash中以后此数组数据不可以再更改
unsigned char code Animation[]={
0x3C,0x42,0xA9,0x85,0x85,0xA9,0x42,0x3C,
0x3C,0x42,0xA1,0x85,0x85,0xA1,0x42,0x3C,
0x3C,0x42,0xA5,0x89,0x89,0xA5,0x42,0x3C,
};
void main()
{
unsigned char i,Offset=0,Count=0;
SRCLK=0;//初始化SCK
SRCLK=0;//初始化
while(1)
{
for(i=0;i<8;i++) //循环8次,显示8列数据
{
MatrixLED_ShowColumn(i,Animation[i+Offset]);
}
Count++; //计次延时
if(Count>15)
{
Count=0;
Offset+=8; //偏移+8,切换下一帧画面
if(Offset>16)
{
Offset=0;
}
}
}
}
十,DS1302实时时钟
普通单片机的定时器计时精度不高,而且会占用CPU的运行时间,除此之外,单片机时钟在断电后不会继续运行时钟会清零。
而DS1302时钟芯片带有备用电池,断电时会自动切换到备用电池,让他在单片机不上电时继续计时,而且在单片机有电时会对备用电池充电。DS1302是由美国DALLAS公司推出的具有涓细电流充电能力的低功耗实时时钟芯片。它可以对年、月、日、周、时、分、秒进行计时,且具有闰年补偿等多种功能
RTC:实时时钟,是一种集成电路,通常称为时钟芯片,DS1302,DS3231就是其中一种。
学一个芯片,最重要的就是会看手册
1,引脚定义和应用电路
DIP表示直插封装,SO表示贴片封装
共有8个引脚,第一部分表示电源VCC1.VCC2,GND,VCC2是主电源,VCC1是备用电池。X1,X2接晶振,晶振频率为32.768Hz,它通过内部电路处理会输出一个1Hz的标准频率,为实时时钟系统提供一个稳定的脉冲。然后单片机通过CE,IO,SCLK三个引脚将时钟芯片的信息读出来,或者把时间写进去。其数据输入输出方式和74HC595类似
引脚名 | 作用 | 引脚名 | 作用 |
VCC2 | 主电源 | CE | 芯片使能 |
VCC1 | 备用电池 | IO | 数据输入/输出 |
GND | 电源地 | SCLK | 串行时钟 |
X1、X2 | 32.768KHz晶振 |
2,内部电路结构
内部时间均存在寄存器RAM中。CE引脚作为芯片使能,相当于一个中介开关,输入移位寄存器的数据到命令控制逻辑后,首先要经过一个CE开关才能到实时时钟中,CE为高电平开关闭合,低电平断开,IO和SCLK就控制输入移位寄存器,IO相当于74HC595的SER一样用于输入数据
3,寄存器
- 与时钟有关的寄存器
共有81h~91h几个地址,每个地址有一个寄存器,一个寄存器有8位,其中如下图第一个地址中的寄存器表示秒。第二个地址中的寄存器表示分,以此类推。WP是write protect,是写保护,当它置为1时 ,写入的操作无效,置为0就可以写入,最后一个寄存器是用来存储涓流充电的
- 命令字
命令字用来控制是读还是写秒,是读秒还是读时等,即对上表选择的控制。
一个寄存器,总共八个字节,最高位固定为1不用管。如果要操作RAM,第6位就要给1,如果给0就是操作CK,即时钟。第5位到第1位就是我们要操作的地址。第0位是读写模式,如果给0就是写,给1就是读。这8位加起来就可以具体到时钟的地址。比如说要写入秒,那就给A4 A3 A2 A1 A0都为0,RD为0,那么这个寄存器就是1000 0000,换为16进制为0x80这就正好对应了秒的地址80h,如果要读秒,那就把RD置为1,则为1000 0001,换为16进制为0x81,正好对应了81h
4,时序图
根据时序图,时钟芯片工作时,先有单片机发一个命令字,再决定是读出还是写入,读出写入到哪里。完成后SCLK,CE均置零。
从CE线可以看出,要读写数据时CE要置为1,写完之后清零。SCLK就是给一个固定的时钟,IO就给数据,当SCLK为高电平,IO上的数据将会被写入,在时钟的下降沿,DS1302就会把他的数据输出,也就是说在时钟上升沿我用单片机给始终写入数据,在时钟下降沿,时钟芯片向单片机写入数据。(当RW给1时即为读数据,这时IO口由时钟芯片掌握,SCLK每输出一个下降沿数据读出一个。当RW给0时即为写数据,这时IO口由单片机掌握,SCLK每输出一个上升沿数据写入一个。)
5,代码实现过程
首先编写初始化函数,将CE和SCLK均置零
再编写写入函数(函数无返回值,参数为命令字和要输入数据共两个字节),参照下方写入的时序图,首先使CE=1。根据类似的74HC595的串行数据输入方式,这里的IO就是SER,给IO一个数据,然后置SCLK为1来存入寄存器。这里需要输入两个字节的数据,第一个字节的数据是命令字,取出命令字(主函数调用写入函数时给定)的第一个数据,置SCLK为1,再置为0,如此循环8次,完成命令字的输入,接下来数据的输入也类似,循环8次完成数据的写入。
再编写读取函数读取DS1302的数据(函数返回值为DS1302的数据,参数为命令字),命令字的输入过程同写入函数,有一个不同点,看下方读取时的时序图,可以看到读取的一个过程SCLK有15个脉冲,写入时SCLK有16个脉冲。这是因为读取时命令字的写入是来一个上升沿写入一次,而读取数据时IO口控制权交给DS1302,并且来一个下降沿读取一次,这就使其中的一个脉冲的上升沿和下降沿均有数据输入和读取,为了解决这个问题,在写入命令字时,在for循环内先令SCLK=1;在令其为0,这样在命令字写入结束时就正好在上升沿的位置,而不会经过下降沿。在读取数据的下降沿中,先令SCLK=1;再令其为0即可。
//DS1302.c
#include <REGX52.H>
sbit DS1302_SCLK=P3^6;
sbit DS1302_IO=P3^4;
sbit DS1302_CE=P3^5;
void DS1302_Init()
{
DS1302_CE=0;
DS1302_SCLK=0;
}
void DS1302_WriteByte(unsigned char Command,Data)
{
DS1302_CE=1;
int i=0;
//第一个for循环为命令字写入
for(i=0;i<=7;i++){
DS1302_IO=Command&(0x01<<i);//取出命令字的第0位
DS1302_SCLK=1;
DS1302_SCLK=0;//立马置为0要考虑时间问题,
//可以去数据手册看一下SCK这个传输过程所需时间为多少
//如果单片机处理速度快于SCK,则需要加延时函数
}
//第二个循环为数据写入
for(i=0;i<=7;i++){
DS1302_IO=Data&(0x01<<i);//取出命令字的第0位
DS1302_SCLK=1;
DS1302_SCLK=0;
}
DS1302_CE=0;//CE清零
}
unsigned char DS1302_ReadByte(unsigned char Command)
{
DS1302_CE=1;
int i=0;
unsigned char Data=0x00;
for(i=0;i<=7;i++){
DS1302_IO=Command&(0x01<<i);//取出命令字的第0位
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;
return Data;
}
- 常见问题:
1,如果在主函数执行一个简单例子,如下代码显示是可以正常显示的,但如果把Num和LCD_ShowNum放到while中,LCD屏就会出现数字显示错误并且不清的问题 。改进方法是对 unsigned char DS1302_ReadByte(unsigned char Command)函数做如下处理
unsigned char DS1302_ReadByte(unsigned char Command)
{
DS1302_CE=1;
int i=0;
unsigned char Data=0x00;
for(i=0;i<=7;i++){
DS1302_IO=Command&(0x01<<i);//取出命令字的第0位
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;}
#include <REGX52.H>
#include "Delay.h"
#include "DS1302.h"
#include "LCD1602.h"
unsigned char Num=0x00;
void main()
{
DS1302_Init();
LCD_Init();
LCD_ShowString(1,1,"RTC");
DS1302_WriteByte(0x80,0x30);
Num=DS1302_ReadByte(0x81);
LCD_ShowNum(1,1,Num,3);
while(1)
{
//Num=DS1302_ReadByte(0x81);
//LCD_ShowNum(1,1,Num,3);
}
}
2,如果读出时间为一个大于59并且不动的数,则芯片可能处于 写保护状态,在DS1302_WriteByte之前加上DS1302_Write(0x8E,0x00)即可解除芯片写保护
3,解决完第1个问题后,LCD显示正常,但在计数时,9会突然变到16。这是因为时钟芯片内的寄存器不是按照正常二进制进行存储的,而是以BCD码进行存储的(BCD码是用4位二进制码来表示1位十进制数,如0001 0011表示13,1000 0101表示85,1010表示10不合法;在十六进制中的体现为0x13表示13,0x85表示85,0x1A不合法)。在BCD编码中0000 1001表示9时,下一位0001 0000表示10,而0001 0000在二进制中表示16,所以会产生突变。可以看到BCD码在16进制中的显示是可以代表十进制数的,因为BCD码的0001 0011就可以转换为0x13,13就是要表示的数。所以第一种解决办法是以16进制显示BCD编码的数。第二种方法可以将BCD转换为十进制,它的公式如下:所以就可以写成LCD_ShowNum(1,1,Num/16*10+Num%16,3);
上述的BCD编码形式在我们之前看的芯片寄存器上也有体现:以秒为例,前4位为Seconds,后三位为10Seconds,也就是说后三位表示十位数字,CH表示时钟静止,如果置1则秒停止计数。
6,应用1:时钟显示
年月日数据很多,都在主函数里调用很麻烦。接下来对DS1302.c进行改进
//DS1302.c
#include <REGX52.H>
sbit DS1302_SCLK=P3^6;
sbit DS1302_IO=P3^4;
sbit DS1302_CE=P3^5;
#define DS1302_SECOND 0X80;
#define DS1302_MINUTE 0X82;
#define DS1302_HOUR 0X84;
#define DS1302_DATE 0X86;
#define DS1302_MONTH 0X88;
#define DS1302_DAY 0X8A;
#define DS1302_YEAR 0X8C;
#define DS1302_WP 0X8E;
unsigned char DS1302_Time[]={24,6,21,16,24,55,6}//定义一个数组存放时间
//24年6月21日16时24分55秒,星期六
void DS1302_Init()
{
DS1302_CE=0;
DS1302_SCLK=0;
}
void DS1302_WriteByte(unsigned char Command,Data)
{
DS1302_CE=1;
int i=0;
//第一个for循环为命令字写入
for(i=0;i<=7;i++){
DS1302_IO=Command&(0x01<<i);//取出命令字的第0位
DS1302_SCLK=1;
DS1302_SCLK=0;//立马置为0要考虑时间问题,
//可以去数据手册看一下SCK这个传输过程所需时间为多少
//如果单片机处理速度快于SCK,则需要加延时函数
}
//第二个循环为数据写入
for(i=0;i<=7;i++){
DS1302_IO=Data&(0x01<<i);//取出命令字的第0位
DS1302_SCLK=1;
DS1302_SCLK=0;
}
DS1302_CE=0;//CE清零
}
unsigned char DS1302_ReadByte(unsigned char Command)
{
DS1302_CE=1;
int i=0;
unsigned char Data=0x00;
for(i=0;i<=7;i++){
DS1302_IO=Command&(0x01<<i);//取出命令字的第0位
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;
return Data;
}
void DS1302_SetTime()//调用此函数,将上述定义的时间数组设置到时钟芯片中去
{
DS1302_WriteByte(0x8E,0x00);//关闭写保护
DS1302_WriteByte(DS1302_YEAR,DS1302_Time[0]/10*16+DS1302_Time[0]%10);
//将其转换为BCD码放入
DS1302_WriteByte(DS1302_YEAR,DS1302_Time[0]/10*16+DS1302_Time[0]%10);
DS1302_WriteByte(DS1302_MONTH,DS1302_Time[1]/10*16+DS1302_Time[0]%10);
DS1302_WriteByte(DS1302_DATE,DS1302_Time[2]/10*16+DS1302_Time[0]%10);
DS1302_WriteByte(DS1302_HOUR,DS1302_Time[3]/10*16+DS1302_Time[0]%10);
DS1302_WriteByte(DS1302_MINUTE,DS1302_Time[4]/10*16+DS1302_Time[0]%10);
DS1302_WriteByte(DS1302_SECOND,DS1302_Time[5]/10*16+DS1302_Time[0]%10);
DS1302_WriteByte(DS1302_DAY,DS1302_Time[6]/10*16+DS1302_Time[0]%10);
}
void DS1302_ReadTime()//调取此函数,将时钟芯片内的数据读取回来存到时间数组中
{
unsigned char Temp;
Temp=DS1302_Time[0]=DS1302_ReadByte(0x8D);
DS1302_Time[0]=Temp/16*10+Temp&16;
Temp=DS1302_Time[1]=DS1302_ReadByte(0x89);
DS1302_Time[1]=Temp/16*10+Temp&16;
Temp=DS1302_Time[2]=DS1302_ReadByte(0x87);
DS1302_Time[2]=Temp/16*10+Temp&16;
Temp=DS1302_Time[3]=DS1302_ReadByte(0x85);
DS1302_Time[3]=Temp/16*10+Temp&16;
Temp=DS1302_Time[4]=DS1302_ReadByte(0x83);
DS1302_Time[4]=Temp/16*10+Temp&16;
Temp=DS1302_Time[5]=DS1302_ReadByte(0x81);
DS1302_Time[5]=Temp/16*10+Temp&16;
Temp=DS1302_Time[6]=DS1302_ReadByte(0x8B);
DS1302_Time[6]=Temp/16*10+Temp&16;
}
DS1302.h文件
#ifndef __DS1302_H__
#define __DS1302_H__
void DS1302_Init();
void DS1302_WriteByte(unsigned char Command,Data);
unsigned char DS1302_ReadByte(unsigned char Command);
void DS1302_ReadTime();
void DS1302_Settime();
#endif
main.c文件
#include <REGX52.H>
#include "LCD1602.h"
#include "DS1302.h"
extern unsigned char DS1302_Time[];
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);//显示秒
}
}
7,应用2:可调时钟
十一,蜂鸣器
1,蜂鸣器介绍
2,驱动电路
(1)三极管驱动
三极管驱动电路中的三极管作为开关
左边电路图中R1左端接单片机IO口,如果IO口给高电压则导通,蜂鸣器响,如果给低电平,三极管电路截至。所以单片机在控制蜂鸣器中起信号控制作用。R1是限流电阻,保证三极管能导通就以
(2)集成电路驱动
由于单片机不能直接驱动元器件(输出高电平不稳定),所以就采用一个芯片来驱动,有点类似于之前说过的双向数据缓冲器74HC245
ULN2003驱动芯片
达林顿管是一种三极管复合的晶体管,可以增强放大能力
如下,ULN2003由7对非门组成,每个NPN达林管组成每个非门,当左端接单片机后,单片机给1,则右边输出为0,以此来驱动元器件。(注意:如果左端给0,右端输出的1不具有驱动能力,实际上相当于右端断路,处于高阻态状态。)
根据上述的电路原理图,给P15一个高电平就可以控制蜂鸣器了
十二,AT24C02(I2C总线)
1,存储器内部简化模型
存储器内部实际上是电路的网状结构,如下图,横向线称为地址总线(用来选中所需的线),竖向线称为数据总线。存储原理:比如在地址总线选中了第一行,可以给他加一个高电平1,其他地址总线都不接。然后把左上角第一个节点两条线连上,然后从左到右连接第二个节点,第三个节点,其他节点不连接(各线的相交处是不连接的,在要存储时才会连接)。前三个节点连上以后,数据总线的前三根线就会变为1。所以在第一根线也就是第一个地址下就存了1110 0000,以此来存储数据。实际上两条线交错处的节点不是简单的相连,而是通过二极管相连,防止每个节点之间的互相干扰。在最初的Mask ROM中,如果这个节点什么都不接如左图,即相当于无数据,当需要将交错线接到一起时,就连接一个二极管,是固定的电路。而PROM中对头连接两个二极管,只需要击穿其中一个二极管就可以通电,所以PROM可以通过编程给二极管两端击穿电压,达到编程存储的效果,由于二极管击穿后就永久损坏了,所以PROM只能编程一次。
由于地址总线一次只能选中一行,所以常在地址总线之前加一个138译码器
2,AT24C02介绍
(1)简介
(2)引脚及应用电路
AT24C02共有8个引脚,WP是写保护,高电平时处于写保护状态。
根据电路图,使用时给AT24C02接上电源,A0~A2接到VCC或者GND,SCL和SDA接外界电路,下面介绍I2C时会详细介绍
3,I2C总线介绍
(1)简介
(2)I2C电路规范
弱上拉模式 :前面介绍过,电路图如下
开漏输出模式:开漏输出模式没有上拉电阻,当开关闭合时,I/O输出为低电平0,开关断开时(信号1),引脚处于浮空状态,此时相当于电路断路,一点小的干扰就会使引脚电压变化,电压不稳定
(3)I2C时序结构
起始条件(即要开始通信之前给各设备一个要开始的信号):SCL高电平期间,SDA从高电平切换到低电平。把SCL最后拉低是为了和下一个时序衔接起来,保证下一个时序不用从高再到低。
终止条件:
发送一个字节:发送一个字节:SCL低电平期间,主机将数据位依次放到SDA线上(高位在前),然后拉高SCL,从机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可发送一个字节。如下图,SDA画叉的地方是数据变化的时候,数据传输时每次传输一位,画叉的地方只是为了便于理解画了两条线,实际上只有一条,当要传输的第一位数据为1时,则线从低电平升高到高电平或者从高电平继续保持高电平
所有线都是由主机控制,这也是由主机发送的,从机(AT24C02)会读取SCL,SDA数据,会自动读取。
接受一个字节:SCL低电平期间,从机将数据位依次放到SDA线上(高位在前),然后拉高SCL,主机将在SCL高电平期间读取数据位,所以SCL高电平期间SDA不允许有数据变化,依次循环上述过程8次,即可接收一个字节(主机在接收之前,需要释放SDA,释放后主机将控制权交给从机),此时从机控制主线,主机接收数据
发送应答:在接收完一个字节之后,主机在下一个时钟发送一位数据,数据0表示应答,数据1表示非应答
接收应答:在发送完一个字节之后,主机在下一个时钟接收一位数据,判断从机是否应答,数据0表示应答,数据1表示非应答(主机在接收之前,需要释放SDA)
可以作为数据发送的第九位
(4)I2C数据帧
发送一帧数据:第一个是发送开始标志S,在之后第一节数据一定要发送从机地址+读写位(共八位,前七位为地址位,其中前四位为固定端,AT24C02固定为1010,其他芯片不一样,后三位可配置,对应AT24C03的A0,A1,A2引脚),然后没发一个字节都跟一个RA接收应答,之后再发送数据字节,发送完最后一节后,应答完跟一个终止。
接收一帧数据 :
先发送再接收数据帧 :
(5)AT24C02数据帧
但是AT24C02实际应用起来并不会像上述写入一帧数据一样byte1,byte2,byte3一直发 ,而是用如下数据帧写入,它的数据帧和上述的I2C数据帧类似,但有所不同。
字节写: 继承了发送一帧数据。字节写只写入一个字节,页写和I2C写入一帧数据一样可以写入多节数据,这里不做介绍。
随机读: 继承了复合格式
此外,还有多种数据帧形式,详细可以见手册。