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

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

电子时钟最重要的要求一是计时准确,二是省电。

硬件设计:

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

除了基本的时钟电路与复位电路,我们用八位数码管来作为时间显示方式,其中P0口控制其段,P2口控制其位;以八个点动按钮作为键盘输入。

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

原理图中数码管原理可以看我上文有清楚介绍,这里暂不详细介绍。

可以看到,数码管位控制加入了一个锁存器,其作用是在待机时方便关闭数码管。其11脚是地址锁存端口,将其接高电平时,锁存器为透明模式,输入与输出完全相同,这里我直接接入VCC;1脚为输出锁存,高电平时无输出,低电平才有输出,这里我们用P3.6来控制其输出。

为了简化电路,蜂鸣器与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时间较长,所以我采用洞洞板来制作电路板,可以看到飞线很多,两块主板之间有较多的连接线。注意焊接单片机底座时,不要把单片机装在底座上,以免焊接时烧坏单片机芯片;同样,焊接晶振时,要尽可能快,避免长时间给晶振加热而损坏晶振;安插单片机芯片时也要小心,其引脚很容易折断;排阻公共端判断方法:在排阻最左边或者最右边会有个白色小点,有白点的一端为公共端。

单片机程序开发常用 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软件自动生成,只要输入需要延时的时间,软件可以自动生成一个延时函数,直接复制粘贴就可以(最小时间为1ns)。   由于我们需要多种时间的延时,所以我们可以先把需要的延时函数先写在前面,方便之后的调用。

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

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

为了添加更多花样,还添加了一个开机‘动画’  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;}
                            }
                      }			 	
	           }
	     }
      }   	
}

程序烧录:

写好程序之后,我们需要进行编译。若是首次编译,通常不会自动生成hex文件,需要进行如下设置:点击图中1处按钮“Option for Target”,在弹出的窗口中点击“Output”,然后勾选“Create HEX file”。点击确定后,点击序号4处的编译按钮,即可编译程序。

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

之后我们需要用到软件STC—IPS,这是专门用于STC系列单片机的程序烧录软件。

 

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

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

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

这样,整个作品就算完成了。

总结:

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

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

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

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

 

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

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

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

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

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

更为先进的办法是使用wifi模块esp8266从网络获取时间,再将时间送给单片机,这样,走时不准的问题就能得到彻底的解决。关于esp8266的用法,稍微复杂些,以后再做介绍。

 

 

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

 

 

热门文章

暂无图片
编程学习 ·

报表热切换是什么意思?如何做到?

