程序使用51单片机实现DS1302时钟和蜂鸣器控制程序


目录

一、DS1302时钟

1、DS1302时钟的介绍

2、DS1302时钟寄存器

3、DS1302时序图

4、BCD码

5、写一个时钟

6、写一个可调时钟

二、蜂鸣器

1、蜂鸣器的介绍​编辑

2、三极管放大驱动蜂鸣器

2.1NPN三极管工作原理(基极电流和发射电流均流向集电区)

2.2PNP三极管工作原理(发射电流分别流向基极和集电极)

3、集成电路驱动蜂鸣器

4、按下按键后蜂鸣器播发声

5、蜂鸣器利用定时器循环播放小星星


一、DS1302时钟

1、DS1302时钟的介绍

DS1302具有涓细电流充电能力的低功耗实时时钟芯片。掉电后能够切换为备用电池。

RTC:实时时钟,是一种集成电路,通常称为时钟芯片。

翻一下51单片机的配套原理图,可以发现DS1302模块的VCC1口引脚并没有连接电源,所以单片机掉电后,并没有备用电源为DS1302时钟供电。

2、DS1302时钟寄存器

第一张小图已经详细列寄存器对应的读写信息了,直接用就行。原理是图二的地址/命令字。

3、DS1302时序图

写入时序步骤

读出时序步骤

步骤1:将CE置为1并开启时钟;

步骤2:将命令字的最低位设置到IO口上;

步骤3:时钟给个上升沿,此时命令字的最低位就会被写入单片机;

步骤4:将时钟重置为0;

步骤5:重复2-4步骤,每次压入一个比特数据,直到命令字和写入字节全部写入。

步骤6:将CE置为0并关闭时钟。

步骤1:将CE置为1并开启时钟;

步骤2:将命令字的最低位设置到IO口上;

步骤3:时钟给个上升沿,此时命令字的最低位就会被写入单片机;

步骤4:将时钟重置为0;

步骤5:重复2-4步骤,每次压入一个比特数据,直到命令字全部写入。命令字写入完毕后的第一个下降沿开始,每个下降沿读出数据。

步骤6:将CE置为0并关闭时钟。

在写读/写函数时,要注意时钟上升沿向时钟写入数据,下降沿时钟数据读出至IO口,跳变有效。当时钟上升与下降时,均会发生数据的写/读,读写情况可以参照上图。总结:读模块要利用上升沿向时钟写入数据,再写下降沿,看图可知,这个下降沿并不影响IO口,处理得就很微妙。写模块也同理。

4、BCD码

调用写/读函数可以发现数字从9直接跳到了16。因为DS1302内部并不是以二进制存储数据,而是使用BCD码。

BCD码:用四个二进制数表示一个十进制数。所以9的BCD码是0000 1001,10的BCD码是0001 0000。但是解码读取到的并不是BCD码,而是16进制,所以会从9突变到16的现象。

BCD码转10进制:DEC=BCD/16*10+BCD%16;(两位BCD)

十进制转BCD码:BCD=DEC/10*16+DEC%10;(两位BCD)

5、写一个时钟

1、DS1302.h

#ifndef __DS1302_H__
#define __DS1302_H__
#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
extern unsigned char DS1302Time[];
unsigned char DS1302Name[];//外部声明变量必须加extern,数组函数可加可不加
void DS1302SetTime();//设置初始时间
void DS1302ReadTime();//不断更新时间
void DS1302_Init();//初始化
void DS1302WriteByte(unsigned char Command,unsigned char Data);//单字节写入时钟
unsigned char DS1302ReadByte(unsigned char Command);//单字节从时钟读出
#endif

2、DS1302.c

