时间¶
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 同步后使用):
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中的精度为秒级别,因此在获取世界时间时,微秒部分可能并不准确,仅供参考。