news 2026/10/21 1:03:51

STM32外挂DS1302实时时钟:三线时序、BCD转换与31字节RAM实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
STM32外挂DS1302实时时钟:三线时序、BCD转换与31字节RAM实战

1. 为什么还要折腾 DS1302 这颗老芯片

1.1 从一次断电丢时间的翻车说起

前阵子帮朋友做一个工业数据采集的小盒子,主控用的是 STM32F103C8T6,功能很简单:每隔十分钟把几路传感器的数据打包存到 SD 卡里,同时屏幕上显示当前时间。一开始我图省事,直接用 STM32 内部的 RTC 加一个纽扣电池供电,结果现场跑了不到一周,朋友打电话过来说时间乱了,日志里的时间戳前后跳得离谱,有的记录甚至显示成了上个月。

拆回来一查,问题出在两个地方:一是内部 RTC 的晶振走时精度本来就一般,常温下一天差个几秒很正常,一周下来累积误差就很可观;二是那块板子的电池座接触不良,断电后 RTC 域供电时断时续,时间自然就飘了。后来我干脆换了个思路,外挂一颗 DS1302 做独立走时,STM32 只负责通过三根线去读它,这样即使主控复位、重新烧录程序,时间也不会丢。

这个项目就是在那次翻车之后整理出来的学习笔记。DS1302 这颗芯片年纪不小了,但胜在便宜、资料多、接线简单,特别适合刚接触 STM32 外设驱动的新手拿来练手。它本质上是一个带涓流充电能力的实时时钟芯片,能计秒、分、时、日、月、周、年,还自带 31 字节的静态 RAM 可以存点小数据。你要做的,就是让 STM32 通过一个类似 SPI 的三线串行接口跟它对话,把时间读出来、写进去。

适合看这篇笔记的人有三类:一是刚学完 GPIO 和串口、想找个综合案例练手的 STM32 新手;二是做小项目需要独立走时、又不想上复杂方案的朋友;三是单纯想搞明白“三线串行时序到底怎么用代码模拟”的嵌入式爱好者。下面我会把整个思路、时序细节、代码实现和踩过的坑都摊开讲,尽量让你看完就能自己复现。

1.2 DS1302 到底是个什么东西

先把这颗芯片的定位说清楚。DS1302 是那种“麻雀虽小五脏俱全”的典型:8 个引脚,DIP 或者 SOP 封装,工作电压 2.0V 到 5.5V,跟 STM32 的 3.3V 电平可以直接对接,不需要额外的电平转换。它的核心是一个 32.768kHz 的晶振分频出来的计时电路,外加一组寄存器来存放时间数据和配置信息。

它跟主控的通信方式很有意思,官方叫“三线串行接口”,实际上就是三根线:一根复位/片选(RST,有的资料叫 CE)、一根时钟(SCLK)、一根数据(I/O)。注意这个 I/O 是双向的,读的时候是输入,写的时候是输出,所以 STM32 这边这个引脚要能在输入和输出之间切换,这是新手最容易忽略的一个点。

DS1302 内部有两块存储区域:一块是时钟/日历寄存器,地址从 0x80 到 0x8D 左右,存的是秒、分、时、日、月、周、年,还有写保护位和涓流充电配置;另一块是 31 字节的 RAM,地址从 0xC0 开始,掉电也能靠后备电池保住,可以拿来存一些设备参数、校准系数之类的东西。这个 RAM 是很多人不知道的隐藏福利,后面我会专门讲怎么用。

注意:DS1302 的时间寄存器用的是 BCD 码,不是普通的二进制。比如“23 分”存进去是 0x23,而不是十进制的 23。这个转换如果搞错,读出来的时间会乱七八糟,这是新手第一个大坑。

2. 整体方案设计与选型考量

2.1 为什么不用 STM32 内部 RTC

很多人第一反应是:STM32 自己就有 RTC,为什么还要外挂一颗?这个问题我在做方案的时候也纠结过,后来把两者的差异列了个表,思路就清楚了。

