Files
STM32F4-Base/docs/子系统设计/配置文件设计说明.md
wandering1 7efa58d9a1
CI / 静态门禁 (pre-commit) (push) Failing after 25s
CI / 主机侧单测 (MSVC) (push) Successful in 48s
network: 台架 IP 迁到 192.168.31.100,并把重复写死的值收进单一真源
分两段(用户裁定 2026-11-02):先做地址迁移,再把同类字面量收敛成
单一真源,以后再改地址只动一处。

一、IP 迁移 192.168.1.100 → 192.168.31.100(gw 同步 → .31.1)
- 工厂默认:Drivers/BSP/NETCFG/net_cfg.c 的 NET_CFG_DEF_IP/_GW
- 注释/文档示例、主机与板级测试、台架 py 脚本、两份 sim_config.ini
- 字节序教学点同步换算 hex:0x6401A8C0 → 0x641FA8C0(内存
  C0 A8 1F 64),旧版示例引文与历史 Trap 记录按冻结纪律不动
- UI 黄金基准:网口设置 4 态 + shot_info 共 5 张 .bin 由
  check_content_pages.py --update 重刷(渲染值 IP 变了,属预期变更)
- 保留判定:客户点表引用(IEC104/继电保护规划)、芯片手册摘录、
  日期冻结的变更记录行、已退役台架档案 MDK-ARM/retracted_bench_*

二、单一真源收口(A+B+C+D,用户批准全做)
- 新建 test/host/bench_defaults.py:台架 py 默认值唯一定义点
  (DEVICE_IP/DEVICE_GW/PC_IP/NET_BCAST),22 个脚本改为引用;
  MDK-ARM 两探针与 scripts/board_ui_nav.py 经 sys.path shim 引入
- test/board/net_test_task.h 新增 TEST_PC_PEER_IP,板测三处
  PHASE*_REMOTE_IP 改引该宏(保留 -D 覆盖);Phase1 断言改
  memcmp 数组比较,数组各只出现一次
- simulator/tests/check_content_pages.py 从 net_cfg.c 正则解析
  NET_CFG_DEF_* 推导期望值(解析失败即报错、不静默兜底),
  脚本内台架 IP 字面量清零
- 装饰性注释去具体值(net_cfg.h/net_socket.h/net_socket.c/
  net_socket_sim.c);字节序教学 hex 对刻意保留为唯一写值点
- 刻意不动:主机单测夹具 IP(逐文件钉住的任意值是正确测试
  实践)、文档正文 IP(那是文档内容本身)

顺带:scripts/gen_task_table.py 规范串清掉「,B5」——编号清退提交
72b4b094 改了受管区没重跑生成器,task-table 门禁因此一直红

验证:产品 @build 0/0;CH395F 套件开关构建 0/0 后还原产品态;
6 套主机单测 rc=0;content_pages(含宏推导路径)/菜单 8 态/
推流位比对全 PASS;bom-drift 99 无漂移;doc-links/coding-standard/
cppcheck 绿;py_compile 23 文件 + --help 冒烟(含 shim 绝对路径)
2026-11-02 14:46:42 +08:00

47 KiB
Raw Permalink Blame History

配置文件设计说明

版本: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 出厂模板(首启自动生成)

; ==========================================================
; 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 分层

通用配置文件分层架构

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) 段键表引擎 —— "以后所有配置进这个文件"的落地形式:

/* 键定义:键名 + 类型 + 段结构体内的偏移/长度 */
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) 外科手术式保存 —— 降级安全的核心:

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) 段变更回调 —— 未来模块的接入点:

void cfg_file_register_cb(const char *p_name, cfg_section_cb_t p_cb);
/* 网络模块注册:[network] 段变化 → 置 SYS_EVT_NET_RECONFIG(复用既有重建路径) */

5. 运行时机制

5.1 上电加载

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 保存

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 上传自动生效

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 墙钟,与调试日志时间戳同源
#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(双端共享)

/* 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 后端接口(每端一份实现,互斥编译)

/* 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 未来模块接入模板

/* 示例:未来 [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 轮询通常跨秒;误报 = 多一次重载(无害)
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。台架步骤与判据见 docs/测试说明/板级测试/实机测试实施指南.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 端到端)