资讯动态

STM32驱动DS1302实时时钟芯片:从时序原理到完整代码实现

发布时间:2026/10/14 1:08:12 来源:尧图企业网站定制
1. 为什么搞了一整天才搞定一个三块钱的时钟芯片先说个实际场景某开发者最近在做一个小作品需要给系统加一个实时时钟功能。一开始直接用 STM32 内部的 RTC但是板子一断电时间就归零重新上电还得手动校准。然后他翻出抽屉里吃灰很久的 DS1302就是那种 DIP8 封装、三根线和单片机通信的芯片淘宝几毛钱一颗很多开发板配套的蓝色小模块。折腾了一整天中间遇到了读出来全是 0xFF、时间不走、数据写不进去各种问题最终调通了。这篇笔记就是把这一天的经验拆开揉碎讲清楚 DS1302 到底怎么驱动。适合同样在做单片机项目、被时钟芯片折磨过或者准备用 DS1302 做小作品的朋友也适合给刚接触 SPI 类时序通信的开发者作为一份入门参考。DS1302 是一颗很老但生命力很强的实时时钟芯片Dallas 公司当年出品的三线接口CE、SCLK、IO支持 BCD 码格式读写时间和日期数据内置 31 字节静态 RAM还能检测电源掉电状态。很多开发板上的时钟模块用的就是它原因很简单便宜、外围电路少、驱动逻辑也不算难。但这颗芯片有个非常坑的地方——对时序要求严格而且它用的是 SPI 协议的变种跟标准 SPI 不完全一样。网上很多例程写得糊里糊涂照抄以后读出来的数据要么全是 0xFF要么时间不走。这篇文章会从原理到代码把每一步都讲清楚。2. 芯片关键信息先把 DS1302 的脾气摸清楚2.1 引脚功能与最小系统搭建DS1302 一共 8 个引脚实际用到的核心就是三个CE芯片使能、SCLK串行时钟、IO数据输入输出。其他引脚分别是 VCC2主电源、VCC1备用电池、X1 和 X232768Hz 晶振、GND。需要注意的一点是VCC1 是接备用电池的很多初学者接反了或者干脆没接导致主电源一断时间就丢。VCC1 直接接一颗 CR2032 纽扣电池正极负极接 GND或者用一颗超级电容也行。VCC2 接系统主电源 3.3V 或者 5V。晶振必须用 32.768kHz不能拿普通晶振替代。因为 DS1302 内部就是靠 32768 分频得到 1Hz 秒脉冲的晶振频率不对秒计数就会偏快或者偏慢。这个晶振的负载电容一般 6pF不接也可以工作但接了更稳。IO 引脚需要接一个上拉电阻一般 4.7kΩ 到 10kΩ 都行。当然有部分开发板模块上已经自带上拉电阻了如果是裸芯片自己搭电路务必加。电源方面还要注意 DS1302 的工作电压范围是 2.0V 到 5.5VSTM32 的开发板一般是 3.3V 供电直接兼容。如果用的是 5V 系统串个 1kΩ 电阻限流会更稳妥。2.2 为什么说 DS1302 的时序是 SPI 的变种DS1302 的数据传输格式跟标准 SPI 类似但细节上差别很大。标准 SPI 有四个引脚SCK、MOSI、MISO、CS全双工通信。而 DS1302 只有一根 IO 线半双工读写时分时复用这一根线。更关键的区别在于数据采样的时序要求。DS1302 是在 SCLK 的上升沿读取数据数据在下降沿之后改变。也就是说你要先把数据放到 IO 引脚上然后给 SCLK 一个上升沿芯片在此时锁存数据。反过来读数据时芯片在 SCLK 的下降沿之后把数据输出到 IO 上STM32 需要在上升沿去采样。很多网上例程的写法是先拉高 SCLK 再改变 IO 数据或者反过来导致读回来的数据错位。这里我直接给结论写数据时先在 SCLK 低电平期间放好 IO 数据然后拉高 SCLK再拉低读数据时先拉高 SCLK然后读 IO 电平再拉低 SCLK。这个时序细节太重要了后面代码部分会严格按这个来写。2.3 命令字节和寄存器地址DS1302 的所有读写操作都从发送一个命令字节开始。命令字节的格式相当有规律第 7 位固定为 1第 6 位选择 RAM 还是时钟寄存器0 表示时钟1 表示 RAM第 5 到第 1 位是寄存器地址第 0 位是读写标志0 表示写1 表示读。所以你要读秒寄存器命令字节就是 0x81写秒寄存器就是 0x80。读年寄存器是 0x8D写年是 0x8C。这个规律掌握以后不需要死记硬背地址表自己都能推算出来。时钟寄存器的地址从 0x00 到 0x06依次是秒、分、时、日、月、星期、年。另外还有一个写保护寄存器地址 0x0E你要往时间寄存器写数据之前必须先关掉写保护也就是往 0x8E 写入 0x00。否则你写进去的数据全都会被忽略。这个坑几乎每个人都会踩一次。3. 硬件连接与初始化流程搭建环境3.1 引脚分配与接线建议我自己用的 STM32F103 系列开发板DS1302 模块接的是普通 GPIO没走硬件 SPI因为 DS1302 的时序用 GPIO 模拟反而更方便。接线方式如下DS1302 引脚STM32 引脚说明CEPB12片选高电平使能SCLKPB13时钟信号IOPB14数据线VCC23.3V主电源VCC1电池正极备用电源GNDGND共地选 PB12、PB13、PB14 是因为这几个引脚挨得近接线方便。实际上任意 GPIO 都行代码里把引脚号改一下就行。但是注意一点尽量选不用到复用功能的引脚避免跟调试口、下载口冲突。比如 PA13、PA14 是 SWD 调试口如果你把它拿来接 DS1302下载一次程序就断一次连接非常痛苦。3.2 初始化代码与 GPIO 配置初始化部分其实很简单就是把三个引脚配置成推挽输出IO 引脚后续还要在读取的时候切换成输入模式。void DS1302_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); GPIO_InitStructure.GPIO_Pin GPIO_Pin_12 | GPIO_Pin_13 | GPIO_Pin_14; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); DS1302_CE_LOW(); // CE 拉低禁用芯片 DS1302_SCLK_LOW(); // SCLK 初始为低 }这里的核心思路是先把 CE 拉低让芯片处于未选中状态防止上电瞬间的杂散信号误触发写入操作。SCLK 也得先拉低保证第一个上升沿是可控的。3.3 晶振起振需要注意的事项还有一个常见问题DS1302 模块上电后时间不走秒寄存器读出来一直是同一个值。大概率是晶振没起振。DS1302 的晶振起振比较慢尤其是匹配电容不合适或者晶振质量一般的时候可能上电后要好几百毫秒甚至更久才稳定振荡。解决办法是初始化之后做一个等待延时等 1 秒钟左右再去做时间设置和读取给晶振足够的时间起振。另外 DS1302 有一个 trickle charger 寄存器地址 0x08用于给备用电池充电。如果你用的是不可充电的纽扣电池千万不要使能这个功能否则电池会被充鼓包甚至漏液。默认状态下这个寄存器是 0x00即充电功能关闭保持默认就行。4. 驱动代码实现一步一步拆解4.1 底层时序函数写字节和读字节驱动代码的核心就是两个函数写入一个字节到 DS1302、从 DS1302 读出一个字节。所有寄存器操作都建立在这两个函数之上。void DS1302_WriteByte(uint8_t dat) { uint8_t i; for(i 0; i 8; i) { if(dat 0x01) DS1302_IO_HIGH(); else DS1302_IO_LOW(); DS1302_SCLK_HIGH(); DS1302_SCLK_LOW(); dat 1; } }这里有个关键点DS1302 是低位先传输。所以从最低位开始写每写一位就产生一个上升沿。先拉低 SCLK放好数据然后再拉高写完这一位之后拉低。uint8_t DS1302_ReadByte(void) { uint8_t i, dat 0; for(i 0; i 8; i) { dat 1; DS1302_SCLK_HIGH(); if(DS1302_IO_READ()) // 读取 IO 电平 dat | 0x80; DS1302_SCLK_LOW(); } return dat; }读字节的时候时序反过来。SCLK 上升沿之后芯片把数据放到 IO 引脚上这时 STM32 去读取 IO 电平。要注意读取之前得把 IO 引脚切换成输入模式否则读到的永远是高电平。注意读字节函数里用的是dat 1之后判断最高位相当于先读到的数据放在高位。配合低位先传的规则正好与写入时低位先发的顺序对应。这一点很容易搞反搞反的后果是读出来的时间数据完全不对。4.2 单字节读写封装带 CE 控制的完整流程只写了字节还不能直接用因为一次完整的寄存器操作需要有 CE 引脚的配合。CE 拉高开始通信通信结束后 CE 拉低。uint8_t DS1302_ReadReg(uint8_t addr) { uint8_t dat; DS1302_CE_HIGH(); DS1302_WriteByte(addr | 0x01); // 命令字节读操作 dat DS1302_ReadByte(); DS1302_CE_LOW(); return dat; } void DS1302_WriteReg(uint8_t addr, uint8_t dat) { DS1302_CE_HIGH(); DS1302_WriteByte(addr 0xFE); // 命令字节写操作 DS1302_WriteByte(dat); DS1302_CE_LOW(); }注意命令字节的构造读命令是在地址基础上 OR 0x01写命令是在地址基础上 AND 0xFE也就是最低位设为 0。地址本身是 7 位的最高位第 8 位固定为 1通过 OR 0x80 来设置。上面的代码里传入的 addr 如果已经是完整的命令字节比如 0x80 或者 0x81那么直接 OR 0x01 或者 AND 0xFE 都没问题。但如果传入的是纯地址0x00 到 0x0E那就需要先 OR 0x80。所以我习惯把 addr 直接定义为完整的命令字节基地址比如读秒传 0x81写秒传 0x80。这样逻辑更清晰也不容易出错。4.3 时间数据结构与 BCD 码转换DS1302 内部存储时间的方式是 BCD 码也就是一个字节的高四位表示十位低四位表示个位。比如秒寄存器中 0x59 表示 59 秒0x05 表示 5 秒。所以驱动里必须有 BCD 码和十进制之间的转换函数。uint8_t BCD2DEC(uint8_t bcd) { return (bcd 4) * 10 (bcd 0x0F); } uint8_t DEC2BCD(uint8_t dec) { return ((dec / 10) 4) | (dec % 10); }这里有个容易犯的错误直接把十进制的 59 写入秒寄存器得到的时间就会完全错乱。必须先转成 BCD 再写入读出来的 BCD 再转回十进制才能显示。我见过不少人卡在这个转换上调试了半天发现时间数值乱跳其实就是忘了 BCD 的事。4.4 完整的时间读取与设置函数时间结构体沿用标准 C 的 tm 结构体思路定义如下typedef struct { uint8_t year; uint8_t month; uint8_t day; uint8_t hour; uint8_t minute; uint8_t second; uint8_t week; } DS1302_Time_t;读取当前时间的代码void DS1302_GetTime(DS1302_Time_t *time) { time-second BCD2DEC(DS1302_ReadReg(0x81)); time-minute BCD2DEC(DS1302_ReadReg(0x83)); time-hour BCD2DEC(DS1302_ReadReg(0x85)); time-day BCD2DEC(DS1302_ReadReg(0x87)); time-month BCD2DEC(DS1302_ReadReg(0x89)); time-week BCD2DEC(DS1302_ReadReg(0x8B)); time-year BCD2DEC(DS1302_ReadReg(0x8D)); }设置时间的代码void DS1302_SetTime(const DS1302_Time_t *time) { DS1302_WriteReg(0x8E, 0x00); // 关闭写保护 DS1302_WriteReg(0x80, DEC2BCD(time-second)); DS1302_WriteReg(0x82, DEC2BCD(time-minute)); DS1302_WriteReg(0x84, DEC2BCD(time-hour)); DS1302_WriteReg(0x86, DEC2BCD(time-day)); DS1302_WriteReg(0x88, DEC2BCD(time-month)); DS1302_WriteReg(0x8A, DEC2BCD(time-week)); DS1302_WriteReg(0x8C, DEC2BCD(time-year)); DS1302_WriteReg(0x8E, 0x80); // 打开写保护 }设置时间时分表要注意一个坑星期寄存器的取值范围是 1 到 7分别对应周日到周六。有些例程里写成 0 到 6会导致星期显示错位一天。4.5 主函数里的调用示例int main(void) { DS1302_Init(); delay_ms(1000); // 等待晶振起振 DS1302_Time_t current_time {23, 12, 30, 23, 59, 55, 6}; DS1302_SetTime(current_time); while(1) { DS1302_GetTime(current_time); printf(20%02d-%02d-%02d %02d:%02d:%02d\n, current_time.year, current_time.month, current_time.day, current_time.hour, current_time.minute, current_time.second); delay_ms(1000); } }设置的时间是 2023 年 12 月 30 日 23:59:55星期 6。设置完以后等 5 秒就能看到时间自动翻到 2024 年秒也正常走动。5. 实操过程记录踩坑与解决实录5.1 读回来全是 0xFF 的原因分析这个问题最常见。一开始我接好线以后运行程序读回来的所有寄存器都是 0xFF。排查步骤大概是这样的第一反应是接线问题。检查 CE、SCLK、IO 三根线是不是接对了位置电源和地是不是共地了。注意单片机和模块必须共地否则信号电平没有参考地通信必然失败。第二反应是 GPIO 配置问题。确认三个引脚是不是真的配置成了输出模式IO 引脚在读取的时候有没有切换成输入模式。很多人只配置了输出没有切换输入导致读引脚读到的一直是高电平也就是 0xFF。第三是时序问题。用逻辑分析仪抓一下 SCLK、CE、IO 三个引脚的波形跟数据手册上的时序图比对。我实际测试中发现有些代码里 SCLK 的持续时间太短高低电平宽度不足 1µsDS1302 跟不上就会导致读取出错。排查下来最常见的还是 IO 引脚方向没有切换。DS1302 的数据线是双向的写入时 STM32 输出读取时 STM32 必须改成输入这个方向切换必须发生在 CE 拉高之后、读字节之前。5.2 时间不走和乱码问题时间不走大概率是晶振问题。可以用示波器或者逻辑分析仪看 X1、X2 引脚是否有时钟信号。如果没有检查晶振有没有焊好、选的频率是不是 32.768kHz、负载电容合不合适。时间乱码则有两种情况。一种是 BCD 转换没做往寄存器里写入了十进制值。另外一种是读写时序错位导致数据串位。如果读回来的数据每个字节的数值都是 0 到 9 范围内但明显不对那基本可以确定没有做 BCD 转换。还有一种隐蔽的情况写保护没有真正关闭。DS1302 的写保护寄存器 0x8E往里面写 0x00 才是关闭写保护。注意这个操作本身也需要在 CE 拉高后执行完整的写时序。如果你在初始化过程中掉了某一步写保护就没关掉时间数据写不进去。5.3 时间走得快或慢怎么办当然DS1302 的晶振误差是客观存在的。我实际测试下来普通晶振一天误差大约在 1 到 3 秒左右。如果你做的东西对时间精度有要求有两个方向可以处理。一个方向是找带温度补偿的晶振但这东西对于 DS1302 这种芯片来说有点大材小用。另一个方向是软件校准也就是定期获取标准时间然后重置 DS1302。比如接上 WiFi 模块或者 4G 模块以后每次上电先通过网络获取一次标准时间写入 DS1302之后系统运行期间就靠 DS1302 维持时间。5.4 备份电池的正确接法接备用电池的时候注意两个问题。第一是电池正极接 VCC1负极接 GND方向绝对不能反。第二是 DS1302 的 trickle charger 功能默认关闭但如果你在代码里不小心操作了 0x90 地址的寄存器就可能把充电功能打开这时候用一次性纽扣电池就有安全风险。此外接备用电池后还有一个细节主电源断电后DS1302 进入低功耗模式靠着 VCC1 的电池继续跑。这种模式下电流大概是 0.3µA 到 1µA 级别一节 CR2032 撑几年没问题。所以不要在主电源正常工作时把备用电池拿掉否则切换电源的瞬间时间可能丢失。6. 关于移植和扩展的一些想法6.1 换一个 STM32 型号怎么改驱动代码跟 STM32 型号的耦合度其实很低。唯一的地址就是把DS1302_IO_HIGH()、DS1302_IO_LOW()、DS1302_IO_READ()这几个宏定义换成实际芯片对应的操作。如果从标准库换到 HAL 库或者 LL 库也只需要改动硬件操作层时序逻辑完全不用动。一个好的驱动设计就是应该把硬件抽象出来让上层逻辑跟具体平台无关。6.2 能不能用硬件 SPI 驱动 DS1302DS1302 是老芯片用硬件 SPI 驱动它完全可行但没必要。因为它的时序不是标准 SPI 时序即便用了硬件 SPI也要去配置 CPOL 和 CPHA 参数而且半双工模式下 IO 方向切换很麻烦反而增加了代码复杂性。GPIO 模拟时序虽然看起来低级但胜在可控、易调试、通用性强。我真的见过有人用硬件 SPI 驱动 DS1302最后为了处理引脚方向切换的问题代码写得比 GPIO 模拟还多。所以这种老芯片老老实实用 GPIO 模拟是最务实的方案。6.3 DS1302 之外的其他 RTC 芯片对比顺便提一嘴市面上除了 DS1302 还有几款常见的 RTC 芯片对比一下方便大家在方案选型时做判断。芯片型号接口类型精度表现特点DS1302三线串行类 SPI一般便宜外围简单有 RAMDS3231I2C高内置 TCXO精度高适合对时间要求高的场合PCF8563I2C一般引脚少I2C 时序比 DS1302 的变种 SPI 更通用RX8025I2C高日系芯片内置晶振精度稳定如果你的项目对时间精度要求很高比如日误差不能超过 1 秒那就别纠结 DS1302 了DS3231 这种带温度补偿的芯片更适合。但如果只是做个闹钟、定时器、记录时间戳DS1302 的精度完全够用而且便宜大碗。6.4 驱动代码的工程组织结构代码组织也很重要。建议把 DS1302 相关代码拆成三个文件ds1302.h、ds1302.c、ds1302_hw.c。ds1302.h放结构体定义和函数声明。ds1302.c放 BCD 转换、时间读写、寄存器操作这些与平台无关的逻辑。ds1302_hw.c放 GPIO 初始化和电平操作这种跟 STM32 强相关的部分。这样以后换平台只需要重写ds1302_hw.c上层逻辑完全复用。这种分层的思路在稍微大一点的嵌入式项目里是基本素养。哪怕就驱动一颗小芯片也值得养成这个习惯。7. 常见问题速查表很多朋友在调试 DS1302 的时候会遇到差不多的问题我整理一个速查表方便大家对照排查。现象可能原因解决方案读到的数据全是 0xFFIO 引脚没有切换输入模式读取前调用切换输入函数读到的数据全是 0x00IO 引脚没有上拉电阻加上拉电阻 4.7kΩ 到 10kΩ时间不走晶振未起振或频率不对检查晶振是否为 32.768kHz等待晶振起振时间写不进去写保护寄存器未关闭先写 0x8E 0x00时间数值乱跳BCD 码未转换写入前 DEC2BCD读取后 BCD2DEC星期显示错位星期寄存器范围搞错确认取值范围为 1~7对应周一或周日视编码习惯而定断电时间丢失VCC1 没接电池接入备用电池或超级电容通信不稳定SCLK 时序太短确保每个时钟高低电平持续时间不低于 1µs排查的时候建议按照这个顺序来先看硬件接线和供电再看时序波形最后查代码逻辑。不要一上来就怀疑时序问题很多坑往往是忘记切换 GPIO 方向这种低级错误。再分享一个我个人觉得很有用的调试习惯驱动 DS1302 的第一件事不要写整套代码先只写读秒寄存器的功能读完打印到串口。如果秒寄存器能正常变化说明时序和晶振都没问题再往下扩展其他寄存器的读写。如果读的是 0xFF继续排查时序和引脚配置。这种方式可以把问题范围快速缩小比写完一整包代码再去调试高效得多。DS1302 这颗芯片虽然老但它背后涉及的 GPIO 模拟时序、BCD 码转换、写保护机制这些知识点做单片机开发都是通用的。把它的驱动逻辑吃透后面再去调其他 SPI、I2C 芯片就能少走不少弯路。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