简介:基于STC15的万年历工程,面向单片机初学者与嵌入式项目开发者,提供一个从时间显示到环境监控的多功能综合案例。项目以STC15为主控,结合DS1302实时时钟芯片读取日期时间,并接入DHT11/DHT22类温湿度传感器实现环境监测与超阈值报警,同时支持蜂鸣器闹钟提醒,覆盖了时钟管理、传感器数据采集、中断与报警控制等单片机常见开发环节。压缩包按rar格式打包,共23个文件,约249KB,包含核心C源码(main.c、STC15.h及启动文件)、Protel电路图(PDF版原理图与工程文件)、Proteus仿真工程(pdsprj)及配套编译链接文件,便于对照代码理解DS1302时序读写逻辑、传感器单总线协议和按键闹钟设置流程。当前已有351人下载学习,适合想通过一个完整项目熟悉STC15开发流程、学习时钟芯片与传感器联调的读者。
1. STC15万年历不是“定时器+显示”,走时才是核心
拿 STC15 做主控做万年历,是很多电子入门者会选的练手题,也是课程设计里高频出现的方案。但这个题目真正的难点不在“怎么把时间显示到 LCD 上”,而在“断电之后时间还能不能走、能撑多久、每天误差多少”。STC15 是 1T 的 8051 内核单片机,比传统 51 快不少,但它本身并没有一个能靠电池撑住的实时时钟,所以绝大多数 STC15 万年历方案都会外挂一颗 DS1302 RTC 芯片,由它负责走时、由 STC15 负责读时间、刷显示、处理按键。本文就按这个最常见的方案展开:硬件怎么接、DS1302 驱动怎么写、LCD1602 怎么显示、调时逻辑怎么设计、最后编译下载有哪些雷,一次性讲完。
适合谁?手上有 STC15 开发板或准备自己打板的人、做课程设计选这个题的学生、以及想搞清楚 DS1302 三线时序和 BCD 码转换的开发者。下面所有代码都基于 C51 裸机,不涉及操作系统,你在普通 51 上也能读懂大部分逻辑,只是延时参数和引脚模式配置需要按你自己的板子重新调。
2. 硬件选型与接线:为什么万年历要用 RTC 芯片
2.1 DS1302 和定时器、DS3231 的取舍
先把原理说清楚:万年历的时间来源有三种做法。第一种是只用单片机定时器,每秒产生一次中断累加。这种方式在板子持续供电时没问题,但一旦断电,时间回到上电初值,重新开机就得手工校时,这不是“万年历”该有的表现。第二种是外接 DS1302,它内部有振荡器和日历寄存器,主电源没电时靠一颗纽扣电池继续走,价格便宜,外围只需要一个 32.768kHz 晶振。第三种是 DS3231,自带温度补偿,年误差可以控制在分钟级,但价格是 DS1302 的几倍,而且对课程设计来说性能过剩。
对 STC15 万年历这个项目来说,DS1302 是性价比最均衡的选择。它和单片机之间是三线串行接口(CE、SCLK、IO),占用 IO 少,工作电压范围宽,官方手册给的是 2.0V 到 5.5V,可以直接和 5V 供电的 STC15 连接而不用做电平转换。更重要的是它有独立的 VCC1 备用电源引脚,断电后走时不丢,这才是“万年历”的基础。DS3231 的优势在于精度,适合做桌面时钟后长时间不校时的场景;如果只是课程设计或练手,DS1302 完全够用。
2.2 接线表与引脚分配实战
我这里给一套常见的接线方案,STC15 用 STC15W408AS 或任何至少 20 引脚的型号都行,DS1302 模块建议买带电池座和晶振的成品模块,避免自己焊晶振电路时引入负载电容问题。LCD1602 用四线模式接法,省 4 个 IO。三颗按键分别接在 P3 口,配合内部上拉使用。
| 器件 | 引脚 | 接 STC15 | 说明 |
|---|---|---|---|
| DS1302 | VCC2 | 5V | 主电源 |
| DS1302 | VCC1 | 纽扣电池正极 | 备用电源,推荐 CR2032 |
| DS1302 | GND | GND | 与单片机共地 |
| DS1302 | CE | P1.0 | 片选,高电平有效 |
| DS1302 | SCLK | P1.1 | 串行时钟 |
| DS1302 | IO | P1.2 | 数据线,读写共用 |
| LCD1602 | RS | P2.0 | 寄存器选择 |
| LCD1602 | RW | P2.1 | 读写选择,直接接地可省 IO |
| LCD1602 | E | P2.2 | 使能 |
| LCD1602 | D4~D7 | P2.4~P2.7 | 四线数据 |
| 按键 | K1~K3 | P3.0~P3.2 | 模式/加/减 |
这里有一个容易忽略的细节:DS1302 的 VCC1 接电池后,如果 VCC2 有 5V 供电,芯片自动用 VCC2;一旦 VCC2 掉电,自动切换用 VCC1。所以你即使把 VCC2 和 VCC1 都接在同一个 5V 上也能工作,但那样断电会丢时间,等于白做。纽扣电池用 CR2032 就够了,容量大、自放电小,撑一年以上没问题。焊电池座之前先看清板子丝印,正负极焊反连后悔药都没有,只能拆了重新焊。
2.3 STC15 引脚模式配置:上电默认状态要先查
STC15 和传统 51 有个很大的区别:它的引脚上电默认模式不是统一的准双向口。STC15 系列上电后 P1、P2、P3 默认是准双向口,可以直接驱动 DS1302 和按键,但 P0 默认是高阻输入,必须通过 P0M0/P0M1 寄存器设置成准双向口或推挽才能用。如果你把 LCD 数据线接在 P0 口并且不配置模式,显示会一直花屏,这是很多新手第一次点不亮 LCD1602 的原因。
常见做法是在 main 函数最开始统一配置一次:
P0M0 = 0x00; // P0 设为准双向口 P0M1 = 0x00; P1M0 = 0x00; // P1 保持默认准双向 P1M1 = 0x00; P2M0 = 0x00; // P2 保持默认准双向 P2M1 = 0x00; P3M0 = 0x00; // P3 保持默认准双向 P3M1 = 0x00;P0M0/P0M1 是 STC15 特有的引脚模式控制寄存器,00 表示准双向口,01 表示推挽,10 表示高阻,11 表示开漏。DS1302 的 IO 是双向数据线,准双向口模式下写入低电平后再切到输入读取,读到的也是低电平,所以读时序里要在读之前把时钟拉低并等待数据稳定。按键和 LCD 用准双向口即可,不需要推挽,这样能减少代码里对引脚模式的频繁切换。
3. DS1302 驱动:三线时序、BCD 码和读写代码
3.1 寄存器表:地址错位是调不通的第一大原因
DS1302 的寄存器靠命令字节寻址,命令字节的最高位固定为 1,bit0 表示读写方向,0 写 1 读。很多人第一次调试时把写地址和读地址搞混,写完再读回来发现全是 0xFF,就是这里出了问题。下面这张表是万年历必须用到的寄存器,建议直接保存在工程注释里。
| 寄存器 | 写命令 | 读命令 | 数据格式 |
|---|---|---|---|
| 秒 | 0x80 | 0x81 | bit7 为 CH,1 表示时钟停止 |
| 分 | 0x82 | 0x83 | BCD 码 |
| 时 | 0x84 | 0x85 | 默认 24 小时制 |
| 日 | 0x86 | 0x87 | BCD 码,月中的号数 |
| 月 | 0x88 | 0x89 | BCD 码 |
| 周 | 0x8A | 0x8B | 1 表示星期一,7 表示星期日 |
| 年 | 0x8C | 0x8D | BCD 码,如 0x25 表示 2025 年 |
| 写保护 | 0x8E | 0x8F | bit7 为 1 时禁止写入 |
| 涓流充电 | 0x90 | 0x91 | 控制充电二极管数和电阻大小 |
DS1302 内部存的是 BCD 码,不是十进制。秒寄存器里的数据 0x23 表示二十三秒,而不是把它当作十六进制数值来理解的三十五秒。读写时如果不做转换,显示到 15 秒之后会出现字母。转换用两个宏最省事:
#define BCD_TO_DEC(x) (((x) >> 4) * 10 + ((x) & 0x0F)) #define DEC_TO_BCD(x) ((((x) / 10) << 4) | ((x) % 10))这两个宏的原理是把高四位当作十位、低四位当作个位分别处理。BCD_TO_DEC 用在读取后显示,DEC_TO_BCD 用在设置时间时把十进制数转换成 BCD 码写进寄存器。初学阶段最容易踩的坑是读出来的数据直接 sprintf 到 LCD 上,比如读到 0x23 直接打印出 23,看起来前 59 秒都正常,一旦到 0x30 这种值就会出现乱码。凡是和时间内容相关的显示,一律先过 BCD_TO_DEC。
3.2 读写字节的时序代码
DS1302 的时序不是标准 SPI,但思路接近:CE 拉高后开始通信,每个 SCLK 上升沿传输一位数据。写入时主机在 SCLK 低电平期间把 IO 设置好,再拉高 SCLK 让 DS1302 采样;读取时主机拉低 SCLK,DS1302 会把数据送到 IO 上,主机读走后再拉高 SCLK。DS1302 时序被人说过是玄学,其实只是读写顺序和方向切换没理顺。
#include <STC15.H> sbit DS1302_CE = P1^0; sbit DS1302_SCLK = P1^1; sbit DS1302_IO = P1^2; void ds1302_write_byte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { DS1302_SCLK = 0; // 先拉低时钟 if (dat & 0x01) DS1302_IO = 1; else DS1302_IO = 0; DS1302_SCLK = 1; // 上升沿,DS1302 采样数据 dat >>= 1; } } unsigned char ds1302_read_byte(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; } return dat; }写字节时先拉低 SCLK,再放数据,最后拉高,保证数据稳定后再产生上升沿。读字节时 dat 先右移再判断 IO,相当于从最高位开始收数据,配合低位先传的时序正好拼成完整字节。这里有个细节:DS1302 的 IO 是三态输出,读操作完成后 CE 拉低,IO 就会释放,不会影响下一次写数据。读函数里不要在图省事加延时,SCLK 高低电平各保持几百纳秒就够,软件延时反而会让部分质量差的模块出现毛刺。
3.3 初始化、设置与读取:突发模式少写 7 次地址
DS1302 支持突发读写,一次命令连续读写 7 个时间寄存器。设置时间时先关写保护、再写入、最后开写保护,顺序不能乱。初始化时要特别检查秒寄存器的 CH 位,芯片首次上电或电池换过之后 CH 位可能为 1,时钟不启动,表现为写了时间但秒不走。
void ds1302_init(void) { ds1302_CE = 1; ds1302_write_byte(0x8E); // 写保护寄存器 ds1302_write_byte(0x00); // 关闭写保护 ds1302_CE = 0; ds1302_CE = 1; ds1302_write_byte(0x80); // 秒寄存器写命令 ds1302_write_byte(0x00); // CH 清零,启动时钟 ds1302_CE = 0; ds1302_CE = 1; ds1302_write_byte(0x90); // 涓流充电控制 ds1302_write_byte(0xA5); // 1 个二极管 + 2k 电阻 ds1302_CE = 0; ds1302_CE = 1; ds1302_write_byte(0x8E); // 重新打开写保护 ds1302_write_byte(0x80); ds1302_CE = 0; } void ds1302_set_time(unsigned char *t) { unsigned char i; // t[0]秒 t[1]分 t[2]时 t[3]日 t[4]月 t[5]周 t[6]年 ds1302_CE = 1; ds1302_write_byte(0x8E); ds1302_write_byte(0x00); // 关写保护 ds1302_CE = 0; ds1302_CE = 1; ds1302_write_byte(0xBE); // 突发写秒寄存器,地址自动递增 for (i = 0; i < 7; i++) { ds1302_write_byte(DEC_TO_BCD(t[i])); } ds1302_CE = 0; ds1302_CE = 1; ds1302_write_byte(0x8E); ds1302_write_byte(0x80); // 开写保护 ds1302_CE = 0; } void ds1302_get_time(unsigned char *t) { unsigned char i; ds1302_CE = 1; ds1302_write_byte(0xBF); // 突发读命令 for (i = 0; i < 7; i++) { t[i] = BCD_TO_DEC(ds1302_read_byte()); } ds1302_CE = 0; }set_time 用突发写命令 0xBE 一次写完 7 个寄存器,get_time 用突发读命令 0xBF 一次读完,比逐个地址操作省时间和代码量。写保护必须在写入前关闭,写完再打开,防止程序跑飞时误改时间。注意年份只在设置时写入一次,平时不需要动,所以 get_time 读出来的年只在需要显示或需要判断闰年时使用。还有一个容易忽略的点:设置时间时传进来的数组要先转成 BCD 码再写,读出来之后要先转成十进制再显示,不要在显示环节做二次转换,否则调试十分钟都找不出错在哪。
4. 显示与调时主程序:LCD1602 + 三键状态机
4.1 LCD1602 四线模式初始化
LCD1602 四线模式相比八线模式少占引脚,但初始化时序比八线严格。刚上电时 LCD 可能处于八线模式,要先用八线指令把模式切到四线,再设置显示参数。下面的初始化函数是通用的标准时序,延时参数用 STC15 在 12MHz 主频下实测没问题,如果你把主频调高,延时要相应加长。
sbit LCD_RS = P2^0; sbit LCD_RW = P2^1; sbit LCD_E = P2^2; sbit LCD_D4 = P2^4; sbit LCD_D5 = P2^5; sbit LCD_D6 = P2^6; sbit LCD_D7 = P2^7; void lcd_write_cmd_8bit(unsigned char cmd) { LCD_RS = 0; LCD_RW = 0; LCD_E = 1; LCD_D4 = (cmd >> 4) & 0x01; LCD_D5 = (cmd >> 5) & 0x01; LCD_D6 = (cmd >> 6) & 0x01; LCD_D7 = (cmd >> 7) & 0x01; lcd_delay_us(40); LCD_E = 0; lcd_delay_us(40); } void lcd_write_cmd(unsigned char cmd) { LCD_RS = 0; LCD_RW = 0; // 高四位 LCD_E = 1; LCD_D4 = (cmd >> 4) & 0x01; LCD_D5 = (cmd >> 5) & 0x01; LCD_D6 = (cmd >> 6) & 0x01; LCD_D7 = (cmd >> 7) & 0x01; lcd_delay_us(40); LCD_E = 0; lcd_delay_us(40); // 低四位 LCD_E = 1; LCD_D4 = cmd & 0x01; LCD_D5 = (cmd >> 1) & 0x01; LCD_D6 = (cmd >> 2) & 0x01; LCD_D7 = (cmd >> 3) & 0x01; lcd_delay_us(40); LCD_E = 0; lcd_delay_ms(2); } void lcd_init(void) { lcd_delay_ms(40); lcd_write_cmd_8bit(0x30); lcd_delay_ms(5); lcd_write_cmd_8bit(0x30); lcd_delay_ms(5); lcd_write_cmd_8bit(0x30); lcd_delay_ms(1); lcd_write_cmd_8bit(0x20); // 切到 4 位模式 lcd_write_cmd(0x28); // 2 行显示 lcd_write_cmd(0x0C); // 显示开,无光标 lcd_write_cmd(0x01); // 清屏 lcd_delay_ms(2); }这个函数先送高四位再送低四位,使能脚 E 的下降沿让 LCD 锁存数据。四线模式下所有命令和数据都是分两次发送,初始化时也不能例外。lcd_delay_us 和 lcd_delay_ms 用软件延时实现即可,不依赖定时器,注意 C51 编译优化等级会影响延时循环的准确性,建议把延时函数声明为 volatile 或者用空循环加 NOP 的方式实现。
4.2 按键调时状态机
调时逻辑用状态机实现最清晰。默认状态是正常运行,按一次模式键进入秒设置,再按进入分、时、日、月、年设置,最后再按一次保存并返回正常运行。加键和减键只在设置状态下起作用。
#define MODE_RUN 0 #define MODE_SEC 1 #define MODE_MIN 2 #define MODE_HOUR 3 #define MODE_DAY 4 #define MODE_MONTH 5 #define MODE_YEAR 6 unsigned char mode = MODE_RUN; void key_scan(unsigned char *t) { if (key_mode_pressed()) // 模式键按下并消抖 { mode++; if (mode > MODE_YEAR) { mode = MODE_RUN; ds1302_set_time(t); // 保存并写回 DS1302 } } if (mode == MODE_RUN) return; if (key_plus_pressed()) // 加键 { t[mode - 1]++; if (t[mode - 1] > 59 && mode <= MODE_HOUR) t[mode - 1] = 0; } if (key_minus_pressed()) // 减键 { if (t[mode - 1] == 0) t[mode - 1] = 59; else t[mode - 1]--; } }注意 t 数组的顺序是秒、分、时、日、月、周、年,而 mode 从 1 到 6 正好对应秒到年。加键时只对分和时做了 59 的上限判断,日和月要根据实际月份天数做上限,否则会出现 2 月 30 日这种非法日期,写进 DS1302 后它虽然能存下,但后面显示或计算会有问题。最简单的处理是在切换到日月年设置时对 t 数组先做一次范围归一化,比如日超过 28 就按当月的最大天数截断。
状态机设计里最容易忽略的问题是:调时过程中 DS1302 还在走秒,主循环每次读回的 t 数组如果被按键修改后又被下一次读覆盖,设置会不生效。所以设置状态下主循环要暂停读 DS1302,改用本地数组累加,退出设置时才写回。上面的 key_scan 已经把 t 数组当本地变量使用,主循环里判断 mode 等于 MODE_RUN 才去读 DS1302,这样就不会互相干扰。
4.3 显示布局和星期处理
显示布局我常用两行:第一行显示星期和年月日,第二行显示时分秒。星期直接从 DS1302 读取,不用自己用日期反推,省掉蔡勒公式。读回来的星期已经是十进制,1 到 7 分别代表星期一至星期日。
void lcd_show_time(unsigned char *t) { unsigned char buf[16]; // 第一行: 2025-01-06 MON sprintf(buf, "%04d-%02d-%02d %s", 2000 + t[6], t[4], t[3], week_str[t[5]]); lcd_show_string(0, 0, buf); // 第二行: 23:59:59 sprintf(buf, "%02d:%02d:%02d", t[2], t[1], t[0]); lcd_show_string(0, 1, buf); }week_str 是一个字符串数组,下标从 1 开始,元素依次是 MON 到 SUN。主程序循环如下:
void main(void) { unsigned char t[7] = {0, 0, 12, 1, 1, 1, 25}; lcd_init(); ds1302_init(); ds1302_set_time(t); // 首次上电设置一次 while (1) { if (mode == MODE_RUN) { ds1302_get_time(t); } lcd_show_time(t); key_scan(t); } }首次上电直接写一个初始时间再进主循环,后面运行就靠按键调时更新。这里把 ds1302_init 放在 set_time 之前,因为初始化里已经清零 CH 位启动时钟,set_time 再写入具体时间,顺序上不要反过来。用 sprintf 拼显示串虽然方便,但 C51 环境下 printf 系列会占用较多代码空间,如果你用的型号 Flash 较小,建议改成手动拼字符;显示日期的横杠用字符 '-' 直接拼,注意 sprintf 在 C51 中默认不支持浮点和长整型,这里用的都是整型,没有问题。
5. 编译下载与五个常见避坑排查
5.1 Keil C51 建工程和 STC-ISP 冷启动下载
Keil 下建工程时选芯片型号这步比较容易出错:选 STC15W408AS 这类 STC 系列需要先在器件库里有对应型号,没有的话可以选一个兼容的 8051 内核型号,然后手动在 Options for Target 里把晶振频率改成实际主频。STC15 是 1T 内核,同主频下多数指令执行速度是传统 51 的 6 到 12 倍,软件延时函数里依赖 NOP 次数的地方都要重新标定,不能直接照搬 89C52 的延时库。
下载用 STC-ISP 软件,流程是:打开生成的 hex 文件,选择串口和波特率,点“下载/编程”,然后再给开发板上电,也就是常说的冷启动。STC15 的 ISP 引导在每次上电复位的最初几百毫秒内检测串口下载命令,所以必须先让电脑进入等待下载状态,再给单片机通电。如果板子已经在上电状态,点下载按钮只会一直等握手,这是很多人第一次烧录的血泪经验。波特率建议先用 9600 或 2400 这类低速档,避免劣质 USB 转串口在高波特率下丢字节导致握手失败。
代码里还有一类编译冲突必须提前避开:不要同时 include reg51.h 和 STC15.H。STC15.H 已经包含了 SFR 定义和位定义,再引入 reg51.h 会对同一个 SFR 产生重复定义,直接编译报错。通用做法是只保留 STC15.H,遇到个别寄存器名冲突时再手动处理。
5.2 避坑排查:下载握手超时
现象:STC-ISP 一直停在“正在检测目标单片机”或类似的等待状态,板子反复上电也进不了下载。
原因:最常见是冷启动顺序反了,板子先上电导致 ISP 引导已经错过检测窗口;其次是串口接错,STC15 的下载串口是 P3.0(RXD)和 P3.1(TXD),有人把 USB 转串口的 TXD 和 RXD 接反,下载软件收不到应答。
解决:先把板子断电,在 STC-ISP 里选好 hex 和串口后点下载,再给板子上电,重复两三次一定能进。接反的情况把 TXD 和 RXD 对调即可。还有一个容易被忽略的点:如果你用的是开发板上的 USB 转串口芯片,要确认它的 TXD 确实接到了单片机的 RXD 而不是另一组引脚。
5.3 避坑排查:LCD1602 显示花屏或只有黑块
现象:第一行全是黑块,或者字符乱码,背光亮但内容不对。
原因:对比度电阻没调好是最常见的,LCD1602 的 VO 引脚悬空或电压不对直接导致显示异常;其次是数据线接错或四线模式下的 D0~D3 悬空未处理;还有一种是 STC15 的 P0 口用作数据线时没有配置引脚模式,高阻输入状态驱动不了 LCD。
解决:VO 引脚接一个 10k 电位器,上电后调到屏幕出现清晰内容且无黑块的位置。四线模式下 D0~D3 建议全部接地,防止内部读到噪声。检查接线和初始化序列后还不行,把初始化延时放大到原来的 10 倍,很多情况下是上电时序不够稳定,LCD 没完成内部复位。
5.4 避坑排查:时间不走或断电清零
现象:刚写好时间,秒数一直停在初始值不动;或者断电再上电,时间回到 2000 年 1 月 1 日。
原因:秒寄存器 CH 位还是 1,时钟被停止,初始化里没写入 0x00 启动;断电清零则是 VCC1 电池没接或接反,DS1302 在主电源消失后没有任何供电来源,寄存器内容直接丢失。还有一部分情况是晶振没起振,32.768kHz 晶振的两脚虚焊,时间一样不走。
解决:初始化里确保对秒寄存器写 0x00,这个动作写一次就够了,不用每次读时间都重写。电池用万用表量一下电压,CR2032 新电池应该在 3V 以上。晶振虚焊的判断方法是换一个 DS1302 模块测试,模块自带晶振和电容,能排除大部分焊接问题。补充一个经验:DS1302 的时间寄存器只要供电正常就能自走,不需要单片机持续给它喂数据,所以程序卡死也不影响走时,这是外挂 RTC 方案相对纯定时器的最大优势。
5.5 避坑排查:按键没有反应或连续跳
现象:按下按键后调时无反应,或者按一次跳好几格。
原因:按键 IO 没有配置上拉,STC15 某些端口处于高阻或开漏模式时按键电平不稳定;消抖时间太短或缺少松手检测,导致一次物理按压被当成多次触发;长按功能逻辑写错,把长按连加和短按修改混在一起。
解决:按键接的 IO 在初始化时确认是准双向口,否则外部加 10k 上拉电阻。消抖延时建议 15ms 左右,同时检测“按下—松开”完整过程,只在松开时执行一次动作,这样短按就稳定了。长按连加单独用一个计时变量,在按下超过 500ms 后每 100ms 自动加一次值,不要和短按共用同一个判断分支。
5.6 避坑排查:每天快慢几十秒
现象:万年历正常运行,但和手机对表发现一天能差出半分钟到一分钟。
原因:DS1302 的时间基准来自外部 32.768kHz 晶振,晶振本身的频率精度和负载电容不匹配会造成系统误差。模块自带的晶振往往没有经过负载电容校准,更有甚者直接拿普通晶振或谐振器替代,误差自然大。室温变化也会影响晶振频率,但短时间观察不明显。
解决:先连续记录三天的误差,确定是快还是慢、每天快多少秒。误差在 30 秒以内可以通过软件补偿修正,在整点校时时把误差匀回给秒寄存器;误差超过 1 分钟,优先换一颗正规 32.768kHz 晶振,并检查两个负载电容是否匹配。这个问题的排查思路放在整个项目的最后做,因为前期电路和代码没稳定时测出来的误差没有参考价值。软件补偿的具体做法放在下一章展开。
6. 进阶:断电保持、涓流充电与软件校准
万年历做完能显示能调时,只能算跑通;真正决定这个作品能不能常年放在桌面上用的,是断电保持和走时精度这两个指标。
断电保持方面,DS1302 的 VCC1 电池座已经解决大部分问题,但要注意涓流充电的控制。初始化里写 0xA5 表示 1 个二极管加 2k 电阻,这是一般模块的推荐值,充电电流小,适合给可充电电池浮充。如果你的电池座里装的是可充电的 LIR2032,2k 电阻没问题;如果装了 CR2032 这种一次性锂电池,涓流充电建议直接关闭,也就是 0x90 写 0x00,防止长期充电导致电池鼓包。我的习惯是:用一次性电池就不开充电,用可充电电池才开 0xA5 或 0xBB(1 个二极管加 4k 电阻,电流更小)。
精度校准这块有一个很实用的方式:利用 STC15 的串口把当前时间每分钟输出一次,和电脑时间对比,连续记录 24 小时得到准确日误差。然后把这个误差值写进 STC15 内部 EEPROM,在整点校时时做一次补偿。比如实测每天快 5 秒,就在每天零点把秒寄存器减 5。实现上不用动分钟,直接对秒寄存器写一个调整后的值,然后再恢复正常读取流程。硬件上如果换用带温度补偿的晶振模块则更省事,但成本会上去。另外还有一种做法是在主循环里用一个 16 位变量累计毫秒计数,利用 STC15 主频相对稳定的特性做日误差微调,不过这个方案受温度影响大,不如直接改 DS1302 寄存器干净。
另一个进阶方向是把 DS1302 去掉,直接用 STC15F2K60S2 这类带内部 RTC 的型号,配合外部 32.768kHz 晶振实现走时。这么做的代价是省一颗芯片,但功耗和掉电保持都要自己处理:内部 RTC 靠外部晶振走时,掉电时要么给单片机整机供电,要么进入低功耗模式,逻辑上比外挂 DS1302 复杂不少。对课程设计来说,外挂 DS1302 是更稳的方案,专用 RTC 芯片反而让项目更可靠。
我自己的习惯是:万年历做好之后先连续跑一周,每天固定时间用手机对一次表,把误差记下来再决定要不要动补偿系数。板上留一个 3 针的串口下载接口,升级固件不用拆芯片,能省掉很多返工时间。做到这一步,这个基于 STC15 的万年历才算真正收官,希望帮到你。
本文还有配套的精品资源,点击获取