基于51单片机电子万年历:12864驱动与DS1302时钟设计
2026/9/17 2:40:00 网站建设 项目流程

简介:面向电子类专业课程设计的一份51单片机电子万年历完整方案,基于STC89C52芯片与12864液晶屏,实现年、月、日、时、分、秒、星期、农历显示、实时温度测量及闹钟、掉电走时等功能,可帮助在校学生快速完成课设答辩与实物制作。压缩包共99个文件、约37.59MB,涵盖C语言源程序、原理图(Sch/ddb)、Proteus仿真(DSN)、电路制作详解、演示视频(wmv)及论文开题报告等文档,目录按程序、原理图、仿真、使用说明、开发工具等模块划分,便于按需查阅。目前已有784人学习下载。资料中除完整可编译的Keil工程与hex文件外,还提供元件清单、焊接注意事项、视频讲解和参考论文,既能对照实物焊接,也能用于仿真调试与报告撰写,适合作为课设起步模板或功能扩展基础。

1. 课设里最容易被低估的,是 12864 的选型

一块 12864 屏,带不带字库,直接决定你这次 51 单片机课设的代码量差三倍。很多人拿到「电子万年历」这个题目,第一反应是先把 DS1302 和 DS18B20 调通,最后卡在液晶上:要么汉字显示不出来,要么字和字之间挤在一起。其实 12864 的驱动才是这道题真正的主战场——时钟芯片和温度芯片的代码加起来不到 200 行,而显示部分的代码,从取模到排版,起步就是 300 行。这篇博文会把整个方案的硬件连接、驱动思路、农历查表、闹钟状态机按可复现的方式讲一遍,适合正在做课设、也适合想把这个题目升级成完整作品的人。

2. 硬件选型与引脚分配:DS1302 走时、DS18B20 测温,为什么这样搭配

2.1 为什么不选 PCF8563 和 DHT11

常见做法里,时钟芯片有两个选择:DS1302 和 PCF8563。DS1302 是串行接口,三根线(CE、SCLK、IO),51 单片机用普通 I/O 口模拟时序就行,而且芯片便宜,资料多到几乎不存在找不到例程的情况。PCF8563 走 I2C,虽然只少一根线,但 I2C 时序对初学者来说不如 DS1302 直观,而且 DS1302 内置涓流充电电路,可以直接给后备电池充电,断电后时间不丢。做课设要的是稳定性和查资料的速度,所以我一般会选 DS1302。

温度测量用 DS18B20。单总线协议,一根数据线既能供电又能传数据,省引脚。相比之下 DHT11 虽然也能测温度,但精度只有 ±2℃,做「实时温度测量」显示在屏幕上其实够了,可它的湿度数据在这个题目里用不上,白白浪费了通信时序的复杂度。DS18B20 精度默认 12 位,分辨率 0.0625℃,拿来显示到小数点后一位完全没问题。

2.2 最小硬件连接与引脚分配

12864 液晶分为带字库(ST7920 主控)和不带字库(KS0108 或兼容主控)两种。带字库的屏,写汉字只需要 GB2312 内码,适合课设;不带字库的屏要自己做字模,每显示一个汉字就要先取模,代码量大很多。这个题目完全没有必要用不带字库的屏。下面是我常用的引脚分配,5 个引脚控制 DS1302,1 个引脚控制 DS18B20,12864 用 4 线并行模式,总共需要 11 个 I/O 口。

模块引脚51 单片机 I/O说明
DS1302CEP1.0复位/片选,拉高开始传输
DS1302SCLKP1.1串行时钟,上升沿采样
DS1302IOP1.2数据输入输出,双向口
DS18B20DQP1.3单总线数据线,需 4.7kΩ 上拉
12864RSP2.0指令/数据选择
12864RWP2.1读/写选择
12864EP2.2使能信号,高电平有效
12864D0~D7P0 口并行数据,需接 10kΩ 上拉排阻

12864 的第三脚 VO 是对比度调节脚,接一个 10kΩ 电位器到 VDD 和 VEE 之间。这个电位器不是可选项——不调对比度,屏就算初始化正确也什么都看不见。另外,ST7920 的 4 线模式只用高四位数据线,D0~D3 悬空不接,但 P0 口内部没有上拉,必须外部接排阻,否则高电平拉不上去。

2.3 让 DS1302 和 DS18B20 先跑起来的最小代码

DS1302 的读写是按字节操作的,每次传输先发命令字节,再读或写数据。命令字节格式是:最高位固定为 1,第 6 位为 1 表示 RAM,为 0 表示时钟寄存器,接下来是寄存器地址,最低位为 1 表示读,为 0 表示写。读时钟的最核心代码如下:

// 从 DS1302 读取一个字节 unsigned char DS1302_ReadByte(void) { unsigned char i, dat = 0; for (i = 0; i < 8; i++) { dat >>= 1; // 低位先出,先把上次数据右移 DS1302_IO = 1; // 释放数据线,准备接收 if (DS1302_IO) dat |= 0x80; // 读数据线,放在最高位 DS1302_SCLK = 1; // 上升沿,DS1302 在此时输出数据 DS1302_SCLK = 0; // 拉低,准备下一个位 } return dat; }

这个函数的关键在移位方向。DS1302 低位在前,所以每次先把dat右移一位给新数据腾位置,然后把读到的位放到最高位。时钟线拉高后延时不要太短,51 单片机在 12MHz 晶振下加几个 NOP 就足够,如果用的是 11.0592MHz 晶振也兼容。

DS18B20 的读取要严格遵循单总线时序。初始化时主机拉低 480μs 以上,然后释放并等待 60μs,如果总线上有设备,它会拉低 60~240μs 作为应答。读温度的核心流程是:复位 → 发跳过 ROM 命令 0xCC → 发温度转换命令 0x44 → 等待 750ms → 复位 → 发 0xCC → 发读暂存器命令 0xBE → 读两个字节。下面是最容易出错的一段:

// 读一个位,先拉低 1μs,然后释放采样 unsigned char DS18B20_ReadBit(void) { unsigned char bit; DS18B20_DQ = 0; _nop_(); _nop_(); // 总线拉低约 2μs DS18B20_DQ = 1; // 释放总线 _nop_(); _nop_(); bit = DS18B20_DQ; // 大约在 15μs 内采样 return bit; }

读位的时序里,采样点必须在拉低后的 15μs 之内完成。很多人写的代码读出来总是 0xFF 或者 0x00,就是因为采样点太晚,总线已经被 DS18B20 释放成高电平了。温度计算时,如果低字节的值大于 0x80,说明温度是负的,要做补码转换,虽然万年历场景一般不会去测零下,但代码里建议保留这个分支。

3. 12864 驱动与农历查表:显示层是代码量的主要来源

3.1 带字库与不带字库的代码差异

带字库的 ST7920 内部固化了 8192 个中文字模,写汉字时只需要发送该汉字在 GB2312 编码中的区位码,然后加上 0x80 偏移就行。不带字库的屏则要在程序里放一张自行取模的汉字点阵表,每个汉字 16×16 点阵占 32 字节,你屏幕上每多一个汉字,程序里就多 32 字节的取模数据。一个万年历要显示「年、月、日、时、分、秒、星期、温度、农历」这些字,取模表轻轻松松超过 1KB 的 code 段,这在 51 单片机上就是明显的资源开销。

所以这个题目的最佳选择是带字库的 12864,主控是 ST7920。它默认的显示坐标是行列混合编码,一行 16×16 的汉字点阵占两个字节的地址,并且要手动设置基本指令集和扩展指令集。下面这段初始化代码是固定套路,不需要改动任何参数就可以在绝大多数 ST7920 屏上跑通:

