From 007f6a0e2f8b0714e009e938e775f036d53fd255 Mon Sep 17 00:00:00 2001 From: Wanderingss <1624155937@qq.com> Date: Sun, 15 Nov 2026 20:55:04 +0800 Subject: [PATCH] =?UTF-8?q?docs(cfg):=20=E9=85=8D=E7=BD=AE=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E6=A1=A3=E9=87=8D=E5=86=99=E4=B8=BA=E7=8E=B0=E7=8A=B6?= =?UTF-8?q?=E7=89=88=EF=BC=8C=E5=88=A0=E9=99=A4=E6=97=A7=E4=BA=8C=E8=BF=9B?= =?UTF-8?q?=E5=88=B6=E6=A1=A3=E5=8F=99=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原「配置文件设计说明.md」是 2026-09 的规划文档(P1~P8 实施计划、D1~D15 决策表、差距调研、模块清单动作列、已终结的开放问题),与现在跑的系统 已有明显出入。改写成只描述当前状态的「配置文件设计.md」。 新文档 9 节:概览 / 文件格式(路径与容量、语法、出厂模板原文、值类型)/ 运行时机制(上电加载、外科手术式保存、2s 轮询与沉降、视图一致性守卫、 写者归属)/ 段与键表(8 段全满、注册顺序、接入新段模板、段回调)/ 接口(引擎 API、平台后端)/ 内存占用(编译期钉死的 9217B 暂存)/ 异常与恢复 / 双端差异 / 已知限制。 顺带修正一处事实错误:旧文档称 FTP STOR 是「写 .TMP 再 rename 原子更新」, 实际 lftpd receive_file 是原地覆写,已按代码现状改写。 旧二进制档(NCFG)的叙述按裁定从全仓移除:文档只描述当前状态,不再 交代旧版本兼容性。删掉网络配置说明 §3.2 整节(后续小节重排为 3.2~3.5), 并清理 README / 架构总览 / 产品需求 / 部署环境等 11 处同类叙述。 同步: - 8 段注册、FTP 原地覆写、2s FAT mtime 粒度等数据更新进配置分层架构图, 重跑后与生成器逐字节一致,check_generated_fresh 里这条存量基线下架 - 全仓 24 处引用改用新文档名,旧节号逐条映射到新节号 - 文档规范 §7.1 变更记录例外名单 4 篇减为 3 篇(该档实际无变更记录表) 验证:48 个提交期钩子全通过;Keil 构建 0 错误 0 警告; check_doc_links / check_mermaid / check_generated_fresh 均通过。 --- App/README.md | 2 +- App/cfg/README.md | 4 +- App/cfg/cfg_file.c | 25 +- App/cfg/cfg_file.h | 2 +- App/cfg/cfg_file_store.h | 2 +- App/cfg/cfg_file_store_mcu.c | 4 +- App/util/README.md | 2 +- Drivers/BSP/NETCFG/README.md | 4 +- Drivers/BSP/NETCFG/net_cfg.c | 12 +- Drivers/BSP/NETCFG/net_cfg.h | 6 +- Drivers/BSP/README.md | 2 +- docs/README.md | 2 +- docs/UI设计/内容页架构.md | 2 +- docs/images/配置文件分层架构.svg | 106 ++-- docs/产品需求/产品需求设计文档.md | 8 +- docs/内存管理/RAM使用分析与优化方案.md | 4 +- docs/子系统设计/网络配置说明.md | 29 +- docs/子系统设计/运行与自检记录设计说明.md | 2 +- docs/子系统设计/配置文件设计.md | 346 +++++++++++ docs/子系统设计/配置文件设计说明.md | 572 ------------------ docs/文档规范.md | 4 +- docs/架构设计/项目启动顺序管理.md | 2 +- docs/架构设计/项目架构总览.md | 4 +- docs/测试说明/专项验证/模拟器差距清单.md | 2 +- .../测试说明/单元测试/配置文件单元测试说明.md | 2 +- docs/测试说明/板级测试/实机测试实施指南.md | 2 +- docs/测试说明/流程与规范/测试开发标准流程.md | 2 +- docs/环境与部署/项目部署环境要求.md | 2 +- docs/算法/采样数据校正全流程.md | 2 +- docs/算法/采集与算法层设计说明.md | 2 +- docs/门禁说明/coding-standard.md | 4 +- scripts/check_generated_fresh.py | 5 +- simulator/README.md | 6 +- simulator/main.c | 4 +- simulator/sim/cfg_file_store_sim.c | 2 +- simulator/tests/check_content_pages.py | 4 +- test/host/cfg_file/cfg_file_test.c | 2 +- test/host/cfg_file_store_mcu/run_unit_test.py | 2 +- test/host/iec104/iec104_test.c | 2 +- tools/arch_diagram/README.md | 2 +- tools/arch_diagram/gen_cfg_arch_svg.py | 27 +- 41 files changed, 501 insertions(+), 719 deletions(-) create mode 100644 docs/子系统设计/配置文件设计.md delete mode 100644 docs/子系统设计/配置文件设计说明.md diff --git a/App/README.md b/App/README.md index 56042566..c1bff395 100644 --- a/App/README.md +++ b/App/README.md @@ -33,5 +33,5 @@ FreeRTOS/CMSIS-RTOS API**(见 `AGENTS.md`「平台约束」)。 - 架构全景:`docs/架构设计/项目架构总览.md` - 启动顺序:`docs/架构设计/项目启动顺序管理.md` -- 配置引擎:`docs/子系统设计/配置文件设计说明.md` +- 配置引擎:`docs/子系统设计/配置文件设计.md` - 日志:`docs/子系统设计/控制台单写者日志设计说明.md`、`docs/子系统设计/运行与自检记录设计说明.md` diff --git a/App/cfg/README.md b/App/cfg/README.md index e0ea7376..ddbde3e5 100644 --- a/App/cfg/README.md +++ b/App/cfg/README.md @@ -20,11 +20,9 @@ - **零分配共享核心**:核心逻辑不含任何分配符号,动态内存收进平台后端 (`cfg_store_alloc/free`:MCU→`pvPortMalloc`,模拟器→宿主堆) - **原子写**:写 `/config.TMP` 后 `f_rename` 替换,避免掉电损坏 -- **旧档迁移已删除(2026-10-10)**:旧格式 `/cfg/netcfg.bin` 不再读取——存在则被忽略(不读、 - 不删、不迁移),配置回落工厂默认;`net_cfg_legacy.c/h` 与 `cfg_store_legacy_path()` 已移除 - **自研代码禁用 C 库堆分配**:MicroLIB 堆仅 512B(`malloc` 必返回 NULL) ## 相关文档 -- `docs/子系统设计/配置文件设计说明.md` — 完整设计(分层/解析/持久化) +- `docs/子系统设计/配置文件设计.md` — 完整设计(分层/解析/持久化) - `docs/子系统设计/网络配置说明.md` — 网络段 diff --git a/App/cfg/cfg_file.c b/App/cfg/cfg_file.c index a12c9eec..59e7b6f7 100644 --- a/App/cfg/cfg_file.c +++ b/App/cfg/cfg_file.c @@ -1,4 +1,4 @@ -/* +/* * 模块名称:cfg_file * 模块功能:通用配置文件引擎(双端共享核心)——INI 段/键表解析校验、外科手术式 * 保存(保留注释/未知内容,缺失段/键写回)、出厂模板生成、沉降去抖轮询 @@ -49,7 +49,7 @@ * 开机 load 始终正确,因 load 只解析一轮) * 函数说明:引擎状态非重入——调用方须收敛同一任务(defaultTask:poll/save; * 启动期 load 先于任务竞争);FS 卷并发由 FatFS REENTRANT 卷锁保证 - * (见《配置文件设计说明》D15 A+ 方案) + * (见《配置文件设计》第 5 节「并发与写者收敛」) */ /* 头文件包含区 */ @@ -122,6 +122,7 @@ static uint8_t s_base_valid; /* 基线是否已建立 */ static uint8_t s_snap[CFG_SNAP_MAX]; static uint16_t s_reject_streak; /* 连续被拒重载计数(视图不一致;0 = 无进行中 拒绝;饱和不回绕,仅用于告警限速节奏) */ +static uint16_t s_missing_streak; /* 建立基线后连续缺失的轮询数(FTP DELE 留痕的限速节奏) */ static int s_sec_hdr[CFG_FILE_MAX_SECTIONS]; /* 段头行号(0xFFFF = 未见) */ static uint16_t s_key_line[CFG_FILE_MAX_SECTIONS][CFG_FILE_MAX_KEYS]; /* 受管键行号(0xFFFF = 未见) */ @@ -1423,9 +1424,27 @@ void cfg_file_poll(void) { cfg_file_stat_t st; if (cfg_store_stat(cfg_store_config_path(), &st) != 0) { - return; /* 文件缺失/读失败:保留当前 RAM 配置 */ + /* 文件缺失/读失败:保留当前 RAM 配置。文件曾经在位、如今不在了 = + * FTP DELE 或卷层掉文件——运行期不重建柜出模板(FTP 上传会在下趟 + * poll 建基线),但持续缺失必须留痕:否则"恢复出厂 = 删文件"会静默 + * 变成"什么都不发生"(setdefault 的既有语义见文件头) */ + if (s_base_valid != 0u) { + if (s_missing_streak < 0xFFFFu) { + s_missing_streak++; + } + if (s_missing_streak == 1u) { + DBG_ERROR("poll: config file GONE (%s), RAM kept (recreate + reboot = factory)", + cfg_store_config_path()); + } + else if ((s_missing_streak % 30u) == 0u) { + DBG_INFO("WARN: config file missing %u rounds", (unsigned)s_missing_streak); + } + } + return; } + s_missing_streak = 0u; /* 在位/重建 = 缺失期结束 */ + if (s_base_valid == 0u) { s_base = st; s_last = st; diff --git a/App/cfg/cfg_file.h b/App/cfg/cfg_file.h index 611c7a65..d7bfe3e9 100644 --- a/App/cfg/cfg_file.h +++ b/App/cfg/cfg_file.h @@ -21,7 +21,7 @@ * v1.5 2026-11-05 DTU 项目组 契约同步视图一致性守卫(issue #73 SNTP 补验揪出): * poll 读视图与 stat 令牌不符或内容坏 = 拒绝应用(RAM/基线不动、2s * 重试自愈);save 读视图不符 = 拒存返回 -1(原文件不动)。 - * 细节见 cfg_file.c v1.6 与《配置文件设计说明》变更节 + * 细节见 cfg_file.c v1.6 与《配置文件设计》第 5 节视图守卫 */ #ifndef CFG_FILE_H diff --git a/App/cfg/cfg_file_store.h b/App/cfg/cfg_file_store.h index 8fc9ad9c..8f8889e3 100644 --- a/App/cfg/cfg_file_store.h +++ b/App/cfg/cfg_file_store.h @@ -104,7 +104,7 @@ const char *cfg_store_config_path(void); const char *cfg_store_tmp_path(void); /* 注:旧版遗留档 getter `cfg_store_legacy_path()` 已于 2026-10-10 随旧档迁移 - * net_cfg_legacy 一并删除(旧格式 /cfg/netcfg.bin 不再兼容、不再读取) */ + * net_cfg_legacy 一并删除(旧二进制档不再读取) */ #ifdef __cplusplus } diff --git a/App/cfg/cfg_file_store_mcu.c b/App/cfg/cfg_file_store_mcu.c index b94d5dc7..d9d5e65d 100644 --- a/App/cfg/cfg_file_store_mcu.c +++ b/App/cfg/cfg_file_store_mcu.c @@ -188,7 +188,7 @@ int cfg_store_write_atomic(const char *p_path, const uint8_t *p_buf, size_t len) * 返回值:0 - 成功;-1 - 文件缺失/读失败 int * 限定条件:无 * 函数说明:FAT 时间分钟级粒度——同分钟内同长度重写不可检出(可接受, - * 见《配置文件设计说明》§6.2 mtime 令牌说明) + * 见《配置文件设计》第 6 节 mtime 令牌说明) */ int cfg_store_stat(const char *p_path, cfg_file_stat_t *p_st) { FILINFO fno; @@ -251,7 +251,7 @@ const char *cfg_store_tmp_path(void) { } /* - * 旧版遗留文件 /cfg/netcfg.bin 的路径 getter(cfg_store_legacy_path)已于 + * 旧二进制档的路径 getter(cfg_store_legacy_path)已于 * 2026-10-10 随旧档迁移 net_cfg_legacy 一并删除:无调用者即无必要保留 * (留着"看起来还支持旧档"的入口会误导后续维护) */ diff --git a/App/util/README.md b/App/util/README.md index 1816eb8c..8e16ff20 100644 --- a/App/util/README.md +++ b/App/util/README.md @@ -11,7 +11,7 @@ > **已删除(2026-10-02)**:`crc.c/h`(CRC-16 Modbus / CRC-8)与 `ringbuf.c/h`(SPSC 环形缓冲) > 两模块**零调用者**且未编入任何构建——CRC 从未被链接使用:名义上的关联点 `net_cfg_legacy.c` -> (旧档 `/cfg/netcfg.bin` 迁移,**该文件亦已于 2026-10-10 删除**)自带静态 `s_crc16_modbus()`, +> (旧档迁移,**该文件亦已于 2026-10-10 删除**)自带静态 `s_crc16_modbus()`, > 从不调用本模块;环形缓冲由 `App/log/console_log.c` 内联实现(`s_con_ring`)。 > 保留只会造成"有统一实现"的误导,故整体移除。 diff --git a/Drivers/BSP/NETCFG/README.md b/Drivers/BSP/NETCFG/README.md index 09c09d5d..4092d1ea 100644 --- a/Drivers/BSP/NETCFG/README.md +++ b/Drivers/BSP/NETCFG/README.md @@ -9,7 +9,7 @@ |---|---| | `net_cfg.c/h` | 网络配置加载(`net_cfg_load`)/ 保存(`net_cfg_save_run`)+ RAM 副本管理 | -> **已删除(2026-10-10)**:`net_cfg_legacy.c/h`——旧二进制档 `/cfg/netcfg.bin` 的"首启自动迁移"。 +> **已删除(2026-10-10)**:`net_cfg_legacy.c/h`——旧二进制档的"首启自动迁移"。 > 现口径:旧档若存在则被**忽略**(不读、不删、不迁移),配置直接回落工厂默认(`/config.ini` > `[network]` 段)。删除理由:该一次性路径把整条配置写链(`net_cfg_save → cfg_file_save → > cfg_store_write_atomic → f_rename → FatFS → dhara FTL`)挂进启动期调用图,使 defaultTask 的 @@ -37,5 +37,5 @@ netTask 消费 SYS_EVT_NET_RECONFIG → 立即重建网络(新 IP 免重启生 ## 相关文档 - `docs/子系统设计/网络配置说明.md` — 网口设置页与即时生效机制 -- `docs/子系统设计/配置文件设计说明.md` — 配置引擎 +- `docs/子系统设计/配置文件设计.md` — 配置引擎 - `docs/架构设计/项目启动顺序管理.md` — 就绪屏障 diff --git a/Drivers/BSP/NETCFG/net_cfg.c b/Drivers/BSP/NETCFG/net_cfg.c index 07a23644..7cdcc986 100644 --- a/Drivers/BSP/NETCFG/net_cfg.c +++ b/Drivers/BSP/NETCFG/net_cfg.c @@ -8,14 +8,14 @@ * 创建日期:2026-09-10 * 修改记录: * v1.0 2026-09-10 王建锋 创建初始版本 - * v1.1 2026-09-14 王建锋 存储迁移至 cfg_file 引擎(INI);旧 NCFG 代码移至 net_cfg_legacy + * v1.1 2026-09-14 王建锋 存储迁移至 cfg_file 引擎(INI);旧二进制档代码移至 net_cfg_legacy * v1.2 2026-09-15 王建锋 保存改跨任务交接(request/state/run + 通知回调); * 引擎属主 = defaultTask(竞态修复) - * v1.3 2026-10-10 王建锋 **删除旧档 NCFG 迁移**(net_cfg_legacy 整体移除):该兼容 + * v1.3 2026-10-10 王建锋 **删除旧档迁移**(net_cfg_legacy 整体移除):该兼容 * 路径把整条配置写链(net_cfg_save → cfg_file_save → * cfg_store_write_atomic → f_rename → FTL)挂进启动期调用图, - * 使 defaultTask 静态最坏链 +600 B;本仓库无携带 - * /cfg/netcfg.bin 的设备,故按"不保留旧档兼容"处置。 + * 使 defaultTask 静态最坏链 +600 B;本仓库无携带旧二进制档 + * 的设备,故按"不保留旧档兼容"处置。 * 旧档若存在 → 被忽略(不读、不删、不迁移),配置回落工厂默认 */ @@ -194,8 +194,8 @@ int net_cfg_load(void) { ftp_cfg_register(); s_sntp_register(); - /* 旧档 NCFG 迁移已于 2026-10-10 删除(net_cfg_legacy 移除;理由见文件头 v1.3)。 - * 若卷上仍残留 /cfg/netcfg.bin:不读、不删、不迁移 → 配置回落工厂默认 */ + /* 旧档迁移已于 2026-10-10 删除(net_cfg_legacy 移除;理由见文件头 v1.3)。 + * 若卷上仍残留旧格式文件:不读、不删、不迁移 → 配置回落工厂默认 */ if (cfg_file_load() != 0) { /* 严重错误:引擎已将 RAM 副本复位工厂默认,行为仍安全 */ DBG_ERROR("load: cfg_file_load FAIL"); diff --git a/Drivers/BSP/NETCFG/net_cfg.h b/Drivers/BSP/NETCFG/net_cfg.h index 423f7ed5..0f1f0991 100644 --- a/Drivers/BSP/NETCFG/net_cfg.h +++ b/Drivers/BSP/NETCFG/net_cfg.h @@ -7,8 +7,8 @@ * = 不覆盖芯片内置 MAC。cfg_file 引擎为静态单任务设计,调用收敛于 * 单一属主任务(MCU = defaultTask:就绪屏障前加载 / 保存执行 / 2s * poll;模拟器 = 主循环);UI 保存只投递请求,netTask 只读 RAM 副本 - * 函数说明:旧版 NCFG 二进制格式(magic+ver+CRC-16+18B 字段)的迁移路径 - * (net_cfg_legacy)**已于 2026-10-10 删除** → 旧档 /cfg/netcfg.bin + * 函数说明:旧版二进制格式(magic+ver+CRC-16+18B 字段)的迁移路径 + * (net_cfg_legacy)**已于 2026-10-10 删除** → 旧二进制档 * 不再读取/迁移,携带旧档的设备升级后回落工厂默认(理由见 net_cfg.c v1.3) * 适用平台:STM32F407ZGTx / PC 模拟器(两端共用) * 作者:王建锋 @@ -50,7 +50,7 @@ typedef struct { * SYS_EVT_STORAGE_READY 前;模拟器 = 主循环启动期);文件读失败 * 自动回退工厂默认(不阻塞网络启动) * 函数说明: 1. 注册 [network] 段(幂等) - * 2. 文件缺失 → 先迁旧档 NCFG(有效 → 旧值物化进新文件),否则 + * 2. 文件缺失 → 先迁旧档(有效 → 旧值物化进新文件),否则 * 生成出厂模板 * 3. 解析文件:有效键 → RAM 副本;无效/缺失键 → 工厂默认并留痕 * 4. 严重错误 → RAM 副本 = 工厂默认(不让坏配置带崩网络) diff --git a/Drivers/BSP/README.md b/Drivers/BSP/README.md index 04463172..e0410110 100644 --- a/Drivers/BSP/README.md +++ b/Drivers/BSP/README.md @@ -13,7 +13,7 @@ | `SD2506/` | SD2506 RTC | I2C | 实时时钟驱动 | `docs/问题记录/SD2506_Trap_Records.md` | | `RS485/` | RS-485 半双工 | UART5 | 收发方向控制(DE 引脚)+ 时序 | — | | `NET/` | BSD Socket 网络抽象层 | — | Socket API + select + 轻量 FTP 服务器 | `docs/网络/`(含 `Net层阻塞模型架构设计.md`) | -| `NETCFG/` | 网络配置 | — | `/config.ini` 网络段的加载/保存(旧 NCFG 档已删除,2026-10-10) | `docs/子系统设计/网络配置说明.md` | +| `NETCFG/` | 网络配置 | — | `/config.ini` 网络段的加载/保存 | `docs/子系统设计/网络配置说明.md` | | `UI/` | LVGL UI 组件 | — | 菜单/内容页/组件/中文字体 | `docs/UI设计/` | ## 根级文件 diff --git a/docs/README.md b/docs/README.md index 68889177..8838fde9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -87,7 +87,7 @@ docs/ | 文档 | 内容 | |---|---| -| [`配置文件设计说明.md`](子系统设计/配置文件设计说明.md) | `/config.ini` 配置引擎:分层、解析、原子持久化(旧 NCFG 档已废弃) | +| [`配置文件设计.md`](子系统设计/配置文件设计.md) | `/config.ini` 配置引擎:分层、解析、原子持久化 | | [`网络配置说明.md`](子系统设计/网络配置说明.md) | 网口设置页:IP/掩码/网关,UI 保存即时生效 | | [`系统墙钟时间维护说明.md`](子系统设计/系统墙钟时间维护说明.md) | `sys_clock`:RTC 基准、tick 回绕防护、30 天惰性再基准化 | | [`控制台单写者日志设计说明.md`](子系统设计/控制台单写者日志设计说明.md) | 控制台日志单写者模型(避免多任务交错输出) | diff --git a/docs/UI设计/内容页架构.md b/docs/UI设计/内容页架构.md index 06322b95..00f4ef26 100644 --- a/docs/UI设计/内容页架构.md +++ b/docs/UI设计/内容页架构.md @@ -494,7 +494,7 @@ extern const content_page_def_t ui_page_device_info_desc; /* 描述符(Flash | 2026-09-09 | 密码页设计定稿(§4.1):边界行为 = 都环绕(数字 9↔0、光标首框↔末框);5 项设计全部确认、无遗留待确认——具备实施条件(框架扩展 + 新页面模块 + 每页保护标志/密码) | | 2026-09-09 | 密码页落地(§4.1):框架描述符新增 title_y/hide_deco/hide_footer/key_intercept + 新 API `ui_content_page_invalidate`;新模块 `ui_page_password`(3 数字框反显光标 + arm API,draw_cb+key_cb);菜单受保护叶子路由(arm→开密码页,确认一致后 `menu_password_ok` **先关页再续做进入动作**);字体 97→102 汉字(198 字形);双端注册;回归扩至 7 页 + 往返水位,五门禁全绿 | | 2026-09-09 | 权限模型(替代"每页专属密码",§4.1):表行尾注记 ` #P`(需密码,无类别);`ui_pwd_def.h` 两个编译期密码(查看 000 / 修改 123)+ 新 `ui_perm_t` 权限枚举——密码页输查看密码授只读权限、输修改密码授修改权限(只有修改权限进入后才能改参数);生成器同步改造并重新生成(pwd 列 = 0/1 布尔,`ui_menu_tab_has_pwd`);框架 `ui_content_page_open(id, perm)` 携授予权限 + 描述符 `edit_capable` + 新 API `ui_content_page_is_editable`(页面钩子据以判只读/可编辑);路由密码检查提到内容页判断之前(修复带内容页需密码叶子绕过密码的路由缺口);无密码叶子直接进、默认只读。§4.1 功能规则/待确认表/落地要点同步。验证:五门禁全绿(菜单 8 态 + 7 页像素一致、池 4784/4864 B 不变、Keil 0 错 0 警、代码规范 PASS) | -| 2026-09-10 | 网口设置内容页:新增 `Drivers/BSP/NETCFG/`(Flash 持久化 magic 'NCFG'+CRC-16/MODBUS,工厂默认 192.168.1.100/24、gw .1、MAC 全零=芯片内置)+ 页面插件 `ui_page_net_cfg`(5 行动态值 + 可编辑:↑↓调字节 0..255 环绕 / ←→移光标 / ENTER 确认,保存行 ENTER → `net_cfg_save` 下次上电生效);框架新增描述符钩子 `init_cb`(开页重置运行态)+ API `ui_content_page_selected_row`;`net_init` 增 `mac` 参数、netTask 改为等 `SYS_EVT_STORAGE_READY` → `net_cfg_load` → `net_init(ip,mask,gw,mac)` → 回写实际 MAC 影子;模拟器 main 调 `net_cfg_load` 使参考图显示工厂默认值;字体修复:lv_font_conv 丢弃低码位符号(U+FF0C 全角逗号 < 0x4E2D 范围起点 → 页脚逗号长期空白)改由 `merge_extra_symbols()` 补回 + 原本缺字(存/密/户/本/用/钥)→ 111 字 / 208 字形;回归扩至 11 页(网口设置 4 态:可编辑/只读/编辑光标反显/保存 OK),五门禁全绿 | +| 2026-09-10 | 网口设置内容页:新增 `Drivers/BSP/NETCFG/`(Flash 持久化二进制档 + CRC-16/MODBUS,工厂默认 192.168.1.100/24、gw .1、MAC 全零=芯片内置)+ 页面插件 `ui_page_net_cfg`(5 行动态值 + 可编辑:↑↓调字节 0..255 环绕 / ←→移光标 / ENTER 确认,保存行 ENTER → `net_cfg_save` 下次上电生效);框架新增描述符钩子 `init_cb`(开页重置运行态)+ API `ui_content_page_selected_row`;`net_init` 增 `mac` 参数、netTask 改为等 `SYS_EVT_STORAGE_READY` → `net_cfg_load` → `net_init(ip,mask,gw,mac)` → 回写实际 MAC 影子;模拟器 main 调 `net_cfg_load` 使参考图显示工厂默认值;字体修复:lv_font_conv 丢弃低码位符号(U+FF0C 全角逗号 < 0x4E2D 范围起点 → 页脚逗号长期空白)改由 `merge_extra_symbols()` 补回 + 原本缺字(存/密/户/本/用/钥)→ 111 字 / 208 字形;回归扩至 11 页(网口设置 4 态:可编辑/只读/编辑光标反显/保存 OK),五门禁全绿 | | 2026-09-10 | 修复网口设置页编辑态渲染 bug:光标段原传整串 `value + start`,而 `lv_draw_label` 按 `coords`(光标盒,仅段宽)折行 → 逐行向盒下方绘制、溢出到其它行与空白区("下方弹出一串字符")。改为只传**段文本**(IP/掩码/网关 3 字符、MAC 2 字符);§3.5 补"`lv_draw_label` 折行陷阱"说明 | | 2026-09-10 | 网口设置页 MAC 值改常识格式 **`A4:94:B6:12:34:56`**(冒号分隔,17 字符 = 102px);布局调整 名 x=6→4、值 x=60→54(MAC ink 止于 155,仍在白带 2..157 内);编辑光标段偏移 MAC 2i→3i | | 2026-09-14 | 运行记录/自检记录动态列表页(`ui_page_runlog` / `ui_page_selftest`):描述符新增 `count_cb` + `refresh`(开页/翻页时重读条数与内容,每条 2 行 = 时间戳/描述),数据源为 `App/log/sys_log` 引擎的 `/log/run.log`、`/log/selftest.log`;**框架不变量 selectable ⇒ band_h>0**——band_h=0 时选中行恒黑字无白带 → 黑底不可见(首版两页均踩中,改 band_h=13 对齐 AC/DC 约定);新坑:本 LVGL 版本 `lv_text_get_width` length≠0 时无 NUL 检查,传 -1 越串尾死循环,必须传 strlen(密码页错误提示首版触发,已修并全库排查) | diff --git a/docs/images/配置文件分层架构.svg b/docs/images/配置文件分层架构.svg index bd33a993..e09e0c65 100644 --- a/docs/images/配置文件分层架构.svg +++ b/docs/images/配置文件分层架构.svg @@ -1,5 +1,5 @@ - - + + 通用配置文件分层架构(/config.ini) INI 文本文件 · FTP 上传/下载 · 段键表引擎 · MCU/模拟器双端共享 @@ -9,60 +9,62 @@ 文本编辑器:改 /config.ini(注释 / 加段 / 改值) FTP 客户端:RETR 下载 / STOR 上传(端口 21) - + lftpd FTP 服务(端口 21) ftpTask · 单连接 · root = /(= 卷根) RETR / STOR / LIST / DELE / CWD(lftpd 现状能力) - -STOR = 原地覆盖(无 tmp+rename) - -路径规范化封死 .. 穿越 - -/config.ini(通用配置文件) -FatFS 卷根(NAND GD5F2GQ5UE · dhara FTL) - -INI 文本:[段] / key=value / ; 注释 - -CRLF · UTF-8(容忍 BOM)· ≤ 4KB - -设备写:tmp + rename(原子);变更识别:size + mtime - -cfg_file(配置文件引擎) -App/cfg_file.c · 双端共享 - -INI 解析 / 序列化(逐键校验 · 容错 · 自愈) - -外科手术式保存(保留注释 / 未知段 → 降级安全) - -2s 变更轮询(f_stat)+ 段变更回调 - -出厂模板生成 · 旧 NCFG blob 迁移 - -段适配(键表) -段 RAM 副本 = 模块静态结构体(稳态 0 增量) - -net_cfg [network]:ip / mask / gateway / mac - -未来:[adc] · [rs485] · [ftp] ...(三步接入) - -消费者 -读 / 写段 RAM 副本 + 应用 - -netTask:组网 / 重建(代次号自愈) - -UI 网口设置页:显示 / 编辑 / 保存(零改动) - -未来模块:cfg_file_section_data() 取段数据 + +STOR 原地覆写(无 tmp+rename):FTP 侧写入不受引擎保护 + +写权限受 [ftp] write 门控;路径规范化封死 .. 穿越 + +/config.ini(通用配置文件) +FatFS 卷根(NAND GD5F2GQ5UE · dhara FTL) + +INI 文本:[段] / key=value / ; 注释 + +设备写:/config.TMP → unlink 旧文件 → rename(引擎侧原子) + +变更识别:size + FAT 打包 mtime(2 s 粒度) + +cfg_file(配置文件引擎) +App/cfg/cfg_file.c · 双端共享 + +非破坏性解析 · 逐键校验 · 越界/坏值保持出厂默认 + +外科手术式保存(保留注释 / 未知段 → 降级安全) + +2 s 轮询 + 4 s 沉降;视图一致性守卫拒绝坏视图 + +出厂模板生成(段注册后按 RAM 副本渲染) + +段适配(键表) +段 RAM 副本 = 模块静态结构体(稳态 0 增量) + +8 段已满(= CFG_FILE_MAX_SECTIONS):network / sntp / ftp / comm + +adc_cal / adc_cal_ofs / adc_cal_sec / run_par + +新增段 = 调一次 cfg_file_register()(三步接入) + +消费者 +读 / 写段 RAM 副本 + 应用 + +netTask:组网 / 重建([network] 唯一挂回调的段) + +UI 网口设置页:显示 / 编辑 / 保存(零改动) + +默认任务:cfg_file_poll(); 模块按需 cfg_file_save() - - - - - - - - -稳态 RAM 零增量(段数据 = 模块既有结构体);暂态堆缓冲 ≤9KB(仅 load/save 时);FF_FS_REENTRANT=1 卷锁串行化多任务 FS 访问;模拟器端 = 宿主文件 sim_config.ini(同核心) + + + + + + + + +稳态 RAM 零增量(段数据 = 模块既有结构体);暂存缓冲 9,217 B(4KB 读入 + 5KB 输出,仅 load/save 时);FF_FS_REENTRANT=1 卷锁串行化多任务 FS 访问;模拟器端 = 宿主文件 sim_config.ini(同核心) diff --git a/docs/产品需求/产品需求设计文档.md b/docs/产品需求/产品需求设计文档.md index bfe9bcd8..9296877e 100644 --- a/docs/产品需求/产品需求设计文档.md +++ b/docs/产品需求/产品需求设计文档.md @@ -353,10 +353,10 @@ flowchart LR - 判据:FTP 改 IP 后 2~5 s 生效(网络重建含等链路 ≤3 s)。 **FR-CFG-4 旧档迁移**(已取消 · 2026-10-10) -- 原需求:旧固件的 `/cfg/netcfg.bin`(NCFG 二进制)首启自动迁移为 INI 并删除旧文件——**已取消,不再实现**。 +- 二进制旧档的首启自动迁移**不再实现**(配置只以 `/config.ini` 为唯一载体)。 - 现口径:旧档若存在则被**忽略**(不读、不删、不迁移),配置直接回落工厂默认(`/config.ini` `[network]` 段模板)。 - 取消理由:该一次性路径把整条配置写链(`net_cfg_save → cfg_file_save → cfg_store_write_atomic → f_rename → FatFS → dhara FTL`)挂进启动期调用图,defaultTask 静态最坏栈链 +600 B;本仓库不存在携带旧档的设备。 -- 判据:卷上残留 `/cfg/netcfg.bin` 时开机不产生任何迁移日志、旧档不被改动,`/config.ini` 按工厂默认生成。 +- 判据:卷上残留旧格式文件时开机不产生任何迁移日志、旧文件不被改动,`/config.ini` 按工厂默认生成。 **FR-CFG-5 扩展性** - 需求:未来所有模块配置(ADC/RS-485/端口等)进同一文件、按段组织。 @@ -1409,7 +1409,7 @@ mac=00:00:00:00:00:00 | 需求 | 要求 | 落地 | |---|---|---| | **自恢复** | 任务死锁/饿死/断言死循环 → 硬件复位 | IWDG + 心跳位图(§5.7) | -| **掉电不丢配置** | 配置原子写(旧档迁移已取消,2026-10-10) | tmp+rename;旧 `/cfg/netcfg.bin` 不读、不删、不迁移 | +| **掉电不丢配置** | 配置原子写 | tmp+rename;引擎是唯一写者 | | **错误可追溯** | 错误码分级/透传/留痕,不吞不猜 | 错误码通道四要素(分类/透传/分级/留痕) | | **存储自愈** | 卷损坏可探针/自愈;丢弃可观测 | 卷健康探针 + 丢弃计数 + TX 僵尸通道自愈 | | **不误杀** | 正常长耗时窗口不触发复位 | 启动段不喂狗 + 心跳切片 + 白名单 | @@ -1805,7 +1805,7 @@ pre-commit run --all-files | 项目全景架构 | [`docs/架构设计/项目架构总览.md`](../../docs/架构设计/项目架构总览.md) | | 存储架构 | [`docs/架构设计/STM32F4-Base存储架构说明.md`](../../docs/架构设计/STM32F4-Base存储架构说明.md) | | 启动顺序 / 就绪屏障 | [`docs/架构设计/项目启动顺序管理.md`](../../docs/架构设计/项目启动顺序管理.md) | -| 配置引擎 | [`docs/子系统设计/配置文件设计说明.md`](../../docs/子系统设计/配置文件设计说明.md) | +| 配置引擎 | [`docs/子系统设计/配置文件设计.md`](../../docs/子系统设计/配置文件设计.md) | | 网络配置 | [`docs/子系统设计/网络配置说明.md`](../../docs/子系统设计/网络配置说明.md) | | 墙钟时间 | [`docs/子系统设计/系统墙钟时间维护说明.md`](../../docs/子系统设计/系统墙钟时间维护说明.md) | | 运行/自检记录 | [`docs/子系统设计/运行与自检记录设计说明.md`](../../docs/子系统设计/运行与自检记录设计说明.md) | diff --git a/docs/内存管理/RAM使用分析与优化方案.md b/docs/内存管理/RAM使用分析与优化方案.md index 0d972fcc..df73c309 100644 --- a/docs/内存管理/RAM使用分析与优化方案.md +++ b/docs/内存管理/RAM使用分析与优化方案.md @@ -436,10 +436,10 @@ void vApplicationMallocFailedHook(void) 四个杠杆(按收益排序,只动自研代码,第三方 dhara/FatFS/HAL 一行未改): -1. **删旧档 NCFG 迁移**(`Drivers/BSP/NETCFG/net_cfg_legacy.c/h` 整体移除,链 −600 B): +1. **删旧配置迁移路径**(`Drivers/BSP/NETCFG/net_cfg_legacy.c/h` 整体移除,链 −600 B): `net_cfg_load` 无条件调 `net_cfg_legacy_migrate()`,把整条配置**写**链(`net_cfg_save → cfg_file_save → s_write_template → s_write_doc → cfg_store_write_atomic → f_rename → FTL`) - 挂进启动期调用图。本仓库无携带 `/cfg/netcfg.bin` 的设备 → 按"不保留旧档兼容"处置; + 挂进启动期调用图。本仓库无携带旧档的设备 → 按"不保留旧档兼容"处置; 旧档若存在则**忽略**(不读、不删、不迁移),配置回落工厂默认。 2. **6 处局部 `FIL` 改函数级 `static`**(每处 −528 B):`Lib/FatFs/ffconf.h` `FF_FS_TINY=0` → `FIL` 内含 `buf[FF_MAX_SS]` = 512 B。落点 `App/log/sys_log_mcu.c`(3 处)、 diff --git a/docs/子系统设计/网络配置说明.md b/docs/子系统设计/网络配置说明.md index 672e658a..38531a12 100644 --- a/docs/子系统设计/网络配置说明.md +++ b/docs/子系统设计/网络配置说明.md @@ -13,7 +13,7 @@ |---|---| | **可配置** | IP 地址、子网掩码、默认网关、MAC 地址 | | **入口** | 屏幕:`通讯参数 → 网口设置`(修改密码 `123` 可编辑;查看密码 `000` 只读) | -| **存储** | NAND Flash 文件 `/config.ini`(INI `[network]` 段,通用配置文件,掉电不丢);旧版 `/cfg/netcfg.bin` **不再读取**(存在则忽略,见 §3.2) | +| **存储** | NAND Flash 文件 `/config.ini`(INI `[network]` 段,通用配置文件,掉电不丢) | | **生效** | 按「保存」后**立即生效**(网络重建约 1~3s,既有 TCP 连接会断开,无需重启) | | **工厂默认** | `192.168.31.100` / `255.255.255.0` / `192.168.31.1` / 芯片内置 MAC | @@ -81,24 +81,11 @@ mac=00:00:00:00:00:00 - **加载校验(逐键容错)**:单个键值非法(越界/格式错)不影响其他键——非法键回工厂默认并留痕(`parse: [network] <键> value invalid`);文件缺失 → 自动生成本模板;文件 > 4KB = 损坏 → 工厂默认 + 重建模板(坏配置不带崩网络)。 - **保存(外科手术式)**:注释 / 未知段 / 未知键逐行保留,受管键按 RAM 副本原位改写、缺失键补写;写盘 = 临时文件 `/config.TMP` + `f_rename` 原子替换(掉电不损坏目标文件)。 -- **外部变更自动重载**(P4 阶段接入):2s 轮询检测 (size, mtime) 变化、4s 沉降(连续 2 次轮询稳定)后重载 `[network]` 段并重建网络——FTP 上传编辑后的文件无需重启即生效。重载/保存均过**视图一致性守卫**(2026-11-05):卷层给回与 stat 不符的旧/短视图时拒绝应用/拒绝落盘(RAM 配置不会被坏视图打回出厂默认,文件也不会被短视图重写吞段),2s 重试自愈;若串口持续出现 `[CFG] poll ... HELD` / `WARN ... held` 告警,重启一次即收敛。机制详见 `docs/子系统设计/配置文件设计说明.md` §5.3。 +- **外部变更自动重载**(P4 阶段接入):2s 轮询检测 (size, mtime) 变化、4s 沉降(连续 2 次轮询稳定)后重载 `[network]` 段并重建网络——FTP 上传编辑后的文件无需重启即生效。重载/保存均过**视图一致性守卫**(2026-11-05):卷层给回与 stat 不符的旧/短视图时拒绝应用/拒绝落盘(RAM 配置不会被坏视图打回出厂默认,文件也不会被短视图重写吞段),2s 重试自愈;若串口持续出现 `[CFG] poll ... HELD` / `WARN ... held` 告警,重启一次即收敛。机制详见 `docs/子系统设计/配置文件设计.md` §3.3、§3.4。 -完整设计(引擎 / 双端 / 并发模型 D15)见 `docs/子系统设计/配置文件设计说明.md`。 +完整设计(引擎 / 双端 / 写者收敛与视图守卫)见 `docs/子系统设计/配置文件设计.md`。 -### 3.2 旧版二进制档 `/cfg/netcfg.bin`:已不再兼容(2026-10-10) - -旧版格式 `/cfg/netcfg.bin`(28B:magic `'NCFG'` + ver + CRC-16/MODBUS + 18B 字段)的"首次启动自动迁移"已于 **2026-10-10 删除**(`net_cfg_legacy.c/h` 整体移除,该格式的解析代码不再存在)。 - -| `/config.ini` | `/cfg/netcfg.bin` | 行为 | -|---|---|---| -| 缺失 | 存在(有效或无效都一样) | 生成出厂模板 → **工厂默认**;旧档**被忽略**(不读、不删、不迁移) | -| 缺失 | 缺失 | 生成出厂模板 → 工厂默认 | -| 存在 | — | 正常加载现有文件 | - -- **删除理由**:该一次性路径把整条配置**写**链(`net_cfg_save → cfg_file_save → cfg_store_write_atomic → f_rename → FatFS → dhara FTL`)无条件挂进启动期调用图,使 defaultTask 的 armlink 静态最坏栈链 **+600 B**;而本仓库不存在携带旧档的设备。 -- **对使用方的影响**:卷上仍残留旧档的设备升级本版本后,网口配置**回落工厂默认**(`192.168.31.100/24`、gw `.1`),需经 UI「网口设置」或 FTP 上传 `/config.ini` 重新配置。 - -### 3.3 存储后端(双端) +### 3.2 存储后端(双端) `App/cfg/cfg_file_store.h` 定义接口(带路径的文件操作),两平台各一份实现(互斥编译): @@ -109,7 +96,7 @@ mac=00:00:00:00:00:00 INI 格式两端一致,文件可互读;mtime 变更令牌:MCU = FAT 打包时间(`fdate<<16\|ftime`,分钟级粒度),模拟器 = 宿主文件 mtime(秒级)。 -### 3.4 `[ftp]` 段:FTP 鉴权(2026-10-11 新增) +### 3.3 `[ftp]` 段:FTP 鉴权(2026-10-11 新增) FTP 服务(lftpd,:21)自 2026-10-11 起启用口令鉴权。配置与 `[network]` **同文件、同引擎**(`net_cfg_load()` 内统一注册,键表与工厂默认见 `Drivers/BSP/NETCFG/ftp_cfg.c/h`;鉴权状态机在 `Drivers/BSP/NET/lftpd/lftpd_auth.c/h`,纯函数、主机侧可测): @@ -150,7 +137,7 @@ write=1 > **诚实边界**:FTP 协议**明文传输口令**——本方案挡的是**未授权访问**,不挡**网络嗅探**(CH395F 上实现 TLS 不现实)。`pwd_hash` 只防 `/config.ini` 泄露后的**离线口令还原**,盐 = 用户名(无每机随机盐,局域网威胁模型下够用)。 -### 3.5 `[run_par]` 段:运行参数(2026-10-25 新增,「数据可信」运行参数段一项) +### 3.4 `[run_par]` 段:运行参数(2026-10-25 新增,「数据可信」运行参数段一项) 遥测死区 9 点的持久载体(引擎属主 defaultTask;**现场远程写入口 = 104 段 `0x8030~0x8038`**,本段键供 FTP 手改 / 排障直读,逐项语义与地址表 → @@ -172,7 +159,7 @@ write=1 - 生效链路:load/poll 原地改副本 → 104 COV 每拍实时读(无回调,104 改读快照与运行参数段契约); 104 写成功 → `SYS_EVT_RUN_PAR_SAVE` → 本任务 `cfg_file_save()` 整档外科落盘。 -### 3.6 `[comm]` 段:IEC 60870-5-104 通讯参数(2026-10-26 新增,「UI 补齐」波次里的非 UI 前置项,道G task-8) +### 3.5 `[comm]` 段:IEC 60870-5-104 通讯参数(2026-10-26 新增,「UI 补齐」波次里的非 UI 前置项,道G task-8) 104 被控站的公共地址与监听端口持久载体(段属主 = `App/protocol/iec104/` 配对模块: 工厂默认与合法带见 `iec104_comm.h`、键表与实现见 `iec104_comm.c`;注册 @@ -203,7 +190,7 @@ write=1 `CFG_FILE_MAX_SECTIONS`(8)或合并既有段。 段/键上限的机器钉 = `test/host/cfg_file` TC-CONFIG-056/057。 -### 3.7 `[sntp]` 段:SNTP 校时参数(2026-11-04 新增,板端 SNTP 客户端接入,#57 调研结论) +### 3.6 `[sntp]` 段:SNTP 校时参数(2026-11-04 新增,板端 SNTP 客户端接入,#57 调研结论) 板端 SNTP 客户端的持久载体(消费方 = `App/sync/time_sntp.c`,挂在 timeSyncTask 既有循环上,48 字节报文走 PR #74 后的 UDP socket 通道;数据本体 = diff --git a/docs/子系统设计/运行与自检记录设计说明.md b/docs/子系统设计/运行与自检记录设计说明.md index 8183b410..c8d2e5db 100644 --- a/docs/子系统设计/运行与自检记录设计说明.md +++ b/docs/子系统设计/运行与自检记录设计说明.md @@ -32,7 +32,7 @@ | 项 | 现状 | |---|------| | 卷 | GD5F2GQ5UE SPI NAND **256MB**(2048 块 × 64 页 × 2KB),dhara FTL(磨损均衡/坏块管理),FatFS FAT32,`FF_USE_LFN=0` → **文件名必须 8.3** | -| FTP | lftpd 根目录 = 卷根 `/`;现有文件 `/config.ini`(8.3);旧档 `/cfg/netcfg.bin` 已废弃——现固件不读不写也不删(仅可能残留在旧设备上) | +| FTP | lftpd 根目录 = 卷根 `/`;现有文件 `/config.ini`(8.3) | | 并发 | `FF_FS_REENTRANT=1` + 系统锁(`ffsystem.c` osMutex):单次 `f_*` 调用受保护,**多调用序列(open→seek→write→close)需单写者纪律** | | 墙钟 | `sys_clock_get_str(buf,20)` → `"YYYY-MM-DD HH:MM:SS"`;`sys_clock_get()` → Unix 秒(失败 = `UINT32_MAX`);SD2506 RTC + timeSyncTask TCP 校时 | | 挂载 | defaultTask:`f_mount(forced=1)`,空卷自动 `f_mkfs` FAT32,成功后置 `SYS_EVT_STORAGE_READY` | diff --git a/docs/子系统设计/配置文件设计.md b/docs/子系统设计/配置文件设计.md new file mode 100644 index 00000000..7f941deb --- /dev/null +++ b/docs/子系统设计/配置文件设计.md @@ -0,0 +1,346 @@ +# 配置文件设计 + +> **读者**:要改配置引擎、接入新配置段、或排查"配置没生效"的人。 +> **读这一篇能得到什么**:当前 `/config.ini` 这套配置体系的**现状设计**——文件长什么样、引擎怎么解析与落盘、谁在什么时候写、异常时退到哪儿。 +> **相关文档**:`网络配置说明.md`(各段键表与使用手册)· `项目架构总览.md`(分层位置)· `docs/内存管理/RAM预算台账.md`(内存数字台账) + +本文只描述**现在**的系统。演进过程不在此展开,需要时查 `git log` 与代码文件头注释。 + +## 1. 概览 + +`/config.ini` 是**设备全部配置的唯一持久载体**:一份可读可编辑的 INI 文本,放在 FatFS 卷根,PC 端可经 FTP 下载/编辑/上传,设备端也可改写(UI 保存、104 写定值、标定落盘)。 + +围绕它有两层代码: + +| 层 | 文件 | 职责 | +|---|---|---| +| 引擎(双端共享) | `App/cfg/cfg_file.c` / `cfg_file.h` | 段注册、INI 解析校验、外科手术式保存、出厂模板生成、2 s 变更轮询与沉降、视图一致性守卫 | +| 平台后端(每端一份) | `App/cfg/cfg_file_store.h`、`cfg_file_store_mcu.c`、`simulator/sim/cfg_file_store_sim.c` | 带路径的文件读写/stat/原子写/删除,以及平台堆(`cfg_store_alloc`) | + +引擎本身**不认识任何具体配置项**:配置项由各模块以"段"为单位注册(段名 + 段 RAM 副本 + 键表 + 可选变更回调),引擎只按注册表解析与回写。新增配置项 = 新模块注册一个段,不动引擎。 + +## 2. 文件格式 + +### 2.1 位置与容量 + +| 端 | 路径 | 说明 | +|---|---|---| +| MCU | `/config.ini` | FatFS 卷根(NAND GD5F2GQ5UE,经 dhara FTL);原子写的临时文件为 `/config.TMP` | +| FTP | `/config.ini` | lftpd 的 root = `/`,即卷根 | +| 模拟器 | `sim_config.ini` | 进程工作目录下的宿主文件;临时文件 `sim_config.ini.tmp` | + +容量与命名全部是编译期常量(`App/cfg/cfg_file.h`): + +| 常量 | 值 | 含义 | +|---|---|---| +| `CFG_FILE_MAX_LEN` | 4096 B | 文件字节上限;超过即判损坏 | +| `CFG_FILE_MAX_SECTIONS` | 8 | 可注册段数上限(**当前已用满**,见 §4.1) | +| `CFG_FILE_MAX_NAME_LEN` | 15 | 段名/键名最长字符数 | +| `CFG_FILE_MAX_KEYS` | 16 | 单段键数上限 | +| `CFG_FILE_SETTLE_CNT` | 2 | 沉降去抖轮数(2 轮 × 2 s 轮询 = 4 s) | + +文件名 `config.ini` 满足 8.3 短名约束(`FF_USE_LFN=0`);段名与键名是**文件内容**,不受 8.3 限制,但受上表 15 字符限制。 + +### 2.2 语法 + +| 元素 | 规则 | +|---|---| +| 段头 | `[name]`;`[` 开头且含 `]`;段名大小写**不敏感**匹配,尾部空白裁掉 | +| 键值行 | `key=value`;键名大小写不敏感;值取首个 `=` 之后到行尾,首尾空白裁掉 | +| 注释 | 整行 `;` 或 `#` 开头(允许行首空白);**不支持行内注释** | +| 空行 / 未知段 / 未知键 | 加载时忽略,保存时**原样保留** | +| 重复键 | 后者生效 | +| 行长 | 超过 127 字符(`CFG_LINE_TEST_MAX`)的行整行跳过 | +| 行尾 | 设备写出 CRLF;解析同时接受 LF 与 CRLF | +| 编码 | UTF-8(内容实际为 ASCII);解析为字节定向,不依赖代码页 | + +### 2.3 出厂模板 + +文件缺失(首启、或 FTP `DELE` 后重建)时,引擎按当前已注册段的 RAM 副本渲染一份新文件:固定 7 行文件头注释 + 每段 `[段名]` 与 `key=value` 行。 + +文件头原文(`s_doc_header`,cfg_file.c): + +```ini +; ============================================================ +; STM32F4-Base 通用配置文件 +; 格式: INI (UTF-8 / CRLF),本文件为配置唯一来源 +; - 以 ';' 或 '#' 开头的行为注释,保存时保留 +; - 未知段/键保存时保留;缺失段/键保存时写回 +; - 编辑后经 FTP 上传或设备端保存,设备 4s 沉降后自动重载 +; ============================================================ +``` + +模板**不含逐段逐键的中文注释**——段落顺序与键序就是注册顺序与键表顺序。 + +### 2.4 值类型 + +键表里每个键声明一种类型,解码/编码由引擎统一完成(`cfg_val_type_t`): + +| 类型 | 文件里怎么写 | 落到 RAM 的形态 | 非法值处理 | +|---|---|---|---| +| `CFG_VAL_UINT` | 十进制(容许 `0x` 前缀) | `len` ∈ {1,2,4} 的无符号整数,小端 | 该键保持工厂默认 | +| `CFG_VAL_INT` | 十进制,可带 `+`/`-` | `len` ∈ {1,2,4} 的有符号补码,小端(范围按宽度收窄) | 同上 | +| `CFG_VAL_IP4` | `a.b.c.d` | `uint8_t[4]` | 同上 | +| `CFG_VAL_MAC` | `xx:xx:xx:xx:xx:xx`(`-`、`.` 亦可作分隔符) | `uint8_t[6]` | 同上 | +| `CFG_VAL_STR` | 原始字符串 | `char[len]`(自动补 NUL) | 同上 | + +**逐键容错**:某个键写错不牵连别的键——非法值只让该键回落到工厂默认并打一条 `DBG_ERROR`,下次保存时该行会被改写成合法当前值(自愈)。 + +## 3. 运行时机制 + +### 3.1 上电加载 + +```mermaid +sequenceDiagram + participant DT as defaultTask + participant FS as FatFS(NAND 卷) + participant NT as netTask + DT->>DT: net_cfg_load():注册 [network] / [ftp] / [sntp] 段 + DT->>DT: 各模块初始化时注册其余段([adc_cal] 三段 / [run_par] / [comm]) + DT->>FS: cfg_file_load():f_stat → 读文件 → 解析入各段 RAM 副本 + alt 文件缺失 / 为空 + DT->>FS: 生成出厂模板写盘(工厂默认) + else 文件 > 4096 B + DT->>FS: 判损坏 → 工厂默认 + 重建模板 + 自检留痕 + end + DT->>NT: 置 SYS_EVT_STORAGE_READY(卷挂载 + 配置就绪) +``` + +- **先注册后加载**:任何段必须在 `cfg_file_load()` 之前完成注册,否则文件里该段不会被解析。 +- 加载前各段 RAM 副本先整体复位为工厂默认,再逐行解析覆盖——因此"文件里缺键"天然等价于"取工厂默认"。 +- `net_cfg_load()` 在置 `SYS_EVT_STORAGE_READY` **之前**调用(`App/task/default_task.c`),使"存储就绪"这一屏障本身蕴含"配置已就绪"。 + +### 3.2 保存:外科手术式行替换 + +`cfg_file_save()` 不改写整份文件,而是**只替换受管键所在的行**,其余内容(注释、空行、未知段、未知键、段落顺序)逐字节保留。 + +```mermaid +flowchart TD + A["cfg_file_save()"] --> B{"f_stat 文件"} + B -->|"缺失 / 空 / > 4096B"| C["按当前 RAM 副本整份重建"] + B -->|"正常"| D["读入全文(≤4096B)"] + D --> E{"视图一致性守卫"} + E -->|"不过"| F["返回 -1:原文件一字节不动"] + E -->|"通过"| G["Pass 1:记下每个受管键所在行号"] + G --> H{"受管键行 > 127B ?"} + H -->|"是"| I["返回 -1:拒存(避免写出重复键)"] + H -->|"否"| J["Pass 2:逐行输出——受管键换当前值,其余原样"] + J --> K["段末尾补写缺失键;整段缺失则补整段"] + K --> L{"输出 > 4096B ?"} + L -->|"是"| M["返回 -1:拒存(自检留痕 save overflow)"] + L -->|"否"| N["s_write_doc → 后端原子写(.TMP + rename)"] +``` + +三条硬性性质: + +1. **降级安全**:未来固件写的段/键,在旧固件保存时被原样保留,不会因为"不认识"被抹掉。 +2. **引擎永不产出超长文件**:`s_write_doc()` 是唯一落盘闸口(模板与外科保存共用),超过 `CFG_FILE_MAX_LEN` 直接拒写。否则下一次开机 `load` 会把整份文件判损坏并销毁用户注释。 +3. **失败即原文件不动**:任何一步失败返回 -1,由调用方保留脏标志重试。 + +### 3.3 变更检测:2 s 轮询 + 沉降去抖 + +`cfg_file_poll()` 由**配置引擎属主任务**周期调用(MCU = defaultTask,2 s;模拟器主循环 2 s),检测"文件被别人改了"(典型是 FTP 上传或编辑器直接改模拟器文件): + +1. `f_stat` 取 `(size, mtime)`,与基线比对; +2. 连续 `CFG_FILE_SETTLE_CNT`(2)轮稳定才认为改动结束(4 s); +3. 沉降后先判 `size > 4096`:是则各段复位工厂默认 + 重建模板 + 自检留痕,**不触发段回调**(保留在线修复通道); +4. 否则读全文并过**视图一致性守卫**(§3.4); +5. 通过后逐段"复位默认 + 解析",挂回调的段若内容有变则调用其回调;最后刷新基线。 + +外部编辑到生效的总延迟约 2~5 s(4 s 沉降 + 网络重建时间)。 + +> 文件在位但内容被判无效时,poll 会保持 RAM 与基线不动并每 2 s 重试;持续被拒会在串口打 `[CFG] poll ... HELD` 与限速 WARN。 + +### 3.4 视图一致性守卫 + +存储层(dhara FTL + FatFS)在某些情形会给回**与 stat 不符的旧视图**:读长不足、读到同尺寸的全 0 旧映射块、或读取过程中文件被改写。若照单应用,后果是全段静默回到工厂默认(且随后落盘把工厂值写回文件,永久覆盖用户整定);若按短视图外科保存,则会吞掉后面的段。 + +因此 poll 与 save 在读入后都要过双重判据: + +| 判据 | 否决条件 | +|---|---| +| stat → read → 再 stat 三点一致 | 读长 ≠ `st.size`,或读期间 `(size, mtime)` 变化 | +| 内容合法(`s_view_bad`) | 长度为 0,或任一偏移出现 NUL 字节(INI 文本约定下不可能出现) | + +- poll 侧:拒绝应用 → RAM 与基线都不动,2 s 重试自愈;首次拒绝打 `DBG_ERROR` 并上报自检,此后每 30 轮一条 WARN。 +- save 侧:拒绝落盘 → 返回 -1,原文件不动,调用方保留脏标志重试。 +- **已知边界**:内容合法但整体陈旧的视图(size/mtime/内容三者一致地滞后)引擎层无法检出——那属于卷层课题,重启是最后防线。 + +### 3.5 写者归属 + +`/config.ini` 的直接写者**收敛为 defaultTask**(配置引擎属主): + +| 写入来源 | 路径 | +|---|---| +| UI 网口设置页保存 | UI 只投递"待保存"标志 + 事件通知,由 defaultTask 调 `net_cfg_save_run() → cfg_file_save()` | +| 104 写运行参数 `[run_par]` | 置脏 → 事件通知 → defaultTask 调 `cfg_file_save()` | +| 104 写标定 `[adc_cal]` 三段 | `adc_cal_set()` / `adc_cal_set_ratio()` 改 RAM 副本 → 属主统一落盘 | +| 出厂模板 / 损坏重建 | 引擎内部(load / poll / save 失败分支) | + +**唯一例外是 FTP 上传(lftpd 的 `STOR`)**:它在 ftpTask 里直接原地覆写 `/config.ini`,不经引擎,也没有"临时文件 + rename"。它的后果由 poll 的沉降去抖与视图一致性守卫兜住(看到完整的新文件才重载);上传中途断开时 lftpd 会删除截断文件并回 `450`。因此"4 s 沉降 + 一致性守卫"不只是体感优化,它是 FTP 这条非原子写入路径的安全网。 + +## 4. 段与键表 + +### 4.1 已注册段总览 + +当前注册 **8 段 = 段上限 8,已满**。新增配置领域必须先扩 `CFG_FILE_MAX_SECTIONS`(并复核 RAM)。 + +| 段名 | 键数 | 段数据所有者 | 变更回调 | 生效方式 | +|---|---|---|---|---| +| `[network]` | 4 | `Drivers/BSP/NETCFG/net_cfg.c` | 有(触发网络重建) | 保存后立即重建网络 | +| `[ftp]` | 3 | `Drivers/BSP/NETCFG/ftp_cfg.c` | 无 | 下次登录生效 | +| `[sntp]` | 4 | `Drivers/BSP/NETCFG/sntp_cfg.c` | 无 | 客户端每轮直读 RAM 副本 | +| `[comm]` | 2 | `App/protocol/iec104/iec104_comm.c` | 无 | 改后重启生效 | +| `[adc_cal]` | 9 | `App/algo/adc_cal.c` | 无 | 消费方按拍直读 | +| `[adc_cal_ofs]` | 8 | `App/algo/adc_cal.c` | 无 | 同上 | +| `[adc_cal_sec]` | 8 | `App/algo/adc_cal.c` | 无 | 同上 | +| `[run_par]` | 9 | `App/algo/run_par.c` | 无 | 104 COV 每拍直读 | + +各段键名、工厂默认值、合法域与语义 → `docs/子系统设计/网络配置说明.md` §1 与 §3.3~§3.6,以及被引用的算法文档。本表只给"有哪几段、归谁、怎么生效"。 + +### 4.2 注册顺序 + +注册由各模块自己的初始化函数完成,引擎按注册顺序建立段表: + +- `net_cfg_load()` 内统一注册 `[network]` / `[ftp]` / `[sntp]`(`s_net_register()` / `ftp_cfg_register()` / `s_sntp_register()`); +- 其余段由各模块在 `cfg_file_load()` 之前的初始化路径里注册(`adc_cal` 三段、`run_par`、`iec104_comm`)。 + +### 4.3 接入一个新段 + +```c +/* 1) 段 RAM 副本 + 工厂默认(两份同布局,默认值建议共用宏避免漂移) */ +typedef struct { + uint16_t sample_ms; + char channel[16]; +} demo_cfg_t; + +static demo_cfg_t s_demo = { 100u, "ch1" }; +static const demo_cfg_t s_demo_defaults = { 100u, "ch1" }; + +/* 2) 键表(const,落 Flash):键名 + 类型 + 段结构体内偏移与长度 */ +static const cfg_key_def_t s_demo_keys[] = { + { "sample_ms", CFG_VAL_UINT, offsetof(demo_cfg_t, sample_ms), sizeof(uint16_t) }, + { "channel", CFG_VAL_STR, offsetof(demo_cfg_t, channel), sizeof(s_demo.channel) }, +}; + +/* 3) 注册(必须在 cfg_file_load() 之前调用;段名重复或超段上限返回 -1) */ +static void demo_cfg_init(void) +{ + static const cfg_section_reg_t s_sec = { + .name = "demo", + .p_data = &s_demo, + .p_defaults = &s_demo_defaults, + .data_size = sizeof(demo_cfg_t), + .p_keys = s_demo_keys, + .key_count = 2u, + .p_cb = NULL, /* 需要"改了就提醒"时挂回调,见 §4.4 */ + }; + (void)cfg_file_register(&s_sec); +} +``` + +约束: + +- 段 `data_size` **≤ 64 字节**(`CFG_SNAP_MAX`)才能挂变更回调——引擎用这块快照做"变没变"的比较; +- 段数上限 8、单段键数上限 16、段名/键名 ≤ 15 字符; +- `p_data` / `p_defaults` / `p_keys` 的生命周期必须覆盖引擎全部使用期(放静态区); +- 新 `.c` 双端注册(uvprojx + `simulator/CMakeLists.txt`),见 `new-source-file` skill。 + +### 4.4 段变更回调 + +```c +void cfg_file_register_cb(const char *p_name, cfg_section_cb_t p_cb); +``` + +回调在**触发重载的任务上下文**里调用(即 defaultTask),语义是"这个段的内容真的变了"。`[network]` 用它复用既有的网络重建路径(置 `SYS_EVT_NET_RECONFIG`,netTask 下一节拍重建网络、代次号 +1、TCP 服务自愈)。 + +没有回调的段,消费方自己按拍从 RAM 副本读——这是多数段的做法,也更省事。 + +## 5. 接口 + +### 5.1 引擎 API(`App/cfg/cfg_file.h`) + +```c +int cfg_file_register(const cfg_section_reg_t *p_reg); /* 注册段:0 成功 / -1 参数非法·段名重复·超上限 */ +int cfg_file_load(void); /* 加载全部段:0 正常(含回退默认)/ -1 严重错误 */ +int cfg_file_save(void); /* 保存全部段:0 成功 / -1 失败(原文件不动) */ +void cfg_file_poll(void); /* 变更检测 + 沉降去抖 + 重载 + 回调 */ +void *cfg_file_section_data(const char *p_name); /* 取段 RAM 副本;未注册 = NULL */ +void cfg_file_register_cb(const char *p_name, cfg_section_cb_t p_cb); +``` + +### 5.2 平台后端(`App/cfg/cfg_file_store.h`,每端一份实现) + +| API | 语义 | +|---|---| +| `cfg_store_read(path, buf, cap, &nread)` | 读文件,至多 `cap` 字节 | +| `cfg_store_write_atomic(path, buf, len)` | 原子写:同目录临时文件 → 关闭 → 删既有目标 → rename | +| `cfg_store_stat(path, &st)` | `(size, mtime)`;`mtime` 只作等值比较的变更令牌 | +| `cfg_store_remove(path)` | 删除文件(恢复出厂用得上) | +| `cfg_store_config_path()` / `cfg_store_tmp_path()` | 本端的目标路径与临时路径 | +| `cfg_store_alloc/free/heap_free` | 平台堆三件套:MCU = FreeRTOS 堆(`pvPortMalloc`),模拟器 = 宿主 CRT 堆 | + +**MCU 端原子写**:`/config.TMP` 写完关闭 → `f_unlink` 既有 `/config.ini` → `f_rename`。先删再改名,是因为本 FatFs 移植的 `f_rename` 对已存在目标返回 `FR_EXIST` 且不覆盖(上游同语义);模拟器端 MSVC CRT 的 `rename` 同样不覆盖,两端写法对称。`unlink → rename` 之间有个极短的非原子窗口,落在其中掉电等于配置文件暂时消失,开机 `load` 会按"首启"重建出厂模板——丢失边界只有本次正在保存的内容。 + +**卷锁**:MCU 端每次文件操作前后经 `vol_lock_wait()` / `vol_lock_release()` 与 FatFS 的 `FF_FS_REENTRANT` 卷互斥协同;取不到锁时**本轮直接跳过**返回 -1(保存请求由 2 s 轮询重试,不阻塞 FTP 传输)。 + +**为什么所有 KB 级缓冲都走 `cfg_store_alloc`**:ARMCC 默认 MicroLIB,C 库堆只有 512 B,直接 `malloc` 必失败且症状是"配置静默回落到工厂默认"。引擎自身(`cfg_file.c`)不出现 `malloc`。 + +## 6. 内存占用 + +当前构建实测(`scripts/ram_baseline.tsv` 与 Keil map): + +| 项 | 数值 | 性质 | +|---|---|---| +| `cfg_file.o` 代码 | 6,768 B | Flash | +| `cfg_file.o` RO 数据 | 1,484 B | Flash(键表/常量) | +| `cfg_file.o` RW + ZI | 882 + 20 B | 稳态静态区(段表、行号表、快照、基线) | +| `cfg_file_store_mcu.o` 代码 | 380 B | Flash | +| `ffsystem.o`(FatFS 互斥 + `get_fattime`) | 204 B 代码 + 30 B RW + 20 B ZI | 稳态 | +| 段 RAM 副本 | **0 增量** | 复用各模块既有静态结构体 | +| 读缓冲 | ≤ 4,097 B | **暂态**(FreeRTOS 堆,load/save/poll 期间) | +| 写出缓冲 | ≤ 5,120 B | **暂态**(同上) | +| 堆峰值(save 期间) | ≤ 9,217 B | 由 `CFG_HEAP_BUDGET`(10,240 B)**编译期钉死**:放大缓冲宏会先编译失败 | + +`cfg_file.c` 里那条 `typedef char cfg_heap_peak_check_t[...]` 就是这道编译期闸:改 `CFG_FILE_MAX_LEN` 或 `CFG_OUT_MAX` 会立刻报负数组大小,逼改动者重新核对平台堆余量,而不是让它在真机上退化成运行期分配失败。 + +平台总量台账(SRAM1 / CCM / Flash 的当前百分比与余量)→ `docs/内存管理/RAM预算台账.md`。 + +## 7. 异常与恢复 + +| 场景 | 行为 | +|---|---| +| 文件不存在 / 为空 | 生成出厂模板,全部段取工厂默认 | +| 文件 > 4096 B | 判损坏:占全部工厂默认 + 重建模板 + 自检留痕 `cfg load oversized` | +| 个别键非法 | 该键工厂默认 + `DBG_ERROR`;下次保存自愈 | +| 受管键行 > 127 B(手工编辑造成) | 拒绝保存,原文件不动 + 自检留痕 `cfg save line too long` | +| 外科输出会超过 4096 B | 拒绝保存,原文件不动 + 自检留痕 `cfg save overflow` | +| 卷层给回旧/短/含 NUL 的视图 | poll 拒绝应用(RAM 与基线不动,2 s 重试);save 拒绝落盘(原文件不动) | +| FTP 上传到一半断开 | lftpd 删除截断文件回 `450`;设备侧 poll 看到文件消失即保持 RAM 不动 | +| 运行期文件被 `DELE` 删掉 | RAM 保持现值;引擎在文件首次缺失时打 `poll: config file GONE`,此后每 30 轮一条 WARN | + +**恢复出厂**:经 FTP `DELE /config.ini` 后**重启**——上电 `load` 发现文件缺失即重建出厂模板。运行期不重建模板,是因为 FTP 上传正在写的那份文件不能被打断;文件缺失期 RAM 保持旧值,设备继续按旧配置运行。 + +自检留痕标签(`syslog_post(SYSLOG_SELFTEST, ...)`):`cfg load oversized`、`cfg template write fail`、`cfg heap alloc fail`、`cfg save read fail`、`cfg save view stale`、`cfg poll view stale`、`cfg save line too long`、`cfg save overflow`、`cfg save write fail`。 + +## 8. 双端差异 + +| 项 | MCU | 模拟器 | +|---|---|---| +| 后端 | `cfg_file_store_mcu.c`(FatFS) | `simulator/sim/cfg_file_store_sim.c`(宿主文件) | +| 文件 | `/config.ini`(+ `/config.TMP`) | `sim_config.ini`(+ `sim_config.ini.tmp`) | +| mtime 来源 | FAT 打包时间 `(fdate << 16) \| ftime`(**2 s 写入粒度**,时间源 = `get_fattime()` 取的 `sys_clock` 墙钟) | 宿主文件系统(毫秒级) | +| 变更轮询 | defaultTask,2 s | 主循环,2 s | +| 堆 | FreeRTOS 堆(`pvPortMalloc`) | 宿主 CRT 堆(不参与 MCU 预算) | +| `[network]` 段"生效" | 重建网络 | 仅刷新 RAM 副本(无网络实体) | +| 自检 | `simulator/main.c --cfgtest` 无头场景 | 同左 | + +`get_fattime()` 在 `Lib/FatFs/ffsystem.c` 里直引用 `sys_clock` 墙钟(与调试日志时间戳同源),因此设备写出的文件时间与日志时间一致、FTP `LIST` 也显示真实时间;墙钟无效时回退固定值,此时变更检测退化为仅比 size。 + +## 9. 已知限制 + +- **段落上限已满**:8/8,新增领域必须先扩宏并复核 RAM。 +- **FTP 上传非原子**:`STOR` 原地覆写,可靠性依赖沉降去抖与视图一致性守卫;上传中掉电会留下截断文件(下次上电按 4096 B 界限判损坏或按逐行容错解析)。 +- **"一致但陈旧"的整卷视图**引擎无法检出:三判据都对,只是内容整体滞后——属卷层(dhara/FTL)课题,"改过配置后重启一次"在卷层根治前始终是最稳口径。 +- **行长度与文件长度是硬边界**:受管键行超过 127 B 会被拒存;文件超过 4,096 B 被判损坏并整份重建(注释与未知段随之丢失)。 +- **无格式版本号行**:靠宽容解析(未知段/键保留)支撑向前兼容。 diff --git a/docs/子系统设计/配置文件设计说明.md b/docs/子系统设计/配置文件设计说明.md deleted file mode 100644 index 1091c8ee..00000000 --- a/docs/子系统设计/配置文件设计说明.md +++ /dev/null @@ -1,572 +0,0 @@ -# 配置文件设计说明 - -> **版本**:v1.0(设计与实施规划) -> **日期**:2026-09-13 -> **状态**:设计完成,按 §10 分阶段实施 -> **关联文档**:`网络配置说明.md`(使用手册)· `项目架构总览.md` · `BSD_Socket_API_使用指南.md` · `docs/内存管理/`(RAM 依据) - -## 1. 背景与目标 - -### 1.1 需求 - -| # | 需求 | 说明 | -|---|---|---| -| 1 | 网络参数配置写入一个文件 | IP / 掩码 / 网关 / MAC 持久化到文件 | -| 2 | 文件可经 FTP 下载 / 上传 | PC 端编辑后上传,设备自动检测并生效 | -| 3 | 通用配置文件 | 以后所有模块配置(ADC / RS-485 / 端口等)都进这个文件,按"段"组织 | -| 4 | 易于文本编辑器编辑 | 格式为纯文本、带注释、自解释 | - -### 1.2 现状与差距分析(2026-09-13 调研) - -**已有资产**(直接复用): - -- 网络配置已是 FatFS 上的文件:`/cfg/netcfg.bin`(28 字节二进制 blob:magic 'NCFG' + ver + CRC-16/MODBUS + 18B 字段)——**但为二进制,不可编辑** -- FTP 通道现成:lftpd root = `/`(= FatFS 卷根),端口 21,支持 RETR / STOR / LIST / DELE -- 双端后端抽象模式现成:`net_cfg_store.h`(MCU = FatFS / 模拟器 = 宿主文件,互斥编译,格式两端一致) -- "立即应用"骨架现成:`net_cfg_set_apply_cb` / `net_cfg_request_apply` → `SYS_EVT_NET_RECONFIG` → netTask 重建网络 → 代次号 → TCP 服务自愈 - -**差距**(本设计的解决项): - -| # | 差距 | 后果 | 设计解法 | -|---|---|---|---| -| ① | 二进制格式 | 编辑器无法编辑 | INI 文本格式(§3) | -| ② | 无通用机制 | 仅网络模块有配置 | 段键表引擎(§4.3) | -| ③ | FatFS 零并发保护(`FF_FS_REENTRANT=0`,应用层无锁) | ftpTask(FTP 传输)/ defaultTask(配置加载/保存/轮询)并发触卷 → 卷损坏 | `FF_FS_REENTRANT=1` + ffsystem.c 互斥钩子(§5.4)+ 三条一致性协议(D15:tmp+rename / 沉降去抖 / 单写者) | -| ④ | 无变更检测 | FTP 上传后不生效(需重启) | defaultTask 每 2s `f_stat` 轮询(size + mtime)(§5.3) | -| ⑤ | `FF_FS_NORTC=1` → 时间戳恒为固定值 | mtime 不变 → 无法检测"同尺寸编辑"(如 IP 尾段 100→200) | `NORTC=0` + `get_fattime`(复用调试日志墙钟源,运行期零 I2C)(§5.5) | -| ⑥ | 固件升级无迁移 | 现场设备升级后用户配置丢失(回工厂默认) | NCFG blob → INI 自动迁移(§9;该迁移已于 2026-10-10 撤销) | -| ⑦ | RAM 静态区仅余 5.8 KB(占用 95.4%) | 不能新增大静态缓冲 | 稳态零增量 + 暂态堆缓冲(§8) | - -### 1.3 非目标(本期不做) - -- 其他模块(ADC / RS-485 / 密码 / 端口等)的配置页与配置项——机制预留,逐项后续接入 -- FTP 鉴权 / 权限(维持现状:无鉴权) -- DHCP 等网络模式扩展(仅 4 参数) -- 内存优化专项(LVGL 池迁 CCM 等,独立规划;本设计不依赖它) - -## 2. 设计决策摘要 - -| # | 决策点 | 选择 | 理由(含否决项) | -|---|---|---|---| -| D1 | 文件格式 | **INI 文本**(`[section]` / `key=value` / 注释行) | 否决 JSON:无现成解析器(需引入,RAM 开销大)、编辑器不友好(无注释、尾逗号等);INI 零依赖、原生可编辑 | -| D2 | 文件位置 | **`/config.ini`(卷根)** | FTP 顶层直接可见;8.3 文件名兼容(`FF_USE_LFN=0` 约束);否决 `/cfg/config.ini`(藏子目录,多一级) | -| D3 | 编码 / 行尾 | UTF-8(内容实际 ASCII);设备写 **CRLF**;解析容忍 LF / BOM | 编辑器友好;解析为字节定向,不依赖代码页 | -| D4 | 保存策略 | **外科手术式行替换**(受管键替换,其余原样保留) | 否决整文件重生成:用户注释 / 未知段会被抹掉;**降级场景**(新文件被旧固件读)会抹掉未来版本段落;外科保留 = 降级安全 | -| D5 | RAM 模型 | **段键表引擎**:段 RAM 副本 = 各模块既有静态结构体;键表(const)映射 键 → 结构体偏移 | 稳态 RAM 零增量;未来模块"注册一个段"即接入 | -| D6 | 文件变更检测 | defaultTask 每 2s `f_stat`,比对 (size, mtime) | 否决内容哈希轮询(每 2s 全文件读,NAND 读流量浪费);mtime 检测依赖真实时间戳(见 D8) | -| D7 | 线程安全 | **`FF_FS_REENTRANT=1` + `Lib/FatFs/ffsystem.c`**(`ff_mutex_*` → osMutex,超时 1s) | FatFs 官方可重入方案;每次 f_* 调用加锁/放锁(逐次粒度,不长期阻塞 FTP 传输);`FR_TIMEOUT` 防死锁 | -| D8 | 时间戳 | **`FF_FS_NORTC=0` + `get_fattime`(复用 `sys_clock` 墙钟,与调试日志时间戳同源)** | mtime 为真实时间、与日志时间戳一致;墙钟 = 开机 RTC 基准 + tick 推进,运行期零 I2C;墙钟无效时回退固定值(有开机告警) | -| D9 | 写原子性 | **一切写入 = 临时文件 + `f_rename`**(设备保存 / FTP STOR / 出厂模板统一:先写 `<名>.TMP`(8.3 兼容),关文件成功后 rename 目标) | 同目录 rename = 单目录项写入,卷锁下原子 → 并发读者只见"旧完整文件或新完整文件",半截窗口结构消除;STOR 中途掉电仅残留 TMP(无害);逐键容错 + 下次保存自愈为次级兜底 | -| D10 | 旧档迁移 | ~~新固件首启:NCFG blob → INI,迁移后删除旧文件~~ → **2026-10-10 撤销:旧档不读、不删、不迁移** | 原理由:防现场升级丢配置;否决"保留旧文件"(双数据源、陈旧值混乱)。**撤销理由**:该一次性路径把整条配置写链(`net_cfg_save → cfg_file_save → cfg_store_write_atomic → f_rename → FatFS → dhara FTL`)挂进启动期调用图,defaultTask 静态最坏栈链 +600 B;本仓库不存在携带旧档的设备 → 按"不保留旧档兼容"处置(见 §9.1) | -| D11 | 出厂默认 | 文件缺失时**自动生成带注释模板**并落盘 | 首启 / 恢复出厂场景;文件自身即"格式手册" | -| D12 | 恢复出厂 | **FTP `DELE /config.ini`**(下次上电重新生成) | 本期不做 UI 入口(开放问题 O1) | -| D13 | 双端 | 共享 `cfg_file` 核心 + 平台 store 后端(沿用 net_cfg_store 模式) | MCU = FatFS `/config.ini`;模拟器 = 宿主文件 `sim_config.ini` | -| D14 | 网络配置 API 兼容 | **`net_cfg_*` 签名与语义不变** | UI 页零改动;load/save 内部改走 cfg_file | -| D15 | FS 并发模型 | **A+ = REENTRANT 互斥(D7)+ 三条一致性协议**:① 写 tmp+rename(D9 扩至 FTP STOR)② 轮询沉降去抖((size,mtime) 连续 2 次稳定才重载)③ 配置文件单写者收敛 defaultTask(UI 保存 = 请求投递至 defaultTask,落盘 + 应用由其执行) | 互斥保证单次调用原子(G1–G4:卷锁全程持有 / 1s 有界等待 / 优先级继承;锁固定 2 把,不随任务/文件数增长);三协议结构性消除"半截可见 / 重载抖动 / 多写者"缺口;**否决 actor 方案(单所有者队列 + 全代理层)**:经完整评估——代理 ~400–600 行、FILE 句柄表、符号改名/影子头、RAM +4KB、所有者自死锁纪律、双端适配,其两项结构优势(整操作原子 / 单所有者)可由上述三协议以 ~40 行等价中性化;实际并发仅 3 消费任务、写偶发 → 机制成本与收益不匹配;D13 store 后端抽象预留将来换入 actor 的空间(MCU 端后端实现) | - -## 3. 文件格式规范 - -### 3.1 文件标识 - -| 端 | 路径 | 说明 | -|---|---|---| -| MCU | `/config.ini` | FatFS 卷根(NAND GD5F2GQ5UE,经 dhara FTL) | -| FTP | `/config.ini` | lftpd root = `/`,即卷根 | -| 模拟器 | `sim_config.ini` | 进程工作目录(宿主文件) | - -### 3.2 语法规则 - -| 元素 | 规则 | -|---|---| -| 参数行 | `key=value`;key 为 `[a-z0-9_]+`(大小写敏感);值 = 首个 `=` 后至行尾,首尾空白去除 | -| 段头行 | `[section]`;段名 `[a-z0-9_]+`(大小写敏感) | -| 注释行 | `;` 或 `#` 开头(允许行首空白) | -| 空行 | 允许,保存时原样保留 | -| 段外键 | 加载时忽略(不设默认段),保存时原样保留 | -| 重复键 | 后者生效 | -| 行长 | > 127 字符 → 该行视为无效(键忽略,原文保留) | -| 文件大小 | > 4096 B → 整文件视为损坏 → 工厂默认 + 下次保存重生成 | -| 编码 | UTF-8;容忍文件头 BOM | -| 行尾 | CRLF(设备写出)/ LF 均接受 | -| 行内注释 | v1 不支持(防止值中 `;` 被误解析);注释仅占整行 | - -### 3.3 出厂模板(首启自动生成) - -```ini -; ========================================================== -; DTU 通用配置文件 -; 语法: [段名] 段头 / key=value 参数 -; 以 ';' 或 '#' 开头的行为注释 -; 编辑后经 FTP 上传 (端口 21, 路径 /config.ini) -; 编辑后数秒内自动检测并生效 (2~6s, 含沉降去抖) -; (网络段变化将重建网络, 既有连接会断开) -; ========================================================== - -[network] -; IP 地址 -ip=192.168.31.100 -; 子网掩码 -mask=255.255.255.0 -; 默认网关 -gateway=192.168.31.1 -; MAC 地址 (全零 = 使用芯片内置 MAC) -mac=00:00:00:00:00:00 -``` - -### 3.4 值类型 - -| 类型 | 语法 | 示例 | 解析失败处理 | -|---|---|---|---| -| `CFG_VAL_IP4` | `a.b.c.d`(兼容前导零) | `192.168.31.100` | 该键取工厂默认 + `DBG_ERROR` | -| `CFG_VAL_MAC` | `aa:bb:cc:dd:ee:ff`(十六进制) | `a4:94:b6:12:34:56` | 同上 | -| `CFG_VAL_UINT` | 十进制(兼容 `0x` 前缀) | `7003` | 同上 | -| `CFG_VAL_STR` | 原始字符串(≤31 字符) | `hello` | 同上 | - -### 3.5 校验与容错(逐键粒度) - -- **逐键解析**:单个键值非法不影响其他键;非法键取工厂默认值(RAM 结构体以工厂默认静态初始化) -- **自愈**:下次保存时该行被写回合法当前值 -- **未知内容保留**:未知段 / 键 / 注释 / 空行在保存时原样保留(外科保留) - -| 场景 | 加载行为 | 保存行为 | -|---|---|---| -| 文件不存在 | 工厂默认;自动生成模板(旧 blob 若存在也**不读、不迁移**,见 §9.1) | 写当前值 | -| 段缺失 / 键缺失 | 该键取工厂默认 | 补写缺失键(段缺失则补整段) | -| 键值非法 | 该键取工厂默认 + 日志 | 重写为合法值(自愈) | -| 文件 > 4KB / 严重损坏 | 全部工厂默认 + `DBG_ERROR` | 整文件重生成(已知数据保留,未知内容丢失) | -| 未来版本多出的段(降级) | 忽略 | **原样保留**(降级安全的核心) | -| FTP 上传半截(掉电) | 逐行解析:完整行生效,尾部残行忽略 | 下次保存自愈 | - -## 4. 总体架构 - -### 4.1 分层 - -![通用配置文件分层架构](../images/配置文件分层架构.svg) - -### 4.2 模块清单 - -| 文件 | 动作 | 端 | 职责 | -|---|---|---|---| -| `App/cfg/cfg_file.h` / `cfg_file.c` | **新增** | 双端 | 配置文件引擎:段注册 / 解析 / 外科保存 / 变更轮询 | -| `App/cfg/cfg_file_store.h` | **新增** | 双端 | 平台文件后端接口 | -| `App/cfg/cfg_file_store_mcu.c` | **新增** | MCU | FatFS 后端(`/config.ini`:读 / stat / 原子写 / 删) | -| `simulator/sim/cfg_file_store_sim.c` | **新增** | 模拟器 | 宿主文件后端(`sim_config.ini`) | -| `Lib/FatFs/ffsystem.c` | **新增** | MCU | REENTRANT 卷互斥钩子(osMutex)+ `get_fattime`(直引用墙钟,与日志时间戳同源) | -| `Lib/FatFs/ffconf.h` | 修改 | MCU | `FF_FS_REENTRANT` 0→1;`FF_FS_NORTC` 1→0 | -| `App/util/sys_clock.c/.h` | 修改 | MCU | 新增 `sys_clock_get_time()`(日历时间获取器);改归 ④中间件层,自 `App/` 移入 `App/util/` | -| `Drivers/BSP/NETCFG/net_cfg.h/.c` | 重构 | 双端 | `net_cfg_*` API 不变;load/save 走 cfg_file;注册 `[network]` 段 | -| `App/protocol/iec104/iec104_comm.h/.c` | 新增(2026-10-26 道G task-8) | 双端(模拟器编译但无消费路径) | 配对模块注册 `[comm]` 段(104 ca/端口,ca 1..65535 / port 1024..65535 合法带 getter 自愈);内核前 `iec104_comm_init()` 注册(同 [adc_cal]/[run_par] 先注册后 load 契约);生效 = 改后重启(键表与理由 → `docs/子系统设计/网络配置说明.md` §3.6);**不走 scripts 白名单**——`.c` 按 new-source-file 口径同笔进 uvprojx + `simulator/CMakeLists.txt` | -| `Drivers/BSP/NETCFG/sntp_cfg.h/.c` | **新增**(2026-11-04 板端 SNTP 接入) | 双端(模拟器编译但无 sync 消费路径) | 注册 `[sntp]` 段(enable / server1 / server2 IPv4 字面量 / poll_min 1..1440 getter 带外自愈,iec104_comm 先例);`net_cfg_load()` 内与 [ftp] 同编排注册;消费方 `App/sync/time_sntp.c`(MCU 单端链接);键表与生效口径 → `docs/子系统设计/网络配置说明.md` §3.7;new-source-file 口径 uvprojx + `simulator/CMakeLists.txt` 双登记 | -| `Drivers/BSP/NETCFG/net_cfg_legacy.c` | **已删除**(2026-10-10;曾于 P3 新增) | — | 旧 NCFG blob 解析 + 迁移;随"不保留旧档兼容"整体移除(见 §9.1) | -| `Drivers/BSP/NETCFG/net_cfg_store.h` / `net_cfg_store_mcu.c` | **删除** | — | 由 `cfg_file_store.*` 取代 | -| `simulator/sim/net_cfg_store_sim.c` | **删除** | 模拟器 | 由 `cfg_file_store_sim.c` 取代 | -| `App/task/net_task.c` | 修改 | MCU | 主循环每 2s `cfg_file_poll()` | -| `simulator/main.c` | 修改 | 模拟器 | 主循环每 2s 轮询 | -| `simulator/CMakeLists.txt` | 修改 | 模拟器 | 源文件列表 | -| `MDK-ARM/STM32F407-Demo.uvprojx` | 修改 | MCU | 新文件注册 | - -### 4.3 关键机制 - -**(1) 段键表引擎** —— "以后所有配置进这个文件"的落地形式: - -```c -/* 键定义:键名 + 类型 + 段结构体内的偏移/长度 */ -typedef struct { - const char *name; /* 键名 */ - cfg_val_type_t type; /* 值类型(UINT / IP4 / MAC / STR) */ - uint16_t offset; /* 值在段 RAM 结构体内的偏移 */ - uint16_t len; /* 值在结构体内的字节数 */ -} cfg_key_def_t; - -/* 段注册:段名 + 模块自有 RAM 副本 + 键表 */ -typedef struct { - const char *name; /* 段名,如 "network" */ - void *data; /* 段 RAM 副本(模块自有静态变量) */ - const cfg_key_def_t *keys; /* 键表(const,Flash) */ - uint16_t key_count; -} cfg_section_reg_t; -``` - -- 加载:逐行解析文件 → 受管键(键表命中)按偏移写入对应模块结构体 -- 保存:按各段结构体当前值生成受管键行 -- 未来模块接入三步:① 定义段结构体(静态,工厂默认初始化)② 写键表 ③ `cfg_file_register()` - -**(2) 外科手术式保存** —— 降级安全的核心: - -```mermaid -flowchart TD - A["cfg_file_save()"] --> B{"读 /config.ini"} - B -->|"不存在"| C["生成出厂模板(全部已注册段 + 注释)"] - B -->|"存在且 ≤ 4KB"| D["逐行扫描"] - B -->|"> 4KB(按损坏处理)"| E["用 RAM 数据整文件重生成"] - D --> F{"行类型"} - F -->|"受管键(已注册 key)"| G["替换为当前 RAM 值,标记 seen"] - F -->|"注释 / 空行 / 未知行"| H["原样保留"] - G --> I{"有键未写入?"} - H --> I - I -->|"是"| J["段末补写缺失键;段缺失则补整段"] - I -->|"否"| K["写 /config.ini.tmp"] - C --> K - E --> K - J --> K - K --> L["unlink 既有目标 + f_rename → /config.ini(原子替换;本 FatFs 移植 f_rename 不覆盖已存在目标,v1.12)"] - L --> M["f_stat 更新变更基线"] -``` - -- **超长守卫(v1.12)**:外科手术输出超 `CFG_FILE_MAX_LEN`(4096B)→ **拒绝保存返回 -1**(原文件不动 + 自检上报 `cfg save overflow`);`s_write_doc`(模板/外科同闸的唯一落盘出口)硬性拒写 >4096B —— "引擎永不产出超长 config.ini"是结构性质。超长文件一律按损坏处置(load/poll 同判,见 §5.3) - -**(3) 变更检测** —— `cfg_file_poll()`(defaultTask / 模拟器主循环,2s 节拍):`f_stat` 取 (size, mtime-epoch) 与基线比对 → 沉降后**先按真实 `st.size` 查超长**(v1.12):>4096B = 损坏 → 各段复位工厂默认 + 重建模板 + 自检留痕(`cfg poll oversized`),**不触发段回调**(保留 FTP/在线修复通道,下次保存或重启收敛);正常路径重载先过**视图一致性守卫**(v1.17,§5.3):stat → read → 再 stat 三点必须一致且**内容过 `s_view_bad`**——读长 ≠ `st.size`、读期间 (size, mtime) 变化、或视图含 NUL 字节(同尺寸全 0 旧映射块,台架实证形态)= 卷层给了旧/短/坏视图或读中被改写 → 拒绝应用(RAM 不动、基线不刷新,2s 重试自愈);一致后重载 → 段变化则触发该段回调 → 刷新基线。 - -重载循环的**解析器契约(v1.19)**:`s_parse()` 必须保持**非破坏性**——入参 `const char *`,逐行拷进本函数栈缓冲(`CFG_LINE_TEST_MAX + 1`)再交给会就地 NUL 化的行切分函数。因为热重载是"对同一份读缓冲按段各解析一轮",旧的就地解析在第一段就把 `=` 与 `\n` 全改写成 NUL,后面各段再也读不到键值行 → 静默全体保持出厂默认,而紧随的落盘会把这份出厂值写回文件、**永久覆盖用户整定**(台架 2026-11-12 实证:104 写入 r/b 当场读得回,约 8 s 后被打回出厂值;开机 `load` 只解析一轮故始终正确,缺陷因此长期不可见)。 - -**(4) 段变更回调** —— 未来模块的接入点: - -```c -void cfg_file_register_cb(const char *p_name, cfg_section_cb_t p_cb); -/* 网络模块注册:[network] 段变化 → 置 SYS_EVT_NET_RECONFIG(复用既有重建路径) */ -``` - -## 5. 运行时机制 - -### 5.1 上电加载 - -```mermaid -sequenceDiagram - participant DT as defaultTask - participant NT as netTask - participant FS as FatFS(NAND卷) - participant NS as 网络层 - DT->>FS: f_mount(无卷自动 f_mkfs) - alt /config.ini 不存在 - DT->>FS: 生成出厂模板 → 写 /config.ini(旧 /cfg/netcfg.bin 不读、不删、不迁移) - end - DT->>FS: net_cfg_load():读 /config.ini → 解析入 RAM 副本(含 [network] 段注册) - DT->>NT: 置 SYS_EVT_STORAGE_READY(= 卷挂载 + 配置就绪) - NT->>NT: 等 SYS_EVT_STORAGE_READY(≤10s) - NT->>NS: net_init(ip, mask, gw, mac)(读 RAM 副本,不触卷) - NT->>NT: 置 SYS_EVT_NET_READY -``` - -- 存储异常(挂载超时 / 失败):回退工厂默认继续组网(与现状一致,不阻塞网络) - -### 5.2 UI 保存 - -```mermaid -sequenceDiagram - participant UI as uiTask(网口设置页) - participant NC as net_cfg - participant DT as defaultTask - participant CF as cfg_file - participant FS as FatFS - participant NT as netTask - UI->>NC: ↑/↓ 调整 → net_cfg_set(实时入 RAM,不写盘) - UI->>NC: ENTER 保存行 → net_cfg_save_request()(置"待保存" + 保存行显 WAIT) - NC->>DT: 置 SYS_EVT_CFG_SAVE(立即唤醒属主任务) - DT->>CF: net_cfg_save_run() → cfg_file_save() - CF->>FS: 读旧文件 → 外科式行替换 → 写 /config.ini.tmp - CF->>FS: f_rename → /config.ini(原子) - CF->>FS: f_stat 更新变更基线 - DT->>NC: 保存成功 → 置 OK 并 net_cfg_request_apply() - NC->>NT: 置 SYS_EVT_NET_RECONFIG - NT->>NT: 下一节拍 net_stack_bring_up()(重建网络,代次 +1) -``` - -- UI 端只投递"保存"意图(微秒级,不触卷不碰引擎);写盘与"立即应用"全部在属主任务 defaultTask 串行执行 -- 保存失败(存储异常)→ 保存行显 ERR(不触发应用,网络保持原配置) - -### 5.3 FTP 上传自动生效 - -```mermaid -sequenceDiagram - participant PC as PC(编辑器+FTP客户端) - participant FT as ftpTask(lftpd) - participant FS as FatFS - participant DT as defaultTask - participant NT as netTask - PC->>FT: RETR /config.ini(下载) - FT->>FS: f_open/f_read(逐次加锁即放) - Note over PC: 编辑器修改(如改 IP) - PC->>FT: STOR /config.ini(原地覆盖) - FT->>FS: f_write/f_close(mtime 更新) - loop 每 2s - DT->>FS: f_stat(size + mtime) - end - DT->>DT: 与基线不同 → 重新加载(解析入 RAM 副本) - alt [network] 段有变化 - DT->>NT: 置 SYS_EVT_NET_RECONFIG → 重建网络(代次 +1,服务自愈) - else 仅其他段变化 - DT->>DT: 仅更新 RAM,触发对应段回调 - end -``` - -- 检测粒度 2s(轮询周期);网络重建含等链路 ≤3s,总生效时间约 2~5s -- 下载天然支持(RETR 即文件读) -- **沉降去抖(D15-②)**:(size, mtime) 相对基线变化且**连续 2 次轮询(4s)稳定**才重载——防 FTP STOR 进行中的中间态触发多次重载(STOR 先写 `.TMP`,目标文件 (size, mtime) 在 rename 前不变,rename 单次原子更新 → 至多一次重载) -- **视图一致性守卫(D15-②延伸,v1.17,issue #73 台架实证)**:沉降触发重载后校验 stat → read → 再 stat 三点一致(读长 == `st.size` 且读期间 (size, mtime) 不变)+ **内容判据 `s_view_bad`**(INI UTF-8 文本约定下任何偏移都不得含 NUL 字节——空读、整块全 0、合法前缀+尾部零填充的"部分旧块"全部命中;解析器遇 NUL 断行,被零块吞掉的尾部内容本就不可达,拒之不误伤任何可解析文档)。台架 15:21 实证:**同尺寸整块全 0 旧映射块可骗过纯三点校验**(dhara 旧映射块恰与目标同长 1064B),解析零错误、RAM 照打回——内容判据是该形态的唯一引擎侧拦截。不一致视图若照单应用 = 全段静默打回工厂默认(真机 8/8 复现、10.5h 对时沉默元凶),若照短视图外科保存 = 重写整档吞段(实测 1066B→986B)。拒绝语义:poll **不动 RAM、不刷基线**(2s 重试,视图收敛即自愈;持续到重启则 boot 全新挂载必然收敛);save **返回 -1**(调用方保留 dirty 标志重试)原文件一字节不动。首次拒绝 ERROR + 自检留痕(`cfg poll view stale` / `cfg save view stale`),poll 此后每 30 趟(≈60s)一条 WARN 计数行(限速);HELD 行打印的是“读长B != stat B”,两数相等即内容判据命中(同尺寸旧映射块)而非空读。**边界**:内容合法但陈旧的整卷视图((size,mtime) 与内容三者皆一致滞后)引擎层无法检出——属卷层(dhara/FTL)领域,重启收敛是最后防线;运行期"改过配置后重启一次"在卷层根治前始终是最稳口径 -- **单写者(D15-③)**:`/config.ini` 的直接写者收敛为 defaultTask(UI 保存 = 请求投递至 defaultTask,由其落盘(tmp+rename)+ 应用(触发网络重建),复用既有 `SYS_EVT_NET_RECONFIG` 路径);唯一例外 = FTP STOR 路径(lftpd,tmp+rename,与 defaultTask 写路径经卷锁互斥) - -### 5.4 线程安全(FatFS 可重入) - -- `FF_FS_REENTRANT=1`:FatFs 为每卷建互斥锁(系统锁 + 卷锁),**每次 f_\* 调用入口加锁、出口放锁**(逐次粒度) -- `Lib/FatFs/ffsystem.c` 实现四个钩子:`ff_mutex_create/delete/take/give` → `osMutexNew/Delete/Acquire/Release`(CMSIS-RTOS V2) -- 锁等待超时 = `FF_FS_TIMEOUT` = 1000ms → 超时返回 `FR_TIMEOUT`(操作优雅失败,下节拍重试,无死锁) -- 并发矩阵(启用后均由卷锁串行化): - -| 任务 | FatFS 访问 | 典型持锁时长 | -|---|---|---| -| defaultTask | f_mount / f_mkfs(启动期)+ 配置加载 / 保存执行 / 2s stat 轮询(配置引擎属主) | 加载/保存秒级(NAND 擦写);轮询毫秒级 | -| netTask | 无直接触卷(配置读 = RAM 副本) | — | -| uiTask | (D15:保存 = 请求投递至 defaultTask,不直接触卷) | — | -| ftpTask | RETR / STOR / LIST / DELE | STOR 秒级(多次短锁) | -| testTask | 存储测试套件(测试构建) | 测试场景 | - -- nand_ftl 页缓存在卷锁范围内调用 → 天然受保护 - -### 5.5 时间戳(变更检测基准) - -- `FF_FS_NORTC=0`:写后关文件时 FatFs 以 `get_fattime()` 的返回值记录 mtime -- `get_fattime`(`Lib/FatFs/ffsystem.c` 内——FatFs 指定的 OS 胶水文件;ff.h 将其声明为"用户方提供"):**直引用 `sys_clock` 墙钟**,与调试日志时间戳同源 - -```c -#include "sys_clock.h" /* 层内中间件引用(sys_clock 归 ④中间件层) */ - -DWORD get_fattime(void) { - if (sys_clock_get_time(&t) != 0) { return FF_TIME_FALLBACK; } /* 日历时间获取器(纯时间 API) */ - return ((t.year-1980)<<25 | t.month<<21 | t.day<<16 | t.hour<<11 | t.minute<<5 | t.second/2); -} -``` - -- **为何直引用而非回调注册**:`get_fattime` 唯一消费者是同库 `ff.c`,唯一外部依赖是时间源;直引用即 C 库平台钩子的标准模式,无注册步骤、无"漏注册 → 时间戳静默退化"失效模式;代价为零——`sys_clock` 是通用平台时间服务(日志/文件/对时共用),**改归 ④中间件层**(实体移入 `App/util/`,与 crc/ringbuf 同目录),二者为**层内引用**,工程无反向依赖 - -- 墙钟由 `sys_clock_init()` 开机建立(读一次 RTC,无效时按 `RTC_DEFAULT_INIT_*` 默认值自动校时),运行期纯 tick 推进——**零 I2C 流量**,与日志时间戳同一机制 -- mtime 为**真实时间**,与调试日志时间戳完全一致(如 `2026-09-13 14:32:05`);FTP LIST 亦显示真实时间 -- 墙钟无效(开机 RTC 读取失败且默认校时也失败的极罕见情形)→ 返回固定值(`FF_NORTC` 约定);检测退化为仅 size,既有开机告警链有效 -- 比较语义:"相对上次是否变化"(非单调性判断);timeSync 校时可能使墙钟跳变 → 至多多一次重载(无害) - -## 6. 接口设计 - -### 6.1 cfg_file 核心 API(双端共享) - -```c -/* App/cfg/cfg_file.h */ - -/* 段变更回调(重载后在触发重载的任务上下文调用;可空) */ -typedef void (*cfg_section_cb_t)(void); - -/* 段注册描述符:name / p_data(段 RAM 副本)/ p_defaults(工厂默认, - * 与 p_data 同布局,load 先复位再解析)/ data_size / p_keys(键表)/ - * key_count / p_cb(变更回调;注册段 data_size 须 ≤ 64 方可挂回调) */ -typedef struct { ... } cfg_section_reg_t; - -int cfg_file_register(const cfg_section_reg_t *p_sec); /* 注册段(首次 load 前;变更回调随段注册) */ -int cfg_file_load(void); /* 0 = 正常(含回退默认)/ -1 = 严重错误 */ -int cfg_file_save(void); /* 0 = 成功 / -1 = 失败(原文件保持不动;输出超 4096B 拒绝保存,v1.12) */ -void cfg_file_poll(void); /* 变更检测 + 沉降去抖(defaultTask / 模拟器主循环,2s 节拍);沉降后先判超长 */ -void *cfg_file_section_data(const char *p_name); /* 取段 RAM 副本(未注册 = NULL) */ -``` - -### 6.2 cfg_file_store 后端接口(每端一份实现,互斥编译) - -```c -/* App/cfg/cfg_file_store.h —— 带路径的文件操作(路径由后端持有,核心不感知平台布局) */ -typedef struct { - uint32_t size; /* 文件长度(字节) */ - uint32_t mtime; /* 变更令牌(仅等值比较;MCU=FAT 时间打包 / 模拟器=宿主时间) */ -} cfg_file_stat_t; - -int cfg_store_read(const char *p_path, uint8_t *p_buf, size_t cap, size_t *p_nread); /* 至多读 cap 字节 */ -int cfg_store_write_atomic(const char *p_path, const uint8_t *p_buf, size_t len); /* 临时文件 + unlink 既有目标 + rename(原子;本 FatFs 移植与 MSVC CRT 的 rename 均不覆盖已存在目标,须先删,v1.9/v1.12 双端对称) */ -int cfg_store_stat(const char *p_path, cfg_file_stat_t *p_st); /* 0 = 存在 / -1 = 缺失/失败 */ -int cfg_store_remove(const char *p_path); - -/* 平台堆(2026-09-18 起):共享核心的 KB 级临时缓冲一律经此三件套取用, - * 核心禁止直接调用 C 库 malloc —— MCU 端 = pvPortMalloc/vPortFree(FreeRTOS 堆), - * 模拟器端 = malloc/free(宿主 CRT 堆,堆余量以 MB 计,不参与 MCU 预算) */ -void *cfg_store_alloc(size_t size); /* 失败返回 NULL,由调用方打印 FAIL */ -void cfg_store_free(void *p_buf); /* NULL 安全;只接受 cfg_store_alloc 返回值 */ -uint32_t cfg_store_heap_free(void); /* 空闲堆诊断量;模拟器恒 0 */ - -/* 路径由后端提供(MCU: /config.ini、/config.TMP; - * 模拟器: sim_config.ini、sim_config.ini.tmp) - * 注:旧版遗留档 getter cfg_store_legacy_path() 已于 2026-10-10 随旧档迁移删除 */ -const char *cfg_store_config_path(void); -const char *cfg_store_tmp_path(void); -``` - -### 6.3 未来模块接入模板 - -```c -/* 示例:未来 [adc] 段 */ -typedef struct { - uint16_t sample_ms; /* 采样周期 */ - char channel[16]; /* 通道名 */ -} adc_cfg_t; - -static adc_cfg_t s_adc_cfg = { 100u, "ch1" }; /* 工厂默认静态初始化 */ -static const adc_cfg_t s_adc_defaults = { 100u, "ch1" }; /* 引擎 p_defaults 目标 */ - -static const cfg_key_def_t s_adc_keys[] = { - { "sample_ms", CFG_VAL_UINT, offsetof(adc_cfg_t, sample_ms), sizeof(uint16_t) }, - { "channel", CFG_VAL_STR, offsetof(adc_cfg_t, channel), sizeof(s_adc_cfg.channel) }, -}; - -void adc_cfg_init(void) -{ - static const cfg_section_reg_t s_sec = { - .name = "adc", - .p_data = &s_adc_cfg, - .p_defaults = &s_adc_defaults, - .data_size = sizeof(adc_cfg_t), - .p_keys = s_adc_keys, - .key_count = 2u, - .p_cb = adc_on_config_changed, /* 可选:变更回调 */ - }; - (void)cfg_file_register(&s_sec); -} -``` - -### 6.4 net_cfg 兼容承诺 - -- `net_cfg_load/save/get/set/get_mac/set_active_mac/fmt_ipv4/set_apply_cb/request_apply` **签名与语义全部不变** -- 内部:`net_cfg_load` = 段注册 + `cfg_file_load`(旧档迁移已于 2026-10-10 删除);`net_cfg_save` = `cfg_file_save` -- UI 页(`ui_page_net_cfg.c`)、启动流程(`net_task.c`)、模拟器入口(`main.c`)的**调用点无需改动** - -## 7. 双端支持 - -| 项 | MCU | 模拟器 | -|---|---|---| -| 后端 | `cfg_file_store_mcu.c`(FatFS) | `cfg_file_store_sim.c`(宿主文件) | -| 文件路径 | `/config.ini`(NAND 卷根) | `sim_config.ini`(工作目录) | -| mtime 来源 | FAT 打包时间(`fdate<<16\|ftime`,分钟级粒度) | 宿主文件系统(秒级) | -| 旧档(已废弃) | `/cfg/netcfg.bin` **不读、不删、不迁移**(2026-10-10) | `sim_netcfg.bin` 同样不读、不删、不迁移 | -| 变更轮询 | defaultTask 2s | 主循环 2s(编辑器改动 2s 内反映) | -| 弃用 | — | `net_cfg_store_sim.c` 删除 | - -模拟器行为:首跑无 `sim_config.ini` → 生成出厂模板(残留的 `sim_netcfg.bin` 被忽略);运行中用编辑器改 `sim_config.ini` → 2s 内自动重载(模拟器无网络重建概念,[network] 段"应用" = 刷新 RAM 副本)。 - -## 8. 内存预算 - -依据:最新 map(2026-09-18 平台堆改造后):静态区(128 KB 映射 SRAM1)已用 **126,064 B(96.2%)**,**余 5,008 B**;FreeRTOS 堆 `configTOTAL_HEAP_SIZE = 49,152 B`,扣除六任务栈 32 KB + 空闲/定时器栈 + IPC 后**空闲 ≈14 KB**(首启实测值直接看串口 `load: OK (…B, heap free …B)` 行)。 - -| 项 | 大小 | 性质 | 说明 | -|---|---|---|---| -| 段 RAM 副本 | **0 增量** | 稳态 | 复用 `net_cfg` 既有 `s_cfg`(22 B,.data) | -| 段表 / 键表 | ~150 B | 稳态(Flash) | const,进 .constdata | -| 段注册表(上限 8 段) | ~350 B | 稳态(.bss) | 含键行号表 256 B / 变更快照 64 B / 基线 16 B(实施实测) | -| REENTRANT 互斥锁 | ~100 B | 稳态(FreeRTOS 堆) | 2 个 osMutex | -| 读缓冲 | ≤ 4,097 B | 暂态(**FreeRTOS 堆**) | `cfg_store_alloc`;`load`/`save`/`poll` 期间 | -| 写出缓冲 | ≤ 5,120 B | 暂态(**FreeRTOS 堆**) | 外科手术输出;溢出 = 放弃保存(原文件不动) | -| **堆峰值(save 期间)** | **≤ 9,217 B** | 暂态 | 编译期由 `CFG_HEAP_BUDGET = 10,240 B` 钉死,放大缓冲宏会先编译失败 | -| **静态 .bss 增量** | **≈ 0.7 KB(实测 706 B)** | 稳态 | 段表 + 行号表 + 快照 + 基线 << 静态余量 5.0 KB ✓ | - -**实现纠偏(2026-09-18)**:本表原设计即"暂态走堆",但实现落在 C 库 `malloc` 上——本机工具链为 **MicroLIB**,其 C 堆只有 `Heap_Size = 0x200`(512 B),真机上 4 处 KB 级分配**必然返回 NULL**,症状是"配置读不到 → 静默回落工厂默认值"(模拟器用宿主 CRT 堆,故两端表现不一致)。现全部改走 `cfg_store_alloc`;map 复核:`malloc.o` 不再参与链接,`Removing startup_stm32f407xx.o(HEAP), (512 bytes)` → **静态区反而净增 512 B 余量**。 - -**结论**:配置引擎自身不需要内存优化专项;但 9,217 B 暂态峰值已占 FreeRTOS 堆空闲的一半以上,且与 lftpd 的每命令分配共存——**流式读写(免整文件驻留内存)仍列为后续优化项**,方案见本文 §6.2/§8(原报告该节已随修复按销项删除,完整记录沉淀在本文)。 - -## 9. 兼容与迁移 - -### 9.1 旧文件迁移:已删除(2026-10-10) - -旧 NCFG 二进制档 `/cfg/netcfg.bin` 的"首启自动迁移"已整体删除(`net_cfg_legacy.c/h` 移除、`cfg_store_legacy_path()` 撤销)。现行为: - -``` -/config.ini 缺失? - ├─ 是 → 生成出厂模板 → 工厂默认 - │ (/cfg/netcfg.bin 无论存在与否、有效与否:不读、不删、不迁移) - └─ 否 → 正常加载现有文件 -``` - -- 旧格式(28B:magic 'NCFG' + ver + CRC-16/MODBUS + 18B 字段)已无解析代码;`App/util` 的 CRC-16/MODBUS 模块随之失去唯一调用者 -- **删除理由**:该一次性路径把整条配置**写**链(`net_cfg_save → cfg_file_save → cfg_store_write_atomic → f_rename → FatFS → dhara FTL`)挂进启动期调用图,使 defaultTask 的 armlink 静态最坏栈链 **+600 B**;而本仓库不存在携带旧档的设备 -- 残留旧档不会被自动清理,需要时可经 FTP 手工删除(该文件对现固件无任何作用) - -### 9.2 现场升级 / 降级场景 - -| 场景 | 行为 | -|---|---| -| 旧设备(有 netcfg.bin)升级新固件 | 旧档**被忽略**(不读、不删、不迁移)→ 配置回落工厂默认,需经 UI / FTP 重新配置(2026-10-10 起) | -| 新设备 / 格式化卷 | 出厂模板 | -| 新文件被**旧固件**读(降级) | 旧固件只读 `/cfg/netcfg.bin`——现固件不读也不删该文件,故它仍在卷上、旧固件按旧值运行;**新旧固件配置互不同步**(已知限制,O2) | -| 含未来段的新文件被旧固件读 | 旧固件不读该文件 → 文件不受影响 | - -### 9.3 恢复出厂 - -- **FTP `DELE /config.ini`** → 下次上电重新生成出厂模板(全段工厂默认) -- 本期无 UI 入口(开放问题 O1) - -## 10. 实施计划 - -| 阶段 | 内容 | 涉及文件 | 验收标准 | -|---|---|---|---| -| **P1** | 设计文档 + 架构图 | 本文档 + `gen_cfg_arch_svg.py` + SVG | 用户确认 §2 决策 | -| **P2** | FS 线程安全 + 时间戳 | `ffconf.h`、`Lib/FatFs/ffsystem.c`(新)、`App/util/sys_clock.c`(sys_clock_get_time;移入中间件层)、`uvprojx`、架构总览 §1.1(sys_clock 改归 ④) | `/keil-build` 0/0;`/serial-boot-test` 启动 PASS(无死锁) | -| **P3** ✅ 2026-09-14 | cfg_file 引擎(双端)+ net_cfg 集成 + 旧档迁移 | `App/cfg/cfg_file.{h,c}`、`App/cfg/cfg_file_store.h`、`App/cfg/cfg_file_store_mcu.c`(新)、`simulator/sim/cfg_file_store_sim.c`(新)、`net_cfg.{h,c}`(段注册 + 引擎化 load/save)、`net_cfg_legacy.c`(新,NCFG 迁移)、删 `net_cfg_store*`、`uvprojx`、`CMakeLists.txt` | Keil `/keil-build` 0/0;模拟器生成 `sim_config.ini` 且编辑回读一致;旧档 NCFG 自动迁移三态正确 —— **已实测通过**(工厂模板 / 编辑回读 / 非法键回退 / 迁移三态 / 双端 0 警告;`cfg_file.c` 含中文注释块 → UTF-8 **带 BOM**) | -| **P4** | 轮询 + D15 协议 + 测试标记 | `net_task.c`(沉降去抖轮询 + 落盘收敛 + 串口标记)、`lftpd.c`(STOR tmp+rename) | `/keil-build`;`/serial-boot-test` 启动新标记(`cfg file: loaded OK` 等);FTP 上传无半截可见(rename 原子) | -| **P5** ✅ 2026-09-14 | 模拟器迁移 | `simulator/sim/cfg_file_store_sim.c`(新)、删 `net_cfg_store_sim.c`、`simulator/main.c`(2s 轮询 + `--cfgtest` 自检)、`CMakeLists.txt` | sim 构建 0 警告 + 4 场景无头 PASS(工厂 / 有效旧档迁移 / 无效旧档保留 / UI 无头);`sim_netcfg.bin` 自动迁移 + 轮询沉降重载 + 外科保存无损回环 —— **已实测通过** | -| **P6** | 文档同步 | `网络配置说明.md`(§0/3/6/7 重写)、`AGENTS.md`、`README.md`、`项目架构总览.md` + `gen_arch_svg.py`、`内容页架构.md`、BSD 指南、`simulator/README.md`、`ui_page_net_cfg.c` 头注释、`docs/测试说明/` | pre-commit 全绿;全库无"Flash"旧表述残留 | -| **P7** | FTP 闭环脚本 | `[计划] test/ftp_config_test.py`(尚未创建,见 §11.2 设计稿)+ 测试说明 | 真机闭环 PASS(§11.2) | -| **P8** | 总回归 + 提交 | `/keil-build` + `/serial-boot-test` + 模拟器 ui-verify + `pre-commit --all-files` | 0 错误 0 警告;全 PASS | - -**建议提交边界**:C1 = P2+P3(FS 线程安全 + 配置引擎)/ C2 = P4+P5(网络配置迁移 + 模拟器)/ C3 = P6+P7(文档 + 测试脚本);P8 发现的收尾问题并入最近一次提交。 - -## 11. 测试计划 - -### 11.1 自动回归 - -| 串口标记 | 场景 | 属性 | -|---|---|---| -| `cfg file: loaded OK` | 上电加载成功 | Required(P4 起纳入 `/serial-boot-test`) | -| `cfg file: factory template written` | 首启 / 删文件 | 条件(手工) | -| `cfg file: legacy NCFG migrated` | 旧文件迁移 | **已作废**(2026-10-10 旧档迁移删除,该标记不再产生) | -| `cfg file: saved OK` | UI 保存 | 条件(手工) | -| `cfg file: change detected` | FTP 上传 | 条件(P7 闭环) | -| `cfg file: ... FAILED` | 任何失败 | Forbidden | - -既有标记不变:`App init OK` / `Network Init` / `net init: OK`;Forbidden:`FAIL` / `ERROR` / `HardFault`。 - -### 11.2 FTP 闭环脚本(`[计划] test/ftp_config_test.py`,尚未创建,以下为设计稿;`.venv` python,stdlib ftplib) - -``` -1. 登录(按 FTP 测试说明的约定) -2. RETR /config.ini → 解析 → 校验格式合法 -3. 注入一条测试注释行 + 修改 IP(尾段 +1) -4. STOR /config.ini -5. 等待 ≤10s → 校验串口日志:"cfg file: change detected" + 网络重建标记 -6. 再次 RETR → 校验:新 IP 生效 且 测试注释保留(外科保留验证) -7. (可选)STOR 损坏文件 → 校验回退默认 + 自愈 -退出码:0 = PASS / 1 = FAIL -``` - -### 11.3 手工用例 - -| # | 用例 | 步骤 | 预期 | -|---|---|---|---| -| M1 | 编辑器往返 | 下载 → Notepad 改 IP + 加注释 → 上传 | 2~5s 内生效;注释保留 | -| M2 | 降级安全 | 文件加 `[future]` 段 → 设备端保存 → 下载 | `[future]` 段原样 | -| M3 | 恢复出厂 | FTP DELE /config.ini → 重启 | 重新生成模板,工厂 IP | -| M4 | 损坏容错 | STOR 一个含非法值 + 残行的文件 | 非法键回退默认,其余键生效,有日志 | -| M5 | 并发 | FTP 下载大文件同时 UI 保存 | 无卷损坏、无死锁(FR_TIMEOUT 重试) | -| M6 | 超长文件 | STOR > 4KB 文件 | 回退默认 + 下次保存重生成 | - -## 12. 风险与开放问题 - -### 12.1 风险 - -| 风险 | 等级 | 缓解 | -|---|---|---| -| FTP STOR 半截写(掉电 / 断链) | 中 | 逐键容错 + 工厂回退;0 字节 STOR 自动删文件(lftpd 现状)→ 下次上电重新生成模板;下次保存自愈 | -| 墙钟无效(开机 RTC 读取失败且默认校时失败) | 低 | mtime 退化为固定值(检测退化为 size);既有开机告警链 + 默认校时(RTC_DEFAULT_INIT)兜底,实际极罕见 | -| mtime 秒边界碰撞(同秒两次写) | 低 | (size, mtime) 双比对;2s 轮询通常跨秒;误报 = 多一次重载(无害) | -| 卷层给回旧/短/坏视图(dhara FTL 缓存 / 整卷回滚 / 同尺寸全 0 旧映射块,台架 issue #73 实证) | 高 | 引擎视图一致性守卫 = 三点校验 + 内容判据 s_view_bad(§4-(3) / §5.3):stat 与 read 不符或视图含 NUL → poll 拒绝应用 / save 拒绝落盘,坏视图既不碰 RAM 也不重写文件;"一致但陈旧"的整卷视图引擎不可检出,属卷层课题,重启收敛兜底 | -| save 堆峰值与 LVGL / lftpd 争用 | 低 | 事件触发(罕见);FR_TIMEOUT 兜底;实施后 map 复核堆空闲 | -| 8.3 文件名约束 | 无 | `config.ini` 合规;段名 / 键名属文件内容,不受限 | -| 卷重新格式化丢配置 | 中(现状一致) | 与现状相同(f_mkfs 清空卷);文档提示 | - -### 12.2 开放问题 - -| # | 问题 | 默认策略(可推翻) | -|---|---|---| -| O1 | UI 是否加"恢复出厂"入口 | 本期不做,用 FTP 删除 | -| O2 | 降级时配置同步策略 | 接受"降级 = 工厂默认" + 文档说明 | -| O3 | 其他模块接入顺序 | 按需(ADC / RS-485 / 端口等) | -| O4 | 文件格式版本号行(如 `; format=1`) | 本期不加(宽容解析已覆盖演进) | diff --git a/docs/文档规范.md b/docs/文档规范.md index cfaba6d3..c874324e 100644 --- a/docs/文档规范.md +++ b/docs/文档规范.md @@ -348,10 +348,10 @@ | **一级标题** | 文档首行为 `# 标题`,标题即主题,不写"文档""说明"等冗余后缀 | | **开篇引言** | 可选 `>` 引用块,写明**适用对象 / 阅读前提 / 相关文档** | | **章节编号** | 多章节文档用 `## 1.` `### 1.1` 编号,便于交叉引用(`§3.2`) | -| **变更记录** | **一律不设该节**(用户裁定 2026-10-28 起新文档、2026-10-29 起老文档也清完;标题变体 `修订记录` / 带编号的 `## 6. 变更记录` / `## 九、变更记录` 同规则)——文档只写现状、改了什么是正文说了算,改动过程查 `git log`。**例外只有 4 篇**(整表保留:表里是真台账、且被别处当唯一证据出处引用):`docs/子系统设计/看门狗设计说明.md`、`docs/子系统设计/配置文件设计说明.md`、`docs/内存管理/RAM预算台账.md`、`docs/UI设计/内容页架构.md`;`scripts/archive/` 下的归档文档整片都是历史,不受本条约束 | +| **变更记录** | **一律不设该节**(用户裁定 2026-10-28 起新文档、2026-10-29 起老文档也清完;标题变体 `修订记录` / 带编号的 `## 6. 变更记录` / `## 九、变更记录` 同规则)——文档只写现状、改了什么是正文说了算,改动过程查 `git log`。**例外只有 3 篇**(整表保留:表里是真台账、且被别处当唯一证据出处引用):`docs/子系统设计/看门狗设计说明.md`、`docs/内存管理/RAM预算台账.md`、`docs/UI设计/内容页架构.md`;`scripts/archive/` 下的归档文档整片都是历史,不受本条约束 | > 数"还剩几篇留着这节"的命令(**别只 grep 裸标题**——带编号的 `## 6. 变更记录`、中文序号的 `## 九、变更记录`、`## 修订记录` 三种写法都要算): -> `grep -rn "^## " docs scripts` 再看命中的标题;2026-10-29 实测 **5 篇**真台账(上表那五篇)+ `scripts/archive/` 归档一篇。 +> `grep -rn "^## " docs scripts` 再看命中的标题;2026-10-29 实测 **5 篇**真台账(上表那五篇)+ `scripts/archive/` 归档一篇;2026-11-15 复测 **3 篇**(上表三篇)+ 归档一篇,与上表一致。 ### 5.2 新增文档检查清单 diff --git a/docs/架构设计/项目启动顺序管理.md b/docs/架构设计/项目启动顺序管理.md index e91d89c9..0348b2b4 100644 --- a/docs/架构设计/项目启动顺序管理.md +++ b/docs/架构设计/项目启动顺序管理.md @@ -131,7 +131,7 @@ if (sys_evtHandle != NULL) { > 因此只在 netTask 上下文广播。 ### Step 3 defaultTask 置位存储就绪(生产者) -在 `defaultTask` 完成 FTL/FatFS 挂载 **+ 网络配置加载(`net_cfg_load`;旧档 `/cfg/netcfg.bin` 不读、不迁移,失败回退工厂默认)** 后: +在 `defaultTask` 完成 FTL/FatFS 挂载 **+ 网络配置加载(`net_cfg_load`;失败回退工厂默认)** 后: ```c osEventFlagsSet(sys_evtHandle, SYS_EVT_STORAGE_READY); ``` diff --git a/docs/架构设计/项目架构总览.md b/docs/架构设计/项目架构总览.md index ffd5bd96..0f1efb72 100644 --- a/docs/架构设计/项目架构总览.md +++ b/docs/架构设计/项目架构总览.md @@ -196,7 +196,7 @@ flowchart LR end ``` -- **配置文件**:通用 `/config.ini`(cfg_file 引擎,静态态单任务收敛于 defaultTask,外部变更 4s 沉降后自动重载),**多段注册制**:`[network]`(net_cfg)/ `[ftp]`(鉴权)/ `[adc_cal]`(标定系数与零点)/ `[run_par]`(运行参数 + 104 地址映射真源)/ `[comm]`(104 ca/端口);新模块加段=注册 `cfg_file` 段处理器,唯一落盘者不变。旧版 NCFG 二进制(`/cfg/netcfg.bin`)已不再读取(2026-10-10 删迁移),存在则忽略。→ `docs/子系统设计/配置文件设计说明.md` +- **配置文件**:通用 `/config.ini`(cfg_file 引擎,静态态单任务收敛于 defaultTask,外部变更 4s 沉降后自动重载),**多段注册制**:`[network]`(net_cfg)/ `[ftp]`(鉴权)/ `[adc_cal]`(标定系数与零点)/ `[run_par]`(运行参数 + 104 地址映射真源)/ `[comm]`(104 ca/端口);新模块加段=注册 `cfg_file` 段处理器,唯一落盘者不变。→ `docs/子系统设计/配置文件设计.md` - **记录九件**:`sys_log` 引擎按 `App/log/sys_log_mcu.c` 的 `s_paths[]` 写**七本**定长 64B 文本记录——`/log/run.log`、`selftest.log`、`accid.log`、`alarm.log`、`start.log`、`oper.log`、`yk.log`;`soe_store` 同先例落 `/log/soe.log`(64B 定长二进制,容量满删除重建);`/log/wdev.log` 是**变长取证行**(掉电不丢,`App/fs/vol_lock` 侧投,不走 64B 定长格式)。defaultTask 是 `/log` 唯一写者(两条队列在 `syslog_drain` 尾部统一排空),FTP 可直下。→ `docs/子系统设计/运行与自检记录设计说明.md` - **录波段文件**:`/wave`(首 boot `mkdir`,8.3 短名 WAVE 前缀轮转、启动扫盘重建下标),段头 + 通道表 + 两处 CRC-16/MODBUS + 整段自检;defaultTask 游标**分次搬运**(每次 64B 批)不独占 FatFs,落盘成功回执才解冻录波环。→ `docs/子系统设计/故障录波设计说明.md` - **实时调试流** = 控制台单写者日志(**UART5**/RS232#1,4KB 环形缓冲 + defaultTask 唯一发送;2026-10-29 因屏板占用 USART1 自 UART5 定态),详见 `docs/子系统设计/控制台单写者日志设计说明.md`。 @@ -393,7 +393,7 @@ flowchart LR | 故障录波(数据面/段格式/回放契约) | `docs/子系统设计/故障录波设计说明.md` | | 屏板链路(USART1 主出口 / 帧 A 帧 B / 实测证据) | `docs/子系统设计/屏板链路设计说明.md` | | IEC 60870-5-104 上行(默认 :2404,可经 `[comm]` 改) | `docs/子系统设计/IEC104上行设计说明.md` | -| 配置文件引擎(/config.ini 多段注册制) | `docs/子系统设计/配置文件设计说明.md` | +| 配置文件引擎(/config.ini 多段注册制) | `docs/子系统设计/配置文件设计.md` | | 运行与自检记录(64B 记录 + SOE) | `docs/子系统设计/运行与自检记录设计说明.md` | | 墙钟时间维护 | `docs/子系统设计/系统墙钟时间维护说明.md` | | 控制台单写者日志(UART5 调试流) | `docs/子系统设计/控制台单写者日志设计说明.md` | diff --git a/docs/测试说明/专项验证/模拟器差距清单.md b/docs/测试说明/专项验证/模拟器差距清单.md index 3609777d..fee1c041 100644 --- a/docs/测试说明/专项验证/模拟器差距清单.md +++ b/docs/测试说明/专项验证/模拟器差距清单.md @@ -52,7 +52,7 @@ | 差距 | 模拟器行为 | 真机行为 | 影响 | 回归补测 | |---|---|---|---|---| | **控制台单写者无对应实现** | `simulator/dbg_log.h` 把 `DBG_*` 映射到 `printf`;**不编译** `console_log.c` | 单写者环形缓冲 + defaultTask 唯一发送 | `console_log` 的环形缓冲溢出/丢弃计数逻辑**模拟器测不到**;共享代码若误调 `console_log.h` 声明,模拟器链接失败(已被此约束捕获) | `/serial-boot-test` + 板端串口观测;`docs/子系统设计/控制台单写者日志设计说明.md` §4.1 | -| **mtime 粒度不同** | 宿主文件 mtime(**秒级**) | FAT 打包时间(`fdate<<16\|ftime`,**分钟级**) | `cfg_file_poll` 的 `(size, mtime)` 变更令牌在两端触发时机不同;分钟级粒度下的"同分钟内两次改动"在真机可能被判为无变化 | 板端 STORAGE 套件;`配置文件设计说明.md` §变更检测 | +| **mtime 粒度不同** | 宿主文件 mtime(**秒级**) | FAT 打包时间(`fdate<<16\|ftime`,**分钟级**) | `cfg_file_poll` 的 `(size, mtime)` 变更令牌在两端触发时机不同;分钟级粒度下的"同分钟内两次改动"在真机可能被判为无变化 | 板端 STORAGE 套件;`配置文件设计.md` §3.3 | | **无应用回调(空操作)** | 模拟器不注册段回调,`cfg_file_poll` 重载后回调为空 | 各段回调真正执行(如网络重配置) | "配置变更 → 触发回调 → 副作用"链路在模拟器上**只验证到写盘**,回调逻辑不覆盖 | `/serial-boot-test`(配置保存 → UI 生效) | | **「清除记录」两端时序不同** | `syslog_request_clear()` 在调用内**同步**删除宿主文件(`sim_syslog.c` 单线程,没有卷锁、没有队列),返回后状态立刻是 `OK`/`FAIL`,`SYSLOG_CLEAR_WAIT` 永不出现 | 非写者上下文只置标志,真正的 `f_unlink` 排在 defaultTask 的 `syslog_drain()` 尾部(卷锁 try:拿不到锁就顺延,见 `App/log/sys_log_mcu.c`) | 「按了按钮 → 下一拍才见结果」这条异步链、卷占用顺延、以及删除与写者重建的交错,在模拟器上**结构上不存在**;UI 页面若把 WAIT 当失败,模拟器上看不出来 | 主机单测 `test/host/sys_log_mcu/`(TC-SYSLOG-110..122:请求侧零盘触点 / 落地只在 drain / 卷占用顺延 / 幂等 / 不误删;TC-SYSLOG-133/133b:清除成功投一条操作记录、清 oper 自己时不投)+ 台架装置维护页目检 | | **两条投递口在宿主端合一** | `syslog_post_quiet()` = `syslog_post()` 的别名(直写宿主文件,没有队列/待发数组),`syslog_post_dropped()` **恒返 0** | 两个口分开且判据不同:quiet 的调用链上不许内联日志行缓冲(protectTask / iec104Task 的静态最坏链硬线),丢弃只累加计数、由 defaultTask 在 `syslog_drain()` 里按差值补留痕 | 五类记录(事故 / 告警 / 启动 / 操作 / 遥控)在模拟器上**永不丢弃**,"队列满丢记录"、"丢了几条"、"投递侧不许打日志"这三件事在模拟器上结构上不可见;UI 若把"投了"当成"落盘了",模拟器看不出来 | 主机单测 `test/host/sys_log_mcu/`(TC-SYSLOG-132 / 134 / 135:队列侧与启动前数组侧各自的计数与零留痕)+ `scripts/stack_report.py` 静态最坏链(真机侧唯一手段) | diff --git a/docs/测试说明/单元测试/配置文件单元测试说明.md b/docs/测试说明/单元测试/配置文件单元测试说明.md index 6f4f6f51..a2f854a6 100644 --- a/docs/测试说明/单元测试/配置文件单元测试说明.md +++ b/docs/测试说明/单元测试/配置文件单元测试说明.md @@ -10,7 +10,7 @@ > [`测试开发标准流程.md`](../流程与规范/测试开发标准流程.md)(测试分层与门禁总览)、 > [`看门狗单元测试说明.md`](看门狗单元测试说明.md)、 > [`系统时钟单元测试说明.md`](系统时钟单元测试说明.md)(同层范本)、 -> [`../../子系统设计/配置文件设计说明.md`](../../子系统设计/配置文件设计说明.md)(模块设计) +> [`../../子系统设计/配置文件设计.md`](../../子系统设计/配置文件设计.md)(模块设计) --- diff --git a/docs/测试说明/板级测试/实机测试实施指南.md b/docs/测试说明/板级测试/实机测试实施指南.md index f895da67..0715766a 100644 --- a/docs/测试说明/板级测试/实机测试实施指南.md +++ b/docs/测试说明/板级测试/实机测试实施指南.md @@ -463,7 +463,7 @@ CH395F / STORAGE / GD5F,未含 RTC;怀疑 RTC 构建下 `testTask` 不被创 | 事项 | 状态 | 实施细节 / 证据所在 | |---|---|---| | **S7** 自检结果判定 | ✅ 已落地并投用 | `docs/测试说明/流程与规范/自动化测试流程.md`;提交 `4a4cd29` | -| **A1** `cfg_file` 堆分配改造 | ✅ 已闭环(含真机持久化) | `子系统设计/配置文件设计说明.md`(实施 v1.11 + 2026-10-03 真机验证记录 + 2026-10-04 独立复跑记录) | +| **A1** `cfg_file` 堆分配改造 | ✅ 已闭环(含真机持久化) | `子系统设计/配置文件设计.md`(2026-10-03 真机验证记录 + 2026-10-04 独立复跑记录) | | **A2** 网络重建失败恢复 | ✅ 已闭环(正常路径零回归;故障分支见「网络负向场景台架验证」) | `网络/Net层阻塞模型架构设计.md` | | **A3** CH395F 字节命令超时 | ✅ 已闭环(正常路径零回归;故障分支待专项窗口) | — | | **A4** `remo_send` 返回值 | ✅ 已闭环(正常路径零回归;推流压测待专项窗口) | — | diff --git a/docs/测试说明/流程与规范/测试开发标准流程.md b/docs/测试说明/流程与规范/测试开发标准流程.md index 3d9fd703..55388262 100644 --- a/docs/测试说明/流程与规范/测试开发标准流程.md +++ b/docs/测试说明/流程与规范/测试开发标准流程.md @@ -328,7 +328,7 @@ static void phaseN_run(void) { | `App/cfg/cfg_file.c` | 1250 行 | ✅ 已覆盖(139 断言,2026-10-02 首批) | 后端抽象(`cfg_file_store.h`) | 内存虚拟 FS 桩;**首测即抓出外科手术保存死码 P0**(见 §5) | | `App/log/sys_log_mcu.c` | — | ⬜ 候选 | 记录编解码(定长 64B) | 编解码纯逻辑;写入端依赖 FatFS + 队列,测编解码即可 | | `App/util/time_sync.c` | — | ⬜ 候选 | 协议帧编解码 | 报文组装/解析纯逻辑;网络收发不测 | -| `Drivers/BSP/NETCFG/net_cfg_legacy.c` | — | ⛔ 已删除(2026-10-10) | — | 旧档 NCFG 迁移整体移除(旧档 `/cfg/netcfg.bin` 不读、不删、不迁移),测试候选随之撤销 | +| `Drivers/BSP/NETCFG/net_cfg_legacy.c` | — | ⛔ 已删除(2026-10-10) | — | 二进制旧档的迁移代码整体移除,测试候选随之撤销 | | `App/protocol/iec104/*`(104 协议栈) | — | ✅ 已覆盖(82 断言,2026-10-12 IEC 104 上行) | 数据源 vtable + tick 注入 + 发送回调注入 | APCI 重组/总召分片/COV/k-w-t1~t3/两段式全离线穷举;`iec104-unit-test` 钩子 | | `Drivers/BSP/GD5F2GQ5UE/nand_ftl.c`(dhara 移植层) | 573 行 | ✅ 已覆盖(149 断言,2026-10-15 起离线钉死 dhara 移植层两条硬约束) | 11 个 SPI 原语(`gd5f2gq5ue.h`)→ **RAM 假芯片** | 移植层是纯地址算术 + 边界判定,换掉芯片即可离线跑;钉死 is_free 整页扫描与六入口越界守卫,并内联旧算法做对照证明断言有牙 | | `App/util/rs485.c` | — | ⏸ 待功能实现 | — | 上游 RS-485 采集链路;当前为空桩 | diff --git a/docs/环境与部署/项目部署环境要求.md b/docs/环境与部署/项目部署环境要求.md index 5d5b231a..3a11420e 100644 --- a/docs/环境与部署/项目部署环境要求.md +++ b/docs/环境与部署/项目部署环境要求.md @@ -550,7 +550,7 @@ $env:DTU_KEIL_UV4 = 'D:\NotExist\UV4.exe' # 仅当前会话,不动用户级 | 参数 | 工厂默认 | 修改方式 | |---|---|---| -| IP | **192.168.31.100**(静态,无 DHCP) | 屏幕 `通讯参数 → 网口设置`(修改密码 `123`)保存后**立即生效**(网络重建约 1~3 s);或预置 `/config.ini`(INI;旧版 `/cfg/netcfg.bin` **不再读取**——存在则忽略、不迁移,配置回落工厂默认,2026-10-10 起) | +| IP | **192.168.31.100**(静态,无 DHCP) | 屏幕 `通讯参数 → 网口设置`(修改密码 `123`)保存后**立即生效**(网络重建约 1~3 s);或预置 `/config.ini`(INI) | | 子网掩码 | 255.255.255.0(/24) | 同上 | | 默认网关 | 192.168.31.1 | 同上 | | MAC | 全零 = 芯片内置 MAC | 同上(全零是"使用芯片内置 MAC"的哨兵,不是"MAC 为 0") | diff --git a/docs/算法/采样数据校正全流程.md b/docs/算法/采样数据校正全流程.md index 90e56119..b4678ee9 100644 --- a/docs/算法/采样数据校正全流程.md +++ b/docs/算法/采样数据校正全流程.md @@ -105,7 +105,7 @@ flowchart TD - **谁能改、改了会怎样**:只有两个入口——104 规约下发或 UI 整定页,都经 `run_par_104_write()` / `run_par_cal_k_set()` 等(`App/algo/run_par.c`)转交 `adc_cal_set()` / `adc_cal_set_ratio()`(`App/algo/adc_cal.c`)。「改草稿」和「点亮小圆点」被绑在同一个动作里,就是防「屏上数值变了、文件永远没改、还不报错」。标定一路要调两个函数:一个管变比(这数是什么单位),一个点亮 `VALID`(这数准不准),少一个都不算完成。 - **什么时候真写进文件**:后台默认任务盯着小圆点,`run_par_store_save()`(`App/algo/run_par.c`)调 `cfg_file_save()` 把三本草稿和运行参数一次写回;写成功才熄灯,写失败灯留着下轮再试。开机读文件、以及「等文件不再变动才读」由 `cfg_file_load()` / `cfg_file_poll()`(`App/cfg/cfg_file.c`)负责。 - **怎么知道存上没有**:`adc_cal_get_stats()` 记着「改了几次」和「存了几次」,两者相减就是还欠着没存的次数;配合 `run_par_dirty_peek()` 看小圆点状态一起判断。 -- **⚠ 台架踩过的坑(已修)**:2026-11-12 标定时出现过「写进去当场读得回、约 8 秒后被打回出厂值」——根因是热重载那条路径**对同一份文件内容按段各解析一轮**,而解析函数会就地破坏缓冲区,第一段解析完后面的段就再也读不到键值行,静默全体回出厂默认,紧接着的保存又把出厂值写回文件、**永久覆盖整定**。修法是把解析改成非破坏性(逐行拷进栈缓冲再切分),并加了「后注册段也必须重载」的机器钉。为什么值得写进本篇:这类缺陷在开机路径上完全看不出来(开机只解析一轮),**只在「运行中改配置」时发作**,而标定正是这么用的。口径与守护 → `docs/子系统设计/配置文件设计说明.md` §4-(3)。 +- **⚠ 台架踩过的坑(已修)**:2026-11-12 标定时出现过「写进去当场读得回、约 8 秒后被打回出厂值」——根因是热重载那条路径**对同一份文件内容按段各解析一轮**,而解析函数会就地破坏缓冲区,第一段解析完后面的段就再也读不到键值行,静默全体回出厂默认,紧接着的保存又把出厂值写回文件、**永久覆盖整定**。修法是把解析改成非破坏性(逐行拷进栈缓冲再切分),并加了「后注册段也必须重载」的机器钉。为什么值得写进本篇:这类缺陷在开机路径上完全看不出来(开机只解析一轮),**只在「运行中改配置」时发作**,而标定正是这么用的。口径与守护 → `docs/子系统设计/配置文件设计.md` §3.3。 --- diff --git a/docs/算法/采集与算法层设计说明.md b/docs/算法/采集与算法层设计说明.md index 491b8a3d..ef70c83d 100644 --- a/docs/算法/采集与算法层设计说明.md +++ b/docs/算法/采集与算法层设计说明.md @@ -469,7 +469,7 @@ UA 无幅值落 IA → 0.0020 Hz。 | 谁落盘、什么时机 | 只有 defaultTask(配置引擎属主)的 `run_par_store_save()`:iec104Task 主循环 `run_par_dirty_peek()` → 置 `SYS_EVT_RUN_PAR_SAVE` → 属主 `cfg_file_save()` 整档外科保存 → 成功清脏,失败留脏下轮重试 | `App/algo/run_par.c` 落盘函数;本模块自身零文件 IO | **`cfg_file_poll()` 不是保存路径**:它的方向是**盘 → RAM 的重载**(外部改档 → 沉降去抖 → 重载,见 -`App/cfg/cfg_file.h` 同族契约与 `docs/子系统设计/配置文件设计说明.md`)。拿它"兜住"未落盘的改值, +`App/cfg/cfg_file.h` 同族契约与 `docs/子系统设计/配置文件设计.md`)。拿它"兜住"未落盘的改值, 等于把 FTP 侧的旧档倒灌回 RAM,比不兜更坏。**任何"改了就等于存了"的意图式陈述一律按本表改写** (`docs/代码规范/pre-commit使用说明.md` §2.5 第二条)。 diff --git a/docs/门禁说明/coding-standard.md b/docs/门禁说明/coding-standard.md index e4eb3dac..b41d1dae 100644 --- a/docs/门禁说明/coding-standard.md +++ b/docs/门禁说明/coding-standard.md @@ -43,7 +43,7 @@ - **CODE-RULE-20 有真事垫背**(2026-10-23 落地):MicroLIB 的 C 堆由 `startup_stm32f407xx.s` 的 `Heap_Size EQU 0x200` = **512 B**,而它的踩坑方式是**模拟器全对、真机必炸**:配置引擎 2026-09-18 那次堆分配纠偏(4 处 KB 级 `malloc` 落在 C 堆上、真机返 NULL,见 - `docs/子系统设计/配置文件设计说明.md` v1.11 记录)与 lftpd 的 `malloc` 隐患(`docs/内存管理/RAM使用分析与优化方案.md` §4.2.1)都是 + `docs/子系统设计/配置文件设计.md` §6 记录)与 lftpd 的 `malloc` 隐患(`docs/内存管理/RAM使用分析与优化方案.md` §4.2.1)都是 **上线后才发现**的那类。宿主照不出来的缺陷,靠评审等于没管。 ## 3. 触发面 / 看不见什么 @@ -114,7 +114,7 @@ Drivers/BSP/X/x.c:42 [CODE-RULE-09] 函数 foo_bar 缺少函数注释(应含 - 豁免方式只有两种:脚本里的显式豁免名单(按名字形状,如 `HAL_` 回调)、或改代码。 - CODE-RULE-20 的豁免是脚本里的 `HEAP_ALLOC_ALLOWLIST`,按**文件路径 + 具体函数名**双条件放行, 现仅一条:`simulator/sim/cfg_file_store_sim.c` → `malloc`/`free`(平台分配后端的宿主侧实现, - 设计依据见 `docs/子系统设计/配置文件设计说明.md` 的 MCU 端/宿主端两段式后端)。 + 设计依据见 `docs/子系统设计/配置文件设计.md` 的 MCU 端/宿主端两段式后端)。 ⚠ 刻意**不做目录级豁免**:下次谁要 `malloc` 就得往这里加一行,评审看得见。 ## 8. 改这个门禁本身 diff --git a/scripts/check_generated_fresh.py b/scripts/check_generated_fresh.py index e76fea24..7016e996 100644 --- a/scripts/check_generated_fresh.py +++ b/scripts/check_generated_fresh.py @@ -101,8 +101,9 @@ RUN_PY = [sys.executable] BASELINE = ( # 2026-10-30 还清下架 1 条:docs/images/项目架构总览.svg <- tools/arch_diagram/gen_arch_svg.py # (分层图数据补录波/SOE/屏板后重跑,脚本与产物同批逐字节一致;见该文件提交说明) - ("docs/images/配置文件分层架构.svg", "tools/arch_diagram/gen_cfg_arch_svg.py", - "图内写 App/cfg_file.c,生成器已改成 App/cfg/cfg_file.c(配置引擎挪目录,产物没重跑)"), + # 2026-11-15 还清下架 1 条:docs/images/配置文件分层架构.svg <- tools/arch_diagram/gen_cfg_arch_svg.py + # (配置设计档重写为现状版后,图内 8 段注册、FTP 直写非原子、视图守卫等数据同步更新并重跑, + # 脚本与产物同批逐字节一致) ("docs/images/RemoDispBus帧格式.svg", "tools/arch_diagram/gen_frame_svg.py", "脚注写 docs/RemoDispBus协议与接收设计说明.md,生成器已改成 docs/UI设计/(挪目录)"), ("docs/images/物理分区布局.svg", "tools/arch_diagram/gen_partition_svg.py", diff --git a/simulator/README.md b/simulator/README.md index a5eece6c..d70aa8c9 100644 --- a/simulator/README.md +++ b/simulator/README.md @@ -18,7 +18,7 @@ simulator/ │ ├── sim_port.c / .h # 显示/输入端口(SDL 窗口 + 键盘 + mono 主题),替代 MCU 端 ui_port.c │ ├── net_socket_sim.c # 网络后端:Winsock 实现 net_* BSD API,替代 MCU 端 net_socket.c(CH395F) │ ├── ui_net_sink_backend_sim.c # RemoDispBus 平台 backend:取帧/注键(配合共享 ui_net_sink.c) -│ └── cfg_file_store_sim.c # 配置文件存储后端:本地 sim_config.ini(旧档 sim_netcfg.bin 已废弃,不读不删) +│ └── cfg_file_store_sim.c # 配置文件存储后端:本地 sim_config.ini ├── lv_conf.h # 模拟器专用 LVGL 配置(与 MCU 端 Lib/lv_conf.h 相互独立) ├── i1_to_png.py # 截图转 PNG:把 --screenshot 的 I1(1bpp) 原始位图转成 PNG(见 §3.5) ├── tests/ # 无头回归脚本 + 本地黄金参考(见 §3.6,流程文档 docs/UI设计/UI_自动化测试流程.md) @@ -47,7 +47,7 @@ simulator/ | `sim/sim_port.c` | 创建 SDL 窗口显示设备(`lv_sdl_window_create`)、初始化 **mono 主题**(`lv_theme_mono_init`,1bpp 必需)、创建键盘输入设备并挂默认输入组。**不创建鼠标**(纯键盘导航,与 MCU 端 keypad 一致)。`sim_port_get_screen_mono()` 经 `SDL_RenderReadPixels` 读回屏幕像素转 1bpp,供推流取帧 | | `sim/net_socket_sim.c` | 网络后端:Winsock 实现 `net_*` BSD Socket API(`socket`/`bind`/`listen`/`accept`/`recv`/`send`/`close`),替代 MCU 端 `Drivers/BSP/NET/net_socket.c`(CH395F) | | `sim/ui_net_sink_backend_sim.c` | RemoDispBus 平台 backend:`ui_net_sink_get_lcdmem`→`sim_port_get_screen_mono`、`ui_net_sink_inject_key`→`lv_group_send_data`;配合**共享** `Drivers/BSP/UI/ui_net_sink.c`(协议服务端,双端同一份,TCP 7003) | -| `sim/cfg_file_store_sim.c` | 通用配置文件存储后端:本地文件 `sim_config.ini`(`fopen`/`rename` 原子写),替代 MCU 端 `App/cfg/cfg_file_store_mcu.c`(FatFS);旧档 `sim_netcfg.bin` **已废弃**——不读、不删、不迁移(2026-10-10) | +| `sim/cfg_file_store_sim.c` | 通用配置文件存储后端:本地文件 `sim_config.ini`(`fopen`/`rename` 原子写),对应 MCU 端 `App/cfg/cfg_file_store_mcu.c`(FatFS) | | `lv_conf.h` | 模拟器专用 LVGL 配置。与 MCU 端 `Lib/lv_conf.h` 的关键差异见 §3.3 | | `dbg_log.h` | MCU 版 `dbg_log.h` 依赖 `sys_clock.h`(RTC 时间戳),PC 无法编译。此 stub 把 `DBG_INFO/ERROR/DEBUG` 映射到 `printf`;因 `simulator/` 在 include 路径最前,**优先生效**覆盖 MCU 版 | | `build_sim.bat` / `build_sim.ps1` | 一键构建入口:vswhere 探测本机 VS 大版本(16=2019 / 17=2022 / 18=2026,取已装最高)→ 映射 CMake 生成器;`build/` 缓存生成器与本机不符(换机/升级 VS)时自动删除重建(带重试,抗文件锁)。**构建入口一律走它,文档/命令中勿写死生成器版本**;`.bat` 仅负责绕过 PowerShell 执行策略并转交 `.ps1` | @@ -125,7 +125,7 @@ LVGL 场景(同一份 ui_menu.c) 2. **CMake + MSVC**:`build_sim.ps1` 按本机 VS 版本自动选生成器(16=2019 / 17=2022 / 18=2026;不同机器版本不同,**勿写死**),`-A x64`;MSVC 下加 `/utf-8` 消除中文注释的 C4819 警告 3. **共享 UI 源码**:`ui_menu.c` / `ui_font_cn_12.c` 直接编译 `Drivers/BSP/UI/` 原件,改一处两端同时生效 4. **纯键盘导航**:不创建鼠标/触摸设备,与 MCU 端 keypad 操作语义一致(↑↓←→ / Enter / Esc) -5. **UI 为主**:不含 CH395F 网络 / NAND 存储 / ADC 等硬件业务;通用配置文件引擎(`cfg_file`)与 `net_cfg` 核心为纯软件,配置加载/保存可在模拟器离线验证(`sim_config.ini`;旧档 `sim_netcfg.bin` 已废弃、不参与) +5. **UI 为主**:不含 CH395F 网络 / NAND 存储 / ADC 等硬件业务;通用配置文件引擎(`cfg_file`)与 `net_cfg` 核心为纯软件,配置加载/保存可在模拟器离线验证(`sim_config.ini`) --- diff --git a/simulator/main.c b/simulator/main.c index 9592c226..dacf5392 100644 --- a/simulator/main.c +++ b/simulator/main.c @@ -37,7 +37,7 @@ * * 配置轮询自检模式(无窗口、进程内闭环、退出码 0=PASS / 非0=FAIL): * lvgl_sim.exe --cfgtest - * 验证链:net_cfg_load(只读 INI;旧 NCFG 档迁移已于 2026-10-10 删除) + * 验证链:net_cfg_load(只读 INI;旧档迁移已于 2026-10-10 删除) * → poll 建基线 → 伪造外部编辑 * (原子写新文档,等价 FTP 上传/文本编辑器)→ poll 两次(4s 沉降) * → 断言 RAM 副本已按新文件重载 @@ -1407,7 +1407,7 @@ static int run_cfgtest_scenarios(void) { setvbuf(stdout, NULL, _IONBF, 0); - (void)net_cfg_load(); /* 加载 INI(含 [network] 段注册;旧 NCFG 档不再读取) */ + (void)net_cfg_load(); /* 加载 INI(含 [network] 段注册;旧档不再读取) */ cfg_file_poll(); /* 第 1 次 poll:建立变更基线 */ diff --git a/simulator/sim/cfg_file_store_sim.c b/simulator/sim/cfg_file_store_sim.c index 1d145044..e1c96316 100644 --- a/simulator/sim/cfg_file_store_sim.c +++ b/simulator/sim/cfg_file_store_sim.c @@ -144,7 +144,7 @@ const char *cfg_store_tmp_path(void) { /* * 旧版遗留档 getter(cfg_store_legacy_path)已于 2026-10-10 随旧档迁移 - * net_cfg_legacy 一并删除:旧格式 sim_netcfg.bin 不再读取 + * net_cfg_legacy 一并删除:旧格式档不再读取 */ /* diff --git a/simulator/tests/check_content_pages.py b/simulator/tests/check_content_pages.py index d95cd031..6016a320 100644 --- a/simulator/tests/check_content_pages.py +++ b/simulator/tests/check_content_pages.py @@ -1139,9 +1139,9 @@ def main(): # 回归一律在空临时目录里跑(原因见 run_sim 的说明)。调用目录里残留的配置档 # 不再影响像素,只提示一句,便于手工跑 lvgl_sim 的人排查。 - # (旧 sim_netcfg.bin 时代的"删除残留"做法已作废:旧档迁移 2026-10-10 删除, + # (旧二进制档时代的"删除残留"做法已作废:旧档迁移 2026-10-10 删除, # 且真正会污染结果的是 --cfgtest 留下的 sim_config.ini,删别人的档不合适) - for leftover in ("sim_config.ini", "sim_netcfg.bin"): + for leftover in ("sim_config.ini", "sim_netcfg.bin"): # 后者为旧档遗留名,仍需清理 if (Path.cwd() / leftover).exists(): print("[提示] 当前目录存在 %s(本脚本不读它;手工运行 lvgl_sim 时留意," "--cfgtest 会伪造 4000 B 配置档)" % leftover) diff --git a/test/host/cfg_file/cfg_file_test.c b/test/host/cfg_file/cfg_file_test.c index fdea8915..da9f9a09 100644 --- a/test/host/cfg_file/cfg_file_test.c +++ b/test/host/cfg_file/cfg_file_test.c @@ -449,7 +449,7 @@ static void tc_capacity(void) cfg_section_reg_t r; /* 数字钉:段上限必须是 8(cfg_file.h:43);改数字 = 容量台账变化,必须 - * 同步 docs/子系统设计/配置文件设计说明.md 与本用例生产占位说明。 + * 同步 docs/子系统设计/配置文件设计.md 与本用例生产占位说明。 * ⚠ 生产已占满 8/8(见本函数头注释)——这个数字一旦被改大, * 同时要复核 [adc_cal_sec] 之后的段不会被静默拒 */ CHECK(CFG_FILE_MAX_SECTIONS == 8u, "CFG_FILE_MAX_SECTIONS pinned to 8"); diff --git a/test/host/cfg_file_store_mcu/run_unit_test.py b/test/host/cfg_file_store_mcu/run_unit_test.py index 804d2cf3..673d33ac 100644 --- a/test/host/cfg_file_store_mcu/run_unit_test.py +++ b/test/host/cfg_file_store_mcu/run_unit_test.py @@ -34,7 +34,7 @@ r"""配置存储后端(cfg_file_store_mcu)的主机侧纯逻辑单元测试 退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。 -设计依据:docs/子系统设计/配置文件设计说明.md §6.2(mtime 令牌)、 +设计依据:docs/子系统设计/配置文件设计.md §6(mtime 令牌)、 docs/架构设计/STM32F4-Base存储架构说明.md §10.1 第 10/11 条、 docs/测试说明/单元测试/主机单测覆盖率口径.md §9 P2 第 2 项 创建日期:2026-10-24 diff --git a/test/host/iec104/iec104_test.c b/test/host/iec104/iec104_test.c index 69ad673a..6ca97d48 100644 --- a/test/host/iec104/iec104_test.c +++ b/test/host/iec104/iec104_test.c @@ -1492,7 +1492,7 @@ static void tc_comm(void) CHECK(cfg_file_load() == 0, "TC-IEC104-031 load without file falls back to defaults"); CHECK(rp_s_vfs_exists == 1, "TC-IEC104-031 load generates factory template"); /* 容量钉:本套单 comm 段模板必须远小于 4096(生产 6 段全量估算 ~1.3 KB, - * 见 docs/子系统设计/配置文件设计说明.md;上限被顶 = 文件判损坏重建) */ + * 见 docs/子系统设计/配置文件设计.md;上限被顶 = 文件判损坏重建) */ CHECK(rp_s_vfs_len <= 1024u, "TC-IEC104-031 single-comm template well under 4096 cap"); CHECK(strstr(comm_vfs_head(1023u), "[comm]") != NULL, "TC-IEC104-031 template carries [comm]"); CHECK(strstr(comm_vfs_head(1023u), "ca=1") != NULL, "TC-IEC104-031 template writes ca default"); diff --git a/tools/arch_diagram/README.md b/tools/arch_diagram/README.md index ffbbf306..0693a6bc 100644 --- a/tools/arch_diagram/README.md +++ b/tools/arch_diagram/README.md @@ -19,7 +19,7 @@ | `项目架构总览.svg` | `gen_arch_svg.py` | 六层分层结构与跨层依赖——「谁在哪一层」(不含数据流边、不含片外硬件) | `LAYERS` / `ROWS` / `FOOT` | `docs/架构设计/软件架构与运行时规格.md`:14、`docs/架构设计/项目架构总览.md`:11 | | `架构总览_端到端链路.svg` | `gen_arch_endtoend_svg.py` | 端到端链路鸟瞰——一个电气量从引脚走到 104 报文 / 显存 / 记录文件**穿过谁、与谁在什么时候换锁**;42 框 37 边 5 条纵带,线型分数据 / 命令 / 屏障心跳 | `NODES`(id / 列 / 行 / 标题 / 副题)+ `EDGES`(出入框方位与 `f` / `g` / `my` / `gx` 旋钮)+ `BANDS`;三条独占通道 `CHAN_B` / `CHAN_78` / `CHAN_SLOTS` 与走廊 `GUT_C4C5` 在几何区 | `docs/架构设计/项目架构总览.md`:53 | | `就绪屏障.svg` | `gen_barrier_svg.py` | Event Flags 屏障模型:位 → 生产者 → 消费者三栏对齐,哪些位真在用、哪些是预留 | `BITS` / `PRODUCERS` / `CONSUMERS` / `FOOT` | `docs/架构设计/软件架构与运行时规格.md`:63、`docs/架构设计/项目启动顺序管理.md`:48、`docs/架构设计/项目架构总览.md`:85 | -| `配置文件分层架构.svg` | `gen_cfg_arch_svg.py` | 配置引擎分层与持久化:谁解析 `[section]`、落盘走哪条链 | `ROWS` 分层清单 + 标题 / 脚注文案 | `docs/产品需求/产品需求设计文档.md`:331、`docs/子系统设计/配置文件设计说明.md`:144 | +| `配置文件分层架构.svg` | `gen_cfg_arch_svg.py` | 配置引擎分层与持久化:谁解析 `[section]`、落盘走哪条链 | `ROWS` 分层清单 + 标题 / 脚注文案 | `docs/产品需求/产品需求设计文档.md`:331、`docs/子系统设计/配置文件设计.md`:144 | | **采集与算法** | | | | | | `ADC前端电路.svg` | `gen_adc_frontend_svg.py` | 端子 → 串阻 → 转换节点 → 转换器引脚;含差分负端与 STM32 侧对照 | `SUB1` / `NOTE_*` 等文案常量 + 面板坐标(电路拓扑固定,网表变了才重画) | `docs/测试说明/专项验证/ADC前端注入验证流程.md`:31、`docs/硬件平台/ADC通道映射说明.md`:161 | | `ADC码值刻度.svg` | `gen_adc_code_scale_svg.py` | 「码」的直观定义:两片转换器的刻度尺对照(16 位双极性 vs 12 位单极性) | `PANELS`(码范围 / LSB / 实测码标记位置)+ `FOOT`(换芯片才重画) | `docs/测试说明/专项验证/ADC前端注入验证流程.md`:104 | diff --git a/tools/arch_diagram/gen_cfg_arch_svg.py b/tools/arch_diagram/gen_cfg_arch_svg.py index 95c865ca..9a0ceb52 100644 --- a/tools/arch_diagram/gen_cfg_arch_svg.py +++ b/tools/arch_diagram/gen_cfg_arch_svg.py @@ -3,7 +3,7 @@ """生成 docs/images/配置文件分层架构.svg —— 通用配置文件(/config.ini)分层架构图(纯标准库,无需第三方依赖)。 由 gen_net_stack_svg.py 复制改数据区:配置体系六层结构 -(docs/子系统设计/配置文件设计说明.md §4.1): +(docs/子系统设计/配置文件设计.md §3.5): PC 端(编辑器/FTP 客户端)→ lftpd FTP 服务 → /config.ini 文件 → cfg_file 引擎 → 段适配(键表)→ 消费者。 用法(仓库根目录,任意 python3): @@ -49,32 +49,33 @@ L_PC = dict( L_FTP = dict( title="lftpd FTP 服务(端口 21)", sub="ftpTask · 单连接 · root = /(= 卷根)", color="#7c3aed", bg="#f5f3ff", chips=["RETR / STOR / LIST / DELE / CWD(lftpd 现状能力)", - "STOR = 原地覆盖(无 tmp+rename)", - "路径规范化封死 .. 穿越"], + "STOR 原地覆写(无 tmp+rename):FTP 侧写入不受引擎保护", + "写权限受 [ftp] write 门控;路径规范化封死 .. 穿越"], ) L_FILE = dict( title="/config.ini(通用配置文件)", sub="FatFS 卷根(NAND GD5F2GQ5UE · dhara FTL)", color="#0d9488", bg="#f0fdfa", chips=["INI 文本:[段] / key=value / ; 注释", - "CRLF · UTF-8(容忍 BOM)· ≤ 4KB", - "设备写:tmp + rename(原子);变更识别:size + mtime"], + "设备写:/config.TMP → unlink 旧文件 → rename(引擎侧原子)", + "变更识别:size + FAT 打包 mtime(2 s 粒度)"], ) L_ENGINE = dict( title="cfg_file(配置文件引擎)", sub="App/cfg/cfg_file.c · 双端共享", color="#2563eb", bg="#eff6ff", - chips=["INI 解析 / 序列化(逐键校验 · 容错 · 自愈)", + chips=["非破坏性解析 · 逐键校验 · 越界/坏值保持出厂默认", "外科手术式保存(保留注释 / 未知段 → 降级安全)", - "2s 变更轮询(f_stat)+ 段变更回调", - "出厂模板生成 · 旧 NCFG blob 迁移"], + "2 s 轮询 + 4 s 沉降;视图一致性守卫拒绝坏视图", + "出厂模板生成(段注册后按 RAM 副本渲染)"], ) L_SECT = dict( title="段适配(键表)", sub="段 RAM 副本 = 模块静态结构体(稳态 0 增量)", color="#16a34a", bg="#f0fdf4", - chips=["net_cfg [network]:ip / mask / gateway / mac", - "未来:[adc] · [rs485] · [ftp] ...(三步接入)"], + chips=["8 段已满(= CFG_FILE_MAX_SECTIONS):network / sntp / ftp / comm", + "adc_cal / adc_cal_ofs / adc_cal_sec / run_par", + "新增段 = 调一次 cfg_file_register()(三步接入)"], ) L_CONS = dict( title="消费者", sub="读 / 写段 RAM 副本 + 应用", color="#e11d48", bg="#fff1f2", - chips=["netTask:组网 / 重建(代次号自愈)", + chips=["netTask:组网 / 重建([network] 唯一挂回调的段)", "UI 网口设置页:显示 / 编辑 / 保存(零改动)", - "未来模块:cfg_file_section_data() 取段数据"], + "默认任务:cfg_file_poll(); 模块按需 cfg_file_save()"], ) # 每一行可放多个"同级并列"的层(上层只依赖下方各层) @@ -87,7 +88,7 @@ ROWS = [ [L_CONS], ] -FOOT = "稳态 RAM 零增量(段数据 = 模块既有结构体);暂态堆缓冲 ≤9KB(仅 load/save 时);FF_FS_REENTRANT=1 卷锁串行化多任务 FS 访问;模拟器端 = 宿主文件 sim_config.ini(同核心)" +FOOT = "稳态 RAM 零增量(段数据 = 模块既有结构体);暂存缓冲 9,217 B(4KB 读入 + 5KB 输出,仅 load/save 时);FF_FS_REENTRANT=1 卷锁串行化多任务 FS 访问;模拟器端 = 宿主文件 sim_config.ini(同核心)" # ----------------------------- 工具函数 -----------------------------