STM32实时时钟(RTC)代码深度解析 | 零基础入门STM32第三十九步

主题 内容 教学目的/扩展视频
RTC时钟的使用重点课程 RTC时钟的原理,电路原理分析,固件库分析,驱动程序分析。在超级终端上显示时钟。 做可修改的超级终端显示RTC的项目。

师从洋桃电子,杜洋老师


文章目录

    • 一、RTC初始化流程分析
      • 1.1 时钟与备份域配置
      • 1.2 初始化检测机制
    • 二、时间处理核心算法
      • 2.1 闰年判断算法
      • 2.2 时间戳转换(Unix时间)
    • 三、时间读取与转换
      • 3.1 读取计数器值
      • 3.2 星期计算算法
    • 四、中断处理机制
      • 4.1 秒中断配置
      • 4.2 闹钟中断配置
    • 五、LED显示逻辑分析
      • 5.1 显示控制代码
      • 5.2 硬件连接建议
    • 六、常见问题解决方案
      • 6.1 晶振不起振
      • 6.2 时间误差过大
      • 6.3 后备电池失效
    • 七、相关资源


▲ 回顾上期STM32实时时钟(RTC)开发详解:从晶振原理到LED动态显示 | 零基础入门STM32第三十八步


一、RTC初始化流程分析

1.1 时钟与备份域配置

void RTC_First_Config(void) {
    // 1. 开启APB1总线时钟(PWR和BKP模块)
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
    
    // 2. 解除后备寄存器写保护
    PWR_BackupAccessCmd(ENABLE);
    
    // 3. 复位备份寄存器
    BKP_DeInit();
    
    // 4. 启动外部低速晶振(LSE)
    RCC_LSEConfig(RCC_LSE_ON);
    while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET); // 等待晶振稳定
    
    // 5. 选择LSE作为RTC时钟源
    RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
    
    // 6. 使能RTC时钟
    RCC_RTCCLKCmd(ENABLE);
    
    // 7. 等待时钟同步
    RTC_WaitForSynchro();
    RTC_WaitForLastTask();
    
    // 8. 设置预分频器(32767+1分频)
    RTC_SetPrescaler(32767);  // 32.768kHz / 32768 = 1Hz
    RTC_WaitForLastTask();
}

关键点说明

  1. 必须同时使能PWR和BKP时钟才能操作后备域
  2. BKP_DeInit()会清除所有备份寄存器内容
  3. 预分频器值计算公式:分频系数 = 32768 - 1

1.2 初始化检测机制

if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5) {
    // 首次初始化流程
    RTC_First_Config();//重新配置RTC
    BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); // 设置初始化标记
}

工作原理

  • 利用BKP_DR1作为"指纹寄存器"
  • 断电后VBAT维持备份域供电
  • 检测到非0xA5A5时执行初始化

二、时间处理核心算法

2.1 闰年判断算法

u8 Is_Leap_Year(u16 year) {                    
    if(year%4==0) {          // 能被4整除
        if(year%100==0) {    // 整百年处理
            if(year%400==0)return 1; // 必须能被400整除
       		else return 0;
        }else return 1;           // 非整百年直接返回闰年
    }else return 0;               // 不能被4整除则非闰年
}

算法特点

  • 符合格里高利历法规则
  • 处理范围:1900-2099年
  • 返回结果:1(闰年)/0(平年)

2.2 时间戳转换(Unix时间)

u8 RTC_Set(u16 syear,u8 smon,u8 sday,u8 hour,u8 min,u8 sec){ //写入当前时间(1970~2099年有效),
	u16 t;
	u32 seccount=0;
	if(syear<2000||syear>2099)return 1;//syear范围1970-2099,此处设置范围为2000-2099       
	for(t=1970;t<syear;t++){ //把所有年份的秒钟相加
		if(Is_Leap_Year(t))seccount+=31622400;//闰年的秒钟数
		else seccount+=31536000;                    //平年的秒钟数
	}
	smon-=1;
	for(t=0;t<smon;t++){         //把前面月份的秒钟数相加
		seccount+=(u32)mon_table[t]*86400;//月份秒钟数相加
		if(Is_Leap_Year(syear)&&t==1)seccount+=86400;//闰年2月份增加一天的秒钟数        
	}
	seccount+=(u32)(sday-1)*86400;//把前面日期的秒钟数相加
	seccount+=(u32)hour*3600;//小时秒钟数
	seccount+=(u32)min*60;      //分钟秒钟数
	seccount+=sec;//最后的秒钟加上去
	RTC_First_Config(); //重新初始化时钟
	BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);//配置完成后,向后备寄存器中写特殊字符0xA5A5
	RTC_SetCounter(seccount);//把换算好的计数器值写入
	RTC_WaitForLastTask(); //等待写入完成
	return 0; //返回值:0,成功;其他:错误代码.    
}

计算公式

总秒数 = 年累计秒数 + 月累计秒数 + 天累计秒数 
        + 小时秒数 + 分钟秒数 + 秒数

三、时间读取与转换

3.1 读取计数器值

