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的用法,稍微复杂些,以后再做介绍。

 

 

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

 

 

热门文章

暂无图片
编程学习 ·

K8S-05-Jenkins部署springCloud项目-gateway

Jenkins部署springCloud项目-gateway代码结构及文件Jenkins配置 代码结构及文件application.yml文件内容server:port: 9527 spring:application:name: cloud-gatewaycloud:nacos:discovery:server-addr: 192.168.0.31:8848gateway:discovery:locator:enabled: trueroutes:- id:…
暂无图片
编程学习 ·

php编写的旅游网站

使用PHP编写一个简单的旅游网站! wampserver集成环境编写php+mysql使用最新的Bootstrap(v4.5.0)框架详细的前端功能详细的用户后台管理具体又详细的文件上传函数文件提取链接:https://pan.baidu.com/s/1EeS4o4FnoSWLQ8f_oFg0CQ 提取码:idqm
暂无图片
编程学习 ·

ClassName(类名)命名

ClassName命名 ClassName的命名应该尽量精短、明确,必须以字母开头命名,且全部字母为小写,单词之间统一使用下划线 “_” 连接。 eg:.nav_top 注意事项 ad、banner、gg、guanggao 等有机会和广告挂勾的字眠不建议直接用来做ClassName,因为有些浏览器插件(Chrome的广告拦截…
暂无图片
编程学习 ·

MySQL

MySQL MySql基础 1:基本数据库命令 命令行连接 #使用cmd连接mysql --连接数据库 mysql -uroot -p123456 -- 修改用户密码 update mysql.user set authentication_string=password(123456) where user=root and Host = localhost; -- 刷新权限 flush privileges;-- 所有的sql语…
暂无图片
编程学习 ·

RabbitMQ 教程

RabbitMQ 教程 文章目录RabbitMQ 教程消息中间件安装及管理windows安装:RabbitMQLinux安装Mac安装基本概念主要概念Exchange的类型RabbitMQ的工作模式及代码示例简单模式 Simple2.工作模式 work (资源竞争消费)3.发布订阅 publish/subscribe (广播)4.路由 routing5.主题订阅…
暂无图片
编程学习 ·

Vue动态缓存页面

原理通过keep-alive标签的include属性及vuex完成 需求说明:A页面到B页面需要缓存,A页面到C页面不需要缓存 所要缓存页面的顶级出口 <keep-alive :include="kpAlive"><router-view/> </keep-alive><script> export default {computed: {/**…
暂无图片
编程学习 ·

Struts2远程代码执行漏洞复现

环境搭建docker pull medicean/vulapps:s_struts2_s2-029docker run -d -p 80:8080 medicean/vulapps:s_struts2_s2-029http://yourIP/default.action打开页面漏洞利用(%23_memberAccess[allowPrivateAccess]=true,%23_memberAccess[allowProtectedAccess]=true,%23_memberAcce…
暂无图片
编程学习 ·

防火墙中的DMZ区域,Trust区域,Untrust区域

** 区域的作用: ** 1.安全策略都基于区域实施 2.在同一区域内部发生的数据流动是不存在风险的,不需要实施任何安全策略。 只有当不同安全区域之间发生数据流动时,才会触发设备的安全检查,并实施相应的安全策略。 3.一个接口只能属于一个区域,而一个区域可以有多个接口。 *…
暂无图片
编程学习 ·

培训网站比较-CSDN-51CTO-慕课网

本人是从事互联网行业,从码农到部门负责人,一路走来,最让我感受深刻的是,技术每天在更新迭代,自己一定要跟上脚步,不然很容易被淘汰。不管是作为技术人员还是部门管理者,技术能力必须得到重视。作为部门负责人,必须督促大家学习技术。我讲讲这几年在这方面的经历:一开…
暂无图片
编程学习 ·

Docker的帮助和镜像命令

帮助命令 docker version 查看docker版本 docker info 显示全系统信息 docker --help 显示docker相关的所有命令 镜像命令 列表镜像 docker images 列表本机上的镜像REPOSITORY --表示镜像的仓库源 TAG --表示镜像的标签 IMAGE ID --镜像的ID CREATED --镜像的创建时间 SIZE --…
暂无图片
编程学习 ·

Portworx Essentials 视频讲解

Portworx Essentials vs. Portworx Enterprise:https://www.iqiyi.com/v_19rzfuk1yw.html欢迎回到Portworx讲解视频系列,我是Ryan Warner。今天我们来介绍一下Portworx Essentials版本,以及与Portworx Enterprise版本的区别。Portworx Essentials是在K8S上运行数据管理的最必…
暂无图片
编程学习 ·

2020李宏毅学习笔记——33.Network Compression(2_6)

3.为什么要pruning? 首先有一个问题:既然最后要得到一个小的network,那为什么不直接在数据集上训练小(有local minima的问题)的模型,而是先训练大模型?解释一:模型越大,越容易在数据集上找到一个局部最优解,而小模型比较难训练,有时甚至无法收敛。 解释二:2018年的…
暂无图片
编程学习 ·

docker方式部署ELK

1.拉取原始镜像: docker pull sebp/elk:6602.启动下镜像方便进入,进行自定义配置修改:docker run -dit --name elk \-p 5601:5601 \-p 9200:9200 \-p 5044:5044 \-v /data/elasticsearch:/var/lib/elasticsearch \-v /etc/localtime:/etc/localtime \sebp/elk:660这里说明下560…
暂无图片
编程学习 ·

Netty之WebSocket应用

1. 什么是Netty?Netty是一个高性能事件驱动,异步非阻塞的IO Java开源框架,由Jboss提供,用于建立Tcp等底层的链接,基于Netty可以建立高性能的Http服务器,快速开发高性能、高可靠的网络服务器和客户端程序。它支持Http,websocket,tcp,udp等协议。同时Netty又是基于NIO的…
暂无图片
编程学习 ·

Android项目复盘4

个人主页:https://chengang.plus/ 文章将会同步到个人微信公众号:Android部落格UVC协议文档网址:https://www.usb.org/documents?search=&type%5B0%5D=55&items_per_page=50 主要下载USB Video Class 1_5,关注下载zip包中的UVC 1.5 Class specification.pdf文件,…
暂无图片
编程学习 ·

【小甲鱼】零基础入门学习Python(二)函数

function.__doc__function(a = x, b = y) 关键字参数def function(a = x, b = y): 默认值参数def function(*params) 收集参数,可输入多个参数打包为元组函数无return时返回Nonegloble 将局部变量转化为全局变量闭包def FunX(x):def FunY(y):return x * yreturn FunYFunX(8)(5…