百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

STM32嵌入式-内部RTC时钟实验

bigegpt 2024-09-04 02:49 3 浏览

学习目标:

1.了解 STM32 才内部时钟结构

2.了解时钟的计算方式。

23.1 STM32 内部 RTC 时钟简介

要讲到 STM32 的内部 RTC 时钟,我们首先要了解一些 STM32 的备份寄存器,备份寄存器是 42 个 16 位的寄存器,可用来存储 84 个字节的用户应用程序数据。他们处在备份域里,当 VDD 电源被切断,他们仍然由 VBAT 维持供电。当系统在待机模式下被唤醒,或系统复位或电源复位时,他们也不会被复位。而STM32 的内部 RTC 时钟就在备份寄存器中。所以我们得到一个结论,就是要操作 RTC 时钟就要操作备份寄存器。接下来我们来看一下 RTC 的结构框图:

从框图中我们可以看出,其实 RTC 时钟里面存储时钟信号的只是一个 32 位

的寄存器,如果按秒来计算的话可以存储可以记录 4294967296 秒,约合 136 年

左右,作为一般应用,这已经是足够了的。但是从这里看出我们要具体知道现在

的时间是哪年哪月哪日,还有时分秒,那么就要自己进行处理了,将读取出来的

计数值,转换为我们熟悉的年月日时分秒。接下来我们来看一下怎么操作 RTC

时钟。

23.2 RTC 时钟的操作对 RTC 时钟的操作一般就是设置初始化 RTC 时钟,之后只是读取时钟了。那如何初始化呢?

1.RTC 的初始化

1) 打开相应的时钟

从上面我们知道,如果我们操作 RTC,那么我们就要操作备份寄存器,所以呢我们要打开一个是备份区域时钟。而一般操作 RTC 的话还会用到一个时钟,就是电源时钟,在电源控制里面有操作 RTC 的一些设置,所以我们还有将电源控制的时钟打开。所以代码为:

/* 使能 PWR 电源时钟和 BKP 备份区域外设时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR|RCC_APB1Periph_BKP, ENABLE);

2) 使能备份寄存器操作

可以使用 PWR_BackupAccessCmd()函数(从开头的 PWR我们就知道这个设置是在电源控制部分设置的,所以我们要打开电源控制的时钟)。它只有一个参数,也就是设置的状态,我们要使能所以设为:ENABLE。

3) 复位备份寄存器

当然这个操作不要每次都执行,因为备份区域的复位将导致之前存在的数据丢失,所以要不要复位,要看情况而定。我们可以使用BKP_DeInit()函数。

4) 设置外部低速时钟

我们要使用外部的低速时钟来控制 RTC,代码为:RCC_LSEConfig(RCC_LSE_ON);//设置外部低速晶振(LSE),

使用外设低速晶振。在开启外部低速时钟的时候,我们还有确定它是否成功起振,之后才能够接着操作,所以我们还要检测,外部低速时钟是否开启。代码为:

/* 检查指定的 RCC 标志位设置与否,等待低速晶振(LSE)就绪 */
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET)();

等待它起振好了之后将它作为 RTC 的时钟,代码为:

RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //设置 RTC 时钟(RTCCLK),选择 LSE 作为 RTC 时钟

5) 使能 RTC 时钟

打开 RTC 时钟,代码为:

RCC_RTCCLKCmd(ENABLE); //使能 RTC 时钟在对 RTC 操作的时候,注意连续操作的时候还要检测它是否执行完成,才能够接着对起进行操作,所以操作完之后再检测是否操作完成。

代码为:

RTC_WaitForLastTask(); //等待最近一次对 RTC 寄存器的写操作完成6) 等待 RTC 时钟寄器同步代码为:
RTC_WaitForSynchro();//等待 RTC 寄存器同步

7) 开启秒中断

我们要读取时钟秒更新一次,所以我们开启秒中断。代码为:

RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能 RTC 秒中断然后等待操作完成。
RTC_WaitForLastTask(); //等待最近一次对 RTC 寄存器的写操作完成

8) 然后设置 RTC 时钟的预分频

首先要进入配置模式。代码为:

RTC_EnterConfigMode(); //允许配置然后开始设置分频,我们要进行 32767 分频。所以代码为:
RTC_SetPrescaler(32767); //设置 RTC 预分频的值然后等待操作完成:
RTC_WaitForLastTask(); //等待最近一次对 RTC 寄存器的写操作完成

9) 设置初始化时间

也就是要初始化的时钟存入到 32 位寄存器,这里原理就是,首先你要找一个最低时间点,比如说,当存储时钟的 32 位的寄存器都是零的时候,那么就表示 2000 年 1 月 1 日,0 时 0 分 0 秒(我们可以叫它基础时间)。那这个时候,每当这个 32 位寄存器加 1,那么比 2000 年 1 月 1 日,0 时 0 分 0秒,多了一秒。就是 2000 年 1 月 1 日,0 时 0 分 1 秒。可能有人不理解,认为那直接讲这个寄存器设为 0 不就好了?不过后面我们读取时间的,将计数器的值,转化为年月日的时候,要一个参考时间。从寄存器中的值,可以有看出当前时间在基础时间上面多了多少秒,然后根据这个数值,计算出当前的年月日时分秒。这个过程是蛮麻烦的,不过也没办法,具体算法可以看后面的程序例程。在这里我们讲一些怎么初始化这个 32 位寄存器。

/*****************************************************************
* Function Name : RTC_SetClock
* Description : 设置时钟
* Input : *time:要设置的时钟值
* Output : None
* Return : None
*****************************************************************/
void RTC_SetClock(RTC_TimeTypeDef *time)
{
RTC_EnterConfigMode(); //允许配置RTC_WaitForLastTask(); //等待最近一次对 RTC 寄
存器的写操作完成
 RTC_SetTime(time); //设置时间
RTC_ExitConfigMode(); //退出配置模式
 RTC_GetTime(); //更新时间
}

这个函数中调用的一个设置时钟的函数 RTC_SetTime()这个函数是用来将我们要设置的年月日转换为计数器计数,然后写入计数器中的函数,代码如下:

/*****************************************************************
* Function Name : RTC_SetTime
* Description : 设置 RTC 时钟的计数器初始值
* Input : time:设置的初始值(注:年份设置从 2000 到 2100 年之
间)
* Output : None
* Return : 0:设置成功;0xFF:设置失败
****************************************************************/
static uint8_t RTC_SetTime(RTC_TimeTypeDef *time)
{
uint8_t leapYear = 0;
uint16_t i;
uint32_t secondCount = 0;
 /* 确定写入的时间不超过年限 */
if((time->year < 2000) || (time->year > 2100)) //从 2000 年到 2100 年,一共100 年
{
 return 0xFF; //超过时限返回失败
 }
/* 将所有的年份秒数相加 */
for(i = RTC_BASE_YEAR; i<time->year; i++)
 {
if(RTC_CheckLeapYear(i) == 0) //如果年份是闰年
{
 secondCount += RTC_LEEP_YEAR_SECOND;
}
 else
{
secondCount += RTC_COMMON_YEAR_SECOND;
 }
 }/* 检测写入年份是闰年还是平年 */
 if(RTC_CheckLeapYear(time->year) == 0) //如果是闰年
{
 leapYear = 1; //标记为闰年
}
else
 {
leapYear = 0; //标记为平年
 }
/* 所有月份秒数相加 */
for(i=1; i<time->month; i++)
 {
if(leapYear == 1)
 {
secondCount += RtcLeapMonth[i - 1] * RTC_DAY_SECOND;
}
else
 {
 secondCount += RtcCommonMonth[i- 1] *
RTC_DAY_SECOND;
}
}
 /* 所有的日期秒数相加 */
for(i=1; i<time->day; i++)
{
secondCount += RTC_DAY_SECOND;
 }
/* 小时的秒数 */
secondCount += RTC_HOUR_SECOND * time->hour;
 /* 分钟的秒数 */
secondCount += 60 * time->minit;
 /* 加上秒数 */
secondCount += time->second;
/* 使能 PWR 电源时钟和 BKP 备份区域外设时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR
|
RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE); //使能 RTC 和后备寄存器访问
RTC_SetCounter(secondCount); //设置 RTC 计数器的值RTC_WaitForLastTask(); //等待最近一次对 RTC 寄存器的写操作完成
return 0; //设置成功返回 0
}

10) 退出配置模式

上面我们一开始设置的时候,我们进入了配置模式,设置完了之后我们要退出模式。代码为:

RTC_ExitConfigMode(); //退出配置模式

11) 初始化 RTC 的中断 NVIC

上面我们要使用 RTC 是时钟的秒中断,所以我们要初始化它的 NVIC,配置 NVIC 的方式我们在前面讲过了,这里就不再详细讲了。

2. 初始化代码

/*************************************************************
* Function Name : RTC_Config
* Description : 初始化时钟,并初始化内部的时钟信息
* Input : time:要初始化的时钟
* Output : None
* Return : 0:初始化成功;0xFF:初始化失败
*************************************************************/
int8_t RTC_Config(RTC_TimeTypeDef *time)
{
uint32_t timeCount;
 if(BKP_ReadBackupRegister(BKP_DR1) != 0x5050)
 {
/* 使能 PWR 电源时钟和 BKP 备份区域外设时钟 */
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR
|
RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);//使能后备寄存器访问
BKP_DeInit(); //复位备份区域
RCC_LSEConfig(RCC_LSE_ON);//设置外部低速晶振(LSE),使用外设低速晶振
/* 检查指定的 RCC 标志位设置与否,等待低速晶振(LSE)就绪 */
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET)
{
timeCount++;
if(timeCount > 0x00FFFFF){
break;
 }
}
/* 外部晶振错误,返回设置失败 */
if(timeCount > 0x00FFFFF)
 {
return 0xFF;
 }
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); //设置 RTC 时钟(RTCCLK),选择 LSE 作为 RTC 时钟
RCC_RTCCLKCmd(ENABLE); //使能 RTC 时钟
RTC_WaitForLastTask();//等待最近一次对RTC 寄存器的写操作完成
RTC_WaitForSynchro();//等待 RTC 寄存器同步
RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能 RTC 秒中断
RTC_WaitForLastTask(); //等待最近一次对 RTC 寄存器的写操作完成
RTC_EnterConfigMode(); //允许配置
RTC_SetPrescaler(32767); //设置 RTC 预分频的值
RTC_WaitForLastTask(); //等待最近一次对 RTC寄存器的写操作完成
RTC_SetTime(time); //设置时间
RTC_ExitConfigMode(); //退出配置模式
BKP_WriteBackupRegister(BKP_DR1, 0X5050); //向指定的后备寄存器中写入用户程序数据
}
else
 {
RTC_WaitForSynchro(); //等待最近一次对 RTC 寄存器的写操作完成
RTC_ITConfig(RTC_IT_SEC, ENABLE); //使能 RTC 秒中断
RTC_WaitForLastTask(); //等待最近一次对 RTC寄存器的写操作完成
}
 RTC_NVIC_Config(); //RCT 中断分组设置,开启中断
RTC_GetTime(); //更新时间return 0;
}

3. 获取时间

我们知道,STM32 的时钟其实也就是一个 24 位长度的计数器而已,而获取时间的方式就是使用 STM32 时钟的秒中断,进入中断,然后读取计算器的计数值,然后计数值转换为时间,我们例程的获取时间函数如下:

/*****************************************************************
* Function Name : RTC_GetTime
* Description : 读取 RTC 计数器的值,并将其转化为日期
* Input : None
* Output : None
* Return : None
****************************************************************/
static void RTC_GetTime(void)
{
 uint8_t leapYear = 0, i = 0;
 uint32_t secondCount = 0;
uint32_t day;
/* 读取时钟计数器的值 */
 secondCount = RTC->CNTH;
 secondCount <<= 16;
secondCount |= RTC->CNTL;
day = secondCount / RTC_DAY_SECOND; //求出天数
 secondCount = secondCount % RTC_DAY_SECOND; //求出剩余秒数
RTC_Time.year = RTC_BASE_YEAR;/* 求出星期几 */
 RTC_Time.week = (day + 6) % 7; //因为 2000 年 1 月 1 日是星期六所以加 6
 /* 求出年份 */
while(day >= 365)
 {
if(RTC_CheckLeapYear(RTC_Time.year) == 0) //是闰年
{
 day -= 366; //闰年有 366 天
 }
else{
day -= 365; //平年有 365 天
 }
 RTC_Time.year++;
}
 /* 求出月份 */
if(RTC_CheckLeapYear(RTC_Time.year) == 0)
 {
leapYear = 1; //如果是闰年标记
}
i = 0;
 RTC_Time.month = 1;
while(day >= 28)
{
if(leapYear == 1)
 {
 if(day < RtcCommonMonth[i]) //天数不够一个月
{
break;
}
 day -= RtcLeapMonth[i]; //减去闰年该月的天数
 }
else
{
if(day < RtcCommonMonth[i]) //天数不够一个月
 {
 break;
}
day -= RtcCommonMonth[i]; //减去平年该月的天数
}
 RTC_Time.month++; //月份加 1
i++; //月份数组加 1
}
/* 求出天数 */
 RTC_Time.day = day + 1; //月份剩下的天数就是日期(日期从 1 号开始)
 RTC_Time.hour = secondCount / RTC_HOUR_SECOND; //求出小时
RTC_Time.minit = secondCount % RTC_HOUR_SECOND / 60; //求出分钟
RTC_Time.second = secondCount % RTC_HOUR_SECOND %60; //求出秒
}

23.3 例程主函数

int main(void)
{
 uint8_t ledState, setState, m;
uint8_t keyValue;
uint16_t i;
/* 初始化时钟值 */
 time.year = 2013;
time.month = 12;
time.day = 24;
time.week = 2;
 time.hour = 12;
 time.minit = 0;
time.second = 0;
/* 初始化 */
 TFT_Init();
 FLASH_Init();
RTC_Config(&time);
LED_Config();
KEY_Config();
/* 彩屏显示初始化 */
TFT_ClearScreen(BLACK);
GUI_Show16Chinese(80, 0, "普中科技", RED, BLACK);
 GUI_Show12ASCII(90, 21, "PRECHIN", RED, BLACK);
GUI_Show12ASCII(60, 42, "www.prechin.com", RED, BLACK);
GUI_Show12Chinese(60, 63, "内部时钟实验", RED, BLACK);
 GUI_Show12Chinese(32, 84, "年 月 日", RED, BLACK);
GUI_Show12ASCII(128, 84, ": :", RED, BLACK);
 GUI_Show12Chinese(0, 105, "右键:进入或者退出设置模式", BLUE,
BLACK);
GUI_Show12Chinese(0, 126, "左键:设置位置左移", BLUE, BLACK);
 GUI_Show12Chinese(0, 147, "上键:设置位置数字加一", BLUE, BLACK);
 GUI_Show12Chinese(0, 168, "下键:设置位置数字减一", BLUE, BLACK);setState = 0; //初始设置为普通模式,非设置模式
 m = 0; //显示无高亮位置
while(1)
{
/*LED 灯闪烁 */
i++;
 if(i > 0xFF)
{
 i = 0;
if(ledState == 0xFE)
{
 ledState = 0xFF;
}
 else
{
ledState = 0xFE;
}
 LED_SetState(ledState);
 }
/* 键盘扫描 */
keyValue = KEY_Scan(); /* 如果按键是右键,进入或者退出设置模式 */
if(keyValue == KEY_RIGHT)
{
if(setState == 0)
 {
 setState = 1;
}
else
{
 setState = 0;
}
if(setState) //退出设置模式则更新时间
 {
m = 1;
 }
else
{
 RTC_SetClock(&time);
 m = 0;
}}
 /* 进入设置模式 */
if(setState == 1)
 {
switch(keyValue)
{
 case(KEY_UP): //上键高亮数字加 1
TIME_Set(m, 1);
 break;
case(KEY_DOWN): //下键高亮数字减 1
TIME_Set(m, 0);
 break;
case(KEY_LEFT): //左键高亮位置左移 1 位
 if(m == 6)
{
m = 1;
}
 else
 {
m++;
}
break;
 default:
 break;
}
}
/* 普通模式显示时钟 */
 else
 {
/* 读取时钟 */
time = RTC_Time; //读取时钟
}
 GUI_DisplayTime(m); //显示时钟
}
}

相关推荐

当Frida来“敲”门(frida是什么)

0x1渗透测试瓶颈目前,碰到越来越多的大客户都会将核心资产业务集中在统一的APP上,或者对自己比较重要的APP,如自己的主业务,办公APP进行加壳,流量加密,投入了很多精力在移动端的防护上。而现在挖...

