尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

51单片机驱动DS1302实时时钟:LCD1602显示与按键调时完整方案

51单片机驱动DS1302实时时钟:LCD1602显示与按键调时完整方案 简介本资源是一套基于STC89C52单片机的实时时钟系统完整开发资料面向嵌入式初学者、电子类课程设计学生及单片机实训教师解决DS1302时钟芯片驱动、LCD1602动态显示与独立按键交互修改时间等典型教学难点。压缩包共64个文件包含4个核心C源文件如DS1302.c、lcd1602.c、4个头文件含key.h、LCD1602.h、原理图SchDoc文档、Keil工程文件uvproj/uvopt、编译生成的hex可执行文件及lst/obj等调试辅助文件全面覆盖硬件设计、软件编程与调试验证全流程。资源包大小仅508KB结构清晰便于快速导入Keil并烧录运行。已有1148人学习下载读者可直接获取可运行的完整程序、标准原理图、模块化函数库及配套编译输出文件显著降低从原理理解到实物调试的学习门槛特别适合课程实验、毕业设计原型开发与技能巩固。 最早接触这个项目的时候我记得是在一块最小系统板上线接得乱成一团DS1302的备用电池座还没焊每次拔电时间就清零。后来一路调通按键修改时间、掉电保存、走时精度修正才意识到这个组合虽然基础但把嵌入式入门最关键的东西全串起来了GPIO控制、时序协议、状态机、模块化编程。这篇文章就把这套STC89C52DS1302LCD1602独立按键的完整方案讲清楚从原理图到驱动代码再到调试时踩过的坑照着做基本能一次跑通。适合谁来参考如果你正在做课程设计、电子设计竞赛的时钟类题目或者单纯想把“51单片机怎么驱动外部芯片”这件事彻底搞懂这篇都能帮上忙。我会尽量把每个操作背后的“为什么”也讲明白而不是只给你一堆能编译的代码。1. 项目整体设计与器件选型思路1.1 为什么是 DS1302 而不是 DS3231、内部定时器很多新手朋友第一反应是51单片机内部不是有定时器吗死循环里跑个定时中断不就能计时了这当然能做但有两个致命问题。第一单片机一旦断电时间就归零了就算你不断电复位一次、下载一次程序计时全部从头开始。第二定时器中断累计计时误差太大晶振一点偏移累计下来一天就差出不少秒。做产品、做课程设计都拿不出手。所以必须上独立的RTC芯片自带晶振、自己走时、备用电池单独供电主控断电它也照走不误。那为什么选DS1302而不是DS3231原因很直接便宜、接线少、资料多。DS1302是经典的串行RTC芯片支持毫安级功耗工作电压2.0V~5.5V能直接挂在3.3V或5V系统上。它采用三线接口CE、SCLK、I/O各一根线总共就三根线接到单片机不像DS3231虽然用I2C也只占两根线但新手容易在I2C地址、应答时序上卡半天。DS1302还有一个优势它内置了31字节的静态RAM你可以在里面存一些掉电不丢的配置数据比如闹钟状态、背光档位虽然这个项目用不上但扩展空间确实大。再补一句实话DS1302属于“简单但过时”的芯片指令手册才十几页寄存器就那十几个新手拿它练时序是再好不过的既不像DS18B20那样只有一根线、时序窗口必须精确到微秒级也不像DS3231那样动不动就搞闹钟和温度补偿。用一句话概括DS1302是那种你照着数据手册写一遍驱动就能彻底理解“串行通信”这件事的芯片。1.2 系统数据流从按键到寄存器到屏幕整个系统的工作模式是这样的上电后STC89C52先从DS1302里读出当前年、月、日、时、分、秒按照“20XX-XX-XX 星期X”和“HH:MM:SS”的布局分批送到LCD1602上刷新显示。在正常显示状态下三个按键各自待命一旦按下“设置”键系统就进入时间修改状态屏幕上当前可修改的字段开始闪烁或光标指示此时按“加”和“减”键调整数值再按“设置”键跳到下一个字段。所有字段调整完之后把整组时间一次性写回DS1302同时回到正常运行状态。这个数据流的精髓在于“读一遍、显示、改完写回”。有些初学者喜欢在“设置”里每改一位就立刻写一次DS1302其实没必要反而容易因为按键抖动、中途掉电而写到一半的数据不完整。我的习惯是所有修改先落在单片机内存里确认无误后再一次性写入RTC。这样既减少了对芯片的写入次数也避免了“改了秒却忘了改分”的尴尬。从工程角度看这个系统还锻炼了两个核心能力一是“外设驱动分层”DS1302驱动只管收发字节LCD驱动只管显示字符按键只管返回状态三个模块互不依赖单测、复用都方便二是“状态机思维”时间调整时的“正常显示→改年→改月→改日→改时→改分→改秒→写回”就是最典型的状态流转这个东西学会了往后写菜单、写界面逻辑都受用。2. 原理图与硬件连接照着接线就能跑2.1 电源、复位和下载电路STC89C52是5V供电的芯片但是STC官方把它定位成“宽电压”实际做板子时建议直接把VCC接到稳定的5V上。这里要特别提醒很多朋友拿USB转TTL的串口模块给板子供电如果你用的下载器质量一般带载能力不够插上1602背光之后电压就往下掉屏幕直接花屏或白屏。一个可靠的做法是给整个板子加一个AMS1117-5.0稳压芯片用USB的5V输入输出接一个100uF电解电容和一个104去耦电容再分给单片机、液晶背光和RTC模块。别小看这个电容组合它解决的是DS1302走时不稳、数据偶发错乱这类隐蔽问题。STC89C52的复位电路是最经典的高电平复位一个10uF电解电容串联到VCC电容下端接复位脚RSTRST对地再接一个10kΩ电阻。上电瞬间电容充电RST维持短暂高电平芯片复位运行时RST被10kΩ电阻拉低正常执行。至于晶振要用11.0592MHz不要用12MHz——理由很现实11.0592MHz是51单片机串口通信的标准频率能精确产生9600、115200这样常用的波特率否则下载程序或者以后扩展串口功能时波特率误差会大得离谱。下载电路比较有STC特色。STC89C52用串口ISP下载只需要把单片机RXDP3.0接到USB转TTL模块的TXTXDP3.1接到模块的RXGND共地。注意下载时要先断电点击下载后在STC-ISP软件里再上电或者直接使用支持自动断电复位的下载器否则会一直卡在“正在检测目标单片机……”这一步。这是STC系芯片的老传统比AVR那种一键下载麻烦但用顺手也就那样。如果你的板子有串口芯片和自锁开关把“先点下载再上电”养成肌肉记忆能少浪费很多时间。2.2 DS1302 三线接口引脚分配和上电顺序DS1302有8个引脚标准接法是VCC2接主电源5VVCC1接备用电池CR2032或3.7V锂电池加二极管隔离GND接地。这里有一个特别关键的坑VCC1和VCC2的接法顺序决定了芯片优先使用哪路电源。如果VCC2大于VCC10.2V芯片用主电源供电同时给VCC1的电池涓流充电反过来如果VCC2掉电了芯片自动切到VCC1。很多朋友为了省事不装电池直接把VCC1悬空这其实也能跑但一旦主电源断开时间就丢了。做课程设计建议至少装一个纽扣电池座给VCC1提供3V保持时间不丢的效果在验收时非常加分。三根通信线建议这样接CE或叫RST接STC89C52的P1.0SCLK接P1.1I/O数据线接P1.2这三根线本身不需要额外上拉电阻因为DS1302内部有上拉。但要小心的是DS1302的I/O方向是双向的驱动代码里必须在读模式下让单片机引脚处于“读”状态写模式下输出。STC89C52是准双向口直接读写P1.2的寄存器就可以不需要像STM32那样在运行中切换推挽/开漏模式。但如果你在STC-ISP的下载选项中把某个端口设成了强推挽输出那就得谨慎处理方向切换否则读数据时可能读到的是自己写出去的信号。时钟芯片对电源纹波非常敏感最好在VCC2对地加一个0.1uF的陶瓷电容如果芯片离主控比较远I/O线上再串一个100Ω电阻用来抑制反射。这些细节在仿真软件里完全看不出来但在实体板上往往是“调试半天无解”的根源。同样地备用电池如果不常用建议串一个1N4148二极管再进VCC1防止主电源通过VCC1给电池反充虽然DS1302内部标称有涓流充电控制但外部多加一级隔离更保险。2.3 LCD1602 4位模式接线LCD1602本身是8位并口的但实际工程里几乎都用4位模式只接DB4~DB7少占用4个IO口速度也没有明显差异。接法建议RS接P2.0RW接P2.1E使能接P2.2DB4~DB7接P2.4~P2.7这样安排的好处是数据口和P2端口的位对应关系很整齐写数据时可以直接用一个switch语法的封装函数或者干脆用一个P2的掩码来处理代码简洁很多。RW引脚很多人默认接地因为绝大多数情况下只写不读液晶。但如果你想要“读忙标志”来判断液晶是否空闲那就必须把RW接到单片机。否则的话初始化完成后每次写命令之间做固定延时比如写命令后延时5ms、写数据后延时1ms大多数场合也够用。我图里默认RW直接接地程序里每次写数据后加延时实测1602的响应速度完全跟得上这个做法在入门项目里非常常见。液晶的3脚V0是对比度调节脚建议通过一个10k电位器接到5V和GND之间中间抽头接V0调节到屏幕显示出黑色实心方块但不至于模糊。V0的电压通常调到0.5V左右效果最好。背光正极接5V串一个100Ω电阻限流背光负极接地这样就算不写任何字屏幕背光亮起也说明电源和液晶接线基本没问题。如果你嫌电位器占地方可以固定用两个电阻分压但不同批次的液晶模块最佳对比度电压略有差异还是电位器更灵活。2.4 独立按键怎么接一次性搞定三个按键独立按键的接法没啥花活就是把按键一端接单片机IO口另一端接地单片机内部启用准双向口平时读为高电平按下时IO被拉低。注意STC89C52的P0口内部没有上拉用独立按键时必须外接4.7k~10k的上拉电阻到VCC否则按下和松开都是低电平无法识别。所以按键千万别图省事接到P0口直接接P1或P3口省掉一堆电阻。我习惯把按键接在P3.0、P3.1、P3.2K1设置/模式接P3.0K2加接P3.1K3减接P3.2这里有个要点P3.0、P3.1同时也是串口的RXD和TXD如果以后程序里初始化串口按键扫描和串口功能会互相干扰。这个项目里不用串口所以没问题。如果你后期想加串口调试就把按键挪到P1口去宁可多接几根线也别牺牲调试通道。另外按键最好加一个“上拉到VCC”的电阻其实不用51的准双向口内部有上拉直接接GND就对了。按键扫描还有一个硬性要求必须加消抖。机械按键按下的一瞬间会因为弹片抖动产生一串几毫秒的脉冲如果不消抖一次按下可能被单片机识别成三四次表现在程序里就是“加一次时间跳了5秒”。消抖的方式有硬件RC滤波和软件延时两种入门项目用软件即可下面软件章节我会给出一个非阻塞的消抖实现避免你为了消抖把整个主循环卡死。到这里把所有器件按上面说明接好你得到的实物应该已经满足“上电背光亮、屏幕有内容就算是乱码也算、按按键引脚能测到电平变化”三个前提了。如果这三个都符合那硬件链路就没问题接下来的重点就全部在软件上。3. DS1302 驱动读写时间数据的核心部分3.1 寄存器到底长什么样DS1302内部有12个寄存器负责时间外加一个写保护寄存器。时间寄存器的地址是连续的从秒寄存器地址0x80/0x81开始依次是分、时、日、月、星期、年最后是写保护寄存器0x8E/0x8F。每个寄存器都是8位但里面并不是直接存二进制数值而是用BCD码存储。什么叫BCD码就是高四位表示十位、低四位表示个位比如十进制的23BCD存法是0x23读出后要自己换算成整数写的时候也要反过来把整数拆成BCD。这可能是新手第一次接触BCD也是最容易栽的地方。很多人读回来时间显示出来的是一堆乱数字比如15点显示成0x15直接打印变成21就是因为没有做BCD到十进制的转换。标准做法是读出来time_sec (bcd 4) * 10 (bcd 0x0F)写进去bcd (sec / 10) 4 | (sec % 10)秒寄存器的最低位置1表示时钟暂停比如你往秒寄存器写入0x80时钟芯片就停走这个位在初始化时一定要清零。写保护寄存器0x8E的最高位WP置1后所有时间寄存器的写入操作都会被拒绝。这个设计是为了防止系统掉电瞬间的误写所以要改时间前先写0x00解除写保护改完了再写0x80开启写保护。这两步是很多驱动代码里最容易漏掉的顺序漏了之后的表现就是程序能正常读时间但一写就无效看起来像芯片坏了其实只是被写保护挡了。对于“星期”字段DS1302的星期寄存器只需要写1~7具体对应关系不同的资料说法不一。我习惯在程序里固定一套映射1表示周日2~6表示周一~周五7表示周六然后建一个字符数组按索引取出对应的“日一二三四五六”这样只要初始化时把星期写对后面就稳定了。如果你不想要手动调星期可以在设置日期后根据日期自动推算星期但入门项目先手动调即可不影响整体功能。3.2 时序一个字节是怎么收发成功的DS1302的通信时序整体是一个SPI主从结构但比标准SPI更简单因为它在片选信号CE拉高后开始通信每个时钟上升沿在I/O线上锁存一位数据低位先发LSB first一个字节8位。读和写的区别在于命令字节的最低位如果是0表示写如果是1表示读。标准流程是先传一个命令字节再传数据字节。我第一次写这个驱动时最困惑的是命令字节的构造。比如要读秒寄存器单片机和DS1302之间先发一个命令字节0x81。其中最高位固定是1这是DS1302规定的寄存器地址最高位必须为1最低位是1表示读中间三位是秒寄存器的地址000完整组合就是10000001即0x81。写秒就是0x80。读年月日就是0x81、0x83、0x85、0x87、0x89、0x8B、0x8D写就是对应的偶数地址。所以代码里可以用宏把读写地址都定义好别在调用时临时算。时序上还有两个细节容易被忽略。第一CE信号拉高之后要留一点时间然后再开始拉SCLK不能CE和SCLK同时变化否则第一比特可能丢失第二在SCLK的每个上升沿数据必须已经稳定所以标准驱动写法往往是“先写数据到IO口再拉高SCLK再拉低SCLK”。读方向也一样拉低SCLK后采样I/O拉高SCLK再准备下一位。总之要保证数据建立时间。DS1302的速度虽然最快能到4MHz但用51单片机慢速翻转GPIO完全没问题不存在跑不过时序的情况。3.3 一套可以直接抄的 DS1302 驱动下面驱动代码是我在自己项目里验证过的只保留最核心的读写函数适用于STC89C52。程序默认用P1.0、P1.1、P1.2接CE、SCLK、DAT。// ds1302.h #include reg52.h sbit DS1302_CE P1^0; sbit DS1302_CLK P1^1; sbit DS1302_DAT P1^2; #define DS1302_READ_SEC 0x81 #define DS1302_WRITE_SEC 0x80 #define DS1302_WRITE_PROTECT 0x8E// ds1302.c void DS1302_WriteByte(unsigned char dat) { unsigned char i; for (i 0; i 8; i) { DS1302_DAT dat 0x01; DS1302_CLK 1; DS1302_CLK 0; dat 1; } } unsigned char DS1302_ReadByte(void) { unsigned char i, dat 0; for (i 0; i 8; i) { dat 1; if (DS1302_DAT) dat | 0x80; DS1302_CLK 1; DS1302_CLK 0; } return dat; } void DS1302_WriteReg(unsigned char cmd, unsigned char dat) { DS1302_CE 1; DS1302_WriteByte(cmd); DS1302_WriteByte(dat); DS1302_CE 0; } unsigned char DS1302_ReadReg(unsigned char cmd) { unsigned char dat; DS1302_CE 1; DS1302_WriteByte(cmd); dat DS1302_ReadByte(); DS1302_CE 0; return dat; }这里有个点在读字节时我先把dat右移一位再判断I/O口是否为1如果为1就最高位置1。这个过程对应“先读最低位再依次移入高一位”因为DS1302读数据也是LSB first所以第一次读到的是最低位dat右移后恰好把它放到bit0然后下一轮读到的bit1放到bit1……循环八次后dat就是完整字节。写保护处理和写秒的示例unsigned char DecToBcd(unsigned char val) { return (val / 10) 4 | (val % 10); } unsigned char BcdToDec(unsigned char val) { return (val 4) * 10 (val 0x0F); } void DS1302_SetTime(unsigned char year, unsigned char month, unsigned char day, unsigned char week, unsigned char hour, unsigned char min, unsigned char sec) { DS1302_CE 0; DS1302_CLK 0; DS1302_WriteReg(DS1302_WRITE_PROTECT, 0x00); // 解除写保护 DS1302_WriteReg(DS1302_WRITE_SEC, DecToBcd(sec)); DS1302_WriteReg(DS1302_WRITE_SEC 2, DecToBcd(min)); DS1302_WriteReg(DS1302_WRITE_SEC 4, DecToBcd(hour)); DS1302_WriteReg(DS1302_WRITE_SEC 6, DecToBcd(day)); DS1302_WriteReg(DS1302_WRITE_SEC 8, DecToBcd(month)); DS1302_WriteReg(DS1302_WRITE_SEC 10, DecToBcd(week)); DS1302_WriteReg(DS1302_WRITE_SEC 12, DecToBcd(year)); DS1302_WriteReg(DS1302_WRITE_PROTECT, 0x80); // 开启写保护 }DS1302_WRITE_SEC 2就是分寄存器的写地址因为秒、分、时、日、月、星期、年七个寄存器地址是连续递增的每2个地址一个寄存器一个读一个写。这个地址递增规律熟悉之后写一整套时间非常顺手。还有一件事必须提醒上电初始化时最少等200ms让DS1302内部振荡器稳定再开始读时间。这个延时如果省了第一次读出来的时间很可能是0或乱码。代码里可以在main函数的开头加一个200ms的延时函数或者调用多次delay函数。4. LCD1602 驱动把时间显示出来4.1 4位模式的初始化序列LCD1602内部是HD44780控制器它上电后默认是8位模式所以要切换到4位模式必须通过特殊的“一次半个字节”的初始化序列。正确流程是上电后延时15ms以上一次性写入0x30即功能设置为8位、双行但此时液晶只收DB7~DB4所以0x30的高四位0011进入再延时5ms再次写0x30再延时5ms第三次写0x30延时1ms后写0x20此时液晶才把接口切到4位模式之后才发送0x284位模式、双行、5x8字库0x08关显示0x01清屏0x06光标右移、地址自动加10x0C开显示、关光标。这个“三次0x30、一次0x20”的流程是HD44780切换模式的关键很多新手只发一句0x28就去初始化结果液晶毫无反应。在4位模式下每条命令要分两次发送先发高4位再发低4位。每次发送时将4位数据放到DB4~DB7RS和RW设置好然后把E从1拉低产生一个下降沿液晶就在下降沿把数据锁存进去。这里有一个细节0x28明明是8位的命令但在4位模式下发两次第一次发高4位0x2第二次发低4位0x8合起来就是0x28。同理0x0C就是先发0x0再发0xC。写数据字符也是一样的流程只不过RS要拉到1表示数据模式。这段初始化代码基本是固定套路任何LCD1602项目都可以直接复用唯一需要调整的是引脚定义。4.2 显示刷新策略与光标指示在主循环里显示刷新不能太频繁否则屏幕会闪烁。我通常会设置一个节拍比如每200ms更新一次时间显示。做法是记录上一次的秒值发现秒变了才重新读时间并整行刷新。这样既保证显示流畅也把RTC通信频率降到每秒一次减少对时钟芯片的干扰。正常显示模式下固定显示第1行20XX-XX-XX 周X 第2行HH:MM:SS如果当前处于设置模式我就在当前可修改字段的位置显示下划线。比如当前修改的是“时”就把第2行“HH:MM:SS”里的“HH”替换成“__”用户一眼就能看出正在调什么。这个实现很简单每次更新显示时根据状态机的当前状态决定把哪个字段替换成下划线。刷新时机也要配合好设置模式下不要从DS1302读时间覆盖缓冲区否则刚改好的数字又被旧值冲掉。我个人推荐把时间数据存成一个数组结构如下unsigned char time_buf[7]; // year, month, day, week, hour, min, sec这样显示函数、按键修改函数都基于同一个数组工作DS1302的读取只是把寄存器的值更新到数组DS1302的写入只是把数组的值写回寄存器。整个程序的数据流变得非常清晰。5. 按键修改时间状态机才是核心5.1 三个按键的完整操作流程三个按键要实现完整的修改时间流程核心问题是怎么设计操作流程。我采用最经典的“模式切换”法正常显示状态下按K1进入设置模式此时默认锁定“年”字段在设置模式下按K1切换到下一个字段顺序为年→月→日→时→分→秒→退出设置任何时候按K2当前字段数值加1按K3减1在“秒”字段按K1会退出设置模式并把整组时间写入DS1302。这个流程不需要额外按键来做“确认”和“取消”操作直观适合课程设计演示。如果你希望用户能取消修改还可以加入一个“长按K1退出不保存”的逻辑但对51单片机的入门项目来说先把基本流程跑通更重要。在设置模式下我觉得有必要把当前字段显示出来。因为LCD1602不支持反白用下划线最简单。再配合一个“光标移动”的思想就是哪个字段在改哪个字段就变成下划线。实际上这个项目里“改年”可能需要四位显示比如“2025”但DS1302的年寄存器只存“25”后两位。所以我设计时会把“年”显示成两个字符“25”修改时也只在0~99之间循环。这样显示和寄存器的关系就完全对上了不用单独处理“20”这个前缀。5.2 时间边界处理和状态机代码处理时间边界最容易出错的是“日”的上下限不固定1、3、5、7、8、10、12月是31天4、6、9、11月是30天2月平年28天、闰年29天。如果用户把日期加到32或者把小时加到24要自动回绕到合法范围。下面给出一个简单的边界函数用查表方式判断某年某月最大天数unsigned char GetMaxDay(unsigned char year, unsigned char month) { unsigned char days[12] {31,28,31,30,31,30,31,31,30,31,30,31}; unsigned char max_day days[month - 1]; if (month 2) { unsigned int y 2000 year; if ((y % 4 0 y % 100 ! 0) || y % 400 0) { max_day 29; } } return max p a hrefhttps://download.csdn.net/download/yangpanpan111/85782832 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p
返回列表