docs(cfg): 配置设计档重写为现状版,删除旧二进制档叙述

原「配置文件设计说明.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 均通过。
This commit is contained in:
2026-11-15 20:55:04 +08:00
parent 26293f5054
commit 007f6a0e2f
41 changed files with 501 additions and 719 deletions
+1 -1
View File
@@ -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) | 控制台日志单写者模型(避免多任务交错输出) |
+1 -1
View File
@@ -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(密码页错误提示首版触发,已修并全库排查) |
+54 -52
View File
@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="952" viewBox="0 0 1200 952" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif">
<rect x="0" y="0" width="1200" height="952" fill="#ffffff"/>
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1048" viewBox="0 0 1200 1048" font-family="Microsoft YaHei, Segoe UI, PingFang SC, sans-serif">
<rect x="0" y="0" width="1200" height="1048" fill="#ffffff"/>
<text x="26" y="40" font-size="20" font-weight="700" fill="#0f172a">通用配置文件分层架构(/config.ini)</text>
<text x="26" y="62" font-size="13" fill="#64748b">INI 文本文件 · FTP 上传/下载 · 段键表引擎 · MCU/模拟器双端共享</text>
<rect x="26" y="78" width="1148" height="92" rx="12" fill="#f1f5f9" stroke="#475569" stroke-opacity="0.45"/>
@@ -9,60 +9,62 @@
<text x="225" y="139" font-size="13" fill="#1e293b" text-anchor="middle">文本编辑器:改 /config.ini(注释 / 加段 / 改值)</text>
<rect x="416" y="116" width="338" height="38" rx="8" fill="#ffffff" stroke="#475569" stroke-opacity="0.55"/>
<text x="586" y="139" font-size="13" fill="#1e293b" text-anchor="middle">FTP 客户端:RETR 下载 / STOR 上传(端口 21)</text>
<rect x="26" y="214" width="1148" height="92" rx="12" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.45"/>
<rect x="26" y="214" width="1148" height="140" rx="12" fill="#f5f3ff" stroke="#7c3aed" stroke-opacity="0.45"/>
<text x="44" y="240" font-size="17" font-weight="700" fill="#7c3aed">lftpd FTP 服务(端口 21)</text>
<text x="286" y="240" font-size="12" fill="#64748b">ftpTask · 单连接 · root = /(= 卷根)</text>
<rect x="44" y="252" width="388" height="38" rx="8" fill="#ffffff" stroke="#7c3aed" stroke-opacity="0.55"/>
<text x="238" y="275" font-size="13" fill="#1e293b" text-anchor="middle">RETR / STOR / LIST / DELE / CWD(lftpd 现状能力)</text>
<rect x="442" y="252" width="258" height="38" rx="8" fill="#ffffff" stroke="#7c3aed" stroke-opacity="0.55"/>
<text x="572" y="275" font-size="13" fill="#1e293b" text-anchor="middle">STOR = 原地覆盖(无 tmp+rename)</text>
<rect x="711" y="252" width="179" height="38" rx="8" fill="#ffffff" stroke="#7c3aed" stroke-opacity="0.55"/>
<text x="801" y="275" font-size="13" fill="#1e293b" text-anchor="middle">路径规范化封死 .. 穿越</text>
<rect x="26" y="350" width="1148" height="92" rx="12" fill="#f0fdfa" stroke="#0d9488" stroke-opacity="0.45"/>
<text x="44" y="376" font-size="17" font-weight="700" fill="#0d9488">/config.ini(通用配置文件)</text>
<text x="300" y="376" font-size="12" fill="#64748b">FatFS 卷根(NAND GD5F2GQ5UE · dhara FTL)</text>
<rect x="44" y="388" width="283" height="38" rx="8" fill="#ffffff" stroke="#0d9488" stroke-opacity="0.55"/>
<text x="185" y="411" font-size="13" fill="#1e293b" text-anchor="middle">INI 文本:[段] / key=value / ; 注释</text>
<rect x="337" y="388" width="257" height="38" rx="8" fill="#ffffff" stroke="#0d9488" stroke-opacity="0.55"/>
<text x="466" y="411" font-size="13" fill="#1e293b" text-anchor="middle">CRLF · UTF-8(容忍 BOM)· ≤ 4KB</text>
<rect x="604" y="388" width="394" height="38" rx="8" fill="#ffffff" stroke="#0d9488" stroke-opacity="0.55"/>
<text x="802" y="411" font-size="13" fill="#1e293b" text-anchor="middle">设备写:tmp + rename(原子);变更识别:size + mtime</text>
<rect x="26" y="486" width="1148" height="140" rx="12" fill="#eff6ff" stroke="#2563eb" stroke-opacity="0.45"/>
<text x="44" y="512" font-size="17" font-weight="700" fill="#2563eb">cfg_file(配置文件引擎)</text>
<text x="271" y="512" font-size="12" fill="#64748b">App/cfg_file.c · 双端共享</text>
<rect x="44" y="524" width="325" height="38" rx="8" fill="#ffffff" stroke="#2563eb" stroke-opacity="0.55"/>
<text x="206" y="547" font-size="13" fill="#1e293b" text-anchor="middle">INI 解析 / 序列化(逐键校验 · 容错 · 自愈)</text>
<rect x="379" y="524" width="337" height="38" rx="8" fill="#ffffff" stroke="#2563eb" stroke-opacity="0.55"/>
<text x="547" y="547" font-size="13" fill="#1e293b" text-anchor="middle">外科手术式保存(保留注释 / 未知段 → 降级安全)</text>
<rect x="726" y="524" width="257" height="38" rx="8" fill="#ffffff" stroke="#2563eb" stroke-opacity="0.55"/>
<text x="855" y="547" font-size="13" fill="#1e293b" text-anchor="middle">2s 变更轮询(f_stat)+ 段变更回调</text>
<rect x="44" y="572" width="254" height="38" rx="8" fill="#ffffff" stroke="#2563eb" stroke-opacity="0.55"/>
<text x="171" y="595" font-size="13" fill="#1e293b" text-anchor="middle">出厂模板生成 · 旧 NCFG blob 迁移</text>
<rect x="26" y="670" width="1148" height="92" rx="12" fill="#f0fdf4" stroke="#16a34a" stroke-opacity="0.45"/>
<text x="44" y="696" font-size="17" font-weight="700" fill="#16a34a">段适配(键表)</text>
<text x="175" y="696" font-size="12" fill="#64748b">段 RAM 副本 = 模块静态结构体(稳态 0 增量)</text>
<rect x="44" y="708" width="361" height="38" rx="8" fill="#ffffff" stroke="#16a34a" stroke-opacity="0.55"/>
<text x="224" y="731" font-size="13" fill="#1e293b" text-anchor="middle">net_cfg [network]:ip / mask / gateway / mac</text>
<rect x="415" y="708" width="352" height="38" rx="8" fill="#ffffff" stroke="#16a34a" stroke-opacity="0.55"/>
<text x="591" y="731" font-size="13" fill="#1e293b" text-anchor="middle">未来:[adc] · [rs485] · [ftp] ...(三步接入)</text>
<rect x="26" y="806" width="1148" height="92" rx="12" fill="#fff1f2" stroke="#e11d48" stroke-opacity="0.45"/>
<text x="44" y="832" font-size="17" font-weight="700" fill="#e11d48">消费者</text>
<text x="107" y="832" font-size="12" fill="#64748b">读 / 写段 RAM 副本 + 应用</text>
<rect x="44" y="844" width="263" height="38" rx="8" fill="#ffffff" stroke="#e11d48" stroke-opacity="0.55"/>
<text x="175" y="867" font-size="13" fill="#1e293b" text-anchor="middle">netTask:组网 / 重建(代次号自愈)</text>
<rect x="317" y="844" width="320" height="38" rx="8" fill="#ffffff" stroke="#e11d48" stroke-opacity="0.55"/>
<text x="477" y="867" font-size="13" fill="#1e293b" text-anchor="middle">UI 网口设置页:显示 / 编辑 / 保存(零改动)</text>
<rect x="648" y="844" width="329" height="38" rx="8" fill="#ffffff" stroke="#e11d48" stroke-opacity="0.55"/>
<text x="813" y="867" font-size="13" fill="#1e293b" text-anchor="middle">未来模块:cfg_file_section_data() 取段数据</text>
<rect x="442" y="252" width="403" height="38" rx="8" fill="#ffffff" stroke="#7c3aed" stroke-opacity="0.55"/>
<text x="644" y="275" font-size="13" fill="#1e293b" text-anchor="middle">STOR 原地覆写(无 tmp+rename):FTP 侧写入不受引擎保护</text>
<rect x="44" y="300" width="368" height="38" rx="8" fill="#ffffff" stroke="#7c3aed" stroke-opacity="0.55"/>
<text x="228" y="323" font-size="13" fill="#1e293b" text-anchor="middle">写权限受 [ftp] write 门控;路径规范化封死 .. 穿越</text>
<rect x="26" y="398" width="1148" height="92" rx="12" fill="#f0fdfa" stroke="#0d9488" stroke-opacity="0.45"/>
<text x="44" y="424" font-size="17" font-weight="700" fill="#0d9488">/config.ini(通用配置文件)</text>
<text x="300" y="424" font-size="12" fill="#64748b">FatFS 卷根(NAND GD5F2GQ5UE · dhara FTL)</text>
<rect x="44" y="436" width="283" height="38" rx="8" fill="#ffffff" stroke="#0d9488" stroke-opacity="0.55"/>
<text x="185" y="459" font-size="13" fill="#1e293b" text-anchor="middle">INI 文本:[段] / key=value / ; 注释</text>
<rect x="337" y="436" width="440" height="38" rx="8" fill="#ffffff" stroke="#0d9488" stroke-opacity="0.55"/>
<text x="557" y="459" font-size="13" fill="#1e293b" text-anchor="middle">设备写:/config.TMP → unlink 旧文件 → rename(引擎侧原子)</text>
<rect x="787" y="436" width="333" height="38" rx="8" fill="#ffffff" stroke="#0d9488" stroke-opacity="0.55"/>
<text x="954" y="459" font-size="13" fill="#1e293b" text-anchor="middle">变更识别:size + FAT 打包 mtime(2 s 粒度)</text>
<rect x="26" y="534" width="1148" height="140" rx="12" fill="#eff6ff" stroke="#2563eb" stroke-opacity="0.45"/>
<text x="44" y="560" font-size="17" font-weight="700" fill="#2563eb">cfg_file(配置文件引擎)</text>
<text x="271" y="560" font-size="12" fill="#64748b">App/cfg/cfg_file.c · 双端共享</text>
<rect x="44" y="572" width="344" height="38" rx="8" fill="#ffffff" stroke="#2563eb" stroke-opacity="0.55"/>
<text x="216" y="595" font-size="13" fill="#1e293b" text-anchor="middle">非破坏性解析 · 逐键校验 · 越界/坏值保持出厂默认</text>
<rect x="398" y="572" width="337" height="38" rx="8" fill="#ffffff" stroke="#2563eb" stroke-opacity="0.55"/>
<text x="567" y="595" font-size="13" fill="#1e293b" text-anchor="middle">外科手术式保存(保留注释 / 未知段 → 降级安全)</text>
<rect x="746" y="572" width="335" height="38" rx="8" fill="#ffffff" stroke="#2563eb" stroke-opacity="0.55"/>
<text x="913" y="595" font-size="13" fill="#1e293b" text-anchor="middle">2 s 轮询 + 4 s 沉降;视图一致性守卫拒绝坏视图</text>
<rect x="44" y="620" width="290" height="38" rx="8" fill="#ffffff" stroke="#2563eb" stroke-opacity="0.55"/>
<text x="189" y="643" font-size="13" fill="#1e293b" text-anchor="middle">出厂模板生成(段注册后按 RAM 副本渲染)</text>
<rect x="26" y="718" width="1148" height="140" rx="12" fill="#f0fdf4" stroke="#16a34a" stroke-opacity="0.45"/>
<text x="44" y="744" font-size="17" font-weight="700" fill="#16a34a">段适配(键表)</text>
<text x="175" y="744" font-size="12" fill="#64748b">段 RAM 副本 = 模块静态结构体(稳态 0 增量)</text>
<rect x="44" y="756" width="502" height="38" rx="8" fill="#ffffff" stroke="#16a34a" stroke-opacity="0.55"/>
<text x="295" y="779" font-size="13" fill="#1e293b" text-anchor="middle">8 段已满(= CFG_FILE_MAX_SECTIONS):network / sntp / ftp / comm</text>
<rect x="556" y="756" width="371" height="38" rx="8" fill="#ffffff" stroke="#16a34a" stroke-opacity="0.55"/>
<text x="741" y="779" font-size="13" fill="#1e293b" text-anchor="middle">adc_cal / adc_cal_ofs / adc_cal_sec / run_par</text>
<rect x="44" y="804" width="361" height="38" rx="8" fill="#ffffff" stroke="#16a34a" stroke-opacity="0.55"/>
<text x="224" y="827" font-size="13" fill="#1e293b" text-anchor="middle">新增段 = 调一次 cfg_file_register()(三步接入)</text>
<rect x="26" y="902" width="1148" height="92" rx="12" fill="#fff1f2" stroke="#e11d48" stroke-opacity="0.45"/>
<text x="44" y="928" font-size="17" font-weight="700" fill="#e11d48">消费者</text>
<text x="107" y="928" font-size="12" fill="#64748b">读 / 写段 RAM 副本 + 应用</text>
<rect x="44" y="940" width="364" height="38" rx="8" fill="#ffffff" stroke="#e11d48" stroke-opacity="0.55"/>
<text x="226" y="963" font-size="13" fill="#1e293b" text-anchor="middle">netTask:组网 / 重建([network] 唯一挂回调的段)</text>
<rect x="418" y="940" width="320" height="38" rx="8" fill="#ffffff" stroke="#e11d48" stroke-opacity="0.55"/>
<text x="579" y="963" font-size="13" fill="#1e293b" text-anchor="middle">UI 网口设置页:显示 / 编辑 / 保存(零改动)</text>
<rect x="749" y="940" width="397" height="38" rx="8" fill="#ffffff" stroke="#e11d48" stroke-opacity="0.55"/>
<text x="948" y="963" font-size="13" fill="#1e293b" text-anchor="middle">默认任务:cfg_file_poll(); 模块按需 cfg_file_save()</text>
<line x1="600.0" y1="176.0" x2="600.0" y2="199.0" stroke="#94a3b8" stroke-width="2.5"/>
<path d="M 600.0 206.0 l -6 -9 h 12 z" fill="#94a3b8"/>
<line x1="600.0" y1="312.0" x2="600.0" y2="335.0" stroke="#94a3b8" stroke-width="2.5"/>
<path d="M 600.0 342.0 l -6 -9 h 12 z" fill="#94a3b8"/>
<line x1="600.0" y1="448.0" x2="600.0" y2="471.0" stroke="#94a3b8" stroke-width="2.5"/>
<path d="M 600.0 478.0 l -6 -9 h 12 z" fill="#94a3b8"/>
<line x1="600.0" y1="632.0" x2="600.0" y2="655.0" stroke="#94a3b8" stroke-width="2.5"/>
<path d="M 600.0 662.0 l -6 -9 h 12 z" fill="#94a3b8"/>
<line x1="600.0" y1="768.0" x2="600.0" y2="791.0" stroke="#94a3b8" stroke-width="2.5"/>
<path d="M 600.0 798.0 l -6 -9 h 12 z" fill="#94a3b8"/>
<text x="26" y="934" font-size="12" fill="#94a3b8">稳态 RAM 零增量(段数据 = 模块既有结构体);暂态堆缓冲 ≤9KB(仅 load/save 时);FF_FS_REENTRANT=1 卷锁串行化多任务 FS 访问;模拟器端 = 宿主文件 sim_config.ini(同核心)</text>
<line x1="600.0" y1="360.0" x2="600.0" y2="383.0" stroke="#94a3b8" stroke-width="2.5"/>
<path d="M 600.0 390.0 l -6 -9 h 12 z" fill="#94a3b8"/>
<line x1="600.0" y1="496.0" x2="600.0" y2="519.0" stroke="#94a3b8" stroke-width="2.5"/>
<path d="M 600.0 526.0 l -6 -9 h 12 z" fill="#94a3b8"/>
<line x1="600.0" y1="680.0" x2="600.0" y2="703.0" stroke="#94a3b8" stroke-width="2.5"/>
<path d="M 600.0 710.0 l -6 -9 h 12 z" fill="#94a3b8"/>
<line x1="600.0" y1="864.0" x2="600.0" y2="887.0" stroke="#94a3b8" stroke-width="2.5"/>
<path d="M 600.0 894.0 l -6 -9 h 12 z" fill="#94a3b8"/>
<text x="26" y="1030" font-size="12" fill="#94a3b8">稳态 RAM 零增量(段数据 = 模块既有结构体);暂存缓冲 9,217 B(4KB 读入 + 5KB 输出,仅 load/save 时);FF_FS_REENTRANT=1 卷锁串行化多任务 FS 访问;模拟器端 = 宿主文件 sim_config.ini(同核心)</text>
</svg>

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

