跳转至

时间

TIME — 时间管理

时间相关功能对 MCU 至关重要。本节提供运行时间、SNTP 对时、世界时间以及供 WSN 时间同步协议使用的 NTP 集成钩子。

MCU中的时间可以分以下几种类型:

  • 运行时间: 指的是MCU从上电到现在的时间。

  • 世界时间: 指的是MCU所在的时区的时间。世界时间可以通过标准的年月日时分秒来表示,也可以表示为UNIX时间戳。


运行时间

ESP有自己的获取运行时间的函数esp_timer_get_time,依赖于esp_timer库。该函数返回从上电到现在的时间,单位为微秒。

为了方便使用,TinyToolbox重新定义了数据类型TinyTimeMark_t,并提供了一个函数tiny_get_running_time来获取运行时间。该函数返回的时间单位为int64_t,其长度足够以避免溢出。

typedef int64_t TinyTimeMark_t;

TinyTimeMark_t tiny_get_running_time(void) { return esp_timer_get_time(); }

使用参考:

void app_main(void)
{
    TinyTimeMark_t running_time = tiny_get_running_time();
    ESP_LOGI(TAG_TIME, "Running Time: %lld us", running_time);
}

世界时间

Warning

注意,获取世界时间需要建立在已经联网的基础上。也就是说,获取世界时间的函数需要在联网成功后调用。

NTP对时

NTP对时

NTP(Network Time Protocol)是网络时间协议的缩写,是一种用于在计算机网络中同步时间的协议。它可以通过互联网或局域网获取准确的时间信息。 NTP协议使用UDP协议进行通信,默认使用123端口。NTP服务器会定期向客户端发送时间信息,客户端根据这些信息来校正自己的系统时间。

   Client                      Server
     |------------------->      |     T1:请求发出
     |                          |
     |         <--------------- |     T2/T3:服务器收到 & 回复
     |                          |
     |------------------->      |     T4:客户端收到响应

NTP对时原理

NTP对时是基于四个时间戳:1. 客户端发送请求时的时间戳T1 2. 服务器接收到请求时的时间戳T2 3. 服务器发送响应时的时间戳T3 4. 客户端接收到响应时的时间戳T4。根据这四个时间戳,可以计算 网络延迟 Delay = (T4 - T1) - (T3 - T2),以及 时间偏移 Offset = ((T2 - T1) + (T3 - T4)) / 2。

ESP32 SNTP对时

ESP32中使用的是SNTP,也就是Simple Network Time Protocol。SNTP是NTP的简化版,适用于对时间精度要求不高的场景。ESP32中对时依赖于esp_sntp库。SNTP的精度通常在ms级别,适用于大多数应用场景。

SNTP 初始化(v1.1 — 基于信号量)

从 v1.1 开始,sync_time_with_timezone() 使用 FreeRTOS 二进制信号量 来阻塞调用任务直到 SNTP 回调触发,替代了旧的轮询循环。这节省了 CPU 资源,并提供了更确定的同步时序。

回调函数在 SNTP 响应到达的第一时间触发 NTP 同步钩子(见下文),为依赖模块(如 FTSP)提供最精确的本地时间锚点:

static void time_sync_notification_cb(struct timeval *tv)
{
    ESP_LOGI(TAG_SNTP, "Time synchronized!");
    /* 在时间戳最新时触发 NTP 钩子 */
    if (s_ntp_sync_hook != NULL && tv != NULL && tv->tv_sec > 946684800L)
    {
        s_ntp_sync_hook((uint32_t)tv->tv_sec);
    }
    if (s_sntp_sem) {
        xSemaphoreGive(s_sntp_sem);
    }
}

初始化增加了备用 NTP 服务器 (time.google.com):

static void initialize_sntp(void)
{
    ESP_LOGI(TAG_SNTP, "Initializing SNTP");
    esp_sntp_setoperatingmode(SNTP_OPMODE_POLL);
    esp_sntp_setservername(0, "pool.ntp.org");
    esp_sntp_setservername(1, "time.google.com");
    esp_sntp_set_time_sync_notification_cb(time_sync_notification_cb);
    esp_sntp_init();
}

现在的 sync_time_with_timezone() 在信号量上阻塞(30 秒超时)而非轮询:

void sync_time_with_timezone(const char *timezone_str)
{
    // ... 校验, setenv("TZ", ...) ...
    s_sntp_sem = xSemaphoreCreateBinary();
    initialize_sntp();

    if (xSemaphoreTake(s_sntp_sem, pdMS_TO_TICKS(30000)) != pdTRUE)
    {
        ESP_LOGW(TAG_SNTP, "NTP sync timeout.");
        // ... 清理 ...
        return;
    }
    // ... 获取时间, 设置 RTC, 输出日志 ...
}

NTP 同步钩子(v1.1 新增)

v1.1 的一个重要新增是 NTP 同步钩子回调 机制。这使得依赖 tiny_toolbox 的模块(如 tiny_measurement 中的 FTSP 时间同步模块)能够接收 NTP 同步通知,而无需创建循环 CMake 依赖

