☰
STM32 GPIO模拟三线时序驱动DS1302实时时钟芯片详解
2026/10/21 1:08:16 网站建设 项目流程

最近把一个吃灰很久的小模块翻了出来:DS1302 实时时钟芯片。起因是在做一个小项目时需要给设备记录断电时间,手头又没有带 RTC 的主控,低成本方案里最容易买到、最容易焊接的就是这颗老芯片。这次干脆把驱动重新写了一遍,整理成一份可以当学习笔记用的开源小项目:STM32 通过 GPIO 模拟三线时序读写 DS1302,实现掉电不停钟、断电保持 RAM 数据,代码结构也做成了方便移植的 bsp 风格。

写这篇笔记的另一个原因,是发现很多刚入门的朋友一提到“RTC 芯片”就去用 DS3231 这类 I2C 接口的现代方案,反而忽略了 DS1302 这种学习价值更高的 SPI 变种协议。DS1302 的接口只有一个双向数据脚,时序介于 SPI 和自定义协议之间,用 GPIO 模拟一遍之后,你对时钟芯片的寄存器、BCD 编码、写保护机制都会有很直观的理解。这篇文章适合正在学 STM32、想搞懂 RTC 芯片原理的人,也适合手头刚好有一块 DS1302 模块不知道怎么下手的老哥。我会把硬件接线、时序原理、完整驱动代码和调试踩坑都放在一起写,直接照着抄基本就能调通。

1. 项目整体拆解:老芯片为什么还值得写一版驱动

1.1 DS1302 是什么,解决什么问题

DS1302 是一颗很经典的实时时钟芯片,供电范围宽,工作电压大概在 2.0V 到 5.5V 之间,直接用 3.3V 甚至 5V 的单片机都能驱动。它内部维护一套完整的日历寄存器,包括年、月、日、星期、时、分、秒,而且会自动处理闰年,年份范围从 2000 到 2099。最方便的是它自带一个涓流充电电路和掉电切换逻辑,主电源 VCC2 断电后会自动切到备用电池 VCC1 上继续走时,所以时间不会因为系统掉电就丢。这也是它在很多需要“记录设备上次运行时间”的场景里一直没被淘汰的原因。

除了时间寄存器,DS1302 内部还带了 31 字节的静态 RAM,这部分数据同样由备用电池供电,掉电不会丢失,可以用来存一些小的配置信息,比如设备编号、校准参数、开机次数之类的。对学习项目来说,这颗芯片能干的事情其实不少,但最核心的还是把“读写时间”这个基础功能吃透,其他功能都是在这个基础上扩展的。

1.2 为什么用 GPIO 模拟而不是硬件 SPI

刚拿到 DS1302 的时候,很多人第一反应是它有 SCLK 和 I/O 两个脚,很像 SPI 的 CLK 和 MOSI,那是不是可以直接挂到 STM32 的硬件 SPI 上?我刚开始也这么想过,翻完数据手册才发现不行。DS1302 的信号线只有三根:CE、SCLK、I/O,其中 I/O 是双向单线,主机写数据的时候它作为输入,主机读数据的时候它作为输出,时序上靠近 SPI 但又不完全是 SPI。标准 SPI 的 MOSI 和 MISO 是分离的,主机可以同时收发,但 DS1302 只有一个 I/O 脚,收发方向由命令字节控制,天然不是标准 SPI 的物理拓扑。

更麻烦的是字节传输顺序,DS1302 是低位在前(LSB first),而 STM32 的硬件 SPI 默认是高位在前(MSB first)。就算你用寄存器把 SPI 的位序改成 LSB first,单线双向收发的问题还是绕不过去。强行用硬件 SPI 的结果是:写命令的时候还得切 GPIO 模式来接管 MISO,折腾半天,协议上也不完全匹配,最后不如直接用 GPIO 模拟来得干净。GPIO 模拟还有个额外的好处——你能通过逻辑分析仪把每一个边沿变化看清楚,对理解芯片时序非常有帮助。

1.3 技术选型:标准库还是 HAL 库都不影响

这次代码我故意没有绑定具体的 STM32 型号和库版本,而是把硬件相关的操作全部收敛成几个宏。CE、SCLK、I/O 的引脚号和电平操作,在头文件里改一下宏就行;I/O 的输入输出切换,封装成两个内部函数。这样无论是用标准库还是 HAL 库,甚至换一块芯片,改动量都很小。

