☰
STM32驱动DS1302从零实战:GPIO模拟时序、BCD码与掉电保持全解析
2026/10/13 5:51:44 网站建设 项目流程

STM32 驱动 DS1302 这种活儿,很多人觉得是个老掉牙的话题,毕竟现在带内置 RTC 的单片机一抓一大把,外挂 RTC 芯片似乎有点多余。但如果你真正把手头的项目做出来、把时序调通、把数据读回来,就会发现这个看似简单的“过气”外设,藏着不少值得琢磨的东西。

这篇学习笔记不是照搬数据手册,而是我实际从零开始把 DS1302 驱动跑通的全过程记录。从最底层的一个 bit 怎么收发,到时间怎么读写、怎么校验,再到上电瞬间为什么时间会乱、掉电后怎么保持,我都会用自己的话说清楚。目标很明确:让你看完之后,不需要再翻芯片手册,也能在自己板子上把 DS1302 玩转。

1. 项目拆解:为什么在 STM32 时代还要学 DS1302

1.1 先搞清楚这个项目的本质需求

很多人看到“STM32 驱动 DS1302”这个标题,第一反应就是“无非就是写几个 GPIO 模拟时序”。这么说也没错,但真正动手之后会发现,这个项目的核心并不在“能不能转起来”,而在于三个层次的需求:

第一层是基础功能需求:能够通过 STM32 的普通 GPIO 引脚,模拟出 DS1302 的通信时序,完成时间、日期的设置和读取。这一层能覆盖 90% 的新手需求——比如做个电子钟、给单片机系统加个时间戳功能。

第二层是可靠性需求:DS1302 是外置时钟芯片,它靠 32.768kHz 的晶振走时,靠后备电池在掉电后维持时间。这里就涉及晶振起振不稳、电池电量不足、上电瞬间寄存器被误写等一系列坑。这一层是“能不能在实际产品中用”的分水岭。

第三层是扩展性需求:DS1302 内部其实还带着 31 字节的 RAM 和涓流充电电路,很多人用了一年 DS1302 都不知道这些功能。把驱动写好之后,顺手把这些能力也解封,才算是真正玩透了一颗芯片。

我自己的体会是:这个项目适合两类人。一类是刚学完 STM32 基础外设、想通过一个综合小项目巩固 GPIO、定时器、串口等知识的初学者;另一类是项目里确实需要一个低成本、低功耗、接口简单的时钟方案,但又不想上来就啃复杂协议的人。

1.2 方案选型:为什么我选择用 GPIO 模拟而不是硬件 SPI

DS1302 的通信接口看起来有点像 SPI,也有 CE、SCLK、DAT 三根线,但它不是标准 SPI。它的读操作要求“先写命令字节,再读数据字节”,而且数据线上是双向半双工的——写的时候 STM32 输出,读的时候 DS1302 驱动数据线,这个切换过程标准 SPI 外设并不会帮你自动处理。

我见过有人用 SPI 硬件接口去驱动 DS1302,结果因为命令字节和数据字节之间需要插入 GPIO 模式切换,最后被迫在中间手动拉 CE、手动切方向,反而比纯 GPIO 模拟更麻烦。所以我的方案非常明确:直接用三个 GPIO 口模拟时序,SCLK 用推挽输出,DAT 在读写时需要切换输入输出方向,CE 一直推挽输出。

这个方案最大的优势就是“哪里有引脚哪里就能用”,不受 SPI 外设引脚复用限制。而且 DS1302 的时钟最高也就 2MHz,GPIO 模拟在 STM32 主频动辄 72MHz 甚至更高的条件下,时序精度绰绰有余。

方案优点缺点结论
硬件 SPI 驱动代码简洁命令/数据切换繁琐、引脚受限不建议
GPIO 模拟驱动灵活通用、引脚自由需要自己控制时序细节推荐,适合学习本项目
使用 I2C 版本 RTC 芯片占用引脚更少需要换芯片型号若追求性价比可换方案

2. DS1302 协议拆解:从引脚到寄存器再到时序

2.1 引脚连接与硬件电路设计

