跳过正文

Unix 时间戳在 Keil C51 中的实现

··1556 字·4 分钟· loading · loading · ·
学习 实习 C C++
Aphcity
作者
Aphcity
只是一介学徒
目录

为了在 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 中,shortint 的类型大小是相同的,均为16字节大小,这也导致之后在 Keil 转移编程时,产生了数据溢出,最后使用了 unsigned long 作为 32 位无符号整数的储存方式,这里附上 C51 中基本通用的数据类型大小参考

Data TypesBitsBytesMinium ValueMaxium Value
bit101
signed char81-128127
unsigned char810255
enum8 / 161 / 2-128 / -32768+127 / +32767
signed short int162-32768+32767
unsigned short int162065535
signed int162-32768+32767
unsigned int162065535
signed long int324-2147483648+2147483647
unsigned long int32404294967295
float324±1.175494E-38±3.402823E+38
double324±1.175494E-38±3.402823E+38
sbit101
sfr810255
sfr16162065535