#include "DS1302.h"
#include "delay.h"
unsigned char DS1302Time[]={22,12,15,18,27,55,4};
unsigned char DS1302Name[]={
DS1302_YEAR,DS1302_MONTH,DS1302_DATE,DS1302_HOUR,DS1302_MINUTE,DS1302_SECOND,DS1302_DAY
};
//设置初始时间
void DS1302SetTime()
{
	unsigned char i=0;
	DS1302WriteByte(DS1302_WP,0X00);//关闭写保护位
	for(i=0;i<7;++i)
	{
		DS1302WriteByte(DS1302Name[i],DS1302Time[i]/10*16+DS1302Time[i]%10);
	}
	
	DS1302WriteByte(DS1302_WP,0X80);//开启写保护位
}
//不断更新时间
void DS1302ReadTime()
{
	unsigned char Tmp,i=0;
	for(i=0;i<7;++i)
	{
		Tmp=DS1302ReadByte(DS1302Name[i]);//读出来的是BCD码
		DS1302Time[i]=Tmp/16*10+Tmp%16;//更新数组
	}
}
void DS1302_Init()
{
	DS1302_CE=0;//因为P3^5引脚默认是高电平,先初始化成低电平
	DS1302_SCLK=0;//同上
	
}
//单字节写
void DS1302WriteByte(unsigned char Command,unsigned char Data)
{
	unsigned char i=0;
	DS1302_CE=1;
	//向时钟写入命令字
	for(;i<8;++i)
	{
		DS1302_IO=(Command>>i)&1;//取出命令字的最低位
		DS1302_SCLK=1;//此处完成时钟从低到高跳变,向时钟写入IO口数据
		//时钟置零,注意时钟从高到低是需要时间的,必要时可以加个延时(不同状态下的延时时间见产品手册)
		DS1302_SCLK=0;//同时此处完成高电平到低电平的跳变,IO口重新被赋值为刚才写入的数据
	}
	//上方for循环结束后时钟是低电平
	//向时钟继续写入Data
	for(i=0;i<8;++i)
	{
		DS1302_IO=(Data>>i)&1;//取出写入字节的最低位
		DS1302_SCLK=1;//低->高跳变,向时钟写入IO口数据
		DS1302_SCLK=0;//跳变
	}
	DS1302_CE=0;//关闭CE
}
//单字节读
unsigned char DS1302ReadByte(unsigned char Command)
{
	unsigned char i=0,Data=0;
	Command|=1;//读的最低位是1,传进来的是写的地址,需要与上1
	DS1302_CE=1;
	//IO口读入命令字
	for(;i<8;++i)
	{
		DS1302_IO=(Command>>i)&1;
		DS1302_SCLK=0;//为了出循环时钟是1,时钟先给0
		DS1302_SCLK=1;//低->高跳变,向时钟写入IO口数据。循环中先给0再给1,保证出循环后时钟是1,时钟是1,下一个0马上可以读出数据
	}
	//上方for循环结束后时钟是高电平
	//读出时钟里的数据
	for(i=0;i<8;++i)
	{
		DS1302_SCLK=1;
		DS1302_SCLK=0;//从高->低跳变,数据从时钟读出至IO口
		if(DS1302_IO==1)
		{
			Data|=(1<<i);
		}
	}
	DS1302_CE=0;
	DS1302_IO=0;//将IO口置0
	return Data;
}

3、main.c

#include "LCD1602.h"
#include "DS1302.h"
#include "delay.h"
void main()
{	
	LCD_Init();
	DS1302_Init();
	
	DS1302SetTime();
	while(1)
	{
		DS1302ReadTime();
		LCD_ShowNum(1,1,DS1302Time[0],2);
		LCD_ShowString(1,3,"-");
		LCD_ShowNum(1,4,DS1302Time[1],2);
		LCD_ShowString(1,6,"-");
		LCD_ShowNum(1,7,DS1302Time[2],2);
		LCD_ShowNum(2,1,DS1302Time[3],2);
		LCD_ShowString(2,3,":");
		LCD_ShowNum(2,4,DS1302Time[4],2);
		LCD_ShowString(2,6,":");
		LCD_ShowNum(2,7,DS1302Time[5],2);
	}
}

6、写一个可调时钟

1、DS1302.h

