DS1302可调闹钟Proteus仿真:时序、BCD码与按键调时全解析
2026/9/16 11:24:23 网站建设 项目流程

简介:基于DS1302实时时钟芯片的Proteus可调闹钟仿真工程,专为嵌入式初学者和单片机课程设计准备。工程支持通过独立按键调整时间与闹钟,并在LCD上实时显示,完整演示了RTC初始化、时间读写、按键消抖和显示刷新的典型流程。压缩包共20个文件,体积仅78KB,以C语言源程序、头文件、Proteus仿真图(DSN)、Keil工程文件和hex烧录文件为主,附带闹钟设定说明文档及工程备份,结构简洁,便于直接打开学习或修改复用。已有263人学习使用。通过该项目可掌握DS1302三线串行通信协议、闰年自动校正、12/24小时制切换等核心知识点,同时熟悉Proteus与Keil联合仿真的操作流程,并参考代码中的错误处理与用户交互设计思路,是实践嵌入式开发、加深RTC芯片理解的实用案例。

1. 用 DS1302 在 Proteus 里跑通可调闹钟仿真,先理解这三个关键词

这个标题拆开看,其实是一道典型的单片机综合题:用 DS1302 做时钟芯片,用 Proteus 做电路仿真,把闹钟做“可调”。很多人在 DS1302 上折腾一个晚上,调出来的时间不走、读回全是 0xFF、闹钟到点不响,问题大多不在代码逻辑,而在时序边沿、BCD 码换算和 Proteus 的元件参数。这篇文章不讲大而全的工程开发流程,只围绕 DS1302 可调闹钟仿真这一件事,从数据手册里的读写时序讲起,到 Proteus 电路搭建,再到 C51 代码和排错验证,全程可复现。适合正在做课程设计、准备电子竞赛,或者刚把 Keil 和 Proteus 装好、想拿真实项目练手的读者。

2. DS1302 时序与寄存器:闹钟可调的前提是时钟可读写

2.1 三线接口的读写沿,和 SPI 不完全一样

DS1302 是 DALLAS 的串行实时时钟芯片,接口只有三根线:CE(片选)、SCLK(串行时钟)、IO(数据)。很多人习惯把这三根线直接理解成 SPI,但 DS1302 并不是标准 SPI 器件,它不区分主从模式,也没有片选之外的应答机制,全靠主机按照固定时序往 IO 上推位。

先记住结论:数据是低位先走,也就是 LSB first。写操作时,CE 拉高后,主机在 SCLK 上升沿把 IO 上的数据锁存进 DS1302;读操作时,命令字节同样按上升沿写入,最后一个命令位送入后,DS1302 会在接下来的 SCLK 下降沿开始把数据放到 IO 上,主机应当在 SCLK 低电平期间读取。

这个边沿差异是仿真里最容易出错的地方。如果代码里读写都用上升沿,读出来的数据会整体错位,典型表现是分钟值和秒值对不上、寄存器地址错乱。

下面是最小可用的字节读写代码,直接对应 DS1302 的时序要求:

#include <reg51.h> #include <intrins.h> sbit DS1302_CE = P2^0; sbit DS1302_SCLK = P2^1; sbit DS1302_IO = P2^2; void ds1302_delay(void) { _nop_(); _nop_(); } void ds1302_write_byte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { DS1302_SCLK = 0; DS1302_IO = dat & 0x01; // 低位先发送 DS1302_SCLK = 1; // 上升沿锁存 dat >>= 1; } }

ds1302_delay()的作用不是摆设。Proteus 的 DS1302 模型虽然不会像真实芯片那样对极短的脉冲完全没响应,但仿真步长过小时,信号竞争容易导致数据位丢失。SCLK 低电平和高电平各自保持几个微秒比较稳妥,实测中把延时放大到 10µs 级别,读回数据的稳定性会明显提升。

读字节代码和写字节不同,关键在方向切换后要先让 IO 引脚置 1:

