为了在 8051 单片机上实现用于物联网模块的计时功能,我们需要使用到 Unix 时间戳 来对时间进行更加规范的记录和传输,但是由于 8 位单片机的特殊性,我们需要在节省空间和符合 C51 代码要求的前提下,进行时间戳转换程序的编程,这也意味着我们无法使用 C99 自带的
times.h
,并对数据类型进行完美计算以防止数据的溢出
资源整合#
源码仓库:https://github.com/Aphcity/timestamp
Unix 时间戳 在线转换工具:https://tool.lu/timestamp/
在 Windows 下算法的创建#
适用于 Windows 等 32/64 位机的源码:Branch 6022208
统一使用时间结构体 TS_time_
来表示 RTC 时间格式
Unix 时间戳 转换为 RTC 时间格式#
time_struct stamptotime(uint32_t stamp)
{
const uint8_t month_buf[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
time_struct t_out;
uint32_t min_add, hour_add, day_add, mon_add, year_add, day_temp, year_temp;
t_out.h_year = 19;
t_out.l_year = 70;
t_out.mon = 1;
t_out.day = 1;
t_out.hour = 0;
t_out.min = 0;
t_out.sec = 0;
min_add = (t_out.sec + stamp) / 60;
t_out.sec = (t_out.sec + stamp) % 60;
hour_add = (t_out.min + min_add) / 60;
t_out.min = (t_out.min + min_add) % 60;
day_add = (t_out.hour + hour_add) / 24;
t_out.hour = (t_out.hour + hour_add) % 24;
mon_add = 0;
day_temp = t_out.day;
for (int i = t_out.mon - 1;; i++)
{
size_t a = i % 12;
year_temp = t_out.h_year * 100 + t_out.l_year + (i / 12);
size_t temp = (a == 1 && leapyear_check(year_temp)) ? (month_buf[a] + 1) : month_buf[a];
/* If we update the month, initial the day to the '1' */
if (mon_add)
day_temp = 1;
if (day_add + day_temp > temp)
{
mon_add++;
day_add -= (temp - day_temp + 1);
}
else
{
t_out.day += day_add;
break;
}
}
year_add = (t_out.mon + mon_add) / 12;
t_out.mon = (t_out.mon + mon_add) % 12;
// 大于30年时重置基准至2000年1月1日0时0分0秒
if (year_add > 30)
{
year_add -= 30;
t_out.h_year = 20 + year_add / 100;
year_add = year_add % 100;
t_out.l_year = 0;
}
else
t_out.h_year = 19;
t_out.l_year += year_add;
return t_out;
}
其中, month_buf
用于存放正常年份的每月天数,leapyear_check
来保证能够稳定判断闰年多出的那一天能够被正确计算在内。
逻辑和排错#
先计算无需复杂计算的 秒,分,时部分,简单取余即可
计算到日期部分,可以先计算出增加的天数,之后开始通过反复遍历 month_buf
来得到经过的年份、月份,得到最后的年增量,以及月增量
这里由于在处理大于30年的增量时,发现原本的代码(如下)在处理时,会导致输出的低位年份会产生溢出,这是因为在计算中年增量是逢百进一的,而起始的年份是1970年,低位年份 70 在处理 30+ 的数据时,会增加成 100+ 的数据。这里有两种解决办法,在进行加法后,再次对低位年份进行检查,大于 100 选择进位到高位;提前进行年增量的判断,当增量大于等于 30 年时,直接对高位加一,同时增量减少 30,相当于将时间戳的初始基准重置到了 2000 年,这样更容易理解,因此选择了后者
在处理实际时间月份处于12月时,由于 12n % 12 == 0,因此,12 月会直接被进位,导致年份加一,同时月份变成 0 月,因此增加条件判断,保证 12 月不被进位
RTC 时间格式 转换为 Unix 时间戳#
这部分相较转换为 RTC 要相对简单,逻辑方面是类似的
uint32_t timetostamp(time_struct t_in)
{
const uint8_t month_buf[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
uint32_t stamp = 0;
uint32_t year = t_in.h_year * 100 + t_in.l_year;
// 计算已过年的秒数
for (uint32_t y = 1970; y < year; y++)
stamp += leapyear_check(y) ? 366 * 86400 : 365 * 86400;
// 计算已过月的秒数
for (uint8_t m = 0; m < t_in.mon - 1; m++)
if (m == 1 && leapyear_check(year)) // February in a leap year
stamp += 29 * 86400;
else
stamp += month_buf[m] * 86400;
// 计算已过天的秒数
stamp += (t_in.day - 1) * 86400;
// 计算已过时分秒的秒数
stamp += t_in.hour * 3600;
stamp += t_in.min * 60;
stamp += t_in.sec;
// 返回 int
return stamp;
}
逻辑#
先把年份增量对应的秒数增量算出,这个可以直接计算,但是要注意闰年会多出一年,然后计算得到从1月开始经历的完整月份,及其包含的天数总和,之后将天数、小时数、分钟数对应的秒增量,以及秒本身,整合加起来,即可得到结果。
逻辑简单,相比上一个函数,也不容易出现计算或思维上的疏漏,并没有在 debug 过程中发现错误
移植 Keil#
在 C51 中,short
和 int
的类型大小是相同的,均为16字节大小,这也导致之后在 Keil 转移编程时,产生了数据溢出,最后使用了 unsigned long
作为 32 位无符号整数的储存方式,这里附上 C51 中基本通用的数据类型大小参考
Data Types | Bits | Bytes | Minium Value | Maxium Value |
---|---|---|---|---|
bit | 1 | 0 | 1 | |
signed char | 8 | 1 | -128 | 127 |
unsigned char | 8 | 1 | 0 | 255 |
enum | 8 / 16 | 1 / 2 | -128 / -32768 | +127 / +32767 |
signed short int | 16 | 2 | -32768 | +32767 |
unsigned short int | 16 | 2 | 0 | 65535 |
signed int | 16 | 2 | -32768 | +32767 |
unsigned int | 16 | 2 | 0 | 65535 |
signed long int | 32 | 4 | -2147483648 | +2147483647 |
unsigned long int | 32 | 4 | 0 | 4294967295 |
float | 32 | 4 | ±1.175494E-38 | ±3.402823E+38 |
double | 32 | 4 | ±1.175494E-38 | ±3.402823E+38 |
sbit | 1 | 0 | 1 | |
sfr | 8 | 1 | 0 | 255 |
sfr16 | 16 | 2 | 0 | 65535 |