docs(时间): 时基口径定稿——内部一律本地钟面(用户裁定「时基先本地为准」),并纠正本文自相矛盾

用户裁定原文:「**时基先本地为准**」。这条定的是**冲突时谁赢**:面板时钟页与 8888 对时把「本地钟面当
epoch」写入,而 104 的 `C_CS NA` 对时命令携的是 UTC,两条写入口**后写者赢** ⇒ 装置墙钟与主机本地时刻
恒差 8 小时。裁定结束「后写者赢」:**内部真源 = 本地钟面**,任何来源写入前都先换算成本地钟面再落库。

**顺手挖出这条 8 小时的真正根因:本文自己就是矛盾的**——§2 的术语表把 epoch 定义成「自 1970-01-01
00:00:00 **UTC** 起的秒数」,而 §15 网络校时协议字段写的是「**本地** Unix 时间戳,与电脑显示一致」;代码
按后者实现。所以「恒差 8 小时」不只是两条写入口打架,**规范文本也没统一过**。现在 §2 改成「本地时间戳:
把本地钟面按 UTC 记法数出来的秒数,内部不加任何时区偏移」,§13 那行旧建议「如需 UTC/时区可在 API 层
加偏移参数」一并标作废(它会把「装置不存时区」这个优点拆掉)。

**为什么选本地**:日志时标、UI 显示、FAT 文件时间三者都是**给现场电气工程师读的**,跟装置所在地钟面
一致才没有解释成本;装置不存时区、不做夏令时,也就没有「时区参数配错」这一类故障。

⚠ **本笔刻意不做的一件事,也是本笔唯一需要人看的地方**:104 **上行**时标 `CP56Time2a` 按标准应发 UTC,
而 `iec104_epoch_ms_to_cp56()` 现在是把内部值**直接按 UTC 分解**发出——即发出去的是本地钟面值。两条出路
要么给装置一个时区常量并上行前减偏移(标准符合,但与「不存时区」相冲),要么维持发本地值(同时区现场
看着正确,第三方主站按 UTC 解读会差 8 小时)。**这不是 bug 修复,是产品对外语义决定,牵动送检与第三方
主站验收**,所以本轮**一行代码都不改**,只把分岔与后果写进 §1.1 末,等单独裁定。

自查:`check_doc_links --all` exit 0(198 篇)、本篇 `--sections-strict` 通过、noBOM
 / LF / U+FFFD=0 / 结尾
