有限嵌入式设备上的时间戳转日期

6 投票
2 回答
3955 浏览
提问于 2025-04-17 20:17

我正在尝试找出一种最佳方法,将从1970年1月1日以来的秒数(也称为epoch秒)转换为日期时间字符串(格式为MM/DD/YY,hh:mm:ss),而不使用任何库、模块或外部函数,因为这些在我的嵌入式设备上不可用。

我最开始的想法是查看Python日期时间模块的源代码,但对我帮助不大。

我在Python中的初步尝试是使用从公历元年(0001-01-01)以来的天数来转换日期,使用了一个叫做getDateFromJulianDay的函数,这个函数是从C++源代码改编而来的,并结合取模运算来获取时间。这个方法是可行的,但有没有更好的方法呢?

def getDateFromJulianDay(julianDay):
    # Gregorian calendar starting from October 15, 1582
    # This algorithm is from:
    # Henry F. Fliegel and Thomas C. van Flandern. 1968.
    # Letters to the editor:
    #     a machine algorithm for processing calendar dates.
    # Commun. ACM 11, 10 (October 1968), 657-. DOI=10.1145/364096.364097
    # http://doi.acm.org/10.1145/364096.364097
    ell = julianDay + 68569;
    n = (4 * ell) / 146097;
    ell = ell - (146097 * n + 3) / 4;
    i = (4000 * (ell + 1)) / 1461001;
    ell = ell - (1461 * i) / 4 + 31;
    j = (80 * ell) / 2447;
    d = ell - (2447 * j) / 80;
    ell = j / 11;
    m = j + 2 - (12 * ell);
    y = 100 * (n - 49) + i + ell;
    return y,m,d

# NTP response (integer portion) for Monday, March 25, 2013 at 6:40:43 PM
sec_since_1900 = 3573225643

# 2415021 is the number of days between 0001-01-01 and 1900-01-01,
#     the start of the NTP epoch
(year,month,day) =  getDateFromJulianDay(2415021 + sec_since_1900/60/60/24)

seconds_into_day = sec_since_1900 % 86400
(hour, sec_past_hour) = divmod(seconds_into_day,3600)
(min, sec) = divmod(sec_past_hour,60)
print 'year:',year,'month:',month,'day:',day
print 'hour:',hour,'min:',min,'sec:',sec

我为什么要这样做: 我从一个NTP服务器获取当前时间,并直接用这个时间来更新一个硬件实时时钟(RTC),这个时钟只接受日期、时间和时区,格式为:MM/DD/YY,hh:mm:ss,±zz。我计划在以后实现真正的NTP功能。关于时间同步方法的讨论最好放在其他地方,比如这个问题

注意事项:

  • 我的嵌入式设备是一个Telit GC-864的蜂窝调制解调器,运行的是Python 1.5.2+,只支持有限的操作符(大部分就是C语言的操作符),没有模块,只有一些预期的内置Python类型。如果你感兴趣,可以在这里查看具体功能。我为这个设备写Python代码时,就像在写C代码一样——我知道这并不是很Pythonic。
  • 我知道NTP最好是用来获取时间偏移,但由于选择有限,我将NTP作为绝对时间源使用(我可以添加对2036年NTP重置的检查,以便再支持136年的运行)。
  • 更新固件的GC-864-V2设备确实具备NTP功能,但我需要使用的GC-864设备固件版本较旧,无法更新。

2 个回答

7

最初提出的 getDateFromJulianDay 函数在嵌入式设备上运行时太耗资源了,因为它需要对大数字进行很多乘法和除法运算,特别是在 C++ 中使用的 longlong 变量。

我觉得我找到了一个适合嵌入式设备的高效算法,可以把纪元时间转换为日期。

经过一番无果的搜索后,我又回到了 Stack Overflow,发现了一个问题 将纪元时间转换为“真实”的日期/时间,这个问题讨论了自己编写的纪元时间到日期的实现,并提供了一个合适的算法。这个问题的 答案 提到了我需要的 C 语言源代码,以便编写 Python 转换算法:

