1. 为什么还要折腾一颗"过时"的时钟芯片第一次接触 DS1302 是在做一个数据采集终端的时候。板子上的主控是 STM32F103,任务很简单:每隔十分钟采一次传感器数据,带上时间戳存进 SD 卡。当时第一反应是用 STM32 内部的 RTC,省事又省芯片。结果实测下来发现两个问题:一是主控断电后时间全丢,得重新对时;二是内部 RTC 走时精度实在一般,几天下来能差出十几秒,对于需要长期无人值守的设备来说不太能接受。后来翻资料翻到 DS1302,一颗老得不能再老的实时时钟芯片,DIP8 封装,价格便宜到可以忽略不计,自带 31 字节的 RAM,最关键的是它有一颗纽扣电池兜底,主电源断了它照样走时。虽然它用的是三线串行接口,通信协议得自己写,但正好适合拿来练手,把 GPIO 模拟时序这套东西吃透。这篇笔记就是把我从零驱动 DS1302 的整个过程整理出来。内容包括硬件怎么接、时序怎么模拟、寄存器怎么读写、BCD 码怎么转换、时间怎么设置和读取,以及我在调试过程中踩过的几个坑。适合刚上手 STM32、想找个外设练手的朋友,也适合手头有项目需要加个掉电走时功能、又不想用复杂方案的开发者。代码层面我会给出可直接移植的驱动框架,你拿到自己的工程里改改引脚定义就能用。需要提前说明的是,DS1302 的通信协议是厂商定义的私有三线协议,不是标准 SPI,也不是 I2C,所以不能直接套用 STM32 的硬件外设,必须用 GPIO 软件模拟。这一点是很多人一开始容易走弯路的地方,后面我会详细展开。2. 先搞清楚 DS1302 到底是个什么东西2.1 芯片内部结构与引脚功能DS1302 内部可以粗略分成三块:时钟/日历寄存器组、31 字节的静态 RAM、以及负责通信的移位寄存器逻辑。时钟部分提供秒、分、时、日、月、周、年七个时间寄存器,年份只到两位数,所以它有个"千年虫"式的设计——2000 年和 2100 年它分不出来,实际项目里得自己在软件层加个世纪位判断。引脚方面,DIP8 封装一共八条腿,真正需要关注的只有五条:引脚名称功能说明1VCC2主电源输入,接系统 3.3V2X132.768kHz 晶振一端3X232.768kHz 晶振另一端4GND地5CE片选/使能,高电平有效6I/O数据线,双向7SCLK串行时钟,由主控提供8VCC1备用电池输入这里有个细节值得说:VCC1 和 VCC2 是内部自动切换的,VCC2 电压高于 VCC1 时用主电源,主电源掉到阈值以下自动切到电池。所以电池那一路不需要任何外部切换电路,直接接一颗 CR2032 就行。我实测过,一颗普通纽扣电池在常温下能撑好几年,具体时长取决于晶振负载和芯片批次,不用太纠结。2.2 三线协议和标准 SPI 的本质区别很多人看到 CE、SCLK、I/O 三根线,第一反应是"这不就是 SPI 吗"。实际上差得远。标准 SPI 有独立的 MOSI 和 MISO,全双工;DS1302 只有一根双向 I/O 线,半双工,而且数据是在 SCLK 的上升沿还是下降沿采样、什么时候切换方向,都有严格规定。具体来说,DS1302 的通信规则是这样的:CE 拉高之后,命令字节从最低位开始,在每个 SCLK 上升沿被芯片采样;命令发完之后,如果是写操作,紧接着在上升沿写入数据;如果是读操作,芯片会在 SCLK 的下降沿把数据放到 I/O 线上,主控在上升沿读取。注意读的时候方向是反的,主控必须在读之前把 I/O 引脚从输出切成输入,否则会和芯片的输出打架。这个方向切换是新手最容易翻车的地方。我一开始没注意,读出来的数据全是 0xFF,查了半天才发现是引脚一直配成推挽输出,芯片根本没法把数据送回来。2.3 命令字节的位定义DS1302 的每一次操作都从一个命令字节开始,八位,从低位到高位依次是:bit0:读写选择,0 表示写,1 表示读bit1 到 bit5:寄存器地址,共 5 位,所以最多寻址 32 个寄存器bit6:固定为 0bit7:固定为 1举个例子,秒寄存器的地址是 0,写秒寄存器的命令字节就是 0x80,读秒寄存器就是 0x81。小时寄存器地址是 4,写命令 0x84,读命令 0x85。这个规律记住之后,所有寄存器的命令字节都能推出来。时钟寄存器的地址分配是这样的:秒 0、分 1、时 2、日 3、月 4、周 5、年 6,写保护寄存器地址是 7。RAM 从地址 0 开始,但命令字节的地址段和时钟寄存器是分开编码的,RAM 的写命令从 0xC0 开始,读命令从 0xC1 开始,一共 31 个字节。3. 硬件连接与上电前的准备工作3.1 最小系统接线方案我用的是一块常见的 STM32F103C8T6 最小系统板,DS1302 模块是市面上那种带纽扣电池座的小板子。接线不复杂,但有几个点要注意。CE 接 PA4,SCLK 接 PA5,I/O 接 PA6,这三个引脚随便选,只要不是复用功能冲突的就行。VCC2 接 3.3V,GND 接地,VCC1 接纽扣电池正极。晶振是模块自带的 32.768kHz 圆柱晶振,不用自己焊。这里有个坑:DS1302 的 I/O 引脚是开漏输出结构,理论上需要外接上拉电阻。市面上大多数模块已经自带了 10k 上拉,但如果你是自己画板子,一定要记得加上,否则读数据会不稳定。我第一版自己画的板子忘了加上拉,结果读出来的秒数偶尔跳变,加了 4.7k 上拉之后就稳了。3.2 上拉电阻和去耦电容的选择上拉电阻的取值有讲究。太大,上升沿变缓,高速通信时波形会塌;太小,灌电流大,增加功耗。DS1302 的 SCLK 频率最高能到 2MHz 左右,但实际用 GPIO 模拟的话,一般跑在几百 kHz 就够了。10k 上拉在 3.3V 系统下对应 0.33mA 的灌电流,完全够用。如果通信距离稍长或者线材质量差,可以降到 4.7k。去耦电容方面,VCC2 和 GND 之间建议并一颗 0.1uF 的陶瓷电容,位置尽量靠近芯片引脚。这个电容的作用是滤掉电源上的高频噪声,DS1302 对电源纹波比较敏感,电源不干净的时候走时精度会受影响。我实测过,不加去耦电容的情况下,晶振起振时间会变长,偶尔还会出现起振失败的情况。3.3 电池选型和寿命估算备用电池一般用 CR2032,标称容量 200mAh 左右。DS1302 在电池供电模式下的典型电流是几百 nA 级别,理论上算下来能撑十几年。但实际寿命受温度影响很大,高温环境下电池自放电会加快。如果设备工作在户外,建议选工业级的宽温纽扣电池,或者干脆用超级电容加充电电路。还有一点:电池座的正负极千万别接反。DS1302 内部虽然有保护,但反接时间长了照样会损坏。我见过有人把电池座焊反,上电之后芯片发烫,直接报废。4. GPIO 模拟时序的核心实现4.1 引脚宏定义与方向切换封装写驱动第一步是把引脚操作封装成宏,方便移植。我习惯这样定义:#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() GPIO_SetBits(DS1302_CE_PORT, DS1302_CE_PIN) #define DS1302_CE_LOW() GPIO_ResetBits(DS1302_CE_PORT, DS1302_CE_PIN) #define DS1302_SCLK_HIGH() GPIO_SetBits(DS1302_SCLK_PORT, DS1302_SCLK_PIN) #define DS1302_SCLK_LOW() GPIO_ResetBits(DS1302_SCLK_PORT, DS1302_SCLK_PIN) #define DS1302_IO_HIGH() GPIO_SetBits(DS1302_IO_PORT, DS1302_IO_PIN) #define DS1302_IO_LOW() GPIO_ResetBits(DS1302_IO_PORT, DS1302_IO_PIN) #define DS1302_IO_READ() GPIO_ReadInputDataBit(DS1302_IO_PORT, DS1302_IO_PIN)方向切换单独写两个函数,因为 STM32 的 GPIO 模式切换需要操作 CRL/CRH 寄存器,用库函数的话就是重新调用 GPIO_Init:void DS1302_IO_Output(void) { GPIO_InitTypeDef gpio; gpio.GPIO_Pin = DS1302_IO_PIN; gpio.GPIO_Mode = GPIO_Mode_Out_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(DS1302_IO_PORT, &gpio); } void DS1302_IO_Input(void) { GPIO_InitTypeDef gpio; gpio.GPIO_Pin = DS1302_IO_PIN; gpio.GPIO_Mode = GPIO_Mode_IPU; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(DS1302_IO_PORT, &gpio); }注意输入模式我配的是上拉输入(IPU),这样即使外部上拉电阻没焊,内部上拉也能顶一下,读数据更稳。4.2 字节写入的时序细节写一个字节的流程是:CE 拉高,然后循环八次,每次先把数据位放到 I/O 线上,再给一个 SCLK 上升沿,芯片在上升沿采样。注意顺序不能反,必须先建立数据再给时钟。void DS1302_WriteByte(uint8_t dat) { uint8_t i; DS1302_IO_Output(); for (i = 0; i < 8; i++) { if (dat & 0x01) DS1302_IO_HIGH(); else DS1302_IO_LOW(); DS1302_SCLK_HIGH(); DS1302_SCLK_LOW(); dat >>= 1; } }这里从最低位开始发,和命令字节的位序一致。SCLK 的高电平持续时间不用太长,几百纳秒就够,但为了保险,我在高低电平之间加了一小段延时,用空循环实现,大概一两微秒。这个延时不是必须的,但加上之后波形更干净,调试时用逻辑分析仪看会舒服很多。4.3 字节读取与方向切换时机读字节和写字节是对称的,区别在于读之前要把 I/O 切成输入,然后在 SCLK 下降沿之后读数据:uint8_t DS1302_ReadByte(void) { uint8_t i, dat = 0; DS1302_IO_Input(); for (i = 0; i < 8; i++) { dat >>= 1; if (DS1302_IO_READ()) dat |= 0x80; DS1302_SCLK_HIGH(); DS1302_SCLK_LOW(); } DS1302_IO_Output(); return dat; }注意这里先读再给时钟,因为芯片是在下降沿把数据放出来的,主控在下一个上升沿之前读走就行。我一开始把读和时钟的顺序搞反了,读出来的全是 0,后来对着时序图一步步捋才改对。方向切换的时机也很关键。读操作结束后要立刻切回输出,否则下一次写操作时 I/O 线还处于输入状态,数据发不出去。这个切换放在 ReadByte 函数末尾是最省事的。5. 寄存器读写与时间数据格式转换5.1 写保护寄存器的解锁与加锁DS1302 上电之后,写保护寄存器(地址 7)的 bit7 默认是 1,表示禁止写入。这时候你往秒寄存器写数据是写不进去的,读出来还是原来的值。所以任何写操作之前,必须先解锁:void DS1302_WriteProtect(uint8_t enable) { DS1302_CE_LOW(); DS1302_SCLK_LOW(); DS1302_CE_HIGH(); DS1302_WriteByte(0x8E); // 写保护寄存器写命令 DS1302_WriteByte(enable ? 0x80 : 0x00); DS1302_CE_LOW(); }enable 为 0 时解锁,为 1 时加锁。我习惯在初始化的时候解锁,设置完时间之后再锁上,防止程序跑飞误写时间寄存器。5.2 单字节读写函数的封装有了底层的字节读写,单寄存器操作就是两层封装:void DS1302_WriteReg(uint8_t cmd, uint8_t dat) { DS1302_CE_LOW(); DS1302_SCLK_LOW(); DS1302_CE_HIGH(); DS1302_WriteByte(cmd); DS1302_WriteByte(dat); DS1302_CE_LOW(); } uint8_t DS1302_ReadReg(uint8_t cmd) { uint8_t dat; DS1302_CE_LOW(); DS1302_SCLK_LOW(); DS1302_CE_HIGH(); DS1302_WriteByte(cmd); dat = DS1302_ReadByte(); DS1302_CE_LOW(); return dat; }注意每次操作前后都要把 CE 拉低再拉高,这是 DS1302 的通信帧边界。CE 拉低的时候芯片复位内部移位寄存器,准备接收下一个命令。5.3 BCD 码与十进制互转的坑DS1302 的时间寄存器用的是 BCD 码,不是二进制。比如秒寄存器里存的是 0x59,表示 59 秒,而不是二进制的 89。所以读写的时候必须转换。BCD 转十进制:高四位乘以 10 加上低四位。十进制转 BCD:除以 10 取商左移四位,加上余数。uint8_t BCD2DEC(uint8_t bcd) { return (bcd >> 4) * 10 + (bcd & 0x0F); } uint8_t DEC2BCD(uint8_t dec) { return ((dec / 10) << 4) | (dec % 10); }这里有个容易忽略的点:小时寄存器在 12 小时制模式下 bit7 是 AM/PM 标志,bit6 是 12/24 制选择。默认上电是 24 小时制,bit6 为 0。如果你不小心写了个 12 小时制的值进去,读出来的小时数会莫名其妙。我建议初始化的时候显式把小时寄存器的 bit7 清零,强制 24 小时制。5.4 时间设置与读取的完整流程设置时间的流程是:解锁写保护,依次写入秒、分、时、日、月、周、年,然后加锁。注意秒寄存器的 bit7 是时钟启动位(CH),为 1 时时钟停止,为 0 时走时。所以写秒的时候要把 bit7 清零。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_WriteProtect(0); DS1302_WriteReg(0x80, DEC2BCD(sec) & 0x7F); DS1302_WriteReg(0x82, DEC2BCD(min)); DS1302_WriteReg(0x84, DEC2BCD(hour) & 0x3F); DS1302_WriteReg(0x86, DEC2BCD(day)); DS1302_WriteReg(0x88, DEC2BCD(month)); DS1302_WriteReg(0x8A, DEC2BCD(week)); DS1302_WriteReg(0x8C, DEC2BCD(year)); DS1302_WriteProtect(1); }读取的时候反过来,把七个寄存器读出来转成十进制就行。注意读秒的时候要把 bit7 屏蔽掉,否则会读到一个很大的数。6. 调试过程中踩过的坑与排查方法6.1 读出来全是 0xFF 或 0x00这是最常见的问题,原因基本逃不出三个:一是 I/O 方向没切换,读的时候引脚还是输出模式;二是上拉电阻缺失,I/O 线悬空被读成高电平;三是 CE 或 SCLK 的初始电平不对,通信根本没建立起来。排查顺序建议这样:先用万用表量一下 I/O 线在空闲状态下的电平,正常应该是高电平(被上拉电阻拉高)。如果量出来是低电平或者浮空,先查上拉。然后用逻辑分析仪抓一下 CE、SCLK、I/O 三根线的波形,看命令字节有没有正确发出去。没有逻辑分析仪的话,可以用示波器看 SCLK 有没有波形,有波形说明 GPIO 配置没问题,问题在协议层。6.2 时间不走或者走时不准时间不走,先读秒寄存器的 bit7,如果是 1,说明时钟被停止了,写秒的时候把 bit7 清零就行。走时不准的话,大概率是晶振的问题。32.768kHz 晶振对负载电容很敏感,DS1302 内部有集成的负载电容,但精度一般。如果对精度要求高,可以换精度更好的晶振,或者在软件层做定期校准。还有一个容易被忽略的点:晶振的焊接质量。我遇到过一块板子,晶振引脚虚焊,结果时间偶尔走偶尔停,查了好久才发现是焊接问题。所以拿到新板子,先用放大镜检查一遍晶振和芯片引脚的焊接。6.3 掉电后时间丢失这个问题基本就是电池没接好或者电池没电了。先量一下 VCC1 引脚的电压,正常应该在 3V 左右。如果电压正常但掉电还是丢时间,检查一下电池座的弹片有没有接触不良。我见过一种情况是电池座的正极弹片氧化,接触电阻变大,主电源掉电后芯片供电不足,时间就丢了。用砂纸打磨一下弹片或者换个电池座就能解决。6.4 通信偶尔失败偶发性的通信失败,通常是时序余量不够。GPIO 模拟的时序受中断影响很大,如果通信过程中被高优先级中断打断,SCLK 的节奏就乱了。解决办法有两个:一是通信期间关中断,二是降低 SCLK 频率,给中断留出响应时间。我一般选择后者,把延时加大一点,牺牲一点速度换稳定性。另外,如果 I/O 线的走线比较长,建议在靠近 DS1302 的一端串一个 100 欧姆左右的电阻,抑制反射。这个技巧在通信距离超过 10cm 的时候特别有用。7. 驱动代码的移植与扩展思路7.1 移植到其他 STM32 型号这套驱动用的是标准外设库的 GPIO 操作,移植到 F4、F7 或者 L4 系列,只需要改 GPIO 初始化和引脚宏定义。如果用的是 HAL 库,把 GPIO_SetBits 换成 HAL_GPIO_WritePin,GPIO_ReadInputDataBit 换成 HAL_GPIO_ReadPin 就行,逻辑层完全不用动。移植的时候注意一点:不同系列的 GPIO 速度配置不一样,F1 有 2MHz、10MHz、50MHz 三档,F4 有 2MHz、25MHz、50MHz、100MHz 四档。DS1302 对速度要求不高,选最低档就行,波形反而更干净。7.2 增加闹钟和 RAM 读写功能DS1302 除了时钟寄存器,还有两个闹钟寄存器和 31 字节的 RAM。闹钟寄存器的地址是 0x8A 和 0x8C,但注意这两个地址和时钟寄存器里的周、年是复用的,需要通过写保护寄存器的 bit7 之外的位来切换。实际用起来比较绕,我一般不用硬件闹钟,直接在软件层比较时间。RAM 读写就简单多了,写命令从 0xC0 开始,读命令从 0xC1 开始,每个地址对应一个字节。我习惯把一些配置参数存在 RAM 里,比如设备 ID、校准偏移量之类的,掉电不丢,比外挂 EEPROM 省事。7.3 低功耗场景下的注意事项如果设备是电池供电的,DS1302 的功耗也要考虑。正常走时模式下电流很小,但通信的时候电流会瞬间拉高。如果主控大部分时间在休眠,建议把 CE、SCLK、I/O 三个引脚在空闲时配成模拟输入或者低电平输出,避免漏电流。还有一点:DS1302 的 VCC2 如果一直供电,芯片就一直工作在主电源模式,电池不耗电。如果 VCC2 也断电,芯片切到电池模式,这时候通信是没法进行的,因为主控也没电了。所以低功耗设计里,要么让主控定时唤醒读时间,要么用外部中断触发。8. 几个实测有效的经验技巧第一个技巧是关于延时的。GPIO 模拟时序的时候,延时函数不要用系统滴答定时器,因为滴答定时器可能被其他模块占用。我一般用空循环,循环次数根据主频调,F103 在 72MHz 下循环十次大概一微秒左右。这个值不用太精确,DS1302 对时序的容忍度其实挺高的。第二个技巧是关于初始化的。上电之后不要急着读写时间,先等个几百毫秒让晶振稳定。我遇到过晶振起振慢的情况,上电立刻读时间会读到全 0。加个延时之后就没问题了。第三个技巧是关于调试的。如果没有逻辑分析仪,可以用两个 GPIO 做软件模拟的"示波器",一个引脚在 SCLK 上升沿翻转,另一个在下降沿翻转,然后用示波器看这两个引脚的波形,就能间接判断时序对不对。这个方法虽然土,但应急的时候很好用。第四个技巧是关于代码结构的。建议把底层时序、寄存器读写、时间转换分成三个文件,底层只负责 GPIO 操作,中间层负责协议,上层负责业务逻辑。这样以后换芯片或者换主控,只需要改底层,中间层和上层基本不用动。我在几个项目里都是这么分的,维护起来省心很多。最后说一个关于精度的实测数据。我用同一批 DS1302 模块做了对比测试,常温下连续走时一个月,误差在正负十几秒左右。这个精度对于大多数应用足够了,如果要求更高,可以考虑用 DS3231,内置温补晶振,精度能到分钟级每年。但 DS3231 用的是 I2C 接口,价格也贵一些,看具体需求取舍。这套驱动我后来用在了好几个项目里,包括数据记录仪、定时开关、还有一个小型的环境监测节点。代码一直在迭代,但核心的时序逻辑没怎么变过。DS1302 虽然老,但胜在简单可靠,对于不需要高精度、又想省成本的场景,依然是个不错的选择。
1. 为什么还要折腾一颗"过时"的时钟芯片第一次接触 DS1302 是在做一个数据采集终端的时候。板子上的主控是 STM32F103,任务很简单:每隔十分钟采一次传感器数据,带上时间戳存进 SD 卡。当时第一反应是用 STM32 内部的 RTC,省事又省芯片。结果实测下来发现两个问题:一是主控断电后时间全丢,得重新对时;二是内部 RTC 走时精度实在一般,几天下来能差出十几秒,对于需要长期无人值守的设备来说不太能接受。后来翻资料翻到 DS1302,一颗老得不能再老的实时时钟芯片,DIP8 封装,价格便宜到可以忽略不计,自带 31 字节的 RAM,最关键的是它有一颗纽扣电池兜底,主电源断了它照样走时。虽然它用的是三线串行接口,通信协议得自己写,但正好适合拿来练手,把 GPIO 模拟时序这套东西吃透。这篇笔记就是把我从零驱动 DS1302 的整个过程整理出来。内容包括硬件怎么接、时序怎么模拟、寄存器怎么读写、BCD 码怎么转换、时间怎么设置和读取,以及我在调试过程中踩过的几个坑。适合刚上手 STM32、想找个外设练手的朋友,也适合手头有项目需要加个掉电走时功能、又不想用复杂方案的开发者。代码层面我会给出可直接移植的驱动框架,你拿到自己的工程里改改引脚定义就能用。需要提前说明的是,DS1302 的通信协议是厂商定义的私有三线协议,不是标准 SPI,也不是 I2C,所以不能直接套用 STM32 的硬件外设,必须用 GPIO 软件模拟。这一点是很多人一开始容易走弯路的地方,后面我会详细展开。2. 先搞清楚 DS1302 到底是个什么东西2.1 芯片内部结构与引脚功能DS1302 内部可以粗略分成三块:时钟/日历寄存器组、31 字节的静态 RAM、以及负责通信的移位寄存器逻辑。时钟部分提供秒、分、时、日、月、周、年七个时间寄存器,年份只到两位数,所以它有个"千年虫"式的设计——2000 年和 2100 年它分不出来,实际项目里得自己在软件层加个世纪位判断。引脚方面,DIP8 封装一共八条腿,真正需要关注的只有五条:引脚名称功能说明1VCC2主电源输入,接系统 3.3V2X132.768kHz 晶振一端3X232.768kHz 晶振另一端4GND地5CE片选/使能,高电平有效6I/O数据线,双向7SCLK串行时钟,由主控提供8VCC1备用电池输入这里有个细节值得说:VCC1 和 VCC2 是内部自动切换的,VCC2 电压高于 VCC1 时用主电源,主电源掉到阈值以下自动切到电池。所以电池那一路不需要任何外部切换电路,直接接一颗 CR2032 就行。我实测过,一颗普通纽扣电池在常温下能撑好几年,具体时长取决于晶振负载和芯片批次,不用太纠结。2.2 三线协议和标准 SPI 的本质区别很多人看到 CE、SCLK、I/O 三根线,第一反应是"这不就是 SPI 吗"。实际上差得远。标准 SPI 有独立的 MOSI 和 MISO,全双工;DS1302 只有一根双向 I/O 线,半双工,而且数据是在 SCLK 的上升沿还是下降沿采样、什么时候切换方向,都有严格规定。具体来说,DS1302 的通信规则是这样的:CE 拉高之后,命令字节从最低位开始,在每个 SCLK 上升沿被芯片采样;命令发完之后,如果是写操作,紧接着在上升沿写入数据;如果是读操作,芯片会在 SCLK 的下降沿把数据放到 I/O 线上,主控在上升沿读取。注意读的时候方向是反的,主控必须在读之前把 I/O 引脚从输出切成输入,否则会和芯片的输出打架。这个方向切换是新手最容易翻车的地方。我一开始没注意,读出来的数据全是 0xFF,查了半天才发现是引脚一直配成推挽输出,芯片根本没法把数据送回来。2.3 命令字节的位定义DS1302 的每一次操作都从一个命令字节开始,八位,从低位到高位依次是:bit0:读写选择,0 表示写,1 表示读bit1 到 bit5:寄存器地址,共 5 位,所以最多寻址 32 个寄存器bit6:固定为 0bit7:固定为 1举个例子,秒寄存器的地址是 0,写秒寄存器的命令字节就是 0x80,读秒寄存器就是 0x81。小时寄存器地址是 4,写命令 0x84,读命令 0x85。这个规律记住之后,所有寄存器的命令字节都能推出来。时钟寄存器的地址分配是这样的:秒 0、分 1、时 2、日 3、月 4、周 5、年 6,写保护寄存器地址是 7。RAM 从地址 0 开始,但命令字节的地址段和时钟寄存器是分开编码的,RAM 的写命令从 0xC0 开始,读命令从 0xC1 开始,一共 31 个字节。3. 硬件连接与上电前的准备工作3.1 最小系统接线方案我用的是一块常见的 STM32F103C8T6 最小系统板,DS1302 模块是市面上那种带纽扣电池座的小板子。接线不复杂,但有几个点要注意。CE 接 PA4,SCLK 接 PA5,I/O 接 PA6,这三个引脚随便选,只要不是复用功能冲突的就行。VCC2 接 3.3V,GND 接地,VCC1 接纽扣电池正极。晶振是模块自带的 32.768kHz 圆柱晶振,不用自己焊。这里有个坑:DS1302 的 I/O 引脚是开漏输出结构,理论上需要外接上拉电阻。市面上大多数模块已经自带了 10k 上拉,但如果你是自己画板子,一定要记得加上,否则读数据会不稳定。我第一版自己画的板子忘了加上拉,结果读出来的秒数偶尔跳变,加了 4.7k 上拉之后就稳了。3.2 上拉电阻和去耦电容的选择上拉电阻的取值有讲究。太大,上升沿变缓,高速通信时波形会塌;太小,灌电流大,增加功耗。DS1302 的 SCLK 频率最高能到 2MHz 左右,但实际用 GPIO 模拟的话,一般跑在几百 kHz 就够了。10k 上拉在 3.3V 系统下对应 0.33mA 的灌电流,完全够用。如果通信距离稍长或者线材质量差,可以降到 4.7k。去耦电容方面,VCC2 和 GND 之间建议并一颗 0.1uF 的陶瓷电容,位置尽量靠近芯片引脚。这个电容的作用是滤掉电源上的高频噪声,DS1302 对电源纹波比较敏感,电源不干净的时候走时精度会受影响。我实测过,不加去耦电容的情况下,晶振起振时间会变长,偶尔还会出现起振失败的情况。3.3 电池选型和寿命估算备用电池一般用 CR2032,标称容量 200mAh 左右。DS1302 在电池供电模式下的典型电流是几百 nA 级别,理论上算下来能撑十几年。但实际寿命受温度影响很大,高温环境下电池自放电会加快。如果设备工作在户外,建议选工业级的宽温纽扣电池,或者干脆用超级电容加充电电路。还有一点:电池座的正负极千万别接反。DS1302 内部虽然有保护,但反接时间长了照样会损坏。我见过有人把电池座焊反,上电之后芯片发烫,直接报废。4. GPIO 模拟时序的核心实现4.1 引脚宏定义与方向切换封装写驱动第一步是把引脚操作封装成宏,方便移植。我习惯这样定义:#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() GPIO_SetBits(DS1302_CE_PORT, DS1302_CE_PIN) #define DS1302_CE_LOW() GPIO_ResetBits(DS1302_CE_PORT, DS1302_CE_PIN) #define DS1302_SCLK_HIGH() GPIO_SetBits(DS1302_SCLK_PORT, DS1302_SCLK_PIN) #define DS1302_SCLK_LOW() GPIO_ResetBits(DS1302_SCLK_PORT, DS1302_SCLK_PIN) #define DS1302_IO_HIGH() GPIO_SetBits(DS1302_IO_PORT, DS1302_IO_PIN) #define DS1302_IO_LOW() GPIO_ResetBits(DS1302_IO_PORT, DS1302_IO_PIN) #define DS1302_IO_READ() GPIO_ReadInputDataBit(DS1302_IO_PORT, DS1302_IO_PIN)方向切换单独写两个函数,因为 STM32 的 GPIO 模式切换需要操作 CRL/CRH 寄存器,用库函数的话就是重新调用 GPIO_Init:void DS1302_IO_Output(void) { GPIO_InitTypeDef gpio; gpio.GPIO_Pin = DS1302_IO_PIN; gpio.GPIO_Mode = GPIO_Mode_Out_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(DS1302_IO_PORT, &gpio); } void DS1302_IO_Input(void) { GPIO_InitTypeDef gpio; gpio.GPIO_Pin = DS1302_IO_PIN; gpio.GPIO_Mode = GPIO_Mode_IPU; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(DS1302_IO_PORT, &gpio); }注意输入模式我配的是上拉输入(IPU),这样即使外部上拉电阻没焊,内部上拉也能顶一下,读数据更稳。4.2 字节写入的时序细节写一个字节的流程是:CE 拉高,然后循环八次,每次先把数据位放到 I/O 线上,再给一个 SCLK 上升沿,芯片在上升沿采样。注意顺序不能反,必须先建立数据再给时钟。void DS1302_WriteByte(uint8_t dat) { uint8_t i; DS1302_IO_Output(); for (i = 0; i < 8; i++) { if (dat & 0x01) DS1302_IO_HIGH(); else DS1302_IO_LOW(); DS1302_SCLK_HIGH(); DS1302_SCLK_LOW(); dat >>= 1; } }这里从最低位开始发,和命令字节的位序一致。SCLK 的高电平持续时间不用太长,几百纳秒就够,但为了保险,我在高低电平之间加了一小段延时,用空循环实现,大概一两微秒。这个延时不是必须的,但加上之后波形更干净,调试时用逻辑分析仪看会舒服很多。4.3 字节读取与方向切换时机读字节和写字节是对称的,区别在于读之前要把 I/O 切成输入,然后在 SCLK 下降沿之后读数据:uint8_t DS1302_ReadByte(void) { uint8_t i, dat = 0; DS1302_IO_Input(); for (i = 0; i < 8; i++) { dat >>= 1; if (DS1302_IO_READ()) dat |= 0x80; DS1302_SCLK_HIGH(); DS1302_SCLK_LOW(); } DS1302_IO_Output(); return dat; }注意这里先读再给时钟,因为芯片是在下降沿把数据放出来的,主控在下一个上升沿之前读走就行。我一开始把读和时钟的顺序搞反了,读出来的全是 0,后来对着时序图一步步捋才改对。方向切换的时机也很关键。读操作结束后要立刻切回输出,否则下一次写操作时 I/O 线还处于输入状态,数据发不出去。这个切换放在 ReadByte 函数末尾是最省事的。5. 寄存器读写与时间数据格式转换5.1 写保护寄存器的解锁与加锁DS1302 上电之后,写保护寄存器(地址 7)的 bit7 默认是 1,表示禁止写入。这时候你往秒寄存器写数据是写不进去的,读出来还是原来的值。所以任何写操作之前,必须先解锁:void DS1302_WriteProtect(uint8_t enable) { DS1302_CE_LOW(); DS1302_SCLK_LOW(); DS1302_CE_HIGH(); DS1302_WriteByte(0x8E); // 写保护寄存器写命令 DS1302_WriteByte(enable ? 0x80 : 0x00); DS1302_CE_LOW(); }enable 为 0 时解锁,为 1 时加锁。我习惯在初始化的时候解锁,设置完时间之后再锁上,防止程序跑飞误写时间寄存器。5.2 单字节读写函数的封装有了底层的字节读写,单寄存器操作就是两层封装:void DS1302_WriteReg(uint8_t cmd, uint8_t dat) { DS1302_CE_LOW(); DS1302_SCLK_LOW(); DS1302_CE_HIGH(); DS1302_WriteByte(cmd); DS1302_WriteByte(dat); DS1302_CE_LOW(); } uint8_t DS1302_ReadReg(uint8_t cmd) { uint8_t dat; DS1302_CE_LOW(); DS1302_SCLK_LOW(); DS1302_CE_HIGH(); DS1302_WriteByte(cmd); dat = DS1302_ReadByte(); DS1302_CE_LOW(); return dat; }注意每次操作前后都要把 CE 拉低再拉高,这是 DS1302 的通信帧边界。CE 拉低的时候芯片复位内部移位寄存器,准备接收下一个命令。5.3 BCD 码与十进制互转的坑DS1302 的时间寄存器用的是 BCD 码,不是二进制。比如秒寄存器里存的是 0x59,表示 59 秒,而不是二进制的 89。所以读写的时候必须转换。BCD 转十进制:高四位乘以 10 加上低四位。十进制转 BCD:除以 10 取商左移四位,加上余数。uint8_t BCD2DEC(uint8_t bcd) { return (bcd >> 4) * 10 + (bcd & 0x0F); } uint8_t DEC2BCD(uint8_t dec) { return ((dec / 10) << 4) | (dec % 10); }这里有个容易忽略的点:小时寄存器在 12 小时制模式下 bit7 是 AM/PM 标志,bit6 是 12/24 制选择。默认上电是 24 小时制,bit6 为 0。如果你不小心写了个 12 小时制的值进去,读出来的小时数会莫名其妙。我建议初始化的时候显式把小时寄存器的 bit7 清零,强制 24 小时制。5.4 时间设置与读取的完整流程设置时间的流程是:解锁写保护,依次写入秒、分、时、日、月、周、年,然后加锁。注意秒寄存器的 bit7 是时钟启动位(CH),为 1 时时钟停止,为 0 时走时。所以写秒的时候要把 bit7 清零。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_WriteProtect(0); DS1302_WriteReg(0x80, DEC2BCD(sec) & 0x7F); DS1302_WriteReg(0x82, DEC2BCD(min)); DS1302_WriteReg(0x84, DEC2BCD(hour) & 0x3F); DS1302_WriteReg(0x86, DEC2BCD(day)); DS1302_WriteReg(0x88, DEC2BCD(month)); DS1302_WriteReg(0x8A, DEC2BCD(week)); DS1302_WriteReg(0x8C, DEC2BCD(year)); DS1302_WriteProtect(1); }读取的时候反过来,把七个寄存器读出来转成十进制就行。注意读秒的时候要把 bit7 屏蔽掉,否则会读到一个很大的数。6. 调试过程中踩过的坑与排查方法6.1 读出来全是 0xFF 或 0x00这是最常见的问题,原因基本逃不出三个:一是 I/O 方向没切换,读的时候引脚还是输出模式;二是上拉电阻缺失,I/O 线悬空被读成高电平;三是 CE 或 SCLK 的初始电平不对,通信根本没建立起来。排查顺序建议这样:先用万用表量一下 I/O 线在空闲状态下的电平,正常应该是高电平(被上拉电阻拉高)。如果量出来是低电平或者浮空,先查上拉。然后用逻辑分析仪抓一下 CE、SCLK、I/O 三根线的波形,看命令字节有没有正确发出去。没有逻辑分析仪的话,可以用示波器看 SCLK 有没有波形,有波形说明 GPIO 配置没问题,问题在协议层。6.2 时间不走或者走时不准时间不走,先读秒寄存器的 bit7,如果是 1,说明时钟被停止了,写秒的时候把 bit7 清零就行。走时不准的话,大概率是晶振的问题。32.768kHz 晶振对负载电容很敏感,DS1302 内部有集成的负载电容,但精度一般。如果对精度要求高,可以换精度更好的晶振,或者在软件层做定期校准。还有一个容易被忽略的点:晶振的焊接质量。我遇到过一块板子,晶振引脚虚焊,结果时间偶尔走偶尔停,查了好久才发现是焊接问题。所以拿到新板子,先用放大镜检查一遍晶振和芯片引脚的焊接。6.3 掉电后时间丢失这个问题基本就是电池没接好或者电池没电了。先量一下 VCC1 引脚的电压,正常应该在 3V 左右。如果电压正常但掉电还是丢时间,检查一下电池座的弹片有没有接触不良。我见过一种情况是电池座的正极弹片氧化,接触电阻变大,主电源掉电后芯片供电不足,时间就丢了。用砂纸打磨一下弹片或者换个电池座就能解决。6.4 通信偶尔失败偶发性的通信失败,通常是时序余量不够。GPIO 模拟的时序受中断影响很大,如果通信过程中被高优先级中断打断,SCLK 的节奏就乱了。解决办法有两个:一是通信期间关中断,二是降低 SCLK 频率,给中断留出响应时间。我一般选择后者,把延时加大一点,牺牲一点速度换稳定性。另外,如果 I/O 线的走线比较长,建议在靠近 DS1302 的一端串一个 100 欧姆左右的电阻,抑制反射。这个技巧在通信距离超过 10cm 的时候特别有用。7. 驱动代码的移植与扩展思路7.1 移植到其他 STM32 型号这套驱动用的是标准外设库的 GPIO 操作,移植到 F4、F7 或者 L4 系列,只需要改 GPIO 初始化和引脚宏定义。如果用的是 HAL 库,把 GPIO_SetBits 换成 HAL_GPIO_WritePin,GPIO_ReadInputDataBit 换成 HAL_GPIO_ReadPin 就行,逻辑层完全不用动。移植的时候注意一点:不同系列的 GPIO 速度配置不一样,F1 有 2MHz、10MHz、50MHz 三档,F4 有 2MHz、25MHz、50MHz、100MHz 四档。DS1302 对速度要求不高,选最低档就行,波形反而更干净。7.2 增加闹钟和 RAM 读写功能DS1302 除了时钟寄存器,还有两个闹钟寄存器和 31 字节的 RAM。闹钟寄存器的地址是 0x8A 和 0x8C,但注意这两个地址和时钟寄存器里的周、年是复用的,需要通过写保护寄存器的 bit7 之外的位来切换。实际用起来比较绕,我一般不用硬件闹钟,直接在软件层比较时间。RAM 读写就简单多了,写命令从 0xC0 开始,读命令从 0xC1 开始,每个地址对应一个字节。我习惯把一些配置参数存在 RAM 里,比如设备 ID、校准偏移量之类的,掉电不丢,比外挂 EEPROM 省事。7.3 低功耗场景下的注意事项如果设备是电池供电的,DS1302 的功耗也要考虑。正常走时模式下电流很小,但通信的时候电流会瞬间拉高。如果主控大部分时间在休眠,建议把 CE、SCLK、I/O 三个引脚在空闲时配成模拟输入或者低电平输出,避免漏电流。还有一点:DS1302 的 VCC2 如果一直供电,芯片就一直工作在主电源模式,电池不耗电。如果 VCC2 也断电,芯片切到电池模式,这时候通信是没法进行的,因为主控也没电了。所以低功耗设计里,要么让主控定时唤醒读时间,要么用外部中断触发。8. 几个实测有效的经验技巧第一个技巧是关于延时的。GPIO 模拟时序的时候,延时函数不要用系统滴答定时器,因为滴答定时器可能被其他模块占用。我一般用空循环,循环次数根据主频调,F103 在 72MHz 下循环十次大概一微秒左右。这个值不用太精确,DS1302 对时序的容忍度其实挺高的。第二个技巧是关于初始化的。上电之后不要急着读写时间,先等个几百毫秒让晶振稳定。我遇到过晶振起振慢的情况,上电立刻读时间会读到全 0。加个延时之后就没问题了。第三个技巧是关于调试的。如果没有逻辑分析仪,可以用两个 GPIO 做软件模拟的"示波器",一个引脚在 SCLK 上升沿翻转,另一个在下降沿翻转,然后用示波器看这两个引脚的波形,就能间接判断时序对不对。这个方法虽然土,但应急的时候很好用。第四个技巧是关于代码结构的。建议把底层时序、寄存器读写、时间转换分成三个文件,底层只负责 GPIO 操作,中间层负责协议,上层负责业务逻辑。这样以后换芯片或者换主控,只需要改底层,中间层和上层基本不用动。我在几个项目里都是这么分的,维护起来省心很多。最后说一个关于精度的实测数据。我用同一批 DS1302 模块做了对比测试,常温下连续走时一个月,误差在正负十几秒左右。这个精度对于大多数应用足够了,如果要求更高,可以考虑用 DS3231,内置温补晶振,精度能到分钟级每年。但 DS3231 用的是 I2C 接口,价格也贵一些,看具体需求取舍。这套驱动我后来用在了好几个项目里,包括数据记录仪、定时开关、还有一个小型的环境监测节点。代码一直在迭代,但核心的时序逻辑没怎么变过。DS1302 虽然老,但胜在简单可靠,对于不需要高精度、又想省成本的场景,依然是个不错的选择。