7efa58d9a1
分两段(用户裁定 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 绝对路径)
592 lines
47 KiB
Markdown
592 lines
47 KiB
Markdown
# 配置文件设计说明
|
||
|
||
> **版本**: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 分层
|
||
|
||

|
||
|
||
### 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 改归 ④) | `/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 端到端)
|