嵌入式项目里一旦碰到需要记录时间、定时上报、日志打点,就绕不开一个老问题:单片机自己没有可靠的时钟。以前用某个开发板搭模拟项目X时,我把时间戳直接怼在系统定时器上,跑了一个晚上之后发现差了将近一分钟,这才意识到实时时钟(RTC)不能靠软件数,得老老实实上外部时钟芯片。翻了一圈手头的模块,最后选了 DS1302,一颗非常经典的三线串行 RTC,价格便宜、电路简单,配合 STM32 的 GPIO 模拟时序,很轻松就能驱动起来。这篇学习笔记就围绕“STM32 驱动 DS1302”从头理一遍,包括为什么要选它、DS1302 的内部寄存器怎么操作、硬件怎么接、代码怎么写、实测中遇到了哪些坑。适合刚接触单片机通信时序、想自己写驱动而不是直接套库的新手,也适合想快速给项目加一个断电保持时钟的老手收藏备用。
1. 为什么这块板子最后选了 DS1302
1.1 需求拆解:项目里“时间”到底是谁来管
STM32 本身并不是没有时间来源。系统定时器(SysTick)可以产生精确的毫秒节拍,但节拍数依赖主频,主频一变或者代码里关闭/改写了中断,时间就会漂移。更关键的是,芯片一旦掉电,RAM 里的计数器全部清零,下次上电时间还是从零开始。很多场景下我们需要的是“日历时间”:年、月、日、时、分、秒,最好断电之后还能继续走,重新上电读出来依然准确。这种需求靠 MCU 内部资源很难做到,除非芯片自带校准过的 RTC 外设,并且你还愿意在 EEPROM 或备份寄存器里做掉电保存逻辑。
所以当时的需求拆解很直接:外挂一个带电池备份的实时时钟芯片,MCU 只需要定期读写它即可。DS1302 这个芯片在国产开发板和学习套件里几乎成了标配,模块不到十块钱,逻辑简单,官方手册和社区资料都很多,拿来做学习和做项目都非常合适。而且它的功耗非常低,一颗 3V 纽扣电池就能让它独立走很久,数据手册上写的时间保持电流在纳安级别,我实测过用旧 CR2032 也能支撑以月为单位的掉电计时。
1.2 DS1302、DS3231 与 MCU 内置 RTC 的取舍
很多人上来就问:为什么不直接用 DS3231?这要从需求精度和成本两个维度看。DS3231 内置温度补偿晶振,精度能到正负两分钟每年,但芯片价格是 DS1302 的几倍甚至十几倍,对普通项目来说是性能过剩。DS1302 靠外部 32.768kHz 晶振走时,精度很大程度取决于晶振质量和工作环境温度,日常场景下一个月的误差大概在几十秒到几分钟,对于多数“非计费型”场景完全够用。
MCU 内置 RTC 更省成本,尤其某些 STM32 系列有独立的 RTC 域和备份电池引脚,理论上不需要外挂芯片。但问题在于:内置 RTC 的寄存器配置不统一,不同系列库函数差异大,而且如果用在开发板上,很多时候没有预留电池座,掉电照样丢失。DS1302 作为独立芯片,库函数和应用逻辑可以跨平台复用,我把之前写过的一版驱动从某个 STM32F103 开发板移植到另一块板子上,只需要改三个引脚宏定义,其他代码一行没动。这种“接口统一、随处复用”的优势,在我这种喜欢折腾不同板子的人手里非常实用。
2. DS1302 的关键原理要先吃透
2.1 三根线的时序:不是 SPI 也形似 SPI
DS1302 对外提供三根线:CE(片选,也叫 RST)、SCLK(串行时钟)和 IO(数据线)。很多人第一次看引脚以为它走 SPI,其实不是标准 SPI,而是一种“命令字 + 数据”的变体时序:每次通信先发送一个 8 位命令字节,告诉芯片接下来要读还是写、访问哪个寄存器,然后再进行 8 位数据的传输。整个过程由 CE 拉高启动,等读写完成后 CE 拉低结束。
时序上要记住几个关键点:CE 必须在整个通信过程中保持高电平,不能中途抖掉;数据在 SCLK 的上升沿被写入芯片,在下降沿被芯片输出;SCLK 空闲时保持低电平。这类时序用 GPIO 模拟最直观,因为你完全控制每一根线的电平变化,调试起来也方便。用硬件 SPI 不是说不行,但需要额外构造命令字格式,还得处理 CE 的边沿关系,收益不大,总结下来就是一句话:三根 GPIO + 两个延时函数,比配置 SPI 外设省事得多。
2.2 命令字节与寄存器映射
DS1302 的每个寄存器都对应一个 8 位地址,访问时必须先通过命令字节封装。命令字节的格式是:最高位固定为 1,次高位在单字节模式保持 0,突发模式置 1,低 5 位是寄存器地址,最低位是读写控制(1 为读,0 为写)。举例来说,读写秒寄存器的命令就是 0x80 和 0x81,读写分钟寄存器就是 0x82 和 0x83,以此类推。
时钟相关的寄存器从 0x80 到 0x8C,依次是:秒、分、时、日、月、星期、年。每个寄存器存的数据都以 BCD 码格式保存,比如十进制 25 秒,存进去的是 0x25。这里有个初学者容易迷糊的点:BCD 码是“用四位二进制表示一位十进制”,所以 0x25 拆开后就是十进制 2 和 5,直接把它当普通十六进制数看也能读出个大概,但真要转换成程序里方便计算的十进制数,就必须做专门的 BCD 转换。除了时间寄存器,还有两个控制寄存器:0x8E 是写保护寄存器,0x90 是充电寄存器,这两个后面单独讲。
2.3 BCD 码和写保护机制:第一次接触的人最容易翻车
BCD 转换本身不复杂,核心就两个公式:
uint8_t DS1302_DecToBcd(uint8_t dec) { return ((dec / 10) << 4) | (dec % 10); } uint8_t DS1302_BcdToDec(uint8_t bcd) { return (((bcd >> 4) & 0x0F) * 10) + (bcd & 0x0F); }但我在第一次写驱动时,直接在((bcd >> 4) * 10)这一步踩了坑:bcd是uint8_t,右移 4 位后与 10 相乘,中间过程会被提升成int,这没问题,坑的是后面和低位相加后隐式转换回来,如果忘了把高位部分单独做掩码,当读出来的字节带了一些标志位(比如秒寄存器的 CH 暂停位、小时寄存器的 12/24 小时标志位),算出来的结果就会完全不对。所以在读取时间时,先掩掉不需要的高位,再去做 BCD 转十进制,逻辑才稳妥。
写保护机制是另一个容易忽略的点。DS1302 默认上电后写保护是开启的,0x8E 寄存器的最高位 WP 为 1 时,所有对时间寄存器的写操作都会被忽略。我第一次只 WriteReg 写时间,写完一读全是 25,差点以为是芯片坏了。正确流程是:先向 0x8E 写入 0x00 关闭写保护,然后写各个时间寄存器,最后再向 0x8E 写入 0x80 恢复写保护,防止程序跑飞时误改时钟。
3. 硬件连接与电路设计
3.1 最小系统:引脚、晶振、电池、电容
DS1302 的封装通常有 8 个引脚,用到的核心器件包括:主电源 VCC2、备用电池 VCC1、地 GND、32.768kHz 晶振 X1/X2、三根通信线 CE/SCLK/IO。VCC2 可以直接接 STM32 的 3.3V 电源轨,VCC1 接纽扣电池的正极,电池负极和 GND 相连即可。芯片内部会自动在主电源和备用电源之间切换:有主电源时用主电源,掉电后自动切到电池供电,不需要额外的开关电路。
晶振和电容要注意走线。DS1302 要求外接 32.768kHz 晶振,负载电容一般在 6~12.5pF 之间,具体看晶振厂商标注。模块板上通常已经焊好了晶振和两个小电容,如果是自己画 PCB,尽量让晶振紧靠 X1/X2 引脚,走线短而直,周围不要铺大面积的铜,也别和 SCLK 这种高频翻转的引脚平行走线。还有一点是 VCC2 附近放一个 0.1uF 的陶瓷电容去耦,能有效改善读写时序不稳定问题。
3.2 GPIO 怎么接:方向切换和上拉是细节活
STM32 跟 DS1302 通信需要三个 GPIO,建议用推挽输出模式或者切换输入模式控制 IO。CE 和 SCLK 始终由 STM32 输出,IO 是双向的,写数据时要输出,读数据时要切输入,所以 IO 在初始化时最好配上拉输入或开漏输出加外部上拉,避免方向切换瞬间出现浮空电平。
如果开发板的 DS1302 模块已经带了上拉电阻,直接用普通的推挽输出切换输入模式也没问题。我习惯把所有 DS1302 相关的引脚定义集中在一个头文件里,改板子时只改宏定义:
#define DS1302_GPIO_PORT GPIOA #define DS1302_CE_PIN GPIO_Pin_0 #define DS1302_SCLK_PIN GPIO_Pin_1 #define DS1302_IO_PIN GPIO_Pin_2#define DS1302_CE_HIGH() GPIO_SetBits(DS1302_GPIO_PORT, DS1302_CE_PIN) #define DS1302_CE_LOW() GPIO_ResetBits(DS1302_GPIO_PORT, DS1302_CE_PIN) #define DS1302_SCLK_HIGH() GPIO_SetBits(DS1302_GPIO_PORT, DS1302_SCLK_PIN) #define DS1302_SCLK_LOW() GPIO_ResetBits(DS1302_GPIO_PORT, DS1302_SCLK_PIN) #define DS1302_IO_HIGH() GPIO_SetBits(DS1302_GPIO_PORT, DS1302_IO_PIN) #define DS1302_IO_LOW() GPIO_ResetBits(DS1302_GPIO_PORT, DS1302_IO_PIN) #define DS1302_IO_READ() GPIO_ReadInputDataBit(DS1302_GPIO_PORT, DS1302_IO_PIN)读的时候还得做方向切换:
static void DS1302_IO_IN(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = DS1302_IO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(DS1302_GPIO_PORT, &GPIO_InitStructure); } static void DS1302_IO_OUT(void) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = DS1302_IO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(DS1302_GPIO_PORT, &GPIO_InitStructure); }3.3 备用电池与充电寄存器的坑:千万别乱碰 0x90
DS1302 有一个充电控制寄存器 0x90,默认值是 0x00,表示不启用涓流充电。很多模块说明书里会提到“可以给备份电容或可充电电池充电”,所以有些资料里会教你直接往 0x90 写 0xA5 之类的方法。如果你用的是 CR2032 这种不可充电的一次性锂电池,千万别开充电功能。我见过有人照着老教程写了充电配置,结果电池发热、鼓包,幸好没炸,教训相当深刻。
我的做法是:只要用的是不可充电池,0x90 这个寄存器保持默认值 0x00,代码里也不要出现任何写 0x90 的逻辑。如果确实要用超级电容或者可充电的 LIR2032 做备份,再仔细看数据手册确认二极管和电阻配置,并且控制充电电流在毫安级,充满后要有停止充电的措施,不要长期挂在充电状态。
4. 软件驱动设计与代码实现
4.1 驱动文件结构:把时序和数据分开,排错才不累
整个驱动我拆成两个文件:bsp_ds1302.h和bsp_ds1302.c。头文件里放引脚宏定义、时间结构体、对外接口声明;源文件里放 GPIO 初始化、底层字节收发、寄存器读写、时间设置与读取。这样分层的好处是:上层业务代码完全不需要知道 BCD 码和时序细节,调用DS1302_GetTime()拿到的就是一个干净的时间结构体;底层如果换引脚甚至换芯片,只需要改底层文件。
定义时间结构体时,我建议直接用十进制字段:
typedef struct { uint8_t year; // 00~99,例如 23 表示 2023 uint8_t month; // 1~12 uint8_t date; // 1~31 uint8_t week; // 1~7,使用方约定,DS1302 不自动计算 uint8_t hour; // 0~23 uint8_t minute; // 0~59 uint8_t second; // 0~59 } DS1302_Time_t;这样在业务层直接tm.hour = 12就能读入,不需要跟 0x12 这类 BCD 常量打交道,可读性高很多。底层负责在结构体和寄存器字节之间做转换。
4.2 底层时序函数:写一个字节、读一个字节
先看最核心的字节收发,这部分是整个驱动的地基。写字节的流程是:把 IO 设为输出,然后循环 8 次,每次把当前位放到 IO 上,拉高 SCLK 让 DS1302 锁存这位数据,再拉低 SCLK 准备下一位。注意先送低字节还是高字节:DS1302 是 LSB first,也就是先从最低位开始发送。
static void DS1302_WriteByte(uint8_t dat) { uint8_t i; DS1302_IO_OUT(); for (i = 0; i < 8; i++) { if (dat & 0x01) DS1302_IO_HIGH(); else DS1302_IO_LOW(); DS1302_SCLK_HIGH(); DS1302_DelayUs(2); DS1302_SCLK_LOW(); DS1302_DelayUs(2); dat >>= 1; } }读字节稍微复杂一些:命令字节发送完成后,IO 要立刻从输出切到输入。DS1302 在 SCLK 下降沿之后把数据放到 IO 上,所以我习惯在 SCLK 低电平期间去读取当前 bit,然后再产生一个完整的 SCLK 脉冲,用来让芯片准备下一位数据。
static uint8_t DS1302_ReadByte(void) { uint8_t i, dat = 0; DS1302_IO_IN(); for (i = 0; i < 8; i++) { if (DS1302_IO_READ()) dat |= (0x01 << i); DS1302_SCLK_HIGH(); DS1302_DelayUs(2); DS1302_SCLK_LOW(); DS1302_DelayUs(2); } return dat; }这个实现里我特意在dat |= (0x01 << i)时把第 i 位置起来,读回来的最低位放到最终返回值的最低位,所以读函数返回的字节和写进去的字节天然对齐,后续做 BCD 转换时不用转来转去。
4.3 寄存器读写与时间设置:写保护开关的顺序不能乱
有了底层字节收发函数之后,寄存器操作就顺理成章了。每次写寄存器之前,要把 CE 拉高启动时序,发送命令字节,然后发送数据字节,最后 CE 拉低:
void DS1302_WriteReg(uint8_t cmd, uint8_t dat) { DS1302_CE_HIGH(); DS1302_WriteByte(cmd); DS1302_WriteByte(dat); DS1302_CE_LOW(); } uint8_t DS1302_ReadReg(uint8_t cmd) { uint8_t dat; DS1302_CE_HIGH(); DS1302_WriteByte(cmd); dat = DS1302_ReadByte(); DS1302_CE_LOW(); return dat; }设置时间时,按照“先关写保护、再写寄存器、最后开写保护”的顺序。秒寄存器还有一个 CH 位,位于最高位,写 1 会让时钟暂停振荡,初始化时必须把它清 0,否则时间一写进去就是静止的。
void DS1302_SetTime(const DS1302_Time_t *tm) { DS1302_WriteReg(0x8E, 0x00); // 关闭写保护 DS1302_WriteReg(0x80, DS1302_DecToBcd(tm->second) & 0x7F); // 秒,CH 位清零 DS1302_WriteReg(0x82, DS1302_DecToBcd(tm->minute)); DS1302_WriteReg(0x84, DS1302_DecToBcd(tm->hour)); // 24 小时制,bit7 保持 0 DS1302_WriteReg(0x86, DS1302_DecToBcd(tm->date)); DS1302_WriteReg(0x88, DS1302_DecToBcd(tm->month)); DS1302_WriteReg(0x8A, DS1302_DecToBcd(tm->week)); DS1302_WriteReg(0x8C, DS1302_DecToBcd(tm->year)); DS1302_WriteReg(0x8E, 0x80); // 打开写保护 }读取时间就按照寄存器地址逐个读回来,再把 BCD 码转成十进制。这里有一个细节:读小时寄存器时我会用掩码去掉最高几位,避免 12 小时制标志位干扰换算;读秒和分钟寄存器也同理处理。
void DS1302_GetTime(DS1302_Time_t *tm) { uint8_t dat; dat = DS1302_ReadReg(0x81); tm->second = DS1302_BcdToDec(dat & 0x7F); dat = DS1302_ReadReg(0x83); tm->minute = DS1302_BcdToDec(dat & 0x7F); dat = DS1302_ReadReg(0x85); tm->hour = DS1302_BcdToDec(dat & 0x7F); dat = DS1302_ReadReg(0x87); tm->date = DS1302_BcdToDec(dat & 0x3F); dat = DS1302_ReadReg(0x89); tm->month = DS1302_BcdToDec(dat & 0x1F); dat = DS1302_ReadReg(0x8B); tm->week = DS1302_BcdToDec(dat & 0x07); dat = DS1302_ReadReg(0x8D); tm->year = DS1302_BcdToDec(dat); }这里小时寄存器用0x7F掩码而不是0x3F,是因为 24 小时制下 bit6 在这个芯片里也应该恒为 0,用 0x7F 可以顺带保留 0~23 的完整范围。实际项目里如果确认全程序只用 24 小时制,用 0x3F 也可以,看个人严谨程度。
5. 实测过程与现象记录
5.1 第一次上电:写入测试时间,验证秒脉冲是否走动
驱动写完后的第一件事不是直接读时间,而是先写一个已知时间,断电再上电,看时间能不能保持。我用的测试场景是这样:初始化时调用DS1302_SetTime()写入一个 2023-01-01 12:38:59,然后延时 1 秒,再调用DS1302_GetTime()读出来打印,看秒是否变成 40。
如果秒位一直不动,优先检查两件事:第一,秒寄存器的 CH 位是不是被置 1 了,把读回来的原始字节打印出来看最高位;第二,晶振有没有振荡,用示波器量 X1/X2 引脚,正常能看到 32.768kHz 的波形。没有示波器也可以做一个变通测试:把 SCLK 引脚用手碰一下,如果时间乱跳,往往说明芯片时序被外部干扰,晶振大概率在跑。
5.2 主循环里每秒刷新:串口打印与显示联动
接入实际场景后,我在主循环里做一个 1 秒定时刷新,每次读回时间结构体,要么格式化打印到串口,要么送到 OLED 屏幕显示。串口打印的格式直接拼字符串:
DS1302_Time_t now; char buf[64]; DS1302_GetTime(&now); sprintf(buf, "20%02d-%02d-%02d %02d:%02d:%02d week=%d\r\n", now.year, now.month, now.date, now.hour, now.minute, now.second, now.week);实测每秒打印一次,时间走动平滑,没有跳秒或回拨现象。跟系统定时器计时对比了一下,跑了一个小时,误差在两三秒以内,完全符合 DS1302 + 普通 32.768kHz 晶振的预期表现。需要注意的是,每次读取时间时,DS1302 的时钟数据在读出瞬间是锁存的吗?其实不是所有寄存器都支持锁存,所以尽量不要在秒边界连续读取,否则时、分、秒寄存器更新不同步时可能读到比如“12:38:59 下一秒跳到 12:39:00”却出现“秒=59,分=38”的边缘情况。实测中这种概率很小,但对时间精度要求严格的场景,可以在一次读取前后各加一次秒数校验,如果读到两次秒不同就重读一遍。
5.3 断电保持与晶振校时:备份电池的真实作用
断电保持功能是 DS1302 这类芯片的立身之本。我把 VCC1 接上一颗旧 CR2032,然后拔掉主电源,等待 10 分钟再重新上电,读出的时间只比断电前慢了几分钟,说明晶振在电池供电下正常起振,数据没有丢。如果 VCC1 悬空,断电后再次上电,读出来的时间会回到固件里初始化的值或者变成乱码,这是正常现象,不是驱动问题。
关于校时,我建议在设备上电初始化时做一次“可选校时”:如果系统有上位机或者网络时间,就把标准时间通过串口/命令解析写入 DS1302;如果没有外部时间源,就直接保留 DS1302 当前的走时。不要每次上电都盲目重置时间,否则断电保持功能就白做了。
6. 常见问题排查速查表
6.1 典型故障现象与原因:一张表搞定大部分问题
我把学习和调试过程中遇到过的故障整理成一张速查表,遇到类似情况可以直接照着查。
| 故障现象 | 可能原因 | 排查方向 |
|---|---|---|
| 读出来永远是 0xFF | IO 方向没有切换为输入;引脚没接上拉;CE 没有保持高 | 检查读函数是否在命令字节后切换IO_IN();示波器量 CE/SCLK/IO 波形 |
| 读出来全是 0x00 | 晶振没起振;电池电压过低导致芯片复位;初始化写保护顺序错误 | 量 X1/X2 波形;万用表测 VCC1、VCC2 电压;检查 0x8E 写保护开关流程 |
| 时间写入后不走 | 秒寄存器最高位 CH 为 1 | 读 0x81 看最高位;写秒时用& 0x7F |
| 时间走得乱跳 | 时序延时不够;SCLK 有毛刺;IO 上拉太弱 | 增大DelayUs到 5us;检查信号线走线;确认模块上拉电阻在 4.7k~10k |
| 上电后时间恢复成初始值 | VCC1 悬空或电池耗尽;初始化代码每次都调用SetTime | 确认电池安装和电压;只在上电后需要校时时才写时间 |
| 电池发热或鼓包 | 误开启了 0x90 充电寄存器,电池不可充电 | 检查代码是否写入过 0x90;换新电池;确保 0x90 保持 0x00 |
| 读出的时间偶尔错乱 | 读取时正好跨秒;中断里访问 DS1302 冲突 | 连续读两次校验;避免在定时器中断里调用GetTime |
6.2 几个排错窍门:不靠猜,靠现象定位
排查时序类问题最有效的办法是在关键位置打 GPIO 翻转点,配合逻辑分析仪或者示波器看波形。比如专门留一个空闲引脚,在DS1302_CE_HIGH()后拉高、DS1302_CE_LOW()前拉低,这样就能一眼看出当前通信片选窗口是否正常、SCLK 有没有多余毛刺。
还有一个窍门是分段验证。不要一开始就写完整驱动然后灰头土脸地调,而是先只发命令字节、不发数据,用示波器看 IO 线上有没有符合命令字格式的波形;确认输出没问题后,再读寄存器验证输入方向切换是否正确。这样每次只解决一个问题,排错效率高很多。
另外要特别注意中断问题。不要在定时器中断回调里直接调DS1302_GetTime(),因为内部 GPIO 时序需要连续操作,一旦被更高优先级的中断打断,波形就被撕裂了,读回来的数据可能完全错误。正确做法是把GetTime放到主循环或专门的低优先级任务里,中断里只置一个标志位。
6.3 关于 32.768kHz 晶振:不起振的原因比想象中多
DS1302 不起振是新手最常见、也最烦人的问题。晶振质量差、负载电容不匹配、焊接温度过高、X1/X2 引脚对地漏电,都可能导致停振。处理建议是优先买成套的 DS1302 模块,模块厂商已经把晶振和电容匹配好了,比自己找散件靠谱得多。
如果自己画板子,晶振下面尽量不要走其他信号线,晶振外壳接地处理也很重要。上电后用手摸一下晶振引脚,如果时间开始走,说明晶振本身没问题,多半是电容匹配不当或者走线寄生电容太大。调试时也可以换一个已知能起振的晶振做交叉测试,能帮你快速定位是芯片问题还是晶振问题。
7. 开源与学习笔记的收尾心得
最后再分享一点个人体会。把 STM32 驱动 DS1302 这一整套代码理清楚后,我发现自己的收获不只是多了一块能用的时钟模块,而是真正理解了“三线串行时序”是怎么一回事:什么时候给时钟沿、什么时候切方向、为什么写保护和 BCD 这些看起来不起眼的细节决定了驱动能不能稳定工作。这些经验用在其他芯片上也是通用的,后边如果再看到 OLED 的 I2C、Flash 的 SPI、甚至单总线协议的传感器,遇到问题时脑海里第一反应都是先想时序图。
当时我把这套驱动整理成开源笔记时,刻意没有用库生成器的一键 RTC 配置,而是用手写 GPIO 的方式,目的就是让每一个翻转电平的细节都暴露在代码里。现在回头看,这个决定非常值得。建议拿到这篇笔记的读者也别急着复制粘贴,先把 DS1302 的数据手册翻一遍,对照本文的命令字节和寄存器地址逐行理解,接着自己敲一遍驱动,最后再跑通完整例程。敲代码时如果遇到0x81、0x8E这些魔法地址,先在手册里查清楚它是哪个寄存器再动手,比出了故障再查轻松得多。
代码原理上能做的东西还有很多,比如用突发模式 0xBE/0xBF 一次性读写 7 个时间字节,或者把 DS1302 的 31 字节 RAM 当成一个小型掉电存储区,存配置参数和设备状态。我建议先把单字节读写的基础版跑通,再按需扩展这些功能。驱动代码说到底就是一个“状态机 + 时序”的组合,理清了底层流转逻辑,扩展只是时间问题。