热切换(Hot Swap)是指在系统不停机的情况下更换系统部件,在报表业务中则是指在不重启报表及相关应用的情况下完成对报表的维护(新增、修改、删除),冷切换则恰好相反。报表业务很不稳定,业务开展的过程中会刺激出更多查询统计需求,如果每次需求实现后都要等系统空闲(往…
暂无图片
编程学习 ·

Java工具类-BASE64加解密

1 引入apache commons-codec依赖<dependency><groupId>commons-codec</groupId><artifactId>commons-codec</artifactId><version>1.10</version> </dependency>2 使用Base64.encodeBase64加密public static String base64Enc…
暂无图片
编程学习 ·

单词学习2020

1. mindset: 心态;观念模式,思维倾向(1)This old mindset has not changed这个旧的思想意识还未转变(2)Enhancing leadership commitment and proactive mindset.增强领导力承诺和积极进取的心态(3)He faces all challenges by aggressive mindset他以积极的心态面对所…
暂无图片
编程学习 ·

Web前端页面制作流程以及注意事项,满满的干货!

每天我们打开电脑,看到各种各样的web前端页面。你知道他们是如何制作的吗?为了让页面更具有规范性,让使用者更加方便,在制作页面过程中必须遵循一定的设计流程。在这里就为大家详细介绍一下制作一个Web前端页面的设计流程及注意事项。一:确定网站主题 每个网站都有自身以及…
暂无图片
编程学习 ·

使用john软件进行账户弱口令检测实验

使用john软件进行账户弱口令检测实验 前言 在生产环境中,服务器账号的密码能够不被黑客入侵破解是尤为重要的,关系着业务正常运行的安全,所以在创建完账户的密码后,我们需要进行弱口令的检测,排查出是否有容易被破解的密码存在。 本次实验使用的破解密码软件是john-1.8.0版…
暂无图片
编程学习 ·

c#UDP协议通讯

在写具体的用c#实现UDP通讯的方式之前,我们要先搞清楚相关的概念,那就是什么是UDP协议。UDP和TCP一样,是同属于TCP/IP协议簇的传输层协议:这里一定要注意TCP/IP是指一堆协议的集合,而这些集合之中又属TCP协议和IP协议比较重要,所以就将此协议集合以TCP/IP的方式进行命名。…
暂无图片
编程学习 ·

蓝桥杯-试题 算法训练 审美课-java

问题描述 《审美的历程》课上有n位学生,帅老师展示了m幅画,其中有些是梵高的作品,另外的都出自五岁小朋友之手。老师请同学们分辨哪些画的作者是梵高,但是老师自己并没有答案,因为这些画看上去都像是小朋友画的……老师只想知道,有多少对同学给出的答案完全相反,这样他就…
暂无图片
编程学习 ·

Flink原理与实现:Flink中的状态管理,keygroup,namespace

namespace维护每个subtask的状态上面Flink原理与实现的文章中,有引用word count的例子,但是都没有包含状态管理。也就是说,如果一个task在处理过程中挂掉了,那么它在内存中的状态都会丢失,所有的数据都需要重新计算。从容错和消息处理的语义上(at least once, exactly onc…
暂无图片
编程学习 ·

献给mac新手,Mac日常软件APP推荐

不少初入Mac 或者 Macbook 的朋友会因为 macOS 系统的独特性而感到新奇又迷惑,它不仅在操作逻辑上和 Windows 大相径庭,软件生态也是截然不同。初用 macOS 系统时可能会一头雾水,完全不知道自己应该去哪里下载 App,也不知道有哪些常用的 App。其实 Mac 上有很多精致又好用的…
暂无图片
编程学习 ·

数据库导出到excel解决科学计数法问题

用Navicat等工具导出数据到excel的时候,身份证等超过11位的数字会自动转换成科学计数法,末尾数字变成“0000”。如何解决?解决方式:给超过11位的数字末尾添加 \t查询的时候,给相关字段添加 \tSELECT name,CONCAT(idcard,\t) from lm_reg然后再将查询结果导出到excel。如…
暂无图片
编程学习 ·

机器学习 | 优秀Tensorflow开源项目汇总(上)

1、Open_model_zoo预先训练的深度学习模型和样本(高质量且快速)https://github.com/opencv/open_model_zoo2、Deep Learning In Productionhttps://github.com/ahkarami/Deep-Learning-in-Production3、AndroidtensorflowmachinelearningexampleAndroid TensorFlow机器学习示…
暂无图片
编程学习 ·

HTTP Header (请求头&响应头)

1. 关于HTTP消息头HTTP消息头是在,客户端请求(Request)或服务器响应(Response)时传递的,位请求或响应的第一行,HTTP消息体(请求或响应的内容)是其后传输。HTTP消息头,以明文的字符串格式传送,是以冒号分隔的键/值对,如:Accept-Charset: utf-8,每一个消息头最后以…
暂无图片
编程学习 ·

叩丁狼开发工程师:SSR服务架构特点分析

叩丁狼开发工程师:SSR服务架构特点分析SSR服务架构是我们在搭建一些开放性平台的时候需要添加的一项功能应用,而今天成都软件开发工程师就通过案例分析来了解一下,关于SSR服务架构的特点都有哪些内容?1、抵抗单页面大流量 要抵抗单页面的大流量,先我们自然而然会想到会使用…
暂无图片
编程学习 ·

[leetCode]83.删除排序链表中的重复元素

解法一 双指针 思路:跟删除排序数组相同元素想法一致,想到的是使用双指针,一个慢指针i,一个快指针j。首先i指向链表头部,j指向i的下一个元素 如果j元素的值等于i元素的值则跳过重复元素j = j.next,否则不用跳过该元素 每次内循环结束(跳过相同元素结束)使i.next = j;i=…
暂无图片
编程学习 ·

request.getParameter()与request.getSession().getAttribute类型比较

以下内容作为个人学习记录,如果给原作者造成不便,请私信,会立即删除。 区别: getParameter是获取表单提交的数据,或者post和get拼接提交的。获得的类型是String类型的。 request.getSession().getAttribute是获取session中的数据,和session.setAttribute搭配使用。获取的…
暂无图片
编程学习 ·

计算机组成原理复习 --- 第四章 指令系统

文章目录4.1 指令概述4.2 指令格式(重点)4.3 寻址方式 4.1 指令概述 计算机的指令有微指令、机器指令和宏指令。微指令为微程序级的命令,主要面向硬件,宏指令是由若干机器指令组成的软件指令,它是面向软件的。机器指令主要处在微指令和宏指令之间。 指令系统:一台计算机中…
暂无图片
编程学习 ·

材料力学与弹性力学中讲到的 扭转

材料力学圆轴扭转、非圆截面扭转、开口薄壁杆件扭转、闭口薄壁杆件扭转 圆轴的塑性扭转1.非圆截面扭转矩形梁进行扭转,边缘处有最大切应力。在边缘上,最大切应力位于长边的中点处。 短边处的最大切应力τ1与长边处的最大切应力τ2都位于他们的中点处,但后者大于前者。 当h/b…
暂无图片
编程学习 ·

液压传动与控制QY-QDSY16

1.液压传动的工作原理 以液体作为工作介质,并以其压力能进行能量传递的方式,即为液压传动。 2.液压传动的特征 ⑴力(或力矩)的传递是按照帕斯卡原理(静压传递定律)进行的 ⑵速度或转速的传递按容积变化相等的原则进行。“液压传动”也称“容积式传动”。 3.液压传动装置的…