1 BKP基本介绍
- BKP(backup register,备份寄存器)。备份寄存器是42个16位的寄存器,可用来存储84个字节的用户应用程序数据。他们处在备份域里,当VDD电源被切断,他们仍然由VBAT维持供电。当系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位。
- 此外,BKP控制寄存器用来管理侵入检测和RTC校准功能。
2 BKP案例
2.1 register版本
-
案例需求:读写备份寄存器的值,验证掉电不丢失。
-
Driver_bkp.c
写数据时,第4.1步需要复位备份域,但读数据时就需要将其注释,否者数据会被清空。
#include "Driver_bkp.h"
void Driver_BKP_Init(void)
{
/* 1. 打开电源控制时钟PWR */
RCC->APB1ENR |= RCC_APB1ENR_PWREN;
/* 2. 打开后备寄存器的时钟BKP */
RCC->APB1ENR |= RCC_APB1ENR_BKPEN;
/* 3. 打开后备寄存器的写保护 */
PWR->CR |= PWR_CR_DBP;
/* 4. 设置合理的RTC时钟 */
// 位于RCC的BDCR中
// 4.1 复位备份域 -> 复位完成记得清空
RCC->BDCR |= RCC_BDCR_BDRST;
Delay_ms(1);
RCC->BDCR &= ~RCC_BDCR_BDRST;
// 4.2 RTC时钟使能
RCC->BDCR |= RCC_BDCR_RTCEN;
//4.3 LSE外部低速时钟使能
RCC->BDCR |= RCC_BDCR_LSEON;
//4.4 等待LSE准备就绪
while ((RCC->BDCR & RCC_BDCR_LSERDY) == 0)
;
//4.5 选择RTC的时钟源为LSE
RCC->BDCR &= ~RCC_BDCR_RTCSEL_1;
RCC->BDCR |= RCC_BDCR_RTCSEL_0;
}
- main.c
写数据,读数据时把
BKP->DR1
赋值那行代码注释,以观察掉电不丢失。
int main(void)
{
/* 测试bkp备份域模式 */
Driver_USART1_Init();
Driver_Key_Init();
printf("hello bkp...\n");
Driver_BKP_Init();
BKP->DR1 = 9527;
Delay_ms(100);
printf("现在DR1中的值为%d\n",BKP->DR1);
while (1)
{
}
}
2.2 HAL库版本
- CubeMX配置
- main.c
/* USER CODE BEGIN 2 */
// 备份域已经初始化完成 => 只需要开启RTC时钟 默认会初始化备份域
// 直接写入对应的数据
HAL_RTCEx_BKUPWrite(&hrtc,RTC_BKP_DR1,9527);
HAL_Delay(1);
// 直接读取备份域的数据
uint32_t num = HAL_RTCEx_BKUPRead(&hrtc,RTC_BKP_DR1);
printf("后备域的值DR1为%d\n", num);
/* USER CODE END 2 */
3 RTC 基本介绍
RTC(Real Time Clock,实时时钟)。是一个掉电后仍然可以继续运行的独立定时器。RTC模块拥有一个连续计数的计数器,在相应的软件配置下,可以提供时钟日历的功能。修改计数器的值可以重新设置当前时间和日期 RTC还包含用于管理低功耗模式的自动唤醒单元。
3.1 RTC功能框图
3.2 RTC的时钟源
- RTC有3路时钟来源:HSE(8MHz)/128,LSE(32.768KHz),LSI(40KHz)。
- 一般的通用做法是使用LSE。2个原因:
- 一是LSE不受主电源掉电的影响,
- 二是它的频率是我们都是选择32768Hz,正好是2^15,分频容易实现。
3.3 APB1接口
用来和APB1总线相连。此单元还包含一组 16 位寄存器,可通过APB1总线对其进行读写操作。APB1接口由APB1总线时钟驱动,用来与APB1总线连接。
通过APB1接口可以访问RTC的相关寄存器(预分频值,计数器值,闹钟值)。
3.4 RTC预分频模块
RTC预分频模块,属于后备区域,VDD掉电后,可以在VBAT下继续运行。包含了一个20位的可编程分频器(RTC预分频器)。它可编程产生 1 秒的 RTC 时间基准 TR_CLK。如果在RTC_CR寄存器中设置了相应的允许位,则在每个TR_CLK周期中RTC产生一个中断(秒中断)。
3.5 32位可编程计数器
这个模块也属于后备区域,是一个32位的可编程计数器,可被初始化为当前的系统时间。一个32位的时钟计数器,按秒钟计算,可以记录4294967296秒,约合136年左右。
3.6 中断
(1)秒中断:每计时1s产生一次中断。
(2)计数器溢出中断。136年才会产生溢出,一般用不上。
(3)RTC闹钟中断。RCT_CN和RTC_ALR会比较相等,如果相等表示闹钟时间到,会产生闹钟中断。
4 案例
4.1 RTC案例一:实时时钟
需求描述:显示时间。通过串口把时间发送给电脑显示。即使关机很多天,再启动后也能正确显示时间。
4.1.1 项目准备
1. 初始化RTC
(1) 打开电源控制PWR时钟
(2) 打开备份域时钟BKP时钟
(3) 默认清空下 备份域写保护的 -> 打开写保护 PWR_CR
(4) 打开LSE 同时切换对应的RTC时钟 使能
(5) 等待RTOFF为1
(6) 进入到配置模式
(7) 修改预分频值
(8) 退出配置模式
2. 设置时间戳的值
(1) 等待RTOFF为1
(2) 进入到配置模式
(3) 修改CNT的值 => 等待秒标志
(4) 退出配置模式
3. 读取时间戳的值转换为日期时间
(1) 读取RTC中的值需要等待 寄存器同步
(2) 读取CNT的值 拼接为32位数字 + 8 * 3600
(3) localtime方法 转换32位数字为结构体tm
(4) 年份+1900 月份+1 0时区的时间(如果想要修改为东八区的时间)
4.1.2 register版本
- Driver_RTC.c
#include "Driver_RTC.h"
void Driver_RTC_Init(void)
{
/* 1. 打开时钟 PWR BKP */
RCC->APB1ENR |= RCC_APB1ENR_PWREN;
RCC->APB1ENR |= RCC_APB1ENR_BKPEN;
/* 2. 打开备份区域的写保护 */
PWR->CR |= PWR_CR_DBP;
/* 3. 配置LSE外部低速时钟 作为RTC的系统时钟 */
// 3.1 开启LSE外部低速时钟
RCC->BDCR |= RCC_BDCR_LSEON;
while ((RCC->BDCR & RCC_BDCR_LSERDY) == 0)
;
// 3.2 选择外部低速时钟作为RTC系统时钟
// 01
RCC->BDCR &= ~RCC_BDCR_RTCSEL_1;
RCC->BDCR |= RCC_BDCR_RTCSEL_0;
// 3.3 开启RTC时钟
RCC->BDCR |= RCC_BDCR_RTCEN;
/* 4. 配置RTC => 计数器 主要配置预分频系数 */
// 4.1 等待RTOFF标志位为1 才能进行后续的操作
while ((RTC->CRL & RTC_CRL_RTOFF) == 0)
;
// 4.2 置为CNF 进入到配置模式
RTC->CRL |= RTC_CRL_CNF;
// 4.3 修改其他寄存器的值 => 主要修改预分频系数 PRL
// 计算频率 = rtcclk(32768) / (prl + 1)
// RTC->PRLH = 32767 >> 16;
RTC->PRLL = 0X7FFF;
// 4.4 清除CNF位 退出配置模式
RTC->CRL &= ~RTC_CRL_CNF;
}
void Driver_RTC_set_timestamp(uint32_t timestamp)
{
/* 将timestamp存入到CNT中 */
// 1. 等待RTOFF标志位
while ((RTC->CRL & RTC_CRL_RTOFF) == 0)
;
// 2. 进入配置模式
RTC->CRL |= RTC_CRL_CNF;
// 3. 对其他寄存器进行配置
// 3.1 等到秒标志
while ((RTC->CRL & RTC_CRL_SECF) == 0)
;
// 3.2 将timpstamp写入到CNT中
RTC->CNTH = timestamp >> 16;
RTC->CNTL = timestamp & 0xffff;
// 4. 退出配置模式
RTC->CRL &= ~RTC_CRL_CNF;
}
void Driver_RTC_get_dateTime(DateTimeStruct *datetime)
{
// 1. 等到寄存器同步
while ((RTC->CRL & RTC_CRL_RSF) == 0)
;
// 2. 读出CNT的值
uint32_t timestamp = (RTC->CNTH << 16) | (RTC->CNTL) + 8 * 3600;
// 3. 将CNT的值转换为年月日时分秒
struct tm *datetime_tm = localtime(×tamp);
datetime->year = datetime_tm->tm_year + 1900;
datetime->month = datetime_tm->tm_mon + 1;
datetime->day = datetime_tm->tm_mday;
datetime->hour = datetime_tm->tm_hour ;
datetime->minute = datetime_tm->tm_min;
datetime->second = datetime_tm->tm_sec;
}
- Driver_RTC.h
#ifndef __DRIVER_RTC_H
#define __DRIVER_RTC_H
#include "stm32f10x.h"
#include "time.h"
typedef struct
{
uint16_t year;
uint8_t month;
uint8_t day;
uint8_t hour;
uint8_t minute;
uint8_t second;
}DateTimeStruct;
void Driver_RTC_Init(void);
void Driver_RTC_set_timestamp(uint32_t timestamp);
void Driver_RTC_get_dateTime(DateTimeStruct *datetime);
#endif
-
main.c
Driver_RTC_set_timestamp
设置好后第二次读就可以注释掉了,因为RTC掉电不丢失。
int main(void)
{
/* 实现实时时钟 */
Driver_USART1_Init();
printf("hello rtc...\n");
Driver_RTC_Init();
// Driver_RTC_set_timestamp(1729234016);
DateTimeStruct datetime;
while (1)
{
Driver_RTC_get_dateTime(&datetime);
printf("当前时间为%04d-%02d-%02d %02d:%02d:%02d\n",
datetime.year,
datetime.month,
datetime.day,
datetime.hour,
datetime.minute,
datetime.second);
Delay_ms(1000);
}
}
4.1.3 实验现象
4.1.4 HAL库版本
- main.c
// 开发板备份域存有RTC的值 直接使用即可
RTC_TimeTypeDef time,pre_time;
RTC_DateTypeDef date,pre_date;
pre_time.Hours = 15;
pre_time.Minutes = 39;
pre_time.Seconds = 0;
pre_date.Year = 54;
pre_date.Month = 10;
pre_date.Date = 18;
HAL_RTC_SetTime(&hrtc,&pre_time,RTC_FORMAT_BIN);
HAL_RTC_SetDate(&hrtc,&pre_date,RTC_FORMAT_BIN);
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
HAL_RTC_GetTime(&hrtc, &time, RTC_FORMAT_BIN);
HAL_RTC_GetDate(&hrtc, &date, RTC_FORMAT_BIN);
printf("现在的时间是%04d-%02d-%02d %02d:%02d:%02d\n",
(uint16_t)date.Year + 1970, date.Month, date.Date, time.Hours, time.Minutes, time.Seconds);
HAL_Delay(1000);
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
}
4.2 RTC案例2:使用闹钟唤醒待机模式
执行完毕正常代码之后,让MCU进入待机模式,设置闹钟,让MCU在设定时间内从待机模式中被唤醒。
4.2.1 项目准备
1. 初始化RTC
(1) 打开电源控制PWR时钟
(2) 打开备份域时钟BKP时钟
(3) 默认清空下 备份域写保护的 -> 打开写保护 PWR_CR
(4) 打开LSE 同时切换对应的RTC时钟 使能
(5) 等待RTOFF为1
(6) 进入到配置模式
(7) 修改预分频值
(8) 退出配置模式
2. 设置闹钟的值
(0) 清空闹钟标志
(1) 等待RTOFF为1
(2) 进入到配置模式
(3) 等到秒标志
(4) 修改CNT为0
(5) 修改alarm的值为second-1
(6) 退出配置模式
4.2.2 register版本
- Driver_RTC.c
在上个案例的同名文件加上一个函数即可
void Driver_RTC_set_alarm(uint32_t second)
{
/* 0. 清理闹钟标志位 */
RTC->CRL &= ~RTC_CRL_ALRF;
/* 1. 等待RTOFF */
while ((RTC->CRL & RTC_CRL_RTOFF) == 0)
;
/* 2. 进入配置模式 */
RTC->CRL |= RTC_CRL_CNF;
/* 3. 等待秒标志 */
while ((RTC->CRL & RTC_CRL_SECF) == 0)
;
/* 4. 先设置CNT为0 */
RTC->CNTH = 0;
RTC->CNTL = 0;
/* 5. 再设置alarm的值 */
// RTC->ALRH = (second - 1) >> 16;
RTC->ALRL = (second - 1);
/* 6. 退出配置模式 */
RTC->CRL &= ~RTC_CRL_CNF;
}
- main.c
#include "Driver_USART1.h"
#include "Delay.h"
#include "Driver_RTC.h"
void enter_standby_mode(void)
{
// 开启PWR的RCC时钟需要提前进行
/* 1. 选择睡眠模式为深度睡眠 */
SCB->SCR |= SCB_SCR_SLEEPDEEP;
/* 2. 深睡眠模式对应的是待机模式 */
PWR->CR |= PWR_CR_PDDS;
/* 3. 使用PA0的wake up功能退出待机模式 */
PWR->CSR |= PWR_CSR_EWUP;
/* 4. 进入到待机模式 */
__WFI();
}
int main(void)
{
/* 实现实时时钟 */
Driver_USART1_Init();
printf("hello rtc...\n");
Driver_RTC_Init();
// 提前打开RCC中的PWR时钟
RCC->APB1ENR |= RCC_APB1ENR_PWREN;
// 每次从main方法的起始位置运行的时候 => 通过唤醒标志位判断是否从待机模式唤醒
if (PWR->CSR & PWR_CSR_WUF)
{
printf("从待机模式被唤醒\n");
// 从待机模式唤醒 需要清除对应的标志位
PWR->CR |= PWR_CR_CSBF;
PWR->CR |= PWR_CR_CWUF;
}
else
{
printf("复位键按下运行\n");
}
while (1)
{
printf("代码运行完成 5s后进入到待机模式\n");
Delay_ms(5000);
// 提前设置好闹钟
Driver_RTC_set_alarm(10);
Delay_ms(1);
enter_standby_mode();
}
}
4.2.3 实验现象
可见唤醒时间如预期一样间隔15s
4.2.4 HAL库版本
CubeMX配置
项目准备
(1) 使用cube 构建代码
(2) 修改系统时钟 => 打开USART1 用于printf输出 => 添加LED2作为正常运行模式的显示
(3) 打开RTC时钟 修改时钟源为LSE 32.768kHz
(4) 修改项目名称 => 创建代码
(5) 修改keil => 勾选lib => 修改debug => 添加对应的文件和目录
设置闹钟的方法
(1) 获取当前的时间
HAL_RTC_GetTime();
(2) 设置闹钟的时间
HAL_RTC_SetAlarm(); alarm.alarmtime.second = time.second + s - 1;
- main.c
/* USER CODE BEGIN 2 */
HAL_GPIO_WritePin(LED2_GPIO_Port, LED2_Pin, GPIO_PIN_RESET);
printf("hello standby...\n");
// 判断是否从待机模式启动
if (__HAL_PWR_GET_FLAG(PWR_CSR_WUF))
{
// 从待机模式被唤醒
printf("从待机模式被唤醒\n");
__HAL_PWR_CLEAR_FLAG(PWR_CSR_WUF);
__HAL_PWR_CLEAR_FLAG(PWR_CSR_SBF);
}
else
{
printf("从复位启动\n");
}
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
printf("代码执行完成 5s后进入待机模式\n");
HAL_Delay(5000);
// 使用封装后的设置闹钟方法
User_set_alarm(5);
HAL_Delay(1);
HAL_SuspendTick();
HAL_PWR_EnterSTANDBYMode();
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
- rtc.c
/* USER CODE BEGIN 1 */
void User_set_alarm(uint32_t second)
{
// 获取当前的时间
RTC_TimeTypeDef time;
HAL_RTC_GetTime(&hrtc,&time,RTC_FORMAT_BIN);
// 加填写的参数的秒数 再写入
RTC_AlarmTypeDef alarm;
alarm.AlarmTime.Hours = time.Hours;
alarm.AlarmTime.Minutes = time.Minutes;
alarm.AlarmTime.Seconds = time.Seconds + second - 1;
HAL_RTC_SetAlarm(&hrtc,&alarm,RTC_FORMAT_BIN);
}
/* USER CODE END 1 */