ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于51单片机电子万年历:12864驱动与DS1302时钟设计

基于51单片机电子万年历:12864驱动与DS1302时钟设计 简介面向电子类专业课程设计的一份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、IO51 单片机用普通 I/O 口模拟时序就行而且芯片便宜资料多到几乎不存在找不到例程的情况。PCF8563 走 I2C虽然只少一根线但 I2C 时序对初学者来说不如 DS1302 直观而且 DS1302 内置涓流充电电路可以直接给后备电池充电断电后时间不丢。做课设要的是稳定性和查资料的速度所以我一般会选 DS1302。温度测量用 DS18B20。单总线协议一根数据线既能供电又能传数据省引脚。相比之下 DHT11 虽然也能测温度但精度只有 ±2℃做「实时温度测量」显示在屏幕上其实够了可它的湿度数据在这个题目里用不上白白浪费了通信时序的复杂度。DS18B20 精度默认 12 位分辨率 0.0625℃拿来显示到小数点后一位完全没问题。2.2 最小硬件连接与引脚分配12864 液晶分为带字库ST7920 主控和不带字库KS0108 或兼容主控两种。带字库的屏写汉字只需要 GB2312 内码适合课设不带字库的屏要自己做字模每显示一个汉字就要先取模代码量大很多。这个题目完全没有必要用不带字库的屏。下面是我常用的引脚分配5 个引脚控制 DS13021 个引脚控制 DS18B2012864 用 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格式化到字符串缓冲区再显示比直接用数字拼一个临时字符串要稳定。本文还有配套的精品资源点击获取
返回列表