STM32驱动DS1302实时时钟:GPIO模拟三线时序与掉电走时实战
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 接 PA4SCLK 接 PA5I/O 接 PA6这三个引脚随便选只要不是复用功能冲突的就行。VCC2 接 3.3VGND 接地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_Initvoid 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_WritePinGPIO_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 虽然老但胜在简单可靠对于不需要高精度、又想省成本的场景依然是个不错的选择。