换行齐、不可判定绝对数 979 未变;本笔**纯文档**,零代码零编号。此刻另有五条道在飞(通讯页 / 计数器 /
对时留痕 / 台架判据文档 / 记录页设计),故只提交这一只文件,其余按道分笔。
This commit is contained in:
2026-10-27 10:50:16 +08:00
parent a10d24c09c
commit 0ae9c10f24
@@ -22,6 +22,25 @@
- 运行期间不再频繁访问 I2C RTC,而是以 **HAL Tick 流逝量** 推算当前时间,避免每次取时都走 I2C 总线;
- 仅在 `sys_clock_set()` 被调用(如用户校时、SNTP 同步)时,才把时间写回 SD2506 实现掉电保持。
### 1.1 时基口径:内部一律**本地钟面**(用户裁定 2026-10-27)
**裁定原文:「时基先本地为准」。** 定的是**冲突时谁赢**:面板时钟页与 8888 对时把「本地钟面当 epoch」写入,
而 104 的 `C_CS NA`(对时命令)携的是 UTC,两条写入口**后写者赢** ⇒ 装置墙钟与主机本地时刻恒差 8 小时。
本裁定结束「后写者赢」:**内部真源 = 本地钟面**,任何来源写入前都要先换算成本地钟面再落库。
这条同时也纠正了**本文自己的矛盾**(这才是 8 小时的根):§2 曾写 epoch = 「自 1970-01-01 00:00:00 **UTC**
起的秒数」,而 §15 的网络校时协议字段写的是「**本地** Unix 时间戳,与电脑显示一致」——两处口径不同,代码
按后者走。现在 §2 已改成「本地时间戳」口径。
**为什么选本地**:日志时标、UI 显示、FAT 文件时间三者都是**给现场电气工程师读的**,跟装置所在地钟面一致
才没有解释成本;装置不存时区、不做夏令时,也就没有「时区参数配错」这一类故障。
⚠ **本裁定没有回答的部分(送检前必须单独定)**:104 **上行**时标 `CP56Time2a` 按标准应发 **UTC**,而现在
`iec104_epoch_ms_to_cp56()` 是把内部值**直接按 UTC 分解**发出(即发出去的是本地钟面值)。两条出路:
① 上行前 **−时区偏移转 UTC**(标准符合,但要给装置一个时区常量,与「不存时区」相冲);② 维持现状发本地
值(现场与主站同时区时看着正确,**第三方主站按 UTC 解读会差 8 小时**)。**在送检/第三方主站验收方向定下来
之前,本仓不改 `iec104_asdu.c` 的分解逻辑**——它不是 bug 修复,是产品对外语义决定。
---
## 2. 角色与依赖
@@ -30,7 +49,7 @@
|------|--------|------|
| 硬件实时钟(RTC) | SD2506API-G | 温补晶振,带备份电池,掉电后继续走时;I2C1(PB6-SCL/PB7-SDA) |
| 系统节拍 | `HAL_GetTick()` | 毫秒级计数,HAL 时基由 TIM7 提供(168MHz 主频派生,精度远高于走时需求) |
| Unix 时间戳 | 本模块计算 | 自 1970-01-01 00:00:00 UTC 起的秒数(`uint32_t`) |
| Unix 时间戳 | 本模块计算 | **本地时间戳**:把本地钟面**按 UTC 记法**数出来的秒数(`uint32_t`),**内部不加任何时区偏移**(口径见 §1.1) |
| 时间结构 | `sd2506_time_t` | SD2506 驱动定义,`year` 为**完整年份(2000~2099)**,含 `week` |
**调用顺序(见 `Src/main.c`):**
@@ -281,7 +300,27 @@ uint32_t now = sys_clock_get(); /* Unix 秒,可用于 FatFS 文件时间、
| 时间范围 | 受 `uint32_t` 限制至 2038 | 改用 `uint64_t`/`time_t` 扩展 |
| 运行期校准 | 30 天惰性再基准化已实现(§7):纯自我参照、零 I2C;**有意不**周期性重读 RTC 重锚——RTC 漂移(±20ppm ≈ ±1.7 s/天)远大于 Tick 漂移,重锚反而降低精度;时间纠偏由时间同步(§15)负责 |
| 网络校时 | 已实现(PC 推送 / TCP,见第 15 节) | SNTP/NTP 公网校时仍可作为后续扩展 |
| 时区 | 按 RTC 本地时间处理 | 如需 UTC/时区,可在 API 层增加偏移参数 |
| 时区 | **口径已定:内部一律本地钟面,不存时区**(用户裁定 2026-10-27,见 §1.1) | 「API 层加偏移参数」这条旧建议**作废**;唯一待决的是 104 上行是否要转 UTC(§1.1 末,送检前定) |
### 13.1 用户 SRAM 的占用分配(共享资源,用之前先在这里登记)
SD2506 有 **70 字节电池保持用户 SRAM**(寄存器 `2CH~71H`,`Drivers/BSP/SD2506/sd2506.h`
的 `SD2506_REG_SRAM_START/END`)。它是全仓唯一"零 NAND 磨损、零卷锁、跨掉电保持"的
小介质,**但没有任何机制防止两个消费者写同一段**——所以占用只能靠本表登记,
新占用请在此加一行再动代码(偏移从低往高排,已用段永不复用)。
| 偏移(SRAM 内) | 寄存器 | 长度 | 属主与内容 | 失效判读 |
|---|---|---|---|---|
| 0..7 | `2CH~33H` | 8 B | `App/util/boot_stats.c`:上电次数账本(magic 2B + 计数 4B + 格式版本 1B + 校验 1B) | 换电池 / 电池欠压 ⇒ 整段丢失属**正常降级**,故口径是「自 RTC 电池周期起」,不是终身次数 |
| 8..69 | `34H~71H` | 62 B | **空闲**(日后新占用从这里起,别在 0..7 里挤) | — |
- 为什么这一格归 RTC 而不是文件系统:上电次数每上电才变一次,落 NAND 要么每次开机
一次 FTL 写、要么做成周期近似写;两者都不如 8 字节 I2C 写干净。写点在**内核启动前**
(`App/app/app_main.c` 紧接 `sd2506_init()`),所以既不占喂狗点、也不碰卷锁。
- ⚠ 同一条 I2C1 总线还挂着别的器件:**不要把这里的读写搬进任何关键链或 ISR**,
本表的属主全部只在启动段单线程访问一次。
- 机理与四态判读(不可信 / 首次启用 / 延续 / 电池域掉过电)的唯一出处是
`App/util/boot_stats.h` 的文件头与函数头,本文不复制第二份。
---
@@ -292,7 +331,8 @@ uint32_t now = sys_clock_get(); /* Unix 秒,可用于 FatFS 文件时间、
| `App/util/sys_clock.c` | 墙钟维护实现(基准法、换算、校时宏) |
| `App/util/sys_clock.h` | 对外 API 声明 |
| `Drivers/BSP/SD2506/sd2506.c` / `.h` | SD2506 RTC I2C 驱动(`sd2506_get_time` / `sd2506_set_time`) |
| `App/app/app_main.c` | 初始化调用顺序:`sd2506_init()` → `sys_clock_init()` |
| `App/app/app_main.c` | 初始化调用顺序:`sd2506_init()` → `boot_stats_init()`(用户 SRAM 上电账本,§13.1)→ `sys_clock_init()` |
| `App/util/boot_stats.c` / `.h` | 用户 SRAM 上电账本 + 本次开机复位原因快照(占用见 §13.1;单测 `test/host/boot_stats/`) |
| `App/sync/time_sync.c` / `time_sync.h` | 时间同步实现(TCP Server,`timeSyncTask`) |
| `test/host/time_sync_server.py` | PC 端时间推送客户端(TCP) |
@@ -384,6 +424,7 @@ PC 端 `python test/host/time_sync_server.py` 每 `interval` 秒推送一次,e
| 日期 | 变更 |
|------|------|
| 2026-10-27 | **时基口径定稿:内部一律本地钟面(用户裁定「时基先本地为准」)**:新增 §1.1;§2 的 epoch 行原写「自 1970 UTC 起」与 §15 网络校时字段「本地 Unix 时间戳」互相矛盾(代码按后者走),现统一为本地口径;§13 时区行的旧建议「API 层加偏移参数」作废。**未决**:104 上行 `CP56Time2a` 现把内部值直接按 UTC 分解发出(= 发的是本地钟面值),要不要上行前转 UTC 属产品对外语义决定,送检前单独裁定,本轮不改 `iec104_asdu.c` |
| 2026-09-15 | v1.3:① `sys_clock_get` 30 天惰性再基准化——修复 49.7 天墙钟回绕(§3/§7/§12);② `sys_clock_init` 检查 RTC 读返回值/写回复验,失败降级为默认时间基准并返回 -1——激活"时钟芯片异常"自检(§5/§6/§9);③ 修正未初始化哨兵判断(§12-6);④ 时间同步任务启动/Socket 占用描述同步为事件屏障 + 单连接实现(§15);⑤ 新增日志串用 ASCII——本机 ARMCC 按 CP936 解读无 BOM 源文件,奇数汉字字符串会吞掉结束引号致编译失败(见 AGENTS.md 编码铁律) |
*文档依据 `App/util/sys_clock.c` / `App/sync/time_sync.c` 实现与《嵌入式C语言代码规范_V1.0》整理。*