弄了个四位带冒号和小数点的数码管,想着快到1000天纪念日了,于是准备弄个计日的小东西,由于自己DIY的比较丑,就网上淘了一个,但是网上的不符合要求呢,没事,反正网上八成用的是51单片机,基本都可以在线编程了,所以买个回来复原下电路,然后自己在写程序呗。
找了个全白一体的模块,电路印刷也比较整齐的,显示效果如图:
图1 数码管显示效果图
图2 模块电路板
由于电路是双面印刷,为保险起见,还是解焊了数码管,用万用表把电路测试一遍,确保恢复的是正确的电路。主控芯片是STC15LE,sop8封装,数码管驱动芯片是TM1650,sop16封装,时钟是DS3231模块,带有16个八位寄存器,也是sop16封装,数码管是八段4位的,第二位小数点被替换成了冒号。
图3 数码管背面
手动画出的电路复原图,还比较简单,毕竟就是个时钟和显示功能,并不复杂。然后开始写程序。
图4 模块电路板背面
图5 手画电路图
图6 电路图草稿:)
先到官网上找到TM1650的芯片资料:
TM1650是一种带键盘扫描接口的LED(发光二极管显示器)驱动控制专用IC
特性说明:
- 两种显示模式( 8段×4 位 和 7 段×4 位)
- 支持单个按键7x4bit(28个按键)和组合按键( 4个)
- 8级亮度可调
- 段驱动电流大于25mA,位驱动电流大于150mA
- 高速2线串行接口( CLK,DAT)
- 振荡方式:内置RC振荡
- 内置上电复位电路
- 内置数据锁存电路
- 支持3-5.5V电源电压
- 抗干扰能力强
重点是引脚定义:
图7 TM1650引脚定义
图8 引脚定义
官网也有例程,其实用起来很简单,用TM1650可以很方便的驱动四位数码管,例:
先定义不同的字节对应的显示状态:
uchar CODE[] = {0xFC,0x84,0xBA,0xAE,0xC6,0x6E,0x7E,0xA4,0xFE,0xEE,0x00};//0-9,全灭状态
显示:
TM1650_Set(0x68, CODE[0]); //第一位显示0,第2、3、4位依次为0x6A、0x6C、0x6E TM1650_Set(0x48,0x21); //设置亮度,亮度从低到高依次为
TM1650的显示内容、亮度状态设置和获取都是通过访问设置寄存器实现的,具体就是通过I2C总线发送地址、数据,所以看看文档里关于寄存器的定义就可以实现,而TM1650的扫描按键功能通过I2C总线发送0x49命令获取,根据I2C总线发回的值判断按下的键,同时要注意,当按揭释放后,第6bit会变为0,据此判断按键释放。
key = Scan_Key(); if(key==0x47) //key set { ……//do something here while(Scan_Key()==0x47); }
DS3231也是I2C总线的,网上资料非常多,这里不复述,引用封装好的函数,可以很方便的完成时间获取、设置和寄存器的访问、设置。本程序中除了用DS3231获取设置时间,还利用DS3231产生的1HZ方波对51引发外部中断,完成调整时的冒号闪烁功能以及显示内容切换(主要在月-日显示与年-周显示之间切换)。外部中断函数如下:
void int2() interrupt 10 //DS3231输出1HZ方波,下降沿更新 { //display time now, flash colon if(dspmode == 0){ sec=!sec; //sec标识冒号的显示与隐藏,实现闪烁 } //when adjust date else if(dspmode == 1){ secMode++; secMode%=8; //secMode标示显示子模式 } //display date now else if(dspmode == 2){ } AutoLight(); //根据当前小时信息进行时钟亮度调整 }
由于模块自带只有两个按键,所以设计功能逻辑的时候还毕竟麻烦,主函数里一堆switch—case,先根据dspmode变量判断显示模式,然后根据setmode判断调整模式,当setmode为0时为该显示模式下的正常显示状态,即不进行调整,当该模式下检测到set按键则跳转到对应的设置状态,当调整模式为0时检测到按键为add,则跳转到不同的显示模式,而除此以外的调整模式下按键add为调整状态。主要涉及时间、日期、年、星期、积日的显示和设定,因而逻辑毕竟繁杂,但是不难。伪代码如下:
switch(dspmode){ case 0://时:分模式 switch(setmode){ case 0: //显示模式 case 1: //调整小时 case 2: //调整分 case 3: //调整秒 } break; case 1: //月:日----年-星期模式 switch(setmode){ case 0: //显示模式 case 1: //调整年份 case 2: //调整月份 case 3: //调整日期 case 2: //调整星期 } break; case 2://日期计数模式 //显示当前日期累计值 break; }
51单片机内置时钟中断,当调整模式时闪烁调整位,代码如下:
void timer0() interrupt 1 { uchar flag; TH0=(65536-50000)/256; TL0=(65536-50000)%256; flag++; if(flag==5) { flag=0; flash=!flash; } }
通过flag标示闪烁与否,闪烁速度比DS3231输出中断快以示区别。
代码总览(代码参考:http://www.pudn.com/Download/item/id/2881058.html),因为STC15LE程序存储空间只有4kb,而程序一度达到4080字节,因而主函数用了整个大的switch-case语句,毕竟重用部分少没有另设函数,所以代码看起来没那么清晰。
//#include <15f104.h> #include<reg51.h> #include<intrins.h> #include"ds3231.h" #include"tm1650.h" #define uchar unsigned char #define uint unsigned int sfr INT_CLKO = 0x8f; //外部中断与时钟输出控制寄存器 sfr IE2 = 0xaf; //中断使能寄存器2 sfr T2H = 0xD6; //定时器2高8位 sfr T2L = 0xD7; //定时器2低8位 uint beginYear=2014; uchar beginMonth=6; uchar beginDate=7; uint year,day; uchar i, setmode,flash,dspmode; uchar month,date,dayofweek; uchar hour,minute; //显示缓冲 uchar second; uchar secMode; uchar CODE[] = {0xFC,0x84,0xBA,0xAE,0xC6,0x6E,0x7E,0xA4,0xFE,0xEE,0x00};//0-9,全灭 bit sec;//1HZ秒点闪烁 //延时 void delaySt(){ uint j; uchar k; for(k=0;k<10;k++){ for(j=0;j<12000;j++){ _nop_(); } } } //从DS3231获取当前日期 void GetDateNow() { uchar dstmp,dstmp2,dstmp3; dstmp = ReadDS3231(RTC_YR_REG_ADDR); dstmp2 = ReadDS3231(RTC_MON_REG_ADDR); dstmp3 = ReadDS3231(RTC_DAY_REG_ADDR)-1; year = dstmp/16*10 + dstmp%16 + ((dstmp2 & 0x80) ? 2000 : 1900); month = dstmp2&0x7F; date = ReadDS3231(RTC_DATE_REG_ADDR); hour = ReadDS3231(RTC_HR_REG_ADDR); dayofweek = dstmp3?dstmp3:7; } //从DS3231获取当前时间 void GetTimeNow() { hour = ReadDS3231(RTC_HR_REG_ADDR); minute = ReadDS3231(RTC_MIN_REG_ADDR); } //判断是否闰年 bit RunYear(uint yearc){ return (yearc%400==0||(yearc%100!=0&&yearc%4==0))?1:0; } //返回当月天数 uchar DayInMonth(uint yearc,uchar monthc){ int day[12]={31,28,31,30, 31,30,31,31, 30,31,30,31}; if(monthc==2){ return RunYear(yearc)?29:28; }else{ return day[monthc-1]; } } //计算当年天数 uint DaysInYear(uint yearc,uchar monthc,uchar datec){ uint daytmp = 0; for(i=1;i<monthc;i++){ daytmp += DayInMonth(yearc,i); } return daytmp + datec; } //计算从开始日期到现在天数 void CalDay(){ uint daytmp; uint i; uint beginDays; uint lastDays; daytmp = 0; GetDateNow(); beginDays = DaysInYear(beginYear,beginMonth,beginDate); lastDays = DaysInYear(year,month/16*10+month%16,date/16*10+date%16); for(i = beginYear;i<year;i++){ daytmp += RunYear(year)?366:365; } day = daytmp + lastDays - beginDays + 1; } //显示年 void display_year(){ TM1650_Set(0x68, CODE[year/1000]); TM1650_Set(0x6A, CODE[year/100%10]); TM1650_Set(0x6C, CODE[year/10%10]); TM1650_Set(0x6E, CODE[year%10]); } //显示日期 void display_date() { uchar dsptmp ; if(secMode<5){//month-date if(month/16) { dsptmp = CODE[month/16]; TM1650_Set(0x68,dsptmp ); } else { dsptmp = 0x00; TM1650_Set(0x68,dsptmp); } TM1650_Set(0x6A, CODE[month%16]+1); TM1650_Set(0x6C, CODE[date/16]); TM1650_Set(0x6E, CODE[date%16]); }else{//year-dayofweek if(year>=10) { dsptmp = CODE[year/10%10]; TM1650_Set(0x68,dsptmp); } else { dsptmp = 0x00; TM1650_Set(0x68,dsptmp); } TM1650_Set(0x6A, CODE[year%10]); TM1650_Set(0x6C, 0x02); //0x02--the center short line TM1650_Set(0x6E, CODE[dayofweek]); } } //显示天数计数 void display_day() { CalDay(); if(day/1000)TM1650_Set(0x68, CODE[day/1000]);else TM1650_Set(0x68, 0x00); if(day>=100)TM1650_Set(0x6A, CODE[day/100%10]);else TM1650_Set(0x6A, 0x00); if(day>=10)TM1650_Set(0x6C, CODE[day/10%10]);else TM1650_Set(0x6C,0x00); TM1650_Set(0x6E, CODE[day%10]); } //根据当前时间调整亮度 void AutoLight(){ //auto set the light level of module if((hour>=0x19)||(hour<7)) TM1650_Set(0x48,0x21); else TM1650_Set(0x48,0x51); } //显示时间 void display_time() { if(hour/16)TM1650_Set(0x68, CODE[hour/16]);else TM1650_Set(0x68, 0x00); TM1650_Set(0x6A, CODE[hour%16]+(sec?1:0)); TM1650_Set(0x6C, CODE[minute/16]); TM1650_Set(0x6E, CODE[minute%16]); } //初始化单片机定时器 void Timer_init() //定时器,用于调整时间时数码管的闪烁 { TMOD=0X01; TH0=(65536-50000)/256; TL0=(65536-50000)%256; TR0=0; ET0=1; INT_CLKO=0x10;//开启外部中断2,下降沿触发 IE2 |= 0x04; EA=1; } //从ds3231读取初始化信息 void DS3232_init() { uchar modetmp = ReadDS3231(MODE_REG_ADDR); if(modetmp==0) { //bcd8421 WriteDS3231(RTC_SEC_REG_ADDR,0x00); //second WriteDS3231(RTC_MIN_REG_ADDR,0x11); //minute WriteDS3231(RTC_HR_REG_ADDR,0x20); //hour WriteDS3231(RTC_YR_REG_ADDR,0x17);//year WriteDS3231(RTC_MON_REG_ADDR,0x82);//month ,&0x08==cent WriteDS3231(RTC_DAY_REG_ADDR,0x02);//day of week WriteDS3231(RTC_DATE_REG_ADDR,0x13);//date WriteDS3231(MODE_REG_ADDR,0x11);// 用闹钟寄存器判断是否掉电 //hex, default is 2014.6.7 WriteDS3231(BEGIN_YR_REG_ADDR,0x0E);//beginyear WriteDS3231(BEGIN_MON_REG_ADDR,0x86);//beginmonth WriteDS3231(BEGIN_DATE_REG_ADDR,0x07);//begindate } else{ dspmode = modetmp >>4 -1; setmode = modetmp % 16 -1; } } void main() { uchar temp; uchar dstmp; uchar numtmp; uchar dayInMontmp; uchar yearAdj;//int month-date,year-day mode ,the adjust bit of year uchar key; Timer_init(); TM1650_Set(0x48,0x51);//初始化为6级灰度,开显示 DS3232_init(); // DS3231掉电时初始化一下 WriteDS3231(RTC_CTL_REG_ADDR,0x00); //设置INT位为1HZ方波输出 while(1) { delaySt(); AutoLight(); switch(dspmode){ case 0: //hour--minute mode************************** GetTimeNow(); key = Scan_Key(); if(key==0x47) //key set { setmode++; setmode %= 4; while(Scan_Key()==0x47);//if key be released, its 6th bit turn to 0 ,thus its 0x07 } switch (setmode) { case 0: //GetTimeNow adjust hour:minute { INT_CLKO=0x10; TR0=0;//adjust flash disable if(key==0x77) //key add { while(Scan_Key()==0x77); //if key be released, its 6th bit turn to 0 ,thus its 0x37 dspmode = 1; setmode = 0; secMode = 0; } break; } case 1: //hour adjust { INT_CLKO=0x00; sec=1; TR0=1; minute = ReadDS3231(RTC_MIN_REG_ADDR); if(key==0x77) { while(Scan_Key()==0x77); temp=ReadDS3231(RTC_HR_REG_ADDR); temp=(temp>>4)*10 + (temp & 0x0F); temp++; temp %= 24; temp = ((temp/10)<<4)|(temp%10); WriteDS3231(RTC_HR_REG_ADDR,temp); } if(flash) hour = ReadDS3231(RTC_HR_REG_ADDR); else hour = 0xaa; break; } case 2: //minute adjust { INT_CLKO=0x00; sec=1; TR0=1; hour = ReadDS3231(RTC_HR_REG_ADDR); if(key==0x77) { while(Scan_Key()==0x77); temp=ReadDS3231(RTC_MIN_REG_ADDR); temp=((temp>>4)*10)+(temp&0x0f); temp++; temp %= 60; temp = ((temp/10)<<4)|(temp%10); WriteDS3231(RTC_MIN_REG_ADDR,temp); } if(flash) minute = ReadDS3231(RTC_MIN_REG_ADDR); else minute = 0xaa; break; } case 3: //second adjust { INT_CLKO=0x00; sec=1; TR0=0; hour= 0xaa; if(key==0x77) { while(Scan_Key()==0x77); WriteDS3231(RTC_SEC_REG_ADDR,0x00); } minute = ReadDS3231(RTC_SEC_REG_ADDR); break; } default: break; } display_time(); break; case 1: //month--date ;;; year-dayofweek mode*************************** GetDateNow(); key = Scan_Key(); if(key==0x47) //key set { setmode++; setmode %= 5; while(Scan_Key()==0x47);//if key be released, its 6th bit turn to 0 ,thus its 0x07 } switch (setmode) { case 0: //display INT_CLKO=0x10; TR0=0; if(key==0x77) //key add { while(Scan_Key()==0x77); //if key be released, its 6th bit turn to 0 ,thus its 0x37 dspmode = 2; setmode = 0; } display_date(); break; case 1: //adjust year INT_CLKO=0x00; TR0=1; display_year(); if(key==0x77) //key add { yearAdj = 0; temp = 1; while(Scan_Key()==0x77); while(temp){ //enter adjust if(flash){ display_year(); } GetDateNow(); key = Scan_Key(); if(key == 0x47){ yearAdj ++; yearAdj %= 5; while(Scan_Key()==0x47); } switch(yearAdj){ case 0: if(!flash){ TM1650_Set(0x68, 0x00); TM1650_Set(0x6A, 0x00); TM1650_Set(0x6C, 0x00); TM1650_Set(0x6E, 0x00); } break; case 1: if(key == 0x77){ while(Scan_Key()==0x77); numtmp = year%10; year -= numtmp; numtmp = (++numtmp)%10; year += numtmp; WriteDS3231(RTC_YR_REG_ADDR,ReadDS3231(RTC_YR_REG_ADDR)&0xf0|numtmp); } if(!flash){ TM1650_Set(0x6E, 0x00); } break; case 2: if(key == 0x77){ while(Scan_Key()==0x77); numtmp = year/10%10; year -= numtmp*10; numtmp = (++numtmp)%10; year += numtmp*10; WriteDS3231(RTC_YR_REG_ADDR,ReadDS3231(RTC_YR_REG_ADDR)&0x0f|(numtmp<<4)); } if(!flash){ TM1650_Set(0x6C, 0x00); } break; case 3: if(key == 0x77){ while(Scan_Key()==0x77); dstmp = ReadDS3231(RTC_MON_REG_ADDR); numtmp = dstmp &0x80?1:0; year -= numtmp*100; numtmp = (++numtmp)%2; year += numtmp*100; if(numtmp == 1){ WriteDS3231(RTC_MON_REG_ADDR,dstmp|0x80); } else{ WriteDS3231(RTC_MON_REG_ADDR,dstmp&0x7f); } } if(!flash){ TM1650_Set(0x68, 0x00); TM1650_Set(0x6A, 0x00); } break; case 4: temp = 0; setmode = 2; break; default: break; } } } break; case 2: //adjust month***************************** INT_CLKO=0x00; TR0=1; secMode = 1; dstmp = ReadDS3231(RTC_MON_REG_ADDR); month = dstmp&0x7f; if(key==0x77) //key add { temp= month; temp=((temp>>4)*10)+(temp&0x0f); temp++; temp %= 12; temp = temp == 0?12:temp; month = ((temp/10)<<4)|(temp%10); temp = month|(dstmp&0x80); WriteDS3231(RTC_MON_REG_ADDR,temp); while(Scan_Key()==0x77); } if(!flash){month = 0xaa;} display_date(); break; case 3: //adjust date***************************** INT_CLKO=0x00; TR0=1; dstmp = ReadDS3231(RTC_DATE_REG_ADDR); if(key==0x77) //key add { temp=dstmp; temp=((temp>>4)*10)+(temp&0x0f); temp++; dayInMontmp = DayInMonth(year,month); temp %= dayInMontmp; temp = temp == 0?dayInMontmp:temp; temp = ((temp/10)<<4)|(temp%10); date = temp; WriteDS3231(RTC_DATE_REG_ADDR,temp); while(Scan_Key()==0x77); } if(!flash){date = 0xaa;} secMode = 1; display_date(); break; case 4: //adjust day in week***************************** INT_CLKO=0x00; TR0=1; secMode = 6; dstmp = ReadDS3231(RTC_DAY_REG_ADDR)-1; dayofweek = dstmp?dstmp:7; if(key==0x77) //key add { //(ReadDS3231(RTC_DAY_REG_ADDR)-1)?(ReadDS3231(RTC_DAY_REG_ADDR)-1):7]; temp = (dstmp+1) % 7; dayofweek = temp?temp:7; temp++; WriteDS3231(RTC_DAY_REG_ADDR,temp); while(Scan_Key()==0x77); } if(!flash){dayofweek = 0x0A;} display_date(); break; default: break; } break; case 2://display day count mode key = Scan_Key(); if(key==0x47) //key set { setmode = 0; while(Scan_Key()==0x47);//if key be released, its 6th bit turn to 0 ,thus its 0x07 } switch (setmode) { case 0: INT_CLKO=0x10; TR0=0; if(key==0x77) //key add { while(Scan_Key()==0x77); //if key be released, its 6th bit turn to 0 ,thus its 0x37 dspmode = 0; setmode = 0; } display_day(); break; } break; default: dspmode = 0; break; } WriteDS3231(MODE_REG_ADDR,dspmode*16 + setmode + 17);//set mode into ds3231 register } } void timer0() interrupt 1 { uchar flag; TH0=(65536-50000)/256; TL0=(65536-50000)%256; flag++; if(flag==5) { flag=0; flash=!flash; } } void int2() interrupt 10 //DS3231输出1HZ方波,每个下降沿更新时间,冒号闪烁 { //display time now, flash colon if(dspmode == 0){ sec=!sec; } //when adjust date else if(dspmode == 1){ secMode++; secMode%=8; } //display date now else if(dspmode == 2){ } }
最终效果见图1和下图:
PS:
中间遇到显示乱码和显示值跳动问题,一直没解决,后来发现是中断函数中调用了某些函数导致函数重入破坏堆栈了,后来就把中断函数中的函数全部移到主函数中就OK了。
后面又改了下程序,弄成起始日期也可以设置的,就是在第三个模式下增加了日期调整功能,又加了200行代码,差点单片机空间不够烧不进去了。主要是只有两个按键造成程序的复杂和庞大,需要代码的留言或邮箱联系,因为增加的功能有点故弄玄虚,没有贴出来的必要。。。
附录:
代码见【http://download.youkuaiyun.com/download/atp1992/10208348】
参考资料【 http://download.youkuaiyun.com/download/atp1992/10208353 】
原文:【http://www.straka.cn/blog/nixie_tube_clock/】