From 484d0682508475c70d5b0491cf68cdb9a1352256 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Sun, 25 Oct 2026 09:23:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E5=AD=98=E5=82=A8):=20=E5=86=99=E5=9B=9E?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E7=A9=BA=E9=97=B2=20flush=20=E5=85=9C?= =?UTF-8?q?=E5=BA=95=E2=80=94=E2=80=94=E5=8D=B7=E7=A9=BA=E9=97=B2=E5=91=A8?= =?UTF-8?q?=E6=9C=9F=20CTRL=5FSYNC=EF=BC=88=E7=99=BB=E8=AE=B0=E7=B0=BF=20N?= =?UTF-8?q?-22=20=E6=94=B6=E5=8F=A3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 道F 批 2 / task-16。FTL 单页写回缓存(nand_ftl.c s_cache_dirty)与 dhara journal 只在换页/整页写/f_close 时刷出,漏网脏页的掉电驻留窗无上限。本 提交以最小面收口:不碰 GD5F/dhara 驱动,全部走既有 FatFs CTRL_SYNC 路径。 实现 App/fs/vol_idle_sync.[ch](新增,仅 MCU 端,uvprojx App 组注册): - 四级门:STORAGE_READY 就绪门 → 5 s 节流(无符号差回绕安全判据)→ vol_lock_wait(0) try(锁被持有=文件序列在做,跳过,不改 STOR 持有期 语义)→ disk_ioctl(0, CTRL_SYNC); - clean 态脏标志+journal 双短路、零 NAND 触点;脏态最坏 = 一次 f_close 的同步段同型成本,不引入新磨损来源; - 失败按边沿 latch 一条 DBG_ERROR、成功重新武装;失败路径也成对放锁; - TEST_SUITE_STORAGE 构建整体 no-op(卷由 testTask 独占)。 接线:defaultTask 主循环 syslog_drain 之后(default_task.c v1.3)。 文档:docs/架构设计/STM32F4-Base存储架构说明.md §5.5 补空闲兜底与边界; 本板无掉电检测硬件、不做掉电前强制 flush、勿当掉电保护——口径同步落 注释与文档(sys_log_mcu.c 两处旧「暂不做」注释随实现改口)。 测试:新主机单测 test/host/vol_idle_sync/(TC-VOLSYNC-001..012,19 断言 全过);scripts/id_registry.json 建 TC-HOST-VOLSYNC 族(仿 ADCCAL 先例); .pre-commit-config.yaml 加 vol-idle-sync-unit-test hook(只加自己条目)。 越界申报(①②③ 均 lead 批准,④ 为门禁连带必要): ① App/task/default_task.c 单点接线(include + 1 调用 + 头修改记录); ② docs/架构设计/STM32F4-Base存储架构说明.md §5.5; ③ .pre-commit-config.yaml 单 hook + scripts/id_registry.json 单族; ④ scripts/check_registration.py SIM_EXEMPT 加一行(vol_idle_sync.c 模拟器 侧豁免,registration-check GAP 所需;与批准③同类别但未列名,特此申报)。 体积增量(vs scripts/ram_baseline.tsv 2026-10-25 行): Flash(Code+RO) 237140→237328(+188 B)、RW 1224→1232(+8 B)、ZI 0; 增量 = 3 个文件级 static + 一处 DBG_ERROR 调用点;栈/堆无变化。 gate: only mem-budget/mem-numbers pending lead ledger backfill 全量 pre-commit --all-files:40 项 Passed,仅 mem-numbers(D-7 体积档)与 mem-budget 两条红 = 台账数字待回填(增量如上,属 lead 单写者禁区); 其余含 doc-links/id-check/registration/test-coverage/全部主机单测皆绿。 @build 0 错误 0 警告(Code=215932 RO=21396 RW=1232 ZI=131984)。 台架:空闲 sync 实机节奏(clean 零触点、脏态时长、无心跳扰动)属实际 测量采样类——本批跳过,留「待台架实测」。 --- .pre-commit-config.yaml | 15 + App/fs/vol_idle_sync.c | 125 ++++++++ App/fs/vol_idle_sync.h | 50 ++++ App/log/sys_log_mcu.c | 11 +- App/task/default_task.c | 9 + MDK-ARM/STM32F407-Demo.uvprojx | 5 + docs/架构设计/STM32F4-Base存储架构说明.md | 10 + scripts/check_registration.py | 1 + scripts/id_registry.json | 22 ++ test/host/vol_idle_sync/run_unit_test.py | 55 ++++ test/host/vol_idle_sync/stub/FreeRTOS.h | 29 ++ test/host/vol_idle_sync/stub/cmsis_os.h | 14 + test/host/vol_idle_sync/stub/cmsis_os2.h | 32 +++ test/host/vol_idle_sync/stub/dbg_log.h | 17 ++ test/host/vol_idle_sync/stub/task.h | 24 ++ test/host/vol_idle_sync/stub/test_config.h | 15 + test/host/vol_idle_sync/vol_idle_sync_test.c | 282 +++++++++++++++++++ 17 files changed, 712 insertions(+), 4 deletions(-) create mode 100644 App/fs/vol_idle_sync.c create mode 100644 App/fs/vol_idle_sync.h create mode 100644 test/host/vol_idle_sync/run_unit_test.py create mode 100644 test/host/vol_idle_sync/stub/FreeRTOS.h create mode 100644 test/host/vol_idle_sync/stub/cmsis_os.h create mode 100644 test/host/vol_idle_sync/stub/cmsis_os2.h create mode 100644 test/host/vol_idle_sync/stub/dbg_log.h create mode 100644 test/host/vol_idle_sync/stub/task.h create mode 100644 test/host/vol_idle_sync/stub/test_config.h create mode 100644 test/host/vol_idle_sync/vol_idle_sync_test.c diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5e8162f1..2513e251 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -312,6 +312,21 @@ repos: files: ^(App/fs/vol_lock\.(c|h)|test/host/vol_lock/.*|test/host/common/unit_test_harness\.py)$ pass_filenames: false + # 7f2) 卷空闲同步(写回缓存空闲 flush)主机侧单元测试(2026-10-25 加,道F 批 2) + # 背景:该模块是写回缓存掉电窗口的兜底网——掉电与"脏页驻留"本身台架 + # 不可按需构造;卷忙跳过 / ioctl 失败边沿 latch / 32 位 tick 回绕 / + # 失败路径成对放锁(不得空放)全是纯状态逻辑。本钩子用 MSVC 离线编译 + # 真实源文件(#include 纳入;桩:可控时钟 + 事件位图 + 卷忙注入 + + # 可注入 disk_ioctl),把四级门与 latch 分支一次钉死。 + # files 含被测源与其头、就绪位属主 sys_events 的消费面 default_task.c、 + # 以及 harness 本身。 + - id: vol-idle-sync-unit-test + name: 卷空闲同步单测(主机侧) + entry: python test/host/vol_idle_sync/run_unit_test.py + language: system + files: ^(App/fs/vol_idle_sync\.(c|h)|App/task/default_task\.c|test/host/vol_idle_sync/.*|test/host/common/unit_test_harness\.py)$ + pass_filenames: false + # 7g) 时间同步(time_sync)主机侧单元测试(2026-10-10 加) # 背景:time_sync.c 是 TCP 时间同步协议状态机,也是 N-17 号看门狗风险的**全部 # 逻辑贡献者**——"listen 重建 -> 800ms~1s 心跳间隔"由本模块的分片延时/退避/ diff --git a/App/fs/vol_idle_sync.c b/App/fs/vol_idle_sync.c new file mode 100644 index 00000000..bb155f7b --- /dev/null +++ b/App/fs/vol_idle_sync.c @@ -0,0 +1,125 @@ +/* + * 模块名称:Volume Idle Sync(写回缓存空闲 flush)— MCU 实现 + * 模块功能:就绪门控 + 周期节流 + 卷锁 try + disk_ioctl(CTRL_SYNC) 的兜底 + * 状态机(设计/使用约定/边界见 vol_idle_sync.h) + * 适用平台:STM32F407ZGTx + FreeRTOS + * 作者:DTU 项目组 + * 创建日期:2026-10-25 + * 修改记录: + * v1.0 2026-10-25 DTU 项目组 创建(道F 批 2:写回缓存空闲 flush 收口) + */ + +/* 头文件包含区 */ +#include "vol_idle_sync.h" + +#include "test_config.h" + +#if !defined(TEST_SUITE_STORAGE) + +#include "cmsis_os.h" +#include "FreeRTOS.h" +#include "task.h" +#include "ff.h" /* FatFs 基础类型(diskio.h 依赖 BYTE/LBA_t 等,须先纳) */ +#include "diskio.h" /* disk_ioctl / CTRL_SYNC / DRESULT */ +#include "vol_lock.h" +#include "sys_events.h" +/* 本模块日志仅一条失败留痕(≤60 字符),默认行缓冲即可,不调 DBG_LINE_BUF_MAX */ +#include "dbg_log.h" + +/* Private defines -----------------------------------------------------------*/ +/* 空闲同步检查周期(= 漏网脏页的最大掉电驻留窗,另加一个主循环拍 ≈50ms)。 + * 取值依据:clean 态为"脏标志 + journal"双短路(零 NAND 触点),高频不伤介质 + * 只是白跑;脏态最多一次页编程 + journal 收账,与 f_close 同型,但落在卷 + * 已空闲的窗口、远离活跃写会话。5 s 与主循环其余慢观测(栈/堆水位 2 s)同 + * 量级而更稀疏——本模块是兜底网,不是持久性主路径(主路径 = close 内同步)。 + * 锁忙(卷不空闲)同样按本周期重试:不做欠账补偿,节奏恒定好推理。 */ +#define VOL_IDLE_SYNC_PERIOD_MS 5000u + +/* 静态变量定义区 */ +/* 状态量放文件级 static(非函数内):主机侧白盒单测(test/host/vol_idle_sync/) + * 以 #include 纳入本文件后需要复位它们;命名带 vol 前缀避免与测试桩撞名 */ +static uint32_t s_vol_sync_last_ms; /* 上次尝试时刻(tick ms,uint32 回绕) */ +static uint8_t s_vol_sync_armed; /* 0 = 从未尝试(首拍直接放行) */ +static uint8_t s_vol_sync_fail_latch; /* 失败留痕边沿 latch:一个 episode 一条 */ + +/* 函数定义区 */ + +/* + * 函数功能:存储空闲周期同步轮询(就绪门控 → 节流 → try 卷锁 → CTRL_SYNC) + * 入口参数:无 void + * 返回值:无 void + * 限定条件:仅 defaultTask 主循环调用;内部绝不阻塞(try 锁 + 常数级耗时, + * 最坏成本 = 一次 f_close 的同步段) + * 函数说明:四级门,逐级变贵: + * ① 就绪门:sys_evtHandle 未建或 STORAGE_READY 未置 → 静默 + * (挂载前 disk_ioctl 只会 RES_NOTRDY,且 bring-up 失败重试期 + * 卷状态未定,一律不碰); + * ② 节流门:距上次尝试不足 VOL_IDLE_SYNC_PERIOD_MS → 返回。 + * 无符号差值判据((uint32)(now - last) < 周期)天然跨 32 位回绕 + * 正确,与 stack_probe_poll 同一 idiom;勿"优化"成大小比较; + * ③ 空闲门:vol_lock_wait(0) try——锁被持有 = 有文件操作序列在做 + * (syslog 落盘 / 配置保存 / lftpd open...close),本轮跳过, + * 绝不改变"文件持有期内不同步"的既有语义; + * ④ 动作:disk_ioctl(0, CTRL_SYNC)——FatFs 官方幂等落盘原语 + * (f_close 尾部同一条路径):FTL 脏页刷入 + dhara_map_sync。 + * clean 时两级短路立即返回,零 NAND 操作。 + * 失败留痕:RES_ERROR 只按"边沿"打一条 DBG_ERROR(latch),持续失败 + * 不刷屏;下一次成功后 latch 复位,新 episode 再报。底层失败形态 + * (Cache flush FAIL / Map sync failed)已由 nand_ftl 就地打点, + * 本条只补"空闲路径也救不回"的上下文,不重复堆日志锚点。 + */ +void vol_idle_sync_poll(void) { + const uint32_t now = (uint32_t)xTaskGetTickCount(); + DRESULT dr; + + /* ① 就绪门 */ + if (sys_evtHandle == NULL) { + return; + } + if ((osEventFlagsGet(sys_evtHandle) & SYS_EVT_STORAGE_READY) == 0u) { + return; + } + + /* ② 节流门(回绕安全判据,见函数说明②) */ + if ((s_vol_sync_armed != 0u) + && ((uint32_t)(now - s_vol_sync_last_ms) + < (uint32_t)pdMS_TO_TICKS(VOL_IDLE_SYNC_PERIOD_MS))) { + return; + } + s_vol_sync_last_ms = now; + s_vol_sync_armed = 1u; + + /* ③ 空闲门:try 锁,拿不到 = 卷不空闲,本轮跳过(下周期再试) */ + if (vol_lock_wait(0u) == 0u) { + return; + } + + /* ④ 幂等落盘:脏页刷入 + journal 收账;clean 时零 NAND 触点 */ + dr = disk_ioctl(0, CTRL_SYNC, NULL); + vol_lock_release(); /* 成败都放锁:同步失败不产生锁泄漏 */ + + if (dr != RES_OK) { + if (s_vol_sync_fail_latch == 0u) { + s_vol_sync_fail_latch = 1u; + DBG_ERROR("[FS] idle sync FAIL (dr=%d)", (int)dr); + } + } else { + s_vol_sync_fail_latch = 0u; /* 成功:重新武装,下个 episode 可再报 */ + } +} + +#else /* TEST_SUITE_STORAGE */ + +/* + * 函数功能:STORAGE 套件构建下的空实现(编译期裁剪落点) + * 入口参数:无 void + * 返回值:无 void + * 限定条件:仅当 test_config.h 选中 TEST_SUITE_STORAGE 时编入本分支 + * 函数说明:该构建里挂载/盘写由 testTask 的 storage_test_run 独占(测试含 + * nand_ftl_deinit 掉电模拟),后台同步与它并发会互相踩坏 FTL/FatFs + * 状态——必须彻底静默,而非仅靠运行时门控。 + */ +void vol_idle_sync_poll(void) { +} + +#endif /* TEST_SUITE_STORAGE */ diff --git a/App/fs/vol_idle_sync.h b/App/fs/vol_idle_sync.h new file mode 100644 index 00000000..94cfd4a9 --- /dev/null +++ b/App/fs/vol_idle_sync.h @@ -0,0 +1,50 @@ +/* + * 模块名称:Volume Idle Sync(写回缓存空闲 flush) + * 模块功能:随 defaultTask 主循环周期轮询,在存储**空闲**时对 NAND FatFs 卷 + * 补发一次 CTRL_SYNC(FatFs 幂等落盘语义 = FTL 单页写回缓存脏页刷入 + * + dhara journal 待办收账,见 docs/架构设计/STM32F4-Base存储架构说明.md + * §5.5),作为写回缓存持久性的兜底网: + * - 正常写事务语义不变:f_close 内部 sync_fs 已同步,本模块零干预; + * - 价值在漏网兜底:任何"缓存留脏、之后再无调用触发 sync"的状态 + * (异常路径中途放锁等)其掉电驻留窗从"无限期,直到下一笔写" + * 收敛为"不超过本模块周期"; + * - 卷不空闲(卷锁被持有:syslog 落盘 / 配置保存 / lftpd + * open...close 序列进行中)本轮跳过——不改变文件持有期(如 FTP + * STOR 进行中)的既有语义。 + * clean 态零 NAND 触点(脏标志与 journal 双短路);脏态成本与一次 + * f_close 同型。周期 5 s(.c 内 VOL_IDLE_SYNC_PERIOD_MS)。 + * ⚠ 边界(勿当成掉电保护):本板无掉电检测硬件,"掉电前强制 flush" + * 不可行;"单次写事务进行中掉电"的丢失边界也不因本模块改变。 + * 适用平台:STM32F407ZGTx + FreeRTOS(CMSIS-RTOS V2);仅 MCU 端, + * 主机侧 L0.5 单测 test/host/vol_idle_sync/ + * 作者:DTU 项目组 + * 创建日期:2026-10-25(道F 批 2:写回缓存空闲 flush 收口) + * 修改记录: + * v1.0 2026-10-25 DTU 项目组 创建:STORAGE_READY 门控 + 周期节流 + + * 卷锁 try + disk_ioctl(CTRL_SYNC) + 失败边沿 latch + */ + +#ifndef VOL_IDLE_SYNC_H +#define VOL_IDLE_SYNC_H + +#ifdef __cplusplus +extern "C" { +#endif + +/* + * 函数功能:存储空闲周期同步的轮询入口(内部自带就绪门控 / 节流 / 非阻塞 + * try 锁 / clean 短路,调用点每轮无条件调用即可) + * 入口参数:无 void + * 返回值:无 void + * 限定条件:仅 defaultTask 主循环调用(存储属主任务;本函数内部绝不阻塞—— + * defaultTask 是看门狗喂狗点,见 vol_lock.h 使用约定); + * TEST_SUITE_STORAGE 构建下为空实现(卷由 testTask 独占,后台同步 + * 必须彻底静默) + */ +void vol_idle_sync_poll(void); + +#ifdef __cplusplus +} +#endif + +#endif /* VOL_IDLE_SYNC_H */ diff --git a/App/log/sys_log_mcu.c b/App/log/sys_log_mcu.c index 74cf1256..52e75e5e 100644 --- a/App/log/sys_log_mcu.c +++ b/App/log/sys_log_mcu.c @@ -334,8 +334,10 @@ static int s_append(syslog_id_t id, const char *desc) { * **不改掉电语义**:批次仍在同一次 syslog_drain() 调用内写完并 * f_close;close 内部 sync_fs → disk_ioctl(CTRL_SYNC) 会调 * ftl_flush_cache() 把 FTL 的单页写回缓存刷进 NAND(nand_ftl.c)。 - * 落盘时机与旧实现每条 close 等价,批次不跨调用驻留 RAM;残余只到 - * "单次写事务进行中掉电/复位"为止(登记簿 N-22,2026-10-22 裁定暂不做)。 + * 落盘时机与旧实现每条 close 等价,批次不跨调用驻留 RAM;漏网脏页 + * (留脏后再无调用触发刷出)已有写回缓存空闲 flush 兜底 + * (App/fs/vol_idle_sync.h:卷空闲 + 周期 CTRL_SYNC),掉电残余只到 + * "单次写事务进行中掉电/复位"为止。 * 自愈重试与错误去重的判据与 s_append_rec 一致;容量不足时返回 -3 * 而不是就地 unlink——"满了就重建"必须由按条慢路径统一执行, * 否则一批 8 条会跨越多轮重建、语义说不清。 @@ -846,8 +848,9 @@ int syslog_last_epoch(syslog_id_t id, uint32_t *p_epoch) { * ⚠ 2026-10-02 round 3~8 那次"复位后写入整段丢失"的根因是 dhara resume 锚错 * checkpoint 使 journal 整段回退(docs/架构设计/STM32F4-Base存储架构说明.md * §5.4.1),不是这层缓存:单页缓存最多丢最后未跨页那笔(≤512 B),丢不出 - * "整段"。真正的残余(无空闲/掉电前周期 flush)= 登记簿 N-22,2026-10-22 - * 用户裁定暂不做。 + * "整段"。空闲周期 flush 兜底已接线(App/fs/vol_idle_sync.h:STORAGE_READY + * 门控 + 周期节流 + 卷锁 try + CTRL_SYNC,脏页驻留窗由周期封顶);本板无 + * 掉电检测硬件,"掉电前强制 flush"仍是物理边界,不在该兜底承诺之内。 * 顺序硬约束不变:s_append 自行取卷锁(非递归锁),必须在 vol_lock_release() * **之后**调用,绝不可在持锁状态下调用。 * 容量护栏:> 8KB 先 unlink 重建,防长跑无界增长(8.3 合法名 wdev.log)。 diff --git a/App/task/default_task.c b/App/task/default_task.c index f5e9920f..30f69d6b 100644 --- a/App/task/default_task.c +++ b/App/task/default_task.c @@ -23,6 +23,9 @@ * 耗时(task_prof_report),并在**心跳位图出现 1→0 边沿** * 的瞬间留一份现场快照(task_prof_report_raw,裸 UART); * 另给控制台排空与 syslog 落盘各加一个耗时探针 + * v1.3 2026-10-25 DTU 项目组 写回缓存空闲 flush 接线:主循环 syslog 排空 + * 之后调 vol_idle_sync_poll()(卷空闲周期 CTRL_SYNC 兜底, + * 模块 = App/fs/vol_idle_sync.[ch],道F 批 2) */ #include "default_task.h" @@ -46,6 +49,7 @@ #include "task_watchdog.h" #include "task_prof.h" /* 各任务最坏耗时上报 */ #include "vol_lock.h" +#include "vol_idle_sync.h" /* 写回缓存空闲 flush 兜底(App/fs/vol_idle_sync.h) */ #include "task_handles.h" /* 任务句柄与属性块的引用契约(定义处仍是 Src/freertos.c) */ /* Private defines -----------------------------------------------------------*/ @@ -275,6 +279,11 @@ void default_task_func(void *argument) { task_prof_enter(TASK_PROF_DEF_SYSLOG); syslog_drain(); task_prof_exit(TASK_PROF_DEF_SYSLOG); + /* 写回缓存空闲 flush 兜底:卷空闲(try 锁)+ 周期节流 + 就绪门控全在 + * 模块内(App/fs/vol_idle_sync.h);clean 态零 NAND 触点,脏态最坏 = + * 一次 f_close 同型的同步段。放在 drain 之后:本拍若写过日志,close + * 已同步过,这里随即 clean 短路——漏网脏页的驻留窗由周期封顶 */ + vol_idle_sync_poll(); #endif /* 栈水位观测:首次 5s 后报告,此后每 2s 采样、出现更深峰值才打印 */ task_wd_seg(TASK_WD_SEG_STACK); diff --git a/MDK-ARM/STM32F407-Demo.uvprojx b/MDK-ARM/STM32F407-Demo.uvprojx index b324cb42..de82a6ac 100644 --- a/MDK-ARM/STM32F407-Demo.uvprojx +++ b/MDK-ARM/STM32F407-Demo.uvprojx @@ -937,6 +937,11 @@ 1 ..\App\fs\vol_lock.c + + vol_idle_sync.c + 1 + ..\App\fs\vol_idle_sync.c + time_sync.c 1 diff --git a/docs/架构设计/STM32F4-Base存储架构说明.md b/docs/架构设计/STM32F4-Base存储架构说明.md index ad95d3b3..94aa83a1 100644 --- a/docs/架构设计/STM32F4-Base存储架构说明.md +++ b/docs/架构设计/STM32F4-Base存储架构说明.md @@ -391,6 +391,16 @@ FTL 之上还有一个 **单页写回缓存 (write-back cache)**: - **写**: 写入缓存 → 标记脏 - **全页写入**: 直接刷出 (跳过缓存) - **同步 (CTRL_SYNC)**: 刷出脏页 + dhara_map_sync +- **空闲 flush 兜底(2026-10-25,道F 批 2)**: defaultTask 主循环按"存储就绪 + + 卷空闲(`vol_lock_wait(0)` try)+ 周期节流(5 s)"周期性下发一次 `CTRL_SYNC`, + 把"缓存留脏、之后再无调用触发 sync"的漏网脏页其掉电驻留窗封顶为一个周期; + 卷锁被持有(syslog 落盘 / 配置保存 / lftpd open...close 进行中)时跳过, + **不改变文件持有期的既有语义**。clean 态 = 脏标志 + journal 双短路、零 NAND + 触点;脏态成本与一次 `f_close` 的同步段同型。实现:`App/fs/vol_idle_sync.[ch]` + (单测 `test/host/vol_idle_sync/`)。 +- ⚠ **边界(勿当成掉电保护)**: 本板**无掉电检测硬件**,"掉电前强制 flush" + 不可行;单次写事务进行中掉电、以及 FTP STOR 文件持有期的落盘边界,均不因 + 空闲兜底改变。 --- diff --git a/scripts/check_registration.py b/scripts/check_registration.py index baaf42b9..6a2f34cc 100644 --- a/scripts/check_registration.py +++ b/scripts/check_registration.py @@ -97,6 +97,7 @@ SIM_EXEMPT = [ ("App/algo/adc_cal.c", "L1.5 标定折算纯算法 + cfg 段注册(消费者 = MCU 侧 adcTask 每拍;模拟器不跑采样任务),模拟器无消费路径"), ("App/app/app_main.c", "固件启动/就绪屏障,MCU 专属"), ("App/fs/vol_lock.c", "FreeRTOS 互斥量卷锁,MCU 专属"), + ("App/fs/vol_idle_sync.c", "MCU 存储属主后台空闲同步(FatFs disk_ioctl + 卷锁 + sys_events),L0.5 覆盖于 test/host/vol_idle_sync/,模拟器无消费路径"), ("App/log/console_log.c", "MCU 控制台输出,模拟器无消费路径"), ("App/sync/time_sync.c", "对时任务(net+RTOS),MCU 专属"), ("App/task/adc_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"), diff --git a/scripts/id_registry.json b/scripts/id_registry.json index 67e663de..cea583b1 100644 --- a/scripts/id_registry.json +++ b/scripts/id_registry.json @@ -1730,6 +1730,28 @@ "owner_note": "B 层口径(用户裁定 2026-10-21,同 TC-HOST-ADCHEALTH/TC-HOST-ADCCAL 先例;2026-10-25 道A 卡7 建族):属主登记处 = 单测代码目录,不逐号补文档", "cursor_note": "游标按建族时实测最大号收录(2026-10-25):越界即视为未登记的冒号牌" }, + { + "name": "TC-HOST-VOLSYNC", + "class": "TC", + "title": "主机侧单测用例号:写回缓存空闲 flush(test/host/vol_idle_sync,道F 批 2)", + "pattern": "\\bTC-VOLSYNC-[0-9]{1,4}[a-z]?\\b", + "digit_width": [ + 2, + 4 + ], + "owner_docs_glob": [ + "test/host/vol_idle_sync/*" + ], + "scope": "global", + "frozen": false, + "cursor": "TC-VOLSYNC-012", + "cursor_owner_only": true, + "check_dangling": true, + "allow_orphans": true, + "subitem_ok": true, + "owner_note": "B 层口径(用户裁定 2026-10-21,同 TC-HOST-ADCHEALTH/TC-HOST-ADCCAL 先例;2026-10-25 道F 批 2 建族):属主登记处 = 单测代码目录,不逐号补文档", + "cursor_note": "游标按建族时实测最大号收录(2026-10-25):越界即视为未登记的冒号牌" + }, { "name": "TC-HOST-IEC104", "class": "TC", diff --git a/test/host/vol_idle_sync/run_unit_test.py b/test/host/vol_idle_sync/run_unit_test.py new file mode 100644 index 00000000..cc97a038 --- /dev/null +++ b/test/host/vol_idle_sync/run_unit_test.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""写回缓存空闲 flush(vol_idle_sync)的主机侧单元测试运行器(L0.5,纯标准库)。 + +做什么: + 用本机 MSVC 编译 test/host/vol_idle_sync/vol_idle_sync_test.c(它以 #include + 纳入**生产源文件** App/fs/vol_idle_sync.c),在 PC 上离线跑就绪门控 / 节流边界 / + 卷忙跳过 / 失败边沿 latch / 成对放锁 / tick 回绕,失败即非 0 退出。 + 零硬件、零烧录、毫秒级。 + + 工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness + `test/host/common/unit_test_harness.py`(本文件只做声明)。 + +为什么要主机侧单测(而不是只用台架): + 本模块是「掉电窗口兜底网」——掉电与脏页驻留窗在台架上都不可按需复现;卷忙 / + ioctl 失败 / 32 位 tick 回绕这些分支在真机上构造成本极高。门控、节流、latch + 全是纯状态逻辑,主机侧一次钉死。 + +用法(仓库根目录): + python test/host/vol_idle_sync/run_unit_test.py + +退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 + +设计依据:App/fs/vol_idle_sync.h 文件头(含「无掉电检测硬件」边界声明); +创建日期:2026-10-25 +""" +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE))) +# harness 位于 test/host/common/ +sys.path.insert(0, os.path.join(ROOT, "test", "host", "common")) + +from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402 + +# 顺序敏感:stub 目录必须**先于**生产目录,才能覆盖 test_config.h / cmsis_os(_2).h / +# FreeRTOS.h / task.h / dbg_log.h(真实头会拖入整套 FreeRTOS/CMSIS/HAL)。 +# ff.h / diskio.h / vol_lock.h / sys_events.h 用**真实头**(接口契约与就绪位数值 +# 原汁,同 lftpd_io / nand_port 先例;本模块经真实 sys_events.h 拿就绪位定义)。 +BUILD = TestBuild( + name="Volume Idle Sync Tests", + src=os.path.join(HERE, "vol_idle_sync_test.c"), + include_dirs=[ + os.path.join(HERE, "stub"), + os.path.join(ROOT, "App", "fs"), + os.path.join(ROOT, "App", "app"), + os.path.join(ROOT, "Lib", "FatFs"), + HERE, + ], + summary_marker="=== Volume Idle Sync Tests:", +) + +if __name__ == "__main__": + sys.exit(run_host_unit_test(BUILD)) diff --git a/test/host/vol_idle_sync/stub/FreeRTOS.h b/test/host/vol_idle_sync/stub/FreeRTOS.h new file mode 100644 index 00000000..a67a01e5 --- /dev/null +++ b/test/host/vol_idle_sync/stub/FreeRTOS.h @@ -0,0 +1,29 @@ +/* + * 主机侧单测桩:FreeRTOS.h(vol_idle_sync 模块专用) + * 模块功能:最小 FreeRTOS 时基类型与宏(真实头会拉整棵 portmacro/projdefs)。 + * ⚠ 仅供 test/host/vol_idle_sync/ 使用,不参与 MCU 构建。 + * 保真点:TickType_t = uint32_t;configTICK_RATE_HZ = 1000 + * (Inc/FreeRTOSConfig.h)→ pdMS_TO_TICKS(ms) == ms。 + * 节流用例的 4999/5000 拍边界直接依赖该等式。 + * 创建日期:2026-10-25 + */ + +#ifndef FREERTOS_H +#define FREERTOS_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef uint32_t TickType_t; + +#define configTICK_RATE_HZ 1000U +#define pdMS_TO_TICKS(ms) ((TickType_t)((uint32_t)(ms) * (1000U / configTICK_RATE_HZ))) + +#ifdef __cplusplus +} +#endif + +#endif /* FREERTOS_H */ diff --git a/test/host/vol_idle_sync/stub/cmsis_os.h b/test/host/vol_idle_sync/stub/cmsis_os.h new file mode 100644 index 00000000..761b1a07 --- /dev/null +++ b/test/host/vol_idle_sync/stub/cmsis_os.h @@ -0,0 +1,14 @@ +/* + * 主机侧单测桩:cmsis_os.h(vol_idle_sync 模块专用) + * 模块功能:真实 cmsis_os.h(CMSIS-RTOS V2 包装)会拖入整套 FreeRTOS;本模块 + * 只需要事件标志接口,全部收敛在 cmsis_os2.h 桩里。 + * ⚠ 仅供 test/host/vol_idle_sync/ 使用,不参与 MCU 构建。 + * 创建日期:2026-10-25 + */ + +#ifndef CMSIS_OS_H +#define CMSIS_OS_H + +#include "cmsis_os2.h" + +#endif /* CMSIS_OS_H */ diff --git a/test/host/vol_idle_sync/stub/cmsis_os2.h b/test/host/vol_idle_sync/stub/cmsis_os2.h new file mode 100644 index 00000000..b83ed0ed --- /dev/null +++ b/test/host/vol_idle_sync/stub/cmsis_os2.h @@ -0,0 +1,32 @@ +/* + * 主机侧单测桩:cmsis_os2.h(vol_idle_sync 模块专用) + * 模块功能:为 App/fs/vol_idle_sync.c(含经 sys_events.h)提供主机可编译的 + * CMSIS-RTOS v2 事件标志最小子集。 + * ⚠ 仅供 test/host/vol_idle_sync/ 使用,不参与 MCU 构建。 + * 保真点(对齐真实 cmsis_os2.h): + * - osEventFlagsId_t = void*; + * - uint32_t osEventFlagsGet(osEventFlagsId_t)(返回当前位图,不消费—— + * 就绪位 osFlagsNoClear 语义在生产调用点之外,本桩只回静态值); + * - 就绪位数值一律来自**真实** App/app/sys_events.h(不在本桩复刻)。 + * osEventFlagsGet 由测试单元给强符号桩(可注入位图)。 + * 创建日期:2026-10-25 + */ + +#ifndef CMSIS_OS2_H +#define CMSIS_OS2_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef void *osEventFlagsId_t; + +uint32_t osEventFlagsGet(osEventFlagsId_t ef_id); + +#ifdef __cplusplus +} +#endif + +#endif /* CMSIS_OS2_H */ diff --git a/test/host/vol_idle_sync/stub/dbg_log.h b/test/host/vol_idle_sync/stub/dbg_log.h new file mode 100644 index 00000000..5ed50d6c --- /dev/null +++ b/test/host/vol_idle_sync/stub/dbg_log.h @@ -0,0 +1,17 @@ +/* + * 主机侧单测桩:dbg_log.h(vol_idle_sync 模块专用) + * 模块功能:DBG_* 换成计数,让"失败留痕 = 边沿 latch、成功不刷屏"可断言。 + * ⚠ 仅供 test/host/vol_idle_sync/ 使用,不参与 MCU 构建。 + * 创建日期:2026-10-25 + */ + +#ifndef DBG_LOG_H +#define DBG_LOG_H + +extern int g_vis_error_count; +extern int g_vis_info_count; + +#define DBG_INFO(fmt, ...) do { (void)(fmt); g_vis_info_count++; } while (0) +#define DBG_ERROR(fmt, ...) do { (void)(fmt); g_vis_error_count++; } while (0) + +#endif /* DBG_LOG_H */ diff --git a/test/host/vol_idle_sync/stub/task.h b/test/host/vol_idle_sync/stub/task.h new file mode 100644 index 00000000..b82da055 --- /dev/null +++ b/test/host/vol_idle_sync/stub/task.h @@ -0,0 +1,24 @@ +/* + * 主机侧单测桩:task.h(vol_idle_sync 模块专用) + * 模块功能:只声明 xTaskGetTickCount,由测试单元给可控时钟强符号桩 + * (驱动节流边界与 32 位回绕用例)。 + * ⚠ 仅供 test/host/vol_idle_sync/ 使用,不参与 MCU 构建。 + * 创建日期:2026-10-25 + */ + +#ifndef TASK_H +#define TASK_H + +#include "FreeRTOS.h" + +#ifdef __cplusplus +extern "C" { +#endif + +TickType_t xTaskGetTickCount(void); + +#ifdef __cplusplus +} +#endif + +#endif /* TASK_H */ diff --git a/test/host/vol_idle_sync/stub/test_config.h b/test/host/vol_idle_sync/stub/test_config.h new file mode 100644 index 00000000..0807f152 --- /dev/null +++ b/test/host/vol_idle_sync/stub/test_config.h @@ -0,0 +1,15 @@ +/* + * 主机侧单测桩:test_config.h(vol_idle_sync 模块专用) + * 模块功能:给主机编译一个"产品构建"形态——四个 TEST_SUITE_* 全部未定义, + * vol_idle_sync.c 走产品分支(STORAGE 套件的 no-op 分支由板端 + * test-suite-switch 构建侧覆盖,主机侧不重复裁剪测试)。 + * ⚠ 仅供 test/host/vol_idle_sync/ 使用,不参与 MCU 构建。 + * 创建日期:2026-10-25 + */ + +#ifndef TEST_CONFIG_H +#define TEST_CONFIG_H + +/* 刻意不定义任何 TEST_SUITE_* / ENABLE_DESTRUCTIVE_TESTS */ + +#endif /* TEST_CONFIG_H */ diff --git a/test/host/vol_idle_sync/vol_idle_sync_test.c b/test/host/vol_idle_sync/vol_idle_sync_test.c new file mode 100644 index 00000000..efe71642 --- /dev/null +++ b/test/host/vol_idle_sync/vol_idle_sync_test.c @@ -0,0 +1,282 @@ +/* + * 模块名称:Volume Idle Sync(写回缓存空闲 flush)主机侧单元测试 + * 模块功能:在 PC 上对 App/fs/vol_idle_sync.c 做离线验证(产品分支全函数覆盖)。 + * + * 被测对象:真实源文件 ../../../App/fs/vol_idle_sync.c(以 #include 原样纳入, + * 保证测的是生产代码而非副本)。平台依赖由本文件给出的**可注入强符号桩**: + * - xTaskGetTickCount -> 可控时钟(节流边界 + 32 位回绕) + * - osEventFlagsGet -> 可注入就绪位图 / 句柄判空 + * - vol_lock_wait -> 可注入 空闲/忙(try 语义)+ 透传记录 + * - vol_lock_release -> 计数(断言成对、失败不泄漏) + * - disk_ioctl -> 可注入 RES_OK/RES_ERROR + pdrv/cmd 记录 + * 事件位数值取自**真实** App/app/sys_events.h(不在桩里复刻)。 + * + * 为什么要主机侧单测(而不是只用台架): + * 本模块是"掉电窗口兜底网"——掉电本身不可台架复现,脏页驻留窗更不能; + * 失败路径(卷忙 / ioctl 失败 / tick 回绕)在真机上极难按需构造。 + * 门控/节流/latch 全是纯状态逻辑,主机侧一次钉死,板端只验证接入存在性。 + * + * 覆盖点: + * TC-VOLSYNC-001 句柄未建 -> 完全静默(不读事件/不取锁/不 ioctl) + * TC-VOLSYNC-002 STORAGE_READY 未置 -> 静默(读了事件但不取锁) + * TC-VOLSYNC-003 就绪首拍:try 锁(timeout=0) + CTRL_SYNC(pdrv=0) + 放锁各一次 + * TC-VOLSYNC-004 周期内重复拍 -> 完全抑制(不取锁不 ioctl) + * TC-VOLSYNC-005 节流边界:周期-1 拍抑制 / 满周期拍放行(1ms tick 等式) + * TC-VOLSYNC-006 卷忙(try 失败)-> 不 ioctl、不 release(不得空放锁) + * TC-VOLSYNC-007 卷忙跳过不欠账:节奏恒定(下次尝试 = 满周期之后) + * TC-VOLSYNC-008 ioctl 失败一个 episode 只打一条(边沿 latch,持续失败不刷屏) + * TC-VOLSYNC-009 成功后 latch 重新武装:下一个失败 episode 再报一条 + * TC-VOLSYNC-010 失败路径同样成对放锁(release 次数 == 有效 ioctl 次数) + * TC-VOLSYNC-011 tick 32 位回绕:跨 0 后节流判据仍正确(该抑制抑制/该放行放行) + * TC-VOLSYNC-012 成功路径零日志(不打 DBG_ERROR/DBG_INFO) + * + * 通过:全部 TC PASS(main 返回 0)。 + * 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。 + * + * 创建日期:2026-10-25 + */ + +#include +#include +#include + +/* ===================== 真实中间件头(与 lftpd_io/nand_port 同一份) ======== */ +#include "ff.h" /* FatFs 基础类型(diskio.h 依赖,先纳) */ +#include "diskio.h" + +/* ===================== 平台桩(stub 目录优先于真实头) ==================== */ +#include "test_config.h" +#include "cmsis_os.h" +#include "FreeRTOS.h" +#include "task.h" +#include "sys_events.h" /* 真实头:SYS_EVT_STORAGE_READY / sys_evtHandle extern */ +#include "dbg_log.h" + +/* ===================== 强符号桩 ===================== + * 时钟:非零起步(1000),避免"初值 0"与"回绕"在断言里混同 */ +static uint32_t s_tick_now; + +TickType_t xTaskGetTickCount(void) { + return (TickType_t)s_tick_now; +} + +/* 事件标志:句柄实体在本测试单元定义(真实 sys_events.h 只 extern) */ +static uint32_t s_evt_flags; +static int s_efget_calls; + +osEventFlagsId_t sys_evtHandle; + +uint32_t osEventFlagsGet(osEventFlagsId_t ef_id) { + (void)ef_id; + s_efget_calls++; + return s_evt_flags; +} + +/* 卷锁:1 = 获取成功,0 = 忙(try 语义下即"本轮跳过") */ +static int s_lock_ok; +static int s_lock_calls; +static int s_release_calls; +static uint32_t s_last_wait_to; + +int vol_lock_wait(uint32_t timeout_ms) { + s_lock_calls++; + s_last_wait_to = timeout_ms; + return s_lock_ok; +} + +void vol_lock_release(void) { + s_release_calls++; +} + +/* 磁盘控制:只接住被测代码真正会发的那一发(pdrv=0 / CTRL_SYNC) */ +static DRESULT s_diskio_res; +static int s_diskio_calls; +static BYTE s_diskio_pdrv; +static BYTE s_diskio_cmd; + +DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) { + (void)buff; + s_diskio_calls++; + s_diskio_pdrv = pdrv; + s_diskio_cmd = cmd; + return s_diskio_res; +} + +/* dbg_log 桩计数(stub/dbg_log.h 声明 extern) */ +int g_vis_error_count; +int g_vis_info_count; + +/* ===================== 被测代码(原样纳入生产源) ===================== */ +#include "vol_idle_sync.c" + +/* ===================== 断言 ===================== */ +static int s_total = 0, s_passed = 0, s_failed = 0; + +#define CHECK(cond, name) do { \ + s_total++; \ + if (cond) { s_passed++; printf(" [PASS] %s\n", name); } \ + else { s_failed++; printf(" [FAIL] %s (line %d)\n", name, __LINE__); } \ +} while (0) + +#define FAKE_EVT_HANDLE ((osEventFlagsId_t)0xE7) +#define PERIOD_TICKS 5000u /* = pdMS_TO_TICKS(VOL_IDLE_SYNC_PERIOD_MS), + * 1 kHz tick 下与毫秒数值等值(桩保真点) */ + +/* 复位所有桩与被测模块状态量(白盒:生产源已纳入本编译单元,文件级 static + * 可直接复位,等价"刚上电") */ +static void fakes_reset(void) { + s_tick_now = 1000u; + s_evt_flags = 0u; + sys_evtHandle = NULL; + s_efget_calls = 0; + s_lock_ok = 1; + s_lock_calls = 0; + s_release_calls = 0; + s_last_wait_to = 0xFFFFFFFFu; + s_diskio_res = RES_OK; + s_diskio_calls = 0; + s_diskio_pdrv = 0xFFu; + s_diskio_cmd = 0xFFu; + g_vis_error_count = 0; + g_vis_info_count = 0; + s_vol_sync_last_ms = 0u; + s_vol_sync_armed = 0u; + s_vol_sync_fail_latch = 0u; +} + +/* 便捷:推进时钟并空转主循环拍(模拟 defaultTask 循环里的连续 poll) */ +static void advance_and_poll(uint32_t ticks) { + s_tick_now += ticks; + vol_idle_sync_poll(); +} + +/* ===================== TC-VOLSYNC-001/002:就绪门控 ===================== */ + +static void tc_gate(void) { + fakes_reset(); + vol_idle_sync_poll(); + CHECK((s_efget_calls == 0) && (s_lock_calls == 0) && (s_diskio_calls == 0), + "TC-VOLSYNC-001 handle NULL: fully silent (no flags read, no lock, no ioctl)"); + + fakes_reset(); + sys_evtHandle = FAKE_EVT_HANDLE; /* 句柄已建但未就绪 */ + vol_idle_sync_poll(); + CHECK((s_efget_calls == 1) && (s_lock_calls == 0) && (s_diskio_calls == 0), + "TC-VOLSYNC-002 STORAGE_READY clear: read flags then bail (no lock, no ioctl)"); +} + +/* ===================== TC-VOLSYNC-003..007:放行/节流/卷忙 ===================== */ + +static void tc_run_throttle(void) { + fakes_reset(); + sys_evtHandle = FAKE_EVT_HANDLE; + s_evt_flags = SYS_EVT_STORAGE_READY; + vol_idle_sync_poll(); + CHECK((s_lock_calls == 1) && (s_last_wait_to == 0u), + "TC-VOLSYNC-003 first ready tick: try-lock taken with timeout=0 (non-blocking)"); + CHECK((s_diskio_calls == 1) && (s_diskio_pdrv == 0u) && (s_diskio_cmd == (BYTE)CTRL_SYNC), + "TC-VOLSYNC-003b ioctl exactly once with pdrv=0 / CTRL_SYNC"); + CHECK(s_release_calls == 1, + "TC-VOLSYNC-003c lock released exactly once after ioctl"); + + vol_idle_sync_poll(); + vol_idle_sync_poll(); + CHECK((s_lock_calls == 1) && (s_diskio_calls == 1), + "TC-VOLSYNC-004 repeats within period fully suppressed (no lock, no ioctl)"); + + /* 边界:+4999 抑制 / +1(累计满 5000)放行 */ + advance_and_poll(PERIOD_TICKS - 1u); + CHECK(s_diskio_calls == 1, "TC-VOLSYNC-005 period-1 tick: still suppressed"); + advance_and_poll(1u); + CHECK(s_diskio_calls == 2, "TC-VOLSYNC-005b exact period tick: fires again"); + + /* 卷忙:ioctl 不加、release 不得空放(相对断言:忙尝试整段零增量) */ + { + const int lk = s_lock_calls; + const int rel = s_release_calls; + s_lock_ok = 0; + advance_and_poll(PERIOD_TICKS); + CHECK((s_lock_calls == lk + 1) && (s_diskio_calls == 2) && (s_release_calls == rel), + "TC-VOLSYNC-006 lock busy: attempt logged, no ioctl and NO release (never unlock what we do not hold)"); + + /* 跳过不欠账:忙的那拍消费了槽位 -> 下次尝试在一个满周期之后 */ + s_lock_ok = 1; + advance_and_poll(PERIOD_TICKS - 1u); + CHECK(s_lock_calls == lk + 1, + "TC-VOLSYNC-007 busy skip consumed the slot: constant cadence"); + advance_and_poll(1u); + CHECK((s_lock_calls == lk + 2) && (s_diskio_calls == 3) && (s_release_calls == rel + 1), + "TC-VOLSYNC-007b next attempt exactly one full period after the skip"); + } +} + +/* ===================== TC-VOLSYNC-008..010/012:失败 latch / 成对放锁 ======= */ + +static void tc_fail_latch(void) { + fakes_reset(); + sys_evtHandle = FAKE_EVT_HANDLE; + s_evt_flags = SYS_EVT_STORAGE_READY; + + vol_idle_sync_poll(); /* 首拍成功 */ + CHECK((g_vis_error_count == 0) && (g_vis_info_count == 0), + "TC-VOLSYNC-012 success path logs nothing"); + + s_diskio_res = RES_ERROR; + advance_and_poll(PERIOD_TICKS); + CHECK(g_vis_error_count == 1, "TC-VOLSYNC-008 first failure logs exactly one line"); + + advance_and_poll(PERIOD_TICKS); + advance_and_poll(PERIOD_TICKS); + CHECK((g_vis_error_count == 1) && (s_diskio_calls == 4), + "TC-VOLSYNC-008b continuing failures do not spam (edge latch holds)"); + + CHECK(s_release_calls == s_diskio_calls, + "TC-VOLSYNC-010 release paired with every attempt incl. failures (no lock leak)"); + + s_diskio_res = RES_OK; + advance_and_poll(PERIOD_TICKS); + CHECK((g_vis_error_count == 1) && (s_vol_sync_fail_latch == 0u), + "TC-VOLSYNC-009 success clears latch (re-armed for next episode)"); + + s_diskio_res = RES_ERROR; + advance_and_poll(PERIOD_TICKS); + CHECK(g_vis_error_count == 2, "TC-VOLSYNC-009b new failing episode logs again (one line)"); +} + +/* ===================== TC-VOLSYNC-011:tick 回绕 ===================== */ + +static void tc_wrap(void) { + fakes_reset(); + sys_evtHandle = FAKE_EVT_HANDLE; + s_evt_flags = SYS_EVT_STORAGE_READY; + /* 白盒置态:上次尝试贴着 32 位顶端,当前拍刚跨过 0 */ + s_vol_sync_armed = 1u; + s_vol_sync_last_ms = 0xFFFFF000u; + s_tick_now = 0x0300u; /* 无符号差 = 0x1300 = 4864 < 5000 */ + vol_idle_sync_poll(); + CHECK(s_diskio_calls == 0, "TC-VOLSYNC-011 wrap: unsigned-diff still suppresses early tick"); + + s_vol_sync_last_ms = 0xFFFFF000u; + s_tick_now = 0x0800u; /* 无符号差 = 0x1800 = 6144 >= 5000 */ + vol_idle_sync_poll(); + CHECK(s_diskio_calls == 1, "TC-VOLSYNC-011b wrap past zero fires when diff says so"); +} + +/* ===================== main ===================== */ + +int main(void) { + printf("=== Volume Idle Sync Tests ===\n"); + + printf("-- gate (readiness) --\n"); + tc_gate(); + printf("-- run / throttle / idle-skip --\n"); + tc_run_throttle(); + printf("-- failure latch / pairing --\n"); + tc_fail_latch(); + printf("-- tick wraparound --\n"); + tc_wrap(); + + printf("\n=== Volume Idle Sync Tests: %d/%d PASSED (failed=%d) ===\n", + s_passed, s_total, s_failed); + return (s_failed == 0) ? 0 : 1; +}