很多学习笔记的问题在于代码和开发板绑定太死,比如直接在中断里写“GPIOA->ODR |= GPIO_Pin_0”,换个板子就完全没法看。我这次用 bsp_ds1302.c 和 bsp_ds1302.h 两层结构,上层只关心“设置时间”“读取时间”“写 RAM”这类功能,底层才碰寄存器。如果你自己写驱动,建议也保持这个思路:先把三线时序的读写字节函数调通,再往上层加业务逻辑,这样每一步是能单独验证的,出了问题也好定位。

2. 硬件连接与电路细节

2.1 引脚定义与推荐接线

DS1302 的引脚不多,正面看去一般有 8 个脚:VCC2、VCC1、GND、X1、X2、CE、SCLK、I/O。VCC2 是主电源,直接接单片机这边的 3.3V 或者 5V 都行;VCC1 是备用电池脚,接一个 3V 纽扣电池的正极,电池负极和 GND 相连;CE 是片选使能脚,高电平有效,SCLK 是时钟,I/O 是双向数据脚。

我这次用的是某块学习板上引出的三个空闲 GPIO,具体分配如下表,你自己接线的时候完全可以换成别的引脚,只要三个引脚不冲突就行。

DS1302 引脚功能说明推荐连接
VCC2主电源3.3V 或 5V
VCC1备用电池正极3V 纽扣电池正极
GND地公共地
X1晶振输入32.768kHz 晶振一端
X2晶振输出32.768kHz 晶振另一端
CE片选使能PC11(示例)
SCLK串行时钟PC10(示例)
I/O双向数据PC12(示例)

这里要特别提醒一点:GPIO 和 DS1302 之间建议留一个 1kΩ 左右的串联电阻在 I/O 线上,尤其是在 5V 供电、单片机是 3.3V 电平的情况下。因为 I/O 是双向脚,输出和输入切换的时候如果电平不一致,会产生短暂的争用,串一个电阻可以保护两边都不至于被灌电流搞坏。如果模块已经带了电阻,那就直接用模块上的,不需要再加。

2.2 晶振和电容的讲究

DS1302 的时间基准来自一颗 32.768kHz 的石英晶振,这个频率是 RTC 行业的标准频率,因为 32768 正好是 2 的 15 次方,内部经过 15 级分频就能得到 1Hz 的秒信号。晶振直接接在 X1 和 X2 两个引脚上,不需要额外串联电阻。

很多新手在这里栽跟头:模块上晶振虚焊、用了质量很差的晶振、或者引脚焊盘之间有残留焊锡导致漏电,结果就是时间不走或者走时误差大得离谱。DS1302 内部对晶振负载电容有优化,外部一般不需要再并电容,但 PCB 上 X1、X2 走线如果太长,寄生电容就会拉低振荡频率,表现是每天慢几十秒。我实际调过的模块,只要晶振焊接正常,走时误差在常温下大概每天几秒到十几秒,如果你需要更高精度,这颗芯片本身就不太合适。

选购模块的时候还要注意一点:有些廉价模块上的晶振负温度系数很差,夏天和冬天误差变化明显。如果你只是做学习演示,玩通时序就够了;如果是批量产品,建议直接评估更高级的 RTC 方案。

2.3 备用电池与电源切换逻辑

DS1302 内置了电源切换电路,VCC2 电压高于 VCC1 时用主电源供电,同时还可以给备用电池涓流充电;VCC2 掉到 VCC1 以下时自动切换到 VCC1 供电。这也是它比普通 RAM 芯片方便的地方,外置切换二极管都不需要了。

备用电池我建议用 3V 纽扣电池,比如常见的 CR2032,正极接 VCC1,负极接 GND。要注意电压不能太高,如果用 3.6V 的可充电锂电池,得确认模块电路允许,否则长期挂在 VCC1 上可能超过芯片绝对最大额定值。另外,如果你不需要掉电保存功能,VCC1 可以悬空不接,芯片不会因此损坏,只是断电后时间会归零。

有一个容易忽略的细节:焊接完芯片和电池座之后,最好先用万用表量一下 VCC1 对 GND 的电压,确认电池确实通过接触片连到了芯片上。我见过不少“时间总是丢”的案例,最后查出来是电池座虚焊,或者电池旁边的二极管焊反了。别嫌这一步啰嗦,硬件问题排查起来最费时间。

3. 驱动原理:三线时序与寄存器地图

3.1 命令字和寄存器地址

