feat(sntp): 板端 SNTP 校时接入——coreSNTP 序列化器 + [sntp] 段 + 设置页(#73)
- vendored coreSNTP v2.0.0 序列化器(MIT,仅 serializer+LICENSE,零堆), 48 字节报文编解码在库,节奏与准入判据在本方(RFC 4330 §10 调研口径) - App/sync/time_sntp.c 挂 timeSyncTask 的 8888 事件循环,等待切片 ≤50 ms, 不饿任务狗与 IWDG;懒建 UDP socket,代际变化/禁用/死错误码即关闭归还 - 防伪 = OriginTimestamp 逐位等于发出时 T1(含低 16 位随机小数位); mode/stratum/闰秒指示/KoD 按 RFC 拒收;DENY·RSTR 换服务器、RATE 只退避 - 主备轮换 + 失败指数退避(5 s 起、60 s 封顶)+ 首轮 1~5 分钟随机错峰; 成功恢复 poll_min 周期(1~1440 分,出厂默认 60,默认不启用不发包) - 钟面 = 本地时间:UTC+8 固定偏移落 sys_clock_set_unix(§1.1 时基裁定, 第三个写入口,装置仍不存时区);|offset|<2 s 只记日志不步进,冷启动或 更大偏差步进并回写 SD2506,留痕 [SNTP] step 与 syslog OPER - 首个校验通过的同步置 SYS_EVT_TIMESYNC_READY(就绪位,消费方按需 NoClear) - [sntp] 配置段(enable/server1/server2/poll_min)属主 sntp_cfg.c, 注册在 net_cfg_load();UI 页 31「SNTP 设置」(密码叶):启用开关 + 主/备服务器 octet 编辑 + 周期(分) + 保存,复用 net_cfg_save_request 链 - 测试与门禁:test/host/sntp 新套件(报文/防伪/KoD/退避周期/阶跃域/配置 注册表)、net_cfg 与 time_sync 套件补 fake、黄金参考 66→74 张零漂移、 字符集 228 字、Keil 0 错 0 警(RO +5,848 B 含字体)、覆盖率基线回填 - 文档:墙钟维护说明 §16、网络配置说明 [sntp] 节、配置文件设计说明变更记录、 产品需求 FR-TIME-3 与 8.3/9.1.2 台账、UI 补齐规划/UI补齐 31 叶口径、 登记簿销项并登记「SNTP 校时真机判据」等台架(PC 时间源 192.168.31.161/.13)
This commit is contained in:
@@ -27,7 +27,7 @@ extern osEventFlagsId_t sys_evtHandle;
|
||||
#define SYS_EVT_NET_READY (1u << 0) /* netTask:net_init + PHY 稳定完成 */
|
||||
#define SYS_EVT_STORAGE_READY (1u << 1) /* defaultTask:FTL/FatFS 挂载完成 */
|
||||
#define SYS_EVT_FTP_READY (1u << 2) /* ftpTask:lftpd_start 完成后置位;暂无消费者(预留) */
|
||||
#define SYS_EVT_TIMESYNC_READY (1u << 3) /* 预留:timeSyncTask 在跑但不置本位,生产者消费者都没有 */
|
||||
#define SYS_EVT_TIMESYNC_READY (1u << 3) /* SNTP 首个校验通过的同步完成(App/sync/time_sntp.c 置位;就绪位,osFlagsNoClear 消费) */
|
||||
#define SYS_EVT_NET_RECONFIG (1u << 4) /* UI:请求立即应用新网口配置(netTask 消费后重建网络) */
|
||||
#define SYS_EVT_CFG_SAVE (1u << 5) /* UI:网口配置保存请求(defaultTask = 配置引擎属主,消费并写盘) */
|
||||
#define SYS_EVT_CONSOLE_TX (1u << 6) /* 控制台单写者:生产者入环形缓冲后置位,defaultTask 消费并发送
|
||||
|
||||
+5
-3
@@ -8,15 +8,17 @@
|
||||
| 文件 | 作用 |
|
||||
|---|---|
|
||||
| `time_sync.c/h` | **时间同步任务**:等待网络就绪 → 与时间源交互 → 校准墙钟 |
|
||||
| `time_sntp.c/h` | **SNTP 校时客户端**(2026-11-04 落地):挂在 time_sync 主循环里的 UDP/123 轮询,编解码用 `Lib/coreSNTP/` 序列化器,配置读 `[sntp]` 段(属主 `../../Drivers/BSP/NETCFG/sntp_cfg.c`);单测 `test/host/sntp/` |
|
||||
|
||||
## 工作流
|
||||
|
||||
```
|
||||
timeSyncTask 启动
|
||||
↓ osEventFlagsWait(SYS_EVT_NET_READY, ...|osFlagsNoClear) // 状态位不可消费!
|
||||
time_sync 主循环 → 与时间源通信 → sys_clock 校准
|
||||
↓
|
||||
置 SYS_EVT_TIMESYNC_READY
|
||||
time_sync 主循环 → TCP :8888 被动接收("TIME"+u32 → sys_clock 校准)
|
||||
↘ 每轮顺带 time_sntp_run(gen):到点主动向 [sntp] 服务器发 48 B 查询
|
||||
(单轮等待切片 ≤50 ms),校验通过按策略步进/只记日志,
|
||||
首个通过的同步置 SYS_EVT_TIMESYNC_READY
|
||||
```
|
||||
|
||||
> ⚠️ 等待**就绪位**(`SYS_EVT_NET_READY`)必须 `osFlagsNoClear`——它是状态位,
|
||||
|
||||
@@ -0,0 +1,606 @@
|
||||
/*
|
||||
* 模块名称:Time SNTP — 板端 SNTP 客户端(状态机实现)
|
||||
* 模块功能:见 time_sntp.h。节拍由 timeSyncTask 主循环驱动(每 ≤200ms 一次),
|
||||
* 报文编解码用 vendor coreSNTP serializer(仅 serializer 层——
|
||||
* client 层要求自带 transport 接口与定时器,本仓 UDP 已队列化在
|
||||
* net_socket,节奏循环留在任务里,见 #57 调研 §4.1)
|
||||
* 适用平台:STM32407ZGTx + CH395F;模拟器/主机单测经 stub 编译
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-11-04
|
||||
* 修改记录:
|
||||
* v1.0 2026-11-04 王建锋 创建初始版本(SNTP 客户端接入,#57 调研结论:
|
||||
* RFC 4330 §10 节奏、§5.3 防欺骗判据、§5.5 步进策略、
|
||||
* 钟面 = 本地时间(UTC+8 固定,§1.1 裁定))
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "time_sntp.h"
|
||||
|
||||
#include "core_sntp_serializer.h"
|
||||
#include "net_socket.h"
|
||||
#include "sntp_cfg.h"
|
||||
#include "sys_clock.h"
|
||||
#include "sys_events.h"
|
||||
#include "sys_log.h"
|
||||
|
||||
/* 深调用链模块(与 time_sync.c 同任务栈预算口径,见其文件头注释):
|
||||
* 日志行缓冲收到 128 B(默认 192) */
|
||||
#define DBG_LINE_BUF_MAX 128U
|
||||
#define DBG_TAG "[SNTP]"
|
||||
#include "dbg_log.h"
|
||||
|
||||
/* 私有宏定义区 */
|
||||
|
||||
#define SNTP_SERVER_PORT 123U
|
||||
/* 应答超时:基础 5s,失败逐次翻倍封顶 60s(RFC 4330 §10:间隔递增;
|
||||
* 翻倍节奏 = "超时即退避",下一次请求最早在 timeout_ms 之后发出) */
|
||||
#define SNTP_RESP_TIMEOUT_BASE_MS 5000U
|
||||
#define SNTP_RESP_TIMEOUT_MAX_MS 60000U
|
||||
/* 首次请求随机散布 1~5 分钟(RFC 4330 §10 避免同批设备上电撞服务器) */
|
||||
#define SNTP_FIRST_DELAY_MIN_MS 60000U
|
||||
#define SNTP_FIRST_DELAY_SPAN_MS 240000U
|
||||
/* 等应答期的单次收片粒度(看门狗 100ms 抽样窗约束,同 time_sync.c 第 8b 轮口径) */
|
||||
#define SNTP_RECV_SLICE_MS 50U
|
||||
/* 步进策略(调研 §5.5):|钟差| < 2s 只记录不步进(防 RTC 秒抖动来回拨针) */
|
||||
#define SNTP_STEP_DEADBAND_MS 2000
|
||||
/* 内部钟面 = 本地时间(#56 裁定 §1.1:sys_clock 存"钟面秒",104 写 UTC 的
|
||||
* 8 小时差已有文档口径)——SNTP 给的是 UTC,落钟面前加固定东八区偏移;
|
||||
* 全仓无时区存储,本常量与 UI 手工对时 / 8888 推送同源 */
|
||||
#define SNTP_TZ_LOCAL_OFFSET_SECS 28800U
|
||||
/* 合理性下限:2020-01-01 UTC;上限 = sys_clock 钟面域 0x7FFFFFFF 扣除时区 */
|
||||
#define SNTP_UNIX_MIN_VALID 1577836800U
|
||||
#define SNTP_UNIX_MAX_UTC (0x7FFFFFFFUL - SNTP_TZ_LOCAL_OFFSET_SECS)
|
||||
/* 收包缓冲:48B 报文 + 富余(>48 的扩展按 BSD 语义截断,serializer 只读前 48) */
|
||||
#define SNTP_RX_BUF_SIZE 64U
|
||||
|
||||
/* 状态机 */
|
||||
#define SNTP_ST_IDLE 0 /* 停用 / 从未启用 */
|
||||
#define SNTP_ST_WAIT_DUE 1 /* 等下一次请求窗口 */
|
||||
#define SNTP_ST_SEND 2 /* 本轮应发请求 */
|
||||
#define SNTP_ST_WAIT_RESP 3 /* 等应答(每次 run 收一个 ≤50ms 片) */
|
||||
|
||||
/* 静态变量定义区 */
|
||||
|
||||
static struct {
|
||||
uint8_t state; /* SNTP_ST_* */
|
||||
int fd; /* UDP socket(-1 = 未开) */
|
||||
uint32_t gen; /* 打开 socket 时的网络代次 */
|
||||
uint32_t due_ms; /* 下一次请求的 tick 锚点(回绕安全比较) */
|
||||
uint32_t timeout_ms; /* 当前应答超时(失败翻倍,成功复位) */
|
||||
uint32_t deadline_ms; /* 本轮应答截止 tick */
|
||||
uint8_t srv; /* 0 = 主 server1,1 = 备 server2 */
|
||||
uint8_t no_srv_logged; /* "未配置服务器"只警一次(配置态,不是故障) */
|
||||
uint8_t ready_posted; /* SYS_EVT_TIMESYNC_READY 已投(一次性) */
|
||||
uint32_t sync_count; /* 校验通过次数(含死区内未步进) */
|
||||
int32_t last_off_ms; /* 最近一次钟差(int64 截断,诊断用) */
|
||||
SntpTimestamp_t t1; /* 本轮请求时间(serializer 要求跨收发保存) */
|
||||
uint8_t pkt[SNTP_PACKET_BASE_SIZE]; /* 请求报文(48B,静态省栈) */
|
||||
} s_ctx;
|
||||
|
||||
static uint32_t s_rng; /* xorshift32:散布/防欺骗随机数(非密钥用途) */
|
||||
static uint8_t s_rng_seeded;
|
||||
|
||||
/* 私有函数区 */
|
||||
|
||||
/*
|
||||
* 函数功能:PRNG 播种一次(芯片 96 位 UID 折叠 ^ tick ^ 当前钟面)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:任务上下文
|
||||
* 函数说明:F407 软件上无可用硬件 RNG 通路(HAL_RNG 在 hal_conf 关闭),
|
||||
* 此处只需要"不可预测的 16 位低分数 + 上电散布",UID 折叠足够;
|
||||
* 非 MCU 构建(宿主单测/模拟器)不碰 0x1FFF7A10,用固定盐——
|
||||
* 防欺骗强度依赖的是应答 echo 回 T1(含随机低 16 位)的攻击者
|
||||
* 不可知性,真机上由 UID+tick 提供
|
||||
*/
|
||||
static void s_rng_seed_once(void) {
|
||||
uint32_t seed;
|
||||
|
||||
if (s_rng_seeded != 0u) {
|
||||
return;
|
||||
}
|
||||
#ifdef STM32F407XX
|
||||
{
|
||||
const volatile uint32_t *uid = (const volatile uint32_t *)0x1FFF7A10UL;
|
||||
seed = uid[0] ^ (uid[1] * 2654435761UL) ^ uid[2];
|
||||
}
|
||||
#else
|
||||
seed = 0x9E3779B9UL;
|
||||
#endif
|
||||
seed ^= (uint32_t)xTaskGetTickCount();
|
||||
seed ^= sys_clock_get();
|
||||
if (seed == 0u) {
|
||||
seed = 0xA5A5A5A5u;
|
||||
}
|
||||
s_rng = seed;
|
||||
s_rng_seeded = 1u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:xorshift32 随机数
|
||||
* 入口参数:无
|
||||
* 返回值:伪随机 uint32(播种前兜底返回黄金比例常数,不会为 0) uint32_t
|
||||
*/
|
||||
static uint32_t s_rng_next(void) {
|
||||
uint32_t x = s_rng;
|
||||
|
||||
if (x == 0u) {
|
||||
x = 0x9E3779B9u;
|
||||
}
|
||||
x ^= x << 13;
|
||||
x ^= x >> 17;
|
||||
x ^= x << 5;
|
||||
s_rng = x;
|
||||
return x;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:回绕安全的"到点"判断
|
||||
* 入口参数:now - 当前 tick;due - 目标 tick uint32_t
|
||||
* 返回值:1 - 已到/已过;0 - 未到 uint8_t
|
||||
*/
|
||||
static uint8_t s_tick_due(uint32_t now, uint32_t due) {
|
||||
return ((int32_t)(now - due) >= 0) ? 1u : 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:IPv4 四字节是否全零(未配置)
|
||||
* 入口参数:p_ip - 4 字节 const uint8_t *
|
||||
* 返回值:1 - 全零;0 - 已配置 uint8_t
|
||||
*/
|
||||
static uint8_t s_ip_is_zero(const uint8_t *p_ip) {
|
||||
return ((p_ip[0] | p_ip[1] | p_ip[2] | p_ip[3]) == 0u) ? 1u : 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取当前应使用的服务器 IP(按 s_ctx.srv 选主/备,缺位自动落另一侧)
|
||||
* 入口参数:无
|
||||
* 返回值:4 字节 IP 指针;主备都未配置返回 NULL const uint8_t *
|
||||
*/
|
||||
static const uint8_t *s_server_ip(const sntp_cfg_t *p_cfg) {
|
||||
const uint8_t *pri = p_cfg->server_pri;
|
||||
const uint8_t *bak = p_cfg->server_bak;
|
||||
|
||||
if (s_ctx.srv == 0u) {
|
||||
if (s_ip_is_zero(pri) == 0u) {
|
||||
return pri;
|
||||
}
|
||||
s_ctx.srv = 1u;
|
||||
}
|
||||
if (s_ip_is_zero(bak) == 0u) {
|
||||
return bak;
|
||||
}
|
||||
if (s_ip_is_zero(pri) == 0u) {
|
||||
s_ctx.srv = 0u;
|
||||
return pri;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:主备切换(仅两侧都配置时有效;失败退避时调用)
|
||||
* 入口参数:p_cfg - 配置副本 const sntp_cfg_t *
|
||||
* 返回值:无
|
||||
*/
|
||||
static void s_server_toggle(const sntp_cfg_t *p_cfg) {
|
||||
if ((s_ip_is_zero(p_cfg->server_pri) == 0u) &&
|
||||
(s_ip_is_zero(p_cfg->server_bak) == 0u)) {
|
||||
s_ctx.srv = (s_ctx.srv == 0u) ? 1u : 0u;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:当前 UTC 秒(钟面有效才返回)
|
||||
* 入口参数:p_utc - 输出 UTC 秒 int64_t *
|
||||
* 返回值:1 - 钟面有效;0 - 未初始化/早于合理性下限 uint8_t
|
||||
*/
|
||||
static uint8_t s_utc_now(int64_t *p_utc) {
|
||||
uint32_t face = sys_clock_get();
|
||||
|
||||
if ((face == UINT32_MAX) ||
|
||||
((uint64_t)face < (uint64_t)SNTP_UNIX_MIN_VALID + SNTP_TZ_LOCAL_OFFSET_SECS)) {
|
||||
return 0u;
|
||||
}
|
||||
*p_utc = (int64_t)face - (int64_t)SNTP_TZ_LOCAL_OFFSET_SECS;
|
||||
return 1u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取当前 SNTP 时间戳(1900 纪元秒 + 毫秒分数)
|
||||
* 入口参数:p_ts - 输出 SntpTimestamp_t *
|
||||
* 返回值:无
|
||||
* 函数说明:钟面无效(冷启动)时用"纪元差 + 开机秒 + 随机扰动"构造非零且
|
||||
* 每次上电唯一的时间戳——serializer 只要求 T1 非零防欺骗,冷启动
|
||||
* 路径的成功处理不使用 clockOffset(直接采用服务器 T3),乱值无害
|
||||
*/
|
||||
static void s_ts_now(SntpTimestamp_t *p_ts) {
|
||||
int64_t utc;
|
||||
uint32_t ms;
|
||||
|
||||
ms = (uint32_t)xTaskGetTickCount();
|
||||
if (s_utc_now(&utc) != 0u) {
|
||||
/* uint32 加和天然回绕 = SNTP era 进位语义(2036 年后仍成立) */
|
||||
p_ts->seconds = (uint32_t)(utc + (int64_t)SNTP_TIME_AT_UNIX_EPOCH_SECS);
|
||||
}
|
||||
else {
|
||||
p_ts->seconds = SNTP_TIME_AT_UNIX_EPOCH_SECS + (ms / 1000u) + (s_rng_next() & 0xFFFFu);
|
||||
}
|
||||
p_ts->fractions = (ms % 1000u) * (1000u * SNTP_FRACTION_VALUE_PER_MICROSECOND);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:打开并绑定 UDP socket(客户端侧动态端口)
|
||||
* 入口参数:gen - 当前网络代次 uint32_t
|
||||
* 返回值:0 - 成功;-1 - 失败(fd 保持 -1) int
|
||||
* 函数说明:UDP bind 恒开芯片服务器形态 Socket(PR #74 后语义),
|
||||
* 端口 0 = 动态分配;发送用带目的地址的 net_sendto,不经
|
||||
* net_udp_set_peer 轻切换(每包显式对端,源过滤未证不依赖)
|
||||
*/
|
||||
static int s_socket_open(uint32_t gen) {
|
||||
struct net_sockaddr_in addr;
|
||||
int s;
|
||||
|
||||
s = net_socket(NET_AF_INET, NET_SOCK_DGRAM);
|
||||
if (s < 0) {
|
||||
DBG_ERROR("net_socket FAIL errno=%d", net_get_errno());
|
||||
return -1;
|
||||
}
|
||||
(void)memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = NET_AF_INET;
|
||||
addr.sin_port = net_htons(0u); /* 0 = 动态本地端口 */
|
||||
addr.sin_addr.s_addr = NET_INADDR_ANY;
|
||||
|
||||
if (net_bind(s, (struct net_sockaddr *)&addr, (int)sizeof(addr)) != 0) {
|
||||
DBG_ERROR("udp bind FAIL errno=%d", net_get_errno());
|
||||
net_close(s);
|
||||
return -1;
|
||||
}
|
||||
s_ctx.fd = s;
|
||||
s_ctx.gen = gen;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:关闭 UDP socket(代次变化 / 配置停用 / 死 fd 时调用)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
*/
|
||||
static void s_socket_close(void) {
|
||||
if (s_ctx.fd >= 0) {
|
||||
net_close(s_ctx.fd);
|
||||
s_ctx.fd = -1;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:一次失败收口——超时翻倍(封顶)并重排下一次请求
|
||||
* 入口参数:now - 当前 tick uint32_t
|
||||
* 返回值:无
|
||||
* 函数说明:状态回 WAIT_DUE;socket 保留(轻问题不重开,代次变化才重开)
|
||||
*/
|
||||
static void s_fail_backoff(uint32_t now) {
|
||||
uint32_t t = s_ctx.timeout_ms * 2u;
|
||||
|
||||
if (t > SNTP_RESP_TIMEOUT_MAX_MS) {
|
||||
t = SNTP_RESP_TIMEOUT_MAX_MS;
|
||||
}
|
||||
s_ctx.timeout_ms = t;
|
||||
s_ctx.due_ms = now + t;
|
||||
s_ctx.state = SNTP_ST_WAIT_DUE;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:int64 毫秒钟差截断到 int32(日志/诊断口)
|
||||
* 入口参数:off - 毫秒钟差 int64_t
|
||||
* 返回值:截断后的 int32 毫秒 int32_t
|
||||
*/
|
||||
static int32_t s_off_clamp(int64_t off) {
|
||||
if (off > 2147483647LL) {
|
||||
return 2147483647;
|
||||
}
|
||||
if (off < -2147483647LL) {
|
||||
return -2147483647;
|
||||
}
|
||||
return (int32_t)off;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:校验通过的应答落地——步进策略 + 节奏复位 + 就绪事件
|
||||
* 入口参数:p_resp - 解析出的应答数据;now - 当前 tick uint32_t
|
||||
* 返回值:无
|
||||
* 函数说明:见文件头步进策略注释;**任何校验通过都算一次成功同步**
|
||||
* (含死区内未步进),复位退避并按 poll_min 重排下一轮;
|
||||
* 首个成功投 SYS_EVT_TIMESYNC_READY(状态位,不消费);
|
||||
* 每次实际步进落一条 quiet 操作记录("clock set by SNTP",
|
||||
* 与 8888/104 同口径,失败不落痕)
|
||||
*/
|
||||
static void s_handle_success(const SntpResponseData_t *p_resp, uint32_t now) {
|
||||
int64_t utc;
|
||||
int64_t new_utc;
|
||||
int32_t off_ms = s_off_clamp(p_resp->clockOffsetMs);
|
||||
uint8_t clock_valid;
|
||||
|
||||
s_ctx.sync_count++;
|
||||
s_ctx.last_off_ms = off_ms;
|
||||
s_ctx.timeout_ms = SNTP_RESP_TIMEOUT_BASE_MS;
|
||||
s_ctx.due_ms = now + (uint32_t)sntp_cfg_poll_min() * 60000u;
|
||||
s_ctx.state = SNTP_ST_WAIT_DUE;
|
||||
|
||||
if ((s_ctx.ready_posted == 0u) && (sys_evtHandle != NULL)) {
|
||||
(void)osEventFlagsSet(sys_evtHandle, SYS_EVT_TIMESYNC_READY);
|
||||
s_ctx.ready_posted = 1u;
|
||||
}
|
||||
|
||||
clock_valid = s_utc_now(&utc);
|
||||
if (clock_valid != 0u) {
|
||||
int64_t off = p_resp->clockOffsetMs;
|
||||
|
||||
if ((off <= (int64_t)SNTP_STEP_DEADBAND_MS) &&
|
||||
(off >= -(int64_t)SNTP_STEP_DEADBAND_MS)) {
|
||||
/* 死区:本地钟足够准,不拨针也不写 RTC(防 SD2506 无谓磨损) */
|
||||
DBG_INFO("off=%ldms within deadband, no step", (long)off_ms);
|
||||
return;
|
||||
}
|
||||
/* 四元组中点估计:把"服务器秒"折算到本地应有的 UTC 秒 */
|
||||
{
|
||||
int64_t off_s = (off >= 0) ? ((off + 500) / 1000) : ((off - 500) / 1000);
|
||||
new_utc = utc + off_s;
|
||||
}
|
||||
}
|
||||
else {
|
||||
/* 冷启动(钟面未初始化/早于 2020):钟差不可信,直接采用服务器 T3 */
|
||||
uint32_t t3_secs = 0u;
|
||||
uint32_t t3_us = 0u;
|
||||
|
||||
if (Sntp_ConvertToUnixTime(&p_resp->serverTime, &t3_secs, &t3_us) != SntpSuccess) {
|
||||
DBG_ERROR("unix convert FAIL");
|
||||
return;
|
||||
}
|
||||
new_utc = (int64_t)t3_secs;
|
||||
}
|
||||
|
||||
if ((new_utc < (int64_t)SNTP_UNIX_MIN_VALID) || (new_utc > (int64_t)SNTP_UNIX_MAX_UTC)) {
|
||||
DBG_ERROR("target unix out of domain, skip step");
|
||||
return;
|
||||
}
|
||||
|
||||
(void)sys_clock_set_unix((uint32_t)(new_utc + (int64_t)SNTP_TZ_LOCAL_OFFSET_SECS));
|
||||
syslog_post_quiet(SYSLOG_OPER, "clock set by SNTP");
|
||||
DBG_INFO("step offset=%ldms face=%lu", (long)off_ms,
|
||||
(unsigned long)(uint32_t)(new_utc + (int64_t)SNTP_TZ_LOCAL_OFFSET_SECS));
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:收包校验 + 反欺骗判据 + 解析落地(WAIT_RESP 态的单包处理)
|
||||
* 入口参数:p_buf - 载荷;len - 长度;p_src - 源地址;now - tick uint32_t
|
||||
* 返回值:1 - 本轮已收口(成功或按失败处理,调用方可停止收片);
|
||||
* 0 - 该包丢弃,继续等到 deadline uint8_t
|
||||
* 函数说明:判据顺序 = 长度 → 源地址(IP+端口必须 = 本轮请求目的)→
|
||||
* stratum≤15 → LI≠3(服务器未同步告警态)→ serializer 校验
|
||||
* (mode=4、origin 与 T1 含随机低 16 位精确匹配、时间戳非零、
|
||||
* KoD 分类)。调研 §5.3:LI=3 / stratum>15 serializer 不拦,
|
||||
* 必须在这里补;KoD 与畸形应答按失败退避(KoD 语义里
|
||||
* ChangeServer 换服务器,RATE 保留当前服务器)
|
||||
*/
|
||||
static uint8_t s_handle_packet(const uint8_t *p_buf, int len,
|
||||
const struct net_sockaddr_in *p_src,
|
||||
const uint8_t *p_srv_ip, uint32_t now) {
|
||||
const sntp_cfg_t *cfg = sntp_cfg_get();
|
||||
SntpResponseData_t resp;
|
||||
SntpTimestamp_t t4;
|
||||
SntpStatus_t st;
|
||||
|
||||
if (len < (int)SNTP_PACKET_BASE_SIZE) {
|
||||
DBG_INFO("WARN: short resp len=%d, drop", len);
|
||||
return 0u;
|
||||
}
|
||||
if ((memcmp(&p_src->sin_addr.s_addr, p_srv_ip, 4u) != 0) ||
|
||||
(p_src->sin_port != net_htons(SNTP_SERVER_PORT))) {
|
||||
/* 防欺骗第一道:应答源必须是我们刚发过请求的那个 IP:123 */
|
||||
DBG_INFO("WARN: resp from unexpected src, drop");
|
||||
return 0u;
|
||||
}
|
||||
if ((p_buf[1] & 0x7FU) > 15u) {
|
||||
DBG_INFO("WARN: stratum=%u invalid, drop", (unsigned)(p_buf[1] & 0x7FU));
|
||||
s_fail_backoff(now);
|
||||
return 1u;
|
||||
}
|
||||
if ((p_buf[0] >> 6) == 3u) {
|
||||
/* LI=3:服务器自己没同步,时间不可用;再等多久也一样 → 直接失败 */
|
||||
DBG_INFO("WARN: server not synced (LI=3)");
|
||||
s_server_toggle(cfg);
|
||||
s_fail_backoff(now);
|
||||
return 1u;
|
||||
}
|
||||
|
||||
s_ts_now(&t4);
|
||||
st = Sntp_DeserializeResponse(&s_ctx.t1, &t4, p_buf, (size_t)len, &resp);
|
||||
|
||||
switch (st) {
|
||||
case SntpSuccess:
|
||||
s_handle_success(&resp, now);
|
||||
return 1u;
|
||||
|
||||
case SntpRejectedResponseChangeServer:
|
||||
case SntpRejectedResponseOtherCode:
|
||||
DBG_INFO("WARN: kiss-of-death code=%lu, switch server",
|
||||
(unsigned long)resp.rejectedResponseCode);
|
||||
s_server_toggle(cfg);
|
||||
s_fail_backoff(now);
|
||||
return 1u;
|
||||
|
||||
case SntpRejectedResponseRetryWithBackoff:
|
||||
DBG_INFO("WARN: kiss-of-death RATE, backoff");
|
||||
s_fail_backoff(now);
|
||||
return 1u;
|
||||
|
||||
case SntpInvalidResponse:
|
||||
default:
|
||||
/* originate 不匹配 / 时间戳为零 / mode 错 = 伪造或损坏:丢弃继续等 */
|
||||
DBG_INFO("WARN: invalid resp st=%d, drop", (int)st);
|
||||
return 0u;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:发起一轮请求(SEND 态执行体:选服务器 → 开 socket → 序列化 → 发送)
|
||||
* 入口参数:net_gen - 网络代次;now - 当前 tick uint32_t
|
||||
* 返回值:无
|
||||
* 函数说明:成功 → 状态进 WAIT_RESP;任一失败 → 主备切换(适用时)+
|
||||
* s_fail_backoff 退避重排;服务器未配置 → 停在 WAIT_DUE 只警一次,
|
||||
* 等下一次 run(配置补齐即恢复,不产生退避——没发过请求不算失败)
|
||||
*/
|
||||
static void s_send_round(uint32_t net_gen, uint32_t now) {
|
||||
const sntp_cfg_t *cfg = sntp_cfg_get();
|
||||
const uint8_t *srv_ip;
|
||||
struct net_sockaddr_in dst;
|
||||
|
||||
srv_ip = s_server_ip(cfg);
|
||||
if (srv_ip == NULL) {
|
||||
if (s_ctx.no_srv_logged == 0u) {
|
||||
DBG_INFO("WARN: enabled but no server configured");
|
||||
s_ctx.no_srv_logged = 1u;
|
||||
}
|
||||
return;
|
||||
}
|
||||
s_ctx.no_srv_logged = 0u;
|
||||
|
||||
if (s_ctx.fd < 0) {
|
||||
if (s_socket_open(net_gen) != 0) {
|
||||
s_fail_backoff(now);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
s_ts_now(&s_ctx.t1);
|
||||
if (Sntp_SerializeRequest(&s_ctx.t1, s_rng_next(), s_ctx.pkt,
|
||||
sizeof(s_ctx.pkt)) != SntpSuccess) {
|
||||
/* T1 为 0 才会失败(ts 构造保证非零)——真到这儿是构造性 bug */
|
||||
DBG_ERROR("serialize FAIL");
|
||||
s_fail_backoff(now);
|
||||
return;
|
||||
}
|
||||
|
||||
(void)memset(&dst, 0, sizeof(dst));
|
||||
dst.sin_family = NET_AF_INET;
|
||||
dst.sin_port = net_htons(SNTP_SERVER_PORT);
|
||||
(void)memcpy(&dst.sin_addr.s_addr, srv_ip, 4u);
|
||||
|
||||
if (net_sendto(s_ctx.fd, s_ctx.pkt, (int)sizeof(s_ctx.pkt), 0,
|
||||
(struct net_sockaddr *)&dst, (int)sizeof(dst))
|
||||
!= (int)sizeof(s_ctx.pkt)) {
|
||||
DBG_ERROR("sendto FAIL errno=%d", net_get_errno());
|
||||
s_server_toggle(cfg);
|
||||
s_fail_backoff(now);
|
||||
return;
|
||||
}
|
||||
|
||||
s_ctx.deadline_ms = now + s_ctx.timeout_ms;
|
||||
s_ctx.state = SNTP_ST_WAIT_RESP;
|
||||
/* 发送轮不占收片时间,立即让出回主循环 */
|
||||
}
|
||||
|
||||
/* 函数定义区 */
|
||||
|
||||
void time_sntp_run(uint32_t net_gen) {
|
||||
const sntp_cfg_t *cfg;
|
||||
const uint8_t *srv_ip;
|
||||
uint32_t now = (uint32_t)xTaskGetTickCount();
|
||||
|
||||
s_rng_seed_once();
|
||||
cfg = sntp_cfg_get();
|
||||
|
||||
/* 网络代次变化:旧 socket 随芯片复位失效,弃掉重排首轮散布 */
|
||||
if (net_gen != s_ctx.gen) {
|
||||
s_socket_close();
|
||||
s_ctx.gen = net_gen;
|
||||
s_ctx.state = SNTP_ST_IDLE;
|
||||
s_ctx.no_srv_logged = 0u;
|
||||
}
|
||||
|
||||
if (cfg->enable == 0u) {
|
||||
if (s_ctx.state != SNTP_ST_IDLE) {
|
||||
s_socket_close();
|
||||
s_ctx.state = SNTP_ST_IDLE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_ctx.state == SNTP_ST_IDLE) {
|
||||
s_ctx.state = SNTP_ST_WAIT_DUE;
|
||||
s_ctx.timeout_ms = SNTP_RESP_TIMEOUT_BASE_MS;
|
||||
s_ctx.due_ms = now + SNTP_FIRST_DELAY_MIN_MS +
|
||||
(s_rng_next() % SNTP_FIRST_DELAY_SPAN_MS);
|
||||
}
|
||||
|
||||
switch (s_ctx.state) {
|
||||
case SNTP_ST_WAIT_DUE:
|
||||
if (s_tick_due(now, s_ctx.due_ms) != 0u) {
|
||||
s_send_round(net_gen, now);
|
||||
}
|
||||
return;
|
||||
|
||||
case SNTP_ST_SEND:
|
||||
s_send_round(net_gen, now);
|
||||
return;
|
||||
|
||||
case SNTP_ST_WAIT_RESP: {
|
||||
uint8_t rxbuf[SNTP_RX_BUF_SIZE];
|
||||
struct net_sockaddr_in src;
|
||||
int src_len = (int)sizeof(src);
|
||||
int n;
|
||||
|
||||
(void)memset(&src, 0, sizeof(src));
|
||||
n = net_recvfrom_to(s_ctx.fd, rxbuf, (int)sizeof(rxbuf),
|
||||
SNTP_RECV_SLICE_MS,
|
||||
(struct net_sockaddr *)&src, &src_len);
|
||||
if (n > 0) {
|
||||
srv_ip = s_server_ip(cfg);
|
||||
if (srv_ip == NULL) {
|
||||
s_fail_backoff(now);
|
||||
return;
|
||||
}
|
||||
(void)s_handle_packet(rxbuf, n, &src, srv_ip, now);
|
||||
return;
|
||||
}
|
||||
if (n == -2) {
|
||||
/* 本片等满无数据:看总截止 */
|
||||
if (s_tick_due(now, s_ctx.deadline_ms) != 0u) {
|
||||
DBG_INFO("WARN: resp timeout, tmo=%lu ms", (unsigned long)s_ctx.timeout_ms);
|
||||
s_server_toggle(cfg);
|
||||
s_fail_backoff(now);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (n == -1) {
|
||||
int e = net_get_errno();
|
||||
|
||||
DBG_ERROR("recvfrom FAIL errno=%d", e);
|
||||
if ((e == NET_ERR_BADF) || (e == NET_ERR_CONNRESET) ||
|
||||
(e == NET_ERR_INVAL)) {
|
||||
/* fd 已死(重建取消/非法):关掉等代次/下轮重开 */
|
||||
s_socket_close();
|
||||
s_fail_backoff(now);
|
||||
}
|
||||
/* 其余瞬时错误:继续等到 deadline,超时自然收口 */
|
||||
return;
|
||||
}
|
||||
/* n == 0:UDP 语义下不应出现(EOF 是 TCP 概念),防御收口 */
|
||||
s_fail_backoff(now);
|
||||
return;
|
||||
}
|
||||
|
||||
default:
|
||||
s_ctx.state = SNTP_ST_IDLE;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t time_sntp_sync_count(void) {
|
||||
return s_ctx.sync_count;
|
||||
}
|
||||
|
||||
int32_t time_sntp_last_offset_ms(void) {
|
||||
return s_ctx.last_off_ms;
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* 模块名称:Time SNTP — 板端 SNTP 客户端(UDP 48 字节报文)
|
||||
* 模块功能:按 [sntp] 段配置向主/备 SNTP 服务器发送 RFC 4330 请求、校验应答
|
||||
* (originate 精确匹配 + 源地址 + mode/stratum/LI/KoD)、计算钟差并
|
||||
* 按阈值步进本地墙钟(sys_clock_set_unix,钟面 = 本地时间 UTC+8),
|
||||
* 首次成功后置 SYS_EVT_TIMESYNC_READY;报文编解码用 vendor 的
|
||||
* coreSNTP serializer(Lib/coreSNTP,MIT)
|
||||
* 适用平台:STM32F407ZGTx + CH395F(timeSyncTask 内运行,模拟器不编)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-11-04
|
||||
*/
|
||||
|
||||
#ifndef TIME_SNTP_H
|
||||
#define TIME_SNTP_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/*
|
||||
* 函数功能:SNTP 客户端节拍入口(幂等、非阻塞——除等待应答期一次 ≤50ms 收片)
|
||||
* 入口参数:net_gen - 调用方本轮读取的网络代次(net_get_generation()) uint32_t
|
||||
* 返回值:无
|
||||
* 限定条件:**仅 timeSyncTask 上下文**调用(看门狗心跳由调用方在片间喂,
|
||||
* 本模块单次调用最长驻留 = 一个 50ms 收片,满足 100ms 抽样窗约束)
|
||||
* 函数说明:内部状态机 停用 → 到期 → 发送 → 等应答 → 步进/退避;配置
|
||||
* (sntp_cfg_get())每轮现读,代次变化自动弃旧 Socket 重开
|
||||
*/
|
||||
void time_sntp_run(uint32_t net_gen);
|
||||
|
||||
/*
|
||||
* 函数功能:取累计成功同步次数(诊断/测试锚点)
|
||||
* 入口参数:无
|
||||
* 返回值:成功次数(含死区内的"未步进但校验通过") uint32_t
|
||||
* 限定条件:无
|
||||
*/
|
||||
uint32_t time_sntp_sync_count(void);
|
||||
|
||||
/*
|
||||
* 函数功能:取最近一次校验通过应答的钟差(诊断锚点)
|
||||
* 入口参数:无
|
||||
* 返回值:钟差毫秒,截断到 int32;正 = 本地钟落后服务器;从未成功 = 0 int32_t
|
||||
* 限定条件:无
|
||||
*/
|
||||
int32_t time_sntp_last_offset_ms(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* TIME_SNTP_H */
|
||||
@@ -19,10 +19,15 @@
|
||||
* 等待(500/2000/200ms)统一走 time_sync_delay_fed()
|
||||
* 按 ≤50ms 分片、每片喂心跳——消除"心跳滑出两个连续
|
||||
* 100ms 抽样窗"的节拍抖动(第 7 轮台架 snap=0x17)
|
||||
* v1.4 2026-11-04 DTU 项目组 SNTP 客户端接入(#57 调研落地):accept 轮询
|
||||
* 片内驱动 time_sntp_run()(48B UDP 请求/应答状态机
|
||||
* 在 App/sync/time_sntp.c,节奏/退避/步进策略见该文件);
|
||||
* 本任务循环结构与 8888 语义不变
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
#include "time_sync.h"
|
||||
#include "time_sntp.h"
|
||||
#include "net_socket.h"
|
||||
#include "sys_clock.h"
|
||||
#include "sys_events.h"
|
||||
@@ -272,6 +277,10 @@ void time_sync_task(void *argument) {
|
||||
ls = -1;
|
||||
break;
|
||||
}
|
||||
/* SNTP 客户端节拍:accept 轮询片内驱动(等待应答期单次
|
||||
* 驻留 ≤50ms 收片,心跳仍由下面的分片延时喂,100ms 抽样窗
|
||||
* 不破)。代次变化由 time_sntp_run 内部自理(弃旧 socket)。 */
|
||||
time_sntp_run(gen);
|
||||
time_sync_delay_fed(200U);
|
||||
}
|
||||
} while (conn < 0);
|
||||
|
||||
Reference in New Issue
Block a user