DS1302这颗芯片,我在51单片机的项目里用了不少年。无论做电子时钟、定时插座、温湿度记录仪,还是给数据采集系统打时间戳,只要涉及“显示当前时间”或者“按日历运行”,它基本上是最省事的选择:三根线就能跟单片机通信,内含完整日历,外部接一颗32.768kHz晶振就能自主走时,还能用备用电池在断电后继续计时。这篇文章我打算把DS1302的使用方式、51单片机硬件接线、完整驱动程序一次讲透,重点是那些资料里不会写清楚的时序细节和实战排障经验。适合正在做课程设计、竞赛项目,或者第一次接触RTC芯片的开发者,也欢迎老手一起交流。
1. DS1302的底细:一颗自带日历的串行时钟
1.1 引脚与内部模块
DS1302常见DIP-8直插封装,引脚不多但功能分工很明确。我第一次用的时候看着封装图还犯了个低级错误,把VCC1和VCC2看反了,焊上后芯片完全不工作。这里先把引脚表摆出来,后面接线就不容易错:
| 引脚号 | 名称 | 功能说明 |
|---|---|---|
| 1 | VCC2 | 主电源,常规接5V或3.3V |
| 2 | X1 | 32.768kHz晶振输入 |
| 3 | X2 | 晶振输出,与X1接同一颗晶振 |
| 4 | GND | 地 |
| 5 | CE | 片选使能,高电平有效 |
| 6 | IO | 串行数据输入/输出 |
| 7 | SCLK | 串行时钟 |
| 8 | VCC1 | 备用电池正极,接3V纽扣电池 |
注意第1脚是VCC2、第8脚是VCC1,这和很多习惯把“1脚靠近VCC”封装的芯片不一样。VCC2为主电源,VCC1为备用电池。当主电源正常供电时,芯片由VCC2供电;当VCC2掉到接近电池电压以下时,芯片自动切换到VCC1供电,保证走时不中断。这个电源切换是芯片内部自动完成的,不需要外加二极管和切换电路,这是DS1302相比很多老式RTC芯片很贴心的一点。
内部模块其实可以分成三块:振荡分频电路、控制逻辑与寄存器组、31字节通用静态RAM。振荡电路把外部32.768kHz晶振分频到1Hz,驱动秒寄存器每秒走一步。RAM则是用户自由使用的存储空间,可以保存配置参数、校准值、临时标志等,掉电后由备用电池供电也照样保持数据。这个RAM很多初学者没注意,但实际做项目时非常实用,比如我可以把整点校时的偏移量存在RAM里,下次上电直接读取,不用重新配置。
1.2 寄存器地图与BCD码这个“坑”
DS1302把时间数据存在7个寄存器里,按地址从0到6依次是秒、分、时、日、月、星期、年;第7个是控制寄存器,第8个是充电控制寄存器。每次访问寄存器时,单片机先发送一个命令字节,由命令字节决定读还是写、访问时钟寄存器还是RAM、以及访问哪个寄存器。先把寄存器格式列出来:
| 寄存器 | 写命令 | 读命令 | 数据位说明 | BCD范围 |
|---|---|---|---|---|
| 秒 | 0x80 | 0x81 | bit7=CH时钟暂停,bit6-bit0为秒BCD码 | 00~59 |
| 分 | 0x82 | 0x83 | bit7固定0,bit6-bit0为分BCD码 | 00~59 |
| 时 | 0x84 | 0x85 | bit7=12/24小时制标志,bit5-bit0为时BCD码 | 00~23或01~12 |
| 日 | 0x86 | 0x87 | bit7-bit6固定0,bit5-bit0为日BCD码 | 01~31 |
| 月 | 0x88 | 0x89 | bit7-bit5固定0,bit4-bit0为月BCD码 | 01~12 |
| 星期 | 0x8A | 0x8B | bit7-bit3固定0,bit2-bit0为星期BCD码 | 01~07 |
| 年 | 0x8C | 0x8D | bit7-bit0为年BCD码 | 00~99 |
| 控制 | 0x8E | 0x8F | bit7=WP写保护,bit6-bit0固定0 | 0x00或0x80 |
| 充电 | 0x90 | 0x91 | 涓流充电选择位 | 0x00~0xFF |
这里最大的坑就是BCD码。芯片内部所有时间值都用BCD码保存,也就是说,23点这个“数值23”,在寄存器里存的是0x23,而不是0x17。如果直接把十进制数写进去,比如想设23秒,写的是0x23才对,但很多人习惯写成23(即十六进制0x17),结果读出来就是乱码。所以驱动里必须做两件事:写之前十进制转BCD,读出来之后BCD转十进制。
十进制转BCD的公式很固定:
unsigned char DecToBcd(unsigned char dec) { return ((dec / 10) << 4) | (dec % 10); }BCD转十进制则是逆过程:
unsigned char BcdToDec(unsigned char bcd) { return (bcd >> 4) * 10 + (bcd & 0x0F); }举个例子:要把42秒写进秒寄存器,42 / 10 = 4,左移4位变成0x40,加上42 % 10 = 2,得到0x42。读出来时0x42的高4位是4,低4位是2,换算成4 × 10 + 2 = 42。这样来回转就不会差。这类转换代码建议放在驱动源文件里做成工具函数,不要在主程序里到处硬编码,否则很容易弄混。
秒寄存器的bit7还有一个特殊作用:CH位(Clock Halt,时钟暂停)。CH位写1时振荡器停振,时间不走;写0时正常走时。所以设置时间时,不能把秒直接转成BCD就写进去,要先把CH位清0,也就是转换结果再 & 0x7F。很多初学者时间写进去之后发现不走,多半就是这一步没做。
小时寄存器的bit7是12/24小时制选择:0表示24小时制,1表示12小时制。工程上一般统一用24小时制,所以写小时寄存器时bit7置0。如果误设置成12小时制,读出来的小时会和实际相差12,这是一个很隐蔽的坑,等你发现显示时间对不上时,往往已经排查半天了。
控制寄存器只有bit7有实际意义,叫WP(Write Protect)。WP为1时,除了控制寄存器本身,其他时间寄存器和RAM都不能写,只能读;WP为0时允许写。芯片上电后的WP状态不固定,所以驱动初始化时第一步应该是把WP清0,确保后续能写入。等设置完时间,如果需要更稳妥,可以再把WP置1,防止意外改写。
2. 和51单片机连起来:硬件接线其实很省心
2.1 三根线加两根电源线就够
DS1302和51单片机通信只需要三根信号线:CE、SCLK、IO。CE相当于片选,SCLK是串行时钟,IO是双向数据线。这种接口比I2C还简单,因为没有设备地址、没有应答机制,纯粹是“时钟线一高一低,数据线一位一位走”的节奏。
我用51单片机(以AT89C52为例)时的典型接法如下:
| DS1302引脚 | 单片机引脚 | 说明 |
|---|---|---|
| CE | P1.0 | 片选,高电平使能 |
| SCLK | P1.1 | 串行时钟 |
| IO | P1.2 | 数据口 |
| VCC2 | +5V | 主电源 |
| VCC1 | 3V纽扣电池正极 | 备用电池,不需要可接地 |
| GND | GND | |
| X1 | 晶振一端 | |
| X2 | 晶振另一端 |
具体接哪个GPIO完全随意,改驱动里的sbit定义就行。不过要注意一点:51单片机P0口是开漏输出,需要外接上拉电阻;P1、P2、P3是准双向口,内部有弱上拉,直接接DS1302没问题。如果图省事,优先用P1口或P2口,别一上来就接P0给自己添麻烦。
2.2 晶振、电池和上拉电阻的处理
晶振方面,DS1302必须外接一颗32.768kHz的晶振,接在X1和X2之间。这颗晶振非常特殊,频率是2的15次方,也就是32768Hz,内部经过15级分频后正好得到1Hz秒脉冲。所以不能用其他频率的晶振替代。晶振两端还要各接一个负载电容到地,容值看晶振规格,常见用6pF到12pF。焊接时要让晶振尽量靠近芯片X1、X2引脚,离得太远容易引入干扰,导致不起振或者走时忽快忽慢。
备用电池方面,如果希望单片机断电后时钟继续走,就在VCC1接一颗3V纽扣电池如CR2032,电池正极接VCC1,负极接地。注意在电池和VCC1之间可以串一个二极管或者使用带隔离的电池座,防止主电源给电池反向充电。原厂芯片内部有电源切换逻辑,VCC2电压高于VCC1时由VCC2供电,反之由VCC1供电。如果不接电池,最简单的做法就是把VCC1直接接地,也能正常工作,只是断电后时间会清零。
还有一个容易被忽视的点:IO线要不要加外部上拉电阻。DS1302的IO是双向口,读取数据时由芯片驱动,写入时由单片机驱动。单片机和芯片之间来回切换驱动方向,如果单片机IO口没有内置上拉(比如P0口),或者导线较长、寄生电容较大,IO边缘就容易失真,导致误码。稳妥做法是在IO线上加一个4.7k到10k的上拉电阻到VCC。我实测下来,P1口不加也能跑,但加上上拉后时序更稳,尤其是当连接线超过20厘米的时候,差别会比较明显。
3. 三线时序:LSB在先,方向藏在命令字节里
3.1 命令字节到底怎么拼
DS1302的命令字节一共8位,每次传输都由命令字节开头。这个字节怎么拼,很多第一次用的人容易记错,我把它拆开看:
| 位 | 作用 |
|---|---|
| bit7 | 固定为1,表示这是一个DS1302命令 |
| bit6 | 0表示访问时钟/日历寄存器,1表示访问RAM |
| bit5-bit1 | 寄存器地址,共5位 |
| bit0 | 0表示写,1表示读 |
所以“写秒寄存器”的命令字节就是:bit7=1,bit6=0,bit5-bit1=00000(秒地址0),bit0=0,拼起来是1000 0000,十六进制就是0x80。读秒寄存器则bit0=1,变成0x81。同理,控制寄存器地址是7,写控制寄存器的命令是1000 0110,即0x8E;读控制寄存器是0x8F。
特别要注意整体发送顺序:字节从最低位开始串行发送。也就是说0x80这个字节实际出现在IO线上的顺序是0、0、0、0、0、0、0、1,最后才是bit7。如果按常规SPI那种先发最高位的习惯写代码,就会把命令识别成完全不同的地址,读回来的数据永远是错的。这个点是我见过最多人栽跟头的地方,尤其是在熟悉SPI或者I2C之后再转过来用这个芯片,很容易顺手按旧套路写。
3.2 写时序与读时序的细微差别
写数据的完整过程是:
- CE拉高,使能芯片
- 发送8位命令字节,LSB first
- 紧接着发送8位数据字节,LSB first
- CE拉低,结束本次操作
SCLK默认低电平。每发送一位时,先把数据放到IO线上,然后SCLK拉高产生上升沿,芯片在上升沿锁存数据,再拉低SCLK为下一位做准备。整个过程就是“放数据、打脉冲、拉低”的循环,像老式的机械密码锁一格一格拨。
读数据的过程前半段和写一样,先发送命令字节,但命令字节的bit0是1。命令字节发送完毕后,IO线要释放,也就是单片机把IO引脚置1,把总线控制权交出去,由DS1302开始驱动IO线向外输出数据。接下来仍然是SCLK打脉冲,但读取时机要注意:常用做法是让SCLK先为低,趁低电平期间采样IO电平,再拉高拉低产生一个脉冲,然后继续读下一位。这样每位数据都是在时钟低电平期间稳定读取的,正好躲开翻转瞬间的毛刺。
这里还有一个很多新手不知道的细节:51单片机读引脚前,必须先向端口写1。比如IO接在P1.2,读之前执行DS1302_IO = 1,再读DS1302_IO的电平,否则读到的可能是端口锁存器的旧值。在C代码里这个动作看起来像输出一个1,实际作用是“释放总线并准备输入”。如果不写这一句,读回来的数据经常会莫名其妙地固定成0xFF。
时序上CE的建立时间也要留意。数据手册要求CE拉高后,要等一小段时间再过几百纳秒再开始发SCLK脉冲;操作结束后CE拉低前,最后一个SCLK下降沿也要和CE之间留间隔。51单片机标准架构下每条指令执行时间在微秒级,天然满足这些时间要求,所以通常不用专门加延时。但如果你换成了高速单片机,比如用STC系列跑几十兆主频,或者用库函数直接翻转IO,就要考虑加几个空操作撑一下时序,否则可能出现偶发读写失败。
3.3 追求效率可以试试Burst模式
除了一个一个寄存器地读,DS1302还支持猝发模式,也就是Burst Read/Write。读所有时间寄存器的命令是0xBF,写所有时间寄存器用0xBE。CE拉高后,发命令字节,然后连续读写8字节,顺序依次是秒、分、时、日、月、星期、年、控制寄存器。也就是说,一次CE周期就能把完整时间全部取出来,效率比逐寄存器读高不少。
我在做需要频繁刷新时间显示的项目时,比较喜欢用Burst读。注意读回来的第8个字节是控制寄存器,时间数据只有前7个,别把它混进时间数组。Burst写也是一样,一次要写完8个字节,如果中途CE拉低,剩余字节无效,所以使用前要确保数组里8个位置都准备好。对大多数51项目来说,逐字节读写已经够用,Burst模式算是进阶优化选项。
4. 驱动代码:从底层字节读写到完整时间读写
4.1 引脚定义和基础工具函数
我直接用Keil C51的sbit语法定义引脚,方便按自己的板子改:
// ds1302.h #ifndef __DS1302_H__ #define __DS1302_H__ #include <reg52.h> sbit DS1302_CE = P1^0; // 片选 sbit DS1302_IO = P1^1; // 数据 sbit DS1302_SCLK = P1^2; // 时钟 void DS1302_Init(void); void DS1302_SetTime(unsigned char *pTime); void DS1302_GetTime(unsigned char *pTime); #endif把秒、分、时这些寄存器的写命令宏定义好,方便后面直接使用:
// ds1302.c #include "ds1302.h" #define DS1302_SEC_W 0x80 #define DS1302_MIN_W 0x82 #define DS1302_HOUR_W 0x84 #define DS1302_DATE_W 0x86 #define DS1302_MONTH_W 0x88 #define DS1302_DAY_W 0x8A #define DS1302_YEAR_W 0x8C #define DS1302_CTRL_W 0x8E #define DS1302_SEC_R 0x81 #define DS1302_MIN_R 0x83 #define DS1302_HOUR_R 0x85 #define DS1302_DATE_R 0x87 #define DS1302_MONTH_R 0x89 #define DS1302_DAY_R 0x8B #define DS1302_YEAR_R 0x8D #define DS1302_CTRL_R 0x8F4.2 字节级读写,第二个函数藏着移植关键
写一个字节,最典型也最好懂的写法:
void DS1302_WriteByte(unsigned char dat) { unsigned char i; for (i = 0; i < 8; i++) { DS1302_IO = dat & 0x01; // 先放最低位 DS1302_SCLK = 1; // 上升沿,芯片锁存 DS1302_SCLK = 0; // 拉低,为下一位做准备 dat >>= 1; // 准备下一个bit } }读一个字节,代码要稍微讲究一点:
unsigned char DS1302_ReadByte(void) { unsigned char i, dat = 0; DS1302_IO = 1; // 释放IO,切换为输入 for (i = 0; i < 8; i++) { dat >>= 1; // 已收到的数整体右移,腾出最高位 if (DS1302_IO) dat |= 0x80; // 当前电平放到最高位 DS1302_SCLK = 1; DS1302_SCLK = 0; } return dat; }这段读函数是很多初学者看不懂的地方。因为DS1302是LSB先发送,第一位出现在IO上的其实是字节的最低位。我们用一个“先右移再把读到的电平放最高位”的循环,经过8次移位,最早收到的那个bit被一步步挪到最低位,最后一位收到的bit留在最高位,正好完成LSB-first数据的重新排列。你可以拿笔在纸上推一遍,或者用逻辑分析仪对比一下IO上的实际顺序,就明白这个函数设计的巧妙了。
如果想让时序更稳,可以在每个SCLK翻转之间插入几个空操作:
#include <intrins.h> #define DS1302_DELAY() _nop_(); _nop_(); _nop_()然后把翻转代码改成:
DS1302_IO = dat & 0x01; DS1302_SCLK = 1; DS1302_DELAY(); DS1302_SCLK = 0; DS1302_DELAY();标准51在12MHz晶振下不加延时也能跑通,因为IO翻转本身不是瞬时的,内部锁存窗口足够宽。但换用高速单片机后,一个IO翻转可能只有几十纳秒,反而可能比DS1302的最小脉冲宽度还快,这时候就需要用延时把SCLK高、低电平的时间撑到几百纳秒以上。
4.3 读时间、写时间、初始化的完整实现
有了底层字节读写,封装寄存器访问就很简单了。写寄存器时先拉高CE,然后写命令字节,再写数据字节,最后拉低CE:
void DS1302_WriteReg(unsigned char cmd, unsigned char dat) { DS1302_CE = 1; DS1302_WriteByte(cmd); DS1302_WriteByte(dat); DS1302_CE = 0; }读寄存器时发送的命令字节要把bit0置1,也就是读标志,命令字节发送完成后直接读数据:
unsigned char DS1302_ReadReg(unsigned char cmd) { unsigned char dat; DS1302_CE = 1; DS1302_WriteByte(cmd | 0x01); // 转成读命令 dat = DS1302_ReadByte(); DS1302_CE = 0; return dat; }这样调用时只需要传写命令地址,函数内部自动把bit0置1,省得每次记读命令的数值。
初始化函数的任务只有一件:清写保护,让后续可以写时间。
void DS1302_Init(void) { DS1302_CE = 0; DS1302_SCLK = 0; DS1302_WriteReg(DS1302_CTRL_W, 0x00); // WP清0 }设置时间函数,传入一个长度7的数组,依次是秒、分、时、日、月、星期、年。注意这个数组顺序我在代码注释里写得很清楚,网上有些代码顺序是年月日,照搬时特别容易混淆:
void DS1302_SetTime(unsigned char *pTime) { unsigned char tmp; DS1302_WriteReg(DS1302_CTRL_W, 0x00); // 关写保护 // 秒寄存器的CH位必须清0,时钟才会走 tmp = DecToBcd(pTime[0]) & 0x7F; DS1302_WriteReg(DS1302_SEC_W, tmp); DS1302_WriteReg(DS1302_MIN_W, DecToBcd(pTime[1])); DS1302_WriteReg(DS1302_HOUR_W, DecToBcd(pTime[2])); DS1302_WriteReg(DS1302_DATE_W, DecToBcd(pTime[3])); DS1302_WriteReg(DS1302_MONTH_W, DecToBcd(pTime[4])); DS1302_WriteReg(DS1302_DAY_W, DecToBcd(pTime[5])); DS1302_WriteReg(DS1302_YEAR_W, DecToBcd(pTime[6])); DS1302_WriteReg(DS1302_CTRL_W, 0x80); // 重新开启写保护 }读时间函数同样用长度7的数组回传当前时间:
void DS1302_GetTime(unsigned char *pTime) { unsigned char tmp; tmp = DS1302_ReadReg(DS1302_SEC_R); pTime[0] = BcdToDec(tmp & 0x7F); // 屏蔽CH位 pTime[1] = BcdToDec(DS1302_ReadReg(DS1302_MIN_R)); pTime[2] = BcdToDec(DS1302_ReadReg(DS1302_HOUR_R)); pTime[3] = BcdToDec(DS1302_ReadReg(DS1302_DATE_R)); pTime[4] = BcdToDec(DS1302_ReadReg(DS1302_MONTH_R)); pTime[5] = BcdToDec(DS1302_ReadReg(DS1302_DAY_R)); pTime[6] = BcdToDec(DS1302_ReadReg(DS1302_YEAR_R)); }再次强调两个细节:读秒寄存器时要去掉bit7再转BCD,否则当CH标志位为1时读出来的数值会大于59,显示直接异常;设置时间时如果不需要重新打开写保护,也可以不写最后那行 DS1302_WriteReg(DS1302_CTRL_W, 0x80),看应用需求来定。
4.4 RAM读写:被很多人忽略的免费存储空间
DS1302自带31字节RAM,掉电不丢失,这是一个非常实用的功能。RAM的访问命令和时钟寄存器是分开的,命令字节bit6要置1,bit5-bit1是RAM地址,bit0还是读写标志。写RAM地址0的命令是0xC0,读RAM地址0的命令是0xC1。每增加一个地址,命令值加2。
封装两个简单的函数:
void DS1302_WriteRam(unsigned char addr, unsigned char dat) { if (addr > 30) return; DS1302_CE = 1; DS1302_WriteByte(0xC0 | (addr << 1)); // 写RAM命令 DS1302_WriteByte(dat); DS1302_CE = 0; } unsigned char DS1302_ReadRam(unsigned char addr) { unsigned char dat; if (addr > 30) return 0; DS1302_CE = 1; DS1302_WriteByte(0xC1 | (addr << 1)); // 读RAM命令 dat = DS1302_ReadByte(); DS1302_CE = 0; return dat; }我可以把设备编号、校准参数、开机标志都放在RAM里。比如做一个需要记录累计开机次数的设备,每次开机把RAM里的值加1,断电后依然保留,比用外部EEPROM更省引脚。前提是VCC1接了备用电池,否则RAM数据在主电源断电后会丢失,这一点要明白它的边界。
4.5 主程序调用示例
主程序里初始化和使用非常简单:
// main.c #include <reg52.h> #include "ds1302.h" // 定义并初始化一个全局时间数组 unsigned char gTime[7] = {0, 30, 23, 25, 12, 3, 24}; // 对应:秒0,分30,时23,日25,月12,星期3,年24 // 即 2024年12月25日 23:30:00,星期三 void delay_ms(unsigned int ms); void main(void) { unsigned char now[7]; DS1302_Init(); DS1302_SetTime(gTime); while (1) { DS1302_GetTime(now); // now[6]是两位年份,要显示20xx需要加2000 // now[4]是月份,now[3]是日,now[2]是时,now[1]是分,now[0]是秒 // 接下来把数组送到LCD1602或者串口输出即可 delay_ms(500); } }这里只是示意,真正工程中往往只在上电时根据需要判断是否校准时间,平时只调用GetTime,不要反复执行SetTime,否则每次都会重新写入所有寄存器,虽然不影响走时,但会增加不必要的IO操作和出错概率。如果只是调试,也可以跳过SetTime直接读,因为DS1302一旦启动就会自己走。
5. 实测排障:我把常见坑都替你踩了一遍
5.1 晶振就是不起振
现象:时间永远是初始值,秒位几乎不动;或者时而走时而不走。
检查思路:先用示波器或逻辑分析仪测X1引脚,看有没有32.768kHz振荡波形。注意普通示波器探头本身有约10pF到20pF的寄生电容,插上去可能把振荡电路压停振,所以看到波形消失不一定是真没振。如果没有示波器,最简单的验证办法是:把秒寄存器先手动写成0x00,隔1秒再读一次,如果变成0x01,说明时钟已经在走。这个办法最快,也最不需要额外设备。
如果确认没有振荡,大概率是晶振没接好、负载电容不合适、或者晶振本身质量有问题。低频晶振对寄生电容非常敏感,芯片引脚到晶振的走线太长,或者旁边有大电流跳变的走线,都可能让振荡器停振。把晶振和电容尽量贴近芯片引脚焊接,通常能解决。要注意的是,有些模块板上晶振装在背面,信号路径绕了一圈,表现就是不稳定的走时,这种情况自己搭一个直插电路反而更可靠。
5.2 一读就是0xFF或0x00
现象:读寄存器返回的全是0xFF,或者全是0x00,不管哪个地址都一样。
看到0xFF,基本可以断定IO线在读取时被拉高了,而芯片没能正常驱动IO。常见原因有几个:CE没拉高就开始发命令;命令字节发送顺序反了,芯片根本不认识;读数据前没有把IO置1释放总线;IO线短路到VCC。逐一排查,先检查接线,再检查命令字节是不是真的LSB first。
看到0x00,则通常是IO线一直为低,可能是IO引脚没接对,或者芯片供电有问题。先量一下VCC2电压是不是正常范围,再检查GND,最后看IO和SCLK两条线之间有没有被焊锡连在一起。这类问题90%是硬件连线疏忽,而不是芯片坏了。
5.3 时间能写进去,但一上电就不对
现象:写完时间后能正常走,可只要断电再上电,时间就回到初始值,或者干脆不走。
先区分两种情况:有备用电池时,断电后时间丢失,说明VCC1上没有正常供电。检查电池正极是否接到了VCC1、负极是否接地、电池座有没有氧化或者虚焊。CR2032这种锂电池在DS1302的负载下能用很久,但长期存放的设备还是建议定期检查电池电压。
没有备用电池时,断电丢时间是正常行为,别当成故障。有些芯片内部RAM和时钟寄存器在没有备用电源时依然被浅度供电维持,但绝大多数情况下是不保证的。如果设计要求必须断电保持时间,唯一的正道就是接好VCC1电池,或者外围自己加一个超级电容,撑过短暂断电。
5.4 时间每天慢几秒,能不能软校准
DS1302的走时精度完全取决于外部晶振。普通32.768kHz晶振的频率容差通常是±20ppm,换算下来一天误差大约1.7秒,一个月就是几十秒。这个误差是正常的,不是芯片坏了,也不是程序写错了。如果实测下来误差特别大,比如一天慢十几秒,那多半是晶振频率偏得离谱,换一颗正规厂家的晶振比调软件高效得多。
要求比较高的场景,可以换温补晶振,也就是TCXO,或者用有源温补晶振,误差能控制在几个ppm以内。没有硬件条件的,也可以用软件校准思路:每天固定时刻通过串口、网络或者按键对时一次,把偏差修正回来。我在一个数据记录仪项目里就是这么干的,每天凌晨3点整通过串口从上位机校时,长期跑下来时间一直很准。芯片本身没有补偿寄存器,所以软件校时是最可行的廉价方案。
5.5 逻辑分析仪看一眼,胜过瞎猜一小时
调试DS1302这类三线串行芯片,最推荐的排查手段是用逻辑分析仪抓IO和SCLK的波形。采样率不用太高,1MHz就够看32.768kHz晶振和串行时序了。抓一次读写操作,对照命令字节的预期波形,马上就能看出是命令发反了、CE时序不对、还是IO切换方向有问题。
举个例子,写秒命令0x80的波形应该是:CE先拉高,SCLK打8个脉冲,IO在每个脉冲的上升沿之前依次为0、0、0、0、0、0、0、1,然后再来8个脉冲发数据字节。如果你抓到的IO顺序是1、0、0、0、0、0、0、0,那就说明代码把MSB first和LSB first搞反了,把传输函数改成从最低位开始即可。这种问题用眼睛盯代码很难发现,但波形上一眼就穿帮。
个人体会是,做串行器件驱动,先花半小时把协议在纸上画一遍,再用逻辑分析仪验证一次,之后基本不会再因为这些低级问题浪费时间。
DS1302这个芯片,核心知识点其实就三样:命令字节的结构、LSB first的时序、BCD码转换。把这三点吃透,驱动能力基本就有了。我个人的体会是,做这类串行时钟芯片,不要急着网上复制代码,先拿着数据手册把时序图看明白,再用逻辑分析仪抓一遍实际波形,把“发命令-读写数据-结束”的节奏搞清楚,后面换任何RTC芯片都会非常快。最后再分享一个小技巧:调试DS1302时,先把秒寄存器设为0x00,然后观察它下一秒是否变成0x01,这是最快确认晶振和通信都正常的办法,比拿示波器到处戳探头省事多了。