DS1302 总共 8 个引脚,但实际需要跟单片机相连的只有三根线,剩下的是电源、地、晶振和备用电池。下面是我在实际板子上用的连接方式:

  • VC 接 +5V(或 +3.3V,DS1302 工作电压范围很宽)
  • GND 接地
  • X1、X2 接 32.768kHz 晶振
  • CE 接 STM32 的一个 GPIO(我用的 PB12)
  • SCLK 接 PB13
  • DAT(I/O)接 PB14
  • VCC1 接后备电池正极,电池负极接地

这里有一个非常关键的点:DS1302 的两根晶振引脚之间必须并联一个大约 6pF 的电容。数据手册上只画了晶振没画电容,但很多新手照抄连接后出现时间不走、时间乱跳的问题,罪魁祸首往往就是没加这两个小电容。我自己第一次搭电路时也犯了同样的错误——晶振起振条件不满足,芯片压根不工作。

备用电池方面,如果只是学习测试,可以用一颗 CR2032 纽扣电池正极接 VCC1、负极接 GND。需要注意:DS1302 的 VCC2 是主电源,VCC1 是备份电源,芯片会优先选择两者中电压较高的那个。实际测试中,如果主电源 3.3V 正常供电,VCC1 电池几乎不耗电,掉电后自动切换到电池供电。

2.2 命令字节格式:所有通信的基础

DS1302 的所有操作都是从发送一个命令字节开始的,这个命令字节一共 8 位,含义如下:

  • bit7:固定为 1
  • bit6:操作方向,1 表示读,0 表示写
  • bit5~bit1:寄存器地址(5 位)
  • bit0:固定为 0(访问普通寄存器时);如果是 RAM 操作则为 1

拿最常用的“写秒寄存器”来说,秒寄存器的地址是 0x80,加上写标志位后命令字节变成 0x80,而读秒寄存器的命令字节就是 0x81。同理,写分钟是 0x82、读分钟是 0x83,写小时 0x84、读小时 0x85,以此类推。

刚开始学的时候,我建议把地址表抄下来贴在自己电脑显示器边沿,写代码时对照着来。后面写得多了,0x80、0x81 这些地址基本就背下来了。

2.3 时间寄存器与 BCD 编码

DS1302 的时间信息分别存储在秒、分、时、日、月、星期、年这 7 个寄存器中,每个寄存器 8 位。这些寄存器里的数据以BCD 码(二进制编码的十进制数)形式存储。

BCD 码什么意思?简单说:一个字节的高 4 位表示十位,低 4 位表示个位。比如十进制数 23,BCD 码就是 0x23,二进制是 0010 0011。而如果是直接存二进制,23 就是 0x17。这意味着我们不能把读到的字节当成普通整数直接用,必须先做个转换。

秒寄存器比较特殊,它的最高位 CH 是“时钟暂停标志”。当这一位为 1 时,晶振停止振荡,时钟不走。所以刚拿到一片全新的 DS1302 或者电池没电过的芯片,上电后时间不动,很可能就是因为 CH 位被置 1 了。写时间的时候必须清零这一位,时钟才真正开始运行。

寄存器读命令写命令数据范围(BCD)备注
秒0x810x8000~59bit7 是 CH 暂停位
分0x830x8200~59正常
小时0x850x8401~12 或 00~23bit7 选择 12/24 小时制
日(日期)0x870x8601~31正常
月0x890x8801~12正常
星期0x8B0x8A01~07正常
年0x8D0x8C00~99正常
写保护0x8F0x8Ebit7 置 1 禁止写所有写操作前提

2.4 通信时序:一个字节是怎么交换的

DS1302 的时序其实就一条主线:CE 拉高表示一次通信开始,然后在 SCLK 上升沿,数据线上的一位一位被写入芯片;读数据时,数据在 SCLK 下降沿被芯片输出,STM32 在上升沿读取。

每次通信流程如下:

  1. CE 置高
  2. 发送 8 位命令字节(在 SCLK 上升沿逐位输出)
  3. 如果要读数据,立即切换 DAT 方向为输入,然后产生 8 个时钟脉冲,在每个上升沿读取一位数据
  4. 如果要写数据,保持 DAT 为输出,在接下来的 8 个 SCLK 上升沿逐位输出数据
  5. CE 置低,通信结束