#ifndef __DS1302_H__
#define __DS1302_H__
#include <REGX52.H>
#include "Timer0.h"
#include "key.h" 
#include "LCD1602.h"
#include "delay.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
extern unsigned char KeyNum,MODE,TimeSetSelect,TimeSetFlashFlag;
extern unsigned char DS1302Time[];
unsigned char DS1302Name[];//外部声明变量必须加extern,数组函数可加可不加
void DS1302SetTime();//设置初始时间
void DS1302ReadTime();//不断更新时间
void DS1302_Init();//初始化
void DS1302WriteByte(unsigned char Command,unsigned char Data);//单字节写入时钟
unsigned char DS1302ReadByte(unsigned char Command);//单字节从时钟读出
void TimeShow();//模式0,显示时间
void TimeSet();//模式1,修改时间
void LegalDate();//合法日期判断
#endif

2、DS1302.c 

#include "DS1302.h"
unsigned char KeyNum,MODE,TimeSetSelect,TimeSetFlashFlag;
unsigned char DS1302Time[]={22,12,15,18,27,55,4};
unsigned char DS1302Name[]={
DS1302_YEAR,DS1302_MONTH,DS1302_DATE,DS1302_HOUR,DS1302_MINUTE,DS1302_SECOND,DS1302_DAY
};

//设置初始时间
void DS1302SetTime()
{
	unsigned char i=0;
	DS1302WriteByte(DS1302_WP,0X00);//关闭写保护位
	for(i=0;i<7;++i)
	{
		DS1302WriteByte(DS1302Name[i],DS1302Time[i]/10*16+DS1302Time[i]%10);
	}
	
	DS1302WriteByte(DS1302_WP,0X80);//开启写保护位
}
//不断更新时间
void DS1302ReadTime()
{
	unsigned char Tmp,i=0;
	for(i=0;i<7;++i)
	{
		Tmp=DS1302ReadByte(DS1302Name[i]);//读出来的是BCD码
		DS1302Time[i]=Tmp/16*10+Tmp%16;//更新数组
	}
}
void DS1302_Init()
{
	DS1302_CE=0;//因为P3^5引脚默认是高电平,先初始化成低电平
	DS1302_SCLK=0;//同上
	
}
//单字节写
void DS1302WriteByte(unsigned char Command,unsigned char Data)
{
	unsigned char i=0;
	DS1302_CE=1;
	//向时钟写入命令字
	for(;i<8;++i)
	{
		DS1302_IO=(Command>>i)&1;//取出命令字的最低位
		DS1302_SCLK=1;//此处完成时钟从低到高跳变,向时钟写入IO口数据
		//时钟置零,注意时钟从高到低是需要时间的,必要时可以加个延时(不同状态下的延时时间见产品手册)
		DS1302_SCLK=0;//同时此处完成高电平到低电平的跳变,IO口重新被赋值为刚才写入的数据
	}
	//上方for循环结束后时钟是低电平
	//向时钟继续写入Data
	for(i=0;i<8;++i)
	{
		DS1302_IO=(Data>>i)&1;//取出写入字节的最低位
		DS1302_SCLK=1;//低->高跳变,向时钟写入IO口数据
		DS1302_SCLK=0;//跳变
	}
	DS1302_CE=0;//关闭CE
}
//单字节读
unsigned char DS1302ReadByte(unsigned char Command)
{
	unsigned char i=0,Data=0;
	Command|=1;//读的最低位是1,传进来的是写的地址,需要与上1
	DS1302_CE=1;
	//IO口读入命令字
	for(;i<8;++i)
	{
		DS1302_IO=(Command>>i)&1;
		DS1302_SCLK=0;//为了出循环时钟是1,时钟先给0
		DS1302_SCLK=1;//低->高跳变,向时钟写入IO口数据。循环中先给0再给1,保证出循环后时钟是1,时钟是1,下一个0马上可以读出数据
	}
	//上方for循环结束后时钟是高电平
	//读出时钟里的数据
	for(i=0;i<8;++i)
	{
		DS1302_SCLK=1;
		DS1302_SCLK=0;//从高->低跳变,数据从时钟读出至IO口
		if(DS1302_IO==1)
		{
			Data|=(1<<i);
		}
	}
	DS1302_CE=0;
	DS1302_IO=0;//将IO口置0
	return Data;
}