对比项STM32 内部 RTC外挂 DS1302
走时精度依赖外部 32.768kHz 晶振,一般同样依赖晶振,但独立供电更稳
断电保持需要 VBAT 引脚接电池,电路稍复杂自带电池引脚,接线简单
占用引脚需要晶振引脚,可能和别的功能冲突任意三个 GPIO 即可
编程复杂度HAL 库封装好,但配置项多需要自己写时序,但逻辑直观
成本几乎为零几毛钱一颗
附加存储有备份寄存器,但数量有限自带 31 字节 RAM

从表里能看出来,内部 RTC 的优势是省事、省成本,如果你的项目对时间精度要求不高,比如只是记录个大概时间,那用内部 RTC 完全够。但如果你遇到下面几种情况,外挂 DS1302 就更合适:主控的晶振引脚被占用了、需要频繁断电重启但时间不能丢、想要一块独立的存储区域存参数、或者单纯就是想学一下三线时序。

我这次选 DS1302,主要就是看中它独立供电和自带 RAM 这两点。工业现场那个盒子经常要断电维护,内部 RTC 的电池电路一旦接触不良就全乱套,而 DS1302 的电池是直接接在芯片上的,可靠性高不少。

2.2 三线接口的时序本质

DS1302 的通信协议说穿了就是“在时钟的控制下,一位一位地搬数据”。它没有标准 SPI 那么快,也没有 I2C 那么讲究,就是最朴素的三线同步串行。理解它的关键,是搞清楚“什么时候读、什么时候写、数据从哪边来”。

整个通信过程分两步:先发一个命令字节,告诉芯片“我要读还是写、操作哪个寄存器”;然后紧接着传输数据字节。命令字节的最高位(bit7)决定方向,1 是读,0 是写;bit6 到 bit1 是寄存器地址;最低位(bit0)固定为 0,如果是写操作,这一位可以是 1,但一般填 0 就行。

时序上有几个硬性要求,我一开始没注意,调了半天才通:

  • RST 拉高之后,必须等至少 4 微秒才能开始发时钟,这是芯片的启动时间。
  • 数据是在 SCLK 的上升沿被芯片采样(写操作),或者在下降沿被芯片输出(读操作)。所以写的时候你要在拉高 SCLK 之前把数据准备好,读的时候要在拉低 SCLK 之后去采样。
  • 最低位(LSB)先发,也就是从 bit0 开始,这跟很多人的直觉相反,SPI 一般是 MSB 先发。
  • 一次完整的读写结束后,要把 RST 拉低,让芯片复位,准备下一次通信。

提示:如果你用硬件 SPI 去模拟,会发现时序对不上,因为 DS1302 的读写边沿和标准 SPI 模式不完全一致。所以最稳妥的办法是用 GPIO 软件模拟,虽然慢一点,但时序完全可控,调试也直观。

2.3 硬件连接与供电设计

接线这块其实很简单,但有几个细节值得说。DS1302 一共 8 个脚,我按功能分组:

  • VCC2(引脚 1):主电源,接 3.3V。
  • VCC1(引脚 8):后备电源,接一颗 3V 纽扣电池,比如 CR2032。
  • GND(引脚 4):接地。
  • X1、X2(引脚 2、3):接 32.768kHz 晶振,两端各配一个 6pF 左右的负载电容。
  • RST(引脚 5):接 STM32 的一个 GPIO,我习惯用 PA0。
  • SCLK(引脚 6):接 STM32 的一个 GPIO,我用 PA1。
  • I/O(引脚 7):接 STM32 的一个 GPIO,我用 PA2。

这里有个关键点:VCC2 和 VCC1 之间,芯片内部有比较电路,会自动选择电压高的那一路供电。正常工作时 VCC2 是 3.3V,比电池的 3V 高,所以用主电源;一旦主电源掉电,自动切到电池,时间继续走。这个切换是芯片自动完成的,不需要软件干预。

晶振和负载电容的选型也有讲究。32.768kHz 晶振的负载电容一般是 6pF 或 12.5pF,具体要看晶振的规格书。如果电容选大了,走时会偏慢;选小了会偏快。我实测下来,6pF 的电容配常见的圆柱晶振,一天误差在 1 秒以内,对大多数项目够用了。如果你对精度要求特别高,可以选那种带温度补偿的晶振,但成本就上去了。

