
简介基于STC15的万年历程序是一套面向单片机初学者的完整实战资料以STC15系列8051内核MCU为核心整合DS1302实时时钟、DHT11/DHT22温湿度采集与报警、以及可编程闹钟等常用功能覆盖从硬件电路到软件逻辑的全流程开发。项目中细致说明了DS1302的串行寄存器读写、传感器单总线协议解析、阈值报警触发方式并辅以Protel电路图与Proteus仿真文件便于在焊接实物前进行功能验证。压缩包共23个文件约249KB主要包含Keil工程源码.c/.h/.hex/.uvproj、原理图.sch/.DDB/.PDF和仿真项目.pdsprj/.pdsbak等类型覆盖设计、编码、编译、仿真全环节适合课程设计、毕业设计或电子爱好者自制时钟时参考。已有351人学习对于想快速上手STC15与时钟芯片联调的用户可提供较为直观的参照。 去年冬天办公室那台电子钟第三次因为停电归零之后我彻底不想再忍了。与其再买一个不知道哪天又“罢工”的成品不如自己动手做一个。手头正好囤了一批STC15W408AS所以这个基于STC15的万年历程序就这么立项了。东西不大功能也不花哨就是年月日时分秒加星期按键可以校时断电之后靠后备电源继续走时做完之后我最大的感受是这种“入门级”项目里藏着的坑真的一点不比复杂项目少尤其是DS1302这颗RTC芯片的时序细节第一次玩的人基本都要被它折磨一遍。STC15W408AS是宏晶STC15系列里很经典的型号属于1T增强型8051内核片上Flash和SRAM都足够宽裕内部还集成了可配置的IRC时钟和EEPROM。万年历这种需求对主控算力要求不高但资源充足的好处是写代码不用抠抠搜搜非常适合拿来练手也可以作为一个给其他设备扩展时钟功能的稳定底座。这篇文章我会从方案选型、硬件电路、软件实现到调试心得完整走一遍。不管你是刚点亮过LED不久的新手还是想在已有设备上增加时钟功能的单片机爱好者按我这套思路去改造基本都能做出来。1. 整体方案设计为什么是STC15配DS13021.1 控制芯片选型STC15系列凭什么合适选STC15不是因为它参数多豪华而是因为“够用且省事”。STC15W系列是宽电压器件支持2.4V到5.5V这意味着用USB的5V、锂电池的3.7V甚至三节干电池的4.5V都能直接工作前级不需要额外加稳压芯片。对于万年历这种常年插电的桌面设备来说供电设计被大幅简化了。另一个很实在的点是下载方式。STC15系列支持串口ISP下载用一个USB转TTL小板就能烧录程序不需要专门的仿真器。板子上只要预留一个四针的下载口写代码、改Bug、重新烧录都非常流畅。再加上它内部有EEPROM虽然容量不大但存时间校准参数、界面设置这类小数据完全够用。内部IRC时钟还能让整个板子省掉一颗晶振对降低BOM成本和焊接难度都有帮助。我实际用的是STC15W408AS48KB Flash、4KB SRAM代码再怎么折腾都装得下。如果你想用更小封装的STC15W204S之类也可以但要注意引脚数量是否够用。万年历项目需要的IO不算多算上LCD1602的6根线、DS1302的3根线、3个按键总共12个IO左右选个20脚左右的封装比较从容。1.2 RTC芯片对比DS1302和DS3231怎么选万年历的核心是必须有独立于MCU的RTC实时时钟芯片。这里要强调一下不能用MCU的定时器去数秒因为一旦主控断电时间就全没了。RTC芯片自带晶振和计数电路主电源断开后靠纽扣电池或者法拉电容继续走时这才是真正意义上的“万年历”。市面上常用的RTC芯片主要有三款我做了个对比芯片接口精度典型值外围复杂度价格区间适用场景DS13023线串口取决于外部晶振一般月误差1-2分钟需要外接32.768kHz晶振和备份电池外围较简单1元左右低成本入门项目、学习用途DS3231I2C内置TCXO年误差1-2分钟几乎不需要外围自带晶振10元以上对走时精度要求高的设备PCF8563I2C同样取决于外部晶振需要外接晶振和电池2元左右I2C总线项目、低功耗场景我做这个万年历选了DS1302。理由很直接万年历对秒级精度没那么敏感DS1302的月误差完全可以通过软件校准修回来而且它的驱动代码在51/STC圈子里遍地都是参考资料多踩坑经验也好找。对于想学习RTC驱动原理的人来说DS1302的三线串口时序比I2C更直观能帮助你把“接口时序”这件事彻底吃透。1.3 显示与按键方案信息完整排在第一位显示方面我选了LCD1602没有选数码管。万年历要显示年月日时分秒加星期这是十几二十个字符的信息量。数码管如果用4位或6位需要反复切换页面才能看全时间日期体验很差。LCD1602两行16字符第一行放日期和星期第二行放时间一屏就能看到所有信息。LCD12864也可以能显示汉字界面更漂亮但接线多、驱动复杂对于万年历这种功能来说有点浪费。LCD1602的4线模式只需要6个IO对IO资源紧张的方案很友好。按键方面我用了三个设置、加、减。三个键就能完成时间日期的调整菜单逻辑做成状态机后面会在软件章节详细展开。2. 硬件电路设计要点别让小细节拖后腿2.1 引脚分配与最小系统这个项目的外围电路其实很简单重点在于引脚分配要顺手。我按自己实际用的SOP20封装做了下面的分配你可以照着接也可以根据手头的板型调整功能MCU引脚说明LCD1602 RSP2.0寄存器选择LCD1602 RWP2.1读写选择直接接GND也行LCD1602 ENP2.2使能信号LCD1602 D4-D7P2.3-P2.64线数据总线DS1302 CEP1.0片选信号DS1302 IOP1.1数据线建议外部加上拉DS1302 SCLKP1.2时钟线按键设置/加/减P3.0/P3.1/P3.2按键另一端接GND电源部分VCC和GND之间放了10uF电解电容加0.1uF陶瓷电容的组合尽量靠近芯片引脚。STC15的宽电压特性让我省掉了LDO直接USB 5V供电就行。下载口用四针排针引出VCC、GND、RXD、TXD交叉连接USB转TTL小板下载时选择对应芯片型号点“下载/编程”再给板子上电就能进入ISP模式。这个习惯要养成否则会出现下载超时的提示。2.2 DS1302外围电路晶振、电池和二极管的讲究DS1302的外围电路是这个项目里最容易翻车的地方值得单独拿出来说。首先是32.768kHz晶振负载电容要选6pF的版本这一点非常重要。我买过一批没有标注负载电容的便宜晶振焊上去怎么都不起振换成6pF负载电容的型号之后一次成功。晶振的两个引脚接DS1302的X1和X2走线尽可能短旁边不要走高频信号线。有些设计习惯会在X1、X2对地各加一个6pF到12pF的电容我画板时预留了位置调试时发现不加也能正常振荡但留位置的好处是万一遇到不起振可以手工补焊。然后是备份电池。DS1302的VCC2接主电源5VVCC1接3V纽扣电池。芯片内部会自动选择电压较高的一侧供电但为了更稳妥我会在VCC1支路上串一个1N4148二极管防止主电源通过芯片内部寄生二极管给电池倒灌充电。需要注意VCC1电压不要超过VCC2加0.3V否则可能引发芯片闩锁问题。如果你不想用纽扣电池VCC1接一个0.1F/5.5V的法拉电容也可以断电后维持几天没问题只是没有纽扣电池时间长。数据走线方面DS1302的IO引脚最好加一个10k上拉电阻到VCC。因为DS1302的数据脚在读取时是输出状态在写入或空闲时是高阻状态外部上拉能保证电平稳定减少误读的概率。CE和SCLK直接接单片机IO即可不需要额外处理。2.3 按键和LCD1602的接线细节按键电路很多人觉得简单但同样有讲究。STC15内部虽然可以配置上拉电阻但内部上拉阻值偏大抗干扰能力弱我习惯在按键外部加10k上拉到VCC按键另一端接GND按下时引脚被拉低读取为0。这样电平跳变干净利落后续消抖的压力也小。LCD1602这边有一个新手必踩的坑是对比度。第三脚VO如果直接接地绝大多数模块会黑成一团看不清内容。正确做法是用10k电位器从VCC到GND分压中间抽头接VO上电后调到屏幕刚好显示两行浅灰方块为止。如果没有电位器可以先接一个2k电阻到GND再根据实际效果调整阻值。背光正极串一个100欧限流电阻接VCC负极接GND亮度足够也不会过流。1602的RW脚如果只做写入操作可以直接接GND省掉一个IO口也少一段初始化逻辑。3. 软件核心实现驱动、日历算法与菜单逻辑3.1 程序整体结构和时间基准软件部分我采用经典的前后台架构即“主循环加定时器中断”。Timer0配置为1ms中断在中断里置一个标志位主循环检测到标志位后执行按键扫描、显示刷新、时间校准等任务。这种结构代码量小、逻辑直观、便于调试对万年历这种“非实时性要求不高”的项目完全够用。实际任务调度我按这个节奏来每10毫秒扫描一次按键并做消抖处理每500毫秒读取一次DS1302的时间并刷新LCD显示。显示不用刷太频繁秒的变化看得清楚就行频繁读写DS1302反而会增加不必要的总线操作和功耗。主循环里其余时间保持空闲这样MCU负载很低也让系统的实时性更稳。// 主循环简写示意 void main(void) { SystemInit(); while (1) { if (flag_1ms) { KeyProcess(); // 按键扫描与菜单处理 if (flag_500ms) { ReadRtcTime(); // 从DS1302读取时间 LcdDisplay(); // 刷新LCD } } } }这种写法虽然朴素但通过时间片划分能让按键响应、显示刷新、数据读取各干各的互不阻塞。对于学习任务调度来说这个框架比上RTOS更容易理解底层逻辑。3.2 DS1302驱动时序与BCD码处理DS1302的驱动是这个项目最核心的部分三线串口通信看着简单时序处理不好就会读出全0或全FF。通信过程大致是先拉高CE片选然后通过SCLK的上升沿逐位写入命令字节和数据字节读取时则是在SCLK下降沿之后采样数据线。#define DS1302_CE P1_0 #define DS1302_IO P1_1 #define DS1302_CLK P1_2 void DS1302_WriteByte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { DS1302_IO dat 0x01; _nop_(); DS1302_CLK 1; _nop_(); DS1302_CLK 0; dat 1; } } unsigned char DS1302_ReadByte(void) { unsigned char i, dat 0; DS1302_IO 1; // 释放数据线让DS1302驱动 for (i 0; i 8; i) { DS1302_CLK 1; _nop_(); DS1302_CLK 0; _nop_(); dat 1; if (DS1302_IO) dat | 0x80; } return dat; } unsigned char DS1302_ReadReg(unsigned char addr) { unsigned char dat; DS1302_CE 0; DS1302_CLK 0; DS1302_CE 1; DS1302_WriteByte(addr); // 读命令bit0为0 dat DS1302_ReadByte(); DS1302_CE 0; return dat; } void DS1302_WriteReg(unsigned char addr, unsigned char dat) { DS1302_CE 0; DS1302_CLK 0; DS1302_CE 1; DS1302_WriteByte(addr); // 写命令bit0为1 DS1302_WriteByte(dat); DS1302_CE 0; }这里要特别提一下读时序。DS1302的手册描述是数据在SCLK下降沿之后由从机输出主机在下降沿后采样比较稳妥。但实际工作中我发现不同的代码写法都能跑有的先行采样再给时钟有的给完时钟再采样差别主要在于时序裕量。如果你写出来的代码读出的时间全是0xFF或者0x00不要急着怀疑芯片坏了先把读字节函数改成“给完一个完整时钟脉冲之后在读数据线”的顺序大概率能解决。这个经验我在调试时踩过也帮朋友排查过同样的问题。DS1302的时间寄存器格式是BCD码也就是一个字节的高四位表示十位、低四位表示个位。比如秒寄存器读出来的0x45代表45秒而不是十进制45。所以需要做转换#define BCD_TO_DEC(x) ((((x) 4) * 10) ((x) 0x0F)) #define DEC_TO_BCD(x) ((((x) / 10) 4) | ((x) % 10))另外要注意DS1302的秒寄存器最高位是CH标志位为1时芯片停止振荡。初始化时一定要把这个位清0否则时间根本不会走。写保护寄存器是0x8E写任何时间数据之前要先关写保护写完再打开防止误写。还有涓流充电寄存器0x90如果是普通纽扣电池就不要启用充电功能否则可能对电池造成损伤初始化时直接写0x00关掉就好。3.3 闰年与日期计算虽然DS1302内部会自己计算月日但你在做按键校时、日期合法性校验或者未来扩展闹钟功能时还是必须自己写一套日期算法。很多人的万年历项目会在设置界面允许用户把日期改成2月30日这就是没有做合法性限制的结果。const unsigned char MonthDays[12] {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; unsigned char IsLeapYear(unsigned int year) { if ((year % 4 0 year % 100 ! 0) || (year % 400 0)) return 1; return 0; } unsigned char GetMonthDays(unsigned int year, unsigned char month) { if (month 2 IsLeapYear(year)) return 29; return MonthDays[month - 1]; }闰年判断的逻辑是“能被4整除但不能被100整除或者能被400整除”。这句很多人背得出来但要注意DS1302的年寄存器只有两位范围是00到99。也就是它只能直接表示2000年到2099年。我在程序里读出来之后统一加上2000得到实际年份这样2025年对应25闰年判断也按2000加实际值来算避免像2100年这种特殊年份的判断错误。另外星期寄存器在DS1302里是1到7分别对应周日到周六。如果你更习惯“周一是一周的第一天”显示前要做一次映射转换。我自己是直接按DS1302的返回值显示星期几只是在打印的时候把1对应成“Sun”避免额外增加一层换算逻辑。3.4 按键调整时间的状态机设计按键菜单用状态机实现是我在这个项目里觉得最顺手的一部分。整体思路是把界面状态分成正常显示、设置年、设置月、设置日、设置时、设置分这几个状态按键操作驱动状态切换typedef enum { STATE_NORMAL, STATE_SET_YEAR, STATE_SET_MONTH, STATE_SET_DAY, STATE_SET_HOUR, STATE_SET_MIN } MenuState;短按设置键可以从正常显示切换到设置年之后每按一次进入下一个字段设置完分钟后再按一次回到正常显示并写入DS1302。加键和减键在当前状态下对对应的字段做加减同时调用合法性判断比如当前是2月且年份不是闰年时日期最大值自动限制为28。按键处理要特别注意两个问题。一是消抖我采用10毫秒扫描一次、连续两次读到相同电平才确认状态变化的软件消抖方式实测下来非常稳定。二是长按连续触发如果用户想从2025年调整到2030年一下一下按太痛苦了。我做了长按0.5秒后每150毫秒自动加一的效果这样既不会误触又能快速调整。写入DS1302时最低位的秒寄存器会触发CH标志位相关操作建议在校时结束的统一时刻把时间一次性写入所有寄存器而不是在设置完年就立刻写一次、设置完月又写一次碎片化写入容易造成数据不一致。我是在“设置分”完成后统一写入写入前关闭写保护写完立即打开。4. 调试记录那些坑我替你踩过了4.1 DS1302晶振不起振秒不走这是我遇到最多的问题也是最多人私信问我的问题。现象很统一代码逻辑看起来完全没问题可读出秒数一直不变。排查思路大概分三步。第一步测硬件用示波器或者万用表频率档测DS1302的X1脚有没有32.768kHz振荡信号。没示波器的话可以直接读秒寄存器看个位数有没有变化。第二步检查晶振型号确认负载电容是6pF而不是12.5pF。第三步检视焊点X1和X2引脚相邻助焊剂残留或者锡桥都会导致停振。有一次我怎么查都正常最后发现是晶振引脚虚焊补了一烙铁就好了。晶振旁边最好不要铺铜寄生电容会影响起振。4.2 掉电之后时间归零掉电时间丢失先说硬件原因VCC1电池没接对。要么是纽扣电池装反要么是1N4148二极管接反要么是电池没电了。排除这些之后还有一种很隐蔽的软件原因——初始化代码把时间写死了。很多教程的示例代码会在初始化里直接调用DS1302_WriteReg写入当前时间如果你照抄了就会导致每次上电都把你设置的固定时间覆盖掉电池走时期间更新过的时间全部白搭。正确做法是上电先读一次时间判断一下有效性比如年寄存器读出来是0或者255才认为是初始上电需要写入默认时间如果读出来是正常年份就跳过写入直接进入显示流程。这样断电重启后时间才能保持在最近一次校时的状态。4.3 走时误差的软件校准DS1302的走时精度完全取决于外部那颗32.768kHz晶振温度变化、晶振本身频率偏差都会带来累积误差一个月差一两分钟是正常现象。如果不想换更高精度的DS3231可以像我一样在软件里做补偿。思路很简单先解决走快还是走慢的问题。和手机时间对时让板子连续跑24小时记录误差秒数。比如实测一天快了5秒那就在程序里每天固定时刻比如凌晨0点0分0秒把每秒数回拨5秒如果慢了3秒就每天晚上加3秒。写入秒值时务必保持CH位为0千万不要往秒寄存器的高位写1否则时钟会当场停掉。这种补偿是开环的不能完全消除误差但能把月误差控制在一分钟内对桌面万年历来说完全够用。4.4 LCD1602乱码与按键跳变LCD1602上电后如果直接初始化经常会出现满屏黑块或者第一行乱码。原因是1602内部控制器需要在上电后等待一段时间才能稳定最短也要等40毫秒以上电压不稳的时候甚至要100毫秒。我习惯在初始化函数最开始加一个至少50毫秒的延时然后再发初始化指令。对比度电位器也要调整到合适位置否则即使代码对显示也会一团糊。按键跳变的问题多数出在消抖和上拉。我调试时发现如果不做连续两次采样确认按键按下的一瞬间会触发两三次操作。除了软件消抖建议在按键引脚加10k上拉电阻缩短到GND的路径能显著减少外部干扰引起的误触发。另外STC15的IO口要配置成准双向口模式不能是高阻输入否则按键引脚在悬空时会读到随机电平。最后分享一点我自己的做事习惯万年历这类项目做完后别急着拆把它放在桌面跑一个月。这一个月里你会直观感受到RTC芯片的长期漂移、按键手感和显示效果。很多芯片数据手册上冷冰冰的参数都会在长时间运行后变成真正属于你的经验。对我个人来说这个基于STC15的万年历项目最大的收获不是代码本身而是把“接口时序”“前后台任务调度”“数据有效性判断”这几个基础概念彻底夯实了。如果你也想动手做一个我建议从一片STC15核心板加一块DS1302模块开始一个晚上就能点亮第一屏时间之后想扩展闹钟、温度显示、红外遥控都有现成的路子可以走。本文还有配套的精品资源点击获取