void TimeShow()
{
	DS1302ReadTime();
	LCD_ShowNum(1,1,DS1302Time[0],2);
	LCD_ShowString(1,3,"-");
	LCD_ShowNum(1,4,DS1302Time[1],2);
	LCD_ShowString(1,6,"-");
	LCD_ShowNum(1,7,DS1302Time[2],2);
	LCD_ShowNum(2,1,DS1302Time[3],2);
	LCD_ShowString(2,3,":");
	LCD_ShowNum(2,4,DS1302Time[4],2);
	LCD_ShowString(2,6,":");
	LCD_ShowNum(2,7,DS1302Time[5],2);
}
void TimeSet()
{
	if(KeyNum==2)
	{
		++TimeSetSelect;
		TimeSetSelect%=6;//如果TimeSetSelect大于等于5,对TimeSetSelect清零
	}
	if(KeyNum==3)
	{
		++DS1302Time[TimeSetSelect];
		if(DS1302Time[0]>99){DS1302Time[0]=0;}//年越界判断
		if(DS1302Time[1]>12){DS1302Time[1]=1;}//月越界判断
		if( DS1302Time[1]==1 || DS1302Time[1]==3 || DS1302Time[1]==5 || DS1302Time[1]==7 || 
			DS1302Time[1]==8 || DS1302Time[1]==10 || DS1302Time[1]==12)//日越界判断
		{
			if(DS1302Time[2]>31){DS1302Time[2]=1;}//大月
		}
		else if(DS1302Time[1]==4 || DS1302Time[1]==6 || DS1302Time[1]==9 || DS1302Time[1]==11)
		{
			if(DS1302Time[2]>30){DS1302Time[2]=1;}//小月
		}
		else if(DS1302Time[1]==2)
		{
			if(DS1302Time[0]%4==0)
			{
				if(DS1302Time[2]>29){DS1302Time[2]=1;}//闰年2月
			}
			else
			{
				if(DS1302Time[2]>28){DS1302Time[2]=1;}//平年2月
			}
		}
		if(DS1302Time[3]>23){DS1302Time[3]=0;}//时越界判断
		if(DS1302Time[4]>59){DS1302Time[4]=0;}//分越界判断
		if(DS1302Time[5]>59){DS1302Time[5]=0;}//秒越界判断
	}
	if(KeyNum==4)
	{
		--DS1302Time[TimeSetSelect];
		if(DS1302Time[0]<0){DS1302Time[0]=99;}//年越界判断
		if(DS1302Time[1]<1){DS1302Time[1]=12;}//月越界判断
		if( DS1302Time[1]==1 || DS1302Time[1]==3 || DS1302Time[1]==5 || DS1302Time[1]==7 || 
			DS1302Time[1]==8 || DS1302Time[1]==10 || DS1302Time[1]==12)//日越界判断
		{
			if(DS1302Time[2]<1){DS1302Time[2]=31;}//大月
			if(DS1302Time[2]>31){DS1302Time[2]=1;}
		}
		else if(DS1302Time[1]==4 || DS1302Time[1]==6 || DS1302Time[1]==9 || DS1302Time[1]==11)
		{
			if(DS1302Time[2]<1){DS1302Time[2]=30;}//小月
			if(DS1302Time[2]>30){DS1302Time[2]=1;}
		}
		else if(DS1302Time[1]==2)
		{
			if(DS1302Time[0]%4==0)
			{
				if(DS1302Time[2]<1){DS1302Time[2]=29;}//闰年2月
				if(DS1302Time[2]>29){DS1302Time[2]=1;}
			}
			else
			{
				if(DS1302Time[2]<1){DS1302Time[2]=28;}//平年2月
				if(DS1302Time[2]>28){DS1302Time[2]=1;}
			}
		}
		if(DS1302Time[3]<0){DS1302Time[3]=23;}//时越界判断
		if(DS1302Time[4]<0){DS1302Time[4]=59;}//分越界判断
		if(DS1302Time[5]<0){DS1302Time[5]=59;}//秒越界判断
	}
	//利用中断更新显示
	if(TimeSetSelect==0&&TimeSetFlashFlag==1){LCD_ShowString(1,1,"  ");}
	else{LCD_ShowNum(1,1,DS1302Time[0],2);}
	LCD_ShowString(1,3,"-");
	
	if(TimeSetSelect==1&&TimeSetFlashFlag==1){LCD_ShowString(1,4,"  ");}
	else{LCD_ShowNum(1,4,DS1302Time[1],2);}
	LCD_ShowString(1,6,"-");
	
	if(TimeSetSelect==2&&TimeSetFlashFlag==1){LCD_ShowString(1,7,"  ");}
	else{LCD_ShowNum(1,7,DS1302Time[2],2);}
	
	if(TimeSetSelect==3&&TimeSetFlashFlag==1){LCD_ShowString(2,1,"  ");}
	else{LCD_ShowNum(2,1,DS1302Time[3],2);}
	LCD_ShowString(2,3,":");
	
	if(TimeSetSelect==4&&TimeSetFlashFlag==1){LCD_ShowString(2,4,"  ");}
	else{LCD_ShowNum(2,4,DS1302Time[4],2);}
	LCD_ShowString(2,6,":");
	
	if(TimeSetSelect==5&&TimeSetFlashFlag==1){LCD_ShowString(2,7,"  ");}
	else{LCD_ShowNum(2,7,DS1302Time[5],2);}
	
	LCD_ShowNum(2,10,TimeSetSelect,2);
}

