通用配置文件 P1–P3:设计文档、FS 并发 A+ 定案、FatFs 墙钟接入、cfg_file 双端引擎与 net_cfg 集成(旧档 NCFG 首启迁移)

This commit is contained in:
2026-09-14 02:15:49 +08:00
parent cf2597f998
commit 97661415ca
29 changed files with 3076 additions and 359 deletions
+396
View File
@@ -0,0 +1,396 @@
/*
* 模块名称: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/(改归 ④中间件层:通用平台时间服务,多服务共用)
*/
#include "sys_clock.h"
#include "main.h"
#include <string.h>
#include <stdio.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;
/* ============================================================
* 内部辅助函数: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) {
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);
/* 星期(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_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 - 成功(始终返回 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,否则年份虚增到 4026,
* 换算出的 Unix 秒数远超 uint32_t 上限被截断回绕,时间彻底错乱。 */
sd2506_get_time(&sd_time);
/* ============================================================
* Step 1.1:开发期校时(可选,由 RTC_DEFAULT_INIT_* 宏控制)
* ============================================================
* 作用:RTC 出厂/掉电异常导致时间非法时,写入一个可用初始值;
* FORCE=1 时每次启动都强制写入,便于首次烧录或硬件时钟丢失。
* 判定"时间无效":年份越界、月/日越界(仅粗判,不细校 2 月 29 日等)。 */
#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;
/* 用编译期默认宏构造初始时间;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);
sd2506_get_time(&sd_time); /* 重新读取校准后的值,确保后续换算用真实 RTC 数据 */
} else {
DBG_ERROR("sys_clock_init: RTC default write FAIL");
}
}
}
#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 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, 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);
}
+88
View File
@@ -0,0 +1,88 @@
/*
* 模块名称:System Clock — Wall Clock Time Maintenance
* 模块功能:基于 HAL Tick + SD2506 RTC 的系统墙钟时间维护
* 启动时从 SD2506 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/(改归 ④中间件层:通用平台时间服务,多服务共用)
*/
#ifndef SYS_CLOCK_H
#define SYS_CLOCK_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#include "sd2506.h"
/*
* 函数功能:系统时钟初始化(从 SD2506 RTC 读取初始时间)
* 入口参数:无
* 返回值:0 - 成功, -1 - 失败
* 限定条件:sd2506_init() 已调用
* 函数说明:读取 SD2506 RTC 当前值转换为 Unix timestamp,设为系统墙钟初始时间
*/
int sys_clock_init(void);
/*
* 函数功能:获取当前系统墙钟时间(Unix epoch seconds)
* 入口参数:无
* 返回值:Unix time (秒),失败返回 UINT32_MAX
* 限定条件:sys_clock_init() 已调用
* 函数说明:基于 HAL Tick 计数 + 上次校准时刻的 Unix 秒数计算当前时间,
* 精度为毫秒级
*/
uint32_t sys_clock_get(void);
/*
* 函数功能:格式化系统墙钟时间为字符串
* 入口参数:buf - 输出缓冲区(至少 20 字节)
* buf_size - 缓冲区大小
* 返回值:buf 指针,失败返回 NULL
* 限定条件:sys_clock_init() 已调用
* 函数说明:格式 "YYYY-MM-DD HH:MM:SS"
*/
char *sys_clock_get_str(char *buf, size_t buf_size);
/*
* 函数功能:获取当前系统墙钟时间(日历时间)
* 入口参数:p_time - 输出时间结构指针(非 NULL)
* 出口参数:p_time - 填充年/月/日/时/分/秒/星期
* 返回值:0 - 成功, -1 - 失败(墙钟未初始化或参数无效)
* 限定条件:sys_clock_init() 已调用
* 函数说明:等价于 "sys_clock_get() + 内部日历换算",为需要日历字段的
* 使用方(如 FatFs 时间钩子)提供类型化接口
*/
int sys_clock_get_time(sd2506_time_t *p_time);
/*
* 函数功能:设置系统墙钟时间
* 入口参数:unix_secs - Unix epoch time (秒)
* sd_time - SD2506 RTC 时间结构体(可选,非 NULL 时同时写入 RTC)
* 返回值:0 - 成功, -1 - 失败
* 限定条件:sys_clock_init() 已调用
* 函数说明:更新系统墙钟基准 + 写回 SD2506 RTC 保持长期准确性
*/
int sys_clock_set(uint32_t unix_secs, const sd2506_time_t *sd_time);
/*
* 函数功能:以 Unix 时间戳设置系统墙钟并写回 SD2506 RTC
* 入口参数:unix_secs - Unix epoch time (秒,本地时间) uint32_t 0~0x7FFFFFFF
* 返回值:0 - 成功
* 限定条件:sys_clock_init() 已调用
* 函数说明:内部将 Unix 秒数转换为 sd2506_time_t 后调用 sys_clock_set(),
* 便于时间同步模块(如 PC 推送)直接以时间戳校时
*/
int sys_clock_set_unix(uint32_t unix_secs);
#ifdef __cplusplus
}
#endif
#endif /* SYS_CLOCK_H */