docs(cfg): 配置设计档补示例文件一章,写作规范补单篇自洽原则

配置文件设计.md 增加第 2 章「一份完整示例文件」:给出首启模板全文
(8 段 46 键,值全部对照工厂默认宏核对),再按段拆成 2.1~2.6 六张
键表(键 / 类型 / 出厂值 / 含义 + 生效时机),读者不必再翻别的文档
才能知道每个段的键是什么意思。

为保持文档自洽顺带收敛:
- 删掉文件格式章里重复的文件头代码块,改为指向第 2 章
- 删掉段表下「默认值见网络配置说明」的跳转,默认值现就在本篇
- 相关文档里把网络配置说明降级为对外使用手册
- 后续章号整体后移一位,交叉引用同步校准

doc-writer 角色补写作原则:原「同一事实只写一次」只覆盖跨篇去重,
照字面执行会逼出一篇到处跳转的文档。改为明确两条的分工——同一篇内
求完整、篇与篇之间求唯一,并给出判断标准(读者会不会被逼着另开一篇
才能读懂)与常见取舍。反模式表同步加一行。

验证:48 个提交期钩子全通过;check_doc_links / check_mermaid 通过。
This commit is contained in:
2026-11-15 21:10:21 +08:00
parent 007f6a0e2f
commit de6d5d863a
2 changed files with 189 additions and 48 deletions
+10 -6
View File
@@ -66,11 +66,13 @@ Diátaxis 是当前技术写作领域最具影响力的信息架构方法论,
- **金字塔结构**:结论/摘要在前,细节在后
- **镜像代码结构**:参考文档的章节层级应与源码目录/模块结构一致
### 3.2 单一真相源与引用锚点稳定性
### 3.2 单篇自洽、跨篇去重(两条原则的适用边界)
- **同一事实只写一次**:其余位置通过链接、包含或代码生成引用
- **锚点选择优先级**:函数名/宏名 > 文件路径 > 行号。行号随重构频繁失效
- **引用完整性门禁**:自动化脚本校验所有内部链接指向的目标是否存在
- **同一事实只写一次**:这是**跨篇**的规则——两个文件不要各写一遍同一个事实,其余位置用链接引用。
- **单篇必须自洽**:这是**同一篇内**的规则——读者打开一篇文档,就应该能读懂这篇讲的事,不被逼着再翻三四篇。凡是理解本篇主线所必需的内容(关键数据结构长什么样、字段含义、出厂值、机制全貌),**完整写在这一篇里**,不要用"详见另一篇"打发。
- 两条并不冲突,分工是:**同一篇内求完整,篇与篇之间求唯一**。判断标准是"读者会不会被逼着另开文档才能读懂这一段"——会,就必须挪进来或补上。
- 常见取舍:把散落在别处的键表/默认值/示例整体搬进主文档,原处改成一句指向;次要细节(历史过程、外部硬件手册、使用流程)仍留给链接。
- **锚点选择优先级**:函数名/宏名 > 文件路径 > 行号。行号随重构频繁失效,引用完整性由 `doc-links` 自动化门禁兜住。
### 3.3 生成器驱动文档
@@ -100,6 +102,7 @@ Diátaxis 是当前技术写作领域最具影响力的信息架构方法论,
| 内部黑话/缩写未解释 | 新成员或跨团队读者无法理解 | 术语表 + 首次出现全称注释 |
| 行号锚点重构后失效 | `file.c#L42` 指向的代码已移位 | 改用函数名/宏名锚点 |
| 多份文档记录同一事实 | 两处数值不一致,不知哪个为准 | 单一真相源 + 引用/生成 |
| 一篇讲不完、全靠跳转 | 读懂一段要另开三四篇,读者被逼着做知识拼图 | 单篇自洽:主线所需的键表/默认值/示例写进本篇,链接只留给次要细节 |
| 混合文档类型 | 教程里塞入大段 API 参考 | Diátaxis 四象限分类,一篇一责 |
| 事后补文档 | 记忆衰减导致遗漏关键设计决策 | 文档先行 + 问题即时记录 |
@@ -139,8 +142,9 @@ Diátaxis 是当前技术写作领域最具影响力的信息架构方法论,
1. **每节第一句先答"我该干什么、为什么"**:实测值与出处只作从句证据。
2. **不许有未解释的内部速记/缩写/裸小节号**:用户点名「黑话难懂」,每个术语首次出现必须解释。
3. **不要写死源码行号**:定位代码改用「文件路径 + 函数名」当锚点,重构后不会漂移。
4. **同一事实只在一处详述**:其他地方用链接引用,反对制造第二真相源。
5. **待办「完成即销项」**:修复/关闭后直接从表中删除该行,不保留"已消化"行。新条目用**语义短语**标识,不再发字母+数字编号。
4. **同一事实只在一处详述(跨篇)**:两个文件不要各写一遍同一个事实,其他地方用链接引用,反对制造第二真相源。
5. **单篇必须自洽(篇内)**:读者打开一篇就要能读懂它讲的事——理解主线所必需的结构、字段、默认值、示例都写在这篇里,不要用"详见另一篇"打发。篇内求完整、篇间求唯一,两条合用。
6. **待办「完成即销项」**:修复/关闭后直接从表中删除该行,不保留"已消化"行。新条目用**语义短语**标识,不再发字母+数字编号。
### 格式要求
1. **编码 UTF-8 无 BOM**:中文文档一律 UTF-8,不带 BOM。
+179 -42
View File
@@ -2,7 +2,7 @@
> **读者**:要改配置引擎、接入新配置段、或排查"配置没生效"的人。
> **读这一篇能得到什么**:当前 `/config.ini` 这套配置体系的**现状设计**——文件长什么样、引擎怎么解析与落盘、谁在什么时候写、异常时退到哪儿。
> **相关文档**:`网络配置说明.md`(各段键表与使用手册)· `项目架构总览.md`(分层位置)· `docs/内存管理/RAM预算台账.md`(内存数字台账)
> **相关文档**:`docs/硬件平台/ADC通道映射说明.md`(采集通道语义)· `项目架构总览.md`(分层位置)· `docs/内存管理/RAM预算台账.md`(内存数字台账)
本文只描述**现在**的系统。演进过程不在此展开,需要时查 `git log` 与代码文件头注释。
@@ -19,9 +19,156 @@
引擎本身**不认识任何具体配置项**:配置项由各模块以"段"为单位注册(段名 + 段 RAM 副本 + 键表 + 可选变更回调),引擎只按注册表解析与回写。新增配置项 = 新模块注册一个段,不动引擎。
## 2. 文件格式
## 2. 一份完整示例文件
### 2.1 位置与容量
设备开箱首启时,引擎按已注册段的工厂默认值生成的文件如下(8 段、46 个键,每段后面的表逐键说明含义):
```ini
; ============================================================
; STM32F4-Base 通用配置文件
; 格式: INI (UTF-8 / CRLF),本文件为配置唯一来源
; - 以 ';' 或 '#' 开头的行为注释,保存时保留
; - 未知段/键保存时保留;缺失段/键保存时写回
; - 编辑后经 FTP 上传或设备端保存,设备 4s 沉降后自动重载
; ============================================================
[network]
ip=192.168.31.100
mask=255.255.255.0
gw=192.168.31.1
mac=00:00:00:00:00:00
[ftp]
user=admin
pwd_hash=d82494f05d6917ba02f7aaa29689ccb444bb73f20380876cb05d1f37537b7892
write=1
[sntp]
enable=0
server1=0.0.0.0
server2=0.0.0.0
poll_min=60
[comm]
ca=1
port=2404
[adc_cal]
k_ua=10000000
k_un=10000000
k_ia=10000000
k_ub=52800000
k_uc=52800000
k_ib=52800000
k_ic=52800000
k_in=52800000
valid=0
[adc_cal_ofs]
b_ua=0
b_un=0
b_ia=0
b_ub=0
b_uc=0
b_ib=0
b_ic=0
b_in=0
[adc_cal_sec]
r_ua=36381980
r_un=36381980
r_ia=1940570
r_ub=36381980
r_uc=36381980
r_ib=1940570
r_ic=1940570
r_in=1940570
[run_par]
db_ua=500
db_ub=500
db_uc=500
db_un=500
db_ia=5
db_ib=5
db_ic=5
db_in=5
db_freq=500
```
读这份文件只要记住三件事:`[段名]` 分隔配置领域、`键=值` 是明文(没有十六进制、没有二进制、没有 CRC)、**段的先后与键的先后就是注册顺序与键表顺序**(引擎写文件时按这个顺序排)。
### 2.1 `[network]`:网口
| 键 | 类型 | 出厂值 | 含义 |
|---|---|---|---|
| `ip` | IP | `192.168.31.100` | 本机静态 IP(无 DHCP) |
| `mask` | IP | `255.255.255.0` | 子网掩码 |
| `gw` | IP | `192.168.31.1` | 网关 |
| `mac` | MAC | `00:00:00:00:00:00` | 全 0 = 用 CH395F 芯片内置 MAC;填非 0 则覆盖 |
改这一段会**立即重建网络**(该段挂了变更回调),约 1~3 s 生效。键表与默认值在 `Drivers/BSP/NETCFG/net_cfg.c`。
### 2.2 `[ftp]`:FTP 登录
| 键 | 类型 | 出厂值 | 含义 |
|---|---|---|---|
| `user` | 字符串 | `admin` | 登录用户名 |
| `pwd_hash` | 字符串 | `d82494f0…`(全串见上) | **SHA-256(用户名 拼接 口令)** 的十六进制;出厂值是 `sha256("adminadmin")`,即用户名 `admin` + 口令 `admin`。明文不落盘,改口令要自己算好摘要再填 |
| `write` | 无符号 | `1` | `1` = 允许 STOR / DELE 等写操作,`0` = 只读 |
下次 FTP 登录生效。来源 `Drivers/BSP/NETCFG/ftp_cfg.c`。
### 2.3 `[sntp]`:SNTP 对时
| 键 | 类型 | 出厂值 | 含义 |
|---|---|---|---|
| `enable` | 无符号 | `0` | `1` = 开启对时客户端 |
| `server1` | IP | `0.0.0.0` | 主服务器;全 0 表示未配置 |
| `server2` | IP | `0.0.0.0` | 备用服务器 |
| `poll_min` | 无符号 | `60` | 对时周期(分钟) |
客户端每轮直读 RAM 副本,改完保存即进入下一轮。来源 `Drivers/BSP/NETCFG/sntp_cfg.c`。
### 2.4 `[comm]`:IEC 60870-5-104 通讯参数
| 键 | 类型 | 出厂值 | 含义 |
|---|---|---|---|
| `ca` | 无符号 | `1` | 公共地址 CA(1~65535) |
| `port` | 无符号 | `2404` | 104 服务端口(1024~65535) |
**改后需重启生效**(该链路在任务启动时建立连接参数)。来源 `App/protocol/iec104/iec104_comm.c`。
### 2.5 `[adc_cal]`、`[adc_cal_ofs]`、`[adc_cal_sec]`:采集标定
三段描述同一批 8 个采集通道(UA / UB / UC / UN / IA / IB / IC / IN),分别管**增益 k**、**零点 b**、**变比 r**:
| 段 | 键 | 类型 | 出厂值 | 含义 |
|---|---|---|---|---|
| `[adc_cal]` | `k_ua`…`k_in` | 有符号 | 电压族 `10000000` / 电流族 `52800000` | 增益,Q16 定点(值 / 65536 = 倍率);出厂值 = 按芯片量程算出的理论系数,只区分 TPAFE 与 STM32 两种来源 |
| | `valid` | 无符号 | `0` | 已标定位图,bit0~bit7 对应上表通道顺序;`0` = 全部未标定 |
| `[adc_cal_ofs]` | `b_ua`…`b_in` | 有符号 | `0` | 零点偏置(同一 Q16 口径) |
| `[adc_cal_sec]` | `r_ua`…`r_in` | 有符号 | 电压族 `36381980` / 电流族 `1940570` | 一次侧变比,Q16;`0` 表示本路不折算 |
- 键顺序(`k_ua, k_un, k_ia, k_ub, k_uc, k_ib, k_ic, k_in`)**不是**通道顺序,看 `valid` 的位定义时按上表含义,别按下标猜。
- 标定系数由标定流程写入并置对应 `valid` 位;手工改这三个段会让采集口径与标定记录脱节,只建议在厂商指导下做。
- 来源 `App/algo/adc_cal.c`;通道语义与硬件对应 → `docs/硬件平台/ADC通道映射说明.md`。
### 2.6 `[run_par]`:运行参数死区
| 键 | 类型 | 出厂值 | 含义 |
|---|---|---|---|
| `db_ua`…`db_un` | 有符号 | `500` | 四路电压死区,毫伏 |
| `db_ia`…`db_in` | 有符号 | `5` | 四路电流死区,毫安 |
| `db_freq` | 有符号 | `500` | 频率死区,毫赫兹 |
用于 104 上送的越死区判断(COV 上送门槛)。值域由模块守卫(电压 ≤ 50000、电流 ≤ 1000、频率 ≤ 5000 毫单位,下限 0),越界写被拒。来源 `App/algo/run_par.c`。
> 这张示例文件是**首启模板**,8 段 46 键全部在场。真实设备上标定落盘后 `[adc_cal]` 三段会变成实测值,其余段则视现场是否改过而定。
## 3. 文件格式
### 3.1 位置与容量
| 端 | 路径 | 说明 |
|---|---|---|
@@ -34,14 +181,14 @@
| 常量 | 值 | 含义 |
|---|---|---|
| `CFG_FILE_MAX_LEN` | 4096 B | 文件字节上限;超过即判损坏 |
| `CFG_FILE_MAX_SECTIONS` | 8 | 可注册段数上限(**当前已用满**,见 §4.1) |
| `CFG_FILE_MAX_SECTIONS` | 8 | 可注册段数上限(**当前已用满**,见 §5.1) |
| `CFG_FILE_MAX_NAME_LEN` | 15 | 段名/键名最长字符数 |
| `CFG_FILE_MAX_KEYS` | 16 | 单段键数上限 |
| `CFG_FILE_SETTLE_CNT` | 2 | 沉降去抖轮数(2 轮 × 2 s 轮询 = 4 s) |
文件名 `config.ini` 满足 8.3 短名约束(`FF_USE_LFN=0`);段名与键名是**文件内容**,不受 8.3 限制,但受上表 15 字符限制。
### 2.2 语法
### 3.2 语法
| 元素 | 规则 |
|---|---|
@@ -54,25 +201,13 @@
| 行尾 | 设备写出 CRLF;解析同时接受 LF 与 CRLF |
| 编码 | UTF-8(内容实际为 ASCII);解析为字节定向,不依赖代码页 |
### 2.3 出厂模板
### 3.3 出厂模板
文件缺失(首启、或 FTP `DELE` 后重建)时,引擎按当前已注册段的 RAM 副本渲染一份新文件:固定 7 行文件头注释 + 每段 `[段名]` 与 `key=value` 行。
文件缺失(首启、或 FTP `DELE` 后重建)时,引擎按当前已注册段的 RAM 副本渲染一份新文件:固定 7 行文件头注释 + 每段 `[段名]` 与 `key=value` 行。完整样子见第 2 章。
文件头原文(`s_doc_header`,cfg_file.c):
模板**不含逐段逐键的中文注释**——段落顺序与键序就是注册顺序与键表顺序;渲染时按 `s_sections[]` 的注册顺序逐段输出,段内按键表顺序逐键输出。
```ini
; ============================================================
; STM32F4-Base 通用配置文件
; 格式: INI (UTF-8 / CRLF),本文件为配置唯一来源
; - 以 ';' 或 '#' 开头的行为注释,保存时保留
; - 未知段/键保存时保留;缺失段/键保存时写回
; - 编辑后经 FTP 上传或设备端保存,设备 4s 沉降后自动重载
; ============================================================
```
模板**不含逐段逐键的中文注释**——段落顺序与键序就是注册顺序与键表顺序。
### 2.4 值类型
### 3.4 值类型
键表里每个键声明一种类型,解码/编码由引擎统一完成(`cfg_val_type_t`):
@@ -86,9 +221,9 @@
**逐键容错**:某个键写错不牵连别的键——非法值只让该键回落到工厂默认并打一条 `DBG_ERROR`,下次保存时该行会被改写成合法当前值(自愈)。
## 3. 运行时机制
## 4. 运行时机制
### 3.1 上电加载
### 4.1 上电加载
```mermaid
sequenceDiagram
@@ -110,7 +245,7 @@ sequenceDiagram
- 加载前各段 RAM 副本先整体复位为工厂默认,再逐行解析覆盖——因此"文件里缺键"天然等价于"取工厂默认"。
- `net_cfg_load()` 在置 `SYS_EVT_STORAGE_READY` **之前**调用(`App/task/default_task.c`),使"存储就绪"这一屏障本身蕴含"配置已就绪"。
### 3.2 保存:外科手术式行替换
### 4.2 保存:外科手术式行替换
`cfg_file_save()` 不改写整份文件,而是**只替换受管键所在的行**,其余内容(注释、空行、未知段、未知键、段落顺序)逐字节保留。
@@ -137,21 +272,21 @@ flowchart TD
2. **引擎永不产出超长文件**:`s_write_doc()` 是唯一落盘闸口(模板与外科保存共用),超过 `CFG_FILE_MAX_LEN` 直接拒写。否则下一次开机 `load` 会把整份文件判损坏并销毁用户注释。
3. **失败即原文件不动**:任何一步失败返回 -1,由调用方保留脏标志重试。
### 3.3 变更检测:2 s 轮询 + 沉降去抖
### 4.3 变更检测:2 s 轮询 + 沉降去抖
`cfg_file_poll()` 由**配置引擎属主任务**周期调用(MCU = defaultTask,2 s;模拟器主循环 2 s),检测"文件被别人改了"(典型是 FTP 上传或编辑器直接改模拟器文件):
1. `f_stat` 取 `(size, mtime)`,与基线比对;
2. 连续 `CFG_FILE_SETTLE_CNT`(2)轮稳定才认为改动结束(4 s);
3. 沉降后先判 `size > 4096`:是则各段复位工厂默认 + 重建模板 + 自检留痕,**不触发段回调**(保留在线修复通道);
4. 否则读全文并过**视图一致性守卫**(§3.4);
4. 否则读全文并过**视图一致性守卫**(§4.4);
5. 通过后逐段"复位默认 + 解析",挂回调的段若内容有变则调用其回调;最后刷新基线。
外部编辑到生效的总延迟约 2~5 s(4 s 沉降 + 网络重建时间)。
> 文件在位但内容被判无效时,poll 会保持 RAM 与基线不动并每 2 s 重试;持续被拒会在串口打 `[CFG] poll ... HELD` 与限速 WARN。
### 3.4 视图一致性守卫
### 4.4 视图一致性守卫
存储层(dhara FTL + FatFS)在某些情形会给回**与 stat 不符的旧视图**:读长不足、读到同尺寸的全 0 旧映射块、或读取过程中文件被改写。若照单应用,后果是全段静默回到工厂默认(且随后落盘把工厂值写回文件,永久覆盖用户整定);若按短视图外科保存,则会吞掉后面的段。
@@ -166,7 +301,7 @@ flowchart TD
- save 侧:拒绝落盘 → 返回 -1,原文件不动,调用方保留脏标志重试。
- **已知边界**:内容合法但整体陈旧的视图(size/mtime/内容三者一致地滞后)引擎层无法检出——那属于卷层课题,重启是最后防线。
### 3.5 写者归属
### 4.5 写者归属
`/config.ini` 的直接写者**收敛为 defaultTask**(配置引擎属主):
@@ -179,9 +314,9 @@ flowchart TD
**唯一例外是 FTP 上传(lftpd 的 `STOR`)**:它在 ftpTask 里直接原地覆写 `/config.ini`,不经引擎,也没有"临时文件 + rename"。它的后果由 poll 的沉降去抖与视图一致性守卫兜住(看到完整的新文件才重载);上传中途断开时 lftpd 会删除截断文件并回 `450`。因此"4 s 沉降 + 一致性守卫"不只是体感优化,它是 FTP 这条非原子写入路径的安全网。
## 4. 段与键表
## 5. 段与键表
### 4.1 已注册段总览
### 5.1 已注册段总览
当前注册 **8 段 = 段上限 8,已满**。新增配置领域必须先扩 `CFG_FILE_MAX_SECTIONS`(并复核 RAM)。
@@ -196,16 +331,18 @@ flowchart TD
| `[adc_cal_sec]` | 8 | `App/algo/adc_cal.c` | 无 | 同上 |
| `[run_par]` | 9 | `App/algo/run_par.c` | 无 | 104 COV 每拍直读 |
各段键名、工厂默认值、合法域与语义 → `docs/子系统设计/网络配置说明.md` §1 与 §3.3~§3.6,以及被引用的算法文档。本表只给"有哪几段、归谁、怎么生效"。
**注意本表的顺序就是注册顺序**(`net_cfg_load()` 先注册 network / ftp / sntp,其余模块初始化时依次跟上),也是引擎写文件时的段落顺序。
### 4.2 注册顺序
各键的类型、出厂值、含义与生效时机 → 本文第 2 章逐段表。对外使用手册(改法、FTP 操作流程、现场排查)→ `docs/子系统设计/网络配置说明.md`。
### 5.2 注册顺序
注册由各模块自己的初始化函数完成,引擎按注册顺序建立段表:
- `net_cfg_load()` 内统一注册 `[network]` / `[ftp]` / `[sntp]`(`s_net_register()` / `ftp_cfg_register()` / `s_sntp_register()`);
- 其余段由各模块在 `cfg_file_load()` 之前的初始化路径里注册(`adc_cal` 三段、`run_par`、`iec104_comm`)。
### 4.3 接入一个新段
### 5.3 接入一个新段
```c
/* 1) 段 RAM 副本 + 工厂默认(两份同布局,默认值建议共用宏避免漂移) */
@@ -233,7 +370,7 @@ static void demo_cfg_init(void)
.data_size = sizeof(demo_cfg_t),
.p_keys = s_demo_keys,
.key_count = 2u,
.p_cb = NULL, /* 需要"改了就提醒"时挂回调,见 §4.4 */
.p_cb = NULL, /* 需要"改了就提醒"时挂回调,见 §5.4 */
};
(void)cfg_file_register(&s_sec);
}
@@ -246,7 +383,7 @@ static void demo_cfg_init(void)
- `p_data` / `p_defaults` / `p_keys` 的生命周期必须覆盖引擎全部使用期(放静态区);
- 新 `.c` 双端注册(uvprojx + `simulator/CMakeLists.txt`),见 `new-source-file` skill。
### 4.4 段变更回调
### 5.4 段变更回调
```c
void cfg_file_register_cb(const char *p_name, cfg_section_cb_t p_cb);
@@ -256,9 +393,9 @@ void cfg_file_register_cb(const char *p_name, cfg_section_cb_t p_cb);
没有回调的段,消费方自己按拍从 RAM 副本读——这是多数段的做法,也更省事。
## 5. 接口
## 6. 接口
### 5.1 引擎 API(`App/cfg/cfg_file.h`)
### 6.1 引擎 API(`App/cfg/cfg_file.h`)
```c
int cfg_file_register(const cfg_section_reg_t *p_reg); /* 注册段:0 成功 / -1 参数非法·段名重复·超上限 */
@@ -269,7 +406,7 @@ void *cfg_file_section_data(const char *p_name); /* 取段 RAM 副本;
void cfg_file_register_cb(const char *p_name, cfg_section_cb_t p_cb);
```
### 5.2 平台后端(`App/cfg/cfg_file_store.h`,每端一份实现)
### 6.2 平台后端(`App/cfg/cfg_file_store.h`,每端一份实现)
| API | 语义 |
|---|---|
@@ -286,7 +423,7 @@ void cfg_file_register_cb(const char *p_name, cfg_section_cb_t p_cb);
**为什么所有 KB 级缓冲都走 `cfg_store_alloc`**:ARMCC 默认 MicroLIB,C 库堆只有 512 B,直接 `malloc` 必失败且症状是"配置静默回落到工厂默认"。引擎自身(`cfg_file.c`)不出现 `malloc`。
## 6. 内存占用
## 7. 内存占用
当前构建实测(`scripts/ram_baseline.tsv` 与 Keil map):
@@ -306,7 +443,7 @@ void cfg_file_register_cb(const char *p_name, cfg_section_cb_t p_cb);
平台总量台账(SRAM1 / CCM / Flash 的当前百分比与余量)→ `docs/内存管理/RAM预算台账.md`。
## 7. 异常与恢复
## 8. 异常与恢复
| 场景 | 行为 |
|---|---|
@@ -323,7 +460,7 @@ void cfg_file_register_cb(const char *p_name, cfg_section_cb_t p_cb);
自检留痕标签(`syslog_post(SYSLOG_SELFTEST, ...)`):`cfg load oversized`、`cfg template write fail`、`cfg heap alloc fail`、`cfg save read fail`、`cfg save view stale`、`cfg poll view stale`、`cfg save line too long`、`cfg save overflow`、`cfg save write fail`。
## 8. 双端差异
## 9. 双端差异
| 项 | MCU | 模拟器 |
|---|---|---|
@@ -337,7 +474,7 @@ void cfg_file_register_cb(const char *p_name, cfg_section_cb_t p_cb);
`get_fattime()` 在 `Lib/FatFs/ffsystem.c` 里直引用 `sys_clock` 墙钟(与调试日志时间戳同源),因此设备写出的文件时间与日志时间一致、FTP `LIST` 也显示真实时间;墙钟无效时回退固定值,此时变更检测退化为仅比 size。
## 9. 已知限制
## 10. 已知限制
- **段落上限已满**:8/8,新增领域必须先扩宏并复核 RAM。
- **FTP 上传非原子**:`STOR` 原地覆写,可靠性依赖沉降去抖与视图一致性守卫;上传中掉电会留下截断文件(下次上电按 4096 B 界限判损坏或按逐行容错解析)。