STM32外接DS1302实时时钟芯片驱动从原理到代码详解
有不少玩 STM32 的朋友最开始做带时间的项目第一反应都是去翻参考手册用芯片内置 RTC。折腾一圈下来你会发现内置 RTC 要配备份域、要理清外部低速晶振和校准逻辑稍不留意读回来的时间就对不上。后来我干脆把目光转向外挂 RTC 芯片DS1302 就这样进入了我常用的清单。DS1302 是一颗非常经典的实时时钟芯片三根线就能通信写一个驱动大概百来行代码结构直观、便宜作为学习 GPIO 模拟时序的入门项目再合适不过。这篇学习笔记会把 DS1302 的驱动从原理到代码完整拆一遍通信协议为什么这样设计、为什么我用 GPIO 模拟而不是硬件 SPI、寄存器操作和 BCD 码换算怎么写、上电实测会遇到哪些坑最后整理成一套可以直接抄走或改着玩的开源驱动代码。正在学 STM32、想搞懂外设驱动原理又不想被芯片手册劝退的朋友应该能从里面找到不少有用的东西。1. 为什么 DS1302 值得写一遍学习笔记芯片特性和选型理由1.1 外挂 DS1302 与内置 RTC、DS3231 之间的取舍先说结论DS1302 不是精度最高的 RTC也不是功能最全的但它是我见过的驱动逻辑最适合学习的一颗。内置 RTC 省了外设成本可它通常挂在备份电源域上涉及 LSE 晶振、校准、备份寄存器乱七八糟一堆配置。调试环境稍有波动就会出现上电时间归零、走时异常的情况排查成本一点都不低。DS3231 那种温补晶振方案精度确实高但价格差不多是 DS1302 的几倍甚至十倍而且 I2C 通信虽然简单对比学习的价值来说没那么“裸”。DS1302 则完全靠一根双向数据线移位读写底层时序规则清晰正好适合拿来理解“为什么芯片手册里要写那么多个上升沿和下降沿”。如果你只是想快速做出一个比别人走得准的时钟DS3231 可能更省事如果你想搞懂三线协议、学会从一个字节开始驱动一颗芯片DS1302 会更适合练手。1.2 引脚功能与最小系统接线DS1302 的引脚不算多但双电源设计是它一个很有特色的点。它有 VCC1 和 VCC2 两个电源脚VCC2 接主电源VCC1 可以接备用电池。正常工作电压 2.0V 到 5.5V所以接 3.3V 或者 5V 都能跑。当主电源掉电后芯片会自动切换到 VCC1 供电这样断电后时间还能继续走。常见的 DS1302 模块基本都预留了电池座或者有一个供你焊接电池的地方。我平时在 F103 上接的默认引脚是 PA0、PA1、PA2具体对应关系如下表DS1302 引脚接到 STM32说明VCC23.3V 或 5V主电源GNDGND共地VCC1纽扣电池正极或悬空备份电源掉电保持时间CEPA0片选使能高有效SCLKPA1串行时钟输入I/OPA2双向数据线X1/X232.768kHz 晶振模块上一般已焊好X1 和 X2 之间要接一个 32.768kHz 晶振。如果买的是集成模块晶振已经在板子上焊好了不需要额外接线。自己搭电路的话还要注意匹配负载电容对此后面精度校准部分再展开。1.3 为什么用 GPIO 模拟时序而不是硬件 SPIDS1302 的三线接口从外观上看很像 SPI不少人拿现成的硬件 SPI 模块去驱动结果发现不好使。原因在于它并不是标准的 SPI。DS1302 要求 CE 在整个通信期间保持高电平命令字节和数据字节在同一个片选窗口内连续完成传输而且后面还有 burst 模式这种扩展帧。标准 SPI 的 NSS 和字节边界管理在这里反而碍手碍脚。再加上 DS1302 的通信速率很低单个时钟周期只要几百纳秒到几微秒GPIO 翻转速度完全够用。用 GPIO 模拟可以精确控制每一拍的建立时间、采样点和 CE 拉高拉低的时机出了问题也容易通过逻辑分析仪定位。所以我在驱动里直接用两个普通 GPIO 加一根双向 IO效果非常稳定。2. 通信底层在干什么CE、SCLK、I/O 三根线的节奏配合2.1 三根线各自的职责把 DS1302 通信想象成一次门岗查验CE 就是门禁CE 拉低时芯片完全不理会外部数据CE 拉高才表示“开始干活”SCLK 是门口保安手里的节拍器每抬一下手就检查一次I/O 是通行证递送窗口递一张证一个 bit配合一下节拍。整个通信过程确实就是一位一位地移数据。在 CE 为高的整个期间主控通过 SCLK 产生时钟脉冲同时在 I/O 线上按位发送或接收数据。时钟线没有动作时I/O 线上的数据不会被芯片关心。这也是为什么有时候你看到代码里先拉低 SCLK、再改 IO、再拉高 SCLK本质上就是“先把数据放到窗口再让保安看一眼”。2.2 命令字节地址和读写方向是如何打包的DS1302 每次操作的第一步都是先发送一个 8 位命令字节。这个字节最高位固定为 1接下来 6 位是寄存器地址最低位是读写控制位0 表示写、1 表示读。所以同样是操作秒寄存器写秒的地址是 0x80读秒的地址是 0x81。按这种规律每个寄存器都对应一对读写命令。比如写分钟用 0x82读分钟用 0x83写小时用 0x84读小时用 0x85。Burst 模式的命令则是 0xBE 写、0xBF 读后者一次可连续传输整组时间数据。低位的读写方向位放在命令字节的最后一位正是前面说的 LSB first 传输顺序。2.3 写字节的时序数据先放好再给上升沿写数据字节时主控在 SCLK 的每个上升沿把 I/O 线上的数据锁存进芯片。因此每一拍的正确顺序是先把 SCLK 拉低准备好 I/O 电平保持一小段时间再把 SCLK 拉高芯片就采到了这一位。这里最容易犯的错是数据还没稳定就开始给上升沿。GPIO 翻转本身有延迟编译器优化程度不同也会影响延迟所以代码里要在 I/O 赋值之后、SCLK 拉高之前加一点延时。延时不需要很大实测 1 微秒左右就非常稳了。2.4 读字节的时序下降沿输出低电平期间采样读数据和写数据的方向相反。命令字节仍然是在 SCLK 上升沿被芯片接收但读命令发送完毕后DS1302 会在随后的每一个 SCLK 下降沿把一位数据从 I/O 脚输出。主控可以在 SCLK 低电平期间读取 I/O因为此时数据已经稳定也可以等 SCLK 下一次上升沿之前读只要保证在有效窗口内就行。所以我的读字节函数里方向切换成输入之后每一拍的顺序是先拉低 SCLK制造一个下降沿延时让芯片把数据输出到 I/O然后读取引脚电平再把 SCLK 拉高。由于 DS1302 的 I/O 输出不是标准三态高阻模式STM32 内部上拉并不会干扰它放心开上拉输入就好了。3. 寄存器地图与 BCD 编码驱动代码的核心逻辑3.1 DS1302 的寄存器分布DS1302 内部除了一堆控制寄存器之外还带了 31 字节掉电保持 RAM 可以用来存用户数据这一点在同类芯片里相对少见。不过学习阶段先关注时间寄存器就够用常见寄存器映射如下功能写命令读命令数据范围或说明秒0x800x8100~59bit7 为时钟暂停位 CH分0x820x8300~59时0x840x8500~23 或 01~12日0x860x8701~31月0x880x8901~12星期0x8A0x8B01~07年0x8C0x8D00~99写保护0x8E0x8Fbit7 为 WP 写保护位充电0x900x91涓流充电配置时钟突发0xBE0xBF一次连续读写 7 字节时间数据秒寄存器里的 bit7 就是 CH 位。这个位写 1 会把振荡器关掉写 0 才让时钟正常运行。很多人设置完时间发现秒不走十有八九是 CH 位没处理对。写保护寄存器里 WP 位写 1 后芯片会忽略所有写操作所以在修改时间前先要关掉写保护。3.2 BCD 码为什么读回 0x59 而不是 59DS1302 的时间寄存器内部存的是 BCD 码每一个字节的高 4 位代表十位低 4 位代表个位。比如要表示 59 秒寄存器里存的是 0x59十六进制显示出来是 59但这并不等于十进制的 59。如果直接把读数用%d打印59 秒会显示成 89 这样的怪数字。换算公式不复杂十进制转 BCD等于“十位左移四位再按位或个位”。BCD 转十进制就是“高位乘以 10 再加低位”。驱动代码里对应两个小函数后面会写出来。强烈建议把换算封装成固定函数不要在业务代码里到处手写(bcd 4) * 10 (bcd 0x0F)容易漏括号。3.3 特殊控制位逐个说清楚除了 CH 和 WP小时寄存器的 bit7 也需要注意。这个位为 1 时芯片进入 12 小时制bit5 表示上午或下午为 0 时是 24 小时制。我写驱动时默认强制使用 24 小时制设置在 0~23 的小时值不会有歧义。如果项目里读出来的时间突然出现“下午 3 点这种偏移”先检查小时寄存器的高位是不是被写成了 1。充电寄存器 0x90 是另一个隐蔽的坑。它默认应该保持 0x00如果需要给备用法拉电容充电才按手册设置充电电流。某些模块默认配置或误操作会意外开启充电如果 VCC1 接的是不可充电的纽扣电池长期过充可能导致电池漏液损坏板子。稳妥做法是初始化时显式写一次 0x00除非你确实要充电。3.4 单个寄存器的读写函数单个寄存器的读写函数是整个驱动的基础所有操作都逃不开这两个函数。直接看代码static void DS1302_Enter(void) { HAL_GPIO_WritePin(DS1302_CE_GPIO_PORT, DS1302_CE_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(DS1302_SCLK_GPIO_PORT, DS1302_SCLK_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(DS1302_IO_GPIO_PORT, DS1302_IO_PIN, GPIO_PIN_RESET); DS1302_DelayUs(2); HAL_GPIO_WritePin(DS1302_CE_GPIO_PORT, DS1302_CE_PIN, GPIO_PIN_SET); DS1302_DelayUs(4); } static void DS1302_Exit(void) { HAL_GPIO_WritePin(DS1302_CE_GPIO_PORT, DS1302_CE_PIN, GPIO_PIN_RESET); DS1302_DelayUs(2); HAL_GPIO_WritePin(DS1302_SCLK_GPIO_PORT, DS1302_SCLK_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(DS1302_IO_GPIO_PORT, DS1302_IO_PIN, GPIO_PIN_RESET); } static uint8_t DS1302_ReadReg(uint8_t addr) { uint8_t dat; DS1302_Enter(); DS1302_WriteByte(addr | 0x01); dat DS1302_ReadByte(); DS1302_Exit(); return dat; } static void DS1302_WriteReg(uint8_t addr, uint8_t dat) { DS1302_Enter(); DS1302_WriteByte(addr); DS1302_WriteByte(dat); DS1302_Exit(); }注意地址参数传入的是基地址读的时候在函数内部自动把最低位置 1这样写驱动的上层代码看起来更干净。4. 驱动代码逐段解读从底层时序函数到时间读写接口4.1 GPIO 宏定义与 IO 方向切换移植这套代码时只需要改头文件里的引脚宏。为了让同一根 IO 既能输出命令又能输入数据驱动里必须提供输入输出方向切换函数。在 HAL 库中可以这样写#define DS1302_CE_GPIO_PORT GPIOA #define DS1302_SCLK_GPIO_PORT GPIOA #define DS1302_IO_GPIO_PORT GPIOA #define DS1302_CE_PIN GPIO_PIN_0 #define DS1302_SCLK_PIN GPIO_PIN_1 #define DS1302_IO_PIN GPIO_PIN_2 static void DS1302_IO_AsOutput(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DS1302_IO_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(DS1302_IO_GPIO_PORT, GPIO_InitStruct); } static void DS1302_IO_AsInput(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; GPIO_InitStruct.Pin DS1302_IO_PIN; GPIO_InitStruct.Mode GPIO_MODE_INPUT; GPIO_InitStruct.Pull GPIO_PULLUP; HAL_GPIO_Init(DS1302_IO_GPIO_PORT, GPIO_InitStruct); }方向切换不是随便切的。命令字节发完之后要等最后一个 SCLK 上升沿过去DS1302 才会接管 IO 线输出数据。所以我在DS1302_ReadByte函数开头才切输入模式之前保持输出模式发命令时机正好。4.2 字节级读写函数一字节的读写是驱动最底层逻辑分别实现 8 个 bit 的移位操作。写字节时每次循环先拉低 SCLK把对应位放到 IO 上再拉高 SCLK。读字节时先切输入模式每个循环先拉低 SCLK 触发芯片输出下一位读引脚再拉高 SCLK。static void DS1302_WriteByte(uint8_t dat) { uint8_t i; DS1302_IO_AsOutput(); for (i 0; i 8; i) { HAL_GPIO_WritePin(DS1302_SCLK_GPIO_PORT, DS1302_SCLK_PIN, GPIO_PIN_RESET); if (dat 0x01) HAL_GPIO_WritePin(DS1302_IO_GPIO_PORT, DS1302_IO_PIN, GPIO_PIN_SET); else HAL_GPIO_WritePin(DS1302_IO_GPIO_PORT, DS1302_IO_PIN, GPIO_PIN_RESET); dat 1; DS1302_DelayUs(2); HAL_GPIO_WritePin(DS1302_SCLK_GPIO_PORT, DS1302_SCLK_PIN, GPIO_PIN_SET); DS1302_DelayUs(2); } } static uint8_t DS1302_ReadByte(void) { uint8_t i, dat 0; DS1302_IO_AsInput(); for (i 0; i 8; i) { HAL_GPIO_WritePin(DS1302_SCLK_GPIO_PORT, DS1302_SCLK_PIN, GPIO_PIN_RESET); DS1302_DelayUs(2); dat 1; if (HAL_GPIO_ReadPin(DS1302_IO_GPIO_PORT, DS1302_IO_PIN) GPIO_PIN_SET) dat | 0x80; HAL_GPIO_WritePin(DS1302_SCLK_GPIO_PORT, DS1302_SCLK_PIN, GPIO_PIN_SET); DS1302_DelayUs(2); } DS1302_IO_AsOutput(); return dat; }DS1302 的数据和命令都是 LSB first也就是先传最低位。写函数里每次都判断dat 0x01然后右移发完一个字节后自然是从 bit0 到 bit7 的顺序。读函数正好相反第一次读到的位被放到 bit7再不断右移最终还原成一个完整字节。4.3 时间结构体与设置、读取接口为了让上层调用更友好我定义了一个时间结构体所有时间成员都用十进制表示内部的 BCD 转换在驱动边界完成。这样业务代码里不用关心芯片内部的编码方式。typedef struct { uint8_t year; uint8_t month; uint8_t day; uint8_t week; uint8_t hour; uint8_t min; uint8_t sec; } DS1302_Time_t; static uint8_t DS1302_BinToBcd(uint8_t value) { return ((value / 10) 4) | (value % 10); } static uint8_t DS1302_BcdToBin(uint8_t value) { return (value 4) * 10 (value 0x0F); }设置时间的流程有一点讲究。直接逐个写时间寄存器而不停振荡器可能出现写入过程中内部时间跳到下一秒导致秒和分钟数据不一致。稳妥做法是先把秒寄存器的 CH 位写成 1 停振再写其他寄存器最后把 CH 位清零启动振荡器。void DS1302_SetTime(DS1302_Time_t *time) { DS1302_WriteReg(DS1302_ADDR_CTRL, 0x00); DS1302_WriteReg(DS1302_ADDR_YEAR, DS1302_BinToBcd(time-year)); DS1302_WriteReg(DS1302_ADDR_WEEK, DS1302_BinToBcd(time-week)); DS1302_WriteReg(DS1302_ADDR_MONTH, DS1302_BinToBcd(time-month)); DS1302_WriteReg(DS1302_ADDR_DAY, DS1302_BinToBcd(time-day)); DS1302_WriteReg(DS1302_ADDR_HOUR, DS1302_BinToBcd(time-hour)); DS1302_WriteReg(DS1302_ADDR_MIN, DS1302_BinToBcd(time-min)); DS1302_WriteReg(DS1302_ADDR_SEC, DS1302_BinToBcd(time-sec) | 0x80); DS1302_WriteReg(DS1302_ADDR_SEC, DS1302_BinToBcd(time-sec)); DS1302_WriteReg(DS1302_ADDR_CTRL, 0x00); }注意写法先写一个带 0x80 的秒值是停振再写不带 0x80 的秒值是启动。年份只支持后两位比如 2025 年在代码里存 25读取后再自行加上基准年。读取时间可以直接挨个读寄存器但驱动里我更推荐用一个连续读取的小技巧先读一次如果秒值跳变再重读一次。因为读 7 个寄存器需要一段时间万一读取过程中秒进位可能读到 59 秒但是分钟已经是下一位数据就自相矛盾了。void DS1302_GetTime(DS1302_Time_t *time) { uint8_t sec_tmp; do { time-sec DS1302_BcdToBin(DS1302_ReadReg(DS1302_ADDR_SEC)); time-min DS1302_BcdToBin(DS1302_ReadReg(DS1302_ADDR_MIN)); time-hour DS1302_BcdToBin(DS1302_ReadReg(DS1302_ADDR_HOUR)); time-day DS1302_BcdToBin(DS1302_ReadReg(DS1302_ADDR_DAY)); time-month DS1302_BcdToBin(DS1302_ReadReg(DS1302_ADDR_MONTH)); time-week DS1302_BcdToBin(DS1302_ReadReg(DS1302_ADDR_WEEK)); time-year DS1302_BcdToBin(DS1302_ReadReg(DS1302_ADDR_YEAR)); sec_tmp DS1302_BcdToBin(DS1302_ReadReg(DS1302_ADDR_SEC)); } while (sec_tmp ! time-sec); }这种方案逻辑简单实测也够用。如果希望一次传输保证整组时间来自同一个通信帧可以使用时间突发模式。4.4 Burst 模式一次读完所有时间寄存器突发模式用命令 0xBF在 CE 为高的同一帧内连续读 7 个字节。这样可以减少多次拉高 CE 带来的时间窗口风险但需要注意芯片输出的顺序是固定的秒、分、时、日、月、星期、年。void DS1302_GetTime_Burst(DS1302_Time_t *time) { uint8_t buf[7]; DS1302_Enter(); DS1302_WriteByte(0xBF); for (uint8_t i 0; i 7; i) buf[i] DS1302_ReadByte(); DS1302_Exit(); time-sec DS1302_BcdToBin(buf[0] 0x7F); time-min DS1302_BcdToBin(buf[1] 0x7F); time-hour DS1302_BcdToBin(buf[2] 0x3F); time-day DS1302_BcdToBin(buf[3] 0x3F); time-month DS1302_BcdToBin(buf[4] 0x1F); time-week DS1302_BcdToBin(buf[5] 0x07); time-year DS1302_BcdToBin(buf[6]); }我在这里对每个字节都做了掩码过滤只保留有效位。秒的 CH 位、小时的 12/24 小时标志位、月份的高位标志都可能干扰显示提前清掉更安全。突发写模式的地址是 0xBE写之前同样要先关闭写保护写完后最好再恢复写保护。5. 上电实测最容易翻车的几个细节5.1 读回来的数据全是 0xFF 或 0x00 怎么办这是 DS1302 驱动最常见的首跑故障。数据全是 0xFF通常是 IO 引脚悬空或者方向切换没生效读成了一个没有驱动的高阻电平。数据全是 0x00多半是 CE 压根没被芯片认可芯片一直处于关闭状态IO 被内部结构拉低或者接线本身就不对。我的排查顺序一般是先量 CE、SCLK、I/O 三根线是不是都在主控和模块之间确实连通然后把逻辑分析仪接在 SCLK 和 I/O 上看命令字节发出去之后有没有波形最后再检查 IO 方向切换时机。很多人在 ReadByte 函数里先切输入没错但如果不小心切得太早命令字节还没发完IO 线上的电平就会被 DS1302 误读。方向切换必须在写命令和读数据边界上严格分开。5.2 时间设置成功但秒数就是不走代码写进去后秒数纹丝不动第一检查项是秒寄存器的 CH 位。如果你写秒时传入了带 0x80 的值振荡器就被暂停了。驱动里写完时间后必须再把 CH 位清零否则时间永远是静止的。第二检查项是晶振。DS1302 的 32.768kHz 晶振如果虚焊或者质量不行芯片一样不工作。还有一个容易被忽略的现象DS1302 刚上电时振荡器启动需要一点时间可能约 1 秒左右才会有秒跳动。如果你上电后立刻读时间看到秒不变不要急着怀疑驱动稍等一两秒再读。若晶振引脚接触不良导致起了振但幅度不够秒位还是会隔几秒跳一次或者走时明显偏慢此时用示波器量一下晶振引脚波形最直观。5.3 读出的数字乱码显示成 0x59 样式乱码问题几乎都是 BCD 换算没做。直接把寄存器里的 0x59 当十进制整数 printf显示成 89 是必然的。反过来设置时间时如果把十进制 59 直接写进寄存器芯片会把它当成 BCD 码 0x59也就是十进制的 89 秒时间自然完全不正确。所以我强烈建议所有 BCD 换算都收敛在驱动层应用层拿到的一定是十进制时间。5.4 12 小时制和 24 小时制的混乱小时寄存器在 12 小时制下和 24 小时制下的数据位定义不一样。如果你在设置时间时没管高位芯片可能处于 12 小时制下午 3 点读回来就成了 3 这种看起来很正常的数但实际它代表的是 15 点后面判断逻辑全错。我在驱动里初始化时固定用 24 小时制并且读写小时时都对高位做掩码处理。一旦发现时间显示总是和真实时间差 12 小时优先查这个位。6. 让驱动真正稳定运行主循环调度与晶振精度6.1 主循环中怎么合理调用读取接口驱动写好之后调用方式很自由。最简单的主循环逻辑是延时后反复调用DS1302_GetTime然后通过串口打印。但我不建议在主循环里高频读取芯片本身每一帧通信要几十微秒100 次循环读一次和 1 次循环读一次差别不大反而白白占用 IO 和 CPU。一般 500 毫秒到 1 秒读一次就足够了。更进阶一点的做法是用一个秒级定时器或者 RTC 闹钟中断去触发读取再把时间数据放到一个共享结构体供主循环显示。这样即使主循环里在处理耗时任务时间采集也不会被拖住。即使不用中断只要保证读取函数一次性执行完并且加了连续读校验数据一致性也是有保障的。6.2 连续读校验的实现细节前面代码里已经用了连续读校验先读一组数据再读一次秒寄存器如果两次秒值不一致说明读取过程中发生过秒跳变整组数据作废重读。这个思路简单可靠不需要引入突发模式也能保证数据一致性。如果你使用了突发模式因为 7 个字节是在同一帧内连续读的本质上已经消除了多次片选之间的时间差但一次帧读本身也可能跨秒只不过概率小很多。要求更高时可以突发读两次再比较秒值。6.3 DS1302 的精度问题与校准思路坦率说DS1302 的走时精度非常依赖外部晶振。普通 32.768kHz 晶振在室温下一天误差几秒甚至几十秒都是正常的。如果项目对时间精度有一定要求我建议从这几个方向入手。硬件方面检查晶振的负载电容是否匹配。晶振规格书里通常会写负载电容比如 12.5pFPCB 上就要用合适容量的对地电容。DS1302 某些模块设计得比较随意电容不匹配会造成频率偏差。你也可以用带微调电容的电路配合一段时间观测去微调。软件方面最简单有效的方案是定期用网络时间校时或者在系统里记录一个漂移系数计算出每天大约快几秒后每天固定时间回拨一次。这个补偿值通常需要在真实环境下跑一两天才能测准不过对于大多数学习项目能稳定走时且误差在可接受范围内就已经够用了。如果精度要求真的很高建议换带温度补偿晶振的 RTC 方案DS1302 更适合低成本入门和原理学习。说实话DS1302 的驱动写到这个程度整套代码也就两三百行却把 GPIO 模拟时序、移位寄存器操作、BCD 码、寄存器读写、双电源备份这些嵌入式基础概念全串起来了。我后来写其他外设驱动遇到 I2C、SPI 芯片时很多思路都是从这个项目迁移过去的。这套代码从协议到工程实现都足够稳定如果你需要修改引脚或者移植到其它型号的单片机只需要替换头文件里的宏定义和 GPIO 初始化函数即可。最后分享一个小经验如果你用的是带电池座的 DS1302 模块首次上电设置完时间之后最好把写保护寄存器清零或者恢复为关闭状态然后测量一下 VCC1 和 GND 之间的电压确认电池确实能给芯片供电。我遇到过很多次“断电后时间丢失”的情况排查到最后都是电池座氧化或者电池根本没夹紧。这些小细节比驱动代码本身更容易让人掉头发。