/* * 模块名称: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 #include #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-CLK-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) == SD2506_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) == SD2506_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) == SD2506_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) == SD2506_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); }