/* * 模块名称:System Clock — Wall Clock Time Maintenance * 模块功能:实现系统墙钟时间维护,包括 RTC 初始读取、HAL Tick 漂移维持 * 适用平台:STM32F407ZGTx (Cortex-M4 FPU, 168MHz) * 作者:王建锋 * 创建日期:2026-07-19 * 修改记录: * v1.0 2026-07-19 王建锋 创建初始版本 */ #include "sys_clock.h" #include "main.h" #include #include #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 1 #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; /* ============================================================ * 内部辅助函数:Unix time ↔ sd2506_time_t 转换 * ============================================================ */ /* * 函数功能:根据公历日期计算星期几 * 入口参数:y - 年份 uint16_t 1970~2099 * m - 月份 uint8_t 1~12 * d - 日 uint8_t 1~31 * 返回值:星期编号 uint8_t 0=Sunday, 1=Monday, ... 6=Saturday * 限定条件:输入为公历合法日期 * 函数说明:1. 采用 Zeller 公式(变种),结果 0 表示周日 * 2. 与 unix_to_sd2506() 内星期算法保持一致 */ static uint8_t rtc_calc_week(uint16_t y, uint8_t m, uint8_t d) { uint32_t wy = y; uint32_t wm = m; if (wm <= 2) { wy--; wm += 12u; } /* Zeller 公式:世纪项 + 月项 + 日项,模 7 得星期(0=周日) */ return (uint8_t)(((uint32_t)(wy % 100u + wy / 100u / 4u - wy / 100u / 15u) + (uint32_t)((26u * (wm + 1u)) / 10u) + (uint32_t)(d % 100u)) % 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) { uint32_t y, m, d; uint8_t h, mi, s; int is_leap; /* 时分秒 */ { 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); } /* 年月日 */ { 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; } 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); /* 星期(Zeller's congruence,0=Sunday) */ uint64_t wy = y; uint64_t wm = m; if (wm <= 2) { wy--; wm += 12uL; } /* Zeller 公式同 rtc_calc_week():世纪项 + 月项 + 日项,模 7 得星期(0=周日) */ uint8_t week = ((uint32_t)(wy % 100uLL + wy / 100uLL / 4uLL - wy / 100uLL / 15uLL) + (uint32_t)(((uint64_t)26 * (wm + 1uL)) / 10uLL) + (uint32_t)(d % 100uLL)) % 7uLL; /* SD2506 year 字段为 0~99(+2000) */ uint16_t sd_year = y >= 2000 ? (y - 2000) : (y + 2000); p_sd_time->year = sd_year; 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 - 成功(始终返回 0) * 限定条件:CubeMX 外设初始化完成、SD2506 RTC 驱动已初始化 * 函数说明:1. 从 SD2506 读取初始时间,必要时按编译期默认宏校时(RTC_DEFAULT_INIT_*) * 2. 将 RTC 时间换算为 Unix epoch 秒数 * 3. 记录当前 HAL Tick 与 Unix 秒数为基准,供 sys_clock_get() 后续推算 * 4. 注意: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; /* Step 1: 从 SD2506 RTC 读取初始时间 * 注意:sd2506_get_time() 返回的 year 已是完整年份(如 2026,驱动内 +2000), * 此处不得再加 2000,否则年份虚增 → unix 秒数超 uint32 被截断回绕。 */ sd2506_get_time(&sd_time); /* RTC 校时(可选):时间无效时,或 FORCE=1 强制时,写入编译期默认时间 */ #if defined(RTC_DEFAULT_INIT_ENABLE) { int rtc_invalid = (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; 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); 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); sd2506_get_time(&sd_time); /* 重新读取校准后的时间 */ } else { DBG_ERROR("sys_clock_init: RTC default write FAIL"); } } } #endif 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; DBG_INFO("sys_clock_init: RTC initial time %04d-%02d-%02d %02d:%02d:%02d", year, month, day, hour, minute, second); /* Step 2: Unix epoch → seconds(简化算法,适用于 1970~2038) */ { uint64_t total_days = 0; for (i = 1970; i < year; i++) { if ((i % 4 == 0 && i % 100 != 0) || (i % 400 == 0)) { total_days += 366uLL; } else { total_days += 365uLL; } } 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; for (i = 0; i < month - 1; i++) { total_days += mdays[i]; } total_days += day - 1uL; unix_secs = (int64_t)(total_days * 86400LL) + hour * 3600LL + minute * 60LL + second; } /* Step 3: 设置系统墙钟基准 */ s_base_tick = HAL_GetTick(); s_base_unix_secs = (uint32_t)unix_secs; 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 0; } /* * 函数功能:获取当前系统墙钟时间(Unix 时间戳) * 入口参数:无 * 返回值:当前 Unix 秒数 uint32_t 未初始化时返回 UINT32_MAX * 限定条件:sys_clock_init() 已成功调用 * 函数说明:1. 以基准 Tick 与基准 Unix 秒数为起点,按当前 HAL Tick 与 1000ms * 的整除差推算流逝秒数(避免每毫秒累加,减少漂移与溢出风险) * 2. 若推算结果为负(基准未建立),返回 UINT32_MAX 表示无效 */ uint32_t sys_clock_get(void) { uint32_t now_tick = HAL_GetTick(); int64_t unix_secs = (int64_t)s_base_unix_secs + (int64_t)((uint32_t)(now_tick - s_base_tick) / 1000uL); 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 + 2000uL, sd.month, sd.day, sd.hour, sd.minute, sd.second); return buf; } /* * 函数功能:设置(更新)系统墙钟时间基准,并可写回 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) { sd2506_set_time(sd_time); DBG_INFO("sys_clock_set: SD2506 RTC written"); } return 0; } 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); }