简介这份LCD1602万年历资源包面向单片机C51学习者和电子爱好者完整演示了DS1302实时时钟、LCD1602液晶显示、I2C/EEPROM存储、温度监测与红外遥控的综合应用。压缩包内含78个文件约1.41MB包括C51源文件与配套头文件、编译生成的hex/obj/lst/log等工程文件以及LCD1602显示时钟接线图jpg便于对照硬件连接与工程构建。项目涵盖闰年自动处理、温度预警P1.7口LED闪烁、红外按键自定义等功能模块资源目录按模块拆分清晰适合需要从零搭建多功能时钟或进阶学习单片机外设通信的开发者。该资源已有289人学习代码结构清晰、模块化程度高是一套可直接用于实操和二次开发的入门到进阶案例。1. LCD1602万年历一块小屏加一颗时钟芯片为什么值得动手LCD1602万年历说白了就是用一块十六字符两行的液晶屏配一颗实时时钟芯片在单片机上做一个断电不丢时间、能显示年月至秒的桌面时钟。这个项目常年不冷不是因为它新鲜而是它把嵌入式入门最需要的三件事一次凑齐了IO操作、通信时序、按键交互。做完它你手里就多了一个能用的桌面摆件而不是一个烧完程序就往抽屉里扔的点灯板。适合谁来动手一类是学过点 C 语言、想从点灯迈向综合项目的新手另一类是做过一些模块实验、但没试过把三四个模块拼成一个完整产品的入门开发者。这个项目的价值不在电路有多难而在你能亲眼看懂“屏幕为什么亮、时间为什么走、按键为什么响应”这三件事。做成之后往后不管做 OLED 菜单还是做温湿度计这套代码骨架都能直接复用。2. 显示与计时方案LCD1602 的工作原理和三种时钟方案怎么选做 LCD1602 万年历之前有两个选型问题必须先定下来屏幕怎么接时钟芯片用哪个。这两个决定看着不起眼实际会影响整个代码结构和后期排错难度。尤其是时钟方案很多人随便抓一个 RTC 模块就焊上去结果一个月后时间差出十几分钟再回来改电路成本比一开始选对高得多。2.1 LCD1602 能显示什么字符映射、DDRAM 地址和 4 线模式的取舍LCD1602 使用的是 HD44780 或兼容控制器内部有一块 DDRAM每个地址对应屏幕上的一个字符位置。第一行地址从 0x00 到 0x0F第二行从 0x40 到 0x4F。往某个地址写一个 ASCII 码对应位置就显示这个字符。它没有中文字库只有 ASCII 字符、日文假名和少量希腊字母所以万年历里的“星期几”一般显示成 SUN、MON 这种英文缩写或者自己用 CGRAM 造字。控制这块屏有两种接法8 线模式一次传一个字节4 线模式分两次传半个字节。8 线模式代码简单但要占 8 个数据引脚外加 RS、RW、EN一共 11 个 IO4 线模式只占 4 个数据引脚加控制脚一共 7 个。我一般做项目都选 4 线模式不是因为省那 4 个引脚而是后续大概率要在这个板子上再挂 DS18B20、按键、蜂鸣器甚至 OLEDIO 资源能省一点是一点。4 线模式唯一的代价是初始化和写数据时要把一个字节拆成高低两半代码多几行时序上多一次脉冲但这点开销对单片机完全无感。还有个常见误区是把 RW 引脚直接接低电平固定为写模式开发阶段没问题但如果你后面想用“读忙标志位”来判断 LCD 是否准备好RW 就得接到 IO 上。新手阶段建议直接接低少一根线少一个故障点。2.2 时钟方案选型DS1302、DS3231 还是主控软件计时万年历对时间精度的要求是“一个月不差出一分钟”这个底线不算苛刻但也不是随便能达标的。常见做时钟的方案有三种我自己按需求这样分练手课设选 DS1302成品和追求精度选 DS3231临时凑合才用主控软件计时。方案接口精度表现掉电续航成本适用场景DS1302三线CE/SCLK/IO取决于晶振质量常见日差从几秒到几十秒CR2032 可撑数年低练手、课程设计、普通桌面时钟DS3231I2C内置温补晶振年误差约一分钟内电池可维持高约 DS1302 的数倍成品、需要温度显示、对走时精度有要求的项目主控软件计时无受主晶振和中断响应影响误差大断电即重置几乎为零临时显示、实验演示DS1302 是经典入门选择三线协议简单代码只有几十行掉电后靠一颗 CR2032 电池维持走时。它的精度问题几乎全出在外部 32.768kHz 晶振上网上买到的模块很多配的是 12.5pF 负载电容晶振而 DS1302 手册建议的是 6pF 左右用错晶振会出现一天快几十秒甚至几分钟的情况。这个问题后面避坑章会细说选型时只要记住一条买晶振时认准 6pF 负载电容。DS3231 是另一个极端自带温补晶振和温度传感器精度碾压 DS1302但价格贵几倍而且模块体积大。如果这个万年历是要长期摆在桌面上用的我建议直接上 DS3231多花的钱换来的是不需要反复校时。如果只是练手、过一遍流程DS1302 完全够。主控软件计时是很多新手想省芯片时的第一反应用一个定时器中断累加秒数。这个方案在实验桌上看着能跑实际没法用单片机主晶振不是频率基准误差受温度、电压影响而且掉电时间直接清零。万年历如果连断电保持都做不到那还不如用手机看时间。所以我的结论很简单这个项目至少要用 DS1302 起步预算允许就 DS3231软件计时不要碰。3. 接线与点亮屏幕引脚对照、最小系统和 LCD 初始化时序确定方案之后动手第一步不是写代码而是把线接对。LCD1602 万年历的硬件说简单也简单但屏幕 16 个引脚、时钟芯片 8 个引脚新手第一次对着排针容易眼花。这一章给出一份可以直接照着接的引脚对照表再把 LCD 初始化时序拆开讲一遍。3.1 硬件接线表LCD1602 与 DS1302 怎么接到主控下面这套接线按最常见的开发板来排数据线用 P2 口接 LCDDS1302 用 P1 口。具体引脚宏在代码里都有定义换成其他型号芯片时只要改头文件里的 sbit 声明即可。LCD1602 引脚功能接到哪里备注1VSSGND电源地2VDD5V逻辑电源3VO10K 电位器滑臂对比度调节不接基本看不到字4RSP2.00命令1数据5RWGND固定为写模式省一个 IO6ENP2.2使能脉冲每次都靠它把数据锁存进去7~10D0~D3悬空4 线模式下不用可接地11D4P2.4数据线接反会乱码12D5P2.5数据线13D6P2.6数据线14D7P2.7数据线15A串 100Ω 电阻接 5V背光正极16KGND背光负极DS1302 那边相对简单VCC2 接 5VVCC1 接 CR2032 电池正极GND 接地X1 和 X2 接 32.768kHz 晶振CE、SCLK、IO 分别接 P1.2、P1.1、P1.0。这里有一个细节DS1302 的 VCC2 和 VCC1 之间有切换电路主电源有电时用它供电主电源掉电才切到电池。所以千万不要把电池直接并到 VCC2 上那样电池会一直在放电。布线上要注意三点。第一晶振两根引脚到 DS1302 的走线越短越好尽量靠近芯片焊很多时间不走的问题就是晶振引脚上的杂散电容太大导致不起振。第二DS1302 的 VCC 和 GND 之间放一个 0.1uF 去耦电容位置靠近芯片。第三如果用的是普通 CR2032 不可充电电池绝对不要去配置 DS1302 的涓流充电寄存器否则电池有漏液风险。市面上也有可充电的 LIR2032那是配合涓流充电用的新手不建议碰。3.2 从 8 位到 4 位模式LCD1602 初始化代码与调试顺序LCD1602 上电后默认工作在 8 位模式想切到 4 位模式必须在初始化时按手册要求的顺序发特定指令。这个握手过程做错后面写什么都是乱码。我习惯把初始化拆成两段先发三次 0x03 并各延时 5ms告诉屏幕“我要用 8 位方式和你说话”再发一次 0x02表示“接下来切到 4 位模式”。这个顺序不能跳也不能用太短的延时。/* 引脚定义按你自己的板子改 */ sbit LCD_RS P2^0; sbit LCD_RW P2^1; sbit LCD_EN P2^2; sbit LCD_D4 P2^4; sbit LCD_D5 P2^5; sbit LCD_D6 P2^6; sbit LCD_D7 P2^7; void DelayMs(unsigned int ms) { unsigned int i, j; for (i 0; i ms; i) for (j 0; j 125; j); /* 12MHz 下约 1ms按你的晶振频率微调 */ } /* 向 LCD 发送半个字节高 4 位或低 4 位 */ void LcdWriteNibble(unsigned char val, unsigned char rs) { LCD_RS rs; /* rs0 发命令rs1 发数据 */ LCD_D4 (val 0) 0x01; LCD_D5 (val 1) 0x01; LCD_D6 (val 2) 0x01; LCD_D7 (val 3) 0x01; DelayMs(1); LCD_EN 1; /* EN 高脉冲LCD 在上升沿锁存数据 */ DelayMs(1); LCD_EN 0; } /* 发送一个完整命令先高 4 位后低 4 位 */ void LcdWriteCmd(unsigned char cmd) { LcdWriteNibble(cmd 4, 0); DelayMs(1); LcdWriteNibble(cmd 0x0F, 0); DelayMs(2); /* 命令执行需要时间不能省 */ } /* 发送一个数据字节用于显示字符 */ void LcdWriteData(unsigned char dat) { LcdWriteNibble(dat 4, 1); DelayMs(1); LcdWriteNibble(dat 0x0F, 1); DelayMs(1); } /* 初始化时序严格按手册的先后顺序来 */ void LcdInit(void) { DelayMs(40); /* 上电等待 40ms */ LcdWriteNibble(0x03, 0); /* 第一次 8 位模式握手 */ DelayMs(5); LcdWriteNibble(0x03, 0); /* 第二次 */ DelayMs(5); LcdWriteNibble(0x03, 0); /* 第三次 */ DelayMs(5); LcdWriteNibble(0x02, 0); /* 切换到 4 位模式 */ LcdWriteCmd(0x28); /* 4 位、2 行、5x7 点阵 */ LcdWriteCmd(0x0C); /* 显示开、光标关、不闪烁 */ LcdWriteCmd(0x06); /* 写入后地址自动加 1 */ LcdWriteCmd(0x01); /* 清屏需要较长延时 */ DelayMs(5); LcdWriteCmd(0x80); /* 光标定位到第一行第一列 */ }这段代码的关键点是每一个命令发出后都要有足够延时。尤其是清屏指令 0x01手册上写的执行时间在 1.6ms 以上实际使用中我给的延时通常放到 5ms否则在快速刷新时会偶发花屏。很多人把这里改成延时几十微秒屏幕能亮但字符会随机错位排查起来非常头大。调试顺序也有讲究。焊完第一件事不是烧程序而是上电后先拧 VO 电位器从一端慢慢拧到另一端正常能看到第二行的方块由深变浅。如果怎么拧都没有方块先查背光 A 和 K 两个引脚是否接反再看 VDD 有没有 5V。屏幕能出方块之后再烧初始化程序这样能快速把“硬件没焊好”和“代码时序不对”分开。从 8 位切到 4 位这个过程用逻辑分析仪抓 EN 引脚最直观没有逻辑分析仪就靠眼睛看初始化后屏幕上是否出现干净的两行空格。出现乱码第一反应检查数据线有没有接错位而不是反复改延时。4. 万年历核心代码读 DS1302 时间、拼显示和按键调时硬件点亮之后真正的重头戏是让 DS1302 把时间读出来并显示到 LCD 上。DS1302 的通信协议比 I2C 简单三根线就能完成读写但它也有自己的规矩命令字节的结构、BCD 码的转换、写保护位的处理每个环节都可能让时间显示成乱码或者干脆不走。这一章从底层读写函数开始一层层写到主循环。4.1 用 IO 模拟三线协议DS1302 读写函数怎么写DS1302 与单片机之间通过 CE、SCLK、IO 三根线通信类似 SPI 但又不完全一样。它的命令字节由“固定控制位 寄存器地址 读写标志”组成常见的映射关系是读秒用 0x81写秒用 0x80读分用 0x83写分用 0x82。规律是地址左移一位后最低位表示读写方向。下面这段是完整的底层函数所有上层操作都建立在它上面。sbit RTC_CE P1^2; sbit RTC_SCK P1^1; sbit RTC_IO P1^0; /* 向 DS1302 写一个字节低位先出 */ void RtcWriteByte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { RTC_IO dat 0x01; /* 先送最低位 */ dat 1; RTC_SCK 1; /* SCLK 上升沿DS1302 采样数据 */ DelayUs(2); RTC_SCK 0; DelayUs(2); } } /* 从 DS1302 读一个字节 */ unsigned char RtcReadByte(void) { unsigned char i, dat 0; RTC_IO 1; /* 引脚置 1释放总线准备读外部电平 */ for (i 0; i 8; i) { dat 1; RTC_SCK 1; DelayUs(2); if (RTC_IO) /* SCLK 高电平期间读 IO 引脚 */ dat | 0x80; RTC_SCK 0; DelayUs(2); } return dat; } /* 写寄存器拉高 CE发命令发数据拉低 CE */ void RtcWriteCmd(unsigned char cmd, unsigned char dat) { RTC_CE 0; RTC_SCK 0; RTC_CE 1; /* CE 拉高后整个读写周期内保持高 */ RtcWriteByte(cmd); /* 先发命令字节 */ RtcWriteByte(dat); /* 再发数据字节 */ RTC_CE 0; /* 结束防止误触发写入 */ } /* 读寄存器拉高 CE发命令读数据 */ unsigned char RtcReadCmd(unsigned char cmd) { unsigned char dat; RTC_CE 0; RTC_SCK 0; RTC_CE 1; RtcWriteByte(cmd); dat RtcReadByte(); RTC_CE 0; return dat; }几个容易忽略的细节。第一读数据前要把 IO 引脚置 1因为常见的 51 单片机端口是准双向口不写 1 直接读的话外部拉低能被读到但外部拉高时可能读成 0导致读出来的数据全是 0x00 或者 0xFF。第二CE 要在整个字节传输期间保持高电平读完立刻拉低这样能避免 DS1302 在噪声干扰下误判为写操作。第三SCLK 的延时不需要特别精确几十微秒级别即可但不要省略因为 DS1302 的输入时钟频率有限制SCLK 太快会采样出错。4.2 BCD 码不是直读的时间读取、格式化和首次上电写入DS1302 内部寄存器存储的时间是 BCD 码也就是一个字节的高四位存十位、低四位存个位。比如 23 这个数字BCD 码是 0x23而不是十进制的 0x17。直接把这个值送去 LCD 显示会出现“12 34”这类奇怪结果。所以读出来的每个寄存器都要做一个转换写入前也要把十进制转回 BCD。这个转换是所有 RTC 芯片的通用套路不只是 DS1302。typedef struct { unsigned char year; /* 0~99存储 2000 年后的年份偏移 */ unsigned char month; /* 1~12 */ unsigned char day; /* 1~31 */ unsigned char week; /* 1~7 */ unsigned char hour; /* 0~23 */ unsigned char minute; /* 0~59 */ unsigned char second; /* 0~59 */ } RtcTime; unsigned char DecToBcd(unsigned char d) { return ((d / 10) 4) | (d % 10); } unsigned char BcdToDec(unsigned char b) { return (b 4) * 10 (b 0x0F); } /* 一次读全部时间到结构体 */ void RtcGetTime(RtcTime *t) { t-second BcdToDec(RtcReadCmd(0x81)); t-minute BcdToDec(RtcReadCmd(0x83)); t-hour BcdToDec(RtcReadCmd(0x85)); t-day BcdToDec(RtcReadCmd(0x87)); t-month BcdToDec(RtcReadCmd(0x89)); t-week BcdToDec(RtcReadCmd(0x8B)); t-year BcdToDec(RtcReadCmd(0x8D)); } /* 首次上电设置时间之后一般只通过按键调时 */ void RtcSetTime(RtcTime *t) { RtcWriteCmd(0x8E, 0x00); /* 0x8E 是写保护寄存器先关写保护 */ RtcWriteCmd(0x80, DecToBcd(t-second)); /* 先写秒最低位为 0 表示启动时钟 */ RtcWriteCmd(0x82, DecToBcd(t-minute)); RtcWriteCmd(0x84, DecToBcd(t-hour)); RtcWriteCmd(0x86, DecToBcd(t-day)); RtcWriteCmd(0x88, DecToBcd(t-month)); RtcWriteCmd(0x8A, DecToBcd(t-week)); RtcWriteCmd(0x8C, DecToBcd(t-year)); RtcWriteCmd(0x8E, 0x80); /* 重新打开写保护防止误写 */ }读时间的代码很简单但第一次烧录时有一个坑全新的 DS1302 芯片出厂时时钟是停止的秒寄存器的最高位 CH 默认为 1。如果不做任何处理直接读时间秒位永远是 00 或者不动。所以首次上电必须调用一次 RtcSetTime把秒写成一个 CH 位为 0 的值。上面代码里先写秒再写其他寄存器就是为了让时钟尽早跑起来。写保护寄存器 0x8E 在每次写寄存器前要关闭写完立刻打开这是 DS1302 的防误写机制很多时间写着写着丢了就是漏了这一步。4.3 按键调时与边界处理从修改秒到闰年判断万年历如果没有调时功能只能算个时钟不能算产品。按键调时有一个很关键的工程习惯先把要改的位调到一个字段短按加减长按快速进位最后再统一写回 DS1302。我一般用三个按键一个切换调整字段一个加一个减。调整项按“年、月、日、时、分、秒”循环第二个按键每次加 1 并做边界回绕到 23 加 1 回 0到 59 加 1 回 0。/* 按键引脚定义 */ sbit KEY_SET P3^0; sbit KEY_UP P3^1; sbit KEY_DOWN P3^2; unsigned char adjustItem 0; /* 0年 1月 2日 3时 4分 5秒 */ /* 月份天数表2 月后面按闰年判断替换 */ code unsigned char MonthDays[] {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; unsigned char IsLeapYear(unsigned char year) { unsigned int y 2000 year; return (y % 4 0 y % 100 ! 0) || (y % 400 0); } void AdjustValue(unsigned char dir) { RtcTime t; unsigned char maxVal 0; RtcGetTime(t); switch (adjustItem) { case 0: maxVal 99; break; case 1: maxVal 12; break; case 2: maxVal MonthDays[t.month]; if (t.month 2 IsLeapYear(t.year)) maxVal 29; break; case 3: maxVal 23; break; case 4: maxVal 59; break; case 5: maxVal 59; break; } if (dir 1) { if (adjustItem 0) t.year; else if (adjustItem 1) t.month; else if (adjustItem 2) t.day; else if (adjustItem 3) t.hour; else if (adjustItem 4) t.minute; else t.second; } else { if (adjustItem 5 t.second 0) t.second 59; else { if (adjustItem 0 t.year 0) t.year--; else if (adjustItem 1 t.month 1) t.month--; else if (adjustItem 2 t.day 1) t.day--; else if (adjustItem 3 t.hour 0) t.hour--; else if (adjustItem 4 t.minute 0) t.minute--; else if (adjustItem 5 t.second 0) t.second--; } } /* 超出上限回绕到 0 或 1 */ if (adjustItem 0 t.year maxVal) t.year 0; if (adjustItem 1 t.month maxVal) t.month 1; if (adjustItem 2 t.day maxVal) t.day 1; if (adjustItem 3 t.hour maxVal) t.hour 0; if (adjustItem 4 t.minute maxVal) t.minute 0; if (adjustItem 5 t.second maxVal) t.second 0; RtcSetTime(t); } void KeyScan(void) { if (!KEY_SET) { /* 按键按下为低电平 */ DelayMs(20); /* 软件消抖 */ if (!KEY_SET) { adjustItem; if (adjustItem 5) adjustItem 0; while (!KEY_SET); /* 等待松手防止连跳 */ } } if (!KEY_UP) { DelayMs(20); if (!KEY_UP) { AdjustValue(1); while (!KEY_UP); } } if (!KEY_DOWN) { DelayMs(20); if (!KEY_DOWN) { AdjustValue(0); while (!KEY_DOWN); } } }这段代码里最容易翻车的是小时寄存器。DS1302 小时寄存器的最高位是 12/24 小时制标志写小时时如果忽略了掩码可能把这一位状态改掉导致 12 小时制和 24 小时制乱跳。上一章 RtcSetTime 里写的 0x84 地址在写入前建议加一个掩码DecToBcd(t-hour) 0x7F确保最高位永远是 0强制 24 小时制。调秒也有一个习惯对时一般把秒清零最简单不要手动去调秒去追手表直接在整点整分时按下 SET 切到秒字段按一下 UP 让秒变成 00 就行。主循环把这几块串起来只需要十几行。注意不要每次都调用 LcdWriteCmd 清屏那样屏幕会闪烁得很厉害。我一般 250ms 刷新一次读写时间只有数值真的变了才重绘对应位置这样屏幕干净代码也简单。void main(void) { RtcTime t; LcdInit(); RtcSetTime(t); /* 首次烧录需要设置初始时间之后可注释掉 */ while (1) { RtcGetTime(t); LcdShowTime(t); KeyScan(); DelayMs(250); } }5. 常见问题排查黑屏、乱码、时间不走和不保存这个项目做到最后你会发现绝大多数问题出在硬件细节和时序假设上而不是逻辑。下面这几条都是我反复见过的典型故障每一条按“现象、原因、解决”的顺序写方便你对照排查。5.1 屏幕不亮或只有黑色方块现象背光不亮或者背光亮了但屏幕上只有两行黑色方块写什么命令都没反应。原因通常有两个VO 对比度没调对或者 LCD 根本没有收到有效时序。很多新手把 VO 直接接地或悬空这会导致屏幕要么全黑要么全白看起来像坏了。解决先把 10K 电位器接上 VO用万用表确认 VDD 和 VSS 之间确实是 5V。然后一边拧电位器一边观察屏幕方块从无到有再到消失说明显示部分正常。如果怎么拧都是深色方块不变用示波器或逻辑分析仪看 EN 引脚是否有脉冲没有脉冲就是程序没跑到初始化。还有个小经验开发板供电不足时背光一开屏幕就闪这时给 VDD 和 GND 之间并联一个 100uF 电容能解决大部分电源纹波问题。5.2 显示乱码与字符错位现象上电后第一行是满屏乱码或者明明写的是数字显示出来却是个别符号。原因一般不是代码逻辑错了而是 4 线模式握手没完成或者 D4 到 D7 四根数据线接的顺序有误。屏幕在 8 位模式下收到了半截数据自然解析出乱码。解决先检查数据线。用万用表通断档从单片机引脚到 LCD 引脚逐根量一遍很多乱码问题就是两根线在杜邦线端插反了。排除接线后再查初始化时序重点确认三次 0x03 之间的延时是不是足够 5ms。有一个很容易忽略的点切换 4 位模式后的第一条命令 0x28 分两次发送中间要留出至少 1ms 间隔很多教程代码这里用的延时太短屏幕偶发乱码。5.3 时间走得飞快或者干脆不走现象刚对好时半天快了几分钟或者秒位偶尔跳一下然后停住。原因大概率是 DS1302 的晶振不匹配。DS1302 手册推荐的是负载电容 6pF 的 32.768kHz 晶振市面上大量模块配的是 12.5pF 的通用晶振频率偏高走时会明显偏快。不走的情况则多半是晶振没起振或者 CE、SCLK、IO 三根线接错。解决优先买标称 6pF 的晶振替换。手头只有 12.5pF 也不是不能用在 X1 和 X2 对地各并一个 6~10pF 的电容做微调能拉回一部分误差。没有示波器时用手机秒表对时然后跑 24 小时算出日偏差值再决定要不要换晶振。如果时间完全不走先量 DS1302 的 VCC2 有没有 5V再用万用表频率档去测 X1 引脚能测到 32kHz 左右说明起振了。5.4 断电重启后时间不保存现象程序烧好时时间正常一旦断电再上电就回到初始值。原因有两个方向。第一CR2032 电池没接对VCC1 悬空或接反了DS1302 在主电源掉电后没有后备电源。第二写保护流程有问题初始化时根本没执行过 RtcSetTime芯片里的时间是默认值上电后读到的自然不是真实时间。解决用万用表量 VCC1 对 GND 的电压应该有 3V 左右。注意电池正极接 VCC1负极接 GND接反了不仅不保存还可能把芯片搞坏。程序上确认首次烧录执行过一次 RtcSetTime并且里面最后把写保护打开了。有一个需要提醒的点如果你用带涓流充电的模块且电池是不可充电的 CR2032一定要避免写 0x90 寄存器开启涓流充电否则电池会鼓包甚至漏液。5.5 调时之后秒被连带改掉现象只想把分钟从 05 调到 06结果秒变成 00或者小时设置完变成 13 点。原因按键调速时没有统一先读一次完整时间再改而是直接对某个寄存器做了写入导致其他字段被覆盖成上次读到的旧值。另一个原因是写小时寄存器时改了 12/24 小时制标志位。解决任何一次调整都先 RtcGetTime 读回整个结构体改完一个字段再整体 RtcSetTime 写回。写小时时给最高位加掩码 0x7F。调秒字段的抖动最好也处理一下按键消抖延时 20ms 只解决机械抖动如果发现按一下秒跳两到三秒多半是 while 等待松手的循环没写成死等加一个超时退出就能解决。6. 让万年历更可靠走时精度验证与三个实用改造屏幕能亮、时间能走、按键能调这只是开始。真正决定这个项目能不能长期摆在桌面上是走时精度。我验证精度的方法很简单用电脑或手机上的时间校准到秒记下当前差几秒跑 24 小时后再对一次算出日差。DS1302 配 6pF 晶振正常日差在 1 秒以内如果日差超过 5 秒先检查晶振再考虑软件补偿。软件补偿的做法是每天固定快 0.8 秒的话在程序中用一个计数器每 75 秒人为减 1 秒但这是补救措施不要一开始就依赖它。三个改造方向都比较实用。第一是加温度显示DS3231 自带温度传感器DS1302 则需要外接 DS18B20 单总线传感器数据线接一个 IO 加 4.7K 上拉电阻读取逻辑不复杂显示在第二行时间后面。第二是加农历显示这个需要一张公历转农历的表覆盖 2000 到 2099 年即可每字节压缩存储月份大小和闰月信息查表计算后把农历日期显示在 LCD 第一行末尾。第三是加整点报时和闹钟用定时器中断维护一个秒计数到整点触发蜂鸣器响一声比在主循环里判断时间可靠得多因为主循环里的 DelayMs 会让按键和显示时间互相干扰。我个人的习惯是每次做完硬件改动先对时再封壳跑三天看累计误差。补一句血泪经验早先贪便宜用过一批杂牌晶振日差普遍在 20 秒以上后来换了正规货源才稳定下来。做电子万年历晶振的钱不能省这是整个项目里最影响体验的一个原件。希望帮到你。本文还有配套的精品资源点击获取