通信协议代码
This commit is contained in:
@@ -0,0 +1,301 @@
|
||||
# IEC 60870-5-101/104 公共时间工具
|
||||
|
||||
`IEC60870_Time.c/h` 为 IEC 60870-5-101 和 IEC 60870-5-104 提供共用的公历计算、CP16/CP24/CP56Time2a 编解码、本地标准时间转换及纪元秒数工具。时间源模块集中在`IEC60870_Clock.c/h`:Clock.c包含公共校验和Windows读写,Clock.h包含业务接口及RTC预留声明。时间源限定为Windows系统时钟或RTC,由`IEC60870_CLOCK_USE_RTC`在编译时二选一;RTC具体芯片驱动暂不实现,也不规划其他操作系统适配。时间相关的 ASDU 业务入口尚未接入;下文约定后续业务使用本地标准时间,并给出调用示例。原始解码保留源日历和标志,业务再调用转换接口取得本地标准时间日历。公共模块不维护连接状态、共享可变状态或动态内存,可独立于协议链路编译和链接;时间戳转日期可选择平台库或保留的纯整数算法。
|
||||
|
||||
## 101/104 的共用边界
|
||||
|
||||
101 和 104 使用 CP56Time2a 时,采用相同的七字节时间字段。串口帧和 TCP/APCI 的差别由各自协议层处理,不影响这里的时间编解码。
|
||||
|
||||
两个协议的业务层接入时均可包含 `IEC60870_Time.h` 并调用同一组 `IEC60870_Time_*` 接口,不需要分别维护时间实现。当前模块覆盖日历工具及 CP16/CP24/CP56Time2a。CP16为2字节经过时间,范围0..65535毫秒;CP24为3字节短时标,保存分钟、分钟内毫秒和IV,不含SU、小时或日期。两者分别使用`To_CP16/From_CP16`和`To_CP24/From_CP24`,成功返回1,失败返回0且保留输出。CP24解析检查bit6保留位、分钟0..59及分钟内毫秒0..59999,不自动恢复完整日期。不同时间类型具有不同字段,不能把CP24交给CP56解码。
|
||||
|
||||
日历支持 1970..2099、将 CP56 两位年份解释为 2000..2099、非零星期一致性及保留位严格检查,属于本模块明确采用的范围和接入策略。通信双方须就世纪和时区约定保持一致,不能把这些实现约定理解为 101 或 104 独有的规则。
|
||||
|
||||
## 本地标准时间与 SU 处理
|
||||
|
||||
IEC 60870-5-101:2003/AMD1:2015 第7.2.6.18条将SU规定为可选且不推荐使用的功能;如果选用,SU=1与时间值早一小时且SU=0的标签表示同一时刻。第7.4.6条要求不使用夏令时时将SU置0。参见[标准修订单第4至6页](https://preview.sist.si/sist-preview/22451/3a4cae004abe434883eba6133f897d87/IEC-60870-5-101-2003-AMD1-2015.pdf)。
|
||||
|
||||
**后续业务发送入口负责保证:使用本地标准时间(固定UTC+8),并显式设置`is_summer_time=0U`。** 公共编解码继续支持SU=0和1,按输入保留合法的SU值;公共编码接口不强制SU=0,也不会自动清除SU=1。该发送约束由后续业务入口落实,目前文档中的发送函数仅为调用示例。
|
||||
|
||||
业务发送时,输入日历须已经是本地标准时间。若来源是夏令时日历,应先完成时间转换,再以SU=0编码,不能只把SU清零而保留夏令时的小时值。接收时保留SU=1的兼容处理:原始解码后调用`IEC60870_Time_CP56_To_Local`,取得已处理SU的本地标准时间日历;接收兼容不改变本地发送SU=0的约定。
|
||||
|
||||
| 操作 | 行为 |
|
||||
|---|---|
|
||||
| 公共编码:`is_summer_time=0`或`1` | 校验其余字段后编码,SU按输入写入,不调整小时值 |
|
||||
| 公共编码:`is_summer_time>1` | 返回0,输出缓冲区不变;标志只能取0或1 |
|
||||
| 公共解码:报文SU=0或1 | 校验其余字段后解码,在`is_summer_time`中保留SU,不调整小时值 |
|
||||
| 转本地标准时间:源标准时区为UTC+8、SU=0 | 保留原年月日和时间,毫秒保留,星期按日期重算 |
|
||||
| 转本地标准时间:源标准时区为UTC+8、SU=1 | 减一小时,自动处理跨日/月/年,输出本地标准时间 |
|
||||
| 转本地标准时间:源标准时区不同 | 根据源偏移和SU换算,最终输出固定UTC+8的日历 |
|
||||
| 转本地标准时间:IV=1或结果越界 | 返回0,输出不变,不能把无效时间用于校时 |
|
||||
| 业务发送示例 | 调用方提供已是本地标准时间的日历,示例显式设置SU=0,IV由零初始化保持为0 |
|
||||
| 业务接收示例 | `From_CP56 → CP56_To_Local`,直接取得本地标准时间日历 |
|
||||
|
||||
`From_CP56`只保留源日历及SU;`CP56_To_Local`读取完整解析值,输出已经修正为本地标准时间的`IEC60870_WallTime_t`。**调用方不需要再加八小时或再减夏令时的一小时。** 使用独立输出存储时,原始输入不改写;若输入与输出重叠,成功写回会覆盖重叠的存储,调用方须按转换后的时间基准使用结果,再次编码时构造新的CP56值并置SU=0。目前仓库尚未提供实际的校时业务处理器,下文给出本地标准时间收发的调用方式。
|
||||
|
||||
转换接口的`source_standard_utc_offset_minutes`表示“源报文标准当地时间减UTC”的分钟数,向东为正、向西为负。双方约定本地标准时间时传`IEC60870_LOCAL_UTC_OFFSET_MINUTES`(480);源报文为UTC时传0,源标准时区为UTC-5时传-300。-1439..1439是本接口约定的参数范围;输出时区始终是本地标准时间。
|
||||
|
||||
源偏移参数**不能包含夏令时的一小时**,SU会单独处理。例如源标准时区为UTC-5、报文SU=1时仍传-300,不传夏令时的-240。计算关系为:
|
||||
|
||||
```text
|
||||
本地标准时间 = 报文日历 - SU × 一小时 - 源标准时区偏移 + 八小时
|
||||
源标准时区为UTC+8时:本地标准时间 = 报文日历 - SU × 一小时
|
||||
```
|
||||
|
||||
| 报文日历 | SU | 源标准时区偏移 | 输出的本地标准时间 |
|
||||
|---|---|---|---|
|
||||
| 2026-07-01 12:00 | 0 | 480(本地标准时间基准) | 2026-07-01 12:00 |
|
||||
| 2026-07-01 12:00 | 1 | 480(本地标准时间基准) | 2026-07-01 11:00 |
|
||||
| 2026-01-01 00:30 | 1 | 480(本地标准时间基准) | 2025-12-31 23:30 |
|
||||
| 2026-07-01 04:00 | 0 | 0(UTC基准) | 2026-07-01 12:00 |
|
||||
|
||||
转换内部先用有符号64位中间值处理SU和源偏移,再展开本地标准时间日历,检查输出范围,不对无符号小时直接减一。输出包含毫秒和本地标准时间日期对应的星期。内部平移八小时用于日历展开的数值不作为Unix时间戳暴露;公共`To_Seconds`、`From_Seconds`和`CP56_To_Seconds`仍是明确的UTC纪元秒数工具,不改变它们的既有含义,也不能把本地标准时间日历直接传给只接受UTC的`To_Seconds`。本地标准时间业务直接使用`CP56_To_Local`的日历结果。
|
||||
|
||||
SU本身不携带时区信息;源偏移须来自双方约定。这里的“本地”指本模块约定的固定UTC+8,不读取操作系统时区,也不根据历史年份自动开启夏令时;接收SU=1时按报文标志归一化为本地标准时间。
|
||||
|
||||
时间源提供UTC纪元秒数时,业务直接调用`IEC60870_Time_Seconds_To_Local(utc_seconds, ms, &local_time)`,一次取得本地年月日、时分秒、毫秒和星期。内部先检查范围、加上固定时区偏移,再展开一次日历;调用方不先生成UTC日历,也不手动加八小时。`CP56_To_Local`在解析源时间基准及SU后复用同一个入口。RTC若直接提供已是本地标准时间的日历,发送时可直接构造CP56解析值并编码。
|
||||
|
||||
## 职责与调用关系
|
||||
|
||||
| 内容 | 负责模块 |
|
||||
|---|---|
|
||||
| 日期合法性、闰年、星期、UTC 日期与纪元秒数互换 | `IEC60870_Time` |
|
||||
| UTC纪元秒数直接转本地标准时间,统一处理时区及日期进位 | `IEC60870_Time_Seconds_To_Local` |
|
||||
| CP56Time2a 七字节布局、日期和星期检查、IV/SU标志保留 | `IEC60870_Time` |
|
||||
| CP56转本地标准时间,检查IV、处理SU和源时区,输出完整日历 | `IEC60870_Time_CP56_To_Local` |
|
||||
| 统一读时、设时入口,校验日历、重算星期并检查时间质量标志 | `IEC60870_Clock` |
|
||||
| Windows系统时钟读写,UTC与固定UTC+8日历互换 | `IEC60870_Clock.c`中的Windows分支 |
|
||||
| 发送前保证日历已是本地标准时间,并统一设置SU=0 | ASDU 业务层,后续业务发送入口落实 |
|
||||
| 事件时间戳、校时 ASDU 的业务处理,确定时间约定并调用转换接口 | ASDU 业务层 |
|
||||
| 实现`IEC60870_Clock_RTC_Read/Set`,处理RTC寄存器、BCD、时间基准、掉电标志及读写一致性 | 板级接入层和具体RTC驱动,暂不实现 |
|
||||
| t1/t2/t3 及发送期限使用的单调计时 | 平台提供 `now_ms`,Link/Connection 使用 |
|
||||
|
||||
使用已有时间值进行转换或编解码时,业务层包含`IEC60870_Time.h`,构建时加入`IEC60870_Time.c`。读取或设置实际时钟时,包含`IEC60870_Clock.h`,再加入`IEC60870_Clock.c`。默认Windows模式链接Kernel32(通常由Windows工具链默认链接);以后使用RTC时,编译Clock.c定义`IEC60870_CLOCK_USE_RTC=1`,加入实现RTC预留接口的板级驱动。仅调用时间转换工具时,无需构建Clock。
|
||||
|
||||
模块不依赖 Codec、Receiver、Link 或 Connection,也不会自动注册 ASDU 处理或设置系统时间。墙上时间用于业务时间戳;链路超时使用单调时间,校时不能改变链路期限。
|
||||
|
||||
## 当前接入状态与待补齐项
|
||||
|
||||
当前已实现日期转换、星期计算、CP56Time2a编解码、统一时间源接口及Windows系统时钟适配。完整的协议时间业务还需补齐以下接入:
|
||||
|
||||
| 项目 | 当前状态与待补齐内容 |
|
||||
|---|---|
|
||||
| 协议时间源读取及设置 | 已提供直接调用的`Clock_Read/Clock_Set`及Windows读写实现;时间源限定为Windows或RTC,RTC目前仅预留接口。日志读取仍使用原有入口 |
|
||||
| RTC数据适配 | 接入函数声明已提供,具体芯片驱动暂不实现;后续接入时再明确RTC采用UTC还是本地标准时间、返回整数还是BCD、年份表示和毫秒来源,并按芯片要求取得一致的日期、时分秒及亚秒值,整理为固定UTC+8的公共日历 |
|
||||
| 时间有效性及失败处理 | `Clock_Read`分别返回操作状态和`is_invalid`,校验日期及0/1标志,失败保留输出;具体驱动负责识别未初始化、掉电或失去可信时间。下文示例传递质量到CP56的IV;实际业务仍须决定是否发送无效时间。IV=1也不能用全零日期占位 |
|
||||
| ASDU业务收发与校时 | `IEC60870_InformationObjects.c`的带时标类型复用基础对象值处理,并调用CP24/CP56编解码;保护事件和延时命令使用CP16。含103校时命令,保留IV/SU。`IEC60870_Asdu.c`维护容器与原文应答。事件采集、时间源读取、校时权限、平台设时及成功/失败应答的业务决策仍需应用接入 |
|
||||
| 日期转换实现 | 保留Windows CRT和纯整数算法两种实现。RTC直接提供日历时无需时间戳展开;需要转换时间戳且没有Windows CRT时使用整数算法,不规划增加其他操作系统时间库 |
|
||||
| 项目内的时间模块回归 | `iec60870_asdu_test.c`已覆盖CP56已知字节、IV/SU保留、非法日期和保留位,并分别使用两种时间实现编译运行;这不等于完整日期转换一致性测试。独立转换边界及平台读写失败、时间质量用例仍待补齐 |
|
||||
|
||||
RTC已有年月日时分秒时,可按约定直接整理后编码;只有需要从时间戳取得日期时,才涉及平台库或整数算法的选择。RTC未提供亚秒值时,可约定毫秒填0,但这不表示时间源具有毫秒精度。已提供的Windows适配从UTC统一转换为固定UTC+8,避免依赖电脑设置的本地时区。
|
||||
|
||||
## 统一时间源读写
|
||||
|
||||
业务直接调用`IEC60870_Clock_Read(&value)`或`IEC60870_Clock_Set(&local_time)`。Clock.c通过编译选项选择本文件内的Windows读写函数或外部的`IEC60870_Clock_RTC_Read/Set`,并负责读取后的结果检查和写入前的日历检查。一个程序选其中一个时间源,无需时钟对象、函数指针或注册初始化。RTC目前仅预留接口,以后接入时,其硬件及总线仍须由应用按驱动要求初始化。不同连接共用选定的时间源,调用方负责设备读写串行化。
|
||||
|
||||
| 接口 | 行为 |
|
||||
|---|---|
|
||||
| `IEC60870_Clock_Read(value)` | 平台实现读取完整日历和`is_invalid`,公共层校验1970..2099年的日期、时分秒、毫秒和质量标志,重算星期,成功后才提交输出 |
|
||||
| `IEC60870_Clock_Set(local_time)` | 校验固定UTC+8日历,重算副本中的星期,再交给平台实现设置设备;不改写调用方输入 |
|
||||
|
||||
Windows读写已经包含在Clock.c内。RTC模式需要板级驱动实现Clock.h声明的`IEC60870_Clock_RTC_Read`和`IEC60870_Clock_RTC_Set`;本模块只声明接口,不提供芯片驱动或空实现,缺少实现时链接会报未定义符号。Windows模式不引用这两个RTC符号。RTC函数同步执行,成功返回1、失败返回0,不得保留公共层传入的局部变量指针,也不能反向调用公共读写入口。只读RTC也须实现Set函数,直接返回0。
|
||||
|
||||
参数非法或底层返回失败时,公共接口返回0。`Clock_Read`返回1表示取得格式合法的结果,仍须查看`value.is_invalid`:1表示源报告时间无效,0只表示未置无效标志,不证明已经同步或校准。读取前局部结果的标志初始化为1,驱动须按实际状态明确填写;无可用日历时应返回0。读取失败不会修改调用方输出,设置失败则不承诺硬件回滚,底层可能已经完成部分寄存器写入。
|
||||
|
||||
公共接口始终使用**固定UTC+8标准时间日历**。RTC本来就保存这种日历时,平台读时函数直接填年月日、时分秒和毫秒,设时函数直接写入这些字段,不需要先转时间戳再还原。BCD解码、两位年份展开、24小时制转换、读跨秒一致性和掉电标志由芯片驱动处理;星期由公共层重算。若RTC保存UTC或其他时间基准,适配层先完成约定的转换,不能把原日历直接声明为UTC+8。
|
||||
|
||||
Windows适配调用`GetSystemTime`一次取得UTC日期、时间及毫秒,通过`SystemTimeToFileTime`/`FileTimeToSystemTime`的64位计数加八小时,统一处理跨日、跨月和跨年;设时反向减八小时后调用`SetSystemTime`。计数只用于内部日历运算,不依赖`IEC60870_TIME_USE_PLATFORM`,也不读取Windows的本地时区或夏令时配置。该适配读取时报告`is_invalid=0`,不检测NTP等同步状态;设置系统时间需要相应权限,系统调用失败时返回0。API含义见[GetSystemTime](https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-getsystemtime)、[SetSystemTime](https://learn.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-setsystemtime)及[FILETIME转换](https://learn.microsoft.com/en-us/windows/win32/api/timezoneapi/nf-timezoneapi-systemtimetofiletime)。
|
||||
|
||||
两种模式都编译`IEC60870_Time.c`和`IEC60870_Clock.c`,通过`IEC60870_CLOCK_USE_RTC`选择时间源:
|
||||
|
||||
| 编译选项 | 时间源及当前状态 | 额外依赖 |
|
||||
|---|---|---|
|
||||
| `0`(默认) | Windows系统时钟,已实现 | Windows的Kernel32接口 |
|
||||
| `1` | RTC,仅预留接口,暂不实现芯片驱动 | 板级驱动提供`IEC60870_Clock_RTC_Read/Set` |
|
||||
|
||||
该选项只接受0或1,必须作用于`IEC60870_Clock.c`的编译;仅在调用方源文件中定义不会改变已编译的Clock。GCC使用`-DIEC60870_CLOCK_USE_RTC=1`,MSVC使用`/DIEC60870_CLOCK_USE_RTC=1`选择RTC。非Windows目标须显式选择RTC;选0会在编译Clock.c时报告不支持Windows时间源。RTC分支不包含Windows头文件,也不调用Windows时钟API。
|
||||
|
||||
`IEC60870_CLOCK_USE_RTC`决定从哪里读取或设置当前时间;`IEC60870_TIME_USE_PLATFORM`决定如何转换已有时间戳,两者分别生效。RTC已有日历时直接填入,不需要先转换成时间戳。
|
||||
|
||||
Windows按默认配置构建后,业务即可直接读取:
|
||||
|
||||
```c
|
||||
#include "IEC60870_Clock.h"
|
||||
|
||||
/* 业务函数内的调用片段。 */
|
||||
IEC60870_ClockValue_t now;
|
||||
if (!IEC60870_Clock_Read(&now)) {
|
||||
return 0;
|
||||
}
|
||||
/* 读取成功后,业务根据now.is_invalid决定是否使用该时间。 */
|
||||
```
|
||||
|
||||
以后接RTC时,由板级驱动实现以下两个预留函数,业务调用方式相同。下例中的板级函数名仅示意,项目尚未实现某一型号芯片的驱动;板级函数须提供固定UTC+8日历、实际质量状态和1/0返回值:
|
||||
|
||||
```c
|
||||
#include "IEC60870_Clock.h"
|
||||
|
||||
int Board_RTC_Read(IEC60870_ClockValue_t *value);
|
||||
int Board_RTC_Set(const IEC60870_WallTime_t *local_time);
|
||||
|
||||
int IEC60870_Clock_RTC_Read(IEC60870_ClockValue_t *value)
|
||||
{
|
||||
return Board_RTC_Read(value);
|
||||
}
|
||||
|
||||
int IEC60870_Clock_RTC_Set(const IEC60870_WallTime_t *local_time)
|
||||
{
|
||||
return Board_RTC_Set(local_time);
|
||||
}
|
||||
```
|
||||
|
||||
读取当前时间并编码CP56时可使用以下业务示例。这里选择把源质量传给IV;实际业务也可在`is_invalid=1`时拒绝发送。日历仍须满足CP56的2000..2099年范围,SU固定为0:
|
||||
|
||||
```c
|
||||
int Encode_Current_Timestamp(uint8_t *out, size_t capacity)
|
||||
{
|
||||
IEC60870_ClockValue_t now;
|
||||
IEC60870_CP56Time2a_t timestamp = {0};
|
||||
|
||||
if (!IEC60870_Clock_Read(&now)) {
|
||||
return 0;
|
||||
}
|
||||
timestamp.time = now.time;
|
||||
timestamp.is_invalid = now.is_invalid;
|
||||
timestamp.is_summer_time = 0U;
|
||||
timestamp.has_weekday = 1U;
|
||||
return IEC60870_Time_To_CP56(×tamp, out, capacity);
|
||||
}
|
||||
```
|
||||
|
||||
校时报文的调用顺序为`From_CP56 → CP56_To_Local → Clock_Set`。转换先检查IV并处理SU和源标准时区,业务再检查校时权限及时间合理性,最后才调用`Clock_Set`。例如已有解析值`timestamp`,且业务已经确认允许校时:
|
||||
|
||||
```c
|
||||
IEC60870_WallTime_t local_time;
|
||||
|
||||
if (!IEC60870_Time_CP56_To_Local(×tamp,
|
||||
IEC60870_LOCAL_UTC_OFFSET_MINUTES,
|
||||
&local_time)) {
|
||||
return 0;
|
||||
}
|
||||
/* local_time已是固定UTC+8标准时间,不再加八小时或减一小时。 */
|
||||
return IEC60870_Clock_Set(&local_time);
|
||||
```
|
||||
|
||||
该例假定源标准时区也是UTC+8;其他源时区应传双方约定的偏移。`Clock_Set`只接收日历,不能自行判断报文IV、SU或校时权限;不能跳过转换直接把解码的源日历交给设时接口。以上函数和片段说明接入顺序,尚未注册为ASDU业务处理器,实际成功/失败的业务应答仍由ASDU层负责。
|
||||
|
||||
## 时间戳转日期的实现选择
|
||||
|
||||
业务层统一调用`IEC60870_Time_From_Seconds`,编译`IEC60870_Time.c`时通过`IEC60870_TIME_USE_PLATFORM`选择实现:
|
||||
|
||||
| 配置 | 实现与依赖 |
|
||||
|---|---|
|
||||
| `1`,Windows默认 | 使用Windows CRT的`_gmtime64_s`,将时间戳转换为UTC日历 |
|
||||
| `0`,其他平台默认,也可在Windows使用 | 使用原有整数算法计算完整天数、年月日及日内时分秒,不包含`<time.h>`,不调用平台时间函数 |
|
||||
|
||||
这是一项**编译配置**,不会在运行时检测或加载时间库。当前平台库后端只接入Windows;嵌入式等没有合适时间库的环境使用`0`。GCC可添加`-DIEC60870_TIME_USE_PLATFORM=0`,MSVC可添加`/DIEC60870_TIME_USE_PLATFORM=0`。选项必须作用于`IEC60870_Time.c`的编译,仅在调用方源文件中定义不会改变已编译的模块。
|
||||
|
||||
Windows显式使用64位版本,避免32位`time_t`限制原有的1970..2099年范围;该接口的UTC语义、参数和范围见[Microsoft文档](https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/gmtime-s-gmtime32-s-gmtime64-s)。两条路径共用参数检查、毫秒赋值和最终输出提交,失败不改输出;平台函数失败时返回0。整数算法保留在源码中,选择平台库时不编译该分支。
|
||||
|
||||
两种实现都将**传入的时间戳**转换为UTC日历,不获取当前时间。`Seconds_To_Local`、`CP56_To_Local`继续复用同一转换入口,统一输出固定UTC+8的本地标准时间,不受Windows系统时区配置影响。日期转秒数、星期计算和CP56编解码仍使用模块内的整数运算。
|
||||
|
||||
`IEC60870_TIME_USE_PLATFORM`只选择时间戳转日期的实现,与是否使用RTC作为时间源无关。RTC驱动若提供UTC纪元秒数,按业务需要调用`From_Seconds`或`Seconds_To_Local`;若已提供符合业务时间基准的年月日时分秒,可整理为`IEC60870_WallTime_t`并构造CP56解析值进行编码。读取RTC、确认其时间基准及将驱动返回值整理为上述格式,由平台接入层和业务层完成。
|
||||
|
||||
## 类型、范围与返回值
|
||||
|
||||
`IEC60870_WallTime_t` 是公开的日历值类型,包含年月日、时分秒、毫秒和派生的星期,不包含时区字段。`Seconds_To_Local`和`CP56_To_Local`输出本地标准时间;`From_Seconds`输出UTC。CP56Time2a中的源日历值须结合SU及双方约定的时间基准解释,不能仅凭结构体类型猜测时区。
|
||||
|
||||
`IEC60870_CP56Time2a_t` 另存 `is_invalid`(IV)、`is_summer_time`(SU)及 `has_weekday`,三个标志都只能是0或1。编解码保留IV/SU,由业务决定是否使用该时间。结构体只用于内存传值,不能按 `sizeof` 复制为报文,也不能把七字节报文强转成结构体指针。
|
||||
|
||||
| 项目 | 约定 |
|
||||
|---|---|
|
||||
| 普通日历范围 | 1970-01-01 至 2099-12-31,按公历检查每月实际天数 |
|
||||
| 纪元秒数范围 | 0..4102444799,对应 UTC 1970-01-01 00:00:00 至 2099-12-31 23:59:59 |
|
||||
| CP56 年份 | 报文只接受 0..99,解释为 2000..2099;不自动猜测世纪 |
|
||||
| 时、分、秒、毫秒 | 0..23、0..59、0..59、0..999;不表示闰秒 |
|
||||
| 内存中的星期 | 0=周日,1=周一,…,6=周六;输入的 `weekday` 不作为编码或秒数转换的依据 |
|
||||
| 返回值 | 1=成功,0=参数/格式非法、结果越界、平台转换失败或将IV=1转为有效时间;失败不改输出 |
|
||||
| 缓冲区 | 编解码检查非空及长度至少为7,仅处理前7字节;实际空间必须与所报长度一致 |
|
||||
|
||||
本地标准时间接口保留毫秒并重算星期。CP56日历输入须在2000..2099年,SU或源时区修正后得到1999年的本地标准时间可以成功;本地标准时间超过2099-12-31 23:59:59则失败。例如源本地标准时间的2099年最后一秒仍可完整输出,源UTC的2099-12-31 16:00则会因转换到本地标准时间2100年而失败。
|
||||
|
||||
`Seconds_To_Local`接受的UTC秒数为0..4102415999:0输出1970-01-01 08:00:00,4102415999输出2099-12-31 23:59:59;再增加一秒会超出本地日历上限,返回0且输出不变。参数必须是原始UTC纪元秒数,不能预先加八小时。
|
||||
|
||||
纪元秒数0是正常结果,不用来表示错误。基础`To_Seconds`和`CP56_To_Seconds`只输出完整秒数;`From_Seconds`和`Seconds_To_Local`同时接收秒数和毫秒余数。所有入口均检查毫秒范围,越界直接报错,不截断、不进位。
|
||||
|
||||
## CP56Time2a 字段策略
|
||||
|
||||
七字节布局采用[Beckhoff 的 CP56Time2a 字段说明](https://infosys.beckhoff.com/content/1033/tf6500_tc3_iec60870_5_10x/983926923.html):
|
||||
|
||||
| 字节(从0开始) | 内容 |
|
||||
|---|---|
|
||||
| 0、1 | 分钟内毫秒,低字节在前,范围0..59999,拆成秒和毫秒余数 |
|
||||
| 2 | bit7为IV,bit6保留,bit0..5为分钟 |
|
||||
| 3 | bit7为SU,bit5..6保留,bit0..4为小时 |
|
||||
| 4 | bit5..7为星期,bit0..4为日 |
|
||||
| 5 | bit4..7保留,bit0..3为月 |
|
||||
| 6 | bit7保留,bit0..6为两位年份;数值100..127不接受 |
|
||||
|
||||
- **星期**:`has_weekday=0` 编码为报文星期0;为1时从日期计算1=周一至7=周日。解码允许星期0,此时仍计算内存中的 `time.weekday`;非零星期必须与日期一致,否则失败。这样不依赖调用方传入的过期星期,也不会默默接受相互矛盾的日期和星期。
|
||||
- **IV**:编解码保留该标志。IV=1表示业务时间无效,不等于报文格式无法解析;调用方必须检查该标志后再决定是否记录或使用时间。即使IV=1,本模块也要求日期和时间字段合法,不接受全零日期等占位数据。
|
||||
- **SU**:公共编解码支持0和1,复制标志而不调整日历值;本地标准时间转换接口在SU=1时扣除一小时,并处理源标准时区与本地标准时间的差值。编码或转换时SU大于1均失败。后续业务发送入口负责设置SU=0;接收时将完整解析值交给`CP56_To_Local`,不丢弃SU后直接使用原时间。
|
||||
- **保留位**:编码统一清零,解码严格拒绝非零值。分钟bit6按本模块采用的布局视为保留位,不接受将它另作扩展标志的报文。这是明确的接入约束,不能把此接口当作任意七字节数据的无损容器。
|
||||
|
||||
数值校验成功只说明日期真实存在、各字段符合上述约定。它不保证远端时钟准确,也不决定校时权限或业务接受策略。
|
||||
|
||||
## 接口与示例
|
||||
|
||||
| 接口 | 功能 |
|
||||
|---|---|
|
||||
| `IEC60870_Time_Is_Valid` | 检查1970..2099年的年月日、时分秒及毫秒;忽略派生星期,不转换时间或判断时钟质量 |
|
||||
| `IEC60870_Time_Get_Weekday` | 校验日期并返回星期 |
|
||||
| `IEC60870_Time_To_Seconds` | UTC日历转完整纪元秒数,成功状态和数值分开返回 |
|
||||
| `IEC60870_Time_From_Seconds` | 纪元秒数加毫秒余数转UTC日历,重新计算星期 |
|
||||
| `IEC60870_Time_Seconds_To_Local` | UTC纪元秒数加毫秒余数直接转固定UTC+8的本地日历,自动处理日期和星期 |
|
||||
| `IEC60870_Time_CP56_To_Local` | CP56解析值转本地标准时间日历,检查IV、处理SU及源时区,保留毫秒并重算星期 |
|
||||
| `IEC60870_Time_CP56_To_Seconds` | CP56解析值转UTC秒数,检查IV、扣除SU的一小时和标准时区偏移 |
|
||||
| `IEC60870_Time_To_CP56` | 日历与标志编码为七字节,校验输出容量 |
|
||||
| `IEC60870_Time_From_CP56` | 从至少七字节的输入解析日历与标志,校验格式和日期 |
|
||||
|
||||
下面示例供后续业务层接入时使用,示例函数尚未作为生产接口实现。示例适用于**通信双方已约定CP56使用本地标准时间基准**的接入:发送使用本地标准时间日历、SU=0;接收支持SU=0和1,输出已修正的本地标准时间日历。输入日历由调用方提供,工具本身不读取系统时钟或设置RTC。
|
||||
|
||||
```c
|
||||
#include "IEC60870_Time.h"
|
||||
|
||||
int Encode_Local_Timestamp(const IEC60870_WallTime_t *local_time,
|
||||
uint8_t *out, size_t capacity)
|
||||
{
|
||||
IEC60870_CP56Time2a_t timestamp = {0};
|
||||
|
||||
if (local_time == NULL) {
|
||||
return 0;
|
||||
}
|
||||
/* 输入须已是本地标准时间;此示例不转换调用方传入的日历。 */
|
||||
timestamp.time = *local_time;
|
||||
timestamp.is_summer_time = 0U; /* 业务发送策略在此落实;公共编码仍允许SU=1。 */
|
||||
timestamp.has_weekday = 1U;
|
||||
/* 直接编码本地标准时间,IV=0、SU=0;编码接口检查日期、毫秒和缓冲区。 */
|
||||
return IEC60870_Time_To_CP56(×tamp, out, capacity);
|
||||
}
|
||||
|
||||
int Decode_Local_Timestamp(const uint8_t *in, size_t length,
|
||||
IEC60870_WallTime_t *local_time)
|
||||
{
|
||||
IEC60870_CP56Time2a_t timestamp;
|
||||
|
||||
if (!IEC60870_Time_From_CP56(in, length, ×tamp)) {
|
||||
return 0;
|
||||
}
|
||||
/* 源标准时区与本地均为UTC+8,只需按SU归一化,输出即为本地标准时间。 */
|
||||
return IEC60870_Time_CP56_To_Local(×tamp,
|
||||
IEC60870_LOCAL_UTC_OFFSET_MINUTES,
|
||||
local_time);
|
||||
}
|
||||
```
|
||||
|
||||
例如收到本地标准时间基准的12:00、SU=0时,输出小时为12;SU=1时输出小时为11。收到00:30、SU=1时,输出前一天23:30,日期和星期同步调整。IV=1、字段非法或转换结果越界时,整个输出日历保持不变。业务完成权限和时间合理性检查后,可以将结果交给按本地标准时间运行的RTC或显示模块,无需再次加八小时。若对端使用其他时区,应将示例中固定的480替换为对端约定的源标准时区偏移。
|
||||
|
||||
## 与原代码草案的差异
|
||||
|
||||
文件、类型、宏和API统一使用 `IEC60870` 公共前缀,供101和104共用。原草案中的单值秒数返回改为“状态返回 + 输出参数”,编解码增加缓冲区长度,并引入独立的CP56标志值类型。所有入口先检查指针、范围和格式再提交输出,修复非法日期、年份混淆、毫秒静默截断和纪元值与错误混用的问题。
|
||||
|
||||
原草案解码时丢弃报文星期和SU、遇IV直接失败;现在保留IV/SU及星期是否出现,检查非零星期的一致性,并通过独立转换接口落实SU=1的一小时修正。原始解码仍可返回IV=1以供业务记录,但转成有效时间戳时会拒绝。已有接收、链路和发送模块的接口及行为不变,时间工具由需要处理时间戳的业务按需接入。
|
||||
Reference in New Issue
Block a user