Files
STM32F4-Base/docs/子系统设计/配置文件设计说明.md
T
wandering1 94b0d79026 feat(ui)+chore(docs): UI 补齐清单清空;数据可信销项与「卡」号清退
本笔含两组收尾工作(同一工作区内交错在 6 个共享文档/门禁配置里,
拆开会产生"文档声称的页数与树里黄金数不一致"的中间提交,故合为一笔):

一、UI 补齐(清单清空)
- 新建 6 个内容页模块:ui_page_accident / oper / alarm / start / yk
  复用 ui_w_records 共享组件(分页、空态"无记录"、按实例读盘缓存),
  记录层与五个写入点已由道F task-10 / 道G task-12 落地,本批只补页面;
  ui_page_runreport 为 13 行 2 页只读统计,四个数逐个挂范围标签
  (自电池周期 / 本次开机),boot_ready=0 显 "--" 不显 0。
- 数据面:ui_data_info_t 尾部追加 9 字段,ui_data_feed 用
  boot_stats_get() + iec104_serv_get_stats() 纯搬运;模拟器填确定值假数据
  (差异登记 docs/测试说明/模拟器差距清单.md §2.3 两行)。
- 接线:菜单树 6 叶绑 #page=,绑定叶 22→28、UI_PAGE_MAP_COUNT=28;
  字库 185→218 字 / 280→313 字形;双端注册 uvprojx +30、CMakeLists +6。
- 回归:内容页黄金 53→60(含 runreport_p2 真拍第 2 页),文本断言
  425 正向 / 36 反向;存量 53 张逐字节零漂移;记录 5 页各用独立空目录
  (模拟器把操作留痕写进 cwd 的 sim_oper.log,共用 tmp 会让空态拍不到)。

二、数据可信销项 + 「数据可信」任务卡号整族清退
- 删除已闭环的"已知电压注入验证"行与闭环结论块(证据在
  docs/硬件平台/ADC通道映射说明.md §8.1 与
  docs/测试说明/ADC前端注入验证流程.md §5~§7),§5 标识列改语义短语。
- 卡 0/0b/1~12 共 14 个号全仓 288 站点 / 56 文件清退成语义短语,
  代码注释、测试、门禁配置与文档一并改;换算表入 docs/编号规范.md §9.3。
  这一族是中文前缀,不进 check_ids 语料,刻意不入 id_aliases.json。

验证(实测)
- @build 0 错 0 警:Code=236456 RO=28124 RW=1876 ZI=137084;
  RAM +464 B(RW +304 / ZI +160),RW_IRAM1 62.2% / RW_CCM 87.9%,
  体积同码凭证与 RAM 台账已回填。
- check_menu_rows 8 状态 rc=0(池 5368/5448 常量未动);
  check_content_pages 60 页 rc=0;check_font_charset / registration /
  coding-standard / bom-drift / mojibake / doc-links --all / layer-deps /
  nested-comment 全 rc=0;check_ids --strict 无错误;主机单测
  ui_data 295 / boot_stats 21 / sys_log_mcu 141 / soe 32 / soe_store 27。

未做(不是遗漏)
- 六页真机判据(真记录翻页、掉电保持、清除后归零、boot_count 自增、
  拔 RTC 电池落 RESTARTED、IWDG 复位落 rst_cause、主站连一次 conn_up+1)
  已登记 docs/任务规划/登记簿.md §7;模拟器结构上照不到。
- docs/产品需求/产品需求设计文档.md 属另一条在飞改动,本笔不含;
  其中我已同步 6 处现状数字(28 叶 / 60 页 / 218 字),随该笔入库。
2026-10-28 07:23:16 +08:00

