简介:本资源是一套完整的51单片机驱动DS1302实时时钟并在1602液晶屏上显示的Keil工程文件,适合电子工程初学者、高校学生及嵌入式爱好者用于学习单片机外设驱动、DS1302读时写时流程和1602液晶指令控制原理。压缩包共23个文件,以C源文件、头文件、编译生成的obj/lst/hex及Keil工程配置为主,其中1602.c、DS1302.c、delay.c和main.c清晰分离了液晶驱动、时钟芯片通信、延时函数与主逻辑,方便按模块对照阅读;编译产物hex文件也可直接烧录验证。资源包整体仅38KB,内容精简,便于下载与快速上手。目前已有288人学习使用,是一份适合从零理解硬件接口和通信协议的入门级实践项目,也可作为课堂实验或课程设计的参考模板。
1. 51单片机驱动DS1302和1602:为什么你复制了Keil工程还是调不通
DS1302这颗时钟芯片,写驱动不难,难的是让秒位真正跳起来。网上能找到的51单片机+DS1302+1602液晶显示Keil工程文件不少,编译基本都能过,烧进开发板后屏幕却停在某个时间不动,或者干脆显示乱码。问题通常不在C源文件本身,而在读时序的细节——DS1302的数据在SCLK下降沿输出,51单片机的准双向口读引脚前如果不先写1,读到的永远是0。另外写保护位WP没清掉,时间数据根本写不进去,秒位自然纹丝不动。这篇文章从寄存器映射、读写时序、液晶初始化一路讲到Keil工程的文件划分和调试方法,目标是让秒位稳定跳动、断电后靠电池继续走时。适合正在做课程设计、电子时钟,或者想把手里的51单片机硬件设计经验延伸到RTC时钟场景的开发者。
2. DS1302寄存器与时序:写驱动前必须攻下的读、写、停振三类问题
DS1302通过SCLK、I/O、CE三根线通信,看似简单,但读和写的时序方向完全不同。写数据时数据在SCLK上升沿被采样,读数据时DS1302在SCLK下降沿把数据放到I/O线上。这个区别是驱动代码里最容易被忽略的地方,也是很多Keil工程明明能编译、运行却不正常的直接原因。在动手写C源文件之前,先把寄存器映射表放在手边。
| 寄存器功能 | 写命令字节 | 读命令字节 | 位定义说明 |
|---|---|---|---|
| 秒 | 0x80 | 0x81 | bit7为CH停振位,为1时时钟不走 |
| 分 | 0x82 | 0x83 | 无特殊控制位 |
| 时 | 0x84 | 0x85 | bit7为12/24小时制选择 |
| 日 | 0x86 | 0x87 | 1~31 |
| 月 | 0x88 | 0x89 | 1~12 |
| 星期 | 0x8A | 0x8B | 1~7 |
| 年 | 0x8C | 0x8D | 00~99 |
| 写保护 | 0x8E | 0x8F | bit7为WP,置1后禁止写入其他寄存器 |
| 涓流充电 | 0x90 | 0x91 | 配置充电二极管和电阻 |
命令字节的格式是固定的:最高位固定为1,接着是寄存器地址,最低位是读写标志位,0表示写、1表示读。地址从0x80开始,是因为秒寄存器的地址是0,左移1位再拼接读写位,就得到了表格里的命令字节。这里容易踩的坑是,命令的传输顺序是低位先出,和大多数51单片机的串口惯例一样,但和很多人习惯的高位先出思维相反,写驱动时要注意移位方向。
2.1 用GPIO模拟三线时序:先写低位还是先写高位
驱动DS1302一般用普通IO口模拟三线协议,不用单片机硬件SPI,因为SCLK频率要求低、时序也特殊。下面的C源文件函数演示了如何写一个字节到DS1302,这个函数是所有写操作的基础。CE拉高之后,每个SCLK上升沿,DS1302会采样I/O线上的电平,所以必须在上升沿之前把数据位准备好。实现上采用循环移位,从最低位开始逐位送数据:
// 向DS1302写入一个字节,低位在前 void DS1302_WriteByte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { DS1302_IO = dat & 0x01; // 取当前最低位放到数据线上 dat >>= 1; DS1302_SCLK = 1; // 上升沿,DS1302采样数据 DS1302_SCLK = 0; // 拉低,准备下一位 } }这段代码里,DS1302_IO被赋值为dat & 0x01,因为先送低位,所以取变量的最低位。循环后变量右移一位,让下一个数据位落到最低位位置。SCLK先拉高再拉低,形成一个完整的上升沿。实际工程里,每两位之间可以不加延时,因为51单片机一条IO翻转指令的周期大约是几十纳秒到几百纳秒,DS1302的时序要求很宽裕。
读时序则要反过来处理,数据在SCLK下降沿被DS1302送到I/O线上。单片机必须在SCLK拉低、数据稳定之后再读取I/O引脚。而且,51单片机的IO口是准双向模式,要读外部输入时,必须先向端口写1,把引脚切换到高阻输入状态,否则复位后端口锁存器里的0会把引脚钳死在低电平。
// 从DS1302读取一个字节,低位在前 unsigned char DS1302_ReadByte(void) { unsigned char i, dat = 0; DS1302_IO = 1; // 写1,使引脚进入高阻输入模式 for (i = 0; i < 8; i++) { dat >>= 1; DS1302_SCLK = 1; DS1302_SCLK = 0; // 下降沿,DS1302输出数据到IO线 if (DS1302_IO) { dat |= 0x80; // 当前位为1,放到最高位 } } return dat; }注意dat >>= 1放在读位之前,意味着新读到的位最终落在最高位。第一次循环时dat为0,右移后还是0,读到位1时把0x80赋给dat,第二次循环dat右移变成0x40,再读到位1时加上0x80变成0xC0。这样八次循环后,低位先读的位排列就正好是正常的字节顺序。如果循环里先dat |= 0x01再移位,最终字节会高低位颠倒,这是常见的错误写法。
2.2 写保护位WP:时间写不进去的头号嫌疑
向DS1302写入时间数据前,必须先向地址0x8E写入0x00,把WP位清掉。WP位是写保护使能位,它在出厂时默认为0,但一旦被置1,所有对时间寄存器的写操作都会被忽略。很多网上流传的keil工程文件在初始化时直接写秒、分、时,没有先清WP,导致表面上写操作执行了,实际寄存器值根本没变。完整的时间写入流程分四步:
- CE引脚拉高,使DS1302进入通信状态;
- 发送写命令字节,比如写秒就发0x80;
- 发送要写入的BCD格式数据字节;
- CE引脚拉低,结束这次通信。
清写保护的命令序列是CE拉高、发送0x8E、发送0x00、CE拉低。之后才能发送时间寄存器写入命令。VCC掉电后,DS1302由VCC1接的电池或超级电容供电,此时WP状态被保存在内部寄存器里,所以如果程序上电初始化时没清WP,电池供电期间也会一直处于写保护状态。还有一点值得注意:每次读或写操作结束后CE拉低,DS1302内部会自动复位通信逻辑,下一次操作前必须重新把CE拉高,这个顺序不能颠倒。
2.3 避免秒寄存器读出不跳变的常见错误
读取时间时,读完成功返回的数据会带特殊标志位,尤其是秒寄存器。秒寄存器的bit7是CH位,即时钟停止位。当CH为1时,内部振荡器被关断,秒位不跳变。刚拿到的新芯片CH位有可能是1,也可能在程序调试时误写入1,所以初始化时务必将0x80地址写入0x00,既清除了CH位也清除了秒数据。读取秒寄存器返回值时,需要判断bit7是否为0,如果为1,说明芯片处于停振状态。显示时用dat & 0x7F屏蔽最高位,避免把0x80这类值当成秒数据。调试时钟不走时,第一步就是读0x81检查CH位,这一步比检查晶振有没有起振更直接。
3. 1602液晶显示:HD44780初始化命令、刷新与自定义字符
1602液晶内部驱动芯片是HD44780,控制逻辑比DS1302更简单,但上电初始化顺序如果不对,屏幕可能不显示或显示乱码。这里的核心是初始化要先送三次0x30等待液晶自启动完成,再设置工作模式,最后清屏。还要区分显示数据RAM和字符生成RAM,前者存的是要显示字符的ASCII码,后者存的是自定义字符的点阵数据。
3.1 常用的1602初始化命令与延时配置
常见的做法是不查忙标志,直接用开环延时代替。初始化时先等待15毫秒以上让液晶自复位,然后写入0x30等指令。下表列出了我一般固定使用的几条指令:
| 指令 | 功能说明 |
|---|---|
| 0x38 | 8位数据总线,2行显示,5x7点阵 |
| 0x0C | 显示开,光标关,光标不闪烁 |
| 0x06 | 写入数据后地址自动加1,光标右移 |
| 0x01 | 清屏,数据指针清零,所有显示消失 |
| 0x02 | 光标归位,但不清除显示内容 |
初始化函数按照标准流程执行,每一条指令之间留出足够的延时。清屏指令耗时最长,有些芯片要1.6毫秒左右,延时给到两毫秒以上更稳妥。读写DS1302时用到的延时函数可以直接复用,只要注意51单片机在不同的晶振频率下延时参数要重新计算。
// 1602初始化:开环延时方式 void LCD1602_Init(void) { DelayMs(15); // 等待液晶上电稳定 LCD1602_WriteCmd(0x38); // 8位模式,2行显示 DelayMs(5); LCD1602_WriteCmd(0x38); DelayMs(5); LCD1602_WriteCmd(0x38); // 连续三次0x38,确保进入8位模式 DelayMs(5); LCD1602_WriteCmd(0x08); // 先关闭显示,避免上电闪烁 LCD1602_WriteCmd(0x01); // 清屏 LCD1602_WriteCmd(0x06); // 地址自动加1 LCD1602_WriteCmd(0x0C); // 开显示,关光标 }0x38连续发送三次的目的,是为了让液晶在5V电源上电过程中无论处于什么状态都能回到8位模式。写命令函数内部操作是:RS引脚置0表示写命令,RW置0表示写模式,然后把数据放到P0口,最后给E引脚一个下降沿锁存数据。写数据函数和写命令函数的区别只有一个RS引脚的电平状态。
3.2 显示时间:把DS1302读出的BCD码变成可打印字符
DS1302返回的是BCD码,比如0x59表示59秒,而1602想要显示的是字符'5'和'9',对应ASCII码0x35和0x39。直接显示BCD码会把0x59当作ASCII码的'Y',屏幕上就会出现字母而不是数字。转换公式很简单:十位数加0x30,个位数加0x30。实际写C源文件时,常用两个函数分别提取高低四位:
// BCD码转换为十进制数字字符串并显示到指定位置 void LCD1602_ShowBCD(unsigned char row, unsigned char col, unsigned char bcd) { unsigned char high = bcd >> 4; // BCD高四位是个位?注意方向 unsigned char low = bcd & 0x0F; // BCD低四位是秒个位 LCD1602_SetCursor(row, col); LCD1602_WriteData(high + '0'); // 十位数字符 LCD1602_WriteData(low + '0'); // 个位数字符 }这里有一个很隐蔽的坑:BCD编码里0x59的高四位是5,低四位是9,分别对应十位和个位。转换后的两个字符先写十位再写个位,屏幕显示顺序正确。如果直接把BCD码当作十六进制做除法取余,比如bcd / 16 * 10 + bcd % 16,得到的是十进制数值,还得再经过一次除10取余才能拆成两个字符,多绕一步。用移位和掩码的方式,一次到位。
3.3 用自定义字符在屏幕右侧指示星期
除了数字,1602还能显示自定义字符。液晶的CGRAM空间从0x40地址开始,每个5x8点阵字符占8个字节,可以定义8个自定义字符。星期指示符可以用一个向上的小箭头表示“当前指向”,或者用简单的图形表示闹钟状态。写CGRAM的思路是,先用一条命令把CGRAM地址指针指向0x40,然后连续写入8个字节点阵数据,之后在显示RAM区写入对应的字符代码0x00到0x07即可调出。这块功能在只是显示时间时不是必须的,但在课程设计里加了自定义字符,整体完成度会高很多。
4. Keil工程结构:ds1302.c、lcd1602.c、main.c 三方协作
一个能长期维护的Keil工程,不会把所有代码堆在一个C源文件里。DS1302驱动、1602驱动、主逻辑各占一个模块,模块之间通过头文件暴露接口。这样做的直接好处是,后续换用DS1307或者别的RTC芯片时,只需要替换ds1302.c,而显示部分和主循环完全不用动。以常见的keil uvision5为例,新建工程后添加这五个文件即可。
| 文件名 | 职责说明 |
|---|---|
| main.c | 主循环、初始化调用、刷新节拍控制 |
| ds1302.c | DS1302读写时序、寄存器读写封装 |
| ds1302.h | 引脚定义、函数声明 |
| lcd1602.c | 液晶初始化、清屏、光标定位、字符显示 |
| lcd1602.h | 液晶引脚定义、函数声明 |
DS1302的引脚定义放在头文件里用宏定义,这样硬件设计改线时只需要改头文件,不用翻逻辑代码。比如SCLK接P3.6、I/O接P3.5、CE接P3.4,修改这三个宏定义就能换到别的端口。但要注意,如果改到P0口这种开漏输出的IO口,外部必须接上拉电阻,否则高电平输出能力不足,DS1302读回来的数据会不稳定。
4.1 时间读取封装:返回结构体而不是散装变量
读取时间时,如果每次在主循环里读秒、读分、读时读三个寄存器,代码臃肿且容易出错。更好的做法是封装一个函数,一次性读取并转换成十进制后放进结构体。DS1302的连续读模式可以一次读出所有时间寄存器,但要注意读取过程中间数据位是连续的,不允许IO被其他中断打断,否则数据会错位。
// 时间结构体 typedef struct { unsigned char year; // 0~99 unsigned char month; // 1~12 unsigned char day; // 1~31 unsigned char week; // 1~7 unsigned char hour; // 0~23 unsigned char minute; // 0~59 unsigned char second; // 0~59 } time_t; // 读取全部时间寄存器,转换成十进制后存入结构体 void DS1302_GetTime(time_t *t) { unsigned char bcd; DS1302_CE = 0; DS1302_CE = 1; // 启动通信 DS1302_WriteByte(0xBF); // 读burst模式,一次读7字节 bcd = DS1302_ReadByte(); // 读取到的是BCD码秒 t->second = (bcd >> 4) * 10 + (bcd & 0x0F); bcd = DS1302_ReadByte(); t->minute = (bcd >> 4) * 10 + (bcd & 0x0F); bcd = DS1302_ReadByte(); t->hour = (bcd >> 4) * 10 + (bcd & 0x0F); bcd = DS1302_ReadByte(); t->day = (bcd >> 4) * 10 + (bcd & 0x0F); bcd = DS1302_ReadByte(); t->month = (bcd >> 4) * 10 + (bcd & 0x0F); bcd = DS1302_ReadByte(); t->week = (bcd >> 4) * 10 + (bcd & 0x0F); bcd = DS1302_ReadByte(); t->year = (bcd >> 4) * 10 + (bcd & 0x0F); DS1302_CE = 0; // 结束通信 }读burst模式一次拉高CE后连续输出7个字节,效率比逐字节读高,代码也短。注意burst命令字节是0xBF,对应的写burst命令是0xBE,这两个地址不是常规寄存器地址,而是DS1302专门给出的连续存取模式入口。结构体中的hour没有做12/24小时制转换,因为初始化时已经把时寄存器设置为24小时制,读出来的就是0~23的数值。
4.2 主循环的刷新节拍:200ms读一次还是每条指令都读
主循环里最简单的写法是不断地读时间、刷屏幕,但这样会让DS1302的CE引脚长时间处于频繁跳变状态,有时会影响走时稳定性。常见的做法是,主循环每200毫秒刷新一次显示,读取本身只花几十微秒,剩下时间CPU可以处理按键扫描或其他任务。刷新频率太高没有必要,秒位变化肉眼能分辨;刷新频率太低又会让人感觉显示迟钝。200毫秒是课程设计和产品原型里都不会出错的折中值。
void main(void) { time_t now; LCD1602_Init(); DS1302_Init(); DS1302_SetTime(24, 10, 31, 15, 30, 00); // 设置初始时间:2024-10-31 15:30:00 while (1) { DS1302_GetTime(&now); LCD1602_ShowTime(&now); DelayMs(200); } }主循环里没有复杂的状态机,每200毫秒读取一次并刷新显示,CPU占用率很低。DS1302_SetTime函数内部做的是清零写保护、依次写入年月日时分秒、恢复写保护三步操作。注意年份寄存器只保存两位,写入24代表2024年,超过99会溢出回0,要跨世纪显示的话,主逻辑里得自己处理20xx和21xx的前缀。
4.3 为什么DS1302初始化要放在1602初始化之后
初始化顺序有一个容易被忽略的点:先初始化1602还是先初始化DS1302,在实际运行上影响不大,但建议先把液晶点亮,再利用液晶的显示能力输出DS1302初始化过程中的状态信息。比如清写保护失败时,屏幕上直接显示WP ERR,比接调试器看变量更快定位问题。开发阶段在液晶上打印调试信息,功能稳定后再删除这些调试代码,这是调试51单片机项目时效率最高的模式。
5. 时钟不准、显示乱码、proteus仿真不出数:DS1302调试技巧与验证方法
DS1302项目调试到最后,通常只剩三类问题:时间不走、时间走但断电后丢失、proteus仿真时数不出来。前两类问题集中在芯片本身,第三类问题多半出在仿真模型的晶振配置上。
5.1 用秒寄存器的bit7判断芯片是否停振
上电后用调试器或者液晶读一次0x81寄存器,如果bit7为1,说明芯片内部振荡器没跑起来。这时检查三个点:VCC2供电是否在2.0V以上、32.768kHz晶振两端是否接了6pF左右的负载电容、CE引脚是否在通信结束后被拉低。新买的DS1302芯片CH位可能出厂就是1,或者之前在proteus仿真51单片机时误写入过1,最彻底的处理方法是在初始化函数里先向0x80写0x00,关闭停振位。接下来连续读两次秒寄存器,如果两次结果相差1,说明走时正常。
// 验证DS1302是否走时:连续读两次秒值 unsigned char sec1 = DS1302_ReadTime(0x81) & 0x7F; DelayMs(1100); unsigned char sec2 = DS1302_ReadTime(0x81) & 0x7F; // sec2 == (sec1 + 1) % 60 则走时正常这段逻辑可以放在调试阶段配合keil调试的watch窗口使用。在sec1和sec2两行分别设置断点,运行到断点时把变量添加到watch窗口观察,能直观确认读时序是否正确。keil调试时如果发现sec1和sec2总是相同,大概率是读函数出了问题,而不是芯片停振。两个值始终等于0x80,说明读到的只有CH位,需要检查读字节时是否先把IO写1。
5.2 用burst模式读7个字节并验证写保护状态
读burst模式是调试时最顺手的功能,一次拉高CE后连续收7个字节,可以快速验证所有时间寄存器。配合涓流充电寄存器0x91,还能确认电池充电配置有没有写进去。涓流充电的配置位是TCS,必须写1010才能使能,否则充电电路不工作,断电后时间数据靠电池保持,但电池永远不会被充电。DS和RS位决定二极管压降和限流电阻,常规选择是1010 0101,代表一个二极管压降加2kΩ电阻,适合3V纽扣电池。
5.3 proteus仿真时的晶振与引脚坑
在proteus仿真51单片机时,DS1302模型对32.768kHz晶振的支持和真实芯片有差异,有时必须在晶振引脚上直接挂一个频率源,或者把模型属性里的晶振频率改成数字时钟源才能起振。仿真里最容易出现的假象是时间完全不走且秒位固定,这时候用示波器看DS1302的SCLK引脚有没有脉冲,看IO脚在读取期间有没有数据变化。如果SCLK正常但IO始终低电平,检查proteus里是否忘了给DS1302的VCC1接电池,仿真模型在VCC1悬空时也会出现异常行为。晶振频率、负载电容和CE引脚的连接顺序,这三项逐个排查完,proteus里的电子时钟基本就能跑起来。
本文还有配套的精品资源,点击获取