3、main.c

#include "DS1302.h"
void main()
{	
	LCD_Init();
	DS1302_Init();
	Timer0_Init();
	
	DS1302SetTime();
	while(1)
	{
		KeyNum=Key();
		if(KeyNum==1)
		{
			MODE=!MODE;//模式切换,这里要取非,不能取反,因为1的反是1111 1110,并不是0
			LCD_ShowNum(1,10,MODE,2);
			if(MODE==0)
			{
				DS1302SetTime();//将数组中的时间重新写入时钟
				LCD_ShowString(2,10,"  ");
			}	
		}
		switch(MODE)
		{
			case 0:
				TimeShow();
				break;
			case 1:
				TimeSet();
				break;
		}
	}
}
void Timer0_Routine() interrupt 1
{
	static unsigned int T0Count;//函数栈帧结束后保留T0Count
	TH0=64535/256;//每次进中断函数后,让这两个寄存器重新回到初值
	TL0=64535%256;
	T0Count++;//每中断一次T0Count++
	if(T0Count>=(500/(12/11)))//达到了定时0.5秒的作用(晶振11.0592)
	{
		T0Count=0;//重置为0
		TimeSetFlashFlag=!TimeSetFlashFlag;
	}
}

可调时钟修改某一位时间,通过定时器中断达到0.5秒闪烁功能。

二、蜂鸣器

1、蜂鸣器的介绍

蜂鸣器是一种将电信号转化为声音信号的器件。按驱动方式可以分为有源蜂鸣器和无源蜂鸣器。

有源蜂鸣器:内部自带振荡源,将正负极接上直流电压即可持续发音,频率固定。

无源蜂鸣器:内部不带振荡源,需要控制器提供振荡脉冲才可发声,调整振荡频率,可发出不同频率的声音。51系列单片机用的就是无源蜂鸣器,无源蜂鸣器仅限振荡发声,切忌长时间连续导通,防止线圈烧坏。

用蜂鸣器之前需要先知道这个蜂鸣器是有源还是无源的。

2、三极管放大驱动蜂鸣器

51单片机的蜂鸣器所需功率比LED大,不能直接用引脚驱动蜂鸣器。这和数码管的74HC245一样,需要驱动电路,引脚的电平将被当成一种控制信号而不是运行蜂鸣器的能量。

2.1NPN三极管工作原理(基极电流和发射电流均流向集电区)

NPN三级管导通要求:BE电压大于导通电压。(图上就是基极电压大于三极管的管压降即可导通)

基区做得很薄是为了能让发射区的电子更容易进入集电区,浓度很低是为了形成更小的基极电流,这样才能有更多的自由电子进入集电区。而发射区电子浓度高是为了确保有足够多的自由电子进入基区和集电区。