@@ -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) |
@@ -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 处)、
+8 -21
View File
@@ -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 通道;数据本体 =
@@ -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` |
+346
View File
@@ -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 被判损坏并整份重建(注释与未知段随之丢失)。
- **无格式版本号行**:靠宽容解析(未知段/键保留)支撑向前兼容。
@@ -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`) | 本期不加(宽容解析已覆盖演进) |
+2 -2
View File
@@ -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 新增文档检查清单
@@ -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);
```
+2 -2
View File
@@ -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` |
@@ -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` 静态最坏链(真机侧唯一手段) |
@@ -10,7 +10,7 @@
> [`测试开发标准流程.md`](../流程与规范/测试开发标准流程.md)(测试分层与门禁总览)、
> [`看门狗单元测试说明.md`](看门狗单元测试说明.md)、
> [`系统时钟单元测试说明.md`](系统时钟单元测试说明.md)(同层范本)、
> [`../../子系统设计/配置文件设计说明.md`](../../子系统设计/配置文件设计说明.md)(模块设计)
> [`../../子系统设计/配置文件设计.md`](../../子系统设计/配置文件设计.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` 返回值 | ✅ 已闭环(正常路径零回归;推流压测待专项窗口) | — |
@@ -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 采集链路;当前为空桩 |
@@ -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") |
+1 -1
View File
@@ -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。
---
@@ -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 第二条)。
+2 -2
View File
@@ -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. 改这个门禁本身