• STM32初学-外部RTC时钟芯片DS3231


            RTC(Real_Time Clock)实时时钟,它是电子产品中不可或缺的东西。其最直接的作用就是时钟功能。细心的朋友可以发现,当我们的电脑或者手机没联网时,仍然可以正常显示日期与时钟,这就是RTC的功劳。

            RTC的运行无需网络连接,只需一个频率固定的振荡源和一个计数器,就能实现精准的计时。假如有一个振荡源,其每秒固定振荡1000次,那我们就可以用计数器对振荡进行计数,每振荡1000次,代表时间过去了1s,然后复位计数器并开始新的计数,同时,秒寄存器加1。如此循环,就能实现时钟的走时。

    21ad4e7542c24f098061485fea6c9146.png

            在单片机的某些使用场景下,RTC时钟是不可或缺的,例如使用了文件系统,就必须启用RTC时钟,用于更新文件的时间。

            STM32内置了RTC时钟模块,只要配置好参数,就能启用RTC。RTC的时钟振荡源可以来自内部也可以来自外部。内部时钟源由HCLK经过分频得到,外部时钟源则由石英晶振提供。

    973457b0641540aaa604c7e12f8583a6.png

             内部时钟源是由高频晶振分频得到,所以其精度计时不高,为了准确计时,一般采用外部时钟源。外部时钟源一般选用32.768KHz的石英晶振。这种参数的石英晶振一秒钟能振荡32768次,正好对应2^16。

    b98f599ebe9bc4ec1b60da566ec7fa39.jpeg

            然而,即使采用了外部晶振的RTC,其精度仍然是有限的。因为晶振外置,线路上的寄生电容电感、温度都会影响晶振频率,短时间可能看不出误差,但是时间一长,其误差就大了。

            如果想进一步减小RTC的误差,则需要使用RTC时钟芯片。时钟芯片的优势在于其内部带有温度补偿功能,能通过检测环境温度来对晶振进行误差补偿,减小计时误差。且时钟芯片的年、月、日等时间数据单独存储在内部的RAM中,对单片机来说,只要通过串口读取特定寄存器地址的数据就能得到时间参数,而无需再去计算。现在流行的时钟芯片很多,如DS1302、 DS1307。但是这些芯片仍需要外置晶振才能工作,所以仍存在误差。

            所以一种在内部集成晶振的时钟芯片应运而生。DS3231就是这样一种时钟芯片。其精度最高可以达到±2ppm。实测6个月在常温下连续运行,误差不超过1分钟。

    61f5e66528d448859fee62d6a3aed26b.png

         DS3231采用快速IIC通信进行数据传输,最高时钟频率400KHz。还带有闹钟,复位等功能。224a5ac113814714a5bbfbb80213de94.png

     DS3231有两种封装,引脚功能一样,16pin的封装只是多了八个空引脚。

    如下是其应用电路。9fb591b6e7464754a250edaffc08b806.png

            可以看到该芯片有两个电源输入,一个是VCC,另一个是Vbat。VCC我们可以与单片机共用3.3V电源,Vbat则接到一颗纽扣电池的正极。当单片机供电断开后,DS3231仍能靠纽扣电池供电维持计时功能,但是无法进行IIC通信,也就是不能读取时间数据,恢复VCC供电后,IIC通信随之被唤醒。

            INT是个漏极开路的输出,如果想输出高电平则需要外接上拉电阻,该引脚可连接到单片机IO口作为单片机的外部中断源。

            32kHz是一个固定输出32KHz频率方波的IO口,也可做为单片机的计时源。同为开漏输出。

            RST是芯片的复位脚,如果没有特别需要,可以直接悬空。该芯片无需复位也能直接初始化。

    软件部分                                                          

            这里我使用的是STM32F4,F1的芯片也是兼容的,只是需要把头文件改成F1的。IIC通信采用软件模拟。iic的软件模拟可参考文章(STM32单片机-IIC通信(软件模拟)),这里我就不详细讲IIC软件模拟的原理了。接下来我们看程序。

    1,我使用的是PB8和PB9分别做为IIC的SCL与SDA。如果想使用其他IO,稍微修改下就行。

    1. #include
    2. /********************软件模拟IIC*********************/
    3. /*****************PB8=SCL,PB9=SDA*****************/
    4. #define DS3231_ADDRESS_Write 0xD0
    5. #define DS3231_ADDRESS_Read 0xD1
    6. /**************DS3231内部寄存器地址***************/
    7. #define DS3231_SEC 0x00 // 秒
    8. #define DS3231_MIN 0x01 //分
    9. #define DS3231_HOUR 0x02 //时
    10. #define DS3231_WEEK 0x03 //周
    11. #define DS3231_DATE 0x04 //日
    12. #define DS3231_MONTH 0x05 //月
    13. #define DS3231_YEAR 0x06 //年
    14. #define DS3231_AL1SEC 0x07
    15. #define DS3231_AL1MIN 0x08
    16. #define DS3231_AL1HOUR 0x09
    17. #define DS3231_AL1WDAY 0x0A
    18. #define DS3231_AL2MIN 0x0B
    19. #define DS3231_AL2HOUR 0x0C
    20. #define DS3231_AL2WDAY 0x0D
    21. #define DS3231_CONTROL 0x0E
    22. #define DS3231_STATUS 0x0F
    23. #define DS3231_AGING_OFFSET 0x0F
    24. #define DS3231_TMP_High 0x11
    25. #define DS3231_TMP_LOW 0x12
    26. /******DS3231内部寄存器地址******/
    27. /**************END**************/
    28. #define Read_IIC_SDA GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_9) //定义Read_IIC_SDA为PB11的输入值
    29. #define IIC_SCL_H() GPIO_SetBits(GPIOB,GPIO_Pin_8) //定义IIC_SCL_H()函数为将RES(PB10)置高电平
    30. #define IIC_SCL_L() GPIO_ResetBits(GPIOB,GPIO_Pin_8) //定义IIC_SCL_L()函数为将RES(PB10)置低电平
    31. #define IIC_SDA_H() GPIO_SetBits(GPIOB,GPIO_Pin_9) //定义IIC_SDA_H()函数为将RES(PB11)置高电平
    32. #define IIC_SDA_L() GPIO_ResetBits(GPIOB,GPIO_Pin_9) //定义IIC_SDA_L()函数为将RES(PB11)置低电平
    33. //最后读取到的时间数据将赋值到下面这几个变量
    34. u8 RTC_Sec,RTC_Min,RTC_Hour,RTC_Week,RTC_Date,RTC_Month;
    35. u16 RTC_Year;
    36. void IIC2_SoftInit(void)
    37. {
    38. GPIO_InitTypeDef GPIO_InitStructure;
    39. RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
    40. GPIO_InitStructure.GPIO_Pin=GPIO_Pin_8 | GPIO_Pin_9; //10--SCL 11--SDA;PB10 PB11
    41. GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
    42. GPIO_InitStructure.GPIO_Mode=GPIO_Mode_OUT;
    43. GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //OD开漏
    44. GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉
    45. GPIO_Init(GPIOB, &GPIO_InitStructure);
    46. }
    47. /******* 设置SDA为输出*******/
    48. void SDA_OUT(void)
    49. {
    50. GPIO_InitTypeDef GPIO_InitStructure;
    51. GPIO_InitStructure.GPIO_Pin= GPIO_Pin_9;
    52. GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
    53. GPIO_InitStructure.GPIO_Mode=GPIO_Mode_OUT; //推挽输出
    54. GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //OD开漏
    55. GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; //上拉
    56. GPIO_Init(GPIOB, &GPIO_InitStructure);
    57. }
    58. /******* 设置SDA为输入*******/
    59. void SDA_IN(void)
    60. {
    61. GPIO_InitTypeDef GPIO_InitStructure;
    62. GPIO_InitStructure.GPIO_Pin= GPIO_Pin_9;
    63. GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
    64. GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN; //上拉输入
    65. GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //OD开漏
    66. GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; //上拉
    67. GPIO_Init(GPIOB, &GPIO_InitStructure);
    68. }
    69. void IIC_Start(void) //起始信号
    70. {
    71. SDA_OUT(); //把SDA作为输出,初始化为推挽输出
    72. IIC_SDA_H(); //SDA输出高电平
    73. IIC_SCL_H(); //SCL输出高电平
    74. Delay_us(2);
    75. IIC_SDA_L(); //SDA输出低点评
    76. Delay_us(2);
    77. IIC_SCL_L(); //SCL输出低电平
    78. Delay_us(2);
    79. }
    80. void IIC_Stop(void) //停止信号
    81. {
    82. IIC_SCL_H(); //SCL输出高电平
    83. IIC_SDA_L(); //SDA输出低点评
    84. Delay_us(2);
    85. IIC_SDA_H(); //SDA输出高电平
    86. Delay_us(2);
    87. }
    88. u8 IIC_Wait_Ask(void) //等待应答信号
    89. {
    90. u8 count;
    91. IIC_SDA_H();
    92. Delay_us(2);
    93. SDA_IN();
    94. Delay_us(2);
    95. IIC_SCL_H();
    96. Delay_us(2);
    97. while(Read_IIC_SDA)
    98. {
    99. count++;
    100. if(count>250)
    101. {
    102. IIC_Stop(); //如果长时间无应答,则认为从站故障,终止数据传输,并返回1
    103. return 1;
    104. }
    105. }
    106. IIC_SCL_L();
    107. Delay_us(1);
    108. return 0;
    109. }
    110. void IIC_Ack(void) //应答信号
    111. {
    112. IIC_SCL_L();
    113. SDA_OUT();
    114. IIC_SDA_L();
    115. Delay_us(2);
    116. IIC_SCL_H();
    117. Delay_us(2);
    118. IIC_SCL_L();
    119. }
    120. void IIC_NAck(void) //主机不产生应答信号NACK
    121. {
    122. IIC_SCL_L();
    123. SDA_OUT();
    124. IIC_SDA_H();
    125. Delay_us(2);
    126. IIC_SCL_H();
    127. Delay_us(2);
    128. IIC_SCL_L();
    129. }
    130. void IIC_WriteByte(u8 data) //写1Byte数据,每个数据都是以写1Byte作为基本单位
    131. {
    132. u8 i;
    133. SDA_OUT(); //SDA切换到数据输出模式
    134. Delay_us(2);
    135. for(i=0;i<8;i++) //循环传输1Byte数据,即8bit
    136. {
    137. IIC_SCL_L(); //SCL置低电平,为下个Bit数据做准备
    138. Delay_us(2);
    139. if(data & 0x80) //MSB,如果date的第八位为1
    140. IIC_SDA_H(); //则SDA置高
    141. else
    142. IIC_SDA_L(); //否则置低
    143. Delay_us(1);
    144. IIC_SCL_H(); //SCL拉高,产生一个时钟信号,从机读取SDA状态
    145. Delay_us(2); //延时,丛机在这段时间读取SDA状态
    146. IIC_SCL_L(); //延时后拉低SCL,为下个Bit数据做准备
    147. data<<=1; //date左移1位,下一bit变成第八位
    148. }
    149. }
    150. //读1个字节,ack=1时,发送ACK,ack=0,发送nACK
    151. u8 IIC_Read_Byte(const unsigned char ack)
    152. {
    153. u8 i,receive=0;
    154. SDA_IN(); //SDA设置为输入
    155. Delay_us(2);
    156. for(i=0;i<8;i++ )
    157. {
    158. IIC_SCL_L();
    159. Delay_us(2);
    160. IIC_SCL_H();
    161. receive<<=1;
    162. Delay_us(2);
    163. if(Read_IIC_SDA)
    164. receive++;
    165. Delay_us(2);
    166. }
    167. if (!ack)
    168. IIC_NAck(); //发送nACK
    169. else
    170. IIC_Ack(); //发送ACK
    171. return receive;
    172. }
    173. void IIC_DS3231_ByteWrite(u8 WriteAddr,u8 date)
    174. {
    175. IIC_Start(); //起始信号
    176. IIC_WriteByte(DS3231_ADDRESS_Write); //DS3231设备地址,写
    177. IIC_Wait_Ask(); //等待应答
    178. IIC_WriteByte(WriteAddr); //寄存器地址
    179. IIC_Wait_Ask(); //等待应答
    180. IIC_WriteByte(date); //写入数据
    181. IIC_Wait_Ask(); //等待应答
    182. IIC_Stop(); //结束信号
    183. }
    184. u8 IIC_DS3231_ByteRead(u8 ReadAddr)
    185. {
    186. u8 data = 0;
    187. IIC_Start(); //产生起始位
    188. IIC_WriteByte(DS3231_ADDRESS_Write); //发送从机地址(写模式D0)
    189. IIC_Wait_Ask(); //等待应答
    190. IIC_WriteByte(ReadAddr); //发送寄存器地址
    191. IIC_Wait_Ask(); //等待应答
    192. IIC_Start(); //重复起始信号
    193. IIC_WriteByte(DS3231_ADDRESS_Read); //发送从机地址(读模式)
    194. IIC_Wait_Ask(); //等待应答
    195. data = IIC_Read_Byte(0); //读取数据,参数设为0 --- NACK
    196. IIC_Stop();
    197. return data;
    198. }
    199. void IIC_DS3231_ReadAll(void) //读取所有时间数据并转换
    200. {
    201. u8 Data_Sec,Data_Min,Data_Hour,Data_Week,Data_Date,Data_Month,Data_Year;
    202. u8 Low,High;
    203. Data_Sec= IIC_DS3231_ByteRead(DS3231_SEC); //读取数据秒
    204. Data_Min = IIC_DS3231_ByteRead(DS3231_MIN); //读取数据分
    205. Data_Hour = IIC_DS3231_ByteRead(DS3231_HOUR); //读取数据时
    206. Data_Week = IIC_DS3231_ByteRead(DS3231_WEEK); //读取数据周
    207. Data_Date = IIC_DS3231_ByteRead(DS3231_DATE); //读取数据日
    208. Data_Month = IIC_DS3231_ByteRead(DS3231_MONTH); //读取数据月
    209. Data_Year = IIC_DS3231_ByteRead(DS3231_YEAR); //读取数据年
    210. Low=Data_Sec&0x0f; //取低四位
    211. High=(( Data_Sec& 0xf0) >> 4); //取高四位
    212. RTC_Sec=High*10+Low; //转换秒(高四位*10+低四位)
    213. Low=Data_Min&0x0f;
    214. High=(( Data_Min& 0xf0) >> 4);
    215. RTC_Min=High*10+Low; //转换分
    216. Low=Data_Hour&0x0f;
    217. High=(( Data_Hour& 0x30) >> 4);
    218. RTC_Hour=High*10+Low; //转换时(高两位*10+低四位)
    219. RTC_Week=Data_Week; //转换周(不需要转换)
    220. Low=Data_Date&0x0f;
    221. High=(( Data_Date& 0xf0) >> 4);
    222. RTC_Date=High*10+Low; //转换日
    223. Low=Data_Month&0x0f;
    224. High=(( Data_Month& 0x10) >> 4);
    225. RTC_Month=High*10+Low; //转换月
    226. Low=Data_Year&0x0f;
    227. High=(( Data_Year& 0xf0) >> 4);
    228. RTC_Year=((Data_Month>>7)+20)*100+High*10+Low; //转换年(世纪*100+高四位*10+低四位)
    229. }
    230. void IIC_DS3231_WriteAll(u16 Year,u8 Month,u8 Date,u8 Week,u8 Hour,u8 Min,u8 Sec)
    231. {
    232. u8 Sec_Date,Min_Date,Hour_Date,Week_Date,Date_Date,Month_Date,Year_Date;
    233. u8 H_Bit,L_Bit;
    234. H_Bit=(Sec/10)<<4; //取高四位
    235. L_Bit=Sec%10; //取低四位
    236. Sec_Date=H_Bit|L_Bit; //合并成八位
    237. H_Bit=(Min/10)<<4;
    238. L_Bit=Min%10;
    239. Min_Date=H_Bit|L_Bit;
    240. H_Bit=Hour/10;
    241. L_Bit=Hour%10;
    242. Hour_Date=(H_Bit<<4)|L_Bit;
    243. Week_Date=Week;
    244. H_Bit=Date/10;
    245. L_Bit=Date%10;
    246. Date_Date=(H_Bit<<4)|L_Bit;
    247. if(Year/100==20)
    248. {
    249. H_Bit=Month/10;
    250. L_Bit=Month%10;
    251. Month_Date=(H_Bit<<4)|L_Bit;
    252. H_Bit=(Year-2000)/10;
    253. L_Bit=(Year-2000)%10;
    254. Year_Date=(H_Bit<<4)|L_Bit;
    255. }
    256. else
    257. {
    258. H_Bit=Month/10+8; //05h第8位如果是1,则为22世纪
    259. L_Bit=Month%10;
    260. Month_Date=(H_Bit<<4)|L_Bit;
    261. H_Bit=(Year-2100)/10;
    262. L_Bit=(Year-2100)%10;
    263. Year_Date=(H_Bit<<4)|L_Bit;
    264. }
    265. IIC_DS3231_ByteWrite(DS3231_YEAR,Year_Date);
    266. IIC_DS3231_ByteWrite(DS3231_MONTH,Month_Date);
    267. IIC_DS3231_ByteWrite(DS3231_DATE,Date_Date);
    268. IIC_DS3231_ByteWrite(DS3231_WEEK,Week_Date);
    269. IIC_DS3231_ByteWrite(DS3231_HOUR,Hour_Date);
    270. IIC_DS3231_ByteWrite(DS3231_MIN,Min_Date);
    271. IIC_DS3231_ByteWrite(DS3231_SEC,Sec_Date);
    272. }
    273. void DS3231_Init(void) //DS3231初始化
    274. {
    275. IIC_DS3231_ByteWrite(0x0E,0x40); //使能1Hz方波,使能振荡器
    276. IIC_DS3231_ByteWrite(0x0F,0x00); //启动振荡器
    277. Delay_ms(100); //延时等待完全起振
    278. }

     延时函数,IIC软件模拟需要用到延时函数,这里我采用的是滴答定时器进行延时,需要注意,F1 与F4的延时函数不能共用,因为主频不一样。

    1. /****************************************************************
    2. * Function: Delay_us
    3. * Description: Microsecond delay.
    4. * Input: nus
    5. * Output:
    6. * Return:
    7. *****************************************************************/
    8. void Delay_us(u16 nus)
    9. {
    10. //Delay_Init();
    11. u32 temp;
    12. SysTick->LOAD = SystemCoreClock / 8000000 * nus; /* Time load (SysTick-> LOAD is 24bit) */
    13. SysTick->VAL = 0x000000; /* Empty counter */
    14. SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; /* Start the countdown */
    15. do
    16. {
    17. temp = SysTick->CTRL;
    18. }
    19. while(temp&0x01 && !(temp&(1<<16))); /* Wait time is reached */
    20. SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; /* Close Counter */
    21. SysTick->VAL = 0x000000; /* Empty counter */
    22. }
    23. /****************************************************************
    24. * Function: Delay_ms
    25. * Description: Millisecond delay.
    26. * Input: nms
    27. * Output:
    28. * Return:
    29. *****************************************************************/
    30. void Delay_ms(u16 nms)
    31. {
    32. u32 temp;
    33. while(nms > 500) //24位定时器最大计数值2^24=16777216,计时器时钟频率初始化为21MHz,最大计时时间为16777216/21000000=0.798s=798ms,所以需要多次复位
    34. {
    35. SysTick->LOAD = SystemCoreClock / 8000 * 500; /* Time load (SysTick-> LOAD is 24bit) */
    36. SysTick->VAL = 0x000000; /* Empty counter */
    37. SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; /* Start the countdown */
    38. do
    39. {
    40. temp = SysTick->CTRL;
    41. }
    42. while(temp&0x01 && !(temp&(1<<16))); /* Wait time is reached */
    43. SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; /* Close Counter */
    44. SysTick->VAL = 0x000000; /* Empty counter */
    45. nms -= 500;
    46. }
    47. SysTick->LOAD = SystemCoreClock / 8000 * nms; /* Time load (SysTick-> LOAD is 24bit) */
    48. SysTick->VAL = 0x000000; /* Empty counter */
    49. SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; /* Start the countdown */
    50. do
    51. {
    52. temp = SysTick->CTRL;
    53. }
    54. while(temp&0x01 && !(temp&(1<<16))); /* Wait time is reached */
    55. SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; /* Close Counter */
    56. SysTick->VAL = 0x000000; /* Empty counter */
    57. }

    嘀嗒定时器初始化:在启用延时函数前必须对定时器进行初始化,配置对应的参数。这里我用主频HCLK的八分频做为其定时频率,即21MHz。也就是一秒钟对应定时器的21000000。

    1. void Delay_Init(void) //嘀嗒计时器初始化,用于Delay函数
    2. {
    3. SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); //计数器频率:168M/8=21MHZ
    4. SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; /* Disability SysTick counter */
    5. }

    main函数:初始化延时函数,初始化DS3231 ,讲时间写入DS3231,讲时间读取出来

    1. #include //F4头文件
    2. void main(void)
    3. {
    4. Delay_Init(); //嘀嗒定时器初始化,必须初始化,延时依靠嘀嗒计时器
    5. DS3231_Init(); //DS3231初始化,唤醒DS3231,并启动振荡器
    6. IIC_DS3231_WriteAll(2023,10,1,1,12,00,00); //设置时间函数,实例为2023年10月1日12:00:00
    7. IIC_DS3231_ReadAll(); //读取DS3231所有时间数据
    8. }

    最终读取转换后的时钟数据分别为这些变量:u8 RTC_Sec,RTC_Min,RTC_Hour,RTC_Week,RTC_Date,RTC_Month;   
    u16 RTC_Year;

    可以通过串口打印,或者其他方式显示出当前时间。

    这里我将时间显示到LCD屏幕上。

    如果觉得本文有用,就点个赞吧~

  • 相关阅读:
    初级网络工程师之入门到入狱(一)
    提升cartographer局部全局SLAM实时性,降低cartographer在低性能板上的堵塞
    网站变灰白css
    Hive简介及安装配置
    Java 并发编程解析 | 如何正确理解Java领域中的锁机制,我们一般需要掌握哪些理论知识?
    Could not read from boot medium. System halted.
    C语言实现面向对象编程 | 干货
    恢复二叉搜索树[根据题意模拟->发现问题->分析问题->见招拆招]
    C 标准库 - <string.h>
    ROS2——常用命令行(四)
  • 原文地址:https://blog.csdn.net/qq_55203246/article/details/129771464