51单片机初学3-从零开始制作一款电子时钟

今天我们用STC89C52制作一款简单的单片机作品:电子时钟。除了基本的走时功能,还能手动调节时间,设置闹钟,待机唤醒。

本文包括硬件与软件设计。

我认为电子时钟需要考虑的两点:一是计时准确,误差小;二是省电,使其能在移动电源供电下超长待机。

硬件设计:

首先我们需要构思好系统框架:

基本的时钟电路与复位电路不用多说,我们用八位数码管来作为时间显示方式(显示样式为:12-00-00),其中P0口控制其段,P2口控制其位;以八个点动按钮作为键盘输入;蜂鸣器、LED分别作为提示音和指示灯。

接下来就可以设计原理图:

可以看到数码管的接线较复杂,其原理暂不多说(可参考文章51单片机初学2-数码管动态扫描_#liufenges#的博客-CSDN博客_数码管动态扫描),可以看到两个数码管的1、2、3、4、5、7、10、11是分别连起来的,然后引出来连接到P0口;两个数码管的6、8、9、12共8个脚与P2口连接。

需要注意,数码管位控制与P2口之间加入了一个锁存器74HC373,其作用是在待机时方便关闭数码管。其11脚是地址锁存端口,将其接高电平时,锁存器为透明模式,输入与输出完全相同,这里我直接接入VCC;1脚为输出锁存,高电平时无输出,低电平才有输出,这里我们用P3.6来控制其输出。下图是74HC373引脚图及其功能。

为了简化电路,蜂鸣器与LED共用一个I/O口;

单片机的数据串口引出来接到排针上,方便程序烧录。

需要注意,为了防止数码管烧坏,在P0口应串联470欧姆的限流电阻(原理图中未画出)。

所以得到所需材料:

STC89C52芯片(1块),40P底座(1只),面包板(2片),3461BS数码管(2只),点动按钮(9只),LED灯(1只),74HC373锁存芯片(1片),10K 9P排阻(4只),470欧电阻(15只),12M晶振(1只),30pF瓷片电容(两个),排针(15针),led灯,有源蜂鸣器一只(关于有源与无源蜂鸣器的区别可在网上查阅),PNP型三极管一只。

最后我们按照原理图焊接元件。以下为成品图片可供参考

为了使作品看起来简洁,我们采用双主板设计,上层为数码管、键盘,下层为单片机最小系统,两层主板使用小螺栓固定。

由于定做PCB时间较长,所以我使用洞洞板来制作电路板(若是不擅长电子焊接,最好是制作PCB),可以看到飞线很多,两块主板之间有较多的连接线(为了防止焊点受力而脱落,可以将线绕在洞洞之间)。注意焊接单片机底座时,不要把单片机装在底座上,以免焊接时烧坏单片机芯片;同样,焊接晶振时,要尽可能快,避免长时间给晶振加热而损坏晶振;安插单片机芯片时要注意对齐引脚,以免折断或者接触不良,插好后可以用万用表测量一遍所有引脚是否与底座导通;排阻公共端判断方法:在排阻最左边或者最右边会有个白色小点,有白点的一端为公共端;点动按钮有四个引脚(一组常开触点,一组常闭触点),可按照原理图所示将两个引脚接入。

单片机程序开发常用 keil软件(这里我们以Keil uVision3为例):

首先新建工程(点击project→new→选择一个文件地址后保存),然后选择CPU型号。

STC89C52是完全兼容AT89C52的(因为STC是国产芯片,keil中没有STC芯片,只能用其他芯片代替),所以我们选择AT89C52即可(首先点Atmel,下拉之后,可以找到AT89C52)。

之后会弹出询问窗口:Copy standard 8051 Startup code to Project Folder and Add File to project?(是否复制8051启动编码到工程文件夹?),点击确认即可。若点击取消,在创建文件时也会自动添加。

可以看到创建了一个Target1的工程文件,下拉时候还有一个Source Group1的文件夹。这个文件夹里有个STARTUP.A51的文件,这就是刚才复制的8051启动编码,里面包含51单片机的寄存器、I/O口等地址的分配,这些都是软件自动生成的,一般不需要去更改。

之后添加C程序文件:File→new。然后会创建一个text1的空白文件。然后我们点击保存(或者Ctrl+S),选择保存地址(保存在一个容易找到的地方,后面需要用到),输入文件名,注意文件名要加后缀.c保存为C文件。如果是用汇编语言写程序,则加后缀.ASM。

接着右击Source Group1,在菜单中找到Add Files To Group ‘Source Group1’点击(这个选项在菜单中有加粗显示)。然后将刚才的c程序文件添加至工程,关闭对话框。可以看到Source Group1下多了之前的C文件。

然后就可以写程序了。

程序编写:

定义单片机C程序的头文件#include<reg51.h>

为了方便后面写程序时,搞混I/O口,我们可以先定义一些功能引脚。例如蜂鸣器,我们查看原理图可以看到,蜂鸣器是由P3.1控制的,所以我们定义P3.1为蜂鸣器:sbit fm=P3^1;(‘sbit’是单片机用于定义引脚的关键字,在C语言中是没有这个关键字的;P3.1之间的点在程序中要用‘^’表示),这样,在之后的程序中,如果我们要用到蜂鸣器,只要让fm等于0或者等于1,就可以控制蜂鸣器的工作了,而不再需要使用P3^1了。

然后我们还要对数码管进行编码,数码管需要显示的字符较多,我们可以使用一个数组来定义:

char codeduan[]={0xc0,0xcf,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0x7f,0xbf,0xff,0x89};

(char数据类型:在单片机中,char数据类型所占空间最少,只有1个字节(八位二进制),但他的范围为 -128~127 (signed有符号型),unsigned为0~255。所以如果该变量数据范围不大,一般用char类型,这样做可以节省单片机空间)

接着定义全局变量 sec,min,hour.之所以定义为全局变量,是为了让这三个量所有函数中都是能使用的。

在本作品中,延时函数必不可少,比如数码管扫描,走时都需要延时函数(常用的方法还有定时器中断)。关于延时函数的计算问题可自行百度,为了方便,我们可以直接使用STC-IPS软件自动生成,只要输入需要延时的时间,软件可以自动生成一个延时函数,直接复制粘贴就可以(最小时间为1us)。   由于我们需要多种时间的延时,所以我们可以先把需要的延时函数先写在前面,方便之后的调用。

定义好需要的变量,我们就可以开始写主函数了。这里我们把数码管扫描与计时作为主程序,数码管扫描与计时同时进行。

接着编写调时子函数,闹钟子函数。在主程序插入判定条件,以此调用子函数。

为了添加更多花样,还添加了一个开机‘动画’  motos();(详情看后面的程序)

需要注意的是,子函数应置于主函数前面,否则编译时会提示 未定义子函数 。

再说说键盘的处理。键盘排列与键位设置如下。

K1、K2控制光标的左右移动,K3、K4控制数字加减,K5为确定键,K6为调时(长按4秒进入),K7设置闹钟,K8待机模式。

其他细节暂不多说,看程序即可。

完整程序如下:

/*电子时钟程序:基本电子时钟功能,能调节时间,能设置闹钟(已删减),有待机模式(已删减)*/
/*LED数码管显示器设定;
P0.0---P0.7段控线,接LED的显示段a,b,c,d,e,f,g,dp.
P2.0---P2.7位控线,从左至右

************键位设置*******************
 	            W3(+)  
  
  W1(光标左移)  W5(确认)   W2(光标右移)  	 W6(调时)      W7(闹铃)      W8 (唤醒) 
                
		        W4(-)           
**************************************/
#include<reg51.h>
#include<intrins.h>    //定义单片机的头文件
sbit fm=P3^1;          //定义单片机蜂鸣器
sbit plays=P3^6;	     //定义73HC373输出控制位
		 //    0    1    2    3    4    5    6    7    8    9    10  11   12   13   //
char codeduan[]={0xc0,0xcf,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90,0x7f,0xbf,0xff,0x89};	   //数码管段编码
            //    0    1    2    3    4    5    6    7    8    9    dp   -    空	H   //
char codebite[]={0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x00};				   //数码管位编码
char sec=0,min=0,hour=0;
void Delay1ms()		//@12.000MHz,1ms延时函数,用于数码管动态输出
{
	unsigned char i, j;
	i = 2;j = 239;
	do
	{
		while (--j);
	} while (--i);
}
void Delay50ms()		//@12.000MHz,用于蜂鸣器提示音,30ms
{
	unsigned char i, j, k;
	i = 2;j = 95;k = 43;
	do
	{	do
		{   while (--k);
		} while (--j);
	} while (--i);
}
void adjust()			     //时间调整模式子程序
{
   int H=0,cursor=3;
   char ks,twi,temps[8],K[8];
   temps[2]=11;
   temps[5]=11;
   fm=0;Delay50ms();fm=1;  //蜂鸣器响一声提示进入时间调整模式
   while(P1!=0xef)		    //如果没有按下K8,则执行循环
      {
	 if(H<180)	    {twi=0;}  	    //进入调整模式后,光标闪烁
	 if(H>180)	    {twi=1;}  
    	 if(H==360)     {H=0;}  
       for(ks=0;ks<8;ks++)
           {
	      if(cursor==1&&twi==0)
		  {
		  temps[0]=12;temps[1]=12;
		   }
		else
		   {temps[0]=sec%10;         //求余计算秒个位
                temps[1]=sec/10;}         //求商计算秒十位
	     
	      if(cursor==2&&twi==0)
		  {
		  temps[3]=12;temps[4]=12;
		   }
		else
		   {temps[3]=min%10;         //求余计算分个位
                temps[4]=min/10;}         //求商计算分十位
		    
		if(cursor==3&&twi==0)
		  {
		  temps[6]=12;temps[7]=12;
		   }
		else				   
		   {temps[6]=hour%10;	       //求余计算时个位
		    temps[7]=hour/10;}      	 //求余计算时十位		        
	      P2=codebite[ks];	      //数码管输出选位,从第0位开始//
	      P0=codeduan[temps[ks]]; //输出段,输出要显示的数字//
	      Delay1ms();			//延时1ms,防止数码管串码
		H++;
	      P0=codeduan[12];
		}
       if(P1==0xfe)	   			  /*按下‘左’键,将光标左移 */
	         { K[1]=1;}
       if(K[1]==1&&P1!=0xfe)
		   {K[1]=0;   cursor++;}
		
	 if(P1==0xfd)			   	   /*按下‘右’键,将光标右移 */
		   { K[2]=1;}
       if(K[2]==1&&P1!=0xfd)
		   {K[2]=0;   cursor--;}
	 if(cursor<1) { cursor=3;}		  
	 if(cursor>3) { cursor=1;}
		   		
	 if(P1==0xfb)			 	  /*按下‘上’键,将数字加一 */
               { K[3]=1;}
       if(K[3]==1&&P1!=0xfb)
		   { K[3]=0;   
		     switch(cursor)
		       {
			  case 1:sec++;break;
			  case 2:min++;break;
			  case 3:hour++;break;
			  default:break;
		        }
		   }		    
	 if(P1==0xf7)			 	  /*按下‘下’键,将数字减一 */
               { K[4]=1;}
       if(K[4]==1&&P1!=0xf7)
		   { K[4]=0;   
		     switch(cursor)
		       {
			  case 1:sec--;break;
			  case 2:min--;break;
			  case 3:hour--;break;
			  default:break;
		        }
		   }
	if(sec>59) {sec=0; }			     /*对时,分,秒范围进行限制 */
	if(sec<0)  {sec=59;}
	if(min>59) {min=0; }
	if(min<0)  {min=59;}	   		   		
   	if(hour>23){hour=0; }
	if(hour<0) {hour=23;}	   		
	 }
       return;					    //如果检测到K8按下,则跳出循环,返回主函数
} 
 /*开机动画子程序*/
void motos()
{
 int mot=0;
 char m;
 char motobit[8]={0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80};
 char motoduan[8]={0xcf,0xa4,0xc0,0xa4,0x8e,0xc7,0xbf,0xbf}; /*编码显示“--LF2021” */
 while(mot<1800)
 {
  for(m=0;m<8;m++)
      { 
	 P2=motobit[m];	      //数码管输出选位,从第0位开始//
	 P0=motoduan[m];   //输出段,输出要显示的数字//
       Delay1ms();			//延时1ms,防止数码管串码
	 P0=codeduan[12];
	 mot++;
	 }
  }
  fm=0;Delay50ms();fm=1;Delay50ms();fm=0;Delay50ms();fm=1;
  return;
} 

 
  /*主程序,包含数码管显示以及计时*/
void main()
 {  int num=0,ks=0;
    char k,temp[8],moto=1;
    plays=0;	     /*打开锁存器74HC373使能端 */
    motos();	     /*调用开机动画 */
    temp[2]=11;
    temp[5]=11;
    while(1)
      {
       for(k=0;k<8;k++)
           {	  		     
	      temp[0]=sec%10;         //求余计算秒个位
            temp[1]=sec/10;         //求商计算秒十位
            temp[3]=min%10;         //求余计算分个位
            temp[4]=min/10;         //求商计算分十位
            temp[6]=hour%10;        //求余计算时个位
            temp[7]=hour/10;        //求商计算时十位
	      P2=codebite[k];	      //数码管输出选位,从第0位开始//
	      P0=codeduan[temp[k]];   //输出段,输出要显示的数字//
	      num++;
		Delay1ms();			//延时1ms,防止数码管串码
	      P0=codeduan[12];
		if(P1==0xdf)		//每次循环判断是否按下K1键
		   { 
		    if(num%10==0&&P1==0xdf)		//每10次循环,10ms,判断K1是否仍然按下
		       { 
		            ks++;		//如果每10次循环K1均按下,ks则自加一次
			      if(ks==300)	//如果KS记到300,表明k1已经连续按下4s,则进入时间调整模式,并将Ks清零
			          {
			           ks=0;
				     adjust();
			           } 
		        }		      //如果K1仍然按下,则将KS+1
		    }
            else{ks=0;}			//如果K1不再按下,则清零ks   
		if(num==865)		//经过与电脑时钟对比,找到最合适的值,以下为计时程序
		     {
		      sec++;
                  num=0;	      
                  if (sec==60)
                     {
                      sec=0;
                      min++;
                      if (min==60)
                           {
                            min=0;
                            hour++;
                            if (hour==24)
                               {hour=0;}
                            }
                      }			 	
	           }
	     }
      }   	
}

程序烧录:

由于单片机不能直接运行C语言或者汇编语言程序,必须将程序编译成hex文件才能写入单片机。写好程序后,若是首次编译,通常不会自动生成hex文件,需要进行如下设置:点击图中1处按钮“Option for Target”,在弹出的窗口中点击“Output”,然后勾选“Create HEX file”。点击确定后,点击序号4处的编译按钮,即可编译程序。

如果编译无误,则会显示0错误,0警告。并提示‘creating hex file from“#工程名#”’,说明HEX文件已经创建成功。

篇幅有限,这里暂不展示proteus仿真过程,可自行按照原理图构建电路并验证程序。

这里直接将程序写入单片机产品中。

我们需要用到软件STC—IPS,这是专门用于STC系列单片机的程序烧录软件,也附带一些辅助功能,比如定时器函数、上文提到的延时函数自动生成。下图是其窗口界面。

烧录之前,我们需要使用USB-TTL将电脑与单片机连接。连接方式如下图所示。

我们先要选择对应的单片机型号,连接单片机之后,若提示“串口打开失败”,则点击“扫描”,电脑会自动找到对应的串口。

接着,我们点击“打开程序文件”,选择刚才生成的hex文件,然后点击“下载/编程”即可将程序下载到单片机。若点击下载之后无反应,则关闭单片机电源重新打开,程序便可写入单片机。

这样,整个作品就算完成了。以下是其写入程序后的成品图。

总结:

♦功耗计算(暂时找不到标准的5V、3V电源):

充电宝供电:电压5.15V,电流30~40mA,功耗5.15X(30~40)=154.5mW~206mW;

三节镍氢电池:电压3.91V,电流20mA左右,功耗3.91X20=78.2mW。

总的来说,功耗还是偏高,经过测试,主要的功率都消耗在数码管。单片机的功耗不超过10mW,所以待机时将数码管关闭能有效减小功耗。

♦误差问题:本时钟经过实测,还是有可见的误差。

可调的误差:运行程序需要占用很多机器时间,总时间=延时函数的时间+其他程序执行时间。而其他程序执行时间是很难计算的,只能经过对比调试来压缩延时函数的时间。

欲尽可能减小误差,需要与标准时钟(电脑或者手机的网络时间)进行对比,计算出误差,然后调节延时函数的时间。

比如:我们延时函数刚开始设置为1000ms,经过与标准时间对比1小时发现,我的时钟慢了1S,说明我时钟的误差为1/3600=0.0002778s=0.2778ms=277.8us(为了更精确计算出误差,我们可以提高对比时间,时间越长,误差越好计算)。这样,我们就可以把延时函数的时间减小278us,那延时函数就要设置为1000000-278us=999722us.为了调节的方便,我们可以使用两级级延时,一级延时函数以ms为单位,二级延时函数以us为单位,这样就很方便调试。

不可调的误差是晶振的温漂问题,晶振的震荡频率是按照25℃环境制作的,如果温度偏大或者偏小,其震荡频率都会有略微变化,进而影响CPU执行速度,造成走时不准。

更为先进的办法是使用wifi模块esp8266从网络获取时间,再将时间送给单片机,这样,走时不准的问题就能得到彻底的解决。还能使用LCD1602或者LCD12864作为显示器,这样可以显示更多的内容,就可以加入更多的花样(以后会专门介绍这两款显示器)。

关于esp8266的用法,稍微复杂些,以后再做介绍。

本文仅供参考,如有不足,还请指出。

猜你喜欢

转载自blog.csdn.net/qq_55203246/article/details/114446168