🚀 我的STM32学习专栏:[点击直达 | 持续更新中]

适用平台:STM32F103C8T6(标准库)
前置知识:RTC实时时钟、BKP备份寄存器、C语言结构体


一、Unix时间戳是个啥?

很多同学第一次听到"Unix时间戳"这个词,脑子里第一反应是:"这玩意儿是不是跟Linux系统有关?我在STM32里用得上吗?"

其实完全不是你想的那样! Unix时间戳本质上就是一个超级大号的秒计数器,跟咱们STM32里定时器的CNT计数器是一个道理,只不过它的"时钟源"不是晶振,而是从1970年1月1日0时0分0秒(UTC/GMT)开始,每过1秒就加1


Unix时间戳概念示意图——本质上就是一个从1970年开始走的秒计数器

1.1 为什么全世界都用时间戳?

我的理解是这样的:你想想,如果你要做一个物联网项目,STM32采集的数据要上传到云端服务器。如果STM32传的是"2026年8月8日18点21分"这种字符串,服务器解析起来得多麻烦?但如果传的是一个32位的整数,比如1754665261,服务器一眼就知道这是哪一刻。时间戳就是时间的"国际通用货币",不管你在中国、美国还是火星,同一个时间戳代表的就是同一个瞬间。

1.2 时间戳的核心特点

特点说明
统一基准全球统一从1970-01-01 00:00:00 UTC开始计数
时区无关同一个时间戳,全世界都一样;时区差异通过加减偏移量来体现
存储高效一个uint32_t就能存下,只占4个字节
运算方便两个时间戳直接相减,得到的就是间隔秒数

⚠️ 注意:时间戳本身不考虑闰秒(Leap Second),所以和原子钟会有微小偏差,但在嵌入式开发里完全够用。


二、C语言time.h:时间戳转换的"万能工具箱"

江科大老师在RTC视频里用到了time.h这个头文件,很多同学可能没太在意。但实际上,这个头文件就是时间戳转换的核心武器库,里面提供了一堆现成函数,让我们不用自己手算闰年、月份天数这些破事儿。

2.1 struct tm 结构体——"人类能看懂的时间格式"

struct tm是C标准库定义的一个结构体,专门用来存"年月日时分秒"这种我们熟悉的时间格式。它里面有几个字段需要特别注意:

  • tm_sec:秒,范围 0~59

  • tm_min:分,范围 0~59

  • tm_hour:时,范围 0~23

  • tm_mday:日,范围 1~31

  • tm_mon:月,范围 0~11 ⚠️ 注意这里是从0开始!

  • tm_year:年,从1900年开始的偏移量 ⚠️ 比如2026年,这里存的是126

  • tm_wday:星期,0~6(0是周日)

  • tm_yday:一年中的第几天,0~365

 关键点tm_mon是从0开始算的(0代表1月),tm_year是从1900年开始算的。这两个偏移量是很多新手踩坑的重灾区,后面我会详细讲。

2.2 核心转换函数一览

函数功能方向
mktime()struct tm → 转换为时间戳(秒计数器)日期时间 → 时间戳
localtime()将时间戳 → 转换为本地时间的struct tm时间戳 → 日期时间
gmtime()将时间戳 → 转换为GMT/UTC时间的struct tm时间戳 → 日期时间
time()获取当前系统时间戳
difftime()计算两个时间戳的差值(秒)

2.3 时区问题——东八区为什么要 ±8小时?

这里很多同学会懵:为什么江科大代码里写+ 8 * 60 * 60或者- 8 * 60 * 60

因为Unix时间戳的基准是UTC/GMT时间(零时区),而我们中国使用的是北京时间(东八区,UTC+8)。也就是说:

  • 当北京时间是 2026-08-08 08:00:00 的时候

  • UTC时间其实是 2026-08-08 00:00:00

  • 这两个时刻的时间戳是一样的

所以:

  • 写入RTC时(北京时间 → UTC时间戳):要 减去 8小时的秒数(28800秒)

  • 读取RTC时(UTC时间戳 → 北京时间):要 加上 8小时的秒数