I/O 引脚是双向的,STM32 这边要配置成开漏输出加外部上拉,或者直接在读的时候切换成输入模式。我一开始图省事,一直配成推挽输出,结果读的时候芯片拉不动这条线,读出来全是 0xFF。后来改成“写的时候推挽输出、读的时候上拉输入”,问题就解决了。

3. 核心细节解析与实操要点

3.1 BCD 码转换:新手第一个拦路虎

前面提过,DS1302 的时间寄存器用 BCD 码存储。什么叫 BCD?就是“用四位二进制表示一位十进制数”。比如十进制的 25,二进制是 0001 1001,而 BCD 码是 0010 0101,高四位是 2,低四位是 5。这样做的目的是方便芯片直接驱动数码管显示,但对写代码的人来说就多了一层转换。

写时间的时候,要把十进制数转成 BCD;读时间的时候,要把 BCD 转回十进制。转换逻辑很简单,但一定要封装成函数,不然代码里到处写位运算,很容易出错。

// 十进制转 BCD uint8_t dec_to_bcd(uint8_t dec) { return ((dec / 10) << 4) | (dec % 10); } // BCD 转十进制 uint8_t bcd_to_dec(uint8_t bcd) { return ((bcd >> 4) * 10) + (bcd & 0x0F); }

看起来简单,但有个坑:DS1302 的小时寄存器有个 12/24 小时模式的选择位。如果你用 24 小时模式,bit7 要清零;如果用 12 小时模式,bit7 是 1,bit5 表示上午下午。我建议直接用 24 小时模式,省得处理 AM/PM。设置的时候,把小时寄存器的 bit7 写 0 就行。

还有星期寄存器,它的值范围是 1 到 7,但具体哪天是 1 由你自己定义,芯片不管。我一般把周一设为 1,周日设为 7,这样符合大多数人的习惯。

3.2 寄存器地址与命令字节的构造

DS1302 的每个操作都要先发一个命令字节。这个字节的构造规则是:bit7 是方向位(0 写 1 读),bit6 到 bit1 是地址,bit0 固定 0。所以写秒寄存器的命令是 0x80,读秒寄存器是 0x81。

我把常用的寄存器地址整理成一张表,方便查阅:

寄存器写命令读命令说明
秒0x800x81bit7 为 0 时正常计时
分0x820x83BCD 码
时0x840x8524 小时模式 bit7=0
日0x860x87BCD 码
月0x880x89BCD 码
周0x8A0x8B1 到 7
年0x8C0x8D00 到 99
写保护0x8E0x8Fbit7 为 1 时禁止写
涓流充电0x900x91配置充电方式
RAM 起始0xC00xC131 字节可用

写保护寄存器是个重点。DS1302 上电后,写保护位默认可能是打开的,这时候你写时间会失败,但读是正常的,很容易让人误以为时序有问题。所以每次写时间之前,先往 0x8E 写 0x00 解除保护,写完再写 0x80 重新保护。这个习惯一定要养成,不然现场误写会出大问题。

涓流充电寄存器一般用不上,除非你要给后备电池充电。如果用的是不可充电的纽扣电池,千万别开这个功能,否则电池会鼓包甚至漏液。我一般直接不碰这个寄存器,保持默认。

3.3 时序模拟的三个关键函数

软件模拟时序,核心就是三个操作:写一个字节、读一个字节、以及它们的底层位操作。我把它们拆开讲。

写一个字节的流程是:把数据准备好,然后循环 8 次,每次先把当前位放到 I/O 线上,再拉高 SCLK,延时一小会儿,再拉低 SCLK。注意 DS1302 是 LSB 先发,所以要从 bit0 开始取。

