江科大STM32入门:Unix时间戳详解


适用平台: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本质上就是一个硬件层面的时间戳计数器!我们只需要:
-
初始化时:把某个时刻的Unix时间戳写入CNT
-
运行时:每秒CNT自动+1,时间戳就自然递增了
-
需要显示时:把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 tm | C标准库定义的时间结构体 | 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参考手册》
如果本文对你有帮助,欢迎点赞 👍 收藏 ⭐ !有任何问题欢迎在评论区交流,一起学习进步!

更多推荐



所有评论(0)