三、STM32中RTC与时间戳的配合逻辑

3.1 RTC的本质——一个32位秒计数器

STM32F103的RTC模块,核心就是一个32位向上计数的CNT寄存器。它的工作流程是这样的:

LSE晶振(32.768kHz) → 预分频器(÷32768) → 1Hz秒脉冲 → CNT每秒+1

你看,RTC的CNT本质上就是一个硬件层面的时间戳计数器!我们只需要:

  1. 初始化时:把某个时刻的Unix时间戳写入CNT

  2. 运行时:每秒CNT自动+1,时间戳就自然递增了

  3. 需要显示时:把CNT的值读出来,用localtime()转成年月日时分秒

3.2 BKP备份寄存器的作用——防止重复初始化

这里必须结合江科大讲的BKP备份寄存器来理解。RTC在备份域里,有独立电池供电,所以主电源掉电后RTC还能继续走。但如果每次上电都重新初始化RTC、重新设置时间,那掉电保持时间就没意义了!

所以标准做法是:

  • 第一次上电:配置RTC,写入时间戳,然后在BKP_DR1里写一个"暗号"(比如0xA5A5

  • 后续上电:读取BKP_DR1,发现暗号已存在,说明RTC已经配好了,直接跳过初始化


四、时间戳转换的完整流程梳理

4.1 设置时间:年月日时分秒 → 时间戳 → 写入RTC

整个流程可以拆解成三步:

第一步:把时间值填入struct tm结构体

这里有两个偏移量要特别注意:

  • 年份要减去1900(因为tm_year存的是从1900年开始的偏移量)

  • 月份要减去1(因为tm_mon从0开始,0代表1月)

第二步:调用mktime()函数转换为时间戳

mktime()接收一个struct tm指针,返回一个time_t类型的秒计数器。这里要注意:mktime()返回的是本地时间(东八区)对应的时间戳,而RTC里应该存的是UTC时间戳。所以转换后还要减去8小时的秒数8 * 60 * 60 = 28800秒)。

第三步:把处理好的时间戳写入RTC的CNT寄存器

调用RTC_SetCounter()写入,然后等待写入完成。

4.2 读取时间:从RTC读出时间戳 → 转成年月日时分秒

第一步:从RTC的CNT寄存器读取当前秒计数器

读出来的值是UTC时间戳,要转回北京时间(东八区),需要加上8小时的秒数

第二步:调用localtime()函数转换为struct tm结构体

localtime()会自动根据系统时区(东八区)进行转换,返回一个指向struct tm的指针。

第三步:把struct tm的值转回普通时间格式

这里同样要注意还原偏移量:

  • 年份要加上1900

  • 月份要加上1


五、💡 我的理解

以下心得板块为博主个人学习总结,仅供参考交流。

5.1 时间戳到底解决了什么问题?

我自己在刚学RTC的时候,一直有个疑惑:既然RTC本身就能记录秒数,为什么还要搞个"Unix时间戳"的概念出来?直接把年月日时分秒存在RTC里不就行了吗?

后来做项目我才明白——时间戳的本质是"统一语言"。你想啊,如果你的STM32要和一个ESP8266模块通信,ESP8266再和服务器通信,服务器可能是Linux系统。如果每个人都用自己的时间格式,那中间转换来转换去,迟早会出错。但如果大家统一用时间戳(一个32位整数),那就没有任何歧义了。

而且时间戳的运算超级方便。比如你想判断"某个事件是否发生在5分钟前",如果是年月日格式,你得先算分钟差、小时差、日期差……但如果是时间戳,直接两个数相减,再和5 * 60 = 300比较就行了。这就是整型运算的魅力,MCU最喜欢干这种事儿。

5.2 为什么江科大代码里用time.h而不是自己写转换算法?

我自己也试过手写一个"年月日转秒数"的函数,结果发现要考虑的东西太多了:闰年怎么算?每个月天数不一样怎么办?闰年的2月是29天……写到最后代码又臭又长,还容易出bug。

