/* * IEC 60870-5-101/104 共用时间源实现:读取当前日历、设置实际时钟。 * IEC60870_CLOCK_USE_RTC=0 使用本文件内的 Windows 读写,=1 调用 RTC 预留接口。 * 公共入口统一使用固定 UTC+8 标准时间,检查日历、读取质量并重算星期。 * Windows 分支负责 UTC 与 UTC+8 的转换,不使用计算机配置的本地时区或夏令时。 * RTC 分支直接收发约定的 UTC+8 日历;芯片初始化、寄存器读写由板级驱动负责。 */ #include "IEC60870_Clock.h" /* 编译时选择时间源;RTC 模式不编译下面的 Windows 实现,也不包含 windows.h。 */ #if !IEC60870_CLOCK_USE_RTC #ifndef _WIN32 #error "Windows clock requires _WIN32; define IEC60870_CLOCK_USE_RTC=1 for RTC" #else #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include /* * 函数功能:在日历数值上加减固定八小时,完成 UTC 与 UTC+8 标准时间的转换。 * 入口参数:source——源日历;to_local——非 0 表示 UTC 加八小时,0 表示 UTC+8 减八小时。 * 出口参数:result——转换后的日历,仅返回成功时可使用;两个指针均由内部调用方保证非空。 * 返回值:1 转换成功;0 Windows 日期转换失败,或加减偏移会使中间计数溢出。 * 说明:FILETIME 以 100 纳秒为单位,从 1601-01-01 起计数;此处只用它进行日期运算。 * 本地日历也按字段数值展开,平移后的中间计数不作为 UTC 时间戳对外返回。 * 加减后交给 Windows 还原年月日,自然处理跨日、跨月、跨年及闰日。 * 1970..2099 的业务日历范围由读写入口检查,本函数不额外限制年份。 */ static int Shift_Calendar(const SYSTEMTIME *source, int to_local, SYSTEMTIME *result) { /* 分钟数 × 每分钟秒数 × 每秒 100 纳秒单位数;先转为 64 位再乘,避免中间结果溢出。 */ const uint64_t offset_ticks = (uint64_t)IEC60870_LOCAL_UTC_OFFSET_MINUTES * UINT64_C(60) * UINT64_C(10000000); FILETIME encoded; uint64_t ticks; /* 只展开日历数值,不查询系统本地时区;转换失败时不使用 encoded。 */ if (!SystemTimeToFileTime(source, &encoded)) { return 0; } /* FILETIME 分为高、低两个 32 位字段,按位组合可避免指针强转带来的对齐和别名问题。 */ ticks = ((uint64_t)encoded.dwHighDateTime << 32) | encoded.dwLowDateTime; if (to_local != 0) { /* 先检查再加,防止无符号整数回绕为较小的时间值。 */ if (ticks > UINT64_MAX - offset_ticks) { return 0; } ticks += offset_ticks; } else { /* 先检查再减,防止越过计数原点后回绕为很大的时间值。 */ if (ticks < offset_ticks) { return 0; } ticks -= offset_ticks; } /* 拆回 FILETIME,由系统重新计算全部日历字段,毫秒随计数一起保留。 */ encoded.dwLowDateTime = (DWORD)(ticks & UINT64_C(0xFFFFFFFF)); encoded.dwHighDateTime = (DWORD)(ticks >> 32); return FileTimeToSystemTime(&encoded, result) != 0; } /* * 函数功能:读取 Windows 系统时间,输出固定 UTC+8 标准时间。 * 入口参数:value——接收结果的指针,不得为空。 * 出口参数:value——合法日历及 is_invalid=0;失败时不修改调用方结果。 * 返回值:1 取得合法结果;0 空指针、偏移转换失败或日历超出模块支持范围。 * 说明:一次 GetSystemTime 取得完整 UTC 日历及毫秒,避免分次取字段时遇到秒或日期进位。 * 固定加八小时,与 Windows 设置的本地时区无关;最终结果仍交由公共入口校验。 */ static int Windows_Read(IEC60870_ClockValue_t *value) { IEC60870_ClockValue_t result = {0}; SYSTEMTIME utc, local; if (value == NULL) { return 0; } GetSystemTime(&utc); if (!Shift_Calendar(&utc, 1, &local)) { return 0; } result.time.year = local.wYear; result.time.month = (uint8_t)local.wMonth; result.time.day = (uint8_t)local.wDay; result.time.hour = (uint8_t)local.wHour; result.time.minute = (uint8_t)local.wMinute; result.time.second = (uint8_t)local.wSecond; result.time.ms = local.wMilliseconds; result.time.weekday = (uint8_t)local.wDayOfWeek; /* 加八小时可能跨到下一年,必须检查转换后的日历是否仍在 1970..2099 内。 */ if (!IEC60870_Time_Is_Valid(&result.time)) { return 0; } /* Windows 读时接口不提供同步质量;0 仅表示未标记无效,不代表已校准或已同步。 */ result.is_invalid = 0U; *value = result; return 1; } /* * 函数功能:将固定 UTC+8 标准时间转换为 UTC,并实际修改 Windows 系统时钟。 * 入口参数:local_time——1970..2099 的合法 UTC+8 日历,原始输入不会被修改。 * 返回值:1 SetSystemTime 报告成功;0 参数非法、转换失败或系统调用失败。 * 说明:SetSystemTime 接收 UTC,因此必须先减八小时;它不使用 wDayOfWeek 字段。 * 调用进程需要系统设时权限,权限不足时返回 0;成功与否以系统调用结果为准。 */ static int Windows_Set(const IEC60870_WallTime_t *local_time) { SYSTEMTIME local = {0}, utc; if (!IEC60870_Time_Is_Valid(local_time)) { return 0; } local.wYear = local_time->year; local.wMonth = local_time->month; local.wDay = local_time->day; local.wHour = local_time->hour; local.wMinute = local_time->minute; local.wSecond = local_time->second; local.wMilliseconds = local_time->ms; if (!Shift_Calendar(&local, 0, &utc)) { return 0; } return SetSystemTime(&utc) != 0; } #endif /* _WIN32 */ #endif /* !IEC60870_CLOCK_USE_RTC */ /* * 函数功能:检查完整日历,再根据年月日重算星期,保证日期与星期一致。 * 入口参数:time——公共读写入口持有的局部日历副本。 * 出口参数:time->weekday——0=周日,1=周一,...,6=周六;其他字段保持原值。 * 返回值:1 日历合法且星期计算成功;0 空指针、日历非法或星期计算失败。 * 说明:传入的 weekday 不参与合法性判断;本函数不校准时钟,也不转换时区。 */ static int Normalize_Calendar(IEC60870_WallTime_t *time) { if (!IEC60870_Time_Is_Valid(time)) { return 0; } return IEC60870_Time_Get_Weekday(time->year, time->month, time->day, &time->weekday); } /* * 函数功能:从选定的 Windows 或 RTC 时间源读取日历,检查完成后再提交输出。 * 入口参数:value——接收结果的指针,不得为空。 * 出口参数:value——固定 UTC+8 日历、重算的星期及时间源提供的 is_invalid。 * 返回值:1 得到合法日历和 0/1 质量标志;0 参数非法、底层读取失败或结果非法。 * 说明:返回 1 只表示本次读取完成,is_invalid=1 的结果仍需业务按无效时间处理。 * 底层仅操作局部结果,失败前即使写入了部分字段,也不会修改调用方输出。 */ int IEC60870_Clock_Read(IEC60870_ClockValue_t *value) { IEC60870_ClockValue_t result = {0}; int status; if (value == NULL) { return 0; } /* 驱动未主动报告有效状态时,保留无效标志,不因零初始化而假定时钟有效。 */ result.is_invalid = 1U; #if IEC60870_CLOCK_USE_RTC /* RTC 直接提供 UTC+8 日历,公共层无需将其转成时间戳再还原。 */ status = IEC60870_Clock_RTC_Read(&result); #else status = Windows_Read(&result); #endif /* 底层必须明确返回 1;质量值只能为 0/1,日历通过检查后才覆盖输出。 */ if (status != 1 || result.is_invalid > 1U || !Normalize_Calendar(&result.time)) { return 0; } *value = result; return 1; } /* * 函数功能:检查固定 UTC+8 日历,将重算星期的副本交给选定的时间源设置。 * 入口参数:local_time——待设置的完整日历,不得为空;传入的 weekday 会被重新计算。 * 返回值:1 底层明确报告设置成功;0 参数非法或底层未报告成功。 * 说明:先复制并校验,检查失败时不调用底层,也不修改原始输入。 * 校时报文的 IV/SU、源时区及业务校时条件应由上层处理,本入口只接收日历。 * 返回 0 不保证硬件未发生部分写入,公共层不提供设备写入回滚。 */ int IEC60870_Clock_Set(const IEC60870_WallTime_t *local_time) { IEC60870_WallTime_t prepared; if (local_time == NULL) { return 0; } prepared = *local_time; if (!Normalize_Calendar(&prepared)) { return 0; } #if IEC60870_CLOCK_USE_RTC return IEC60870_Clock_RTC_Set(&prepared) == 1; #else return Windows_Set(&prepared) == 1; #endif }