void ds1302_write_byte(uint8_t dat) { uint8_t i; for (i = 0; i < 8; i++) { DS1302_IO_OUT(); // 切换为输出 if (dat & 0x01) DS1302_IO_HIGH(); else DS1302_IO_LOW(); DS1302_SCLK_HIGH(); delay_us(2); DS1302_SCLK_LOW(); delay_us(2); dat >>= 1; } }

读一个字节稍微麻烦一点,因为要在 SCLK 下降沿之后去采样 I/O 线。流程是:先把 I/O 切成输入模式,然后循环 8 次,每次拉高 SCLK,延时,拉低 SCLK,延时,然后读 I/O 线的电平,拼到结果的对应位上。

uint8_t ds1302_read_byte(void) { uint8_t i, dat = 0; DS1302_IO_IN(); // 切换为输入 for (i = 0; i < 8; i++) { dat >>= 1; DS1302_SCLK_HIGH(); delay_us(2); DS1302_SCLK_LOW(); delay_us(2); if (DS1302_IO_READ()) dat |= 0x80; } return dat; }

注意读的时候是先移位再采样,这样第一次采到的位会落在 bit7,循环 8 次之后正好是完整的字节。这个顺序如果搞反,读出来的数据位序就是颠倒的。

有了这两个函数,读写寄存器就简单了:拉高 RST,发命令字节,发或读数据字节,拉低 RST。

void ds1302_write_reg(uint8_t cmd, uint8_t dat) { DS1302_RST_LOW(); DS1302_SCLK_LOW(); DS1302_RST_HIGH(); delay_us(4); ds1302_write_byte(cmd); ds1302_write_byte(dat); DS1302_RST_LOW(); } uint8_t ds1302_read_reg(uint8_t cmd) { uint8_t dat; DS1302_RST_LOW(); DS1302_SCLK_LOW(); DS1302_RST_HIGH(); delay_us(4); ds1302_write_byte(cmd); dat = ds1302_read_byte(); DS1302_RST_LOW(); return dat; }

注意:每次通信开始前,先把 RST 和 SCLK 都拉低,再拉高 RST,这个顺序不能乱。如果 SCLK 一开始是高电平,芯片可能会误判时钟边沿,导致第一个位就错位。

4. 完整实操流程与代码实现

4.1 工程结构与 GPIO 初始化

我用的开发环境是 Keil MDK,芯片是 STM32F103C8T6,库用的是标准外设库。工程结构很简单,新建一个 ds1302.c 和 ds1302.h,把驱动代码放进去,主函数里调用就行。

GPIO 初始化这块,我用的是 PA0 接 RST、PA1 接 SCLK、PA2 接 I/O。RST 和 SCLK 配置成推挽输出,I/O 配置成开漏输出加外部上拉,这样读的时候可以直接切换成输入模式。

void ds1302_gpio_init(void) { GPIO_InitTypeDef gpio; RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE); gpio.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1; gpio.GPIO_Mode = GPIO_Mode_Out_PP; gpio.GPIO_Speed = GPIO_Speed_50MHz; GPIO_Init(GPIOA, &gpio); gpio.GPIO_Pin = GPIO_Pin_2; gpio.GPIO_Mode = GPIO_Mode_Out_OD; GPIO_Init(GPIOA, &gpio); }

I/O 引脚的模式切换用宏来实现,读的时候切成上拉输入,写的时候切回开漏输出。

#define DS1302_IO_IN() do { \ GPIO_InitTypeDef gpio; \ gpio.GPIO_Pin = GPIO_Pin_2; \ gpio.GPIO_Mode = GPIO_Mode_IPU; \ GPIO_Init(GPIOA, &gpio); \ } while(0) #define DS1302_IO_OUT() do { \ GPIO_InitTypeDef gpio; \ gpio.GPIO_Pin = GPIO_Pin_2; \ gpio.GPIO_Mode = GPIO_Mode_Out_OD; \ GPIO_Init(GPIOA, &gpio); \ } while(0)

这里有个性能上的小问题:每次读写都重新初始化 GPIO,会有点慢。如果你的项目对速度要求高,可以改成直接操作寄存器,把 CRL 寄存器的对应位改一下就行。不过 DS1302 本身通信速度就不快,这点开销可以忽略。

4.2 时间读写函数的封装

有了底层函数,读写时间就是按顺序操作各个寄存器。我封装了两个函数:一个把时间写进去,一个把时间读出来。时间用一个结构体来存,包含年月日时分秒和星期。

typedef struct { uint8_t year; uint8_t month; uint8_t day; uint8_t week; uint8_t hour; uint8_t minute; uint8_t second; } ds1302_time_t; void ds1302_set_time(ds1302_time_t *t) { ds1302_write_reg(0x8E, 0x00); // 解除写保护 ds1302_write_reg(0x80, dec_to_bcd(t->second) & 0x7F); // 秒,bit7 清零启动计时 ds1302_write_reg(0x82, dec_to_bcd(t->minute)); ds1302_write_reg(0x84, dec_to_bcd(t->hour) & 0x3F); // 24 小时模式 ds1302_write_reg(0x86, dec_to_bcd(t->day)); ds1302_write_reg(0x88, dec_to_bcd(t->month)); ds1302_write_reg(0x8A, dec_to_bcd(t->week)); ds1302_write_reg(0x8C, dec_to_bcd(t->year)); ds1302_write_reg(0x8E, 0x80); // 恢复写保护 } void ds1302_get_time(ds1302_time_t *t) { t->second = bcd_to_dec(ds1302_read_reg(0x81) & 0x7F); t->minute = bcd_to_dec(ds1302_read_reg(0x83)); t->hour = bcd_to_dec(ds1302_read_reg(0x85) & 0x3F); t->day = bcd_to_dec(ds1302_read_reg(0x87)); t->month = bcd_to_dec(ds1302_read_reg(0x89)); t->week = bcd_to_dec(ds1302_read_reg(0x8B)); t->year = bcd_to_dec(ds1302_read_reg(0x8D)); }

秒寄存器的 bit7 是时钟启动位,写 0 才开始计时。如果你写进去的时候没清零这一位,芯片会处于停止状态,时间不走。这个坑我踩过一次,当时读出来时间一直是初始值,查了半天才发现是启动位没处理。

读的时候也要注意,秒寄存器的 bit7 读出来可能是 1,所以要用 0x7F 屏蔽掉。小时寄存器同理,24 小时模式下 bit7 和 bit6 都是 0,用 0x3F 屏蔽比较保险。

4.3 利用 31 字节 RAM 存参数

DS1302 的 31 字节 RAM 是个很实用的功能,地址从 0xC0 开始,每个字节一个地址,读写命令依次是 0xC0、0xC2、0xC4……一直到 0xFC。这些 RAM 靠后备电池供电,主电源掉了数据也不丢,可以拿来存设备序列号、校准系数、报警阈值之类的东西。

void ds1302_write_ram(uint8_t addr, uint8_t dat) { ds1302_write_reg(0x8E, 0x00); ds1302_write_reg(0xC0 + (addr << 1), dat); ds1302_write_reg(0x8E, 0x80); } uint8_t ds1302_read_ram(uint8_t addr) { return ds1302_read_reg(0xC1 + (addr << 1)); }

注意 RAM 的地址计算:第 0 个字节的写命令是 0xC0,第 1 个是 0xC2,所以是 0xC0 加上 addr 左移一位。读命令则是 0xC1 加上 addr 左移一位。这个规律跟时间寄存器是一样的,理解了命令字节的构造规则就不会搞错。

我在那个工业盒子里就用这 31 字节存了设备的校准系数和上次上报的时间戳,省了一颗 EEPROM,成本又降了一点。

4.4 主函数里的调用与显示

主函数里初始化完 GPIO 和 DS1302,就可以设置时间、读取时间了。我一般先判断一下芯片是否在走时,如果读出来的年份明显不对,比如是 0 或者 99,就说明电池没电或者芯片是新的,这时候需要重新设置时间。

int main(void) { ds1302_time_t now; ds1302_gpio_init(); ds1302_get_time(&now); if (now.year > 50) { // 年份异常,重新设置 ds1302_time_t init = {24, 6, 15, 6, 10, 30, 0}; ds1302_set_time(&init); } while (1) { ds1302_get_time(&now); printf("20%02d-%02d-%02d %02d:%02d:%02d week=%d\r\n", now.year, now.month, now.day, now.hour, now.minute, now.second, now.week); delay_ms(1000); } }

串口打印出来之后,对着电脑时间核对一下,如果差得不多,说明走时正常。如果差很多,先检查晶振和负载电容,再检查 BCD 转换有没有写错。

5. 常见问题与排查技巧实录

5.1 读出来全是 0xFF 或 0x00

这是最常见的问题,基本可以锁定在 I/O 引脚的输入输出切换上。如果你一直把 I/O 配成推挽输出,读的时候芯片没法把线拉低,STM32 读到的永远是高电平,结果就是 0xFF。解决办法就是读之前切成输入模式,读完了再切回输出。

还有一种情况是外部上拉电阻没接。开漏输出如果没有上拉,输出高电平时线是浮空的,读出来也不稳定。我一般会在 I/O 线上接一个 4.7k 到 10k 的上拉电阻到 3.3V,这样最稳妥。

5.2 写进去的时间读出来不对

先检查写保护有没有解除。DS1302 上电后写保护位可能是 1,这时候写操作会被忽略,但读操作正常,所以你会看到“写不进去但读得出来”的现象。每次写之前往 0x8E 写 0x00,写完再写 0x80,养成习惯。

如果写保护解除了还是不对,那就检查 BCD 转换。很多人写时间的时候直接写十进制数,比如 25 写成 0x19,读出来再转 BCD 就乱了。记住:写进去的必须是 BCD 码,读出来的也要先转成十进制再显示。

5.3 时间走得快或走得慢

走时精度问题基本都出在晶振和负载电容上。32.768kHz 晶振的负载电容要跟规格书匹配,常见的是 6pF 和 12.5pF 两种。如果你用的电容不对,走时就会偏。我实测过,电容偏大走时偏慢,电容偏小走时偏快,一天差个几秒到几十秒都有可能。

另外,晶振的布局也很重要。晶振要尽量靠近芯片的 X1、X2 引脚,走线要短,下面最好不要走其他信号线,否则会引入干扰。如果对精度要求高,可以在晶振下面铺一层地,起到屏蔽作用。

5.4 断电后时间丢失

这个问题要分两种情况。如果主电源掉了之后时间还能走,说明后备电池在起作用;如果时间直接归零,说明电池没接好或者没电了。先量一下电池电压,CR2032 新电池应该是 3V 左右,低于 2.5V 就该换了。

还有一种情况是电池有电但时间还是丢,那可能是 VCC1 和 VCC2 的切换电路有问题。检查一下电池座的正负极有没有接反,以及 VCC1 引脚有没有虚焊。我遇到过一块板子,电池座焊盘氧化了,接触电阻很大,芯片检测到的电池电压偏低,就不往电池切换,主电源一掉时间就没了。

5.5 常见问题速查表

现象可能原因排查方法
读出全 0xFFI/O 未切输入、无上拉检查模式切换和上拉电阻
读出全 0x00芯片未供电、RST 时序错量 VCC、检查 RST 拉高时机
写不进去写保护未解除写 0x8E 为 0x00
时间乱码BCD 转换错误检查转换函数
走时不准晶振电容不匹配换 6pF 或 12.5pF 电容
断电丢时间电池没电或接触不良量电池电压、检查焊点
时间不走秒寄存器 bit7 未清零写秒时与 0x7F

提示:调试 DS1302 的时候,最好手边有一台示波器或者逻辑分析仪。把 RST、SCLK、I/O 三根线同时抓下来,对着时序图看,问题一目了然。没有仪器的话,可以在代码里加串口打印,把每一步的中间值打出来,也能定位到大概位置。

6. 几个让我印象深刻的实操心得

6.1 延时函数别用系统滴答

我一开始图省事,在时序里用了 SysTick 的延时函数,结果发现读出来的数据时好时坏。后来才想明白,SysTick 的中断优先级如果比别的中断低,在时序关键的地方被中断打断,时钟边沿就错位了。DS1302 的时序虽然不快,但对边沿的完整性要求很高,中间被打断就容易出错。

解决办法有两个:一是用简单的空循环做微秒级延时,虽然不准但不会被中断打断;二是在时序操作期间关掉全局中断,操作完了再打开。我一般用第一种,写个delay_us函数,循环几十次,实测下来很稳。

void delay_us(uint16_t us) { uint16_t i; while (us--) { for (i = 0; i < 8; i++); // 根据主频调整 } }

这个循环次数要根据你的主频来调。72MHz 的 STM32,8 次循环大概 1 微秒左右。你可以用示波器量一下,或者用翻转 GPIO 的方式校准。

6.2 初始化顺序有讲究

DS1302 上电之后,我习惯先拉低 RST 和 SCLK,延时一小会儿,再开始第一次通信。这个“冷启动”的等待时间大概 10 毫秒左右,让芯片内部的电源稳定下来。如果上电就立刻通信,有时候第一个命令会丢,读出来全是 0。

另外,如果你在初始化的时候要设置时间,记得先解除写保护,设置完再恢复。我见过有人设置完时间忘了恢复写保护,结果程序跑着跑着时间被意外改写了,查了好久才发现。

6.3 电池选型别贪便宜

后备电池我推荐用 CR2032 这种锂锰电池,标称 3V,容量 200mAh 左右,DS1302 的待机电流只有几百纳安,一颗电池能用好几年。千万别用可充电的 LIR2032,虽然电压也是 3V,但如果你不小心开了涓流充电,充电电流控制不好会把电池充坏。

还有,电池座要选那种弹片接触可靠的,别用太便宜的。我吃过亏,一块板子用了劣质电池座,现场震动之后电池接触不良,时间就丢了。后来换成带卡扣的电池座,问题再没出现过。

6.4 读时间的频率别太高

DS1302 的通信速度不快,一次完整的读时间大概要几百微秒。如果你在主循环里不停地读,会占用不少 CPU 时间。我的做法是每秒读一次,或者用定时器触发读取,读完存到一个全局变量里,其他地方直接用这个变量。这样既保证了时间的实时性,又不影响主循环的其他任务。

如果你需要更精确的时间戳,可以在两次读取之间用 SysTick 做插值,但要注意 DS1302 本身只有秒级精度,插值也只能到毫秒级,再细就没意义了。

6.5 关于开源和分享

这个项目的代码我已经整理成独立的驱动文件,放到自己的代码仓库里了。写学习笔记的好处就是,过一段时间回头看,能发现自己当时理解不到位的地方。比如我第一版代码里,读字节的函数没有先移位再采样,导致读出来的数据位序是反的,当时调了好久才找到原因。现在把这些坑记下来,下次再用就能直接避开。

如果你也在做类似的项目,建议把驱动层和应用层分开,驱动层只负责读写寄存器和时序,应用层负责时间格式转换和业务逻辑。这样换一颗 RTC 芯片,比如换成 DS3231,只需要改驱动层,应用层基本不用动。这个习惯在项目做多了之后会越来越觉得值。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 1:02:21

嵌入式Linux驱动开发实战:从环境搭建到面试表达

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/21 1:01:51

STM32C5与CubeMX2深度解析:新芯片定位、工具迁移与实战配置

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/21 1:01:38

工业领域技术岗位JD的关键词提取与权重建模方法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/21 1:01:24

嵌入式Linux与安卓驱动开发实战:从字符设备到HAL层完整项目指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/21 0:59:08

PDFsam Basic 4.2.12:稳定可靠的PDF批量处理开源工具

简介&#xff1a;本资源为PDFsam 4.2.12桌面端PDF处理工具的完整安装包&#xff0c;面向办公人员、学术研究者及PDF高频使用者&#xff0c;解决PDF文档合并、拆分、页面提取、旋转与混合编排等核心管理需求。压缩包共232个文件&#xff0c;含53个jar&#xff08;核心功能模块&a…

作者头像 李华
网站建设 2026/10/21 0:48:52

C#调用OpenVINO+ByteTrack实现工业级多目标追踪

简介&#xff1a;本资源是一个面向C#开发者与计算机视觉初学者的实战型目标检测与追踪Demo&#xff0c;聚焦于在Windows平台下用C#集成OpenVINO推理引擎部署YOLOv8模型&#xff0c;并结合ByteTrack实现多目标实时追踪&#xff0c;适用于智能安防、工业巡检等需低延迟响应的边缘…

作者头像 李华