首页 / 资讯中心 / 文章详情

STM32驱动DS1302实战:三线接口、BCD码与写保护详解

STM32驱动DS1302实战:三线接口、BCD码与写保护详解 ★ FEATURED ARTICLE
1. 为什么还要折腾一颗“老掉牙”的时钟芯片如果你手头有一块 STM32 的开发板想给它加一个“断电也能记住时间”的功能大概率会在选型表里撞见 DS1302 这个名字。它便宜、好买、资料多几乎每个电子爱好者的入门盒子里都躺着几颗。但真上手写驱动的时候很多人会卡在几个地方三线接口怎么模拟、BCD 码怎么换算、写保护怎么解、时间读出来为什么总是乱跳。这篇笔记就是把我自己从零把 DS1302 跑通的全过程摊开来讲。核心关键词是STM32 驱动 DS1302涉及三线串行接口、BCD 码转换、写保护寄存器、涓流充电这几个关键点。它解决的是“MCU 掉电后时间丢失”这个具体问题适合刚接触 STM32 外设驱动、想拿一个简单芯片练手 GPIO 时序的朋友。读完你至少能拿到一份可以直接编译、烧录、跑起来的驱动代码并且知道每一行代码为什么这么写。DS1302 是那种“看起来简单细节全是坑”的芯片。它的数据手册只有十几页但如果你不把时序图看仔细读出来的秒数可能永远是 85 或者 165 这种鬼数字。我一开始就踩了这个坑后来才发现是 BCD 码和二进制搞混了。所以这篇笔记不会只给你代码我会把每个寄存器的含义、每条时序的意图、每个参数的计算过程都讲清楚。2. 先搞清楚 DS1302 到底是个什么东西2.1 芯片的基本能力和引脚定义DS1302 是一颗实时时钟芯片能计秒、分、时、日、月、周、年并且自带 31 字节的静态 RAM。它最讨喜的地方是宽电压范围 2.0V 到 5.5V跟 STM32 的 3.3V 系统可以直接对接不需要电平转换。主电源和备用电池通过内部电路自动切换主电源掉电后自动切到纽扣电池继续走时功耗低到微安级别。它的通信接口不是标准的 I2C 或 SPI而是一个三线串行接口CE片选/使能、I/O数据线、SCLK串行时钟。注意这个 I/O 是双向的读的时候是输入写的时候是输出所以 STM32 这边必须把对应的 GPIO 配置成开漏输出或者动态切换输入输出模式。我一开始图省事直接配成推挽输出结果读数据死活读不到后来改成开漏加上拉电阻才正常。引脚方面VCC1 接备用电池VCC2 接主电源GND 接地。X1 和 X2 接 32.768kHz 晶振这个频率是 2 的 15 次方经过 15 级分频正好得到 1Hz 的秒信号。CE 是使能脚低电平的时候芯片处于待机状态高电平才允许通信。SCLK 上升沿写数据下降沿读数据这个方向一定要记牢否则时序全乱。2.2 寄存器地图和 BCD 码的坑DS1302 的寄存器地址是 8 位最低位是读写方向位0 表示写1 表示读。比如秒寄存器的写地址是 0x80读地址就是 0x81。这一点跟很多芯片不一样很多人第一次写会直接把 0x80 当成读地址用结果当然读不出来。时间寄存器一共 7 个从秒到年地址依次是 0x80、0x82、0x84、0x86、0x88、0x8A、0x8C写地址。每个寄存器的高 4 位是十位低 4 位是个位存储格式是BCD 码。比如秒数是 45寄存器里存的是 0x45而不是二进制的 0x2D。如果你直接把 45 这个十进制数写进去读出来就会变成 0x45 对应的 BCD 值也就是 69 秒完全对不上。我踩过的坑就在这里第一次读秒串口打印出来是 85我以为是芯片坏了后来才反应过来 0x85 的 BCD 解析是 85 秒但秒数不可能超过 59说明我读到的根本不是秒寄存器而是地址错位了。所以写驱动之前一定要把地址表和 BCD 转换函数准备好。2.3 写保护寄存器和涓流充电寄存器地址 0x8E 是写保护寄存器最高位 WP 为 1 时禁止写入任何时间寄存器为 0 时才允许写。芯片上电默认是写保护的所以你必须先往 0x8E 写 0x00 解除保护才能设置时间。设置完之后建议再写回 0x80 重新上锁防止程序跑飞误改时间。地址 0x90 是涓流充电寄存器用来控制备用电池的充电方式。如果你用的是不可充电的纽扣电池这个寄存器必须保持 0x00否则可能损坏电池。如果你用的是可充电的法拉电容或者充电电池可以配置成二极管加电阻的充电模式。具体配置是高 4 位固定为 0x5低 4 位选择二极管和电阻。比如 0xA5 表示选择一个二极管和一个 2k 电阻0xA6 表示一个二极管和一个 4k 电阻。这个参数要根据电池的充电电流来算一般纽扣电池不需要充电直接写 0x00 最省事。3. 硬件连接和 GPIO 配置的实操细节3.1 三线接口的硬件接法我用的是一块常见的 STM32F103 开发板DS1302 模块是某宝上几块钱的那种自带 32.768kHz 晶振和纽扣电池座。接线很简单VCC2 接 3.3VGND 接地CE 接 PA0I/O 接 PA1SCLK 接 PA2。备用电池用的是 CR2032电压 3V正好在 DS1302 的允许范围内。这里有个细节DS1302 的 I/O 引脚是开漏结构必须外接一个上拉电阻通常 4.7k 到 10k 都可以。我一开始没加上拉读出来的数据全是 0xFF后来在 I/O 和 3.3V 之间焊了一个 10k 电阻立刻正常。如果你买的模块上已经带了上拉电阻就不用额外加了但最好用万用表量一下确认。另外晶振的负载电容也很关键。DS1302 要求 32.768kHz 晶振的负载电容是 6pF如果你随便拿一个 12.5pF 的晶振走时可能会偏快或偏慢。我实测过用 12.5pF 的晶振一天大概快 3 到 5 秒换成 6pF 之后一天误差在 1 秒以内。所以如果你对走时精度有要求晶振和电容一定要按手册选。3.2 GPIO 模式配置和方向切换STM32 的 GPIO 配置成开漏输出模式输出速度选 2MHz 就够了因为 DS1302 的 SCLK 频率最高也就 2MHz 左右实际用 1MHz 以下更稳。CE 和 SCLK 可以配成推挽输出因为它们只做输出。I/O 引脚需要动态切换方向写数据的时候配成开漏输出读数据的时候配成浮空输入或者上拉输入。我写了一个宏来切换方向#define DS1302_IO_IN() { GPIOA-CRL 0xFFFFFF0F; GPIOA-CRL | 0x00000040; } #define DS1302_IO_OUT() { GPIOA-CRL 0xFFFFFF0F; GPIOA-CRL | 0x00000030; }这里 0x40 表示浮空输入0x30 表示开漏输出。注意 CRL 寄存器的低 8 位控制 PA0 到 PA7每个引脚占 4 位。PA1 对应的是第 4 到第 7 位所以掩码是 0xFFFFFF0F。这个配置只适用于 STM32F1 系列如果你用的是 F4 或者 H7寄存器结构不一样需要用 HAL 库的GPIO_Init函数重新配置。提示切换方向之前最好先把 I/O 引脚拉高避免在切换瞬间产生毛刺。我一般会在写操作结束后把 I/O 置高然后再切回输入模式。3.3 电源和电池的注意事项DS1302 的 VCC1 接备用电池VCC2 接主电源。当 VCC2 大于 VCC1 加 0.2V 时芯片由 VCC2 供电当 VCC2 掉电自动切到 VCC1。这个切换是内部自动完成的不需要软件干预。但要注意如果你用的备用电池电压低于 2.0V芯片可能无法正常走时。CR2032 的标称电压是 3V但用到后期会降到 2.5V 左右这时候就要考虑更换了。还有一个坑如果你在焊接的时候把 VCC1 和 VCC2 接反了芯片可能会直接烧掉。我有个朋友就是这么干的上电之后芯片发烫赶紧断电但已经晚了。所以焊接前一定要对照模块上的丝印确认 VCC1 和 VCC2 的位置。4. 驱动代码的完整实现和逐行解析4.1 底层时序函数写一个字节和读一个字节DS1302 的通信协议是CE 拉高之后先发送一个命令字节最低位是读写方向然后跟着发送或接收数据字节。数据在 SCLK 的上升沿被写入在下降沿被读出。所以写数据的时候先准备好 I/O 电平然后拉高 SCLK再拉低 SCLK读数据的时候先拉低 SCLK然后读取 I/O 电平再拉高 SCLK。写一个字节的函数void DS1302_WriteByte(uint8_t dat) { DS1302_IO_OUT(); for (uint8_t i 0; i 8; i) { DS1302_IO dat 0x01; DS1302_SCLK 1; DS1302_SCLK 0; dat 1; } }注意这里是低位先发因为 DS1302 的命令字节和数据字节都是 LSB 优先。我一开始按 MSB 优先写结果地址完全错位读出来的数据毫无规律。读一个字节的函数uint8_t DS1302_ReadByte(void) { uint8_t dat 0; DS1302_IO_IN(); for (uint8_t i 0; i 8; i) { dat 1; if (DS1302_IO) dat | 0x80; DS1302_SCLK 1; DS1302_SCLK 0; } return dat; }这里有个细节读的时候要先右移再判断这样第一次读到的位会落在最低位最后一次读到的位会落在最高位。如果你先判断再右移顺序就反了。我实测过先判断再右移的话读出来的秒数会是原来的位反转值比如 0x45 会变成 0xA2完全不对。4.2 寄存器读写函数和 BCD 转换写寄存器的函数void DS1302_WriteReg(uint8_t addr, uint8_t dat) { DS1302_CE 0; DS1302_SCLK 0; DS1302_CE 1; DS1302_WriteByte(addr); DS1302_WriteByte(dat); DS1302_CE 0; }读寄存器的函数uint8_t DS1302_ReadReg(uint8_t addr) { uint8_t dat; DS1302_CE 0; DS1302_SCLK 0; DS1302_CE 1; DS1302_WriteByte(addr); dat DS1302_ReadByte(); DS1302_CE 0; return dat; }注意读地址是写地址加 1比如秒的写地址是 0x80读地址就是 0x81。这个在调用的时候要手动加或者封装一个宏。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); }这两个函数看起来简单但如果你忘了转换写进去的时间就会完全乱套。比如你想设置 23 分直接写 23 进去寄存器里存的是 0x17读出来 BCD 解析是 17 分差了 6 分钟。所以每次读写时间寄存器都要过一遍这两个函数。4.3 时间设置和读取的完整流程设置时间的流程是先解除写保护然后依次写入秒、分、时、日、月、周、年最后重新上锁。注意秒寄存器的最高位是 CH 位为 1 时时钟停止为 0 时正常走时。所以写秒的时候要确保最高位是 0。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_WriteReg(0x8E, 0x00); DS1302_WriteReg(0x80, DEC2BCD(sec) 0x7F); DS1302_WriteReg(0x82, DEC2BCD(min)); DS1302_WriteReg(0x84, DEC2BCD(hour)); DS1302_WriteReg(0x86, DEC2BCD(day)); DS1302_WriteReg(0x88, DEC2BCD(month)); DS1302_WriteReg(0x8A, DEC2BCD(week)); DS1302_WriteReg(0x8C, DEC2BCD(year)); DS1302_WriteReg(0x8E, 0x80); }读取时间的流程类似只是地址要加 1并且读出来的数据要过 BCD2DEC。void DS1302_GetTime(uint8_t *year, uint8_t *month, uint8_t *day, uint8_t *week, uint8_t *hour, uint8_t *min, uint8_t *sec) { *sec BCD2DEC(DS1302_ReadReg(0x81) 0x7F); *min BCD2DEC(DS1302_ReadReg(0x83)); *hour BCD2DEC(DS1302_ReadReg(0x85)); *day BCD2DEC(DS1302_ReadReg(0x87)); *month BCD2DEC(DS1302_ReadReg(0x89)); *week BCD2DEC(DS1302_ReadReg(0x8B)); *year BCD2DEC(DS1302_ReadReg(0x8D)); }这里读秒的时候要跟 0x7F 做与运算把 CH 位屏蔽掉否则如果时钟停了读出来的秒数会带上最高位变成 80 多秒。5. 调试过程中遇到的坑和排查方法5.1 读出来的数据全是 0xFF 或者 0x00这是最常见的问题通常有三个原因。第一I/O 引脚没有上拉电阻开漏输出无法拉高读到的永远是低电平。解决办法是在 I/O 和 VCC 之间加一个 4.7k 到 10k 的上拉电阻。第二CE 引脚没有正确拉高芯片根本没有进入通信状态。用万用表量一下 CE 在通信期间是不是高电平。第三SCLK 频率太高DS1302 的时序要求 SCLK 高电平至少 250ns低电平至少 250ns如果你用 72MHz 的主频直接翻转 GPIO周期可能只有几十纳秒芯片反应不过来。解决办法是在 SCLK 翻转之间加几个 NOP 延时或者降低 GPIO 的输出速度。我一开始就是 SCLK 太快读出来的数据全是 0xFF后来在 SCLK 拉高和拉低之间各加了 10 个 NOP立刻正常。所以如果你也遇到这个问题先降速试试。5.2 时间走时不准一天差好几秒这个问题多半是晶振的负载电容不对。DS1302 要求 6pF 的负载电容如果你用的是 12.5pF 的晶振走时就会偏快。我实测过12.5pF 的晶振一天快 3 到 5 秒换成 6pF 之后一天误差在 1 秒以内。另外晶振的走时精度还受温度影响如果你在户外使用温度变化大的话误差会更大。这种场合建议用 DS3231它自带温度补偿精度比 DS1302 高一个数量级。还有一个可能备用电池电压不足。如果 VCC1 低于 2.0V芯片可能无法正常走时或者走时断断续续。用万用表量一下电池电压低于 2.5V 就换掉。5.3 写入时间后读出来没变化这种情况通常是写保护没有解除。DS1302 上电默认是写保护的你必须先往 0x8E 写 0x00才能写入时间寄存器。如果你忘了这一步写入操作会被忽略读出来的还是旧时间。另外写完之后建议重新上锁往 0x8E 写 0x80防止程序跑飞误改时间。还有一种可能你写入的地址不对。比如你想写分寄存器地址应该是 0x82如果你写成 0x83那就是读地址写入操作会无效。所以每次写之前确认地址是偶数写地址读地址是奇数。5.4 常见问题速查表现象可能原因排查方法解决办法读数据全是 0xFFI/O 无上拉、CE 未拉高、SCLK 太快量 I/O 电平、量 CE 电平、示波器看 SCLK加上拉电阻、检查 CE 时序、加 NOP 延时时间走时不准晶振负载电容不对、电池电压低查晶振规格、量电池电压换 6pF 晶振、换电池写入后读不到变化写保护未解除、地址写错检查 0x8E 写入值、检查地址奇偶先写 0x00 解除保护、确认写地址是偶数秒数读出来大于 59CH 位未屏蔽、BCD 转换错误检查读秒后是否与 0x7F、检查 BCD 函数加 0x7F、确认 BCD2DEC 正确芯片发烫VCC1 和 VCC2 接反对照丝印检查接线断电重新焊接注意如果你用的是 3.3V 的 STM32DS1302 的 VCC2 也接 3.3V不要接 5V虽然 DS1302 支持 5V但 I/O 电平会变成 5V可能损坏 STM32 的引脚。如果你必须用 5V需要在 I/O 线上加电平转换电路。6. 一些进阶玩法和个人经验6.1 用备用 RAM 存储配置参数DS1302 除了时间寄存器还有 31 字节的静态 RAM地址从 0xC0 到 0xFD写地址。这些 RAM 在主电源掉电后只要备用电池有电数据就不会丢失。你可以把一些不经常改的配置参数比如报警阈值、设备 ID、校准偏移量存在这里。读写方法和时间寄存器一样只是地址不同。我一般会把一个简单的校验和存在 RAM 的最后一个字节上电的时候先读出来校验如果校验失败说明 RAM 数据不可靠就重新写入默认值。这样比用 STM32 内部的 Flash 模拟 EEPROM 简单得多而且不需要擦除操作写入速度也快。6.2 涓流充电的配置计算如果你用的是可充电的纽扣电池比如 LIR2032可以配置涓流充电。涓流充电寄存器的低 4 位选择二极管和电阻具体组合如下低 4 位值二极管数量电阻值0x112k0x214k0x318k0x522k0x624k0x728k充电电流的计算公式是I (VCC2 - Vdiode - Vbattery) / R。假设 VCC2 是 3.3V二极管压降 0.7V电池电压 3.0V选 2k 电阻那么 I (3.3 - 0.7 - 3.0) / 2000 -0.2mA负值说明电池电压高于充电电压根本充不进去。所以涓流充电适合 VCC2 是 5V 的场合3.3V 系统基本用不上。如果你非要用建议选 2k 电阻并且确认电池电压低于 2.5V 再充。6.3 个人踩坑体会我前后一共焊了三块 DS1302 模块第一块因为 I/O 没加上拉调了一晚上没通第二块因为晶振负载电容不对走时一天快 5 秒第三块才完全正常。所以如果你刚开始调不要怀疑自己的代码先检查硬件上拉电阻、晶振电容、电池电压、接线顺序。这四个地方对了代码只要时序没问题基本一次就能通。另外DS1302 的时序对 SCLK 的占空比没有严格要求但高低电平的持续时间必须大于 250ns。如果你用 HAL 库的HAL_GPIO_WritePin函数函数本身的执行时间可能就超过 250ns所以不需要额外加延时。但如果你用寄存器直接操作翻转速度太快就必须加 NOP。我一般会在 SCLK 拉高和拉低之间各加 5 个 NOP实测在 72MHz 主频下很稳。最后再分享一个小技巧如果你读出来的时间偶尔跳变比如秒数从 45 直接跳到 47可能是读操作被中断打断了。解决办法是在读时间的时候关掉全局中断读完再打开。或者连续读两次如果两次结果一致才采用不一致就重读。这个技巧在干扰比较大的场合特别有用。
阅读完成 · 觉得有帮助?
咨询建站