
简介一份基于89C51单片机、DS1302实时时钟芯片与LCD1602液晶屏的日历时钟软件源码包适合单片机入门学习者、电子爱好者及嵌入式开发初学者用于快速掌握RTC芯片读写与液晶显示驱动的完整流程。压缩包共18个文件核心包括Keil工程文件uvproj/uvopt、C语言源程序、可烧录的hex文件、DS1302LCD1602显示测试工程另附89C51开发板原理图PDF与1602液晶安装图方便对照硬件连接与程序逻辑整体大小仅1.25MB目录结构清晰。工程源码已包含初始化、时间设置与读取、显示刷新等模块编译后可实际运行。已有263人学习下载可作为课程设计或DIY电子时钟项目的参考模板。1. 89C51、DS1302与LCD1602日历时钟时间芯片为什么比显示更难调89C51单片机驱动DS1302时钟芯片、再用LCD1602把年月日时分秒显示出来这套日历时钟在51单片机课程设计和毕业设计里出镜率极高。不少初学者拿到源码后的第一反应是研究LCD1602的显示函数结果编译烧录完发现时钟不走、显示乱码、复位后时间归零问题几乎都出在更前面。这块板子的真正难点有三个DS1302是三线串行接口读写时序错了就读回0xFFDS1302内部用BCD码存时间直接当十六进制处理会看到完全不合理的数值P0口同时挂着LCD1602和数码管不处理锁存器就会互相干扰。这篇按硬件接线、时序读写、源码数据流、编译排错的顺序拆开讲新手照着能点亮老手可以重点看第四章的故障判定逻辑和第五章的软件调度写法。2. DS1302硬件接线与时序模型串行RTC为什么容易读错2.1 89C51上的引脚分配与最小系统关系DS1302是DIP-8封装引脚不多但每一根都有讲究。VCC2接主电源5VVCC1接备用电池或超级电容主电掉电后自动切换到VCC1供电这是它能“断电走时”的根本原因。X1和X2之间接32.768kHz晶振这个频率是RTC行业的标准频率分频300多次就能得到1Hz的秒脉冲换别的频率芯片内部时序就乱套了。信号推荐接法说明CE片选P3^5高电平有效读写全程保持高SCLK串行时钟P3^6上升沿写数据下降沿后读数据IO数据线P3^7双向口读数据前要释放总线VCC25V主电源VCC1电池或0.1F电容掉电保持用X1/X232.768kHz晶振要接6pF负载电容不是随便来如果你的板子是现在常见的stc89c52rc lcd1602最小系统板引脚编号基本沿用AT89C51的习惯DS1302也可能被挪到P1口或P2口第二种接法在开发板原理图.pdf里能查到。判断CE/SCLK/IO接在哪组口最直接的办法是看原理图里网络标号别只看丝印。LCD1602这边RS、RW、E一般接P1.0到P1.2D0到D7接P0口。P0是开漏输出必须外接上拉电阻否则LCD1602的数据线电平不确定表现为白屏或者显示乱字符这个问题在课程设计里出现频率非常高。2.2 寄存器地址与BCD码读回来的数据为什么不直观DS1302内部有秒、分、时、日、月、周、年七个时间寄存器加上写保护寄存器和涓流充电寄存器。访问方式是先写一个命令字节命令字节的最低位置1表示读、置0表示写bit0是寄存器地址的最低位。参考地址表如下。寄存器功能写地址读地址关键位说明秒0x800x81bit7CH为1时振荡器停止分0x820x83无特殊位时0x840x85bit7为1时进入12小时制日0x860x87范围1到31月0x880x89范围1到12周0x8A0x8B范围1到7年0x8C0x8D范围00到99写保护0x8E0x8Fbit7为1时禁止写其他寄存器涓流充电0x900x91充电模式配置这里最容易踩的坑是BCD码。DS1302内部存的0x23表示十进制23不是十六进制的35。如果你把读回来的字节直接丢给串口打印或者做数值运算时间就会变成“35点67分”这类完全没法看的结果。把BCD转成十进制标准写法是(bcd 4) * 10 (bcd 0x0F)反过来初始化时间时要把十进制数拆成高四位和低四位。很多蓝桥杯单片机国赛客观题和吉林大学单片机实验里都考过这个转换关系做题时容易错写驱动时更容易错。2.3 单字节读写时序CE、SCLK、IO三根线的配合DS1302的时序不算复杂但顺序错了就全错。一次完整的单字节写操作分两段先发命令字节再发数据字节全部低位在前。命令字节发完之后紧接着发数据中间不需要停顿。参考实现如下。typedef unsigned char uchar; sbit CE P3^5; sbit SCLK P3^6; sbit IO P3^7; void Write1302(uchar reg, uchar dat) { uchar i; CE 0; // 先拉低片选确保一次干净传输 SCLK 0; CE 1; // 片选拉高DS1302开始接收 for (i 0; i 8; i) { // 送命令字节低位在前 IO reg 0x01; reg 1; SCLK 1; // 上升沿芯片锁存IO上的电平 SCLK 0; } for (i 0; i 8; i) { // 紧接着送数据字节 IO dat 0x01; dat 1; SCLK 1; SCLK 0; } CE 0; // 传输结束片选拉低 }每次循环里先把要发送的位放到IO上等IO稳定后再产生一次上升沿这个顺序不能反过来。如果先拉高SCLK再改IO芯片锁存到的就是上一轮的电平写进去的数据整体错位。读操作前半段和写相同区别在数据阶段要释放IO让DS1302驱动这根线参考代码见下。uchar Read1302(uchar reg) { uchar i, dat 0; CE 0; SCLK 0; CE 1; for (i 0; i 8; i) { // 写命令字节 IO reg 0x01; reg 1; SCLK 1; SCLK 0; } IO 1; // 释放IO准备读取 for (i 0; i 8; i) { dat 1; // 先腾出最高位 if (IO) dat | 0x80; // 当前IO为1则置最高位 SCLK 1; // 产生一个脉冲让芯片输出下一位 SCLK 0; } CE 0; return dat; }注意读数据时取电平的时机SCLK为低时读IO读完再产生脉冲。有的例程习惯在SCLK高电平时采样也能读到但容易在临界处读到跳变沿上的毛刺。IO1那句不是输出高电平而是把引脚配置成输入模式51单片机准双向口写1之后才能正确读外部电平漏掉这一步读回来的秒值会一直是0xFF。3. 软件源码拆解main()、Set1302/Get1302与LCD1602刷新链路3.1 工程文件里谁是谁网上经常有人问哪里可以下载lcd1602的驱动代码其实这份源码包里自带了一份可用的完整驱动工程文件也都是齐的。.uvproj是Keil工程文件.Uv2.bak是老版本Keil的工程备份.c是主源码.hex是编译出来的烧录文件.M51是链接器生成的存储器映射文件.LST是汇编列表文件。.M51这个文件容易被忽略程序跑飞时打开看堆栈段和变量覆盖能省不少排查时间。.build_log.htm是编译日志里面最后一行显示0 Error(s)就说明编译通过不用管中间的警告。DS1302LCD1602显示.uvgui.junzh是Keil的界面布局文件删了也不影响工程编译。真正要改的只有.c文件改完重新编译新的.hex会覆盖旧的。3.2 main()里为什么先锁存数码管再释放P0口这份源码的main()开头有一段看起来“多余”的P0操作它解决的是P0口二选一的问题。很多51开发板上P0口既接LCD1602的数据线又通过74HC573锁存器接数码管。上电瞬间P0口电平不确定数码管可能乱亮LCD1602也可能读到错误数据。锁存器的原理是锁存使能引脚为高时输出跟随输入拉低后输出保持当前状态不再变化。所以先把P0全部拉低再让段选锁存器锁存一次数码管就保持熄灭状态之后P0口随便被LCD1602占用都不会影响数码管。void main() { uchar i; lcd_system_reset(); // LCD1602初始化设置8位模式、开显示、清屏 P0 0x00; // P0全部拉低 dula 1; // 段选锁存器使能 wela 0; // 位选锁存器不使能 delay1(); dula 0; // 关闭段选锁存数码管被锁住 wela 0; delay1(); lcd_data_port 0xff; // 释放P0端口 Set1302(time_data_buff); // 把缓冲区初值写入DS1302 while (1) { Get1302(time_data_buff); // 读取当前时间到缓冲区 refresh_lcd(); // 把缓冲区内容刷到LCD1602 delay1(); // 适当降频减少闪烁 } }dula和wela具体接哪个引脚看你开发板原理图常见是P2.6和P2.7。要确认锁存方向是否写反有个简单办法把dula改成0、wela改成1试一下如果数码管反而乱闪说明两个信号定义和原理图正好相反对调即可。3.3 Set1302和Get1302写入与读取的时间数据流Set1302做的是把内存里的一组BCD时间初值搬到DS1302寄存器。写之前要先关写保护写完之后打开写保护避免主程序跑飞时误改时钟。秒寄存器写入时有个重要细节p[0] 0x7f把最高位CH清零让振荡器开始跑。如果初值的秒是0x00CH本来就是0这一步体现不出来如果从别的程序移植过来时秒寄存器里残留CH1时钟就永远不走这是“为什么我时间设置对了但秒针不动”的最常见原因。void Set1302(uchar *p) { Write1302(0x8E, 0x00); // 关闭写保护 Write1302(0x80, p[0] 0x7f); // 秒清CH位 Write1302(0x82, p[1]); // 分 Write1302(0x84, p[2]); // 时 Write1302(0x86, p[3]); // 日 Write1302(0x88, p[4]); // 月 Write1302(0x8A, p[5]); // 周 Write1302(0x8C, p[6]); // 年 Write1302(0x8E, 0x80); // 打开写保护 } void Get1302(uchar *p) { p[0] Read1302(0x81); // 秒 p[1] Read1302(0x83); // 分 p[2] Read1302(0x85); // 时 p[3] Read1302(0x87); // 日 p[4] Read1302(0x89); // 月 p[5] Read1302(0x8B); // 周 p[6] Read1302(0x8D); // 年 }读时间为什么不用关写保护写保护寄存器只限制写操作不影响读所以Get1302直接读就行。读回来的数组下标对应关系是0秒、1分、2时、3日、4月、5周、6年。缓冲区类型是uchar里面存的是BCD码这一点贯穿整个显示逻辑。3.4 LCD1602刷新两位数怎么拆成两个字符LCD1602显示ASCII字符而DS1302给的是BCD字节。0x23这个字节要显示成“23”必须把高半字节2变成字符‘2’低半字节3变成字符‘3’。mun_to_char这张表做的事就是把0到9映射到对应的ASCII码。uchar code mun_to_char[] {0,1,2,3,4,5,6,7,8,9}; uchar code lcd1602_line1[] : : ; // 时:分:秒模板 uchar code lcd1602_line2[] 20 - - ; // 20xx-xx-xx模板 void refresh_lcd(void) { // 第一行时、分、秒 lcd1602_line1[0] mun_to_char[time_data_buff[2] 4]; // 时十位 lcd1602_line1[1] mun_to_char[time_data_buff[2] 0x0f];// 时个位 lcd1602_line1[3] mun_to_char[time_data_buff[1] 4]; // 分十位 lcd1602_line1[4] mun_to_char[time_data_buff[1] 0x0f];// 分个位 lcd1602_line1[6] mun_to_char[time_data_buff[0] 4]; // 秒十位 lcd1602_line1[7] mun_to_char[time_data_buff[0] 0x0f];// 秒个位 // 第二行年、月、日 lcd1602_line2[2] mun_to_char[time_data_buff[6] 4]; lcd1602_line2[3] mun_to_char[time_data_buff[6] 0x0f]; lcd1602_line2[5] mun_to_char[time_data_buff[4] 4]; lcd1602_line2[6] mun_to_char[time_data_buff[4] 0x0f]; lcd1602_line2[8] mun_to_char[time_data_buff[3] 4]; lcd1602_line2[9] mun_to_char[time_data_buff[3] 0x0f]; lcd_write_command(0x80); // 光标移到第一行首地址 lcd_write_string(lcd1602_line1); lcd_write_command(0xC0); // 光标移到第二行首地址 lcd_write_string(lcd1602_line2); }这里用code关键字把模板数组放到程序存储区不占宝贵的片内RAM。4取高四位 0x0f取低四位。显示秒表时直接改模板里的冒号位置比用sprintf拼字符串省一半ROM。如果后续要移植到串口屏把mun_to_char替换成ASCII码表同样成立。这套位拆分的思路是51单片机里的通用手法后面做Modbus帧接收、模拟PT2262发射脉冲甚至驱动WS2811灯带本质都是对位进行操作。4. Keil工程编译、STC-ISP烧录与四类典型故障排查4.1 把源码编成可烧录的hex拿到.zip解压后双击DS1302LCD1602显示.uvproj就能打开Keil工程。如果打开后左边文件树是空的检查.uvproj和.c文件是否在同一目录下Keil用的是相对路径路径里有中文也可能导致加载失败。编译前先做一件事点击Options for Target在Output页勾选Create HEX File。不勾这一项编译只生成Objects目录里的.axf和.build_log.htm没有.hex烧录器什么都不认。烧录工具一般用STC-ISP。芯片型号这里要注意很多卖家标“89C51”实际芯片是STC89C52RC选错型号会握手失败。打开STC-ISP后选STC89C52RC系列加载编译出来的.hex断电再上电点击下载多试一次就能进。如果不想买开发板Proteus仿真里单片机型号常用AT89C51LCD1602用LM016L模型DS1302用自带的DS1302模型仿真时引脚电平逻辑和实物一致但晶振起振条件仿真不出来所以仿真通过不等于实物能走时。4.2 LCD1602黑屏或整屏方块LCD1602上电后出现黑块常见原因有四个。第一V0对比度引脚悬空或者电压不对这个引脚要用10k电位器分压调到V0电压在0.5V到1V之间字符才清晰。第二RW引脚没有拉低LCD1602数据手册要求写操作时RW为低。第三P0口没有接上拉电阻P0口内部没有上拉必须外接4.7k到10k排阻。第四初始化时序里延时不够LCD1602上电后要等40ms以上才稳定初始化函数前面加一个50ms延时很多“第一次上电白屏、马上复位就正常”的现象都是这个原因。void lcd_system_reset(void) { lcd_data_port 0xff; delay_ms(50); // 等待LCD1602内部上电复位 lcd_write_command(0x38); // 8位数据总线两行显示5x8点阵 lcd_write_command(0x0C); // 开显示不显示光标 lcd_write_command(0x06); // 地址指针加一写入后光标右移 lcd_write_command(0x01); // 清屏光标归位 delay_ms(5); // 清屏指令执行时间较长 }0x38这条命令经常被写成0x3C多出来的0x04是5x11字模模式LCD1602不支持写进去不会报错但显示区域会变得异常。确认命令作用时可以对着LCD1602的指令表逐位看比死记代码快。4.3 时间不走、乱跳、断电归零时间不走先别怀疑晶振第一件事是确认秒寄存器CH位。用调试器或者临时写一段代码读0x81如果读回来的值是0x80以上说明CH为1振荡器被关掉了。Set1302里p[0] 0x7f就是干这个的但如果你跳过了Set1302直接Get1302芯片出厂默认CH可能是1。第二检查32.768kHz晶振和两个6pF负载电容DS1302对晶振要求不高但很多开发板把负载电容配成12.5pF的这会导致起振困难。手里有示波器就量X2引脚没有示波器就用手摸晶振引脚有时候手摸上去时间开始走了说明振荡电路工作点在临界状态。断电归零的问题出在VCC1仔细看开发板原理图VCC1位置有没有装电池座或者0.1F超级电容。如果原理图上VCC1是空焊盘那断电后时间必然丢失这不是程序bug。还有一个隐蔽坑部分开发板的DS1302 VCC1和GND之间没有防倒灌二极管用普通锂电池长期挂着会有过放风险课程设计答辩演示用超级电容更稳妥。4.4 现象与排查速查表现象第一步查什么可能的根因LCD有黑块无字符V0电压、RW接地对比度、读写方向错误显示乱码且变化P0上拉、dula/wela锁存数码管信号串扰时间不走0x81最高位、晶振CH位、负载电容不对时间走得过快或过慢晶振频率32.768kHz偏差、温度影响复位后时间清零VCC1、电池备用电源缺失偶尔读到0xFF读前IO是否置1总线没释放这几种故障里“时间不走”和“偶尔读到0xFF”最容易混淆。前者是秒寄存器固定不变后者是读出来的值一会儿正常一会儿0xFF。前者查CH位和晶振后者查Read1302里IO 1;这行是不是被优化掉或者CE引脚有没有被别的外设占用。5. 扩展玩法软件定时调度、电池保持与时钟精度校准5.1 用分钟变化触发任务省掉定时器中断日历时钟的主循环里每圈都在Get1302这个周期性的时间读取本身就是天然的调度节拍。如果要做整点报时、继电器定时翻转这类分钟级任务不需要开定时器中断只需用一个变量记住上一分钟的“分”值。当分发生变化时执行一次任务天然对齐RTC的分钟边界代码量少还不占定时器资源。uchar last_min 0xff; // 初值故意错开保证上电后第一次读就能触发 void minute_task(void) { // 每分钟执行一次翻转继电器、累加计数、发送状态帧都可以放这里 } void main(void) { // ...初始化省略... while (1) { Get1302(time_data_buff); if (time_data_buff[1] ! last_min) { // 分钟变了 last_min time_data_buff[1]; minute_task(); } refresh_lcd(); delay1(); } }要每秒执行的任务就比对秒字段判断条件改成time_data_buff[0] ! last_sec即可。这个写法的容错点在于last_min初始化成0xff第一次进循环时一定会触发一次任务避免“上电后要等整分钟才开始工作”的尴尬。如果任务本身耗时超过1秒就不要在秒级任务里做除法、浮点运算51单片机跑不了那么多计算量常见做法是把任务拆成状态机让单片机遇上状态机这套思路在这里非常适用。5.2 备用电池和涓流充电配置DS1302的VCC1支持备用电池同时内部有一个涓流充电寄存器0x90可以对可充电电池或超级电容充电但不能对一次性纽扣电池充电。充电配置常见三种0xA5表示一个二极管加2k电阻0xA6表示一个二极管加3k电阻0xA7表示一个二极管加6k电阻电流依次减小。超级电容选0xA7更安全镍氢电池选0xA5充电速度快但要注意DS1302的最大充电电流只有几毫安容量大的电池充满需要很长时间。如果VCC1接的是不可充电的CR2032绝对不能写0x90寄存器否则等效于给一次性电池反向充电轻则电量耗尽重则电池漏液腐蚀板子。很多开发板出厂时并没有给DS1302配二极管和限流电阻此时涓流充电功能并不完整最稳妥的做法是VCC1单独接一颗好的CR2032加座子。5.3 日误差的测量与校正DS1302的时间精度取决于32.768kHz晶振的频偏普通晶振的日误差在1到5秒之间。先测量实际误差校准时间后运行24小时记录快或慢了多少秒。如果每天快2秒可以在每天固定时刻比如23点59分59秒读一次时间把“分”字段临时减1再写回这种一次性补偿够课程设计用。要求更高的话把误差值存到AT24C02里每次启动时做累计修正。校准晶振本身也能调精度X1和X2并接一个5到20pF的可调电容减小负载电容会让晶振频率略微升高反向调整则降低频率。注意DS1302的晶振振荡幅度很小示波器探头直接挂上去会导致停振测量频率用非接触式频率计或者靠计时结果反推。最后一招是放弃DS1302做守时核心让它在系统启动时校准一次平时走秒用单片机定时器精度能到每天0.5秒以内整点任务的误差可以控制在秒级。本文还有配套的精品资源点击获取