unsigned char ds1302_read_byte(void) { unsigned char i, dat = 0; DS1302_IO = 1; // 准双向口读之前先置 1 for (i = 0; i < 8; i++) { DS1302_SCLK = 0; // 下降沿后数据出现在 IO 上 if (DS1302_IO) { dat |= (0x01 << i); // 低位先到达 } DS1302_SCLK = 1; } return dat; }

51 单片机的 P2 口是准双向口,读外部引脚之前必须先把端口写 1,否则读回来的永远是 0。这个细节在 Keil 里不会报错,在 Proteus 里表现为“时间全是 0”,排查起来非常隐蔽。读字节循环里用的是dat |= (0x01 << i),对应低位先出的规则。

读写单字节都准备好之后,操作寄存器的 API 就是在前面加上地址字节,再处理 CE 拉低结束通信:

void ds1302_write_reg(unsigned char addr, unsigned char dat) { DS1302_CE = 0; DS1302_SCLK = 0; DS1302_CE = 1; ds1302_write_byte(addr); // 写命令,例如 0x80 ds1302_write_byte(dat); // 写入数据 DS1302_CE = 0; } unsigned char ds1302_read_reg(unsigned char addr) { unsigned char dat; DS1302_CE = 0; DS1302_SCLK = 0; DS1302_CE = 1; ds1302_write_byte(addr | 0x01); // 读命令,写地址最低位置 1 dat = ds1302_read_byte(); DS1302_CE = 0; return dat; }

这里注意地址参数传入的是写地址。例如读秒寄存器时调用ds1302_read_reg(0x80),函数内部会自动把最低位改成 1,变成读命令 0x81。如果调用时直接传入 0x81,命令字节会变成 0x81 | 0x01 = 0x81 不变,看起来没问题,但换成其他寄存器就可能错位。统一约定传写地址更不容易乱。

2.2 寄存器表和 BCD 码,直接决定“可调”怎么写

DS1302 的时间寄存器按秒、分、时、日、月、星期、年排列,每个寄存器都有独立读写地址。仿真里用到最多的是前四个,控制寄存器 0x8E 和涓流充电寄存器 0x90 也值得了解。

功能写地址读地址数据范围备注
秒寄存器0x800x8100~59 BCDbit7 是 CH,1 表示停振
分寄存器0x820x8300~59 BCD无特殊标志位
时寄存器0x840x8500~23 BCDbit7 选择 12/24 小时制
日寄存器0x860x8701~31 BCD低 6 位有效
控制寄存器0x8E0x8F0x00bit7 是写保护 WP

寄存器内部存放的是 BCD 码,而不是十进制数。比如 9 点 45 分,小时寄存器里存的是 0x09,分钟寄存器里是 0x45。如果直接把 45 塞进寄存器,芯片会把它当成 0x45,也就是十位是 4、个位是 5,这在显示上恰好还是 45,但一旦进行加法操作,问题就会暴露。

加减 1 分钟的正确做法是:先读寄存器,把 BCD 转成十进制,加减运算后再把十进制转回 BCD,写回寄存器。转换函数如下:

unsigned char bcd_to_dec(unsigned char bcd) { return (bcd >> 4) * 10 + (bcd & 0x0F); } unsigned char dec_to_bcd(unsigned char dec) { return ((dec / 10) << 4) | (dec % 10); }

仿真中常见的“按一下加 1,9 变成 0x10”、“分钟从 09 跳到 0A”这一类现象,都是省略了 BCD 到十进制的换算。在做可调闹钟时,这个转换函数就是修改时间的最底层依赖。

2.3 第一次上电必须做的三件事:清写保护、清 CH、写初值

DS1302 默认状态容易让新手误判。芯片上电后,控制寄存器 0x8E 的写保护位可能是 1,此时所有写入操作都被忽略。另外秒寄存器的 bit7(CH 位)如果为 1,振荡器停振,秒值不跳动。还有一点:Proteus 仿真启动时 DS1302 模型内部的时间和 PC 时间不一定同步,甚至可能是 00:00:00 起步。

初次初始化建议直接写成固定时间,方便观察秒动作:

void ds1302_init(unsigned char hour, unsigned char minute, unsigned char second) { ds1302_write_reg(0x8E, 0x00); // 取消写保护 ds1302_write_reg(0x80, dec_to_bcd(second) & 0x7F); // 清 CH 位 ds1302_write_reg(0x82, dec_to_bcd(minute)); ds1302_write_reg(0x84, dec_to_bcd(hour) & 0xBF); // 24 小时制 ds1302_write_reg(0x8E, 0x80); // 恢复写保护,防止误写 }

写秒寄存器时& 0x7F的作用不是必须的,但如果传入的 second 大于 59,BCD 结果会污染 CH 位,保险起见保留。小时寄存器& 0xBF是把 12/24 小时制标志位置 0,强制 24 小时制,这样后续判断“几点”不必关心 AM/PM。

完全不必在初始化里写入日、月、年这些字段,因为闹钟只用到“时”和“分”,日期不参与比较。保持寄存器状态简单,后面读回来的数据也好排查。

3. Proteus 搭建 DS1302 闹钟电路:元件清单、接线与固件加载

3.1 先列元件清单,DS1302 是核心但不是全部

在 Proteus 8 Professional 里新建工程后,直接在元件搜索框输入名称选择即可。下面这张清单是经过验证可用的最小配置,去掉任何一个都可能导致现象不在预期范围。

元件Proteus 搜索关键词数量说明
单片机AT89C511经典 51 内核,12MHz 晶振
RTC 芯片DS13021仿真模型包含在标准库中
晶振CRYSTAL2单片机用 12MHz,DS1302 用 32.768kHz
电容CAP4晶振负载电容,仿真中可省略
排阻RESPACK-81P0 口上拉,10kΩ
电阻RES4按键下拉或三极管基极限流
按键BUTTON3模式切换、加、减
数码管7SEG-MPX4-CA-BLUE1四位共阳数码管,显示时:分
PNP 三极管2N29074数码管位选驱动,低电平导通
蜂鸣器BUZZER15V 有源蜂鸣器,闹钟输出
NPN 三极管2N22221蜂鸣器驱动

DS1302 的晶振频率必须选 32.768kHz,这是 RTC 芯片的标准低频晶振。Proteus 提供的 CRYSTAL 元件属性里有 Frequency 参数,双击晶振改成 32.768kHz。如果误填成 12MHz,DS1302 秒值会异常跳动,而且误差没有任何规律。

3.2 接线参数与 sbit 定义,先统一再连线

Proteus 仿真是先连电路再写代码,但引脚分配必须提前定好,否则电路和程序对不上。下面这组分配合适,段选数据从 P0 走,控制线集中在 P2,按键独占 P1,互不干扰。

sbit DS1302_CE = P2^0; sbit DS1302_SCLK = P2^1; sbit DS1302_IO = P2^2; #define SEG_PORT P0 // 数码管段选,a~g 分别接 P0.0~P0.6 #define DIG_PORT P2 // 位选复用 P2.3~P2.6,低电平点亮 sbit KEY_MODE = P1^0; // 模式切换 sbit KEY_UP = P1^1; // 加 sbit KEY_DOWN = P1^2; // 减 sbit BUZZER = P1^7; // 蜂鸣器

接线的关键有三处。第一,P0 口必须接排阻上拉,否则高电平驱动能力不足,数码管亮度不匀甚至不亮。第二,DS1302 的 CE、SCLK、IO 三根线建议各接一个 10kΩ 上拉电阻,Proteus 内部模型不强依赖上拉,但这是真实电路的常规做法。第三,共阳数码管的公共端需要通过 PNP 三极管接 VCC,位选引脚为低电平时三极管导通,对应位才会点亮。

蜂鸣器驱动也不建议直接把 IO 口接蜂鸣器。用 P1.7 经 1kΩ 电阻接到 2N2222 的基极,发射极接地,集电极接有源蜂鸣器负极,蜂鸣器正极接 VCC。这样仿真效果和真实电路一致,代码里只要给 P1.7 高电平就能响。

3.3 Keil 编译出 hex,Proteus 双击芯片加载固件

电路画完,代码在 Keil C51 工程里编译生成 hex 文件。注意 Proteus 仿真依赖的外部时钟频率要和 Keil 里的晶振设置一致,如果代码里用了_nop_()做延时,那延时实际时长由 Proteus 里 AT89C51 的 Clock Frequency 决定,默认 12MHz 即可。

加载固件的操作路径是:在 Proteus 原理图上双击 AT89C51 芯片,在弹出的 Edit Component 对话框里找到 Program File 一栏,点击文件夹图标选中编译好的 .hex 文件。Crystal Frequency 保持 12MHz,其他参数不需要改。加载完成后点击左下角运行按钮,仿真立即开始。

如果此时数码管没有任何显示,先检查 P0 排阻是否接好、位选三极管方向是否正确。更快的验证手段是用 Proteus 的 Virtual Terminal(虚拟终端)接到单片机串口,在代码里打印 DS1302 读回的寄存器值。这个方法比盯数码管盲排快得多,后面排错还会用到。

4. DS1302 可调闹钟的 C 语言实现:按键改时间、BCD 换算与闹铃触发

4.1 底层驱动补齐 RAM 读写,闹钟值存进 DS1302 片内 RAM

上一章的读写函数只覆盖了时间寄存器。DS1302 内部除了时钟寄存器,还有 31 字节的通用 RAM,地址从 0xC0 开始,写地址是偶数,读地址是奇数。把闹钟的“时”和“分”存在这里,比放在单片机普通变量里更贴合标题中“可调仿真”的含义,断电后数据也能保留。

void ds1302_write_ram(unsigned char addr, unsigned char dat) { DS1302_CE = 0; DS1302_SCLK = 0; DS1302_CE = 1; ds1302_write_byte(addr); // 0xC0 起始,写入 RAM 地址 ds1302_write_byte(dat); DS1302_CE = 0; } unsigned char ds1302_read_ram(unsigned char addr) { unsigned char dat; DS1302_CE = 0; DS1302_SCLK = 0; DS1302_CE = 1; ds1302_write_byte(addr | 0x01); // 转成读命令 dat = ds1302_read_byte(); DS1302_CE = 0; return dat; }

写 RAM 之前同样需要清除写保护位,否则写入被忽略。闹钟存储方案定为:RAM 地址 0xC0 存闹钟小时,0xC2 存闹钟分钟。选 0xC0 和 0xC2 而不是 0xC0 和 0xC1,是为了让读地址与写地址错开时更直观,两个地址之间留出余量,避免 RAM 地址混用时互相覆盖。

改用这两个 API 之后,初始化代码里多做一步:把闹钟默认值写入 RAM,比如早上 7 点 30 分:

ds1302_write_reg(0x8E, 0x00); ds1302_write_ram(0xC0, dec_to_bcd(7)); ds1302_write_ram(0xC2, dec_to_bcd(30)); ds1302_write_reg(0x8E, 0x80);

4.2 按键状态机:模式切换、加减、写回

“可调”的本质是一个状态机。用三个按键实现四档切换,比用矩阵键盘简洁,也够展示 DS1302 的完整操作路径。

状态显示内容K1 按下后进入下一状态K2 / K3 效果
MODE_TIME当前时间MODE_SET_HOUR无操作
MODE_SET_HOUR当前小时MODE_SET_MINUTE小时加减并写回 DS1302
MODE_SET_MINUTE当前分钟MODE_SET_ALARM_HOUR分钟加减并写回 DS1302
MODE_SET_ALARM_HOUR闹钟小时MODE_SET_ALARM_MINUTE闹钟小时加减并写入 RAM
MODE_SET_ALARM_MINUTE闹钟分钟MODE_TIME闹钟分钟加减并写入 RAM

按键扫描需要处理抖动。Proteus 里按键抖动现象不如真实硬件明显,但代码层面最好加上 10ms 延时消抖,否则一次按下被识别成多次,调时间根本停不住。

#define MODE_TIME 0 #define MODE_SET_HOUR 1 #define MODE_SET_MINUTE 2 #define MODE_SET_ALARM_HOUR 3 #define MODE_SET_ALARM_MINUTE 4 unsigned char mode = MODE_TIME; void scan_keys(void) { if (KEY_MODE == 0) { delay_ms(10); if (KEY_MODE == 0) { mode = (mode + 1) % 5; while (KEY_MODE == 0); // 等待释放 } } if (KEY_UP == 0) { delay_ms(10); if (KEY_UP == 0) { if (mode == MODE_SET_HOUR || mode == MODE_SET_ALARM_HOUR) { adjust_hour(1); } else if (mode == MODE_SET_MINUTE || mode == MODE_SET_ALARM_MINUTE) { adjust_minute(1); } while (KEY_UP == 0); } } }

adjust_houradjust_minute内部做读、转十进制、加减、越界回绕、转 BCD、写回整套动作。越界回绕要注意 24 小时制和 60 分钟制的模数不同,小时数对 24 取余,分钟对 60 取余。

下面给出调整小时的实现,分钟的逻辑几乎一样:

void adjust_hour(unsigned char delta) { unsigned char hour; if (mode == MODE_SET_HOUR) { hour = bcd_to_dec(ds1302_read_reg(0x84)); hour = (hour + delta) % 24; ds1302_write_reg(0x8E, 0x00); ds1302_write_reg(0x84, dec_to_bcd(hour)); ds1302_write_reg(0x8E, 0x80); } else { hour = bcd_to_dec(ds1302_read_ram(0xC0)); hour = (hour + delta) % 24; ds1302_write_reg(0x8E, 0x00); ds1302_write_ram(0xC0, dec_to_bcd(hour)); ds1302_write_reg(0x8E, 0x80); } }

这段代码展示了可调操作中最重要的顺序:每次写入前都清写保护,写完立即恢复。写保护恢复后,DS1302 继续正常走时,只是禁止后续写入,这一层保护在真实产品里有意义,仿真里至少能让时序状态更可控。

4.3 闹钟比较逻辑与响铃控制

闹钟触发条件有两个层面:时间匹配、响铃去重复。每秒读一次时间,如果时、分、秒恰好等于闹钟设定值且秒为 0,表示这一分钟刚到来,只响一次,避免同一分钟内反复触发。

#define ALARM_RING_TICKS 50 unsigned char alarm_seconds = 0; void check_alarm(unsigned char hour, unsigned char minute, unsigned char second) { unsigned char alarm_h, alarm_m; if (second != 0) { return; } alarm_h = bcd_to_dec(ds1302_read_ram(0xC0)); alarm_m = bcd_to_dec(ds1302_read_ram(0xC2)); if (hour == alarm_h && minute == alarm_m) { if (alarm_seconds == 0) { alarm_seconds = ALARM_RING_TICKS; } } } void ring_task(void) { if (alarm_seconds > 0) { BUZZER = 1; delay_ms(50); alarm_seconds--; } else { BUZZER = 0; } }

ALARM_RING_TICKS是响铃时长控制。每次主循环调用一次ring_task,响铃持续约 2.5 秒。有源蜂鸣器在 Proteus 里只要给高电平就会发声,这一点和真实器件行为一致。

秒值比较放在check_alarm的最前面,是个实用技巧。DS1302 的秒寄存器每秒变化一次,主循环如果在同一秒内多次调用check_alarm,靠second != 0拦截后,只有秒为 0 的那一次才有机会进入比较,天然避免了重复触发。

主循环把读取、显示、按键、闹钟四件事串起来:

void main(void) { unsigned char h, m, s; ds1302_init(23, 59, 50); ds1302_write_reg(0x8E, 0x00); ds1302_write_ram(0xC0, dec_to_bcd(0)); ds1302_write_ram(0xC2, dec_to_bcd(0)); ds1302_write_reg(0x8E, 0x80); while (1) { h = bcd_to_dec(ds1302_read_reg(0x84)); m = bcd_to_dec(ds1302_read_reg(0x82)); s = bcd_to_dec(ds1302_read_reg(0x80)); display_time(h, m); scan_keys(); check_alarm(h, m, s); ring_task(); } }

初始化时间设在 23:59:50,闹钟默认值设为 00:00,这样仿真启动 10 秒后就能看到蜂鸣器响应,不用长时间等待。

5. DS1302 闹钟仿真排错与验证:先看时序、再查上拉、最后核对 BCD

5.1 用 Proteus 虚拟示波器验证时序是否正确

时间读不出来时,最有价值的手段不是反复看代码,而是直接看波形。Proteus 8 Professional 左侧工具栏里有 Virtual Instruments,选择 Oscilloscope 添加到电路图中,把通道 A 接到 DS1302 的 SCLK,通道 B 接到 IO,CE 接通道 C 或直接用逻辑分析仪替代。

正常读一次秒寄存器的波形应该具备两个特征:SCLK 上出现连续 16 个脉冲,前 8 个对应命令字节 0x81,后 8 个对应数据字节;IO 上的第一位出现在第 8 个 SCLK 下降沿之后,而不是从第一个脉冲就开始输出。

如果只看到 8 个脉冲,说明代码在发送命令之后没有继续产生时钟,检查ds1302_read_byte是否被调用。如果 IO 波形一直是低电平,先查 P2.2 有没有被外部电路强行拉低,再查读之前DS1302_IO = 1是否执行。

5.2 常见仿真故障对照表

现象原因处理方式
读回全为 0xFFIO 未上拉或方向切换失败三根控制线接 10kΩ 上拉,读前先写 1
读回全为 0x00IO 被拉死,或读循环边沿错误检查 SCLK 低电平期间读 IO 的时序
秒不走,秒值不变CH 位为 1 或晶振频率不对写秒寄存器时清 bit7,确认晶振 32.768kHz
分钟 09 加 1 变 0ABCD 码直接做十进制运算先 bcd_to_dec,计算后 dec_to_bcd 写回
调小时不生效写保护未取消写寄存器前先写 0x8E = 0x00
数码管亮度低P0 无上拉接 RESPACK-8 排阻到 VCC

其中“秒不走”在 Proteus 里还有一个容易忽略的原因:仿真暂停时 DS1302 模型不走时间,但代码里读秒的值不会变。点击运行按钮后如果秒值仍不变,才需要往寄存器方向排查。

5.3 仿真提速技巧:临时改晶振频率验证闹钟触发

验证闹钟逻辑时,等待真实时间触发太慢。DS1302 的外部晶振是 32.768kHz,Proteus 里双击 DS1302 旁边的晶振元件,把 Frequency 临时改成 327.68kHz,时钟走速就变成原来的 10 倍,10 分钟的时间流逝约 1 分钟就能观察到。

这个办法比在代码里强行跳过时间判断更可靠,因为 DS1302 芯片本身的状态没有被修改,只是仿真环境的晶振频率改变了。验证完成后把晶振改回 32.768kHz,代码不用动,后续要以真实速率验证时,一切恢复正常。测试时把闹钟时间设在当前时间后 2 分钟,晶振放大 10 倍后大约 12 秒就能看到蜂鸣器动作。

验证完整路径建议按三步走:第一步,初始化时间设到 23:59:50,闹钟设 00:00,确认秒走、分走、到点响铃;第二步,按 K1 进入调小时模式,按 K2 加 1,退出后显示值变化;第三步,进入调闹钟模式,把闹钟值调整到当前分钟,验证可调后仍能正常触发。三步全部通过,这个 DS1302 可调闹钟仿真才算真正闭环。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询