今天我们用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
为了方便后面写程序时,搞混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
#include //定义单片机的头文件
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的用法,稍微复杂些,以后再做介绍。
本文仅供参考,如有不足,还请指出。