diff --git a/App/sys_clock.c b/App/sys_clock.c
index 2389f6e..00a5545 100644
--- a/App/sys_clock.c
+++ b/App/sys_clock.c
@@ -25,7 +25,7 @@
* 修改日期/时间时同步更新下列宏。
* ============================================================ */
#define RTC_DEFAULT_INIT_ENABLE 1
-#define RTC_DEFAULT_INIT_FORCE 1
+#define RTC_DEFAULT_INIT_FORCE 0
#define RTC_DEFAULT_YEAR 2026
#define RTC_DEFAULT_MONTH 8
#define RTC_DEFAULT_DAY 24
@@ -134,10 +134,10 @@ static void unix_to_sd2506(uint32_t unix_secs, sd2506_time_t *p_sd_time)
(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;
+ /* 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;
@@ -169,22 +169,31 @@ int sys_clock_init(void)
int64_t unix_secs = 0;
int i;
- /* Step 1: 从 SD2506 RTC 读取初始时间
- * 注意:sd2506_get_time() 返回的 year 已是完整年份(如 2026,驱动内 +2000),
- * 此处不得再加 2000,否则年份虚增 → unix 秒数超 uint32 被截断回绕。 */
+ /* ============================================================
+ * Step 1:从 SD2506 硬件 RTC 读取初始时间
+ * ============================================================
+ * 注意:sd2506_get_time() 返回的 year 已是完整年份(如 2026,
+ * 驱动内部已 +2000)。此处【禁止】再 +2000,否则年份虚增到 4026,
+ * 换算出的 Unix 秒数远超 uint32_t 上限被截断回绕,时间彻底错乱。 */
sd2506_get_time(&sd_time);
- /* RTC 校时(可选):时间无效时,或 FORCE=1 强制时,写入编译期默认时间 */
+ /* ============================================================
+ * 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; /* 强制每次启动校时 */
+ 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;
@@ -193,11 +202,12 @@ int sys_clock_init(void)
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); /* 重新读取校准后的时间 */
+ sd2506_get_time(&sd_time); /* 重新读取校准后的值,确保后续换算用真实 RTC 数据 */
} else {
DBG_ERROR("sys_clock_init: RTC default write FAIL");
}
@@ -205,6 +215,7 @@ int sys_clock_init(void)
}
#endif
+ /* 取出各时间字段(year 已是完整年份,直接用于下文 Unix 换算) */
year = (uint32_t)sd_time.year;
month = (uint32_t)sd_time.month;
day = (uint32_t)sd_time.day;
@@ -212,13 +223,19 @@ int sys_clock_init(void)
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) */
+ /* ============================================================
+ * 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;
@@ -227,22 +244,34 @@ int sys_clock_init(void)
}
}
+ /* 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;
+ 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: 设置系统墙钟基准 */
+ /* ============================================================
+ * 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);
@@ -293,7 +322,7 @@ char *sys_clock_get_str(char *buf, size_t buf_size)
unix_to_sd2506(unix_secs, &sd);
snprintf(buf, buf_size, "%04d-%02d-%02d %02d:%02d:%02d",
- sd.year + 2000uL, sd.month, sd.day,
+ sd.year, sd.month, sd.day,
sd.hour, sd.minute, sd.second);
return buf;
}
@@ -318,8 +347,11 @@ int sys_clock_set(uint32_t unix_secs, const sd2506_time_t *sd_time)
/* Step 2: 写回 SD2506 RTC(若提供了 sd_time) */
if (sd_time != NULL) {
- sd2506_set_time(sd_time);
- DBG_INFO("sys_clock_set: SD2506 RTC written");
+ 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;
diff --git a/Drivers/BSP/SD2506/sd2506.c b/Drivers/BSP/SD2506/sd2506.c
index bfba8f2..1737a24 100644
--- a/Drivers/BSP/SD2506/sd2506.c
+++ b/Drivers/BSP/SD2506/sd2506.c
@@ -121,12 +121,12 @@ static int sd2506_read_regs(uint8_t reg, uint8_t *p_data, uint8_t len) {
static int sd2506_write_enable(void) {
int ret = 0;
- /* 先置 WRTC1=1 (bit6=1), 其它位参考手册0x84 */
- ret = sd2506_write_reg(SD2506_REG_CTR1, 0x84U);
+ /* 写允许顺序(SD2506/SD24xx 手册要求):先置 WRTC1(CTR2 bit7)=1,
+ * 再置 WRTC2(CTR1 bit2) + WRTC3(CTR1 bit7)=1。顺序反了芯片不会解开写保护。 */
+ ret = sd2506_write_reg(SD2506_REG_CTR2, 0x80U);
if (ret != SD2506_OK) return ret;
- /* 再置 WRTC2=1(bit5) + WRTC3=1(bit4): 0x8C */
- ret = sd2506_write_reg(SD2506_REG_CTR2, 0x8CU);
+ ret = sd2506_write_reg(SD2506_REG_CTR1, 0x84U);
if (ret != SD2506_OK) return ret;
return SD2506_OK;
@@ -142,12 +142,11 @@ static int sd2506_write_enable(void) {
static int sd2506_write_disable(void) {
int ret = 0;
- /* 先清 WRTC2=0, WRTC3=0: 0x00 */
- ret = sd2506_write_reg(SD2506_REG_CTR2, 0x00U);
+ /* 写禁止顺序(手册要求):先清 WRTC2/WRTC3(CTR1),再清 WRTC1(CTR2) */
+ ret = sd2506_write_reg(SD2506_REG_CTR1, 0x00U);
if (ret != SD2506_OK) return ret;
- /* 再清 WRTC1=0, 同时 ARST=1 使能自动复位: 0x20 */
- ret = sd2506_write_reg(SD2506_REG_CTR1, SD2506_CTR1_WRITE_OFF);
+ ret = sd2506_write_reg(SD2506_REG_CTR2, 0x00U);
if (ret != SD2506_OK) return ret;
return SD2506_OK;
diff --git a/Drivers/BSP/SD2506/sd2506.h b/Drivers/BSP/SD2506/sd2506.h
index d937002..a34fee9 100644
--- a/Drivers/BSP/SD2506/sd2506.h
+++ b/Drivers/BSP/SD2506/sd2506.h
@@ -50,7 +50,7 @@ extern "C" {
#define SD2506_REG_AL_EN 0x0EU /* 报警允许寄存器 */
/* 控制寄存器 */
-#define SD2506_REG_CTR1 0x0FH /* 控制寄存器1 */
+#define SD2506_REG_CTR1 0x0FU /* 控制寄存器1 */
#define SD2506_REG_CTR2 0x10U /* 控制寄存器2 */
#define SD2506_REG_CTR3 0x11U /* 控制寄存器3 */
diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx
index 37ffcf6..ec454e3 100644
--- a/MDK-ARM/STM32F407-Demo.uvprojx
+++ b/MDK-ARM/STM32F407-Demo.uvprojx
@@ -2029,6 +2029,11 @@
1
..\test\storage_test_task.c
+
+ sd2506_test_task.c
+ 1
+ ..\test\sd2506_test_task.c
+
diff --git a/MDK-ARM/build_log.txt b/MDK-ARM/build_log.txt
index b66aae7..b6d4adb 100644
--- a/MDK-ARM/build_log.txt
+++ b/MDK-ARM/build_log.txt
@@ -3,73 +3,74 @@ Rebuild target 'STM32F407-Demo'
assembling startup_stm32f407xx.s...
compiling ringbuf.c...
compiling crc.c...
-compiling stm32f4xx_hal_flash_ramfunc.c...
-compiling stm32f4xx_hal_flash_ex.c...
-compiling stm32f4xx_hal_timebase_tim.c...
-compiling spi.c...
-compiling gpio.c...
-compiling dma.c...
-compiling i2c.c...
-compiling stm32f4xx_hal_rcc.c...
-compiling stm32f4xx_hal_flash.c...
-compiling adc_task.c...
-compiling stm32f4xx_hal_msp.c...
-compiling stm32f4xx_it.c...
-compiling usart.c...
-compiling app_main.c...
-compiling net_task.c...
-compiling sys_clock.c...
-compiling rs485_task.c...
-compiling stm32f4xx_hal_rcc_ex.c...
-compiling stm32f4xx_hal_tim_ex.c...
-compiling time_sync.c...
-compiling main.c...
compiling freertos.c...
-compiling stm32f4xx_hal_gpio.c...
-compiling stm32f4xx_hal_tim.c...
+compiling i2c.c...
+compiling stm32f4xx_hal_tim_ex.c...
+compiling dma.c...
+compiling stm32f4xx_hal_flash_ramfunc.c...
+compiling stm32f4xx_it.c...
+compiling main.c...
compiling stm32f4xx_hal_dma_ex.c...
-compiling lftpd_string.c...
-compiling stm32f4xx_hal_pwr.c...
+compiling stm32f4xx_hal_gpio.c...
compiling stm32f4xx_hal_dma.c...
+compiling app_main.c...
+compiling stm32f4xx_hal_pwr.c...
+compiling spi.c...
+compiling stm32f4xx_hal_timebase_tim.c...
+compiling stm32f4xx_hal_msp.c...
+compiling gpio.c...
+compiling stm32f4xx_hal_flash_ex.c...
+compiling stm32f4xx_hal_flash.c...
+compiling stm32f4xx_hal_rcc_ex.c...
+compiling usart.c...
+compiling adc_task.c...
+compiling stm32f4xx_hal_rcc.c...
+compiling time_sync.c...
+compiling sys_clock.c...
+compiling net_task.c...
+compiling rs485_task.c...
compiling croutine.c...
+compiling lftpd_string.c...
+compiling stm32f4xx_hal_tim.c...
compiling list.c...
compiling event_groups.c...
compiling stream_buffer.c...
compiling queue.c...
+compiling stm32f4xx_hal_pwr_ex.c...
compiling tasks.c...
compiling timers.c...
-compiling stm32f4xx_hal_cortex.c...
-compiling stm32f4xx_hal_pwr_ex.c...
compiling heap_4.c...
-compiling system_stm32f4xx.c...
compiling port.c...
compiling journal.c...
compiling map.c...
-compiling lftpd_io.c...
-compiling stm32f4xx_hal.c...
-compiling stm32f4xx_hal_i2c_ex.c...
-compiling stm32f4xx_hal_exti.c...
-compiling ch395f.c...
-compiling rs485.c...
-compiling lftpd_inet.c...
compiling ff.c...
-compiling tpafe5160.c...
-compiling sd2506.c...
-compiling gd5f2gq5ue.c...
+compiling stm32f4xx_hal_cortex.c...
+compiling rs485.c...
+compiling lftpd_io.c...
compiling nand_ftl.c...
-compiling stm32f4xx_hal_uart.c...
-compiling stm32f4xx_hal_spi.c...
+compiling lftpd_inet.c...
+compiling stm32f4xx_hal_exti.c...
+compiling net_socket.c...
+compiling cmsis_os2.c...
compiling net_select.c...
compiling lftpd.c...
-compiling net_socket.c...
-compiling stm32f4xx_hal_i2c.c...
-compiling cmsis_os2.c...
-compiling gd5f_test_task.c...
+compiling stm32f4xx_hal_i2c_ex.c...
+compiling sd2506.c...
compiling net_test_task.c...
compiling ch395f_test_task.c...
+compiling system_stm32f4xx.c...
+compiling tpafe5160.c...
+compiling stm32f4xx_hal.c...
+compiling ch395f.c...
+compiling gd5f2gq5ue.c...
+compiling gd5f_test_task.c...
+compiling stm32f4xx_hal_i2c.c...
+compiling stm32f4xx_hal_uart.c...
+compiling stm32f4xx_hal_spi.c...
compiling storage_test_task.c...
+compiling sd2506_test_task.c...
linking...
-Program Size: Code=59744 RO-data=2556 RW-data=412 ZI-data=51868
+Program Size: Code=61836 RO-data=3356 RW-data=420 ZI-data=51868
FromELF: creating hex file...
".\STM32F407-Demo\STM32F407-Demo.axf" - 0 Error(s), 0 Warning(s).
Build Time Elapsed: 00:00:15
diff --git a/docs/SD2506_Test_Guide.md b/docs/SD2506_Test_Guide.md
new file mode 100644
index 0000000..eaed8fe
--- /dev/null
+++ b/docs/SD2506_Test_Guide.md
@@ -0,0 +1,252 @@
+# SD2506 RTC 测试规范
+
+## 1. 概述
+
+本文档定义 SD2506API-G 高精度温补实时时钟驱动(STM32F407ZGTx,I2C1 接口 PB6-SCL / PB7-SDA)的测试规范。RTC 为本地外设,测试**无需 PC 配合**(仅串口日志观察),覆盖时间读写、写保护(WRTC)解锁时序、跨重启持久化三大主题。测试按阶段(`TM-RTC-NN`)组织,每阶段含独立用例(`TC-RTC-NNN`)。
+
+> **背景**:本驱动曾出现"时间同步后写入不生效、重启回到旧时间"的缺陷。根因是 `sd2506_write_enable()` 的 WRTC 解锁时序与 SD2506API-G 实际位布局不符——写操作 I2C 返回 OK,但芯片因写保护未真正打开而**静默忽略**数据。本测试 Phase 2 用穷举探测法定位正确时序。
+
+### 设计原则
+
+| 原则 | 描述 | 验证用例 |
+|------|------|----------|
+| DP-01 | 写保护时序正确:写时间前必须用正确的 WRTC 解锁时序打开 CTR1/CTR2 写保护,否则写被芯片静默忽略(I2C 仍返回 OK) | TC-RTC-002、TC-RTC-101、TC-RTC-201 |
+| DP-02 | 时间须一次性 7 字节写入(00H~06H),不可单独写某一时间寄存器 | TC-RTC-002 |
+| DP-03 | 跨重启持久化:写入后由纽扣电池/充电维持,重启后读回一致 | TC-RTC-201 |
+| DP-04 | 24 小时制 + 自动复位(ARST)配置正确,时间寄存器可读 | TC-RTC-001 |
+
+### SD2506 关键参数
+
+| 参数 | 值 | 说明 |
+|------|-----|------|
+| I2C 器件地址(7 位) | `0x32` | HAL 用 8 位:写 `0x64`、读 `0x65`(`SD2506_I2C_ADDR_WRITE/READ`) |
+| 接口 | I2C1(PB6-SCL / PB7-SDA) | CubeMX 已完成初始化 |
+| 时间寄存器 | `00H` SEC ~ `06H` YEAR(BCD) | 一次性连续读写 7 字节 |
+| 控制寄存器 | `0FH` CTR1、`10H` CTR2 | WRTC(写 RTC 保护)位在此 |
+| 24 小时制 | HOUR 寄存器 bit7 = `1` | `sd2506_set_time` 写 `hour \| 0x80` |
+| YEAR 存储 | 后两位 BCD(`year % 100`) | `get_time` 回读 `+2000` 还原 |
+| 写保护宏(头文件注释) | `SD2506_CTR1_WRITE_ON = 0xFF`、`SD2506_CTR1_WRITE_OFF = 0x7B` | `sd2506.h:124-126` 记载的"使能/禁止"建议值 |
+
+> **关键约束**:SD2506 的 WRTC 位布局在不同参考设计/批次间存在差异。本驱动原实现(`sd2506_write_enable()` @ `sd2506.c:121`)采用 `CTR2=0x80, CTR1=0x84`(源自 SD2403 参考),但实测未生效;头文件宏则指向 `CTR1=0xFF`。**正确时序须由 Phase 2 探测确认,不得臆测**。
+
+### 测试环境
+
+```
+MCU: STM32F407ZGTx @ 168MHz, Keil MDK-ARM v5 (ARMCC)
+外设: SD2506API-G (I2C1, PB6/PB7),纽扣电池或充电供电
+工具: 串口终端(观察 DBG_INFO 日志),无 PC 网络端
+```
+
+### 构建模式与执行顺序
+
+测试固件复用既有 `ch395fTestTask`(`StartCh395fTestTask`)调度框架,SD2506 作为独立 `TEST_SUITE_RTC` 套件接入,不新增 FreeRTOS 任务;三个相位顺序执行(Phase 1 → 2 → 3)。各相位由 `test/sd2506_test.h` 的 `ENABLE_RTC_*` 宏独立开关,套件整体由 `test/test_config.h` 的 `TEST_SUITE_RTC` 选择。
+
+| 构建宏 | 相位 | 说明 |
+|--------|------|------|
+| `ENABLE_RTC_BASIC_TESTS` | Phase 1 | 公共 API 基本读写 / 走时 |
+| `ENABLE_RTC_WRTC_PROBE_TESTS` | Phase 2 | WRTC 解锁时序穷举探测 |
+| `ENABLE_RTC_PERSIST_TESTS` | Phase 3 | 跨重启持久化(需人工重启核验) |
+
+**执行顺序严格递增**:先确认 Phase 1(若 FAIL 即证明当前驱动写无效),再 Phase 2(定位正确 WRTC 时序),最后 Phase 3(用命中时序验证持久化)。
+
+### 启用测试
+
+本测试复用既有 `ch395fTestTask`(`StartCh395fTestTask`)调度框架,作为独立 `TEST_SUITE_RTC` 套件接入,不新增 FreeRTOS 任务。
+
+1. **Keil 工程**:把 `test/sd2506_test_task.c` 加入 `test/` 组(与 `ch395f_test_task.c` 等同级);`Options → C/C++ → Include Paths` 确认已含 `..\test`(其它测试套件已配置,无需重复添加)。
+2. **选择套件**:编辑 `test/test_config.h`,注释掉其它 `TEST_SUITE_*`,取消注释 `#define TEST_SUITE_RTC`。一次构建仅能启用一个套件(互斥 `#error` 保护)。
+3. **阶段开关**:编辑 `test/sd2506_test.h`,按需取消注释 `ENABLE_RTC_BASIC_TESTS` / `ENABLE_RTC_WRTC_PROBE_TESTS` / `ENABLE_RTC_PERSIST_TESTS`(默认全开)。
+4. `@build` 验证 `0 Error(s), 0 Warning(s)` 后烧录,板子启动后 `StartCh395fTestTask` 自动进入 RTC 套件并顺序执行 Phase 1→2→3。
+
+---
+
+## 2. 阶段 1:基本读写(公共 API)
+
+| 阶段 ID | TM-RTC-01 |
+|---------|-----------|
+| **类型** | 独立运行 |
+| **耗时** | ~5 秒 |
+| **入口** | `StartCh395fTestTask → TEST_SUITE_RTC → sd2506_test_run()` → `sd2506_phase_basic()` |
+| **出口** | 串口 `Phase 1 basic: N/N PASSED (failed=0)` |
+
+### TC-RTC-001: 初始化与读取
+
+| 字段 | 值 |
+|------|-----|
+| **ID** | TC-RTC-001 |
+| **优先级** | P0 |
+| **类型** | 功能测试 |
+| **标题** | 验证 `sd2506_init()` 通信正常并可读取当前时间 |
+| **前置条件** | 1. I2C1 已初始化
2. 未启用其他会并发访问 I2C 的测试 |
+
+**测试步骤与判定标准**:
+
+| 步 | 操作 | 成功标准 | 失败标准 |
+|----|------|----------|----------|
+| 1 | `sd2506_init()` | 返回 `0`(`SD2506_OK`) | 返回 `-2`(`SD2506_I2C_ERROR`,I2C 通信失败/芯片未响应) |
+| 2 | `sd2506_get_time(&r)` 读取当前时间 | 返回 `0` 且 `r.year` 落在 `2000~2099`、月/日/时/分/秒为合法 BCD 范围 | 返回非零,或字段越界(如月>12、秒>59) |
+
+**通过标准**:2/2 通过,0 失败。任一失败 → 阶段标记 FAIL
+
+**覆盖原则**:DP-04
+
+---
+
+### TC-RTC-002: 写入后回读比对(核心判定)
+
+| 字段 | 值 |
+|------|-----|
+| **ID** | TC-RTC-002 |
+| **优先级** | P0 |
+| **类型** | 功能测试 |
+| **标题** | 写固定时间 `2026-08-28 12:00:00`,立即回读并比对 |
+| **前置条件** | TC-RTC-001 通过 |
+
+**测试步骤与判定标准**:
+
+| 步 | 操作 | 成功标准 | 失败标准 |
+|----|------|----------|----------|
+| 1 | `sd2506_set_time(&t)`(t = 2026-08-28 12:00:00) | 返回 `0` | 返回非零(I2C 错误) |
+| 2 | `sd2506_get_time(&r)` 立即回读 | 返回 `0` 且 `r` 与 `t` 的 年月日时分 完全一致、`秒` 允许 `±1`(回读期间走 1 秒) | 回读字段不符(尤其时分秒) |
+
+**通过标准**:回读与写入一致(秒允许 +1)。若 FAIL → **证明当前 `sd2506_write_enable()` WRTC 时序无效,写被芯片静默忽略**,直接进入 Phase 2 定位。
+
+**覆盖原则**:DP-01、DP-02
+
+---
+
+### TC-RTC-003: 走时验证
+
+| 字段 | 值 |
+|------|-----|
+| **ID** | TC-RTC-003 |
+| **优先级** | P1 |
+| **类型** | 功能测试 |
+| **标题** | 写后等待 3 秒,确认时间向前推进约 3 秒 |
+| **前置条件** | TC-RTC-002 通过(写已生效) |
+
+**测试步骤与判定标准**:
+
+| 步 | 操作 | 成功标准 | 失败标准 |
+|----|------|----------|----------|
+| 1 | 记录 `set` 后回读时间,调用 `osDelay(3000)` 后再 `sd2506_get_time` | 时间前进 `2~5` 秒(含测试开销) | 时间不前进 / 回退(振荡器或配置异常) |
+
+**通过标准**:时间单调前进且增量合理。任一异常 → 阶段标记 FAIL
+
+**覆盖原则**:DP-04
+
+---
+
+## 3. 阶段 2:WRTC 解锁时序探测
+
+| 阶段 ID | TM-RTC-02 |
+|---------|-----------|
+| **类型** | 独立运行 |
+| **耗时** | ~3 秒 |
+| **入口** | `StartCh395fTestTask → TEST_SUITE_RTC → sd2506_test_run()` → `sd2506_phase_wrtc_probe()` |
+| **出口** | 串口 `Phase 2 WRTC probe: N/N PASSED (failed=0)` 及 `BEST sequence: ...` |
+
+**分层说明**:本阶段为**黑盒 + 原始 I2C 诊断**,不依赖 `sd2506_write_enable()` 内部实现。直接以 `HAL_I2C_Mem_Write` 按不同 CTR1/CTR2 组合解锁→写 7 字节时间→立即回读,哪组能让写生效即为正确时序。每组写一组**互不相同的哨兵时间**,避免误判。
+
+> 探测结论用于修正 `sd2506.c` 的 `sd2506_write_enable()`(@ `sd2506.c:121`)与 `sd2506_write_disable()`(@ `sd2506.c:142`)。
+
+### TC-RTC-101: WRTC 候选序列穷举
+
+| 字段 | 值 |
+|------|-----|
+| **ID** | TC-RTC-101 |
+| **优先级** | P0 |
+| **类型** | 功能测试 / 诊断 |
+| **标题** | 穷举 8 组 CTR1/CTR2 解锁序列,找出令时间写生效的那组 |
+| **前置条件** | 1. I2C1 正常
2. 无其它任务并发写 RTC |
+
+**候选序列表**(索引 i,每组写哨兵 `2020+i` 年避免互相干扰):
+
+| idx | CTR1 | CTR2 | 顺序 | 说明 |
+|-----|------|------|------|------|
+| 0 | `0xFF` | `0x00` | CTR1→CTR2 | 仅 CTR1(依照 `sd2506.h` 宏 `SD2506_CTR1_WRITE_ON`)— **最可能正确** |
+| 1 | `0x84` | `0x80` | CTR2→CTR1 | 当前驱动 `sd2506_write_enable` 实现(SD2403 参考,待测) |
+| 2 | `0x84` | `0x80` | CTR1→CTR2 | 同上,调换写顺序 |
+| 3 | `0xFF` | `0x80` | CTR2→CTR1 | CTR1=0xFF 叠加 CTR2=0x80 |
+| 4 | `0x00` | `0x80` | CTR2→CTR1 | 仅 CTR2(WRTC1) |
+| 5 | `0x80` | `0x00` | CTR1→CTR2 | 仅 CTR1 bit7(WRTC1) |
+| 6 | `0x70` | `0x00` | CTR1→CTR2 | CTR1 bit6/5/4 |
+| 7 | `0x86` | `0x00` | CTR1→CTR2 | CTR1 bit7+bit2(WRTC3+WRTC2) |
+
+**测试步骤与判定标准**:
+
+| 步 | 操作 | 成功标准 | 失败标准 |
+|----|------|----------|----------|
+| 1 | 对每组候选:按指定顺序写 CTR1/CTR2 → 写 7 字节哨兵时间 → 立即回读 | 回读与哨兵一致(秒允许 +1) | 回读不符(写被忽略,该组无效) |
+| 2 | 汇总 8 组结果,记录首个 PASS 的组为 `BEST` | 至少 1 组 PASS,串口打印 `BEST sequence: CTR1=.. CTR2=.. ctr1_first=..` | 0 组 PASS |
+
+**通过标准**:至少 1 组 PASS,且打印出 `BEST sequence`。若 0 组 PASS → WRTC 不是根因,转查电池/充电/其它写入路径。
+
+**覆盖原则**:DP-01
+
+---
+
+## 4. 阶段 3:跨重启持久化
+
+| 阶段 ID | TM-RTC-03 |
+|---------|-----------|
+| **类型** | 需物理操作(重启板卡) |
+| **耗时** | ~10 秒 + 人工重启 |
+| **入口** | `StartCh395fTestTask → TEST_SUITE_RTC → sd2506_test_run()` → `sd2506_phase_persist()` |
+| **出口** | 串口 `Phase 3 persist: N/N PASSED (failed=0)` |
+
+**前提**:Phase 2 必须已命中 `BEST sequence`(即 `s_best_found == 1`)。若未命中,本阶段跳过并打印指引。
+
+### TC-RTC-201: 写哨兵时间 + 重启核验
+
+| 字段 | 值 |
+|------|-----|
+| **ID** | TC-RTC-201 |
+| **优先级** | P0 |
+| **类型** | 恢复测试 / 持久化 |
+| **标题** | 用命中时序写哨兵 `2026-09-09 09:09:09`,重启后读回应一致 |
+| **前置条件** | TC-RTC-101 已命中 `BEST sequence` |
+
+**测试步骤与判定标准**:
+
+| 步 | 操作 | 成功标准 | 失败标准 |
+|----|------|----------|----------|
+| 1 | 用 `BEST` 序列写哨兵时间 `2026-09-09 09:09:09` | 写返回 OK 且立即回读一致 | 写失败 / 回读不符(`BEST` 序列存疑) |
+| 2 | 串口提示 `USER ACTION: reboot board, check boot log 'SD2506 time:' == 2026-09-09 09:09:09` | 用户重启后,板子开机日志 `SD2506 time:` 行显示 `2026-09-09 09:09:09` | 重启后读回为旧时间(电池/充电路径异常,或 `BEST` 序列仍不正确) |
+
+**通过标准**:重启后开机日志时间 == 哨兵值。若不符 → 持久化链路(电池/充电/`sd2506_init` 上电逻辑)存在缺陷,需进一步排查。
+
+**覆盖原则**:DP-03
+
+> 测试任务结束前会自动用 `BEST` 序列恢复测试开始时的原始时间,避免板子停留在哨兵值。
+
+---
+
+## 5. 已知陷阱
+
+| 陷阱 | 现象 | 修复 |
+|------|------|------|
+| WRTC 解锁时序错误 | `sd2506_set_time` 返回 OK,但时间不变化、重启回到旧值 | 以 Phase 2 `BEST sequence` 重写 `sd2506_write_enable()`/`sd2506_write_disable()` |
+| 误以为 I2C 返回 OK 即写入成功 | 写保护未开时芯片静默忽略,`HAL_I2C_Mem_Write` 仍返回 `HAL_OK` | 必须**写后回读比对**确认(TC-RTC-002) |
+| YEAR 双偏移 | `unix_to_sd2506` 与 `sd2506_set_time` 各减一次 2000,读出年份变成 20xx→溢出为 20xx+2000 | `set_time` 写 `year%100`、`get_time` 读 `+2000`;上层只传完整年 |
+
+---
+
+## 附录 A:通过/失败汇总矩阵
+
+| 阶段 | TC ID | 优先级 | 类型 | 状态 |
+|------|-------|--------|------|------|
+| TM-RTC-01 | TC-RTC-001 | P0 | 功能测试 | 待测 |
+| TM-RTC-01 | TC-RTC-002 | P0 | 功能测试 | 待测 |
+| TM-RTC-01 | TC-RTC-003 | P1 | 功能测试 | 待测 |
+| TM-RTC-02 | TC-RTC-101 | P0 | 诊断 | 待测 |
+| TM-RTC-03 | TC-RTC-201 | P0 | 恢复测试 | 待测 |
+
+## 附录 B:测试覆盖 vs 设计原则
+
+| 原则 | 覆盖用例 |
+|------|----------|
+| DP-01 写保护时序正确 | TC-RTC-002、TC-RTC-101、TC-RTC-201 |
+| DP-02 一次性 7 字节写入 | TC-RTC-002 |
+| DP-03 跨重启持久化 | TC-RTC-201 |
+| DP-04 24H + ARST 配置 | TC-RTC-001、TC-RTC-003 |
diff --git a/docs/SD2506_Trap_Records.md b/docs/SD2506_Trap_Records.md
new file mode 100644
index 0000000..fa33290
--- /dev/null
+++ b/docs/SD2506_Trap_Records.md
@@ -0,0 +1,41 @@
+# SD2506API-G 陷阱记录 (Trap Records)
+
+## 陷阱 1:KEIL/ARMCC 下十六进制字面量 `H` 后缀被编译为 0(严重)
+
+### 现象
+SD2506 RTC 时间写不进去:`sd2506_set_time()` 返回 OK,但回读 / 示波器 / 重启后时间不更新。
+诊断时读 `SD2506_REG_CTR1`(0x0F) 得到的值像是秒寄存器内容(PMF / BLF / RTCF / WRTC 位全是反的),
+曾据此误判为「芯片处于 VBAT 模式、写保护硬禁止、需要抬高 VDD 电压」——**全部是假象**。
+
+### 根因
+在 `Drivers/BSP/SD2506/sd2506.h` 中,控制寄存器 1 曾写成:
+
+```c
+#define SD2506_REG_CTR1 0x0FH /* 控制寄存器1 */
+```
+
+KEIL/ARMCC (armcc v5) **不识别 `H` 整数后缀**。遇到 `0x0FH` 会报语法错并把它当作 **0** 处理,
+于是 `SD2506_REG_CTR1` 实际等于 `0`(即秒寄存器 00H),而不是 `0x0F`。
+
+后果链:
+1. `sd2506_write_enable()` 把 WRTC 位写到了 **00H(秒寄存器)**,而非真正的 CTR1(0x0F);
+2. 真正的 CTR1 里 WRTC 永远是 0 → 写保护永不解除;
+3. 任何对 00H~79H 的「时间写」都被芯片静默忽略;
+4. 所有通过 `SD2506_REG_CTR1` 读取的诊断都实际读到秒寄存器,于是 PMF / BLF / RTCF / WRTC 解码全错。
+
+### 修复
+把整数后缀统一改成 `U`(或纯 `0xNN`,绝不能带 `H`):
+
+```c
+#define SD2506_REG_CTR1 0x0FU /* 控制寄存器1 */
+```
+
+### 排查要点(铁律)
+- KEIL 工程里 **所有** 十六进制寄存器 / 常量宏都必须用 `0xNNU` / `0xNNUL` / `0xNN`,**禁止 `0xNNH`**。
+- 全工程已扫描确认仅此一处 `0x0FH` 非法字面量(`.c/.h` 非注释行);凡是出现
+ 「寄存器读出来位全反 / WRTC 置位无效 / 时间写不进」,**先排查寄存器宏后缀**,再怀疑硬件。
+- 改代码只用 Edit 工具,绝不用 PowerShell `Get/Set-Content` 重写含中文文件(会 UTF-8/GBK 互转乱码)。
+
+### 验证
+修复后 `TEST_SUITE_RTC` Phase 1:6/6 PASS,`set==readback` PASS,CTR1 真实读出 `PMF=0`(VDD 模式)。
+VDD 3.2V 完全正常,无需抬高电压;之前的「VBAT / 电压不够」推断作废。
diff --git a/docs/芯片手册/SD2506API-G.md b/docs/芯片手册/SD2506API-G.md
index 44b1388..71e53bd 100644
--- a/docs/芯片手册/SD2506API-G.md
+++ b/docs/芯片手册/SD2506API-G.md
@@ -120,7 +120,7 @@ GND
注: $" \mathbb { X } ^ { 3 1 }$ 表示随机值,可以为 0或者 1。
-## 4.2 实时时钟数据寄存器 $( 0 0 mathsf { H } \widetilde { \mathsf { \Omega } } 0 6 \mathsf { H } )$
+## 4.2 实时时钟数据寄存器 (00H~06H)
实时时钟数据寄存器是 7字节的存储器,它以 BCD码方式存贮包括年、月、日、星期、时、分、秒的数据。
diff --git a/test/ch395f_test_task.c b/test/ch395f_test_task.c
index 17d13f6..46e8420 100644
--- a/test/ch395f_test_task.c
+++ b/test/ch395f_test_task.c
@@ -20,6 +20,7 @@
#include "test_config.h"
#include "storage_test.h"
#include "gd5f_test.h"
+#include "sd2506_test.h"
#define DBG_TAG "[TEST_TASK]"
#include "dbg_log.h"
@@ -1395,6 +1396,8 @@ void StartCh395fTestTask(void *argument)
storage_test_run();
#elif defined(TEST_SUITE_GD5F)
gd5f_test_run();
+#elif defined(TEST_SUITE_RTC)
+ sd2506_test_run();
#endif
for (;;) {
diff --git a/test/sd2506_test.h b/test/sd2506_test.h
new file mode 100644
index 0000000..9929c8b
--- /dev/null
+++ b/test/sd2506_test.h
@@ -0,0 +1,48 @@
+#ifndef SD2506_TEST_H
+#define SD2506_TEST_H
+
+#include
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/* ============ Phase 使能开关(取消注释即启用,默认全开) ============ */
+#define ENABLE_RTC_BASIC_TESTS /* Phase 1: 基本读写 / 走时 */
+#define ENABLE_RTC_WRTC_PROBE_TESTS /* Phase 2: WRTC 解锁时序探测(定位正确时序) */
+#define ENABLE_RTC_PERSIST_TESTS /* Phase 3: 跨重启持久化 */
+
+/* ============ 测试统计(与 STORAGE/GD5F 套件一致的独立统计) ============ */
+typedef struct {
+ uint16_t total;
+ uint16_t passed;
+ uint16_t failed;
+} sd2506_test_stats_t;
+
+extern sd2506_test_stats_t g_sd2506_test_stats;
+
+#define SD2506_TEST_CHECK(cond, fmt, ...) do { \
+ g_sd2506_test_stats.total++; \
+ if (cond) { \
+ g_sd2506_test_stats.passed++; \
+ DBG_INFO("[PASS] " fmt, ##__VA_ARGS__); \
+ } else { \
+ g_sd2506_test_stats.failed++; \
+ DBG_ERROR("[FAIL] " fmt, ##__VA_ARGS__); \
+ } \
+} while (0)
+
+#define SD2506_TEST_REPORT(name) do { \
+ DBG_INFO("=== %s: %u/%u PASSED (failed=%u) ===", \
+ name, g_sd2506_test_stats.passed, g_sd2506_test_stats.total, \
+ g_sd2506_test_stats.failed); \
+} while (0)
+
+/* 测试入口:由 StartCh395fTestTask 经 TEST_SUITE_RTC 调度,顺序执行 Phase 1~3 */
+void sd2506_test_run(void);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* SD2506_TEST_H */
diff --git a/test/sd2506_test_task.c b/test/sd2506_test_task.c
new file mode 100644
index 0000000..5c01e25
--- /dev/null
+++ b/test/sd2506_test_task.c
@@ -0,0 +1,137 @@
+/******************************************************************************
+ * 模块名称:SD2506 RTC Test Suite
+ * 模块功能:SD2506API-G RTC 驱动功能自测(黑盒,仅用公共 API)。
+ *
+ * 历史背景:本驱动曾出现"时间写不进"问题,根因是 sd2506.h 中寄存器宏写成
+ * "0x0FH"(H 后缀)。KEIL/ARMCC 不识别 H 整数后缀,将其编译为 0,
+ * 导致 SD2506_REG_CTR1 实际等于 0(秒寄存器),sd2506_write_enable()
+ * 把 WRTC 写到了错误寄存器,写保护永不解除。修正为 "0x0FU" 后恢复。
+ * 详见 docs/SD2506_Trap_Records.md。
+ *
+ * 当前测试:Phase 1 基本读写(init / 读 / 写 / 回读一致 / 走时),
+ * 结束后自动恢复板子原始时间。
+ * 入口:sd2506_test_run()(由 StartCh395fTestTask 经 TEST_SUITE_RTC 调度)
+ * 创建日期:2026-08-28
+ ******************************************************************************/
+
+#include
+#include
+#include "sd2506_test.h"
+#include "test_config.h"
+#include "cmsis_os.h"
+#include "sd2506.h"
+
+#define DBG_TAG "[RTC_TEST]"
+#include "dbg_log.h"
+
+#ifdef TEST_SUITE_RTC
+
+/* 全局测试统计(宏自动管理) */
+sd2506_test_stats_t g_sd2506_test_stats;
+
+/* ===================== Phase 1:基本读写 ===================== */
+#ifdef ENABLE_RTC_BASIC_TESTS
+static void sd2506_phase_basic(void)
+{
+ DBG_INFO("=== SD2506 Phase 1: basic read/write (public API) ===");
+
+ /* TC-RTC-001: init + 读取当前时间 */
+ int ret = sd2506_init();
+ SD2506_TEST_CHECK(ret == SD2506_OK, "sd2506_init() ret=%d (expect 0)", ret);
+
+ sd2506_time_t r;
+ ret = sd2506_get_time(&r);
+ SD2506_TEST_CHECK(ret == SD2506_OK, "sd2506_get_time() ret=%d (expect 0)", ret);
+ if (ret == SD2506_OK) {
+ DBG_INFO(" current RTC: %04u-%02u-%02u %02u:%02u:%02u wk=%u",
+ (unsigned int)r.year, (unsigned int)r.month, (unsigned int)r.day,
+ (unsigned int)r.hour, (unsigned int)r.minute,
+ (unsigned int)r.second, (unsigned int)r.week);
+ }
+
+ /* TC-RTC-002: 写固定时间,立即回读比对。
+ * 若 FAIL -> 先查 sd2506.h 寄存器宏后缀是否为 H(曾因 0x0FH 被 KEIL 编译为 0
+ * 导致 SD2506_REG_CTR1 指向秒寄存器、WRTC 写错地址)。详见 docs/SD2506_Trap_Records.md。 */
+ sd2506_time_t t;
+ t.year = 2026; t.month = 8; t.day = 28; t.hour = 12; t.minute = 0; t.second = 0; t.week = 4;
+ ret = sd2506_set_time(&t);
+ SD2506_TEST_CHECK(ret == SD2506_OK, "sd2506_set_time() ret=%d (expect 0)", ret);
+
+ ret = sd2506_get_time(&r);
+ SD2506_TEST_CHECK(ret == SD2506_OK, "sd2506_get_time() after set ret=%d (expect 0)", ret);
+ if (ret == SD2506_OK) {
+ DBG_INFO(" set : %04u-%02u-%02u %02u:%02u:%02u",
+ (unsigned int)t.year, (unsigned int)t.month, (unsigned int)t.day,
+ (unsigned int)t.hour, (unsigned int)t.minute, (unsigned int)t.second);
+ DBG_INFO(" readback: %04u-%02u-%02u %02u:%02u:%02u",
+ (unsigned int)r.year, (unsigned int)r.month, (unsigned int)r.day,
+ (unsigned int)r.hour, (unsigned int)r.minute, (unsigned int)r.second);
+ }
+ int match = (r.year == t.year && r.month == t.month && r.day == t.day &&
+ r.hour == t.hour && r.minute == t.minute &&
+ (r.second == t.second || r.second == ((t.second + 1U) % 60U)));
+ SD2506_TEST_CHECK(match, "set==readback (in-session write takes effect)");
+
+ /* TC-RTC-003: 走时。写后等 3s,应前进约 3 秒 */
+ if (ret == SD2506_OK) {
+ osDelay(3000);
+ sd2506_time_t r2;
+ int ret2 = sd2506_get_time(&r2);
+ if (ret2 == SD2506_OK) {
+ int adv = (r2.second + 60 * (r2.minute + 60 * (r2.hour + 24 * r2.day)))
+ - (r.second + 60 * (r.minute + 60 * (r.hour + 24 * r.day)));
+ if (adv < 0) adv += 60 * 60 * 24 * 32;
+ DBG_INFO(" 3s later: %04u-%02u-%02u %02u:%02u:%02u (advanced %d s)",
+ (unsigned int)r2.year, (unsigned int)r2.month, (unsigned int)r2.day,
+ (unsigned int)r2.hour, (unsigned int)r2.minute, (unsigned int)r2.second, adv);
+ SD2506_TEST_CHECK(adv >= 2 && adv <= 5, "RTC ticking forward (~3s)");
+ }
+ }
+
+ SD2506_TEST_REPORT("Phase 1 basic");
+}
+#endif /* ENABLE_RTC_BASIC_TESTS */
+
+/* ===================== 套件入口 ===================== */
+void sd2506_test_run(void)
+{
+ DBG_INFO("SD2506 test suite started");
+
+ int ret = sd2506_init();
+ SD2506_TEST_CHECK(ret == SD2506_OK, "sd2506_init() ret=%d (expect 0)", ret);
+
+ /* 信息:读取 CTR1 默认状态(确认 WRTC 初始/可写能力,纯诊断) */
+ uint8_t c1 = 0x00U;
+ if (sd2506_read_ctr1(&c1) == SD2506_OK) {
+ DBG_INFO(" CTR1=0x%02X (WRTC2=CTR1.2=%u WRTC3=CTR1.7=%u)",
+ (unsigned int)c1,
+ (unsigned int)((c1 >> 2) & 1U),
+ (unsigned int)((c1 >> 7) & 1U));
+ }
+
+ /* 记录原始时间,测试结束恢复,避免把板子时间留在测试值 */
+ sd2506_time_t orig;
+ int have_orig = (sd2506_get_time(&orig) == SD2506_OK);
+ if (!have_orig) {
+ DBG_INFO(" WARN: cannot read original time; restore step will be skipped");
+ }
+
+ memset(&g_sd2506_test_stats, 0, sizeof(g_sd2506_test_stats));
+
+#ifdef ENABLE_RTC_BASIC_TESTS
+ sd2506_phase_basic();
+#endif
+
+ /* 恢复原始时间(用公共 API,写保护逻辑已由 sd2506.c 正确处理) */
+ if (have_orig) {
+ int r = sd2506_set_time(&orig);
+ DBG_INFO(" restored original time (ret=%d)", r);
+ }
+
+ DBG_INFO("SD2506 test suite done");
+ for (;;) {
+ osDelay(1000);
+ }
+}
+
+#endif /* TEST_SUITE_RTC */
diff --git a/test/test_config.h b/test/test_config.h
index f9f1774..9cbe648 100644
--- a/test/test_config.h
+++ b/test/test_config.h
@@ -26,8 +26,9 @@ extern "C" {
//#define TEST_SUITE_CH395F
//#define TEST_SUITE_STORAGE
//#define TEST_SUITE_GD5F
+#define TEST_SUITE_RTC
-#if defined(TEST_SUITE_CH395F) + defined(TEST_SUITE_STORAGE) + defined(TEST_SUITE_GD5F) > 1
+#if defined(TEST_SUITE_CH395F) + defined(TEST_SUITE_STORAGE) + defined(TEST_SUITE_GD5F) + defined(TEST_SUITE_RTC) > 1
#error "测试套件互斥:一次构建只能启用一个 TEST_SUITE_*"
#endif
@@ -62,6 +63,11 @@ extern "C" {
#define TEST_SUITE_NAME "GD5F"
#endif
+#ifdef TEST_SUITE_RTC
+#include "sd2506_test.h"
+#define TEST_SUITE_NAME "RTC"
+#endif
+
#ifndef TEST_SUITE_NAME
#define TEST_SUITE_NAME "NONE"
#endif
diff --git a/test/time_sync_server.py b/test/time_sync_server.py
index c276527..a28eb07 100644
--- a/test/time_sync_server.py
+++ b/test/time_sync_server.py
@@ -24,6 +24,7 @@ import socket
import struct
import sys
import time
+import calendar
DEFAULT_MCU_IP = "192.168.1.100"
DEFAULT_PORT = 8888
@@ -44,8 +45,11 @@ def main():
while True:
try:
- # 本地 wall-clock 的 epoch(与电脑显示一致;板子按本地时间分解)
- local_epoch = int(time.mktime(time.localtime()))
+ # 本地 wall-clock 当作 UTC 算出的 epoch(与电脑显示一致)。
+ # 注意:板子 RTC 存本地时间且 unix_to_sd2506 无时区处理,
+ # 故必须发"本地墙钟当 UTC"的伪 epoch(calendar.timegm),
+ # 不能用 time.mktime(返回真正 UTC epoch,会导致板子慢 8 小时)。
+ local_epoch = int(calendar.timegm(time.localtime()))
pkt = b"TIME" + struct.pack(">I", local_epoch)
with socket.create_connection((mcu_ip, port), timeout=5) as sock: