扫码加入

  • 正文
  • 相关推荐
申请入驻 产业图谱

嵌入式开发的时间 “陷阱”:UTC、时区这些你真懂?

03/13 10:44
355
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

大家好,我是杂烩君。

我们一直在东八区写代码,处理时间时习惯性地把时区写死为东八区,即设备的小时数 = GMT小时数 + 8。设备上使用GPS授时来校准时间,GPS给出的是UTC时间。在国内一切正常,但设备一旦到了海外,时间就对不上了。

时区写死是嵌入式开发中非常常见的问题,尤其是当设备需要出口到不同国家和地区时。要正确处理这个问题,我们需要理解一套完整的知识链:UTC时间 -> 时区 -> 本地时间

一、基础概念:GMT 与 UTC

GMT(Greenwich Mean Time,格林威治平均时间),基于天文观测,曾作为全球时间的基准。

UTC(Coordinated Universal Time,协调世界时),基于原子钟,精度远高于GMT,是当前国际通用的时间标准。

在工程实践中,UTC 和 GMT 的时刻基本相同(误差不超过0.9秒),可以认为等价。但严格来说,UTC 才是现代标准,我们在代码中应统一使用 UTC 作为基准。

1、获取UTC时间戳

C标准库提供了 time() 函数,返回自 1970-01-01 00:00:00 UTC(即 Unix 纪元)至今的秒数:

#include <time.h>
time_t time(time_t *tloc);

使用例子:

#include <stdio.h>
#include <time.h>

int main(int argc, char **argv)
{
    time_t utc_time = time(NULL);
    printf("UTC timestamp = %ld sn", utc_time);
    return 0;
}

运行结果:

2、将UTC时间戳转换为日历时间

#include <time.h>
struct tm *gmtime_r(const time_t *timep, struct tm *result);
struct tm *localtime_r(const time_t *timep, struct tm *result);

struct tm 结构如下:

struct tm 
{
    int tm_sec;    /* 秒 (0-60,60用于闰秒) */
    int tm_min;    /* 分 (0-59) */
    int tm_hour;   /* 时 (0-23) */
    int tm_mday;   /* 日 (1-31) */
    int tm_mon;    /* 月 (0-11,注意从0开始) */
    int tm_year;   /* 年 (自1900起的偏移) */
    int tm_wday;   /* 星期 (0-6,Sunday=0) */
    int tm_yday;   /* 一年中的第几天 (0-365) */
    int tm_isdst;  /* 夏令时标志 (>0生效, 0未生效, <0未知) */
};

使用例子:

#include <stdio.h>
#include <time.h>

int main(int argc, char **argv)
{
    time_t utc_time = time(NULL);
    printf("UTC timestamp = %ld sn", utc_time);

    struct tm gmt_tm;
    gmtime_r(&utc_time, &gmt_tm); 
    printf("UTC time = %.4d-%.2d-%.2d %.2d:%.2d:%.2dn",
            gmt_tm.tm_year + 1900,
            gmt_tm.tm_mon + 1,
            gmt_tm.tm_mday,
            gmt_tm.tm_hour,
            gmt_tm.tm_min,
            gmt_tm.tm_sec);
    return0;
}

运行结果:

gmtime_r() 可以将时间戳转换为 struct tm 结构的UTC日历时间。相比 gmtime()gmtime_r() 是可重入版本,结果写入调用者提供的缓冲区,在RTOS等多任务嵌入式环境下使用更安全。同理,后面用到的 localtime_r() 也是 localtime() 的可重入版本。

二、时区原理

由于地球各地经度不同,太阳照射的时间也不同,因此全球划分为不同的时区。

1、时区划分规则

全球共分为 24个时区。以英国格林尼治天文台所在的经线(本初子午线)为中心,划分为零时区(中时区),向东为东1区至东12区,向西为西1区至西12区。

每个时区横跨经度15度(360° / 24 = 15°),对应1小时的时间差。每个时区以其中央经线的时间作为统一的区时。东12区和西12区各跨7.5度,以180度经线为界。

2、经度与时区对照

3、用代码计算本地时区

localtime_r() 函数可以将UTC时间戳转换为本地日历时间(依赖系统的时区设置):

通过对比 gmtime_r() 和 localtime_r() 的结果差异,可以计算出当前时区:

#include <stdio.h>
#include <time.h>

int main(int argc, char **argv)
{
    time_t utc_time = time(NULL);
    printf("UTC timestamp = %ld sn", utc_time);

    struct tm gmt_result, local_result;
    gmtime_r(&utc_time, &gmt_result);
    localtime_r(&utc_time, &local_result);

    printf("UTC   time = %.4d-%.2d-%.2d %.2d:%.2d:%.2dn",
            gmt_result.tm_year + 1900, gmt_result.tm_mon + 1,
            gmt_result.tm_mday, gmt_result.tm_hour,
            gmt_result.tm_min, gmt_result.tm_sec);

    printf("Local time = %.4d-%.2d-%.2d %.2d:%.2d:%.2dn",
            local_result.tm_year + 1900, local_result.tm_mon + 1,
            local_result.tm_mday, local_result.tm_hour,
            local_result.tm_min, local_result.tm_sec);

    int tz_offset = local_result.tm_hour - gmt_result.tm_hour;
    if (tz_offset < -12) 
    {
        tz_offset += 24; 
    } 
    elseif (tz_offset > 12) 
    {
        tz_offset -= 24;
    }

    printf("Local timezone = UTC%+dn", tz_offset);

    return0;
}

在北京时间环境下运行:

把Ubuntu系统的时区切换到其它国家,再次运行:

三、嵌入式实战:根据GPS经度计算时区

对于带GPS模块的嵌入式设备,可以从GPS获取当前经度,再通过经度推算时区。

计算规则:用经度除以15度,若余数的绝对值小于等于7.5度,商即为时区数;若余数的绝对值大于7.5度,则时区数为商+1(东经)或商-1(西经)。

#include <stdio.h>
#include <math.h>

int calc_timezone_by_longitude(double longitude)
{
    if (longitude < -180.0 || longitude > 180.0)
    {
        return0;
    }

    int quotient = (int)(longitude / 15);
    double remainder = fabs(longitude - quotient * 15);

    if (remainder <= 7.5)
    {
        return quotient;
    }
    else
    {
        return quotient + (longitude >= 0 ? 1 : -1);
    }
}

int main(int argc, char **argv)
{
    double test_cases[] = {116.4, -73.9, 0.0, 139.7, -122.4, 55.3};
    constchar *cities[] = {"Beijing", "New York", "London", "Tokyo", "San Francisco", "Dubai"};

    for (int i = 0; i < 6; i++)
    {
        int tz = calc_timezone_by_longitude(test_cases[i]);
        printf("%-15s longitude=%.1f => UTC%+dn", cities[i], test_cases[i], tz);
    }

    return0;
}

局限性: 经度计算只能得到理论时区,实际时区受政治、地理因素影响,可能与理论值偏差1小时甚至更多。例如中国横跨5个时区,但全国统一使用UTC+8。西班牙地理上属于UTC+0,实际使用UTC+1。因此,这种方法适合作为一个粗略的兜底方案,而非精确解。

四、注意点

1、夏令时(DST)—— 最容易被忽略的大坑

很多国家在夏季会将时钟拨快1小时以充分利用日照,这就是夏令时(Daylight Saving Time)。

前面 struct tm 里的 tm_isdst 字段就是用来标识夏令时状态的:

> 0:夏令时生效中

= 0:非夏令时

< 0:未知

夏令时带来的问题:

    • 同一个地方,一年中有一段时间的 UTC 偏移与其它时间不同(例如美国东部,冬季UTC-5,夏季UTC-4)每个国家的夏令时起止日期不同,有的国家甚至不用夏令时

仅靠经度完全无法判断夏令时

对于资源充足的嵌入式设备(如跑Linux的),可以利用系统的 IANA 时区数据库(tzdata)来处理。对于资源受限的MCU,则需要预置一张简化的时区/夏令时规则表,或通过云端/手机APP下发时区信息。

