网上找 51 万年历的资料,翻来覆去就那几种:1602 只显示个时间、没有农历、代码贴一半就没了下文,或者干脆只给个原理图不给代码。找得挺烦,干脆自己写了一套完整的。
这块屏是 LCD12864B(ST7920 自带中文字库),时钟用 DS1302,温湿度是 DHT11,另外做了农历换算、闹钟,闹钟时刻还能掉电保存。Proteus 里从接线到点亮完整跑通,9 个模块 1500 行左右
这篇先把效果和功能全部演示一遍,后面再按模块逐个拆开讲。
一、先上效果图![]()
整机跑起来之后的状态:屏幕四行都有内容,DS1302 持续走时,DHT11 间隔回传湿度。连续跑了十几分钟没有卡死或者花屏。
农历是这个项目里我觉得最值钱的部分 —— 它不是硬编码进去的几个日期,是 1901–2100 年查表算出来的,闰月也处理了。网上带农历的 51 万年历,成体系的很少。
二、主要功能
能跑的、有瑕疵的、没做的,三档全列 ——尤其最后一档,别人的教程一般不写。
已跑通,能演示
- 公历年月日、时分秒(DS1302)
- 中文星期一到星期日
- 农历月日(1901–2100 查表,含闰月)
- 湿度
%RH(DHT11 单总线)
- 闹钟掉电记忆(存在 DS1302 的 NV RAM,断电再来还是原来那个点)
- 闹钟到点蜂鸣器响(P2.6)
- 12/24 小时制一键切换(K4,直接改 DS1302 小时寄存器的标志位)
- 全中文界面,字库 ST7920 自带,不用自己取模
三、硬件清单与接线
模块 | 型号 | 接法 |
主控 | AT89C52(仿真)/ STC89C52(实物) | 12MHz |
显示 | LCD12864B(ST7920) | 数据 P1,RS=P2.3 RW=P2.4 E=P2.5PSB=P2.0 |
时钟 | DS1302 | SCLK=P3.6 IO=P3.5 CE=P3.4 |
温湿度 | DHT11 | P2.7 |
按键 | 4 路独立按键 | P3.1 P3.0 P3.2 P3.3 |
蜂鸣器 | 无源 | P2. |
四、屏幕布局与 DDRAM 地址
这张表建议直接收藏。ST7920 的 DDRAM 地址是跳着排的,搞不清就会一头撞进"字跑到别的行去"。
行 | 地址范围 | 内容 |
第一行 | 0x80–0x87 |
|
第二行 | 0x90–0x97 | 时 : 分 : 秒 + 星期 |
第三行 | 0x88–0x8B | 农历 月 日 |
第三行(后半) | 0x8C–0x8F |
|
第四行 | 0x98–0x99 | 温度 + 单位 |
第四行(后半) | 0x9D–0x9E | 湿度 + |
按键功能
按键 | 引脚 | 运行模式 | 调时模式 |
K1 | P3.1 | 进入调时 | 保存并退出(写回 DS1302 + NV RAM) |
K2 | P3.0 | 摄氏 / 华氏切换 | 切换选中位(0→8 循环) |
K3 | P3.2 | 无 | 当前位 +1(带大小月 / 闰年越界处理) |
K4 | P3.3 | 12/24 小时制切换 | 当前位 −1 |
调时模式下,选中的那一位 500ms 闪一次,靠 Timer0 的 1ms 定时 + 计数翻转实现。只有按 K1 退出时才写回 DS1302 和 NV RAM—— 调的过程中改的只是数组,绝不碰芯片。
五、核心代码:主循环
```c void main() { unsigned int dly; PSB = 1; // 并口模式,串行模式下屏不亮 for(dly = 0; dly < 30000; dly++); // 上电延时,等液晶内部复位完成 write_com(0x0C); write_com(0x30); // 8位并口基本指令集 write_com(0x06); Timer0Init(); DS1302_SetTime(); Alarm_Load(); // 上电从 NV RAM 读回闹钟时刻 while(1) { conversion_(DS1302_Time[0], DS1302_Time[1], DS1302_Time[2], &n_year, &n_yue, &n_ri); // 公历转农历 KeyNum = Key(); if(KeyNum == 1) // K1:模式切换 { if(MODE == 0){ MODE = 1; TimeSetSelect = 0; } else { MODE = 0; DS1302_SetTime(); Alarm_Save(); } } switch(MODE) { case 0: TimeShow(); break; // 正常运行:显示 + 闹钟比对 case 1: TimeSet(); break; // 调时:选中位闪烁 } } } ```这三行,每一行都是一个能单独写一篇的坑:
①PSB = 1必须拉高。LCD12864B 并口串口都支持,PSB 拉低就进串口模式。这时候你按并口时序往里写,屏幕一点反应都没有—— 不报错、不提示,就是一片黄。我第一次翻车就在这。
② 上电那 30000 次空循环,一个都不能省。液晶上电后内部复位要时间,主控跑得比它快,不等它就发指令,第一条直接丢。表现是偶尔点得亮、偶尔不亮,属于最难查的那种"时好时坏"。
③DS1302_SetTime()每次上电都会把初始时间写进去。初始值就是数组里那个2026-10-13 12:30:00。想让它真正掉电走时,正确姿势是:先烧一次带SetTime()的版本把时间写进去,再把这行注释掉、重新烧一次。DS1302 的常规操作,别看见时间不对就以为芯片坏了。
六、闹钟掉电保存:别浪费 DS1302 那 31 字节 NV RAM
大部分 DS1302 教程只讲到时钟寄存器就收手了。其实这颗芯片还白送你 31 字节 NV RAM,后备电池供电,掉电不丢 —— 不用白不用,拿来存闹钟时刻刚好。
地址从0xC0起,写命令是偶数地址(0xC00xC2…),读命令是奇数地址(0xC10xC3…)。我这的DS1302_ReadByte()内部已经做了Command |= 0x01,所以读的时候传同一个地址就行
```c #define DS1302_NV_ALARM_HOUR 0xC0 // NV RAM 第 0 字节:闹钟时 #define DS1302_NV_ALARM_MIN 0xC2 // NV RAM 第 1 字节:闹钟分 void Alarm_Save(void) { DS1302_WriteByte(DS1302_NV_ALARM_HOUR, DS1302_Time[7]); DS1302_WriteByte(DS1302_NV_ALARM_MIN, DS1302_Time[8]); } void Alarm_Load(void) { unsigned char h, m; h = DS1302_ReadByte(DS1302_NV_ALARM_HOUR); m = DS1302_ReadByte(DS1302_NV_ALARM_MIN); // 首次上电或后备电池耗尽时读出来是 0xFF,做合法性检查 if(h <= 23 && m <= 59) { DS1302_Time[7] = h; DS1302_Time[8] = m; } } ```调用就两处:上电DS1302_SetTime()之后Alarm_Load(),退出调时时Alarm_Save()。
七、12/24 小时制:bit7 那个坑
DS1302 小时寄存器(0x84)的bit7 是 12/24 标志,12 小时制下bit5 是 AM/PM 位:
bit7 | 模式 | 小时值怎么读 |
0 | 24 小时制 | bit5–bit0 就是小时的 BCD 码 |
1 | 12 小时制 | bit4 是十位,bit5 是 AM/PM(0=AM,1=PM) |
坑就坑在:你直接把读出来的值做 BCD 转换,bit7 和 bit5 会被一起算进去。我第一次切到 12 小时制,屏幕上小时直接显示成 90 多 —— 就是标志位没剥。
```c Temp = DS1302_ReadByte(DS1302_HOUR); if(Temp & 0x80) // 12小时制:先剥掉 bit7 与 bit5 再转 BCD DS1302_Time[3] = (Temp & 0x1F) / 16 * 10 + (Temp & 0x1F) % 16; else // 24小时制 DS1302_Time[3] = (Temp & 0x3F) / 16 * 10 + (Temp & 0x3F) % 16; ```切换本身只是改 bit7:
```c void Set12HourMode() { unsigned char Temp; Temp = DS1302_ReadByte(DS1302_HOUR); // 读出原始寄存器值(含标志位) Temp &= 0x9F; // 清 bit5(AM/PM) 与 bit6 DS1302_WriteByte(DS1302_WP, 0x00); // 关写保护 DS1302_WriteByte(DS1302_HOUR, Temp | 0x80); // bit7=1 -> 12小时制 DS1302_WriteByte(DS1302_WP, 0x80); // 开写保护 jinzhiMODE = 0; } ```还有一处最容易漏:DS1302_SetTime()写小时时必须把标志位一起写回去,否则每次调时都会把模式冲回 24 小时制 —— 你会以为切换功能坏了,其实是写时间的时候被抹了。
```c DS1302_WriteByte(DS1302_HOUR, (DS1302_Time[3] / 10 * 16 + DS1302_Time[3] % 10) | (jinzhiMODE ? 0x00 : 0x80)); ```八、温度为什么不自动刷新
TimeShow()最后那三行写温度的代码,是被我注释掉的:
DHT11_receive(); write_com(0x9d); Num(humidity); write_com(0x9e); hzkdis("%RH "); // 补了空格:ST7920 连续 ASCII 必须偶数个,否则会错位 // write_com(0x98); // Num(temperature); // write_com(0x99); // hzkdis("摄氏度");原因很实在:DHT11_receive()末尾有一句DHT11_delay_ms(1000),调用一次要阻塞整整一秒。把它塞进主循环,整个刷新节奏当场就垮,秒的跳动会变得很难看。
现在改成常态只刷湿度,按一次 K2 才读一次温度。这是个妥协,不是设计—— V1.0 的遗留问题,整合篇会改掉。
九、代码结构
``` main.c 384行 主循环、按键分发、界面显示、闹钟判断 12864bin.c 69行 ST7920 并口底层(写指令/写数据/忙检测/显字符串/显数字) DS1302.c 143行 三线时序读写、BCD 转换、写保护、12/24制、NV RAM 闹钟存取 DHT11.c 107行 单总线启动、收 40bit、校验 ds1302change.c 228行 公历转农历算法 ds1302change.h 352行 year_code[597] 农历数据表(1901-2100) Timer0.c 34行 1ms 定时,500ms 翻转闪烁标志 Key.c 13行 4 路按键查询 + 延时消抖 Clock.c 114行 音阶频率定义(闹钟铃声,本版未接入) Delay.c 15行 软件延时 ```十、Proteus 工程和源码
工程文件12864b.pdsprj和编译好的 hex 都在,下载下来直接能跑,不用重新搭原理图。
完整源码和 Proteus 工程我一并打包好了,需要的在评论区留言,我看到就发。
写在最后
这篇只是把成果摆出来。接下来这个系列会把每个模块拆开讲,下一篇最值得看:
这个项目里卡我最久的问题 —— Proteus 里 LCD12864B 一片黄、什么都不显示、还不报错。查了一圈资料才定位到是模型、PSB 引脚、忙检测三件事叠在一起坑我。如果你仿真时也撞上黄屏,直接看下一篇
后面还有:LCD12864B 驱动写法、ST7920 的 ASCII 偶数对齐、DS1302 时序与 BCD 码、DHT11 单总线、农历查表算法、整合篇(按键 / 闹钟 / IAP 那些坑)。
本文是「51 万年历」系列第 1 篇,完整源码和 Proteus 工程见上文。
有问题评论区问,我看到就回。