time.h里的mktime()localtime()是C标准库函数,经过几十年的打磨,算法绝对可靠。在STM32上,这些函数由编译器提供(Keil的ARM Compiler自带),我们直接调用就行,省时省力还不容易出错。我的建议是:能用标准库就用标准库,别重复造轮子。

5.3 关于2038年问题的思考

time_t在Keil环境下默认是32位有符号整数,最大能表示到2038年1月19日03:14:07。过了这个时间,时间戳就会溢出变成负数。这就是著名的"2038年问题"。

不过对于我们现在的学习和项目来说,这完全不是问题。而且STM32F103的RTC CNT本身就是32位的,就算你想用64位时间戳,硬件也存不下。所以现阶段不用纠结这个,知道有这么回事就行。


六、⚠️ 踩坑记录

以下踩坑记录为博主在调试过程中真实遇到的问题,希望能帮你少走弯路。

❌ 坑1:月份显示总是差1(1月显示成2月)

现象:设置时间是2023年1月1日,读出来显示2023年2月1日。

原因struct tm里的tm_mon是从0开始的!0代表1月,1代表2月。写入时要-1,读取时要+1。我当时就是忘了这个偏移量,调了一下午。

解决办法:记住这个口诀——"存的时候减1,读的时候加1"

❌ 坑2:年份显示成"123年"而不是"2023年"

现象:OLED上显示"Date: 0123-01-01"。

原因tm_year存的是从1900年开始的偏移量,2023年对应的值是123。写入时没-1900,或者读取时没+1900

解决办法"存的时候减1900,读的时候加1900"

❌ 坑3:时间比实际快/慢8小时

现象:北京时间晚上8点,RTC显示的是中午12点。

原因:时区转换搞反了。mktime()返回的是本地时间(东八区)的时间戳,而RTC里应该存UTC时间戳。如果写入时没- 8 * 60 * 60,那RTC存的就是东八区时间戳,等读取时再加8小时,就多了8小时。

解决办法:画个时区转换图,理清楚:

  • 写入RTC:北京时间 → mktime() → 得到东八区时间戳 → 减去8小时 → 得到UTC时间戳 → 写入CNT

  • 读取RTC:从CNT读出UTC时间戳 → 加上8小时 → 得到东八区时间戳 → localtime() → 得到北京时间


七、📊 总结表格

知识点核心内容易错点提醒
Unix时间戳从1970-01-01 00:00:00 UTC开始的秒计数器全球统一,与时区无关
struct tmC标准库定义的时间结构体tm_mon从0开始,tm_year从1900开始
mktime()struct tm → 时间戳返回的是本地时间戳,写入RTC要减8小时
localtime()时间戳 → struct tm(本地时间)会自动加上时区偏移
时区转换北京时间 = UTC时间 + 8小时写入减8h,读取加8h,方向别搞反
BKP标志位0xA5A5判断RTC是否已配置防止每次上电重复初始化
LSE晶振32.768kHz外部低速晶振精度高,掉电可走;没焊晶振会卡死
LSI备用方案内部低速RC振荡器不用外接晶振,但掉电停走,精度较差

写这篇文章的时候,我把江科大的视频又回顾了一遍,特别是RTC那一章,发现很多第一遍看的时候没注意到的细节,比如RTC_WaitForLastTask()这个函数为什么每次操作后都要调用、BKP备份寄存器到底怎么防止重复初始化等等。其实STM32的每一个外设都不难,难的是把这些知识点串成一条线。时间戳这个知识点,把C语言标准库、RTC硬件、时区概念、BKP备份寄存器全串起来了,理解了它,RTC这一章就真正通了。

📌 开发环境:Keil5 + STM32F103C8T6最小系统板 + ST-Link
📌 参考资料:江科大STM32入门教程、《STM32F10xxx参考手册》

如果本文对你有帮助,欢迎点赞 👍 收藏 ⭐ !有任何问题欢迎在评论区交流,一起学习进步!

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