Files
wandering1 78248a1cdf feat(S-5): 收编 SD2506/TPAFE 并给 dhara 失败分类,门禁基线 19→17
前一笔立契约与门禁(`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 状态改为 🟡 主体已落地。
2026-10-23 07:42:02 +08:00

490 lines
22 KiB
C
Raw Permalink 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.
/*
* 模块名称: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);
}