STM32后备区域:读写BKP备份寄存器与使用RTC实时时钟详解

目录

STM32后备区域:读写BKP备份寄存器与使用RTC实时时钟详解

[1 什么是STM32的后备区域](#1 什么是STM32的后备区域)

分割线*

[2.1 BKP备份寄存器简介](#2.1 BKP备份寄存器简介)

[2.2 BKP备份寄存器基本结构](#2.2 BKP备份寄存器基本结构)

[2.3 BKP外设头文件 bkp.h介绍](#2.3 BKP外设头文件 bkp.h介绍)

[2.4 读写 BKP备份寄存器 操作步骤](#2.4 读写 BKP备份寄存器 操作步骤)

[2.5 编写 读写备份寄存器](#2.5 编写 读写备份寄存器)

[5.1 文件介绍](#5.1 文件介绍)

main.c

分割线*

[3.1 什么是Unix时间戳](#3.1 什么是Unix时间戳)

[3.2 C语言中时间戳转换函数](#3.2 C语言中时间戳转换函数)

[2.1 C语言提供的time.h函数介绍](#2.1 C语言提供的time.h函数介绍)

[2.2 时间戳转换图](#2.2 时间戳转换图)

[3.3 RTC简介](#3.3 RTC简介)

[3.4 RTC时钟选择](#3.4 RTC时钟选择)

[3.5 RTC框图](#3.5 RTC框图)

[3.6 RTC基本框图](#3.6 RTC基本框图)

[3.7 RTC硬件电路](#3.7 RTC硬件电路)

[3.8 RTC头文件介绍](#3.8 RTC头文件介绍)

[3.9 使用 RTC实时时钟 操作步骤](#3.9 使用 RTC实时时钟 操作步骤)

[3.10 编写RTC实时时钟显示年月日时分秒](#3.10 编写RTC实时时钟显示年月日时分秒)

[3.10.1 文件介绍](#3.10.1 文件介绍)

MyRTC.c

MyRTC.h

main.c


STM32后备区域:读写BKP备份寄存器与使用RTC实时时钟详解

1 什么是STM32的后备区域

STM32 的后备区域是芯片内部的一个特殊区域。

特点和作用

  • 后备区域可以在主电源VCC断电之后由备用电源VBA提供供电。
  • 后备区域存储的数据不会因为复位而重置
  • 但是如果包括主电源和VBAT都断电了。那么备用区域也会清除,因为他们的存储器本质是RAM存储器,掉电丢失。

后备区域有什么:

  • BKP备份寄存器
  • RTC实时时钟

分割线*

下面开始BKP部分

2.1 BKP备份寄存器简介

BKP(Backup Registers)备份寄存器(后备寄存器)

它位于后备区域。

  • BKP可用于存储数据。

  • 存储特性:

    1. 当VDD(2.0~3.6V)电源(主电源)被切断,后备区域仍然由VBAT备用电源(1.8~3.6V)维持供电。
    2. 并且系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位
    3. 但是如果备用电源VBAT和主电源VCC都断电了,就会清除数据,因为BKP本质是RAM存储器。掉电丢失数据
  • STM32的TAMPER引脚

    他可以产生的侵入事件可以将所有备份寄存器内容清除

    1. TAMPER是用于引入检测信号(可以是或上升沿/下降沿)的,当发生入侵时,将清除BKP所有内容,并申请中断。
    2. 并且他是由备用电源供电,主电源断电后侵入检测仍然有效,以保证数据安全
  • BKP的RTC引脚可以输出RTC校准时钟、RTC闹钟脉冲或者秒脉冲

  • BKP存储RTC时钟校准寄存器

  • STM32后备区域的供电特性:

    1. 当VDD主电源掉电时,后备区域仍然可以由VBAT的备用电池供电。
    2. 当VDD主电源上电时,后备区域供电会自动从VBAT切换到VDD。
  • BKP数据存储容量:

    1. 20字节(中容量和小容量)/ 84字节(大容量和互联型)
  • 手册建议

    1. 如果没有外部电池,建议VBAT引脚接到VDD,就是VBAT和主电源接到一起,并且再连接一个100nf的滤波电容

2.2 BKP备份寄存器基本结构

其中橙色部分就是后备区域,BKP处于后备区域。但后备区域不止有BKP。

STM32后备区域的特性:

  • 当VDD主电源掉电时,后备区域仍然可以由VBAT的备用电池供电。
  • 当VDD主电源上电时,后备区域供电会自动从VBAT切换到VDD。

数据寄存器:

  • 每个寄存器有16位(可存储2个字节)。
  • 中小容量设备有DR1~DR10,总共10个数据寄存器,每个寄存器存储2个字节,总容量为20字节。
  • 大容量和互联型设备有42个数据寄存器(DR)。

TAMPER引脚:

  • 用于引入检测信号(上升沿/下降沿),清除BKP所有内容以保证数据安全。

时钟输出:

  • 可以将RTC相关时钟从PC13位置的RTC引脚输出出去,供外部使用。
  • 输出校准时钟时,可以配合校准寄存器对RTC的误差进行校准。

2.3 BKP外设头文件 bkp.h介绍

void BKP_DeInit(void);

  • 恢复缺省配置(用于清空BKP所有BKP寄存器)

void BKP_TamperPinLevelConfig(uint16_t BKP_TamperPinLevel);

  • 配置TAMPER引脚的有效电平

void BKP_TamperPinCmd(FunctionalState NewState);

  • 是否开启侵入检测功能

void BKP_ITConfig(FunctionalState NewState);

  • 是否开启中断

void BKP_RTCOutputConfig(uint16_t BKP_RTCOutputSource);

  • 时钟输出功能配置(在RTC引脚上输出时钟信号、RTC校准时钟、RTC闹钟脉冲、秒脉冲)

void BKP_SetRTCCalibrationValue(uint8_t CalibrationValue);

  • 设置RTC校准值

void BKP_WriteBackupRegister(uint16_t BKP_DR, uint16_t Data);

  • 写BKP备份寄存器
    uint16_t BKP_ReadBackupRegister(uint16_t BKP_DR);

  • 读BKP寄存器

FlagStatus BKP_GetFlagStatus(void);

查看标志位

void BKP_ClearFlag(void);

清除标志位

ITStatus BKP_GetITStatus(void);

查看中断标志位

void BKP_ClearITPendingBit(void);

清除中断标志位

pwr.h中也有一个函数是需要用到的 void PWR_BackupAccessCmd(FunctionalState NewState);

  • 备份寄存器访问使能(其实是设置BDP位)

2.4 读写 BKP备份寄存器 操作步骤

  1. 启PWR和BKP的时钟
    • PWR的开启函数为RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); 它的目的是开启后备电源的时钟(可以理解为开启后备电源VBAT)。
    • BKP的开启函数RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE); 他的目的是开启BKP外设的时钟,可以看到他们都是APB1总线下的。
  2. 使能备份区域的访问
    • 因为RTC实时时钟和BKP备份寄存器,都处于备份区域中。在STM32中,想访问RTC和BKP,就要先开启备份区域的访问权限。它在PwR.h中 函数:PWR_BackupAccessCmd(ENABLE);
  3. 读写操作
    • 写入:BKP_WriteBackupRegister(BKP_DR1,Data);
    • 读出:Data = BKP_ReadBackupRegister(BKP_DR1);

2.5 编写 读写备份寄存器

5.1 文件介绍

  • main.c : 读写测试BKP备份寄存器是否在单主电源掉电时丢失数据。是否受到复位影响...

main.c

#include "stm32f10x.h"                  // Device header
#include "oled.h"
#include "Delay.h"
#include "Key.h"

/**
  * 函    数:STM32 BKP备份寄存器的读写测试
  * 参    数:无
  * 返 回 值:无
  * 注意事项:无
  */
  
uint16_t WriteArr[] = {0x0000, 0x0001};
uint16_t ReadArr[2] = { 0 };
  
int main()
{
    OLED_Init();//初始化OLED;
    KEY_Init();//初始化按键
    
    //使能时钟电源和后备接口时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR,ENABLE);
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP,ENABLE);
    
    //使能备份访问控制
    PWR_BackupAccessCmd(ENABLE);
    
    OLED_ShowString(1,1,"W:");
    OLED_ShowString(2,1,"R:");
    while(1)
    {
        if(KEY_Get() == 1)
        {
            //写入BKP
            BKP_WriteBackupRegister(BKP_DR1,WriteArr[0]);
            BKP_WriteBackupRegister(BKP_DR2,WriteArr[1]);
            
            //显示写入的值
            OLED_ShowHexNum(1,3,WriteArr[0],4);
            OLED_ShowHexNum(1,8,WriteArr[1],4);
            
            WriteArr[0]++;
            WriteArr[1]++;
        }
        //读取BKP
        ReadArr[0] = BKP_ReadBackupRegister(BKP_DR1);
        ReadArr[1] = BKP_ReadBackupRegister(BKP_DR2);
        //显示读取的值
        OLED_ShowHexNum(2,3,ReadArr[0],4);
        OLED_ShowHexNum(2,8,ReadArr[1],4);
        
    }
}

分割线*

下面开始RTC部分

3.1 什么是Unix时间戳

  • Unix时间戳,最早是Unix系统使用的。所以叫Unix时间戳。

  • 目前Windows、linux、安卓等系统都是实用的Unix时间戳。

  • Unix 时间戳(Unix Timestamp)定义为从UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数,不考虑闰秒。

    • GMT(Greenwich Mean Time)格林尼治标准时间是一种以地球自转为基础的时间计量系统。它将地球自转一周的时间间隔等分为24小时,以此确定计时标准 (不过他不精准,因为地球自转是越来越慢的。所以又有了如下的规定)

      (格林尼治是伦敦的一个区)

    • UTC(Universal Time Coordinated)协调世界时是一种以原子钟为基础的时间计量系统。它规定铯133原子基态的两个超精细能级间在零磁场下跃迁辐射9,192,631,770周所持续的时间为1秒。当原子钟计时一天的时间与地球自转一周的时间相差超过0.9秒时,UTC会执行闰秒来保证其计时与地球自转的协调一致

    • 我们时间戳所说的1970年1月1日0时0分0秒。指的是伦敦伦敦时间的0时0秒。 其他的位置可以分为24个时区。每偏差一个时区,相应的时间就要加或减一个小时

  • 时间戳存储在一个秒计数器中,秒计数器为32位/64位的整型变量

  • 世界上所有时区的秒计数器相同 ,不同时区通过添加偏移来得到当地时间 比如Unix时间戳为0,代表伦敦的0时0分,那么北京就是+8得到8时0分

  • C语言官方为我们提供了函数,可以直接把Unix时间戳转换为时间。

  • 可以在百度直接搜索unix在线时间戳。

3.2 C语言中时间戳转换函数

2.1 C语言提供的time.h函数介绍

  • C语言的time.h模块提供了时间获取和时间戳转换的相关函数,可以方便地进行秒计数器、日期时间和字符串之间的转换

  • 一些常用的函数如下:(粗细为更重要)

    time_t time(time_t*); 获取系统时钟
    struct tm* gmtime(const time_t*); 秒计数器转换为日期时间(格林尼治时间)
    struct tm* localtime(const time_t*); 秒计数器转换为日期时间(当地时间)
    time_t mktime(struct tm*); 日期时间转换为秒计数器(当地时间)
    char* ctime(const time_t*); 秒计数器转换为字符串(默认格式)
    char* asctime(const struct tm*); 日期时间转换为字符串(默认格式)
    size_t strftime(char*, size_t, const char*, const struct tm*); 日期时间转换为字符串(自定义格式)

2.2 时间戳转换图

这张图就清晰了显示了各个函数的作用:其实就是在各种数据类型之间进行转换。

  • 首先先了解一下各个数据类型是什么
    1. 秒计数器数据类型:time_t,其实就是一个32或64位的有符号的整形数据。也就是64位的秒计数器(如果不特别声明,默认为64。)

    2. 日期时间数据类型:struct tm,这时一个结构体类型。成员如下:

      • 秒、
      • 分、
      • 时、
      • 月的几日、
      • 月份、(需要+1偏移量)
      • 年份(需要+1900偏移量)、
      • 周某开始的星期几、从1月1日开始的第几天、
      • 是否使用夏令时 (是为了鼓励夏天时早睡早起节约用电设计的,目前个别国家在使用)
    3. 字符串型数据类型:char*,用来指向一个表示时间的字符串

所以,在转换时,要根据函数的返回值,来进行相应赋值。比如struct tm* gmtime(const time_t*);的返回值为Struct tm的指针类型。那么在赋值给自己定义的Struct tm xxx 结构体时,就要先用*取值,才能正确赋值

等等以此类推,需要根据不同的类型进行转换,

(time_t 整形、Struct tm 结构体 Char* 字符指针..)

3.3 RTC简介

RTC (Real Time Clock):实时时钟。

  • RTC也位于STM32的后备区域中。可以由VBAT备用电源供电
  • RTC是一个独立的定时器,可为系统提供时钟和日历的功能。
  • RTC和时钟配置系统处于后备区域,系统复位时数据不清零,因为在VDD(2.0 - 3.6V)断电后可借助VBAT(1.8 - 3.6V)供电继续走时。
  • RTCCLK可选择三种RTC时钟源:
    1. HSE时钟除以128(通常为8MHz/128)
    2. LSE振荡器时钟(通常为32.768KHz)
    3. LSI振荡器时钟(40KHz)

3.4 RTC时钟选择

时钟信号解释

  • HSE = 高速外部时钟信号

  • HSI = 高速内部时钟信号

  • LSI = 低速内部时钟信号

  • LSE = 低速外部时钟信号

  • H (High):高速,L (Low):低速,E (External):外部,I (Internal):内部

时钟选择

  • 外部高速时钟:一般作为系统主时钟。供内部程序运行和主要外设使用。
  • 内部低速时钟:主要用做看门狗等的使用。
  • 外部低速时钟:这个时钟一般都是为了给RTC提供时钟的。
    1. 因为LSE外部低速时钟,才可以通过VBAT备用电池供电
    2. 所以在主电源断电情况下LSE可以继续震荡,实现RTC主电源掉电继续走时的功能。
    3. 并且他的频率为32.768KHZ。经过2^15分频之后每次自然溢出时刚好为1s,也就是1HZ。

3.5 RTC框图

灰色填充区域均是后备区域。

可编程预分频器:

  • RTC_CNT每秒自增,因此驱动计数器的时钟 TR_CLK 需要是1Hz的信号。 实际提供RTC模块的时钟(RTCCLK)频率较高,因此RTCCLK经过20位RTC预分频器(1~2^20分频),保证输出给计数器的频率为1Hz。

分频和计数:

  • 输入时钟RTCCLK,经过RTC预分频器(由重装载寄存器RTC_PRL和余数寄存器RTC_DIV控制),计数器重装值ARR和CNT进行分频。

RTC_CNT:

  • 可以作为Unix时间戳的秒计数器,再借用time.h的函数可以方便地得到年月日时分秒。

闹钟寄存器RTC_ALR:

  • 32位寄存器,用来设置闹钟。设置闹钟时,将ALR写入一个秒数,当CNT的值等于ALR设定的闹钟值时,就会产生RTC_Alarm闹钟信号。 通过中断系统,在闹钟中断里执行相应操作。
  • 同时,闹钟信号可以让STM32退出待机模式。 此外,这个闹钟值是一个定值,只能响一次。若想实现周期闹钟,在每次闹钟响过后,都需要重新设置下一次闹钟时间。

中断信号:

  • RTC_Second(秒中断):来自于CNT的输入时钟。开启此中断后,程序会每秒进入一次RTC中断。
  • RTC_Overflow(溢出中断):来自CNT右边,表示CNT的32位计数器计满溢出时触发一次中断。
  • RTC_Alarm(闹钟中断):当计数器的值和闹钟值相等时触发中断,同时可以唤醒设备退出待机模式。

中断标志位和中断输出控制

  • F(Flag) 结尾的是对应的中断标志位。
  • IE(Interrupt Enable) 结尾的是中断使能。
  • 最后三个信号通过一个或汇聚到NVIC中断控制器。

APB1总线和APB1接口:

  • 程序读写寄存器的地方可以通过APB1总线完成,RTC位于APB1总线上的设备。 退出待机模式:唤醒机制
  • 闹钟信号和WKUP引脚都可以唤醒设备,退出待机模式。

3.6 RTC基本框图

时钟来源配置:

  • 最左边的RTCCLK时钟来源在RCC中配置,可以从三个时钟中选择一个作为RTCCLK。

时钟预分频:

  • 选择的RTCCLK经过预分频器对时钟进行分频。
  • 余数计数器是一个自减计数器,存储当前的计数值。
  • 重装寄存器决定计数目标和分频值。
  • 分频后得到1Hz的秒计数信号,传递给32位计数器,每秒自增一次。

闹钟设定:

  • 32位计数器下有一个32位的闹钟值,可以设定闹钟时间。

中断信号触发:

  • 右侧有三个信号可以触发中断:秒信号、计数器溢出信号和闹钟信号。
  • 这三个信号通过中断输出控制,进行中断使能。
  • 启用的中断信号才能传递到NVIC,然后向CPU申请中断。

程序配置步骤:

**配置数据选择器:**选择RTCCLK时钟来源。

**配置重装寄存器:**选择分频系数。

配置32位计数器:

  • 进行日期时间的读写。
  • 如果需要闹钟,配置32位闹钟值。

配置中断:

  • 启用中断,再配置NVIC。
  • 最后,编写对应的中断函数。

3.7 RTC硬件电路

  1. 备用电池供电
    • **简单连接(左侧):**使用一个3V的电池B1直接连接到VBAT和GND。这样设计简单,但是电源冗余不高。
    • **推荐连接(中间):**使用两个3V的电池B2和B3通过两个二极管D1和D2连接到VBAT和GND。这样设计增加了电源的可靠性,因为如果一个电池失效,另一个电池还能提供电源。电容C3(0.1uF)用于滤波,稳定电压。
  2. 外部低速晶振
    • **晶振部分(中间):**使用一个32.768kHz的晶振(X1)连接到两个10pF的电容(C1和C2),并接地。这部分电路提供了一个稳定的时钟信号,通常用于RTC(实时时钟)功能。
    • **连接到STM32单片机(右侧):**OSC32_IN和OSC32_OUT分别连接到STM32单片机的PC14和PC15引脚。
  3. STM32单片机连接
    • **供电和地(右侧):**VDD和VSS分别是电源和地,VDD连接到电源正极,VSS连接到地。VBAT连接到备用电池供电部分的输出。
    • **时钟信号(右侧):**PC14和PC15分别连接到外部低速晶振的OSC32_IN和OSC32_OUT。
    • **其他引脚(右侧):**图中列出了STM32F103C8T6单片机的引脚配置,包括PA0到PA15,PB0到PB15等。这些引脚可以根据具体应用进行配置。

3.8 RTC头文件介绍

void RTC_ITConfig(uint16_t RTC_IT, FunctionalState NewState);

  • RTC 中断使能:通过传入指定的中断类型 RTC_IT 和状态 NewState,实现对 RTC 中断的使能或失能控制。

void RTC_EnterConfigMode(void);

  • 进入 RTC 配置模式:用于进入 RTC 的配置状态,以便进行相关参数的修改。

void RTC_ExitConfigMode(void);

  • 退出 RTC 配置模式:在完成 RTC 配置操作后,使用此函数退出配置模式。

uint32_t RTC_GetCounter(void);

  • 获取 RTC 计数器的值:返回 RTC 计数器的当前数值。

void RTC_SetCounter(uint32_t CounterValue);

  • 设置 RTC 计数器的值:将 RTC 计数器设置为指定的数值 CounterValue

void RTC_SetPrescaler(uint32_t PrescalerValue);

  • 设置 RTC 预分频的值:为 RTC 预分频设置特定的值 PrescalerValue

void RTC_SetAlarm(uint32_t AlarmValue);

  • 设置 RTC 闹钟的值:设定 RTC 闹钟的触发值为 AlarmValue

uint32_t RTC_GetDivider(void);

  • 获取 RTC 预分频分频因子的值:获取当前 RTC 预分频分频因子的数值。

void RTC_WaitForLastTask(void);

  • 等待最近一次对 RTC 寄存器的写操作完成:确保之前对 RTC 寄存器的写入操作已经完成。

void RTC_WaitForSynchro(void);

  • 等待 RTC 寄存器与 RTC 的 APB 时钟同步:等待 RTC 相关寄存器(如 RTC_CNTRTC_ALRRTC_PRL)与 APB 时钟完成同步。

FlagStatus RTC_GetFlagStatus(uint16_t RTC_FLAG);

  • 检查指定的 RTC 标志位设置与否:通过传入标志位 RTC_FLAG,返回其状态。

void RTC_ClearFlag(uint16_t RTC_FLAG);

  • 清除 RTC 的待处理标志位:清除指定的 RTC 标志位。

ITStatus RTC_GetITStatus(uint16_t RTC_IT);

  • 检查指定的 RTC 中断发生与否:根据传入的中断类型 RTC_IT,判断中断是否发生。

void RTC_ClearITPendingBit(uint16_t RTC_IT);

  • 清除 RTC 的中断待处理位:清除指定 RTC 中断的待处理位。

3.9 使用 RTC实时时钟 操作步骤

RTC使用时,一般都是和BKP一起使用的:

因为RTC在初始化时,需要配置其32位的时间戳计数器。

但每次复位或者主电源上电后,在执行主程序时,会重新初始化RTC的时间戳计数器。这就导致了RTC实时时钟本来能在备份电源的供给下计时,但是上电后又给我重新初始化覆盖了。导致不实时了!

所以需要在初始化程序中判断是否需要设置32位的时间戳计数器。

一般的方法是,在第一次初始化RTC时,顺便在BKP备份寄存器中写入特定值。在下一次初始化RTC时,对BKP备份寄存器的值进行判断。如果之前没有写入,那么就初始化。否则不初始化。

所以使用RTC实时时钟操作步骤如下:

  • 执行以下操作将使能对BKP和RTC的访问:

    1. 启PWR和BKP的时钟

      • PWR的开启函数为RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); 开启后备电源的时钟
      • BKP的开启函数RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE); 开启BKP外设的时钟
    2. 使能备份区域的访问

      • 在STM32中,想访问RTC和BKP,就要先开启备份区域的访问权限。

      函数:PWR_BackupAccessCmd(ENABLE);

  • 判断是否需要初始化RTC见代码部分

  • 开启LSE时钟

    1. 开启LSE时钟 RCC_LSEConfig(RCC_LSE_ON);
    2. 等待LSE时钟开启完毕 while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET);
    3. 选择RTCCLK时钟为LSERCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
    4. 使能RTCCLK时钟 RCC_RTCCLKCmd(ENABLE);
  • 等待时钟同步

    1. 因为可能在恢复主电源之后,APB1总线刚刚恢复震荡频率,但是RTCCLK需要经过外部震荡源分频后才能有一次输出。如果直接读取,会导致读取不准确。 所以需要等待RTCCLK产生上升沿来激活更新一下时间戳计数器,这时APB1直接读取。才是准确的。 所以软件读取时必须等待RTCCLK来一个上升沿,使RTC_CRL寄存器中的**RSF位(寄存器同步标志)被硬件置1。**这时再读取RTC_CRL寄存器。才能正确读取。

      函数:RTC_WaitForSynchro(); (等待时钟同步)

  • 等待写入完成

    1. 对RTC任何寄存器的写操作,都必须在前一次写操作结束后进行。

    2. 可以通过查询RTC_CR寄存器中的RTOFF状态位,判断RTC寄存器是否处于更新中。 当RTOFF状态位是1时,才可以写入RTC寄存器

      函数:RTC_WaitForLastTask();

  • 设置RTC预分频器

    1. 设置预分频器RTC_SetPrescaler(32768 - 1);
    2. 等待写入完成RTC_WaitForLastTask();
  • 写入前其实是需要设置RTC_CRL寄存器中的CNF位 (函数会帮我们自动完成,所以不需要)

    1. 在写入时,必须设置RTC_CRL寄存器中的CNF位, 使RTC进入配置模式后,才能写入RTC_PRL(预分频器)、RTC_CNT(时间戳计数器)、RTC_ALR(闹钟寄存器)寄存器
  • 设置CNT时间戳计数器时间

    (利用C语言中time.h来转换时间戳并写入,详见代码)

  • 在BKP备份寄存器中写入特定数据。为下次复位或仅主电源断电后上电时判断是否初始化打下基础

3.10 编写RTC实时时钟显示年月日时分秒

3.10.1 文件介绍

  • MyRTC.c 初始化RTC、通过C语言time.h函数来编写 时间转换时间戳、时间戳转换时间的函数
  • MyRTC.h 函数声明
  • main.c 测试MyRTC显示

MyRTC.c

#include "stm32f10x.h"                  // Device header
#include <time.h>

uint16_t MyRTC_Time[] = {2024, 8, 18, 23, 46, 0};   //定义全局的时间数组,数组内容分别为年、月、日、时、分、秒

void MyRTC_SetTime(void);           //函数声明

/**
  * 函    数:RTC初始化
  * 参    数:无
  * 返 回 值:无
  */
void MyRTC_Init(void)
{
    /*开启时钟*/
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);     //开启PWR的时钟
    RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);     //开启BKP的时钟
    
    /*备份寄存器、RTC访问使能*/
    PWR_BackupAccessCmd(ENABLE);                            //使用PWR开启对备份寄存器和RTC的访问
    
    if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)          //通过写入备份寄存器的标志位,判断RTC是否是第一次配置
                                                            //if成立则执行第一次的RTC初始化
    {
        RCC_LSEConfig(RCC_LSE_ON);                          //开启LSE时钟
        while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET);  //等待LSE准备就绪
        
        RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);             //选择RTCCLK来源为LSE
        RCC_RTCCLKCmd(ENABLE);                              //RTCCLK使能
                                        
        RTC_WaitForSynchro();                               //等待同步
        RTC_WaitForLastTask();                              //等待上一次操作完成
        
        RTC_SetPrescaler(32768 - 1);                        //设置RTC预分频器,预分频后的计数频率为1Hz
        RTC_WaitForLastTask();                              //等待上一次操作完成
        
        MyRTC_SetTime();                                    //设置时间,调用此函数,全局数组里时间值刷新到RTC硬件电路
        
        BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);           //在备份寄存器写入自己规定的标志位,用于判断RTC是不是第一次执行配置
    }
    else                                                    //RTC不是第一次配置
    {
        RTC_WaitForSynchro();                               //等待同步
        RTC_WaitForLastTask();                              //等待上一次操作完成
    }
}
/**
  * 函    数:RTC设置时间
  * 参    数:无
  * 返 回 值:无
  * 说    明:调用此函数后,全局数组里时间值将刷新到RTC硬件电路
  */
void MyRTC_SetTime(void)
{
    time_t time_cnt = 0;    //定义秒计数器数据类型
    struct tm time_date;    //定义日期时间数据类型
    
    time_date.tm_year = MyRTC_Time[0] - 1900;       //将数组的时间赋值给日期时间结构体
    time_date.tm_mon = MyRTC_Time[1] - 1;
    time_date.tm_mday = MyRTC_Time[2];
    time_date.tm_hour = MyRTC_Time[3];
    time_date.tm_min = MyRTC_Time[4];
    time_date.tm_sec = MyRTC_Time[5];
    
    time_cnt = mktime(&time_date) - 8 * 60 * 60;    //调用mktime函数,将日期时间转换为秒计数器格式
                                                    //- 8 * 60 * 60为东八区的时区调整
    
    RTC_SetCounter(time_cnt);                       //将秒计数器写入到RTC的CNT中
    RTC_WaitForLastTask();                          //等待上一次操作完成
}

/**
  * 函    数:RTC读取时间
  * 参    数:无
  * 返 回 值:无
  * 说    明:调用此函数后,RTC硬件电路里时间值将刷新到全局数组
  */
void MyRTC_ReadTime(void)
{
    time_t time_cnt;        //定义秒计数器数据类型
    struct tm time_date;    //定义日期时间数据类型
    
    time_cnt = RTC_GetCounter() + 8 * 60 * 60;      //读取RTC的CNT,获取当前的秒计数器
                                                    //+ 8 * 60 * 60为东八区的时区调整
    
    time_date = *localtime(&time_cnt);              //使用localtime函数,将秒计数器转换为日期时间格式
    
    MyRTC_Time[0] = time_date.tm_year + 1900;       //将日期时间结构体赋值给数组的时间
    MyRTC_Time[1] = time_date.tm_mon + 1;
    MyRTC_Time[2] = time_date.tm_mday;
    MyRTC_Time[3] = time_date.tm_hour;
    MyRTC_Time[4] = time_date.tm_min;
    MyRTC_Time[5] = time_date.tm_sec;
}

MyRTC.h

#ifndef __MYRTC_H
#define __MYRTC_H

extern uint16_t MyRTC_Time[];

void MyRTC_Init(void);

void MyRTC_SetTime(void);

void MyRTC_ReadTime(void);

#endif

main.c

#include "stm32f10x.h"                  // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyRTC.h"

int main(void)
{
    /*模块初始化*/
    OLED_Init();        //OLED初始化
    MyRTC_Init();       //RTC初始化
    
    /*显示静态字符串*/
    OLED_ShowString(1, 1, "Date:XXXX-XX-XX");
    OLED_ShowString(2, 1, "Time:XX:XX:XX");
    OLED_ShowString(3, 1, "CNT :");
    OLED_ShowString(4, 1, "DIV :");
    
    while (1)
    {
        MyRTC_ReadTime();                           //RTC读取时间,最新的时间存储到MyRTC_Time数组中
        
        OLED_ShowNum(1, 6, MyRTC_Time[0], 4);       //显示MyRTC_Time数组中的时间值,年
        OLED_ShowNum(1, 11, MyRTC_Time[1], 2);      //月
        OLED_ShowNum(1, 14, MyRTC_Time[2], 2);      //日
        OLED_ShowNum(2, 6, MyRTC_Time[3], 2);       //时
        OLED_ShowNum(2, 9, MyRTC_Time[4], 2);       //分
        OLED_ShowNum(2, 12, MyRTC_Time[5], 2);      //秒
        
        OLED_ShowNum(3, 6, RTC_GetCounter(), 10);   //显示32位的秒计数器
        OLED_ShowNum(4, 6, RTC_GetDivider(), 10);   //显示余数寄存器
    }
}
相关推荐
yutian06062 小时前
Keil MDK下载程序后MCU自动重启设置
单片机·嵌入式硬件·keil
析木不会编程4 小时前
【小白51单片机专用教程】protues仿真独立按键控制LED
单片机·嵌入式硬件·51单片机
枯无穷肉8 小时前
stm32制作CAN适配器4--WinUsb的使用
stm32·单片机·嵌入式硬件
不过四级不改名6779 小时前
基于HAL库的stm32的can收发实验
stm32·单片机·嵌入式硬件
嵌入式科普9 小时前
十一、从0开始卷出一个新项目之瑞萨RA6M5串口DTC接收不定长
c语言·stm32·cubeide·e2studio·ra6m5·dma接收不定长
嵌入式大圣9 小时前
单片机UDP数据透传
单片机·嵌入式硬件·udp
云山工作室9 小时前
基于单片机的视力保护及身姿矫正器设计(论文+源码)
stm32·单片机·嵌入式硬件·毕业设计·毕设
嵌入式-老费9 小时前
基于海思soc的智能产品开发(mcu读保护的设置)
单片机·嵌入式硬件
qq_3975623111 小时前
MPU6050 , 设置内部低通滤波器,对于输出数据的影响。(简单实验)
单片机
liyinuo201711 小时前
嵌入式(单片机方向)面试题总结
嵌入式硬件·设计模式·面试·设计规范