u8 RTC_Get(void){//读出当前时间值 //返回值:0,成功;其他:错误代码.
	static u16 daycnt=0;
	u32 timecount=0;
	u32 temp=0;
	u16 temp1=0;
	timecount=RTC_GetCounter();		
	temp=timecount/86400;   //得到天数(秒钟数对应的)
	if(daycnt!=temp){//超过一天了
		daycnt=temp;
		temp1=1970;  //从1970年开始
		while(temp>=365){
		     if(Is_Leap_Year(temp1)){//是闰年
			     if(temp>=366)temp-=366;//闰年的秒钟数
			     else {temp1++;break;} 
		     }
		     else temp-=365;       //平年
		     temp1++; 
		}  
		ryear=temp1;//得到年份
		temp1=0;
		while(temp>=28){//超过了一个月
			if(Is_Leap_Year(ryear)&&temp1==1){//当年是不是闰年/2月份
				if(temp>=29)temp-=29;//闰年的秒钟数
				else break;
			}else{
	            if(temp>=mon_table[temp1])temp-=mon_table[temp1];//平年
	            else break;
			}
			temp1++; 
		}
		rmon=temp1+1;//得到月份
		rday=temp+1;  //得到日期
	}
	temp=timecount%86400;     //得到秒钟数      
	rhour=temp/3600;     //小时
	rmin=(temp%3600)/60; //分钟     
	rsec=(temp%3600)%60; //秒钟
	rweek=RTC_Get_Week(ryear,rmon,rday);//获取星期  
	return 0;
}  

转换流程

  1. 计算总天数 → 年
  2. 剩余天数 → 月
  3. 剩余天数 → 日
  4. 剩余秒数 → 时分秒

3.2 星期计算算法

u8 RTC_Get_Week(u16 year,u8 month,u8 day){ //按年月日计算星期(只允许1901-2099年)//已由RTC_Get调用    
	u16 temp2;
	u8 yearH,yearL;
	yearH=year/100;     
	yearL=year%100;
	// 如果为21世纪,年份数加100 
	if (yearH>19)yearL+=100;
	// 所过闰年数只算1900年之后的 
	temp2=yearL+yearL/4;
	temp2=temp2%7;
	temp2=temp2+day+table_week[month-1];
	if (yearL%4==0&&month<3)temp2--;
	return(temp2%7); //返回星期值
}

返回值对应

  • 0 = 星期日
  • 1 = 星期一
  • 6 = 星期六

四、中断处理机制

4.1 秒中断配置

void RTC_IRQHandler(void){ //RTC时钟1秒触发中断函数(名称固定不可修改)
	if (RTC_GetITStatus(RTC_IT_SEC) != RESET){

	}
	RTC_ClearITPendingBit(RTC_IT_SEC); 
	RTC_WaitForLastTask();
}

典型应用

  • 实时刷新显示
  • 定时数据采集
  • 低功耗唤醒

4.2 闹钟中断配置

void RTCAlarm_IRQHandler(void){	//闹钟中断处理(启用时必须调高其优先级)
	if(RTC_GetITStatus(RTC_IT_ALR) != RESET){
	
	}
	RTC_ClearITPendingBit(RTC_IT_ALR);
	RTC_WaitForLastTask();
}

注意事项

  • 闹钟寄存器比较的是秒计数值
  • 需提前计算目标时间戳
  • 优先级应高于其他中断

五、LED显示逻辑分析

5.1 显示控制代码

while(1) {
    if(RTC_Get()==0){ //读出时间值,同时判断返回值是不是0,非0时读取的值是错误的。	
			GPIO_WriteBit(LEDPORT,LED1,(BitAction)(rsec%2)); //LED1接口
			GPIO_WriteBit(LEDPORT,LED2,(BitAction)(rmin%2)); //LED2接口;
    }
}

显示规律

  • LED1:1Hz闪烁(亮500ms/灭500ms)
  • LED2:1/60Hz变化(每分钟切换状态)

5.2 硬件连接建议

信号 GPIO引脚 LED驱动方式
LED1
LED2
PB0
PB1
推挽输出+1kΩ限流电阻

▲ 完整工程代码示例⏬LED灯显示RTC走时程序


六、常见问题解决方案

6.1 晶振不起振

可能原因

  1. 负载电容不匹配(建议6pF)
  2. PCB布局不良(晶振距离MCU过远)
  3. 晶振质量差(选择±20ppm精度)

6.2 时间误差过大

校准方法

// 软件校准(单位:ppm)
RTC_CalibOutputCmd(ENABLE);
RTC_CalibOutputConfig(RTC_CalibOutput_512Hz);

硬件校准

  • 使用示波器测量RTCCLK输出
  • 调节TRIMMER电容补偿误差

6.3 后备电池失效

检测方法

if(RCC_GetFlagStatus(RCC_FLAG_PORRST)) {
    // 电源复位,可能丢失时间
    LCD_ShowError("RTC数据丢失!");
}

七、相关资源

[1] 洋桃电子B站课程-STM32入门100步
[2] STM32F103xx官方数据手册
[3] STM32F103X8-B数据手册(中文)
[4] STM32F103固件函数库用户手册(中文)
[5] LED灯显示RTC走时程序


技术讨论(请在评论区留言~)

下期预告:下一期将探讨超级终端显示日历程序重点课程,欢迎持续关注!

点击查阅往期【STM32专栏】文章

版权声明:本文采用[CC BY-NC-SA 4.0]协议,转载请注明来源
实测开发版:洋桃1号开发版(基于STM32F103C8T6)
更新日志

  • v1.0 初始版本(2025-02-28)

你可能感兴趣的:(STM32,stm32,单片机,嵌入式硬件)