纪念日计数数码管小时钟
Abstract:A nixie tube clock with functions of date accumulate and time/date display. It’s made by ICs of TM1650、STC15L104N、DS3231.
弄了个四位带冒号和小数点的数码管,想着快到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可以很方便的驱动四位数码管,例:
先定义不同的字节对应的显示状态:
1 |
uchar CODE[] = {0xFC,0x84,0xBA,0xAE,0xC6,0x6E,0x7E,0xA4,0xFE,0xEE,0x00};//0-9,全灭状态 |
显示:
1 2 |
TM1650_Set(0x68, CODE[0]); //第一位显示0,第2、3、4位依次为0x6A、0x6C、0x6E TM1650_Set(0x48,0x21); //设置亮度,亮度从低到高依次为 |
TM1650的显示内容、亮度状态设置和获取都是通过访问设置寄存器实现的,具体就是通过I2C总线发送地址、数据,所以看看文档里关于寄存器的定义就可以实现,而TM1650的扫描按键功能通过I2C总线发送0x49命令获取,根据I2C总线发回的值判断按下的键,同时要注意,当按揭释放后,第6bit会变为0,据此判断按键释放。
1 2 3 4 5 6 |
key = Scan_Key(); if(key==0x47) //key set { ……//do something here while(Scan_Key()==0x47); } |
DS3231也是I2C总线的,网上资料非常多,这里不复述,引用封装好的函数,可以很方便的完成时间获取、设置和寄存器的访问、设置。本程序中除了用DS3231获取设置时间,还利用DS3231产生的1HZ方波对51引发外部中断,完成调整时的冒号闪烁功能以及显示内容切换(主要在月-日显示与年-周显示之间切换)。外部中断函数如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
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为调整状态。主要涉及时间、日期、年、星期、积日的显示和设定,因而逻辑毕竟繁杂,但是不难。伪代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
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单片机内置时钟中断,当调整模式时闪烁调整位,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 |
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语句,毕竟重用部分少没有另设函数,所以代码看起来没那么清晰。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 |
//#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行代码,差点单片机空间不够烧不进去了。主要是只有两个按键造成程序的复杂和庞大,需要代码的留言或邮箱联系,因为增加的功能有点故弄玄虚,没有贴出来的必要。。。
附录:
代码见【Day_fixDay】
参考资料【TM1650_V2.0】