为什么右侧电路图中蜂鸣器接VCC端而不是接GND端?如果蜂鸣器接GND端,一旦蜂鸣器自身电阻过大,基区电压可能驱使不了蜂鸣器(基区电压小于(管压降+蜂鸣器压降),电路不通);但是蜂鸣器接VCC端,只要基区电压大于三级管的管压降,电路即可导通。

2.2PNP三极管工作原理(发射电流分别流向基极和集电极)

PNP三极管信号电流给低电平导通。

PNP三级管导通要求:EB电压大于导通电压。(VCC>基区电压+管压降即可导通)

所以蜂鸣器不接VCC端,如果接VCC端,蜂鸣器电阻一旦过大,三极管可能不导通。

3、集成电路驱动蜂鸣器

51系列单片机使用集成电路来驱动蜂鸣器。

4、按下按键后蜂鸣器播发声

1、Buzzer.h

#ifndef __BUZZER_H__
#define __BUZZER_H__
//调用这个函数,蜂鸣器就响多少秒
void Buzzer_Time(unsigned int ms);
#endif

2、Buzzer.c

#include "Buzzer.h"
#include <REGX52.H>
#include "delay.h"
#include <INTRINS.H>
sbit Buzzer=P2^5;
void Buzzer_Delay500us()		//@11.0592MHz
{
	unsigned char i;

	_nop_();
	i = 227;
	while (--i);
}

//调用这个函数,蜂鸣器就响多少秒
void Buzzer_Time(unsigned int ms)
{
	unsigned int i;
	for(i=0;i<ms*2;++i)
	{
		Buzzer=!Buzzer;
		Buzzer_Delay500us();//必须要有延时,保证高低电平维持0.5ms(几字波形)
	}	
}

3、main.c

#include <REGX52.H>
#include "key.h"
#include "Nixie.h"
#include "Buzzer.h"
#include "delay.h"
unsigned char KeyNum;
void main()
{
	Nixie(1,0);
	while(1)
	{
		KeyNum=Key();
		if(KeyNum!=0)
		{
			Buzzer_Time(100);//蜂鸣器发声100ms 	
			Nixie(1,KeyNum);
		}
	}
}

5、蜂鸣器利用定时器循环播放小星星

反转两次电平,为两个发声周期,对应一个完整周期,计一个数。部分音符对应的定时器重装值。

#include <REGX52.H>
#include "delay.h"
#include "Timer0.h"
#define SPEED 500
sbit Buzzer=P2^5;
unsigned char FreqSelect,MusicSelect;//频率下标/音色顺序
unsigned int FreqTable[]={
	0,
	63465,63577,63691,63792,63892,63981,64070,64152,64229,64303,64372,64442,
	64499,64557,64612,64664,64713,64759,64803,64844,64883,64919,64954,64987,
	65017,65047,65074,65100,65124,65148,65169,65190,65209,65228,65245,65261,
};
//假设16分音符为1
unsigned char Music[]={
13,4,
13,4,
20,4,
20,4,
22,4,
22,4,
20,4+4,
0,4,//停止符
18,4,
18,4,
17,4,
17,4,
15,4,
15,4,
13,4+4
};
void main()
{
	Timer0_Init();
	while(1)
	{
		if(MusicSelect>=sizeof(Music))//判断越界
		{
			MusicSelect=0;
		}
		FreqSelect=Music[MusicSelect++];
		delay_ms(SPEED/4*Music[MusicSelect++]);//根据音符的不同,进行不同的延时
		TR0=0;//让每一个音间隔开来,关闭定时器0
		delay_ms(5);
		TR0=1;
	}
}
void Timer0_Routine() interrupt 1
{

	TH0=FreqTable[FreqSelect]/256;//每次进中断函数后,让这两个寄存器重新回到初值
	TL0=FreqTable[FreqSelect]%256;
	//频率不为0时,蜂鸣器才振荡
	if(FreqTable[FreqSelect])
	{
		Buzzer=!Buzzer;
	}	
}

根据乐谱频率,设定定时器重装值,并对蜂鸣器电平进行改变,蜂鸣器就能根据指定的电平信号发出不同频率的声音。 

物联沃分享整理
物联沃-IOTWORD物联网 » 程序使用51单片机实现DS1302时钟和蜂鸣器控制程序

发表评论