592 lines
47 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 配置文件设计说明
> **版本**: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.1.100
; 子网掩码
mask=255.255.255.0
; 默认网关
gateway=192.168.1.1
; MAC 地址 (全零 = 使用芯片内置 MAC)
mac=00:00:00:00:00:00
```
### 3.4 值类型
| 类型 | 语法 | 示例 | 解析失败处理 |
|---|---|---|---|
| `CFG_VAL_IP4` | `a.b.c.d`(兼容前导零) | `192.168.1.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/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/在线修复通道,下次保存或重启收敛);正常路径重载 → 段变化则触发该段回调 → 刷新基线。
**(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-③)**:`/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 改归 ④) | `@build` 0/0;`@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 `@build` 0/0;模拟器生成 `sim_config.ini` 且编辑回读一致;旧档 NCFG 自动迁移三态正确 —— **已实测通过**(工厂模板 / 编辑回读 / 非法键回退 / 迁移三态 / 双端 0 警告;`cfg_file.c` 含中文注释块 → UTF-8 **带 BOM**) |
| **P4** | 轮询 + D15 协议 + 测试标记 | `net_task.c`(沉降去抖轮询 + 落盘收敛 + 串口标记)、`lftpd.c`(STOR tmp+rename) | `@build`;`@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** | 总回归 + 提交 | `@build` + `@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 起纳入 `@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 轮询通常跨秒;误报 = 多一次重载(无害) |
| 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`) | 本期不加(宽容解析已覆盖演进) |
---
## 变更记录
| 日期 | 变更 |
|---|---|
| 2026-09-13 | v1.0:初版设计(基于现状调研) |
| 2026-09-13 | v1.1:`get_fattime` 由 SD2506 RTC 改为运行时间伪时(零 I2C 流量、无 RTC 依赖);相应回退 SD2506 驱动锁(P2 修正) |
| 2026-09-13 | v1.2:`get_fattime` 移入 `App/sys_clock.c` 改用墙钟(与调试日志时间戳同源,mtime 为真实时间;修正伪时的 24h 回绕盲区与日期失真) |
| 2026-09-13 | v1.3:`get_fattime` 移入独立适配文件 `App/ff_time.c`(时间子系统不再引用存储头文件);`sys_clock` 新增纯时间 API `sys_clock_get_time()` |
| 2026-09-13 | v1.4:`get_fattime` 改为时间提供回调——实现回归 `Lib/FatFs/ffsystem.c`(FatFs 指定的 OS 胶水文件,不引用 App 头),App 启动时注册墙钟时间源;删除 `App/ff_time.c`;保持分层单向(与既有平台后端模式同型) |
| 2026-09-13 | v1.5:`get_fattime` 改为直引用(ffsystem.c 直接引用 `sys_clock` 墙钟);去掉回调注册机制(删除 `ffsystem.h`、`app_main.c` 注册步骤);反向依赖在架构总览 §1.2 登记为工程唯一例外 |
| 2026-09-13 | v1.6:`sys_clock` 由 ③基础服务层改归 **④中间件层**(通用平台时间服务),文件移入 `App/util/`(与 crc/ringbuf 同目录):`ffsystem` → `sys_clock` 成为**层内引用**,工程反向依赖消除;"反向依赖例外" 措辞同步撤销 |
| 2026-09-13 | v1.7:FS 并发定案 **A+**(REENTRANT 互斥 + 三条一致性协议:写 tmp+rename 扩至 FTP STOR / 沉降去抖重载 / 配置文件单写者收敛 netTask);**actor 方案(单所有者队列 + 全代理层)经完整评估否决**(代理 ~400–600 行、RAM +4KB、符号改名/影子头、自死锁纪律——结构优势可由三协议 ~40 行等价中性化);新增 D15,D9 改为"一切写入 tmp+rename",P4 增列 lftpd STOR 改造 |
| 2026-09-14 | v1.8:**P3 完成**(双端 0/0):`cfg_file` 段键表引擎 + `cfg_file_store` 双端后端 + `net_cfg` [network] 段集成(对外 API 不变,D14 兑现)+ `net_cfg_legacy` 旧档迁移;无头实测通过(工厂模板生成 / 编辑回读 / 非法键工厂回退 / 未知段容留 / 迁移三态 / 双端 0 警告);§6 接口修订为实现版(store 带路径参数 + 三路径 getter;变更回调随段注册,撤销独立 `cfg_file_register_cb`);§8 静态增量按实测 706 B 修订 |
| 2026-09-14 | v1.9:**P5 完成**(sim 0 警告):模拟器主循环接入 2s `cfg_file_poll`(与 MCU netTask 同周期);新增无头自检 `lvgl_sim.exe --cfgtest`(加载/迁移 → 建基线 → 伪造外部编辑 → 两次轮询沉降 → 断言 RAM 更新 + 外科保存回环校验注释/值无损);修复其暴露的两处缺陷:① MSVC `rename()` 不覆盖已存在目标(C1117)——模拟器后端先删目标再 rename(目标丢失时引擎自动重建模板兜底);② **外科保存截断 bug**——`s_split_line` 将 CRLF 行的 `\r`/`\n` 双双 NUL 化,旧 `s_restore_newlines` 只复原一个 `\n` 即遇 NUL 终止,保存产物只剩首行(P3 未触发"对已存在文件保存"故未暴露)——改为 Pass 1 用一次性副本(堆峰值维持约 9KB)、删除复原函数 |
| 2026-09-15 | v1.10:**配置引擎属主任务变更** netTask → defaultTask(P1-3 跨任务竞态修复,P1-5 一并落地)——① 上电加载移入 defaultTask 的 STORAGE_READY 屏障之前(屏障语义 = 卷挂载 + 配置就绪);② UI 保存改跨任务交接:`net_cfg_save_request()`("待保存"标志 + `SYS_EVT_CFG_SAVE` 事件唤醒)→ defaultTask `net_cfg_save_run()` 执行写盘、成功触发"立即应用"(`SYS_EVT_NET_RECONFIG` → netTask 重建),保存行随交接状态显示 ENTER/WAIT/OK/ERR(写盘期间的新请求重位置位不丢失);③ 2s poll 收敛 defaultTask——引擎静态状态仅单任务触碰,竞态在结构上消除;④ netTask 只读 RAM 副本、不触碰配置文件引擎;D15-③ 单写者、§5.1~5.4 时序图与并发矩阵同步;双端构建 0/0、UI 五步门禁全绿 |
| 2026-09-18 | v1.11:**P0-1 修复(临时缓冲改走平台堆)**——共享核心 4 处 `malloc`(读缓冲 4,097B / 模板输出 5,120B / 外科保存 Pass1 副本 + 输出 5,120B)此前落在 **MicroLIB C 堆**上,而该堆由 `startup_stm32f407xx.s` 的 `Heap_Size EQU 0x200` 限定为 **512 B**,真机必然分配失败 → 配置加载静默回落工厂默认(模拟器用宿主 CRT 堆,两端不一致,故双端 0 警告 0 错也照不出来)。① `cfg_file_store` 新增 `cfg_store_alloc/cfg_store_free/cfg_store_heap_free` 后端三件套(MCU = `pvPortMalloc/vPortFree`,与 `lftpd_io.c` 同模式;模拟器 = `malloc/free`),共享核心不再出现 C 库分配符号;② 新增编译期堆峰值预算 `CFG_HEAP_BUDGET = 10240B`(typedef 负数组实现,ARMCC 5 C99 无 `_Static_assert`),放大 `CFG_FILE_MAX_LEN`/`CFG_OUT_MAX` 直接编译失败;③ 四条失败路径(load 读失败 / 模板写失败 / 保存分配失败 / 保存写盘失败)增 `syslog_post(SYSLOG_SELFTEST, …)` 上报,不再只留串口行;`load: OK` 行附带 `heap free` 实测值;④ 属主任务注释同步为 defaultTask。map 复核:`malloc.o` 不参与链接、`Removing startup_stm32f407xx.o(HEAP), (512 bytes)`,静态区 126,584 → **126,064 B**(余 4,488 → **5,008 B**);双端 0 错 0 警、`--cfgtest` PASS |
| 2026-09-22 | v1.12:**E-01 超长守卫根治**(错误码传播审计第一档 #3,commit d030cb0)——poll 的"超长=损坏"检查写在截断后读长(`s_load_file` 读长钉死 4096)上**恒假=死码**,超长文件被截断当有效解析 → 界外静默回工厂默认 → 基线粘住 → 用户配置销毁链。① poll 沉降分支判定改用 `cfg_store_stat` 真实 `st.size` 且**先于读取**,超长走"工厂默认 + 重建模板 + `cfg poll oversized` 自检留痕",**刻意不触发段回调**(避免按工厂 IP 重建网络造成静默失联,保留 FTP 修复通道,保存/重启收敛)② `s_surgical` 输出上限 `CFG_OUT_MAX`(5120) 收紧至 `CFG_FILE_MAX_LEN`(4096),超限走溢出中止路径(原文件不动 + `cfg save overflow` 自检)——消除"合法写出 4097~5120B 文件 = 下次开机销毁"的雷 ③ `s_write_doc` 唯一落盘闸口硬拒 >4096B(结构性保证)④ load 超长分支补 `cfg load oversized` 自检;`cfg_file.h` v1.2 契约同步;模拟器 cfgtest 新增两回归锁(4160B 伪造 → poll 判损坏重建;4000B 注释文件 → save 拒写且原文件未动)。**台架验证暴露并修复**:`cfg_store_write_atomic` MCU 后端 `f_rename` 对已存在目标返回 `FR_EXIST` 不覆盖 → 设备端一切"目标存在的覆盖写"(UI 保存/模板重建)静默失败、仅首启建文件成功——v1.9 已为模拟器后端修过同款(MSVC `rename()` C1117),MCU 侧漏了对称修复;修复 = rename 前先 `unlink` 目标(commit 70f1cc8)。双端 0 错 0 警、cfgtest PASS、台架 FTP 5047B 用例 poll/load 双路径自愈 PASS(重建 523B 工厂模板 + selftest 留痕 + 不掉线)、`@test` 默认闭环 PASS、ST-LINK 全片回读一致 |
| 2026-10-03 | v1.13:**真机验证 PASS(A1 闭环)**——v1.11 的 P0-1 堆分配改造在真机上验证通过:`[CFG] load: OK (523B, heap free 8840B)`(**经 FreeRTOS 堆分配**,即 512B C 堆约束的验收点);Forbidden `FAIL`/`[ERR]` 零命中;FTP 改配置 IP→`192.168.1.101` 后**复位即加载生效**(`[NET_CFG] load: OK ip=192.168.1.101` + 芯片 `IP_INF` .101 + 21 端口探测可达),原档恢复后复测回 .100。真机全闭环 `@test` PASS 7/7。**台架步骤与判据见 `测试说明/实机测试实施指南.md` §3 / §10** |
| 2026-10-04 | **P0-1 真机独立复跑 PASS(二次确认,代码零改动)**——`@build` 0/0(增量,无源文件新于镜像)→ `@test` **VERDICT PASS(rc=0)**:`[2026-10-04 00:01:21][CFG] load: OK (523B, heap free 8840B)` + `[NET_CFG] load: OK ip=192.168.1.100`,Forbidden `FAIL`/`ERROR`/`HardFault`/`[ERR]` 零命中,`selftest: no fault record`(先清掉 2026-10-02/03 故障注入窗口遗留的 67 条台账)→ ST-LINK 全片回读 `No difference found`(`0x08000000~0x08040F9B`)。**本次为 `待处理内容.md` §4.1「待验证」唯一未闭环项(P0-1)的收口**,该节现已清空 |
| 2026-10-10 | v1.14:**删除旧档 NCFG 迁移(D10 撤销)**——`Drivers/BSP/NETCFG/net_cfg_legacy.{c,h}` 整体移除、`cfg_store_legacy_path()` 撤销(MCU / 模拟器后端与主机单测 `test/host/cfg_file/cfg_file_test.c` 同步删除),`net_cfg_load` 不再调用 `net_cfg_legacy_migrate()`;旧档 `/cfg/netcfg.bin` 若存在则被**忽略**(不读、不删、不迁移),配置回落工厂默认。理由:该一次性路径把整条配置写链挂进启动期调用图,defaultTask 静态最坏栈链 **+600 B**(该链整体 5312 → 2776 B,另三个杠杆见 `docs/内存管理/RAM使用分析与优化方案.md`),而本仓库不存在携带旧档的设备。连带影响:`App/util` 的 CRC-16/MODBUS 失去唯一调用者(现为零调用者);模拟器 `--cfgtest` 不再覆盖迁移场景;§2 D10 / §3.5 / §4.2 / §5.1 / §6.2 / §6.4 / §7 / §9 / §11.1 同步 |
| 2026-10-26 | v1.15:**新增 `[comm]` 段(道G task-8,「UI 补齐」波次里的非 UI 前置项)**——`App/protocol/iec104/iec104_comm.h/.c` 配对模块(104 公共地址 / 监听端口 2 键,getter 合法带自愈,adc_cal 先例);内核前 `iec104_comm_init()` 注册([adc_cal] / [run_par] 同「先注册后 load」契约);生效 = **改后重启**(不挂回调、不新增事件位,区别于 [network] 免重启重建);段占位 5→6 / 8,键表与主站地址不做理由 → `docs/子系统设计/网络配置说明.md` §3.6;机器钉 = `test/host/cfg_file` TC-CONFIG-056/057(段/键上限)+ `test/host/iec104` TC-IEC104-031~033(模板/重载/带外/ca 端到端)