文章目录
RTC实时时钟与BKP
Unix时间戳
可以使用time.h,头文件中的函数将 时间戳转化为各种类型的时间。
UTC/GMT
UTC(协调世界时)
- 定义与计时基准
- 以 原子钟(铯 - 133 原子振荡周期) 为计时基础,1 秒定义为 “铯 - 133 原子基态的两个超精细能级之间跃迁所对应的辐射的 9192631770 个周期的持续时间”,精度极高(上千万年误差约 1 秒)。
- 引入 闰秒机制:当原子钟计时与地球自转(因地球自转变慢导致的计时偏差)相差超过 0.9 秒 时,通过在 UTC 时间中插入或删除 1 秒(如出现
23:59:60
或跳过23:59:59
),消除与地球自转的误差,确保时间与昼夜同步(如 2023 年未添加闰秒,最近一次闰秒为 2016 年 12 月 31 日)。
- 与 GMT 的关系
- GMT(格林尼治标准时间) 是基于地球自转的天文时间 时间长之后会出现误差(以格林尼治天文台为基准),而 UTC 是更严谨的原子时标准。
- 生活场景中可视为等同:两者在实际应用中(如手机、电脑的时区设置)差异可忽略,例如 东八区时间 既表示为
UTC+8
也表示为GMT+8
,均指比 UTC/GMT 早 8 小时的时间(如北京时间)。
时间戳转换
时间戳转换
BKP简介
- 含义与用途 :BKP 即 backup registers,是备份寄存器,可存储用户自定义数据。
- 电源特性 :当 VDD 主电源切断,由 VBAT 备用电池供电,系统唤醒、复位时数据不复位。VDD 供电电压 2.0 - 3.6 伏,VBAT 供电电压 1.8 - 3.6 伏。
- 引脚定义 :VDD 相关引脚为系统主电源,正常使用时接 3.3 伏电源;VBAT 为备用电池供电引脚,使用 BKP 和 RTC 需接备用电池,电池负极与主电源负极共地。若没有外部电池,建议 VBT 引脚接到 VDD 并连接 100 纳法滤波电容。
- 额外功能 :Temper 引脚侵入事件可清除备份寄存器内容,用于设备防拆(将该引脚与设备外壳相连当外壳被破坏时,会给引脚一个信号,该引脚将寄存器中数据清空);可输出 RTC 校准时钟、闹钟脉冲或秒脉冲,外部设备可测量校准时钟校准 RTC 误差;存储 RTC 时钟校准计数器,配合校准时钟输出功能校准 RTC。
- 存储容量 :中容量和小容量设备里为 20 个字节,大容量和互联型设备里为 84 个字节。
BKP基本结构
1. 电池供电模块(VBAT 输入)
- 电源特性:BKP 位于后备区域,VDD(2.0-3.6V)主电源掉电后,自动切换为 VBAT(1.8-3.6V)备用电池供电,确保数据不丢失(系统复位、唤醒时数据保持)。
- 电路设计:无外部电池时,VBAT 可接 VDD 并串联 100nF 滤波电容(图中未体现具体电路,但逻辑上支持 VBAT 与 VDD 的电源切换)。
2. 侵入检测模块(TAMPER 输入)
- 防拆功能:TAMPER 引脚检测到侵入事件(电平跳变)时,自动清除 BKP 数据寄存器内容(图中 “侵入检测” 模块直接对应此功能,用于设备安全防护,如防拆锁设计)。
3. 时钟输出模块(RTC 输出)
- 功能复用:BKP 可输出 RTC 的校准时钟、闹钟脉冲或秒脉冲(图中 “时钟输出” 模块与会议中 “BKP 额外功能” 的时钟输出特性一致,支持外部设备校准 RTC 误差)。
4. 内部寄存器组
- 数据寄存器(DRx)
- 容量差异
- 中 / 小容量芯片(如 STM32F103C8T6):10 个 16 位寄存器(DR1-DR10),共 20 字节(图中 DR1-DR10 明确标注)。
- 大容量 / 互联型芯片:42 个 16 位寄存器(DR1-DR42),共 84 字节(图中 “大容量和互联型” 扩展部分,与会议内容匹配)。
- 存储特性:寄存器数据在 VDD 掉电后由 VBAT 维持,属于掉电保持型存储器。
- 容量差异
- 控制 / 状态 / 校准寄存器
- 控制寄存器:配置侵入检测使能、时钟输出模式等(如 TAMPER 触发使能)。
- 状态寄存器:反映侵入事件状态(如 TAMPER 触发标志)。
- RTC 时钟校准寄存器:存储 RTC 校准计数器,配合时钟输出功能校准 RTC 计时误差(图中单独列出,对应会议中 “存储 RTC 时钟校准计数器” 的描述)。
RTC简介
后面将RTC框图时,会对下面的简介进行一一验证。
RTC时钟源
- 可以看到RTC时钟选择器 来 进行RTC时钟选择,有三种时钟源。
- HSE 外部高速时钟 为8Mhz,要产生1hz的频率,要先进行128分频之后才能进行,时钟传输。因为时钟频率太大了。
- LSE外部低速时钟, 可以说就是为了RTC,设计的时钟,始终频率为32.768KHz,是2的次方倍,后面进行时钟分频好操作。(一般会选择使用)
- LSI 40Khz,也可是为RTC提供时钟源,但是主要为看门狗外设提供时钟源。
RTC框图
1. 硬件架构与区域划分
- 后备区域(灰色模块)
- 包含预分频器(RTC_PRL/RTC_DIV)和计数计时单元(RTC_CNT/RTC_ALR),待机时由 VBAT 供电(持续计时),与 APB1 接口(待机断电)分离,确保主电源掉电后 RTC 独立运行(匹配会议中 “VDD 断电后 VBAT 供电继续走时” 的特性)。
- APB1 接口:用于软件配置(如预分频值、闹钟时间),待机时断电(降低功耗)。
2. 预分频器与时钟处理
- RTCCLK 分频:通过
RTC_PRL
(重装值)和RTC_DIV
(余数计数)将输入时钟(如 32.768kHz LSE)分频为1Hz(TR_CLK),驱动 32 位计数器(RTC_CNT)递增(“20 位可编程预分频器适配输入时钟”,实现秒级计时)。
3. 计数计时与中断逻辑
- 32 位计数器(RTC_CNT):存储 Unix 秒数,溢出周期约 2106 年(触发
RTC_Overflow
中断, “溢出中断” 的硬件源)。 - 闹钟比较(RTC_ALR):与 RTC_CNT 匹配时触发
RTC_Alarm
中断,可以唤醒待机设备(通过WKP_STDBY
逻辑, “闹钟唤醒设备退出待机模式”)。 - 中断使能(RTC_CR)
SECIE
(秒中断)、OWIE
(溢出中断)、ALRIE
(闹钟中断)分别控制三类中断,经 NVIC 处理( “中断输出使能和 NVIC 部分” 的硬件实现)。
4. 低功耗与待机特性
- 后备区域供电:预分频器和计数计时单元在待机时持续供电(VBAT),确保计时不中断;APB1 接口、RTC_CR、NVIC 待机时断电。
RTC基本结构
预分频器中余数寄存器是一个自减设置的,就是来一个时钟就会计数值减一,当当减到0,会产生一个时钟信号向后传递,PRl重装值,设置周期,记满多少次向后产生一个时钟,当余数寄存器减到0,会将重装寄存器中的数据重装到自己的寄存器中,继续计数。
硬件电路
1. 备用电池供电电路
- 简单连接:VBAT 直接接 3V 纽扣电池(如 CR2032),负极与主电源共地。
- 推荐连接:通过二极管(D1/D2)隔离主电源(VDD)与备用电池(VBAT),并联 0.1μF 电容(C3)防电流倒灌,确保 VDD 掉电后 VBAT 独立供电(图中推荐电路,“防止电源倒灌,提升可靠性”)。
2. 外部低速晶振(LSE)电路
- 32.768kHz 晶振:连接 OSC32_IN(PC14)和 OSC32_OUT(PC15),两端并联 10pF 电容(C1/C2)到地(“LSE 为 RTC 首选时钟源,精度高,主电源掉电后 VBAT 供电持续工作”)。
- 作用:提供 1Hz 秒脉冲(经 15 位自然分频,32768=2^15),驱动 RTC 32 位计数器,实现高精度计时(“20 位可编程预分频器适配输入时钟”,LSE 无需复杂分频计算)。
3. 引脚功能与复用
- VBAT 引脚(PC13)
- 复用为 TAMPER(侵入检测,触发时清除 BKP 数据)、RTC 时钟输出(秒脉冲 / 闹钟信号)或 GPIO(“BKP 额外功能” 的硬件载体,如防拆检测、时钟校准输出)。
- OSC32 引脚(PC14/PC15):专属 RTC 时钟输入,连接 LSE 晶振( LSE 是 RTC 的核心时钟源)。
4. 硬件设计规范
- 电源滤波:VBAT(0.1μF)、LSE 晶振(10pF)均需滤波电容,抑制电源噪声。
- 二极管隔离:推荐电路中的 D1/D2(如 1N4148)确保主备电源互不影响,VBAT 在 VDD 掉电后自动接管供电。
RTC操作注意事项
后面进行代码编写时会着重强调。
1. 使能 BKP 和 RTC 访问(时钟与权限)
- 硬件保护机制:
BKP 和 RTC 位于后备区域,默认受保护(防止误操作)。使能 PWR/BKP 时钟(PWREN
/BKPEN
)确保模块供电,DBP
(Disable Backup Protection)位解锁访问权限,避免非授权代码篡改掉电保持数据(如 BKP 存储的校准参数、RTC 计时状态),保障数据完整性(尤其在主电源掉电后 VBAT 供电的敏感场景)。
2. 读取 RTC 前等待 RSF(Registers Synchronized Flag) 同步标志
- 时钟域异步性:
RTC(RTCCLK,如 32.768kHz)与 APB1 总线(PCLK1,高频)异步。RSF=1 表示 RTC 寄存器(如 CNT 秒计数)已与 APB1 接口同步,确保读取时数据最新(避免因时钟不同步导致时间戳错误,如读取到未更新的秒数),保证计时精度。
3. 写入 RTC 前进入配置模式(CNF 位 Configuration Flag)
- 运行时保护:
RTC 正常运行时(CNF=0),计数 / 分频 / 闹钟寄存器为只读(防止运行中误写,如秒计数器递增时写入导致计时混乱)。进入配置模式(CNF=1)后,RTC 暂停计时逻辑(“冻结” 时钟),确保写入操作(如设置初始时间、闹钟)的原子性,避免中间状态错误(如预分频值半写入导致频率偏差)。
4. 写操作等待 RTOFF(RTC Register Transfer Ongoing Flag) 位(前次写结束)
- 寄存器更新时序:
RTC 寄存器属于 RTCCLK 域,写入后需时间同步到硬件(如预分频器重装)。RTOFF=1 表示前次写操作完成(值已生效),避免流水线写操作的竞态条件(如连续写 CNT 和 ALR 时,确保 CNT 先更新,ALR 基于新值比较),保证配置与计时逻辑的一致性,提升系统可靠性(尤其在低功耗场景下,待机唤醒后 RTC 状态恢复的稳定性)。
BKP 库函数介绍
函数名 | 功能 | 参数 | 返回值 | 说明 |
---|---|---|---|---|
BKP_DeInit |
恢复 BKP 模块寄存器至复位默认值 | 无 | 无 | 初始化前重置配置,确保干净的工作状态 |
BKP_TamperPinLevelConfig |
配置侵入检测引脚的有效触发电平 | uint16_t BKP_TamperPinLevel (如 BKP_TamperPinLevel_High /Low ) |
无 | 设定侵入检测(Tamper)的触发条件(高 / 低电平) |
BKP_TamperPinCmd |
使能 / 禁止侵入检测功能 | FunctionalState NewState (ENABLE /DISABLE ) |
无 | 控制是否启用侵入检测(用于系统安全监测) |
BKP_ITConfig |
使能 / 禁止 BKP 模块的中断 | FunctionalState NewState |
无 | 开启 / 关闭 BKP 相关中断(如侵入检测中断),配合中断处理函数使用 |
BKP_RTCOutputConfig |
配置 RTC_OUT 引脚的输出信号源 | uint16_t BKP_RTCOutputSource (如校准时钟、闹钟脉冲、秒脉冲) |
无 | 将 RTC 时钟信号输出到外部(如示波器、时钟模块),扩展 RTC 功能 |
BKP_SetRTCCalibrationValue |
设置 RTC 校准值 | uint8_t CalibrationValue (-128~+127,补偿晶振误差) |
无 | 调整 RTC 时钟精度,补偿晶振频率偏移 |
BKP_WriteBackupRegister |
写入备份寄存器(掉电保持) | uint16_t BKP_DR (寄存器编号,如BKP_DR1 ),uint16_t Data (16 位数据) |
无 | 存储关键数据(如系统配置、掉电前状态),主电源掉电后由备用电池保持 |
BKP_ReadBackupRegister |
读取备份寄存器数据 | uint16_t BKP_DR |
uint16_t (寄存器存储的值) |
恢复掉电前保存的数据(如系统重启后读取配置) |
BKP_GetFlagStatus |
获取 BKP 模块的标志位状态 | 无 | FlagStatus (SET /RESET ) |
轮询检查事件(如侵入检测是否触发) |
BKP_ClearFlag |
清除 BKP 模块的标志位 | 无 | 无 | 处理标志位后清除(避免重复响应,如侵入检测标志) |
BKP_GetITStatus |
获取 BKP 模块的中断状态 | 无 | ITStatus (SET /RESET ) |
检查是否有未处理的中断(用于中断服务函数) |
BKP_ClearITPendingBit |
清除 BKP 模块的中断挂起位 | 无 | 无 | 中断处理后清除挂起位,防止重复进入中断 |
实时时钟
RCC有关库函数
函数名 | 功能描述 | 调用时机 | 注意事项 |
---|---|---|---|
RCC_LSE_Configure |
配置并启动外部低速时钟(LSE)(如 32.768kHz 晶振)。 | 初始化 RTC 前,需手动开启 LSE 时钟 | LSE 默认关闭,需调用此函数启动;启动后需等待LSE_READY 标志位为 1。 |
RCC_LSI_Command |
配置并启动内部低速时钟(LSI)(约 40kHz,用于备份时钟)。 | 当 LSE 不起振时(如实验调试场景) | 可作为 LSE 的替代时钟源,用于应急或测试。 |
RTC_CLK_Configure |
选择 RTC 的时钟源(如 LSE/LSI),配置时钟源数据选择器。 | 启动 LSE/LSI 后,配置 RTC 时钟源时 | 需与RTC_CLK_Command 配合使用,先配置时钟源,再使能时钟。 |
RTC_CLK_Command |
使能 RTC 时钟(在配置完时钟源后调用)。 | 配置完RTC_CLK_Configure 后 |
必须调用此函数才能激活 RTC 时钟。 |
RCC_GetFlagStatus |
获取时钟标志位状态(如LSE_FLAG ),判断时钟是否稳定启动。 |
调用RCC_LSE_Configure 后 |
需循环等待LSE_READY 标志位为 1,确保 LSE 时钟稳定工 |
RTC有关库函数
函数名 | 功能描述 | 调用时机 | 注意事项 |
---|---|---|---|
模式切换函数 | |||
RTC_EnterConfigMode |
进入 RTC 配置模式(置CRL 寄存器的CNF 位为 1)。 |
需修改 RTC 寄存器(如预分频器、计数器)前 | 必须先进入配置模式,否则无法写入寄存器! |
RTC_ExitConfigMode |
退出 RTC 配置模式(清CRL 寄存器的CNF 位为 0)。 |
完成 RTC 寄存器配置后 | 配置完成后需立即退出,避免误操作。 |
数据操作函数 | |||
RTC_GetCounter |
读取 RTC 计数器(CNT )的值,即当前时间(单位:秒)。 |
需要获取实时时间时 | 直接返回计数器值,需自行转换为时分秒格式。 |
RTC_SetCounter |
设置 RTC 计数器初始值(如设置初始时间)。 | 初始化 RTC 时配置初始时间 | 需先进入配置模式(调用RTC_EnterConfigMode )。 |
RTC_SetPrescaler |
配置预分频器(PRL 寄存器),设置计数器时钟频率(通常为 1Hz)。 |
初始化 RTC 时配置分频系数 | 分频值计算公式:PRL = LSE频率/1Hz - 1 (如 LSE=32768Hz 时,PRL=32767 )。 |
RTC_SetAlarm |
设置 RTC 闹钟值(ALR 寄存器)。 |
需要配置闹钟功能时 | 需配合中断配置(RTC_ITConfig )使用。 |
RTC_GetDivider |
读取预分频器余数寄存器(DIV ),获取亚秒级精度时间(如毫秒)。 |
需要更高时间精度时(如调试) | CNT 计数间隔为 1 秒,DIV 寄存器用于记录余数,可计算更细粒度时间。 |
等待与同步函数 | |||
RTC_WaitForLastTask |
等待上次 RTC 写操作完成(循环检测RTF 标志位)。 |
每次写操作(如设置计数器、预分频器)后 | 防止寄存器操作未完成时进行下一次写操作,避免数据错误! |
RTC_WaitForSynchro |
等待 RTC 时钟同步完成(循环检测RSF 标志位)。 |
配置时钟源或初始化 RTC 时 | 确保 RTC 时钟与时钟源同步,避免计时误差。 |
中断与标志位函数 | |||
RTC_ITConfig |
配置 RTC 中断(如闹钟中断、溢出中断)。 | 需要启用中断功能时 | 需结合中断控制器(NVIC)配置。 |
[其他标志位函数] |
(如检查同步状态、中断标志等,会议未详细列出,需结合具体寄存器操作) | 状态监控或错误处理时 | 需参考官方库函数文档或寄存器定义。 |
实时时钟代码编写步骤
配置时钟源 -》选择时钟源信号 -》配置预分频器参数 -》配置计数器等
- 开启 LSE 时钟 :调用 RCC 里的 LSE config 函数,参数用 RCC LSE on 启动外部 LSE 晶振,通过 while 循环调用 RCC get flag status 函数等待 LSE ready 标志位(等待时钟开启完成)。
- 选择时钟源 :调用 RCC RTC c LK config 函数,选择 RCC RTCCLK source LSE,再调用 RCC RTC CRK command 函数使能时钟。
- 等待操作 :调用等待同步和等待上一次写入操作完成的函数,防止时钟不同步造成 bug。
- 配置预分频器 :调用 RTC set prescaler 函数,分频系数为 32768 - 1,写入后调用 RTC 等待函数确保操作完成,库函数自带进入和退出配置模式代码。
- 设置初始时间 :调用 RTC set counter 函数,使用 PPT 里 2023 年 1 月 1 日的秒数设定初始时间,写入后调用等待函数。
RTC相关代码
// MyRTC.c
#include "stm32f10x.h" // Device header
#include <time.h>
//创建一个数组,将要写入的时间存储到数组中,这里设置的为0时区时间
uint16_t Time[] = {2013, 1, 1, 15, 59, 55};
void MyRTC_SetTime(void);
//会出现复位 重新初始化的问题 只要BKP没有掉电就不用初始化
void RTC_Init(void)
{
//使能部分代码,开启时钟 PWR 和 BKP
RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE);
RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE);
//pwr 使能对bkp 和 rtc的数据访问
PWR_BackupAccessCmd(ENABLE);
if(BKP_ReadBackupRegister(BKP_DR1) != 0xAAaa)
{
//使用lse 时钟源 配置
RCC_LSEConfig(RCC_LSE_ON);
//开启时钟要等待开启完成 转化完成会置标志位
while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET);
//选择时钟源
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
//开启RTC时钟开关
RCC_RTCCLKCmd(ENABLE);
//下面对RTC进行操作
//等待时钟同步
RTC_WaitForSynchro();
//每一次进行写操作时,要进行判断上一个字节是否写入完成
RTC_WaitForLastTask();
//设置分频器数据
//分频后的频率 = 时钟源频率 / (预分频系数)
RTC_SetPrescaler(32768 - 1);
//等待数据写入完成
RTC_WaitForLastTask();
//设置计数值 这里是初始化所以就是初始值
MyRTC_SetTime();
//等待数据写入完成
RTC_WaitForLastTask();
BKP_WriteBackupRegister(BKP_DR1, 0xAAaa);
}
else
{
//使用lse 时钟源 配置
RCC_LSEConfig(RCC_LSE_ON);
//开启时钟要等待开启完成 转化完成会置标志位
while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) != SET);
//选择时钟源
RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE);
//开启RTC时钟开关
RCC_RTCCLKCmd(ENABLE);
//下面对RTC进行操作
//等待时钟同步
RTC_WaitForSynchro();
//每一次进行写操作时,要进行判断上一个字节是否写入完成
RTC_WaitForLastTask();
}
}
//实现写时间的函数
void MyRTC_SetTime(void)
{
time_t time_cnt;
struct tm time_data;
//将要设置的时间写入结构体中
time_data.tm_year = Time[0] - 1900;
time_data.tm_mon = Time[1] - 1;
time_data.tm_mday = Time[2];
time_data.tm_hour = Time[3];
time_data.tm_min = Time[4];
time_data.tm_sec = Time[5];
//将结构体中数据转化为时间戳
time_cnt = mktime(&time_data);
//将获取的秒计数进行 计数器设置
//设置计数值 这里是初始化所以就是初始值
RTC_SetCounter(time_cnt);
//等待数据写入完成
RTC_WaitForLastTask();
}
//实现读取时间函数
//读取秒计数,将秒计数转化为结构体将,结构体中数据按顺序存储到数组中
void MyRTC_GetData(void)
{
time_t time_cnt;
struct tm time_data;
//将得到的0时区的时间转化为东8区时间
time_cnt = RTC_GetCounter() + 8 * 60 * 60;
time_data = *localtime(&time_cnt);
Time[0] = time_data.tm_year + 1900;
Time[1] = time_data.tm_mon + 1;
Time[2] = time_data.tm_mday;
Time[3] = time_data.tm_hour;
Time[4] = time_data.tm_min;
Time[5] = time_data.tm_sec;
}
主函数实现代码
//main.c
#include "stm32f10x.h" // Device header
#include "Delay.h"
#include "OLED.h"
#include "MyRTC.h"
int main(void)
{
OLED_Init();
RTC_Init();
OLED_ShowString(1, 1, "Date:XXXX-XX-XX");
OLED_ShowString(2, 1, "Time:XX:XX:XX");
OLED_ShowString(3, 1, "CNT :");
OLED_ShowString(4, 1, "DIV :");
while(1)
{
MyRTC_GetData(); //RTC读取时间,最新的时间存储到MyRTC_Time数组中
OLED_ShowNum(1, 6, Time[0], 4); //显示MyRTC_Time数组中的时间值,年
OLED_ShowNum(1, 11, Time[1], 2); //月
OLED_ShowNum(1, 14, Time[2], 2); //日
OLED_ShowNum(2, 6, Time[3], 2); //时
OLED_ShowNum(2, 9, Time[4], 2); //分
OLED_ShowNum(2, 12, Time[5], 2); //秒
OLED_ShowNum(3, 6, RTC_GetCounter(), 10); //显示32位的秒计数器
OLED_ShowNum(4, 6, RTC_GetDivider(), 10); //显示余数寄存器
}
}