服务端性能测试实战3-性能测试脚本开发

前言在前面的两篇文章中,我们分别介绍了性能测试的理论知识以及性能测试计划制定,本篇文章将重点介绍性能测试脚本开发。脚本开发将分为两个阶段:阶段一:了解各个接口的入参、出参,使用Python代码模拟前端...

Springboot整合Apache Ftpserver拓展功能及业务讲解(三)

今日分享每天分享技术实战干货,技术在于积累和收藏,希望可以帮助到您,同时也希望获得您的支持和关注。架构开源地址:https://gitee.com/msxyspringboot整合Ftpserver参...

Linux和Windows下:Python Crypto模块安装方式区别

一、Linux环境下:fromCrypto.SignatureimportPKCS1_v1_5如果导包报错:ImportError:Nomodulenamed'Crypt...

Python 3 加密简介(python des加密解密)

Python3的标准库中是没多少用来解决加密的,不过却有用于处理哈希的库。在这里我们会对其进行一个简单的介绍,但重点会放在两个第三方的软件包:PyCrypto和cryptography上,我...

怎样从零开始编译一个魔兽世界开源服务端Windows

第二章:编译和安装我是艾西,上期我们讲述到编译一个魔兽世界开源服务端环境准备,那么今天跟大家聊聊怎么编译和安装我们直接进入正题(上一章没有看到的小伙伴可以点我主页查看)编译服务端:在D盘新建一个文件夹...

附1-Conda部署安装及基本使用(conda安装教程)

Windows环境安装安装介质下载下载地址:https://www.anaconda.com/products/individual安装Anaconda安装时,选择自定义安装,选择自定义安装路径:配置...

如何配置全世界最小的 MySQL 服务器

配置全世界最小的MySQL服务器——如何在一块IntelEdison为控制板上安装一个MySQL服务器。介绍在我最近的一篇博文中,物联网,消息以及MySQL,我展示了如果Partic...

如何使用Github Action来自动化编译PolarDB-PG数据库

随着PolarDB在国产数据库领域荣膺桂冠并持续获得广泛认可,越来越多的学生和技术爱好者开始关注并涉足这款由阿里巴巴集团倾力打造且性能卓越的关系型云原生数据库。有很多同学想要上手尝试,却卡在了编译数据...

面向NDK开发者的Android 7.0变更(ndk android.mk)

订阅Google官方微信公众号:谷歌开发者。与谷歌一起创造未来!受Android平台其他改进的影响,为了方便加载本机代码,AndroidM和N中的动态链接器对编写整洁且跨平台兼容的本机...

信创改造--人大金仓(Kingbase)数据库安装、备份恢复的问题纪要

问题一:在安装KingbaseES时,安装用户对于安装路径需有“读”、“写”、“执行”的权限。在Linux系统中,需要以非root用户执行安装程序,且该用户要有标准的home目录,您可...

OpenSSH 安全漏洞,修补操作一手掌握

1.漏洞概述近日,国家信息安全漏洞库(CNNVD)收到关于OpenSSH安全漏洞(CNNVD-202407-017、CVE-2024-6387)情况的报送。攻击者可以利用该漏洞在无需认证的情况下,通...

Linux:lsof命令详解(linux lsof命令详解)

介绍欢迎来到这篇博客。在这篇博客中,我们将学习Unix/Linux系统上的lsof命令行工具。命令行工具是您使用CLI(命令行界面)而不是GUI(图形用户界面)运行的程序或工具。lsoflsof代表&...

幻隐说固态第一期:固态硬盘接口类别

前排声明所有信息来源于网络收集,如有错误请评论区指出更正。废话不多说,目前固态硬盘接口按速度由慢到快分有这几类:SATA、mSATA、SATAExpress、PCI-E、m.2、u.2。下面我们来...

新品轰炸 影驰SSD多款产品登Computex

分享泡泡网SSD固态硬盘频道6月6日台北电脑展作为全球第二、亚洲最大的3C/IT产业链专业展,吸引了众多IT厂商和全球各地媒体的热烈关注,全球存储新势力—影驰,也积极参与其中,为广大玩家朋友带来了...