1. 为什么还要折腾一颗"上古"时钟芯片
第一次接触 DS1302 是在一个数据记录仪的项目上,主控用的是 STM32F103C8T6,需求很简单:每隔十分钟采一次环境数据,带上准确的时间戳存进 SD 卡。当时第一反应是用 STM32 内部的 RTC,毕竟片上外设,省一颗芯片、省两根线,何乐而不为。结果实测下来,掉电之后时间全丢,除非额外挂一颗纽扣电池到 VBAT 引脚,而且内部 RTC 的走时精度受晶振和温度影响比较明显,连续跑一周能偏出十几秒。项目要求时间误差控制在分钟级以内,内部 RTC 勉强能用,但客户还提了一个要求:设备要能随时更换主控板而不丢时间。这就意味着时间必须独立于主控存在,于是 DS1302 重新回到了选型清单里。
DS1302 这颗芯片年纪不小了,从上世纪九十年代末一直用到现在,价格便宜到几毛钱,DIP8 和 SOP8 封装都有,手焊友好。它的核心价值在于:独立走时、掉电保持、SPI 三线接口、自带 31 字节 RAM。对于 STM32 来说,驱动它只需要三个普通 GPIO,不需要硬件 SPI 外设,软件模拟时序即可,移植性极强。这篇笔记就把我从零驱动 DS1302 的完整过程拆开讲,包括时序怎么抠、寄存器怎么配、读写怎么防坑,以及那些数据手册上不会写但实际会遇到的幺蛾子。
适合谁看?如果你正在做带时间戳的数据采集、定时开关、闹钟提醒这类小项目,手头又正好有 STM32 和 DS1302,那这篇内容可以直接抄作业。如果你只是想了解一下三线制时序芯片怎么驱动,里面的时序分析和防坑思路同样通用。
2. 硬件连接与选型考量
2.1 引脚定义与最小系统接线
DS1302 一共 8 个引脚,实际用到的就 5 个:VCC2(主电源)、VCC1(备用电池)、GND、CE(片选,有的手册叫 RST)、I/O(数据线)、SCLK(时钟线)。X1、X2 接 32.768kHz 晶振。接线逻辑很直白:
| DS1302 引脚 | 接到 STM32 | 说明 |
|---|---|---|
| VCC2 | 3.3V | 主电源,设备工作时供电 |
| VCC1 | 3V 纽扣电池正极 | 掉电后维持走时和 RAM 数据 |
| GND | GND | 共地 |
| CE | 任意 GPIO(如 PA4) | 片选/复位,低电平复位内部移位寄存器 |
| I/O | 任意 GPIO(如 PA5) | 双向数据线,需要开漏或双向配置 |
| SCLK | 任意 GPIO(如 PA6) | 时钟,上升沿写、下降沿读 |
这里有个细节值得说:I/O 线是双向的,STM32 端配置成开漏输出加上拉电阻是最稳妥的做法,因为 DS1302 在读操作时会把这条线拉低输出数据,如果 STM32 端一直强推挽输出,两边就会打架。我一般直接在 I/O 上挂一个 4.7k 到 10k 的上拉电阻到 3.3V,然后 STM32 端在输出模式(开漏)和输入模式(浮空或上拉)之间切换。也有做法是用一个电阻串联保护,但实测下来开漏加上拉已经足够稳。
2.2 为什么不用硬件 SPI
很多人会问,DS1302 看起来像 SPI,为什么不直接接硬件 SPI 外设?原因有三个。第一,DS1302 的时序并不是标准 SPI:它是低位先行(LSB first),而 STM32 的硬件 SPI 默认是高位先行,虽然可以配置成 LSB first,但第二点更麻烦——它的数据线是单线双向的,写的时候主机输出,读的时候主机输入,硬件 SPI 的全双工模式在这里反而添乱。第三,DS1302 的时钟极性要求是空闲低电平,数据在上升沿被写入、下降沿被读出,和标准 SPI 的四种模式都不完全吻合。综合下来,软件模拟时序反而更简单、更可控,三个 GPIO 随便挪,移植到别的 MCU 上也是复制粘贴的事。
2.3 备用电池与晶振的选择
备用电池我用的是 CR2032 纽扣电池座,3V 供电,DS1302 在电池模式下功耗只有几百纳安,一颗电池撑几年没问题。注意 VCC1 和 VCC2 的电压关系:DS1302 内部会自动选择电压较高的那一路供电,所以主电源 3.3V 高于电池 3V 时用主电源,掉电后自动切到电池。如果你主电源也是 3V,那就要保证主电源略高于电池,否则可能出现切换异常。
晶振必须用32.768kHz的,这是 RTC 芯片的标准频率,因为 32768 是 2 的 15 次方,经过 15 级二分频正好得到 1Hz。晶振的负载电容一般是 6pF 或 12.5pF,DS1302 内部已经集成了振荡电路,外部不需要再加电容,但晶振的精度直接决定走时精度。我踩过的坑是买了一批便宜晶振,常温下一天能偏两三秒,后来换成精度 ±20ppm 的,一个月误差才几秒。如果对精度要求高,可以在软件里做温度补偿或者定期校准。
3. DS1302 寄存器与通信协议拆解
3.1 命令字的结构
DS1302 的所有操作都从一个命令字节开始。这个字节的位定义是这样的:
- bit7:固定为 1,表示使能访问
- bit6:0 表示访问时钟/日历寄存器,1 表示访问 RAM
- bit5~bit1:寄存器地址
- bit0:0 表示写,1 表示读
举个例子,要写秒寄存器(地址 0x80),命令字就是 0x80;要读秒寄存器,命令字就是 0x81。要写 RAM 的第一个字节(地址 0xC0),命令字是 0xC0;读就是 0xC1。这个规律记住之后,所有寄存器的读写命令都能推出来。
时钟寄存器一共 7 个,地址从 0x80 到 0x8C,依次是秒、分、时、日、月、星期、年。每个寄存器都是 BCD 码格式,这点很关键——比如秒是 45,存进去的是 0x45,不是 0x2D。BCD 和十进制之间的转换是驱动里必须处理的,后面会讲。
3.2 写时序的精确控制
写一个字节到 DS1302 的流程是这样的:
- CE 拉高,启动通信
- 发送命令字节,低位先行,每个 bit 在 SCLK 上升沿被 DS1302 采样
- 发送数据字节,同样低位先行
- CE 拉低,结束通信
时序的关键在于建立时间和保持时间。DS1302 要求数据在 SCLK 上升沿之前至少稳定 50ns,上升沿之后保持 50ns。STM32F103 跑 72MHz 时,一个指令周期约 14ns,所以直接在拉高 SCLK 之前写数据、拉高之后稍作延时即可。我一般用几个__NOP()来保证时序,不放心的话加一个微秒级延时函数。
void DS1302_WriteByte(uint8_t dat) { for (uint8_t i = 0; i < 8; i++) { DS1302_IO_OUT(); // 切换为输出 if (dat & 0x01) DS1302_IO_H(); else DS1302_IO_L(); DS1302_SCLK_H(); __NOP(); __NOP(); DS1302_SCLK_L(); __NOP(); __NOP(); dat >>= 1; // 低位先行 } }注意这里dat >>= 1配合dat & 0x01实现的就是低位先行。如果你习惯高位先行,写出来就是反的,DS1302 会收到完全错误的命令。
3.3 读时序与数据线方向切换
读操作比写多一个步骤:发送完命令字节后,要把 I/O 线从输出切换为输入,然后再产生时钟脉冲读取数据。DS1302 在 SCLK 的下降沿输出数据,所以主机应该在下降沿之后读取 I/O 线的电平。
uint8_t DS1302_ReadByte(void) { uint8_t dat = 0; DS1302_IO_IN(); // 切换为输入 for (uint8_t i = 0; i < 8; i++) { dat >>= 1; if (DS1302_IO_READ()) dat |= 0x80; DS1302_SCLK_H(); __NOP(); __NOP(); DS1302_SCLK_L(); __NOP(); __NOP(); } DS1302_IO_OUT(); return dat; }这里有个容易翻车的点:读完之后必须把 I/O 切回输出,否则下一次写操作时数据线处于高阻态,DS1302 收不到数据。我第一次调试时就是忘了切回来,结果写进去的时间全是乱的,查了半天才发现是方向没切。
4. 完整驱动实现与时间读写
4.1 BCD 码转换与数据校验
DS1302 的时钟寄存器用 BCD 码存储,比如 23 分存的是 0x23。写之前要把十进制转 BCD,读之后要转回十进制。转换函数很简单:
uint8_t DEC2BCD(uint8_t dec) { return ((dec / 10) << 4) | (dec % 10); } uint8_t BCD2DEC(uint8_t bcd) { return ((bcd >> 4) * 10) + (bcd & 0x0F); }但这里有个坑:秒寄存器的 bit7 是时钟停止位(CH)。上电后 DS1302 默认是停止走时的,CH 位为 1。如果你写秒的时候直接把 BCD 值写进去,CH 位可能被清零也可能被置位,取决于你写的数据。正确做法是写秒的时候确保 bit7 为 0,启动振荡器。我一般这样写:
DS1302_WriteReg(0x80, DEC2BCD(sec) & 0x7F); // 清除 CH 位,启动走时读秒的时候也要注意,bit7 是状态位不是数据位,转换前先屏蔽掉:
uint8_t sec = BCD2DEC(DS1302_ReadReg(0x81) & 0x7F);小时寄存器也有类似情况,bit7 是 12/24 小时制选择位,bit6 在 12 小时制下是 AM/PM 标志。如果你用 24 小时制,写的时候确保 bit7 为 0。
4.2 写保护寄存器的处理
DS1302 有一个写保护寄存器,地址 0x8E。上电默认是写保护的,也就是说你直接写时间寄存器是写不进去的。必须先把 0x8E 写 0x00 解除保护,写完时间后再写 0x80 重新上保护。这个设计是为了防止误写,但很多新手会在这里卡住——明明时序对、数据对,就是写不进去,原因就是写保护没解除。
void DS1302_SetTime(uint8_t year, uint8_t month, uint8_t day, uint8_t week, uint8_t hour, uint8_t min, uint8_t sec) { DS1302_WriteReg(0x8E, 0x00); // 解除写保护 DS1302_WriteReg(0x80, DEC2BCD(sec) & 0x7F); DS1302_WriteReg(0x82, DEC2BCD(min)); DS1302_WriteReg(0x84, DEC2BCD(hour) & 0x3F); // 24小时制 DS1302_WriteReg(0x86, DEC2BCD(day)); DS1302_WriteReg(0x88, DEC2BCD(month)); DS1302_WriteReg(0x8A, DEC2BCD(week)); DS1302_WriteReg(0x8C, DEC2BCD(year)); DS1302_WriteReg(0x8E, 0x80); // 重新上保护 }注意年份寄存器只存两位,00 到 99,具体对应 2000 到 2099 还是别的区间,取决于你的软件怎么解释。我一般默认 20xx 年,读出来加 2000 就行。
4.3 突发模式批量读写
DS1302 支持突发模式(Burst Mode),命令字是 0xBE(写)和 0xBF(读),可以一次性把 7 个时间寄存器全部读写完,不需要逐个发命令。这样做的好处是保证时间的一致性——如果你逐个读秒、分、时,读到一半时间进位了,就会得到一组错乱的数据。突发模式在内部锁存了所有寄存器,读出来的是一个完整的时间快照。
void DS1302_ReadTimeBurst(uint8_t *buf) { DS1302_CE_L(); DS1302_SCLK_L(); DS1302_CE_H(); DS1302_WriteByte(0xBF); // 突发读命令 for (uint8_t i = 0; i < 7; i++) { buf[i] = DS1302_ReadByte(); } DS1302_CE_L(); }读出来的 buf[0] 到 buf[6] 依次是秒、分、时、日、月、星期、年,全是 BCD 码,再逐个转换即可。写的时候用 0xBE,同样一次性写 7 个字节,记得先解除写保护。
5. 实操中踩过的坑与排查技巧
5.1 时间不走、读出来全是 0 或 0xFF
这是最常见的问题,原因通常有三个。第一,晶振没起振。DS1302 对晶振比较挑剔,劣质晶振或者焊接时温度过高都可能损坏。判断方法是用示波器看 X1 引脚有没有 32.768kHz 的正弦波,如果没有,换晶振。第二,CH 位没清零。前面说过,上电默认停止走时,写秒的时候必须把 bit7 清掉。第三,写保护没解除,写操作全部无效,读出来自然是初始值。
排查顺序建议是:先确认写保护已解除,再确认 CH 位已清零,最后查晶振。我遇到过最隐蔽的一次是晶振引脚虚焊,万用表量通断是通的,但实际接触电阻很大,振荡不起来,重新补焊就好了。
5.2 走时偏快或偏慢
如果时间能走但精度差,基本是晶振的问题。DS1302 内部集成了振荡电路,但负载电容是固定的,配不同负载电容的晶振会有偏差。标准 32.768kHz 晶振配 6pF 负载电容,如果你买的是 12.5pF 的,走时就会偏慢。解决办法是换匹配的晶振,或者在晶振两端各加一个几皮法到十几皮法的微调电容,边测边走边调。我一般用频率计测 X1 的实际频率,调到 32768Hz 附近,一天误差能控制在 1 秒以内。
5.3 读写数据错位
数据错位通常和时序有关。检查几个点:SCLK 空闲时是不是低电平?数据是不是在上升沿之前就稳定了?读的时候是不是在下降沿之后才采样?还有一个容易忽略的是CE 的时序——CE 拉高之后要稍等一小会儿再发第一个时钟,CE 拉低之前要确保最后一个时钟已经完成。我在 72MHz 主频下用__NOP()做延时,如果主频更高,比如 168MHz,可能需要加更多 NOP 或者用微秒延时。
5.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 读出来全是 0xFF | I/O 线没上拉或方向没切回 | 检查上拉电阻,确认读后切回输出 |
| 写不进去 | 写保护未解除 | 先写 0x8E 为 0x00 |
| 时间不走 | CH 位为 1 或晶振未起振 | 清 CH 位,示波器查晶振 |
| 走时偏差大 | 晶振负载电容不匹配 | 换晶振或加微调电容 |
| 数据偶尔错乱 | 时序延时不足 | 增加 NOP 或微秒延时 |
| 掉电后时间丢失 | 备用电池没接或电压不足 | 检查 VCC1 电池电压 |
5.5 几个实用的经验技巧
第一,初始化时先读一次时间,如果读出来是非法值(比如月大于 12、日大于 31),说明芯片没电池或者第一次上电,这时候再写入一个默认时间。这样设备换电池后能自动恢复。
第二,RAM 可以用来存校准参数。DS1302 有 31 字节 RAM,掉电不丢,我一般把走时校准值、设备编号这些存在里面,省一颗 EEPROM。
第三,软件模拟时序时尽量关中断。如果读写过程中被中断打断,时序就可能乱掉。我一般在读写函数前后加__disable_irq()和__enable_irq(),虽然会短暂影响实时性,但 DS1302 读写一次也就几十微秒,影响可以忽略。
第四,上拉电阻不要省。我试过不加外部上拉,靠 STM32 内部上拉,结果读数据时高电平只有 2.5V 左右,DS1302 识别不稳定。换成 4.7k 外部上拉后一切正常。
6. 驱动封装与项目集成建议
6.1 把驱动做成可移植的模块
写驱动的时候我习惯把硬件相关的部分抽出来,比如 GPIO 操作宏定义单独放一个头文件,换 MCU 时只改这一层。DS1302 的时序逻辑和寄存器操作是通用的,不管你是 STM32、GD32 还是别的平台,这部分代码都不用动。
// ds1302_port.h #define DS1302_CE_H() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_SET) #define DS1302_CE_L() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_4, GPIO_PIN_RESET) #define DS1302_SCLK_H() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6, GPIO_PIN_SET) #define DS1302_SCLK_L() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_6, GPIO_PIN_RESET) #define DS1302_IO_H() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_SET) #define DS1302_IO_L() HAL_GPIO_WritePin(GPIOA, GPIO_PIN_5, GPIO_PIN_RESET) #define DS1302_IO_READ() HAL_GPIO_ReadPin(GPIOA, GPIO_PIN_5)这样封装之后,上层的时间读写、BCD 转换、突发模式这些逻辑完全和硬件解耦,移植成本极低。
6.2 时间戳与系统时间的同步策略
在实际项目里,我一般让 DS1302 作为唯一的时间源,系统启动时从 DS1302 读一次时间,之后用 SysTick 或者定时器维护一个软件时间,每隔一段时间(比如一小时)再和 DS1302 同步一次。这样做的好处是避免频繁读 DS1302 影响实时性,同时又能保证长期精度。同步的时候如果发现偏差超过阈值,就更新软件时间;如果偏差很小,就忽略,避免频繁调整导致时间跳变。
6.3 低功耗场景下的注意事项
如果项目是电池供电的低功耗设备,DS1302 的待机功耗很低,但要注意 STM32 的 GPIO 状态。如果 STM32 进入睡眠模式,而 GPIO 还保持输出高电平驱动 DS1302 的 SCLK 或 CE,可能会有漏电流。我一般在不操作 DS1302 时把三个 GPIO 都配置成模拟输入或者低电平输出,减少不必要的功耗。另外,DS1302 的 CE 在空闲时应该保持低电平,这样内部逻辑复位,功耗最低。
6.4 关于精度校准的补充
如果项目对时间精度要求特别高,比如月误差要小于 10 秒,光靠 DS1302 和普通晶振可能不够。我的做法是在软件里加一个校准寄存器,记录每天的偏差值,然后定期补偿。比如实测每天快 2 秒,那就在每天零点把时间减 2 秒。这个校准值可以存在 DS1302 的 RAM 里,掉电不丢。当然,更彻底的办法是用带温度补偿的 RTC 芯片,但成本就上去了,DS1302 的定位就是够用、便宜、好焊。
我个人在实际操作中的体会是,DS1302 这颗芯片虽然老,但只要你把时序抠准、把写保护和 CH 位这两个坑填上,它就是一个非常可靠的独立时钟源。驱动代码写一次,后面所有项目都能直接复用,性价比极高。最后再分享一个小技巧:调试的时候可以先写一个固定时间,然后每隔一秒读一次打印出来,看秒是不是在正常递增,这样能快速判断是通信问题还是走时问题。