RTC(Real_Time Clock)即实时时钟,它是电子产品中不可或缺的东西。其最直接的作用就是时钟功能。细心的朋友可以发现,当我们的电脑或者手机没联网时,仍然可以正常显示日期与时钟,这就是RTC的功劳。
RTC的运行无需网络连接,只需一个频率固定的振荡源和一个计数器,就能实现精准的计时。假如有一个振荡源,其每秒固定振荡1000次,那我们就可以用计数器对振荡进行计数,每振荡1000次,代表时间过去了1s,然后复位计数器并开始新的计数,同时,秒寄存器加1。如此循环,就能实现时钟的走时。
在单片机的某些使用场景下,RTC时钟是不可或缺的,例如使用了文件系统,就必须启用RTC时钟,用于更新文件的时间。
STM32内置了RTC时钟模块,只要配置好参数,就能启用RTC。RTC的时钟振荡源可以来自内部也可以来自外部。内部时钟源由HCLK经过分频得到,外部时钟源则由石英晶振提供。
内部时钟源是由高频晶振分频得到,所以其精度计时不高,为了准确计时,一般采用外部时钟源。外部时钟源一般选用32.768KHz的石英晶振。这种参数的石英晶振一秒钟能振荡32768次,正好对应2^16。
然而,即使采用了外部晶振的RTC,其精度仍然是有限的。因为晶振外置,线路上的寄生电容电感、温度都会影响晶振频率,短时间可能看不出误差,但是时间一长,其误差就大了。
如果想进一步减小RTC的误差,则需要使用RTC时钟芯片。时钟芯片的优势在于其内部带有温度补偿功能,能通过检测环境温度来对晶振进行误差补偿,减小计时误差。且时钟芯片的年、月、日等时间数据单独存储在内部的RAM中,对单片机来说,只要通过串口读取特定寄存器地址的数据就能得到时间参数,而无需再去计算。现在流行的时钟芯片很多,如DS1302、 DS1307。但是这些芯片仍需要外置晶振才能工作,所以仍存在误差。
所以一种在内部集成晶振的时钟芯片应运而生。DS3231就是这样一种时钟芯片。其精度最高可以达到±2ppm。实测6个月在常温下连续运行,误差不超过1分钟。
DS3231采用快速IIC通信进行数据传输,最高时钟频率400KHz。还带有闹钟,复位等功能。
DS3231有两种封装,引脚功能一样,16pin的封装只是多了八个空引脚。
如下是其应用电路。
可以看到该芯片有两个电源输入,一个是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,稍微修改下就行。
#include<stm32f4xx.h>
/********************软件模拟IIC*********************/
/*****************PB8=SCL,PB9=SDA*****************/
#define DS3231_ADDRESS_Write 0xD0
#define DS3231_ADDRESS_Read 0xD1
/**************DS3231内部寄存器地址***************/
#define DS3231_SEC 0x00 // 秒
#define DS3231_MIN 0x01 //分
#define DS3231_HOUR 0x02 //时
#define DS3231_WEEK 0x03 //周
#define DS3231_DATE 0x04 //日
#define DS3231_MONTH 0x05 //月
#define DS3231_YEAR 0x06 //年
#define DS3231_AL1SEC 0x07
#define DS3231_AL1MIN 0x08
#define DS3231_AL1HOUR 0x09
#define DS3231_AL1WDAY 0x0A
#define DS3231_AL2MIN 0x0B
#define DS3231_AL2HOUR 0x0C
#define DS3231_AL2WDAY 0x0D
#define DS3231_CONTROL 0x0E
#define DS3231_STATUS 0x0F
#define DS3231_AGING_OFFSET 0x0F
#define DS3231_TMP_High 0x11
#define DS3231_TMP_LOW 0x12
/******DS3231内部寄存器地址******/
/**************END**************/
#define Read_IIC_SDA GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_9) //定义Read_IIC_SDA为PB11的输入值
#define IIC_SCL_H() GPIO_SetBits(GPIOB,GPIO_Pin_8) //定义IIC_SCL_H()函数为将RES(PB10)置高电平
#define IIC_SCL_L() GPIO_ResetBits(GPIOB,GPIO_Pin_8) //定义IIC_SCL_L()函数为将RES(PB10)置低电平
#define IIC_SDA_H() GPIO_SetBits(GPIOB,GPIO_Pin_9) //定义IIC_SDA_H()函数为将RES(PB11)置高电平
#define IIC_SDA_L() GPIO_ResetBits(GPIOB,GPIO_Pin_9) //定义IIC_SDA_L()函数为将RES(PB11)置低电平
//最后读取到的时间数据将赋值到下面这几个变量
u8 RTC_Sec,RTC_Min,RTC_Hour,RTC_Week,RTC_Date,RTC_Month;
u16 RTC_Year;
void IIC2_SoftInit(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
RCC_AHB1PeriphClockCmd(RCC_AHB1Periph_GPIOB, ENABLE);
GPIO_InitStructure.GPIO_Pin=GPIO_Pin_8 | GPIO_Pin_9; //10--SCL 11--SDA;PB10 PB11
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_OUT;
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //OD开漏
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_UP; //上拉
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
/******* 设置SDA为输出*******/
void SDA_OUT(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin= GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_OUT; //推挽输出
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //OD开漏
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; //上拉
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
/******* 设置SDA为输入*******/
void SDA_IN(void)
{
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin= GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed=GPIO_Speed_50MHz;
GPIO_InitStructure.GPIO_Mode=GPIO_Mode_IN; //上拉输入
GPIO_InitStructure.GPIO_OType = GPIO_OType_PP; //OD开漏
GPIO_InitStructure.GPIO_PuPd = GPIO_PuPd_NOPULL; //上拉
GPIO_Init(GPIOB, &GPIO_InitStructure);
}
void IIC_Start(void) //起始信号
{
SDA_OUT(); //把SDA作为输出,初始化为推挽输出
IIC_SDA_H(); //SDA输出高电平
IIC_SCL_H(); //SCL输出高电平
Delay_us(2);
IIC_SDA_L(); //SDA输出低点评
Delay_us(2);
IIC_SCL_L(); //SCL输出低电平
Delay_us(2);
}
void IIC_Stop(void) //停止信号
{
IIC_SCL_H(); //SCL输出高电平
IIC_SDA_L(); //SDA输出低点评
Delay_us(2);
IIC_SDA_H(); //SDA输出高电平
Delay_us(2);
}
u8 IIC_Wait_Ask(void) //等待应答信号
{
u8 count;
IIC_SDA_H();
Delay_us(2);
SDA_IN();
Delay_us(2);
IIC_SCL_H();
Delay_us(2);
while(Read_IIC_SDA)
{
count++;
if(count>250)
{
IIC_Stop(); //如果长时间无应答,则认为从站故障,终止数据传输,并返回1
return 1;
}
}
IIC_SCL_L();
Delay_us(1);
return 0;
}
void IIC_Ack(void) //应答信号
{
IIC_SCL_L();
SDA_OUT();
IIC_SDA_L();
Delay_us(2);
IIC_SCL_H();
Delay_us(2);
IIC_SCL_L();
}
void IIC_NAck(void) //主机不产生应答信号NACK
{
IIC_SCL_L();
SDA_OUT();
IIC_SDA_H();
Delay_us(2);
IIC_SCL_H();
Delay_us(2);
IIC_SCL_L();
}
void IIC_WriteByte(u8 data) //写1Byte数据,每个数据都是以写1Byte作为基本单位
{
u8 i;
SDA_OUT(); //SDA切换到数据输出模式
Delay_us(2);
for(i=0;i<8;i++) //循环传输1Byte数据,即8bit
{
IIC_SCL_L(); //SCL置低电平,为下个Bit数据做准备
Delay_us(2);
if(data & 0x80) //MSB,如果date的第八位为1
IIC_SDA_H(); //则SDA置高
else
IIC_SDA_L(); //否则置低
Delay_us(1);
IIC_SCL_H(); //SCL拉高,产生一个时钟信号,从机读取SDA状态
Delay_us(2); //延时,丛机在这段时间读取SDA状态
IIC_SCL_L(); //延时后拉低SCL,为下个Bit数据做准备
data<<=1; //date左移1位,下一bit变成第八位
}
}
//读1个字节,ack=1时,发送ACK,ack=0,发送nACK
u8 IIC_Read_Byte(const unsigned char ack)
{
u8 i,receive=0;
SDA_IN(); //SDA设置为输入
Delay_us(2);
for(i=0;i<8;i++ )
{
IIC_SCL_L();
Delay_us(2);
IIC_SCL_H();
receive<<=1;
Delay_us(2);
if(Read_IIC_SDA)
receive++;
Delay_us(2);
}
if (!ack)
IIC_NAck(); //发送nACK
else
IIC_Ack(); //发送ACK
return receive;
}
void IIC_DS3231_ByteWrite(u8 WriteAddr,u8 date)
{
IIC_Start(); //起始信号
IIC_WriteByte(DS3231_ADDRESS_Write); //DS3231设备地址,写
IIC_Wait_Ask(); //等待应答
IIC_WriteByte(WriteAddr); //寄存器地址
IIC_Wait_Ask(); //等待应答
IIC_WriteByte(date); //写入数据
IIC_Wait_Ask(); //等待应答
IIC_Stop(); //结束信号
}
u8 IIC_DS3231_ByteRead(u8 ReadAddr)
{
u8 data = 0;
IIC_Start(); //产生起始位
IIC_WriteByte(DS3231_ADDRESS_Write); //发送从机地址(写模式D0)
IIC_Wait_Ask(); //等待应答
IIC_WriteByte(ReadAddr); //发送寄存器地址
IIC_Wait_Ask(); //等待应答
IIC_Start(); //重复起始信号
IIC_WriteByte(DS3231_ADDRESS_Read); //发送从机地址(读模式)
IIC_Wait_Ask(); //等待应答
data = IIC_Read_Byte(0); //读取数据,参数设为0 --- NACK
IIC_Stop();
return data;
}
void IIC_DS3231_ReadAll(void) //读取所有时间数据并转换
{
u8 Data_Sec,Data_Min,Data_Hour,Data_Week,Data_Date,Data_Month,Data_Year;
u8 Low,High;
Data_Sec= IIC_DS3231_ByteRead(DS3231_SEC); //读取数据秒
Data_Min = IIC_DS3231_ByteRead(DS3231_MIN); //读取数据分
Data_Hour = IIC_DS3231_ByteRead(DS3231_HOUR); //读取数据时
Data_Week = IIC_DS3231_ByteRead(DS3231_WEEK); //读取数据周
Data_Date = IIC_DS3231_ByteRead(DS3231_DATE); //读取数据日
Data_Month = IIC_DS3231_ByteRead(DS3231_MONTH); //读取数据月
Data_Year = IIC_DS3231_ByteRead(DS3231_YEAR); //读取数据年
Low=Data_Sec&0x0f; //取低四位
High=(( Data_Sec& 0xf0) >> 4); //取高四位
RTC_Sec=High*10+Low; //转换秒(高四位*10+低四位)
Low=Data_Min&0x0f;
High=(( Data_Min& 0xf0) >> 4);
RTC_Min=High*10+Low; //转换分
Low=Data_Hour&0x0f;
High=(( Data_Hour& 0x30) >> 4);
RTC_Hour=High*10+Low; //转换时(高两位*10+低四位)
RTC_Week=Data_Week; //转换周(不需要转换)
Low=Data_Date&0x0f;
High=(( Data_Date& 0xf0) >> 4);
RTC_Date=High*10+Low; //转换日
Low=Data_Month&0x0f;
High=(( Data_Month& 0x10) >> 4);
RTC_Month=High*10+Low; //转换月
Low=Data_Year&0x0f;
High=(( Data_Year& 0xf0) >> 4);
RTC_Year=((Data_Month>>7)+20)*100+High*10+Low; //转换年(世纪*100+高四位*10+低四位)
}
void IIC_DS3231_WriteAll(u16 Year,u8 Month,u8 Date,u8 Week,u8 Hour,u8 Min,u8 Sec)
{
u8 Sec_Date,Min_Date,Hour_Date,Week_Date,Date_Date,Month_Date,Year_Date;
u8 H_Bit,L_Bit;
H_Bit=(Sec/10)<<4; //取高四位
L_Bit=Sec%10; //取低四位
Sec_Date=H_Bit|L_Bit; //合并成八位
H_Bit=(Min/10)<<4;
L_Bit=Min%10;
Min_Date=H_Bit|L_Bit;
H_Bit=Hour/10;
L_Bit=Hour%10;
Hour_Date=(H_Bit<<4)|L_Bit;
Week_Date=Week;
H_Bit=Date/10;
L_Bit=Date%10;
Date_Date=(H_Bit<<4)|L_Bit;
if(Year/100==20)
{
H_Bit=Month/10;
L_Bit=Month%10;
Month_Date=(H_Bit<<4)|L_Bit;
H_Bit=(Year-2000)/10;
L_Bit=(Year-2000)%10;
Year_Date=(H_Bit<<4)|L_Bit;
}
else
{
H_Bit=Month/10+8; //05h第8位如果是1,则为22世纪
L_Bit=Month%10;
Month_Date=(H_Bit<<4)|L_Bit;
H_Bit=(Year-2100)/10;
L_Bit=(Year-2100)%10;
Year_Date=(H_Bit<<4)|L_Bit;
}
IIC_DS3231_ByteWrite(DS3231_YEAR,Year_Date);
IIC_DS3231_ByteWrite(DS3231_MONTH,Month_Date);
IIC_DS3231_ByteWrite(DS3231_DATE,Date_Date);
IIC_DS3231_ByteWrite(DS3231_WEEK,Week_Date);
IIC_DS3231_ByteWrite(DS3231_HOUR,Hour_Date);
IIC_DS3231_ByteWrite(DS3231_MIN,Min_Date);
IIC_DS3231_ByteWrite(DS3231_SEC,Sec_Date);
}
void DS3231_Init(void) //DS3231初始化
{
IIC_DS3231_ByteWrite(0x0E,0x40); //使能1Hz方波,使能振荡器
IIC_DS3231_ByteWrite(0x0F,0x00); //启动振荡器
Delay_ms(100); //延时等待完全起振
}
延时函数,IIC软件模拟需要用到延时函数,这里我采用的是滴答定时器进行延时,需要注意,F1 与F4的延时函数不能共用,因为主频不一样。
/****************************************************************
* Function: Delay_us
* Description: Microsecond delay.
* Input: nus
* Output:
* Return:
*****************************************************************/
void Delay_us(u16 nus)
{
//Delay_Init();
u32 temp;
SysTick->LOAD = SystemCoreClock / 8000000 * nus; /* Time load (SysTick-> LOAD is 24bit) */
SysTick->VAL = 0x000000; /* Empty counter */
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; /* Start the countdown */
do
{
temp = SysTick->CTRL;
}
while(temp&0x01 && !(temp&(1<<16))); /* Wait time is reached */
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; /* Close Counter */
SysTick->VAL = 0x000000; /* Empty counter */
}
/****************************************************************
* Function: Delay_ms
* Description: Millisecond delay.
* Input: nms
* Output:
* Return:
*****************************************************************/
void Delay_ms(u16 nms)
{
u32 temp;
while(nms > 500) //24位定时器最大计数值2^24=16777216,计时器时钟频率初始化为21MHz,最大计时时间为16777216/21000000=0.798s=798ms,所以需要多次复位
{
SysTick->LOAD = SystemCoreClock / 8000 * 500; /* Time load (SysTick-> LOAD is 24bit) */
SysTick->VAL = 0x000000; /* Empty counter */
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; /* Start the countdown */
do
{
temp = SysTick->CTRL;
}
while(temp&0x01 && !(temp&(1<<16))); /* Wait time is reached */
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; /* Close Counter */
SysTick->VAL = 0x000000; /* Empty counter */
nms -= 500;
}
SysTick->LOAD = SystemCoreClock / 8000 * nms; /* Time load (SysTick-> LOAD is 24bit) */
SysTick->VAL = 0x000000; /* Empty counter */
SysTick->CTRL |= SysTick_CTRL_ENABLE_Msk; /* Start the countdown */
do
{
temp = SysTick->CTRL;
}
while(temp&0x01 && !(temp&(1<<16))); /* Wait time is reached */
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; /* Close Counter */
SysTick->VAL = 0x000000; /* Empty counter */
}
嘀嗒定时器初始化:在启用延时函数前必须对定时器进行初始化,配置对应的参数。这里我用主频HCLK的八分频做为其定时频率,即21MHz。也就是一秒钟对应定时器的21000000。
void Delay_Init(void) //嘀嗒计时器初始化,用于Delay函数
{
SysTick_CLKSourceConfig(SysTick_CLKSource_HCLK_Div8); //计数器频率:168M/8=21MHZ
SysTick->CTRL &= ~SysTick_CTRL_ENABLE_Msk; /* Disability SysTick counter */
}
main函数:初始化延时函数,初始化DS3231 ,讲时间写入DS3231,讲时间读取出来
#include<stm32f4xx.h> //F4头文件
void main(void)
{
Delay_Init(); //嘀嗒定时器初始化,必须初始化,延时依靠嘀嗒计时器
DS3231_Init(); //DS3231初始化,唤醒DS3231,并启动振荡器
IIC_DS3231_WriteAll(2023,10,1,1,12,00,00); //设置时间函数,实例为2023年10月1日12:00:00
IIC_DS3231_ReadAll(); //读取DS3231所有时间数据
}
最终读取转换后的时钟数据分别为这些变量:u8 RTC_Sec,RTC_Min,RTC_Hour,RTC_Week,RTC_Date,RTC_Month;
u16 RTC_Year;
可以通过串口打印,或者其他方式显示出当前时间。
这里我将时间显示到LCD屏幕上。
如果觉得本文有用,就点个赞吧~