ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

STM32驱动DS1302时钟芯片:三线时序、BCD码与掉电走时实战

2026/9/20 6:52:55 拓冰建站 浏览量
STM32驱动DS1302时钟芯片:三线时序、BCD码与掉电走时实战 1. 为什么还要折腾 DS1302 这颗老时钟芯片STM32 驱动 DS1302 这个组合放在今天看确实有点“复古”。现在随便一颗 STM32 内部都带 RTC外挂一颗 DS3231 精度也甩 DS1302 几条街为什么还要专门写一篇 DS1302 的驱动笔记我自己的答案是它是理解“三线串行时序”最便宜、最直观的教具。DS1302 用的是类似 SPI 但又不是标准 SPI 的三线协议——一根 CE有的资料叫 RST、一根 SCLK、一根双向 IO靠严格的时序一位一位地读写。你把这颗芯片啃下来再去写 74HC595、写单总线传感器、写各种杂牌外设的时序心里就有底了。这篇笔记面向的是刚上手 STM32、想找一个“有实际功能、又不至于太复杂”的小项目练手的朋友。我会把 DS1302 的通信协议、STM32 端的 GPIO 操作、寄存器读写、BCD 码处理、掉电走时这些环节全部拆开讲清楚代码可以直接抄去用。同时我也会把踩过的坑、时序上容易翻车的地方、以及为什么这么设计的原因一并交代让你不只是“跑通”而是真的明白每一步在干什么。需要提前说明的是DS1302 本身精度一般走时误差一天几秒是常态所以它适合做学习项目、做对时间精度要求不高的场合。如果你要的是长期精准计时那应该考虑带温度补偿的方案。但作为学习三线时序的载体DS1302 依然是性价比极高的选择。2. 先搞清楚 DS1302 到底是个什么东西2.1 芯片内部结构与引脚功能DS1302 是一颗带涓流充电能力的实时时钟芯片能对秒、分、时、日、月、周、年进行计数并且能自动处理月末日期和闰年补偿补偿到 2100 年之前。它内部有 31 字节的静态 RAM可以拿来存一些掉电不能丢的小数据这一点很多人不知道其实挺实用。引脚方面常见的是 8 脚封装核心引脚就这几个引脚名功能说明VCC2主电源接系统 3.3V 或 5VVCC1备用电源接纽扣电池掉电时维持走时GND地共地CE片选/复位有的资料标 RST高电平有效SCLK串行时钟由主机提供I/O数据线双向读写共用这里有个关键点VCC2 和 VCC1 谁电压高芯片就用谁供电。正常工作时 VCC2系统电源电压高于电池芯片用系统电系统掉电后 VCC2 变低芯片自动切到 VCC1 电池继续走时。所以电池不是“充电宝”而是备用电源主电源在的时候它基本不耗电。2.2 三线协议的本质一位一位地搬DS1302 的通信协议说穿了就一句话CE 拉高开始一次传输然后每个 SCLK 上升沿写一位、下降沿读一位数据从最低位开始。注意这个“上升沿写、下降沿读”的方向是 DS1302 的硬性规定写反了数据就全乱。一次完整的读写分两个阶段先发一个命令字节Command Byte告诉芯片“我要读还是写、操作哪个寄存器”然后紧接着发或收数据字节。命令字节的位定义是这样的Bit 01 表示读0 表示写Bit 1~5寄存器地址Bit 6固定为 0Bit 7固定为 1举个例子要写秒寄存器地址 0x80命令字节就是 0x80要读秒寄存器命令字节就是 0x81。这个规律很整齐地址左移一位最低位补读写标志最高位补 1。2.3 为什么数据是 BCD 码而不是二进制DS1302 里存的时间数据是 BCD 码不是普通二进制。比如“23 秒”寄存器里存的是 0x23而不是十进制的 230x17。BCD 码的好处是每一位十进制数字单独占 4 个 bit人看起来直观芯片做十进制计数也方便。但对写代码的人来说就多了一步转换写之前要把二进制转 BCD读回来要把 BCD 转二进制。转换逻辑很简单BCD 转二进制就是(高四位 * 10) 低四位二进制转 BCD 就是((值 / 10) 4) | (值 % 10)。这两个宏我建议直接写成函数或者宏定义后面到处都要用。3. STM32 端 GPIO 该怎么配、怎么控3.1 三个引脚的方向管理是核心难点DS1302 的 I/O 是双向的这是整个驱动里最容易出错的地方。STM32 的 GPIO 要能在“输出模式”和“输入模式”之间来回切。我的做法是写数据时把 IO 配成推挽输出读数据时切成上拉输入或浮空输入。具体到寄存器操作用标准库的话就是改 CRL/CRH 里的 MODE 位用 HAL 库的话就是调用HAL_GPIO_Init重新配置。为了效率我一般直接操作寄存器因为一次读写要切换好几次方向函数调用开销虽然不大但寄存器操作更干脆。CE 和 SCLK 都是单向输出配成推挽输出即可速度不用太高2MHz 足够DS1302 的时序很慢跑太快反而容易出问题。3.2 微秒级延时是时序的命根子DS1302 对时序有明确要求比如 SCLK 高电平时间和低电平时间都有最小值。STM32 主频动辄 72MHz、168MHz一条指令就是纳秒级如果不加延时SCLK 翻转速度会远超 DS1302 能接受的范围结果就是读回来全是 0xFF 或者 0x00。所以必须有一个微秒级延时函数。我通常用 SysTick 或者简单的空循环来实现。下面是一个基于空循环的delay_us具体循环次数要根据你的主频实测调整void ds1302_delay_us(uint32_t us) { while (us--) { // 根据主频调整72MHz 下大约循环 8 次是 1us for (volatile uint8_t i 0; i 8; i); } }注意这个延时不需要特别精确DS1302 对时序的容忍度其实挺宽只要别快到离谱就行。我实测 1~2us 的延时非常稳。3.3 写一位和读一位的时序实现写一位的流程是先把 IO 设成输出把要写的位放到 IO 上然后 SCLK 拉高延时SCLK 拉低延时。注意数据要在 SCLK 上升沿之前就准备好因为 DS1302 是在上升沿采样。读一位的流程是先把 IO 设成输入SCLK 拉高延时读 IO 的电平SCLK 拉低延时。DS1302 在下降沿输出数据所以要在拉低之后读或者拉高之后读也行具体看时序图。我习惯在 SCLK 拉高后读实测没问题。void ds1302_write_byte(uint8_t dat) { for (uint8_t i 0; i 8; i) { DS1302_IO_OUT(); if (dat 0x01) DS1302_IO_HIGH(); else DS1302_IO_LOW(); DS1302_SCLK_HIGH(); ds1302_delay_us(1); DS1302_SCLK_LOW(); ds1302_delay_us(1); dat 1; } } uint8_t ds1302_read_byte(void) { uint8_t dat 0; for (uint8_t i 0; i 8; i) { dat 1; DS1302_IO_IN(); DS1302_SCLK_HIGH(); ds1302_delay_us(1); if (DS1302_IO_READ()) dat | 0x80; DS1302_SCLK_LOW(); ds1302_delay_us(1); } return dat; }这里有个细节读的时候是先右移再填最高位因为 DS1302 是低位先出第一次读到的位最终要落在 bit0所以循环里先移位再填高位八次之后正好归位。这个逻辑我一开始写反过读出来的时间全是乱的。4. 寄存器读写与时间数据的完整处理4.1 写寄存器和读寄存器的封装有了读写字节的基础函数寄存器操作就是“发命令 发/收数据”两步void ds1302_write_reg(uint8_t cmd, uint8_t dat) { DS1302_CE_LOW(); DS1302_SCLK_LOW(); DS1302_CE_HIGH(); ds1302_write_byte(cmd); ds1302_write_byte(dat); DS1302_CE_LOW(); } uint8_t ds1302_read_reg(uint8_t cmd) { uint8_t dat; DS1302_CE_LOW(); DS1302_SCLK_LOW(); DS1302_CE_HIGH(); ds1302_write_byte(cmd); dat ds1302_read_byte(); DS1302_CE_LOW(); return dat; }注意每次传输前要把 CE 拉低再拉高这是复位传输状态的标准动作。CE 拉低的时候 SCLK 也要拉低否则可能产生一个多余的时钟沿。4.2 写保护寄存器和时钟启动DS1302 有一个写保护寄存器地址 0x8E上电默认是写保护的也就是你不能直接改时间。要写时间之前必须先往 0x8E 写 0x00 解除保护写完时间后再写 0x80 重新上锁。这个设计是为了防止误写很合理但新手经常忘了这一步结果时间怎么都写不进去。另外秒寄存器的最高位bit7是时钟启动位CH为 1 时时钟停止为 0 时时钟运行。所以初始化的时候要确保这一位是 0否则时间不走。4.3 BCD 码转换与时间数组的组织我习惯用一个 7 字节的数组来存时间顺序是秒、分、时、日、月、周、年。读写的时候循环处理代码简洁uint8_t time_buf[7] {0}; void ds1302_set_time(void) { ds1302_write_reg(0x8E, 0x00); // 解除写保护 for (uint8_t i 0; i 7; i) { ds1302_write_reg(0x80 i * 2, (time_buf[i] / 10 4) | (time_buf[i] % 10)); } ds1302_write_reg(0x8E, 0x80); // 重新上锁 } void ds1302_get_time(void) { for (uint8_t i 0; i 7; i) { uint8_t tmp ds1302_read_reg(0x81 i * 2); time_buf[i] (tmp 4) * 10 (tmp 0x0F); } }这里0x80 i * 2就是写命令0x81 i * 2就是读命令因为地址每次加 2左移一位。这个规律让代码非常整齐。提示读秒寄存器的时候bit7 是 CH 位转换前最好先屏蔽掉否则算出来的秒会多 80。我一般读回来先tmp 0x7F。4.4 涓流充电寄存器的配置DS1302 地址 0x90 是涓流充电寄存器可以配置给备用电池充电。但这里要特别小心如果你用的是不可充电的纽扣电池比如 CR2032千万不要开充电否则电池会漏液甚至炸。我一般直接写 0x00 关闭充电用可充电的 LIR2032 才考虑开启。这个坑很多人踩过电池漏液腐蚀板子得不偿失。5. 实操过程与调试记录5.1 硬件连接与上电检查我的接线是这样的VCC2 接 3.3VVCC1 接一颗 CR2032GND 共地CE 接 PA0SCLK 接 PA1IO 接 PA2。上电后第一件事是用万用表量 VCC1 电压确认电池有电。然后量 CE、SCLK 的静态电平应该是低电平。如果读回来全是 0xFF先检查 IO 方向切换有没有问题如果全是 0x00检查 CE 有没有拉高、SCLK 有没有翻转。我遇到过 IO 一直配成输出导致读不回来的情况排查了半天。5.2 初始化流程与首次写时间初始化顺序我固定为配置 GPIO → 关闭写保护 → 关闭涓流充电 → 写入初始时间 → 开启写保护 → 启动时钟。首次写时间的时候我建议先写一个明显的时间比如 2024 年 1 月 1 日 12:00:00然后读回来对比确认读写都正常。void ds1302_init(void) { // GPIO 初始化略 ds1302_write_reg(0x8E, 0x00); ds1302_write_reg(0x90, 0x00); // 关闭涓流充电 time_buf[0] 0; // 秒 time_buf[1] 0; // 分 time_buf[2] 12; // 时 time_buf[3] 1; // 日 time_buf[4] 1; // 月 time_buf[5] 1; // 周 time_buf[6] 24; // 年 ds1302_set_time(); ds1302_write_reg(0x8E, 0x80); }5.3 串口打印验证走时为了验证走时我把读回来的时间通过串口打印出来每秒打印一次。观察几分钟看秒是不是正常递增分和时有没有进位。如果秒跳变正常但分不动可能是分寄存器写错了如果秒一直不变检查 CH 位。我实测下来DS1302 走时一天大概差 2~5 秒这个误差是正常的跟晶振精度和环境温度有关。如果你要更准可以换 6pF 负载电容的晶振或者做软件校准。5.4 掉电走时测试拔掉主电源等几分钟再上电读时间看有没有继续走。如果时间归零了说明备用电池没起作用检查 VCC1 接线和电池电压。如果时间走了但走得很慢可能是电池电压不够。这个测试是 DS1302 的核心价值所在一定要做。6. 常见问题与排查技巧实录6.1 读回来全是 0xFF 或 0x00这是最常见的问题原因通常有三个一是 IO 方向没切换读的时候还是输出模式二是延时太短SCLK 翻转太快三是 CE 时序不对没有正确拉高拉低。排查顺序建议先查方向再查延时最后查 CE。6.2 时间写不进去九成是忘了解除写保护。DS1302 上电默认写保护必须先写 0x8E 为 0x00。另外检查秒寄存器的 CH 位如果 CH 为 1时钟是停的写进去也不走。6.3 时间乱跳或进位错误多半是 BCD 码转换写错了或者读秒的时候没屏蔽 CH 位。还有一种可能是读写时序里数据位顺序搞反了DS1302 是低位先出如果你按高位先出处理读出来的数据就是反的。6.4 常见问题速查表现象可能原因解决方法读回全 0xFFIO 方向未切换/延时太短读时切输入加 1~2us 延时读回全 0x00CE 未拉高/SCLK 未翻转检查 CE 和 SCLK 接线与电平时间写不进写保护未解除先写 0x8E 为 0x00秒不走CH 位为 1写秒寄存器时确保 bit7 为 0进位错误BCD 转换错误检查转换公式掉电归零备用电池未接或没电检查 VCC1 和电池电压实操心得调试 DS1302 最好用逻辑分析仪或者示波器抓一下 SCLK 和 IO 的波形一眼就能看出时序对不对。没有仪器的话就老老实实加延时宁慢勿快。7. 几个容易被忽略的进阶细节7.1 内部 RAM 的妙用DS1302 那 31 字节 RAM 很多人浪费了。它的地址从 0xC0 开始读写方式和时间寄存器一样。我一般拿它存一些设备配置参数比如报警阈值、校准偏移量掉电不丢比外挂 EEPROM 省事。注意 RAM 也受写保护寄存器控制写之前同样要解除保护。7.2 时钟停止检测与上电处理上电后可以先读一次秒寄存器如果 CH 位是 1说明芯片之前处于停止状态可能是第一次上电或者电池没电这时候时间是不可信的应该重新初始化时间。这个判断逻辑在实际产品里很有用能避免显示一个错误的时间。7.3 与 STM32 内部 RTC 的取舍如果你的项目对时间精度要求不高又不想外挂芯片STM32 内部 RTC 配合 32.768kHz 晶振也能用省一颗 DS1302 和电池。但内部 RTC 的坑也不少比如晶振起振慢、负载电容匹配难。DS1302 的好处是独立供电、掉电走时简单直接接线也简单。我的建议是学习三线时序用 DS1302做产品看具体需求。7.4 代码分层与移植性我把 DS1302 的驱动分成两层底层是 GPIO 操作和延时上层是寄存器读写和时间处理。底层用宏定义包起来换 STM32 型号或者换到别的单片机只改底层就行。这样代码复用性高也方便以后维护。这个习惯我从做第一个项目就养成了后面换平台省了大量时间。最后分享一个我自己的小习惯每次调通一个外设驱动我都会把关键时序参数和踩过的坑记在一个文档里下次再用直接翻记录。DS1302 这个驱动我前后改过三版第一版没加延时跑不通第二版忘了写保护写不进时间第三版才稳定。这些记录比代码本身还值钱。