资讯详情 STM32 GPIO模拟三线时序驱动DS1302实时时钟完整教程
📅 2026/10/13 1:27:54
最近在做一个小项目需要给设备加上掉电不丢时间的功能顺手就把DS1302这颗经典实时时钟芯片调通了。本来只想快速搞定功能结果越挖越有意思干脆整理成一篇开源性质的学习笔记把从硬件接线到驱动代码再到调试排坑的整个过程都记录下来。如果你正在用STM32驱动DS1302或者准备拿它练手学习GPIO模拟时序这篇笔记应该能让你少走不少弯路。先说结论DS1302这颗芯片虽然老但作为学习IC通信的入门素材非常合适——它没有I2C那么复杂的状态机也没有标准SPI的多种模式配置问题用三根线就能把数据收进来发出去逻辑非常直白。我用的是一块常见的STM32F1系列开发板主频72MHz通过普通GPIO模拟三线时序来驱动DS1302全程没有用到硬件外设。这么做的好处后面细说但先剧透一句移植性极好换任何MCU都能用这也是我坚持用GPIO模拟而不是偷懒上硬件SPI的原因。1. 项目概述与设计思路1.1 DS1302到底是什么一颗会自己数秒的芯片DS1302是一颗串行实时时钟芯片核心功能就是上电后自动走时通过外部32.768kHz晶振产生精准的秒脉冲。它内部维护了年、月、日、时、分、秒、星期这七组时间数据全部以BCD码格式存储读出来就是直接能用十进制转换的数字。更实用的一点是它自带31字节静态RAM掉电不丢失可以用来存一些配置参数、校准数据或者设备序列号之类的小量信息。这颗芯片采用双电源供电设计主电源和备份电源引脚分开主电掉电后自动切换到备用电源继续走时。这意味着只要纽扣电池还有电系统完全断电后时间依然在走下次重新上电读到的时间就是连续的不需要每次开机手动校时。这对很多需要记录事件时间戳的嵌入式设备非常关键。DS1302在工作电压范围上比较宽容2.0V到5.5V都能跑和STM32的3.3V电平直接连接不需要加电平转换电路。通信接口是典型的三线制CE片选、SCLK串行时钟、I/O双向数据线。虽然看起来和SPI很像但它和标准SPI有个关键差异——数据是LSB先行的而且读写是半双工方式。1.2 为什么不用硬件SPIGPIO模拟的三线时序很多初学者上来就想用STM32的硬件SPI去接DS1302觉得省事但实际做下来反而更麻烦。原因有三个第一DS1302虽然是三线接口但它的时序本质上是自成一派的“类似SPI但并非SPI”协议。它的数据在SCLK上升沿写入芯片在下降沿由芯片输出这个行为模式确实接近SPI模式0但标准SPI是全双工同时收发DS1302则是先发命令字节再收或发数据字节方向切换逻辑和SPI外设的行为不完全一致。第二DS1302要求LSB先行而STM32F1系列的硬件SPI虽然也能配置成LSB先行但配置项和中断处理逻辑会变得更绕。为了兼容一颗老芯片去折腾SPI外设的LSB模式、NSS引脚行为整个工程的复杂度和出错概率不降反升。第三也是最实际的原因GPIO模拟方案把整个时序过程完全摊开在代码里每一步做什么都清清楚楚一旦出问题用逻辑分析仪抓一遍就能定位。硬件SPI虽然代码短但出了问题不好查时序细节全部藏在外设内部。所以我的设计决策很明确用普通的推挽输出GPIO模拟SCLK和CE用可切换方向的GPIO模拟数据线I/O通过空循环制造亚微秒级延时来满足DS1302的时序要求。这颗芯片的通信速率上限本来只有4MHz实际工作中给它几百KHz的SCLK就绰绰有余GPIO翻转速度完全够用。2. 硬件设计与关键细节2.1 引脚连接与最小系统DS1302最常用的封装是8脚DIP或SOIC引脚数量少接线非常直接。我用的是开发板上的三个空余GPIO引脚连接关系如下DS1302引脚功能说明连接到STM32VCC2主电源3.3VVCC1备用电池正极纽扣电池正极CE片选使能PB12SCLK串行时钟PB13I/O双向数据PB14X1 / X2晶振引脚32.768kHz晶振两端GND地GND这里有一个特别容易出问题的点CE引脚的电平状态非常敏感建议在CE和GND之间加一个10kΩ下拉电阻。原因是DS1302在上电瞬间或MCU复位期间如果CE引脚处于悬空状态可能被干扰信号误触发通信导致芯片状态错乱。VCC1备用电池可以接3V纽扣电池型号用常见的CR2032或者小一点的CR1220都行。注意电池座正极接VCC1负极接GND。如果不想接备用电池直接把VCC1也接到3.3V主电源也能正常工作只是掉电后时间会丢失。去耦电容方面VCC2和GND之间放一个100nF陶瓷电容尽量靠近芯片电源引脚。这个电容位置放不好会导致芯片供电纹波太大严重时晶振会停振时间直接不走。2.2 晶振选择与备用电池注意事项DS1302必须外接一颗32.768kHz的晶振才能走时频率精度直接决定了时钟的漂移速度。普通的圆柱晶振2×6mm封装就行频率误差一般在±20ppm也就是一天大概差一两秒的量级。如果项目对时间精度要求高可以选温补晶振或者加软件校准但学习用途完全没必要。晶振的两个引脚分别接X1和X2不需要接外部负载电容DS1302芯片内部已经集成了6pF左右的负载电容直接用就行。曾经见过有人按常规习惯在晶振两端各加一个15pF电容结果反而导致起振困难甚至干脆不起振。这个坑需要注意DS1302确实和普通MCU晶振电路不太一样别画蛇添足。备用电池如果使用一次性纽扣电池绝对不要开启芯片的涓流充电功能否则电池会被持续充放电轻则提前耗尽重则漏液损坏电路板。如果使用可充电纽扣电池比如LIR2032可以通过控制寄存器配置涓流充电但充电电流的选择要保守通常选2kΩ限流电阻档位。这个配置后面代码部分细说。另外有一个硬件细节第一次焊接完上电后如果发现DS1302没有起振先别急着怀疑芯片坏了用示波器测一下X1引脚是否有32.768kHz的正弦波信号。很多时候是晶振虚焊或者烙铁焊接时间过长导致晶振内部晶体受损补焊一颗新晶振就恢复了。3. 驱动代码实现从时序到API3.1 底层时序字节读写是怎么工作的DS1302的所有操作都是从CE拉高开始的。CE拉高使能通信后SCLK上升沿把数据写入芯片下降沿让芯片送出数据。整个“命令字节数据字节”传输过程中CE必须始终保持高电平全部结束后才能拉低。有一个细节非常重要写入数据时数据引脚要配置为输出模式而读取数据时在写完命令字节之后、开始读数据之前必须把数据引脚切换为输入模式。如果忘了切换方向读回来的数据不是固定高电平就是固定低电平根本拿不到真实值。字节传输的顺序是LSB先行也就是先从最低位开始发。这一点和大多数SPI外设的MSB先行正好相反代码写起来要特别注意。延时方面DS1302对时序要求不算苛刻SCLK高低电平持续时间只要大于几百纳秒就没问题。我直接用一个空循环延时函数来实现72MHz主频下一个循环大约耗时1微秒足够余量。3.2 寄存器读写与BCD码转换DS1302的命令字节格式很规律最高位bit7恒为1bit6用于区分操作对象bit6等于0时访问时钟寄存器等于1时访问RAMbit5到bit1是地址最低位bit0表示读还是写1为读0为写。常用寄存器地址和命令字对应关系如下表寄存器写命令读命令数据范围说明秒0x800x810x00-0x59bit7为CH1停止振荡器分0x820x830x00-0x59正常BCD时0x840x850x00-0x2324小时模式日0x860x870x01-0x31按月份增减月0x880x890x01-0x12BCD星期0x8A0x8B0x01-0x071为周日7为周六年0x8C0x8D0x00-0x99存两位BCD码写保护0x8E0x8F0x80/0x00bit7为WP1时禁止写寄存器涓流充电0x900x91配置字节需匹配TCS1010才生效BCD码和十进制互相转换是驱动DS1302必须要处理的环节。BCD码的含义就是一个字节拆成高四位和低四位分别表示十位和个位比如秒寄存器读到0x45代表的就是45秒不能用普通的十六进制转十进制的方式去理解。转换函数等下给出。3.3 时间和RAM读写API封装驱动代码我拆成了两个文件ds1302.h只管接口定义ds1302.c负责具体实现。先看头文件的核心部分#ifndef __DS1302_H #define __DS1302_H #include stm32f1xx_hal.h // 引脚映射按自己的硬件修改 #define DS1302_CE_PORT GPIOB #define DS1302_CE_PIN GPIO_PIN_12 #define DS1302_SCLK_PORT GPIOB #define DS1302_SCLK_PIN GPIO_PIN_13 #define DS1302_IO_PORT GPIOB #define DS1302_IO_PIN GPIO_PIN_14 #define DS1302_CE_HIGH() HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_SET) #define DS1302_CE_LOW() HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_RESET) #define DS1302_SCLK_HIGH() HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_SET) #define DS1302_SCLK_LOW() HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_RESET) typedef struct { uint8_t year; // 0-99实际年份 2000 year uint8_t month; // 1-12 uint8_t day; // 1-31 uint8_t hour; // 0-23 uint8_t minute; // 0-59 uint8_t second; // 0-59 uint8_t week; // 1-7 } DS1302_TimeTypeDef; void DS1302_Init(void); void DS1302_SetTime(DS1302_TimeTypeDef *time); void DS1302_GetTime(DS1302_TimeTypeDef *time); uint8_t DS1302_ReadRAM(uint8_t addr); void DS1302_WriteRAM(uint8_t addr, uint8_t data); #endif这里做一个重要说明DS1302的星期寄存器编码并不是国际通用的星期一到星期日而是1代表周日、2代表周一一直到7代表周六。换算的时候要注意别搞反了。我平时习惯在应用层做一个星期数组映射比如const char *weekdays[] {, 周日, 周一, 周二, 周三, 周四, 周五, 周六}直接用寄存器值当索引。接下来是核心实现文件先看底层的字节读写和IO方向切换#include ds1302.h // 数据线方向控制 static void DS1302_IO_Output(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DS1302_IO_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_NOPULL; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DS1302_IO_PORT, GPIO_InitStruct); } static void DS1302_IO_Input(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DS1302_IO_PIN; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(DS1302_IO_PORT, GPIO_InitStruct); } // 简单延时72MHz下大约1us static void DS1302_Delay(void) { for (volatile uint8_t i 0; i 10; i); } // 写一个字节LSB先行 static void DS1302_WriteByte(uint8_t dat) { DS1302_IO_Output(); for (uint8_t i 0; i 8; i) { if (dat 0x01) HAL_GPIO_WritePin(DS1302_IO_PORT, DS1302_IO_PIN, GPIO_PIN_SET); else HAL_GPIO_WritePin(DS1302_IO_PORT, DS1302_IO_PIN, GPIO_PIN_RESET); DS1302_SCLK_HIGH(); DS1302_Delay(); DS1302_SCLK_LOW(); DS1302_Delay(); dat 1; } } // 读一个字节LSB先行 static uint8_t DS1302_ReadByte(void) { uint8_t dat 0; DS1302_IO_Input(); for (uint8_t i 0; i 8; i) { dat 1; if (HAL_GPIO_ReadPin(DS1302_IO_PORT, DS1302_IO_PIN) GPIO_PIN_SET) dat | 0x80; DS1302_SCLK_HIGH(); DS1302_Delay(); DS1302_SCLK_LOW(); DS1302_Delay(); } return dat; }读字节这段代码刚起步的人容易绕昏这里拆开讲一下。初始化时dat是0每处理一位先把dat右移一位把高位腾出来然后判断数据线上是高电平还是低电平。如果是高电平把dat的bit7置1如果是低电平bit7保持0。循环8次之后最先收到的LSB就一路右移到了bit0位置整个字节顺序就对了。这和写字节时从bit0开始发送是严格对应的。接下来是命令读写和BCD转换static uint8_t BCD2Dec(uint8_t bcd) { return (bcd 4) * 10 (bcd 0x0F); } static uint8_t Dec2BCD(uint8_t dec) { return ((dec / 10) 4) | (dec % 10); } static void DS1302_WriteCmd(uint8_t cmd, uint8_t data) { DS1302_CE_HIGH(); DS1302_WriteByte(cmd); DS1302_WriteByte(data); DS1302_CE_LOW(); } static uint8_t DS1302_ReadCmd(uint8_t cmd) { uint8_t data 0; DS1302_CE_HIGH(); DS1302_WriteByte(cmd); data DS1302_ReadByte(); DS1302_CE_LOW(); return data; }BCD转换函数很好记十进制转BCD就是把十位数放到高四位个位数放到低四位。BCD转十进制就是反过来高四位乘以10加上低四位。读时间函数要把七个寄存器的值全部取回来注意读操作和写操作在命令字上的区别仅仅是最低位的0和1void DS1302_GetTime(DS1302_TimeTypeDef *time) { time-second BCD2Dec(DS1302_ReadCmd(0x81)); time-minute BCD2Dec(DS1302_ReadCmd(0x83)); time-hour BCD2Dec(DS1302_ReadCmd(0x85)); time-day BCD2Dec(DS1302_ReadCmd(0x87)); time-month BCD2Dec(DS1302_ReadCmd(0x89)); time-week BCD2Dec(DS1302_ReadCmd(0x8B)); time-year BCD2Dec(DS1302_ReadCmd(0x8D)); }写时间的时候有一个很多教程没讲透的细节写秒寄存器时秒寄存器最高位是时钟暂停位CH置1时振荡器停止置0时启动。如果直接把时间写进去而CH位恰好为1芯片就一直不走时。稳妥的做法是先停止时钟把所有时间写完后最后再写一次秒寄存器把CH位清零同时写入正确的秒值这样就能保证走时从设定的时间点正式开始。void DS1302_SetTime(DS1302_TimeTypeDef *time) { // 关闭写保护 DS1302_WriteCmd(0x8E, 0x00); // 写秒寄存器时CH位先置1停止时钟振荡 DS1302_WriteCmd(0x80, Dec2BCD(time-second) | 0x80); DS1302_WriteCmd(0x82, Dec2BCD(time-minute)); DS1302_WriteCmd(0x84, Dec2BCD(time-hour)); DS1302_WriteCmd(0x86, Dec2BCD(time-day)); DS1302_WriteCmd(0x88, Dec2BCD(time-month)); DS1302_WriteCmd(0x8A, Dec2BCD(time-week)); DS1302_WriteCmd(0x8C, Dec2BCD(time-year)); // 开启写保护 DS1302_WriteCmd(0x8E, 0x80); // 重新启动时钟写入真正的秒值 DS1302_WriteCmd(0x80, Dec2BCD(time-second)); }写保护是一个很容易被忽略的机制。DS1302内部的控制寄存器bit7是WP位为1时整个时钟寄存区和涓流充电配置都被写保护数据写不进去。所以每次写时间之前必须先把WP清0写完再恢复。如果发现写时间没反应十有八九是写保护没关。RAM的读写逻辑和时钟寄存器几乎一模一样只是命令字的bit6变成1地址从0x20到0x3F对应芯片内部的31字节静态RAM。访问RAM和时钟寄存器使用相同的命令字结构但命令字bit6不同RAM命令字最低字节范围是0xC0到0xFE。封装如下void DS1302_WriteRAM(uint8_t addr, uint8_t data) { if (addr 31) return; DS1302_WriteCmd(0x80 | (addr 1) | 0x40, data); // 写RAMbit61bit00 } uint8_t DS1302_ReadRAM(uint8_t addr) { if (addr 31) return 0; return DS1302_ReadCmd(0x81 | (addr 1) | 0x40); // 读RAMbit61bit01 }初始化函数很简单只需要把GPIO初始化做好、CE拉低、SCLK拉低void DS1302_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); GPIO_InitStruct.Pin DS1302_CE_PIN | DS1302_SCLK_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Pull GPIO_PULLDOWN; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(GPIOB, GPIO_InitStruct); DS1302_CE_LOW(); DS1302_SCLK_LOW(); }这里GPIO初始化有个小技巧CE和SCLK引脚配成内部下拉。CE引脚本来就该加外部下拉电阻内部下拉能起到双保险作用防止上电瞬间引入噪声干扰导致误触发通信。SCLK配下拉则是保证SCLK默认处于低电平符合时序要求。4. 应用层整合与演示4.1 在STM32工程里怎么组织文件驱动层放在一个模块里应用层单独放我实际项目里目录大概长这样Project/ ├── Core/ │ ├── Inc/ │ └── Src/ │ └── main.c ├── Drivers/ │ └── BSP/ │ ├── Inc/ │ │ └── ds1302.h │ └── Src/ │ └── ds1302.cmain函数里不需要对DS1302做任何时序层面的操作只调用接口就行。这样分层的好处是驱动可以原封不动地搬到其他工程应用层怎么展示时间完全看业务需求。初始化顺序上有一个容易踩的坑DS1302的Init函数必须在系统时钟配置完成之后再调用。因为GPIO的翻转速度依赖系统时钟如果在SystemClock_Config()之前就初始化GPIO并操作DS1302延时函数的时间基准就不对可能导致通信失败。虽然没有严格到会立刻出错但养成好习惯先配时钟、再配GPIO、再初始化外设。4.2 一个完整的读写时间Demo下面给一个直接用串口打印时间的完整例子。STM32侧我用的是HAL库串口部分只展示关键片段#include ds1302.h #include stdio.h // 重定向printf到串口 int fputc(int ch, UART_HandleTypeDef *huart) { HAL_UART_Transmit(huart, (uint8_t *)ch, 1, 100); return ch; } int main(void) { HAL_Init(); SystemClock_Config(); MX_GPIO_Init(); MX_USART1_UART_Init(); DS1302_Init(); // 第一次烧录时设置时间设置成功后可以注释或删除这一段 DS1302_TimeTypeDef initTime {25, 1, 15, 10, 30, 0, 3}; // 2025年1月15日 周三 10:30:00 DS1302_SetTime(initTime); DS1302_TimeTypeDef now; while (1) { DS1302_GetTime(now); printf(20%02d-%02d-%02d %02d:%02d:%02d Week:%d\r\n, now.year, now.month, now.day, now.hour, now.minute, now.second, now.week); HAL_Delay(1000); } }第一次烧录时设置时间等时间走起来后第二次烧录要把DS1302_SetTime那段注释掉。否则每次上电都会把时间重新设置成固定值就测不出DS1302在断电后继续保持走时的效果了。串口打印如果用的标准库printf必须重定向fputc。上面代码里我把fputc的第一个参数类型写成了UART_HandleTypeDef *这是配合STM32CubeMX常见写法编译时会有警告但不影响功能。实际工程建议用#ifdef __GNUC__区分Keil和GCC工具链细节这里不展开。串口输出效果如下2025-01-15 10:30:01 Week:3 2025-01-15 10:30:02 Week:3 2025-01-15 10:30:03 Week:34.3 扩展秒表模式与涓流充电配置DS1302还有个不太被注意的功能——秒表模式。通过控制寄存器的bit4到bit6可以配置秒表间隔为1/100秒、1秒、1分钟等不同精度配合RAM可以做简易计时器。我这次没用秒表模式但研究寄存器表时发现这个功能在低功耗计时场景挺实用至少比用定时器省资源。涓流充电的配置值得单独说一说。芯片的涓流充电寄存器地址是0x91配置字节高四位必须写成1010才会真正使能充电这是芯片的安全机制。bit3和bit2选择串联二极管数量bit1和bit0选择限流电阻阻值。使用可充电纽扣电池时一个保守的配置是0xA5对应1010 01 01也就是一个二极管加2kΩ电阻充电电流大约在几百微安量级。如果是不可充电的一次性电池保持默认值0x00绝对不能改。5. 踩坑记录与调试技巧5.1 六个高频问题及解法调试过程中我整理了一个问题速查表都是实际会遇到的高频坑问题现象可能原因解决方法读时间全是0xFF数据引脚没有切换为输入模式或命令字地址不对检查读字节前是否调用IO输入切换核对命令字读时间全是0x00时序过快或SCLK有毛刺加大延时检查SCLK是否悬空写入时间不生效写保护位WP没有清零写任何时间寄存器前先写0x8E寄存器为0x00时间完全不走秒寄存器CH位为1振荡器停止重写秒寄存器CH位必须为0走时误差很大晶振频率偏差或负载电容不匹配换精度更高的晶振确认没有外接多余负载电容掉电后时间丢失VCC1没有接备用电池或电池电压太低确认电池电压大于2.0V检查电池座极性5.2 调试工具与方法DS1302时序速度很慢逻辑分析仪是第一选择。把CE、SCLK、I/O三根线夹上逻辑分析仪采样率设到1MHz就绰绰有余抓一次读写过程就能看清每一个字节从LSB开始逐位发送的过程。我调试的时候就是用逻辑分析仪确认了命令字格式里bit6的设置很快就发现RAM地址和时钟寄存器地址的命令字搞错了。如果没有逻辑分析仪也可以用示波器单次触发抓CE上升沿和SCLK的波形至少能确认通信是否发生。DS1302的SCLK频率很低普通台式示波器完全能看清楚。另外一个很实用的调试技巧用RAM当调试存储区。程序里先往某个RAM地址写入已知数据再读回来放到串口打印如果数据一致说明底层读写逻辑是对的问题在时间寄存器那块如果数据都对不上时序或方向切换就得再查查。这样能把问题范围迅速缩小。5.3 移植到其他MCU的小贴士移植DS1302驱动到其他MCU需要注意的点其实不多。头文件里的引脚宏直接改底层GPIO操作换成目标平台的HAL库或寄存器操作延时函数根据目标主频调整循环次数即可。有个容易忽视的地方如果目标MCU的IO引脚电压和DS1302不在同一电平域比如MCU是5V而DS1302是3.3V供电需要确认引脚是否容忍5V输入。DS1302的I/O引脚在3.3V供电下直接用5V MCU驱动可能会超出绝对最大额定值稳妥做法是加一个电平转换电路或者选择3.3V版本MCU。在不同主频的MCU上延时循环的时间基准完全不一样。从72MHz的STM32移植到168MHz的平台上原来空循环10次大约1微秒可能变成0.4微秒虽然DS1302对时序的容忍度比较高但最好还是量一下延时是否充足。最保险的方式是用定时器或者systick来精确延时几百纳秒一劳永逸地解决移植误差问题。5.4 关于开源与后续扩展按标题里的定位这是一份开源学习笔记整个工程代码都可以直接拿去用。实际在做开源整理的时候我特意把硬件原理图、焊接注意事项、代码注释全部打成了一包这样拿到资料的人不需要看任何额外的讲解视频就能复现。后续扩展方向我大概想了几个把DS1302的时间通过ESP8266或4G模组同步到网络校时服务解决长时间运行下晶振漂移积累的问题给DS1302加一个软件秒表功能做实验数据记录的时间戳把读取到的时间用到LCD显示屏上做一个桌面电子日历。这些都不难核心还是先把DS1302的驱动吃透后面就能灵活组合。结尾调DS1302这颗芯片的过程让我重新认识了一件事很多时候你以为在用一颗老芯片其实是在学一套通信协议的设计哲学。DS1302的3线接口、LSB先行、方向切换、写保护机制每个设计点背后都有它的道理。把这些逻辑彻底弄懂再去学SPI、I2C甚至一些更复杂的传感器协议都会感觉顺畅不少。如果你正在调这颗芯片建议按这个顺序来排查先确认硬件接线有没有把CE的下拉电阻和晶振处理好再确认底层读写字节的时序能否读出RAM数据最后再碰时间寄存器。一次只解决一个变量问题很容易定位。最后再分享一个小经验DS1302的驱动程序写完以后一定要做断电测试。把单片机电源完全断开等几秒再重新上电看时间是否连续。如果时间从头开始了说明备用电池回路有问题电源管理和掉电保持这部分没做好。这个测试虽然简单却是所有实时时钟项目验收时最关键的一环。