本来这块功能也只是个“会用就行”的小模块,但真要放到项目里,从硬件连接、初始化时序到读取逻辑,处处都有讲究。这篇文章就把我从零开始驱动 DS1302 的完整过程记录下来,包括为什么这么连、为什么这么写时序、代码怎么组织结构最舒服,以及实测中踩过哪些坑。内容偏向学习笔记,代码基于 STM32 标准外设库编写,地址范围、寄存器定义都会拆开讲清楚,方便你移植或改写成 HAL 库版本。
1. 项目整体设计与思路拆解
1.1 为什么选 DS1302,而不是 DS3231 或内部 RTC
做实时时钟方案,第一反应可能是直接用 STM32 内部 RTC,或者挂一颗 DS3231。
内部 RTC 的优点是不用额外花钱,但缺点是:主芯片断电后依靠 VBAT 引脚供电,如果 MCU 本身处于低功耗模式,RTC 依然能跑;但如果芯片完全断电,VBackup 域的电源没处理好,时间就会丢失。另一个问题是内部 RTC 的校准精度一般,32.768kHz 晶振的匹配电容需要手工调节,温度漂移也偏大。
DS3231 确实精度很高,自带温度补偿,但价格比 DS1302 贵了几倍,而且常用的模块基本上是 I2C 接口。如果项目只需要“能显示时间、掉电后继续走、精度不要求到秒级误差几 ppm”,DS1302 是最有性价比的选择。
DS1302 是 3 线接口,本质上是一种 SPI 变体,但和标准 SPI 又有区别:它支持单字节突发模式和多字节突发模式,数据位是从 LSB 开始传输的,而且 CE(片选)高电平期间可以连续操作。很多人第一次写驱动时,最容易犯的错误就是用硬件 SPI 去连它,结果发现怎么调都不对。
DS1302 的工作电压范围是 2.0V 到 5.5V,STM32F103 系列是 3.3V 供电,直接可以兼容。如果你用的是 5V 单片机(比如 Arduino UNO 的 ATmega328P),虽然也能工作,但还是建议串一个 1k 电阻在数据线上,防止电平不匹配。
1.2 功能拆解:这个项目的核心需求
这个项目的需求很明确:STM32 通过 GPIO 模拟时序,驱动 DS1302 芯片,实现三个功能。
- 写时间:把当前时间写入 DS1302 内部寄存器。
- 读时间:从 DS1302 读回当前时间,用于显示或者后台记录。
- 掉电保持:DS1302 外部接一个 3V 纽扣电池,在主电源断开后依然能维持计时。
这三个功能看似简单,但驱动层面涉及的东西并不少:控制字的格式、寄存器的地址映射、读写时序的 GPIO 方向切换、BCD 码转换、以及最常见的“读出来的秒数据跳动异常”问题。
所以在设计驱动代码时,我做了如下分层:
- 底层的 GPIO 模拟层:封装 SCLK、IO、CE 三个引脚的初始化、拉高拉低、输入输出模式切换。
- 协议层:实现 DS1302 的单字节读写时序、突发模式读写、以及控制命令的构造。
- 应用层:提供
DS1302_SetTime、DS1302_GetTime这样的 API,返回一个结构体,直接就是年月日时分秒星期,和 BCD 码无关。
这种分层的好处是,换芯片型号时只需要改底层,协议层和应用层不用动;换 MCU 平台时,也只改底层 GPIO 操作函数,协议代码基本可以直接复用。
1.3 移植思路:如何从标准库快速改到 HAL 库
这次给出的代码是基于标准外设库的,因为学习笔记用标准库更直观,GPIO 的每一个操作都写在明面上。如果你想移植到 HAL 库,核心工作就是把DS1302_SClk_High()、DS1302_SClk_Low()这一层换成对应的HAL_GPIO_WritePin()调用,同时把引脚初始化改成MX_GPIO_Init()里对应的配置。
模拟时序部分是最容易移植的,因为本质就是延迟 + 拉电平。唯一要注意的是 IO 口的方向切换。
DS1302 的数据线是双向的,在写命令时,IO 口必须是输出模式;在读数据时,IO 口必须立刻切换成输入模式。如果切换速度太慢,可能会导致第一个 bit 读错。在标准库里,我是这样处理的:
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_IO_PORT, &GPIO_InitStructure);改为输入模式时,把GPIO_Mode_Out_PP换成GPIO_Mode_IPU。这里用内部上拉输入,是为了读取外部信号时有一个确定的默认电平。如果你用的是外部上拉电阻,内部上拉可以不开,但大多数情况下内部上拉就够用。
2. 芯片基础与协议细节:DS1302 到底是什么
2.1 DS1302 的引脚功能与硬件连接
DS1302 有 8 个引脚,常见的封装是 DIP-8 和 SOIC-8。各引脚定义如下:
| 引脚号 | 名称 | 功能说明 |
|---|---|---|
| 1 | VCC2 | 主电源正极,接 3.3V 或 5V |
| 2 | X1 | 32.768kHz 晶振输入 |
| 3 | X2 | 32.768kHz 晶振输出 |
| 4 | GND | 接地 |
| 5 | CE | 片选使能,高电平有效 |
| 6 | SCLK | 串行时钟输入 |
| 7 | IO | 串行数据输入/输出 |
| 8 | VCC1 | 备用电池正极,接 3V 纽扣电池 |
DS1302 内部的电源电路会在 VCC2 和 VCC1 之前自动切换。当 VCC2 的电压高于 VCC1 约 0.2V 时,使用 VCC2 供电;当 VCC2 掉电时,自动切换到 VCC1。这个切换过程是芯片内部完成的,不需要外部二极管防倒灌,但实际设计时最好在 VCC2 引脚加一个 0.1uF 的退耦电容,VCC1 加一个 1uF 左右的电容。
晶振的选型有讲究。DS1302 需要外接 32.768kHz 晶振,负载电容通常是 6pF。实际买模块时,很多商家已经把晶振和两个 6pF 电容焊在板子上了。如果你是自己画 PCB,不要省钱,晶振尽量买大厂的正品。劣质晶振的温漂很大,一天下来差几十秒都很正常。
接 STM32 时,GPIO 的选择比较随意,任意三个空闲引脚都可以。我习惯把 CE、SCLK、IO 接在同一组 GPIO 上,方便初始化代码统一处理。比如用 GPIOA 的 Pin 0、Pin 1、Pin 2。这三个引脚在 STM32F103 上还可以复用为 ADC 或 USART,注意避开你项目已经用掉的功能就行。
2.2 寄存器和内部 RAM 的地址分布
DS1302 的内部结构分两部分:时钟寄存器和通用 RAM。
时钟寄存器从地址 0x80 开始,每个寄存器占两个地址,一个是写地址,一个是读地址。具体映射如下:
| 寄存器名称 | 写地址 | 读地址 | 取值范围 | 说明 |
|---|---|---|---|---|
| 秒 | 0x80 | 0x81 | 0-59 | 最高位为时钟暂停位 CH |
| 分 | 0x82 | 0x83 | 0-59 | - |
| 时 | 0x84 | 0x85 | 1-12 或 0-23 | 最高位为 12/24 小时制选择 |
| 日 | 0x86 | 0x87 | 1-31 | - |
| 月 | 0x88 | 0x89 | 1-12 | - |
| 星期 | 0x8A | 0x8B | 1-7 | 1 表示星期日,7 表示星期六 |
| 年 | 0x8C | 0x8D | 0-99 | 存储的是年份的后两位 |
| 控制 | 0x8E | 0x8F | - | 写保护控制寄存器 |
我刚开始写驱动时,最困惑的就是为什么同一个寄存器有写地址和读地址两个。仔细研究后才发现,这是 DS1302 的命令字节决定的。
命令字节(也就是你发送到芯片的第一个字节)的格式如下:
- Bit 7:固定为 1,表示有效命令。
- Bit 6:0 表示时钟寄存器,1 表示 RAM 区。
- Bit 5 到 Bit 1:寄存器地址。
- Bit 0:0 表示写,1 表示读。
所以写秒寄存器的命令字节是0x80,读秒寄存器是0x81。写控制寄存器是0x8E,读控制寄存器是0x8F。理解了这一层,寄存器表就不用死记了,随时可以推导出来。
控制寄存器里只有一个位需要注意:Bit 7 是 Write Protect(WP)。上电时 WP 默认为 1,也就是写保护开启。你要写入时间之前,必须先把 WP 清零,写完之后可以恢复为 1。
2.3 BCD 码:DS1302 与单片机之间的数据格式
DS1302 内部存储的是 BCD 码,不是普通的二进制数。
BCD 码用 4 个二进制位表示一个十进制数字。比如十进制的 23,BCD 码就是 0x23,二进制表示为 0010 0011。直接看十六进制值,就能一眼看出它对应的十进制数值,这在调试时非常方便。
但 STM32 内部运算用的是二进制数。比如时间结构体里存的是uint8_t hour = 23,它的二进制值是 0001 0111。如果你直接把这个值发送给 DS1302,芯片会把它当成 BCD 码 0x17,也就是 17 点,这就错了。
所以驱动里必须有转换函数:
uint8_t DS1302_DecToBcd(uint8_t val) { return (val / 10) << 4 | (val % 10); } uint8_t DS1302_BcdToDec(uint8_t val) { return (val >> 4) * 10 + (val & 0x0F); }道理很简单:十进制转 BCD 时,十位数放高四位,个位数放低四位;BCD 转十进制时,反过来操作。这个转换是这一整个驱动的核心,如果这里写错了,就会出现“写入 12 点,读出来 18 点”这种怎么都查不出来的诡异问题。
3. 实测核心过程:GPIO 模拟时序与代码逐段解析
3.1 GPIO 初始化与底层操作函数
首先定义引脚宏。我使用 STM32F103C8T6 的 GPIOA:
#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_GPIO_CLK RCC_APB2Periph_GPIOA初始化时,三个引脚的初始状态有要求:CE 必须为低电平,SCLK 必须为低电平,IO 可以输出低电平。这是因为 DS1302 在 CE 上升沿之前,所有逻辑都处于复位状态。如果 CE 一开始就是高电平,芯片可能会误判为一次无效的访问,导致状态机错乱。
void DS1302_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(DS1302_GPIO_CLK, ENABLE); GPIO_InitStructure.GPIO_Pin = DS1302_CE_PIN | DS1302_SCLK_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(DS1302_GPIO_PORT, &GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin = DS1302_IO_PIN; GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_Init(DS1302_GPIO_PORT, &GPIO_InitStructure); DS1302_CE_LOW(); DS1302_SCLK_LOW(); DS1302_IO_LOW(); }接着是底层电平操作。我封装成宏,方便移植:
#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)还要有一个 IO 方向切换函数:
void DS1302_IO_ModeSet(uint8_t mode) { GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin = DS1302_IO_PIN; GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz; if (mode) { GPIO_InitStructure.GPIO_Mode = GPIO_Mode_Out_PP; } else { GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPU; } GPIO_Init(DS1302_GPIO_PORT, &GPIO_InitStructure); }3.2 单字节写入时序的完整实现
DS1302 的写时序可以拆解为以下几个阶段:
- CE 拉高,启动通信。
- 发送命令字节,一个 bit 一个 bit 地往 IO 上送。
- 发送要写入的数据字节。
- CE 拉低,结束通信。
每个 bit 的发送方式完全一致:先把数据放到 IO 上,然后拉高 SCLK,拉低 SCLK。数据在 SCLK 上升沿被芯片锁存。
代码如下:
void DS1302_WriteByte(uint8_t dat) { uint8_t i; DS1302_IO_ModeSet(1); // 设置为输出模式 for (i = 0; i < 8; i++) { if (dat & 0x01) { DS1302_IO_HIGH(); } else { DS1302_IO_LOW(); } delay_us(2); DS1302_SCLK_HIGH(); delay_us(2); DS1302_SCLK_LOW(); delay_us(2); dat >>= 1; } }注意这里是从 LSB(最低位)开始发送的。这和大多数 SPI 设备先发最高位不一样。很多人照着 SPI 的习惯写代码,把数据从最高位开始发,结果时间完全错乱。
以DS1302_WriteReg(0x80, 0x00)为例,命令字节 0x80 的二进制是 1000 0000。发送顺序是:先发 Bit0=0,再发 Bit1=0,一直发到 Bit7=1。这意味着命令字节的数据实际上是反向传输的,第一次看这种时序会觉得别扭,但芯片就是这么要求的。
地址和数据的完整写入函数如下:
void DS1302_WriteReg(uint8_t addr, uint8_t dat) { DS1302_CE_HIGH(); DS1302_WriteByte(addr); DS1302_WriteByte(dat); DS1302_CE_LOW(); }这里地址参数是写地址,比如0x80。
写入时间的具体调用流程是这样的:
DS1302_WriteReg(0x8E, 0x00); // 关闭写保护 DS1302_WriteReg(0x80, 0x00); // 秒,清零 CH 位,开始走钟 DS1302_WriteReg(0x82, 0x00); // 分 DS1302_WriteReg(0x84, 0x12); // 时,这里 BCD 码 0x12 表示 12 点 DS1302_WriteReg(0x8E, 0x80); // 打开写保护你会发现,秒寄存器写入 0x00 时,最高位 CH 也是 0,这样 DS1302 才会启动振荡器。如果秒寄存器最高位是 1,芯片会处于暂停状态,时间不走。
3.3 单字节读取时序的完整实现
读时序比写时序多一个数据方向切换的环节。
先发送命令字节(比如 0x81,即读秒寄存器),此时 IO 是输出模式。命令发送完毕,必须立刻把 IO 切换为输入模式,然后开始接收芯片返回的数据。
接收数据的时刻是 SCLK 下降沿。也就是说,芯片在 SCLK 下降沿驱动 IO,单片机在 SCLK 下降沿之后去读 IO,两边的动作刚好配合上。
uint8_t DS1302_ReadByte(void) { uint8_t i, dat = 0; DS1302_IO_ModeSet(0); // 切换到输入模式 for (i = 0; i < 8; i++) { dat >>= 1; DS1302_SCLK_HIGH(); delay_us(2); DS1302_SCLK_LOW(); delay_us(2); if (DS1302_IO_READ() != 0) { dat |= 0x80; } } DS1302_IO_ModeSet(1); // 恢复输出模式 return dat; }几个细节说明一下。
dat >>= 1放在读 bit 之前,保证第一次读到的 bit0 最后会被放到 dat 的 Bit0 位置。因为最终返回值dat |= 0x80会把当前读到的位放到最高位,但紧接着下一次循环dat >>= 1就把它移动到正确位置。整个过程做完后,返回的dat已经是一个正常的 8 位数据。
DS1302_IO_READ()宏定义:
#define DS1302_IO_READ() GPIO_ReadInputDataBit(DS1302_GPIO_PORT, DS1302_IO_PIN)读取寄存器的完整函数:
uint8_t DS1302_ReadReg(uint8_t addr) { uint8_t dat; DS1302_CE_HIGH(); DS1302_WriteByte(addr | 0x01); // 把最低位改成 1,转为读命令 dat = DS1302_ReadByte(); DS1302_CE_LOW(); return dat; }为什么addr | 0x01?因为调用时传入的是写地址,比如秒寄存器是 0x80。读命令要求最低位是 1,所以加上 1 就变成 0x81。这样调用时不用额外记一套读地址表,写代码时更顺手。
3.4 整包读写时间:突发模式更省事
DS1302 支持多字节突发读写。连续读时,地址会自动递增,从秒寄存器一直读到年寄存器,一次通信就能把 7 个寄存器全部捞出来。
读全部寄存器的时间函数:
void DS1302_GetTime(DS1302_Time_t *time) { uint8_t buf[7]; uint8_t i; DS1302_CE_HIGH(); DS1302_WriteByte(0xBF); // 读时钟突发模式命令 for (i = 0; i < 7; i++) { buf[i] = DS1302_ReadByte(); } DS1302_CE_LOW(); time->sec = DS1302_BcdToDec(buf[0] & 0x7F); time->min = DS1302_BcdToDec(buf[1]); time->hour = DS1302_BcdToDec(buf[2]); time->date = DS1302_BcdToDec(buf[3]); time->month = DS1302_BcdToDec(buf[4]); time->dayOfWeek = DS1302_BcdToDec(buf[5]); time->year = DS1302_BcdToDec(buf[6]); }之所以对buf[0]做& 0x7F操作,是因为秒寄存器最高位是 CH(时钟暂停标志)。如果你在调试时发现读回来的秒数据超过 59,多半就是没有屏蔽这一位。
写时间的突发模式类似,命令字节是0xBE,后面跟着 7 个字节的数据:
void DS1302_SetTime(DS1302_Time_t *time) { DS1302_WriteReg(0x8E, 0x00); // 关闭写保护 DS1302_CE_HIGH(); DS1302_WriteByte(0xBE); // 写时钟突发模式命令 DS1302_WriteByte(DS1302_DecToBcd(time->sec)); DS1302_WriteByte(DS1302_DecToBcd(time->min)); DS1302_WriteByte(DS1302_DecToBcd(time->hour)); DS1302_WriteByte(DS1302_DecToBcd(time->date)); DS1302_WriteByte(DS1302_DecToBcd(time->month)); DS1302_WriteByte(DS1302_DecToBcd(time->dayOfWeek)); DS1302_WriteByte(DS1302_DecToBcd(time->year)); DS1302_CE_LOW(); DS1302_WriteReg(0x8E, 0x80); // 打开写保护 }3.5 延迟时间的确定:与时钟频率的换算关系
DS1302 的时序参数在数据手册里有明确数值,比如 SCLK 高电平最小宽度是 250ns,数据建立时间最小是 100ns。对于 STM32F103 跑 72MHz 来说,一个delay_us(2)就是 2 微秒,远大于芯片的最小要求。
但这里有一个 MCU 的注意点:如果你用的是 72MHz 主频,不加延迟直接翻转 GPIO,硬件上其实也能工作,因为 STM32 的 GPIO 翻转速度也在几 ns 级别,不会违反 DS1302 的时序要求。只是考虑到不同型号的单片机速度差异很大,加上简单的延迟函数反而能保证移植性。
我通常把延迟函数做成微秒级阻塞延时,简单粗暴:
void delay_us(uint32_t us) { uint32_t i; for (i = 0; i < (SystemCoreClock / 1000000) * us; i++) { __NOP(); } }实际效果是 1us 延时会执行几十条 NOP 指令,在调试时完全够用。如果你想更精确,可以考虑用 SysTick 定时器做延时,但对于 DS1302 这种对时序要求不高的芯片,浪费一个定时器资源实在没必要。
4. 工程文件的可靠组织方式与代码健壮性编写
4.1 时间结构体与文件规划
为了不把应用层和协议层揉在一起,我习惯建立一个ds1302.h头文件,里面放结构体定义和函数声明:
#ifndef __DS1302_H #define __DS1302_H #include "stm32f10x.h" typedef struct { uint8_t sec; uint8_t min; uint8_t hour; uint8_t date; uint8_t month; uint8_t dayOfWeek; uint8_t year; } DS1302_Time_t; void DS1302_Init(void); void DS1302_SetTime(DS1302_Time_t *time); void DS1302_GetTime(DS1302_Time_t *time); #endif对应的ds1302.c文件里放上述所有实现。主程序里只需要:
DS1302_Time_t time; time.sec = 30; time.min = 15; time.hour = 10; time.date = 20; time.month = 12; time.dayOfWeek = 5; time.year = 24; DS1302_SetTime(&time); while (1) { DS1302_GetTime(&time); printf("20%02d-%02d-%02d %02d:%02d:%02d\n", time.year, time.month, time.date, time.hour, time.min, time.sec); delay_ms(1000); }如果接入 OLED 或 LCD,可以直接把time结构体的字段显示出来。
4.2 关于 BCD 码边界情况的处理
边界情况最容易出问题的地方是年份和小时制。
年份寄存器的范围是 0-99,对应 2000-2099 年。如果项目跨世纪,比如到了 2099 年之后,那这个方案就失效了。市面上绝大多数消费级产品用 DS1302 都是这个 100 年限制,在项目一开始做方案选型时就要想清楚。
小时寄存器有一个最高位 HR12/24,决定 12 小时制还是 24 小时制。如果这个位被意外写成 1,芯片会切换到 12 小时制,而且 Bit5 变成了 AM/PM 标志。读回来时,下午时间会多出 0x20 这个偏移,比如下午 3 点,BCD 码可能是 0x23(Bit5 为 1 表示 PM),直接 BCD 转十进制就成了 23,明显不对。
规避方法:写小时寄存器时,直接强制清掉最高位,确保 24 小时制:
uint8_t hourReg = DS1302_DecToBcd(time->hour) & 0x3F; // 清除最高位4.3 写保护机制背后的坑
DS1302 有一个非常容易踩的坑:写保护位 WP 在芯片上电时默认是 1。
这意味着,如果你拿到一颗全新的 DS1302,直接调用DS1302_WriteReg(0x80, 0x00)想清零秒寄存器,芯片会拒绝执行,因为控制寄存器里的 WP 已经保护了整个时钟寄存器区。
正确顺序确实是:先给控制寄存器写入 0x00 关掉写保护,然后写时间,最后重新打开写保护。但有人会问:如果中途断电,控制寄存器里的 WP 是多少呢?
这就涉及 DS1302 的一个特性:控制寄存器的 WP 在重新上电后会恢复为 1。所以每次写入时间前,都需要先发送关保护命令,不能想着“我上次已经关了,这次直接写吧”。
在实际项目中,如果 MCU 和 DS1302 共用电源,每次系统启动时 DS1302 都是刚上电的复位状态,所以必须按这个顺序来。如果 DS1302 一直在 MCU 的 3.3V 供电下没有断电,但 MCU 自己复位了,这时 WP 可能保持为 0,但为了代码通用性,还是每次都先关保护稳妥。
4.4 时间丢失与保险丝位
DS1302 的秒寄存器最高位是 CH,作用不是单纯显示,而是控制振荡器是否工作。
- CH = 1:振荡器停止,时间不走,秒寄存器数据保留当前值。
- CH = 0:振荡器启动,开始正常走秒。
晶振不起振的原因,除了 CH 位被置 1,还有一个可能是芯片内部的“保险丝位”问题。DS1302 内部有一个特殊的 8 字节 RAM 区,其中地址为 0x20 的寄存器 Bit7 就是保险丝位(Trickle Charge Enable 相关),它控制涓流充电功能。
这句话可能让很多人懵:DS1302 本身没法给电池充电,除非外部电路设计了充电回路。这个 Bit7 只是控制芯片内部是否连接到一个电阻通路,用来配合外部超级电容或者可充电电池。如果你用的是那种 3V 不可充电纽扣电池,绝对不能开启涓流充电功能,否则电池会充爆。所以这个位保持默认值 0 就行。
5. 常见问题实录与排查方法总结
5.1 读出来的时间完全为 0 或时间不走
现象:DS1302_GetTime读回来全部是 0,或者秒数一直不变。
排查顺序:
- 检查 CE 初始状态。CE 必须确保为低电平。用万用表量引脚,如果上电瞬间 CE 有抖动为高,芯片可能进入了误状态。解决方法是初始化 GPIO 时先把 CE 拉低,再初始化其他引脚。
- 检查晶振是否起振。用示波器量 X1 和 X2 引脚,如果看到 32.768kHz 正弦波,说明晶振工作正常。没示波器的话,可以先写一个死循环读秒寄存器,看数据是否有变化,一秒内读 20 次,如果 20 次读数都完全一致,大概率是晶振没起振。
- 检查 CH 位。读出来的秒数据如果最高位是 1,说明振荡器停了。特别要注意:如果之前写过秒寄存器,并且写入值的最高位恰好是 1,芯片就会停止走时。清零方法:调用
DS1302_WriteReg(0x80, 0x00)。
5.2 读出来的时间总是不正确,偏差很大
现象:写入 12 点,读取时是 21 点;或者读到的分钟数在 0-99 之间乱跳。
经验判断,这个问题的元凶九成是 BCD 转换写反了或者写漏了。一个检查技巧:读回原始寄存器值,用表格打印出十六进制,然后手工验证转换逻辑。
比如读回来的小时寄存器是 0x12,如果 BCD 转十进制函数写的是(val & 0x0F) * 10 + (val >> 4),那结果就是 21,明显错误。正确的顺序必须是先取高四位乘以 10,再加低四位。
另一个可能原因是 IO 方向切换不及时。读取数据时,命令字节的最后一位发送完毕后,要保证 IO 已切换为输入模式,并在 SCLK 下降沿之后才读取数据。有些芯片速度较快,命令刚发完,数据位就开始出现在 IO 上。如果切换输入模式时 GPIO 内部还处于输出驱动器使能状态,此时外部数据信号和内部输出信号会互相打架,可能把数据拉偏。
5.3 后备电池耗电过快
现象:纽扣电池原本能用三年,实际一两个月就没了。
DS1302 本身的功耗很低,数据手册给出的典型时间是保持电流小于 300nA。如果电池耗电快,往往是电路设计问题。
常见原因:
- 电池正极和 VCC1 之间没加二极管,电池电流倒灌到 VCC2 侧的其它器件上。
- 外部上拉电阻跨接到 VCC1 上。比如 IO 引脚接了上拉电阻到 3.3V,但 3.3V 电源掉电后,上拉电阻正常应该是通过 VCC1 电池供电,但如果电路画错,上拉直接接 VCC1,电池就成了整个上拉网络的电源,耗电自然快。
- SOC 封装设计时引脚间有焊桥或助焊剂残留,形成微短路。
排查办法:用万用表电流档串在电池回路里,测量静态电流。如果大于 10uA,说明电路一定有问题。正常情况下,静态电流应该是几百 nA 级别。
5.4 读数据时干扰严重
现象:和电机、继电器共电源时,时间偶尔跳变或读错。
DS1302 是低速器件,但它对电源噪声很敏感。解决办法:
- VCC2 引脚旁边加 0.1uF 高频退耦电容,紧贴芯片引脚放置。
- 主电源 3.3V 和 VCC2 之间再加一个 10uF 电解电容,吸收低频波动。
- IO、SCLK、CE 三根线尽量短,不要飞线太长。
- 如果 MCU 已经驱动继电器、数码管等大电流设备,加一个磁珠或者 10R 电阻隔离一下 DS1302 的供电。
电机启动瞬间会引起电源跌落,如果 VCC2 低于 VCC1,芯片可能切换到备用电池供电。如果这时正在进行读写操作,时序就会被破坏。所以驱动大功率设备时,最好等操作完成后再启动负载,或者给 DS1302 单独供电。
5.5 新芯片上电后完全无响应
现象:无论怎么写命令,读回来的数据都是 0xFF 或者 0x00。
有一种特殊情况:芯片本来就是坏的或者引脚接错了。DS1302 的 IO 引脚是 CMOS 电平,如果和 STM32 的 GPIO 直连,3.3V 供电没问题。但如果你用的是 5V 的 DS1302 模块,模块上自带下拉电阻和上拉电阻,这时连接到 3.3V 的 STM32 就需要谨慎处理了。
市场上很多 DS1302 模块在设计上是兼容 5V 单片机的,所以模块上会引出一个 VCC 引脚,既可以是 5V 也可以是 3.3V。如果你接 3.3V,IO 电平就是 3.3V,和 STM32 兼容。如果你接 5V,那 IO 高电平就是 5V,STM32 的 GPIO 是 FT(5V 容忍)引脚才可以直连,普通引脚会损坏。
还有个低级错误:把 VCC1 和 VCC2 搞混。VCC1 接的是电池,VCC2 接主电源。如果你反过来接,芯片直接用电池供电也能跑,但主电源掉电后反而断电,时间仍然保持不住,而且电池耗电极快。
5.6 快速排查速查表
| 现象 | 可能原因 | 排查与解法 |
|---|---|---|
| 读回全为 0 | CE 初始电平不对、晶振没起振、CH 位被置 1 | 检查 GPIO 初始化顺序,用示波器看晶振波形,写 0x00 到秒寄存器 |
| 读回全为 0xFF | 引脚连接错误、IO 方向切换不及时、芯片电源未接 | 检查接线,用万用表量 CE、SCLK、IO 引脚电平 |
| 时间不走 | 振荡器暂停、晶振虚焊、CH=1 | 检查秒寄存器最高位,补焊晶振引脚 |
| 时间数值偏差大 | BCD 转换写反、小时制被改成 12 小时制 | 打印原始十六进制值,检查小时寄存器最高位 |
| 电池耗电快 | VCC1 和 VCC2 接反、外部上拉跨接、焊桥短路 | 用电流表测静态电流,检查 PCB 走线与上拉连接 |
| 写入无效 | 写保护未关闭 | 写入前必须先发0x8E, 0x00关闭 WP |
在做这个学习笔记时,我特意把硬件连接、时序、代码分成了三个独立模块来验证。先单独调试写秒寄存器,再调试读秒寄存器,最后才整合成时间结构体,这样每一步的问题都能快速定位。实际调试中出现最多的,反而是 BCD 转换和 IO 方向切换这两个看似不起眼的小点。
DS1302 虽老,但足够简单可靠。如果你第一次接触这个芯片,按照上面的代码顺序走一遍,基本不会有大问题。如果后续要接 OLED 显示,直接把DS1302_GetTime返回的时间结构体拿去格式化输出就行。