2、半时区 —— 不是所有时区都是整数

很多时候我们会假设时区偏移都是整数小时,但现实并非如此:

    印度:UTC+5:30尼泊尔:UTC+5:45伊朗:UTC+3:30澳大利亚中部:UTC+9:30

因此,在代码中存储时区偏移时,不要用 int timezone_hour,而应该用分钟作为单位:

typedef struct {
    int tz_offset_minutes;  /* 时区偏移,单位分钟,如 UTC+5:30 = 330 */
} device_timezone_t;

五、嵌入式设备时间架构设计建议

结合以上知识点,推荐以下架构:

┌─────────────┐     ┌──────────────┐     ┌─────────────┐
│  时间源      │ ──> │ 设备内部存储  │ ──> │ 显示/业务层  │
│ (GPS/NTP/APP)│     │ (统一用UTC)   │     │ (UTC+偏移)   │
└─────────────┘     └──────────────┘     └─────────────┘

设备内部统一存储和处理UTC时间,仅在显示和业务逻辑的最外层做时区转换。

1. 时间源校准

    GPS授时:NMEA语句中的时间即UTC,直接使用NTP校准:如果设备联网,NTP获取的也是UTC手机APP校准:APP将UTC时间戳和当地时区偏移(分钟)一并下发给设备

2. 时区信息获取(优先级由高到低)

    手机APP/云端下发(最准确,可包含夏令时信息)用户手动设置GPS经度推算(兜底方案,不含夏令时)

3. 本地时间转换

#include <stdio.h>
#include <time.h>

typedefstruct {
    int tz_offset_minutes;
} device_timezone_t;

void utc_to_local(time_t utc_timestamp, device_timezone_t *tz,
                  struct tm *local_tm)
{
    time_t local_timestamp = utc_timestamp + tz->tz_offset_minutes * 60;
    gmtime_r(&local_timestamp, local_tm);
}

int main(void)
{
    time_t utc_now = time(NULL);

    device_timezone_t tz_dubai = { .tz_offset_minutes = 240 };   /* UTC+4 */
    device_timezone_t tz_india = { .tz_offset_minutes = 330 };   /* UTC+5:30 */

    struct tm local;

    utc_to_local(utc_now, &tz_dubai, &local);
    printf("Dubai:  %.4d-%.2d-%.2d %.2d:%.2d:%.2dn",
            local.tm_year + 1900, local.tm_mon + 1, local.tm_mday,
            local.tm_hour, local.tm_min, local.tm_sec);

    utc_to_local(utc_now, &tz_india, &local);
    printf("India:  %.4d-%.2d-%.2d %.2d:%.2d:%.2dn",
            local.tm_year + 1900, local.tm_mon + 1, local.tm_mday,
            local.tm_hour, local.tm_min, local.tm_sec);

    return0;
}

4. 定时功能的处理

定时功能(如"每天早上7:00开机")需要特别注意:

      • 存储定时任务时,记录的应该是

    本地时间 + 时区偏移

      ,或直接转换为UTC时间存储时区变化时(设备移动到新地区、夏令时切换),需要重新计算定时任务的UTC触发时间推荐方案:每次手机APP连接设备时,自动同步UTC时间和时区信息,并重新校准定时任务

六、总结

要点 说明
内部统一用UTC 所有存储、传输、比较都用UTC时间戳
时区用分钟存储 支持UTC+5:30等半时区
不要写死时区 通过APP/云端/GPS动态获取
注意夏令时 仅靠经度无法处理,需要外部信息源
定时任务注意时区变化 时区更新后需要重新计算触发时间

相关推荐

登录即可解锁
  • 海量技术文章
  • 设计资源下载
  • 产业链客户资源
  • 写文章/发需求
立即登录

本公众号专注于嵌入式技术,包括但不限于C/C++、嵌入式、物联网、Linux等编程学习笔记,同时,公众号内包含大量的学习资源。欢迎关注,一同交流学习,共同进步!