STM32内部时钟读取并通过串口输出
STM32内部时钟读取并通过串口输出
实验任务
了解STM32F103的RTC(实时时钟)原理,并读取STM32F103C8T6内部的时钟(年月日时分秒),日历(星期x),1秒周期,通过串口输出到PC上位机。
原理介绍
RTC(Real-Time Clock)是一种用于追踪和记录实际时间的时钟系统。在STM32中,RTC通常用于提供实时时钟和日期信息,例如年、月、日、时、分、秒等。RTC在许多应用中都是重要的,尤其是需要记录事件发生时间的系统,如数据日志、定时器等。
以下是STM32中RTC的一些关键特性和功能:
-
实时时钟功能: RTC提供了秒、分、时、日、月、年等实时时钟信息,能够精确追踪时间。
-
备份寄存器: RTC还提供了一些用于存储备份数据的寄存器。这些寄存器在系统掉电或软复位时仍然保持数据。
-
日历功能: RTC支持日期和时间的日历功能,包括闰年的计算等。
-
低功耗模式: RTC可以在系统进入低功耗模式时继续运行,保持实时时钟的计时。
总体而言,RTC在STM32中为实时时钟提供了一种可靠的方式,并且其与低功耗模式的兼容性使其在需要长时间运行并保持时间计数的场景中非常有用。
本次实验将采用读写备份寄存器(BKP)来实现实时时钟,下面是BKP的相关介绍:
-
作用:
- 提供用于存储数据的备份寄存器。
- 在掉电情况下保持存储的数据。
-
主要功能:
- 备份寄存器:
- 提供多个备份寄存器,这些寄存器可以在掉电时保持其值。
- 可以用于存储配置信息、计数器值、标志位等。
- 掉电数据保护:
- 当STM32微控制器进入低功耗模式或掉电模式时,BKP模块的备份寄存器可以保持其内容。
- 这允许在系统重新上电时恢复先前存储的数据。
- 掉电备份电池:
- 有些STM32系列的芯片提供掉电备份电池(Vbat),它可以提供电源给BKP模块,以保持备份寄存器中的数据。
- 这对于需要在掉电时保持关键信息的应用场景非常有用。
- 备份寄存器:
实验过程
RTC时钟代码编写
创建好Keil项目后,在System文件夹中创建两个文件:MyRTC.c
和MyRTC.h
,用来编写RTC内部时钟相关代码。
在MyRTC.c
中编写如下代码:
#include <time.h>
#include "stm32f10x.h"
// 预设的日期和时间信息:年、月、日、时、分、秒
uint16_t MyRTC_Time[] = {2023, 11, 20, 21, 12, 00};
// 设置RTC的初始时间
void MyRTC_SetTime(void);
// 初始化RTC模块
void MyRTC_Init(void) {
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
PWR_BackupAccessCmd(ENABLE);
// 如果BKP寄存器中的标志位不为0xA5A5,说明需要进行RTC的初始化
if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5) {
RCC_LSEConfig(RCC_LSE_ON);
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET);
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
RCC_RTCCLKCmd(ENABLE);
RTC_WaitForSynchro();
RTC_WaitForLastTask();
RTC_SetPrescaler(32768 - 1);
RTC_WaitForLastTask();
MyRTC_SetTime(); // 设置RTC的初始时间
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
} else {
RTC_WaitForSynchro();
RTC_WaitForLastTask();
}
}
// 设置RTC的初始时间
void MyRTC_SetTime(void) {
time_t time_cnt;
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];
// mktime函数将tm结构体转换为秒数,减去8小时是为了调整时差
time_cnt = mktime(&time_date) - 8 * 60 * 60;
// 设置RTC的计数器
RTC_SetCounter(time_cnt);
RTC_WaitForLastTask();
}
// 读取RTC的当前时间
void MyRTC_ReadTime(void) {
time_t time_cnt;
struct tm time_date;
// 获取RTC的计数器值
time_cnt = RTC_GetCounter() + 8 * 60 * 60;
// 将秒数转换为tm结构体
time_date = *localtime(&time_cnt);
// 更新MyRTC_Time数组的元素
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;
}
在给出的STM32的RTC时钟代码中,PWR和BKP模块主要用于配置和初始化RTC时钟,并在掉电时保持时间信息。代码的思路如下:
-
初始化PWR和BKP模块:
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE); PWR_BackupAccessCmd(ENABLE);
- 启用PWR和BKP的时钟,以便后续可以配置PWR和使用BKP。
-
检查备份寄存器是否包含特定标记:
if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5)
- 通过读取备份寄存器(BKP_DR1)来检查是否已经进行过RTC初始化。0xA5A5是一个标记,用于指示是否进行过初始化。
-
配置RTC时钟:
RCC_LSEConfig(RCC_LSE_ON); while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET); RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE); RTC_WaitForSynchro(); RTC_WaitForLastTask(); RTC_SetPrescaler(32768 - 1); RTC_WaitForLastTask();
- 启用低速外部晶体振荡器(LSE),等待振荡器准备好。
- 配置RTC时钟源为LSE。
- 启用RTC时钟。
- 设置RTC的预分频器,以便在RTC的时钟源上获得1秒的时间。
-
设置RTC初始时间:
MyRTC_SetTime(); BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
- 调用
MyRTC_SetTime
函数,将预定义的时间信息转换为秒,并设置RTC的计数器。 - 将标记写入备份寄存器,以指示RTC已经初始化。
- 调用
-
读取RTC时间:
MyRTC_ReadTime();
- 调用
MyRTC_ReadTime
函数,读取RTC计数器的值,并将其转换为年月日时分秒的格式,存储在数组MyRTC_Time
中。
- 调用
总体来说,PWR和BKP模块的主要作用是在初始化RTC时钟时提供电源管理和掉电时的数据备份功能。在掉电时,通过备份寄存器,可以在系统重新上电时保持RTC的初始时间。这在一些应用场景中很有用,例如需要在掉电时保持实时时钟的设备。
在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
该文件对变量和函数进行了相关的声明。
定时器代码编写
继续在System文件夹中添加两个文件:Timer.c
和Timer.h
,用于定时器的设置。
Timer.c
代码如下:
#include "stm32f10x.h" // Device header
void Timer_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM2, ENABLE);
TIM_InternalClockConfig(TIM2);
TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;
TIM_TimeBaseInitStructure.TIM_ClockDivision = TIM_CKD_DIV1;
TIM_TimeBaseInitStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInitStructure.TIM_Period = 10000 - 1;
TIM_TimeBaseInitStructure.TIM_Prescaler = 7200 - 1;
TIM_TimeBaseInitStructure.TIM_RepetitionCounter = 0;
TIM_TimeBaseInit(TIM2, &TIM_TimeBaseInitStructure);
TIM_ClearFlag(TIM2, TIM_FLAG_Update);
TIM_ITConfig(TIM2, TIM_IT_Update, ENABLE);
NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2);
NVIC_InitTypeDef NVIC_InitStructure;
NVIC_InitStructure.NVIC_IRQChannel = TIM2_IRQn;
NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;
NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority = 2;
NVIC_InitStructure.NVIC_IRQChannelSubPriority = 1;
NVIC_Init(&NVIC_InitStructure);
TIM_Cmd(TIM2, ENABLE);
}
/*
void TIM2_IRQHandler(void)
{
if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET)
{
TIM_ClearITPendingBit(TIM2, TIM_IT_Update);
}
}
*/
在Timer.h
中声明:
#ifndef __TIMER_H
#define __TIMER_H
void Timer_Init(void);
#endif
以上代码初始化了定时器TIM2
,并且每隔1秒触发一次定时器中断。(具体内容看这里)
串口代码编写
接下来,在Hardware文件夹中创建两个文件:Serial.c
和Serial.h
,用于串口通信,即STM32给上位机发送日期和时间信息。
Serial.c
代码如下:
#include "stm32f10x.h" // Device header
#include <stdio.h>
#include <stdarg.h>
void Serial_Init(void)
{
RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_9;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
USART_InitTypeDef USART_InitStructure;
USART_InitStructure.USART_BaudRate = 9600;
USART_InitStructure.USART_HardwareFlowControl = USART_HardwareFlowControl_None;
USART_InitStructure.USART_Mode = USART_Mode_Tx;
USART_InitStructure.USART_Parity = USART_Parity_No;
USART_InitStructure.USART_StopBits = USART_StopBits_1;
USART_InitStructure.USART_WordLength = USART_WordLength_8b;
USART_Init(USART1, &USART_InitStructure);
USART_Cmd(USART1, ENABLE);
}
void Serial_SendByte(uint8_t Byte)
{
USART_SendData(USART1, Byte);
while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
}
void Serial_SendArray(uint8_t *Array, uint16_t Length)
{
uint16_t i;
for (i = 0; i < Length; i ++)
{
Serial_SendByte(Array[i]);
}
}
void Serial_SendString(char *String)
{
uint8_t i;
for (i = 0; String[i] != '\0'; i ++)
{
Serial_SendByte(String[i]);
}
}
uint32_t Serial_Pow(uint32_t X, uint32_t Y)
{
uint32_t Result = 1;
while (Y --)
{
Result *= X;
}
return Result;
}
void Serial_SendNumber(uint32_t Number, uint8_t Length)
{
uint8_t i;
for (i = 0; i < Length; i ++)
{
Serial_SendByte(Number / Serial_Pow(10, Length - i - 1) % 10 + '0');
}
}
int fputc(int ch, FILE *f)
{
Serial_SendByte(ch);
return ch;
}
void Serial_Printf(char *format, ...)
{
char String[100];
va_list arg;
va_start(arg, format);
vsprintf(String, format, arg);
va_end(arg);
Serial_SendString(String);
}
Serial.h
中声明:
#ifndef __SERIAL_H
#define __SERIAL_H
#include <stdio.h>
void Serial_Init(void);
void Serial_SendByte(uint8_t Byte);
void Serial_SendArray(uint8_t *Array, uint16_t Length);
void Serial_SendString(char *String);
void Serial_SendNumber(uint32_t Number, uint8_t Length);
void Serial_Printf(char *format, ...);
#endif
以上代码,可以实现串口通信。(具体看这里)
主函数代码编写
-
根据日期计算星期
在main函数上方添加如下代码:char* weekday; void CalculateWeekDay(uint16_t year, uint16_t month, uint16_t day) { if (month == 1 || month ==2) { month +=12; year--; } uint16_t iWeek = (day + 2 * month + 3 * (month + 1)/5 + year + year/4 - year/100 + year/400) % 7; switch (iWeek) { case 0: weekday = "星期一"; break; case 1: weekday = "星期二"; break; case 2: weekday = "星期三"; break; case 3: weekday = "星期四"; break; case 4: weekday = "星期五"; break; case 5: weekday = "星期六"; break; case 6: weekday = "星期日"; break; } }
输入年月日,就可以得出这一天是星期几。
-
在main函数之后添加中断处理函数:
void TIM2_IRQHandler(void) { if (TIM_GetITStatus(TIM2, TIM_IT_Update) == SET) { Serial_Printf("%d-%d-%d %d:%d:%d ", MyRTC_Time[0], MyRTC_Time[1], MyRTC_Time[2], MyRTC_Time[3], MyRTC_Time[4], MyRTC_Time[5]); CalculateWeekDay(MyRTC_Time[0], MyRTC_Time[1], MyRTC_Time[2]); Serial_Printf("%s", weekday); Serial_Printf("\r\n"); TIM_ClearITPendingBit(TIM2, TIM_IT_Update); } }
中断处理函数负责把具体的时间信息(年、月、日、时、分、秒、星期)发送到上位机。
-
main内添加如下代码:
int main(void) { OLED_Init(); MyRTC_Init(); Timer_Init(); Serial_Init(); MyRTC_SetTime(); while (1) { MyRTC_ReadTime(); } }
设定好初始时间后,不断循环读取时间。
运行效果
将代码烧录后,打开串口调试助手,波特率调到9600
,观察结果:
总结体会
通过本次实验,我学会了如何读取STM32的RTC内部时钟,并且回顾了之前学过的串口和定时器的知识。
遇到的问题:代码运行时间长了以后,秒数明显会走得很慢,不知道是不是因为接了串口以及加了定时器中断处理导致的。