☰
51单片机万年历Proteus仿真跑通实录:LCD12864B+DS1302+DHT11,带农历和闹钟(效果图+接线表+全套源码)
2026/10/15 3:52:58 网站建设 项目流程

网上找 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

20+ 年 月 日

第二行

0x90–0x97

时 : 分 : 秒 + 星期

第三行

0x88–0x8B

农历 月 日

第三行(后半)

0x8C–0x8F

钟+ 闹钟 时 : 分

第四行

0x98–0x99

温度 + 单位

第四行(后半)

0x9D–0x9E

湿度 +%RH

按键功能

按键

引脚

运行模式

调时模式

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 工程见上文。

有问题评论区问,我看到就回。

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

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

立即咨询