/**
 * @brief 注册一个在每次 NTP 同步成功后调用的回调
 * @param hook  bool (*hook)(uint32_t unix_sec), 或 NULL 清除
 */
void tiny_ntp_set_sync_hook(bool (*hook)(uint32_t unix_sec));

使用示例:

#include "tiny_time.h"

// FTSP 注册其 NTP 记录函数作为钩子
tiny_ntp_set_sync_hook(ftsp_ntp_record_sync);

// 之后,当 sync_time_with_timezone() 成功时,
// ftsp_ntp_record_sync() 会自动携带 Unix 时间戳被调用

NTP 同步状态(v1.1 新增)

模块维护内部 NTP 同步状态,用于追踪精确世界时间计算的锚点:

typedef struct TinyNtpSync_t
{
    uint32_t unix_sec;          // NTP 同步得到的 Unix 秒时间戳
    uint64_t local_us_anchor;   // 同步时刻的本地运行时间(微秒)
    uint64_t last_sync_time_us; // 上次同步的本地时间(微秒)
    uint8_t  sync_quality;      // NTP 同步质量指示器 (0-100%)
    bool     is_synced;         // NTP 同步是否有效
} TinyNtpSync_t;

查询 NTP 状态的公开函数:

// 手动记录 NTP 同步事件
bool tiny_ntp_record_sync(uint32_t unix_sec);

// 获取当前世界时间(微秒精度)
uint64_t tiny_ntp_get_world_time_us(void);

// 获取 NTP 同步信息
bool tiny_ntp_get_sync_info(TinyNtpSync_t *sync_info);

// 检查 NTP 是否已同步(带 5 分钟过期检查)
bool tiny_ntp_is_synced(void);

// 设置 NTP 同步质量
void tiny_ntp_set_quality(uint8_t quality);

// 获取当前世界时间(TinyDateTime_t 格式)
bool tiny_ntp_get_datetime_us(TinyDateTime_t *dt);

世界时间获取

TinyDateTime_t 结构体以微秒精度存储日历时间:

typedef struct TinyDateTime_t
{
    int     year;
    int     month;
    int     day;
    int     hour;
    int     minute;
    int     second;
    int32_t microsecond;   // 0-999999
} TinyDateTime_t;

tiny_get_current_datetime() 从系统获取当前墙钟时间(SNTP 同步后使用):

TinyDateTime_t tiny_get_current_datetime(bool print_flag);

UNIX 时间戳转换(v1.1 新增)

v1.1 添加了完整的 UNIX 时间戳转换工具:

// 将 Unix 秒转换为日历时间
bool tiny_unix_to_datetime(uint32_t unix_sec, TinyDateTime_t *dt);

// 将 Unix 微秒转换为日历时间(保留 µs 精度)
bool tiny_unix_us_to_datetime(uint64_t unix_us, TinyDateTime_t *dt);

// 将日历时间转换回 Unix 秒
uint32_t tiny_datetime_to_unix(const TinyDateTime_t *dt);

// 验证日历时间的有效性
bool tiny_is_valid_datetime(const TinyDateTime_t *dt);

// 闰年判断(内联辅助函数)
static inline bool tiny_is_leap_year(uint16_t year);

时间格式化(v1.1 新增)

v1.1 添加了用于日志记录和显示的人性化格式化工具:

// ISO 8601: "2026-05-15T10:30:00Z"
int tiny_format_datetime_iso8601(const TinyDateTime_t *dt, char *buf, size_t buflen);

// 带微秒: "2026-05-15 10:30:00.123456"
int tiny_format_datetime_us(const TinyDateTime_t *dt, char *buf, size_t buflen);

// 带毫秒: "2026-05-15 10:30:00.123"
int tiny_format_datetime_ms(const TinyDateTime_t *dt, char *buf, size_t buflen);

// 直接格式化 Unix 秒
int tiny_format_unix_sec(uint32_t unix_sec, char *buf, size_t buflen);

// 直接格式化 Unix 微秒
int tiny_format_unix_us(uint64_t unix_us, char *buf, size_t buflen);

// 自动缩放时长: "45 us" / "2.500 ms" / "1.200 sec" / "3.50 min" / "1.25 hr"
int tiny_format_duration_us(uint64_t duration_us, char *buf, size_t buflen);
}

return result;

} 使用参考:c void appmain(void) { // Initialize SNTP and sync time synctimewithtimezone("CST-8"); // Get current time TinyDateTimet currenttime = tinygetcurrentdatetime(true); // Print current time ESPLOGI(TAGTIME, "Current Time: %04d-%02d-%02d %02d:%02d:%02d.%06ld", currenttime.year, currenttime.month, currenttime.day, currenttime.hour, currenttime.minute, currenttime.second, currenttime.microsecond); } ```

使用效果:

Danger

SNTP同步到RTC中的精度为秒级别,因此在获取世界时间时,微秒部分可能并不准确,仅供参考。