/*
 * gmtime - convert the calendar time into broken down time
 */
/* $Header: /opt/proj/minix/cvsroot/src/lib/ansi/gmtime.c,v 1.1.1.1 2005/04/21 14:56:05 beng Exp $ */

#include        <time.h>
#include        <limits.h>
#include        "loc_time.h"

struct tm *
gmtime(register const time_t *timer)
{
        static struct tm br_time;
        register struct tm *timep = &br_time;
        time_t time = *timer;
        register unsigned long dayclock, dayno;
        int year = EPOCH_YR;

        dayclock = (unsigned long)time % SECS_DAY;
        dayno = (unsigned long)time / SECS_DAY;

        timep->tm_sec = dayclock % 60;
        timep->tm_min = (dayclock % 3600) / 60;
        timep->tm_hour = dayclock / 3600;
        timep->tm_wday = (dayno + 4) % 7;       /* day 0 was a thursday */
        while (dayno >= YEARSIZE(year)) {
                dayno -= YEARSIZE(year);
                year++;
        }
        timep->tm_year = year - YEAR0;
        timep->tm_yday = dayno;
        timep->tm_mon = 0;
        while (dayno >= _ytab[LEAPYEAR(year)][timep->tm_mon]) {
                dayno -= _ytab[LEAPYEAR(year)][timep->tm_mon];
                timep->tm_mon++;
        }
        timep->tm_mday = dayno + 1;
        timep->tm_isdst = 0;

        return timep;
}

此外,关于问题 为什么 gmtime 这样实现?分析 也让我确认了 gmtime 函数的效率相当不错。

通过访问 raspberryginger.com 的 minix Doxygen 文档网站,我找到了在 gmtime.c 中包含的 C 宏和常量,这些来自于 loc_time.h。相关的代码片段是:

#define YEAR0           1900                    /* the first year */
#define EPOCH_YR        1970            /* EPOCH = Jan 1 1970 00:00:00 */
#define SECS_DAY        (24L * 60L * 60L)
#define LEAPYEAR(year)  (!((year) % 4) && (((year) % 100) || !((year) % 400)))
#define YEARSIZE(year)  (LEAPYEAR(year) ? 366 : 365)
#define FIRSTSUNDAY(timp)       (((timp)->tm_yday - (timp)->tm_wday + 420) % 7)
#define FIRSTDAYOF(timp)        (((timp)->tm_wday - (timp)->tm_yday + 420) % 7)
#define TIME_MAX        ULONG_MAX
#define ABB_LEN         3

extern const int _ytab[2][10];

extern const int _ytab 是在 misc.c 中定义的:

const int _ytab[2][12] = {
                { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 },
                { 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
        };

我还发现了一些其他的事情:

  • gmtime.c 文件参考 对于查找依赖关系非常有帮助。
  • gmtime 函数的月份、星期几和年份的索引都是从零开始的(最大范围分别是 0-11、0-6 和 0-365),而日期是从 1 开始的(1-31),具体可以参考 IBM gmtime() 参考

我为 Python 1.5.2+ 重写了 gmtime 函数:

def is_leap_year(year):
    return ( not ((year) % 4) and ( ((year) % 100) or (not((year) % 400)) ) )

def year_size(year):
    if is_leap_year(year):
        return 366
    else:
        return 365

def ntp_time_to_date(ntp_time):
    year = 1900         # EPOCH_YR for NTP
    ytab =  [ [ 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31],
              [ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] ]

    (dayno,dayclock) = divmod(ntp_time, 86400L)
    dayno = int(dayno)

    # Calculate time of day from seconds on the day's clock.
    (hour, sec_past_hour) = divmod(dayclock,3600)
    hour = int(hour)
    (min, sec) = divmod(int(sec_past_hour),60)

    while (dayno >= year_size(year)):
        dayno = dayno - year_size(year)
        year = year + 1
    month = 1                           # NOTE: month range is (1-12)
    while (dayno >= ytab[is_leap_year(year)][month]):
        dayno = dayno - ytab[is_leap_year(year)][month]
        month = month + 1
    day = dayno + 1

    return (year, month, day, hour, min, sec)

我在将 C++ 的 gmtime 函数重构为我的 Python 函数 ntp_time_to_date(ntp_time) 时做了一些修改:

  • 将纪元从 1970 年的 UNIX 纪元改为 1900 年的 NTP 纪元(NTP 的主要纪元)。
  • 稍微简化了时间计算的过程。
    • 比较 gmtimentp_time_to_date 的时间计算:
      • divmod(dayclock,3600)divmod(sec_past_hour,60) 中,(dayclock % 3600) / 60dayclock / 3600 的计算都是在后台进行的。
      • 唯一的区别是 divmod(sec_past_hour,60) 避免了对 dayclock(0-86399)进行 dayclock % 60 的取模,而是对 sec_past_hour(0-3599)在 divmod(sec_past_hour,60) 中进行取模。
  • 去掉了一些不需要的变量和代码,比如星期几。
  • 将月份的索引改为从 1 开始,因此月份范围变为 (1-12),而不是 (0-11)。
  • 在值小于 65535 时,尽早将变量类型转换为非 long 类型,以大幅减少代码执行时间。
    • 需要使用 long 类型的变量有:
      • ntp_time,自 1900 年以来的秒数(0-4294967295)
      • dayclock,一天中的秒数(0-86399)
    • 其余变量中最大的就是计算出的年份。

这个 Python 的 ntp_time_to_date 函数(以及它的依赖项)在嵌入式版本的 Python 1.5.2+ 上以及 Python 2.7.3 上都能成功运行,但当然如果可以的话,最好使用 datetime 库。

0

简要说明

如果你在使用Telit GC-864这个设备,Python解释器在执行每一行代码时似乎会插入一些延迟。

在Telit GC-864上,我提到的函数getDateFromJulianDay(julianDay)比我回答中的函数ntp_time_to_date(ntp_time)执行得更快。

详细信息

在GC-864上,代码的行数对执行时间的影响比代码的复杂性更大——听起来很奇怪,对吧。我的问题中的函数getDateFromJulianDay(julianDay)虽然有一些复杂的操作,大概有15行代码,但它的执行速度更快。而我回答中的函数ntp_time_to_date(ntp_time)虽然计算上比较简单,但因为有while循环,导致执行的代码行数超过100行:

  • 一个循环从1900年数到当前年份
  • 另一个循环从1月数到当前月份

测试结果

在实际的GC-864上进行的时间测试(注意:不是 GC-864-V2),每次试验使用相同的NTP时间输入(每个函数输出“3/25/2013 18:40”)。时间测试是通过printf语句调试进行的,计算机上的串口终端会给GC-864发送的每一行加上时间戳。

getDateFromJulianDay(julianDay)的测试结果:

  • 0.3802秒
  • 0.3370秒
  • 0.3370秒
  • 平均:0.3514秒

ntp_time_to_date(ntp_time)的测试结果:

  • 0.8899秒
  • 0.9072秒
  • 0.8986秒
  • 平均:0.8986秒

结果的变化部分是因为GC-864的蜂窝调制解调器会定期处理网络任务。

为了完整性,尽快将long变量转换为intntp_time_to_date(ntp_time)中有相当显著的效果。如果没有这个优化:

  • 2.3155秒
  • 1.5034秒
  • 1.5293秒
  • 2.0995秒
  • 2.0909秒
  • 平均:1.9255秒

在Telit GC-864上运行.pyo文件进行复杂计算并不是个好主意。使用GC-864-V2,它内置了NTP功能,可能是遇到这个问题时的一个解决方案。此外,更新的机器对机器(M2M)或物联网(IoT)蜂窝调制解调器能力更强。

如果你在GC-864上遇到类似问题,考虑使用更新更现代的蜂窝调制解调器

撰写回答