78248a1cdf
前一笔立契约与门禁(`Drivers/BSP/bsp_err.h` + `bsp-err` 钩子 + RS485 试点);这一笔按用户
指定顺序 C3 → C1 → C2 把三个模块真正收编,并让存储失败第一次变得可读、可分类。
**C3 · SD2506 整模块(11 个公共 API + 6 个内部 helper)**:三枚私有宏 `SD2506_OK`/
`SD2506_ERROR`/`SD2506_I2C_ERROR` **删除而不是留别名**——留别名等于新代码还能用旧形状,
把迁移洗掉;编译器替我们找全所有调用点。新增 `sd2506_err_from_hal()`,**把 I2C 超时从
"和没人应答共用一个 −2"里拆出来归成 `BSP_ERR_TIMEOUT`(可重试)**——SD2506 是启动链第一站、
I2C1 上电时序敏感,这是本波唯一真正的语义改善,不是改名。`sd2506_init` 的 charge 寄存器
**回读校验不符**原返参数类错误,改归 `BSP_ERR_IO`(那不是调用方的错)。
**C1 · 同形两义**:`GD5F_BUSY_TIMEOUT` → `GD5F_WAIT_EXPIRED`(**值 −2 不变**,纯消"名字叫
TIMEOUT 其实是返回码")。**GD5F 私有码集整体收编刻意没做**:169 处引用里 48 处在板级套件、
还要动 Trap 记录用词,属另一波带真机+套件回归的工作,不夹带。TPAFE5160 收编 23 处:
`BUSY_TIMEOUT → BSP_ERR_TIMEOUT`、`ERROR → BSP_ERR_PARAM`(**同值 −1,行为零变化**)、
`FRSTDATA_ERR → BSP_ERR_NO_DEVICE`(E-44 的本义就是"芯片没驱动 DB 总线",归到器件无响应比
私有 −3 有用,且避开与 `BSP_ERR_TIMEOUT` 撞值)。实测 `tpafe5160.o`/`gd5f2gq5ue.o` 的 Code
增量为 **0**——改名不产生一条指令。
⚠ **`tpafe5160_snapshot()` 刻意不套成败码**:它是 1=有新帧 / 0=无新帧 / −1=参数错 的三态
**状态**返回,0 不是错误而是"这一拍没有新数据",强行收编会把慢消费者的正常路径变成失败。
顺带订正我上一版在 `bsp_err.h` 的断言——"本层只有 `net_*` 把数量与错误挤同一返回值"是错的,
`snapshot`/`adc_input_poll` 就是第二族;两族现都已写明并各注保留理由。
**C2 · FTL 坍缩点分类**:`ftl_flush_cache`/`ftl_read_page` 不再把 dhara 失败压成
`GD5F_ERROR`,
按**处置差异**分三类,并在最早同时知道 LPN 与原始码的位置就地打印:`ECC`/`TOO_BAD` →
`BSP_ERR_CRC`(页级,重试或换页,不代表卷坏);`JOURNAL_FULL`/`MAP_FULL` →
`BSP_ERR_NO_SPACE`(介质健康但块池/日志见底,此时重格式化只是把问题推回去);
`BAD_BLOCK`/`RECOVER`/`CORRUPT_MAP`/越界 → `BSP_ERR_IO`(完整性故障,保卷 + 上报,不许自愈
重格式化)。这与 **E-31** 是同一条纪律的两端。可读名用自建短表:**本仓 dhara 没有编
`error.c`,用 `dhara_strerror()` 会直接链接失败**。`disk_read`/`disk_write` 仍回
`RES_ERROR`
(FatFs 契约);要让 defaultTask 卷健康探针按类分支还差跨调用存活的快照 → C4 待还。
**两次靠核实拦住的自我错误**:① 我一度认定 `DBG_ERROR("... err=%d", err)` 是把结构体塞进
varargs 的格式 bug——核 `Lib/dhara/error.h` 后确认 `dhara_error_t` 是 `enum`(标量),那行日志
本来合法,改了反而是引入错误;② `bsp-err` 一次 `replace_all` 越界把 8 处 **GD5F 码空间**的
判据改成 `!= BSP_OK`(数值同为 0、无行为变化),但那正是"把两个码空间混起来",已逐一还原。
`cppcheck` 亦抓到 `cls` 声明作用域过宽,已收进失败分支。
**验证**:`@build` 0 错 0 警告(`Code` 213,648、RO 18,860、`ER_IROM1` 232,508 B /
22.2%、
`ROM 合计` 233,696;**RAM 三项零增量**);`@test` PASS(692 ms,烧入戳 `Oct 23 01:35:32`)
且逐条读到被改模块的锚点:`SD2506 init: OK (ret=0)` 与 ID/时间/温度/电池四条读回正常、
`ADC init: OK`、`NAND init: OK`、`Map resumed: 14094/97504`、`adc=` 帧计数持续递增(证明
snapshot 三态未被改坏)、`[ERR]`/Fault/FAIL 0 条。主机单测:系统时钟 100/100、dhara NAND
移植层 149/149(两者都因本次改动需补 `Drivers/BSP` include 路径,补后在钩子内实跑 Passed)。
`pre-commit run` 全量 RC=0(20 个实跑钩子 Passed);`bsp-err` 基线 **19 → 17**(还掉两个
`*_BUSY_TIMEOUT = -2`)、`--prune` 干净;`doc-links --all --sections-strict` 147 篇
0 问题;
`id-check --strict`、`layer-deps`、`mem-numbers`、`mem-budget`、`ui-golden` 全过;33
个文件
字节卫生 0 异常(无 CR / 无 U+FFFD / 无行尾空格 / 均有尾换行)。
**口径与数字联动**:新增 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.4(三类码与处置
理由);`pre-commit使用说明.md` §2.27 与 `AGENTS.md` 基线计数 19 → 17;TPAFE README、
GD5F 测试指南与 Trap 记录均**保留旧名留痕**以便对旧日志;配图桶按 .map 对象级增量重算
(`dhara/FTL` 10,931 → 11,222,+291 全部落在 `nand_ftl.o`;应用代码/UI +36 落在
`app_main.o`;其余散量入"其他"7,557;桶和 = Code+RO 断言通过、文本溢出 0);
`ram_baseline --update/--write`、`ram_report_20261023.md` 快照、报告 §1.3/§5.1/§5.4
与
部署文档 §3.3 数字全部同步;报告 §0.4/§9 的 S-5 状态改为 🟡 主体已落地。
490 lines
22 KiB
C
490 lines
22 KiB
C
/*
|
||
* 模块名称:System Clock — Wall Clock Time Maintenance
|
||
* 模块功能:实现系统墙钟时间维护,包括 RTC 初始读取、HAL Tick 漂移维持
|
||
* 适用平台:STM32F407ZGTx (Cortex-M4 FPU, 168MHz)
|
||
* 作者:王建锋
|
||
* 创建日期:2026-07-19
|
||
* 修改记录:
|
||
* v1.0 2026-07-19 王建锋 创建初始版本
|
||
* v1.1 2026-09-13 王建锋 新增 sys_clock_get_time()(日历时间获取器,供 FatFs 时间钩子等使用)
|
||
* v1.2 2026-09-13 王建锋 文件移入 App/util/(改归 ④中间件层:通用平台时间服务,多服务共用)
|
||
* v1.3 2026-09-15 王建锋 修复 49.7 天墙钟回绕:sys_clock_get 每 30 天惰性再基准
|
||
* (自我参照、无损、零 I2C);修正"未初始化"哨兵判断;
|
||
* sys_clock_init 检查 RTC 读返回值/写回复验,失败时降级为
|
||
* 编译期默认时间基准并返回 -1(激活 app_main 时钟芯片自检)
|
||
*/
|
||
|
||
#include "sys_clock.h"
|
||
#include "main.h"
|
||
#include <string.h>
|
||
#include <stdio.h>
|
||
#include "FreeRTOS.h" /* taskENTER_CRITICAL:惰性再基准化的并发保护 */
|
||
#include "task.h"
|
||
|
||
#define DBG_TAG "[CLK]"
|
||
#include "dbg_log.h"
|
||
|
||
/* ============================================================
|
||
* RTC 校时(开发期一次性)
|
||
* 使能 RTC_DEFAULT_INIT_ENABLE 后:
|
||
* RTC_DEFAULT_INIT_FORCE=1:每次启动都写入默认时间(首次/覆盖校时用)
|
||
* RTC_DEFAULT_INIT_FORCE=0:仅当 RTC 时间无效(越界)时写入
|
||
* 校时完成后建议将 FORCE 置 0(或注释 ENABLE),避免每次开机覆盖用户设的时间。
|
||
* 修改日期/时间时同步更新下列宏。
|
||
* ============================================================ */
|
||
#define RTC_DEFAULT_INIT_ENABLE 1
|
||
#define RTC_DEFAULT_INIT_FORCE 0
|
||
#define RTC_DEFAULT_YEAR 2026
|
||
#define RTC_DEFAULT_MONTH 8
|
||
#define RTC_DEFAULT_DAY 24
|
||
#define RTC_DEFAULT_HOUR 0
|
||
#define RTC_DEFAULT_MINUTE 0
|
||
#define RTC_DEFAULT_SECOND 0
|
||
|
||
/* ============================================================
|
||
* 内部状态:系统墙钟
|
||
* ============================================================ */
|
||
|
||
static volatile uint32_t s_base_tick = 0; /* HAL_GetTick() 基准 */
|
||
static volatile uint32_t s_base_unix_secs = UINT32_MAX;
|
||
|
||
/*
|
||
* 再基准化周期(惰性再基准化,见 sys_clock_get 说明):
|
||
* 32 位 tick 差值窗口必须恒 < 一整圈(2^32 ms ≈ 49.7 天),否则差值混叠、
|
||
* 墙钟"倒退"一圈。30 天 = 2,592,000,000 ms < 2^32,为惰性触发延迟留 19.7 天余量。
|
||
*/
|
||
#define SYS_CLOCK_REBASE_MS (30u * 24u * 3600u * 1000u)
|
||
|
||
/* ============================================================
|
||
* 内部辅助函数:Unix time ↔ sd2506_time_t 转换
|
||
* ============================================================ */
|
||
|
||
/*
|
||
* 函数功能:根据公历日期计算星期几
|
||
* 入口参数:y - 年份 uint16_t 1970~2099
|
||
* m - 月份 uint8_t 1~12(**1-based**,与 sd2506_time_t.month 一致)
|
||
* d - 日 uint8_t 1~31
|
||
* 返回值:星期编号 uint8_t 0=Sunday, 1=Monday, ... 6=Saturday
|
||
* 限定条件:输入为公历合法日期
|
||
* 函数说明:1. 采用 Zeller 同余式(Gregorian):
|
||
* h = (d + ⌊13(m+1)/5⌋ + K + ⌊K/4⌋ + ⌊J/4⌋ + 5J) mod 7
|
||
* 其中 mm 为调整月(1/2 月按上年 13/14 月处理),
|
||
* K = wy % 100,J = wy / 100;h: 0=Saturday, 1=Sunday, ...
|
||
* 结果映射 (h+6) mod 7 → 0=Sunday(sd2506 约定)。
|
||
* 注:5J ≡ -2J (mod 7),取 5J 以保证各项非负(无符号安全)。
|
||
* 2. 与 unix_to_sd2506() 内星期算法保持一致(后者按 0-based 月
|
||
* 调用本函数前 +1,切勿再传 0-based 月)。
|
||
* 修改记录:
|
||
* 2026-10-02 修正:原式错用 (wy%100 + wy/100/4 - wy/100/15)(缺 K/4、
|
||
* 5J/世纪项,且把 1-based 月当 2-based 用),实测 1970~2099
|
||
* 全部日期星期皆错(主机侧单测 TC-SYSCLK-014/019 发现)。
|
||
*/
|
||
static uint8_t rtc_calc_week(uint16_t y, uint8_t m, uint8_t d) {
|
||
uint32_t wy = y;
|
||
uint32_t wm = m; /* wm: 1-based 月,1/2 月上调为 13/14 月 */
|
||
|
||
if (wm <= 2u) {
|
||
wy--;
|
||
wm += 12u;
|
||
}
|
||
|
||
{
|
||
uint32_t K = wy % 100u;
|
||
uint32_t J = wy / 100u;
|
||
uint32_t h;
|
||
/* 各项均非负:d + 月项 + K + K/4 + J/4 + 5J,模 7 得 0=Saturday */
|
||
h = (uint32_t)(d + (13u * (wm + 1u)) / 5u + K + K / 4u + J / 4u + 5u * J) % 7u;
|
||
/* h:0=Sat,1=Sun,... → 0=Sun 约定 */
|
||
return (uint8_t)((h + 6u) % 7u);
|
||
}
|
||
}
|
||
|
||
/*
|
||
* 函数功能:将 Unix 时间戳(自 1970-01-01 00:00:00 起的秒数)转换为 SD2506 RTC 时间结构
|
||
* 入口参数:unix_secs - Unix 时间戳 uint32_t 0~0x7FFFFFFF(~2038)
|
||
* p_sd_time - 输出时间结构指针 sd2506_time_t* 不得为 NULL
|
||
* 出口参数:p_sd_time - 填充年/月/日/时/分/秒/星期
|
||
* 返回值:无
|
||
* 限定条件:p_sd_time 必须指向有效缓冲区
|
||
* 函数说明:1. 先取模拆分时分秒,再按累计天数推算年月日
|
||
* 2. 星期由 Zeller 公式(与 rtc_calc_week 同算法)得出
|
||
* 3. SD2506 年份字段为相对值(0~99,+2000),故回写时减 2000
|
||
*/
|
||
static void unix_to_sd2506(uint32_t unix_secs, sd2506_time_t *p_sd_time) {
|
||
uint8_t h, mi, s;
|
||
|
||
/* 时分秒 */
|
||
{
|
||
uint64_t total = (uint64_t)unix_secs;
|
||
s = (uint8_t)(total % 60uLL);
|
||
total /= 60uLL;
|
||
mi = (uint8_t)(total % 60uLL);
|
||
total /= 60uLL;
|
||
h = (uint8_t)(total % 24uL);
|
||
}
|
||
|
||
/* 年月日 */
|
||
{
|
||
uint32_t y, m, d;
|
||
int is_leap;
|
||
uint64_t day_count = ((uint64_t)unix_secs / 86400uL);
|
||
|
||
y = 1970u;
|
||
while (day_count >= 365uLL) {
|
||
uint64_t diy = 365uLL;
|
||
if ((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0)) {
|
||
diy = 366uLL;
|
||
}
|
||
// cppcheck-suppress knownConditionTrueFalse ; 闰年 diy=366 时 day_count 可能为 365
|
||
if (day_count < diy) break;
|
||
day_count -= diy;
|
||
y++;
|
||
}
|
||
|
||
is_leap = ((y % 4 == 0 && y % 100 != 0) || (y % 400 == 0));
|
||
uint8_t mdays[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
|
||
if (is_leap) mdays[1] = 29;
|
||
|
||
m = 0;
|
||
while (m < 12u) {
|
||
if (day_count < (uint64_t)mdays[m]) break;
|
||
day_count -= mdays[m];
|
||
m++;
|
||
}
|
||
d = (uint32_t)(day_count + 1uL);
|
||
|
||
/* 星期:复用 rtc_calc_week()(1-based 月;此处 m 为 0-based → 传 m+1)
|
||
* 2026-10-02 修正:原内联变种公式有误(与 rtc_calc_week 同缺陷),
|
||
* 统一收敛到 rtc_calc_week,避免两处算法再次发散。 */
|
||
uint8_t week = rtc_calc_week((uint16_t)y, (uint8_t)(m + 1u), (uint8_t)d);
|
||
|
||
/* sd2506_time_t.year 采用完整年份(如 2026),与 sd2506_set_time
|
||
* 中 year-2000、sd2506_get_time 中 +2000 的约定保持一致;
|
||
* 此处绝不能再减 2000,否则写 RTC 时会被二次减 2000 导致年份错乱。 */
|
||
p_sd_time->year = (uint16_t)y;
|
||
p_sd_time->month = m + 1uL; /* 月份从 0 → 1 */
|
||
p_sd_time->day = d;
|
||
p_sd_time->hour = h;
|
||
p_sd_time->minute = mi;
|
||
p_sd_time->second = s;
|
||
p_sd_time->week = week;
|
||
}
|
||
}
|
||
|
||
/* ============================================================
|
||
* 系统墙钟时间维护
|
||
* ============================================================ */
|
||
|
||
/*
|
||
* 函数功能:初始化系统墙钟时间基准
|
||
* 入口参数:无
|
||
* 返回值:0 - 成功;-1 - RTC 不可用(启用编译期默认值时降级为编译期默认时间
|
||
* 基准:墙钟仍可用,返回值报告芯片故障;否则不建基准,墙钟保持"未初始化")
|
||
* 限定条件:CubeMX 外设初始化完成、SD2506 RTC 驱动已初始化
|
||
* 函数说明:1. 从 SD2506 读取初始时间(检查返回值:I2C 读失败 → 判 RTC 异常),
|
||
* 必要时按编译期默认宏校时(RTC_DEFAULT_INIT_*),写回后复验范围
|
||
* 2. 将 RTC 时间换算为 Unix epoch 秒数(仅合法值进入换算,防垃圾月份在
|
||
* i < month - 1 处下溢空转约 40 亿次)
|
||
* 3. 记录当前 HAL Tick 与 Unix 秒数为基准,供 sys_clock_get() 后续推算
|
||
* 4. RTC 不可用时:启用编译期默认值则以默认时间建基准(仅 RAM、不依赖芯片)
|
||
* 并返回 -1;否则不建基准,返回 -1
|
||
* 5. 注意:sd2506_get_time() 返回的 year 已是完整年份(驱动内 +2000),
|
||
* 换算时不得再加 2000,否则年份虚增导致 Unix 秒数超 uint32 回绕
|
||
*/
|
||
int sys_clock_init(void) {
|
||
sd2506_time_t sd_time;
|
||
uint32_t year, month, day, hour, minute, second;
|
||
int64_t unix_secs = 0;
|
||
int i;
|
||
int rtc_ok; /* RTC 读取可用(1)/不可用(0) */
|
||
int degraded = 0; /* 1 = 以编译期默认时间建基准(RTC 故障,返回值报 -1) */
|
||
|
||
/* ============================================================
|
||
* Step 1:从 SD2506 硬件 RTC 读取初始时间
|
||
* ============================================================
|
||
* 注意:sd2506_get_time() 返回的 year 已是完整年份(如 2026,
|
||
* 驱动内部已 +2000)。此处【禁止】再 +2000,否则年份虚增到 4026,
|
||
* 换算出的 Unix 秒数远超 uint32_t 上限被截断回绕,时间彻底错乱。
|
||
* 返回值必须检查:I2C 失败时字段为垃圾,不得用于建立基准。 */
|
||
rtc_ok = (sd2506_get_time(&sd_time) == BSP_OK) ? 1 : 0;
|
||
|
||
/* ============================================================
|
||
* Step 1.1:开发期校时(可选,由 RTC_DEFAULT_INIT_* 宏控制)
|
||
* ============================================================
|
||
* 作用:RTC 出厂/掉电异常导致时间非法时,写入一个可用初始值;
|
||
* FORCE=1 时每次启动都强制写入,便于首次烧录或硬件时钟丢失。
|
||
* 判定"时间无效":年份越界、月/日越界(仅粗判,不细校 2 月 29 日等)。 */
|
||
#if defined(RTC_DEFAULT_INIT_ENABLE)
|
||
{
|
||
int rtc_invalid = (rtc_ok == 0) ||
|
||
(sd_time.year < 2000 || sd_time.year > 2099 ||
|
||
sd_time.month < 1 || sd_time.month > 12 ||
|
||
sd_time.day < 1 || sd_time.day > 31);
|
||
#if (RTC_DEFAULT_INIT_FORCE == 1)
|
||
rtc_invalid = 1; /* 强制每次启动校时:会覆盖用户/网络已设的正确时间,仅限开发期 */
|
||
#endif
|
||
if (rtc_invalid) {
|
||
sd2506_time_t init_t;
|
||
/* 用编译期默认宏构造初始时间;week 由 Zeller 公式(rtc_calc_week)推算 */
|
||
init_t.year = RTC_DEFAULT_YEAR;
|
||
init_t.month = RTC_DEFAULT_MONTH;
|
||
init_t.day = RTC_DEFAULT_DAY;
|
||
init_t.hour = RTC_DEFAULT_HOUR;
|
||
init_t.minute = RTC_DEFAULT_MINUTE;
|
||
init_t.second = RTC_DEFAULT_SECOND;
|
||
init_t.week = rtc_calc_week(init_t.year, init_t.month, init_t.day);
|
||
|
||
/* 写回 SD2506:断电后该时间被保留,下次上电即为有效值 */
|
||
if (sd2506_set_time(&init_t) == BSP_OK) {
|
||
DBG_INFO("sys_clock_init: RTC set %04d-%02d-%02d %02d:%02d:%02d",
|
||
init_t.year, init_t.month, init_t.day,
|
||
init_t.hour, init_t.minute, init_t.second);
|
||
rtc_ok = (sd2506_get_time(&sd_time) == BSP_OK) ? 1 : 0; /* 重新读取校准后的值(同样检查返回值) */
|
||
} else {
|
||
DBG_ERROR("sys_clock_init: RTC default write FAIL");
|
||
}
|
||
}
|
||
}
|
||
#endif
|
||
|
||
/*
|
||
* 写回后复验范围:能写入但读回仍越界的芯片同样判故障;
|
||
* 同时保证进入 Step 2 换算循环的必为合法值(见函数说明第 2 条)。
|
||
*/
|
||
if (rtc_ok != 0 &&
|
||
(sd_time.year < 2000 || sd_time.year > 2099 ||
|
||
sd_time.month < 1 || sd_time.month > 12 ||
|
||
sd_time.day < 1 || sd_time.day > 31)) {
|
||
rtc_ok = 0;
|
||
}
|
||
|
||
if (rtc_ok == 0) {
|
||
#if (RTC_DEFAULT_INIT_ENABLE == 1)
|
||
/*
|
||
* 降级基准:以编译期默认时间建立基准(仅 RAM、不依赖芯片)。
|
||
* 墙钟保持可用(日志时间戳为默认时间);返回值报告芯片故障,
|
||
* 触发 app_main 的 "clock chip fault" 自检。
|
||
*/
|
||
DBG_ERROR("sys_clock_init: RTC abnormal, fallback to default time");
|
||
sd_time.year = RTC_DEFAULT_YEAR;
|
||
sd_time.month = RTC_DEFAULT_MONTH;
|
||
sd_time.day = RTC_DEFAULT_DAY;
|
||
sd_time.hour = RTC_DEFAULT_HOUR;
|
||
sd_time.minute = RTC_DEFAULT_MINUTE;
|
||
sd_time.second = RTC_DEFAULT_SECOND;
|
||
sd_time.week = rtc_calc_week(sd_time.year, sd_time.month, sd_time.day);
|
||
degraded = 1;
|
||
#else
|
||
DBG_ERROR("sys_clock_init: RTC read FAIL, wall clock uninitialized");
|
||
return -1;
|
||
#endif
|
||
}
|
||
|
||
/* 取出各时间字段(year 已是完整年份,直接用于下文 Unix 换算) */
|
||
year = (uint32_t)sd_time.year;
|
||
month = (uint32_t)sd_time.month;
|
||
day = (uint32_t)sd_time.day;
|
||
hour = (uint32_t)sd_time.hour;
|
||
minute = (uint32_t)sd_time.minute;
|
||
second = (uint32_t)sd_time.second;
|
||
|
||
/* ============================================================
|
||
* Step 2:将 RTC 日历时间换算为 Unix epoch 秒数
|
||
* ============================================================
|
||
* Unix 时间定义:自 1970-01-01 00:00:00 起经过的秒数。
|
||
* 算法:总天数(1970~去年整年 + 今年到上月 + 本月已过天)× 86400
|
||
* + 当天时分秒。
|
||
* 用 int64_t / uint64_t 累加避免中间溢出;最终结果(受 uint32_t 限制)
|
||
* 有效范围约 1970-01-01 ~ 2038-01-19,超出将回绕(见文档第 12 节)。 */
|
||
{
|
||
uint64_t total_days = 0;
|
||
|
||
/* 2.1 累加 1970 至(今年-1)的整年天数:闰年 366、平年 365
|
||
* 闰年规则:能被 4 整除但不能被 100 整除,或能被 400 整除 */
|
||
for (i = 1970; i < year; i++) {
|
||
if ((i % 4 == 0 && i % 100 != 0) || (i % 400 == 0)) {
|
||
total_days += 366uLL;
|
||
} else {
|
||
total_days += 365uLL;
|
||
}
|
||
}
|
||
|
||
/* 2.2 判断今年是否闰年,构造各月天数表(2 月特判) */
|
||
int is_leap = ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0));
|
||
uint8_t mdays[12] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
|
||
if (is_leap) mdays[1] = 29; /* 闰年 2 月 29 天 */
|
||
|
||
/* 2.3 累加今年 1 月至(本月-1)月的天数 */
|
||
for (i = 0; i < month - 1; i++) {
|
||
total_days += mdays[i];
|
||
}
|
||
/* 2.4 加上本月已过天数(day-1:从今天 0 点起算) */
|
||
total_days += day - 1uL;
|
||
|
||
/* 2.5 总天数×86400(一天秒数)+ 当日时分秒 = 绝对 Unix 秒数 */
|
||
unix_secs = (int64_t)(total_days * 86400LL) + hour * 3600LL + minute * 60LL + second;
|
||
}
|
||
|
||
/* ============================================================
|
||
* Step 3:建立"墙钟基准"(基准法核心)
|
||
* ============================================================
|
||
* 记录当前 HAL Tick 与对应的 Unix 秒数。此后 sys_clock_get() 仅用
|
||
* now_unix = s_base_unix_secs + (HAL_GetTick() - s_base_tick)/1000
|
||
* 推算当前时间,运行期不再访问 I2C RTC,零阻塞、无累积误差。 */
|
||
s_base_tick = HAL_GetTick();
|
||
s_base_unix_secs = (uint32_t)unix_secs;
|
||
|
||
DBG_INFO("sys_clock_init: RTC initial time %04d-%02d-%02d %02d:%02d:%02d",
|
||
year, month, day, hour, minute, second);
|
||
|
||
DBG_INFO("sys_clock_init: wall clock base = unix %lu, base_tick=%lu",
|
||
(unsigned long)s_base_unix_secs, (unsigned long)s_base_tick);
|
||
|
||
return (degraded != 0) ? -1 : 0;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:获取当前系统墙钟时间(Unix 时间戳)
|
||
* 入口参数:无
|
||
* 返回值:当前 Unix 秒数 uint32_t 未初始化时返回 UINT32_MAX
|
||
* 限定条件:sys_clock_init() 已调用
|
||
* 函数说明:1. 以基准 Tick 与基准 Unix 秒数为起点,按当前 HAL Tick 与 1000ms
|
||
* 的整除差推算流逝秒数(避免每毫秒累加,减少漂移与溢出风险);
|
||
* 32 位无符号模减对 Tick 回绕天然无缝
|
||
* 2. 差值窗口达到 SYS_CLOCK_REBASE_MS(30 天)时触发"惰性再基准化":
|
||
* 基准前移流逝秒数(墙钟值不变)、差值窗口清零,亚秒余数结转入
|
||
* 新窗(base_tick = now − 余数)故无损,保证差值永不因一整圈
|
||
* 2^32 ms(≈49.7 天)而混叠
|
||
* 3. 再基准化为自我参照:纯内存操作、零 I2C、非阻塞——不重读 RTC
|
||
* 重锚(RTC 精度远逊 CPU Tick,重锚反而吸收其漂移);临界区 +
|
||
* 区内复核防多任务并发重复累加
|
||
* 4. 若基准未建立(init 失败),返回 UINT32_MAX 表示无效
|
||
*/
|
||
uint32_t sys_clock_get(void) {
|
||
uint32_t now_tick = HAL_GetTick();
|
||
uint32_t diff = now_tick - s_base_tick; /* 32 位模减,跨 Tick 回绕无缝 */
|
||
int64_t unix_secs;
|
||
|
||
if (s_base_unix_secs != UINT32_MAX && diff > SYS_CLOCK_REBASE_MS) {
|
||
uint32_t secs = diff / 1000u;
|
||
uint32_t rem = diff - secs * 1000u; /* 亚秒余数,结转入新窗 → 无损 */
|
||
|
||
taskENTER_CRITICAL();
|
||
diff = now_tick - s_base_tick; /* 临界区内复核:另一任务可能已先再基准化 */
|
||
if (diff > SYS_CLOCK_REBASE_MS) {
|
||
s_base_unix_secs += secs; /* 基准前移:墙钟值本身不变 */
|
||
s_base_tick = now_tick - rem; /* 新窗自 (now − 余数) 起算,余数续计 */
|
||
diff = rem;
|
||
}
|
||
taskEXIT_CRITICAL();
|
||
}
|
||
|
||
if (s_base_unix_secs == UINT32_MAX) {
|
||
return UINT32_MAX; /* 未初始化(显式哨兵判断) */
|
||
}
|
||
|
||
unix_secs = (int64_t)s_base_unix_secs + (int64_t)(diff / 1000u);
|
||
if (unix_secs < 0) {
|
||
return UINT32_MAX;
|
||
}
|
||
|
||
return (uint32_t)unix_secs;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:获取当前墙钟时间的可读字符串(YYYY-MM-DD HH:MM:SS)
|
||
* 入口参数:buf - 输出字符串缓冲区 char* 不得为 NULL
|
||
* buf_size - 缓冲区大小 size_t 须 ≥ 20(含结尾 '\0')
|
||
* 出口参数:buf - 填充格式化时间字符串
|
||
* 返回值:buf 指针(便于链式调用)
|
||
* 限定条件:buf 指向至少 buf_size 字节的有效缓冲区;sys_clock_init() 已调用
|
||
* 函数说明:1. 内部先取 Unix 秒数,未初始化时输出占位串 "----/--/-- --:--:--"
|
||
* 2. 通过 unix_to_sd2506() 转换为 SD2506 结构后格式化
|
||
* 3. 使用 snprintf 防止缓冲区溢出
|
||
*/
|
||
char *sys_clock_get_str(char *buf, size_t buf_size) {
|
||
uint32_t unix_secs = sys_clock_get();
|
||
if (unix_secs == UINT32_MAX) {
|
||
snprintf(buf, buf_size, "----/--/-- --:--:--");
|
||
return buf;
|
||
}
|
||
|
||
sd2506_time_t sd;
|
||
unix_to_sd2506(unix_secs, &sd);
|
||
|
||
snprintf(buf, buf_size, "%04d-%02d-%02d %02d:%02d:%02d",
|
||
sd.year, sd.month, sd.day,
|
||
sd.hour, sd.minute, sd.second);
|
||
return buf;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:获取当前系统墙钟时间(日历时间)
|
||
* 入口参数:p_time - 输出时间结构指针(非 NULL)
|
||
* 出口参数:p_time - 填充年/月/日/时/分/秒/星期
|
||
* 返回值:0 - 成功, -1 - 失败(墙钟未初始化或参数无效)
|
||
* 限定条件:sys_clock_init() 已调用
|
||
* 函数说明:1. 取当前 Unix 秒数后经 unix_to_sd2506() 换算为日历时间
|
||
* 2. 与调试日志时间戳同一时间源(开机 RTC 基准 + tick 推进,运行期零 I2C)
|
||
*/
|
||
int sys_clock_get_time(sd2506_time_t *p_time) {
|
||
uint32_t unix_secs = sys_clock_get();
|
||
|
||
if (p_time == NULL) {
|
||
return -1;
|
||
}
|
||
if (unix_secs == UINT32_MAX) {
|
||
return -1;
|
||
}
|
||
|
||
unix_to_sd2506(unix_secs, p_time);
|
||
return 0;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:设置(更新)系统墙钟时间基准,并可写回 SD2506 RTC
|
||
* 入口参数:unix_secs - 新的 Unix 时间戳 uint32_t 0~0x7FFFFFFF
|
||
* sd_time - 对应的 RTC 时间结构指针 sd2506_time_t* 可为 NULL(仅更新基准)
|
||
* 出口参数:无
|
||
* 返回值:0 - 成功(始终返回 0)
|
||
* 限定条件:unix_secs 为合法 Unix 时间戳
|
||
* 函数说明:1. 重置基准 Tick 与基准 Unix 秒数,使后续 sys_clock_get() 以此为准
|
||
* 2. 若 sd_time 非 NULL,将其写回 SD2506 RTC,实现断电保持
|
||
*/
|
||
int sys_clock_set(uint32_t unix_secs, const sd2506_time_t *sd_time) {
|
||
/* Step 1: 更新墙钟基准 */
|
||
s_base_tick = HAL_GetTick();
|
||
s_base_unix_secs = unix_secs;
|
||
|
||
DBG_INFO("sys_clock_set: wall clock updated to %lu", (unsigned long)unix_secs);
|
||
|
||
/* Step 2: 写回 SD2506 RTC(若提供了 sd_time) */
|
||
if (sd_time != NULL) {
|
||
if (sd2506_set_time(sd_time) == BSP_OK) {
|
||
DBG_INFO("sys_clock_set: SD2506 RTC written");
|
||
} else {
|
||
DBG_ERROR("sys_clock_set: SD2506 RTC write FAIL");
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*
|
||
* 函数功能:设置系统时间为指定 Unix 秒数并写回 RTC
|
||
* 入口参数:unix_secs - Unix 时间戳(秒) uint32_t
|
||
* 返回值:0 - 成功 int
|
||
* 限定条件:无
|
||
* 函数说明:将 Unix 秒数转换为 SD2506 时间结构,再调用 sys_clock_set()
|
||
* 更新运行期基准并写回 RTC,实现掉电保持
|
||
*/
|
||
int sys_clock_set_unix(uint32_t unix_secs) {
|
||
sd2506_time_t sd;
|
||
|
||
/* 内部静态函数:将 Unix 秒数拆分为 SD2506 时间结构 */
|
||
unix_to_sd2506(unix_secs, &sd);
|
||
|
||
/* 更新运行期基准并写回 RTC,实现掉电保持 */
|
||
return sys_clock_set(unix_secs, &sd);
|
||
}
|