Unix时间戳 核心概念定义Unix 时间戳是从协调世界时UTC/GMT的1970年1月1日0时0分0秒开始所经过的秒数。需要注意的是它不考虑闰秒。存储方式时间戳存储在一个秒计数器中该计数器是一个32位或64位的整型变量。时区处理世界上所有时区的秒计数器是完全相同的。不同时区通过在这个相同的计数上添加时区偏移量来获得当地的时间。 图表解读时间轴换算图片下方通过一条时间轴展示了相同的“秒计数器”数值在不同时区对应的具体时间秒计数器010000000001672588795日期时间 (伦敦 - UTC/零时区)1970-1-1 0:0:02001-9-9 1:46:402023-1-1 15:59:55日期时间 (北京 - UTC8)1970-1-1 8:0:02001-9-9 9:46:402023-1-1 23:59:55可以看出北京的时间正好比伦敦时间快了 8 个小时东八区偏移量这是验证了第3条知识点。UTC/GMT以下是对这张幻灯片内容的详细解读 GMT (Greenwich Mean Time) - 格林尼治标准时间基础原理基于地球自转天文学。计时方式将地球自转一周的时间等分为24小时以此作为计时标准。特点由于地球自转速度并非恒定不变受潮汐摩擦等因素影响自转在缓慢变慢GMT并不是极其精确的物理时间。 UTC (Universal Time Coordinated) - 协调世界时基础原理基于原子钟物理学。精确定义规定铯133原子基态的两个超精细能级间在零磁场下跌迁辐射9,192,631,770周所持续的时间为1秒。这是目前人类定义的最均匀、最精确的时间尺度国际原子时 TAI。闰秒机制关键因为原子钟过于精确而地球自转又存在波动如果完全使用原子钟时间长了会导致“原子时”与“地球自转时”产生巨大偏差比如白天变黑夜。因此当原子钟计时一天的时间与地球自转的时间相差超过0.9秒时UTC会执行闰秒加一秒或减一秒来保证其计时与地球自转的协调一致。时间戳转换时间戳转换C语言的time.h模块提供了丰富的函数用于在以下三种数据形态之间进行自由转换秒计数器time_t即前面提到的 Unix时间戳长整型。日期时间struct tm一个包含年、月、日、时、分、秒等成员的结构体是转换的核心桥梁。字符串char*人类可读的时间格式如 Wed Jun 30 21:49:08 1993\n。BKP简介在嵌入式系统特别是STM32等微控制器中BKP通常与RTC实时时钟配合使用用于在系统主电源掉电时保存关键数据和时间。 BKP 核心概念BKP备份寄存器是微控制器内部的一块特殊内存区域。它不是普通的RAM它的特点是极低功耗且独立供电。 核心特性逐条解析1. 双电源域与掉电保持最关键系统正常工作时BKP由主电源VDD2.0~3.6V供电。一旦主电源 VDD 被切断BKP 会自动切换到备用纽扣电池VBAT1.8~3.6V维持供电。此时BKP内的数据不会丢失。应用场景设备突然拔掉主电源依靠纽扣电池系统可以记录断电瞬间的时间戳结合前文讲的time_t防止重启后时间归零。2. 复位“免疫”系统在待机模式被唤醒、系统复位按复位键或者电源复位时普通RAM的数据会被清零但BKP中的数据不会被复位。应用场景记录设备发生故障重启的次数。每次重启读取BKP里的计数并1这样就能知道设备曾经复位过多少次。3. 安全防篡改TAMPER引脚芯片上有一个专门的TAMPER引脚。如果检测到外部入侵事件比如外壳被拆开触发引脚电平变化硬件会自动清除所有备份寄存器的内容。应用场景智能电表、POS机等对安全要求高的设备。一旦被非法拆解密钥或关键数据瞬间销毁防止被窃取。4. 与 RTC 的深度协同输出脉冲RTC引脚可以输出 RTC校准时钟、RTC闹钟脉冲或秒脉冲每秒跳变一次的信号可用于驱动外部时钟或闪烁LED。存储校准值用来存储 RTC 时钟校准寄存器修正晶振带来的时间误差。时间保持结合前几张幻灯片Unix时间戳秒计数器通常存在RTC计数器里而RTC靠VBAT供电走时。BKP就是维护这个时间体系稳定的“安全屋”。5. 存储容量中容量和小容量芯片20字节。大容量和互联型芯片84字节。注意由于容量非常小BKP只能存极度关键的标志位、时间戳、校准值或简短的状态数据不能当成大容量存储器用。BKP基本结构左侧外部接口输入/输出这部分展示了BKP模块与外界交互的三个核心引脚VBAT电池供电这是整个BKP模块的“生命线”。当主电源VDD掉电时外接的纽扣电池通过VBAT引脚持续为内部寄存器供电保证数据不丢失。TAMPER侵入检测安全防护引脚。外部触发此引脚产生电平变化比如设备外壳被撬开会直接联动内部逻辑清除所有数据寄存器。RTC实时时钟双向接口。一方面接收RTC的时钟信号另一方面可以将内部存储的校准值或闹钟脉冲输出到RTC引脚供外部设备使用。右侧内部结构橙色区域这是BKP模块的内部寄存器全部位于VBAT供电域内。分为四类数据寄存器DR1 ~ DR42这是一个个16位的寄存器[15:0]表示宽度为16个比特即2个字节。容量对应完美呼应上一张PPT中小容量芯片只有DR1 ~ DR10共10个寄存器 × 2字节 20字节。大容量和互联型芯片扩展到了DR1 ~ DR42图中用大括号标注了新增部分共42个寄存器 × 2字节 84字节。控制寄存器用来配置BKP的工作模式比如使能TAMPER检测、设置RTC输出类型等。状态寄存器用来记录当前的状态比如是否发生过TAMPER侵入事件、RTC闹钟是否触发等。RTC时钟校准寄存器专门划拨出一部分空间用来存储RTC时钟的校准值以修正晶振误差带来的时间偏差。硬件工作逻辑总结数据保护只要VBAT有电不管主系统怎么复位系统复位、电源复位、待机唤醒数据寄存器DRx的内容都不会被清除。安全机制一旦TAMPER引脚检测到入侵比如拆机硬件会自动清空DRx保护机密信息如密钥、计费数据不被窃取。时间与数据协同RTC负责走时BKP负责存储时间的校准值和断电前的关键时间戳。RTC简介RTCReal Time Clock实时时钟。一、 RTC 是什么核心定义字面意思Real Time Clock实时时钟。本质它是一个独立的定时器独立于CPU主频运转。作用为整个单片机系统提供绝对的时间和日期日历功能。特点它位于系统的后备区域和上一张图的BKP处于同一个供电域。这意味着只要VBAT有电不管系统怎么复位、主电源VDD怎么断电RTC都会像个永不停歇的钟表一样继续走时。RTC 的底层原理硬核硬件参数32位可编程计数器完美呼应Unix时间戳我们前面讲的Unix时间戳秒计数器在物理层面就是靠这个32位寄存器来存储的。它每来一个“秒脉冲”就加1。由于是32位它最大可以记录到2^32 - 1秒也就是我们在第一张图提到的“2038年问题”。20位可编程预分频器为什么需要它单片机外部的晶振通常振动得非常快比如32.768KHz但我们需要的是“1秒跳一次”。预分频器就像一个“减速齿轮”把高频的时钟源降频成精准的 1Hz1秒。20位意味着它可以处理非常复杂的降频比例。三种时钟源选择这决定了RTC走得准不准需要根据成本和功耗来权衡HSE / 128高速外部时钟通常用8MHz的主晶振除以128。优点不需要额外晶振。缺点主电源断电后8MHz晶振通常就不工作了RTC会停摆除非有独立供电的HSE很少见。LSE低速外部时钟通常接一个32.768KHz的专用纽扣晶振。优点精度极高且功耗极低配合VBAT可以走时好几年。缺点需要额外加一个晶振。嵌入式设计中的绝对主流方案LSI低速内部时钟芯片内部自带的40KHzRC振荡器。优点不需要外部元器件省钱省空间。缺点内部RC振荡器受温度影响大时间长了容易跑偏可能一天差几秒到几十秒。神奇的数字 32.768KHz为什么这么多电子表、单片机都用这个频率因为32768 2^15。把32768分频15次刚好就是1Hz每秒一次非常适合数字电路的分频。知识大串联一个完整的嵌入式电子钟是怎么工作的结合你之前的所有提问一个完整的断电走时电子钟系统是这样的硬件底层系统上电你通过代码配置RTC选择LSE32.768KHz晶振作为时钟源。经过20位预分频器将时钟降频为 1Hz。RTC的32位计数器开始每秒加1这就产生了底层的 Unix时间戳。断电保护主电源 VDD 突然断开VDD掉电。RTC和BKP自动切换为VBAT纽扣电池供电32位计数器继续默默加1。安全防护如果外壳被撬TAMPER引脚触发BKP内的关键数据被清空。软件应用系统重新上电复位。C语言程序读取RTC的32位计数器也就是time_t。调用localtime()将其转换成带有年月日时分秒的结构体struct tm。调用strftime()把它格式化成字符串显示在LCD屏幕上或者通过串口发出去。从Unix时间戳 - UTC/GMT - C语言函数 - BKP备份寄存器 - RTC实时时钟硬件电路整个“时间管理系统”从抽象概念走向实际物理硬件的落地。备用电池供电电路这是保证RTC和BKP在断电后存活的关键。图中对比了两种设计简单连接一个3V电池B1直接接到VBAT引脚。缺点是如果设备有主电源3.3V主电源和电池可能会互相“打架”反向充电或耗电。推荐连接重点主电源3V3通过二极管D2接到VBAT。备用电池B2通过二极管D1接到VBAT。并联一个0.1uF的电容C3用于滤波。核心原理二极管具有单向导电性。当主电源3V3正常时3.3V 3VD2导通D1截止系统由主电源供电节省电池电量。当主电源掉电时3V3变为0D2截止D1导通系统无缝切换到备用电池继续维持RTC走时和BKP数据保存。外部低速晶振电路这是我们之前提到的LSE低速外部时钟的物理实现。晶振 X132.768KHz 的圆柱形晶振。为什么是这个奇怪数字因为32768 2^15单片机的预分频器只需要将它连续除以2十五次就能完美得到精准的 1Hz1秒1跳脉冲。匹配电容 C1, C2两个10pF的电容。它们连接在晶振两端并接地用于起振和稳定频率补偿晶振的负载电容。没有这两个电容晶振可能不起振或者走时不准。连接引脚分别连接到单片机的OSC32_IN和OSC32_OUT。STM32主控引脚映射这里用的是经典的STM32F103C8T648脚封装最小系统。重点看左上角我们需要的引脚引脚 1 (VBAT)接入左侧的备用电源。引脚 2 (PC13-TAMPER-RTC)这是一个多功能复用引脚。对应了我们之前讲的TAMPER引脚防拆机检测和 RTC 功能。在代码中我们可以配置这个引脚一旦电平发生变化外壳被撬硬件自动清除BKP数据。引脚 3, 4 (PC14-OSC32_IN,PC15-OSC32_OUT)接入中间的32.768KHz晶振。四、 实物图上图一个独立的RTC模块。我们可以看到一颗CR2032纽扣电池座、一颗32.768KHz贴片晶振以及一根I2C/SPI排针。这就是将上述电路集成在一个小PCB上的成品。下图一块STM32开发板。可以清晰看到右下角的纽扣电池座以及旁边焊接着的银色32.768KHz圆柱晶振。RTC操作注意事项RTC 寄存器操作“开锁”四部曲第一步开总闸与拿钥匙原文使能RCC_APB1ENR的PWREN和BKPEN设置PWR_CR的DBP。大白话给电源接口PWR和备份接口BKP提供时钟开电源开关。必须解锁。DBP位Disable Backup Protection是后备区域的“防盗门”。如果不把 DBP 位置 1你根本无法对 RTC 和 BKP 的任何寄存器进行写操作。第二步等同步对表原文若 APB1 接口曾禁止读 RTC 寄存器前必须等待RTC_CRL的RSF位被硬件置 1。大白话如果你刚才把 RTC 时钟关了再次打开时系统总线APB1和 RTC 时钟通常是很慢的 32.768KHz步调不一致。RSFRegisters Synchronized Flag就是告诉硬件“等对好表再操作”。第三步进入配置模式改表原文必须设置RTC_CRL的CNF位进入配置模式后才能写入RTC_PRL、RTC_CNT、RTC_ALR寄存器。大白话CNFConfiguration Flag是修改时间参数的唯一开关。你想改预分频PRL、当前时间戳CNT还是闹钟ALR不把 CNF 置 1写了也是白写。第四步等写完原文对 RTC 任何寄存器的写操作都必须在前一次写操作结束后进行。查询RTC_CR的RTOFF状态位仅当RTOFF 1时才可以写入。大白话因为 RTC 时钟源32.768KHz比系统主频72MHz慢得多写一个数据进去RTC 硬件需要好几个 RTC 时钟周期才能真正写完。RTOFFRTC Operation OFF就是告诉你“我写完了你可以发下一条指令了”。如果不加while(!(RTC-CRL RTOFF));死等你连续写入数据就会丢失或卡死。读写备份寄存器main.c #include stm32f10x.h #include Delay.h #include OLED.h #include Key.h uint8_t key 0; uint16_t array_w[] {0x1234, 0x5678}; uint16_t array_r[2]; int main(void) { RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE); PWR_BackupAccessCmd(ENABLE); OLED_Init(); KeyInit(); OLED_ShowString(1, 1, W:); OLED_ShowString(2, 1, R:); while(1) { key presskey(); if (1 key) { array_w[0]; array_w[1]; BKP_WriteBackupRegister(BKP_DR1, array_w[0]); BKP_WriteBackupRegister(BKP_DR2, array_w[1]); OLED_ShowHexNum(1, 3, array_w[0], 4); OLED_ShowHexNum(1, 8, array_w[1], 4); } array_r[0] BKP_ReadBackupRegister(BKP_DR1); array_r[1] BKP_ReadBackupRegister(BKP_DR2); OLED_ShowHexNum(2, 3, array_r[0], 4); OLED_ShowHexNum(2, 8, array_r[1], 4); } return 0; } key.c #include stm32f10x.h // Device header #include Delay.h void KeyInit() { //GPIO配置 RCC_APB2PeriphClockCmd( RCC_APB2Periph_GPIOB,ENABLE); GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_ModeGPIO_Mode_IPU; GPIO_InitStructure.GPIO_PinGPIO_Pin_1; GPIO_InitStructure.GPIO_SpeedGPIO_Speed_50MHz; GPIO_Init(GPIOB,GPIO_InitStructure); } char presskey() { static char last_state1; uint8_t current_stateGPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1); uint8_t num0; if(last_state1current_state0) { Delay_ms(20); if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1)0) { Delay_ms(20); num1; last_state0; } } if(last_state0current_state1) { last_state1; } return num; }只要拔掉VBT才会使BKP里的数据恢复为0正常断开VCC不会导致数据丢失实时时钟RTC#include rtc.h #include stm32f10x.h #include time.h // 引入C标准库时间函数使用 time_t, struct tm, mktime, localtime uint16_t rtc_time[] {2024, 1, 1, 23, 59, 55}; // 格式年, 月, 日, 时, 分, 秒 void rtc_set_time(void) { time_t cnt; // Unix时间戳秒计数器 struct tm t; // C标准库的时间结构体 // 1. 将数组中的时间赋值给结构体注意C标准库的格式要求 t.tm_year rtc_time[0] - 1900; // 年份要减去1900比如2024年tm_year存124 t.tm_mon rtc_time[1] - 1; // 月份要减11月存012月存11 t.tm_mday rtc_time[2]; // 日1-31 t.tm_hour rtc_time[3]; // 时0-23 t.tm_min rtc_time[4]; // 分0-59 t.tm_sec rtc_time[5]; // 秒0-59 // 2. 【核心转换】利用 mktime 将本地时间结构体转为Unix时间戳 // 为什么减去 8*60*60因为北京时间是东八区UTC8 // 而RTC底层硬件计数器只认UTC/GMT时间对应第一张PPT的理论。 cnt mktime(t) - 8 * 60 * 60; // 3. 将计算好的秒计数器写入RTC硬件计数器 RTC_SetCounter(cnt); RTC_WaitForLastTask(); // 写完必须等待硬件操作完成 } void rtc_init(void) { // 1. 使能PWR和BKP时钟对应《RTC操作注意事项》第一步 RCC_APB1PeriphClockCmd(RCC_APB1Periph_PWR, ENABLE); RCC_APB1PeriphClockCmd(RCC_APB1Periph_BKP, ENABLE); // 2. 解除备份区域写保护拿钥匙使能对BKP和RTC的访问 PWR_BackupAccessCmd(ENABLE); // 3. 判断是否是首次上电利用魔术字 0xA5A5 if (BKP_ReadBackupRegister(BKP_DR1) ! 0xA5A5) { // ----- 首次上电需要完整初始化RTC ----- // 开启外部低速晶振 LSE (32.768KHz) RCC_LSEConfig(RCC_LSE_ON); while(RCC_GetFlagStatus(RCC_FLAG_LSERDY) ! SET); // 死等LSE起振稳定刚上电晶振需要几百毫秒 // 配置RTC时钟源为LSE并使能RTC时钟 RCC_RTCCLKConfig(RCC_RTCCLKSource_LSE); RCC_RTCCLKCmd(ENABLE); // 等待同步对应RSF位和等待上一次写操作完成对应RTOFF位 RTC_WaitForSynchro(); RTC_WaitForLastTask(); // 设置预分频器32768 - 1 // 核心原理32.768KHz / 32768 1Hz每秒跳动一次 RTC_SetPrescaler(32768 - 1); RTC_WaitForLastTask(); // 写入PRL后必须等待写完成 // 调用自定义函数设置初始时间 rtc_set_time(); // 写入魔术字 0xA5A5标记RTC已初始化完毕 BKP_WriteBackupRegister(BKP_DR1, 0xA5A5); } else { // ----- 非首次上电系统复位或主电源掉电后重启 ----- // 此时RTC和BKP由VBAT电池维持供电时间正在滴答走不能重置 // 只需要等待同步和上一次写操作完成就可以直接使用。 RTC_WaitForSynchro(); RTC_WaitForLastTask(); } } void rtc_get_time(void) { time_t cnt; // 存储读取出来的Unix时间戳 struct tm t; // 存储转换后的本地时间结构体 // 1. 【核心转换】读取RTC硬件计数器UTC秒数并手动加上东八区的8小时偏移 cnt RTC_GetCounter() 8 * 60 * 60; // 2. 利用 localtime 将Unix时间戳转换为 struct tm 结构体 // 注意localtime 返回的是指针前面加 * 解引用赋值给 t t *localtime(cnt); // 3. 将结构体中的数据反向提取到全局数组中加回1900和1 rtc_time[0] t.tm_year 1900; rtc_time[1] t.tm_mon 1; rtc_time[2] t.tm_mday; rtc_time[3] t.tm_hour; rtc_time[4] t.tm_min; rtc_time[5] t.tm_sec; }#include stm32f10x.h #include delay.h #include oled.h #include RTC.h int main(void) { //初始化 OLED_Init(); rtc_init(); //显示十六进制 //OLED_ShowHexNum(3, 1, 0xAA55, 4); 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) { rtc_get_time(); OLED_ShowNum(1, 6, rtc_time[0], 4); //显示MyRTC_Time数组中的时间值年 OLED_ShowNum(1, 11, rtc_time[1], 2); //月 OLED_ShowNum(1, 14, rtc_time[2], 2); //日 OLED_ShowNum(2, 6, rtc_time[3], 2); //时 OLED_ShowNum(2, 9, rtc_time[4], 2); //分 OLED_ShowNum(2, 12, rtc_time[5], 2); //秒 OLED_ShowNum(3, 6, RTC_GetCounter(), 10); //显示32位的秒计数器 OLED_ShowNum(4, 6, RTC_GetDivider(), 10); //显示余数寄存器 } return 0; }
阅读完成 · 觉得有帮助?