DS1302 的操作方式是通过发送一个命令字节来指定后续要读写哪个寄存器。命令字节的组成比较特殊:bit7 必须是 1,bit6 固定是 0,bit5 选择操作区域(0 是时钟寄存器,1 是 RAM),bit4 到 bit1 是寄存器地址,bit0 是读写标志,1 表示读,0 表示写。因为传输顺序是低位在前,所以你发一个 0x81,实际线上第一个 bit 是 1,表示读操作。

时间寄存器的地址表如下,注意同一组寄存器读和写的命令字就差最后一位:

寄存器功能写命令读命令BCD 格式说明
秒0x800x81bit7 是时钟暂停位,1 暂停
分0x820x83高 3 位十位,低 4 位个位
小时0x840x85bit7 是 12/24 小时制标志
日期0x860x871 到 31
月份0x880x891 到 12
星期0x8A0x8B1 到 7,可以自定义对应星期几
年份0x8C0x8D00 到 99,表示 2000 到 2099
写保护0x8E0x8Fbit7 为 1 时禁止写时间寄存器
涓流充电0x900x91控制充电开关、二极管和电阻

写保护寄存器是最容易被忽略的,它的 bit7 是 WP,当 WP 为 1 时,所有时间寄存器的写操作都会被忽略。所以正常流程是:先写 0x8E 把 WP 清 0,然后写时间,写完再把 WP 设回 1。这一步不是可选项,不关写保护你写进去的时间不会生效;不恢复写保护,后续如果有代码误操作,可能把时间改乱。

3.2 读写一字节的时序细节

三线时序的核心是 CE 拉高表示一次传输开始,然后 SCLK 提供时钟,I/O 线上按位传输数据。传完一个字节后,CE 拉低结束整次通信。每次传输的字节低位在前,这一点和很多习惯了 SPI 高位在前的朋友不太一样,写代码的时候要特别注意。

写字节的过程:先把 I/O 置为输出模式,然后从命令字节的最高位之前的“最低位”开始,依次把每一位放到 I/O 上,每放一位就产生一个 SCLK 上升沿,芯片在这个上升沿采样 I/O 电平。这里容易踩的坑是顺序问题——如果你按“先发高 4 位再发低 4 位”的习惯写,命令字就完全错乱了。我见过的现象是:读命令发的是 0x81,但芯片收到的实际是反过来的值,结果读出来的全是 0xFF 或者乱码。

读字节的过程略有不同:命令字节发完后,I/O 要切换成输入模式,然后每产生一个 SCLK 下降沿,芯片就在 I/O 上输出一位数据,主机在低电平期间或下降沿后延时采样。这里也要注意方向切换的时机,切换太早或太晚都会导致第一位数据读不到。下面这段是典型的读字节代码,注释里标注了每个步骤的执行顺序:

static uint8_t DS1302_ReadByte(void) { uint8_t i, dat = 0; DS1302_IO_IN(); /* 切换为输入模式 */ for (i = 0; i < 8; i++) { DS1302_SCLK_H; /* 拉高时钟 */ DS1302_DelayUS(2); DS1302_SCLK_L; /* 产生下降沿 */ DS1302_DelayUS(2); dat >>= 1; /* 先移位,最低位空出来 */ if (DS1302_IO_READ()) /* 低电平期间采样 */ { dat |= 0x80; } } return dat; }

这个函数里先拉高再拉低是为了产生一个下降沿,同时也保证 SCLK 有足够的最小高电平宽度。DS1302 对时序要求不算苛刻,但主频跑在 168MHz 的 STM32 上,不加延时直接翻转 GPIO,很容易超出芯片速度上限,表现就是数据随机跳变。所以我在每个边沿之后都加了大约 2 微秒的延时,宁慢勿快。读字节时数据位从最低位开始接收,每次右移把新读到的位放进最高位,循环 8 次后低位先收到的数据正好落在结果的最低位,不需要再翻转。

3.3 BCD 码转换

DS1302 的时间寄存器里存的是 BCD 码,不是普通十六进制,更不是十进制。比如秒是 35 秒,寄存器里存的是 0x35,高 4 位是十位的 3,低 4 位是个位的 5。如果你直接把读到的字节用 %d 打印出来,看到 35 会觉得好像没问题,但一旦读到 0x15,就会显示成 21——因为它会被当成十六进制 0x15 来解释成十进制 21。

所以读出来之后一定要做 BCD 到十进制的转换,写进去之前要做十进制到 BCD 的转换。转换公式很简单:

