简介:这份资源是一套基于DS1302时钟芯片的万年历课程设计完整方案,面向正在做单片机课程设计或电子类毕业设计的学生,尤其适合需要51单片机实战案例的初学者。方案以1602液晶屏作为显示界面,DS1302提供时间基准,通过三个按键实现日期与时间的加减调整,功能完整、逻辑清晰。压缩包共4个文件,约22KB,包含2个h头文件、1个c源程序和1个bmp图片,分别用于模块声明、主程序实现与仿真截图展示,并附有Proteus仿真效果,程序采用C语言编写,可直接编译使用。目前已有296人学习下载,说明该方案在同类课程设计中具有一定参考价值。读者可从中获得完整的时钟电路设计思路、DS1302与1602的驱动代码、按键调整逻辑以及仿真验证方法,适合作为课程设计模板或二次开发的基础,帮助快速搭建可运行的万年历系统。
1. 从一块掉电就归零的时钟说起:这个万年历项目到底能干什么
如果你玩过 51 单片机,大概率踩过同一个坑:用定时器做出来的电子钟,一断电时间就回到 2000 年 1 月 1 日,第二天上电还得重新对时。我最早做课程设计那会儿就栽在这上面,后来才老老实实上了 DS1302 这颗实时时钟芯片。这份「基于 DS1302 的万年历」资源,解决的就是这个最朴素的痛点——让单片机系统在没有网络、没有上位机对时的前提下,靠一颗纽扣电池把年月日时分秒、星期、闰年判断全部记住,掉电不丢。
它适合两类人:一类是刚学完 51 单片机、想找一个「外设驱动 + 人机交互 + 完整逻辑」都齐全的练手项目的同学;另一类是手上有现成硬件、想直接拿一份能跑通的代码改造成自己产品的开发者。整个项目围绕 DS1302 三线串行接口、LCD1602 或数码管显示、独立按键调时这三块展开,麻雀虽小,但把单总线时序、BCD 码转换、按键消抖、闰年算法这些嵌入式基本功全串了一遍。下面我就按「它是什么 → 怎么跑起来 → 怎么改」的顺序,把这份资源拆开讲。
2. 硬件架构与目录结构:先看清 DS1302 到底挂在哪几条线上
2.1 系统整体框图与器件分工
这个万年历的硬件结构非常典型,属于「一颗主控 + 一颗时钟芯片 + 一块显示 + 几个按键」的最小系统。主控一般是 STC89C52 或 AT89S52 这类经典 51 内核单片机,负责轮询 DS1302 读时间、驱动显示、扫描按键。DS1302 通过三根线与主控相连:CE(也叫 RST,复位/片选)、I/O(数据线)、SCLK(串行时钟)。注意这三根线不是标准 I2C,也不是 SPI,而是 DS1302 自己的一套三线同步串行协议,时序要求比较死,后面代码里会重点讲。
显示部分常见两种方案:LCD1602 字符屏,或者 4 位/6 位共阳数码管。前者能直接显示「2024-05-20 13:45」这种完整字符串,后者更省成本但要自己写动态扫描。按键一般配 3 个:模式键、加键、减键,用来进入调时状态、修改当前选中的时间位。电源部分,DS1302 的 VCC1 接纽扣电池(CR2032),VCC2 接系统 5V,芯片内部会自动在主电源掉电时切到电池供电,这就是「掉电不丢时间」的物理基础。
| 器件 | 型号 | 作用 | 关键引脚 |
|---|---|---|---|
| 主控 | STC89C52 | 主逻辑、显示驱动、按键扫描 | P1/P2/P3 通用 IO |
| 时钟 | DS1302 | 走时、掉电保持 | CE、I/O、SCLK |
| 显示 | LCD1602 | 显示年月日时分秒 | RS、RW、E、D0-D7 |
| 按键 | 独立按键 x3 | 调时、切换、加减 | 接 IO 并下拉 |
2.2 源码目录与文件职责
拿到这份资源后,目录结构通常是这样组织的(不同整理版本可能略有差异,但职责划分一致):
DS1302_Calendar/ ├── main.c // 主循环:读时间、刷显示、扫按键 ├── ds1302.c // DS1302 底层驱动:写字节、读字节、寄存器读写 ├── ds1302.h // 引脚宏定义、寄存器地址宏、函数声明 ├── lcd1602.c // LCD1602 驱动:初始化、写命令、写数据、显示字符串 ├── lcd1602.h ├── key.c // 按键扫描与消抖 ├── key.h └── delay.c // 微秒/毫秒级延时,DS1302 时序依赖它这个划分方式的好处是「驱动和业务分离」:ds1302.c只关心怎么按协议把字节送出去、读回来,不关心读的是秒还是年;main.c只关心业务逻辑,比如「现在是调时模式,加键按下,秒加一」。你二次开发时,改显示换lcd1602.c,改交互改key.c,基本不会互相牵连。我一般拿到这种项目,第一件事就是看头文件里的宏定义,因为引脚接法和寄存器地址全在那里,改硬件时只动头文件就够了。
2.3 DS1302 寄存器地址与 BCD 码约定
DS1302 内部有一片 RAM 和一组时间寄存器,写命令时地址的最低位是读写标志:0 表示写,1 表示读。比如秒寄存器写地址是0x80,读地址就是0x81。这一点如果没搞清楚,读出来的数据永远是乱的,属于新手最容易翻车的地方。
// ds1302.h 中常见的寄存器地址宏定义 #define DS1302_SEC_WRITE 0x80 // 秒寄存器,写 #define DS1302_SEC_READ 0x81 // 秒寄存器,读 #define DS1302_MIN_WRITE 0x82 #define DS1302_MIN_READ 0x83 #define DS1302_HOUR_WRITE 0x84 #define DS1302_HOUR_READ 0x85 #define DS1302_DATE_WRITE 0x86 #define DS1302_DATE_READ 0x87 #define DS1302_MONTH_WRITE 0x88 #define DS1302_MONTH_READ 0x89 #define DS1302_WEEK_WRITE 0x8A #define DS1302_WEEK_READ 0x8B #define DS1302_YEAR_WRITE 0x8C #define DS1302_YEAR_READ 0x8D #define DS1302_WP_WRITE 0x8E // 写保护寄存器参数说明:每个宏后面的十六进制值就是命令字节,最低位区分读写。DS1302_WP_WRITE是写保护寄存器,往里面写0x00才能修改时间,写0x80则锁定,防止误写。DS1302 里存的时间数据是 BCD 码格式,比如「23 分」存的是0x23而不是十进制的 23,所以读写时都要做 BCD 和十进制之间的转换,这个转换函数后面会单独讲。
3. 核心驱动拆解:三线时序、BCD 转换和主循环怎么配合
3.1 DS1302 写字节与读字节的时序实现
DS1302 的通信是「先发命令字节,再收发数据字节」,全部靠软件模拟时序完成。写字节时数据在 SCLK 上升沿被芯片采样,所以要先准备好 I/O 电平,再把 SCLK 拉高。读字节则相反,在 SCLK 下降沿之后芯片把数据放到 I/O 上,主控去读。
// 向 DS1302 写入一个字节,低位先发 void DS1302_WriteByte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { DS1302_IO = dat & 0x01; // 先送最低位 DS1302_SCLK = 1; // 上升沿,芯片采样 DS1302_SCLK = 0; // 拉低,准备下一位 dat >>= 1; // 数据右移 } } // 从 DS1302 读出一个字节,低位先收 unsigned char DS1302_ReadByte(void) { unsigned char i, dat = 0; for (i = 0; i < 8; i++) { dat >>= 1; // 先右移,给新位腾位置 if (DS1302_IO) { dat |= 0x80; // 读到 1,放到最高位 } DS1302_SCLK = 1; DS1302_SCLK = 0; // 下降沿后芯片输出下一位 } return dat; }逻辑说明:写字节时dat & 0x01取最低位送到 I/O,然后 SCLK 产生一个上升沿让 DS1302 采样,循环 8 次正好发完一个字节。读字节时先把dat右移一位,再判断 I/O 电平,如果是高就把最高位置 1,这样 8 次循环后拼出来的字节顺序才是对的。参数上唯一要注意的是DS1302_IO、DS1302_SCLK这些宏必须和实际接线一致,改板子时只改头文件里的sbit定义。
3.2 时间读写与 BCD 码转换
有了读写字节的底层函数,读写时间寄存器就是「发命令 + 收发数据」的组合。写之前要先关写保护,写完再打开,这是很多人第一次调不通的原因——忘了关 WP,数据写不进去。
// 写一个寄存器 void DS1302_WriteReg(unsigned char cmd, unsigned char dat) { DS1302_CE = 0; DS1302_SCLK = 0; DS1302_CE = 1; // 拉高 CE 启动一次传输 DS1302_WriteByte(cmd); // 先发命令字节 DS1302_WriteByte(dat); // 再发数据字节 DS1302_CE = 0; // 拉低结束 } // 读一个寄存器 unsigned char DS1302_ReadReg(unsigned char cmd) { unsigned char dat; DS1302_CE = 0; DS1302_SCLK = 0; DS1302_CE = 1; DS1302_WriteByte(cmd); // 发读命令 dat = DS1302_ReadByte(); // 收数据 DS1302_CE = 0; return dat; } // BCD 转十进制 unsigned char BCD_To_Dec(unsigned char bcd) { return (bcd >> 4) * 10 + (bcd & 0x0F); } // 十进制转 BCD unsigned char Dec_To_BCD(unsigned char dec) { return ((dec / 10) << 4) | (dec % 10); }逻辑说明:DS1302_WriteReg和DS1302_ReadReg都遵循「CE 拉高开始、CE 拉低结束」的框架,中间先发命令再收发数据。BCD 转换里,高四位是十位、低四位是个位,所以(bcd >> 4) * 10 + (bcd & 0x0F)就能还原成十进制。写时间前记得先DS1302_WriteReg(DS1302_WP_WRITE, 0x00)解除保护,写完再写回0x80。
3.3 主循环:读时间、刷显示、扫按键
主循环是整个项目的调度中心,逻辑不复杂但顺序有讲究:先读时间,再根据当前模式决定显示什么,最后扫按键处理输入。
void main(void) { unsigned char time[7]; // 秒分时日月周年 LCD1602_Init(); DS1302_Init(); // 首次上电写入初始时间 while (1) { // 1. 连续读 7 个时间寄存器 time[0] = DS1302_ReadReg(DS1302_SEC_READ) & 0x7F; // 去掉最高位时钟停止位 time[1] = DS1302_ReadReg(DS1302_MIN_READ); time[2] = DS1302_ReadReg(DS1302_HOUR_READ) & 0x3F; time[3] = DS1302_ReadReg(DS1302_DATE_READ); time[4] = DS1302_ReadReg(DS1302_MONTH_READ); time[5] = DS1302_ReadReg(DS1302_WEEK_READ); time[6] = DS1302_ReadReg(DS1302_YEAR_READ); // 2. 显示或调时 if (mode == 0) { Display_Time(time); // 正常走时显示 } else { Display_Set(time); // 调时模式,闪烁当前位 } // 3. 按键扫描 Key_Scan(&mode, time); } }逻辑说明:读秒寄存器时& 0x7F是为了屏蔽最高位,那一位是 CH(Clock Halt)时钟停止位,上电默认可能是 1,不清掉的话时钟不走,这是很隐蔽的一个坑。读小时& 0x3F是屏蔽 12/24 小时制标志位。mode变量控制当前是走时还是调时,Key_Scan里根据按键修改time数组并写回 DS1302。整个循环没有阻塞式长延时,保证按键响应和显示刷新都跟得上。
4. 编译烧录与上电验证:从源码到能走时的完整步骤
4.1 开发环境与工程配置
这份资源是标准 C 语言 51 工程,用 Keil uVision(C51 编译器)打开即可。如果你手上是 STC 系列芯片,烧录用 STC-ISP;如果是 AT89S52,用对应的并口或 USB 编程器。新建工程时注意两点:一是芯片型号要选对,STC89C52 和 AT89C52 的 RAM 大小不同;二是晶振频率要填对,常见 11.0592MHz 或 12MHz,延时函数是按这个算的,填错会导致 DS1302 时序偏差。
# 典型编译烧录流程(以 STC-ISP 为例) 1. Keil 中 Project -> Build Target,确认 0 Error 0 Warning 2. 打开 STC-ISP,选择对应 COM 口和芯片型号 3. 加载生成的 .hex 文件 4. 点击"下载",然后给单片机上电复位参数说明:.hex文件在 Keil 工程目录的Objects文件夹下。STC-ISP 下载时需要先点下载再上电,是因为 STC 芯片上电时会先检测串口是否有下载命令。如果一直提示「正在检测单片机」,先检查串口驱动和 COM 口是否选对。
4.2 上电后的验证清单
烧录完成后,按下面顺序验证,能快速定位问题出在哪一层:
- 上电后 LCD1602 是否亮、是否显示初始时间。不亮先查背光和对比度电位器。
- 时间是否在走。不走先查 DS1302 的 CH 位有没有清、晶振有没有起振(32.768kHz)。
- 断电再上电,时间是否保持。不保持查纽扣电池电压和 VCC1 接线。
- 按键能否进入调时、加减是否生效。不生效查按键消抖和 IO 定义。
提示:DS1302 必须外接 32.768kHz 晶振,且晶振两端要各配一个 6pF 左右的负载电容,缺了或容值不对,走时要么不走要么偏得离谱。
4.3 首次上电初始化时间
DS1302 第一次上电时寄存器内容是随机的,所以代码里一般会有一个DS1302_Init函数,在检测到特定条件(比如某个 RAM 标志位没写)时写入一个默认时间。
void DS1302_Init(void) { unsigned char flag = DS1302_ReadReg(0xC1); // 读 RAM 里的标志 if (flag != 0x5A) { // 首次上电 DS1302_WriteReg(DS1302_WP_WRITE, 0x00); // 关写保护 DS1302_WriteReg(DS1302_SEC_WRITE, 0x00); DS1302_WriteReg(DS1302_MIN_WRITE, Dec_To_BCD(30)); DS1302_WriteReg(DS1302_HOUR_WRITE, Dec_To_BCD(12)); DS1302_WriteReg(DS1302_DATE_WRITE, Dec_To_BCD(1)); DS1302_WriteReg(DS1302_MONTH_WRITE, Dec_To_BCD(1)); DS1302_WriteReg(DS1302_YEAR_WRITE, Dec_To_BCD(24)); DS1302_WriteReg(DS1302_WP_WRITE, 0x80); // 开写保护 DS1302_WriteReg(0xC0, 0x5A); // 写标志,下次不再初始化 } }逻辑说明:用 DS1302 内部 RAM 的一个字节当「已初始化」标志,只有第一次上电才写默认时间,之后每次上电都跳过,这样就不会把用户调好的时间覆盖掉。这个技巧在实际产品里很常用,属于「后悔药」式的设计——避免每次上电都重置。
5. 避坑与排查:DS1302 万年历最常见的 5 个翻车现场
5.1 时间不走或走一下就停
现象:上电后显示正常,但秒数不动。原因:秒寄存器最高位 CH 位为 1,芯片处于时钟停止状态。解决:读秒时& 0x7F,或者在初始化时明确写0x00到秒寄存器。这个坑我第一次调的时候卡了整整一晚上,后来才发现是 CH 位没清。
5.2 读出来的时间全是乱码或 0xFF
现象:显示的数字明显不对,或者全是 15。原因:三线时序不对,最常见的是 SCLK 极性搞反,或者 CE 没有在传输前后正确拉高拉低。解决:用示波器看 SCLK 和 I/O 波形,确认写字节时数据在上升沿前稳定;没有示波器就逐行核对DS1302_WriteByte里的电平顺序。
5.3 断电后时间丢失
现象:一断电再上电就回到初始时间。原因:纽扣电池没装、装反,或者 VCC1 没接电池、VCC2 和 VCC1 接反。解决:万用表量 VCC1 对地电压,正常应在 3V 左右;确认电池正极接 VCC1。另外注意有些模块把电池座和芯片做在一起,要检查焊点。
5.4 走时偏快或偏慢
现象:一天差几分钟甚至几十分钟。原因:32.768kHz 晶振负载电容不匹配,或者晶振质量差。解决:换 6pF 负载电容的晶振,两端电容按晶振规格调整;软件上也可以在秒中断里做微调,但硬件问题优先硬件解决。
5.5 按键调时后写不进去
现象:调时界面数字能变,退出后时间没变。原因:写 DS1302 前忘了关写保护寄存器 WP。解决:写时间前DS1302_WriteReg(DS1302_WP_WRITE, 0x00),写完再写0x80。这个和 5.1 一样,属于 DS1302 特有的「必须做但容易忘」的操作。
6. 二次开发改造:从能走到好用,我一般会动这几处
把基础功能跑通只是第一步,真正拿去做东西,还得按需求改。我拆这类项目时,改造通常从三个方向入手,你可以按自己的场景挑。
第一个方向是显示升级。LCD1602 只能显示两行 16 字符,信息量有限。如果想显示农历、温度、闹钟,建议换成 OLED(SSD1306)或者 12864 液晶。改造时只需要替换lcd1602.c里的底层写命令/写数据函数,上层Display_Time的格式化逻辑基本不用动。我一般会先把显示内容抽象成一个char buf[17],用sprintf拼好再统一送屏,这样换屏时改动最小。
第二个方向是加闹钟和整点报时。DS1302 本身有闹钟寄存器(地址0x90起),但很多人不用它,而是在主循环里比较当前时分和设定值。两种做法各有取舍:用芯片闹钟能配合中断,省 CPU;用软件比较则灵活,想加几组加几组。下面是一个软件比较的写法:
// 在主循环里判断是否到闹钟时间 if (BCD_To_Dec(time[2]) == alarm_hour && BCD_To_Dec(time[1]) == alarm_min && BCD_To_Dec(time[0]) == 0) { // 秒为 0 时触发一次 Buzzer_On(); Delay_ms(500); Buzzer_Off(); }参数说明:time[2]是小时、time[1]是分钟、time[0]是秒,比较前都要先转成十进制。判断秒为 0 是为了避免一分钟内重复触发。蜂鸣器接一个 IO 口,用三极管驱动,别直接拿 IO 拉,电流不够。
第三个方向是掉电检测与数据保存。如果你还想存一些自定义参数(比如闹钟时间、屏幕亮度),可以写进 DS1302 的 RAM 区(地址0xC0往后),它同样有电池保持,比外挂 EEPROM 省事。我一般会封装两个函数Save_Param和Load_Param,把参数结构体按字节写进 RAM,上电时读出来校验一下。
验证改造是否成功,我的习惯是「三步走」:先看编译有没有警告,再看上电初始状态对不对,最后做一次完整的断电重启测试。尤其是涉及写 DS1302 的改动,每次都要确认写保护有没有正确开关,不然很容易出现「调的时候好好的,一断电全没了」这种玄学问题。从那以后我每次改完时间相关的代码,都强制走一遍「调时 → 断电 → 上电 → 核对」的流程,再也不敢省这一步。希望这份拆解能帮你少走点弯路,把这块小板子玩出更多花样。
本文还有配套的精品资源,点击获取