这里特别要注意的是:CE 在整个通信过程中必须保持高电平,中间不能掉下来。CE 的作用相当于 SPI 的片选,但它比片选更严格——DS1302 会把 CE 下降沿之后收到的时钟脉冲忽略掉,一旦 CE 提前拉低,这次通信就废了。

我用一个很生活化的类比来理解这个时序:CE 就像两个人通电话前的“接通”动作,SCLK 就像拨号时的脉冲信号,DAT 就像电话线里的声音。没接通电话之前,拨号脉冲再精准也没意义;通话过程中挂断,对方说一半的话也传不过去。

3. STM32 驱动代码实现:从单字节到完整读写

3.1 工程结构与底层配置

我的工程基于标准外设库风格编写,但逻辑上完全不影响你用 HAL 库或寄存器版本来移植。核心驱动分为两个文件:ds1302.c和ds1302.h。

引脚配置方面,STM32 GPIO 要做如下设置:

  • CE(PB12):推挽输出,默认低电平
  • SCLK(PB13):推挽输出,默认低电平
  • DAT(PB14):推挽输出,但读取数据时需要切换为浮空输入

为什么要用浮空输入而不是上拉输入?因为 DS1302 的数据线是双向的,芯片输出低电平时会主动拉低,输出高电平时也会主动驱动到高电平。如果加上拉电阻,在某些边界情况下会影响电平判断,实测浮空输入最稳。

3.2 底层引脚操作函数

先写最底层的延时函数。DS1302 时钟周期最小要求是 250ns,STM32 跑 72MHz 时一个空循环也要不少时间,直接用循环延时就可以满足。

void DS1302_Delay(void) { /* 简单延时,约 1us @ 72MHz */ for (uint8_t i = 0; i < 20; i++); }

然后是引脚电平控制:

#define DS1302_CE_HIGH() GPIO_SetBits(GPIOB, GPIO_Pin_12) #define DS1302_CE_LOW() GPIO_ResetBits(GPIOB, GPIO_Pin_12) #define DS1302_SCLK_HIGH() GPIO_SetBits(GPIOB, GPIO_Pin_13) #define DS1302_SCLK_LOW() GPIO_ResetBits(GPIOB, GPIO_Pin_13) #define DS1302_DAT_HIGH() GPIO_SetBits(GPIOB, GPIO_Pin_14) #define DS1302_DAT_LOW() GPIO_ResetBits(GPIOB, GPIO_Pin_14) void DS1302_DAT_OUT(void) { GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_14; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_Out_PP; GPIO_InitStruct.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOB, &GPIO_InitStruct); } void DS1302_DAT_IN(void) { GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Pin = GPIO_Pin_14; GPIO_InitStruct.GPIO_Mode = GPIO_Mode_IN_FLOATING; GPIO_Init(GPIOB, &GPIO_InitStruct); }

3.3 单字节读写:把时序落成代码

写入一个字节的实现,关键在于“上升沿写入”。我在每次将 DAT 设置好电平后,先 SCLK 拉低、再拉高,形成一个上升沿,这样数据就会被锁存进芯片:

void DS1302_WriteByte(uint8_t dat) { for (uint8_t i = 0; i < 8; i++) { if (dat & 0x01) DS1302_DAT_HIGH(); else DS1302_DAT_LOW(); DS1302_SCLK_LOW(); DS1302_Delay(); DS1302_SCLK_HIGH(); DS1302_Delay(); dat >>= 1; } }

读一个字节时,数据在 SCLK 下降沿会由 DS1302 放到数据线上,所以我的做法是:先把 SCLK 拉低,芯片输出第一位数据,然后拉高 SCLK,在上升沿的时候读入 GPIO 电平:

uint8_t DS1302_ReadByte(void) { uint8_t dat = 0; DS1302_DAT_IN(); for (uint8_t i = 0; i < 8; i++) { dat >>= 1; DS1302_SCLK_LOW(); DS1302_Delay(); DS1302_SCLK_HIGH(); DS1302_Delay(); if (GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_14)) dat |= 0x80; } DS1302_DAT_OUT(); return dat; }

