Files
STM32F4-Base/IEC60870-Slave/common/IEC60870_Time.h
T
STF 4c55544f05
CI / L0 静态门禁 (pre-commit) (push) Failing after 45s
CI / L0.5 主机侧单测 (MSVC) (push) Failing after 42s
通信协议代码
2026-10-26 08:31:48 -04:00

245 lines
15 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 模块名称:IEC 60870-5-101/104 公共时间工具
* 模块功能:CP16/CP24/CP56编解码、公历日期/星期、本地标准时间及纪元秒数工具。
* 适用协议:101 和 104 的时间格式共用同一实现,独立于各自的帧格式和链路层。
* 职责边界:公共编解码保留 IV/SU,允许合法的 SU=0/1,不调整输入日历。
* 业务接收时通过 CP56_To_Local 检查 IV、处理 SU 和源时区,取得本地标准时间日历。
* 后续业务发送入口须保证日历已是本地标准时间,并统一设置 SU=0。
* 接入状态:IEC60870_Asdu 已复用 CP56 编解码;事件采集、时间源读取和实际校时仍由业务接入。
* 适用平台:可选平台库或纯整数日期转换;不读取系统时钟、不设置 RTC、不推断时区规则或处理链路定时器。
*/
#ifndef IEC60870_TIME_H
#define IEC60870_TIME_H
#include <stddef.h>
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
/* CP16为经过时间;CP24仅含分钟及分钟内毫秒,不能单独恢复完整日期。 */
#define IEC60870_CP16_LEN 2U
#define IEC60870_CP24_LEN 3U
#define IEC60870_CP56_LEN 7U
#define IEC60870_TIME_MIN_YEAR 1970U
#define IEC60870_TIME_MAX_YEAR 2099U
/* 两位年份按 2000 年起解释是本实现的世纪约定,须与通信对端一致。 */
#define IEC60870_CP56_MIN_YEAR 2000U
#define IEC60870_CP56_MAX_YEAR 2099U
/* 2099-12-31 23:59:59 UTC;本模块不接受 uint32_t 可表示但超出约定年份的秒数。 */
#define IEC60870_TIME_MAX_SECONDS UINT32_C(4102444799)
/*
* 本地标准时区固定为 UTC+8,不读取系统时区。
* 源标准时区也为 UTC+8 时,转换接口传入此值;即使报文 SU=1,源标准偏移仍为 480。
* 其他源时区由调用方按双方约定提供标准偏移,不能一律传入此值。
*/
#define IEC60870_LOCAL_UTC_OFFSET_MINUTES 480
/*
* 时间戳转日历的编译选项:1 使用平台库,0 使用本模块保留的纯整数算法。
* 当前平台库后端为 Windows CRT 的 _gmtime64_s;Windows 默认 1,其他平台默认 0。
* 无合适时间库时,在编译 IEC60870_Time.c 时定义 IEC60870_TIME_USE_PLATFORM=0。
* 两种实现均输出 UTC,支持相同的 1970..2099 年范围,不读取当前时间或本地时区。
*/
#ifndef IEC60870_TIME_USE_PLATFORM
#ifdef _WIN32
#define IEC60870_TIME_USE_PLATFORM 1
#else
#define IEC60870_TIME_USE_PLATFORM 0
#endif
#endif
typedef struct {
uint16_t milliseconds; /* CP16经过时间,0..65535毫秒,无IV或SU。 */
} IEC60870_CP16Time2a_t;
typedef struct {
uint16_t milliseconds; /* CP24分钟内毫秒,0..59999,包含秒。 */
uint8_t minute; /* 0..59;不含小时、日期或时区。 */
uint8_t is_invalid; /* IV,0/1;编码和解码保留。 */
} IEC60870_CP24Time2a_t;
/**
* 功能:编码CP16经过时间或CP24短时标。
* 输入参数:value——非空解析值;capacity——输出容量,分别至少2/3字节。
* 输出参数:out——低字节在前的协议字节,不改变输入时间基准。
* 返回值:1成功;0参数或字段非法,输出不变。
* 说明:CP24保留IV但检查分钟和毫秒范围,不推断小时或日期,不携带SU。
*/
int IEC60870_Time_To_CP16(const IEC60870_CP16Time2a_t *value, uint8_t *out, size_t capacity);
int IEC60870_Time_To_CP24(const IEC60870_CP24Time2a_t *value, uint8_t *out, size_t capacity);
/**
* 功能:解码CP16经过时间或CP24短时标。
* 输入参数:in——非空字节串;length——可读长度,分别至少2/3字节。
* 输出参数:value——成功后保存独立解析值,失败不变。
* 返回值:1成功;0参数、长度、CP24保留位或字段非法。
* 说明:只读取前2/3字节;CP24的IV=1仍可解析,但不表示时间有效。
*/
int IEC60870_Time_From_CP16(const uint8_t *in, size_t length, IEC60870_CP16Time2a_t *value);
int IEC60870_Time_From_CP24(const uint8_t *in, size_t length, IEC60870_CP24Time2a_t *value);
/*
* 公历时间值,不携带时区或 SU;Seconds_To_Local、CP56_To_Local 输出本地标准时间,
* From_Seconds 输出 UTC。
* 调用 To_Seconds 时须提供 UTC 日历,不能把本地标准时间日历直接当成 UTC 转秒数。
* weekday 为派生字段:0=周日,1=周一,...,6=周六。
* 编码及转秒数时忽略传入的 weekday;解码及从秒数转换时按日期重新计算。
* 此类型仅供内存中传值,不能直接作为 CP56Time2a 的报文字节布局。
*/
typedef struct {
uint16_t year; /* 普通日历为 1970..2099;CP56 编解码限于 2000..2099 */
uint8_t month; /* 1..12 */
uint8_t day; /* 1..当月实际天数 */
uint8_t hour; /* 0..23 */
uint8_t minute; /* 0..59 */
uint8_t second; /* 0..59,不表示闰秒 */
uint16_t ms; /* 0..999 */
uint8_t weekday; /* 0..6,输出时由日期推导 */
} IEC60870_WallTime_t;
/*
* CP56Time2a 的解析值;标志与普通日历值分开,避免丢失报文中的时间质量信息。
* is_invalid、is_summer_time 和 has_weekday 只能为 0 或 1。
* 编码由 has_weekday 决定是否携带星期,IV 和 SU 按输入保留。
* IV=1 仍可编解码,但日期字段仍须合法;成功不表示该时间可用于校时。
* 标准允许选用 SU 但不推荐使用;本项目后续业务发送入口负责统一设置 SU=0。
* 公共编解码支持 SU=0/1,不强制清零;发送前须确保日历与标志一致,不能只清 SU 而保留夏令时日历。
* SU 不携带时区信息;取本地标准时间时将完整解析值及源标准时区偏移交给 CP56_To_Local。
*/
typedef struct {
IEC60870_WallTime_t time; /* 2000..2099;按双方约定的时间基准及 SU 解释 */
uint8_t is_invalid; /* IV:1=时间无效,0=未置无效标志 */
uint8_t is_summer_time; /* SU:0=标准时间,1=夏令时;原始编解码保留,CP56 转换接口在为 1 时扣除一小时 */
uint8_t has_weekday; /* 1=报文带星期,0=报文星期域为 0(未使用) */
} IEC60870_CP56Time2a_t;
/*
* 所有接口:成功返回 1;参数/格式非法、结果越界、平台转换失败或将 IV=1 转为有效时间时返回 0。
* 返回 0 时不修改任何输出;调用方仍须保证指针有效且实际空间不少于所报长度。
* 模块不维护共享可变状态或动态内存。输入和输出允许重叠。
* 成功写回输出时,会覆盖与输出重叠的输入存储;需要保留原始输入时应使用独立输出存储。
*/
/**
* 功能:检查日历的年月日、时分秒及毫秒是否合法,不进行时间转换。
* 输入:time——待检查的日历值;年份1970..2099,毫秒0..999。
* 返回:1 数值合法;0 空指针、无效日期或字段越界。
* 说明:忽略派生字段weekday;不判断时区、时钟是否准确或是否允许校时。
*/
int IEC60870_Time_Is_Valid(const IEC60870_WallTime_t *time);
/**
* 功能:检查公历日期并计算星期。
* 输入:year——1970..2099;month、day——合法月份和日期。
* 输出:weekday——0=周日,1=周一,...,6=周六。
* 返回:1 成功;0 日期非法或输出为空,输出不变。
*/
int IEC60870_Time_Get_Weekday(uint16_t year, uint8_t month, uint8_t day, uint8_t *weekday);
/**
* 功能:将 UTC 日期和时间转换为自 1970-01-01 00:00:00 UTC 起的完整秒数。
* 输入:time——年份 1970..2099,毫秒 0..999,其余字段须组成合法时间。
* 输出:seconds——0..IEC60870_TIME_MAX_SECONDS;0 是有效纪元时间,不表示失败。
* 返回:1 成功;0 参数非法,输出不变。
* 说明:ms 参与范围校验,但不计入完整秒数;需保留亚秒精度时另行保存 time->ms。
* 忽略 weekday,不进行本地时区换算,也不计闰秒。
* time 没有时区或 SU,函数无法检查其是否真的采用 UTC,须由调用方保证。
* 对 CP56 解析值,取本地标准时间调用 CP56_To_Local;取纪元秒数调用 CP56_To_Seconds。
*/
int IEC60870_Time_To_Seconds(const IEC60870_WallTime_t *time, uint32_t *seconds);
/**
* 功能:将纪元秒数和毫秒余数转换为 UTC 日期和时间。
* 输入:seconds——0..IEC60870_TIME_MAX_SECONDS;ms——0..999。
* 输出:time——完整公历时间及按日期计算的 weekday。
* 返回:1 成功;0 参数非法或平台转换失败,输出不变;不会把越界毫秒截断成 999。
* 说明:由 IEC60870_TIME_USE_PLATFORM 选择平台库或纯整数算法;均不做本地时区换算。
*/
int IEC60870_Time_From_Seconds(uint32_t seconds, uint16_t ms, IEC60870_WallTime_t *time);
/**
* 功能:将 UTC 纪元秒数和毫秒余数直接转换为本地标准时间日历(固定 UTC+8)。
* 输入:utc_seconds——未加时区偏移的 UTC 纪元秒数;ms——0..999。
* 本地日期须在 1970..2099 年内,因此 utc_seconds 上限为
* IEC60870_TIME_MAX_SECONDS - IEC60870_LOCAL_UTC_OFFSET_MINUTES * 60。
* 输出:local_time——本地年月日、时分秒、毫秒及重新计算的星期。
* 返回:1 成功;0 参数非法、本地日期越界或平台转换失败,输出不变。
* 说明:内部统一处理时区偏移及跨日/月/年,调用方不再加八小时。
* 不读取系统时区或开启夏令时;已是固定 UTC+8 标准时间的 RTC 日历可直接用于 CP56 编码。
*/
int IEC60870_Time_Seconds_To_Local(uint32_t utc_seconds, uint16_t ms,
IEC60870_WallTime_t *local_time);
/**
* 功能:将 CP56Time2a 解析值转换为 UTC 纪元完整秒数,SU=1 时扣除一小时。
* 输入:value——合法的 2000..2099 年日期时间,IV=0,SU/has_weekday 为 0/1;
* standard_utc_offset_minutes——源报文标准当地时间减 UTC 的分钟数,范围 -1439..1439,
* 向东为正、向西为负;UTC 填 0,UTC+8 填 480,UTC-5 填 -300。
* 此偏移必须不含夏令时的一小时;SU 会单独扣除,不能重复计算。
* 输出:seconds——0..IEC60870_TIME_MAX_SECONDS 的 UTC 秒数。
* 返回:1 成功;0 参数非法、IV=1 或换算结果越界,输出不变。
* 说明:UTC = 报文日历对应秒数 - SU * 3600 - 标准时区偏移 * 60。
* 在秒数上运算,支持跨日/月/年;年份 2000 的输入可换算为 1999 年的 UTC。
* ms 校验后不计入完整秒数;未与输出重叠时,输入日期、SU 和毫秒均保持不变。
* 忽略传入的 weekday;不根据日期猜测夏令时,不访问系统时区或设置 RTC。
*/
int IEC60870_Time_CP56_To_Seconds(const IEC60870_CP56Time2a_t *value,
int16_t standard_utc_offset_minutes, uint32_t *seconds);
/**
* 功能:将 CP56Time2a 解析值转换为本地标准时间日历(固定 UTC+8),并处理 SU。
* 输入:value——合法的 2000..2099 年日期时间,IV=0,SU/has_weekday 为 0/1;
* source_standard_utc_offset_minutes——源报文标准当地时间减 UTC 的分钟数,
* 范围 -1439..1439,不含夏令时;双方约定本地标准时间时传
* IEC60870_LOCAL_UTC_OFFSET_MINUTES(480),源报文为 UTC 时传 0。
* 输出:local_time——1970..2099 年的本地标准时间年月日、时分秒、毫秒及重新计算的星期。
* 返回:1 成功;0 参数非法、IV=1、本地标准时间越界或平台转换失败,输出不变。
* 说明:本地标准时间 = 报文日历 - SU * 一小时 - 源标准时区偏移 + 八小时。
* 源标准时区为 UTC+8 时,SU=0 保持原时间,SU=1 减一小时,自动处理跨日/月/年。
* 毫秒原样保留;日历结果通过 local_time 输出,函数返回值仅表示成功或失败。
* 输入与输出未重叠时,不改写输入日历或 SU。
* 结果已是本地标准时间,调用方无需再次加八小时或减夏令时的一小时。
* 业务再次发送此日历时应构造新的 CP56 值并置 SU=0,不能沿用原报文的 SU=1。
*/
int IEC60870_Time_CP56_To_Local(const IEC60870_CP56Time2a_t *value,
int16_t source_standard_utc_offset_minutes,
IEC60870_WallTime_t *local_time);
/**
* 功能:将 CP56Time2a 解析值编码为 7 字节报文,毫秒域按低字节在前写入。
* 输入:value——合法的 2000..2099 年日期时间,三个标志字段均为 0/1;
* capacity——out 可写字节数,至少 IEC60870_CP56_LEN。
* 输出:out——仅写前 7 字节,保留位清零,IV 和 SU 按输入保留。
* 返回:1 成功;0 参数非法,整个输出缓冲区不变。
* 说明:has_weekday=1 时由日期计算报文星期(1=周一..7=周日),否则填 0。
* 不校时、不转换时区,也不因 SU=1 而调整小时值。
* SU=0/1 均可编码,本接口不会自动清零或因 SU=1 而拒绝合法输入。
* 后续业务发送入口负责传入已是本地标准时间的日历并设置 SU=0;此处只执行公共编码。
* IV=1 不放宽日期、时间及标志取值的检查。
*/
int IEC60870_Time_To_CP56(const IEC60870_CP56Time2a_t *value, uint8_t *out, size_t capacity);
/**
* 功能:检查并解析前 7 字节 CP56Time2a,后续字节留给调用方处理。
* 输入:in——报文起点;length——可读字节数,至少 IEC60870_CP56_LEN。
* 输出:value——源报文的日历时间、IV、SU 及报文是否携带星期,尚未转换为本地标准时间;
* weekday 始终由日期计算。
* 返回:1 成功;0 参数、保留位、日期时间非法,输出不变。
* 说明:年份域只接受 0..99,解释为 2000..2099;星期域 0 表示未使用,
* 非零星期必须与日期一致。分钟 bit6 等保留位必须为 0,不接收扩展含义。
* SU=1 本身不导致失败,IV/SU 均原样返回;不转换时区或夏令时。
* IV=1 且其余字段合法时仍可成功解析,供业务记录;两个 CP56 转换接口会拒绝该无效时间。
* 解码后取本地标准时间调用 CP56_To_Local;需要纪元秒数时调用 CP56_To_Seconds。
* 转换前须保留完整的日历及 SU,不可先清 SU,也不可先减一小时再携带 SU=1 转换。
* 业务仍须检查时间约定及校时权限;不自动设置 RTC。
*/
int IEC60870_Time_From_CP56(const uint8_t *in, size_t length, IEC60870_CP56Time2a_t *value);
#ifdef __cplusplus
}
#endif
#endif /* IEC60870_TIME_H */