void LCD12864_Init(void) { delay_ms(50); // 等待屏内部上电稳定 LCD12864_WriteCmd(0x30); // 基本指令集,8 位模式 delay_ms(5); LCD12864_WriteCmd(0x30); // 重复发送,确保稳定 LCD12864_WriteCmd(0x0C); // 显示开,光标关,闪烁关 LCD12864_WriteCmd(0x01); // 清屏 delay_ms(10); LCD12864_WriteCmd(0x06); // 光标右移,整屏不移动 }

初始化后要等待足够时间,尤其是清屏指令 0x01,执行时间约 1.6ms,如果紧接着发送显示数据,新内容会被吞掉。我见过实际出错的情况不是初始化顺序不对,而是清屏后没延时就刷第一帧数据,导致左上角少几个字符。

3.2 显示中文字符串的具体写法

ST7920 显示中文时,先设置地址,再连续写入两个字节的编码。GB2312 内码的高字节和低字节都要加上 0x80。写一个字符串的函数如下:

// 在指定位置显示中文字符串 // x: 0~7 列,y: 0~3 行 void LCD12864_ShowChinese(unsigned char x, unsigned char y, unsigned char *str) { unsigned char addr; // 行地址偏移:第0行 0x80,第1行 0x90,第2行 0x88,第3行 0x98 addr = 0x80 + (y % 2) * 0x10 + (y > 1 ? 0x08 : 0) + x; LCD12864_WriteCmd(addr); // 设置 DDRAM 地址 while (*str != '\0') { LCD12864_WriteData(*str++); // 先写高字节 LCD12864_WriteData(*str++); // 再写低字节 } }

地址编码是第一行从 0x80 开始,第二行从 0x90 开始,第三行从 0x88 开始,第四行从 0x98 开始,每行 8 个汉字位。这个映射关系容易记混,建议把它写成查表的方式而不是公式。

温度数据显示会用到 ASCII 字符,ST7920 的 ASCII 是半宽的,一个汉字位可以放两个 ASCII 字符。混合显示时要注意:写入汉字之后紧接着写 ASCII,地址会自动移动半个汉字位,这是 ST7920 的正常行为,不要人为去改地址,否则会错位。

3.3 农历查表法与边界数据处理

农历显示是纯软件算法,51 单片机没有足够的 RAM 和 ROM 去运行完整的农历计算库,所以课设的标准方案是查表法。把 1900 年到 2099 年每年的农历信息压缩成一个 32 位的整数,其中前 4 位表示闰月月份,中间 20 位表示每月是大月(30 天)还是小月(29 天),后 4 位表示闰月是大月还是小月。具体来说,每年用一个 32 位数组元素表示,下面是 2000 年到 2002 年的数据示例:

// 农历数据表,每个元素对应一年 // 高16位:闰月月份和闰月大小,低16位:每月大小 code unsigned int lunar_info[] = { 0x04bd8, // 2000 年 0x04ae0, // 2001 年 0x0a570, // 2002 年 };

查农历的流程是:先根据公历年份确定当年春节的公历日期,然后计算当前日期距离春节的天数差,再逐月减掉农历每个月的天数,得到一个偏移量,最后根据偏移量算出农历月和日。现在的查表方案基本都是基于「春节偏移」的算法,具体算法网上有成熟的 C 语言版本可以直接移植。

这里特别提醒一个坑:查表数据一般以 1900 年 1 月 31 日为基准,这个日期是农历 1900 年正月初一。如果你的数组从 1900 年开始,但填错了一个字节的位序,会导致某一年的闰月整个错乱。验证方法是用你出生那天的农历日期做交叉测试,对比手机日历。这个步骤不能省,因为农历数据表不是你自己算出来的,是从网上复制来的,复制过程中0x04bd8这种十六进制数很容易打错一位。

4. 闹钟与按键交互:状态机比中断更好维护

4.1 闹钟判断逻辑与状态机设计

闹钟功能如果写成一个if (hour == alarm_hour && minute == alarm_minute)的简单判断,会遇到重复触发的问题——只要秒还在走,这个条件在一分钟内始终成立,蜂鸣器会一直响到天荒地老。常见做法是设置一个触发标志位,判断逻辑改成:当前时间等于闹钟时间,并且标志位为 0,触发响铃,同时标志位置 1;当时间不再是闹钟时间时,标志位置 0。

// 闹钟触发状态机 // alarm_minute 是设定的分钟,alarm_on 表示闹钟开关 // triggered 是触发标志,防止一分钟内重复触发 void Alarm_Check(void) { if (alarm_on) { if (time_minute == alarm_minute && !triggered) { triggered = 1; Buzzer_On(); // 拉高蜂鸣器引脚 alarm_ring_count = 0; // 用于 30 秒自动关闭 } if (triggered && time_minute != alarm_minute) { triggered = 0; Buzzer_Off(); } } }

注意这里用!triggered做门槛,而不是先清标志再判断。如果用中断方式做闹钟,还要额外处理按键和中断的资源冲突;而状态机方式直接放在主循环里轮询,DS1302 的秒寄存器刷新频率是 1Hz,主循环空闲时间完全足够。按键扫描、显示刷新、闹钟检查全部串行执行,不需要开定时器中断,简单很多。

4.2 蜂鸣器驱动与按键消抖

蜂鸣器分有源和无源两种。有源蜂鸣器内部自带振荡电路,通电就响,直接用一个 NPN 三极管(如 S8050)驱动就行;无源蜂鸣器需要 PWM 方波才能响,代码要额外写一个定时器翻转 I/O。课设建议用有源蜂鸣器,省掉一个定时器资源。驱动电路里三极管基极串 1kΩ 电阻接到单片机引脚,发射极接地,集电极接蜂鸣器负极,蜂鸣器正极接 5V。注意不要把蜂鸣器直接接在单片机引脚上,驱动电流不够,声音也很小。

按键扫描放在主循环里执行,消抖用软件延时。最基础的做法是:检测到按键按下,延时 10ms,再检测一次,确认按下才算有效。这个延时可以用一个delay_ms(10)函数,但在主循环里延时 10ms 会导致 DS18B20 的 750ms 转换时间被拉长感知到,所以更规范的做法是用一个 1ms 的定时器做时基,按键状态用状态机处理:

// 按键状态机:0=空闲,1=确认按下,2=按下消抖中 unsigned char key_state = 0; unsigned char key_scan(void) { static unsigned char cnt = 0; unsigned char key = 0; if (KEY_PIN == 0) { if (key_state == 0) { key_state = 1; cnt = 0; // 开始计 10ms } else if (key_state == 1 && ++cnt > 10) { key_state = 2; key = 1; // 消抖完成,返回有效按键 } } else { if (key_state == 2) { key_state = 0; // 按键释放 } else { key_state = 0; } } return key; }

这个状态机的优势在于消抖期间不阻塞主循环。cnt的计数依赖于主循环周期,如果主循环里显示刷新和数据采集耗时超过 5ms,要相应调整阈值。原则是:检测间隔要小于按键抖动持续时间,检测次数要超过抖动周期才能滤除毛刺。

4.3 时间设置流程的菜单层级

电子万年历的设置界面一般会做成这样:正常显示状态,按一下「设置」键,进入时间设置;此时当前正在编辑的字段闪烁,用「加」和「减」键修改数值;设置完一个字段按「确认」跳到下一个字段;最后一个字段设置完自动退出。这个流程里要维护一个菜单索引变量set_index,取值 0~7 分别对应年、月、日、时、分、秒、闹钟开关、闹钟分钟。

菜单状态和显示状态要分离。用一个display_mode变量区分正常显示、设置年、设置月等状态,主循环里根据状态决定刷新哪些内容。这里有一个常见的实现问题:设置模式下,画面上的数字是用户正在改的,不是当前真实时间,所以显示函数要传入一个「是否编辑中」的标志,编辑中的字段用取反的方式闪烁,其他字段显示缓冲区值。

5. 合上电前先验证这几个点:对比度、85℃ 和上电时序

5.1 12864 白屏和花屏的排查顺序

板子通电后如果 12864 白屏,第一步拧电位器调对比度,无效再检查 RS、RW、E 三个控制引脚的接线,尤其是 RW 引脚,很多屏内部已经拉低到地,外部如果再接地就会造成读写冲突。第二步检查 P0 口上拉排阻,ST7920 的 8 位并行模式必须依赖外部上拉。第三步用示波器或者逻辑分析仪探 E 引脚的下降沿,正常初始化后 E 引脚应该有周期性脉冲,如果没有,程序根本没执行到液晶初始化。

花屏和乱码的排查方向完全不一样。花屏多半是初始化时序不完整,清屏后没有等待 1.6ms;乱码则多半是编码问题——写中文时没有给内码加 0x80。ST7920 的字符表里也有 ASCII,直接写 0x41 会显示成 A,但如果写 GB2312 内码而不加偏移,屏上就是一片乱码。

5.2 DS18B20 读回 85℃ 的处理

DS18B20 上电后的默认值是 0x0550,也就是 85℃,如果程序读出来的温度一直精确地停在 85.0℃,不要怀疑芯片坏了,这是正常现象。原因是芯片上电后暂存器里的温度值没有被清空,此时你读到的是出厂测试时留下的值。解决办法并不是在代码里把这个值过滤掉,而是确保每次读温度前,执行了正确的温度转换命令,并且等待转换完成后再读取。转换时间是 750ms,很多人为省时间把延时缩短到 500ms,读回来的数据就是上一次转换的残留值。判断方法是:读回来的温度如果超过 80℃,改成长延时 1 秒后再读,数值变化了说明之前是转换没完成。

5.3 农历和星期的交叉验证方法

万年历最终验收时,屏幕上同时有公历、农历、星期、温度,这几路数据很容易各显各的数字但对不上日期。我用过一个很实用的自检方法:把系统时间设到 2024 年 2 月 10 日,这一天是农历甲辰年正月初一,同时是星期六,屏幕上这三个信息可以交叉核对。再设到 2024 年 10 月 1 日,这天是农历八月廿九,星期二。这两组数据覆盖了农历年初和年末的边界,如果农历查表有闰月错误,通常会在第二组数据之后的下一个月暴露出来。

还有一个低频坑:星期和农历都用 DS1302 读出的寄存器值去计算。DS1302 的星期寄存器是用户可写的,它不会自动根据日期更新。如果代码里直接读星期寄存器显示,日期改了而星期寄存器没跟着改,屏幕上就会出现星期三配 1 月 1 日的矛盾。最保险的方案是不读星期寄存器,而是根据公历年月日用蔡勒公式算星期,这样星期永远和日期对应。温度显示的末位补零问题也别忽略,用sprintf格式化到字符串缓冲区再显示,比直接用数字拼一个临时字符串要稳定。

本文还有配套的精品资源,点击获取

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

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

立即咨询