现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名, 不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体 run_boot_test.py 的真实调用。 - AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改 「编译(/keil-build)」「串口回归(/serial-boot-test)」 - keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree 方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围) - 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入 等 41 行历史证据保留旧名 - gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑 - 顺手补并行方案同行漏网计数 51→52 钩子 验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52 钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
47 KiB
配置文件设计说明
版本: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 | 旧档迁移 | 原理由:防现场升级丢配置;否决"保留旧文件"(双数据源、陈旧值混乱)。撤销理由:该一次性路径把整条配置写链(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.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 分层
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()的返回值记录 mtimeget_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 端到端) |