uint8_t BCDToDec(uint8_t bcd) { return (bcd >> 4) * 10 + (bcd & 0x0F); } uint8_t DecToBCD(uint8_t dec) { return ((dec / 10) << 4) | (dec % 10); }

这里还有一个特殊位要处理:秒寄存器的 bit7 是时钟暂停标志 CH。如果你想通过往秒寄存器写 0 来让时钟跑起来,就不要把 CH 位置 1。如果发现写进去时间之后秒不走,先查一下是不是秒寄存器最高位被写成了 1。小时寄存器也有类似坑,bit7 是 12/24 小时制选择,建议固定使用 24 小时制,也就是 bit7 置 0,避免出现下午时间读出来变成 9 这种混乱情况。

4. 完整驱动代码实现

4.1 底层 GPIO 封装

这次代码把硬件相关的部分全部封装在头文件里,你在自己的板子上用只需要改宏定义。以 HAL 库为例,CE、SCLK、I/O 三个引脚分别定义一组宏:

#define DS1302_GPIO_PORT GPIOC #define DS1302_CE_PIN GPIO_PIN_11 #define DS1302_SCLK_PIN GPIO_PIN_10 #define DS1302_IO_PIN GPIO_PIN_12 #define DS1302_CE_H() HAL_GPIO_WritePin(DS1302_GPIO_PORT, DS1302_CE_PIN, GPIO_PIN_SET) #define DS1302_CE_L() HAL_GPIO_WritePin(DS1302_GPIO_PORT, DS1302_CE_PIN, GPIO_PIN_RESET) #define DS1302_SCLK_H() HAL_GPIO_WritePin(DS1302_GPIO_PORT, DS1302_SCLK_PIN, GPIO_PIN_SET) #define DS1302_SCLK_L() HAL_GPIO_WritePin(DS1302_GPIO_PORT, DS1302_SCLK_PIN, GPIO_PIN_RESET) #define DS1302_IO_H() HAL_GPIO_WritePin(DS1302_GPIO_PORT, DS1302_IO_PIN, GPIO_PIN_SET) #define DS1302_IO_L() HAL_GPIO_WritePin(DS1302_GPIO_PORT, DS1302_IO_PIN, GPIO_PIN_RESET) #define DS1302_IO_READ() HAL_GPIO_ReadPin(DS1302_GPIO_PORT, DS1302_IO_PIN)

GPIO 的初始化里,CE 和 SCLK 直接配置成推挽输出,初始状态都拉低;I/O 引脚先配置成开漏输出或者推挽输出都可以,因为代码里每次读写前都会切换模式。如果你用的是标准库,把 HAL_GPIO_WritePin 换成 GPIO_WriteBit 就行,对应的初始化函数也要换成库函数,核心逻辑完全不用动。

切换 I/O 输入输出方向的函数如下,以 HAL 库为例:

static void DS1302_IO_OUT(void) { GPIO_InitTypeDef gpio; gpio.Pin = DS1302_IO_PIN; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DS1302_GPIO_PORT, &gpio); } static void DS1302_IO_IN(void) { GPIO_InitTypeDef gpio; gpio.Pin = DS1302_IO_PIN; gpio.Mode = GPIO_MODE_INPUT; gpio.Pull = GPIO_PULLUP; HAL_GPIO_Init(DS1302_GPIO_PORT, &gpio); }

有人会担心每次读写都调用 GPIO 初始化会不会影响性能,实测完全不会,一次时间读取最多切换一两次方向,开销可以忽略不计。比起频繁切换,更重要的是不要在通信过程中反复初始化整个 GPIO 端口,否则容易把其他引脚配置冲掉。

4.2 核心函数:读写一个字节

先把最小的传输单元写好。写字节函数负责把命令字或数据按 LSB first 的顺序发出去,读字节函数负责在命令字发完后接收 8 位数据。注意读写命令是由上层函数控制的,这里只做纯粹的字节传输:

static void DS1302_WriteByte(uint8_t dat) { uint8_t i; DS1302_IO_OUT(); for (i = 0; i < 8; i++) { if (dat & 0x01) { DS1302_IO_H(); } else { DS1302_IO_L(); } DS1302_SCLK_H(); DS1302_DelayUS(2); DS1302_SCLK_L(); DS1302_DelayUS(2); dat >>= 1; } }

