1. 为什么还要折腾 DS1302 这颗老芯片
前阵子帮朋友做一个带定时开关的控制器,主控用的是 STM32F103C8T6 最小系统板,需求很简单:每天固定时间点触发继电器,断电重启后时间不能丢。第一反应肯定是上 DS3231,温补晶振、I2C 接口、精度高,但翻了下手头的元件盒,DS3231 模块用完了,倒是翻出来几颗当年买错的 DS1302 直插芯片和配套的 32.768kHz 晶振。想着反正精度要求不高,一天误差几秒也能接受,就干脆用 DS1302 把这事做了。
DS1302 这颗芯片,玩过 51 单片机的人基本都认识,属于那种"入门必点"的外设。它用的是三线串行接口,严格说不是标准 SPI,也不是 I2C,而是自己的一套时序协议:一根 CE(有的资料叫 RST)、一根 SCLK、一根双向 IO。正因为时序是自己定义的,所以移植到 STM32 上不能直接套用硬件 SPI 或 I2C 外设,得老老实实用 GPIO 模拟时序。这也是很多人第一次在 STM32 上驱动 DS1302 时踩坑的地方——习惯了 HAL 库一把梭,突然要自己数微秒,节奏就乱了。
这篇笔记就是把我从接线、写时序、调读写、处理 BCD 码到最终跑通定时功能的完整过程整理出来。内容偏向实操,代码基于 STM32 HAL 库,但时序逻辑是通用的,换成标准库或者别的 Cortex-M 芯片,改改 GPIO 操作就能用。适合两类人看:一类是刚接触 STM32、想找个简单外设练手时序操作的新手;另一类是手头正好有 DS1302 库存、想快速把它用起来的老玩家。我会把每个关键决策背后的原因讲清楚,比如为什么上拉电阻不能省、为什么写保护要先关、为什么读出来的秒要屏蔽最高位,这些细节文档里往往一笔带过,但实际调试时全是坑。
2. DS1302 到底是个什么东西,先把原理捋顺
2.1 三线接口的本质:不是 SPI 也不是 I2C
很多人看到 DS1302 三根线,第一反应是"这不就是 SPI 吗"。其实差得远。标准 SPI 有明确的 CPOL/CPHA 四种模式,主机在时钟边沿采样,从机在另一个边沿输出,而且 SPI 是主机驱动时钟、数据全双工。DS1302 的协议更原始:它把一次通信拆成"命令字节 + 数据字节"两个阶段,命令字节固定从 bit0 开始发,数据字节则是 bit0 先发还是 bit7 先发取决于读还是写。
具体来说,DS1302 的通信规则是这样的:CE 拉高之后,SCLK 上升沿把数据写进芯片,下降沿把数据从芯片读出来。注意这里有个容易搞混的点——写的时候是上升沿采样,读的时候是下降沿输出。所以你在写时序函数的时候,如果是写操作,先准备好 IO 电平,再拉高 SCLK;如果是读操作,先拉低 SCLK,再去读 IO 电平。这个顺序反了,读出来的数据就是乱的。
命令字节的格式也值得说一下。bit7 固定是 1,表示使能访问;bit6 是 RAM 还是 CLOCK 的选择,0 表示时钟寄存器,1 表示 RAM;bit1 到 bit5 是寄存器地址;bit0 是读写位,0 写 1 读。举个例子,要写秒寄存器(地址 0x80),命令字节就是 0x80;要读秒寄存器,命令字节就是 0x81。这个规律记住之后,后面所有寄存器的操作都能推出来。
2.2 寄存器地图:哪些能用,哪些别碰
DS1302 的时钟寄存器一共 8 个,地址从 0x80 到 0x8E,全是偶地址(写),读的时候地址加 1。我整理了一张表,把常用寄存器的位定义列出来,方便对照:
| 寄存器 | 写地址 | 读地址 | bit7 | bit6 | bit5 | bit4 | bit3 | bit2 | bit1 | bit0 |
|---|---|---|---|---|---|---|---|---|---|---|
| 秒 | 0x80 | 0x81 | CH | 10秒 | 秒个位 | |||||
| 分 | 0x82 | 0x83 | 0 | 10分 | 分个位 | |||||
| 时 | 0x84 | 0x85 | 0 | 12/24 | 10时/PM | 时个位 | ||||
| 日 | 0x86 | 0x87 | 0 | 0 | 0 | 10日 | 日个位 | |||
| 月 | 0x88 | 0x89 | 0 | 0 | 0 | 10月 | 月个位 | |||
| 周 | 0x8A | 0x8B | 0 | 0 | 0 | 0 | 0 | 周 | ||
| 年 | 0x8C | 0x8D | 10年 | 年个位 | ||||||
| 写保护 | 0x8E | 0x8F | WP | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
这里有几个关键位必须注意。秒寄存器的 bit7 是 CH 位,Clock Halt,上电默认是 1,意思是时钟停止。你如果只写了时间没清这个位,会发现时间根本不走,读出来永远是那个值。我第一次调的时候就栽在这,盯着串口打印的固定时间看了半天,以为是晶振没起振,换了两个晶振才发现是 CH 位没清。
写保护寄存器 0x8E 的 bit7 是 WP 位,上电默认是 1,表示写保护开启,这时候你往任何时钟寄存器写数据都会被忽略。所以标准流程是:先写 0x8E 写 0x00 关保护,改完时间再写 0x8E 写 0x80 开保护。这个顺序不能省,省了就会出现"写进去读出来还是旧值"的诡异现象。
2.3 备用电池和晶振:硬件上最容易翻车的两个点
DS1302 有个 VCC1 和 VCC2 双电源引脚,VCC1 接备用电池,VCC2 接主电源。芯片内部有个电源检测电路,主电源掉电时自动切到电池,保证时钟继续走。这里有个细节:电池电压要低于主电源电压,否则芯片可能判断不出主电源掉电,一直用电池供电,反而加速电池消耗。一般用 3V 的 CR2032 纽扣电池,主电源 3.3V,压差刚好合适。
晶振这块,DS1302 要求外接 32.768kHz 晶振,而且对负载电容有要求。市面上常见的圆柱形晶振,负载电容一般是 6pF 或 12.5pF,你得配对应的电容。我手头那批晶振是 6pF 的,一开始随手焊了两个 22pF 的电容上去,结果时间走得明显偏慢,一天能差十几秒。后来查了晶振规格书,换成两个 6pF 的电容,误差就降到一天两三秒了。如果你对精度有要求,建议直接上 DS3231,省得跟电容较劲。
另外,DS1302 的 IO 引脚是开漏输出,必须接上拉电阻,一般 4.7k 到 10k 都行。我习惯用 4.7k,上升沿更陡一点,时序更稳。有些模块板子上已经集成了上拉电阻,如果你是自己画板子,千万别忘了这三个电阻,否则读出来的数据全是 0xFF。
3. 硬件接线与工程搭建
3.1 接线方案:三根信号线怎么分配
我用的 STM32F103C8T6 最小系统板,DS1302 模块是那种带电池座的小板子。接线如下:
| DS1302 引脚 | STM32 引脚 | 说明 |
|---|---|---|
| VCC2 | 3.3V | 主电源 |
| VCC1 | CR2032 正极 | 备用电池 |
| GND | GND | 共地 |
| CE | PA4 | 片选/复位 |
| SCLK | PA5 | 串行时钟 |
| IO | PA6 | 双向数据 |
| X1/X2 | 32.768kHz 晶振 | 晶振引脚 |
这里 CE、SCLK、IO 我选了 PA4、PA5、PA6,其实随便选三个普通 GPIO 都行,只要不是那种有特殊功能的引脚(比如 JTAG 占用的 PA13/PA14/PA15)。选 PA4 到 PA6 纯粹是因为它们挨着,接线方便,而且不跟串口冲突,调试的时候能用串口打印时间。
IO 引脚要注意配置成开漏输出加外部上拉,或者配置成推挽输出但在读的时候切换成输入模式。我两种都试过,开漏加外部上拉更省事,不用来回切模式,代码也简洁。如果你板子上没有外部上拉,那就得在代码里动态切换输入输出方向,稍微麻烦一点。
3.2 工程配置:CubeMX 里怎么点
用 CubeMX 配置的话,PA4、PA5 配成 GPIO_Output,PA6 配成 GPIO_Output 开漏模式(Output Open Drain),上拉使能。时钟树按默认的 72MHz 配就行,DS1302 的时序对主频不敏感,因为我们是靠软件延时控制节奏的。
延时函数这块,我直接用 HAL 库的HAL_Delay()做毫秒级延时,微秒级延时用__NOP()循环凑。DS1302 的时序要求其实很宽松,SCLK 最高频率 2MHz,也就是周期 500ns,我们只要保证每个电平保持个一两微秒,就绝对安全。所以微秒延时不用太精确,用循环空转就行。
void ds1302_delay_us(uint16_t us) { while (us--) { __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); __NOP(); } }这个延时函数在 72MHz 下大概每个循环 1 微秒左右,不用精确到纳秒,因为 DS1302 对时序的容忍度很高。我实测过,就算延时翻倍或者减半,读写照样正常。
3.3 引脚宏定义:方便移植的写法
为了让代码好移植,我把引脚操作都封装成宏:
#define DS1302_CE_PORT GPIOA #define DS1302_CE_PIN GPIO_PIN_4 #define DS1302_SCLK_PORT GPIOA #define DS1302_SCLK_PIN GPIO_PIN_5 #define DS1302_IO_PORT GPIOA #define DS1302_IO_PIN GPIO_PIN_6 #define DS1302_CE_HIGH() HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_SET) #define DS1302_CE_LOW() HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_RESET) #define DS1302_SCLK_HIGH() HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_SET) #define DS1302_SCLK_LOW() HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_RESET) #define DS1302_IO_HIGH() HAL_GPIO_WritePin(DS1302_IO_PORT, DS1302_IO_PIN, GPIO_PIN_SET) #define DS1302_IO_LOW() HAL_GPIO_WritePin(DS1302_IO_PORT, DS1302_IO_PIN, GPIO_PIN_RESET) #define DS1302_IO_READ() HAL_GPIO_ReadPin(DS1302_IO_PORT, DS1302_IO_PIN)这样写的好处是,换芯片或者换引脚的时候,只改这几个宏就行,底层时序函数一行都不用动。我后来把这个驱动移植到另一块 GD32 的板子上,就花了五分钟改宏定义。
4. 时序代码逐行拆解
4.1 写一个字节:从 bit0 开始,上升沿有效
DS1302 写字节的规则是:从 bit0 开始,每次把数据放到 IO 上,然后拉高 SCLK,再拉低 SCLK,循环 8 次。注意命令字节和数据字节都是 bit0 先发,这点跟 SPI 的 MSB first 不一样,别搞混了。
void ds1302_write_byte(uint8_t dat) { uint8_t i; for (i = 0; i < 8; i++) { if (dat & 0x01) DS1302_IO_HIGH(); else DS1302_IO_LOW(); ds1302_delay_us(1); DS1302_SCLK_HIGH(); ds1302_delay_us(1); DS1302_SCLK_LOW(); ds1302_delay_us(1); dat >>= 1; } }这段代码里,dat & 0x01取最低位,然后右移,保证下一次取的是新的最低位。延时放在拉高 SCLK 之前和之后,是为了满足建立时间和保持时间。DS1302 的建立时间要求是 50ns,保持时间也是 50ns,我们延时 1 微秒,余量足够大。
4.2 读一个字节:下降沿之后采样
读字节的规则稍微绕一点:DS1302 在 SCLK 下降沿把数据输出到 IO 上,所以我们要先拉低 SCLK,等一会儿,再去读 IO 电平。注意读的时候也是从 bit0 开始,读到的第一位要放到变量的 bit0 位置。
uint8_t ds1302_read_byte(void) { uint8_t i, dat = 0; for (i = 0; i < 8; i++) { dat >>= 1; if (DS1302_IO_READ()) dat |= 0x80; DS1302_SCLK_HIGH(); ds1302_delay_us(1); DS1302_SCLK_LOW(); ds1302_delay_us(1); } return dat; }这里有个细节:dat >>= 1放在读之前,然后如果读到 1 就dat |= 0x80。这样第一次读到的位最终会落在 bit0,第二次落在 bit1,以此类推。如果你写成先读再移位,顺序就反了,读出来的数据是 bit 倒序的。我第一次写的时候就是先读后移,结果读出来的秒是 0x80 而不是 0x01,查了半天才发现是移位顺序错了。
4.3 写寄存器:命令字节加数据字节
写寄存器的流程是:CE 拉高,发命令字节,发数据字节,CE 拉低。CE 拉低之后,一次通信结束。注意 CE 拉高之后要先延时一小会儿再发第一个字节,给芯片一点准备时间。
void ds1302_write_reg(uint8_t cmd, uint8_t dat) { DS1302_CE_LOW(); DS1302_SCLK_LOW(); DS1302_CE_HIGH(); ds1302_delay_us(2); ds1302_write_byte(cmd); ds1302_write_byte(dat); DS1302_CE_LOW(); ds1302_delay_us(2); }CE 拉低的时候顺便把 SCLK 也拉低,这是个好习惯,保证下次通信从已知状态开始。有些资料说 CE 拉低之后 SCLK 要保持低电平,其实拉低 CE 之后芯片内部会复位时序,SCLK 什么状态无所谓,但统一拉低更规范。
4.4 读寄存器:注意命令字节的 bit0 要置 1
读寄存器的命令字节是写命令字节加 1,也就是 bit0 从 0 变 1。比如读秒寄存器,写命令是 0x80,读命令就是 0x81。读的时候,发完命令字节之后,IO 要切换成输入模式(如果用的是开漏加外部上拉,就不用切,直接读就行)。
uint8_t ds1302_read_reg(uint8_t cmd) { uint8_t dat; DS1302_CE_LOW(); DS1302_SCLK_LOW(); DS1302_CE_HIGH(); ds1302_delay_us(2); ds1302_write_byte(cmd); dat = ds1302_read_byte(); DS1302_CE_LOW(); ds1302_delay_us(2); return dat; }如果你用的是推挽输出模式,读之前要把 IO 引脚切成输入模式,读完之后再切回输出。我一开始用推挽,读出来的数据全是 0xFF,就是因为 IO 还在输出模式,芯片输出的数据和 MCU 输出的电平打架了。后来改成开漏加外部上拉,这个问题就没了。
5. BCD 码转换与时间读写
5.1 BCD 码:为什么 0x23 表示 23
DS1302 内部所有时间寄存器都是 BCD 码格式,也就是用 4 个二进制位表示一个十进制数字。比如秒寄存器读到 0x23,不是十进制的 35,而是表示 23 秒。高 4 位是十位,低 4 位是个位。这种格式在早期 RTC 芯片里很常见,因为用 BCD 码做十进制显示的时候不用做除法。
转换公式很简单:
uint8_t bcd_to_dec(uint8_t bcd) { return (bcd >> 4) * 10 + (bcd & 0x0F); } uint8_t dec_to_bcd(uint8_t dec) { return ((dec / 10) << 4) | (dec % 10); }比如 0x23 转十进制:高 4 位是 2,乘以 10 得 20,低 4 位是 3,加起来 23。反过来 23 转 BCD:23 除以 10 得 2,左移 4 位变成 0x20,23 模 10 得 3,或上去就是 0x23。
5.2 设置时间:先关写保护,再清 CH 位
设置时间的完整流程是这样的:先关写保护,然后依次写秒、分、时、日、月、周、年,最后开写保护。注意写秒的时候要把 CH 位清零,否则时钟不走。
void ds1302_set_time(uint8_t year, uint8_t month, uint8_t day, uint8_t week, uint8_t hour, uint8_t min, uint8_t sec) { ds1302_write_reg(0x8E, 0x00); // 关写保护 ds1302_write_reg(0x80, dec_to_bcd(sec) & 0x7F); // 秒,清CH位 ds1302_write_reg(0x82, dec_to_bcd(min)); // 分 ds1302_write_reg(0x84, dec_to_bcd(hour)); // 时,24小时制 ds1302_write_reg(0x86, dec_to_bcd(day)); // 日 ds1302_write_reg(0x88, dec_to_bcd(month)); // 月 ds1302_write_reg(0x8A, dec_to_bcd(week)); // 周 ds1302_write_reg(0x8C, dec_to_bcd(year)); // 年 ds1302_write_reg(0x8E, 0x80); // 开写保护 }这里dec_to_bcd(sec) & 0x7F是为了确保 CH 位是 0。如果你写的时间秒数是奇数,比如 23 秒,dec_to_bcd(23)得到 0x23,最高位本来就是 0,与 0x7F 之后还是 0x23。但如果之前 CH 位是 1,这个与操作就能把它清掉。
5.3 读取时间:秒寄存器要屏蔽 CH 位
读时间的时候,秒寄存器的最高位是 CH 位,不是时间数据,所以要屏蔽掉。其他寄存器直接转 BCD 就行。
void ds1302_get_time(uint8_t *year, uint8_t *month, uint8_t *day, uint8_t *week, uint8_t *hour, uint8_t *min, uint8_t *sec) { *sec = bcd_to_dec(ds1302_read_reg(0x81) & 0x7F); *min = bcd_to_dec(ds1302_read_reg(0x83)); *hour = bcd_to_dec(ds1302_read_reg(0x85) & 0x3F); *day = bcd_to_dec(ds1302_read_reg(0x87) & 0x3F); *month = bcd_to_dec(ds1302_read_reg(0x89) & 0x1F); *week = bcd_to_dec(ds1302_read_reg(0x8B) & 0x07); *year = bcd_to_dec(ds1302_read_reg(0x8D)); }小时寄存器我屏蔽了 bit6 和 bit7,因为 bit7 是 12/24 小时制选择位,bit6 在 12 小时制下是 AM/PM 标志。我们统一用 24 小时制,所以把高两位都屏蔽掉。日寄存器屏蔽 bit6 和 bit7,月寄存器屏蔽 bit5 到 bit7,周寄存器屏蔽 bit3 到 bit7,这些都是保留位,读出来可能是随机值,屏蔽掉更安全。
6. 调试过程中踩过的坑和排查方法
6.1 读出来全是 0xFF 或者 0x00
这是最常见的问题,基本可以锁定三个原因。第一,上拉电阻没接或者阻值太大,IO 线在空闲时处于浮空状态,读出来就是随机的。用万用表量一下 IO 线对 VCC 的电阻,正常应该是 4.7k 左右。第二,CE 引脚没有正确拉高,芯片根本没被选中。用示波器看 CE 波形,发命令的时候应该是高电平。第三,晶振没起振,芯片内部时钟停了,这时候读寄存器可能返回默认值。拿示波器探头碰一下晶振引脚,能看到 32.768kHz 的正弦波就说明起振了。
我遇到过一次读出来全是 0xFF,查了半天发现是 IO 引脚配置成了推挽输出,读的时候没切成输入,MCU 和 DS1302 在 IO 线上打架。改成开漏加外部上拉之后问题消失。
6.2 时间不走,读出来永远是设定值
这个基本就是 CH 位没清。DS1302 上电默认 CH=1,时钟停止。你写时间的时候如果只写了秒数据没清 CH 位,芯片就一直在暂停状态。解决办法就是在写秒寄存器的时候,确保最高位是 0。我习惯用& 0x7F强制清零,这样不管之前 CH 是什么状态,写完之后一定是走的。
还有一种可能是写保护没关。DS1302 上电默认 WP=1,所有写操作都被忽略。你写进去的数据实际上没生效,读出来还是旧值。标准流程是先写 0x8E 写 0x00,再写时间,最后写 0x8E 写 0x80。这个顺序不能乱。
6.3 时间走得快或者走得慢
如果一天误差在几秒以内,那是晶振精度问题,正常。DS1302 配普通晶振,一天差个三五秒很常见。如果一天差几十秒甚至几分钟,那就要检查负载电容了。晶振规格书上的负载电容值要跟实际焊接的电容匹配,6pF 的晶振配 6pF 的电容,12.5pF 的晶振配 12.5pF 的电容。电容大了走得慢,电容小了走得快。
另外,晶振的走线要尽量短,远离高频信号线。我有一次把晶振线布在了 SPI 时钟线旁边,时间走得乱七八糟,后来重新布线就好了。如果对精度要求高,建议直接换 DS3231,内置温补晶振,一年误差也就几分钟。
6.4 常见问题速查表
| 现象 | 可能原因 | 排查方法 | 解决办法 |
|---|---|---|---|
| 读出全 0xFF | IO 无上拉 | 量 IO 对 VCC 电阻 | 加 4.7k 上拉 |
| 读出全 0x00 | CE 未拉高 | 示波器看 CE 波形 | 检查 CE 宏定义 |
| 时间不走 | CH 位为 1 | 读秒寄存器看 bit7 | 写秒时 & 0x7F |
| 写入无效 | 写保护未关 | 读 0x8E 看 bit7 | 先写 0x00 关保护 |
| 时间偏差大 | 负载电容不匹配 | 查晶振规格书 | 换匹配电容 |
| 数据乱跳 | 时序太快 | 加大延时 | 延时加到 2us |
| 偶尔读错 | 电源不稳 | 量 VCC 纹波 | 加 0.1uF 去耦 |
7. 从驱动到应用:定时开关的完整实现
7.1 每秒读一次时间,比较触发条件
驱动调通之后,应用逻辑就简单了。我在主循环里每 500ms 读一次时间,然后跟设定的触发时间比较。如果时和分都匹配,就触发继电器,同时记录一个标志位,防止同一分钟内重复触发。
uint8_t last_min = 0xFF; while (1) { uint8_t year, month, day, week, hour, min, sec; ds1302_get_time(&year, &month, &day, &week, &hour, &min, &sec); if (min != last_min) { last_min = min; if (hour == target_hour && min == target_min) { relay_on(); HAL_Delay(1000); relay_off(); } } HAL_Delay(500); }这里用last_min做去重,保证一分钟只触发一次。如果你需要触发多个时间点,可以搞个数组,遍历比较。
7.2 掉电恢复:电池供电下的时间保持
DS1302 接上 CR2032 之后,主电源掉电时芯片自动切到电池,时钟继续走。我实测过,拔掉主电源放了一晚上,第二天上电读出来的时间还是准的,误差在正常范围内。注意电池正极接 VCC1,负极接 GND,别接反了。CR2032 的寿命大概两三年,如果发现掉电后时间丢失,先量一下电池电压,低于 2.5V 就该换了。
7.3 精度优化:软件补偿的思路
如果你不想换 DS3231,又想让 DS1302 准一点,可以在软件里做补偿。具体做法是:先让 DS1302 跑一天,记录误差,比如一天快 5 秒,那就在每天固定时间点把秒数减 5。这个补偿逻辑可以放在主循环里,每天执行一次。不过这种方法比较粗糙,适合对精度要求不高的场景。真要高精度,还是建议上温补晶振的方案。
8. 代码组织与移植建议
8.1 文件划分:驱动层和应用层分开
我习惯把 DS1302 的驱动单独放在ds1302.c和ds1302.h里,只暴露ds1302_init()、ds1302_set_time()、ds1302_get_time()三个接口。应用层的定时逻辑放在main.c里,这样换项目的时候驱动直接复制过去就能用。
头文件里只放函数声明和必要的宏,具体的引脚定义放在源文件里,或者用条件编译区分不同板子。我一般会在头文件里留一个DS1302_USE_OPEN_DRAIN的宏,用来切换开漏和推挽模式,方便适配不同硬件。
8.2 移植到其他平台:改三个地方就行
这套代码移植到别的平台,比如 GD32、ESP32 或者 51 单片机,只需要改三个地方:GPIO 操作宏、微秒延时函数、初始化函数里的 GPIO 配置。时序逻辑本身是纯软件模拟的,跟硬件平台无关。我把它移植到 GD32F103 上的时候,就改了宏定义和延时函数,编译下载直接就跑起来了。
如果你用的是 RTOS,注意 DS1302 的读写操作不要被打断,否则时序会乱。可以在读写前后关中断,或者用互斥锁保护。我一般是在ds1302_write_reg和ds1302_read_reg里关中断,操作完再开,这样最省事。
9. 一些零散但有用的经验
DS1302 的 IO 引脚在 CE 拉低之后会变成高阻态,这时候如果外部上拉电阻太大,IO 线上的电平可能会慢慢漂移。我试过用 10k 上拉,发现读数据偶尔会出错,换成 4.7k 之后就稳了。所以上拉电阻别省,4.7k 是个比较稳妥的值。
写命令字节的时候,bit7 必须是 1,这个位叫"使能位"。如果你不小心写成了 0,芯片会忽略整个命令。我一开始看资料没注意这个细节,命令字节直接写寄存器地址,结果怎么都不对。后来才发现地址要或上 0x80 才是完整的命令字节。
DS1302 的 RAM 寄存器(地址 0xC0 到 0xFD)可以当普通存储器用,掉电不丢数据(有电池的情况下)。我有时候会拿它存一些配置参数,比如触发时间、设备编号之类的,省得外挂 EEPROM。不过 RAM 容量只有 31 字节,存不了太多东西。
最后说一个调试技巧:如果你手头没有示波器,可以用 LED 来粗略判断时序。在ds1302_write_byte里每发一位就翻转一次 LED,如果 LED 闪烁频率均匀,说明时序在跑;如果 LED 常亮或者常灭,说明程序卡在某个循环里了。这个方法虽然土,但应急的时候挺好用。
这个 DS1302 的驱动我从第一次调通到现在,已经在好几个小项目里复用了,每次都是复制粘贴改引脚,十分钟搞定。虽然芯片老,但胜在便宜、好买、资料多,对于精度要求不高的场景,完全够用。如果你也在用这颗芯片,希望这篇笔记能帮你少走点弯路。