这里有个容易写错的地方:我见过很多人读字节时用“先读低位再左移”的方式,导致读回来的数据 bit 顺序反了。上面这个代码用了dat >>= 1然后再把最高位置位,本质是把最先收到的位放在最高位,但整体顺序要跟写字节的dat >>= 1配合好。我在调试时踩过这个坑,最后打印出来发现秒、分、时全部数值颠倒,比如 0x01 变成 0x80,排查了好久才发现是移位方向搞反了。

3.4 寄存器的读写封装

有了单字节读写函数,寄存器操作就水到渠成了。写寄存器是:CE 拉高 → 发送命令 → 发送数据 → CE 拉低。

void DS1302_WriteRegister(uint8_t reg, uint8_t dat) { DS1302_CE_HIGH(); DS1302_WriteByte(reg); // reg 是写命令,如 0x80 DS1302_WriteByte(dat); DS1302_CE_LOW(); }

读寄存器是:CE 拉高 → 发送读命令 → 读取数据 → CE 拉低。

uint8_t DS1302_ReadRegister(uint8_t reg) { uint8_t dat = 0; DS1302_CE_HIGH(); DS1302_WriteByte(reg); // reg 是读命令,如 0x81 DS1302_DAT_IN(); dat = DS1302_ReadByte(); DS1302_DAT_OUT(); DS1302_CE_LOW(); return dat; }

注意读操作在发完命令之后,必须立刻把 DAT 线切换成输入模式。这个切换不能太晚,因为 DS1302 在命令字节的最后一位到达后就开始准备输出数据了。我在第一次写代码时,因为切换滞后,导致最高位读错,表现是秒的读数忽大忽小、不规律跳变,后来加了DS1302_DAT_IN()并做了短暂延时才恢复正常。

3.5 BCD 与十进制互转

时间寄存器内部是 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); }

这两个函数的逻辑不复杂,但实际使用中频繁调用,性能开销可以忽略不计。我习惯把秒、分、时、日、月、星期、年这 7 个字段分别读出并转换,存进一个时间结构体,这样主程序用起来非常顺手。

typedef struct { uint8_t year; /* 0~99 */ uint8_t month; /* 1~12 */ uint8_t day; /* 1~31 */ uint8_t weekday; /* 1~7 */ uint8_t hour; /* 0~23 */ uint8_t minute; /* 0~59 */ uint8_t second; /* 0~59 */ } DS1302_Time;

3.6 写入与读取时间函数

写入时间函数会按照寄存器地址逐个写入,但是有个细节:必须先写秒寄存器,再写分、时、日、月、星期、年。因为写秒寄存器时,如果写进去的秒恰好是 0,秒溢出会进位到分钟,如果你先写了分钟再写秒,分钟进位可能把你刚写的分钟值覆盖掉。虽然实际概率不高,但这是数据手册建议的操作顺序。

void DS1302_SetTime(DS1302_Time *time) { DS1302_WriteRegister(0x8E, 0x00); /* 清除写保护 */ DS1302_WriteRegister(0x80, DEC2BCD(time->second)); DS1302_WriteRegister(0x82, DEC2BCD(time->minute)); DS1302_WriteRegister(0x84, DEC2BCD(time->hour)); DS1302_WriteRegister(0x86, DEC2BCD(time->day)); DS1302_WriteRegister(0x88, DEC2BCD(time->month)); DS1302_WriteRegister(0x8A, DEC2BCD(time->weekday)); DS1302_WriteRegister(0x8C, DEC2BCD(time->year)); DS1302_WriteRegister(0x8E, 0x80); /* 使能写保护 */ }

这里写保护寄存器 0x8E 是个大坑:DS1302 默认开启写保护,如果不把它的 bit7 清零,前面的所有写操作都会失败。我看到网上很多代码漏了这一步,结果现象就是“设置时间没反应”“读了半天全是 0”。别问为什么我知道,问就是踩过。

读取时间函数就简单多了,逐个读寄存器并转换即可:

void DS1302_GetTime(DS1302_Time *time) { time->second = BCD2DEC(DS1302_ReadRegister(0x81)); time->minute = BCD2DEC(DS1302_ReadRegister(0x83)); time->hour = BCD2DEC(DS1302_ReadRegister(0x85)); time->day = BCD2DEC(DS1302_ReadRegister(0x87)); time->month = BCD2DEC(DS1302_ReadRegister(0x89)); time->weekday = BCD2DEC(DS1302_ReadRegister(0x8B)); time->year = BCD2DEC(DS1302_ReadRegister(0x8D)); }