写字节的过程就是“摆好数据、拉高时钟、拉低时钟”的循环,8 次之后正好发完一个字节。这里要留意的是拉高 SCLK 之前,I/O 上的数据必须已经稳定。如果先用 SCLK_H 再去设置 I/O,芯片采样到的就是上一个状态的残留电平,数据就错了。

单字节传输的封装在这里:

static void DS1302_SendByte(uint8_t dat) { DS1302_CE_H(); DS1302_WriteByte(dat); DS1302_DelayUS(2); DS1302_CE_L(); }

虽然名字叫 SendByte,但它既用于发命令字,也用于发数据。CE 拉高之后,芯片开始监听 SCLK,字节传输完成后立刻拉低 CE,让芯片知道本次操作结束。这个顺序不能反,也不能省略,否则芯片会把下一次不相关的电平变化当成同一帧数据来处理。

4.3 时间读取与设置

在底层字节函数基础上,读写时间就很简单了。先定义一个时间结构体:

typedef struct { uint8_t year; /* 00-99 */ uint8_t month; /* 01-12 */ uint8_t date; /* 01-31 */ uint8_t week; /* 1-7 */ uint8_t hour; /* 0-23 */ uint8_t minute; /* 0-59 */ uint8_t second; /* 0-59 */ } DS1302_Time;

读取时间的函数,我习惯把“连续读两次”写进去。为什么这么做?因为 RTC 的秒寄存器在老跳到 0 的边界时会变化,如果你先读了秒、再读分钟,而秒正好在 59 跳到 00,那读出来的分钟可能已经进位了,秒却是 0,前后数据不自洽。多读一次,如果两次的秒值一致,就采用第二次结果,否则重来:

uint8_t DS1302_GetTime(DS1302_Time *tm) { DS1302_Time buf; uint8_t i; for (i = 0; i < 3; i++) { DS1302_ReadAll(&buf); DS1302_ReadAll(&buf); /* 简单校验:两次读到的秒一致才认为数据稳定 */ if (buf.second == *((uint8_t *)&buf + 0)) { *tm = buf; return 1; } } return 0; }

上面例子中的比较写得太粗糙,实际代码不要这么干。更可靠的做法是在 ReadAll 里按“秒、分、时、日、月、星期、年”的顺序连续读,然后整体比较两次结果里的秒字段。如果你偷懒只想读一次,大概率也能用,但跨秒边界读到不一致数据的概率在你做长时间测试时会冒出来,与其到时候排查,不如一次把读取函数写稳。

设置时间的函数要处理写保护。完整流程是:先把写保护寄存器 WP 清零,然后按地址依次写入年、月、日、星期、时、分、秒,最后再把 WP 设回 1。注意写入顺序没有强制要求,但秒寄存器包含 CH 暂停位,建议最后写秒,让时间在秒写入那一刻才生效:

uint8_t DS1302_SetTime(DS1302_Time *tm) { /* 关闭写保护 */ DS1302_CE_H(); DS1302_WriteByte(0x8E); /* 写保护寄存器写命令 */ DS1302_WriteByte(0x00); DS1302_CE_L(); /* 写入各个寄存器,数据全部转成 BCD 码 */ DS1302_WriteRegister(0x8C, DecToBCD(tm->year)); DS1302_WriteRegister(0x8A, DecToBCD(tm->week)); DS1302_WriteRegister(0x88, DecToBCD(tm->month)); DS1302_WriteRegister(0x86, DecToBCD(tm->date)); DS1302_WriteRegister(0x84, DecToBCD(tm->hour)); DS1302_WriteRegister(0x82, DecToBCD(tm->minute)); DS1302_WriteRegister(0x80, DecToBCD(tm->second)); /* 开启时钟运行 */ /* 恢复写保护 */ DS1302_CE_H(); DS1302_WriteByte(0x8E); DS1302_WriteByte(0x80); DS1302_CE_L(); return 1; }

时间寄存器里的年份只有两位,比如 25 代表 2025 年,所以程序里转换时不要跟当前世纪的“20”搞混。你可以在上层用一个全局变量记录世纪前缀,显示的时候拼成 2025-01-01 这种格式。

4.4 调通后的扩展思路

基础读写调通之后,DS1302 还有两处值得玩的地方。一个是它的 31 字节 RAM。这部分寄存器的地址从 0xC0 开始,读写命令和时钟寄存器规律一样,读命令为 0xC1 等。往 RAM 里写数据不需要关闭写保护,但要注意 RAM 地址范围别越界,我之前就是没仔细看手册,往 31 字节之外的地址写数据,结果数据神秘消失。

