W5波+N-19收口:FTP鉴权落地、Trap25三连定案修复(N-30)、N-20/N-23/N-26

N-19 FTP 鉴权:
- lftpd_auth(USER/PASS 状态机 + 连续5次失败锁60s)+ SHA-256
  (user||pwd) 口令哈希;config.ini [ftp] 段 user/pwd_hash/write,
  出厂 admin/admin 在用有启动告警;lftpd_log.c 删除并入 lftpd_auth
- ftp_cfg 配置读写;台架 ftp_auth_bench.py 13/13

N-30 / Trap 25(CH395F_Trap_Records.md 四次定案全记录):
- 修复1 closed-with-data accept:客户端发完正常 FIN 后芯片按
  FUN_PARA(0x09) §5.52② 保留 Socket+RX 缓冲,旧代码把 CLOSED 一律
  relisten 自毁缓冲 → STOR 必挂 425;改为接受"已关但可读"连接排空
- 修复2 丢 CONNECT 中断探针 net_lost_connect_probe + last_peer_port
  幽灵甄别(拆链窗口握手完成但中断丢失,双信源去抖补 accept)
- 修复3 send_list/send_nlst 批量单发:背靠背 ~60B 小段毒化芯片 TX
  (芯片报 SEND_OK 但对端全丢),整份目录一次性写入
- 证伪存档:固定沉降/Socket 轮换冷却/listen 门控(台架 v7 矩阵)
- 台架工具 test/board/ch395f_reuse_bench.{c,h} + host 脚本存档
  (不注册编译;实验分支 n30-chip-bench 保留)

N-20:Error_Handler 死循环降级(记录+复位路径)
N-23:scripts/check_registration.py 双端注册反向校验(pre-commit)
N-26:GD5F NAND 直 DMA 调用方缓冲窗口断言

验证:BUILD_OK 0/0;ftp_regression 连跑 4 轮 11/11(修复前
LIST#4/#7 100% 挂、STOR#2 曾 100% 425);ftp_auth_bench 13/13;
@test 启动标记 PASS;ci_local L0+L0.5 全过;doc-links 通过。
登记簿 N-30 已销项。
This commit is contained in:
2026-10-11 18:50:24 +08:00
parent eddbedeee8
commit d1e6bb4ba7
75 changed files with 3928 additions and 352 deletions
+7 -1
View File
@@ -5,6 +5,7 @@
"fault-handlers-3": "f3afab8d2be18794",
"fault-handler-enable": "be87fcd3a7926138",
"hardfault-report-impl": "f3afab8d2be18794",
"error-handler-degrade": "d02a92afda945c4b",
"stack-overflow-check": "d4735e3a265e16ee",
"malloc-failed-hook": "6b86b273ff34fce1",
"assert-not-deadloop": "d4a00718ab2bd071",
@@ -21,7 +22,12 @@
"ioc-task-stacks": "3262f737d92dcbee",
"adc1-hal-enabled": "1a8ed6cff34f7ba6",
"adc1-calibration-call": "8d5f4b40fb218913",
"adc1-calibration-impl": "d33bc4323e243248"
"adc1-calibration-impl": "d33bc4323e243248",
"adc-tick-tim3-branch": "39c3548c37f6c4ae",
"ioc-adc-trig-t3trgo": "a9939de260ee46f8",
"ioc-adc-dma-continuous": "189126670d85d80a",
"adc1-dma-link": "397fce92dca3b728",
"adc1-dma-irq-priority": "beff08e91333229b"
},
"_note": "CubeMX 存活审计基线;改动用 --write 重建"
}
+1 -1
View File
@@ -40,7 +40,7 @@ RemoDispBus 推流回归单独跑:`.venv\Scripts\python.exe simulator/tests/ch
- **改 `simulator/lv_conf.h` 触发 `lv-conf-sync` 门禁**:它与 MCU 端 `Lib/lv_conf.h` 必须保持同一特性子集(按**生效值**比对,未定义取 `lv_conf_internal.h` 兜底);白名单与四类判定 → `docs/代码规范/pre-commit使用说明.md` §2.15 / §5.5
- **模拟器代码与固件同标准受检**(`cppcheck` / `coding-standard` / 乱码 / 改写型钩子 / `doc-links` 全覆盖 `simulator/`);**改动触及 `sim/` 任何后端,必须同步在 `docs/测试说明/模拟器差距清单.md` 登记一行**——模拟器跑过 ≠ 真机跑过
## 构建/运行报错速查(详解 simulator/README.md §4)
## 构建/运行报错速查(详解 `simulator/README.md`「四、已知坑(构建/运行)」)
| 现象 | 根因 |
|---|---|
+43 -1
View File
@@ -154,9 +154,10 @@ repos:
# 5) 文档一致性门禁(D-7)
# 存在性:md 互链 / 反引号路径是否指向真实文件(防迁移/删除后残留死链)
# 章节:`§N.M` 必须能在目标文档的编号标题里找到(--sections-strict:自引用失效也拦)
- id: doc-links
name: 文档链接存在性 (D-3/D-7)
entry: python scripts/check_doc_links.py
entry: python scripts/check_doc_links.py --sections-strict
language: system
files: \.md$
# 外部资料(芯片手册转录)链接形态不受控;第三方目录整体排除
@@ -354,6 +355,29 @@ repos:
files: ^(App/algo/prot_elm\.(c|h)|App/algo/prot_set\.(c|h)|App/algo/phasor\.(c|h)|test/host/prot_elm/.*|test/host/common/unit_test_harness\.py)$
pass_filenames: false
# 7k) SHA-256 主机侧单元测试(2026-10-11 加,N-19 FTP 鉴权)
# sha256 是 FTP pwd_hash 校验的地基——哈希算错是"正确口令被拒 /
# 错误口令放行"的静默语义错误,板端只见 530/230 看不出根因;
# FIPS 180-4 标准向量(空串/abc/56B 补位跨界/1MB)+ hex_eq 非法输入
# 在主机侧毫秒级钉死。零平台依赖,无桩。
- id: sha256-unit-test
name: SHA-256 单测(主机侧)
entry: python test/host/sha256/run_unit_test.py
language: system
files: ^(App/util/sha256\.(c|h)|test/host/sha256/.*|test/host/common/unit_test_harness\.py)$
pass_filenames: false
# 7l) FTP 鉴权状态机主机侧单元测试(2026-10-11 加,N-19)
# 锁定 60s/解锁/成功清零/tick 32 位回绕是"防口令爆破"契约核心,
# 板端每验一条要真等 60s、回绕要跑满 49.7 天;lftpd_auth 纯函数
# 设计(tick 参数注入)就是为了让这些分支主机侧毫秒级穷举。
- id: ftp-auth-unit-test
name: FTP 鉴权状态机单测(主机侧)
entry: python test/host/ftp_auth/run_unit_test.py
language: system
files: ^(Drivers/BSP/NET/lftpd/lftpd_auth\.(c|h)|App/util/sha256\.(c|h)|test/host/ftp_auth/.*|test/host/common/unit_test_harness\.py)$
pass_filenames: false
# 8) 测试套件开关门禁(2026-10-01 加)
# 背景:test_config.h 的 TEST_SUITE_* 决定固件里编入哪个测试套件,
# 全部注释 = 产品构建。但此前**无任何门禁检查**——实测已提交的 HEAD 里
@@ -367,3 +391,21 @@ repos:
language: system
files: ^test/board/test_config\.h$
pass_filenames: false
# 9) 双端注册反向校验(2026-10-12 加,登记簿 N-23 / 审查报告 N-06 残余)
# 背景:新 .c 必须手动注册两处(`MDK-ARM/*.uvprojx` + `simulator/CMakeLists.txt`)
# 才会被编译;历史上 `App/task/rs485_task.c`、`Drivers/BSP/NET/lftpd/lftpd_log.c`
# **两处都没注册** = 写了却从不参与任何一端编译的**死代码**,误导读者以为功能已生效。
# 本钩子反向核对两份注册表:①存在但未进 uvprojx ②存在但未进 CMakeLists
# (MCU-only 豁免见脚本 SIM_EXEMPT,显式清单+每行理由,不整目录一刀切)
# ③注册了但文件不存在(幽灵注册)。
# 存量缺口(rs485_task/lftpd_log)已进脚本 KNOWN_GAPS 白名单 → WARN 不拦,
# 只对新缺口 FAIL,故钩子可立即上线不拦现有提交(`--strict` 供审计关白名单)。
# ⚠ pre-commit 4.x 已移除 `always: true` → files 必须覆盖所有触发面:
# 任意 .c + uvprojx + CMakeLists.txt + 脚本自身(脚本自身改动也值得复跑)。
- id: registration-check
name: 双端注册反向校验 (uvprojx + CMakeLists)
entry: python scripts/check_registration.py
language: system
files: (\.c$|\.uvprojx$|CMakeLists\.txt$|^scripts/check_registration\.py$)
pass_filenames: false
+6 -5
View File
@@ -87,8 +87,8 @@
## 提交门禁(pre-commit)
- 任一失败即拦截,共 **26 个钩子**(逐个详解 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 10 个(`-unit-test` 后缀)+ `test-suite-switch`
- **范围 = 项目代码**:第三方(`Lib/`、`Middlewares/`、CMSIS、HAL、`lftpd`、`dhara`、`MDK-ARM`)排除;`cppcheck`/`coding-standard` 另排除 `test/`,规范检查再排除 CubeMX 生成的 `Src/`/`Inc/`。**`simulator/` 已纳入全部钩子**(2026-10-11)——生成物只靠 `.gitignore` 挡,**不在 `exclude` 里写目录名**(教训见使用说明 §5.5)
- 任一失败即拦截,共 **29 个钩子**(逐个详解 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 12 个(`-unit-test` 后缀)+ `test-suite-switch` + `registration-check`(双端注册反向校验,N-23)
- **范围 = 项目代码**:第三方(`Lib/`、`Middlewares/`、CMSIS、HAL、`lftpd`、`dhara`、`MDK-ARM`)排除;`cppcheck`/`coding-standard` 另排除 `test/`,规范检查再排除 CubeMX 生成的 `Src/`/`Inc/`。**`simulator/` 已纳入全部钩子**(2026-10-11)——生成物只靠 `.gitignore` 挡,**不在 `exclude` 里写目录名**(教训见 `docs/代码规范/pre-commit使用说明.md` §5.5)
- ⚠ **pre-commit 4.x 已移除 `always: true`**(实测对 `local` 钩子只告警 `Unexpected key(s)` 并跳过)→ 想"总是跑"的钩子**必须让 `files:` 覆盖到所有会触发它的文件**,没有兜底开关
- **改写型钩子(`trailing-whitespace` + `end-of-file-fixer`)豁免 CubeMX/Keil 生成文件**(配置里 YAML 锚点 `&cubemx-generated`);⚠ **只读检查不受影响**:`cppcheck` / 乱码 / `cubemx-survival` / `task-table` / `mem-numbers` 仍覆盖 `Src/`、`Inc/`
- 提交信息:标题 ≤100 字符、**正文每行 ≤80**(`.gitlint` + gitlint 默认)→ 长说明一律用**短标题 + `git commit -F <UTF-8 无 BOM 文件>` 多行正文**(中文长标题极易超限,且 `-m` 会被终端代码页转码破坏)
@@ -140,7 +140,7 @@
- 所有 `net_*` socket 操作经 `netMsgQueue` 在 netTask 串行执行;非 netTask 任务直接调 `net_*` API,勿直接碰 CH395F 驱动
- **`net_*` 公开 API 的错误码经"任务通知值"回传**:`net_msg_t` 是**按值拷入队列**的,netTask 改的是自己那份副本,调用方永远看不到 → 错误码搭 `xTaskNotify()` 通知值回来。**勿给 `net_msg_t` 加"回传用"字段**(调用方只会读到栈垃圾)
- **单连接 socket 的断开语义**:EOF 时 net 层已 `auto_relisten`,应用层**不得**在 EOF 时 `net_close` 该 fd(会连监听一起毁掉 → 端口"连接被拒");仅当数据 fd ≠ 监听 fd(模拟器多连接后端)时才关
- 网口配置:`/config.ini` [network] 段(工厂默认 192.168.1.100/24、gw .1;MAC 全零 = 芯片内置;UI 保存立即生效;**旧档 `/cfg/netcfg.bin` 迁移已于 2026-10-10 删除**,旧档存在则被忽略)→ `docs/子系统设计/网络配置说明.md`
- 网口配置:`/config.ini` [network] 段(工厂默认 192.168.1.100/24、gw .1;MAC 全零 = 芯片内置;UI 保存立即生效;**旧档 `/cfg/netcfg.bin` 迁移已于 2026-10-10 删除**,旧档存在则被忽略);同文件 **[ftp] 段 = FTP 鉴权**(`user` / `pwd_hash` = SHA-256(user‖password) / `write`,出厂默认 admin/admin,默认口令在用有启动告警;连续 5 次失败锁 60 s)→ `docs/子系统设计/网络配置说明.md`
- **新 .c 文件必须手动注册到两处**才能双端编译:`MDK-ARM/STM32F407-Demo.uvprojx` + `simulator/CMakeLists.txt`(用 `new-source-file` skill);⚠ **新文件落在还没有的新目录,还要把该目录加进 `uvprojx` 的 `<IncludePath>`**——否则报错落在**消费者 TU** 上(`cannot open source input file`),极易误判
- 测试构建:`test/board/test_config.h` 的 `TEST_SUITE_CH395F/STORAGE/GD5F/RTC` **互斥**(一次只能一个,`#error`);全部注释 = 产品构建,**受 `test-suite-switch` 钩子门禁**;`ENABLE_DESTRUCTIVE_TESTS` **默认关闭**(关闭时套件只读模式,可随时在在用设备上跑)——⚠ 跑一轮破坏性 GD5F 即让 dhara 多记一个坏块且该计数持久化,普通恢复清不掉 → `docs/测试说明/存储维护与容量恢复.md`;板级套件运行用 **`board-test-suite` skill**
- **改驱动前先读对应 `docs/问题记录/*_Trap_Records.md`**(CH395F / GD5F2GQ5UE / SD2506);各模块测试指南见 `docs/测试说明/`
@@ -150,7 +150,8 @@
- **登记簿类文档「完成即销项」**:`docs/待处理内容.md` 这类待办登记簿,事项修复/关闭后**直接从表中删除该行**(不保留"已消化"行)——结论与证据应已沉淀进对应模块文档/问题记录。销项时必须:① 先核对目标文档确已含完整记录 ② 同步清理其他文档对该编号的引用 ③ 改完跑 `check_doc_links.py --all` + pre-commit
- 图形走 **`draw-diagram` skill**:精确布局图用 `tools/arch_diagram/gen_*.py` 生成器(产物放 `docs/images/`,脚本与 .svg 一并提交);拓扑图用 Mermaid;**勿手绘 ASCII 框图**。文件引用关系图走 `tools/refgraph`(`& 'tools\refgraph\gen_refgraph.bat'`)
- 文档写作规范(编码/BOM/链接/表格/一致性钩子)→ `docs/文档规范.md`;**改本文件前先读** `docs/AGENTS编写指南.md`(入口文件的取舍与维护纪律);全量索引与推荐阅读顺序 → `docs/README.md`
- **改名 / 挪目录一律走 `scripts/rename_ref.py`(先 dry-run,确认后 `--apply`)**:`git mv` + 自动改写全库引用(反引号引用 / Markdown 链接 / 图片 `../` 层级换算 / `#include` / `uvprojx` `<FilePath>` / `CMakeLists.txt`),落盘后自动跑 `check_doc_links --all` 复核——手工扫引用既慢又漏(一次改名历史上波及 10~30 文件)。引用章节用「`路径.md` §N.M」**紧邻写法**(`doc-links` 校验编号是否还在:紧邻/裸名/前缀/别名/自引用**全部拦提交**(钩子带 `--sections-strict`);每次跑打印「可判定比例」,`--section-todo` 出待改清单,昵称歧义就登记 `scripts/section_aliases.json` 或写清楚)→ `docs/文档规范.md` §3.5 / §3.6
- 文档写作规范(编码/BOM/链接/表格/一致性钩子)→ `docs/文档规范.md`;全量索引与推荐阅读顺序 → `docs/README.md`
## 边界(Ask first / Never)
@@ -169,7 +170,7 @@
| ADC 采样节拍 / 通道映射 / V0.1 硬件缺陷 | `docs/子系统设计/ADC采样节拍设计说明.md`、`docs/硬件平台/ADC通道映射说明.md` |
| 存储栈(FatFs / dhara / lftpd / 两条 FatFs 陷阱) | `docs/架构设计/STM32F4-Base存储架构说明.md` |
| RAM / 栈预算 / 容量台账 | `docs/内存管理/RAM使用分析与优化方案.md`、`内存内容分配策略.md` |
| pre-commit 26 个钩子逐个详解 | `docs/代码规范/pre-commit使用说明.md` |
| pre-commit 29 个钩子逐个详解 | `docs/代码规范/pre-commit使用说明.md` |
| 代码规范 V1.0 / cppcheck 口径 | `docs/代码规范/嵌入式C语言代码规范_V1.0.md`、`cppcheck使用说明.md` |
| 自动化测试 / 实机测试 / 板级套件 | `docs/测试说明/自动化测试流程.md`、`实机测试实施指南.md`、`board-test-suite` skill |
| 烧录与复位语义 | `docs/测试说明/ST-LINK_CLI_复位使用说明.md` |
+253
View File
@@ -0,0 +1,253 @@
/*
* 模块名称:sha256
* 模块功能:SHA-256 安全散列(FIPS 180-4)实现——纯标准库、零堆分配
* 适用平台:STM32F407ZGTx / PC 主机侧单测(双端共用)
* 作者:王建锋
* 创建日期:2026-10-11
* 修改记录:
* v1.0 2026-10-11 王建锋 创建初始版本(N-19 FTP 鉴权;标准向量由
* test/host/sha256 主机单测钉死)
*/
/* 头文件包含区 */
#include <string.h>
#include "sha256.h"
/* 私有宏定义区 */
#define SHA256_ROTR32(x, n) (((x) >> (n)) | ((x) << (32u - (n))))
#define SHA256_CH(x, y, z) (((x) & (y)) ^ ((~(x)) & (z)))
#define SHA256_MAJ(x, y, z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
#define SHA256_EP0(x) (SHA256_ROTR32(x, 2u) ^ SHA256_ROTR32(x, 13u) ^ SHA256_ROTR32(x, 22u))
#define SHA256_EP1(x) (SHA256_ROTR32(x, 6u) ^ SHA256_ROTR32(x, 11u) ^ SHA256_ROTR32(x, 25u))
#define SHA256_SIG0(x) (SHA256_ROTR32(x, 7u) ^ SHA256_ROTR32(x, 18u) ^ ((x) >> 3u))
#define SHA256_SIG1(x) (SHA256_ROTR32(x, 17u) ^ SHA256_ROTR32(x, 19u) ^ ((x) >> 10u))
/* 私有变量定义区 */
/* 轮常量(FIPS 180-4 §4.2.2.2:前 64 个素数立方根小数部分前 32 位) */
static const uint32_t s_k[64] = {
0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u,
0x3956c25bu, 0x59f111f1u, 0x923f82a4u, 0xab1c5ed5u,
0xd807aa98u, 0x12835b01u, 0x243185beu, 0x550c7dc3u,
0x72be5d74u, 0x80deb1feu, 0x9bdc06a7u, 0xc19bf174u,
0xe49b69c1u, 0xefbe4786u, 0x0fc19dc6u, 0x240ca1ccu,
0x2de92c6fu, 0x4a7484aau, 0x5cb0a9dcu, 0x76f988dau,
0x983e5152u, 0xa831c66du, 0xb00327c8u, 0xbf597fc7u,
0xc6e00bf3u, 0xd5a79147u, 0x06ca6351u, 0x14292967u,
0x27b70a85u, 0x2e1b2138u, 0x4d2c6dfcu, 0x53380d13u,
0x650a7354u, 0x766a0abbu, 0x81c2c92eu, 0x92722c85u,
0xa2bfe8a1u, 0xa81a664bu, 0xc24b8b70u, 0xc76c51a3u,
0xd192e819u, 0xd6990624u, 0xf40e3585u, 0x106aa070u,
0x19a4c116u, 0x1e376c08u, 0x2748774cu, 0x34b0bcb5u,
0x391c0cb3u, 0x4ed8aa4au, 0x5b9cca4fu, 0x682e6ff3u,
0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u,
0x90befffau, 0xa4506cebu, 0xbef9a3f7u, 0xc67178f2u,
};
/* 函数定义区 */
/*
* 函数功能:处理一个 64 B 分组(消息扩展 + 64 轮压缩)
* 入口参数:p_ctx - 上下文 p_block - 64 B 分组(大端字序)
* 返回值:无
* 限定条件:p_block 恰为 64 B 有效数据
* 函数说明:w[64] 为栈上消息扩展数组(256 B,ftpTask 4 KB 栈可容纳)
*/
static void s_transform(sha256_ctx_t *p_ctx, const uint8_t *p_block) {
uint32_t w[64];
uint32_t a;
uint32_t b;
uint32_t c;
uint32_t d;
uint32_t e;
uint32_t f;
uint32_t g;
uint32_t h;
for (uint32_t i = 0u; i < 16u; i++) {
w[i] = ((uint32_t)p_block[4u * i] << 24) |
((uint32_t)p_block[4u * i + 1u] << 16) |
((uint32_t)p_block[4u * i + 2u] << 8) |
(uint32_t)p_block[4u * i + 3u];
}
for (uint32_t i = 16u; i < 64u; i++) {
w[i] = SHA256_SIG1(w[i - 2u]) + w[i - 7u] +
SHA256_SIG0(w[i - 15u]) + w[i - 16u];
}
a = p_ctx->state[0];
b = p_ctx->state[1];
c = p_ctx->state[2];
d = p_ctx->state[3];
e = p_ctx->state[4];
f = p_ctx->state[5];
g = p_ctx->state[6];
h = p_ctx->state[7];
for (uint32_t i = 0u; i < 64u; i++) {
uint32_t t1 = h + SHA256_EP1(e) + SHA256_CH(e, f, g) + s_k[i] + w[i];
uint32_t t2 = SHA256_EP0(a) + SHA256_MAJ(a, b, c);
h = g;
g = f;
f = e;
e = d + t1;
d = c;
c = b;
b = a;
a = t1 + t2;
}
p_ctx->state[0] += a;
p_ctx->state[1] += b;
p_ctx->state[2] += c;
p_ctx->state[3] += d;
p_ctx->state[4] += e;
p_ctx->state[5] += f;
p_ctx->state[6] += g;
p_ctx->state[7] += h;
}
/*
* 函数功能:初始化上下文(清零 + 置初始链接变量,FIPS 180-4 §5.3.3)
* 入口参数:p_ctx - 上下文
* 返回值:无
* 限定条件:p_ctx 为 NULL 时静默返回(fail-safe)
*/
void sha256_init(sha256_ctx_t *p_ctx) {
if (p_ctx == NULL) {
return;
}
(void)memset(p_ctx, 0, sizeof(*p_ctx));
/* 初始链接变量(FIPS 180-4 §5.3.3:前 8 个素数平方根小数部分前 32 位) */
p_ctx->state[0] = 0x6a09e667u;
p_ctx->state[1] = 0xbb67ae85u;
p_ctx->state[2] = 0x3c6ef372u;
p_ctx->state[3] = 0xa54ff53au;
p_ctx->state[4] = 0x510e527fu;
p_ctx->state[5] = 0x9b05688cu;
p_ctx->state[6] = 0x1f83d9abu;
p_ctx->state[7] = 0x5be0cd19u;
}
/*
* 函数功能:流式喂入数据(任意长度,内部按 64 B 分组压缩)
* 入口参数:p_ctx - 上下文 p_data - 数据 len - 字节数
* 返回值:无
* 限定条件:p_ctx/p_data 为 NULL 时静默返回
*/
void sha256_update(sha256_ctx_t *p_ctx, const void *p_data, size_t len) {
const uint8_t *p = (const uint8_t *)p_data;
if ((p_ctx == NULL) || (p == NULL)) {
return;
}
for (size_t i = 0u; i < len; i++) {
p_ctx->buf[p_ctx->buf_len] = p[i];
p_ctx->buf_len++;
if (p_ctx->buf_len >= SHA256_BLOCK_SIZE) {
s_transform(p_ctx, p_ctx->buf);
p_ctx->bitlen += (uint64_t)SHA256_BLOCK_SIZE * 8u;
p_ctx->buf_len = 0u;
}
}
}
/*
* 函数功能:收尾输出 32 B 摘要(补位 + 长度字段 + 末次压缩)
* 入口参数:p_ctx - 上下文 p_out - 32 B 摘要输出
* 返回值:无
* 限定条件:p_ctx/p_out 为 NULL 时静默返回;收尾后上下文清零(防敏感残留)
*/
void sha256_final(sha256_ctx_t *p_ctx, uint8_t p_out[SHA256_DIGEST_SIZE]) {
uint64_t bitlen;
uint8_t pad = 0x80u;
uint8_t zero = 0x00u;
uint8_t len_be[8];
if ((p_ctx == NULL) || (p_out == NULL)) {
return;
}
/* 总长 = 完整分组累计 + 暂存区(必须在补位前定稿) */
bitlen = p_ctx->bitlen + (uint64_t)p_ctx->buf_len * 8u;
(void)sha256_update(p_ctx, &pad, 1u);
while (p_ctx->buf_len != 56u) {
(void)sha256_update(p_ctx, &zero, 1u);
}
for (uint32_t i = 0u; i < 8u; i++) {
len_be[i] = (uint8_t)((bitlen >> (56u - 8u * i)) & 0xFFu);
}
(void)sha256_update(p_ctx, len_be, 8u);
for (uint32_t i = 0u; i < 8u; i++) {
p_out[4u * i] = (uint8_t)(p_ctx->state[i] >> 24);
p_out[4u * i + 1u] = (uint8_t)(p_ctx->state[i] >> 16);
p_out[4u * i + 2u] = (uint8_t)(p_ctx->state[i] >> 8);
p_out[4u * i + 3u] = (uint8_t)(p_ctx->state[i]);
}
(void)memset(p_ctx, 0, sizeof(*p_ctx));
}
/*
* 函数功能:一次性计算 32 B 摘要(init+update+final 便捷封装)
* 入口参数:p_data - 数据 len - 字节数 p_out - 摘要输出
* 返回值:无
* 限定条件:栈上上下文 ≈108 B;NULL 参数由子函数兜底
*/
void sha256_calc(const void *p_data, size_t len, uint8_t p_out[SHA256_DIGEST_SIZE]) {
sha256_ctx_t ctx;
sha256_init(&ctx);
sha256_update(&ctx, p_data, len);
sha256_final(&ctx, p_out);
}
/*
* 函数功能:hex 字符 → 半字节值
* 入口参数:c - 字符 p_out - 输出 0~15
* 返回值:0 - 合法;-1 - 非法 int
* 限定条件:p_out 非空
*/
static int s_hex_nibble(char c, uint8_t *p_out) {
uint8_t v;
if ((c >= '0') && (c <= '9')) {
v = (uint8_t)(c - '0');
} else if ((c >= 'a') && (c <= 'f')) {
v = (uint8_t)(c - 'a') + 10u;
} else if ((c >= 'A') && (c <= 'F')) {
v = (uint8_t)(c - 'A') + 10u;
} else {
return -1;
}
*p_out = v;
return 0;
}
/*
* 函数功能:摘要与 64 位 hex 串恒定时间比较(大小写不敏感)
* 入口参数:p_digest - 32 B 摘要 p_hex - 64 字符 hex 串
* 返回值:1 - 相等;0 - 不等 / 长度非 64 / 非法 hex / NULL
* 限定条件:比较段不短路(XOR 累加器);hex 非法属配置错误,早退不构成口令侧信道
*/
int sha256_hex_eq(const uint8_t p_digest[SHA256_DIGEST_SIZE], const char *p_hex) {
uint8_t acc = 0u;
uint8_t hi;
uint8_t lo;
if ((p_digest == NULL) || (p_hex == NULL)) {
return 0;
}
if (strlen(p_hex) != (SHA256_DIGEST_SIZE * 2u)) {
return 0;
}
for (uint32_t i = 0u; i < SHA256_DIGEST_SIZE; i++) {
if ((s_hex_nibble(p_hex[2u * i], &hi) != 0) ||
(s_hex_nibble(p_hex[2u * i + 1u], &lo) != 0)) {
/* hex 非法 = 配置错误(可信来源),早退不构成口令侧信道 */
return 0;
}
acc |= (uint8_t)(p_digest[i] ^ (uint8_t)((hi << 4) | lo));
}
return (acc == 0u) ? 1 : 0;
}
+79
View File
@@ -0,0 +1,79 @@
/*
* 模块名称:sha256
* 模块功能:SHA-256 安全散列(FIPS 180-4)——流式 + 一次性接口,外加摘要与
* 64 位 hex 串的恒定时间比较;纯标准库、零堆分配,MCU 与主机侧
* 单测共用(无平台依赖)
* 适用平台:STM32F407ZGTx / PC 主机侧单测(双端共用)
* 作者:王建锋
* 创建日期:2026-10-11
* 修改记录:
* v1.0 2026-10-11 王建锋 创建初始版本(N-19 FTP 鉴权:[ftp] pwd_hash 校验)
*/
#ifndef SHA256_H
#define SHA256_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stddef.h>
#include <stdint.h>
#define SHA256_DIGEST_SIZE 32u /* 摘要字节数 */
#define SHA256_BLOCK_SIZE 64u /* 分组字节数 */
/* 散列上下文(≈108 B,调用方栈上或静态均可) */
typedef struct {
uint32_t state[8]; /* 链接变量 */
uint64_t bitlen; /* 已处理的完整分组累计比特数 */
uint8_t buf[SHA256_BLOCK_SIZE];/* 未满分组的暂存区 */
uint32_t buf_len; /* 暂存区已用字节数 */
} sha256_ctx_t;
/*
* 函数功能:初始化散列上下文
* 入口参数:p_ctx - 上下文(不可为 NULL) sha256_ctx_t *
* 返回值:无
* 限定条件:无
*/
void sha256_init(sha256_ctx_t *p_ctx);
/*
* 函数功能:喂入一段消息(可多次调用,任意切分)
* 入口参数:p_ctx - 上下文 p_data - 数据 len - 字节数
* 返回值:无
* 限定条件:p_ctx 已 init;p_data 为 NULL 或 len=0 时空操作
*/
void sha256_update(sha256_ctx_t *p_ctx, const void *p_data, size_t len);
/*
* 函数功能:补位收尾并输出 32 B 摘要(调用后上下文被清零,不可续用)
* 入口参数:p_ctx - 上下文 p_out - 32 B 输出缓冲(不可为 NULL)
* 返回值:无
* 限定条件:p_ctx 已 init
*/
void sha256_final(sha256_ctx_t *p_ctx, uint8_t p_out[SHA256_DIGEST_SIZE]);
/*
* 函数功能:一次性计算整段消息的 SHA-256
* 入口参数:p_data - 数据 len - 字节数 p_out - 32 B 输出缓冲
* 返回值:无
* 限定条件:p_data/p_out 非空
*/
void sha256_calc(const void *p_data, size_t len, uint8_t p_out[SHA256_DIGEST_SIZE]);
/*
* 函数功能:恒定时间比较 32 B 摘要与 64 字符 hex 串(大小写不敏感)
* 入口参数:p_digest - 32 B 摘要 p_hex - 64 字符 hex 串(可 NULL)
* 返回值:1 - 相等;0 - 不等 / hex 非法 / 参数为空 int
* 函数说明:hex 来自配置文件(可信来源),格式非法早退不构成侧信道;
* 摘要与 hex 内容的比较逐字节累积异或,耗时只随长度(固定 64)变化
*/
int sha256_hex_eq(const uint8_t p_digest[SHA256_DIGEST_SIZE], const char *p_hex);
#ifdef __cplusplus
}
#endif
#endif /* SHA256_H */
+9 -11
View File
@@ -56,14 +56,13 @@ volatile uint8_t g_spi1_dma_done;
* 否则静默失败(CCM 首次扩容实验实证的根因)。页级大块传输(2048)仍走 DMA,
* 源/目的全部为文件作用域静态缓冲。 */
/* DMA 可达内存区间:必须与分散加载文件 RW_IRAM1(0x20000000,128KB,
* SRAM1+SRAM2)一致。CCM(0x10000000,64KB)DMA1/DMA2 物理不可达,
* RW_CCM 区任何缓冲都不得进 DMA。阈值只能挡住"当前已知的小栈缓冲",
* 挡不住"将来新加的 >1024B 栈缓冲";gd5f_dma_buf_ok() 把这条约定
* 升级为运行时强制检查(2026-09-21 加固)。若将来新增其它 DMA 可达
* 内存(如外部 SDRAM),此处区间必须同步扩展。 */
#define GD5F_DMA_RAM_BASE 0x20000000UL
#define GD5F_DMA_RAM_END 0x20020000UL
/* DMA 可达内存窗口 GD5F_DMA_RAM_BASE/END 与 CCM 窗口常量已上移到 gd5f2gq5ue.h
* (N-26,2026-10-12:nand_ftl.c 的 init 期自检也要用),并带编译期守卫
* (白名单窗口与 CCM 永不相交、阈值契约)。此处只留语义提醒:
* 窗口必须与分散加载 RW_IRAM1(0x20000000,128KB,SRAM1+SRAM2)一致;
* CCM(0x10000000,64KB)DMA1/DMA2 物理不可达,RW_CCM 区任何缓冲都不得进 DMA。
* 阈值只能挡住"当前已知的小栈缓冲",挡不住"将来新加的 >1024B 栈缓冲";
* gd5f_dma_buf_ok() 把这条约定升级为运行时强制检查(2026-09-21 加固)。 */
/*
* 函数功能:SPI1 TX DMA 完成回调(PROGRAM_LOAD 数据发送用)
@@ -104,8 +103,7 @@ static int gd5f_private_bbt_find_best(void);
static int gd5f_bbt_load(void);
static int gd5f_bbt_save(void);
/* DMA 可达性守卫(见下方实现) */
static int gd5f_dma_buf_ok(const void *p_buf, size_t size);
/* DMA 可达性守卫:实现见下方(N-26 起为公开 API,声明在 gd5f2gq5ue.h) */
/* ======================== DMA 可达性守卫 ======================== */
@@ -120,7 +118,7 @@ static int gd5f_dma_buf_ok(const void *p_buf, size_t size);
* 本函数把"栈/堆缓冲禁入 DMA"的约定升级为运行时强制约束:
* 起止地址任一越出 SRAM 区间即判不可达。
*/
static int gd5f_dma_buf_ok(const void *p_buf, size_t size) {
int gd5f_dma_buf_ok(const void *p_buf, size_t size) {
uint32_t start = (uint32_t)(uintptr_t)p_buf;
uint32_t end;
+41
View File
@@ -77,6 +77,35 @@ extern "C" {
*/
#define GD5F_DMA_THRESHOLD 1024
/* ==================== DMA 可达内存窗口(N-26,2026-10-12) ====================
* DMA1/DMA2 物理不可达 CCM(0x10000000~0x1000FFFF):以 CCM 缓冲做 DMA 源/目的
* 会静默不落地(2026-09-20 首轮 CCM 扩容实验实证根因;口径见
* docs/内存管理/内存内容分配策略.md §2 与 §4 规则 R2/R3)。
* 白名单窗口必须与分散加载 RW_IRAM1(0x20000000,128KB,SRAM1+SRAM2)一致
* (MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct);将来新增其它 DMA 可达内存
* (如外部 SDRAM)须同步扩展本窗口。
* ⚠ 缓冲**自身地址**无法编译期断言:.bss 地址链接期才确定,armcc 的常量表达式
* 取不到(__builtin 系在 MicroLIB 下亦不可用,见 Src/main.c Error_Handler 注释)。
* 静态缓冲的落位由 scatter(RW_CCM 只列 heap_4.o)+ nand_ftl.c 的 init 期自检
* (gd5f_dma_buf_ok)双层保证;动态传入缓冲由每次传输的守卫强制。 */
#define GD5F_DMA_RAM_BASE 0x20000000UL
#define GD5F_DMA_RAM_END 0x20020000UL
#define GD5F_CCM_RAM_BASE 0x10000000UL
#define GD5F_CCM_RAM_END 0x10010000UL
/* 编译期守卫(N-26):AC5 无 _Static_assert(C11 不支持),用项目惯用的
* 负数组 typedef(同 App/cfg/cfg_file.c、Drivers/BSP/UI/ui_menu_tab.h)——
* 条件不成立则数组尺寸 -1 → 编译错。
* ① DMA 白名单窗口与 CCM 窗口**永不得重叠**(有人改上面常量时立即爆炸);
* ② 阈值契约(见 GD5F_DMA_THRESHOLD 处注释):≥ 存储路径最大栈缓冲
* (dhara meta 132 B = DHARA_META_SIZE 上限)且 < 页大小(页级传输保留 DMA)。 */
typedef char gd5f_dma_window_no_ccm_check_t[
((GD5F_DMA_RAM_END <= GD5F_CCM_RAM_BASE) || (GD5F_DMA_RAM_BASE >= GD5F_CCM_RAM_END))
? 1 : -1];
typedef char gd5f_dma_threshold_contract_check_t[
((GD5F_DMA_THRESHOLD >= 132U) && (GD5F_DMA_THRESHOLD < GD5F_PAGE_SIZE))
? 1 : -1];
/* 坏块表持久化:保留块池数量(从 NAND 顶部向下挑选出厂好块存储 BBT) */
#define GD5F_BBT_POOL_COUNT 4
@@ -253,6 +282,18 @@ int32_t gd5f_program_exec(uint32_t page_addr);
int32_t gd5f_block_erase(uint32_t block_addr);
int32_t gd5f_check_ecc(void);
/*
* 函数功能:判定缓冲整段是否位于 DMA 可达内存(GD5F_DMA_RAM_* 白名单窗口)
* 入口参数:p_buf - 传输缓冲首地址 const void*
* size - 传输字节数 size_t
* 返回值:1 - 整段 DMA 可达;0 - 不可达(含 size=0 / 地址回绕 / 越窗)
* 限定条件:无
* 函数说明:N-26(2026-10-12)由 gd5f2gq5ue.c 私有守卫升级为公开 API——
* 驱动内部每次 >阈值 传输前用它(不可达即回落 CPU 轮询),
* FTL 层(nand_ftl.c)在 init 期用它自检静态页缓冲的落位。
*/
int gd5f_dma_buf_ok(const void *p_buf, size_t size);
/*
* 函数功能:NAND 长耗时操作看门狗心跳弱钩子
* 入口参数:无
+18
View File
@@ -228,6 +228,24 @@ DSTATUS disk_initialize(BYTE pdrv) {
DBG_INFO("Initializing NAND FTL...");
/* N-26 启动期地址自检(2026-10-12):本文件三个页缓冲是 SPI1 DMA 路径上
* 唯一的**静态**缓冲(dhara_map_read/write → dhara_nand_read/prog/copy,
* 2048 B > GD5F_DMA_THRESHOLD;s_page_buf 现仅装 dhara meta 132 B 走轮询,
* 但它同为页尺寸且交给 dhara 内部使用,一并守住防将来越窗)。
* DMA1/DMA2 不可达 CCM——若 scatter 改动/段放置把它们挪进 CCM,DMA 会
* **静默不落地**(数据错而传输"成功")。缓冲地址链接期才确定,armcc
* 无法编译期断言(见 gd5f2gq5ue.h 的窗口常量注释),故在 init 期自检:
* 任一缓冲越出 DMA 白名单窗口 → 上报并拒绝挂载(存储降级、不挂死,
* 与 E-31 的"保留卷、上报自检"形态一致)。每次传输的运行期守卫
* (gd5f_read_from_cache/gd5f_program_load 内)保留作第二层兜底。 */
if ((gd5f_dma_buf_ok(s_cache_buf, FTL_PAGE_SIZE) == 0) ||
(gd5f_dma_buf_ok(s_copy_buf, FTL_PAGE_SIZE) == 0) ||
(gd5f_dma_buf_ok(s_page_buf, FTL_PAGE_SIZE) == 0)) {
DBG_ERROR("FTL page buffer not DMA-reachable (cache=0x%08lX), "
"storage refused", (unsigned long)(uintptr_t)s_cache_buf);
return STA_NOINIT;
}
s_nand.log2_page_size = 11;
s_nand.log2_ppb = 6;
/* dhara 可见块数 = 总块数 - 保留块池(BBT 存储区不交给 dhara,避免被擦写) */
+132 -9
View File
@@ -16,6 +16,8 @@
#include "private/lftpd_io.h"
#include "net_socket.h"
#include "net_config.h"
#include "lftpd_auth.h"
#include "ftp_cfg.h"
#include "ff.h"
typedef struct {
@@ -62,6 +64,10 @@ static command_t commands[] = {
{ NULL, NULL },
};
/* N-19 FTP 鉴权:全局失败计数/锁定状态(跨会话持久——单连接服务器,全局
* 计数即可;成功清零、锁定期满自动放行,见 lftpd_auth.c) */
static ftp_auth_state_t s_auth_state;
static int send_response(int socket, int code, bool include_code,
bool multiline_start, const char *format, ...)
{
@@ -100,8 +106,8 @@ static int send_response(int socket, int code, bool include_code,
static int send_list(int socket, const char *path)
{
static const char *directory_format = "drw-rw-rw- 1 owner group %13lu Jan 01 1970 %s";
static const char *file_format = "-rw-rw-rw- 1 owner group %13lu Jan 01 1970 %s";
static const char *directory_format = "drw-rw-rw- 1 owner group %13lu Jan 01 1970 %s\r\n";
static const char *file_format = "-rw-rw-rw- 1 owner group %13lu Jan 01 1970 %s\r\n";
void *dp = lftpd_io_opendir(path);
if (dp == NULL) {
@@ -112,17 +118,39 @@ static int send_list(int socket, const char *path)
uint32_t size;
int is_dir;
/* N30-PROBE(临时实验):整份目录一次性发送(验证"背靠背多小段
* 毒化芯片 TX"假设——每行一个 net_send 时 LIST#4 必挂) */
static char batch[1536]; /* static: off-stack */
int blen = 0;
int trunc = 0;
while (lftpd_io_readdir(dp, name, sizeof(name), &size, &is_dir) == 0) {
int n;
if (is_dir) {
send_multiline_response_line(socket, directory_format,
(unsigned long)size, name);
n = snprintf(batch + blen, sizeof(batch) - blen, directory_format,
(unsigned long)size, name);
} else {
send_multiline_response_line(socket, file_format,
(unsigned long)size, name);
n = snprintf(batch + blen, sizeof(batch) - blen, file_format,
(unsigned long)size, name);
}
if (n < 0 || (size_t)n >= sizeof(batch) - blen) {
trunc = 1;
break;
}
blen += n;
}
lftpd_io_closedir(dp);
/* lftpd_inet_write 成功返回字节数、失败返回 -1(非 0=成功语义) */
if (blen > 0) {
if (lftpd_inet_write(socket, batch, blen) < 0) {
return -1;
}
}
if (trunc) {
return -1;
}
return 0;
}
@@ -137,13 +165,33 @@ static int send_nlst(int socket, const char *path)
uint32_t size;
int is_dir;
/* N30-PROBE(临时实验):同 send_list,一次性批量发送 */
static char batch[1536]; /* static: off-stack */
int blen = 0;
int trunc = 0;
while (lftpd_io_readdir(dp, name, sizeof(name), &size, &is_dir) == 0) {
if (!is_dir) {
send_multiline_response_line(socket, name);
int n = snprintf(batch + blen, sizeof(batch) - blen, "%s\r\n", name);
if (n < 0 || (size_t)n >= sizeof(batch) - blen) {
trunc = 1;
break;
}
blen += n;
}
}
lftpd_io_closedir(dp);
/* lftpd_inet_write 成功返回字节数、失败返回 -1(非 0=成功语义) */
if (blen > 0) {
if (lftpd_inet_write(socket, batch, blen) < 0) {
return -1;
}
}
if (trunc) {
return -1;
}
return 0;
}
@@ -241,6 +289,14 @@ static int cmd_cwd(lftpd_client_t *client, const char *arg)
static int cmd_dele(lftpd_client_t *client, const char *arg)
{
/* N-19:写命令门——[ftp] write=0 时拒绝(鉴权门已保证此处必为已登录态) */
const ftp_cfg_t *cfg = ftp_cfg_get();
if ((cfg == NULL) || (cfg->write_en == 0u)) {
send_simple_response(client->socket, 550, STATUS_550);
return 0;
}
if (arg == NULL || strlen(arg) == 0) {
send_simple_response(client->socket, 550, STATUS_550);
return -1;
@@ -374,7 +430,33 @@ static int cmd_noop(lftpd_client_t *client, const char *arg)
static int cmd_pass(lftpd_client_t *client, const char *arg)
{
send_simple_response(client->socket, 230, STATUS_230);
const ftp_cfg_t *cfg = ftp_cfg_get();
ftp_auth_creds_t creds;
ftp_auth_result_t res;
if (cfg == NULL) {
/* fail-closed:配置不可用即拒绝(s_cfg 静态常驻,正常不会走到) */
send_simple_response(client->socket, 530, STATUS_530);
return 0;
}
creds.p_user = cfg->user;
creds.p_pwd_hash = cfg->pwd_hash;
creds.write_en = cfg->write_en;
res = ftp_auth_check(&creds, client->auth_user_ok,
(arg != NULL) ? arg : "",
&s_auth_state, (uint32_t)xTaskGetTickCount());
if (res == FTP_AUTH_OK) {
client->auth_logged_in = 1;
send_simple_response(client->socket, 230, STATUS_230);
return 0;
}
/* 预期事件(爆破/配错口令):用 INFO 而非 ERROR——[ERR] 是 @test Forbidden 标记 */
if (res == FTP_AUTH_LOCKED) {
lftpd_log_info("auth locked after %u failures", (unsigned)FTP_AUTH_FAIL_MAX);
} else {
lftpd_log_info("auth failed for user '%s'", client->auth_user);
}
send_simple_response(client->socket, 530, STATUS_530);
return 0;
}
@@ -500,6 +582,14 @@ static int cmd_size(lftpd_client_t *client, const char *arg)
static int cmd_stor(lftpd_client_t *client, const char *arg)
{
/* N-19:写命令门——[ftp] write=0 时拒绝(鉴权门已保证此处必为已登录态) */
const ftp_cfg_t *cfg = ftp_cfg_get();
if ((cfg == NULL) || (cfg->write_en == 0u)) {
send_simple_response(client->socket, 550, STATUS_550);
return 0;
}
if (client->data_socket == -1) {
send_simple_response(client->socket, 425, STATUS_425);
return -1;
@@ -552,7 +642,20 @@ static int cmd_type(lftpd_client_t *client, const char *arg)
static int cmd_user(lftpd_client_t *client, const char *arg)
{
send_simple_response(client->socket, 230, STATUS_230);
const ftp_cfg_t *cfg = ftp_cfg_get();
client->auth_logged_in = 0;
client->auth_user[0] = '\0';
client->auth_user_ok = 0;
if ((arg != NULL) && (cfg != NULL)) {
strncpy(client->auth_user, arg, sizeof(client->auth_user) - 1);
client->auth_user[sizeof(client->auth_user) - 1] = '\0';
client->auth_user_ok = (uint8_t)ftp_auth_user_match(cfg->user,
client->auth_user);
}
/* 不论用户名对错一律 331:不泄露"用户是否存在";错用户名在 cmd_pass
* 仍走一次同耗时哈希比较(时间拉平,见 lftpd_auth.h 说明 2) */
send_simple_response(client->socket, 331, STATUS_331);
return 0;
}
@@ -595,6 +698,21 @@ static int handle_control_channel(lftpd_client_t *client)
command_tmp[i] = (char)toupper((int)command_tmp[i]);
}
/* N-19:未鉴权命令门——仅 QUIT/FEAT/SYST/NOOP 放行(客户端兼容需要,
* 且无信息量);其余一律 530。USER/PASS 本身也在放行名单外,
* 但它们在 gate 之前不可能通过 → 单独放行见下(USER/PASS 例外) */
if (client->auth_logged_in == 0) {
if ((strcmp(command_tmp, "USER") != 0) &&
(strcmp(command_tmp, "PASS") != 0) &&
(strcmp(command_tmp, "QUIT") != 0) &&
(strcmp(command_tmp, "FEAT") != 0) &&
(strcmp(command_tmp, "SYST") != 0) &&
(strcmp(command_tmp, "NOOP") != 0)) {
send_simple_response(client->socket, 530, STATUS_530);
continue;
}
}
bool matched = false;
for (int i = 0; commands[i].command; i++) {
if (strcmp(commands[i].command, command_tmp) == 0) {
@@ -640,6 +758,11 @@ int lftpd_start(const char *directory, int port, lftpd_t *lftpd)
lftpd->port = port;
lftpd->server_socket = -1; /* memset 清零后显式置"无监听器"(0 是合法 fd) */
/* N-19:出厂默认口令在用告警(台架锚点;改了 [ftp] pwd_hash 即消失) */
if (ftp_cfg_is_default_pwd() != 0) {
lftpd_log_info("using factory default password");
}
while (true) {
lftpd_task_wd_beat(); /* 看门狗:事件循环每轮喂心跳(ftpTask 主循环节拍点) */
if (lftpd->server_socket < 0) {
+5
View File
@@ -6,6 +6,11 @@ typedef struct {
char directory[256];
int socket;
int data_socket;
/* N-19 FTP 鉴权会话态(每会话 memset 0 = 未登录;跨会话锁定计数在
* lftpd.c 的 s_auth_state,不在此处) */
char auth_user[32]; /* USER 阶段暂存的用户名 */
uint8_t auth_user_ok; /* 1 = 用户名与 [ftp] user 匹配 */
uint8_t auth_logged_in; /* 1 = PASS 校验通过 */
} lftpd_client_t;
typedef struct {
+100
View File
@@ -0,0 +1,100 @@
/*
* 模块名称:lftpd_auth
* 模块功能:FTP 鉴权状态机实现(纯函数,仅依赖 stdint/string 与 sha256)
* 适用平台:STM32F407ZGTx / PC 主机侧单测(双端共用)
* 作者:王建锋
* 创建日期:2026-10-11
* 修改记录:
* v1.0 2026-10-11 王建锋 创建初始版本(N-19 FTP 鉴权;契约由
* test/host/ftp_auth 主机单测钉死)
*/
/* 头文件包含区 */
#include <stddef.h>
#include <string.h>
#include "lftpd_auth.h"
#include "sha256.h"
/* 函数定义区 */
/*
* 函数功能:用户名匹配(大小写不敏感)
* 入口参数:p_expect - 配置用户名 p_user - 客户端 USER 参数
* 返回值:1 - 匹配;0 - 不匹配或任一为 NULL int
* 限定条件:无
*/
int ftp_auth_user_match(const char *p_expect, const char *p_user) {
if ((p_expect == NULL) || (p_user == NULL)) {
return 0;
}
for (size_t i = 0u; ; i++) {
char a = p_expect[i];
char b = p_user[i];
if ((a >= 'A') && (a <= 'Z')) {
a = (char)(a + 32);
}
if ((b >= 'A') && (b <= 'Z')) {
b = (char)(b + 32);
}
if (a != b) {
return 0;
}
if (a == '\0') {
return 1;
}
}
}
/*
* 函数功能:口令校验 + 失败计数 + 限时锁定(鉴权契约核心)
* 入口参数:p_creds - 凭据 user_ok - USER 阶段匹配结果 p_pass - 口令
* p_state - 会话外共享状态 now_tick - 当前 tick(ms)
* 返回值:FTP_AUTH_OK / FAIL / LOCKED(枚举)
* 限定条件:锁定判定先于哈希(锁定期内不做哈希);用户名错也走哈希(耗时拉平);
* tick 有符号差抗 32 位回绕;NULL 入参 fail-closed 返回 FAIL
*/
ftp_auth_result_t ftp_auth_check(const ftp_auth_creds_t *p_creds,
uint8_t user_ok, const char *p_pass,
ftp_auth_state_t *p_state, uint32_t now_tick) {
sha256_ctx_t ctx;
uint8_t digest[SHA256_DIGEST_SIZE];
int ok;
if ((p_creds == NULL) || (p_state == NULL)) {
return FTP_AUTH_FAIL; /* fail-closed */
}
if (p_state->fail_count >= FTP_AUTH_FAIL_MAX) {
/* 有符号差:now 在 lock_until 之前(含回绕)= 仍在锁定窗内 */
if ((int32_t)(now_tick - p_state->lock_until_tick) < 0) {
return FTP_AUTH_LOCKED;
}
p_state->fail_count = 0u; /* 锁定期满,重新起算 */
}
/* 盐 = 期望用户名;用户名错也走同一哈希路径(耗时拉平,见头文件说明 2) */
sha256_init(&ctx);
if (p_creds->p_user != NULL) {
sha256_update(&ctx, p_creds->p_user, strlen(p_creds->p_user));
}
if (p_pass != NULL) {
sha256_update(&ctx, p_pass, strlen(p_pass));
}
sha256_final(&ctx, digest);
ok = ((user_ok != 0u) &&
(sha256_hex_eq(digest, p_creds->p_pwd_hash) != 0)) ? 1 : 0;
if (ok != 0) {
p_state->fail_count = 0u;
return FTP_AUTH_OK;
}
p_state->fail_count++;
if (p_state->fail_count >= FTP_AUTH_FAIL_MAX) {
p_state->lock_until_tick = now_tick + FTP_AUTH_LOCK_TICKS;
}
return FTP_AUTH_FAIL;
}
+73
View File
@@ -0,0 +1,73 @@
/*
* 模块名称:lftpd_auth
* 模块功能:FTP 鉴权状态机(纯函数、零平台依赖,主机侧可测)——用户名匹配、
* SHA-256(user‖password) 恒定时间校验、连续失败计数与限时锁定;
* tick 由调用方注入(MCU = xTaskGetTickCount,单测 = 合成值)
* 适用平台:STM32F407ZGTx / PC 主机侧单测(双端共用)
* 作者:王建锋
* 创建日期:2026-10-11
* 修改记录:
* v1.0 2026-10-11 王建锋 创建初始版本(N-19 FTP 鉴权;lftpd.c 为唯一消费者)
*/
#ifndef LFTPD_AUTH_H
#define LFTPD_AUTH_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#define FTP_AUTH_FAIL_MAX 5u /* 连续失败 N 次后锁定 */
#define FTP_AUTH_LOCK_TICKS 60000u /* 锁定时长(tick = 1 ms → 60 s) */
/* 一次校验所需的配置快照(由消费者从 ftp_cfg_t 组装,本模块不认配置引擎) */
typedef struct {
const char *p_user; /* 期望用户名 */
const char *p_pwd_hash; /* 64 字符 hex 摘要 */
uint8_t write_en; /* 透传位:写命令门由 lftpd 侧执行,本模块不判 */
} ftp_auth_creds_t;
/* 鉴权状态(跨会话持久;单连接服务器,全局一份即可) */
typedef struct {
uint8_t fail_count; /* 连续失败计数(成功即清零) */
uint32_t lock_until_tick; /* 锁定截止 tick(fail_count ≥ FAIL_MAX 时有效) */
} ftp_auth_state_t;
typedef enum {
FTP_AUTH_OK = 0, /* 口令正确 */
FTP_AUTH_FAIL, /* 口令/用户名错误(已计入失败计数) */
FTP_AUTH_LOCKED /* 锁定中(本次不校验、不计数) */
} ftp_auth_result_t;
/*
* 函数功能:用户名匹配(大小写不敏感,与配置引擎键名口径一致)
* 入口参数:p_expect - 期望用户名 p_user - 来客输入
* 返回值:1 - 匹配;0 - 不匹配或任一为 NULL int
* 限定条件:无
*/
int ftp_auth_user_match(const char *p_expect, const char *p_user);
/*
* 函数功能:口令校验 + 失败计数 + 限时锁定(一次 PASS 的完整判定)
* 入口参数:p_creds - 配置快照 user_ok - USER 阶段的用户名匹配结果
* p_pass - 来客口令(NULL 按空串) p_state - 鉴权状态
* now_tick - 当前 tick(ms)
* 返回值:ftp_auth_result_t
* 限定条件:p_creds/p_state 非空(NULL 一律 FAIL,fail-closed)
* 函数说明:1. 锁定中直接 LOCKED(不算哈希——锁定状态非机密,无侧信道);
* tick 差用 (int32_t) 有符号比较,天然抗 32 位回绕
* 2. 无论用户名对错都走同一条哈希路径(盐 = 期望用户名),
* 耗时一致 → 不泄露"用户是否存在"
* 3. 成功清零计数;失败累计到 FTP_AUTH_FAIL_MAX 上锁
*/
ftp_auth_result_t ftp_auth_check(const ftp_auth_creds_t *p_creds,
uint8_t user_ok, const char *p_pass,
ftp_auth_state_t *p_state, uint32_t now_tick);
#ifdef __cplusplus
}
#endif
#endif /* LFTPD_AUTH_H */
-27
View File
@@ -1,27 +0,0 @@
#include "private/lftpd_log.h"
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#include "dbg_log.h"
void lftpd_log_internal(const char* level, const char* format, ...) {
static char buffer[256]; /* static: called at depth in ftpTask, keep off-stack */
static char line[288]; /* static: 组装含前缀的整行,交给控制台单写者 */
va_list args;
va_start(args, format);
int err = vsnprintf(buffer, sizeof(buffer), format, args);
va_end(args);
if (err >= sizeof(buffer)) {
return;
}
/* 走控制台单写者(App/log/console_log.c):整行一次入缓冲,避免逐字符 fputc 交错 */
int n = snprintf(line, sizeof(line), "%s %s\r\n", level, buffer);
if (n > 0) {
if (n > (int)sizeof(line) - 1) {
n = (int)sizeof(line) - 1;
}
(void)dbg_console_write_line(line, (unsigned)n);
}
}
+143 -11
View File
@@ -581,8 +581,18 @@ static void net_reconcile_tick(void) {
bad = 1;
} else if ((p_sock->state == NET_SOCK_STATE_LISTENING) &&
(st[1] == CH395F_TCP_ESTABLISHED)) {
/* ② 本层 LISTENING 而芯片已完成握手(丢 CONNECT 中断)→ 补做 accept */
bad = 2;
/* ② 本层 LISTENING 而芯片已完成握手(丢 CONNECT 中断)→ 补做 accept
* N-30(Trap 25)幽灵态甄别:拆链收尾窗口内芯片假报 ESTABLISHED,
* 回读对端端口 = 上一条连接端口 → 不补 accept(补了会把数据发给
* 已死的旧连接 = 生产 LIST#4 数据被吞的直接成因),等拆净或新 SYN */
uint8_t remote[6] = {0, 0, 0, 0, 0, 0};
ch395f_get_remot_ipp((uint8_t)i, remote);
uint16_t peer = (uint16_t)(remote[4] | (remote[5] << 8));
/* 仅"端口非零且 = 上一条连接端口"判幽灵拦截;peer==0 不拦截
* (对端端口提交滞后时不得挡掉丢中断救援,见 net_lost_connect_probe 注释) */
if (!((peer != 0U) && (peer == p_sock->last_peer_port))) {
bad = 2;
}
} else if ((p_sock->state == NET_SOCK_STATE_LISTENING) &&
((st[0] == CH395F_SOCKET_CLOSED) || (st[1] != CH395F_TCP_LISTEN))) {
/* ③ 本层 LISTENING 而芯片监听已消失 → 重建监听 */
@@ -768,6 +778,9 @@ void net_process_messages(void) {
* close/重监听命令(会摧毁芯片侧仍健康的监听器 → 端口永拒,
* run8 实证 170s 拒绝);新连接的 CONNECT 中断会把两侧重新同步 */
p_sock->state = NET_SOCK_STATE_LISTENING;
/* N-30(Trap 25):死连接对端端口转登幽灵甄别基准 + 去抖清零 */
p_sock->last_peer_port = p_sock->remote_port;
p_sock->lost_conn_seen = 0U;
/* 取消本 fd 在死连接上的挂起 recv(必通知,防调用方挂死) */
net_pending_cancel(msg.sockfd, NET_ERR_CONNRESET);
/* 仅修正 MCU 侧状态,不发芯片命令,直接返回成功 */
@@ -1090,6 +1103,8 @@ int net_listen_locked(int sockfd) {
p_sock->state = NET_SOCK_STATE_LISTENING;
/* 单连接模式:连接断开后由 net 层自动回到 LISTEN(模拟 BSD 监听常驻) */
p_sock->auto_relisten = 1;
/* N-30(Trap 25):新监听会话,丢中断去抖计数清零 */
p_sock->lost_conn_seen = 0U;
/* 单连接:CONNECT 中断直接作用于本 Socket 自身;启动监听前清空各
* Socket 及全局中断状态寄存器(读即清除),避免历史残留/伪中断在
* 监听刚开始时误触发连接或断开事件 */
@@ -1164,6 +1179,84 @@ int net_listen(int sockfd) {
* 限定条件:Socket 处于 LISTENING 状态
* 函数说明:单连接:监听 Socket 自身在 CONNECT 后已转为 ESTABLISHED
*/
/* ===== N-30 accept 路径"丢 CONNECT 中断"补 accept(2026-10-11 裸机芯片台架实证)=====
* 台架结论(剥离 OS/net/FTP 直接对芯片做受控 close→open→listen→connect 矩阵):
* 芯片关闭连接后的拆链收尾窗口**变长且不可见**(状态寄存器显示 CLOSED 也不代表
* 拆完,实测 >2s 仍可能未拆完);该窗口内重新 listen 后,新连接握手照常完成
* (芯片状态寄存器 → ESTABLISHED)但 **CONNECT 中断被丢**——纯事件驱动的 accept
* 永远等不到 → 数据被吞 / 425。固定沉降延时、Socket 轮换、listen 后 close 重试、
* 缓冲重分配均已被实验证伪;唯一有效 = "中断 + 状态轮询"双信源(修复组 15/15
* 全过,机制触发 5 次全部救回)。→ CH395F_Trap_Records.md Trap 25
* 幽灵态甄别:拆链窗口内芯片也会直接假报 ESTABLISHED(旧连接未拆净),此时回读
* 对端端口 = 上一条连接端口;新连接临时端口几乎必然不同 → 端口相同不 accept,
* 等旧连接拆净 / 新 SYN 到达(新连接若也丢中断,后续轮询以新端口命中)。 */
#define NET_LOST_CONN_STABLE_N 2U /* 连续确认轮次(accept 轮询 100ms/轮,≈200ms 去抖) */
/*
* 函数功能:监听等待期的芯片状态轮询探针——芯片已 ESTABLISHED 而 CONNECT 中断
* 未到(丢中断)时,去抖满后补做 accept(复用 CONNECT 事件簿记路径)。
* 入口参数:p_sock - 处于 LISTENING 的监听控制块 net_sock_t *
* 出口参数:无(命中时 p_sock->state 经 handle_connect_event 转 ESTABLISHED)
* 返回值:无
* 限定条件:netTask 上下文(net_accept_locked 内调用,SPI 访问安全)
* 函数说明:1. 每轮 accept 轮询(100ms)读一次芯片状态,兼作状态第二信源
* 2. 幽灵态(对端端口 = last_peer_port)不动手,去抖清零
* 3. 连续 NET_LOST_CONN_STABLE_N 轮确认后才补 accept(防"刚 listen
* 尚未生效"的瞬态误判——2026-09-23 实测教训)
*/
static void net_lost_connect_probe(net_sock_t *p_sock) {
/* 芯片侧状态两字节:[0]=socket 状态,[1]=TCP 状态 */
uint8_t st[2] = {0, 0};
/* 对端 IP+端口回读缓冲(4B IP + 2B 端口小端) */
uint8_t remote[6] = {0, 0, 0, 0, 0, 0};
/* 读芯片状态 */
ch395f_get_socket_status(p_sock->ch395_sock, st);
/* 芯片尚未完成握手:去抖计数清零,本轮无动作 */
if (st[1] != CH395F_TCP_ESTABLISHED) {
p_sock->lost_conn_seen = 0U;
return;
}
/* 已 ESTABLISHED:回读对端端口做幽灵甄别 */
ch395f_get_remot_ipp(p_sock->ch395_sock, remote);
/* 对端端口(小端 2 字节) */
uint16_t peer = (uint16_t)(remote[4] | (remote[5] << 8));
/* 幽灵甄别:仅"端口非零且 = 上一条连接端口"才判幽灵拦截;
* peer==0(芯片对端端口提交滞后,Trap 15 同类)不拦截——宁可补 accept
* 也不退回丢中断死等(2026-10-11 验证轮实测:peer==0 拦截把首连接
* 丢中断的原有对账救援也挡死,LIST#1 卡满 20s,比修复前更糟) */
if ((peer != 0U) && (peer == p_sock->last_peer_port)) {
p_sock->lost_conn_seen = 0U;
return;
}
/* 去抖:连续确认满门槛才补 accept(封顶防溢出) */
if (p_sock->lost_conn_seen < NET_LOST_CONN_STABLE_N) {
p_sock->lost_conn_seen++;
}
if (p_sock->lost_conn_seen < NET_LOST_CONN_STABLE_N) {
return;
}
/* 动手前清去抖计数 */
p_sock->lost_conn_seen = 0U;
/* 留痕:丢中断自愈命中 */
DBG_INFO("sock%d lost-CONNECT self-accept (peer=%u)",
(int)p_sock->ch395_sock, peer);
/* 复用 CONNECT 事件的簿记路径(转 ESTABLISHED + 记对端 + 事件回调) */
handle_connect_event(p_sock);
}
/*
* 函数功能:TCP Server 接受新连接(内部版本,必须在 netTask 上下文调用)
* 入口参数:sockfd - 监听 Socket 描述符 int
* 出口参数:addr - 输出:客户端地址结构体指针 struct net_sockaddr *(可为 NULL)
* addrlen - 输入输出:地址结构体长度 int *(可为 NULL)
* 返回值:新连接的 Socket 描述符,失败返回 -1 int
* 限定条件:Socket 处于 LISTENING 状态
* 函数说明:1. 单连接:监听 Socket 自身在 CONNECT 后已转为 ESTABLISHED
* 2. 等待期叠加芯片状态轮询探针(丢 CONNECT 中断自愈,Trap 25)
* 3. CLOSED + 芯片 RX 缓冲有残留数据:接受"已关但可读"连接排空
* (FUN_PARA §5.52 ② 保留现场契约,Trap 25 三次定案)
*/
static int net_accept_locked(int sockfd, struct net_sockaddr *addr, int *addrlen) {
/* 监听 Socket 控制块 */
net_sock_t *p_listen_sock;
@@ -1178,18 +1271,43 @@ static int net_accept_locked(int sockfd, struct net_sockaddr *addr, int *addrlen
/* 先轮询一次:把状态机刷新到芯片侧最新状态 */
net_poll();
/* N-30(Trap 25):监听等待期兼做芯片状态第二信源——丢 CONNECT 中断时
* 去抖补 accept(裸机台架实证:拆链窗口丢中断是数据被吞/425 的主根因,
* 纯事件等待无自愈可能;详见 net_lost_connect_probe 区块注释) */
if (p_listen_sock->state == NET_SOCK_STATE_LISTENING) {
net_lost_connect_probe(p_listen_sock);
}
/* 单连接:本 Socket 自身在 CONNECT 后处于 ESTABLISHED 才表示有可接受的连接
* 芯片丢中断导致的失同步不在此处自愈:单次状态读会误判"刚 listen 尚未生效"
* (2026-09-23 实测风暴:监听被反复重建直至放弃),统一由 net_reconcile_tick() 周期对账(带去抖)处理 */
* (含上一步探针补 accept 的命中);其余失同步由 net_reconcile_tick()
* 周期对账(带去抖)兜底 */
if (p_listen_sock->state != NET_SOCK_STATE_ESTABLISHED) {
/* 连接已断开(CLOSED)且启用自动重监听:先把 Socket 重新置回 LISTEN,
* 下一次 accept 即可等待新连接(BSD 风格监听常驻) */
/* N-30(2026-10-11 手册 §5.23⑤ + 生产时间线定案):
* "closed-but-readable" 接受——STOR 类传输客户端**发完数据立即关数据连接**
* (正常 FTP 行为,Python ftplib sendall 后即 close),芯片按
* FUN_PARA(SOCKET_CLOSE=0) 规格保留 Socket 与 RX 缓冲供 MCU 排空
* (实测 DISCONNECT 后 st=0x05/0x00、recv_len=1027=完整载荷)。
* 若 accept 轮询晚于对端 FIN,盲目 relisten(close+open 清缓冲)会把
* 已到达的数据销毁 → 425(此前 N-30"425/被吞"的主根因,并非芯片杀连接)。
* 处置:芯片 RX 缓冲有数据 → 接受这条"已关但可读"连接,应用经
* net_recv_locked 快路径(无状态门槛)排空,排空后 EOF 自然触发 relisten。 */
int drain_pending = 0;
if (p_listen_sock->state == NET_SOCK_STATE_CLOSED && p_listen_sock->auto_relisten) {
/* 非阻塞:失败由 D10 重试轮询接管 */
(void)net_relisten_try(p_listen_sock);
if (ch395f_get_recv_len((uint8_t)sockfd) > 0) {
drain_pending = 1;
DBG_INFO("sock%d closed-with-data accept (drain pending)", sockfd);
} else {
/* 连接已断开(CLOSED)且无残留数据:先把 Socket 重新置回 LISTEN,
* 下一次 accept 即可等待新连接(BSD 风格监听常驻) */
/* 非阻塞:失败由 D10 重试轮询接管 */
(void)net_relisten_try(p_listen_sock);
}
}
/* 当前无连接 → WOULDBLOCK */
return NET_ERR_RETURN_1(NET_ERR_WOULDBLOCK);
if (drain_pending == 0) {
/* 当前无连接 → WOULDBLOCK */
return NET_ERR_RETURN_1(NET_ERR_WOULDBLOCK);
}
/* drain_pending:落入下方 accept 返回(对端簿记仍在控制块,排空后 EOF 了结) */
}
/* 填充对端地址(若调用方需要) */
@@ -1895,6 +2013,10 @@ static int net_relisten_locked(net_sock_t *p_sock) {
p_sock->state = NET_SOCK_STATE_LISTENING;
/* 新监听不得继承上一条连接的释放原因(Trap 23 待办 #3) */
p_sock->released_abnormal = 0U;
/* N-30(Trap 25):清零前把对端端口转登为幽灵甄别基准 */
p_sock->last_peer_port = p_sock->remote_port;
/* N-30:新监听会话,丢中断去抖计数清零 */
p_sock->lost_conn_seen = 0U;
/* 清对端端口(新连接待用) */
p_sock->remote_port = 0;
/* 清对端 IP */
@@ -2432,7 +2554,11 @@ static int net_close_locked(net_sock_t *p_sock) {
} while (1);
/* 清零控制块(memset 0 = 释放本 fd 全部状态) */
/* N-30(Trap 25):幽灵甄别基准跨 fd 释放保留(幽灵属芯片 Socket 级状态,
* 与 fd 复用无关——close 后新 listen 落在同一芯片 Socket 上仍可能撞幽灵) */
uint16_t keep_peer = p_sock->remote_port;
memset(p_sock, 0, sizeof(net_sock_t));
p_sock->last_peer_port = keep_peer;
/* 关闭即取消该 fd 的待重试(D10 重试队列) */
net_relisten_clear(sockfd);
/* 关闭即清除该 fd 的挂起状态(L3;幂等) */
@@ -3478,7 +3604,8 @@ static void handle_disconnect_event(net_sock_t *p_sock) {
}
/* 单连接:先不关 CH395F Socket,
* 留给 net_recv_locked 把剩余数据读出来再关,置 CLOSED 等待应用重新监听 */
DBG_INFO("sock%d DISCONNECTED, releasing (chip tcp=0x%02X)", p_sock->ch395_sock, st[1]);
DBG_INFO("sock%d DISCONNECTED, releasing (chip st=0x%02X/0x%02X)",
p_sock->ch395_sock, st[0], st[1]);
/* Trap 23 推广(2026-09-24 台架实证):正常 DISCONNECT 释放留下的芯片收尾状态
* 会在复用同一 fd 的下一条连接上以"伪 DISCONNECT"形式投递(实测 accept 后约 10~20ms)→ 该连接被提前释放,应用把已到的 2048B 当整份文件并回报 226;
* 仅**本连接收过数据**时登记沉降(实测触发源 = 收过数据的数据通道被关闭),纯连接即关/只发不收的连接免沉降,保住 Trap 22 的快速复连时序(:21 @0ms ×50 全通过) */
@@ -3494,6 +3621,9 @@ static void handle_disconnect_event(net_sock_t *p_sock) {
}
/* 正常释放(对端 FIN):断连了结回 EOF(0) */
p_sock->released_abnormal = 0U;
/* N-30(Trap 25):登记本连接对端端口(幽灵态甄别基准,须在状态
* 迁移前记录——remote_port 此刻仍有效,relisten 清零) */
p_sock->last_peer_port = p_sock->remote_port;
/* 状态 → CLOSED */
p_sock->state = NET_SOCK_STATE_CLOSED;
/* 触发断连事件回调 */
@@ -3581,6 +3711,8 @@ static void handle_timeout_event(net_sock_t *p_sock) {
* NET_ERR_CONNRESET 了结(而非 EOF),应用(lftpd STOR)回报 5xx
* 而非误报 226(见 net_pending_tick 断连分支) */
p_sock->released_abnormal = 1U;
/* N-30(Trap 25):异常释放同样登记对端端口(幽灵甄别基准) */
p_sock->last_peer_port = p_sock->remote_port;
/* 状态 → CLOSED */
p_sock->state = NET_SOCK_STATE_CLOSED;
} else if (p_sock->state == NET_SOCK_STATE_LISTENING) {
+10
View File
@@ -179,6 +179,16 @@ typedef struct {
* 连接的释放原因。 */
uint8_t released_abnormal;
/* N-30(2026-10-11 裸机芯片台架实证):本 Socket 上一条被释放连接的**对端端口**。
* 芯片拆链收尾窗口内 Socket 会假报 ESTABLISHED(幽灵态),此时回读的对端端口
* = 旧连接端口;新连接的临时端口几乎必然不同。accept 路径补 accept 与对账②
* 均以此甄别幽灵态(见 net_socket.c Trap 25)。跨 fd 释放保留(幽灵属芯片
* Socket 级,与 fd 复用无关),新连接断开时刷新。 */
uint16_t last_peer_port;
/* N-30:accept 路径"丢 CONNECT 中断补 accept"的去抖计数(连续确认次数,
* 节拍 = accept 轮询 100ms;新监听/补 accept 后清零) */
uint8_t lost_conn_seen;
/* 统计信息 */
uint32_t recv_bytes; /* 累计接收字节数 */
uint32_t send_bytes; /* 累计发送字节数 */
+116
View File
@@ -0,0 +1,116 @@
/*
* 模块名称:FTP 配置([ftp] 段)
* 模块功能:RAM 副本 + 工厂默认 + cfg_file 引擎注册(键表 user/pwd_hash/write)
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-10-11
* 修改记录:
* v1.0 2026-10-11 王建锋 创建初始版本(N-19 FTP 鉴权)
*/
/* 头文件包含区 */
#include <stddef.h>
#include <string.h>
#include "ftp_cfg.h"
#include "cfg_file.h"
/* 私有变量定义区 */
/* 工厂默认(单一来源宏见 ftp_cfg.h;s_cfg 初值与其一致 = load 前也 fail-closed 可用) */
static const ftp_cfg_t s_defaults = {
FTP_CFG_DEF_USER,
FTP_CFG_DEF_HASH,
FTP_CFG_DEF_WRITE,
};
/* RAM 副本(引擎原地更新;注册于 net_cfg_load 内 cfg_file_load 之前) */
static ftp_cfg_t s_cfg = {
FTP_CFG_DEF_USER,
FTP_CFG_DEF_HASH,
FTP_CFG_DEF_WRITE,
};
/* [ftp] 段键表(ftp_cfg_t 布局内偏移) */
static const cfg_key_def_t s_ftp_keys[] = {
{ "user", CFG_VAL_STR, offsetof(ftp_cfg_t, user), FTP_CFG_USER_MAX },
{ "pwd_hash", CFG_VAL_STR, offsetof(ftp_cfg_t, pwd_hash), FTP_CFG_HASH_MAX },
{ "write", CFG_VAL_UINT, offsetof(ftp_cfg_t, write_en), 1u },
};
/* 函数定义区 */
/*
* 函数功能:大小写不敏感字符串相等判断
* 入口参数:p_a - 串 A p_b - 串 B(均可 NULL)
* 返回值:1 - 相等;0 - 不等或任一为 NULL int
* 限定条件:无
*/
static int s_str_ci_eq(const char *p_a, const char *p_b) {
if ((p_a == NULL) || (p_b == NULL)) {
return 0;
}
for (size_t i = 0u; ; i++) {
char a = p_a[i];
char b = p_b[i];
if ((a >= 'A') && (a <= 'Z')) {
a = (char)(a + 32);
}
if ((b >= 'A') && (b <= 'Z')) {
b = (char)(b + 32);
}
if (a != b) {
return 0;
}
if (a == '\0') {
return 1;
}
}
}
/*
* 函数功能:注册 [ftp] 段到 cfg_file 引擎(须在 cfg_file_load 之前调用)
* 入口参数:无
* 返回值:无
* 限定条件:幂等(重复调用只注册一次);由 net_cfg_load 统一触发
*/
void ftp_cfg_register(void) {
static uint8_t s_done;
cfg_section_reg_t reg;
if (s_done != 0u) {
return;
}
(void)memset(&reg, 0, sizeof(reg));
reg.name = "ftp";
reg.p_data = &s_cfg;
reg.p_defaults = &s_defaults;
reg.data_size = sizeof(ftp_cfg_t);
reg.p_keys = s_ftp_keys;
reg.key_count = 3u;
reg.p_cb = NULL; /* 无"立即应用"需求:口令在下次 PASS 时生效 */
(void)cfg_file_register(&reg);
s_done = 1u;
}
/*
* 函数功能:取 [ftp] 段 RAM 副本(load 前后均有效,load 前 = 工厂默认)
* 入口参数:无
* 返回值:配置指针(恒非 NULL,指向静态存储)
* 限定条件:调用方只读
*/
const ftp_cfg_t *ftp_cfg_get(void) {
return &s_cfg;
}
/*
* 函数功能:判断当前口令哈希是否仍为出厂默认(启动告警依据)
* 入口参数:无
* 返回值:1 - 仍是默认;0 - 已改 int
* 限定条件:大小写不敏感比较
*/
int ftp_cfg_is_default_pwd(void) {
return s_str_ci_eq(s_cfg.pwd_hash, FTP_CFG_DEF_HASH);
}
+68
View File
@@ -0,0 +1,68 @@
/*
* 模块名称:FTP 配置([ftp] 段)
* 模块功能:FTP 服务鉴权配置——RAM 副本 + 工厂默认 + cfg_file 引擎注册
* ([ftp] 段:user / pwd_hash / write);消费者(lftpd/lftpd_auth)
* 经 ftp_cfg_get() 只读,配置引擎属主仍为 defaultTask
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
* 作者:王建锋
* 创建日期:2026-10-11
* 修改记录:
* v1.0 2026-10-11 王建锋 创建初始版本(N-19 FTP 鉴权)
*/
#ifndef FTP_CFG_H
#define FTP_CFG_H
#ifdef __cplusplus
extern "C" {
#endif
#include <stdint.h>
#define FTP_CFG_USER_MAX 16u /* 用户名缓冲长度(含 NUL) */
#define FTP_CFG_HASH_MAX 65u /* SHA-256 hex 摘要 64 字符 + NUL */
/* 工厂默认:user=admin、口令=admin(pwd_hash = SHA256("adminadmin"))、write=1。
* write=1 保持既有维护工作流(FTP 推 config.ini);生产配置应改口令并按需 write=0。
* 出厂默认口令在用期间,lftpd_start 每次启动打 INFO 告警锚点(N-19) */
#define FTP_CFG_DEF_USER "admin"
#define FTP_CFG_DEF_HASH "d82494f05d6917ba02f7aaa29689ccb444bb73f20380876cb05d1f37537b7892"
#define FTP_CFG_DEF_WRITE 1u
typedef struct {
char user[FTP_CFG_USER_MAX]; /* 期望用户名(大小写不敏感) */
char pwd_hash[FTP_CFG_HASH_MAX]; /* SHA-256(user‖password) hex(大小写不敏感) */
uint8_t write_en; /* 1 = 允许 STOR/DELE;0 = 只读 */
} ftp_cfg_t;
/*
* 函数功能:向 cfg_file 引擎注册 [ftp] 段(幂等)
* 入口参数:无
* 返回值:无
* 限定条件:cfg_file_load 之前调用(net_cfg_load 内统一编排)
*/
void ftp_cfg_register(void);
/*
* 函数功能:取 [ftp] 段 RAM 副本(只读)
* 入口参数:无
* 返回值:恒非 NULL(静态常驻,load 前初值 = 工厂默认) const ftp_cfg_t *
* 限定条件:无
* 函数说明:defaultTask poll 重载瞬间可能读到撕裂值 → 该次登录失败
* (fail-closed,重试即恢复;配置变更本就罕见)
*/
const ftp_cfg_t *ftp_cfg_get(void);
/*
* 函数功能:判断当前 pwd_hash 是否仍为出厂默认口令哈希
* 入口参数:无
* 返回值:1 - 默认口令在用;0 - 已改 int
* 限定条件:无
*/
int ftp_cfg_is_default_pwd(void);
#ifdef __cplusplus
}
#endif
#endif /* FTP_CFG_H */
+4
View File
@@ -30,6 +30,8 @@
#include "cfg_file.h"
#include "ftp_cfg.h"
/* 深调用链模块(net_cfg_load/save 在 defaultTask 启动链与保存链上):
* 日志行缓冲收到 128 B(默认 192),本模块最长行渲染后 < 128 → 不截断 */
#define DBG_LINE_BUF_MAX 128U
@@ -145,6 +147,8 @@ static bool s_mac_is_zero(const uint8_t *p_mac) {
*/
int net_cfg_load(void) {
s_net_register();
/* N-19:[ftp] 段一并注册(所有受管段必须先注册、后 load,引擎按注册表解析) */
ftp_cfg_register();
/* 旧档 NCFG 迁移已于 2026-10-10 删除(net_cfg_legacy 移除;理由见文件头 v1.3)。
* 若卷上仍残留 /cfg/netcfg.bin:不读、不删、不迁移 → 配置回落工厂默认 */
+15
View File
@@ -912,6 +912,11 @@
<FileType>1</FileType>
<FilePath>..\App\util\task_prof.c</FilePath>
</File>
<File>
<FileName>sha256.c</FileName>
<FileType>1</FileType>
<FilePath>..\App\util\sha256.c</FilePath>
</File>
<File>
<FileName>vol_lock.c</FileName>
<FileType>1</FileType>
@@ -1553,6 +1558,11 @@
<FileType>1</FileType>
<FilePath>..\Drivers\BSP\NETCFG\net_cfg.c</FilePath>
</File>
<File>
<FileName>ftp_cfg.c</FileName>
<FileType>1</FileType>
<FilePath>..\Drivers\BSP\NETCFG\ftp_cfg.c</FilePath>
</File>
<File>
<FileName>nand_ftl.c</FileName>
<FileType>1</FileType>
@@ -1563,6 +1573,11 @@
<FileType>1</FileType>
<FilePath>..\Drivers\BSP\NET\lftpd\lftpd.c</FilePath>
</File>
<File>
<FileName>lftpd_auth.c</FileName>
<FileType>1</FileType>
<FilePath>..\Drivers\BSP\NET\lftpd\lftpd_auth.c</FilePath>
</File>
<File>
<FileName>lftpd_inet.c</FileName>
<FileType>1</FileType>
+25 -5
View File
@@ -57,6 +57,11 @@
/* Private macro -------------------------------------------------------------*/
/* USER CODE BEGIN PM */
/* N-20(2026-10-12):本文件唯一的 DBG_* 调用点是 Error_Handler;其帧经
* MX_*_Init 调用链叠在 MSP 上,而 MSP 仅 1KB(startup Stack_Size 0x400)
* → 行缓冲收紧到 128 B(本文件最长行 = Error_Handler 行 ≈78 B < 128,不截断)。
* 日志成本口径见 dbg_log.h 的 DBG_LINE_BUF_MAX 注释。 */
#define DBG_LINE_BUF_MAX 128U
#define DBG_TAG "[MAIN]"
#include "dbg_log.h"
/* USER CODE END PM */
@@ -274,11 +279,26 @@ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
void Error_Handler(void)
{
/* USER CODE BEGIN Error_Handler_Debug */
/* User can add his own implementation to report the HAL error return state */
__disable_irq();
while (1)
{
}
/* N-20 统一降级(2026-10-12):旧形态 __disable_irq()+while(1) = 单处外设初始化
* 失败即整机挂死,只剩 IWDG 兜底且无任何自检日志。现:打 1 条自检日志 +
* NVIC_SystemReset()(与 4 个 Fault 处理同款自恢复;IWDG 是最后兜底而非唯一
* 出路;复位归因 SFTRSTF → 开机日志 "software (NVIC_SystemReset)")。
* ⚠ 不得 __disable_irq():日志必须能走串口。
* 调用方识别(为何不打地址):本函数无参数、调用点全在 CubeMX 生成区(不可加标签);
* ⚠ __builtin_return_address(0) **不可用**——AC5 前端接受但编成库函数调用,
* MicroLIB 无该符号 → 链接错 L6218E(2026-10-12 台架实证,勿再尝试);
* ARMCC __asm 只有整函数形态且取不到 C 符号、带不动 C 日志(看门狗设计说明 §10.4)。
* → 子系统定位靠启动日志锚点:失败点在"最后一条成功日志"与 main() 的
* MX_*_Init 顺序(GPIO→DMA→USART1→SPI2→SPI1→I2C1→UART4→UART5→USART2→
* USART3→IWDG→ADC1→TIM3→app_main_init)之间。
* 日志路径内核前安全:调用点全部在 osKernelStart 之前,控制台写者未注册
* → con_append 走 dbg_console_write_raw(DWT/计数有界轮询,huart1 未就绪
* 即返回),sys_clock_get_str 未初始化时打占位串——全程无内核 API。
* 日志成本:仅这 1 条(≈90 B Flash;行缓冲已在本文件 PM 区收到 128 B)。
* 重生成存活:函数体在 USER CODE Error_Handler_Debug 区(L2,KeepUserCode
* 逐字保留);另登记 check_cubemx_survival.py 审计项兜底(L3)。 */
DBG_ERROR("boot init failure, resetting (locate MX_*_Init via boot order)");
NVIC_SystemReset();
/* USER CODE END Error_Handler_Debug */
}
#ifdef USE_FULL_ASSERT
-109
View File
@@ -1,109 +0,0 @@
# AGENTS 编写指南——一份好的 AGENTS.md 应该怎么写
> **定位**:回答"AGENTS.md 该写什么、不该写什么、怎么维护"。适用于本仓库 `AGENTS.md` 与任何采用 [AGENTS.md 开放格式](https://agents.md/) 的项目。
> **依据**:官方规范 + 两篇实践指南 + 一个标杆范例 + 本仓库实测(51 KB → 25 KB 瘦身与重构,见 §8)。
> **关联**:文档体系通用约定 → [`文档规范.md`](文档规范.md);人类索引 → [`README.md`](README.md)。
---
## 1. 定位:它是谁的什么
| 文件 | 读者 | 内容 |
|---|---|---|
| `README.md` | 人类贡献者 | 项目介绍、快速上手、贡献指南 |
| `AGENTS.md` | **AI 编码代理** | 构建/测试命令、约定、约束、坑——那些"放 README 会碍事、但 agent 每次都需要"的上下文 |
[官方定义](https://agents.md/):*"Think of it as a README for agents"*、*"anything you'd tell a new teammate belongs here"*。它没有必填字段、纯 Markdown,但**位置即命运**——它坐在每次请求的上下文最前面,写什么就得为什么付费(§3 P3)。
## 2. 一句话定位(写之前先回答)
文件第一行必须让 agent 回答"**这是什么东西、给谁用、跑在哪**"。[aihero 指南](https://www.aihero.dev/a-complete-guide-to-agents-md)的说法:这句话是**锚定后续每个决策的角色提示**——"嵌入式固件"与"Next.js 网站"会让 agent 对同一个"加个日志"请求做出完全不同的权衡。没有这句话,后面所有规则都失去解释框架。
反例:直接以"编码规范"开头——agent 知道规矩,不知道为什么有这些规矩。
## 3. 六条核心原则
| # | 原则 | 为什么 | 反例信号 |
|---|---|---|---|
| P1 | **一句话定位打头**(§2) | 所有规则的锚点 | 文件从规范讲起,产品是什么要翻三节才拼出来 |
| P2 | **宏观先行**:概况 → 目录地图 → 架构边界 → 再是操作规则 | [airflow 范例](https://github.com/apache/airflow/blob/main/AGENTS.md)把 Repository Structure 与 Architecture Boundaries 放在编码规范之前;agent 先知道"代码怎么摆、边界在哪"才不会把算法塞进任务文件 | 规则堆完才出现"架构要点",或根本没有宏观视图 |
| P3 | **指令预算**:模型只能稳定遵循约 150~200 条指令,且**每个 token 每次请求都加载**([Humanlayer](https://www.humanlayer.dev/blog/writing-a-good-claude-md) / aihero) | 膨胀不是"浪费",是**稀释**——不相关的指令会实际降低执行质量 | 文件 >30 KB;同一主题超过 5 条规则 |
| P4 | **渐进披露**:入口只留面包屑,细节进 `docs/` 与 skills;大仓用嵌套 AGENTS.md(就近生效,[官方推荐](https://agents.md/)) | 用到才加载 = 零常驻成本。本仓库把串口助手、模拟器整节移进 skill 后入口各减数 KB 而能力无损 | 入口里出现完整命令表/参数表/FAQ |
| P5 | **描述能力,不写易漂移路径**(aihero):目录地图只到**顶层稳定边界**,细节让 agent 自己 `ls` | 深层路径随重构漂移,agent 会**拒绝相信眼前的真实代码**去服从过期地图 | 入口罗列到三级以下的文件清单 |
| P6 | **每条 = 禁令/事实 + 一句为什么 + 指针**(本仓库实测) | "为什么"让 agent 能外推到未列举的场景;只有禁令会催生新坑的新禁令(回 P3 雪球) | 只有"禁止 X"没有半句原因;或大段叙事讲踩坑史 |
## 4. 推荐结构(模板)
综合[官方常用节](https://agents.md/)(Project overview / Build & test / Code style / Testing / Security)与 airflow 实践:
```
# 项目名
一句话定位(P1)+ 平台/工具链一行 + 最高频命令一行(如"改完必须 build")
## 项目概况 产品现状一句话(含最大缺口)· 数据/调用主线(能力级,P5)
· 顶层目录地图(目录|职责|可改性 三列)· 分层边界
## 架构要点 运行时视图(任务/线程/启动顺序),机器生成表用标记包裹(§5)
## 操作规则 编辑铁律 · 构建与测试命令(含退出码判读)· 提交门禁
## 领域约束 平台/硬件/协议陷阱——每条带为什么(P6),按主题分组
## 索引 按主题查哪份文档(表格;子域知识入口=skills)
```
要点:**构建/测试命令必须给全**(官方:agent 会自动执行文中列出的检查命令并在完成前修复失败);**边界用"必须/禁止"级措辞**——airflow 的 `Never` / `Ask first` 两栏清单是范例。
## 5. 机器可读契约(本仓库经验)
入口文件里凡是被脚本/钩子消费的部分,用**显式标记**声明契约,人改脚本改都撞不上:
- 生成表用锚点包裹:`<!-- TASK_TABLE:BEGIN generated by scripts/gen_task_table.py -->`,由 `--check` 钩子校验与代码一致(同类:`MEM_SNAPSHOT:BEGIN/END`)→ `代码规范/pre-commit使用说明.md`
- **段名是跨文档引用键**:其它文档以「AGENTS.md 平台约束」这类**段名**做引用,改名 = 静默断链。重构时段名保持稳定,删节可以、改名要全库 grep 引用
- 入口自身纳入 `doc-links` 类门禁:文中路径全部可验证存在
## 6. 不该进入口的内容(反模式清单)
| 反模式 | 为什么坏 | 去处 |
|---|---|---|
| **规则雪球**(每次 agent 犯错加一条) | [aihero](https://www.aihero.dev/a-complete-guide-to-agents-md):数月后成 ball of mud,互相矛盾、无人通读 | 同类规则合并成一条带"为什么"的总则 |
| **自动生成全量 dump**(`/init` 一把梭) | 生成器追求"面面俱到"而非克制,等于开局就把预算花光 | 生成后必须人工按 P3 裁剪 |
| **历史叙事**(日期、commit 号、踩坑过程) | 决策依据进 changelog/问题记录;入口只留结论 | `docs/问题记录/`、文档变更记录 |
| **从 docs 抄全表**(引脚表、命令表、FAQ) | 双写必漂移,违反单一事实源 | 一行指针(本仓库「完成即销项」约定) |
| **个人偏好**(某人的缩进口味) | 每次请求全员付费 | 个人级 agent 配置 / editorconfig |
| **过期路径与旧工具名** | 比没有更糟——agent 会服从它 | 定期瘦身(§7) |
## 7. 维护纪律
1. **单一事实源**:同一事实只在一处详述,其余位置用链接指向(本仓库 `docs/文档规范.md` 同此要求)。
2. **瘦身方法论**(本仓库实测,先审计后动刀):全量条目普查 → 逐条核对去处(docs/skills 是否已完整承接)→ 缺口先迁移 → 再删除 → 跑链接/一致性门禁。**禁止**没核对去处就删。
3. **skills 是披露层**:子域工作流(构建工具、串口、模拟器…)整段进 skill,入口索引行留名——目录常驻、全文用到才加载。
4. **改完跑门禁**:链接存在性、生成表一致性、表格格式、编码(本仓库 `check_doc_links.py --all` + `pre-commit`)。
5. **体量红线**:入口超过 ~30 KB 就触发瘦身流程(P3);新增一节前先问"这条是否**每次**都需要在场"。
## 8. 本仓库案例:51 KB → 25 KB
| 步骤 | 动作 | 结果 |
|---|---|---|
| 1 普查 | 脚本统计各节行数/字节数,49 个主题逐条核对 docs 承接情况 | 48/49 主题文档已有完整承接,9 条入口独有 → 先迁移 |
| 2 迁移 | 叙事史/日期/commit 号回写各设计文档;串口、模拟器两节移入 skills | 零信息损失(逐节 diff 验证) |
| 3 删除 | 入口重写:禁令 + 一句为什么 + 指针 | 51,067 → 21,795 B(−57%) |
| 4 重构 | 按 P1/P2 补一句话定位 + 项目概况(数据主线/目录地图/分层边界),平台约束 17 条分 5 组 | 25,306 B,段名与机器标记全保留,门禁全绿 |
**教训**:瘦身最大的风险不是删错,而是**删了唯一副本**——所以"先核对去处"必须先于任何删除;第二大风险是**改段名断引用**——跨文档按段名引用在 Markdown 链接检查里不可见。
## 9. 质量自查清单
- [ ] 第一行能否让新 agent 说出"这是什么、跑在哪、用什么工具链"?
- [ ] 前五节之内能否回答"代码怎么摆、哪些目录不能碰"?
- [ ] 每条规则是否带"为什么"(或本身自明)?
- [ ] 有没有任何一条在 docs/skills 已有更全版本?(有 → 删,留指针)
- [ ] 构建/测试命令是否可直接复制执行、含退出码判读?
- [ ] 生成表/锚点标记是否完好?段名是否未改(或已全库同步引用)?
- [ ] 文件多大?>30 KB 触发瘦身。
- [ ] 最近一次 agent 犯错,是"缺规则"还是"规则太多没执行"?(后者 → 删规则而不是加规则)
## 10. 参考资料
| 来源 | 要点 |
|---|---|
| [agents.md 官方规范](https://agents.md/) | 定位/常用节/嵌套文件/60k+ 项目案例(openai/codex、apache/airflow 等) |
| [A Complete Guide To AGENTS.md](https://www.aihero.dev/a-complete-guide-to-agents-md)(Matt Pocock) | 指令预算、渐进披露、能力>路径、反雪球、skills 披露层 |
| [Writing a good CLAUDE.md](https://www.humanlayer.dev/blog/writing-a-good-claude-md)(Humanlayer) | "instruction budget"概念出处:前沿模型约 150~200 条指令 |
| [apache/airflow AGENTS.md](https://github.com/apache/airflow/blob/main/AGENTS.md) | 标杆结构:Repository Structure / Architecture Boundaries / Ask first–Never / References |
+4 -5
View File
@@ -20,7 +20,6 @@ docs/
├── 测试说明/ 测试流程、模块测试指南、工具说明
├── 代码规范/ 编码规范与静态检查门禁
├── 文档规范.md 本文档体系的规范(命名/链接/门禁/结构)
├── AGENTS编写指南.md 如何写好 AGENTS.md(入口文件的取舍、结构与维护纪律)
├── 内存管理/ RAM 预算、分配策略与优化调研
├── 问题记录/ 模块陷阱台账、专项审计与项目评审报告
├── 环境与部署/ 环境搭建与部署核对清单
@@ -169,7 +168,6 @@ docs/
| 文档 | 内容 |
|---|---|
| [`文档规范.md`](文档规范.md) | **文档体系强制约定**:目录/命名(无空格)、编码换行、链接形态(互链用根相对、图片用目录相对)、`[计划]` 标记、门禁三档、结构要素与常见错误速查 |
| [`AGENTS编写指南.md`](AGENTS编写指南.md) | **如何写好 AGENTS.md**(agent 入口文件):定位与一句话锚点、六条核心原则(指令预算/渐进披露/能力>路径)、推荐结构模板、机器可读契约、反模式清单、瘦身方法论 + 本仓库 51KB→25KB 实测案例与外部标杆调研(agents.md 官方 / airflow / aihero) |
### 7.2 内存管理(**新增任务/缓冲/栈前必读**)
@@ -254,7 +252,7 @@ docs/
| 问题 | 位置 | 说明 |
|---|---|---|
| 指向未实现工具 | `UI_界面制作流程.md`、`UI_自动化测试流程.md` → `tools/ui_mock/` | 该目录**始终不存在**;golden 回归实际落在 `simulator/tests/`(四脚本)。引用待改指,见报告 §6.1 |
| 指向未实现工具 | `UI_界面制作流程.md`、`UI_自动化测试流程.md` → `tools/ui_mock/` | 该目录**始终不存在**;golden 回归实际落在 `simulator/tests/`(四脚本)。引用待改指,见 `docs/问题记录/项目审查与优化建议报告.md` §6.1 |
| 产物缺失 | `内存管理/RAM使用分析与优化方案.md` → `scripts/ram_baseline.tsv` | 方案写了、工具未建(引用已加 `[计划]` 标记) |
| ~~目录名含空格~~ | ~~`UI 设计/`~~ | ✅ 已修复(2026-09-25,D-6):改名为 `UI设计/`,全仓库 67 处引用同步;命名约定见 [`文档规范.md`](文档规范.md) §1 |
| ~~缺一致性门禁~~ | — | ✅ 已落地(2026-09-25/26,D-7 三档齐):`scripts/check_doc_links.py`(存在性)+ `scripts/gen_task_table.py`(数值)+ `scripts/check_mem_numbers.py`(体积),接入 pre-commit `doc-links` / `task-table` / `mem-numbers` 钩子 |
@@ -268,10 +266,11 @@ docs/
| 日期 | 变更 | 说明 |
|---|---|---|
| 2026-10-11 | 新增文档 | 新建 [`AGENTS编写指南.md`](AGENTS编写指南.md)(好的 AGENTS.md 怎么写:六原则/模板/反模式/维护纪律 + 本仓库瘦身重构案例与外部标杆调研);本索引 §1 目录树与 §7.1b 登记;同日 `AGENTS.md` 按该指南完成宏观重构(补项目概况/目录地图、平台约束分组) |
| 2026-10-11 | 功能 | N-19 FTP 鉴权:`/config.ini` 新增 `[ftp]` 段(`user` / `pwd_hash` = SHA-256(user‖password) / `write`)+ 恒定时间比较 + 连续 5 次失败锁 60 s + 未登录命令门;出厂默认 admin/admin,默认口令在用有启动告警。新增 `App/util/sha256.*`、`Drivers/BSP/NETCFG/ftp_cfg.*`、`Drivers/BSP/NET/lftpd/lftpd_auth.*`;主机侧单测 `test/host/sha256/`(13 例)+ `test/host/ftp_auth/`(36 例),pre-commit 钩子 26→28;台架脚本 `test/host/ftp_auth_bench.py`。文档同步:`docs/子系统设计/网络配置说明.md`、`docs/架构设计/STM32F4-Base存储架构说明.md`、`docs/代码规范/pre-commit使用说明.md`、`docs/产品需求/产品需求设计文档.md` |
| 2026-10-11 | 文档调整 | `AGENTS编写指南.md` 新建后**同日删除**(要点已沉淀入 `AGENTS.md` 宏观重构与本索引变更记录,指南类内容不单独成文);撤销 §1 目录树与 §7.1b 登记;同日 `AGENTS.md` 完成宏观重构(补项目概况/目录地图、平台约束分组、边界块) |
| 2026-10-10 | 新增子系统设计 | 新建 `docs/子系统设计/ADC采样节拍设计说明.md`(ADC 采集链路 1 kHz 硬件定时器触发的 CubeMX 配置规格与代码改造清单,B1 遗留项规划);本索引 §5.1 补登记 |
| 2026-10-10 | 新增算法专题 | 新建 `docs/算法/继电保护功能复现规划.md`(保护链路复现总规划);本索引 §1 目录树补 `算法/`、§5 拆为 5.1 功能子系统 / 5.2 算法专题并登记 `算法/` 两篇文档 |
| 2026-10-05 | 新增产品需求文档 | 新建 `docs/产品需求/产品需求设计文档.md`(面向产品的需求基线);本索引新增 §3 产品需求、§2 推荐阅读顺序置顶、§1 目录树补 `产品需求/`;后续章节号顺延(架构设计 §4 … 变更记录 §12) |
| 2026-10-05 | 新增产品需求文档 | 新建 `docs/产品需求/产品需求设计文档.md`(面向产品的需求基线);本索引新增 §3 产品需求、§2 推荐阅读顺序置顶、§1 目录树补 `产品需求/`;后续章节号顺延(本文 §4 … 本文 §12) |
| 2026-09-25 | 目录重组 + 索引建立 | 顶层 16 篇散落 md 归入 5 个新目录(`架构设计/` `子系统设计/` `网络/` `审查记录/` `环境与部署/`);全仓库 90 处引用同步;新建本索引(D-5) |
| 2026-09-25 | D-6 目录去空格 | `docs/UI 设计/` → `docs/UI设计/`;全仓库 67 处引用同步(含 URL 编码形态 `UI%20设计`);命名约定写入 §10 |
| 2026-09-25 | D-7 文档-代码一致性门禁 | 新建 `scripts/check_doc_links.py`(链接存在性,含行内代码豁免/就近状态词/`[计划]` 标记)、`scripts/gen_task_table.py`(任务表由 `Src/freertos.c` 生成/校验,锚点 `TASK_TABLE:BEGIN/END`);接入 pre-commit `doc-links`/`task-table` 钩子;全库 66 文档 0 失效 |
+11 -11
View File
@@ -1,7 +1,7 @@
# UI 界面制作流程(参考图 → LVGL)
> **用途**:规范"参考图 → 设计规格 → LVGL 代码 → 双端验证"的完整管线,规定每一步的**动作、产物、放行条件(gate)**,保证过程可重复、可交接。
> **适用**:所有 UI 新界面 / 界面大改;小改动走 §10 快速通道。
> **适用**:所有 UI 新界面 / 界面大改;小改动走「小改动快速通道」一节(见文末第十节)。
> **配套文档**:`UI_设计架构.md`(系统如何工作)、`菜单页面设计.md`(菜单/导航行为与数据流设计)、`LVGL模拟器部署与使用说明.md`(模拟器)、`UI_中文字体生成与配置.md`(字体生成)、`UI_自动化测试流程.md`(P2/P3 回归门禁的可执行版本:脚本 + 池水位常量 + 黄金参考更新)。
> **首个实例**:界面 01 主界面(根页),规格 `mockups/01_主界面_说明.md`(本文所有示例数字均取自该试点)。
@@ -24,7 +24,7 @@ flowchart TD
P3 -- "Gate: @build 0 错 0 警 + @test PASS + 上机目检通过" --> P4(["P4 归档<br/>→ 变更记录/台账 + 随代码提交(代码+字体+字集+规格+文档)"])
```
> 小改动(文案/间距/纯交互/字体重生成)走 §10 快速通道,无需全流程。
> 小改动(文案/间距/纯交互/字体重生成)走「小改动快速通道」一节(见文末第十节),无需全流程。
### 1.1 各阶段职责分工
@@ -160,7 +160,7 @@ flowchart TD
| 黑/白背景 | `lv_style_set_bg_color` + `lv_style_set_bg_opa` | 1bpp 仅纯黑/纯白 |
| 文本 | `lv_label_create` + `lv_label_set_text_static` | 静态字符串(C5) |
| 文字对齐 | 标签定位 / `lv_label_set_long_mode` | 默认左对齐(见 §3.6 推论) |
| 选中反色 | `LV_STATE_CHECKED` 样式(bg/文字色互换) | 条目对象**自身**带 CHECKED(防白底白字,架构 §5.5) |
| 选中反色 | `LV_STATE_CHECKED` 样式(bg/文字色互换) | 条目对象**自身**带 CHECKED(防白底白字,`UI_设计架构.md` §5.5) |
| 隐藏状态 | `LV_OBJ_FLAG_HIDDEN` | 页面/状态切换 |
| 圆角/阴影/透明/渐变 | **禁止**(C8) | 规格里就不该有 |
@@ -175,8 +175,8 @@ flowchart TD
### 4.3 翻译纪律
1. **逐参数翻译**:代码中每个 px 值必须能在规格"布局参数表"中找到出处;禁止"看着办"
2. 样式沿用现有 `s_style_flat` 模式(静态样式、一次性初始化、后加优先覆盖主题)
3. 焦点对象恒为**页面容器**;条目不入输入组(架构 §5.4)
4. 容器**永不删除**(indev 释放事件 use-after-free,架构 §5.2);只建/毁框体(行自绘、无对象)
3. 焦点对象恒为**页面容器**;条目不入输入组(`UI_设计架构.md` §5.4)
4. 容器**永不删除**(indev 释放事件 use-after-free,`UI_设计架构.md` §5.2);只建/毁框体(行自绘、无对象)
5. 编码纪律(C6):Edit 工具精确替换;文件保持 UTF-8 BOM 不变
6. 注释按代码规范 V1.0:文件头模块块 + 函数头块(功能/参数/返回/限定/说明)
@@ -241,7 +241,7 @@ flowchart TD
### 5.4 Gate(进入 P3)
- [ ] 模拟器出图 `shot.png` 与规格参数表逐项一致、与参考图结构一致(§5.2,忽略 5.3)
- [ ] 导航行为符合键位语义表(架构 §3.4)
- [ ] 导航行为符合键位语义表(`UI_设计架构.md` §3.4)
- [ ] 自动化回归 PASS:`check_menu_rows.py` + `check_content_pages.py`(`UI_自动化测试流程.md`)
---
@@ -307,17 +307,17 @@ flowchart TD
| 用了字体子集外的字 | 该字显示空白 | P0 字符集核对 + P1 字体重生成 |
| 字体缺字形(如宋体无 ▶) | 该字符显示成方框 □ | `make_font.py` 已内置 `merge_arrow_glyph()` 自动补回;其余缺字形同法(从有该字的字体提取位图合并,§4.4) |
| 新 .c 只注册了 Keil(或只改了 CMake) | 单端编译缺函数/静默缺失 | 双端注册(C9) |
| 模拟器主循环调 `SDL_PollEvent` | 键盘全部无响应 | 事件只由 LVGL SDL 驱动内部轮询(架构 §5.7) |
| 选中行"白底白字" | 选中文字不可见 | 历史坑(2026-09-02):子 label 自身 card 样式总命中、继承不到父选中色;现行行自绘(绘制时给色、无样式继承)已根除(架构 §5.5) |
| 按键处理里删除焦点容器 | use-after-free 崩溃 | 容器持久不删(架构 §5.2) |
| 模拟器主循环调 `SDL_PollEvent` | 键盘全部无响应 | 事件只由 LVGL SDL 驱动内部轮询(`UI_设计架构.md` §5.7) |
| 选中行"白底白字" | 选中文字不可见 | 历史坑(2026-09-02):子 label 自身 card 样式总命中、继承不到父选中色;现行行自绘(绘制时给色、无样式继承)已根除(`UI_设计架构.md` §5.5) |
| 按键处理里删除焦点容器 | use-after-free 崩溃 | 容器持久不删(`UI_设计架构.md` §5.2) |
| LVGL 池爆 40KB | 对象创建失败/画面残缺 | §4.5 预算 + 按需建毁 |
| 极性配反 | 黑屏/白屏/不可读 | 模拟器 mono 主题 `dark_mode` 参数 + MCU 端背景色一致 |
| 步进宽接近/超出格宽的标签居中 | 亚像素错位/不对称 | 左对齐 + 字体侧边距(§3.6) |
| 关闭了 `LV_DRAW_SW_COMPLEX`(省代码) | `lv_line` 斜线不绘制(如主界面顶部"浅 V"两腿消失,只剩水平线) | `LV_DRAW_SW_COMPLEX` 两端显式 =1;`LV_DRAW_SW_CIRCLE_CACHE_SIZE` 须非零(默认 4,否则零长数组 C2466)(架构 §关键约束 7) |
| 关闭了 `LV_DRAW_SW_COMPLEX`(省代码) | `lv_line` 斜线不绘制(如主界面顶部"浅 V"两腿消失,只剩水平线) | `LV_DRAW_SW_COMPLEX` 两端显式 =1;`LV_DRAW_SW_CIRCLE_CACHE_SIZE` 须非零(默认 4,否则零长数组 C2466)(`UI_设计架构.md` §关键约束 7) |
| 斜线用细 `line_width`(如 2px) | 1bpp 阈值吃掉斜线边缘,腿偏细、比设计稿细 | 斜线加粗(腿 3px),或按段拆 `lv_line` 分粗细(如"浅 V"两腿 3 / 水平线 2) |
| 规格引入翻译不出来的效果 | P1 翻译不出来 | 1bpp 禁区(C8),P0 阶段就拦下 |
| UI 源文件(UTF-8 BOM)里 `snprintf` 格式串含中文 | 模拟器 cl.exe 报 C4819(代码页 936 误读 UTF-8 字节) | 格式化输出用 `lv_snprintf`(LVGL 自带,双端一致),勿用标准库 `snprintf` |
| 常驻样式用动态 `lv_style_t` + set_prop | LVGL 池永久占用:首次 set_prop 分配 `values_and_props` 且永不释放(x64 实测 88 B/个) | 常驻样式一律常量样式:`LV_STYLE_CONST_INIT(var, props[])` + `lv_style_const_prop_t[]`(属性存 Flash、零池分配,见架构 §5.6) |
| 常驻样式用动态 `lv_style_t` + set_prop | LVGL 池永久占用:首次 set_prop 分配 `values_and_props` 且永不释放(x64 实测 88 B/个) | 常驻样式一律常量样式:`LV_STYLE_CONST_INIT(var, props[])` + `lv_style_const_prop_t[]`(属性存 Flash、零池分配,见`UI_设计架构.md` §5.6) |
---
+7 -20
View File
@@ -465,7 +465,7 @@ sequenceDiagram
| 服务 | 协议 / 端口 | 用途 | 核对方法 |
|---|---|---|---|
| FTP | TCP **21** | 文件上传/下载(根目录 `/`,NAND 存储) | 任意 FTP 客户端列目录/读写 |
| FTP | TCP **21** | 文件上传/下载(根目录 `/`,NAND 存储) | 登录后任意 FTP 客户端列目录/读写(鉴权见 §7.6) |
| 墙钟对时 | TCP **8888** | PC 主动推送时间到设备 | 对时后核对设备时间 / 日志时间戳 |
| 远程显示(RemoDispBus) | TCP **7003** | 远端看图(160×160 1bpp)+ 远端导航按键 | DTU-RemoteLCD 或任意 TCP 客户端连 7003 收帧 |
@@ -512,6 +512,7 @@ sequenceDiagram
- **实现**:lftpd(轻量 FTP 服务器),root = `/`(FatFS 卷根),端口 21,**单客户端**。
- **支持命令**:登录 / TYPE I / PASV / STOR / RETR / LIST / DELE / QUIT。
- **鉴权**(已鉴权 · 2026-10-11,N-19):口令登录(`[ftp]` 段 `user` / `pwd_hash` = SHA-256(user‖password) / `write`);未登录仅 USER/PASS/QUIT/FEAT/SYST/NOOP 放行,其余一律 530;`write=0` 可禁 STOR/DELE;连续 5 次失败锁 60 s;出厂默认 `admin`/`admin`,默认口令在用有启动告警。口径详见 `docs/子系统设计/网络配置说明.md` §3.4。
- **启动门**:`ftpTask` 必须等 `NET_READY` **且** `STORAGE_READY`(两位 `NoClear`)才 `lftpd_start`。
- **注意**:同一时刻只服务 1 个会话,旧会话异常断开后约 30 s 恢复监听;测试脚本须内置连接重试,**不要用裸 TCP 工具探测 21 端口**。
- **实测**:128 KB 上传下载逐字节一致(约 49/60 KB/s)。
@@ -678,7 +679,7 @@ cfg-file-unit-test、test-suite-switch、gitlint。
| 项 | 现状 |
|---|---|
| FTP 鉴权 | **无**(维持现状,非本期目标) |
| FTP 鉴权 | **已鉴权**(2026-10-11,N-19):口令登录(`[ftp]` 段存 SHA-256 摘要,不存明文)+ 连续 5 次失败锁 60 s + `write=0` 可选只读;出厂默认 `admin`/`admin`,默认口令在用有启动告警。边界:FTP 明文传口令,挡未授权访问、不挡网络嗅探(详见 `docs/子系统设计/网络配置说明.md` §3.4) |
| UI 菜单权限 | 受保护叶子(行尾 `#P`)先经密码页;密码**仅编译期写入**(查看 `000` / 修改 `123`),不落 Flash、无运行期设置页 |
| 远程显示 | 无鉴权,单连接;现场同网段部署 |
@@ -778,24 +779,10 @@ cfg-file-unit-test、test-suite-switch、gitlint。
### 9.2 阶段路线图
```text
【阶段 B 功能】 B0 产品定义确认 ✅ ─┬─ B3 E-59 门禁固化(并行)
(当前最高优先) └─ E-58 原子快照 → B1 ADC ✅ → B2 RS485
↓
【阶段 C 结构】 C1 pending 二期 ─ C3 日志管线 ─ C4 栈(S-1) ─ C5 收口 ─ C6 USART1 TX-only
↓
【阶段 D 常态】 D2 N-3 L5 基线重锁 / D5 N-16 负向台架 / D6 N-17 探针窗口
```
| 阶段 | 关键任务 | 前置 |
|---|---|---|
| **B0** | 产品定义确认 ✅(2026-10-10):持续采集 / 1000 Hz·20 点周波 / 上行 IEC 60870-5-104(2404)/ RS-485 对下 ModBus RTU;详见 `docs/算法/继电保护功能复现规划.md` §1.5 | — |
| **B1** | ADC 采集链路实现(周期 CONVST + `adc_task` 消费;**先修原子快照 API**) | ✅ 已实现并 `@test` 闭环(2026-10-10) |
| **B2** | RS-485 采集链路实现(双缓冲 + 序号 + DE 收尾 + 协议状态机) | B0 + E-59 门禁 |
| **B3** | E-59 门禁固化入 `AGENTS.md`(接消费者前先修缺陷 + 栈预算) | — |
| **C1** | pending 二期(ACCEPT + SEND pending 化) | — |
| **C4** | `defaultTask` 栈余量(**当前 10%,需尽快处理**):4096→4608 或 GC 链局部削减 | — |
| **C6** | USART1 TX-only 结构根治(灭 ORE 僵尸机制) | 单开一轮 |
> **顺序唯一权威 → [`../待处理内容.md`](../待处理内容.md) §2(统一推进顺序,波次 W1~W8)**。
> 本节自 2026-10-11 起不再维护路线图(原阶段图删除,避免与登记簿漂移;原 C4"defaultTask 栈余量 10%"
> 已于 2026-10-10 削链闭环销项,见 `../内存管理/RAM使用分析与优化方案.md`)。
> 功能缺口口径见 §9.1;B0 产品定义结论见 `../算法/继电保护功能复现规划.md` §1.5。
### 9.3 未接线开关说明
+39 -2
View File
@@ -24,7 +24,8 @@ git commit
├─ lv-conf-sync 两份 lv_conf.h 特性子集一致性(比对生效值)
├─ cubemx-survival CubeMX 重生成存活审计(易失定制清单 + 基线漂移)
├─ watchdog-unit-test 看门狗判定状态机主机侧单测(L0.5)
└─ test-suite-switch 测试套件开关=产品构建(防测试固件静默提交)
├─ test-suite-switch 测试套件开关=产品构建(防测试固件静默提交)
└─ registration-check 双端注册反向校验(uvprojx + CMakeLists,N-23)
└─ commit-msg(.git/hooks/commit-msg)
└─ gitlint 校验 commit message 格式
```
@@ -48,6 +49,7 @@ git commit
| scripts/check_mem_numbers.py | 随仓库提交 | 内存/Flash 数字一致性(纯 Python,无依赖) |
| scripts/check_cubemx_survival.py | 随仓库提交 | CubeMX 重生成存活审计(纯 Python,无依赖) |
| scripts/check_test_suite.py | 随仓库提交 | 测试套件开关门禁(纯 Python,无依赖) |
| scripts/check_registration.py | 随仓库提交 | 双端注册反向校验(纯 Python,无依赖) |
> **PATH 要求**:cppcheck 安装后需在 PATH 中可找到(winget 自动写入 User PATH,但当前终端不会立即生效,需**重开终端**);装在非 PATH 位置时可用 `DTU_CPPCHECK` 环境变量指向 `cppcheck.exe` 完整路径(显式设置但无效 → 硬报错,不静默回退)。
@@ -334,6 +336,41 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法
| **触发范围** | `Lib/lv_conf.h`、**`simulator/lv_conf.h`** 或脚本自身变动 → 两端任一改动都会跑。⚠ 早期版本只能由 MCU 端触发,因为顶层 `exclude` 整目录挡掉了 `simulator/`(已删,见 §5);pre-commit 4.x 又移除了 `always: true`(实测对 `local` 钩子只告警并跳过),所以没有"总是跑"的兜底写法——**必须让文件模式覆盖两端** |
| **首次落地实测** | 两端共清掉 **26 个 9.3 不存在的选项**(LVGL 8 / 9.4 模板残留)、3 处同文件重复定义,并把 10 项 MCU 端靠默认值隐式生效的开关显式固化为 0(含默认 =1 的 `LV_USE_OBSERVER` / `LV_USE_DRAW_PXP` / `LV_USE_IMAGEBUTTON`)。本次清理对镜像体积**中性**(那些 TU 本就不在 uvprojx,或编成 0 字节空对象),价值在防漂移与可读性 |
### 2.16 sha256-unit-test — SHA-256 主机侧单测(2026-10-11 加,N-19)
| 项 | 说明 |
|---|---|
| **触发条件** | `App/util/sha256.*`、`test/host/sha256/**` 或 `test/host/common/unit_test_harness.py` 变动 |
| **处理方式** | **阻止提交**(单测失败即拦截) |
| **被测对象** | `App/util/sha256.c/h`(FIPS 180-4 实现,零堆分配)——FTP `pwd_hash` 校验的地基 |
| **用例** | 13 例:FIPS 180-4 标准向量(空串 / `abc` / 56 B 补位跨界 / 1 MB)+ `hex_eq` 非法输入 |
| **为什么需要** | 哈希算错是"正确口令被拒 / 错误口令放行"的**静默语义错误**,板端只见 `530`/`230` 看不出根因;主机侧毫秒级钉死。零平台依赖,无桩 |
| **运行器** | `test/host/sha256/run_unit_test.py`(MSVC `cl.exe`,L0.5) |
### 2.17 ftp-auth-unit-test — FTP 鉴权状态机主机侧单测(2026-10-11 加,N-19)
| 项 | 说明 |
|---|---|
| **触发条件** | `Drivers/BSP/NET/lftpd/lftpd_auth.*`、`App/util/sha256.*`、`test/host/ftp_auth/**` 或 `test/host/common/unit_test_harness.py` 变动 |
| **处理方式** | **阻止提交**(单测失败即拦截) |
| **被测对象** | `Drivers/BSP/NET/lftpd/lftpd_auth.c/h`(纯函数鉴权状态机,tick 由调用方参数注入,零平台依赖) |
| **用例** | 36 例:连续 5 败锁定 / 60 s 解锁 / 成功清零 / tick 32 位回绕 |
| **为什么需要** | 锁定 60 s / 解锁 / 清零 / 回绕是**防口令爆破契约核心**——板端每验一条要真等 60 s、回绕要跑满 49.7 天;纯函数设计(tick 注入)就是为让这些分支主机侧毫秒级穷举 |
| **运行器** | `test/host/ftp_auth/run_unit_test.py`(MSVC `cl.exe`,L0.5) |
### 2.18 registration-check — 双端注册反向校验(2026-10-12 加,N-23)
| 项 | 说明 |
|---|---|
| **触发条件** | 任意 `.c` 文件 + `*.uvprojx` + `CMakeLists.txt` + 脚本自身变动(pre-commit 4.x 无 `always: true`,`files:` 必须覆盖全部触发面) |
| **处理方式** | **阻止提交**(存在**新**缺口或幽灵注册即拦截);存量缺口走白名单只 `[WARN]` 不拦 |
| **检查对象** | 扫描 `App/`、`Drivers/BSP/`、`Src/` 下全部 `.c`(排除第三方),对照两份注册表:`MDK-ARM/STM32F407-Demo.uvprojx` 的 `<FilePath>` + `simulator/CMakeLists.txt` 的显式源列表(先展开 `${NETCFG_DIR}` 等变量) |
| **三类缺口** | ① 存在但未进 uvprojx(固件不编译)② 存在但未进 CMakeLists(模拟器不编译,**仅当本应进模拟器时**才算)③ 注册了但文件不存在(**幽灵注册**,改名/删除后残留) |
| **豁免机制** | MCU-only 文件不进模拟器是**正确**的(模拟器另有 `*_sim.c` 替身或无对应)。豁免用**显式清单 + 每行理由**(命名模式 `*_mcu.c` + 个别点名),**不整目录一刀切**(N-23 教训正是目录级盲区);唯一目录级豁免 `Src/`(CubeMX 生成,MCU 专属)→ 见脚本 `SIM_EXEMPT` |
| **存量白名单** | `KNOWN_GAPS` 登记 N-23 存量缺口(`App/task/rs485_task.c`、`Drivers/BSP/NET/lftpd/lftpd_log.c`,Lead 处置中)→ 命中打 `[WARN]` 不拦,只对新缺口 FAIL,故钩子可立即上线不拦现有提交;`--strict` 关白名单供审计 |
| **为什么需要** | 新 `.c` 必须手动注册两处才会被编译;历史上 `rs485_task.c`/`lftpd_log.c` **两处都没注册** = 写了却从不参与任何一端编译的**死代码**,误导读者以为功能已生效(出处审查报告 N-06 残余) |
| **运行器** | `scripts/check_registration.py`(纯标准库;退出码 0=一致 / 1=有缺口 / 2=环境错误) |
---
## 3. 日常工作流
@@ -461,7 +498,7 @@ $env:PATH = [Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [Env
| 文件 | 位置 | 说明 |
|---|---|---|
| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**26 个钩子**,2026-10-11 清点;本文 §2 详解非单测类,主机侧单测 10 个见 AGENTS.md 门禁条) |
| `.pre-commit-config.yaml` | 仓库根 | 钩子配置(**29 个钩子**,2026-10-12 清点;本文 §2 详解非单测类——N-19 新增的两个单测钩子见 §2.16/§2.17,N-23 双端注册反向校验见 §2.18,其余主机侧单测见 AGENTS.md 门禁条;主机侧单测共 12 个) |
| `.gitlint` | 仓库根 | gitlint 规则(宽松,兼容中文) |
| `.cubemx_survival_baseline.json` | 仓库根 | CubeMX 易失定制基线(哈希;`--write` 重建) |
| `scripts/check_mojibake.py` | scripts/ | U+FFFD 检测脚本 |
@@ -36,10 +36,10 @@
<!-- MEM_SNAPSHOT:BEGIN generated by scripts/check_mem_numbers.py -->
| 区域 | 用量 | 容量 | 占比 |
|---|---|---|---|
| ER_IROM1 | 210,148 B (205.22 KB) | 1,048,576 B (1024 KB) | 20.0% |
| RW_IRAM1 | 91,536 B (89.39 KB) | 131,072 B (128 KB) | 69.8% |
| ER_IROM1 | 213,256 B (208.26 KB) | 1,048,576 B (1024 KB) | 20.3% |
| RW_IRAM1 | 94,768 B (92.55 KB) | 131,072 B (128 KB) | 72.3% |
| RW_CCM | 49,184 B (48.03 KB) | 65,280 B (64 KB) | 75.3% |
| **Flash (ROM 合计)** | **211,024 B (206.1 KB)** | 1,048,576 B (1024 KB) | **20.1%** |
| **Flash (ROM 合计)** | **214,224 B (209.2 KB)** | 1,048,576 B (1024 KB) | **20.4%** |
<!-- MEM_SNAPSHOT:END -->
各区的容量与常量来源:
@@ -203,7 +203,7 @@
| 堆测量 | `xPortGetFreeHeapSize()` / `xPortGetTotalHeapSize()` 随时可查;heap_4 支持 free 但有碎片化,需对比"理论余量 vs 实测 free"观察碎片 | FreeRTOS 内核源码 `heap_4.c`(工程内);[FreeRTOS 文档](https://docs.freertos.org/) |
| 运行时统计 | `vTaskGetRunTimeStats` 需 `configGENERATE_RUN_TIME_STATS=1` + `portCONFIGURE_TIMERS_FOR_RUN_TIME_STATS`(占用一个硬件定时器),给出每任务 CPU 占比 | 本工程内核 `task.h:1694-1756` 注释 |
| LVGL 缓冲 | 官方文档:PARTIAL 渲染模式"buffers can be smaller than the display to save RAM,**至少 1/10 屏**";单色 I1 格式缓冲 = `(W×H/8)+8`;双缓冲可并行渲染/搬运 | LVGL v9.2 文档 [Display interface](https://github.com/lvgl/lvgl/blob/release/v9.2/docs/porting/display.rst)(lvgl.io/docs/porting/display 的源文件) |
| STM32F4 CCM | F407 含 64KB CCM @0x10000000;**只能由 CPU 访问,DMA 不可达**,无 cache/ECC;适合纯 CPU 读写的大缓冲 | ST RM0090(STM32F405/407/415/417/42x 参考手册)§3.1.1 |
| STM32F4 CCM | F407 含 64KB CCM @0x10000000;**只能由 CPU 访问,DMA 不可达**,无 cache/ECC;适合纯 CPU 读写的大缓冲 | `docs/芯片手册/STM32F4xx参考手册_V18英文版_1-1000.md` §3.1(RM0090 原文该小节为 §3.1.1 CCM,本仓摘录只到 §3.1) |
| FatFs | `FF_FS_TINY=1`:FIL 结构体去掉 `buf[FF_MAX_SS]` 私有扇区窗,改用卷内共享 9 字节窗口(FAT32),每打开文件省 512B | 本工程 `Lib/FatFs/ff.h` FIL 结构注释、`Lib/FatFs/ffconf.h:32` |
| 静态占用测量 | Keil map 文件 "Image component sizes" 给出每个 .o 的 RO/RW/ZI 精确字节数;`fromelf --image_info xxx.axf` 可输出各 Region 用量 | 本工程 `STM32F407-Demo.map` "Image component sizes" 表(行号随每次重建变化,勿引用行号) |
@@ -344,7 +344,7 @@ void vApplicationMallocFailedHook(void)
- 帧宽榜直接点名宽度大户(自身帧):`s_append` 704 B(FIL 512 + 记录 64 + 时间戳)、`StartDefaultTask` 560 B(探针 `FIL __chk`)、`cfg_store_read`/`cfg_store_write_atomic` 576 B、LVGL 绘制 624/560/400 B——与 §1.4 的整改项对得上。**lftpd 三项已于 2026-09-21 静态化(§1.4),已退出帧宽榜前 15**(原 `handle_control_channel` 1104 B、`receive_file`/`send_file` 1088 B、`send_response` 800 B)。
- **静态分析的天然盲区**:递归/互递归(LVGL widget 树、`layout_update_core`、`apply_theme` 等)与间接调用只能按 "Cycle" 近似,armlink 明确标注 `+ Unknown(Cycles, Untraceable Function Pointers)`。因此 uiTask 的 912 B 是**下限**(实测 3824 B),必须靠运行时水位。
- testTask 在产品构建里只是 `osDelay` 空循环(168 B);2048 words 是给**存储套件构建**(`TEST_SUITE_STORAGE`)预留的,测套件时应重跑本脚本看该构建的静态值。
- **测 ftpTask 的复现配方**:设备就绪后跑 `& '.venv\Scripts\python.exe' test/host/ftp_speed_test.py --size 128`(STOR 128KB + RETR + 逐字节比对),再按上面命令叠加串口日志出报告。**不要用裸 TCP 工具探测 21 端口**——CH395F 单客户端复用,探测会占掉唯一会话(见 FTP 指南 §8.9)。
- **测 ftpTask 的复现配方**:设备就绪后跑 `& '.venv\Scripts\python.exe' test/host/ftp_speed_test.py --size 128`(STOR 128KB + RETR + 逐字节比对),再按上面命令叠加串口日志出报告。**不要用裸 TCP 工具探测 21 端口**——CH395F 单客户端复用,探测会占掉唯一会话(见 `docs/测试说明/FTP_Test_Guide.md` §8.9)。
#### 2026-10-10 复测:**不抬栈**把 defaultTask 从 FAIL 拉回 PASS(S-1 销项)
@@ -24,7 +24,7 @@
3. CCM 存在性已用 `ST-LINK_CLI -r32 0x10000000` 本机实测可读写;**克隆片可能缺 SRAM3,换料必须重测并回退布局**。
4. 工程不进入 Stop/Standby 低功耗模式,两区域掉电保留行为无差异,不构成分配判据。
> 勘误(原《STM32F4-Base 内存使用分析报告》2026-09-11 快照,随主文档合并保留于此):该报告内存地图中 "SRAM3 64KB (0x2001_C000)" 为笔误——F407 的 SRAM3 即 CCM,位于 `0x10000000`(AXI);AHB2 侧只有 SRAM1(112KB)+SRAM2(16KB)。**以本文为准**(主文档 §8.4 与 `内存布局_RAM.svg` 均按此口径)。
> 勘误(原《STM32F4-Base 内存使用分析报告》2026-09-11 快照,随主文档合并保留于此):该报告内存地图中 "SRAM3 64KB (0x2001_C000)" 为笔误——F407 的 SRAM3 即 CCM,位于 `0x10000000`(AXI);AHB2 侧只有 SRAM1(112KB)+SRAM2(16KB)。**以本文为准**(`RAM使用分析与优化方案.md` §8.4 与 `内存布局_RAM.svg` 均按此口径)。
## 3. 全板 DMA 使用方清点(DMA 缓冲必须为 SRAM 静态)
@@ -71,7 +71,7 @@ CCM 64KB 只能容纳 {FreeRTOS 堆 48KB} 与 {LVGL 集合 ~52.5KB} 之一(两
当时提出的两条判据经核实**均为事实但不具区分力**:
- "UI 不用 DMA" ✓ 属实(§3 路径核实:DMA 只寻址驱动静态缓冲)→ 堆同样不用 DMA,两候选在 DMA 轴**等价**
- "图形不紧迫速度" ✗ 无效判据(§2.1:M4 无 cache,CCM 与主 SRAM 对 CPU 同速)→ 两候选在速度轴**等价**
- "图形不紧迫速度" ✗ 无效判据(§2:M4 无 cache,CCM 与主 SRAM 对 CPU 同速)→ 两候选在速度轴**等价**
真正可判别的轴是**"该内存损坏后的后果" × "DMA 不可达带来的隔离收益"**:
@@ -148,4 +148,4 @@ LVGL 集合明细(均在 SRAM):LVGL 池 40,960B(`lv_mem_core_builtin.o`
已知未决风险:
- `.ioc` 未与 `freertos.c` 的栈值直改同步——CubeMX 重新生成会覆盖栈值(已注释标记,未修)
- CCM 存在性依赖芯片批次(克隆片可能缺 SRAM3,见 §2.3)
- CCM 存在性依赖芯片批次(克隆片可能缺 SRAM3,见 §2)
@@ -2,7 +2,7 @@
> 版本 v1.1 日期 2026-09-21 作者 AI 辅助调研
> 数据来源:`data/ram_report_20260921.md`(.map 静态占用)+ 逐行代码核验(`nand_ftl.c` / `gd5f2gq5ue.c` / `Lib/dhara` / `Lib/FatFs/ffconf.h` / `lftpd*`)
> 定位:`RAM使用分析与优化方案.md` 的存储专项补充;优化项编号 P0/P1/P2,与总方案 §6 的 S 序号在"规划"一节对齐
> 定位:`RAM使用分析与优化方案.md` 的存储专项补充;优化项编号 P0/P1/P2,与 `docs/内存管理/RAM使用分析与优化方案.md` §6 的 S 序号在"规划"一节对齐
> v1.1 清理(2026-09-21 同日):移除已落地的 P0-1 BBT 页缓冲收缩(−1,776B,commit `be1a30a`)与编程回读校验整体移除的实施记录(详见 GD5F Trap 05 / 错误码传播审计),以及过期逐模块基线表;仅保留未实施项与回归要求
## 1. 现状约束(不变,是下述全部方案的依据)
@@ -28,7 +28,7 @@
**明确不做**:移除 dhara(丢掉电一致性与坏块重映射,代价不对称);journal checkpoint 分片(违反 dhara 协议);调 `gc_ratio`(无 RAM 收益);`FF_FS_REENTRANT=0`(defaultTask+ftpTask 并发访问 FatFs 必需)。
## 3. 分阶段规划(与总方案 §6 S 序号对齐)
## 3. 分阶段规划(与 `RAM使用分析与优化方案.md` §6 S 序号对齐)
| 阶段 | 内容 | 收益 | 前置 |
|---|---|---:|---|
@@ -223,7 +223,7 @@ t=1000 µs 下一个 TIM6 更新事件
- 本配置已为此预留:**1 µs 分辨率** + **ARR 预装载使能** → 运行期
`__HAL_TIM_SET_AUTORELOAD(&htim6, 周期µs - 1)` 即可无毛刺改频。
- 需要配套:① 测频源(过零/DFT 相位);② 把当前采样率同步给保护算法(相量/定值);
③ **消费端改事件驱动**(§6.7)——否则固定周期轮询在非 1 kHz 采样率下会丢帧。
③ **消费端改事件驱动**(§10)——否则固定周期轮询在非 1 kHz 采样率下会丢帧。
✅ **已落地(2026-10-10,§10)**:消费唤醒与 TIM6 同源 → 调频时只改 ARR,消费节拍自动跟随。
---
@@ -330,7 +330,7 @@ t=1000 µs 下一个 TIM6 更新事件
HAL **没有** `HAL_ADCEx_Calibration_Start` → 由 `Src/adc.c` 的 `adc1_calibrate()` 实现
(`USER CODE` 区 = L2,审计项 `adc1-calibration-call` / `adc1-calibration-impl` 兜底)。
实测校准前后同一拍 STM32 侧 6 路**同向**偏移 +10~+63 LSB、TPAFE 侧一字不变 → 此前 ADC 确实
带失调在跑。它**不消** 2 ms 忙等,但把"未校准"这条可疑路径从 §7.2 的候选根因里划掉。
带失调在跑。它**不消** 2 ms 忙等,但把"未校准"这条可疑路径从本节的候选根因里划掉。
- **启动 fail-safe(设计如此,已按此实现)**:TIM6 未启动 / 停摆 → `adcTask` 永久阻塞于
`osThreadFlagsWait(osWaitForever)` → `TASK_WD_ADC` 缺席 → IWDG 复位。属安全 fail-safe,
非缺陷(确保"节拍源缺失"不被静默放过)。
@@ -627,7 +627,7 @@ enter/exit 段还能读出"此刻仍在段内已多久"——**永久阻塞的
| 来源 | 引用点 |
|---|---|
| `docs/芯片手册/STM32F4xx参考手册_V18英文版_1-1000.md` §21 | 12 位递减计数器、LSI 时钟源、启动后不可停、超时公式、`SR.PVU/RVU` 同步 |
| 同上 §6.3.22(`RCC_CSR`) | `PORRSTF/BORRSTF/PINRSTF/SFTRSTF/IWDGRSTF/WWDGRSTF/RMVF`;"复位标志仅由电源复位清除" |
| `docs/芯片手册/STM32F4xx参考手册_V18英文版_1-1000.md` §6.3.22(`RCC_CSR`) | `PORRSTF/BORRSTF/PINRSTF/SFTRSTF/IWDGRSTF/WWDGRSTF/RMVF`;"复位标志仅由电源复位清除" |
| `Drivers/CMSIS/.../stm32f407xx.h` | `IWDG_TypeDef`(KR/PR/RLR/SR)、`IWDG_BASE`、`IWDG_SR_PVU/RVU`、`RCC_CSR_*` 位定义 |
| `STM32F407-Demo.ioc` | `Mcu.IP4=IWDG`、`IWDG.Prescaler/Reload`、`RCC.LSI_VALUE=32000`、`FREERTOS.Tasks01`、两个加固宏 |
| `Inc/stm32f4xx_hal_conf.h:58` | `HAL_IWDG_MODULE_ENABLED`(已启用) |
+45 -1
View File
@@ -1,6 +1,6 @@
# 网口设置页配置说明
> 版本:v2.2 日期:2026-10-10
> 版本:v2.3 日期:2026-10-11
> 适用工程:STM32F4-Base(STM32F407ZGTx + CH395F 以太网控制器)
> 范围:UI「网口设置」页可配置的网口参数(IP 地址 / 子网掩码 / 默认网关 / MAC 地址)——
> 参数与默认值、持久化、生效链路、页面操作与权限。
@@ -77,6 +77,8 @@ gw=192.168.1.1
mac=00:00:00:00:00:00
```
> 出厂模板同时含 `[ftp]` 段(`user` / `pwd_hash` / `write`,见 §3.4)——本块只展示 `[network]` 部分。
- **加载校验(逐键容错)**:单个键值非法(越界/格式错)不影响其他键——非法键回工厂默认并留痕(`parse: [network] <键> value invalid`);文件缺失 → 自动生成本模板;文件 > 4KB = 损坏 → 工厂默认 + 重建模板(坏配置不带崩网络)。
- **保存(外科手术式)**:注释 / 未知段 / 未知键逐行保留,受管键按 RAM 副本原位改写、缺失键补写;写盘 = 临时文件 `/config.TMP` + `f_rename` 原子替换(掉电不损坏目标文件)。
- **外部变更自动重载**(P4 阶段接入):2s 轮询检测 (size, mtime) 变化、4s 沉降(连续 2 次轮询稳定)后重载 `[network]` 段并重建网络——FTP 上传编辑后的文件无需重启即生效。
@@ -107,6 +109,47 @@ mac=00:00:00:00:00:00
INI 格式两端一致,文件可互读;mtime 变更令牌:MCU = FAT 打包时间(`fdate<<16\|ftime`,分钟级粒度),模拟器 = 宿主文件 mtime(秒级)。
### 3.4 `[ftp]` 段:FTP 鉴权(2026-10-11 新增,N-19)
FTP 服务(lftpd,:21)自 N-19 起启用口令鉴权。配置与 `[network]` **同文件、同引擎**(`net_cfg_load()` 内统一注册,键表与工厂默认见 `Drivers/BSP/NETCFG/ftp_cfg.c/h`;鉴权状态机在 `Drivers/BSP/NET/lftpd/lftpd_auth.c/h`,纯函数、主机侧可测):
```ini
[ftp]
user=admin
pwd_hash=d82494f05d6917ba02f7aaa29689ccb444bb73f20380876cb05d1f37537b7892
write=1
```
| 键 | 类型 | 工厂默认 | 说明 |
|---|---|---|---|
| `user` | STR(≤15 字符) | `admin` | 期望用户名,**大小写不敏感** |
| `pwd_hash` | STR(64 位 hex) | `d82494f05d6917ba02f7aaa29689ccb444bb73f20380876cb05d1f37537b7892`(即口令 `admin`) | **SHA-256(user‖password)** 摘要,hex 大小写不敏感 |
| `write` | UINT(0/1) | `1` | 1 = 允许 STOR/DELE;**0 = 写命令一律 `550` 拒**(只读卷) |
**鉴权流程**:
1. `USER`:不论用户名对错一律回 `331`(不泄露"用户是否存在";错用户名在 `PASS` 阶段仍走同耗时哈希路径拉平时间)。
2. `PASS`:计算 SHA-256(期望用户名‖口令),与 `pwd_hash` **恒定时间比较** → 对回 `230`、错回 `530`。
3. **未登录命令门**:登录成功前仅 `USER` / `PASS` / `QUIT` / `FEAT` / `SYST` / `NOOP` 放行,其余命令一律 `530`。
4. **失败锁定**:连续 **5 次**失败 → **锁定 60 s**(窗内所有 `PASS` 直接 `530`、不再计数);登录成功清零计数;锁定状态**全局跨会话**、易失(重启清零)。
**改口令步骤**(无 UI 入口;下载 `/config.ini` 编辑后上传,或经串口/烧录工具改写):
1. 生成新摘要 hex(对 `user + 新口令` 拼接串做 SHA-256;下例中 `user` / `pwd` 为占位,替换为字符串字面量,如 `('admin'+'新口令')`):
```powershell
python -c "import hashlib;print(hashlib.sha256((user+pwd).encode()).hexdigest())"
```
2. 把结果写入 `[ftp]` 段的 `pwd_hash=`(hex 大小写随意);
3. 上传回设备——配置引擎 poll 沉降 **≈4 s** 后重载,**下次登录生效**(既有会话不受影响)。
**忘口令恢复路径**:口令**不可**从 `pwd_hash` 反推(这正是哈希的意义)——经**串口/烧录工具改写配置**,或**重烧固件**回落出厂默认,然后立即改口令。
**出厂默认口令告警**:`pwd_hash` 仍为出厂值时,每次启动打 INFO 锚点 `[FTP] using factory default password`(改了 `pwd_hash` 即消失)。
> **诚实边界**:FTP 协议**明文传输口令**——本方案挡的是**未授权访问**,不挡**网络嗅探**(CH395F 上实现 TLS 不现实)。`pwd_hash` 只防 `/config.ini` 泄露后的**离线口令还原**,盐 = 用户名(无每机随机盐,局域网威胁模型下够用)。
---
## 4. 生效链路
@@ -265,6 +308,7 @@ ENTER 进入选中地址行 → `↑/↓` 调活动字节(0..255 **环绕**)
| 日期 | 变更 |
|---|---|
| 2026-10-11 | v2.3:**新增 §3.4 `[ftp]` 段(N-19 FTP 鉴权)**——`user` / `pwd_hash`(SHA-256(user‖password))/ `write` 字段表、鉴权流程(USER 恒 331 / 恒定时间比较 / 未登录命令门 / 5 败锁 60 s)、改口令步骤与忘口令恢复路径、出厂默认口令启动告警、明文嗅探边界 |
| 2026-10-10 | v2.2:**删除旧档 NCFG 迁移**——`net_cfg_legacy.c/h` 整体移除、`cfg_store_legacy_path()` 撤销;旧档 `/cfg/netcfg.bin` 若存在则被忽略(不读、不删、不迁移),配置回落工厂默认。理由:该一次性路径把整条配置写链挂进启动期调用图,defaultTask 静态最坏栈链 +600 B,且本仓库无携带旧档的设备 |
| 2026-09-14 | v2.1:存储迁移至**通用配置文件** `/config.ini`(INI `[network]` 段,cfg_file 引擎:逐键校验 / 外科手术式保存 / 原子写 / 旧档首启迁移 / 沉降轮询);`net_cfg_store.*` 弃用(由 `cfg_file_store.*` 取代);对外 API 不变 |
| 2026-09-10 | MAC 显示改为常识格式 **`A4:94:B6:12:34:56`**(冒号分隔,17 字符);页面布局左移(名 x=4 / 值 x=54)以容纳 102px 宽度,编辑光标段偏移随之为 3i |
+72 -39
View File
@@ -12,7 +12,7 @@
> **不在本文件范围**:`问题记录/错误码传播审计.md`(E-xx,审计报告,非待办池)、
> `问题记录/项目审查与优化建议报告.md`(体系层评审报告)、`测试说明/实机测试实施指南.md`(执行流程)。
>
> **创建**:2026-09-22(L3 pending 台架收尾);**合并重写**:2026-10-04。
> **创建**:2026-09-22(L3 pending 台架收尾);**合并重写**:2026-10-04;**统一排序 + 审查报告并轨**:2026-10-11。
---
@@ -27,6 +27,7 @@
| `S1~S14` / `A-1~A-7` | 测试充分性与架构改进专项 | `测试与架构改进建议.md`(2026-10-02 复审) | **编号不随条目删除而回收** |
| `E-xx` | 错误码传播审计发现 | `错误码传播审计.md` | **不并入本文件**,交叉引用 |
| 阶段 `A/B/C/D` + `B0~B3`/`C1~C6`/`D2~D6` | 任务排序分组 | `下一步任务规划.md` | 见 §2 |
| 报告 `N-x`/`R-x`/`F-x`/`S-x`/`U-x`/`D-x` | **审查报告自有编号** | `项目审查与优化建议报告.md` | ⚠ **与本登记簿 N-x/S-x 同名不同义**(报告 N-16 = 日志逐条开关,登记簿 N-16 = A2/A4 负向台架;报告 S-3 = check_registration.py,登记簿 S-3 = USART1 TX-only)→ **引用一律带文档名**。2026-10-11 其未闭环项已并入本登记簿,新编 `N-19`~`N-29`(出处列保留报告编号) |
**严重度定义**(沿用审计口径):P0 = 确认功能 bug|P1 = 疑似 bug 或健壮性风险|P2 = 改进|P3 = 卫生。
**置信度**:high = 代码级确证|medium = 依赖外部条件(手册/硬件/产品定义)。
@@ -37,51 +38,58 @@
| 级别 | 数量 | 关键项 |
|---|---|---|
| **P1** | 0 (+1 收尾) | P1-10 测试固件构建实测闭环(主体已闭环,见 §3.1) |
| 登记簿 | 9 | 网络:N-2(49s 日志静默,**低风险观察项**)、N-3(L5 基线重验)、N-4(pending 二期)、N-16(A2/A4 专项负向验证)、N-17(探针窗口心跳缺口);存储/日志:**S-3(USART1 TX-only 根治)**、S-4(dhara 重试自旋)、S-5(可选全序列卷锁);硬件:**N-18(8 路电气量分属两转换器 = V0.1 缺陷,V0.2 改版项)** |
| 测试/基础设施 | 5 | T-7(CI runner 未就绪)+ 测试架构专项 **S12**(断言宏)、**S1-c**(可选单测)、**A-4**(采集链路测试约束)、**A-5 残余**(CI 体积门禁盲区) |
| **安全** | 0 | — |
| **P1** | 0 (+1 收尾) | P1-10 测试固件构建实测闭环(主体已闭环,见 §3.1);~~N-30~~ 已销项(2026-10-11 三连定案修复,回归 4 轮 11/11,见 `问题记录/CH395F_Trap_Records.md` Trap 25) |
| 登记簿 | 20 | 网络:N-2(49s 日志静默,**低风险观察项**)、N-3(L5 基线重验)、N-4(pending 二期)、N-16(A2/A4 专项负向验证)、N-17(探针窗口心跳缺口);存储/日志:**S-3(USART1 TX-only 根治)**、S-4(dhara 重试自旋)、S-5(可选全序列卷锁)、N-22(写回 flush)、N-24(日志逐条开关);数据正确性:**N-21(dhara is_free/copy,报告 N-12/13)**;结构:**N-20(Error_Handler 死循环)、N-23(注册反向校验)、N-26(NAND DMA 断言)**;预算:**N-27(内存优化簇)、N-28(Flash 死代码簇)**;仓库:**N-29(.git 149 MB,需用户决策)**;硬件:**N-18(V0.2 改版项)** |
| 测试/基础设施 | 6 | T-7(CI runner 未就绪 + 推送后两 job 闭环 + 第二机交叉验证)+ **S12**(断言宏)、**S1-c**(可选单测)、**A-4**(采集链路测试约束)、**A-5 残余**(CI 体积门禁盲区)、**D7**(串口助手 A6/A7 待 CH340) |
> **网络侧另有 E-xx 未闭环项**(E-12/15/16/23/24/25/26/27/28/30/47/48/49/50/58/59/61,共 17 项)
> 与 1 项回归监督(E-62),详见 `问题记录/错误码传播审计.md` §3。本文件不重复登记。
> **UI 标准化簇**(报告 U1~U5,27 页未落地)与**覆盖率缺口**(lftpd/dhara 移植层无主机测试)
> 为整簇排期项,见 §2 W7 / N-21,明细指报告 §6/§7。
### 1.1 结构性判断(排序依据)
1. **网络层已是全项目最成熟子系统**(pending 机制 + 四份 Trap 记录 + 量化基线 + 探针族)。
剩余网络工作都是"二期扩展/收口",**没有紧急项**。
2. **产品功能缺口正在收口:ADC 采集链路(B1)已闭环,RS-485(B2)仍为空桩**。
`adc_task` 已由空桩转为真实消费者并 `@test` 实机闭环(2026-10-10:`frame_seq` 严格 1000 Hz
递增、`idle=0`、`frst_err=0`);`rs485_task` 仍为空桩,且受 E-59 门禁挡住(E-50 / E-49 未修)。
DTU 的核心职责(数据采集与转发)中,**采集侧还差 RS-485 一路**——即 E-59 那条"休眠缺陷闸门"的另一面。
3. **验证债已基本还清,重心是"功能债"**:剩余验证债仅 **N-3(L5 基线重锁,待 PC peer)**
(原 **S-1 栈风险已于 2026-10-10 削链闭环并销项**:defaultTask 静态最坏链 5312 → 2776 B、
实测峰值 3392 → 2152 B,**未抬栈**,见 `docs/内存管理/RAM使用分析与优化方案.md`)。
**当前最值得投入的是阶段 B(ADC/RS485 功能)**。
1. **产品功能主线只剩"采集与转发"两环**:ADC 侧 ✅(B1 台架闭环);**RS-485 侧被 E-50/E-49 挡着(B2)**;**上行 IEC104(B4)与 protectTask 立项(B5)此前只写在产品需求/保护规划,未进本登记簿——2026-10-11 补登**。B4 数据源已具备,可与 B2 并行。
2. **数据正确性最高风险 = N-21**(dhara `is_free` 只查页首 64 B / `copy` 不处理哨兵):低概率、后果重(数据丢失),且**至今只能靠人工读代码发现**——修复须配主机侧移植层测试(一并补报告"覆盖率"缺口)。
3. **内存优化被预算卡死**:CCM 只剩 16,304 B,54 KB 的 LVGL 池+UI 缓冲迁不进去(R-2 后半段堵死)→ 必须先做 §5.5 台账三件套(`ram_baseline.tsv` + 预算台账 + 92% 门禁)再排期 R-9→R-2。
4. **验证债仅剩窗口性事项**(D2/D5/D6/D7 + P1-10 收尾),不单独排期、逢窗口顺手做。
---
## 2. 任务排序(阶段 A/B/C/D)
## 2. 统一推进顺序(全项目唯一权威)
> 排序原则:**先降风险(产品级故障)→ 再补功能(核心链路)→ 再清结构债 → 最后卫生**。
> 本节只给「做什么 / 为什么现在 / 入口在哪 / 完成判据」,明细指向 §3 对应条目。
> **本节是全项目所有工作流唯一的权威顺序**:`产品需求设计文档.md` §9.2、`算法/继电保护功能复现规划.md`、
> `项目审查与优化建议报告.md` §0.3 中的路线图均为局部视图,**顺序变更只改本节,其它文档只留指针**。
> 排序原则:**安全风险 > 产品功能主线 > 数据正确性 > 预算/结构债 > 验证常态 > UI 扩展 > 仓库决策**。
### 2.1 阶段 B:核心功能补齐(产品价值最高,产品定义已确认)
### 2.0 波次总览
> 目标:让 DTU 真正采到数——这是当前**最大的功能空洞**,**当前最高优先级**。
> **B0 产品定义已于 2026-10-10 确认并销项**:上行 **IEC 60870-5-104**(以太网 TCP 2404)/
| 波次 | 内容 | 为什么在这 |
|---|---|---|
| **W2 功能主线** | **修 E-50/E-49 → B2** RS-485(ModBus RTU)∥ **B4** IEC104 上行 → **B5** protectTask 立项 | DTU 核心职责"采集与转发"仅剩两环:RS-485 侧被 E-59 挡、上行侧未动工;B4 数据源(ADC ✅)已具备,**可与 B2 并行**。**已定(2026-10-11):B2 先行(先修 E-50/E-49 驱动契约)、B4 并行开工** |
| **W3 数据正确性** | **N-21** dhara `is_free`/`copy`(报告 N-12/13) | 低概率、后果重(数据丢失);修复须配主机侧移植层测试,一并补报告"覆盖率"缺口 |
| **W4 预算与内存** | **N-27**:`docs/问题记录/项目审查与优化建议报告.md` §5.5 台账三件套 → R-9 池 40→16 KB → R-2 后半(54 KB→CCM) | CCM 只剩 16,304 B,R-2 后半被堵死;三件套是排期前提且工作量小 |
| **W5 结构债** | C1 pending 二期 · C3 日志管线(N-2 + N-24)· C5 收口 · C6 USART1(S-3)· N-20 `Error_Handler` · N-23 注册反向校验 · N-22/N-25/N-26/N-28 | 二期/健壮性/评估类,无紧急;N-23 是唯一"1 小时工作量"的一条;~~N-30~~ 已销项(Trap 25 定案修复,2026-10-11 回归 4 轮 11/11) |
| **W6 验证常态** | P1-10 收尾 · T-7 CI · D2 N-3 · D5 N-16 · D6 N-17 · **D7 串口助手 A6/A7** · S12 · S1-c · A-5 | 窗口性,逢机顺带,不单独排期 |
| **W7 UI 扩展** | U4 残余(golden + 字符集差集挂门禁)→ U1 令牌层 → U2/U3 → **27 页**落地 | 产品驱动、体量大,需单独立项(顺序按 `docs/问题记录/项目审查与优化建议报告.md` §6.5) |
| **W8 仓库决策** | **N-29** `.git` 149 MB + 图片 36.3 MB | `git filter-repo` 历史重写影响未推送提交与协作者 → **需用户决策** |
| **硬件改版** | N-18 8 路电气量全接 TPAFE(V0.2) | 下一版硬件;固件侧已用语义映射隔离,改版只动映射表 |
### 2.1 W2 功能主线详述
> **B0 产品定义已于 2026-10-10 确认并销项**:上行 **IEC 60870-5-104**(以太网 TCP 2404,遥测短浮点系数 1)/
> 持续采集 / **1000 Hz(20 点/周波)** / RS-485 **对下 ModBus RTU 采集**;结论详见
> `docs/算法/继电保护功能复现规划.md` §1.5,并已同步 `docs/产品需求/产品需求设计文档.md` §5.9/§5.10/§9。
> **B3(纪律门禁)已于 2026-10-10 固化进 `AGENTS.md` 并销项**;同日 TPAFE 侧前置缺陷
> **E-44 / E-45 / E-58 全部修复** → **B1(ADC 采集链路)已于 2026-10-10 实现并 `@test`
> 实机闭环销项**(`adc_task` 真实消费者 + 建任务 + 栈预算 + 台架 1000 Hz 确认);
> B2 仍欠 **E-50 / E-49**(RS485 驱动契约未修,门禁仍挡着)。
> **B3(纪律门禁)已固化进 `AGENTS.md`**;**B1(ADC 采集链路)已 `@test` 实机闭环销项**(2026-10-10)。
| 序 | 任务 | 前置 | 说明 |
|---|---|---|---|
| **B2** | **RS-485 采集链路实现**(消化 E-50、E-49) | B0 ✅ + **E-59 门禁**(**尚未满足**:E-50 / E-49 未修) | 双缓冲 + 序号(防包撕裂)+ DE 收尾(超时路径等 TC)+ 协议状态机(`App/protocol/` 正是为此预留);**B0 定用途 = 对下 ModBus RTU 采集** |
| **B2** | **RS-485 采集链路实现**(消化 E-50、E-49) | **E-59 门禁尚未满足**(E-50 / E-49 未修,先修驱动契约) | 双缓冲 + 序号(防包撕裂)+ DE 收尾(超时路径等 TC)+ ModBus RTU 状态机(`App/protocol/` 正是为此预留);设计约束见 A-4 |
| **B4** | **IEC 60870-5-104 上行**(TCP 2404,遥测/遥信/遥控上送) | B0 ✅ + 数据源(ADC ✅,**可与 B2 并行**) | 协议解析与 I/O 分离(A-4 约束 1)→ 天然适合 L0.5 主机单测;注意 CH395F 单连接约束(104 客户端与 FTP/远显/对时抢同一 socket 池,接入前定抢占策略,关联报告 N-09) |
| **B5** | **protectTask 立项**(L3 板端接线) | B2/B4 推进中;E-59 同款门禁 | `prot_set`/`prot_elm` 软件侧已落地(链接器移除、零足迹);建任务前后各跑 `stack_report.py`;定值/回路接线按保护规划路线图走 |
**建议**:B1 已闭环;下一步是 **B2**——但 B2 之前必须先修 **E-50 / E-49**(RS485 驱动契约未修,E-59 门禁仍挡着)。
### 2.2 阶段 C:结构债清理(二期)+ 存储侧健壮性
### 2.2 W5 结构债详述
| 序 | 任务 | 说明 | 出处 |
|---|---|---|---|
@@ -90,24 +98,30 @@
| **C5** | 一档剩余 | E-49(RS485 DE 收尾)、E-15(ringbuf 定去留——**已确认零调用者,倾向删除**)、E-61(SD2506 注释订正)、E-16(cfg_file 段名长度)、E-26(UDP 定案,倾向移除)、E-23、E-30 | 一档/二档 |
| **C6** | **USART1 TX-only 结构根治**(S-3) | 灭 ORE 僵尸机制根源:改 `.ioc` 令 USART1 不使能 RX / PA10 不复用为 `USART1_RX` → CubeMX 重生成 → 跑 `check_cubemx_survival.py`。属结构性改动,需单开一轮 + 复跑台架闭环 | S-3 |
### 2.3 阶段 D:验证与卫生(常态化)
### 2.3 W6 验证常态详述
| 序 | 任务 | 说明 |
|---|---|---|
| **D2** | N-3 L5 基线重锁 | 下次 Phase 13 窗口顺带做(低风险:阈值余量 >8×;**需 PC peer**) |
| **D5** | **N-16 专项负向台架窗口** | A2/A4 负向场景:PHY 拔线(需断 DTU 侧/关受管交换机端口)、A2 芯片级 `net_init` 失败(SPI 故障注入)、A4 短写压测 |
| **D6** | **N-17 探针窗口心跳缺口** | 定位 `time_sync` 监听重建为何阻塞 NET/FTP/UI(~1s 缺口,余量仅 ~0.7s);评估缩短重建重试或让掉线路径不阻塞其它任务 |
| **D7** | **串口助手 A6/A7 正路径补验** | 需 CH340 硬件接入后补验(`串口助手设计说明.md` 状态行 A1–A5/A8 已过) |
### 2.4 建议执行顺序(一图)
### 2.4 顺序一图
```
【阶段 B 功能】 B0 产品定义确认 ✅ ─┬─ B3 E-59 门禁固化(并行)
(当前最高优先) └─ E-58 原子快照 → B1 ADC ✅ → B2 RS485
↓
【阶段 C 结构】 C1 pending 二期 ─ C3 日志管线(N-2/S-3)
─ C5 收口 ─ C6 USART1 TX-only
↓
【阶段 D 常态】 D2 N-3 / D5 N-16 负向 / D6 N-17 探针窗口
W2 功能 E-50/E-49 → B2 RS485(ModBus) ∥ B4 IEC104 上行 → B5 protectTask
↓
W3 数据 N-21 dhara is_free/copy(配主机侧移植层测试)
↓
W4 预算 内存台账三件套 → R-9 池瘦身 → R-2 后半(54KB→CCM)
↓
W5 结构 C1 pending 二期 · C3 日志(N-2/N-24) · C5 · C6(S-3) · N-20 · N-23 · N-22/25/26/28
↓
W6 验证 P1-10 · T-7 CI · D2/D5/D6/D7 · S12 · S1-c · A-5
↓
W7 UI U4 残余 → U1 令牌层 → U2/U3 → 27 页(单独立项)
W8 仓库 N-29 .git 149MB(用户决策) 硬件:N-18 V0.2
```
---
@@ -133,7 +147,7 @@
| N-3 | **L5 队列往返基线(1223/988µs)的 L3 后重验**——L5 测 DONTWAIT 队列全程;L3 对该路径逐位不变 → 机制上无影响,但基线数值属分片时代,严格重锁需跑一轮 Phase 13(需 PC peer) | 2026-09-22(L3 收尾留档) | 低风险:10ms/100ms 阈值余量 >8×;下次 Phase 13 窗口顺带重锁即可 | `链路延迟测试说明.md` §6.4 L5 注¹ |
| N-4 | **pending 机制二期:ACCEPT + SEND 阻塞路径 pending 化**——E-06、E-20、E-21 剩余分量共用该机制扩展;届时 L3/L5 基线再重锁、探针族扩充、看门狗接 selftest 留痕、模拟器差距清单 +1 | — | 一期(RECV)已落地并入主线;架构现状见 `Net层阻塞模型架构设计.md`,二期设计基线见 §5.1 | `错误码传播审计.md` 第二档 #2 |
| N-16 | **A2/A4 专项场景台架验证待办**(正常路径已实测零回归):① PHY 拔线负向测试——断链需断 **DTU 侧**线缆或关**受管交换机端口**;② A2 芯片级 `net_init` 失败分支(需 SPI 故障注入);③ A4 短写压测(需远显客户端 + UI 推流压力) | 2026-10-03(台架拓扑 = CH395↔交换机,拔线操作受限) | 待专项台架窗口 | `实机测试实施指南.md` §10 |
| N-17 | **`time_sync` 30s 挂起客户端掉线 + 监听重建期间,网络任务心跳缺口达 800ms~1s(声明 stall 但未复位)**——V5 长跑 44 次 `task stall: mask=0x00000001`,**100% 与 `e02_probe` 的 30s 挂起客户端触发 `time_sync: no TIME packet in 30s, drop conn` → 监听重建(约 4s)重合**;隔离实验(仅 FTP,120s)**0 次 stall** → 探针场景专属。缺口(~1s)< IWDG 最坏超时(1701ms),余量仅 ~0.7s。**待办**:定位监听重建为何阻塞 NET/FTP/UI;评估缩短重建重试(5×2s)或让掉线路径不阻塞其它任务 | 2026-10-03(V5 台架) | `实机测试实施指南.md` §4;`子系统设计/看门狗设计说明.md` |
| N-17 | **`time_sync` 30s 挂起客户端掉线 + 监听重建期间,网络任务心跳缺口达 800ms~1s(声明 stall 但未复位)。**待办**:定位监听重建为何阻塞 NET/FTP/UI;评估缩短重建重试(5×2s)或让掉线路径不阻塞其它任务 | 2026-10-03(V5 台架) | V5 长跑 44 次 `task stall: mask=0x00000001`,**100% 与 `e02_probe` 的 30s 挂起客户端触发 `time_sync: no TIME packet in 30s, drop conn` → 监听重建(约 4s)重合**;隔离实验(仅 FTP,120s)**0 次 stall** → 探针场景专属。缺口(~1s)< IWDG 最坏超时(1701ms),余量仅 ~0.7s。 | `实机测试实施指南.md` §4;`子系统设计/看门狗设计说明.md` |
**存储 / 日志管线**
@@ -153,7 +167,23 @@
| # | 事项 | 优先级 | 问题与证据 | 处置方向 | 出处 |
|---|---|---|---|---|---|
| T-7 | **CI runner 未就绪(部署前置)** | 中 | `.gitea/workflows/ci.yml` 已落地,但 CI 仅在 runner 就绪后才真正生效:① Gitea 实例 `[actions] ENABLED = true`;② 注册 Windows runner(labels 含 `windows`),装 Python 3.x / cppcheck / pre-commit / MSVC | runner 未就绪前,**提交前仍须在本地跑 `scripts/ci_local.py` 或 `pre-commit run --all-files` 自查**(L0 唯一保障,不能因"已配 CI"而放宽) | `.gitea/workflows/ci.yml`;`测试说明/测试开发标准流程.md` §9 |
| T-7 | **CI runner 未就绪(部署前置)** | 中 | `.gitea/workflows/ci.yml` 已落地,但 CI 仅在 runner 就绪后才真正生效:① Gitea 实例 `[actions] ENABLED = true`;② 注册 Windows runner(labels 含 `windows`),装 Python 3.x / cppcheck / pre-commit / MSVC;③ runner 就绪后**推送远端跑一次两 job 闭环验证**(当前 21 个提交未推送,CI 从未真实执行);④ **第二机交叉验证**(不同工具安装路径,验 `DTU_*` 环境变量方案,→ `环境与部署/本地工具路径环境变量方案.md` 变更记录 TODO) | runner 未就绪前,**提交前仍须在本地跑 `scripts/ci_local.py` 或 `pre-commit run --all-files` 自查**(L0 唯一保障,不能因"已配 CI"而放宽) | `.gitea/workflows/ci.yml`;`测试说明/测试开发标准流程.md` §9 |
**审查报告并轨(2026-10-11;⚠ 报告编号与本登记簿同名不同义,出处列保留报告编号,引用一律带文档名)**
| # | 事项 | 影响 / 现状 | 处置方向 | 波次 | 出处 |
|---|---|---|---|---|---|
| N-20 | `Error_Handler` 仍 `__disable_irq(); while(1){}`(`Src/main.c:274-282`) | 一处初始化失败 = 整机挂死(只剩 IWDG 兜底),无自检日志 | 统一降级策略:记自检 + 禁用该子系统 + 其余照常 | W5 | 报告 N-03c |
| N-21 | **dhara 两处**:`dhara_nand_is_free()` 只查页首 64 B;`dhara_nand_copy()` 不处理 `DHARA_NO_PAGE` 哨兵 | 映射恢复误判 → **数据丢失风险**(低概率、后果重);哨兵页号被当真实页 → 未定义行为;至今只能靠人工读代码发现 | 整页(或首/中/尾)扫描 + 返回 `DHARA_E_UNSUPPORTED`;**必须配主机侧移植层单测**(一并补报告"覆盖率"缺口) | **W3** | 报告 N-12/N-13 + §7 |
| N-22 | 写回缓存无空闲/掉电强制 flush(整页写与换页已 flush) | 写回窗口内掉电丢最近日志/配置写 | defaultTask 空闲 N ms flush | W5 | 报告 N-14 |
| N-23 | **双端注册无反向校验**:`rs485_task.c`、`lftpd_log.c` 既不在 uvprojx 也不在 simulator | "写了不编译"= 死代码 + 误导 | `check_registration.py`(报告中唯一"1 小时工作量")+ 挂 pre-commit;`app_cfg.h` 未接线开关一并处置(C5) | W5 | 报告 N-06 残余 + 报告 S-3 |
| N-24 | 日志每条 `f_open+f_write+f_close`(`sys_log_mcu.c:123-140`) | 每条日志 2–4 次 NAND 页读写;UI 翻记录逐条 open/seek/close | 与 C3 日志管线合并评估(现仅 `FIL` static 化省 528 B 栈) | W5 | 报告 N-16(**≠ 本登记簿 N-16**) |
| N-25 | 性能/资源建议簇:PASV 每次新建监听;`NET_RECV_TIMEOUT_MS 30000`;PHY 强制 100M 无自协商回退;`NET_MAX_SOCKETS 8` 与单连接语义不符;`HAL_Delay(1)` 阻塞 DE | 次级性能/语义一致性 | 逐项见 `docs/问题记录/项目审查与优化建议报告.md` §4.2/§4.5/§4.7,随窗口评估 | W5 | 报告 N-09/P2-8/PHY/P3-x/rs485 |
| N-26 | NAND 直 DMA 调用方缓冲(已加运行时守卫,阈值 1024) | CCM 缓冲被 DMA = 静默传错(与 ADC DMA 不可达 CCM 同类坑) | 补编译期/启动期地址断言 + **新铁律:堆缓冲不可被 DMA 直接寻址** | W5 | 报告 N-11 |
| N-27 | **内存优化簇**:LVGL 池 + UI 两份缓冲 54,146 B 仍在 SRAM1,CCM 只剩 16,304 B;`ram_baseline.tsv` / `RAM预算台账.md` / 92% 阈值门禁**均未建** | R-2 后半段(迁 CCM)被堵死 → B2/B4/B5 的功能预算看不清 | **前置 §5.5 台账三件套** → R-9(`LV_MEM_SIZE` 40→16 KB,池水位门禁数据已具备)→ R-2 后半;R-3/R-4/R-5/R-6 逐项评估(R-6 前置已满足) | **W4** | 报告 R-2 剩余/R-3~R-6/R-9/§5.5 |
| N-28 | Flash 死代码评估簇:剩余 ≈10 KB(`box_shadow` 3,246 / `draw_sw_img` 2,960 / `bin_decoder` 1,972 / `image_decoder` 952 / `triangle` 930)+ 双精度 `dadd` 等 ≈1 KB;uvprojx 仍挂 201 个空 LVGL TU(F-1/F-2/F-3 已闭环,省的是编译时长) | 编译时长 + 文档噪音 | 逐项实测再关;⚠ `mask`/`mask_rect`/`grad` 是文字与矩形绘制必经路径,**不可盲关** | W5 | 报告 F-4/F-5/F-6 |
| N-29 | **仓库膨胀**:`.git` 149 MB + 图片 36.3 MB | clone/备份慢,且持续恶化 | 降采样 / 拆 submodule / LFS 三选一 **+ 一次 `git filter-repo` 历史重写**。**已决策(2026-10-11):暂不处理、登记观察**——推送/协作前重评(届时未推送提交更少、重写代价最低) | **W8** | 报告 D-1 |
| U 簇 | UI 标准化:无令牌层/组件库/页注册表(手写 `case` 仍在),**27 页**未落地;golden 未挂 pre-commit、字符集差集检查仍是约定 | 新增一页成本仍是 8 处手工改动 | 按 `docs/问题记录/项目审查与优化建议报告.md` §6.5:U4 残余(两脚本入门禁,可逢窗口顺带)→ U1 令牌层 → U2/U3 → 页面落地;U4 golden 可直接当 U1 验收工具。**已确认(2026-10-11):本轮不排期**,等功能主线出结果后按产品需求启动 | W7 | 报告 U1~U5 |
### 3.3 测试与架构改进专项(S1x / A-x)
@@ -345,6 +375,9 @@ S14 已在 workflow 显式提示"体积门禁未执行"(`::notice::`)并在
**不留变更记录**,历史见 `git log` 与各模块文档。
- 已闭环条目的**结论与证据沉淀进模块文档**,本文件不再重复。
- 编号一律**沿用且不回收**(P-x / N-x / S-x / T-x / S1x / A-x);`错误码传播审计.md` 的 E-xx 独立维护。
- **引用审查报告编号一律带文档名**(报告 N-16 ≠ 登记簿 N-16、报告 S-3 ≠ 登记簿 S-3);
报告并轨项用本登记簿新编号(N-19~N-29 + U 簇),出处列保留报告编号。
- **§2 是全项目唯一的推进顺序权威**:`产品需求设计文档.md` §9.2 等只留指针,顺序变更只改 §2。
- 阶段 B 完成后重写 §1(届时 ADC/RS485 应从未闭环消失)。
- 新增发现按既有惯例分流:能立即修的进 §2(阶段 C5);需排期的进 §3 对应编号空间;跨版本观察的进 §3.2(登记簿)。
- 上游引用关系:`问题记录/项目审查与优化建议报告.md`(体系层评审)、`问题记录/错误码传播审计.md`(E-xx)、
+70 -1
View File
@@ -102,6 +102,69 @@
`check_doc_links.py` 扫描前会剥离行内代码 span,故写法示例不会误报;但**反引号内的仓库根相对路径**(`docs/xxx.md`)仍会被校验——那是本工程的引用约定形态。
### 3.5 引用章节:紧邻写法 + 别名表,让门禁能校验
引用某文档的某小节,写成 **`` `docs/xxx/yyy.md` §N.M ``**(文档名与 `§` 紧邻,中间只允许空白/反引号/括号)。
| 写法 | 门禁 |
|---|---|
| `` `docs/子系统设计/看门狗设计说明.md` §10.4 `` | ✅ 硬门禁(失效拦提交) |
| `` `看门狗设计说明.md` §10.4 ``(裸文件名,全库唯一) | ✅ 硬门禁 |
| `看门狗设计说明 §10.4`(省略 `.md`,文件名全库唯一) | ✅ 硬门禁 |
| `链路延迟 §8.3`(文档名**前缀**,全库唯一) | ✅ 硬门禁 |
| 「内存方案 §1.4」——已登记别名 | ✅ 硬门禁 |
| 「报告 §4.2」——昵称歧义、未登记 `[示例]` | ⚠️ 判不了 → 进 `--section-todo` 清单 |
| 本文档内「见 §3.1」 | ✅ 硬门禁(本文档必须有该编号;确实指别篇却省了文档名的,机器判不了 → 跳过并进待办) |
**两档严重性**:目标明确(路径 / 裸名 / 前缀 / 别名 / 同句 `§A / §B` 链)→ **错误**;按自引用解析 → 默认**告警**,但 **pre-commit 钩子带 `--sections-strict`,两档都拦提交**(2026-10-11 起:全库告警已清零,清零之后就该守住)。手工跑想只看告警:去掉该 flag。
**别名表 `scripts/section_aliases.json`**:昵称 → 文档路径。登记一次,正文一个字不用改,引用就纳入硬门禁。**只登记无歧义的昵称**(「报告」命中多篇 → 不登记,逼着写清楚)。
**覆盖率必须看着**:每次跑 `doc-links` 都打印一行
```
章节引用:共 1576 处,机器可判定 843 处(53%);跳过 → 昵称式 68 / 变更记录 419 / 外部手册 187 / …
```
比例只许涨不许跌。清 backlog:
```powershell
& '.venv\Scripts\python.exe' scripts/check_doc_links.py --all --section-todo
```
它列出全部机器判不了的 `§`(能唯一猜到的还给「建议改写:`docs/…` §N」),改一处涨一处;猜不到的就是**必须写清楚文档名**的那些。
> **口径怎么调出来的**:全库 1578 处 `§`。宽松版报警 173 处(几乎全是昵称误判)→ 加文档名解析 111 → 加紧邻 + `§` 链 + 前缀匹配 + 别名表 + 「父级编号不存在就不是自引用」判别 → 可判定比例 **7% → 58%(914 处)**,`--section-todo` 待办 **83 → 0**;清完 backlog 后**两档都拦提交**。变更记录小节(422 处)与外部手册编号(218 处)刻意不校验:前者是「当时那个编号」的历史指称,后者归厂商文档。
>
> **把待办清零靠的三类动作**(按性价比排序;引号内是**改前**写法 `[示例]`):① **手册引用写全名**——「手册 §9.2.1」一律改成 `docs/芯片手册/CH395F.md` §9.2.1(CH395F / GD5F / SD2506 / RM0090 摘录里都有编号标题,改完立刻纳入校验);② **昵称写全或登记别名**——「架构 §5.5」→ `UI_设计架构.md` §5.5、「报告 §6.5」→ 全路径、「V0.0 报告 §4.1」→ `V0.0 §4.1`(别名表登记 `V0.0`);③ **路径补 `docs/` 前缀**——`子系统设计/xxx.md` 这种「从 docs/ 起算」的写法机器解析不到。
>
> **两条写作铁律**(机器只看**同一行**):`§` 必须与文档名**紧邻**(换行/跨句就判不了);「见/按/该 + 昵称」这类含糊指代,宁可多打 30 个字符写全路径。
>
> **首轮清账揪出的真断链(41 处,已修)**——`[示例]` 下列编号是**修复前的错值**,故意原样保留作反例:`链路延迟测试说明.md` 10 处引 §8.1,而该文 §8 下只有 8.2/8.3/8.4(§8.1 早被删);`GD5F2GQ5UE_Test_Guide.md` 引 §4.8(BBT 实为 §3);`自动化测试流程.md` 引 `ST-LINK_CLI_复位使用说明.md` §10(该文只到 §7);`模拟器差距清单.md` 引 §219(实为 §4.1);`实机测试实施指南.md` 引 §4.4 白名单(白名单在 `看门狗设计说明.md` §4.3);`V0.1_网表详细分析报告.md` 表格里 12 处裸 `§3.4`/`§5.3` 其实指 V0.0 报告(补 `V0.0` 前缀 + 登记别名)。**教训:编号会随重构漂移,只有机器校验能发现。**
### 3.6 改名 / 挪目录:用 `scripts/rename_ref.py`,不要手工扫
**固定动作**(改名、挪目录、合并文档一律走这条,别靠 `git grep` 手改):
```powershell
# 1) 预览:看它打算改哪些文件的哪些行(默认 dry-run,不落盘)
& '.venv\Scripts\python.exe' scripts/rename_ref.py docs/旧/名.md docs/新/名.md
# 2) 确认无误后落盘:git mv + 改写全库引用 + 自动跑 check_doc_links --all 复核
& '.venv\Scripts\python.exe' scripts/rename_ref.py docs/旧/名.md docs/新/名.md --apply
# 整目录搬迁(末尾带 / = 挪进该目录并保留文件名;旧/新都是目录 = 前缀改写)
& '.venv\Scripts\python.exe' scripts/rename_ref.py docs/旧目录/ docs/新目录/ --apply
```
它覆盖的引用形态:反引号根相对引用、Markdown 链接(根相对与文档相对都按原风格改写)、**图片链接的 `../` 层级换算**(§3.2 那条实证坑由它兜住)、`#include "x.h"` 裸文件名、`MDK-ARM/*.uvprojx` 的 `<FilePath>`、`simulator/CMakeLists.txt` 的 `${PROJECT_ROOT}/…` 与 `${VAR}/…`、以及 skill / 脚本 / 配置里的路径串。
**它管不到的三件事**(脚本会在结尾提示):
1. **昵称式引用**(「内存方案 §1.4」)与正文里的裸文件名散文 → 落盘后 `git grep -n '<旧文件名>'` 扫一遍;
2. **生成物**(`docs/images/引用关系图.*`)不改写 → 重跑 `& 'tools\refgraph\gen_refgraph.bat'`;
3. 改的是 `.c/.h` 且**新增了目录** → 还要把目录加进 `uvprojx` 的 `<IncludePath>`(→ `new-source-file` skill),随后 `@build` 验证。
---
## 4. 门禁:文档-代码一致性(D-7)
@@ -110,7 +173,7 @@
| 层次 | 钩子 | 检查内容 | 触发条件 |
|---|---|---|---|
| **存在性** | `doc-links` | `docs/**/*.md` 里的链接、反引号路径是否指向真实文件 | 改动任意 `.md` |
| **存在性** | `doc-links` | `docs/**/*.md` 里的链接、反引号路径是否指向真实文件;`§N.M` 章节引用是否指向真实小节(§3.5) | 改动任意 `.md` |
| **数值** | `task-table` | `README.md`、`AGENTS.md` 的 FreeRTOS 任务表是否与 `Src/freertos.c` 一致 | 改动 `README.md` / `AGENTS.md` / `Src/freertos.c` |
| **体积** | `mem-numbers` | 内存/Flash 数字是否与最新 `.map` 一致(受管区 + 陈旧散句数字) | 改动内存文档 / `App`·`Src`·`Inc` 源码 / 脚本 |
| **体积** | `check-added-large-files` | 阻止 >1MB 大文件入库 | 任意文件 |
@@ -121,6 +184,10 @@
# 全库文档链接存在性
& '.venv\Scripts\python.exe' scripts/check_doc_links.py --all
# 改名 / 挪目录(先预览再落盘,见 §3.6)
& '.venv\Scripts\python.exe' scripts/rename_ref.py docs/旧/名.md docs/新/名.md
& '.venv\Scripts\python.exe' scripts/rename_ref.py docs/旧/名.md docs/新/名.md --apply
# 任务表一致性(只检查)
& '.venv\Scripts\python.exe' scripts/gen_task_table.py --check
@@ -194,6 +261,8 @@
- 外部 URL(`http/https/mailto`)与纯锚点(`#...`)
- `docs/芯片手册/` 等外部资料目录
**章节引用(D 类)另有的跳过**:昵称式引用(「内存方案 §1.4」)、非紧邻写法、变更记录小节内的历史指称、外部手册编号(RM0090 等)、目标文档本身不用数字编号标题。
> ⚠️ 状态词按**就近**(±40 字符)判定,不是整行——否则文件名本身含「不存在」「删除」等字样时会误豁免同行的其它真实引用(2026-09-25 实测修正)。
---
@@ -117,7 +117,7 @@ HAL_Init → SystemClock_Config → MX_GPIO_Init → MX_USART1_UART_Init
2. READ_ID (9Fh)
└─ 校验 MID=0xC8, DID=0x52
3. BBT 扫描
└─ 读每块第一页 (page 0) spare byte 0(列地址 0x800,手册 §12.4 初始坏块标记位)
└─ 读每块第一页 (page 0) spare byte 0(列地址 0x800,`docs/芯片手册/GD5F2GQ5UExxG.md` §12.4 初始坏块标记位)
└─ 非 0xFF 即出厂坏块,写入 s_bbt[]
4. SET_FEATURE (B0h=10h) — 使能内部 8bit ECC
5. SET_FEATURE (A0h=00h) — 解除全部块保护
@@ -186,7 +186,7 @@ DMA 消除了轮询模式下 SPI 状态寄存器查检的逐字节 CPU 开销,
- `gd5f_bbt_scan()` — 在 `gd5f2gq5ue_init()` 阶段、ECC 使能前扫描构建 BBT;运行时不再提供公开重扫接口(见 GD5F2GQ5UE_Trap_Records.md)
- `gd5f2gq5ue_print_bbt()` — 打印 BBT 摘要(调试用)
> **注意**:`gd5f_private_bbt_scan()` 选择在 ECC 使能前扫描。按手册 §12.6,ECC 使能后整段 spare 仍可读取(仅 0x840~0x87F 的 ECC 校验区禁止编程),因此读取 0x800(spare byte 0)即便 ECC 开启也安全;但扫描放在 ECC 禁用阶段是更稳妥的默认做法。运行时坏块只来源于真实擦除/编程失败,不要主动重新扫描。
> **注意**:`gd5f_private_bbt_scan()` 选择在 ECC 使能前扫描。按`docs/芯片手册/GD5F2GQ5UExxG.md` §12.6,ECC 使能后整段 spare 仍可读取(仅 0x840~0x87F 的 ECC 校验区禁止编程),因此读取 0x800(spare byte 0)即便 ECC 开启也安全;但扫描放在 ECC 禁用阶段是更稳妥的默认做法。运行时坏块只来源于真实擦除/编程失败,不要主动重新扫描。
### 4.7 页内寻址模型(行地址 / 列地址)
@@ -197,7 +197,7 @@ NAND 访问是**二维寻址**,两个地址含义不同:
| 行地址(页地址 / page address) | 第几页 | `gd5f_page_read(page)` | 把哪一页从存储阵列搬进芯片内部 cache 寄存器 |
| 列地址(column address) | 该页内第几个字节 | `gd5f_read_from_cache(column, ...)`(发送 `0Bh` + 2 字节列地址) | 从 cache 里偏移 `column` 处开始吐数据 |
**一页的内部布局**(手册 §12.6 Table 12-8):一页 = 2KB 主数据 + 64B 备用区(spare) = 2112 字节,页内连续编址为 column 0 ~ 2111:
**一页的内部布局**(`docs/芯片手册/GD5F2GQ5UExxG.md` §12.6 Table 12-8):一页 = 2KB 主数据 + 64B 备用区(spare) = 2112 字节,页内连续编址为 column 0 ~ 2111:
```
page (2112B) = main(2048B) + spare(64B)
@@ -209,7 +209,7 @@ NAND 访问是**二维寻址**,两个地址含义不同:
- `column 0 ~ 2047` → 主数据区(main data)
- `column 2048 ~ 2111` → 备用区(spare),其中 **2048(0x800)就是 spare 的第 0 字节**
**为什么坏块标记的列地址是 2048**:main 区占据了前 2048 字节,spare 区从 2048 起算,故 spare byte 0 落在列地址 2048(0x800)。驱动把 `GD5F_PAGE_SIZE`(=2048)作为 `column` 传给 `gd5f_read_from_cache()`(`gd5f2gq5ue.c:307-308` 将其拼成 2 字节列地址),芯片即从 spare[0] 开始回数据——这正是出厂坏块标记位(手册 §12.4 / Table 12-6:First spare area location = Byte 2048)。
**为什么坏块标记的列地址是 2048**:main 区占据了前 2048 字节,spare 区从 2048 起算,故 spare byte 0 落在列地址 2048(0x800)。驱动把 `GD5F_PAGE_SIZE`(=2048)作为 `column` 传给 `gd5f_read_from_cache()`(`gd5f2gq5ue.c:307-308` 将其拼成 2 字节列地址),芯片即从 spare[0] 开始回数据——这正是出厂坏块标记位(`docs/芯片手册/GD5F2GQ5UExxG.md` §12.4 / Table 12-6:First spare area location = Byte 2048)。
> 一句话:**page 选页,column 选页内字节偏移;main 占前 2048 字节,spare 从 0x800 起算。**
@@ -569,3 +569,12 @@ if (nand_ftl_format() == 0) {
11. **FA_OPEN_APPEND 陷阱 / 短写** — `FA_OPEN_APPEND`(0x30) = `FA_OPEN_ALWAYS|FA_SEEKEND`,**不含 `FA_WRITE`(0x02)**——不带写位打开的文件 `f_write` 恒返回 `FR_DENIED`(7)(这是当年 `/log` 写失败的真根因);追加写必须写 `FA_WRITE | FA_OPEN_APPEND`(2026-09-20 台架实测,`Lib/FatFs` 移植版)。该移植的 `f_write` 在簇分配失败时返回 `FR_OK` + 短写(**没有错误码**)→ 判失败必须校验写回的字节数。标志位为**旧式编号**:`FA_READ`=0x01 / `FA_WRITE`=0x02 / `FA_CREATE_NEW`=0x04 / `FA_CREATE_ALWAYS`=0x08 / `FA_OPEN_ALWAYS`=0x10 / `FA_OPEN_APPEND`=0x30 / `FA_SEEKEND`=0x20(`ff.c` 内部定义),与上游 FatFs R0.14 不同,**勿照抄上游代码示例**。
12. **崩溃可观测性(勿屏蔽)** — 已开启 `configCHECK_FOR_STACK_OVERFLOW=2` + `configUSE_MALLOC_FAILED_HOOK` + HardFault 现场报告(`Src/stm32f4xx_it.c`:裸 UART 打印 PC/LR/xPSR + CFSR/HFSR/BFAR/MMFAR 后复位,对照 `.map` 定位符号)。defaultTask **1024** words、testTask **2048** words 是**实测下限,勿降**。开机日志出现 `[ERR] STACK OVERFLOW task=` / `[ERR] HARDFAULT ...` / `MALLOC FAILED` 是精确定位线索,禁止屏蔽或降级。
13. **BBT 恢复模式** — 运行期坏块会随 TC-STO-03 注入累积;当介质被**误标脏**(如 DMA 误打 CCM 栈导致随机块被写坏并持久化)时,打开 `test/board/storage_test.h` 的 `ENABLE_STORAGE_RECOVERY_TESTS` 编译**恢复构建**,testTask 会执行 `gd5f2gq5ue_bbt_rebuild()`(出厂扫描清除运行期误标)+ `nand_ftl_format()` 清账后复位。恢复后容量回到基线(即扣除 BBT 保留池后的正常容量,非出厂裸容量)。
### 10.2 FTP 服务(lftpd)鉴权语义(2026-10-11,N-19)
FTP 根目录 = 卷根,lftpd 是卷的网络文件入口(`Drivers/BSP/NET/lftpd/`)。N-19 起启用口令鉴权——`[ftp]` 配置段、鉴权流程与改口令方法的**权威口径**在 `docs/子系统设计/网络配置说明.md` §3.4,此处只登记存储侧语义:
- **未登录命令门** — 登录成功前仅 `USER`/`PASS`/`QUIT`/`FEAT`/`SYST`/`NOOP` 放行,其余命令(含 RETR/STOR/LIST/DELE)一律 `530`。修复前 `cmd_user`/`cmd_pass` 对任意口令无条件回 `230` = **匿名全读写整卷**(N-19 根因)。
- **STOR/DELE 写门** — `[ftp] write=0` 时写命令一律 `550` 拒,卷对 FTP 侧只读(`/log`、`/config.ini` 仍可下载不可改写)。
- **全局失败锁定** — 连续 5 次鉴权失败 → 锁定 60 s(跨会话全局、易失、重启清零);纯 lftpd 层状态,不触存储。
- **默认口令告警锚点** — 出厂 `pwd_hash` 在用期间每次启动打 INFO `[FTP] using factory default password`。
+2 -2
View File
@@ -190,7 +190,7 @@ flowchart TB
**单连接约束**:CH395F 配置为多 Socket + TCP Server 单连接(`set_fun_para(0x09)`),Socket 0 复用监听与数据通道;FTP / 对时 / 远程显示三个 TCP 服务**串行**占用连接,连接断开后自动回到 LISTEN。
> **FTP 启动门(2026-09-21 修复)**:`ftpTask` 必须等 `NET_READY` **且** `STORAGE_READY`(两位均 `NoClear`)才 `lftpd_start`——此前就绪位被其它等待者消费,FTP **静默不监听**(详见 §2.2 与 `docs/测试说明/FTP_Test_Guide.md` §8.6)。
> **单客户端实测注意**:同一时刻只服务 1 个会话,旧会话异常断开后约 30s 才恢复监听。测试脚本必须内置连接重试(`test/host/ftp_up_dl_test.py`、`test/host/ftp_speed_test.py` 已内置),且**不要用裸 TCP 工具探测 21 端口**(探测会占掉唯一会话,见该指南 §8.7)。
> **单客户端实测注意**:同一时刻只服务 1 个会话,旧会话异常断开后约 30s 才恢复监听。测试脚本必须内置连接重试(`test/host/ftp_up_dl_test.py`、`test/host/ftp_speed_test.py` 已内置),且**不要用裸 TCP 工具探测 21 端口**(探测会占掉唯一会话,见 `docs/测试说明/FTP_Test_Guide.md` §8.7)。
> 2026-09-21 实测:登录/TYPE I/PASV/STOR/RETR/LIST/DELE/QUIT 全通,128KB 上传下载逐字节一致(约 49/60 KB/s,低于阶段 5 裸 socket 基线,待查)。
---
@@ -314,7 +314,7 @@ flowchart LR
| 控制台单写者日志(USART1 调试流) | `docs/子系统设计/控制台单写者日志设计说明.md` |
| RAM 预算与优化 | `docs/内存管理/RAM使用分析与优化方案.md` |
| 栈/内存观测工具 | `scripts/stack_report.py`(Keil 静态调用图 × 串口 `[STACK]` 水位,判定取两者更坏者)· `scripts/ram_report.py`(.map 静态占用)—— 用法与基线见内存方案 §0.6 |
| FTP 功能与陷阱 | `docs/测试说明/FTP_Test_Guide.md`(启动门 §8.6 / 单客户端 §8.7)· 脚本 `test/host/ftp_up_dl_test.py`、`test/host/ftp_speed_test.py` |
| FTP 功能与陷阱 | `docs/测试说明/FTP_Test_Guide.md` §8.6(启动门)/ §8.7(单客户端)· 脚本 `test/host/ftp_up_dl_test.py`、`test/host/ftp_speed_test.py` |
| UI 设计架构 / 内容页 / 菜单 | `docs/UI设计/UI_设计架构.md` · `内容页架构.md` · `菜单页面设计.md` |
| UI 自动化测试 | `docs/UI设计/UI_自动化测试流程.md` |
| 驱动已知陷阱 | `docs/问题记录/{CH395F,GD5F2GQ5UE,SD2506}_Trap_Records.md` |
+2 -2
View File
@@ -17,8 +17,8 @@
| 参数 | 值 | 说明 |
|------|-----|------|
| CH395F 内部总缓冲 | **24KB(48 块 × 512B)** | 手册 §5.45;CH395F.md "内置 24K RAM"。**经 Phase 7 实测确认块 0~47 全部有效**(此前 32×1KB 记录有误) |
| 上电默认分配 | Socket 0~3 各 接收8块(4KB)+发送4块(2KB);Socket 4~7 未分配 | 见 §5.45,用到 >4 个 Socket 必须重分配 |
| CH395F 内部总缓冲 | **24KB(48 块 × 512B)** | `docs/芯片手册/CH395F.md` §5.45;CH395F.md "内置 24K RAM"。**经 Phase 7 实测确认块 0~47 全部有效**(此前 32×1KB 记录有误) |
| 上电默认分配 | Socket 0~3 各 接收8块(4KB)+发送4块(2KB);Socket 4~7 未分配 | 见 `docs/芯片手册/CH395F.md` §5.45,用到 >4 个 Socket 必须重分配 |
| **本固件分配(net_init)** | 8 个 Socket 各 **接收 4 块(2KB) + 发送 2 块(1KB)** | 块数由 `CH395F_TCP_MSS` 推导(接收 `CH395F_RECV_BLOCKS`=2×MSS、发送 `CH395F_SEND_BLOCKS`=1×MSS,每块 512B);起始块 `recv=i*6`、`send=i*6+4`;用尽 0~47,互不重叠 |
| **TCP MSS** | **1024** | 由接收缓冲(2KB) ≥ 2×MSS 决定,8 Socket 共享 24KB 时的最大值 |
| **SPI DMA 缓冲** | **4100B** | `CH395F_SPI_DMA_BUF_SIZE` |
+2 -2
View File
@@ -20,7 +20,7 @@
因此测试代码可在任意任务上下文调用,但要注意调用任务会被 SPI 传输阻塞数毫秒(擦除最久)。
- **先擦后写**:NAND 只能将 1 写为 0,**写操作前目标块必须已擦除**,否则数据不可预期(见边界条件 TC-GD5F-1003)。
- **ECC 默认开启**:`gd5f2gq5ue_init()` 会置 `Feature(B0h).ECC_EN=1`,读/写均走内部 ECC。
- **坏块以 BBT 管理**:出厂坏块标记在每块的第一页(page 0)spare byte 0(列地址 0x800,见手册 §12.4 / Table 12-6 / Note);`init` 阶段扫描构建 RAM 中的 BBT。
- **坏块以 BBT 管理**:出厂坏块标记在每块的第一页(page 0)spare byte 0(列地址 0x800,见 `docs/芯片手册/GD5F2GQ5UExxG.md` §12.4 / Table 12-6 / Note);`init` 阶段扫描构建 RAM 中的 BBT。
### 芯片参数(关键参数)
@@ -706,7 +706,7 @@ ECC 状态(`ECCS1:ECCS0`):`00`=无错;`01/10/11`=纠正 1/2/3 bit;`10`
| **测试步骤** | 1. `gd5f2gq5ue_bbt_rebuild()`(关 ECC → 重新扫描出厂坏块 → 开 ECC → `gd5f_bbt_save` 持久化,覆盖注入坏块)<br>2. `nand_ftl_format()` 清空 dhara map<br>3. `gd5f_bbt_dump_flash` 校验持久化 BBT 版本 ≥1 |
| **通过标准** | 重建返回 OK 且持久化 BBT 版本 ≥1;设备坏块回到出厂集合,注入坏块被清除 |
| **注意事项** | 运行一次即恢复;恢复后建议断电重启让 init 重新加载干净 BBT。本 TC 默认关闭,避免常规测试每次重置设备 |
| **依赖** | `gd5f2gq5ue_bbt_rebuild`(详见 §4.8 持久化设计) |
| **依赖** | `gd5f2gq5ue_bbt_rebuild`(详见 §3 坏块管理与 BBT 持久化) |
---
@@ -3,8 +3,8 @@
> 依据:本机 ST-LINK Utility 4.5.0.0 自带 `ST-LINK_CLI.exe`(CLI 版本 v3.5.0.0,`-h` 全文已实测核对)+ ST 官方文档语义。
> 探针路径:经 `DTU_STLINK_CLI` 环境变量解析(本机设为标准安装位 `C:\Program Files (x86)\STMicroelectronics\STM32 ST-LINK Utility\ST-LINK Utility\ST-LINK_CLI.exe`;未设时回退 PATH,`stlink_flash.py` 均可用)
> 2026-09-19 已上板实测(ST-LINK/V2,FW V2J37S7,SN `0B250D12A218303030303032`;STM32F407ZGTx 目标,SWD)。
> 2026-09-20 复测(V0.1 板):烧写静默失败根因闭环、完整启动闭环(详见 §10)。
> ⚠️ **本机接线:3 根线(SWDIO / SWCLK / GND),nRST 未接** → 能力边界见 §0,实测记录见 §9。
> 2026-09-20 复测(V0.1 板):烧写静默失败根因闭环(见 §2 ⑤ 与 §7)、完整启动闭环(见 §5)。
> ⚠️ **本机接线:3 根线(SWDIO / SWCLK / GND),nRST 未接** → 能力边界见 §0,实测记录见 §5。
> 文中标注 ⓞ 的为"官方帮助/文档说法"与"本机实测"不一致处的更正。
---
@@ -108,7 +108,7 @@ ST-LINK_CLI -c SWD -P "MDK-ARM\STM32F407-Demo\STM32F407-Demo.hex" -Rst -Run
- ⓞ **`.hex` 直接支持已实测**:1MB Intel HEX 编程耗时 **6.5s**,`Programming Complete`(远快于 UV4/CMSIS-DAP 路径);`.bin` 同理(`fromelf --bin` 可转,ARMCC 自带 `C:\Keil_v5\ARM\ARMCC\bin\fromelf.exe`);`.axf` 直读未测,无需。
- 加 `-V` 编程后校验、`-Q` 静默。
- 与 `stlink_flash.py flash` 同源(都走 ST-LINK_CLI)。原 `download.bat`(`UV4 -f` 走 CMSIS-DAP)与 `reset.bat` **均已删除**(2026-10-10),烧录/复位/回读统一由 `stlink-flash` skill 承载。
- ⓞ **静默失败模式(2026-09-20 实测,代价最大的坑)**:省略结尾 `-Rst -Run` 的裸 `-c SWD -P <文件>` 仍报 `Memory programmed in 1s 906ms / Programming Complete`,但芯片 flash **不变**(回读向量表 = 旧镜像)——目标核处于死循环/异常状态时,"Normal 连接、不复位"的会话写入不落地;同文件改用完整 `-P <文件> -Rst -Run` 即正常落地(6.5s / 1MB)。**铁律:任何编程命令必须以 `-Rst -Run` 收尾;任何烧录之后必须 `-CmpFile` 全片回读校验**(详见 §10)。
- ⓞ **静默失败模式(2026-09-20 实测,代价最大的坑)**:省略结尾 `-Rst -Run` 的裸 `-c SWD -P <文件>` 仍报 `Memory programmed in 1s 906ms / Programming Complete`,但芯片 flash **不变**(回读向量表 = 旧镜像)——目标核处于死循环/异常状态时,"Normal 连接、不复位"的会话写入不落地;同文件改用完整 `-P <文件> -Rst -Run` 即正常落地(6.5s / 1MB)。**铁律:任何编程命令必须以 `-Rst -Run` 收尾;任何烧录之后必须 `-CmpFile` 全片回读校验**(详见 §7)。
### ⑥ 不扰目标连接(HOTPLUG,运行中板子的只读检查)✅ 本机可用
@@ -207,7 +207,7 @@ ST-LINK_CLI -c SWD -P "MDK-ARM\STM32F407-Demo\STM32F407-Demo.hex" -Rst -Run
| `stlink_flash.py halt` | `-c SWD -Rst -Halt` | ✅ 复位停核(一次性会话,读状态须同命令行附加;断开后核自动恢复运行) |
| `stlink_flash.py run` | `-c SWD -Run` | ✅ 恢复被停核的目标 |
| `stlink_flash.py list` | `-List` | ✅ 枚举探针 |
| `stlink_flash.py flash [F]` | `-c SWD -P F -Rst -Run`(F 缺省 = 档案 `hex` = `MDK-ARM\STM32F407-Demo\STM32F407-Demo.hex`) | ✅ 烧写 + 复位 + 运行(1MB ≈ 6.5s;2026-09-20 复测验证,见 §10) |
| `stlink_flash.py flash [F]` | `-c SWD -P F -Rst -Run`(F 缺省 = 档案 `hex` = `MDK-ARM\STM32F407-Demo\STM32F407-Demo.hex`) | ✅ 烧写 + 复位 + 运行(1MB ≈ 6.5s;2026-09-20 复测验证,见 §2 ⑤) |
| `stlink_flash.py cmp [F]` | `-c SWD -CmpFile F 0x08000000` | ✅ 全片回读比对(`No difference found` = 真写入) |
退出码:`0`=OK;`1`=探针/目标错误;`2`=CLI 未找到/无效;`3`=用法错误。
@@ -221,8 +221,8 @@ ST-LINK_CLI -c SWD -P "MDK-ARM\STM32F407-Demo\STM32F407-Demo.hex" -Rst -Run
| `[RM=xxx]: Unknown debug protocol or option!`(exit=1) | ⓞ v3.5.0.0 CLI 不支持 `RM=`(帮助与实现不符);**且本机 nRST 未接线**——直接用软件复位 `-Rst`(`stlink_flash.py reset`) |
| 连接失败 | 目标供电;SWDIO/SWCLK 接线(本机 3 线,勿依赖 nRST 辅助);芯片休眠 → 加 `LPM`;看门狗反复复位 → 提高连接速度(本机无 `UR` 可用);RDP 被锁 → 只能读有限信息 |
| 复位后程序没跑 | 读复位向量确认 Flash 有效:`-R32 0x08000000 1`(应指向 0x08xxxxxx,`0xFFFFFFFF`=空片);查 BOOT0 状态;看串口是否有输出 |
| 复位后卡死(串口日志中断在半路) | ⓞ 2026-09-19 实测案例:`-c SWD HOTPLUG -SCore -CoreReg` 读 PC/寄存器定位卡点(案例:V2 引脚固件卡在 0x08000FA0 启动循环,见 §9)——**先定位再下结论**,勿直接怀疑探针 |
| 烧录报成功但芯片行为不变 | ⓞ **先怀疑静默写入失败**(2026-09-20 实测):命令缺 `-Rst -Run`(裸 `-P` 在目标核死循环/异常时不落地)或选项字节异常(RDP 字节非法,见 §10.4)。处置:`-CmpFile` 全片比对/回读向量表首字 → 用 `stlink_flash.py flash` 重烧 → 再 `-CmpFile` 复核;勿直接怀疑探针/接线 |
| 复位后卡死(串口日志中断在半路) | ⓞ 2026-09-19 实测案例:`-c SWD HOTPLUG -SCore -CoreReg` 读 PC/寄存器定位卡点(案例:V2 引脚固件卡在 0x08000FA0 启动循环,见 §5)——**先定位再下结论**,勿直接怀疑探针 |
| 烧录报成功但芯片行为不变 | ⓞ **先怀疑静默写入失败**(2026-09-20 实测):命令缺 `-Rst -Run`(裸 `-P` 在目标核死循环/异常时不落地)或选项字节异常(RDP 字节非法,见 §3 与 §7)。处置:`-CmpFile` 全片比对/回读向量表首字 → 用 `stlink_flash.py flash` 重烧 → 再 `-CmpFile` 复核;勿直接怀疑探针/接线 |
| `daemon unresponsive after auto-start`(serial_tool) | ⓞ 复位后几秒内 daemon 忙于排空日志突发;稍等后直接读 `state/serial.log` 文件验证,勿反复重试 |
| `port not found: COMx (available: COMy)`(serial_tool) | ⓞ 端口重新枚举(板子重插常见):改 `.serialtest/config.json` → **kill 旧 daemon 进程** → 重新 `open`(daemon 启动时缓存配置,不重启不生效) |
| `log --grep` 查不到明明存在的标记 | ⓞ 该子命令实测不可靠(0 命中);改用直接读/`Select-String` 查 `state/serial.log` |
+2 -2
View File
@@ -1,7 +1,7 @@
# M2 链路延迟实测数据(Phase 13,安静档)
本目录保存 M2(Phase 13:芯片 + 队列时延基线)的**原始实测产物**。判定阈值与解读见
[`../链路延迟测试说明.md`](../链路延迟测试说明.md) §6.2;缺陷总表见 §8.1。
[`../链路延迟测试说明.md`](../链路延迟测试说明.md) §6.2;缺陷总表见 `docs/测试说明/链路延迟测试说明.md` §8。
## 1. 运行元数据
@@ -115,6 +115,6 @@ frame#10:first=16.522ms batches=[1024,1024,1024,137] tail_lag=+55.7ms
## 7. 未纳入本目录的数据
- run1~run11 为缺陷定位过程(D11~D16 见测试说明 §6.1),其中 run10/run11 的 L7 失败、
- run1~run11 为缺陷定位过程(D11~D16 见 `docs/测试说明/链路延迟测试说明.md` §6.1),其中 run10/run11 的 L7 失败、
run9 的 170s 连接拒绝均已定位并修复;
- 压力档(`--ftp-stress`,E-02 复现)与 `--mode remo`(生产 RemoDispBus 客户端)属 M3。
@@ -344,7 +344,7 @@ Get-Content '.dsh\skills\serial-assistant\scripts\state\daemon.log' -Tail 30 #
- **daemon 会一直占着内存吗?** 一个 Python 进程约 20MB;不需要时 `stop` 即释放。常驻与否都不影响正确性。
- **daemon 被强杀(taskkill)会怎样?** 端口由系统自动释放;下次任何命令自动拉起新 daemon。**串口不会自动重开**,需要时再 `open`(这是设计:避免在设备异常时盲目重连)。
- **能同时开两个串口吗?** v1 单口(一个 daemon 管一个口)。多口需求见设计文档 §8 v2 候选。
- **能同时开两个串口吗?** v1 单口(一个 daemon 管一个口)。多口需求见 `docs/测试说明/串口助手设计说明.md` §8 v2 候选。
- **能直接删 `serial.log` 吗?** 可以,但代际 `gen` 不会变、写句柄不变——建议用 `clear`(原子 + 代际 +1 + 标记行)。
- **为什么 TX 日志里是字面 `\r\n` 而不是真换行?** 日志按行存储;真换行会把一条 TX 劈成两行,转义后一条 TX 恒一行。
- **ST-Link 烧录会抢串口吗?** 不会(烧录走 SWD,不走 COM)——这正是"串口全程保持打开、烧录前后不重开"的设计基础,也彻底绕开了 CH340 释放/重开陷阱。
@@ -113,7 +113,7 @@
|---|---|---|---|
| **V2** | 任务饿死 | 临时在 `netTask` 循环插 `while(1);` | 800 ms 后串口 `[ERR] WATCHDOG: task stall mask=0x...`;≤2.5 s 复位;重启后 `[WD] reset cause=watchdog reset` |
| **V4** | 关中断死循环 | 临时插 `taskDISABLE_INTERRUPTS(); while(1);` | ≤2.5 s 复位(检验 IWDG **独立于中断**);`cause=watchdog reset` |
| **V5** | 长期不误杀(**核心验收项**) | FTP 大文件传输 + UI 菜单遍历 + 配置保存 + 网络重配 **≥30 min** | **零复位**(重点验证 §4.4 白名单是否完备) |
| **V5** | 长期不误杀(**核心验收项**) | FTP 大文件传输 + UI 菜单遍历 + 配置保存 + 网络重配 **≥30 min** | **零复位**(重点验证 `docs/子系统设计/看门狗设计说明.md` §4.3 白名单是否完备) |
> ⚠️ **V5 是核心验收项**:误杀比不杀更有害。必须在**真实业务负载**下跑足时长。
@@ -124,7 +124,7 @@
# 1) 基线:跑一轮产品 @test 确认无复位
& '.venv\Scripts\python.exe' .dsh/skills/serial-boot-test/scripts/run_boot_test.py
# 2) 注入:按设计说明 §10 在 netTask 循环插 while(1);(或 taskDISABLE_INTERRUPTS(); while(1);)
# 2) 注入:按 `docs/子系统设计/看门狗设计说明.md` §7 在 netTask 循环插 while(1);(或 taskDISABLE_INTERRUPTS(); while(1);)
# —— 改完 @build,然后烧录
& '.venv\Scripts\python.exe' .dsh\skills\keil-build\scripts\keil_build.py
& '.venv\Scripts\python.exe' .dsh\skills\stlink-flash\scripts\stlink_flash.py flash MDK-ARM\STM32F407-Demo\STM32F407-Demo.hex
@@ -148,7 +148,7 @@ $cli = if ($env:DTU_STLINK_CLI) { $env:DTU_STLINK_CLI } else { (Get-Command ST-L
**风险/回滚**:
- 故障注入用例**会复位设备多次**——确认设备非在用/非已部署。
- 长跑涉及 NAND 写入(FTP 上传)**消耗 NAND 块**——若设备已接近容量上限,改用只读传输。
- ⚠️ **R5 风险**(设计说明 §9):NAND 擦写偶发超 800 ms 可能误复位——若 V5 出现偶发复位,
- ⚠️ **R5 风险**(`docs/子系统设计/看门狗设计说明.md` §9):NAND 擦写偶发超 800 ms 可能误复位——若 V5 出现偶发复位,
先按设计说明增大容忍次数或把存储操作改分段置位,**不要盲目放宽全局超时**。
---
+2 -2
View File
@@ -51,7 +51,7 @@
| # | 差距 | 模拟器行为 | 真机行为 | 影响 | 回归补测 |
|---|---|---|---|---|---|
| L-1 | **控制台单写者无对应实现** | `simulator/dbg_log.h` 把 `DBG_*` 映射到 `printf`;**不编译** `console_log.c` | 单写者环形缓冲 + defaultTask 唯一发送 | `console_log` 的环形缓冲溢出/丢弃计数逻辑**模拟器测不到**;共享代码若误调 `console_log.h` 声明,模拟器链接失败(已被此约束捕获) | `@test` + 板端串口观测;`docs/子系统设计/控制台单写者日志设计说明.md` §219 |
| L-1 | **控制台单写者无对应实现** | `simulator/dbg_log.h` 把 `DBG_*` 映射到 `printf`;**不编译** `console_log.c` | 单写者环形缓冲 + defaultTask 唯一发送 | `console_log` 的环形缓冲溢出/丢弃计数逻辑**模拟器测不到**;共享代码若误调 `console_log.h` 声明,模拟器链接失败(已被此约束捕获) | `@test` + 板端串口观测;`docs/子系统设计/控制台单写者日志设计说明.md` §4.1 |
| L-2 | **mtime 粒度不同** | 宿主文件 mtime(**秒级**) | FAT 打包时间(`fdate<<16\|ftime`,**分钟级**) | `cfg_file_poll` 的 `(size, mtime)` 变更令牌在两端触发时机不同;分钟级粒度下的"同分钟内两次改动"在真机可能被判为无变化 | 板端 STORAGE 套件;`配置文件设计说明.md` §变更检测 |
| L-3 | **无应用回调(空操作)** | 模拟器不注册段回调,`cfg_file_poll` 重载后回调为空 | 各段回调真正执行(如网络重配置) | "配置变更 → 触发回调 → 副作用"链路在模拟器上**只验证到写盘**,回调逻辑不覆盖 | `@test`(配置保存 → UI 生效) |
| L-4 | **堆后端不同** | `cfg_store_alloc` = 宿主 `malloc`(堆余量以 MB 计) | FreeRTOS 堆(`configTOTAL_HEAP_SIZE` 48 KB,实测最低水位 ~4.8 KB) | **分配失败路径在模拟器上永不触发**——P0-1 类"KB 级分配必然返回 NULL"缺陷模拟器照不出来(`AGENTS.md` 已列为铁律) | 内存预算门禁 + 真机 `[HEAP]` 水位告警(S11) |
@@ -66,7 +66,7 @@
| H-4 | **SD2506 RTC** | 无 | I2C RTC:时间读写、闹钟、温度、电池电压 | 时间源相关(`sys_clock_init` 降级路径)在模拟器上不可触发 | 板端 RTC 套件(18 断言点)+ `sys_clock` L0.5(已覆盖算术面) |
| H-5 | **TPAFE5160 ADC** | 无 | SPI ADC + BUSY EXTI 中断 | 中断驱动采集链路 | 待功能实现(上游 B1) |
| H-6 | **RS-485 半双工** | 无 | UART4 + DE 方向控制,DWT 微秒延时 | DE 收发切换时序(特有陷阱) | 待功能实现(上游 B2) |
| H-7 | **启动段硬件初始化** | 只跑纯软件部分 | I2C/SPI 外设初始化、BBT 扫描耗时、FatFS 挂载 | 启动段死机(无看门狗保护窗口)**只能在真机复现** | 真机开机日志判读(`项目启动顺序管理.md` §6.8,S10 落地) |
| H-7 | **启动段硬件初始化** | 只跑纯软件部分 | I2C/SPI 外设初始化、BBT 扫描耗时、FatFS 挂载 | 启动段死机(无看门狗保护窗口)**只能在真机复现** | 真机开机日志判读(`项目启动顺序管理.md` §6,S10 落地) |
### 2.4 哪些差异是"有意的对齐"(非缺陷)
@@ -529,7 +529,7 @@ flowchart LR
> 新增任务/栈/缓冲/代码导致的体积增长**不会在 CI 被拦**。故该跳过以 `::warning::` 明示
> (不是弱化的 notice)并写入 step-summary。**兜底必须靠本地**:改代码后 `@build`(0/0)
> → `python scripts/check_mem_numbers.py --scan`。详见
> [`../环境与部署/CI配置指南.md`](../环境与部署/CI配置指南.md) §0.0。
> [`../环境与部署/CI配置指南.md`](../环境与部署/CI配置指南.md) §0。
### 9.2 两个 job
@@ -93,7 +93,7 @@ test/host/watchdog/watchdog_test.c
| **门禁层级** | **L0.5 层**(介于 L0 静态门禁与 L1 板端套件之间):主机侧、秒级、纯逻辑,比板端套件便宜、比静态检查更强 |
| **pre-commit 钩子** | `watchdog-unit-test`(本地钩子,`.pre-commit-config.yaml` §7) |
| **汇总行** | `=== Task Watchdog Tests: N/N PASSED (failed=0) ===` |
| **与板端测试的关系** | 互补:本单测保证**判定逻辑**正确;板端 `@test` 故障注入(V2/V4,见 `子系统设计/看门狗设计说明.md` §7 验证记录)保证**真机复位链路**正确 |
| **与板端测试的关系** | 互补:本单测保证**判定逻辑**正确;板端 `@test` 故障注入(V2/V4,见 `docs/子系统设计/看门狗设计说明.md` §7 验证记录)保证**真机复位链路**正确 |
---
+1 -1
View File
@@ -82,7 +82,7 @@
| 抓到日志缺 `Network Init` | `-SettleMs` 太小(boot 任务间有 ~3-4s 自然静默间隙),用默认 8000ms;2026-10-10 起首字节到达前不启动静默计时(`--first-wait-ms` 默认 12000),首行晚到不再被提前收尾 |
| 烧录后无数据(`no_data`) | 2026-10-10 起 `no_data` = **本次等待期间零字节**(旧实现比 daemon 累计字节,常驻 daemon 下永不触发,只能靠"日志里没有 [RX] 行"兜底);开串口 + clear 必须在烧录**前**完成(`run_boot_test.py` 已保证);核对 COM 号、板子供电,看 `state/serial.log` 有无 SYS 异常行 |
| `[MISS] fw build: <戳>`(其余标记全中) | 芯片里跑的不是这份构建:烧录没落地 / 烧错文件 / hex 未重编译。处置:`stlink_flash.py flash <hex>` 重烧并看 `READBACK: OK`;确认 `@build` 已重跑(戳取自编译时刻) |
| `READBACK: FAIL` / `flash readback mismatch` | 芯片内容与 hex 逐字节不一致(写入未落地、选项字节/RDP 异常、烧错目标)。处置:按 `docs/测试说明/ST-LINK_CLI_复位使用说明.md` §10 排查;`stlink_flash.py flash` 重烧后闭环会自动复核 |
| `READBACK: FAIL` / `flash readback mismatch` | 芯片内容与 hex 逐字节不一致(写入未落地、选项字节/RDP 异常、烧错目标)。处置:按 `docs/测试说明/ST-LINK_CLI_复位使用说明.md` §7 排查;`stlink_flash.py flash` 重烧后闭环会自动复核 |
| `boot banner x2 > allowed 1` | 采集窗口内板子自己复位过(IWDG 咬狗 / Fault 后 `NVIC_SystemReset` / 掉电重启)。处置:看判定里逐条打印的 `reset cause:` 定位;跑**故意复位**的套件用 `--max-boot-count N` 放宽 |
| `marker order violated` | 标记全中但**先后顺序颠倒** = 某步在另一步之后补做(就绪屏障失序 / 失败重试补跑)。处置:看判定打印的各标记首次行号定位;`--require-order` 只在需要强约束时启用 |
| `boot time N ms > limit` | 能开机但慢(mount 重试 / PHY 重协商 / 复位重试)。实测基线:`net init: OK` 在 **+537ms**(2026-10-10 台架);默认只报告不判(`--max-boot-ms 0`),要设门禁再给预算 |
+12 -12
View File
@@ -26,7 +26,7 @@ MCU 侧真实处理只占 6.7ms,任务拾取/调度约 2.3ms(2026-09-22 重
非 Windows 用"入站轻推"兜底(**29.4ms**,2.3×);协议与固件都不需要改(§7.4)。
- **设备侧无可行解**(帧长 3209 为质数、TX FIFO = MSS、CH395F 无 Nagle 开关)→ 不必再投入。
- **缺陷**:D10(断线重监听 588ms 冻结 netTask)、E-02(一条空闲 FTP 控制连接冻结全网 17.7s)、
D17(INIT 应答帧字节数字段恒 0)等已量化并**修复 + 台架复验**(§8.1);
D17(INIT 应答帧字节数字段恒 0)等已量化并**修复 + 台架复验**(§8);
D1(重传周期 500→200ms)/ D6(per-recv 日志)2026-09-22 已实施(§10.1);D20 已定位未修(§8.2)。
---
@@ -642,7 +642,7 @@ sock4 auto-relisten ok / relisten retry ok ← 监听器随后自行恢复
|---|---|---|
| 每帧都**稳定**慢 ≈150~300ms | 结构性基线(§10.1 的 D2/D5/D6/D8) | 跑 §5 的 Phase 13/14 对照基线 |
| **间歇**卡,且 ≈200ms 的整数倍(旧固件 500ms) | **D1** 丢包重传 | Wireshark `tcp.analysis.retransmission`;查 PHY/网线/Wi-Fi |
| **间歇**整屏冻结数秒~数十秒 | **E-02** 头阻塞(已修)/ D10 重连(已修)/ 回归 | 查 FTP/对时会话是否挂着;核对 §8.1 修复在位 |
| **间歇**整屏冻结数秒~数十秒 | **E-02** 头阻塞(已修)/ D10 重连(已修)/ 回归 | 查 FTP/对时会话是否挂着;核对 §8 修复在位 |
| 只在开 FTP / 对时后卡 | E-02 + 单连接 socket 争用 | 临时停 ftpTask/timeSyncTask 对照 |
| 换网口/换线/换 PC 就好 | D7 物理链路 | §10.2-S3 拓扑纪律 |
@@ -652,20 +652,20 @@ sock4 auto-relisten ok / relisten retry ok ← 监听器随后自行恢复
|---|---|---|
| L1/L2 超 crit | SPI 走线/PCB/SPI2 分频或 DMA 配置 | 对照 `docs/问题记录/CH395F_Trap_Records.md`;逻辑分析仪抓 SCK/CS |
| L3 spike ≥200ms(连续台阶;旧固件 500ms) | **D1 重传 / D7 链路** | Wireshark `tcp.analysis.retransmission`;查 PHY 协商/网线/Wi-Fi |
| L3 基线(非 spike)抬高 | 芯片发送路径 / 中断响应 | 对照 CH395F 手册 §5.17/5.18(注意:状态寄存器**不被 SET 类命令更新**,`GET_CMD_STATUS` 恒读到 RESET 的 0x10 陈旧残留,2026-09-22 台架实证——不可用作 SET 值验收) |
| L3 基线(非 spike)抬高 | 芯片发送路径 / 中断响应 | 对照 `docs/芯片手册/CH395F.md` §5.17/5.18(注意:状态寄存器**不被 SET 类命令更新**,`GET_CMD_STATUS` 恒读到 RESET 的 0x10 陈旧残留,2026-09-22 台架实证——不可用作 SET 值验收) |
| L5 压力档爆表 | **E-02 头阻塞(已修)→ 回归** | 定位触发源(阻塞 recv 的 FTP/对时);核对 L3 pending 在位(`net_recv_locked` 阻塞分支 = 登记 pending 而非等待;`net_pending_tick` 在主循环) |
| L5/L6 静默档抬高 | D5 调度(任务抢占/节拍) | 查任务优先级与 osDelay;`stack_report.py` 核对栈水位 |
| L6/L7 整体抬高且 L1~L5 正常 | 线路+PC 段(L4) | Wireshark 段间间隙;`Δ` 对照定位 MCU 侧净贡献 |
| **L7/L8 尾批固定迟到 ~50ms** | 已定性(§7.3):次 MSS 尾段被 Nagle 扣住 + Windows 延迟 ACK | 客户端侧解法已实测(§7.4):`--ack-probe` / `--ack-fast` → tail 52→3.9ms;生产客户端已实装;**设备侧无解**,不必再投入 |
| **L5 出现重复投递**(收到 > 发送) | **D11**:芯片 TCP RX 饱和重复递交 | 降低灌入速率/加抽干(§8.1 D11) |
| **会话无征兆失步**(结果错位/挂死) | **D12**:net 层任务通知值语义 | 核对 `net_wait_result` 清除掩码全 1 + 按返回值判定(§8.1 D12/D12-a) |
| **L7 一直超时 / peer 报 `l7_crc.fail`** | **D15**:帧格式不一致 / peer 超读 | 对照 `ui_net_sink.c` 的 `remo_send`/`remo_handle`(§8.1 D15) |
| **会话跑完一段后无限循环、peer 空等 300s** | **D16**:Phase 13 状态机迁移目标错 | 核对顺序 L5→L3→L10→L6→L7→DONE(§8.1 D16) |
| **`[LAT]` 段行缺失/截断、wait-idle 判不出空闲** | **D12-b**:热路径日志洪水 | 删除 net 热路径逐调用日志(§8.1 D12-b) |
| **L5 出现重复投递**(收到 > 发送) | **D11**:芯片 TCP RX 饱和重复递交 | 降低灌入速率/加抽干(§8 D11) |
| **会话无征兆失步**(结果错位/挂死) | **D12**:net 层任务通知值语义 | 核对 `net_wait_result` 清除掩码全 1 + 按返回值判定(§8 D12/D12-a) |
| **L7 一直超时 / peer 报 `l7_crc.fail`** | **D15**:帧格式不一致 / peer 超读 | 对照 `ui_net_sink.c` 的 `remo_send`/`remo_handle`(§8 D15) |
| **会话跑完一段后无限循环、peer 空等 300s** | **D16**:Phase 13 状态机迁移目标错 | 核对顺序 L5→L3→L10→L6→L7→DONE(§8 D16) |
| **`[LAT]` 段行缺失/截断、wait-idle 判不出空闲** | **D12-b**:热路径日志洪水 | 删除 net 热路径逐调用日志(§8 D12-b) |
| **开机日志连续丢行**(含必检标记 `net init: OK` 等)但系统健康 | **日志并发竞态**:多任务并发 `printf` → 非重入 `fputc`/`HAL_UART_Transmit` 丢整行、UART 状态短暂卡住(2026-09-22 台架实证:boot 时 testTask 启动行与 netTask 日志并发 → 连续 4 行丢失、~2s 后自恢复) | 必现证据行采用"静默采集 + 延迟到安静窗口打印"模式(范例:P0-a 验收行曾按此模式实装,验收完成后探针已移除);根治 = S8 日志非阻塞/串行化(§10.2) |
| **`relisten` 段 FAIL、断线固定 ~588ms** | **D10** 回归(退回 `osDelay(500)`) | 核对 `net_relisten_try`/`net_relisten_retry_tick` 非阻塞重试在位(§8.1 D10) |
| **`relisten` 段 FAIL、断线固定 ~588ms** | **D10** 回归(退回 `osDelay(500)`) | 核对 `net_relisten_try`/`net_relisten_retry_tick` 非阻塞重试在位(§8 D10) |
| **远端显示整帧卡住十几秒后自恢复** | **E-02** 回归 | 核对 L3 pending 在位:`net_recv_locked` 阻塞分支登记 pending(哨兵 `NET_PENDING_REGISTERED`)+ `net_pending_tick` 在主循环 + 调用方剩余预算循环(lftpd/timeSync)(§8.3 要点 4) |
| **INIT 应答帧字节数字段恒为 0** | **D17** 回归 | 核对 `remo_send_init` 发满 8B(§8.1 D17) |
| **INIT 应答帧字节数字段恒为 0** | **D17** 回归 | 核对 `remo_send_init` 发满 8B(§8 D17) |
| **开机首个 FTP 会话第 2 个数据连接 425** | **D20**(已定位未修,§8.2) | 复现:`test/host/ftp_regression.py`(复位开边界);现场规避:客户端重试 425(瞬态 4xx);根治方向:数据 Socket 轮换 |
| **`[LAT]` 多段数值雷同 / 全 0 却 verdict=OK** | **D18/D19**(测试工装) | 核对 `lat_probe_add_seg` 段名过滤 + `lat_time_init` 调用 + `med==0` 判无效 |
| **连接建立后 15~30s 无流量即被 RST** | **D13**:芯片空闲超时(芯片行为) | 客户端须容忍并重连(peer `--wait` 重试是正常路径) |
@@ -677,13 +677,13 @@ sock4 auto-relisten ok / relisten retry ok ← 监听器随后自行恢复
## 10. 结构性成本项与待办方向
### 10.1 静态分析十项结构原因(D1~D10,独立于 §8.1 的台架缺陷编号)与当前状态
### 10.1 静态分析十项结构原因(D1~D10,独立于 §8 的台架缺陷编号)与当前状态
| # | 原因 | 实测验证(2026-09-22 台架) | 当前状态 |
|---|---|---|---|
| D1 | CH395F TCP 重传固定周期(非 RTT 自适应) | 台架链路(有线 100M 全双工)多轮零重传尖峰 → 台架无丢包;**弱网(Wi-Fi/双工不匹配)仍是秒级卡顿首要嫌疑**,200ms 粒度台阶 = 重传指纹(旧固件 500ms) | **已修(P0-a,2026-09-22)**:`NET_RETRANS_PERIOD_MS` 500→**200**(窗口 8×200=1.6s,原 4s)。台架验收说明:`GET_CMD_STATUS` 对 SET 类命令无判别力(500/2000ms 超限对照均读 0x10 陈旧残留,§9.2)→ 接受性以手册范围(≤1000ms)+ 全套件零回归(§6.2/§6.3)+ 与 500ms 行为同构为准。验收探针(3×20ms 状态轮询 + 证据行)验收完成后已从代码移除,结论保留于本文档 |
| D2 | 3209B × 1KB 发送 FIFO 停止等待(4 次串行分块握手,期间 netTask 全程占用) | `chunk_wait` 219µs/块 ≈ SPI 搬运成本的 1/7 → 握手本身**不是瓶颈**;每帧固定成本小(MCU 净 7.1ms) | 基线成本,可接受;根治 = 差量/RLE + 帧版本号(待办 S2) |
| D3 | netMsgQueue 头阻塞(= §8.1 E-02) | 已量化:一条空闲 FTP 控制连接冻结全网 17.7s | **两阶段闭环**:① 5ms 分片有界读 + 让出(f8d6e90/4a1605b,缓解)② **L3 pending 化**(`a45c0cb`,结构性——执行器零占用,缺陷类消灭,§8.3 要点 4) |
| D3 | netMsgQueue 头阻塞(= §8 E-02) | 已量化:一条空闲 FTP 控制连接冻结全网 17.7s | **两阶段闭环**:① 5ms 分片有界读 + 让出(f8d6e90/4a1605b,缓解)② **L3 pending 化**(`a45c0cb`,结构性——执行器零占用,缺陷类消灭,§8.3 要点 4) |
| D4 | PC 端延迟 ACK 与"每段等 ACK"停止等待交互 | `--ack-fast` 带内实验证实:关掉延迟 ACK 尾批自行消失(52→3.9ms) | **已客户端侧解决**(生产客户端关延迟 ACK/轻推兜底,§7.4) |
| D5 | uiTask 节拍:10ms 循环 + 每拍一次 64B recv + step 在 poll 前 | `ui_loop` 10986µs ≈ 设计值(渲染 ~1ms);拾取调度 ~2.3ms(P0-b 删 per-recv 日志后,原 ~6.8ms) | 基线行为;可选优化 S7(poll 前置 + while 排空),低优先 |
| D6 | 阻塞式日志在关键路径(fputc 逐字节 @115200) | `logtax_line` 4402µs/行;per-recv 日志已删(2026-09-22):`queue_rt_recv` 5997→1223µs、帧周期 66.0→61.6ms、首字节 13.9→9.6ms | **部分已修(P0-b,2026-09-22)**;S8 非阻塞日志仍待办(同时是开机日志并发丢行 §9.2 的根治项) |
+1 -1
View File
@@ -341,7 +341,7 @@ github.com 会在 checkout 步骤卡住/失败。三种处置:
| `l0-gates` 报 `::error::runner 缺少:Python, cppcheck` | runner 没装或不在 PATH | 装完后**重启 runner 服务**(新进程才吃到新 PATH) |
| `checkout` 步骤卡住/超时 | 拉不到 `actions/checkout@v4`(内网) | 见 §4.3 |
| `l05` 日志出现 `::warning::runner 未安装 MSVC` | VS 装在非默认路径或未装 | 见 §3.2(默认路径 / 同步改两处根目录) |
| `mem-numbers` 显示 `Skipped` | 正常(CI 无 Keil,无 `.map`) | 无需处理;但注意 **CI 未守体积门禁**(见 §0.0),改代码后本地 `@build` + `check_mem_numbers.py --scan` 兜底 |
| `mem-numbers` 显示 `Skipped` | 正常(CI 无 Keil,无 `.map`) | 无需处理;但注意 **CI 未守体积门禁**(见 §0),改代码后本地 `@build` + `check_mem_numbers.py --scan` 兜底 |
| L0.5 显示 `2 runner(s), X/Y ...` 但 X<Y | 有单测用例未通过 | 看该模块的 `::error::unit test failed` 行与其 `ci-artifacts/l05-<模块>.log` |
| L0.5 报 `produced no '=== ... N/M PASSED ===' summary line` | 该模块运行器未输出汇总行(改了 harness 或运行器崩溃) | 核对 `test/host/common/unit_test_harness.py` 的 `summary_marker` 与该模块 `TestBuild` 是否一致 |
| `Tee-Object` 步骤报 `TerminatorExpectedAtEndOfString` / 字符串缺少终止符 | `run:` 块混入了非 ASCII(GBK 解码错位) | 见文件头 🔴🔴;`run:` 内必须纯 ASCII |
+1 -1
View File
@@ -133,7 +133,7 @@ STM32(U17)为 LQFP144 脚号。
| ADC1 上电自校准 | **已补(2026-10-10)**:`Src/adc.c` 的 `USER CODE BEGIN ADC1_Init 2` 调用 `adc1_calibrate()`(实现同文件 `USER CODE BEGIN 1`,按 RM0090 §13.3.6:清 ADON→等生效→置 ADON→置 CAL→等 CAL 由硬件清零)。⚠ **CubeMX 不生成这一步**,且本仓库裁剪版 HAL **没有** `HAL_ADCEx_Calibration_Start`、裁剪版 CMSIS 头**没有** `ADC_CR2_CAL`(bit3,代码里按 RM 自建 `ADC1_CAL_BIT`)→ 属 L2 手工定制,审计项 `adc1-calibration-call` / `adc1-calibration-impl` 兜底 | 实测有效:校准前后同一拍 STM32 侧 6 路**同向**变化 `UB 898→961`、`UC 978→1038`、`IB 906→922`、`IC 1061→1071`、`IN 1017→1055`、`T 790→846`(+10~+63 LSB ≈ 0.2~1.5 % FS);TPAFE 侧 `UA/UN/IA` 一字不变 → 变化确来自 ADC1 校准。缺这一步不报错,只是**读数带失调** |
| ADC 时钟 | CubeMX 的 F4 ADC 面板**有** Clock Prescaler 一项(此前"面板没有该项、固定 DIV2"的判断有误),已选 **PCLK2/4 → ADCCLK = 21 MHz** ✓。面板里 **DIV2 是灰的、选不到**:DB 里 `FadcConstraint Max=36000000`,APB2 = 84 MHz → DIV2 = 42 MHz 超限,该候选值被 `Action="DISABLE"` 禁掉,超规值进不来 | **不设额外防护(2026-10-09 决定)**:约束已由 CubeMX 把关;`.ioc` 虽不保存 `ADC1.ClockPrescaler` 键,重生成也只会回落到约束允许的值。按 144+12 cycles 算 ≈ 7.4 µs/通道、9 通道一轮 ≈ 67 µs |
| `adc_task` | **已实现(B1,2026-10-10;L1 层 v2.3;P1-5 事件驱动消费,2026-10-10)**:`App/task/adc_task.c` 每拍调 `adc_input_poll()`(`App/algo/adc_input.c`)= TPAFE `tpafe5160_snapshot()` 原子快照(8 通道)+ 消费一帧 STM32 ADC1 DMA 乒乓缓冲,**经 `adc_input_raw()` 按语义取数**(不再直接索引驱动缓冲);**消费唤醒由 TIM3 ISR 置位 `ADC_WAKE_FLAG`**(不再 1 ms 轮询);任务在 `Src/freertos.c` 的 `USER CODE` 区创建为 `adcTask`(512 words / **AboveNormal**) | 数据路径已通(台架 `@test`:`frames` 严格 1000 Hz 递增、**`idle=0`**、`adc1_rc=0` / `adc1_err=0`) |
| TPAFE 采样触发 | **周期 CONVST 已实现(2026-10-10)**:`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 **TIM3 更新事件(1 kHz)** 内调 `tpafe5160_start_conv_irq()`;`app_main_init()` 用 `HAL_TIM_Base_Start_IT(&htim3)` 启动节拍(在 `tpafe5160_init()` 之后)。同一 TIM3 更新事件还经 **TRGO** 硬件触发 ADC1 一轮 9 通道(DMA 搬运),并调 `adc_tick_irq_notify()` **唤醒消费者**(P1-5 方案 a,2026-10-10;内含"消费者就绪"门)→ 两片转换器由同一硬件事件同源对齐 | 连续波形(1 kHz);详见 `子系统设计/ADC采样节拍设计说明.md` §10 / §11 |
| TPAFE 采样触发 | **周期 CONVST 已实现(2026-10-10)**:`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 **TIM3 更新事件(1 kHz)** 内调 `tpafe5160_start_conv_irq()`;`app_main_init()` 用 `HAL_TIM_Base_Start_IT(&htim3)` 启动节拍(在 `tpafe5160_init()` 之后)。同一 TIM3 更新事件还经 **TRGO** 硬件触发 ADC1 一轮 9 通道(DMA 搬运),并调 `adc_tick_irq_notify()` **唤醒消费者**(P1-5 方案 a,2026-10-10;内含"消费者就绪"门)→ 两片转换器由同一硬件事件同源对齐 | 连续波形(1 kHz);详见 `docs/子系统设计/ADC采样节拍设计说明.md` §10 / §11 |
| 前端偏置 | 每路 1k 串联 + 1k 到 `*_GND` + 滤波 C,**无中点偏置** | STM32 侧只能测 **0~3.3 V 单极性**,交流量负半周被削顶;主板侧只做监视可接受,若要算 RMS/功率需硬件补偏置或明确算法口径 |
| TPAFE 量程 | 默认 ±5 V(RANGE 接 GND),16 位二进制补码,LSB ≈ 152.59 µV | 用 `tpafe5160_to_voltage()` 换算 |
| 取数契约(**2026-10-10 已修**,E-44 / E-45 / E-58 闭环) | 旧的 `get_buf` / `data_ready` / `clear_ready` **已删除**(删除前确认全仓库零调用者):`get_buf` 只给指针、任务再拷 → 被 BUSY EXTI 抢占就是**两帧混合**;`s_ready` 电平标志被 `start_conv_irq` 触发前清 0 → **重触发即丢未消费帧**;`read_all` 从不看 FRSTDATA(宏有定义、代码零引用)→ 芯片死亡时返回 OK + 常量垃圾。现口径:**`tpafe5160_snapshot(dst, count)`**(临界区原子拷贝,返回 1/0/-1,**仅任务上下文**)+ **`tpafe5160_frame_seq()`**(帧序号只增不清)+ **`tpafe5160_frstdata_err()`**(无跳变帧计数);`tpafe5160_init()` 增**首帧自检**——一帧读下来 FRSTDATA 无跳变(芯片不驱动并行总线;板上 R24 4.7k 上拉会把它钉死为高)→ 返回 `-3` + `[ERR]` | 消费者**只能**用 `snapshot()` 取数(`AGENTS.md` 已立为铁律);真机实测开机 `[ADC] Init OK` 现只在自检帧通过后才打印 → 判据在本板成立、非空转 |
@@ -277,7 +277,7 @@ flowchart LR
- 器件为 **SD2506API 模块**(非裸片):内置 32.768k 晶振 + **内置充电电池 + 充电电路**(手册 L11:电池满充常温下掉电走时约 8 个月,电池寿命 5~8 年;自动电源切换:VDD<2.4V 且 VDD<VBAT 时自动转电池供电,PMF 位指示电源模式)。
- **封装与引脚编号(已核实,设计正确)**:实物封装 6 个引脚,但编号**不按 1~6 顺序**——模块只引出手册脚号 **1/4/5/6/7/8**(表 1:1=TEST, 4=GND, 5=SDA, 6=SCL, 7=INT, 8=VDD;pin2/3 未引出)。网表接 4/5/6/8 = GND/SDA/SCL/VDD ✓,1(TEST,手册要求悬空)与 7(INT,未用)悬空 ✓,与手册完全一致。
- **掉电走时(已核实,无缺陷)**:后备电源为模块内置充电电池(无外接 VBAT 脚),不存在"无后备电池"问题;18H 充电寄存器非易失(手册 §4.8:出厂均配置 82H = EN_Charge+5K 电阻),长时掉电可能改值——代码每次上电**回读校验 18H**(=0x82 不写;≠0x82 经写允许窗口重置为 0x82 并回读验证,2026-09-21 修复 E-32,`sd2506_init()`),满足手册 L390 每次上电重置 18H 的要求;开机另读电池电压寄存器 1AH/1BH 输出 battery: 标记(`sd2506_get_battery_voltage()`),与手册一致。
- **掉电走时(已核实,无缺陷)**:后备电源为模块内置充电电池(无外接 VBAT 脚),不存在"无后备电池"问题;18H 充电寄存器非易失(`docs/芯片手册/SD2506API-G.md` §4.8:出厂均配置 82H = EN_Charge+5K 电阻),长时掉电可能改值——代码每次上电**回读校验 18H**(=0x82 不写;≠0x82 经写允许窗口重置为 0x82 并回读验证,2026-09-21 修复 E-32,`sd2506_init()`),满足手册 L390 每次上电重置 18H 的要求;开机另读电池电压寄存器 1AH/1BH 输出 battery: 标记(`sd2506_get_battery_voltage()`),与手册一致。
#### U19 = GD5F2GQ5UE(WSON-8,SPI NAND)
@@ -44,10 +44,10 @@
| **B1** 引 MDC/MDIO | ❌ 未落实 | U2.25/26 仍无任何网络 |
| **B2** 主轨改 buck + 5V 排针 + TVS | ❌ 未落实 | U6/U7 SOT-223 不变、G3.3V 双轨不变、无新增 5V 接口/TVS |
| **B3②** SPI1→U18(EXTKO1/2) 耦合改线 | ⚪ **复核后撤销**(无需改线) | 数据手册核对:两线落点(PD8/PD9→PF1/PF2)均无 SPI1 AF 且代码从未配置,实为悬空 GPIO 线,"SPI 流量干扰"不成立;风险并入 B3①(13 条悬空线配置安全电平,§4.4/§11.3) |
| **§3.4** T1 远端 4/5/12/13 对地旁路 | ❌ 未落实 | 仍悬空 |
| **§3.4** 网名 'R16' 与电阻 R16 撞名改名 | ❌ 未落实 | 网络 'R16' 仍存在 |
| **§3.4** TPAFE RESET 改 GPIO | ❌ 未落实 | HRST 仍为 R16 10k + C31 上电 RC 脉冲电路 |
| §5.3 R35 1.5k→10k(视 U8 分档) | —(条件项) | 仍 1.5k,分档未核则维持原判 |
| **V0.0 §3.4** T1 远端 4/5/12/13 对地旁路 | ❌ 未落实 | 仍悬空 |
| **V0.0 §3.4** 网名 'R16' 与电阻 R16 撞名改名 | ❌ 未落实 | 网络 'R16' 仍存在 |
| **V0.0 §3.4** TPAFE RESET 改 GPIO | ❌ 未落实 | HRST 仍为 R16 10k + C31 上电 RC 脉冲电路 |
| V0.0 §5.3 R35 1.5k→10k(视 U8 分档) | —(条件项) | 仍 1.5k,分档未核则维持原判 |
| 代码项(B3① GPIO 配置、功能级断言) | 与网表无关 | 见本文 §9 固件同步清单(重排后工作量变大) |
### 1.4 V0.1 新增关注项(建议编号 V1~V10,纳入待核)
@@ -154,7 +154,7 @@ C45(5V 轨 220µF)仍为 BD6.3,未受影响。
| R16 | 85 (PD14) | **10 (PF0)** | | SDCKE | 109 (PA14) | **28 (PC2)** |
| K3 | 86 (PD15) | **11 (PF1)** | | | | |
B3 性质不变(仍为 A 侧输入悬空驱动子板输出的浮空风险,修复仍是"代码配置 11 个 GPIO 安全电平");**这 11 脚现在占用的是原 LCD 引脚区**,V0.0 报告 §4.1/§5.2 中列的旧脚号在 V0.1 全部作废。⚠ V15/J3/F3 落在 PC13/PC14/PC15(备份域电源开关供电,数据手册 Note 3:**灌流 ≤3mA、≤2MHz、不可作电流源**)——子板 LED 驱动电流若超 3mA 亮度受限,见 V8。另注意 V0.0 报告中"SDCKE=109"的脚在 V0.0 实为 PA14(SWCLK 脚)——与 V0.0 SWCLK 记在 104 同属该板网表 SWD/SDCKE 脚位记录混乱的一部分,V0.1 已全部理顺(§11.3)。
B3 性质不变(仍为 A 侧输入悬空驱动子板输出的浮空风险,修复仍是"代码配置 11 个 GPIO 安全电平");**这 11 脚现在占用的是原 LCD 引脚区**,V0.0 §4.1/§5.2 中列的旧脚号在 V0.1 全部作废。⚠ V15/J3/F3 落在 PC13/PC14/PC15(备份域电源开关供电,数据手册 Note 3:**灌流 ≤3mA、≤2MHz、不可作电流源**)——子板 LED 驱动电流若超 3mA 亮度受限,见 V8。另注意 V0.0 报告中"SDCKE=109"的脚在 V0.0 实为 PA14(SWCLK 脚)——与 V0.0 SWCLK 记在 104 同属该板网表 SWD/SDCKE 脚位记录混乱的一部分,V0.1 已全部理顺(§11.3)。
### 4.6 SWD 与 ADC17(2 网)
@@ -173,7 +173,7 @@ B3 性质不变(仍为 A 侧输入悬空驱动子板输出的浮空风险,
## 5. LED 对应关系变化(6 网,网名不变)
V0.0 报告 §2.2/§4.7 的 6 路 MCU 控制 LED(实板位号 LED5~LED10,阴极经 R59~R64 1k→3.3V,该侧接法不变):
V0.0 §2.2/§4.7 的 6 路 MCU 控制 LED(实板位号 LED5~LED10,阴极经 R59~R64 1k→3.3V,该侧接法不变):
| 网名 | V0.0:U17 脚 → 实板 LED | V0.1:U17 脚 → 实板 LED |
|---|---|---|
@@ -246,7 +246,7 @@ VDD50、5V、C3.3V、CGND、AVDDL(成员同,仅 C3 封装变)、STM_VDDA
3. **ADC 通道映射表重写**(§6):**已落地** → [`docs/硬件平台/ADC通道映射说明.md`](ADC通道映射说明.md)(语义 UA/UB/UC/UN、IA/IB/IC/IN、V_ZL2=温度检测,其余 8 路备用;含 TPAFE `buf[0..7]` 下标映射、STM32 通道宏、自检注入口径)。STM32 侧 9 路中 8 路在 PA0~PA7(IN0~IN7),**网名 ADC17(pin 44 = PC4)实现时用 `ADC_CHANNEL_14`**——原文写的 `ADC_CHANNEL_12` 与"第 17 路 = V_ZL2"均已勘误:V0.1 中 **ADC17 网名承载 AIN2 = UB**,**V_ZL2 走 ADC14 = PA5 = `ADC12_IN5`**。当前代码未启用 STM32 ADC(`HAL_ADC_MODULE_ENABLED` 注释状态),属实现时的注意事项。
4. **裁剪**:删除/停用 LCD(H1 8080)与 KEY1~8 相关代码(若有);面板按键功能如仍需要,需另立需求(V0.1 已无板上按键,仅 SW1 复位保留)。
5. **13 条悬空 GPIO 线配置安全默认电平**(B3①):U18 桥 11 线(PE6/PC13/PC14/PC15/PF0/PF1/PF4/PF5/PF6/PC2/PC3,LED 灭/继电器断/告警静默)+ SPI 镜像 2 线(PF1/PF2);其中 3 线在 PC13/14/15,**只能灌流 ≤3mA**(V8)。
6. **EXTKI×3 读取(如启用)必须配在 PE2/PE3/PE4(脚 1/2/3)**——V0.0 报告 §5.2 写的"PC13/14/15"是引脚搞错(那三脚在 V0.1 已被 V15/J3/F3 占用,见 §11.2);硬件通路(U18 A 侧→PE2/3/4)V0.1 未变。
6. **EXTKI×3 读取(如启用)必须配在 PE2/PE3/PE4(脚 1/2/3)**——V0.0 §5.2 写的"PC13/14/15"是引脚搞错(那三脚在 V0.1 已被 V15/J3/F3 占用,见 §11.2);硬件通路(U18 A 侧→PE2/3/4)V0.1 未变。
7. V0.0 §5.2 功能级断言照做(ADC 注入读回、RS485 回环、CH395F PHY 寄存器读回)。
## 10. 上板验证补充(在 V0.0 §5.1 基础上追加)
@@ -283,7 +283,7 @@ VDD50、5V、C3.3V、CGND、AVDDL(成员同,仅 C3 封装变)、STM_VDDA
**结论**:V0.0 报告"封装引脚编号(NRST=25、HSE=23/24、PB12-15=73-76、VDDA=32/33)与常见数据手册图记忆不符"是**作者记忆偏差**(V0.0 分析时未实际比对在库手册)——网表编号与手册完全一致。因此:
- V0.0 报告 §3.3-3 的**实物抽查 3 脚**(25=NRST、23/24=8MHz 波形、73=PB12)**由手册证据直接关闭**,无需再实测;
- V0.0 §3.3-3 的**实物抽查 3 脚**(25=NRST、23/24=8MHz 波形、73=PB12)**由手册证据直接关闭**,无需再实测;
- 自本报告起,所有网表脚号可直接按手册读引脚名(§4/§11.3 各表已注明);
- 手册 Table 7 中 48/49 为合并行:**48 = PB2/BOOT1、49 = PF11**(F407 无 BOOT1 功能,PB2 为普通 GPIO)。
@@ -292,12 +292,12 @@ VDD50、5V、C3.3V、CGND、AVDDL(成员同,仅 C3 封装变)、STM_VDDA
| 位置 | V0.0 报告原文 | 事实(手册 + 代码核对) |
|---|---|---|
| §4.2 | "INT→PA0(pin 4)" | pin 4 = **PE5**;代码 `CH395_INT_Pin = GPIO_PIN_5 / GPIOE`(main.h L60-61)——一直是 PE5,报告误判为 PA0 |
| §4.6 | "ST_IN_E4/C3/D4→pin 3/2/1 = PC15/PC14/PC13(标准 LQFP-144 编号…)" | pin 1/2/3 = **PE2/PE3/PE4**(即 ST_IN_D4/C3/E4);EXTKI 三线实际在 PE2/PE3/PE4(V0.1 未变);**V0.0 报告 §5.2"启用 PC13/14/15 读 EXTKI"是引脚搞错**——V0.1 中 PC13/14/15 已被 V15/J3/F3 子板 LED 线占用 |
| §4.6 | "ST_IN_E4/C3/D4→pin 3/2/1 = PC15/PC14/PC13(标准 LQFP-144 编号…)" | pin 1/2/3 = **PE2/PE3/PE4**(即 ST_IN_D4/C3/E4);EXTKI 三线实际在 PE2/PE3/PE4(V0.1 未变);**V0.0 §5.2"启用 PC13/14/15 读 EXTKI"是引脚搞错**——V0.1 中 PC13/14/15 已被 V15/J3/F3 子板 LED 线占用 |
| §3.3-3 / §4.1 | "封装引脚编号…与常见数据手册图记忆不符→判定网表正确,待实物抽查" | 与数据手册一致(§11.1);抽查项由手册证据关闭 |
| §4.1 / §5.3 | SWCLK = U17.104 | pin 104 = **PA12(无 JTAG 功能,AF 为 USART1_RTS/CAN1_TX/OTG_DP)**;实测证实 **V0.0 实板 SWD 不通**(用户飞线至 PA14=109 后方可调试)→ **V0.0 真实设计缺陷(B4)**,V0.1 已更正 |
| §4.7 / §1.6 | "ADC17→U17.46" 列为正常通道 | pin 46 = **PB0 = `ADC12_IN8`**(**2026-10-09 勘误:PB0 确为 ADC 输入脚**,原判"非 ADC 输入脚 / 电气上无效"不成立)→ V0.0 的"ADC17"(V_ZL2)通道电气上有效,只是与 LED 复用冲突、代码未读;V0.1 改接 44 = PC4 = **`ADC12_IN14`**(代码用 `ADC_CHANNEL_14`) |
| V0.0 §4.1 / §5.3 | SWCLK = U17.104 | pin 104 = **PA12(无 JTAG 功能,AF 为 USART1_RTS/CAN1_TX/OTG_DP)**;实测证实 **V0.0 实板 SWD 不通**(用户飞线至 PA14=109 后方可调试)→ **V0.0 真实设计缺陷(B4)**,V0.1 已更正 |
| V0.0 §4.7 / §1.6 | "ADC17→U17.46" 列为正常通道 | pin 46 = **PB0 = `ADC12_IN8`**(**2026-10-09 勘误:PB0 确为 ADC 输入脚**,原判"非 ADC 输入脚 / 电气上无效"不成立)→ V0.0 的"ADC17"(V_ZL2)通道电气上有效,只是与 LED 复用冲突、代码未读;V0.1 改接 44 = PC4 = **`ADC12_IN14`**(代码用 `ADC_CHANNEL_14`) |
另:V0.0 报告 §2.2 存储项"GD5F2GQ5UE + SD 卡(FatFs)"表述有误——两版网表均无 SDIO 引脚,代码 FatFs 卷在 **SPI NAND** 上(`App/cfg/cfg_file_store_mcu.c` 头部:"NAND 上 FatFS 文件 /config.ini(卷根)"),主板本无 SD 卡/SDIO。
另:V0.0 §2.2 存储项"GD5F2GQ5UE + SD 卡(FatFs)"表述有误——两版网表均无 SDIO 引脚,代码 FatFs 卷在 **SPI NAND** 上(`App/cfg/cfg_file_store_mcu.c` 头部:"NAND 上 FatFS 文件 /config.ini(卷根)"),主板本无 SD 卡/SDIO。
### 11.3 V0.1 换脚引脚逐脚核对(30 网全部"电气成立")
@@ -350,7 +350,7 @@
| 定值折算 | 定点系数 `2^10` | 与参考一致 |
| 遥测微调 | ±10%,0.1% 步长(TLV10) | 参考口径 |
> **误差来源与预算**(参考《ADC采集至数据使用全流程详解》§12):采样量化(TPAFE LSB ≈ 152.6 µV)、通道标定残差、窗长偏离整周波(频偏)、系数定点舍入。前三项决定可达精度,最后一项可忽略。
> **误差来源与预算**(参考《ADC采集至数据使用全流程详解》[计划] §12):采样量化(TPAFE LSB ≈ 152.6 µV)、通道标定残差、窗长偏离整周波(频偏)、系数定点舍入。前三项决定可达精度,最后一项可忽略。
### 4.3 算力与内存预算
@@ -603,6 +603,6 @@ B0 产品定义 ✅(2026-10-10 已确认,见 §1.5)
| 2026-10-10 | **L1-3/L1-4 落地:STM32 ADC1 扫描 + 语义映射** | 新增 `App/algo/adc_input.c/h`(L1 采集层):① `k_adc_map[]` **语义通道表**(`UA/UN/IA`→TPAFE `buf[6]/[5]/[2]`,`UB/UC/IB/IC/IN/TEMP`→ADC1 `CH14/3/6/1/2/5`)+ `adc_input_desc()`/`adc_input_raw()` **按语义取数**;② `stm32_scan()` STM32 ADC1 9 通道软件触发顺序扫描(`HAL_ADC_Start` + 逐通道 `PollForConversion`/`GetValue`);③ `adc_input_poll()` 统一"TPAFE 快照 + STM32 扫描"。`adc_task.c` 取数改走本层(首帧日志补全 8 路 + 温度、心跳增 `adc1_rc`/`adc1_err`);`app_main.c` 在 `tpafe5160_init()` 后调 `adc_input_init()`(含**映射自检**:STM32 映射通道必落在扫描序列内,失败记 `SYSLOG_SELFTEST`);Keil 工程挂 `adc_input.c` 并加 `../App/algo` 包含路径。§1.2 采样软件行、§3.1 L1-3/L1-4 同步。`@build` 0 错 0 警;台架 `@test` PASS(`ADC input layer init: OK`、`adc1_rc=0`/`adc1_err=0`、`seq` 仍 1000.0 Hz、`adc` 栈峰值 528→584 B、回读一致、零 `[ERR]`/HardFault),证据 `测试说明/boot_log_data/20261010_prod_adc_l1_boot_log.txt` |
| 2026-10-10 | **L2 相量层落地 + L2-2 实例数口径对齐** | 新增 `App/algo/sample_ring.c/h`(L2 输入:按语义通道保存 2 周波 = 40 点环缓冲,整帧写后统一推进写指针)与 `App/algo/phasor.c/h`(L2 核心:全周傅氏单频 DFT,N = 20;系数表**离线预乘 √2/N** → `fAmSQ` 直接为**有效值**;计算区/使用区**双区原子切换**;实例表 `k_inst[]` 数据驱动);主机侧单测 `test/host/phasor/` **57 用例全绿**(`phasor-unit-test` 钩子)。**§3.2 L2-2 行口径对齐**:参考项目为 **4 个实例**,本项目按 V0.1 有效通道展开为 **5 个**(`C01-IA-F1`/`C02-UA-F1`/`C03-UN-F1`/`C04-IA-F2`/`C05-IA-F5`),已加注说明 4 → 5 的来历;§4.3 相量区 RAM 行同步为 5 实例。⚠ §3.2 的 L2 各点状态**仍标 ⬜**——实现与主机单测已完成,但**台架 `@test` 闭环未做**,且 L2-3 的完成判据(标准源读数 = 设定有效值)在无信号源台架上不可达,故不置 ✅(口径见 §3.2 表下注) |
| 2026-10-10 | **L1-1/L1-3 节拍源与搬运方式升级(v2.8)** | 节拍源 **TIM6 → TIM3**(更正硬事实:F4 上 ADC1 的外部触发清单里没有 TIM6/TIM7 TRGO,只接 DAC),TIM3 更新事件同时触发 TPAFE CONVST 与(经 TRGO)ADC1 一轮 9 通道;L1-3 由"软件触发 + 逐通道轮询"改为 **DMA2_Stream4 循环搬运 + 乒乓双缓冲**,`stm32_scan()` → `stm32_take()`,单拍 `adcp` **122 µs → 8 µs**。§1.4 采样软件行、§3.2 L1-1/L1-3/L1-4 同步。完整设计与实测见 `docs/子系统设计/ADC采样节拍设计说明.md` §11 |
| 2026-10-11 | **L2 台架闭环:P0 收尾** | `@build` 0 错 0 警 → `@test` 闭环把 `phasor(first)`/`phasor(hb)` 列为 Required 并校验顺序 `App init OK>phasor(first)>phasor(hb)`(⚠ `net init: OK` **不在**该序内——adcTask 与 netTask 并行,`phasor(first)` ≈+500 ms 早于 `net init: OK` ≈+574 ms 属正常)→ **PASS**:5 实例相量开机即产出、心跳 5 实例持续刷新、`frames` 每 10 s +10000(1000.0 Hz)、`idle=0`、`frst_err=0`、`adc1_err=0`、`adc1_hw=0`、`fw build` 身份戳匹配、回读一致、锚点 ×1、零 `[ERR]`/HardFault;证据 `boot_log_data/20261011_prod_phasor_boot_log.txt`。栈复测:adcTask 静态 832 B / 实测峰值 **928/2048(余 54%)**,7 任务全 PASS(RAM 方案 §2.3 已同步)。§3.1 L1-9 置 ✅(`C04-IA-F2`/`C05-IA-F5` 台架产出);§3.2 L2-1~L2-4/L2-6/L2-7 置 ✅(精度判据保留"待标准源"注),**L2-5 更正为"V0.1 不适用、随 V0.2"**(跨转换器合成被 N-18 禁止、零序走外部专用通道——原表下注"L2-1~L2-7 实现完成"对 L2-5 系误记);§7.1 P0 里程碑置 ✅(遗留 L1-5/L1-6/L1-7)。下一步 = **P1**(A 相过流/过负荷 + 定值模型)与 **L1-6 采样有效性校验** |
| 2026-10-11 | **L1-6 采样有效性校验落地** | 新增 `App/algo/adc_health.c/h`(**纯逻辑判定层**,零平台依赖免桩单测):① 静默——两侧帧序号连续 ≥100 ms 无前进(电平位,可自愈;`adc_health_flags()` 为 L5-4 闭锁预留);② 丢帧——TPAFE 序号一次前进 >1 计 `tgap`(与 STM32 侧 `adc1_gap` 对称,边沿事件不告警——消费者超拍是已知现象,告警会淹没真故障);③ 单调性——序号倒退锁存位;④ 错误增量——FRSTDATA / HAL 错误回调 / STM32 停摆计数增量(事件位与锁存位**编号对齐**,消费者按位图"每类每次开机留痕一次")。`adc_task.c` 只做告警编排:严重事件/上升沿 `DBG_ERROR` + `SYSLOG_SELFTEST`("adc sampling fault"),下降沿 `DBG_INFO`,心跳行增 `tgap`。主机单测 `test/host/adc_health/` **31 用例全绿**(100 ms 门限两侧、tick 回绕、边沿语义、NULL 守卫、init 幂等;`adc-health-unit-test` 钩子登记)。台架 `@test` PASS:零 `[ERR]`(健康不误报)、`tgap=1→2` 与 `adc1_gap` 同步可见、`frames` 1000.0 Hz;证据 `boot_log_data/20261011_prod_adc_health_boot_log.txt`。⚠ **抬栈**:`adc_health_report`→`syslog_post` 控制台回显尾使 adcTask 静态最坏链 832→**1080 B**(512 words 余 968 B < 1024 B 底线)→ **512→640 words**,复测余 **1480 B / 57%**、堆 min_ever 7152→6640 B(AGENTS 任务表 / RAM 方案 §2.3 已同步)。§3.1 L1-6 置 ✅ |
| 2026-10-11 | **L2 台架闭环:P0 收尾** | `@build` 0 错 0 警 → `@test` 闭环把 `phasor(first)`/`phasor(hb)` 列为 Required 并校验顺序 `App init OK>phasor(first)>phasor(hb)`(⚠ `net init: OK` **不在**该序内——adcTask 与 netTask 并行,`phasor(first)` ≈+500 ms 早于 `net init: OK` ≈+574 ms 属正常)→ **PASS**:5 实例相量开机即产出、心跳 5 实例持续刷新、`frames` 每 10 s +10000(1000.0 Hz)、`idle=0`、`frst_err=0`、`adc1_err=0`、`adc1_hw=0`、`fw build` 身份戳匹配、回读一致、锚点 ×1、零 `[ERR]`/HardFault;证据 `boot_log_data/20261011_prod_phasor_boot_log.txt`。栈复测:adcTask 静态 832 B / 实测峰值 **928/2048(余 54%)**,7 任务全 PASS(`docs/内存管理/RAM使用分析与优化方案.md` §2.3 已同步)。§3.1 L1-9 置 ✅(`C04-IA-F2`/`C05-IA-F5` 台架产出);§3.2 L2-1~L2-4/L2-6/L2-7 置 ✅(精度判据保留"待标准源"注),**L2-5 更正为"V0.1 不适用、随 V0.2"**(跨转换器合成被 N-18 禁止、零序走外部专用通道——原表下注"L2-1~L2-7 实现完成"对 L2-5 系误记);§7.1 P0 里程碑置 ✅(遗留 L1-5/L1-6/L1-7)。下一步 = **P1**(A 相过流/过负荷 + 定值模型)与 **L1-6 采样有效性校验** |
| 2026-10-11 | **L1-6 采样有效性校验落地** | 新增 `App/algo/adc_health.c/h`(**纯逻辑判定层**,零平台依赖免桩单测):① 静默——两侧帧序号连续 ≥100 ms 无前进(电平位,可自愈;`adc_health_flags()` 为 L5-4 闭锁预留);② 丢帧——TPAFE 序号一次前进 >1 计 `tgap`(与 STM32 侧 `adc1_gap` 对称,边沿事件不告警——消费者超拍是已知现象,告警会淹没真故障);③ 单调性——序号倒退锁存位;④ 错误增量——FRSTDATA / HAL 错误回调 / STM32 停摆计数增量(事件位与锁存位**编号对齐**,消费者按位图"每类每次开机留痕一次")。`adc_task.c` 只做告警编排:严重事件/上升沿 `DBG_ERROR` + `SYSLOG_SELFTEST`("adc sampling fault"),下降沿 `DBG_INFO`,心跳行增 `tgap`。主机单测 `test/host/adc_health/` **31 用例全绿**(100 ms 门限两侧、tick 回绕、边沿语义、NULL 守卫、init 幂等;`adc-health-unit-test` 钩子登记)。台架 `@test` PASS:零 `[ERR]`(健康不误报)、`tgap=1→2` 与 `adc1_gap` 同步可见、`frames` 1000.0 Hz;证据 `boot_log_data/20261011_prod_adc_health_boot_log.txt`。⚠ **抬栈**:`adc_health_report`→`syslog_post` 控制台回显尾使 adcTask 静态最坏链 832→**1080 B**(512 words 余 968 B < 1024 B 底线)→ **512→640 words**,复测余 **1480 B / 57%**、堆 min_ever 7152→6640 B(AGENTS 任务表 / `docs/内存管理/RAM使用分析与优化方案.md` §2.3 已同步)。§3.1 L1-6 置 ✅ |
| 2026-10-11 | **P1/P2 软件侧:定值模型 + 保护元件框架** | 新增 `App/algo/prot_set.c/h`(**定值模型**:Q10 定点与参考 2^10 口径一致;31 项元数据表**通讯地址逐项对照参考 `Comm/Pub/Const_CommSetPara.c`**——过流 Ⅰ/Ⅱ/Ⅲ = 0x8856~0x8861、过负荷 = 0x8871~0x8874、零序过压 = 0x888E~0x8890、过压/欠压沿用告警段 0x8027~0x802A;范围校验 + 整表装载 all-or-nothing + **sum32 正/反码封存自检闭锁**(参考 `dwTagSum/dwTagSumN` 模式,L5-1);⚠ 默认值/范围为**拟定**,参考 EEPROM 出厂表不在本仓库,待对照修订)+ `App/algo/prot_elm.c/h`(**元件框架**:通用"越限 + 返回系数滞回 + 定时限"核,数据驱动实例表 7 行 = OC1/OC2/OC3/OL(IA)+ OV/UV(UA)+ 3U0(UN);`prot_elm_eval()` 纯判据可主机单测,`prot_elm_run()` 为 phasor→测量薄适配(相量未就绪整拍跳过);定值闭锁 → 全元件复位)。主机单测 `test/host/prot_set/`(30 用例)+ `test/host/prot_elm/`(27 用例:1.01× 启动 / 100 ms 到点 / 滞回保持与释放 / 欠量方向 / 投退 / 闭锁联动 / 时基回绕)全绿,钩子 `prot-set-unit-test` / `prot-elm-unit-test` 登记。`@build` 0 错 0 警(**未接线 → 链接器整对象移除,零 Flash/RAM 足迹**);板端接线随 protectTask 立项(栈预算 + IWDG 门禁),标准源注入验收随其后。§3.3 L3-1/3/4/6 置 🔶、L3-2/5 注明框架就绪待通道/频率算法;§3.5 L5-1/2/4 置 🔶;§5.4 落实进度注 |
@@ -4,7 +4,7 @@
> 使用指南回答"**上层该怎么调**"(公开语义 / 示例 / 常见问题);本文回答"**内部函数各自做什么、互相怎么调用、为什么这么设计**"(调用关系 / 设计意图 / 踩坑)。
>
> **范围**:MCU 端 `Drivers/BSP/NET/net_socket.c`(约 2870 行,全部函数)。
> 模拟器端后端 `simulator/sim/net_socket_sim.c`(Winsock)是另一套实现,见使用指南 §2.3;`net_select.c` 不在此展开,见使用指南 §7 / §10.5。
> 模拟器端后端 `simulator/sim/net_socket_sim.c`(Winsock)是另一套实现,见 `docs/网络/BSD_Socket_API_使用指南.md` §2.3;`net_select.c` 不在此展开,见 `docs/网络/BSD_Socket_API_使用指南.md` §7 / §10.5。
>
> **交叉引用约定**:机制标 Trap 号 → `docs/问题记录/CH395F_Trap_Records.md`;用法语义引用使用指南 §;关键参数值与 `net_config.h` / `net_socket.c` 内宏一致。
@@ -103,7 +103,7 @@ flowchart TB
| 函数 | 设计作用 | 主要调用 | 意图 / 注意 |
|---|---|---|---|
| `net_init(ip,mask,gw,mac)` | 芯片一次性配置:探测/复位 → 参数 → MAC → 协议栈初始化 → 8 Socket 缓冲布局 → 使能 PING | `net_pending_cancel_all` → `ch395f_check_exist` / `ch395f_reset` / `set_fun_para(0x09)` / `set_tcp_mss` / `set_retrans_count+period` / `set_keepalive_idle+intvl+cnt` → `set_mac_addr` → `parse_ip_string` + `set_ip_addr` / `set_gwip_addr` / `set_mask_addr`(或 `set_dhcp`)→ `ch395f_init` → `set_send_buf` / `set_recv_buf` ×8 → `ping_enable` | ① **重建前**先取消全部挂起 recv 并逐一通知(不通知 = 调用方永久挂死),且必须早于控制块清零与芯片复位;② 重传/KeepAlive/**MAC**/IP/GW/MASK 参数**必须**在 `ch395f_init` 之前下发(手册 §9.2.1:SET_MAC 为步骤①,INIT_CH395 加载 MAC——顺序反了本次上电不生效,P1-7);③ `fun_para=0x09`:多 Socket 模式 + TCP Server 单连接 + DISCONNECT/TIMEOUT 不清缓冲(软件排空后手动关);④ 缓冲布局每 Socket 6 块(收 4 + 发 2)×8 = 48 块恰好用尽,Socket 0~7 必须显式重分配(Trap 01/02);⑤ 捕获 `xTaskGetCurrentTaskHandle` 存入 `s_net_task_handle`(防自通知死锁的判据) |
| `net_init(ip,mask,gw,mac)` | 芯片一次性配置:探测/复位 → 参数 → MAC → 协议栈初始化 → 8 Socket 缓冲布局 → 使能 PING | `net_pending_cancel_all` → `ch395f_check_exist` / `ch395f_reset` / `set_fun_para(0x09)` / `set_tcp_mss` / `set_retrans_count+period` / `set_keepalive_idle+intvl+cnt` → `set_mac_addr` → `parse_ip_string` + `set_ip_addr` / `set_gwip_addr` / `set_mask_addr`(或 `set_dhcp`)→ `ch395f_init` → `set_send_buf` / `set_recv_buf` ×8 → `ping_enable` | ① **重建前**先取消全部挂起 recv 并逐一通知(不通知 = 调用方永久挂死),且必须早于控制块清零与芯片复位;② 重传/KeepAlive/**MAC**/IP/GW/MASK 参数**必须**在 `ch395f_init` 之前下发(`docs/芯片手册/CH395F.md` §9.2.1:SET_MAC 为步骤①,INIT_CH395 加载 MAC——顺序反了本次上电不生效,P1-7);③ `fun_para=0x09`:多 Socket 模式 + TCP Server 单连接 + DISCONNECT/TIMEOUT 不清缓冲(软件排空后手动关);④ 缓冲布局每 Socket 6 块(收 4 + 发 2)×8 = 48 块恰好用尽,Socket 0~7 必须显式重分配(Trap 01/02);⑤ 捕获 `xTaskGetCurrentTaskHandle` 存入 `s_net_task_handle`(防自通知死锁的判据) |
| `net_get_generation()` | 读网络代次号(纯 RAM) | 无 | FTP/UI/对时服务据此感知"网络已重建"→ 放弃旧监听并重建 |
| `net_generation_inc()` | 代次号 +1 | 无 | 唯一合法递增点 = `net_stack_bring_up()` 末尾(PHY/链路就绪后)——太早递增会让服务在网络不可用时抢跑重建;模拟器端恒为 0(无重建概念,上层自愈逻辑空转) |
@@ -349,7 +349,7 @@ sequenceDiagram
| `ch395f_check_exist` / `ch395f_reset` | `net_init` | 上电探测与复位 |
| `ch395f_set_fun_para(0x09)` | `net_init` | 多 Socket + TCP 单连接 + 手动关缓冲 |
| `ch395f_set_tcp_mss` / `set_retrans_count` / `set_retrans_period` / `set_keepalive_idle+intvl+cnt` | `net_init`(**均须在 `ch395f_init` 之前**) | 值见 `net_config.h` |
| `ch395f_set_mac_addr` | `net_init`(**须在 `ch395f_init` 之前**) | 写芯片 EEPROM;INIT_CH395 加载 MAC(手册 §9.2.1 步骤①,P1-7) |
| `ch395f_set_mac_addr` | `net_init`(**须在 `ch395f_init` 之前**) | 写芯片 EEPROM;INIT_CH395 加载 MAC(`docs/芯片手册/CH395F.md` §9.2.1 步骤①,P1-7) |
| `ch395f_set_ip_addr` / `set_gwip_addr` / `set_mask_addr` / `set_dhcp` | `net_init`(**均须在 `ch395f_init` 之前**) | 固定 IP 或 DHCP |
| `ch395f_init` / `ch395f_ping_enable` | `net_init` | 协议栈初始化(INIT_CH395)/ PING(须在 init 之后) |
| `ch395f_set_send_buf` / `ch395f_set_recv_buf` | `net_init`(8 Socket 布局);`net_bind`(UDP)(Socket 4~7 兜底) | 每 Socket 收 4 块 + 发 2 块 |
+96 -8
View File
@@ -19,7 +19,7 @@
### 注意事项
- 数据 Socket 不调 `OPEN_SOCKET`,由 CH395F 在连接到达时自动打开
- DISCONNECT 后 CH395F 自动关闭 Socket,但保留协议/端口/缓冲区配置,可被再次分配
- 手册 §9.2.6 要求数据 Socket **先设源端口再设协议类型**(与监听 Socket 顺序相反)
- `docs/芯片手册/CH395F.md` §9.2.6 要求数据 Socket **先设源端口再设协议类型**(与监听 Socket 顺序相反)
### 对应文件
`Drivers/BSP/NET/net_socket.c` → `net_listen()`
@@ -35,7 +35,7 @@ Phase 8 压力测试,2025-07
多连接模式下多个 Socket 数据互相覆盖,收到错误数据或客户端挂死。
### 根因
CH395F 默认只为 Socket 0~3 各分配独立收发缓冲(共 48 块 × 512B 用满,见手册 §5.45),Socket 4~7 为零块。`net_listen()` 未对 Socket 4~7 显式分配独立缓冲区块,后续代码误用相同块号(如 28~31)导致数据重叠。
CH395F 默认只为 Socket 0~3 各分配独立收发缓冲(共 48 块 × 512B 用满,见`docs/芯片手册/CH395F.md` §5.45),Socket 4~7 为零块。`net_listen()` 未对 Socket 4~7 显式分配独立缓冲区块,后续代码误用相同块号(如 28~31)导致数据重叠。
### 解决方案
每个数据 Socket 独占 4 个缓冲区块(2 发 + 2 收),按 `ds * 4` 基址分配,确保不重叠:
@@ -496,14 +496,14 @@ NET 层 TCP Client(阶段 7)`net_connect()` 返回 0,但 `net_poll()` 报
### 解决
1. `net_init`:`ch395f_set_fun_para(0x09)`(bit0=1, bit1=0, bit3=1)。
2. `ch395f_init()` 之后,循环 0~7 用 `ch395f_set_send_buf(sock, i*6+4, 2)` / `ch395f_set_recv_buf(sock, i*6, 4)` 给每个 Socket 分配独立收发缓冲。按手册 §5.45(48 块 × 512B = 24KB)+ §9.2.8 最优分配:每 Socket 6 块 = 接收 4 块(2KB) + 发送 2 块(1KB),`ch395f_set_tcp_mss(1024)`,恰好用尽 0~47 块、互不重叠。
2. `ch395f_init()` 之后,循环 0~7 用 `ch395f_set_send_buf(sock, i*6+4, 2)` / `ch395f_set_recv_buf(sock, i*6, 4)` 给每个 Socket 分配独立收发缓冲。按`docs/芯片手册/CH395F.md` §5.45(48 块 × 512B = 24KB)+ §9.2.8 最优分配:每 Socket 6 块 = 接收 4 块(2KB) + 发送 2 块(1KB),`ch395f_set_tcp_mss(1024)`,恰好用尽 0~47 块、互不重叠。
3. MSS 选取依据 §9.2.8:接收缓冲(2KB) ≥ 2×MSS(1024) 满足建议、≥ MSS 满足必须;发送缓冲(1KB) ≤ 8KB 满足必须。MSS 取 1024 为 8-Socket 共享 24KB 时的最大值。
4. `net_init` 缓冲分配顺序遵循 Trap 01:先配数据 Socket,再配监听 Socket 0(本 Net 层 Socket 按需 open,故统一在 init 一次性配好)。UDP 路径 `net_socket()` 中对 Socket 4~7 的兜底重设也同步为同一布局。
5. PC 端 `tcp_server` 改为每连接一线程的并发回显(`--max-conn` 为并发计数),否则 Phase 7 全量连接会触发累计计数上限而拒掉 4~7(Trap 19)。
### 注意事项
- 4~7 的缓冲必须显式分配,否则其 CONNECT/RECV 中断不触发(Trap 01/02)。
- 上述布局**以手册 §5.45 的 48×512B 缓冲几何为准**,并已由 Phase 7 TC704(40/40 PASS)实测确认块 0~47 全部有效(此前文档中"32×1KB"记录有误)。Socket 4~7 分配与 8 路并发回显均验证通过。
- 上述布局**以`docs/芯片手册/CH395F.md` §5.45 的 48×512B 缓冲几何为准**,并已由 Phase 7 TC704(40/40 PASS)实测确认块 0~47 全部有效(此前文档中"32×1KB"记录有误)。Socket 4~7 分配与 8 路并发回显均验证通过。
- 多 Socket 模式下 TCP Server 单连接语义不变:Socket 0 监听且连接停留在本 Socket,Sockets 1~7 由应用当作独立 client 使用。
- `net_poll` 用 `GET_GLOB_INT_STATUS_ALL`(2 字节,支持 0~7)并遍历 0~7,Socket n 中断位即 `bit(n+4)`(Socket 0~3→bit4~7,Socket 4~7→bit8~11),原代码正确无需改动。
@@ -738,13 +738,13 @@ NET 层 TCP Client(阶段 7)`net_connect()` 返回 0,但 `net_poll()` 报
---
## Trap 24:MAC 写入顺序违反手册 §9.2.1(SET_MAC 必须在 INIT_CH395 之前)
## Trap 24:MAC 写入顺序违反`docs/芯片手册/CH395F.md` §9.2.1(SET_MAC 必须在 INIT_CH395 之前)
### 现象(静态审查发现;手册确证)
`net_init()` 原顺序为 `ch395f_init()`(CMD_INIT_CH395)→ `ch395f_set_mac_addr()`,与手册 §9.2.1 规定的初始化顺序**相反**。后果:INIT_CH395 已按 EEPROM 旧值加载 MAC,其后写入 EEPROM 的新 MAC 本次上电**不生效**(回落芯片内置/上次值,实际"滞后一次上电")→ UI 配置的 MAC 与设备实际在线 MAC 不一致,设备在线身份错误、网口排查困难。本轮未做"UI 改 MAC + 重启"台架复现,结论由手册条文确证。
`net_init()` 原顺序为 `ch395f_init()`(CMD_INIT_CH395)→ `ch395f_set_mac_addr()`,与`docs/芯片手册/CH395F.md` §9.2.1 规定的初始化顺序**相反**。后果:INIT_CH395 已按 EEPROM 旧值加载 MAC,其后写入 EEPROM 的新 MAC 本次上电**不生效**(回落芯片内置/上次值,实际"滞后一次上电")→ UI 配置的 MAC 与设备实际在线 MAC 不一致,设备在线身份错误、网口排查困难。本轮未做"UI 改 MAC + 重启"台架复现,结论由手册条文确证。
### 根因
手册 §9.2.1「初始化 CH395,必要操作」顺序为:
`docs/芯片手册/CH395F.md` §9.2.1「初始化 CH395,必要操作」顺序为:
① `SET_MAC_ADDR` → ② `SET_IP_ADDR` → ③ `SET_GWIP_ADDR` → ④ `SET_MASK_ADDR` → ⑤ `INIT_CH395`。
- §5.7:`SET_MAC_ADDR` 把 MAC **写入芯片内部 EEPROM**(约 30us);
- §5.13:`INIT_CH395` **初始化 CH395 的 MAC、PHY 以及 TCP/IP 协议栈**(约 5ms)。
@@ -766,4 +766,92 @@ NET 层 TCP Client(阶段 7)`net_connect()` 返回 0,但 `net_poll()` 报
`Drivers/BSP/NET/net_socket.c`(`net_init` SET_MAC 顺序)、`Drivers/BSP/NET/net_socket.h`(`net_init` 注释)、`Drivers/BSP/CH395F/ch395f.c`(四个 SET 的限定条件注释);`docs/网络/BSD_Socket_API_函数调用关系.md`(§4.1 / §7 顺序)。
### 发现时间
2026-10-04(静态审查项 P1-7;依据手册 §9.2.1 / §5.7 / §5.13 定案)。
2026-10-04(静态审查项 P1-7;依据`docs/芯片手册/CH395F.md` §9.2.1 / §5.7 / §5.13 定案)。
## Trap 25:FTP 数据连接三连故障——丢 CONNECT 中断 / accept 竞态自毁缓冲 / 背靠背小段毒化 TX(N-30,2026-10-11 定案并修复,回归 4 轮 11/11)
### 现象(生产 FTP 回归,多轮复现)
同一 FTP 会话内**第 2 次及以后**的数据连接(LIST/STOR/RETR 各一次)出现两类失败,两种面孔同一根因:
- **吞数据(SWALLOW)**:新数据连接建立成功(芯片 0x05/0x04 ESTABLISHED)、服务端 `net_send` 返回成功,客户端 20 s 收不到任何字节(无 RST、无 DISCONNECT,直到客户端放弃);
- **425**:新数据连接被接受后 ~10-20 ms 内出现 DISCONNECT(`chip tcp=0x00`),服务端 accept 失败 → `425 Can't open data connection`。(⚠ 本条当时的解读"芯片自杀连接"**有误**,见文末"三次定案"——实为客户端发完数据后的**正常 FIN** + 我方 accept 竞态自毁缓冲。)
失败概率随**上一条数据连接的负载大小与间隔**变化(NAND 目录文件越多 LIST 越大越易触发),与提交历史无关——在 D20 基线 e5733b34 上以快速连发同样复现(D20 时代"11/11 通过"是慢速台架 + 干净目录的运气)。
### 定案实验(裸机芯片台架,剥离 OS/net/FTP)
实验分支 `n30-chip-bench`(worktree),`test/board/ch395f_reuse_bench.c` 在**内核启动前**直接调 `ch395f_*` 做受控矩阵:`{socket, close 后复开延迟, TX 长度, 关闭顺序}` 组合循环,每 50 ms 打印芯片两字节状态 + 中断寄存器;主机侧 `test/host/ch395f_reuse_host.py` 同步做纯 TCP 客户端。**OS/net/FTP 全部剥离后 bug 依旧复现** → 纯芯片操作时序问题,非上层逻辑、非芯片硬件缺陷。
关键现场(芯片日志):
```
post-listen st=0x05/0x04 ← listen 返回成功,但 TCP 态停在旧连接的 ESTABLISHED(幽灵态)
unexpected int=0x10 while wait ← 只等来旧连接滞后的 DISCONNECT
no CONNECT within 10s ← 新连接的 CONNECT 中断永远没来(芯片状态却已是 0x04)
```
**芯片级规律**:CH395F 关闭连接后的拆链收尾窗口**变长且不可观测**——状态寄存器显示 CLOSED 持续 1 s 以上也不代表拆完(实测窗口可 >2 s 且时长不定)。窗口内重新 `open+listen`:
1. 新连接握手照常完成(状态寄存器 → ESTABLISHED)但 **CONNECT 中断被丢**(Trap 22 丢中断机制的窗口期形态)→ 事件驱动的 accept 永远等待 → 吞数据;
2. 或 listen 后状态直接假报 0x04(幽灵态),此时回读对端端口 = **旧连接端口**;
3. 或旧连接滞后的 DISCONNECT 打到新监听 → 触发 relisten 毁掉刚到的新连接 → 425。
### 已证伪的修复(全部经台架受控实验)
| 尝试 | 结果 |
|---|---|
| close 后固定沉降 100/300/800/2000/3000 ms | 仍挂(2 s 也挂过)——窗口变长,固定等待无保证 |
| Socket 轮换(sock3↔sock4 交替) | 仍挂——上一轮拆链尾巴跨 socket 毒化下一轮 |
| listen 后 close+重试 / 耐心等幽灵消退 | 持平对照组,无效 |
| 收发缓冲重分配(clear_recv_buf + set_*_buf) | 无效 |
| close 前 200 ms TX 排空延时 | 无效 |
| 禁用对账 tick(验证因果) | 仍挂——对账不是元凶 |
### 有效修复(台架修复组 15/15,机制触发 5 次全部救回)
**双信源原则:中断只是快路径,状态寄存器才是真相。** 等连接时不能只等中断:
1. **accept 路径内联探针**(`net_lost_connect_probe`,`net_accept_locked` 每 100 ms 轮询挂探针):芯片状态已 ESTABLISHED 而 CONNECT 中断未到 → 连续 2 轮去抖后补做 accept(复用 `handle_connect_event` 簿记)。
2. **幽灵态甄别**:补 accept 前回读对端端口(`GET_REMOTE_IPP`),**端口 = 上一条连接端口(`last_peer_port`)→ 幽灵,不 accept**——否则会把数据发给已死的旧连接(这正是生产 LIST#4"accept 成功但数据被吞"的直接成因:对账②曾把幽灵连接补 accept 了)。
3. **对账②同步加幽灵甄别**(`net_reconcile_tick` 类型②分支同样比对 `last_peer_port`)。
4. `last_peer_port` 登记点:`handle_disconnect_event` / `handle_timeout_event` / `net_relisten_try`(清零 remote_port 前转登)/ `net_close_locked`(跨 memset 保留——幽灵属芯片 Socket 级,与 fd 复用无关)/ NET_MSG_FIX 陈旧 ESTABLISHED 修复路径。
### 与 Trap 22 的关系
Trap 22 的周期对账(200ms 节拍 + 400ms 去抖 + 1s 冷却)设计目标是"自愈兜底",对常驻监听器有效;但 FTP 数据通道是"活 1 秒就死"的短命监听器,且对账②**无幽灵甄别**——它既救不全(冷却期内 accept 已超时)又反过来帮凶(把幽灵补 accept)。Trap 25 把"看状态补 accept"内联进 accept 热路径(短去抖、无冷却)并补上端口甄别,对账②保留为兜底网。
### 对应文件
`Drivers/BSP/NET/net_socket.c`(`net_lost_connect_probe` / `net_accept_locked`(含 closed-with-data accept)/ `net_reconcile_tick`② / `last_peer_port` 各登记点)、`Drivers/BSP/NET/net_types.h`(`last_peer_port` / `lost_conn_seen` 字段)、`Drivers/BSP/NET/lftpd/lftpd.c`(`send_list`/`send_nlst` 批量发送);台架工具 `test/board/ch395f_reuse_bench.{c,h}` + `test/host/ch395f_reuse_host.py`(实验分支 `n30-chip-bench` 注册编译,主分支只存档不注册)。
### 验证(台架层)
`ch395f_reuse_bench` 修复组(丢中断捕获自 accept)15/15,机制触发 5 次全部救回(对照同表 4/15 挂)。⚠ 生产回归当时未达 11/11——生产主导模式与台架不同,见下两节。
### 三次定案:所谓"kill 模式"是寄存器状态误读(2026-10-11 当日终案,双向验证)
第二轮验证(仅双信源探针修复,4 轮:1/11、5/11、2/11、2/11)暴露 STOR #2 仍 100% 失败,当时解读为"芯片在 accept 后 10-20 ms 杀连接"。后经**手册条文 + 网络资料双向核对**推翻:
**寄存器读数本身全部正确**(`GET_SOCKET_STATUS_SN` §5.22:`05H=SOCKET_OPEN/00H=SOCKET_CLOSED`;第二字节 TCP 态 `00H=CLOSED/04H=ESTABLISHED`,与 WCH 官方数据手册表 5-8/5-9 一致;`GET_RECV_LEN_SN` §5.33 低字节在前,驱动实现一致)。**错在对 DISCONNECT 事件语义的解读**:
1. `net_init` 的 `ch395f_set_fun_para(0x09)` 置 **FUN_PARA_FLAG_SOCKET_CLOSE=1**(§5.52 ②):产生 DISCONNECT/TIMEOUT 后芯片**不关 Socket、不清缓冲**,就是留给单片机把残留数据读出后再关 Socket。
2. 判别探针现场:`kill-check recv_len=1027 re-read st=0x05/0x00`——Socket 仍 OPEN、TCP 已 CLOSED、**RX 缓冲 1027 字节 = STOR 载荷一个不少全在**。这是芯片按 §5.52 规格**保留现场**,不是杀连接。
3. 那个 DISCONNECT 是**客户端的正常 FIN**:Python ftplib `storbinary` 语义 = `sendall` 发完立即关数据连接,然后回控制通道等 226——标准 FTP 行为。局域网小文件下"连接→发完→关闭"在 ~5 ms 内完成。
**425 真根因(竞态 + 自毁缓冲)**:客户端 5 ms 内完成发送并关闭;`cmd_stor` 的 accept RPC 赶到 net 层时 MCU 态已是 CLOSED;旧 `net_accept_locked` 把 CLOSED 一律当"无连接" → `net_relisten_try`(close+open **清空芯片 RX 缓冲**)→ 1027 字节被**我方亲手销毁** → WOULDBLOCK → lftpd 判"数据套接字已关" → 425。芯片全程无辜(此模式)。
**有效修复(已落地,`net_accept_locked` CLOSED 分支)**:CLOSED + auto_relisten + **芯片 RX 缓冲有数据**(`GET_RECV_LEN_SN`>0)→ 接受这条"已关但可读"连接(日志 `closed-with-data accept (drain pending)`),应用经 `net_recv_locked` 快路径(无状态门槛)排空,排空后 EOF 自然触发 relisten——完全对齐 §5.52 ②"读出残留数据后再关闭"的芯片契约。**验证:修复后 4 轮回归 STOR #2 从 4/4 失败 → 4/4 通过**(7/11、6/11、9/11、5/11,失败项中已无 STOR)。
**连带修正**:台架 mode0(主机先关)观察到的"accept 后 10-20 ms DISCONNECT"大概率同为误读——那是主机收完数据自己关的。"拆链窗口杀连接"理论对生产 425 不成立;台架**丢 CONNECT 中断模式**(主机侧 got=0 + 芯片 0x04 + 无中断,主机端独立证据)仍然成立,双信源探针修复保留。
**当时的遗留**:LIST #4/#7 超时(服务端 `net_send` 目录、客户端 20 s 收 0 字节、无 DISCONNECT)——发送方向的真问题,与上述 425 机制不同——已由下节"四次定案"解决。
### 四次定案:LIST 吞包 = 背靠背小段写毒化芯片 TX(2026-10-11 终案,修复后 8/8 通过)
探针定位过程(全部生产固件 + 临时探针,一轮复现):
1. PASV→accept 间隔 2 s(`net_listen_locked` open 重试在跑)→ 曾疑"listen 撞拆链窗口";
2. accept 正常、CONNECT 正常、发送后中断位 `0x03`(SENBUF_FREE+SEND_OK)——**芯片自认发送成功**;
3. `GET_REMOTE_IPP` 对端 IP:port 完全正确(排除对端混淆);
4. **Socket 轮换**(服务端先关后 4 s 冷却、数据连接轮转 sock3~7):**证伪**——LIST 换到全新 sock5 仍 100% 挂(芯片行为与失败前完全一致);
5. **决定性对照**:同一目录同一载荷,LIST #1(会话首连,FAT 冷、readdir 慢、行间天然间隔大)通过;LIST #4/#7(FAT 热、每行一个 `net_send` 背靠背小段)必挂。
**定案**:`send_list`/`send_nlst` 每行一次 `net_send`(~60 B 小段、间隔仅 ms 级)——CH395F 对同一连接的**背靠背多段小写入**会把 TX 整条连接毒化(芯片报 SEND_OK 但对端协议栈全部丢弃,无 RST 无 DISCONNECT)。`net_send_locked` 的逐块协议(等 SENBUF_FREE 再写下一块)本身合规(§5.45),但芯片对该模式的内部处理不可靠;RETR 的 1 KB 满块连发不受影响(实测通过)。
**修复(`lftpd.c`)**:`send_list`/`send_nlst` 整份目录**一次性批量发送**(static 1536 B 缓冲,snprintf 累积 + 单次 `lftpd_inet_write`;溢出截断判 -1→550)。注意 `lftpd_inet_write` 成功返回**字节数**非 0(首版误按 0=成功判 → 全 550,已纠)。
**验证**:修复后 ftp_regression 连跑 **4 轮 11/11**(终版:批量发送 + closed-with-data accept + 丢中断探针 + 幽灵甄别,无轮换无探针)+ `ftp_auth_bench.py` **13/13** + `@test` 启动标记 PASS;此前同条件 LIST #4/#7 为 100% 失败、STOR #2 曾 100% 425。轮换冷却机制随证伪**已撤**(不留未证明的复杂度;其 5 轮 11/11 一并存档备查)。
**教训(Trap 25 总纲)**:本轮先后误读三次——把客户端正常 FIN 读成"芯片杀连接"、把 open 重试读成"拆链窗口"、把芯片假 SEND_OK 读成"数据已上线"。铁律:**CH395F 的"成功"(中断位/状态码)只代表芯片本地动作完成,不代表对端收到;判定必须回到对端可见证据(主机侧收发)+ 手册语义(FUN_PARA §5.52、状态码 §5.22)**。
**顺带发现的疑点(存疑,暂不动)**:`set_fun_para(0x09)` 的 bit0——§5.52 表明确写 **bit0 保留、必须为 0**,而 `net_init` 注释声称"bit0=1 多 Socket 模式"(手册与网络资料均无此说法,CH395 的 8 Socket 是芯片原生能力,单/多 Socket 之分在手册中只关联 SCS0 引脚硬件形态)。现固件 bit0=1 下 sock1/2/3 多路并行正常 → 该位大概率被芯片忽略。**本轮不改**(若真为多 Socket 使能,改 0x08 会毁掉全部 1~7 号 Socket);后续单独台架验证"0x08 下多 Socket 是否仍正常"再定。
### 发现时间
2026-10-11(登记簿 N-30;裸机芯片台架定案实验见本节,实验分支 `n30-chip-bench`)。
+2 -2
View File
@@ -60,7 +60,7 @@
>
> **2026-10-03 变更**:**E-34 / E-04 已修复并通过真机验证**(CH395F 字节命令有限超时 + `remo_send` 返回值校验;见 §5 第一档)。
> **2026-10-03 变更②**:**GD5F 错误通道统一(E-10 / E-42 / E-53 / E-54 / E-56 / E-57)已修复并通过台架套件回归**(`=== GD5F2GQ5UE Tests: 16/16 PASSED (failed=0) ===`,只读模式;见 §5 第二档 #1)。
> 真机全闭环 `@test` PASS 7/7;执行判据见 `测试说明/实机测试实施指南.md` §3 / §10。
> 真机全闭环 `@test` PASS 7/7;执行判据见 `docs/测试说明/实机测试实施指南.md` §3 / §10。
---
@@ -455,7 +455,7 @@ E-57(`int` / `int32_t` 混用)、**E-10 / E-42 / E-56(GD5F init / BBT / ra
| **E-23** | `test/` | 测试套件缺代次检查(accept 失败分支);TC704 并发度偏高 | **仍然存在** | 加代次断言;TC704 降并发或在测试构建抑制服务任务 |
| **E-30** | `simulator/sim/net_socket_sim.c` | sim 后端 errno 语义与 MCU 不对齐(成功即清 errno,MCU 不清) | **仍然存在**(语义差异在 §6 表中仍成立) | 对齐语义;并在缺陷记录中标明"sim 复现盲区" |
| **E-57** | `Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.h:119/140/189/222-253` | 公开 API 返回类型 `int` / `int32_t` 混用(码值均为小负数,无功能影响) | ✅ **已修复(2026-10-03)**:公开 API 全部统一为 `int32_t` | 见 §5 第二档 #1 |
| **E-60** | `Drivers/BSP/SD2506/sd2506.c/h` | `SD2506_CHARGE_5K` 宏位与手册 §4.8 不符的风险 | **已订正**(2026-09-21,`41e111b`):宏位已改为 `bit1`,18H 回读校验在位 | 无需改动,仅作为"手册总表 vs 专用表不一致"的案例保留 |
| **E-60** | `Drivers/BSP/SD2506/sd2506.c/h` | `SD2506_CHARGE_5K` 宏位与 `docs/芯片手册/SD2506API-G.md` §4.8 不符的风险 | **已订正**(2026-09-21,`41e111b`):宏位已改为 `bit1`,18H 回读校验在位 | 无需改动,仅作为"手册总表 vs 专用表不一致"的案例保留 |
| **E-61** | `Drivers/BSP/SD2506/sd2506.c:482` | `sd2506_clear_int` 注释称"ARST=1 时自动清除 INTAF",实际 ARST 在 **CTR3**(11H)而代码从未写过 CTR3 | **仍然存在(纯注释错误)** | 修注释:明确"ARST 未启用,INTAF 靠读 CTR1 清除" |
| **E-62** | `Lib/FatFs/ffconf.h` | `FF_USE_LFN=0` → 严格 8.3 短名;自愈逻辑若把 `FR_INVALID_NAME` 归入损坏触发集 → 不可逆重格式化(`.fschk` 无限重格式化事故根因) | **铁律已固化**(`AGENTS.md`);`ff.c` `create_short` 全部拒绝点已加 DEBUG 打印 | 无需改动,回归监督项(与 §3.2-E-31 后续同族) |
@@ -55,6 +55,9 @@
## 0.4 未闭环清单(2026-10-11 复核)
> 只列**未办 / 半办**项;已修项已按「修复即销项」删除(去处见变更记录)。图例:❌ 未办 / 🟡 半办。
> **统一排序与推进**:本表各项已于 2026-10-11 并入 [`../待处理内容.md`](../待处理内容.md) §2/§3.2
> (新编号 N-19~N-29 + U 簇,出处列保留本报告编号);**顺序权威在登记簿 §2,本表为评审快照**。
> ⚠ 本报告编号与登记簿同名不同义(报告 N-16 = 日志逐条开关,登记簿 N-16 = A2/A4 负向台架)→ 引用带文档名。
| 类别 | 编号 | 未闭环内容 | 下一步 |
|------|------|-----------|--------|
@@ -710,7 +713,7 @@ UI 剩余页面批量落地(U5)、目录级层次迁移(S-1 的物理搬
| 已销项条目 | 去处 |
|-----------|------|
| N-01(配置引擎 C 堆必失败) | `../子系统设计/配置文件设计说明.md`(v1.13,§6.2/§8) |
| N-01(配置引擎 C 堆必失败) | `../子系统设计/配置文件设计说明.md` §6.2 / §8(v1.13) |
| N-03a/N-03b/N-07/N-08(IWDG、`configASSERT`、两个钩子、4 个 Fault 现场) | `../子系统设计/看门狗设计说明.md`(§4.4、§10.1/§10.2)+ AGENTS.md「平台约束」 |
| N-10(UI accept 门控) | `../UI设计/RemoDispBus协议与接收设计说明.md` §3 + 看门狗设计说明 §11.4 |
| P1-1(ADC 数据路径) | `../子系统设计/ADC采样节拍设计说明.md` §10/§11 + `App/algo/README.md` |
+12 -1
View File
@@ -11,7 +11,8 @@
|---|---|---|
| `check_coding_standard.py` | `C coding standard` | C 代码规范机械检查(命名前缀/常量右置/头文件保护宏等) |
| `check_mojibake.py` | `Forbid U+FFFD mojibake` | 扫描 `U+FFFD` 乱码(`EF BF BD` 字节) |
| `check_doc_links.py` | `doc-links` | **文档链接存在性** + 命名规范(D-3/D-7) |
| `check_doc_links.py` | `doc-links` | **文档链接存在性** + 命名规范(D-3/D-7)+ **`§N.M` 章节引用**(口径见 `docs/文档规范.md` §3.5)。钩子已带 `--sections-strict`(自引用失效也拦);`--section-todo` 列出机器判不了目标的 `§`(带改写建议)。每次跑打印「可判定比例」行,防覆盖率回落 |
| `section_aliases.json` | (被 `check_doc_links.py` 读) | 文档**昵称别名表**:「内存方案」→ `docs/内存管理/RAM使用分析与优化方案.md`。只登记无歧义昵称;登记一次即把该昵称的引用纳入硬门禁 |
| `gen_task_table.py` | `task-table` | FreeRTOS 任务表以 `Src/freertos.c` 为**唯一事实源**生成/校验(D-2/D-7) |
| `check_mem_numbers.py` | `mem-numbers` | 内存/Flash 数字以 `.map` 为唯一事实源校验(D-7 体积档) |
| `check_test_suite.py` | `test-suite-switch` | 测试套件开关=产品构建(防测试固件静默提交) |
@@ -23,6 +24,14 @@
| `ram_report.py` | 从 `.map` 生成**静态 RAM/Flash 占用**报告(→ `docs/内存管理/data/`) |
| `stack_report.py` | 从 Keil 静态调用图 + 串口 `[STACK]` 水位生成**每任务最坏链**报告 |
### 引用同步(改名 / 挪目录)
| 脚本 | 作用 |
|---|---|
| `rename_ref.py` | **改名/挪目录 + 全库引用自动改写**:默认 dry-run 预览,`--apply` 才落盘(`git mv` → 改写引用 → 自动跑 `check_doc_links --all` 复核)。覆盖反引号引用、Markdown 链接、图片 `../` 层级换算、`#include`、`uvprojx`/`CMakeLists.txt` 源列表。固定动作与边界 → `docs/文档规范.md` §3.6 |
> `check_doc_links.py` 除链接存在性外,还校验 **`§N.M` 章节引用**是否指向目标文档真实存在的编号标题(口径见 `docs/文档规范.md` §3.5)。
### CI 与本地等价(2026-10-01 T-6)
| 脚本 / 文件 | 作用 |
@@ -43,6 +52,8 @@
# 单个脚本
& '.venv\Scripts\python.exe' scripts/check_doc_links.py --all
& '.venv\Scripts\python.exe' scripts/rename_ref.py docs/旧/名.md docs/新/名.md # 预览
& '.venv\Scripts\python.exe' scripts/rename_ref.py docs/旧/名.md docs/新/名.md --apply # 落盘
& '.venv\Scripts\python.exe' scripts/gen_task_table.py --check # 或 --write / --diff
& '.venv\Scripts\python.exe' scripts/check_mem_numbers.py --check
& '.venv\Scripts\python.exe' scripts/check_test_suite.py # 门禁判定
+16
View File
@@ -142,6 +142,22 @@ CHECKS = [
hint="崩溃现场报告实现(打印 PC/LR/xPSR/CFSR/HFSR/BFAR/MMFAR)"
"须在 USER CODE BEGIN 0 区内(生成区内的标准保护位)。",
),
# N-20(2026-10-12):Error_Handler 统一降级。函数体本身在 CubeMX 的
# `USER CODE BEGIN/END Error_Handler_Debug` 区内(L2 受保护),重生成不丢;
# 本项是 L3 兜底:被打回模板默认(__disable_irq+while(1) 静默挂死)**不报编译错**,
# 只在真出外设初始化故障时暴露——整机只剩 IWDG 兜底且无自检日志。
dict(
id="error-handler-degrade",
kind="protected",
path="Src/main.c",
region="Error_Handler_Debug",
must_contain=["DBG_ERROR", "NVIC_SystemReset"],
must_not_contain=["__disable_irq", "__builtin_return_address"],
hint="Error_Handler 必须打 1 条自检日志后 NVIC_SystemReset(),不得回到 "
"__disable_irq()+while(1) 静默挂死(无日志、只剩 IWDG 兜底)。"
"⚠ 勿用 __builtin_return_address:AC5 编成库调用,MicroLIB 无符号 = L6218E(2026-10-12 实证)。"
"恢复位置 = `USER CODE BEGIN Error_Handler_Debug` 区。",
),
# ---------------- P1:FreeRTOS 加固 ----------------
# ⚠ 2026-10-09 升级:这两项原先靠 FreeRTOSConfig.h 里**自造**的
# `USER CODE BEGIN FREE_RTOS_CONFIG_HARDENING` 区——模板不认这个区名,
+382 -1
View File
@@ -9,6 +9,9 @@
B. 反引号路径引用 `docs/xxx/yyy.md` —— 相对仓库根解析(本工程约定:文档间引用
一律用「仓库根相对路径」)
C. 文件名规范 目录名 / 文件名不得含空格与路径敏感字符(见 docs/文档规范.md §1)
D. 章节引用 `docs/xxx.md` §N.M —— 目标文档里必须真有该编号的标题。
刻意只校验**紧邻写法**(文档名与 § 之间只允许空白/反引号/括号,
实测可把误报压到 0);昵称式引用如「内存方案 §1.4」不校验。
豁免(不算失效,不报错):
* 以 `[计划]` 前缀标记的路径(尚未创建的约定产物,见《项目审查与优化建议报告》D-3 建议)
@@ -30,6 +33,7 @@ import re
import sys
import pathlib
import urllib.parse
from collections import Counter
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
@@ -61,6 +65,50 @@ STATUS_PROXIMITY = 40
PLACEHOLDER_TOKENS = ("NN_", "XX_", "界面名", "模块名", "日期", "示例", "{{", "}}",
"${", "%s", "%d", "<", ">")
# --- D 类:章节引用 §N.M -------------------------------------------------
# 引用写法:§ + 数字编号(§10 / §10.4 / §2.1.1)
SEC_RE = re.compile(r"§\s*(\d+(?:\.\d+)*)")
# 文档名 token:带目录的路径,或裸文件名(中文/下划线/短横均可)。
# 非 .md 后缀也要认——`.pre-commit-config.yaml §7` 这类要识别成「非文档锚点」并跳过,
# 否则会误当自引用去校验本文档编号。
PATH_EXT = r"(?:md|ya?ml|py|c|h|txt|json|ini|ioc|s|bat)"
DOC_NAME_RE = re.compile(r"((?:[\w$.\-一-鿿]+/)+[\w$.\-一-鿿]+\." + PATH_EXT
+ r"|[\w$.\-一-鿿]+\." + PATH_EXT + r")")
MD_SUFFIX_RE = re.compile(r"\.md$")
# 裸文档名(不带 .md):紧邻 § 的词,用于「看门狗设计说明 §10」「V0.0 §4.7」这类写法
# (允许点号,否则 `V0.0` 会被截成 `0`)
STEM_RE = re.compile(r"([A-Za-z0-9_一-鿿][\w.一-鿿]{1,30})[`\s))】\]]{0,4}$")
# 自引用线索:§ 前面是这些词/符号 → 指向本文档
SELF_CUE_RE = re.compile(r"(见|参见|详见|如|按|第|本文|本节|下文|上文|下面|上方|$"
r"|[((、。::||\[\]])[`\s))】\]]{0,4}$")
# 文档昵称的常见结尾:命中说明 § 前面是「别的文档的名字」,不能当自引用
DOC_NOUN_TAIL = ("方案", "报告", "规划", "说明", "指南", "流程", "清单", "架构",
"总览", "手册", "规范", "记录", "审计", "分析", "对照", "要求",
"部署", "使用", "设计", "文档", "台账", "总结", "综述")
# 标题与其编号:`## 3. 链接规范` / `### 2.1.1 xxx`
HEAD_RE = re.compile(r"^#{1,6}\s+(.+?)\s*$")
HEAD_NUM_RE = re.compile(r"^(\d+(?:\.\d+)*)\s*[.、))]?\s")
# 文档名与 § 之间允许的最大间隔字符数,且间隔只能是空白/反引号/括号类
SEC_MAX_GAP = 8
SEC_GAP_CHARS = "` \t)(()【】[]「」《》、::"
SEC_GAP_RE = re.compile("[" + re.escape(SEC_GAP_CHARS) + "]{0,8}$")
# § 链:同一句内连续引用(`a.md` §3.2 L2 设计点 / §10.2 术语表)——后续 § 继承前一个的目标。
# 中间允许描述性文字,但不允许句末标点,也不允许再出现文档名(那会换目标)
SEC_CHAIN_MAX = 40
SEC_TERMINATORS = "。;!?"
# 变更记录类小节:里面的 § 是「当时那个编号」的历史指称,不参与校验
CHANGELOG_WORDS = ("变更记录", "更新记录", "修订记录", "历史", "changelog")
# 外部资料编号(芯片手册等):不受本仓库标题编号约束
EXTERNAL_HINTS = ("RM0090", "参考手册", "数据手册", "手册", "datasheet",
"FreeRTOS", "CMSIS", "FatFs", "dhara", "LVGL", "Graphviz", "RFC")
# D 类计数(覆盖率可见化:每次跑都打印,防止比例悄悄回落)
SEC_STATS = Counter()
# 机器判不了目标的 § 引用清单(--section-todo 打印,供逐步改成紧邻写法)
SEC_TODO = []
# 昵称别名表:昵称 → 仓库根相对路径
ALIAS_FILE = REPO_ROOT / "scripts" / "section_aliases.json"
_alias_map = None
def is_placeholder(text: str) -> bool:
if "*" in text or "<" in text or ">" in text or "{" in text or "}" in text:
@@ -149,6 +197,286 @@ def check_backtick_paths(path: pathlib.Path, lines, errors):
errors.append((path, lineno, target, "路径引用"))
# --- D 类:章节引用 §N.M -------------------------------------------------
_doc_index = None # 文件名 -> [Path]
_head_cache = {} # 文档绝对路径 -> 该文档的数字编号标题集合
def build_doc_index():
"""全库 .md 索引(按文件名),用于解析裸文件名引用。"""
global _doc_index
if _doc_index is not None:
return _doc_index
idx = {}
for p in REPO_ROOT.rglob("*.md"):
if is_skipped(p):
continue
idx.setdefault(p.name, []).append(p)
_doc_index = idx
return idx
def numbered_headings(path: pathlib.Path):
"""目标文档里出现的数字编号标题(`## 3. xxx` / `### 2.1.1 xxx`)。"""
key = str(path)
if key in _head_cache:
return _head_cache[key]
out = set()
try:
text = path.read_text(encoding="utf-8")
except (UnicodeDecodeError, OSError):
_head_cache[key] = out
return out
for line in text.splitlines():
m = HEAD_RE.match(line)
if not m:
continue
m2 = HEAD_NUM_RE.match(m.group(1).strip())
if m2:
out.add(m2.group(1))
_head_cache[key] = out
return out
def stem_candidates(token: str, by_stem):
"""解析裸文档名:先全名相等,再按前缀(`V0.0` → `V0.0_网表详细分析报告.md`)。
返回候选列表;调用方只在**唯一**时采信,所以「报告」「方案」这类会命中多篇而自动跳过。
前缀匹配要求 ≥3 字符,否则「文档」「手册」这类剥剩的通用词会乱认邻居(「主文档」→「文档」
→ 前缀命中「文档规范」)。
"""
if token in by_stem:
return by_stem[token]
if len(token) < 3:
return []
out = []
for stem, paths in by_stem.items():
if stem.startswith(token):
for p in paths:
if p not in out:
out.append(p)
return out
def load_aliases():
"""昵称别名表(`scripts/section_aliases.json`):「内存方案」→ 具体文档。
存在的意义:昵称式引用(「内存方案 §1.4」)读起来顺、机器判不了;登记一次别名,
正文不用改,引用就纳入校验。表里没有的昵称不猜(宁缺毋滥)。
"""
global _alias_map
if _alias_map is not None:
return _alias_map
_alias_map = {}
if ALIAS_FILE.exists():
try:
import json
data = json.loads(ALIAS_FILE.read_text(encoding="utf-8"))
if isinstance(data, dict):
_alias_map = {k: v for k, v in data.items() if isinstance(v, str)}
except (ValueError, OSError, UnicodeDecodeError):
pass
return _alias_map
def resolve_name(name: str, by_stem):
"""裸文档名 → 目标:先文件名(含前缀唯一),再别名表。判不了返回 (None, None)。"""
cand = stem_candidates(name, by_stem)
if len(cand) == 1:
return cand[0], "stem"
alias = load_aliases().get(name)
apath = (REPO_ROOT / alias) if alias else None
if apath is not None and apath.exists():
return apath, "alias"
return None, None
def suggest_doc(token: str, by_stem):
"""给「昵称式引用」猜一个目标文档,用于 --section-todo 的改写建议。
只在**唯一命中**时给建议:昵称整体出现在文件名里,或「昵称前段出现在目录名 +
昵称后两字出现在文件名」(「内存方案」→ docs/内存管理/RAM使用分析与优化方案.md)。
"""
if not token or len(token) < 2:
return None
hits = []
for stem, paths in by_stem.items():
for p in paths:
rel = _rel(p)
if token in stem or stem in token:
hits.append(p)
elif len(token) > 3 and token[-2:] in stem and token[:-2] in rel:
hits.append(p)
uniq = []
for p in hits:
if p not in uniq:
uniq.append(p)
return uniq[0] if len(uniq) == 1 else None
def sec_plausible(heads, ref: str) -> bool:
"""编号存在,或是现有编号的祖先(§8 → 8.2 在位)/后代。"""
return ref in heads or any(h.startswith(ref + ".") for h in heads)
def sec_parent_exists(heads, ref: str) -> bool:
"""ref 的父级编号在文档里存在——不存在说明这个数量级不属于本文档,
该 § 其实是「省略了文档名的跨文档引用」,机器判不了(跳过而非误报)。"""
parts = ref.split(".")
if len(parts) == 1:
return any(h.split(".")[0] == parts[0] for h in heads)
parent = ".".join(parts[:-1])
return parent in heads or any(h.startswith(parent + ".") for h in heads)
# 昵称前常挂的动词/介词:「见内存方案 §1.4」「顺序按报告 §6.5」——先剥掉再查别名/文件名
CUE_STRIP_PREFIXES = ("参见", "详见", "并同步更新", "教训见", "用法与基线见", "反向依赖在",
"改动前先看", "多口需求见", "写入顺序违反", "宏位与", "顺序按",
"依据", "参照", "对照", "违反", "参考", "按", "见", "如", "该", "以",
"与", "和", "及", "或", "并", "在", "的", "第", "看", "主")
# 明确指「本文档」的昵称 → 按自引用处理
SELF_TOKENS = ("本文档", "本文", "该文", "该文档", "本节", "上文", "下文", "下面", "上方")
def strip_cue(token: str) -> str:
prev = None
while prev != token:
prev = token
for c in CUE_STRIP_PREFIXES:
if token.startswith(c) and len(token) > len(c) >= 1:
token = token[len(c):]
break
return token
def chain_blocked(between: str, by_stem) -> bool:
"""§ 链能否跨过这段文字:出现句末标点、或出现**与某文档文件名完全相同**的裸名就不许跨
(`… §3 + 看门狗设计说明 §11.4` 里的 §11.4 属于后者,不能继承前一个目标)。
只认完全相等:`内容页 / §5.6` 里的「内容页」是描述词,不该断链。"""
if len(between) > SEC_CHAIN_MAX:
return True
if any(t in between for t in SEC_TERMINATORS):
return True
if DOC_NAME_RE.search(between):
return True
for m in re.finditer(r"[A-Za-z0-9_一-鿿][\w.一-鿿]{1,30}", between):
if m.group(0) in by_stem:
return True
return False
def check_section_refs(path: pathlib.Path, lines, errors, warns):
"""D 类:§N.M 章节引用 —— 目标文档必须真有该编号标题。
目标判定优先级(能判就判,判不了就跳过并计数,口径同 mem-numbers 的「窄而准」):
1. 紧邻的文档名(`docs/x.md` / `x.md`,≤8 字符间隔)→ 唯一解析即采信
2. 同一句内的 § 链(`a.md` §3.2 … / §10.2)→ 继承前一个 § 的目标
3. 紧邻的裸文档名(`看门狗设计说明 §10`、`V0.0 §4.7`)→ 全名或前缀唯一匹配
4. 自引用线索(见/本文/本节/(/、/句首…)→ 目标 = 本文档
跳过:非 .md 锚点(`.pre-commit-config.yaml §7`)、变更记录小节、含状态词的行、
外部手册(RM0090 等)、目标文档不用数字编号标题、自引用但父级编号不存在。
两档严重性:**目标明确**的失效 = 错误(拦提交);**自引用**的失效 = 告警
(打印不拦,因为省略文档名的跨文档引用与真失效机器分不干净)。
"""
idx = build_doc_index()
by_stem = {}
for name, paths in idx.items():
for p in paths:
by_stem.setdefault(p.stem, []).append(p)
in_changelog = False
for lineno, line in enumerate(lines, 1):
hm = HEAD_RE.match(line)
if hm:
in_changelog = any(w in hm.group(1) for w in CHANGELOG_WORDS)
if EXAMPLE_MARKER in line or "[计划]" in line:
continue
prev_end = None
prev_tgt = None
prev_kind = None
for m in SEC_RE.finditer(line):
ref = m.group(1)
SEC_STATS["total"] += 1
window = line[max(0, m.start() - 120):m.start()]
tgt = None
kind = None
names = list(DOC_NAME_RE.finditer(window))
if names:
last = names[-1]
raw = urllib.parse.unquote(last.group(1))
# 注意:last 的坐标是**窗口内**的,不能拿 m.start()(整行坐标)去减
gap_txt = window[last.end():]
near = (len(gap_txt) <= SEC_MAX_GAP and SEC_GAP_RE.match(gap_txt))
if near and MD_SUFFIX_RE.search(raw):
cand = ([b / raw for b in (path.parent, REPO_ROOT)
if (b / raw).exists()]
if "/" in raw else idx.get(raw, []))
if len(cand) == 1:
tgt, kind = cand[0], "path"
elif near:
SEC_STATS["skip_nonmd_anchor"] += 1
prev_end, prev_tgt, prev_kind = None, None, None
continue
if tgt is None and prev_tgt is not None:
between = line[prev_end:m.start()]
if not chain_blocked(between, by_stem):
# 继承前一个 § 的目标**与它的判定档位**(自引用链仍按自引用处理)
tgt, kind = prev_tgt, prev_kind
if tgt is None:
sm = STEM_RE.search(window)
if sm:
stem = sm.group(1)
tgt, kind = resolve_name(stem, by_stem)
if tgt is None:
short = strip_cue(stem)
if stem in SELF_TOKENS or short in SELF_TOKENS:
tgt, kind = path, "self"
elif short != stem:
tgt, kind = resolve_name(short, by_stem)
if tgt is None and short.endswith(DOC_NOUN_TAIL):
SEC_STATS["skip_nickname"] += 1
SEC_TODO.append((path, lineno, ref, short, "昵称未解析"))
prev_end, prev_tgt, prev_kind = None, None, None
continue
if tgt is None and stem.endswith(DOC_NOUN_TAIL):
SEC_STATS["skip_nickname"] += 1
SEC_TODO.append((path, lineno, ref, stem, "昵称未解析"))
prev_end, prev_tgt, prev_kind = None, None, None
continue
if tgt is None:
if SELF_CUE_RE.search(window):
tgt, kind = path, "self"
else:
SEC_STATS["skip_unknown"] += 1
SEC_TODO.append((path, lineno, ref, "", "目标无法判定"))
prev_end, prev_tgt, prev_kind = None, None, None
continue
prev_end, prev_tgt, prev_kind = m.end(), tgt, kind
if any(h in line for h in EXTERNAL_HINTS) or "芯片手册" in window:
SEC_STATS["skip_external"] += 1
continue
if in_changelog or has_status_hint(line, m.start(), m.end() - m.start()):
SEC_STATS["skip_changelog"] += 1
continue
heads = numbered_headings(tgt)
if not heads:
SEC_STATS["skip_unnumbered"] += 1
continue
if sec_plausible(heads, ref):
SEC_STATS["checked"] += 1
continue
if kind == "self" and not sec_parent_exists(heads, ref):
SEC_STATS["skip_implicit_crossdoc"] += 1
SEC_TODO.append((path, lineno, ref, "", "省略了文档名"))
continue
SEC_STATS["checked"] += 1
where = f"§{ref}({tgt.relative_to(REPO_ROOT).as_posix()} 无此编号标题)"
if kind == "self":
warns.append((path, lineno, where))
else:
errors.append((path, lineno, where, "章节引用"))
def _rel(p: pathlib.Path) -> str:
"""仓库相对路径(正斜杠),用于与 SKIP_PARTS 前缀比较。"""
try:
@@ -210,9 +538,12 @@ def iter_docs(argv):
def main(argv):
argv = argv[1:] if len(argv) > 1 else []
strict = "--sections-strict" in argv
errors = []
warns = []
n_files = 0
for doc in iter_docs(argv[1:] if len(argv) > 1 else []):
for doc in iter_docs(argv):
if is_skipped(doc):
continue
try:
@@ -223,8 +554,55 @@ def main(argv):
lines = text.splitlines()
check_md_links(doc, lines, errors)
check_backtick_paths(doc, lines, errors)
check_section_refs(doc, lines, errors, warns)
check_name(doc, errors)
# 覆盖率可见化:每次跑都打印,防止「可校验比例」悄悄回落
total = SEC_STATS["total"]
checked = SEC_STATS["checked"]
if total:
print(f"章节引用:共 {total} 处,机器可判定 {checked} 处"
f"({100.0 * checked / total:.0f}%);跳过 → 昵称式 {SEC_STATS['skip_nickname']}"
f" / 非文档锚点 {SEC_STATS['skip_nonmd_anchor']}"
f" / 变更记录 {SEC_STATS['skip_changelog']}"
f" / 外部手册 {SEC_STATS['skip_external']}"
f" / 省略文档名的跨文档引用 {SEC_STATS['skip_implicit_crossdoc']}"
f" / 目标无编号体系 {SEC_STATS['skip_unnumbered']}"
f" / 无法判定 {SEC_STATS['skip_unknown']}")
if warns:
print(f"\n⚠️ 章节引用待确认({len(warns)} 处:按自引用解析后本文档没有该编号;"
"不拦提交):")
for path, lineno, where in warns[:40]:
try:
rel = path.relative_to(REPO_ROOT)
except ValueError:
rel = path
print(f" {rel}:{lineno} {where}")
if len(warns) > 40:
print(f" … 其余 {len(warns) - 40} 处略")
print(" 修法:确认它到底指哪篇 → 写成「`路径.md` §N.M」(此后自动纳入硬门禁),"
"或改指本文档现有编号。")
if strict:
for path, lineno, where in warns:
errors.append((path, lineno, where, "章节引用(自引用,--sections-strict)"))
if "--section-todo" in argv and SEC_TODO:
by_stem = {}
for name, paths in build_doc_index().items():
for p in paths:
by_stem.setdefault(p.stem, []).append(p)
print(f"\n📋 待改成紧邻写法的章节引用({len(SEC_TODO)} 处,机器判不了目标):")
for path, lineno, ref, token, why in SEC_TODO:
try:
rel = path.relative_to(REPO_ROOT)
except ValueError:
rel = path
sug = suggest_doc(token, by_stem) if token else None
tip = ""
if sug is not None:
tip = f" → 建议改写:`{sug.relative_to(REPO_ROOT).as_posix()}` §{ref}"
print(f" {rel}:{lineno} §{ref} [{why}]{(' ' + token) if token else ''}{tip}")
if errors:
print(f"发现的失效文档引用 / 命名问题({len(errors)} 处):")
for path, lineno, target, kind in errors:
@@ -239,6 +617,9 @@ def main(argv):
"已删除的文件请写明「已删除」状态词;"
"引用报错样例/日志片段请在该行加 `[示例]` 标记;"
"命名规范见 docs/文档规范.md §1。")
print("章节引用失效([章节引用]):目标文档已无该编号小节 → 改指现有编号或直接写小节名。"
"可校验的写法:`路径.md` §N.M / 裸文件名 §N.M / 文档名前缀(V0.0)/ 已登记别名;"
"判不了目标的跑 --section-todo 看清单与改写建议。")
return 1
print(f"文档路径检查通过({n_files} 个文档)。")
+301
View File
@@ -0,0 +1,301 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""check_registration.py - 双端注册反向校验门禁(纯标准库)。
背景(登记簿 N-23,出处审查报告 N-06 残余):
本工程 Keil 工程与 PC 模拟器都用**显式文件列表**(无通配扫描)。新 `.c`
必须手动注册两处才会被编译:
① `MDK-ARM/STM32F407-Demo.uvprojx` 的 `<FilePath>` 条目(MCU 固件构建)
② `simulator/CMakeLists.txt` 的显式源列表(PC 模拟器构建)
历史上 `App/task/rs485_task.c`、`Drivers/BSP/NET/lftpd/lftpd_log.c` **两处都没注册**
= 写了却从不参与任何一端编译的**死代码**,误导读者以为功能已生效。
本脚本做什么(三类缺口):
扫描 `App/`、`Drivers/BSP/`、`Src/` 下全部 `.c`(排除第三方),对照两份注册表:
① 存在但未进 uvprojx -> 固件不编译(死代码 / 漏注册)
② 存在但未进 CMakeLists -> 模拟器不编译(**仅当该文件本应进模拟器时**才是缺口;
MCU-only 文件不进模拟器是**正确**的,见下方豁免)
③ 注册了但文件不存在(反向)-> 幽灵注册(改名/删除后残留的注册条目)
豁免机制(关键):
模拟器只建模 **UI + 网络配置共享核心**;其余模块(MCU 驱动 / 固件任务 / 纯算法 /
CubeMX 生成层)**不进模拟器是设计事实**,模拟器另有 `_sim` 替身或根本无对应。
豁免用**显式清单 + 每行理由注释**(命名模式 `*_mcu.c` + 个别点名),
**不做整目录一刀切**(N-23 教训正是"目录级盲区")。唯一例外是 `Src/`:
CubeMX 生成的外设初始化/中断/main,按构造即 MCU 专属、不可能有模拟器替身,
故整目录豁免(任务口径明确认可"Src/ 全部")。
已知缺口过渡(存量不拦、只对新缺口 FAIL):
`KNOWN_GAPS` 登记存量缺口(N-23 存量,Lead 处置中):命中 -> 打印 `[WARN]` 不拦;
新出现的缺口 -> 打印 `[GAP]` 并 FAIL。`--strict` 关闭白名单(存量也 FAIL),供审计。
退出码:
0 = 一致(无新缺口;存量缺口已白名单 WARN)
1 = 有缺口(存在未白名单的新缺口或幽灵注册)
2 = 脚本环境错误(注册表文件缺失 / 无法读取)
用法(仓库根目录):
& '.venv\\Scripts\\python.exe' scripts/check_registration.py
& '.venv\\Scripts\\python.exe' scripts/check_registration.py --strict
设计依据:docs/待处理内容.md N-23;.dsh/skills/new-source-file
创建日期:2026-10-12
"""
import argparse
import fnmatch
import os
import re
import sys
# --- 仓库根(scripts/ 的上一级)---
REPO_ROOT = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
UVPROJX = os.path.join(REPO_ROOT, "MDK-ARM", "STM32F407-Demo.uvprojx")
CMAKELISTS = os.path.join(REPO_ROOT, "simulator", "CMakeLists.txt")
SIM_DIR = os.path.join(REPO_ROOT, "simulator")
# --- 扫描范围与第三方排除 ---
SCAN_ROOTS = ["App", "Drivers/BSP", "Src"]
# 第三方目录(即便落在扫描根下也跳过)
THIRD_PARTY = [
"Drivers/STM32F4xx_HAL_Driver",
"Drivers/CMSIS",
"Middlewares",
"Lib",
]
# --- 模拟器豁免清单(category ②:不进模拟器是**正确**的)---
# 每项 = (fnmatch 模式(对仓库相对 posix 路径匹配), 理由)。
# 原则:命名模式 + 个别点名,不整目录一刀切;唯一目录级豁免 Src/(CubeMX 生成,MCU 专属)。
SIM_EXEMPT = [
# CubeMX 生成的外设初始化 / 中断向量 / main / freertos:按构造即 MCU 专属,
# 模拟器无对应替身(任务口径明确认可"Src/ 全部")。唯一目录级豁免。
("Src/*", "CubeMX 生成的外设初始化/中断/main,MCU 专属,模拟器无替身"),
# MCU 平台后端:命名约定 *_mcu.c,模拟器另有同名 *_sim.c 替身(在 simulator/sim/)。
("*_mcu.c", "MCU 平台后端,模拟器另有 *_sim.c 替身"),
# --- App/:固件任务 / MCU 依赖 / 纯算法(模拟器无消费路径)---
("App/algo/adc_health.c", "L1 有效性校验纯算法,模拟器无消费路径"),
("App/algo/adc_input.c", "依赖 HAL/TPAFE 取数,MCU 专属"),
("App/algo/phasor.c", "L2 相量纯算法,模拟器无消费路径"),
("App/algo/prot_elm.c", "L3 保护元件纯算法,模拟器无消费路径"),
("App/algo/prot_set.c", "L5 保护定值纯算法,模拟器无消费路径"),
("App/algo/sample_ring.c", "L2 环形缓冲纯算法,模拟器无消费路径"),
("App/app/app_main.c", "固件启动/就绪屏障,MCU 专属"),
("App/fs/vol_lock.c", "FreeRTOS 互斥量卷锁,MCU 专属"),
("App/log/console_log.c", "MCU 控制台输出,模拟器无消费路径"),
("App/sync/time_sync.c", "对时任务(net+RTOS),MCU 专属"),
("App/task/adc_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"),
("App/task/default_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"),
("App/task/net_task.c", "固件 FreeRTOS 任务,模拟器不跑固件任务"),
("App/task/rs485_task.c", "固件 RS-485 任务,MCU 专属(另:uvprojx 缺口见 KNOWN_GAPS)"),
("App/task/ui_task.c", "固件 UI 任务,模拟器另有 main.c 驱动 UI"),
("App/util/sha256.c", "纯工具(FTP 鉴权在 MCU 端),模拟器无消费路径"),
("App/util/sys_clock.c", "RTC/SD2506 依赖墙钟,MCU 专属"),
("App/util/task_prof.c", "任务性能探针,MCU 专属"),
("App/util/task_watchdog.c", "看门狗判定(RTOS 依赖),MCU 专属"),
# --- Drivers/BSP/:MCU 芯片/网络驱动与 FTP 服务(模拟器无对应)---
("Drivers/BSP/CH395F/ch395f.c", "MCU SPI 网卡驱动"),
("Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue.c", "MCU SPI NAND 驱动"),
("Drivers/BSP/GD5F2GQ5UE/nand_ftl.c", "dhara FTL 适配,MCU 存储栈"),
("Drivers/BSP/NET/lftpd/lftpd.c", "MCU FTP 服务器"),
("Drivers/BSP/NET/lftpd/lftpd_auth.c", "MCU FTP 鉴权"),
("Drivers/BSP/NET/lftpd/lftpd_inet.c", "MCU FTP 网络层"),
("Drivers/BSP/NET/lftpd/lftpd_io.c", "MCU FTP 文件 IO"),
("Drivers/BSP/NET/lftpd/lftpd_string.c", "MCU FTP 字符串工具"),
("Drivers/BSP/NET/net_select.c", "MCU socket 选择层"),
("Drivers/BSP/NET/net_socket.c", "MCU socket 后端,模拟器另有 net_socket_sim.c"),
("Drivers/BSP/RS485/rs485.c", "MCU RS-485 驱动"),
("Drivers/BSP/SD2506/sd2506.c", "MCU RTC 驱动"),
("Drivers/BSP/TPAFE5160/tpafe5160.c", "MCU 计量芯片驱动"),
("Drivers/BSP/UI/ui_port.c", "MCU UI 端口层(显示/触摸硬件),模拟器另有 sim/sim_port.c"),
]
# --- 已知缺口白名单(存量,N-23,Lead 已处置)---
# 命中 -> WARN 不拦;新缺口 -> GAP FAIL。仅针对"存在但未注册"方向。
KNOWN_GAPS = {
"App/task/rs485_task.c": "E-59 门禁刻意未接线(B2 消费者就绪时随任务立项注册)",
}
def to_repo_rel(path):
"""把绝对/相对路径归一为仓库相对 posix 路径。"""
p = os.path.normpath(path)
rel = os.path.relpath(p, REPO_ROOT)
return rel.replace("\\", "/")
def is_scan_scope(repo_rel):
"""是否在扫描范围内(App/、Drivers/BSP/、Src/ 下的 .c,排除第三方)。"""
if not repo_rel.endswith(".c"):
return False
for tp in THIRD_PARTY:
if repo_rel == tp or repo_rel.startswith(tp + "/"):
return False
for root in SCAN_ROOTS:
if repo_rel.startswith(root + "/"):
return True
return False
def scan_sources():
"""扫描扫描根下全部 .c,返回仓库相对 posix 路径集合。"""
found = set()
for root in SCAN_ROOTS:
abs_root = os.path.join(REPO_ROOT, root.replace("/", os.sep))
if not os.path.isdir(abs_root):
continue
for dirpath, _dirnames, filenames in os.walk(abs_root):
for fn in filenames:
if fn.endswith(".c"):
abs_p = os.path.join(dirpath, fn)
rel = to_repo_rel(abs_p)
if is_scan_scope(rel):
found.add(rel)
return found
def parse_uvprojx():
"""解析 uvprojx 的 <FilePath>,返回仓库相对 posix 路径集合(含 .c/.s)。"""
with open(UVPROJX, "r", encoding="utf-8", errors="replace") as f:
text = f.read()
mdk_dir = os.path.dirname(UVPROJX)
entries = set()
for m in re.finditer(r"<FilePath>(.*?)</FilePath>", text):
raw = m.group(1).strip()
if not raw:
continue
# uvprojx 路径相对 MDK-ARM/ 目录,反斜杠或正斜杠混用
joined = os.path.join(mdk_dir, raw.replace("\\", os.sep))
entries.add(to_repo_rel(joined))
return entries
def _expand_var(s, varmap, depth=0):
"""递归展开 ${VAR}。"""
if depth > 12:
return s
def repl(m):
name = m.group(1)
val = varmap.get(name, m.group(0))
return _expand_var(val, varmap, depth + 1)
return re.sub(r"\$\{(\w+)\}", repl, s)
def parse_cmake():
"""解析 CMakeLists 的 *_SOURCES 列表(先展开变量),返回仓库相对 posix 路径集合。"""
with open(CMAKELISTS, "r", encoding="utf-8", errors="replace") as f:
text = f.read()
# 1) 单值 set(NAME value):目录变量(不含 *_SOURCES)
varmap = {"CMAKE_CURRENT_SOURCE_DIR": SIM_DIR}
for m in re.finditer(r'set\s*\(\s*([A-Za-z_]\w*)\s+("?)([^"\r\n]*?)\2\s*\)', text):
name = m.group(1)
val = m.group(3).strip()
if name.endswith("_SOURCES"):
continue
varmap[name] = val
# 2) 多值 set(NAME ... ) 中名字以 _SOURCES 结尾的块
sources = set()
for m in re.finditer(r"set\s*\(\s*([A-Za-z_]\w*_SOURCES)\b(.*?)\)", text, re.DOTALL):
body = m.group(2)
# 逐行剥注释
lines = []
for line in body.splitlines():
idx = line.find("#")
if idx >= 0:
line = line[:idx]
lines.append(line)
body = " ".join(lines)
for tok in body.split():
expanded = _expand_var(tok, varmap)
if not expanded:
continue
# 展开后若仍是绝对/带盘符路径直接用,否则相对 simulator/ 目录
if os.path.isabs(expanded) or re.match(r"^[A-Za-z]:[\\/]", expanded):
cand = expanded
else:
cand = os.path.join(SIM_DIR, expanded)
sources.add(to_repo_rel(cand))
return sources
def main():
ap = argparse.ArgumentParser(description="双端注册反向校验(uvprojx + CMakeLists)")
ap.add_argument("--strict", action="store_true",
help="关闭 KNOWN_GAPS 白名单:存量缺口也按 FAIL 处理(供审计)")
args = ap.parse_args()
# 环境校验
for path, label in ((UVPROJX, "uvprojx"), (CMAKELISTS, "CMakeLists.txt")):
if not os.path.isfile(path):
print("[ENV-ERROR] 注册表缺失: %s (%s)" % (label, path))
return 2
try:
scanned = scan_sources()
uv = parse_uvprojx()
cmake = parse_cmake()
except OSError as e:
print("[ENV-ERROR] 读取失败: %s" % e)
return 2
uv_c = {p for p in uv if is_scan_scope(p)}
cmake_c = {p for p in cmake if is_scan_scope(p)}
gaps = [] # 新缺口 -> FAIL
warns = [] # 存量白名单 -> WARN
# ① 存在但未进 uvprojx(无豁免:扫描范围内每个 .c 都应编入固件)
for f in sorted(scanned):
if f not in uv:
if (not args.strict) and f in KNOWN_GAPS:
warns.append("[WARN] %s | 缺 uvprojx(存量:%s)" % (f, KNOWN_GAPS[f]))
else:
gaps.append("[GAP] %s | 缺 uvprojx" % f)
# ② 存在但未进 CMakeLists(MCU-only 豁免)
for f in sorted(scanned):
if f in cmake_c:
continue
if any(fnmatch.fnmatch(f, pat) for pat, _ in SIM_EXEMPT):
continue
if (not args.strict) and f in KNOWN_GAPS:
warns.append("[WARN] %s | 缺 CMakeLists(存量:%s)" % (f, KNOWN_GAPS[f]))
else:
gaps.append("[GAP] %s | 缺 CMakeLists" % f)
# ③ 幽灵注册:注册了但文件不存在(反向)
for f in sorted(uv_c):
if not os.path.isfile(os.path.join(REPO_ROOT, f.replace("/", os.sep))):
gaps.append("[GAP] %s | 幽灵注册(uvprojx)" % f)
for f in sorted(cmake):
if not os.path.isfile(os.path.join(REPO_ROOT, f.replace("/", os.sep))):
gaps.append("[GAP] %s | 幽灵注册(CMakeLists)" % f)
# 输出
print("== 双端注册反向校验 ==")
print("扫描 .c: %d | uvprojx(扫描范围): %d | CMakeLists(扫描范围): %d"
% (len(scanned), len(uv_c), len(cmake_c)))
for w in warns:
print(w)
for g in gaps:
print(g)
if gaps:
print("结果: FAIL(%d 个新缺口/幽灵注册,%d 个存量 WARN)" % (len(gaps), len(warns)))
return 1
if warns:
print("结果: PASS(无新缺口;%d 个存量缺口已白名单 WARN,N-23 Lead 处置中)" % len(warns))
return 0
print("结果: PASS(双端注册一致)")
return 0
if __name__ == "__main__":
sys.exit(main())
+11 -5
View File
@@ -19,17 +19,23 @@ MAP_DEFAULT = os.path.join(
)
# Rows in "Image component sizes" that re-aggregate objects (would double-count sums).
TOTALS_ROWS = {"Object Totals", "Library Totals", "User Totals", "mc_w.l", "mf_w.l"}
TOTALS_ROWS = {"Object Totals", "Library Totals", "User Totals"}
# ARM library aggregate rows (e.g. mc_w.l / mf_w.l / m_wm.l): the linker prints one
# aggregate line per library in addition to the individual member .o lines, so any
# "<lib>.l" row is a re-aggregation and must be dropped from per-file sums.
# (2026-10-11: sha256's 64-bit shifts pulled in m_wm.l — the hardcoded name list
# missed it and the self-consistency check went +58; match the name pattern instead.)
_LIB_AGG_RE = re.compile(r'^[a-z]{1,2}_[a-z0-9]+\.l$')
def _is_aggregate(name):
"""Aggregate rows to drop from per-file sums: library-name summaries and Totals lines.
'(incl. Padding)' / '(incl. Generated)' are linker metadata; they are excluded from
per-file tables but their contribution IS counted in Grand Totals, so the per-file
sums will differ from Grand Totals by the alignment-padding delta (~300B here).
'(incl. Padding)' / '(incl. Generated)' are linker metadata ledger rows; they ARE
kept so that per-file sums reconcile exactly with Grand Totals (self-check Δ=0).
"""
return name in TOTALS_ROWS
return name in TOTALS_ROWS or bool(_LIB_AGG_RE.match(name))
def parse_execution_region_header(line):
+395
View File
@@ -0,0 +1,395 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
改名 / 挪目录 + 全库引用自动改写(把"同步引用链接"从手工活变成一条命令)。
背景:本仓库 1800+ 处路径引用(`docs/xxx.md` 反引号引用 + Markdown 链接 +
`#include` + `uvprojx`/`CMakeLists.txt` 源文件列表),改名或挪目录后要靠人
逐文件扫改(历史上一次改名波及 10~30 个文件)。本脚本把这件事机械化。
用法:
python scripts/rename_ref.py <旧路径> <新路径> # 默认 dry-run,只打印将要改什么
python scripts/rename_ref.py <旧路径> <新路径> --apply # 真正落盘(git mv + 改写引用)
python scripts/rename_ref.py <旧路径> <新目录/ # 挪进目录(保留文件名)
python scripts/rename_ref.py docs/a/ docs/b/ --apply # 整目录搬迁(前缀改写)
路径一律写**仓库根相对路径**(正斜杠),末尾不带 `/` 也可。
改写覆盖的引用形态:
1. 反引号根相对引用 `docs/xxx/yyy.md`
2. Markdown 链接 [文本](docs/xxx/yyy.md)(根相对) / [文本](../xxx/yyy.md)(文档相对)
3. 图片链接 ![x](images/a.svg) —— 含"文档挪层级后 ../ 前缀要跟着变"这条实证坑
4. C 的 #include #include "yyy.h"(改名时连裸文件名引用一起改)
5. 构建文件 MDK-ARM/*.uvprojx 的 <FilePath>、simulator/CMakeLists.txt 的
${PROJECT_ROOT}/… 与 ${VAR}/… 源文件列表
6. 任意文本文件里的路径串(.py/.bat/.yaml/.json/.txt/skill 等)
保真约定(见 docs/文档规范.md §2、scripts/README.md「脚本编写约定」):
* 按原样回写 BOM 与换行符(LF/CRLF),无改动的文件不写
* 只扫 git 跟踪的文本文件;第三方/生成物目录跳过
* 默认 dry-run;--apply 才落盘,且落盘后自动跑 check_doc_links.py --all 复核
退出码:0 成功;1 有错误(旧路径不存在 / 新路径已存在 / 复核失败);2 用法错误。
"""
import argparse
import os
import pathlib
import re
import subprocess
import sys
import urllib.parse
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
# 不扫描的路径片段(第三方 / 生成物 / 本地状态)
SKIP_PARTS = ("Lib/", "Middlewares/", "Drivers/STM32F4xx_HAL_Driver/",
"Drivers/CMSIS/", "MDK-ARM/objects/", "MDK-ARM/Listings/",
"MDK-ARM/RTE/", "MDK-ARM/STM32F407-Demo/", ".venv/",
"node_modules/", "__pycache__/", "build/", "obj/",
".workbuddy-ai/", ".mnemon/", "simulator/SDL2/",
"docs/芯片手册/")
# 生成物:不改写,只在结尾提示重跑生成器(改了也是白改,下次生成会覆盖)
GENERATED = ("docs/images/引用关系图.",)
# 不扫描的扩展名(二进制 / 生成物)
SKIP_EXT = {".o", ".d", ".map", ".axf", ".bin", ".hex", ".elf", ".htm", ".lst",
".png", ".jpg", ".jpeg", ".gif", ".ttf", ".woff", ".dll", ".lib",
".pyc", ".zip", ".gz", ".pdf", ".docx", ".xlsx", ".ico", ".cur"}
# 路径 token:至少含一个分隔符;允许 ${VAR} 前缀、中文、点、下划线、短横
CH = r"[\w$.\-{}一-鿿]"
TOKEN_RE = re.compile(CH + r"+(?:[/\\]" + CH + r"+)+")
VAR_PREFIX_RE = re.compile(r"^(\$\{[^}]+\}/)(.+)$")
INCLUDE_RE = re.compile(r'(#\s*include\s*)([<"])([^<>"\n]+)([">])')
FILEPATH_RE = re.compile(r"(<FilePath>)([^<\n]+)(</FilePath>)")
BOM = b"\xef\xbb\xbf"
def rel(p: pathlib.Path) -> str:
"""仓库根相对 POSIX 路径。"""
try:
return p.resolve().relative_to(REPO_ROOT).as_posix()
except ValueError:
return p.as_posix()
def is_skipped(relpath: str) -> bool:
for s in SKIP_PARTS:
if relpath == s.rstrip("/") or relpath.startswith(s):
return True
return False
def is_generated(relpath: str) -> bool:
"""生成物(引用关系图等):跳过改写,只提示重跑生成器。"""
return any(relpath.startswith(g) for g in GENERATED)
def tracked_files():
"""git 跟踪 + 未跟踪但未忽略的文件(自动排除生成物与 .gitignore 内容)。"""
out = subprocess.run(["git", "ls-files", "-z", "--cached", "--others",
"--exclude-standard"],
cwd=REPO_ROOT, stdout=subprocess.PIPE, check=True).stdout
for name in out.decode("utf-8").split("\0"):
if not name:
continue
name = name.replace("\\", "/")
if is_skipped(name):
continue
if pathlib.Path(name).suffix.lower() in SKIP_EXT:
continue
p = REPO_ROOT / name
if p.is_file():
yield p
def read_text(p: pathlib.Path):
"""返回 (text, bom, ok)。二进制/非 UTF-8 → ok=False。"""
data = p.read_bytes()
bom = data.startswith(BOM)
if bom:
data = data[len(BOM):]
try:
return data.decode("utf-8"), bom, True
except UnicodeDecodeError:
return None, bom, False
def resolve_token(tok: str, file_dir: pathlib.Path, old_abs: pathlib.Path):
"""把 token 解析成绝对路径;解析不到 old 就返回 (None, "", False)。
返回 (解析到的绝对路径, ${VAR}/ 前缀, 是否按仓库根解析)。
依次尝试:${VAR}/ 前缀剥离 → 相对仓库根 → 相对引用文件目录 → 尾段后缀匹配。
"""
var_prefix = ""
body = tok
m = VAR_PREFIX_RE.match(tok)
if m:
var_prefix, body = m.group(1), m.group(2)
unquoted = urllib.parse.unquote(body)
roots = []
if not unquoted.startswith("."):
roots.append((REPO_ROOT, True))
roots.append((file_dir, False))
if unquoted.startswith("."):
roots.insert(0, (REPO_ROOT, True))
for base, via_root in roots:
for b in (unquoted, body):
b2 = b.rstrip(".")
try:
rc = pathlib.Path(os.path.normpath(base / b2))
except (OSError, ValueError):
continue
if rc == old_abs or str(rc).startswith(str(old_abs) + os.sep):
return rc, var_prefix, via_root
# ${VAR}/name.ext 之类:变量指向的目录未知,按「尾段后缀」匹配
parts = re.split(r"[/\\]", body)
old_parts = list(old_abs.parts)
if var_prefix and len(parts) <= 2:
if parts[-1] == old_parts[-1] and (len(parts) == 1 or parts[-2] == old_parts[-2]):
return old_abs, var_prefix, True
return None, "", False
def resolve_relative(tok: str, file_dir: pathlib.Path):
"""按「相对引用文件所在目录」解析,且目标必须真实存在(用于被移动文件的出链)。"""
if VAR_PREFIX_RE.match(tok):
return None
for b in (urllib.parse.unquote(tok), tok):
b2 = b.rstrip(".")
p = pathlib.Path(os.path.normpath(file_dir / b2))
if p.exists():
return p
return None
def rel_spelling(tok: str, target: pathlib.Path, base_dir: pathlib.Path) -> str:
"""保持原分隔符风格,重算相对 base_dir 的写法。"""
body = os.path.relpath(target, base_dir).replace("\\", "/")
if "\\" in tok:
body = body.replace("/", "\\")
if "%" in tok:
body = urllib.parse.quote(body, safe="/")
return body
def new_spelling(tok: str, resolved: pathlib.Path, var_prefix: str, via_root: bool,
base_dir: pathlib.Path) -> str:
"""按原写法风格算出新 token(根相对保持根相对,相对保持相对)。"""
old_abs = old_abs_global[0]
new_abs = new_abs_global[0]
if resolved == old_abs:
remainder = pathlib.Path(".")
else:
remainder = resolved.relative_to(old_abs)
target = new_abs if remainder == pathlib.Path(".") else new_abs / remainder
if var_prefix or via_root:
new_body = os.path.relpath(target, REPO_ROOT)
else:
new_body = os.path.relpath(target, base_dir)
if "\\" in tok:
new_body = new_body.replace("/", "\\")
else:
new_body = new_body.replace("\\", "/")
if "%" in tok:
new_body = urllib.parse.quote(new_body, safe="/")
return var_prefix + new_body
old_abs_global = [None]
new_abs_global = [None]
def rewrite_text(text: str, file_path: pathlib.Path, changes: list, lineno_map=True):
"""改写一个文件里的所有引用;changes 收集 (lineno, old, new)。"""
old_abs = old_abs_global[0]
new_abs = new_abs_global[0]
file_abs = file_path.resolve()
# 被移动文件(含整目录搬迁时目录内的文件)自身的相对链接(图片等):
# **解析**用旧目录(链接当前指向的位置),**改写**用新目录
# ——docs/文档规范.md §3.2 记的实证坑(迁目录后 images/ 未跟着改 ../)
moved = (file_abs == old_abs
or str(file_abs).startswith(str(old_abs) + os.sep))
resolve_dir = file_path.parent
emit_dir = new_abs.parent if moved else file_path.parent
out = []
pos = 0
for m in TOKEN_RE.finditer(text):
tok = m.group(0)
resolved, var_prefix, via_root = resolve_token(tok, resolve_dir, old_abs)
if resolved is None and moved:
# 被移动文件自身的**出链**(图片等):目标没变但基准目录变了 → 重算写法
own = resolve_relative(tok, resolve_dir)
if own is not None:
rep = rel_spelling(tok, own, emit_dir)
if rep != tok:
lineno = text.count("\n", 0, m.start()) + 1
changes.append((lineno, tok, rep))
out.append(text[pos:m.start()])
out.append(rep)
pos = m.end()
continue
if resolved is None:
continue
rep = new_spelling(tok, resolved, var_prefix, via_root, emit_dir)
if rep == tok:
continue
lineno = text.count("\n", 0, m.start()) + 1
changes.append((lineno, tok, rep))
out.append(text[pos:m.start()])
out.append(rep)
pos = m.end()
out.append(text[pos:])
new_text = "".join(out)
# 改名时:裸文件名引用(#include "x.h"、<FilePath>x.s</FilePath>)
old_name = old_abs.name
new_name = new_abs.name
if old_name != new_name and old_abs != new_abs:
def fix_include(m):
inc = m.group(3)
if pathlib.PurePosixPath(inc.replace("\\", "/")).name == old_name:
base = inc[: len(inc) - len(old_name)]
new_inc = base + new_name
changes.append((text.count("\n", 0, m.start()) + 1, inc, new_inc))
return m.group(1) + m.group(2) + new_inc + m.group(4)
return m.group(0)
def fix_filepath(m):
fp = m.group(2)
if pathlib.PurePosixPath(fp.replace("\\", "/")).name == old_name:
base = fp[: len(fp) - len(old_name)]
new_fp = base + new_name
changes.append((text.count("\n", 0, m.start()) + 1, fp, new_fp))
return m.group(1) + new_fp + m.group(3)
return m.group(0)
new_text = INCLUDE_RE.sub(fix_include, new_text)
if file_path.suffix.lower() in (".uvprojx", ".uvoptx"):
new_text = FILEPATH_RE.sub(fix_filepath, new_text)
return new_text
def main(argv=None):
ap = argparse.ArgumentParser(
description="改名/挪目录并自动改写全库引用(默认 dry-run)")
ap.add_argument("old", help="旧路径(仓库根相对)")
ap.add_argument("new", help="新路径(仓库根相对;以 / 结尾表示挪进该目录)")
ap.add_argument("--apply", action="store_true", help="真正落盘(默认只预览)")
ap.add_argument("--no-git", action="store_true", help="用 os.rename 代替 git mv")
ap.add_argument("--no-verify", action="store_true", help="跳过 check_doc_links 复核")
args = ap.parse_args(argv)
old_rel = args.old.replace("\\", "/").rstrip("/")
new_rel = args.new.replace("\\", "/")
if new_rel.endswith("/"):
new_rel = new_rel + old_rel.rsplit("/", 1)[-1]
new_rel = new_rel.rstrip("/")
if not new_rel or old_rel == new_rel:
print("错误:新旧路径相同或新路径为空")
return 2
old_abs = (REPO_ROOT / old_rel).resolve()
new_abs = (REPO_ROOT / new_rel).resolve()
if not old_abs.exists():
print(f"错误:旧路径不存在:{old_rel}")
return 1
if new_abs.exists():
print(f"错误:新路径已存在:{new_rel}")
return 1
if is_skipped(old_rel):
print(f"错误:{old_rel} 属于第三方/生成物范围,不应由本脚本改写")
return 1
old_abs_global[0] = old_abs
new_abs_global[0] = new_abs
plan = []
hit_generated = False
for f in tracked_files():
if is_generated(rel(f)):
if resolve_token(old_rel, f.parent, old_abs)[0] is not None:
hit_generated = True
continue
text, bom, ok = read_text(f)
if not ok:
continue
changes = []
new_text = rewrite_text(text, f, changes)
if changes:
plan.append((f, text, new_text, bom, changes))
n_refs = sum(len(c) for _, _, _, _, c in plan)
print(f"计划:{old_rel} → {new_rel}")
print(f"受影响文件 {len(plan)} 个,引用改写 {n_refs} 处\n")
for f, _, _, _, changes in plan:
print(f"── {rel(f)} ({len(changes)} 处)")
for lineno, o, n in changes[:12]:
print(f" :{lineno} {o} → {n}")
if len(changes) > 12:
print(f" … 其余 {len(changes) - 12} 处略")
print()
if not args.apply:
print("(dry-run:未做任何修改。确认无误后加 --apply)")
return 0
# 1) 先移动文件本身
new_abs.parent.mkdir(parents=True, exist_ok=True)
if args.no_git:
os.rename(old_abs, new_abs)
else:
r = subprocess.run(["git", "mv", "--", old_rel, new_rel],
cwd=REPO_ROOT, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL)
if r.returncode != 0:
os.rename(old_abs, new_abs)
print(f"已移动:{old_rel} → {new_rel}")
# 2) 再改写引用(保 BOM / 换行)
# 被移动文件本身已换位置 → 内容必须写到**新**路径,否则会在旧路径复活一份
def mapped(f: pathlib.Path) -> pathlib.Path:
fa = f.resolve()
if fa == old_abs:
return new_abs
if str(fa).startswith(str(old_abs) + os.sep):
return new_abs / fa.relative_to(old_abs)
return f
for f, _, new_text, bom, _ in plan:
target = mapped(f)
target.parent.mkdir(parents=True, exist_ok=True)
data = new_text.encode("utf-8")
if bom:
data = BOM + data
target.write_bytes(data)
print(f"已改写 {len(plan)} 个文件、{n_refs} 处引用")
# 2b) 收尾提示(脚本管不到的部分)
if hit_generated:
print("ℹ 引用关系图是生成物,未改写 → 重跑 & 'tools\\refgraph\\gen_refgraph.bat'")
if old_abs.suffix.lower() in (".c", ".h") or new_abs.suffix.lower() in (".c", ".h"):
print("ℹ 改了 .c/.h:已同步 uvprojx 与 CMakeLists;若**新增目录**还需把目录加进 "
"uvprojx 的 <IncludePath>(见 new-source-file skill),随后跑 @build 验证")
print("ℹ 正文里的**裸文件名/昵称引用**(如「内存方案 §1.4」「app_main.c/h」)无法机械判定,"
"已尽量改写,仍建议 grep 一遍旧名:git grep -n '<旧文件名>'")
# 3) 复核
if not args.no_verify:
r = subprocess.run([sys.executable, str(REPO_ROOT / "scripts" / "check_doc_links.py"),
"--all"], cwd=REPO_ROOT)
if r.returncode != 0:
print("\n⚠️ check_doc_links 未通过——上面列出的断链需人工确认(多为裸文件名引用)")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())
+6
View File
@@ -0,0 +1,6 @@
{
"内存方案": "docs/内存管理/RAM使用分析与优化方案.md",
"架构总览": "docs/架构设计/项目架构总览.md",
"规划": "docs/算法/继电保护功能复现规划.md",
"V0.0": "docs/硬件平台/V0.0_网表详细分析报告.md"
}
+1
View File
@@ -104,6 +104,7 @@ set(SYSLOG_SOURCES
# 网络配置(双端共享核心 + cfg_file 引擎 + 平台存储后端;MCU 端编 cfg_file_store_mcu.c,见 Keil 工程)
set(NETCFG_SOURCES
${NETCFG_DIR}/net_cfg.c
${NETCFG_DIR}/ftp_cfg.c
${PROJECT_ROOT}/App/cfg/cfg_file.c
sim/cfg_file_store_sim.c
)
+1 -1
View File
@@ -282,4 +282,4 @@ UI 改动的自动化门禁脚本(完整流程见 `docs/UI设计/UI_自动化
> **模拟器跑过 ≠ 真机跑过**:本目录与真机的**全部已知差异**(网络 pending 机制缺失、代次号恒 0、
> 日志/配置的 mtime 粒度与堆后端差异、IWDG/NAND/RTC 等硬件不可建模)统一登记在
> [`../docs/测试说明/模拟器差距清单.md`](../docs/测试说明/模拟器差距清单.md)——
> 改动触及 `sim/` 任何后端时,**必须同步在那里登记一行**;改动前先看该文档 §3 判断是否必须回真机。
> 改动触及 `sim/` 任何后端时,**必须同步在那里登记一行**;改动前先看 `docs/测试说明/模拟器差距清单.md` §3 判断是否必须回真机。
+373
View File
@@ -0,0 +1,373 @@
/**
* @file ch395f_reuse_bench.c
* @brief N-30 芯片级 Socket 复用时序台架(裸机,内核前运行,OS/net/FTP 全剥离)
* @details 直接调用 ch395f_* 命令,循环执行受控时序矩阵(延迟/关闭次序/载荷/Socket 号),
* 每步打印芯片状态与中断寄存器,定位"关→开→监听→连接"复用时序中
* 芯片行为的确切规律。配套主机脚本 test/host/ch395f_reuse_host.py。
* 定案结论与实验矩阵演进 → CH395F_Trap_Records.md Trap 25。
*
* 存档状态:本文件在主分支**不注册编译**(未加入 uvprojx),仅作实验工具存档;
* 运行方法:在实验分支(如 n30-chip-bench)把本 .c 注册进 uvprojx Test 组、
* Src/main.c 的 USER CODE 2 区在 app_main_init() 之前调 ch395f_reuse_bench()
* (内核前裸机循环,永不返回,断电退出),烧录后跑主机脚本对照芯片日志。
* 当前矩阵为定案轮(对照 vs "丢 CONNECT 捕获自 accept"修复组),见 s_vars[]。
*
* close_mode:0=主机先关(芯片等 DISCONNECT);1=芯片先关;2=无数据连上即关
* verify:0=无修复;1=listen 后确认 LISTEN 重试;2=缓冲重分配;
* 3=幽灵耐心等;4=等 CONNECT 期轮询状态+自 accept(定案修复)
*/
#include "ch395f_reuse_bench.h"
#if N30_CHIP_BENCH
#include "ch395f.h"
#include "dbg_log.h"
#include "stm32f4xx_hal.h"
/* 变体表:{sock, close 后复开延迟 ms, TX 字节数, close_mode, verify_listen} */
typedef struct {
uint8_t sock;
uint16_t delay_ms;
uint16_t tx_len;
uint8_t close_mode;
uint8_t verify; /* 1 = listen 后确认 TCP==0x01,否则 close 重试(修复假设) */
} bench_var_t;
static const bench_var_t s_vars[] = {
{ 3, 0, 100, 0, 0 }, /* 0ms 对照 */
{ 3, 300, 100, 0, 0 }, /* 300ms 对照 */
{ 3, 800, 2000, 0, 0 }, /* 2KB 对照 */
{ 3, 0, 100, 0, 4 }, /* 0ms + 丢CONNECT捕获自accept */
{ 3, 300, 100, 0, 4 }, /* 300ms + 丢CONNECT捕获 */
{ 3, 800, 2000, 0, 4 }, /* 2KB + 丢CONNECT捕获 */
};
#define BENCH_N_VARS (sizeof(s_vars) / sizeof(s_vars[0]))
#define BENCH_PORT(i) (5000U + (uint16_t)(i))
/* TX 图案缓冲(静态,SRAM;2KB = 最大变体载荷) */
static uint8_t s_tx_pat[2048];
/* RX 排空缓冲 */
static uint8_t s_rx_buf[2048];
/*
* 函数功能:轮询本 Socket 中断(读全局→读本 Socket,读即清)
* 入口参数:sock - Socket 号 uint8_t
* timeout - 超时 ms uint32_t
* out_bits - 输出最后一次读到的 SINT 位 uint8_t *
* 返回值:1=拿到中断 0=超时 int
*/
static int bench_wait_sock_int(uint8_t sock, uint32_t timeout, uint8_t *out_bits) {
uint32_t t0 = HAL_GetTick();
while ((uint32_t)(HAL_GetTick() - t0) < timeout) {
uint16_t glob = ch395f_get_glob_int_status_all();
if ((glob & (1U << (sock + 4U))) != 0U) {
uint8_t si = ch395f_get_sock_int_status(sock);
if (out_bits != 0) {
*out_bits = si;
}
return 1;
}
HAL_Delay(10);
}
return 0;
}
/* 打印芯片两字节状态(Socket 态 / TCP 态) */
static void bench_log_state(const char *tag, uint8_t sock) {
uint8_t st[2] = { 0, 0 };
ch395f_get_socket_status(sock, st);
DBG_INFO("BENCH %s sock%u st=0x%02X/0x%02X t=%u", tag, (unsigned)sock,
st[0], st[1], (unsigned)HAL_GetTick());
}
/* 芯片级初始化(复刻 net_init 序列,剥离全部内核 API) */
static int bench_chip_init(void) {
if (ch395f_check_exist() != CH395F_STATUS_OK) {
DBG_ERROR("BENCH chip not found");
return -1;
}
ch395f_reset();
/* 等 PHY link(Trap 09:link up 前 open 必失败) */
for (int i = 0; i < 100; i++) {
if (ch395f_get_phy_status() != 0U) {
break;
}
HAL_Delay(100);
}
ch395f_set_fun_para(0x09);
ch395f_set_tcp_mss(CH395F_TCP_MSS);
ch395f_set_retrans_count(3);
ch395f_set_retrans_period(200);
ch395f_set_keepalive_idle(60000);
ch395f_set_keepalive_intvl(5000);
ch395f_set_keepalive_cnt(3);
/* 静态 IP(与生产一致) */
{
uint8_t ip[4] = { 192, 168, 1, 100 };
uint8_t gw[4] = { 192, 168, 1, 1 };
uint8_t msk[4] = { 255, 255, 255, 0 };
ch395f_set_ip_addr(ip);
ch395f_set_gwip_addr(gw);
ch395f_set_mask_addr(msk);
}
if (ch395f_init() != CH395F_STATUS_OK) {
DBG_ERROR("BENCH ch395f_init failed");
return -1;
}
/* 8 Socket 缓冲布局(与生产一致) */
for (int i = 7; i >= 0; i--) {
uint8_t recv_start = (uint8_t)(i * 6);
ch395f_set_send_buf((uint8_t)i, (uint8_t)(recv_start + 4), 2);
ch395f_set_recv_buf((uint8_t)i, recv_start, 4);
}
ch395f_ping_enable(1);
return 0;
}
/* 单轮实验 */
static void bench_round(unsigned idx) {
const bench_var_t *v = &s_vars[idx];
uint8_t sock = v->sock;
uint16_t port = BENCH_PORT(idx);
uint8_t si = 0;
/* 幽灵等待期已见 CONNECT 的标志(跳过后续 CONNECT 等待) */
int pre_connected = 0;
DBG_INFO("BENCH === r%u sock%u port%u delay%u tx%u mode%u t=%u",
idx, (unsigned)sock, (unsigned)port, (unsigned)v->delay_ms,
(unsigned)v->tx_len, (unsigned)v->close_mode, (unsigned)HAL_GetTick());
/* 1. open + listen(Trap 09:close 后需延时重开,失败重试 ×3) */
ch395f_set_proto_type(sock, CH395F_PROTO_TYPE_TCP);
ch395f_set_sour_port(sock, port);
uint8_t ost = CH395F_ERR_SUCCESS;
for (int open_try = 0; open_try < 3; open_try++) {
ost = ch395f_open_socket(sock);
if (ost == CH395F_ERR_SUCCESS) {
break;
}
DBG_INFO("BENCH open retry%d err=0x%02X t=%u", open_try, ost, (unsigned)HAL_GetTick());
ch395f_close_socket(sock);
HAL_Delay(500);
}
if (ost != CH395F_ERR_SUCCESS) {
DBG_ERROR("BENCH open fail 0x%02X", ost);
return;
}
uint8_t lst = ch395f_tcp_listen(sock);
if (lst != CH395F_ERR_SUCCESS) {
DBG_ERROR("BENCH listen fail 0x%02X", lst);
ch395f_close_socket(sock);
return;
}
/* 缓冲重分配变体:listen 后重设收发缓冲(隔离"缓冲管理残留"假设) */
if (v->verify == 2U) {
uint8_t recv_start = (uint8_t)(sock * 6);
ch395f_clear_recv_buf(sock);
ch395f_set_send_buf(sock, (uint8_t)(recv_start + 4), 2);
ch395f_set_recv_buf(sock, recv_start, 4);
DBG_INFO("BENCH realloc done sock%u t=%u", (unsigned)sock, (unsigned)HAL_GetTick());
}
/* 清残留中断(与生产 listen 后一致) */
for (int j = 0; j < 8; j++) {
(void)ch395f_get_sock_int_status((uint8_t)j);
}
(void)ch395f_get_glob_int_status_all();
bench_log_state("post-listen", sock);
/* 修复假设验证:listen 后 TCP 态必须真正 == 0x01(LISTEN);
* mode1:close 快速重试;mode3:耐心等幽灵消退(5s,每 100ms 复查),
* 期间幽灵→LISTEN 转变会补发 CONNECT 中断则直接放行 */
if (v->verify == 1U || v->verify == 3U) {
uint32_t budget = (v->verify == 3U) ? 5000U : 1000U;
int att_max = (v->verify == 3U) ? 2 : 4;
for (int att = 0; att < att_max; att++) {
uint8_t st[2] = { 0, 0 };
uint32_t tw = HAL_GetTick();
int connect_fired = 0;
/* 等 TCP 进 LISTEN(幽灵消退)或 CONNECT 中断补发 */
while ((uint32_t)(HAL_GetTick() - tw) < budget) {
uint16_t glob = ch395f_get_glob_int_status_all();
if ((glob & (1U << (sock + 4U))) != 0U) {
uint8_t si = ch395f_get_sock_int_status(sock);
if ((si & CH395F_SINT_STAT_CONNECT) != 0U) {
connect_fired = 1;
DBG_INFO("BENCH verify CONNECT during ghost-wait att%d t=%u",
att, (unsigned)HAL_GetTick());
break;
}
}
ch395f_get_socket_status(sock, st);
if (st[1] == 0x01U) {
break;
}
HAL_Delay(100);
}
DBG_INFO("BENCH verify att%d st=0x%02X/0x%02X cf=%d t=%u",
att, st[0], st[1], connect_fired, (unsigned)HAL_GetTick());
if (connect_fired) {
/* 幽灵期主机已连上:跳过 CONNECT 等待直接进流程 */
pre_connected = 1;
break;
}
if (st[1] == 0x01U) {
break;
}
/* 幽灵未消:close → 50ms → 重新 open+listen(mode3 只试一次) */
ch395f_close_socket(sock);
HAL_Delay(50);
ch395f_open_socket(sock);
ch395f_tcp_listen(sock);
for (int j = 0; j < 8; j++) {
(void)ch395f_get_sock_int_status((uint8_t)j);
}
(void)ch395f_get_glob_int_status_all();
}
}
/* 2. 等 CONNECT(10s;幽灵等待期已见 CONNECT 则跳过)
* v6 关键探针:等待期每 500ms 读芯片状态;若已 ESTABLISHED(0x04)
* 说明握手完成但 CONNECT 中断丢失 → 补做 accept(对账②模拟) */
int got = pre_connected;
uint32_t t0 = HAL_GetTick();
while (!got && (uint32_t)(HAL_GetTick() - t0) < 10000U) {
if (bench_wait_sock_int(sock, 100, &si)) {
if ((si & CH395F_SINT_STAT_CONNECT) != 0U) {
got = 1;
break;
}
DBG_INFO("BENCH unexpected int=0x%02X while wait", si);
}
if (v->verify == 4U) {
uint8_t st[2] = { 0, 0 };
ch395f_get_socket_status(sock, st);
if ((uint32_t)(HAL_GetTick() - t0) % 1000U < 110U) {
DBG_INFO("BENCH wait-state st=0x%02X/0x%02X t=%u",
st[0], st[1], (unsigned)HAL_GetTick());
}
if (st[1] == 0x04U) {
DBG_INFO("BENCH LOST-CONNECT caught: state ESTABLISHED, self-accept t=%u",
(unsigned)HAL_GetTick());
got = 1;
break;
}
}
}
if (!got) {
DBG_ERROR("BENCH no CONNECT within 10s (host not synced?)");
ch395f_close_socket(sock);
HAL_Delay(v->delay_ms);
return;
}
bench_log_state("post-accept", sock);
/* 对端信息 + RX 残渣检查(旧连接残留的直接证据) */
{
uint8_t rem[6] = { 0 };
ch395f_get_remot_ipp(sock, rem);
uint16_t rl = ch395f_get_recv_len(sock);
DBG_INFO("BENCH remote port=%u recv_len=%u",
(unsigned)(rem[4] | (rem[5] << 8)), (unsigned)rl);
if (rl > 0U) {
uint16_t n = (rl > 2048U) ? 2048U : rl;
ch395f_read_recv_buf(sock, s_rx_buf, n);
DBG_INFO("BENCH drained %u residue bytes", (unsigned)n);
}
}
/* 3. TX 探针(1KB 分块,Trap 20) */
if (v->tx_len > 0U) {
uint16_t sent = 0;
while (sent < v->tx_len) {
uint16_t chunk = (uint16_t)(v->tx_len - sent);
if (chunk > 1024U) {
chunk = 1024U;
}
ch395f_write_send_buf(sock, &s_tx_pat[sent], chunk);
sent += chunk;
/* 等 SENBUF_FREE(500ms) */
if (sent < v->tx_len) {
uint32_t tw = HAL_GetTick();
while ((uint32_t)(HAL_GetTick() - tw) < 500U) {
if (bench_wait_sock_int(sock, 50, &si)) {
if ((si & CH395F_SINT_STAT_SENBUF_FREE) != 0U) {
break;
}
}
}
}
}
DBG_INFO("BENCH tx %u written t=%u", (unsigned)v->tx_len, (unsigned)HAL_GetTick());
/* 连接存活观察窗:500ms 内每 50ms 记状态+中断(抓"秒杀") */
for (int k = 0; k < 10; k++) {
uint16_t glob = ch395f_get_glob_int_status_all();
if ((glob & (1U << (sock + 4U))) != 0U) {
si = ch395f_get_sock_int_status(sock);
DBG_INFO("BENCH int during window=0x%02X t=%u", si, (unsigned)HAL_GetTick());
}
HAL_Delay(50);
bench_log_state("window", sock);
}
}
/* 4. 关闭次序 */
if (v->close_mode == 1U) {
/* 芯片先关 */
HAL_Delay(300);
ch395f_close_socket(sock);
DBG_INFO("BENCH chip closed first t=%u", (unsigned)HAL_GetTick());
} else {
/* 等主机关(DISCONNECT 事件,5s) */
uint32_t tw = HAL_GetTick();
int disc = 0;
while ((uint32_t)(HAL_GetTick() - tw) < 5000U) {
if (bench_wait_sock_int(sock, 100, &si)) {
if ((si & (CH395F_SINT_STAT_DISCONNECT | CH395F_SINT_STAT_SOCK_TIMEOUT)) != 0U) {
disc = 1;
break;
}
}
}
DBG_INFO("BENCH host-close seen=%d t=%u", disc, (unsigned)HAL_GetTick());
ch395f_close_socket(sock);
}
/* 5. 关闭后拆链观察:1s 内每 50ms 记状态 + 中断(Trap 14 滞后中断取证) */
for (int k = 0; k < 20; k++) {
uint16_t glob = ch395f_get_glob_int_status_all();
uint8_t late = 0;
if ((glob & (1U << (sock + 4U))) != 0U) {
late = ch395f_get_sock_int_status(sock);
}
if (late != 0U) {
DBG_INFO("BENCH late-int=0x%02X t=%u", late, (unsigned)HAL_GetTick());
}
if ((k % 4) == 0) {
bench_log_state("post-close", sock);
}
HAL_Delay(50);
}
/* 6. 受控延迟后进入下一轮 */
HAL_Delay(v->delay_ms);
}
void ch395f_reuse_bench(void) {
DBG_INFO("BENCH ===== N30 chip-level reuse bench START =====");
if (bench_chip_init() != 0) {
DBG_ERROR("BENCH init failed, giving up (normal boot continues)");
return;
}
DBG_INFO("BENCH chip ready, cycling %u variants forever", (unsigned)BENCH_N_VARS);
for (unsigned i = 0; i < 2048U; i++) {
s_tx_pat[i] = (uint8_t)(0xA5U + (i % 251U));
}
unsigned idx = 0;
for (;;) {
bench_round(idx);
idx = (idx + 1U) % (unsigned)BENCH_N_VARS;
}
}
#endif /* N30_CHIP_BENCH */
+13
View File
@@ -0,0 +1,13 @@
/*
* ch395f_reuse_bench.h - N-30 芯片级 Socket 复用时序台架(裸机实验,临时分支专用)
*/
#ifndef CH395F_REUSE_BENCH_H
#define CH395F_REUSE_BENCH_H
/* 实验总开关:1 = 内核前进入裸机台架(OS 永不启动,断电退出) */
#define N30_CHIP_BENCH 1
void ch395f_reuse_bench(void);
#endif /* CH395F_REUSE_BENCH_H */
+92
View File
@@ -0,0 +1,92 @@
# N-30 芯片级复用时序台架:主机端(与 test/board/ch395f_reuse_bench.c 共用变体表)
# 用法: python ch395f_reuse_host.py [--host IP] [--cycles N]
# 纯 TCP,无 FTP。按 idx 轮转连接 5000+idx 端口,记录收发结果与时刻。
import argparse
import socket
import sys
import time
# {idx: (tx_len, close_mode)} 与固件表一致
VARS = {
0: (100, 0), # 0ms control
1: (100, 0), # 300ms control
2: (2000, 0), # 2KB control
3: (100, 0), # 0ms + ghost-wait fix
4: (100, 0), # 300ms + ghost-wait fix
5: (2000, 0), # 2KB + ghost-wait fix
}
def ts():
return time.strftime("%H:%M:%S") + ".%03d" % (int(time.time() * 1000) % 1000)
def run_round(host, idx, tx_len, close_mode):
port = 5000 + idx
# 等端口监听(芯片还没 open 时连接被拒,重试)
s = None
t0 = time.time()
while time.time() - t0 < 12.0:
try:
s = socket.create_connection((host, port), timeout=5)
break
except OSError:
time.sleep(0.2)
if s is None:
print("%s r%u CONNECT timeout" % (ts(), idx))
return False
ok = True
try:
s.settimeout(5)
got = 0
while got < tx_len:
chunk = s.recv(min(1024, tx_len - got))
if not chunk:
break
got += len(chunk)
note = ""
if got != tx_len:
note = " <<<< SWALLOWED (want %d)" % tx_len
ok = False
if close_mode == 1:
# 等芯片 FIN(mode1 芯片先关)
try:
fin = s.recv(1)
if fin:
note += " unexpected data after tx: %r" % fin
except socket.timeout:
note += " <<<< no FIN from chip (mode1)"
ok = False
print("%s r%u connected got=%d/%d%s" % (ts(), idx, got, tx_len, note))
except socket.timeout:
print("%s r%u recv timeout <<<< STALL" % (ts(), idx))
ok = False
except OSError as e:
print("%s r%u sock error %r" % (ts(), idx, e))
ok = False
finally:
s.close() # 主机关闭(mode0/2 主机先关)
return ok
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--host", default="192.168.1.100")
ap.add_argument("--cycles", type=int, default=3)
args = ap.parse_args()
fails = []
for c in range(args.cycles):
for idx in sorted(VARS):
tx_len, mode = VARS[idx]
print("%s === cycle%u r%u (tx=%d mode=%d) ===" % (ts(), c, idx, tx_len, mode))
if not run_round(args.host, idx, tx_len, mode):
fails.append((c, idx))
time.sleep(0.3)
print("RESULT: %d failures %s" % (len(fails), fails))
return 1 if fails else 0
if __name__ == "__main__":
sys.exit(main())
+203
View File
@@ -0,0 +1,203 @@
/*
* 模块名称:FTP 鉴权状态机(lftpd_auth)主机侧单元测试
* 模块功能:在 PC 上对 Drivers/BSP/NET/lftpd/lftpd_auth.c 做离线验证
* (用户名匹配 / 口令校验 / 失败计数 / 限时锁定 / tick 回绕)。
*
* 被测对象:真实源文件 lftpd_auth.c + 其依赖 sha256.c(均以 #include 原样
* 纳入,保证测的是生产代码而非副本;两模块零平台依赖,无需桩——
* tick 由参数注入正是本模块的纯函数设计意图)。
*
* 为什么要主机侧单测(而不是只用台架):
* 锁定 60 s / 解锁 / 成功清零 / tick 32 位回绕是"防口令爆破"契约的核心,
* 板端每验一条要真等 60 s 且回绕要跑满 49.7 天;主机侧注入合成 tick
* 毫秒级穷举。用户名侧信道路径(错用户名也走哈希)同属纯逻辑。
*
* 覆盖点:
* TC-FA-001 user_match:相等 / 大小写不敏感 / 不等 / NULL
* TC-FA-002 正确用户名+正确口令 → OK,计数 0
* TC-FA-003 错口令 → FAIL,计数 1
* TC-FA-004 错用户名+对口令 → FAIL(哈希路径已走,时间拉平)
* TC-FA-005 连续 5 次失败 → 第 6 次(锁定期内)LOCKED,对口令也拒
* TC-FA-006 锁定期满(+60000 tick)→ 对口令 OK,计数清零
* TC-FA-007 成功清零计数:3 败 + 1 成 → 再 4 败不触发锁定(第 5 败才锁)
* TC-FA-008 tick 32 位回绕:锁定期内回绕点仍 LOCKED,期满回绕点放行
* TC-FA-009 配置 pwd_hash 非法 hex → 恒 FAIL(fail-closed)
* TC-FA-010 空口令 / NULL 口令 → FAIL;NULL creds/state → FAIL
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-11
*/
#include <stdio.h>
#include <string.h>
/* 生产源文件原样纳入(顺序敏感:先 sha256 后 lftpd_auth) */
#include "sha256.c"
#include "lftpd_auth.c"
/* ===================== 断言框架 ===================== */
static int s_total;
static int s_passed;
static int s_failed;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" PASS %s\n", name); } \
else { s_failed++; printf(" FAIL %s (line %d)\n", name, __LINE__); } \
} while (0)
/* 与 ftp_cfg.h FTP_CFG_DEF_HASH 同源钉值(SHA256("adminadmin")) */
#define DEF_HASH "d82494f05d6917ba02f7aaa29689ccb444bb73f20380876cb05d1f37537b7892"
static const ftp_auth_creds_t s_creds = { "admin", DEF_HASH, 1u };
/* ===================== TC-FA-001:user_match ===================== */
static void tc_user_match(void)
{
CHECK(ftp_auth_user_match("admin", "admin") == 1, "TC-FA-001a 相等匹配");
CHECK(ftp_auth_user_match("admin", "ADMIN") == 1, "TC-FA-001b 大小写不敏感");
CHECK(ftp_auth_user_match("admin", "root") == 0, "TC-FA-001c 不等拒绝");
CHECK(ftp_auth_user_match("admin", "admin2") == 0, "TC-FA-001d 前缀不算匹配");
CHECK(ftp_auth_user_match("admin", NULL) == 0, "TC-FA-001e NULL 拒绝");
}
/* ===================== TC-FA-002..004:基本判定 ===================== */
static void tc_basic(void)
{
ftp_auth_state_t st;
(void)memset(&st, 0, sizeof(st));
CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, 1000u) == FTP_AUTH_OK,
"TC-FA-002 对用户名+对口令 → OK");
CHECK(st.fail_count == 0u, "TC-FA-002b 成功后计数为 0");
(void)memset(&st, 0, sizeof(st));
CHECK(ftp_auth_check(&s_creds, 1u, "wrong", &st, 1000u) == FTP_AUTH_FAIL,
"TC-FA-003 错口令 → FAIL");
CHECK(st.fail_count == 1u, "TC-FA-003b 失败计数 1");
(void)memset(&st, 0, sizeof(st));
CHECK(ftp_auth_check(&s_creds, 0u, "admin", &st, 1000u) == FTP_AUTH_FAIL,
"TC-FA-004 错用户名+对口令 → FAIL(哈希路径已走)");
}
/* ===================== TC-FA-005..006:锁定与解锁 ===================== */
static void tc_lock(void)
{
ftp_auth_state_t st;
uint32_t t = 10000u;
(void)memset(&st, 0, sizeof(st));
for (int i = 0; i < 5; i++) {
CHECK(ftp_auth_check(&s_creds, 1u, "wrong", &st, t) == FTP_AUTH_FAIL,
"TC-FA-005 第 1..5 次失败 → FAIL");
}
CHECK(st.fail_count == 5u, "TC-FA-005b 计数到达 FAIL_MAX");
/* 锁定期内:对口令也 LOCKED,且不再计数 */
CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, t + 1u) == FTP_AUTH_LOCKED,
"TC-FA-005c 锁定期内对口令 → LOCKED");
CHECK(st.fail_count == 5u, "TC-FA-005d LOCKED 不改变计数");
/* 59999 tick:仍在窗内(差值 < 60000) */
CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st,
st.lock_until_tick - 1u) == FTP_AUTH_LOCKED,
"TC-FA-005e 窗内最后 1 tick 仍 LOCKED");
/* 期满:恰好到达 lock_until → 放行 */
CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st,
st.lock_until_tick) == FTP_AUTH_OK,
"TC-FA-006 期满对口令 → OK");
CHECK(st.fail_count == 0u, "TC-FA-006b 解锁成功后计数清零");
}
/* ===================== TC-FA-007:成功清零计数 ===================== */
static void tc_reset(void)
{
ftp_auth_state_t st;
uint32_t t = 5000u;
(void)memset(&st, 0, sizeof(st));
for (int i = 0; i < 3; i++) {
(void)ftp_auth_check(&s_creds, 1u, "wrong", &st, t);
}
CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, t) == FTP_AUTH_OK,
"TC-FA-007a 3 败后成功 → OK");
CHECK(st.fail_count == 0u, "TC-FA-007b 成功清零");
for (int i = 0; i < 4; i++) {
CHECK(ftp_auth_check(&s_creds, 1u, "wrong", &st, t) == FTP_AUTH_FAIL,
"TC-FA-007c 清零后再 4 败仍 FAIL(未锁)");
}
CHECK(ftp_auth_check(&s_creds, 1u, "wrong", &st, t) == FTP_AUTH_FAIL,
"TC-FA-007d 第 5 败 FAIL 并上锁");
CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, t + 1u) == FTP_AUTH_LOCKED,
"TC-FA-007e 连续 5 败后 LOCKED");
}
/* ===================== TC-FA-008:tick 32 位回绕 ===================== */
static void tc_wraparound(void)
{
ftp_auth_state_t st;
/* 手工构造:锁定截止在 0xFFFFFFF0(距 32 位翻转仅 16 tick) */
(void)memset(&st, 0, sizeof(st));
st.fail_count = 5u;
st.lock_until_tick = 0xFFFFFFF0u;
/* 回绕前 240 tick:仍在窗内 */
CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, 0xFFFFFF00u) == FTP_AUTH_LOCKED,
"TC-FA-008a 回绕前窗内 → LOCKED");
/* 回绕后 32 tick:now=0x10,(int32_t)(0x10-0xFFFFFFF0)=+32 → 期满 */
CHECK(ftp_auth_check(&s_creds, 1u, "admin", &st, 0x00000010u) == FTP_AUTH_OK,
"TC-FA-008b 回绕后期满 → OK(有符号差抗回绕)");
}
/* ===================== TC-FA-009..010:fail-closed 边界 ===================== */
static void tc_fail_closed(void)
{
ftp_auth_state_t st;
ftp_auth_creds_t bad_creds = { "admin", "zz2494f05d6917ba02f7aaa29689ccb444bb73f20380876cb05d1f37537b7892", 1u };
(void)memset(&st, 0, sizeof(st));
CHECK(ftp_auth_check(&bad_creds, 1u, "admin", &st, 1000u) == FTP_AUTH_FAIL,
"TC-FA-009 非法 hex 配置 → 恒 FAIL(无人能登录,fail-closed)");
(void)memset(&st, 0, sizeof(st));
CHECK(ftp_auth_check(&s_creds, 1u, "", &st, 1000u) == FTP_AUTH_FAIL,
"TC-FA-010a 空口令 → FAIL");
CHECK(ftp_auth_check(&s_creds, 1u, NULL, &st, 1000u) == FTP_AUTH_FAIL,
"TC-FA-010b NULL 口令 → FAIL");
CHECK(ftp_auth_check(NULL, 1u, "admin", &st, 1000u) == FTP_AUTH_FAIL,
"TC-FA-010c NULL creds → FAIL");
CHECK(ftp_auth_check(&s_creds, 1u, "admin", NULL, 1000u) == FTP_AUTH_FAIL,
"TC-FA-010d NULL state → FAIL");
}
/* ===================== main ===================== */
int main(void)
{
printf("=== FTP Auth Tests ===\n");
printf("-- user match --\n");
tc_user_match();
printf("-- basic check --\n");
tc_basic();
printf("-- lock / unlock --\n");
tc_lock();
printf("-- success reset --\n");
tc_reset();
printf("-- tick wraparound --\n");
tc_wraparound();
printf("-- fail-closed --\n");
tc_fail_closed();
printf("\n=== FTP Auth Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}
+50
View File
@@ -0,0 +1,50 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""FTP 鉴权状态机(lftpd_auth)的主机侧单元测试运行器(L0.5,纯标准库)。
做什么:
用本机 MSVC 编译 test/host/ftp_auth/ftp_auth_test.c(它以 #include 纳入
**生产源文件** Drivers/BSP/NET/lftpd/lftpd_auth.c 与 App/util/sha256.c),
在 PC 上离线跑口令校验/失败计数/限时锁定/tick 回绕,失败即非 0 退出。
零硬件、零烧录、毫秒级。
工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness
`test/host/common/unit_test_harness.py`(本文件只做声明)。
为什么要主机侧单测(而不是只用台架):
锁定 60s / 解锁 / 成功清零 / tick 32 位回绕是"防口令爆破"契约核心,
板端每验一条要真等 60s、回绕要跑满 49.7 天;lftpd_auth 的纯函数设计
(tick 参数注入)正是为了让这些分支在主机侧毫秒级穷举。
两被测模块零平台依赖,无需任何桩。
用法(仓库根目录):
python test/host/ftp_auth/run_unit_test.py
或统一用 .venv: & '.venv\\Scripts\\python.exe' test/host/ftp_auth/run_unit_test.py
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
设计依据:Drivers/BSP/NET/lftpd/lftpd_auth.h;创建日期:2026-10-11
"""
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
# harness 位于 test/host/common/
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
BUILD = TestBuild(
name="FTP Auth Tests",
src=os.path.join(HERE, "ftp_auth_test.c"),
include_dirs=[
os.path.join(ROOT, "Drivers", "BSP", "NET", "lftpd"),
os.path.join(ROOT, "App", "util"),
HERE,
],
summary_marker="=== FTP Auth Tests:",
)
if __name__ == "__main__":
sys.exit(run_host_unit_test(BUILD))
+173
View File
@@ -0,0 +1,173 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""FTP 鉴权台架回归(N-19,需真机在线)
验证固件侧 lftpd 鉴权集成的完整行为矩阵(主机单测 test/host/ftp_auth 覆盖
不到的"协议接线"部分:命令门、530/331/230/550 响应码、跨会话锁定):
T1 匿名登录(anonymous) → 期望 530 拒
T2 未登录直接 NLST → 期望 530(命令门)
T3 对用户名 + 错口令 ×5 → 期望 530 ×5(并触发锁定)
T4 锁定窗内正确口令 → 期望 530(LOCKED,不算哈希)
T5 等待锁定期满后正确口令 → 期望 230(60s + 余量)
T6 登录后 NLST / STOR / RETR / DELE → 期望全通(出厂 write=1)
T7 会话结束重连、错口令一次 → 期望 530 且计数已清零(T5 成功清零)
注意:CH395F 单连接——每步用完必须 quit() 释放 socket,否则下一步连不上;
跑之前确保 UI 远程显示(7003)/对时(8888)客户端未占用 socket。
用法(设备默认 192.168.1.100,出厂口令 admin/admin):
& '.venv\\Scripts\\python.exe' test/host/ftp_auth_bench.py
改过口令: ... --pwd <新口令>
跳过 60s 锁定等待(只验锁定触发不验解锁): ... --skip-unlock
退出码:0=全部通过 / 1=任一失败 / 2=环境错误(连不上设备)
创建日期:2026-10-11
"""
import argparse
import io
import socket
import sys
import time
from ftplib import FTP, error_perm
LOCK_WAIT_S = 62 # 固件锁定 60 s + 余量
def _try_login(host, port, timeout, user, pwd):
"""尝试一次登录;返回 (ok, code_or_exc)。失败不断言连接状态。
单连接 socket:上一次会话结束后 net 层 auto_relisten ~50 ms,但 accept
失败会走"毁监听→重建"路径(3 s 重试窗)→ connect 被拒时重试 3 次。
"""
f = FTP()
last_err = None
for attempt in range(3):
try:
time.sleep(0.5)
f.connect(host, port, timeout=timeout)
break
except (ConnectionRefusedError, socket.timeout, OSError) as e:
last_err = e
f = FTP()
else:
return None, repr(last_err), None
try:
try:
f.login(user, pwd)
return True, "230", f
except error_perm as e:
f.close()
return False, str(e), None
except (socket.timeout, OSError) as e:
return None, repr(e), None
def main(argv=None):
ap = argparse.ArgumentParser(description="FTP auth bench matrix (N-19)")
ap.add_argument("--host", default="192.168.1.100")
ap.add_argument("--port", type=int, default=21)
ap.add_argument("--timeout", type=int, default=15)
ap.add_argument("--user", default="admin")
ap.add_argument("--pwd", default="admin")
ap.add_argument("--skip-unlock", action="store_true",
help="不验 60s 解锁(省 1 分钟;锁定触发仍验)")
args = ap.parse_args(argv)
fails = []
def expect(name, cond, detail=""):
tag = "PASS" if cond else "FAIL"
print(" [%s] %s %s" % (tag, name, detail))
if not cond:
fails.append(name)
# ---- T1 匿名登录被拒 ----
ok, code, conn = _try_login(args.host, args.port, args.timeout, "anonymous", "x@x")
if ok is None:
print("[env] 连不上 %s:%d → %s" % (args.host, args.port, code))
return 2
expect("T1 匿名登录 → 530", (ok is False) and code.startswith("530"), code)
if conn:
conn.quit()
# ---- T2 未登录直接 NLST → 530(命令门)----
time.sleep(0.5) # 单连接 socket:等 auto_relisten 沉降(~50 ms + 余量)
f = FTP()
f.connect(args.host, args.port, timeout=args.timeout)
try:
f.sendcmd("NLST")
expect("T2 未登录 NLST → 530", False, "居然放行")
except error_perm as e:
expect("T2 未登录 NLST → 530", str(e).startswith("530"), str(e))
except Exception as e: # noqa: BLE001
expect("T2 未登录 NLST → 530", False, repr(e))
# ⚠ 必须 close:单连接模型下不关 = 占住 socket,后续步骤全被拒
# (2026-10-11 台架实测:QUIT 后不 close → 芯片视角旧连接仍 ESTABLISHED)
try:
f.quit()
except Exception: # noqa: BLE001
try:
f.close()
except Exception: # noqa: BLE001
pass
time.sleep(0.5)
# ---- T3 错口令 ×5 → 530 ×5 ----
for i in range(1, 6):
ok, code, conn = _try_login(args.host, args.port, args.timeout,
args.user, "wrong-pass-%d" % i)
expect("T3.%d 错口令 → 530" % i, (ok is False) and code.startswith("530"), code)
if conn:
conn.quit()
# ---- T4 锁定窗内正确口令 → 530 ----
ok, code, conn = _try_login(args.host, args.port, args.timeout, args.user, args.pwd)
expect("T4 锁定窗内对口令 → 530", (ok is False) and code.startswith("530"), code)
if conn:
conn.quit()
if args.skip_unlock:
print("[skip] 未验 60s 解锁(--skip-unlock)")
else:
# ---- T5 期满正确口令 → 230 ----
print(" [wait] 等锁定期满 %d s ..." % LOCK_WAIT_S)
time.sleep(LOCK_WAIT_S)
ok, code, conn = _try_login(args.host, args.port, args.timeout,
args.user, args.pwd)
expect("T5 期满对口令 → 230", ok is True, code)
if conn:
# ---- T6 登录后读写全通(出厂 write=1)----
try:
names = conn.nlst()
expect("T6a NLST", len(names) >= 0, "%d 项" % len(names))
payload = b"N19-auth-bench-" + b"X" * 500
conn.storbinary("STOR n19t.tmp", io.BytesIO(payload))
got = io.BytesIO()
conn.retrbinary("RETR n19t.tmp", got.write)
expect("T6b STOR/RETR 往返一致", got.getvalue() == payload)
conn.delete("n19t.tmp")
expect("T6c DELE", True)
except Exception as e: # noqa: BLE001
expect("T6 登录后读写", False, repr(e))
# ---- T7 成功已清零计数:本会话退出后错一次只算 1(不锁)----
conn.quit()
ok, code, conn = _try_login(args.host, args.port, args.timeout,
args.user, "one-wrong")
expect("T7 成功后错 1 次 → 530 且未锁(下次对口令可登)",
(ok is False) and code.startswith("530"), code)
if conn:
conn.quit()
ok, code, conn = _try_login(args.host, args.port, args.timeout,
args.user, args.pwd)
expect("T7b 紧接对口令 → 230", ok is True, code)
if conn:
conn.quit()
print("\nVERDICT: %s (%d fail)" % ("PASS" if not fails else "FAIL", len(fails)))
return 0 if not fails else 1
if __name__ == "__main__":
sys.exit(main())
+5 -1
View File
@@ -30,6 +30,10 @@ def main(argv=None):
ap.add_argument("--host", default="192.168.1.100", help="device IP")
ap.add_argument("--port", type=int, default=21, help="FTP control port")
ap.add_argument("--timeout", type=int, default=20, help="socket timeout (s)")
# N-19(2026-10-11):FTP 已强制鉴权,匿名登录被 530 拒;默认口令 admin/admin
# (出厂值,见 ftp_cfg.h),改过 [ftp] pwd_hash 后用这两个参数覆盖
ap.add_argument("--user", default="admin", help="FTP user ([ftp] user)")
ap.add_argument("--pwd", default="admin", help="FTP password")
args = ap.parse_args(argv)
results = []
@@ -52,7 +56,7 @@ def main(argv=None):
f = FTP()
print("[ftp] connecting %s:%d ..." % (args.host, args.port))
f.connect(args.host, args.port, timeout=args.timeout)
f.login("anonymous", "x@x")
f.login(args.user, args.pwd)
print("[ftp] login ok: %s" % f.getwelcome())
# 同一会话内连续 8 次数据连接(含两次大文件)—— D20 的失败点是第 2 次
+74
View File
@@ -0,0 +1,74 @@
# N-30 确定性复现:连续快速 STOR(无间隔),统计 425/超时失败次数
# 用法: python n30_repro.py [--host IP] [--user U --pwd P] [--n 10] [--size 1024]
# 退出码: 0=全部成功 / 1=有失败 / 2=连接错误
import argparse
import io
import sys
from ftplib import FTP, error_temp, error_perm
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--host", default="192.168.1.100")
ap.add_argument("--port", type=int, default=21)
ap.add_argument("--user", default=None, help="None=anonymous")
ap.add_argument("--pwd", default=None)
ap.add_argument("--n", type=int, default=10)
ap.add_argument("--size", type=int, default=1024)
args = ap.parse_args()
data = b"X" * args.size
fails = []
try:
f = FTP()
f.connect(args.host, args.port, timeout=15)
if args.user is None:
f.login("anonymous", "x@x")
else:
f.login(args.user, args.pwd)
f.voidcmd("TYPE I")
for i in range(args.n):
name = "n30_%02d.tmp" % i
try:
with io.BytesIO(data) as b:
f.storbinary("STOR " + name, b)
print("STOR #%02d OK" % (i + 1))
except (error_temp, error_perm, OSError, EOFError) as e:
print("STOR #%02d FAIL %r" % (i + 1, e))
fails.append(i + 1)
# 会话可能已重启:尝试重登继续计数
try:
f.quit()
except Exception:
pass
try:
f = FTP()
f.connect(args.host, args.port, timeout=15)
if args.user is None:
f.login("anonymous", "x@x")
else:
f.login(args.user, args.pwd)
f.voidcmd("TYPE I")
except Exception as e2:
print("re-login failed: %r" % e2)
break
# 清理
try:
for i in range(args.n):
try:
f.delete("n30_%02d.tmp" % i)
except Exception:
pass
f.quit()
except Exception:
pass
except Exception as e:
print("connect error: %r" % e)
return 2
print("RESULT: %d/%d failed %s" % (len(fails), args.n, fails))
return 1 if fails else 0
if __name__ == "__main__":
sys.exit(main())
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""SHA-256(sha256)的主机侧单元测试运行器(L0.5,纯标准库)。
做什么:
用本机 MSVC 编译 test/host/sha256/sha256_test.c(它以 #include 纳入
**生产源文件** App/util/sha256.c),在 PC 上离线跑 FIPS 180-4 标准向量、
流式等价、hex_eq 边界,失败即非 0 退出。零硬件、零烧录、毫秒级。
工具链探测 / INCLUDE-LIB 拼接 / 编译运行 / 退出码判定全部委托给通用 harness
`test/host/common/unit_test_harness.py`(本文件只做声明)。
为什么要主机侧单测(而不是只用板端):
sha256 是 FTP pwd_hash 校验(N-19)的地基——哈希算错是"正确口令被拒 /
错误口令放行"的静默语义错误,板端只见 530/230 看不出根因;标准向量
(空串/abc/56B 补位跨界/1MB 长度进第二分组)主机侧一次钉死。
本模块零平台依赖,无需任何桩。
用法(仓库根目录):
python test/host/sha256/run_unit_test.py
或统一用 .venv: & '.venv\\Scripts\\python.exe' test/host/sha256/run_unit_test.py
退出码:0=PASS / 1=测试失败 / 2=环境错误(找不到编译器 / 编译失败 / 无汇总行)。
设计依据:App/util/sha256.h;创建日期:2026-10-11
"""
import os
import sys
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(HERE)))
# harness 位于 test/host/common/
sys.path.insert(0, os.path.join(ROOT, "test", "host", "common"))
from unit_test_harness import TestBuild, run_host_unit_test # noqa: E402
BUILD = TestBuild(
name="SHA-256 Tests",
src=os.path.join(HERE, "sha256_test.c"),
include_dirs=[
os.path.join(ROOT, "App", "util"),
HERE,
],
summary_marker="=== SHA-256 Tests:",
)
if __name__ == "__main__":
sys.exit(run_host_unit_test(BUILD))
+192
View File
@@ -0,0 +1,192 @@
/*
* 模块名称:SHA-256 主机侧单元测试
* 模块功能:在 PC 上对 App/util/sha256.c 做离线验证(FIPS 180-4 标准向量 +
* 流式等价 + sha256_hex_eq 边界)。
*
* 被测对象:真实源文件 ../../../App/util/sha256.c(以 #include 原样纳入,
* 保证测的是生产代码而非副本;本模块零平台依赖,无需任何桩)。
*
* 为什么要主机侧单测(而不是只靠台架):
* sha256 是 FTP pwd_hash 校验(N-19)的地基——哈希算错的后果是
* "正确口令被拒 / 错误口令放行"这类静默语义错误,板端只看到 530/230,
* 看不出哪一步坏;标准向量在主机侧毫秒级钉死。
*
* 覆盖点:
* TC-SH-001 FIPS 180-4 向量:空串
* TC-SH-002 FIPS 180-4 向量:"abc"
* TC-SH-003 FIPS 180-4 向量:56 字节(补位跨分组边界)
* TC-SH-004 FIPS 180-4 向量:1,000,000 × 'a'(流式,长度字段进第二分组)
* TC-SH-005 流式任意切分与一次性计算等价(逐字节喂入)
* TC-SH-006 hex_eq:小写匹配 / 大写匹配
* TC-SH-007 hex_eq:内容不等 / 非法字符 / 长度 63/65 / NULL → 全 0
* TC-SH-008 默认口令哈希钉值:SHA256("adminadmin") = FTP_CFG_DEF_HASH 同源值
*
* 通过:全部 TC PASS(main 返回 0)。
* 入口:int main(void),返回 0=PASS / 1=FAIL(由 run 脚本判读)。
*
* 创建日期:2026-10-11
*/
#include <stdio.h>
#include <string.h>
/* 生产源文件原样纳入(零平台依赖,直接编) */
#include "sha256.c"
/* ===================== 断言框架 ===================== */
static int s_total;
static int s_passed;
static int s_failed;
#define CHECK(cond, name) do { \
s_total++; \
if (cond) { s_passed++; printf(" PASS %s\n", name); } \
else { s_failed++; printf(" FAIL %s (line %d)\n", name, __LINE__); } \
} while (0)
/* 摘要 → hex 小写串(仅测试用,允许栈上 sprintf) */
static void digest_to_hex(const uint8_t d[SHA256_DIGEST_SIZE], char out[65])
{
static const char hexd[] = "0123456789abcdef";
for (int i = 0; i < 32; i++) {
out[2 * i] = hexd[(d[i] >> 4) & 0xF];
out[2 * i + 1] = hexd[d[i] & 0xF];
}
out[64] = '\0';
}
static int hex_eq_vector(const uint8_t d[SHA256_DIGEST_SIZE], const char *want)
{
char got[65];
digest_to_hex(d, got);
return strcmp(got, want) == 0;
}
/* ===================== TC-SH-001..004:标准向量 ===================== */
static void tc_vectors(void)
{
uint8_t d[SHA256_DIGEST_SIZE];
sha256_calc("", 0u, d);
CHECK(hex_eq_vector(d, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"),
"TC-SH-001 空串向量");
sha256_calc("abc", 3u, d);
CHECK(hex_eq_vector(d, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"),
"TC-SH-002 \"abc\" 向量");
sha256_calc("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq", 56u, d);
CHECK(hex_eq_vector(d, "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"),
"TC-SH-003 56 字节向量(补位跨分组)");
{
sha256_ctx_t ctx;
sha256_init(&ctx);
for (int i = 0; i < 1000000; i++) {
sha256_update(&ctx, "a", 1u);
}
sha256_final(&ctx, d);
}
CHECK(hex_eq_vector(d, "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0"),
"TC-SH-004 1e6 × 'a' 向量(流式)");
}
/* ===================== TC-SH-005:流式等价 ===================== */
static void tc_stream_equiv(void)
{
static const char msg[] = "the quick brown fox jumps over the lazy dog";
uint8_t d1[SHA256_DIGEST_SIZE];
uint8_t d2[SHA256_DIGEST_SIZE];
sha256_ctx_t ctx;
sha256_calc(msg, strlen(msg), d1);
sha256_init(&ctx);
for (size_t i = 0; i < strlen(msg); i++) {
sha256_update(&ctx, &msg[i], 1u);
}
sha256_final(&ctx, d2);
CHECK(memcmp(d1, d2, sizeof(d1)) == 0, "TC-SH-005 逐字节喂入与一次性等价");
}
/* ===================== TC-SH-006..007:hex_eq ===================== */
static void tc_hex_eq(void)
{
uint8_t d[SHA256_DIGEST_SIZE];
char lower[65];
char upper[65];
char shortv[65];
char longv[66];
sha256_calc("adminadmin", 10u, d);
digest_to_hex(d, lower);
for (int i = 0; i < 64; i++) {
upper[i] = (char)((lower[i] >= 'a') ? (lower[i] - 32) : lower[i]);
}
upper[64] = '\0';
CHECK(sha256_hex_eq(d, lower) == 1, "TC-SH-006a 小写 hex 匹配");
CHECK(sha256_hex_eq(d, upper) == 1, "TC-SH-006b 大写 hex 匹配(大小写不敏感)");
lower[63] = (char)((lower[63] == '0') ? '1' : '0');
CHECK(sha256_hex_eq(d, lower) == 0, "TC-SH-007a 内容不等 → 0");
lower[63] = (char)((lower[63] == '0') ? '1' : '0'); /* 还原 */
{
char bad[65];
memcpy(bad, lower, 64);
bad[64] = '\0';
bad[10] = 'g';
CHECK(sha256_hex_eq(d, bad) == 0, "TC-SH-007b 非法 hex 字符 → 0");
}
memcpy(shortv, lower, 63);
shortv[63] = '\0';
CHECK(sha256_hex_eq(d, shortv) == 0, "TC-SH-007c 长度 63 → 0");
memcpy(longv, lower, 64);
longv[64] = 'a';
longv[65] = '\0';
CHECK(sha256_hex_eq(d, longv) == 0, "TC-SH-007d 长度 65 → 0");
CHECK(sha256_hex_eq(d, NULL) == 0, "TC-SH-007e NULL → 0");
}
/* ===================== TC-SH-008:默认口令哈希钉值 ===================== */
static void tc_default_hash(void)
{
uint8_t d[SHA256_DIGEST_SIZE];
/* 与 ftp_cfg.h FTP_CFG_DEF_HASH 同源钉值(两处若漂移本测试立刻红) */
sha256_calc("adminadmin", 10u, d);
CHECK(sha256_hex_eq(d, "d82494f05d6917ba02f7aaa29689ccb444bb73f20380876cb05d1f37537b7892") == 1,
"TC-SH-008 SHA256(\"adminadmin\") = 出厂默认 pwd_hash");
}
/* ===================== main ===================== */
int main(void)
{
printf("=== SHA-256 Tests ===\n");
printf("-- FIPS 180-4 vectors --\n");
tc_vectors();
printf("-- streaming equivalence --\n");
tc_stream_equiv();
printf("-- hex_eq --\n");
tc_hex_eq();
printf("-- default password hash --\n");
tc_default_hash();
printf("\n=== SHA-256 Tests: %d/%d PASSED (failed=%d) ===\n",
s_passed, s_total, s_failed);
return (s_failed == 0) ? 0 : 1;
}