From 565f23ea1845f86b607068aa4f20d354c1581863 Mon Sep 17 00:00:00 2001
From: wandering1 <1624155937@qq.com>
Date: Fri, 28 Aug 2026 21:59:04 +0800
Subject: [PATCH] =?UTF-8?q?=E6=97=B6=E9=92=9F=E6=B5=8B=E8=AF=95=E5=AE=8C?=
=?UTF-8?q?=E6=AF=95?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Drivers/BSP/SD2506/sd2506.c | 100 ++++++------
MDK-ARM/build_log.txt | 86 +++++-----
docs/SD2506_Test_Guide.md | 309 ++++++++++--------------------------
test/sd2506_test.h | 3 +-
test/sd2506_test_task.c | 83 ++++++++++
test/test_config.h | 2 +-
6 files changed, 259 insertions(+), 324 deletions(-)
diff --git a/Drivers/BSP/SD2506/sd2506.c b/Drivers/BSP/SD2506/sd2506.c
index 1737a24..e86eace 100644
--- a/Drivers/BSP/SD2506/sd2506.c
+++ b/Drivers/BSP/SD2506/sd2506.c
@@ -30,8 +30,8 @@ uint8_t sd2506_bcd_to_dec(uint8_t bcd) {
}
/*
- * 函数功能:十进制�?BCD �? * 入口参数:dec - 十进制数�? uint8_t 0 - 99
- * �?�?值:BCD 编码�? * 限定条件:输入值在 0 - 99 范围�? * 函数说明:十位存入高 4 位,个位存入�?4 �? */
+ * 函数功能:十进制转 BCD 码
+ * 入口参数:dec - 十进制数 uint8_t 0 - 99 */
uint8_t sd2506_dec_to_bcd(uint8_t dec) {
return ((dec / 10) << 4) | (dec % 10);
}
@@ -177,17 +177,17 @@ int sd2506_init(void) {
return ret;
}
- /* 上电重置充电寄存�?18H = 82H (开启充�? 5K电阻)
- * 手册强烈建议每次上电时重置此�?*/
+ /* 上电重置充电寄存器 18H = 82H (开启充电 5K电阻)
+ * 手册强烈建议每次上电时重置此寄存器 */
ret = sd2506_write_reg(SD2506_REG_CHARGE, 0x82U);
if (ret != SD2506_OK) {
return ret;
}
- /* 配置: 24小时�? 自动复位使能
- * 0FH = 0x20 (bit5=ARST=1, 其它标志位清�?
- * 写入时需注意: 写允许状态下 0FH �?WRTC 位必须为1
- * 此处直接写入 0x20 即可 (ARST=1, �?WRTC �? */
+ /* 配置: 24小时制,写允许状态下 0FH 的 WRTC 位必须为 1
+ * 0FH = 0x20 (bit5=ARST=1, 其它标志位清零)
+ * 注意: 写入时需注意 0FH 的 WRTC 位必须为 1
+ * 此处直接写入 0x20 即可 (ARST=1, 的 WRTC 位) */
ret = sd2506_write_reg(SD2506_REG_CTR1, SD2506_CTR1_WRITE_OFF);
if (ret != SD2506_OK) {
return ret;
@@ -197,13 +197,13 @@ int sd2506_init(void) {
}
/*
- * 函数功能:设�?RTC 时间日期
+ * 函数功能:设置 RTC 时间日期
* 入口参数:p_time - 时间结构体指针,包含要设置的时间
- * 返回值:0 - 成功�?2 - I2C 错误
- * 限定条件:sd2506_init() 已调�?
- * 函数说明�?. 先开写保�?
- * 2. 一次性写�?7 字节时间数据 (00H~06H)
- * 3. 关闭写保�?
+ * 返回值:0 - 成功,2 - I2C 错误
+ * 限定条件:sd2506_init() 已调用成功
+ * 函数说明:1. 先开写保护
+ * 2. 一次性写入 7 字节时间数据 (00H~06H)
+ * 3. 关闭写保护
*/
int sd2506_set_time(const sd2506_time_t *p_time) {
int ret = 0;
@@ -213,25 +213,25 @@ int sd2506_set_time(const sd2506_time_t *p_time) {
return SD2506_ERROR;
}
- /* 组装 7 字节时间数据 (BCD �? */
- buf[0] = sd2506_dec_to_bcd(p_time->second); /* 00H: �?*/
- buf[1] = sd2506_dec_to_bcd(p_time->minute); /* 01H: �?*/
- buf[2] = sd2506_dec_to_bcd(p_time->hour) | 0x80U; /* 02H: �?(bit7=1, 24小时�? */
+ /* 组装 7 字节时间数据 (BCD 码) */
+ buf[0] = sd2506_dec_to_bcd(p_time->second); /* 00H: 秒 */
+ buf[1] = sd2506_dec_to_bcd(p_time->minute); /* 01H: 分 */
+ buf[2] = sd2506_dec_to_bcd(p_time->hour) | 0x80U; /* 02H: 时(bit7=1, 24小时制) */
buf[3] = sd2506_dec_to_bcd(p_time->week); /* 03H: 星期 */
- buf[4] = sd2506_dec_to_bcd(p_time->day); /* 04H: �?*/
- buf[5] = sd2506_dec_to_bcd(p_time->month); /* 05H: �?*/
- buf[6] = sd2506_dec_to_bcd(p_time->year - 2000U); /* 06H: �?*/
+ buf[4] = sd2506_dec_to_bcd(p_time->day); /* 04H: 日 */
+ buf[5] = sd2506_dec_to_bcd(p_time->month); /* 05H: 月 */
+ buf[6] = sd2506_dec_to_bcd(p_time->year - 2000U); /* 06H: 年 */
/* 开启写保护 */
ret = sd2506_write_enable();
if (ret != SD2506_OK) return ret;
- /* 一次性写�?7 字节时间数据 (00H~06H)
- * 手册要求: 不可单独写某一个时间寄存器,否则可能引起错误进�?*/
+ /* 一次性写入 7 字节时间数据 (00H~06H)
+ * 手册要求: 不可单独写某一个时间寄存器,否则可能引起错误进入 */
ret = sd2506_write_regs(SD2506_REG_SEC, buf, 7);
if (ret != SD2506_OK) return ret;
- /* 关闭写保�?*/
+ /* 关闭写保护 */
ret = sd2506_write_disable();
if (ret != SD2506_OK) return ret;
@@ -254,20 +254,20 @@ int sd2506_get_time(sd2506_time_t *p_time) {
return SD2506_ERROR;
}
- /* 一次读�?7 字节时间数据 (00H~06H)
- * 手册说明: 读取时所有实时数据被锁存,避免错�?*/
+ /* 一次读取 7 字节时间数据 (00H~06H)
+ * 手册说明: 读取时所有实时数据被锁存,避免错误 */
ret = sd2506_read_regs(SD2506_REG_SEC, buf, 7);
if (ret != SD2506_OK) {
return ret;
}
- p_time->second = sd2506_bcd_to_dec(buf[0] & 0x7FU); /* 00H: �?*/
- p_time->minute = sd2506_bcd_to_dec(buf[1] & 0x7FU); /* 01H: �?*/
+ p_time->second = sd2506_bcd_to_dec(buf[0] & 0x7FU); /* 00H: 秒 */
+ p_time->minute = sd2506_bcd_to_dec(buf[1] & 0x7FU); /* 01H: 分 */
p_time->hour = sd2506_bcd_to_dec(buf[2] & 0x7FU); /* 02H: 屏蔽 bit7 (12/24标志) */
p_time->week = sd2506_bcd_to_dec(buf[3] & 0x07U); /* 03H: 星期 */
- p_time->day = sd2506_bcd_to_dec(buf[4] & 0x3FU); /* 04H: �?*/
- p_time->month = sd2506_bcd_to_dec(buf[5] & 0x1FU); /* 05H: �?*/
- p_time->year = sd2506_bcd_to_dec(buf[6]) + 2000U; /* 06H: �?*/
+ p_time->day = sd2506_bcd_to_dec(buf[4] & 0x3FU); /* 04H: 日 */
+ p_time->month = sd2506_bcd_to_dec(buf[5] & 0x1FU); /* 05H: 月 */
+ p_time->year = sd2506_bcd_to_dec(buf[6]) + 2000U; /* 06H: 年 */
return SD2506_OK;
}
@@ -294,12 +294,12 @@ int sd2506_get_temperature(int8_t *p_temp) {
return ret;
}
- /* bit7 为符号位, 其余为温度�?*/
+ /* bit7 为符号位, 其余为温度值 */
if (val & 0x80U) {
- /* 负温�? 取补�?*/
+ /* 负温度 取补码 */
*p_temp = (int8_t)(val | 0xF0U);
} else {
- /* 正温�?*/
+ /* 正温度 */
*p_temp = (int8_t)(val & 0x7FU);
}
@@ -324,17 +324,17 @@ int sd2506_get_battery_voltage(uint16_t *p_voltage) {
return SD2506_ERROR;
}
- /* 读取 1AH bit7 (BAT8_VAL) �?1BH (BAT_VL) */
+ /* 读取 1AH bit7 (BAT8_VAL) 与 1BH (BAT_VL) */
ret = sd2506_read_reg(SD2506_REG_CTR5, &val_high);
if (ret != SD2506_OK) return ret;
ret = sd2506_read_reg(SD2506_REG_BAT_VAL, &val_low);
if (ret != SD2506_OK) return ret;
- /* 组合 9 �? bit8=BAT8_VAL(1AH bit7), bit7~0=BAT_VL(1BH)
- * �?1AH=80H, 1BH=30H => 电压 = 0x130 = 304 => 3.04V => 3040mV */
+ /* 组合 9 位值 bit8=BAT8_VAL(1AH bit7), bit7~0=BAT_VL(1BH)
+ * 例: 1AH=80H, 1BH=30H => 电压 = 0x130 = 304 => 3.04V => 3040mV */
uint16_t raw = ((val_high & 0x80U) << 1) | val_low;
- *p_voltage = raw * 10; /* 转换为毫�?(raw 单位 0.01V) */
+ *p_voltage = raw * 10; /* 转换为毫伏 (raw 单位 0.01V) */
return SD2506_OK;
}
@@ -398,8 +398,8 @@ int sd2506_write_sram(uint8_t addr, const uint8_t *p_buf, uint8_t len) {
return SD2506_ERROR;
}
- /* SRAM 无需开写保护即可写�?(写保护仅�?00H~71H 有效, SRAM �?2CH~71H)
- * 但为安全起见, SRAM 写入也走写保护流�?*/
+ /* SRAM 无需开写保护即可写入 (写保护仅对 00H~71H 有效, SRAM 为 2CH~71H)
+ * 但为安全起见, SRAM 写入也走写保护流程 */
int ret = 0;
ret = sd2506_write_enable();
if (ret != SD2506_OK) return ret;
@@ -429,29 +429,29 @@ int sd2506_set_alarm(const sd2506_time_t *p_time, uint8_t mask) {
}
/* 组装 8 字节报警数据 (07H~0EH) */
- buf[0] = sd2506_dec_to_bcd(p_time->second) & 0x7FU; /* 07H: 秒报�?*/
- buf[1] = sd2506_dec_to_bcd(p_time->minute) & 0x7FU; /* 08H: 分报�?*/
- buf[2] = sd2506_dec_to_bcd(p_time->hour) & 0x3FU; /* 09H: 时报�?(最高位始终�?) */
+ buf[0] = sd2506_dec_to_bcd(p_time->second) & 0x7FU; /* 07H: 秒报 */
+ buf[1] = sd2506_dec_to_bcd(p_time->minute) & 0x7FU; /* 08H: 分报 */
+ buf[2] = sd2506_dec_to_bcd(p_time->hour) & 0x3FU; /* 09H: 时报(最高位始终为0) */
buf[3] = 0x00U; /* 0AH: 星期报警 */
- buf[4] = sd2506_dec_to_bcd(p_time->day) & 0x3FU; /* 0BH: 日报�?*/
- buf[5] = sd2506_dec_to_bcd(p_time->month) & 0x1FU; /* 0CH: 月报�?*/
- buf[6] = sd2506_dec_to_bcd(p_time->year - 2000U); /* 0DH: 年报�?*/
+ buf[4] = sd2506_dec_to_bcd(p_time->day) & 0x3FU; /* 0BH: 日报 */
+ buf[5] = sd2506_dec_to_bcd(p_time->month) & 0x1FU; /* 0CH: 月报 */
+ buf[6] = sd2506_dec_to_bcd(p_time->year - 2000U); /* 0DH: 年报 */
buf[7] = mask; /* 0EH: 报警允许 */
/* 开启写保护 */
ret = sd2506_write_enable();
if (ret != SD2506_OK) return ret;
- /* 写入报警寄存�?(07H~0EH) */
+ /* 写入报警寄存器 (07H~0EH) */
ret = sd2506_write_regs(SD2506_REG_AL_SEC, buf, 8);
if (ret != SD2506_OK) return ret;
- /* 使能报警中断: CTR2 �?INTAE=1, INTS1=0, INTS0=1, IM=1 (周期�? */
+ /* 使能报警中断: CTR2 中 INTAE=1, INTS1=0, INTS0=1, IM=1 (周期中断) */
ret = sd2506_write_reg(SD2506_REG_CTR2,
SD2506_CTR2_INTAE | SD2506_CTR2_INTS0 | SD2506_CTR2_IM);
if (ret != SD2506_OK) return ret;
- /* 关闭写保�?*/
+ /* 关闭写保护 */
ret = sd2506_write_disable();
if (ret != SD2506_OK) return ret;
@@ -471,7 +471,7 @@ int sd2506_clear_alarm(void) {
int ret = 0;
uint8_t val = 0;
- /* 读取 CTR1, ARST=1 时自动清�?INTAF */
+ /* 读取 CTR1, ARST=1 时自动清除 INTAF */
ret = sd2506_read_reg(SD2506_REG_CTR1, &val);
if (ret != SD2506_OK) {
return ret;
diff --git a/MDK-ARM/build_log.txt b/MDK-ARM/build_log.txt
index b6d4adb..c8e3665 100644
--- a/MDK-ARM/build_log.txt
+++ b/MDK-ARM/build_log.txt
@@ -1,76 +1,76 @@
*** Using Compiler 'V5.06 update 7 (build 960)', folder: 'C:\Keil_v5\ARM\ARMCC\Bin'
Rebuild target 'STM32F407-Demo'
assembling startup_stm32f407xx.s...
-compiling ringbuf.c...
compiling crc.c...
-compiling freertos.c...
-compiling i2c.c...
-compiling stm32f4xx_hal_tim_ex.c...
+compiling ringbuf.c...
+compiling stm32f4xx_it.c...
+compiling app_main.c...
compiling dma.c...
compiling stm32f4xx_hal_flash_ramfunc.c...
-compiling stm32f4xx_it.c...
-compiling main.c...
-compiling stm32f4xx_hal_dma_ex.c...
+compiling i2c.c...
+compiling sys_clock.c...
+compiling stm32f4xx_hal_timebase_tim.c...
compiling stm32f4xx_hal_gpio.c...
+compiling stm32f4xx_hal_dma_ex.c...
compiling stm32f4xx_hal_dma.c...
-compiling app_main.c...
+compiling stm32f4xx_hal_flash_ex.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 stm32f4xx_hal_flash.c...
+compiling gpio.c...
+compiling usart.c...
+compiling stm32f4xx_hal_tim_ex.c...
compiling net_task.c...
+compiling freertos.c...
+compiling stm32f4xx_hal_rcc_ex.c...
compiling rs485_task.c...
-compiling croutine.c...
+compiling main.c...
+compiling adc_task.c...
+compiling time_sync.c...
compiling lftpd_string.c...
+compiling croutine.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 stream_buffer.c...
compiling timers.c...
+compiling tasks.c...
compiling heap_4.c...
compiling port.c...
+compiling stm32f4xx_hal_pwr_ex.c...
compiling journal.c...
compiling map.c...
compiling ff.c...
-compiling stm32f4xx_hal_cortex.c...
-compiling rs485.c...
-compiling lftpd_io.c...
-compiling nand_ftl.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 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 system_stm32f4xx.c...
+compiling lftpd_io.c...
+compiling stm32f4xx_hal_cortex.c...
+compiling stm32f4xx_hal_i2c_ex.c...
+compiling stm32f4xx_hal_exti.c...
+compiling rs485.c...
+compiling tpafe5160.c...
+compiling sd2506.c...
compiling ch395f.c...
-compiling gd5f2gq5ue.c...
-compiling gd5f_test_task.c...
-compiling stm32f4xx_hal_i2c.c...
-compiling stm32f4xx_hal_uart.c...
+compiling nand_ftl.c...
compiling stm32f4xx_hal_spi.c...
+compiling lftpd_inet.c...
+compiling gd5f2gq5ue.c...
+compiling net_select.c...
+compiling stm32f4xx_hal_uart.c...
+compiling lftpd.c...
+compiling net_socket.c...
+compiling stm32f4xx_hal_i2c.c...
+compiling cmsis_os2.c...
+compiling ch395f_test_task.c...
+compiling net_test_task.c...
+compiling gd5f_test_task.c...
compiling storage_test_task.c...
compiling sd2506_test_task.c...
linking...
-Program Size: Code=61836 RO-data=3356 RW-data=420 ZI-data=51868
+Program Size: Code=64436 RO-data=3936 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
+Build Time Elapsed: 00:00:14
diff --git a/docs/SD2506_Test_Guide.md b/docs/SD2506_Test_Guide.md
index eaed8fe..589890b 100644
--- a/docs/SD2506_Test_Guide.md
+++ b/docs/SD2506_Test_Guide.md
@@ -1,252 +1,105 @@
-# SD2506 RTC 测试规范
+# SD2506_RTC 测试指南
+
+> 对应驱动:`Drivers/BSP/SD2506/sd2506.c`
+> 测试代码:`test/sd2506_test_task.c` + `test/sd2506_test.h`
## 1. 概述
-本文档定义 SD2506API-G 高精度温补实时时钟驱动(STM32F407ZGTx,I2C1 接口 PB6-SCL / PB7-SDA)的测试规范。RTC 为本地外设,测试**无需 PC 配合**(仅串口日志观察),覆盖时间读写、写保护(WRTC)解锁时序、跨重启持久化三大主题。测试按阶段(`TM-RTC-NN`)组织,每阶段含独立用例(`TC-RTC-NNN`)。
+本测试套件对 `sd2506.c` 做**黑盒功能自测**,只调用公共 API,不触碰驱动内部函数。
+每个用例通过 `SD2506_TEST_CHECK` 记录 pass/fail,最终由 `SD2506_TEST_REPORT` 汇总。
-> **背景**:本驱动曾出现"时间同步后写入不生效、重启回到旧时间"的缺陷。根因是 `sd2506_write_enable()` 的 WRTC 解锁时序与 SD2506API-G 实际位布局不符——写操作 I2C 返回 OK,但芯片因写保护未真正打开而**静默忽略**数据。本测试 Phase 2 用穷举探测法定位正确时序。
+历史背景:本驱动曾出现“时间写不进”问题,根因是寄存器宏写成 `0x0FH`(KEIL/ARMCC
+不识别 `H` 整数后缀,被编译为 0),导致 `SD2506_REG_CTR1` 实际等于 0(秒寄存器),
+`sd2506_write_enable()` 把 WRTC 写错地址、写保护永不解除。修正为 `0x0FU` 后恢复。
+详见 `docs/SD2506_Trap_Records.md`。
-### 设计原则
+## 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 |
+`test/test_config.h` 中一次只能启用一个 `TEST_SUITE_*`(互斥):
-### 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 网络端
+```c
+//#define TEST_SUITE_CH395F
+//#define TEST_SUITE_STORAGE
+//#define TEST_SUITE_GD5F
+#define TEST_SUITE_RTC /* 启用 RTC 测试套件 */
```
-### 构建模式与执行顺序
+`test/sd2506_test.h` 中控制各阶段(默认全开):
-测试固件复用既有 `ch395fTestTask`(`StartCh395fTestTask`)调度框架,SD2506 作为独立 `TEST_SUITE_RTC` 套件接入,不新增 FreeRTOS 任务;三个相位顺序执行(Phase 1 → 2 → 3)。各相位由 `test/sd2506_test.h` 的 `ENABLE_RTC_*` 宏独立开关,套件整体由 `test/test_config.h` 的 `TEST_SUITE_RTC` 选择。
+```c
+#define ENABLE_RTC_BASIC_TESTS /* Phase 1: 基本读写 / 走时 */
+#define ENABLE_RTC_API_TESTS /* Phase 2: 全功能 API 覆盖 */
+```
-| 构建宏 | 相位 | 说明 |
-|--------|------|------|
-| `ENABLE_RTC_BASIC_TESTS` | Phase 1 | 公共 API 基本读写 / 走时 |
-| `ENABLE_RTC_WRTC_PROBE_TESTS` | Phase 2 | WRTC 解锁时序穷举探测 |
-| `ENABLE_RTC_PERSIST_TESTS` | Phase 3 | 跨重启持久化(需人工重启核验) |
+编译(要求 0 错误 0 警告):
-**执行顺序严格递增**:先确认 Phase 1(若 FAIL 即证明当前驱动写无效),再 Phase 2(定位正确 WRTC 时序),最后 Phase 3(用命中时序验证持久化)。
+```bat
+MDK-ARM\build.bat
+```
-### 启用测试
+烧录后在串口(USART1, 115200bps)观察 `[RTC_TEST]` 日志。
-本测试复用既有 `ch395fTestTask`(`StartCh395fTestTask`)调度框架,作为独立 `TEST_SUITE_RTC` 套件接入,不新增 FreeRTOS 任务。
+## 3. 测试用例
-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。
+| 编号 | 目标 | 覆盖函数 | 判定 |
+|------|------|----------|------|
+| TC-RTC-001 | 初始化并读取当前时间 | `sd2506_init` / `sd2506_get_time` | init 返回 0;get 返回 0 |
+| TC-RTC-002 | 写固定时间立即回读一致 | `sd2506_set_time` / `sd2506_get_time` | set==readback(允许 +1s 进位) |
+| TC-RTC-003 | 走时验证 | `sd2506_get_time` | 延时 3s 后秒数前进 2~5s |
+| TC-RTC-010 | BCD↔DEC 互转 | `sd2506_dec_to_bcd` / `sd2506_bcd_to_dec` | 10 组样本双向一致 |
+| TC-RTC-011 | 内部温度读取 | `sd2506_get_temperature` | 返回 0;值 ∈ [-40, 85] |
+| TC-RTC-012 | 电池电压(mV) | `sd2506_get_battery_voltage` | 返回 0;值 ∈ (2000, 5000) |
+| TC-RTC-013 | 芯片 8 字节 ID | `sd2506_get_id` | 返回 0;读出 8 字节 |
+| TC-RTC-014 | 用户 SRAM 回环 | `sd2506_write_sram` / `sd2506_read_sram` | 写入 8 字节后回读一致 |
+| TC-RTC-015 | 报警设置 + 清除 | `sd2506_set_alarm` / `sd2506_clear_alarm` | 两者均返回 0 |
----
+## 4. 覆盖矩阵
-## 2. 阶段 1:基本读写(公共 API)
+| 公共 API | 是否覆盖 | 用例 |
+|----------|----------|------|
+| `sd2506_init` | ✅ | TC-RTC-001 |
+| `sd2506_get_time` | ✅ | TC-RTC-001/002/003 |
+| `sd2506_set_time` | ✅ | TC-RTC-002 |
+| `sd2506_get_temperature` | ✅ | TC-RTC-011 |
+| `sd2506_get_battery_voltage` | ✅ | TC-RTC-012 |
+| `sd2506_get_id` | ✅ | TC-RTC-013 |
+| `sd2506_read_sram` | ✅ | TC-RTC-014 |
+| `sd2506_write_sram` | ✅ | TC-RTC-014 |
+| `sd2506_set_alarm` | ✅ | TC-RTC-015 |
+| `sd2506_clear_alarm` | ✅ | TC-RTC-015 |
+| `sd2506_read_ctr1` | △(诊断) | run() 内打印 CTR1 |
+| `sd2506_bcd_to_dec` / `sd2506_dec_to_bcd` | ✅ | TC-RTC-010 |
-| 阶段 ID | TM-RTC-01 |
-|---------|-----------|
-| **类型** | 独立运行 |
-| **耗时** | ~5 秒 |
-| **入口** | `StartCh395fTestTask → TEST_SUITE_RTC → sd2506_test_run()` → `sd2506_phase_basic()` |
-| **出口** | 串口 `Phase 1 basic: N/N PASSED (failed=0)` |
+> 未覆盖(需专项验证,不在本自测范围):倒计时寄存器、温度报警历史、
+> 跨重启持久化(原 Phase 3 已废弃,依赖实际掉电/上电,建议手动或另写用例)。
-### TC-RTC-001: 初始化与读取
+## 5. 运行与判定
-| 字段 | 值 |
-|------|-----|
-| **ID** | TC-RTC-001 |
-| **优先级** | P0 |
-| **类型** | 功能测试 |
-| **标题** | 验证 `sd2506_init()` 通信正常并可读取当前时间 |
-| **前置条件** | 1. I2C1 已初始化
2. 未启用其他会并发访问 I2C 的测试 |
+- `sd2506_test_run()` 由测试调度器调用,先 init 并记录原始时间,最后用
+ `sd2506_set_time` 把板子时间**恢复**为测试前的值(不污染真实时钟)。
+- 输出形如:
+ ```
+ [RTC_TEST] === SD2506 Phase 1: basic read/write (public API) ===
+ [RTC_TEST] [PASS] sd2506_init() ret=0 (expect 0)
+ ...
+ [RTC_TEST] === Phase 1 basic: 6/6 PASSED (failed=0) ===
+ [RTC_TEST] === SD2506 Phase 2: full API coverage ===
+ ...
+ [RTC_TEST] === Phase 2 API: 9/9 PASSED (failed=0) ===
+ ```
+- 通过标准:所有 Phase 的 `failed=0`,且总量 `X/Y PASSED` 中 Y 与 PASS 数相等。
-**测试步骤与判定标准**:
+## 6. 失败排查
-| 步 | 操作 | 成功标准 | 失败标准 |
-|----|------|----------|----------|
-| 1 | `sd2506_init()` | 返回 `0`(`SD2506_OK`) | 返回 `-2`(`SD2506_I2C_ERROR`,I2C 通信失败/芯片未响应) |
-| 2 | `sd2506_get_time(&r)` 读取当前时间 | 返回 `0` 且 `r.year` 落在 `2000~2099`、月/日/时/分/秒为合法 BCD 范围 | 返回非零,或字段越界(如月>12、秒>59) |
+- **TC-RTC-002 FAIL(set≠readback)**:优先查 `sd2506.h` 中各寄存器宏是否被写成
+ `0xNNH`(H 后缀陷阱)。例如 `SD2506_REG_CTR1` 必须是 `0x0FU` 而非 `0x0FH`。
+- **init/get/set 返回 -2(I2C 错误)**:查 I2C1 接线(PB6-SCL / PB7-SDA)、
+ 上拉电阻、芯片供电与器件地址 `0x32`。
+- **温度/电压越界**:多为芯片未上电或 I2C 读到全 0xFF,按 I2C 链路排查。
-**通过标准**:2/2 通过,0 失败。任一失败 → 阶段标记 FAIL
+## 7. 已知限制
-**覆盖原则**: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 |
+- TC-RTC-015 会让 `sd2506_set_alarm` 置 `CTR2.INTAE=1`(报警中断允许)。
+ 测试固件未挂 INT 中断处理,且 INT 引脚在板上未连接,对运行无影响;
+ 若要完全复位,可手动再调用一次 `set_alarm(..., mask=0)`。
+- SRAM 回环会在 `addr=0` 写入测试数据(8 字节),属用户 SRAM 区域,不影响时间/控制寄存器。
diff --git a/test/sd2506_test.h b/test/sd2506_test.h
index 9929c8b..153084a 100644
--- a/test/sd2506_test.h
+++ b/test/sd2506_test.h
@@ -9,8 +9,7 @@ extern "C" {
/* ============ Phase 使能开关(取消注释即启用,默认全开) ============ */
#define ENABLE_RTC_BASIC_TESTS /* Phase 1: 基本读写 / 走时 */
-#define ENABLE_RTC_WRTC_PROBE_TESTS /* Phase 2: WRTC 解锁时序探测(定位正确时序) */
-#define ENABLE_RTC_PERSIST_TESTS /* Phase 3: 跨重启持久化 */
+#define ENABLE_RTC_API_TESTS /* Phase 2: 全功能 API 覆盖(BCD互转/温度/电压/ID/SRAM/报警) */
/* ============ 测试统计(与 STORAGE/GD5F 套件一致的独立统计) ============ */
typedef struct {
diff --git a/test/sd2506_test_task.c b/test/sd2506_test_task.c
index 5c01e25..4212ab4 100644
--- a/test/sd2506_test_task.c
+++ b/test/sd2506_test_task.c
@@ -92,6 +92,85 @@ static void sd2506_phase_basic(void)
}
#endif /* ENABLE_RTC_BASIC_TESTS */
+/* ===================== Phase 2:全功能 API 覆盖 ===================== */
+#ifdef ENABLE_RTC_API_TESTS
+static void sd2506_phase_api(void)
+{
+ DBG_INFO("=== SD2506 Phase 2: full API coverage ===");
+
+ /* TC-RTC-010: BCD<->DEC 互转(纯算法,确定性,无需硬件交互) */
+ int conv_ok = 1;
+ uint8_t samples[][2] = {
+ {0, 0x00U}, {1, 0x01U}, {9, 0x09U}, {10, 0x10U}, {23, 0x23U},
+ {59, 0x59U}, {60, 0x60U}, {99, 0x99U}, {42, 0x42U}, {88, 0x88U}
+ };
+ for (uint32_t i = 0U; i < (sizeof(samples) / sizeof(samples[0])); i++) {
+ uint8_t dec = samples[i][0];
+ uint8_t bcd = samples[i][1];
+ if (sd2506_dec_to_bcd(dec) != bcd) {
+ conv_ok = 0;
+ DBG_ERROR(" dec_to_bcd(%u) != 0x%02X", (unsigned int)dec, (unsigned int)bcd);
+ }
+ if (sd2506_bcd_to_dec(bcd) != dec) {
+ conv_ok = 0;
+ DBG_ERROR(" bcd_to_dec(0x%02X) != %u", (unsigned int)bcd, (unsigned int)dec);
+ }
+ }
+ SD2506_TEST_CHECK(conv_ok, "bcd<->dec round-trip (10 samples)");
+
+ /* TC-RTC-011: 内部温度读取 */
+ int8_t temp = 0;
+ int ret = sd2506_get_temperature(&temp);
+ SD2506_TEST_CHECK(ret == SD2506_OK, "sd2506_get_temperature() ret=%d (expect 0)", ret);
+ if (ret == SD2506_OK) {
+ DBG_INFO(" temperature: %d C", (int)temp);
+ SD2506_TEST_CHECK(temp >= -40 && temp <= 85,
+ "temperature in range [-40,85]: %d", (int)temp);
+ }
+
+ /* TC-RTC-012: 电池电压(毫伏) */
+ uint16_t volt = 0U;
+ ret = sd2506_get_battery_voltage(&volt);
+ SD2506_TEST_CHECK(ret == SD2506_OK, "sd2506_get_battery_voltage() ret=%d (expect 0)", ret);
+ if (ret == SD2506_OK) {
+ DBG_INFO(" battery: %u mV", (unsigned int)volt);
+ SD2506_TEST_CHECK(volt > 2000U && volt < 5000U,
+ "battery voltage plausible: %u mV", (unsigned int)volt);
+ }
+
+ /* TC-RTC-013: 芯片 8 字节 ID */
+ uint8_t id[8] = {0};
+ ret = sd2506_get_id(id);
+ SD2506_TEST_CHECK(ret == SD2506_OK, "sd2506_get_id() ret=%d (expect 0)", ret);
+ if (ret == SD2506_OK) {
+ DBG_INFO(" id: %02X%02X%02X%02X%02X%02X%02X%02X",
+ id[0], id[1], id[2], id[3], id[4], id[5], id[6], id[7]);
+ }
+
+ /* TC-RTC-014: 用户 SRAM 读写回环(addr=0, len=8) */
+ uint8_t wbuf[8] = {0xA1U, 0xB2U, 0xC3U, 0xD4U, 0xE5U, 0xF6U, 0x07U, 0x18U};
+ uint8_t rbuf[8] = {0};
+ ret = sd2506_write_sram(0U, wbuf, 8U);
+ SD2506_TEST_CHECK(ret == SD2506_OK, "sd2506_write_sram() ret=%d (expect 0)", ret);
+ if (ret == SD2506_OK) {
+ ret = sd2506_read_sram(0U, rbuf, 8U);
+ SD2506_TEST_CHECK(ret == SD2506_OK, "sd2506_read_sram() ret=%d (expect 0)", ret);
+ int sram_ok = (memcmp(wbuf, rbuf, 8U) == 0);
+ SD2506_TEST_CHECK(sram_ok, "SRAM write==readback");
+ }
+
+ /* TC-RTC-015: 报警设置 + 清除(验证 API 通路,不影响时间寄存器) */
+ sd2506_time_t at;
+ at.year = 2026; at.month = 8; at.day = 28; at.hour = 12; at.minute = 0; at.second = 30; at.week = 4;
+ ret = sd2506_set_alarm(&at, (uint8_t)(SD2506_AL_EN_EAS | SD2506_AL_EN_EAMN));
+ SD2506_TEST_CHECK(ret == SD2506_OK, "sd2506_set_alarm() ret=%d (expect 0)", ret);
+ ret = sd2506_clear_alarm();
+ SD2506_TEST_CHECK(ret == SD2506_OK, "sd2506_clear_alarm() ret=%d (expect 0)", ret);
+
+ SD2506_TEST_REPORT("Phase 2 API");
+}
+#endif /* ENABLE_RTC_API_TESTS */
+
/* ===================== 套件入口 ===================== */
void sd2506_test_run(void)
{
@@ -122,6 +201,10 @@ void sd2506_test_run(void)
sd2506_phase_basic();
#endif
+#ifdef ENABLE_RTC_API_TESTS
+ sd2506_phase_api();
+#endif
+
/* 恢复原始时间(用公共 API,写保护逻辑已由 sd2506.c 正确处理) */
if (have_orig) {
int r = sd2506_set_time(&orig);
diff --git a/test/test_config.h b/test/test_config.h
index 9cbe648..f16839e 100644
--- a/test/test_config.h
+++ b/test/test_config.h
@@ -26,7 +26,7 @@ extern "C" {
//#define TEST_SUITE_CH395F
//#define TEST_SUITE_STORAGE
//#define TEST_SUITE_GD5F
-#define TEST_SUITE_RTC
+//#define TEST_SUITE_RTC
#if defined(TEST_SUITE_CH395F) + defined(TEST_SUITE_STORAGE) + defined(TEST_SUITE_GD5F) + defined(TEST_SUITE_RTC) > 1
#error "测试套件互斥:一次构建只能启用一个 TEST_SUITE_*"