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

**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中,对单片机来说,只要通过串口读取特定寄存器地址的数据就能得到时间参数,而无需再去计算。现在流行的时钟芯片很多,如DS1302DS1307。但是这些芯片仍需要外置晶振才能工作,所以仍存在误差。

所以一种在内部集成晶振的时钟芯片应运而生。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,稍微修改下就行。

cpp 复制代码
#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的延时函数不能共用,因为主频不一样。

cpp 复制代码
/****************************************************************
 * 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。

cpp 复制代码
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,讲时间读取出来

cpp 复制代码
#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屏幕上。

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

相关推荐
智者知已应修善业2 小时前
【51单片机用数码管显示流水灯的种类是按钮控制数码管加一和流水灯】2022-6-14
c语言·经验分享·笔记·单片机·嵌入式硬件·51单片机
智商偏低8 小时前
单片机之helloworld
单片机·嵌入式硬件
青牛科技-Allen9 小时前
GC3910S:一款高性能双通道直流电机驱动芯片
stm32·单片机·嵌入式硬件·机器人·医疗器械·水泵、
森焱森11 小时前
无人机三轴稳定控制(2)____根据目标俯仰角,实现俯仰稳定化控制,计算出升降舵输出
c语言·单片机·算法·架构·无人机
白鱼不小白11 小时前
stm32 USART串口协议与外设(程序)——江协教程踩坑经验分享
stm32·单片机·嵌入式硬件
S,D12 小时前
MCU引脚的漏电流、灌电流、拉电流区别是什么
驱动开发·stm32·单片机·嵌入式硬件·mcu·物联网·硬件工程
芯岭技术15 小时前
PY32F002A单片机 低成本控制器解决方案,提供多种封装
单片机·嵌入式硬件
youmdt15 小时前
Arduino IDE ESP8266连接0.96寸SSD1306 IIC单色屏显示北京时间
单片机·嵌入式硬件
嘿·嘘15 小时前
第七章 STM32内部FLASH读写
stm32·单片机·嵌入式硬件
Meraki.Zhang16 小时前
【STM32实践篇】:I2C驱动编写
stm32·单片机·iic·驱动·i2c