提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
提示:以下是本篇文章正文内容,下面案例可供参考
一、RTC是什么?
RTC时钟,全称为Real-Time Clock(实时时钟),用于提供精确时间信息。RTC时钟是一个独立的定时器,它包含一个连续计数的计数器,在相应的软件配置下,可以提供时钟日历的功能(F1没有更高级别的单片机有这个功能)。通过寄存器配置可以产生1HZ的时钟信号作为精确的时间基准,计数器根据晶体振荡器的分频后的时钟信号进行递增,从而追踪时间的流逝。
简单点来说就是:
通过配置后RTC的计数器每隔1秒加一。
计数器不受单片机复位和断电影响(注意:需要适当的算法,和VBAT电源,下文有详细讲解)。
二、RTC配置步骤
/* 使能电源时钟 // 使能备份时钟 /
/ 取消备份区写保护 /
/ 开启外部低速振荡器 /
/ 等待LSE准备好 */
/* 选择LSE,作为RTC时钟 */
/* 使能RTC时钟 /
/ 等待RTC寄存器操作完成 *
/* 等待RTC寄存器同步 */
允许秒中断 */ 按照需求配置
允许闹钟中断 / 之后再次/ 等待RTC寄存器操作完成 /检查 RTOFF位
/ 允许配置 /相关位清置1
RTC控制寄存器低位(RTC_CRL)
此位必须由软件置’1’以进入配置模式,从而允许向RTC_CNT、RTC_ALR或RTC_PRL寄存器
写入数据。只有当此位在被置’1’并重新由软件清’0’后,才会执行写操作。
/ LSE频率为32.768Khz /配置预分频
/ 配置更新 / 与允许配置相对应 相关位清零
/ 等待RTC寄存器操作完成 */
将上面步骤转化为代为:
简单介绍一下功能,配置RTC为1HZ,启用秒中断和时钟中断;
void rtc_init(void)
{
RCC->APB1ENR |= 0x01<<28; /* PWREN 使能电源时钟 */
RCC->APB1ENR |= 0x01<<27; /* BKPEN 使能备份时钟 */
PWR->CR |= 0x01<<8; /* DBP 取消备份区写保护 */
RCC->BDCR |= 1; /* 开启外部低速振荡器 */
while(!(RCC->BDCR & 0x02)); /* 等待LSE准备好 */
RCC->BDCR &=~(3<<8); /* 清零8/9位 方便下一步选择LSE,作为RTC时钟,因为选择时钟用了两位*/
RCC->BDCR|=1<<8; /* 选择LSE,作为RTC时钟 */
RCC->BDCR |= 1<<15; /* 使能RTC时钟 */
while(!(RTC->CRL & (1 << 5)));/* 等待RTC寄存器操作完成 */
while(!(RTC->CRL & (1 << 3)));/* 等待RTC寄存器同步 */
RTC->CRH |= 1; /* SECF = 1, 允许秒中断 */
RTC->CRH |= 1 << 1; /* ALRF = 1, 允许闹钟中断 */
RTC->CRL |=1<<4; /* 允许配置 */
RTC->PRLH =0; /*配置分频系数*/
RTC->PRLL =32767; /*配置分频系数*/
RTC->CRL &= ~1; /* 配置更新 */
while(!(RTC->CRL & (1 << 5)));/* 等待RTC寄存器操作完成 */
sys_nvic_init(0, 0, RTC_IRQn, 2); /* 优先级设置 */
}
/**
* @brief 设置NVIC(包括分组/抢占优先级/子优先级等)
* @param pprio: 抢占优先级(PreemptionPriority)
* @param sprio: 子优先级(SubPriority)
* @param ch: 中断编号(Channel)
* @param group: 中断分组
* @arg 0, 组0: 0位抢占优先级, 4位子优先级
* @arg 1, 组1: 1位抢占优先级, 3位子优先级
* @arg 2, 组2: 2位抢占优先级, 2位子优先级
* @arg 3, 组3: 3位抢占优先级, 1位子优先级
* @arg 4, 组4: 4位抢占优先级, 0位子优先级
* @note 注意优先级不能超过设定的组的范围! 否则会有意想不到的错误
* @retval 无
*/
void sys_nvic_init(uint8_t pprio, uint8_t sprio, uint8_t ch, uint8_t group)
{
uint32_t temp;
sys_nvic_priority_group_config(group); /* 设置分组 */
temp = pprio << (4 - group);
temp |= sprio & (0x0f >> group);
temp &= 0xf; /* 取低四位 */
NVIC->ISER[ch / 32] |= 1 << (ch % 32); /* 使能中断位(要清除的话,设置ICER对应位为1即可) */
NVIC->IP[ch] |= temp << 4; /* 设置响应优先级和抢断优先级 */
}
三、正点原子寄存器库
.h文件
#ifndef __RTC_H
#define __RTC_H
#include "./SYSTEM/sys/sys.h"
/* 时间结构体, 包括年月日周时分秒等信息 */
typedef struct
{
uint8_t hour; /* 时 */
uint8_t min; /* 分 */
uint8_t sec; /* 秒 */
/* 公历年月日周 */
uint16_t year; /* 年 */
uint8_t month; /* 月 */
uint8_t date; /* 日 */
uint8_t week; /* 周 */
} _calendar_obj;
extern _calendar_obj calendar; /* 时间结构体 */
/* 静态函数 */
static uint8_t rtc_is_leap_year(uint16_t year); /* 判断当前年份是不是闰年 */
static long rtc_date2sec(uint16_t syear, uint8_t smon, uint8_t sday, uint8_t hour, uint8_t min, uint8_t sec); /* 将年月日时分秒转换成秒钟数 */
/* 接口函数 */
uint8_t rtc_init(void); /* 初始化RTC */
void rtc_get_time(void); /* 获取RTC时间信息 */
uint16_t rtc_read_bkr(uint32_t bkrx); /* 读取后备寄存器 */
void rtc_write_bkr(uint32_t bkrx, uint16_t data); /* 写后备寄存器 */
uint8_t rtc_get_week(uint16_t year, uint8_t month, uint8_t day); /* 根据年月日获取星期几 */
uint8_t rtc_set_time(uint16_t syear, uint8_t smon, uint8_t sday, uint8_t hour, uint8_t min, uint8_t sec); /* 设置时间 */
uint8_t rtc_set_alarm(uint16_t syear, uint8_t smon, uint8_t sday, uint8_t hour, uint8_t min, uint8_t sec); /* 设置闹钟时间 */
#endif
.c文件
值得注意的是
在STM32微控制器中,晶振(晶体振荡器)通常用于提供时钟信号。一旦晶振起振,它会持续振荡,直到断电。如果不断电,即使进行复位操作,晶振本身还是会保持振荡状态。但是,微控制器内部的时钟电路可能会因为复位而受到影响。
因此需要再备份区设置一个标志(LSE已经起振)。复位后如果检测到备份区的标志,就不对LSE和RTC进行操作,不然每次复位都会对计数产生影响。
在25℃是晶振大概需要2.5s起振。
晶振的起振过程
/**
* @brief RTC写入后备区域SRAM
* @param bkrx : 后备区寄存器编号,范围:0~41
* @param data : 要写入的数据,16位长度
* @retval 无
*/
void rtc_write_bkr(uint32_t bkrx, uint16_t data)
{
uint32_t temp = 0;
PWR->CR |= 1 << 8; /* 取消备份区写保护 */
temp = BKP_BASE + 4 + bkrx * 4;
(*(uint16_t *)temp) = data;
}
/**
* @brief RTC读取后备区域SRAM
* @param bkrx : 后备区寄存器编号,范围:0~41
* @retval 读取到的值
*/
uint16_t rtc_read_bkr(uint32_t bkrx)
{
uint32_t temp = 0;
temp = BKP_BASE + 4 + bkrx * 4;
return (*(uint16_t *)temp); /* 返回读取到的值 */
}
/**
* @brief RTC初始化
* @note
* 默认尝试使用LSE,当LSE启动失败后,切换为LSI.
* 通过BKP寄存器0的值,可以判断RTC使用的是LSE/LSI:
* 当BKP0==0X5050时,使用的是LSE
* 当BKP0==0X5051时,使用的是LSI
* 注意:切换LSI/LSE将导致时间/日期丢失,切换后需重新设置.
*
* @param 无
* @retval 0,成功
* 1,进入初始化模式失败
*/
uint8_t rtc_init(void)
{
/* 检查是不是第一次配置时钟 */
uint16_t bkpflag = 0;
uint16_t retry = 200;
uint32_t tempreg = 0;
uint32_t clockfreq = 0;
RCC->APB1ENR |= 1 << 28; /* 使能电源时钟 */
RCC->APB1ENR |= 1 << 27; /* 使能备份时钟 */
PWR->CR |= 1 << 8; /* 取消备份区写保护 */
bkpflag = rtc_read_bkr(0); /* 读取BKP0的值 */
if (bkpflag != 0X5050) /* 之前使用的不是LSE */
{
RCC->BDCR |= 1 << 0; /* 开启外部低速振荡器 */
while (retry && ((RCC->BDCR & 0X02) == 0)) /* 等待LSE准备好 */
{
retry--;
delay_ms(5);
}
tempreg = RCC->BDCR; /* 读取BDCR的值 */
tempreg &= ~(3 << 8); /* 清零8/9位 */
if (retry == 0) /* 开启LSE失败? */
{
RCC->CSR |= 1 << 0; /* LSI使能 */
while (!(RCC->CSR & 0x02)); /* 等待LSI就绪 */
tempreg |= 1 << 9; /* LSE开启失败,启动LSI. */
clockfreq = 40000 - 1; /* LSI频率约40Khz(参考F103数据手册说明) */
rtc_write_bkr(0, 0X5051); /* 标记已经初始化过了,使用LSI */
}
else
{
tempreg |= 1 << 8; /* 选择LSE,作为RTC时钟 */
clockfreq = 32768 - 1; /* LSE频率为32.769Khz */
rtc_write_bkr(0, 0X5050); /* 标记已经初始化过了,使用LSE */
}
tempreg |= 1 << 15; /* 使能RTC时钟 */
RCC->BDCR = tempreg; /* 重新设置BDCR寄存器 */
while (!(RTC->CRL & (1 << 5))); /* 等待RTC寄存器操作完成 */
while (!(RTC->CRL & (1 << 3))); /* 等待RTC寄存器同步 */
RTC->CRH |= 1 << 0; /* SECF = 1, 允许秒中断 */
RTC->CRH |= 1 << 1; /* ALRF = 1, 允许闹钟中断 */
while (!(RTC->CRL & (1 << 5))); /* 等待RTC寄存器操作完成 */
RTC->CRL |= 1 << 4; /* 允许配置 */
RTC->PRLH = 0X0000;
RTC->PRLL = clockfreq; /* 时钟周期设置(有待观察,看是否跑慢了?)理论值:32767 */
RTC->CRL &= ~(1 << 4); /* 配置更新 */
while (!(RTC->CRL & (1 << 5))); /* 等待RTC寄存器操作完成 */
if (bkpflag != 0X5051) /* BKP0的内容既不是0X5050,也不是0X5051,说明是第一次配置,需要设置时间日期. */
{
rtc_set_time(2020, 4, 22, 6, 59, 55); /* 设置时间 */
}
}
else /* 系统继续计时 */
{
retry = 30; /* 避免卡死 */
while ((!(RTC->CRL & (1 << 3)) && retry)) /* 等待RTC寄存器同步 */
{
delay_ms(5);
retry--;
}
retry = 100; /* 检测LSI/LSE是否正常工作 */
tempreg = RTC->DIVL; /* 读取DIVL寄存器的值 */
while (retry)
{
delay_ms(5);
retry--;
if (tempreg != RTC->DIVL) /* 对比DIVL和tempreg, 如果有差异, 则退出 */
{
break; /* DIVL != tempreg, 说明RTC在计数, 说明晶振没问题 */
}
}
if (retry == 0)
{
rtc_write_bkr(0, 0XFFFF); /* 标记错误的值 */
RCC->BDCR = 1 << 16; /* 复位BDCR */
delay_ms(10);
RCC->BDCR = 0; /* 结束复位 */
return 1; /* 初始化失败 */
}
else
{
RTC->CRH |= 0X01; /* 允许秒中断 */
while (!(RTC->CRL & (1 << 5))); /* 等待RTC寄存器操作完成 */
}
}
sys_nvic_init(0, 0, RTC_IRQn, 2); /* 优先级设置 */
rtc_get_time(); /* 更新时间 */
return 0;
}
/**
* @brief RTC时钟中断
* @note 秒钟中断服务函数,顺带处理闹钟标志
* 根据RTC_CRL寄存器的 SECF 和 ALRF 位区分是哪个中断
* @param 无
* @retval 无
*/
void RTC_IRQHandler(void)
{
if (RTC->CRL & (1 << 0)) /* SECF = 1, 秒钟中断 */
{
rtc_get_time(); /* 更新时间 */
RTC->CRL &= ~(1 << 0); /* SECF = 0, 清秒钟中断 */
//printf("sec:%d\r\n", calendar.sec); /* 打印秒钟 */
}
/* 顺带处理闹钟标志 */
if (RTC->CRL & (1 << 1)) /* ALRF = 1, 闹钟标志 */
{
RTC->CRL &= ~(1 << 1); /* ALRF = 0, 清闹钟标志 */
/* 输出闹钟时间 */
printf("Alarm Time:%d-%d-%d %d:%d:%d\n", calendar.year, calendar.month, calendar.date, calendar.hour, calendar.min, calendar.sec);
}
RTC->CRL &= ~(1 << 2); /* OWF = 0, 清除溢出中断标志 */
while (!(RTC->CRL & (1 << 5))); /* 等待RTC寄存器操作完成, 即等待RTOFF == 1 */
}
四、HAL库底层配置
算法都是相同的,我用HAL库只实现到实现RTC计时的功能
#ifndef __RTC_H__
#define __RTC_H__
#include "sys.h"
#include "time.h"
void rtc_init(void);
uint16_t rtc_read_bkr(uint8_t bkrx);
void rtc_write_bkr(uint8_t bkrx, uint16_t data);
#endif
#include "rtc.h"
#include "stdio.h"
RTC_HandleTypeDef rtc_handle = {0};
void rtc_init(void)
{
__HAL_RCC_PWR_CLK_ENABLE();
__HAL_RCC_BKP_CLK_ENABLE();
HAL_PWR_EnableBkUpAccess();
rtc_handle.Instance = RTC;
rtc_handle.Init.AsynchPrediv = 32767;
rtc_handle.Init.OutPut = RTC_OUTPUTSOURCE_NONE;
HAL_RTC_Init(&rtc_handle);
}
void HAL_RTC_MspInit(RTC_HandleTypeDef *hrtc)
{
__HAL_RCC_RTC_ENABLE();
RCC_OscInitTypeDef osc_initstruct = {0};
RCC_PeriphCLKInitTypeDef periphclk_initstruct = {0};
osc_initstruct.OscillatorType = RCC_OSCILLATORTYPE_LSE;
osc_initstruct.LSEState = RCC_LSE_ON;
osc_initstruct.PLL.PLLState = RCC_PLL_NONE;
HAL_RCC_OscConfig(&osc_initstruct);
periphclk_initstruct.PeriphClockSelection = RCC_PERIPHCLK_RTC;
periphclk_initstruct.RTCClockSelection = RCC_RTCCLKSOURCE_LSE;
HAL_RCCEx_PeriphCLKConfig(&periphclk_initstruct);
HAL_NVIC_SetPriority(RTC_Alarm_IRQn, 2, 2);
HAL_NVIC_EnableIRQ(RTC_Alarm_IRQn);
}
void RTC_Alarm_IRQHandler(void)
{
}
uint16_t rtc_read_bkr(uint8_t bkrx)
{
uint32_t data = 0;
data = HAL_RTCEx_BKUPRead(&rtc_handle, bkrx);
return (uint16_t)data;
}
void rtc_write_bkr(uint8_t bkrx, uint16_t data)
{
HAL_RTCEx_BKUPWrite(&rtc_handle, bkrx, data);
}
下面是将时间戳换算成时间的相关算法
/**
* @brief 判断年份是否是闰年
* @note 月份天数表:
* 月份 1 2 3 4 5 6 7 8 9 10 11 12
* 闰年 31 29 31 30 31 30 31 31 30 31 30 31
* 非闰年 31 28 31 30 31 30 31 31 30 31 30 31
* @param year : 年份
* @retval 0, 非闰年; 1, 是闰年;
*/
static uint8_t rtc_is_leap_year(uint16_t year)
{
/* 闰年规则: 四年闰百年不闰,四百年又闰 */
if ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))
{
return 1;
}
else
{
return 0;
}
}
/**
* @brief 设置时间, 包括年月日时分秒
* @note 以1970年1月1日为基准, 往后累加时间
* 合法年份范围为: 1970 ~ 2105年
* @param syear : 年份
* @param smon : 月份
* @param sday : 日期
* @param hour : 小时
* @param min : 分钟
* @param sec : 秒钟
* @retval 0, 成功; 1, 失败;
*/
uint8_t rtc_set_time(uint16_t syear, uint8_t smon, uint8_t sday, uint8_t hour, uint8_t min, uint8_t sec)
{
uint32_t seccount = 0;
seccount = rtc_date2sec(syear, smon, sday, hour, min, sec); /* 将年月日时分秒转换成总秒钟数 */
/* 设置时钟 */
RCC->APB1ENR |= 1 << 28; /* 使能电源时钟 */
RCC->APB1ENR |= 1 << 27; /* 使能备份时钟 */
PWR->CR |= 1 << 8; /* 取消备份区写保护 */
/* 上面三步是必须的! */
RTC->CRL |= 1 << 4; /* 允许配置 */
RTC->CNTL = seccount & 0xffff;
RTC->CNTH = seccount >> 16;
RTC->CRL &= ~(1 << 4); /* 配置更新 */
while (!(RTC->CRL & (1 << 5))); /* 等待RTC寄存器操作完成 */
rtc_get_time(); /* 设置完之后更新一下数据 */
return 0;
}
/**
* @brief 设置闹钟, 具体到年月日时分秒
* @note 以1970年1月1日为基准, 往后累加时间
* 合法年份范围为: 1970 ~ 2105年
* @param syear : 年份
* @param smon : 月份
* @param sday : 日期
* @param hour : 小时
* @param min : 分钟
* @param sec : 秒钟
* @retval 0, 成功; 1, 失败;
*/
uint8_t rtc_set_alarm(uint16_t syear, uint8_t smon, uint8_t sday, uint8_t hour, uint8_t min, uint8_t sec)
{
uint32_t seccount = 0;
seccount = rtc_date2sec(syear, smon, sday, hour, min, sec); /* 将年月日时分秒转换成总秒钟数 */
/* 设置时钟 */
RCC->APB1ENR |= 1 << 28; /* 使能电源时钟 */
RCC->APB1ENR |= 1 << 27; /* 使能备份时钟 */
PWR->CR |= 1 << 8; /* 取消备份区写保护 */
/* 上面三步是必须的! */
RTC->CRL |= 1 << 4; /* 允许配置 */
RTC->ALRL = seccount & 0xffff;
RTC->ALRH = seccount >> 16;
RTC->CRL &= ~(1 << 4); /* 配置更新 */
while (!(RTC->CRL & (1 << 5))); /* 等待RTC寄存器操作完成 */
return 0;
}
/**
* @brief 得到当前的时间
* @note 该函数不直接返回时间, 时间数据保存在calendar结构体里面
* @param 无
* @retval 无
*/
void rtc_get_time(void)
{
static uint16_t daycnt = 0;
uint32_t seccount = 0;
uint32_t temp = 0;
uint16_t temp1 = 0;
const uint8_t month_table[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; /* 平年的月份日期表 */
seccount = RTC->CNTH; /* 得到计数器中的值(秒钟数) */
seccount <<= 16;
seccount += RTC->CNTL;
temp = seccount / 86400; /* 得到天数(秒钟数对应的) */
if (daycnt != temp) /* 超过一天了 */
{
daycnt = temp;
temp1 = 1970; /* 从1970年开始 */
while (temp >= 365)
{
if (rtc_is_leap_year(temp1)) /* 是闰年 */
{
if (temp >= 366)
{
temp -= 366; /* 闰年的秒钟数 */
}
else
{
break;
}
}
else
{
temp -= 365; /* 平年 */
}
temp1++;
}
calendar.year = temp1; /* 得到年份 */
temp1 = 0;
while (temp >= 28) /* 超过了一个月 */
{
if (rtc_is_leap_year(calendar.year) && temp1 == 1) /* 当年是不是闰年/2月份 */
{
if (temp >= 29)
{
temp -= 29; /* 闰年的秒钟数 */
}
else
{
break;
}
}
else
{
if (temp >= month_table[temp1])
{
temp -= month_table[temp1]; /* 平年 */
}
else
{
break;
}
}
temp1++;
}
calendar.month = temp1 + 1; /* 得到月份 */
calendar.date = temp + 1; /* 得到日期 */
}
temp = seccount % 86400; /* 得到秒钟数 */
calendar.hour = temp / 3600; /* 小时 */
calendar.min = (temp % 3600) / 60; /* 分钟 */
calendar.sec = (temp % 3600) % 60; /* 秒钟 */
calendar.week = rtc_get_week(calendar.year, calendar.month, calendar.date); /* 获取星期 */
}
/**
* @brief 将年月日时分秒转换成秒钟数
* @note 输入公历日期得到星期(起始时间为: 公元0年3月1日开始, 输入往后的任何日期, 都可以获取正确的星期)
* 使用 基姆拉尔森计算公式 计算, 原理说明见此贴:
* https://www.cnblogs.com/fengbohello/p/3264300.html
* @param syear : 年份
* @param smon : 月份
* @param sday : 日期
* @retval 0, 星期天; 1 ~ 6: 星期一 ~ 星期六
*/
uint8_t rtc_get_week(uint16_t year, uint8_t month, uint8_t day)
{
uint8_t week = 0;
if (month < 3)
{
month += 12;
--year;
}
week = (day + 1 + 2 * month + 3 * (month + 1) / 5 + year + (year >> 2) - year / 100 + year / 400) % 7;
return week;
}
/**
* @brief 将年月日时分秒转换成秒钟数
* @note 以1970年1月1日为基准, 1970年1月1日, 0时0分0秒, 表示第0秒钟
* 最大表示到2105年, 因为uint32_t最大表示136年的秒钟数(不包括闰年)!
* 本代码参考只linux mktime函数, 原理说明见此贴:
* http://www.openedv.com/thread-63389-1-1.html
* @param syear : 年份
* @param smon : 月份
* @param sday : 日期
* @param hour : 小时
* @param min : 分钟
* @param sec : 秒钟
* @retval 转换后的秒钟数
*/
static long rtc_date2sec(uint16_t syear, uint8_t smon, uint8_t sday, uint8_t hour, uint8_t min, uint8_t sec)
{
uint32_t Y, M, D, X, T;
signed char monx = smon;/* 将月份转换成带符号的值, 方便后面运算 */
if (0 >= (monx -= 2)) /* 1..12 -> 11,12,1..10 */
{
monx += 12; /* Puts Feb last since it has leap day */
syear -= 1;
}
Y = (syear - 1) * 365 + syear / 4 - syear / 100 + syear / 400; /* 公元元年1到现在的闰年数 */
M = 367 * monx / 12 - 30 + 59;
D = sday - 1;
X = Y + M + D - 719162; /* 减去公元元年到1970年的天数 */
T = ((X * 24 + hour) * 60 + min) * 60 + sec; /* 总秒钟数 */
return T;
}
总结
参考了正点原子的历程,和官方历程。