205 lines
8.8 KiB
C
205 lines
8.8 KiB
C
/*
|
|
* 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 <windows.h>
|
|
|
|
/*
|
|
* 函数功能:在日历数值上加减固定八小时,完成 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
|
|
}
|