3.7 初始化:给芯片一个确定的起点

DS1302 初始化代码没有太多套路,但有一步很关键——在写时间之前先读一次秒寄存器,判断 CH 位。如果 CH 为 1,说明芯片从未开始走时或者电池没电过,此时应当提示用户重新设置时间;如果 CH 为 0,说明时钟正在运行,直接读取即可。

void DS1302_Init(void) { /* 配置 GPIO 引脚模式 */ DS1302_CE_LOW(); DS1302_SCLK_LOW(); DS1302_DAT_LOW(); /* 检查秒寄存器 CH 位 */ uint8_t sec = DS1302_ReadRegister(0x81); if (sec & 0x80) { /* 时钟未运行,需要设置时间 */ DS1302_Time t = { 0, 0, 0, 0, 0, 0, 0 }; DS1302_SetTime(&t); } }

这个初始化策略是我在项目里逐渐摸索出来的:最开始我不管三七二十一,上电就默认写一次固定时间,结果每次断电再上电,如果电池没装好,时间就会被重置为同一个初始值,系统里所有的时间戳记录全部作废。加上 CH 位检查之后,只有真正需要时才重置时间,可靠性高了一大截。

4. 实际调试记录:我遇到的那些坑和排查方法

4.1 问题一:怎么读时间都是 0xFF 或 0x00

这个现象很典型——读回来的秒、分、时要么全是 0xFF,要么全是 0x00。排查思路从硬件到软件一层层剥:

第一步查供电:用万用表量 DS1302 的 VC 引脚和 GND 之间电压是否达标。我遇到过一次 3.3V 和 5V 混接的情况,DS1302 虽然能工作但电平识别出现异常,读回的数据不稳定。

第二步查 CE 和 SCLK 引脚顺序:DS1302 引脚间距不算大,如果用了杜邦线连接,经常会出现插错的情况。CE 和 SCLK 互换之后,芯片不会正常响应,表现出来就是读回 0xFF 或 0x00。

第三步查命令字节是否写对:把 DS1302_WriteRegister 的第一个参数打印出来,确认是不是 0x81 这样的读命令。有一次我把读秒的命令字节误写成了 0x80,结果命令发出去的瞬间芯片以为是要写秒寄存器,后续时钟脉冲全部被当成要写入的数据,读回来当然是一团糟。

4.2 问题二:时间能设置,但断电重上电后又变成初始值

这个问题我排查了很久。因为 DS1302 是靠 VCC1 引脚上的备用电池来维持走时的,如果 VCC1 没有接电池或者电池电压过低,掉电后芯片内部寄存器内容就会丢失。

