ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

STM32手写DS1302驱动:时序、BCD码与工程实践详解

STM32手写DS1302驱动:时序、BCD码与工程实践详解 1. 为什么要自己写一个DS1302驱动接触STM32的朋友迟早会遇到一个需求系统要记录时间而且断电之后不能丢。很多人第一反应是直接用芯片内部的RTC但STM32的RTC要么需要外部低速晶振和纽扣电池配合要么在调试频繁烧录的情况下容易把备份域搞乱对刚入门的朋友来说并不算友好。DS1302这颗芯片在嵌入式圈子里经久不衰走的是三线串行接口外部挂一个32.768kHz晶振再加一颗纽扣电池就能独立走时主芯片掉电后RTC照常运行。它的驱动逻辑非常典型命令字节、读写时序、寄存器地址映射这些概念在后续学SPI、I2C的时候也用得上。所以我一直觉得手写一遍DS1302驱动是性价比很高的练习项目既能解决实际的时间记录需求又能把“软件模拟时序”这套基本功练扎实。这篇笔记适合已经有STM32基础会建工程、会点灯但没写过复杂外设驱动的朋友。我会从芯片手册的关键点讲起再给出完整驱动代码和接线方式最后把我在实际调试中踩过的坑和排查方法一并整理出来。所有代码放在GitHub上MIT协议可自由取用。2. 整体方案选型与实践思路2.1 为什么选DS1302而不是其他时钟芯片市面上常见的时钟芯片主要是DS1302、DS3231、PCF8563这三类。DS3231是I2C接口带温度补偿精度高但价格也高一截PCF8563同样是I2C接口走时精度一般DS1302是三线串口逻辑最简单价格最低性能足够日常用。对于学习来说DS1302的三线时序比I2C更容易观察和调试因为两根数据线和一根时钟线看一眼逻辑分析仪就能明白整个通信过程。还有一点很关键DS1302的读写控制完全靠命令字节完成这意味着它的驱动代码可以做到非常短却包含了“写命令—读数据/写数据”的完整通信范式。这种简单性对初学者非常友好可控性和可读性都很好。2.2 软件模拟时序还是硬件SPIDS1302的三线接口和SPI很像但又不完全是标准的SPI协议。它在每次读写前要先发送一个命令字节然后紧接着进行一字节的读或写读和写的模式由命令字节的第0位决定。直接用STM32的硬件SPI外设去驱动它会比较别扭因为SPI外设的时序约束和DS1302的要求不完全匹配反而要频繁调整CPOL、CPHA。我采用的是软件模拟时序方案用三个普通GPIO分别模拟时钟线、数据线、片选线。这个方案有几个实际好处不占用SPI外设资源引脚可以随便选布线灵活。时序完全由代码控制读写模式可以随时调整排查问题方便。逻辑清晰每一句代码对应一条时序非常适合学习。软件模拟时序的坏处是CPU占用高但对于毫秒级的时钟读写操作来说完全没有性能压力。后续如果要做低功耗或者频繁读取时间再考虑硬件SPI也不迟。2.3 开源项目的组织方式这个项目我采用了“最小可编译工程独立驱动模块”的结构。注意驱动模块和业务逻辑是分开的ds1302.c和ds1302.h只管时钟芯片的读写不关心主控用什么系统、怎么显示、怎么处理按键。这样做的好处是以后换主控芯片或者换RTOS驱动文件可以直接搬过去用。Project/ ├── Core/ │ ├── Inc/ │ └── Src/ │ ├── main.c │ └── ... ├── Drivers/ │ └── BSP/ │ ├── ds1302.c │ ├── ds1302.h │ └── ... └── README.md3. DS1302核心原理与寄存器映射3.1 芯片引脚和最小系统DS1302一共有8个引脚常用的是VCC2、VCC1、GND、X1、X2、SCLK、I/O、CE有的手册写作RST。接线要点VCC2接主电源即STM32开发板的3.3V。VCC1接备用电池正极常用CR2032纽扣电池负极接地。X1和X2之间接32.768kHz晶振这是RTC走时的时基来源。SCLK接STM32的时钟输出脚I/O接数据脚CE接片选脚。这里有个容易踩的坑DS1302官方手册标注的电源范围较宽但实际使用中3.3V供电才能保证逻辑电平稳定匹配。之前见过有人用5V供电结果STM32的3.3V引脚去读它的时候电平判定出问题误码率很高。下面会说怎么避免。关于晶振DS1302要求是6pF负载电容的32.768kHz晶振不能随便拿一块12.5pF的用否则走时会偏得离谱。负载电容不匹配会导致振荡频率偏移日误差可能达到几十秒。买芯片的时候顺便问清楚晶振参数这是保证走时准确的第一步。3.2 寄存器和地址映射DS1302内部有一组寄存器分为“秒、分、时、日、月、星期、年”等每个寄存器存的是BCD码这一点要特别注意。给几个关键命令字节功能命令字节读命令字节写秒寄存器0x810x80分寄存器0x830x82时寄存器0x850x84日期寄存器0x870x86月份寄存器0x890x88星期寄存器0x8B0x8A年份寄存器0x8D0x8C写保护寄存器0x8F0x8E看到规律了吗命令字节的最低4位就是寄存器地址bit7固定为1bit6固定为0。bit0是读写标志位0表示写1表示读。例如“秒”的地址是0000那么写命令就是0x80读命令就是0x81。还有一个重要的寄存器是写保护寄存器地址0x8E。往这个寄存器写入数据时最高位WPWrite Protect必须为0才能写入其他寄存器否则DS1302会拒绝所有写操作。这是新手最容易卡住的地方时间改不进去查半天代码结果是忘了关闭写保护。BCD码的问题也要专门提醒。比如秒寄存器读出来的值可能是0x45这个不是十进制45而是十六进制的45代表时间的“45秒”。换算规则很简单高4位是十位低4位是个位。所以0x45对应的二进制是0100 0101十位是4个位是5秒数就是45秒。如果读出来是0x59那对应的秒数就是59秒。写寄存器的时候反过来把十进制59拆成高4位5、低4位9变成0x59写入。很多bug都出在时和日换算上什么“显示35点”、“13月”都是BCD转换没做对。3.3 时序要求DS1302的时序核心是三段拉高CE启动通信发送一个命令字节然后根据命令字的bit0决定读一个字节还是写一个字节。具体来说写字节的过程是CE拉高。在SCLK的第一个上升沿之前把I/O设置成输出模式。先发送命令字节的bit0然后每次SCLK产生一个上升沿同时I/O输出下一个数据位直到8位命令发送完毕。接着发送要写入的数据字节同样是最低位先发。全部发完后CE拉低一个完整的写操作结束。读字节的过程是CE拉高。发送读命令字节发送完第8位时把I/O切换成输入模式。之后每个SCLK上升沿从I/O读取一个数据位先读的是bit0。读完8位后CE拉低。这里有一个关键点DS1302是低位先行也就是LSB first。我见过不少人在写命令的时候按“先发送命令字节的bit7”来写结果读出来的数据全是乱的。这一点一定记牢。时序的时间要求方面时钟频率不能太高SCLK最高频率理论值2MHz实际用普通GPIO模拟出来的时钟频率远低于这个值完全不用焦虑。真正的坑在于片选CE拉高后要等一段时间再开始产生时钟脉冲文档建议至少4μs。如果刚拉高就立刻发时钟芯片可能还没准备好第一个字节就会丢。4. STM32端驱动实现与接线4.1 引脚选择和GPIO配置我用的主控是STM32F103C8T6这也是大家最常用的“蓝板”。引脚分配如下功能STM32引脚说明DS1302 CEPA3片选/复位DS1302 SCLKPA2串行时钟输出DS1302 I/OPA1串行数据需要输入输出切换实际接线时DS1302的I/O脚最好通过一个10kΩ上拉电阻接到3.3V防止空闲状态下数据线浮空。另外建议在DS1302的VCC2和地之间放一个0.1μF的陶瓷电容给芯片提供一个稳定的电源滤波。这个电容虽然不是必须的但加了之后走时稳定性会好一些实际测试中尤其是晶振起振会更可靠。GPIO的配置方法是SCLK和CE配置为推挽输出。I/O引脚在发送数据时配置为推挽输出在读取数据时配置为浮空输入。注意STM32的GPIO复用配置会带来一个坑如果之前用过同一引脚的复用功能比如串口重映射或者I2CGPIO的复用寄存器还挂在那个引脚上这时候简单配置成普通推挽输出可能不生效。稳妥的解决办法是先调用GPIO_DeInit把引脚恢复默认状态再重新初始化。4.2 完整驱动代码实现下面贴的是我整理的驱动代码核心部分。先说清楚这个驱动是阻塞式的操作期间CPU会一直占用但一次读或写只花几百微秒完全够用。先看头文件#ifndef __DS1302_H #define __DS1302_H #include stm32f1xx_hal.h /* 引脚宏定义移植时只需修改这里 */ #define DS1302_CE_PORT GPIOA #define DS1302_CE_PIN GPIO_PIN_3 #define DS1302_SCLK_PORT GPIOA #define DS1302_SCLK_PIN GPIO_PIN_2 #define DS1302_IO_PORT GPIOA #define DS1302_IO_PIN GPIO_PIN_1 /* 寄存器命令 */ #define DS1302_CMD_WRITE_SEC 0x80 #define DS1302_CMD_READ_SEC 0x81 #define DS1302_CMD_WRITE_MIN 0x82 #define DS1302_CMD_READ_MIN 0x83 #define DS1302_CMD_WRITE_HOUR 0x84 #define DS1302_CMD_READ_HOUR 0x85 #define DS1302_CMD_WRITE_DATE 0x86 #define DS1302_CMD_READ_DATE 0x87 #define DS1302_CMD_WRITE_MONTH 0x88 #define DS1302_CMD_READ_MONTH 0x89 #define DS1302_CMD_WRITE_DAY 0x8A #define DS1302_CMD_READ_DAY 0x8B #define DS1302_CMD_WRITE_YEAR 0x8C #define DS1302_CMD_READ_YEAR 0x8D #define DS1302_CMD_WRITE_CTRL 0x8E #define DS1302_CMD_READ_CTRL 0x8F /* 时间结构体 */ typedef struct { uint8_t year; // 0~99存储的是20xx年 uint8_t month; // 1~12 uint8_t date; // 1~31 uint8_t day; // 1~71表示星期日 uint8_t hour; // 0~23 uint8_t minute; // 0~59 uint8_t second; // 0~59 } DS1302_Time_t; void DS1302_Init(void); void DS1302_SetTime(const DS1302_Time_t *time); void DS1302_GetTime(DS1302_Time_t *time); uint8_t DS1302_ReadByte(uint8_t cmd); void DS1302_WriteByte(uint8_t cmd, uint8_t data); #endif实现文件里关键是三个函数写一个字节、读一个字节、初始化和时间设置。#include ds1302.h /* 基础IO操作宏 */ #define DS1302_CE_HIGH() HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_SET) #define DS1302_CE_LOW() HAL_GPIO_WritePin(DS1302_CE_PORT, DS1302_CE_PIN, GPIO_PIN_RESET) #define DS1302_SCLK_HIGH() HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_SET) #define DS1302_SCLK_LOW() HAL_GPIO_WritePin(DS1302_SCLK_PORT, DS1302_SCLK_PIN, GPIO_PIN_RESET) #define DS1302_IO_HIGH() HAL_GPIO_WritePin(DS1302_IO_PORT, DS1302_IO_PIN, GPIO_PIN_SET) #define DS1302_IO_LOW() HAL_GPIO_WritePin(DS1302_IO_PORT, DS1302_IO_PIN, GPIO_PIN_RESET) #define DS1302_IO_READ() HAL_GPIO_ReadPin(DS1302_IO_PORT, DS1302_IO_PIN) /* 将IO引脚配置为输出模式 */ static void DS1302_IO_Output(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_PORT, GPIO_InitStruct); } /* 将IO引脚配置为输入模式 */ static void DS1302_IO_Input(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_PORT, GPIO_InitStruct); } /* 向DS1302写一个字节 */ void DS1302_WriteByte(uint8_t data) { DS1302_IO_Output(); for (uint8_t i 0; i 8; i) { if (data 0x01) { DS1302_IO_HIGH(); } else { DS1302_IO_LOW(); } DS1302_SCLK_HIGH(); DS1302_SCLK_LOW(); data 1; } } /* 从DS1302读一个字节 */ uint8_t DS1302_ReadByte(void) { uint8_t data 0; DS1302_IO_Input(); for (uint8_t i 0; i 8; i) { data 1; if (DS1302_IO_READ()) { data | 0x80; } DS1302_SCLK_HIGH(); DS1302_SCLK_LOW(); } return data; }注意读字节函数里的细节先移位再读值。因为DS1302是最低位先行第0个时钟周期读上来的bit0应该最终落到数据的bit0。所以data 1把已读到的数据整体右移给新读到的位腾出高位然后把读到的值放到bit7。8个周期跑完正好完成LSB到MSB的翻转。初始化函数也很关键void DS1302_Init(void) { GPIO_InitTypeDef GPIO_InitStruct {0}; /* 使能时钟 */ __HAL_RCC_GPIOA_CLK_ENABLE(); /* CE和SCLK初始化为推挽输出默认拉低 */ GPIO_InitStruct.Pin DS1302_CE_PIN | DS1302_SCLK_PIN; GPIO_InitStruct.Mode GPIO_MODE_OUTPUT_PP; GPIO_InitStruct.Speed GPIO_SPEED_FREQ_LOW; HAL_GPIO_Init(GPIOA, GPIO_InitStruct); DS1302_CE_LOW(); DS1302_SCLK_LOW(); /* IO初始化为输出模式 */ DS1302_IO_Output(); /* 关闭写保护 */ DS1302_WriteByte(DS1302_CMD_WRITE_CTRL, 0x00); }时间写入函数void DS1302_SetTime(const DS1302_Time_t *time) { /* 先关闭写保护 */ DS1302_WriteByte(DS1302_CMD_WRITE_CTRL, 0x00); DS1302_CE_HIGH(); DS1302_WriteByte(DS1302_CMD_WRITE_YEAR); DS1302_WriteByte(BinToBcd(time-year)); DS1302_CE_LOW(); DS1302_CE_HIGH(); DS1302_WriteByte(DS1302_CMD_WRITE_MONTH); DS1302_WriteByte(BinToBcd(time-month)); DS1302_CE_LOW(); DS1302_CE_HIGH(); DS1302_WriteByte(DS1302_CMD_WRITE_DATE); DS1302_WriteByte(BinToBcd(time-date)); DS1302_CE_LOW(); DS1302_CE_HIGH(); DS1302_WriteByte(DS1302_CMD_WRITE_DAY); DS1302_WriteByte(BinToBcd(time-day)); DS1302_CE_LOW(); DS1302_CE_HIGH(); DS1302_WriteByte(DS1302_CMD_WRITE_HOUR); DS1302_WriteByte(BinToBcd(time-hour)); DS1302_CE_LOW(); DS1302_CE_HIGH(); DS1302_WriteByte(DS1302_CMD_WRITE_MIN); DS1302_WriteByte(BinToBcd(time-minute)); DS1302_CE_LOW(); DS1302_CE_HIGH(); DS1302_WriteByte(DS1302_CMD_WRITE_SEC); DS1302_WriteByte(BinToBcd(time-second)); DS1302_CE_LOW(); /* 写完开启写保护 */ DS1302_WriteByte(DS1302_CMD_WRITE_CTRL, 0x80); }时间读取函数void DS1302_GetTime(DS1302_Time_t *time) { uint8_t temp; DS1302_CE_HIGH(); DS1302_ReadByteRequest(DS1302_CMD_READ_YEAR); temp DS1302_ReadByte(); time-year BcdToBin(temp); DS1302_CE_LOW(); DS1302_CE_HIGH(); DS1302_ReadByteRequest(DS1302_CMD_READ_MONTH); temp DS1302_ReadByte(); time-month BcdToBin(temp); DS1302_CE_LOW(); /* ... 以此类推 ... */ }这里注意上面DS1302_ReadByteRequest函数是我封装出来的一个辅助函数作用是拉高CE后先发送命令字节再调用DS1302_ReadByte读数据最后拉低CE。串口的引脚切换就是在发送命令字节阶段保持在输出模式发送完切输入模式读数据。我把这个完整实现放在开源仓库里了功能一样结构稍微规整一点。BCD码转换函数uint8_t BinToBcd(uint8_t bin) { return ((bin / 10) 4) | (bin % 10); } uint8_t BcdToBin(uint8_t bcd) { return (bcd 4) * 10 (bcd 0x0F); }这两个函数写成内联函数放进头文件也可以。我在Win下测试对0x59这样超过BCD合法范围的输入做了简单的钳制处理防止非法时间被写进芯片。实际上DS1302本身不会校验数据的合理性你往“时”寄存器写入0x15它就会把时间变成“15点”写入0x99时间就变“99点”完全不会报错。所以写时间前做好范围检查是必要的。4.3 主程序调用示例主程序逻辑很直白系统启动后初始化DS1302如果一个“时间已设置”标志为假就调用DS1302_SetTime写入默认时间之后循环读取时间并通过串口打印。int main(void) { HAL_Init(); SystemClock_Config(); UART_Init(); DS1302_Init(); DS1302_Time_t now { .year 25, .month 2, .date 14, .day 5, .hour 10, .minute 30, .second 0 }; DS1302_SetTime(now); while (1) { DS1302_GetTime(now); printf(%04d-%02d-%02d %02d:%02d:%02d\n, now.year 2000, now.month, now.date, now.hour, now.minute, now.second); HAL_Delay(1000); } }注意printf重定向到串口的方法和硬件平台有关我这里默认已经做好了fputc重定向。如果你用STM32CubeIDE自带的工具链建议直接用ITM_SendChar或者DMA方式输出避免阻塞主循环。5. 实操观察与示波器验证5.1 用逻辑分析仪核对时序波形代码写完之后不要急着直接上板跑强烈建议用逻辑分析仪抓一次时序波形确认每个命令字节和数据字节的顺序都正确。我用的逻辑分析仪是24MHz采样的入门款几十块钱足够用。STM32的GPIO模拟时钟频率大概在几百kHz采样完全跟得上。抓波形的接线注意逻辑分析仪的通道0接SCLK通道1接I/O通道2接CE地线要跟STM32共地。采样的触发方式我习惯设置成“通道2上升沿触发”也就是CE拉高的地方。增益和阈值电压按3.3V标准设置。一组正确的写时间波形应该是CE拉高之后SCLK和I/O协同产生脉冲先发送命令字节紧接着发送数据字节然后CE拉低。如果命令字节和数据字节之间出现了多余的时钟脉冲或者数据位的顺序反了先发了高位波形的形态就会和预期差很远你就知道问题出在哪了。之前调试时我遇到过一个问题读时间完全正常但写时间后就锁死必须断电才能恢复。最后用逻辑分析仪发现写时间时从命令字节到数据字节之间多了一个SCLK下降沿。原因是我写的某个延时函数在IO翻转之间插入了多余的循环导致时序不完整。去掉多余的延时问题立刻消失。5.2 秒寄存器变化规律验证如果你手头没有逻辑分析仪也可以用最土的办法验证连续读取秒寄存器的值观察是不是按BCD码从0x00变到0x59然后归零。在串口助手里加一个每隔一秒打印一次读值的定时任务打印10组数据。如果看到0x00、0x01、0x02……这样递增说明读时序基本对了如果值乱跳先检查是BCD转换的问题还是时序问题。再补充一个验证方法直接读时钟芯片的“充电寄存器”看看引脚的电平变化是否正常。DS1302内部有一个涓流充电电路虽然这个功能通常只在需要给超级电容或电池充电时用但它的寄存器状态可以反映芯片是否在正常工作。如果读出来全0xFF大概率是通信没有建立起来优先查引脚接线。6. 常见问题与排查技巧实录6.1 时间写不进去或写进去之后跑乱这个问题90%是写保护寄存器没处理好。我打过一个比方DS1302内部就像一个储物柜写保护寄存器就是柜子外面的锁。你不先解锁写0x00后面所有写操作都会被拒绝。很多人的代码里写了设置时间的函数但忘了调DS1302_WriteByte(DS1302_CMD_WRITE_CTRL, 0x00)从开机到设置时间这一句一直没有执行过。另外还有一个隐蔽问题是设置时间时最后一笔写秒寄存器。DS1302内部有一个“涓流充电和时钟暂停”位在秒寄存器的bit7这一位如果写1芯片会暂停走时秒数固定不动。我见过有人设置时间的时候把秒寄存器整个写成了0x80结果秒永远停在0分和时都正常。这是典型的手册细节没注意到的坑写秒寄存器时bit7不能置1。正确的做法是秒寄存器正常写入BCD秒数比如写入0x45就代表45秒绝对不要加0x80。6.2 读出来的时间全为0xFF或全为0x00读到0xFF一般是通信链路没建立最常见的原因是IO引脚方向没切换对。DS1302读模式的时序是先输出命令字节此时IO配置成输出模式命令字节发完之后立刻把IO切换成输入模式。如果切换晚了第一个数据位会因为引脚还处于推挽输出状态而读到错误的值如果一直没切换读回来的就是自己输出的电平不是芯片输出的电平。排查方法在DS1302_ReadByte函数里命令字节发送完成后加一个延时1μs再切输入模式看看数据是否恢复。另外检查一下硬件IO脚有没有虚焊、杜邦线是否松动、上拉电阻是否接上了。正常情况下IO引脚空闲时应为高电平用万用表量一下如果量到0V大概率接线错了。读到全0x00通常是因为SCLK没有产生有效的上升沿。DS1302是在SCLK上升沿时刻采样数据的如果SCLK一直保持低电平或者产生了下降沿但没有上升沿数据就传不进去。检查SCLK引脚是不是配置成了开漏输出而没有接上拉这里配置成推挽输出就不会有问题。6.3 走时偏差大或秒跳得不均匀这个问题集中在晶振上。DS1302的走时精度完全由32.768kHz晶振决定晶振质量不好或者负载电容不匹配秒就会或快或慢。先说负载电容DS1302需要的晶振负载电容是6pF市面上很多标称12.5pF的晶振会更便宜但用上去走时明显偏快一天能快几十秒。买晶振的时候直接搜“32.768kHz 6pF 圆柱晶振”不要买错。第二个因素是芯片旁边的走线和接地。晶振周围不要铺大面积的电源走线会产生寄生电容影响振荡频率。稳妥的布局是晶振尽量靠近DS1302的X1和X2引脚走线短而直地线单独走一条。为了减小干扰可以在晶振两端分别对地接6pF的负载电容这也是DS1302数据手册中推荐的电路。如果你发现秒值不是均匀跳变比如从0x00直接跳到0x03说明芯片在走时过程中有被干扰复位或暂停。检查一下电源DS1302的VCC2上有没有较大的纹波尤其是如果主控板和DS1302共用一个电源芯片大电流外设比如电机、显示屏的开关会导致电压跌落芯片重启或者暂停。在VCC2和地之间并联一个100μF的电解电容和0.1μF的陶瓷电容能有效改善这种情况。6.4 更换备用电池后时间格式异常有人反馈换了纽扣电池之后读出来时间变成0x99或者0x00走时完全错乱。这种情况通常是芯片在电池电压过低的时候写入了非法数据。DS1302虽然支持备用电池供电但电池电压降到2V以下时内部寄存器的数据可能损坏。解决办法是换电池后给芯片做一次完整的上电复位再重新设置一次时间。上电复位的操作很简单断电后等芯片完全掉电重新上电在初始化函数里延时几毫秒再执行写保护关闭。如果已经写入非法数据可以直接向目标寄存器写入0x00或者正确的BCD值把异常状态覆盖掉。我自己在处理这类问题时的经验是写一个完整的初始化流程包括关闭写保护、设置时间、开启写保护三个步骤每次上电都执行这样最省心。6.5 HAL_Delay卡死问题很多初学者在调试串口打印的时候遇到HAL_Delay卡死的现象。这个问题的根源在于SysTick中断的优先级和HAL_Delay的实现机制。HAL_Delay依赖SysTick中断的uwTick计数如果你的工程里还启用了其他中断并且那个中断的优先级和SysTick一样或者更高并且中断处理函数里又调用了延时函数就会造成死锁。排查方法打开调试器暂停程序查看当前PC指针停在哪里。如果停在HAL_Delay的while循环里基本上就是这个原因。解决思路有三个把SysTick中断优先级设置为最低数字最大。避免在中断服务函数里调用HAL_Delay。用DWT-CYCCNT实现非阻塞延时这个方法是ARM Cortex-M内核自带的硬件计数器不用占SysTick。DS1302驱动本身没有使用HAL_Delay所以不存在内部卡死问题但和你的主循环逻辑放在一起时还是要注意延时函数的使用场景。7. 这个项目后续还能怎么扩展驱动写完只是第一步。DS1302这个项目我实际用在了两个场景里一个是智能鱼缸控制器用来记录水泵和灯光开启的准确时间另一个是桌面温湿度记录仪每分钟把时间戳和温湿度数据存到SD卡里。这两个场景对时间精度的要求都不高DS1302完全满足。如果你想继续往深了学可以从下面几个方向扩展用DS1302的闹钟中断功能DS1302有一个闹钟寄存器可以设置闹钟时间到点后INT引脚会产生一个低脉冲。接上STM32的外部中断就能实现定时唤醒这在低功耗设备里很实用。把驱动改成硬件SPI版本DS1302的三线协议和SPI mode 0基本兼容只需要注意读写模式的切换。改成硬件SPI后读取效率会更高也更适合在RTOS环境下使用。加入时间校准功能DS1302的走时精度受晶振影响可以在代码里写一个校准函数通过实测每天慢多少秒来给秒寄存器做补偿精度能从±2秒/天提升到±1秒/月左右虽然不能和DS3231比但玩一玩很有意思。8. 一个小技巧用调试器直接观察寄存器变化最后再分享一个调试技巧。STM32的调试器支持实时变量查看你在DS1302_GetTime执行完后暂停程序把时间结构体变量加到Watch窗口然后单步执行几行看看各个成员变量的值是否合理。如果不合理先排除BCD转换问题再回头查时序。这个方法比单纯靠串口打印要快得多尤其是当串口本身也有问题的时候能帮你把通信和显示两个环节解耦排查。
返回列表