Compare commits
273 Commits
IEC60870
...
ci/remote-ci
| Author | SHA1 | Date | |
|---|---|---|---|
| 529c58da39 | |||
| dc5d6a0680 | |||
| ae80bb8e34 | |||
| c1e7e8c42c | |||
| 51ecd36192 | |||
| 2d18193d38 | |||
| 5869c0307b | |||
| b24b2b5f31 | |||
| ac9ca33f0f | |||
| 41c277a8ae | |||
| b1280e3cd1 | |||
| 496b4dc5e2 | |||
| 8ff2743374 | |||
| b623a406ad | |||
| 52e28885cb | |||
| 45086cf5f9 | |||
| 1d4b6a17ce | |||
| e7136cbea3 | |||
| 0f8b88337e | |||
| 7d9db33a25 | |||
| 77779d49d6 | |||
| 0f7b5a87fb | |||
| 7635480e06 | |||
| 50e5877882 | |||
| 68cbf8ea75 | |||
| 17ece864d1 | |||
| ac51ff2154 | |||
| a92baa269e | |||
| 26a236a597 | |||
| e2d3f5114e | |||
| 737bffc9b5 | |||
| 4c05b58f3a | |||
| ceb753bd44 | |||
| 1a624b1fae | |||
| 9d90e00d52 | |||
| f6e9106f0d | |||
| a21f3d391f | |||
| 621421f3ca | |||
| e771a8ea21 | |||
| 3e03375c88 | |||
| 741564fa28 | |||
| c23b21eed8 | |||
| 6a904c1bee | |||
| 66f862e770 | |||
| 2d16fdddc0 | |||
| 4777ed2373 | |||
| 138aebc6d6 | |||
| 2808ea9fb8 | |||
| 7099c56c34 | |||
| 498cc60b9d | |||
| dea75914f5 | |||
| fa80b17d5f | |||
| a8de47ca3f | |||
| 3dc7f4a088 | |||
| 345e12ebd7 | |||
| 4332142680 | |||
| 9bd7ff96d3 | |||
| 45393be3d5 | |||
| 8ee3be4c97 | |||
| 67c469fa40 | |||
| a6d6588257 | |||
| 4380098cc1 | |||
| 30ebbe0c26 | |||
| 4cba2eae02 | |||
| 3965eb3f28 | |||
| 42b47aa5ac | |||
| 7bbcc9cf6f | |||
| 8c72698666 | |||
| 0cbae7dab2 | |||
| 8c47005fe9 | |||
| acc547efaf | |||
| cc3595a1c4 | |||
| efc3ed1c61 | |||
| 6f18f8d811 | |||
| c89c8effd4 | |||
| adeef7d21b | |||
| 969fc5c132 | |||
| 9c2653c6ef | |||
| 62987b4596 | |||
| a3c6e89378 | |||
| b746a1ff00 | |||
| 48bc50e794 | |||
| 98664a11ed | |||
| 201d713226 | |||
| 8cbc7bc6b6 | |||
| 042e85820c | |||
| 6f99dea3a9 | |||
| 484d068250 | |||
| 13ef0715bb | |||
| db9f256fd0 | |||
| bd7f15fc3e | |||
| 73f9e10492 | |||
| 8c28f3bfdd | |||
| 54dd019950 | |||
| d07a9a4d67 | |||
| a28123115d | |||
| 5f1057ac74 | |||
| 383b4500d0 | |||
| a9ba8ec745 | |||
| 59da40bb4a | |||
| 891ca81995 | |||
| ffd462ef3c | |||
| c4edf352fe | |||
| adabb6af12 | |||
| 16917bf165 | |||
| 317e04219d | |||
| d89c07c40c | |||
| c1b425dc53 | |||
| f711130b4c | |||
| a5f7385cad | |||
| d39e5c9ab5 | |||
| 3528644e9d | |||
| 7c4628b043 | |||
| e1967cf2ea | |||
| ba9450c50b | |||
| e25e51c378 | |||
| d9ab9552c3 | |||
| b802651a56 | |||
| a3f0c60aef | |||
| 2e0f66a07e | |||
| 65c5e7d310 | |||
| 231dc81239 | |||
| 411594bf67 | |||
| 658fbe7a72 | |||
| 56893709fe | |||
| 15aa08552c | |||
| 1e6996ecd0 | |||
| 7048e9cb9a | |||
| 92b8bf9bcf | |||
| ef688485ad | |||
| ea7d0170e2 | |||
| 42886581ef | |||
| 9c2e241978 | |||
| 4a09d4c63f | |||
| 924ac3d428 | |||
| 5a806d8a26 | |||
| 794906cdc8 | |||
| 4ac869058d | |||
| c4ca292102 | |||
| e3276368b2 | |||
| 2d7c4fd40b | |||
| 744a1cb086 | |||
| 2c3939494d | |||
| ec2dbc9e01 | |||
| c8de948089 | |||
| 957ec64645 | |||
| a0c261b5ee | |||
| 8aaf9bbb0e | |||
| 59e6d77224 | |||
| b4441f0e1e | |||
| 0a3b932395 | |||
| fa4cde36f6 | |||
| e307213745 | |||
| 5224f21622 | |||
| 658bc40ccc | |||
| 6bdf063f50 | |||
| b1291f1308 | |||
| e70b861215 | |||
| 6505cef9df | |||
| 2bfa09ed15 | |||
| 49a257b34c | |||
| c0ccd777ae | |||
| 70dd6f8192 | |||
| 1e4f74ba14 | |||
| b145e74ded | |||
| b22779d244 | |||
| 6822726b2c | |||
| 871ace422b | |||
| dc44c1e54e | |||
| 3488617bc9 | |||
| 2575177e81 | |||
| d9b887c0af | |||
| cc2966319a | |||
| 78248a1cdf | |||
| 8d3349c587 | |||
| 23daa3d9ad | |||
| 8b0ea17b71 | |||
| 4c21cff753 | |||
| af5bad5609 | |||
| 15d01d4dc1 | |||
| e7f28502bc | |||
| 6ede1443e9 | |||
| 7e77af08a0 | |||
| 36a3c3449c | |||
| 67fbdd9897 | |||
| 538c44c1cc | |||
| 504db26698 | |||
| f4b9bd57d5 | |||
| d4867ddbe2 | |||
| af411866a8 | |||
| 125140a3c6 | |||
| 11c82c0d99 | |||
| ee25b10053 | |||
| a38f862fb9 | |||
| ebe2807ffd | |||
| 0a66e56770 | |||
| 790e153cd6 | |||
| 9f826dbb4d | |||
| 60a650c175 | |||
| 513becd61d | |||
| a26a9d6f52 | |||
| 3c69411409 | |||
| 4d18e3b60d | |||
| 514a514601 | |||
| a9fd5572a3 | |||
| fca414102e | |||
| 43fbfa140b | |||
| 52976bdec3 | |||
| 51604f7717 | |||
| 5a9988b557 | |||
| 64d8b052b1 | |||
| 0f0d002afc | |||
| 858434492c | |||
| d74cd030ea | |||
| 4b9a4f4e24 | |||
| bda7ab2b50 | |||
| 301c3e5183 | |||
| efeaccbb18 | |||
| fafe13f7ea | |||
| 64abefaa00 | |||
| 44dec5c49b | |||
| dafc18f5ed | |||
| 9d06798f2d | |||
| 118ea4f005 | |||
| db16694a87 | |||
| 720f841494 | |||
| 8739faa7fe | |||
| ff9fcbd43c | |||
| 172b3f4a20 | |||
| 0cece5a305 | |||
| a7f526569a | |||
| 506eccc68f | |||
| 803f7717ed | |||
| 617c84f570 | |||
| 1e568b8549 | |||
| d770e36c7c | |||
| cea2b51efb | |||
| 4f8e298394 | |||
| 2a6080827d | |||
| dd51221ebc | |||
| 63cf73b4d0 | |||
| 0edbfaf08b | |||
| e95d2be30b | |||
| f849c79b7a | |||
| df13dec479 | |||
| a12367981d | |||
| 1a35b3684c | |||
| 491dd031f1 | |||
| 51472199b4 | |||
| 925058a5ae | |||
| 74ba7ea61b | |||
| b445cbb464 | |||
| c7459b393f | |||
| 123d80afc7 | |||
| f51c0833ae | |||
| 6394853fef | |||
| 83a70721bd | |||
| f7f17931c9 | |||
| 33405a57b5 | |||
| d346e4ee84 | |||
| 8f902e09fd | |||
| 13a4a40a14 | |||
| d96113bcc4 | |||
| eac36526e7 | |||
| bfbe24e423 | |||
| b3ef7289a0 | |||
| d5bdc6eb54 | |||
| b8c6a3dab0 | |||
| 4c0a0af9f8 | |||
| ae17d36f5a | |||
| b8d87c62d7 | |||
| 9ac9879527 | |||
| a0b9d27821 |
@@ -1,20 +1,21 @@
|
||||
{
|
||||
"snapshot": {
|
||||
"iwdg-defer-start": "ca839ffc1cda439f",
|
||||
"hardfault-handler": "f3afab8d2be18794",
|
||||
"fault-handlers-3": "f3afab8d2be18794",
|
||||
"hardfault-handler": "a7f538eccdf1d88d",
|
||||
"fault-handlers-3": "a7f538eccdf1d88d",
|
||||
"fault-handler-enable": "be87fcd3a7926138",
|
||||
"hardfault-report-impl": "f3afab8d2be18794",
|
||||
"hardfault-report-impl": "a7f538eccdf1d88d",
|
||||
"error-handler-degrade": "d02a92afda945c4b",
|
||||
"stack-overflow-check": "d4735e3a265e16ee",
|
||||
"malloc-failed-hook": "6b86b273ff34fce1",
|
||||
"assert-not-deadloop": "d4a00718ab2bd071",
|
||||
"assert-impl": "5d862ceb21c77a86",
|
||||
"stack-overflow-hook-impl": "a006edbd30d2c4d7",
|
||||
"assert-impl": "0ccf10a7fb115aa8",
|
||||
"stack-overflow-hook-impl": "03a383dda578d12a",
|
||||
"malloc-failed-hook-impl": "fcd7250e5cd6403b",
|
||||
"stack-overflow-hook-helper": "cacd9fd028be9b5b",
|
||||
"watchdog-module": "663f05caf22cf7d0",
|
||||
"reset-cause-latch": "52fc71acbb0f1aed",
|
||||
"watchdog-module": "07808632996d66e8",
|
||||
"net-msgq-adopt": "a0f913e3f34c8e9e",
|
||||
"reset-cause-latch": "983927d587cb9f48",
|
||||
"iwdg-hal-driver": "4b902875a84554e9",
|
||||
"hal-iwdg-enabled": "1a8ed6cff34f7ba6",
|
||||
"ioc-keepusercode": "b5bea41b6c623f7c",
|
||||
|
||||
@@ -14,6 +14,10 @@ whenToUse: UI 代码新增中文文案、用户报告屏幕缺字/空白块、
|
||||
# 1. 代码落库(新界面字符串已写入 ui_menu.c / ui_content_page.c 等 UI 源码)
|
||||
# 2. 提取字符集(纯标准库,任意 python3 均可)
|
||||
python tools/font_gen/extract_cn.py # 扫描 UI 源码字符串 → 仓库根 ui_cn_chars.txt
|
||||
# ⚠ extract 只认"当前源码字面量",会把清单里 KEEP_EXTRA 那批**刻意保留字**(历史落盘
|
||||
# 记录用字,逐条理由见 check_font_charset.py 的 KEEP_EXTRA 字典——不在本文列举,列举必
|
||||
# 然过期)从 txt 洗掉:跑 make_font 前必须把它们并回 ui_cn_chars.txt,否则 font-charset
|
||||
# 门禁 A 段 FAIL 并逐字打印保留理由;事故路径实名登记见 docs/门禁说明/font-charset.md §6.1
|
||||
# 3. 生成字体(需 fontTools,一律用 .venv;宋体 .ttc 的 face0 抽取与 ▶ 箭头补回均自动完成,幂等)
|
||||
.venv\Scripts\python.exe tools/font_gen/make_font.py
|
||||
# 变体:--bpp 2(宋体细笔画发虚时)/ --font C:/path/to/font.ttf(换字体)
|
||||
@@ -30,4 +34,5 @@ python tools/font_gen/extract_cn.py # 扫描 UI 源码字符串 →
|
||||
- `requirements.txt` 必须**纯 ASCII**(Windows 下 pip 按 GBK 解码,中文注释 → UnicodeDecodeError)
|
||||
- 生僻字(宋体可能无字形)→ **写码前先手动验证字形**,避免"代码写完才发现是空白"
|
||||
- 生成成功后跑 `ui-verify` skill 全回归;黄金参考因字形变化而 FAIL 属预期 → `check_content_pages.py --update` 刷新
|
||||
- 字数变化时同步更新文档中的字符集计数(当前 97 汉字 + ASCII 0x20-0x7E,共 193 字形):`UI_中文字体生成与配置.md`、`UI_界面制作流程.md` C3、`UI_设计架构.md`、`simulator/README.md` 已知坑 #4
|
||||
- 字数变化时同步更新文档中的字符集计数(当前 179 汉字/符号 + ASCII 0x20-0x7E,共 274 字形):`UI_中文字体生成与配置.md`、`UI_界面制作流程.md` C3、`UI_设计架构.md`、`simulator/README.md` 已知坑 #4
|
||||
- **新增字低于既有最小码位**(如 Ⅰ U+2160)会下探稀疏 cmap 起点并吞掉单码位补回条目(实测 ▶ 箭头全变方框而旧版 coverage 全绿)——`make_font.py`(2026-10-25 起)按 LVGL 首命中语义插入/守卫/校验,扩字后模拟器像素回归**必跑**
|
||||
|
||||
@@ -26,9 +26,11 @@ PC 上编译运行与 MCU **同一份** UI 源码(CMake + MSVC + SDL2,x64,
|
||||
| `lvgl_sim.exe --screenshot shot.bin` | 离屏渲染首帧 → `simulator/i1_to_png.py` 转 PNG 目检 |
|
||||
| `lvgl_sim.exe --keys "ud" shot.bin` | 注入键序后截图:`u`=↑ `d`=↓ `l`=← `r`=→ `e`=Enter `s`=Esc(`main.c` 实锤) |
|
||||
| `lvgl_sim.exe --standby shot.bin` | 待机页截图 |
|
||||
| `lvgl_sim.exe --cfgtest` | 配置引擎闭环:加载→轮询→外部改文件重载→外科式保存回验(在当前 CWD 产生 `sim_config.ini`) |
|
||||
| `lvgl_sim.exe --cfgtest` | 配置引擎闭环:加载→轮询→外部改文件重载→外科式保存回验。临时用当前 CWD 的 `sim_config.ini`,**跑完无论成败一律删除**(旧行为连 PASS 路径也会留一份 4000 B 伪造档 → 之后每次运行都被加载 → 引擎按设计拒写超长文档 → 网口设置页保存行变 `ERR`,把像素回归带偏) |
|
||||
| `lvgl_sim.exe --logtest` | syslog 后端闭环(定长记录/时间戳/轮转) |
|
||||
|
||||
⚠ **模拟器按当前工作目录读写 `sim_config.ini`**:目录里残留一份配置就会改变页面内容。`simulator/tests/` 两套回归因此一律在**空临时目录**里跑(`cwd=tmp`);手工跑 `--keys`/`--screenshot` 做对比时,也要先确认当前目录没有 `sim_config.ini`,否则"复现不了别人的 FAIL"。
|
||||
|
||||
## UI 改动回归 → 用 `ui-verify` skill(勿在此零散手跑)
|
||||
|
||||
5 步门禁 = 构建 → `check_menu_rows.py`(菜单 8 状态像素 + 池基线)→ `check_content_pages.py`(文本断言 + 11 页黄金参考 + 往返池)→ `@build` → `coding-standard`。
|
||||
@@ -37,14 +39,14 @@ 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
|
||||
- **改 `simulator/lv_conf.h` 触发 `lv-conf-sync` 门禁**:它与 MCU 端 `Lib/lv_conf.h` 必须保持同一特性子集(按**生效值**比对,未定义取 `lv_conf_internal.h` 兜底);四类判定与端专属白名单 → `docs/门禁说明/lv-conf-sync.md`;顶层 exclude 的教训 → `docs/代码规范/pre-commit使用说明.md` §5.5
|
||||
- **模拟器代码与固件同标准受检**(`cppcheck` / `coding-standard` / 乱码 / 改写型钩子 / `doc-links` 全覆盖 `simulator/`);**改动触及 `sim/` 任何后端,必须同步在 `docs/测试说明/模拟器差距清单.md` 登记一行**——模拟器跑过 ≠ 真机跑过
|
||||
|
||||
## 构建/运行报错速查(详解 `simulator/README.md`「四、已知坑(构建/运行)」)
|
||||
|
||||
| 现象 | 根因 |
|
||||
|---|---|
|
||||
| 找不到 `SDL2/SDL.h` | LVGL SDL 驱动写死该路径,头文件须有 `include/SDL2/` 副本(仓库已内置) |
|
||||
| 找不到 `SDL2/SDL.h` | LVGL SDL 驱动写死该路径,须有 `include/SDL2/` 副本。⚠ **这份 SDK 不入库**(`.gitignore` 排除 `simulator/SDL2/`,实测 `git ls-files` 在该路径命中 0 个文件)——"下载才有"不等于"仓库已内置";CI/runner 的干净检出里没有它,2026-10-27 run 45 就在这里红。CI 侧用机器级 `DTU_SDL2_DIR` 指一份 SDK,见 `docs/CI/远程CI完整指南.md` §4.5 |
|
||||
| 找不到 `lv_conf.h` | `LV_CONF_DIR`/`SDL2_INCLUDE_DIR` 必须在 `add_subdirectory(lvgl)` **之前**定义 |
|
||||
| 窗口全黑 | mono 主题未生效 / 缺 `lv_refr_now(NULL)` / 重复喂 tick(SDL 驱动内部已 `lv_tick_set_cb`) |
|
||||
| 画面正常但按键全无效 | 主循环**不得**调 `SDL_PollEvent`——LVGL SDL 驱动内部 5ms 定时器统一分发,先轮询会抢光事件 |
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
---
|
||||
name: remote-ci-ops
|
||||
description: 远程 CI(Gitea Actions + 本机 Windows 自建 runner)运维闭环:runner 保活与抢锁清理、"没有 run / 任务消失"的四因定位、红因三分法(job 没跑起来 / runner 缺环境 / 真门禁失败)、无令牌时用 Gitea 的 MySQL 直查 runner 在线与历次 run/job 时长与红绿史(含 status 枚举定死办法)、用 gitea-runner.exe generate-config 枚举 runner 真实配置键而不猜键名、本地对照日志落 ci-artifacts/、门禁数字口径、gitlint 在 CI 的两种空转(含 runner 无 git 身份致 exit 254 假失败)、PS 5.1 原生 stderr 与编码陷阱、改 workflow / 门禁脚本的红线与改后验收清单。背景与全部实测细节在 docs/CI/远程CI完整指南.md,本 skill 只给操作顺序与判据。
|
||||
whenToUse: CI 没跑、run 被取消或零日志、页面报的失败时长/红绿史跟本地或日志对不上、"这 job 到底跑没跑起来"、门禁假绿/假红、runner 显示 online 却每 10 秒重启、自建 runner 有哪些可用配置项(cache 之类,不确定键名)、要改 .gitea/workflows/ci.yml 或 scripts/ci_*.ps1|py、或没有 API 令牌却要判断 CI 状态时。
|
||||
---
|
||||
|
||||
# remote-ci-ops — 远程 CI 运维闭环
|
||||
|
||||
**权威细节**:`docs/CI/远程CI完整指南.md`(部署、全部实测坑、章节导航),唯一入口 `docs/CI/README.md`。
|
||||
本 skill 只回答两件事:**出问题时按什么顺序查**、**动手前后各验什么**。同一事实不在两处详述。
|
||||
|
||||
> **TODO(待同步回指南)**:`docs/CI/远程CI完整指南.md` §6.1 目前只写 `action_runner.last_online` 的在线核验与 `mysql.exe` CLI;
|
||||
> 本轮把同一通道扩到 `action_run` / `action_run_job` / `action_task` 的历史与时长,且实际驱动是
|
||||
> **pymysql**(不是 mysql.exe)。下面 §4.3 / §7 是本轮实测口径,落定后应并回`docs/CI/远程CI完整指南.md` §6.1 与 §2.3。
|
||||
|
||||
**本机事实**(变了就改指南,别改这里):runner `win-stm32-ci`(`gitea-runner` v4.0.1,labels `[windows]`,与 `runs-on: windows` 匹配);目录 `D:\Docker\act_runner\`;Gitea 1.25.4 监听 `127.0.0.1:3001`,数据库 MySQL `127.0.0.1:3306`/gitea(凭据在 `D:\Docker\Gitea\custom\conf\app.ini` 的 `[database]`,**口令不进脚本、不进报告**);本仓库 `repository.id = 10`(`wandering1/stm32f4-base`,另一仓 stm32f103 = 13);**CI 只跑这一台机器**,与其他机器无关。
|
||||
|
||||
## 0. 先分层,别一上来读 workflow
|
||||
|
||||
**分层之前先做 §1**:一次红很可能压根不是"门禁内容"问题,直接读 workflow 会白绕一圈。
|
||||
|
||||
| 层 | 就位判据 | 不在位的症状 |
|
||||
|---|---|---|
|
||||
| ① 服务端 Actions 开关 | `docs/CI/远程CI完整指南.md` §1.1 | push 后 Actions 页**完全空白** |
|
||||
| ② runner 在线且工具链对 SYSTEM 可见 | `docs/CI/远程CI完整指南.md` §4.1 + 下面 §4 | run 建了但一直排队;或 L0 死在"环境自检"、一个钩子都没跑 |
|
||||
| ③ workflow 与门禁 | `scripts/ci_local.py` 本地全绿 | 具体 job 红/绿与本地不一致 |
|
||||
|
||||
**本地永远先跑这个**(runner 掉线也不影响它,是 L0 的唯一保障;日志落法见 §5):
|
||||
|
||||
```powershell
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py --keep-going # L0 + L0b + L0.5
|
||||
```
|
||||
|
||||
## 1. 第一步:这个 job 到底跑没跑起来(红因三分法)
|
||||
|
||||
**只看一条判据**:runner 侧那份 job 日志(`D:\Docker\act_runner\log\job\*-task-NN.log`)里
|
||||
**有没有出现任何一条钩子名(`cppcheck.......Passed`)或 `pre-commit hooks -- passed=A failed=B skipped=C` 汇总行**。
|
||||
|
||||
| 类 | 判据(日志里的证据行) | 结论与动作 |
|
||||
|---|---|---|
|
||||
| **A. job 没跑起来** | checkout 步骤报拉不到 `actions/checkout` / 连 `github.com` 超时,**通篇零钩子输出** | 门禁内容无罪。内网无公网时的三种处置在 `docs/CI/远程CI完整指南.md` §5.3(推荐零依赖纯 git checkout)。**先修它再谈别的**,否则后面所有"红"都不可读 |
|
||||
| **B. runner 机器缺环境** | `::warning::runner 未安装 MSVC`、`no Python with a SYSTEM-visible pre-commit`、`runner missing cppcheck`、`test-coverage` 因缺 **mscoverage** 组件而 FAIL(按用户裁定缺件即 FAIL,不 skip)、`ui-golden` 因缺 **SDL2 SDK** 而 FAIL(该 SDK 不入库,见指南 §4.5) | 也是"非代码问题",但**别改成 skip 蒙过去**:按`docs/CI/远程CI完整指南.md` §4.1/§4.2 补机器级工具链或 `DTU_*`,改完重启 runner |
|
||||
| **C. 真门禁失败** | 钩子清单完整且 `failed>0` 或某条 `Failed`,**且 §5 的本地同命令复现同样的红** | 这才轮到读 workflow / 门禁脚本,按失败钩子逐条定位 |
|
||||
|
||||
**顺序不能反**:A/B 两类在本地都是"全绿",用"本地绿 + CI 红"直接推"环境问题"也推不出是 A 还是 B——只有钩子输出能把它们分开。本轮实测:页面报的红看着像门禁失败,实际是 A(checkout 联网超时),而 A 修好之后 L0 还会再红两次——一条 C、两条 B。
|
||||
|
||||
## 2. 症状 → 一条判据 → 动作
|
||||
|
||||
| 症状 | 判据(跑这一条就够) | 结论 / 动作 |
|
||||
|---|---|---|
|
||||
| Actions 页空白、无 run | 看改了啥 + 怎么推的 | 四因:①服务端开关未开 ②workflow 没推到远程 ③**纯文档**(`paths-ignore: docs/**`、`**/*.md`)④**新建分支的首次 push**——`before` 是全零 SHA,变更文件集算不出来被 `paths-ignore` 滤空。④ 的实测:建分支那次 push 无 run,紧接着一次普通 push 立刻出 task;绕法 `workflow_dispatch` 或再推一个非文档提交 |
|
||||
| **页面报的时长/红数与本地或日志对不上** | §4.3 查 `action_run`/`action_run_job` 的 created/started/stopped | **页面时长不可照抄**。本轮实测页面写 `Failing after 6m49s`,库里 41 次 run **最长 ≈4m33s**,run 41 总时长 95 s(L0 45 s / L0.5 43 s)——**没有任何一条记录对得上 6m49s**(多半掺了排队/重试/页面自身口径)。以库里的 `total=stopped-created`、`run=stopped-started`、`queue=started-created` 三个数为准 |
|
||||
| run 全 **cancelled**、零日志 | `& '.\scripts\ci_runner_bootstrap.ps1' -Status` | runner 掉线;且 `ci.yml` 的 `concurrency.cancel-in-progress: true` 会把排队 run 被下次 push 取消 → **症状是"任务消失"而不是"任务失败"**(历史上 16–21 连续 12 个 job 零日志即此) |
|
||||
| `ui-golden` 红、日志是 `C1083: SDL2/SDL.h` 或 `MSB1009: 项目文件不存在`,本地全绿 | 前者 = 检出里根本没有 SDL2 SDK(`.gitignore` 排除它,`git ls-files` 在该 SDK 目录下命中 0 个文件);后者 = 上一轮配置在硬校验处中断,**CMake 缓存文件已写下、工程文件却没生成**,于是下一轮跳过配置 | 设机器级 `DTU_SDL2_DIR`(指南 §4.5);模拟器构建脚本已能自愈,也可手删那份构建目录。**别把构建产物路径当文档引用写**——它不入库,CI 的干净检出里不存在,`doc-links` 会「本地过、CI 挂」(指南 §4.1.2 同族) |
|
||||
| runner 显示 **online** 却每 10 秒重启、日志暴涨(实测 ~20 MB/天) | daemon 日志出现 `another gitea-runner process is already using ".runner"` | **双看门狗抢锁**:`Unregister-ScheduledTask` **不会**停掉旧实例起的进程 → 孤儿看门狗养着旧 daemon → 它持 `.runner.lock` → 新任务里的 daemon 起不来,而**服务端仍显示 online**(旧 daemon 在服务)。修:管理员 `-StartNow`(它**先杀看门狗、再杀 daemon**,顺序反了会被 10 秒内复活) |
|
||||
| L0 失败但**本地同一条命令全绿**,日志 `exit code: 254` + `Missing git configuration: please set user.name` | 看是不是 254 | **不是提交信息问题**:`actions/checkout` 把 HOME 指向每次任务的临时目录,SYSTEM 无全局 git 身份 → gitlint 在读规则**之前**就配置错误退出。已由 `scripts/ci_gitlint_range.ps1` 用 `GIT_CONFIG_*` 注入一次性身份修掉;**254 = 门禁没跑起来(exit 2),1 = 信息真违规(exit 1)**,别把两者混成一句 |
|
||||
| 钩子条数对不上 | `grep -c 'id:' .pre-commit-config.yaml` | **报告数 = 定义数 − 1**(gitlint 属 `commit-msg` stage,`--all-files` 跑不到且表现为全绿 = 静默空转)。增删钩子后重算这个差值 |
|
||||
| `test-coverage` 恒 `[ENV] exit 2`(缺 `Microsoft.CodeCoverage.Console.exe`),或换 VS 世代后大面积假红 | `& '.venv\Scripts\python.exe' scripts/check_test_coverage.py --list` 看它探到几个根;再看机器级 `DTU_MSVC_CL` 指的是哪套 cl | 该 exe **只随 VS 分发**(NuGet 实测拿不到);装在非默认盘还要显式指 `DTU_MSVC_CL`。安装入口 + 组件 ID + 三条管理员命令在 `docs/CI/远程CI完整指南.md` §4.4 |
|
||||
| `mem-numbers` 报"文档与 .map 不一致",但**没改过代码也没改过那篇文档** | `.map` 的 mtime 是否早于最近一次切分支 | **跨分支假象**:`.map` 是 gitignored 的 Keil 产物,切分支后它仍属于另一分支的代码。重新 `@build` 即一致,不要去改文档数字 |
|
||||
| 明明设了 `DTU_KEIL_UV4` 却报 `UV4.exe not found` | `[Environment]::GetEnvironmentVariable('DTU_KEIL_UV4','User')` | 会话没继承用户级变量。**回填时别按"进程里已有的变量名"枚举**——Machine 轮会把只在 User 定义的变量置空(PS 赋空值 = 删除),User 轮就再也看不到它。正确:名字取 Machine/User **并集**,且只在取到值时写入 |
|
||||
| 日志里出现 `gitea-runner.exe : …` + 4 行 `CategoryInfo`/`FullyQualifiedErrorId` | 实测 2 行真日志写成 8 行 | PS 5.1 的 `2>&1` 把每条原生 stderr 包成 ErrorRecord。修:`2>&1 \| ForEach-Object { "$_" }`(等价于原始行,且 `$LASTEXITCODE` 不受影响)。详见`docs/CI/远程CI完整指南.md` §7.3 |
|
||||
| 日志文件半 UTF-8 半乱码 | 文件头是否 `FF FE` | `>` / `*>>` 在 PS 5.1 = `Out-File` = **默认 UTF-16LE**;与 `Add-Content -Encoding ascii` 混用会一个文件两种编码 |
|
||||
|
||||
## 3. runner 保活与注册(唯一入口脚本)
|
||||
|
||||
```powershell
|
||||
# 免管理员预演,先看它会做什么
|
||||
& '.\scripts\ci_runner_bootstrap.ps1' -DryRun
|
||||
# 正式:写机器级 DTU_* + 装看门狗任务 + 立即起(需管理员)
|
||||
powershell -ExecutionPolicy Bypass -File scripts\ci_runner_bootstrap.ps1 -SetEnv -StartNow
|
||||
# 体检(非管理员会明说哪些项读不到,而不是谎报)
|
||||
& '.\scripts\ci_runner_bootstrap.ps1' -Status
|
||||
```
|
||||
|
||||
要点(细节与理由见 `docs/CI/远程CI完整指南.md` §2.4):
|
||||
|
||||
- **不能只起 daemon**:v4 无服务支持,且**首次 Declare 失败就退出** → 一次 Gitea 重启就永久静默掉线。
|
||||
- **不能用 Windows 服务**(服务化方案已被实测推翻,见 `docs/CI/远程CI完整指南.md` §3.0);任务计划程序的"失败后重启"**永不触发**,因为 daemon 的退出是**正常退出**。
|
||||
- 任务对象:`-AtStartup` + `Delay=PT1M`、`ExecutionTimeLimit` 为零、`RestartCount 999`、`MultipleInstances IgnoreNew`、`$settings.StartWhenAvailable=$true`——⚠ `StartWhenAvailable` 是**任务设置**属性,挂到 trigger 上会抛异常(曾因此注册崩溃)。
|
||||
- `0x41301` = `SCHED_S_TASK_RUNNING`(不是失败);`-AtStartup` 的任务"下次运行时间"为空是正常的。
|
||||
- 改完 bootstrap 脚本里的启动器模板,**磁盘上的看门狗要下次 `-StartNow` 或开机才刷新**——不必为此专门重启 runner。
|
||||
|
||||
## 4. 没有 API 令牌时怎么判断状态
|
||||
|
||||
匿名调 `/api/v1/repos/.../actions/runs` 是 **401**。三条不依赖令牌的路,按"在线 → 落没落地 → 历史与时长"排。
|
||||
|
||||
### 4.1 在线核验(查 `action_runner`)
|
||||
|
||||
`action_runner.last_online` 与当前 Unix 秒相差 **< 30 秒**即在正常心跳;`agent_labels` 应为 `["windows"]`(不含 windows 就是标签配错)。口径见 `docs/CI/远程CI完整指南.md` §6.1。
|
||||
|
||||
### 4.2 看 runner 侧(最直接,能证明"job 真落到本机了")
|
||||
|
||||
```powershell
|
||||
$d = 'D:\Docker\act_runner\log'
|
||||
# 日志被 daemon 占着,必须共享读打开(ReadAllBytes 会 IOException)
|
||||
$fs = [System.IO.File]::Open("$d\runner-daemon.log",'Open','Read','ReadWrite')
|
||||
# 关键行:declare successfully / writing the log of task N to … / Running job with …
|
||||
Get-ChildItem "$d\job" -File | Sort-Object LastWriteTime -Descending | Select-Object -First 5
|
||||
```
|
||||
|
||||
判读:`declare successfully` 后长时间零输出 = **在线且空闲**;`log\job\*.log` 出现新文件 = 有 task 落地;每个 job 结束时 `[runner] task N finished: success|failure`。**这份 `finished:` 行也是 §4.3 里校验 status 枚举的裁判**(§1 的三分法同样从这些文件起手)。
|
||||
|
||||
⚠ 非管理员**读不到 SYSTEM 进程的 `CommandLine`**(返回 `$null` → 过滤器静默匹配 0 个)→ 绝不能据此说"没有看门狗"。`Process.StartTime` 对 SYSTEM 也不可读,但 CIM 的 `CreationDate` 可读 → 用**进程存活时长**对比 10 秒重启间隔来判断重启循环,**不要 grep 不轮转的日志尾部**(旧失败一直在尾部,会对健康 runner 报假警)。
|
||||
⚠ 查询进程自己的命令行含关键字会**自匹配**:把模式拆开写,如 `'run_runner_watch' + 'dog\.ps1'`。
|
||||
|
||||
### 4.3 查 run / job 历史与时长(推翻页面上的说法)
|
||||
|
||||
**驱动就认 pymysql**:本机 `C:/ProgramData/Anaconda3/python.exe` 实测 `pymysql=True`、`MySQLdb=False`、`mysql.connector` **缺失**(import 直接 `ModuleNotFoundError`)→ `docs/CI/远程CI完整指南.md` §6.1 的 `mysql.exe` CLI 路线本机走不通,改 pymysql 连 `127.0.0.1:3306`/gitea。
|
||||
|
||||
**口令只从 app.ini 运行时读**:`[database]` 段里 `HOST` 是 `host:port` 一体要拆开,`NAME`/`USER`/`PASSWD` 直接用;**绝不要把口令写进脚本字面量、报告、截图或提交**(本轮就是这么漏的一次)。同段还有 `PATH = ...gitea.db`,但 `DB_TYPE = mysql` → **别去读那个 sqlite 文件**。`token`/`PASSWD` 一类行在打印任何配置文件时统一 redact。
|
||||
|
||||
```python
|
||||
# 写成文件跑(ci-artifacts/_q.py),别内联,见 §6
|
||||
import re, pymysql
|
||||
ini = open(r'D:/Docker/Gitea/custom/conf/app.ini', encoding='utf-8').read()
|
||||
sec = ini.split('[database]')[1].split('\n[', 1)[0]
|
||||
kv = dict(re.findall(r'(?m)^(\w+)\s*=\s*(.*)$', sec))
|
||||
host, port = kv['HOST'].split(':')
|
||||
db = pymysql.connect(host=host, port=int(port), user=kv['USER'], password=kv['PASSWD'],
|
||||
database=kv['NAME'], charset='utf8mb4',
|
||||
cursorclass=pymysql.cursors.DictCursor)
|
||||
cur = db.cursor()
|
||||
cur.execute("SHOW TABLES LIKE 'action%'") # → action_run / action_run_job / action_task /
|
||||
# action_run_index / action_runner / action_task_step …
|
||||
cur.execute("SHOW COLUMNS FROM action_run_job") # 🔴 列名以这里为准,别凭记忆 SELECT
|
||||
```
|
||||
|
||||
**取数(本仓库务必带 `repo_id=10`,否则混进 stm32f103 的 run)**:
|
||||
|
||||
- `action_run`:`id, repo_id, status, title, ref, sha, event, created, started, stopped`,`ORDER BY id DESC`;
|
||||
- `action_run_job`:按 run 关联到 `id, status, attempt, task_id, created, started, stopped`(本轮 run 41 → job 85/86)。
|
||||
**`task_id` 就是 job 日志文件名的 `task-NN`** → `action_run_job.task_id = 66` ↔ `log/job/20261025-133227-task-66.log`,这是 DB 与 runner 日志互校的唯一把手;
|
||||
- 时长三个数:`total = stopped-created`、`run = stopped-started`、`queue = started-created`(本轮实测 queue 普遍 1 s,所以页面 6m49s 不可能是排队)。时间戳是 **Unix 秒**,`datetime.datetime.fromtimestamp(x)` 再打印;
|
||||
- **连续红次数按 job 数、不按 run**:把同一 job 历次 `status` 从新到旧排,数连续非"成功"的长度(本轮据此得出 L0 自上线 **13 次全红、从未绿过**,run 28~41)。
|
||||
|
||||
**status 枚举:先定死再下结论**(本轮最大的误判源)。
|
||||
|
||||
| 码 | 含义 | 依据 |
|
||||
|---|---|---|
|
||||
| 0 | Unknown / 未开始 | 本轮映射试错后才定 |
|
||||
| 1 | Success | 同上 |
|
||||
| 2 | **Failure** | run 41 = 2 → Failure,与页面红、与 runner 日志 `task finished: failure` 三方一致 |
|
||||
| ≥3 | **本轮未核实** | 试错时猜过 `3=Failure`/`4=Cancelled`/`5=Skipped`,**都没有交叉验证** → 用到之前先拿一条已知结局的 run 反推(TODO:从 Gitea 源码定死,别再靠猜) |
|
||||
| 5 / 6 | **非终态**(排队中 / 运行中) | 观测于 run 45:推送后先看到 job=5(未开始)与 6(进行中),数分钟后转 1 / 2;**别把它当成库里坏了或卡死** |
|
||||
|
||||
- **写查询时同时打印原始数字和映射名**;`action_run` 与 `action_run_job` **默认按同一套码处理是危险假设**,跨到别的表要重验。
|
||||
- 判据:同一个 run 换一套映射后红绿翻转(本轮 step 53 判 `Success`、step 56 判 `Failure`)→ 说明你还没定死枚举,**这时任何"红绿史"都不许出口**。
|
||||
- 中文标题在 bash 终端回显成乱码(本轮 `'docs(log): ???'`)只是**显示**问题,库里数据没坏 → **认 run 用 `id`/`sha`/`ref`,不要用标题文本匹配**;要看中文就把结果写文件再按 UTF-8 读。
|
||||
|
||||
## 5. 本地对照:日志一律落 `ci-artifacts/`
|
||||
|
||||
判"本地绿 CI 红"必须有同一份可引用的本地日志。落点在仓库内 gitignored 的 `ci-artifacts/`(`.gitignore`:159),**不要写仓库外绝对路径**:
|
||||
|
||||
```bash
|
||||
mkdir -p ci-artifacts && .venv/Scripts/python.exe scripts/ci_local.py --keep-going \
|
||||
> ci-artifacts/local_ci_$(date +%Y%m%d).log 2>&1; echo EXIT=$? >> ci-artifacts/local_ci_$(date +%Y%m%d).log
|
||||
```
|
||||
|
||||
**本轮实测的替换**:先写到 `D:/Code/_sync_backup_20261026/ci_local_*.log`(仓库外)在后台 bash job 里直接 `No such file or directory` → 改落 `ci-artifacts/` 才跑通。跑门禁这类分钟级任务用后台 job 起,再 `job_output` 收,别占着前台。Keil 构建对照同理落 `ci-artifacts/local_build_*.log`。
|
||||
|
||||
## 6. 脚本层:写查询与批量探测的规矩(本轮踩点集中在这里)
|
||||
|
||||
- **一段 Python 查询写成 JS 模板字符串会炸**(本轮两次:`Expected unicode escape`、`Expected ';', '}' or <eof>`)。正确做法:按行存字符串数组 → `join('\n')` → 落盘 `ci-artifacts/_qN.py` → 用 `C:/ProgramData/Anaconda3/python.exe` 跑 → **收尾统一 `fs.rmSync` 删掉**(临时查询脚本不入库,也别留在仓库里)。
|
||||
- **`run_code` 里不能用 `import x from '…'`**(`'import', and 'export' cannot be used outside of module code`)→ `const fs = await import('node:fs')`。
|
||||
- **批量探测拆小批**:一次调用塞 4 条 Windows 探测(worktree / 目录 / 服务枚举 / app.ini)撞过 120 s deadline,只拿到部分输出。一次 1~2 条,慢源(`Get-ChildItem -Recurse`、`Get-CimInstance Win32_Service` 全量、`generate-config`)单独跑并显式 `timeoutMs`。
|
||||
- **别对全部 runner job 日志一把梭 grep**:日志混合编码(UTF-16LE / CP936),逐文件返回一堆 `[ERR …]`。要么单文件 `head`/`sed -n 'A,Bp'`/`grep`(本轮有效路径),要么解析时逐文件 try/catch 容错。
|
||||
- **`pwsh`/`read`/`subagent` 这类工具只能在 `run_code` 程序里 `await tools.xxx()` 调**,直接当工具名调会 `unknown tool`;每次调用**都要带 `description`**(漏了会 `missing required property "description"`)。
|
||||
|
||||
## 7. runner 有哪些配置项:先 `generate-config`,别猜键名
|
||||
|
||||
**判断"这个键存不存在"的唯一凭据是 runner 自己 dump 出来的默认配置文本**,全程只读——不写 `config.yaml`、不删 `.runner`(那是注册凭据,删了要重新 register)、不重启/注销 runner(只有改 `runner.labels` 才必须重启,`docs/CI/远程CI完整指南.md` §2.3.2)。
|
||||
|
||||
```powershell
|
||||
& 'D:\Docker\act_runner\gitea-runner.exe' generate-config 2>&1 | Out-String -Width 160
|
||||
```
|
||||
|
||||
- 从输出里 **grep 目标键名**(本轮实测清单含 `cache` / `capacity` / `container` / `default_image` / `action_shallow_clone` / `access_key_file` / `addr` / `allocate_pty` / `bind_workdir` / `bucket` / `docker_host` / `docker_timeout` / `enabled` / `endpoint` / `env_file` / `envs` / `external_secret` / `external_secret_file` …,以及 `#set_act_env: true`)。
|
||||
- **dump 里绝大多数键是带 `#` 的注释行** → "在清单里" ≠ "本机已启用";是否被显式覆盖要跟 `config.yaml` 逐行比:**注释=默认值生效,非注释=本机写死了**。
|
||||
- **别拿段名当 grep 锚点**:本轮找 `[actions]` 段和 `DEFAULT_ACTIONS_URL` 时段名锚点落空、上下文打出来的是 `log_report_*` 那几行 → 直接按**键名** grep。另外服务端的 `DEFAULT_ACTIONS_URL` 是 app.ini 的键(`docs/CI/远程CI完整指南.md` §1.2.1),不要当成 runner 的键去 `config set`。
|
||||
- 老教训仍然成立:v4 的 `log` 段只有 `level` 和 `job`,**没有 `file`**,`config set log.file …` 会 `unknown config key`(`docs/CI/远程CI完整指南.md` §2.3.2)→ 改键前以 `generate-config` 输出为准;写完用 `gitea-runner.exe config get <键>` 自证。
|
||||
|
||||
## 8. 门禁口径:讲规则,不讲数字
|
||||
|
||||
数字随分支和提交漂移(实测同一句"18 个钩子"在另一分支是 26,master 后来涨到 33;单测从 3 套件/288 断言涨到 10 套件/647)。**只有规则稳定**:
|
||||
|
||||
- L0:`pre-commit hooks -- passed=A failed=B skipped=C`,其中 **A+B+C = 定义数 − 1**;`skipped` 本机唯一常见项是 `mem-numbers`(无 `.map` 即跳过,并以 `::warning::` 明示"CI 不守体积门禁",见`docs/CI/远程CI完整指南.md` §0.0)。
|
||||
- L0b:`gitlint: N commit message(s) checked, 0 failed`。
|
||||
- L0.5:`<N> runner(s), A/A assertions passed, fail=0`,**N = `test/host/` 下 `run_unit_test.py` 的个数**。
|
||||
- **判定只看 `failed=0`、`fail=0`、A==A**;抄历史数字当基准必然误判。
|
||||
|
||||
## 9. 改 workflow / 门禁脚本的红线
|
||||
|
||||
`.gitea/workflows/ci.yml`:
|
||||
|
||||
- `shell: powershell`,**不是 `pwsh`**(Windows 自带只有 5.1,`pwsh` 会 `Cannot find: pwsh in PATH`)。
|
||||
- **`run:` 块内必须纯 ASCII**:runner 把每块写成无 BOM 临时 `.ps1` 交给 5.1 → 按 CP936 解码 → 中文导致"字符串缺少终止符"(报错行号正好在含中文那行,极易误判为逻辑错)。中文只写在 YAML 的 `name:`/注释里。
|
||||
- 写 `GITHUB_ENV` / `GITHUB_PATH` / `GITHUB_OUTPUT` 必须显式 `-Encoding ascii`(`Out-File -Append` 默认 UTF-16LE)。
|
||||
- 零依赖存档:`Tee-Object` 到 `ci-artifacts/*.log` + `$GITHUB_STEP_SUMMARY` + `::notice::`;**不用 `actions/upload-artifact`**(内网可能拉不到外层 action,`docs/CI/远程CI完整指南.md` §5.3)。
|
||||
- 工具链探测收在 `scripts/ci_find_python.ps1`(可离线自测 + 机器级安装兜底);`DTU_*` 最高优先级,且**显式设置就必须有效、不静默回退**。
|
||||
|
||||
`scripts/ci_*.ps1`:纯 ASCII(含中文的 `.ps1` 才需要 BOM,见`docs/CI/远程CI完整指南.md` §7.1);`-Encoding utf8NoBOM` 是 **PS 7 才有的**,5.1 要写文件用 `[System.IO.File]::WriteAllText($p, $s, (New-Object System.Text.UTF8Encoding($false)))`。
|
||||
|
||||
gitlint 专属:消息文件必须写在**仓库内**(跨盘符被 pre-commit 拒 `path is on mount 'C:', start on mount 'D:'`)且 **UTF-8 无 BOM**(BOM 占标题 1 字符,扭曲 100 上限)。
|
||||
|
||||
## 10. 改完必须验(顺序别省)
|
||||
|
||||
```powershell
|
||||
# .ps1:纯 ASCII + 语法
|
||||
$b=[System.IO.File]::ReadAllBytes($p); @($b|?{$_ -gt 127}).Count # 必须 0
|
||||
$e=$null; [System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path $p),[ref]$null,[ref]$e)|Out-Null; @($e).Count # 必须 0
|
||||
|
||||
# run: 块:必须按 runner 的语义测——点源(10-27 用 run 44 换来的两条教训)
|
||||
# runner 执行方式: powershell -command . '<块体>' ← 只有这个是真语义
|
||||
$tmp = '.gitea/_blk_probe.ps1' # 用 python 从 ci.yml 抽出某步 run 正文写到这里,别另抄一份
|
||||
& powershell.exe -NoProfile -Command "`$ErrorActionPreference='Stop'; . (Resolve-Path $tmp)"
|
||||
# 教训一:块被点源进会话,$ErrorActionPreference 继承为 Stop 时,原生程序往 stderr
|
||||
# 写一行就变成**终止性** NativeCommandError,`2>` 重定向挡不住(`2>` 加 `$null` 也不行)。
|
||||
# 所以块首显式 `$ErrorActionPreference='Continue'`,原生调用一律摊平成字符串。
|
||||
# 教训二:`exit 1` 的退出码语义随调用方式变——`-File` 直跑=1;`-File` 包一层点源=**0 且继续执行**;
|
||||
# `-Command` 点源=1。用 `-File` 测会把失败全测成 rc=0(我第一版就栽在这,四条失败路径全绿)。
|
||||
|
||||
# .yml:可解析 + 全部 run: 块纯 ASCII + 无 BOM + LF
|
||||
& '.venv\Scripts\python.exe' -c "import yaml;d=yaml.safe_load(open(r'.gitea/workflows/ci.yml',encoding='utf-8'));print(list(d))"
|
||||
|
||||
# 文档:链接与章节引用
|
||||
& '.venv\Scripts\python.exe' scripts/check_doc_links.py --all
|
||||
|
||||
# 全量门禁(等价 CI 的两个 job)
|
||||
& '.venv\Scripts\python.exe' scripts/ci_local.py --keep-going
|
||||
```
|
||||
|
||||
提交信息:标题 ≤100、**正文每行 ≤80**、UTF-8 无 BOM、`git commit -F <文件>`;**先程序化数一遍长度再提交**(中文标题最容易超,`-m` 还会被终端代码页转码)。
|
||||
|
||||
推送后到 runner 侧确认(§4.2):两个 job 各自的 `SUMMARY` 行 + `finished: success`。**"推送成功"不等于"CI 跑过"**。
|
||||
|
||||
**汇报口径**:说"红了几次 / 跑了多久"必须写明**来源**(库里的 `action_run_job` 时长,还是 runner 日志,还是页面)与**所用的 status 映射**;三者不一致时以库 + 日志为准并说清页面为什么不可信。只读排查就全程只读——不改文件、不 commit/push、不动 runner 注册状态。
|
||||
|
||||
## 11. 分支与发布(走通的做法)
|
||||
|
||||
- 预演冲突面,不动工作区:`git merge-tree --write-tree --name-only <a> <b>`(exit 1 = 有冲突)。
|
||||
- 数字口径类冲突的正确解法:**改成规则 + 分分支快照**,而不是选一边留数字——否则同一篇文档在另一分支上必错,下次合并再撞一次。
|
||||
- 变更记录是**历史,永不改写**,只在最上方按日期加行。
|
||||
- 想验证 CI 又不想动主干:复制一条分支推上去(`git branch <copy> <src>` → `git push -u origin <copy>`),跑完再删。注意**新建分支的首次 push 不触发 run**(§2 症状表第 1 行)。
|
||||
- 主干落后很多时先同步再排障:备份脏文件 → 只 stash 被跟踪的改动 → `checkout` → `merge --ff-only` → `stash pop`,同步完再跑 §1(本轮本地落后 248 个提交,先同步才看得出真实红因)。
|
||||
- 删分支前先算独有提交:`git rev-list --count origin/master..<branch>`,**> 0 就别删**(删了只能靠 SHA 找回);本地一律 `git branch -d`(未合并它会自己拒绝)。
|
||||
- `git commit` / `git push` 都是"先问"动作(AGENTS.md 边界节),不是任何任务的隐含步骤。
|
||||
@@ -14,7 +14,9 @@ whenToUse: 需要向目标板烧录或复位后,用串口日志自动判定运
|
||||
| 脚本 | 作用 |
|
||||
|---|---|
|
||||
| `.dsh/skills/serial-boot-test/scripts/run_boot_test.py` | 一键闭环:open+clear → 烧录 → wait-idle → 提取 boot log → 烧录回读校验 → 标记校验。串口助手按 `$DSH_SERIAL_TOOL` > 同目录 > `../serial-assistant/scripts/serial_tool.py` 解析 |
|
||||
| `.dsh/skills/serial-boot-test/scripts/verify_log.py` | 对任意日志文件做标记校验(Required/Forbidden/锚点计数/顺序/耗时),可单独用 |
|
||||
| `.dsh/skills/serial-boot-test/scripts/verify_log.py` | 对任意日志文件做标记校验(Required/Forbidden/锚点计数/顺序/耗时),可单独用。`--exact` 做**整行 `^…$` 锚定**(子串口径照不出"行还在但行首被吃掉/行内被插字节"那一类损坏)、`--pair` 做跨复位配对、`--allow-residual/--head-char` 服务崩溃注入腿、`--selftest` 跑内置判别用例(**证明这套判据不是空跑**)。四个行级开关(`--exact` / `--pair` / `--allow-residual` / `--head-char`)同时接进了 `run_boot_test.py`,@test 闭环直接可用 |
|
||||
|
||||
> **判据取向(重要,会复发)**:`--exact` 默认**只锚文本、不钉长度**——字节长度属于某一份构建,钉死它会在无辜改动上假红(`@@LEN=<n>` 是显式逃生口,不是默认)。子串 `--required` 不能用于"可能丢行首/被插字节"的行。来龙去脉与固定动作见本仓 `docs/测试说明/启动日志原子性复验流程.md`。
|
||||
|
||||
交互式串口操作(open/send/log/wait-idle/status 等)不属于本 skill——
|
||||
**用 `serial-assistant` skill**。
|
||||
@@ -47,7 +49,9 @@ whenToUse: 需要向目标板烧录或复位后,用串口日志自动判定运
|
||||
| `done_marker` + `max_boot_ms` | 开机耗时口径与上限 | 空/0 = 只报告不判 |
|
||||
| `settle_ms` / `max_wait_ms` / `first_wait_ms` | RX 静默判据 / 总超时 / 首字节超时 | 8000 / 30000 / 12000 |
|
||||
| `require_order` / `expect_reset_cause` / `require_suite_summary` | 顺序校验 / 复位原因白名单 / 套件汇总行 | 空 = 关闭 |
|
||||
| `require_selftest_pass` + `device_ip` | 设备侧自检台账联合判定(FTP 抓取) | 关闭 |
|
||||
| `selftest_scope` | 自检台账判定口径:`this-run`(烧录前取基线,只判新增)/ `all`(累计,任一记录即 FAIL) | `this-run` |
|
||||
| `ftp_user` / `ftp_pass` | 抓 `/log/selftest.log` 用的 FTP 账号(设备无匿名通道) | `admin` / `admin` |
|
||||
| `require_selftest_pass` + `device_ip` | 设备侧自检台账联合判定(FTP 抓取;**默认只判本次启动新增**,抓取失败/无数据源一律 FAIL) | 关闭 |
|
||||
|
||||
**先跑项目的构建**(本仓库 = `keil-build` skill,其它 Keil 项目照该 skill 套;
|
||||
非 Keil 项目用项目自己的构建入口),确认 0 错误 0 警告后再进闭环。
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
|
||||
"require_selftest_pass": false,
|
||||
"device_ip": "",
|
||||
"selftest_scope": "this-run",
|
||||
"ftp_user": "admin",
|
||||
"ftp_pass": "admin",
|
||||
|
||||
"httpPort": 18765
|
||||
}
|
||||
|
||||
@@ -19,9 +19,12 @@
|
||||
5. verify_log.py 标记校验(含启动锚点计数 config "boot_marker" + --max-boot-count;
|
||||
--expect-reset-cause 原因白名单;--require-order 标记顺序;
|
||||
--done-marker + --max-boot-ms 开机耗时)
|
||||
6. (仅当 --require-selftest-pass)自检记录联合判定(S7):
|
||||
--selftest-log 提供本地副本则直接用它,否则从设备 FTP 抓取 /log/selftest.log
|
||||
(需 config "device_ip");判定:文件缺失 / 0 字节 = 无故障 → PASS
|
||||
6. (仅当 --require-selftest-pass)自检台账联合判定(S7):默认**只判本次启动新增**
|
||||
——烧录前先经 FTP 取 /log/selftest.log 基线,启动后再取一次,只把"基线之后新增"的
|
||||
记录交给判定(该文件是 512 条环形累计历史,"非空即 FAIL"判一次启动永远不可能过)。
|
||||
设备侧无此文件 = 台账干净 = PASS;**抓取失败 / 没有数据源 / 基线被环覆盖 → FAIL**,
|
||||
不再"视为无记录"放过。累计口径用 --selftest-scope all;--selftest-log 走本地副本
|
||||
(无基线可比,自动降级为 all)。FTP 凭据 --ftp-user/--ftp-pass(默认出厂 admin)。
|
||||
|
||||
采集在烧录**之前**武装(open + clear 先于烧录),daemon 常驻全量日志兜底,
|
||||
无采集窗口/首字节等待仪式。
|
||||
@@ -46,7 +49,7 @@
|
||||
[--flash-cmd "<烧录命令>"] [--flash-timeout 180]
|
||||
[--required "m1;m2"] [--forbidden "FAIL;ERROR;HardFault"]
|
||||
[--require-suite-summary "Storage Tests"]
|
||||
[--require-selftest-pass] [--selftest-log <本地副本>]
|
||||
[--require-selftest-pass [--selftest-scope this-run|all]] [--selftest-log <副本>]
|
||||
[--device-ip 192.168.1.100] [--out-dir <目录>] [--show-tail-lines 15]
|
||||
[--fw-hex <构建产物 hex>] [--no-fw-check]
|
||||
[--no-verify-flash] [--readback-timeout 120]
|
||||
@@ -58,7 +61,7 @@
|
||||
|
||||
退出码(与旧版一致):
|
||||
0 = PASS(采集成功 + Required 全中 + Forbidden 零命中)
|
||||
1 = FAIL(标记缺失/命中)
|
||||
1 = FAIL(标记缺失/命中;或要求自检台账判定却判不了——无数据源/抓取失败/无法证明无新增)
|
||||
2 = 采集或烧录错误(含烧录回读不一致 = 芯片内容与刚编的镜像不同,本次运行作废)
|
||||
3 = 用法/预检错误
|
||||
"""
|
||||
@@ -240,11 +243,53 @@ def _prune_out_dir(out_dir, keep):
|
||||
return removed
|
||||
|
||||
|
||||
def _fetch_selftest_log(device_ip, out_dir, stamp):
|
||||
"""从设备 FTP 抓取 /log/selftest.log(lftpd :21 匿名,根目录 = 卷根)。
|
||||
def _selftest_records(path):
|
||||
"""把 selftest.log 的本地副本切成 64 B 定长记录(尾部残块不算记录)。
|
||||
|
||||
成功返回本地路径字符串;失败(网络不可达 / FTP 错误 / 设备侧文件缺失)返回 ""
|
||||
(按 S7 语义视为无故障记录;此处打 WARN,调用方不判整体 FAIL)。
|
||||
返回 [] = 台账为空(干净设备);返回 None = **未知**(文件不可读等)。
|
||||
两者绝不能混——把"看不见"当成"没有"正是假绿的成因。
|
||||
"""
|
||||
if not path:
|
||||
return []
|
||||
try:
|
||||
raw = Path(path).read_bytes()
|
||||
except OSError as exc:
|
||||
print("RUN_TEST_WARN: 自检台账本地副本不可读: {}".format(exc))
|
||||
return None
|
||||
n = len(raw) - (len(raw) % 64)
|
||||
return [raw[i:i + 64] for i in range(0, n, 64)]
|
||||
|
||||
|
||||
def _selftest_new_records(baseline, after):
|
||||
"""基线对比 → 本次运行新增的记录,返回 (新增列表, 能否证明)。
|
||||
|
||||
· baseline is None → 基线未知 → 无法证明
|
||||
· baseline == [] → 设备原本无台账 → after 全部算本次新增
|
||||
· 基线最后一条在 after 里找得到 → 它之后的就是新增
|
||||
· 找不到 → 512 条环已覆盖基线(更新量 ≥ 环容)→ 无法证明;
|
||||
宁可 FAIL,也不拿"看不到"当"没有"
|
||||
"""
|
||||
if baseline is None or after is None:
|
||||
return [], False
|
||||
if not baseline:
|
||||
return list(after), True
|
||||
last = baseline[-1]
|
||||
for idx in range(len(after) - 1, -1, -1):
|
||||
if after[idx] == last:
|
||||
return list(after[idx + 1:]), True
|
||||
return [], False
|
||||
|
||||
def _fetch_selftest_log(device_ip, out_dir, stamp, ftp_user="admin", ftp_pass="admin"):
|
||||
"""从设备 FTP 抓取 /log/selftest.log(lftpd :21 需鉴权,根目录 = 卷根)。
|
||||
|
||||
返回 (本地路径, 状态),状态 ∈ {"ok", "absent", "error"}:
|
||||
ok = 抓到文件(路径有效)
|
||||
absent = 设备侧确实没有这个文件 = 台账干净(正常路径,不是故障)
|
||||
error = 连不上 / 登录被拒 / FTP 故障 → 结果**未知**,调用方必须判 FAIL
|
||||
|
||||
⚠ 2026-10-22 关键更正:旧实现把 absent 与 error 都返回 "",调用方一律"视为无
|
||||
记录 → PASS"。于是抓取失败会被读成"设备很干净"——这就是那条假绿。三种结果
|
||||
必须分开。
|
||||
|
||||
2026-10-02 台架修复:原实现只 `except OSError`,而 ftplib 的 error_temp /
|
||||
error_perm 继承自 Exception 而非 OSError —— 台账"仅故障才写",干净设备上
|
||||
@@ -256,26 +301,28 @@ def _fetch_selftest_log(device_ip, out_dir, stamp):
|
||||
ftp = ftplib.FTP()
|
||||
try:
|
||||
ftp.connect(device_ip, 21, timeout=10)
|
||||
ftp.login() # 匿名(lftpd 接受 USER anonymous / 空 PASS)
|
||||
# lftpd 按 /config.ini 的 [ftp] 校验账号(user / pwd_hash),出厂 admin/admin;
|
||||
# 设备没有匿名通道——匿名登录一律 530,所以这里必须带凭据。
|
||||
ftp.login(ftp_user, ftp_pass)
|
||||
except (OSError, ftplib.Error) as e:
|
||||
print("RUN_TEST_WARN: selftest fetch failed: {} "
|
||||
"(按 S7 语义视为无记录;设备网络/FTP 需另行排查)".format(e))
|
||||
return ""
|
||||
print("SELFTEST_FETCH: {} -> ERROR ({}) 连不上/登录被拒/FTP 故障 → 状态未知"
|
||||
.format(device_ip, e))
|
||||
return "", "error"
|
||||
|
||||
buf = bytearray()
|
||||
try:
|
||||
ftp.retrbinary("RETR /log/selftest.log", buf.extend)
|
||||
except ftplib.all_errors as e:
|
||||
# 450/550 = 设备侧无此文件:干净台账,S7 判 PASS(正常路径,非故障)
|
||||
print("SELFTEST_FETCH: {} -> (ledger absent: {}) 按 S7 视为无故障记录"
|
||||
print("SELFTEST_FETCH: {} -> (ledger absent: {}) 台账干净"
|
||||
.format(device_ip, str(e).strip()))
|
||||
_ftp_quit(ftp)
|
||||
return ""
|
||||
return "", "absent"
|
||||
|
||||
_ftp_quit(ftp)
|
||||
local.write_bytes(bytes(buf))
|
||||
print("SELFTEST_FETCH: {} -> {} ({} bytes)".format(device_ip, local, len(buf)))
|
||||
return str(local)
|
||||
return str(local), "ok"
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
@@ -308,14 +355,23 @@ def main(argv=None):
|
||||
help="forbidden patterns, ';' separated (config 'forbidden')")
|
||||
ap.add_argument("--require-suite-summary", default=None,
|
||||
help="suite names that MUST appear as '=== <name>: N/M PASSED (failed=0) ===' "
|
||||
"with N>0, ';' separated (T-2). Empty = off")
|
||||
"with N>0, ';' separated. Empty = off")
|
||||
ap.add_argument("--require-selftest-pass", action="store_true", default=None,
|
||||
help="jointly judge /log/selftest.log (S7): missing/empty = PASS, "
|
||||
"any 64B record = FAIL")
|
||||
ap.add_argument("--selftest-log", default=None,
|
||||
help="local copy of /log/selftest.log (skips device-side fetch when given)")
|
||||
ap.add_argument("--selftest-scope", choices=("this-run", "all"), default=None,
|
||||
help="this-run (default): only records added by THIS run count as "
|
||||
"faults -- needs a pre-flash baseline over FTP, so device_ip is "
|
||||
"required. all: legacy cumulative rule (any record = FAIL); use it "
|
||||
"for audits or a first baseline. Unproveable cases FAIL, never PASS.")
|
||||
ap.add_argument("--device-ip", default=None,
|
||||
help="device IP for fetching /log/selftest.log via FTP (anonymous)")
|
||||
help="device IP for fetching /log/selftest.log via FTP")
|
||||
ap.add_argument("--ftp-user", default=None,
|
||||
help="FTP user for the selftest fetch (config 'ftp_user'; default admin)")
|
||||
ap.add_argument("--ftp-pass", default=None,
|
||||
help="FTP password for the selftest fetch (config 'ftp_pass'; default admin)")
|
||||
ap.add_argument("--out-dir", default=None)
|
||||
ap.add_argument("--show-tail-lines", type=int, default=None)
|
||||
ap.add_argument("--fw-hex", default=None,
|
||||
@@ -336,6 +392,23 @@ def main(argv=None):
|
||||
ap.add_argument("--require-order", default=None,
|
||||
help="'>'-separated markers that must appear in that line order "
|
||||
"(catches out-of-order boot steps); empty = off")
|
||||
ap.add_argument("--exact", action="append", default=[],
|
||||
help="repeatable regex that must match a WHOLE firmware line "
|
||||
"(forwarded to verify_log.py --exact). Use it instead of --required "
|
||||
"for any marker whose line could lose its head or gain spliced "
|
||||
"bytes -- substring matching cannot see that class of damage. "
|
||||
"Suffix '@@LEN=<n>' pins byte length: opt-in only, lengths belong "
|
||||
"to one image")
|
||||
ap.add_argument("--pair", action="append", nargs=2, metavar=("FIRST", "THEN"), default=[],
|
||||
help="repeatable cross-reset pairing (exactly one FIRST, then exactly "
|
||||
"one THEN after it), e.g. --pair MEMFAULT 'reset cause:'")
|
||||
ap.add_argument("--allow-residual", type=int, default=0,
|
||||
help="accept up to N stray bytes before the line head on a --exact hit "
|
||||
"(crash-dump legs: 1 leftover from a reset mid-byte is known; "
|
||||
"recorded, not a failure)")
|
||||
ap.add_argument("--head-char", default="",
|
||||
help="every --exact hit must start with this char after the optional "
|
||||
"residual prefix (crash-dump legs: '[')")
|
||||
ap.add_argument("--done-marker", default=None,
|
||||
help="marker whose line marks boot completion (for --max-boot-ms); "
|
||||
"empty = don't measure")
|
||||
@@ -384,10 +457,17 @@ def main(argv=None):
|
||||
args.forbidden = str(_pick(args.forbidden, "forbidden", default=FORBIDDEN_DEFAULT))
|
||||
args.require_suite_summary = str(_pick(args.require_suite_summary,
|
||||
"require_suite_summary", default="") or "")
|
||||
args.selftest_scope = str(_pick(args.selftest_scope, "selftest_scope",
|
||||
default="this-run") or "this-run")
|
||||
if args.selftest_scope not in ("this-run", "all"):
|
||||
print("RUN_TEST_USAGE_ERROR: selftest_scope must be this-run or all")
|
||||
return 3
|
||||
args.require_selftest_pass = bool(_pick(args.require_selftest_pass,
|
||||
"require_selftest_pass", default=False))
|
||||
args.selftest_log = str(_pick(args.selftest_log, "selftest_log", default="") or "")
|
||||
args.device_ip = str(_pick(args.device_ip, "device_ip", default="") or "")
|
||||
args.ftp_user = str(_pick(args.ftp_user, "ftp_user", default="admin") or "admin")
|
||||
args.ftp_pass = str(_pick(args.ftp_pass, "ftp_pass", default="admin") or "admin")
|
||||
args.out_dir = str(_pick(args.out_dir, "out_dir",
|
||||
default=str(Path(tempfile.gettempdir()) / "serial-boot-test")))
|
||||
args.show_tail_lines = int(_pick(args.show_tail_lines, "show_tail_lines", default=15))
|
||||
@@ -446,6 +526,34 @@ def main(argv=None):
|
||||
print("Port: {} @ {} settle={}ms max={}ms".format(
|
||||
port, args.baud_rate, args.settle_ms, args.max_wait_ms))
|
||||
|
||||
# 0.6 自检台账基线(S7「只看本次新增」):**必须排在串口采集武装之前**。
|
||||
# /log/selftest.log 是 512 条环形累计历史,用"文件非空即 FAIL"去判一次启动,在任何
|
||||
# 做过故障注入或长跑的设备上永远不可能 PASS(本仓台架实测:抓到的 29 条记录时间戳
|
||||
# 全是 2026-10-10 ~ 10-14 留下的,与本次启动无关)。这里记下基线的最后一条记录,
|
||||
# 启动后只判"它之后新增了几条";抓不到基线就判不了本次 → 退出码 2,不降级成"视为
|
||||
# 无记录"。
|
||||
# ⚠ 为什么排在 open/clear 之前(2026-10-22 实测踩到):抓基线是一次真实 FTP 会话,
|
||||
# 设备会把 `USER/PASS/RETR` 打到串口上,客户端退出时还会多打一行 `[ERR] [FTP] error
|
||||
# reading next command`——放在采集武装之后,这些行就落进本次启动日志:既让 Forbidden
|
||||
# `[ERR]` 命中(本次判 FAIL),又把开机耗时从 ~0.6 s 拉到 ~8 s(假指标)。
|
||||
st_baseline = None
|
||||
if args.require_selftest_pass and args.selftest_scope == "this-run":
|
||||
if args.selftest_log:
|
||||
print("SELFTEST_BASELINE: 用的是本地副本,无基线可比 → 本次按累计口径判定")
|
||||
args.selftest_scope = "all"
|
||||
elif not args.device_ip:
|
||||
print("SELFTEST_BASELINE: 没有 device_ip 就取不到基线 → 本次按累计口径判定")
|
||||
args.selftest_scope = "all"
|
||||
else:
|
||||
base_local, base_status = _fetch_selftest_log(
|
||||
args.device_ip, out_dir, stamp + "_base", args.ftp_user, args.ftp_pass)
|
||||
if base_status == "error":
|
||||
print("RUN_TEST_ERROR: 自检台账基线抓取失败 → 无法证明本次无新增故障"
|
||||
"(不把抓取失败当成设备干净)")
|
||||
return 2
|
||||
st_baseline = _selftest_records(base_local)
|
||||
print("SELFTEST_BASELINE: 既有记录 {} 条(status={})"
|
||||
.format(len(st_baseline or []), base_status))
|
||||
# 1. open(幂等;未运行则自动拉起常驻 daemon)
|
||||
data, out = tool("open", "--port", port, "--baud", str(args.baud_rate), "--timeout", "15")
|
||||
if data is None:
|
||||
@@ -590,13 +698,50 @@ def main(argv=None):
|
||||
print("RUN_TEST_ERROR: flash readback mismatch (chip content != built image)")
|
||||
return 2
|
||||
|
||||
# 5.5 自检记录抓取(S7):启用且未提供本地副本时才走 FTP(需 config "device_ip")
|
||||
# 5.5 自检记录判定(S7):默认只判「本次启动新增」,累计口径要显式指定。
|
||||
# 三种抓取结果必须分开对待(旧实现把后两种都当"无记录 → PASS",就是假绿):
|
||||
# absent(设备侧无此文件) → 台账干净 → 无新增 → PASS
|
||||
# error(连不上/登录被拒/故障)→ 未知 → FAIL(要求判定却没数据,不能当通过)
|
||||
# 没有数据源(无 device_ip 又无 --selftest-log)→ FAIL(同上)
|
||||
selftest_local = args.selftest_log
|
||||
if args.require_selftest_pass and not selftest_local and not args.device_ip:
|
||||
print("RUN_TEST_WARN: require_selftest_pass but no device_ip/selftest_log "
|
||||
"-> treated as no record (S7 PASS path)")
|
||||
if args.require_selftest_pass and not selftest_local and args.device_ip:
|
||||
selftest_local = _fetch_selftest_log(args.device_ip, out_dir, stamp)
|
||||
selftest_forced_fail = False
|
||||
if args.require_selftest_pass:
|
||||
if selftest_local:
|
||||
pass # 本地副本:按它自己的口径判(基线已在 2.5 降级)
|
||||
elif not args.device_ip:
|
||||
print("RUN_TEST_FAIL: 要求自检台账判定,但既没有 device_ip 也没有 --selftest-log"
|
||||
" → 无数据源,判 FAIL(旧行为是 WARN 后当无记录放过)")
|
||||
selftest_forced_fail = True
|
||||
else:
|
||||
after_local, after_status = _fetch_selftest_log(
|
||||
args.device_ip, out_dir, stamp, args.ftp_user, args.ftp_pass)
|
||||
if after_status == "error":
|
||||
print("RUN_TEST_FAIL: 自检台账抓取失败(设备不可达 / FTP 故障)→ 判 FAIL"
|
||||
"(不把抓取失败当成设备干净)")
|
||||
selftest_forced_fail = True
|
||||
elif after_status == "absent":
|
||||
selftest_local = None # 空台账 = 确无故障
|
||||
elif args.selftest_scope == "this-run":
|
||||
new_recs, provable = _selftest_new_records(
|
||||
st_baseline, _selftest_records(after_local))
|
||||
if not provable:
|
||||
print("SELFTEST_JUDGE: FAIL —— 无法证明本次无新增"
|
||||
"(基线最后一条已被 512 条环覆盖,或基线/台账不可读)")
|
||||
selftest_forced_fail = True
|
||||
elif not new_recs:
|
||||
print("SELFTEST_JUDGE: 本次启动无新增记录(基线 {} 条 → 台账不变)"
|
||||
.format(len(st_baseline or [])))
|
||||
selftest_local = None
|
||||
else:
|
||||
# 文件名必须落在 selftest_log_*.log 里,才会被 --keep-out 的保留规则收走
|
||||
delta = out_dir / "selftest_log_{}_new.log".format(stamp)
|
||||
delta.write_bytes(b"".join(new_recs))
|
||||
print("SELFTEST_JUDGE: 本次启动新增 {} 条 → 只把新增部分交给判定"
|
||||
.format(len(new_recs)))
|
||||
selftest_local = str(delta)
|
||||
else:
|
||||
selftest_local = after_local # --selftest-scope all:累计口径
|
||||
|
||||
|
||||
# 6. 标记校验(含固件身份戳、启动锚点计数、可选的自检记录联合判定)
|
||||
# 启动锚点(config "boot_marker",如本仓库的 'reset cause:')未配置 =
|
||||
@@ -615,7 +760,14 @@ def main(argv=None):
|
||||
expect_reset_cause=args.expect_reset_cause,
|
||||
require_order=args.require_order,
|
||||
done_marker=args.done_marker,
|
||||
max_boot_ms=args.max_boot_ms)
|
||||
max_boot_ms=args.max_boot_ms,
|
||||
exact=args.exact, pairs=args.pair,
|
||||
allow_residual=args.allow_residual, head_char=args.head_char)
|
||||
if selftest_forced_fail and rc == 0:
|
||||
# 标记校验全过,但自检台账判定根本没能执行(无数据源 / 抓取失败 / 无法证明无新增)
|
||||
# —— 这类"判不了"必须以 FAIL 收口,否则就又回到假绿。
|
||||
print("RUN_TEST_VERDICT: FAIL —— 自检台账判定未能执行(见上方 SELFTEST 行)")
|
||||
rc = 1
|
||||
print("RUN_TEST_RC={}".format(rc))
|
||||
return rc
|
||||
|
||||
|
||||
@@ -37,15 +37,19 @@
|
||||
- 读取编码:先按 UTF-8,解码失败回退系统 locale 编码(旧版 Get-Content 行为)
|
||||
- 控制台输出重配置为 UTF-8(errors=replace):任何 Windows 控制台代码页
|
||||
都只影响终端显示,不会导致本脚本崩溃
|
||||
- 汇总行(2026-10-01 加,T-2):纯 Required 子串匹配有个盲区——套件启动横幅
|
||||
- 汇总行(2026-10-01 加):纯 Required 子串匹配有个盲区——套件启动横幅
|
||||
命中即判 PASS,**套件是否跑到汇总行、断言数是否为 0 完全不可见**(套件中途
|
||||
挂死/复位、或 Phase 宏全关时会被误判为通过)。`--require-suite-summary`
|
||||
要求日志中出现 `=== <名称>: N/M PASSED (failed=0) ===` 且 **N > 0**。
|
||||
- 自检记录联合判定(S7,2026-10 实机窗口):`--require-selftest-pass` 对 /log/selftest.log
|
||||
的本地副本(定长 64B 记录,格式见 App/log/sys_log.h)判定。自检仅故障写盘(v1 不记
|
||||
通过项):**文件缺失 / 0 字节 = 无故障 → PASS**;存在任何一条记录 = 有故障 → FAIL
|
||||
(退出码 1),并逐条打印时间戳 + 描述。抓取失败(设备不可达 / FTP 错误等)由调用方
|
||||
(run_boot_test.py)先打 WARN,本地文件按缺失处理 → 按上述规则不判整体 FAIL。
|
||||
- 自检记录联合判定(S7,2026-10 实机窗口;判据 2026-10-22 改为"只看本次新增"):
|
||||
`--require-selftest-pass` 对 /log/selftest.log 的本地副本(定长 64B 记录,格式见
|
||||
App/log/sys_log.h)判定。自检仅故障写盘(v1 不记通过项):**文件缺失 / 0 字节 = 无故障
|
||||
→ PASS**;存在记录 = FAIL(退出码 1),逐条打印时间戳 + 描述。
|
||||
⚠ 该文件是 512 条环形**累计历史**,所以本函数收到的"有记录"必须由调用方先做差集:
|
||||
run_boot_test.py 在烧录前取基线、启动后只把"基线之后新增"的记录写成临时副本交给本
|
||||
函数(`--selftest-scope this-run`,默认)。**抓取失败/无数据源/无法证明无新增一律由
|
||||
调用方判 FAIL**——旧实现把这些都当"无记录 → PASS",是假绿(详见
|
||||
docs/测试说明/自动化测试流程.md 的自检结果判定节)。
|
||||
- 启动锚点计数(2026-10-10):`--max-boot-count N`(run_boot_test.py 默认传 1)统计
|
||||
`reset cause:` 锚点出现次数——**0 次** = 采集没从复位开始(标记全中也可能只是上一轮
|
||||
的残留日志);**>N 次** = 板子在采集窗口内自己复位过(IWDG 咬狗 / Fault 后
|
||||
@@ -110,6 +114,106 @@ def _read_lines(path):
|
||||
return raw.decode("utf-8", errors="replace").splitlines()
|
||||
|
||||
|
||||
# --- whole-line exact matching (2026-10-26, task-36; absorbed from bench-runner scratch) ---
|
||||
# Why this exists (measured, not theoretical): -Required is a SUBSTRING check, and the
|
||||
# console-interleave defect kept the substring while destroying the line, e.g. the
|
||||
# headless victim line "26-10-26 08:37:07][NAND_FTL] Initializing NAND FTL..." still
|
||||
# contains the marker "Initializing NAND FTL" -> the old check called that boot a PASS.
|
||||
# Any marker whose line can lose its head is therefore NOT verifiable by substring alone.
|
||||
# --exact anchors the WHOLE payload (^ ... $) so one spliced byte breaks the match, and it
|
||||
# also reports lines containing the pattern's longest literal run that do NOT match fully:
|
||||
# that is the splicing signature and it must never stay silent. Length pinning is OPT-IN
|
||||
# ("@@LEN=<n>"): byte lengths belong to one image, pinning them by default gives a false
|
||||
# red on innocent commits (measured uiTask stack swing 4116..4188 on ONE image).
|
||||
EXACT_LEN_SEP = "@@LEN="
|
||||
_DAEMON_RX = re.compile(r"^\[RX\] \S+ (.*)$")
|
||||
_RUNBT_RX = re.compile(r"^\[\+\d+ms\] (.*)$")
|
||||
|
||||
|
||||
def _parse_exact(spec):
|
||||
"""'REGEX@@LEN=42' -> (regex, 42); no suffix -> (regex, None)."""
|
||||
if EXACT_LEN_SEP in spec:
|
||||
pat, _, ln = spec.rpartition(EXACT_LEN_SEP)
|
||||
return pat, int(ln.strip())
|
||||
return spec, None
|
||||
|
||||
|
||||
def _payload_of(line):
|
||||
"""Drop the collector prefix so ^...$ applies to the firmware line itself.
|
||||
|
||||
Handles serial-assistant daemon lines ('[RX] <iso> ...'), run_boot_test boot logs
|
||||
('[+Nms] ...') and bare payload lines. Archived evidence files may carry a leading
|
||||
'#' provenance header; those are dropped unless include_comments is set (otherwise a
|
||||
header that merely MENTIONS [ERR] would trip Forbidden / pollute exact counts).
|
||||
"""
|
||||
for rx in (_DAEMON_RX, _RUNBT_RX):
|
||||
m = rx.match(line)
|
||||
if m:
|
||||
return m.group(1)
|
||||
return line
|
||||
|
||||
|
||||
def _longest_literal(pattern):
|
||||
"""Longest run of non-metacharacters in a regex -> a greppable neighbour probe."""
|
||||
parts = re.split(r"\\.|\\\[|\]|\(|\)|\{|\}|\||\*|\+|\?|\^|\$|\.|\[:", pattern)
|
||||
parts = [x for x in parts if x and not x.startswith("\\")]
|
||||
return max(parts, key=len) if parts else ""
|
||||
|
||||
|
||||
def exact_check(payloads, spec, allow_residual=0, head_char=""):
|
||||
"""(ok, detail, neighbours, residuals) for one --exact spec. See block note above."""
|
||||
pat, want_len = _parse_exact(spec)
|
||||
rx = re.compile("^" + pat + "$")
|
||||
hits, residuals, consumed = [], [], set()
|
||||
for idx, pl in enumerate(payloads):
|
||||
if rx.match(pl):
|
||||
hits.append(pl)
|
||||
consumed.add(idx)
|
||||
continue
|
||||
if allow_residual > 0:
|
||||
for k in range(1, allow_residual + 1):
|
||||
if len(pl) > k and pl[k] == "[" and rx.match(pl[k:]):
|
||||
hits.append(pl[k:])
|
||||
residuals.append((k, pl[:k]))
|
||||
consumed.add(idx)
|
||||
break
|
||||
key = _longest_literal(pat)
|
||||
# a line counted (directly or after residual realignment) is never also a neighbour
|
||||
neigh = [pl for i2, pl in enumerate(payloads) if key and key in pl and i2 not in consumed]
|
||||
detail = "hits={} lens={} loose_neighbours={}".format(
|
||||
len(hits), [len(h) for h in hits], len(neigh))
|
||||
if residuals:
|
||||
detail += " residual_head_bytes={}".format([(k, repr(v)) for k, v in residuals])
|
||||
ok = len(hits) >= 1 and not neigh
|
||||
if want_len is not None:
|
||||
ok = ok and all(len(h) == want_len for h in hits)
|
||||
detail += " want_len={}".format(want_len)
|
||||
if head_char:
|
||||
bad = [h for h in hits if not h.startswith(head_char)]
|
||||
ok = ok and not bad
|
||||
detail += " head_char={!r} bad={}".format(head_char, len(bad))
|
||||
return ok, detail, neigh[:3], residuals[:3]
|
||||
|
||||
|
||||
def pair_check(payloads, first_pat, then_pat):
|
||||
"""Exactly one FIRST line with exactly one THEN line after it (cross-reset pairing).
|
||||
|
||||
Crash-dump legs need this: the fault line must pair with the NEXT boot's reset cause,
|
||||
and a second following cause would mean an unlogged or repeated reset.
|
||||
"""
|
||||
frx, trx = re.compile(first_pat), re.compile(then_pat)
|
||||
fi = [i for i, pl in enumerate(payloads) if frx.search(pl)]
|
||||
ti = [i for i, pl in enumerate(payloads) if trx.search(pl)]
|
||||
if len(fi) != 1:
|
||||
return False, "first matches={} (want exactly 1), then matches={}".format(len(fi), len(ti))
|
||||
after = [i for i in ti if i > fi[0]]
|
||||
if len(after) != 1:
|
||||
return False, "first at L{} but following THEN count={} at {} (want exactly 1)".format(
|
||||
fi[0] + 1, len(after), [x + 1 for x in after])
|
||||
return True, "first L{} -> only following THEN at L{} (gap {} line(s))".format(
|
||||
fi[0] + 1, after[0] + 1, after[0] - fi[0])
|
||||
|
||||
|
||||
def _reset_cause_of(line, marker):
|
||||
"""从 `reset cause: software (NVIC_SystemReset) (CSR=0x14000000)` 取原因文本。"""
|
||||
i = line.find(marker)
|
||||
@@ -125,7 +229,8 @@ def verify(log_path, required=REQUIRED_DEFAULT, forbidden=FORBIDDEN_DEFAULT,
|
||||
selftest_pass=False, selftest_log="",
|
||||
boot_marker=BOOT_MARKER_DEFAULT, max_boot_count=0,
|
||||
expect_reset_cause="", require_order="", done_marker="",
|
||||
max_boot_ms=0, as_json=False):
|
||||
max_boot_ms=0, as_json=False, exact=None, pairs=None,
|
||||
allow_residual=0, head_char="", include_comments=False):
|
||||
"""标记校验核心(可被 run_boot_test.py 复用)。返回退出码 0/1/2。
|
||||
|
||||
suite_summary: ';' 分隔的套件名列表。非空时,要求日志中每个套件都出现
|
||||
@@ -147,7 +252,8 @@ def verify(log_path, required=REQUIRED_DEFAULT, forbidden=FORBIDDEN_DEFAULT,
|
||||
"""
|
||||
res = {"log_path": str(log_path), "lines": 0, "bytes": 0, "required_missing": [],
|
||||
"forbidden_hits": [], "boot": None, "suites_bad": [], "selftest": None,
|
||||
"order_bad": [], "timing": None, "tail": [], "verdict": None, "exit": None}
|
||||
"order_bad": [], "timing": None, "tail": [], "verdict": None, "exit": None,
|
||||
"exact_bad": [], "exact_ok": [], "pair_bad": [], "pair_ok": []}
|
||||
emit = out
|
||||
|
||||
def finish(code):
|
||||
@@ -169,6 +275,10 @@ def verify(log_path, required=REQUIRED_DEFAULT, forbidden=FORBIDDEN_DEFAULT,
|
||||
except OSError:
|
||||
out("VERIFY_ERROR: cannot read log file: {}".format(log_path))
|
||||
return finish(2)
|
||||
if not include_comments:
|
||||
# archived evidence files carry an ASCII provenance header; a header that merely
|
||||
# MENTIONS [ERR] must not trip Forbidden (tool-level false-red guard, task-36)
|
||||
lines = [l for l in lines if not l.lstrip().startswith("#")]
|
||||
|
||||
full = "\n".join(lines)
|
||||
req_list = _split_list(required)
|
||||
@@ -209,6 +319,37 @@ def verify(log_path, required=REQUIRED_DEFAULT, forbidden=FORBIDDEN_DEFAULT,
|
||||
else:
|
||||
out("[OK] forbidden pattern absent: {}".format(f))
|
||||
|
||||
# ---- whole-line exact checks + cross-reset pairing (task-36) ----
|
||||
payloads = [_payload_of(l) for l in lines]
|
||||
res["payload_lines"] = len(payloads)
|
||||
exact_bad = []
|
||||
for spec in (exact or []):
|
||||
pat, _want = _parse_exact(spec)
|
||||
ok_x, detail, neigh, residuals = exact_check(payloads, spec, allow_residual, head_char)
|
||||
if ok_x:
|
||||
out("[OK] exact line: {} {}".format(pat[:64], detail))
|
||||
res["exact_ok"].append({"pattern": pat, "detail": detail})
|
||||
else:
|
||||
out("[FAIL] exact line: {} {}".format(pat[:64], detail))
|
||||
for nb in neigh:
|
||||
out(" inexact neighbour: {}".format(nb.strip()[:170]))
|
||||
exact_bad.append(pat)
|
||||
res["exact_bad"].append({"pattern": pat, "detail": detail,
|
||||
"neighbours": [n.strip()[:170] for n in neigh]})
|
||||
for k, v in residuals:
|
||||
out("[NOTE] {} residual byte(s) before the line head, realigned and NOT a "
|
||||
"failure: {}".format(k, repr(v)))
|
||||
pair_bad = []
|
||||
for f_pat, t_pat in (pairs or []):
|
||||
ok_p, detail = pair_check(payloads, f_pat, t_pat)
|
||||
if ok_p:
|
||||
out("[OK] pair: {} -> {}".format(f_pat[:44], t_pat[:44]))
|
||||
res["pair_ok"].append({"first": f_pat, "then": t_pat, "detail": detail})
|
||||
else:
|
||||
out("[FAIL] pair: {} -> {} {}".format(f_pat[:44], t_pat[:44], detail))
|
||||
pair_bad.append({"first": f_pat, "then": t_pat, "detail": detail})
|
||||
res["pair_bad"].append(detail)
|
||||
|
||||
# 启动锚点计数(2026-10-10):抓"跑完但中途复位过"和"采集没从复位开始"
|
||||
boot_issue = False
|
||||
if max_boot_count > 0:
|
||||
@@ -307,7 +448,7 @@ def verify(log_path, required=REQUIRED_DEFAULT, forbidden=FORBIDDEN_DEFAULT,
|
||||
"(run_boot_test.py adds it; --raw-log turns it off)")
|
||||
res["timing"] = timing
|
||||
|
||||
# 套件汇总行解析(T-2):抓"没跑到汇总行 / 断言数为 0"的盲区
|
||||
# 套件汇总行解析:抓"没跑到汇总行 / 断言数为 0"的盲区
|
||||
bad_summary = []
|
||||
if summary_list:
|
||||
parsed = {}
|
||||
@@ -380,13 +521,75 @@ def verify(log_path, required=REQUIRED_DEFAULT, forbidden=FORBIDDEN_DEFAULT,
|
||||
|
||||
out("")
|
||||
if (missing or hits or bad_summary or selftest_fail or boot_issue
|
||||
or order_issue or timing_issue):
|
||||
or order_issue or timing_issue or exact_bad or pair_bad):
|
||||
out("VERDICT: FAIL")
|
||||
return finish(1)
|
||||
out("VERDICT: PASS")
|
||||
return finish(0)
|
||||
|
||||
|
||||
def selftest(out=print):
|
||||
"""Prove the exact/pair machinery actually discriminates (exit 0 = all behaviours ok).
|
||||
|
||||
Written because a PASS that cannot fail is worthless: case 2/3 are the real captured
|
||||
shapes of the 2026-10-26 console-interleave defect, and case 9 shows that the legacy
|
||||
substring check calls the SAME defective line a PASS -- that is the blind spot --exact
|
||||
exists to close.
|
||||
"""
|
||||
T = "[2026-10-26 08:00:00]"
|
||||
prot_re = (r"\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]\[PROT\] uncalibrated -> protection "
|
||||
r"blocked \(UA/UN/IA VALID not all set\)")
|
||||
nand_re = r"\[\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\]\[NAND_FTL\] Initializing NAND FTL\.\.\."
|
||||
prot_ok = T + "[PROT] uncalibrated -> protection blocked (UA/UN/IA VALID not all set)"
|
||||
prot_spliced = T + "[PROT] uncalibrated -> protection [blocked (UA/UN/IA VALI2D not all s0et)"
|
||||
nand_ok = T + "[NAND_FTL] Initializing NAND FTL..."
|
||||
nand_headless = "26-10-26 08:37:07][NAND_FTL] Initializing NAND FTL..."
|
||||
mem_re = r"\[ERR\] MEMFAULT PC=\S+ LR=\S+ CFSR=\S+ HFSR=\S+"
|
||||
mem_ok = "[ERR] MEMFAULT PC=0x08004a12 LR=0x08004a3f CFSR=0x00008200 HFSR=0x40000000"
|
||||
rc_ok = T + "[APP] reset cause: software (NVIC_SystemReset) (CSR=0x14000000)"
|
||||
cases = []
|
||||
|
||||
def chk(name, got, want):
|
||||
cases.append((name, got == want, "got={!r} want={!r}".format(got, want)))
|
||||
|
||||
chk("clean exact", exact_check([prot_ok], prot_re)[0], True)
|
||||
ok_s, _d_s, neigh_s, _r_s = exact_check([prot_spliced], prot_re)
|
||||
chk("spliced line rejected", ok_s, False)
|
||||
chk("spliced line fails even with broken probe key", len(neigh_s), 0)
|
||||
ok_n, _d_n, neigh_n, _r_n = exact_check([nand_headless], nand_re)
|
||||
chk("headless line rejected", ok_n, False)
|
||||
chk("headless line reported as neighbour", len(neigh_n) >= 1, True)
|
||||
chk("residual 1 rejected when not allowed", exact_check(["#" + nand_ok], nand_re)[0], False)
|
||||
ok_r, det_r, _n_r, res_r = exact_check(["#" + nand_ok], nand_re, allow_residual=1)
|
||||
chk("residual 1 accepted when allowed", ok_r and len(res_r) == 1, True)
|
||||
chk("residual 2 still rejected", exact_check(["##" + nand_ok], nand_re, allow_residual=1)[0], False)
|
||||
chk("head_char enforced", exact_check([nand_ok], nand_re, 0, "[")[0], True)
|
||||
chk("head_char catches wrong head", exact_check([nand_headless], r"26-10-26.*NAND_FTL.*", 0, "[")[0], False)
|
||||
chk("len pin ok", exact_check([nand_ok], nand_re + EXACT_LEN_SEP + str(len(nand_ok)))[0], True)
|
||||
chk("len pin mismatch fails", exact_check([nand_ok], nand_re + EXACT_LEN_SEP + "9")[0], False)
|
||||
chk("pair good", pair_check([mem_ok, rc_ok], "MEMFAULT", "reset cause:")[0], True)
|
||||
chk("pair two following causes fails",
|
||||
pair_check([mem_ok, rc_ok, rc_ok], "MEMFAULT", "reset cause:")[0], False)
|
||||
chk("pair wrong order fails",
|
||||
pair_check([rc_ok, mem_ok], "MEMFAULT", "reset cause:")[0], False)
|
||||
# blind spot: legacy substring would PASS on this defective line
|
||||
full = "\n".join([prot_ok, nand_headless])
|
||||
legacy_present = "Initializing NAND FTL" in full # what --required sees
|
||||
exact_ok_here = exact_check([prot_ok, nand_headless], nand_re)[0]
|
||||
chk("blind spot proven: legacy substring PASSES where --exact FAILS",
|
||||
legacy_present and not exact_ok_here, True)
|
||||
|
||||
bad = 0
|
||||
for name, ok, detail in cases:
|
||||
out("{} {}".format("[OK] " if ok else "[FAIL]", name))
|
||||
if not ok:
|
||||
out(" {}".format(detail))
|
||||
bad += 1
|
||||
out("SELFTEST: {} case(s), {} failure(s) -> {}".format(
|
||||
len(cases), bad, "PASS" if bad == 0 else "FAIL"))
|
||||
return 0 if bad == 0 else 1
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
# 控制台输出固定 UTF-8:Windows 代码页(GBK/UTF-8 终端)均不会让本脚本崩溃
|
||||
for stream in (sys.stdout, sys.stderr):
|
||||
@@ -403,7 +606,7 @@ def main(argv=None):
|
||||
help="forbidden patterns, ';' separated (case-insensitive)")
|
||||
ap.add_argument("--require-suite-summary", default="",
|
||||
help="suite names that MUST appear as '=== <name>: N/M PASSED (failed=0) ===' "
|
||||
"with N>0, ';' separated (T-2). Empty = off")
|
||||
"with N>0, ';' separated. Empty = off")
|
||||
ap.add_argument("--show-tail-lines", type=int, default=40,
|
||||
help="show last N log lines (0 = off)")
|
||||
ap.add_argument("--require-selftest-pass", action="store_true",
|
||||
@@ -427,10 +630,36 @@ def main(argv=None):
|
||||
ap.add_argument("--max-boot-ms", type=int, default=0,
|
||||
help="fail if time from first line to --done-marker exceeds N ms "
|
||||
"(needs '[+Nms]' line prefix; 0 = off)")
|
||||
ap.add_argument("--exact", action="append", default=[],
|
||||
help="repeatable regex that must match a WHOLE firmware line "
|
||||
"(collector prefix stripped, anchored ^...$); a spliced byte or a "
|
||||
"missing line head fails. Optional suffix '@@LEN=<n>' pins the byte "
|
||||
"length -- opt-in only, lengths belong to one image. Use --exact "
|
||||
"instead of --required for any marker whose line can lose its head.")
|
||||
ap.add_argument("--allow-residual", type=int, default=0,
|
||||
help="accept up to N stray bytes before the line head on an --exact hit "
|
||||
"(crash-dump legs: 1 shift-register leftover is known; recorded, "
|
||||
"not a failure). 0 = strict (default)")
|
||||
ap.add_argument("--head-char", default="",
|
||||
help="every --exact hit must start with this char right after the "
|
||||
"optional residual prefix (crash-dump legs: '[')")
|
||||
ap.add_argument("--pair", action="append", nargs=2, metavar=("FIRST", "THEN"), default=[],
|
||||
help="repeatable pair: exactly one FIRST line, and exactly one THEN line "
|
||||
"after it (cross-reset pairing, e.g. 'MEMFAULT' 'reset cause:')")
|
||||
ap.add_argument("--include-comments", action="store_true",
|
||||
help="do NOT skip '#' lines (archived evidence files carry an ASCII "
|
||||
"provenance header; skipping it is the default so a header that "
|
||||
"merely mentions [ERR] cannot trip --forbidden)")
|
||||
ap.add_argument("--selftest", action="store_true",
|
||||
help="run the built-in discrimination cases and exit (0 = all behave "
|
||||
"as specified); needs no log file and no hardware")
|
||||
ap.add_argument("--json", action="store_true",
|
||||
help="emit one JSON object instead of the text verdict (exit code unchanged)")
|
||||
args = ap.parse_args(argv)
|
||||
|
||||
if args.selftest:
|
||||
return selftest()
|
||||
|
||||
return verify(args.log_path, args.required, args.forbidden,
|
||||
args.show_tail_lines, suite_summary=args.require_suite_summary,
|
||||
selftest_pass=args.require_selftest_pass,
|
||||
@@ -441,7 +670,10 @@ def main(argv=None):
|
||||
require_order=args.require_order,
|
||||
done_marker=args.done_marker,
|
||||
max_boot_ms=args.max_boot_ms,
|
||||
as_json=args.json)
|
||||
as_json=args.json,
|
||||
exact=args.exact, pairs=args.pair,
|
||||
allow_residual=args.allow_residual, head_char=args.head_char,
|
||||
include_comments=args.include_comments)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: task-worktree
|
||||
description: 多任务并行开发的空间管理(git worktree 生命周期):new 开一棵工作树 + 任务分支、目录联接共享 .venv 与模拟器 SDK、build/bench 两类独占锁、done 预演合并回主干并审计并集残留。规格见 docs/环境与部署/并行任务工作树方案.md。
|
||||
whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;要为新任务开一个隔离空间;要把任务合并回主干;或遇到两个任务抢 COM6 / UV4、"改了文档被覆盖"、并集合并残留时。
|
||||
description: 多任务并行开发的空间管理(git worktree 生命周期):new 开一棵工作树 + 任务分支、目录联接共享 .venv 与模拟器 SDK、活跃任务硬上限 3 与腾位、build/bench 两类独占锁(含非登记任务占锁后「缺台账」释放不掉的正解)、done 预演合并回主干、门禁未全绿的补基线收尾、并集合并残留审计。规格见 docs/环境与部署/并行任务工作树方案.md。
|
||||
whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;要为新任务开一个隔离空间;要把任务合并回主干;或遇到 `new` 报「已有 3 个活跃任务(上限 3)」、`lock release` 报「任务 X 不存在(缺台账)」、`done` 报「门禁未全绿:工作树与分支保留,未清场」、两个任务抢 COM6 / UV4、"改了文档被覆盖"、并集合并残留时。
|
||||
---
|
||||
|
||||
# task-worktree — 多任务并行开发空间(短命令 `@task`)
|
||||
@@ -14,7 +14,15 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
|
||||
& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" <子命令> [参数]
|
||||
```
|
||||
|
||||
bash(`ops_bash`)里固定两个变量,下文用 `$TW` 代指脚本:
|
||||
|
||||
```bash
|
||||
PY=".venv/Scripts/python.exe"; TW=".dsh/skills/task-worktree/scripts/task_worktree.py"
|
||||
"$PY" "$TW" list
|
||||
```
|
||||
|
||||
退出码:`0`=成功 / `1`=操作失败 / `2`=前置或环境错误 / `3`=用法。
|
||||
**⚠ 这几个码只有「不接管道」时才读得到**——原因与对策见「读输出」一节。
|
||||
|
||||
## 固定命令
|
||||
|
||||
@@ -24,14 +32,19 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
|
||||
| `list` | 所有任务一览:分支 / 落后领先 / 脏文件数 / 台架状态 + 两个锁的归属 |
|
||||
| `status <ID>` | 单任务详情(未提交清单、相对基线漂移、台架记录、已建联接) |
|
||||
| `sync <ID>` | 在任务工作树里 `rebase` 追平主干,把冲突控制在任务分支上 |
|
||||
| `lock acquire\|release\|status <build\|bench>` | 独占资源锁 |
|
||||
| `lock acquire|release|status <build|bench>` | 独占资源锁 |
|
||||
| `done <ID>` | 预演 → 合入主干 → 并集残留审计 → 重生成产物 → 门禁 → 清场 |
|
||||
| `abort <ID>` | 放弃任务(工作树 + 分支一起删,**永久丢弃**) |
|
||||
|
||||
`new` 常用开关:`--base <分支>`(默认 `v2-on-ui`)· `--needs-bench`(无台架 PASS 则 `done` 拒合)·
|
||||
`--lean`(跳过 Git LFS smudge)· `--no-bootstrap`(不建联接)。
|
||||
`--lean`(跳过 Git LFS smudge)· `--no-bootstrap`(不建联接)·
|
||||
`--force`(**仅跳过并发上限 3 的检查**,不解决「谁该让位」,见 A 节)。
|
||||
|
||||
`done` 常用开关:`--dry-run`(只预演,零改动)· `--skip-build` · `--skip-gates` · `--force`。
|
||||
`done` 常用开关:`--dry-run`(只预演,零改动)· `--skip-build` · `--skip-gates` · `--force`(清场时强制移除工作树)。
|
||||
|
||||
`lock` 开关:`--task <ID>`(acquire 必填)· `--wait <秒>` · `--ttl <小时>`(自报窗口)· `--note <文本>` ·
|
||||
`--bench-pass`(release 时给台账记 PASS)· `--force`。
|
||||
⚠ `--note` 只在 **acquire** 时写进锁 payload,**release 时被忽略**——别指望它留下验收证据(证据写进主干提交说明或文档)。
|
||||
|
||||
**环境变量**(高级):`DTU_WT_ROOT`(任务容器位置,默认仓库同级 `_wt/`)·
|
||||
`DTU_TASK_BASE`(默认基线分支,默认 `v2-on-ui`)·
|
||||
@@ -51,6 +64,121 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
|
||||
& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" done N21
|
||||
```
|
||||
|
||||
## 读输出:管道与截断(两起误判的共同根因)
|
||||
|
||||
1. **`... 2>&1 | tail -N` 的退出码是 `tail` 的,恒为 0** → 脚本的 `1` / `2` 全被吞。实测三条印着
|
||||
「rc=0」的输出其实都是 exit 2:`new` 撞活跃上限、`lock release` 缺台账两次。
|
||||
所以**判据先看文本**(`[ok]` / `!!` / `前置/环境错误:` / `失败:`),要真码就**直接执行不接管道**或补一句
|
||||
`echo RC=$?`;坚持接管道就在 bash 里取 `${PIPESTATUS[0]}`。
|
||||
2. **`done` / `done --dry-run` 的输出不要用 `tail -2` 截**:接管道时 python 的 stdout 是块缓冲、git
|
||||
子进程的 stderr 不缓冲,两股流错位混排,`tail -2` 只剩「--dry-run:到此为止…」加上 stderr 里带
|
||||
`conflict` 字样的片段 → 据此误判「有冲突」。实测:`tail -2` 误读成 conflict,放宽到 `tail -10` 才看到
|
||||
`[ok] 无冲突,预演结果树 <sha>`。dry-run 全文只有 5~8 行,直接读全量。
|
||||
|
||||
## 分步操作(三条运行时机制)
|
||||
|
||||
### A. `new` 被拒「已有 3 个活跃任务(上限 3)」——怎么腾位
|
||||
|
||||
1. 读原文:`前置/环境错误:已有 3 个活跃任务(上限 3);先 done/abort,或加 --force`(exit 2)。
|
||||
上限是脚本常量 `MAX_CONCURRENT = 3`;**「活跃」的判据 = `_wt/.state/<ID>.json` 台账存在,且台账里
|
||||
`path` 指向的目录还在**(手工删了工作树目录就不占名额,但会留下悬挂台账,见第 5 步)。
|
||||
2. `$TW list` 看全景(分支 / 落后领先 / 脏文件数)决定谁让位:**已完成的先 `done`**(合并回收,一步不省)。
|
||||
3. 没人认领、零提交、尚未 claim 的空闲树 → `abort <ID>` 腾位(实测就是 abort 掉排在后面的空闲树换出名额)。
|
||||
`abort` = 工作树 + 分支一起删、**永久丢弃**;工作树有未提交改动或已有提交时脚本会拒绝,确认可弃再加 `--force`。
|
||||
4. 确实要同时开第 4 棵才 `new <ID> <slug> --force`:磁盘代价约 **83 MB/棵**(`--lean` 约 45 MB),
|
||||
并行度上去了、**冲突面与台架排队也一起上去**——不是免费的。
|
||||
5. **别用手工 `git worktree remove` 腾位**:台账不会被删(下次 `new` 同 ID 会报「任务 <ID> 已存在」),
|
||||
还绕过了「先摘联接」(铁律 5),有让 git 递归进联接把主工作树 `.venv` 删掉的风险。
|
||||
6. 腾位后复验:`$TW list` 的条目数与 `ls _wt` 的目录数一致,再 `new`。
|
||||
|
||||
### B. 自己占的锁没有台账:`lock release` 报「任务 X 不存在(缺台账)」
|
||||
|
||||
**症状**(lead 在主工作树跑台架验收窗,用临时名占锁:`lock acquire bench --task acceptwin`):
|
||||
|
||||
```
|
||||
"$PY" "$TW" lock release bench --task acceptwin --bench-pass
|
||||
前置/环境错误:任务 acceptwin 不存在(缺台账 _wt/.state/acceptwin.json)
|
||||
"$PY" "$TW" lock release bench --task acceptwin --force --bench-pass # 同一句错误,锁仍在
|
||||
```
|
||||
|
||||
**根因**(读脚本确认):`release` 分支里**只要带 `--bench-pass` 就先 `load_state(task)`** 去给台账写 PASS,
|
||||
台账不在就抛「前置/环境错误」,**根本走不到释放那一步**;`--force` 只覆盖「持有者与 `--task` 不符」和
|
||||
acquire 的陈旧接管,管不到台账缺失。而 `acquire` 侧**不查台账**——这个不对称就是坑本身;报错文案里的
|
||||
「任务不存在」也别误读成「我没占过锁」。
|
||||
|
||||
1. 先 `$TW lock status` 确认持有者 / pid / 持有时长 / TTL(如 `bench acceptwin 持有(pid=2784,
|
||||
已 0.1 / TTL 6.0 小时)`)——**别反复重试 release**。
|
||||
2. **不需要同步记 PASS** → 直接 `$TW lock release bench --task <NAME>`(不带 `--bench-pass`)就能释放。
|
||||
3. **需要 `--bench-pass` 而这个名字不是登记任务** → 手写**最小占位台账** `_wt/.state/<NAME>.json`
|
||||
(键与 `new` 写出的完全一致,共 11 个):
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "acceptwin",
|
||||
"slug": "lead-window",
|
||||
"branch": "(none: lead 验收窗占位台账)",
|
||||
"path": "D:\\Code\\DTU Code\\STM32F4-Base",
|
||||
"base": "v2-on-ui",
|
||||
"created_at": "2026-10-26 17:00:00",
|
||||
"junctions": [],
|
||||
"needs_bench": true,
|
||||
"bench_pass": false,
|
||||
"note": "lead 验收窗(非工作树任务,为 lock 台账占位)",
|
||||
"lean": true
|
||||
}
|
||||
```
|
||||
|
||||
用写文件的工具落 UTF-8(无 BOM),别用 `echo` / `Set-Content`(中文乱码铁律)。
|
||||
4. `$TW lock release bench --task <NAME> --bench-pass` → `[ok] 已为任务 … 记录台架 PASS` +
|
||||
`已释放锁 bench(原持有者 …)`。
|
||||
5. **立刻删掉占位文件**:它活着期间会被 `list` 当任务列出,并**占掉 3 个活跃名额之一**(`path` 指向的
|
||||
目录存在就算活跃);而且**绝不对这个 ID 跑 `done` / `abort`**——清场会拿台账里的 `path` / `branch`
|
||||
去 `git worktree remove` 和删分支。
|
||||
6. 复验 `$TW lock status`:`build 空闲(TTL 1.0h)` / `bench 空闲(TTL 6.0h)`。
|
||||
7. 占位台账一删,那条 PASS 也随之消失。**这个 PASS 后面还要用的话,从一开始就用已登记的任务 ID 占锁**
|
||||
(lead 的验收窗借某个已 `new` 出来的 ID 最省事),或把验收结论写进主干提交说明/文档。
|
||||
8. **为什么必须马上释放**:`done` 的前置会跑 `release_stale_locks()`——**未到 TTL 的活锁直接判失败**:
|
||||
`独占资源 bench 仍被 acceptwin 占用(已 0.1 / TTL 6.0 小时),先 release`。lead 挂着锁 = 挡住所有任务
|
||||
的收口;只有超过 TTL 的锁才会被自动接管。
|
||||
9. 最后手段(脚本本身都走不通时):锁就是 `_wt/.state/locks/<name>.lock` 里的单行 JSON
|
||||
(`name/task/pid/host/epoch/at/note/ttl`),`release` 的实现就是 unlink 它——手工删这个文件等价于
|
||||
「不记 PASS 的强制释放」,只在前面几步都不可用时才用。
|
||||
|
||||
### C. `done <ID>` 的正确用法(它是合并点,不是查看器)
|
||||
|
||||
1. **合并前确认任务属主已把全部改动提交完**。`$TW status <ID>` 看未提交清单与领先提交数;跨会话任务
|
||||
再问一句属主(`send_message`)。实测教训:属主还剩「2/2」没提交就 `done`,把只含 1/2 的分支合进了主干,
|
||||
只能另开一棵树补后半并向对方说明(是自己的时序错,不是分支问题)。
|
||||
2. **先 `--dry-run`,并读全量输出**(别 `tail -2`,见「读输出」)。判据两行:
|
||||
`[ok] 前置检查通过(领先 <base> 共 N 个提交)` + `[ok] 无冲突,预演结果树 <sha>`;
|
||||
**N 必须等于属主应交的提交数**,对不上回到第 1 步。dry-run 零改动。
|
||||
3. 正式 `$TW done <ID>`——**必须在主工作树**(铁律 1)。
|
||||
4. 输出 `!! 门禁未全绿:工作树与分支**保留**,未清场` 时,紧跟的 `主干已含合并提交 <sha>` 已经说明
|
||||
**合并落进主干了**。
|
||||
- ✅ 走提示里的 **a) 在主工作树修正后追加提交**;
|
||||
- ❌ **绝不用 b) 的 `git -C "<主树>" reset --hard <sha>`**——那是丢弃已入库的合并提交,而
|
||||
`reset --hard` 本身是项目级 Never(会连未提交工作一起丢)。提示 b 只是列出的可能性,不是建议。
|
||||
- 先定位是**哪条钩子**红:门禁输出里的 `[SUMMARY] L0: pre-commit hooks -- passed=N failed=M`,
|
||||
不同根因处置不同(见陷阱「`done` 的门禁 = `pre-commit run --all-files`」)。最常态是 `.map` 滞后 → 第 5 步。
|
||||
5. 补绿固定链(2026-10-25 实测;顺序即因果,全在**主工作树**跑,逐条不接管道看退出码):
|
||||
|
||||
```bash
|
||||
"$PY" scripts/check_mem_numbers.py --write # 台账受管区 ↔ 新 .map 对齐
|
||||
"$PY" scripts/ram_baseline.py --write # RAM 快照 / 92% 预算台账
|
||||
"$PY" scripts/ram_baseline.py --update # 趋势基线追加一行
|
||||
"$PY" scripts/check_test_coverage.py --update-baseline # 覆盖率基线(新单测 / 新套件时必刷)
|
||||
"$PY" scripts/check_doc_links.py --all # 文档链接与章节引用
|
||||
git add <上面几条刷出来的基线/快照文件> # 显式列文件,别 -A 把别人在飞的改动一起提交
|
||||
git commit -F <UTF-8 无 BOM 的正文文件> # 提交属 Ask first(AGENTS.md 边界),按用户指示再做
|
||||
```
|
||||
|
||||
为什么必然要补,见陷阱「`done` 的门禁跑在 `@build` 之前」。
|
||||
6. **重跑 `done` 之前主工作树必须干净**——否则前置检查直接
|
||||
`失败:主工作树不干净(…)——合并会把未提交改动卷进来`,所以第 5 步的产物必须**先提交**。重跑时若分支
|
||||
已并入,会打印 `[i] task/<ID>-<slug> 已合入 <base>,跳过合并,直接清场` → 摘联接 → `完成(分支此前已合入)`。
|
||||
7. 清场完成后再敲一次 `done <ID>` 会得到 `前置/环境错误:任务 <ID> 不存在(缺台账 …json)`(exit 2)——
|
||||
**这不是失败**:清场的最后一步 unlink 了台账。判完成以 `git log` 为准。
|
||||
|
||||
## 铁律
|
||||
|
||||
1. **`done` 只能在主工作树执行**——主干分支检出在主工作树,git 不允许同一分支在两处检出。
|
||||
@@ -59,17 +187,23 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
|
||||
设备 IP `192.168.1.100` 各只有一份。**构建与烧录都不可并发。**
|
||||
- `build` 锁由 **`@build` 自持自释**,调用方无需手工 acquire/release;
|
||||
- `bench` 锁**必须手工**:`lock acquire bench --task <ID>` → 烧录/`@test`/探针 →
|
||||
`lock release bench --task <ID> --bench-pass`。
|
||||
`lock release bench --task <ID> --bench-pass`。非登记任务(lead 的验收窗)占了锁怎么放 → B 节。
|
||||
4. **单写者文档**:`docs/任务规划/`(整个文件夹)、`AGENTS.md` 的任务表块、`MDK-ARM/*.uvprojx`、
|
||||
`.pre-commit-config.yaml`、内存快照 —— 任务期**不在任务工作树里改**,留到 `done` 统一处理。
|
||||
5. **清场必须先摘联接**:脚本会先 `os.rmdir` 掉 `.venv` / `simulator/SDL2` 联接再
|
||||
`git worktree remove`。**手工清场时千万别直接 `git worktree remove`**——git 可能递归进联接,
|
||||
把主工作树的 `.venv` 一起删掉。
|
||||
6. **并发上限 3 棵工作树**(每棵约 83 MB 检出)。
|
||||
6. **活跃任务硬上限 3**(脚本常量 `MAX_CONCURRENT = 3`,每棵约 83 MB 检出)。满了 `new` 直接 exit 2,
|
||||
腾位顺序与 `--force` 的代价 → A 节。
|
||||
7. **工作树放仓库同级 `_wt/`,绝不在仓库内部**——否则污染 `git status`、`pre-commit --all-files` 与全库搜索。
|
||||
8. **`done` 是合并点**:属主没提交完就别 `done`;合并提交一旦进了主干,只用「主工作树补提交」收尾,
|
||||
**不用 `reset --hard` 反悔** → C 节。
|
||||
|
||||
## 陷阱(实测)
|
||||
|
||||
- **`--force` 不是万能钥匙**:它只覆盖四类检查——并发上限、持有者与 `--task` 不符、陈旧锁接管、
|
||||
清场时强制移除工作树。**台账缺失这类前置/环境错误(exit 2)跟它无关**(实测 `lock release
|
||||
--force --bench-pass` 报的是同一句话,锁照旧没放)。
|
||||
- **联接不是符号链接**:本仓库 `core.symlinks=false`,git 里的 symlink 会被检出成
|
||||
「内容是目标路径的文本文件」。共享 `.venv` 只能用 Windows **目录联接**(`mklink /J`,
|
||||
不需要管理员权限),且联接必须建在版本控制之外。
|
||||
@@ -78,19 +212,34 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
|
||||
subprocess,含空格的路径(本项目 `DTU Code`)由 subprocess 自动加引号。
|
||||
- **联接建失败不中止 `new`**:只告警并打印手工补建命令,避免留下半成品任务空间;
|
||||
但该任务里跑不了 `.venv\Scripts\python.exe`,`@build` / `ci_local.py` 会找不到解释器。
|
||||
- **`done` 前主工作树必须干净**:否则未提交改动会被卷进合并。
|
||||
- **`done` 前主工作树必须干净**:否则未提交改动会被卷进合并(补完基线再重跑 `done` 时最容易忘,见 C 第 6 步)。
|
||||
- **⚠ `done` 的门禁跑在 `@build` 之前 → 依赖 `.map` 的钩子会误报**(2026-10-12 实测):
|
||||
`done` 的顺序是「合并 → 重生成 → **门禁(ci_local)** → `@build`」,而
|
||||
`mem-budget`(N-27)/ `mem-numbers` 是拿**文档快照 vs `.map`** 比对 —— 门禁跑时
|
||||
`mem-budget`(登记簿条目「缩堆迁 CCM 后半(暂缓)」)/ `mem-numbers` 是拿**文档快照 vs `.map`** 比对 —— 门禁跑时
|
||||
`.map` 还是**主工作树合并前**的旧产物,与刚合并进来的新台账**必然不一致** →
|
||||
误报 `[FAIL] 台账受管区与 .map 不一致`。`@build` 跑完后 `.map` 才是新的。
|
||||
**处置**:门禁失败且只差这两项时,按提示在主工作树跑
|
||||
`python scripts/ram_baseline.py --write` + `python scripts/check_mem_numbers.py --write`
|
||||
后追加提交,再重跑 `done`(它会检测到「已合入」直接清场)。
|
||||
**处置(2026-10-25 补全)**:按 C 节第 5 步的完整链补——`check_mem_numbers.py --write` →
|
||||
`ram_baseline.py --write` → `ram_baseline.py --update` → `check_test_coverage.py --update-baseline` →
|
||||
`check_doc_links.py --all` → 提交 → 重跑 `done`(它会检测到「已合入」直接清场)。
|
||||
此时主干已含合并提交,**绝不能用提示里的 `reset --hard` 回退**。
|
||||
- **`done` 的门禁 = `ci_local.py` = `pre-commit run --all-files`(50 条钩子全跑)**(2026-10-26):红了先读
|
||||
`[SUMMARY] L0: pre-commit hooks -- passed=N failed=M skipped=K`,**按钩子 id 定根因**,别一律当「`.map` 滞后」
|
||||
去跑补基线链。典型例外:`generated-fresh`(生成器幂等性,触发面 `tools/**`、`docs/images/*.svg`、UI 生成文件)——
|
||||
合进来的分支改了生成器却没重跑产物时,补基线链救不了,处置是在**主工作树重跑那个生成器**、脚本与产物同一笔提交,
|
||||
再重跑 `done`。另注意 `pre-commit run --all-files` 只跑 pre-commit stage,个别只在 commit-msg / 手工阶段跑的守卫不在此列
|
||||
(口径见 `scripts/ci_local.py` 头部说明)。
|
||||
- **⚠ 新建工作树从未构建过 → 体积类门禁必然红**(2026-10-25 实测):`.map` 是每棵树各自的
|
||||
构建产物、不入版本库,所以 `new` 出来的空间里 `mem-numbers` / `mem-budget` / `ci_local.py`
|
||||
的 L0c 都拿不到事实源,报 `[FAIL] 找不到 .map 事实源`(脚本提示已写明原因,不是代码缺陷)。
|
||||
同理 `done` 的重生成步骤会 `[skip] 未找到 .map,跳过内存快照同步(需先 @build)`。
|
||||
**处置**:新空间第一次提交前先 `@build` 一次(`build` 锁自动排队,与别的树串行)。与上一条
|
||||
的区别要分清:上一条是「有 `.map` 但是旧的」,本条是「根本没有 `.map`」,处置动作不同。
|
||||
- **⚠ 增量构建可能不重链 → `.map` 数字瞬态不一致**(2026-10-12 实测):
|
||||
`@build` 若判定无需重链,`.map` 保持旧值,此时 `--write` 生成的快照是**瞬态值**
|
||||
(实测同一份源码两次读到 223,560 与 223,724,差 164 B)。**以主工作树(集成点)
|
||||
的 `.map` 为准**;改完 RAM 相关代码后确认重链再刷新快照。
|
||||
- **`ram_baseline.py` 的 help 串里有未转义的 `%`**(2026-10-25 实测:某些直调姿势会渲染 argparse help 并崩)——
|
||||
查用法读脚本头部 docstring,别靠 `--help`;修脚本本身属 `mem-budget` skill 的范围。
|
||||
- **合并冲突常落在「生成物」上**:`docs/内存管理/RAM使用分析与优化方案.md`(mem-numbers
|
||||
快照)在双方都改过时必冲突,且**两边都不能直接选**(各自基于不同基线)——
|
||||
正确做法是解冲突后用**合并后的实际构建**重新生成,不要挑一侧。
|
||||
@@ -110,14 +259,28 @@ whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;
|
||||
bench 是会话级(flash→`@test`→探针),才配得上小时级。持有者还可用
|
||||
`lock acquire ... --ttl <小时>` **自报**更精确的窗口(`keil_build.py` 即按其 `--timeout` 自报)。
|
||||
- **陈旧锁自动接管**:超过该锁 TTL 未释放即视为陈旧,下次 `acquire` 会接管并提示;
|
||||
未到 TTL 的活锁需 `--force`。`done` 的收口前置也会**自动释放陈旧锁**(不再硬失败)。
|
||||
未到 TTL 的活锁需 `--force`。`done` 的收口前置也会**自动释放陈旧锁**(不再硬失败),但
|
||||
**未到 TTL 的活锁会把 `done` 挡在前置检查**(B 节第 8 步)。
|
||||
- **`build` 锁不必手工持**(2026-10-12 起):`@build`(`keil_build.py`)**自持自释**——
|
||||
进入 acquire、`finally` 无条件 release,所以"Agent 忘了 release、锁一直挂到 TTL"这条在构建路径上已消除。
|
||||
`done` 的门禁自己持锁并给子进程设 `DTU_LOCK_HELD=build` 告知跳过(新旧脚本都兼容)。
|
||||
**`bench` 锁仍须 Agent 手工持**(会话作用域:跨烧录→`@test`→探针,见 `keil-build` SKILL.md)。
|
||||
|
||||
## 验证
|
||||
|
||||
- `$TW list`:条目数 = `ls _wt` 的目录数且 ≤ 3;刚回收的任务与 B 节的占位条目都不再出现。
|
||||
- `$TW lock status`:`build 空闲(TTL 1.0h)` + `bench 空闲(TTL 6.0h)`——没有本会话留下的持有者。
|
||||
- `git log --oneline -3`:能看到 `merge: task/<ID>-<slug> 合入 <base>` 与其后的补基线提交;
|
||||
`git status --porcelain` 为空(补链改动已提交,才谈得上重跑 `done`)。
|
||||
- `ls _wt/.state/`:该 ID 的台账已无(清场最后一步 unlink);重复 `done` 报「任务不存在」不必再追。
|
||||
- 补绿链的每一条都自己 `echo RC=$?` 看到 0,最后以重跑 `done` 打出的**清场日志**(`已摘除联接` /
|
||||
`已删除分支` / `完成(分支此前已合入)`)为成功判据,而不是 `tail` 出来的最后一行。
|
||||
|
||||
## 与其它 skill 的衔接
|
||||
|
||||
- 构建 = `keil-build` skill(`@build`);烧录/回读 = `stlink-flash` skill;串口闭环 = `serial-boot-test` skill
|
||||
- 烧录产物的真实 hex 路径以 `.serialtest/config.json` 的 hex 键为准(实测
|
||||
`MDK-ARM/Objects/STM32F407-Demo.hex` 这个路径并不存在),拷错等于烧了旧镜像 → `serial-boot-test` skill
|
||||
- 新 `.c` 文件 = `new-source-file` skill(`uvprojx` 与 `CMakeLists.txt` 双端注册)
|
||||
- `.map` 口径 / RAM 台账与趋势 → `mem-budget` skill;UI 回归 → `ui-verify` skill
|
||||
- 完整机制与目录分配 → `docs/环境与部署/并行任务工作树方案.md`
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
name: template-skill
|
||||
description: Replace with description of the skill and when Claude should use it.
|
||||
---
|
||||
|
||||
# Insert instructions below
|
||||
@@ -38,8 +38,10 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
|
||||
|
||||
| 常量 | 脚本 | 当前值 | 含义 |
|
||||
|---|---|---|---|
|
||||
| `MENU_POOL_BASELINE` | check_menu_rows.py | 4784 B | 菜单根框建完的 LVGL 池 used(x64 模拟器) |
|
||||
| `POOL_AFTER_ROUNDTRIP` | check_content_pages.py | 4864 B | 开页-关页往返后 = 基线 + 80 B 一次性残留 |
|
||||
| `MENU_POOL_BASELINE` | check_menu_rows.py | 5368 B | 菜单根框建完的 LVGL 池 used(x64 模拟器;2026-10-11 随 LVGL 控件裁剪由 4784 上移) |
|
||||
| `POOL_AFTER_ROUNDTRIP` | check_content_pages.py | 5448 B | 开页-关页往返后 = 基线 + 80 B 一次性残留 |
|
||||
|
||||
⚠ 两个常量量的是池的 **`used`**,与 `LV_MEM_SIZE` 总大小**无关** → 改池大小(如 2026-10-15 缩池 40→16 KB)**不该动这两个数**;动了就是页面结构变化或泄漏。
|
||||
|
||||
**禁止为过门禁盲目改常量**——无法解释的漂移 = 内存回归 bug。
|
||||
|
||||
@@ -47,5 +49,6 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
|
||||
|
||||
- `.venv` 缺失/换机:`& <本机任一 python ≥3.10> -m venv .venv; & '.venv\Scripts\python.exe' -m pip install -r requirements.txt`(`requirements.txt` 已含 `pre-commit`,重建后 L0 门禁直接可跑)
|
||||
- 新增 UI `.c` 文件先走 `new-source-file` skill 双端注册,否则一端静默缺文件
|
||||
- 改 `ui_menu_tree.txt` 或界面里的中文字符串 → **同一笔**重跑生成器(`tools/menu_gen/gen_menu.py`;字体走 **font-regen** 四步),否则提交门禁 `generated-fresh` 直接拦:它把生成器拷进系统临时目录重跑、与工作区逐字节比对(实证 `0f7b5a87` 改了模板没重跑)→ `docs/门禁说明/generated-fresh.md`
|
||||
- 字体重生成后全页大面积像素差异属预期:确认字形正确后 `--update` 刷参考
|
||||
- 涉及新界面/大改时本 skill 是 P3 门禁,完整管线(P0~P5)见 `docs/UI设计/UI_界面制作流程.md`
|
||||
|
||||
+346
-31
@@ -10,16 +10,25 @@
|
||||
# 明确不做的两件事(都是刻意为之):
|
||||
# 1. `@build`(Keil UV4)—— 需本机 Keil 授权,runner 上不具备;且 L0 的 cppcheck +
|
||||
# 代码规范已覆盖"编译前的静态问题",真编译仍是开发者的本地红线(AGENTS.md)。
|
||||
# 2. `mem-numbers` 钩子 —— 它的事实源是 `MDK-ARM/**/*.map`(Keil 构建产物、已 gitignore),
|
||||
# 无 `@build` 即无 `.map`。故本 workflow 在**检测不到 .map 时自动跳过该钩子**
|
||||
# (见下方 "决定跳过哪些钩子" 步骤),有 .map 的 runner(如自建带 Keil 的机器)则照常跑。
|
||||
# 2. `mem-numbers` / `mem-budget` 两根钩子 —— 事实源都是 `MDK-ARM/**/*.map`(Keil
|
||||
# 构建产物、已 gitignore),无 `@build` 即无 `.map`。故本 workflow 在**检测不到
|
||||
# .map 时自动跳过它们**(见下方 "决定跳过哪些钩子" 步骤),有 .map 的 runner 照常跑。
|
||||
#
|
||||
# 🔴 门禁盲区声明(S14,2026-10-02):跳过 `mem-numbers` 意味着 **CI 不守 Flash/RAM 体积门禁**。
|
||||
# 体积变更(新增任务/缓冲/代码)不会在 CI 被拦下;必须靠**本地 `@build` 后**
|
||||
# `python scripts/check_mem_numbers.py --scan` 兜底。故该跳过以 `::warning::` 明示
|
||||
# (不是弱化的 `::notice::`),并写入 job step-summary 的醒目行。
|
||||
# 🔴 体积门禁:盲区已关闭为**强制路径**(2026-10-25 起;原先只做到"声明")
|
||||
# 跳过钩子只解释了"runner 没有 Keil",没解决**"没跑 = 通过"**:钩子在无 .map 时
|
||||
# 自适应 SKIP 并返回 0,于是 CI 全绿仍可能被读成"体积也守住了"。现由 L0 作业**最后
|
||||
# 一步** `python scripts/check_size_gate.py --ci` 在**不需要 Keil** 的前提下补两条判据:
|
||||
# · 同码性:`scripts/size_attestation.json` 的源码指纹必须等于本次检出树的指纹,
|
||||
# 而该凭证只能由"新鲜 .map + 当时未超线"的真实链接测量写出(见 --stamp 三道前置)
|
||||
# ——等价于"这份代码至少被 Keil 链接并核算过一次";
|
||||
# · 红线:该次测量记录的受管区占比 ≤92%,与 `mem-budget` 同一常数同一口径。
|
||||
# 强制落点 = **主干 push**(唯一能保证"这就是要发出去的那份"的位置);其余事件降级为
|
||||
# `::warning::`——任务分支上的代码尚未定型,拦红只制造噪音,还会诱发多条道同时
|
||||
# 抢写 scripts/ 台账。仍守不住的(**口径正文唯一**在 scripts/check_size_gate.py 的
|
||||
# 模块 docstring,此处不重复):绝对数字由本地真实构建产生(runner 无 Keil 许可证)、
|
||||
# 编译器选项(uvprojx,UV4 每次改写且不入库)不在指纹范围内。
|
||||
#
|
||||
# 产物存档(S3,2026-10-02):两个 job 把关键输出 `Tee-Object` 到 `ci-artifacts/*.log`,
|
||||
# 产物存档与结果可见性(2026-10-02 起):两个 job 把关键输出 `Tee-Object` 到 `ci-artifacts/*.log`,
|
||||
# 并写 $GITHUB_STEP_SUMMARY(Gitea UI 直接可见)+ `::notice::` 汇总行。
|
||||
# **刻意不用 `actions/upload-artifact`**——内网可能拉不到外层 action(见 docs/CI/远程CI完整指南.md §5.3);
|
||||
# 零依赖方案下日志留在 runner 工作区,UI 通过 step-summary 即可排障。
|
||||
@@ -116,11 +125,128 @@ jobs:
|
||||
name: L0 静态门禁 (pre-commit)
|
||||
runs-on: windows
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# 钩子要对比基线与历史(doc-links 等读工作区即可,但 gitlint/cubemx 需完整历史)
|
||||
fetch-depth: 0
|
||||
|
||||
- name: 检出源码(原生 git;不依赖 github.com 上的 action)
|
||||
shell: powershell
|
||||
env:
|
||||
DTU_REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git
|
||||
DTU_REPO_REF: ${{ github.ref }}
|
||||
DTU_REPO_SHA: ${{ github.sha }}
|
||||
DTU_REPO_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Pure ASCII only (see file header).
|
||||
#
|
||||
# WHY NOT a "uses:" action: the runner resolves every uses: by contacting the
|
||||
# actions URL during "Prepare all required actions" -- BEFORE the first step
|
||||
# runs. On this intranet runner that call intermittently dies with a connect
|
||||
# timeout: runs 36 (10-21) / 41 (10-25) / 42 (10-26) each burned ~45 s and
|
||||
# produced ZERO hook output, while a manual curl to the same URL minutes later
|
||||
# returned 200. Manual reachability is not CI health: the resolver runs as
|
||||
# SYSTEM and the runner caches nothing across jobs. Gitea 1.25.4 also removed
|
||||
# the custom DEFAULT_ACTIONS_URL knob, so the only fix this repo owns is to
|
||||
# stop depending on the public resolver at all.
|
||||
#
|
||||
# KEEP IN SYNC with the same step in the other job. This logic deliberately
|
||||
# does NOT live in scripts/*.ps1 (unlike the Python / MSVC probes): the file is
|
||||
# not checked out yet at the moment it would be needed -- chicken and egg.
|
||||
#
|
||||
# $ErrorActionPreference is set to Continue EXPLICITLY. The runner executes a
|
||||
# run: block by dot-sourcing it into a session, so an inherited Stop turns any
|
||||
# raw native stderr line into a TERMINATING NativeCommandError. Measured
|
||||
# 2026-10-27, run 44 / task-72: a benign "error: No such remote 'origin'" from
|
||||
# a probe killed the whole job 3 seconds in, before any gate could run.
|
||||
$ErrorActionPreference = 'Continue'
|
||||
function Invoke-DtuGit {
|
||||
# Single funnel for git. Two reasons it must be a funnel, not bare calls:
|
||||
# 1) native stderr has to be flattened into plain strings (see above), and
|
||||
# 2) $LASTEXITCODE has to stay readable by the caller afterwards.
|
||||
# Returns Code + lines and lets the caller decide what to print.
|
||||
param([string[]]$G)
|
||||
$lines = & git @G 2>&1 | ForEach-Object { [string]$_ }
|
||||
return [pscustomobject]@{ Code = $LASTEXITCODE; Lines = @($lines) }
|
||||
}
|
||||
if (-not $env:DTU_REPO_SHA) { Write-Host '::error::DTU_REPO_SHA is empty'; exit 1 }
|
||||
if (-not $env:DTU_REPO_URL) { Write-Host '::error::DTU_REPO_URL is empty'; exit 1 }
|
||||
# Never let a fetch wait for a human: the runner has no console, and a
|
||||
# credential prompt would burn the whole job timeout while looking like a hung
|
||||
# CI. Anonymous fetch works here (the repository is public; measured HTTP 200
|
||||
# on /info/refs?service=git-upload-pack), so CI must not depend on a credential
|
||||
# store it cannot answer.
|
||||
$env:GIT_TERMINAL_PROMPT = '0'
|
||||
$ws = (Get-Location).Path
|
||||
Write-Host "workspace: $ws"
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $ws '.git'))) {
|
||||
$r = Invoke-DtuGit -G @('init', '-q', '.')
|
||||
if ($r.Code -ne 0) { Write-Host '::error::git init failed'; $r.Lines | ForEach-Object { Write-Host " $_" }; exit 1 }
|
||||
}
|
||||
# "git remote get-url" writes to stderr when the remote is absent -- that is
|
||||
# the exact line that killed run 44. Ask the config instead: an absent key
|
||||
# prints nothing and only sets a non-zero code, so nothing escapes as an error.
|
||||
$probe = Invoke-DtuGit -G @('config', '--get', 'remote.origin.url')
|
||||
$url = ($probe.Lines -join '').Trim()
|
||||
if ($probe.Code -eq 0 -and $url) {
|
||||
$r = Invoke-DtuGit -G @('remote', 'set-url', 'origin', $env:DTU_REPO_URL)
|
||||
} else {
|
||||
$r = Invoke-DtuGit -G @('remote', 'add', 'origin', $env:DTU_REPO_URL)
|
||||
}
|
||||
if ($r.Code -ne 0) { Write-Host '::error::configuring remote origin failed'; $r.Lines | ForEach-Object { Write-Host " $_" }; exit 1 }
|
||||
# Full refspec, NOT --depth 1: the commit-message gate resolves base..head
|
||||
# ranges and several gates diff against history, so a shallow clone turns them
|
||||
# into "unknown revision" failures. Tags are skipped on purpose: this repository
|
||||
# has zero tags and no script uses "git describe --tags" (measured 2026-10-27).
|
||||
$r = Invoke-DtuGit -G @('config', 'remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*')
|
||||
if ($r.Code -ne 0) { Write-Host '::error::setting remote.origin.fetch failed'; exit 1 }
|
||||
# The token, when the instance provides one, travels only as a per-invocation
|
||||
# -c override: it never lands in .git/config and is never echoed in logs.
|
||||
$auth = @('-c', 'credential.helper=', '-c', 'credential.interactive=false')
|
||||
if ($env:DTU_REPO_TOKEN) {
|
||||
# Same scheme actions/checkout uses against Gitea git-http: Basic with a
|
||||
# placeholder user and the token as the password. (Bearer is the GitHub API
|
||||
# style, not the git-http style.) Honest caveat: this branch CANNOT be
|
||||
# verified here -- the repository is public, so an anonymous fetch succeeds
|
||||
# and any token we fabricate locally only proves that a bad one is
|
||||
# rejected. If the repository ever goes private this is the first line to
|
||||
# check; the failure mode is a loud rc=1, never a silent fallback.
|
||||
$pair = 'x-access-token:' + $env:DTU_REPO_TOKEN
|
||||
$b64 = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair))
|
||||
$auth += @('-c', ('http.extraheader=AUTHORIZATION: Basic ' + $b64))
|
||||
}
|
||||
$f = Invoke-DtuGit -G ($auth + @('fetch', '--prune', '--no-tags', 'origin'))
|
||||
if ($f.Code -ne 0) {
|
||||
$f.Lines | ForEach-Object { Write-Host " $_" }
|
||||
Write-Host ('::error::git fetch origin failed (exit ' + $f.Code + ')')
|
||||
exit 1
|
||||
}
|
||||
# Some event refs (PR merge commits, refs outside refs/heads) are not covered
|
||||
# by the branch refspec -- fall back to an explicit ref fetch, then INSIST the
|
||||
# wanted SHA is really present rather than silently building the wrong tree.
|
||||
$want = $env:DTU_REPO_SHA + '^{commit}'
|
||||
$chk = Invoke-DtuGit -G @('cat-file', '-e', $want)
|
||||
if ($chk.Code -ne 0) {
|
||||
Write-Host 'sha not covered by the branch refspec -- fetching the event ref explicitly'
|
||||
$f2 = Invoke-DtuGit -G ($auth + @('fetch', '--prune', '--no-tags', 'origin', $env:DTU_REPO_REF))
|
||||
if ($f2.Code -ne 0) { $f2.Lines | ForEach-Object { Write-Host " $_" } }
|
||||
$chk = Invoke-DtuGit -G @('cat-file', '-e', $want)
|
||||
}
|
||||
if ($chk.Code -ne 0) {
|
||||
Write-Host ('::error::' + $env:DTU_REPO_SHA + ' is not present in the fetched history')
|
||||
Write-Host ' (refusing to run the gates against a tree we did not verify)'
|
||||
exit 1
|
||||
}
|
||||
$c = Invoke-DtuGit -G @('checkout', '-q', '--detach', $env:DTU_REPO_SHA)
|
||||
if ($c.Code -ne 0) { $c.Lines | ForEach-Object { Write-Host " $_" }; Write-Host '::error::detached checkout failed'; exit 1 }
|
||||
$rr = Invoke-DtuGit -G @('reset', '--hard', '-q', $env:DTU_REPO_SHA)
|
||||
if ($rr.Code -ne 0) { Write-Host '::error::reset --hard failed'; exit 1 }
|
||||
# -ffd, deliberately not -ffdx: -x would delete IGNORED files. Within ONE job several
|
||||
# hooks share this directory (pip / the simulator build tree), and local reruns reuse
|
||||
# it. NOTE measured 2026-10-27: CI gives every job a FRESH workspace (the L0 dirs of
|
||||
# run 44/45/46 all differ, each paying a full 137-161 s fetch), so -ffd is a safe
|
||||
# default, not a speed trick.
|
||||
$cl = Invoke-DtuGit -G @('clean', '-ffd', '-q')
|
||||
if ($cl.Code -ne 0) { $cl.Lines | ForEach-Object { Write-Host " $_" }; Write-Host '::error::git clean failed'; exit 1 }
|
||||
$head = (Invoke-DtuGit -G @('rev-parse', '--short', 'HEAD')).Lines -join ''
|
||||
$ncommits = (Invoke-DtuGit -G @('rev-list', '--count', 'HEAD')).Lines -join ''
|
||||
$dirty = @(Invoke-DtuGit -G @('status', '--porcelain')).Lines.Count
|
||||
Write-Host ('checkout ok: ' + $head + ' | commits=' + $ncommits + ' | dirty_files=' + $dirty)
|
||||
- name: 环境自检(Python / cppcheck / pre-commit)
|
||||
shell: powershell
|
||||
run: |
|
||||
@@ -235,28 +361,32 @@ jobs:
|
||||
id: skip
|
||||
shell: powershell
|
||||
run: |
|
||||
# mem-numbers fact source is the Keil build artifact .map (gitignored).
|
||||
# No .map => this hook cannot run => skip (not "pass", but "not applicable").
|
||||
# mem-numbers / mem-budget fact source is the Keil build artifact .map
|
||||
# (gitignored). No .map => those hooks cannot run => skip them EXPLICITLY.
|
||||
#
|
||||
# S14 (2026-10-02): make this GAP loud. mem-numbers is the ONLY gate that
|
||||
# checks docs/memory numbers against the real .map (Flash/RAM size). When
|
||||
# it is skipped, CI effectively does NOT guard the size budget -- a silent
|
||||
# ::notice:: was too easy to miss. So:
|
||||
# - emit ::warning:: (not ::notice::) so it shows in the run summary
|
||||
# - write a machine-readable marker file for the summary step
|
||||
# - append a bold line to the job step-summary
|
||||
# 2026-10-25: two honesty fixes here.
|
||||
# 1) mem-budget used NOT to be listed, yet it self-skips inside the script
|
||||
# when .map is missing (exit 0) -- so on a Keil-less runner it reported
|
||||
# "Passed" while checking nothing. A gate that fakes green is worse than
|
||||
# a gate that is visibly skipped: skip it here so the report says Skipped.
|
||||
# 2) "skipped" alone is still the blind spot, so the compensation is a
|
||||
# real step, not only a warning: see the "size gate" step at the end of
|
||||
# this job (scripts/check_size_gate.py --ci, no Keil needed: attestation
|
||||
# parity + 92% re-check on the recorded numbers). Boundary and rationale
|
||||
# live in that script docstring (single source of truth).
|
||||
$map = Get-ChildItem -Path 'MDK-ARM' -Recurse -Filter '*.map' -ErrorAction SilentlyContinue
|
||||
if ($map) {
|
||||
Write-Host "found .map: $($map[0].FullName) -- mem-numbers runs normally"
|
||||
Write-Host "found .map: $($map[0].FullName) -- size hooks run normally"
|
||||
"value=" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||||
"sizegate=on" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||||
"> Volume gate (mem-numbers): RAN (.map present)" |
|
||||
"> Volume gates (mem-numbers, mem-budget): RAN (.map present)" |
|
||||
Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
|
||||
} else {
|
||||
Write-Host "::warning::no .map found (no Keil build) -- SKIPPING mem-numbers, so the Flash/RAM size gate does NOT run in CI. Size changes are NOT caught here; verify locally with 'python scripts/check_mem_numbers.py --scan' after a Keil build."
|
||||
"value=mem-numbers" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||||
Write-Host "::warning::no .map found (no Keil build) -- SKIPPING mem-numbers and mem-budget, so absolute Flash/RAM numbers are NOT recomputed here."
|
||||
Write-Host "::warning::that gap is no longer silent: the final step of this job runs scripts/check_size_gate.py --ci (forced path: attestation parity + 92% budget re-check). Locally: scripts/ci_local.py step L0c."
|
||||
"value=mem-numbers,mem-budget" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||||
"sizegate=off" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding ascii
|
||||
"> :warning: Volume gate (mem-numbers): **NOT RUN** -- no `.map` (no Keil build)." |
|
||||
"> :warning: Volume gates (mem-numbers, mem-budget): **NOT RUN** -- no `.map` (no Keil build); compensated by the size-gate step below." |
|
||||
Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append -Encoding utf8
|
||||
}
|
||||
|
||||
@@ -302,7 +432,7 @@ jobs:
|
||||
if: always()
|
||||
shell: powershell
|
||||
run: |
|
||||
# S3: zero-dependency artifact archival. We do NOT use
|
||||
# Result visibility: zero-dependency artifact archival. We do NOT use
|
||||
# actions/upload-artifact (inner networks may not fetch outer actions);
|
||||
# instead we keep the log on the runner AND surface its path + tail in
|
||||
# the step summary, so the UI is self-sufficient for triage.
|
||||
@@ -314,6 +444,71 @@ jobs:
|
||||
}
|
||||
Write-Host "::notice::L0 artifacts kept on runner at: ci-artifacts\l0-pre-commit.log"
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# 强制体积门禁(2026-10-25):不需要 Keil,也不需要 .map
|
||||
# 放在 L0 作业**最后**且 if: always()——L0 失败时这条结论同样要看得到,
|
||||
# 而它自己失败会让整个作业变红(这正是"强制"的含义)。
|
||||
- name: 体积门禁(强制路径:同码凭证 + 92% 红线,无需 Keil)
|
||||
if: always()
|
||||
shell: powershell
|
||||
env:
|
||||
GATE_REF: ${{ github.ref }}
|
||||
GATE_EVENT: ${{ github.event_name }}
|
||||
run: |
|
||||
# Pure ASCII only (see file header).
|
||||
#
|
||||
# WHY THIS STEP EXISTS: mem-numbers / mem-budget read the Keil .map and
|
||||
# self-skip (exit 0) without it, so "not measured" used to look like
|
||||
# "passed" -- that is exactly the blind spot ("CI all green does not
|
||||
# mean the size budget held"). Merely declaring it did not close it.
|
||||
#
|
||||
# This step enforces a check that needs NO Keil and NO .map:
|
||||
# * parity -- scripts/size_attestation.json (a committed attestation
|
||||
# that --stamp refuses to write unless the .map is present, newer
|
||||
# than every source file in scope, and inside budget) must
|
||||
# fingerprint-match THIS checked-out tree => this exact code was
|
||||
# linked for real at least once;
|
||||
# * budget -- the region percentages that measurement recorded must
|
||||
# stay within the same 92% line mem-budget uses.
|
||||
#
|
||||
# Enforcement point = trunk push, the only ref guaranteed to be the code
|
||||
# we ship. Task branches / WIP pull requests run --warn-only instead: the
|
||||
# code there is still moving, and blocking it would push every lane to
|
||||
# edit the same scripts/ ledger file at the same time.
|
||||
New-Item -ItemType Directory -Force -Path 'ci-artifacts' | Out-Null
|
||||
$log = 'ci-artifacts\size-gate.log'
|
||||
if (-not $env:PYTHON) {
|
||||
# The environment probe step already failed in that case, so the job is
|
||||
# red anyway; do not mask that with a second, unrelated error.
|
||||
Write-Host "::warning::PYTHON unresolved (env step failed) -- size gate cannot run"
|
||||
exit 0
|
||||
}
|
||||
$trunks = @('refs/heads/v2-on-ui', 'refs/heads/master', 'refs/heads/main')
|
||||
$enforce = ($env:GATE_EVENT -eq 'push') -and ($trunks -contains $env:GATE_REF)
|
||||
if ($enforce) {
|
||||
$mode = @()
|
||||
$label = 'ENFORCING (trunk push)'
|
||||
} else {
|
||||
$mode = @('--warn-only')
|
||||
$label = 'advisory (non-trunk event)'
|
||||
}
|
||||
$saved = $ErrorActionPreference
|
||||
$ErrorActionPreference = 'Continue'
|
||||
& $env:PYTHON -s scripts/check_size_gate.py --ci @mode 2>&1 |
|
||||
Tee-Object -FilePath $log
|
||||
$rc = $LASTEXITCODE
|
||||
$ErrorActionPreference = $saved
|
||||
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "" -Encoding utf8
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "### Volume gate (forced path)" -Encoding utf8
|
||||
Add-Content -Path $env:GITHUB_STEP_SUMMARY -Value "size gate: rc=$rc mode=$label (attestation parity + 92% re-check; no Keil)" -Encoding utf8
|
||||
Write-Host "::notice::size gate: rc=$rc mode=$label (log: $log)"
|
||||
if ($rc -ne 0) {
|
||||
Write-Host "::error::volume gate FAILED: this tree has no matching real Keil measurement, or the measured regions exceed the 92% budget."
|
||||
Write-Host "::error::fix: run the Keil build locally (0 errors 0 warnings), then 'python scripts/ram_baseline.py --update', and commit scripts/size_attestation.json (scripts/ram_baseline.tsv is refreshed by the same command). Rationale/boundary: scripts/check_size_gate.py docstring."
|
||||
exit $rc
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# L0.5:主机侧纯逻辑单测(零硬件;缺 MSVC 则跳过)
|
||||
# ------------------------------------------------------------------
|
||||
@@ -321,8 +516,128 @@ jobs:
|
||||
name: L0.5 主机侧单测 (MSVC)
|
||||
runs-on: windows
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: 检出源码(原生 git;与 l0-gates 同一实现口径)
|
||||
shell: powershell
|
||||
env:
|
||||
DTU_REPO_URL: ${{ github.server_url }}/${{ github.repository }}.git
|
||||
DTU_REPO_REF: ${{ github.ref }}
|
||||
DTU_REPO_SHA: ${{ github.sha }}
|
||||
DTU_REPO_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
# Pure ASCII only (see file header).
|
||||
#
|
||||
# WHY NOT a "uses:" action: the runner resolves every uses: by contacting the
|
||||
# actions URL during "Prepare all required actions" -- BEFORE the first step
|
||||
# runs. On this intranet runner that call intermittently dies with a connect
|
||||
# timeout: runs 36 (10-21) / 41 (10-25) / 42 (10-26) each burned ~45 s and
|
||||
# produced ZERO hook output, while a manual curl to the same URL minutes later
|
||||
# returned 200. Manual reachability is not CI health: the resolver runs as
|
||||
# SYSTEM and the runner caches nothing across jobs. Gitea 1.25.4 also removed
|
||||
# the custom DEFAULT_ACTIONS_URL knob, so the only fix this repo owns is to
|
||||
# stop depending on the public resolver at all.
|
||||
#
|
||||
# KEEP IN SYNC with the same step in the other job. This logic deliberately
|
||||
# does NOT live in scripts/*.ps1 (unlike the Python / MSVC probes): the file is
|
||||
# not checked out yet at the moment it would be needed -- chicken and egg.
|
||||
#
|
||||
# $ErrorActionPreference is set to Continue EXPLICITLY. The runner executes a
|
||||
# run: block by dot-sourcing it into a session, so an inherited Stop turns any
|
||||
# raw native stderr line into a TERMINATING NativeCommandError. Measured
|
||||
# 2026-10-27, run 44 / task-72: a benign "error: No such remote 'origin'" from
|
||||
# a probe killed the whole job 3 seconds in, before any gate could run.
|
||||
$ErrorActionPreference = 'Continue'
|
||||
function Invoke-DtuGit {
|
||||
# Single funnel for git. Two reasons it must be a funnel, not bare calls:
|
||||
# 1) native stderr has to be flattened into plain strings (see above), and
|
||||
# 2) $LASTEXITCODE has to stay readable by the caller afterwards.
|
||||
# Returns Code + lines and lets the caller decide what to print.
|
||||
param([string[]]$G)
|
||||
$lines = & git @G 2>&1 | ForEach-Object { [string]$_ }
|
||||
return [pscustomobject]@{ Code = $LASTEXITCODE; Lines = @($lines) }
|
||||
}
|
||||
if (-not $env:DTU_REPO_SHA) { Write-Host '::error::DTU_REPO_SHA is empty'; exit 1 }
|
||||
if (-not $env:DTU_REPO_URL) { Write-Host '::error::DTU_REPO_URL is empty'; exit 1 }
|
||||
# Never let a fetch wait for a human: the runner has no console, and a
|
||||
# credential prompt would burn the whole job timeout while looking like a hung
|
||||
# CI. Anonymous fetch works here (the repository is public; measured HTTP 200
|
||||
# on /info/refs?service=git-upload-pack), so CI must not depend on a credential
|
||||
# store it cannot answer.
|
||||
$env:GIT_TERMINAL_PROMPT = '0'
|
||||
$ws = (Get-Location).Path
|
||||
Write-Host "workspace: $ws"
|
||||
if (-not (Test-Path -LiteralPath (Join-Path $ws '.git'))) {
|
||||
$r = Invoke-DtuGit -G @('init', '-q', '.')
|
||||
if ($r.Code -ne 0) { Write-Host '::error::git init failed'; $r.Lines | ForEach-Object { Write-Host " $_" }; exit 1 }
|
||||
}
|
||||
# "git remote get-url" writes to stderr when the remote is absent -- that is
|
||||
# the exact line that killed run 44. Ask the config instead: an absent key
|
||||
# prints nothing and only sets a non-zero code, so nothing escapes as an error.
|
||||
$probe = Invoke-DtuGit -G @('config', '--get', 'remote.origin.url')
|
||||
$url = ($probe.Lines -join '').Trim()
|
||||
if ($probe.Code -eq 0 -and $url) {
|
||||
$r = Invoke-DtuGit -G @('remote', 'set-url', 'origin', $env:DTU_REPO_URL)
|
||||
} else {
|
||||
$r = Invoke-DtuGit -G @('remote', 'add', 'origin', $env:DTU_REPO_URL)
|
||||
}
|
||||
if ($r.Code -ne 0) { Write-Host '::error::configuring remote origin failed'; $r.Lines | ForEach-Object { Write-Host " $_" }; exit 1 }
|
||||
# Full refspec, NOT --depth 1: the commit-message gate resolves base..head
|
||||
# ranges and several gates diff against history, so a shallow clone turns them
|
||||
# into "unknown revision" failures. Tags are skipped on purpose: this repository
|
||||
# has zero tags and no script uses "git describe --tags" (measured 2026-10-27).
|
||||
$r = Invoke-DtuGit -G @('config', 'remote.origin.fetch', '+refs/heads/*:refs/remotes/origin/*')
|
||||
if ($r.Code -ne 0) { Write-Host '::error::setting remote.origin.fetch failed'; exit 1 }
|
||||
# The token, when the instance provides one, travels only as a per-invocation
|
||||
# -c override: it never lands in .git/config and is never echoed in logs.
|
||||
$auth = @('-c', 'credential.helper=', '-c', 'credential.interactive=false')
|
||||
if ($env:DTU_REPO_TOKEN) {
|
||||
# Same scheme actions/checkout uses against Gitea git-http: Basic with a
|
||||
# placeholder user and the token as the password. (Bearer is the GitHub API
|
||||
# style, not the git-http style.) Honest caveat: this branch CANNOT be
|
||||
# verified here -- the repository is public, so an anonymous fetch succeeds
|
||||
# and any token we fabricate locally only proves that a bad one is
|
||||
# rejected. If the repository ever goes private this is the first line to
|
||||
# check; the failure mode is a loud rc=1, never a silent fallback.
|
||||
$pair = 'x-access-token:' + $env:DTU_REPO_TOKEN
|
||||
$b64 = [System.Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($pair))
|
||||
$auth += @('-c', ('http.extraheader=AUTHORIZATION: Basic ' + $b64))
|
||||
}
|
||||
$f = Invoke-DtuGit -G ($auth + @('fetch', '--prune', '--no-tags', 'origin'))
|
||||
if ($f.Code -ne 0) {
|
||||
$f.Lines | ForEach-Object { Write-Host " $_" }
|
||||
Write-Host ('::error::git fetch origin failed (exit ' + $f.Code + ')')
|
||||
exit 1
|
||||
}
|
||||
# Some event refs (PR merge commits, refs outside refs/heads) are not covered
|
||||
# by the branch refspec -- fall back to an explicit ref fetch, then INSIST the
|
||||
# wanted SHA is really present rather than silently building the wrong tree.
|
||||
$want = $env:DTU_REPO_SHA + '^{commit}'
|
||||
$chk = Invoke-DtuGit -G @('cat-file', '-e', $want)
|
||||
if ($chk.Code -ne 0) {
|
||||
Write-Host 'sha not covered by the branch refspec -- fetching the event ref explicitly'
|
||||
$f2 = Invoke-DtuGit -G ($auth + @('fetch', '--prune', '--no-tags', 'origin', $env:DTU_REPO_REF))
|
||||
if ($f2.Code -ne 0) { $f2.Lines | ForEach-Object { Write-Host " $_" } }
|
||||
$chk = Invoke-DtuGit -G @('cat-file', '-e', $want)
|
||||
}
|
||||
if ($chk.Code -ne 0) {
|
||||
Write-Host ('::error::' + $env:DTU_REPO_SHA + ' is not present in the fetched history')
|
||||
Write-Host ' (refusing to run the gates against a tree we did not verify)'
|
||||
exit 1
|
||||
}
|
||||
$c = Invoke-DtuGit -G @('checkout', '-q', '--detach', $env:DTU_REPO_SHA)
|
||||
if ($c.Code -ne 0) { $c.Lines | ForEach-Object { Write-Host " $_" }; Write-Host '::error::detached checkout failed'; exit 1 }
|
||||
$rr = Invoke-DtuGit -G @('reset', '--hard', '-q', $env:DTU_REPO_SHA)
|
||||
if ($rr.Code -ne 0) { Write-Host '::error::reset --hard failed'; exit 1 }
|
||||
# -ffd, deliberately not -ffdx: -x would delete IGNORED files. Within ONE job several
|
||||
# hooks share this directory (pip / the simulator build tree), and local reruns reuse
|
||||
# it. NOTE measured 2026-10-27: CI gives every job a FRESH workspace (the L0 dirs of
|
||||
# run 44/45/46 all differ, each paying a full 137-161 s fetch), so -ffd is a safe
|
||||
# default, not a speed trick.
|
||||
$cl = Invoke-DtuGit -G @('clean', '-ffd', '-q')
|
||||
if ($cl.Code -ne 0) { $cl.Lines | ForEach-Object { Write-Host " $_" }; Write-Host '::error::git clean failed'; exit 1 }
|
||||
$head = (Invoke-DtuGit -G @('rev-parse', '--short', 'HEAD')).Lines -join ''
|
||||
$ncommits = (Invoke-DtuGit -G @('rev-list', '--count', 'HEAD')).Lines -join ''
|
||||
$dirty = @(Invoke-DtuGit -G @('status', '--porcelain')).Lines.Count
|
||||
Write-Host ('checkout ok: ' + $head + ' | commits=' + $ncommits + ' | dirty_files=' + $dirty)
|
||||
- name: 探测 MSVC(VS BuildTools / Community 等)
|
||||
id: msvc
|
||||
shell: powershell
|
||||
@@ -456,7 +771,7 @@ jobs:
|
||||
if: always()
|
||||
shell: powershell
|
||||
run: |
|
||||
# S3: same zero-dependency archival as L0.
|
||||
# Result visibility: same zero-dependency archival as L0.
|
||||
if (Test-Path 'ci-artifacts') {
|
||||
$logs = Get-ChildItem -Path 'ci-artifacts' -Filter '*.log' -ErrorAction SilentlyContinue
|
||||
foreach ($f in $logs) {
|
||||
|
||||
+24
-3
@@ -120,9 +120,14 @@ simulator/SDL2/
|
||||
*.exe
|
||||
*.pdb
|
||||
*.ilk
|
||||
# 内容页黄金参考(ref/*.bin)与目检 PNG 均为本地生成物,不入库:
|
||||
# 首次/换机后先跑 check_content_pages.py --update 生成 ref/*.bin,再跑 regen_ref_png.py 出 PNG
|
||||
simulator/tests/ref/
|
||||
# 内容页像素黄金参考 ref/*.bin **已入库**(2026-10-22):整目录不入库时,任何新克隆 /
|
||||
# 新工作树跑 ui-golden 都必然 FAIL(缺基准),这条像素回归就只在老机器上真实存在。
|
||||
# 交叉验证:两份各自独立构建的 lvgl_sim(不同构建目录、相隔 11 天的代码)对同一批 ref
|
||||
# 逐字节一致 → 渲染可复现,基线可入库。派生的目检 PNG 仍是本地产物,继续忽略:
|
||||
simulator/tests/ref/*.png
|
||||
# ⚠ 上面第 115 行的全仓 `*.bin` 兜底(挡模拟器的 sim_netcfg.bin 之类运行产物)会连入库的
|
||||
# 黄金参考一起挡掉 → 必须显式反选;反选只有排在 `*.bin` **之后**才生效(.gitignore 后者覆盖前者)
|
||||
!simulator/tests/ref/*.bin
|
||||
# 架构图 PNG 预览(源为 docs/images/*.svg,PNG 本地渲染生成,不入库)
|
||||
docs/images/*.png
|
||||
*.pyc
|
||||
@@ -133,6 +138,16 @@ docs/images/*.png
|
||||
# Serial assistant runtime state (daemon log/meta, local runtime artifacts)
|
||||
.dsh/skills/serial-assistant/scripts/state/
|
||||
|
||||
# agent 会话草稿(2026-10-14 定):提交信息草稿 / 门禁输出 / 台架日志 / 一次性改名
|
||||
# 脚本 / 故障注入 patch —— 一律不入库。判据三条,命中任一就不值得留:
|
||||
# ① 能重跑的(门禁输出、日志);② 内容已在 git 历史里的(已合并实现的过程 diff);
|
||||
# ③ 离开已丢弃的任务分支就跑不通的(依赖 NET_FAULT_INJECT 的注入产物)——
|
||||
# 这种提交进去是"看着有证据、实际不可复现"的假证据,**方法应沉淀成文档文字**
|
||||
# (见 docs/网络/网络自愈架构设计.md §5.2 E18/E19)。
|
||||
# ⚠ .dsh/skills/ 是**入库的项目知识**(skills = 子域方法论),必须放行。
|
||||
.dsh/*
|
||||
!.dsh/skills/
|
||||
|
||||
# 记忆工具(Mnemon)本地状态:项目文档索引 documents/ + 运行时记忆
|
||||
# runtime/MEMORY.md、USER.md、memories.json。**只留本机、不进版本库**
|
||||
# (2026-10-11 定):记忆是按机器/按人累积的私有状态,入库会让每次记忆
|
||||
@@ -141,4 +156,10 @@ docs/images/*.png
|
||||
|
||||
# CI 运行时产物(.gitea/workflows/ci.yml 的 S3 步骤在 runner 工作区落盘的日志;
|
||||
# 零依赖存档方案——落在工作区目录而非上传 artifact,故必须忽略以免误提交)
|
||||
# 子进程环境残缺时 Windows 会按字面量建目录(本会话实测两次复发:UV4 / 字体
|
||||
# 工具在缺 SystemDrive、ProgramData 的环境里于仓库根创建
|
||||
# %SystemDrive%/ProgramData/Microsoft/Windows/Caches/*.db,约 1 MB 字体缓存库)。
|
||||
# 与"一次性中间产物不进 .gitignore"那条教训不同:这是任何一条道随时可能再生的环境
|
||||
# 副产物,不兜住就会被某次 git add -A 带进历史。正解仍是继承完整环境跑这类工具。
|
||||
%SystemDrive%/
|
||||
ci-artifacts/
|
||||
|
||||
+352
-20
@@ -1,5 +1,8 @@
|
||||
# pre-commit 自动化代码审查配置
|
||||
# 详见 docs/代码规范/pre-commit使用说明.md
|
||||
# 通用机制(阶段 / stash 陷阱 / 触发面 / 增量拦截三段式)→ docs/代码规范/pre-commit使用说明.md §2
|
||||
# 每个门禁一篇详解(含限制与盲区)→ docs/门禁说明/README.md
|
||||
# 加/删一条钩子:同一笔提交里改 AGENTS.md 的「共 N 个钩子」与本文件注释;口径 = YAML 解析后的 hook 条目数,
|
||||
# 自查命令与「声明 / 文档 / 实跑」三数同源核对 → docs/门禁说明/README.md §2(直接数文本行会把下面注释掉的候选条目算进去)
|
||||
default_install_hook_types: [pre-commit, commit-msg]
|
||||
|
||||
# 顶层 exclude 只挡**第三方与生成物**。
|
||||
@@ -8,8 +11,15 @@ default_install_hook_types: [pre-commit, commit-msg]
|
||||
# tests/*.py、两份配置与 README)。整目录豁免 = 自研代码落在所有钩子之外(连
|
||||
# lv-conf-sync 都拦不到);实测把 simulator/ 纳入 cppcheck 后,立刻揪出
|
||||
# sim_syslog.c 一处真实的空指针解引用。真正的生成物/第三方(`simulator/build/`、
|
||||
# `simulator/SDL2/`、`simulator/tests/ref/`、`simulator/shot*.{bin,png}`、`__pycache__/`)
|
||||
# **已在 .gitignore 里**,永远不会进 git 索引 → 无需在此重复豁免,故直接删掉该条。
|
||||
# `simulator/SDL2/`、`simulator/shot*.{bin,png}`、`__pycache__/`)**已在 .gitignore 里**,
|
||||
# 不会进 git 索引 → 无需在此重复豁免,故直接删掉该条。
|
||||
# ⚠ 例外(2026-10-22):`simulator/tests/ref/*.bin`(ui-golden 的像素黄金参考)已从
|
||||
# 「整目录 gitignore」改为**入库**——基线不入库时任何新克隆 / 新工作树都必然报
|
||||
# 「缺黄金参考」,这条门禁就只在有老副本的机器上真实存在。入库前提是实测可复现:
|
||||
# 两份各自独立构建的 lvgl_sim(不同构建目录、相隔 11 天的代码)对同一批 11 个 .bin
|
||||
# SHA-256 全部一致。改写型钩子(trailing-whitespace / end-of-file-fixer)声明
|
||||
# `types: [text]`,位图含 NUL → 被判二进制自动 Skipped(实测「索引 blob == 磁盘 ==
|
||||
# 参照」三方一致),无需为它另加 exclude。
|
||||
exclude: |
|
||||
(?x)^(
|
||||
\.opencode/node_modules/|
|
||||
@@ -152,11 +162,11 @@ repos:
|
||||
Inc/
|
||||
)
|
||||
|
||||
# 5) 文档一致性门禁(D-7)
|
||||
# 5) 文档一致性门禁
|
||||
# 存在性:md 互链 / 反引号路径是否指向真实文件(防迁移/删除后残留死链)
|
||||
# 章节:`§N.M` 必须能在目标文档的编号标题里找到(--sections-strict:自引用失效也拦)
|
||||
- id: doc-links
|
||||
name: 文档链接存在性 (D-3/D-7)
|
||||
name: 文档链接存在性
|
||||
entry: python scripts/check_doc_links.py --sections-strict
|
||||
language: system
|
||||
files: \.md$
|
||||
@@ -169,23 +179,23 @@ repos:
|
||||
MDK-ARM/RTE/|
|
||||
docs/芯片手册/
|
||||
)
|
||||
# 数值:README/AGENTS 任务表须与 Src/freertos.c 一致(D-2/D-7)
|
||||
# 数值:README/AGENTS 任务表须与 Src/freertos.c 一致
|
||||
- id: task-table
|
||||
name: FreeRTOS 任务表一致性 (D-2/D-7)
|
||||
name: FreeRTOS 任务表一致性
|
||||
entry: python scripts/gen_task_table.py --check
|
||||
language: system
|
||||
files: ^(README\.md|AGENTS\.md|Src/freertos\.c)$
|
||||
pass_filenames: false
|
||||
# 体积:内存/Flash 数字须与最新 .map 一致(D-7 第三档)
|
||||
# 体积:内存/Flash 数字须与最新 .map 一致(体积档)
|
||||
# 只在内存文档或代码变动时触发;.map 未生成时脚本自身给出明确提示
|
||||
- id: mem-numbers
|
||||
name: 内存/Flash 数字一致性 (D-7 体积档)
|
||||
name: 内存/Flash 数字一致性(体积档)
|
||||
entry: python scripts/check_mem_numbers.py --scan
|
||||
language: system
|
||||
files: ^(docs/内存管理/.*\.md|scripts/check_mem_numbers\.py|App/.*\.c|App/.*\.h|Src/.*\.c|Inc/.*\.h)$
|
||||
pass_filenames: false
|
||||
|
||||
# 5d) 两份 lv_conf.h 特性子集一致性(2026-10-11 加,随 F-1/F-2/F-3 落地)
|
||||
# 5d) 两份 lv_conf.h 特性子集一致性(2026-10-11 加)
|
||||
# 背景:`Lib/lv_conf.h`(MCU)与 `simulator/lv_conf.h`(PC)是两份独立文件,
|
||||
# 此前长期漂移——模拟器早已 I1-only,MCU 端却让 11 个 `LV_DRAW_SW_SUPPORT_*`、
|
||||
# 5 个未用控件类、`LV_USE_THEME_SIMPLE`、`LV_USE_FLEX/GRID` 靠 lv_conf_internal
|
||||
@@ -304,8 +314,23 @@ repos:
|
||||
files: ^(App/fs/vol_lock\.(c|h)|test/host/vol_lock/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7f2) 卷空闲同步(写回缓存空闲 flush)主机侧单元测试(2026-10-25 加,道F 批 2)
|
||||
# 背景:该模块是写回缓存掉电窗口的兜底网——掉电与"脏页驻留"本身台架
|
||||
# 不可按需构造;卷忙跳过 / ioctl 失败边沿 latch / 32 位 tick 回绕 /
|
||||
# 失败路径成对放锁(不得空放)全是纯状态逻辑。本钩子用 MSVC 离线编译
|
||||
# 真实源文件(#include 纳入;桩:可控时钟 + 事件位图 + 卷忙注入 +
|
||||
# 可注入 disk_ioctl),把四级门与 latch 分支一次钉死。
|
||||
# files 含被测源与其头、就绪位属主 sys_events 的消费面 default_task.c、
|
||||
# 以及 harness 本身。
|
||||
- id: vol-idle-sync-unit-test
|
||||
name: 卷空闲同步单测(主机侧)
|
||||
entry: python test/host/vol_idle_sync/run_unit_test.py
|
||||
language: system
|
||||
files: ^(App/fs/vol_idle_sync\.(c|h)|App/task/default_task\.c|test/host/vol_idle_sync/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7g) 时间同步(time_sync)主机侧单元测试(2026-10-10 加)
|
||||
# 背景:time_sync.c 是 TCP 时间同步协议状态机,也是 N-17 号看门狗风险的**全部
|
||||
# 背景:time_sync.c 是 TCP 时间同步协议状态机,也是探针窗口心跳缺口那项看门狗风险的**全部
|
||||
# 逻辑贡献者**——"listen 重建 -> 800ms~1s 心跳间隔"由本模块的分片延时/退避/
|
||||
# 结算等待组成,板端只能看到"是否复位"这一结果,看不到各段延时是否如设计。本钩子
|
||||
# 用 MSVC 离线编译真实源文件(#include 纳入)+ 可脚本化的网络/事件桩(含 longjmp
|
||||
@@ -333,6 +358,57 @@ repos:
|
||||
files: ^(App/algo/adc_health\.(c|h)|App/task/adc_task\.c|test/host/adc_health/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7h2) 通道标定层(L1.5,「数据可信」卡 2/卡 4)主机侧单元测试(2026-10-25 加,道A)
|
||||
# 背景:标定折算是"码值→工程量"的唯一插入点,Q16 系数/零点两段落盘 +
|
||||
# 合法带哨兵回退 + 两点标定公式——板端无标准源、坏文件注入路径在真机
|
||||
# 上难以复现,只有离线端能逐系数钉死。钩子用 MSVC 编译真实源文件
|
||||
# (#include 纳入;桩:dbg_log / cfg_file_register / adc_input_desc 强符号)。
|
||||
# files 含被测源与头、系数口径联动的 adc_health/cfg_file/adc_task 消费方,
|
||||
# 以及 harness 本身。
|
||||
- id: adc-cal-unit-test
|
||||
name: 通道标定层单测(主机侧)
|
||||
entry: python test/host/adc_cal/run_unit_test.py
|
||||
language: system
|
||||
files: ^(App/algo/adc_cal\.(c|h)|App/algo/adc_health\.(c|h)|App/cfg/cfg_file\.(c|h)|App/task/adc_task\.c|test/host/adc_cal/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
# 7h3) 量纲回归(「数据可信」卡 7,道A)主机侧单元测试(2026-10-25 加)
|
||||
# 背景:台架曾长期缺"不该动作时不动作"的判据,码值/工程量错配曾以
|
||||
# 恒假动作的形态混过验收。本钩子用真实链(adc_cal→ring→phasor→
|
||||
# prot_elm)+ 显式换算的合成波把七类元件的量纲钉死:折算点被旁路
|
||||
# 或量纲再错位,断言立刻翻转。files 覆盖链上全部生产源与两处编排。
|
||||
- id: prot-dim-unit-test
|
||||
name: 量纲回归单测(主机侧)
|
||||
entry: python test/host/prot_dim/run_unit_test.py
|
||||
language: system
|
||||
files: ^(App/algo/(adc_cal|prot_elm|prot_set|phasor|sample_ring|freq)\.(c|h)|App/task/(adc_task|protect_task)\.c|test/host/prot_dim/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7h4) 遥测快照层(L2 发布侧,「数据可信」卡 8,道D)主机侧单元测试(2026-10-25 加)
|
||||
# 背景:卡 8 的 DoD 一半是并发与结构类判据——双区切换无撕裂(撕裂窗
|
||||
# 200 ms 量级,台架人工比对观测不到)、seq 只增、§4.5 布局与字段偏移
|
||||
# (卡 9/UI 直接按整体拷贝消费,破布局 = 破协议)、CLIPPED 品质汇总
|
||||
# (削顶位图要等卡 12 标定才真实出现)。主机侧以 #include 原样纳入
|
||||
# meas/phasor/sample_ring 生产源,合成 9 通道帧序列一次性钉死;
|
||||
# 读数与注入源比对类判据仍归台架(实测采样类)。files 覆盖被测源、
|
||||
# 同链核(phasor/sample_ring)、编排方 adc_task 与 harness 本身。
|
||||
- id: meas-unit-test
|
||||
name: 遥测快照层单测(主机侧)
|
||||
entry: python test/host/meas/run_unit_test.py
|
||||
language: system
|
||||
files: ^(App/algo/meas\.(c|h)|App/algo/(phasor|sample_ring)\.(c|h)|App/task/adc_task\.c|test/host/meas/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7h5) 运行参数段(「数据可信」卡 10,道D)主机侧单元测试(2026-10-25 加)
|
||||
# 死区把守突发上报的量、104 整定段是现场唯一远程写入口:注册契约、
|
||||
# 分档量程守卫、两段式、系数直通的合法带与生效值回显必须离线钉死。
|
||||
- id: run-par-unit-test
|
||||
name: 运行参数段单测(主机侧)
|
||||
entry: python test/host/run_par/run_unit_test.py
|
||||
language: system
|
||||
files: ^(App/algo/run_par\.(c|h)|App/algo/adc_cal\.(c|h)|App/cfg/cfg_file(_store)?\.(c|h)|test/host/run_par/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
|
||||
# 7i) 保护定值模型(P1 / L5-1 载体)主机侧单元测试(2026-10-11 加)
|
||||
# 定值是保护动作的"输入宪法":范围校验、整表装载 all-or-nothing、
|
||||
# 校验和封存-自检-闭锁(参考 tagPRS dwTagSum/dwTagSumN 模式)必须在
|
||||
@@ -366,7 +442,7 @@ repos:
|
||||
files: ^(App/algo/freq\.(c|h)|App/algo/phasor\.(c|h)|App/algo/sample_ring\.(c|h)|test/host/freq/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7k) SHA-256 主机侧单元测试(2026-10-11 加,N-19 FTP 鉴权)
|
||||
# 7k) SHA-256 主机侧单元测试(2026-10-11 加,FTP 鉴权)
|
||||
# sha256 是 FTP pwd_hash 校验的地基——哈希算错是"正确口令被拒 /
|
||||
# 错误口令放行"的静默语义错误,板端只见 530/230 看不出根因;
|
||||
# FIPS 180-4 标准向量(空串/abc/56B 补位跨界/1MB)+ hex_eq 非法输入
|
||||
@@ -378,7 +454,7 @@ repos:
|
||||
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)
|
||||
# 7l) FTP 鉴权状态机主机侧单元测试(2026-10-11 加,FTP 鉴权)
|
||||
# 锁定 60s/解锁/成功清零/tick 32 位回绕是"防口令爆破"契约核心,
|
||||
# 板端每验一条要真等 60s、回绕要跑满 49.7 天;lftpd_auth 纯函数
|
||||
# 设计(tick 参数注入)就是为了让这些分支主机侧毫秒级穷举。
|
||||
@@ -392,14 +468,15 @@ repos:
|
||||
# 7m) IEC 60870-5-104 被控站协议栈主机侧单元测试(2026-10-12 加,B4)
|
||||
# APCI 重组/STARTDT/总召分片/COV/k-w-t1~t3/遥控定值两段式/C_CS/错误
|
||||
# 应答——link 层 tick 注入 + pts 数据源 vtable 抽象就是为 L0.5 全离线
|
||||
# 穷举设计(A-4 约束 1);板端复现这些分支要真主站+真等待。
|
||||
# 穷举设计(分层约束);板端复现这些分支要真主站+真等待。
|
||||
# 触发面含数据源依赖(prot_set/prot_elm/phasor/sample_ring)——测试
|
||||
# 直接 #include 这些生产源,签名变了要立刻在这里暴露。
|
||||
- id: iec104-unit-test
|
||||
name: IEC104 协议栈单测(主机侧)
|
||||
entry: python test/host/iec104/run_unit_test.py
|
||||
language: system
|
||||
files: ^(App/protocol/iec104/iec104_(asdu|pts|link)\.(c|h)|App/algo/(prot_set|prot_elm|phasor|sample_ring|adc_input|freq)\.(c|h)|test/host/iec104/.*|test/host/common/unit_test_harness\.py)$
|
||||
# 卡 10 起本 TU 链入真实 cfg_file/adc_cal/run_par(板端真源链),触发面同步扩
|
||||
files: ^(App/protocol/iec104/iec104_(asdu|pts|link)\.(c|h)|App/algo/(prot_set|prot_elm|phasor|sample_ring|adc_input|freq|meas|adc_cal|run_par)\.(c|h)|App/cfg/cfg_file(_store)?\.(c|h)|test/host/iec104/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7m2) SOE 动作记录编解码主机侧单元测试(2026-10-12 加,B9)
|
||||
@@ -414,13 +491,49 @@ repos:
|
||||
files: ^(App/log/soe_rec\.(c|h)|App/log/soe_store\.(c|h)|test/host/soe/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7n) dhara NAND 移植层(nand_ftl)主机侧单元测试(2026-10-15 加)
|
||||
# 背景:移植层六个 HAL 入口是**纯地址算术**,此前对越界页/块号(含
|
||||
# DHARA_PAGE_NONE 哨兵)**不做任何上界校验**——加偏移送进 SPI 命令后被地址位
|
||||
# 截断成某个真实页 → 静默读写/搬走无辜数据;而 is_free 旧实现只看页首 64 B,
|
||||
# 会把"页首 0xFF、后段有数据"的已编程页误判空闲 → dhara 往已编程页再编程
|
||||
# = 数据丢失。两类缺陷真机**极难复现**(要恰好撞上特定页形态 / 被污染的 meta),
|
||||
# 板端只能表现为"某次复位后丢数据",无从定位。
|
||||
# 本钩子用 MSVC 离线编译真实 nand_ftl.c(#include 纳入)+ **RAM 假芯片**
|
||||
# (32 块 x 64 页 x 2048 B):把"双编程"做成可数事件、记录全部
|
||||
# 访问次数,于是"越界不产生任何芯片访问""越界标坏块次数为 0"可机械断言;
|
||||
# 并内联一份"只看页首 64 B"的旧算法做对照,证明这组断言真能咬人。
|
||||
# 触发面含 dhara 本体(测试一并 #include map.c/journal.c 跑真链路场景)。
|
||||
- id: nand-port-unit-test
|
||||
name: dhara NAND 移植层单测(主机侧)
|
||||
entry: python test/host/nand_port/run_unit_test.py
|
||||
language: system
|
||||
files: ^(Drivers/BSP/GD5F2GQ5UE/nand_ftl\.(c|h)|Drivers/BSP/GD5F2GQ5UE/gd5f2gq5ue\.h|Lib/dhara/(map|journal|nand|error|bytes)\.(c|h)|test/host/nand_port/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7z) UI 数据快照层(道E task-14)主机侧单元测试(2026-10-25 加)
|
||||
# 背景:ui_data 是"页面看到的整个世界",但 ui-golden 只覆盖已接线的页面——
|
||||
# 遥信 27 点、定值 40 项、遥测未接线的通道错了不会让任何参考图变红。本钩子
|
||||
# 用 MSVC 离线编译真实 ui_data.c 与 ui_w_soe.c(#include 纳入,零桩),把两件事
|
||||
# 钉死:① 快照存储语义(首帧前读 -1 / 序号由本层盖章 / 三通道互不串台 /
|
||||
# NULL 防御);② 跨层不透明下标与显示名映射的**现场一致性**——测试单元在同一
|
||||
# TU 里同时 include BSP 侧与 App/algo、App/log 真头,逐项比对 adc_semantic_t /
|
||||
# prot_set_id_t / soe_type_t / prot_elm_id_t(解耦换来的"魔法下标"风险,口径
|
||||
# docs/UI设计/UI_数据供给设计说明.md §9.4;SOE 类型含未标定闭锁=7/复归=8)。
|
||||
# files 覆盖被测两件 + 编号真源四头 + 页面消费面(ui_content_page/ui_menu*)+ harness。
|
||||
- id: ui-data-unit-test
|
||||
name: UI 数据快照层单测(主机侧)
|
||||
entry: python test/host/ui_data/run_unit_test.py
|
||||
language: system
|
||||
files: ^(Drivers/BSP/UI/(ui_data|ui_w_soe|ui_w_records|ui_content_page|ui_menu)\.(c|h)|App/app/ui_data_feed\.(c|h)|App/algo/(adc_input|prot_set|prot_elm)\.(c|h)|App/log/soe_rec\.(c|h)|test/host/ui_data/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 8) 测试套件开关门禁(2026-10-01 加)
|
||||
# 背景:test_config.h 的 TEST_SUITE_* 决定固件里编入哪个测试套件,
|
||||
# 全部注释 = 产品构建。但此前**无任何门禁检查**——实测已提交的 HEAD 里
|
||||
# TEST_SUITE_CH395F 就是开启状态(.map 含 34 处 test 代码),即仓库里
|
||||
# 躺着的是测试固件。本钩子抓这个静默漏网:默认要求产品构建,跑套件回归
|
||||
# 确需提交测试构建时用 `--allow-test-build` / ALLOW_TEST_BUILD=1 显式放行。
|
||||
# 详见 docs/任务规划/登记簿.md T-1
|
||||
# 详见 docs/任务规划/登记簿.md
|
||||
- id: test-suite-switch
|
||||
name: 测试套件开关=产品构建
|
||||
entry: python scripts/check_test_suite.py
|
||||
@@ -428,7 +541,7 @@ repos:
|
||||
files: ^test/board/test_config\.h$
|
||||
pass_filenames: false
|
||||
|
||||
# 9) 双端注册反向校验(2026-10-12 加,登记簿 N-23 / 审查报告 N-06 残余)
|
||||
# 9) 双端注册反向校验(2026-10-12 加,登记簿条目「rs485_task 白名单缺口」)
|
||||
# 背景:新 .c 必须手动注册两处(`MDK-ARM/*.uvprojx` + `simulator/CMakeLists.txt`)
|
||||
# 才会被编译;历史上 `App/task/rs485_task.c`、`Drivers/BSP/NET/lftpd/lftpd_log.c`
|
||||
# **两处都没注册** = 写了却从不参与任何一端编译的**死代码**,误导读者以为功能已生效。
|
||||
@@ -445,17 +558,236 @@ repos:
|
||||
language: system
|
||||
files: (\.c$|\.uvprojx$|CMakeLists\.txt$|^scripts/check_registration\.py$)
|
||||
pass_filenames: false
|
||||
# 10) RAM 预算 92% 阈值门禁(2026-10-12 加,登记簿 N-27 前半 / 审查报告 §5.5 三件套)
|
||||
# 10) RAM 预算 92% 阈值门禁(2026-10-12 加,登记簿条目「缩堆迁 CCM 后半(暂缓)」的台账/门禁那半 / 审查报告 §5.5 三件套)
|
||||
# 背景:报告 §5.5 第 1 条要求"超阈值即失败 + Top-10 附表"——此前 mem-numbers 只防
|
||||
# "文档数字漂移",**没有任何机械检查拦"占用超阈值"**;台账三件套(ram_baseline.py +
|
||||
# ram_baseline.tsv + docs/内存管理/RAM预算台账.md)落地后,本钩子把 92% 红线机械化。
|
||||
# 逻辑:RW_IRAM1/RW_CCM 任一 >92% → FAIL + Top-10 RW+ZI 对象表;台账受管区漂移 → FAIL
|
||||
# (提示 --write);无 .map → 自适应跳过(同 mem-numbers 在 CI 的行为,盲区登记 A-5 残余)。
|
||||
# (提示 --write);无 .map → 自适应跳过(同 mem-numbers 在 CI 的行为,盲区已登记)。
|
||||
# ⚠ pre-commit 4.x 无 `always: true` → files 覆盖所有能改变 RAM 的触发面:
|
||||
# 全部自研代码目录 + lv_conf.h + 测试板配置 + 脚本/tsv/台账自身 + 本配置文件。
|
||||
- id: mem-budget
|
||||
name: RAM 预算 92% 阈值门禁 (N-27)
|
||||
name: RAM 预算 92% 阈值门禁
|
||||
entry: python scripts/ram_baseline.py --gate
|
||||
language: system
|
||||
files: ^(App/|Drivers/BSP/|Src/|Inc/|Lib/lv_conf\.h|test/board/|scripts/ram_baseline\.|docs/内存管理/|\.pre-commit-config\.yaml$)
|
||||
pass_filenames: false
|
||||
|
||||
# 11) UI 字符集差集检查(2026-10-15 加,报告 §6.5 的 UI 字符集与像素回归门禁残余 / §7 交付物清单"字符集差集检查"行)
|
||||
# 背景:`ui_font_cn_12.c` 是**子集**字体,只含 `ui_cn_chars.txt` 列出的码位。
|
||||
# 生成链 UI 源码 --extract_cn--> ui_cn_chars.txt --make_font--> 字体 .c,其中
|
||||
# verify_coverage 只在**生成期**校验"txt ⊆ cmap";改完 UI 字符串忘了重跑
|
||||
# font-regen 就提交 → 缺字上机渲染为空白(白框),无日志无报错只能肉眼截图发现
|
||||
# (font-regen skill 的"缺字渲染为空白"约定,此前纯靠自律)。本钩子把它机械化:
|
||||
# A. 源码在用 vs txt 缺 → FAIL(白框方向);多 → WARN(只费 Flash,--strict 升级)
|
||||
# B. txt vs 字体 cmap 缺 → FAIL(lv_font_conv 对缺字形/低码位**静默丢字**)
|
||||
# A 过 + B 过 ⟹ 字体 ⊇ 源码每个在用字。提取与 cmap 解析**复用** tools/font_gen
|
||||
# 两个生成器的同一套规则(importlib 直载 extract_cn.SCAN/collect + make_font.
|
||||
# verify_coverage),杜绝第二套解析规则漂移(§5.5 同类教训)。
|
||||
# ⚠ pre-commit 4.x 已移除 `always: true` → files 必须覆盖全部触发面:
|
||||
# UI 源码(含字体 .c 自身)+ ui_task.{c,h} + 字符集 txt + 两个生成器 + 本脚本。
|
||||
- id: font-charset
|
||||
name: UI 字符集差集检查(防缺字白框)
|
||||
entry: python scripts/check_font_charset.py
|
||||
language: system
|
||||
files: ^(Drivers/BSP/UI/|App/task/ui_task\.(c|h)$|ui_cn_chars\.txt$|tools/font_gen/|scripts/check_font_charset\.py$)
|
||||
pass_filenames: false
|
||||
# 12a) BOM 漂移门禁(2026-10-25 道E task-20)
|
||||
# 背景:AGENTS 铁律第 2 条——编辑工具会**静默剥掉**文件原有 BOM,ARMCC 于是按 CP936
|
||||
# 解码带中文的源文件,报 #870-D / #8 / #274 一类**假语法错**(报错位置与内容都不指向
|
||||
# 真因),而 MSVC(模拟器侧)与既有钩子全照不出来。2026-10-25 一天内道E 踩 4 次、
|
||||
# 道B 批 1 踩 1 次,此前全靠人工逐文件比 HEAD 兜底——人工审计只防得住记得做的人。
|
||||
# 判定:HEAD 有 EF BB BF 而工作区没有 → FAIL。两条刻意不报(新增文件无 BOM、HEAD 无
|
||||
# BOM 而工作区加了 BOM)与一条刻意不做(不自动补:整文件重写正是本仓更大的坑)都
|
||||
# 写在脚本头部。files 与 AGENTS 名单、脚本内 BOM_LIST **三处互指**,脚本每次跑核一次,
|
||||
# 单独改任何一处都会在别人的提交里被拦下——名单散在三处而无人同步等于没有名单。
|
||||
- id: bom-guard
|
||||
name: BOM 漂移检查(带 BOM 文件被剥掉即拦)
|
||||
entry: python scripts/check_bom_drift.py
|
||||
language: system
|
||||
files: ^(Drivers/BSP/UI/|App/task/ui_task\.c$|App/cfg/cfg_file\.c$|Lib/lv_conf\.h$)
|
||||
|
||||
# 12b) 定值查看行表一致性(2026-10-25 道E task-20 追加批;脚本随 task-15 批 3 落)
|
||||
# 核 Drivers/BSP/UI/ui_w_setview.c 的四张视图表与 App/algo/prot_set 的枚举序、
|
||||
# g_prot_set_meta 上下限是否仍是同一份口径:id 恰好覆盖 0..39 各一次、数值行
|
||||
# min/max 等于 meta 换算、kind 与 meta.is_ctl 一致、单位纯 ASCII、小数位查表与行表
|
||||
# 一致。为什么必须机械化:BSP 侧看不到 App 头,行表里的 id 是裸数字 + 行尾注名,
|
||||
# 这条对齐在编译期**物理上无法自证**;而页面上只有当前可见的 8 行会被像素断言看到,
|
||||
# 错位要翻到第 2、3 页才可能被人发现(发现了也已被基线钉死)。
|
||||
# ⚠ 跨分支先后:脚本文件随 task-15 分支合入主干;在此之前 files 永不命中 → 钩子自然
|
||||
# skip(skip 不是失败),合并后自动生效,不需要任何人再动这条配置。
|
||||
- id: setview-check
|
||||
name: 定值查看行表与 prot_set 枚举/上下限一致性
|
||||
entry: python tools/setview_check/check_setview_limits.py
|
||||
language: system
|
||||
files: ^(tools/setview_check/|Drivers/BSP/UI/ui_w_setview\.(c|h)$)
|
||||
pass_filenames: false
|
||||
|
||||
# 12) UI 像素回归门禁(2026-10-20 加,W6 的 UI 字符集与像素回归门禁收尾第三件 / 报告 §6.5 同项、§6.1 golden 行)
|
||||
# 背景:golden 两脚本(check_menu_rows.py / check_content_pages.py)早已存在且一直在
|
||||
# 人工跑,但**从未进自动门禁**——"有基线"不等于"有门禁"(报告 §6.1 原文即自认此缺口)。
|
||||
# 形态选型依据实测:改一个 UI 源文件后 build_sim 增量重编 + 两条 golden 合计 **2~4 秒**,
|
||||
# 成本可接受 → 走自动门禁(若实测要几分钟,则应改为文档化人工门禁,一个逼人
|
||||
# --no-verify 的门禁等于没有门禁)。
|
||||
# 有牙实测:给 ui_page_dc.c 的 .top_y 注入 +3 位移 → build_sim 重编 →
|
||||
# golden FAIL 且点名 `[dc] FAIL 与参考页不一致(1366 px 差异)`、退出码 1;还原后 diff 干净。
|
||||
# ⚠ 两条 golden 按"整棵菜单树 + 全量参考图"工作、不接受文件参数,故包装器有意忽略
|
||||
# argv 并 pass_filenames: false(pre-commit 传的变更名对它无意义)。
|
||||
# ⚠ 参考图 simulator/tests/ref/*.bin 是判据本身,改动它等于改判据——它已入库,
|
||||
# 评审时看 `git diff -- simulator/tests/ref` 必须逐页过目并在提交正文说明;
|
||||
# 任何时候都不得用 --update 刷基线来"让钩子变绿"。
|
||||
# ⚠ pre-commit 4.x 无 `always: true` → files 必须覆盖所有能改变像素的触发面。
|
||||
# ⚠ 已知瞬时现象:首次实测时在"连续注入/还原的紧邻重建"之间偶发失败一次,
|
||||
# 疑似 MSBuild 节点争用;此后 3 次干净树连跑均绿。判据:单次失败先复跑一次再看,
|
||||
# 连续两次失败才当真(不许直接 --no-verify 绕过)。
|
||||
- id: ui-golden
|
||||
name: UI 像素回归(菜单 8 状态 + 内容页 40 页 黄金参考)
|
||||
entry: python scripts/check_ui_golden.py
|
||||
language: system
|
||||
files: ^(Drivers/BSP/UI/|App/task/ui_task\.(c|h)$|simulator/(tests|sim|src)/|simulator/main\.c$|simulator/CMakeLists\.txt$|simulator/lv_conf\.h$|scripts/check_ui_golden\.py$|\.pre-commit-config\.yaml$)
|
||||
pass_filenames: false
|
||||
|
||||
# 12a) 板端 UI 导航器认屏自检(2026-10-25 道E task-25)
|
||||
# 背景:大窗实测——靠预置键序盲走到深页时,9 帧里只有 3 帧真是它声称的态;
|
||||
# KEY 的 ACK 是空应答、不携带状态,所以**画面是唯一事实源**。判据脚本
|
||||
# scripts/board_ui_nav.py 据此改成闭环:每按一键重新认屏再决定下一个键。
|
||||
# 本钩子跑它的**快检档** --selftest-quick:只核"认屏认得对"(18 张黄金参考 +
|
||||
# 真机留档帧,目录不在版本控制里、缺就整段跳过),秒级、无构建产物依赖。
|
||||
# ⚠ 不挂完整 --selftest:第三段闭环需要 lvgl_sim.exe(构建产物,新工作树里没有)
|
||||
# —— 正是同一天 mem-numbers 缺 .map 那课的形状;脚本对缺 exe 报 FAIL 并给出
|
||||
# 可执行提示,不"跳过当通过"。合入前 / 上板前手工跑一次完整档:
|
||||
# python scripts/board_ui_nav.py --selftest(约 30 s,含全绑定页闭环)。
|
||||
# ⚠ 触发面刻意只两块(脚本自身、黄金参考帧):字体或菜单树变动**不**触发它,
|
||||
# 那两类的守卫分别是 font-charset / ui-golden / registration-check 与 id 侧;
|
||||
# 把每个 UI 提交都拉去跑导航器,只会让人习惯等一个与本改动无关的钩子。
|
||||
- id: uinav-selftest
|
||||
name: 板端 UI 导航器认屏自检(快检:黄金参考帧分类)
|
||||
entry: python scripts/board_ui_nav.py --selftest-quick
|
||||
language: system
|
||||
files: ^(scripts/board_ui_nav\.py$|simulator/tests/ref/.*\.bin$)
|
||||
pass_filenames: false
|
||||
|
||||
# 13) 编号门禁(2026-10-20 加,docs/编号规范.md §8 的 8 项校验)
|
||||
# 背景:§1.2 用仓内证据列了 10 类编号事故(同字母多义 / 跨文档同号不同义 /
|
||||
# 销项删行后残留悬空引用 / 自造前缀 / 会话临时号入册…),而 36 个钩子里
|
||||
# **只有 TC 一族**有机械检查(check_test_traceability.py,且未挂钩子、正则还漏 [示例]
|
||||
# `TC-AI-01A` [示例] 这类大写后缀)——E/N/S/B/C/D/A/U/O/gen/Trap 全部裸奔。本钩子把
|
||||
# 规范 §8 的 8 项机械化:配置两份 `scripts/id_registry.json`(前缀 → 属主登记处
|
||||
# /形态/冻结/游标,逐项来自规范 §4 总表)+ `scripts/id_aliases.json`(旧号→新号,
|
||||
# 照搬 section_aliases.json 的成功范式)。
|
||||
# 判据要点:「定义」= 字面量出现在该族**属主登记处文档**里;于是 §5.5 的
|
||||
# 「销项 = 删行 + 清全库引用」只做一半,校验 3 立刻报(销项号被重新取走那类撞号正是这条)。
|
||||
# 盲区显式登记在 registry 的 `blind_spots` / `unmanaged_bare`:裸形同字母多义
|
||||
# (§1.2:P1 五义 / L1 四义 / UI 状态机号四义)与元器件位号同形(§4.4),机械全库判定
|
||||
# 必然一边倒误报 → 裸形族只在属主文档内判越游标、文档外降告警;收口属 §9 阶段
|
||||
# 1-② 的迁移批次。**不是「检查通过」,是记在案上的盲区。**
|
||||
# ⚠ 当前 warn-only(§8 上线节奏:先清 backlog → 清零后 entry 加 --strict 拦提交,
|
||||
# 与 doc-links 章节校验同一条路径)。校验 8 的「可判定比例」跌破
|
||||
# id_registry.json 里的基线时**任何模式都失败**——那根线就是防回退的本体。
|
||||
# ⚠ pre-commit 4.x 已移除 `always: true` → files 必须覆盖所有能改编号的触发面
|
||||
# (任意 md/c/h/py/yaml/json/txt + 本配置文件);扫描范围恒为全库
|
||||
# (悬空/孤儿/重复定义是跨文件判定,pass_filenames: false)。
|
||||
- id: id-check
|
||||
name: 编号门禁(docs/编号规范.md §8 八项校验)
|
||||
entry: python scripts/check_ids.py --strict
|
||||
language: system
|
||||
files: \.(md|c|h|py|ya?ml|json|txt)$|^\.pre-commit-config\.yaml$
|
||||
pass_filenames: false
|
||||
|
||||
# 14) 分层方向门禁(2026-10-22 加,结构债的"防扩大"那一半)
|
||||
# 背景:AGENTS.md 与 项目架构总览.md 早就写着"六层单向依赖",但**从来没有机械检查**
|
||||
# ——Drivers/BSP/ 里可以随手 include "cfg_file.h" 而无人报警。后果可测量:2026-09 审查
|
||||
# 报告记"NETCFG 一处",2026-10-22 实测已长到 **8 个文件 9 处**。边界的静默腐蚀只能
|
||||
# 靠门禁止住,所以本钩子做**增量拦截**(与 id-check 同构):现状 6 条钉成 BASELINE
|
||||
# (只减不增,没有 --write 往里加),新出现的 (BSP 文件, App 头) 组合直接 FAIL;
|
||||
# 3 条横切基础设施头走 ALLOW 永久豁免(日志后端 / 时间源 / 纯算法工具——它们是
|
||||
# 任何层都要用的叶子能力,不是上层服务)。修完存量后跑 --prune 会点名让你收紧基线,
|
||||
# 于是"还欠几条"变成可见进度,而不是文档里的一句口号。
|
||||
# 注:本钩子只拦"静态 include 方向";**运行时的属主纪律本来就没错**(UI 只投递
|
||||
# net_cfg_save_request(),写盘在 defaultTask),别把它当缺陷读。
|
||||
# 扫描范围恒为全库(要跨文件建 App 头索引),故 pass_filenames: false。
|
||||
- id: layer-deps
|
||||
name: 分层方向(BSP 不得反向 include App)
|
||||
entry: python scripts/check_layer_deps.py
|
||||
language: system
|
||||
files: ^Drivers/BSP/|^scripts/check_layer_deps\.py$|^\.pre-commit-config\.yaml$
|
||||
pass_filenames: false
|
||||
|
||||
# 15) BSP 错误码与超时命名约定(机械检查那一半)。
|
||||
# 四项检查:公开 API 不得用 HAL_StatusTypeDef/bool 当错误通道(谓词 is_/has_/can_/valid
|
||||
# 不在此列);*_TIMEOUT 宏不得取负值(名字叫 TIMEOUT 其实是返回码);非 void 的公开 API
|
||||
# 必须有「返回值:」注释;**返回错误码的函数不得裸丢弃返回值**(接住或显式 (void)——实测
|
||||
# ARMCC v5 与 cppcheck 都不报这类,只有这条会响)。存量钉 BASELINE、只减不增(2026-10-23
|
||||
# 首清点 19 条 → 已还 18;新增一项又暴露 37 条 → **现 38 条**:37 丢弃 + 1 个 bool 待改签名),
|
||||
# 修完跑 --prune 点名收紧基线。约定正文 = Drivers/BSP/bsp_err.h,规则说明 =
|
||||
# scripts/check_bsp_err.py 文件头,逐项详解(含限制与盲区)= docs/门禁说明/bsp-err.md。
|
||||
# 恒扫全库(要按 git 清单建公开头索引),故 pass_filenames: false。
|
||||
- id: bsp-err
|
||||
name: BSP 错误码与超时命名约定
|
||||
entry: python scripts/check_bsp_err.py
|
||||
language: system
|
||||
files: ^Drivers/BSP/|^scripts/check_bsp_err\.py$|^\.pre-commit-config\.yaml$
|
||||
pass_filenames: false
|
||||
|
||||
# 8) 主机单测覆盖率总闸(2026-10-24 接线,用户裁定选 FAIL 语义)
|
||||
# 为什么现在才挂:接线前本文件只有 16 个 *-unit-test 钩子,而仓库已有 28 套主机单测——
|
||||
# P1 新增的 12 套不在任何提交钩子里。原先"再加一份全量插桩等于同一件事做两遍"
|
||||
# 这条理由到现在已经**反了**(现状是有一遍没做),所以必须挂;缺口此前如实登记在
|
||||
# docs/测试说明/主机单测覆盖率口径.md §8。
|
||||
# 为什么挂这一根而不是补 12 个 per-module 钩子:本钩子跑**全部 28 套**,并且守 §5 的
|
||||
# 回退台账(某套被删、或某文件的已覆盖行变少 → [REGRESS] 直接红)。这是 per-module
|
||||
# 钩子做不到的——删掉一套测试,它那根钩子只会因为"没有文件要检查"而静默跳过。
|
||||
# ⚠ FAIL 语义(用户裁定 2026-10-24):找不到 mscoverage 时脚本退出码 2 → 本钩子**失败**,
|
||||
# 不做 skip。理由:skip 等于造一根会静默失效的门禁(与 pre-commit 4.x 移除 always: true
|
||||
# 是同一类坑);宁可在没装组件的机器上被挡一次、看清 [ENV] 那行提示,也不要一根假绿的闸。
|
||||
# 本机与 CI runner 同一台;实测两份 VS 安装都自带该 exe(口径 §7 第 8 条已按实测更正)。
|
||||
# 口径 / 分母 / 基线 / 盲区详解都在 docs/测试说明/主机单测覆盖率口径.md,本处只留接线事实。
|
||||
# 触发面**含门禁自身**(.pre-commit-config.yaml):不带自身就等于谁改坏这根闸的 files:
|
||||
# 模式,它就再也不跑且无人察觉(同 lv-conf-sync / bsp-err 的既有做法)。
|
||||
# ⚠ 读输出的坑(2026-10-24 连踩两次才定位):本钩子没写 stages: → 默认属于**所有阶段**,
|
||||
# 于是同一次提交的输出里它出现两次:pre-commit 阶段那一遍才可能实跑;commit-msg 阶段
|
||||
# 那一遍的"文件"是提交信息文件,几乎必然 (no files to check)Skipped。**只看输出结尾会
|
||||
# 把"实跑通过"读成"根本没跑"**——判它跑没跑要认**第一次**出现的那一行。
|
||||
# 实跑证据:只改本文件的提交(e327636 / c4ca292)里 test-coverage 与 bsp-err 都是 Passed。
|
||||
- id: test-coverage
|
||||
name: 主机单测覆盖率(增量拦截:已覆盖行只增不减)
|
||||
entry: python scripts/check_test_coverage.py
|
||||
language: system
|
||||
files: ^(App/|Drivers/BSP/|test/host/|scripts/check_test_coverage\.py|scripts/test_coverage_baseline\.json|\.pre-commit-config\.yaml$)
|
||||
pass_filenames: false
|
||||
|
||||
# 16) 生成器幂等性(2026-10-26 加,task-18;判据正文 docs/文档规范.md §4.4,索引行见 docs/门禁说明/README.md)
|
||||
# 背景(实证,不是预防性):0f7b5a87 改了 tools/menu_gen/gen_menu.py 的**模板文字**却没重跑
|
||||
# 生成物 → HEAD 里 Drivers/BSP/UI/ui_menu_tab.h 与模板不一致,后来是别人重跑生成器才被顺带
|
||||
# 纠正(属主确认以生成器输出为准)。接线前实测又扫出 **5 张 docs/images/*.svg** 与各自生成器
|
||||
# 不一致(文档挪目录后 rename_ref.py 只改了 tools/ 里的引用路径、产物没跟着重跑)——生成物
|
||||
# 比生成器旧是**系统性**缺口。隐蔽之处:改生成器的人看不到任何 diff(脚本改动编译、单测全过),
|
||||
# 下一个**碰巧**重跑的人却看到一段「不是我改的」改动 → 要么连带提交、要么 revert 掉真修复。
|
||||
# 判定:把声明的输入与生成器脚本一起拷进**系统临时目录**重跑(三个生成器都按 __file__ 的三级
|
||||
# 父目录定位仓库根 → 只会写 temp),再逐字节比对 temp 与工作区:DIFF / NEW / 生成器报错 = FAIL。
|
||||
# **仓库一个字节都不写**,因此不需要还原、也不会 stash 别人在飞的改动(多人同树可安全跑)。
|
||||
# 覆盖:gen_menu(含它回写的 uvprojx + CMakeLists 两份注册表)、中文字库链(extract_cn →
|
||||
# ui_cn_chars.txt、make_font → ui_font_cn_12.c)、tools/arch_diagram 全部配图生成器。
|
||||
# 一处刻意不按裸字节比对:ui_cn_chars.txt 按 font-regen 约定 = 源码在用 ∪ KEEP_EXTRA,改判
|
||||
# 「txt 恰等于 重跑 ∪ KEEP_EXTRA」(两向都不许差;清单 importlib 直载 scripts/check_font_charset.py,
|
||||
# 不留第二份清单)。存量基线 = 接线当天已漂的那 5 张 SVG,钉进脚本内 BASELINE 走 WARN 不拦提交
|
||||
# (只减不增,还清即删、默认运行会点名);业务生成物(菜单表 / 页面注册表 / 字库 / 双端注册表)
|
||||
# **零豁免面**,漂了就是 FAIL。
|
||||
# ⚠ entry 是裸 python,而 pre-commit 在提交环境里可能把它解到**机器级**解释器:2026-10-26 真实提
|
||||
# 交实测解到 C:/Python314(无 fontTools)→ 字体链报 No module named fontTools = 假红。所以脚本
|
||||
# 自己按「显式 DTU_GEN_PYTHON > 当前解释器 > 仓库 .venv > PATH」解析并**逐个探针校验**(要真能
|
||||
# import fontTools 才算数),整轮统一用同一个解释器;挑不出来打 [ENV]+候选表+修法、退出码 2 不
|
||||
# SKIP。不把 entry 写死成 .venv 路径:那是 ci_local 2026-10-11「候选收窄 → runner 上 L0 全灭」
|
||||
# 的同一条坑(详解 docs/门禁说明/generated-fresh.md §4)。
|
||||
# ⚠ 字体链外部工具(node + 全局 lv_font_conv + 系统 simsun.ttc / seguisym.ttf)缺失时该项 SKIP 并
|
||||
# 打印 [SKIP],--strict-env 才升 FAIL——与 test-coverage「缺 mscoverage 必 FAIL」刻意不同:
|
||||
# 缺工具的机器上没有可执行的修法,SKIP 至少把「这台没验过」留在输出里(盲区同登记在索引行)。
|
||||
# ⚠ pre-commit 4.x 已移除 always: true → files 必须覆盖全部触发面:任何生成器(tools/)、
|
||||
# 生成器的输入(Drivers/BSP/UI 源码、App/task/ui_task、ui_menu_tree.txt、ui_cn_chars.txt)、
|
||||
# 生成物(docs/images/*.svg + 两份注册表)、脚本自身与本配置文件(同 registration-check 口径)。
|
||||
- id: generated-fresh
|
||||
name: 生成器幂等性(重跑到临时目录 → 与工作区逐字节一致)
|
||||
entry: python scripts/check_generated_fresh.py
|
||||
language: system
|
||||
files: ^(tools/.+|Drivers/BSP/UI/.+|docs/images/.*\.svg|App/task/ui_task\.(c|h)|ui_menu_tree\.txt|ui_cn_chars\.txt|MDK-ARM/STM32F407-Demo\.uvprojx|simulator/CMakeLists\.txt|scripts/check_generated_fresh\.py|scripts/check_font_charset\.py|\.pre-commit-config\.yaml)$
|
||||
pass_filenames: false
|
||||
|
||||
@@ -10,11 +10,11 @@
|
||||
|
||||
## 项目概况
|
||||
|
||||
**现状一句话**:平台能力(网络 / 存储 / 时间 / UI / 日志 / 看门狗 / FTP / 配置)**已实现并台架验收**;采集主线已闭环到业务消费者(L1→L2 相量→L3 保护判据 protectTask→104 遥信,B5);**RS-485 采集尚未接线**(B2 按用户决策冻结,见 `docs/任务规划/暂缓-RS485.md`)——当前最大功能缺口,接线节奏受 E-59 门禁约束(见「平台约束」)。
|
||||
**现状一句话**:平台能力(网络 / 存储 / 时间 / UI / 日志 / 看门狗 / FTP / 配置)**已实现并台架验收**;采集主线已闭环到业务消费者(L1→L2 相量→L2.5 遥测快照→L3 保护判据 protectTask→104 遥信/遥测+UI,B5/批 2);⚠ **保护判据现处于「未标定闭锁」态**——产品构建下 `adc_cal_flags()` 的 `ADC_CAL_F_VALID` 未置位,`protect_task.c:98` 直接停走判据,等卡 12 两点标定写入 `[adc_cal] valid` 后自动解卡(用户裁定 2026-10-25:保持闭锁,不用万用表走捷径);**RS-485 采集尚未接线**(B2 按用户决策冻结,见 `docs/任务规划/暂缓-RS485.md`)——当前最大功能缺口,接线节奏受 **RS-485 接线门禁**约束(见「平台约束」)。
|
||||
|
||||
**数据主线**(能力视角;分层图与跨层依赖表 → `docs/架构设计/项目架构总览.md`):
|
||||
|
||||
- **采集**:TPAFE5160(计量级 8 路,并行总线)+ 片内 ADC1(9 路,DMA)→ TIM3 1 kHz 节拍 + 原子快照 → adcTask → L1 有效性校验 → L2 相量 → L3 保护(protectTask 20 ms 判据节拍,B5)→ 104 遥信上送
|
||||
- **采集**:TPAFE5160(计量级 8 路,并行总线)+ 片内 ADC1(9 路,DMA)→ TIM3 1 kHz 节拍 + 原子快照 → adcTask → L1 有效性校验 → L2 相量 → **L2.5 遥测快照(`meas` 200 ms 双区,104 与 UI 共用同一份上游,批 2)**+ L3 保护(protectTask 20 ms 判据节拍,B5)→ 104 遥信/遥测上送
|
||||
- **存储**:GD5F SPI NAND → dhara FTL → FatFs → `/log` 运行与自检记录、`/config.ini` 配置、FTP 根目录
|
||||
- **网络**:CH395F → netTask 串行 socket 栈 → FTP(21) / 对时(8888) / RemoDispBus 远程显示(7003) / IEC 60870-5-104 上行(2404)
|
||||
- **人机**:LVGL 160×160 单色菜单(uiTask),**与 PC 模拟器同一份 UI 源码**,远端可看图 + 注键
|
||||
@@ -46,7 +46,7 @@
|
||||
- netTask:`net_stack_bring_up`(配置已由 defaultTask 在屏障前加载,本任务只读 RAM 副本)→ 置 NET_READY → 循环 `net_poll` + `net_process_messages`;消费 `SYS_EVT_NET_RECONFIG` → 按网络代次号重建监听(新 IP 免重启生效)
|
||||
- defaultTask:`f_mount` forced(无卷自动 `f_mkfs`;挂载后卷健康探针,FAT 区陈旧时自愈重格式化)→ 加载网络配置 → STORAGE_READY → LED 闪烁;同时是 syslog(`/log/run.log`·`selftest.log`,定长 64B 记录)的**唯一写者**与**配置引擎属主**(netTask/uiTask 不得直接触碰配置引擎)
|
||||
- adcTask:**纯消费者**——每拍 `adc_input_poll()`(TPAFE 原子快照 + 消费一帧 STM32 ADC1 DMA 双缓冲),只取数不做保护运算;**节拍与唤醒同源**(TIM3 ISR 内触发 CONVST + 同一更新事件经 TRGO 触发 ADC1,并调 `adc_tick_irq_notify()` 置位,任务 `osThreadFlagsWait` 等唤醒)。⚠ ISR 侧**只能**走 `adc_tick_irq_notify()`(内含"消费者就绪"门,TIM3 比调度器早 ≈100 ms 启动);手写于 `Src/freertos.c` 的 `USER CODE` 区,纳入 IWDG `required_mask`(`TASK_WD_ADC`)
|
||||
- **采集与算法层(L1/L2/L3)落在 `App/algo/`**:`adc_input`/`adc_health` = L1,`sample_ring`/`phasor` = L2,`freq` = L2.5 测频(B8,自适应窗),`prot_set`/`prot_elm` = L3。板端消费者 = **protectTask**(B5,2026-10-12):20 ms 节拍 `prot_elm_run()` + L5-4 采样故障闭锁 + L5-1 定值校验和巡检;跨上下文复归(104 YK11)经 `prot_elm_request_reset()` 落请求。纳入 IWDG `required_mask`(`TASK_WD_PROTECT`,产品 `0x1F7`)。→ `App/algo/README.md`
|
||||
- **采集与算法层(L1/L2/L3)落在 `App/algo/`**:`adc_input`/`adc_health` = L1,`sample_ring`/`phasor` = L2,`freq` = L2.5 测频(B8,自适应窗),`prot_set`/`prot_elm` = L3。板端消费者 = **protectTask**(B5,2026-10-12):20 ms 节拍 `prot_elm_run()` + L5-4 采样故障闭锁 + L5-1 定值校验和巡检;跨上下文复归(104 YK11)经 `prot_elm_request_reset()` 落请求。纳入 IWDG `required_mask`(`TASK_WD_PROTECT`,产品 `0x1F7`)。→ `docs/算法/采集与算法层设计说明.md`(设计与判据)、`App/algo/README.md`(目录索引)
|
||||
- 全景 → `docs/架构设计/项目架构总览.md`;启动时序与内核前模式 → `docs/架构设计/项目启动顺序管理.md`
|
||||
|
||||
<!-- TASK_TABLE:BEGIN generated by scripts/gen_task_table.py -->
|
||||
@@ -63,12 +63,12 @@
|
||||
| protectTask | 512 | Normal |
|
||||
<!-- TASK_TABLE:END -->
|
||||
|
||||
(实际值以 `Src/freertos.c` 为准,本表由 `scripts/gen_task_table.py` 生成、`task-table` 钩子校验;testTask 仅在启用测试套件时创建。FreeRTOS 堆已迁 CCM,任务栈不占 SRAM;**每次抬栈的实测最坏链与余量台账** → `docs/内存管理/RAM使用分析与优化方案.md` §1.4 与 `docs/内存管理/内存内容分配策略.md` §6)
|
||||
(实际值以 `Src/freertos.c` 为准,本表由 `scripts/gen_task_table.py` 生成、`task-table` 钩子校验;testTask 仅在启用测试套件时创建。FreeRTOS 堆已迁 CCM,任务栈不占 SRAM;**每次抬栈的实测最坏链与余量台账** → `docs/内存管理/RAM使用分析与优化方案.md` §2.3(任务栈配置与实测峰值)与 §4.0.6(静态最坏链与判定)、`docs/内存管理/内存内容分配策略.md` §6(容量台账))
|
||||
|
||||
## 铁律:源码编辑与编码
|
||||
|
||||
- 改代码**只用 Edit 工具**(精确子串替换 / replaceAll);**绝不用 `Get-Content`/`Set-Content`/`cmd` 重写含中文的文件**——整文件重写按系统默认编码重解码 → `U+FFFD` 乱码且不可逆
|
||||
- **Edit 会静默剥掉文件原有的 UTF-8 BOM**:带 BOM 的文件(`Drivers/BSP/UI/*`、`App/task/ui_task.c`、`App/cfg/cfg_file.c`、`Lib/lv_conf.h`)被 Edit 后丢 BOM → ARMCC 改按 CP936 解码 → 中文字面量报 `#870-D: invalid multibyte character sequence`(**只有重编译该文件才暴露,历史"0 警告"不代表安全**)。改完须核对 BOM 并按原样补回(只前缀 3 字节 `EF BB BF`,勿整文件重写)
|
||||
- **Edit 会静默剥掉文件原有的 UTF-8 BOM**:带 BOM 的文件(`Drivers/BSP/UI/*`、`App/task/ui_task.c`、`App/cfg/cfg_file.c`、`Lib/lv_conf.h`)被 Edit 后丢 BOM → ARMCC 改按 CP936 解码 → 中文字面量报 `#870-D: invalid multibyte character sequence`(**只有重编译该文件才暴露,历史"0 警告"不代表安全**)。改完须核对 BOM 并按原样补回(只前缀 3 字节 `EF BB BF`,勿整文件重写)。**已机械化:`bom-guard` 钩子**(`scripts/check_bom_drift.py`:HEAD 有 BOM 而工作区没有即拦;上面这份名单与钩子 `files` 正则、脚本内 `BOM_LIST` **三处互指**,脚本每次跑都核一次,单独改一处会在别人的提交里被拦下)
|
||||
- **字符串字面量必须纯 ASCII**:无 BOM 文件里奇数个汉字会吞掉结束引号(偶数侥幸通过)→ 中文只写注释
|
||||
- 怀疑乱码先统计 `EF BF BD` 字节数(勿信终端/Read 渲染);**不要急著 `git checkout`**(会丢弃全部未提交工作)——用 Edit 只修坏行
|
||||
- 行尾由 `.gitattributes` 写死(`* text=auto eol=lf`;`*.bat`/`*.cmd` 强制 CRLF):**不依赖 `core.autocrlf`、不要把工作区文件改回 CRLF** → 权威口径 `docs/文档规范.md` §2
|
||||
@@ -76,6 +76,7 @@
|
||||
|
||||
## 构建与验证
|
||||
|
||||
- **门禁时机(用户裁定 2026-10-13)**:`@build` / `scripts/ci_local.py` / `check_doc_links.py` **只在准备提交时跑**(用户让 commit 后统一跑一遍),**不是每次中间改动后都跑**——多步改动期间直接改即可,验证集中在提交门禁;`@test` 硬件闭环仍按任务需要跑
|
||||
- `@build` = `& '.venv\Scripts\python.exe' .dsh\skills\keil-build\scripts\keil_build.py`(仓库根执行)。退出码 **0=PASS / 1=构建失败 / 2=环境错误(UV4/工程/档案)/ 3=用法 / 4=build 锁被占用**;日志 `MDK-ARM/build_log.txt`;换机必设 `DTU_KEIL_UV4` → 细节 **`keil-build` skill** + `docs/环境与部署/本地工具路径环境变量方案.md`
|
||||
- `@test` = `& '.venv\Scripts\python.exe' .dsh\skills\serial-boot-test\scripts\run_boot_test.py`:开串口+清日志 → 烧录(**固件身份戳**保证烧进去的就是这份构建、**回读校验**)→ wait-idle → 采日志 → 标记校验(含启动锚点计数)。退出码 **0=PASS / 1=FAIL / 2=采集或烧录错误 / 3=用法**;戳提取规则、锚点计数判据与按模块定制标记见 `docs/测试说明/自动化测试流程.md`
|
||||
- Forbidden 标记含 `[ERR]`:**四个 Fault 的崩溃日志都以 `[ERR]` 开头 → 出现 Fault 即本次不通过**(刻意)
|
||||
@@ -85,11 +86,12 @@
|
||||
## Python 环境
|
||||
|
||||
- 统一入口 `& '.venv\Scripts\python.exe'`(3.12,fontTools/Pillow 已装);PATH 上的 `python` 是 msys64 的,**不要用它**跑依赖第三方库的脚本;纯标准库脚本任意 python3 均可
|
||||
- ⚠ **钩子 entry 的裸 `python` 不等于 .venv**:2026-10-26 真实提交实测 pre-commit 把它解析成**机器级**解释器(无 fontTools)→ 字体链生成器报 `No module named fontTools`、`generated-fresh` 假红(同族第二次发作见 `scripts/ci_local.py` 文件头 2026-10-11「L0 全灭」)。凡依赖第三方库的门禁脚本,一律在脚本内自己按「显式 var > 当前解释器 > 仓库 .venv > PATH」解析并**探针校验**(装了 pre-commit ≠ 装了 fontTools),但**别把 entry 写死成某台机器的绝对路径**(那等于把门禁绑死在一台机器上)→ `docs/门禁说明/generated-fresh.md` §4
|
||||
- 重建/换机:`<本机任一 python ≥3.10> -m venv .venv` + `pip install -r requirements.txt`(含 `pre-commit`;`DTU_PRECOMMIT_PYTHON` 可指向别的装了 pre-commit 的解释器);`requirements.txt` 必须**纯 ASCII**(Windows 下 pip 按 GBK 代码页解码)
|
||||
|
||||
## 提交门禁(pre-commit)
|
||||
|
||||
- 任一失败即拦截,共 **33 个钩子**(逐个详解 → `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` + 主机侧单测 15 个(`-unit-test` 后缀)+ `test-suite-switch` + `registration-check`(双端注册反向校验,N-23)+ `mem-budget`(RAM 92% 预算门禁,N-27)
|
||||
- 任一失败即拦截,共 **50 个钩子**(**口径 = YAML 解析后的 hook 条目数**:自查命令与「声明 / 文档 / 实跑」三数同源核对见 `docs/门禁说明/README.md` §2 首条——⚠ 直接 grep 配置里的声明行会多算一条**注释掉的**候选钩子(= 51),不是口径;加一条钩子就同批改 AGENTS 与门禁索引两处计数。2026-10-25 道E 连加 `bom-guard` / `setview-check` / `uinav-selftest` 三条,2026-10-26 加 `generated-fresh` 一条;**每种门禁一篇详解(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径)→ `docs/门禁说明/README.md` 索引**;通用机制与整体口径 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `id-check`(**编号门禁**,`docs/编号规范.md` §8 八项校验;**已关闸为【增量拦截】**:只拦 `scripts/id_known_backlog.json` 台账之外的新错误级问题,存量按指纹豁免、台账只减不增;另有一根恒生效的线——可判定比例跌破 `id_registry.json` 基线即失败;**2026-10-26 方案 B 同批加了第二根**:不可判定引用**绝对数**超过 `undecidable_total_ceiling` 即失败,因为比值会被"把可判定的引用改成散文"稀释(详见 `docs/门禁说明/id-check.md` §7.6))+ `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 **22 根专属钩子**(`-unit-test` 后缀 = 有独立 `files:` 触发面、需要秒级归因的那个子集;2026-10-25 增补 adc-cal / prot-dim / vol-idle-sync / ui-data / meas / run-par 六根)——**这是口径不是缺口**:`test/host/` 实测 40 套带 `run_unit_test.py`,其余 **18 套**(含 `test/host/ui_sink`)没有专属钩子,但每次提交都被 `test-coverage` 总闸全量真跑并守基线回退,**失败照样拦**,只是归因落在总闸输出上;**没有专属钩子 ≠ 没被跑**,18 套台账与补钩子的判据 → `docs/门禁说明/host-unit-tests.md` §6 第 4 条(原写「23 个 / 含 ui-sink 七个」是双重错:既把 22 说成 23,又宣称 ui-sink 有钩子——配置历史里 `ui-sink-unit-test` 从未存在)+ `test-coverage`(**覆盖率总闸**:跑全部 40 套并守基线回退;缺 mscoverage 组件时按用户裁定 **FAIL**、不 skip,2026-10-24)+ `test-suite-switch` + `registration-check`(双端注册反向校验 + UI 页面描述符表接线,登记簿条目「rs485_task 白名单缺口」)+ `generated-fresh`(**生成器幂等性**:把 `tools/menu_gen/gen_menu.py`、中文字库链、`tools/arch_diagram/gen_*.py` 拷进**系统临时目录**重跑,与工作区**逐字节**必须一致——钩子只读、不写仓库也不 stash 别人在飞的改动;实证 `0f7b5a87` 改了模板文字没重跑生成物,接线前实测另有 5 张 `docs/images/` 配图已漂 → 钉成只减不增基线,**业务生成物零豁免面** → `docs/门禁说明/generated-fresh.md`)+ `mem-budget`(RAM 92% 预算门禁,登记簿条目「缩堆迁 CCM 后半(暂缓)」)+ `font-charset`(UI 字符集差集检查,防缺字白框)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 40 页黄金参考)+ `layer-deps`(**分层方向**:`Drivers/BSP/**` 不得 include `App/**` 的头;存量 3 条基线只减不增(2026-10-25 记录 reader 注入削掉 UI 记录 3 条边,6→3))+ `bsp-err`(**BSP 错误码与超时命名约定**:公开 API 不得拿 `HAL_StatusTypeDef`/`bool` 当错误通道、`*_TIMEOUT` 宏不得取负值、非 void 须写「返回值:」、**返回错误码的函数不得裸丢弃返回值**(须接住或显式 `(void)`;实测编译器与 cppcheck 都不报这类,只有这条会响);约定正文 `Drivers/BSP/bsp_err.h`,存量基线只减不增(2026-10-23 首清点 19 条 → 已还 18;`consume-result` 上线又暴露 37 条存量丢弃 → **现 38 条**))
|
||||
- **范围 = 项目代码**:第三方(`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/`
|
||||
@@ -109,7 +111,7 @@
|
||||
|
||||
- **`osKernelStart` 之前禁止任何 FreeRTOS/CMSIS-RTOS 创建类 API**(`osMessageQueueNew` 等会杀死启动,2026-09-20 实证根因)→ 内核前标准模式(纯寄存器/HAL 轮询 + **无锁静态缓冲** + 内核启动后**惰性创建**真队列)见 `docs/架构设计/项目启动顺序管理.md`
|
||||
- **就绪屏障位必须 `osFlagsNoClear`**:`osEventFlagsWait` **默认消费**所等位,就绪位被先消费掉 → `ftpTask` 的 `WaitAll` 永不成立 → **FTP 21 端口静默不监听**(无日志无报错)。命令位(`SYS_EVT_CFG_SAVE`/`NET_RECONFIG`)保持消费型
|
||||
- **自研代码禁用 C 库堆分配**(`malloc/calloc/realloc/strdup/free`):MicroLIB 的 C 堆由 `startup_stm32f407xx.s` 的 `Heap_Size EQU 0x200` 限定为 **512 B**,KB 级分配必然返回 NULL,而**模拟器(宿主 CRT 堆)完全正常、照不出来** → 只用 `pvPortMalloc/vPortFree`,或把分配收进平台后端 → `docs/内存管理/RAM使用分析与优化方案.md`
|
||||
- **自研代码禁用 C 库堆分配**(`malloc/calloc/realloc/strdup/free`):**2026-10-23 起由提交门禁机器拦截**(`coding-standard` 的 CODE-RULE-20,唯一豁免 = 平台分配后端的宿主侧实现,见 `docs/门禁说明/coding-standard.md` §7):MicroLIB 的 C 堆由 `startup_stm32f407xx.s` 的 `Heap_Size EQU 0x200` 限定为 **512 B**,KB 级分配必然返回 NULL,而**模拟器(宿主 CRT 堆)完全正常、照不出来** → 只用 `pvPortMalloc/vPortFree`,或把分配收进平台后端 → `docs/内存管理/RAM使用分析与优化方案.md`
|
||||
- **内存预算紧张**:新增任何任务/缓冲/栈前先核算 RAM 余量;改栈或加大局部变量**前后各跑一次** `scripts/stack_report.py`(静态最坏链 + 串口 `[STACK]` 水位)与 `scripts/ram_report.py`(.map 静态占用);**总量红线已机械化**:`mem-budget` 钩子拦 RW_IRAM1/RW_CCM >92%,子系统配额 / CCM 台账 / 趋势见 `docs/内存管理/RAM预算台账.md`(**每次 `@build` 后跑 `scripts/ram_baseline.py --update`**)→ `docs/内存管理/`
|
||||
- FreeRTOS V10.3.1(CMSIS-RTOS V2);HAL 时基 = **TIM7**(非 SysTick),tick = 1ms;NVIC 4 位优先级分组,**外设中断优先级 ≥ 5**
|
||||
|
||||
@@ -118,7 +120,7 @@
|
||||
- **ADC 取数只用 `tpafe5160_snapshot()`(临界区原子快照)**:旧的 `get_buf()`/`data_ready()`/`clear_ready()` 已从驱动**删除**(两步之间被抢占会拷回两帧混合数据;`s_ready` 三写者竞态)。消费者侧一律经 `adc_input_poll()` / `adc_input_raw()` **按语义**取数(不写死下标)→ `docs/子系统设计/ADC采样节拍设计说明.md`、`App/algo/README.md`
|
||||
- **F4 上 ADC1 的硬件触发源与 DMA 流是硬约束**:① ADC1 的外部触发清单**没有 TIM6/TIM7 TRGO**(F4 上它们只接 DAC)→ 同时驱动两片转换器必须用 **TIM3**(PSC 83 / ARR 999 → 1 kHz + TRGO_UPDATE);② ADC1 在 DMA2 **只挂 Stream0 或 Stream4** → 选 **Stream4** 才不挤走 SPI1/USART1 RX,且**换流会被重置为优先级 0**(违反"外设中断 ≥5")→ 改流后必须回 NVIC 核对 `Src/dma.c`;③ `DMAContinuousRequests` **必须 ENABLE**(关掉 = 一轮 9 通道只发一次请求,静默丢 8 路);④ DMA 目的缓冲必须在 SRAM(DMA2 不可达 CCM)→ `docs/硬件平台/ADC通道映射说明.md`
|
||||
- **STM32F4 ADC 上电必须自校准**:CubeMX **不生成**这一步,本仓裁剪版 HAL 没有 `HAL_ADCEx_Calibration_Start`、裁剪版 CMSIS 头没有 `ADC_CR2_CAL` → 按 RM0090 §13.3.6 手写 `adc1_calibrate()`(`Src/adc.c` 的 `USER CODE BEGIN 1` 区)+ `MX_ADC1_Init()` 末尾调用;缺它**不报任何错**,只是读数带失调
|
||||
- **接 ADC / RS-485 消费者前的强制门禁(E-59)**:先确认驱动契约已修(TPAFE E-44/E-45/E-58 ✅;RS485 E-36/E-37 ✅ 但 **E-50 单缓冲包撕裂、E-49 TX 超时拉 DE 未修 ❌** → `rs485_task` 仍被挡)+ 建任务前后各跑一次 `stack_report.py`
|
||||
- **接 ADC / RS-485 消费者前的强制门禁(RS-485 接线门禁)**:先确认驱动契约已修——TPAFE 三条 ✅(原子快照取数 / 帧序号取代就绪电平标志 / FRSTDATA 数据有效性校验);RS485 已修两条 ✅,但 **单缓冲包撕裂、TX 超时拉 DE 两条未修 ❌** → `rs485_task` 仍被挡。建任务前后各跑一次 `stack_report.py`
|
||||
|
||||
**存储与网络外设**
|
||||
|
||||
@@ -134,11 +136,11 @@
|
||||
|
||||
- **完整引脚表(含 SD2506 I2C1、SWD、调试口)→ `docs/产品需求/产品需求设计文档.md` §4.2**(权威,V0.1 口径;勿在本文重复抄表)
|
||||
- ⚠ **网名 `AINn` ≠ 板级 `ADCn` ≠ 转换器通道号**(17 路里 13 路置换)→ 取数**必须查** `docs/硬件平台/ADC通道映射说明.md`(TPAFE `buf[2]=IA`/`buf[5]=UN`/`buf[6]=UA`;网名 ADC17=PC4 用 `ADC_CHANNEL_14`,**不是** `ADC_CHANNEL_12`)
|
||||
- ⚠ **8 路电气量被分给两片转换器是 V0.1 硬件缺陷,不是有意分配**(跨转换器无共同采样触发 → 无同一时刻性、相位不确定)→ V0.1 上**禁止**跨转换器做相位/功率/功率因数/矢量运算;**V0.2 须把 8 路全接 TPAFE**(登记簿 N-18)
|
||||
- ⚠ **8 路电气量被分给两片转换器是 V0.1 硬件缺陷,不是有意分配**(跨转换器无共同采样触发 → 无同一时刻性、相位不确定)→ V0.1 上**禁止**跨转换器做相位/功率/功率因数/矢量运算;**V0.2 须把 8 路全接 TPAFE**(登记簿条目「双转换器无共同采样触发」)
|
||||
|
||||
## 模块约定
|
||||
|
||||
- **CH395F 单连接 socket**:监听与数据通道复用同一 Socket(CONNECT 后转数据通道,DISCONNECT/TIMEOUT 后重开 → LISTEN);所有 CH395F 中断在 `net_poll()` 内经 `GET_GLOB_INT_STATUS` 集中读取并清除,**不得**在 ISR 或其他地方单独处理
|
||||
- **CH395F 单连接 socket**:监听与数据通道复用同一 Socket(CONNECT 后转数据通道,DISCONNECT/TIMEOUT 后重开 → LISTEN);所有 CH395F 中断在 `net_poll()` 内经 `GET_GLOB_INT_STATUS` 集中读取并清除,**不得**在 ISR 或其他地方单独处理。⚠ 全局寄存器**整体读即清、芯片无按位清除手段**,而 net_poll 只在全局位为 1 时才读该 Socket 的 SINT → "顺手全量清一遍"等于把别的在用 Socket 的门焊死(B3 实证)。**现存读清点全量审计 + 新增一处读清的三条准入判据 → `docs/网络/网络自愈架构设计.md` §4.10**
|
||||
- 所有 `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(模拟器多连接后端)时才关
|
||||
@@ -150,9 +152,9 @@
|
||||
|
||||
## 文档与图形
|
||||
|
||||
- **登记簿类文档「完成即销项」**:`docs/任务规划/README.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'`)
|
||||
- **改名 / 挪目录一律走 `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
|
||||
- **登记簿类文档「完成即销项」**:`docs/任务规划/README.md` 这类待办登记簿,事项修复/关闭后**直接从表中删除该行**(不保留"已消化"行)——结论与证据应已沉淀进对应模块文档/问题记录。销项时必须:① 先核对目标文档确已含完整记录 ② 同步清理其他文档对该编号的引用 ③ 改完跑 `check_doc_links.py --all` + pre-commit。**新条目标识(用户裁定 2026-10-14)**:`#` 列填**语义短语**(如 `沉降窗 800ms 上限只留痕`),**不再新增字母+数字编号**——"编号不回收"只写规则却不留痕迹就不可执行(销项删行后仓库里毫无痕迹,2026-10-14 实测就发生过「销项号被重新取走」的撞号事故),语义标识自带唯一性;**存量编号(登记簿 N-x / S-x / T-x 与专项 A-x)已于 2026-10-26 全部转成语义短语**——旧号进 `scripts/id_aliases.json` 标 `retired-to-phrase`,不得再指;**已随销项消失的号永久作废**、不得复用 → `docs/任务规划/README.md` §0
|
||||
- 图形走 **`draw-diagram` skill**:精确布局图用 `tools/arch_diagram/gen_*.py` 生成器(产物放 `docs/images/`,**脚本与产物同一笔提交**:改生成器没重跑,下一个碰巧重跑的人会看到一段「不是我改的」diff——已机械化 `generated-fresh`,重跑到临时目录逐字节比对 → `docs/文档规范.md` §4.4);拓扑图用 Mermaid;**勿手绘 ASCII 框图**。文件引用关系图走 `tools/refgraph`(`& 'tools\refgraph\gen_refgraph.bat'`)
|
||||
- **改名 / 挪目录一律走 `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。**编号改名走 `scripts/rename_ids.py`**(映射表驱动、**显式 scope**、干跑默认、自动写 `scripts/id_aliases.json` 的 scoped 旧号→新号条目;`[示例]`/`[计划]` 行与变更记录表不改写;**绝不全局查找替换**——同一裸形字面量可能同时属多个族,全局替换等于造新撞号,实测 `S3` 三义)
|
||||
- 文档写作规范(编码/BOM/链接/表格/一致性钩子)→ `docs/文档规范.md`;全量索引与推荐阅读顺序 → `docs/README.md`
|
||||
|
||||
## 边界(Ask first / Never)
|
||||
@@ -172,7 +174,9 @@
|
||||
| ADC 采样节拍 / 通道映射 / V0.1 硬件缺陷 | `docs/子系统设计/ADC采样节拍设计说明.md`、`docs/硬件平台/ADC通道映射说明.md` |
|
||||
| 存储栈(FatFs / dhara / lftpd / 两条 FatFs 陷阱) | `docs/架构设计/STM32F4-Base存储架构说明.md` |
|
||||
| RAM / 栈预算 / 容量台账 | `docs/内存管理/RAM使用分析与优化方案.md`、`内存内容分配策略.md` |
|
||||
| pre-commit 33 个钩子逐个详解 | `docs/代码规范/pre-commit使用说明.md` |
|
||||
| **每个提交门禁一篇详解**(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径) | `docs/门禁说明/README.md`(50 个钩子 → 26 篇索引 + 1 指针) |
|
||||
| **要不要给某个东西发编号**(默认:不发,写语义短语)/ 允许用号的 8 个族 / `id-check` 八项校验 | `docs/编号规范.md`(§1 先问三句、§4.1 族总表〔穷举〕、§8 门禁与实测基线) |
|
||||
| 提交门禁通用机制(阶段 / stash 陷阱 / 触发面 / 增量拦截三段式) | `docs/代码规范/pre-commit使用说明.md` §2 |
|
||||
| 代码规范 V1.0 / cppcheck 口径 | `docs/代码规范/嵌入式C语言代码规范_V1.0.md`、`cppcheck使用说明.md` |
|
||||
| 自动化测试 / 实机测试 / 板级套件 | `docs/测试说明/自动化测试流程.md`、`实机测试实施指南.md`、`board-test-suite` skill |
|
||||
| 烧录与复位语义 | `docs/测试说明/ST-LINK_CLI_复位使用说明.md` |
|
||||
@@ -181,4 +185,4 @@
|
||||
| 待办与审计发现登记簿 | `docs/任务规划/README.md` |
|
||||
| 多任务并行开发(开空间 / 合并回收 / 台架与构建排队) | **`task-worktree` skill**(`@task`,用到自动加载)、`docs/环境与部署/并行任务工作树方案.md` |
|
||||
| 换机 / 工具路径环境变量 | `docs/环境与部署/` |
|
||||
| 远程 CI(Gitea Actions,**只跑本机一台 runner**) | [`docs/CI/README.md`](docs/CI/README.md) |
|
||||
| 远程 CI(Gitea Actions,**只跑本机一台 runner**) | **`remote-ci-ops` skill**(排障顺序与判据,用到自动加载)、[`docs/CI/README.md`](docs/CI/README.md) |
|
||||
|
||||
+2
-2
@@ -13,10 +13,10 @@ FreeRTOS/CMSIS-RTOS API**(见 `AGENTS.md`「平台约束」)。
|
||||
|
||||
| 目录 | 内容 | README |
|
||||
|---|---|---|
|
||||
| `app/` | **应用入口与全局**:`app_main.c/h`(初始化入口)、`app_cfg.h`(编译开关)、`sys_events.h`(就绪事件) | [`app/README.md`](app/README.md) |
|
||||
| `app/` | **应用入口与全局**:`app_main.c/h`(初始化入口)、`app_cfg.h`(裁剪决策占位说明,当前无宏)、`sys_events.h`(就绪事件) | [`app/README.md`](app/README.md) |
|
||||
| `cfg/` | **配置引擎**:`/config.ini` 解析 + 原子持久化(旧二进制档已废弃,2026-10-10) | [`cfg/README.md`](cfg/README.md) |
|
||||
| `sync/` | **时间同步任务**:`time_sync.c/h` | [`sync/README.md`](sync/README.md) |
|
||||
| `algo/` | **采集与算法层(L1/L2)**:`adc_input.c/h`(语义通道映射 + STM32 ADC1 扫描) | [`algo/README.md`](algo/README.md) |
|
||||
| `algo/` | **采集与算法层(L1/L2/L3)**:语义取数 + 有效性判定 + 采样环/相量/测频 + 定值模型 + 保护元件框架 | [`algo/README.md`](algo/README.md)(索引);设计 → `docs/算法/采集与算法层设计说明.md` |
|
||||
| `log/` | **日志子系统**:syslog(NAND)+ 控制台单写者日志 | [`log/README.md`](log/README.md) |
|
||||
| `task/` | **FreeRTOS 任务实现**(6 个任务) | [`task/README.md`](task/README.md) |
|
||||
| `util/` | **通用工具**:CRC / 环形缓冲 / 墙钟时间 | [`util/README.md`](util/README.md) |
|
||||
|
||||
+150
-335
@@ -1,360 +1,175 @@
|
||||
# App/algo/ — 采集与算法层(L1 / L2)
|
||||
# App/algo/ — 采集与算法层(L1 / L2 / L3)
|
||||
|
||||
> **自研代码**,属 `docs/算法/继电保护功能复现规划.md` §2.1 分层中的 **L1(采集与预处理)**
|
||||
> 与 **L2(相量计算)**。原则:**算法与 I/O、与调度分离**——本层不建任务、不做保护判据、
|
||||
> 不碰寄存器(唯一例外见 §3.1),只做"数"(原始码值 → 语义通道)与"相量"
|
||||
> (采样序列 → 复相量),因而可被主机侧单元测试(L0.5)离线全覆盖(落实登记簿 **A-4**)。
|
||||
>
|
||||
> 任务侧(`App/task/adc_task.c`)只做**消费与编排**;任何新算法一律落在本目录,勿塞进 `App/task/`。
|
||||
> 本目录的**索引**:文件清单、依赖、逐文件的"改代码必须知道"、构建与测试入口、新增模块动作。
|
||||
> **详细设计——机制、契约、判据、为什么这样设计 → `docs/算法/采集与算法层设计说明.md`**(本文不复制其内容)。
|
||||
> 分层归属与功能项状态 → `docs/算法/继电保护功能复现规划.md` §2.1;
|
||||
> 通道映射唯一权威 → `docs/硬件平台/ADC通道映射说明.md`;采样节拍 → `docs/子系统设计/ADC采样节拍设计说明.md`。
|
||||
|
||||
## 1. 文件清单(一眼版)
|
||||
|
||||
| 文件 | 层 | 作用(一句话) | 主要对外入口 | 覆盖规划项 |
|
||||
|---|---|---|---|---|
|
||||
| `adc_input.h` / `adc_input.c` | L1 | **统一语义取数层**:把两片转换器的原始码值按语义(`UA…IN`/温度)暴露,并消费 STM32 ADC1 的 DMA 乒乓双缓冲 | `adc_input_init()` `adc_input_poll()` `adc_input_raw()` | L1-2 / L1-3 / L1-4 |
|
||||
| `adc_health.h` / `adc_health.c` | L1 | **采样有效性判定层**:吃"计数快照"(帧序号/错误计数),产出静默/丢帧/单调性/错误增量故障位——判定与告警编排分离 | `adc_health_init()` `adc_health_check()` `adc_health_flags()` | L1-6 |
|
||||
| `sample_ring.h` / `sample_ring.c` | L2 输入 | **采样环缓冲**:按语义通道保存最近 2 个周波(40 点)采样值,供相量层"以当前时刻为终止"取窗 | `sample_ring_push_frame()` `sample_ring_window()` | L2 输入(为 L2-6 滑动窗供数) |
|
||||
| `phasor.h` / `phasor.c` | L2 | **全周傅氏相量计算**:一周波 20 点单频 DFT → 基波/谐波相量(有效值口径),双区原子切换供无锁读 | `phasor_run()` `phasor_get()` `phasor_calc()` | L2-1 / L2-2 / L2-3 / L2-6 / L2-7 |
|
||||
| `prot_set.h` / `prot_set.c` | L3 输入 | **保护定值模型**:Q10 定点 RAM 表(通讯地址对照参考 `g_tCommSetParaTab`)+ 范围校验 + 校验和封存自检闭锁 | `prot_set_init()` `prot_set_load_raw()` `prot_set_get_f()` `prot_set_selfcheck()` | §5 定值模型 / L5-1 |
|
||||
| `prot_elm.h` / `prot_elm.c` | L3 | **保护元件框架**:通用"越限 + 返回系数滞回 + 定时限"核 + 数据驱动实例表(过流三段 / 过负荷 / 过压 / 欠压 / 零序过压 / **过频 / 低频**) | `prot_elm_init()` `prot_elm_eval()` `prot_elm_run()` `prot_elm_active_mask()` | L3-1 / L3-3 / L3-4 / L3-5 / L3-6(软件侧) |
|
||||
| `freq.h` / `freq.c` | L2 派生量 | **频率测量**:相邻两拍基波相量共轭乘积取角(每拍相位增量)+ 自适应窗长滑动平均 + 假值钳制 + 幅值门(UA→IA 源回退) | `freq_init()` `freq_run()` `freq_get()` `freq_valid()` | L3-5 测量侧(`docs/算法/继电保护功能复现规划.md` §4.2 测频范围) |
|
||||
| `adc_input.h` / `.c` | L1 | 统一语义取数:两片转换器的码值按语义(`UA…IN` / 温度)暴露,并消费 STM32 ADC1 的 DMA 乒乓缓冲 | `adc_input_init()` `adc_input_poll()` `adc_input_raw()` | L1-2 / L1-3 / L1-4 |
|
||||
| `adc_health.h` / `.c` | L1 | 采样有效性判定:吃计数快照,产出静默 / 丢帧 / 单调性 / 错误增量故障位(判定与告警编排分离);L5-2 残余的码值贴轨观察器同层 | `adc_health_check()` `adc_health_flags()` `adc_health_rail_watch()` | L1-6 / L5-2(残余) |
|
||||
| `adc_cal.h` / `.c` | L1.5 | 通道标定折算(「数据可信」卡 2/卡 4):工程量 =(code+b)×k,Q16 系数/零点两段落盘 + 三态 flags + 合法带哨兵 + 两点公式 | `adc_cal_apply()` `adc_cal_flags()` `adc_cal_set()` | L1-5(M4 半边) |
|
||||
| `meas.h` / `.c` | L2 发布 | 遥测快照(「数据可信」卡 8,M5 地基):200 ms 基波有效值 + 品质快照,双区无锁读,全仓唯一发布者 = adcTask | `meas_tick()` `meas_get()` `meas_seq()` | M5(卡 9/11 与 UI 是读者) |
|
||||
| `sample_ring.h` / `.c` | L2 输入 | 按语义通道存最近 2 周波(40 点),供相量层"以当前时刻为终止"取窗 | `sample_ring_push_frame()` `sample_ring_window()` | L2 输入(为 L2-6 供数) |
|
||||
| `phasor.h` / `.c` | L2 | 全周傅氏:一周波 20 点 DFT → 基波 / 谐波相量(有效值口径),双区原子切换供无锁读 | `phasor_run()` `phasor_get()` `phasor_calc()` | L2-1 / L2-2 / L2-3 / L2-6 / L2-7 |
|
||||
| `freq.h` / `.c` | L2 派生量 | 频率测量:相邻两拍基波相量共轭乘积取角 + 自适应窗滑动平均 + 假值钳制 + 幅值门(UA→IA 源回退) | `freq_run()` `freq_get()` `freq_valid()` | L3-5 测量侧(测频范围 → `docs/算法/继电保护功能复现规划.md` §4.2) |
|
||||
| `prot_set.h` / `.c` | L3 输入 | 保护定值模型:Q10 定点 RAM 表 + 范围校验 + 校验和封存 / 自检 / 闭锁 + 持久化 blob 纯逻辑 | `prot_set_load_raw()` `prot_set_get_f()` `prot_set_selfcheck()` | L5-1 载体 |
|
||||
| `prot_elm.h` / `.c` | L3 | 保护元件框架:通用"越限 + 返回系数滞回 + 定时限"核 + 数据驱动实例表(过流三段 / 过负荷 / 过压 / 欠压 / 零序过压 / 过频 / 低频) | `prot_elm_run()` `prot_elm_eval()` `prot_elm_active_mask()` | L3-1 / L3-3 / L3-4 / L3-5 / L3-6(软件侧) |
|
||||
|
||||
依赖关系(自下而上,无环):`adc_input.h`(纯枚举 + 声明)← `sample_ring` ← `phasor` ← `freq`;
|
||||
`prot_set` **独立**(仅 `stdint.h`);`prot_elm` ← `prot_set` + `phasor` + `freq`。
|
||||
`sample_ring.c` / `phasor.c` / `adc_health.c` / `prot_set.c` / `prot_elm.c` / `freq.c` **零平台依赖**
|
||||
(只用 `math.h` / `stddef.h` / `stdint.h` 与本目录头);
|
||||
`adc_input.c` 另依赖 `adc.h`(`hadc1`)与 `tpafe5160.h`(快照),主机侧以桩头替身。
|
||||
依赖(自下而上、无环):`adc_input.h`(纯枚举 + 声明)← `adc_cal`(另依赖 `cfg_file.h` 接口,
|
||||
纯头文件级)← `sample_ring` ← `phasor` ← `freq`;`meas` ← `sample_ring` + `phasor`
|
||||
(仅 `phasor_calc` 纯核;频率/品质由调用方注入,**不链** `freq`/`adc_cal`);
|
||||
`prot_set` **独立**;`prot_elm` ← `prot_set` + `phasor` + `freq`。
|
||||
`adc_health` 只吃调用方喂进来的计数 / 码值,不反向依赖 L1 实现(L5-2 贴轨门限由调用方
|
||||
从 `adc_cal_rail_ths()` 取,本层仍零算法依赖)。
|
||||
|
||||
## 2. 数据流(一拍 = TIM3 1 kHz 节拍)
|
||||
**除 `adc_input.c` 外全部零平台依赖**(只用 `stdint.h` / `math.h` / `stddef.h` 与本目录头)
|
||||
→ 可被主机单测 `#include` 原样纳入、免桩跑。`adc_input.c` 另依赖 `adc.h`(`hadc1`)与
|
||||
`tpafe5160.h`(快照),主机侧以桩头替身。
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
TIM3["TIM3 更新事件 1 kHz(PSC 83 / ARR 999)<br/>ISR: tpafe5160_start_conv_irq() + adc_tick_irq_notify()<br/>同一事件经 TRGO 触发 ADC1"] --> TPAFE["TPAFE5160 转换<br/>BUSY EXTI 收帧"]
|
||||
TIM3 --> ADC["STM32 ADC1 一轮 9 通道(≈67 us)<br/>DMA2_Stream4 循环模式自动搬运"]
|
||||
ADC --> PP["乒乓双缓冲 s_stm32_dma[2×9]<br/>半传输 / 传输完成回调各推进 seq"]
|
||||
TIM3 -. "ADC_WAKE_FLAG<br/>osThreadFlagsWait" .-> TASK
|
||||
TPAFE --> POLL
|
||||
PP --> POLL
|
||||
TASK["adcTask(事件驱动唤醒,非定时轮询)<br/>App/task/adc_task.c"] --> POLL["adc_input_poll()<br/>TPAFE 快照 + 消费刚完成的那半区(零忙等)"]
|
||||
POLL --> RAW["adc_input_raw(ch) × 9<br/>按语义取原始码值"]
|
||||
RAW --> RING["sample_ring_push_frame()<br/>整帧入环(40 点窗)"]
|
||||
RING --> RUN["phasor_run()<br/>逐实例 20 点重算 + 双区切换"]
|
||||
RUN --> FREQ["freq_run()<br/>共轭乘积相位增量 + 自适应窗均值(B8)"]
|
||||
RUN --> GET["phasor_get(idx)"]
|
||||
FREQ --> FGET["freq_get() / freq_valid()"]
|
||||
GET --> CONS["消费者:adcTask 日志(现状)<br/>UI / 自检 / L3 保护任务(待接)"]
|
||||
FGET --> PCONS["消费者:prot_elm_run() OF/UF 判据(B8)<br/>adcTask 心跳 freq 行"]
|
||||
```
|
||||
## 2. 一拍的数据流(一眼版)
|
||||
|
||||
初始化落点:`adc_input_init()` 与 `phasor_init()`、`freq_init()` 由 `App/app/app_main.c` 的
|
||||
`app_main_init()` 在**内核启动前**调用(各 `*_init()` 均无 RTOS 依赖、幂等);
|
||||
`phasor_init()` 内部会连带 `sample_ring_init()`,等价于"刚上电"。
|
||||
`adc_input_poll()` → `adc_input_raw()` ×9 → `adc_cal_apply()` ×9(L1.5 折算,现口径 =
|
||||
ADC 引脚电压 V)+ `adc_health_rail_watch()` ×9(L5-2,码值域)→ `sample_ring_push_frame()`
|
||||
→ `phasor_run()` → `freq_run()` → 消费者(`adcTask` 心跳与 L1 健康判定;`protectTask` 每 20 ms 取相量 / 频率跑
|
||||
`prot_elm_run()`;**每 200 ms** `meas_tick()` 发布遥测快照供 iec104Task / uiTask 读,卡 8)。节拍 = TIM3 1 kHz 更新事件,两片转换器同源触发。
|
||||
完整数据流图与初始化落点 → `docs/算法/采集与算法层设计说明.md` §2。
|
||||
|
||||
## 3. 逐文件说明
|
||||
## 3. 逐文件要点(只留"改代码时必须知道"的;机制与理由见设计文档)
|
||||
|
||||
### 3.1 `adc_input.c` / `adc_input.h` — L1 ADC 输入层
|
||||
> **改本目录前先记五条红线**:① 取数只经 `adc_input_raw()`,不写死下标;② **禁止跨转换器**做
|
||||
> 相位 / 功率 / 矢量运算(双转换器无共同采样触发);③ 帧值已是 L1.5 工程量,但 **未标定(`adc_cal_flags()` 无
|
||||
> VALID)时单位只是 ADC 引脚电压**,不是端子二次值——下游消费精度判据见「数据可信」§4.2 三态
|
||||
> 表;④ 工程量 → 帧的唯一折算点是 `adc_cal_apply()`,禁止旁路再乘系数;⑤ 本层不建任务、
|
||||
> 不等事件、不写日志(`adc_cal` 的 `[ERR]` 哨兵留痕是唯一例外,且每通道每开机一次),
|
||||
> `*_init()` 才可在内核前调。
|
||||
> 全文 → `docs/算法/采集与算法层设计说明.md` §10。
|
||||
|
||||
**解决的问题**:V0.1 上 8 路电气量被两片转换器分片(硬件缺陷 **N-18**),且网名 `AINn`
|
||||
≠ 板级 `ADCn` ≠ 转换器通道号(13/17 路置换)。若消费侧直接索引驱动缓冲,改版即全线错位。
|
||||
本层把"哪一路"收进一张表,上层**按语义取数、不写死下标**。
|
||||
### 3.1 `adc_input.c` / `adc_input.h` — L1 语义取数
|
||||
- 语义映射表 `k_adc_map[]` 的**内容**唯一权威是 `docs/硬件平台/ADC通道映射说明.md`;V0.2 改版只改表内容,API 不动。
|
||||
- `adc_input_poll()` **仅任务上下文**(TPAFE 快照是临界区);STM32 侧固定滞后 1 拍。
|
||||
- 四类 STM32 计数(`adc1_seq` / `adc1_gap` / `adc1_err` / `adc1_hw`)成因不同,**勿混读**。
|
||||
- 本目录**唯一**允许出现 HAL 调用的文件。→ `docs/算法/采集与算法层设计说明.md` §3。
|
||||
|
||||
| API | 干什么 | 注意 |
|
||||
### 3.2 `sample_ring.c` / `.h` — L2 输入采样环
|
||||
- 整帧写入后才推进写指针 → 取到的窗永远落在整帧边界。
|
||||
- 本拍取数失败 → **整拍跳过入环**(`adc_task.c` 的 `skip`),**绝不补 0 点**。
|
||||
- 不做跨通道运算、不做丢帧判定(只出 `frames()` 计数)。→ `docs/算法/采集与算法层设计说明.md` §5。
|
||||
|
||||
### 3.3 `phasor.c` / `.h` — L2 相量计算
|
||||
- 系数表预乘 √2/N → `fAmSQ` **直接是有效值**(口径 `docs/算法/傅氏算法详解.md` §2.3)。
|
||||
- 实例表 `k_inst[]` 数据驱动:**新增实例只在表尾加一行**。
|
||||
- 任一实例不足一周波 → **本拍整体不产出**(不切使用区);每拍 20 点**全部重算**(不做增量递推)。
|
||||
- → `docs/算法/采集与算法层设计说明.md` §6。
|
||||
|
||||
### 3.4 `adc_health.c` / `.h` — L1 有效性判定
|
||||
- 三类位**严格分开**:电平(可自愈)/ 锁存(必留痕)/ 边沿事件(仅当拍);**静默 ≠ 丢帧**。
|
||||
- **gap 不告警**(消费者超拍是已知现象);告警编排在 `adc_task.c`,不在本层。
|
||||
- 首拍 `check()` 只建基线。→ `docs/算法/采集与算法层设计说明.md` §4。
|
||||
|
||||
### 3.5 `prot_set.c` / `.h` — 保护定值模型(L5-1 载体)
|
||||
- 装载 **all-or-nothing**:任一项越界整表不装载,越界项由 `prot_set_bad_mask()` 指认。
|
||||
- 批量 `prot_set_set()` 后**必须** `prot_set_seal()`;单项原子写用 `prot_set_set_sealed()`。
|
||||
- 定值项 / 实例**只许尾部追加**(枚举序 = 数组序 = 校验和覆盖序)。
|
||||
- blob **换代(返回 2)≠ 损坏(返回负)**:前者打 INFO、不记自检。→ `docs/算法/采集与算法层设计说明.md` §8、§12.3。
|
||||
|
||||
### 3.6 `prot_elm.c` / `.h` — L3 保护元件框架
|
||||
- 数据缺失两种处理**刻意不同**:相量类任一未就绪 → 整拍跳过;频率类无有效读数 → 按方向填"必不启动"值,**不整拍跳过**。
|
||||
- `prot_set_locked()` 或投退 = 0 → 元件整体复位不判据;跨上下文复归走 `prot_elm_request_reset()`。
|
||||
- 零序过流(IN 走 STM32)**故意不在实例表**;OF / UF 无独立 104 遥信点,只经 YX15 总聚合可见。
|
||||
- `prot_elm_init()` 须在 `phasor_init()` 之后。→ `docs/算法/采集与算法层设计说明.md` §9。
|
||||
|
||||
### 3.7 `freq.c` / `.h` — 频率测量(L3-5 的测量输入)
|
||||
- `freq_run()` **仅 adcTask 上下文**,须在 `phasor_run()` 之后同拍调;保护口径 =
|
||||
`freq_get() == 0 && freq_valid() == 1`。
|
||||
- 精度靠**自适应窗长**(窗长 = 泄漏纹波周期的整数倍);定长平均压不到 0.02 Hz,
|
||||
换窗长 = 换精度,二者不是独立旋钮。
|
||||
- 源切换即废相位基准重建(UA / IA 无共同相位基准,即双转换器无共同采样触发)。
|
||||
- 判据 **J1~J6** 与实测口径 → `docs/算法/采集与算法层设计说明.md` §7.2(与 `test/host/freq/freq_test.c` 文件头同源)。
|
||||
|
||||
### 3.8 `adc_cal.c` / `.h` — L1.5 通道标定折算(「数据可信」卡 2 / 卡 4)
|
||||
- 折算模型:工程量 = (code + b) × k;**k 落盘用 int32 Q16(µV/码 × 65536)**、b 用
|
||||
int32 码值(负零点靠 `CFG_VAL_INT`)——单位与定点格式的选型依据见
|
||||
`docs/任务规划/数据可信.md` §4.1,勿改回 V/码 直存(Q16 只剩 20 个刻度)。
|
||||
- **ROM 默认 = 卡 1 定死的引脚侧 LSB**(TPAFE 152.59 / STM32 805.66 µV/码,
|
||||
`docs/硬件平台/ADC通道映射说明.md` §7.1);表下标 = 语义枚举(UA0…IN7),
|
||||
V0.2 改版与本目录 `adc_input` 映射表同步改。
|
||||
- 三态可用性 `adc_cal_flags()`:VALID(已按变比标定,二次值成立——卡 0b/12 前恒 0)
|
||||
/ CLIPPED(运行期按来源派生,V0.2 后自动清零)。**消费者判「能不能信」只许查它**,
|
||||
`adc_cal_apply()` 不返回可用性(「数据可信」§8 风险表)。
|
||||
- 合法带哨兵:k ∉ ROM×[0.5,2.0] 或 |b| > 8192 → 折算回退 ROM + 每通道每开机一条
|
||||
`[ERR]` + VALID 被压制(**即便文件位图说已标定**);这层兜底同时挡坏文件与坏标定。
|
||||
- `adc_cal_set()` / `adc_cal_two_point()`:整定与两点公式的算术核已齐备(单测还原合成
|
||||
k/b);**台架两点标定(卡 12)待实测**,本层不含注入。
|
||||
- cfg 两段 `[adc_cal]` / `[adc_cal_ofs]` **不挂回调**(RAM 副本即生效值,引擎 load/poll
|
||||
原地改写,逐点现读);注册在 `app_main_init()` 内核前完成,必须先于 `cfg_file_load`。
|
||||
|
||||
### 3.9 `meas.c` / `.h` — 遥测快照发布(L2 发布侧,「数据可信」卡 8)
|
||||
- **唯一发布者 = adcTask**:`phasor_run()`/`freq_run()` 同拍、200 ms **tick 经过**分频
|
||||
调 `meas_tick()`(非拍计数——idle/skip 拍拉长名义节拍)。docs/任务规划/数据可信.md §8
|
||||
风险表"快照双写"行 = 评审判据:全仓 `meas_tick` 调用点数须为 1;iec104Task /
|
||||
uiTask 只读。
|
||||
- 值口径(批 2 卡 8 A-lite 裁决):**基波有效值瞬时透传**,与 phasor 使用区
|
||||
**同拍同核逐位相等**(单测钉死);**不做** 4 点平均 / 谐波方和根——按
|
||||
docs/任务规划/数据可信.md §4.5"上送节拍非平均窗,再平均引入滞后";谐波 k-bin 轨
|
||||
(docs/算法/继电保护功能复现规划.md L2-8)扩展只许结构尾部追加,
|
||||
docs/任务规划/数据可信.md §4.5 字段序 = 协议(布局由单测按偏移钉死,72 B × 双区)。
|
||||
- 并发 = phasor 双区 + 单字节索引翻转(L2-7 同型,无锁);seq 只增供消费者判换帧。
|
||||
- 品质注入:削顶位图唯一源 = `adc_cal_flags()`(口径 docs/任务规划/数据可信.md §4.2
|
||||
三态表;由 adc_task 编排注入,本层不链 `adc_cal`);TEMP = 最新一拍原始值透传 +
|
||||
`NT`(「数据可信」决策 5);`MEAS_QDS_*` 低字节与 104 QDS 线上字节同值
|
||||
(本层**不** include 协议层头——分层方向)。
|
||||
- 104 死区迁移 / 频率点绑定 = 卡 9/卡 11(道D 后续分支);读数与注入源比对 = **待台架实测**。
|
||||
|
||||
## 4. 构建与测试
|
||||
|
||||
### 4.1 挂载与模拟器边界
|
||||
- 九个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`;`../App/algo` 已在 `<IncludePath>` 内。
|
||||
- 本层**不进** `simulator/CMakeLists.txt`(UI 模拟器不跑采集链路):九个文件全部登记在
|
||||
`scripts/check_registration.py` 的 `SIM_EXEMPT`(附理由),`registration-check` 钩子反向校验(登记簿条目「rs485_task 白名单缺口」)。
|
||||
|
||||
### 4.2 主机侧单测(L0.5)——生产源文件以 `#include` 原样纳入
|
||||
|
||||
| 模块 | 命令 | pre-commit 钩子 |
|
||||
|---|---|---|
|
||||
| `adc_input_init()` | 清缓冲与统计 + **映射表自检**(每个 STM32 语义通道必须落在扫描 Rank 序列内)+ `HAL_ADC_Start_DMA()` 挂载乒乓缓冲(**只挂一次**) | 内核启动前调用;`-1` = 映射指向了从不扫描的通道(会恒读陈旧值);`-2` = DMA 挂载失败 |
|
||||
| `adc_input_poll()` | 采一拍:`tpafe5160_snapshot()` 原子快照(8 通道)+ **消费刚完成的那半区**(`stm32_take()`,按 `seq` 奇偶选半区,零忙等) | **仅任务上下文**(快照走临界区);只搬码值,不做标定/运算;STM32 侧固定滞后 1 拍 |
|
||||
| `adc_input_raw(ch, &out)` | 按语义读原始码值(TPAFE 16 位补码 / STM32 0..4095) | 消费侧取数**唯一入口**;须先成功 `poll()`;`-2` = 该来源尚无有效数据 |
|
||||
| `adc_input_desc(ch)` | 语义 → 物理通道描述(来源 + 下标/通道号) | 供自检注入、UI 通道序;返回静态地址,勿改 |
|
||||
| `adc_input_stm32_rc()` | STM32 侧最近一拍状态码(0 OK / -2 本拍无新帧) | 心跳日志 `adc1_rc` |
|
||||
| `adc_input_stm32_seq()` / `_errs()` / `_gaps()` / `_hw_errs()` | DMA 整帧完成计数 / 停摆(连续 ≥2 拍无新帧)/ 丢帧(seq 一次前进 >1)/ HAL 错误回调 | 心跳日志 `adc1_seq` / `adc1_err` / `adc1_gap` / `adc1_hw`——四类成因不同,**勿混读** |
|
||||
| 相量 / 采样环 | `& '.venv\Scripts\python.exe' test/host/phasor/run_unit_test.py` | `phasor-unit-test` |
|
||||
| ADC 输入层 | `& '.venv\Scripts\python.exe' test/host/adc_input/run_unit_test.py` | `adc-input-unit-test` |
|
||||
| 有效性校验 | `& '.venv\Scripts\python.exe' test/host/adc_health/run_unit_test.py` | `adc-health-unit-test` |
|
||||
| 定值模型 | `& '.venv\Scripts\python.exe' test/host/prot_set/run_unit_test.py` | `prot-set-unit-test` |
|
||||
| 元件框架 | `& '.venv\Scripts\python.exe' test/host/prot_elm/run_unit_test.py` | `prot-elm-unit-test` |
|
||||
| 频率测量 | `& '.venv\Scripts\python.exe' test/host/freq/run_unit_test.py` | `freq-unit-test` |
|
||||
| 通道标定(含真实 cfg_file 引擎集成) | `& '.venv\Scripts\python.exe' test/host/adc_cal/run_unit_test.py` | `adc-cal-unit-test` |
|
||||
| 遥测快照(布局钉死/双区无撕裂/品质汇总) | `& '.venv\Scripts\python.exe' test/host/meas/run_unit_test.py` | `meas-unit-test` |
|
||||
|
||||
**要点**
|
||||
改本目录任一 `.c/.h`,提交时对应钩子自动跑。**用例清单与判据数字的属主**是各
|
||||
`test/host/<模块>/` 与 `docs/测试说明/` 下的单测说明,本文不复制(三处抄数字必然打架)。
|
||||
|
||||
- 语义映射表 `k_adc_map[]` 的**唯一权威口径**是 `docs/硬件平台/ADC通道映射说明.md`:
|
||||
`UA/UN/IA` → TPAFE `buf[6]/[5]/[2]`;`UB/UC/IB/IC/IN/TEMP` → ADC1 `CH14/3/6/1/2/5`。
|
||||
**V0.2 改版只改这张表的内容**,本层 API 与上层语义不动。
|
||||
- STM32 侧是**顺序扫描**(Rank1..8 = CH0..CH7、Rank9 = CH14,与 `MX_ADC1_Init()` 严格一致),
|
||||
9 个通道**不是同一时刻**(一轮 ≈67 µs)。v2.0 起这一轮由 **TIM3 TRGO 硬件触发 + DMA 自动搬运**,
|
||||
消费侧**零忙等**(旧实现逐通道轮询、单通道超时 2 ms,已于 2026-10-10 移除)。
|
||||
代价:STM32 侧数据**固定滞后 1 拍(1 ms)**——与 TPAFE 侧本来就没有同刻性(N-18),无新增误差。
|
||||
- 本文件是本层**唯一**允许出现 HAL 调用的地方(它必须驱动 ADC1);新增模块若不需要外设,
|
||||
请保持 `sample_ring` / `phasor` 那种零依赖形态,才能免桩单测。
|
||||
### 4.3 板端
|
||||
`@build` 0 错误 0 警告 → `@test` 看 `[ADC_TASK]` 首拍 / 心跳(`phasor(...)`、`freq(...)`、
|
||||
`cal: ua_mv=… vl=0x00 cl=0xE6 rail=…`、`ring` / `skip` / `adc1_*` / `tgap`);
|
||||
健康判据 = **零 `[ERR]`**(健康告警与标定哨兵全以 `[ERR]` 开头,误报会被闭环直接抓住;
|
||||
`vl` 在未标定默认构建必须为 0x00,卡 5 闭锁落地后 `cal:` 行就是台架期望的直接证据)。
|
||||
→ `docs/算法/采集与算法层设计说明.md` §12。
|
||||
|
||||
### 3.2 `sample_ring.c` / `sample_ring.h` — L2 输入采样环
|
||||
|
||||
**解决的问题**:相量计算要"以当前时刻为终止的一周波",而 L1 每次只给最新一帧。
|
||||
本层按**语义通道**存最近 `SAMPLE_RING_CYCLES = 2` 个周波(`SAMPLE_RING_DEPTH = 40` 点),
|
||||
与参考系统 `CN_BUFSAM_CYCLE = 2` 一致,并为后续加长数据窗(1.5~2 周波抗衰减直流)预留空间。
|
||||
|
||||
| API | 干什么 | 返回 |
|
||||
|---|---|---|
|
||||
| `sample_ring_init()` | 清全部通道历史与计数(幂等,内核启动前可调) | — |
|
||||
| `sample_ring_push_frame(values, n)` | 整帧压入(长度须 `= ADC_SEM_COUNT`) | `0` / `-1` 参数错 |
|
||||
| `sample_ring_window(ch, n, out)` | 取该语义通道最近 `n` 点,**按时间序旧 → 新**(`out[n-1]` 最新) | `0` / `-1` 参数错 / `-2` 点数不足 |
|
||||
| `sample_ring_filled()` | 已累积点数(**饱和**于 40)——判"够不够一周波" | `0..40` |
|
||||
| `sample_ring_frames()` | 累计入环帧数(**不饱和**,1 kHz 下约 49.7 天回绕) | 帧数 |
|
||||
|
||||
**要点**
|
||||
|
||||
- **整帧写入后统一推进写指针** → 单帧内不可能"半新半旧",消费侧取到的窗永远落在整帧边界。
|
||||
- 本层**只做按时序存/取,不做任何跨通道运算**:同刻性由上游 L1 负责,本层既不保证也不假设。
|
||||
- `frames()` 与 L1 的 `tpafe5160_frame_seq()` 对照即可判丢帧(L1-6 的判据之一);
|
||||
本层只提供计数,**判定不在这里**。
|
||||
- 上层若某通道本拍取数失败,应**整拍跳过入环**(现 `adc_task` 的 `s_frames_skip` 即此),
|
||||
切勿补 0 点——0 会污染相量窗。
|
||||
|
||||
### 3.3 `phasor.c` / `phasor.h` — L2 相量计算
|
||||
|
||||
**算法**:全周傅氏(单频 DFT),`N = PHASOR_N = 20`(1 kHz @ 50 Hz)。系数表
|
||||
`k_fft_sin/k_fft_cos` **离线生成**并**预乘 √2/N**(口径 `docs/算法/傅氏算法详解.md` §2.3),
|
||||
故 `fAmSQ = √(fRe²+fIm²)` **直接就是有效值**,`fAm = √2·fAmSQ` 为峰值。运行期不调 `sinf/cosf`。
|
||||
|
||||
| API | 干什么 | 注意 |
|
||||
|---|---|---|
|
||||
| `phasor_init()` | 双区清零 + 使用区/就绪复位 + `sample_ring_init()` | 幂等;内核启动前调用 |
|
||||
| `phasor_inst_count()` / `phasor_inst(idx)` | 遍历实例表(`{name, sem, harm}`) | 返回静态地址,勿改;越界 `NULL` |
|
||||
| `phasor_run()` | 逐实例取 20 点窗 → 重算 → 写计算区 → **一次性切换使用区** | 仅任务上下文;须先 `push_frame()` |
|
||||
| `phasor_get(idx, &out)` | 读**使用区**结果 | `0` / `-1` 参数错 / `-2` 未攒满一周波 |
|
||||
| `phasor_calc(x, n, harm, &out)` | 对给定窗口算 `harm` 次相量 | **纯函数**,可脱离采样环独立单测 |
|
||||
|
||||
**要点**
|
||||
|
||||
- **实例表数据驱动**(`k_inst[]`,现 5 行):`C01-IA-F1`(过流/过负荷)、`C02-UA-F1`(过压/欠压/频率)、
|
||||
`C03-UN-F1`(零序过压)、`C04-IA-F2`(二次谐波制动)、`C05-IA-F5`(五次谐波选线)。
|
||||
**新增实例只在表尾加一行**,遍历/日志/单测自动适配。
|
||||
- **双区(计算区 / 使用区)**:结果先写非使用区,全部实例写完后单字节 `volatile` store 切换
|
||||
`s_use_zone`(Cortex-M 上原子)→ 消费侧无锁、无撕裂。今日唯一消费者是 adcTask 自身,
|
||||
该机制为后续独立 L3 保护任务预留。
|
||||
- **任一实例数据不足一周波 → 本拍整体不产出**(不切换使用区),避免"部分新部分旧"的混合结果。
|
||||
- 每拍 20 点**全部重算**(暴力法,无累积误差;迭代加减首尾项会累积)。相量刷新周期 = 1 ms。
|
||||
- 相位(L2-4)**未常驻**结构体,需要时自行 `atan2f(fIm, fRe)`。
|
||||
|
||||
### 3.4 `adc_health.c` / `adc_health.h` — L1-6 采样有效性判定
|
||||
|
||||
**解决的问题**:驱动侧只有"观测计数"(`tpafe5160_frame_seq/_frstdata_err`、
|
||||
`adc_input_stm32_seq/_errs/_gaps/_hw_errs`),**没有判定**——芯片静默、消费者超拍丢帧、
|
||||
计数器被改写这些故障此前只能靠人读心跳日志发现。本层把判定收进纯逻辑,
|
||||
告警编排留在消费者(`adc_task.c` 的 `adc_health_report()`)→ 判定可主机免桩单测。
|
||||
|
||||
| API | 干什么 | 注意 |
|
||||
|---|---|---|
|
||||
| `adc_health_init()` | 清全部状态(幂等) | 首拍 `check()` 只建基线、不判定 |
|
||||
| `adc_health_check(&in, &out)` | 吃计数快照做一次判定(每拍调,常数次整数运算) | 差值法 `(int32_t)(now-prev)`,tick/计数回绕安全 |
|
||||
| `adc_health_flags()` | 电平故障位:`TPAFE_STALL` / `STM32_STALL`(≥100 ms 无前进,可自愈)/ `SEQ_BACKWARD`(锁存) | 为 L5-4 闭锁预留 |
|
||||
| `adc_health_sticky()` | 锁存位(含 `FRSTDATA` / `STM32_ERR` / `HW_ERR` 增量痕迹) | 自愈后仍留痕 |
|
||||
| `adc_health_tpafe_gaps()` | TPAFE 累计丢帧点数(序号前进 >1 的超出部分) | 与 STM32 侧 `adc_input_stm32_gaps()` 对称 |
|
||||
|
||||
**要点**
|
||||
|
||||
- **三类语义严格分开**:电平位(stall,可自愈)/ 锁存位(出现过必留痕)/ 边沿事件位
|
||||
(仅当拍有效)。`ADC_HE_FRSTDATA/STM32_ERR/HW_ERR` 与同名 `ADC_HF_*` **编号对齐**,
|
||||
消费者直接用事件掩码对"已留痕位图"做每类一次 `SYSLOG_SELFTEST` 的编排。
|
||||
- **gap 不告警**:消费者超拍(心跳日志 ~2 ms)是已知现象,`adc1_gap`/`tgap` 每 10 s 心跳
|
||||
可见;若按事件告警会把真故障淹掉(台架实测开机 `adc1_gap≈24`、稳态每 10 s +2)。
|
||||
- **静默 ≠ 丢帧**:d=0 才是静默候选;d>1 只说明消费者没跟上,生产者还活着——两者
|
||||
处置完全不同,判据不许混。
|
||||
- 告警策略(在 `adc_task.c`,不在本层):严重事件/电平上升沿 `DBG_ERROR` +
|
||||
`SYSLOG_SELFTEST`(每类每次开机一次,防刷屏写满 32KB selftest.log);下降沿 `DBG_INFO`。
|
||||
|
||||
### 3.5 `prot_set.c` / `prot_set.h` — 保护定值模型(P1 / L5-1 载体)
|
||||
|
||||
**解决的问题**:保护判据的"输入宪法"。参考系统把定值放 EEPROM + `tagPRS` 常量表
|
||||
校验和自检;本层做 **RAM 模型 + 持久化纯逻辑层**(blob 序列化/解析;文件胶水在
|
||||
`App/cfg/prot_set_store.c`,B7),把编号 / 通讯地址 / 范围 / 默认值 / 校验和闭环钉死。
|
||||
|
||||
| API | 干什么 | 注意 |
|
||||
|---|---|---|
|
||||
| `prot_set_init()` | 装默认值 + 封存校验和(幂等) | 内核启动前可调 |
|
||||
| `prot_set_load_raw(vals)` | 整表装载(B7 由 `prot_set_store_parse` 内部走同一路径),逐项范围校验 | **all-or-nothing**:任一项越界整表不装载,`prot_set_bad_mask()` 指认越界项;影子区 + 单字交换,读侧无中间态 |
|
||||
| `prot_set_set(id, q10)` | 单项写入(范围校验,批处理路径) | **不自动重封存**——批量改完必须 `prot_set_seal()`,否则 selfcheck 判"非法改写";板端单项写一律用下行 |
|
||||
| `prot_set_set_sealed(id, q10)` | **原子写+封存(B7)**:影子区改好重封后单字交换 | selfcheck 永不见中间态(收口 B5 竞态窗口);置脏标志 |
|
||||
| `prot_set_dirty_peek/take/mark()` | 脏标志三件套(B7 落盘握手) | peek=iec104Task 通知;take=属主落盘前消费;mark=写失败回置重试 |
|
||||
| `prot_set_store_serialize/parse(buf,len)` | 持久化 blob(`PSET`+版本+值区+sum32 正/反码对) | parse **整表原子**:magic/版本/长度/校验对/范围任一失败 = 一字不动 |
|
||||
| `prot_set_get_f()` / `get_ctl()` | 工程量浮点 / 投退读取 | 单字读原子 |
|
||||
| `prot_set_selfcheck()` | 重算 sum32 与封存正/反码比对,失配 → 闭锁 | L5-1;seqlock 式复核:提交撞见 = 本轮跳过不误锁;`prot_set_seal()` 是解锁的唯一合法路径 |
|
||||
|
||||
**要点**
|
||||
|
||||
- **Q10 定点**(×1024)与参考 `2^10` 折算口径一致;元数据表(地址/范围/默认)是
|
||||
ROM 常量,校验和只覆盖 RAM 值区。
|
||||
- **通讯地址逐项对照参考** `Comm/Pub/Const_CommSetPara.c` `g_tCommSetParaTab`
|
||||
(基址 0x8820);单测 TC-PS-006 保证地址不重复、`min ≤ def ≤ max`、投退项 0..1。
|
||||
- ⚠ **默认值/范围为拟定值**(参考 EEPROM 出厂表不在本仓库),待对照修订——
|
||||
见规划 §5.4 落实进度注。
|
||||
- **B8 扩 8 项(32→40)**:`PS_OF_EN/OF_HZ/OF_DLY/UF_EN/UF_HZ/UF_DLY/RET_OF/RET_UF`
|
||||
**尾追加**(编号不回收,枚举序 = 数组序 = 校验和覆盖序)。OF/UF 的逐项通讯地址
|
||||
不在本仓库(参考保护定值段 0x8820~0x8882 仅示"含低压/低频"类目)→ 全部按
|
||||
**本地项 comm_addr=0** 登记(104 C_SE 不可寻址),待对照参考表补。
|
||||
频率返回系数**独立**(`RET_OF=0.99 / RET_UF=1.01`,返回门槛 = 定值×系数)——
|
||||
频率滞回带与电气量 0.95/1.05 不同量级,勿共用。
|
||||
- **B8 换代语义**:项数变 → blob 144→176 B、`PROT_SET_BLOB_VER` 1→2。B7 旧档
|
||||
(ver=1/32 项)在新固件 parse 即被**版本/长度闸**拒绝 → 出厂默认;
|
||||
`prot_set_store_load()` 返回码分流:**2 = 换代**(`prot_set_store.c` 打 INFO
|
||||
"prot set blob version change, defaults in use",**不是** CORRUPT、不记自检),
|
||||
−1 = 真损坏(defaultTask 打 ERROR + 自检留痕)。升级路径不得报成损坏。
|
||||
- **bad_mask 随项数扩为 `uint64_t`**(bit i = 第 i 项越界;消费方仅主机单测,
|
||||
板端闭锁走 `prot_set_locked()`)。
|
||||
|
||||
### 3.6 `prot_elm.c` / `prot_elm.h` — 保护元件框架(L3,P1 子集)
|
||||
|
||||
**解决的问题**:参考系统 89 个 `PRS_*.c` 各自实现"滞回 + 定时限"——本框架把这段
|
||||
公共核收敛为**一个通用核 + 数据驱动实例表**(同 `phasor` 的 `k_inst` 模式),
|
||||
新增元件 = 表加一行 + 定值加几项。
|
||||
|
||||
| API | 干什么 | 注意 |
|
||||
|---|---|---|
|
||||
| `prot_elm_init()` | 复位状态 + 解析相量实例索引(按名前缀 `C01/C02/C03`) | 须在 `phasor_init()` 之后调;`src=FREQ` 行无相量绑定(`pidx=0xFF`,run 不查它) |
|
||||
| `prot_elm_eval(now_ms, meas)` | **纯判据**:定值 RAM + 测量数组 → 全元件状态机 | 主机单测入口;NULL 测量按 0 |
|
||||
| `prot_elm_run(now_ms)` | 适配层:按实例表 `src` 组测量 → eval(`src=0` 取 `phasor_get()` rms,`src=1` 取 `freq_get()`) | 相量类任一未就绪 → **整拍跳过**(不引入陈旧判据);频率类无有效读数 → **按方向填"必不启动"值**(过量填 0 / 欠量填 `1e30` → 滞回比较恒假 → 元件复位),**不整拍跳过**——频率源缺失不得饿死电流/电压类元件 |
|
||||
| `prot_elm_state()` / `prot_elm_active_mask()` | 状态快照 / 动作位图 | 供 L4 出口与心跳日志 |
|
||||
|
||||
**要点**
|
||||
|
||||
- **滞回**:已启动用返回门槛(`定值 × 返回系数`),未启动用动作门槛——
|
||||
过量/欠量方向由实例表 `over` 位统一处理(`UV`/`UF` 为欠量实例)。
|
||||
- **定时限**:`(uint32_t)(int32_t)(now - t0) >= dly` 差值法,tick 回绕安全;
|
||||
`dly=0` → 启动同拍即动作。返回**即时**(参考带 10 ms 返回延时,P4 接出口硬件时再补)。
|
||||
- **闭锁联动**:`prot_set_locked()`(L5-1)或投退=0 → 该元件状态整体复位、不判据。
|
||||
- **V0.1 实例**(N-18):`OC1/OC2/OC3/OL`(IA)、`OV/UV`(UA)、`3U0`(UN 外部通道)、
|
||||
`OF/UF`(freq 层测频,B8)。零序过流(IN 走 STM32)**故意不在表中**。
|
||||
- **OF/UF 出口可见性(B8)**:两元件**未单独映射 104 遥信点**(送检点表 = B10 口径),
|
||||
其动作目前**只经 YX15 保护总聚合可见**(`iec104_pts` 的 fault-total 掩码含 OC/3U0,
|
||||
OF/UF 动作会置总位但无独立分点)——README 与规划 §3.3 L3-5 行必须同此口径,勿写"独立遥信"。
|
||||
- **板端接线已落地(B5,2026-10-12)**:`App/task/protect_task.c`(protectTask,
|
||||
20 ms 节拍)消费 `prot_elm_run()` + L5-4 采样故障闭锁 + L5-1 校验和巡检;
|
||||
跨上下文复归走 `prot_elm_request_reset()`(104 YK11 落请求、eval 头部消费)。
|
||||
判据结果经 104 遥信上送(`iec104_pts` YX15~23/31)。→ `docs/算法/继电保护功能复现规划.md` §7
|
||||
⚠ 实例数 7→9(B8):protectTask 启动锚点 `elm=` 与 `prot_elm_active_mask()` 位宽随之变,
|
||||
`.serialtest` 锚点表未含 `elm=` 数字(已核),无需改配置。
|
||||
|
||||
### 3.7 `freq.c` / `freq.h` — 频率测量层(L2 派生量,L3-5 测量输入,B8)
|
||||
|
||||
**解决的问题**:过频/低频(L3-5)需要系统频率读数,而 L2 只出相量。本层把
|
||||
"相邻两拍基波相量的相位差"变成"频率",并解决**非整周波窗长的 DFT 泄漏纹波**
|
||||
这一决定精度的关键问题。
|
||||
|
||||
| API | 干什么 | 注意 |
|
||||
|---|---|---|
|
||||
| `freq_init()` | 清增量环/计数/读数/相位基准/有效标志(幂等) | 内核启动前可调,须在 `phasor_init()` 之后无要求 |
|
||||
| `freq_run()` | 每拍步进:选源(UA→IA 过幅值门)→ 共轭乘积 `P=Z(m)·conj(Z(m-1))` 取角 → 假值钳制 → 自适应窗均值 | **仅 adcTask 上下文**,须在 `phasor_run()` 之后同拍调 |
|
||||
| `freq_get(out)` | 读数(Hz) | 0 有读数(可能是保持值)/ −1 参数错 / −2 从未启动收敛(写 0) |
|
||||
| `freq_valid()` | "本拍新鲜"标志 | 1 = 幅值门 OK 且已收敛;保护消费口径 = `get()==0 && valid()==1` |
|
||||
| `freq_last()` | 最近有效读数 | 从未收敛返回 0.0 |
|
||||
|
||||
**算法要点(精度是实测驱动定稿的,两轮弯路记录在 freq.c 文件头)**
|
||||
|
||||
- **相位微分免解缠绕**:`arg(Z_now·conj(Z_prev))` 天然落在 (−π, π],纯音每拍
|
||||
角**增大** 2πf/Fs(本层相量约定 φ=atan2(fIm,fRe)=窗首初相,窗每拍前滑一点);
|
||||
45~55 Hz 段 |Δφ| ≈ 0.28~0.35 rad,距 ±π 裕度巨大。
|
||||
- **自适应窗长**(核心):20 点矩形窗在非整周波时混入**负序泄漏项**
|
||||
(45/55 Hz 处 |S₂/S₁| ≈ 3~5%),相量端轨迹成椭圆 → 逐拍相位差带周期
|
||||
δ(φ) 纹波,周期 = fs/(2f) ≈ 9.1~11.1 拍。定长窗平均不整除纹波周期 →
|
||||
扫频最坏误差 **0.317 Hz**(40 拍算术平均)/ **0.197 Hz**(60 拍复数和 Kay,
|
||||
交叉项和角不随 K² 衰减,勿再尝试)。改 `K_eff = round(8·Fs/(2·f̂))`
|
||||
(45~55 Hz 段 73~89 拍)使窗长恰为纹波周期整数倍 → 纹波相消,最坏 **0.0101 Hz**。
|
||||
- **假值钳制带 35~65 Hz**(宽于系统合理带 40.5~60.5 = 参考 `CN_HERTZ_SOFTFR_MIN/MAX`,
|
||||
规划 §10.1):单拍相位差本身带 ±5 Hz 纹波,钳制带贴边会**非对称丢弃纹波半周**
|
||||
→ 均值偏置(实测 45 Hz 点 +0.16 Hz,已修)。35~65 仍拦得住窗滑 2 拍的 ≈2f
|
||||
(90~110 Hz)与相位跳变假值;系统合理带由 OF/UF 定值范围在消费侧把关。
|
||||
- **幅值门** `FREQ_AMP_MIN=50.0`(原始码值口径,**拟定待 L1-5 标定复核**):
|
||||
低于门 = 无有效波形(浮空/PT 断线 rms≈0,相位无意义)→ 停更新、`valid` 清零、
|
||||
读数保持 → 台架浮空不发散。信号消失过渡段(≤20 ms 残窗)读数漂移有界 ≤0.3 Hz。
|
||||
- **源优先级 UA(C02)→IA(C01)**,按名前缀解析、每拍动态回退(台架只注电流亦可测频);
|
||||
源切换即废相位基准重建(UA/IA 无共同相位基准,N-18)。
|
||||
|
||||
**精度判据(J1~J6,与 `test/host/freq/freq_test.c` 文件头同口径,数字为实测)**
|
||||
|
||||
| 判据 | 条件 | 门槛 | 实测最坏 |
|
||||
|---|---|---|---|
|
||||
| J1 扫频 | 纯音 45.00~55.00 Hz,0.5 Hz 步进(21 点) | **≤ 0.02 Hz** | **0.0101 Hz** @55.0 Hz |
|
||||
| J2 噪声 | 幅值 1000 码、噪声 rms = 1% 幅值 | **≤ 0.10 Hz** | **0.0160 Hz** |
|
||||
| J3 启动收敛 | 首个有效相量 → 首个有效读数 | **≤ 40 ms** | 20 ms(@39 ms 拍,相量 @19 ms) |
|
||||
| J4 频率阶跃 | 50→51 Hz 进入 ±0.05 Hz 并保持 | **≤ 120 ms** | 98 ms(稳态 err 0.0053 Hz) |
|
||||
| J5 零幅/浮空 | rms < 幅值门 | valid=0、不发散、保持漂移 ≤0.3 Hz | 漂移 0.13 Hz,无 NaN |
|
||||
| J6 假值钳制 | 幅值+频率双跳变拍 | 60 ms 内回 ±0.05 Hz | 0.0000 Hz |
|
||||
|
||||
附加鲁棒性(同套件实测):直流偏置 8000 码 → err 0.0018 Hz;3/5 次谐波 20%/10%
|
||||
→ err 0.0065 Hz;UA 无幅值落 IA → err 0.0020 Hz。
|
||||
|
||||
**J4 = 120 ms 是"精度优先于响应"的显式取舍(Lead 批准 2026-10-12)**:自适应窗
|
||||
(M=8 ≈ 4 个泄漏纹波周期 ≈ 4 个信号周波)换来 0.317→0.0101 Hz,代价是频率骤变后
|
||||
的窗冲刷(实测 98 ms)。OF/UF 定时限默认 **1000 ms** → 测频响应占保护总动作时间
|
||||
**<10%**;暂态假值另有 J6(跳变后 ≤60 ms 回 ±0.05 Hz)兜底。故 120 ms 是工程整定,
|
||||
不是判据失守——若将来要压响应,须同时重估精度(缩短窗 = 精度回退到 0.2~0.3 Hz 档)。
|
||||
|
||||
**板端可见性(DoD)**:adcTask 心跳/首拍日志追加一行
|
||||
`freq(hb): f=5000 valid=1 (x0.01)`(MicroLIB 无 %f → ×0.01 定点,分辨率 0.01 Hz)。
|
||||
正常带波形应 `valid=1` 且 `f≈5000`;浮空应 `valid=0`、`f` 为保持值。
|
||||
|
||||
## 4. 关键约定
|
||||
|
||||
- **语义映射唯一权威口径**:`docs/硬件平台/ADC通道映射说明.md`。取数一律经 `adc_input_raw()`,
|
||||
禁止在消费侧直接索引驱动缓冲或写死下标。
|
||||
- **禁止跨转换器运算**(V0.1 硬件缺陷 N-18):`UA/UN/IA` 在 TPAFE、`UB/UC/IB/IC/IN` 在 STM32 ADC1,
|
||||
两者无共同采样触发 → 无同一时刻性、相位不确定。故本层**不做**跨转换器的相位 / 功率 /
|
||||
功率因数 / 负序零序类矢量运算(V0.2 把 8 路电气量全接 TPAFE 后方可解禁)。
|
||||
- **标定(L1-5)尚未实现**(缺标准源):现以**原始码值**充当工程量(系数 = 1),
|
||||
因此相量读数只证明**链路通**,**不可作幅值精度判据**。
|
||||
- **本层不建任务、不等事件、不写日志之外的副作用**;日志由调用侧(adcTask)打。
|
||||
- 内核启动前只允许调 `*_init()`;`adc_input_poll()` / `phasor_run()` 仅任务上下文。
|
||||
|
||||
## 5. 静态足迹(BSS,供栈/RAM 预算参考)
|
||||
|
||||
| 位置 | 量 |
|
||||
|---|---|
|
||||
| `sample_ring.c` `s_ring[9][40]` float | **1440 B** |
|
||||
| `phasor.c` `s_zone[2][5]` `phasor_t` | 160 B |
|
||||
| `adc_input.c` `s_stm32_raw[16]` + `s_tpafe_raw[8]` | 48 B |
|
||||
| `adc_health.c` 内部状态(9 × uint32 + uint8) | ≈ 40 B |
|
||||
| `prot_set.c` `s_val[2][40]` + 封存/位图(64 位)/闭锁 | 336 B(B8:31→40 项) |
|
||||
| `prot_elm.c` `s_rt[9]`(16 B/实例) | 144 B(B8:7→9 实例) |
|
||||
| `freq.c` 相位增量环 `fq_d[96]` + 占位标志 + 状态 | **≈ 520 B**(B8 新增) |
|
||||
| 运行期栈:`phasor_run()` 的 `win[20]` | 80 B/帧 |
|
||||
| 运行期栈:`freq_run()` 局部 | ≈ 30 B/帧(无大局部数组) |
|
||||
|
||||
改容量(`SAMPLE_RING_CYCLES` / 实例数 / `ADC_SEM_COUNT` / `FREQ_AVG_TAPS`)前后各跑一次
|
||||
`& '.venv\Scripts\python.exe' scripts/stack_report.py`(口径见 `mem-budget` skill)。
|
||||
**B8 实测(adcTask 静态最坏链)**:见 READY 消息与 `docs/内存管理/` 台账回填。
|
||||
|
||||
## 6. 构建与测试
|
||||
|
||||
- **挂载**:八个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`,`../App/algo` 已在 `<IncludePath>` 内。
|
||||
本层**不进** `simulator/CMakeLists.txt`(UI 模拟器不跑采集链路)——纯算法且模拟器无消费路径的
|
||||
新文件(`prot_set.c` / `freq.c`)改在 `scripts/check_registration.py` 的 `SIM_EXEMPT` 登记豁免
|
||||
(`registration-check` 钩子反向校验,N-23)。
|
||||
- **主机侧单测(L0.5)**——生产源文件以 `#include` 原样纳入,无需改产品代码:
|
||||
|
||||
| 套件 | 命令 | pre-commit 钩子 |
|
||||
|---|---|---|
|
||||
| 相量层(57 用例:系数表逐项复核 / 幅值·相位 / 谐波抑制与提取 / 频偏 / 采样环 / 双区交换) | `& '.venv\Scripts\python.exe' test/host/phasor/run_unit_test.py` | `phasor-unit-test` |
|
||||
| ADC 输入层(逐 Rank 注入互异码值钉死映射 + HAL 启动失败/超时分支) | `& '.venv\Scripts\python.exe' test/host/adc_input/run_unit_test.py` | `adc-input-unit-test` |
|
||||
| 有效性校验(31 用例:100 ms 静默门限两侧 / 丢帧边沿 / 倒退锁存 / 错误增量 / tick 回绕 / NULL 守卫 / init 幂等) | `& '.venv\Scripts\python.exe' test/host/adc_health/run_unit_test.py` | `adc-health-unit-test` |
|
||||
| 定值模型(90 用例:范围校验 / all-or-nothing 装载 / 封存-自检-闭锁 / Q10 往返 / 元数据表自检 / B7 blob 往返与损坏拒绝 / **B8 OF-UF 行 + 64 位 bad_mask + 换代拒绝**) | `& '.venv\Scripts\python.exe' test/host/prot_set/run_unit_test.py` | `prot-set-unit-test` |
|
||||
| 元件框架(47 用例:滞回与定时限 / 欠量方向 / 投退 / 闭锁联动 / 时基回绕 / 位图聚合 / **B8 OF-UF 滞回与方向隔离 + run() 测频接线**) | `& '.venv\Scripts\python.exe' test/host/prot_elm/run_unit_test.py` | `prot-elm-unit-test` |
|
||||
| **频率测量(46 用例:J1 扫频 45~55 Hz / J2 噪声 / J3 启动收敛 / J4 阶跃 / J5 浮空不发散 / J6 假值钳制 / 直流·谐波抑制 / UA→IA 回退 / 守卫与幂等)** | `& '.venv\Scripts\python.exe' test/host/freq/run_unit_test.py` | `freq-unit-test`(**待登记**,合并时由 Lead 加钩子) |
|
||||
|
||||
改本目录任一 `.c/.h`,提交时对应钩子自动跑;详见 `docs/测试说明/相量层单元测试说明.md`。
|
||||
- **板端**:`@build` 0 错误 0 警告 → `@test` 看 `[ADC_TASK]` 首拍/心跳日志(`phasor(...)` 行、
|
||||
`freq(...)` 行、`ring` / `skip` 计数、`adc1_rc` / `adc1_err`、`tgap`);健康判据 = 零 `[ERR]`
|
||||
(`adc_health_report` 的告警全部以 `[ERR]` 开头 → 误报会被闭环直接抓住)。
|
||||
- **定值档升级 vs 损坏(B8,台架判读)**:`prot_set_store_load()` **rc=2 = 换代**
|
||||
(旧固件/旧项数的档被版本闸拒绝)→ 只打 INFO `prot set blob version change, defaults in use`,
|
||||
**不是** `[ERR] ... CORRUPT`、不记自检;**rc<0 = 真损坏** → `[ERR] prot set blob CORRUPT` + 自检留痕。
|
||||
升级固件后首启看到"version change"一行属**预期**,勿当故障排查。
|
||||
|
||||
## 7. 新增一个算法模块的固定动作
|
||||
## 5. 新增一个算法模块的固定动作
|
||||
|
||||
1. 在本目录建 `xxx.c/h`,文件头写模块注释块(规范 V1.0),**尽量零平台依赖**以便免桩单测。
|
||||
2. 用 `new-source-file` skill 注册到 `MDK-ARM/STM32F407-Demo.uvprojx`(目录已在 IncludePath,
|
||||
若新建子目录则必须补 IncludePath)。模拟器侧二选一:有消费路径 → 加
|
||||
`simulator/CMakeLists.txt`;**纯算法无消费路径 → 加 `scripts/check_registration.py`
|
||||
的 `SIM_EXEMPT`**(照 `prot_set.c` / `freq.c` 先例写理由),否则 `registration-check` 钩子拦。
|
||||
2. 用 `new-source-file` skill 注册到 `MDK-ARM/STM32F407-Demo.uvprojx`(新建子目录还要补 `<IncludePath>`)。
|
||||
模拟器侧二选一:有消费路径 → 加 `simulator/CMakeLists.txt`;纯算法无消费路径 → 加
|
||||
`scripts/check_registration.py` 的 `SIM_EXEMPT`(照本目录既有七条写理由),否则 `registration-check` 拦。
|
||||
3. 加 `test/host/<模块>/` 主机单测 + `.pre-commit-config.yaml` 钩子(照 `phasor-unit-test` 抄)。
|
||||
4. 消费者写在 `App/task/`,本层只出"数";跨转换器运算的禁令见 §4。
|
||||
4. 消费者写在 `App/task/`,本层只出"数";跨转换器禁令见 §3 开头四条红线。
|
||||
5. 在本 README §1 清单加一行,并在 `docs/算法/采集与算法层设计说明.md` 里写清该模块的契约与理由。
|
||||
|
||||
## 8. 相关文档
|
||||
## 6. 相关文档
|
||||
|
||||
- **本层设计与判据**:`docs/算法/采集与算法层设计说明.md`
|
||||
- 通道映射权威口径:`docs/硬件平台/ADC通道映射说明.md`
|
||||
- 分层与路线图(L1/L2 清单与状态):`docs/算法/继电保护功能复现规划.md`
|
||||
- 分层与功能项清单:`docs/算法/继电保护功能复现规划.md`
|
||||
- 傅氏算法与有效值口径:`docs/算法/傅氏算法详解.md`
|
||||
- 采样节拍(TIM3 1 kHz + TRGO 触发 ADC1/DMA):`docs/子系统设计/ADC采样节拍设计说明.md`
|
||||
- 相量层单测细则:`docs/测试说明/相量层单元测试说明.md`
|
||||
- 采样节拍(TIM3 1 kHz + TRGO 触发 ADC1 / DMA):`docs/子系统设计/ADC采样节拍设计说明.md`
|
||||
- 相量层 / ADC 输入层单测细则:`docs/测试说明/相量层单元测试说明.md`、`docs/测试说明/ADC输入层单元测试说明.md`
|
||||
- RAM 与栈台账:`docs/内存管理/RAM使用分析与优化方案.md`
|
||||
|
||||
@@ -0,0 +1,425 @@
|
||||
/*
|
||||
* 模块名称:通道标定与量纲折算(L1.5)——实现
|
||||
* 模块功能:见 adc_cal.h。折算模型(工程量 = (code + b) × k × 1e-6,k 为 Q16
|
||||
* µV/码)与两段 cfg 布局([adc_cal]:k_* + valid 位图;[adc_cal_ofs]:
|
||||
* b_*)照 docs/任务规划/数据可信.md §4.1 / §4.4 落地;系数口径
|
||||
* (ROM 默认 = 卡 1 定死的引脚侧 LSB)见 docs/硬件平台/ADC通道映射说明.md
|
||||
* §7.1。
|
||||
* 并发契约:cfg RAM 副本属主 = 配置引擎(defaultTask 的 load/poll 原地
|
||||
* 改写),本层逐点折算现读——int32 对齐字在 Cortex-M4 上读写原子,
|
||||
* poll 重载「复位默认 + 逐键解析」存在 µs 级窗口,撞上则该拍该通道
|
||||
* 为默认值/半新值(相量 20 点窗摊薄,且触发面 = 用户改文件后 4 s
|
||||
* 沉降一次性事件)——「数据可信」§4.4 明载此设计(不挂回调)。
|
||||
* 合法带哨兵(防损坏文件/错误标定把假电压送进下游):k ∉ ROM×[0.5,2.0]
|
||||
* 或 |b| > ADC_CAL_B_MAX → 该通道折算回退 ROM + b=0,flags 不报
|
||||
* VALID,每通道每次开机首见沿打一条 [ERR](adc_cal_init 重置留痕)。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-25(道A 卡 2/卡 4)
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 DTU 项目组 创建:Q16 系数/零点两段落盘 + 折算 + 三态 flags
|
||||
* v1.1 2026-10-25 DTU 项目组 卡 10(道D):新增 adc_cal_b_codes() 生效零点
|
||||
* 读取口;set() 授权写者扩至 iec104Task 直通路径
|
||||
* (run_par 0x8040~/0x8048~,契约同 §4.4)
|
||||
* + 合法带哨兵回退 + 两点标定公式(台架注入待跑)
|
||||
* v1.2 2026-10-26 DTU 项目组 道F task-16:新增 adc_cal_prot_blocked()——未标定
|
||||
* 闭锁(UA/UN/IA 三路 VALID 全齐才放行)收成本层单点,
|
||||
* DTU_BENCH 旁路同时收成全仓唯一一处;此前该判据在
|
||||
* protect_task.c 与 ui_data_feed.c 各写一份,两处一旦
|
||||
* 漂移,屏上「未标定」与判据实际停走就不是同一件事
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "adc_cal.h"
|
||||
|
||||
#include "cfg_file.h"
|
||||
|
||||
#define DBG_TAG "[CAL]"
|
||||
#include "dbg_log.h"
|
||||
|
||||
/* 常量定义区 */
|
||||
|
||||
/* 段名(≤ CFG_FILE_MAX_NAME_LEN)与键名(k_ua…b_in,均 4/5 字符)
|
||||
* ——「数据可信」§4.4 布局;键的书写顺序按该表,存储下标按语义枚举 */
|
||||
#define ADC_CAL_SEC_K "adc_cal"
|
||||
#define ADC_CAL_SEC_B "adc_cal_ofs"
|
||||
|
||||
/* Q16 → V 的合并比例因子:1e-6 / 65536(float 常量,精确到 1 ulp 以内即可:
|
||||
* 系数本身 Q16 舍入误差 < 0.001 %,见「数据可信」§4.1) */
|
||||
#define ADC_CAL_K_SCALE (1.52587890625e-11f)
|
||||
|
||||
/* 静态变量定义区 */
|
||||
|
||||
/* [adc_cal] 段 RAM 副本:k 按语义下标 0..7,valid 位图 bit=语义下标。
|
||||
* data_size 取 offsetof(valid)+1 = 33(尾随对齐填充不属于引擎视图,
|
||||
* 引擎「复位默认」按 data_size memcpy,必须覆盖 valid,见单测 TC-ADCCAL-001) */
|
||||
typedef struct {
|
||||
int32_t k[8];
|
||||
uint8_t valid;
|
||||
} adc_cal_kcfg_t;
|
||||
|
||||
/* [adc_cal_ofs] 段 RAM 副本:b 按语义下标 0..7(int32 有符号,卡 3 的
|
||||
* CFG_VAL_INT 是负零点偏移能干净落盘的前提) */
|
||||
typedef struct {
|
||||
int32_t b[8];
|
||||
} adc_cal_bcfg_t;
|
||||
|
||||
static adc_cal_kcfg_t s_kcfg; /* 生效系数副本(引擎原地更新) */
|
||||
static adc_cal_bcfg_t s_bcfg; /* 生效零点副本 */
|
||||
|
||||
/* 工厂默认(ROM 理论系数,卡 1 定死,「数据可信」§4.4):**下标 = 语义枚举**
|
||||
* (UA0 / UB1 / UC2 / UN3 / IA4 / IB5 / IC6 / IN7)——UA/UN/IA 走 TPAFE
|
||||
* (16 位 ±5 V 档),UB/UC/IB/IC/IN 走 STM32 ADC1;逐路对过
|
||||
* docs/硬件平台/ADC通道映射说明.md §3(IN=ADC11/STM32,勿再按分组顺序误写)。
|
||||
* ROM 表按**当前 V0.1 来源排布**显式书写(不查 desc 运行期派生),因为它是
|
||||
* 卡 1 定稿的存档事实;V0.2 改版(消除双转换器无共同采样触发)时本表与 adc_input 映射表同步改,
|
||||
* 上层语义与 API 不动。CLIPPED 位则**运行期按来源派生**(见 adc_cal_flags) */
|
||||
static const adc_cal_kcfg_t s_kcfg_rom = {
|
||||
/* idx0 UA / idx1 UB / idx2 UC / idx3 UN */
|
||||
{ ADC_CAL_K_Q16_TPAFE, ADC_CAL_K_Q16_STM32, ADC_CAL_K_Q16_STM32,
|
||||
ADC_CAL_K_Q16_TPAFE,
|
||||
/* idx4 IA / idx5 IB / idx6 IC / idx7 IN */
|
||||
ADC_CAL_K_Q16_TPAFE, ADC_CAL_K_Q16_STM32, ADC_CAL_K_Q16_STM32,
|
||||
ADC_CAL_K_Q16_STM32 },
|
||||
0u /* 出厂未标定:VALID 全 0 */
|
||||
};
|
||||
static const adc_cal_bcfg_t s_bcfg_rom = { { 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L } };
|
||||
|
||||
/* 合法带哨兵留痕位(bit=语义下标;每通道每次开机只打一条 [ERR],
|
||||
* adc_cal_init 清零 = 新一轮观测窗) */
|
||||
static uint8_t s_err_latched;
|
||||
|
||||
/* 键表:offset 用 offsetof 取真值(k[0]@0 / k[1]@4 … valid@32) */
|
||||
static const cfg_key_def_t s_k_keys_tbl[9] = {
|
||||
{ "k_ua", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[0]), 4u },
|
||||
{ "k_un", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[3]), 4u },
|
||||
{ "k_ia", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[4]), 4u },
|
||||
{ "k_ub", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[1]), 4u },
|
||||
{ "k_uc", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[2]), 4u },
|
||||
{ "k_ib", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[5]), 4u },
|
||||
{ "k_ic", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[6]), 4u },
|
||||
{ "k_in", CFG_VAL_INT, offsetof(adc_cal_kcfg_t, k[7]), 4u },
|
||||
{ "valid", CFG_VAL_UINT, offsetof(adc_cal_kcfg_t, valid), 1u },
|
||||
};
|
||||
static const cfg_key_def_t s_b_keys_tbl[8] = {
|
||||
{ "b_ua", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[0]), 4u },
|
||||
{ "b_un", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[3]), 4u },
|
||||
{ "b_ia", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[4]), 4u },
|
||||
{ "b_ub", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[1]), 4u },
|
||||
{ "b_uc", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[2]), 4u },
|
||||
{ "b_ib", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[5]), 4u },
|
||||
{ "b_ic", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[6]), 4u },
|
||||
{ "b_in", CFG_VAL_INT, offsetof(adc_cal_bcfg_t, b[7]), 4u },
|
||||
};
|
||||
|
||||
/* 函数定义区 */
|
||||
|
||||
/*
|
||||
* 函数功能:判定语义通道是否 STM32 源(经 adc_input 权威映射表,不另立第二份表)
|
||||
* 入口参数:sem - 语义通道 unsigned
|
||||
* 返 回 值:1 - STM32;0 - TPAFE(含 desc 取不到时的保守分支) int
|
||||
* 限定条件:无
|
||||
*/
|
||||
static int s_rom_k_stm32(unsigned sem) {
|
||||
const adc_channel_desc_t *d = adc_input_desc((adc_semantic_t)sem);
|
||||
|
||||
return ((d != NULL) && (d->source == ADC_SRC_STM32)) ? 1 : 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取语义通道的 ROM 默认系数(合法带/回退参照)
|
||||
* 入口参数:sem - 语义下标(0..7) unsigned
|
||||
* 返 回 值:Q16 系数 int32_t
|
||||
* 限定条件:sem < 8(调用方已先行拦截 TEMP/越界)
|
||||
* 函数说明:直接读出厂表本体——保证「哨兵合法带基准」与「引擎工厂默认」
|
||||
* 永远同一份数(两者若靠重复表达式各算各的,改一处漏一处就是暗雷)
|
||||
*/
|
||||
static int32_t s_rom_k(unsigned sem) {
|
||||
return s_kcfg_rom.k[sem];
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:k/b 合法带检查(ROM×[0.5,2.0] / |b| ≤ ADC_CAL_B_MAX)
|
||||
* 入口参数:sem - 语义下标(0..7) unsigned
|
||||
* k - 待检系数 Q16 int32_t
|
||||
* b - 待检零点 码值 int32_t
|
||||
* 返 回 值:1 - 合法;0 - 越带 int
|
||||
* 限定条件:sem < 8
|
||||
*/
|
||||
static int s_kband_ok(unsigned sem, int32_t k, int32_t b) {
|
||||
const int32_t rom = s_rom_k(sem);
|
||||
|
||||
if (k < (rom >> 1) || k > (rom << 1)) {
|
||||
return 0;
|
||||
}
|
||||
if (b > ADC_CAL_B_MAX || b < -ADC_CAL_B_MAX) {
|
||||
return 0;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:越带留痕(每通道每次开机首见沿一条 [ERR],init 重置)
|
||||
* 入口参数:sem - 语义下标 0..7;k/b - 现场值
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅 adc_cal_apply/set 内部
|
||||
*/
|
||||
static void s_note_bad(unsigned sem, int32_t k, int32_t b) {
|
||||
const uint8_t bit = (uint8_t)(1u << sem);
|
||||
|
||||
if ((s_err_latched & bit) == 0u) {
|
||||
s_err_latched |= bit;
|
||||
DBG_ERROR("ch=%u k_q16=%ld b=%ld out of ROM band [0.5x,2x]/|b|<=%ld -> ROM fallback",
|
||||
(unsigned)sem, (long)k, (long)b, (long)ADC_CAL_B_MAX);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读通道「当前生效且合法」的 k/b(越带就地回退 ROM)
|
||||
* 入口参数:sem - 语义下标 0..7;p_k/p_b - 输出
|
||||
* 返 回 值:1 - 用了合法值;0 - 已回退 ROM
|
||||
* 限定条件:sem < 8
|
||||
*/
|
||||
static int s_eff_kb(unsigned sem, int32_t *p_k, int32_t *p_b) {
|
||||
const int32_t k = s_kcfg.k[sem];
|
||||
const int32_t b = s_bcfg.b[sem];
|
||||
|
||||
if (s_kband_ok(sem, k, b) != 0) {
|
||||
*p_k = k;
|
||||
*p_b = b;
|
||||
return 1;
|
||||
}
|
||||
s_note_bad(sem, k, b);
|
||||
*p_k = s_rom_k(sem);
|
||||
*p_b = 0L;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:标定层初始化——RAM 副本置 ROM 默认 + 注册两段进 cfg 引擎(幂等)
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 限定条件:内核启动前调用(app_main_init);注册必须先于 cfg_file_load
|
||||
* 函数说明:完整契约见 adc_cal_init() 同名声明(adc_cal.h);注册失败(段名重复/
|
||||
* 段数超限)打 [ERR] 后继续——ROM 默认系数照常生效,仅文件整定失效
|
||||
*/
|
||||
void adc_cal_init(void) {
|
||||
cfg_section_reg_t reg;
|
||||
|
||||
(void)memcpy(&s_kcfg, &s_kcfg_rom, sizeof(s_kcfg));
|
||||
(void)memcpy(&s_bcfg, &s_bcfg_rom, sizeof(s_bcfg));
|
||||
s_err_latched = 0u;
|
||||
|
||||
(void)memset(®, 0, sizeof(reg));
|
||||
reg.name = ADC_CAL_SEC_K;
|
||||
reg.p_data = &s_kcfg;
|
||||
reg.p_defaults = &s_kcfg_rom;
|
||||
reg.data_size = (offsetof(adc_cal_kcfg_t, valid) + 1u); /* 33:覆盖到 valid */
|
||||
reg.p_keys = s_k_keys_tbl;
|
||||
reg.key_count = 9u;
|
||||
reg.p_cb = NULL; /* 不挂回调:RAM 副本即生效值(「数据可信」§4.4) */
|
||||
if (cfg_file_register(®) != 0) {
|
||||
DBG_ERROR("register '%s' FAIL (dup/full?) - calibration file values inactive",
|
||||
ADC_CAL_SEC_K);
|
||||
}
|
||||
|
||||
(void)memset(®, 0, sizeof(reg));
|
||||
reg.name = ADC_CAL_SEC_B;
|
||||
reg.p_data = &s_bcfg;
|
||||
reg.p_defaults = &s_bcfg_rom;
|
||||
reg.data_size = sizeof(s_bcfg); /* 32 */
|
||||
reg.p_keys = s_b_keys_tbl;
|
||||
reg.key_count = 8u;
|
||||
reg.p_cb = NULL;
|
||||
if (cfg_file_register(®) != 0) {
|
||||
DBG_ERROR("register '%s' FAIL (dup/full?) - zero-offset file values inactive",
|
||||
ADC_CAL_SEC_B);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:码值 → 工程量折算(「数据可信」§4.1 模型)
|
||||
* 入口参数:sem - 语义通道;code - 原始码值 adc_semantic_t / int32_t
|
||||
* 返 回 值:工程量浮点(未标定 = ADC 引脚电压 V;TEMP/越界 = 原样透传码值) float
|
||||
* 限定条件:无(任意上下文,纯读)
|
||||
* 函数说明:见 adc_cal.h 同名声明;k/b 经 s_eff_kb 合法带哨兵(越带回退 ROM)
|
||||
*/
|
||||
float adc_cal_apply(adc_semantic_t sem, int32_t code) {
|
||||
int32_t k;
|
||||
int32_t b;
|
||||
|
||||
if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) {
|
||||
return (float)code; /* TEMP/越界:不纳入线性标定(决策 5),原样透传 */
|
||||
}
|
||||
(void)s_eff_kb((unsigned)sem, &k, &b);
|
||||
return ((float)(code + b)) * ((float)k) * ADC_CAL_K_SCALE;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读通道可用性标志(消费者判「能不能信」的唯一入口)
|
||||
* 入口参数:sem - 语义通道 adc_semantic_t
|
||||
* 返 回 值:ADC_CAL_F_VALID / ADC_CAL_F_CLIPPED 组合;TEMP/越界恒 0 uint32_t
|
||||
* 限定条件:无
|
||||
* 函数说明:VALID 需文件位图与 k 合法带**同时**成立(哨兵压制说谎位图,见文件头)
|
||||
*/
|
||||
uint32_t adc_cal_flags(adc_semantic_t sem) {
|
||||
uint32_t f = 0u;
|
||||
|
||||
if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) {
|
||||
return 0u;
|
||||
}
|
||||
/* VALID = 文件位图置位 且 k 合法(哨兵不落共享状态,随重载自愈) */
|
||||
if (((s_kcfg.valid & (uint8_t)(1u << (unsigned)sem)) != 0u) &&
|
||||
(s_kband_ok((unsigned)sem, s_kcfg.k[sem], s_bcfg.b[sem]) != 0)) {
|
||||
f |= ADC_CAL_F_VALID;
|
||||
}
|
||||
/* CLIPPED 由**来源**派生(R5:STM32 侧单极性无偏置 → 负半周削顶)——
|
||||
* V0.2 改版(8 路全接 TPAFE,消除双转换器无共同采样触发)后本位自动恒 0,无需改码,
|
||||
* 对应「数据可信」§10「页面与 104 绑定零改动」的要求 */
|
||||
if (s_rom_k_stm32((unsigned)sem) != 0) {
|
||||
f |= ADC_CAL_F_CLIPPED;
|
||||
}
|
||||
return f;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:未标定闭锁判定(判据所需的 UA/UN/IA 三路是否齐)
|
||||
* 返 回 值:1 - 闭锁;0 - 放行 uint8_t(契约、三路集合的理由、判据停走的四路
|
||||
* 来源、两个同族谓词的边界都写在 adc_cal.h,本处不重述——重述就是第二真源)
|
||||
* 函数说明:**现查 flags(),本函数不留任何状态**。刻意不复用保护任务里那份
|
||||
* 「上拍闭锁态」——那是给边沿日志用的,开机首拍前恒 0;拿它当判据等于在
|
||||
* 「还没判过」的时候谎称「已标定」,而这正是本谓词最贵的失效(屏上
|
||||
* 「未标定」徽标一消失,现场读到的就是「改了定值就会动作」)。
|
||||
* DTU_BENCH 旁路留在这里、且全仓只此一处;用 #ifndef 把产品默认路径留在
|
||||
* 主分支——#ifdef 双分支会让 cppcheck 对未定义宏取真分支、把静态分析引到
|
||||
* 旁路上报 knownConditionTrueFalse 假信号(实测 2026-10-25,这条形态说明原先
|
||||
* 写在 protect_task.c 的实现处,随判据一起搬到这里)
|
||||
*/
|
||||
uint8_t adc_cal_prot_blocked(void) {
|
||||
#ifndef DTU_BENCH
|
||||
if (((adc_cal_flags(ADC_SEM_UA) & ADC_CAL_F_VALID) == 0u) ||
|
||||
((adc_cal_flags(ADC_SEM_UN) & ADC_CAL_F_VALID) == 0u) ||
|
||||
((adc_cal_flags(ADC_SEM_IA) & ADC_CAL_F_VALID) == 0u)) {
|
||||
return 1u;
|
||||
}
|
||||
#endif
|
||||
return 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读通道当前生效系数(µV/码,回退后口径)
|
||||
* 入口参数:sem - 语义通道 adc_semantic_t
|
||||
* 返 回 值:k[µV/码];TEMP/越界返 0 float
|
||||
* 限定条件:无
|
||||
* 函数说明:Q16 现值 ÷ 65536 浮点还原;勿据其反推二次值(可用性看 flags)
|
||||
*/
|
||||
float adc_cal_k_uv(adc_semantic_t sem) {
|
||||
int32_t k;
|
||||
int32_t b;
|
||||
|
||||
if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) {
|
||||
return 0.0f;
|
||||
}
|
||||
(void)s_eff_kb((unsigned)sem, &k, &b);
|
||||
return ((float)k) / 65536.0f;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读通道当前生效零点偏移(码值,浮点)——adc_cal_k_uv 孪生入口
|
||||
* 入口参数:sem - 语义通道 adc_semantic_t
|
||||
* 返 回 值:b[码值](越带时 s_eff_kb 已回 0);TEMP/越界返 0 float
|
||||
* 限定条件:无(任意上下文,纯读)
|
||||
* 函数说明:卡 10 的 104 b 覆盖回显 + k 单侧改写 RMW 读侧(App/algo/run_par.c)
|
||||
*/
|
||||
float adc_cal_b_codes(adc_semantic_t sem) {
|
||||
int32_t k;
|
||||
int32_t b;
|
||||
|
||||
if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) {
|
||||
return 0.0f;
|
||||
}
|
||||
(void)s_eff_kb((unsigned)sem, &k, &b);
|
||||
return ((float)b);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:整定入口——写通道系数与零点,并置位 VALID(「数据可信」卡 4)
|
||||
* 入口参数:sem - 语义通道;k_q16 - 系数(µV/码×65536);b_codes - 零点(码值)
|
||||
* 返 回 值:0 - 成功;-1 - 参数非法/越出合法带(RAM 不动,[ERR] 留痕) int
|
||||
* 限定条件:整定路径上下文(defaultTask 整定/标定链;iec104Task 经 run_par
|
||||
* 直通为卡 10 授权写者,单字原子契约同 §4.4);落盘由属主经 cfg_file_save
|
||||
* 函数说明:见 adc_cal.h 同名声明
|
||||
*/
|
||||
int adc_cal_set(adc_semantic_t sem, int32_t k_q16, int32_t b_codes) {
|
||||
if ((unsigned)sem >= (unsigned)ADC_SEM_TEMP) {
|
||||
return -1;
|
||||
}
|
||||
if (s_kband_ok((unsigned)sem, k_q16, b_codes) == 0) {
|
||||
DBG_ERROR("set ch=%u rejected: k_q16=%ld b=%ld out of ROM band [0.5x,2x]/|b|<=%ld",
|
||||
(unsigned)sem, (long)k_q16, (long)b_codes, (long)ADC_CAL_B_MAX);
|
||||
return -1;
|
||||
}
|
||||
s_kcfg.k[sem] = k_q16;
|
||||
s_bcfg.b[sem] = b_codes;
|
||||
s_kcfg.valid |= (uint8_t)(1u << (unsigned)sem);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:两点标定公式(卡 12 的算术核,纯函数无状态)
|
||||
* 入口参数:c_lo/c_hi、v_lo_uv/v_hi_uv、p_k_q16/p_b 见 adc_cal.h 同名声明
|
||||
* 返 回 值:0 成功 / -1 参数或斜率非法 / -2 零点越 ±B_MAX int
|
||||
* 限定条件:无(int64 中间量,主机可测;台架注入待跑)
|
||||
*/
|
||||
int adc_cal_two_point(int32_t c_lo, int32_t c_hi, int32_t v_lo_uv, int32_t v_hi_uv,
|
||||
int32_t *p_k_q16, int32_t *p_b) {
|
||||
long long dc;
|
||||
long long dv;
|
||||
long long kq;
|
||||
long long bb;
|
||||
|
||||
if ((p_k_q16 == NULL) || (p_b == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
dc = (long long)c_hi - (long long)c_lo;
|
||||
dv = (long long)v_hi_uv - (long long)v_lo_uv;
|
||||
if ((dc <= 0LL) || (dv <= 0LL)) {
|
||||
return -1; /* 斜率必须为正:码与电压同向,反向 = 映射/接线错 */
|
||||
}
|
||||
kq = ((dv << 16) + (dc >> 1)) / dc; /* (v_hi−v_lo)×65536/dc,四舍五入(dc>0) */
|
||||
if ((kq <= 0LL) || (kq > 100000000LL)) {
|
||||
return -1; /* 绝对合理域上界 1526 µV/code:越界 = 单位/量级错 */
|
||||
}
|
||||
bb = (((long long)v_lo_uv) << 16) / kq - (long long)c_lo;
|
||||
if ((bb > (long long)ADC_CAL_B_MAX) || (bb < -(long long)ADC_CAL_B_MAX)) {
|
||||
return -2; /* 零点异常 → 先查前端,别用系数凑(§8 风险表) */
|
||||
}
|
||||
*p_k_q16 = (int32_t)kq;
|
||||
*p_b = (int32_t)bb;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:通道码值域贴轨带门限(L5-2 判定的门限来源,按 FS 百分比)
|
||||
* 入口参数:sem - 语义通道;p_lo_th/p_hi_th - 输出下/上轨带界 int32_t *
|
||||
* 返 回 值:0 - 已给出门限;-1 - TEMP/越界/参数空(不做贴轨监视) int
|
||||
* 限定条件:无
|
||||
* 函数说明:状态机在 adc_health_rail_watch();本函数只供门限,无状态
|
||||
*/
|
||||
int adc_cal_rail_ths(adc_semantic_t sem, int32_t *p_lo_th, int32_t *p_hi_th) {
|
||||
if ((p_lo_th == NULL) || (p_hi_th == NULL) ||
|
||||
((unsigned)sem >= (unsigned)ADC_SEM_TEMP)) {
|
||||
return -1; /* TEMP(热敏非线性,不判「轨」)与越界不做监视 */
|
||||
}
|
||||
if (s_rom_k_stm32((unsigned)sem) != 0) {
|
||||
*p_hi_th = (int32_t)((4095L * 98L) / 100L); /* 4013 = 98 % FS */
|
||||
*p_lo_th = (int32_t)((4095L * 2L) / 100L); /* 81 = 触底带(削顶签名) */
|
||||
} else {
|
||||
*p_hi_th = 32112L; /* +98 % 半量程 */
|
||||
*p_lo_th = -32112L; /* -98 % 半量程 */
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
/*
|
||||
* 模块名称:通道标定与量纲折算(L1.5,「数据可信」卡 2/卡 4)
|
||||
* 模块功能:把 adc_input 的原始码值折算成工程量——模型与系数口径的唯一落点:
|
||||
* 工程量 = (code + b) × k_f,k_f = k[µV/码] × 1e-6
|
||||
* k 以 int32 **Q16**(µV/码 × 65536)落盘、b 以 int32 码值落盘,
|
||||
* RAM 副本注册进 cfg_file 引擎([adc_cal] / [adc_cal_ofs] 两段,
|
||||
* 布局见 docs/任务规划/数据可信.md §4.4);**不挂回调**——RAM 副本
|
||||
* 即生效值,逐点折算现取现用(1 kHz × 9 通道 = 9k 次/秒浮点乘,可忽略)。
|
||||
* 三态可用性:ADC_CAL_F_VALID(已按变比标定 → 工程量为二次值)与
|
||||
* ADC_CAL_F_CLIPPED(V0.1 硬件削顶)由 adc_cal_flags() 单点查询,
|
||||
* 104 / UI / 保护闭锁(卡 5)共用(docs/任务规划/数据可信.md §4.2)。
|
||||
* **未标定闭锁的判定本身也归本层**(`adc_cal_prot_blocked()`):它讲的是
|
||||
* 「这几路标定齐了没」,属标定层事实,不是任务编排逻辑——保护任务与 UI
|
||||
* 组装器都只是它的消费者(道F task-16 把此前的两份实现收成一份)。
|
||||
* ⚠ 输出单位口径 = **ADC 引脚电压 V**(eng=0):ROM 默认系数按卡 1
|
||||
* 定死的引脚侧 LSB(TPAFE 152.59 µV/码、STM32 805.66 µV/码,见
|
||||
* docs/硬件平台/ADC通道映射说明.md §7.1);端子变比(卡 0b)与两点
|
||||
* 标定(卡 12)完成前不得把输出当二次值消费。TEMP 不参与线性标定
|
||||
* (决策 5),adc_cal_apply() 原样透传码值。
|
||||
* 本层**纯算法 + 纯数据**:不碰寄存器、不等事件;adc_cal_init() 内核前
|
||||
* 可调(注册进引擎必须先于 cfg_file_load,FTP 鉴权确立的契约);apply/flags
|
||||
* 任意上下文可读(cfg RAM 副本 4 字节对齐字读写在 Cortex-M4 上原子,
|
||||
* 与 defaultTask 的 load/poll 复位存在 µs 级混窗——设计接受,见 .c 头)。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4F);主机侧 L0.5 单测 test/host/adc_cal/
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-25(道A 卡 2/卡 4)
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 DTU 项目组 创建:Q16 系数/零点 + 折算/flags/集
|
||||
* v1.1 2026-10-25 DTU 项目组 卡 10:+adc_cal_b_codes()(run_par 回显/RMW 用)
|
||||
* v1.2 2026-10-26 DTU 项目组 道F task-16:+adc_cal_prot_blocked()——「UA/UN/IA
|
||||
* 三路 VALID 是否齐」这份判据停走条件从
|
||||
* protect_task.c 与 ui_data_feed.c 的两份重复实现收成本层
|
||||
* 单点(DTU_BENCH 旁路同时收成一处)
|
||||
*/
|
||||
|
||||
#ifndef ADC_CAL_H
|
||||
#define ADC_CAL_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "adc_input.h" /* adc_semantic_t(语义通道编号 = 系数表下标口径) */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 通道可用性标志(adc_cal_flags() 返回值位) */
|
||||
#define ADC_CAL_F_VALID (0x01u) /* k 已按变比标定 → 工程量为二次值(卡 12 后置位) */
|
||||
#define ADC_CAL_F_CLIPPED (0x02u) /* 硬件削顶(V0.1 STM32 侧单极性无偏置)→ 交流量不可信 */
|
||||
|
||||
/*
|
||||
* ROM 默认系数(int32 Q16 = µV/码 × 65536,卡 1 定死的引脚侧口径):
|
||||
* TPAFE ±5 V 档:10 V 跨度 / 65536 码 = 152.587890625 µV/码 → ×65536 = 10,000,000(整)
|
||||
* STM32 0~3.3 V:3.3 V / 4096 码 = 805.6640625 µV/码 → ×65536 = 52,800,000(整)
|
||||
* 两个默认值在 Q16 下**无舍入**(系数定点舍入项从误差预算里抹掉,
|
||||
* 选型依据 → docs/任务规划/数据可信.md §4.1)
|
||||
*/
|
||||
#define ADC_CAL_K_Q16_TPAFE (10000000L)
|
||||
#define ADC_CAL_K_Q16_STM32 (52800000L)
|
||||
|
||||
/* 系数合法带(「数据可信」§8 风险表):k 必须落在板级 ROM 系数的 ×[0.5, 2.0]
|
||||
* 区间内;带宽用 ROM 值移位实现(>>1 / <<1),不设独立宏防两处真相 */
|
||||
|
||||
/* 零点偏移幅值上限(码值):超此即判文件损坏/异常标定,回退 ROM 并留痕 */
|
||||
#define ADC_CAL_B_MAX (8192L)
|
||||
|
||||
/*
|
||||
* 函数功能:标定层初始化——RAM 副本置 ROM 默认 + 注册两段进 cfg 引擎(幂等)
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 限定条件:内核启动前调用(app_main_init);注册必须先于 cfg_file_load
|
||||
* (「所有受管段先注册、后 load」契约,见 Drivers/BSP/NETCFG/net_cfg.c 的
|
||||
* net_cfg_load 注释)
|
||||
* 函数说明:段属主为 defaultTask(配置引擎),本模块只提供 RAM 副本与默认值;
|
||||
* 文件值由引擎 load/poll 原地写入 RAM 副本,本层逐点折算现读
|
||||
*/
|
||||
void adc_cal_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:码值 → 工程量折算(「数据可信」§4.1 模型)
|
||||
* 入口参数:sem - 语义通道(TEMP 透传不折算) adc_semantic_t
|
||||
* code - 原始码值(TPAFE 16 位补码 / STM32 0..4095) int32_t
|
||||
* 返 回 值:工程量浮点(未标定时单位 = ADC 引脚电压 V;VALID 后置位 = 二次值) float
|
||||
* 限定条件:无(任意上下文,纯读)
|
||||
* 函数说明:eng = (code + b) × k[µV/码] × 1e-6,由 Q16 现值派生,无缓存副本
|
||||
* (避免无回调下的陈旧系数);k/b 越合法带 → 该通道回退 ROM 默认 +
|
||||
* 首见沿 [ERR] 留痕(adc_cal_init 重置留痕位),flags 同时不报 VALID
|
||||
*/
|
||||
float adc_cal_apply(adc_semantic_t sem, int32_t code);
|
||||
|
||||
/*
|
||||
* 函数功能:读通道可用性标志(消费者判「能不能信」的唯一入口)
|
||||
* 入口参数:sem - 语义通道 adc_semantic_t
|
||||
* 返 回 值:ADC_CAL_F_VALID / ADC_CAL_F_CLIPPED 组合;TEMP 恒 0;sem 越界恒 0 uint32_t
|
||||
* 限定条件:无
|
||||
* 函数说明:VALID = 文件位图置位 **且** 该通道 k 在合法带内(哨兵不落状态,
|
||||
* 天然跟随 load/poll 重载);CLIPPED = V0.1 静态表(UB/UC/IB/IC/IN)
|
||||
*/
|
||||
uint32_t adc_cal_flags(adc_semantic_t sem);
|
||||
|
||||
/*
|
||||
* 函数功能:**未标定闭锁判定**——保护判据所需的三路标定是否齐(「数据可信」卡 5
|
||||
* / §4.3;道F task-16 把它从两处重复实现收敛成这一份真源)
|
||||
* 入口参数:无
|
||||
* 返 回 值:1 - 闭锁(UA/UN/IA 中至少一路 `ADC_CAL_F_VALID` 未置位,判据链应停走);
|
||||
* 0 - 三路全 VALID(标定侧允许判据跑) uint8_t
|
||||
* 限定条件:任意上下文(纯读 `adc_cal_flags()`,与它同一条并发契约:int32 对齐
|
||||
* 字读写原子、cfg poll 重载 µs 级混窗设计接受);**现查,本层不留任何状态**
|
||||
* 函数说明:① **为什么恰好这三路**:V0.1 把 8 路电气量分给两片转换器(AGENTS
|
||||
* 「双转换器无共同采样触发」),UA/UN/IA = TPAFE 侧这三路是**唯一有效**
|
||||
* 的测量通道;UB/UC/IB/IC/IN 即使将来标定也另受 `CLIPPED` 品质约束
|
||||
* (M5 快照层处理),不掺进本闭锁源。
|
||||
* ② ⚠ **不得当「保护在跑」的总括用**:判据停走共四路来源,本函数只覆盖
|
||||
* 第二路——(a) L5-4 采样故障闭锁(`adc_health_flags()`);(b) 本函数
|
||||
* (未标定);(c) 定值校验和失配闭锁(`prot_set_locked()`);(d) 全局软压板
|
||||
* 退出(`PS_GLOBAL_EN` = 0)。要判「这拍到底评估了没有」必须四路一起看
|
||||
* (编排顺序见 `App/task/protect_task.c`)。
|
||||
* ③ ⚠ **两个同族谓词与它不是一件事,不要顺手合并**(三种口径各有用途):
|
||||
* · 电气 **8** 路全 VALID 才升 eng=二次值(量纲标签,判据在
|
||||
* `App/app/ui_data_feed.c` 的 `feed_eng()`)——并进来会让部分标定的整帧
|
||||
* 被误标二次值;反过来把本三路口径当 8 路用,V0.1 下 UB/UC/IB/IC/IN
|
||||
* 永不置位 → 徽标与停走**永不解除**;
|
||||
* · 逐通道打印 VALID/CLIPPED 位图是诊断输出(`App/task/adc_task.c`),
|
||||
* 不是判定。
|
||||
* ④ 台架调试旁路 = 编译期 `-DDTU_BENCH`(「数据可信」§4.3 决策 1 的例外通道,
|
||||
* 仅本地台架自行定义;本仓任何工程档都不定义 = 产品恒强制)。旁路**只在
|
||||
* 本函数实现内部这一处**,调用方不再各自写 `#ifndef`:否则有人只给单个
|
||||
* TU 加宏,屏上「未标定」徽标与判据停走当场分叉,且 0 错误 0 警告。
|
||||
*/
|
||||
uint8_t adc_cal_prot_blocked(void);
|
||||
|
||||
/*
|
||||
* 函数功能:读通道当前生效系数(µV/码,浮点)
|
||||
* 入口参数:sem - 语义通道 adc_semantic_t
|
||||
* 返 回 值:k[µV/码](回退后口径);TEMP / 越界返 0 float
|
||||
* 限定条件:无
|
||||
* 函数说明:L5-2 门限换算、日志与整定界面回显用;勿据其反推二次值(看 flags)
|
||||
*/
|
||||
float adc_cal_k_uv(adc_semantic_t sem);
|
||||
|
||||
/*
|
||||
* 函数功能:读通道当前生效零点偏移(码值,浮点)——adc_cal_k_uv 的孪生入口
|
||||
* 入口参数:sem - 语义通道 adc_semantic_t
|
||||
* 返 回 值:b[码值](越带时为 0,与折算同视图);TEMP/越界返 0 float
|
||||
* 限定条件:无
|
||||
* 函数说明:104 运行参数段(「数据可信」卡 10)b 覆盖回显与 k 单侧改写的 RMW
|
||||
* 读侧用(App/algo/run_par.c);勿据其反推二次值(看 flags)。
|
||||
*/
|
||||
float adc_cal_b_codes(adc_semantic_t sem);
|
||||
|
||||
/*
|
||||
* 函数功能:整定入口——写通道系数与零点,并置位 VALID(「数据可信」卡 4)
|
||||
* 入口参数:sem - 语义通道(电气 8 路) adc_semantic_t
|
||||
* k_q16 - 系数,µV/码 × 65536 内 int32_t
|
||||
* b_codes - 零点偏移,码值 内 int32_t
|
||||
* 返 回 值:0 - 成功;-1 - 参数非法/越出合法带(RAM 不动,[ERR] 留痕) int
|
||||
* 限定条件:**整定路径上下文**(defaultTask 标定流程/整定链;卡 10 起 iec104Task
|
||||
* 经 App/algo/run_par.c 的 0x8040~/0x8048~ 直通写亦为授权写者——写的仍是
|
||||
* 引擎对齐 int32 单字,µs 级混窗契约同「数据可信」§4.4)。勿在 adcTask 调用
|
||||
* 函数说明:只写 RAM 副本;落盘由属主经 cfg_file_save 完成(卡 10 前整定链
|
||||
* 未接,台架两点标定(卡 12)是首个消费者)
|
||||
*/
|
||||
int adc_cal_set(adc_semantic_t sem, int32_t k_q16, int32_t b_codes);
|
||||
|
||||
/*
|
||||
* 函数功能:两点标定公式(「数据可信」卡 12 的算术核,纯函数无状态)
|
||||
* 入口参数:c_lo/c_hi - 两点码值(c_hi > c_lo) int32_t
|
||||
* v_lo_uv/v_hi_uv - 两点实测电压(µV,v_hi > v_lo,端子/引脚域均可) int32_t
|
||||
* p_k_q16 / p_b - 输出:系数(Q16)与零点偏移(码值) int32_t *
|
||||
* 返 回 值:0 - 成功;-1 - 参数/斜率非法;-2 - 零点越 ±ADC_CAL_B_MAX(先查前端,
|
||||
* 不得用系数凑错斜率,「数据可信」§8 风险表) int
|
||||
* 限定条件:无(纯整数运算,主机侧可直接单测)
|
||||
* 函数说明:k = (v_hi−v_lo)×65536/(c_hi−c_lo)(四舍五入),b = v_lo×65536/k − c_lo;
|
||||
* 实测注入未做(台架待跑),本函数即「公式齐备」的交付物
|
||||
*/
|
||||
int adc_cal_two_point(int32_t c_lo, int32_t c_hi, int32_t v_lo_uv, int32_t v_hi_uv,
|
||||
int32_t *p_k_q16, int32_t *p_b);
|
||||
|
||||
/*
|
||||
* 函数功能:通道码值域「贴轨带」门限(L5-2 越界检出的门限来源,按 FS 百分比)
|
||||
* 入口参数:sem - 语义通道 adc_semantic_t
|
||||
* p_lo_th - 下轨带上界(code ≤ 此值判触底) int32_t *
|
||||
* p_hi_th - 上轨带下界(code ≥ 此值判触顶) int32_t *
|
||||
* 返 回 值:0 - 已给出门限;-1 - TEMP/越界(不做贴轨监视) int
|
||||
* 限定条件:无
|
||||
* 函数说明:TPAFE 双极性 ±98% FS(±32112);STM32 上轨 98% FS(4013)、
|
||||
* 下轨 2% FS(81);判定状态机在 adc_health_rail_watch()(L5-2)
|
||||
*/
|
||||
int adc_cal_rail_ths(adc_semantic_t sem, int32_t *p_lo_th, int32_t *p_hi_th);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* ADC_CAL_H */
|
||||
+129
-1
@@ -1,16 +1,25 @@
|
||||
/*
|
||||
* 模块名称:ADC 采样有效性校验(L1 采集与预处理,规划项 L1-6)
|
||||
* 模块功能:见 adc_health.h 文件头。本文件为**纯逻辑实现**:输入计数快照、
|
||||
* 输出故障位/事件位,零平台依赖(仅 stdint.h)→ 主机侧免桩单测。
|
||||
* 输出故障位/事件位,零平台依赖(仅 stdint.h/string.h 标准 C)→
|
||||
* 主机侧免桩单测。
|
||||
* 适用平台:STM32F407ZGTx + TPAFE5160
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-11
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-11 王建锋 创建:L1-6 采样有效性校验
|
||||
* v1.1 2026-10-21 王建锋 新增 adc_health_age_stale():无模块状态的帧年龄
|
||||
* 纯判据(参照系由调用方自存),供 protectTask 的
|
||||
* 兜底新鲜度门使用,见 adc_health.h 同名声明
|
||||
* v1.2 2026-10-25 DTU 项目组 新增 L5-2 残余:码值越界(贴轨)观察器
|
||||
* (rail_watch/rail_sticky/rail_init),判据与释放
|
||||
* 沿理由见 adc_health.h L5-2 段注释
|
||||
*/
|
||||
|
||||
#include "adc_health.h"
|
||||
|
||||
#include <string.h> /* memset(L5-2 贴轨观察器数组复位) */
|
||||
|
||||
/* ===================== 内部状态 ===================== */
|
||||
|
||||
/* 基线是否已建立:首次 check() 只记基线、不判定(开机首拍无参照系) */
|
||||
@@ -33,6 +42,17 @@ static uint32_t s_flags; /* 电平位(stall 可自愈;backward 锁存在
|
||||
static uint32_t s_sticky; /* 锁存位(含电平位历史并集) */
|
||||
static uint32_t s_tpafe_gaps;
|
||||
|
||||
/* L5-2 贴轨观察器(每通道独立小状态机,单写者 = adcTask) */
|
||||
typedef struct {
|
||||
uint16_t run_hi; /* 连续贴上轨计数(饱和 0xFFFF) */
|
||||
uint16_t run_lo; /* 连续贴下轨计数 */
|
||||
uint8_t seen_mid; /* 见过中带样本(释放沿)——锁存的必要前提 */
|
||||
uint8_t latched; /* 已报过边沿(不重复报) */
|
||||
} adc_rail_t;
|
||||
|
||||
static adc_rail_t s_rail[ADC_RAIL_CH_NUM];
|
||||
static uint32_t s_rail_sticky;
|
||||
|
||||
/* ===================== 对外接口 ===================== */
|
||||
|
||||
/*
|
||||
@@ -43,6 +63,7 @@ static uint32_t s_tpafe_gaps;
|
||||
* 函数说明:清全部内部状态;判定从下一次 check() 的首拍基线重新开始。
|
||||
*/
|
||||
void adc_health_init(void) {
|
||||
adc_health_rail_init();
|
||||
s_primed = 0u;
|
||||
s_tpafe_seq = 0u;
|
||||
s_tpafe_frst = 0u;
|
||||
@@ -166,6 +187,43 @@ void adc_health_check(const adc_health_in_t *in, adc_health_status_t *out) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:帧年龄判定(无模块状态,保护侧兜底门专用)
|
||||
* 入口参数:见 adc_health.h 同名声明
|
||||
* 返 回 值:1 - 已陈旧;0 - 新鲜或仍在建立基线
|
||||
* 限定条件:任意任务上下文;不读写本模块任何 static(与 check() 的关键区别)
|
||||
* 函数说明:三步——建基线 → 有前进即刷新参照系 → 原地不动才量"距最后一次前进"。
|
||||
* 倒退也按"有变化"处理(本函数不判单调性,那归 ADC_HF_SEQ_BACKWARD)。
|
||||
*/
|
||||
uint8_t adc_health_age_stale(adc_health_age_t *a_p_age, uint32_t a_cur_seq,
|
||||
uint32_t a_now_ms, uint32_t a_age_ms) {
|
||||
if (a_p_age == 0) {
|
||||
return 0u;
|
||||
}
|
||||
|
||||
/* 1) 首拍建基线:此刻序号往往已非 0(TIM3 比调度器早约 100 ms 就在生产帧),
|
||||
* 没有"上一拍"就没有参照系——开机窗口内绝不允许把保护锁掉 */
|
||||
if (a_p_age->primed == 0u) {
|
||||
a_p_age->primed = 1u;
|
||||
a_p_age->seq = a_cur_seq;
|
||||
a_p_age->adv_ms = a_now_ms;
|
||||
return 0u;
|
||||
}
|
||||
|
||||
/* 2) 有任何前进(含跨多帧、含跨回绕、含倒退)即算新鲜,并刷新参照系 */
|
||||
if (a_cur_seq != a_p_age->seq) {
|
||||
a_p_age->seq = a_cur_seq;
|
||||
a_p_age->adv_ms = a_now_ms;
|
||||
return 0u;
|
||||
}
|
||||
|
||||
/* 3) 序号原地不动:只看"距最后一次前进多久",含等号(与 stall 判据同边界) */
|
||||
if ((uint32_t)(int32_t)(a_now_ms - a_p_age->adv_ms) >= a_age_ms) {
|
||||
return 1u;
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读当前电平故障位
|
||||
* 入口参数:无
|
||||
@@ -198,3 +256,73 @@ uint32_t adc_health_sticky(void) {
|
||||
uint32_t adc_health_tpafe_gaps(void) {
|
||||
return s_tpafe_gaps;
|
||||
}
|
||||
|
||||
/* ===================== L5-2 贴轨观察器实现 ===================== */
|
||||
|
||||
/*
|
||||
* 函数功能:贴轨观察器复位(幂等;adc_health_init() 已内含)
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 限定条件:与 check() 同为单写者任务上下文
|
||||
*/
|
||||
void adc_health_rail_init(void) {
|
||||
(void)memset(s_rail, 0, sizeof(s_rail));
|
||||
s_rail_sticky = 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:单通道单拍贴轨观察(判据与释放沿前提见 adc_health.h 同名声明)
|
||||
* 入口参数:ch / code / lo_th / hi_th - 见 adc_health.h
|
||||
* 返 回 值:1 - 本拍新锁存(边沿);0 - 无新事件(含 ch 越界忽略) uint32_t
|
||||
* 限定条件:每拍每通道至多一次;纯算术、零平台依赖
|
||||
*/
|
||||
uint32_t adc_health_rail_watch(uint8_t ch, int32_t code, int32_t lo_th, int32_t hi_th) {
|
||||
adc_rail_t *p;
|
||||
|
||||
if (ch >= (uint8_t)ADC_RAIL_CH_NUM) {
|
||||
return 0u;
|
||||
}
|
||||
p = &s_rail[ch];
|
||||
|
||||
if (code >= hi_th) { /* 贴上轨带:清对侧计数 */
|
||||
p->run_lo = 0u;
|
||||
if (p->run_hi < 0xFFFFu) {
|
||||
p->run_hi++;
|
||||
}
|
||||
if ((p->run_hi >= (uint16_t)ADC_RAIL_N_RUN) && (p->seen_mid != 0u)
|
||||
&& (p->latched == 0u)) {
|
||||
p->latched = 1u;
|
||||
s_rail_sticky |= (1uL << ch);
|
||||
return 1u;
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
if (code <= lo_th) { /* 贴下轨带(对称) */
|
||||
p->run_hi = 0u;
|
||||
if (p->run_lo < 0xFFFFu) {
|
||||
p->run_lo++;
|
||||
}
|
||||
if ((p->run_lo >= (uint16_t)ADC_RAIL_N_RUN) && (p->seen_mid != 0u)
|
||||
&& (p->latched == 0u)) {
|
||||
p->latched = 1u;
|
||||
s_rail_sticky |= (1uL << ch);
|
||||
return 1u;
|
||||
}
|
||||
return 0u;
|
||||
}
|
||||
/* 中带:记释放沿(锁存的必要前提),并清两侧连续计数 */
|
||||
p->seen_mid = 1u;
|
||||
p->run_hi = 0u;
|
||||
p->run_lo = 0u;
|
||||
return 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读贴轨锁存位图
|
||||
* 入口参数:无
|
||||
* 返 回 值:bit ch = 曾检出贴轨(低 9 位有效) uint32_t
|
||||
* 限定条件:无
|
||||
*/
|
||||
uint32_t adc_health_rail_sticky(void) {
|
||||
return s_rail_sticky;
|
||||
}
|
||||
|
||||
+91
-1
@@ -8,12 +8,25 @@
|
||||
* ④ 错误计数增量:FRSTDATA 跳变 / HAL ADC·DMA 错误 / STM32 停摆计数。
|
||||
* ⚠ 本层**纯逻辑**:不碰寄存器、不取数、不打日志——输入是调用方
|
||||
* 采集的计数快照,输出是故障位;日志与自检上报由消费者
|
||||
* (`App/task/adc_task.c`)按位执行 → 主机侧可单测(L0.5,A-4)。
|
||||
* (`App/task/adc_task.c`)按位执行 → 主机侧可单测(L0.5)。
|
||||
* 适用平台:STM32F407ZGTx + TPAFE5160
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-11
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-11 王建锋 创建:L1-6 采样有效性校验(静默/丢帧/单调性/错误增量)
|
||||
* v1.1 2026-10-21 王建锋 新增 adc_health_age_stale()(无模块状态的帧年龄
|
||||
* 纯判据,参照系由调用方自存)与 ADC_HF_STALE 位:
|
||||
* 判定层的 stall/backward 位只在 adcTask 每拍跑
|
||||
* check() 时才可能置起,节拍源停摆会把本任务卡在
|
||||
* osThreadFlagsWait 上 → 那些位永远是 0,而相量冻在
|
||||
* 最后一帧仍被 protectTask 用于判据。该纯判据让保护侧
|
||||
* 能用**自己的时基 + 中断侧计数器**独立判"数据还新不新" * v1.2 2026-10-25 DTU 项目组 新增 L5-2 残余「码值越界(贴轨)检出」:
|
||||
* adc_health_rail_watch()/rail_sticky()/rail_init()
|
||||
* ——连续 N 拍贴满量程带且该通道见过释放沿才锁存
|
||||
* (空载恒 0 / 开机恒 0 是合法工况,不误报);
|
||||
* 判据动因 docs/问题记录/采集与数据处理短板评测.md
|
||||
* §6.2,门限来源 adc_cal_rail_ths()(按 FS 百分比)
|
||||
|
||||
*/
|
||||
|
||||
#ifndef ADC_HEALTH_H
|
||||
@@ -51,6 +64,13 @@ extern "C" {
|
||||
#define ADC_HE_FRSTDATA (1uL << 3) /* 本次 FRSTDATA 计数有增量 */
|
||||
#define ADC_HE_HW_ERR (1uL << 5) /* 本次 HAL 错误计数有增量 */
|
||||
|
||||
/* 兜底新鲜度位(bit8,与判定层 bit0..bit7 位段错开)
|
||||
* ⚠ 本位**不由本模块置位**:由 protectTask 调 adc_health_age_stale() 独立观测后
|
||||
* OR 进闭锁位图。因此它不进 sticky、不进 adc_task 编排层的 logged 位图、
|
||||
* 也不由本模块打日志——留痕走 protect 侧既有的闭锁边沿日志与 SOE。
|
||||
* 位段纪律见 docs/算法/采样有效性校验设计说明.md §14 */
|
||||
#define ADC_HF_STALE (1uL << 8) /* 帧年龄超限(电平,序号恢复前进即清) */
|
||||
|
||||
/* 计数快照(由调用方采集,本模块不触碰任何驱动/外设) */
|
||||
typedef struct {
|
||||
uint32_t tpafe_seq; /* tpafe5160_frame_seq() */
|
||||
@@ -71,6 +91,15 @@ typedef struct {
|
||||
uint32_t tpafe_stall_ms; /* TPAFE 当前静默时长(0=正常) */
|
||||
} adc_health_status_t;
|
||||
|
||||
/* 帧年龄参照系(adc_health_age_stale() 用)。状态**归调用方所有**,所以多个观察者
|
||||
* 各判各的、互不污染——这是它与本模块其余接口的关键区别(check() 用的是模块级
|
||||
* 单写者状态,只能有一个每拍调用者) */
|
||||
typedef struct {
|
||||
uint32_t seq; /* 上次看到的序号 */
|
||||
uint32_t adv_ms; /* 最后一次看到序号前进的时刻(与调用方时基同源) */
|
||||
uint8_t primed; /* 0 = 尚未建立基线(首拍只记录、不判定) */
|
||||
} adc_health_age_t;
|
||||
|
||||
/*
|
||||
* 函数功能:采样有效性校验初始化(幂等,内核启动前可调)
|
||||
* 入口参数:无
|
||||
@@ -91,6 +120,23 @@ void adc_health_init(void);
|
||||
*/
|
||||
void adc_health_check(const adc_health_in_t *in, adc_health_status_t *out);
|
||||
|
||||
/*
|
||||
* 函数功能:帧年龄判定(无模块状态,供与消费者存活解耦的兜底门使用)
|
||||
* 入口参数:a_p_age - 调用方自存的参照系
|
||||
* a_cur_seq - 本次读到的序号(须是**中断侧**递增的计数器,如
|
||||
* tpafe5160_frame_seq() / adc_input_stm32_seq())
|
||||
* a_now_ms - 本次时基(与 a_p_age->adv_ms 取同一个任务的 tick)
|
||||
* a_age_ms - 判老门限(ms)
|
||||
* 返 回 值:1 - 已陈旧(调用方应据此闭锁);0 - 新鲜,或仍在建立基线
|
||||
* 限定条件:任意任务上下文;零内核依赖、零共享状态 → 主机侧免桩可直接单测
|
||||
* 函数说明:差值口径与 adc_health_check() 一致((int32_t) 差值,计数与 tick
|
||||
* 回绕均安全)。首次调用只建基线并返回 0;序号有任何前进即返回 0
|
||||
* (一次前进多帧属消费者超拍,不是故障);倒退不在此判老,单调性
|
||||
* 破坏归 ADC_HF_SEQ_BACKWARD 管。参数错返回 0——宁可不锁,不可误锁。
|
||||
*/
|
||||
uint8_t adc_health_age_stale(adc_health_age_t *a_p_age, uint32_t a_cur_seq,
|
||||
uint32_t a_now_ms, uint32_t a_age_ms);
|
||||
|
||||
/*
|
||||
* 函数功能:读当前电平故障位
|
||||
* 入口参数:无
|
||||
@@ -119,6 +165,50 @@ uint32_t adc_health_sticky(void);
|
||||
*/
|
||||
uint32_t adc_health_tpafe_gaps(void);
|
||||
|
||||
/* ===================== L5-2 残余:码值越界(贴轨)检出,2026-10-25 ===================== */
|
||||
|
||||
/* 贴轨锁存的连续拍数:20 拍 = 1 kHz 节拍下一个 50 Hz 周波。削顶/触轨波形的
|
||||
* 贴带段持续 ≥ 半周波;噪声毛刺不可能连续 20 拍钉在同侧轨带内(短板评测 §6.2
|
||||
* 判据①「连续 N 点贴满量程」的定标)。 */
|
||||
#define ADC_RAIL_N_RUN 20u
|
||||
|
||||
/* 贴轨监视的通道数上限(9 = 8 电气量 + 温度;温度是否监视由调用方经
|
||||
* adc_cal_rail_ths() 返回 -1 决定,本层不内嵌语义知识) */
|
||||
#define ADC_RAIL_CH_NUM 9u
|
||||
|
||||
/*
|
||||
* 函数功能:贴轨观察器复位(adc_health_init() 已内含,单独暴露供编排层复用)
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 限定条件:与 check() 同——仅任务上下文(单写者)
|
||||
*/
|
||||
void adc_health_rail_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:单通道单拍贴轨观察(L5-2 码值越界检出,纯算术)
|
||||
* 入口参数:ch - 通道号(0..ADC_RAIL_CH_NUM-1,越界忽略) uint8_t
|
||||
* code - 本拍原始码值(**码值域**,与标定无关) int32_t
|
||||
* lo_th - 下轨带上界(code ≤ 此值算贴底) int32_t
|
||||
* hi_th - 上轨带下界(code ≥ 此值算贴顶) int32_t
|
||||
* 返 回 值:1 - 本拍**新锁存**该通道(边沿,供编排层落日志);0 - 无新事件 uint32_t
|
||||
* 限定条件:每拍每通道至多一次(与采样帧同节奏,单写者)
|
||||
* 函数说明:锁存条件 = 连续 ADC_RAIL_N_RUN 拍贴**同一侧**轨带,且该通道此前
|
||||
* 出现过中带样本(「释放沿」)。释放沿要求的理由:空载信号恒 0、
|
||||
* 开机未采到帧恒 0 都是**合法工况**(短板评测 §6.2:空载与零值都是
|
||||
* 合法工况)——只有「动过又钉死在轨上」才是削顶/越界签名。锁存后
|
||||
* 不再重复报边沿,直到 rail_init/health_init 重置。
|
||||
*/
|
||||
uint32_t adc_health_rail_watch(uint8_t ch, int32_t code, int32_t lo_th, int32_t hi_th);
|
||||
|
||||
/*
|
||||
* 函数功能:读贴轨锁存位图
|
||||
* 入口参数:无
|
||||
* 返 回 值:bit ch = 该通道曾检出贴轨(1 kHz × 9 路 → 低 9 位有效) uint32_t
|
||||
* 限定条件:无
|
||||
* 函数说明:供心跳日志 / 后续品质位映射(卡 8/9 的 qds 消费者)只读。
|
||||
*/
|
||||
uint32_t adc_health_rail_sticky(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+26
-16
@@ -12,6 +12,10 @@
|
||||
* "TIM3 TRGO 硬件触发 + DMA 双缓冲消费":本层不再触发
|
||||
* 也不等待转换(消除最坏 2~3 ms 忙等);新增三个 HAL
|
||||
* 回调(ISR 内只计数)与 seq / hw_errs 观测量
|
||||
* v2.1 2026-10-21 王建锋 丢帧计数口径由「次数」改为「帧点数」(累加每次前进
|
||||
* 的超出部分),与判定层 TPAFE 侧
|
||||
* adc_health_tpafe_gaps() 同单位。⚠ 换版前固件里心跳
|
||||
* 字段 adc1_gap 是次数口径,不可跨版本直接对比
|
||||
*/
|
||||
|
||||
#include "adc_input.h"
|
||||
@@ -37,7 +41,7 @@
|
||||
* 权威口径:docs/硬件平台/ADC通道映射说明.md(2026-10-09 硬件确认的语义分配)。
|
||||
* index 含义:TPAFE 源 = 驱动 buf 下标 0..7;STM32 源 = ADC1 通道号 0..15。
|
||||
* ⚠ 消费侧一律经本表按语义取数,**不得写死下标**(V0.1 有 13/17 路被置换,
|
||||
* 网名编号与通道号不等价);V0.2 改版(N-18)只改本表内容。 */
|
||||
* 网名编号与通道号不等价);V0.2 改版(消除双转换器无共同采样触发)只改本表内容。 */
|
||||
static const adc_channel_desc_t k_adc_map[ADC_SEM_COUNT] = {
|
||||
[ADC_SEM_UA] = { ADC_SRC_TPAFE, 6U }, /* TPAFE buf[6](ch7) */
|
||||
[ADC_SEM_UB] = { ADC_SRC_STM32, (uint8_t)ADC_CHANNEL_14 }, /* PC4 */
|
||||
@@ -69,7 +73,7 @@ static uint16_t s_stm32_dma[ADC_INPUT_STM32_DMA_LEN];
|
||||
static volatile uint32_t s_dma_seq; /* DMA 半帧/整帧完成次数(ISR 递增,只增不清) */
|
||||
static uint32_t s_dma_seq_seen; /* 消费侧上次看到的完成计数 */
|
||||
static volatile uint32_t s_stm32_hw_errs; /* HAL ADC/DMA 错误回调累计(ISR 写) */
|
||||
static uint32_t s_stm32_gaps; /* 丢帧:seq 一次前进 >1(消费者超拍漏读)*/
|
||||
static uint32_t s_stm32_gaps; /* 丢帧点数:Σ(前进量-1),与 tgap 同单位 */
|
||||
static uint8_t s_stall_run; /* 连续"无新帧"拍数(相位伪影只会是 1 拍)*/
|
||||
/* TPAFE5160 最新一帧原始码值(16 位二进制补码),按驱动 buf 下标索引 */
|
||||
static int16_t s_tpafe_raw[ADC_INPUT_TPAFE_CH_NUM];
|
||||
@@ -79,21 +83,22 @@ static int s_stm32_rc; /* STM32 最近一次扫描状态码 */
|
||||
static uint32_t s_stm32_errs; /* STM32 扫描累计失败次数 */
|
||||
|
||||
/*
|
||||
* 函数功能:消费一帧 STM32 ADC1 采样(L1-3,v2.0 DMA 双缓冲版)
|
||||
* 函数功能:消费一帧 STM32 ADC1 采样(L1-3,DMA 双缓冲)
|
||||
* 入口参数:无
|
||||
* 返 回 值:0 - 取到新一帧;-2 - 本拍无新帧(TIM3 未启动 / ADC 停摆 / DMA 出错)
|
||||
* 限定条件:仅任务上下文调用;adc_input_init() 已挂好 DMA
|
||||
* 函数说明:本函数**既不触发转换也不等待转换**——转换由 TIM3 TRGO 硬件触发、由 DMA
|
||||
* 自动搬运。每拍只做"看完成计数是否前进 + 拷 9 个半字",故**零忙等**:
|
||||
* 原轮询版常态 adcp=122 µs、超时拍最坏 2~3 ms,现在两档都降到 ~1 µs 量级。
|
||||
* 单拍只剩"读一个计数 + 拷 9 个半字"→ µs 量级。
|
||||
* 读的是"上一拍刚完成的那一半"乒乓缓冲(DMA 此刻正在写另一半)→ 无撕裂。
|
||||
* ⚠ 计数未前进 = 本拍 STM32 侧**无有效数据**,必须清 s_stm32_valid,绝不
|
||||
* 沿用上一拍旧值(同 2026-10-10 修的陈旧帧缺陷口径)→ adc_input_raw()
|
||||
* 沿用上一拍旧值(半新半旧的混合帧会污染相量窗)→ adc_input_raw()
|
||||
* 返 -2 → adc_task 走既有"整帧跳过"路径;下一拍自动恢复。
|
||||
*/
|
||||
static int stm32_take(void) {
|
||||
uint32_t seq = s_dma_seq; /* 单次 32 位读:与 ISR 自增不产生撕裂 */
|
||||
const uint16_t *src;
|
||||
uint32_t d;
|
||||
uint8_t half;
|
||||
uint8_t i;
|
||||
|
||||
@@ -105,7 +110,7 @@ static int stm32_take(void) {
|
||||
* 看到"计数没前进"(那个完成已被上一拍消费掉)。这种伪影**只会连续 1 拍**——
|
||||
* DMA 若真停,第 2 拍仍不会前进。故只有连续 >= ADC_INPUT_STALL_RUN_MAX 拍
|
||||
* 无新帧才计入故障计数(实测开机日志突发期约 21~26 次伪影、稳态每 10 s 心跳 1 次,
|
||||
* 全部为单拍;见 docs/子系统设计/ADC采样节拍设计说明.md §10.5) */
|
||||
* 全部为单拍;见 docs/子系统设计/ADC采样节拍设计说明.md §8) */
|
||||
if (s_stall_run < 255u) {
|
||||
s_stall_run++;
|
||||
}
|
||||
@@ -115,11 +120,14 @@ static int stm32_take(void) {
|
||||
return ADC_INPUT_RC_STALL;
|
||||
}
|
||||
|
||||
if ((uint32_t)(seq - s_dma_seq_seen) > 1u) {
|
||||
d = seq - s_dma_seq_seen;
|
||||
if (d > 1u) {
|
||||
/* 计数一次前进 >1 = 中间有帧没被读走(乒乓缓冲已被覆盖)→ **真丢帧**,
|
||||
* 与"无新帧"分开计数:前者是消费者超拍(本任务单拍耗时 > 1 ms),
|
||||
* 后者多半是相位伪影 */
|
||||
s_stm32_gaps++;
|
||||
* 后者多半是相位伪影。按**丢掉的帧数**累计(d-1),与判定层 TPAFE 侧的
|
||||
* adc_health_tpafe_gaps() 同单位——否则心跳里 adc1_gap 与 tgap 一个是次
|
||||
* 一个是点,跨字段对比会得出错误结论 */
|
||||
s_stm32_gaps += (d - 1u);
|
||||
}
|
||||
s_stall_run = 0u;
|
||||
s_dma_seq_seen = seq;
|
||||
@@ -145,8 +153,8 @@ static int stm32_take(void) {
|
||||
* 函数说明:逐条校验映射表与扫描配置是否一致——① TPAFE 源下标须落在
|
||||
* buf[0..7];② STM32 源的通道号必须出现在 k_stm32_rank_ch[] 中,
|
||||
* 否则该通道永远不会被扫描(读到的恒为陈旧值)。这是"通道宏写错 →
|
||||
* 读到另一只脚/读不到"这一 §5 警告的代码级兜底(无信号源时无法做
|
||||
* 引脚级注入验证,见 docs/硬件平台/ADC通道映射说明.md §8)。
|
||||
* 读到另一只脚/读不到"这一警告的代码级兜底(警告口径与逐通道验证方法:
|
||||
* docs/硬件平台/ADC通道映射说明.md §5、§8;台架无信号源时注入验证做不了)。
|
||||
*/
|
||||
static int map_check(void) {
|
||||
uint8_t s;
|
||||
@@ -232,7 +240,7 @@ int adc_input_init(void) {
|
||||
* STM32 侧无新帧不影响 TPAFE 快照与返回值语义(其状态另由
|
||||
* adc_input_stm32_rc() / adc_input_stm32_seq() 暴露)。
|
||||
* ⚠ STM32 侧数据比 TPAFE 侧**滞后 1 拍**(乒乓双缓冲的固有延迟,
|
||||
* 9 路一致);按 N-18,V0.1 本就禁止跨转换器做矢量运算。
|
||||
* 9 路一致);因双转换器无共同采样触发,V0.1 本就禁止跨转换器做矢量运算。
|
||||
*/
|
||||
int adc_input_poll(void) {
|
||||
int r = tpafe5160_snapshot(s_tpafe_raw, (uint8_t)ADC_INPUT_TPAFE_CH_NUM);
|
||||
@@ -310,12 +318,14 @@ uint32_t adc_input_stm32_errs(void) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读取 STM32 ADC1 的丢帧次数
|
||||
* 函数功能:读取 STM32 ADC1 累计丢帧点数
|
||||
* 入口参数:无
|
||||
* 返 回 值:DMA 完成计数一次前进 >1 的次数(中间那帧已被乒乓缓冲覆盖)
|
||||
* 返 回 值:Σ(完成计数每次前进的超出部分),即被乒乓缓冲覆盖掉、没被消费的帧数
|
||||
* 限定条件:无
|
||||
* 函数说明:这是"消费者超拍"的直接证据(本任务单拍耗时 > 1 ms,如突发日志突发期);
|
||||
* 与 adc_input_stm32_errs()(节拍源停摆)是两类不同的故障。
|
||||
* 函数说明:这是"消费者超拍"的直接证据(本任务单拍耗时 > 1 ms,如心跳日志突发期);
|
||||
* 与 adc_input_stm32_errs()(节拍源停摆)是两类不同的故障,**勿混读**。
|
||||
* 单位与判定层 TPAFE 侧 adc_health_tpafe_gaps() 一致(都是帧点数),心跳里
|
||||
* 可直接对比;注意 2026-10-21 之前固件该字段是"次数"口径。
|
||||
*/
|
||||
uint32_t adc_input_stm32_gaps(void) {
|
||||
return s_stm32_gaps;
|
||||
|
||||
+13
-8
@@ -4,8 +4,8 @@
|
||||
* 暴露,使上层(UI / 自检 / 保护算法)**按语义取数,不写死下标**:
|
||||
* ① 语义通道映射表(L1-4)——权威口径 docs/硬件平台/ADC通道映射说明.md;
|
||||
* ② STM32 ADC1 9 通道采样(L1-3)——由 **TIM3 TRGO 硬件触发 + DMA 双缓冲
|
||||
* 搬运**(v2.0),本层只消费、不触发、不等待。
|
||||
* V0.1 上 8 路电气量分属两片转换器(硬件缺陷 N-18):UA/UN/IA 走
|
||||
* 搬运**,本层只消费、不触发、不等待。
|
||||
* V0.1 上 8 路电气量分属两片转换器(硬件缺陷:双转换器无共同采样触发):UA/UN/IA 走
|
||||
* TPAFE5160(16 位 / ±5 V 双极性),UB/UC/IB/IC/IN 走 STM32 ADC1
|
||||
* (12 位 / 0~3.3 V 单极性)。V0.2 改版(8 路电气量全部接 TPAFE)
|
||||
* 只改映射表内容,本层 API 与上层语义不动。
|
||||
@@ -16,8 +16,11 @@
|
||||
* v1.0 2026-10-10 王建锋 创建:L1-3(STM32 ADC1 扫描)+ L1-4(语义映射)
|
||||
* v2.0 2026-10-10 王建锋 STM32 侧改 TIM3 TRGO 硬件触发 + DMA 双缓冲消费:
|
||||
* 消除逐通道轮询的 2 ms 忙等;节拍源 TIM6 -> TIM3
|
||||
* (F4 上 ADC1 只认 TIM2/TIM3/TIM8 的 TRGO,TIM6 TRGO
|
||||
* 仅接 DAC);新增 adc_input_stm32_seq()/_hw_errs()
|
||||
* (换源判据:F4 上 ADC1 收不到 TIM6/TIM7 的 TRGO,见
|
||||
* docs/子系统设计/ADC采样节拍设计说明.md §3.2);新增
|
||||
* adc_input_stm32_seq() / _hw_errs()
|
||||
* v2.1 2026-10-21 王建锋 adc_input_stm32_gaps() 单位由「次数」改为「帧点数」,
|
||||
* 与判定层 adc_health_tpafe_gaps() 同单位
|
||||
*/
|
||||
|
||||
#ifndef ADC_INPUT_H
|
||||
@@ -44,7 +47,7 @@ typedef enum {
|
||||
ADC_SEM_COUNT /* 语义通道数(哨兵) */
|
||||
} adc_semantic_t;
|
||||
|
||||
/* 数据来源:V0.1 的 8 路电气量分属两片转换器(N-18) */
|
||||
/* 数据来源:V0.1 的 8 路电气量分属两片转换器(双转换器无共同采样触发) */
|
||||
typedef enum {
|
||||
ADC_SRC_TPAFE = 0, /* TPAFE5160:16 位、±5 V 双极性、8 通道同步 */
|
||||
ADC_SRC_STM32 /* STM32 ADC1:12 位、0~3.3 V 单极性、9 通道顺序扫描 */
|
||||
@@ -61,7 +64,7 @@ typedef struct {
|
||||
/* STM32 ADC1 顺序扫描通道数(Rank 1..9) */
|
||||
#define ADC_INPUT_STM32_CH_NUM 9U
|
||||
|
||||
/* STM32 ADC1 最近一次取帧状态码(v2.0:DMA 消费,已无"轮询超时"这一档) */
|
||||
/* STM32 ADC1 最近一次取帧状态码(DMA 消费,已无"轮询超时"这一档) */
|
||||
#define ADC_INPUT_RC_OK (0) /* 取到新一帧 */
|
||||
#define ADC_INPUT_RC_START (-1) /* HAL_ADC_Start_DMA 挂载失败 */
|
||||
#define ADC_INPUT_RC_STALL (-2) /* 本拍无新帧(TIM3 未启动 / ADC 停摆 / DMA 出错) */
|
||||
@@ -129,12 +132,14 @@ int adc_input_stm32_rc(void);
|
||||
uint32_t adc_input_stm32_errs(void);
|
||||
|
||||
/*
|
||||
* 函数功能:读取 STM32 ADC1 的丢帧次数
|
||||
* 函数功能:读取 STM32 ADC1 累计丢帧点数
|
||||
* 入口参数:无
|
||||
* 返 回 值:DMA 完成计数一次前进 >1 的次数(中间那帧已被覆盖,未及消费)
|
||||
* 返 回 值:Σ(完成计数每次前进的超出部分),即没被消费就被覆盖掉的帧数
|
||||
* 限定条件:无
|
||||
* 函数说明:与 errs 分开——gap = 消费者超拍(单拍耗时 > 1 ms),
|
||||
* err = 节拍源/ DMA 停摆。两类故障处置不同,混在一起就查不了。
|
||||
* 单位与判定层 adc_health_tpafe_gaps() 一致(帧点数),心跳里可直接对比;
|
||||
* 2026-10-21 之前的固件该字段是"次数"口径。
|
||||
*/
|
||||
uint32_t adc_input_stm32_gaps(void);
|
||||
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
* ≈4 个信号周波),使窗长恰为纹波周期的整数倍 → δ(m+K)≈δ(m),残差进入
|
||||
* 0.01 Hz 量级。f_hat 用上一拍读数(收敛后误差 <0.1 Hz → K_eff 偏差 <1 拍,
|
||||
* 自洽)。
|
||||
* **精度优先于响应的显式取舍(Lead 批准 2026-10-12)**:4 周波窗换来
|
||||
* **精度优先于响应的显式取舍**:4 周波窗换来
|
||||
* 0.317→0.0101 Hz,代价是频率骤变后的窗冲刷(J4 阶跃整定 ≤120 ms,实测
|
||||
* 98 ms)。OF/UF 定时限默认 1000 ms → 测频响应占保护总动作时间 <10%;
|
||||
* 暂态假值另有 J6(跳变后 ≤60 ms 回 ±0.05 Hz)兜底。故 J4 的 120 ms 是
|
||||
|
||||
+11
-5
@@ -9,7 +9,7 @@
|
||||
* 推导与实测见 freq.c 文件头与 test/host/freq 输出)。
|
||||
* 覆盖规划 §3.3 L3-5 的测量侧与 §4.2 测频范围(45~55 Hz 建议段)。
|
||||
*
|
||||
* 原理与判据(详见 App/algo/README.md §3.7 与 test/host/freq/):
|
||||
* 原理与判据(详见 docs/算法/采集与算法层设计说明.md §7 与 test/host/freq/):
|
||||
* - 采样率 Fs = 1000 Hz(TIM3 节拍,与 phasor 同源);45~55 Hz 段每拍
|
||||
* 相位增量 ≈ 0.28~0.35 rad,距 ±π 缠绕边界裕度巨大,无歧义;
|
||||
* - 瞬时值钳制:单拍相位差折算越出 35~65 Hz(**假值带**,宽于系统合理带
|
||||
@@ -21,7 +21,7 @@
|
||||
* - 信号源优先级:UA 基波 → IA 基波(UA 无幅值时用 IA,台架只注电流亦可测频)。
|
||||
*
|
||||
* ⚠ 本层**纯算法**:不碰寄存器、不碰 RTOS、不自己取数(输入 = phasor 使用区)
|
||||
* → 主机侧可免桩单测(L0.5,落实登记簿 A-4)。
|
||||
* → 主机侧可免桩单测(L0.5)。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-12(B8)
|
||||
@@ -49,9 +49,15 @@
|
||||
#define FREQ_INST_MIN_HZ 35.0f
|
||||
#define FREQ_INST_MAX_HZ 65.0f
|
||||
|
||||
/* 幅值门限(有效值,**原始码值口径**,L1-5 标定未实现):低于该值判
|
||||
* "无有效波形"。⚠ 拟定值(远小于正常波形千级码值、远大于浮空噪声),
|
||||
* 标定落地后须按工程量口径复核(同 prot_set 默认值"拟定"口径)。 */
|
||||
/* 幅值门限(有效值,**二次值 V 口径**,「数据可信」卡 6 重定,依据其 §2.3 表第 1 行):
|
||||
* 低于该值判"无有效波形"。取值 50 V = 0.5×Un(相电压标称 100 V,与
|
||||
* App/algo/prot_set.c 头部量纲基准同源):正常带载 UA 在 0.9~1.1×Un,
|
||||
* 50 V 之下只可能是断保险/PT 空载/浮空噪声(浮空实测 ~1.7 V 引脚域、
|
||||
* 二次域更小),不会吃掉正常纹波(纹波谷值远大于 0.5×Un)。
|
||||
* ⚠ 过渡期行为(eng=0,卡 12 标定前):帧值还是引脚电压(≤5 V < 50 V),
|
||||
* 本门**恒失效** → freq_valid()=0 → OF/UF 按"必不启动"填充复位——与卡 5
|
||||
* 未标定闭锁同向(判据链本就停走),不构成额外风险;标定后自动恢复本意。
|
||||
* 数值沿用原 50.0f 属巧合(原为"远小于千级码值"的拟定值),不是漏改。 */
|
||||
#define FREQ_AMP_MIN 50.0f
|
||||
|
||||
/* 相位增量滑窗环容量(拍):96 拍 = 96 ms 上限。实际平均窗长**自适应**
|
||||
|
||||
+170
@@ -0,0 +1,170 @@
|
||||
/*
|
||||
* 模块名称:遥测快照发布层(L2 派生·发布侧,「数据可信」卡 8)
|
||||
* 模块功能:实现见 meas.h——200 ms 基波有效值 + 品质快照,双区无锁读。
|
||||
* 本文件纯算法:输入 = sample_ring 取窗 + 调用方注入(频率/削顶位图),
|
||||
* 无 RTOS / 无寄存器 / 无日志(「数据可信」层内禁令:本目录不写日志)。
|
||||
* 双区不变量(docs/任务规划/数据可信.md §4.5,L2-7 同型,先例 = App/algo/
|
||||
* phasor.c 的双区注释):写侧只碰**非使用区**,全部字段填完后一次
|
||||
* volatile 单字节 store 翻转使用区;读者先取索引再整体拷贝 → 拷贝期
|
||||
* 间写者不可能改读者所在区(两帧间隔 200 ms 远大于 72 B 拷贝)。
|
||||
* 单核顺序执行,无需内存屏障;volatile 阻编译器把字段写入挪到翻转后。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-25(道D 卡 8)
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 DTU 项目组 创建:200 ms 遥测快照 + 双区无锁读
|
||||
*/
|
||||
|
||||
#include "meas.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "phasor.h" /* phasor_calc():基波 DFT 纯函数核(与使用区同核) */
|
||||
#include "sample_ring.h" /* sample_ring_window():以本拍为终止的一周波窗 */
|
||||
|
||||
/* ===== 双区(计算区 / 使用区),口径同 App/algo/phasor.c 的 L2-7 ===== */
|
||||
static meas_snap_t s_ms_snap[2];
|
||||
static volatile uint8_t s_ms_use;
|
||||
static uint8_t s_ms_ready; /* 使用区是否已产出过有效结果 */
|
||||
static uint32_t s_ms_seq; /* 只增帧号(写者单写者,读者原子单字读) */
|
||||
|
||||
/*
|
||||
* 函数功能:清零一份快照(含 rsv 与全部数组元素)
|
||||
* 入口参数:p - 目标快照
|
||||
* 返 回 值:无
|
||||
* 限定条件:p 非空
|
||||
* 函数说明:静态辅助;逐字段清零(不依赖 memset,行为与本目录 phasor_zero 同型)。
|
||||
*/
|
||||
static void meas_zero(meas_snap_t *p) {
|
||||
uint8_t i;
|
||||
|
||||
p->seq = 0u;
|
||||
p->tick_ms = 0u;
|
||||
p->valid = 0u;
|
||||
p->rsv[0] = 0u;
|
||||
p->rsv[1] = 0u;
|
||||
p->rsv[2] = 0u;
|
||||
p->freq_valid = 0u;
|
||||
p->freq_hz = 0.0f;
|
||||
for (i = 0u; i < (uint8_t)ADC_SEM_COUNT; i++) {
|
||||
p->yc[i] = 0.0f;
|
||||
p->qds[i] = 0u;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:遥测快照层初始化
|
||||
* 入口参数:无
|
||||
* 返 回 值:0 - 成功
|
||||
* 限定条件:内核启动前或任务上下文皆可;幂等,可重复调用
|
||||
* 函数说明:清双区 + 索引/就绪/序号复位。init 后到首次发布前,meas_get() 恒
|
||||
* 返 -2(out 清零)、meas_seq() 恒返 0。
|
||||
*/
|
||||
int meas_init(void) {
|
||||
meas_zero(&s_ms_snap[0]);
|
||||
meas_zero(&s_ms_snap[1]);
|
||||
s_ms_use = 0u;
|
||||
s_ms_ready = 0u;
|
||||
s_ms_seq = 0u;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:一拍遥测发布(被调即发布,节拍把关在调用方)
|
||||
* 入口参数:tick_ms - 本拍时基(毫秒) uint32_t
|
||||
* freq_valid_in - 频率读数新鲜度(freq_valid() 值) uint8_t
|
||||
* freq_hz_in - 频率读数(freq_last() 值) float
|
||||
* clip_mask - 削顶通道位图(bit s = 语义通道 s,来源唯一 =
|
||||
* adc_cal_flags(),编排方注入) uint32_t
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅 adcTask 上下文、全仓唯一调用点(meas.h 发布纪律);须在
|
||||
* phasor_run() 之后调用(本拍窗含最新点,与使用区同窗同核)
|
||||
* 函数说明:电气 8 路(语义序中 TEMP 之前的全部通道,映射表属主 =
|
||||
* docs/硬件平台/ADC通道映射说明.md)逐路一周波基波 DFT;任一路取窗
|
||||
* 不足(开机未满一周波)→ 该路 IV|NT + 整帧 valid=0,但照常发布
|
||||
* (品质节拍性刷新,见 meas_tick 头注)。TEMP 只取最新一点透传 +
|
||||
* NT(「数据可信」决策 5)。品质值不因 IV 清零——"值在场但不可信"
|
||||
* 与"值缺失"分开,消费者按 qds 裁决。
|
||||
*/
|
||||
void meas_tick(uint32_t tick_ms, uint8_t freq_valid_in, float freq_hz_in,
|
||||
uint32_t clip_mask) {
|
||||
meas_snap_t *z = &s_ms_snap[1u - s_ms_use]; /* 计算区 = 非使用区 */
|
||||
float win[PHASOR_N];
|
||||
phasor_t p;
|
||||
uint8_t all_ok = 1u;
|
||||
uint8_t s;
|
||||
|
||||
meas_zero(z);
|
||||
z->seq = s_ms_seq + 1u;
|
||||
z->tick_ms = tick_ms;
|
||||
|
||||
for (s = 0u; s < (uint8_t)ADC_SEM_TEMP; s++) {
|
||||
if (sample_ring_window((adc_semantic_t)s, (uint8_t)PHASOR_N, win) == 0) {
|
||||
phasor_calc(win, (uint8_t)PHASOR_N, 1u, &p);
|
||||
z->yc[s] = p.fAmSQ; /* 有效值口径(系数已折算,同 phasor 层) */
|
||||
if ((clip_mask & (1uL << s)) != 0u) {
|
||||
z->qds[s] |= (uint16_t)MEAS_QDS_CLIPPED;
|
||||
}
|
||||
} else {
|
||||
/* 本路窗不足:值缺位(yc 保持 0)+ IV|NT;整帧判不可信 */
|
||||
z->qds[s] = (uint16_t)(MEAS_QDS_IV | MEAS_QDS_NT);
|
||||
all_ok = 0u;
|
||||
}
|
||||
}
|
||||
|
||||
/* TEMP:非交流量语义(「数据可信」决策 5),最新一点原始帧值透传 + NT;
|
||||
* clip_mask 位 8 不参与(adc_cal_flags(TEMP) 恒 0,无削顶语义) */
|
||||
{
|
||||
float tv = 0.0f;
|
||||
|
||||
if (sample_ring_window(ADC_SEM_TEMP, 1u, &tv) == 0) {
|
||||
z->yc[ADC_SEM_TEMP] = tv;
|
||||
z->qds[ADC_SEM_TEMP] = (uint16_t)MEAS_QDS_NT;
|
||||
} else {
|
||||
z->qds[ADC_SEM_TEMP] = (uint16_t)(MEAS_QDS_IV | MEAS_QDS_NT);
|
||||
}
|
||||
}
|
||||
|
||||
z->valid = all_ok;
|
||||
z->freq_valid = (freq_valid_in != 0u) ? 1u : 0u;
|
||||
z->freq_hz = freq_hz_in;
|
||||
|
||||
/* 全部字段就绪后才翻索引 + 置就绪(顺序即协议,勿交换) */
|
||||
s_ms_seq = z->seq;
|
||||
s_ms_use = (uint8_t)(1u - s_ms_use);
|
||||
s_ms_ready = 1u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读当前使用区快照(无锁整体拷贝)
|
||||
* 入口参数:out - 输出快照;失败路径同样清零填充
|
||||
* 返 回 值:0 - 成功;-1 - 参数错;-2 - 从未发布过 int
|
||||
* 限定条件:任意上下文;无(读 volatile 索引 + 普通拷贝)
|
||||
* 函数说明:先取索引再索引拷贝,避免拷贝中途索引被翻转导致"半旧半新"。
|
||||
* 返回 0 时帧内部自洽(同一发布拍的 seq/tick_ms/yc/qds 成套)。
|
||||
*/
|
||||
int meas_get(meas_snap_t *out) {
|
||||
uint8_t z;
|
||||
|
||||
if (out == NULL) {
|
||||
return -1;
|
||||
}
|
||||
if (s_ms_ready == 0u) {
|
||||
meas_zero(out);
|
||||
return -2;
|
||||
}
|
||||
z = s_ms_use;
|
||||
*out = s_ms_snap[z];
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读已发布帧数(只增)
|
||||
* 入口参数:无
|
||||
* 返 回 值:seq,0 = 从未发布(单字读,原子) uint32_t
|
||||
* 限定条件:任意上下文
|
||||
* 函数说明:轻量"是否换帧"探针,正式取数以 meas_get() 为准。
|
||||
*/
|
||||
uint32_t meas_seq(void) {
|
||||
return s_ms_seq;
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 模块名称:遥测快照发布层(L2 派生·发布侧,「数据可信」卡 8)
|
||||
* 模块功能:每 **200 ms** 从 L2 层取一次基波有效值 + 品质,产出一份全仓唯一的
|
||||
* 遥测快照(meas_snap_t,布局权威 = docs/任务规划/数据可信.md §4.5),
|
||||
* 让 104 与 UI **共用同一份上游**——"什么时候算新值、品质怎么判"只此
|
||||
* 一处决定(M5 遥测链的地基;104 改读快照属卡 9,UI 侧 ui_data_feed 属
|
||||
* 「UI 扩展」波次,两者都只是本快照的读者)。
|
||||
* 值口径(批 2 卡 8 的 A-lite 裁决,2026-10-25):**基波有效值瞬时透传**
|
||||
* (同拍 phasor 使用区口径,系数已在 L1.5 折算进帧值——现单位 = ADC
|
||||
* 引脚电压 V,二次值语义等 adc_cal_flags() 出 VALID,见
|
||||
* docs/算法/采集与算法层设计说明.md §10 红线③);**不做** 4 点平均 /
|
||||
* 谐波方和根——docs/任务规划/数据可信.md §4.5 明示"200 ms 是上送节拍
|
||||
* 不是平均窗,再平均引入滞后";谐波 k-bin 轨(docs/算法/继电保护功能
|
||||
* 复现规划.md L2-8)将来扩展时只许在本结构**尾部追加**或留在发布侧
|
||||
* 内部状态,既有字段序不得变更。
|
||||
* TEMP 通道(「数据可信」决策 5:不参与线性标定):yc = 最新一拍原始
|
||||
* 帧值透传,qds 恒置 NT——值在场但非交流量语义,UI 按「数据可信」§4.2
|
||||
* 三态表显示占位符,104 无对应点。
|
||||
* 并发(docs/任务规划/数据可信.md §4.5 原文口径):沿用 phasor 的
|
||||
* **双区 + 单字节活跃索引翻转**(L2-7 同型,不是新机制)——写侧填满
|
||||
* 计算区后一次 store 翻转,读者先取索引再整体拷贝,全程无锁;
|
||||
* seq 只增,消费者据此判"是否换帧"。
|
||||
* 本层**纯算法**:不碰寄存器、不碰 RTOS、不自己取数(窗自 sample_ring、
|
||||
* 频率/品质由调用方注入)→ 主机侧免桩单测 test/host/meas/(L0.5)。
|
||||
* 发布纪律:唯一发布者 = adcTask(在 phasor_run()/freq_run() 之后 200 ms 分频
|
||||
* 调 meas_tick())。评审判据 = 全仓 meas_tick 调用点数 == 1(「数据可信」
|
||||
* §8 风险表"快照双写"行);其余任务/模块**只读**。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-25(道D 卡 8)
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 DTU 项目组 创建:200 ms 遥测快照 + 双区无锁读
|
||||
*/
|
||||
|
||||
#ifndef MEAS_H
|
||||
#define MEAS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "adc_input.h" /* adc_semantic_t / ADC_SEM_COUNT:yc/qds 下标 = 语义通道 */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 上送节拍(ms):adc_task 按本宏对 1 kHz 拍做**经过时间**分频(非拍计数——
|
||||
* idle/skip 拍会把拍计数口径拉得比名义值长),落点见 App/task/adc_task.c */
|
||||
#define MEAS_PERIOD_MS 200u
|
||||
|
||||
/* 每点品质位(uint16)。**低字节与线上 104 QDS 字节同值**(OV/NT/IV,数值
|
||||
* 口径同 App/protocol/iec104/iec104_asdu.h 的 IEC104_QDS_*)——但本层**不得**
|
||||
* include 协议层头(分层方向:算法层在协议层之下,反向依赖禁止),故在此自
|
||||
* 定义同值常量;卡 9 的 104 侧消费直接截低字节上送,一致性由本目录单测与
|
||||
* test/host/iec104/ 既有断言双向钉住。高字节为 meas 域扩展位(线上不直接
|
||||
* 出现,由消费者映射): */
|
||||
#define MEAS_QDS_OV (0x0001u) /* 溢出(留位,卡 8 不产出——判据待运行参数化) */
|
||||
#define MEAS_QDS_NT (0x0020u) /* 非当前值:未就绪 / 语义未处理(TEMP) */
|
||||
#define MEAS_QDS_IV (0x0080u) /* 无效:该路取窗失败,无有效读数 */
|
||||
#define MEAS_QDS_CLIPPED (0x0100u) /* 硬件削顶(adc_cal_flags 位图直通,
|
||||
* 「数据可信」§4.2 三态表;AC 量不可信,
|
||||
* 消费者应映射为品质无效,如卡 9 的 104 IV) */
|
||||
|
||||
/* 遥测快照(docs/任务规划/数据可信.md §4.5 权威布局;实测 72 B,2×72 ≈ 144 B
|
||||
* 双区 = 卡 8 DoD 预算口径,由 test/host/meas/ 的布局钉死用例锁字段偏移——
|
||||
* 改字段 = 破协议,须同步卡 9 / UI) */
|
||||
typedef struct {
|
||||
uint32_t seq; /* 只增;消费者据此判断是否换帧 */
|
||||
uint32_t tick_ms; /* 发布拍时基(osKernelGetTickCount 原值) */
|
||||
uint8_t valid; /* 整帧有效:本拍 8 电气路取窗全部成功 */
|
||||
uint8_t rsv[3]; /* 对齐填充,恒 0 */
|
||||
float yc[ADC_SEM_COUNT]; /* 工程量(引脚电压 V 域;二次值等 VALID 标定) */
|
||||
uint16_t qds[ADC_SEM_COUNT]; /* 每点品质 MEAS_QDS_*(值本身不因品质清零) */
|
||||
uint8_t freq_valid; /* 频率读数新鲜度(freq_valid() 注入) */
|
||||
float freq_hz; /* 频率读数(freq_last() 注入,未收敛为 0) */
|
||||
} meas_snap_t;
|
||||
|
||||
/*
|
||||
* 函数功能:遥测快照层初始化(清双区 / 索引 / 序号 / 就绪标志)
|
||||
* 入口参数:无
|
||||
* 返 回 值:0 - 成功 int
|
||||
* 限定条件:内核启动前或任务上下文皆可(纯 .bss 状态,无 RTOS 依赖);
|
||||
* 本仓落点 = adc_task_init()(每开机一次,幂等可重复调)
|
||||
* 函数说明:init 后 meas_get() 返 -2、meas_seq() 返 0,直到首次 meas_tick()
|
||||
* 发布——消费者据 -2 走"未就绪"分支,不读半初始化数据。
|
||||
*/
|
||||
int meas_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:一拍遥测发布(200 ms 分频由调用方把关,本函数被调即发布一帧)
|
||||
* 入口参数:tick_ms - 本拍时基(毫秒) uint32_t
|
||||
* freq_valid_in - 频率读数新鲜度(freq_valid() 值) uint8_t
|
||||
* freq_hz_in - 频率读数(freq_last() 值;从未收敛为 0.0) float
|
||||
* clip_mask - 削顶通道位图(bit s = 语义通道 s;来源唯一 =
|
||||
* adc_cal_flags() 的 ADC_CAL_F_CLIPPED,编排方注入,
|
||||
* 本层不链 adc_cal——可用性属主在折算侧) uint32_t
|
||||
* 返 回 值:无
|
||||
* 限定条件:**仅 adcTask 上下文、全仓唯一调用点**(发布纪律见文件头);
|
||||
* 须在 sample_ring_push_frame()/phasor_run() 之后调用,本拍窗才含最新点
|
||||
* 函数说明:8 电气路逐路取"以本拍为终止的一周波"做基波 DFT(复用 phasor_calc
|
||||
* 纯函数核,与 phasor 使用区同窗同核 → 同拍同值,见单测钉死),
|
||||
* 任一路窗不足 → 该路 yc=0 + qds=IV|NT、整帧 valid=0(不产出半新半旧
|
||||
* 混合帧;与 phasor_run 的差异:phasor 宁可不切区,meas **照常发布**
|
||||
* 让品质位可见——消费者需要节拍性的品质刷新,seq 亦随之只增)。
|
||||
* TEMP = 最新一点透传 + NT(见文件头)。末尾单字节翻转使用区,
|
||||
* 读者无锁。
|
||||
*/
|
||||
void meas_tick(uint32_t tick_ms, uint8_t freq_valid_in, float freq_hz_in,
|
||||
uint32_t clip_mask);
|
||||
|
||||
/*
|
||||
* 函数功能:读当前使用区快照(无锁整体拷贝)
|
||||
* 入口参数:out - 输出快照;失败(含 -2 未就绪)时**清零填充**,调用方可安全读
|
||||
* 返 回 值:0 - 成功;-1 - 参数错;-2 - 从未发布过(out 已清零,seq=0) int
|
||||
* 限定条件:任意上下文(任务级读者:iec104Task / uiTask / protectTask)
|
||||
* 函数说明:先取一次使用区索引再索引拷贝——拷贝期间写者最多改"**非使用区**",
|
||||
* 本帧无撕裂(双区不变量,同 phasor_get 口径;写者两帧间隔 200 ms
|
||||
* 远大于 72 B 拷贝耗时,读者被压超 200 ms 的竞态窗口按 §4.5 设计接受)。
|
||||
* 判"新不新"用 seq:与上次不同则换帧。
|
||||
*/
|
||||
int meas_get(meas_snap_t *out);
|
||||
|
||||
/*
|
||||
* 函数功能:读已发布帧数(只增)
|
||||
* 入口参数:无
|
||||
* 返 回 值:seq(0 = 从未发布;单字 32 位读,Cortex-M 原子) uint32_t
|
||||
* 限定条件:任意上下文
|
||||
* 函数说明:轻量观测入口(104 周期上送可先比 seq 省 72 B 拷贝);
|
||||
* 与 meas_get 之间无原子性配对要求(读者以 get 返回的 seq 为准)。
|
||||
*/
|
||||
uint32_t meas_seq(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* MEAS_H */
|
||||
+3
-3
@@ -44,7 +44,7 @@ static const float k_fft_cos[PHASOR_N] = {
|
||||
/* ===== L2-2 相量实例表 =====
|
||||
* 参考项目为 **4 个实例**(C01 基波 I/U、C02 零序基波、C03 二次谐波、C04 五次谐波,
|
||||
* `APP/CLS100_Dsp/InstCalCell.c`)。本项目按 V0.1 **有效通道**(`UA`/`UN`/`IA`,
|
||||
* 见规划 §1.6:`UB/UC/IB/IC/IN` 无中点偏置 → 削顶不可用)展开——C01 的"I/U"
|
||||
* 见 docs/算法/继电保护功能复现规划.md §1.6:`UB/UC/IB/IC/IN` 无中点偏置 → 削顶不可用)展开——C01 的"I/U"
|
||||
* 拆为两行(IA 电流 / UA 电压),并补出 C04 的五次谐波轨(L1-9)。
|
||||
* 新增实例只需在表尾加一行,其余代码自动适配。 */
|
||||
static const phasor_inst_t k_inst[] = {
|
||||
@@ -62,8 +62,8 @@ static const phasor_inst_t k_inst[] = {
|
||||
* (单字节 store,Cortex-M 上原子),消费侧读"使用区" → 无锁、无撕裂。
|
||||
* s_use_zone 声明 volatile:阻止编译器把结果写入挪到切换之后
|
||||
* (本平台单核顺序执行,无需额外内存屏障)。
|
||||
* 今日唯一消费者是 adcTask 自身(同拍读写、实际无竞争),本机制为后续
|
||||
* 独立保护任务(L3)预留。 */
|
||||
* 写者 = adcTask(同拍);读者跨两个上下文:adcTask 心跳与 protectTask 判据
|
||||
* (prot_elm_run → phasor_get)→ 无锁读正是为后者服务,勿改成加锁或去掉双区。 */
|
||||
static phasor_t s_zone[2][PHASOR_INST_N];
|
||||
static volatile uint8_t s_use_zone;
|
||||
static uint8_t s_ready; /* 使用区是否已产出过有效结果 */
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
* 覆盖规划 §3.2:L2-1 系数表 / L2-2 实例 / L2-3 有效值输出 /
|
||||
* L2-6 每 1 ms 滑动窗 / L2-7 双区指针原子交换。
|
||||
* ⚠ 本层**纯算法**:不碰寄存器、不碰任务调度、不自己取数(输入由
|
||||
* `sample_ring` 提供)→ 主机侧可单测(L0.5,落实登记簿 A-4)。
|
||||
* `sample_ring` 提供)→ 主机侧可单测(L0.5)。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-10
|
||||
|
||||
+70
-11
@@ -18,12 +18,20 @@ typedef struct {
|
||||
uint8_t over; /* 1 = 过量(越上限动作),0 = 欠量(越下限动作) */
|
||||
uint8_t src; /* 测量源:0 = phasor 基波有效值,1 = freq 测频 */
|
||||
prot_set_id_t en_id; /* 投退定值 */
|
||||
uint8_t trip_ctl; /* 跳闸投退定值项(PS_*_TRIP);PROT_ELM_NO_TRIP =
|
||||
* 该元件没有这一项(告警类)→ 动作只出告警记录。
|
||||
* 用 uint8_t 而不是 prot_set_id_t 就是因为要能表达
|
||||
* "无此项"这个第 41 种取值(PS_ID_COUNT 是数组长度,
|
||||
* 拿去喂 prot_set_get_ctl() 会越界读) */
|
||||
prot_set_id_t set_id; /* 动作定值 */
|
||||
prot_set_id_t dly_id; /* 延时定值(ms) */
|
||||
prot_set_id_t ret_id; /* 返回系数定值(RET_OVER / RET_UNDER) */
|
||||
const char *pprefix; /* 相量实例名前缀("C01"…);src=1 时为 0(不用) */
|
||||
} pe_cfg_t;
|
||||
|
||||
/* k_pe[].trip_ctl 的"无此定值项"取值(见上方字段注释) */
|
||||
#define PROT_ELM_NO_TRIP 0xFFu
|
||||
|
||||
/* 测量源枚举(pe_cfg_t.src) */
|
||||
#define PE_SRC_PHASOR 0u
|
||||
#define PE_SRC_FREQ 1u
|
||||
@@ -31,18 +39,24 @@ typedef struct {
|
||||
/* freq 无有效读数时给欠量元件的"必不启动"填充值(远大于任何欠量定值上限) */
|
||||
#define PE_MEAS_BIG 1e30f
|
||||
|
||||
static const pe_cfg_t k_pe[PE_COUNT] = {
|
||||
{ "OC1-IA", 1u, PE_SRC_PHASOR, PS_OC1_EN, PS_OC1_CUR, PS_OC1_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OC2-IA", 1u, PE_SRC_PHASOR, PS_OC2_EN, PS_OC2_CUR, PS_OC2_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OC3-IA", 1u, PE_SRC_PHASOR, PS_OC3_EN, PS_OC3_CUR, PS_OC3_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OL-IA", 1u, PE_SRC_PHASOR, PS_OL_EN, PS_OL_CUR, PS_OL_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OV-UA", 1u, PE_SRC_PHASOR, PS_OV_EN, PS_OV_VOL, PS_OV_DLY, PS_RET_OVER, "C02" },
|
||||
{ "UV-UA", 0u, PE_SRC_PHASOR, PS_UV_EN, PS_UV_VOL, PS_UV_DLY, PS_RET_UNDER, "C02" },
|
||||
{ "3U0-UN", 1u, PE_SRC_PHASOR, PS_3U0_EN, PS_3U0_VOL, PS_3U0_DLY, PS_RET_OVER, "C03" },
|
||||
/* 列序 = pe_cfg_t 字段序(name, over, src, en_id, trip_ctl, set_id, dly_id, ret_id,
|
||||
* pprefix)。⚠ trip_ctl 紧跟 en_id 插入,新增元件必须照这一列的口径填:有跳闸投退
|
||||
* 定值项的填 PS_*_TRIP,没有的填 PROT_ELM_NO_TRIP(= 该元件动作只出告警记录)。*/
|
||||
static const pe_cfg_t k_pe[PE_COUNT] = { /* ← 表头注释见上,行内列序与之一致 */
|
||||
{ "OC1-IA", 1u, PE_SRC_PHASOR, PS_OC1_EN, PS_OC1_TRIP, PS_OC1_CUR, PS_OC1_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OC2-IA", 1u, PE_SRC_PHASOR, PS_OC2_EN, PS_OC2_TRIP, PS_OC2_CUR, PS_OC2_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OC3-IA", 1u, PE_SRC_PHASOR, PS_OC3_EN, PS_OC3_TRIP, PS_OC3_CUR, PS_OC3_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OL-IA", 1u, PE_SRC_PHASOR, PS_OL_EN, PS_OL_TRIP, PS_OL_CUR, PS_OL_DLY, PS_RET_OVER, "C01" },
|
||||
/* ↓ 三项 + 下面两项频率元件:参考系统的告警段(0x8027~0x802A)本就没有「跳闸
|
||||
投退」这一档,g_prot_set_meta 也无对应项 → PROT_ELM_NO_TRIP,动作一律归告警。
|
||||
这不是兜底猜测,是照实反映("哪些元件算跳闸"的真源始终是这张表 + 定值表)。*/
|
||||
{ "OV-UA", 1u, PE_SRC_PHASOR, PS_OV_EN, PROT_ELM_NO_TRIP, PS_OV_VOL, PS_OV_DLY, PS_RET_OVER, "C02" },
|
||||
{ "UV-UA", 0u, PE_SRC_PHASOR, PS_UV_EN, PROT_ELM_NO_TRIP, PS_UV_VOL, PS_UV_DLY, PS_RET_UNDER, "C02" },
|
||||
{ "3U0-UN", 1u, PE_SRC_PHASOR, PS_3U0_EN, PROT_ELM_NO_TRIP, PS_3U0_VOL, PS_3U0_DLY, PS_RET_OVER, "C03" },
|
||||
/* 过频/低频(L3-5,B8):测量量 = freq 层测得频率(Hz),返回系数独立
|
||||
* (RET_OF/RET_UF——频率滞回带与电气量不同量级,勿共用 0.95/1.05) */
|
||||
{ "OF-F", 1u, PE_SRC_FREQ, PS_OF_EN, PS_OF_HZ, PS_OF_DLY, PS_RET_OF, 0 },
|
||||
{ "UF-F", 0u, PE_SRC_FREQ, PS_UF_EN, PS_UF_HZ, PS_UF_DLY, PS_RET_UF, 0 },
|
||||
{ "OF-F", 1u, PE_SRC_FREQ, PS_OF_EN, PROT_ELM_NO_TRIP, PS_OF_HZ, PS_OF_DLY, PS_RET_OF, 0 },
|
||||
{ "UF-F", 0u, PE_SRC_FREQ, PS_UF_EN, PROT_ELM_NO_TRIP, PS_UF_HZ, PS_UF_DLY, PS_RET_UF, 0 },
|
||||
};
|
||||
|
||||
/* 运行时状态(picked/asserted 的对外快照 + 延时起点 + 相量实例索引缓存) */
|
||||
@@ -189,7 +203,8 @@ void prot_elm_eval(uint32_t now_ms, const float meas[PE_COUNT]) {
|
||||
* 函数功能:适配层——按实例表 src 组装测量数组后调判据核
|
||||
* 入口参数:now_ms - 单调时基(tick ms)
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅保护消费者任务上下文;工程量↔码值换算 = 1(L1-5 标定未实现)
|
||||
* 限定条件:仅保护消费者任务上下文;测量已是 L1.5 工程量(未标定域由
|
||||
* protectTask 卡 5 闭锁挡在门外,本函数不感知标定态)
|
||||
* 函数说明:src=PHASOR 行任一相量未就绪(未满一周波)→ **整拍跳过**,
|
||||
* 不引入陈旧/半新测量(B5 口径)。src=FREQ 行:freq 有有效读数
|
||||
* (started 且幅值门 OK)→ 填测得频率;否则按方向填"必不启动"值
|
||||
@@ -258,6 +273,50 @@ uint32_t prot_elm_active_mask(void) {
|
||||
return mask;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:聚合启动位图
|
||||
* 入口参数:无
|
||||
* 返 回 值:bit i = 元件 i picked(越限电平,早于/等于 asserted)
|
||||
* 限定条件:任意上下文(只读快照,无锁;与 prot_elm_active_mask 同口径)
|
||||
* 函数说明:调用方是 protectTask 的「保护启动」记录边沿检测(task-10)。本函数
|
||||
* **只聚合电平**,不造边沿——边沿属观测者状态(s_prev_pick 在那一侧),
|
||||
* 判据核里再存一份"上一拍"会让 eval 不再幂等。
|
||||
*/
|
||||
uint32_t prot_elm_pick_mask(void) {
|
||||
uint32_t mask = 0u;
|
||||
uint32_t i;
|
||||
|
||||
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
|
||||
if (s_rt[i].picked != 0u) {
|
||||
mask |= (1uL << i);
|
||||
}
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:判定该元件动作上升沿应归哪一类记录(见 prot_elm.h 同号声明)
|
||||
* 入口参数:id - 元件编号 prot_elm_id_t
|
||||
* 返 回 值:PROT_REC_TRIP / PROT_REC_ALARM / PROT_REC_NONE(id 越界) prot_rec_kind_t
|
||||
* 限定条件:任意上下文;只读,不改任何状态
|
||||
* 函数说明:判据唯一来源 = k_pe[id].trip_ctl 指向的那枚 PS_*_TRIP 投退定值。
|
||||
* trip_ctl == PROT_ELM_NO_TRIP 时**先返回告警**,绝不把 0xFF 喂给
|
||||
* prot_set_get_ctl()(那是数组越界读,会读到定值区外的 RAM——
|
||||
* 与"未标定拿 ADC 引脚电压当二次值"同类的静默错数据形状)。
|
||||
*/
|
||||
prot_rec_kind_t prot_elm_record_kind(prot_elm_id_t id) {
|
||||
uint8_t trip_ctl;
|
||||
|
||||
if (id >= PE_COUNT) {
|
||||
return PROT_REC_NONE;
|
||||
}
|
||||
trip_ctl = k_pe[(uint32_t)id].trip_ctl;
|
||||
if (trip_ctl == (uint8_t)PROT_ELM_NO_TRIP) {
|
||||
return PROT_REC_ALARM;
|
||||
}
|
||||
return (prot_set_get_ctl((prot_set_id_t)trip_ctl) != 0u) ? PROT_REC_TRIP : PROT_REC_ALARM;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取实例名(日志用)
|
||||
* 入口参数:id - 元件编号
|
||||
|
||||
+42
-2
@@ -4,7 +4,7 @@
|
||||
* 同 phasor 实例表模式)。判据为**纯逻辑**(吃测量值 + 定值),
|
||||
* 主机侧可离线单测;出口/SOE(L4)与任务编排为后续。
|
||||
*
|
||||
* V0.1 有效实例(N-18:仅 TPAFE 三路):
|
||||
* V0.1 有效实例(双转换器无共同采样触发 → 仅 TPAFE 三路):
|
||||
* OC1/OC2/OC3 三段式相间过流(A 相 IA,L3-1)
|
||||
* OL 过负荷(A 相 IA,L3-3)
|
||||
* OV/UV 过压/欠压(UA,L3-4)
|
||||
@@ -22,7 +22,10 @@
|
||||
* - 返回**即时**(参考 GRly_TimeRelay 带 10 ms 返回延时——V0.1 无出口硬件,
|
||||
* 抖动只影响日志,P4 接出口时再补返回延时);
|
||||
* - 复压/方向/无压无流闭锁(dwSetFyCtrl 类)随 P2/P3 元件扩展;
|
||||
* - 工程量↔码值换算系数暂为 1(L1-5 标定未实现,同 phasor 层口径)。
|
||||
* - 测量输入 = adc_task 经 adc_cal_apply() 折算后的工程量(L1.5,卡 2 已落地):
|
||||
* 未标定(VALID=0)时单位只是 ADC 引脚电压——该域下判据必然错配,由
|
||||
* protectTask 的未标定闭锁(卡 5)停走整条判据链,元件核本身不掺和
|
||||
* 标定语义(保持纯逻辑);标定后单位 = 二次值,定值口径才名副其实。
|
||||
*
|
||||
* 线程约定:init 内核启动前或保护消费者任务上下文;eval/run 仅保护消费者任务
|
||||
* 上下文(B5 起 = protectTask);request_reset 任意上下文(跨上下文复归请求,
|
||||
@@ -97,6 +100,43 @@ void prot_elm_state(prot_elm_id_t id, prot_elm_state_t *out);
|
||||
/* 动作位图(bit i = 元件 i asserted),供 L4 出口 / 心跳日志 */
|
||||
uint32_t prot_elm_active_mask(void);
|
||||
|
||||
/* 启动位图(bit i = 元件 i picked),供「保护启动」记录边沿(task-10,2026-10-26):
|
||||
* picked 是越限滞回后的电平,早于(或同拍于)asserted——只看 active_mask 会让
|
||||
* "启动了但没到点"这一整段在记录里彻底消失,而它正是"保护启动"页要看的东西。 */
|
||||
uint32_t prot_elm_pick_mask(void);
|
||||
|
||||
/*
|
||||
* 元件动作边沿的记录归属(task-10「五类记录」)。
|
||||
* ⚠ 本枚举刻意**不**用 syslog_id_t:那会把 App/log 的头拽进 algo 层,而
|
||||
* App/algo/prot_elm.c 同时被 test/host/prot_dim 编进测试单元(那个运行器的
|
||||
* include 路径里没有 App/log,且不在本道写域)→ 一改就在别人的单测上爆错。
|
||||
* "元件 → 哪一类"是判据域事实(属主在本文件),"哪一类 → 哪个日志文件"是
|
||||
* 记录域映射(属主在 App/task/protect_task.c 的一行三元),两边各自单源。
|
||||
*/
|
||||
typedef enum {
|
||||
PROT_REC_TRIP = 0u, /* 跳闸型动作 → 事故记录 */
|
||||
PROT_REC_ALARM, /* 告警型动作 → 保护告警记录 */
|
||||
PROT_REC_NONE /* id 越界 → 调用方一条都不许落 */
|
||||
} prot_rec_kind_t;
|
||||
|
||||
/*
|
||||
* 函数功能:判定"该元件若发生动作上升沿,应归哪一类记录"
|
||||
* 入口参数:id - 元件编号 prot_elm_id_t
|
||||
* 返回值:PROT_REC_TRIP / PROT_REC_ALARM / PROT_REC_NONE(越界) prot_rec_kind_t
|
||||
* 限定条件:任意上下文(只读 k_pe[] + prot_set RAM,不落任何状态)
|
||||
* 函数说明:判据**唯一来源** = 该元件的跳闸投退定值项 g_prot_set_meta 里的
|
||||
* PS_*_TRIP(其枚举注释即"0 告警 / 1 跳闸")。元件→投退项的对应关系
|
||||
* 放在实例表 k_pe[] 的 trip_ctl 列(与 en_id/set_id/dly_id 同一张表,
|
||||
* 新增元件只加一行);**本函数里没有第二份"哪些元件算跳闸"的名单**,
|
||||
* 所以它不会与 g_prot_set_meta / k_pe[] / 整定视图门禁互相打架。
|
||||
* 过压/欠压/零序过压/过频/低频五项本就没有 TRIP 投退项(告警类)→
|
||||
* trip_ctl = PROT_ELM_NO_TRIP → 恒 PROT_REC_ALARM,这是照实反映,
|
||||
* 不是兜底猜测。
|
||||
* 定值巡检闭锁(prot_set_locked)不影响本判定:闭锁态下元件会被复位、
|
||||
* 根本走不到动作上升沿,调用方无需在此重复判闭锁。
|
||||
*/
|
||||
prot_rec_kind_t prot_elm_record_kind(prot_elm_id_t id);
|
||||
|
||||
/* 实例名(ROM 常量,日志用) */
|
||||
const char *prot_elm_name(prot_elm_id_t id);
|
||||
|
||||
|
||||
@@ -5,6 +5,14 @@
|
||||
* ⚠ 默认值/范围为拟定值(参考 EEPROM 出厂默认表不在本仓库,规划 §10.3 待补);
|
||||
* 通讯地址逐项对照参考 Comm/Pub/Const_CommSetPara.c g_tCommSetParaTab。
|
||||
*
|
||||
* 量纲口径(「数据可信」卡 6 复核,2026-10-25):A/V 项 = **二次值**工程量,
|
||||
* 标称基准 100 V / 1 A(UA/UB/UC 相电压、3U0、OV/UV 同域——OV 120 = 1.2×Un、
|
||||
* UV 20 = 0.2×Un、3U0 15 = 0.15×Un;OC1/2/3 = 10/5/2 ×Ir、OL = 1.0×Ir,
|
||||
* 三段递升与过负荷低于速断均自洽)。ADC 帧仅在 adc_cal VALID 置位后才处
|
||||
* 于此域(未标定 = 引脚电压域,由卡 5 闭锁停走判据,见 protect_task.c);
|
||||
* min/max 范围覆盖 0~4×标称,无需随本卡改动数值——换算依据与三处下游
|
||||
* 常数同源,见 docs/任务规划/数据可信.md §2.3。
|
||||
*
|
||||
* 创建日期:2026-10-11
|
||||
*/
|
||||
#include "prot_set.h"
|
||||
|
||||
+7
-3
@@ -26,9 +26,13 @@
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* Q10 定点:工程量 × 1024(与参考 2^10 口径一致) */
|
||||
#define PROT_SET_Q10(v) ((int32_t)(((double)(v)) * 1024.0))
|
||||
#define PROT_SET_FROM_Q10(v) ((float)((double)(v) / 1024.0))
|
||||
/* Q10 定点:工程量 × 1024(与参考 2^10 口径一致)。
|
||||
* 必须写成单精度:×/÷1024 只是 2^10 指数平移,float 与 double 的结果逐位相同
|
||||
* (宏里 42 个字面量 + int32 全域往返均已穷举核对),而写成 double 会把
|
||||
* MicroLIB 的软浮点拉进镜像——F407 只有单精度 FPU,本工程的保护判据每
|
||||
* 20 ms 节拍每元件要读三次定值(见 prot_elm_run),软件双精度纯属白烧。 */
|
||||
#define PROT_SET_Q10(v) ((int32_t)((float)(v) * 1024.0f))
|
||||
#define PROT_SET_FROM_Q10(v) ((float)(v) / 1024.0f)
|
||||
|
||||
/* 定值项编号(枚举序 = RAM 数组序 = 校验和覆盖序,只增不删不改序) */
|
||||
typedef enum {
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
/*
|
||||
* 模块名称:运行参数段(L2/L3 整定层,「数据可信」卡 10)——实现
|
||||
* 模块功能:见 run_par.h。工厂默认 = 「数据可信」卡 6 额定 0.5 % 档(电压 500
|
||||
* mV / 电流 5 mA / 频率 500 mHz,毫单位 int32 定点落盘);[run_par]
|
||||
* 段 9 键(db_ua…db_freq,均 ≤15 字符)≤ CFG_FILE_MAX_KEYS 不拆段
|
||||
* (通道系数覆盖**不落本段**——直通 adc_cal 的 [adc_cal]/[adc_cal_ofs],
|
||||
* 系数唯一真源在那两段,lead 裁②a)。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-25(道D 卡 10)
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 DTU 项目组 创建:[run_par] 段 + 104 映射 + 直通 adc_cal
|
||||
* + B7 式脏标志落盘请求
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
#include <math.h>
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "run_par.h"
|
||||
|
||||
#include "adc_cal.h"
|
||||
#include "cfg_file.h"
|
||||
|
||||
#define DBG_TAG "[PAR]"
|
||||
#include "dbg_log.h"
|
||||
|
||||
/* 常量定义区 */
|
||||
|
||||
/* cfg 段名(≤ CFG_FILE_MAX_NAME_LEN);键名 6~7 字符小写 */
|
||||
#define RUN_PAR_SEC "run_par"
|
||||
|
||||
/* 104 运行参数地址(复现规划 §5.1 类内落实;权威逐项表不在仓,送检点表到位
|
||||
* 后以送检为准替换本常量段——lead 裁①。0x8027~0x802A = prot_set 告警段,永不让位) */
|
||||
#define RUN_PAR_IOA_DB0 0x8030u /* 9 点:0x8030~0x8038 */
|
||||
#define RUN_PAR_IOA_K0 0x8040u /* k 覆盖 8 路:0x8040~0x8047 */
|
||||
#define RUN_PAR_IOA_B0 0x8048u /* b 覆盖 8 路:0x8048~0x804F */
|
||||
|
||||
/* 死区合法域上限(毫单位):电压 50 V / 电流 1 A / 频率 5 Hz——守卫挡误写,
|
||||
* 不限工况;下限统一 0(负死区 = 配置错误) */
|
||||
#define RUN_PAR_DB_MAX_U 50000L
|
||||
#define RUN_PAR_DB_MAX_I 1000L
|
||||
#define RUN_PAR_DB_MAX_F 5000L
|
||||
|
||||
/* 静态变量定义区 */
|
||||
|
||||
/* [run_par] 段 RAM 副本(引擎原地更新;属主契约与 µs 级混窗见「数据可信」§4.4,
|
||||
* 同 adc_cal——本层不挂回调,副本即生效值,消费侧实时读) */
|
||||
typedef struct {
|
||||
int32_t db[RUN_PAR_DB_N];
|
||||
} run_par_db_t;
|
||||
|
||||
static const run_par_db_t s_db_rom = {
|
||||
{ 500L, 500L, 500L, 500L, /* UA/UB/UC/UN:0.5 V(卡 6 额定档) */
|
||||
5L, 5L, 5L, 5L, /* IA/IB/IC/IN:0.005 A */
|
||||
500L } /* 频率:0.5 Hz(绝对档,非额定百分比) */
|
||||
};
|
||||
static run_par_db_t s_db;
|
||||
|
||||
/* 键表 offset 与 run_par_db_t 字段一一对应(sizeof(int32_t) 步进) */
|
||||
static const cfg_key_def_t s_db_keys[] = {
|
||||
{ "db_ua", CFG_VAL_INT, 0u, 4u },
|
||||
{ "db_ub", CFG_VAL_INT, 4u, 4u },
|
||||
{ "db_uc", CFG_VAL_INT, 8u, 4u },
|
||||
{ "db_un", CFG_VAL_INT, 12u, 4u },
|
||||
{ "db_ia", CFG_VAL_INT, 16u, 4u },
|
||||
{ "db_ib", CFG_VAL_INT, 20u, 4u },
|
||||
{ "db_ic", CFG_VAL_INT, 24u, 4u },
|
||||
{ "db_in", CFG_VAL_INT, 28u, 4u },
|
||||
{ "db_freq", CFG_VAL_INT, 32u, 4u }
|
||||
};
|
||||
|
||||
/* 落盘脏标志:104 写成功置位,run_par_store_save 成功清零(B7 prot_set 同款,
|
||||
* 只置不清 = 写盘失败天然重试);单字节,跨任务 peek 无害 */
|
||||
static uint8_t s_dirty;
|
||||
|
||||
/* 内部函数实现区 */
|
||||
|
||||
/*
|
||||
* 函数功能:死区点量程上限(毫单位)
|
||||
* 入口参数:idx - 0..8 uint8_t
|
||||
* 返 回 值:上限;idx 越界返 0 长(一切值皆拒) 内 int32_t
|
||||
*/
|
||||
static int32_t s_db_cap_milli(uint8_t idx) {
|
||||
if (idx < 4u) {
|
||||
return RUN_PAR_DB_MAX_U;
|
||||
}
|
||||
if (idx < RUN_PAR_DB_N - 1u) {
|
||||
return RUN_PAR_DB_MAX_I;
|
||||
}
|
||||
if (idx == (uint8_t)(RUN_PAR_DB_N - 1u)) {
|
||||
return RUN_PAR_DB_MAX_F;
|
||||
}
|
||||
return 0L;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:工程量浮点 → 毫单位定点(只舍不入的截断由调用侧范围守卫兜底)
|
||||
* 入口参数:v - 工程量(须已判有限) float
|
||||
* 返 回 值:四舍五入最近毫单位 内 int32_t
|
||||
*/
|
||||
static int32_t s_milli_of(float v) {
|
||||
return (int32_t)lrintf(v * 1000.0f);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:死区值校验(不提交),set/104 选择段共用同一守卫 = 两路语义同源
|
||||
* 入口参数:idx - 0..8 uint8_t;db - 工程量 float
|
||||
* 返 回 值:合法毫单位 ≥0;-1 非法(越界/NaN/负/越程) 内 int32_t
|
||||
*/
|
||||
static int32_t s_db_check(uint8_t idx, float db) {
|
||||
int32_t milli;
|
||||
|
||||
if ((idx >= RUN_PAR_DB_N) || !isfinite(db) || !(db >= 0.0f)) {
|
||||
return -1L;
|
||||
}
|
||||
milli = s_milli_of(db);
|
||||
if ((milli < 0L) || ((long long)milli > (long long)s_db_cap_milli(idx))) {
|
||||
return -1L;
|
||||
}
|
||||
return milli;
|
||||
}
|
||||
|
||||
/* 公共函数实现区 */
|
||||
|
||||
/*
|
||||
* 函数功能:初始化——副本置工厂默认 + 注册 [run_par] 段(幂等,内核前)
|
||||
* 返 回 值:无
|
||||
*/
|
||||
void run_par_init(void) {
|
||||
cfg_section_reg_t reg;
|
||||
|
||||
(void)memcpy(&s_db, &s_db_rom, sizeof(s_db));
|
||||
s_dirty = 0u;
|
||||
|
||||
(void)memset(®, 0, sizeof(reg));
|
||||
reg.name = RUN_PAR_SEC;
|
||||
reg.p_data = &s_db;
|
||||
reg.p_defaults = &s_db_rom;
|
||||
reg.data_size = sizeof(s_db);
|
||||
reg.p_keys = s_db_keys;
|
||||
reg.key_count = (uint8_t)(sizeof(s_db_keys) / sizeof(s_db_keys[0]));
|
||||
reg.p_cb = NULL; /* 不挂回调:RAM 副本即生效值(同 adc_cal) */
|
||||
if (cfg_file_register(®) != 0) {
|
||||
DBG_ERROR("register %s FAIL (dup/full?) - file values inactive",
|
||||
RUN_PAR_SEC);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读第 idx 路死区(工程量)
|
||||
* 返 回 值:V / A / Hz;越界返 0.0(消费侧按"无死区"保守处理) float
|
||||
*/
|
||||
float run_par_deadband(uint8_t idx) {
|
||||
if (idx >= RUN_PAR_DB_N) {
|
||||
return 0.0f;
|
||||
}
|
||||
return ((float)s_db.db[idx]) / 1000.0f; /* 毫→工程唯一换算点 */
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:写第 idx 路死区(守卫通过才提交;成功置脏)
|
||||
* 返 回 值:0 成功;-1 非法(副本不动) int
|
||||
*/
|
||||
int run_par_deadband_set(uint8_t idx, float db) {
|
||||
int32_t milli = s_db_check(idx, db);
|
||||
|
||||
if (milli < 0L) {
|
||||
return -1;
|
||||
}
|
||||
s_db.db[idx] = milli;
|
||||
s_dirty = 1u;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:104 运行参数地址读(命中集精确匹配,见 run_par.h 地址表)
|
||||
* 返 回 值:0 命中;-1 非本段(调用方落 prot_set) int
|
||||
*/
|
||||
int run_par_104_read(uint32_t addr, float *out) {
|
||||
if (out == NULL) {
|
||||
return -1;
|
||||
}
|
||||
if ((addr >= RUN_PAR_IOA_DB0) && (addr < RUN_PAR_IOA_DB0 + RUN_PAR_DB_N)) {
|
||||
*out = run_par_deadband((uint8_t)(addr - RUN_PAR_IOA_DB0));
|
||||
return 0;
|
||||
}
|
||||
if ((addr >= RUN_PAR_IOA_K0) && (addr < RUN_PAR_IOA_K0 + RUN_PAR_CH_N)) {
|
||||
*out = adc_cal_k_uv((adc_semantic_t)(addr - RUN_PAR_IOA_K0));
|
||||
return 0;
|
||||
}
|
||||
if ((addr >= RUN_PAR_IOA_B0) && (addr < RUN_PAR_IOA_B0 + RUN_PAR_CH_N)) {
|
||||
*out = adc_cal_b_codes((adc_semantic_t)(addr - RUN_PAR_IOA_B0));
|
||||
return 0;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:104 运行参数地址写(两段式;k/b 直通 adc_cal_set——**run_par 不存
|
||||
* 系数**,系数唯一真源 = [adc_cal]/[adc_cal_ofs],lead 裁②a)
|
||||
* 返 回 值:0 接受 / -1 拒绝 / -2 非本段(调用方落 prot_set) int
|
||||
* 函数说明:选择段 = 校验不提交(死区走与 set 同源守卫;k/b 的 adc_cal 合法带
|
||||
* 由执行段裁决——IEC104 允许执行段 PN,两段结果可不一致,注释在此
|
||||
* 明说防"选择过了怎么还 PN"的现场困惑);k/b 单侧写 RMW 用**当前
|
||||
* 生效值**做另一侧(生效值经合法带哨兵,天然满足再写校验)。
|
||||
*/
|
||||
int run_par_104_write(uint32_t addr, float v, uint8_t select) {
|
||||
uint8_t slot; /* 每个命中分支先赋值再使用(无未init读路径) */
|
||||
int32_t k_q16;
|
||||
int32_t b_codes;
|
||||
int rc;
|
||||
|
||||
if ((addr >= RUN_PAR_IOA_DB0) && (addr < RUN_PAR_IOA_DB0 + RUN_PAR_DB_N)) {
|
||||
int32_t milli;
|
||||
|
||||
slot = (uint8_t)(addr - RUN_PAR_IOA_DB0);
|
||||
milli = s_db_check(slot, v);
|
||||
if (milli < 0L) {
|
||||
return -1;
|
||||
}
|
||||
if (select == 0u) {
|
||||
s_db.db[slot] = milli;
|
||||
s_dirty = 1u;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if ((addr >= RUN_PAR_IOA_K0) && (addr < RUN_PAR_IOA_K0 + RUN_PAR_CH_N)) {
|
||||
slot = (uint8_t)(addr - RUN_PAR_IOA_K0);
|
||||
if (!isfinite(v) || !(v > 0.0f) || (v > 1.0e9f)) {
|
||||
return -1;
|
||||
}
|
||||
if (select != 0u) {
|
||||
return 0; /* 粗守过了 ≠ 执行必过:合法带裁决在执行段(见函数头) */
|
||||
}
|
||||
k_q16 = (int32_t)lrintf(v * 65536.0f);
|
||||
b_codes = (int32_t)lrintf(adc_cal_b_codes((adc_semantic_t)slot));
|
||||
rc = adc_cal_set((adc_semantic_t)slot, k_q16, b_codes);
|
||||
if (rc == 0) {
|
||||
s_dirty = 1u;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
if ((addr >= RUN_PAR_IOA_B0) && (addr < RUN_PAR_IOA_B0 + RUN_PAR_CH_N)) {
|
||||
slot = (uint8_t)(addr - RUN_PAR_IOA_B0);
|
||||
if (!isfinite(v) || (v < -1000000.0f) || (v > 1000000.0f)) {
|
||||
return -1; /* 粗守 ±1e6 码;±8192 真带由 adc_cal 执行段裁 */
|
||||
}
|
||||
if (select != 0u) {
|
||||
return 0;
|
||||
}
|
||||
b_codes = (int32_t)lrintf(v);
|
||||
k_q16 = (int32_t)lrintf(adc_cal_k_uv((adc_semantic_t)slot) * 65536.0f);
|
||||
rc = adc_cal_set((adc_semantic_t)slot, k_q16, b_codes);
|
||||
if (rc == 0) {
|
||||
s_dirty = 1u;
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
return -2;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:落盘脏探查(非消费)
|
||||
* 返 回 值:1 有未落盘整定;0 干净 uint8_t
|
||||
*/
|
||||
uint8_t run_par_dirty_peek(void) {
|
||||
return s_dirty;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:执行落盘(整档外科保存成功后清脏;失败脏保持 = 下轮重试)
|
||||
* 返 回 值:0 成功/无脏;-1 写盘失败 int
|
||||
* 限定条件:defaultTask(配置引擎属主)上下文,SYS_EVT_RUN_PAR_SAVE 驱动
|
||||
* 函数说明:一次 save 同时覆盖 [run_par] 9 键与刚被 adc_cal_set 改写的
|
||||
* [adc_cal]/[adc_cal_ofs] 副本(lead 裁②:一条事件位一次 save)。
|
||||
*/
|
||||
int run_par_store_save(void) {
|
||||
if (s_dirty == 0u) {
|
||||
return 0;
|
||||
}
|
||||
if (cfg_file_save() != 0) {
|
||||
DBG_ERROR("cfg save FAIL - keep dirty, retry next round");
|
||||
return -1;
|
||||
}
|
||||
s_dirty = 0u;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 模块名称:运行参数段(L2/L3 整定层,「数据可信」卡 10)
|
||||
* 模块功能:遥测死区(9 点)与通道标定系数覆盖(k/b 各 8 路)的**单一真源**:
|
||||
* - 死区 = 本模块 int32 毫单位 RAM 副本(注册进 cfg 引擎 [run_par] 段,
|
||||
* 工厂默认 = 「数据可信」卡 6 额定 0.5 % 档),104 消费侧
|
||||
* (App/protocol/iec104/iec104_pts.c)每拍实时读取——无投递、无缓存
|
||||
* 副本(卡 9 曾在 pts 侧留运行时副本 s_yc_db,卡 10 落地后收编于此,
|
||||
* 双源必打架);
|
||||
* - 系数覆盖 = **直通 adc_cal_set()**([adc_cal]/[adc_cal_ofs] 是系数
|
||||
* 唯一真源,卡 4 已定;本模块只加 104 地址映射 + 落盘请求,不另存
|
||||
* 一份系数——同 D1 裁"两处判断必打架"逻辑)。
|
||||
* 104 地址映射(docs/算法/继电保护功能复现规划.md §5.1 运行参数类
|
||||
* 0x8020H~0x8058H;逐项表原文"待补"(参考项目 CommPara_ConfigTable
|
||||
* 不在仓),本表即卡 10 的落实,送检点表若另给权威地址只改常量段:
|
||||
* 0x8030~0x8038 = 死区 UA/UB/UC/UN/IA/IB/IC/IN/FREQ(工程量 V/A/Hz);
|
||||
* 0x8040~0x8047 = k 覆盖(µV/码);0x8048~0x804F = b 覆盖(码值,可负);
|
||||
* 其余留白(未启用类别),**0x8027~0x802A 属 prot_set 告警段**,本表
|
||||
* 永不让位(两表按精确地址分流,见 iec104_pts.c set 路由)。
|
||||
* 持久化(B7 prot_set 先例同款):任一写成功 → 脏标志只置不清;
|
||||
* iec104Task 主循环 peek → 置 SYS_EVT_RUN_PAR_SAVE → defaultTask
|
||||
* (配置引擎属主)run_par_store_save() 调 cfg_file_save() 整档外科
|
||||
* 保存(含 [run_par] 与刚被 adc_cal_set 改写的 [adc_cal]/[adc_cal_ofs]
|
||||
* 副本)→ 成功清脏;失败留脏 = 下轮天然重试。本模块自身零文件 IO
|
||||
* (属主纪律)。
|
||||
* 并发契约:引擎副本 int32 对齐字读写在 Cortex-M4 上原子;poll 重载 µs 级窗口
|
||||
* 为「数据可信」§4.4 明载设计(adc_cal 同款)。写路径 = 104 执行段
|
||||
* (iec104Task)与引擎读路径互不加锁,勿改成"读侧缓存"(缓存 = 新真源)。
|
||||
* 纯算法:只用 stdint/stddef/math + cfg_file.h/adc_cal.h 头(注册/直通均为
|
||||
* 接口级)→ 主机侧可单测(test/host/run_par,链真 cfg 引擎 + 真 adc_cal)。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-25(道D 卡 10)
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 DTU 项目组 创建:[run_par] 段 + 104 地址映射 + 直通
|
||||
* adc_cal + B7 式脏标志落盘请求
|
||||
*/
|
||||
|
||||
#ifndef RUN_PAR_H
|
||||
#define RUN_PAR_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 死区/覆盖点数(死区 = YC 表行数:电气 8 + 频率;覆盖 = 语义电气 8 路) */
|
||||
#define RUN_PAR_DB_N 9u
|
||||
#define RUN_PAR_CH_N 8u
|
||||
|
||||
/*
|
||||
* 函数功能:运行参数初始化——副本置工厂默认 + 注册 [run_par] 段进 cfg 引擎
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 限定条件:**内核启动前**调用(app_main_init,adc_cal_init 之后);注册必须先
|
||||
* 于 defaultTask 的 cfg_file_load(FTP 鉴权确立的契约,同 adc_cal 先例);幂等
|
||||
* 函数说明:注册失败(段名重复/段数超限)打 [ERR] 后继续——工厂默认照常生效,
|
||||
* 仅文件整定失效(adc_cal_init 同款降级)。
|
||||
*/
|
||||
void run_par_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:读第 idx 路遥测死区(工程量)
|
||||
* 入口参数:idx - 0..7 = 语义通道序(UA..IN),8 = 频率 uint8_t
|
||||
* 返 回 值:死区 V / A / Hz;idx 越界返 0.0(调用方按"无死区"保守处理) float
|
||||
* 限定条件:任意上下文(int32 单字读原子;104 COV 每拍调用)
|
||||
* 函数说明:毫单位 → 浮点的换算点(除以 1000 唯一在此,勿在消费侧另写)。
|
||||
*/
|
||||
float run_par_deadband(uint8_t idx);
|
||||
|
||||
/*
|
||||
* 函数功能:写第 idx 路死区(工程量,含范围守卫 + 脏置位)
|
||||
* 入口参数:idx - 同上 uint8_t;db - 死区工程量(V/A/Hz,≥0) float
|
||||
* 返 回 值:0 成功;-1 越界/越程/NaN(副本不动) int
|
||||
* 限定条件:**iec104Task 上下文**(104 执行段 / 卡 11 后的整定路径;UI 若整定
|
||||
* 需另行走配置引擎属主路径,勿跨任务直写)
|
||||
* 函数说明:合法域按点型分档——电压 0~50 V、电流 0~1 A、频率 0~5 Hz(上限 =
|
||||
* 额定 × 二档裕量,守卫目的是挡误写不是限工况;见 .c 常量段)。
|
||||
* 成功即置脏(落盘请求,见文件头持久化契约)。
|
||||
*/
|
||||
int run_par_deadband_set(uint8_t idx, float db);
|
||||
|
||||
/*
|
||||
* 函数功能:104 运行参数地址读(C_RD 路由,先于 prot_set 扫描)
|
||||
* 入口参数:addr - 通讯地址(0x8020~0x8058 段内) uint32_t;out - 工程量浮点
|
||||
* (死区 V/A/Hz;k 覆盖 µV/码;b 覆盖 码值) float *
|
||||
* 返 回 值:0 命中并写出;-1 地址不归本段(调用方继续走 prot_set) int
|
||||
* 限定条件:iec104Task 上下文
|
||||
* 函数说明:命中集 = 0x8030~0x8038 / 0x8040~0x8047 / 0x8048~0x804F 精确匹配;
|
||||
* k/b 读回 = adc_cal **当前生效值**(与折算同视图:文件坏值被合法带
|
||||
* 哨兵回退 ROM 时,回显的就是 ROM——主站看到的即下游实际吃到的)。
|
||||
*/
|
||||
int run_par_104_read(uint32_t addr, float *out);
|
||||
|
||||
/*
|
||||
* 函数功能:104 运行参数地址写(C_SE 两段式路由,先于 prot_set 扫描)
|
||||
* 入口参数:addr - 通讯地址 uint32_t;v - 工程量浮点 float;
|
||||
* select - 1 = 选择段(只校验不落副本)/ 0 = 执行段 uint8_t
|
||||
* 返 回 值:0 接受;-1 拒绝(越程/NaN/合法带外/非本段可写地址命中守卫);
|
||||
* -2 地址不归本段(调用方继续走 prot_set) int
|
||||
* 限定条件:iec104Task 上下文;执行段成功即置脏
|
||||
* 函数说明:k/b 覆盖经 adc_cal_set() 合法带哨兵(µV/码 ×[0.5,2.0]、|b|≤8192,
|
||||
* 越带 -1 → 104 侧 PN 否定确认,adc_cal 不留痕由**其自身**越带 [ERR]
|
||||
* 机制负责——本模块直通不重复留痕);两段式与 prot_set 定值写同构。
|
||||
*/
|
||||
int run_par_104_write(uint32_t addr, float v, uint8_t select);
|
||||
|
||||
/*
|
||||
* 函数功能:落盘脏标志探查(非消费)
|
||||
* 入口参数:无
|
||||
* 返 回 值:1 = 有已整定未落盘的改动;0 = 干净 uint8_t
|
||||
* 限定条件:任意上下文(单字节);iec104_serv 主循环 peek 后投递事件(B7 同款)
|
||||
*/
|
||||
uint8_t run_par_dirty_peek(void);
|
||||
|
||||
/*
|
||||
* 函数功能:执行落盘请求(cfg_file_save 整档外科保存 + 成功清脏)
|
||||
* 入口参数:无
|
||||
* 返 回 值:0 成功(或无脏可存);-1 写盘失败(脏保持,下轮重试) int
|
||||
* 限定条件:**defaultTask(配置引擎属主)上下文**,由 SYS_EVT_RUN_PAR_SAVE 驱动
|
||||
* 函数说明:save 走引擎唯一写闸(超长拒写/原子改名);本函数不清
|
||||
* 引擎以外模块的脏语义([adc_cal] 副本值已由写路径就位)。
|
||||
*/
|
||||
int run_par_store_save(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* RUN_PAR_H */
|
||||
@@ -1,7 +1,7 @@
|
||||
/*
|
||||
* 模块名称:采样环缓冲(L2 相量计算的输入)
|
||||
* 模块功能:按**语义通道**保存最近若干周波的采样值,供 L2 相量计算按窗取数。
|
||||
* V0.1 上 8 路电气量分属两片转换器(硬件缺陷 N-18),本层只做"按时序
|
||||
* V0.1 上 8 路电气量分属两片转换器(双转换器无共同采样触发),本层只做"按时序
|
||||
* 存/取",**不做任何跨通道运算**——同刻性由上游(L1)保证,本层不假设。
|
||||
* 容量取 **2 个周波(40 点)**,与参考系统 `CN_BUFSAM_CYCLE = 2`
|
||||
* (规划 §10.1)一致,为后续加长数据窗(1.5~2 周波,抗衰减直流,
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
| 文件 | 作用 |
|
||||
|---|---|
|
||||
| `app_main.c/h` | **应用初始化入口 `app_main_init`**:时间子系统优先 → RS485 → NAND → ADC → 诊断(在 `osKernelStart` 之前执行) |
|
||||
| `app_cfg.h` | **功能模块编译开关**(裁剪测试套件/模块用) |
|
||||
| `app_cfg.h` | **功能裁剪的决策占位说明**——2026-10-22 起**不含任何宏**(原 3 个 `APP_*_ENABLE` 零消费点,已删;接回方法见该文件修改记录 v1.2) |
|
||||
| `sys_events.h` | **系统就绪事件定义**(CMSIS-RTOS v2 Event Flags:`SYS_EVT_NET_READY` / `SYS_EVT_STORAGE_READY` / `SYS_EVT_FTP_READY` / `SYS_EVT_TIMESYNC_READY` 及命令位) |
|
||||
|
||||
## 初始化顺序(`app_main_init`)
|
||||
|
||||
+12
-33
@@ -1,47 +1,26 @@
|
||||
/*
|
||||
* 模块名称:Application Configuration
|
||||
* 模块功能:应用层全局配置宏(**预留占位,当前均未接线**)
|
||||
* 模块功能:功能裁剪开关的决策占位说明(**本头文件当前不提供任何宏,也不被任何 .c 包含**)
|
||||
* 适用平台:STM32F407ZGTx
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-07-19
|
||||
* 修改记录:
|
||||
* v1.0 2026-07-19 王建锋 创建初始版本
|
||||
* v1.1 2026-10-04 王建锋 P2-12:下列宏此前被表述为"功能模块使能开关",但全库
|
||||
* 零代码引用(grep 仅本文件有定义)——"开关名存实亡"会
|
||||
* 误导评审者以为模块可裁剪。现如实标注为**预留占位**:
|
||||
* 当前**不门控任何代码**,各模块一律编译进产品。是否/
|
||||
* 如何接线取决于产品定义(docs/任务规划/README.md §2 阶段 B0),
|
||||
* 确认后再在真实初始化点(如 app_main.c 的 rs485_init /
|
||||
* tpafe5160_init)加门控;在此之前**不要**把它们当开关用。
|
||||
* v1.1 2026-10-04 王建锋 如实标注三个 APP_*_ENABLE 为"未接线占位"(全库零消费点)
|
||||
* v1.2 2026-10-22 王建锋 收口(登记簿 C5):删除 4 个零消费点的宏
|
||||
* (APP_NET_ENABLE / APP_ADC_ENABLE / APP_RS485_ENABLE /
|
||||
* APP_SYS_TIME_SRC)。留"看起来能裁剪、其实不生效"的宏比不留更糟:
|
||||
* 评审者会误信模块可裁剪,而关掉它们不会产生任何编译期效果。
|
||||
* 需要真实门控时(产品定义 B0 落定后),在真实初始化点接入并**在本
|
||||
* 文件登记**,同时接回 App/app/app_main.c 的 include;在此之前
|
||||
* 本模块一律编译进产品(各外设的常驻事实见
|
||||
* docs/环境与部署/项目部署环境要求.md §3.2)。
|
||||
* 注意:本文件不是 App/app/README.md 里说的"编译开关",那行已同步更正。
|
||||
*/
|
||||
|
||||
#ifndef APP_CFG_H
|
||||
#define APP_CFG_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*
|
||||
* 系统时钟源选择(**预留占位,未接线**)
|
||||
* 0 - DWT CYCCNT(当前实现:console_log.c 直接使用 DWT,未读本宏)
|
||||
* 1 - TIM2(未实现)
|
||||
* 说明:本宏当前无任何消费点;实际的"与时间源无关硬上界"由 console_log.c 内联
|
||||
* 使能 DWT 实现,不经过本宏。
|
||||
*/
|
||||
#define APP_SYS_TIME_SRC 0
|
||||
|
||||
/*
|
||||
* 功能模块"预留开关"(**当前均未接线,不门控任何代码**)
|
||||
* 三个宏一律保持定义(= 模块编译进产品);它们**不产生任何编译期效果**,
|
||||
* 仅作为产品定义(B0)落定后接入真实门控的占位锚点。
|
||||
*/
|
||||
#define APP_NET_ENABLE
|
||||
#define APP_ADC_ENABLE
|
||||
#define APP_RS485_ENABLE
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
/* 故意不提供任何宏:功能裁剪当前不存在。理由与接回方法见上方修改记录 v1.2。 */
|
||||
|
||||
#endif /* APP_CFG_H */
|
||||
|
||||
+46
-14
@@ -12,7 +12,6 @@
|
||||
*/
|
||||
|
||||
#include "app_main.h"
|
||||
#include "app_cfg.h"
|
||||
|
||||
#include "main.h" /* ST_DIR4_GPIO_Port / ST_DIR4_Pin 等 CubeMX 宏 */
|
||||
#include "stm32f4xx_it.h" /* fault_handler_enable() */
|
||||
@@ -24,12 +23,15 @@
|
||||
#include "gd5f2gq5ue.h"
|
||||
#include "tpafe5160.h"
|
||||
#include "adc_input.h" /* L1 采集层:adc_input_init()(语义映射 + STM32 ADC1 扫描) */
|
||||
#include "adc_cal.h" /* L1.5 标定层:adc_cal_init()(ROM 系数 + cfg 两段注册,道A 卡2/4) */
|
||||
#include "run_par.h" /* 运行参数段:run_par_init()([run_par] 注册 + 104 地址映射真源,道D 卡10) */
|
||||
#include "iec104_comm.h" /* [comm] 段:iec104_comm_init()(ca/端口注册,道G task-8) */
|
||||
#include "phasor.h" /* L2 相量层:phasor_init()(系数表 + 实例 + 双区交换) */
|
||||
#include "freq.h" /* L2 派生量:freq_init()(相位解缠绕测频,B8) */
|
||||
#include "prot_set.h" /* L3 定值模型:prot_set_init()(B5,内核前封存) */
|
||||
#include "prot_elm.h" /* L3 元件框架:prot_elm_init()(B5,解析相量索引) */
|
||||
#include "sys_log.h"
|
||||
#include "task_watchdog.h" /* task_wd_seg_report():round 8 复位不丢"进段打点"上报 */
|
||||
#include "task_watchdog.h" /* task_wd_seg_report():第 8 轮复位不丢"进段打点"上报 */
|
||||
#include "test_config.h" /* 套件开关:身份戳的构建变体串(FW_BUILD_VARIANT)依赖本头,
|
||||
* 改 TEST_SUITE_* / ENABLE_DESTRUCTIVE_TESTS 必重编本 TU →
|
||||
* __DATE__/__TIME__ 随之刷新,见 app_main_init() 的戳注释 */
|
||||
@@ -130,7 +132,7 @@ int app_main_init(void) {
|
||||
|
||||
/* 日志引擎初始化(创建待发队列;须先于任何 syslog_post) */
|
||||
(void)syslog_mcu_init();
|
||||
/* 启动锚点(2026-10-02 S10):本函数在内核启动前执行、不创建任何 RTOS 对象,
|
||||
/* 启动锚点(2026-10-02 加):本函数在内核启动前执行、不创建任何 RTOS 对象,
|
||||
* 恒成功且无副作用;本行是"启动进度轨迹"的起点——之后的每一步都有结果日志,
|
||||
* 故某次启动卡死时,最后一行日志即可指向卡住的步骤。 */
|
||||
DBG_INFO("boot: syslog init OK");
|
||||
@@ -165,10 +167,10 @@ int app_main_init(void) {
|
||||
}
|
||||
}
|
||||
|
||||
/* 0b) round 8 取证:上报上一轮 defaultTask 主循环的**最后进段**。
|
||||
/* 0b) 第 8 轮取证:上报上一轮 defaultTask 主循环的**最后进段**。
|
||||
* 轨迹槽在 .noinit(scatter UNINIT_CCM),IWDG/引脚复位不清 SRAM →
|
||||
* 误复位后开机第一时间可见 `PREVRUN seg=N`,直接指向主循环阻塞点
|
||||
* (round 7 观察"判定分支未执行"经 round 8b 更正:真因是 IWDG 超时被静默
|
||||
* (第 7 轮观察"判定分支未执行"经第 8b 轮更正:真因是 IWDG 超时被静默
|
||||
* 配成复位默认值,见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)。
|
||||
* 裸 UART 直发,且一次复位只报一次(内部清 magic)。 */
|
||||
task_wd_seg_report();
|
||||
@@ -177,14 +179,14 @@ int app_main_init(void) {
|
||||
* 1) 时间子系统初始化(尽早完成,后续所有日志带时间戳)
|
||||
* sd2506_init() 从 RTC 读取初始时间
|
||||
* sys_clock_init() 将 RTC 时间转为 Unix epoch,建立墙钟基准
|
||||
* S10(2026-10-02):sd2506_init 原先所有失败路径静默返回,是启动段
|
||||
* 启动段可观测性(2026-10-02 加):sd2506_init 原先所有失败路径静默返回,是启动段
|
||||
* 唯一无日志的初始化;现内部已逐路径打 DBG_ERROR,此处再补一行
|
||||
* 带错误码的结果回执,使 I2C 不通时能看到"在哪一步、什么错"。
|
||||
*/
|
||||
{
|
||||
int rtc_ret = sd2506_init();
|
||||
bsp_err_t rtc_ret = sd2506_init();
|
||||
DBG_INFO("SD2506 init: %s (ret=%d)",
|
||||
(rtc_ret == SD2506_OK) ? "OK" : "FAILED", rtc_ret);
|
||||
(rtc_ret == BSP_OK) ? "OK" : "FAILED", rtc_ret);
|
||||
}
|
||||
if (sys_clock_init() != 0) {
|
||||
syslog_post(SYSLOG_SELFTEST, "clock chip fault");
|
||||
@@ -199,7 +201,7 @@ int app_main_init(void) {
|
||||
DBG_INFO("NAND init: %s",
|
||||
(ret == GD5F_OK) ? "OK" :
|
||||
(ret == GD5F_ERROR) ? "ERROR" :
|
||||
(ret == GD5F_BUSY_TIMEOUT) ? "BUSY TIMEOUT" :
|
||||
(ret == GD5F_WAIT_EXPIRED) ? "BUSY TIMEOUT" :
|
||||
(ret == GD5F_ECC_ERROR) ? "ECC ERROR" :
|
||||
(ret == GD5F_PROGRAM_FAIL) ? "PROGRAM FAIL" :
|
||||
(ret == GD5F_ERASE_FAIL) ? "ERASE FAIL" :
|
||||
@@ -209,10 +211,12 @@ int app_main_init(void) {
|
||||
}
|
||||
|
||||
/* 4) TPAFE5160 ADC 测试 */
|
||||
int tp_ret = tpafe5160_init();
|
||||
bsp_err_t tp_ret = tpafe5160_init();
|
||||
DBG_INFO("ADC init: %s",
|
||||
(tp_ret == TPAFE5160_OK) ? "OK" : "TIMEOUT");
|
||||
if (tp_ret != TPAFE5160_OK) {
|
||||
(tp_ret == BSP_OK) ? "OK" :
|
||||
(tp_ret == BSP_ERR_TIMEOUT) ? "TIMEOUT" :
|
||||
(tp_ret == BSP_ERR_NO_DEVICE) ? "NO CHIP" : "FAIL");
|
||||
if (tp_ret != BSP_OK) {
|
||||
syslog_post(SYSLOG_SELFTEST, "ADC fault");
|
||||
}
|
||||
|
||||
@@ -249,6 +253,34 @@ int app_main_init(void) {
|
||||
(void)freq_init();
|
||||
DBG_INFO("freq layer init: OK");
|
||||
|
||||
/* 4c-3) 通道标定层(L1.5,「数据可信」卡 2/卡 4)初始化:RAM 系数副本置
|
||||
* ROM 默认(卡 1 定死的引脚侧 LSB,docs/硬件平台/ADC通道映射说明.md §7.1)
|
||||
* + 向配置引擎注册 [adc_cal] / [adc_cal_ofs] 两段。注册必须先于
|
||||
* defaultTask 的 cfg_file_load(「先注册后 load」契约)→ 只能落内核前;
|
||||
* 折算消费在 adcTask 每拍(adc_cal_apply),标定文件值随 load/poll 原地
|
||||
* 生效,无回调。 */
|
||||
adc_cal_init();
|
||||
DBG_INFO("adc cal init: OK (k_q16 tpafe=%ld stm32=%ld)",
|
||||
(long)ADC_CAL_K_Q16_TPAFE, (long)ADC_CAL_K_Q16_STM32);
|
||||
|
||||
/* 4c2) 运行参数段初始化(「数据可信」卡 10):死区 9 点工厂默认 + 注册
|
||||
* [run_par] 段。同 adc_cal 的「先注册后 load」契约 → 必须内核前;
|
||||
* 104 消费(COV 实时读 / 0x8020 段读写映射)在 iec104Task 启动后进行。 */
|
||||
run_par_init();
|
||||
DBG_INFO("run par init: OK (db9 k/b via adc_cal, evt=1<<8)");
|
||||
|
||||
/* 4c3) 通讯参数段初始化(道G task-8,「UI 补齐」第 5 项非 UI 前置):ca/端口
|
||||
* 工厂默认 + 注册 [comm] 段。与 adc_cal / run_par 同「先注册后 load」契约
|
||||
* → 必须内核前:defaultTask 在 storage_bring_up 里才做 net_cfg_load →
|
||||
* cfg_file_load(引擎唯一读点),本注册必须排在它前面。生效语义 =
|
||||
* 改后重启(iec104Task 启动读 getter,不挂回调,见 iec104_comm.h)。 */
|
||||
if (iec104_comm_init() != 0) {
|
||||
DBG_ERROR("comm init FAIL (dup/full?) - file values inactive");
|
||||
} else {
|
||||
DBG_INFO("comm init: OK (ca=%u port=%u)",
|
||||
(unsigned)iec104_comm_ca(), (unsigned)iec104_comm_port());
|
||||
}
|
||||
|
||||
/* 4d) 保护定值 + 元件层初始化(B5):出厂默认定值封存校验和 + 元件状态
|
||||
* 复位并解析相量索引(须在 phasor_init 之后)。内核前完成 → iec104
|
||||
* 定值读/protectTask 首拍前定值必已装载。判据消费在 protectTask。 */
|
||||
@@ -289,8 +321,8 @@ int app_main_init(void) {
|
||||
/* 6) 启动 1 kHz 采样节拍:TIM3 更新事件 → ① ISR 内脉冲 CONVST(Src/main.c)
|
||||
* ② 同一事件经 TRGO 硬件触发 STM32 ADC1 一轮 9 通道转换(DMA 自动搬运)。
|
||||
* ⚠ 必须在 tpafe5160_init() 成功之后启动,否则 ISR 会在驱动首帧自检
|
||||
* (E-44 有效性探针)期间乱发 CONVST。消费端 adcTask 退化为纯消费者。
|
||||
* ⚠ 节拍源 v2.0 由 TIM6 改为 **TIM3**:F4 上 ADC1 的外部触发只认
|
||||
* (有效性探针)期间乱发 CONVST。消费端 adcTask 退化为纯消费者。
|
||||
* ⚠ 节拍源必须能触发 ADC1,故用 **TIM3**:F4 上 ADC1 的外部触发只认
|
||||
* TIM1/TIM2/TIM3/TIM4/TIM5/TIM8 的 CC 或 TRGO,**TIM6/TIM7 的 TRGO 只接
|
||||
* DAC**(查 CubeMX DB 与本仓库 stm32f4xx_hal_adc.h 的 EXTSEL 表证实)
|
||||
* → 要让两片转换器由同一个硬件事件驱动,节拍源必须换成 TIM3。 */
|
||||
|
||||
@@ -17,6 +17,14 @@ extern "C" {
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/*
|
||||
* 复位原因锁存影子(定义于 app_main.c)。Src/main.c 在 HAL_Init() 之后、串口/日志
|
||||
* 尚未就绪时读 RCC->CSR 并清标志,这里提供引用契约——原先写成 main.c 函数内的局部
|
||||
* extern(跨文件裸 extern,改名/删除都不会有提示)。读→清两步的顺序约束见 main.c
|
||||
* 该段注释,不可调换。
|
||||
*/
|
||||
extern volatile uint32_t g_rcc_csr_latched;
|
||||
|
||||
int app_main_init(void);
|
||||
void app_main_run(void);
|
||||
|
||||
|
||||
@@ -34,6 +34,10 @@ extern osEventFlagsId_t sys_evtHandle;
|
||||
#define SYS_EVT_PROT_SET_SAVE (1u << 7) /* 保护定值落盘请求(B7):iec104Task 检测到定值脏置位,
|
||||
* defaultTask(配置引擎属主)消费并写 /prot_set.bin
|
||||
* (消费型命令位;写盘失败脏标志保留 → 下轮再通知重试) */
|
||||
#define SYS_EVT_RUN_PAR_SAVE (1u << 8) /* 运行参数段落盘请求(「数据可信」卡 10):iec104Task
|
||||
* 检测到 run_par 脏(死区 9 键 / 系数覆盖联动 [adc_cal])
|
||||
* 置位,defaultTask 消费并 cfg_file_save 整档外科保存
|
||||
* (消费型命令位;失败脏保留 → 下轮重试,B7 同款) */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
@@ -0,0 +1,618 @@
|
||||
/*
|
||||
* 模块名称:UI 数据组装器(ui_data_feed)
|
||||
* 模块功能:App → UI 数据通道的写侧——按节拍从采集/标定/相量/测频/保护/协议/墙钟
|
||||
* 各模块取数,组装成 docs/UI设计/UI_数据供给设计说明.md §4 契约的三帧
|
||||
* (实时量 rt / 参数 par / 装置信息 info),经 Drivers/BSP/UI/ui_data.h
|
||||
* 的发布 API 推给 UI 层;本模块是快照的**唯一填充者**,不含任何布局、
|
||||
* 格式化与 LVGL 调用(显示规格全在页面行表,同文档 §8)
|
||||
* 写方向(道F task-1):同一条 tick 顺带把页面投递的定值整定请求落定——
|
||||
* ui_data_param_drain() 每轮取一条,执行动作经本文件的 s_param_ops
|
||||
* (locked = prot_set_locked / check = g_prot_set_meta 上下限 /
|
||||
* apply = prot_set_set_sealed)注入给 BSP;值域与落盘事实都留在 App
|
||||
* 侧,BSP 只有队列与状态机(契约 §6.1"UI 不能直写"的落地形态)
|
||||
* 量纲诚实性:eng 字段按**真值**填——docs/任务规划/数据可信.md §2.6 把承诺边界定在
|
||||
* "引脚电压",故未做两点标定前 eng = UI_DATA_ENG_PIN(值 = ADC 引脚电压 V,
|
||||
* 不是端子二次值);电气 8 路 adc_cal_flags() 的 VALID 全置位才升
|
||||
* UI_DATA_ENG_SEC。TEMP 按决策不参与线性标定 → 恒带 UI_DATA_Q_RAW,
|
||||
* 页面据此不挂单位(三态口径同文档 §4.2)
|
||||
* 适用平台:STM32F407ZGTx(PC 模拟器不链接本文件,见 ui_data_feed.h 端接法)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-25
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 王建锋 创建(道E task-14)
|
||||
* v1.1 2026-10-26 DTU 项目组 道F task-1:① tick 前置 ui_data_param_drain()
|
||||
* (定值写路径出队 → 闭锁/越界预检 → set_sealed,
|
||||
* 见 s_param_ops);② 参数帧尾填 cal_block(未标定
|
||||
* 闭锁徽标,三路口径同 protect_task.c,不用 eng)
|
||||
* v1.2 2026-10-26 DTU 项目组 道F task-16:删除本地镜像 feed_cal_block(),
|
||||
* 两个调用点(填帧 / 门控比较)改调
|
||||
* adc_cal_prot_blocked()——同一份三路判定此前在
|
||||
* 本文件与 protect_task.c 各写一遍,且各自带一份
|
||||
* #ifndef DTU_BENCH(只给单个 TU 加宏就能让屏与判据
|
||||
* 分叉);真源现落在 App/algo/adc_cal.c,旁路全仓一处。
|
||||
* 行为不变(判定式逐字同构、调用时机不变)
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
#include <string.h>
|
||||
|
||||
#include "cmsis_os.h" /* osKernelGetTickCount:与 adc_task 同一 ms 时基 */
|
||||
|
||||
#include "ui_data_feed.h"
|
||||
|
||||
/* 日志锚点:本文件只在"点表点数与快照点数不等"这一条静默漂移上打一条一次性日志
|
||||
* (每条约 90 B Flash,见 AGENTS「日志不是免费的」——常规发布路径刻意一条都不加)。
|
||||
* ⚠ DBG_TAG 必须在 include 之前定义:dbg_log.h 的 DBG_ERROR 是宏,漏 include 时
|
||||
* ARMCC 只把 DBG_ERROR 当隐式声明函数放过编译期,错误落在链接期(L6218E
|
||||
* Undefined symbol),比漏写头文件本身更难归因 */
|
||||
#define DBG_TAG "[UIDATA]"
|
||||
#include "dbg_log.h"
|
||||
|
||||
/* 下层(Drivers/BSP):快照存侧 + 两个端口/用户名常量来源(App → BSP 是合法方向) */
|
||||
#include "ui_data.h"
|
||||
#include "ui_net_sink.h" /* UI_NET_SINK_PORT:远程显示(RemoDispBus)监听端口 */
|
||||
#include "ftp_cfg.h" /* FTP_CFG_USER_MAX / ftp_cfg_get() */
|
||||
|
||||
/* 同层(App)数据源 */
|
||||
#include "adc_input.h" /* ADC_SEM_COUNT / adc_input_raw(TEMP 码值) */
|
||||
#include "adc_cal.h" /* adc_cal_flags:VALID/CLIPPED 三态可用性;adc_cal_prot_blocked:未标定闭锁单点 */
|
||||
#include "phasor.h" /* 基波/谐波有效值(fAmSQ) */
|
||||
#include "freq.h" /* 测频读数与新鲜度 */
|
||||
#include "prot_elm.h" /* 元件动作态(遥信位图来源) */
|
||||
#include "prot_set.h" /* 40 项定值 Q10 + 校验和 + 闭锁 + 越界位图 */
|
||||
#include "sys_clock.h" /* 墙钟字符串 */
|
||||
#include "iec104_serv.h" /* IEC104_SERV_PORT:实际监听端口(非协议层同名常量) */
|
||||
#include "iec104_pts.h" /* task-15 批4:遥信点数与单/双点性质(点表事实的唯一来源) */
|
||||
|
||||
/* 私有宏定义区 */
|
||||
|
||||
/* 实时帧与信息帧发布节拍(ms);参数帧只在定值校验和变化时发布
|
||||
* (节拍依据 docs/UI设计/UI_数据供给设计说明.md §5.2) */
|
||||
#define FEED_RT_PERIOD_MS 200u
|
||||
|
||||
/* 遥信位号:**与 App/protocol/iec104/iec104_pts.c 板端源的位图同源**(该文件把
|
||||
* 位号写死在函数体内、未导出,UI 侧不得 include 协议头,故在此镜像一份)。
|
||||
* 变更纪律:104 点表位号改动时必须同步这里 + 页面名表 + 主机一致性测试
|
||||
* (test/host/ui_data 的位图断言)。V0.1 有硬件来源的只有以下 6 位,
|
||||
* 其余 27 位恒无效(开入未实现);过压/欠压/过频/低频四个元件在
|
||||
* V0.1 点表里没有对应遥信点,故本函数不产出。 */
|
||||
#define FEED_YX_BIT_FAULT_ALL 14u /* YX15 故障总(任一电压/电流段或零序动作) */
|
||||
#define FEED_YX_BIT_OC1 15u /* YX16 过流 I 段 */
|
||||
#define FEED_YX_BIT_OC2 16u /* YX17 过流 II 段 */
|
||||
#define FEED_YX_BIT_OC3 17u /* YX18 过流 III 段 */
|
||||
#define FEED_YX_BIT_OL 22u /* YX23 过载(过负荷) */
|
||||
#define FEED_YX_BIT_LOCK 30u /* YX31 保护闭锁(定值校验和自检失败) */
|
||||
/* YX1~4(bit0..3)是开关位置类双点点,V0.1 无开入硬件 → 恒 0 且不在有效位图内,
|
||||
* 因此本文件不为它们定义常量(定义了也不参与任何判定 = 看着像接了线其实没有) */
|
||||
/* 有效位图:YX15~18/23/31(与 104 点表 YX_VALID_MASK 同集合) */
|
||||
#define FEED_YX_VALID_MASK ((((uint64_t)1u << FEED_YX_BIT_FAULT_ALL) | \
|
||||
((uint64_t)1u << FEED_YX_BIT_OC1) | \
|
||||
((uint64_t)1u << FEED_YX_BIT_OC2) | \
|
||||
((uint64_t)1u << FEED_YX_BIT_OC3) | \
|
||||
((uint64_t)1u << FEED_YX_BIT_OL) | \
|
||||
((uint64_t)1u << FEED_YX_BIT_LOCK)))
|
||||
|
||||
/*
|
||||
* 装置身份(版本类):仓库内**没有**产品级版本登记表,值沿用页面既有显示值
|
||||
* (docs/UI设计/内容页架构.md 的装置信息行表现值),有正式口径后只改本段一处。
|
||||
* ⚠ 软件校验不抄参考项目遗留的 "4454":构建期没有 CRC 生成步骤,如实显示 "--"
|
||||
* (处置口径 docs/UI设计/页面扩充详细方案.md §3.7 第 5 行)
|
||||
*/
|
||||
#define FEED_MODEL "F30"
|
||||
#define FEED_SW_VER "SV0.010"
|
||||
#define FEED_HW_VER "HW0.010"
|
||||
#define FEED_SW_SUM "--"
|
||||
|
||||
/* 编译期一致性守卫(ARMCC C99 无 _Static_assert,用项目惯用的数组宽度法,
|
||||
* 先例 App/cfg/cfg_file.c 与 Drivers/BSP/UI/ui_menu_tab.h):
|
||||
* 快照下标空间与各 App 枚举一旦漂移,编译即失败,不留到运行期
|
||||
* (契约 docs/UI设计/UI_数据供给设计说明.md §9.4 第 1/2 条) */
|
||||
typedef char feed_chk_yc_n_t[(UI_DATA_YC_N == (uint32_t)ADC_SEM_COUNT) ? 1 : -1];
|
||||
typedef char feed_chk_set_n_t[(UI_DATA_SET_N == (uint32_t)PS_ID_COUNT) ? 1 : -1];
|
||||
/* 写路径注入表的 id 形参是 uint8_t(BSP 侧不透明编号宽度,位图只有 64 位):
|
||||
* 定值项数一旦破 64/255,request 的 mask 与 check/apply 的编号都会静默截断,
|
||||
* 故在写侧也钉一道(BSP 侧同一条守卫在 Drivers/BSP/UI/ui_data.c) */
|
||||
typedef char feed_chk_set_u8_t[((uint32_t)PS_ID_COUNT <= 64u) ? 1 : -1];
|
||||
typedef char feed_chk_fuser_t[(UI_DATA_FUSER_LEN >= FTP_CFG_USER_MAX) ? 1 : -1];
|
||||
|
||||
/* 静态变量定义区 */
|
||||
|
||||
/* 组装缓冲:三帧串行填充,共用一块存储,省两份 RAM。
|
||||
* 实际最大者是**实时帧**(208 B > par 192 B > info),故给 par 尾追加字段
|
||||
* 只要不超过 rt 就一字节 RAM 都不多花(这条由主机单测 TC-UIDATA-021g/h 钉住,
|
||||
* 尺寸一变这里就是唯一会撒谎的地方) */
|
||||
static union {
|
||||
ui_data_rt_t rt;
|
||||
ui_data_par_t par;
|
||||
ui_data_info_t info;
|
||||
} s_scr;
|
||||
|
||||
/* 语义通道 → 基波相量实例下标缓存(-1 = 该通道无基波实例 → 该路发 IV);
|
||||
* 首帧惰性建表(相量实例表是 ROM 表,运行期不变,建一次即可) */
|
||||
static int8_t s_yc_phasor[ADC_SEM_COUNT];
|
||||
static uint8_t s_map_done;
|
||||
|
||||
/* 发布节拍与参数变化检测状态
|
||||
* par 帧有**三个各自独立**的变化源,任一变了就要重发一帧(详见 tick 里的门控注释):
|
||||
* s_par_sum 定值改动(校验和)
|
||||
* s_par_locked 定值闭锁(L5-1 自检失配,校验和不动)
|
||||
* s_par_cal_block 未标定闭锁(卡 12 标定完成,校验和也不动)
|
||||
* 后两个各占一字节——省下来就是"屏上状态与真值脱拍"这笔债 */
|
||||
static uint32_t s_last_rt_ms;
|
||||
static uint32_t s_par_sum;
|
||||
static uint8_t s_par_sum_valid;
|
||||
static uint8_t s_par_locked;
|
||||
static uint8_t s_par_cal_block;
|
||||
static uint8_t s_first_done;
|
||||
|
||||
/* 函数定义区 */
|
||||
|
||||
/*
|
||||
* 函数功能:定长缓冲内拷贝字符串(跨层快照只允许定长数组,禁指针)
|
||||
* 入口参数:p_dst - 目标缓冲(结构体内数组首地址) char *
|
||||
* dst_len - 目标容量(含 NUL) size_t
|
||||
* a_src - 源串(Flash 字面量或上层缓冲) const char *
|
||||
* 返回值:无
|
||||
* 限定条件:三者非 NULL 且 dst_len > 0;截断时保证 NUL 结尾
|
||||
* 函数说明:不用 strcpy/sprintf(门禁 CODE-RULE-06 禁用不安全函数),
|
||||
* 超长**静默截断**——快照字段本就是定长显示串,截断只会少显示尾部,
|
||||
* 不会越界;本函数每 200 ms 调用个位数次,不为此加日志
|
||||
*/
|
||||
static void feed_copy_str(char *p_dst, size_t dst_len, const char *a_src) {
|
||||
size_t n;
|
||||
|
||||
if ((p_dst == NULL) || (a_src == NULL) || (dst_len == 0u)) {
|
||||
return;
|
||||
}
|
||||
n = strlen(a_src);
|
||||
if (n > (dst_len - 1u)) {
|
||||
n = dst_len - 1u;
|
||||
}
|
||||
(void)memcpy(p_dst, a_src, n);
|
||||
p_dst[n] = '\0';
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:建立"语义通道 → 基波相量实例"映射(首帧一次)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:uiTask 上下文,ui_data_feed_tick 首帧前调用
|
||||
* 函数说明:只认 harm == 1 的实例;一个语义通道有多个基波实例时取**首个**
|
||||
* (现状 k_inst[] 每通道最多一个基波,取首个即唯一解)。
|
||||
* UB/UC/IB/IC/IN 无实例(V0.1 硬件削顶:双转换器无共同采样触发)→ 留 -1,该路发 IV
|
||||
*/
|
||||
static void feed_map_phasor(void) {
|
||||
uint8_t n;
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0u; i < (uint8_t)ADC_SEM_COUNT; i++) {
|
||||
s_yc_phasor[i] = -1;
|
||||
}
|
||||
n = phasor_inst_count();
|
||||
for (i = 0u; i < n; i++) {
|
||||
const phasor_inst_t *p = phasor_inst(i);
|
||||
|
||||
if ((p == NULL) || (p->harm != 1u) || ((uint8_t)p->sem >= (uint8_t)ADC_SEM_COUNT)) {
|
||||
continue;
|
||||
}
|
||||
if (s_yc_phasor[(uint8_t)p->sem] < 0) {
|
||||
s_yc_phasor[(uint8_t)p->sem] = (int8_t)i;
|
||||
}
|
||||
}
|
||||
s_map_done = 1u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:算整帧量纲口径 eng
|
||||
* 入口参数:无
|
||||
* 返回值:UI_DATA_ENG_PIN 或 UI_DATA_ENG_SEC uint8_t
|
||||
* 限定条件:无(纯读)
|
||||
* 函数说明:**全或无**——电气 8 路(TEMP 除外,它不参与线性标定)的
|
||||
* ADC_CAL_F_VALID 全置位才升二次值。取全或无是因为整帧共用一个 eng:
|
||||
* 只要还有一路是引脚电压,页面挂"二次值"就会把那一路的读数误标
|
||||
* (宁可整体显示"未标定",也不给部分真值贴真值标签)
|
||||
*/
|
||||
static uint8_t feed_eng(void) {
|
||||
uint8_t sem;
|
||||
|
||||
for (sem = 0u; sem < (uint8_t)ADC_SEM_TEMP; sem++) {
|
||||
if ((adc_cal_flags((adc_semantic_t)sem) & ADC_CAL_F_VALID) == 0u) {
|
||||
return UI_DATA_ENG_PIN;
|
||||
}
|
||||
}
|
||||
return UI_DATA_ENG_SEC;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:组装遥信双点位图(bit i = 第 i 点是双点型 M_DP)
|
||||
* 入口参数:无
|
||||
* 返回值:位图 uint64_t
|
||||
* 限定条件:uiTask 上下文;点数上限 UI_DATA_YX_N(<64,故位图放得下)
|
||||
* 函数说明:逐点问点表 `iec104_pts_yx_is_dp()`,**不在此复制"前 4 点是双点"这个事实**
|
||||
* ——那句话写在这里就是第二真相,点表一改就悄悄错(下发位图的理由见
|
||||
* ui_data.h 的 yx_dp 字段注释)
|
||||
*/
|
||||
static uint64_t feed_yx_dp(void) {
|
||||
uint64_t m = 0u;
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0u; (i < (uint8_t)UI_DATA_YX_N) && (i < 64u); i++) {
|
||||
if (iec104_pts_yx_is_dp(i) != 0u) {
|
||||
m |= ((uint64_t)1u << i);
|
||||
}
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:组装遥信位图(bit i = 第 i 点"合/动作")
|
||||
* 入口参数:无
|
||||
* 返回值:位图 uint64_t
|
||||
* 限定条件:uiTask 上下文(读 protectTask 写的元件状态快照,读侧拷贝语义,
|
||||
* 与 App/protocol/iec104/iec104_pts.c 板端源同形)
|
||||
* 函数说明:位号镜像本文件 FEED_YX_BIT_* 一组宏的注释(含同步纪律);
|
||||
* 双点遥信 YX1~4 无硬件 → 恒 0 且不在有效位图内
|
||||
*/
|
||||
static uint64_t feed_yx_bits(void) {
|
||||
prot_elm_state_t st;
|
||||
uint64_t bits = 0u;
|
||||
uint8_t fault_all;
|
||||
|
||||
fault_all = 0u;
|
||||
prot_elm_state(PE_OC1, &st);
|
||||
if (st.asserted != 0u) {
|
||||
bits |= ((uint64_t)1u << FEED_YX_BIT_OC1);
|
||||
fault_all = 1u;
|
||||
}
|
||||
prot_elm_state(PE_OC2, &st);
|
||||
if (st.asserted != 0u) {
|
||||
bits |= ((uint64_t)1u << FEED_YX_BIT_OC2);
|
||||
fault_all = 1u;
|
||||
}
|
||||
prot_elm_state(PE_OC3, &st);
|
||||
if (st.asserted != 0u) {
|
||||
bits |= ((uint64_t)1u << FEED_YX_BIT_OC3);
|
||||
fault_all = 1u;
|
||||
}
|
||||
prot_elm_state(PE_3U0, &st);
|
||||
if (st.asserted != 0u) {
|
||||
fault_all = 1u;
|
||||
}
|
||||
if (fault_all != 0u) {
|
||||
bits |= ((uint64_t)1u << FEED_YX_BIT_FAULT_ALL);
|
||||
}
|
||||
prot_elm_state(PE_OL, &st);
|
||||
if (st.asserted != 0u) {
|
||||
bits |= ((uint64_t)1u << FEED_YX_BIT_OL);
|
||||
}
|
||||
if (prot_set_locked() != 0u) {
|
||||
bits |= ((uint64_t)1u << FEED_YX_BIT_LOCK);
|
||||
}
|
||||
return bits;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:组装实时量快照(遥测 9 路 + 相量实例槽 + 遥信 + 频率 + 墙钟)
|
||||
* 入口参数:p_snap - 目标帧(调用方已清零) ui_data_rt_t *
|
||||
* tick_ms - 发布时刻(tick 域) uint32_t
|
||||
* 返回值:无
|
||||
* 限定条件:uiTask 上下文;映射表已建(feed_map_phasor)
|
||||
* 函数说明:取数口径与 104 上送同源不同路——本函数读**相量使用区**(1 ms 刷新、
|
||||
* 双区原子交换),不做死区与平均;遥测链快照(docs/任务规划/数据可信.md
|
||||
* §4.5 的 meas 快照)落地后,本函数改成读该快照即可,页面零改动
|
||||
* (这正是本层存在的意义:换数据源只改这里)
|
||||
*/
|
||||
static void feed_fill_rt(ui_data_rt_t *p_snap, uint32_t tick_ms) {
|
||||
uint32_t now_ep;
|
||||
uint8_t sem;
|
||||
char ts[UI_DATA_CLOCK_LEN];
|
||||
|
||||
p_snap->tick_ms = tick_ms;
|
||||
p_snap->valid = 1u;
|
||||
p_snap->eng = feed_eng();
|
||||
|
||||
/* 遥测:语义通道序的基波有效值(采样环已按 adc_cal 折算 → 当前为引脚电压域) */
|
||||
for (sem = 0u; sem < (uint8_t)ADC_SEM_COUNT; sem++) {
|
||||
int8_t pi = s_yc_phasor[sem];
|
||||
|
||||
p_snap->yc[sem].val = 0.0f;
|
||||
p_snap->yc[sem].qds = 0u;
|
||||
|
||||
if (sem == (uint8_t)ADC_SEM_TEMP) {
|
||||
/* 温度:不参与线性标定,透传转换器码值并打 RAW 位(页面不得挂单位) */
|
||||
int32_t code = 0;
|
||||
|
||||
p_snap->yc[sem].qds = UI_DATA_Q_RAW;
|
||||
if (adc_input_raw((adc_semantic_t)sem, &code) == 0) {
|
||||
p_snap->yc[sem].val = (float)code;
|
||||
} else {
|
||||
p_snap->yc[sem].qds |= UI_DATA_Q_IV;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (pi < 0) {
|
||||
/* 无基波实例 = V0.1 该通道无可用采样源(削顶) → 无效 */
|
||||
p_snap->yc[sem].qds = UI_DATA_Q_IV;
|
||||
} else {
|
||||
phasor_t ph;
|
||||
int rc = phasor_get((uint8_t)pi, &ph);
|
||||
|
||||
if (rc == 0) {
|
||||
p_snap->yc[sem].val = ph.fAmSQ; /* 有效值口径(系数已折 sqrt2/N) */
|
||||
} else if (rc == -2) {
|
||||
p_snap->yc[sem].qds = UI_DATA_Q_NT; /* 未攒满一周波 */
|
||||
} else {
|
||||
p_snap->yc[sem].qds = UI_DATA_Q_IV;
|
||||
}
|
||||
}
|
||||
if ((adc_cal_flags((adc_semantic_t)sem) & ADC_CAL_F_CLIPPED) != 0u) {
|
||||
p_snap->yc[sem].qds |= UI_DATA_Q_OV; /* 硬件削顶:交流量不可信 */
|
||||
}
|
||||
}
|
||||
|
||||
/* 相量实例槽:含谐波(Ia 二次/五次),交流量页的 Ia2/Ia5 行走这条带 */
|
||||
{
|
||||
uint8_t n = phasor_inst_count();
|
||||
|
||||
if (n > UI_DATA_PH_MAX) {
|
||||
n = UI_DATA_PH_MAX; /* 超出槽上限的实例本帧不发布(现 5 实例,余 3 槽) */
|
||||
}
|
||||
p_snap->ph_n = n;
|
||||
for (sem = 0u; sem < UI_DATA_PH_MAX; sem++) {
|
||||
p_snap->ph[sem].val = 0.0f;
|
||||
p_snap->ph[sem].qds = UI_DATA_Q_IV;
|
||||
}
|
||||
for (sem = 0u; sem < n; sem++) {
|
||||
phasor_t ph;
|
||||
|
||||
if (phasor_get(sem, &ph) == 0) {
|
||||
p_snap->ph[sem].val = ph.fAmSQ;
|
||||
p_snap->ph[sem].qds = 0u;
|
||||
} else {
|
||||
p_snap->ph[sem].qds = UI_DATA_Q_NT;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p_snap->yx_bits = feed_yx_bits();
|
||||
p_snap->yx_valid = FEED_YX_VALID_MASK;
|
||||
p_snap->yx_dp = feed_yx_dp();
|
||||
/* 点数漂移检查(只在真漂走时打一条日志):点表 YX_N 与本层 UI_DATA_YX_N 一旦不等,
|
||||
* 页面按 33 行画、协议按另一个数上送,两边都"看起来正常"——这是静默错,值得花一条
|
||||
* DBG(约 90 B Flash)换一次现场可见的自证;只打一次不刷屏(同丢弃计数口径) */
|
||||
{
|
||||
static uint8_t s_yx_n_bad;
|
||||
uint8_t n = iec104_pts_yx_count();
|
||||
|
||||
if ((n != (uint8_t)UI_DATA_YX_N) && (s_yx_n_bad == 0u)) {
|
||||
s_yx_n_bad = 1u;
|
||||
DBG_ERROR("yx point count drift: pts=%u ui_name_table=%u",
|
||||
(unsigned)n, (unsigned)UI_DATA_YX_N);
|
||||
}
|
||||
}
|
||||
|
||||
/* 测频:读数新鲜度单独走 freq_valid()(幅值门 + 收敛),两者任一不成立
|
||||
* 页面即显示 "--"——eng=0 期间幅值门恒失效,读数本就不可消费,如实反映 */
|
||||
{
|
||||
float hz = 0.0f;
|
||||
|
||||
p_snap->freq_valid = 0u;
|
||||
p_snap->freq_hz = 0.0f;
|
||||
if (freq_get(&hz) == 0) {
|
||||
p_snap->freq_hz = hz;
|
||||
if (freq_valid() != 0u) {
|
||||
p_snap->freq_valid = 1u;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 墙钟:UINT32_MAX 是 sys_clock_get() 的失败哨兵 → clock_ok=0、串清空 */
|
||||
p_snap->clock_ok = 0u;
|
||||
p_snap->clock[0] = '\0';
|
||||
now_ep = sys_clock_get();
|
||||
if ((now_ep != UINT32_MAX) && (sys_clock_get_str(ts, sizeof(ts)) != NULL)) {
|
||||
feed_copy_str(p_snap->clock, sizeof(p_snap->clock), ts);
|
||||
p_snap->clock_ok = 1u;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── 写路径(契约 §6.2):把 prot_set 的三个动作包成注入给 BSP 的函数表 ────── */
|
||||
|
||||
/*
|
||||
* 函数功能:写路径判定"定值闭锁中吗"(BSP 经 ui_data_param_ops_t 来问)
|
||||
* 入口参数:无
|
||||
* 返 回 值:1 - 闭锁(定值校验和自检失败态,禁一切定值写入);0 - 可写 uint8_t
|
||||
* 限定条件:uiTask 上下文(只读 prot_set 状态位)
|
||||
* 函数说明:这里的"闭锁"是**定值闭锁**(= par.locked = YX31 遥信),**不是**未标定
|
||||
* 闭锁(= par.cal_block):后者仍可改定值、只是判据停走,两者在屏上不得
|
||||
* 互认(三词三源口径 docs/任务规划/数据可信.md §4.3 + 落地简报 §7-6)
|
||||
*/
|
||||
static uint8_t feed_param_locked(void) {
|
||||
return prot_set_locked();
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:写路径整批预检——单项是否落在 meta 上下限内
|
||||
* 入口参数:a_id - 定值项编号(位图位序;uint8_t 是 BSP 侧不透明编号的宽度)
|
||||
* a_q10 - 待写 Q10 定点值
|
||||
* 返 回 值:0 - 合法;-1 - 编号越界或值越界 int
|
||||
* 限定条件:只读 g_prot_set_meta(ROM 表),任意上下文安全
|
||||
* 函数说明:上下限**只此一处取**——BSP 行表(Drivers/BSP/UI/ui_w_setview.c)是它的
|
||||
* 换算镜像,由 setview-check 钩子核一致;本函数再抄一份数字就是第二真相
|
||||
* ("查看合法、整定越界"两套边界就是这么来的)
|
||||
*/
|
||||
static int feed_param_check(uint8_t a_id, int32_t a_q10) {
|
||||
if (a_id >= (uint8_t)PS_ID_COUNT) {
|
||||
return -1;
|
||||
}
|
||||
if ((a_q10 < g_prot_set_meta[a_id].min) || (a_q10 > g_prot_set_meta[a_id].max)) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:写路径落值——单项**原子**写入 + 重封校验和 + 置脏
|
||||
* 入口参数:a_id - 定值项编号;a_q10 - Q10 定点值
|
||||
* 返 回 值:0 - 已写入;-1 - 编号越界或 prot_set 拒写(原值不动) int
|
||||
* 限定条件:uiTask 上下文;调用前 BSP 已整批预检通过(两条件与本函数一致)
|
||||
* 函数说明:用 prot_set_set_sealed()(影子拷贝→改→重封→单字交换,**每项自带置脏**,
|
||||
* 落盘由 defaultTask 50 ms 圈扫脏写 /prot_set.bin,本模块不碰 FatFs)。
|
||||
* ⚠ 不得改成 prot_set_set() + 批末 prot_set_seal():两条路径之间有
|
||||
* "已改未封存"窗口,prot_set.c 明令不得交错,撞上就被 L5-1 自检判成
|
||||
* "定值被非法改写"→ 整台保护闭锁
|
||||
*/
|
||||
static int feed_param_apply(uint8_t a_id, int32_t a_q10) {
|
||||
if (a_id >= (uint8_t)PS_ID_COUNT) {
|
||||
return -1;
|
||||
}
|
||||
return prot_set_set_sealed((prot_set_id_t)a_id, a_q10);
|
||||
}
|
||||
|
||||
/* 注入给 ui_data_param_drain() 的执行侧实现(BSP 不保存副本,每次调用现传) */
|
||||
static const ui_data_param_ops_t s_param_ops = {
|
||||
feed_param_locked,
|
||||
feed_param_check,
|
||||
feed_param_apply
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
* 函数功能:组装参数快照(40 项定值 Q10 + 校验和 + 封存态 + 越界位图 + 未标定闭锁)
|
||||
* 入口参数:p_snap - 目标帧(调用方已清零) ui_data_par_t *
|
||||
* sum - 本次发布的 prot_set_checksum() 值 uint32_t
|
||||
* 返回值:无
|
||||
* 限定条件:uiTask 上下文(读侧;写者 = iec104Task 的 set_sealed 与装载路径,
|
||||
* 读到的永远是"值区与封存一致"的完整快照)
|
||||
* 函数说明:只搬 Q10 原值不换算小数位——换算属表现层
|
||||
* (docs/UI设计/UI_数据供给设计说明.md §8)
|
||||
*/
|
||||
static void feed_fill_par(ui_data_par_t *p_snap, uint32_t sum) {
|
||||
uint8_t i;
|
||||
|
||||
p_snap->checksum = sum;
|
||||
p_snap->valid = 1u;
|
||||
p_snap->locked = prot_set_locked();
|
||||
p_snap->bad_mask = prot_set_bad_mask();
|
||||
/* 越界位图非 0 = 上一次整表装载/提交有越界项(prot_set_bad_mask 的既有口径),
|
||||
* 页面据此反显;为 0 时无意义位图不参与显示 */
|
||||
p_snap->bad_valid = (p_snap->bad_mask != 0u) ? 1u : 0u;
|
||||
/* v2 尾追加:未标定闭锁徽标(与判据停走**同一个判定**:App/algo/adc_cal.c 的
|
||||
* adc_cal_prot_blocked(),道F task-16 起本文件不再自带一份三路判定——两份实现
|
||||
* 可以各自漂移,屏上"未标定"就会与判据实际停不走同一件事)。
|
||||
* ⚠ 不要改回用 rt.eng 反推:eng 要求电气 **8** 路全 VALID,V0.1 双转换器下
|
||||
* UB/UC/IB/IC/IN 永不置位 → eng 恒为引脚档,照它画徽标就永不解锁;
|
||||
* 两个谓词各有用途,边界写在 adc_cal.h 的函数头第③条。
|
||||
* ⚠ 与上面的 locked 是两件事——locked 禁编辑,cal_block 只说明"改了也不会动作" */
|
||||
p_snap->cal_block = adc_cal_prot_blocked();
|
||||
for (i = 0u; i < (uint8_t)PS_ID_COUNT; i++) {
|
||||
p_snap->q10[i] = prot_set_get((prot_set_id_t)i);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:组装装置信息快照(身份 + 运行时长 + 通讯参数)
|
||||
* 入口参数:p_snap - 目标帧(调用方已清零) ui_data_info_t *
|
||||
* tick_ms - 当前 tick(换算 uptime_s 用) uint32_t
|
||||
* 返回值:无
|
||||
* 限定条件:uiTask 上下文
|
||||
* 函数说明:页面不得自己 include 协议/配置头取这些值,一律经本帧下发
|
||||
* (docs/UI设计/页面扩充详细方案.md §3.5"值必须在 App 侧取");
|
||||
* 公共地址 CA 不在此下发——固件里 CA 是逐帧从 ASDU 头解析的,
|
||||
* 没有集中常量可取,待 104 侧给出查询点后按扩展规则追加字段
|
||||
*/
|
||||
static void feed_fill_info(ui_data_info_t *p_snap, uint32_t tick_ms) {
|
||||
const ftp_cfg_t *p_ftp = ftp_cfg_get();
|
||||
|
||||
p_snap->valid = 1u;
|
||||
p_snap->uptime_s = tick_ms / 1000u;
|
||||
p_snap->port_104 = (uint16_t)IEC104_SERV_PORT;
|
||||
p_snap->port_disp = (uint16_t)UI_NET_SINK_PORT;
|
||||
feed_copy_str(p_snap->model, sizeof(p_snap->model), FEED_MODEL);
|
||||
feed_copy_str(p_snap->sw_ver, sizeof(p_snap->sw_ver), FEED_SW_VER);
|
||||
feed_copy_str(p_snap->hw_ver, sizeof(p_snap->hw_ver), FEED_HW_VER);
|
||||
feed_copy_str(p_snap->build_date, sizeof(p_snap->build_date), __DATE__);
|
||||
feed_copy_str(p_snap->sw_sum, sizeof(p_snap->sw_sum), FEED_SW_SUM);
|
||||
if (p_ftp != NULL) {
|
||||
feed_copy_str(p_snap->ftp_user, sizeof(p_snap->ftp_user), p_ftp->user);
|
||||
} else {
|
||||
p_snap->ftp_user[0] = '\0';
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:装配节拍入口——先落定一条定值整定请求(写方向),再发布实时帧与
|
||||
* 信息帧(满 200 ms 一次)、参数帧(仅校验和变化时);首轮立即发布
|
||||
* (免启动初期出现一闪的 "--")
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:uiTask 上下文,主循环每轮一次(UI 循环周期 10 ms)
|
||||
* 函数说明:每轮未到期时代价 = 一次 osKernelGetTickCount + 两次减法比较 +
|
||||
* 一次空队列判断(drain 无请求时只是一次计数比较);
|
||||
* 差值法吸收 tick 回绕(同 App/algo/adc_health.h 的 now_ms 口径)。
|
||||
* 写路径失败态(ERR_RANGE/ERR_LOCKED/ERR_BUSY)走屏上通道,不打日志
|
||||
* (每条 DBG 约 90 B Flash + 216 B 栈,AGENTS「日志不是免费的」)
|
||||
*/
|
||||
void ui_data_feed_tick(void) {
|
||||
uint32_t now;
|
||||
uint32_t sum;
|
||||
uint8_t due_rt;
|
||||
|
||||
now = osKernelGetTickCount();
|
||||
|
||||
if (s_map_done == 0u) {
|
||||
feed_map_phasor();
|
||||
}
|
||||
|
||||
/* 写路径:先落定整定请求,**再**取校验和——本轮刚写进 prot_set 的定值就进
|
||||
* 本轮"校验和变化才发"那道门,页面保存行的 OK 与新值同一帧回来,不必多等
|
||||
* 一轮(10 ms)。顺序反了不出错,只是慢一轮。
|
||||
* 每轮最多处理一条(ui_data_param_drain 的限定条件):整定是人工低频事件,
|
||||
* 一轮一条已经把最坏耗时钉住,不必排空整个队列
|
||||
* ⚠ 返回值必须显式丢弃:本函数不消费"处理了几条",但 bsp-err 的
|
||||
* consume-result 只认接住或 (void) 两种写法(实测编译器与 cppcheck 都不报) */
|
||||
(void)ui_data_param_drain(&s_param_ops);
|
||||
|
||||
sum = prot_set_checksum();
|
||||
|
||||
due_rt = 0u;
|
||||
if ((s_first_done == 0u) || ((uint32_t)(now - s_last_rt_ms) >= FEED_RT_PERIOD_MS)) {
|
||||
due_rt = 1u;
|
||||
s_first_done = 1u;
|
||||
s_last_rt_ms = now;
|
||||
}
|
||||
if (due_rt != 0u) {
|
||||
(void)memset(&s_scr, 0, sizeof(s_scr));
|
||||
feed_fill_rt(&s_scr.rt, now);
|
||||
(void)ui_data_publish_rt(&s_scr.rt);
|
||||
|
||||
(void)memset(&s_scr, 0, sizeof(s_scr));
|
||||
feed_fill_info(&s_scr.info, now);
|
||||
(void)ui_data_publish_info(&s_scr.info);
|
||||
}
|
||||
|
||||
/* 参数帧门控:定值变化是低频事件,不随 200 ms 白拷一帧(契约 §5.2)。
|
||||
* 但"校验和没变"不等于"页面该看的没变"——本帧里还有两个与校验和无关的事实:
|
||||
* · locked(定值闭锁):自检失配时置位,校验和一字不变,
|
||||
* 而整定页保存行要显示 LOCK 并禁编辑;
|
||||
* · cal_block(未标定闭锁):卡 12 两点标定写完 [adc_cal] 的 valid 就翻转,
|
||||
* 校验和同样不动,而标题上的"未标定"徽标要当场消失。
|
||||
* 只盯校验和,这两件事都到不了屏上(真机现象:标定做完了屏上徽标永远挂着,
|
||||
* 直到某次定值改动才被顺带刷掉——那是把两件不相干的事当同步点)。
|
||||
* 代价:每轮多两次字节比较(无变化时零拷贝),换来屏上状态与真值同拍 */
|
||||
{
|
||||
uint8_t locked_now = prot_set_locked();
|
||||
uint8_t cal_now = adc_cal_prot_blocked(); /* 标定层单点,与判据同源 */
|
||||
|
||||
if ((s_par_sum_valid == 0u) || (sum != s_par_sum)
|
||||
|| (locked_now != s_par_locked) || (cal_now != s_par_cal_block)) {
|
||||
(void)memset(&s_scr, 0, sizeof(s_scr));
|
||||
feed_fill_par(&s_scr.par, sum);
|
||||
(void)ui_data_publish_par(&s_scr.par);
|
||||
s_par_sum = sum;
|
||||
s_par_locked = locked_now;
|
||||
s_par_cal_block = cal_now;
|
||||
s_par_sum_valid = 1u;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* 模块名称:UI 数据组装器(ui_data_feed)
|
||||
* 模块功能:App → UI 数据通道的"写侧"——按节拍从采集/算法/保护/协议/时钟各模块
|
||||
* 取数,组装成 docs/UI设计/UI_数据供给设计说明.md §4 契约的三帧
|
||||
* (实时量 rt / 参数 par / 装置信息 info),经 Drivers/BSP/UI/ui_data.h
|
||||
* 的发布 API 推给 UI 层;本模块是快照的**唯一填充者**
|
||||
* 适用范围:App/app(装配层:可同时 include 两侧头,方向仍是上层依赖下层,
|
||||
* scripts/check_layer_deps.py 只拦"Drivers/BSP 目录反向 include App 目录")
|
||||
* 线程约定:uiTask 上下文,主循环每轮调 ui_data_feed_tick() 一次;与页面读写同
|
||||
* 上下文串行 → 无锁(docs/UI设计/UI_数据供给设计说明.md §5.1)。
|
||||
* ⚠ 不得在 adcTask/protectTask/netTask 里调用本模块
|
||||
* 端接法:MCU = App/task/ui_task.c 主循环;PC 模拟器**不链接本文件**(模拟器不跑
|
||||
* 固件装配链,快照由 simulator/main.c 直接发布固定帧保证像素回归可复现,
|
||||
* 见 docs/UI设计/UI_数据供给设计说明.md §9.2;模拟器豁免登记在
|
||||
* scripts/check_registration.py 的 SIM_EXEMPT)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-25
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 王建锋 创建(道E task-14)
|
||||
* v1.1 2026-10-26 DTU 项目组 道F task-1:tick 兼任定值写路径的落定节拍
|
||||
* (契约 §6.2;实现见 .c 的 s_param_ops)
|
||||
*/
|
||||
|
||||
#ifndef UI_DATA_FEED_H
|
||||
#define UI_DATA_FEED_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*
|
||||
* 函数功能:装配节拍入口——先落定一条定值整定请求(写方向),再发布实时帧与
|
||||
* 信息帧(满 200 ms 一次)、参数帧(仅校验和变化时);首轮立即发布
|
||||
* (免启动后出现一闪的 "--")
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:uiTask 上下文,主循环每轮一次(UI 循环周期 10 ms);页面"保存"按键
|
||||
* 与本页节拍同上下文,故写路径的投递/落定之间不需要任何锁
|
||||
* (Drivers/BSP/UI/ui_data.h 写路径节)
|
||||
* 函数说明:本函数每轮代价 = 一次 tick 差值比较 + 一次校验和比较 + 一次
|
||||
* ui_data_param_drain()(队列空时只是一次计数判断);真正的取数与组装
|
||||
* 只在到期时发生(docs/UI设计/UI_数据供给设计说明.md §5.2)。
|
||||
* ⚠ drain 排在取校验和**之前**:本轮写入的定值要在本轮就被"校验和变化
|
||||
* 才发"那道门看见,页面才能在同一帧里同时显示 OK 与新值
|
||||
* 内部零阻塞、零堆分配、零日志(日志每条约 90 B Flash + 216 B 栈,
|
||||
* 200 ms 周期打不起);整定失败态走屏上通道,不打日志
|
||||
*/
|
||||
void ui_data_feed_tick(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* UI_DATA_FEED_H */
|
||||
+105
-19
@@ -8,20 +8,28 @@
|
||||
* 创建日期:2026-09-14
|
||||
* 修改记录:
|
||||
* v1.0 2026-09-14 王建锋 创建初始版本(P3:段键表引擎 + net_cfg 集成)
|
||||
* v1.1 2026-09-18 王建锋 P0-1:临时缓冲改经 cfg_store_alloc(MCU = FreeRTOS 堆),
|
||||
* v1.1 2026-09-18 王建锋 临时缓冲改经 cfg_store_alloc(MCU = FreeRTOS 堆),
|
||||
* 移除对 C 库 malloc 的全部依赖(MicroLIB C 堆仅 512B,
|
||||
* 原 4 处 KB 级 malloc 真机必然失败);增编译期堆峰值预算
|
||||
* 校验 + 失败上报自检记录
|
||||
* v1.2 2026-09-21 王建锋 E-01(错误码传播审计 P0):超长文件守卫根治——poll 判定
|
||||
* v1.2 2026-09-21 王建锋 超长文件守卫根治——poll 判定
|
||||
* 前置到 cfg_store_stat 真实 st.size(旧检查位于截断后的
|
||||
* len 上恒假=死码,超长文件被截断当有效解析 → 界外静默
|
||||
* 回工厂默认 → 基线粘住 → 用户配置销毁链);save 输出上限
|
||||
* 由 CFG_OUT_MAX 收紧至 CFG_FILE_MAX_LEN;s_write_doc 写盘
|
||||
* 唯一闸口拒写超长文档;load/poll 超长处置补自检留痕
|
||||
* v1.3 2026-10-04 王建锋 P2-1:受管键行超 CFG_LINE_TEST_MAX 时拒绝保存——
|
||||
* v1.3 2026-10-04 王建锋 受管键行超 CFG_LINE_TEST_MAX 时拒绝保存——
|
||||
* 原实现 Pass1 静默跳过超长行,Pass2 遂在段头补写新 KV 且
|
||||
* 原文超长行原样保留 → 同一键出现两行(旧值残留、静默截断)
|
||||
* 函数说明:引擎状态非重入——调用方须收敛同一任务(defaultTask:poll/save,见 P1-3;
|
||||
* v1.4 2026-10-15 王建锋 生成文档边界余量收口——
|
||||
* s_gen_document 两处 `off += snprintf(...)` 改检查式(截断即
|
||||
* 返回 -1,不再跳过缓冲末尾);注册拒超长键名(≤ 15 字符),
|
||||
* 使每行 112 字节余量成为可编译期验证的前提
|
||||
* v1.5 2026-10-25 DTU 项目组 数据可信卡 3:新增值类型 CFG_VAL_INT(有符号十进制,
|
||||
* len 1/2/4 小端补码)——s_decode_int() 用 strtol + ERANGE +
|
||||
* 宽度上下界校验,s_encode() 带负号输出;负零点偏移从此可
|
||||
* 干净落盘(动因见 docs/任务规划/数据可信.md §4.4 与 §7 决策 3)
|
||||
* 函数说明:引擎状态非重入——调用方须收敛同一任务(defaultTask:poll/save;
|
||||
* 启动期 load 先于任务竞争);FS 卷并发由 FatFS REENTRANT 卷锁保证
|
||||
* (见《配置文件设计说明》D15 A+ 方案)
|
||||
*/
|
||||
@@ -29,12 +37,14 @@
|
||||
/* 头文件包含区 */
|
||||
#include <string.h>
|
||||
|
||||
#include <stdlib.h> /* strtoul(键值解码);堆分配一律走 cfg_store_alloc,禁 malloc */
|
||||
#include <stdlib.h> /* strtoul / strtol(键值解码);堆分配一律走 cfg_store_alloc,禁 malloc */
|
||||
|
||||
#include <stdio.h>
|
||||
|
||||
#include <ctype.h>
|
||||
|
||||
#include <errno.h> /* ERANGE:strtol 饱和判别(INT 键解码需要,数据可信卡 3) */
|
||||
|
||||
#include "cfg_file.h"
|
||||
|
||||
#include "cfg_file_store.h"
|
||||
@@ -63,7 +73,7 @@ static const char s_doc_header[] =
|
||||
#define CFG_OUT_MAX 5120u /* 保存输出缓冲上限(经 cfg_store_alloc 取,见下方堆峰值预算) */
|
||||
#define CFG_LINE_TEST_MAX 127u /* 受管键行匹配的行长上限(超长必非受管键行) */
|
||||
#define CFG_SNAP_MAX 64u /* 变更检测段快照缓冲(挂回调段 data_size 须 ≤ 此值) */
|
||||
#define CFG_NAME_MAX 16u /* 段名缓冲(15 字符 + NUL) */
|
||||
#define CFG_NAME_MAX (CFG_FILE_MAX_NAME_LEN + 1u) /* 段名/键名缓冲(15 字符 + NUL) */
|
||||
#define CFG_HEAP_BUDGET 10240u /* 暂态堆峰值预算:读缓冲(1B NUL) + 输出缓冲之和上限 */
|
||||
|
||||
/* 编译期钉死暂态堆峰值:放大 CFG_FILE_MAX_LEN / CFG_OUT_MAX 会先在此编译失败,强制
|
||||
@@ -259,6 +269,28 @@ static int s_encode(const cfg_key_def_t *p_key, const uint8_t *p_src, char *p_ds
|
||||
n = snprintf(p_dst, cap, "%lu", v);
|
||||
break;
|
||||
}
|
||||
case CFG_VAL_INT: {
|
||||
long v = 0L;
|
||||
unsigned long raw = 0uL;
|
||||
size_t i;
|
||||
|
||||
if (p_key->len != 1u && p_key->len != 2u && p_key->len != 4u) {
|
||||
return -1;
|
||||
}
|
||||
for (i = 0u; i < p_key->len; i++) {
|
||||
raw |= ((unsigned long)p_src[i]) << (8u * i);
|
||||
}
|
||||
/* 小端补码还原为有符号值(两端 long = 32 位:ARMCC 32 位 ARM / MSVC LLP64) */
|
||||
if (p_key->len == 1u) {
|
||||
v = (long)(int8_t)(raw & 0xFFuL);
|
||||
} else if (p_key->len == 2u) {
|
||||
v = (long)(int16_t)(raw & 0xFFFFuL);
|
||||
} else {
|
||||
v = (long)(int32_t)raw;
|
||||
}
|
||||
n = snprintf(p_dst, cap, "%ld", v);
|
||||
break;
|
||||
}
|
||||
case CFG_VAL_IP4:
|
||||
n = snprintf(p_dst, cap, "%u.%u.%u.%u",
|
||||
(unsigned)p_src[0], (unsigned)p_src[1],
|
||||
@@ -272,7 +304,7 @@ static int s_encode(const cfg_key_def_t *p_key, const uint8_t *p_src, char *p_ds
|
||||
case CFG_VAL_STR: {
|
||||
size_t slen;
|
||||
|
||||
/* P3-1:len==0 时 `p_key->len - 1u` 回绕为 SIZE_MAX → memcpy 巨长越界。
|
||||
/* len==0 时 `p_key->len - 1u` 回绕为 SIZE_MAX → memcpy 巨长越界。
|
||||
* STR 字段至少 1 字节(含结尾 NUL),len==0 非法,直接拒绝 */
|
||||
if (p_key->len == 0u) {
|
||||
return -1;
|
||||
@@ -325,6 +357,44 @@ static int s_decode_uint(uint8_t *p_dst, size_t len, const char *p_val) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:解析有符号十进制值(小端补码写入,数据可信卡 3)
|
||||
* 入口参数:p_dst - 目标字段 uint8_t *
|
||||
* len - 字段字节数(1/2/4) size_t
|
||||
* p_val - 值文本(可带 +/- 号) const char *
|
||||
* 返回值:0 - 成功;-1 - 非法(空/非数字/带后缀/超本宽度范围/strtol 饱和) int
|
||||
* 限定条件:无
|
||||
* 函数说明:两端 long = 32 位,strtol 对超范围文本饱和到 LONG_MAX/MIN 并置
|
||||
* ERANGE(如 "2147483648" 饱和成 INT32_MAX 本身,光比 v>max_v 抓不住),
|
||||
* 故 ERANGE 与宽度上下界两道都要查;1/2 字节键按 int8/int16 范围收窄
|
||||
*/
|
||||
static int s_decode_int(uint8_t *p_dst, size_t len, const char *p_val) {
|
||||
const long min_v = (len == 1u) ? -128L : ((len == 2u) ? -32768L : (-2147483647L - 1L));
|
||||
const long max_v = (len == 1u) ? 127L : ((len == 2u) ? 32767L : 2147483647L);
|
||||
char *p_end = NULL;
|
||||
long v;
|
||||
size_t i;
|
||||
|
||||
if (len != 1u && len != 2u && len != 4u) {
|
||||
return -1;
|
||||
}
|
||||
if (p_val[0] == '\0') {
|
||||
return -1;
|
||||
}
|
||||
errno = 0;
|
||||
v = strtol(p_val, &p_end, 10);
|
||||
if (p_end == p_val || *p_end != '\0' || errno == ERANGE) {
|
||||
return -1;
|
||||
}
|
||||
if (v < min_v || v > max_v) {
|
||||
return -1;
|
||||
}
|
||||
for (i = 0u; i < len; i++) {
|
||||
p_dst[i] = (uint8_t)(((unsigned long)v) >> (8u * i));
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:解析点分 IPv4("a.b.c.d",每段 0~255)
|
||||
* 入口参数:p_dst - 4 字节目标 uint8_t *
|
||||
@@ -436,6 +506,8 @@ static int s_decode(const cfg_key_def_t *p_key, uint8_t *p_dst, const char *p_va
|
||||
switch (p_key->type) {
|
||||
case CFG_VAL_UINT:
|
||||
return s_decode_uint(p_dst, p_key->len, p_val);
|
||||
case CFG_VAL_INT:
|
||||
return s_decode_int(p_dst, p_key->len, p_val);
|
||||
case CFG_VAL_IP4:
|
||||
return s_decode_ip4(p_dst, p_val);
|
||||
case CFG_VAL_MAC:
|
||||
@@ -457,6 +529,7 @@ static int s_decode(const cfg_key_def_t *p_key, uint8_t *p_dst, const char *p_va
|
||||
*/
|
||||
static int s_gen_document(char *p_buf, size_t cap, size_t *p_len) {
|
||||
size_t off;
|
||||
int n;
|
||||
int i;
|
||||
int k;
|
||||
size_t hlen;
|
||||
@@ -480,7 +553,11 @@ static int s_gen_document(char *p_buf, size_t cap, size_t *p_len) {
|
||||
if (cap < off + 112u) {
|
||||
return -1;
|
||||
}
|
||||
off += (size_t)snprintf(p_buf + off, cap - off, "\r\n[%s]\r\n", p_sec->name);
|
||||
n = snprintf(p_buf + off, cap - off, "\r\n[%s]\r\n", p_sec->name);
|
||||
if (n < 0 || (size_t)n >= cap - off) {
|
||||
return -1;
|
||||
}
|
||||
off += (size_t)n;
|
||||
|
||||
for (k = 0; k < (int)p_sec->key_count; k++) {
|
||||
const cfg_key_def_t *p_key = &p_sec->p_keys[k];
|
||||
@@ -492,7 +569,11 @@ static int s_gen_document(char *p_buf, size_t cap, size_t *p_len) {
|
||||
if (cap < off + 112u) {
|
||||
return -1;
|
||||
}
|
||||
off += (size_t)snprintf(p_buf + off, cap - off, "%s=%s\r\n", p_key->name, v);
|
||||
n = snprintf(p_buf + off, cap - off, "%s=%s\r\n", p_key->name, v);
|
||||
if (n < 0 || (size_t)n >= cap - off) {
|
||||
return -1;
|
||||
}
|
||||
off += (size_t)n;
|
||||
}
|
||||
}
|
||||
*p_len = off;
|
||||
@@ -538,7 +619,7 @@ static int s_load_file(uint8_t **p_pbuf, size_t *p_len) {
|
||||
* len - 数据长度 size_t
|
||||
* 返回值:0 - 成功;-1 - 写失败或超长拒写 int
|
||||
* 限定条件:p_buf 非空;len ≤ CFG_FILE_MAX_LEN
|
||||
* 函数说明:E-01 纵深防御——超长文档一旦落盘,下次开机 load 守卫即判损坏、
|
||||
* 函数说明:纵深防御——超长文档一旦落盘,下次开机 load 守卫即判损坏、
|
||||
* 整份用户配置连同注释/未知段销毁,故在唯一出口硬性拦截,使
|
||||
* "引擎永不产出超长 config.ini"成为结构性质(调用者新增路径免疫)
|
||||
*/
|
||||
@@ -842,7 +923,7 @@ static int s_surgical(char *p_in, size_t in_len, char *p_out, size_t out_cap, si
|
||||
* i_sec - 段索引 int
|
||||
* 返回值:1 - 是受管键;0 - 不是(无 '=' 或键名不匹配) int
|
||||
* 限定条件:仅比较 '=' 前的键名(大小写无关、剥尾部空白),不解析值
|
||||
* 函数说明:P2-1 用于"超长行是否落在受管键上"的判定(值可能很长,无法拷入定长缓冲);
|
||||
* 函数说明:用于"超长行是否落在受管键上"的判定(值可能很长,无法拷入定长缓冲);
|
||||
* 非修改式,故可安全用于仍供 Pass2 原样保留的行
|
||||
*/
|
||||
static int s_line_key_managed(const char *p_line, int i_sec) {
|
||||
@@ -888,9 +969,9 @@ static int s_line_key_managed(const char *p_line, int i_sec) {
|
||||
/*
|
||||
* 函数功能:保存 Pass 1——定位段头行号与各受管键行号
|
||||
* 入口参数:p_in - 原文本(可修改) char *
|
||||
* 返回值:0 - 成功;-1 - 受管键行超 CFG_LINE_TEST_MAX(拒绝保存,见 P2-1) int
|
||||
* 返回值:0 - 成功;-1 - 受管键行超 CFG_LINE_TEST_MAX(拒绝保存) int
|
||||
* 限定条件:行末被 NUL 化且不复原——调用方须传一次性副本
|
||||
* 函数说明:P2-1(2026-10-04):受管键行超过 CFG_LINE_TEST_MAX 时**不能**静默跳过
|
||||
* 函数说明:受管键行超过 CFG_LINE_TEST_MAX 时**不能**静默跳过
|
||||
* ——Pass2 会因该行未登记而在段头补写新 KV,同时原文超长行原样保留,导致
|
||||
* 同一键出现两行(旧值残留 + 新值,解析取谁取决于行序)。故此处显式拒绝:
|
||||
* 返回 -1,调用方中止保存、原文件不动、自检留痕。非受管键的超长行照旧跳过
|
||||
@@ -952,7 +1033,7 @@ static int s_surgical_pass1(char *p_in) {
|
||||
}
|
||||
}
|
||||
else if (i_cur >= 0) {
|
||||
/* 超长行(> CFG_LINE_TEST_MAX):若其键名为本段受管键 → 拒绝保存(P2-1);
|
||||
/* 超长行(> CFG_LINE_TEST_MAX):若其键名为本段受管键 → 拒绝保存;
|
||||
* 否则按未知内容放过(Pass2 原样保留) */
|
||||
if (s_line_key_managed(p_line, i_cur) != 0) {
|
||||
return -1;
|
||||
@@ -1004,13 +1085,18 @@ int cfg_file_register(const cfg_section_reg_t *p_reg) {
|
||||
for (k = 0; k < (int)p_reg->key_count; k++) {
|
||||
const cfg_key_def_t *p_key = &p_reg->p_keys[k];
|
||||
|
||||
if (p_key->name == NULL || p_key->len == 0u) {
|
||||
if (p_key->name == NULL || p_key->len == 0u ||
|
||||
strlen(p_key->name) >= CFG_NAME_MAX) {
|
||||
return -1;
|
||||
}
|
||||
if (p_key->type == CFG_VAL_UINT &&
|
||||
(p_key->len != 1u && p_key->len != 2u && p_key->len != 4u)) {
|
||||
return -1;
|
||||
}
|
||||
if (p_key->type == CFG_VAL_INT &&
|
||||
(p_key->len != 1u && p_key->len != 2u && p_key->len != 4u)) {
|
||||
return -1;
|
||||
}
|
||||
if (p_key->type == CFG_VAL_IP4 && p_key->len != 4u) {
|
||||
return -1;
|
||||
}
|
||||
@@ -1079,7 +1165,7 @@ int cfg_file_load(void) {
|
||||
}
|
||||
|
||||
if (st.size > CFG_FILE_MAX_LEN) {
|
||||
/* 文件超长 = 损坏:留工厂默认 + 重建模板(E-01:补自检留痕——销毁
|
||||
/* 文件超长 = 损坏:留工厂默认 + 重建模板(补自检留痕——销毁
|
||||
* 用户文件属重大事件,仅串口 DBG_ERROR 在现场无调试器时不可追溯) */
|
||||
if (s_write_template() != 0) {
|
||||
DBG_ERROR("load: corrupt file, template write FAIL");
|
||||
@@ -1168,7 +1254,7 @@ int cfg_file_save(void) {
|
||||
(void)memcpy(p_in1, p_in, in_len + 1u);
|
||||
|
||||
if (s_surgical_pass1((char *)p_in1) != 0) {
|
||||
/* P2-1:受管键行超 CFG_LINE_TEST_MAX → 中止保存(原文件不动)。此类超长行
|
||||
/* 受管键行超 CFG_LINE_TEST_MAX → 中止保存(原文件不动)。此类超长行
|
||||
* 只可能来自手工编辑(代码生成的受管 KV 行恒 <127B);若强行外科保存会
|
||||
* 产生重复键行,故宁可拒存并留痕,也不静默写坏。 */
|
||||
cfg_store_free(p_in1);
|
||||
@@ -1190,7 +1276,7 @@ int cfg_file_save(void) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* E-01:输出上限由 CFG_OUT_MAX(5120) 收紧至 CFG_FILE_MAX_LEN(4096)——
|
||||
/* 输出上限由 CFG_OUT_MAX(5120) 收紧至 CFG_FILE_MAX_LEN(4096)——
|
||||
* 旧上限允许把 >4096B 文档合法写盘(= 下一颗雷:下次开机 load 守卫
|
||||
* 判损坏、整份用户文件连同注释/未知段一起销毁)。超限走下方既有
|
||||
* 溢出中止路径:原文件不动 + 自检上报,RAM 副本保留(可修复后重试) */
|
||||
@@ -1256,7 +1342,7 @@ void cfg_file_poll(void) {
|
||||
{
|
||||
int i;
|
||||
|
||||
/* E-01(2026-09-21 审计 P0):超长守卫必须基于 cfg_store_stat 的真实
|
||||
/* 2026-09-21 审计 P0:超长守卫必须基于 cfg_store_stat 的真实
|
||||
* st.size 且先于 s_load_file——s_load_file 读长钉死 CFG_FILE_MAX_LEN
|
||||
* (nread ≤ 4096 恒成立),旧检查写在读后的 len 上恒假(死码):
|
||||
* 超长文件被截断当有效解析 → 界外段/键静默回工厂默认 → 基线粘住 →
|
||||
|
||||
+21
-9
@@ -8,11 +8,16 @@
|
||||
* 创建日期:2026-09-14
|
||||
* 修改记录:
|
||||
* v1.0 2026-09-14 王建锋 创建初始版本(P3:段键表引擎 + net_cfg 集成)
|
||||
* v1.1 2026-09-15 王建锋 属主任务由 netTask 改为 defaultTask(配置引擎单一属主,P1-3)
|
||||
* v1.2 2026-09-21 王建锋 E-01(错误码传播审计 P0):超长守卫契约同步——poll 沉降
|
||||
* v1.1 2026-09-15 王建锋 属主任务由 netTask 改为 defaultTask(配置引擎单一属主)
|
||||
* v1.2 2026-09-21 王建锋 超长守卫契约同步——poll 沉降
|
||||
* 后先按真实 st.size 判超长(旧实现查截断后读长恒假)、
|
||||
* save 输出上限收紧至 CFG_FILE_MAX_LEN、写盘唯一闸口拒写
|
||||
* 超长;超长处置补自检留痕
|
||||
* v1.3 2026-10-15 王建锋 新增公开常量
|
||||
* CFG_FILE_MAX_NAME_LEN(段名/键名上限 15 字符)
|
||||
* v1.4 2026-10-25 DTU 项目组 数据可信卡 3:新增 CFG_VAL_INT 有符号十进制值类型
|
||||
* (len = 1/2/4,小端补码)——标定零点偏移等负值需有符号
|
||||
* 落盘,动因与取舍见 docs/任务规划/数据可信.md §4.4
|
||||
*/
|
||||
|
||||
#ifndef CFG_FILE_H
|
||||
@@ -27,12 +32,16 @@ extern "C" {
|
||||
#include <stddef.h>
|
||||
|
||||
/* 配置文件长度上限(字节);超过 = 文件损坏,按出厂模板重建
|
||||
* (load 与 poll 同判,E-01:poll 在读取前基于真实 st.size 判定) */
|
||||
* (load 与 poll 同判:poll 在读取前基于真实 st.size 判定) */
|
||||
#define CFG_FILE_MAX_LEN 4096u
|
||||
|
||||
/* 引擎可注册段数上限 */
|
||||
#define CFG_FILE_MAX_SECTIONS 8u
|
||||
|
||||
/* 段名 / 键名最大字符数(不含结尾 NUL);超长的注册描述符被 cfg_file_register 拒绝。
|
||||
* 该上限是生成文档每行余量(112 字节)可编译期验证的前提,勿放宽 */
|
||||
#define CFG_FILE_MAX_NAME_LEN 15u
|
||||
|
||||
/* 单段键数上限 */
|
||||
#define CFG_FILE_MAX_KEYS 16u
|
||||
|
||||
@@ -45,17 +54,20 @@ extern "C" {
|
||||
* IP4 :点分 IPv4 "a.b.c.d";字段 uint8_t[4]
|
||||
* MAC :十六进制 MAC "xx:xx:xx:xx:xx:xx"(分隔符兼容 - 与 .);字段 uint8_t[6]
|
||||
* STR :ASCII 字符串;字段 char[len](len = 缓冲长度,自动补 NUL)
|
||||
* INT :有符号十进制(可带 + / - 号);len = 目标字段字节数(1/2/4,小端补码,
|
||||
* 范围按宽度收窄:int8/int16/int32;负值合法,如标定零点偏移)
|
||||
*/
|
||||
typedef enum {
|
||||
CFG_VAL_UINT = 0,
|
||||
CFG_VAL_IP4,
|
||||
CFG_VAL_MAC,
|
||||
CFG_VAL_STR
|
||||
CFG_VAL_STR,
|
||||
CFG_VAL_INT
|
||||
} cfg_val_type_t;
|
||||
|
||||
/* 键定义(offset/len 均相对段 RAM 副本起始) */
|
||||
typedef struct {
|
||||
const char *name; /* 键名(小写) */
|
||||
const char *name; /* 键名(小写,≤ CFG_FILE_MAX_NAME_LEN 字符) */
|
||||
cfg_val_type_t type; /* 值类型 */
|
||||
size_t offset; /* 字段偏移 */
|
||||
size_t len; /* 字段长度 */
|
||||
@@ -63,7 +75,7 @@ typedef struct {
|
||||
|
||||
/* 段注册描述符 */
|
||||
typedef struct {
|
||||
const char *name; /* 段名(≤ 15 字符) */
|
||||
const char *name; /* 段名(≤ CFG_FILE_MAX_NAME_LEN 字符) */
|
||||
void *p_data; /* 段 RAM 副本(模块自有静态区,引擎原地更新) */
|
||||
const void *p_defaults; /* 工厂默认值(与 p_data 同布局;load 先复位再解析) */
|
||||
size_t data_size; /* RAM 副本字节数 */
|
||||
@@ -91,7 +103,7 @@ int cfg_file_register(const cfg_section_reg_t *p_reg);
|
||||
* 2. 文件缺失 → 按当前 RAM 副本生成出厂模板写盘(首启建文件)
|
||||
* 3. 文件存在 → 解析:有效键写入 RAM 副本;无效键留工厂默认并留痕
|
||||
* 4. 文件超长(> CFG_FILE_MAX_LEN)= 损坏 → 工厂默认 + 重建模板
|
||||
* + 自检留痕(poll 检出运行期超长文件执行同一契约,E-01)
|
||||
* + 自检留痕(poll 检出运行期超长文件执行同一契约)
|
||||
*/
|
||||
int cfg_file_load(void);
|
||||
|
||||
@@ -104,7 +116,7 @@ int cfg_file_load(void);
|
||||
* 值原位改写,缺失段/键补写;文件缺失/损坏 → 按 RAM 副本重建
|
||||
* 完整文档(含文件头注释);写盘 = 临时文件 + 原子改名;输出超
|
||||
* CFG_FILE_MAX_LEN → 拒绝保存返回 -1(原文件不动 + 自检上报,
|
||||
* 引擎写盘闸口同拒,E-01)
|
||||
* 引擎写盘闸口同拒)
|
||||
*/
|
||||
int cfg_file_save(void);
|
||||
|
||||
@@ -117,7 +129,7 @@ int cfg_file_save(void);
|
||||
* 函数说明:(size, mtime 变更令牌) 相对基线变化且连续 CFG_FILE_SETTLE_CNT
|
||||
* 次轮询稳定 → 先查 st.size 超 CFG_FILE_MAX_LEN = 损坏 → 工厂默认
|
||||
* + 重建模板 + 自检留痕(不触发段回调:保留在线修复通道,重启
|
||||
* 收敛,E-01);否则重载全部段(各段复位工厂默认后解析);挂回调
|
||||
* 收敛);否则重载全部段(各段复位工厂默认后解析);挂回调
|
||||
* 段有差异 → 调用其回调(如触发网络重建);重载后刷新基线
|
||||
* (回调触发的写盘不引发二次重载)
|
||||
*/
|
||||
|
||||
@@ -9,16 +9,20 @@
|
||||
* 修改记录:
|
||||
* v1.0 2026-09-14 王建锋 创建初始版本
|
||||
* v1.1 2026-09-18 王建锋 增 cfg_store_alloc / cfg_store_free / cfg_store_heap_free:
|
||||
* 共享核心临时缓冲改走 FreeRTOS 堆(P0-1,MicroLIB C 堆仅 512B)
|
||||
* 共享核心临时缓冲改走 FreeRTOS 堆(MicroLIB C 堆仅 512B)
|
||||
* v1.2 2026-09-22 王建锋 原子写修复:rename 前先 unlink 已存在目标——本 FatFs
|
||||
* 移植 f_rename 不覆盖既有目标(FR_EXIST),旧实现一切
|
||||
* "目标存在的覆盖写"必然失败(E-01 台架验证暴露)
|
||||
* "目标存在的覆盖写"必然失败(台架验证暴露)
|
||||
* v1.3 2026-10-02 王建锋 四个后端函数纳入卷锁 try 语义(vol_lock_wait(0)):
|
||||
* 与 ftpTask 的 lftpd 文件操作互斥(并发 FTL 写 →
|
||||
* dhara journal 竞争实证);busy 返回 -1 由调用方
|
||||
* 按既有失败语义处理(poll 保留 RAM/保存待重试),
|
||||
* 本任务 = 看门狗喂狗点,绝不阻塞等锁
|
||||
* v1.4 2026-10-05 王建锋 原子写临时文件由 p_path 推导(P3-2):与目标同目录的
|
||||
* v1.5 2026-10-24 王建锋 仅注释更正:cfg_store_read 的"限定条件"引用了
|
||||
* FRESULT 里不存在的 FR_NO_SYSTEM,改成实际会返回的
|
||||
* FR_NOT_READY / FR_NO_FILESYSTEM 并写明"非 FR_OK 一律
|
||||
* 转 -1,不要枚举错误码集合"(无行为改动)
|
||||
* v1.4 2026-10-05 王建锋 原子写临时文件由 p_path 推导:与目标同目录的
|
||||
* 固定 8.3 名 config.TMP,不再硬编码卷根;目标恒为
|
||||
* "/config.ini" 时推导结果与旧值逐字节一致
|
||||
*/
|
||||
@@ -47,7 +51,15 @@ static const char s_path_tmp[] = "/config.TMP";
|
||||
* cap - 缓冲容量 size_t
|
||||
* p_nread - 实际读字节数输出(不可为 NULL) size_t *
|
||||
* 返回值:0 - 成功;-1 - 文件缺失/读失败 int
|
||||
* 限定条件:卷已挂载(未挂载时 f_open 返回 FR_NO_SYSTEM,转 -1)
|
||||
* 限定条件:卷已挂载。未挂载时本函数一律转 -1,但**别照着不存在的错误码去写
|
||||
* 判据**:Lib/FatFs/ff.h 的 FRESULT 共 20 个码(FR_OK..FR_INVALID_PARAMETER),
|
||||
* 没有 FR_NO_SYSTEM(旧注释此处笔误,2026-10-24 更正)。本移植里卷未挂载
|
||||
* 实际返回 FR_NOT_READY(3)(物理驱动器不可用)或 FF_VOLUMES 内无已挂载卷时
|
||||
* 由 mount_volume 直接给错;卷上无有效 FAT 则返回 FR_NO_FILESYSTEM(13)。
|
||||
* 本函数(以及 write_atomic / stat / remove)对**任何**非 FR_OK 一律转 -1,
|
||||
* 所以这些码的差异不影响对外语义——也正因如此,谁都不该去枚举错误码集合,
|
||||
* 除了自愈触发集那种"必须区分能不能靠删文件修好"的场合(见 sys_log_mcu.c
|
||||
* 的 s_is_selfheal_code)。
|
||||
* 函数说明:FIL 为**函数级 static**而非局部——本 FatFs 移植 FF_FS_TINY=0,
|
||||
* `FIL` 内含 `buf[FF_MAX_SS]`=512 B(Lib/FatFs/ff.h),一个局部对象就吃掉
|
||||
* 528 B 栈,是配置/syslog 调用链栈深的主要来源(2026-10-10 栈预算整改)。
|
||||
@@ -86,7 +98,7 @@ int cfg_store_read(const char *p_path, uint8_t *p_buf, size_t cap, size_t *p_nre
|
||||
#define CFG_STORE_TMP_PATH_MAX 24u
|
||||
|
||||
/*
|
||||
* 函数功能:由目标路径推导同目录的临时文件路径(P3-2)
|
||||
* 函数功能:由目标路径推导同目录的临时文件路径
|
||||
* 入口参数:p_path - 目标文件路径 const char *
|
||||
* p_out - 输出缓冲 char *
|
||||
* cap - 缓冲容量 size_t
|
||||
@@ -132,7 +144,7 @@ int cfg_store_write_atomic(const char *p_path, const uint8_t *p_buf, size_t len)
|
||||
if (p_buf == NULL) {
|
||||
return -1;
|
||||
}
|
||||
/* P3-2:临时文件由 p_path 推导(同目录 + 固定 8.3 名),不再硬编码卷根 */
|
||||
/* 临时文件由 p_path 推导(同目录 + 固定 8.3 名),不再硬编码卷根 */
|
||||
if (s_build_tmp_path(p_path, tmp_path, sizeof(tmp_path)) != 0) {
|
||||
return -1;
|
||||
}
|
||||
@@ -153,7 +165,7 @@ int cfg_store_write_atomic(const char *p_path, const uint8_t *p_buf, size_t len)
|
||||
}
|
||||
/* 本 FatFs 移植的 f_rename 对已存在目标返回 FR_EXIST、不覆盖(ff.c
|
||||
* "new name already in use" 检查,上游同语义)——"目标存在的覆盖写"必须先
|
||||
* unlink 目标再 rename。2026-09-22 台架实证(E-01 验证暴露):缺本步时
|
||||
* unlink 目标再 rename。2026-09-22 台架实证:缺本步时
|
||||
* 一切非首启写(UI 保存/模板重建)静默失败,模拟器后端(POSIX rename
|
||||
* 覆盖语义)照不出。unlink→rename 非原子窗口掉电 = 配置文件暂失,
|
||||
* 开机 load 按"首启"重建工厂模板——与函数失败语义一致,丢失边界 =
|
||||
@@ -250,7 +262,7 @@ const char *cfg_store_tmp_path(void) {
|
||||
* 返回值:缓冲指针(内容未清零);失败返回 NULL void *
|
||||
* 限定条件:任务上下文调用(heap_4 内部临界区自护);释放必须用 cfg_store_free
|
||||
* 函数说明:MCU 端 MicroLIB C 堆仅 512B(startup Heap_Size=0x200),共享核心的 KB 级
|
||||
* 缓冲若走 C 库 malloc 必然返回 NULL,故统一由本接口取 FreeRTOS 堆(P0-1)
|
||||
* 缓冲若走 C 库 malloc 必然返回 NULL,故统一由本接口取 FreeRTOS 堆
|
||||
*/
|
||||
void *cfg_store_alloc(size_t size) {
|
||||
return pvPortMalloc(size);
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* 模块名称:Volume Idle Sync(写回缓存空闲 flush)— MCU 实现
|
||||
* 模块功能:就绪门控 + 周期节流 + 卷锁 try + disk_ioctl(CTRL_SYNC) 的兜底
|
||||
* 状态机(设计/使用约定/边界见 vol_idle_sync.h)
|
||||
* 适用平台:STM32F407ZGTx + FreeRTOS
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-25
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 DTU 项目组 创建(道F 批 2:写回缓存空闲 flush 收口)
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
#include "vol_idle_sync.h"
|
||||
|
||||
#include "test_config.h"
|
||||
|
||||
#if !defined(TEST_SUITE_STORAGE)
|
||||
|
||||
#include "cmsis_os.h"
|
||||
#include "FreeRTOS.h"
|
||||
#include "task.h"
|
||||
#include "ff.h" /* FatFs 基础类型(diskio.h 依赖 BYTE/LBA_t 等,须先纳) */
|
||||
#include "diskio.h" /* disk_ioctl / CTRL_SYNC / DRESULT */
|
||||
#include "vol_lock.h"
|
||||
#include "sys_events.h"
|
||||
/* 本模块日志仅一条失败留痕(≤60 字符),默认行缓冲即可,不调 DBG_LINE_BUF_MAX */
|
||||
#include "dbg_log.h"
|
||||
|
||||
/* Private defines -----------------------------------------------------------*/
|
||||
/* 空闲同步检查周期(= 漏网脏页的最大掉电驻留窗,另加一个主循环拍 ≈50ms)。
|
||||
* 取值依据:clean 态为"脏标志 + journal"双短路(零 NAND 触点),高频不伤介质
|
||||
* 只是白跑;脏态最多一次页编程 + journal 收账,与 f_close 同型,但落在卷
|
||||
* 已空闲的窗口、远离活跃写会话。5 s 与主循环其余慢观测(栈/堆水位 2 s)同
|
||||
* 量级而更稀疏——本模块是兜底网,不是持久性主路径(主路径 = close 内同步)。
|
||||
* 锁忙(卷不空闲)同样按本周期重试:不做欠账补偿,节奏恒定好推理。 */
|
||||
#define VOL_IDLE_SYNC_PERIOD_MS 5000u
|
||||
|
||||
/* 静态变量定义区 */
|
||||
/* 状态量放文件级 static(非函数内):主机侧白盒单测(test/host/vol_idle_sync/)
|
||||
* 以 #include 纳入本文件后需要复位它们;命名带 vol 前缀避免与测试桩撞名 */
|
||||
static uint32_t s_vol_sync_last_ms; /* 上次尝试时刻(tick ms,uint32 回绕) */
|
||||
static uint8_t s_vol_sync_armed; /* 0 = 从未尝试(首拍直接放行) */
|
||||
static uint8_t s_vol_sync_fail_latch; /* 失败留痕边沿 latch:一个 episode 一条 */
|
||||
|
||||
/* 函数定义区 */
|
||||
|
||||
/*
|
||||
* 函数功能:存储空闲周期同步轮询(就绪门控 → 节流 → try 卷锁 → CTRL_SYNC)
|
||||
* 入口参数:无 void
|
||||
* 返回值:无 void
|
||||
* 限定条件:仅 defaultTask 主循环调用;内部绝不阻塞(try 锁 + 常数级耗时,
|
||||
* 最坏成本 = 一次 f_close 的同步段)
|
||||
* 函数说明:四级门,逐级变贵:
|
||||
* ① 就绪门:sys_evtHandle 未建或 STORAGE_READY 未置 → 静默
|
||||
* (挂载前 disk_ioctl 只会 RES_NOTRDY,且 bring-up 失败重试期
|
||||
* 卷状态未定,一律不碰);
|
||||
* ② 节流门:距上次尝试不足 VOL_IDLE_SYNC_PERIOD_MS → 返回。
|
||||
* 无符号差值判据((uint32)(now - last) < 周期)天然跨 32 位回绕
|
||||
* 正确,与 stack_probe_poll 同一 idiom;勿"优化"成大小比较;
|
||||
* ③ 空闲门:vol_lock_wait(0) try——锁被持有 = 有文件操作序列在做
|
||||
* (syslog 落盘 / 配置保存 / lftpd open...close),本轮跳过,
|
||||
* 绝不改变"文件持有期内不同步"的既有语义;
|
||||
* ④ 动作:disk_ioctl(0, CTRL_SYNC)——FatFs 官方幂等落盘原语
|
||||
* (f_close 尾部同一条路径):FTL 脏页刷入 + dhara_map_sync。
|
||||
* clean 时两级短路立即返回,零 NAND 操作。
|
||||
* 失败留痕:RES_ERROR 只按"边沿"打一条 DBG_ERROR(latch),持续失败
|
||||
* 不刷屏;下一次成功后 latch 复位,新 episode 再报。底层失败形态
|
||||
* (Cache flush FAIL / Map sync failed)已由 nand_ftl 就地打点,
|
||||
* 本条只补"空闲路径也救不回"的上下文,不重复堆日志锚点。
|
||||
*/
|
||||
void vol_idle_sync_poll(void) {
|
||||
const uint32_t now = (uint32_t)xTaskGetTickCount();
|
||||
DRESULT dr;
|
||||
|
||||
/* ① 就绪门 */
|
||||
if (sys_evtHandle == NULL) {
|
||||
return;
|
||||
}
|
||||
if ((osEventFlagsGet(sys_evtHandle) & SYS_EVT_STORAGE_READY) == 0u) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* ② 节流门(回绕安全判据,见函数说明②) */
|
||||
if ((s_vol_sync_armed != 0u)
|
||||
&& ((uint32_t)(now - s_vol_sync_last_ms)
|
||||
< (uint32_t)pdMS_TO_TICKS(VOL_IDLE_SYNC_PERIOD_MS))) {
|
||||
return;
|
||||
}
|
||||
s_vol_sync_last_ms = now;
|
||||
s_vol_sync_armed = 1u;
|
||||
|
||||
/* ③ 空闲门:try 锁,拿不到 = 卷不空闲,本轮跳过(下周期再试) */
|
||||
if (vol_lock_wait(0u) == 0u) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* ④ 幂等落盘:脏页刷入 + journal 收账;clean 时零 NAND 触点 */
|
||||
dr = disk_ioctl(0, CTRL_SYNC, NULL);
|
||||
vol_lock_release(); /* 成败都放锁:同步失败不产生锁泄漏 */
|
||||
|
||||
if (dr != RES_OK) {
|
||||
if (s_vol_sync_fail_latch == 0u) {
|
||||
s_vol_sync_fail_latch = 1u;
|
||||
DBG_ERROR("[FS] idle sync FAIL (dr=%d)", (int)dr);
|
||||
}
|
||||
} else {
|
||||
s_vol_sync_fail_latch = 0u; /* 成功:重新武装,下个 episode 可再报 */
|
||||
}
|
||||
}
|
||||
|
||||
#else /* TEST_SUITE_STORAGE */
|
||||
|
||||
/*
|
||||
* 函数功能:STORAGE 套件构建下的空实现(编译期裁剪落点)
|
||||
* 入口参数:无 void
|
||||
* 返回值:无 void
|
||||
* 限定条件:仅当 test_config.h 选中 TEST_SUITE_STORAGE 时编入本分支
|
||||
* 函数说明:该构建里挂载/盘写由 testTask 的 storage_test_run 独占(测试含
|
||||
* nand_ftl_deinit 掉电模拟),后台同步与它并发会互相踩坏 FTL/FatFs
|
||||
* 状态——必须彻底静默,而非仅靠运行时门控。
|
||||
*/
|
||||
void vol_idle_sync_poll(void) {
|
||||
}
|
||||
|
||||
#endif /* TEST_SUITE_STORAGE */
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* 模块名称:Volume Idle Sync(写回缓存空闲 flush)
|
||||
* 模块功能:随 defaultTask 主循环周期轮询,在存储**空闲**时对 NAND FatFs 卷
|
||||
* 补发一次 CTRL_SYNC(FatFs 幂等落盘语义 = FTL 单页写回缓存脏页刷入
|
||||
* + dhara journal 待办收账,见 docs/架构设计/STM32F4-Base存储架构说明.md
|
||||
* §5.5),作为写回缓存持久性的兜底网:
|
||||
* - 正常写事务语义不变:f_close 内部 sync_fs 已同步,本模块零干预;
|
||||
* - 价值在漏网兜底:任何"缓存留脏、之后再无调用触发 sync"的状态
|
||||
* (异常路径中途放锁等)其掉电驻留窗从"无限期,直到下一笔写"
|
||||
* 收敛为"不超过本模块周期";
|
||||
* - 卷不空闲(卷锁被持有:syslog 落盘 / 配置保存 / lftpd
|
||||
* open...close 序列进行中)本轮跳过——不改变文件持有期(如 FTP
|
||||
* STOR 进行中)的既有语义。
|
||||
* clean 态零 NAND 触点(脏标志与 journal 双短路);脏态成本与一次
|
||||
* f_close 同型。周期 5 s(.c 内 VOL_IDLE_SYNC_PERIOD_MS)。
|
||||
* ⚠ 边界(勿当成掉电保护):本板无掉电检测硬件,"掉电前强制 flush"
|
||||
* 不可行;"单次写事务进行中掉电"的丢失边界也不因本模块改变。
|
||||
* 适用平台:STM32F407ZGTx + FreeRTOS(CMSIS-RTOS V2);仅 MCU 端,
|
||||
* 主机侧 L0.5 单测 test/host/vol_idle_sync/
|
||||
* 作者:DTU 项目组
|
||||
* 创建日期:2026-10-25(道F 批 2:写回缓存空闲 flush 收口)
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-25 DTU 项目组 创建:STORAGE_READY 门控 + 周期节流 +
|
||||
* 卷锁 try + disk_ioctl(CTRL_SYNC) + 失败边沿 latch
|
||||
*/
|
||||
|
||||
#ifndef VOL_IDLE_SYNC_H
|
||||
#define VOL_IDLE_SYNC_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*
|
||||
* 函数功能:存储空闲周期同步的轮询入口(内部自带就绪门控 / 节流 / 非阻塞
|
||||
* try 锁 / clean 短路,调用点每轮无条件调用即可)
|
||||
* 入口参数:无 void
|
||||
* 返回值:无 void
|
||||
* 限定条件:仅 defaultTask 主循环调用(存储属主任务;本函数内部绝不阻塞——
|
||||
* defaultTask 是看门狗喂狗点,见 vol_lock.h 使用约定);
|
||||
* TEST_SUITE_STORAGE 构建下为空实现(卷由 testTask 独占,后台同步
|
||||
* 必须彻底静默)
|
||||
*/
|
||||
void vol_idle_sync_poll(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* VOL_IDLE_SYNC_H */
|
||||
+61
-2
@@ -6,6 +6,13 @@
|
||||
* 创建日期:2026-10-02
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-02 王建锋 创建
|
||||
* v1.1 2026-10-26 DTU 项目组 道G task-17:① 新增 4 个观测计数(T-a/T2/T3/T4,
|
||||
* 共 16 B 静态)与 vol_lock_get_stats() 只读口 —— 不取锁、
|
||||
* 零外部调用、零局部变量,13 处调用点一字未动;
|
||||
* ② 修 :38「返回值」注释与代码相反的错(fail-open 原被
|
||||
* 归给 0,实际返 1);③ 刻意不记时刻:时刻要读 tick,
|
||||
* 等于给 defaultTask 取锁链添帧,改由消费方观测并自盖
|
||||
* (命名 obs_first_ms/obs_last_ms,见 vol_lock.h 告诫 ②)
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
@@ -16,6 +23,22 @@
|
||||
/* 静态变量定义区 */
|
||||
static osMutexId_t s_lock; /* 卷串行化互斥锁(vol_lock_init 创建) */
|
||||
|
||||
/* 观测计数(道G task-17「卷锁 5 态可观测性」)—— 本模块唯一的新增状态,共 16 B。
|
||||
* 为什么计数必须在锁层、而且只有计数没有别的:
|
||||
* ① 13 处调用点各自 return、历史上零痕;在每个调用点各写一份 = 13 份真源。
|
||||
* ② 不能在这里打日志或投记录:唯一能落盘的通道(/log/wdev.log、run.log)自己也要
|
||||
* 取这把卷锁,而卷锁**非递归** —— 被卷忙挡住时既写不出东西,还会自取死锁。
|
||||
* ③ 也不读 tick 记时刻:那会给 defaultTask 的取锁链添一个 callee 与几字节帧,而
|
||||
* 这条链的栈余量正是要省着花的;"什么时候开始忙"交给消费方观测(口径见 vol_lock.h
|
||||
* 的告诫 ②:观测时刻不等于发生时刻,别写成首次发生)。
|
||||
* 并发:多个任务可同时自增同一个计数,竞态下少计不会多计(判据是"是否 > 0 / 是否还
|
||||
* 在增长",不是精确值),所以不为此上临界区 —— 上临界区反倒把观测做成了故障本身。
|
||||
* volatile:跨任务读写且无锁保护,禁止把读侧重复读优化掉。 */
|
||||
static volatile uint32_t s_st_acquired; /* T-a 真取到互斥 */
|
||||
static volatile uint32_t s_st_try_timeout; /* T2 try(timeout=0)失败:卷被别人占住 */
|
||||
static volatile uint32_t s_st_blk_timeout; /* T3 分片等待超时:ftpTask 正常退让 */
|
||||
static volatile uint32_t s_st_failopen; /* T4 锁不存在却返 1:谎报成功那一态 */
|
||||
|
||||
/* 函数定义区 */
|
||||
|
||||
/*
|
||||
@@ -35,18 +58,35 @@ int vol_lock_init(void) {
|
||||
/*
|
||||
* 函数功能:获取卷锁(首次调用自动 vol_lock_init)
|
||||
* 入口参数:timeout_ms - 等待上限(毫秒);0 = try 语义(不阻塞) uint32_t
|
||||
* 返回值:1 - 已持锁;0 - 超时/未取得,或锁创建失败(fail-open) int
|
||||
* 返回值:1 - 已取得互斥,**或锁不存在的 fail-open(返 1 但此刻并未串行化)**;
|
||||
* 0 - 未取得(timeout=0 的 try 失败,或 timeout>0 的分片等待超时) int
|
||||
* 限定条件:返回 0 时调用方**不得** vol_lock_release;fail-open 是为保证
|
||||
* 看门狗喂狗点(defaultTask)绝不能因等锁而阻塞
|
||||
* 函数说明:⚠ 原 v1.0 这行「返回值」把 fail-open 归给 0,与本函数 :46 的代码和
|
||||
* TC-VOLLOCK-008 都相反(道G task-17 修正)。"返 1"有两种成因,调用方
|
||||
* 无需各自分辨(它们只关心能不能干活),但整机的判据必须看
|
||||
* vol_lock_get_stats().failopen —— 累计 > 0 = 本 boot 至少发生过一次
|
||||
* "无互斥读写卷"。本函数只自增计数:不打日志、不投记录、不读 tick、
|
||||
* 不新增局部变量(理由见上面计数块)。
|
||||
*/
|
||||
int vol_lock_wait(uint32_t timeout_ms) {
|
||||
if (s_lock == NULL) {
|
||||
(void)vol_lock_init();
|
||||
if (s_lock == NULL) {
|
||||
s_st_failopen++; /* T4:返 1 但零互斥 —— 谎报成功那一态 */
|
||||
return 1; /* 锁创建失败:fail-open(不阻塞,保持启动段可用) */
|
||||
}
|
||||
}
|
||||
return (osMutexWait(s_lock, timeout_ms) == osOK) ? 1 : 0;
|
||||
if (osMutexWait(s_lock, timeout_ms) == osOK) {
|
||||
s_st_acquired++; /* T-a:真持锁 */
|
||||
return 1;
|
||||
}
|
||||
if (timeout_ms == 0u) {
|
||||
s_st_try_timeout++; /* T2:压根没等(喂狗点/绘制路径只有这一态会发生) */
|
||||
} else {
|
||||
s_st_blk_timeout++; /* T3:等了一会儿没等到(分片退让),语义与 T2 不同 */
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -60,3 +100,22 @@ void vol_lock_release(void) {
|
||||
(void)osMutexRelease(s_lock);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读卷锁观测计数(契约与消费方告诫见 vol_lock.h 的函数头)
|
||||
* 入口参数:p_out - 输出缓冲;NULL 时无害返回 vol_lock_stats_t *
|
||||
* 返 回 值:无 void
|
||||
* 限定条件:不取卷锁、不关中断、不阻塞;**零外部调用、零局部变量**,因此不改变
|
||||
* 任何调用方的栈最坏链(这条是本卡选型的结论,不要"顺手优化"掉)
|
||||
* 函数说明:逐字段拷而非整体赋值 —— 每个字段都是某个真实值,不会读到半更新的字;
|
||||
* 四元组之间不保证同一瞬间(诊断够用,理由见 .h 的告诫 ①)
|
||||
*/
|
||||
void vol_lock_get_stats(vol_lock_stats_t *p_out) {
|
||||
if (p_out == NULL) {
|
||||
return;
|
||||
}
|
||||
p_out->acquired = s_st_acquired;
|
||||
p_out->try_timeout = s_st_try_timeout;
|
||||
p_out->blk_timeout = s_st_blk_timeout;
|
||||
p_out->failopen = s_st_failopen;
|
||||
}
|
||||
|
||||
+62
-1
@@ -13,6 +13,13 @@
|
||||
* 长耗时恢复旋转饥饿 defaultTask 主循环(喂狗点)
|
||||
* → IWDG 误复位;单写者卷属主假设在多任务 FatFs
|
||||
* 调用下不成立,须显式串行化)
|
||||
* v1.1 2026-10-26 DTU 项目组 道G task-17:① 修契约——vol_lock.c 原 :38 的
|
||||
* 「返回值」把 fail-open 归给 0,而代码(:46)与
|
||||
* TC-VOLLOCK-008 都是返 1;现在 .h/.c 两处都按代码写实,
|
||||
* 并显式警告"返 1 也可能是零互斥";② 新增 5 态计数与
|
||||
* vol_lock_get_stats() 只读口(不取锁、任意上下文可调);
|
||||
* ③ 使用约定补「可观测性」段(为何计数在锁层、为何不得
|
||||
* 在失败分支里取锁打日志)
|
||||
*
|
||||
* 使用约定(铁律):
|
||||
* - 锁跨越"一次完整文件操作序列"(open...close / opendir...closedir /
|
||||
@@ -23,6 +30,12 @@
|
||||
* - ftpTask 调用方用 100ms 分片阻塞等待 + 每片心跳(lftpd_task_wd_beat),
|
||||
* 防长等待导致 FTP 心跳位缺席误复位;
|
||||
* - 锁在内核启动后由 defaultTask 存储 bring-up 创建(vol_lock_init 幂等)。
|
||||
*
|
||||
* 可观测性(道G task-17):锁层自带 5 态计数 + `vol_lock_get_stats()` 只读口。
|
||||
* 为什么计数必须在**锁层**:唯一能把事写下来的通道(`/log/wdev.log` 等)自己也要
|
||||
* 取这把锁,而卷锁**非递归**——被卷忙挡住时既写不出日志,也不许在失败分支里再取
|
||||
* 同一把锁。所以锁层只自增计数;一次性留痕交给 defaultTask(syslog 唯一写者)按边沿
|
||||
* 补投自检记录。调用方 13 处 try 的分支与返回值一字不动(不动分支 = 不动行为)。
|
||||
*/
|
||||
|
||||
#ifndef VOL_LOCK_H
|
||||
@@ -45,11 +58,59 @@ int vol_lock_init(void);
|
||||
/*
|
||||
* 函数功能:获取卷锁
|
||||
* 入口参数:timeout_ms - 等待超时(0 = 非阻塞 try) uint32_t
|
||||
* 返回值:1 - 已获取;0 - 超时未获取(卷被占用) int
|
||||
* 返回值:1 - 已取得互斥,**或锁不存在的 fail-open(此时并未串行化)**;
|
||||
* 0 - 未取得(try 超时,或分片等待超时) int
|
||||
* 限定条件:见文件头使用约定(defaultTask 必须 timeout_ms=0)
|
||||
* 函数说明:⚠ 道G task-17 修掉的契约缺口——本函数只有 0/1 两值,而"返 1"有两种
|
||||
* 成因:真取到锁,与 `osMutexNew` 失败时的 fail-open(返 1 但对卷的访问
|
||||
* **零互斥**)。要求 13 处调用方各自区分是不现实的(它们只关心"能不能干活"),
|
||||
* 判据请改用 `vol_lock_get_stats()` 的 `failopen` 计数:累计 > 0 =
|
||||
* 本 boot 内至少发生过一次"无互斥读写卷"。
|
||||
* 本函数内只**自增计数**:不打日志、不投记录、不阻塞(理由见 .c 计数块)。
|
||||
*/
|
||||
int vol_lock_wait(uint32_t timeout_ms);
|
||||
|
||||
/*
|
||||
* 卷锁观测计数(道G task-17「卷锁 5 态可观测性」)。四个字段就是四态,语义**不得**合并:
|
||||
* T-a `acquired` = 真取到互斥的次数
|
||||
* T2 `try_timeout` = timeout=0 的 try 失败次数(卷被别的任务占住;defaultTask/uiTask
|
||||
* 的喂狗与绘制路径只有这一态会发生)
|
||||
* T3 `blk_timeout` = timeout>0 的分片等待超时次数(ftpTask 100 ms 分片正常退让,
|
||||
* 语义与 T2 不同:那是"等了一会儿没等到",这是"压根没等")
|
||||
* T4 `failopen` = 锁不存在却返 1 的次数(**谎报成功**那一态,见 wait 的函数头)
|
||||
* T1「从未调用过」不占计数:三个计数全 0 就说明这条路径没跑过——区分"没跑过"与
|
||||
* "跑了但一直忙"正是本卡的目的(此前两者在日志上完全一样)。
|
||||
* ⚠ 本层**刻意不记时刻**:记时刻就得读 tick,等于给 defaultTask 的取锁链添一个
|
||||
* callee 与几字节帧,而这条链的余量我们整天都在省着花(诊断不值那几个字节)。
|
||||
* 代价是"什么时候开始忙"只能由消费方观测,见下面 get_stats 的第② 条告诫。
|
||||
*/
|
||||
typedef struct {
|
||||
uint32_t acquired;
|
||||
uint32_t try_timeout;
|
||||
uint32_t blk_timeout;
|
||||
uint32_t failopen;
|
||||
} vol_lock_stats_t;
|
||||
|
||||
/*
|
||||
* 函数功能:读卷锁观测计数(纯拷字段)
|
||||
* 入口参数:p_out - 输出缓冲;NULL 时无害返回 vol_lock_stats_t *
|
||||
* 返 回 值:无 void
|
||||
* 限定条件:任意上下文可调——**不取卷锁**、不关中断、不阻塞(卷被长期占住时正是最
|
||||
* 需要读这几位的时刻,观测口再抢锁就把观测做成故障的一部分)
|
||||
* 函数说明:① 四个字段各是 32 位对齐量,Cortex-M4 上单字读写原子,但**不保证是同一
|
||||
* 瞬间的四元组快照**(诊断够用)。计数本身跨任务读-改-写,竞态下可能
|
||||
* **少计不会多计** → 判据用「是否 > 0 / 与上一轮比是否增长」,别拿精确值当账。
|
||||
* ② ⚠ **给消费方的告诫**(消费方 = 看门狗/自检记录道,接口按此写就不会错):
|
||||
* 本层不产出时刻。消费方若自行盖时间戳,那是「**被观测到的时刻**」,不是
|
||||
* 「**发生的时刻**」——真实发生可以早到一整个轮询周期之前。命名请用
|
||||
* `obs_first_ms` / `obs_last_ms`(观测时刻)这类,**别用** `first_ms`/`last_ms`,
|
||||
* 文案里也别写「首次/末次发生」(那就是新的谎报)。
|
||||
* 一次性留痕自己做边沿:照 `sys_log.h` 的 `syslog_post_dropped()` 先例(唯一
|
||||
* 写者按差值补发,一个 episode 一条),锁层自身绝不产日志、不投记录。
|
||||
* ③ 本函数零外部调用、零局部变量(除出参),因此不改变任何调用方的栈最坏链。
|
||||
*/
|
||||
void vol_lock_get_stats(vol_lock_stats_t *p_out);
|
||||
|
||||
/*
|
||||
* 函数功能:释放卷锁(获取方任务释放)
|
||||
* 入口参数:无
|
||||
|
||||
+62
-1
@@ -7,7 +7,7 @@
|
||||
|
||||
| 文件 | 作用 |
|
||||
|---|---|
|
||||
| `sys_log.h` | **日志接口(平台无关)**:定长 64B 记录写入 `/log/run.log`·`selftest.log` |
|
||||
| `sys_log.h` | **日志接口(平台无关)**:定长 64B 记录,七类各一个 `/log` 文件(run / selftest / accid / alarm / start / oper / yk)|
|
||||
| `sys_log_mcu.c` | `sys_log` 的 MCU 端后端实现(NAND + FatFs) |
|
||||
| `console_log.c/h` | **控制台单写者日志**:多任务日志经队列汇入唯一写者,避免串口输出交错 |
|
||||
| `soe_rec.c/h` | **SOE 记录纯编解码**(B9):64B 定长记录(CP56Time2a 时标 + 类型 + 元件号 + 单调序号 + 值 + sum8 撕裂校验),零 I/O 零 OS 依赖,主机单测覆盖 |
|
||||
@@ -23,6 +23,67 @@
|
||||
内核启动后由 `syslog_drain` 惰性创建真队列并排空
|
||||
- ⚠️ 写盘必须校验 `f_write` 返回的**字节数**(该 FatFs 移植在簇分配失败时返回 `FR_OK` + 短写,
|
||||
不报错误码);追加写必须 `FA_WRITE|FA_OPEN_APPEND`(`FA_OPEN_APPEND` 单独用**不含写位**)
|
||||
- **清除记录 = 请求 / 落地分离**:非写者上下文(uiTask 等)只能 `syslog_request_clear(id)`
|
||||
置一个标志(零盘触点、零卷锁),真正的 `f_unlink` 在 **defaultTask** 的 `syslog_drain()`
|
||||
**尾部**执行——顺序也是判据的一部分:排在条目排空之后,否则本轮刚排空的旧记录会被
|
||||
写回去、`syslog_count()` 不归零。结果读 `syslog_clear_state(id)`
|
||||
(`NONE/OK/WAIT/FAIL`),事实复核读 `syslog_count()`。
|
||||
落地函数 `syslog_clear_apply(id)` 是 MCU 后端专属(模拟器不实现 → 共享代码误调在
|
||||
链接期就报错):卷锁一律 try,卷占用返 -1 且**不动原文件**;幂等只认 `FR_NO_FILE`,
|
||||
`FR_NO_PATH` 如实返 -1 且**不做任何卷级动作**(不补目录、不重格式化)
|
||||
- 删除的作用域 = 上表七个记录文件(`syslog_request_clear(id)` 逐 id 独立请求);取证文件
|
||||
`wdev.log` 与 `soe.log` 各有各的属主与通道,不在本 API 作用域,想清它们要另开 API。
|
||||
清除成功会往**操作记录**里落一条 `clear <文件>`——但清 `oper.log` 自己时**刻意不投**,
|
||||
否则"我清除了记录"这条会把刚删空的文件又写成 1 条,直接砸掉「删除后 `syslog_count()`
|
||||
归零」这条判据(`TC-SYSLOG-133b` 钉住)
|
||||
- ⚠ 宿主后端(`simulator/sim/sim_syslog.c`)与 MCU 端**判据相同、时序不同**:模拟器单线程,
|
||||
七类对应工作目录下七份宿主文件(`sim_accid.log` 等),`syslog_post_quiet()` 在本端
|
||||
等价于 `syslog_post()`(宿主进程没有那条栈硬线,也没有队列与丢弃);
|
||||
`syslog_request_clear()` 调用内就删完了,因此 `WAIT` 态在模拟器上永不出现
|
||||
(同 `syslog_post` 在两端"有队列 / 直写无队列"的差异)
|
||||
|
||||
### 五类记录与「记录类型 = 日志 ID」(道F task-10,2026-10-26;task-11 记录 5 页按此做)
|
||||
|
||||
**决策一:记录类型就是 `syslog_id_t` 本身,一类记录 = `/log` 下一个严格 8.3 短名的文件。**
|
||||
64B 定长记录体里**没有类型字段位**:`[0..18]` 时标 / `[19]` 空格 / `[20..29]` epoch /
|
||||
`[30]` 空格 / `[31..58]` 描述 / 尾部补零,这个布局被 UI 记录页与主机解码工具按**固定偏移**
|
||||
读了了很久(run.log / selftest.log 是既有文件,既有读者)。塞类型字节要么挪走 epoch
|
||||
(上电/重启分类就断),要么挪走描述(屏上少字),且两端读者同批改——所以
|
||||
"这条记录是什么类型"由**它在哪个文件里**回答。`TC-SYSLOG-136` 把这条钉成机器断言:
|
||||
同一条内容在七类之间逐字节相同,记录体不编码类型。
|
||||
|
||||
**决策二:不动 `soe_type_t`(二进制 SOE 通道保持现有 8 型)。** 加 SOE 类型必须同步 UI 侧
|
||||
`ui_w_soe.c` 的类型名表(那是另一位队友的写域),而 SOE 页与这五页本就是两套读者
|
||||
(二进制解码器 vs 文本列表)。**别把新类型顺手加进 SOE**:那会同时造出
|
||||
"同一个事件两个真源"。
|
||||
|
||||
| 记录 | 日志 ID / 文件 | 容量上限 | 写入点 | 触发判据 |
|
||||
|---|---|---|---|---|
|
||||
| 事故 | `SYSLOG_ACCIDENT` / `accid.log` | 512 | `App/task/protect_task.c:181` | `asserted` 上升沿**且**该元件跳闸投退项 = 1 |
|
||||
| 保护告警 | `SYSLOG_ALARM` / `alarm.log` | 1024 | 同文件 `184` | `asserted` 上升沿**且**跳闸投退项 = 0 或该元件无此项 |
|
||||
| 保护启动 | `SYSLOG_START` / `start.log` | 1024 | 同文件 `176` | `picked` 上升沿(越限电平,早于/等于动作) |
|
||||
| 操作 | `SYSLOG_OPER` / `oper.log` | 512 | `App/protocol/iec104/iec104_pts.c:574/602`、`App/task/default_task.c:242`、`App/log/sys_log_mcu.c:646`(宿主 `sim_syslog.c:245`) | 强制复归受理 / 软压板投退落地 / 定值**真落盘成功** / 清除记录成功 |
|
||||
| 遥控 | `SYSLOG_YK` / `yk.log` | 256 | `App/protocol/iec104/iec104_pts.c:573/576/582/586/596/598/609` | 104 双点命令的选择段、执行段、非法 DPC、未知 IOA 各一条 |
|
||||
|
||||
- **"哪些元件算跳闸"只有一个真源**:定值表 `g_prot_set_meta` 里的 `PS_*_TRIP`(枚举注释
|
||||
即"0 告警 / 1 跳闸")+ 实例表 `k_pe[]` 的 `trip_ctl` 列。分类入口
|
||||
`prot_elm_record_kind()`(`App/algo/prot_elm.c`)里**没有第二份元件名单**,
|
||||
没有该投退项的五项(OV/UV/3U0/OF/UF)填 `PROT_ELM_NO_TRIP=0xFF` → 恒告警;
|
||||
`TC-PROTELEM-015` 逐元钉住"逐项独立 / 改定值即改类别 / 无此项者不受别人定值影响"。
|
||||
- **描述只用 ROM 常量或 `prot_elm_name()` 直传**:所有写入点零 `snprintf`、零拼接。
|
||||
最长一条 20 字节 < `SYSLOG_DESC_MAX`(28);`TC-IEC104-030` 逐条量长度(超 28 会被
|
||||
投递侧静默截断,屏上少几个字,没人会去数)。
|
||||
- ⚠️ **两个投递口,按调用者的栈预算选**:`syslog_post()` 的两个"满态丢弃留痕"分支
|
||||
内联着日志行缓冲(每处约 216 B 栈),因此 `syslog_post_quiet()` 给静态最坏链吃紧的
|
||||
任务用(**protectTask、iec104Task 必须用它**),丢弃只累加 `syslog_post_dropped()`、
|
||||
由唯一写者 defaultTask 在 `syslog_drain()` 里按差值补一条留痕——同 `soe_store_post`
|
||||
当年那条 `872 → 1192 B FAIL` 的实证纪律。收/丢判据两个口完全一致。
|
||||
- **读侧契约(给 task-11)**:`syslog_count(id)` + `syslog_get_record(id, idx, ts, ts_len,
|
||||
desc, desc_len)` 与运行/自检记录**同一枚 reader**,"最近 20 条"= 从 `count-20` 起读;
|
||||
`count()==0` 就是空态,页面显示"无记录"。**不要**在共享代码里调 MCU 专属的
|
||||
`syslog_clear_apply()`(模拟器不实现 → 链接期报错是故意的);清除一律走
|
||||
`syslog_request_clear()` + `syslog_clear_state()`,并把 `WAIT` 当正常态看(模拟器端
|
||||
永不出现 `WAIT`,见下条)。
|
||||
|
||||
### console_log(控制台单写者)
|
||||
|
||||
|
||||
+365
-49
@@ -8,17 +8,17 @@
|
||||
* 修改记录:
|
||||
* 2026-09-24 王建锋 创建(自 Src/usart.c USER CODE 区迁出,实现逻辑不变,
|
||||
* 仅变更文件位置:HAL 库生成文件不得包含应用代码)
|
||||
* 2026-10-02 王建锋 僵尸通道喂狗节奏保护(台架 gen-288 实证):round-3 的
|
||||
* 2026-10-02 王建锋 僵尸通道喂狗节奏保护(台架日志代次 288 实证):第 3 轮的
|
||||
* "单次 drain 预算 250ms" 只限单次调用——ORE 锁存在外设软复位后
|
||||
* 毫秒级重锁(PA10 悬空 + RX 使能),背靠背 drain 每轮再耗 100ms
|
||||
* 块超时 → 主循环周期拉到 450~500ms → 100ms 节流的喂狗抽样间隙
|
||||
* 超过 IWDG 最坏容限 1.7s → 停喂 → 误复位(且心跳位全员健康、
|
||||
* 无 stall 日志,纯节奏性饥饿)。修复:发送故障后立即结束本轮
|
||||
* drain 并进入 300ms 发送冷却(冷却期内 drain 直接让位主循环),
|
||||
* 保证喂狗节奏 ≤ ~300ms。根治(USART1 TX-only 灭 ORE 机制)
|
||||
* 另行推进(.ioc 结构性修改 + CubeMX 重生成,见 docs/任务规划/登记簿.md S-3)。
|
||||
* 2026-10-02 王建锋 round 8b 加固:发送超时判据改用 DWT CYCCNT(时间源无关)。
|
||||
* 台架 round 8a 取证(见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)定位到:defaultTask
|
||||
* 保证喂狗节奏 ≤ ~300ms。根治(把 USART1 改成只发不收)已于 2026-10-26
|
||||
* 由用户裁定不做,本缓解即长期方案;机制详述 docs/子系统设计/控制台单写者日志设计说明.md §12。
|
||||
* 2026-10-02 王建锋 第 8b 轮加固:发送超时判据改用 DWT CYCCNT(时间源无关)。
|
||||
* 台架第 8a 轮取证(见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)定位到:defaultTask
|
||||
* 冻结在 console 突发行内、无 STALL、复位为 IWDGRSTF。唯一自洽机理 =
|
||||
* HAL_UART_Transmit 的超时判据 `HAL_GetTick() - Tickstart` 依赖
|
||||
* TIM7 的 uwTick —— TIM7 一旦停摆,超时永不成立 → 写者死等 TXE →
|
||||
@@ -31,6 +31,37 @@
|
||||
* adcTask(B1)等周期任务此时落日志会与 defaultTask 的存储初始化
|
||||
* 日志字节级交错(台架实测 [ADC_TASK] 首帧行与 [NAND_FTL] 行交错)。
|
||||
* 提供就绪查询供周期任务门控自身日志,不改直发路径本身。
|
||||
* 2026-10-26 王建锋 **O1 第二个实例(台架实证)修复:裸发路径加「行级单一所有者」**。
|
||||
* 台架同镜像两次启动(fw 戳 Oct 26 2026 16:27:01,排除构建差异):16:28 那次
|
||||
* protectTask 的未标定 INFO 完好,16:37 那次同一行变成
|
||||
* "protection [blocked (UA/UN/IA VALI2D not all s0et)" —— 多出的 3 个字节是
|
||||
* '[' '2' '0',而 DBG 行前缀恰是 '[' + sys_clock_get_str() 的 "2026-…",
|
||||
* **即另一条行的行首三个字符**;串口 framing 错只会改字节、不会让行变长,
|
||||
* 故只可能是另一个写者在同一行内插入。机理 = 写者注册前(内核启动 ->
|
||||
* default_task.c 的 dbg_console_writer_attach,中间隔着整段 storage_bring_up)
|
||||
* 所有任务的整行都走 dbg_console_write_raw 逐字节轮询 USART1->DR,**零互斥**
|
||||
* (docs/子系统设计/控制台单写者日志设计说明.md 在册 O1;20261012_B6 那次
|
||||
* "st[art:" 同一形态)。修法 = con_tx_try_claim()/con_tx_release() 一对
|
||||
* **非阻塞**所有权(只在几条指令的临界区里做 test-and-set,绝不把 7.8 ms
|
||||
* 的发送包进临界区、绝不关中断、绝不等待);抢不到所有权的那条行**不丢**,
|
||||
* 落进既有环形缓冲延后发(竞争失败不该变成丢行,更不该变成 [ERR] 假红——
|
||||
* 那个窗口每 3 次启动命中 2 次),只有环也放不下才计入既有 dropped 账
|
||||
* (真溢出仍然看得见)。顺带堵掉第二处洞:**attach 恰好撞上别人已发出的半行**
|
||||
* ——dbg_console_drain 现在也必须先拿到所有权才开轮,拿不到就让位
|
||||
* (tail 不推进,环里一条不少,下轮 ≤50 ms 后重发,这条路径永不丢)。
|
||||
* 另导出 dbg_console_tx_begin()/tx_end() 供"一条行分多段裸发"的调用方收口
|
||||
* (Src/freertos.c 故障钩子是下一张卡),本模块不改任何调用方文件。
|
||||
* 2026-10-24 王建锋 主机单测(test/host/console_log)照出的四条一并修复:
|
||||
* 冷却改判独立标志 s_con_cooling(旧写法拿 tick 值当"是否在冷却"哨兵,
|
||||
* 故障恰好落在 HAL_GetTick()==0 那一拍时冷却等于没记);
|
||||
* con_append 的"复核空位 + 整行拷贝 + 推进 head"收进同一临界区(旧写法
|
||||
* 判据在区外,多生产者可在环将满时同时过判据 -> head 越过容量把未发出
|
||||
* 的旧行静默覆盖,既不报错也不计丢弃);
|
||||
* drain 失败分诊:Instance==NULL(未初始化)不再走外设软复位自愈(那会
|
||||
* 在 HAL 未就绪时重配 RCC/GPIO),改记 dropped;僵尸通道仍自愈;
|
||||
* 三本账分开(docs §13-O2):新增 dbg_console_tx_timeout(),dropped 只记
|
||||
* 真丢的,tx_fault 仍只记软复位自愈次数;[ERR] dropped 行改为"发成功才
|
||||
* 销账",通道不可用期间不再把丢弃证据一起冲掉。
|
||||
*/
|
||||
|
||||
#include "console_log.h"
|
||||
@@ -55,7 +86,10 @@
|
||||
* ① 行级原子:整行在一次临界区内拷入环形缓冲,写者按块一次 HAL_UART_Transmit
|
||||
* 发出 → 行与行不可能再交错(UART 只有这一个调用点);
|
||||
* ② 指针可控:进 UART 的缓冲恒为本文件的环形缓冲,调用方脏指针再也到不了 UART;
|
||||
* ③ 写者未注册(内核启动早期/裸机)或中断上下文:回退直接阻塞发送(无并发或必须裸发)。
|
||||
* ③ 写者未注册(内核启动早期/裸机)或中断上下文:回退直接发送,但**整行独占**
|
||||
* 由 con_tx_try_claim() 保证(道G task-23:未注册期原来就是"多个任务都直发、
|
||||
* 零互斥",O1 两个实例都出在这里);抢不到独占的那条行不丢,落环形缓冲延后发,
|
||||
* 与当场直发逐字节相同。ISR / 调度器未启动两种上下文放行(不许等)。
|
||||
* 约束:不新增任务、不动态分配;环形缓冲为 BSS(SRAM1,非 CCM),无 DMA 可达性冲突。
|
||||
*/
|
||||
#define DBG_CON_RING_SIZE 4096U /* 环形缓冲字节数(必须为 2 的幂) */
|
||||
@@ -64,13 +98,13 @@
|
||||
#define DBG_CON_TX_TIMEOUT_MS 100U /* 单次发送超时:256B 块 @115200 标称 22ms;僵尸通道限时 + 自愈 */
|
||||
#define DBG_CON_DRAIN_BUDGET_MS 250U /* 单次 drain 连续发送上限:让位喂狗轮询(缺失容忍 800ms) */
|
||||
#define DBG_CON_FAULT_COOL_MS 300U /* 故障后发送冷却:ORE 锁存毫秒级重锁,背靠连发无益
|
||||
且拖长主循环周期 → 喂狗节奏饥饿(gen-288 实证);
|
||||
且拖长主循环周期 → 喂狗节奏饥饿(台架日志代次 288 实证);
|
||||
冷却期内 drain 只让位、不发送,下轮重试 */
|
||||
|
||||
/* ==== round 8b:与时间源无关的硬上界(DWT CYCCNT)====
|
||||
/* ==== 第 8b 轮:与时间源无关的硬上界(DWT CYCCNT)====
|
||||
* HAL_UART_Transmit 的超时判据是 `HAL_GetTick() - Tickstart > Timeout`,而 HAL 时基
|
||||
* = TIM7 的 uwTick。TIM7 停摆 → 超时永不成立 → 写者死等 TXE → 唯一喂狗者停摆 →
|
||||
* IWDG 误复位(round 8a 台架现场)。改用 DWT CYCCNT(内核硬件周期计数,不依赖任何
|
||||
* IWDG 误复位(第 8a 轮台架现场)。改用 DWT CYCCNT(内核硬件周期计数,不依赖任何
|
||||
* 中断/uwTick)做判据;DWT 使能失败时退回 HAL_UART_Transmit(旧行为,功能不退化)。 */
|
||||
#define CON_TX_TIMEOUT_CYC (SystemCoreClock / 10U) /* 发送硬上界 = 100ms 对应周期数 */
|
||||
/*
|
||||
@@ -89,20 +123,74 @@
|
||||
static uint8_t s_con_ring[DBG_CON_RING_SIZE];
|
||||
static volatile uint16_t s_con_head; /* 生产者写入位置 */
|
||||
static volatile uint16_t s_con_tail; /* 写者读取位置 */
|
||||
static volatile uint32_t s_con_drop; /* 因缓冲满丢弃的行数 */
|
||||
static volatile uint32_t s_con_drop; /* 真没落地的丢弃数(一次一行/一块) */
|
||||
static volatile uint32_t s_con_tx_fault; /* TX 失败(僵尸通道)次数,均经外设软复位自愈 */
|
||||
static volatile uint8_t s_con_tx_fault_log; /* TX 故障留痕待上报(由写者任务在 drain 之外补记 run.log) */
|
||||
static volatile uint32_t s_con_cool_until; /* 故障冷却截止 tick(0 = 无冷却) */
|
||||
static volatile uint32_t s_con_cool_until; /* 故障冷却截止 tick(只存时刻,不再兼作哨兵) */
|
||||
static volatile uint8_t s_con_cooling; /* 1 = 正在故障冷却(独立标志:tick 恰好等于 0 的那一拍也必须算在冷却) */
|
||||
static volatile uint32_t s_con_tx_timeout; /* 发送块超时次数(写者排空所见,与丢弃账分开) */
|
||||
static volatile uint8_t s_con_draining; /* 1 = 处于 drain 中(重入保护) */
|
||||
static volatile uint8_t s_con_writer_up; /* 1 = 写者已注册(此后走环形缓冲) */
|
||||
static volatile uint8_t s_con_dwt_up; /* 1 = DWT CYCCNT 已使能(硬上界基准可用) */
|
||||
static TaskHandle_t s_con_writer_task; /* 写者任务句柄(自等保护用) */
|
||||
/* 行级单一所有者(道G task-23,O1 第二实例):USART1 数据寄存器同一时刻只允许
|
||||
* 一个上下文在逐字节轮询写入。NULL = 空闲。
|
||||
* ⚠ 这不是"锁",是**所有权凭证**:拿不到就当场让路,绝不排队等待(见
|
||||
* con_tx_try_claim 函数头:裸发一行 @115200 约 7.8 ms,把它包进临界区等于给
|
||||
* 启动窗口塞进 7.8 ms 的中断延迟与最坏喂狗抖动,比交错本身更糟)。 */
|
||||
/*
|
||||
* 本模块"只有几条指令"的关中断窗口(道G task-26 收口时定,2026-10-26)
|
||||
* ---------------------------------------------------------------------
|
||||
* 刻意**不用** taskENTER_CRITICAL()/taskEXIT_CRITICAL(),改用 CMSIS 的 PRIMASK
|
||||
* 保存 + 恢复。理由是一条只在**静态调用图**上看得见、真机上却很贵的坑:
|
||||
* taskEXIT_CRITICAL() => vPortExitCritical() => configASSERT()
|
||||
* => vApplicationAssertFailed() (本仓断言钩子自 2026-09-29 起就调
|
||||
* dbg_console_write_raw 上报 file:line)
|
||||
* => 又回到本模块 => 又进临界区
|
||||
* armlink 因此在本模块周围长出三条互递归环(con_tx_try_claim => vPortExitCritical、
|
||||
* dbg_console_write_raw => con_tx_release、con_ring_try => vPortExitCritical),并把
|
||||
* 这坨算进**每一条含临界区的任务链**:实测 vApplicationAssertFailed 的 Max Depth 从
|
||||
* 约 40 B 变 128 B + In Cycle,defaultTask 3,040->3,120、netTask 1,920->2,000、
|
||||
* iec104Task 1,464->1,544 —— 三条同涨 80 B 就是"加深在共享帧上"的铁证,iec104 的
|
||||
* margin 因此掉到 1,016 < 1,024 ISR 底线。
|
||||
* 换成 PRIMASK 之后:
|
||||
* ① 三个原语都是 core_cmFunc.h 里的静态内联 —— 零调用帧、零断言,那三条环不成立;
|
||||
* ② **屏蔽面变宽是有意的**:本 port(RVDS/ARM_CM4F)的临界区走 BASEPRI,只挡
|
||||
* 优先级数值 >= configMAX_SYSCALL_INTERRUPT_PRIORITY 的中断,PRIMASK 连更高
|
||||
* 优先级的也一起挡(NMI 与 HardFault 除外)。这是**语义变强**,不是变弱;窗口
|
||||
* 只有几条指令(一次 test-and-set / 一次清属主 / 一次整行入环拷贝,与换之前
|
||||
* 同宽度同时长),代价是几微秒级的额外屏蔽,读代码的人必须知道这一点。
|
||||
* ③ 不依赖调度器与 uxCriticalNesting,未注册期与中断里同样可用。
|
||||
* ⚠ **为什么必须把 PRIMASK 存进局部变量、退出时原样写回,而不是直接 __enable_irq()**:
|
||||
* 本模块的窗口可能嵌套在调用者已有的临界区里(断言/故障钩子、写者 drain 轮内的
|
||||
* 留痕行都是)。无条件开中断 = **替别人把任务级临界区提前打开**,那是比交错严重
|
||||
* 一个量级的竞态;保存/恢复则天然支持嵌套。保存量放局部而不是共享静态量,是因为
|
||||
* 窗口可能被更高优先级上下文再次进入,共享量会被它改写、退出时就恢复错了。
|
||||
* ⚠ 只允许包住"几条指令"的量级,**绝不允许**包住串口发送本身:一行 90 B @115200
|
||||
* 约 7.8 ms,把它关进中断等于给启动窗口塞 7.8 ms 中断延迟与最坏喂狗抖动
|
||||
* (红线与机器判据见 con_tx_bounded 函数头与主机单测 TC-CONLOG-015b/177)。
|
||||
*/
|
||||
#define CON_CRIT_ENTER(pm_var) do { (pm_var) = __get_PRIMASK(); __disable_irq(); } while (0)
|
||||
#define CON_CRIT_EXIT(pm_var) do { __set_PRIMASK(pm_var); } while (0)
|
||||
|
||||
static volatile TaskHandle_t s_con_tx_owner;
|
||||
static volatile uint8_t s_con_tx_depth; /* 同一任务重入深度(drain 轮内的裸发留痕行) */
|
||||
|
||||
/* con_tx_try_claim 的三种结果(区分开是必要的:**放行**的那些上下文没有持有凭证,
|
||||
* 绝不能在释放时把别人的所有权清掉) */
|
||||
#define CON_TX_NO 0u /* 别人正持有整行 -> 调用方让路(不阻塞) */
|
||||
#define CON_TX_BYPASS 1u /* 放行:本上下文不许等也无需凭证(ISR / 调度器未启动) */
|
||||
#define CON_TX_HELD 2u /* 已持有:调用方发完必须 con_tx_release */
|
||||
|
||||
/*
|
||||
* 函数功能:计算环形缓冲已用字节数(head-tail 差值回绕取模,无锁)
|
||||
* 入口参数:无
|
||||
* 返回值:已用字节数 uint16_t
|
||||
*/
|
||||
static uint8_t con_ring_try(const char *p_buf, uint16_t len);
|
||||
/* 定义在下方 con_append 旁;裸发让路路径要用,故前置声明
|
||||
* (ARMCC 对隐式声明报警告,本仓红线是 0 警告) */
|
||||
|
||||
static uint16_t con_used(void) {
|
||||
return (uint16_t)((uint16_t)(s_con_head - s_con_tail) & (DBG_CON_RING_SIZE - 1U));
|
||||
}
|
||||
@@ -179,7 +267,7 @@ static HAL_StatusTypeDef con_tx_bounded(const uint8_t *p_buf, uint16_t len) {
|
||||
|
||||
con_dwt_ensure();
|
||||
if (huart1.Instance == NULL) {
|
||||
return HAL_ERROR; /* USART1 未初始化:无可用通道,安全让位 */
|
||||
return HAL_ERROR; /* USART1 未初始化:无可用通道,安全让位(不是超时,不记超时账) */
|
||||
}
|
||||
if (s_con_dwt_up == 0U) {
|
||||
return con_tx_counted(huart1.Instance, p_buf, len);
|
||||
@@ -190,33 +278,169 @@ static HAL_StatusTypeDef con_tx_bounded(const uint8_t *p_buf, uint16_t len) {
|
||||
for (i = 0U; i < len; i++) {
|
||||
while ((u->SR & USART_SR_TXE) == 0U) {
|
||||
if ((uint32_t)(DWT->CYCCNT - t0) >= CON_TX_TIMEOUT_CYC) {
|
||||
return HAL_TIMEOUT; /* 有界:本块放弃(调用方自愈) */
|
||||
return HAL_TIMEOUT; /* 有界:本块放弃(记账由调用方按返回值做) */
|
||||
}
|
||||
}
|
||||
u->DR = (uint16_t)p_buf[i];
|
||||
}
|
||||
while ((u->SR & USART_SR_TC) == 0U) {
|
||||
if ((uint32_t)(DWT->CYCCNT - t0) >= CON_TX_TIMEOUT_CYC) {
|
||||
return HAL_TIMEOUT;
|
||||
return HAL_TIMEOUT; /* TC 超时:字节已交给发送器,调用方分账 */
|
||||
}
|
||||
}
|
||||
return HAL_OK;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:控制台裸发送(异常/早期路径专用:有界、单次、不加锁)
|
||||
* 函数功能:尝试取得"USART1 整行发送所有权"(**非阻塞**,道G task-23)
|
||||
* 返回值:CON_TX_HELD 已持有(发完必须 con_tx_release)/ CON_TX_BYPASS 放行且不记账
|
||||
* / CON_TX_NO 别人正持有整行,调用方须让路
|
||||
* 限定条件:任意上下文(内部只有一次几条指令的临界区,绝不睡眠、绝不长期关中断)
|
||||
* 函数说明:三条规则各有硬理由,改动前请连读 docs/子系统设计/控制台单写者日志设计说明.md
|
||||
* 的「行级单一所有者」一节:
|
||||
* ① IPSR != 0(异常/中断,含四个 Fault 的崩溃上报)-> **放行**:这些上下文不许阻塞,
|
||||
* 且一发生就要复位,日志是唯一遗言,绝不允许在这里"让路"掉。已知残余(登记在同一
|
||||
* 篇文档):ISR 可以打断某任务已发出半行的裸发,两侧仍可能交错——但 Fault 行以
|
||||
* "[ERR]" 起头且整段在同一次 con_tx_bounded 内,标记校验按字节 grep 仍能命中,
|
||||
* 不构成漏检;把钩子改成"一条行一次发完"是另一张卡的活(API 已备:tx_begin/tx_end)。
|
||||
* ② 调度器未启动(xTaskGetCurrentTaskHandle() == NULL,osKernelStart 之前)-> **放行**:
|
||||
* 此刻全系统只有一个执行流,本就没有第二个写者;早期启动日志一条都不该丢。
|
||||
* ③ 已调度且空闲 -> 临界区里"看 + 认领"(几条指令),拿到即整行独占;已被持有 ->
|
||||
* 立刻返回 CON_TX_NO,**不排队**:裸发一行 @115200 约 7.8 ms,等锁等于把自己的
|
||||
* 日志节拍压到别人的串口耗时上,adcTask(AboveNormal,1 kHz 消费)可能被压住
|
||||
* 数十拍,比交错更糟。
|
||||
*/
|
||||
static uint8_t con_tx_try_claim(void) {
|
||||
TaskHandle_t me;
|
||||
uint32_t pm;
|
||||
uint8_t got = 0U;
|
||||
|
||||
if (__get_IPSR() != 0U) {
|
||||
return (uint8_t)CON_TX_BYPASS;
|
||||
}
|
||||
me = xTaskGetCurrentTaskHandle();
|
||||
if (me == NULL) {
|
||||
return (uint8_t)CON_TX_BYPASS;
|
||||
}
|
||||
if (s_con_tx_owner == me) {
|
||||
if (s_con_tx_depth < 255U) {
|
||||
s_con_tx_depth++;
|
||||
}
|
||||
return (uint8_t)CON_TX_HELD;
|
||||
}
|
||||
CON_CRIT_ENTER(pm); /* 窗口选型与嵌套语义理由见 CON_CRIT_ENTER 定义处 */
|
||||
if (s_con_tx_owner == NULL) {
|
||||
s_con_tx_owner = me;
|
||||
s_con_tx_depth = 1U;
|
||||
got = 1U;
|
||||
}
|
||||
CON_CRIT_EXIT(pm);
|
||||
|
||||
return (got != 0U) ? (uint8_t)CON_TX_HELD : (uint8_t)CON_TX_NO;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:归还整行发送所有权(与 con_tx_try_claim 成对)
|
||||
* 入口参数:held - con_tx_try_claim 的返回值(只有 CON_TX_HELD 才真正归还)
|
||||
* 返回值:无
|
||||
* 限定条件:与取锁同上下文;CON_TX_BYPASS / CON_TX_NO 一律空操作——放行的那些上下文
|
||||
* 从未持有凭证,误清会把别人正持有的整行放开,等于把这次修复改没
|
||||
* 函数说明:同任务嵌套按深度递减(写者一轮 drain 内还会再裸发 [WARN]/[ERR] 留痕行,
|
||||
* 那是嵌套持有者,最外层释放时才清 owner)
|
||||
*/
|
||||
static void con_tx_release(uint8_t held) {
|
||||
TaskHandle_t me;
|
||||
uint32_t pm;
|
||||
|
||||
if (held != (uint8_t)CON_TX_HELD) {
|
||||
return;
|
||||
}
|
||||
me = xTaskGetCurrentTaskHandle();
|
||||
if (s_con_tx_owner != me) {
|
||||
return;
|
||||
}
|
||||
if (s_con_tx_depth > 1U) {
|
||||
s_con_tx_depth--;
|
||||
return;
|
||||
}
|
||||
CON_CRIT_ENTER(pm);
|
||||
if (s_con_tx_owner == me) {
|
||||
s_con_tx_owner = NULL;
|
||||
s_con_tx_depth = 0U;
|
||||
}
|
||||
CON_CRIT_EXIT(pm);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:把"一条行分几段裸发"的调用方收成一个整行独占者(道G task-23)
|
||||
* 返回值:1 - 已独占(此后本任务每次 dbg_console_write_raw 都直接发,段间无人插入)
|
||||
* 0 - 别人正持有;调用方**照发即可**(本函数不改变任何调用方的既有语义,
|
||||
* 绝不阻塞、绝不吞字,只是段间仍可能被插——调用方自己权衡)
|
||||
* 限定条件:任务上下文(ISR 内调用会返回 1 但不记账,见 con_tx_try_claim ①)
|
||||
* 函数说明:给多段裸发的诊断/崩溃钩子用(Src/freertos.c 的钩子把一条行拆成
|
||||
* head/name/":"/" (resetting)" 等多次直发,段与段之间就是插字节的位置)。
|
||||
* 配对使用:tx_begin() -> 若干次 write_raw -> tx_end()。嵌套安全(按深度计)。
|
||||
*/
|
||||
uint8_t dbg_console_tx_begin(void) {
|
||||
uint8_t held = con_tx_try_claim();
|
||||
|
||||
return (held == (uint8_t)CON_TX_NO) ? 0U : 1U;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:结束一次整行独占(与 dbg_console_tx_begin 成对)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:只在 dbg_console_tx_begin() 之后调用;未持有时为空操作(幂等)
|
||||
*/
|
||||
void dbg_console_tx_end(void) {
|
||||
con_tx_release((uint8_t)CON_TX_HELD);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:控制台裸发送(异常/早期路径专用:有界、单次;**整行独占发送器**)
|
||||
* 入口参数:buf - 文本缓冲;len - 字节数(超上限截断)
|
||||
* 限定条件:串口已初始化;仅用于写者未注册、中断/异常上下文、崩溃上报
|
||||
* 返回值:无
|
||||
* 限定条件:串口已初始化;写者未注册、中断/异常上下文、崩溃上报、诊断转储
|
||||
* 函数说明:走 con_tx_bounded(DWT 硬上界),tick 停摆时仍能在有界时间内返回。
|
||||
* 道G task-23 起本函数是裸发的**唯一漏斗**(所有裸发调用点都经过它,所以调用方
|
||||
* 文件一个都不用改):
|
||||
* · 拿到所有权 -> 整行一次发完,中途不会有别人的字节插进来;
|
||||
* · 拿不到 -> **本行不丢**,改投既有环形缓冲延后发。竞争失败不该变成丢行,更不该
|
||||
* 变成 [ERR]:台架那个未注册窗口每 3 次启动命中 2 次,按"丢弃 + [ERR]"处理只会
|
||||
* 把静默缺陷换成常红的门禁([ERR] 是 @test 的 Forbidden 标记)。
|
||||
* 入环的是**同一批字节原文**,本模块与 drain 都不补前缀、不重打时间戳(DBG_* 的
|
||||
* 前缀在调用点就已 snprintf 进调用方缓冲)-> 延后发出去的行与它当场直发时
|
||||
* 逐字节相同(判据⑦),取证时不会出现"证据说谎";
|
||||
* · 环也放不下 -> 这才计入既有 s_con_drop(真溢出,仍然可见,不静默);
|
||||
* · ISR / 调度器未启动 -> 放行直发(con_tx_try_claim ①②)。
|
||||
*/
|
||||
void dbg_console_write_raw(const char *buf, unsigned len) {
|
||||
uint8_t held;
|
||||
|
||||
if ((buf == NULL) || (len == 0U)) {
|
||||
return;
|
||||
}
|
||||
if (len > DBG_CON_CHUNK_MAX) {
|
||||
len = DBG_CON_CHUNK_MAX;
|
||||
}
|
||||
|
||||
held = con_tx_try_claim();
|
||||
if (held == (uint8_t)CON_TX_NO) {
|
||||
if (con_ring_try(buf, (uint16_t)len) != 0U) {
|
||||
/* 延后发:写者空出来就排出去(未注册时也一样——注册后第一轮 drain 就发)。
|
||||
* 唤醒事件只在事件组已建时发(内核前 sys_evtHandle 还是 NULL) */
|
||||
if (sys_evtHandle != NULL) {
|
||||
(void)osEventFlagsSet(sys_evtHandle, SYS_EVT_CONSOLE_TX);
|
||||
}
|
||||
} else {
|
||||
s_con_drop++; /* 让了路又放不下环 = 真丢,进既有那本账 */
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
(void)con_tx_bounded((const uint8_t *)buf, (uint16_t)len);
|
||||
con_tx_release(held);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -268,6 +492,7 @@ static void con_uart_reinit(void) {
|
||||
void dbg_console_drain(void) {
|
||||
uint16_t used;
|
||||
uint32_t t0;
|
||||
uint8_t held;
|
||||
|
||||
if (s_con_writer_up == 0U) {
|
||||
return;
|
||||
@@ -276,18 +501,32 @@ void dbg_console_drain(void) {
|
||||
return; /* 重入保护:写者自记录不触发嵌套 drain */
|
||||
}
|
||||
|
||||
/* 整行所有权(道G task-23):本函数按块直写 USART1->DR,若此刻别的应用上下文
|
||||
* 还在未注册期那条裸发里发到自己一半(attach 就发生在 defaultTask 里,与别的
|
||||
* 任务裸发**可以**前后脚撞上),两边就会逐字节交错——那正是本卡要修的形态。
|
||||
* 拿不到就**本轮让位**:tail 不推进 => 环里一条不少,主循环 ≤50 ms 后再试,
|
||||
* 所以这条路径**永不丢弃**,也就不该在这里加任何计数(加了就是把让位谎报成丢行)。 */
|
||||
held = con_tx_try_claim();
|
||||
if (held == (uint8_t)CON_TX_NO) {
|
||||
return;
|
||||
}
|
||||
|
||||
s_con_draining = 1U;
|
||||
t0 = (uint32_t)HAL_GetTick();
|
||||
|
||||
/* 故障冷却:僵尸通道(ORE 重锁)秒内不自愈,冷却期内不发送——
|
||||
* 本轮 drain 直接让位主循环,保住喂狗节奏(gen-288 停喂误复位的
|
||||
* 直接诱因 = 背靠连发的块超时链)。无符号差值跨 49.7 天回绕安全 */
|
||||
if (s_con_cool_until != 0U) {
|
||||
* 本轮 drain 直接让位主循环,保住喂狗节奏(日志代次 288 停喂误复位的
|
||||
* 直接诱因 = 背靠连发的块超时链)。是否在冷却看 s_con_cooling 独立标志,
|
||||
* **不再拿 tick 值当哨兵**(2026-10-24 实测:故障恰好落在 tick==0 那一拍时,旧写法
|
||||
* 记出的 cool_until==0 等于没记冷却,下一轮立刻再耗一次 100ms 块超时)。
|
||||
* 无符号差值跨 49.7 天回绕安全 */
|
||||
if (s_con_cooling != 0U) {
|
||||
if ((uint32_t)(t0 - s_con_cool_until) < DBG_CON_FAULT_COOL_MS) {
|
||||
s_con_draining = 0U;
|
||||
con_tx_release(held); /* 让位也要归还凭证,否则别人永远进不来 */
|
||||
return;
|
||||
}
|
||||
s_con_cool_until = 0U;
|
||||
s_con_cooling = 0U;
|
||||
}
|
||||
|
||||
while ((used = con_used()) != 0U) {
|
||||
@@ -309,12 +548,28 @@ void dbg_console_drain(void) {
|
||||
taskEXIT_CRITICAL();
|
||||
|
||||
if (st != HAL_OK) {
|
||||
if (st == HAL_ERROR) {
|
||||
/* 2026-10-24 修复:USART1 **未初始化**(huart1.Instance == NULL,
|
||||
* 只可能出现在 HAL_Init 早期或断言/故障上报路径)与"僵尸通道"是两回事:
|
||||
* 这里**绝不做外设重初始化**——那会在 HAL 尚未就绪时跑 MspInit 重配
|
||||
* RCC/GPIO;也不计 tx_fault(那本账的语义是"软复位自愈次数")。
|
||||
* 本块字节确实一个都没上线 -> 计入 dropped(只记真丢的),
|
||||
* 末尾的 [ERR] dropped 行负责留痕;本轮到此为止(失败即止)。 */
|
||||
s_con_drop++;
|
||||
break;
|
||||
}
|
||||
/* 僵尸通道:软复位自愈 + 进入发送冷却(ORE 毫秒级重锁,
|
||||
* 继续背靠连发只会把主循环耗在块超时链上);本块丢弃
|
||||
* (行已出环,不可重放) */
|
||||
* (行已出环,不可重放),本块在超时账与丢弃账上各记一笔 */
|
||||
s_con_tx_timeout++; /* 新增账:块超时次数(TXE 与 TC 两条硬上界
|
||||
* 都算,口径 = 写者排空时观察到的块超时) */
|
||||
con_uart_reinit();
|
||||
s_con_tx_fault++;
|
||||
s_con_drop++; /* 确认不了的块算真丢(一次丢弃事件),
|
||||
* 旧写法只涨 tx_fault -> 账上看不出日志在丢,
|
||||
* docs §13-O2 记的正是这条 */
|
||||
s_con_cool_until = (uint32_t)HAL_GetTick();
|
||||
s_con_cooling = 1U;
|
||||
if (s_con_tx_fault == 1U) {
|
||||
static const char note[] = "\r\n[WARN] CONSOLE tx fault: UART reinit (self-healed)\r\n";
|
||||
|
||||
@@ -338,31 +593,96 @@ void dbg_console_drain(void) {
|
||||
|
||||
s_con_draining = 0U;
|
||||
|
||||
/* 丢弃可观测性:缓冲满丢行必须显式可见(否则会掩盖开机自检/测试标记的丢失) */
|
||||
/* 丢弃可观测性:缓冲满丢行必须显式可见(否则会掩盖开机自检/测试标记的丢失)。
|
||||
* 账上有两种成因,都算"真没落地":① 环满等不到空位(con_append);② 抢不到整行
|
||||
* 所有权**且**环也放不下(dbg_console_write_raw,道G task-23)。正常让路不入这本账,
|
||||
* 所以 [ERR] 行至今仍是"控制台在丢东西"的唯一信号,没被稀释。 */
|
||||
if (s_con_drop != 0U) {
|
||||
char note[64];
|
||||
uint32_t d = s_con_drop;
|
||||
int n;
|
||||
|
||||
s_con_drop = 0U;
|
||||
n = snprintf(note, sizeof(note), "\r\n[ERR] CONSOLE dropped %lu lines\r\n",
|
||||
(unsigned long)d);
|
||||
if (n > 0) {
|
||||
if (n > (int)sizeof(note) - 1) {
|
||||
n = (int)sizeof(note) - 1;
|
||||
}
|
||||
dbg_console_write_raw(note, (unsigned)n);
|
||||
/* 只有**真发出去了才销账**(2026-10-24 一并修正):旧写法先把计数清零
|
||||
* 再裸发,通道不可用/僵尸时那一发也失败 -> 丢弃证据被一起冲掉,[ERR] 行
|
||||
* 永远出不来("永不静默"变成"永不显示")。留到下一轮或串口恢复后补发。 */
|
||||
/* 所有权仍在手里(本凭证覆盖整轮,含这条留痕行) */
|
||||
if (con_tx_bounded((const uint8_t *)note, (uint16_t)n) == HAL_OK) {
|
||||
/* 按"本次报出去的这条"扣账而不是清零:期间生产者新丢的行留下轮报。
|
||||
* 递减只发生在这一处(单写者 + s_con_draining 重入保护),生产者只增,
|
||||
* 故 s_con_drop >= d 恒成立,无需再写下溢分支(写了也永远测不到)。 */
|
||||
s_con_drop -= d;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
con_tx_release(held);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:尝试把一整行原文放进环形缓冲(**只试一次,绝不等待**)
|
||||
* 入口参数:p_buf - 整行文本;len - 字节数(调用方已钳位)
|
||||
* 返回值:1 - 已入环(head 已推进);0 - 空位不够,什么都没写
|
||||
* 限定条件:任务上下文(内部是一次几条指令至多拷满 192B 的临界区,不睡眠)
|
||||
* 函数说明:两个调用方共用同一份判据,避免长出"两套入环语义":
|
||||
* ① con_append 的常规路径(外面套有界等待,等写者腾位);
|
||||
* ② dbg_console_write_raw 抢不到整行所有权时的让路路径(**不套等待**:延后发一条行
|
||||
* 可以,把调用者压住 200ms 就是另一场事故)。
|
||||
* 复核空位 + 整行拷贝 + 推进 head **必须在同一个临界区内**(2026-10-24 修复):旧写法
|
||||
* 先出区判 free_space >= len 再进区写,两个生产者可在环 >95% 满时同时过判据,head
|
||||
* 合计越过容量 -> con_used() 回绕变小 -> 后写的行覆盖尚未发出的旧行,且既不报错也不
|
||||
* 计丢弃(静默损坏)。等待(vTaskDelay/DWT 判据)刻意留在区外。
|
||||
* ⚠ 入环的是调用方给的**字节原文**:本函数与 drain 都不补前缀、不重打时间戳
|
||||
* (DBG_* 的前缀在调用点就已 snprintf 进调用方缓冲)——延后发与当场直发逐字节
|
||||
* 相同,这是 task-23 判据⑦的结构保证。
|
||||
*/
|
||||
static uint8_t con_ring_try(const char *p_buf, uint16_t len)
|
||||
{
|
||||
uint32_t pm;
|
||||
uint8_t fitted = 0U;
|
||||
|
||||
/* 窗口用 PRIMASK 而不是 taskENTER_CRITICAL():后者会经 vPortExitCritical 里的
|
||||
* configASSERT 接回断言钩子,再回到本模块,静态调用图上长出一条环,armlink 把它
|
||||
* 整条记进**每一条含临界区的任务链**(实测三条链同涨 80 B)。理由全文见
|
||||
* CON_CRIT_ENTER 定义处。屏蔽时长与换之前一致(就是这一次整行拷贝)。 */
|
||||
CON_CRIT_ENTER(pm);
|
||||
if ((uint16_t)((DBG_CON_RING_SIZE - 1U) - con_used()) >= len) {
|
||||
uint16_t head = s_con_head;
|
||||
uint16_t first = (uint16_t)(DBG_CON_RING_SIZE - head);
|
||||
uint16_t i;
|
||||
|
||||
if (first > len) {
|
||||
first = len;
|
||||
}
|
||||
for (i = 0U; i < first; i++) {
|
||||
s_con_ring[head + i] = (uint8_t)p_buf[i];
|
||||
}
|
||||
for (i = first; i < len; i++) {
|
||||
s_con_ring[(uint16_t)((uint16_t)(head + i) & (DBG_CON_RING_SIZE - 1U))] =
|
||||
(uint8_t)p_buf[i];
|
||||
}
|
||||
s_con_head = (uint16_t)((uint16_t)(head + len) & (DBG_CON_RING_SIZE - 1U));
|
||||
fitted = 1U;
|
||||
}
|
||||
CON_CRIT_EXIT(pm);
|
||||
|
||||
return fitted;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:把一段文本放入控制台(写者已注册)/直接发出(未注册或中断上下文)
|
||||
* 返回值:0 - 已入缓冲或已直发;-1 - 缓冲满丢弃(计入 dbg_console_dropped)
|
||||
* 函数说明:生产者不持锁等待过久:最多 DBG_CON_WAIT_MS,仍满则丢弃该行;
|
||||
* "复核空位 -> 整行拷贝 -> 推进 head"是一个原子的临界区动作;
|
||||
* 环满等待(vTaskDelay / DWT 判据)一律在临界区之外,区内绝不睡眠。
|
||||
* 写者自身记录日志时先排空缓冲,避免自等。
|
||||
* 等待上界优先用 DWT 真实时间(CON_APPEND_WAIT_CYC,时间源无关)——
|
||||
* tick 停摆时 vTaskDelay 本身也会停,故不能用它作唯一判据(round 8b)。
|
||||
* tick 停摆时 vTaskDelay 本身也会停,故不能用它作唯一判据(第 8b 轮加固)。
|
||||
*/
|
||||
static int con_append(const char *buf, uint16_t len) {
|
||||
uint32_t waited = 0U;
|
||||
@@ -392,10 +712,9 @@ static int con_append(const char *buf, uint16_t len) {
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
uint16_t used = con_used();
|
||||
uint16_t free_space = (uint16_t)((DBG_CON_RING_SIZE - 1U) - used);
|
||||
|
||||
if (free_space >= len) {
|
||||
/* 判据与拷贝都在 con_ring_try 的那一个临界区里完成(函数头有为什么);
|
||||
* 本函数只负责"等多久"——等待一律在区外,区内绝不睡眠 */
|
||||
if (con_ring_try(buf, len) != 0U) {
|
||||
break;
|
||||
}
|
||||
if (have_dwt != 0U) {
|
||||
@@ -411,26 +730,6 @@ static int con_append(const char *buf, uint16_t len) {
|
||||
waited++;
|
||||
}
|
||||
|
||||
taskENTER_CRITICAL();
|
||||
{
|
||||
uint16_t head = s_con_head;
|
||||
uint16_t first = (uint16_t)(DBG_CON_RING_SIZE - head);
|
||||
uint16_t i;
|
||||
|
||||
if (first > len) {
|
||||
first = len;
|
||||
}
|
||||
for (i = 0U; i < first; i++) {
|
||||
s_con_ring[head + i] = (uint8_t)buf[i];
|
||||
}
|
||||
for (i = first; i < len; i++) {
|
||||
s_con_ring[(uint16_t)((uint16_t)(head + i) & (DBG_CON_RING_SIZE - 1U))] =
|
||||
(uint8_t)buf[i];
|
||||
}
|
||||
s_con_head = (uint16_t)((uint16_t)(head + len) & (DBG_CON_RING_SIZE - 1U));
|
||||
}
|
||||
taskEXIT_CRITICAL();
|
||||
|
||||
if (sys_evtHandle != NULL) {
|
||||
(void)osEventFlagsSet(sys_evtHandle, SYS_EVT_CONSOLE_TX); /* 唤醒写者 */
|
||||
}
|
||||
@@ -472,6 +771,23 @@ uint32_t dbg_console_tx_fault(void) {
|
||||
return s_con_tx_fault;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:已发生的发送块超时次数(诊断用)
|
||||
* 返回值:块超时累计次数 uint32_t
|
||||
* 限定条件:无(任意上下文可读,含 ISR)
|
||||
* 函数说明:三本账分开(docs/子系统设计/控制台单写者日志设计说明.md §13-O2)——
|
||||
* dbg_console_dropped() = **真没落地**的丢弃数(缓冲满丢的一行,或一个
|
||||
* 没能确认送达而出环的块;[ERR] 行报的就是它);
|
||||
* dbg_console_tx_fault() = 外设软复位**自愈次数**(语义不变);
|
||||
* dbg_console_tx_timeout() = 本账:写者排空时观察到的**块超时**次数
|
||||
* (TXE 与 TC 两条硬上界都算)。
|
||||
* 裸发路径(内核前 / ISR / 两条告警行自身)的超时不记本账——那些上下文里
|
||||
* 没有"块"的概念,也只可能发生在启动极早期,混进来只会让运维读数失去意义。
|
||||
* UART 物理失效但字节已发出的情形只涨本账,不污染另两本。 */
|
||||
uint32_t dbg_console_tx_timeout(void) {
|
||||
return s_con_tx_timeout;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取走"控制台 TX 故障待上报"标志(读后清)
|
||||
* 返回值:1 - 需要补记一条 run.log 证据,0 - 无
|
||||
|
||||
+21
-3
@@ -15,6 +15,9 @@
|
||||
* 修改记录:
|
||||
* 2026-09-24 王建锋 创建(自 Src/usart.c USER CODE 区迁出:
|
||||
* HAL 库生成文件不得包含应用代码)
|
||||
* 2026-10-26 王建锋 道G task-23:裸发路径加「行级单一所有者」(O1 第二实例),
|
||||
* 新增 dbg_console_tx_begin()/dbg_console_tx_end() 供"一条行分多段裸发"
|
||||
* 的调用方收口;dbg_console_write_raw() 的"不加锁"三个字从接口上取消
|
||||
*/
|
||||
|
||||
#ifndef CONSOLE_LOG_H
|
||||
@@ -32,15 +35,27 @@ extern "C" {
|
||||
* dbg_console_write_line - 生产者入口:调用方已格式化好的整行文本一次入环形缓冲
|
||||
* (刻意**不用**自研可变参数函数——规范 S12 7.6 禁止;格式化在
|
||||
* DBG_PRINT 宏内用库 snprintf 完成)
|
||||
* dbg_console_write_raw - 异常/早期路径:有界单次直发(不加锁)
|
||||
* dbg_console_write_raw - 异常/早期路径:有界单次直发;**整行独占发送器**
|
||||
* (道G task-23:拿不到独占就把整行原文转入环形缓冲延后发,
|
||||
* 与当场直发逐字节相同——不补前缀、不重打时间戳;
|
||||
* 连环都放不下才计入 dbg_console_dropped())
|
||||
* dbg_console_writer_attach- 写者任务(defaultTask)注册自己为唯一发送者
|
||||
* dbg_console_writer_ready - 写者是否已注册(1=已注册)。供**周期性任务**在写者就绪前
|
||||
* 抑制日志用:写者未注册时 con_append 走无锁直发路径
|
||||
* (仅假设 defaultTask 在日志),其余任务此时落日志会与
|
||||
* defaultTask 的存储初始化日志字节级交错(2026-10-10 台架实测)
|
||||
* dbg_console_drain - 写者任务排空缓冲并发送(单次有界;TX 失败自动软复位自愈)
|
||||
* dbg_console_dropped - 因缓冲满丢弃的行数(诊断)
|
||||
* dbg_console_tx_fault - 因僵尸通道软复位自愈的次数(诊断)
|
||||
* dbg_console_dropped - 真没落地的丢弃数:① 缓冲满丢的一行;② 一个没能确认
|
||||
* 送达而出环的块;③ 抢不到整行所有权**且**环也放不下的裸发
|
||||
* (道G task-23)。诊断;[ERR] CONSOLE dropped 行报的就是它。
|
||||
* ⚠ 抢不到所有权但成功让路入环的那条行**不入本账**(它没丢,
|
||||
* 只是晚一点发)——这本账必须保持"红得有意义",因为
|
||||
* [ERR] 是 @test 的 Forbidden 标记)
|
||||
* dbg_console_tx_fault - 因僵尸通道软复位自愈的次数(诊断,语义不变)
|
||||
* dbg_console_tx_timeout - 写者排空时观察到的块超时次数(TXE / TC 两条硬上界都算)。
|
||||
* 三本账分开:看日志有没有在丢 -> dropped;看外设自愈了几次
|
||||
* -> tx_fault;看 UART 物理失效(含"字节已发出但 TC 不置位")
|
||||
* -> tx_timeout。别把三本账混成一个数(docs §13-O2 )
|
||||
* dbg_console_tx_fault_log_take
|
||||
* - 取走"TX 故障待上报"标志(读后清)。**必须**由写者任务在
|
||||
* dbg_console_drain() 之外调用后自行 syslog_post——本模块
|
||||
@@ -50,11 +65,14 @@ extern "C" {
|
||||
#define DBG_CON_LINE_MAX 192U /* 单行文本上限(含结尾 '\0',超出截断) */
|
||||
int dbg_console_write_line(const char *line, unsigned len);
|
||||
void dbg_console_write_raw(const char *buf, unsigned len);
|
||||
uint8_t dbg_console_tx_begin(void); /* 1=已独占一条行;0=别人持有(调用方照发即可) */
|
||||
void dbg_console_tx_end(void); /* 与 tx_begin 成对;未持有时空操作(幂等) */
|
||||
void dbg_console_writer_attach(void);
|
||||
uint8_t dbg_console_writer_ready(void);
|
||||
void dbg_console_drain(void);
|
||||
uint32_t dbg_console_dropped(void);
|
||||
uint32_t dbg_console_tx_fault(void);
|
||||
uint32_t dbg_console_tx_timeout(void);
|
||||
uint8_t dbg_console_tx_fault_log_take(void);
|
||||
|
||||
/*
|
||||
|
||||
+7
-1
@@ -3,7 +3,7 @@
|
||||
* 模块功能:定长 64B SOE(Sequence-of-Events)记录的构建与解析:
|
||||
* CP56Time2a 时标 + 记录类型 + 元件号 + 单调序号 + 值 + 校验和。
|
||||
* **零 I/O、零 OS 依赖**(仅 stdint/string)→ 主机侧 L0.5 离线单测
|
||||
* (落实登记簿 A-4 约束 1"协议/记录与硬件 I/O 分离")。
|
||||
* (分层约束:协议/记录与硬件 I/O 分离)。
|
||||
* 记录口径(docs/算法/继电保护功能复现规划.md §3.4 L4-4/L4-5,B9):
|
||||
* 落盘文件 = /log/soe.log(8.3 合规;FF_USE_LFN=0 约束),定长 64B 一条
|
||||
* (照 run.log 先例:count = filesize/64 O(1)、容量满删除重建)。
|
||||
@@ -61,6 +61,8 @@ extern "C" {
|
||||
* 4 软压板投入 value = 1
|
||||
* 5 采样闭锁 value = adc_health 闭锁位图(触发时的 hf & mask)
|
||||
* 6 采样恢复 value = 0
|
||||
* 7 标定闭锁 value = UA/UN/IA VALID 未齐的通道位图(卡 5,2026-10-25)
|
||||
* 8 标定复归 value = 0
|
||||
*/
|
||||
typedef enum {
|
||||
SOE_T_ELM_ASSERT = 1u,
|
||||
@@ -69,6 +71,10 @@ typedef enum {
|
||||
SOE_T_PLATE_IN,
|
||||
SOE_T_SAMPLE_BLOCK,
|
||||
SOE_T_SAMPLE_RESUME,
|
||||
/* 未标定闭锁(「数据可信」卡 5,2026-10-25 道A 追加):UA/UN/IA 的 VALID
|
||||
* 未全置位 → 判据停走;与采样闭锁分型,边沿各留一条记录 */
|
||||
SOE_T_CAL_BLOCK,
|
||||
SOE_T_CAL_RESUME,
|
||||
SOE_T_COUNT /* 哨兵值,非有效类型 */
|
||||
} soe_type_t;
|
||||
|
||||
|
||||
+183
-5
@@ -3,6 +3,16 @@
|
||||
* 模块功能:实现 soe_store.h——protectTask 投递(取时标 + soe_rec_build +
|
||||
* SPSC 待发环 / 消息队列,零 FS);defaultTask(syslog_drain 尾部)
|
||||
* 排空落 /log/soe.log(64B 定长,容量满删除重建)。
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-12 王建锋 创建初始版本(B9,设计见 docs/算法/继电保护功能复现规划.md §3.4)
|
||||
* v1.1 2026-10-24 王建锋 仅注释/文档更正(D 项,无行为改动):s_soe_append
|
||||
* 函数头原写"打开/写入结构性错误 → unlink 重试一次
|
||||
* (同 run.log)",与实现不符且方向错误——实现里
|
||||
* f_open 失败直接返回 -1、不 unlink,这是**有意**的:
|
||||
* SOE 是事件序列,打开失败就删文件等于销毁已发生的
|
||||
* 事件。改为如实描述"仅写失败才重试一次",并补上
|
||||
* 与 sys_log_mcu.c s_is_selfheal_code 同源的逐码表
|
||||
* (名字/参数类错误码两侧都绝不 unlink)
|
||||
* 创建日期:2026-10-12(B9)
|
||||
*/
|
||||
|
||||
@@ -38,6 +48,16 @@ static osMessageQueueId_t s_queue; /* 运行期队列(drain 首次调
|
||||
static const osMessageQueueAttr_t s_queue_attr = { .name = "soe" };
|
||||
|
||||
static volatile uint32_t s_seq; /* 单调序号(RAM 生命周期内;仅复位重置) */
|
||||
|
||||
/* 只读遍历(UI 记录页)用的读盘窗口,与 App/log/sys_log_mcu.c 读侧同构:
|
||||
* 定长记录按 8 条一窗整块取回(8 × 64 B = 512 B),命中窗口零 I/O;
|
||||
* 代数由写者在卷锁区间内自增,读者也在卷锁区间内采样代数 → 窗口不会跨写失效 */
|
||||
#define SOE_RD_WINDOW 8u
|
||||
static uint8_t s_rd_win[SOE_RD_WINDOW][SOE_REC_SIZE];
|
||||
static uint32_t s_rd_win_base;
|
||||
static uint32_t s_rd_win_cnt;
|
||||
static volatile uint32_t s_rd_gen; /* 写者落盘/重建次数(对齐 32 位读写在 ARM 上原子) */
|
||||
static uint32_t s_rd_win_gen;
|
||||
static volatile uint32_t s_drop_cnt; /* 丢弃计数(队列满/环满) */
|
||||
static uint32_t s_drop_logged; /* 丢弃留痕水位(drain 侧差值上报) */
|
||||
static uint8_t s_fault_reported; /* 写故障留痕去重(成功写复位) */
|
||||
@@ -88,6 +108,30 @@ void soe_store_post(uint8_t type, uint8_t elm_id, uint32_t value) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* ── 自愈触发集逐码依据(与 sys_log_mcu.c 的同名资料块同源;2026-10-24 逐条核对
|
||||
* Lib/FatFs/ff.c)─────────────────────────────────────────────────────────
|
||||
* 进触发集(run.log 侧允许 unlink 重建一次):介质/结构类 FR_DISK_ERR(1) /
|
||||
* FR_INT_ERR(2) / FR_NO_FILESYSTEM(13) / FR_DENIED(7)。本模块**只在写失败路径**
|
||||
* 上碰自己的文件——打开失败连这些码也不 unlink。
|
||||
* 不算自愈:FR_NO_FILE(4)——带 FA_OPEN_APPEND 打开时文件会被直接建出来;返回本
|
||||
* 码的唯一现实场景是同名对象是目录(ff.c:3854),unlink 修不好它。
|
||||
* 两侧都绝不 unlink:名字/参数类 FR_INVALID_NAME(6) / FR_NO_PATH(5) /
|
||||
* FR_INVALID_OBJECT(9),以及 FR_NOT_READY(3) / FR_NOT_ENABLED(12) /
|
||||
* FR_TIMEOUT(15)——同名同因,删了也修不好,反而每轮丢一份历史(§10.1 第 10 条
|
||||
* 严禁的正是这种归集)。
|
||||
* 写失败路径不接这套判定:f_write 的返回码里不存在名字/参数类(能走到那里说明
|
||||
* 已经打开成功),而簇分配失败是 FR_OK + 短写、根本没有错误码可判,只能无条件
|
||||
* 重试一次。
|
||||
* 为什么 SOE 比 run.log 保守(两侧资料块各写一份、互指,改动必须同步):run.log
|
||||
* 是滚动运行日志,坏了重建损失有限;SOE 是**事件序列**,文件里是一条条已经发生
|
||||
* 过的保护事件,"打开失败就删整个历史文件"等于**销毁事件证据**——SOE 的存在就
|
||||
* 是为了留这些事件,删了比写不进去更糟。正确出路是等 syslog_mcu_mounted() 每 2s
|
||||
* 重试补目录 / 等卷恢复,条目留在待发环与队列里,不丢。
|
||||
* (run.log 侧的同一张表 + 同一理由见 App/log/sys_log_mcu.c 的同名资料块,测试
|
||||
* 侧两侧对齐由 TC-SYSLOG-101/102 与 TC-SOESTORE-022/022b 分别钉住。)
|
||||
*/
|
||||
|
||||
/*
|
||||
* 函数功能:追加一条 64B SOE 记录到 /log/soe.log
|
||||
* 入口参数:rec - 完整记录(SOE_REC_SIZE 字节) const uint8_t *
|
||||
@@ -98,11 +142,20 @@ void soe_store_post(uint8_t type, uint8_t elm_id, uint32_t value) {
|
||||
* ② FA_WRITE|FA_OPEN_APPEND(本移植 OPEN_APPEND 不含 WRITE,
|
||||
* 缺写位 f_write 恒 FR_DENIED,2026-09-20 实证);
|
||||
* ③ 判失败必须校验写回字节数(FR_OK+短写 = 失败,簇分配无错误码);
|
||||
* ④ 容量满 unlink 重建;打开/写入结构性错误 → unlink 重试一次
|
||||
* (L1 文件级自愈,同 run.log)。
|
||||
* ④ 容量满 unlink 重建;**仅写入失败(含 FR_OK+短写)才 unlink 重试
|
||||
* 一次**——f_open 失败一律直接返回 -1、不 unlink。
|
||||
* (2026-10-24 定案:本条旧注释写的是"打开/写入结构性错误都 unlink
|
||||
* 重试一次(同 run.log)",与实际实现不符,也与本模块的语义相反:
|
||||
* run.log 是**滚动运行日志**,坏了重建损失有限;SOE 是**事件序列**,
|
||||
* 文件里是已经发生的事件,打开失败就把整个历史文件删掉等于销毁
|
||||
* 证据,方向是错的。正确出路是让 syslog_mcu_mounted() 每 2s 重试
|
||||
* 补目录/等卷恢复,条目留在待发环与队列里,不丢。)
|
||||
* ⑤ 自愈触发集与 sys_log_mcu.c 的 s_is_selfheal_code() 同源,但本模块取
|
||||
* 更保守的子集:打开失败**任何错误码都不 unlink**,只有写失败才重试
|
||||
* 一次。逐码依据与两侧差异见上方同名资料块。
|
||||
* ⚠ 本模块**没有**任何"卷损坏"归类/重格式化路径:unlink 失败、
|
||||
* 名字/参数类错误码一律只留痕(存储架构 §10.1 教训),条目丢弃
|
||||
* 计数,绝不升级成卷操作。
|
||||
* 计数,绝不升级成卷操作(全模块无 f_mount/f_mkfs/f_opendir)。
|
||||
*/
|
||||
static int s_soe_append(const uint8_t *rec) {
|
||||
static FIL f; /* static:FF_FS_TINY=0 → FIL=528B 栈;
|
||||
@@ -121,6 +174,9 @@ static int s_soe_append(const uint8_t *rec) {
|
||||
? ((uint32_t)finfo.fsize / SOE_REC_SIZE) : 0u;
|
||||
if (cnt >= SOE_STORE_MAX) {
|
||||
(void)f_unlink(SOE_PATH);
|
||||
/* 代数自增(读者窗口失效):**unlink 当场就加**,不等写成功——文件已没了,
|
||||
* 旧窗口里那几条记录在任何索引上都不再存在,晚一步都会有读者按陈旧窗口读数 */
|
||||
s_rd_gen++;
|
||||
DBG_INFO("cap full, recreate " SOE_PATH);
|
||||
}
|
||||
|
||||
@@ -129,8 +185,9 @@ static int s_soe_append(const uint8_t *rec) {
|
||||
fr = f_write(&f, rec, SOE_REC_SIZE, &bw);
|
||||
(void)f_close(&f);
|
||||
if ((fr != FR_OK) || (bw != SOE_REC_SIZE)) {
|
||||
/* L1 文件级自愈:陈旧/损坏条目 → 删除重建、重试一次(同 run.log;
|
||||
* unlink 返回值刻意不检查——失败也只是重试失败 → 留痕,不升级) */
|
||||
/* L1 文件级自愈:**只有写失败**才删除重建、重试一次(打开失败走下面
|
||||
* 的 else,不删——见函数头 ④)。unlink 返回值刻意不检查:失败也只是
|
||||
* 重试失败 → 留痕计数,绝不升级成卷操作。 */
|
||||
(void)f_unlink(SOE_PATH);
|
||||
bw = 0u;
|
||||
fr = f_open(&f, SOE_PATH, FA_WRITE | FA_OPEN_APPEND);
|
||||
@@ -143,10 +200,13 @@ static int s_soe_append(const uint8_t *rec) {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* 打开失败:只留痕、**不 unlink**(SOE 是事件序列,删文件 = 销毁历史;
|
||||
* 名字/参数类错误码 unlink 也修不好,见函数头的逐码表) */
|
||||
rc = -1;
|
||||
}
|
||||
if (rc == 0) {
|
||||
s_fault_reported = 0u;
|
||||
s_rd_gen++; /* 落盘成功 → 读者窗口作废(在卷锁区间内自增,与读者同锁同调) */
|
||||
} else if (s_fault_reported == 0u) {
|
||||
s_fault_reported = 1u;
|
||||
DBG_ERROR("write " SOE_PATH " fail (%d)", (int)fr);
|
||||
@@ -208,3 +268,121 @@ void soe_store_drain(void) {
|
||||
uint32_t soe_store_dropped(void) {
|
||||
return s_drop_cnt;
|
||||
}
|
||||
/* ========================= 只读遍历(UI 记录页) ============================ */
|
||||
/*
|
||||
* 函数功能:已落盘的 SOE 记录条数(= filesize / SOE_REC_SIZE,O(1))
|
||||
* 入口参数:无
|
||||
* 返回值:条数;文件缺失、未挂载或 f_stat 失败返回 0 uint32_t
|
||||
* 限定条件:任意任务上下文可调(uiTask 的 SOE 记录页用);只做 f_stat,不取卷锁
|
||||
* ——与 App/log/sys_log_mcu.c 的 syslog_count() 同一口径,不另起第二套
|
||||
* 函数说明:条数按**整条 64 B 槽**相除,写者留下的半条尾槽天然不计入,这是
|
||||
* "读者看不到半成品记录"的第一道保障(另两道见 soe_store_read 头注)
|
||||
*/
|
||||
uint32_t soe_store_count(void) {
|
||||
FILINFO finfo;
|
||||
|
||||
if (f_stat(SOE_PATH, &finfo) != FR_OK) {
|
||||
return 0u;
|
||||
}
|
||||
return (uint32_t)finfo.fsize / SOE_REC_SIZE;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:把包含 idx 的那一窗(最多 8 条)整块读进 RAM 并取出目标记录
|
||||
* 入口参数:idx - 记录序号 uint32_t(调用方已保证 < count)
|
||||
* p_rec - 目标记录输出(SOE_REC_SIZE 字节) uint8_t *
|
||||
* 返回值:0 - 成功;-1 - 卷忙 / 越界 / 打开定位读失败 / 短读 int
|
||||
* 限定条件:内部函数;卷锁 try 语义(0 超时),拿不到锁立即返回不阻塞
|
||||
* 函数说明:定长记录一次取 8 条(8 × 64 B = 512 B)而不是逐条 open/read:一页 8 行
|
||||
* = 4 条记录,逐条读等于同一扇区 open 四次。记录页只在开页与翻页时重算,命中
|
||||
* 窗口即零 I/O——整套"窗口 + 代数 + try 锁"结论与 App/log/sys_log_mcu.c 读侧
|
||||
* 同源,刻意不另起第二套读盘路径(两套读盘规则必然漂移,是本仓反复在防的)
|
||||
*/
|
||||
static int s_rd_fill(uint32_t idx, uint8_t *p_rec) {
|
||||
static FIL f; /* static:FF_FS_TINY=0 → FIL=528 B 栈;
|
||||
* open~close 全程在卷锁区间内,跨任务由卷锁串行化 */
|
||||
FRESULT fr;
|
||||
UINT br = 0u;
|
||||
uint32_t total;
|
||||
uint32_t base;
|
||||
uint32_t want;
|
||||
|
||||
if (vol_lock_wait(0u) == 0u) {
|
||||
return -1; /* 卷忙不是故障:uiTask 下一轮重绘自然重试 */
|
||||
}
|
||||
/* 等锁期间写者可能已推进(甚至已 unlink 重建)→ 锁内重新取条数再校验边界 */
|
||||
total = soe_store_count();
|
||||
if ((total == 0u) || (idx >= total)) {
|
||||
vol_lock_release();
|
||||
return -1;
|
||||
}
|
||||
base = (idx / SOE_RD_WINDOW) * SOE_RD_WINDOW;
|
||||
want = total - base;
|
||||
if (want > SOE_RD_WINDOW) {
|
||||
want = SOE_RD_WINDOW;
|
||||
}
|
||||
fr = f_open(&f, SOE_PATH, FA_READ);
|
||||
if (fr == FR_OK) {
|
||||
fr = f_lseek(&f, (FSIZE_t)base * SOE_REC_SIZE);
|
||||
if (fr == FR_OK) {
|
||||
fr = f_read(&f, s_rd_win, want * SOE_REC_SIZE, &br);
|
||||
}
|
||||
(void)f_close(&f);
|
||||
}
|
||||
if ((fr != FR_OK) || (br != (UINT)(want * SOE_REC_SIZE))) {
|
||||
s_rd_win_cnt = 0u; /* 作废窗口:坏窗口留着会让后续行继续读到垃圾 */
|
||||
s_rd_win_gen = 0u;
|
||||
vol_lock_release();
|
||||
return -1;
|
||||
}
|
||||
s_rd_win_base = base;
|
||||
s_rd_win_cnt = want;
|
||||
/* 代数在**卷锁区间内**采样:写者也只在持锁时自增,二者同侧 → 窗口与代数一致 */
|
||||
s_rd_win_gen = s_rd_gen;
|
||||
vol_lock_release();
|
||||
|
||||
(void)memcpy(p_rec, (const uint8_t *)s_rd_win + ((size_t)(idx - base) * SOE_REC_SIZE),
|
||||
SOE_REC_SIZE);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读取第 idx 条 SOE 记录(解析后的结构体;idx 0 = 最早一条)
|
||||
* 入口参数:idx - 记录序号 uint32_t
|
||||
* p_out - 输出(不可为 NULL) soe_rec_t *
|
||||
* 返回值:0 - 成功;-1 - p_out 为空 / 越界 / 卷忙 / I/O 失败 / 记录校验不过 int
|
||||
* 限定条件:uiTask 上下文(记录页逐行取数);**不阻塞**——卷忙立即 -1,页面这行
|
||||
* 退化显示"读失败",下一轮重绘再试;SOE 页坏不得拖住主界面节拍
|
||||
* 函数说明:读者不得看到半成品记录,三道保障缺一不可:
|
||||
* ① count 只数整条 64 B 槽(半条尾槽不计入);
|
||||
* ② 窗口代数:写者每次成功落盘/重建在卷锁区间内自增 s_rd_gen,读者
|
||||
* 比对填充时记下的代数,不匹配即重填——对齐 32 位读写在 ARM 上原子,
|
||||
* 跨任务无需临界区,最坏多读一轮(同 syslog 读侧结论);
|
||||
* ③ 单条硬门槛:soe_rec_parse() 校版本 + 校验和,撕裂/半写一律 -3 拒收。
|
||||
* count 与 read 之间新落一条的竞态**容忍**为"本次遍历少看到一条"
|
||||
* (200 ms 重绘自然追上),绝不容忍读到半条——所以边界一律以锁内
|
||||
* 重取到的 total 为准,不用调用方传进来的旧 total。
|
||||
* soe_rec_parse 的 -1/-2/-3 在本 API 折成一个 -1:调用方(UI)对三者
|
||||
* 的处理完全相同(这行显示读不到),把内部码外溢只会诱导页面写死分支
|
||||
*/
|
||||
int soe_store_read(uint32_t idx, soe_rec_t *p_out) {
|
||||
uint8_t rec[SOE_REC_SIZE];
|
||||
|
||||
if (p_out == NULL) {
|
||||
return -1;
|
||||
}
|
||||
/* 快路径:同代数且落在窗口内 → 零 I/O(代数比较用无锁读,最坏多刷一轮) */
|
||||
if ((s_rd_win_cnt != 0u) && (s_rd_win_gen == s_rd_gen) &&
|
||||
(idx >= s_rd_win_base) && (idx < s_rd_win_base + s_rd_win_cnt)) {
|
||||
(void)memcpy(rec, (const uint8_t *)s_rd_win
|
||||
+ ((size_t)(idx - s_rd_win_base) * SOE_REC_SIZE), SOE_REC_SIZE);
|
||||
} else if (s_rd_fill(idx, rec) != 0) {
|
||||
return -1;
|
||||
} else {
|
||||
/* 窗口未命中的分支已在 s_rd_fill 里完成取数与校验,这里不重复读盘 */
|
||||
}
|
||||
if (soe_rec_parse(rec, p_out) != 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -59,6 +59,43 @@ void soe_store_drain(void);
|
||||
*/
|
||||
uint32_t soe_store_dropped(void);
|
||||
|
||||
/* ---------------- 只读遍历(UI 的 SOE 记录页用;写侧零影响) ----------------
|
||||
* 读者不得看到半成品记录,三道保障缺一不可(实现见 soe_store.c 同号函数):
|
||||
* 1) count 只数**整条 64 B 槽**(filesize / SOE_REC_SIZE 整除),写者留下的
|
||||
* 半条尾槽天然不计入;
|
||||
* 2) 窗口代数:写者每次成功落盘 / 容量重建,在**卷锁区间内**自增内部代数,
|
||||
* 读者比对填窗时记下的代数,不符即重填。对齐的 32 位读写在 ARM 上原子,
|
||||
* 跨任务不需要临界区;最坏多读一轮,不会读出错数据;
|
||||
* 3) 单条硬门槛:soe_rec_parse() 校版本 + 校验和,撕裂/半写一律拒收(-3)。
|
||||
* 容忍的竞态只有一个:count 与 read 之间新落一条 → 本次遍历少看到一条,下一轮
|
||||
* 重绘(uiTask 200 ms 节拍)自然追上。**不容忍**的是读到半条,所以边界一律以
|
||||
* 卷锁内重新取到的条数为准,不用调用方传进来的旧值。
|
||||
* 与 App/log/sys_log_mcu.c 的 syslog_count/syslog_get_record 同构(同一套窗口 +
|
||||
* 代数 + try 锁纪律),刻意不另起第二套读盘路径。
|
||||
*/
|
||||
|
||||
/*
|
||||
* 函数功能:已落盘的 SOE 记录条数(= filesize / SOE_REC_SIZE,O(1))
|
||||
* 入口参数:无
|
||||
* 返回值:条数;文件缺失、未挂载或 f_stat 失败返回 0 uint32_t
|
||||
* 限定条件:任意任务上下文可调(uiTask 记录页用);只 f_stat,不取卷锁
|
||||
* 函数说明:见上方"只读遍历"总说明第 1 道保障
|
||||
*/
|
||||
uint32_t soe_store_count(void);
|
||||
|
||||
/*
|
||||
* 函数功能:读取第 idx 条 SOE 记录(解析后的结构体,idx 0 = 最早一条)
|
||||
* 入口参数:idx - 记录序号 uint32_t
|
||||
* p_out - 输出(不可为 NULL) soe_rec_t *
|
||||
* 返回值:0 - 成功;-1 - p_out 为空 / 越界 / 卷忙 / I/O 失败 / 版本或校验和不过 int
|
||||
* 限定条件:uiTask 等读上下文;**不阻塞**——卷被占用立即返回 -1(调用方这行退化
|
||||
* 显示"读失败",下轮重绘再试),排障页不得拖住主界面节拍
|
||||
* 函数说明:soe_rec_parse 的 -1/-2/-3 在此折成一个 -1:调用方对三者的处置完全相同
|
||||
* (这行读不到),把内部码外溢只会诱导页面为区分不出来的差异写分支。
|
||||
* 时间字段取记录内的 CP56(事件自己的时标),不用"现在的时间"换算
|
||||
*/
|
||||
int soe_store_read(uint32_t idx, soe_rec_t *p_out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+176
-7
@@ -7,6 +7,20 @@
|
||||
* 创建日期:2026-09-14
|
||||
* 修改记录:
|
||||
* v1.0 2026-09-14 王建锋 创建初始版本(设计见 docs/子系统设计/运行与自检记录设计说明.md §4)
|
||||
* v1.2 2026-10-26 王建锋 新增「记录删除」API(UI 装置维护「清除记录」的前置,
|
||||
* 道F task-4;判据 docs/任务规划/UI补齐.md §2 第 3 行
|
||||
* 「不允许只做一个没有实际效果的按钮」):
|
||||
* 共享层 syslog_request_clear() / syslog_clear_state()
|
||||
* (双端都实现,UI 侧只准用这两个)+ MCU 后端专属
|
||||
* syslog_clear_apply()(落地半边,仅 defaultTask)。
|
||||
* 请求与落地分离的理由与执行点写在下面声明处
|
||||
* v1.1 2026-10-24 王建锋 仅契约注释更正,与 App/log/sys_log_mcu.c 的 C/F 两项
|
||||
* 整改同步(无签名、无行为改动):
|
||||
* syslog_get_record 写死容量要求(ts_len > SYSLOG_TS_LEN、
|
||||
* desc_len > SYSLOG_DESC_MAX)并明确"容量不足一律 -1、
|
||||
* 不截断、不写调用方缓冲"——旧注释像是"给少了会被截断",
|
||||
* 实际是整条读不到;syslog_post 写明必须假设丢弃存在、
|
||||
* 且丢弃留痕已按通道节流(节流后无丢弃计数)
|
||||
*/
|
||||
|
||||
#ifndef SYS_LOG_H
|
||||
@@ -21,10 +35,28 @@ extern "C" {
|
||||
|
||||
/*
|
||||
* 日志 ID:决定落盘文件与容量上限
|
||||
*
|
||||
* ⚠ **记录类型 = 日志 ID 本身**(一类记录 = /log 下一个严格 8.3 短名的文件),
|
||||
* 记录体内**没有**类型字段位(Lead 裁定 2026-10-26,task-10):64B 定长格式被
|
||||
* UI 记录页(文本读者)与主机解码工具同时按固定偏移读,塞类型字节等于当场砸掉
|
||||
* 这两批既有读者;而运行/自检记录已经在这份格式里跑了很久,不能为五类新记录
|
||||
* 改版本位。于是"这条记录是什么类型"由它在哪个文件里回答。
|
||||
* 同理二进制 SOE 通道的 soe_type_t 本轮**不动**:改它必须同步 UI 侧的类型名表
|
||||
* (类型名表在 UI 侧 <Drivers/BSP/UI/ui_w_soe.c>,不在本道写域),而且 SOE
|
||||
* 页与这五页是两套读者。
|
||||
* 口径与五个写入点已登记进 App/log/README.md(task-11「记录 5 页」按此做)。
|
||||
*/
|
||||
typedef enum {
|
||||
SYSLOG_RUN = 0u, /* 运行记录 /log/run.log:上电/重启事件 */
|
||||
SYSLOG_SELFTEST, /* 自检记录 /log/selftest.log:开机自检故障(仅故障才写) */
|
||||
/* ↓ 五类记录(docs/UI设计/页面扩充详细方案.md §3.6 的五行,task-10 落写入点)
|
||||
* ⚠ 只准**尾部追加**:id 值即枚举序 = 两端 s_paths[]/s_caps[] 的下标,
|
||||
* 中间插值会让老文件被新 id 读成别的类型(静默错类别,比崩更难查)。 */
|
||||
SYSLOG_ACCIDENT, /* 事故记录 /log/accid.log:跳闸投退=1 的元件动作 */
|
||||
SYSLOG_ALARM, /* 保护告警 /log/alarm.log:跳闸投退=0 或该项不存在的元件动作 */
|
||||
SYSLOG_START, /* 保护启动 /log/start.log:元件 picked 上升沿 */
|
||||
SYSLOG_OPER, /* 操作记录 /log/oper.log:强制复归 / 清除记录 / 对时 / 定值保存 */
|
||||
SYSLOG_YK, /* 遥控记录 /log/yk.log:104 遥控的选择 / 执行 / 拒绝 */
|
||||
SYSLOG_COUNT /* 哨兵值,非有效 ID */
|
||||
} syslog_id_t;
|
||||
|
||||
@@ -46,9 +78,21 @@ typedef enum {
|
||||
|
||||
/*
|
||||
* 容量上限(条数;满时删除重建,设计文档 §4.3)
|
||||
*
|
||||
* ⚠ **上限是逻辑上限,不是预分配**:文件按追加写自然增长,一条不写就不占簇;
|
||||
* 到顶才 unlink 重建(照 run.log 先例)。七类全满的名义峰值 =
|
||||
* (1024+512+512+1024+1024+512+256) × 64B = **304 KB**,NAND 2 GB、且这些
|
||||
* 记录只在事件边沿才写(判据动作/遥控/人工操作),不构成容量或磨损瓶颈。
|
||||
* 告警/启动给到 1024 是因为一次系统故障会连片触发多个元件,别让最想了解
|
||||
* 故障过程的两类记录最先被截掉。
|
||||
*/
|
||||
#define SYSLOG_RUN_MAX 1024u /* /log/run.log:64KB */
|
||||
#define SYSLOG_SELFTEST_MAX 512u /* /log/selftest.log:32KB */
|
||||
#define SYSLOG_ACCIDENT_MAX 512u /* /log/accid.log:32KB(跳闸稀有,换长历史) */
|
||||
#define SYSLOG_ALARM_MAX 1024u /* /log/alarm.log:64KB */
|
||||
#define SYSLOG_START_MAX 1024u /* /log/start.log:64KB */
|
||||
#define SYSLOG_OPER_MAX 512u /* /log/oper.log:32KB */
|
||||
#define SYSLOG_YK_MAX 256u /* /log/yk.log:16KB(低频,与事故同稀有) */
|
||||
|
||||
/*
|
||||
* 上电/重启分类窗口(秒):run.log 末条 epoch 距 now < 该值 → "reboot",否则 "power on"
|
||||
@@ -58,12 +102,52 @@ typedef enum {
|
||||
/*
|
||||
* 函数功能:投递一条日志(任意上下文;非阻塞,队列满则丢弃)
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* desc - 描述串(UTF-8,≤SYSLOG_DESC_MAX 字节;超长截断) const char *
|
||||
* desc - 描述串(UTF-8;超过 SYSLOG_DESC_MAX 的部分丢弃,落盘只留前 28 字节)
|
||||
* 本参数**投递时即拷贝进待发条目**:调用方缓冲区可立即复用,
|
||||
* 不要求生命周期覆盖到落盘(2026-10-24 台架实证后由「只存指针」
|
||||
* 改为条目内嵌定长缓冲,理由见 sys_log_mcu.c 的 syslog_entry_t 注释)
|
||||
* 指针本身不可为 NULL const char *
|
||||
* 返回值:无
|
||||
* 限定条件:MCU 端 FreeRTOS 启动前调用走静态待发数组(由 defaultTask 挂载后落盘)
|
||||
* 函数说明:调用方**必须假设投递可能被丢弃**——MCU 后端(sys_log_mcu.c)队列 8 槽、
|
||||
* 启动前待发数组 8 槽,卷被 ftpTask 长期占用时 defaultTask 会把整批回投
|
||||
* 队列(队列因此可长时间处于满态),此时本函数直接丢弃该条,并只在
|
||||
* "转入故障"的那一次留痕(丢弃留痕按正常→故障节流,F 项整改 2026-10-24;
|
||||
* 节流后**丢弃总数没有计数**,要计量得另加计数器)。必须留住的证据走
|
||||
* sys_log_evidence(),不要靠 syslog_post 重试。
|
||||
* 模拟器后端(simulator/sim/sim_syslog.c)是单线程直写宿主文件、**没有
|
||||
* 队列与丢弃**,因此上面这段满态语义只约束 MCU 端。
|
||||
*/
|
||||
void syslog_post(syslog_id_t id, const char *desc);
|
||||
|
||||
/*
|
||||
* 函数功能:投递一条日志——**调用链上不带日志行缓冲**的那一个版本
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* desc - 描述串(NUL 结尾;超过 SYSLOG_DESC_MAX 截断),同 syslog_post
|
||||
* 返回值:无
|
||||
* 限定条件:任意上下文;MCU 端不碰 FatFs、不碰卷锁、不阻塞
|
||||
* 函数说明:为什么要有第二个投递口(不是洁癖):每个 DBG_* 调用点在调用者栈上
|
||||
* 内联约 216 B 行缓冲,而 syslog_post 的两个"满态丢弃留痕"分支就躺在
|
||||
* 它自己的帧里——于是"调一次 post"在静态最坏链上等于"带上这两处缓冲"。
|
||||
* protectTask 的静态最坏链是 B9 写进 DoD 的硬线(872 B / 57%,当年在
|
||||
* post 里加一条 DBG_* 实测垫到 1192 B = stack_report FAIL),iec104Task
|
||||
* 同样是深链任务,所以这五类记录的写入点一律走本函数。
|
||||
* 代价是丢弃时不当场留痕:只累加 syslog_post_dropped(),由唯一写者
|
||||
* defaultTask 在 syslog_drain() 里按差值补一条(照 soe_store 的先例)。
|
||||
* 与 syslog_post 的差别**只有留痕路径**,收/丢的判据逐条相同。
|
||||
* 模拟器后端没有队列也没有栈预算问题,本函数等价于 syslog_post。
|
||||
*/
|
||||
void syslog_post_quiet(syslog_id_t id, const char *desc);
|
||||
|
||||
/*
|
||||
* 函数功能:取 syslog_post_quiet() 通道的累计丢弃条数(可观测性)
|
||||
* 入口参数:无
|
||||
* 返回值:MCU 端 = 队列满 / 启动前数组满累计次数;模拟器端恒 0(直写不丢) uint32_t
|
||||
* 函数说明:与 soe_store_dropped() 同款定位。留痕由 drain 按差值上报,本函数是
|
||||
* 给单测与台架读数用的,不参与任何判定路径。
|
||||
*/
|
||||
uint32_t syslog_post_dropped(void);
|
||||
|
||||
/*
|
||||
* 函数功能:获取记录条数(= filesize / SYSLOG_REC_SIZE,O(1))
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
@@ -75,11 +159,19 @@ uint32_t syslog_count(syslog_id_t id);
|
||||
* 函数功能:读取第 idx 条记录(时间戳 + 描述,均 NUL 结尾)
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* idx - 记录序号(0 起) uint32_t
|
||||
* p_ts - 时间戳输出缓冲(≥SYSLOG_TS_LEN+1 字节;不可为 NULL) char *
|
||||
* ts_len- p_ts 容量 size_t
|
||||
* p_desc- 描述输出缓冲(≥SYSLOG_DESC_MAX+1 字节;不可为 NULL) char *
|
||||
* desc_len - p_desc 容量 size_t
|
||||
* 返回值:0 - 成功;-1 - 越界/读失败 int
|
||||
* p_ts - 时间戳输出缓冲(不可为 NULL) char *
|
||||
* ts_len- p_ts 容量,必须 > SYSLOG_TS_LEN(即 ≥ SYSLOG_TS_LEN+1 = 20 字节) size_t
|
||||
* p_desc- 描述输出缓冲(不可为 NULL) char *
|
||||
* desc_len - p_desc 容量,必须 > SYSLOG_DESC_MAX(即 ≥ 29 字节) size_t
|
||||
* 返回值:0 - 成功;-1 - 越界 / 读失败 / **缓冲容量不足** int
|
||||
* 限定条件:容量不足一律返回 -1,**不做静默截断、也不写调用方一个字节**
|
||||
* (C 项整改 2026-10-24;旧注释只写"≥…+1 字节"像是"给少了会被截断",
|
||||
* 照它给 28 字节缓冲的结果是整条读不到而不是读到 27 字符)。
|
||||
* 描述字段在写入侧已截到 SYSLOG_DESC_MAX,所以按上面的最小容量给缓冲
|
||||
* 就一定装得下,读侧不需要第二道截断。
|
||||
* MCU 后端与模拟器后端(sim_syslog.c)在这条守卫上取值相同,行为一致;
|
||||
* 模拟器侧那份同样不可达的截断支还留在文件里(未列入本轮改动范围)。
|
||||
* 调用方:uiTask 的日志页(行缓冲 32B)与 SYSLOG 取证打印。
|
||||
*/
|
||||
int syslog_get_record(syslog_id_t id, uint32_t idx, char *p_ts, size_t ts_len, char *p_desc, size_t desc_len);
|
||||
|
||||
@@ -91,6 +183,58 @@ int syslog_get_record(syslog_id_t id, uint32_t idx, char *p_ts, size_t ts_len, c
|
||||
*/
|
||||
int syslog_last_epoch(syslog_id_t id, uint32_t *p_epoch);
|
||||
|
||||
/*
|
||||
* 记录删除(「清除记录」):请求 → 落地分离
|
||||
*
|
||||
* 为什么必须分两步:defaultTask 是本模块**唯一写者**(存储架构说明 §10.1 第 7 条),
|
||||
* uiTask 直接 f_unlink 会与写者的追加写/「容量满 unlink 重建」交错,而且删除要走
|
||||
* 卷锁 + FAT 回写——uiTask 在 IWDG required_mask 内、每 100 ms 喂一次心跳,
|
||||
* 给它一条最坏数百 ms 的存储路径就是拿误复位当代价。故 UI 侧只登记请求,
|
||||
* 真正的删除在 defaultTask 的 syslog_drain() 尾部执行
|
||||
* (范式同 App/algo/prot_elm.c 的 prot_elm_request_reset():单字节标志 + 消费者落地)。
|
||||
*
|
||||
* 状态码(syslog_clear_state 返回值;写成宏而不是枚举,与本头既有 SYSLOG_* 常量同族)
|
||||
*/
|
||||
#define SYSLOG_CLEAR_NONE 0 /* 从未请求过清除(上电初值) */
|
||||
#define SYSLOG_CLEAR_OK 1 /* 已清空:文件不在盘上,同拍 syslog_count() 必为 0 */
|
||||
#define SYSLOG_CLEAR_WAIT (-1) /* 请求排队中:等 defaultTask 下一轮 drain(含卷占用顺延、卷未就绪挂起) */
|
||||
#define SYSLOG_CLEAR_FAIL (-2) /* 最近一次落地失败:原文件未被删除,可再发一次请求 */
|
||||
|
||||
/*
|
||||
* 函数功能:登记「清除某日志全部记录」的请求(任意上下文;不碰存储、不阻塞)
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* 返回值:0 - 请求已登记(同一 id 重复登记合并为一次,幂等)
|
||||
* -1 - id 越界(请求未登记,状态一个字节都没动) int
|
||||
* 限定条件:非唯一写者的上下文(uiTask / netTask / iec104Task)**只能**用本函数发起
|
||||
* 删除,不得直调 MCU 后端的 syslog_clear_apply()
|
||||
* 函数说明:本函数**不等于"已清除"**,只代表"已受理"——MCU 端的删除动作发生在
|
||||
* defaultTask 的下一次 syslog_drain()(50 ms 节拍),结果读
|
||||
* syslog_clear_state(),事实复核读 syslog_count()。
|
||||
* ⚠ 两端时序有别(同 syslog_post 的队列/直写差异,那边写在 post 的头注释里):
|
||||
* MCU 端返回后状态通常是 SYSLOG_CLEAR_WAIT;模拟器端单线程、本调用内
|
||||
* 同步落地,返回后立即是 OK/FAIL,**永远不会读到 WAIT**。UI 侧要按
|
||||
* "WAIT 也正常"写判断,别把 WAIT 当失败。
|
||||
* 刻意**不**在删除成功后补一条 "records cleared" 留痕:判据要求
|
||||
* syslog_count() 归零,写了立刻变 1;要留痕归操作记录/SOE
|
||||
* (docs/任务规划/UI补齐.md 第 6 项的写入方),不是本 API 的职责。
|
||||
*/
|
||||
int syslog_request_clear(syslog_id_t id);
|
||||
|
||||
/*
|
||||
* 函数功能:查「清除记录」的状态(给 UI 出真实反馈,不是"按了就算成功")
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* 返回值:SYSLOG_CLEAR_NONE(0) / SYSLOG_CLEAR_OK(1) / SYSLOG_CLEAR_WAIT(-1) /
|
||||
* SYSLOG_CLEAR_FAIL(-2);id 越界按 NONE 返回(等价"从没请求过") int
|
||||
* 限定条件:任意上下文(MCU 端只读两个 volatile 单字节,Cortex-M 上原子,无锁)
|
||||
* 函数说明:排队位优先于结果字节——否则连点两次会先把上一次的结果当成这次的,
|
||||
* 还在排队的请求被显示成"已清空"。
|
||||
* 无锁交接的最坏竞态只有两种,都不产生错误数据:① 写者已销账、尚未写
|
||||
* 结果的那几个指令周期里读 → 读到上一次的结果(同向,多为 OK);
|
||||
* ② 上一次失败被新请求清零 → 那次失败不再显示(用户已在重新发起,
|
||||
* 语义正确)。为它上临界区要引 FreeRTOS 头 + 宿主桩,不值。
|
||||
*/
|
||||
int syslog_clear_state(syslog_id_t id);
|
||||
|
||||
/*
|
||||
* MCU 后端专属 API(sys_log_mcu.c;模拟器端无对应实现,勿在共享代码调用)
|
||||
*/
|
||||
@@ -116,16 +260,41 @@ void syslog_mcu_mounted(void);
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:仅 defaultTask(唯一写者)调用;卷未就绪时直接返回(条目滞留待重试)
|
||||
* 本函数尾部同时消费「清除记录」请求(syslog_request_clear 的落地半边,
|
||||
* 排在条目排空之后——删除必须是本轮对这两个文件的最后一次盘上动作,
|
||||
* 先删后写会把刚排空的旧记录又落回去,"清除"就变成没效果的按钮)
|
||||
*/
|
||||
void syslog_drain(void);
|
||||
|
||||
/*
|
||||
* 函数功能:立即删除某日志的全部记录(「清除记录」的落地半边,MCU 后端专属)
|
||||
* 入口参数:id - 日志 ID syslog_id_t
|
||||
* 返回值:0 - 记录已清空(含"文件本就不存在"的幂等成功,见下)
|
||||
* -1 - 未清空:卷被占用或删除失败,**原文件一个字节都没少** int
|
||||
* 限定条件:仅 defaultTask(syslog 唯一写者)上下文,且调用处**不得持卷锁**
|
||||
* (卷锁非递归锁);卷锁一律 try(vol_lock_wait(0),本任务 = 看门狗
|
||||
* 喂狗点,阻塞等锁 = 停喂 = IWDG 误复位)
|
||||
* 函数说明:其它上下文请调 syslog_request_clear()(本头文件上方);本函数在
|
||||
* MCU 后端之外**没有任何实现**,共享代码误调会在链接期报错,
|
||||
* 这比运行时断言硬。产品路径上唯一的调用者是 syslog_drain 尾部的
|
||||
* 请求消费循环,直接调用只用于板级套件与主机单测。
|
||||
* 幂等口径:只有 FR_NO_FILE 算"已经没有什么可删了"→ 返 0;
|
||||
* FR_NO_PATH(/log 目录不在)**不算**幂等——记录确实读不到,但成因是
|
||||
* 目录没了而不是"被清空",补目录归 syslog_mcu_mounted() 每 2s 的重试,
|
||||
* 这里如实返 -1,不把卷异常洗成"清除成功"。
|
||||
* 名字/参数类错误码(FR_INVALID_NAME 等)一律只返 -1 不做任何补救动作:
|
||||
* 本函数压根没有卷级动作(无 f_mkdir / 无 f_mkfs / 不重试 unlink),
|
||||
* 这正是存储架构说明 §10.1 第 10 条在删除路径上的落地形态。
|
||||
*/
|
||||
int syslog_clear_apply(syslog_id_t id);
|
||||
|
||||
/*
|
||||
* 函数功能:把一条取证行追加到 /log/wdev.log(掉电不丢;变长行,不走 64B 定长格式)
|
||||
* 入口参数:p_desc - 取证描述行(调用方已格式化;不可为 NULL) const char *
|
||||
* 返回值:0 - 成功;-1 - 卷占用或写失败(证据可能回滚,**不阻断判定**) int
|
||||
* 限定条件:poller(defaultTask)上下文调用;卷锁一律 try(绝不阻塞,
|
||||
* 本上下文 = 看门狗喂狗点)
|
||||
* 函数说明:round 7 取证通道(设计见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录:
|
||||
* 函数说明:第 7 轮取证通道(设计见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录:
|
||||
* 误复位后 RAM 态随复位消失、stall 行经僵尸串口丢失,故证据必须落盘)。
|
||||
* 内部在释放卷锁后立刻追加一条 s_append(SYSLOG_RUN, ...) 写另一个
|
||||
* 文件——FTL 单页缓存语义为"写到不同 LPN 才把前一脏页刷进 NAND",
|
||||
|
||||
+853
-110
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,7 @@
|
||||
* 模块名称:IEC 60870-5-104 ASDU 编解码(应用层,纯逻辑)
|
||||
* 模块功能:104 规约应用服务数据单元(ASDU)的类型/传送原因常量、组帧与解析。
|
||||
* **零 I/O、零平台依赖**(仅 stdint.h)→ 主机侧 L0.5 可离线单测
|
||||
* (登记簿 A-4 约束 1"协议解析与硬件 I/O 分离")。
|
||||
* (分层约束:协议解析与硬件 I/O 分离)。
|
||||
* 口径来源:docs/算法/继电保护功能复现规划.md §1.5(B0 产品定义)——
|
||||
* 公共地址 2 字节、传送原因 2 字节、信息体地址 IOA **3 字节**(101 为 2 字节);
|
||||
* 遥测上送**短浮点** M_ME_NC_1(系数 = 1);遥信双点上双点、其余单点;
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 通讯参数段([comm] 配置)实现
|
||||
* 模块功能:/config.ini [comm] 段的 RAM 副本 / 工厂默认 / 键表与注册 +
|
||||
* getter 合法带自愈 + setter 守卫。契约、线程约定与生效语义
|
||||
* (改后重启生效、不挂回调不置事件位、主站地址不做)→
|
||||
* iec104_comm.h 文件头;键表口径 → docs/子系统设计/网络配置说明.md
|
||||
* §3.6;热生效代价分析 → docs/子系统设计/IEC104上行设计说明.md §8。
|
||||
* 适用平台:STM32F407ZGTx / PC 模拟器(双端编译:模拟器运行期无 104 任务,
|
||||
* 本 TU 不产生行为,按双端注册口径入 CMakeLists 保持构建事实一致)
|
||||
* 创建日期:2026-10-26(道G task-8,「UI 补齐」第 5 项非 UI 前置)
|
||||
* 修改记录:
|
||||
* 2026-10-26 DTU 项目组 创建:实现体自 iec104_pts.c 尾段**同笔整体迁移**
|
||||
* (零逻辑改动)。动因 = coding-standard
|
||||
* CODE-RULE-17 (2.2)「非 static 函数须在配对头文件
|
||||
* 声明」按文件名配对判定——初版把实现放 iec104_pts.c
|
||||
* 而声明在 iec104_comm.h,钩子按规则判红;本模块
|
||||
* 本就是独立配置段,配对拆分即正解。
|
||||
*/
|
||||
#include "iec104_comm.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "cfg_file.h"
|
||||
|
||||
/* ---------------- [comm] RAM 副本与工厂默认 ----------------
|
||||
* 引擎属主 = defaultTask:本段只被 cfg_file_load/save/poll 通用遍历,本文件
|
||||
* 不触引擎任何写路径;内核前注册点 = app_main.c(「先注册后 load」契约,同
|
||||
* adc_cal_init / run_par_init)。App/protocol 零日志惯例保持——注册失败留痕
|
||||
* 由调用方 app_main.c 锚点负责,getter/setter 纯 RAM 不打日志(iec104Task
|
||||
* 深链省栈,AGENTS 日志成本口径)。 */
|
||||
|
||||
/* RAM 副本(引擎原地更新;u16 半字对齐读原子,撕裂域 = 单字段新旧值) */
|
||||
typedef struct {
|
||||
uint16_t ca; /* 公共地址(= 链路地址,ASDU 头 2B,点表 §1.5 口径) */
|
||||
uint16_t port; /* 被控站 TCP 监听端口 */
|
||||
} iec104_comm_t;
|
||||
|
||||
static iec104_comm_t s_comm;
|
||||
static const iec104_comm_t s_comm_rom = {
|
||||
IEC104_COMM_DEF_CA, IEC104_COMM_DEF_PORT
|
||||
};
|
||||
|
||||
/* 键表(s_comm 布局内偏移,同 net_cfg/ftp_cfg 写法) */
|
||||
static const cfg_key_def_t s_comm_keys[] = {
|
||||
{ "ca", CFG_VAL_UINT, offsetof(iec104_comm_t, ca), 2u },
|
||||
{ "port", CFG_VAL_UINT, offsetof(iec104_comm_t, port), 2u },
|
||||
};
|
||||
|
||||
/* 函数定义区 */
|
||||
|
||||
/*
|
||||
* 函数功能:置工厂默认并向引擎注册 [comm] 段(幂等,内核前)
|
||||
* 返回值:0 注册成功;-1 重复注册/段数超限(RAM 副本已复位,adc_cal 同款) int
|
||||
* 限定条件:先于 defaultTask 的 cfg_file_load;不挂回调(改后重启生效)
|
||||
*/
|
||||
int iec104_comm_init(void) {
|
||||
cfg_section_reg_t reg;
|
||||
|
||||
(void)memcpy(&s_comm, &s_comm_rom, sizeof(s_comm));
|
||||
|
||||
(void)memset(®, 0, sizeof(reg));
|
||||
reg.name = IEC104_COMM_SEC;
|
||||
reg.p_data = &s_comm;
|
||||
reg.p_defaults = &s_comm_rom;
|
||||
reg.data_size = sizeof(s_comm);
|
||||
reg.p_keys = s_comm_keys;
|
||||
reg.key_count = (uint8_t)(sizeof(s_comm_keys) / sizeof(s_comm_keys[0]));
|
||||
reg.p_cb = NULL; /* 不挂回调:iec104Task 启动读 getter(D5 口径) */
|
||||
return cfg_file_register(®);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取生效公共地址
|
||||
* 返回值:RAM 副本 ca,带外回退工厂默认;恒 ∈ IEC104_COMM_CA_MIN..MAX uint16_t
|
||||
* 限定条件:无(任意上下文,纯读)
|
||||
*/
|
||||
uint16_t iec104_comm_ca(void) {
|
||||
uint16_t v = s_comm.ca;
|
||||
|
||||
/* 只判下界。上界 IEC104_COMM_CA_MAX 就是 uint16_t 的最大值,写成
|
||||
* 「v > 65535」对 uint16_t 恒假(cppcheck compareValueOutOfTypeRangeError),
|
||||
* 留着只会让下一个读代码的人以为上界真被守过。合法带口径不变,
|
||||
* 仍照 iec104_comm.h 的常量注释(ca 1..65535 / port 1024..65535)。 */
|
||||
if (v < IEC104_COMM_CA_MIN) {
|
||||
return IEC104_COMM_DEF_CA;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取生效监听端口
|
||||
* 返回值:RAM 副本 port,带外回退工厂默认;恒 ∈ IEC104_COMM_PORT_MIN..MAX uint16_t
|
||||
* 限定条件:无(任意上下文,纯读)
|
||||
*/
|
||||
uint16_t iec104_comm_port(void) {
|
||||
uint16_t v = s_comm.port;
|
||||
|
||||
if (v < IEC104_COMM_PORT_MIN) { /* 上界恒假,同 iec104_comm_ca 处注释 */
|
||||
return IEC104_COMM_DEF_PORT;
|
||||
}
|
||||
return v;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:写 ca 进 RAM 副本(守卫 = getter 同带;非法不提交)
|
||||
* 入口参数:ca - 公共地址 uint16_t
|
||||
* 返回值:0 提交;-1 带外(副本不动) int
|
||||
* 限定条件:UI 保存交接路(task-9);落盘走 SYS_EVT_CFG_SAVE 整档外科 save
|
||||
*/
|
||||
int iec104_comm_ca_set(uint16_t ca) {
|
||||
if (ca < IEC104_COMM_CA_MIN) { /* 上界恒假,同 iec104_comm_ca 处注释 */
|
||||
return -1;
|
||||
}
|
||||
s_comm.ca = ca;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:写 port 进 RAM 副本(守卫 = getter 同带;非法不提交)
|
||||
* 入口参数:port - 监听端口 uint16_t
|
||||
* 返回值:0 提交;-1 带外(副本不动) int
|
||||
* 限定条件:同 iec104_comm_ca_set
|
||||
*/
|
||||
int iec104_comm_port_set(uint16_t port) {
|
||||
if (port < IEC104_COMM_PORT_MIN) { /* 上界恒假,同 iec104_comm_ca 处注释 */
|
||||
return -1;
|
||||
}
|
||||
s_comm.port = port;
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 通讯参数段([comm] 配置)公共接口
|
||||
* 模块功能:/config.ini [comm] 段的对外契约——公共地址(CA = ASDU 公共地址,
|
||||
* 2B)与被控站 TCP 监听端口的工厂默认、getter/setter 与合法带。
|
||||
* 实现 = 配对文件 App/protocol/iec104/iec104_comm.c(coding-standard
|
||||
* CODE-RULE-17 (2.2)「非 static 函数须在**配对头文件**声明」按文件名
|
||||
* 判定;初版曾寄身 iec104_pts.c 尾段,2026-10-26 lead 纠偏迁出)。
|
||||
* 修改记录:
|
||||
* 2026-10-26 DTU 项目组 创建(道G task-8);同日实现位置改配对拆分。
|
||||
* 线程约定:注册/复位 = app_main_init 内核前(run_par_init / adc_cal_init 同款
|
||||
* "先注册后 load"契约);load/save/poll = defaultTask(配置引擎属主,
|
||||
* netTask/uiTask 不得直接触碰引擎);getter = 任意消费上下文(u16
|
||||
* 半字对齐读天然原子,重载瞬间最坏 = 单字段新旧值之一);setter =
|
||||
* UI 保存交接路(task-9,net_cfg_set_* 先例)。
|
||||
* 生效语义:**改后重启生效**(docs/任务规划/UI补齐.md §2 第 5 行验收口径)——
|
||||
* iec104Task 启动时取一次 getter 填 link cfg;端口另在下次重 bind
|
||||
* (拆链/网络代次重建)自然生效,不必整机重启。不挂段回调、不新增
|
||||
* 事件位、不走 SYS_EVT_NET_RECONFIG(网络代次链语义不同;且 CH395F
|
||||
* 单连接 socket 上"换端口/拒绝重听"= close + re-listen,扫描主机
|
||||
* 可打成风暴,详见 docs/子系统设计/IEC104上行设计说明.md §8)。
|
||||
* 主站地址(accept 过滤)本卡**不做**:现无既有消费点,理由同上 §8。
|
||||
* 创建日期:2026-10-26(道G task-8,「UI 补齐」第 5 项非 UI 前置)
|
||||
*/
|
||||
|
||||
#ifndef IEC104_COMM_H
|
||||
#define IEC104_COMM_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* 段名(cfg_file 引擎注册用;≤ CFG_FILE_MAX_NAME_LEN) */
|
||||
#define IEC104_COMM_SEC "comm"
|
||||
|
||||
/* 工厂默认:CA = 1(点表口径,V0.1 沿用);端口 = 2404(DL/T 634.5104 标准,
|
||||
* 与 App/task/iec104_serv.h 的 IEC104_SERV_PORT 同值——该宏现仅作
|
||||
* ui_data_feed 快照兼容出口保留,运行期真源 = 本段 getter) */
|
||||
#define IEC104_COMM_DEF_CA 1u
|
||||
#define IEC104_COMM_DEF_PORT 2404u
|
||||
|
||||
/* getter 合法带(带外 = RAM 副本被直写/手改时的自愈回退,adc_cal「合法带哨兵」
|
||||
* 同款先例):
|
||||
* ca 1..65535:0 为非法公共地址(104 半字地址域,点表无 0 号站)
|
||||
* port 1024..65535:拒绝把监听端口配到 1..1023 系统保留段,避免误配顶掉
|
||||
* 系统服务;与 8888/7003 带内相撞属配置者责任(启动锚点
|
||||
* "i104: listening on %u" 打印实际端口,可核) */
|
||||
#define IEC104_COMM_CA_MIN 1u
|
||||
#define IEC104_COMM_CA_MAX 65535u
|
||||
#define IEC104_COMM_PORT_MIN 1024u
|
||||
#define IEC104_COMM_PORT_MAX 65535u
|
||||
|
||||
/*
|
||||
* 函数功能:置工厂默认并向配置引擎注册 [comm] 段(幂等,内核前调用)
|
||||
* 返回值:0 - 注册成功;-1 - 重复注册/段名冲突/段数超限(RAM 副本已复位
|
||||
* 工厂默认,引擎侧仍按既有注册表工作) int
|
||||
* 限定条件:必须先于 defaultTask 的 cfg_file_load(net_cfg_load 内);
|
||||
* 重复调用 = 仅复位 RAM 副本(单测复位依赖此语义,adc_cal 同款)
|
||||
* 函数说明:不挂变更回调(改后重启生效口径,见文件头)
|
||||
*/
|
||||
int iec104_comm_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:取生效公共地址(消费点 = iec104Task 启动填 link cfg)
|
||||
* 返回值:RAM 副本 ca,带外回退 IEC104_COMM_DEF_CA;恒 ∈ 1..65535 uint16_t
|
||||
* 限定条件:无(任意上下文,纯读)
|
||||
*/
|
||||
uint16_t iec104_comm_ca(void);
|
||||
|
||||
/*
|
||||
* 函数功能:取生效监听端口(消费点 = iec104_serv s_bind_listen + 启动日志)
|
||||
* 返回值:RAM 副本 port,带外回退 IEC104_COMM_DEF_PORT;恒 ∈ 1024..65535 uint16_t
|
||||
* 限定条件:无(任意上下文,纯读)
|
||||
*/
|
||||
uint16_t iec104_comm_port(void);
|
||||
|
||||
/*
|
||||
* 函数功能:写 ca 到 RAM 副本(守卫 = getter 合法带;非法值不提交)
|
||||
* 入口参数:ca - 公共地址 1..65535 uint16_t
|
||||
* 返回值:0 - 提交;-1 - 带外(副本不动) int
|
||||
* 限定条件:调用方 = UI 保存交接路(task-9);落盘经 SYS_EVT_CFG_SAVE
|
||||
* 整档外科 save(net_cfg_save_request 同款交接,本模块不写盘)
|
||||
*/
|
||||
int iec104_comm_ca_set(uint16_t ca);
|
||||
|
||||
/*
|
||||
* 函数功能:写 port 到 RAM 副本(守卫 = getter 合法带;非法值不提交)
|
||||
* 入口参数:port - 监听端口 1024..65535 uint16_t
|
||||
* 返回值:0 - 提交;-1 - 带外(副本不动) int
|
||||
* 限定条件:同 iec104_comm_ca_set
|
||||
*/
|
||||
int iec104_comm_port_set(uint16_t port);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* IEC104_COMM_H */
|
||||
@@ -161,18 +161,23 @@ static int s_send_asdu(iec104_link_t *lk, uint8_t type, uint8_t vsq,
|
||||
*/
|
||||
static void s_cmd_con(iec104_link_t *lk, uint8_t type, const uint8_t *obj,
|
||||
uint16_t obj_len, uint8_t cot, uint8_t pn) {
|
||||
uint8_t body[BODY_MAX];
|
||||
uint16_t n = obj_len;
|
||||
|
||||
if (n > BODY_MAX) {
|
||||
n = 0u;
|
||||
n = 0u; /* 超长回显退化为空信息体 */
|
||||
}
|
||||
if (n > 0u) {
|
||||
uint8_t body[BODY_MAX]; /* 作用域收进真要回显的分支,不把未初始化缓冲传下去 */
|
||||
|
||||
(void)memcpy(body, obj, n);
|
||||
(void)s_send_asdu(lk, type, 1u, cot,
|
||||
(pn != 0u) ? IEC104_COT_BIT_PN : 0u,
|
||||
0u, 0u, body, n, 0u);
|
||||
} else {
|
||||
(void)s_send_asdu(lk, type, 1u, cot,
|
||||
(pn != 0u) ? IEC104_COT_BIT_PN : 0u,
|
||||
0u, 0u, NULL, 0u, 0u);
|
||||
}
|
||||
(void)s_send_asdu(lk, type, 1u, cot,
|
||||
(pn != 0u) ? IEC104_COT_BIT_PN : 0u,
|
||||
0u, 0u, body, n, 0u);
|
||||
}
|
||||
|
||||
/* ---------------- 数据上送 ---------------- */
|
||||
@@ -280,12 +285,13 @@ static void s_spont_step(iec104_link_t *lk, uint8_t *budget) {
|
||||
uint8_t body[8]; /* 最大 = SIQ + CP56(7)(M_SP_TB_1) */
|
||||
uint8_t i;
|
||||
uint8_t d;
|
||||
uint8_t blen;
|
||||
uint8_t type;
|
||||
int rc;
|
||||
|
||||
while ((*budget > 0u) && ((lk->pend_yx != 0u) || (lk->pend_yc != 0u))) {
|
||||
if (lk->pend_yx != 0u) {
|
||||
uint8_t blen;
|
||||
|
||||
i = 0u;
|
||||
while (i < 33u) {
|
||||
if ((lk->pend_yx & (1uLL << i)) != 0u) {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* → clock_sync 回调)、试验(C_TS)与进程复位(C_RP,清快照)。
|
||||
* **零 I/O**:收 = on_rx 喂 TCP 流片段,发 = send_apdu 回调,
|
||||
* 时基 = 调用方注入 now_ms(tick ms,回绕安全:差值法)
|
||||
* → 全栈(asdu+pts+link)主机侧 L0.5 可离线跑(登记簿 A-4 约束 1)。
|
||||
* → 全栈(asdu+pts+link)主机侧 L0.5 可离线跑(分层约束)。
|
||||
* 参数(DL/T 634.5104 标准默认,§1.5 未另定):k=12 w=8 t1=15s t2=10s t3=20s。
|
||||
* 端口:TCP **2404**(§1.5 B0 定义;本 DTU = 被控站/服务器,主站连入)。
|
||||
* 线程约定:单实例单任务上下文(板端 = 104 服务在 netTask 消息循环内驱动,
|
||||
@@ -25,8 +25,9 @@
|
||||
|
||||
#include "iec104_pts.h"
|
||||
|
||||
/* 服务端口(§1.5:以太网 TCP 2404) */
|
||||
#define IEC104_PORT 2404U
|
||||
/* 服务端口真源已迁 [comm] 段(道G task-8):工厂默认 2404 = IEC104_COMM_DEF_PORT,
|
||||
* 运行期取 iec104_comm_port()(App/protocol/iec104/iec104_comm.h)。原死 define
|
||||
* IEC104_PORT 全仓零 .c 消费,2026-10-26 删除(旧引用见 docs/UI设计/页面扩充详细方案.md)。 */
|
||||
|
||||
/* 链路参数(标准默认值) */
|
||||
#define IEC104_K_MAX 12U /* 未确认 I 帧上限 */
|
||||
|
||||
+348
-103
@@ -1,43 +1,94 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 点表与数据源适配(实现)
|
||||
* 模块功能:实现 iec104_pts.h——YC/YX/YK/定值点表(ROM)、板端默认源
|
||||
* (phasor / prot_elm / prot_set)、COV 快照比对(遥信变位 +
|
||||
* 遥测死区)。点表口径见 iec104_pts.h 文件头与设计文档 §3。
|
||||
* (**L2 遥测快照**(卡 9)/ prot_elm / prot_set)、COV 比对(遥信变位 +
|
||||
* 遥测死区 2 点确认 + 品质变化)。点表口径见 iec104_pts.h 文件头与
|
||||
* 设计文档 §3。
|
||||
* 线程约定:init/bind 建服务前;其余仅 104 服务任务上下文(快照非原子)。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
* 修改记录:
|
||||
* 2026-10-25 DTU 项目组 卡 6(「数据可信」):YC 死区由拟定码值域重定为
|
||||
* 二次值工程量域(额定 0.5 % 规则),判据依据见
|
||||
* k_yc 表头注释;数值随卡 9 迁运行参数,本表将退化为
|
||||
* ROM 默认载体。
|
||||
* 2026-10-25 DTU 项目组 卡 9/卡 11(「数据可信」,道D):YC 板端源改读
|
||||
* L2 遥测快照(App/algo/meas.h,meas_get 无锁双区);
|
||||
* 削顶五路由快照品质 CLIPPED 驱动线上 IV(不再绑死 -1,
|
||||
* V0.2 消除双转换器无共同采样触发后零改动自动启用);0x401d 绑频率源(卡 11);
|
||||
* 死区改运行时副本 + set/get 入口;COV 遥测腿 = 死区
|
||||
* 连续 2 点确认(板端按快照 seq 去重)+ 品质翻转即报。
|
||||
* 单测 = test/host/iec104 TC-IEC104-025~028。
|
||||
* 卡 10 2026-10-25 DTU 项目组 死区真源收编 App/algo/run_par.c(本层 s_yc_db
|
||||
* 副本删除,set/get 转转发壳,壳保留清确认计数语义);
|
||||
* 定值读写路由 = run_par 表先、prot_set 后(0x8020~
|
||||
* 0x8058 运行参数段 C_RD/C_SE,系数覆盖直通 adc_cal)。
|
||||
* 2026-10-26 DTU 项目组 道G task-8「UI 补齐」第 5 项非 UI 前置:新增
|
||||
* [comm] 通讯参数段(ca/port)初版落本文件尾部;
|
||||
* s_src_yk_exec() 等遥控/记录投递路径零改动。
|
||||
* 2026-10-26 DTU 项目组 同日晚纠偏:[comm] 实现整体迁出至配对模块
|
||||
* iec104_comm.c(coding-standard CODE-RULE-17 (2.2)
|
||||
* 按文件名配对判定,本文件的公开符号必须声明于
|
||||
* iec104_pts.h——[comm] 不属点表,拆出正解;契约
|
||||
* = iec104_comm.h,本文件不再引用 cfg_file)。
|
||||
*/
|
||||
#include "iec104_pts.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "iec104_asdu.h" /* QDS/DIQ 品质位常量 */
|
||||
#include "phasor.h" /* 板端源:遥测 = 基波有效值 */
|
||||
#include "meas.h" /* 板端源:遥测 = L2 遥测快照(卡 9;品质域位同源) */
|
||||
#include "run_par.h" /* 卡 10:死区真源 + 运行参数段 104 读写路由 */
|
||||
#include "prot_elm.h" /* 板端源:遥信 = 元件状态 */
|
||||
#include "prot_set.h" /* 板端源:定值 + 闭锁 */
|
||||
#include "sys_log.h" /* task-10:遥控记录(SYSLOG_YK)/ 操作记录(SYSLOG_OPER) */
|
||||
#include "adc_input.h" /* adc_semantic_t(phasor 实例按语义绑定) */
|
||||
|
||||
/* ---------------- YC 表(§1.5 点号 4011+;死区 = 拟定码值,L1-5 后复核) -- */
|
||||
/* ---------------- YC 表(§1.5 点号 4011+;死区列 = ROM 默认值) ------------ */
|
||||
/* 源类型(「数据可信」卡 11:YC 表加 src 类型区分相量/频率) */
|
||||
#define PTS_SRC_MEAS 0u /* L2 快照槽 sem(电气量,yc/qds 直读) */
|
||||
#define PTS_SRC_FREQ 1u /* 快照频率(freq_valid 门 → IV) */
|
||||
|
||||
typedef struct {
|
||||
uint32_t ioa;
|
||||
int8_t sem; /* 绑定的 phasor 语义通道(-1 = 无 phasor 源,恒无效) */
|
||||
float deadband; /* 死区(工程量绝对值) */
|
||||
int8_t sem; /* 快照 yc 槽 = 语义通道(-1 = 非测量量点,当前仅频率) */
|
||||
float db_default; /* 死区工厂默认(生效真源 = run_par [run_par] 段,卡 10) */
|
||||
uint8_t src; /* PTS_SRC_* 源类型 */
|
||||
} pts_yc_def_t;
|
||||
|
||||
/* 死区规则 = 额定二次值的 0.5 %(「数据可信」§2.3 表第 2 行建议规则,卡 6 采纳;
|
||||
* 额定基准 100 V / 1 A 与 App/algo/prot_set.c 头部同源,勿在此另立口径):
|
||||
* 电压类 0.5 % × 100 V = 0.5 V 电流类 0.5 % × 1 A = 0.005 A
|
||||
* 频率点不适用"额定百分比"(额定附近 0.5 Hz 与旧值同量级,维持 0.5 并随
|
||||
* 卡 10 运行参数段可调)。自卡 9 起本表**只作工厂默认载体**:生效死区 =
|
||||
* App/algo/run_par.c 的 [run_par] 引擎副本(卡 10 真源收编,本层不再存副本);削顶五路不再 sem=-1 绑死,而是绑快照槽、由品质 CLIPPED
|
||||
* 驱动线上 IV——V0.2 削顶消失后点位零改动自动启用(docs/任务规划/
|
||||
* 数据可信.md §10 的「双转换器无共同采样触发」行)。
|
||||
* ⚠ 过渡期(eng=0 引脚电压域,卡 12 标定前):上送值本身非二次值,死区
|
||||
* 数字按本域偏保守(迟报不漏报);标定落地后复核(「数据可信」§2.3 表
|
||||
* 第 2 行)。判定 = 死区连续 2 点确认(卡 9),见 iec104_pts.h COV 说明。 */
|
||||
static const pts_yc_def_t k_yc[] = {
|
||||
{ 0x4011u, (int8_t)ADC_SEM_UA, 10.0f }, /* 1Ua */
|
||||
{ 0x4012u, -1, 10.0f }, /* 1Ub:V0.1 削顶 → IV */
|
||||
{ 0x4013u, -1, 10.0f }, /* 1Uc:同上 */
|
||||
{ 0x4014u, (int8_t)ADC_SEM_UN, 10.0f }, /* 1U0(外部零序专用通道) */
|
||||
{ 0x4015u, (int8_t)ADC_SEM_IA, 10.0f }, /* 1Ia */
|
||||
{ 0x4016u, -1, 10.0f }, /* 1Ib */
|
||||
{ 0x4017u, -1, 10.0f }, /* 1Ic */
|
||||
{ 0x4018u, -1, 10.0f }, /* 1I0 */
|
||||
{ 0x401Du, -1, 0.5f }, /* 1f:L1-7 未实现 → IV */
|
||||
{ 0x4011u, (int8_t)ADC_SEM_UA, 0.5f, PTS_SRC_MEAS }, /* 1Ua:0.5 % × 100 V */
|
||||
{ 0x4012u, (int8_t)ADC_SEM_UB, 0.5f, PTS_SRC_MEAS }, /* 1Ub:削顶 → 品质驱动 IV */
|
||||
{ 0x4013u, (int8_t)ADC_SEM_UC, 0.5f, PTS_SRC_MEAS }, /* 1Uc:同上 */
|
||||
{ 0x4014u, (int8_t)ADC_SEM_UN, 0.5f, PTS_SRC_MEAS }, /* 1U0(外部零序专用通道) */
|
||||
{ 0x4015u, (int8_t)ADC_SEM_IA, 0.005f, PTS_SRC_MEAS }, /* 1Ia:0.5 % × 1 A */
|
||||
{ 0x4016u, (int8_t)ADC_SEM_IB, 0.005f, PTS_SRC_MEAS }, /* 1Ib:削顶 → 品质驱动 IV */
|
||||
{ 0x4017u, (int8_t)ADC_SEM_IC, 0.005f, PTS_SRC_MEAS }, /* 1Ic:同上 */
|
||||
{ 0x4018u, (int8_t)ADC_SEM_IN, 0.005f, PTS_SRC_MEAS }, /* 1I0:同上 */
|
||||
{ 0x401Du, -1, 0.5f, PTS_SRC_FREQ }, /* 1f:卡 11 绑频率源,
|
||||
* freq_valid=0 → IV */
|
||||
};
|
||||
#define YC_N ((uint8_t)(sizeof(k_yc) / sizeof(k_yc[0])))
|
||||
|
||||
/* phasor 实例下标缓存(init 时按 sem+harm==1 查表;-1 = 未找到) */
|
||||
static int8_t s_yc_phasor_idx[YC_N];
|
||||
/* ===== 遥测 COV 运行时状态(卡 9;仅 iec104Task 上下文访问,无并发) =====
|
||||
* 判据链:新测量点 → |v − 上次上送值| > 死区?→ 连续 2 点确认 → 置变化位。
|
||||
* 基准 = **上次上送值**(非上次读值——否则慢漂移永不出死区,参考系统同款);
|
||||
* 品质(线上 qds)翻转 = 事件:不参与 2 点计数、当拍直报。 */
|
||||
static float s_yc_rep[YC_N]; /* 上次上送值(建线/确认上送时更新) */
|
||||
static uint8_t s_yc_repq[YC_N]; /* 上次上送的线上 qds */
|
||||
static uint8_t s_yc_cnf[YC_N]; /* 越死区连续计数(1 = 待确认;2 = 确认) */
|
||||
static uint32_t s_yc_seq; /* COV 已消费到的快照 seq(板端源测点去重) */
|
||||
static uint8_t s_yc_seqmode; /* 1 = 板端默认源(测点 = meas 换帧) */
|
||||
|
||||
/* ---------------- YX 表(IOA = 1..33;双点 = 开关位置类) ------------------ */
|
||||
#define YX_N 33u
|
||||
@@ -48,63 +99,62 @@ static int8_t s_yc_phasor_idx[YC_N];
|
||||
| (1uLL << 17u) | (1uLL << 22u) | (1uLL << 30u)))
|
||||
|
||||
/* ---------------- 数据源(默认 = 板端;bind_src 可整体替换) -------------- */
|
||||
static int s_src_yc_read(uint8_t idx, float *out);
|
||||
static int s_src_yc_read_ex(uint8_t idx, float *out, uint16_t *qds);
|
||||
static uint64_t s_src_yx_bits(void);
|
||||
static uint64_t s_src_yx_valid(void);
|
||||
static int s_src_yk_exec(uint32_t ioa, uint8_t dpc, uint8_t select);
|
||||
static int s_src_set_read(uint32_t addr, float *out);
|
||||
static int s_src_set_write(uint32_t addr, float v, uint8_t select);
|
||||
|
||||
/* 板端默认源:遥测走**带品质**的 ex 通道(legacy yc_read 槽置 NULL——
|
||||
* s_yc_point 优先 ex;注入源两种槽位均可,见 iec104_pts.h) */
|
||||
static const iec104_pts_src_t k_default_src = {
|
||||
s_src_yc_read, s_src_yx_bits, s_src_yx_valid,
|
||||
s_src_yk_exec, s_src_set_read, s_src_set_write
|
||||
NULL, s_src_yx_bits, s_src_yx_valid,
|
||||
s_src_yk_exec, s_src_set_read, s_src_set_write, s_src_yc_read_ex
|
||||
};
|
||||
static const iec104_pts_src_t *s_src;
|
||||
|
||||
/* ---------------- COV 快照 ---------------- */
|
||||
static float s_yc_last[YC_N];
|
||||
static uint16_t s_yc_have; /* bit i = 已有上次快照 */
|
||||
/* ---------------- COV 快照(遥信侧;遥测侧状态见上方 s_yc_* 块) --------- */
|
||||
static uint16_t s_yc_have; /* bit i = 已建立上送基线 */
|
||||
static uint64_t s_yx_last;
|
||||
static uint8_t s_yx_snap_ok;
|
||||
|
||||
/* ============================ 公开接口 ================================== */
|
||||
|
||||
/*
|
||||
* 函数功能:初始化点表模块(扫 phasor 基波实例映射、恢复默认源、清快照)
|
||||
* 函数功能:初始化点表模块(死区装 ROM 默认、清 COV/快照状态、恢复默认源)
|
||||
* 限定条件:内核启动前或 104 服务启动前,调一次
|
||||
* 函数说明:卡 9 起不再扫 phasor 实例(数据源 = 快照,槽位 = 语义通道,编译期
|
||||
* 直接可定);重复调用 = 全状态复位(单测 reset 依赖此语义)。
|
||||
*/
|
||||
void iec104_pts_init(void) {
|
||||
uint8_t i;
|
||||
uint8_t n;
|
||||
|
||||
(void)memset(s_yc_phasor_idx, -1, sizeof(s_yc_phasor_idx));
|
||||
n = phasor_inst_count();
|
||||
for (i = 0u; i < YC_N; i++) {
|
||||
uint8_t j;
|
||||
|
||||
if (k_yc[i].sem < 0) {
|
||||
continue;
|
||||
}
|
||||
for (j = 0u; j < n; j++) {
|
||||
const phasor_inst_t *inst = phasor_inst(j);
|
||||
|
||||
if ((inst != NULL) && ((uint8_t)inst->sem == (uint8_t)k_yc[i].sem)
|
||||
&& (inst->harm == 1u)) {
|
||||
s_yc_phasor_idx[i] = (int8_t)j;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
s_src = &k_default_src;
|
||||
s_yc_have = 0u;
|
||||
/* 死区不在本层复位——真源 = run_par(cfg 引擎副本),复位入口唯一
|
||||
* run_par_init();本函数只复位 COV 状态(单测 reset 依赖此语义) */
|
||||
(void)memset(s_yc_rep, 0, sizeof(s_yc_rep));
|
||||
(void)memset(s_yc_repq, 0, sizeof(s_yc_repq));
|
||||
(void)memset(s_yc_cnf, 0, sizeof(s_yc_cnf));
|
||||
s_yc_have = 0u;
|
||||
s_yc_seq = 0u;
|
||||
s_yc_seqmode = 1u;
|
||||
s_src = &k_default_src;
|
||||
s_yx_snap_ok = 0u;
|
||||
s_yx_last = 0u;
|
||||
s_yx_last = 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:替换数据源(NULL = 恢复板端默认源;单测注入假源)
|
||||
* 函数说明:注入源同时把 COV 测点口径切为**每次调用一测点**(假源无快照节拍,
|
||||
* 步进由调用方控制);默认源 = 测点按 meas 换帧(seq 去重)。换源
|
||||
* 不重建基线(基线状态统一由 init 复位,防跨源半截确认序列)。
|
||||
*/
|
||||
void iec104_pts_bind_src(const iec104_pts_src_t *src) {
|
||||
s_src = (src != NULL) ? src : &k_default_src;
|
||||
if (src != NULL) {
|
||||
s_src = src;
|
||||
s_yc_seqmode = 0u;
|
||||
} else {
|
||||
s_src = &k_default_src;
|
||||
s_yc_seqmode = 1u;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -122,35 +172,108 @@ uint32_t iec104_pts_yc_ioa(uint8_t idx) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读遥测(值 + 品质 descriptor;源 -2 → NT,其余失败 → IV)
|
||||
* 返 回 值:0 有效 / -1 无效
|
||||
* 函数功能:品质域位映射(uint16 meas 域 → uint8 线上 QDS,卡 9)
|
||||
* 入口参数:q16 - 源侧品质(低字节与线上同值;高字节为 MEAS_QDS_CLIPPED 域内位)
|
||||
* 返 回 值:线上 QDS 字节 uint8_t
|
||||
* 函数说明:CLIPPED(硬件削顶,104 无对应位)映射为 IV——「削顶通道 104 恒
|
||||
* IV」口径(docs/任务规划/数据可信.md §8 风险表末行)改由快照品质
|
||||
* 驱动,不再靠点表绑死 -1;OV/NT/IV 透传,其余高位丢弃。
|
||||
*/
|
||||
int iec104_pts_yc_read(uint8_t idx, float *v, uint8_t *qds) {
|
||||
int rc;
|
||||
static uint8_t s_q_map(uint16_t q16) {
|
||||
uint8_t w = (uint8_t)(q16 & (uint16_t)(IEC104_QDS_OV | IEC104_QDS_NT
|
||||
| IEC104_QDS_IV));
|
||||
|
||||
if (v != NULL) {
|
||||
*v = 0.0f;
|
||||
if ((q16 & (uint16_t)MEAS_QDS_CLIPPED) != 0u) {
|
||||
w |= (uint8_t)IEC104_QDS_IV;
|
||||
}
|
||||
if ((idx >= YC_N) || (s_src == NULL) || (s_src->yc_read == NULL)) {
|
||||
if (qds != NULL) {
|
||||
*qds = IEC104_QDS_IV;
|
||||
}
|
||||
return w;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读单点遥测(统一源通道 + 品质归一,yc_read 与 COV 共用唯一读径)
|
||||
* 入口参数:idx - YC 表下标 v/q - 输出(值 / 线上 qds;调用方保证非空)
|
||||
* 返 回 值:0 有效;-1 IV(含 CLIPPED 映射);-2 NT(未就绪) int
|
||||
* 函数说明:ex 源在场 → 品质由源给(板端 = 快照槽位透传);否则退 legacy
|
||||
* yc_read,品质从返回值推导(-1→IV / -2→NT / 0→无位)——旧注入源
|
||||
* 行为与卡 9 前**完全一致**(单测假源零改动)。判定顺序 IV > NT:
|
||||
* 开机首帧 qds=IV|NT 按无效处理(NT 只留给纯暂态)。任何无效 →
|
||||
* 信息体清零(线上口径:主站只信品质)。
|
||||
*/
|
||||
static int s_yc_point(uint8_t idx, float *v, uint8_t *q) {
|
||||
int rc;
|
||||
uint16_t q16 = 0u;
|
||||
|
||||
if ((idx >= YC_N) || (s_src == NULL)
|
||||
|| ((s_src->yc_read == NULL) && (s_src->yc_read_ex == NULL))) {
|
||||
*v = 0.0f;
|
||||
*q = (uint8_t)IEC104_QDS_IV;
|
||||
return -1;
|
||||
}
|
||||
rc = s_src->yc_read(idx, v);
|
||||
if (rc == 0) {
|
||||
if (qds != NULL) {
|
||||
*qds = 0u;
|
||||
}
|
||||
return 0;
|
||||
if (s_src->yc_read_ex != NULL) {
|
||||
rc = s_src->yc_read_ex(idx, v, &q16);
|
||||
} else {
|
||||
rc = s_src->yc_read(idx, v);
|
||||
q16 = (rc == -2) ? (uint16_t)MEAS_QDS_NT
|
||||
: ((rc != 0) ? (uint16_t)MEAS_QDS_IV : 0u);
|
||||
}
|
||||
if (v != NULL) {
|
||||
(void)rc; /* 有效性由归一后的 q 终裁(IV/NT 即无效),源返回值不直通 */
|
||||
*q = s_q_map(q16);
|
||||
rc = ((*q & (uint8_t)IEC104_QDS_IV) != 0u) ? -1
|
||||
: (((*q & (uint8_t)IEC104_QDS_NT) != 0u) ? -2 : 0);
|
||||
if (rc != 0) {
|
||||
*v = 0.0f;
|
||||
}
|
||||
if (qds != NULL) {
|
||||
*qds = (rc == -2) ? IEC104_QDS_NT : IEC104_QDS_IV;
|
||||
return rc;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读遥测(值 + 线上品质位;无效值清零,判据见 s_yc_point)
|
||||
* 返 回 值:0 有效 / -1 无效(NT 亦无效,品质位与 IV 区分) int
|
||||
*/
|
||||
int iec104_pts_yc_read(uint8_t idx, float *v, uint8_t *qds) {
|
||||
float val = 0.0f;
|
||||
uint8_t q = (uint8_t)IEC104_QDS_IV;
|
||||
|
||||
(void)s_yc_point(idx, &val, &q);
|
||||
if (v != NULL) {
|
||||
*v = val;
|
||||
}
|
||||
return -1;
|
||||
if (qds != NULL) {
|
||||
*qds = q;
|
||||
}
|
||||
return (q == 0u) ? 0 : -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:写死区(卡 9 API 壳,转发 run_par 真源 + 清本层确认计数)
|
||||
* 入口参数:idx - YC 表下标 db - 工程量绝对死区(≥0,分档上限见 run_par)
|
||||
* 返 回 值:0 成功;-1 越界/负值/NaN/越程(run_par 副本不动) int
|
||||
* 限定条件:**iec104Task 上下文**(run_par 副本写 = 引擎对齐 int32 单字,
|
||||
* 104 执行段之外调用需自行避免与 104 写并发)
|
||||
* 函数说明:成功时清零该点待确认计数——跨死区档位不保留半截确认序列;
|
||||
* 不隐式重建基线(上送基准延续,新死区即刻生效)。落盘由 run_par
|
||||
* 脏标志 + SYS_EVT_RUN_PAR_SAVE 走 defaultTask(卡 10 持久化链)。
|
||||
*/
|
||||
int iec104_pts_yc_set_deadband(uint8_t idx, float db) {
|
||||
if (idx >= YC_N) {
|
||||
return -1;
|
||||
}
|
||||
if (run_par_deadband_set(idx, db) != 0) {
|
||||
return -1; /* 守卫(NaN/负/越程分档上限)单一实现,在 run_par */
|
||||
}
|
||||
s_yc_cnf[idx] = 0u; /* 壳语义:跨死区档位不保留半截确认序列 */
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读当前生效死区(转发 run_par 真源)
|
||||
* 入口参数:idx - YC 表下标
|
||||
* 返 回 值:生效死区;idx 越界返 0.0 float
|
||||
* 限定条件:任意上下文(引擎副本 int32 单字读,Cortex-M 原子)
|
||||
* 函数说明:UI 回显 / 读写往返校验用;工厂默认查点表 db_default 列。
|
||||
*/
|
||||
float iec104_pts_yc_deadband(uint8_t idx) {
|
||||
return (idx < YC_N) ? run_par_deadband(idx) : 0.0f;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -225,28 +348,72 @@ void iec104_pts_cov_check(iec104_pts_cov_t *cov) {
|
||||
s_yx_snap_ok = 1u;
|
||||
}
|
||||
|
||||
/* YC:死区比对(仅对"本次有效 且 上次有快照"的点判定) */
|
||||
if ((s_src != NULL) && (s_src->yc_read != NULL)) {
|
||||
uint8_t i;
|
||||
/* YC(卡 9):测点推进 = 板端源按快照换帧(100 ms poll vs 200 ms 快照,
|
||||
* 不去重则同一帧计两次 → "连续 2 点确认"退化成"连续 2 次轮询",单帧
|
||||
* 噪声即可被确认,抗噪语义失效);注入源 = 每次调用一测点。
|
||||
* 判定 = 死区连续 2 点确认 + 品质翻转直报(口径全文 → iec104_pts.h)。 */
|
||||
if ((s_src != NULL)
|
||||
&& ((s_src->yc_read != NULL) || (s_src->yc_read_ex != NULL))) {
|
||||
uint8_t step = 1u;
|
||||
|
||||
for (i = 0u; i < YC_N; i++) {
|
||||
float v;
|
||||
if (s_yc_seqmode != 0u) {
|
||||
uint32_t sq = meas_seq();
|
||||
|
||||
if (s_src->yc_read(i, &v) != 0) {
|
||||
continue;
|
||||
if (sq == s_yc_seq) {
|
||||
step = 0u; /* 未换帧:本 poll 不算新测量点 */
|
||||
} else {
|
||||
s_yc_seq = sq;
|
||||
}
|
||||
if (((s_yc_have >> i) & 1u) != 0u) {
|
||||
float d = v - s_yc_last[i];
|
||||
}
|
||||
if (step != 0u) {
|
||||
uint8_t i;
|
||||
|
||||
if (d < 0.0f) {
|
||||
d = -d;
|
||||
for (i = 0u; i < YC_N; i++) {
|
||||
float v;
|
||||
uint8_t q;
|
||||
int rc = s_yc_point(i, &v, &q);
|
||||
|
||||
if (((s_yc_have >> i) & 1u) == 0u) {
|
||||
if (rc == 0) { /* 首个有效读数只建线不上报(初值归总召) */
|
||||
s_yc_have |= (uint16_t)(1u << i);
|
||||
s_yc_rep[i] = v;
|
||||
s_yc_repq[i] = q;
|
||||
s_yc_cnf[i] = 0u;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (d > k_yc[i].deadband) {
|
||||
if (q != s_yc_repq[i]) {
|
||||
/* 品质翻转 = 事件直报(含 0x401d 的 IV 进出,卡 11) */
|
||||
local.yc_changed |= (uint16_t)(1u << i);
|
||||
s_yc_repq[i] = q;
|
||||
s_yc_cnf[i] = 0u;
|
||||
if (rc == 0) {
|
||||
s_yc_rep[i] = v;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (rc != 0) {
|
||||
s_yc_cnf[i] = 0u; /* 无效期间不计数(品质未变 = 状态延续) */
|
||||
continue;
|
||||
}
|
||||
{
|
||||
float d = v - s_yc_rep[i];
|
||||
|
||||
if (d < 0.0f) {
|
||||
d = -d;
|
||||
}
|
||||
if (d > run_par_deadband(i)) {
|
||||
s_yc_cnf[i]++;
|
||||
if (s_yc_cnf[i] >= 2u) { /* 连续 2 越 → 确认:报 + 移基准 */
|
||||
local.yc_changed |= (uint16_t)(1u << i);
|
||||
s_yc_rep[i] = v;
|
||||
s_yc_cnf[i] = 0u;
|
||||
}
|
||||
} else {
|
||||
s_yc_cnf[i] = 0u; /* 回摆入死区 → 撤销候选(抗噪核心) */
|
||||
}
|
||||
}
|
||||
}
|
||||
s_yc_last[i] = v;
|
||||
s_yc_have |= (uint16_t)(1u << i);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,24 +458,44 @@ int iec104_pts_set_write(uint32_t addr, float v, uint8_t select) {
|
||||
/* ============================ 板端默认源 ================================= */
|
||||
|
||||
/*
|
||||
* 函数功能:板端默认源 - 遥测 = 基波相量有效值(fAmSQ)
|
||||
* 返 回 值:0 / -1 无 phasor 源(削顶通道/频率未实现)/ -2 未满一周波(NT)
|
||||
* 函数功能:板端默认源 - 遥测 = L2 遥测快照直读(「数据可信」卡 9/卡 11)
|
||||
* 入口参数:idx - YC 表下标 out/qds - 值 + uint16 域品质(MEAS_QDS_*)
|
||||
* 返 回 值:0 / -1 越界或频率未新鲜(IV)/ -2 快照从未发布(NT) int
|
||||
* 限定条件:iec104Task 上下文(meas_get 为双区无锁读,App/algo/meas.h)
|
||||
* 函数说明:**不做任何本地判断**——电气路值/品质整体透传快照槽(削顶路快照
|
||||
* 已置 CLIPPED,s_q_map 映上线 IV;开机首帧 IV|NT 由 s_yc_point 按
|
||||
* IV 处理),"新不新、能不能信"全在发布侧一处决定(卡 8 设计意图,
|
||||
* 两处判断必打架)。频率路(PTS_SRC_FREQ):snap.freq_valid==0 →
|
||||
* IV(卡 11 DoD 原句);有效时值 = snap.freq_hz,与 freq_get() 同源。
|
||||
*/
|
||||
static int s_src_yc_read(uint8_t idx, float *out) {
|
||||
phasor_t p;
|
||||
int8_t pi;
|
||||
static int s_src_yc_read_ex(uint8_t idx, float *out, uint16_t *qds) {
|
||||
meas_snap_t snap;
|
||||
|
||||
if ((idx >= YC_N) || (out == NULL)) {
|
||||
if ((idx >= YC_N) || (out == NULL) || (qds == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
pi = s_yc_phasor_idx[idx];
|
||||
if (pi < 0) {
|
||||
return -1; /* 无 phasor 源(削顶通道/频率未实现) */
|
||||
if (meas_get(&snap) != 0) {
|
||||
*out = 0.0f; /* 从未发布(开机 <200 ms)= 暂态 */
|
||||
*qds = (uint16_t)(MEAS_QDS_IV | MEAS_QDS_NT);
|
||||
return -2;
|
||||
}
|
||||
if (phasor_get((uint8_t)pi, &p) != 0) {
|
||||
return -2; /* 未满一周波:未就绪(NT) */
|
||||
if (k_yc[idx].src == (uint8_t)PTS_SRC_FREQ) {
|
||||
if (snap.freq_valid == 0u) {
|
||||
*out = 0.0f;
|
||||
*qds = (uint16_t)MEAS_QDS_IV;
|
||||
return -1;
|
||||
}
|
||||
*out = snap.freq_hz;
|
||||
*qds = 0u;
|
||||
return 0;
|
||||
}
|
||||
*out = p.fAmSQ; /* 有效值口径(系数已折算) */
|
||||
if (k_yc[idx].sem < 0) {
|
||||
*out = 0.0f; /* 理论不可达:现无未绑测量量点 */
|
||||
*qds = (uint16_t)MEAS_QDS_IV;
|
||||
return -1;
|
||||
}
|
||||
*out = snap.yc[(uint8_t)k_yc[idx].sem];
|
||||
*qds = snap.qds[(uint8_t)k_yc[idx].sem];
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -366,31 +553,77 @@ static uint64_t s_src_yx_valid(void) {
|
||||
* YK12 走 prot_set_set_sealed(原子写+封存,B7);protectTask 下一拍
|
||||
* 读到压板态即复位元件并停判据(protect_task.c)。
|
||||
*/
|
||||
/* 遥控记录的文案表(task-10)。**全部 ROM 常量、零拼接**:本回调跑在
|
||||
* iec104Task,而 iec104_serv.c 的日志缓冲已经因为"深链模块"被收到 128 B,
|
||||
* 这里再 snprintf 一条"YK ioa=0x600B dpc=2 sel=0 rc=0"就是往那条链上加缓冲;
|
||||
* 记录类型由 SYSLOG_YK 这个文件本身回答,屏上话术归 UI(App/log/README.md 口径)。
|
||||
* 最长一条 20 字符 < SYSLOG_DESC_MAX(28)。 */
|
||||
#define YK_REC_YK11_SEL "YK11 select"
|
||||
#define YK_REC_YK11_EXEC "YK11 exec"
|
||||
#define YK_REC_YK12_SEL "YK12 select"
|
||||
#define YK_REC_YK12_ON "YK12 exec on"
|
||||
#define YK_REC_YK12_OFF "YK12 exec off"
|
||||
#define YK_REC_YK12_FAIL "YK12 exec fail"
|
||||
#define YK_REC_DPC_BAD "YK12 bad dpc"
|
||||
#define YK_REC_UNKNOWN "YK unknown ioa"
|
||||
|
||||
static int s_src_yk_exec(uint32_t ioa, uint8_t dpc, uint8_t select) {
|
||||
switch (ioa) {
|
||||
case 0x600Au: /* YK11 信号复归 → 元件状态复位(异步一拍) */
|
||||
(void)dpc; /* 复归与双点值无关 */
|
||||
if (select == 0u) {
|
||||
prot_elm_request_reset();
|
||||
/* 遥控记录(执行段已受理)+ 操作记录(本地/远端强制复归)。
|
||||
* 两条不是重复留痕:前者回答"主站下了什么命令、结果如何",后者
|
||||
* 回答"装置被谁改动了状态"——UI 的遥控记录页与操作记录页各读一份。
|
||||
* YK11 没有可失败的下游(request_reset 只置单字节标志),所以它
|
||||
* 只有受理态,没有 fail 分支。 */
|
||||
syslog_post_quiet(SYSLOG_YK, YK_REC_YK11_EXEC);
|
||||
syslog_post_quiet(SYSLOG_OPER, "force reset");
|
||||
} else {
|
||||
syslog_post_quiet(SYSLOG_YK, YK_REC_YK11_SEL);
|
||||
}
|
||||
return 0;
|
||||
case 0x600Bu: /* YK12 全局软压板(PS_GLOBAL_EN)两段式 */
|
||||
case 0x600Bu: { /* YK12 全局软压板(PS_GLOBAL_EN)两段式 */
|
||||
int rc;
|
||||
if ((dpc != (uint8_t)IEC104_DPC_ON) && (dpc != (uint8_t)IEC104_DPC_OFF)) {
|
||||
syslog_post_quiet(SYSLOG_YK, YK_REC_DPC_BAD);
|
||||
return -1; /* 非法双点命令:选择段即拒 */
|
||||
}
|
||||
if (select != 0u) {
|
||||
syslog_post_quiet(SYSLOG_YK, YK_REC_YK12_SEL);
|
||||
return 0; /* 选择段 = 只校验 */
|
||||
}
|
||||
return prot_set_set_sealed(PS_GLOBAL_EN,
|
||||
(dpc == (uint8_t)IEC104_DPC_ON)
|
||||
? PROT_SET_Q10(1) : PROT_SET_Q10(0));
|
||||
/* 执行段:set_sealed 自带范围校验与封存,失败 = 值被拒、RAM 未改。
|
||||
* 这里**必须接住返回值再落记录**(bsp-err:错误码不得裸丢弃;
|
||||
* 原实现是 return 直出,现在多一条记录后仍然原值返回,语义不变)。 */
|
||||
rc = prot_set_set_sealed(PS_GLOBAL_EN,
|
||||
(dpc == (uint8_t)IEC104_DPC_ON)
|
||||
? PROT_SET_Q10(1) : PROT_SET_Q10(0));
|
||||
if (rc != 0) {
|
||||
syslog_post_quiet(SYSLOG_YK, YK_REC_YK12_FAIL);
|
||||
} else {
|
||||
syslog_post_quiet(SYSLOG_YK,
|
||||
(dpc == (uint8_t)IEC104_DPC_ON) ? YK_REC_YK12_ON
|
||||
: YK_REC_YK12_OFF);
|
||||
/* 压板投退是一次真实的运维动作(保护退出/恢复服务)→ 同时进操作记录 */
|
||||
syslog_post_quiet(SYSLOG_OPER,
|
||||
(dpc == (uint8_t)IEC104_DPC_ON) ? "plate in"
|
||||
: "plate out");
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
default:
|
||||
syslog_post_quiet(SYSLOG_YK, YK_REC_UNKNOWN);
|
||||
return -1; /* 其余 YK:无出口硬件/无对应能力 → 拒绝 */
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:板端默认源 - 定值读(扫 g_prot_set_meta 通讯地址映射)
|
||||
* 函数功能:板端默认源 - 定值读(**路由顺序 = run_par 表先、未命中落 prot_set**:
|
||||
* 0x8027~0x802A 属 prot_set 告警段,run_par 命中集为精确地址匹配、
|
||||
* 永不覆盖之;两表共存靠「先专属表、后通用扫」分流,勿反序——反序
|
||||
* 当前无冲突但失去 run_par 守卫先行留痕的意义)
|
||||
* 返 回 值:0 / -1 未知地址(本地项 comm_addr=0 不可寻址,B7 起显式排除)
|
||||
*/
|
||||
static int s_src_set_read(uint32_t addr, float *out) {
|
||||
@@ -399,6 +632,9 @@ static int s_src_set_read(uint32_t addr, float *out) {
|
||||
if (out == NULL) {
|
||||
return -1;
|
||||
}
|
||||
if (run_par_104_read(addr, out) == 0) {
|
||||
return 0; /* 运行参数段命中(卡 10) */
|
||||
}
|
||||
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
|
||||
if ((g_prot_set_meta[i].comm_addr != 0u)
|
||||
&& (g_prot_set_meta[i].comm_addr == (uint16_t)addr)) {
|
||||
@@ -410,13 +646,22 @@ static int s_src_set_read(uint32_t addr, float *out) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:板端默认源 - 定值写(Q10 范围校验;执行段 prot_set_set_sealed
|
||||
* 原子写+封存,B7 收口 B5 竞态窗口;置脏 → 104 任务通知落盘)
|
||||
* 返 回 值:0 / -1 越界或未知地址(本地项 comm_addr=0 不可寻址)
|
||||
* 函数功能:板端默认源 - 定值写(**先 run_par 后 prot_set**,分流理由见
|
||||
* s_src_set_read 头注;prot_set 腿 = Q10 范围校验 + 执行段
|
||||
* prot_set_set_sealed 原子写+封存,B7 收口 B5 竞态窗口)
|
||||
* 返 回 值:0 / -1 越界或未知地址(本地项 comm_addr=0 不可寻址);两条落盘
|
||||
* 链独立:run_par 腿脏 → SYS_EVT_RUN_PAR_SAVE(cfg_file),
|
||||
* prot_set 腿脏 → SYS_EVT_PROT_SET_SAVE(/prot_set.bin)
|
||||
*/
|
||||
static int s_src_set_write(uint32_t addr, float v, uint8_t select) {
|
||||
uint32_t i;
|
||||
int32_t q10;
|
||||
int rp;
|
||||
|
||||
rp = run_par_104_write(addr, v, select);
|
||||
if (rp != -2) {
|
||||
return rp; /* 本段地址:0 接受 / -1 拒绝,不再下探 */
|
||||
}
|
||||
|
||||
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
|
||||
const prot_set_meta_t *meta = &g_prot_set_meta[i];
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
* 模块功能:104 点表(遥测 YC / 遥信 YX / 遥控 YK / 定值)的定义、读取与变化检测
|
||||
* (COV:遥信变位 + 遥测死区)。**数据源经 vtable 注入**:板端绑
|
||||
* phasor/prot_elm/prot_set 真实源;主机单测绑假源 → 协议栈全链
|
||||
* (asdu+pts+link)可离线跑(登记簿 A-4 约束 1)。
|
||||
* (asdu+pts+link)可离线跑(分层约束:协议解析与硬件 I/O 分离)。
|
||||
* 点表口径(docs/算法/继电保护功能复现规划.md §1.5,验收 = 省电科院点表):
|
||||
* - YC:IOA 0x4011~0x4018 + 0x401d(频率),短浮点、系数 1;
|
||||
* ⚠ V0.1 有效通道仅 UA/UN/IA(TPAFE 三路,§1.6)——其余点**存在但置 IV**
|
||||
* (无效位),频率随 L1-7 落地前同样置 IV;
|
||||
* - YC:IOA 0x4011~0x4018 + 0x401d(频率),短浮点、系数 1;数据源 =
|
||||
* L2 遥测快照(App/algo/meas.h,「数据可信」卡 9/11):电气 8 路绑快照槽,
|
||||
* 有效性由快照品质驱动——V0.1 削顶路(docs/算法/继电保护功能复现规划.md
|
||||
* §1.6)快照置 CLIPPED → 本层映射为线上 IV(V0.2 后自动启用,绑定零
|
||||
* 改动,见 docs/任务规划/数据可信.md §10「双转换器无共同采样触发」行);0x401d 绑频率源,
|
||||
* freq_valid()==0 → IV;
|
||||
* - YX:IOA 1~33(§1.5 表(3));双点 = 开关位置类(YX1~4,本期无硬件 → IV),
|
||||
* 保护动作类 YX15~23 映射 prot_elm 状态,闭锁类 YX31~33 映射 prot_set;
|
||||
* - YK:IOA 0x6000+(§1.5 表(3));V0.1 无出口硬件(规划 R7)——
|
||||
@@ -21,6 +24,13 @@
|
||||
* 按未知 IOA(COT 14 + EVI)应答,登记为待办(设计文档)。
|
||||
* 线程约定:init/bind 内核启动前或建服务前;cov_check/read 仅 104 服务任务上下文。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
* 修改记录:
|
||||
* 2026-10-25 DTU 项目组 卡 9/卡 11(「数据可信」,道D):YC 数据源 = L2 遥测
|
||||
* 快照(削顶路由品质驱动 IV);死区改运行时副本 +
|
||||
* set/get 入口;COV 遥测腿 = 死区 2 点确认(seq 去重)
|
||||
* + 品质变化即报;0x401d 绑频率源(freq_valid → IV)。
|
||||
* 2026-10-25 DTU 项目组 卡 10:死区真源迁 run_par([run_par] cfg 段 +
|
||||
* 0x8020~0x8058 读写映射;本层 set/get 为转发壳)。
|
||||
*/
|
||||
#ifndef IEC104_PTS_H
|
||||
#define IEC104_PTS_H
|
||||
@@ -42,6 +52,11 @@ typedef struct {
|
||||
int (*set_read)(uint32_t addr, float *out);
|
||||
/* 定值写:select=1 预置校验(范围)/ 0 执行写入。返回 0 / -1 越界或未知 */
|
||||
int (*set_write)(uint32_t addr, float v, uint8_t select);
|
||||
/* 遥测读(带品质,卡 9 追加于表尾——旧源 NULL = 走 yc_read 退路,品质由
|
||||
* 返回值推导:-1 → IV、-2 → NT、0 → 无位,行为与旧源完全一致)。
|
||||
* qds = uint16 域位(App/algo/meas.h MEAS_QDS_*:低字节 = 线上 QDS 同值,
|
||||
* 高字节 CLIPPED 由本层映射线上 IV);返回 0 / -1 IV / -2 NT */
|
||||
int (*yc_read_ex)(uint8_t idx, float *out, uint16_t *qds);
|
||||
} iec104_pts_src_t;
|
||||
|
||||
/* ---------------- 变化检测结果(cov_check 输出) ---------------- */
|
||||
@@ -65,8 +80,17 @@ void iec104_pts_bind_src(const iec104_pts_src_t *src);
|
||||
/* ---------------- YC ---------------- */
|
||||
uint8_t iec104_pts_yc_count(void);
|
||||
uint32_t iec104_pts_yc_ioa(uint8_t idx);
|
||||
/* 读 YC:v 有效值(无效时为 0);qds 输出品质位(IV/NT 等)。返回 0/无效-1 */
|
||||
/* 读 YC:v 有效值(无效时为 0);qds 输出线上品质位(IV/NT/OV,CLIPPED 已映射
|
||||
* 为 IV)。返回 0/无效-1(NT 亦无效,品质位区分) */
|
||||
int iec104_pts_yc_read(uint8_t idx, float *v, uint8_t *qds);
|
||||
/* 死区写(卡 9 API;卡 10 起 = 转发 App/algo/run_par.c 真源 + 清本层确认计数):
|
||||
* db = 工程量绝对值 ≥0(电压 50 V / 电流 1 A / 频率 5 Hz 分档上限);越界/负值/
|
||||
* NaN → -1 且真源不动;成功时清零该点待确认计数(跨档位不残留半截确认——此壳
|
||||
* 行为有单测钉住)。仅 104 服务任务上下文调用。落盘 = run_par 脏 → 事件 →
|
||||
* defaultTask(卡 10 持久化链,docs/子系统设计/IEC104上行设计说明.md §3.4)。 */
|
||||
int iec104_pts_yc_set_deadband(uint8_t idx, float db);
|
||||
/* 读当前生效死区(越界 idx 返 0) */
|
||||
float iec104_pts_yc_deadband(uint8_t idx);
|
||||
|
||||
/* ---------------- YX ---------------- */
|
||||
uint8_t iec104_pts_yx_count(void);
|
||||
@@ -77,10 +101,19 @@ void iec104_pts_yx_read(uint8_t idx, uint8_t *out);
|
||||
|
||||
/* ---------------- COV(每 poll 一次) ---------------- */
|
||||
/*
|
||||
* 函数功能:读全表快照并与上次比对(变位 + 死区),输出变化集
|
||||
* 函数功能:读全表并与上次比对(遥信变位 + 遥测死区 2 点确认 + 品质变化),
|
||||
* 输出变化集
|
||||
* 入口参数:cov - 输出(可为 NULL = 只刷快照不取变化)
|
||||
* 函数说明:首次调用只建快照、变化集全 0(突发由初始化/总召覆盖)。
|
||||
* 死区 = 点表内置绝对值(§1.5 运行参数 8020H 段本期取内置默认)。
|
||||
* 函数说明:遥测判据(「数据可信」卡 9,对标参考系统 CalfYcMea 死区 2 点确认,
|
||||
* 短板评测口径):**连续 2 个新测量点** |v - 上次上送值| > 死区才置变化位
|
||||
* (单点尖峰不确认——噪声回摆即撤销候选);品质位(线上 qds)相对上次
|
||||
* 上送值翻转 → 当拍直接置位(不等 2 点,有效性变化必须即时可见,
|
||||
* 含 0x401d 的 IV 进出)。死区 = 运行时副本(ROM 默认 = 点表列,
|
||||
* iec104_pts_yc_set_deadband() 覆盖——卡 10 运行参数段的接线点)。
|
||||
* 测量点定义:板端默认源 = **快照换帧**(meas seq 前进;100 ms poll 对
|
||||
* 200 ms 快照,同一帧重复比对会计数两次 → seq 去重是确认语义成立的
|
||||
* 前提);注入源(单测假源)= 每次调用一测点。首次读到有效值只建基线
|
||||
* 不上报(突发由总召覆盖)。
|
||||
*/
|
||||
void iec104_pts_cov_check(iec104_pts_cov_t *cov);
|
||||
|
||||
|
||||
+41
-17
@@ -10,10 +10,10 @@
|
||||
* v1.1 2026-08-28 王建锋 改为 TCP Server,迁入独立任务 timeSyncTask,
|
||||
* 所有 socket 操作经 net_socket 线程安全 API 走消息队列,
|
||||
* netTask 仅负责 net_poll/net_process_messages,多任务并发安全
|
||||
* v1.2 2026-10-02 王建锋 round 8b 消抖:等待片长 200ms→50ms,并把各处长
|
||||
* v1.2 2026-10-02 王建锋 第 8b 轮消抖:等待片长 200ms→50ms,并把各处长
|
||||
* 等待(500/2000/200ms)统一走 time_sync_delay_fed()
|
||||
* 按 ≤50ms 分片、每片喂心跳——消除"心跳滑出两个连续
|
||||
* 100ms 抽样窗"的节拍抖动(round 7 台架 snap=0x17)
|
||||
* 100ms 抽样窗"的节拍抖动(第 7 轮台架 snap=0x17)
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
@@ -33,10 +33,10 @@
|
||||
#define TIME_SYNC_PORT 8888
|
||||
|
||||
/*
|
||||
* E-02 头阻塞修复 + L3 pending 化(timeSync 侧;lftpd 侧见 lftpd_inet.c):
|
||||
* 头阻塞修复 + L3 pending 化(timeSync 侧;lftpd 侧见 lftpd_inet.c):
|
||||
* 旧实现用单次 30s 阻塞 net_recv(默认 NET_RECV_TIMEOUT_MS)等时间包——等待期间
|
||||
* netTask(全部 socket 操作的唯一执行者)被独占,其余任务的网络操作(远端显示 /
|
||||
* FTP)整体停摆(docs/问题记录/错误码传播审计.md E-02)。
|
||||
* FTP)整体停摆(历史审计遗留问题)。
|
||||
* 2026-09-22 先落地分片有界读(5ms 片),同日 L3 pending 化后改为"挂起请求":
|
||||
* 每次等待 = 一次挂起登记(netTask 零占用,完成按主循环节拍检查),调用方
|
||||
* 单次超时 = 剩余总预算(总等待 ≤ TIME_SYNC_TOTAL_BUDGET_MS,语义与旧 30s
|
||||
@@ -47,7 +47,7 @@
|
||||
/* 看门狗分片等待:单次 net_recv_to 的等待片长上限。须显著小于看门狗抽样窗
|
||||
* (task_watchdog.c WD_POLL_PERIOD_MS=100ms)——片长 ≤50ms 时每片前喂的心跳
|
||||
* 必然落在同一个 100ms 抽样窗内,消除"心跳恰好滑过两个连续窗口"的边界抖动
|
||||
* (round 7 台架稳态 snap=0x17 即抽样窗 vs 心跳节拍的良性错位,round 8b 消抖)。
|
||||
* (第 7 轮台架稳态 snap=0x17 即抽样窗 vs 心跳节拍的良性错位,第 8b 轮消抖)。
|
||||
* 语义上仅把一次长等待拆成多次短等待,总预算不变。 */
|
||||
#define TIME_SYNC_WAIT_SLICE_MS 50U
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
* 函数说明:看门狗抽样窗为 100ms(task_watchdog.c WD_POLL_PERIOD_MS)。把任意长
|
||||
* 等待切成 ≤TIME_SYNC_WAIT_SLICE_MS 的短片、每片前喂心跳,使心跳必然
|
||||
* 落在单个抽样窗内——语义等价于一次长延时,仅把"心跳节奏"钉在窗内
|
||||
* (round 8b 消抖;不改变总延时预算)。
|
||||
* (第 8b 轮消抖;不改变总延时预算)。
|
||||
*/
|
||||
static void time_sync_delay_fed(uint32_t ms) {
|
||||
while (ms > 0U) {
|
||||
@@ -124,9 +124,11 @@ static int time_sync_open_listener(void) {
|
||||
* 入口参数:conn_sock - 已建立的连接 Socket 描述符(单连接模式下即监听 Socket 自身)
|
||||
* 返回值:无
|
||||
* 函数说明:pending 读取 PC 发来的时间包("TIME"+uint32 大端 本地时间),解析后
|
||||
* 写回 RTC,然后关闭连接。每次等待 = 一次挂起请求(netTask 零占用,
|
||||
* L3 pending 化),单次超时 = 剩余总预算(≤30s,语义与旧 30s 阻塞一致);
|
||||
* 跨调用累积,容忍时间包被 TCP 分段送达
|
||||
* 写回 RTC。**不在此 close**(单连接模式下 conn_sock 即
|
||||
* 监听 fd,close 连监听一起毁、重建窗 ~0.3-1s;net 层 auto_relisten
|
||||
* 已把同 fd 收回监听,外层直接续 accept——lftpd 样板)。每次等待 =
|
||||
* 一次挂起请求(netTask 零占用),单次超时 = 剩余总预算
|
||||
* (≤30s,语义与旧 30s 阻塞一致);跨调用累积,容忍时间包被 TCP 分段送达
|
||||
*/
|
||||
static void time_sync_handle_conn(int conn_sock) {
|
||||
uint8_t buf[16];
|
||||
@@ -177,9 +179,18 @@ static void time_sync_handle_conn(int conn_sock) {
|
||||
sys_clock_set_unix(epoch);
|
||||
DBG_INFO("time_sync: wall clock updated (epoch=%lu)", (unsigned long)epoch);
|
||||
}
|
||||
} else {
|
||||
/* 头阻塞收口(docs/网络/网络自愈架构设计.md §5.2 E13):本层放弃会话 → 主动拆除。
|
||||
* 挂起但 TCP 仍活的客户端(PC 内核自动 ACK keepalive,芯片僵尸判不死)
|
||||
* 会永久占住单连接监听;EOF 路径 drop 幂等返 0(net 层已 relisten)。
|
||||
* 模拟器多连接后端 conn≠ls,由 sim 实现按数据 fd 关闭。 */
|
||||
(void)net_drop_conn(conn_sock);
|
||||
}
|
||||
|
||||
net_close(conn_sock);
|
||||
/* 原 net_close(conn_sock) 已删——单连接模式 conn==监听 fd,
|
||||
* close 毁监听(AGENTS 模块约定:EOF 时应用层不得 close);net 层
|
||||
* auto_relisten 已把同 fd 收回 LISTENING,外层循环直接续 accept。
|
||||
* 模拟器多连接后端 conn≠ls,由调用方(外层)负责关数据 fd。 */
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -187,8 +198,10 @@ static void time_sync_handle_conn(int conn_sock) {
|
||||
* 入口参数:argument - 未使用
|
||||
* 返回值:无
|
||||
* 限定条件:net_init() 已由 netTask 完成;netTask 正在处理消息队列
|
||||
* 函数说明:等待网络栈就绪后,循环:建监听 -> 等连接 -> 处理 -> 关闭 -> 重建监听。
|
||||
* 全部 socket 操作经消息队列由 netTask 串行执行,与本任务/其它网络任务并发安全。
|
||||
* 函数说明:等待网络栈就绪后,循环:建监听 -> 等连接 -> 处理 -> **同 fd 续
|
||||
* accept**(会话收尾不 close,监听跨会话常驻,仅网络
|
||||
* 代次变化时重建)。全部 socket 操作经消息队列由 netTask 串行执行,
|
||||
* 与本任务/其它网络任务并发安全。
|
||||
*/
|
||||
void time_sync_task(void *argument) {
|
||||
(void)argument;
|
||||
@@ -213,6 +226,9 @@ void time_sync_task(void *argument) {
|
||||
}
|
||||
}
|
||||
|
||||
/* 监听 fd 跨会话常驻:仅首次/网络代次变化/被毁时重建 */
|
||||
int ls = -1;
|
||||
|
||||
for (;;) {
|
||||
/* 心跳:本任务在 required_mask 内。以下每一处可能长于 100ms 抽样窗的
|
||||
* 阻塞点(监听重试 2s、accept 轮询 200ms/次、收尾 50ms)都就近补心跳。 */
|
||||
@@ -221,11 +237,13 @@ void time_sync_task(void *argument) {
|
||||
/* 记录本次监听对应的网络代次;代次变化(网络重建)则放弃旧监听重建 */
|
||||
uint32_t gen = net_get_generation();
|
||||
|
||||
int ls = time_sync_open_listener();
|
||||
if (ls < 0) {
|
||||
DBG_ERROR("time_sync: open listener FAIL, retry later");
|
||||
time_sync_delay_fed(2000U);
|
||||
continue;
|
||||
ls = time_sync_open_listener();
|
||||
if (ls < 0) {
|
||||
DBG_ERROR("time_sync: open listener FAIL, retry later");
|
||||
time_sync_delay_fed(2000U);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
/* 等待 PC 连接:net_accept 在无连接时立即返回 -1,循环重试同一监听 Socket */
|
||||
@@ -235,6 +253,7 @@ void time_sync_task(void *argument) {
|
||||
if (conn < 0) {
|
||||
if (net_get_generation() != gen) {
|
||||
net_close(ls); /* 网络重建:旧监听失效,回外层重建 */
|
||||
ls = -1;
|
||||
break;
|
||||
}
|
||||
time_sync_delay_fed(200U);
|
||||
@@ -247,7 +266,12 @@ void time_sync_task(void *argument) {
|
||||
}
|
||||
|
||||
time_sync_handle_conn(conn);
|
||||
/* conn(单连接模式下即 ls)已被 net_close 释放,下次循环重建监听 */
|
||||
/* 会话收尾不 close(单连接模式 conn==ls,close 连监听
|
||||
* 一起毁);net 层 auto_relisten 已把同 fd 收回 LISTENING,下轮循环
|
||||
* 直接续 accept。模拟器多连接后端(conn≠ls)才关数据 fd。 */
|
||||
if (conn != ls) {
|
||||
net_close(conn);
|
||||
}
|
||||
|
||||
time_sync_delay_fed(50U);
|
||||
}
|
||||
|
||||
+202
-49
@@ -7,15 +7,15 @@
|
||||
* 状态说明:**B1 已落地(2026-10-10)**——本任务从空桩转为真实消费者:
|
||||
* 用 tpafe5160_snapshot() 原子取数(AGENTS 铁律),统计帧数/无新帧次数,
|
||||
* 并纳入看门狗 required_mask(TASK_WD_ADC)。
|
||||
* **采样节拍(v2.2,2026-10-10)**:由 **TIM3 更新中断(1 kHz)** 硬件触发——
|
||||
* **采样节拍**:由 **TIM3 更新中断(1 kHz)** 硬件触发——
|
||||
* ISR 内调 tpafe5160_start_conv_irq()(Src/main.c 的
|
||||
* HAL_TIM_PeriodElapsedCallback);本任务**不再自行触发**,退化为**纯消费者**。
|
||||
* **消费唤醒(v2.7,2026-10-10,Task #32 P1-5 方案 a)**:由 1 ms `osDelayUntil`
|
||||
* **消费唤醒**:由 1 ms `osDelayUntil`
|
||||
* 轮询改为 **TIM3 ISR 置位 `ADC_WAKE_FLAG` 的事件驱动**——唤醒与生产同源锁定,
|
||||
* 消除 0~1 ms 的 tick 量化相位抖动与"超拍永久落后 → 空转"风险;本任务优先级
|
||||
* 同步由 Normal 提到 **AboveNormal**(唤醒即抢占)。详见
|
||||
* docs/子系统设计/ADC采样节拍设计说明.md §10。
|
||||
* **L1 采集层(v2.3,2026-10-10)**:取数改走 App/algo/adc_input.c——
|
||||
* docs/子系统设计/ADC采样节拍设计说明.md §6。
|
||||
* **L1 采集层**:取数改走 App/algo/adc_input.c——
|
||||
* 本任务每拍调 adc_input_poll()(TPAFE 快照 + STM32 ADC1 9 通道扫描),
|
||||
* 经 adc_input_raw() **按语义**读通道(L1-3/L1-4),不再直接触碰驱动缓冲。
|
||||
* V0.1 上只有 TPAFE 三路(UA/UN/IA)测量有效;本任务只取数,不做保护运算。
|
||||
@@ -28,7 +28,7 @@
|
||||
* 创建日期:2026-07-19
|
||||
* 修改记录:
|
||||
* v1.0 2026-07-19 王建锋 创建初始版本
|
||||
* v1.1 2026-10-05 王建锋 P3-11:注明"预留桩(未启用)",避免误认为已实现
|
||||
* v1.1 2026-10-05 王建锋 注明"预留桩(未启用)",避免误认为已实现
|
||||
* v2.0 2026-10-10 王建锋 B1:实现真实消费者(1ms 节拍 + 原子快照 + 帧统计 +
|
||||
* 看门狗心跳),由 Src/freertos.c 创建为 adcTask
|
||||
* v2.1 2026-10-10 王建锋 日志门控:首帧/周期统计仅在 dbg_console_writer_ready()
|
||||
@@ -49,25 +49,25 @@
|
||||
* ⚠ L1-5 通道标定未实现(缺标准源)→ 暂以**原始码值**
|
||||
* 充作工程量(系数 = 1),故相量读数只证明**链路通**,
|
||||
* 不可作幅值精度判据(同 L1-3 的替代口径)。
|
||||
* v2.5 2026-10-10 王建锋 Task #32 的 P2 测量埋点:单拍加常开耗时探针
|
||||
* v2.5 2026-10-10 王建锋 常开耗时测量埋点:单拍加探针
|
||||
* (task_prof ADC_BEAT),用于判定本任务是否超拍
|
||||
* (名义节拍 1 ms = 168000 周期)而挤占低优先级任务
|
||||
* v2.6 2026-10-10 王建锋 Task #32 的 P0 修复(两处):
|
||||
* §5 无新帧仍压环 → 改为**仅 r == 1 时**压环 + 重算
|
||||
* v2.6 2026-10-10 王建锋 两处正确性修复:
|
||||
* 无新帧仍压环 → 改为**仅 r == 1 时**压环 + 重算
|
||||
* (消除重复采样污染 20 点 DFT 窗的系统性误差);
|
||||
* §4.3 osDelayUntil 返回值被丢弃 → 落后时一次性重同步
|
||||
* osDelayUntil 返回值被丢弃 → 落后时一次性重同步
|
||||
* (原写法会永久落后 → 不再阻塞 → 空转抢占 uiTask)
|
||||
* v2.7 2026-10-10 王建锋 Task #32 P1-5 方案 a(事件驱动消费):
|
||||
* v2.7 2026-10-10 王建锋 事件驱动消费改造:
|
||||
* 消费唤醒 osDelayUntil(1ms) → osThreadFlagsWait(ADC_WAKE_FLAG,
|
||||
* osWaitForever),标志由 TIM3 ISR 经 adc_tick_irq_notify() 置位;
|
||||
* 删除 ADC_TASK_BEAT_MS 与 next(P0-2 的重同步因此不再需要);
|
||||
* 删除 ADC_TASK_BEAT_MS 与 next(上一轮加的重同步钩子因此不再需要);
|
||||
* 新增"消费者就绪"门 s_consumer_ready——TIM3 在 app_main_init()
|
||||
* 就启动,**远早于** MX_FREERTOS_Init()/osKernelStart(),
|
||||
* ISR 不得在此之前对内核动手(同 Src/main.c 里 TIM7 的
|
||||
* 2026-10-02 实测坑)。优先级 Normal → AboveNormal(见 freertos.c)
|
||||
* v2.8 2026-10-10 王建锋 节拍源 TIM6 → **TIM3**(F4 上 ADC1 的外部触发没有
|
||||
* TIM6 TRGO,只有 TIM2/TIM3/TIM8 系 → 换 TIM3 才能让
|
||||
* 两片转换器由同一硬件事件驱动);STM32 侧改 DMA 双缓冲
|
||||
* v2.8 2026-10-10 王建锋 节拍源换 TIM3(F4 上 ADC1 收不到 TIM6/TIM7 的
|
||||
* TRGO,换源判据见 docs/子系统设计/ADC采样节拍设计说明.md
|
||||
* §3.2);STM32 侧改 DMA 双缓冲
|
||||
* 消费,adc_input_poll() 不再忙等;心跳日志增
|
||||
* adc1_seq / adc1_hw(adc1_err 语义改为"本拍无新帧")
|
||||
* v2.9 2026-10-11 王建锋 L1-6 采样有效性校验:每拍调 adc_health_check()
|
||||
@@ -75,28 +75,52 @@
|
||||
* 单调性/错误计数增量)——严重事件与故障电平上升沿
|
||||
* 打 DBG_ERROR + 每类每次开机一次 SYSLOG_SELFTEST;
|
||||
* 恢复打 DBG_INFO;心跳日志增 tgap(TPAFE 侧丢帧,
|
||||
* 与 adc1_gap 对称)。判定不在本文件(A-4:算法与
|
||||
* 与 adc1_gap 对称)。判定不在本文件(算法与
|
||||
* I/O 分离),本文件只做"消费计数 → 告警"的编排
|
||||
* v3.0 2026-10-21 王建锋 修复"任务初始化入口从未被调用":adc_task_func 进入
|
||||
* 循环之前先调 adc_task_init()。原实现下判定层状态靠
|
||||
* .bss 清零 + 首拍建基线兜住,功能没错,但函数头写的
|
||||
* "限定条件:adc_task_init() 已调用"是假的,会误导后来
|
||||
* 者;该入口同时不再打日志——它落在本任务静态最坏链上,
|
||||
* 每条 DBG_* 约 216 B 栈,而这一行毫无诊断价值(本任务
|
||||
* 存活由首帧日志与 TASK_WD_ADC 心跳证明)
|
||||
* v3.1 2026-10-25 DTU 项目组 L1.5 折算 + L5-2 贴轨观测(「数据可信」卡 2,道A):
|
||||
* frame[s] 由 (float)code 改 adc_cal_apply()——工程量 =
|
||||
* (code+b)×k,现口径 = ADC 引脚电压 V(eng=0,卡 1 冻结的
|
||||
* ROM 系数,docs/硬件平台/ADC通道映射说明.md §7.1);同拍
|
||||
* 经 adc_health_rail_watch() 逐通道贴轨观察(L5-2 残余,
|
||||
* 门限 adc_cal_rail_ths,码值域与标定正交),锁存边沿 →
|
||||
* DBG_ERROR + 每开机一次 SYSLOG;心跳增 cal: 行(mV 定点
|
||||
* + valid/clipped 位图 + rail 锁存),台架注入口径见该行判读
|
||||
* v3.2 2026-10-25 DTU 项目组 L2 遥测快照发布(「数据可信」卡 8,道D,M5):新模块
|
||||
* App/algo/meas.c——本任务为唯一发布者,新帧拍内
|
||||
* freq_run() 之后按 200 ms **tick 经过**分频调
|
||||
* meas_tick()(全仓调用点 = 1,评审判据);品质注入源 =
|
||||
* adc_cal_flags() 削顶位图,频率 = 同拍 freq 读数;心跳新增
|
||||
* meas: 行(seq/v/ua·un·ia_mv/f/fv),读数比对类判据待台架实测
|
||||
*/
|
||||
|
||||
#include "adc_task.h"
|
||||
#include "cmsis_os.h"
|
||||
#include "task_handles.h" /* adcTaskHandle(原为本文件局部 extern) */
|
||||
#include "adc_input.h" /* L1 采集层:adc_input_poll()/adc_input_raw()(按语义取数) */
|
||||
#include "sample_ring.h" /* L2 输入:sample_ring_push_frame()(采样环缓冲) */
|
||||
#include "phasor.h" /* L2 相量层:phasor_run()/phasor_get()(全周傅氏) */
|
||||
#include "freq.h" /* L2 派生量:freq_run()/freq_get()(测频,B8) */
|
||||
#include "adc_health.h" /* L1-6:采样有效性校验(静默/丢帧/单调性/错误增量) */
|
||||
#include "adc_health.h" /* L1-6:采样有效性校验(静默/丢帧/单调性/错误增量/贴轨) */
|
||||
#include "adc_cal.h" /* L1.5:通道标定折算(「数据可信」卡 2;flags 供 cal 心跳行) */
|
||||
#include "meas.h" /* L2 发布侧:遥测快照 meas_tick()(「数据可信」卡 8,道D) */
|
||||
#include "tpafe5160.h"
|
||||
#include "task_watchdog.h"
|
||||
#include "console_log.h"
|
||||
#include "sys_log.h" /* SYSLOG_SELFTEST:采样故障留痕(仅故障才写) */
|
||||
#include "task_prof.h" /* P2 常开探针:单拍耗时(Task #32) */
|
||||
#include "task_prof.h" /* 常开耗时探针:单拍耗时 */
|
||||
|
||||
#define DBG_TAG "[ADC_TASK]"
|
||||
#include "dbg_log.h"
|
||||
|
||||
/* 心跳日志周期(ms):周期性上报帧统计,作为采集链路存活的观测面。
|
||||
* (原 ADC_TASK_BEAT_MS = 1U 的消费轮询周期已随 v2.7 事件驱动改造删除——
|
||||
* (原 ADC_TASK_BEAT_MS = 1U 的消费轮询周期已随事件驱动改造删除——
|
||||
* 消费节拍改由 TIM3 ISR 置位 ADC_WAKE_FLAG 提供,与生产同源,不再自定周期) */
|
||||
#define ADC_TASK_REPORT_MS 10000U
|
||||
|
||||
@@ -114,8 +138,17 @@ static uint32_t s_frames_skip;
|
||||
static uint32_t s_health_prev;
|
||||
static uint32_t s_health_logged;
|
||||
|
||||
/* adcTask 的内核句柄,定义在 Src/freertos.c(引用方式同 App/task/default_task.c) */
|
||||
extern osThreadId_t adcTaskHandle;
|
||||
/* L5-2 贴轨编排状态(锁存本体在 adc_health 内,这里只留编排层视角):
|
||||
* s_rail_edge_latched - 本开机以来出现过锁存边沿的通道位图(心跳 cal: 行展示)
|
||||
* s_rail_syslogged - SYSLOG 留痕只写一次的闸门(init 重置) */
|
||||
static uint32_t s_rail_edge_latched;
|
||||
static uint8_t s_rail_syslogged;
|
||||
|
||||
/* 遥测快照发布闸门(「数据可信」卡 8):上次发布拍时基。分频用 **tick 经过**
|
||||
* 对比 MEAS_PERIOD_MS,非拍计数——idle/skip 拍会把拍计数口径拉得超过名义
|
||||
* 200 ms(与 v2.6 修"重复采样污染 DFT 窗"同族误差,勿"优化"成拍计数) */
|
||||
static uint32_t s_meas_ms;
|
||||
|
||||
|
||||
/* 消费者就绪门:由 adcTask 自己在进入等待前置 1;单写者 = 本任务,ISR 只读。
|
||||
* ⚠ 为什么必须有这道门:TIM3 在 app_main_init()(Src/main.c:140)里就
|
||||
@@ -135,8 +168,8 @@ static volatile uint8_t s_consumer_ready;
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅 ISR 上下文。TIM3 抢占优先级 = 6 ≥ configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY
|
||||
* (=5) → osThreadFlagsSet 走 xTaskNotifyFromISR + portYIELD_FROM_ISR,合法且立即切换
|
||||
* 函数说明:Task #32 P1-5 方案 a——ISR **只发唤醒**,全部计算留在任务里(拒绝"ISR 内计算":
|
||||
* adc_input_poll() 内含 2 ms 忙等,绝不可入 ISR)。线程标志粘滞 → 单拍偶超 1 ms 时
|
||||
* 函数说明:ISR **只发唤醒**,全部计算留在任务里——"ISR 内计算"是反模式:取数与相量
|
||||
* 计算都读写任务级共享数据,且本回调属 CubeMX 生成区,重生成即丢。线程标志粘滞 → 单拍偶超 1 ms 时
|
||||
* 多个脉冲合并成一次唤醒,下一轮 adc_input_poll() 返回 r==0 记 idle,不会重复处理同一帧
|
||||
*/
|
||||
void adc_tick_irq_notify(void) {
|
||||
@@ -153,7 +186,9 @@ void adc_tick_irq_notify(void) {
|
||||
* 限定条件:仅任务上下文;调用方须保证 dbg_console_writer_ready() 为真
|
||||
* 函数说明:MicroLIB 的 snprintf **不支持 %f** → 以 ×0.1 定点整数打印
|
||||
* (打印值 = 实际值 × 10,分辨率 0.1)。逐实例数据驱动输出,
|
||||
* 实例表加行即自动覆盖。⚠ L1-5 标定未实现 → 读数单位是**原始码值**。
|
||||
* 实例表加行即自动覆盖。输入自 v3.1 起为 L1.5 折算后的工程量
|
||||
* (现口径 = ADC 引脚电压 V,见 cal: 心跳行与文件头 v3.1 记录);
|
||||
* 二次值语义仅在 adc_cal_flags() 出 VALID 后成立。
|
||||
*/
|
||||
static void log_phasors(const char *tag) {
|
||||
uint8_t k;
|
||||
@@ -230,14 +265,21 @@ static void adc_health_report(const adc_health_status_t *hs) {
|
||||
* 入口参数:无
|
||||
* 返 回 值:0 - 成功,-1 - 失败
|
||||
* 限定条件:tpafe5160_init() 已在 app_main_init()(内核启动前)完成
|
||||
* 函数说明:驱动初始化不在本任务(见限定条件);此处仅清帧统计。
|
||||
* 函数说明:驱动初始化不在本任务(见限定条件);此处清帧统计 + 清判定层参照系
|
||||
* (adc_health_init())。由 adc_task_func 进入循环之前调用一次;幂等,
|
||||
* 可重复调用(运行期要重建判定参照系,走的也是这里)。
|
||||
* ⚠ 本函数内禁止加日志:它在 adcTask 的静态最坏调用链上,每条 DBG_*
|
||||
* 约 216 B 栈,而本任务栈余量按 1024 B ISR 底线核算。
|
||||
*/
|
||||
int adc_task_init(void) {
|
||||
s_frames_consumed = 0u;
|
||||
s_frames_idle = 0u;
|
||||
s_frames_skip = 0u;
|
||||
adc_health_init();
|
||||
DBG_INFO("adc_task_init");
|
||||
s_rail_edge_latched = 0u;
|
||||
s_rail_syslogged = 0u;
|
||||
s_meas_ms = 0u;
|
||||
adc_health_init(); /* 内含 L5-2 贴轨观察器复位(rail_init) */
|
||||
(void)meas_init(); /* 遥测快照双区/序号复位(幂等;发布状态属本任务,init 无日志) */
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -245,10 +287,11 @@ int adc_task_init(void) {
|
||||
* 函数功能:ADC 任务主函数
|
||||
* 入口参数:arg - FreeRTOS 传入参数(未使用)
|
||||
* 返 回 值:无
|
||||
* 限定条件:adc_task_init() 已调用;内核已启动
|
||||
* 限定条件:内核已启动。本函数自己在进入循环前调 adc_task_init()——不依赖外部
|
||||
* 调用(历史上该入口全库无调用点,判定层状态只靠 .bss 清零兜住)
|
||||
* 函数说明:每拍采集一次(adc_input_poll():TPAFE 原子快照 + 消费一帧 STM32 ADC1 DMA 乒乓缓冲)
|
||||
* 并统计。采样节拍由 TIM3 硬件定时器提供(ISR 内触发 CONVST),本任务不再触发;
|
||||
* **本任务的唤醒同样由该 TIM3 ISR 置位 ADC_WAKE_FLAG 驱动**(v2.7 事件驱动,
|
||||
* **本任务的唤醒同样由该 TIM3 ISR 置位 ADC_WAKE_FLAG 驱动**(事件驱动,
|
||||
* 不再自定 1 ms 周期)。稳态下 consumed 每拍 +1、idle 不再增长;idle 持续增长 =
|
||||
* EXTI 未在读帧或 TIM3 未启动(异常),由周期心跳日志暴露。
|
||||
* ⚠ 若 TIM3 停摆,本任务会**永久阻塞**在 osThreadFlagsWait → TASK_WD_ADC 缺席 →
|
||||
@@ -261,6 +304,10 @@ void adc_task_func(void *arg) {
|
||||
|
||||
(void)arg;
|
||||
|
||||
/* 任务自有状态清零(含判定层参照系)。放在开闸之前:此刻 ISR 还不会唤醒
|
||||
* 本任务,init 与首拍之间不存在"用半初始化状态做判定"的窗口 */
|
||||
(void)adc_task_init();
|
||||
|
||||
/* 开闸:此刻任务已存在、调度器必已运行 → ISR 的唤醒从此生效。
|
||||
* 放在进入循环之前:置位与等待之间不会漏唤醒(线程标志粘滞) */
|
||||
s_consumer_ready = 1u;
|
||||
@@ -269,8 +316,8 @@ void adc_task_func(void *arg) {
|
||||
int r;
|
||||
uint32_t now = osKernelGetTickCount(); /* 本拍时基:健康判定 + 心跳共用一份 */
|
||||
|
||||
/* P2 探针:单拍起点。名义节拍 1 ms = 168000 周期;若实测**持续**超过,
|
||||
* 本任务(v2.7 起为 osPriorityAboveNormal)会持续抢占 uiTask
|
||||
/* 探针:单拍起点。名义节拍 1 ms = 168000 周期;若实测**持续**超过,
|
||||
* 本任务(osPriorityAboveNormal)会持续抢占 uiTask
|
||||
* (osPriorityBelowNormal),是"UI 心跳缺席"的候选占用者之一 */
|
||||
task_prof_enter(TASK_PROF_ADC_BEAT);
|
||||
|
||||
@@ -278,9 +325,8 @@ void adc_task_func(void *arg) {
|
||||
* DMA 双缓冲(仅任务上下文;TPAFE 侧 1=有新帧 0=无新帧 -1=参数错)。
|
||||
* 采样节拍由 TIM3 更新事件提供(ISR 内触发 CONVST + TRGO 触发 ADC1),
|
||||
* 本任务既不触发也不等待转换。
|
||||
* P2 探针:本段单独计时。v2.0(DMA)起本段**零忙等**——原轮询版常态
|
||||
* adcp=122 us、超时拍最坏 2~3 ms(HAL_ADC_PollForConversion 是不让出的
|
||||
* 忙等),现在两档都降到 us 量级(只剩 TPAFE 快照临界区 + 拷 9 个半字)。
|
||||
* 探针:本段单独计时。本段**零忙等**(转换由 TIM3 TRGO 触发、DMA 搬运,
|
||||
* 不再有逐通道轮询超时),只剩 TPAFE 快照临界区 + 拷 9 个半字 → us 量级。
|
||||
* ⚠ adcm(整拍)的 ~2.1 ms 峰值来自每 10 s 一次的心跳日志,不是本段,勿混读 */
|
||||
task_prof_enter(TASK_PROF_ADC_POLL);
|
||||
r = adc_input_poll();
|
||||
@@ -296,31 +342,45 @@ void adc_task_func(void *arg) {
|
||||
/* 2) L2 输入 + 相量重算:把本拍 9 路语义值转浮点帧压入采样环,再重算全部
|
||||
* 相量实例(L2-6 滑动窗:每拍以"当前时刻为终止的前一周波"整窗重算)。
|
||||
*
|
||||
* ⚠ P0 修复(2026-10-10,Task #32 §5):**仅在本拍确有新帧(r == 1)时**才压环。
|
||||
* ⚠ **仅在本拍确有新帧(r == 1)时**才压环。
|
||||
* 原实现在 r == 0(TPAFE 无新帧)时**仍然**压环 + phasor_run(),只有 r == -1
|
||||
* 才跳过 —— 于是把**上一拍的重复采样值**塞进 20 点 DFT 窗,等效采样间隔被拉长。
|
||||
* 证据是日志里的恒等式 `ring = frames + idle`(09:36:21: 9957+44=10001 ✓;
|
||||
* 09:36:31: 19352+649=20001 ✓)。本例 idle 占 3.2% → 窗长从 20.0 ms 漂到约
|
||||
* 20.6 ms → 对 50 Hz 信号相当于约 3% 的频率失配 → 幅值/相位**系统性**误差
|
||||
* (不是随机噪声)。修好后 `ring` 只在真实新帧上增长(ring == frames)。
|
||||
* 量级参考(台架曾测得 idle 占 3.2%):窗长从 20.0 ms 漂到约 20.6 ms → 对 50 Hz
|
||||
* 相当于约 3% 频率失配 → 幅值/相位**系统性**误差(不是随机噪声)。
|
||||
* 回归判据 = 恒等式 `ring == frames`(只有真新帧才入环)。
|
||||
*
|
||||
* 任一通道读不到(尚未首次成功)→ 本拍**整帧跳过**:不向窗内注入 0 点
|
||||
* (单个 0 点会显著污染 20 点窗),并计入 skip 观测。 */
|
||||
if (r == 1) {
|
||||
float frame[ADC_SEM_COUNT];
|
||||
uint8_t s;
|
||||
uint8_t all_ok = 1u;
|
||||
float frame[ADC_SEM_COUNT];
|
||||
uint8_t s;
|
||||
uint8_t all_ok = 1u;
|
||||
uint32_t rail_edge = 0u;
|
||||
|
||||
for (s = 0u; s < (uint8_t)ADC_SEM_COUNT; s++) {
|
||||
int32_t code = 0;
|
||||
int32_t code = 0;
|
||||
int32_t lo_th = 0;
|
||||
int32_t hi_th = 0;
|
||||
|
||||
if (adc_input_raw((adc_semantic_t)s, &code) != 0) {
|
||||
all_ok = 0u;
|
||||
break;
|
||||
}
|
||||
/* ⚠ L1-5 通道标定尚未实现(缺标准源)→ 暂以**原始码值**充作工程量
|
||||
* (系数 = 1)。标定落地后此处改为 工程量 = (code + b) × k(定点 2^10)。 */
|
||||
frame[s] = (float)code;
|
||||
/* L1.5 折算(「数据可信」卡 2,§4.1 模型):工程量 = (code + b) × k。
|
||||
* 现口径 = **ADC 引脚电压 V**(eng=0,卡 1 冻结的 ROM 系数,见
|
||||
* docs/硬件平台/ADC通道映射说明.md §7.1);卡 0b/卡 12 写入标定
|
||||
* 系数(adc_cal_flags() 出 VALID)后才按二次值消费。TEMP 透传码值
|
||||
* 不折算(决策 5)。*/
|
||||
frame[s] = adc_cal_apply((adc_semantic_t)s, code);
|
||||
|
||||
/* L5-2 码值越界(贴轨)观察:在**码值域**判定,与标定正交——
|
||||
* 连续一周波贴轨带且此前见过释放沿才锁存(空载恒 0 不误报)。
|
||||
* TEMP 无「轨」语义,adc_cal_rail_ths() 返 -1 自然跳过。*/
|
||||
if (adc_cal_rail_ths((adc_semantic_t)s, &lo_th, &hi_th) == 0) {
|
||||
if (adc_health_rail_watch(s, code, lo_th, hi_th) != 0u) {
|
||||
rail_edge |= (1uL << s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (all_ok != 0u) {
|
||||
@@ -329,9 +389,43 @@ void adc_task_func(void *arg) {
|
||||
/* L2 派生量测频(B8):同拍紧随 phasor_run——本拍使用区已
|
||||
* 切换,freq 读的就是本拍相量(1 ms 同上下文,无竞态) */
|
||||
freq_run();
|
||||
|
||||
/* L2 遥测快照发布(「数据可信」卡 8,道D,M5):200 ms tick 经过分频。
|
||||
* 唯一发布者 = 本任务(全仓 meas_tick 调用点数 = 1 为评审判据,
|
||||
* 「数据可信」§8 风险表"快照双写"行);只在**压环成功的真新帧拍**发布
|
||||
* ——idle/skip 拍压环未推进,发布 = 重复旧窗。品质注入唯一源 =
|
||||
* adc_cal_flags()(§4.2 三态表;纯读,每 200 ms 才跑一遍)。 */
|
||||
if ((uint32_t)(now - s_meas_ms) >= MEAS_PERIOD_MS) {
|
||||
uint32_t cm = 0u;
|
||||
uint8_t m;
|
||||
|
||||
s_meas_ms = now;
|
||||
for (m = 0u; m < (uint8_t)ADC_SEM_COUNT; m++) {
|
||||
if ((adc_cal_flags((adc_semantic_t)m) & ADC_CAL_F_CLIPPED) != 0u) {
|
||||
cm |= (1uL << m);
|
||||
}
|
||||
}
|
||||
meas_tick(now, freq_valid(), freq_last(), cm);
|
||||
}
|
||||
} else {
|
||||
s_frames_skip++;
|
||||
}
|
||||
|
||||
/* 贴轨锁存边沿留痕:帧循环后统一处理(skip 拍也已在循环内逐通道
|
||||
* 观察过,不因取数半途失败而漏报已观察的通道)。SYSLOG 每开机只
|
||||
* 留痕一次——贴轨是持续工况,逐拍写会把 selftest.log 刷满 */
|
||||
if (rail_edge != 0u) {
|
||||
s_rail_edge_latched |= rail_edge;
|
||||
if (dbg_console_writer_ready() != 0u) {
|
||||
DBG_ERROR("rail: edge=0x%lx sticky=0x%lx (code-domain, 20-run+release)",
|
||||
(unsigned long)rail_edge,
|
||||
(unsigned long)adc_health_rail_sticky());
|
||||
}
|
||||
if (s_rail_syslogged == 0u) {
|
||||
s_rail_syslogged = 1u;
|
||||
(void)syslog_post(SYSLOG_SELFTEST, "adc code rail fault");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 2b) L1-6 采样有效性校验(每拍,常数次整数运算):把驱动侧观测计数交给
|
||||
@@ -398,7 +492,7 @@ void adc_task_func(void *arg) {
|
||||
{
|
||||
if ((int32_t)(now - report_at) >= 0) {
|
||||
if (dbg_console_writer_ready() != 0u) {
|
||||
/* STM32 侧观测量(v2.0 DMA 版),四类各有独立成因:
|
||||
/* STM32 侧观测量(DMA 版),四类各有独立成因:
|
||||
* adc1_rc = 最近一拍取帧状态(0 OK / -2 本拍无新帧)
|
||||
* adc1_seq = DMA 整帧完成计数(正常时 ≈ seq,恒定小偏移)
|
||||
* adc1_gap = 丢帧:seq 一次前进 >1 → 本任务超拍(单拍 > 1 ms)
|
||||
@@ -424,21 +518,80 @@ void adc_task_func(void *arg) {
|
||||
(unsigned long)adc_health_tpafe_gaps());
|
||||
|
||||
log_phasors("hb");
|
||||
|
||||
/* L1.5/L5-2 台架观测面(×1000 mV 定点,MicroLIB 无 %f):
|
||||
* ua/un/ia_mv = 三路 TPAFE 引脚电压毫伏(未标定域,**不是**
|
||||
* 二次值;卡 12 注入验证的读数落点,偏差判据 ≤2 %)
|
||||
* vl = VALID 位图(默认构建应恒 0x00——未标定即未标定,
|
||||
* 卡 5 的闭锁判据在 protect 侧同源消费这个位)
|
||||
* cl = CLIPPED 位图(V0.1 应恒 0xE6 = UB/UC/IB/IC/IN)
|
||||
* rail/edge = 贴轨锁存/历史边沿位图(注入饱和电平后应起)*/
|
||||
{
|
||||
int32_t c = 0;
|
||||
long ua_mv = 0L, un_mv = 0L, ia_mv = 0L;
|
||||
uint32_t vl = 0u, cl = 0u;
|
||||
uint8_t i;
|
||||
|
||||
if (adc_input_raw(ADC_SEM_UA, &c) == 0) {
|
||||
ua_mv = (long)(adc_cal_apply(ADC_SEM_UA, c) * 1000.0f);
|
||||
}
|
||||
if (adc_input_raw(ADC_SEM_UN, &c) == 0) {
|
||||
un_mv = (long)(adc_cal_apply(ADC_SEM_UN, c) * 1000.0f);
|
||||
}
|
||||
if (adc_input_raw(ADC_SEM_IA, &c) == 0) {
|
||||
ia_mv = (long)(adc_cal_apply(ADC_SEM_IA, c) * 1000.0f);
|
||||
}
|
||||
for (i = 0u; i < 8u; i++) {
|
||||
const uint32_t f = adc_cal_flags((adc_semantic_t)i);
|
||||
|
||||
if ((f & ADC_CAL_F_VALID) != 0u) {
|
||||
vl |= (1uL << i);
|
||||
}
|
||||
if ((f & ADC_CAL_F_CLIPPED) != 0u) {
|
||||
cl |= (1uL << i);
|
||||
}
|
||||
}
|
||||
DBG_INFO("cal: ua_mv=%ld un_mv=%ld ia_mv=%ld vl=0x%02lx cl=0x%02lx rail=0x%03lx edge=0x%03lx",
|
||||
ua_mv, un_mv, ia_mv,
|
||||
(unsigned long)vl, (unsigned long)cl,
|
||||
(unsigned long)adc_health_rail_sticky(),
|
||||
(unsigned long)s_rail_edge_latched);
|
||||
}
|
||||
|
||||
/* M5 遥测快照台架观测面(定点打印,MicroLIB 无 %f):
|
||||
* seq = 发布帧数(10 s 心跳间隔内应 +500±,不涨 = 发布闸门停)
|
||||
* v = 整帧有效;ua/un/ia = 基波有效值毫伏(未标定域 = 引脚电压;
|
||||
* 本行是 200 ms 快照轨,cal: 行是当拍瞬时码折算——同量级不同
|
||||
* 相位,勿互为判据)
|
||||
* f/fv = 频率读数与新鲜度(x0.01)
|
||||
* 读数与注入源比对类判据 = **待台架实测**(实测采样类跳过口径) */
|
||||
{
|
||||
meas_snap_t ms;
|
||||
|
||||
(void)meas_get(&ms); /* 未就绪(-2) = 清零帧,以下打印全 0 */
|
||||
DBG_INFO("meas: seq=%lu v=%u ua_mv=%ld un_mv=%ld ia_mv=%ld f=%ld fv=%u",
|
||||
(unsigned long)ms.seq, (unsigned)ms.valid,
|
||||
(long)(ms.yc[ADC_SEM_UA] * 1000.0f),
|
||||
(long)(ms.yc[ADC_SEM_UN] * 1000.0f),
|
||||
(long)(ms.yc[ADC_SEM_IA] * 1000.0f),
|
||||
(long)(ms.freq_hz * 100.0f),
|
||||
(unsigned)ms.freq_valid);
|
||||
}
|
||||
}
|
||||
report_at = now + ADC_TASK_REPORT_MS;
|
||||
}
|
||||
}
|
||||
|
||||
/* 7) 等下一次节拍唤醒(v2.7 事件驱动,Task #32 P1-5 方案 a)
|
||||
/* 7) 等下一次节拍唤醒(事件驱动)
|
||||
* 原实现是 osDelayUntil(next += 1ms):把消费唤醒**钉在 FreeRTOS tick(1 ms)边界**
|
||||
* → 与 TIM3 生产之间有 0~1 ms 随机相位抖动(日志里的 idle 正是这么来的),
|
||||
* 且一旦超拍,next 就永久落后 → 任务不再阻塞 → 空转抢占低优先级 uiTask
|
||||
* (P0-2 修的正是它)。现在唤醒与生产**同源**:TIM3 ISR 触发 CONVST 后置位
|
||||
* (空转抢占低优先级任务正是这么发生的)。现在唤醒与生产**同源**:TIM3 ISR 触发 CONVST 后置位
|
||||
* ADC_WAKE_FLAG,本任务被唤醒的延迟 = ISR + 调度(µs 级;本任务已提至
|
||||
* AboveNormal → 立即抢占),无 tick 量化、无累积漂移,也就不再需要重同步钩子。
|
||||
* 标志是命令位(等待即消费)→ osFlagsWaitAny,不加 osFlagsNoClear;
|
||||
* osWaitForever 无超时 = 刻意的 fail-safe(TIM3 停摆 → 无心跳 → IWDG 复位)。 */
|
||||
task_prof_exit(TASK_PROF_ADC_BEAT); /* P2 探针:单拍耗时结算(不含等待) */
|
||||
task_prof_exit(TASK_PROF_ADC_BEAT); /* 探针:单拍耗时结算(不含等待) */
|
||||
(void)osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -6,7 +6,7 @@
|
||||
* 创建日期:2026-07-19
|
||||
* 修改记录:
|
||||
* v1.0 2026-07-19 王建锋 创建初始版本
|
||||
* v1.1 2026-10-10 王建锋 Task #32 P1-5 方案 a:新增 ADC_WAKE_FLAG 与
|
||||
* v1.1 2026-10-10 王建锋 新增 ADC_WAKE_FLAG 与
|
||||
* adc_tick_irq_notify()——消费节拍由 osDelayUntil(1ms)
|
||||
* 轮询改为 TIM3 ISR 事件驱动唤醒
|
||||
*/
|
||||
@@ -21,7 +21,7 @@ extern "C" {
|
||||
int adc_task_init(void);
|
||||
void adc_task_func(void *arg);
|
||||
|
||||
/* Task #32 P1-5(方案 a,2026-10-10):事件驱动消费。
|
||||
/* 事件驱动消费的唤醒标志。
|
||||
* TIM3 1 kHz 更新中断触发 CONVST 后,用本标志唤醒 adcTask 消费一拍。
|
||||
* 线程标志在此是**命令位**(等待即消费),不是状态位 → 用 osFlagsWaitAny,
|
||||
* 不加 osFlagsNoClear(AGENTS「就绪屏障位必须 osFlagsNoClear」只约束状态位) */
|
||||
|
||||
+42
-40
@@ -2,7 +2,7 @@
|
||||
* 模块名称:Default Application Task
|
||||
* 模块功能:主控任务(defaultTask)任务体实现:
|
||||
* ① 启动段:存储 bring-up(挂载 + 空卷自动建卷 + 卷健康探针两击 +
|
||||
* 自愈重格式化,E-31 失败后主循环 3×2s 有界重试)+ 网络配置加载
|
||||
* 自愈重格式化,挂载失败后主循环 3×2s 有界重试)+ 网络配置加载
|
||||
* + STORAGE_READY 屏障 + "保存网络配置"回调注册
|
||||
* ② 控制台单写者:dbg_console_writer_attach 注册 + 主循环 dbg_console_drain
|
||||
* 排空(串口唯一发送者,见 App/log/console_log.c 设计说明)
|
||||
@@ -19,10 +19,13 @@
|
||||
* FTP 文件操作与 syslog/配置的卷访问自此串行化
|
||||
* (2026-10-02 台架:并发 FTL 写 → journal 竞争 →
|
||||
* 本任务主循环喂狗点饥饿 → IWDG 误复位)
|
||||
* v1.2 2026-10-10 王建锋 Task #32 的 P2 测量接线:主循环周期上报各任务最坏
|
||||
* v1.2 2026-10-10 王建锋 任务耗时测量接线:主循环周期上报各任务最坏
|
||||
* 耗时(task_prof_report),并在**心跳位图出现 1→0 边沿**
|
||||
* 的瞬间留一份现场快照(task_prof_report_raw,裸 UART);
|
||||
* 另给控制台排空与 syslog 落盘各加一个耗时探针
|
||||
* v1.3 2026-10-25 DTU 项目组 写回缓存空闲 flush 接线:主循环 syslog 排空
|
||||
* 之后调 vol_idle_sync_poll()(卷空闲周期 CTRL_SYNC 兜底,
|
||||
* 模块 = App/fs/vol_idle_sync.[ch],道F 批 2)
|
||||
*/
|
||||
|
||||
#include "default_task.h"
|
||||
@@ -35,7 +38,9 @@
|
||||
#include "nand_ftl.h"
|
||||
#include "net_cfg.h"
|
||||
#include "cfg_file.h"
|
||||
#include "run_par.h" /* 运行参数段落盘(卡 10,配置引擎属主 = 本任务) */
|
||||
#include "prot_set_store.h"
|
||||
#include "prot_set.h" /* task-10:prot_set_dirty_peek(区分"无待写"与"真落盘") */
|
||||
#include "sys_log.h"
|
||||
#include "sys_events.h"
|
||||
/* 深调用链模块(本任务 = 存储 bring-up + 配置引擎属主 + syslog 唯一写者,
|
||||
@@ -44,29 +49,10 @@
|
||||
#define DBG_LINE_BUF_MAX 128U
|
||||
#include "dbg_log.h"
|
||||
#include "task_watchdog.h"
|
||||
#include "task_prof.h" /* P2 测量:各任务最坏耗时上报(Task #32) */
|
||||
#include "task_prof.h" /* 各任务最坏耗时上报 */
|
||||
#include "vol_lock.h"
|
||||
|
||||
/* 任务句柄与属性块(CubeMX 生成全局,定义于 Src/freertos.c):
|
||||
* 栈水位观测需各任务句柄 + 属性块 stack_size(配置栈大小的唯一定义处) */
|
||||
extern osThreadId_t defaultTaskHandle;
|
||||
extern const osThreadAttr_t defaultTask_attributes;
|
||||
extern osThreadId_t netTaskHandle;
|
||||
extern const osThreadAttr_t netTask_attributes;
|
||||
extern osThreadId_t ftpTaskHandle;
|
||||
extern const osThreadAttr_t ftpTask_attributes;
|
||||
extern osThreadId_t testTaskHandle;
|
||||
extern const osThreadAttr_t testTask_attributes;
|
||||
extern osThreadId_t uiTaskHandle;
|
||||
extern const osThreadAttr_t uiTask_attributes;
|
||||
extern osThreadId_t timeSyncTaskHandle;
|
||||
extern const osThreadAttr_t timeSyncTask_attributes;
|
||||
extern osThreadId_t adcTaskHandle;
|
||||
extern const osThreadAttr_t adcTask_attributes;
|
||||
extern osThreadId_t iec104TaskHandle;
|
||||
extern const osThreadAttr_t iec104Task_attributes;
|
||||
extern osThreadId_t protectTaskHandle;
|
||||
extern const osThreadAttr_t protectTask_attributes;
|
||||
#include "vol_idle_sync.h" /* 写回缓存空闲 flush 兜底(App/fs/vol_idle_sync.h) */
|
||||
#include "task_handles.h" /* 任务句柄与属性块的引用契约(定义处仍是 Src/freertos.c) */
|
||||
|
||||
/* Private defines -----------------------------------------------------------*/
|
||||
/* 栈水位观测节奏:首次报告须晚于 bring-up 深链(mount/mkfs/探针/cfg/syslog),
|
||||
@@ -80,7 +66,7 @@ extern const osThreadAttr_t protectTask_attributes;
|
||||
* 2026-10-12(B5):堆扩至 56KB(.ioc configTOTAL_HEAP_SIZE=57344,CCM 腾给
|
||||
* protectTask/iec104Task 栈),阈值**不随堆缩放**——它是绝对字节安全线。 */
|
||||
#define HEAP_LOW_WATER_B 3072u
|
||||
/* E-31 自愈策略(2026-09-21 审计 P0 修复):启动期存储挂载失败后,
|
||||
/* 存储自愈策略(2026-09-21 审计 P0 修复):启动期存储挂载失败后,
|
||||
* 主循环按 2s 周期重跑完整 bring-up 的次数上限;耗尽则维持无存储降级
|
||||
* (自检已上报、卷保留、不重格式化),等下次上电重新 resume */
|
||||
#define STORAGE_BRINGUP_RETRIES 3u
|
||||
@@ -121,7 +107,7 @@ static FRESULT storage_bring_up(FATFS *fs);
|
||||
* 只在内核运行期被调度,启动段允许使用 CMSIS 对象
|
||||
* 函数说明:启动段 = 存储 bring-up + 保存回调注册 + 控制台写者注册;
|
||||
* 主循环 = 事件等待(配置保存/控制台待发,50ms 兜底)→ 控制台排空
|
||||
* → 配置保存执行 → 2s 配置轮询(含 E-31 有界重试)→ LED 心跳
|
||||
* → 配置保存执行 → 2s 配置轮询(含存储自愈有界重试)→ LED 心跳
|
||||
* → syslog 落盘 → 栈水位观测
|
||||
*/
|
||||
void default_task_func(void *argument) {
|
||||
@@ -136,7 +122,7 @@ void default_task_func(void *argument) {
|
||||
* 完整 bring-up 序列(挂载 + 空卷自动建卷 + 健康探针 + 自愈重格式化
|
||||
* + 日志挂载 + 配置加载 + 就绪屏障)实现于 storage_bring_up()。
|
||||
*
|
||||
* E-31(2026-09-21 审计,P0 修复):dhara_map_resume 失败不再被当
|
||||
* 存储自愈(2026-09-21 审计,P0 修复):dhara_map_resume 失败不再被当
|
||||
* "新卷"静默重格式化(扫描中任何一次瞬态毛刺都会销毁全部用户数据)——
|
||||
* nand_ftl 仅把 DHARA_E_TOO_BAD(真空白)判为新卷,其余失败使
|
||||
* disk_initialize 失败 → f_mount 失败 → 下方自检上报并保留卷;
|
||||
@@ -146,7 +132,7 @@ void default_task_func(void *argument) {
|
||||
* 小写两击确认、FR_INVALID_NAME 不入重格式化集、写保护类只上报,
|
||||
* 详见该函数注释)。 */
|
||||
#if !defined(TEST_SUITE_STORAGE)
|
||||
/* E-31 自愈:存储 bring-up 剩余重试次数(0 = 已就绪或已耗尽);
|
||||
/* 存储自愈:存储 bring-up 剩余重试次数(0 = 已就绪或已耗尽);
|
||||
* 须在启动块之前声明(启动块失败路径写入、主循环读取) */
|
||||
uint32_t storage_retry = 0u;
|
||||
#endif
|
||||
@@ -161,12 +147,12 @@ void default_task_func(void *argument) {
|
||||
if (fr != FR_OK) {
|
||||
/* 存储不可写:自检故障仅能滞留待发队列(串口已告警) */
|
||||
syslog_post(SYSLOG_SELFTEST, "filesystem fault");
|
||||
/* E-31 自愈:启动主循环中的有界重试(每 2s 一次) */
|
||||
/* 存储自愈:启动主循环中的有界重试(每 2s 一次) */
|
||||
storage_retry = STORAGE_BRINGUP_RETRIES;
|
||||
}
|
||||
|
||||
/* 注册"保存网络配置"请求通知:UI(uiTask)投递 → 置事件 → 本任务立即执行写盘
|
||||
* (配置引擎属主 = 本任务;UI 保存不再直接触碰配置文件引擎,P1-3) */
|
||||
* (配置引擎属主 = 本任务;UI 保存不再直接触碰配置文件引擎) */
|
||||
net_cfg_set_save_req_cb(default_task_net_cfg_save_notify);
|
||||
#else
|
||||
/* STORAGE 套件构建:挂载/配置引擎/日志盘写全部跳过,存储由 testTask 独占 */
|
||||
@@ -192,14 +178,14 @@ void default_task_func(void *argument) {
|
||||
task_wd_start();
|
||||
|
||||
for (;;) {
|
||||
/* round 8 取证:进段打点——每步前记录段号到 .noinit 槽(复位不丢)。
|
||||
/* 第 8 轮取证:进段打点——每步前记录段号到 .noinit 槽(复位不丢)。
|
||||
* 误复位后开机 `PREVRUN seg=N` 即"上一轮主循环死在哪个调用点"
|
||||
* (round 7 初判"poller 自身停摆"经 round 8b 更正:真因是 IWDG 超时被静默
|
||||
* (第 7 轮初判"poller 自身停摆"经第 8b 轮更正:真因是 IWDG 超时被静默
|
||||
* 配成复位默认值,见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)。 */
|
||||
task_wd_seg(TASK_WD_SEG_POLL);
|
||||
/* 心跳抽样与喂狗判定(内部节流至 100ms;全员到位才喂,否则累计停喂) */
|
||||
task_wd_poll();
|
||||
/* P2 测量(Task #32):某位由"到过"变为"缺席"的瞬间留一份现场快照。
|
||||
/* 耗时测量:某位由"到过"变为"缺席"的瞬间留一份现场快照。
|
||||
* 走裸 UART 直发——缺席窗口内环形缓冲可能积压,这两行必须出去。
|
||||
*
|
||||
* ⚠ 判据用**位图边沿**,不能用 miss 累计数:`task_wd_miss_count()` 返回的是
|
||||
@@ -223,7 +209,7 @@ void default_task_func(void *argument) {
|
||||
if (sys_evtHandle != NULL) {
|
||||
(void)osEventFlagsWait(sys_evtHandle,
|
||||
SYS_EVT_CFG_SAVE | SYS_EVT_CONSOLE_TX
|
||||
| SYS_EVT_PROT_SET_SAVE,
|
||||
| SYS_EVT_PROT_SET_SAVE | SYS_EVT_RUN_PAR_SAVE,
|
||||
osFlagsWaitAny, 50u);
|
||||
}
|
||||
/* 控制台唯一发送点:把环形缓冲中的日志按块发出(无待发时立即返回) */
|
||||
@@ -245,14 +231,25 @@ void default_task_func(void *argument) {
|
||||
task_wd_seg(TASK_WD_SEG_CFGSAVE);
|
||||
net_cfg_save_run();
|
||||
/* 保护定值落盘(B7):内部消费脏标志,无改动 = 空操作;
|
||||
* 写失败脏回置 → iec104Task 下轮 poll 再投递事件 = 天然重试 */
|
||||
(void)prot_set_store_save();
|
||||
* 写失败脏回置 → iec104Task 下轮 poll 再投递事件 = 天然重试。
|
||||
* task-10「操作记录」写入点之一:⚠ 不能按 prot_set_store_save() 的返回值
|
||||
* 记——它对"无待写"同样返 0,照返回值记就是每 50 ms 刷一条**没发生过**的
|
||||
* 操作(正是"不许用静态演示数据充数"的反面)。先 peek 脏位(非消费,
|
||||
* iec104_serv.c 通知属主用的就是同一枚),只有"确实有待写且这趟写成功"
|
||||
* 才算一次整定操作。失败不记(判据 = 动作成功后写一条),脏位保留、
|
||||
* 下轮重试成功后才留痕。 */
|
||||
if ((prot_set_dirty_peek() != 0u) && (prot_set_store_save() == 0)) {
|
||||
syslog_post(SYSLOG_OPER, "settings saved");
|
||||
}
|
||||
/* 运行参数段落盘(卡 10):同款脏语义;save = 引擎整档外科写
|
||||
* ([run_par] 9 键 + 被 adc_cal_set 联动改写的 [adc_cal] 两段一趟落) */
|
||||
(void)run_par_store_save();
|
||||
#endif
|
||||
/* 配置文件变更轮询(2s 周期;沉降去抖在引擎内) */
|
||||
if ((cfg_poll_cnt++ % 40u) == 0u) {
|
||||
#if !defined(TEST_SUITE_STORAGE)
|
||||
task_wd_seg(TASK_WD_SEG_CFGPOLL);
|
||||
/* E-31 自愈:启动期挂载失败时,每 2s 重跑完整 bring-up——
|
||||
/* 存储自愈:启动期挂载失败时,每 2s 重跑完整 bring-up——
|
||||
* 瞬态毛刺(如 dhara resume 扫描的瞬时 SPI 故障)通常第二次
|
||||
* 即恢复;成功后由 storage_bring_up 补跑启动时跳过的
|
||||
* "存储就绪"序列(日志挂载/配置加载/屏障置位,均幂等);
|
||||
@@ -269,7 +266,7 @@ void default_task_func(void *argument) {
|
||||
syslog_post(SYSLOG_SELFTEST, "filesystem fault");
|
||||
}
|
||||
}
|
||||
/* P2-9:卷已就绪(STORAGE_READY)但日志后端未就绪(建 /log 曾失败,
|
||||
/* 卷已就绪(STORAGE_READY)但日志后端未就绪(建 /log 曾失败,
|
||||
* s_fs_ready 恒 0 → syslog_drain 永久 no-op)→ 2s 周期重试(幂等;
|
||||
* 就绪后 syslog_mcu_mounted 立即返回,仅一次 volatile 读)。 */
|
||||
if (sys_evtHandle != NULL
|
||||
@@ -295,11 +292,16 @@ void default_task_func(void *argument) {
|
||||
task_prof_enter(TASK_PROF_DEF_SYSLOG);
|
||||
syslog_drain();
|
||||
task_prof_exit(TASK_PROF_DEF_SYSLOG);
|
||||
/* 写回缓存空闲 flush 兜底:卷空闲(try 锁)+ 周期节流 + 就绪门控全在
|
||||
* 模块内(App/fs/vol_idle_sync.h);clean 态零 NAND 触点,脏态最坏 =
|
||||
* 一次 f_close 同型的同步段。放在 drain 之后:本拍若写过日志,close
|
||||
* 已同步过,这里随即 clean 短路——漏网脏页的驻留窗由周期封顶 */
|
||||
vol_idle_sync_poll();
|
||||
#endif
|
||||
/* 栈水位观测:首次 5s 后报告,此后每 2s 采样、出现更深峰值才打印 */
|
||||
task_wd_seg(TASK_WD_SEG_STACK);
|
||||
stack_probe_poll();
|
||||
/* P2 测量(Task #32):各任务最坏耗时 / 次数 / "仍在段内"快照。
|
||||
/* 各任务最坏耗时 / 次数 / "仍在段内"快照上报。
|
||||
* 内部节流(首次 5s,此后每 2s);判据:
|
||||
* uisn 持续增长 → uiTask 卡在 ui_net_sink_poll 的往返里(阻塞)
|
||||
* uipn 持续增长而 uisn 为 0 → 被调度饿死(本轮迟迟跑不完)
|
||||
@@ -417,7 +419,7 @@ static void stack_probe_poll(void) {
|
||||
* 下整体不参与编译(卷由 testTask 独占)
|
||||
* 函数说明:
|
||||
* 由 defaultTask 调用:启动期一次;若失败,主循环每 2s 重试至多
|
||||
* STORAGE_BRINGUP_RETRIES 次(E-31 自愈策略,2026-09-21):dhara_map_resume
|
||||
* STORAGE_BRINGUP_RETRIES 次(存储自愈策略,2026-09-21):dhara_map_resume
|
||||
* 失败分类修复后(nand_ftl.c 仅真空白判新卷,其余失败保留卷并返回
|
||||
* disk_initialize 失败),重试让瞬态毛刺(SPI 瞬时故障/扫描窗口抖动)
|
||||
* 在数秒内免重启恢复,成功时补跑启动期跳过的就绪序列(日志挂载/配置
|
||||
|
||||
+71
-19
@@ -1,22 +1,31 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 被控站服务(板端胶水任务)
|
||||
* 模块功能:2404 监听 + 单连接 socket 收发 + 把字节流喂给 iec104_link 纯逻辑
|
||||
* 层;对时经 sys_clock_set_unix 落墙钟;数据源走 iec104_pts 默认源
|
||||
* 模块功能:监听端口来自 [comm] 段(工厂默认 2404)的 TCP server + 单连接
|
||||
* socket 收发 + 把字节流喂给 iec104_link 纯逻辑层;对时经
|
||||
* sys_clock_set_unix 落墙钟;数据源走 iec104_pts 默认源
|
||||
* (phasor / prot_elm / prot_set)。协议语义全部在 App/protocol/iec104,
|
||||
* 本文件只做 I/O 编排(A-4 约束 1)。
|
||||
* 本文件只做 I/O 编排(分层约束)。
|
||||
* 线程约定:iec104Task 独占(NET_READY 屏障后循环,100ms 节拍 + 看门狗心跳)。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
* 修改记录:
|
||||
* 2026-10-26 DTU 项目组 道G task-8:公共地址与监听端口取值来源改
|
||||
* [comm] 段 getter(iec104_comm.h,改后重启生效;
|
||||
* 端口另在下次重 bind 自然生效)。只改取值来源,
|
||||
* 链路编排零改动。
|
||||
*/
|
||||
#include "iec104_serv.h"
|
||||
|
||||
#define DBG_LINE_BUF_MAX 128U /* 深链模块日志缓冲收紧(time_sync 先例) */
|
||||
#define DBG_TAG "[I104]"
|
||||
|
||||
#include "iec104_comm.h" /* [comm] 段 getter:ca / 监听端口(道G task-8) */
|
||||
#include "iec104_link.h"
|
||||
#include "iec104_pts.h"
|
||||
#include "net_socket.h"
|
||||
#include "prot_set.h" /* 定值脏标志 peek(B7 落盘通知) */
|
||||
#include "run_par.h" /* 运行参数脏 peek(卡 10 落盘通知,同款语义) */
|
||||
#include "sys_clock.h"
|
||||
#include "sys_log.h" /* task-19:对时成功落一条操作记录(SYSLOG_OPER) */
|
||||
#include "sys_events.h"
|
||||
#include "task_watchdog.h"
|
||||
#include "dbg_log.h"
|
||||
@@ -30,9 +39,9 @@
|
||||
/* 接收搬运缓冲(静态,不入栈;link 层内部另有 512B 重组缓冲) */
|
||||
#define IEC104_RX_SZ 128u
|
||||
/* 兜底节拍:门控判"无数据"时每 32 轮无条件收一次(ui_net_sink RM 先例,
|
||||
* 防 RECV_OK 中断丢失把数据永久藏住);须为 2 的幂 */
|
||||
* 防 RECV_OK 事件未收到把数据永久藏住);须为 2 的幂 */
|
||||
#define IEC104_LIVENESS_DIV 32u
|
||||
/* 连续发送失败阈值:同 ui_net_sink E-04(短写=对端收坏帧) */
|
||||
/* 连续发送失败阈值:同 ui_net_sink 先例(短写=对端收坏帧) */
|
||||
#define IEC104_SEND_FAIL_MAX 3u
|
||||
|
||||
/* ---------------- 服务状态(iec104Task 独占) ---------------- */
|
||||
@@ -50,15 +59,17 @@ static void s_send_apdu(void *ctx, const uint8_t *apdu, uint16_t len);
|
||||
static int s_clock_sync(void *ctx, uint64_t epoch_ms);
|
||||
static uint64_t s_epoch_ms(void *ctx);
|
||||
|
||||
/* V0.1 公共地址固定 1(点表口径;可配置项挂二期) */
|
||||
static const iec104_link_cfg_t s_cfg = { 1u, s_send_apdu, s_clock_sync, s_epoch_ms };
|
||||
/* 链路 cfg:ca 占位工厂默认,任务启动经 s_apply_comm_ca() 从 [comm] 段取真值
|
||||
* (道G task-8;点表口径默认 1);非 const 即此一处写、其余只读 */
|
||||
static iec104_link_cfg_t s_cfg = { IEC104_COMM_DEF_CA,
|
||||
s_send_apdu, s_clock_sync, s_epoch_ms };
|
||||
|
||||
/*
|
||||
* 函数功能:link 层发送回调——经 net_send 串行 RPC 发出一个 APDU
|
||||
* 入口参数:ctx 未用;apdu/len = 完整 APCI 帧
|
||||
* 返回值:无
|
||||
* 限定条件:s_client >= 0(在 link 调用栈内执行,即本任务上下文)
|
||||
* 函数说明:短写/失败累计(E-04 先例);连续 3 次 → 置 s_conn_dead,
|
||||
* 函数说明:短写/失败累计(ui_net_sink 先例);连续 3 次 → 置 s_conn_dead,
|
||||
* 由 s_service_link 在 link 调用返回后统一拆链(严禁在回调里拆)
|
||||
*/
|
||||
static void s_send_apdu(void *ctx, const uint8_t *apdu, uint16_t len) {
|
||||
@@ -84,19 +95,34 @@ static void s_send_apdu(void *ctx, const uint8_t *apdu, uint16_t len) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:link 层对时回调——epoch 毫秒落系统墙钟(含 SD2506 RTC 写回)
|
||||
* 函数功能:link 层对时回调——epoch 毫秒落系统墙钟(含 SD2506 RTC 写回),
|
||||
* 成功时顺带落一条操作记录(道F task-19:「时钟设置」是四处操作记录
|
||||
* 写入点之一,另外三处见 App/log/README.md 的五类记录对照表)
|
||||
* 入口参数:ctx 未用;epoch_ms = C_CS 解析出的 UTC 时刻
|
||||
* 返回值:0 成功 / -1 失败(link 层回 actcon+PN)
|
||||
* 函数说明:uint32 秒域上限 2106-02-07,越界显式拒绝(CP56 域到 2127)
|
||||
* 函数说明:uint32 秒域上限 2106-02-07,越界显式拒绝(CP56 域到 2127)。
|
||||
* **只在真正改成功之后才投记录**:越界拒绝与 sys_clock_set_unix 失败两条腿
|
||||
* 一条都不投——操作记录是审计面,写进去一条没发生过的"对时"比少一条更坏
|
||||
* (TC-I104SRV-050b / 051 分别钉这两条腿)。
|
||||
* 投递用 syslog_post_quiet():iec104Task 是深链任务(本文件顶部已把
|
||||
* DBG_LINE_BUF_MAX 收到 128 就是同一件事的证据),不许在它的调用链上
|
||||
* 内联日志行缓冲;两个投递口的收/丢判据一致,差别只在留痕位置
|
||||
* (口径与理由见 App/log/README.md「两个投递口」)。
|
||||
* 文案是 ROM 常量、17 字节 < SYSLOG_DESC_MAX(28):iec104Task 里不 snprintf。
|
||||
*/
|
||||
static int s_clock_sync(void *ctx, uint64_t epoch_ms) {
|
||||
uint64_t secs = epoch_ms / 1000ULL;
|
||||
int rc;
|
||||
|
||||
(void)ctx;
|
||||
if (secs > 0xFFFFFFFFULL) {
|
||||
return -1;
|
||||
}
|
||||
return sys_clock_set_unix((uint32_t)secs);
|
||||
rc = sys_clock_set_unix((uint32_t)secs);
|
||||
if (rc == 0) {
|
||||
syslog_post_quiet(SYSLOG_OPER, "clock set by 104");
|
||||
}
|
||||
return rc;
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -117,7 +143,18 @@ static uint64_t s_epoch_ms(void *ctx) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:创建并监听 2404(socket+bind+listen,抄 ui_net_sink bind_listen)
|
||||
* 函数功能:把 [comm] 段公共地址取进链路 cfg(道G task-8 取值来源改造)
|
||||
* 返回值:无
|
||||
* 限定条件:iec104_link_init 之前调用(任务启动一次)——ca 生效语义 =
|
||||
* 改后重启;端口不同:s_bind_listen 每次现取 iec104_comm_port(),
|
||||
* 下次重 bind(拆链重建/网络代次重建)即生效,不必整机重启
|
||||
*/
|
||||
static void s_apply_comm_ca(void) {
|
||||
s_cfg.ca = iec104_comm_ca();
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:创建并监听 [comm] port(socket+bind+listen,抄 ui_net_sink bind_listen)
|
||||
* 返回值:0 成功(s_listen 写回)/ -1 失败
|
||||
* 限定条件:net 子系统可用(NET_READY 屏障后)
|
||||
*/
|
||||
@@ -131,7 +168,7 @@ static int s_bind_listen(void) {
|
||||
}
|
||||
(void)memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = NET_AF_INET;
|
||||
addr.sin_port = net_htons(IEC104_SERV_PORT);
|
||||
addr.sin_port = net_htons(iec104_comm_port());
|
||||
addr.sin_addr.s_addr = NET_INADDR_ANY;
|
||||
if (net_bind(ls, (struct net_sockaddr *)&addr, (int)sizeof(addr)) < 0) {
|
||||
net_close(ls);
|
||||
@@ -189,8 +226,8 @@ static void s_service_link(uint32_t now) {
|
||||
* 函数功能:单次服务循环(代次自愈 → accept → 门控收 → 喂 link → 节拍)
|
||||
* 返回值:无
|
||||
* 限定条件:NET_READY 已置位;s_cfg/s_link 已 init
|
||||
* 函数说明:门控(net_can_accept/net_can_read 纯 RAM 读)+ 兜底节拍照
|
||||
* ui_net_sink I-1/I-2;EOF 时 client==listen 严禁 close(net 层
|
||||
* 函数说明:门控(net_listener_armed/net_can_read 纯 RAM 读)+ 兜底节拍照
|
||||
* ui_net_sink 先例;EOF 时 client==listen 严禁 close(net 层
|
||||
* 已 auto_relisten,close 会连监听一起毁)
|
||||
*/
|
||||
static void s_serv_poll(void) {
|
||||
@@ -224,7 +261,13 @@ static void s_serv_poll(void) {
|
||||
if (s_client < 0) {
|
||||
int c;
|
||||
|
||||
if (net_can_accept(s_listen) == 0) {
|
||||
/* F-NET-4:门控从 net_can_accept(只认 ESTABLISHED,已退役)
|
||||
* 迁 net_listener_armed——旧门控把 LISTENING 窗口全挡掉,:2404 的丢
|
||||
* CONNECT 探针(net_accept_locked 内)因此从不运行(与已修的 :7003
|
||||
* 同型);armed=ESTABLISHED 待 accept / LISTENING /
|
||||
* CLOSED+auto_relisten+残留(ESTABLISHED 放行由台架实证补齐:
|
||||
* 漏它则本任务 200ms 周期抢不过对账② ~400ms 去抖、整场饿死) */
|
||||
if (net_listener_armed(s_listen) == 0) {
|
||||
if (net_get_sock(s_listen) == NULL) {
|
||||
s_listen = -1; /* 控制块消失:交回上方重建 */
|
||||
}
|
||||
@@ -252,7 +295,10 @@ static void s_serv_poll(void) {
|
||||
s_liveness_ctr = 0u;
|
||||
}
|
||||
|
||||
n = net_recv(s_client, s_rx, (int)sizeof(s_rx), NET_MSG_DONTWAIT);
|
||||
/* DONTWAIT 已转正为具名 API net_recv_nb——本任务在 IWDG
|
||||
* required_mask 内且有 100ms 周期职责(喂狗/链路节拍/上送),禁止无限
|
||||
* 阻塞 recv(文档契约见 net_socket.h net_recv 注释),非阻塞轮询是义务 */
|
||||
n = net_recv_nb(s_client, s_rx, (int)sizeof(s_rx));
|
||||
if (n > 0) {
|
||||
iec104_link_on_rx(&s_link, s_rx, (uint16_t)n, now);
|
||||
} else if (n == 0) {
|
||||
@@ -297,13 +343,14 @@ void iec104_serv_task(void *argument) {
|
||||
}
|
||||
|
||||
iec104_pts_init();
|
||||
s_apply_comm_ca();
|
||||
iec104_link_init(&s_link, &s_cfg, NULL);
|
||||
s_gen = net_get_generation();
|
||||
if (s_bind_listen() < 0) {
|
||||
DBG_ERROR("i104: bind port %u FAIL, retry in loop",
|
||||
(unsigned)IEC104_SERV_PORT);
|
||||
(unsigned)iec104_comm_port());
|
||||
} else {
|
||||
DBG_INFO("i104: listening on %u", (unsigned)IEC104_SERV_PORT);
|
||||
DBG_INFO("i104: listening on %u", (unsigned)iec104_comm_port());
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
@@ -315,6 +362,11 @@ void iec104_serv_task(void *argument) {
|
||||
if ((prot_set_dirty_peek() != 0u) && (sys_evtHandle != NULL)) {
|
||||
(void)osEventFlagsSet(sys_evtHandle, SYS_EVT_PROT_SET_SAVE);
|
||||
}
|
||||
/* 运行参数整定(C_SE 0x8020~0x8058 段)→ 通知属主落盘 config.ini
|
||||
* (卡 10;peek 非消费 = 写盘失败天然重试,与上块 B7 语义完全同款) */
|
||||
if ((run_par_dirty_peek() != 0u) && (sys_evtHandle != NULL)) {
|
||||
(void)osEventFlagsSet(sys_evtHandle, SYS_EVT_RUN_PAR_SAVE);
|
||||
}
|
||||
osDelay(IEC104_POLL_MS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,10 @@
|
||||
#ifndef IEC104_SERV_H
|
||||
#define IEC104_SERV_H
|
||||
|
||||
/* IEC 60870-5-104 被控站监听端口(DL/T 634.5104 标准端口) */
|
||||
/* IEC 60870-5-104 被控站监听端口(DL/T 634.5104 标准端口)。
|
||||
* 道G task-8 起运行期真源 = [comm] 段(App/protocol/iec104/iec104_comm.h,
|
||||
* 工厂默认 IEC104_COMM_DEF_PORT 与本宏同值);本宏保留仅作快照兼容出口
|
||||
* (App/app/ui_data_feed.c 的 port_104——该处切 getter 属 UI 道 task-9)。 */
|
||||
#define IEC104_SERV_PORT 2404u
|
||||
|
||||
/*
|
||||
|
||||
+19
-29
@@ -11,16 +11,20 @@
|
||||
* v1.2 2026-07-21 王建锋 网络初始化移入本任务(net_init + TCP 监听)
|
||||
* v1.3 2026-07-22 王建锋 单连接模式重构,增加大文件/边界/KeepAlive 测试
|
||||
* v1.4 2026-09-14 王建锋 新增自检埋点:PHY 链路 3s 未建立 → sys_log "comms fault"
|
||||
* v1.5 2026-09-15 王建锋 配置加载移交 defaultTask(配置引擎属主,P1-3);
|
||||
* v1.5 2026-09-15 王建锋 配置加载移交 defaultTask(配置引擎属主);
|
||||
* 本任务只读 RAM 副本
|
||||
* v1.6 2026-09-15 王建锋 P1-6:入口加 CH395F 硬件层测试模式自挂起守卫
|
||||
* v1.6 2026-09-15 王建锋 入口加 CH395F 硬件层测试模式自挂起守卫
|
||||
* (基础线程无条件创建,该模式下不运行);
|
||||
* freertos.c USER CODE 区重复创建已删除
|
||||
* v1.7 2026-10-02 王建锋 P1-8:初始组网失败/重建失败 → 自检记录 + 30s 周期重试
|
||||
* v1.7 2026-10-02 王建锋 初始组网失败/重建失败 → 自检记录 + 30s 周期重试
|
||||
* (原实现:初始失败 = netTask 无限驻留;重建失败 = 静默丢配置)
|
||||
* v1.8 2026-10-10 王建锋 Task #32 的 P2 测量埋点:net_poll() 单独计时——
|
||||
* v1.8 2026-10-10 王建锋 任务耗时测量埋点:net_poll() 单独计时——
|
||||
* 本任务每 ~1ms 分派一次消息队列,若驱动轮询被拉长,
|
||||
* 其它任务(尤其 UI 的 net_accept 往返)的等待随之拉长
|
||||
* v1.9 2026-10-21 王建锋 删除 net_task_init():空壳入口(体内只有一条
|
||||
* DBG_INFO),全库无调用点,而函数头却写"创建 TCP
|
||||
* Server 监听 Socket,注册回调等"——描述与实际不符,
|
||||
* 监听实际在 net_stack_bring_up() 与各服务里做
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
@@ -34,7 +38,7 @@
|
||||
#include "sys_log.h"
|
||||
#include "test_config.h" /* 测试模式宏(ENABLE_HW_LAYER_TESTS);产品构建全未定义,无作用 */
|
||||
#include "task_watchdog.h"
|
||||
#include "task_prof.h" /* P2 探针:net_poll 耗时(Task #32) */
|
||||
#include "task_prof.h" /* 探针:net_poll 耗时 */
|
||||
|
||||
#define DBG_TAG "[NET_TASK]"
|
||||
#include "dbg_log.h"
|
||||
@@ -48,7 +52,7 @@
|
||||
* 函数说明:只置事件位;真正的重建在主循环内完成(CH395F SPI 归 netTask 独占,
|
||||
* UI 不得直接操作)
|
||||
*/
|
||||
/* P1-8(2026-10-02):重建失败延迟重试的截止 tick;0 = 无待重试 */
|
||||
/* 重建失败延迟重试的截止 tick;0 = 无待重试 */
|
||||
static uint32_t s_reinit_retry_at;
|
||||
|
||||
static void net_reconfig_request_cb(void) {
|
||||
@@ -110,14 +114,12 @@ int net_stack_bring_up(void) {
|
||||
ch395f_set_phy(CH395F_PHY_100M_FULL);
|
||||
|
||||
/* 强制 PHY 模式后链路会短暂掉线再重建,CH395F 会锁存 GINT_STAT_PHY_CHANGE(0x04) 全局中断标志。
|
||||
* 这里轮询该标志等待"链路状态变化"(每次读取即清除),同时用 ch395f_get_phy_status()
|
||||
* 确认链路是否已真正建立——避免因 PHY_CHANGE 只反映"掉线"边沿而误判就绪。
|
||||
* 链路一就绪(phy != DISCONN)即跳出;最多等约 3s(60 × 50ms),超时仍按实际状态继续(由下方日志暴露问题)。
|
||||
* 注:此处读取全局中断状态属"启动期一次性链路等待",net_poll 尚未启动,不与其中断集中处理冲突;
|
||||
* NET 模式下该标志后续仍会被 net_poll 正常消费/清除(见 net_socket.c)。 */
|
||||
* 这里只轮询 ch395f_get_phy_status() 确认链路是否已真正建立——**不读全局中断寄存器**:
|
||||
* 它读即清,唯一读者 = net_poll(F-NET-7,用户裁定 2026-10-13 测试代码同样禁自行读取)。
|
||||
* 锁存的 PHY_CHANGE 会在 net_poll 首轮被正常消费(喂驱动链路态缓存,无害)。
|
||||
* 链路一就绪(phy != DISCONN)即跳出;最多等约 3s(60 × 50ms),超时仍按实际状态继续(由下方日志暴露问题)。 */
|
||||
uint8_t phy = CH395F_PHY_DISCONN;
|
||||
for (unsigned int i = 0U; i < 60U; i++) {
|
||||
(void)ch395f_get_glob_int_status_all(); /* 读取并清除 PHY_CHANGE 等全局中断标志 */
|
||||
phy = ch395f_get_phy_status();
|
||||
if (phy != CH395F_PHY_DISCONN) {
|
||||
break;
|
||||
@@ -147,18 +149,6 @@ int net_stack_bring_up(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:网络任务初始化
|
||||
* 入口参数:无
|
||||
* 返 回 值:0 - 成功,-1 - 失败
|
||||
* 限定条件:CH395F 协议栈已初始化
|
||||
* 函数说明:创建 TCP Server 监听 Socket,注册回调等
|
||||
*/
|
||||
int net_task_init(void) {
|
||||
DBG_INFO("net_task_init");
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:网络任务主函数
|
||||
* 入口参数:arg - FreeRTOS 传入参数(未使用)
|
||||
@@ -196,7 +186,7 @@ void net_task_func(void const *arg) {
|
||||
* 2026-10-02 台架修复:10s 超时等待改 100ms 分片 + 每片喂心跳——原实现
|
||||
* 在等待期无心跳:若存储 bring-up 失败,本等待跑满 10s 超时,而 IWDG
|
||||
* 此时已启动(defaultTask 在 bring-up 后即启动),NET 位长期缺失 →
|
||||
* 必需掩码不可达 → 复位循环,E-31 的 2s 自愈重试根本没有机会。
|
||||
* 必需掩码不可达 → 复位循环,存储自愈的 2s 重试根本没有机会。
|
||||
* 分片喂位保证"存储未就绪"降级态下 NET 位持续新鲜。 */
|
||||
uint32_t rounds_left = 100u; /* 100 × 100ms = 原 10s 超时语义 */
|
||||
for (;;) {
|
||||
@@ -212,7 +202,7 @@ void net_task_func(void const *arg) {
|
||||
}
|
||||
}
|
||||
|
||||
/* P1-8(2026-10-02):初始组网失败不自驻留——自检台账留痕 + 30s 周期重试
|
||||
/* 初始组网失败不自驻留——自检台账留痕 + 30s 周期重试
|
||||
* (1s 分片、每片喂心跳):线缆/芯片恢复即可当场完成组网;原 for(;;)osDelay
|
||||
* 会令看门狗 NET 位持续陈旧,并阻塞全部下游就绪屏障 */
|
||||
for (;;) {
|
||||
@@ -245,7 +235,7 @@ void net_task_func(void const *arg) {
|
||||
|
||||
/* UI 保存网口配置后请求立即应用:在此重建网络(新 IP/掩码/网关/MAC)。
|
||||
* 重建完成后代次号已递增,各 TCP 服务据此重建监听;期间连接会断开(预期)。
|
||||
* P2-11(2026-10-04):用 osEventFlagsWait(timeout=0) 原子"取走并清除"RECONFIG
|
||||
* 用 osEventFlagsWait(timeout=0) 原子"取走并清除"RECONFIG
|
||||
* 位,替换原 Get+Clear 两步;原写法在 Get 与 Clear 之间若有生产者在同一位上
|
||||
* Set,该次请求会被 Clear 一并吞掉(丢事件窗口)。 */
|
||||
if (sys_evtHandle != NULL) {
|
||||
@@ -257,7 +247,7 @@ void net_task_func(void const *arg) {
|
||||
(void)osEventFlagsSet(sys_evtHandle, SYS_EVT_NET_READY);
|
||||
s_reinit_retry_at = 0U;
|
||||
} else {
|
||||
/* P1-8(2026-10-02):重建失败 → 自检留痕 + 30s 延迟重试(重事件位),
|
||||
/* 重建失败 → 自检留痕 + 30s 延迟重试(重事件位),
|
||||
* 新配置不再静默丢失(原实现仅一条 DBG_ERROR 后丢弃) */
|
||||
DBG_ERROR("netTask: re-init FAIL, retry in 30s");
|
||||
(void)syslog_post(SYSLOG_SELFTEST, "net reinit fail");
|
||||
@@ -265,7 +255,7 @@ void net_task_func(void const *arg) {
|
||||
}
|
||||
} else if ((s_reinit_retry_at != 0U)
|
||||
&& ((int32_t)((uint32_t)HAL_GetTick() - s_reinit_retry_at) >= 0)) {
|
||||
/* P1-8:30s 倒计时到 → 重新触发重建(符号差处理 tick 回绕) */
|
||||
/* 30s 倒计时到 → 重新触发重建(符号差处理 tick 回绕) */
|
||||
s_reinit_retry_at = 0U;
|
||||
(void)osEventFlagsSet(sys_evtHandle, SYS_EVT_NET_RECONFIG);
|
||||
}
|
||||
|
||||
+2
-2
@@ -8,7 +8,8 @@
|
||||
* v1.0 2026-07-19 王建锋 创建初始版本
|
||||
* v1.1 2026-08-24 王建锋 提取 net_stack_bring_up() 供硬件层/NET 层两种测试模式共用
|
||||
* v1.2 2026-09-15 王建锋 硬件层模式描述修正:netTask 恒创建(基础线程),
|
||||
* 该模式下自挂起不运行(P1-6 守卫)
|
||||
* 该模式下自挂起不运行(入口自挂起守卫)
|
||||
* v1.3 2026-10-21 王建锋 删除 net_task_init() 声明(空壳入口、无调用点)
|
||||
*/
|
||||
|
||||
#ifndef NET_TASK_H
|
||||
@@ -23,7 +24,6 @@ extern "C" {
|
||||
* netTask 自挂起(创建但不运行),由 testTask 调用后独占 CH395F。 */
|
||||
int net_stack_bring_up(void);
|
||||
|
||||
int net_task_init(void);
|
||||
void net_task_func(void const *arg);
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
+160
-19
@@ -8,11 +8,42 @@
|
||||
* 可忽略;失配 → prot_set 内部闭锁,eval 自动复位全元件);
|
||||
* ③ prot_elm_run()(phasor rms → 判据步进)+ 动作位图边沿日志;
|
||||
* ④ 全局软压板 PS_GLOBAL_EN(104 YK12 两段式投退,B7):退出 =
|
||||
* 复位元件 + 停判据(与采样闭锁同构,独立边沿日志)。
|
||||
* 复位元件 + 停判据(与采样闭锁同构,独立边沿日志);
|
||||
* ⑤ 未标定闭锁(「数据可信」卡 5,M4 唯一新增行为变更):UA/UN/IA
|
||||
* 的 adc_cal VALID 位未全置位 → 工程量还只是 ADC 引脚电压
|
||||
* (eng=0),拿去比二次值定值必然错判 → 复位元件 + 本拍不 eval。
|
||||
* 默认构建(未标定)恒闭锁是**期望行为**:边沿用 DBG_INFO 而非
|
||||
* [ERR](未标定是出厂常态、不是故障;每次开机都走这条沿,
|
||||
* [ERR] 还会误触 @test 禁用标记);台架旁路只走编译期宏
|
||||
* DTU_BENCH(「数据可信」§4.3:不进 cfg、不进 104,本仓任何
|
||||
* 提交目标都不定义它),且该宏**只作用于判定本体**
|
||||
* adc_cal_prot_blocked()(App/algo/adc_cal.c,全仓唯一一处 #ifndef;
|
||||
* 道F task-16 之前本文件另有一份判定,两处可被分别开关 = 屏上
|
||||
* 「未标定」与判据停能量分叉,这就是收敛的原因)。判定优先级 =
|
||||
* 采样可信在前、标定可用在后(数据都旧了谈不上标定)。
|
||||
* 定值/元件初始化在 app_main_init(内核前,B5);本任务只消费。
|
||||
* 线程约定:prot_elm_run / prot_set_selfcheck 仅本任务上下文(prot_elm.h);
|
||||
* 跨上下文复归(104 YK11)经 prot_elm_request_reset 落请求。
|
||||
* 创建日期:2026-10-12(B5)
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-12 王建锋 创建:L5-4 采样故障闭锁 + L5-1 定值巡检 + 判据节拍
|
||||
* v1.1 2026-10-21 王建锋 ①之外增加**兜底新鲜度门**:本任务用自己的时基直接
|
||||
* 看两个中断侧计数器是否还在前进(ADC_HF_STALE)。
|
||||
* adc_health 的 stall 位依赖 adcTask 每拍跑 check(),
|
||||
* 节拍源一停本任务就再没有任何人判"数据新不新",
|
||||
* 而相量冻在最后一帧上还会被本任务继续用于判据
|
||||
* v1.2 2026-10-25 DTU 项目组 卡 5:新增第⑤路「未标定闭锁」(adc_cal_flags
|
||||
* VALID 口径,docs/任务规划/数据可信.md §4.3):
|
||||
* 采样恢复分支内、巡检/压板之前判;边沿 INFO 对
|
||||
* + SOE_T_CAL_BLOCK/RESUME(不用 [ERR]:未标定是
|
||||
* 常态非故障,且 @test 禁用 [ERR]);DTU_BENCH
|
||||
* 编译期旁路(本仓目标不定义 = 产品恒强制闭锁)
|
||||
* v1.3 2026-10-26 DTU 项目组 道F task-16:本文件的三路判定 s_cal_locked()
|
||||
* 删除,改调 App/algo/adc_cal.c 的 adc_cal_prot_blocked()
|
||||
(同一事实的单一真源;DTU_BENCH 旁路随之收成一处)。
|
||||
行为不变:判定式、调用位置(采样恢复分支内、巡检/压板
|
||||
之前)、边沿日志与 SOE 分型全部照旧;s_prev_cal_lock
|
||||
仍只作边沿日志用,**不是**判据缓存
|
||||
*/
|
||||
#include "protect_task.h"
|
||||
|
||||
@@ -22,21 +53,41 @@
|
||||
#define DBG_LINE_BUF_MAX 128U
|
||||
#include "dbg_log.h"
|
||||
|
||||
#include "adc_input.h" /* adc_input_stm32_seq():DMA 完成计数(中断侧写) */
|
||||
#include "adc_health.h"
|
||||
#include "adc_cal.h" /* 卡 5:adc_cal_prot_blocked() = 未标定闭锁判定单点(道F task-16) */
|
||||
#include "console_log.h" /* dbg_console_writer_ready():启动锚点行门控(B6) */
|
||||
#include "prot_elm.h"
|
||||
#include "prot_set.h"
|
||||
#include "soe_store.h" /* B9:动作/复归边沿 → SOE 投递(零 FS,队列异步) */
|
||||
#include "sys_log.h" /* task-10:事故 / 保护告警 / 保护启动 三类文本记录 */
|
||||
#include "task_watchdog.h"
|
||||
#include "tpafe5160.h" /* tpafe5160_frame_seq():帧序号(BUSY EXTI 写) */
|
||||
|
||||
/* L5-4 采样故障电平闭锁位:stall(可自愈)+ backward(锁存)都须停判据。
|
||||
* 锁存类(FRSTDATA/HW_ERR 等)不在此列——它们是一次性事件痕迹,不构成
|
||||
* "当前测量不可信";持续 stall/backward 才是。 */
|
||||
#define PROT_HF_LOCK_MASK (ADC_HF_TPAFE_STALL | ADC_HF_STM32_STALL | ADC_HF_SEQ_BACKWARD)
|
||||
#define PROT_HF_LOCK_MASK (ADC_HF_TPAFE_STALL | ADC_HF_STM32_STALL | \
|
||||
ADC_HF_SEQ_BACKWARD | ADC_HF_STALE)
|
||||
|
||||
/* 兜底新鲜度门限。判定层的 stall 是 100 ms,但那是"每拍都在跑的观察者"的口径;
|
||||
* 本门每 20 ms 才观测一次,最坏比判定层晚一整周期 → 取 100 + 20 = 120 ms,让
|
||||
* "判定层先报(带日志与自检留痕)、本门兜底(判定层死了也报)"的时序阶梯成立。
|
||||
* 排查时 hf 里 0x1/0x2 与 0x100 的先后,本身就是"判定层当时活着吗"的答案。 */
|
||||
#define PROT_STALE_AGE_MS (ADC_HEALTH_STALL_MS + PROTECT_TASK_CYCLE_MS)
|
||||
|
||||
/* 兜底门的参照系:两条车道各存一份。⚠ primed 标志也必须各自独立——若共用一个,
|
||||
* 先被观测的那条会把它置 1,另一条首拍就拿 adv_ms = 0 去算年龄 → 开机即误锁。
|
||||
* 状态放调用方而不是复用 adc_health 的模块级状态:那个是"每拍一次、单写者"的
|
||||
* 契约,两个任务同写会互相刷新差值基线,把现有判定层一起弄坏。 */
|
||||
static adc_health_age_t s_age_tp; /* TPAFE 车道 */
|
||||
static adc_health_age_t s_age_a1; /* STM32 车道 */
|
||||
|
||||
static uint32_t s_prev_active; /* 上拍动作位图(边沿日志用) */
|
||||
static uint32_t s_prev_pick; /* 上拍启动位图(「保护启动」记录边沿用,task-10) */
|
||||
static uint8_t s_prev_hf_lock; /* 上拍采样闭锁态(边沿日志用) */
|
||||
static uint8_t s_prev_plate_out; /* 上拍全局软压板退出态(边沿日志用,B7) */
|
||||
static uint8_t s_prev_cal_lock; /* 上拍未标定闭锁态(边沿日志用,卡 5) */
|
||||
|
||||
/*
|
||||
* 函数功能:动作位图变化沿日志 + SOE 投递(逐变化元件)
|
||||
@@ -72,6 +123,61 @@ static void s_log_active_edge(uint32_t active) {
|
||||
s_prev_active = active;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:把元件状态边沿落成三类文本记录(事故 / 保护告警 / 保护启动)
|
||||
* 入口参数:active - 本拍 prot_elm_active_mask()(asserted 位图) uint32_t
|
||||
* pick - 本拍 prot_elm_pick_mask()(picked 位图) uint32_t
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅本任务上下文;**只投递、不碰盘**(唯一写者是 defaultTask),
|
||||
* 投递走 syslog_post_quiet()——本任务静态最坏链是 B9 写进 DoD 的硬线
|
||||
* (872 B / 57%),syslog_post 的两个满态留痕分支各内联约 216 B 行缓冲,
|
||||
* 在这里调它等于把链垫回 stack_report 的 FAIL 区(同本文件不用
|
||||
* snprintf/DBG 拼记录的纪律,见 s_log_active_edge 函数头)
|
||||
* 函数说明:三类记录的**类型就是目标文件**(一类一个 /log 文件,记录体内没有
|
||||
* 类型字段位;决策与理由见 App/log/README.md)。判据:
|
||||
* 1) picked 上升沿 → SYSLOG_START("保护启动"):越限电平一到就记,
|
||||
* 不等定时限到点——只记动作会让"启动了但没到点"那段在记录里消失;
|
||||
* 2) asserted上升沿 → 归 事故 还是 告警 由 prot_elm_record_kind() 判,
|
||||
* 其判据唯一来源是定值表里的 PS_*_TRIP(0 告警 / 1 跳闸);本函数
|
||||
* 不另立"哪些元件算跳闸"的名单,避免与 g_prot_set_meta / k_pe[] 打架。
|
||||
* 返回沿(asserted/picked 掉落)**不记**:复归留痕已由 s_log_active_edge
|
||||
* 的 SOE 通道承担,再写一份就是同一事件两个真源。
|
||||
* 同拍既启动又动作(延时段 0ms / 速断)→ 两条都记,顺序先启动后动作,
|
||||
* 与物理事件次序一致。
|
||||
* desc 直接用 prot_elm_name() 的 ROM 常量("OC1-IA" 等,最长 6 字节
|
||||
* << SYSLOG_DESC_MAX=28),零格式化、零栈缓冲。
|
||||
*/
|
||||
static void s_log_record_edge(uint32_t active, uint32_t pick) {
|
||||
uint32_t p_chg = pick ^ s_prev_pick;
|
||||
uint32_t a_chg = active ^ s_prev_active;
|
||||
uint32_t i;
|
||||
|
||||
for (i = 0u; (i < (uint32_t)PE_COUNT) && (((p_chg | a_chg) != 0u)); i++) {
|
||||
const uint32_t bit = 1u << i;
|
||||
|
||||
if (((p_chg & bit) != 0u) && ((pick & bit) != 0u)) {
|
||||
syslog_post_quiet(SYSLOG_START, prot_elm_name((prot_elm_id_t)i));
|
||||
}
|
||||
if (((a_chg & bit) != 0u) && ((active & bit) != 0u)) {
|
||||
switch (prot_elm_record_kind((prot_elm_id_t)i)) {
|
||||
case PROT_REC_TRIP:
|
||||
syslog_post_quiet(SYSLOG_ACCIDENT, prot_elm_name((prot_elm_id_t)i));
|
||||
break;
|
||||
case PROT_REC_ALARM:
|
||||
syslog_post_quiet(SYSLOG_ALARM, prot_elm_name((prot_elm_id_t)i));
|
||||
break;
|
||||
default:
|
||||
/* PROT_REC_NONE:元件号越界。位图来自 prot_elm 自己的表,正常
|
||||
* 走不到这里;真走到了就是表与位图不一致,一条都不能落——
|
||||
* 落错类别的审计记录比少一条记录更坏。 */
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
s_prev_pick = pick;
|
||||
s_prev_active = active;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:protectTask 线程入口
|
||||
* 入口参数:argument - 未用
|
||||
@@ -89,6 +195,18 @@ void protect_task(void *argument) {
|
||||
uint32_t now = osKernelGetTickCount();
|
||||
uint32_t hf = adc_health_flags();
|
||||
|
||||
/* 兜底新鲜度门(L5-4 的独立通道):节拍源停摆时 adcTask 会永久阻塞在
|
||||
* osThreadFlagsWait(那是刻意的 fail-safe),check() 不再执行 → stall 位
|
||||
* 永远是 0。这里用本任务自己的时基直接读两个**中断侧递增**的计数器,
|
||||
* 与 adcTask 的存亡无关;两者都只要 TIM3 还在触发就会前进。
|
||||
* STM32 车道只更新参照系、不额外置位:任一侧老了就同一个位闭锁。 */
|
||||
if (adc_health_age_stale(&s_age_tp, tpafe5160_frame_seq(), now,
|
||||
PROT_STALE_AGE_MS) != 0u) {
|
||||
hf |= ADC_HF_STALE;
|
||||
}
|
||||
(void)adc_health_age_stale(&s_age_a1, adc_input_stm32_seq(), now,
|
||||
PROT_STALE_AGE_MS);
|
||||
|
||||
/* 启动锚点行:控制台写者注册后首拍补打(2026-10-10 门控纪律,adcTask
|
||||
* 首帧同款)。写者注册前 con_append 走无锁裸发,与 defaultTask 启动日志
|
||||
* 逐字节交错——B6 台架实证:本行曾损坏为 "st[art: cycle=220ms elm=70"
|
||||
@@ -118,33 +236,56 @@ void protect_task(void *argument) {
|
||||
SOE_ELM_NONE, 0u);
|
||||
s_prev_hf_lock = 0u;
|
||||
}
|
||||
/* L5-1 巡检:定值区与封存校验和不符 → prot_set 闭锁(下一拍起
|
||||
* eval 自动复位全元件)。B7 收口:104 定值写改走
|
||||
* prot_set_set_sealed(影子区写+封存后单字交换),selfcheck 另有
|
||||
* seqlock 式复核(提交撞见 = 本轮跳过),误锁窗口已消除。 */
|
||||
(void)prot_set_selfcheck();
|
||||
/* 全局软压板(PS_GLOBAL_EN,104 YK12 两段式投退,B7):退出 =
|
||||
* 保护停止判据并复位元件状态(重新投入从干净快照起步) */
|
||||
if (prot_set_get_ctl(PS_GLOBAL_EN) == 0u) {
|
||||
if (s_prev_plate_out == 0u) {
|
||||
DBG_INFO("global plate OUT -> protection disabled");
|
||||
(void)soe_store_post((uint8_t)SOE_T_PLATE_OUT,
|
||||
/* 卡 5:未标定闭锁——夹在「采样可信」与「巡检/压板」之间:
|
||||
* eng=0 域(引脚电压)拿去比二次值定值必然错判(UV 尤甚),
|
||||
* 标定 VALID 未全置位前整条判据链停走(「数据可信」§4.3 决策 1) */
|
||||
/* 判定本体在 adc_cal_prot_blocked()(标定层单点,道F task-16):本任务
|
||||
* 只是消费者,不再自带一份三路判定 */
|
||||
if (adc_cal_prot_blocked() != 0u) {
|
||||
if (s_prev_cal_lock == 0u) {
|
||||
DBG_INFO("uncalibrated -> protection blocked (UA/UN/IA VALID not all set)");
|
||||
(void)soe_store_post((uint8_t)SOE_T_CAL_BLOCK,
|
||||
SOE_ELM_NONE, 0u);
|
||||
s_prev_plate_out = 1u;
|
||||
s_prev_cal_lock = 1u;
|
||||
}
|
||||
prot_elm_init();
|
||||
} else {
|
||||
if (s_prev_plate_out != 0u) {
|
||||
DBG_INFO("global plate IN -> protection enabled");
|
||||
(void)soe_store_post((uint8_t)SOE_T_PLATE_IN,
|
||||
if (s_prev_cal_lock != 0u) {
|
||||
DBG_INFO("calibration VALID -> protection unlocked");
|
||||
(void)soe_store_post((uint8_t)SOE_T_CAL_RESUME,
|
||||
SOE_ELM_NONE, 1u);
|
||||
s_prev_plate_out = 0u;
|
||||
s_prev_cal_lock = 0u;
|
||||
}
|
||||
/* L5-1 巡检:定值区与封存校验和不符 → prot_set 闭锁(下一拍起
|
||||
* eval 自动复位全元件)。B7 收口:104 定值写改走
|
||||
* prot_set_set_sealed(影子区写+封存后单字交换),selfcheck 另有
|
||||
* seqlock 式复核(提交撞见 = 本轮跳过),误锁窗口已消除。 */
|
||||
(void)prot_set_selfcheck();
|
||||
/* 全局软压板(PS_GLOBAL_EN,104 YK12 两段式投退,B7):退出 =
|
||||
* 保护停止判据并复位元件状态(重新投入从干净快照起步) */
|
||||
if (prot_set_get_ctl(PS_GLOBAL_EN) == 0u) {
|
||||
if (s_prev_plate_out == 0u) {
|
||||
DBG_INFO("global plate OUT -> protection disabled");
|
||||
(void)soe_store_post((uint8_t)SOE_T_PLATE_OUT,
|
||||
SOE_ELM_NONE, 0u);
|
||||
s_prev_plate_out = 1u;
|
||||
}
|
||||
prot_elm_init();
|
||||
} else {
|
||||
if (s_prev_plate_out != 0u) {
|
||||
DBG_INFO("global plate IN -> protection enabled");
|
||||
(void)soe_store_post((uint8_t)SOE_T_PLATE_IN,
|
||||
SOE_ELM_NONE, 1u);
|
||||
s_prev_plate_out = 0u;
|
||||
}
|
||||
prot_elm_run(now);
|
||||
}
|
||||
prot_elm_run(now);
|
||||
}
|
||||
}
|
||||
|
||||
s_log_active_edge(prot_elm_active_mask());
|
||||
/* 三类文本记录(task-10):放在 SOE 边沿之后、喂狗之前,纯 RAM 投递 */
|
||||
s_log_record_edge(prot_elm_active_mask(), prot_elm_pick_mask());
|
||||
|
||||
task_wd_beat(TASK_WD_PROTECT);
|
||||
osDelay(PROTECT_TASK_CYCLE_MS);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* 创建日期:2026-07-19
|
||||
* 修改记录:
|
||||
* v1.0 2026-07-19 王建锋 创建初始版本
|
||||
* v1.1 2026-10-05 王建锋 P3-11:注明"预留桩(未启用)",避免误认为已实现
|
||||
* v1.1 2026-10-05 王建锋 注明"预留桩(未启用)",避免误认为已实现
|
||||
*/
|
||||
|
||||
#include "rs485_task.h"
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 模块名称:Task Handles(内核任务句柄与属性块的集中声明)
|
||||
* 模块功能:把散落在各 .c 里的跨文件裸 extern 收拢成一份有名字的契约,供栈水位观测、
|
||||
* 看门狗位图、按句柄取剩余栈等消费方引用
|
||||
* 适用平台:STM32F407ZGTx
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-22
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-22 王建锋 从 App/task/default_task.c 的 18 行局部 extern 收拢而来。
|
||||
* 唯一定义处仍是 Src/freertos.c(CubeMX 生成的任务表),本文件
|
||||
* 只是引用契约:新增/改名任务必须同步这里,否则报错落在链接期
|
||||
* 而不是静默——这是有意的(原先每加一个任务就往 default_task.c
|
||||
* 再抄两行 extern,抄漏一行就是读别人的栈)。
|
||||
*/
|
||||
|
||||
#ifndef TASK_HANDLES_H
|
||||
#define TASK_HANDLES_H
|
||||
|
||||
#include "cmsis_os.h" /* osThreadId_t / osThreadAttr_t */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/*
|
||||
* 以下符号全部定义于 Src/freertos.c(CubeMX 任务表,只写 USER CODE 区,不可外移)。
|
||||
* p_attr->stack_size 是「配置栈大小」的唯一起点;实测最坏链与余量台账见
|
||||
* docs/内存管理/RAM使用分析与优化方案.md 的 2.3 与 4.0.6 小节(此处不重复数字)。
|
||||
* testTask 仅在启用板级测试套件时创建,未创建时句柄为 NULL(消费方须判空)。
|
||||
*/
|
||||
extern osThreadId_t defaultTaskHandle;
|
||||
extern osThreadId_t netTaskHandle;
|
||||
extern osThreadId_t ftpTaskHandle;
|
||||
extern osThreadId_t testTaskHandle;
|
||||
extern osThreadId_t uiTaskHandle;
|
||||
extern osThreadId_t timeSyncTaskHandle;
|
||||
extern osThreadId_t adcTaskHandle;
|
||||
extern osThreadId_t iec104TaskHandle;
|
||||
extern osThreadId_t protectTaskHandle;
|
||||
|
||||
extern const osThreadAttr_t defaultTask_attributes;
|
||||
extern const osThreadAttr_t netTask_attributes;
|
||||
extern const osThreadAttr_t ftpTask_attributes;
|
||||
extern const osThreadAttr_t testTask_attributes;
|
||||
extern const osThreadAttr_t uiTask_attributes;
|
||||
extern const osThreadAttr_t timeSyncTask_attributes;
|
||||
extern const osThreadAttr_t adcTask_attributes;
|
||||
extern const osThreadAttr_t iec104Task_attributes;
|
||||
extern const osThreadAttr_t protectTask_attributes;
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* TASK_HANDLES_H */
|
||||
+354
-8
@@ -6,11 +6,11 @@
|
||||
* 创建日期:2026-08-30
|
||||
* 修改记录:
|
||||
* v1.1 2026-09-14 王建锋 新增待机显示页初始化(ui_standby_init,空闲 60s 切入)
|
||||
* v1.2 2026-10-10 王建锋 Task #32 的 P2 测量埋点:主循环加常开探针
|
||||
* v1.2 2026-10-10 王建锋 任务耗时测量埋点:主循环加常开探针
|
||||
* (循环周期 / 单轮工作耗时 / ui_net_sink_poll 往返),
|
||||
* 用于把"心跳缺席"拆成 饿死 / 阻塞在往返 / 自身拖长
|
||||
* 三种互斥机理(判读见 docs/子系统设计/看门狗设计说明.md §11.4)
|
||||
* v1.3 2026-10-10 王建锋 Task #32 的 P1-4a(解耦心跳,看门狗设计说明 §11.3):把 task_wd_beat
|
||||
* v1.3 2026-10-10 王建锋 解耦心跳(docs/子系统设计/看门狗设计说明.md §11.3):把 task_wd_beat
|
||||
* (TASK_WD_UI) 提到 ui_net_sink_poll() **之前**。原写法心跳
|
||||
* 在推流往返之后 → 心跳周期 = 渲染 + 一次 netTask 往返 + 10ms,
|
||||
* 实测 uiw≈uis≈50ms(心跳被 netTask 往返主导),心跳实际测的是
|
||||
@@ -18,6 +18,20 @@
|
||||
* "UI 自己被调度到了"。注:若往返**永久**阻塞,本改仍不能救
|
||||
* (心跳只发一次)→ 真要彻底去掉依赖须走事件驱动 accept
|
||||
* (NET_EVENT_CONNECTED 回调),待复现快照确认阻塞机理后再定。
|
||||
* v1.4 2026-10-25 DTU 项目组 F3(大窗判读):向 ui_net_sink 注册心跳回调
|
||||
* (ui_task_sink_beat)——推流段内**每个可等待 net 往返**
|
||||
* (accept/recv/send/close/重建)返回点即喂本任务心跳,
|
||||
* 风暴期「单轮多个慢往返之和」不再串成 800ms 缺席链
|
||||
* (2026-10-25 实测 bit4 8 连击、uisn=154ms 仍在段内;
|
||||
* 按位连击判据 = docs/子系统设计/看门狗设计说明.md §5.3)。
|
||||
* 单个往返自身仍无上界——该上界与彻底解耦(事件驱动
|
||||
* accept)归登记簿「net 层跨上下文收口」(批 3),本票只做心跳续命。
|
||||
* v1.5 2026-10-26 DTU 项目组 道F task-3 装置维护三页的执行侧:新增
|
||||
* ui_data_maint_ops_t 六件适配(时钟=写+回读 RTC、
|
||||
* 复归=prot_elm_request_reset、清除=syslog_request_clear
|
||||
* /clear_state 直通)并在 ui_menu_init 之前注入;记录
|
||||
* reader 的 2 条 if 链改成 7 条映射表 s_log_map,并加
|
||||
* 编译期守卫把 UI_W_REC_LOG_* == syslog_id_t 枚举序钉死
|
||||
*/
|
||||
#include "ui_task.h"
|
||||
|
||||
@@ -30,7 +44,16 @@
|
||||
#include "ui_menu.h"
|
||||
#include "ui_net_sink.h"
|
||||
#include "ui_standby.h"
|
||||
#include "ui_w_records.h" /* task-6 记录 reader 注入点(UI 页面不再 include sys_log.h) */
|
||||
#include "ui_w_soe.h" /* task-15 SOE 记录 reader 注入点(同上:UI 不 include soe_store.h) */
|
||||
#include <stdio.h> /* task-15 snprintf:CP56 → UI 视图里的 char[20] 时标 */
|
||||
#include "ui_data.h" /* task-14 快照存侧(BSP):本任务只负责先复位再喂帧 */
|
||||
#include "ui_data_feed.h" /* task-14 数据装配节拍入口(App/app,UI 侧唯一发布者) */
|
||||
#include "sys_log.h"
|
||||
#include "soe_store.h" /* task-15 SOE 只读遍历(count/read);写侧仍归 protectTask/defaultTask */
|
||||
#include "ui_tokens.h" /* UI 设计令牌:循环周期 UI_TASK_TICK_MS / 待机阈值 UI_STANDBY_IDLE_MS */
|
||||
#include "sys_clock.h"
|
||||
#include "prot_elm.h" /* 道F task-3 装置维护:强制复归请求 + 动作态元件位图 */
|
||||
#include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */
|
||||
#include "net_latency_test.h" /* M3 探针:ui_loop(生产循环实测周期) */
|
||||
#include "task_watchdog.h"
|
||||
@@ -48,6 +71,302 @@ static void ui_task_time_cb(char *buf, size_t len) {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* UI 记录源编号 → syslog_id_t 映射表(道F task-6 起 7 类,task-3 装置维护「清除记录」
|
||||
* 要在 7 行上分行显示;表长 = UI_W_REC_LOG_N,两端映射表同源同长)
|
||||
*
|
||||
* ⚠ 顺序契约(Lead 裁定 2026-10-26):UI_W_REC_LOG_* 逐字等于 syslog_id_t 枚举序,
|
||||
* 只准尾部追加——枚举序就是 App/log 两端 s_paths[]/s_caps[] 的下标,中间插值会
|
||||
* 安静地把事故读成告警。下面的编译期守卫就是这条契约在本端的机械防线(记录侧
|
||||
* 已有 TC-SYSLOG-130/131/131b,UI 侧对应的一条在 test/host/ui_data TC-UIDATA-024,
|
||||
* 两端各钉一次,谁中间插号谁编译不过 / 单测红)。
|
||||
* 即便如此,适配层**始终显式逐项映射**、从不把 UI 编号强转成 syslog_id_t:
|
||||
* 等值是今天的事实,不是页面可以依赖的契约(UI_W_REC_LOG_* 对页面仍是不透明号)。
|
||||
*/
|
||||
static const syslog_id_t s_log_map[UI_W_REC_LOG_N] = {
|
||||
SYSLOG_RUN, SYSLOG_SELFTEST, SYSLOG_ACCIDENT, SYSLOG_ALARM,
|
||||
SYSLOG_START, SYSLOG_OPER, SYSLOG_YK
|
||||
};
|
||||
|
||||
#if defined(__STDC_VERSION__) && (__STDC_VERSION__ >= 199901L)
|
||||
typedef char ui_task_log_order_check_t[
|
||||
((UI_W_REC_LOG_RUN == (uint8_t)SYSLOG_RUN) &&
|
||||
(UI_W_REC_LOG_SELFTEST == (uint8_t)SYSLOG_SELFTEST) &&
|
||||
(UI_W_REC_LOG_ACCIDENT == (uint8_t)SYSLOG_ACCIDENT) &&
|
||||
(UI_W_REC_LOG_ALARM == (uint8_t)SYSLOG_ALARM) &&
|
||||
(UI_W_REC_LOG_START == (uint8_t)SYSLOG_START) &&
|
||||
(UI_W_REC_LOG_OPER == (uint8_t)SYSLOG_OPER) &&
|
||||
(UI_W_REC_LOG_YK == (uint8_t)SYSLOG_YK) &&
|
||||
(UI_W_REC_LOG_N == (uint8_t)SYSLOG_COUNT)) ? 1 : -1];
|
||||
#endif
|
||||
|
||||
/*
|
||||
* 函数功能:记录 reader 适配——UI 不透明编号 → syslog_id_t 后取条数
|
||||
* 入口参数:log_id - UI 记录源编号(UI_W_REC_LOG_*) uint8_t
|
||||
* 返回值:记录条数;未知编号返回 0(页面退化为空态,与合并前直调同形) uint16_t
|
||||
* 限定条件:uiTask 上下文同步调用(记录页与清除记录页的绘制路径,无 I/O 之外开销:
|
||||
* syslog_count 读的是文件长度/内存计数,不逐条读盘)
|
||||
*/
|
||||
static uint16_t ui_task_rec_count(uint8_t log_id) {
|
||||
if (log_id >= (uint8_t)UI_W_REC_LOG_N) {
|
||||
return 0u;
|
||||
}
|
||||
return (uint16_t)syslog_count(s_log_map[log_id]);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:记录 reader 适配——读记录字段直通 sys_log(错误码不改写)
|
||||
* 入口参数:log_id/idx/ts/ts_len/desc/desc_len - 契约见 ui_w_records.h ui_rec_reader_t
|
||||
* 返回值:0 成功;-1 编号/序号越界(未知编号同 -1);-2 读失败 int
|
||||
* 限定条件:同上
|
||||
*/
|
||||
static int ui_task_rec_get(uint8_t log_id, uint32_t idx, char *ts, uint32_t ts_len,
|
||||
char *desc, uint32_t desc_len) {
|
||||
if (log_id >= (uint8_t)UI_W_REC_LOG_N) {
|
||||
return -1;
|
||||
}
|
||||
return syslog_get_record(s_log_map[log_id], idx, ts, (size_t)ts_len, desc,
|
||||
(size_t)desc_len);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* ============================ 道F task-3:装置维护写通道(UI 注入执行侧)
|
||||
*
|
||||
* 为什么在这一层实现而不是在页面里:页面属 Drivers/BSP/UI,反调 App 侧的
|
||||
* sys_clock / prot_elm / sys_log 是分层方向的反例(layer-deps 拦),更关键的是
|
||||
* **模拟器根本不链 sys_clock**(simulator/dbg_log.h:4「模拟器无 sys_clock/RTC」)——
|
||||
* 页面里用一次就是"MCU 编得过、模拟器链接期爆"。写动作一律走 ui_data_maint_ops_t,
|
||||
* 两端各给实现(口径 docs/UI设计/UI_数据供给设计说明.md §6.2/§7.2)。
|
||||
*
|
||||
* uiTask 阻塞上界(IWDG 800ms 窗、本任务在 required_mask 内):本通道里只有
|
||||
* 时钟设置会碰硬件——sys_clock_set_unix 内含 1 次 SD2506 写,再叠加本适配层刻意
|
||||
* 加的回读复核 1 次读,各 SD2506_I2C_TIMEOUT_MS=100ms ⇒ 最坏 ≈200ms 落在一次
|
||||
* "保存"按键上(心跳在循环尾,仍远小于 800ms;对时任务同样在本机上做过这件事)。
|
||||
* 复归/清除两条只写内存标志或投递给别的任务,无 I/O。
|
||||
*/
|
||||
|
||||
/*
|
||||
* 函数功能:设置墙钟并**回读复核**(真机 = 写 RAM 基准 + 写 SD2506 RTC)
|
||||
* 入口参数:a_epoch - Unix 秒(本地时间域,与 sys_clock 同一口径,无时区偏移) uint32_t
|
||||
* 返回值:0 - 已生效且 RTC 回读一致;-1 - 越界 / 未生效 / RTC 回读不符 int
|
||||
* 限定条件:uiTask 上下文(页面"保存"按键直达,同步返回终态)
|
||||
* 函数说明:⚠ **sys_clock_set() 恒返 0**(App/util/sys_clock.c:462 起:RTC 写失败
|
||||
* 只打 DBG_ERROR,不改返回值),所以本函数不能拿它的返回值当结论——
|
||||
* 回读是唯一真判据;而只回读 sys_clock_get() 证明的仅是"RAM 基准换了",
|
||||
* 面板对时的用户意图是**掉电后还要是对的**,故必须回读 RTC 本身。
|
||||
* RTC 坏了但墙钟已改时本函数返 -1(屏上"保存行=ERR + 值列跟着新时间",
|
||||
* 两个事实各说各的,比笼统报 OK 诚实);秒位允许 ±3 与"分/时/日回绕"两种
|
||||
* 形态,因为两次读之间墙钟与 RTC 都在走。
|
||||
* 上限 UI_DATA_CLOCK_EPOCH_MAX(2038) 在 UI 层已挡,这里再挡一次:
|
||||
* 执行侧不信任调用侧(0 还兼作"未建钟"哨兵,面板永远不该发 0)
|
||||
*/
|
||||
static int ui_task_maint_clock_set(uint32_t a_epoch) {
|
||||
sd2506_time_t want;
|
||||
sd2506_time_t got;
|
||||
uint32_t now;
|
||||
int ds;
|
||||
|
||||
if ((a_epoch == 0u) || (a_epoch > (uint32_t)UI_DATA_CLOCK_EPOCH_MAX)) {
|
||||
return -1;
|
||||
}
|
||||
if (sys_clock_set_unix(a_epoch) != 0) {
|
||||
return -1; /* 今天到不了这条腿(sys_clock_set 恒 0),留着是因为它是契约 */
|
||||
}
|
||||
now = sys_clock_get();
|
||||
if ((now == UINT32_MAX) || ((now + 2u) < a_epoch) || (now > (a_epoch + 2u))) {
|
||||
return -1; /* RAM 基准没跟上:连"现在就对"都没成立 */
|
||||
}
|
||||
if (sys_clock_get_time(&want) != 0) {
|
||||
return -1;
|
||||
}
|
||||
if (sd2506_get_time(&got) != BSP_OK) {
|
||||
return -1; /* RTC 读不动:掉电保持这件事无法证明 */
|
||||
}
|
||||
if ((got.year != want.year) || (got.month != want.month) || (got.day != want.day) ||
|
||||
(got.hour != want.hour) || (got.minute != want.minute)) {
|
||||
return -1;
|
||||
}
|
||||
ds = (int)got.second - (int)want.second;
|
||||
if (ds < 0) {
|
||||
ds = -ds;
|
||||
}
|
||||
if ((ds > 3) &&
|
||||
!(((want.second >= 58u) && (got.second <= 3u)) ||
|
||||
((got.second >= 58u) && (want.second <= 3u)))) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:请求强制复归(跨上下文:protectTask 下一拍消费)
|
||||
* 入口参数:无
|
||||
* 返回值:0 - 已投递(prot_elm_request_reset 无拒绝分支) int
|
||||
* 限定条件:uiTask 上下文;不等待执行结果
|
||||
* 函数说明:只投递不阻塞(契约:UI 页面二次确认后调,页面立刻给"已请求复归")。
|
||||
* **不在这里投操作记录**:远端 104 YK11 那条已经在
|
||||
* App/protocol/iec104/iec104_pts.c 投过,在这里再投一次就会给每一次
|
||||
* 主站复归多记一条"面板复归",审计面直接失真。本地这条由页面投
|
||||
* (只有页面知道"这是人按的"),措辞也不同串:见 Drivers/BSP/UI/ui_page_reset.c
|
||||
*/
|
||||
static int ui_task_maint_reset_req(void) {
|
||||
prot_elm_request_reset();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取当前处于动作态的保护元件个数(复归页观测行 "保护动作 N" 的 N)
|
||||
* 入口参数:无
|
||||
* 返回值:位图中置 1 的位数(纯 RAM 读,无锁) uint32_t
|
||||
* 限定条件:uiTask 绘制路径每帧可直调
|
||||
* 函数说明:给个数而不是位图,是为了让 160x160 上一行放得下、也让"复归前 N>0 →
|
||||
* 复归后 N=0"这个台架判据肉眼可读;判据前提(产品构建处于未标定闭锁,
|
||||
* App/task/protect_task.c:98 停走判据 ⇒ 闭锁期 N 恒 0)已写进页面注释
|
||||
*/
|
||||
static uint32_t ui_task_maint_active_count(void) {
|
||||
uint32_t mask = prot_elm_active_mask();
|
||||
uint32_t n = 0u;
|
||||
|
||||
while (mask != 0u) {
|
||||
n += (mask & 0x1u);
|
||||
mask >>= 1;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:投一条本地操作记录(面板动作留痕)
|
||||
* 入口参数:a_desc - 描述串(ASCII,长度须 <= SYSLOG_DESC_MAX=28) const char *
|
||||
* 返回值:无
|
||||
* 限定条件:uiTask 上下文;只在动作真正被受理之后由页面调用
|
||||
* 函数说明:用 quiet 口(与 iec104_serv.c 对时留痕同一选择):uiTask 不属于深链
|
||||
* 任务,但 quiet 口的收/丢判据与 syslog_post 一致且不在调用链上摊开
|
||||
* 行缓冲,留痕这件事没理由选更贵的那个口。
|
||||
* 丢条不回报页面:syslog 侧有累计丢弃计数(syslog_post_quiet_dropped())
|
||||
* 可查;页面上"动作做了没"与"这条审计写没写"是两件事,不该混在一个
|
||||
* 态标里——把后者做成 ERR 会让人以为复归没做而再按一次
|
||||
*/
|
||||
static void ui_task_maint_oper_note(const char *a_desc) {
|
||||
if (a_desc == NULL) {
|
||||
return;
|
||||
}
|
||||
syslog_post_quiet(SYSLOG_OPER, a_desc);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:请求清除某一类记录(只投递,落地在 defaultTask 的 syslog_drain 尾部)
|
||||
* 入口参数:a_log_id - UI 记录源编号(UI_W_REC_LOG_*) uint8_t
|
||||
* 返回值:0 - 已受理(MCU 端随后通常是 SYSLOG_CLEAR_WAIT);-1 - 越界编号 int
|
||||
* 限定条件:uiTask 上下文
|
||||
* 函数说明:本层是 UI 编号 → syslog_id_t 的**唯一**映射点之一(与 s_log_map 同表),
|
||||
* 绝不在 UI 侧做 f_unlink / 也不调 MCU 专属的 syslog_clear_apply():
|
||||
* 记录卷的属主是 defaultTask,uiTask 去碰 FAT 既违反唯一写者,又给
|
||||
* IWDG required_mask 内的任务添一条无上界的存储路径(口径 App/log/README.md)
|
||||
*/
|
||||
static int ui_task_maint_clear_req(uint8_t a_log_id) {
|
||||
if (a_log_id >= (uint8_t)UI_W_REC_LOG_N) {
|
||||
return -1;
|
||||
}
|
||||
return (syslog_request_clear(s_log_map[a_log_id]) == 0) ? 0 : -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取上条清除请求的状态(值直通,不改写、不翻译)
|
||||
* 入口参数:a_log_id - UI 记录源编号 uint8_t
|
||||
* 返回值:UI_DATA_CLEAR_ST_NONE/_OK/_WAIT/_FAIL;越界编号按 NONE int
|
||||
* 限定条件:uiTask 绘制路径每帧可直调(纯内存状态读)
|
||||
* 函数说明:SYSLOG_CLEAR_* 与 UI_DATA_CLEAR_ST_* 逐值相等(0/1/-1/-2),由
|
||||
* test/host/ui_data TC-UIDATA-024 钉住;这里保持直通,是为了让页面
|
||||
* 显示的就是日志侧的事实,而不是"适配层以为的事实"
|
||||
*/
|
||||
static int ui_task_maint_clear_state(uint8_t a_log_id) {
|
||||
if (a_log_id >= (uint8_t)UI_W_REC_LOG_N) {
|
||||
return UI_DATA_CLEAR_ST_NONE;
|
||||
}
|
||||
return syslog_clear_state(s_log_map[a_log_id]);
|
||||
}
|
||||
|
||||
/* 维护通道函数表(六件齐全;ui_data_maint_bind 拒绝残表,缺件就是装配错) */
|
||||
static const ui_data_maint_ops_t s_maint_ops = {
|
||||
ui_task_maint_clock_set,
|
||||
ui_task_maint_reset_req,
|
||||
ui_task_maint_active_count,
|
||||
ui_task_maint_oper_note,
|
||||
ui_task_maint_clear_req,
|
||||
ui_task_maint_clear_state
|
||||
};
|
||||
|
||||
static const ui_rec_reader_t s_rec_reader = { ui_task_rec_count, ui_task_rec_get };
|
||||
|
||||
/*
|
||||
* 函数功能:SOE 记录条数适配(道E task-15 reader 注入)——读落盘文件的完整槽数
|
||||
* 入口参数:无
|
||||
* 返回值:条数;超过 uint16 上限时钳到 65535(组件契约是 uint16_t) uint16_t
|
||||
* 限定条件:uiTask 上下文同步调用(SOE 记录页绘制路径)
|
||||
*/
|
||||
static uint16_t ui_task_soe_count(void) {
|
||||
uint32_t n = soe_store_count();
|
||||
|
||||
return (n > 65535u) ? 65535u : (uint16_t)n;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读第 idx 条 SOE 记录并翻成 UI 视图(二进制 → 不透明显示字段)
|
||||
* 入口参数:idx - 记录序号 uint32_t
|
||||
* p_out - UI 侧记录视图(契约见 Drivers/BSP/UI/ui_w_soe.h) ui_w_soe_rec_t *
|
||||
* 返回值:0 - 成功;-1 - 读失败 / 卷忙 / 校验和不过(页面这行退化显示,不猜) int
|
||||
* 限定条件:uiTask 绘制路径;soe_store_read() 是 try 锁非阻塞,绝不拖住 UI 节拍
|
||||
* 函数说明:时标**取记录内的 CP56**(事件自己的时间),不用"现在"换算。两条口径
|
||||
* 叠起来正好与文本记录页一致,别"顺手补 8 小时":
|
||||
* ① sys_clock 的 epoch 域本身就是本地时间(App/util/sys_clock.h 的
|
||||
* sys_clock_set_unix 注明"Unix epoch time (秒,本地时间)");
|
||||
* ② App/log/soe_rec.c 的 epoch→CP56 分解不加任何时区偏移。
|
||||
* 所以这里打出来的墙上时钟 == 运行/自检记录页里 sys_clock_get_str() 的口径;
|
||||
* 哪天真要引入显式时区,两侧必须一起改(跨模块陷阱,注释即防线)。
|
||||
* 时钟无效(IV)不写在这里:它是显示语义,由 UI 侧按 SOE_FLG_IV 决定
|
||||
* (组件把那行名字打"--"、值打"时钟无效"),App 只交代原始记录。
|
||||
*/
|
||||
static int ui_task_soe_get(uint32_t idx, ui_w_soe_rec_t *p_out) {
|
||||
soe_rec_t rec;
|
||||
uint32_t sec;
|
||||
|
||||
if ((p_out == NULL) || (soe_store_read(idx, &rec) != 0)) {
|
||||
return -1;
|
||||
}
|
||||
/* CP56 字节表见 App/log/soe_rec.c 的 s_soe_epoch_to_cp56:
|
||||
* [0..1]=分内毫秒(秒域时基 → 恒为秒×1000,亚秒不造假)[2]=分 [3]=时
|
||||
* [4]=日 [5]=月 [6]=年-2000 */
|
||||
sec = (((uint32_t)rec.cp56[1] << 8) | (uint32_t)rec.cp56[0]) / 1000u;
|
||||
(void)snprintf(p_out->ts, sizeof(p_out->ts), "%04u-%02u-%02u %02u:%02u:%02u",
|
||||
(unsigned)(2000u + ((uint32_t)rec.cp56[6] & 0x7Fu)),
|
||||
(unsigned)((uint32_t)rec.cp56[5] & 0x0Fu),
|
||||
(unsigned)((uint32_t)rec.cp56[4] & 0x1Fu),
|
||||
(unsigned)((uint32_t)rec.cp56[3] & 0x1Fu),
|
||||
(unsigned)((uint32_t)rec.cp56[2] & 0x3Fu),
|
||||
(unsigned)sec);
|
||||
p_out->type = rec.type;
|
||||
p_out->elm = rec.elm_id;
|
||||
p_out->flags = rec.flags;
|
||||
p_out->seq = rec.seq;
|
||||
p_out->value = rec.value;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static const ui_soe_reader_t s_soe_reader = { ui_task_soe_count, ui_task_soe_get };
|
||||
|
||||
/*
|
||||
* 函数功能:推流往返边界的心跳续命钩(F3,注册给 ui_net_sink)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:uiTask 轮询上下文(sink 在往返返回处调用),只做置位、不阻塞
|
||||
* 函数说明:为何经回调注入而非 sink 直调 task_wd_beat()——分层边界(BSP 不得
|
||||
* include App 头,layer-deps 门禁);心跳属主仍是本任务。
|
||||
*/
|
||||
static void ui_task_sink_beat(void) {
|
||||
task_wd_beat(TASK_WD_UI);
|
||||
}
|
||||
|
||||
#define DBG_TAG "[UI_TASK]"
|
||||
#include "dbg_log.h"
|
||||
|
||||
@@ -102,13 +421,30 @@ void ui_task_func(void *argument) {
|
||||
for (;;) { osDelay(1000); }
|
||||
}
|
||||
DBG_INFO("UI port init: OK");
|
||||
/* 记录 reader 注入:必须在 ui_menu_init 之前(记录页在此前不可能被打开,
|
||||
* 但契约把"启动期先绑定"写死;未绑定窗口组件按空态防御——见 ui_w_records.h) */
|
||||
ui_w_records_bind(&s_rec_reader);
|
||||
ui_w_soe_bind(&s_soe_reader); /* task-15:SOE 记录页数据入口(未绑定按"无记录"空态) */
|
||||
/* 装置维护写通道注入(道F task-3:时钟设置 / 强制复归 / 清除记录):同样必须先于
|
||||
* ui_menu_init。未注入的窗口里六个入口一律安全返回(页面显 ERR 或空态),
|
||||
* 既不崩也不假成功;但残表是真故障,必须能在启动日志里看见,不能静默 */
|
||||
if (ui_data_maint_bind(&s_maint_ops) != 0) {
|
||||
DBG_ERROR("UI maint bind: FAIL");
|
||||
}
|
||||
/* 快照层复位:必须先于 ui_menu_init(首帧之前任何页面被打开都按"无数据"
|
||||
* 渲染 "--",不会读到上电残留)——UI_数据供给设计说明.md §5.3 */
|
||||
ui_data_init();
|
||||
/* 首帧立即发布:待机屏/菜单屏在启动早期就可能被远端推流打开,等 200 ms
|
||||
* 会闪一下 "--"(同文档 §5.3);本调用在 ui_menu_init 之前,页面尚未建立,
|
||||
* 发布路径不触 LVGL,安全 */
|
||||
ui_data_feed_tick();
|
||||
DBG_INFO("UI task: ui_menu_init enter");
|
||||
if (ui_menu_init() != 0) {
|
||||
DBG_ERROR("UI menu init: FAIL");
|
||||
for (;;) { osDelay(1000); }
|
||||
}
|
||||
DBG_INFO("UI menu init: OK");
|
||||
if (ui_standby_init(60000u, ui_task_time_cb) != 0) {
|
||||
if (ui_standby_init(UI_STANDBY_IDLE_MS, ui_task_time_cb) != 0) {
|
||||
DBG_ERROR("UI standby init: FAIL");
|
||||
} else {
|
||||
DBG_INFO("UI standby: ready (idle=60s)");
|
||||
@@ -118,6 +454,9 @@ void ui_task_func(void *argument) {
|
||||
} else {
|
||||
DBG_INFO("UI net sink: started (port %u)", (unsigned)UI_NET_SINK_PORT);
|
||||
}
|
||||
/* F3:往返边界心跳钩(契约见 ui_net_sink.h;须在首轮 poll 之前注册)。
|
||||
* start 失败也要注:失败路径的 close/重建重试同样经往返 */
|
||||
ui_net_sink_set_beat_cb(ui_task_sink_beat);
|
||||
|
||||
#if defined(TEST_LATENCY_PROBE)
|
||||
/* M3 探针 ui_loop:主循环实测周期(= LVGL 渲染 + 推流轮询 + osDelay(10))。
|
||||
@@ -127,7 +466,7 @@ void ui_task_func(void *argument) {
|
||||
uint8_t tm_loop_valid = 0U;
|
||||
#endif
|
||||
|
||||
/* P2 常开探针(Task #32):TEST_LATENCY_PROBE 只在 CH395F 套件构建下打开,
|
||||
/* 常开探针:TEST_LATENCY_PROBE 只在 CH395F 套件构建下打开,
|
||||
* 且样本消费者是 testTask(产品构建里不跑)→ 产品构建拿不到任何 UI 周期数据。
|
||||
* 故此处独立埋点,恒定生效、只写本模块的静态计数,由 defaultTask 周期上报。 */
|
||||
|
||||
@@ -149,13 +488,20 @@ void ui_task_func(void *argument) {
|
||||
lat_mark(&tm_loop);
|
||||
tm_loop_valid = 1U;
|
||||
#endif
|
||||
ui_port_step(10);
|
||||
/* 心跳**先于**推流往返发出(P1-4a,看门狗设计说明 §11.3):UI 心跳测的是
|
||||
/* 数据装配节拍(200 ms 内部自分频,未到期时只比一次 tick):
|
||||
* 排在渲染之前,本轮绘制的就是本轮刚发布的那一帧
|
||||
* (UI_数据供给设计说明.md §5.1 单写者同上下文的前提就在这儿) */
|
||||
ui_data_feed_tick();
|
||||
ui_port_step(UI_TASK_TICK_MS);
|
||||
/* 心跳**先于**推流往返发出(判读口径 docs/子系统设计/看门狗设计说明.md §11.3):UI 心跳测的是
|
||||
* "UI 自身被调度到了",而非"netTask 回执快慢"。原写法心跳在 sink 之后,
|
||||
* 致实测 uiw≈uis≈50ms(心跳被 netTask 往返主导)。提到往返之前后,
|
||||
* 即使往返偶发变慢,心跳也不被其拖垮。
|
||||
* ⚠ 若往返**永久**阻塞(xTaskNotifyWait portMAX_DELAY 不返回),本改只能发
|
||||
* 一次心跳,救不了 → 彻底解耦须走事件驱动 accept,见文件头 v1.3 说明。 */
|
||||
* 一次心跳,救不了 → 彻底解耦须走事件驱动 accept,见文件头修改记录 v1.3。
|
||||
* v1.4 补充:sink 内每个往返**返回点**已各自补喂(set_beat_cb 注入),
|
||||
* 「单轮多个慢往返之和」不再构成缺席链;剩单往返 >800ms 才报——那是
|
||||
* netTask 自身停摆,按位判据抓对了人(上界归「net 层跨上下文收口」)。 */
|
||||
task_wd_beat(TASK_WD_UI);
|
||||
/* 推流轮询段单独 enter/exit:它内部经 net_accept → net_wait_result 走到
|
||||
* xTaskNotifyWait(portMAX_DELAY) 的**无限期阻塞**。若 uiTask 永久卡在这里,
|
||||
@@ -167,6 +513,6 @@ void ui_task_func(void *argument) {
|
||||
/* 单轮工作耗时(不含 osDelay):与循环周期对比即得"调度延迟" */
|
||||
task_prof_sample(TASK_PROF_UI_WORK, task_prof_cyc() - prof_t0);
|
||||
|
||||
osDelay(10);
|
||||
osDelay(UI_TASK_TICK_MS);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
> 两模块**零调用者**且未编入任何构建——CRC 从未被链接使用:名义上的关联点 `net_cfg_legacy.c`
|
||||
> (旧档 `/cfg/netcfg.bin` 迁移,**该文件亦已于 2026-10-10 删除**)自带静态 `s_crc16_modbus()`,
|
||||
> 从不调用本模块;环形缓冲由 `App/log/console_log.c` 内联实现(`s_con_ring`)。
|
||||
> 保留只会造成"有统一实现"的误导,故整体移除(详见 `docs/任务规划/排期详述.md` E-15(C5))。
|
||||
> 保留只会造成"有统一实现"的误导,故整体移除。
|
||||
|
||||
## 设计要点
|
||||
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-11
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-11 王建锋 创建初始版本(N-19 FTP 鉴权;标准向量由
|
||||
* v1.0 2026-10-11 王建锋 创建初始版本(FTP 鉴权;标准向量由
|
||||
* test/host/sha256 主机单测钉死)
|
||||
*/
|
||||
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-11
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-11 王建锋 创建初始版本(N-19 FTP 鉴权:[ftp] pwd_hash 校验)
|
||||
* v1.0 2026-10-11 王建锋 创建初始版本(FTP 鉴权:[ftp] pwd_hash 校验)
|
||||
*/
|
||||
|
||||
#ifndef SHA256_H
|
||||
|
||||
@@ -77,7 +77,7 @@ static volatile uint32_t s_base_unix_secs = UINT32_MAX;
|
||||
* 修改记录:
|
||||
* 2026-10-02 修正:原式错用 (wy%100 + wy/100/4 - wy/100/15)(缺 K/4、
|
||||
* 5J/世纪项,且把 1-based 月当 2-based 用),实测 1970~2099
|
||||
* 全部日期星期皆错(主机侧单测 TC-CLK-014/019 发现)。
|
||||
* 全部日期星期皆错(主机侧单测 TC-SYSCLK-014/019 发现)。
|
||||
*/
|
||||
static uint8_t rtc_calc_week(uint16_t y, uint8_t m, uint8_t d) {
|
||||
uint32_t wy = y;
|
||||
@@ -206,7 +206,7 @@ int sys_clock_init(void) {
|
||||
* 驱动内部已 +2000)。此处【禁止】再 +2000,否则年份虚增到 4026,
|
||||
* 换算出的 Unix 秒数远超 uint32_t 上限被截断回绕,时间彻底错乱。
|
||||
* 返回值必须检查:I2C 失败时字段为垃圾,不得用于建立基准。 */
|
||||
rtc_ok = (sd2506_get_time(&sd_time) == SD2506_OK) ? 1 : 0;
|
||||
rtc_ok = (sd2506_get_time(&sd_time) == BSP_OK) ? 1 : 0;
|
||||
|
||||
/* ============================================================
|
||||
* Step 1.1:开发期校时(可选,由 RTC_DEFAULT_INIT_* 宏控制)
|
||||
@@ -235,11 +235,11 @@ int sys_clock_init(void) {
|
||||
init_t.week = rtc_calc_week(init_t.year, init_t.month, init_t.day);
|
||||
|
||||
/* 写回 SD2506:断电后该时间被保留,下次上电即为有效值 */
|
||||
if (sd2506_set_time(&init_t) == SD2506_OK) {
|
||||
if (sd2506_set_time(&init_t) == BSP_OK) {
|
||||
DBG_INFO("sys_clock_init: RTC set %04d-%02d-%02d %02d:%02d:%02d",
|
||||
init_t.year, init_t.month, init_t.day,
|
||||
init_t.hour, init_t.minute, init_t.second);
|
||||
rtc_ok = (sd2506_get_time(&sd_time) == SD2506_OK) ? 1 : 0; /* 重新读取校准后的值(同样检查返回值) */
|
||||
rtc_ok = (sd2506_get_time(&sd_time) == BSP_OK) ? 1 : 0; /* 重新读取校准后的值(同样检查返回值) */
|
||||
} else {
|
||||
DBG_ERROR("sys_clock_init: RTC default write FAIL");
|
||||
}
|
||||
@@ -460,7 +460,7 @@ int sys_clock_set(uint32_t unix_secs, const sd2506_time_t *sd_time) {
|
||||
|
||||
/* Step 2: 写回 SD2506 RTC(若提供了 sd_time) */
|
||||
if (sd_time != NULL) {
|
||||
if (sd2506_set_time(sd_time) == SD2506_OK) {
|
||||
if (sd2506_set_time(sd_time) == BSP_OK) {
|
||||
DBG_INFO("sys_clock_set: SD2506 RTC written");
|
||||
} else {
|
||||
DBG_ERROR("sys_clock_set: SD2506 RTC write FAIL");
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-10 王建锋 创建(Task #32 的 P2 测量埋点)
|
||||
* v1.0 2026-10-10 王建锋 创建:常开任务耗时探针
|
||||
*
|
||||
* 设计依据:docs/子系统设计/看门狗设计说明.md §11.4(探针字段语义与实测基线)
|
||||
*/
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-10 王建锋 创建(Task #32 的 P2 测量埋点)
|
||||
* v1.0 2026-10-10 王建锋 创建:常开任务耗时探针
|
||||
*
|
||||
* 设计背景(docs/子系统设计/看门狗设计说明.md §11.4):
|
||||
* uiTask 偶发心跳缺席导致 IWDG 复位,但"占用者"无法从既有日志判定。
|
||||
|
||||
+115
-47
@@ -8,26 +8,34 @@
|
||||
* 创建日期:2026-09-29
|
||||
* 修改记录:
|
||||
* v1.0 2026-09-29 王建锋 创建:心跳位图 + 抽样判定 + IWDG 延后启动
|
||||
* v1.1 2026-10-02 王建锋 round 7 纯取证埋点:近 8 次抽样位图环 + 最大喂狗
|
||||
* v1.1 2026-10-02 王建锋 第 7 轮纯取证埋点:近 8 次抽样位图环 + 最大喂狗
|
||||
* 间隔统计 + 停喂现场落 /log/wdev.log(掉电不丢);
|
||||
* 全部在 TASK_WATCHDOG_UNIT_TEST 守卫内,判定状态机
|
||||
* 与主机侧单测逐字不变(见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)
|
||||
* v1.2 2026-10-02 王建锋 round 8 取证:复位不丢的"进段打点"(.noinit 槽 +
|
||||
* PREVRUN 开机上报 + STALL 行附 seg)——round 7 结论为
|
||||
* v1.2 2026-10-02 王建锋 第 8 轮取证:复位不丢的"进段打点"(.noinit 槽 +
|
||||
* PREVRUN 开机上报 + STALL 行附 seg)——第 7 轮结论为
|
||||
* "poller 自身停摆",需定位 defaultTask 主循环阻塞点
|
||||
* v1.3 2026-10-02 王建锋 round 8b 取证:第二个 .noinit 槽"时间源存活性"
|
||||
* v1.3 2026-10-02 王建锋 第 8b 轮取证:第二个 .noinit 槽"时间源存活性"
|
||||
* (TIM7 ISR 每 1ms 写 uwTick/FreeRTOS tick/计数)+
|
||||
* TICKRUN 开机上报(含相对进段时刻的增量 duw/dfr)——
|
||||
* round 8a 定位到 console 通道后,需钉死是 TIM7(HAL tick)
|
||||
* 第 8a 轮定位到 console 通道后,需钉死是 TIM7(HAL tick)
|
||||
* 还是 SysTick(FreeRTOS tick) 停摆(见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)
|
||||
*
|
||||
* v1.5 2026-10-25 DTU 项目组 **停喂判定按位分账**(大窗判读 F2):stall/停喂的唯一
|
||||
* 判据从"复合连续缺席窗数"改为"任一必需位的连续缺席窗数"
|
||||
* (同一任务 800ms 没跑过才停喂申报复位);复合缺席窗仍照打
|
||||
* "MISS start"(降级为诊断行,不再代表停喂)——根治"本窗缺 A、
|
||||
* 下窗缺 B"式互不相干亚秒抖动被累计成复合 800ms 假停摆并停喂
|
||||
* (2026-10-25 大窗 16:24/16:28 两起实证;真单任务停摆如
|
||||
* 16:39 型 UI 单串 800ms+ 仍必报)。判据正文同步
|
||||
* docs/子系统设计/看门狗设计说明.md §4;主机单测 TC-WATCHDOG-017/018
|
||||
* v1.4 2026-10-02 王建锋 **P0 修复**:wd_platform_start() 补写 IWDG 的
|
||||
* PR/RLR(= WD_IWDG_PRESCALER / WD_IWDG_RELOAD,即设计值
|
||||
* /64 + 1249 → 1701~2498ms)。此前为"延后启动"删掉
|
||||
* HAL_IWDG_Init() 时,一并丢掉了**唯一**写这两只寄存器的
|
||||
* 路径 → 实机停在复位默认值 PR=0/RLR=4095(≈349~512ms),
|
||||
* 比设计短约 5 倍 → 800ms 容忍分支成死代码、任何 >400ms
|
||||
* 心跳缺口直接 IWDG 误复位(round 3~8 全部"空 STOR 必
|
||||
* 心跳缺口直接 IWDG 误复位(第 3~8 轮全部"空 STOR 必
|
||||
* 复位"的共同根因)。SWD 现场改回设计值后同一用例 100% 通过
|
||||
*
|
||||
* 设计依据:docs/子系统设计/看门狗设计说明.md
|
||||
@@ -56,8 +64,18 @@
|
||||
#include "FreeRTOS.h" /* xTaskGetCurrentTaskHandle / TaskHandle_t(FTL 心跳路由) */
|
||||
#include "task.h"
|
||||
#include "lftpd.h" /* lftpd_task_wd_beat() 弱钩子声明(本文件提供强符号,见下方) */
|
||||
#include "sys_log.h" /* SYSLOG_RUN(flush 诱导落盘)+ sys_log_evidence()(round 7 取证落盘) */
|
||||
#include "sys_log.h" /* SYSLOG_RUN(flush 诱导落盘)+ sys_log_evidence()(第 7 轮取证落盘) */
|
||||
#include "console_log.h" /* dbg_console_tx_fault()(僵尸通道计数) */
|
||||
/* 测试构建可见性(2026-10-25 台架实测缺陷):wd_required_mask() 里那条
|
||||
* ENABLE_HW_LAYER_TESTS 分支要按"硬件层独占测试构建"剔除 netTask,而该宏定义在
|
||||
* test/board/ch395f_test.h(由 test_config.h 拉入)。本文件此前没 include 它 →
|
||||
* 宏在本 TU 永远不可见 → **那条分支自加入起就是死代码**,HW 层测试构建仍按产品
|
||||
* 掩码 0x1F7 要求 netTask 供位,而 netTask 按设计自挂起 → 每 ~110ms 刷一条
|
||||
* [ERR][WD] task stall(台架一轮刷出 468 条)。做法与 App/task/net_task.c 的
|
||||
* #include "test_config.h" 一致;产品构建 TEST_SUITE_* 全注释、宏未定义,
|
||||
* 走的仍是原分支(实测产品固件 Code/RO/RW/ZI 逐项不变)。
|
||||
* 证据:boot log 20261025_050539(req=0x1F7 而 mask 最高只到 0x1F5)。 */
|
||||
#include "test_config.h"
|
||||
#endif
|
||||
|
||||
#define DBG_TAG "[WD]"
|
||||
@@ -101,14 +119,22 @@
|
||||
* 单字读-改-写在 Cortex-M4 上天然原子,无需临界区(§3.3) */
|
||||
static volatile uint32_t s_hb_bits;
|
||||
|
||||
static uint32_t s_miss_streak; /* 连续未达标轮数(达到容忍上限即停喂) */
|
||||
static uint32_t s_miss_streak; /* 复合缺席连续窗数(饱和封顶)——自 v1.5 起仅作
|
||||
* "MISS start" 诊断行的 episode 边沿检测,不再
|
||||
* 参与停喂判定(停喂按 s_miss_bits 逐位分账) */
|
||||
/* 按位分账(2026-10-25 大窗判读 F2):每个必需位"连续缺席抽样窗数"。位出现即清零,
|
||||
* 缺席饱和到容忍值。停喂/stall 声明的唯一判据 = 任一必需位连击 >= WD_MISS_TOLERANCE,
|
||||
* 即"同一个任务连续 8 个窗没跑过"。多任务各缺各的(本窗 A、下窗 B)各自清零,
|
||||
* 不再串成复合 800ms 停喂——旧复合判据把互不相干的亚秒抖动累计成真停摆申报
|
||||
* (2026-10-25 大窗 16:24/16:28 两起实证;判据正文 docs/子系统设计/看门狗设计说明.md §4)。 */
|
||||
static uint8_t s_miss_bits[TASK_WD_ID_MAX];
|
||||
static uint32_t s_miss_total; /* 累计未达标轮数(观测用) */
|
||||
static uint32_t s_last_mask; /* 上次抽样到的位图(定位卡死任务用) */
|
||||
static uint32_t s_started; /* 1 = IWDG 已启动 */
|
||||
static uint32_t s_last_tick; /* 上次抽样时刻(100ms 节流基准) */
|
||||
static uint32_t s_stall_evidenced; /* 1 = 本停摆 episode 已落盘证据(门闩;见 task_wd_poll) */
|
||||
|
||||
/* ==== round 7 纯取证状态(仅 MCU 构建;判定状态机逐字不变)====
|
||||
/* ==== 第 7 轮纯取证状态(仅 MCU 构建;判定状态机逐字不变)====
|
||||
* 设计背景见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录:误复位后 RAM 态随复位消失,
|
||||
* 故把"停喂现场"快照落盘(/log/wdev.log)供复位后回读定性。 */
|
||||
#ifndef TASK_WATCHDOG_UNIT_TEST
|
||||
@@ -119,7 +145,7 @@ static uint32_t s_last_feed_tick; /* 上次喂狗时刻(ms) *
|
||||
static uint32_t s_max_feed_gap; /* 观测到的最大喂狗间隔(ms) */
|
||||
static uint32_t s_feed_seen; /* 1 = 已记录首个喂狗时刻 */
|
||||
|
||||
/* ==== round 8 取证:复位不丢的"进段打点"槽(.noinit → SRAM 保留)====
|
||||
/* ==== 第 8 轮取证:复位不丢的"进段打点"槽(.noinit → SRAM 保留)====
|
||||
* IWDG/引脚复位不改变 SRAM 内容,故复位后仍能读出"上一轮 defaultTask 主循环
|
||||
* 最后走到哪一段"。magic 用于区分"本模块写过"与上电时的随机值。
|
||||
* 布局:[0]=magic [1]=段号 [2]=进段时刻(ms) [3]=循环序号
|
||||
@@ -129,7 +155,7 @@ static uint32_t s_feed_seen; /* 1 = 已记录首个喂狗时
|
||||
#define WD_SEG_MAGIC 0x57445347u /* 'WDSG' */
|
||||
__attribute__((section(".noinit"), zero_init)) volatile uint32_t s_wd_seg_trail[4];
|
||||
|
||||
/* ==== round 8b 取证:复位不丢的"时间源存活性"槽(.noinit → SRAM 保留)====
|
||||
/* ==== 第 8b 轮取证:复位不丢的"时间源存活性"槽(.noinit → SRAM 保留)====
|
||||
* 两个槽(均在 UNINIT_CCM,复位不清):
|
||||
* s_wd_tick_cur [0]=magic [1]=uwTick(最新) [2]=本轮 TIM7 中断计数 [3]=保留
|
||||
* —— TIM7 ISR 每 1ms 更新("本轮活动值")
|
||||
@@ -212,7 +238,7 @@ static uint32_t wd_required_mask(void) {
|
||||
|
||||
#ifndef TASK_WATCHDOG_UNIT_TEST
|
||||
/*
|
||||
* 函数功能:记录喂狗时刻并累计最大喂狗间隔(round 7 纯取证)
|
||||
* 函数功能:记录喂狗时刻并累计最大喂狗间隔(第 7 轮纯取证)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:由 wd_platform_feed() 内调用(任意喂狗路径均经过)
|
||||
@@ -234,7 +260,7 @@ static void wd_feed_track(void) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:把停喂现场落盘到 /log/wdev.log(round 7 纯取证)
|
||||
* 函数功能:把停喂现场落盘到 /log/wdev.log(第 7 轮纯取证)
|
||||
* 入口参数:snapshot - 本次抽样到的位图 uint32_t
|
||||
* 返回值:无
|
||||
* 限定条件:task_wd_poll 停喂分支(defaultTask 上下文)
|
||||
@@ -266,20 +292,25 @@ static void wd_stall_evidence(uint32_t snapshot) {
|
||||
/* 裸 UART 直发(不经环形缓冲/不依赖 defaultTask 排空):停喂窗口里
|
||||
* 环形缓冲的排空本身可能停摆,裸发是本行证据的可靠通道(与盘上
|
||||
* 证据互补——卷锁被阻塞方持有时盘上通道会失败,见 §0.5.4 判据缺口) */
|
||||
/* 整行独占(道G task-26):本行两条裸发(正文 + CRLF),段间原本是缝;这行是
|
||||
* 停喂判据的核心证据(盘上通道在卷锁被阻塞方持有时会失败,串口是唯一保底)。
|
||||
* begin 非阻塞:拿不到也照发,停喂现场绝不能因为抢锁而丢。 */
|
||||
(void)dbg_console_tx_begin();
|
||||
dbg_console_write_raw(line, (unsigned)strlen(line));
|
||||
dbg_console_write_raw("\r\n", 2u);
|
||||
dbg_console_tx_end();
|
||||
(void)sys_log_evidence(line);
|
||||
}
|
||||
}
|
||||
|
||||
/* ==================== round 8 取证:复位不丢的"进段打点" ====================
|
||||
* 见 task_watchdog.h 的说明(round 7 初判"poller 自身停摆"经 round 8b 更正:
|
||||
/* ==================== 第 8 轮取证:复位不丢的"进段打点" ====================
|
||||
* 见 task_watchdog.h 的说明(第 7 轮初判"poller 自身停摆"经第 8b 轮更正:
|
||||
* 真因是 IWDG 超时被静默配成复位默认值,见 docs/子系统设计/看门狗设计说明.md
|
||||
* 2026-10-02 变更记录)→ 需定位 defaultTask 主循环冻结在哪个调用点。
|
||||
* 槽放 .noinit(scatter UNINIT_CCM):IWDG/引脚复位不改变 SRAM,复位后仍可读。 */
|
||||
|
||||
/*
|
||||
* 函数功能:记录"即将进入的主循环段"到复位不丢的 .noinit 槽(round 8 取证)
|
||||
* 函数功能:记录"即将进入的主循环段"到复位不丢的 .noinit 槽(第 8 轮取证)
|
||||
* 入口参数:seg - 段号(task_wd_seg_t) uint32_t
|
||||
* 返回值:无
|
||||
* 限定条件:defaultTask 主循环调用(任意上下文亦可,纯 SRAM 写)
|
||||
@@ -294,7 +325,7 @@ void task_wd_seg(uint32_t seg) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:开机快照上一轮的"时间源存活性"(round 8b 取证)
|
||||
* 函数功能:开机快照上一轮的"时间源存活性"(第 8b 轮取证)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:**必须在 HAL_Init() 之前调用**(main() 的 USER CODE BEGIN 1)
|
||||
@@ -320,7 +351,7 @@ void task_wd_tick_snapshot(void) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:记录"时间源存活性"到复位不丢的 .noinit 槽(round 8b 取证)
|
||||
* 函数功能:记录"时间源存活性"到复位不丢的 .noinit 槽(第 8b 轮取证)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:TIM7 中断上下文调用(HAL_TIM_PeriodElapsedCallback 内,每 1ms)
|
||||
@@ -337,13 +368,13 @@ void task_wd_tick_probe(void) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:开机上报上一轮最后进段 + 时间源存活性(round 8 取证)
|
||||
* 函数功能:开机上报上一轮最后进段 + 时间源存活性(第 8 轮取证)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:启动早期调用(内核启动前亦可,裸 UART 直发)
|
||||
* 函数说明:magic 命中才打一行 `PREVRUN seg=<段> seq=<序号> t=<ms>`(裸 UART,
|
||||
* 不依赖环形缓冲写者),随后清 magic(一次复位只报一次)。
|
||||
* round 8b:紧接着打 `TICKRUN uw= n= duw=`——uw 为 TIM7 最后一次记下的
|
||||
* 第 8b 轮:紧接着打 `TICKRUN uw= n= duw=`——uw 为 TIM7 最后一次记下的
|
||||
* HAL tick、n 为 TIM7 中断计数、duw = uw - t(时间源是否活到复位)。
|
||||
*/
|
||||
void task_wd_seg_report(void) {
|
||||
@@ -365,7 +396,7 @@ void task_wd_seg_report(void) {
|
||||
}
|
||||
s_wd_seg_trail[0] = 0u; /* 一次复位只报一次 */
|
||||
|
||||
/* round 8b:时间源存活性(取自开机快照 prev,增量相对上一行的 t = 最后进段时刻)。
|
||||
/* 第 8b 轮:时间源存活性(取自开机快照 prev,增量相对上一行的 t = 最后进段时刻)。
|
||||
* duw≈0 → TIM7 与 defaultTask 同刻停摆(HAL 时基停摆);duw≈2000+ → TIM7 活到
|
||||
* 复位(冻结另有阻塞点);n = 上一轮 TIM7 中断总数(≈该轮存活毫秒数)。 */
|
||||
if (s_wd_tick_prev[0] == WD_TICK_MAGIC) {
|
||||
@@ -429,15 +460,22 @@ void task_wd_start(void) {
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:仅 defaultTask 主循环调用(唯一喂狗者);内部节流到 100ms 一次
|
||||
* 函数说明:取走并清零位图 → 与必需掩码比较:
|
||||
* 全中 → 清零缺失计数并喂狗;
|
||||
* 有缺 → 累计;连续达到 WD_MISS_TOLERANCE 次后**停止喂狗**,
|
||||
* 让 IWDG 自然溢出复位(这样 RCC_CSR 记录的是权威的
|
||||
* IWDGRSTF,而不是 NVIC_SystemReset 留下的 SFTRSTF —— §5.3)。
|
||||
* 函数说明:取走并清零位图 → 与必需掩码比较(v1.5 起按位分账,判据推导见设计说明 §4):
|
||||
* 全中 → 清零复合边沿与证据门闩并喂狗;
|
||||
* 有缺但每位连击 < 容忍 → **继续喂狗**(单窗/亚秒级缺席是负载抖动,不是
|
||||
* 停摆;只按位记账并在复合边沿打一条诊断行);
|
||||
* 任一位连击 >= WD_MISS_TOLERANCE(同一任务 800ms 没跑过)= 真停摆 →
|
||||
* 申报 [WD] task stall 并**停止喂狗**,让 IWDG 自然溢出复位
|
||||
* (RCC_CSR 记权威 IWDGRSTF 而非 SFTRSTF —— §5.3)。
|
||||
* ⚠ 判定故意不用"复合连续缺席窗数"当停喂门槛:那会把"本窗缺 A、下窗缺
|
||||
* B"这类各自远低于容忍度的抖动串成假停摆(2026-10-25 大窗实证)。
|
||||
*/
|
||||
void task_wd_poll(void) {
|
||||
uint32_t now;
|
||||
uint32_t snapshot;
|
||||
uint32_t mask_now;
|
||||
uint32_t worst;
|
||||
uint32_t i;
|
||||
|
||||
if (s_started == 0u) {
|
||||
return; /* 未启动:不做任何判定 */
|
||||
@@ -454,51 +492,72 @@ void task_wd_poll(void) {
|
||||
s_last_mask = snapshot;
|
||||
|
||||
#ifndef TASK_WATCHDOG_UNIT_TEST
|
||||
/* round 7 取证:留存近 8 次抽样位图(环形),停喂时随证据落盘 */
|
||||
/* 第 7 轮取证:留存近 8 次抽样位图(环形),停喂时随证据落盘 */
|
||||
s_snap_hist[s_snap_hist_idx] = snapshot;
|
||||
s_snap_hist_idx = (s_snap_hist_idx + 1u) % WD_SNAP_HIST_DEPTH;
|
||||
#endif
|
||||
|
||||
if (snapshot == wd_required_mask()) {
|
||||
mask_now = wd_required_mask();
|
||||
|
||||
/* 按位分账:位出现→该位清零;缺席→+1(饱和封顶)。掩码外的位不入账
|
||||
* (监视范围随构建配置变化时,账本自动跟随,不会留旧账)。 */
|
||||
worst = 0u;
|
||||
for (i = 0u; i < (uint32_t)TASK_WD_ID_MAX; i++) {
|
||||
if ((mask_now & (1u << i)) == 0u) {
|
||||
s_miss_bits[i] = 0u;
|
||||
continue;
|
||||
}
|
||||
if ((snapshot & (1u << i)) != 0u) {
|
||||
s_miss_bits[i] = 0u;
|
||||
} else if (s_miss_bits[i] < (uint8_t)WD_MISS_TOLERANCE) {
|
||||
s_miss_bits[i]++;
|
||||
}
|
||||
if ((uint32_t)s_miss_bits[i] > worst) {
|
||||
worst = (uint32_t)s_miss_bits[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (snapshot == mask_now) {
|
||||
s_miss_streak = 0u;
|
||||
s_stall_evidenced = 0u; /* 恢复:允许下次停摆再落一次证据 */
|
||||
wd_platform_feed(); /* 全员健康:喂狗 */
|
||||
return;
|
||||
}
|
||||
|
||||
/* 有任务缺席:累计(不喂狗) */
|
||||
/* 本窗有缺席位(复合缺席):记总数;**复合边沿**(上一窗全中)只打一条
|
||||
* 诊断标记行——v1.5 起该行不再意味着停喂,停喂只看按位连击 */
|
||||
s_miss_total++;
|
||||
if (s_miss_streak < WD_MISS_TOLERANCE) {
|
||||
s_miss_streak++;
|
||||
}
|
||||
|
||||
if (s_miss_streak == 0u) {
|
||||
/* 复合缺席 episode 起点裸 UART 标记(每 episode 一条):证明 poller 仍在
|
||||
* 抽样、并给出**首次**缺席位图——定位谁先停跑(第 7 轮出身)。⚠ v1.5 起
|
||||
* 本行为**纯诊断**——复合边沿 ≠ 停喂,stall 判定权在下面的按位连击。 */
|
||||
#ifndef TASK_WATCHDOG_UNIT_TEST
|
||||
if (s_miss_streak == 1u) {
|
||||
/* 停喂起点裸 UART 标记(每次缺席 episode 仅一条):证明 poller 仍在
|
||||
* 抽样、并给出**首次**缺席的位图——定位是哪一任务先停摆(round 7) */
|
||||
char m[96];
|
||||
int mn = snprintf(m, sizeof(m),
|
||||
"MISS start snap=0x%08X req=0x%08X t=%lu\r\n",
|
||||
(unsigned)snapshot, (unsigned)wd_required_mask(),
|
||||
(unsigned)snapshot, (unsigned)mask_now,
|
||||
(unsigned long)now);
|
||||
if (mn > 0) {
|
||||
dbg_console_write_raw(m, (unsigned)strlen(m));
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
if (s_miss_streak < WD_MISS_TOLERANCE) {
|
||||
s_miss_streak++; /* 饱和封顶;仅供复合 episode 边沿 */
|
||||
}
|
||||
|
||||
if (s_miss_streak >= WD_MISS_TOLERANCE) {
|
||||
/* 连续 800ms 有任务未动过 → 已停喂 → 约 2.5s 后 IWDG 复位。
|
||||
* 最后遗言走裸 UART(不依赖内核、不遍历指针、有界长度)。 */
|
||||
if (worst >= WD_MISS_TOLERANCE) {
|
||||
/* 同一位连续缺席 8 窗(同一任务 800ms 没跑过)= 真停摆 → 停喂 → 约 2.5s
|
||||
* 后 IWDG 复位。最后遗言走裸 UART(不依赖内核、不遍历指针、有界长度)。 */
|
||||
DBG_ERROR("[WD] task stall: mask=0x%08X req=0x%08X miss=%lu",
|
||||
(unsigned)snapshot,
|
||||
(unsigned)wd_required_mask(),
|
||||
(unsigned long)s_miss_total);
|
||||
#ifndef TASK_WATCHDOG_UNIT_TEST
|
||||
/* round 7 取证:把现场落 /log/wdev.log(掉电不丢)。
|
||||
/* 第 7 轮取证:把现场落 /log/wdev.log(掉电不丢)。
|
||||
* 串口 stall 行经僵尸通道可能丢失,故此处独立留盘上证据。
|
||||
* ⚠ 本 episode 只写**一次**(s_stall_evidenced 门闩;注意 s_miss_streak
|
||||
* 达上限后会**饱和**在容忍值,不能用它判"首次"):证据落盘经
|
||||
* ⚠ 本 episode 只写**一次**(s_stall_evidenced 门闩;注意 worst 连击达
|
||||
* 上限后会**饱和**在容忍值,不能用它判"首次"):证据落盘经
|
||||
* sys_log_evidence() 走 NAND 写 → FTL 路径调 nand_ftl_wd_feed()
|
||||
* **直接喂狗**。若每轮 100ms 都写,等于在"停喂"窗口里持续喂狗 →
|
||||
* IWDG 永不溢出 → 停喂语义被证据机制自身抵消(2026-10-03 台架 V2
|
||||
@@ -510,6 +569,11 @@ void task_wd_poll(void) {
|
||||
wd_stall_evidence(snapshot);
|
||||
}
|
||||
#endif
|
||||
} else {
|
||||
/* 有缺席位、但每一位连击都未到容忍值(含"本窗缺 A、下窗缺 B"的交替型):
|
||||
* 照常喂狗——多任务各自亚秒抖动不再串成复合停喂(按位分账收口的正身,
|
||||
* 2026-10-25 大窗 16:24/16:28 型事件的根治)。 */
|
||||
wd_platform_feed();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -556,7 +620,7 @@ uint32_t task_wd_is_armed(void) {
|
||||
*/
|
||||
__weak void wd_platform_feed(void) {
|
||||
IWDG->KR = WD_IWDG_KEY_FEED; /* 重装计数器(喂狗) */
|
||||
wd_feed_track(); /* round 7 取证:记录喂狗时刻/最大间隔 */
|
||||
wd_feed_track(); /* 第 7 轮取证:记录喂狗时刻/最大间隔 */
|
||||
}
|
||||
|
||||
/* ==== 为什么 IWDG 参数必须在这里写(2026-10-02 台架实测,P0 根因)====
|
||||
@@ -568,10 +632,10 @@ __weak void wd_platform_feed(void) {
|
||||
* 实测读数:`IWDG->PR = 0`、`IWDG->RLR = 4095` → 实际超时 4×4096/LSI
|
||||
* ≈ 349~512ms,比设计的 1701~2498ms **短约 5 倍**。后果有二:
|
||||
* ① `WD_MISS_TOLERANCE`(8×100ms=800ms)**永远等不到**就被 IWDG 复位
|
||||
* → 停喂判定分支成为死代码(这正是 round 7 观察到"STALL 从未执行"
|
||||
* → 停喂判定分支成为死代码(这正是第 7 轮观察到"STALL 从未执行"
|
||||
* 的原因,当时误判为"poller 自身停摆");
|
||||
* ② 任何 >400ms 的心跳缺口都直接 IWDG 误复位。空 STOR(0 字节截断)
|
||||
* → dhara/FTL flush 使 NET/FTP/UI 心跳缺 ~400ms → 复位(round 3~8
|
||||
* → dhara/FTL flush 使 NET/FTP/UI 心跳缺 ~400ms → 复位(第 3~8 轮
|
||||
* 全部"空 STOR 必复位"的共同根因,P0-B 出口阻塞点)。
|
||||
* 实测验证:SWD 现场把 PR/RLR 改成 4/1249 后,同一个空 STOR 由 5/5 复位
|
||||
* 变为 100% 成功(`226 Closing data connection.`)。
|
||||
@@ -714,8 +778,12 @@ void nand_ftl_wd_feed(void) {
|
||||
*/
|
||||
#ifdef TASK_WATCHDOG_UNIT_TEST
|
||||
void task_wd_test_reset(void) {
|
||||
uint32_t i;
|
||||
s_hb_bits = 0u;
|
||||
s_miss_streak = 0u;
|
||||
for (i = 0u; i < (uint32_t)TASK_WD_ID_MAX; i++) {
|
||||
s_miss_bits[i] = 0u; /* 按位账本一并清零(v1.5 判定输入) */
|
||||
}
|
||||
s_miss_total = 0u;
|
||||
s_last_mask = 0u;
|
||||
s_started = 0u;
|
||||
|
||||
+10
-10
@@ -85,10 +85,10 @@ uint32_t task_wd_miss_count(void); /* 累计未达标轮数 */
|
||||
uint32_t task_wd_is_armed(void); /* 1 = IWDG 已启动 */
|
||||
|
||||
/*
|
||||
* ==================== round 8 取证:复位不丢的"进段打点" ====================
|
||||
* 背景(2026-10-02 台架 round 7~8 取证,完整链条见
|
||||
* ==================== 第 8 轮取证:复位不丢的"进段打点" ====================
|
||||
* 背景(2026-10-02 台架第 7~8 轮取证,完整链条见
|
||||
* docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录):误复位时判定分支(STALL)从未
|
||||
* 执行,当时判为**喂狗点 task_wd_poll 自身停摆**(round 8b 已更正:真因是 IWDG 超时
|
||||
* 执行,当时判为**喂狗点 task_wd_poll 自身停摆**(第 8b 轮已更正:真因是 IWDG 超时
|
||||
* 被静默配成复位默认值),而非某个被监视任务饿死。RAM 态随复位消失、串口又可能来不及打,
|
||||
* 故需要一条**复位后仍可读**的"defaultTask 主循环最后走到哪一段"轨迹。
|
||||
*
|
||||
@@ -96,7 +96,7 @@ uint32_t task_wd_is_armed(void); /* 1 = IWDG 已启动 */
|
||||
* MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct 的 UNINIT_CCM)——IWDG/引脚复位
|
||||
* 不改变 SRAM 内容,故复位后仍能读出上一轮的最后进段。槽布局:
|
||||
* [0]=magic(校验是否为本模块写过)[1]=段号 [2]=进段时刻(ms) [3]=循环序号
|
||||
* round 8b 追加"时间源存活性"槽(同样在 .noinit):
|
||||
* 第 8b 轮追加"时间源存活性"槽(同样在 .noinit):
|
||||
* cur = [magic, uwTick, 本轮 TIM7 计数, 0](TIM7 ISR 每 1ms 更新)
|
||||
* prev = [magic, uwTick, 上一轮 TIM7 计数, 0](main() 在 HAL_Init 之前快照)
|
||||
* 注意:**仅 MCU 构建存在**(TASK_WATCHDOG_UNIT_TEST 下不编入,主机单测不受影响)。
|
||||
@@ -117,7 +117,7 @@ typedef enum {
|
||||
} task_wd_seg_t;
|
||||
|
||||
/*
|
||||
* 函数功能:记录"即将进入的主循环段"(round 8 取证;**仅 MCU 构建**)
|
||||
* 函数功能:记录"即将进入的主循环段"(第 8 轮取证;**仅 MCU 构建**)
|
||||
* 入口参数:seg - 段号(task_wd_seg_t)
|
||||
* 返回值:无
|
||||
* 限定条件:defaultTask 主循环调用(任意上下文亦可,纯 SRAM 写)
|
||||
@@ -126,20 +126,20 @@ typedef enum {
|
||||
void task_wd_seg(uint32_t seg);
|
||||
|
||||
/*
|
||||
* 函数功能:开机上报上一轮最后进段 + 时间源存活性(round 8 取证;**仅 MCU 构建**)
|
||||
* 函数功能:开机上报上一轮最后进段 + 时间源存活性(第 8 轮取证;**仅 MCU 构建**)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:启动早期调用(内核启动前亦可,裸 UART 直发)
|
||||
* 函数说明:magic 命中才打一行 `PREVRUN seg=<段> seq=<序号> t=<ms>`(裸 UART,
|
||||
* 不依赖环形缓冲写者),随后清 magic(一次复位只报一次)。
|
||||
* round 8b:紧接着打一行 `TICKRUN uw= n= duw=`——uw 为 TIM7 在**上一轮**
|
||||
* 第 8b 轮:紧接着打一行 `TICKRUN uw= n= duw=`——uw 为 TIM7 在**上一轮**
|
||||
* 最后一次记下的 HAL tick、n 为上一轮 TIM7 中断总数、duw = uw - t
|
||||
* (相对进段时刻的增量),据此判定 TIM7 是否在冻结同刻停摆。
|
||||
*/
|
||||
void task_wd_seg_report(void);
|
||||
|
||||
/*
|
||||
* 函数功能:开机快照上一轮的"时间源存活性"(round 8b 取证;**仅 MCU 构建**)
|
||||
* 函数功能:开机快照上一轮的"时间源存活性"(第 8b 轮取证;**仅 MCU 构建**)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:**必须在 HAL_Init() 之前调用**(main() 的 USER CODE BEGIN 1)
|
||||
@@ -150,13 +150,13 @@ void task_wd_seg_report(void);
|
||||
void task_wd_tick_snapshot(void);
|
||||
|
||||
/*
|
||||
* 函数功能:记录"时间源存活性"到复位不丢的 .noinit 槽(round 8b 取证;**仅 MCU 构建**)
|
||||
* 函数功能:记录"时间源存活性"到复位不丢的 .noinit 槽(第 8b 轮取证;**仅 MCU 构建**)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:TIM7 中断上下文调用(`HAL_TIM_PeriodElapsedCallback` 内,每 1ms)
|
||||
* 函数说明:更新 cur 槽 = [magic, uwTick, 本轮 TIM7 中断计数, 0](纯 SRAM 写,无阻塞)。
|
||||
* 与 task_wd_seg 的"进段打点"配合,复位后由 task_wd_seg_report() 上报。
|
||||
* 背景(2026-10-02 台架 round 8b 取证,见 docs/子系统设计/看门狗设计说明.md
|
||||
* 背景(2026-10-02 台架第 8b 轮取证,见 docs/子系统设计/看门狗设计说明.md
|
||||
* 2026-10-02 变更记录):HAL_UART_Transmit 的超时判据
|
||||
* 依赖 uwTick,时间源停摆即死等——必须先把"TIM7 是否停摆"钉死再定向修复。
|
||||
* ⚠ **本函数内严禁调用任何 FreeRTOS/CMSIS-RTOS API**:TIM7 在 HAL_Init
|
||||
|
||||
@@ -21,9 +21,9 @@
|
||||
|
||||
| Trap | 问题 | 状态 |
|
||||
|---|---|---|
|
||||
| Trap 21 | TC-NET-604 "RX 窗口冻结" 判定为**非缺陷**(原报告系测试程序自吞数据 + 判读工具误判) | ✅ 结案 |
|
||||
| Trap 22 | **快速复连丢 CONNECT/DISCONNECT 中断 → 监听静默死亡**;修复 = `net_reconcile_tick()` 周期对账自愈 | ✅ 已修 |
|
||||
| Trap 23 | **上传中止后首笔复连被静默截断(≤2048B)并误报成功**;修复 = 异常释放后复监听隔离排空 | ✅ 已修 |
|
||||
| TC-NET-604 | "RX 窗口冻结" 判定为**非缺陷**(原报告系测试程序自吞数据 + 判读工具误判) | ✅ 结案 |
|
||||
| TRAP-NET-17 | **快速复连事件面未收到 → 监听静默死亡**(E14 复核:非芯片丢事件,是读清消费竞态);修复 = `net_reconcile_tick()` 周期对账自愈 | ✅ 已修 |
|
||||
| TRAP-NET-18 | **上传中止后首笔复连被静默截断(≤2048B)并误报成功**;修复 = 异常释放后复监听隔离排空 | ✅ 已修 |
|
||||
|
||||
> 完整台账与复现配方见 `docs/问题记录/CH395F_Trap_Records.md`(**改驱动前必读**)。
|
||||
|
||||
@@ -31,4 +31,4 @@
|
||||
|
||||
- 测试指南:`docs/测试说明/CH395F_Test_Guide.md`
|
||||
- 上层抽象:`../NET/README.md`
|
||||
- 台架探针:`MDK-ARM/tcp_reconnect_probe.py`、`MDK-ARM/ftp_abort_probe.py`
|
||||
- 台架探针:`MDK-ARM/tcp_reconnect_probe.py`、`test/host/ftp_abort_probe.py`
|
||||
|
||||
+386
-23
@@ -8,9 +8,9 @@
|
||||
* 修改记录:
|
||||
* 2026-03-14 王建锋 创建初始版本,按代码规范重构
|
||||
* 2026-07-18 王建锋 改进write_send_buf/read_recv_buf使用SPI2 DMA批量传输
|
||||
* 2026-10-02 王建锋 E-34:3 处字节级 SPI 调用由 HAL_MAX_DELAY 改有限超时(100ms)
|
||||
* 2026-10-02 王建锋 3 处字节级 SPI 调用由 HAL_MAX_DELAY 改有限超时(100ms)
|
||||
* + 返回值检查 + 故障锁存(沿程只记一条日志);原 SPI 异常时调用方永久挂死
|
||||
* 2026-10-04 王建锋 P2-2:ch395f_spi_dma_transfer 由 void 改为返回状态(0 成功/-1 失败),
|
||||
* 2026-10-04 王建锋 ch395f_spi_dma_transfer 由 void 改为返回状态(0 成功/-1 失败),
|
||||
* DMA 初始化失败/超时中止均锁存通道故障;read_recv_buf 在 DMA 失败时不再把
|
||||
* rx_buf 内容拷入调用方缓冲(改零填充),消除"失败当成功"的静默数据损坏
|
||||
*/
|
||||
@@ -20,6 +20,7 @@
|
||||
#include <stdio.h>
|
||||
#include "ch395f.h"
|
||||
#include "spi.h"
|
||||
#include "gd5f2gq5ue.h" /* SPI1 分支转调 gd5f_spi1_notify_*(HAL SPI 回调全局唯一,本文件代管分发) */
|
||||
#include "usart.h"
|
||||
|
||||
/*
|
||||
@@ -39,8 +40,7 @@
|
||||
#define CH395F_CHECK_EXIST_TEST_DATA 0x57U /* 芯片检测测试数据 */
|
||||
#define CH395F_RESET_DELAY_MS 20U /* 复位后等待延时(ms) */
|
||||
#define CH395F_INIT_TIMEOUT_MS 500U /* 初始化超时时间(ms) */
|
||||
#define CH395F_SPI_BEGIN_DELAY_MS 1U /* SPI事务开始延时(ms) */
|
||||
#define CH395F_SPI_BYTE_TIMEOUT_MS 100U /* E-34:字节级SPI有限超时(原 HAL_MAX_DELAY) */
|
||||
#define CH395F_SPI_BYTE_TIMEOUT_MS 100U /* 字节级SPI有限超时(原 HAL_MAX_DELAY) */
|
||||
/* DMA缓冲大小使用ch395f.h中公开的CH395F_SPI_DMA_BUF_SIZE */
|
||||
#define CH395F_DMA_BUF_SIZE CH395F_SPI_DMA_BUF_SIZE
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
* 静态变量定义区 - DMA传输相关
|
||||
*/
|
||||
static volatile uint8_t s_spi2_dma_done = 0U; /* DMA传输完成标志 */
|
||||
static uint8_t s_spi_fault = 0U; /* E-34/P2-2:SPI 通道故障锁存(字节级或 DMA 级超时/出错置位,MCU复位清除) */
|
||||
static uint8_t s_spi_fault = 0U; /* SPI 通道故障锁存(字节级或 DMA 级超时/出错置位,MCU复位清除) */
|
||||
|
||||
__attribute__((aligned(4)))
|
||||
static uint8_t s_spi2_dma_tx_buf[CH395F_DMA_BUF_SIZE]; /* DMA发送缓冲区 */
|
||||
@@ -90,7 +90,7 @@ static void ch395f_spi_end(void) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:锁存一次 SPI 通道故障(E-34/P2-2:仅首条故障记日志)
|
||||
* 函数功能:锁存一次 SPI 通道故障(仅首条故障记日志)
|
||||
* 入口参数:kind - 故障类别标签("byte"/"dma-hal"/"dma-timeout") const char*
|
||||
* 返回值:无
|
||||
* 限定条件:无
|
||||
@@ -114,7 +114,7 @@ static void ch395f_spi_fault_set(const char *kind) {
|
||||
* 2. 阻塞等待DMA完成回调置标志位
|
||||
* 3. 超时(1秒)时强制中止SPI DMA并打印错误信息
|
||||
* 4. 传输完成后检查SPI错误码并清除溢出标志
|
||||
* 5. P2-2(2026-10-04):失败路径锁存通道故障并返回 -1,调用方据此
|
||||
* 5.(2026-10-04):失败路径锁存通道故障并返回 -1,调用方据此
|
||||
* 判定 rx_buf 内容不可信,不再"失败当成功"继续消费
|
||||
*/
|
||||
static int ch395f_spi_dma_transfer(uint16_t len) {
|
||||
@@ -174,14 +174,11 @@ static int ch395f_spi_dma_transfer(uint16_t len) {
|
||||
* 入口参数:hspi - SPI句柄指针 SPI_HandleTypeDef*
|
||||
* 函数说明:由HAL SPI驱动在DMA TX+RX完成后调用
|
||||
*/
|
||||
/* SPI1 DMA完成标志(定义在gd5f2gq5ue.c中) */
|
||||
extern volatile uint8_t g_spi1_dma_done;
|
||||
|
||||
void HAL_SPI_TxRxCpltCallback(SPI_HandleTypeDef *hspi) {
|
||||
if (hspi->Instance == SPI2) {
|
||||
s_spi2_dma_done = 1U;
|
||||
} else if (hspi->Instance == SPI1) {
|
||||
g_spi1_dma_done = 1U;
|
||||
gd5f_spi1_notify_done(); /* 归 GD5F 所有:本文件只代 HAL 分发,不碰它的标志 */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,7 +191,7 @@ void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi) {
|
||||
if (hspi->Instance == SPI2) {
|
||||
s_spi2_dma_done = 1U;
|
||||
} else if (hspi->Instance == SPI1) {
|
||||
g_spi1_dma_done = 1U;
|
||||
gd5f_spi1_notify_error(); /* 勿删:少了这行 GD5F 的 DMA 出错只能靠超时退出 */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,7 +209,7 @@ void HAL_SPI_ErrorCallback(SPI_HandleTypeDef *hspi) {
|
||||
uint8_t ch395f_write_cmd(uint8_t cmd) {
|
||||
uint8_t rx_data = 0xFFU;
|
||||
|
||||
/* 通过SPI发送命令并接收回显(E-34:有限超时+返回值检查;失败返回0xFF占位并锁存故障) */
|
||||
/* 通过SPI发送命令并接收回显(有限超时+返回值检查;失败返回0xFF占位并锁存故障) */
|
||||
if (HAL_SPI_TransmitReceive(&hspi2, &cmd, &rx_data, 1,
|
||||
CH395F_SPI_BYTE_TIMEOUT_MS) != HAL_OK) {
|
||||
ch395f_spi_fault_set("byte");
|
||||
@@ -229,7 +226,7 @@ uint8_t ch395f_write_cmd(uint8_t cmd) {
|
||||
uint8_t ch395f_write_data(uint8_t data) {
|
||||
uint8_t rx_data = 0xFFU;
|
||||
|
||||
/* 通过SPI发送数据并接收回显(E-34:有限超时+返回值检查) */
|
||||
/* 通过SPI发送数据并接收回显(有限超时+返回值检查) */
|
||||
if (HAL_SPI_TransmitReceive(&hspi2, &data, &rx_data, 1,
|
||||
CH395F_SPI_BYTE_TIMEOUT_MS) != HAL_OK) {
|
||||
ch395f_spi_fault_set("byte");
|
||||
@@ -247,7 +244,7 @@ uint8_t ch395f_read_data(void) {
|
||||
uint8_t tx_dummy = 0xFFU;
|
||||
uint8_t rx_data = 0xFFU;
|
||||
|
||||
/* 发送0xFF作为时钟源,读取芯片返回数据(E-34:有限超时+返回值检查) */
|
||||
/* 发送0xFF作为时钟源,读取芯片返回数据(有限超时+返回值检查) */
|
||||
if (HAL_SPI_TransmitReceive(&hspi2, &tx_dummy, &rx_data, 1,
|
||||
CH395F_SPI_BYTE_TIMEOUT_MS) != HAL_OK) {
|
||||
ch395f_spi_fault_set("byte");
|
||||
@@ -256,7 +253,7 @@ uint8_t ch395f_read_data(void) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:查询 SPI 通道故障锁存(E-34 / P2-2)
|
||||
* 函数功能:查询 SPI 通道故障锁存
|
||||
* 返回值:0 - 正常 1 - 曾发生 SPI 超时/错误(字节级或 DMA 级)
|
||||
* 限定条件:无
|
||||
* 函数说明:锁存式(不自动清除,MCU 复位前一直有效);上层据此判定
|
||||
@@ -694,7 +691,7 @@ ch395f_status_t ch395f_init(void) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:设置CH395的IP地址(低字节在前)
|
||||
* 函数功能:设置CH395的IP地址(点分十进制序:p_ip[0]=第一段,见头文件声明注)
|
||||
* 入口参数:p_ip - 4字节IP地址指针 uint8_t* 192.168.x.x格式
|
||||
* 限定条件:1. p_ip不可为NULL
|
||||
* 2. 必须在 ch395f_init() 之前调用(手册 §9.2.1:SET_IP 先于 INIT_CH395)
|
||||
@@ -723,7 +720,7 @@ void ch395f_set_ip_addr(uint8_t *p_ip) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:设置CH395的网关IP地址(低字节在前)
|
||||
* 函数功能:设置CH395的网关IP地址(点分十进制序,同 set_ip_addr)
|
||||
* 入口参数:p_gwip - 4字节网关IP指针 uint8_t*
|
||||
* 限定条件:1. p_gwip不可为NULL
|
||||
* 2. 必须在 ch395f_init() 之前调用(手册 §9.2.1:SET_GWIP 先于 INIT_CH395)
|
||||
@@ -752,7 +749,7 @@ void ch395f_set_gwip_addr(uint8_t *p_gwip) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:设置CH395的子网掩码(低字节在前)
|
||||
* 函数功能:设置CH395的子网掩码(点分十进制序,同 set_ip_addr)
|
||||
* 入口参数:p_mask - 4字节子网掩码指针 uint8_t*
|
||||
* 限定条件:1. p_mask不可为NULL
|
||||
* 2. 必须在 ch395f_init() 之前调用(手册 §9.2.1:SET_MASK 先于 INIT_CH395)
|
||||
@@ -1182,9 +1179,9 @@ void ch395f_write_send_buf(uint8_t sock, uint8_t *p_data, uint16_t len) {
|
||||
}
|
||||
if (len > (uint16_t)CH395F_SEND_BUF_SIZE_BYTES) {
|
||||
/* §5.32:单次写入长度不得大于该 Socket 的发送缓冲区(本仓 = 2 块 = 1KB)。
|
||||
* 超出不会被命令拒绝,而是写穿发送缓冲导致数据错乱(Trap 20)→ 只告警不拦截,
|
||||
* 超出不会被命令拒绝,而是写穿发送缓冲导致数据错乱(TRAP-NET-16)→ 只告警不拦截,
|
||||
* 分块责任在调用方(NET 层 NET_SEND_CHUNK_MAX)。 */
|
||||
DBG_ERROR("write_send_buf OVERSIZE-SEND BUF: sock %u len %u > send buf %u (Trap 20)",
|
||||
DBG_ERROR("write_send_buf OVERSIZE-SEND BUF: sock %u len %u > send buf %u (TRAP-NET-16)",
|
||||
(unsigned)sock, (unsigned)len, (unsigned)CH395F_SEND_BUF_SIZE_BYTES);
|
||||
}
|
||||
|
||||
@@ -1200,7 +1197,7 @@ void ch395f_write_send_buf(uint8_t sock, uint8_t *p_data, uint16_t len) {
|
||||
/* 数据同步屏障:确保memcpy写入对DMA可见 */
|
||||
__DSB();
|
||||
|
||||
/* CS低 → DMA批量传输 → CS高(P2-2:失败已由 transfer 锁存通道故障并记日志,
|
||||
/* CS低 → DMA批量传输 → CS高(失败已由 transfer 锁存通道故障并记日志,
|
||||
* 调用方经 ch395f_spi_byte_fault() 查询;此处无后续处理,失败即放弃本次发送) */
|
||||
ch395f_spi_begin();
|
||||
(void)ch395f_spi_dma_transfer(total);
|
||||
@@ -1253,7 +1250,7 @@ void ch395f_read_recv_buf(uint8_t sock, uint8_t *p_data, uint16_t len) {
|
||||
rc = ch395f_spi_dma_transfer(total);
|
||||
ch395f_spi_end();
|
||||
|
||||
/* P2-2(2026-10-04):DMA 失败时 rx_buf 内容不可信,禁止拷入调用方缓冲
|
||||
/* (2026-10-04):DMA 失败时 rx_buf 内容不可信,禁止拷入调用方缓冲
|
||||
* (否则把总线残留/半截数据当有效载荷 → 静默数据损坏);改零填充并返回,
|
||||
* 调用方据 ch395f_spi_byte_fault() 判定通道异常。 */
|
||||
if (rc != 0) {
|
||||
@@ -1409,3 +1406,369 @@ void ch395f_get_unreach_info(uint8_t *p_buf) {
|
||||
}
|
||||
ch395f_spi_end();
|
||||
}
|
||||
|
||||
/* ============================================================================
|
||||
* 自愈驱动原语(设计 docs/网络/网络自愈架构设计.md §4.4;
|
||||
* 形态定案锚点 同文档 §5.2 E2/E3/E4)
|
||||
* ========================================================================== */
|
||||
|
||||
/* 每 Socket 状态缓存(刷新点:主动回读 ch395f_get_sock_state;
|
||||
* 以及 net_poll 每轮对在用 Socket 各刷一次——E2 实测 18.1us/次,
|
||||
* 4 监听 ~72us 远小于 1ms 轮询周期) */
|
||||
static ch395f_sock_state_t g_sock_state[8];
|
||||
|
||||
/* sock_reset 非阻塞状态机槽位(每 Socket 一个;t0 用 16 位 tick 差值,
|
||||
* 各阶段超时 <=2s,远小于 65535ms 回绕窗) */
|
||||
#define CH395F_RST_ST_IDLE 0U
|
||||
#define CH395F_RST_ST_RX_DRAIN 1U
|
||||
#define CH395F_RST_ST_CLOSE 2U
|
||||
#define CH395F_RST_ST_MIN_WAIT 3U
|
||||
#define CH395F_RST_ST_EVT_DRAIN 4U
|
||||
#define CH395F_RST_ST_WAIT_CLOSE 5U
|
||||
#define CH395F_RST_RX_MAX_ROUNDS 4U /* RX 缓冲 2KB,单读即空;4 为安全上限 */
|
||||
#define CH395F_RST_QUIESCENT 2U /* 排空事件判据:连续 2 轮无新位(E8 M5 口径) */
|
||||
#define CH395F_RST_RX_TMO_MS 500U
|
||||
#define CH395F_RST_EVT_TMO_MS 500U
|
||||
#define CH395F_RST_CLS_TMO_MS 2000U
|
||||
|
||||
typedef struct {
|
||||
uint16_t t0; /* 当前阶段起始 tick */
|
||||
uint8_t state; /* CH395F_RST_ST_* */
|
||||
uint8_t flags; /* bit7=mode(1=QUARANTINE) bit6..4=quiescent 轮数 bit3..0=drain 轮数(饱和) */
|
||||
} ch395f_rst_slot_t;
|
||||
static ch395f_rst_slot_t g_rst[8];
|
||||
|
||||
/* 链路态缓存(F-DRV-13) */
|
||||
static uint8_t g_link_up;
|
||||
static uint8_t g_link_valid;
|
||||
static uint16_t g_link_changes;
|
||||
|
||||
/* 边界 fault 计数(sn 越界等;日志只留痕一次,其余只计数) */
|
||||
static uint16_t g_p2_faults;
|
||||
static uint8_t g_p2_fault_logged;
|
||||
|
||||
/*
|
||||
* 函数功能:驱动原语边界异常留痕(首次 DBG_ERROR,之后只计数)
|
||||
* 入口参数:tag - 出错点短标签 const char*
|
||||
*/
|
||||
static void ch395f_p2_fault_note(const char *tag) {
|
||||
g_p2_faults++;
|
||||
if (g_p2_fault_logged == 0U) {
|
||||
g_p2_fault_logged = 1U;
|
||||
DBG_ERROR("ch395f P2 boundary fault @%s (further ones counted only)", tag);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读空并丢弃 RX(排空契约形态=整缓冲单读,与 ch395f_read_recv_buf
|
||||
* 同形但不拷出——复位残渣是垃圾,零新增 RAM)
|
||||
* 入口参数:sn - Socket 索引 uint8_t
|
||||
* len - 本次读出字节数(<=CH395F_SPI_DMA_MAX_PAYLOAD) uint16_t
|
||||
*/
|
||||
static void ch395f_rst_rx_discard(uint8_t sn, uint16_t len) {
|
||||
s_spi2_dma_tx_buf[0] = CH395F_CMD_READ_RECV_BUF_SN;
|
||||
s_spi2_dma_tx_buf[1] = sn;
|
||||
s_spi2_dma_tx_buf[2] = len & 0xFFU;
|
||||
s_spi2_dma_tx_buf[3] = (len >> 8) & 0xFFU;
|
||||
memset(&s_spi2_dma_tx_buf[4], 0xFFU, len);
|
||||
ch395f_spi_begin();
|
||||
(void)ch395f_spi_dma_transfer(4U + len);
|
||||
ch395f_spi_end();
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:主动回读本 Socket 状态并刷新驱动缓存(缓存刷新点之一,另一个
|
||||
* 是 net_poll 每轮全量刷新)
|
||||
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
|
||||
* 返回值:类型化状态;sn>7 返回 {CLOSED, 哨兵} 并计一次边界 fault
|
||||
* 限定条件:芯片已初始化;netTask 上下文(与其它 CH395F 事务串行)
|
||||
*/
|
||||
ch395f_sock_state_t ch395f_get_sock_state(uint8_t sn) {
|
||||
ch395f_sock_state_t st;
|
||||
uint8_t raw[2];
|
||||
|
||||
if (sn >= 8U) {
|
||||
ch395f_p2_fault_note("get_state sn");
|
||||
st.open = CH395F_SOCKET_CLOSED;
|
||||
st.tcp_state = CH395F_SOCK_STATE_NONE;
|
||||
return st;
|
||||
}
|
||||
ch395f_get_socket_status(sn, raw);
|
||||
st.open = raw[0];
|
||||
st.tcp_state = (raw[0] == CH395F_SOCKET_OPEN) ? raw[1] : CH395F_SOCK_STATE_NONE;
|
||||
g_sock_state[sn] = st;
|
||||
return st;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读驱动缓存(零 SPI 成本;新鲜度取决于刷新点,见上)
|
||||
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
|
||||
* 返回值:最近一次刷新的状态;sn>7 返回 {CLOSED, 哨兵}
|
||||
* 限定条件:芯片已初始化
|
||||
*/
|
||||
ch395f_sock_state_t ch395f_sock_state_cached(uint8_t sn) {
|
||||
if (sn >= 8U) {
|
||||
ch395f_sock_state_t st;
|
||||
|
||||
ch395f_p2_fault_note("cached sn");
|
||||
st.open = CH395F_SOCKET_CLOSED;
|
||||
st.tcp_state = CH395F_SOCK_STATE_NONE;
|
||||
return st;
|
||||
}
|
||||
return g_sock_state[sn];
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:判"本 Socket 正在监听"(读缓存:open=OPEN 且 TCP=LISTEN)
|
||||
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
|
||||
* 返回值:1=在听 0=不在听 int
|
||||
* 限定条件:缓存新鲜度同 ch395f_sock_state_cached
|
||||
*/
|
||||
int ch395f_sock_is_listening(uint8_t sn) {
|
||||
ch395f_sock_state_t c = ch395f_sock_state_cached(sn);
|
||||
|
||||
return (c.open == CH395F_SOCKET_OPEN && c.tcp_state == CH395F_TCP_LISTEN) ? 1 : 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:事件护栏——只读本 Socket SINT(读即清),永不触碰全局寄存器
|
||||
* (F-NET-7 收窄:两级预清会清掉其它 Socket 的事件位)
|
||||
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
|
||||
* 返回值:SINT 位图;sn>7 返回 0 uint8_t
|
||||
* 限定条件:芯片已初始化;全局寄存器唯一读者=net_poll(驱动侧契约)
|
||||
*/
|
||||
uint8_t ch395f_sock_events(uint8_t sn) {
|
||||
if (sn >= 8U) {
|
||||
ch395f_p2_fault_note("events sn");
|
||||
return 0U;
|
||||
}
|
||||
/* 契约:只读本 Socket SINT(0x30 读即清),永不触碰全局寄存器 0x19——
|
||||
* 全局寄存器读即清且含全部 Socket 事件位,唯一读者=net_poll(F-NET-7) */
|
||||
return ch395f_get_sock_int_status(sn);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:启动 Socket 复合复位(非阻塞状态机,每 net_poll 步进;E4 定案序列
|
||||
* 读空 RX -> CLOSE -> 排空事件(quiescent) -> 轮询 byte0=00H)
|
||||
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
|
||||
* mode - 复位模式(GRACEFUL 100ms / QUARANTINE 400ms 最短等待)
|
||||
* 返回值:0=已启动 -1=参数错或已在运行(调用方逻辑错误) int
|
||||
* 限定条件:netTask 上下文;调用前本层已放弃该会话(复位丢弃 RX 残渣)
|
||||
*/
|
||||
int ch395f_sock_reset_begin(uint8_t sn, ch395f_reset_mode_t mode) {
|
||||
if (sn >= 8U) {
|
||||
ch395f_p2_fault_note("reset_begin sn");
|
||||
return -1;
|
||||
}
|
||||
if (g_rst[sn].state != CH395F_RST_ST_IDLE) {
|
||||
/* 已在运行:调用方逻辑错误(同一 Socket 不允许并发复位) */
|
||||
return -1;
|
||||
}
|
||||
g_rst[sn].state = CH395F_RST_ST_RX_DRAIN;
|
||||
g_rst[sn].flags = (mode == CH395F_RESET_QUARANTINE) ? 0x80U : 0x00U;
|
||||
g_rst[sn].t0 = (uint16_t)HAL_GetTick();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:复位状态机步进(net_poll 或测试循环周期调用,禁复制忙等)
|
||||
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
|
||||
* 返回值:0=完成(含未启动,幂等) 1=进行中 -1=失败(超时/参数,槽位已复位可重开) int
|
||||
* 限定条件:netTask 上下文
|
||||
*/
|
||||
int ch395f_sock_reset_poll(uint8_t sn) {
|
||||
ch395f_rst_slot_t *p;
|
||||
uint16_t now;
|
||||
uint16_t el;
|
||||
|
||||
if (sn >= 8U) {
|
||||
ch395f_p2_fault_note("reset_poll sn");
|
||||
return -1;
|
||||
}
|
||||
p = &g_rst[sn];
|
||||
if (p->state == CH395F_RST_ST_IDLE) {
|
||||
return 0; /* 未启动/已完成:幂等 */
|
||||
}
|
||||
now = (uint16_t)HAL_GetTick();
|
||||
el = (uint16_t)(now - p->t0);
|
||||
|
||||
switch (p->state) {
|
||||
case CH395F_RST_ST_RX_DRAIN: {
|
||||
/* E4 定案第 1 步:close 前 RX 必须为空(带脏缓冲关闭 -> 拆链停滞 ->
|
||||
* 毒化下一会话且级联自持,C 变体 0/20 实证) */
|
||||
uint16_t len = ch395f_get_recv_len(sn);
|
||||
uint8_t rounds = (uint8_t)(p->flags & 0x0FU);
|
||||
|
||||
if (len == 0U) {
|
||||
p->state = CH395F_RST_ST_CLOSE;
|
||||
p->t0 = now;
|
||||
break;
|
||||
}
|
||||
if (rounds < CH395F_RST_RX_MAX_ROUNDS) {
|
||||
ch395f_rst_rx_discard(sn, (len > (uint16_t)CH395F_SPI_DMA_MAX_PAYLOAD)
|
||||
? (uint16_t)CH395F_SPI_DMA_MAX_PAYLOAD : len);
|
||||
p->flags = (uint8_t)((p->flags & 0xF0U) | (rounds + 1U));
|
||||
} else {
|
||||
/* 排不空(异常形态):不再恋战,继续 CLOSE,失败由 WAIT_CLOSE 超时兜底 */
|
||||
ch395f_p2_fault_note("rx drain stall");
|
||||
p->state = CH395F_RST_ST_CLOSE;
|
||||
p->t0 = now;
|
||||
}
|
||||
if ((uint16_t)(now - p->t0) > CH395F_RST_RX_TMO_MS) {
|
||||
p->state = CH395F_RST_ST_IDLE;
|
||||
ch395f_p2_fault_note("rx drain tmo");
|
||||
return -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CH395F_RST_ST_CLOSE:
|
||||
/* E4 定案第 2 步:CLOSE(裸命令,不复制 poll_cmd_status 忙等——
|
||||
* E3 实证其对 CLOSE 无判别力;完成判据=状态回读,E3 定案) */
|
||||
ch395f_spi_begin();
|
||||
ch395f_write_cmd(CH395F_CMD_CLOSE_SOCKET_SN);
|
||||
ch395f_write_data(sn);
|
||||
ch395f_spi_end();
|
||||
p->state = CH395F_RST_ST_MIN_WAIT;
|
||||
p->t0 = now;
|
||||
break;
|
||||
case CH395F_RST_ST_MIN_WAIT: {
|
||||
uint16_t need = ((p->flags & 0x80U) != 0U) ? 400U : 100U;
|
||||
if (el >= need) {
|
||||
p->state = CH395F_RST_ST_EVT_DRAIN;
|
||||
p->t0 = now;
|
||||
p->flags = (uint8_t)(p->flags & 0x8FU); /* quiescent 计数清零 */
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CH395F_RST_ST_EVT_DRAIN: {
|
||||
/* E4 定案第 3 步:排空事件至 quiescent(陈旧粘滞位会误导事件驱动
|
||||
* accept——E4 C 变体签名 0x04 陈旧 RECV;读即清,连续 2 轮无新位即静) */
|
||||
uint8_t bits = ch395f_sock_events(sn);
|
||||
if (bits == 0U) {
|
||||
uint8_t q = (uint8_t)((p->flags >> 4) & 0x07U);
|
||||
if ((q + 1U) >= CH395F_RST_QUIESCENT) {
|
||||
p->state = CH395F_RST_ST_WAIT_CLOSE;
|
||||
p->t0 = now;
|
||||
} else {
|
||||
p->flags = (uint8_t)((p->flags & 0x8FU) | ((q + 1U) << 4));
|
||||
}
|
||||
} else {
|
||||
p->flags = (uint8_t)(p->flags & 0x8FU); /* 有新位:quiescent 计数清零 */
|
||||
}
|
||||
if (el > CH395F_RST_EVT_TMO_MS) {
|
||||
p->state = CH395F_RST_ST_IDLE;
|
||||
ch395f_p2_fault_note("evt drain tmo");
|
||||
return -1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case CH395F_RST_ST_WAIT_CLOSE:
|
||||
/* E4 定案第 4 步:轮询 byte0=00H(E4 实测 closed_ms=0——命令落定即空,
|
||||
* 但为契约必需步) */
|
||||
if (ch395f_cmd_done(sn, CH395F_DONE_CLOSE) == 1) {
|
||||
p->state = CH395F_RST_ST_IDLE;
|
||||
return 0;
|
||||
}
|
||||
/* 二段关闭实证(TC-NET-1501 台架 2026-10-13):LISTEN 态 Socket 的 CLOSE 返
|
||||
* SUCCESS 但只拆监听(TCP 01->00),byte0 恒 05;需再发一次 CLOSE 才落
|
||||
* 00(生产 net 层靠 TRAP-NET-09 open 重试吸收此行为)。复位状态机自行补发,
|
||||
* 补发标记=drain 字段置 0xF(本阶段已用尽,复用安全)。 */
|
||||
{
|
||||
ch395f_sock_state_t c = ch395f_sock_state_cached(sn);
|
||||
if (c.open == CH395F_SOCKET_OPEN && c.tcp_state == CH395F_TCP_CLOSED
|
||||
&& (p->flags & 0x0FU) != 0x0FU) {
|
||||
p->flags = (uint8_t)((p->flags & 0xF0U) | 0x0FU);
|
||||
ch395f_spi_begin();
|
||||
ch395f_write_cmd(CH395F_CMD_CLOSE_SOCKET_SN);
|
||||
ch395f_write_data(sn);
|
||||
ch395f_spi_end();
|
||||
}
|
||||
}
|
||||
if (el > CH395F_RST_CLS_TMO_MS) {
|
||||
p->state = CH395F_RST_ST_IDLE;
|
||||
ch395f_p2_fault_note("wait closed tmo");
|
||||
return -1;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
p->state = CH395F_RST_ST_IDLE;
|
||||
ch395f_p2_fault_note("bad rst state");
|
||||
return -1;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:非阻塞命令完成查询(E3 定案=状态回读判据;单次回读 18.1us,E2 背书)
|
||||
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
|
||||
* what - 完成判据(OPEN=byte0 05H / LISTEN=+TCP 01H / CLOSE=byte0 00H)
|
||||
* 返回值:1=已完成 0=未完成 -1=参数错 int
|
||||
* 限定条件:netTask 上下文;勿复制 poll_cmd_status 忙等(E3 实证无判别力)
|
||||
*/
|
||||
int ch395f_cmd_done(uint8_t sn, ch395f_done_what_t what) {
|
||||
ch395f_sock_state_t st;
|
||||
|
||||
if (sn >= 8U) {
|
||||
return -1;
|
||||
}
|
||||
st = ch395f_get_sock_state(sn); /* 主动回读,单次 18.1us(E2) */
|
||||
switch (what) {
|
||||
case CH395F_DONE_OPEN:
|
||||
return (st.open == CH395F_SOCKET_OPEN) ? 1 : 0;
|
||||
case CH395F_DONE_LISTEN:
|
||||
return (st.open == CH395F_SOCKET_OPEN && st.tcp_state == CH395F_TCP_LISTEN) ? 1 : 0;
|
||||
case CH395F_DONE_CLOSE:
|
||||
return (st.open == CH395F_SOCKET_CLOSED) ? 1 : 0;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:喂全局中断位(net_poll 读全局后喂 bit0~3;
|
||||
* bit2 PHY_CHANGE 置位时重读 PHY 更新 link_up 缓存 + 变化计数)
|
||||
* 入口参数:glob_bits - 全局中断状态低字节语义位图 uint16_t
|
||||
* 返回值:无
|
||||
* 限定条件:netTask 上下文(会发 SPI 读 PHY)
|
||||
*/
|
||||
void ch395f_glob_event_feed(uint16_t glob_bits) {
|
||||
uint8_t up;
|
||||
|
||||
if ((glob_bits & CH395F_GINT_STAT_PHY_CHANGE) == 0U) {
|
||||
return;
|
||||
}
|
||||
/* B9 判据(TRAP-NET-09 修正):PHY != DISCONN 即 link up */
|
||||
up = (ch395f_get_phy_status() != CH395F_PHY_DISCONN) ? 1U : 0U;
|
||||
if (g_link_valid == 0U) {
|
||||
g_link_valid = 1U;
|
||||
g_link_up = up;
|
||||
} else if (up != g_link_up) {
|
||||
g_link_up = up;
|
||||
g_link_changes++;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读链路态缓存(首次查询惰性读 PHY 建档,不依赖 feed 已发生)
|
||||
* 入口参数:无
|
||||
* 返回值:1=link up 0=down uint8_t(B9 判据:PHY != DISCONN 即 up)
|
||||
* 限定条件:芯片已初始化
|
||||
*/
|
||||
uint8_t ch395f_link_up_cached(void) {
|
||||
if (g_link_valid == 0U) {
|
||||
/* 惰性建档(首次查询):不依赖 feed 已发生过 */
|
||||
g_link_up = (ch395f_get_phy_status() != CH395F_PHY_DISCONN) ? 1U : 0U;
|
||||
g_link_valid = 1U;
|
||||
}
|
||||
return g_link_up;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读链路变化计数(net 层订阅:计数变化 -> 日志/可选重配)
|
||||
* 入口参数:无
|
||||
* 返回值:自建档以来的 PHY 连接状态翻转次数 uint16_t
|
||||
* 限定条件:无
|
||||
*/
|
||||
uint16_t ch395f_link_change_count(void) {
|
||||
return g_link_changes;
|
||||
}
|
||||
|
||||
+138
-23
@@ -28,12 +28,10 @@ extern "C" {
|
||||
|
||||
/* 无输入数据,无输出数据 */
|
||||
#define CH395F_CMD_RESET_ALL 0x05U
|
||||
#define CH395F_CMD_ENTER_SLEEP 0x03U
|
||||
|
||||
/* 无输入数据,1 字节输出 */
|
||||
#define CH395F_CMD_GET_IC_VER 0x01U
|
||||
#define CH395F_CMD_GET_CMD_STATUS 0x2CU
|
||||
#define CH395F_CMD_GET_GLOB_INT_STATUS 0x29U
|
||||
#define CH395F_CMD_GET_PHY_STATUS 0x26U
|
||||
#define CH395F_CMD_GET_DHCP_STATUS 0x42U
|
||||
|
||||
@@ -119,7 +117,7 @@ extern "C" {
|
||||
* 由 net_init 经 ch395f_set_tcp_mss() 下发(须在 ch395f_init 之前);此处为单一真值源。
|
||||
* 维持 1024:8 Socket 共享 24KB 缓冲时,单 Socket 接收缓冲 = 2×MSS = 2KB(手册 §9.2.8
|
||||
* 满足上限的最大值)。MSS=512 裕量方案未采纳;当年支撑该决策的台架判据不可信,
|
||||
* 已随 TC-NET-604 结案为非缺陷一并撤回(2026-09-23,见 Trap 21)——维持 1024 的
|
||||
* 已随 TC-NET-604 结案为非缺陷一并撤回(2026-09-23)——维持 1024 的
|
||||
* 现有理由 = 手册允许的最大值、满缓冲周期段数少。MSS 只影响芯片分包与窗口动态,
|
||||
* 与下方缓冲块布局解耦。
|
||||
*/
|
||||
@@ -136,16 +134,17 @@ extern "C" {
|
||||
#define CH395F_RECV_BLOCKS 4U
|
||||
#define CH395F_SEND_BLOCKS 2U
|
||||
/* 单 Socket 硬件收发缓冲字节数(= 块数 × 块大小)。写入发送 FIFO 的单次长度严禁超过此值,
|
||||
* 否则会写穿有限的发送缓冲(见 Trap 20)。 */
|
||||
* 否则会写穿有限的发送缓冲(见 TRAP-NET-16)。 */
|
||||
#define CH395F_SEND_BUF_SIZE_BYTES (CH395F_SEND_BLOCKS * CH395F_RAM_BLOCK_SIZE)
|
||||
#define CH395F_RECV_BUF_SIZE_BYTES (CH395F_RECV_BLOCKS * CH395F_RAM_BLOCK_SIZE)
|
||||
|
||||
/*
|
||||
* SPI DMA 事务限制
|
||||
* CH395F_SPI_DMA_MAX_PAYLOAD: 单次 DMA 事务最大有效载荷(总缓冲 - 4 字节命令头)
|
||||
* 接收方向需一次性读空整个 Socket 接收缓冲(= 接收缓冲大小,避免 Trap 13 部分读
|
||||
* 导致剩余数据不可见),故总缓冲 = 接收缓冲大小 + 4(4 为命令头开销),
|
||||
* 有效载荷 = 接收缓冲大小(2KB)。
|
||||
* 缓冲按"一条命令读空整个 Socket 接收缓冲(2KB)"定容——排空契约(TRAP-NET-20,
|
||||
* bit3=1 残数据由 MCU 读出)的形态本身就是整缓冲单读。
|
||||
* (旧解释"部分读致 recv_len 归零、剩余数据不可见"未证实且与手册 §5.34 相左,
|
||||
* 定容不依赖该解释;结案见 `CH395F_Trap_Records.md` 台架纪律。)
|
||||
* 2026-09-22:与 MSS 解耦(此前 = 2×MSS+4,MSS 下调会把 DMA 缓冲缩到装不下整缓冲
|
||||
* → 整缓冲读会被截断丢弃)。
|
||||
*/
|
||||
@@ -160,11 +159,9 @@ extern "C" {
|
||||
#define CH395F_CMD_SET_KEEPALIVE_INTVL 0x57U
|
||||
#define CH395F_CMD_SET_KEEPALIVE_CNT 0x58U
|
||||
#define CH395F_CMD_SET_KEEPALIVE_SN 0x59U
|
||||
#define CH395F_CMD_EEPROM_ERASE 0xE9U
|
||||
#define CH395F_CMD_EEPROM_WRITE 0xEAU
|
||||
#define CH395F_CMD_EEPROM_READ 0xEBU
|
||||
#define CH395F_CMD_READ_GPIO_REG 0xECH
|
||||
#define CH395F_CMD_WRITE_GPIO_REG 0xEDU
|
||||
/* EEPROM(0xE9..0xEB)/GPIO(0xEC/0xED)/SLEEP(0x03) 命令宏与
|
||||
* GET_GLOB_INT_STATUS(0x29,只有 2 字节版 0x19 有实现) 死声明已删除——
|
||||
* 全仓零引用已核实;需要时按手册 §5.x 重新登记 */
|
||||
|
||||
/*
|
||||
* CH395 命令执行状态码(CMD_GET_CMD_STATUS 返回值,与芯片手册一致)
|
||||
@@ -199,10 +196,13 @@ extern "C" {
|
||||
#define CH395F_PHY_AUTO_NEGOTIATE 0x20U /* 自动协商 */
|
||||
|
||||
/*
|
||||
* 功能参数标志位(CMD_SET_FUN_PARA)
|
||||
* 功能参数标志位(CMD_SET_FUN_PARA,§5.52 表)
|
||||
* bit3 SOCKET_CLOSE 需芯片版本 ≥0x46(版本门控见 net_init);
|
||||
* bit1 TCP_SERVER 多连接模式——本项目单连接模型不用(红线,勿置 1);
|
||||
* bit0 保留必须为 0(TRAP-NET-15 定案更正:0x09→0x08)
|
||||
*/
|
||||
#define CH395F_FUN_PARA_TCP_SERVER (1UL << 1) /* TCP Server 多连接模式 */
|
||||
#define CH395F_FUN_PARA_SOCKET_CLOSE (1UL << 3) /* Socket 关闭控制(0=芯片自动,1=外部控制) */
|
||||
#define CH395F_FUN_PARA_SOCKET_CLOSE (1UL << 3) /* Socket 关闭控制(0=芯片自动,1=外部控制;TRAP-NET-20 排空契约) */
|
||||
#define CH395F_FUN_PARA_DISABLE_SEND_OK (1UL << 4) /* 禁用 SEND_OK 中断 */
|
||||
|
||||
/*
|
||||
@@ -235,16 +235,15 @@ extern "C" {
|
||||
#define CH395F_TCP_TIME_WAIT 0x0AU
|
||||
|
||||
/*
|
||||
* 全局中断状态位(CMD_GET_GLOB_INT_STATUS 1字节版,仅Socket 0~3)
|
||||
* 全局中断状态位低字节(bit0~3 全局事件;§5.16——2 字节版
|
||||
* GET_GLOB_INT_STATUS_ALL 低字节同语义。Socket 事件位(bit4 起)由
|
||||
* net_poll 按 1U<<(i+4) 计算,旧 SOCK0..3 单列宏已删(
|
||||
* 全仓零引用;1 字节版命令 0x29 亦已随死声明清理)
|
||||
*/
|
||||
#define CH395F_GINT_STAT_UNREACH 0x01U
|
||||
#define CH395F_GINT_STAT_IP_CONFLI 0x02U
|
||||
#define CH395F_GINT_STAT_PHY_CHANGE 0x04U
|
||||
#define CH395F_GINT_STAT_DHCP 0x08U
|
||||
#define CH395F_GINT_STAT_SOCK0 0x10U
|
||||
#define CH395F_GINT_STAT_SOCK1 0x20U
|
||||
#define CH395F_GINT_STAT_SOCK2 0x40U
|
||||
#define CH395F_GINT_STAT_SOCK3 0x80U
|
||||
|
||||
/*
|
||||
* 全局中断状态位(CMD_GET_GLOB_INT_STATUS_ALL 2字节版,支持Socket 0~7)
|
||||
@@ -308,7 +307,7 @@ uint8_t ch395f_write_data(uint8_t data);
|
||||
uint8_t ch395f_read_data(void);
|
||||
|
||||
/*
|
||||
* 函数功能:查询 SPI 通道故障锁存(E-34 / P2-2)
|
||||
* 函数功能:查询 SPI 通道故障锁存
|
||||
* 返回值:0 - 正常 1 - 曾发生 SPI 超时/错误(字节级或 DMA 级)
|
||||
* 限定条件:无
|
||||
* 函数说明:锁存式(MCU 复位前一直有效);上层据此判定"SPI 通道异常"
|
||||
@@ -377,21 +376,23 @@ ch395f_status_t ch395f_init(void);
|
||||
*/
|
||||
|
||||
/*
|
||||
* 函数功能:设置 CH395 IP 地址(低字节在前)
|
||||
* 函数功能:设置 CH395 IP 地址(点分十进制序:p_ip[0]=第一段,如 192;
|
||||
* 字节序曾误注为"低字节在前"——调用方 parse_ip_string 产出
|
||||
* 点分数组直传、芯片按首段在前接受,台架多年 IP 生效实证)
|
||||
* 入口参数:p_ip - 4 字节 IP 地址指针 uint8_t*
|
||||
* 限定条件:指针非空,芯片已初始化
|
||||
*/
|
||||
void ch395f_set_ip_addr(uint8_t *p_ip);
|
||||
|
||||
/*
|
||||
* 函数功能:设置网关 IP 地址(低字节在前)
|
||||
* 函数功能:设置网关 IP 地址(点分十进制序,同 ch395f_set_ip_addr)
|
||||
* 入口参数:p_gwip - 4 字节网关 IP 指针 uint8_t*
|
||||
* 限定条件:指针非空,芯片已初始化
|
||||
*/
|
||||
void ch395f_set_gwip_addr(uint8_t *p_gwip);
|
||||
|
||||
/*
|
||||
* 函数功能:设置子网掩码(低字节在前)
|
||||
* 函数功能:设置子网掩码(点分十进制序,同 ch395f_set_ip_addr)
|
||||
* 入口参数:p_mask - 4 字节子网掩码指针 uint8_t*
|
||||
* 限定条件:指针非空,芯片已初始化
|
||||
*/
|
||||
@@ -480,6 +481,8 @@ void ch395f_set_proto_type(uint8_t sock, uint8_t proto);
|
||||
* 入口参数:sock - Socket 索引 uint8_t 0 - 7
|
||||
* p_ip - 4 字节目标 IP 指针 uint8_t*
|
||||
* 限定条件:指针非空
|
||||
* 函数说明:字节序与 ch395f_get_remot_ipp 输出一致(net_connect 直传其输出,
|
||||
* 台架出站连接往返实证)
|
||||
*/
|
||||
void ch395f_set_des_ip(uint8_t sock, uint8_t *p_ip);
|
||||
|
||||
@@ -577,6 +580,12 @@ uint16_t ch395f_get_recv_len(uint8_t sock);
|
||||
* 函数功能:获取全局中断状态(2 字节版,支持 Socket 0~7)
|
||||
* 返回值:中断状态 uint16_t(低字节=Socket 0~3 + PHY/DHCP,高字节=Socket 4~7 + DHCPv6)
|
||||
* 限定条件:芯片已初始化
|
||||
* ⚠ 护栏(F-NET-7,I7 不变量):本寄存器**读即清**且含全部 Socket 事件位——
|
||||
* 任何未配对 SINT 消费的读取都会吞掉其它 Socket 的事件通知。
|
||||
* **唯一读者 = net_poll(net_socket.c);测试/台架代码同样禁自行读取**
|
||||
* (用户裁定 2026-10-13)——等事件一律直读本 Socket ch395f_get_sock_int_status()。
|
||||
* 唯一例外:被测对象就是本 API 的板级用例(寄存器断言 / Phase 12 L1
|
||||
* 时延基准,硬件层独占上下文、无并发消费者)。
|
||||
*/
|
||||
uint16_t ch395f_get_glob_int_status_all(void);
|
||||
|
||||
@@ -743,6 +752,112 @@ void ch395f_set_ttl(uint8_t sock, uint8_t ttl);
|
||||
*/
|
||||
void ch395f_get_unreach_info(uint8_t *p_buf);
|
||||
|
||||
/*
|
||||
* 函数声明区 - 自愈驱动原语(设计 docs/网络/网络自愈架构设计.md §4.4;
|
||||
* 形态定案锚点 同文档 §5.2 E2/E3/E4)
|
||||
* E2 实测:单次状态回读 18.1us;LISTEN 可见延迟 100% <=10ms。
|
||||
*/
|
||||
|
||||
/* 类型化状态回读 + 每 Socket 缓存(F-DRV-1/3) */
|
||||
#define CH395F_SOCK_STATE_NONE 0xFFU /* open=CLOSED 时 tcp_state 哨兵("无意义") */
|
||||
|
||||
typedef struct {
|
||||
uint8_t open; /* byte0:CH395F_SOCKET_CLOSED / CH395F_SOCKET_OPEN */
|
||||
uint8_t tcp_state; /* byte1:CH395F_TCP_*;open=CLOSED 时为 CH395F_SOCK_STATE_NONE */
|
||||
} ch395f_sock_state_t;
|
||||
|
||||
/*
|
||||
* 函数功能:主动回读本 Socket 状态并刷新驱动缓存(缓存刷新点之一,另一个
|
||||
* 是 net_poll 每轮全量刷新)
|
||||
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
|
||||
* 返回值:类型化状态;sn>7 返回 {CLOSED, 哨兵} 并计一次边界 fault uint8_t×2
|
||||
* 限定条件:芯片已初始化;netTask 上下文(与其它 CH395F 事务串行)
|
||||
*/
|
||||
ch395f_sock_state_t ch395f_get_sock_state(uint8_t sn);
|
||||
|
||||
/*
|
||||
* 函数功能:读驱动缓存(零 SPI 成本;数据新鲜度取决于刷新点,见上)
|
||||
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
|
||||
* 返回值:最近一次刷新的状态;sn>7 返回 {CLOSED, 哨兵}
|
||||
*/
|
||||
ch395f_sock_state_t ch395f_sock_state_cached(uint8_t sn);
|
||||
|
||||
/*
|
||||
* 函数功能:判"本 Socket 正在监听"(读缓存:open=OPEN 且 TCP=LISTEN)
|
||||
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
|
||||
* 返回值:1=在听 0=不在 int
|
||||
*/
|
||||
int ch395f_sock_is_listening(uint8_t sn);
|
||||
|
||||
/*
|
||||
* 函数功能:事件护栏——只读本 Socket SINT(0x30,读即清),永不触碰全局寄存器
|
||||
* (F-NET-7 收窄:两级预清"先读全局再读本 socket"会清掉其它 Socket 的
|
||||
* 事件位;全局寄存器唯一读者=net_poll 写成驱动侧契约)
|
||||
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
|
||||
* 返回值:SINT 位图;sn>7 返回 0 uint8_t
|
||||
*/
|
||||
uint8_t ch395f_sock_events(uint8_t sn);
|
||||
|
||||
/* 复合复位模式(F-DRV-4;两模式都必须排 RX——E4 定案,差异只剩最短等待) */
|
||||
typedef enum {
|
||||
CH395F_RESET_GRACEFUL = 0, /* 最短等待 100ms(TRAP-NET-18 二阶段实测值) */
|
||||
CH395F_RESET_QUARANTINE /* 最短等待 400ms */
|
||||
} ch395f_reset_mode_t;
|
||||
|
||||
/*
|
||||
* 函数功能:启动 Socket 复合复位(非阻塞状态机,每 net_poll 步进;内部序列
|
||||
* E4 定案:读空 RX -> CLOSE -> 排空事件(quiescent) -> 轮询 byte0=00H)
|
||||
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
|
||||
* mode - 复位模式 ch395f_reset_mode_t
|
||||
* 返回值:0=已启动 -1=参数错或已在运行(调用方逻辑错误) int
|
||||
* 限定条件:netTask 上下文;调用前本层已放弃该会话(复位会丢弃 RX 残渣)
|
||||
*/
|
||||
int ch395f_sock_reset_begin(uint8_t sn, ch395f_reset_mode_t mode);
|
||||
|
||||
/*
|
||||
* 函数功能:复位状态机步进(在 net_poll 或测试循环里周期调用,禁忙等复制)
|
||||
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
|
||||
* 返回值:0=完成(含未启动,幂等) 1=进行中 -1=失败(超时/参数,槽位已复位可重开) int
|
||||
*/
|
||||
int ch395f_sock_reset_poll(uint8_t sn);
|
||||
|
||||
/* 命令完成判据(F-DRV-8;E3 定案=状态回读,GET_CMD_STATUS 对这三条无判别力) */
|
||||
typedef enum {
|
||||
CH395F_DONE_OPEN = 0, /* open 完成 = byte0 变 05H */
|
||||
CH395F_DONE_LISTEN, /* listen 完成 = byte0=05H 且 TCP=01H(E2:<=10ms 可见) */
|
||||
CH395F_DONE_CLOSE /* close 完成 = byte0=00H */
|
||||
} ch395f_done_what_t;
|
||||
|
||||
/*
|
||||
* 函数功能:非阻塞命令完成查询(单次主动回读 18.1us,E2 背书;勿复制
|
||||
* poll_cmd_status 式忙等——E3 实证其对 OPEN/LISTEN/CLOSE 恒白等且无判别力)
|
||||
* 入口参数:sn - Socket 索引 uint8_t 0 - 7
|
||||
* what - 完成判据 ch395f_done_what_t
|
||||
* 返回值:1=已完成 0=未完成 -1=参数错 int
|
||||
*/
|
||||
int ch395f_cmd_done(uint8_t sn, ch395f_done_what_t what);
|
||||
|
||||
/* 链路态缓存(F-DRV-13;net_poll 读全局后喂 bit0~3) */
|
||||
|
||||
/*
|
||||
* 函数功能:喂全局中断位(net_poll 读 GET_GLOB_INT_STATUS_ALL 后调用;
|
||||
* bit2 PHY_CHANGE 置位时驱动重读 PHY 更新 link_up 缓存 + 变化计数)
|
||||
* 入口参数:glob_bits - 全局中断状态低字节语义位图 uint16_t
|
||||
*/
|
||||
void ch395f_glob_event_feed(uint16_t glob_bits);
|
||||
|
||||
/*
|
||||
* 函数功能:读链路态缓存(首次查询惰性读 PHY 建档)
|
||||
* 返回值:1=link up 0=down uint8_t(判据 B9:PHY != DISCONN 即 up)
|
||||
*/
|
||||
uint8_t ch395f_link_up_cached(void);
|
||||
|
||||
/*
|
||||
* 函数功能:读链路变化计数(net 层订阅用:计数变化 -> 日志/可选重配)
|
||||
* 返回值:自建档以来的 PHY 连接状态翻转次数 uint16_t
|
||||
*/
|
||||
uint16_t ch395f_link_change_count(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
/* 头文件包含区 */
|
||||
#include <string.h>
|
||||
#include "gd5f2gq5ue.h"
|
||||
#include "spi.h" /* SPI1 句柄由 Inc/spi.h 统一导出,本文件不再重复 extern(2026-10-22 分层纠偏) */
|
||||
|
||||
/*
|
||||
* 调试输出配置
|
||||
@@ -43,11 +44,15 @@ static uint8_t s_bbt_page_buf[GD5F_BBT_HDR_SIZE + GD5F_BBT_SIZE]; /* BBT 读
|
||||
static uint8_t s_bbt_best[GD5F_BBT_SIZE]; /* 最高版本位图缓存 */
|
||||
static const uint8_t s_bbt_magic[4] = { 'G', 'B', 'B', 'T' }; /* BBT 存储魔数 "GBBT" */
|
||||
|
||||
/* 外部 SPI 句柄声明 */
|
||||
extern SPI_HandleTypeDef hspi1;
|
||||
|
||||
/* DMA 完成标志(ch395f.c 中的 HAL_SPI_TxRxCpltCallback 会设置此标志) */
|
||||
volatile uint8_t g_spi1_dma_done;
|
||||
/* DMA 完成标志(本模块私有,2026-10-22 收编):
|
||||
* HAL 的 SPI 完成/错误回调是**全局唯一符号**(整个工程只能有一份实现),当前由
|
||||
* Drivers/BSP/CH395F/ch395f.c 实现并按 hspi->Instance 分发——SPI1 分支必须转调本
|
||||
* 模块导出的 gd5f_spi1_notify_done() / gd5f_spi1_notify_error(),不得反过来由
|
||||
* CH395F 直接写本模块的变量(原先是跨模块裸 extern,删掉那两行不会报错,只会让
|
||||
* 下面的 DMA 等待循环在出错时退化成干等 GD5F_SPI_TIMEOUT)。
|
||||
*/
|
||||
static volatile uint8_t s_spi1_dma_done;
|
||||
|
||||
/* DMA 阈值(字节数超过此值使用 DMA 传输):取值定义在头文件,理由如下——
|
||||
* 阈值以下走 CPU 轮询。存在把**栈上小缓冲**直接传进本驱动做收发的大小
|
||||
@@ -57,7 +62,7 @@ volatile uint8_t g_spi1_dma_done;
|
||||
* 源/目的全部为文件作用域静态缓冲。 */
|
||||
|
||||
/* DMA 可达内存窗口 GD5F_DMA_RAM_BASE/END 与 CCM 窗口常量已上移到 gd5f2gq5ue.h
|
||||
* (N-26,2026-10-12:nand_ftl.c 的 init 期自检也要用),并带编译期守卫
|
||||
* (DMA 可达性三层防线,2026-10-12:nand_ftl.c 的 init 期自检也要用),并带编译期守卫
|
||||
* (白名单窗口与 CCM 永不相交、阈值契约)。此处只留语义提醒:
|
||||
* 窗口必须与分散加载 RW_IRAM1(0x20000000,128KB,SRAM1+SRAM2)一致;
|
||||
* CCM(0x10000000,64KB)DMA1/DMA2 物理不可达,RW_CCM 区任何缓冲都不得进 DMA。
|
||||
@@ -69,11 +74,11 @@ volatile uint8_t g_spi1_dma_done;
|
||||
* 入口参数:hspi - 触发回调的 SPI 句柄 SPI_HandleTypeDef*
|
||||
* 返回值:无
|
||||
* 限定条件:由 HAL DMA 中断调用
|
||||
* 函数说明:仅当 SPI1 传输完成时置位 g_spi1_dma_done 标志
|
||||
* 函数说明:仅当 SPI1 传输完成时置位 s_spi1_dma_done 标志
|
||||
*/
|
||||
void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
|
||||
if (hspi->Instance == SPI1) {
|
||||
g_spi1_dma_done = 1U;
|
||||
s_spi1_dma_done = 1U;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,10 +90,32 @@ void HAL_SPI_TxCpltCallback(SPI_HandleTypeDef *hspi) {
|
||||
*/
|
||||
void HAL_SPI_RxCpltCallback(SPI_HandleTypeDef *hspi) {
|
||||
if (hspi->Instance == SPI1) {
|
||||
g_spi1_dma_done = 1U;
|
||||
s_spi1_dma_done = 1U;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:SPI1 DMA 完成通知(供实际注册 HAL 回调的一方转调)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:由 HAL SPI 回调(中断上下文)调用;只写一个标志,不阻塞
|
||||
* 函数说明:HAL 的 SPI 回调在整个工程里只能有一份实现,本模块不抢占 Error 回调
|
||||
* 的所有权,只导出这两个通知入口。**转调方不得省略本调用**——少了它,
|
||||
* 出错路径只能靠 GD5F_SPI_TIMEOUT 超时退出(能自保但会静默变慢)。
|
||||
*/
|
||||
void gd5f_spi1_notify_done(void) {
|
||||
s_spi1_dma_done = 1U;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:SPI1 DMA 错误通知(释放等待者;语义与 notify_done 相同)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:由 HAL SPI 错误回调(中断上下文)调用
|
||||
*/
|
||||
void gd5f_spi1_notify_error(void) {
|
||||
s_spi1_dma_done = 1U;
|
||||
}
|
||||
/* ======================== 私有函数声明 ======================== */
|
||||
|
||||
static int gd5f_page_program(uint32_t page_addr, uint16_t column,
|
||||
@@ -103,7 +130,7 @@ static int gd5f_private_bbt_find_best(void);
|
||||
static int gd5f_bbt_load(void);
|
||||
static int gd5f_bbt_save(void);
|
||||
|
||||
/* DMA 可达性守卫:实现见下方(N-26 起为公开 API,声明在 gd5f2gq5ue.h) */
|
||||
/* DMA 可达性守卫:实现见下方(随 DMA 可达性三层防线升为公开 API,声明在 gd5f2gq5ue.h) */
|
||||
|
||||
/* ======================== DMA 可达性守卫 ======================== */
|
||||
|
||||
@@ -143,16 +170,16 @@ int gd5f_dma_buf_ok(const void *p_buf, size_t size) {
|
||||
/*
|
||||
* 函数功能:等待芯片操作完成(轮询 OIP 位)
|
||||
* 入口参数:timeout_ms - 超时时间 uint32_t > 0
|
||||
* 返回值:GD5F_OK(0) - 操作完成;GD5F_BUSY_TIMEOUT(-2) - 超时(可重试);
|
||||
* 返回值:GD5F_OK(0) - 操作完成;GD5F_WAIT_EXPIRED(-2) - 超时(可重试);
|
||||
* GD5F_ERROR(-1) - SPI 传输真故障(不可重试,须上报自检)
|
||||
* 限定条件:SPI 已初始化
|
||||
* 函数说明:循环读取状态寄存器直到 OIP 位清零或超时。
|
||||
* E-53(2026-10-03,GD5F 错误通道统一):三处 HAL 调用的返回值**必须**检查
|
||||
* (2026-10-03,GD5F 错误通道统一):三处 HAL 调用的返回值**必须**检查
|
||||
* ——原实现全部丢弃,失败时 `status` 保持初值 0 → `(status & OIP) == 0`
|
||||
* 立即成立 → **把"操作完成"伪造出来**,后续命令在未就绪的芯片上执行,
|
||||
* 最终表现为"静默报成功但数据没落盘"。现区分两类失败:
|
||||
* ① SPI 层失败(HAL≠OK)→ GD5F_ERROR(真故障);
|
||||
* ② OIP 长时间不落 → GD5F_BUSY_TIMEOUT(可重试)。
|
||||
* ② OIP 长时间不落 → GD5F_WAIT_EXPIRED(可重试)。
|
||||
* 调用点据此分级:超时重试 / 真故障上报自检并降级(见审计 §5 第二档 #1)。
|
||||
*/
|
||||
/* 看门狗心跳弱钩子:默认空实现(无看门狗/模拟器构建);强符号由
|
||||
@@ -190,7 +217,7 @@ int32_t gd5f_wait_busy(uint32_t timeout_ms) {
|
||||
GD5F_CS_HIGH();
|
||||
|
||||
if (hs != HAL_OK) {
|
||||
/* E-53:SPI 真故障——绝不伪造成"操作完成"(详见函数说明) */
|
||||
/* SPI 真故障——绝不伪造成"操作完成"(详见函数说明) */
|
||||
return GD5F_ERROR;
|
||||
}
|
||||
|
||||
@@ -198,7 +225,7 @@ int32_t gd5f_wait_busy(uint32_t timeout_ms) {
|
||||
return GD5F_OK;
|
||||
}
|
||||
if ((HAL_GetTick() - tick_start) >= timeout_ms) {
|
||||
return GD5F_BUSY_TIMEOUT;
|
||||
return GD5F_WAIT_EXPIRED;
|
||||
}
|
||||
if ((uint32_t)(HAL_GetTick() - last_beat) >= 100u) {
|
||||
last_beat = HAL_GetTick();
|
||||
@@ -271,7 +298,7 @@ static void gd5f_page_read_issue(uint32_t page_addr) {
|
||||
* 函数说明:13h 单次发令 + WIP 轮询(与 Linux 主线 / GigaDevice 厂商驱动
|
||||
* 形状完全一致:13h×1 → OIP 轮询 → 0Bh)。
|
||||
* 2026-09-20 台架异常史实(归因、证伪、沉降/二次发令的加与除)
|
||||
* 统一记于 docs/问题记录/GD5F2GQ5UE_Trap_Records.md Trap 05,
|
||||
* 统一记于 docs/问题记录/GD5F2GQ5UE_Trap_Records.md TRAP-GD5F-05,
|
||||
* 复现手段见测试 Phase 11 用例。
|
||||
*/
|
||||
int32_t gd5f_page_read(uint32_t page_addr) {
|
||||
@@ -319,16 +346,16 @@ int32_t gd5f_read_from_cache(uint16_t column, uint8_t *p_buf,
|
||||
|
||||
if (use_dma != 0U) {
|
||||
uint32_t tick_start = HAL_GetTick();
|
||||
g_spi1_dma_done = 0U;
|
||||
s_spi1_dma_done = 0U;
|
||||
if (HAL_SPI_Receive_DMA(&hspi1, p_buf, (uint16_t)size) != HAL_OK) {
|
||||
GD5F_CS_HIGH();
|
||||
return GD5F_ERROR;
|
||||
}
|
||||
while (g_spi1_dma_done == 0U) {
|
||||
while (s_spi1_dma_done == 0U) {
|
||||
if ((HAL_GetTick() - tick_start) >= GD5F_SPI_TIMEOUT) {
|
||||
HAL_SPI_DMAStop(&hspi1);
|
||||
GD5F_CS_HIGH();
|
||||
return GD5F_BUSY_TIMEOUT;
|
||||
return GD5F_WAIT_EXPIRED;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -400,17 +427,17 @@ int32_t gd5f_program_load(uint16_t column, const uint8_t *p_buf, size_t size) {
|
||||
|
||||
if (use_dma != 0U) {
|
||||
uint32_t tick_start = HAL_GetTick();
|
||||
g_spi1_dma_done = 0U;
|
||||
s_spi1_dma_done = 0U;
|
||||
if (HAL_SPI_Transmit_DMA(&hspi1, (uint8_t *)p_buf, (uint16_t)size)
|
||||
!= HAL_OK) {
|
||||
GD5F_CS_HIGH();
|
||||
return GD5F_ERROR;
|
||||
}
|
||||
while (g_spi1_dma_done == 0U) {
|
||||
while (s_spi1_dma_done == 0U) {
|
||||
if ((HAL_GetTick() - tick_start) >= GD5F_SPI_TIMEOUT) {
|
||||
HAL_SPI_DMAStop(&hspi1);
|
||||
GD5F_CS_HIGH();
|
||||
return GD5F_BUSY_TIMEOUT;
|
||||
return GD5F_WAIT_EXPIRED;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -473,8 +500,8 @@ int32_t gd5f_check_ecc(void) {
|
||||
* 返回值:GD5F_OK - 成功;其它 - SPI 传输/等待失败错误码
|
||||
* 限定条件:SPI 已初始化
|
||||
* 函数说明:先写使能,发 1Fh + 地址 + 数据,等待操作完成。
|
||||
* E-53(2026-10-03):HAL 传输与 wait_busy 的返回值必须检查并上报——
|
||||
* 原实现恒返回 GD5F_OK,"配置未生效"对上层不可见(E-10 同族问题)。
|
||||
* (2026-10-03):HAL 传输与 wait_busy 的返回值必须检查并上报——
|
||||
* 原实现恒返回 GD5F_OK,"配置未生效"对上层不可见(同族问题)。
|
||||
*/
|
||||
static int gd5f_set_feature(uint8_t addr, uint8_t data) {
|
||||
uint8_t cmd[3];
|
||||
@@ -507,10 +534,10 @@ static int gd5f_set_feature(uint8_t addr, uint8_t data) {
|
||||
* 出口参数:p_data - 读到的字节输出指针 uint8_t* 不为 NULL
|
||||
* 返回值:GD5F_OK - 成功;GD5F_ERROR - SPI 传输失败
|
||||
* 限定条件:SPI 已初始化
|
||||
* 函数说明:E-10(2026-10-03):init 需**回读** B0h 校验 ECC 使能位真正生效
|
||||
* 函数说明:(2026-10-03):init 需**回读** B0h 校验 ECC 使能位真正生效
|
||||
* (原实现只写不读,"init 返回 OK 但 ECC 未打开"→ 存储栈在无 ECC
|
||||
* 保护下静默降级)。与 gd5f_read_status 同构(后者固定 C0h),
|
||||
* 此处参数化地址;同样检查 HAL 返回值(E-53)。
|
||||
* 此处参数化地址;同样检查 HAL 返回值。
|
||||
*/
|
||||
static int gd5f_private_get_feature(uint8_t addr, uint8_t *p_data) {
|
||||
uint8_t cmd = GD5F_CMD_GET_FEATURE;
|
||||
@@ -573,7 +600,7 @@ int32_t gd5f_block_erase(uint32_t block_addr) {
|
||||
* 限定条件:SPI 已初始化,ECC 尚未使能
|
||||
* 函数说明:读取每个块首页(page 0)的 spare byte 0,
|
||||
* 若不为 0xFF 则为出厂坏块(符合 datasheet §12.4)。
|
||||
* E-42(2026-10-03,GD5F 错误通道统一):原实现丢弃 page_read /
|
||||
* (2026-10-03,GD5F 错误通道统一):原实现丢弃 page_read /
|
||||
* read_from_cache 的返回值且对外恒返回 GD5F_OK——SPI 读失败时
|
||||
* `spare[8]` 保持上一轮的**陈旧内容**,且"整片扫描全失败也返回成功",
|
||||
* 上层以为坏块表可用(rebuild 路径还会把错表持久化覆盖好副本)。
|
||||
@@ -632,26 +659,58 @@ void gd5f2gq5ue_bbt_clear(void) {
|
||||
* 返回值:GD5F_OK - 成功,其他 - 错误码
|
||||
* 限定条件:gd5f2gq5ue_init() 已成功调用
|
||||
* 函数说明:临时关闭 ECC 重新扫描每块 page0 spare[0] 的出厂坏块标记,覆盖 RAM BBT,
|
||||
* 再写回持久化 BBT 池(版本 +1,掉电安全),从而清除 TC-STO-03 等运行时
|
||||
* 再写回持久化 BBT 池(版本 +1,掉电安全),从而清除 TC-STORAGE-03 等运行时
|
||||
* 标记的坏块。调用后建议再执行 nand_ftl_format() 清空 dhara map。
|
||||
*/
|
||||
int32_t gd5f2gq5ue_bbt_rebuild(void) {
|
||||
int ret;
|
||||
int restore_ret;
|
||||
uint8_t feat = 0;
|
||||
|
||||
DBG_INFO("Rebuilding BBT from factory scan...");
|
||||
|
||||
/* 1. 关闭 ECC 以读取原始出厂坏块标记(与 gd5f2gq5ue_init 的扫描前提一致) */
|
||||
gd5f_set_feature(0xB0, 0x00);
|
||||
gd5f_wait_busy(100);
|
||||
/* 1. 关闭 ECC 以读取原始出厂坏块标记(与 gd5f2gq5ue_init 的扫描前提一致)。
|
||||
* 2026-10-23(bsp-err 的 consume-result 检查上线当天照出来的一处真窟窿):这两行原是**裸调用**——关 ECC 失败就是没关掉,扫描会在
|
||||
* ECC 语义下读 spare,把厂标好块误判成坏块,"重建 BBT"反而制造坏块。 */
|
||||
ret = gd5f_set_feature(0xB0, 0x00);
|
||||
if (ret != GD5F_OK) {
|
||||
DBG_ERROR("BBT rebuild: ECC off failed (%d), aborted", ret);
|
||||
return ret;
|
||||
}
|
||||
if (gd5f_wait_busy(100) != GD5F_OK) {
|
||||
/* 命令发了但没等到完成 = 状态未知;宁可什么都不做(ECC 仍在,是安全侧) */
|
||||
DBG_ERROR("BBT rebuild: wait after ECC off failed, aborted");
|
||||
return GD5F_ERROR;
|
||||
}
|
||||
|
||||
/* 2. 重新扫描出厂坏块(覆盖 RAM BBT,剔除运行时注入的坏块) */
|
||||
ret = gd5f_bbt_scan();
|
||||
|
||||
/* 3. 重新使能 ECC(无论扫描成败都须恢复,否则后续 I/O 在无 ECC 下运行) */
|
||||
gd5f_set_feature(0xB0, 0x10);
|
||||
gd5f_wait_busy(100);
|
||||
/* 3. 重新使能 ECC:无论扫描成败都必须恢复,而且要**验证真的生效**
|
||||
* (2026-10-23,由 bsp-err 的 consume-result 检查照出)。原先这两行返回值全丢:SPI 一抖 ECC 就保持关闭,而
|
||||
* gd5f_check_ecc() 在 ECC 关闭时恒读"无 ECC 错"——整条链再没有任何信号,设备
|
||||
* 从此在无位翻转保护下读写却"一切正常"。init 路径有"写完回读 B0h 校验
|
||||
* ECC_EN 位",这里补同一套;ECC 没恢复比扫描失败更严重,所以优先返回它。 */
|
||||
restore_ret = gd5f_set_feature(0xB0, 0x10);
|
||||
if ((gd5f_wait_busy(100) != GD5F_OK) && (restore_ret == GD5F_OK)) {
|
||||
restore_ret = GD5F_WAIT_EXPIRED;
|
||||
}
|
||||
if (restore_ret == GD5F_OK) {
|
||||
restore_ret = gd5f_private_get_feature(GD5F_REG_FEATURE, &feat);
|
||||
}
|
||||
if ((restore_ret == GD5F_OK) && ((feat & GD5F_FEATURE_ECC_EN) == 0u)) {
|
||||
restore_ret = GD5F_ERROR; /* 写了但没生效,与写失败同罪 */
|
||||
}
|
||||
if (restore_ret != GD5F_OK) {
|
||||
DBG_ERROR("BBT rebuild: ECC restore FAILED (ret=%d B0h=0x%02X), NAND UNPROTECTED",
|
||||
restore_ret, feat);
|
||||
/* 没持久化成功就别让 RAM 留着"比盘上更新"的表:回读盘上副本,使 RAM 与
|
||||
* 下次上电必然看到的状态一致(否则本次运行按新表跑、重启又退回旧表)。 */
|
||||
(void)gd5f_bbt_load();
|
||||
return restore_ret;
|
||||
}
|
||||
|
||||
/* E-42(2026-10-03,数据安全优先):扫描失败 → **放弃持久化**(保留持久区
|
||||
/* (2026-10-03,数据安全优先):扫描失败 → **放弃持久化**(保留持久区
|
||||
* 既有好副本),并从盘上回读 BBT 恢复 RAM 视图。原实现无论扫描成败都照常
|
||||
* save(),会把"读失败拼出来的错表"覆盖掉正确副本。 */
|
||||
if (ret != GD5F_OK) {
|
||||
@@ -747,7 +806,7 @@ int32_t gd5f2gq5ue_init(void) {
|
||||
DBG_INFO("Scanning bad blocks...");
|
||||
ret = gd5f_bbt_scan();
|
||||
if (ret != GD5F_OK) {
|
||||
/* E-10:扫坏块失败 → init 失败,不静默继续(上层据此上报自检并降级) */
|
||||
/* 扫坏块失败 → init 失败,不静默继续(上层据此上报自检并降级) */
|
||||
DBG_ERROR("BBT scan failed: %d", ret);
|
||||
return ret;
|
||||
}
|
||||
@@ -758,7 +817,7 @@ int32_t gd5f2gq5ue_init(void) {
|
||||
DBG_ERROR("Enable ECC failed: %d", ret);
|
||||
return ret;
|
||||
}
|
||||
/* E-10:回读 B0h 校验 ECC 使能位(bit4)**真正生效**——原实现只写不读,
|
||||
/* 回读 B0h 校验 ECC 使能位(bit4)**真正生效**——原实现只写不读,
|
||||
* "init 返回 OK 但 ECC 未真正打开"会使存储栈在无 ECC 保护下静默降级。 */
|
||||
{
|
||||
uint8_t feat = 0;
|
||||
|
||||
@@ -62,7 +62,7 @@ extern "C" {
|
||||
/*
|
||||
* 页读取就绪判定 = WIP 轮询(与 Linux 主线 / GigaDevice 厂商驱动完全一致,
|
||||
* 单发令形状)。2026-09-20 沉降 / 13h 二次发令史实统一记于
|
||||
* GD5F2GQ5UE_Trap_Records.md Trap 05。
|
||||
* GD5F2GQ5UE_Trap_Records.md TRAP-GD5F-05。
|
||||
*/
|
||||
#define GD5F_TOTAL_BLOCKS 2048
|
||||
#define GD5F_TOTAL_SIZE (GD5F_TOTAL_BLOCKS * GD5F_BLOCK_SIZE)
|
||||
@@ -77,7 +77,7 @@ extern "C" {
|
||||
*/
|
||||
#define GD5F_DMA_THRESHOLD 1024
|
||||
|
||||
/* ==================== DMA 可达内存窗口(N-26,2026-10-12) ====================
|
||||
/* ==================== DMA 可达内存窗口(DMA 可达性三层防线,2026-10-12) ====================
|
||||
* DMA1/DMA2 物理不可达 CCM(0x10000000~0x1000FFFF):以 CCM 缓冲做 DMA 源/目的
|
||||
* 会静默不落地(2026-09-20 首轮 CCM 扩容实验实证根因;口径见
|
||||
* docs/内存管理/内存内容分配策略.md §2 与 §4 规则 R2/R3)。
|
||||
@@ -93,7 +93,7 @@ extern "C" {
|
||||
#define GD5F_CCM_RAM_BASE 0x10000000UL
|
||||
#define GD5F_CCM_RAM_END 0x10010000UL
|
||||
|
||||
/* 编译期守卫(N-26):AC5 无 _Static_assert(C11 不支持),用项目惯用的
|
||||
/* 编译期守卫(DMA 可达性三层防线一环):AC5 无 _Static_assert(C11 不支持),用项目惯用的
|
||||
* 负数组 typedef(同 App/cfg/cfg_file.c、Drivers/BSP/UI/ui_menu_tab.h)——
|
||||
* 条件不成立则数组尺寸 -1 → 编译错。
|
||||
* ① DMA 白名单窗口与 CCM 窗口**永不得重叠**(有人改上面常量时立即爆炸);
|
||||
@@ -116,7 +116,7 @@ typedef char gd5f_dma_threshold_contract_check_t[
|
||||
/* 返回值定义 */
|
||||
#define GD5F_OK 0
|
||||
#define GD5F_ERROR -1
|
||||
#define GD5F_BUSY_TIMEOUT -2
|
||||
#define GD5F_WAIT_EXPIRED -2 /* 旧名 GD5F_BUSY_TIMEOUT;timeout-name 检查:**返回码不许叫 *_TIMEOUT** */
|
||||
#define GD5F_ECC_ERROR -3
|
||||
#define GD5F_PROGRAM_FAIL -4
|
||||
#define GD5F_ERASE_FAIL -5
|
||||
@@ -160,7 +160,7 @@ int32_t gd5f2gq5ue_read_id(uint8_t *p_mid, uint8_t *p_did);
|
||||
* 返回值:GD5F_OK(0) - SPI 传输完成;其它 - 错误码
|
||||
* 限定条件:gd5f2gq5ue_init() 已成功调用
|
||||
* 函数说明:自动处理跨页读取,每次读取不超过当前页剩余空间。
|
||||
* ⚠ **raw 契约(E-56,2026-10-03 显式文档化)**:返回 0 只表示 **SPI 传输完成**,
|
||||
* ⚠ **raw 契约(2026-10-03 显式文档化)**:返回 0 只表示 **SPI 传输完成**,
|
||||
* **不代表数据无位翻转**——ECC 不可纠正页(ECCS 非零)在 SPI 层属正常完成,
|
||||
* 本函数仍返回 0。数据正确性由 **ECC 层**保证:生产主路径经 dhara glue
|
||||
* (`nand_ftl.c`)自有 ECC 检查保护;**直接使用本 raw API** 的调用方
|
||||
@@ -225,7 +225,7 @@ void gd5f2gq5ue_print_bbt(void);
|
||||
* 入口参数:无
|
||||
* 返回值:GD5F_OK - 成功,其他 - 错误码
|
||||
* 限定条件:gd5f2gq5ue_init() 已成功调用
|
||||
* 函数说明:用于测试(如 TC-STO-03 注入坏块后)或出厂重置后恢复。
|
||||
* 函数说明:用于测试(如 TC-STORAGE-03 注入坏块后)或出厂重置后恢复。
|
||||
* 临时关闭 ECC 重新扫描每块 page0 spare[0] 的出厂坏块标记,覆盖 RAM BBT,
|
||||
* 再写回持久化 BBT 池(版本 +1,掉电安全),从而清除运行时 mark 的坏块。
|
||||
* 调用后建议再执行 nand_ftl_format() 清空 dhara map。
|
||||
@@ -272,14 +272,89 @@ int32_t gd5f_bbt_reload(void);
|
||||
|
||||
/* ==================== SPI 原语(FTL 共享) ==================== */
|
||||
|
||||
/*
|
||||
* 函数功能:等待芯片空闲(轮询状态位 BUSY/OIP)
|
||||
* 入口参数:timeout_ms - 等待上限 uint32_t 毫秒
|
||||
* 返 回 值:GD5F_OK - 已空闲;GD5F_WAIT_EXPIRED - 时限内未空闲(**可重试**,旧名 BUSY_TIMEOUT);GD5F_ERROR - SPI 读取失败
|
||||
* 限定条件:SPI 已初始化;不得在中断里调用
|
||||
* 函数说明:把"超时"与"读状态本身失败"分开返回,两者处置不同(前者再等、后者查硬件)
|
||||
*/
|
||||
int32_t gd5f_wait_busy(uint32_t timeout_ms);
|
||||
|
||||
/*
|
||||
* 函数功能:发送 06h WRITE ENABLE(编程/擦除前必须成功执行一次)
|
||||
* 入口参数:无
|
||||
* 返 回 值:GD5F_OK - 成功;其它 - SPI 传输或等待失败错误码
|
||||
* 限定条件:SPI 已初始化
|
||||
* 函数说明:本函数返回值必须被检查并上报(错误通道统一纪律),忽略它会让后续 program/erase 静默无效
|
||||
*/
|
||||
int32_t gd5f_write_enable(void);
|
||||
|
||||
/*
|
||||
* 函数功能:读取状态寄存器(0Fh / READ STATUS)
|
||||
* 入口参数:p_status - 状态字节输出 uint8_t* 不得为 NULL
|
||||
* 返 回 值:GD5F_OK - 成功(*p_status 有效);GD5F_ERROR - SPI 失败(*p_status 不可信,调用方不得读用)
|
||||
* 限定条件:SPI 已初始化
|
||||
* 函数说明:同族纪律——失败时出参内容不可信,故判成败看返回值而不是看 *p_status 是否像样
|
||||
*/
|
||||
int32_t gd5f_read_status(uint8_t *p_status);
|
||||
|
||||
/*
|
||||
* 函数功能:页读取(13h:把数据从存储阵列装进内部缓存)
|
||||
* 入口参数:page_addr - 物理页地址 uint32_t 0 - GD5F_TOTAL_PAGES-1
|
||||
* 返 回 值:GD5F_OK - 命令已发出且等待完成;其它 - 透传底层 SPI/等待错误码
|
||||
* 限定条件:SPI 已初始化;**本函数成功不代表数据无位翻转**,正确性由随后的 gd5f_check_ecc() 判定
|
||||
* 函数说明:装载完成后须用 gd5f_read_from_cache() 取数据
|
||||
*/
|
||||
int32_t gd5f_page_read(uint32_t page_addr);
|
||||
|
||||
/*
|
||||
* 函数功能:从内部缓存读出数据(0Bh READ FROM CACHE)
|
||||
* 入口参数:column - 缓存内起始列 uint16_t 0 - GD5F_PAGE_SIZE-1
|
||||
* p_buf - 数据输出缓冲 uint8_t* 不得为 NULL
|
||||
* size - 读取字节数 uint32_t 1 - GD5F_PAGE_SIZE-column
|
||||
* 返 回 值:GD5F_OK - 成功;GD5F_ERROR - 参数非法或 SPI 失败(**此时 p_buf 内容不可信**)
|
||||
* 限定条件:必须先成功调用 gd5f_page_read() 完成阵列→缓存装载
|
||||
* 函数说明:同族——调用方不得在失败分支继续使用 p_buf(FTL 侧据此把读失败保守判为"非空页")
|
||||
*/
|
||||
int32_t gd5f_read_from_cache(uint16_t column, uint8_t *p_buf, uint32_t size);
|
||||
|
||||
/*
|
||||
* 函数功能:把数据装进内部缓存(02h PROGRAM LOAD)
|
||||
* 入口参数:column - 缓存内起始列 uint16_t 0 - GD5F_PAGE_SIZE-1
|
||||
* p_buf - 待写数据 const uint8_t* 不得为 NULL
|
||||
* size - 字节数 uint32_t 1 - GD5F_PAGE_SIZE-column
|
||||
* 返 回 值:GD5F_OK - 装载完成;GD5F_WAIT_EXPIRED - 装载后等待 OIP 超时(**可重试**);GD5F_ERROR - 参数/SPI 失败
|
||||
* 限定条件:先成功执行 gd5f_write_enable();缓冲须在 DMA 可达窗口内(GD5F_DMA_RAM_* 白名单)
|
||||
* 函数说明:装载本身不写阵列,真正的编程由 gd5f_program_exec() 触发
|
||||
*/
|
||||
int32_t gd5f_program_load(uint16_t column, const uint8_t *p_buf, uint32_t size);
|
||||
|
||||
/*
|
||||
* 函数功能:触发页编程(10h PROGRAM EXEC)并检查 P_FAIL
|
||||
* 入口参数:page_addr - 目标物理页地址 uint32_t 合法页号
|
||||
* 返 回 值:GD5F_OK - 编程完成且无 P_FAIL;**GD5F_PROGRAM_FAIL - 芯片置 P_FAIL(块退化的信号)**;其它 - 透传底层/SPI 错误
|
||||
* 限定条件:本调用前须已 gd5f_program_load() 成功
|
||||
* 函数说明:P_FAIL 是**器件自己认定这一页写不下去**的位,与 SPI 层错误性质不同(FTL 据此标坏块)
|
||||
*/
|
||||
int32_t gd5f_program_exec(uint32_t page_addr);
|
||||
|
||||
/*
|
||||
* 函数功能:擦除一个块(D8h 擦除序列 + 等待 + 检查 E_FAIL)
|
||||
* 入口参数:block_addr - 物理块地址 uint32_t 0 - GD5F_TOTAL_BLOCKS-1
|
||||
* 返 回 值:GD5F_OK - 擦净;**GD5F_ERASE_FAIL - 芯片置 E_FAIL(坏块信号)**;其它 - 透传底层/SPI 错误
|
||||
* 限定条件:先成功执行 gd5f_write_enable()
|
||||
* 函数说明:E_FAIL 与 SPI 错误的区别同 program_exec;擦除超时归 GD5F_WAIT_EXPIRED(可重试)
|
||||
*/
|
||||
int32_t gd5f_block_erase(uint32_t block_addr);
|
||||
|
||||
/*
|
||||
* 函数功能:检查上一次页读取的 ECC 结果(读 00h/BCh 特征寄存器的 ECC 状态)
|
||||
* 入口参数:无
|
||||
* 返 回 值:GD5F_OK - 无不可纠错误(可能含已纠正的位翻转);GD5F_ECC_ERROR - **存在不可纠正的 ECC 错误**
|
||||
* 限定条件:SPI 已初始化且 ECC 已使能(init 期扫描在使能前调用本函数无意义)
|
||||
* 函数说明:这是数据正确性的唯一判定点——SPI 层读到"内容"不等于"内容对"(见 §8.1 与本头"同族纪律"注释)
|
||||
*/
|
||||
int32_t gd5f_check_ecc(void);
|
||||
|
||||
/*
|
||||
@@ -288,7 +363,7 @@ int32_t gd5f_check_ecc(void);
|
||||
* size - 传输字节数 size_t
|
||||
* 返回值:1 - 整段 DMA 可达;0 - 不可达(含 size=0 / 地址回绕 / 越窗)
|
||||
* 限定条件:无
|
||||
* 函数说明:N-26(2026-10-12)由 gd5f2gq5ue.c 私有守卫升级为公开 API——
|
||||
* 函数说明:2026-10-12 由 gd5f2gq5ue.c 私有守卫升级为公开 API(DMA 可达性三层防线一环)——
|
||||
* 驱动内部每次 >阈值 传输前用它(不可达即回落 CPU 轮询),
|
||||
* FTL 层(nand_ftl.c)在 init 期用它自检静态页缓冲的落位。
|
||||
*/
|
||||
@@ -324,6 +399,19 @@ void nand_ftl_wd_beat(void);
|
||||
*/
|
||||
void nand_ftl_wd_feed(void);
|
||||
|
||||
/*
|
||||
* 函数功能:SPI1 DMA 完成 / 错误通知(由实际注册 HAL SPI 回调的模块转调)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:中断上下文(HAL SPI 回调内),两者只写一个完成标志
|
||||
* 函数说明:HAL 的 SPI 完成/错误回调是全局唯一符号,本模块只实现 TxCplt/RxCplt
|
||||
* 两个(供自己的收发用),Error 回调的所有权留给回调的实际注册方
|
||||
* (当前 = Drivers/BSP/CH395F/ch395f.c,按 hspi->Instance 分发)。**SPI1
|
||||
* 分支必须转调这两个函数**,否则本模块的 DMA 等待在出错时只能靠超时
|
||||
* 兜底。2026-10-22:取代原先被 CH395F 跨模块裸 extern 直接写的全局变量。
|
||||
*/
|
||||
void gd5f_spi1_notify_done(void);
|
||||
void gd5f_spi1_notify_error(void);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -6,12 +6,25 @@
|
||||
* 创建日期:2026-07-20
|
||||
* 修改记录:
|
||||
* 2026-07-20 王建锋 创建初始版本
|
||||
* 2026-09-21 E-31 resume 失败按错误分类:仅真空白判新卷,其余失败
|
||||
* 2026-09-21 resume 失败按错误分类:仅真空白判新卷,其余失败
|
||||
* 返回 mount 层(禁止静默清 map 触发整卷重格式化)
|
||||
* 2026-10-02 王建锋 resume 成功后增打 journal head/tail/epoch:复盘
|
||||
* "复位后所有写入丢失"时 sectors used 不足以判定
|
||||
* journal 回退(覆盖写不改 count),head/tail/epoch 是
|
||||
* 唯一能暴露 resume 锚错 checkpoint 的证据
|
||||
* 2026-10-15 王建锋 落实 dhara 移植层两条硬约束:① is_free 改**整页扫描**——
|
||||
* 旧实现只看页首 64 B,会把"页首 0xFF、后段有数据"的
|
||||
* 已编程页误判空闲,dhara 遂往已编程页再编程 = 数据丢失
|
||||
* (页首有数据的绝大多数页走快路径,成本与旧实现同价);
|
||||
* ② HAL 六个入口全部加几何上界守卫——越界页/块号(含
|
||||
* DHARA_PAGE_NONE)旧会被地址位截断成真实页 → 静默读写
|
||||
* 错页,现报 DHARA_E_CORRUPT_MAP(不能用 BAD_BLOCK:会
|
||||
* 误标无辜好块;不能用 RECOVER:会触发回收);
|
||||
* ③ 补主机侧移植层单测 test/host/nand_port/(L0.5);
|
||||
* ④ 几何字面量收成 FTL_LOG2_PAGE_SIZE / FTL_LOG2_PPB
|
||||
* 单一真源 + 两条负数组编译期守卫:越界上界依赖
|
||||
* log2_ppb,换料时只改 GD5F_* 而忘改字面量会**静默**
|
||||
* 把守卫算大 = 越界守卫失效,现改为直接编译错
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
@@ -21,6 +34,7 @@
|
||||
#include "diskio.h"
|
||||
#include "nand.h"
|
||||
#include "map.h"
|
||||
#include "bsp_err.h" /* C2:把 dhara 错误归类成跨模块可判定的失败类 */
|
||||
|
||||
/* FTL 分区起始地址(块 0,偏移 0,占用全部 256MB) */
|
||||
#define FTL_FATFS_OFFSET 0U
|
||||
@@ -41,10 +55,58 @@
|
||||
#define FTL_NUM_BLOCKS (GD5F_TOTAL_BLOCKS - FTL_START_BLOCK)
|
||||
#define FTL_START_PAGE (FTL_START_BLOCK * GD5F_PAGES_PER_BLOCK)
|
||||
|
||||
/* dhara 几何字面量的**单一真源**(TRAP-GD5F-07 遗留 3 的收口):disk_initialize 与
|
||||
* 下方守卫都取这两个宏,不再各写一遍 11 / 6。 */
|
||||
#define FTL_LOG2_PAGE_SIZE 11u
|
||||
#define FTL_LOG2_PPB 6u
|
||||
|
||||
/* 编译期守卫:`ftl_page_in_range()` 的越界上界 = `num_blocks << log2_ppb`,
|
||||
* **完全依赖 log2_ppb**。将来换料(例如 128 页/块的兼容片)只改了 GD5F_* 常量
|
||||
* 而忘了同步这里的字面量,守卫会静默算错——偏大就是越界守卫失效,正是它要防的
|
||||
* 那一类错误,而且不报任何错。下面两条把"字面量 == GD5F 常量的 log2"钉死,
|
||||
* 不一致直接编译错。负数组 typedef 是 AC5 下的项目惯用法(AC5 不支持 C11
|
||||
* _Static_assert),同本目录 gd5f2gq5ue.h 的 DMA 契约守卫。 */
|
||||
typedef char ftl_geometry_log2_page_size_check_t[
|
||||
(FTL_PAGE_SIZE == (1 << FTL_LOG2_PAGE_SIZE)) ? 1 : -1];
|
||||
typedef char ftl_geometry_log2_ppb_check_t[
|
||||
(GD5F_PAGES_PER_BLOCK == (1 << FTL_LOG2_PPB)) ? 1 : -1];
|
||||
|
||||
/* dhara 逻辑地址到物理地址转换 */
|
||||
static inline uint32_t ftl_phy_block(dhara_block_t b) { return b + FTL_START_BLOCK; }
|
||||
static inline uint32_t ftl_phy_page(dhara_page_t p) { return p + FTL_START_PAGE; }
|
||||
|
||||
/*
|
||||
* dhara 移植层硬约束(2026-10-15):移植层地址守卫
|
||||
*
|
||||
* 旧实现把 dhara 传来的页/块号直接加偏移送进 SPI 命令,**不做任何上界校验**。
|
||||
* 越界页号(含 dhara 的"无页"哨兵 DHARA_PAGE_NONE = 0xffffffff,journal.h:46)
|
||||
* 经 ftl_phy_page() 相加后会被芯片地址位截断成某个**真实存在的页** → 静默读/写
|
||||
* 错页,比报错严重得多(数据错而传输"成功",与 CCM-DMA 那条同族)。
|
||||
*
|
||||
* 为什么报 DHARA_E_CORRUPT_MAP 而不是报告原建议的 DHARA_E_UNSUPPORTED:
|
||||
* 本仓裁剪版 dhara 的错误枚举(Lib/dhara/error.h:20-31)里**没有**
|
||||
* E_UNSUPPORTED,写上去直接编译失败。
|
||||
* 为什么不用 DHARA_E_BAD_BLOCK:journal.c:904 的 recover_from 只对 BAD_BLOCK
|
||||
* 走"标坏块 + 跳块重试"——对一个凭空的地址标坏块 = 毁掉一个无辜的好块。
|
||||
* 为什么不会触发静默清卷:map.c:275 的 try_recover 只对 E_RECOVER 启动回收,
|
||||
* 其余错误一律原样上抛(教训:单次瞬态毛刺不得销毁整卷数据)。
|
||||
* is_free / is_bad 没有错误通道 → 一律保守取值(判非空 / 判坏),与同族
|
||||
* 口径一致:宁可少用一个块,不可把不可信的内容当有效。
|
||||
*/
|
||||
static int ftl_page_in_range(const struct dhara_nand *n, dhara_page_t p) {
|
||||
if ((n == NULL) || (n->num_blocks == 0u)) {
|
||||
return 0;
|
||||
}
|
||||
return p < ((dhara_page_t)n->num_blocks << n->log2_ppb);
|
||||
}
|
||||
|
||||
static int ftl_block_in_range(const struct dhara_nand *n, dhara_block_t b) {
|
||||
if ((n == NULL) || (n->num_blocks == 0u)) {
|
||||
return 0;
|
||||
}
|
||||
return b < n->num_blocks;
|
||||
}
|
||||
|
||||
/* ======================== 私有变量 ======================== */
|
||||
|
||||
/* dhara 核心数据结构 */
|
||||
@@ -65,56 +127,122 @@ static uint8_t s_initialized;
|
||||
|
||||
/* ======================== 私有函数声明 ======================== */
|
||||
|
||||
static int ftl_flush_cache(void);
|
||||
static int ftl_read_page(dhara_sector_t lpn);
|
||||
static bsp_err_t ftl_flush_cache(void);
|
||||
static bsp_err_t ftl_read_page(dhara_sector_t lpn);
|
||||
|
||||
/* SPI 原语从 gd5f2gq5ue.h 获取 */
|
||||
|
||||
/* ======================== dhara 错误的可读名与失败类 ======================== */
|
||||
|
||||
/*
|
||||
* 函数功能:把 dhara 错误枚举翻成短字符串(**本仓 dhara 没编 error.c,没有 dhara_strerror()**,
|
||||
* 用它会链接失败,故自己给一张小表)
|
||||
* 入口参数:err - dhara 错误枚举值
|
||||
* 返回值:静态字符串常量("NONE" / "ECC" / "MAP_FULL" ...,越界返回 "OOB")
|
||||
* 限定条件:只用于日志,不得用于判等
|
||||
* 函数说明:以前这里只打 `err=%d`——现场拿到一串数字还得翻 Lib/dhara/error.h 才知道是什么,
|
||||
* 存储故障又大多是"看完日志就没机会复现"的那类,可读名是排障成本最低的改法。
|
||||
*/
|
||||
static const char *ftl_err_name(dhara_error_t err) {
|
||||
static const char *const s_names[] = {
|
||||
"NONE", "BAD_BLOCK", "ECC", "TOO_BAD", "RECOVER",
|
||||
"JOURNAL_FULL", "NOT_FOUND", "MAP_FULL", "CORRUPT_MAP",
|
||||
};
|
||||
|
||||
if ((unsigned int)err < (sizeof(s_names) / sizeof(s_names[0]))) {
|
||||
return s_names[err];
|
||||
}
|
||||
return "OOB";
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:把 dhara 错误归类成统一失败类(C2)
|
||||
* 入口参数:err - dhara 错误枚举值
|
||||
* 返回值:BSP_ERR_CRC(位翻转不可纠 / 页太坏);BSP_ERR_NO_SPACE(journal 或映射层预算
|
||||
* 耗尽);其余(坏块 / 找回失败 / 映射损坏 / 越界)→ BSP_ERR_IO
|
||||
* 限定条件:入参应是**失败**的 err(DHARA_E_NONE 会得 BSP_OK)
|
||||
* 函数说明:分这三类的理由是它们在**处置上真的不同**,不是为了码好看:
|
||||
* · ECC/TOO_BAD → 该重试读或标页坏,**不是**卷的问题;
|
||||
* · JOURNAL_FULL/MAP_FULL → 介质还健康,是**块池/日志预算见底**,此时重格式化只是
|
||||
* 把问题推回去(教训正是"别把非空白错误当新卷清 map"),该走容量恢复流程;
|
||||
* · BAD_BLOCK/CORRUPT_MAP/RECOVER → 映射或介质完整性故障,该保卷 + 上报,不许自愈重格式化。
|
||||
* 本函数先把这三类**打出来**(就地日志,最早且唯一同时知道 LPN 与原始码的位置);
|
||||
* 要让 defaultTask 的卷健康探针按类分支,还需要一个跨调用存活的快照(C4 待还)。
|
||||
*/
|
||||
static bsp_err_t ftl_class_from_dhara(dhara_error_t err) {
|
||||
switch (err) {
|
||||
case DHARA_E_NONE:
|
||||
return BSP_OK;
|
||||
case DHARA_E_ECC:
|
||||
case DHARA_E_TOO_BAD:
|
||||
return BSP_ERR_CRC;
|
||||
case DHARA_E_JOURNAL_FULL:
|
||||
case DHARA_E_MAP_FULL:
|
||||
return BSP_ERR_NO_SPACE;
|
||||
default:
|
||||
return BSP_ERR_IO;
|
||||
}
|
||||
}
|
||||
|
||||
/* ======================== 页面缓存管理 ======================== */
|
||||
|
||||
static int ftl_flush_cache(void) {
|
||||
static bsp_err_t ftl_flush_cache(void) {
|
||||
if (!s_cache_dirty) {
|
||||
return GD5F_OK;
|
||||
return BSP_OK;
|
||||
}
|
||||
|
||||
dhara_error_t err;
|
||||
if (dhara_map_write(&s_map, s_cached_lpn, s_cache_buf, &err) < 0) {
|
||||
DBG_ERROR("Cache flush failed: LPN=%lu err=%d", s_cached_lpn, err);
|
||||
return GD5F_ERROR;
|
||||
bsp_err_t cls = ftl_class_from_dhara(err); /* 只在失败分支需要(cppcheck variableScope) */
|
||||
DBG_ERROR("Cache flush FAIL: LPN=%lu dhara=%s class=%d",
|
||||
s_cached_lpn, ftl_err_name(err), cls);
|
||||
return cls;
|
||||
}
|
||||
|
||||
s_cache_dirty = 0;
|
||||
return GD5F_OK;
|
||||
return BSP_OK;
|
||||
}
|
||||
|
||||
static int ftl_read_page(dhara_sector_t lpn) {
|
||||
static bsp_err_t ftl_read_page(dhara_sector_t lpn) {
|
||||
dhara_error_t err;
|
||||
|
||||
if (dhara_map_read(&s_map, lpn, s_cache_buf, &err) < 0) {
|
||||
DBG_ERROR("Page read failed: LPN=%lu err=%d", lpn, err);
|
||||
return GD5F_ERROR;
|
||||
bsp_err_t cls = ftl_class_from_dhara(err); /* 只在失败分支需要(cppcheck variableScope) */
|
||||
DBG_ERROR("Page read FAIL: LPN=%lu dhara=%s class=%d",
|
||||
lpn, ftl_err_name(err), cls);
|
||||
return cls;
|
||||
}
|
||||
|
||||
s_cached_lpn = lpn;
|
||||
s_cache_dirty = 0;
|
||||
return GD5F_OK;
|
||||
return BSP_OK;
|
||||
}
|
||||
|
||||
/* ======================== dhara NAND HAL ======================== */
|
||||
|
||||
int dhara_nand_is_bad(const struct dhara_nand *n, dhara_block_t b) {
|
||||
(void)n;
|
||||
if (ftl_block_in_range(n, b) == 0) {
|
||||
return 1; /* 越界块:保守判坏,让 dhara 跳过 */
|
||||
}
|
||||
return gd5f2gq5ue_is_block_bad(ftl_phy_block(b));
|
||||
}
|
||||
|
||||
void dhara_nand_mark_bad(const struct dhara_nand *n, dhara_block_t b) {
|
||||
(void)n;
|
||||
if (ftl_block_in_range(n, b) == 0) {
|
||||
return; /* 越界块:绝不标坏,避免毁掉无辜好块 */
|
||||
}
|
||||
gd5f2gq5ue_mark_block_bad(ftl_phy_block(b));
|
||||
}
|
||||
|
||||
int dhara_nand_erase(const struct dhara_nand *n, dhara_block_t b, dhara_error_t *err) {
|
||||
(void)n;
|
||||
int ret = gd5f_block_erase(ftl_phy_block(b));
|
||||
int ret;
|
||||
|
||||
if (ftl_block_in_range(n, b) == 0) {
|
||||
dhara_set_error(err, DHARA_E_CORRUPT_MAP);
|
||||
return -1;
|
||||
}
|
||||
|
||||
ret = gd5f_block_erase(ftl_phy_block(b));
|
||||
if (ret == GD5F_ERASE_FAIL || ret != GD5F_OK) {
|
||||
dhara_nand_mark_bad(n, b);
|
||||
dhara_set_error(err, DHARA_E_BAD_BLOCK);
|
||||
@@ -125,7 +253,13 @@ int dhara_nand_erase(const struct dhara_nand *n, dhara_block_t b, dhara_error_t
|
||||
|
||||
int dhara_nand_prog(const struct dhara_nand *n, dhara_page_t p, const uint8_t *data, dhara_error_t *err) {
|
||||
int ret;
|
||||
uint32_t phy_pg = ftl_phy_page(p);
|
||||
uint32_t phy_pg;
|
||||
|
||||
if ((ftl_page_in_range(n, p) == 0) || (data == NULL)) {
|
||||
dhara_set_error(err, DHARA_E_CORRUPT_MAP);
|
||||
return -1;
|
||||
}
|
||||
phy_pg = ftl_phy_page(p);
|
||||
|
||||
gd5f_write_enable();
|
||||
ret = gd5f_program_load(0, data, FTL_PAGE_SIZE);
|
||||
@@ -145,42 +279,66 @@ int dhara_nand_prog(const struct dhara_nand *n, dhara_page_t p, const uint8_t *d
|
||||
/* 编程正确性以 P_FAIL 状态位(gd5f_program_exec 内检查)+ 读时 ECC +
|
||||
* journal 读时门(magic/CRC)为准;数据路径不做整页回读比对——
|
||||
* 2026-09-20 曾加回读校验(GD5F_PROG_VERIFY),2026-09-21 经评估整体
|
||||
* 移除(依据见 docs/内存管理/存储栈RAM优化调研.md 与 GD5F Trap Records Trap 05) */
|
||||
* 移除(依据见 docs/内存管理/存储栈RAM优化调研.md 与 GD5F Trap Records TRAP-GD5F-05) */
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* dhara 移植层硬约束(2026-10-15):整页扫描
|
||||
*
|
||||
* 旧实现只看页首 64 B。dhara 的 journal 用 is_free 判断"检查点组是否未编程"
|
||||
* (journal.c:416 cp_free、journal.c:520 找下一空闲页),其正确性前提是
|
||||
* **is_free 答"空闲"的页必须真的全 0xFF**——journal.c:405-414 的注释论证了
|
||||
* "只要组内被编程过的页含非 0xFF 字节就能识别出来",可若那一页恰好页首 64 B
|
||||
* 是 0xFF、后段才有数据,旧实现就会误判空闲。误判的后果不是报错,而是
|
||||
* **往已编程的页再编程一次** → 位碰撞、journal/map 指向垃圾 = 数据丢失。
|
||||
* 本卷上跑的是 FatFS 数据页,"页首一段 0xFF、后段有数据"完全常见(刚格式化的
|
||||
* 卷、稀疏文件块、日志尾部),所以必须整页判。
|
||||
*
|
||||
* 成本处理:先查页首 64 B——页首就有数据的页(绝大多数已编程页)当场返回非空,
|
||||
* 与旧实现同价;只有"目前读到的全是 0xFF"的页才继续扫后段。分块用栈上 64 B
|
||||
* 缓冲,不新增 2 KB 静态 RAM(页缓冲三块不可合并的论证见
|
||||
* docs/内存管理/存储栈RAM优化调研.md §5)。
|
||||
*/
|
||||
int dhara_nand_is_free(const struct dhara_nand *n, dhara_page_t p) {
|
||||
(void)n;
|
||||
uint32_t phy_pg = ftl_phy_page(p);
|
||||
int fre;
|
||||
uint8_t buf[64];
|
||||
uint32_t phy_pg;
|
||||
uint32_t off;
|
||||
uint32_t i;
|
||||
|
||||
if (ftl_page_in_range(n, p) == 0) {
|
||||
return 0; /* 越界页号:保守判非空 */
|
||||
}
|
||||
phy_pg = ftl_phy_page(p);
|
||||
|
||||
if (gd5f_page_read(phy_pg) != GD5F_OK) {
|
||||
fre = 0;
|
||||
} else {
|
||||
uint8_t buf[64];
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (gd5f_read_from_cache(0, buf, sizeof(buf)) != GD5F_OK) {
|
||||
/* E-42 同族(2026-10-03):读失败 → buf 可能为陈旧/垃圾内容,
|
||||
for (off = 0; off < FTL_PAGE_SIZE; off += sizeof(buf)) {
|
||||
if (gd5f_read_from_cache((uint16_t)off, buf, sizeof(buf)) != GD5F_OK) {
|
||||
/* 同族(2026-10-03):读失败 → buf 可能为陈旧/垃圾内容,
|
||||
* 保守判为非空(块不可复用),避免把"没读到内容的块"判成空闲。 */
|
||||
fre = 0;
|
||||
} else {
|
||||
uint32_t i;
|
||||
|
||||
fre = 1;
|
||||
for (i = 0; i < sizeof(buf); i++) {
|
||||
if (buf[i] != 0xFF) {
|
||||
fre = 0;
|
||||
break;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
for (i = 0; i < sizeof(buf); i++) {
|
||||
if (buf[i] != 0xFFu) {
|
||||
return 0; /* 页首命中即返回 = 快路径,不读后段 */
|
||||
}
|
||||
}
|
||||
}
|
||||
return fre;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int dhara_nand_read(const struct dhara_nand *n, dhara_page_t p, size_t offset, size_t length, uint8_t *data, dhara_error_t *err) {
|
||||
(void)n;
|
||||
uint32_t phy_pg = ftl_phy_page(p);
|
||||
uint32_t phy_pg;
|
||||
|
||||
if ((ftl_page_in_range(n, p) == 0) || (data == NULL) ||
|
||||
(offset >= FTL_PAGE_SIZE) || (length > (FTL_PAGE_SIZE - offset))) {
|
||||
dhara_set_error(err, DHARA_E_CORRUPT_MAP);
|
||||
return -1;
|
||||
}
|
||||
phy_pg = ftl_phy_page(p);
|
||||
|
||||
if (gd5f_page_read(phy_pg) != GD5F_OK) {
|
||||
dhara_set_error(err, DHARA_E_ECC);
|
||||
@@ -192,7 +350,7 @@ int dhara_nand_read(const struct dhara_nand *n, dhara_page_t p, size_t offset, s
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* E-42 同族(2026-10-03):read_from_cache 返回值必须检查——失败时 data 内容
|
||||
/* 同族(2026-10-03):read_from_cache 返回值必须检查——失败时 data 内容
|
||||
* 不可信,原实现仍返回 0(成功),会把未读到的数据当有效数据交给上层。 */
|
||||
if (gd5f_read_from_cache((uint16_t)offset, data, length) != GD5F_OK) {
|
||||
dhara_set_error(err, DHARA_E_ECC);
|
||||
@@ -201,8 +359,23 @@ int dhara_nand_read(const struct dhara_nand *n, dhara_page_t p, size_t offset, s
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* dhara 移植层硬约束(2026-10-15):src / dst 双端守卫
|
||||
*
|
||||
* 报告原判断"dhara map 在回收/擦除路径会用 DHARA_NO_PAGE 试探"经逐点核对
|
||||
* **不成立**:本仓裁剪版没有 DHARA_NO_PAGE,哨兵叫 DHARA_PAGE_NONE
|
||||
* (journal.h:46),而 map.c 三处 dhara_journal_copy 调用点都已先挡掉它
|
||||
* (map.c:260 挡 pad_queue 的 p、map.c:434/440 挡 alt_page、map.c:242 的 src
|
||||
* 来自 trace_path 的真实页)。但"移植层不做任何上界校验"这个风险是真的:
|
||||
* 哨兵一旦从别处漏进来(未来改动、被污染的 meta),ftl_phy_page() 会把
|
||||
* 0xffffffff 截断成某个真实页 → **静默搬走无辜数据**。故 src/dst 各挡一次,
|
||||
* dst 越界尤其致命——那是一次写入。
|
||||
*/
|
||||
int dhara_nand_copy(const struct dhara_nand *n, dhara_page_t src, dhara_page_t dst, dhara_error_t *err) {
|
||||
(void)n;
|
||||
if ((ftl_page_in_range(n, src) == 0) || (ftl_page_in_range(n, dst) == 0)) {
|
||||
dhara_set_error(err, DHARA_E_CORRUPT_MAP);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (dhara_nand_read(n, src, 0, FTL_PAGE_SIZE, s_copy_buf, err) < 0) {
|
||||
return -1;
|
||||
@@ -228,15 +401,16 @@ DSTATUS disk_initialize(BYTE pdrv) {
|
||||
|
||||
DBG_INFO("Initializing NAND FTL...");
|
||||
|
||||
/* N-26 启动期地址自检(2026-10-12):本文件三个页缓冲是 SPI1 DMA 路径上
|
||||
/* DMA 可达性三层防线——启动期地址自检(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 内部使用,一并守住防将来越窗)。
|
||||
* 2048 B > GD5F_DMA_THRESHOLD;s_page_buf 页内虽只攒 132 B 元数据,
|
||||
* 但编程的是**整页**(journal.c:992),传输长度同为 2048 B、同样走 DMA,
|
||||
* 一并守住防将来越窗)。
|
||||
* 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) ||
|
||||
@@ -246,8 +420,8 @@ DSTATUS disk_initialize(BYTE pdrv) {
|
||||
return STA_NOINIT;
|
||||
}
|
||||
|
||||
s_nand.log2_page_size = 11;
|
||||
s_nand.log2_ppb = 6;
|
||||
s_nand.log2_page_size = FTL_LOG2_PAGE_SIZE;
|
||||
s_nand.log2_ppb = FTL_LOG2_PPB;
|
||||
/* dhara 可见块数 = 总块数 - 保留块池(BBT 存储区不交给 dhara,避免被擦写) */
|
||||
s_nand.num_blocks = gd5f_get_usable_blocks();
|
||||
|
||||
@@ -265,10 +439,10 @@ DSTATUS disk_initialize(BYTE pdrv) {
|
||||
* 显式格式化后);失败的 resume 已把 journal 复位为空态 →
|
||||
* 按新卷处理,由上层自动建卷(此处 dhara_map_clear 为空操作:
|
||||
* resume 失败时 count 已清零,map 本已为空)。 */
|
||||
DBG_INFO("No valid map found (err=%d), creating fresh", err);
|
||||
DBG_INFO("No valid map found (dhara=%s), creating fresh", ftl_err_name(err));
|
||||
dhara_map_clear(&s_map);
|
||||
} else {
|
||||
/* E-31(2026-09-21 审计 P0 修复):非"空白"错误(ECC /
|
||||
/* (2026-09-21 审计 P0 修复):非"空白"错误(ECC /
|
||||
* BAD_BLOCK / CORRUPT_MAP / 瞬态 I/O 等)= 真实完整性故障——
|
||||
* 芯片上 journal 可能仍持有有效 checkpoint。旧行为把任何失败
|
||||
* 当新卷清 map,上层随即整卷重格式化 → 单次瞬态毛刺即静默销毁
|
||||
@@ -276,8 +450,9 @@ DSTATUS disk_initialize(BYTE pdrv) {
|
||||
* defaultTask 自检上报 "filesystem fault" 并保留卷;瞬态故障
|
||||
* 由主循环重试/下次上电重新 resume;重格式化只走显式命令
|
||||
* (nand_ftl_format)。 */
|
||||
DBG_ERROR("Map resume failed (err=%d), volume kept, "
|
||||
"storage unavailable", err);
|
||||
DBG_ERROR("Map resume failed (dhara=%s class=%d), volume kept, "
|
||||
"storage unavailable", ftl_err_name(err),
|
||||
ftl_class_from_dhara(err));
|
||||
return STA_NOINIT;
|
||||
}
|
||||
} else {
|
||||
@@ -346,10 +521,10 @@ DRESULT disk_read(BYTE pdrv, BYTE *buff, LBA_t sector, UINT count) {
|
||||
}
|
||||
|
||||
if (lpn != s_cached_lpn) {
|
||||
if (ftl_flush_cache() != GD5F_OK) {
|
||||
if (ftl_flush_cache() != BSP_OK) {
|
||||
return RES_ERROR;
|
||||
}
|
||||
if (ftl_read_page(lpn) != GD5F_OK) {
|
||||
if (ftl_read_page(lpn) != BSP_OK) {
|
||||
return RES_ERROR;
|
||||
}
|
||||
}
|
||||
@@ -381,10 +556,10 @@ DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) {
|
||||
}
|
||||
|
||||
if (lpn != s_cached_lpn) {
|
||||
if (ftl_flush_cache() != GD5F_OK) {
|
||||
if (ftl_flush_cache() != BSP_OK) {
|
||||
return RES_ERROR;
|
||||
}
|
||||
if (ftl_read_page(lpn) != GD5F_OK) {
|
||||
if (ftl_read_page(lpn) != BSP_OK) {
|
||||
return RES_ERROR;
|
||||
}
|
||||
}
|
||||
@@ -393,7 +568,7 @@ DRESULT disk_write(BYTE pdrv, const BYTE *buff, LBA_t sector, UINT count) {
|
||||
s_cache_dirty = 1;
|
||||
|
||||
if (offset == 0 && batch == FTL_SECTORS_PER_PAGE) {
|
||||
if (ftl_flush_cache() != GD5F_OK) {
|
||||
if (ftl_flush_cache() != BSP_OK) {
|
||||
return RES_ERROR;
|
||||
}
|
||||
}
|
||||
@@ -416,7 +591,7 @@ DRESULT disk_ioctl(BYTE pdrv, BYTE cmd, void *buff) {
|
||||
|
||||
switch (cmd) {
|
||||
case CTRL_SYNC:
|
||||
if (ftl_flush_cache() != GD5F_OK) {
|
||||
if (ftl_flush_cache() != BSP_OK) {
|
||||
return RES_ERROR;
|
||||
}
|
||||
{
|
||||
@@ -459,7 +634,7 @@ int nand_ftl_format(void) {
|
||||
}
|
||||
}
|
||||
|
||||
if (ftl_flush_cache() != GD5F_OK) {
|
||||
if (ftl_flush_cache() != BSP_OK) {
|
||||
return GD5F_ERROR;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,15 @@
|
||||
/*
|
||||
* 模块名称:SPI NAND FTL(dhara 移植层 + FatFs 磁盘接口)
|
||||
* 模块功能:把 GD5F SPI NAND 经 dhara 映射层暴露成 FatFs 的 0 号磁盘
|
||||
* (disk_initialize/status/ioctl/read/write),并提供显式格式化入口。
|
||||
* 适用平台:STM32F4 系列 + GD5F2GQ5UE
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-09
|
||||
* 修改记录:
|
||||
* 2026-10-23 王建锋 补模块头与函数注释(return-doc 检查);返回值语义写明"失败类目前
|
||||
* 只进日志、不外传",避免调用方以为能拿到 ECC / 预算耗尽的区别
|
||||
*/
|
||||
|
||||
#ifndef NAND_FTL_H
|
||||
#define NAND_FTL_H
|
||||
|
||||
@@ -7,12 +19,42 @@ extern "C" {
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/*
|
||||
* 函数功能:初始化 FTL(等价于 disk_initialize(0):SPI 起链 + dhara_map_resume)
|
||||
* 入口参数:无
|
||||
* 返回值:0 - 成功(映射可 resume 或判为真空白卷);-1 - 失败(disk_initialize 非 0)
|
||||
* 限定条件:由 FatFs 的 f_mount(forced=1) 间接触发,或测试代码直接调用
|
||||
* 函数说明:⚠ 这里的 -1 **抹掉了失败类**——"ECC 读不动"、"journal 满"与"映射损坏"对外
|
||||
* 都只是 -1。分类信息目前只在失败点就地进日志(见 nand_ftl.c 的 ftl_class_from_dhara
|
||||
* 与 ../架构设计/STM32F4-Base存储架构说明.md §10.4);要让调用方按类分支,需要跨调用
|
||||
* 存活的失败类快照(C4 待还项,当前无消费者故未做)。
|
||||
* 调用方**不得**把 -1 解读成"卷坏了,可以重格式化"——教训正在这条上。
|
||||
*/
|
||||
int nand_ftl_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:清空 dhara 映射树并同步(**破坏性**:整卷逻辑地址→物理映射作废)
|
||||
* 入口参数:无
|
||||
* 返回值:GD5F_OK(0) - 已清空并同步;GD5F_ERROR(-1) - 初始化/刷写缓存/同步失败(卷状态未定,
|
||||
* 须重新挂载判定,不要连环重试)
|
||||
* 限定条件:只允许走显式命令或恢复流程调用;不得因"某次读写失败"自动调用
|
||||
* 函数说明:新卷从空树写起,避免旧 journal 残留与新卷共存(结构卫生)。
|
||||
* 容量恢复动作的区分(A 清日志 / B 重挂载 / C 全片擦除 / D 残留清理)
|
||||
* → ../测试说明/存储维护与容量恢复.md。
|
||||
*/
|
||||
int nand_ftl_format(void);
|
||||
|
||||
/*
|
||||
* 函数功能:复位 FTL 内存态,使下次 nand_ftl_init() 重新走 dhara_map_resume
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:测试用途(掉电恢复 / 坏块注入后重建场景模拟设备重启)
|
||||
* 函数说明:只清 RAM 状态位,不碰闪存内容。
|
||||
*/
|
||||
void nand_ftl_deinit(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
#endif /* NAND_FTL_H */
|
||||
|
||||
+23
-14
@@ -7,7 +7,8 @@
|
||||
|
||||
| 文件 | 作用 |
|
||||
|---|---|
|
||||
| `net_socket.c/h` | **BSD Socket API 实现**:`net_socket` / `net_connect` / `net_send` / `net_recv` / `net_close` / `net_listen` / `net_accept`,含 L3 挂起(pending)与错误码 |
|
||||
| `net_socket.c/h` | **BSD Socket API 实现**:`net_socket` / `net_connect` / `net_send` / `net_recv` / `net_close` / `net_listen` / `net_accept`,含挂起 recv(pending)与错误码 |
|
||||
| `net_pending.c/h` | **挂起 recv / send 请求表**(阻塞挂起模型:登记 / 每轮完成检查(recv)/ 推进(send)/ 取消 / close 结算;真机专属,模拟器走 `net_socket_sim.c`) |
|
||||
| `net_select.c/h` | `select`/`poll` 多路复用(基于芯片中断事件 + 周期轮询) |
|
||||
| `net_types.h` | 网络层公共类型定义 |
|
||||
| `net_config.h` | 网络层编译期配置(缓冲区大小、超时、对账周期等) |
|
||||
@@ -15,26 +16,34 @@
|
||||
|
||||
## 核心机制
|
||||
|
||||
### 阻塞模型(L3 pending)
|
||||
### 阻塞模型(挂起 recv / send / pending)
|
||||
|
||||
- 一期(RECV)已落地:`net_recv` 等支持带超时的阻塞(`pending` 挂起 + tick 唤醒)
|
||||
- 二期(ACCEPT + SEND)待做 → `docs/任务规划/登记簿.md` N-4
|
||||
- **recv 挂起(pending)已落地**:`net_recv` 阻塞不占 netTask——登记挂起请求 + 每轮 tick 唤醒
|
||||
- **send 挂起已落地**:`net_send` 快路径有界自旋(`NET_SEND_SPIN_MS` = 2ms),超预算登记
|
||||
挂起 send,由 `net_send_pending_tick` 按主循环节拍分块推进;netTask 最坏停滞由 1s 压到 2ms
|
||||
- **ACCEPT 决定不挂起化(2026-10-15)**(调用方轮询问题,不是队头阻塞;pending 化收益仅 RPC 开销与抖动)
|
||||
- 架构细节:`docs/网络/Net层阻塞模型架构设计.md`
|
||||
|
||||
### 周期对账自愈(Trap 22 修复)
|
||||
### 周期对账自愈(TRAP-NET-17 修复;对账与监听重建合一)
|
||||
|
||||
- `net_reconcile_tick()`(200ms)回读芯片状态修正三类失同步:
|
||||
- `net_reconcile_tick()`(100ms,判据读**驱动状态缓存**——唯一
|
||||
刷新点 `net_poll` 每轮全量刷新,E2 实测单次回读 18.1us)修正失同步:
|
||||
本层 ESTABLISHED 而芯片已断 → 断开;本层 LISTENING 而芯片已握手 → 补 accept;
|
||||
本层 LISTENING 而芯片监听消失 → 重建
|
||||
- **去抖(连续 2 次)+ 冷却(1s)**——单次状态读的重建动作**必须去抖**,
|
||||
否则"刚 listen 未生效"瞬态会反复重建直至 `relisten give up`(三监听口全死)
|
||||
本层 LISTENING/CLOSED(监听型 fd)而芯片监听消失 → 重建(形态③④);
|
||||
本层 CLOSED 而芯片在听 → 只重同步(U11);芯片静默回收 → RESYNC(⑤)
|
||||
- **分级去抖(②=1 轮 / ③④⑤=2 轮 / ①=4 轮)+ 冷却(1s)**——监听失位的重建
|
||||
动作**必须去抖**,否则"刚 listen 未生效"瞬态会反复重建直至
|
||||
`relisten retry budget exhausted`(三监听口全死);①的 4 轮窗是 F-NET-17 M3
|
||||
可疑电平的让位租约(勿改小)
|
||||
|
||||
### 异常复监听隔离(Trap 23 修复)
|
||||
### 异常复监听隔离(TRAP-NET-18 修复)
|
||||
|
||||
- 异常释放(TIMEOUT)的 socket 复监听前隔离:排空 RX 残渣 + 主动 close + 等 CLOSED
|
||||
- 400ms 起、800ms 上限强制放行(上限须 < 重试放弃时限 2000ms)
|
||||
- 400ms 起;**800ms 上限只留痕、不强行放行**(芯片未回 CLOSED 时 open 注定吃 USE 错,
|
||||
真正兜底 = D10 预算到期 + 对账形态④ 重新登记)
|
||||
- 仅异常路径生效,正常 FIN 路径零改动
|
||||
|
||||
### 错误码(E-03 修复)
|
||||
### 错误码
|
||||
|
||||
- **按任务隔离**:`s_net_errno_slots[8]`(每槽 `{任务句柄, err}`),替代原全局 `s_net_errno`
|
||||
- `net_get_errno()` 只读本任务槽位,**对外语义不变**(成功不清零)
|
||||
@@ -54,12 +63,12 @@
|
||||
- 就绪屏障位(`SYS_EVT_NET_READY` 等)等待必须 `osFlagsNoClear`,**不能用默认消费型**
|
||||
- 每次 CH395F SPI 事务须 `ch395f_spi_begin()/end()` 包裹
|
||||
- **改任何自愈/重建逻辑必须去抖**,否则会引发重建风暴
|
||||
- 相关陷阱:`docs/问题记录/CH395F_Trap_Records.md` Trap 21/22/23
|
||||
- 相关陷阱:`docs/问题记录/CH395F_Trap_Records.md` TRAP-NET-17/-18
|
||||
|
||||
## 相关文档
|
||||
|
||||
- 上层用法:`docs/网络/BSD_Socket_API_使用指南.md`
|
||||
- 内部调用关系:`docs/网络/BSD_Socket_API_函数调用关系.md`
|
||||
- 架构设计:`docs/网络/Net层阻塞模型架构设计.md`(阻塞等待 / pending 请求模型)
|
||||
- 二期设计基线:`docs/任务规划/基线与待确认.md` §2.1
|
||||
- ACCEPT 挂起化不实施的判据:`docs/网络/Net层阻塞模型架构设计.md` §10
|
||||
- 测试:`docs/测试说明/CH395F_Test_Guide.md`、`docs/测试说明/FTP_Test_Guide.md`
|
||||
|
||||
@@ -29,13 +29,13 @@ lftpd(本目录)
|
||||
|
||||
- **ftpTask 就绪屏障**:`osEventFlagsWait` 等两位就绪必须加 `osFlagsNoClear`,
|
||||
否则位被先到的消费者清掉 → FTP 21 端口静默不监听(无日志无报错)
|
||||
- **lftpd 大局部缓冲**:已按 `docs/内存管理/RAM使用分析与优化方案.md` §1.4 静态化 9 处
|
||||
- **lftpd 大局部缓冲**:已按 `docs/内存管理/RAM使用分析与优化方案.md` §4.1.4 静态化 9 处
|
||||
大局部缓冲(BSS +4.6KB),使 ftpTask 栈峰值从 3480B 降到 1184B——**勿改回局部变量**
|
||||
- **上传中止截断**:FTP STOR 中途断开后首笔复连被截断(Trap 23)已在 `net_socket.c` 侧修复
|
||||
- **上传中止截断**:FTP STOR 中途断开后首笔复连被截断(TRAP-NET-18)已在 `net_socket.c` 侧修复
|
||||
- 本目录受 pre-commit 的**部分排除**(第三方代码)
|
||||
|
||||
## 相关文档
|
||||
|
||||
- 测试指南:`docs/测试说明/FTP_Test_Guide.md`
|
||||
- 台架探针:`MDK-ARM/ftp_abort_probe.py`、`MDK-ARM/ftp_freeze_probe.py`
|
||||
- 内存优化:`docs/内存管理/RAM使用分析与优化方案.md` §1.4
|
||||
- 台架探针:`test/host/ftp_abort_probe.py`、`MDK-ARM/ftp_freeze_probe.py`
|
||||
- 内存优化:`docs/内存管理/RAM使用分析与优化方案.md` §4.1.4
|
||||
|
||||
+114
-50
@@ -64,7 +64,7 @@ static command_t commands[] = {
|
||||
{ NULL, NULL },
|
||||
};
|
||||
|
||||
/* N-19 FTP 鉴权:全局失败计数/锁定状态(跨会话持久——单连接服务器,全局
|
||||
/* FTP 鉴权:全局失败计数/锁定状态(跨会话持久——单连接服务器,全局
|
||||
* 计数即可;成功清零、锁定期满自动放行,见 lftpd_auth.c) */
|
||||
static ftp_auth_state_t s_auth_state;
|
||||
|
||||
@@ -104,6 +104,32 @@ static int send_response(int socket, int code, bool include_code,
|
||||
#define send_multiline_response_end(socket, code, format, ...) \
|
||||
send_response(socket, code, true, false, format, ##__VA_ARGS__)
|
||||
|
||||
/* 一块装满就整块发出、清空继续装。
|
||||
* 为什么仍要"批量":定案三(../问题记录/CH395F_Trap_Records.md,docs 里那条 LIST 吞包
|
||||
* 定案)实测同一连接上**背靠背 ~60 B 小段写**会毒化 CH395F 的 TX(芯片报 SEND_OK 但对端
|
||||
* 全丢,无 RST 无 DISCONNECT),而 RETR 的 1 KB 满块连发安全——所以这里只发大块,
|
||||
* 绝不退回"每行一发"。
|
||||
* 为什么必须"续发"而不是截断:原先装满就 break 并回 -1,cmd_list 据此回 550,客户端
|
||||
* 既拿到**不完整**的目录、又收到错误码(实测 24 条目/1512 B 之后全部被吞)。目录大
|
||||
* 小不该由一个缓冲区上限决定。
|
||||
* 返回:0 = 已发出(或本就空),-1 = 写失败 */
|
||||
static int list_batch_flush(int socket, char *batch, int *p_blen)
|
||||
{
|
||||
int len = *p_blen;
|
||||
|
||||
if (len <= 0) {
|
||||
return 0;
|
||||
}
|
||||
lftpd_task_wd_beat(); /* 看门狗:目录列表分块发送处 */
|
||||
if (lftpd_inet_write(socket, batch, len) < 0) {
|
||||
return -1;
|
||||
}
|
||||
*p_blen = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
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\r\n";
|
||||
@@ -118,11 +144,9 @@ 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;
|
||||
int fail = 0;
|
||||
|
||||
while (lftpd_io_readdir(dp, name, sizeof(name), &size, &is_dir) == 0) {
|
||||
int n;
|
||||
@@ -133,25 +157,45 @@ static int send_list(int socket, const char *path)
|
||||
n = snprintf(batch + blen, sizeof(batch) - blen, file_format,
|
||||
(unsigned long)size, name);
|
||||
}
|
||||
if (n < 0 || (size_t)n >= sizeof(batch) - blen) {
|
||||
trunc = 1;
|
||||
if (n < 0) {
|
||||
fail = 1;
|
||||
break;
|
||||
}
|
||||
if ((size_t)n >= sizeof(batch) - blen) {
|
||||
if (list_batch_flush(socket, batch, &blen) != 0) {
|
||||
fail = 1;
|
||||
break;
|
||||
}
|
||||
/* 单行就超过整块 → 明确失败,绝不静默少发条目
|
||||
* (8.3 短名 + 定长格式远小于 1536,真发生说明格式被改坏) */
|
||||
if ((size_t)n >= sizeof(batch)) {
|
||||
fail = 1;
|
||||
break;
|
||||
}
|
||||
if (is_dir) {
|
||||
n = snprintf(batch, sizeof(batch), directory_format,
|
||||
(unsigned long)size, name);
|
||||
} else {
|
||||
n = snprintf(batch, sizeof(batch), file_format,
|
||||
(unsigned long)size, name);
|
||||
}
|
||||
if (n < 0 || (size_t)n >= sizeof(batch)) {
|
||||
fail = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
blen += n;
|
||||
}
|
||||
|
||||
lftpd_io_closedir(dp);
|
||||
lftpd_io_closedir(dp); /* 所有出口都必须走到:卷锁持有至 closedir */
|
||||
|
||||
/* lftpd_inet_write 成功返回字节数、失败返回 -1(非 0=成功语义) */
|
||||
if (blen > 0) {
|
||||
if (!fail && blen > 0) {
|
||||
/* lftpd_inet_write 成功返回字节数、失败返回 -1(非 0=成功语义) */
|
||||
if (lftpd_inet_write(socket, batch, blen) < 0) {
|
||||
return -1;
|
||||
fail = 1;
|
||||
}
|
||||
}
|
||||
if (trunc) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
return fail ? -1 : 0;
|
||||
}
|
||||
|
||||
static int send_nlst(int socket, const char *path)
|
||||
@@ -165,34 +209,44 @@ 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;
|
||||
int fail = 0;
|
||||
|
||||
while (lftpd_io_readdir(dp, name, sizeof(name), &size, &is_dir) == 0) {
|
||||
if (!is_dir) {
|
||||
int n = snprintf(batch + blen, sizeof(batch) - blen, "%s\r\n", name);
|
||||
if (n < 0 || (size_t)n >= sizeof(batch) - blen) {
|
||||
trunc = 1;
|
||||
if (n < 0) {
|
||||
fail = 1;
|
||||
break;
|
||||
}
|
||||
if ((size_t)n >= sizeof(batch) - blen) {
|
||||
if (list_batch_flush(socket, batch, &blen) != 0) { /* 同 send_list:满块续发 */
|
||||
fail = 1;
|
||||
break;
|
||||
}
|
||||
if ((size_t)n >= sizeof(batch)) {
|
||||
fail = 1;
|
||||
break;
|
||||
}
|
||||
n = snprintf(batch, sizeof(batch), "%s\r\n", name);
|
||||
if (n < 0 || (size_t)n >= sizeof(batch)) {
|
||||
fail = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
blen += n;
|
||||
}
|
||||
}
|
||||
|
||||
lftpd_io_closedir(dp);
|
||||
lftpd_io_closedir(dp); /* 所有出口都必须走到:卷锁持有至 closedir */
|
||||
|
||||
/* lftpd_inet_write 成功返回字节数、失败返回 -1(非 0=成功语义) */
|
||||
if (blen > 0) {
|
||||
if (lftpd_inet_write(socket, batch, blen) < 0) {
|
||||
return -1;
|
||||
if (!fail && blen > 0) {
|
||||
if (lftpd_inet_write(socket, batch, blen) < 0) { /* 成功返回字节数,非 0=成功 */
|
||||
fail = 1;
|
||||
}
|
||||
}
|
||||
if (trunc) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
return fail ? -1 : 0;
|
||||
}
|
||||
|
||||
static int send_file(int socket, const char *path)
|
||||
@@ -250,7 +304,7 @@ static int receive_file(int socket, const char *path)
|
||||
|
||||
if (err < 0) {
|
||||
/* 异常结束(连接重置/超时/预算耗尽):删除截断文件防残留并返回错误码
|
||||
* → STOR 回报 450 而非误报 226(Trap 23 待办 #3) */
|
||||
* → STOR 回报 450 而非误报 226(TRAP-NET-18 待办 #3) */
|
||||
lftpd_io_unlink(path);
|
||||
return err;
|
||||
}
|
||||
@@ -289,7 +343,7 @@ 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 时拒绝(鉴权门已保证此处必为已登录态) */
|
||||
/* 写命令门——[ftp] write=0 时拒绝(鉴权门已保证此处必为已登录态) */
|
||||
const ftp_cfg_t *cfg = ftp_cfg_get();
|
||||
|
||||
if ((cfg == NULL) || (cfg->write_en == 0u)) {
|
||||
@@ -368,7 +422,8 @@ static int cmd_list(lftpd_client_t *client, const char *arg)
|
||||
send_simple_response(client->socket, 150, STATUS_150);
|
||||
int data_sock = lftpd_inet_accept_timeout(client->data_socket, 5000);
|
||||
if (data_sock < 0) {
|
||||
lftpd_log_error("%s accept failed, sk=%d", __func__, client->data_socket);
|
||||
lftpd_log_peer("%s data accept failed (peer gone?), sk=%d",
|
||||
__func__, client->data_socket);
|
||||
send_simple_response(client->socket, 425, STATUS_425);
|
||||
lftpd_inet_close(client->data_socket);
|
||||
client->data_socket = -1;
|
||||
@@ -400,7 +455,8 @@ static int cmd_nlst(lftpd_client_t *client, const char *arg)
|
||||
send_simple_response(client->socket, 150, STATUS_150);
|
||||
int data_sock = lftpd_inet_accept_timeout(client->data_socket, 5000);
|
||||
if (data_sock < 0) {
|
||||
lftpd_log_error("%s accept failed, sk=%d", __func__, client->data_socket);
|
||||
lftpd_log_peer("%s data accept failed (peer gone?), sk=%d",
|
||||
__func__, client->data_socket);
|
||||
send_simple_response(client->socket, 425, STATUS_425);
|
||||
lftpd_inet_close(client->data_socket);
|
||||
client->data_socket = -1;
|
||||
@@ -462,19 +518,14 @@ static int cmd_pass(lftpd_client_t *client, const char *arg)
|
||||
|
||||
static int cmd_pasv(lftpd_client_t *client, const char *arg)
|
||||
{
|
||||
int listener_socket = -1;
|
||||
int retry;
|
||||
|
||||
if (client->data_socket >= 0) {
|
||||
lftpd_inet_close(client->data_socket);
|
||||
client->data_socket = -1;
|
||||
}
|
||||
|
||||
for (retry = 0; retry < 3; retry++) {
|
||||
listener_socket = lftpd_inet_listen(0);
|
||||
if (listener_socket >= 0) break;
|
||||
vTaskDelay(pdMS_TO_TICKS(200));
|
||||
}
|
||||
/* 原 3×200ms 应用层重试已删(lftpd_inet_listen 单次化)——
|
||||
* 失败直接 425,FTP 客户端自带传输重试语义;服务端不再叠阻塞重试 */
|
||||
int listener_socket = lftpd_inet_listen(0);
|
||||
if (listener_socket < 0) {
|
||||
send_simple_response(client->socket, 425, STATUS_425);
|
||||
return -1;
|
||||
@@ -494,6 +545,10 @@ static int cmd_pasv(lftpd_client_t *client, const char *arg)
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* s_addr 是网络序整数(本层唯一约定,见 net_inet_addr 的声明处),这里
|
||||
* 先转主机序再按高字节到低字节拆 = 线序顺位,PASV 227 的 IP 文本因此正确
|
||||
* (TRAP-NET-21 已定案"双重反转恰好正确,勿误判";2026-10-24 字节序统一后复核:
|
||||
* 本行不属于被统一掉的那一族,语义未变,勿"顺手改对"成直接拆 s_addr) */
|
||||
uint32_t ip = net_htonl(local_addr.sin_addr.s_addr);
|
||||
send_simple_response(client->socket, 227, STATUS_227,
|
||||
(ip >> 24) & 0xff,
|
||||
@@ -531,7 +586,8 @@ static int cmd_retr(lftpd_client_t *client, const char *arg)
|
||||
send_simple_response(client->socket, 150, STATUS_150);
|
||||
int data_sock = lftpd_inet_accept_timeout(client->data_socket, 5000);
|
||||
if (data_sock < 0) {
|
||||
lftpd_log_error("%s accept failed, sk=%d", __func__, client->data_socket);
|
||||
lftpd_log_peer("%s data accept failed (peer gone?), sk=%d",
|
||||
__func__, client->data_socket);
|
||||
send_simple_response(client->socket, 425, STATUS_425);
|
||||
lftpd_inet_close(client->data_socket);
|
||||
client->data_socket = -1;
|
||||
@@ -582,7 +638,7 @@ 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 时拒绝(鉴权门已保证此处必为已登录态) */
|
||||
/* 写命令门——[ftp] write=0 时拒绝(鉴权门已保证此处必为已登录态) */
|
||||
const ftp_cfg_t *cfg = ftp_cfg_get();
|
||||
|
||||
if ((cfg == NULL) || (cfg->write_en == 0u)) {
|
||||
@@ -598,7 +654,8 @@ static int cmd_stor(lftpd_client_t *client, const char *arg)
|
||||
send_simple_response(client->socket, 150, STATUS_150);
|
||||
int data_sock = lftpd_inet_accept_timeout(client->data_socket, 5000);
|
||||
if (data_sock < 0) {
|
||||
lftpd_log_error("%s accept failed, sk=%d", __func__, client->data_socket);
|
||||
lftpd_log_peer("%s data accept failed (peer gone?), sk=%d",
|
||||
__func__, client->data_socket);
|
||||
send_simple_response(client->socket, 425, STATUS_425);
|
||||
lftpd_inet_close(client->data_socket);
|
||||
client->data_socket = -1;
|
||||
@@ -663,7 +720,7 @@ static int handle_control_channel(lftpd_client_t *client)
|
||||
{
|
||||
int err = send_simple_response(client->socket, 220, STATUS_220);
|
||||
if (err != 0) {
|
||||
lftpd_log_error("error sending welcome message");
|
||||
lftpd_log_peer("peer gone before welcome acked");
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
@@ -673,7 +730,9 @@ static int handle_control_channel(lftpd_client_t *client)
|
||||
int line_len = lftpd_inet_read_line(client->socket, read_buffer,
|
||||
sizeof(read_buffer));
|
||||
if (line_len != 0) {
|
||||
lftpd_log_error("error reading next command");
|
||||
/* 绝大多数是客户端正常收尾(关窗口 / QUIT 前先断了 TCP);
|
||||
* 真读错也由返回值区分得出来,留 ret=%d 便于排障 */
|
||||
lftpd_log_peer("control channel closed by peer (ret=%d)", line_len);
|
||||
goto cleanup;
|
||||
}
|
||||
|
||||
@@ -698,7 +757,7 @@ static int handle_control_channel(lftpd_client_t *client)
|
||||
command_tmp[i] = (char)toupper((int)command_tmp[i]);
|
||||
}
|
||||
|
||||
/* N-19:未鉴权命令门——仅 QUIT/FEAT/SYST/NOOP 放行(客户端兼容需要,
|
||||
/* 未鉴权命令门——仅 QUIT/FEAT/SYST/NOOP 放行(客户端兼容需要,
|
||||
* 且无信息量);其余一律 530。USER/PASS 本身也在放行名单外,
|
||||
* 但它们在 gate 之前不可能通过 → 单独放行见下(USER/PASS 例外) */
|
||||
if (client->auth_logged_in == 0) {
|
||||
@@ -722,7 +781,7 @@ static int handle_control_channel(lftpd_client_t *client)
|
||||
if (index < (int)strlen(read_buffer)) {
|
||||
strncpy(arg_buf, read_buffer + index + 1, sizeof(arg_buf) - 1);
|
||||
arg_buf[sizeof(arg_buf) - 1] = '\0';
|
||||
arg = lftpd_string_trim(arg_buf);
|
||||
arg = lftpd_string_ltrim(arg_buf);
|
||||
}
|
||||
|
||||
lftpd_log_info("CMD %s %s", command_tmp, arg ? arg : "");
|
||||
@@ -758,7 +817,7 @@ 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 即消失) */
|
||||
/* 出厂默认口令在用告警(台架锚点;改了 [ftp] pwd_hash 即消失) */
|
||||
if (ftp_cfg_is_default_pwd() != 0) {
|
||||
lftpd_log_info("using factory default password");
|
||||
}
|
||||
@@ -791,7 +850,7 @@ int lftpd_start(const char *directory, int port, lftpd_t *lftpd)
|
||||
|
||||
int client_socket = lftpd_inet_accept(lftpd->server_socket);
|
||||
if (client_socket < 0) {
|
||||
lftpd_log_error("error accepting client socket");
|
||||
lftpd_log_peer("listener rebuilt (reason logged by accept layer)");
|
||||
lftpd_inet_close(lftpd->server_socket);
|
||||
lftpd->server_socket = -1; /* 监听器已不可用(网络重建):下轮重建 */
|
||||
continue;
|
||||
@@ -800,11 +859,16 @@ int lftpd_start(const char *directory, int port, lftpd_t *lftpd)
|
||||
{
|
||||
struct net_sockaddr_in addr;
|
||||
int addrlen = sizeof(addr);
|
||||
if (net_getsockname(client_socket,
|
||||
/* E13 定案 修复:原用 getsockname 打印的是**本机** IP:21(且
|
||||
* ntoa 段序反转成 "100.1.168.192")——改 getpeername 打印真实
|
||||
* 对端(现场排障需要"谁连进来");未连接则退回无地址形态 */
|
||||
if (net_getpeername(client_socket,
|
||||
(struct net_sockaddr *)&addr, &addrlen) == 0) {
|
||||
char ip_str[16];
|
||||
net_inet_ntoa(addr.sin_addr.s_addr, ip_str);
|
||||
int remote_port = lftpd_inet_get_socket_port(client_socket);
|
||||
/* 对端端口:直接取 getpeername 结果(原 get_socket_port
|
||||
* 走 getsockname,打印的是本机监听端口) */
|
||||
int remote_port = (int)net_ntohs(addr.sin_port);
|
||||
lftpd_log_info("connection received from %s:%d...",
|
||||
ip_str, remote_port);
|
||||
} else {
|
||||
|
||||
@@ -6,7 +6,7 @@ typedef struct {
|
||||
char directory[256];
|
||||
int socket;
|
||||
int data_socket;
|
||||
/* N-19 FTP 鉴权会话态(每会话 memset 0 = 未登录;跨会话锁定计数在
|
||||
/* FTP 鉴权会话态(每会话 memset 0 = 未登录;跨会话锁定计数在
|
||||
* lftpd.c 的 s_auth_state,不在此处) */
|
||||
char auth_user[32]; /* USER 阶段暂存的用户名 */
|
||||
uint8_t auth_user_ok; /* 1 = 用户名与 [ftp] user 匹配 */
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-11
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-11 王建锋 创建初始版本(N-19 FTP 鉴权;契约由
|
||||
* v1.0 2026-10-11 王建锋 创建初始版本(FTP 鉴权;契约由
|
||||
* test/host/ftp_auth 主机单测钉死)
|
||||
*/
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-11
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-11 王建锋 创建初始版本(N-19 FTP 鉴权;lftpd.c 为唯一消费者)
|
||||
* v1.0 2026-10-11 王建锋 创建初始版本(FTP 鉴权;lftpd.c 为唯一消费者)
|
||||
*/
|
||||
|
||||
#ifndef LFTPD_AUTH_H
|
||||
|
||||
@@ -6,16 +6,13 @@
|
||||
#include <string.h>
|
||||
|
||||
/*
|
||||
* E-02 头阻塞修复 + L3 pending 化(2026-09-22,M3 台架实证):
|
||||
* netTask 是全部 socket 操作的唯一执行者;lftpd 原先用默认 30s 的**阻塞**
|
||||
* net_recv 等待对端下一行/下一块数据 → 只要客户端连上却不发完整行,netTask
|
||||
* 就被长期占用,其余任务(含远端显示)的网络操作整体冻结(实测一次空闲 FTP
|
||||
* 登录让 LCDMEM 整帧等待 17.7s)。
|
||||
* 2026-09-22 先落地分片有界读(5ms 片;实测 100ms 片下受压帧仍达 ~750ms,
|
||||
* 收到 5ms 后降到 ~10ms 量级,见 m3_data 修复后数据),同日 L3 pending 化
|
||||
* 后改为"挂起请求":每次等待 = 一次挂起登记(netTask 零占用,完成按主循环
|
||||
* 节拍检查),调用方单次超时 = 剩余总预算(总等待 ≤ LFTPD_TOTAL_BUDGET_MS,
|
||||
* **总超时语义与旧实现一致**),旧分片协议退役。
|
||||
* 头阻塞修复 + recv pending 化(M3 台架实证):
|
||||
* netTask 是全部 socket 操作的唯一执行者;lftpd 若用**阻塞** net_recv 等待对端
|
||||
* 下一行/下一块数据 → 只要客户端连上却不发完整行,netTask 就被长期占用,
|
||||
* 其余任务(含远端显示)的网络操作整体冻结(实测一次空闲 FTP 登录让 LCDMEM
|
||||
* 整帧等待 17.7s)。
|
||||
* 现形态:每次等待 = 一次挂起登记(netTask 零占用,完成按主循环节拍检查),
|
||||
* 调用方单次超时 = 剩余总预算(总等待 ≤ LFTPD_TOTAL_BUDGET_MS)。
|
||||
*/
|
||||
#define LFTPD_TOTAL_BUDGET_MS 30000U
|
||||
|
||||
@@ -34,39 +31,35 @@ void lftpd_task_wd_beat(void)
|
||||
|
||||
int lftpd_inet_listen(int port)
|
||||
{
|
||||
int retry;
|
||||
|
||||
for (retry = 0; retry < 5; retry++) {
|
||||
lftpd_task_wd_beat(); /* 看门狗:每轮喂心跳(重试间隔 500ms 在 800ms 容差内) */
|
||||
int s = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
if (s < 0) {
|
||||
if (retry == 0) DBG_ERROR("create socket");
|
||||
vTaskDelay(pdMS_TO_TICKS(500));
|
||||
continue;
|
||||
}
|
||||
|
||||
struct net_sockaddr_in addr;
|
||||
addr.sin_family = NET_AF_INET;
|
||||
addr.sin_port = net_htons((uint16_t)port);
|
||||
addr.sin_addr.s_addr = 0;
|
||||
|
||||
if (net_bind(s, (struct net_sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||
DBG_ERROR("bind port %d", port);
|
||||
net_close(s);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (net_listen(s) >= 0) {
|
||||
return s;
|
||||
}
|
||||
|
||||
net_close(s);
|
||||
if (retry < 4) {
|
||||
vTaskDelay(pdMS_TO_TICKS(2000));
|
||||
}
|
||||
/* 应用层阻塞重试退役(原 5 轮 500ms/2000ms,最坏 10s 阻塞
|
||||
* ftpTask)——TRAP-NET-09 的启动形态已由就绪屏障消除(FTP_READY 等 NET_READY,
|
||||
* lftpd_start 外层本就有监听失败重建循环);芯片忙窗失败属罕见,PASV 数据
|
||||
* 监听交给 FTP 客户端自身的重试语义(主流 FTP 服务器 passive listen 失败
|
||||
* 亦直接 425,不在服务端叠应用层阻塞重试) */
|
||||
lftpd_task_wd_beat(); /* 看门狗:入口喂心跳 */
|
||||
int s = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
if (s < 0) {
|
||||
DBG_ERROR("create socket");
|
||||
return -1;
|
||||
}
|
||||
|
||||
DBG_ERROR("listen failed after 5 retries (total 10s wait)");
|
||||
struct net_sockaddr_in addr;
|
||||
addr.sin_family = NET_AF_INET;
|
||||
addr.sin_port = net_htons((uint16_t)port);
|
||||
addr.sin_addr.s_addr = 0;
|
||||
|
||||
if (net_bind(s, (struct net_sockaddr *)&addr, sizeof(addr)) < 0) {
|
||||
DBG_ERROR("bind port %d", port);
|
||||
net_close(s);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (net_listen(s) >= 0) {
|
||||
return s;
|
||||
}
|
||||
|
||||
DBG_ERROR("listen port %d failed", port);
|
||||
net_close(s);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -95,7 +88,8 @@ int lftpd_inet_accept(int socket)
|
||||
return s;
|
||||
}
|
||||
if (net_get_generation() != gen) {
|
||||
DBG_ERROR("network re-init: abort accept, relisten");
|
||||
/* 运维改 IP 的正常后果,不是缺陷 → info(级别划分见存储架构说明 §10.3) */
|
||||
DBG_INFO("network re-init: abort accept, relisten");
|
||||
return -1;
|
||||
}
|
||||
lftpd_task_wd_beat(); /* 看门狗:自旋轮次喂心跳(无限等待,可能长时间无客户端) */
|
||||
@@ -112,10 +106,14 @@ int lftpd_inet_accept_timeout(int socket, int timeout_ms)
|
||||
if (s >= 0) {
|
||||
return s;
|
||||
}
|
||||
/* 数据连接已被对端关闭时提前退出,不等待超时 */
|
||||
net_sock_t *p_sock = net_get_sock(socket);
|
||||
if (p_sock == NULL || p_sock->state != NET_SOCK_STATE_LISTENING) {
|
||||
DBG_ERROR("data socket closed, accept aborted");
|
||||
/* F-NET-15 M2(自愈专项实验 2026-10-12):镜像门控退役——原直读 p_sock->state 在
|
||||
* relisten 忙期(CLOSED 瞬态)误判"已关闭",扔掉 ~4.9s 等待预算;armed 门控
|
||||
* 认"监听器在服役(LISTENING)或待重建且 RX 有残留(closed-with-data 救回窗)",
|
||||
* 与 :7003 的监听生命周期 P1 同型修复 */
|
||||
if (net_listener_armed(socket) == 0) {
|
||||
/* **故意留 error**:控制连接还活着、我方数据监听却已不在服役 →
|
||||
* 这是本侧异常,不是对端行为 */
|
||||
DBG_ERROR("data listener disarmed, accept aborted");
|
||||
return -1;
|
||||
}
|
||||
lftpd_task_wd_beat(); /* 看门狗:每轮喂心跳(数据连接等待最长 5s) */
|
||||
@@ -123,7 +121,8 @@ int lftpd_inet_accept_timeout(int socket, int timeout_ms)
|
||||
elapsed += 100;
|
||||
}
|
||||
|
||||
DBG_ERROR("accept timeout (%dms)", timeout_ms);
|
||||
/* 客户端连上控制通道却没来建数据连接(中止/被防火墙拦)= 对端行为 → info */
|
||||
DBG_INFO("accept timeout (%dms) - peer never opened data channel", timeout_ms);
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -135,8 +134,8 @@ int lftpd_inet_read_line(int socket, char *buffer, size_t buffer_len)
|
||||
int total_read_len = 0;
|
||||
|
||||
while (total_read_len < (int)buffer_len) {
|
||||
/* 单次预算 = 剩余总预算(L3 pending:等待期 netTask 零占用,调用方
|
||||
* 在自己上下文睡任务通知,旧片间 vTaskDelay(1) 让出退役) */
|
||||
/* 单次预算 = 剩余总预算(pending:等待期 netTask 零占用,调用方
|
||||
* 在自己上下文睡任务通知) */
|
||||
TickType_t elapsed = xTaskGetTickCount() - t0;
|
||||
uint32_t rem = (elapsed >= (TickType_t)pdMS_TO_TICKS(LFTPD_TOTAL_BUDGET_MS)) ? 0U
|
||||
: (uint32_t)((TickType_t)LFTPD_TOTAL_BUDGET_MS - elapsed);
|
||||
@@ -217,8 +216,8 @@ int lftpd_inet_write(int socket, const void *buf, int len)
|
||||
|
||||
int lftpd_inet_read(int socket, void *buf, int len)
|
||||
{
|
||||
/* L3 pending 读:总预算 30s 不变,等待期 netTask 零占用
|
||||
* (空闲数据连接不再冻结全网;旧分片协议随 L3 pending 化退役) */
|
||||
/* pending 读:总预算 30s 不变,等待期 netTask 零占用
|
||||
* (空闲数据连接不再冻结全网) */
|
||||
TickType_t t0 = xTaskGetTickCount();
|
||||
|
||||
for (;;) {
|
||||
|
||||
@@ -3,14 +3,16 @@
|
||||
|
||||
#include "private/lftpd_string.h"
|
||||
|
||||
char *lftpd_string_trim(char *s)
|
||||
char *lftpd_string_ltrim(char *s)
|
||||
{
|
||||
char *p = s;
|
||||
/* 只去行首空白:返回跳过后的指针,不搬移缓冲。
|
||||
* 原先此处的"去尾部空白"循环,起点是 strlen(p) 那一格 NUL,isspace('\0') 为假
|
||||
* → 从未执行过;而 FTP 参数的尾随空格属于参数本体(见本文件头与
|
||||
* private/lftpd_string.h 的说明),2026-10-23 删掉这条死代码并改名为 *_ltrim,
|
||||
* 让函数名与实际行为一致(**行为零变化**)。 */
|
||||
for (int i = 0, len = (int)strlen(s); i < len && isspace((int)s[i]); i++) {
|
||||
p++;
|
||||
}
|
||||
for (int i = (int)strlen(p); i >= 0 && isspace((int)p[i]); i--) {
|
||||
p[i] = '\0';
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user