51单片机万年历Proteus仿真跑通实录:LCD12864B+DS1302+DHT11,带农历和闹钟(效果图+接线表+全套源码)

网上找 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.5 PSB=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 ------ 调的过程中改的只是数组,绝不碰芯片。

五、核心代码:主循环

bash 复制代码
```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 起,写命令是偶数地址(0xC0 0xC2...),读命令是奇数地址(0xC1 0xC3...)。我这的 DS1302_ReadByte() 内部已经做了 Command |= 0x01,所以读的时候传同一个地址就行

cpp 复制代码
```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 多 ------ 就是标志位没剥。

cpp 复制代码
```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:

cpp 复制代码
```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 小时制 ------ 你会以为切换功能坏了,其实是写时间的时候被抹了。

cpp 复制代码
```c
	DS1302_WriteByte(DS1302_HOUR,
	    (DS1302_Time[3] / 10 * 16 + DS1302_Time[3] % 10) | (jinzhiMODE ? 0x00 : 0x80));
```

八、温度为什么不自动刷新

TimeShow() 最后那三行写温度的代码,是被我注释掉的:

cpp 复制代码
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 的遗留问题,整合篇会改掉。

九、代码结构

cpp 复制代码
```
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 工程见上文。

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

相关推荐
ThinkerQAQ_1 小时前
并发编程(十):BlockingQueue 与 Channel 的实现——从 Runtime 到 CPU
后端
霂鳯1 小时前
新手勇闯网络安全 | PHP基础(四)
后端
海岳云舟1 小时前
程序常驻后台运行的原理和方法
后端·全栈
彩雲演文1 小时前
Proteus仿真LCD12864B不显示/黄屏?别急着改接线,真凶是BusyCheck死循环(51单片机实战排查)
后端
Rain的Java大神之路2 小时前
Java 三层架构,90% 的人只答对一半:Controller 不是瘦,Service 不是透传,Dao 不是只写 SQL
java·后端·面试
小羊没烦恼!2 小时前
浅谈SQL Server中的事务日志(二)----事务日志在修改数据时的角色
前端·数据库·后端·python·web安全·oracle
IT_陈寒3 小时前
一文搞懂Vite的核心概念
前端·人工智能·后端
一个简单的TTS开发者3 小时前
中文TTS工具有哪些-8款主流方案横向对比
人工智能·后端·python·indextts
高频因子挖掘机4 小时前
量化回测应该保存不复权数据吗?从数据口径到收益计算的工程实践
后端·github·api