还有一个更容易忽略的点:备用电池不能直接用普通充电电池。普通 1.5V 碱性电池虽然也能供电,但 DS1302 内部电路对电压跌落比较敏感,电压低于 1.2V 时寄存器数据就可能损坏。建议用 3V 纽扣电池。如果你在设计产品,还可以在 VCC1 引脚和电池之间串一个二极管,防止主电源给电池反充(DS1302 内置了电源切换电路,但外部防倒灌更保险。

还有一次我遇到“时间能走但断电就丢”,排查到最后发现是晶振的一根引脚虚焊。晶振平时有主电源供电时还能勉强振荡,但切到电池供电后振荡幅度不足,时间不走,寄存器里的秒值停留在断电瞬间,重上电自然就“回到过去”了。重新补焊晶振后问题彻底消失。

4.3 问题三:时间走动正常,但小时的小时数偏大或偏小

秒、分都正常,唯独小时数据不对,而且有时出现 12、13 这样超出预期的数。这个问题的根源是小时寄存器的 bit7 —— 它控制 12 小时制还是 24 小时制。如果 bit7 被置 1,芯片进入 12 小时制模式,此时 bit5 表示上午/下午,小时数只占低 5 位,表现出来就是“下午 3 点显示成 03,而且不知道上午还是下午”。

我的解决办法是:在 SetTime 函数里,写小时寄存器时强制把 bit7 清零,确保一直是 24 小时制。具体代码就是DEC2BCD(hour) & 0x3F,把高两位清掉。这样就不会出现小时制混乱的问题。

4.4 问题四:读时间偶尔出现“跳秒”或“分钟突然加一”

这种问题多半来自读操作和 DS1302 内部进位撞车了。比如你读秒的时候,秒值正好从 59 跳转到 00,进位到分钟,但这次进位可能来不及体现在你刚读到的分钟值上,于是你读到的分钟是旧的,秒是新的,看起来就像“时间倒流”或“跳变”。

这个问题在实时性要求高的场合必须处理。两个思路:

  • 连续读两次,对比秒读数是否一致,如果一致则采用,不一致则再读一次。
  • 在程序中加一个“读保护”逻辑:比如先读分钟,再读秒,如果秒小于第一次读到的秒,说明发生进位,重新读一遍分钟。

考虑到 DS1302 应用场景大多不是高精度同步场景,我一般用第一种“双读校验”就够了,代码量小,效果稳定。

4.5 问题五:读取时间偶尔卡死

这个问题的根源是通信过程中 DAT 线电气状态冲突。比如你发完写命令后,芯片已经把 DAT 线驱动为高电平,此时你又把 STM32 的 DAT 引脚配置为推挽输出并输出低电平,两个输出短接,虽然没有物理损坏,但会导致电平不确定、逻辑混乱,极端情况下 SCLK 继续翻转时芯片进入了非预期的状态。

解决办法很粗暴但有效:每次通信结束后,把 DAT 线切回输出模式,并拉低。同时在每次通信开始前,把 CE 拉低、SCLK 拉低,确保芯片处于空闲状态。严格执行这个流程后,卡死现象我再也没遇到过。

现象可能原因排查优先级
读数全 0xFF引脚接错 / 供电异常先查硬件连接
读数全 0x00缺少上电初始化流程检查 CH 位
时间不更新写保护未关闭检查 0x8E
小时制混乱小时寄存器 bit7 被设置强制 24 小时制
读取偶发错位读写方向切换太慢优化 DAT 切换时序
掉电丢数据电池未接 / 晶振虚焊物理排查

5. 进阶玩法:用 DS1302 的涓流充电和 RAM 做点有意义的事

5.1 涓流充电是什么,怎么配置

DS1302 有一个涓流充电寄存器(地址 0x90/0x91),作用是给后备电池或超级电容充电。这个功能很多人不知道,但非常有实用价值——如果你在系统里放了一颗超级电容当成断电后的备份电源,需要让主电源给超级电容慢速充电,就可以用这个寄存器。

寄存器 0x90 的低 4 位控制充电逻辑:

  • bit3~bit2:二极管选择,常用10表示串联 1 个二极管
  • bit1~bit0:限流电阻选择,01是 2kΩ,10是 4kΩ,11是 8kΩ

以 3.3V 主电源为例,如果串联一个二极管再串 2kΩ 电阻,充电电流大约是 (3.3V - 约0.6V) / 2kΩ,大概 1.35mA。这个电流给一颗几法拉级的超级电容充电,时间可控、也不至于过流损坏。

void DS1302_EnableTrickleCharge(void) { DS1302_WriteRegister(0x90, 0x05); /* 1 二极管 + 2kΩ 电阻 */ }

不过要小心:如果你用的是不可充电的 CR2032 纽扣电池,千万不要开涓流充电!我身边有人开了这个功能,结果给不可充电锂电池充电导致电池鼓包。使用前先看清楚自己的电池类型。

5.2 31 字节 RAM 的读写

DS1302 自带 31 字节的静态 RAM,地址范围是 0xC0~0xFD(写命令)、0xC1~0xFE(读命令)。这 31 个字节掉电后由电池供电保持,非常适合存一些配置参数、校正值、设备序号之类的少量数据。

用法跟寄存器几乎一样,只是命令字节的 bit0 要置 1,表示访问 RAM。写第一个字节:

void DS1302_WriteRAM(uint8_t addr, uint8_t dat) { /* addr 范围 0~30,对应芯片内部地址 0~30 */ uint8_t cmd = 0xC0 | ((addr & 0x1F) << 1); DS1302_CE_HIGH(); DS1302_WriteByte(cmd); DS1302_WriteByte(dat); DS1302_CE_LOW(); }

读取时命令是 0xC1 开头。我实际用过这个 RAM 来存一个“设备上电次数”的计数器,每次开机读出来加一写回去,即使掉电也不丢。比起外部 EEPROM,这条路径简单得多。

5.3 与主控系统配合的两种架构

DS1302 是外置 RTC,系统运行时有两种使用方式:

第一种是“查询式”:主程序每隔一段时间(比如每秒)通过 GPIO 读取一次时间,更新时间结构体。这种方式简单直观,我在学习阶段用的就是这种。缺点是不停轮询会消耗一点 CPU,但对于 STM32 这种主频几十上百兆的芯片完全无所谓。

第二种是“中断式”:把 DS1302 的秒脉冲功能利用起来(虽然 DS1302 本身不像 DS3231 有 INT 引脚),或者用一个 STM32 定时器产生 1Hz 中断,在中断里读取 DS1302 时间并更新全局变量。这种方式结构更专业,时间抖动也更小,适合做需要精确计时的系统。

我自己后来的项目用的是第二种:STM32 的 TIM2 产生 1Hz 中断,在中断服务函数里读一次 DS1302,更新一个全局时间结构体。主程序随时读这个结构体就能拿到当前时间,不用每次访问外设,逻辑上清爽很多。

5.4 用串口把时间显示出来

学习阶段最直观的验证方式就是用串口把时间打印到电脑上。STM32 的串口初始化代码就不列了,重点说打印格式:

void PrintTime(DS1302_Time *t) { printf("20%02d-%02d-%02d %d %02d:%02d:%02d\r\n", t->year, t->month, t->day, t->weekday, t->hour, t->minute, t->second); }

这里年份显示需要加“20”前缀,因为 DS1302 的年份寄存器只存 0~99,没有世纪信息。如果要处理跨越 2000 年的情况,就得在应用层自己做判断,这也是 DS1302 比现代 RTC 芯片差的地方之一。

6. 亲手从头写一遍驱动的收获与建议

这套驱动我前后重写过三次。第一次照抄网上的代码,能读时间但出问题完全不知道怎么排查;第二次自己对着数据手册敲,边敲边注释,慢慢理解了时序每一步的意义;第三次才开始加入写保护、CH 位判断、双读校验这些人性化设计。

如果让我给刚接触 DS1302 的开发者一个建议,那就是:尽量不要直接复制别人的驱动代码。哪怕时间再紧,也要把别人代码里每个函数对应数据手册的哪个图、哪个表搞清楚。这个“慢慢咀嚼”的过程,比驱动本身更有价值。DS1302 所有操作都是围绕“命令字节 + 数据字节”这两个基础动作展开的,一旦你把这个底层逻辑印在脑子里,换任何其他外设都能快速上手。

此外,调试时手里一定要有一块逻辑分析仪。几十块钱的逻辑分析仪配合上位机软件,就能把 SCLK、DAT、CE 三根线上的波形看得清清楚楚。哪一位对不上、哪个时序有问题,波形上直接能看出来,比对着串口打印猜要高效十倍。我之前调试 DAT 方向切换的问题,就是靠逻辑分析仪一眼看出命令字节最后一位和数据线切换之间有一个微小的时间差。

回到最初的问题:STM32 又不是没有内置 RTC,为什么还要折腾 DS1302?我的理解是,DS1302 更像一个“协议的启蒙老师”,它用最简单的方式告诉你什么叫时序、什么叫双向数据线、什么叫写保护寄存器。这些概念你在 DS1302 上学通了,再看 SPI、I2C、SDIO 这些复杂协议,会觉得亲切许多。而且 DS1302 本身自带电池备份、涓流充电、31 字节 RAM,一颗芯片能玩出不少花活,成本也就一两块钱,作为学习项目再合适不过了。

到最后,我这个驱动还留了一个小功能没写上去——DS1302 的 Burst 模式。一次通信就能读写全部 7 个时间寄存器,效率比逐个读高不少。留给你自己琢磨。原理就是在发送命令字节时将地址位全部置 1,即读命令 0xBF、写命令 0xBE,之后连续收发 7 个字节。等你把单个寄存器读写彻底吃透,再打开这个模式,一切都会顺理成章。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询