最近把手里一块 DS1302 模块重新驱动了一遍,顺便把之前零散的笔记整理成了这篇完整的学习记录。标题写得很直白:STM32 驱动 DS1302,开源、学习笔记。如果你正在用 STM32 做时钟、定时记录、数据日志,或者单纯想搞明白“GPIO 模拟时序读芯片”是怎么回事,这篇笔记应该能帮你省不少时间。DS1302 是一颗很经典的串行实时时钟芯片,三线接口、价格便宜、外围电路简单,特别适合学习阶段拿来做“从引脚到寄存器再到应用”的完整闭环。我会从芯片选型、硬件接线、通信协议、代码实现一直写到真实调试中遇到的问题,最后再聊聊怎么把学习笔记整理成开源项目。
1. 为什么用 DS1302:选型思考与硬件准备
1.1 和主流 RTC 芯片比,DS1302 到底强在哪
很多初学者拿到 DS1302 的第一反应是“这颗芯片太老了”,但做产品选型不是追新,而是看需求匹配度。把市面上常见的几颗 RTC 芯片放在一起对比,你会发现 DS1302 的定位非常清晰:低功耗、低价格、低接口复杂度。
| 芯片 | 接口 | 典型精度 | 电池备份 | 价格区间 | 适合场景 |
|---|---|---|---|---|---|
| DS1302 | 三线串行(CE/SCLK/IO) | 月误差几十秒到几分钟 | 支持 | 低 | 低成本时钟、定时记录 |
| DS3231 | I2C | 温补精度高(年误差几秒) | 支持 | 较高 | 需要高精度的时钟设备 |
| PCF8563 | I2C | 月误差几十秒 | 支持 | 低 | 低压低功耗场景 |
这里的“精度”要特别说明一句:DS1302 本身依赖外部 32.768kHz 晶振,晶振的频率偏差和环境温度直接决定走时误差,所以它并不是一颗高精度时钟芯片。如果你的项目是“做个电子钟”,或者记录“最近一次操作时间”,DS1302 完全够用;如果要做计费、精密日志、网络授时备份,那还是加钱上温补芯片吧。
我选 DS1302 还有一个更实际的原因:接口简单。它只需要三根控制线,不占用 I2C 总线,也不涉及设备地址冲突,用普通 GPIO 就能驱动,非常适合做“底层协议学习”的教材。相比之下 I2C 芯片还需要处理时钟拉伸、应答位这些概念,对刚入门的人来说信息量偏大。
1.2 模块引脚与 STM32 接线
市面上的 DS1302 模块大同小异,核心引脚就 8 个:VCC2、VCC1、GND、X1、X2、CE、SCLK、IO。其中 X1 和 X2 是晶振引脚,通常模块上已经焊好了 32.768kHz 晶振和两个负载电容,不需要自己处理。模块如果带电池座,还会引出 VCC1 作为备用电源输入。
接线表如下(这是一组我实测稳定的接法,你也可以根据自己板子的引脚布局调整):
| DS1302 引脚 | STM32 引脚 | 说明 |
|---|---|---|
| VCC2 | 3.3V | 主电源,也可以接 5V,但注意模块上是否有 LDO |
| VCC1 | 3.3V | 备用电源,接电池正极时会悬空 |
| GND | GND | 共地是必须的 |
| CE | PB0 | 片选,高电平有效 |
| SCLK | PB1 | 串行时钟 |
| IO | PB2 | 数据线,开漏输出+外部上拉 |
这里有一个很关键的细节:IO 数据线必须接上拉电阻。模块上一般会做这个电阻,如果你是自己搭的电路,千万别省。IO 是双向引脚,读数据时 DS1302 会驱动它输出“0”或“1”,写数据时 STM32 要驱动它,两种情况都需要一个确定的电平基准。开漏输出模式下,如果上拉电阻缺失,写高电平的时候引脚就挂在“高阻”上,读回来的数据自然就是乱的。
1.3 为什么不用硬件 SPI 而是 GPIO 模拟
DS1302 的三线接口长得像 SPI,但并不是标准 SPI,主要有三个区别:
- 数据只有一根线,是半双工双向通信,而 SPI 的 MISO 和 MOSI 是分开的。
- 命令字节和数据字节都是低位在前(LSB first),SPI 通常是高位在前。
- 时序里有片选信号 CE 的独立控制逻辑,和 SPI 的 CS 拉低时序不完全一致。
所以用 STM32 的硬件 SPI 去适配 DS1302 会很别扭,要做各种 bit 序反转、方向切换,代码复杂度反而高。GPIO 模拟则完全按数据手册的时序图走,逻辑一目了然,出问题也容易排查。这也是为什么这类芯片的驱动代码普遍是 GPIO 模拟风格。
2. 读懂通信机制:命令字节、寄存器与时序
2.1 命令字节就是一把钥匙
DS1302 的每一次操作,不管是读还是写,都从发送一个命令字节开始。命令字节的位结构如下:
| 位 | 内容 |
|---|---|
| bit7 | 固定为 1 |
| bit6~bit1 | 寄存器地址(A5~A1) |
| bit0 | 读写方向:0=写,1=读 |
举个例子,秒寄存器的地址是 0x80(二进制 1000 0000),那么读秒的操作就要发送 0x80 | 0x01 = 0x81,写秒则发送 0x80 & 0xFE = 0x80。很多人在这一步搞混,写代码时把读命令写成了 0x81,写命令也写成 0x81,结果芯片一直处于读模式,时间怎么都写不进去。
另一个容易忽略的点是:所有字节都是低位先发送。也就是说,发送 0x81,实际在线上被先传出去的位是 bit0=1,然后才是 0、0、0、0、1、0、0。这个位序问题在调试时用逻辑分析仪一看就明白,但如果只看代码容易懵,我建议把“低位在前”直接注释在代码头部。
2.2 寄存器表:读时间要碰哪些地址
DS1302 的时间寄存器非常规整,秒、分、时、日、月、星期、年,按顺序排开,每个寄存器的读地址和写地址正好差 1(bit0 不同)。
| 寄存器 | 写地址 | 读地址 | 取值范围 | 备注 |
|---|---|---|---|---|
| 秒 | 0x80 | 0x81 | 00~59 | bit7 为时钟暂停标志 CH |
| 分 | 0x82 | 0x83 | 00~59 | |
| 时 | 0x84 | 0x85 | 01~12 或 00~23 | 取决于 12/24 小时位 |
| 日 | 0x86 | 0x87 | 01~31 | |
| 月 | 0x88 | 0x89 | 01~12 | |
| 星期 | 0x8A | 0x8B | 01~07 | |
| 年 | 0x8C | 0x8D | 00~99 | 不包含世纪 |
| 写保护 | 0x8E | 0x8F | 0x00/0x80 | bit7=WP |
| 涓流充电 | 0x90 | 0x91 | 0x00~0xFF | 按需配置 |
这些寄存器存储的时间值全部是 BCD 码,不是十进制直接存储。比如秒寄存器的值 0x45 代表 45 秒,而不是十进制的 45。BCD 编码在 8 位寄存器里分成高 4 位和低 4 位,高 4 位代表十位,低 4 位代表个位。代码里做转换只需要两次位移和一次乘法,后面会写转换函数。
秒寄存器的 bit7 是个大坑。这个位叫 CH(Clock Halt),写 0 表示振荡器启动,写 1 表示停止。模块出厂或者芯片复位后,这个位可能是 1,表现出来的现象就是“时间永远停在初始值”。如果你初始化代码只写了时间、没把 CH 位清 0,时钟就是不走。我自己的代码里写时间函数会强制把秒寄存器的 bit7 清零,这也是最稳妥的做法。
2.3 完整读写过程的时序理解
一次写操作的过程是:
- CE 拉高,开始通信。
- 发送命令字节(bit0=0)。
- 紧接着发送一个数据字节。
- CE 拉低,通信结束。
一次读操作的过程是:
- CE 拉高。
- 发送命令字节(bit0=1)。
- 读取一个数据字节。
- CE 拉低。
关于时序,我用一句话概括:CE 高电平期间才有意义,写命令和数据都在 SCLK 上升沿被 DS1302 锁存,读数据时 DS1302 在 SCLK 下降沿更新 IO 上的电平,所以主机应该在 SCLK 为低电平的时间段去采样 IO。只要抓住“上升沿写、下降沿切数据、低电平读”这个核心逻辑,写代码就不会跑偏。
还有一个细节:CE 在整个命令字节和数据字节传输过程中必须保持高电平,不能在字节之间去拉低它。比如“发完命令,拉低 CE,再发数据”这种操作是错的,DS1302 会把一次通信截断成两次。
3. 从零写的 STM32 驱动:代码细节与分层
3.1 引脚配置:开漏输出比推挽输出省心
GPIO 的初始化是第一个容易出问题的点。CE 和 SCLK 是纯输出,配推挽输出没问题;IO 是双向数据线,我推荐配成开漏输出模式,并开启内部上拉或者外部加上拉电阻。
为什么要开漏?因为在开漏模式下,IO 引脚仍然可以输出低电平,但要输出高电平时引脚会变成高阻,由外部上拉电阻把电平拉高,这时候读取 GPIO 的输入数据寄存器(IDR)就能读到 DS1302 驱动出来的电平。这样一来就不需要频繁地在输入模式和输出模式之间切换,读写函数可以共用一个引脚配置。
初始化代码示意(HAL 库写法):
#define DS1302_CE_PORT GPIOB #define DS1302_CE_PIN GPIO_PIN_0 #define DS1302_SCLK_PORT GPIOB #define DS1302_SCLK_PIN GPIO_PIN_1 #define DS1302_IO_PORT GPIOB #define DS1302_IO_PIN GPIO_PIN_2 static void DS1302_GPIO_Init(void) { GPIO_InitTypeDef gpio = {0}; __HAL_RCC_GPIOB_CLK_ENABLE(); /* CE 和 SCLK:推挽输出 */ gpio.Pin = DS1302_CE_PIN | DS1302_SCLK_PIN; gpio.Mode = GPIO_MODE_OUTPUT_PP; gpio.Pull = GPIO_NOPULL; gpio.Speed = GPIO_SPEED_FREQ_HIGH; HAL_GPIO_Init(DS1302_CE_PORT, &gpio); /* IO:开漏输出,内部上拉 */ gpio.Pin = DS1302_IO_PIN; gpio.Mode = GPIO_MODE_OUTPUT_OD; gpio.Pull = GPIO_PULLUP; HAL_GPIO_Init(DS1302_IO_PORT, &gpio); HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_RESET); HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_RESET); }如果你用的是标准外设库或者 LL 库,思路完全一样:输出开漏、使能内部上拉、CE/SCLK 推挽输出。这里再强调一句:内部上拉在很多 STM32 上只有 30k~50k 左右,对于 DS1302 的 IO 线来说通常是够用的,但如果你处于强干扰环境,最好再外接一个 10k 电阻,可靠性更高。
3.2 微秒延时:不要用 HAL_Delay
DS1302 对时序的要求不算苛刻,数据手册里 SCLK 的高低电平时间最小是几百纳秒量级,但做驱动时我会留足余量,把时钟周期拉到微秒级,按 72MHz 主频随便跑都不出问题。关键是不能用 HAL_Delay,因为它最小粒度是 1ms,太慢了,每次读写都要等这么久,通信效率低到没法看。
uint32_t DWT_Delay_Init(void) { CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk; DWT->CYCCNT = 0; DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk; return 0; } void DWT_Delay_us(uint32_t us) { uint32_t start = DWT->CYCCNT; uint32_t ticks = us * (SystemCoreClock / 1000000); while ((DWT->CYCCNT - start) < ticks); }用 DWT 的 CYCCNT 做微秒延时,精度高、不占用定时器,也很适合其他需要短延时的传感器驱动。初始化时记得先调用一次 DWT_Delay_Init(),否则读出来是 0,延时直接失效。
3.3 核心读写函数:字节是基础单元
DS1302 的底层操作可以抽象成两个函数:写一个字节、读一个字节。写字节的代码:
static void DS1302_WriteByte(uint8_t dat) { for (uint8_t i = 0; i < 8; i++) { HAL_GPIO_WritePin(DS1302_IO_PORT, DS1302_IO_PIN, (dat & 0x01) ? GPIO_PIN_SET : GPIO_PIN_RESET); HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_SET); DWT_Delay_us(1); HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_RESET); DWT_Delay_us(1); dat >>= 1; } }每次循环先放数据到 IO,然后把 SCLK 拉高产生上升沿,DS1302 在这个上升沿锁存数据,之后拉低 SCLK。注意每次放的都是dat & 0x01,也就是最低位,发完之后dat >>= 1,把下一位移到最低位,这正好实现“低位在前”。
读字节的代码:
static uint8_t DS1302_ReadByte(void) { uint8_t dat = 0; for (uint8_t i = 0; i < 8; i++) { HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_SET); DWT_Delay_us(1); HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_RESET); DWT_Delay_us(1); if (HAL_GPIO_ReadPin(DS1302_IO_PORT, DS1302_IO_PIN) == GPIO_PIN_SET) { dat |= (0x01 << i); } } return dat; }读的过程是:SCLK 先拉高再拉低,产生下降沿,DS1302 在下降沿之后把下一位数据放到 IO 上,我们在 SCLK 低电平期间采样。代码里把读引脚的时机放在拉低之后,就是遵循这个时序。这里我用了dat |= (0x01 << i)而不是dat >>= 1加判断,因为低位在前的顺序下,第一次读到的应该放在 bit0,第二次放在 bit1,直接移位最清晰。
3.4 寄存器读写封装与时间结构体
有了字节读写函数,寄存器级操作就顺理成章了:
static void DS1302_WriteReg(uint8_t reg, uint8_t dat) { HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_SET); DS1302_WriteByte(reg & 0xFE); // bit0 = 0,写方向 DS1302_WriteByte(dat); HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_RESET); } static uint8_t DS1302_ReadReg(uint8_t reg) { uint8_t dat = 0; HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_SET); DS1302_WriteByte(reg | 0x01); // bit0 = 1,读方向 dat = DS1302_ReadByte(); HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_RESET); return dat; }这两个函数里 CE 的拉高拉低位置很关键。通信开始前 CE 拉高,通信结束后 CE 拉低。不要在命令字节和数据字节之间动 CE。命令字节里的reg & 0xFE和reg | 0x01分别用于写和读方向,外面传进来的 reg 直接用寄存器的基础地址,比如 0x80 表示秒寄存器,读的时候函数内部自动转成 0x81。
然后是 BCD 码转换和时间结构体:
typedef struct { uint8_t year; // 00-99 uint8_t month; // 01-12 uint8_t day; // 01-31 uint8_t week; // 01-07 uint8_t hour; // 00-23 uint8_t minute; // 00-59 uint8_t second; // 00-59 } DS1302_Time_t; #define BCD_TO_DEC(x) ((((x) >> 4) * 10) + ((x) & 0x0F)) #define DEC_TO_BCD(x) ((((x) / 10) << 4) | ((x) % 10)) void DS1302_GetTime(DS1302_Time_t *time) { time->second = BCD_TO_DEC(DS1302_ReadReg(0x80) & 0x7F); time->minute = BCD_TO_DEC(DS1302_ReadReg(0x82)); time->hour = BCD_TO_DEC(DS1302_ReadReg(0x84)); time->day = BCD_TO_DEC(DS1302_ReadReg(0x86)); time->month = BCD_TO_DEC(DS1302_ReadReg(0x88)); time->week = BCD_TO_DEC(DS1302_ReadReg(0x8A)); time->year = BCD_TO_DEC(DS1302_ReadReg(0x8C)); } void DS1302_SetTime(DS1302_Time_t *time) { DS1302_WriteReg(0x8E, 0x00); // 清写保护 DS1302_WriteReg(0x80, DEC_TO_BCD(time->second) & 0x7F); // CH 位清零 DS1302_WriteReg(0x82, DEC_TO_BCD(time->minute)); DS1302_WriteReg(0x84, DEC_TO_BCD(time->hour)); DS1302_WriteReg(0x86, DEC_TO_BCD(time->day)); DS1302_WriteReg(0x88, DEC_TO_BCD(time->month)); DS1302_WriteReg(0x8A, DEC_TO_BCD(time->week)); DS1302_WriteReg(0x8C, DEC_TO_BCD(time->year)); DS1302_WriteReg(0x8E, 0x80); // 重新打开写保护,防止误改 }读秒寄存器时& 0x7F是为了把 CH 位屏蔽掉,避免秒的高位出现异常数值。写秒寄存器时DEC_TO_BCD(time->second) & 0x7F是保证 CH 位清零的同时不改变秒的数据。这两个屏蔽操作虽然看似微不足道,却是时钟能不能正常走的关键。
3.5 主程序演示:串口打印时间
主程序的逻辑很简单:初始化 DWT,初始化 GPIO,设置一次初始时间(也可以在代码里用条件编译控制),然后循环读时间并通过串口打印。串口只是观察手段,换成 OLED 或者把数据写到 SD 卡日志里也一样。
int main(void) { HAL_Init(); SystemClock_Config(); DWT_Delay_Init(); DS1302_GPIO_Init(); UART_Init(); DS1302_Time_t now = { .year = 25, .month = 6, .day = 15, .week = 7, .hour = 21, .minute = 30, .second = 0 }; DS1302_SetTime(&now); while (1) { DS1302_GetTime(&now); printf("%02d-%02d-%02d %02d:%02d:%02d week=%d\r\n", now.year + 2000, now.month, now.day, now.hour, now.minute, now.second, now.week); HAL_Delay(1000); } }如果你不想每次上电都重新设时间,可以把DS1302_SetTime这段改成条件编译,只在第一次烧录时执行。我自己的做法是:用一个 GPIO 按键作为“设置时间”的触发入口,平时按一下才会写时间。这样日常掉电后重新上电,时间是保持住的。
4. 调试实录:四类高频问题与完整排查链路
4.1 现象一:读出来的时间全是 0xFF 或乱码
这个问题在串口打印里非常典型:秒、分、时读出来全是 255 或者莫名其妙的大数。第一次遇到时很容易怀疑芯片坏了,但绝大多数情况不是芯片问题,而是时序或逻辑问题。
我的排查顺序是:
- 先用万用表量 CE、SCLK、IO 三个引脚在静止状态下的电平。正常情况是 CE 低、SCLK 低、IO 高(上拉)。
- 再看接线是否共地。VCC2 和 GND 都供电了,但 STM32 和模块的地不是同一个,就会导致信号电平基准不一致。
- 然后看命令字节方向。确认读操作发送的是
reg | 0x01而不是reg。如果发送 0x80 给芯片,DS1302 会认为这是一次写操作,后面的读时序根本不成立。 - 最后用逻辑分析仪抓 SCLK 和 IO。重点看 IO 是否有被拉低的动作,如果 IO 一直是高电平,说明芯片根本没有驱动数据线,很可能是命令字节根本没被正确锁存。
还有一个容易翻车的地方:很多人习惯把 CE 拉高和 SCLK 拉高同时做。DS1302 要求 CE 拉高后经过一段时间再产生时钟,我建议在DS1302_WriteByte之前加一个DWT_Delay_us(2)左右的小延时,给芯片一点准备时间。实测这个延时加上之后,读出来的数据稳定很多。
4.2 现象二:时间能写进去,但时钟不走
刚写完时间时,串口打印出来的数据和设置值一致,但过一分钟再看,秒数一点没变。这个现象在 DS1302 里基本就两个原因:
第一,秒寄存器的 CH 位是 1。如果你写秒寄存器时直接把 BCD 值整个写进去,没有做& 0x7F处理,而原来寄存器里 CH 位恰好是 1,那芯片就会一直处于振荡器暂停状态。解决办法就是写秒时强制把最高位清零。
第二,晶振没起振。晶振起振需要时间,如果模块上电到写入时间之间间隔太短,芯片还没来得及完成振荡器启动,你读到的就是初始值。可以等 1 秒后再读。如果等了很久还是不走,就要怀疑晶振本身或者负载电容问题了。DS1302 的 32.768kHz 晶振对负载电容比较敏感,模块上一般配 6pF 电容,如果你自己打板,不要随意换大容量的电容,否则会停振。
判断晶振是否工作最简单的办法是拿示波器看 X1 引脚,正常能看到 32.768kHz 的正弦波形。没有示波器的话,可以连续读秒寄存器:如果秒值永远不变,基本就是振荡器停振了。
4.3 现象三:掉电后时间复位,备用电池没生效
DS1302 掉电后靠 VCC1 维持走时。如果模块上装了 CR2032 电池,但每次断电再上电时间都回到初始状态,通常是两个原因:
一是 VCC1 没有真正连到电池正极。有些模块的电池座在出厂时没有焊接跳线,或者电池正负极方向装反了。CR2032 正极是平整的那面,测量一下 VCC1 对 GND 的电压,有 2V 以上才算正常。
二是电池座旁边有一个二极管,模块在“主电源断电”时通过它切换到备用电源。如果二极管损坏或者虚焊,备用电源路径就断了。这种情况万用表查起来很快,断电状态测 VCC1 电压就行。
这里还要提醒一句:DS1302 的涓流充电寄存器(0x90)默认是关闭的,不建议在没有完全搞清楚充电电路的情况下开启它。如果你拿可充电的 LIR2032 放上去,又错误地配置了充电参数,可能会过冲损坏电池。我的习惯是:VCC1 上直接接 CR2032 一次性电池,0x90 寄存器保持 0x00 不动。
4.4 现象四:仿真软件里时间不走,上板子反而正常
很多初学者喜欢先在仿真软件里跑,结果发现 DS1302 仿真模型不走时,以为是代码问题。实际上,仿真软件里的晶振模型默认可能不是 32.768kHz,或者仿真模型的参数设置被忽略了。最有效的做法是查看一下 DS1302 器件属性,把 X1/X2 的晶振频率显式设置为 32768Hz。
仿真环境还有另一个坑:某些版本的模型对时序要求比真实芯片严格,为了兼容,我通常会把延时从 1us 提高到 5us。这个改动在真实芯片上完全没问题,在仿真器上也能更稳定。如果你发现代码在仿真里怎么都不对,先把延时拉大试试,这可能是耗时最短的排查手段。
5. 学习笔记的开源化:文件结构、README 与后续扩展
5.1 代码分层:驱动不要和应用混在一起
写 DS1302 驱动是嵌入式里很好的“代码分层”练习。最小系统是:GPIO 初始化、字节读写、寄存器读写、时间结构体转换,这四层应该放到独立的驱动文件中,不要把串口打印、按键处理、OLED 显示的逻辑塞进来。分层清晰之后,换一块板子只需要改引脚宏定义,其他代码原样保留。
我在整理这份开源代码时用了这样的目录结构:
ds1302_driver/ ├── README.md ├── LICENSE ├── doc/ │ ├── wiring.png │ └── debug_notes.md ├── inc/ │ ├── ds1302.h │ └── bcd.h └── src/ ├── ds1302.c └── bcd.c头文件ds1302.h里只暴露必要的 API:初始化、设置时间、读取时间、时间结构体定义。底层字节读写函数放在.c文件里用static修饰,不让外部直接调用。这样做的好处是用户只需要关心“设置时间”和“获取时间”,不用管底层时序怎么实现,也意味着后续优化时序时不会影响上层代码。
5.2 README 里写什么:接线、示例、还是坑
开源仓库的 README 是别人看到你项目的第一个窗口。一份合格的 README 至少要包含:硬件接线表、编译环境说明、一个最小示例、常见问题链接。
我在 README 里放了“快速开始”代码段,用户复制进去就能跑出时间打印。还要在 README 里标注清楚:芯片精度受外部晶振影响,走时误差可能达到每月几十秒,不适合高精度场景。这样既不夸大项目能力,也能帮使用者提前判断这颗芯片是否适合他们的应用。
许可证方面,学习笔记类开源项目我推荐选择 MIT 协议。理由很简单:它允许别人自由使用、修改和商用,只需要保留版权声明。你如果希望代码被更多人用到,MIT 是最省事的许可证。
5.3 从一个 RTC 驱动延伸到更完整的时钟系统
驱动 DS1302 只是第一步,写这篇笔记的过程让我意识到,围绕它展开的学习空间其实很大。
如果想让这个项目更实用,可以加一个掉电时间存储的应用:在系统正常运行时定期把当前时间写入 DS1302,并在上电时比较 RTC 时间与时间戳,解决“上次运行位置”的问题。还能把时间通过串口输出成标准时间协议格式,方便上位机校准。
更进一步,可以把整个驱动从 HAL 库移植到 LL 库或者寄存器操作,体会不同抽象层对代码风格的影响;也可以把 GPIO 模拟的方法推广到其他类似时序的芯片,比如温湿度传感器、单总线协议芯片。学会了“看时序图写代码”这个能力,以后遇到再冷门的芯片都不会怕。
我个人的体会是:DS1302 驱动不难,但它是把“数据手册、时序图、GPIO 操作、BCD 编码、调试工具”串起来的极佳例子。从原理到代码,再到真实板子上的排障,每一步都能学到东西。这份笔记整理成开源项目后,以后有新人问“RTC 怎么驱动”,我就可以直接把这套代码和文档甩过去,比自己从头讲解要高效得多。