另一个是涓流充电。0x90 写命令对应的寄存器,控制着备用电池的充电开关。TCS 位必须写成 0b1010 才能开启充电,之后可以选择一个二极管和一个电阻的组合。我建议新手不要乱开涓流充电,除非你确定装的是可充电的纽扣电池,否则一直给普通 CR2032 充电,轻则电池鼓包,重则漏液腐蚀电路板。大多数应用场景直接用一次性纽扣电池就够了,掉电保存时间已经足够长。

5. 联调实录与问题排查手册

5.1 我第一次调通踩过的坑

第一次调这块芯片的时候,我犯过一个特别低级的错误:忘了把 I/O 线在读写之间切方向,结果读出来的数据永远是 0xFF。当时以为是芯片坏了,换了一个还是 0xFF,后来用逻辑分析仪看波形才发现,命令字发完之后,I/O 引脚还是推挽输出状态,直接把芯片要输出的数据位“顶”住了。切换成输入模式之后,数据立刻正常。

第二个坑是延时不加不行。STM32 的 GPIO 翻转速度很快,我刚开始写了一个不延时的高频版本,在 72MHz 主频下读秒寄存器,10 次里有 3 次读出来是 0x00 或者 0x50 这类明显不对的值。加了两微秒延时之后,连续读一万次都没有错。这个现象很难直接猜到,如果之前没接触过 I2C 模拟,可能还会以为是寄存器地址写错了。

第三个坑和晶振有关。用万能板飞线焊接后,时间能读能写,但秒寄存器根本不动,永远是 0x00。查了很久,最后发现是 X1 和 X2 引脚之间的飞线太长,而且跟旁边的信号线平行走了两厘米,寄生电容太大把振荡电路“压死”了。把晶振引脚直接焊在芯片旁边,秒立刻正常跳动。晶振摆放对一个 RTC 模块来说真的不能随便。

5.2 常见问题速查表

在调试现场,最有用的工具其实就三样:万用表、逻辑分析仪、串口打印。把常见问题和排查思路整理成表,遇到问题可以直接对着查。

问题现象可能原因排查方法
读出来的数据全是 0xFFI/O 没有切换成输入模式读命令发完后确认 IO 方向,用逻辑分析仪看 IO 电平
读出来的数据全是 0x00I/O 被外部拉低或短路断掉单片机,量 I/O 对地阻值;检查模块上的下拉电阻
时间能写,秒不走秒寄存器 CH 位被置 1写入秒之前检查 DecToBCD 是否把最高位置位
时间能读,写不进去写保护寄存器没清零写时间前先写 0x8E 并写入 0x00
断电后时间丢失备用电池没接或接触不良量 VCC1 对 GND 电压,确认电池座焊牢
时间走时明显偏快/慢晶振负载电容不当或芯片引脚分布电容大重新焊接晶振,缩短走线,必要时调整外部负载电容
读到的秒偶尔跳变GPIO 翻转太快,时序边沿宽度不足在两个 SCLK 边沿之间都增加 2 微秒以上延时
同一组地址读写逻辑相反命令字节 LSB first 顺序搞反检查写字节函数是不是先发 bit0,再发 bit7

还有一个人人都可能遇到的问题:程序复位或者重新下载固件的时候,CE 引脚如果处于高电平,SCLK 上的抖动可能让 DS1302 误以为你要写数据,把时间寄存器改乱。解决办法是在系统初始化里,第一时间把 CE 和 SCLK 都配置成推挽输出并拉低,不要让它们在高阻状态下浮空。

5.3 最后一点经验

这块芯片我陆陆续续用过好几次,最大的体会是“老芯片不等于简单芯片”,它的三线协议和数据格式里藏着不少容易翻车的细节。不过反过来说,正因为这些细节足够多,把它完整调通一遍之后,再去看其他 SPI 类、SD 卡类器件的时序文档,你会感觉轻松很多。

如果你拿到代码后调试一直不顺,我建议按下面的顺序推进:先读秒寄存器,因为秒是变化最快的,秒的 BCD 值能连续变化,说明晶振、CE、SCLK、I/O 四根线全部正常;再读分钟、小时,确认地址表没搞错;最后尝试写时间,验证写保护逻辑。这个顺序每次都能帮我很快定位问题出在硬件还是软件。调试的时候把串口打印间隔设成 1 秒一条,观察几次跳动,比盲目猜原因高效得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询