一、初识 RTC 时钟
在嵌入式系统领域,实时时钟(RTC)如同系统的 “时间管家”,其重要性不言而喻。在工业自动化场景中,RTC 能为设备运行数据添加精确时间戳,方便后续故障追溯与数据分析;智能家居设备借助 RTC 实现定时开关、预约任务等功能。而 STM32 最小系统作为探索 STM32 单片机功能的起点,由电源电路、晶振电路、复位电路等核心部分构成。电源电路为芯片提供稳定电压;晶振电路为芯片工作提供时钟信号;复位电路则能在系统异常时恢复到初始状态,它们共同为 RTC 功能的实现搭建起稳定平台。
STM32 芯片内部集成的 RTC 模块,具备独特的双电源供电机制。正常工作时由主电源供电,当主电源掉电,备用电源(如 CR1220 等型号的纽扣电池)无缝接管,确保时间计数不会中断。其工作原理基于高精度定时器,以 32.768kHz 的外部晶振为计时基准,该晶振产生的高频信号经 RTC 模块内部多级分频器处理,最终转换为 1Hz 的秒脉冲信号,以此实现对时间的精准计量。例如,从秒到分钟、小时、日期的进位计数,都是基于这个稳定的 1Hz 信号逐级累加计算得出。
Unix时间戳
UTC与GMT
- 定义
- UTC:它是一种以原子时秒长为基础,在时刻上尽量接近于世界时的一种时间计量系统。UTC 通过精确的原子钟来保持时间的准确性,并会根据地球自转的变化进行调整,以确保与太阳时保持同步。
- GMT:它是以位于英国伦敦郊区的皇家格林尼治天文台的本初子午线为标准,将太阳穿过本初子午线的那一刻定为中午 12 点,并以此来计算一天的时间。
- 应用
- UTC:由于其高精度和稳定性,被广泛应用于科学研究、航空航天、国际通信、天文观测等领域,是全球统一的时间标准,用于协调世界各地的时间差异。例如,在卫星导航系统中,精确的时间同步对于确定位置至关重要,UTC 就被用作统一的时间基准。
- GMT:在过去,GMT 曾是全球通用的时间标准,在航海、航空等领域有着重要的应用。虽然现在 UTC 已成为主要的时间标准,但 GMT 在一些特定的历史文献、传统的航海领域以及英国等一些国家的日常时间表示中仍有一定的使用。例如,英国的一些老教堂在记录礼拜时间等活动时,可能还会使用 GMT。
C语言使用time.h进行时间戳转换日期
函数名 | 用途 | 参数 | 返回值 |
---|---|---|---|
time_t time(time_t *timer) | 获取当前的日历时间(自 1970 年 1 月 1 日以来的秒数)。如果 timer 不为空指针,会将时间值存储在该指针指向的位置。 | timer :指向 time_t 类型变量的指针,用于存储时间值。如果为 NULL ,则不存储时间值。 | 当前的日历时间,以自 1970 年 1 月 1 日以来的秒数表示。如果发生错误,返回 (time_t)(-1) 。 |
char *ctime(const time_t *timer) | 将 time_t 类型的时间值转换为一个字符串,格式为 Www Mmm dd hh:mm:ss yyyy\n 。 | timer :指向 time_t 类型变量的指针,包含要转换的时间值。 | 指向包含格式化时间字符串的指针。该字符串是静态分配的,每次调用此函数时会被覆盖。 |
struct tm *localtime(const time_t *timer) | 将 time_t 类型的时间值转换为本地时间,以 struct tm 结构体形式返回。 | timer :指向 time_t 类型变量的指针,包含要转换的时间值。 | 指向 struct tm 结构体的指针,该结构体包含分解后的本地时间信息。该结构体是静态分配的,每次调用此函数时会被覆盖。 |
struct tm *gmtime(const time_t *timer) | 将 time_t 类型的时间值转换为协调世界时(GMT),以 struct tm 结构体形式返回。 | timer :指向 time_t 类型变量的指针,包含要转换的时间值。 | 指向 struct tm 结构体的指针,该结构体包含分解后的 UTC 时间信息。该结构体是静态分配的,每次调用此函数时会被覆盖。 |
time_t mktime(struct tm *tm) | 将 struct tm 结构体表示的本地时间转换为 time_t 类型的时间值。 | tm :指向 struct tm 结构体的指针,包含要转换的本地时间信息。 | 转换后的 time_t 类型的时间值。如果发生错误,返回 (time_t)(-1) 。 |
double difftime(time_t time1, time_t time0) | 计算两个 time_t 类型时间值之间的差值(以秒为单位)。 | time1 :较晚的时间值。time0 :较早的时间值。 | time1 和 time0 之间的差值,以秒为单位。 |
size_t strftime(char *s, size_t max, const char *format, const struct tm *tm) | 根据指定的格式字符串将 struct tm 结构体表示的时间转换为字符串。 | s :指向存储结果字符串的字符数组的指针。max :字符数组的最大长度。format :格式字符串,指定输出的时间格式。tm :指向 struct tm 结构体的指针,包含要转换的时间信息。 | 实际写入字符数组的字符数(不包括终止空字符)。如果结果字符串长度超过 max - 1 ,则返回 0。 |
BKP(备份寄存器)
二、RTC 时钟的初始化与配置
(一)硬件连接准备
在 STM32 最小系统上搭建 RTC 硬件电路时,需格外注意细节。32.768kHz 的外部晶振通常有两个引脚,分别连接到芯片对应的 OSC32_IN 和 OSC32_OUT 引脚,晶振的负载电容一般选择 6pF - 12pF,具体参数需参考芯片数据手册。连接时,晶振引脚走线应尽量短且远离干扰源,防止信号受到电磁干扰导致频率偏移。
备用电源连接方面,以 STM32F103 系列为例,需将纽扣电池连接到 VBAT 引脚。部分最小系统板可能集成了电池座,直接安装电池即可;若没有,需自行焊接电池座或导线。连接时要注意电池的正负极性,错误连接可能损坏芯片或导致电池电量快速耗尽。此外,为防止主电源和备用电源同时供电时产生冲突,电路中通常会加入肖特基二极管进行隔离。
(二)时钟配置
- 开启时钟使能
在 STM32 的系统架构中,RTC 模块的运行依赖于特定时钟的开启。RCC(复位和时钟控制)寄存器作为系统时钟的 “总开关”,我们需对其进行精准配置。RCC_LSEConfig(RCC_LSE_ON);函数的作用是启动低速外部时钟(LSE),它会向 LSE 控制寄存器写入特定指令,使能 LSE 振荡器。执行该函数后,需要通过循环while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);来等待 LSE 稳定,因为振荡器从启动到输出稳定的时钟信号需要一定时间。只有当 LSE 稳定标志位(RCC_FLAG_LSERDY)置 1 时,才能确保后续 RTC 工作的准确性。
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);用于开启电源接口(PWR)和备份域(BKP)时钟。PWR 时钟控制着电源管理相关功能,BKP 时钟则用于访问备份寄存器,这些寄存器在系统复位或掉电时能保存关键数据,开启这两个时钟是后续配置 RTC 的必要前提。
- 设置 RTC 预分频器
RTC 预分频器如同一个 “频率转换器”,其核心作用是将输入的高频时钟信号转换为适合时间计数的低频信号。RTC_SetPrescaler()函数内部会操作 RTC 预分频器寄存器(RTCCR),将传入的参数值写入相应位域。当输入时钟为 32.768kHz,将预分频器设置为 32767 时,根据分频公式
(其中fin为输入频率,N为预分频值),可得输出频率为 1Hz。这样,RTC 计数器每计数一次就代表时间过去了 1 秒,为后续分钟、小时等时间单位的计数提供了基础。
(三)时间与日期的设置和读取
- 时间设置
RTC_SetCounter()函数用于设置 RTC 的计数值,进而确定具体时间。在使用该函数前,需要将年、月、日、时、分、秒等时间信息按照特定格式转换为 RTC 可识别的计数值。以 STM32 的 RTC 时间格式为例,它采用 BCD 码(二进制编码的十进制数)存储时间数据。例如,将小时数 “10” 转换为 BCD 码,需拆分为 “0001 0000”,分别对应十位的 “1” 和个位的 “0”。在设置时间时,还需注意 RTC 的 12 小时制和 24 小时制模式选择,如示例代码中RTC_TimeStructure.RTC_H12 = RTC_H12_AM;指定为 12 小时制上午模式。
- 时间读取
RTC_GetCounter()函数从 RTC 计数器寄存器中获取当前计数值,该计数值是一个基于预分频后 1Hz 时钟累加的数值。获取计数值后,需根据预分频设置和时间格式规则进行反向转换。例如,先将计数值除以 60 得到分钟数和剩余秒数,再将分钟数除以 60 得到小时数和剩余分钟数,依次类推,最终将计数值还原为年、月、日、时、分、秒的实际时间信息
代码
#include "stm32f10x.h"
#include <time.h>
// 配置RTC
void RTC_Configuration(void) {
// 使能PWR和BKP外设的时钟,因为PWR用于管理备份域电源,BKP用于备份寄存器操作
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR | RCC_APB1Periph_BKP, ENABLE);
// 允许访问备份寄存器,默认情况下备份寄存器受保护,需要开启此权限才能操作
PWR_BackupAccessCmd(ENABLE);
// 检查备份寄存器BKP_DR1的值,如果不等于0xA5A5,说明RTC未初始化
if (BKP_ReadBackupRegister(BKP_DR1) != 0xA5A5) {
// 对备份寄存器进行复位操作,恢复到默认状态
BKP_DeInit();
// 开启外部低速时钟(LSE),通常RTC使用LSE作为时钟源以保证计时精度
RCC_LSEConfig(RCC_LSE_ON);
// 等待LSE时钟稳定,只有时钟稳定后才能作为可靠的时钟源
while (RCC_GetFlagStatus(RCC_FLAG_LSERDY) == RESET);
// 选择LSE作为RTC的时钟源
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
// 使能RTC时钟,让RTC开始工作
RCC_RTCCLKCmd(ENABLE);
// 等待RTC寄存器同步,因为RTC时钟和APB1时钟不同步,需要进行同步操作
RTC_WaitForSynchro();
// 等待上一次RTC操作完成,确保操作的完整性
RTC_WaitForLastTask();
// 设置RTC预分频器,32767用于将32.768kHz的LSE时钟分频为1Hz,使RTC以秒为单位计时
RTC_SetPrescaler(32767);
// 等待上一次RTC操作完成
RTC_WaitForLastTask();
// 在备份寄存器BKP_DR1中写入0xA5A5,标记RTC已经初始化
BKP_WriteBackupRegister(BKP_DR1, 0xA5A5);
}
}
// 设置RTC时间
void RTC_SetTime(struct tm *timeinfo) {
// 将struct tm结构体表示的时间转换为time_t类型的时间戳
time_t rawtime = mktime(timeinfo);
// 将time_t类型的时间戳转换为uint32_t类型,以便设置到RTC计数器中
uint32_t counterValue = (uint32_t)rawtime;
// 设置RTC计数器的值,从而设置RTC的时间
RTC_SetCounter(counterValue);
// 等待上一次RTC操作完成
RTC_WaitForLastTask();
}
// 读取RTC时间
void RTC_GetTime(struct tm *timeinfo) {
// 读取RTC计数器的值
uint32_t counterValue = RTC_GetCounter();
// 将RTC计数器的值转换为time_t类型的时间戳
time_t rawtime = (time_t)counterValue;
// 将time_t类型的时间戳转换为struct tm结构体,方便后续处理和显示
localtime_r(&rawtime, timeinfo);
}
int main(void) {
// 调用RTC配置函数,对RTC进行初始化
RTC_Configuration();
// 定义一个struct tm结构体变量,用于存储要设置的时间信息
struct tm timeinfo = {0};
// 设置年份,注意struct tm中的年份是从1900开始计算的,所以2025年要减去1900
timeinfo.tm_year = 2025 - 1900;
// 设置月份,struct tm中的月份从0开始计数,所以5月要减去1
timeinfo.tm_mon = 5 - 1;
// 设置日期
timeinfo.tm_mday = 7;
// 设置小时
timeinfo.tm_hour = 12;
// 设置分钟
timeinfo.tm_min = 30;
// 设置秒
timeinfo.tm_sec = 0;
// 调用RTC_SetTime函数,将设置好的时间信息写入RTC
RTC_SetTime(&timeinfo);
// 定义一个struct tm结构体变量,用于存储从RTC读取的当前时间信息
struct tm currentTime;
// 调用RTC_GetTime函数,从RTC读取当前时间信息
RTC_GetTime(¤tTime);
while (1) {
// 主循环可以添加其他功能,例如周期性读取时间、根据时间执行特定任务等
}
}