资讯动态

STM32驱动DS1302时钟芯片:GPIO模拟三线时序与BCD码转换实战

发布时间:2026/10/14 15:53:00 来源:尧图企业网站定制
1. 项目缘起与整体设计思路1.1 为什么还要折腾 DS1302 这颗“老古董”先说结论如果你手头有一个需要记录“年月日时分秒”的嵌入式项目又不想让主控一直通电DS1302 依然是一颗值得考虑的时钟芯片。它便宜、好买、资料多最关键的是——它自带纽扣电池切换电路主电源掉电后靠一颗 CR2032 就能继续走时好几年。我这次做这个项目的起因很朴素手头一个数据采集的小板子需要给每条采集记录打上时间戳。主控用的是 STM32F103C8T6最小系统板上正好留了一组空闲的 GPIO。本来想用主控内部的 RTC但发现两个问题一是 F103 的内部 RTC 精度实在一般不加外部晶振的话一天能飘出好几秒二是板子经常断电搬运每次上电都要重新对时太麻烦。于是翻出手头一盒 DS1302 模块决定把它驱动起来。DS1302 是 Maxim 公司现被收购推出的一款涓流充电时钟芯片通过三根线与主控通信CE片选/复位、I/O数据线、SCLK串行时钟。它内部有 31 字节的静态 RAM除了存时间还能顺便存一些掉电不能丢的小数据比如设备编号、校准系数。这一点在成本敏感的项目里很实用相当于白送了一小片 NVRAM。这个项目适合谁看如果你正在学 STM32 的 GPIO 模拟时序、想搞懂“单总线/三线制”器件怎么驱动、或者单纯需要一个靠谱的掉电走时方案那这篇笔记应该能帮到你。我会把时序原理、代码实现、踩过的坑都摊开讲代码可以直接拿去改。1.2 整体方案选型软件模拟时序 vs 硬件 SPIDS1302 的通信协议是自定义的三线串行协议不是标准 SPI也不是 I2C。STM32 的硬件 SPI 没法直接匹配它的时序DS1302 是低位先出、数据在时钟下降沿变化、上升沿采样所以最稳妥的方案就是用普通 GPIO 软件模拟时序。我对比过两种思路方案优点缺点适用场景硬件 SPI 外设速度快、CPU 占用低时序不匹配需要额外逻辑转换容易出错不推荐用于 DS1302GPIO 软件模拟时序完全可控、移植方便、代码直观占用 CPU 时间、需要精确延时绝大多数场景首选软件模拟的核心在于延时控制。DS1302 的时序对时间要求不算苛刻SCLK 最高频率约 2MHz也就是说半周期最小 250ns 左右。在 72MHz 的 STM32 上一个__NOP()大约 14ns所以插入几个空操作就能满足。但要注意如果你用了 FreeRTOS 或者开了中断延时可能被拉长这时候要适当留余量或者干脆在通信期间关中断。我最终选的是软件模拟理由很简单代码可读性强换到别的 MCU 上改改 GPIO 宏定义就能用不依赖特定外设。下面这张图是我画的整体连接示意文字描述版DS1302 的 CE 接 STM32 的 PB0DS1302 的 I/O 接 PB1DS1302 的 SCLK 接 PB2VCC2 接主电源 3.3VVCC1 接 CR2032 正极GND 共地注意DS1302 的工作电压是 2.0V~5.5VSTM32 的 GPIO 输出是 3.3V电平完全兼容不需要电平转换。但如果你用的是 5V 主控I/O 线上最好串一个 1kΩ 电阻保护一下。2. 核心细节解析与实操要点2.1 DS1302 寄存器地图命令字怎么拼驱动 DS1302 的第一步是搞懂它的“命令字”结构。每次通信主控先发一个字节的命令告诉芯片“我要读还是写、操作哪个寄存器”。这个字节的位定义如下位名称含义71固定为 1起始标志6RAM/CK1 表示操作 RAM0 表示操作时钟寄存器5~1A4~A0寄存器地址0RD/W1 表示读0 表示写举个例子要写“秒”寄存器地址 0x80命令字就是1000 0000 0x80要读“秒”就是1000 0001 0x81。要写 RAM 的第 0 字节地址 0xC0命令字是1100 0000 0xC0。时钟寄存器的地址分布是这样的十六进制0x80秒bit7 是 CH 位1 表示时钟停止0x82分0x84时bit7 是 12/24 小时制选择0x86日0x88月0x8A星期0x8C年0x8E写保护bit7 是 WP 位1 表示禁止写0x90涓流充电控制这些寄存器里存的是 BCD 码不是二进制。比如“23 分”存进去是 0x23不是 0x17。这一点新手特别容易搞混我第一次写的时候直接把十进制数塞进去读出来全是乱码。BCD 码的转换逻辑很简单一个字节的高 4 位是十位低 4 位是个位。比如 0x23 表示十进制的 23。转换函数我后面会给。2.2 三线时序的四个关键动作DS1302 的通信过程可以拆成四个基本动作所有读写都是这四个动作的组合启动CE 拉高SCLK 保持低电平。CE 上升沿之后芯片开始监听。写一个位先把 I/O 线设置成输出准备好数据然后把 SCLK 拉高再拉低。DS1302 在 SCLK 上升沿采样数据。读一个位先把 I/O 线设置成输入释放总线然后 SCLK 拉高再拉低在下降沿之后读取 I/O 线上的电平。停止CE 拉低通信结束。这里有个细节很多人会忽略写命令字和写数据之间CE 必须保持高电平。也就是说一次完整的“写寄存器”操作是CE 拉高 → 发 8 位命令 → 发 8 位数据 → CE 拉低。中间不能断开。读操作稍微复杂一点CE 拉高 → 发 8 位命令写方向→ 把 I/O 切为输入 → 读 8 位数据 → CE 拉低。注意命令字的最后一位是 1读标志但发送的时候仍然是按写的方式发出去。实操心得I/O 线的方向切换时机很关键。写命令字的时候是输出读完命令字最后一个位之后要立刻把 GPIO 切成输入模式否则 DS1302 输出的数据会和 STM32 的输出打架读出来全是 0xFF 或者 0x00。我在这上面卡了半个晚上。2.3 上电初始化CH 位和写保护DS1302 上电后秒寄存器的 bit7CH 位默认是 1表示时钟停止。所以初始化流程里必须先把 CH 位清零时钟才会开始走。标准初始化步骤关闭写保护向 0x8E 写 0x00。清除 CH 位读 0x80把 bit7 清零再写回 0x80。设置时间依次写秒、分、时、日、月、星期、年。打开写保护向 0x8E 写 0x80。写保护这个设计挺有意思它防止了意外写入。但很多人调试时忘了关写保护结果时间怎么都写不进去还以为是时序问题。我的建议是初始化时先关写保护设置完再打开后续如果要修改时间记得再关一次。涓流充电寄存器0x90一般用默认值就行。如果你用的是可充电的 LIR2032可以配置它给电池涓流充电如果用普通 CR2032千万别开充电否则电池会鼓包甚至漏液。3. 实操过程与核心代码实现3.1 硬件连接与 GPIO 配置先明确接线。我用的是 STM32F103C8T6 最小系统板DS1302 模块是市面上常见的那种带纽扣电池座的小板。接线如下DS1302 引脚STM32 引脚说明VCC23.3V主电源VCC1CR2032 正极备用电源GNDGND共地CEPB0片选/复位I/OPB1数据线SCLKPB2串行时钟GPIO 配置代码基于标准外设库HAL 库同理void DS1302_GPIO_Init(void) { GPIO_InitTypeDef GPIO_InitStructure; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOB, ENABLE); // CE 和 SCLK 推挽输出 GPIO_InitStructure.GPIO_Pin GPIO_Pin_0 | GPIO_Pin_2; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOB, GPIO_InitStructure); // I/O 先配置为推挽输出读的时候再切换 GPIO_InitStructure.GPIO_Pin GPIO_Pin_1; GPIO_InitStructure.GPIO_Mode GPIO_Mode_Out_PP; GPIO_Init(GPIOB, GPIO_InitStructure); // 初始状态CE 低SCLK 低 GPIO_ResetBits(GPIOB, GPIO_Pin_0); GPIO_ResetBits(GPIOB, GPIO_Pin_2); }I/O 线的方向切换我用宏定义封装方便阅读#define DS1302_IO_OUT() { GPIOB-CRL ~(0xF 4); GPIOB-CRL | (0x3 4); } #define DS1302_IO_IN() { GPIOB-CRL ~(0xF 4); GPIOB-CRL | (0x4 4); }这里直接操作 CRL 寄存器把 PB1 在推挽输出和浮空输入之间切换。用库函数也行但寄存器方式更快时序更稳。3.2 底层读写函数一个位一个位地抠先写最基础的字节写入函数。逻辑是低位先出循环 8 次每次把数据的最低位放到 I/O 线上然后给一个时钟脉冲。void DS1302_WriteByte(uint8_t dat) { uint8_t i; DS1302_IO_OUT(); for (i 0; i 8; i) { DS1302_IO dat 0x01; // 先放最低位 DS1302_SCLK 1; __NOP(); __NOP(); // 短延时保证建立时间 DS1302_SCLK 0; __NOP(); __NOP(); dat 1; } }读字节函数要注意方向切换的时机uint8_t DS1302_ReadByte(void) { uint8_t i, dat 0; DS1302_IO_IN(); // 先切成输入 for (i 0; i 8; i) { dat 1; if (DS1302_IO) dat | 0x80; // 读到的位放最高位 DS1302_SCLK 1; __NOP(); __NOP(); DS1302_SCLK 0; __NOP(); __NOP(); } DS1302_IO_OUT(); // 读完切回输出 return dat; }注意读的时候是“先读电平再给时钟”。因为 DS1302 在时钟下降沿之后才把下一位数据放到 I/O 线上所以要在拉低 SCLK 之后稍等片刻再读。我上面的代码是在循环开头读配合前面的下降沿延时实测稳定。有了这两个函数写寄存器和读寄存器就是套壳void DS1302_WriteReg(uint8_t cmd, uint8_t dat) { DS1302_CE 0; DS1302_SCLK 0; DS1302_CE 1; DS1302_WriteByte(cmd); DS1302_WriteByte(dat); DS1302_CE 0; } uint8_t DS1302_ReadReg(uint8_t cmd) { uint8_t dat; DS1302_CE 0; DS1302_SCLK 0; DS1302_CE 1; DS1302_WriteByte(cmd); dat DS1302_ReadByte(); DS1302_CE 0; return dat; }3.3 BCD 码转换与时间结构体前面说过DS1302 存的是 BCD 码。我定义了一个时间结构体方便上层调用typedef struct { uint8_t year; // 00-99 uint8_t month; // 1-12 uint8_t day; // 1-31 uint8_t week; // 1-7 uint8_t hour; // 0-23 uint8_t min; // 0-59 uint8_t sec; // 0-59 } DS1302_Time_t;BCD 和十进制互转的两个小函数uint8_t BCD2DEC(uint8_t bcd) { return (bcd 4) * 10 (bcd 0x0F); } uint8_t DEC2BCD(uint8_t dec) { return ((dec / 10) 4) | (dec % 10); }设置时间的函数void DS1302_SetTime(DS1302_Time_t *t) { DS1302_WriteReg(0x8E, 0x00); // 关写保护 DS1302_WriteReg(0x80, DEC2BCD(t-sec) 0x7F); // 清 CH 位 DS1302_WriteReg(0x82, DEC2BCD(t-min)); DS1302_WriteReg(0x84, DEC2BCD(t-hour)); DS1302_WriteReg(0x86, DEC2BCD(t-day)); DS1302_WriteReg(0x88, DEC2BCD(t-month)); DS1302_WriteReg(0x8A, DEC2BCD(t-week)); DS1302_WriteReg(0x8C, DEC2BCD(t-year)); DS1302_WriteReg(0x8E, 0x80); // 开写保护 }读取时间的函数void DS1302_GetTime(DS1302_Time_t *t) { t-sec BCD2DEC(DS1302_ReadReg(0x81) 0x7F); t-min BCD2DEC(DS1302_ReadReg(0x83)); t-hour BCD2DEC(DS1302_ReadReg(0x85) 0x3F); t-day BCD2DEC(DS1302_ReadReg(0x87)); t-month BCD2DEC(DS1302_ReadReg(0x89)); t-week BCD2DEC(DS1302_ReadReg(0x8B)); t-year BCD2DEC(DS1302_ReadReg(0x8D)); }读秒的时候要 0x7F把 CH 位屏蔽掉读小时的时候 0x3F屏蔽 12/24 小时制标志位。这些掩码操作看着不起眼但漏掉就会读出奇怪的值。3.4 主函数调用示例int main(void) { DS1302_Time_t now; DS1302_GPIO_Init(); // 设置初始时间2025年1月15日 星期三 14:30:00 now.year 25; now.month 1; now.day 15; now.week 3; now.hour 14; now.min 30; now.sec 0; DS1302_SetTime(now); while (1) { DS1302_GetTime(now); printf(20%02d-%02d-%02d %02d:%02d:%02d\r\n, now.year, now.month, now.day, now.hour, now.min, now.sec); Delay_ms(1000); } }串口打印出来应该是每秒刷新一次时间正常递增。如果发现秒数跳变异常比如从 09 直接跳到 10 没问题但从 09 跳到 0A 就是 BCD 转换没做对。4. 常见问题与排查技巧实录4.1 读出来全是 0xFF 或 0x00这是最典型的故障八成是 I/O 线方向切换的问题。排查顺序确认读数据前是否调用了DS1302_IO_IN()。如果 I/O 一直是输出模式DS1302 的数据根本传不进来。确认读完之后是否切回了输出。如果不切回下一次写命令字时 I/O 是输入状态命令发不出去。用示波器看 I/O 线波形。正常读操作时I/O 线上应该有 DS1302 驱动的方波。如果一直是低电平可能是芯片没供电或者 CE 没拉高。我遇到过一次读出来全是 0xFF查了半天发现是杜邦线接触不良I/O 线虚焊。所以硬件问题永远优先排查别一上来就怀疑代码。4.2 时间不走秒数不变如果读出来的秒数一直是同一个值先检查 CH 位。CH 位在秒寄存器的 bit7上电默认是 1表示时钟停止。初始化时写秒寄存器要确保 bit7 是 0。还有一种可能是写保护没关。写保护寄存器 0x8E 的 bit7 是 WP 位默认是 1。如果没写 0x00 关掉它所有写入操作都会被忽略但读操作正常。所以你会看到时间能读但设不进去。4.3 时间走得不准一天差好几秒DS1302 的精度取决于外接的 32.768kHz 晶振。市面上很多廉价模块配的晶振负载电容不匹配走时误差能到每天十几秒。解决办法换一个质量好点的 32.768kHz 晶振负载电容选 6pF 或 12.5pF 的。在晶振两端各接一个 6pF~12pF 的电容到地具体值要试。如果对精度要求高可以在软件里做校准记录一段时间内的误差然后定期修正。我实测过一个几毛钱的晶振一天快了 8 秒换了一个稍微贵点的一天误差在 2 秒以内。所以晶振别省。4.4 掉电后时间丢失掉电丢失说明备用电池没起作用。检查VCC1 是否接了纽扣电池正极GND 是否共地。电池电压是否正常CR2032 低于 2.0V 就要换了。涓流充电寄存器是否误开了充电。如果用的是不可充电的 CR2032开了充电会加速电池损耗。还有一个隐蔽的问题有些模块的 VCC1 和 VCC2 之间有一个二极管如果主电源掉电后二极管反向漏电电池会被慢慢耗光。这种情况只能换模块或者自己搭电路。4.5 常见问题速查表现象可能原因排查方法读出全 0xFFI/O 方向未切换检查读函数是否切输入读出全 0x00CE 未拉高或芯片未供电量 CE 和 VCC 电压时间不走CH 位为 1读秒寄存器看 bit7写不进去写保护未关向 0x8E 写 0x00走时不准晶振或负载电容问题换晶振、调电容掉电丢失电池未接或电压低量 VCC1 电压数据偶尔出错延时不足或中断干扰增加延时、关中断独家避坑技巧调试 DS1302 的时候我习惯先用一个最简单的“读秒寄存器并打印”的程序验证硬件通路。只要秒数能正常递增说明时序和硬件都没问题再去搞 BCD 转换和时间设置。这样能把问题范围缩小不至于一上来就陷入复杂的代码里。5. 进阶玩法与项目扩展5.1 用 DS1302 的 RAM 存配置参数DS1302 有 31 字节的 RAM地址从 0xC0 到 0xFC偶数地址。这些 RAM 和时钟寄存器一样掉电后由备用电池维持。我经常用它来存一些设备参数比如0xC0设备编号1 字节0xC2采集间隔2 字节0xC4校准系数2 字节读写 RAM 和读写时钟寄存器一模一样只是命令字不同。比如写 RAM 第 0 字节DS1302_WriteReg(0xC0, value)读就是DS1302_ReadReg(0xC1)。这样就不需要额外加 EEPROM 了省成本又省板子面积。但要注意RAM 的写入也要先关写保护。5.2 加一个超级电容做备用电源CR2032 虽然能用几年但换电池麻烦。如果板子空间允许可以用一个 0.1F 的超级电容代替纽扣电池。主电源在的时候通过一个二极管给超级电容充电主电源掉电后超级电容给 DS1302 供电。超级电容的优点是充放电循环次数多、不用换缺点是容量有限一般只能维持几天到几周。适合那种偶尔断电、但断电时间不长的场景。计算一下DS1302 的工作电流典型值是 300nA电池供电模式0.1F 电容从 3.3V 降到 2.0V 释放的电荷量是 0.1F × 1.3V 0.13C。按 300nA 算理论能撑 0.13 / 300e-9 ≈ 43 万秒大约 5 天。实际因为漏电流可能只有 2~3 天。所以超级电容适合短时间备电长时间还是得靠电池。5.3 时间戳与数据记录的结合我最常用的场景是STM32 采集传感器数据每条数据前面加上 DS1302 的时间戳然后存到 SD 卡或者通过串口发出去。这样后期分析数据的时候能精确知道每个数据点是什么时候采的。实现上可以在主循环里每秒读一次时间缓存到全局变量采集数据时直接取缓存的时间避免频繁读 DS1302 影响采集节奏。DS1302 的读操作虽然不慢但三线时序毕竟占用 CPU能少读就少读。5.4 移植到 HAL 库的注意事项如果你用的是 HAL 库GPIO 操作换成HAL_GPIO_WritePin和HAL_GPIO_ReadPin就行但要注意 HAL 库的函数调用开销比直接操作寄存器大。在 72MHz 下一次 HAL 调用可能要几百纳秒可能影响时序。我的建议是时序关键的写位、读位函数里直接用寄存器操作其他不关键的地方用 HAL 库。或者干脆全部用寄存器代码也不复杂。另外HAL 库的 GPIO 初始化结构体里输入模式要选GPIO_MODE_INPUT输出选GPIO_MODE_OUTPUT_PP。切换方向的时候重新调用HAL_GPIO_Init比较慢不如直接改寄存器。6. 我个人的调试体会这个 DS1302 的驱动我从开始写到稳定运行前前后后花了大概两个晚上。第一个晚上卡在 I/O 方向切换上读出来全是 0xFF一度以为是模块坏了。后来用示波器一看I/O 线在读的时候还是被 STM32 拉着才反应过来是方向没切。第二个晚上卡在 BCD 转换上写进去 23 分读出来是 0x17愣了半天才想起 BCD 码这回事。现在回头看DS1302 的驱动本身不复杂核心就是三线时序加 BCD 转换。但它特别适合用来练手因为你能完整地经历“看数据手册 → 写底层时序 → 调硬件 → 处理数据格式 → 上层应用”这个完整流程。搞懂这一颗芯片以后再遇到类似的单总线器件比如 DS18B20基本就是换个协议的事。最后分享一个小技巧如果你手头没有示波器可以用一个 LED 串在 I/O 线上加限流电阻通过观察 LED 的闪烁来判断有没有数据活动。虽然看不到具体波形但至少能确认线路上有信号比盲猜强。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