简介这是一份基于DS1302实时时钟芯片的可调闹钟仿真项目适用群体是单片机入门者和嵌入式实训学生核心目标是帮助理解实时时钟芯片的三线串行接口通信、按键设置时间与闹钟、液晶屏动态刷新等关键环节。整个压缩包约78KB共包含20个文件主要类型有C语言源文件、头文件、Proteus原理图工程、Keil开发环境工程配置、Hex烧录文件、文本说明文档以及若干编译中间文件目录结构完整清晰方便对照源码与仿真同步联调。目前已有263人浏览学习项目支持直接修改时间、独立设置闹钟并在仿真环境中实时反映时间走时、闰年自动校正以及十二小时与二十四小时制切换。借助工程内的源码和“闹钟设定方法”说明可以快速掌握芯片初始化流程、读写时序、按键消抖处理与液晶显示刷新思路适合动手实践并二次开发是一份兼顾原理与实操的实时时钟仿真参考资料。1. 用 Proteus 把 DS1302 可调闹钟仿真跑通先解决“时间不动”和“按键无效”下载一个名为 DS1302.rar 的 Proteus 工程双击打开后最常见的两个现象是LCD 上的时间一动不动或者按键按了没反应。多数人以为是 51 代码的问题实际上这是 DS1302 在 Proteus 仿真里特有的“三连坑”——晶振频率没设对、秒寄存器最高位没有清 0、以及 DS1302 本身并不带闹钟中断引脚闹钟必须要靠单片机程序去比较触发。下面就从寄存器读写、按键状态机、闹钟触发到虚拟示波器验证把这条链路一次讲完。适合正在做单片机课程设计、想把手里的 DS1302 仿真工程改成“时间可调、闹钟可设”的人。2. 先吃透 DS1302 时序再谈 Proteus 里的可调闹钟仿真2.1 DS1302 寄存器映射与 BCD 编码DS1302 内部有 12 个时钟寄存器和 31 个 RAM 单元时钟部分以 BCD 码保存时间数据。每个寄存器都有独立的读地址和写地址命令字最高位固定为 1中间 5 位是寄存器地址最低位是读写方向1 为读、0 为写。所以写秒命令是 0x80读秒命令是 0x81。时间寄存器映射如下功能写命令读命令BCD 范围说明秒0x800x8100-59bit7 是 CH 位1 表示振荡器暂停分0x820x8300-59时0x840x8500-23bit7 为 12/24 小时制选择日0x860x8701-31依月份与闰年自动修正月0x880x8901-12周0x8A0x8B01-07任意指定哪一天为 1年0x8C0x8D00-99以 2000 年为基准写保护0x8E0x8F0x00/0x80bit71 时禁止写所有寄存器可调闹钟仿真里最常用的是一次性写入 8 个字节的突发模式写命令 0xBE、读命令 0xBF从秒寄存器开始连续读写到写保护寄存器。相比逐字节设置时间突发模式写一次就能完成初始化代码也短。BCD 码是这颗芯片最容易踩的坑。0x15 代表十进制 15而不是二进制 21。很多人在调试时看到 LCD 显示 0F 或时间乱跳原因是直接把 BCD 当十进制显示了。转换函数记牢十进制转 BCD 是(dec / 10) 4 | (dec % 10)BCD 转十进制是(bcd 4) * 10 (bcd 0x0F)。2.2 从单字节读写到突发模式的 C51 驱动DS1302 与单片机之间只有三根线CE也叫 RST、SCLK、I/O。CE 拉高后开始一次通信SCLK 上升沿时 DS1302 锁存 I/O 上的数据位SCLK 下降沿时 DS1302 在 I/O 上输出数据。命令字节和数据字节都是低位在前。// 使用 AT89C51 的 P2.0-P2.2 与 DS1302 连接 sbit DS1302_SCLK P2^0; sbit DS1302_IO P2^1; sbit DS1302_RST P2^2; // 向 DS1302 写入一个字节SCLK 上升沿锁存LSB 在前 void DS1302_WriteByte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { DS1302_IO dat 0x01; // 先送最低位 DS1302_SCLK 1; // 上升沿DS1302 采样 I/O DS1302_SCLK 0; dat 1; } } // 从 DS1302 读取一个字节在低电平期间读上升沿切换下一位 unsigned char DS1302_ReadByte(void) { unsigned char i, dat 0; for (i 0; i 8; i) { dat 1; DS1302_SCLK 0; // 给 DS1302 输出稳定窗口 if (DS1302_IO) dat | 0x80; DS1302_SCLK 1; // 上升沿后 DS1302 切换下一位 } return dat; } // 读取时间寄存器reg 为完整读命令如读秒用 0x81 unsigned char DS1302_ReadTime(unsigned char reg) { unsigned char dat; DS1302_RST 0; DS1302_SCLK 0; DS1302_RST 1; DS1302_WriteByte(reg); dat DS1302_ReadByte(); DS1302_RST 0; return dat; }提示读字节时先采样再拉高写字节时先置数据再拉高这两条对应的是 DS1302 手册里“输入上升沿锁存、输出下降沿切换”的行为。不要为了对称把读和写写成同一套循环。读字节时我刻意在SCLK0后再读 I/O而不是先拉高再读原因是 DS1302 的数据输出发生在下降沿附近低电平期间数据已经稳定此刻采样最安全。先读再拉高上升沿恰好让 DS1302 在上升沿切换到下一位。如果你把顺序写反Proteus 仿真里可能偶尔能读对但换到真实芯片或换了仿真速度后就会出乱码。DS1302_ReadTime的参数是完整命令字节比如读秒要传 0x81而不是传地址 0。这一点和很多库函数不一样注释里标清楚避免后面在可调闹钟仿真里把寄存器读串。突发写 8 个时间字节也是初始化时间的关键路径// 突发模式设置时间参数为 BCD 码数组 // 顺序固定秒、分、时、日、月、周、年、写保护 unsigned char code InitTime[8] {0x50, 0x59, 0x23, 0x01, 0x06, 0x05, 0x24, 0x00}; // 当前时间23:59:506月1日周五2024年最后一个字节写保护0x00 表示关闭 void DS1302_Init(void) { unsigned char i; DS1302_RST 0; DS1302_SCLK 0; // 先单独关闭写保护再写时间 DS1302_RST 1; DS1302_WriteByte(0x8E); DS1302_WriteByte(0x00); DS1302_RST 0; // 突发写 8 个字节 DS1302_RST 1; DS1302_WriteByte(0xBE); for (i 0; i 8; i) { DS1302_WriteByte(InitTime[i]); } DS1302_RST 0; }InitTime 数组里的 0x50 是 BCD 码代表 50 秒而不是 80。可调闹钟仿真中用户按加号把秒加到 59 后要变成 0x00 并进位所以后面状态机里所有时间运算都建议直接用十进制做写回芯片前再转 BCD。不要在 BCD 上直接做否则会出现 0x0A 这种非法 BCD 值。2.3 Proteus 8 最小工程连线与晶振设置新建 Proteus 工程后放上 AT89C51、DS1302、LCD1602、CRYSTAL、两个 22pF 电容、一个 10k 电位器LCD 对比度。常见连线方式DS1302 的 VCC1 接 3.6V 纽扣电池模型或直接接 5VVCC2 接 5V 主电源GND 接地SCLK、I/O、RST 分别接 P2.0、P2.1、P2.2晶振接在 X1 和 X2 之间两个电容一端接晶振脚、另一端接地。Proteus 里 CRYSTAL 元件的默认频率通常是 1MHz必须双击改写成 32.768kHz。很多人把 DS1302 放进来就走了结果仿真时间一秒都不走。改完之后可以在晶振输出脚挂虚拟示波器观察是否有 32.768kHz 波形。另一个容易踩的坑是 I/O 引脚属于漏极开路结构Proteus 仿真中如果单片机端口对上拉不明确读回来的数据可能全 0。稳妥做法是在 I/O 与 VCC 之间加一个 4.7k 上拉电阻SCLK 和 RST 也可以加。出现“能写不能读、LCD 时间不变”时第一个查的就是这个上拉。3. 按键可调的状态机设计与 DS1302 写回3.1 四个按键建立“时钟-闹钟”调整状态机可调仿真的核心在交互。用 4 个独立按键接到 P3.0-P3.3定义如下按键引脚功能KEY_MODEP3.0切换调整对象正常→时钟时分→闹钟时分→正常KEY_UPP3.1当前选中项加 1KEY_DOWNP3.2当前选中项减 1KEY_OKP3.3确认设定并退出调整顺便关闭正在响的闹钟代码里用枚举定义状态比到处 if 嵌套清晰得多typedef enum { RUN_MODE, // 正常运行显示时间 SET_TIME_HOUR, // 调整时钟-小时 SET_TIME_MIN, // 调整时钟-分钟 SET_ALARM_HOUR, // 调整闹钟-小时 SET_ALARM_MIN // 调整闹钟-分钟 } SysState; SysState state RUN_MODE; unsigned char alarm_hour 0x07; // 闹钟默认 07:00BCD unsigned char alarm_min 0x30;每次按键按下时只做两件事根据当前状态切换到下一个状态或者对当前对象做加减。加 1 和减 1 做成一个函数同时处理回绕进位void IncDecAdjust(unsigned char *value, unsigned char min, unsigned char max, bit is_inc) { unsigned char dec; dec ((*value 4) 0x0F) * 10 (*value 0x0F); // BCD - DEC if (is_inc) { dec; if (dec max) dec min; } else { if (dec min) dec max; else dec--; } *value ((dec / 10) 4) | (dec % 10); // DEC - BCD }因为是 BCD 码value指向的对象需要在加减前先转成十进制算完再转回 BCD。max和min分别传入 0/23 或 0/59就能同时处理小时和分钟的边界。注意分加满后不自动进位小时这是闹钟设定的常见设计设置闹钟分钟到 59 再加 1回到 0而不是进到小时时钟在正常运行中才由 DS1302 自己进位。3.2 按键扫描、消抖与 Proteus 仿真节奏Proteus 仿真中按键默认带机械弹跳若代码里不消抖按下一次可能被识别成多下时间会跳好几个数。用 2ms 定时器扫描键盘连续读到稳定电平约 250 次约 250ms才认为按下一次同时记录上一次按键状态只在“从没按下到按下”的边沿触发一次unsigned char KeyScan(void) { unsigned char key 0; if (KEY_MODE 0) key 0x01; if (KEY_UP 0) key 0x02; if (KEY_DOWN 0) key 0x04; if (KEY_OK 0) key 0x08; return key; } static unsigned char last_key 0, stable_cnt 0; unsigned char GetSingleKey(void) { unsigned char now, ret 0; now KeyScan(); if (now last_key) { if (stable_cnt 250) stable_cnt; if (stable_cnt 250) ret now; // 稳定后的首次 } else { last_key now; stable_cnt 0; } if (stable_cnt 250) ret 0; // 长按不重复触发 return ret; }GetSingleKey 在按下一次并稳定后的第一次调用返回键值之后 stable_cnt 保持大于 250函数返回 0直到松开后电平变化才允许下一次。这样长按不会重复触发Proteus 里演示效果更接近真实。实际工程如果需要长按连加要在按键稳定后另加一个 500ms 重复定时器。这里有一个仿真专属的参数注意点Proteus 的默认仿真速度会尽量快但 AT89C51 用的是指令周期2ms 定时器在仿真里仍然是真实的软件时间和真实芯片一致。不要为了看效果把 Animation Frame Rate 调得太低否则按键扫描被画面刷新拖慢会出现“按了没反应”。3.3 调整后写回 DS1302闹钟值存哪里时钟小时和分钟调整完成后需要把新值写回 DS1302。写回时有两件事必须同时做关闭写保护寄存器以及在写秒寄存器时清除秒的最高位 CH。CH 位是时钟暂定标志如果它为 1秒寄存器不会递增时间就会停住。void DS1302_SetClock(unsigned char hour, unsigned char min) { unsigned char sec; sec DS1302_ReadTime(0x81); // 读当前秒保留原值 sec 0x7F; // 清除 CH 位允许振荡 DS1302_RST 0; DS1302_SCLK 0; DS1302_RST 1; DS1302_WriteByte(0x8E); DS1302_WriteByte(0x00); // 关闭写保护 DS1302_RST 0; DS1302_RST 1; DS1302_WriteByte(0x80); // 写秒寄存器 DS1302_WriteByte(sec); DS1302_RST 0; DS1302_RST 1; DS1302_WriteByte(0x82); // 写分寄存器 DS1302_WriteByte(min); DS1302_RST 0; DS1302_RST 1; DS1302_WriteByte(0x84); // 写时寄存器 DS1302_WriteByte(hour); DS1302_RST 0; }为什么不在写时分时把秒也强制清零因为要保证用户可以微调分钟而不打断秒的连续性。若把秒强制写成 0会让人感觉“我的表被拨停了”。闹钟值不需要写进 DS1302 的 RAM虽然它有 31 字节 RAM但为了仿真可读性和代码可移植性直接把闹钟值放在单片机变量里更合适这也是多数可调闹钟仿真工程的做法。真实产品如果需要掉电保存闹钟再考虑用 DS1302 的 RAM 或 EEPROM。4. 闹钟仿真在 Proteus 中的触发逻辑与典型坑4.1 DS1302 没有闹钟中断引脚用比较触发实现很多人在 Proteus 里找 DS1302 的 INT 引脚找不到就以为仿真模型不完整。实际上 DS1302 数据手册里确实没有闹钟功能它只有时钟和 RAM。可调闹钟仿真里的“闹钟”是单片机的软件行为周期读取 DS1302 的时和分与设定值比较相等则拉高蜂鸣器引脚。比较逻辑放在 1ms 定时器中断里做而不是在主循环里比较。主循环要显示时间和扫描按键一旦某个环节被按键消抖的延时阻塞闹钟可能晚触发甚至漏触发。void Timer0_ISR(void) interrupt 1 { static unsigned char tick_1s 0, last_min 0xFF; unsigned char hour, min; TH0 0xFC; TL0 0x66; // 12MHz 晶振1ms 中断重装初值 if (tick_1s 1000) { // 每秒读取一次 DS1302 tick_1s 0; hour BCD2DEC(DS1302_ReadTime(0x85)); // 读小时 min BCD2DEC(DS1302_ReadTime(0x83)); // 读分钟 LCD_ShowTime(hour, min); // 刷新显示 if (hour BCD2DEC(alarm_hour) min BCD2DEC(alarm_min)) { if (last_min ! min) { // 防止同一分钟内重复触发 alarm_ring 1; last_min min; } } else { last_min 0xFF; // 未匹配时复位标记 } } }last_min的作用是让闹钟只在分钟变化的边沿触发一次否则蜂鸣器会在这一分钟内被主循环反复刷新逻辑上进入冗余触发。设置alarm_ring 1后主循环驱动蜂鸣器和 LED 闪烁按键 KEY_OK 可以将其清零。这里有读时序开销问题每秒读两次 DS1302时和分约 600us 的中断占用对 12MHz 的 AT89C51 来说占比不小。如果后面要加更多功能更稳妥的结构是每秒只读一次用突发模式把秒、分、时一起读到缓冲区再在中断外解析。4.2 Proteus 中 DS1302 时间不走与蜂鸣器不响的排查Proteus 仿真中时间不走不一定是代码 bug。最常遇到的三种情况CRYSTAL 频率忘记改秒寄存器 CH 位没清I/O 口没有上拉。排查时不要先改代码右键 DS1302 打开调试窗口直接看秒寄存器的值如果停了看 CH 位是 0 还是 1。Proteus 各版本中DS1302 模型属性里一般都有初始化寄存器的选项可以在放入器件后直接预设初始时间方便仿真从固定时刻开始演示闹钟。蜂鸣器不响的常见原因是元件选型和驱动方式。Proteus 里有 BUZZER有源蜂鸣器和 SOUNDER压电式前者 DC 电压就能响后者需要交流驱动或方波脉冲。仿真工程里直接用一个 BUZZER 接到单片机引脚另一端接 VCC引脚输出低电平驱动是最省事做法若用 SOUNDER则要输出 1-4kHz 方波才能听到声音否则示波器上看到引脚电平翻转但喇叭没动静。真实硬件中蜂鸣器要串三极管和续流二极管Proteus 仿真为简化可以不加但移植到开发板前要记得补上。还有一个在可调闹钟仿真里很隐蔽的坑LCD 显示时间用每秒刷新用户调整闹钟或时间后按确认键写回 DS1302但下一次秒刷新把时间重新覆盖显示看起来就像“设定被还原”。原因往往是按键确认只改了 LCD 缓存没有真正执行 DS1302_SetClock或者写回了但下一秒读的是旧值。在代码里给“正在写回”做一个标志位写完后立刻触发一次完整读取可以有效规避。5. 用虚拟示波器与调试窗口验证 DS1302 可调闹钟仿真5.1 把示波器接上 SCLK 与 I/OProteus 左侧工具栏选择 Virtual Instruments放置 OSCILLOSCOPE将通道 A 接 DS1302 的 SCLK通道 B 接 I/O通道 C 接 RST。运行仿真后按下“设置”按键观察三个引脚在写时间时的电平变化RST 先拉高SCLK 出现 8 个脉冲用于命令字节紧接着再有 8 个脉冲用于数据。如果只有命令脉冲没有数据脉冲说明 WriteByte 只执行了一次问题在按键状态机。示波器时间轴分辨率要放到 1ms 或更低12MHz 单片机运行的时序在仿真里相当快自动刻度可能看不出波形。读到数据的关键看 I/O 在 SCLK 高电平之前的零点几毫秒是否稳定这就是读时序代码里“先读、后拉高”的验证要点。5.2 可调闹钟仿真验证清单验证项操作预期结果时钟走时正常运行 1 分钟LCD 秒位按秒递增分位在满 60 时进位按键消抖快速按 KEY_UP 十次小时或分钟值稳定增加不跳变多个时间可调进入时钟设定改时/分后确认重启仿真后时间仍为新值闹钟触发把闹钟设成当前时间 1 分钟时间到达时蜂鸣器响LED 同步闪烁闹钟停止响铃时按 KEY_OK蜂鸣器停止状态回到 RUN_MODE写保护初始化后单独读 0x8Fbit7 为 1防止意外改写最后一项验证容易被忽略DS1302_Init 里写完时间后应当重新把写保护打开。若写保护一直关闭仿真中任何干扰都可能改写时间真实产品还会导致 RAM 数据混乱。Proteus 的模型对写保护的实现与真实芯片一致可以在调试窗口里修改写保护寄存器测试防护是否生效。5.3 从 Proteus 到真实硬件的三个改动仿真通过后移植到 STC89C52、普中或江协开发板时DS1302 部分通常要改三处I/O 口换成带内部上拉的 P0 以外的口外部上拉电阻保留晶振旁的两个 22pF 电容要靠近 DS1302 引脚走线尽可能短否则 32.768kHz 容易停振VCC1 接 3V 纽扣电池作为备用电源主电源掉电后时间才能保持。另外真实芯片的读时序对边沿更敏感如果示波器实测读到的数据偶发错误可以在 SCLK 边沿之间插入几个 NOPProteus 仿真不会暴露这类问题拿到真实芯片上再补。闹钟的蜂鸣器驱动那侧NPN 三极管基极串 1k 电阻蜂鸣器并联续流二极管别直接拿引脚去顶喇叭。还有一个用于验证位序的小技巧把 DS1302_WriteByte 和 ReadByte 里的循环次数抽成宏 READ_TRIES 和 WRITE_TRIES。当怀疑命令字节位序错位时把写循环改为 9 次观察发送的第一个额外脉冲对应的数据是否为 0就能立刻判断命令字是否多移了一位。这个改法不需要动协议函数主体对可调闹钟仿真这种代码量不大的工程用来确认位序和循环次数尤其直接。本文还有配套的精品资源点击获取