merge: v2-on-ui 并入 master(49 个提交)
把开发主线 v2-on-ui 整体并入远端默认分支 master。
关系:master 是 v2-on-ui 的祖先(v2-on-ui..master = 0),无分叉、
无并集合并残留;预演 git merge-tree rc=0(结果树 db30893d)。
按项目惯例用 --no-ff 保留合并记录。
内容概要(自 master 66186e1 起 49 个提交):
- B4 IEC104 上行规约(App/protocol/iec104/ 三件套 + iec104Task 2404)
- N-32 CH395F 监听生命周期根治(P0 口径修正 / P1 session_dead 电平化 /
P2a 监听器 pump 单一所有者 / §10 抽 net_listener.c 独立模块)
- N-27 RAM 台账三件套 + mem-budget 92% 阈值门禁
- N-19 FTP 鉴权、N-30/N-31、N-33 等
- 多任务并行开发机制(task-worktree skill + 设计文档)
合并后内存水位:RW_IRAM1 73.4% / RW_CCM 87.9%(阈值 92%)。
This commit is contained in:
@@ -5,6 +5,7 @@
|
||||
"fault-handlers-3": "f3afab8d2be18794",
|
||||
"fault-handler-enable": "be87fcd3a7926138",
|
||||
"hardfault-report-impl": "f3afab8d2be18794",
|
||||
"error-handler-degrade": "d02a92afda945c4b",
|
||||
"stack-overflow-check": "d4735e3a265e16ee",
|
||||
"malloc-failed-hook": "6b86b273ff34fce1",
|
||||
"assert-not-deadloop": "d4a00718ab2bd071",
|
||||
@@ -12,14 +13,21 @@
|
||||
"stack-overflow-hook-impl": "a006edbd30d2c4d7",
|
||||
"malloc-failed-hook-impl": "fcd7250e5cd6403b",
|
||||
"stack-overflow-hook-helper": "cacd9fd028be9b5b",
|
||||
"watchdog-module": "3d908285b092c402",
|
||||
"watchdog-module": "663f05caf22cf7d0",
|
||||
"reset-cause-latch": "52fc71acbb0f1aed",
|
||||
"iwdg-hal-driver": "4b902875a84554e9",
|
||||
"hal-iwdg-enabled": "1a8ed6cff34f7ba6",
|
||||
"ioc-keepusercode": "b5bea41b6c623f7c",
|
||||
"ioc-iwdg-present": "d3a6e667048c95db",
|
||||
"ioc-task-stacks": "3262f737d92dcbee",
|
||||
"adc1-hal-enabled": "1a8ed6cff34f7ba6"
|
||||
"adc1-hal-enabled": "1a8ed6cff34f7ba6",
|
||||
"adc1-calibration-call": "8d5f4b40fb218913",
|
||||
"adc1-calibration-impl": "d33bc4323e243248",
|
||||
"adc-tick-tim3-branch": "39c3548c37f6c4ae",
|
||||
"ioc-adc-trig-t3trgo": "a9939de260ee46f8",
|
||||
"ioc-adc-dma-continuous": "189126670d85d80a",
|
||||
"adc1-dma-link": "397fce92dca3b728",
|
||||
"adc1-dma-irq-priority": "beff08e91333229b"
|
||||
},
|
||||
"_note": "CubeMX 存活审计基线;改动用 --write 重建"
|
||||
}
|
||||
|
||||
@@ -35,9 +35,11 @@ whenToUse: 需要编译 Keil MDK 工程、验证代码改动是否 0 错误 0
|
||||
硬件闭环只会把编译错误伪装成运行问题。
|
||||
- **警告也算失败**:判 PASS 只认退出码 0,别只看"有没有 error:"。
|
||||
- **`MDK-ARM/*.uvprojx` 每次构建后被 UV4 自动改写,改动一律不入库**:UV4 会把工程
|
||||
XML 归一化成本机风格(`<Tag />` → `<Tag></Tag>` 等)并把 `PackID` 降到本机 DFP 版本。
|
||||
XML 归一化成本机风格(`<Tag />` → `<Tag></Tag>`、XML 声明加 `standalone="no"`)并把
|
||||
`PackID` 降到**本机装的 DFP 版本**(属降级——提交后会把别人的 Pack 版本一起改掉)。
|
||||
判断:`git diff` 里只见 XML 格式/`PackID` 差异而**无源文件挂载增删** →
|
||||
`git checkout -- <uvprojx>` 丢弃;只有真的增删了源文件(grep 文件名计数变化)才提交。
|
||||
同目录的 `*.uvoptx` / `*.uvguix.*` 是 IDE 个人设置,已在 `.gitignore` 里,不会露出来。
|
||||
- 报错定位读日志别只看终端回显:本仓库 = `MDK-ARM\build_log.txt`。
|
||||
- **exit 2 先分清"真没设"还是"会话没继承"**(2026-10-10 实测):agent 的 shell 由
|
||||
常驻服务派生,服务启动**之后**设置的用户级环境变量不会自动进来。查
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
name: lvgl-simulator
|
||||
description: LVGL PC 模拟器构建/运行/自检入口:build_sim.bat 自动探测本机 VS、lvgl_sim.exe 无头截图与自检开关(--screenshot/--keys/--standby/--cfgtest/--logtest/--idle)、三套回归脚本、两份 lv_conf.h 同步门禁、构建报错速查(SDL2/lv_conf 找不到、全黑、键盘无响应)。
|
||||
whenToUse: 需要构建或运行模拟器、无头截图出图、跑模拟器自检或 UI 回归、修改 simulator/ 目录代码或 simulator/lv_conf.h、排查模拟器构建/运行异常时。UI 改动后的完整回归走 ui-verify skill。
|
||||
---
|
||||
|
||||
# LVGL PC 模拟器(simulator/)
|
||||
|
||||
PC 上编译运行与 MCU **同一份** UI 源码(CMake + MSVC + SDL2,x64,160×160 I1 单色),免烧录。
|
||||
**目录架构 / 完整坑与 FAQ → `simulator/README.md`;首次部署(SDL2 SDK)→ `docs/UI设计/LVGL模拟器部署与使用说明.md`。本 skill 只放操作入口。**
|
||||
|
||||
## 构建与运行(仓库根执行)
|
||||
|
||||
```powershell
|
||||
& "simulator\build_sim.bat" # 增量构建;vswhere 自动探测本机 VS(16/17/18),勿写死生成器
|
||||
& "simulator\build_sim.bat" -Clean # 全量重建(遇到奇怪问题先加它)
|
||||
.\simulator\build\Debug\lvgl_sim.exe # 窗口模式:↑↓←→ / Enter / Esc(纯键盘,无鼠标)
|
||||
```
|
||||
|
||||
⚠ 窗口模式默认 **60 s 空闲自动退出**(防挂后台),`--idle <秒>` 覆盖。
|
||||
|
||||
## 无头自检(退出码 0=PASS / 非 0=FAIL)
|
||||
|
||||
| 命令(在 `simulator\build\Debug\` 下或用全路径) | 用途 |
|
||||
|---|---|
|
||||
| `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 --logtest` | syslog 后端闭环(定长记录/时间戳/轮转) |
|
||||
|
||||
## UI 改动回归 → 用 `ui-verify` skill(勿在此零散手跑)
|
||||
|
||||
5 步门禁 = 构建 → `check_menu_rows.py`(菜单 8 状态像素 + 池基线)→ `check_content_pages.py`(文本断言 + 11 页黄金参考 + 往返池)→ `@build` → `coding-standard`。
|
||||
RemoDispBus 推流回归单独跑:`.venv\Scripts\python.exe simulator/tests/check_remo_sink.py`。
|
||||
黄金参考 `tests/ref/` 是**本地生成物不入库**:换机后先 `check_content_pages.py --update` 生成,再 `regen_ref_png.py` 出目检 PNG。
|
||||
|
||||
## 两条铁律
|
||||
|
||||
- **改 `simulator/lv_conf.h` 触发 `lv-conf-sync` 门禁**:它与 MCU 端 `Lib/lv_conf.h` 必须保持同一特性子集(按**生效值**比对,未定义取 `lv_conf_internal.h` 兜底);白名单与四类判定 → `docs/代码规范/pre-commit使用说明.md` §2.15 / §5.5
|
||||
- **模拟器代码与固件同标准受检**(`cppcheck` / `coding-standard` / 乱码 / 改写型钩子 / `doc-links` 全覆盖 `simulator/`);**改动触及 `sim/` 任何后端,必须同步在 `docs/测试说明/模拟器差距清单.md` 登记一行**——模拟器跑过 ≠ 真机跑过
|
||||
|
||||
## 构建/运行报错速查(详解 `simulator/README.md`「四、已知坑(构建/运行)」)
|
||||
|
||||
| 现象 | 根因 |
|
||||
|---|---|
|
||||
| 找不到 `SDL2/SDL.h` | LVGL SDL 驱动写死该路径,头文件须有 `include/SDL2/` 副本(仓库已内置) |
|
||||
| 找不到 `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 定时器统一分发,先轮询会抢光事件 |
|
||||
| 中文显示空白 | 子集字体缺字 → **`font-regen` skill** 重生成 `ui_font_cn_12` |
|
||||
| 新加 UI `.c` 一端编不过 | 双端注册(uvprojx + CMakeLists)→ **`new-source-file` skill** |
|
||||
@@ -46,7 +46,7 @@ whenToUse: 换新机器/新克隆要把环境搭起来、构建或烧录报"环
|
||||
(`env_check.py` 对这种情况报 WARN 并给注入命令。)
|
||||
- 换机后 COM 口几乎必变:只改 `.serialtest/config.json` 的 `portName`,
|
||||
然后 `serial_tool.py restart`(daemon 启动时缓存配置)。
|
||||
- 串口"拒绝访问"先查残留旧 daemon(AGENTS.md 串口助手节),不是驱动问题。
|
||||
- 串口"拒绝访问"先查残留旧 daemon(`serial-assistant` skill「陷阱」节),不是驱动问题。
|
||||
|
||||
## 其它项目怎么套
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
__pycache__/
|
||||
@@ -0,0 +1,111 @@
|
||||
---
|
||||
name: task-worktree
|
||||
description: 多任务并行开发的空间管理(git worktree 生命周期):new 开一棵工作树 + 任务分支、目录联接共享 .venv 与模拟器 SDK、build/bench 两类独占锁、done 预演合并回主干并审计并集残留。规格见 docs/环境与部署/并行任务工作树方案.md。
|
||||
whenToUse: 要同时推进多个任务、担心同分支同文档互相冲突;要为新任务开一个隔离空间;要把任务合并回主干;或遇到两个任务抢 COM6 / UV4、"改了文档被覆盖"、并集合并残留时。
|
||||
---
|
||||
|
||||
# task-worktree — 多任务并行开发空间(短命令 `@task`)
|
||||
|
||||
**执行体 = 本 skill 自带脚本** `.dsh/skills/task-worktree/scripts/task_worktree.py`
|
||||
(纯标准库,任意 python3;拷整个 skill 目录到别的项目即可用)。
|
||||
|
||||
```powershell
|
||||
# cwd = 仓库根(或任意子目录)
|
||||
& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" <子命令> [参数]
|
||||
```
|
||||
|
||||
退出码:`0`=成功 / `1`=操作失败 / `2`=前置或环境错误 / `3`=用法。
|
||||
|
||||
## 固定命令
|
||||
|
||||
| 命令 | 作用 |
|
||||
|---|---|
|
||||
| `new <ID> <slug>` | 建分支 `task/<ID>-<slug>` + 工作树 + 共享联接 + 台账;打印新空间路径 |
|
||||
| `list` | 所有任务一览:分支 / 落后领先 / 脏文件数 / 台架状态 + 两个锁的归属 |
|
||||
| `status <ID>` | 单任务详情(未提交清单、相对基线漂移、台架记录、已建联接) |
|
||||
| `sync <ID>` | 在任务工作树里 `rebase` 追平主干,把冲突控制在任务分支上 |
|
||||
| `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`(不建联接)。
|
||||
|
||||
`done` 常用开关:`--dry-run`(只预演,零改动)· `--skip-build` · `--skip-gates` · `--force`。
|
||||
|
||||
**环境变量**(高级):`DTU_WT_ROOT`(任务容器位置,默认仓库同级 `_wt/`)·
|
||||
`DTU_TASK_BASE`(默认基线分支,默认 `v2-on-ui`)·
|
||||
`DTU_INTEGRATION_WORKTREE`(集成点,默认主工作树——用于隔离演练/测试)。
|
||||
|
||||
## 典型流程
|
||||
|
||||
```powershell
|
||||
& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" new N21 dhara-isfree --needs-bench
|
||||
# → 把新会话的工作目录切到打印出来的 _wt/N21-dhara-isfree/,在那里干活
|
||||
|
||||
& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" lock acquire bench --task N21
|
||||
# 烧录 / @test / 板级套件……
|
||||
& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" lock release bench --task N21 --bench-pass
|
||||
|
||||
# 回到主工作树执行回收
|
||||
& $PY ".dsh\skills\task-worktree\scripts\task_worktree.py" done N21
|
||||
```
|
||||
|
||||
## 铁律
|
||||
|
||||
1. **`done` 只能在主工作树执行**——主干分支检出在主工作树,git 不允许同一分支在两处检出。
|
||||
2. **`done` 不 push**。它结束于本地合入 + 门禁全绿;推送需用户明确许可(项目既有纪律)。
|
||||
3. **`build` 与 `bench` 都必须持锁**:UV4 不能两个实例同时 `-b`;串口 COM6、ST-LINK、
|
||||
设备 IP `192.168.1.100` 各只有一份。**构建与烧录都不可并发。**
|
||||
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 检出)。
|
||||
7. **工作树放仓库同级 `_wt/`,绝不在仓库内部**——否则污染 `git status`、`pre-commit --all-files` 与全库搜索。
|
||||
|
||||
## 陷阱(实测)
|
||||
|
||||
- **联接不是符号链接**:本仓库 `core.symlinks=false`,git 里的 symlink 会被检出成
|
||||
「内容是目标路径的文本文件」。共享 `.venv` 只能用 Windows **目录联接**(`mklink /J`,
|
||||
不需要管理员权限),且联接必须建在版本控制之外。
|
||||
实测(2026-10-12):用 ctypes 直调 `DeviceIoControl(FSCTL_SET_REPARSE_POINT)`
|
||||
会被系统拒绝(Win32 4392),`mklink /J` 正常——所以脚本走后者,参数以列表交给
|
||||
subprocess,含空格的路径(本项目 `DTU Code`)由 subprocess 自动加引号。
|
||||
- **联接建失败不中止 `new`**:只告警并打印手工补建命令,避免留下半成品任务空间;
|
||||
但该任务里跑不了 `.venv\Scripts\python.exe`,`@build` / `ci_local.py` 会找不到解释器。
|
||||
- **`done` 前主工作树必须干净**:否则未提交改动会被卷进合并。
|
||||
- **⚠ `done` 的门禁跑在 `@build` 之前 → 依赖 `.map` 的钩子会误报**(2026-10-12 实测):
|
||||
`done` 的顺序是「合并 → 重生成 → **门禁(ci_local)** → `@build`」,而
|
||||
`mem-budget`(N-27)/ `mem-numbers` 是拿**文档快照 vs `.map`** 比对 —— 门禁跑时
|
||||
`.map` 还是**主工作树合并前**的旧产物,与刚合并进来的新台账**必然不一致** →
|
||||
误报 `[FAIL] 台账受管区与 .map 不一致`。`@build` 跑完后 `.map` 才是新的。
|
||||
**处置**:门禁失败且只差这两项时,按提示在主工作树跑
|
||||
`python scripts/ram_baseline.py --write` + `python scripts/check_mem_numbers.py --write`
|
||||
后追加提交,再重跑 `done`(它会检测到「已合入」直接清场)。
|
||||
- **⚠ 增量构建可能不重链 → `.map` 数字瞬态不一致**(2026-10-12 实测):
|
||||
`@build` 若判定无需重链,`.map` 保持旧值,此时 `--write` 生成的快照是**瞬态值**
|
||||
(实测同一份源码两次读到 223,560 与 223,724,差 164 B)。**以主工作树(集成点)
|
||||
的 `.map` 为准**;改完 RAM 相关代码后确认重链再刷新快照。
|
||||
- **合并冲突常落在「生成物」上**:`docs/内存管理/RAM使用分析与优化方案.md`(mem-numbers
|
||||
快照)在双方都改过时必冲突,且**两边都不能直接选**(各自基于不同基线)——
|
||||
正确做法是解冲突后用**合并后的实际构建**重新生成,不要挑一侧。
|
||||
- **并集合并残留**:双方都改过的文件(尤其 `.gitignore`)会被 git 自动并成**并集**,
|
||||
可能留下指向已删目录的死规则或重复条目。脚本会自动报「双方都改过」的文件清单 +
|
||||
连续重复行;**权威判据是 base / 两个 tip 三方对比**(`git show <ref>:<file>` 逐项 `grep -F`),
|
||||
别用 `tail -N` 目测。
|
||||
- **`.workbuddy-ai/` 刻意不共享**:记忆不随任务分叉,也不给联接递归删除的机会。
|
||||
任务工作树自己的记忆由 `done` / `abort` 在清场前归并回主工作树。
|
||||
- **`--lean` 只省磁盘不省时间**:实测全量 smudge 7.25 s / 83 MB,跳过 smudge 7.05 s / 45 MB,
|
||||
两者工作区都干净。默认走全量(参考图可用),磁盘紧张时才用 `--lean`。
|
||||
- **锁不绑定进程存活**:`lock acquire` 本身是短命进程,而真正用资源的烧录/构建在别的进程里跑。
|
||||
所以判「陈旧」只看**时效**(默认 6 小时,`LOCK_TTL_HOURS`)加显式 `--force`;pid 仅作排障记录。
|
||||
同任务重复 acquire 是幂等的(不会把自己锁死)。
|
||||
- **陈旧锁可自动接管**:超过 6 小时未释放即视为陈旧,下次 acquire 会接管并提示;活锁需 `--force`。
|
||||
|
||||
## 与其它 skill 的衔接
|
||||
|
||||
- 构建 = `keil-build` skill(`@build`);烧录/回读 = `stlink-flash` skill;串口闭环 = `serial-boot-test` skill
|
||||
- 新 `.c` 文件 = `new-source-file` skill(`uvprojx` 与 `CMakeLists.txt` 双端注册)
|
||||
- 完整机制与目录分配 → `docs/环境与部署/并行任务工作树方案.md`
|
||||
@@ -0,0 +1,857 @@
|
||||
#!/usr/bin/env python3
|
||||
"""task-worktree — 多任务并行开发的空间管理(git worktree 生命周期)
|
||||
|
||||
执行体 = 本脚本(纯标准库)。规格见 docs/环境与部署/并行任务工作树方案.md,
|
||||
用法与铁律见同目录 SKILL.md。
|
||||
|
||||
退出码:0=成功 / 1=操作失败 / 2=前置或环境错误 / 3=用法错误
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
# 输出统一 UTF-8,避免被控制台代码页(cp936)转码破坏
|
||||
for _stream in (sys.stdout, sys.stderr):
|
||||
try:
|
||||
_stream.reconfigure(encoding="utf-8")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
EXIT_OK, EXIT_FAIL, EXIT_ENV, EXIT_USAGE = 0, 1, 2, 3
|
||||
|
||||
BRANCH_PREFIX = "task/"
|
||||
MAX_CONCURRENT = 3
|
||||
LOCK_NAMES = ("build", "bench")
|
||||
LOCK_TTL_HOURS = 6.0 # 超过此持有时间即视为陈旧锁,下次 acquire 自动接管
|
||||
JUNCTION_SPECS = (
|
||||
(".venv", "Python 环境"),
|
||||
("simulator/SDL2", "模拟器 SDL2 SDK"),
|
||||
)
|
||||
FILE_ATTRIBUTE_REPARSE_POINT = 0x400
|
||||
|
||||
ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]*$")
|
||||
SLUG_RE = re.compile(r"^[a-z0-9][a-z0-9-]*$")
|
||||
|
||||
|
||||
class Fail(Exception):
|
||||
"""操作失败(退出码 1)"""
|
||||
|
||||
|
||||
class EnvError(Exception):
|
||||
"""前置或环境错误(退出码 2)"""
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- 仓库定位
|
||||
|
||||
|
||||
def _main_worktree_of(start: Path):
|
||||
"""借 git 反查「主工作树」路径——脚本可能被从任务工作树里调用,不能只靠 __file__"""
|
||||
for extra in (["--path-format=absolute"], []):
|
||||
r = subprocess.run(
|
||||
["git", "-C", str(start), "rev-parse", *extra, "--git-common-dir"],
|
||||
capture_output=True, text=True, encoding="utf-8", errors="replace",
|
||||
stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if r.returncode == 0 and r.stdout.strip():
|
||||
common = Path(r.stdout.strip())
|
||||
if not common.is_absolute():
|
||||
common = (start / common).resolve()
|
||||
return common.parent if common.name == ".git" else common
|
||||
return None
|
||||
|
||||
|
||||
_SCRIPT_ROOT = Path(__file__).resolve().parents[4]
|
||||
REPO_ROOT = _main_worktree_of(_SCRIPT_ROOT) or _SCRIPT_ROOT
|
||||
WT_ROOT = Path(os.environ.get("DTU_WT_ROOT") or (REPO_ROOT.parent / "_wt"))
|
||||
BASE_BRANCH = os.environ.get("DTU_TASK_BASE", "v2-on-ui")
|
||||
|
||||
STATE_DIR = WT_ROOT / ".state"
|
||||
LOCKS_DIR = STATE_DIR / "locks"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- 基础工具
|
||||
|
||||
|
||||
def git(*args, cwd=None, check=True, env=None):
|
||||
cmd = ["git"] + [str(a) for a in args]
|
||||
full_env = dict(os.environ)
|
||||
if env:
|
||||
full_env.update(env)
|
||||
r = subprocess.run(
|
||||
cmd, cwd=str(cwd or REPO_ROOT), capture_output=True, text=True,
|
||||
encoding="utf-8", errors="replace", stdin=subprocess.DEVNULL, env=full_env,
|
||||
)
|
||||
if check and r.returncode != 0:
|
||||
raise Fail("git {} 失败(rc={})\n{}".format(
|
||||
" ".join(cmd[1:]), r.returncode, (r.stdout + r.stderr).strip()))
|
||||
return r
|
||||
|
||||
|
||||
def git_out(*args, **kw):
|
||||
return git(*args, **kw).stdout.strip()
|
||||
|
||||
|
||||
def worktree_entries():
|
||||
out = git_out("worktree", "list", "--porcelain")
|
||||
entries, cur = [], None
|
||||
for line in out.splitlines():
|
||||
if line.startswith("worktree "):
|
||||
if cur:
|
||||
entries.append(cur)
|
||||
cur = {"path": Path(line[len("worktree "):]), "branch": None}
|
||||
elif cur is None:
|
||||
continue
|
||||
elif line.startswith("branch refs/heads/"):
|
||||
cur["branch"] = line[len("branch refs/heads/"):]
|
||||
if cur:
|
||||
entries.append(cur)
|
||||
return entries
|
||||
|
||||
|
||||
def main_worktree() -> Path:
|
||||
"""集成点(合并发生的地方)。默认 = 主工作树;可用环境变量覆盖,便于演练/测试。"""
|
||||
override = os.environ.get("DTU_INTEGRATION_WORKTREE")
|
||||
if override:
|
||||
p = Path(override)
|
||||
if not p.is_absolute():
|
||||
p = (REPO_ROOT / p).resolve()
|
||||
if not p.is_dir():
|
||||
raise EnvError("DTU_INTEGRATION_WORKTREE 不存在:{}".format(p))
|
||||
return p
|
||||
entries = worktree_entries()
|
||||
if not entries:
|
||||
raise EnvError("无法解析 git worktree 列表")
|
||||
return entries[0]["path"]
|
||||
|
||||
|
||||
def venv_python(tree: Path = None) -> Path:
|
||||
"""优先用该工作树自己的 .venv(任务树里是指向主树的联接),否则退回主工作树"""
|
||||
for root in (tree, REPO_ROOT):
|
||||
if root is None:
|
||||
continue
|
||||
p = Path(root) / ".venv" / "Scripts" / "python.exe"
|
||||
if p.is_file():
|
||||
return p
|
||||
return Path(sys.executable)
|
||||
|
||||
|
||||
def run_script(script: str, args, cwd: Path, label: str, tail: int = 40) -> bool:
|
||||
"""跑项目脚本并回报。输出可能上千行,只回显尾部(结论与报错都在尾部)
|
||||
|
||||
⚠ 脚本必须取自 **cwd 那棵树**,不能固定用 REPO_ROOT:
|
||||
项目脚本普遍以 `__file__` 反推仓库根(如 `scripts/ci_local.py`、
|
||||
`scripts/check_registration.py`),若拿主树的脚本、却给集成树的 cwd,
|
||||
脚本仍会去校验主树——门禁就测错了对象(2026-10-12 实测踩到:
|
||||
报出主树才有的 iec104 未注册缺口,而集成树里根本没那些文件)。
|
||||
"""
|
||||
script_path = Path(cwd) / script
|
||||
if not script_path.is_file():
|
||||
script_path = REPO_ROOT / script
|
||||
cmd = [str(venv_python(Path(cwd))), str(script_path)] + [str(x) for x in args]
|
||||
r = subprocess.run(
|
||||
cmd, cwd=str(cwd), capture_output=True, text=True,
|
||||
encoding="utf-8", errors="replace", stdin=subprocess.DEVNULL,
|
||||
)
|
||||
out = (r.stdout + r.stderr).strip()
|
||||
print("[{}] {}".format("ok" if r.returncode == 0 else "!!", label))
|
||||
if out:
|
||||
lines = out.splitlines()
|
||||
if len(lines) > tail:
|
||||
print(" …(前 {} 行略)".format(len(lines) - tail))
|
||||
lines = lines[-tail:]
|
||||
print(" " + "\n ".join(lines))
|
||||
return r.returncode == 0
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- 台账
|
||||
|
||||
|
||||
def state_file(task_id: str) -> Path:
|
||||
return STATE_DIR / (task_id + ".json")
|
||||
|
||||
|
||||
def load_state(task_id: str) -> dict:
|
||||
p = state_file(task_id)
|
||||
if not p.is_file():
|
||||
raise EnvError("任务 {} 不存在(缺台账 {})".format(task_id, p))
|
||||
return json.loads(p.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def save_state(st: dict) -> None:
|
||||
STATE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
state_file(st["id"]).write_text(
|
||||
json.dumps(st, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def all_states():
|
||||
if not STATE_DIR.is_dir():
|
||||
return []
|
||||
out = []
|
||||
for p in sorted(STATE_DIR.glob("*.json")):
|
||||
try:
|
||||
out.append(json.loads(p.read_text(encoding="utf-8")))
|
||||
except Exception:
|
||||
continue
|
||||
return out
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- 独占锁
|
||||
|
||||
|
||||
def lock_age_hours(info) -> float:
|
||||
try:
|
||||
return max(0.0, (time.time() - float(info.get("epoch") or 0)) / 3600.0)
|
||||
except (TypeError, ValueError):
|
||||
return float("inf")
|
||||
|
||||
|
||||
def lock_path(name: str) -> Path:
|
||||
return LOCKS_DIR / (name + ".lock")
|
||||
|
||||
|
||||
def lock_read(name: str):
|
||||
p = lock_path(name)
|
||||
if not p.is_file():
|
||||
return None
|
||||
try:
|
||||
return json.loads(p.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
return {"task": "?", "pid": 0, "epoch": 0, "at": "?"}
|
||||
|
||||
|
||||
def lock_acquire(name: str, task: str, wait: int = 0, force: bool = False, note: str = ""):
|
||||
"""抢占独占资源。
|
||||
|
||||
⚠ 锁**不**绑定进程存活:`lock acquire` 本身是短命进程,而用锁的烧录/构建
|
||||
在另一个进程里跑。因此判「陈旧」只看**时效**(超过 LOCK_TTL_HOURS),
|
||||
外加显式 `--force`;pid 仅作记录便于排障。
|
||||
"""
|
||||
LOCKS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
p = lock_path(name)
|
||||
deadline = time.time() + max(0, wait)
|
||||
while True:
|
||||
try:
|
||||
fd = os.open(str(p), os.O_CREAT | os.O_EXCL | os.O_WRONLY)
|
||||
except FileExistsError:
|
||||
info = lock_read(name) or {}
|
||||
if info.get("task") == task:
|
||||
return True, info
|
||||
age = lock_age_hours(info)
|
||||
if force or age > LOCK_TTL_HOURS:
|
||||
print("[lock] 接管 {} 锁(原持有者 {},已持有 {:.1f} 小时{})".format(
|
||||
name, info.get("task"), age, ",--force" if force else ",视为陈旧"))
|
||||
try:
|
||||
p.unlink()
|
||||
except OSError:
|
||||
pass
|
||||
continue
|
||||
if time.time() >= deadline:
|
||||
return False, info
|
||||
print("[lock] {} 被 {} 占用(已持有 {:.1f} 小时),等待…".format(
|
||||
name, info.get("task"), age))
|
||||
time.sleep(2)
|
||||
else:
|
||||
now = time.time()
|
||||
payload = {
|
||||
"name": name, "task": task, "pid": os.getpid(),
|
||||
"host": os.environ.get("COMPUTERNAME", "?"),
|
||||
"epoch": now,
|
||||
"at": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(now)),
|
||||
"note": note,
|
||||
}
|
||||
os.write(fd, (json.dumps(payload, ensure_ascii=False) + "\n").encode("utf-8"))
|
||||
os.close(fd)
|
||||
return True, payload
|
||||
|
||||
|
||||
def lock_release(name: str, task=None, force: bool = False):
|
||||
p = lock_path(name)
|
||||
if not p.is_file():
|
||||
return True, "锁 {} 未被持有".format(name)
|
||||
info = lock_read(name) or {}
|
||||
if not force and task and info.get("task") not in (task, None):
|
||||
return False, "锁 {} 由 {} 持有,与 {} 不符;强制释放请加 --force".format(
|
||||
name, info.get("task"), task)
|
||||
try:
|
||||
p.unlink()
|
||||
except OSError as e:
|
||||
return False, "释放锁 {} 失败:{}".format(name, e)
|
||||
return True, "已释放锁 {}(原持有者 {})".format(name, info.get("task"))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- 目录联接
|
||||
|
||||
|
||||
def is_reparse(p: Path) -> bool:
|
||||
try:
|
||||
st = os.lstat(str(p))
|
||||
except OSError:
|
||||
return False
|
||||
return bool(getattr(st, "st_file_attributes", 0) & FILE_ATTRIBUTE_REPARSE_POINT)
|
||||
|
||||
|
||||
def make_junction(link: Path, target: Path) -> None:
|
||||
"""创建 Windows 目录联接(不需要管理员权限,也不是符号链接)
|
||||
|
||||
走 `mklink /J`——这是唯一不需要 SeCreateSymbolicLinkPrivilege 的目录链接形式。
|
||||
⚠ 实测(2026-10-12):直接用 ctypes 调 `DeviceIoControl(FSCTL_SET_REPARSE_POINT)`
|
||||
会被系统拒绝(Win32 4392 / ERROR_REPARSE_TAG_INVALID,缓冲区经 hexdump 核对无误),
|
||||
而 `mklink /J` 正常,故采用后者。参数以**列表**交给 subprocess,
|
||||
含空格的路径(本项目 `DTU Code`)由 subprocess 自动加引号,不经手写引号。
|
||||
"""
|
||||
if os.name != "nt":
|
||||
raise EnvError("目录联接是 Windows 专属能力;当前平台 {}".format(os.name))
|
||||
if os.path.lexists(str(link)):
|
||||
raise Fail("联接位置已被占用:{}".format(link))
|
||||
if not target.exists():
|
||||
raise Fail("联接源不存在:{}".format(target))
|
||||
link.parent.mkdir(parents=True, exist_ok=True)
|
||||
r = subprocess.run(
|
||||
["cmd", "/c", "mklink", "/J", str(link), str(target)],
|
||||
capture_output=True, stdin=subprocess.DEVNULL,
|
||||
)
|
||||
if r.returncode != 0 or not is_reparse(link):
|
||||
detail = (r.stdout or b"").decode("mbcs", "replace").strip()
|
||||
raise Fail("创建目录联接失败:{} -> {}\n rc={} {}".format(
|
||||
link, target, r.returncode, detail))
|
||||
|
||||
|
||||
def remove_junction(link: Path) -> bool:
|
||||
"""只摘掉联接本身,绝不触碰目标内容(os.rmdir = RemoveDirectoryW)"""
|
||||
if not is_reparse(link):
|
||||
return False
|
||||
try:
|
||||
os.rmdir(str(link))
|
||||
return True
|
||||
except OSError as e:
|
||||
print("[warn] 摘除联接失败 {}:{}".format(link, e))
|
||||
return False
|
||||
|
||||
|
||||
def bootstrap(wt: Path):
|
||||
"""建立共享联接。单个联接失败只告警不中止——避免留下半成品任务空间"""
|
||||
made, failed = [], []
|
||||
for rel, desc in JUNCTION_SPECS:
|
||||
target = REPO_ROOT / rel
|
||||
if not target.exists():
|
||||
print("[skip] {}({})不存在,跳过联接".format(rel, desc))
|
||||
continue
|
||||
try:
|
||||
make_junction(wt / rel, target)
|
||||
except (Fail, EnvError) as e:
|
||||
failed.append(rel)
|
||||
print("[WARN] {} 联接失败:{}".format(rel, e))
|
||||
continue
|
||||
made.append(rel)
|
||||
print("[ok] 联接 {} -> {}".format(rel, target))
|
||||
if failed:
|
||||
print()
|
||||
print("!! {} 个共享联接未建成;该任务里跑 `.venv\\Scripts\\python.exe` 会找不到解释器。".format(len(failed)))
|
||||
for rel in failed:
|
||||
print(" 手工补:cmd /c mklink /J \"{}\\{}\" \"{}\\{}\"".format(wt, rel, REPO_ROOT, rel))
|
||||
return made
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- 子进程脚本
|
||||
|
||||
|
||||
def regen(cwd: Path) -> None:
|
||||
run_script("scripts/gen_task_table.py", ["--write"], cwd, "重生成 FreeRTOS 任务表")
|
||||
if list((Path(cwd) / "MDK-ARM").glob("**/*.map")):
|
||||
run_script("scripts/check_mem_numbers.py", ["--write"], cwd, "同步内存/Flash 快照")
|
||||
else:
|
||||
print("[skip] 未找到 .map,跳过内存快照同步(需先 @build)")
|
||||
|
||||
|
||||
def run_gates(cwd: Path, skip_build: bool) -> bool:
|
||||
ok = run_script("scripts/ci_local.py", [], cwd, "L0 + L0.5 本地等价 CI")
|
||||
if skip_build:
|
||||
print("[skip] 按 --skip-build 跳过 @build")
|
||||
return ok
|
||||
got, info = lock_acquire("build", "__done__", wait=900)
|
||||
if not got:
|
||||
print("!! build 锁被 {} 占用(UV4 不能并发构建),跳过 @build".format(info.get("task")))
|
||||
return False
|
||||
try:
|
||||
if not run_script(".dsh/skills/keil-build/scripts/keil_build.py", [], cwd,
|
||||
"@build(0 错 0 警)"):
|
||||
ok = False
|
||||
finally:
|
||||
lock_release("build", "__done__", force=True)
|
||||
return ok
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- 合并辅助
|
||||
|
||||
|
||||
def write_merge_message(st: dict, base: str, cwd: Path) -> Path:
|
||||
raw = git_out("log", "--format=%s", "{}..{}".format(base, st["branch"]), cwd=cwd)
|
||||
subjects = [s.strip() for s in raw.splitlines() if s.strip()]
|
||||
title = "merge: {} 合入 {}".format(st["branch"], base)
|
||||
if len(title) > 100:
|
||||
title = title[:97] + "..."
|
||||
lines = [title, "", "任务 {}({} 个提交)经 task-worktree 回收:".format(st["id"], len(subjects))]
|
||||
for s in subjects:
|
||||
lines.append("- " + (s[:73] + "..." if len(s) > 76 else s))
|
||||
if st.get("note"):
|
||||
note = st["note"]
|
||||
lines.append("")
|
||||
lines.append("备注:" + (note[:73] + "..." if len(note) > 76 else note))
|
||||
fd, path = tempfile.mkstemp(prefix="taskwt-merge-", suffix=".txt")
|
||||
os.close(fd)
|
||||
Path(path).write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n")
|
||||
return Path(path)
|
||||
|
||||
|
||||
def audit_residue(base_before: str, branch: str, cwd: Path) -> None:
|
||||
"""并集合并残留审计:双方都改过的文件会被 git 自动并成并集"""
|
||||
mb = git_out("merge-base", base_before, branch, cwd=cwd)
|
||||
task_files = set(filter(None, git_out("diff", "--name-only", mb, branch, cwd=cwd).splitlines()))
|
||||
base_files = set(filter(None, git_out("diff", "--name-only", mb, base_before, cwd=cwd).splitlines()))
|
||||
both = sorted(task_files & base_files)
|
||||
if not both:
|
||||
print("[ok] 没有「双方都改过」的文件,无并集残留风险")
|
||||
return
|
||||
print("以下 {} 个文件双方都改过,git 会自动并成并集,需人工核对:".format(len(both)))
|
||||
for f in both[:30]:
|
||||
print(" - " + f)
|
||||
if len(both) > 30:
|
||||
print(" …(其余 {} 个略)".format(len(both) - 30))
|
||||
hits = []
|
||||
for f in both:
|
||||
p = cwd / f
|
||||
if not p.is_file():
|
||||
continue
|
||||
try:
|
||||
lines = p.read_text(encoding="utf-8", errors="replace").splitlines()
|
||||
except OSError:
|
||||
continue
|
||||
dups = [i + 1 for i in range(1, len(lines))
|
||||
if lines[i] == lines[i - 1] and lines[i].strip()]
|
||||
if dups:
|
||||
hits.append((f, dups))
|
||||
if hits:
|
||||
print()
|
||||
print("!! 连续重复行(并集合并残留嫌疑):")
|
||||
for f, dups in hits:
|
||||
print(" - {} 第 {} 行".format(f, ", ".join(str(d) for d in dups[:8])))
|
||||
else:
|
||||
print("[ok] 未检测到连续重复行")
|
||||
print("提示:权威判据是 base / 两个 tip 三方对比(git show <ref>:<file>),别只看结果文件。")
|
||||
|
||||
|
||||
def harvest_memory(st: dict) -> None:
|
||||
"""任务工作树会被删除,先把它自己的记忆归并回主工作树,避免丢上下文"""
|
||||
src = Path(st["path"]) / ".workbuddy-ai" / "memory"
|
||||
if not src.is_dir():
|
||||
return
|
||||
dst_dir = REPO_ROOT / ".workbuddy-ai" / "memory"
|
||||
dst_dir.mkdir(parents=True, exist_ok=True)
|
||||
for f in sorted(src.glob("*.md")):
|
||||
text = f.read_text(encoding="utf-8", errors="replace").strip()
|
||||
if not text:
|
||||
continue
|
||||
dst = dst_dir / f.name
|
||||
marker = "## [task-worktree 归并自任务 {} / {}]".format(st["id"], st["slug"])
|
||||
with dst.open("a", encoding="utf-8", newline="\n") as fh:
|
||||
fh.write("\n" + marker + "\n\n" + text + "\n")
|
||||
print("[ok] 记忆归并 {} -> {}".format(f.name, dst))
|
||||
|
||||
|
||||
def teardown_worktree(st: dict, force: bool = False, base: str = None) -> None:
|
||||
wt = Path(st["path"])
|
||||
base = base or st.get("base")
|
||||
# 1) 先摘联接——绝不能让 git 递归进联接去删目标内容(.venv / SDL2 会被一起删掉)
|
||||
for rel in st.get("junctions", []):
|
||||
if remove_junction(wt / rel):
|
||||
print("[ok] 已摘除联接 {}".format(rel))
|
||||
# 2) 归并任务侧记忆
|
||||
if wt.is_dir():
|
||||
harvest_memory(st)
|
||||
# 3) 移除工作树
|
||||
r = git("worktree", "remove", *(["--force"] if force else []), str(wt), check=False)
|
||||
if r.returncode != 0:
|
||||
raise Fail("移除工作树失败:{}\n(提示:先处理未跟踪文件,或加 --force)".format(
|
||||
(r.stdout + r.stderr).strip()))
|
||||
# 4) 删分支。⚠ 不用 `git branch -d`——它按**环境 HEAD** 判断是否已合并,
|
||||
# 集成点被 DTU_INTEGRATION_WORKTREE 改走时会误判。改为显式核对 base。
|
||||
if force or git("merge-base", "--is-ancestor", st["branch"], base,
|
||||
check=False).returncode == 0:
|
||||
r = git("branch", "-D", st["branch"], check=False)
|
||||
if r.returncode != 0:
|
||||
print("[warn] 删除分支 {} 失败:{}".format(
|
||||
st["branch"], (r.stdout + r.stderr).strip()))
|
||||
else:
|
||||
print("[ok] 已删除分支 {}".format(st["branch"]))
|
||||
else:
|
||||
print("[warn] 分支 {} 未并入 {},保留不删(确认可弃再 `git branch -D {}`)".format(
|
||||
st["branch"], base, st["branch"]))
|
||||
state_file(st["id"]).unlink(missing_ok=True)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- 子命令
|
||||
|
||||
|
||||
def cmd_new(a) -> int:
|
||||
if not ID_RE.match(a.task_id):
|
||||
raise EnvError("任务 ID 非法:{}(字母数字开头,可用 . _ -)".format(a.task_id))
|
||||
if not SLUG_RE.match(a.slug):
|
||||
raise EnvError("slug 非法:{}(小写字母数字与连字符)".format(a.slug))
|
||||
branch = "{}{}-{}".format(BRANCH_PREFIX, a.task_id, a.slug)
|
||||
wt = WT_ROOT / "{}-{}".format(a.task_id, a.slug)
|
||||
|
||||
if state_file(a.task_id).is_file():
|
||||
raise EnvError("任务 {} 已存在({})".format(a.task_id, state_file(a.task_id)))
|
||||
if os.path.lexists(str(wt)):
|
||||
raise EnvError("目录已存在:{}".format(wt))
|
||||
if git("rev-parse", "--verify", "--quiet", "refs/heads/" + branch, check=False).returncode == 0:
|
||||
raise EnvError("分支已存在:{}".format(branch))
|
||||
if git("rev-parse", "--verify", "--quiet", a.base, check=False).returncode != 0:
|
||||
raise EnvError("基线分支不存在:{}".format(a.base))
|
||||
|
||||
active = [s for s in all_states() if Path(s["path"]).is_dir()]
|
||||
if len(active) >= MAX_CONCURRENT and not a.force:
|
||||
raise EnvError("已有 {} 个活跃任务(上限 {});先 done/abort,或加 --force".format(
|
||||
len(active), MAX_CONCURRENT))
|
||||
|
||||
WT_ROOT.mkdir(parents=True, exist_ok=True)
|
||||
print("[1/3] 建工作树(分支 {},基线 {}{})…".format(
|
||||
branch, a.base, ",跳过 LFS smudge" if a.lean else ""))
|
||||
git("worktree", "add", "-b", branch, str(wt), a.base,
|
||||
env={"GIT_LFS_SKIP_SMUDGE": "1"} if a.lean else None)
|
||||
|
||||
print("[2/3] 建立共享联接…")
|
||||
junctions = [] if a.no_bootstrap else bootstrap(wt)
|
||||
|
||||
st = {
|
||||
"id": a.task_id, "slug": a.slug, "branch": branch, "path": str(wt),
|
||||
"base": a.base, "created_at": time.strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"junctions": junctions, "needs_bench": bool(a.needs_bench),
|
||||
"bench_pass": False, "note": a.note or "", "lean": bool(a.lean),
|
||||
}
|
||||
save_state(st)
|
||||
print("[3/3] 台账已写入 {}".format(state_file(a.task_id)))
|
||||
print()
|
||||
print("下一步:把新会话的工作目录切到")
|
||||
print(" " + str(wt))
|
||||
print("在该目录里干活、跑 @build 与 ci_local.py(并行安全);")
|
||||
print("要动台架先 `lock acquire bench --task {}`,完事 `lock release bench --task {} --bench-pass`。".format(
|
||||
a.task_id, a.task_id))
|
||||
print("回收用 `done {}`(**必须在主工作树执行**)。".format(a.task_id))
|
||||
return EXIT_OK
|
||||
|
||||
|
||||
def cmd_list(a) -> int:
|
||||
states = all_states()
|
||||
if not states:
|
||||
print("(没有任务空间。用 `new <ID> <slug>` 建一个)")
|
||||
return EXIT_OK
|
||||
print("{:<12} {:<32} {:>5} {:>5} {:>4} {}".format(
|
||||
"任务", "分支", "落后", "领先", "脏", "台架"))
|
||||
for st in states:
|
||||
wt = Path(st["path"])
|
||||
if not wt.is_dir():
|
||||
print("{:<12} {:<32} {:>5} {:>5} {:>4} {}".format(
|
||||
st["id"], st["branch"], "-", "-", "-", "[工作树缺失]"))
|
||||
continue
|
||||
ab = git_out("rev-list", "--left-right", "--count",
|
||||
"{}...{}".format(st["base"], st["branch"]), cwd=wt, check=False).split()
|
||||
behind, ahead = (ab[0], ab[1]) if len(ab) == 2 else ("?", "?")
|
||||
dirty = len(git_out("status", "--porcelain", cwd=wt, check=False).splitlines())
|
||||
bench = "PASS" if st.get("bench_pass") else ("需测" if st.get("needs_bench") else "-")
|
||||
print("{:<12} {:<32} {:>5} {:>5} {:>4} {}".format(
|
||||
st["id"], st["branch"], behind, ahead, dirty, bench))
|
||||
print()
|
||||
for n in LOCK_NAMES:
|
||||
info = lock_read(n)
|
||||
if not info:
|
||||
print("锁 {:<6} 空闲".format(n))
|
||||
else:
|
||||
print("锁 {:<6} {} 持有(已 {:.1f} 小时,于 {})".format(
|
||||
n, info.get("task"), lock_age_hours(info), info.get("at")))
|
||||
return EXIT_OK
|
||||
|
||||
|
||||
def cmd_status(a) -> int:
|
||||
st = load_state(a.task_id)
|
||||
wt = Path(st["path"])
|
||||
print("任务 {} ({})".format(st["id"], st["slug"]))
|
||||
print("分支 {} ← 基线 {}".format(st["branch"], st["base"]))
|
||||
print("工作树 {}{}".format(wt, "" if wt.is_dir() else " [缺失]"))
|
||||
print("建立于 {}".format(st.get("created_at")))
|
||||
if st.get("note"):
|
||||
print("备注 {}".format(st["note"]))
|
||||
if wt.is_dir():
|
||||
dirty = git_out("status", "--porcelain", cwd=wt, check=False).splitlines()
|
||||
print("未提交 {} 项".format(len(dirty)))
|
||||
for line in dirty[:10]:
|
||||
print(" " + line)
|
||||
ab = git_out("rev-list", "--left-right", "--count",
|
||||
"{}...{}".format(st["base"], st["branch"]), cwd=wt, check=False).split()
|
||||
if len(ab) == 2:
|
||||
print("相对基线 落后 {} / 领先 {} 个提交".format(ab[0], ab[1]))
|
||||
print("台架 {}{}".format(
|
||||
"需要," if st.get("needs_bench") else "不涉及,",
|
||||
"已记录 PASS" if st.get("bench_pass") else "尚无 PASS 记录"))
|
||||
if st.get("junctions"):
|
||||
print("联接 {}".format(", ".join(st["junctions"])))
|
||||
return EXIT_OK
|
||||
|
||||
|
||||
def cmd_sync(a) -> int:
|
||||
st = load_state(a.task_id)
|
||||
wt = Path(st["path"])
|
||||
base = a.base or st["base"]
|
||||
if not wt.is_dir():
|
||||
raise EnvError("工作树不存在:{}".format(wt))
|
||||
if git_out("status", "--porcelain", cwd=wt, check=False):
|
||||
raise Fail("任务工作树有未提交改动,先提交或 stash 再 sync")
|
||||
r = git("rebase", base, cwd=wt, check=False)
|
||||
if r.returncode != 0:
|
||||
print((r.stdout + r.stderr).strip())
|
||||
raise Fail("rebase 有冲突:在 {} 里解决后 `git rebase --continue`,放弃用 `git rebase --abort`".format(wt))
|
||||
print("[ok] 任务 {} 已 rebase 到 {}".format(st["id"], base))
|
||||
return EXIT_OK
|
||||
|
||||
|
||||
def cmd_lock(a) -> int:
|
||||
if a.action == "status":
|
||||
for n in LOCK_NAMES:
|
||||
info = lock_read(n)
|
||||
if not info:
|
||||
print("{:<6} 空闲".format(n))
|
||||
else:
|
||||
age = lock_age_hours(info)
|
||||
mark = " [陈旧,可被自动接管]" if age > LOCK_TTL_HOURS else ""
|
||||
print("{:<6} {} 持有(pid={},于 {},已 {:.1f} 小时,备注 {}){}".format(
|
||||
n, info.get("task"), info.get("pid"), info.get("at"), age,
|
||||
info.get("note") or "-", mark))
|
||||
return EXIT_OK
|
||||
|
||||
if a.name not in LOCK_NAMES:
|
||||
raise EnvError("未知资源 {}(可用:{})".format(a.name, " / ".join(LOCK_NAMES)))
|
||||
|
||||
if a.action == "acquire":
|
||||
if not a.task:
|
||||
raise EnvError("acquire 需要 --task <ID>")
|
||||
got, info = lock_acquire(a.name, a.task, wait=a.wait, force=a.force, note=a.note or "")
|
||||
if not got:
|
||||
print("获取失败:{} 被 {} 占用(pid={},于 {})".format(
|
||||
a.name, info.get("task"), info.get("pid"), info.get("at")))
|
||||
return EXIT_FAIL
|
||||
print("已获取 {} 锁(task={} pid={})".format(a.name, a.task, os.getpid()))
|
||||
return EXIT_OK
|
||||
|
||||
if a.bench_pass:
|
||||
if not a.task:
|
||||
raise EnvError("--bench-pass 需要 --task <ID>")
|
||||
st = load_state(a.task)
|
||||
st["bench_pass"] = True
|
||||
save_state(st)
|
||||
print("[ok] 已为任务 {} 记录台架 PASS".format(a.task))
|
||||
ok, msg = lock_release(a.name, a.task, force=a.force)
|
||||
print(msg)
|
||||
return EXIT_OK if ok else EXIT_FAIL
|
||||
|
||||
|
||||
def cmd_done(a) -> int:
|
||||
st = load_state(a.task_id)
|
||||
wt = Path(st["path"])
|
||||
branch = st["branch"]
|
||||
base = a.base or st["base"]
|
||||
main = main_worktree()
|
||||
|
||||
print("=== 前置检查 ===")
|
||||
if not wt.is_dir():
|
||||
raise EnvError("工作树不存在:{}".format(wt))
|
||||
if git_out("status", "--porcelain", cwd=wt, check=False):
|
||||
raise Fail("任务工作树有未提交改动,先提交")
|
||||
if git_out("status", "--porcelain", cwd=main, check=False):
|
||||
raise Fail("主工作树不干净({})——合并会把未提交改动卷进来".format(main))
|
||||
cur = git_out("rev-parse", "--abbrev-ref", "HEAD", cwd=main)
|
||||
if cur != base:
|
||||
raise Fail("主工作树当前在 {},不是集成分支 {};先切过去再 done".format(cur, base))
|
||||
for n in LOCK_NAMES:
|
||||
info = lock_read(n)
|
||||
if info:
|
||||
raise Fail("独占资源 {} 仍被 {} 占用(已 {:.1f} 小时),先 release".format(
|
||||
n, info.get("task"), lock_age_hours(info)))
|
||||
|
||||
ahead = int(git_out("rev-list", "--count", "{}..{}".format(base, branch), cwd=wt) or "0")
|
||||
already = git("merge-base", "--is-ancestor", branch, base, cwd=main, check=False).returncode == 0
|
||||
if ahead == 0 and not already:
|
||||
raise Fail("任务分支相对 {} 没有新提交,无需合并".format(base))
|
||||
if already and ahead == 0:
|
||||
print("[i] {} 已合入 {},跳过合并,直接清场".format(branch, base))
|
||||
print("=== 清场 ===")
|
||||
teardown_worktree(st, force=a.force, base=base)
|
||||
print("\n完成(分支此前已合入)。")
|
||||
return EXIT_OK
|
||||
|
||||
if st.get("needs_bench") and not st.get("bench_pass"):
|
||||
raise Fail("任务标记 --needs-bench 但无台架 PASS 记录;跑完 @test 后执行 "
|
||||
"`lock release bench --task {} --bench-pass`".format(st["id"]))
|
||||
print("[ok] 前置检查通过(领先 {} 共 {} 个提交)".format(base, ahead))
|
||||
|
||||
print("=== 合并预演(git merge-tree,不改动任何东西)===")
|
||||
r = git("merge-tree", "--write-tree", base, branch, cwd=main, check=False)
|
||||
if r.returncode != 0:
|
||||
print((r.stdout + r.stderr).strip())
|
||||
raise Fail("预演发现冲突:到任务工作树里 `git merge {}` 解决并提交后重跑 done".format(base))
|
||||
tree = (r.stdout.splitlines() or ["?"])[0].strip()
|
||||
print("[ok] 无冲突,预演结果树 {}".format(tree))
|
||||
if a.dry_run:
|
||||
print()
|
||||
print("--dry-run:到此为止,未做任何改动。")
|
||||
return EXIT_OK
|
||||
|
||||
print("=== 合入主干({},--no-ff)===".format(main))
|
||||
base_before = git_out("rev-parse", "HEAD", cwd=main)
|
||||
msgfile = write_merge_message(st, base, wt)
|
||||
r = git("merge", "--no-ff", "--no-edit", branch, "-F", str(msgfile), cwd=main, check=False)
|
||||
msgfile.unlink(missing_ok=True)
|
||||
if r.returncode != 0:
|
||||
raise Fail("合并失败:\n{}".format((r.stdout + r.stderr).strip()))
|
||||
print((r.stdout or "").strip())
|
||||
base_after = git_out("rev-parse", "HEAD", cwd=main)
|
||||
|
||||
print("=== 合并残留审计 ===")
|
||||
audit_residue(base_before, branch, cwd=main)
|
||||
|
||||
print("=== 重生成受管产物 ===")
|
||||
regen(main)
|
||||
|
||||
if a.skip_gates:
|
||||
print("=== 门禁(--skip-gates 已跳过)===")
|
||||
gates_ok = True
|
||||
else:
|
||||
print("=== 门禁 ===")
|
||||
gates_ok = run_gates(main, skip_build=a.skip_build)
|
||||
|
||||
if not gates_ok:
|
||||
print()
|
||||
print("!! 门禁未全绿:工作树与分支**保留**,未清场。")
|
||||
print(" 主干已含合并提交 {}".format(base_after[:12]))
|
||||
print(" 可选:a) 在主工作树修正后追加提交(推荐)")
|
||||
print(" b) 回退本次合并:git -C \"{}\" reset --hard {}".format(main, base_before[:12]))
|
||||
return EXIT_FAIL
|
||||
|
||||
print("=== 清场 ===")
|
||||
teardown_worktree(st, force=a.force, base=base)
|
||||
print()
|
||||
print("完成。主干 {} 现为 {}。".format(base, base_after[:12]))
|
||||
print("push 需人工许可,本次未推送。")
|
||||
return EXIT_OK
|
||||
|
||||
|
||||
def cmd_abort(a) -> int:
|
||||
st = load_state(a.task_id)
|
||||
wt = Path(st["path"])
|
||||
if wt.is_dir():
|
||||
dirty = git_out("status", "--porcelain", cwd=wt, check=False).splitlines()
|
||||
ahead = int(git_out("rev-list", "--count",
|
||||
"{}..{}".format(st["base"], st["branch"]), cwd=wt,
|
||||
check=False) or "0")
|
||||
if (dirty or ahead) and not a.force:
|
||||
print("任务 {} 有 {} 项未提交改动、{} 个未合并提交,abort 会**永久丢弃**:".format(
|
||||
st["id"], len(dirty), ahead))
|
||||
for line in dirty[:20]:
|
||||
print(" " + line)
|
||||
raise Fail("确认丢弃请加 --force")
|
||||
teardown_worktree(st, force=True)
|
||||
print("\n已放弃任务 {}(工作树与分支均已删除)".format(st["id"]))
|
||||
return EXIT_OK
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- 入口
|
||||
|
||||
|
||||
def build_parser():
|
||||
ap = argparse.ArgumentParser(
|
||||
prog="task_worktree.py",
|
||||
description="多任务并行开发的空间管理(git worktree 生命周期)")
|
||||
sub = ap.add_subparsers(dest="cmd")
|
||||
|
||||
p = sub.add_parser("new", help="新建任务空间")
|
||||
p.add_argument("task_id")
|
||||
p.add_argument("slug")
|
||||
p.add_argument("--base", default=BASE_BRANCH, help="基线分支(默认 %(default)s)")
|
||||
p.add_argument("--lean", action="store_true",
|
||||
help="跳过 Git LFS smudge(省约 38MB,参考图变成指针文件)")
|
||||
p.add_argument("--no-bootstrap", action="store_true", help="不建共享联接")
|
||||
p.add_argument("--needs-bench", action="store_true",
|
||||
help="需台架验证;无 PASS 记录则 done 拒绝合并")
|
||||
p.add_argument("--note", default="")
|
||||
p.add_argument("--force", action="store_true", help="忽略并发上限")
|
||||
p.set_defaults(func=cmd_new)
|
||||
|
||||
p = sub.add_parser("list", help="列出所有任务空间")
|
||||
p.set_defaults(func=cmd_list)
|
||||
|
||||
p = sub.add_parser("status", help="单个任务详情")
|
||||
p.add_argument("task_id")
|
||||
p.set_defaults(func=cmd_status)
|
||||
|
||||
p = sub.add_parser("sync", help="rebase 追平主干")
|
||||
p.add_argument("task_id")
|
||||
p.add_argument("--base", default=None)
|
||||
p.set_defaults(func=cmd_sync)
|
||||
|
||||
p = sub.add_parser("lock", help="独占资源锁(build / bench)")
|
||||
p.add_argument("action", choices=["acquire", "release", "status"])
|
||||
p.add_argument("name", nargs="?", choices=list(LOCK_NAMES))
|
||||
p.add_argument("--task", default=None)
|
||||
p.add_argument("--wait", type=int, default=0, help="等待秒数(0=不等待)")
|
||||
p.add_argument("--force", action="store_true")
|
||||
p.add_argument("--note", default="")
|
||||
p.add_argument("--bench-pass", action="store_true",
|
||||
help="release bench 时顺带为该任务记录台架 PASS")
|
||||
p.set_defaults(func=cmd_lock)
|
||||
|
||||
p = sub.add_parser("done", help="回收:预演 → 合入主干 → 审计 → 重生成 → 门禁 → 清场")
|
||||
p.add_argument("task_id")
|
||||
p.add_argument("--base", default=None)
|
||||
p.add_argument("--dry-run", action="store_true", help="只预演,不做任何改动")
|
||||
p.add_argument("--skip-build", action="store_true", help="跳过 @build")
|
||||
p.add_argument("--skip-gates", action="store_true", help="跳过全部门禁")
|
||||
p.add_argument("--force", action="store_true", help="清场时强制移除工作树")
|
||||
p.set_defaults(func=cmd_done)
|
||||
|
||||
p = sub.add_parser("abort", help="放弃任务(不合并)")
|
||||
p.add_argument("task_id")
|
||||
p.add_argument("--force", action="store_true")
|
||||
p.set_defaults(func=cmd_abort)
|
||||
return ap
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
ap = build_parser()
|
||||
a = ap.parse_args(argv)
|
||||
if not getattr(a, "func", None):
|
||||
ap.print_help()
|
||||
return EXIT_USAGE
|
||||
if not (REPO_ROOT / ".git").exists():
|
||||
print("!! 无法定位仓库根(推导为 {})".format(REPO_ROOT), file=sys.stderr)
|
||||
return EXIT_ENV
|
||||
try:
|
||||
return a.func(a)
|
||||
except EnvError as e:
|
||||
print("前置/环境错误:{}".format(e), file=sys.stderr)
|
||||
return EXIT_ENV
|
||||
except Fail as e:
|
||||
print("失败:{}".format(e), file=sys.stderr)
|
||||
return EXIT_FAIL
|
||||
except KeyboardInterrupt:
|
||||
print("已中断", file=sys.stderr)
|
||||
return EXIT_FAIL
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -1,13 +1,18 @@
|
||||
---
|
||||
name: ui-verify
|
||||
description: UI 改动后的回归验证:5 步门禁(模拟器构建、菜单像素回归、内容页黄金参考、Keil 构建、代码规范)。
|
||||
whenToUse: Drivers/BSP/UI、字体 ui_font_cn_12.c / ui_cn_chars.txt、菜单/内容页被修改之后,或用户要求"验证 UI / 跑 UI 回归 / UI 改动检查"时。
|
||||
whenToUse: Drivers/BSP/UI、字体 ui_font_cn_12.c / ui_cn_chars.txt、菜单/内容页被修改的前后(写码前先读「两条实证坑」节),或用户要求"验证 UI / 跑 UI 回归 / UI 改动检查"时。
|
||||
---
|
||||
|
||||
# UI 回归验证
|
||||
|
||||
UI 相关改动完成后必须跑完 5 个门禁再提交。完整说明见 `docs/UI设计/UI_自动化测试流程.md`。
|
||||
|
||||
## 两条实证坑(写码前先读,模拟器照不出来)
|
||||
|
||||
- **I1 绘制缓冲含 8B palette 头**:布局 `[palette 8B][pixel data]`,但 LVGL 容量校验只看 `stride×h` → 只给 `stride×h` 会把末 8 字节写到缓冲外。`ui_port.c` 按 `stride×h + palette` 供给并在 flush 时 `+palette` → `docs/UI设计/UI_设计架构.md` §1.4 第 8 条
|
||||
- **MCU 端必须显式应用 mono 主题**:`lv_theme_mono_init(disp, true, ...)` + `lv_display_set_theme()` + 对活动屏幕再 `lv_theme_apply()`,否则活动屏仍是默认浅灰而内容恒用白色 → **白底白字整屏纯白**(远端"纯白屏"根因)→ `docs/UI设计/UI_设计架构.md` §1.4 第 9 条
|
||||
|
||||
## 步骤(仓库根目录执行,python 一律用 .venv)
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -133,6 +133,12 @@ docs/images/*.png
|
||||
# Serial assistant runtime state (daemon log/meta, local runtime artifacts)
|
||||
.dsh/skills/serial-assistant/scripts/state/
|
||||
|
||||
# 记忆工具(Mnemon)本地状态:项目文档索引 documents/ + 运行时记忆
|
||||
# runtime/MEMORY.md、USER.md、memories.json。**只留本机、不进版本库**
|
||||
# (2026-10-11 定):记忆是按机器/按人累积的私有状态,入库会让每次记忆
|
||||
# 写入都变成提交抖动,还可能带出本机绝对路径与个人偏好。
|
||||
.mnemon/
|
||||
|
||||
# CI 运行时产物(.gitea/workflows/ci.yml 的 S3 步骤在 runner 工作区落盘的日志;
|
||||
# 零依赖存档方案——落在工作区目录而非上传 artifact,故必须忽略以免误提交)
|
||||
ci-artifacts/
|
||||
|
||||
+195
-4
@@ -2,11 +2,18 @@
|
||||
# 详见 docs/代码规范/pre-commit使用说明.md
|
||||
default_install_hook_types: [pre-commit, commit-msg]
|
||||
|
||||
# 顶层 exclude 只挡**第三方与生成物**。
|
||||
# ⚠ 2026-10-11 修正:原先在这里整目录挡 `simulator/` 是错的——`simulator/` 下 21 个
|
||||
# tracked 文件**全是本项目自研代码**(模拟器入口 main.c、平台后端 sim/*.c、回归脚本
|
||||
# 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 索引 → 无需在此重复豁免,故直接删掉该条。
|
||||
exclude: |
|
||||
(?x)^(
|
||||
\.opencode/node_modules/|
|
||||
Lib/lvgl/|
|
||||
simulator/|
|
||||
MDK-ARM/RTE/
|
||||
)
|
||||
|
||||
@@ -147,17 +154,18 @@ repos:
|
||||
|
||||
# 5) 文档一致性门禁(D-7)
|
||||
# 存在性:md 互链 / 反引号路径是否指向真实文件(防迁移/删除后残留死链)
|
||||
# 章节:`§N.M` 必须能在目标文档的编号标题里找到(--sections-strict:自引用失效也拦)
|
||||
- id: doc-links
|
||||
name: 文档链接存在性 (D-3/D-7)
|
||||
entry: python scripts/check_doc_links.py
|
||||
entry: python scripts/check_doc_links.py --sections-strict
|
||||
language: system
|
||||
files: \.md$
|
||||
# 外部资料(芯片手册转录)链接形态不受控;第三方目录整体排除
|
||||
# (`simulator/` 已不再排除:其下 3 个 README 是本项目文档,2026-10-11)
|
||||
exclude: |
|
||||
(?x)^(
|
||||
\.opencode/node_modules/|
|
||||
Lib/lvgl/|
|
||||
simulator/|
|
||||
MDK-ARM/RTE/|
|
||||
docs/芯片手册/
|
||||
)
|
||||
@@ -177,6 +185,27 @@ repos:
|
||||
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 落地)
|
||||
# 背景:`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
|
||||
# 默认值隐式生效 → 固件白链 **64,576 B 死代码**,且模拟器 golden 回归跑的是
|
||||
# 另一套特性子集(等于验证不到固件实际编译的配置)。
|
||||
# 口径:比对**生效值**(文件未定义则取 lv_conf_internal.h 的兜底默认),
|
||||
# 并抓"写了 LVGL 不认的宏名"(`LV_USE_THEME_BASIC` 同类,实测两端共 26 处)
|
||||
# 与同文件重复定义。规则正文见 docs/UI设计/UI_设计架构.md §1.4 第 10 条。
|
||||
# ⚠ 触发范围(2026-10-11 两次修正):本钩子由**两端任一配置**或脚本自身触发。
|
||||
# 原先顶层 exclude 整目录挡掉 `simulator/`,导致"只改 simulator/lv_conf.h"
|
||||
# 的提交拦不到;已删该条(见文件顶部 exclude 注释),故这里可直接匹配
|
||||
# `simulator/lv_conf.h`。(pre-commit 4.x 已移除 `always: true`,
|
||||
# 实测对 local 钩子只告警并跳过——不能靠它兜底。)
|
||||
- id: lv-conf-sync
|
||||
name: 两份 lv_conf.h 特性子集一致性
|
||||
entry: python scripts/check_lv_conf_sync.py
|
||||
language: system
|
||||
files: ^(Lib/lv_conf\.h|simulator/lv_conf\.h|scripts/check_lv_conf_sync\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 6) CubeMX 重生成存活审计(2026-09-29 加)
|
||||
# 背景:.ioc 的 KeepUserCode 只保护 USER CODE 标记区;标记之外的手工改动
|
||||
# 重生成即被覆盖,且多数不报编译错(崩溃采集被打回空 while(1)、加固宏被删)。
|
||||
@@ -232,16 +261,178 @@ repos:
|
||||
files: ^(App/cfg/cfg_file\.(c|h)|App/cfg/cfg_file_store\.h|App/log/sys_log\.h|test/host/cfg_file/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7d) 相量层(L2)主机侧单元测试(2026-10-10 加)
|
||||
# 背景:App/algo/ 为**纯算法**层(无寄存器、无 RTOS、无外设依赖),是本项目
|
||||
# 首个可完全离线验证的算法层。板端**无信号源**,无法注入已知幅值/相位/频率的
|
||||
# 标准波形,故"读数 = 设定值"这一核心判据在板上不可验;本钩子用 MSVC 离线编译
|
||||
# 真实源文件(#include 纳入,无需桩)+ 合成任意波形,把系数表、幅值/相位、
|
||||
# 直流与谐波抑制、频偏敏感度、滑动窗、双区交换一次性钉死。
|
||||
# files 含被测的两个源与其头,以及 harness 本身。
|
||||
- id: phasor-unit-test
|
||||
name: 相量层单测(主机侧)
|
||||
entry: python test/host/phasor/run_unit_test.py
|
||||
language: system
|
||||
files: ^(App/algo/(phasor|sample_ring)\.(c|h)|test/host/phasor/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7e) ADC 输入层(L1)主机侧单元测试(2026-10-10 加)
|
||||
# 背景:adc_input.c 的核心是"通道号 <-> Rank 顺序 <-> 语义"三者映射的正确性;
|
||||
# 板端无信号源,无法注入已知电压验证"读到的是哪只脚"(映射说明 §8)。本钩子用
|
||||
# MSVC 离线编译真实源文件(#include 纳入)+ HAL/TPAFE 可注入桩:给每个 Rank
|
||||
# 注入互不相同的码值以**逐通道钉死映射**,并一次钉死 HAL 启动失败 / 转换超时 /
|
||||
# 参数错等真机难复现的分支。
|
||||
# files 含被测源与其头、两个桩头所在目录,以及 harness 本身。
|
||||
- id: adc-input-unit-test
|
||||
name: ADC 输入层单测(主机侧)
|
||||
entry: python test/host/adc_input/run_unit_test.py
|
||||
language: system
|
||||
files: ^(App/algo/adc_input\.(c|h)|test/host/adc_input/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7f) 卷锁(vol_lock)主机侧单元测试(2026-10-10 加)
|
||||
# 背景:vol_lock 的**核心不变量是 fail-open**——互斥量创建失败时
|
||||
# `vol_lock_wait()` 必须**返回 1 且绝不阻塞**,因为它的调用者 defaultTask 是
|
||||
# 看门狗喂狗点,一旦在此阻塞即整机复位。板端该路径**只在内存耗尽时才可能走到**,
|
||||
# 无法人为复现;本钩子用 MSVC 离线编译真实源文件(#include 纳入)+ CMSIS 互斥桩,
|
||||
# 把"创建失败 -> 直接放行"、"创建成功 -> 正常加解锁"、"未持锁释放"、
|
||||
# 超时参数透传等分支一次性钉死。
|
||||
# files 含被测源与其头,以及 harness 本身。
|
||||
- id: vol-lock-unit-test
|
||||
name: 卷锁单测(主机侧)
|
||||
entry: python test/host/vol_lock/run_unit_test.py
|
||||
language: system
|
||||
files: ^(App/fs/vol_lock\.(c|h)|test/host/vol_lock/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7g) 时间同步(time_sync)主机侧单元测试(2026-10-10 加)
|
||||
# 背景:time_sync.c 是 TCP 时间同步协议状态机,也是 N-17 号看门狗风险的**全部
|
||||
# 逻辑贡献者**——"listen 重建 -> 800ms~1s 心跳间隔"由本模块的分片延时/退避/
|
||||
# 结算等待组成,板端只能看到"是否复位"这一结果,看不到各段延时是否如设计。本钩子
|
||||
# 用 MSVC 离线编译真实源文件(#include 纳入)+ 可脚本化的网络/事件桩(含 longjmp
|
||||
# 跳出 for(;;) 任务循环),把分片喂狗节奏、监听退避、报文重组(半包/粘连/EOF/真错/
|
||||
# 预算耗尽/tick 回绕)、重听门控逐条钉死。
|
||||
# files 含被测源与其头、被测的真实看门狗头,以及 harness 本身。
|
||||
- id: time-sync-unit-test
|
||||
name: 时间同步单测(主机侧)
|
||||
entry: python test/host/time_sync/run_unit_test.py
|
||||
language: system
|
||||
files: ^(App/sync/time_sync\.(c|h)|App/util/task_watchdog\.h|test/host/time_sync/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7h) ADC 采样有效性校验(L1-6)主机侧单元测试(2026-10-11 加)
|
||||
# 背景:adc_health.c 是"静默/丢帧/序号倒退/错误计数增量"的判定层——这些
|
||||
# 故障在健康台架上**不可复现**(复现 = 拔芯片/改驱动),板端闭环只能证明
|
||||
# "健康时不误报",证不了"故障时必报";100ms 门限、tick 回绕、事件边沿
|
||||
# 语义等边界只有合成计数序列能精确构造。本钩子用 MSVC 离线编译真实源文件
|
||||
# (#include 纳入,纯逻辑零桩)把 11 组判据一次性钉死。
|
||||
# files 含被测源与其头、adc_task.c(告警编排的消费方),以及 harness 本身。
|
||||
- id: adc-health-unit-test
|
||||
name: ADC 有效性校验单测(主机侧)
|
||||
entry: python test/host/adc_health/run_unit_test.py
|
||||
language: system
|
||||
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
|
||||
|
||||
# 7i) 保护定值模型(P1 / L5-1 载体)主机侧单元测试(2026-10-11 加)
|
||||
# 定值是保护动作的"输入宪法":范围校验、整表装载 all-or-nothing、
|
||||
# 校验和封存-自检-闭锁(参考 tagPRS dwTagSum/dwTagSumN 模式)必须在
|
||||
# 任何元件接线之前钉死——板端无法穷举越界定值注入,主机侧毫秒级全跑。
|
||||
- id: prot-set-unit-test
|
||||
name: 保护定值模型单测(主机侧)
|
||||
entry: python test/host/prot_set/run_unit_test.py
|
||||
language: system
|
||||
files: ^(App/algo/prot_set\.(c|h)|test/host/prot_set/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7j) 保护元件框架(L3 P1 子集)主机侧单元测试(2026-10-11 加)
|
||||
# 判据核(滞回/定时限/欠量方向/闭锁联动/时基回绕)是纯逻辑,
|
||||
# 板端复现需要标准源 + 故障注入,主机侧用合成测量序列即可穷举
|
||||
# 1.2×/0.9× 验收口径(规划 §8)。
|
||||
- id: prot-elm-unit-test
|
||||
name: 保护元件框架单测(主机侧)
|
||||
entry: python test/host/prot_elm/run_unit_test.py
|
||||
language: system
|
||||
files: ^(App/algo/prot_elm\.(c|h)|App/algo/prot_set\.(c|h)|App/algo/phasor\.(c|h)|test/host/prot_elm/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7k) SHA-256 主机侧单元测试(2026-10-11 加,N-19 FTP 鉴权)
|
||||
# sha256 是 FTP pwd_hash 校验的地基——哈希算错是"正确口令被拒 /
|
||||
# 错误口令放行"的静默语义错误,板端只见 530/230 看不出根因;
|
||||
# FIPS 180-4 标准向量(空串/abc/56B 补位跨界/1MB)+ hex_eq 非法输入
|
||||
# 在主机侧毫秒级钉死。零平台依赖,无桩。
|
||||
- id: sha256-unit-test
|
||||
name: SHA-256 单测(主机侧)
|
||||
entry: python test/host/sha256/run_unit_test.py
|
||||
language: system
|
||||
files: ^(App/util/sha256\.(c|h)|test/host/sha256/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 7l) FTP 鉴权状态机主机侧单元测试(2026-10-11 加,N-19)
|
||||
# 锁定 60s/解锁/成功清零/tick 32 位回绕是"防口令爆破"契约核心,
|
||||
# 板端每验一条要真等 60s、回绕要跑满 49.7 天;lftpd_auth 纯函数
|
||||
# 设计(tick 参数注入)就是为了让这些分支主机侧毫秒级穷举。
|
||||
- id: ftp-auth-unit-test
|
||||
name: FTP 鉴权状态机单测(主机侧)
|
||||
entry: python test/host/ftp_auth/run_unit_test.py
|
||||
language: system
|
||||
files: ^(Drivers/BSP/NET/lftpd/lftpd_auth\.(c|h)|App/util/sha256\.(c|h)|test/host/ftp_auth/.*|test/host/common/unit_test_harness\.py)$
|
||||
pass_filenames: false
|
||||
|
||||
# 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)\.(c|h)|test/host/iec104/.*|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 T-1
|
||||
- id: test-suite-switch
|
||||
name: 测试套件开关=产品构建
|
||||
entry: python scripts/check_test_suite.py
|
||||
language: system
|
||||
files: ^test/board/test_config\.h$
|
||||
pass_filenames: false
|
||||
|
||||
# 9) 双端注册反向校验(2026-10-12 加,登记簿 N-23 / 审查报告 N-06 残余)
|
||||
# 背景:新 .c 必须手动注册两处(`MDK-ARM/*.uvprojx` + `simulator/CMakeLists.txt`)
|
||||
# 才会被编译;历史上 `App/task/rs485_task.c`、`Drivers/BSP/NET/lftpd/lftpd_log.c`
|
||||
# **两处都没注册** = 写了却从不参与任何一端编译的**死代码**,误导读者以为功能已生效。
|
||||
# 本钩子反向核对两份注册表:①存在但未进 uvprojx ②存在但未进 CMakeLists
|
||||
# (MCU-only 豁免见脚本 SIM_EXEMPT,显式清单+每行理由,不整目录一刀切)
|
||||
# ③注册了但文件不存在(幽灵注册)。
|
||||
# 存量缺口(rs485_task/lftpd_log)已进脚本 KNOWN_GAPS 白名单 → WARN 不拦,
|
||||
# 只对新缺口 FAIL,故钩子可立即上线不拦现有提交(`--strict` 供审计关白名单)。
|
||||
# ⚠ pre-commit 4.x 已移除 `always: true` → files 必须覆盖所有触发面:
|
||||
# 任意 .c + uvprojx + CMakeLists.txt + 脚本自身(脚本自身改动也值得复跑)。
|
||||
- id: registration-check
|
||||
name: 双端注册反向校验 (uvprojx + CMakeLists)
|
||||
entry: python scripts/check_registration.py
|
||||
language: system
|
||||
files: (\.c$|\.uvprojx$|CMakeLists\.txt$|^scripts/check_registration\.py$)
|
||||
pass_filenames: false
|
||||
# 10) RAM 预算 92% 阈值门禁(2026-10-12 加,登记簿 N-27 前半 / 审查报告 §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 残余)。
|
||||
# ⚠ pre-commit 4.x 无 `always: true` → files 覆盖所有能改变 RAM 的触发面:
|
||||
# 全部自研代码目录 + lv_conf.h + 测试板配置 + 脚本/tsv/台账自身 + 本配置文件。
|
||||
- id: mem-budget
|
||||
name: RAM 预算 92% 阈值门禁 (N-27)
|
||||
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
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"fw_marker": "fw build: ",
|
||||
"readback_cmd": ".venv\\Scripts\\python.exe .dsh\\skills\\stlink-flash\\scripts\\stlink_flash.py cmp {hex}",
|
||||
|
||||
"required": "App init OK;Network Init;net init: OK",
|
||||
"required": "App init OK;Network Init;net init: OK;protect task start;i104: listening on 2404;[STACK] protect;[STACK] iec104;[STACK] heap",
|
||||
"forbidden": "FAIL;ERROR;HardFault;[ERR]",
|
||||
"boot_marker": "reset cause:",
|
||||
"max_boot_count": 1,
|
||||
|
||||
@@ -1,62 +1,53 @@
|
||||
# STM32F4-Base
|
||||
|
||||
STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v5 (ARMCC, C99) 工具链**;FreeRTOS V10.3.1 / CMSIS-RTOS2;192KB SRAM / 1MB Flash。
|
||||
**工业级嵌入式 DTU 平台固件**:STM32F407ZGTx @168MHz,集成以太网(CH395F)、SPI NAND 存储(GD5F)、电气量采集(TPAFE5160 + 片内 ADC)、RS-485、FTP 文件服务,自带远程显示(UI 推流)、墙钟对时、看门狗自恢复、运行/自检记录——既是可独立部署的现场设备,也是**可复用的平台底座**(保护测控等后续产品在此底座上补齐)。产品基线 → `docs/产品需求/产品需求设计文档.md` §2.1。
|
||||
|
||||
**仅 Keil MDK-ARM v5 (ARMCC, C99) 工具链**;FreeRTOS V10.3.1 / CMSIS-RTOS2;192KB SRAM / 1MB Flash。
|
||||
|
||||
**任何代码修改后必须先 `@build` 验证(0 错误 0 警告)再提交;硬件闭环用 `@test`。**
|
||||
|
||||
## 铁律:源码编辑与编码
|
||||
> 本文件 = agent 入口:宏观概况 + 每次都在场的铁律在这里,细节在 `docs/` 与 skills(文末索引)。同一事实只在一处详述(`docs/文档规范.md` 同此要求)。
|
||||
|
||||
- 改代码**只用 Edit 工具**(精确子串替换);批量替换用 Edit 的 replaceAll。**绝不用 `Get-Content`/`Set-Content`/`cmd` 重写含中文的文件**——整文件重写会按系统默认编码重解码 → `U+FFFD` 乱码且不可逆
|
||||
- **字符串字面量必须纯 ASCII**(2026-09-15 实测):本机 ARMCC 对**无 BOM** 源文件按 CP936 解读,字符串内含**奇数个**汉字时末尾高字节吞掉结束引号 → 编译错(偶数个汉字侥幸通过);中文只写注释(注释不参与解析)。带 BOM 的文件(`Drivers/BSP/UI/*`、`App/task/ui_task.c`、`App/cfg/cfg_file.c`、`Lib/lv_conf.h`)经 BOM 按 UTF-8 解码,不受此条限制
|
||||
- 确需脚本级重写:工程源码为 UTF-8(多数文件无 BOM,UI 系带 BOM),按原样同编码回写、保持 BOM 状态;拿不准就别碰
|
||||
- 发现乱码:先统计 `EF BF BD` 字节数确认真损坏(勿信终端/Read 渲染);**不要急著 `git checkout`**(会丢弃全部未提交工作)——先用 Edit 只修坏行
|
||||
- **Edit 工具会剥掉文件原有的 UTF-8 BOM**(2026-09-18 实测):带 BOM 文件(如 `App/cfg/cfg_file.c`)被 Edit 后 BOM 静默消失 → ARMCC 改按 CP936 解码 → 中文字符串字面量报 `#870-D: invalid multibyte character sequence`(**只有重编译该文件才暴露,历史"0 警告"不代表安全**)。改完带中文字面量的文件须核对 BOM 并按原样补回(只在文件头前缀 3 字节 `EF BB BF`,勿整文件重写)
|
||||
- **行尾策略已写死进 `.gitattributes`(2026-10-09)**:`* text=auto eol=lf`(仓库 blob 与工作区统一 LF)+ `*.bat`/`*.cmd` 强制 CRLF(cmd.exe 对 LF-only 的 goto/标签不可靠)+ PNG/TTF/DLL/LIB 等显式 `binary` + `*.jpg` 走 LFS 且 `-text`(该条必须排在 `*` 之后才能覆盖)。**不要再依赖 `core.autocrlf`,也不要把工作区文件"改回 CRLF"**——CubeMX 重生成写的就是 LF;此前每次重生成后 40+ 个第三方文件在 `git status` 里显示"已修改"(内容归一化后一致),根因就是行尾由每台机器各自的 autocrlf 决定。第三方/生成文件的"文件末尾空行"已一次性对齐工具输出(`53bd9af`),**勿再用改写型钩子去修剪**(已在 `trailing-whitespace`/`end-of-file-fixer` 豁免)
|
||||
## 项目概况
|
||||
|
||||
## 构建与验证
|
||||
**现状一句话**:平台能力(网络 / 存储 / 时间 / UI / 日志 / 看门狗 / FTP / 配置)**已实现并台架验收**;采集主线已闭环到业务消费者(L1→L2 相量→L3 保护判据 protectTask→104 遥信,B5);**RS-485 采集尚未接线**(B2 按用户决策冻结,见 `docs/任务规划/暂缓-RS485.md`)——当前最大功能缺口,接线节奏受 E-59 门禁约束(见「平台约束」)。
|
||||
|
||||
- `@build` = **`keil-build` skill**:`& '.venv\Scripts\python.exe' .dsh\skills\keil-build\scripts\keil_build.py`(仓库根目录执行;纯标准库,任意 python3 亦可)。退出码 0 = 0 错误 0 警告(PASS)、1 = 构建失败(打印报错行)、2 = UV4 未找到/无效、3 = 用法错误。UV4 解析链 `--uv4` > `.serialtest/config.json` 的 `uv4` > `$DSH_KEIL_UV4` > `$DTU_KEIL_UV4` > PATH(**显式设了但无效 = 立即报错、不静默回退**);日志 `MDK-ARM/build_log.txt`。⚠ **原 `MDK-ARM/build.bat` 已于 2026-10-10 删除**(判定口径同款,但无超时保护、UV4 只认 `DTU_KEIL_UV4`、失败时 dump 整份日志);`test/run_all_suites.py` 已改调本 skill。换机必设 `DTU_KEIL_UV4`,见 `docs/环境与部署/本地工具路径环境变量方案.md`
|
||||
- `@test` / `.dsh/skills/serial-boot-test/scripts/run_boot_test.py` 一键闭环(2026-09-20 起为纯 Python 标准库实现,替代 PowerShell 版;`& '.venv\Scripts\python.exe' .dsh/skills/serial-boot-test/scripts/run_boot_test.py`):开串口+清日志 → 烧录(**`stlink-flash` skill**:`stlink_flash.py flash` = `-P <hex> -Rst -Run`;`flash_cmd` 默认取 `.serialtest/config.json`,本仓库已指向本 skill)→ **固件身份戳**(从刚烧进去的 hex 里抠 `fw build: <DATE> <TIME>` 追加成 Required 标记)→ wait-idle(等 RX 静默 8s,超时 30s,首字节 12s 未到判 `no_data`)→ 提取日志 → **烧录回读校验**(`stlink_flash.py cmp` = `-CmpFile`;不一致 = 本次运行作废 exit 2)→ `.dsh/skills/serial-boot-test/scripts/verify_log.py` 标记校验(含**启动锚点计数**:`reset cause:` 出现 0 次或 >1 次 = FAIL,`--max-boot-count` 调;Forbidden 命中**逐行打印行号+内容**;boot_log 每行带 `[+Nms]` daemon 毫秒时基 → 可查标记顺序 `--require-order` 与开机耗时 `--done-marker`/`--max-boot-ms`,实测 `net init: OK` = **+537ms**);退出码 0=PASS / 1=FAIL / 2=采集或烧录错误(含回读不一致)/ 3=用法/预检错误
|
||||
- 默认校验标记:Required `App init OK` / `Network Init` / `net init: OK` + **自动追加的固件身份戳 `fw build: <__DATE__> <__TIME__>`**(`App/app/app_main.c` 开机打印,`run_boot_test.py` 从待烧 hex 里抠出该串当 Required——此前所有标记都是固定串,**旧镜像/错镜像照样全中**,"反复重下载、芯片一直是旧镜像"在自动闭环里不可见,2026-10-10 补;`--no-fw-check` 关闭);Forbidden `FAIL` / `ERROR` / `HardFault` / `[ERR]`(**四个 Fault 的崩溃日志都以 `[ERR]` 开头 → 一律判 FAIL**,这是刻意的:出现 Fault 即本次运行不通过);按模块定制标记(网络/存储/RTC/ADC/RS485/UI)见 `docs/测试说明/自动化测试流程.md`
|
||||
- ⚠️ 旧绝对路径 `D:\Code\DTU 程序\...` 出现即失效(仓库已搬迁),一律改用仓库相对路径
|
||||
- **下载/烧录铁律(2026-09-20 实测)**:走 **`stlink-flash` skill** 的 `& '.venv\Scripts\python.exe' .dsh\skills\stlink-flash\scripts\stlink_flash.py flash [文件]`(= `ST-LINK_CLI -c SWD -P <文件> -Rst -Run`),烧后 `-Rst -Run` **不可省略**——裸 `-P` 会报 `Programming Complete`,但目标核处于死循环/异常状态时写入并不落地(芯片内容不变;这就是 09/19~09/20 "反复重下载、芯片一直是旧镜像、二分法失效" 的根因);`.hex`/`.bin` 均可直烧(1MB ≈ 6.5s,快于 UV4 路径);只重启不烧录用 `stlink_flash.py reset`(= `-c SWD -Rst -Run`,本机 3 线接线唯一复位手段);`stlink_flash.py hard`(`RM=Hrst -HardRst PULSE=100`)在 v3.5.0 CLI 上被拒 → **自动降级**为系统复位并打印 `RESET_NOTE`。⚠ **原 `MDK-ARM/reset.bat` 已于 2026-10-10 删除**(子命令已全并入本 skill)
|
||||
- **烧录后必须回读校验**:`& '.venv\Scripts\python.exe' .dsh\skills\stlink-flash\scripts\stlink_flash.py cmp [文件]`(= `ST-LINK_CLI -c SWD -CmpFile <文件> 0x08000000`,全片逐字节比对,`No difference found` 才算真正写入);`run_boot_test.py` 已默认自动跑(`--no-verify-flash` 关闭,不一致 → exit 2 本次运行作废)。⚠ **`-CmpFile` 连接时会复位目标**(2026-10-10 实测:比对瞬间板子重打一遍开机日志)→ 在自动闭环里必须排在**日志采集之后**,否则开机日志被截断且启动锚点计数会误判成"中途复位";CLI 经 `DTU_STLINK_CLI` 环境变量解析(未设时回退 PATH;本机已入 PATH,未设也能工作)(v3.5.0.0,探针 SN `0B250D12A218303030303032`)→ 完整用法/复位语义/故障排查 `docs/测试说明/ST-LINK_CLI_复位使用说明.md`
|
||||
**数据主线**(能力视角;分层图与跨层依赖表 → `docs/架构设计/项目架构总览.md`):
|
||||
|
||||
## Python 环境
|
||||
- **采集**:TPAFE5160(计量级 8 路,并行总线)+ 片内 ADC1(9 路,DMA)→ TIM3 1 kHz 节拍 + 原子快照 → adcTask → L1 有效性校验 → L2 相量 → 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 源码**,远端可看图 + 注键
|
||||
|
||||
- 统一入口 `& '.venv\Scripts\python.exe'`(3.12,fontTools/Pillow 已装);PATH 默认 `python` 是 msys64 的,**不要用它**跑依赖第三方库的脚本;纯标准库脚本任意 python3 均可
|
||||
- 重建/换机:`& <本机任一 python ≥3.10> -m venv .venv` + `pip install -r requirements.txt`(含 `pre-commit`,重建后 L0 门禁直接可跑;`DTU_PRECOMMIT_PYTHON` 可指向装了 pre-commit 的其他解释器)(`.venv/` 已 gitignore)
|
||||
- `requirements.txt` 必须**纯 ASCII**(Windows 下 pip 按 GBK 代码页解码,中文注释会 UnicodeDecodeError)
|
||||
**目录地图**(只列顶层——稳定边界;细结构自己 `ls`,别背路径):
|
||||
|
||||
## 提交门禁(pre-commit)
|
||||
| 目录 | 职责 | 可改性 |
|
||||
|---|---|---|
|
||||
| `Src/` `Inc/` | CubeMX 生成(外设初始化 / 中断向量 / `freertos.c` 任务表) | **只写 `USER CODE` 区** |
|
||||
| `App/` | 应用层:`app/` 启动与就绪事件 · `task/` 任务编排 · `algo/` 采集与算法 L1~L3 · `cfg/` 配置引擎 · `log/` 日志 · `fs/` 卷锁 · `sync/` 对时 · `util/` 工具 | 手写 |
|
||||
| `Drivers/BSP/` | 手写芯片驱动与服务:`TPAFE5160` `CH395F` `NET`(socket 栈+lftpd) `NETCFG` `GD5F2GQ5UE` `SD2506` `RS485` `UI`(LVGL 界面) | 手写 |
|
||||
| `Drivers/STM32F4xx_HAL_Driver/` `Drivers/CMSIS/` `Middlewares/` | ST HAL / CMSIS / FreeRTOS | 第三方,只读 |
|
||||
| `Lib/` | 第三方库:`lvgl` `FatFs` `dhara` + `lv_conf.h`(MCU 端) | 只读(本地补丁须有文档登记,如 LVGL I1 补丁) |
|
||||
| `MDK-ARM/` | Keil 工程(`.uvprojx`、scatter、下载脚本) | 工程配置手改;UV4 抖动不入库 |
|
||||
| `simulator/` | LVGL PC 模拟器(CMake + SDL2),与固件同标准受检 | 手写 → **`lvgl-simulator` skill** |
|
||||
| `test/` | `board/` 板级套件 · `host/` 主机侧单测(提交门禁实跑) | 手写 |
|
||||
| `scripts/` `tools/` | 门禁与报告脚本(`check_*` / `gen_*` / `ci_local.py`)· 配图/字体/引用图生成器 | 手写 |
|
||||
| `docs/` | 全量设计 / 测试 / 规范文档(人类索引 `docs/README.md`) | 手写,`doc-links` 门禁 |
|
||||
| `.dsh/skills/` `.gitea/` `.serialtest/` | agent skills(子域知识入口)· CI 工作流 · 串口/烧录配置 | 手写 |
|
||||
|
||||
- 任一失败即拦截,共 **18 个钩子**(`.pre-commit-config.yaml`,2026-10-10 清点):通用 5 个(`trailing-whitespace` / `end-of-file-fixer` / `check-added-large-files` / `check-merge-conflict` / `check-yaml`)+ `gitlint`(提交信息)+ `cppcheck`(warning/performance/portability/style)+ `test-cppcheck`(`test/` 只开 warning)+ `no-mojibake`(U+FFFD)+ `coding-standard`(规范 V1.0)+ 文档一致性三件套 `doc-links` / `task-table` / `mem-numbers` + `cubemx-survival`(重生成存活审计)+ 主机侧单测 `watchdog-unit-test` / `sys-clock-unit-test` / `cfg-file-unit-test` + `test-suite-switch`(测试开关=产品构建)。范围仅项目代码(第三方 `Lib/`/`Middlewares/`/CMSIS/HAL/`lftpd`/`dhara`/`MDK-ARM` 排除;`cppcheck`/`coding-standard` 另排除 `test/`,规范检查再排除 CubeMX 生成的 `Src/`/`Inc/`)
|
||||
- **改写型钩子(`trailing-whitespace` + `end-of-file-fixer`)豁免 CubeMX/Keil 生成文件**(2026-10-09 定;配置里 YAML 锚点 `&cubemx-generated` 两处共用):`Src/*.{c,h}`、`Inc/*.{c,h}`、`Drivers/CMSIS/`、`Drivers/STM32F4xx_HAL_Driver/`、`Middlewares/`、`MDK-ARM/*.{uvprojx,uvoptx,sct,map}`、`.mxproject`、`*.ioc`——这类文件每次重生成按工具自己的风格写回(实测每次 10+ 个文件各被补 1 行末尾空行),改写只会制造提交抖动。⚠ **只读检查不受影响**:`cppcheck` / 乱码 / `cubemx-survival` / `task-table` / `mem-numbers` 仍覆盖 `Src/`、`Inc/`;`App/`、`Drivers/BSP/` 自研代码仍受改写型钩子检查
|
||||
- 提交信息:**单行中文标题 ≤100 字符**(gitlint `title-max-length=100`)+ **正文每行 ≤80 字符**(gitlint 默认 `body-max-line-length`,本项目未关)→ 长说明一律用**短标题 + `git commit -F -` 多行正文**(中文长标题极易超限);UTF-8 无 BOM + `git commit -F <文件>`(防终端转码破坏)
|
||||
- 代码规范 V1.0:小写下划线命名(`g_`/`s_`/`p_`/`a_` 前缀)、头文件保护宏 `XXX_H`、常量在右、文件头模块注释块 → `docs/代码规范/`
|
||||
- 全库扫描 `pre-commit run --all-files`;详见 `docs/代码规范/pre-commit使用说明.md`
|
||||
- **CI(Gitea Actions,2026-10-01 T-6)**:`.gitea/workflows/ci.yml` 在 push/PR 跑 **L0 静态门禁 + L0.5 主机侧单测**(Windows runner);**不含 L1/L2/L3**(需真机+ST-LINK)、**刻意不跑 `@build`**(需 Keil 授权)、无 `.map` 时自适应跳过 `mem-numbers`。本地等价自查:`& '.venv\Scripts\python.exe' scripts/ci_local.py`(**runner 未就绪时这是 L0 的唯一保障**,不能因"已配 CI"放宽本地门禁)。详见 `docs/测试说明/测试开发标准流程.md` §9
|
||||
**分层边界**:上层只依赖相邻下层(业务 → UI/基础服务 → 中间件 → BSP 驱动 → HAL/RTOS → 硬件,**单向**);新算法模块进 `App/algo/`,勿塞 `App/task/`(任务只做消费/编排);新 `.c` 必须双端注册(**`new-source-file` skill**)。
|
||||
|
||||
## 平台约束
|
||||
## 架构要点
|
||||
|
||||
- `Inc/`/`Src/` 中 CubeMX 生成文件带 `USER CODE BEGIN/END` 标记:自定义代码**只写标记区之间**;`Drivers/BSP/` 为纯手工代码
|
||||
- **CubeMX 重生成会覆盖标记区之外的一切**(`.ioc` 的 `KeepUserCode=true` **只**保护 `USER CODE` 区;2026-09-29 两次重生成各覆盖 4~5 处定制,其中 P1 两处**不报编译错**、只在真出故障时暴露)。防止定制丢失按三层优先级:**L1 写进 `.ioc`**(凡 CubeMX 界面能表达的配置,如任务栈/优先级/队列/事件组/堆大小/IWDG 参数——权限最高,重生成只会照写)→ **L2 搬进 `USER CODE` 区**(**只信 CubeMX 当前模板真实生成的区名**,优先文件头 `USER CODE BEGIN 0` 这类从未变过的标准位)→ **L3 审计兜底**。**每次重生成后立刻跑 `python scripts/check_cubemx_survival.py`**(已挂 pre-commit `cubemx-survival` 钩子;改动 `.ioc`/`Src/`/`Inc/`/`task_watchdog`/`.uvprojx` 触发)——清单 21 项 + 哈希基线漂移检测,直接列出"被覆盖了哪几处"。**2026-10-09 第三次重生成(启用 ADC1)补充**:`Inc/FreeRTOSConfig.h` 的自造 `FREE_RTOS_CONFIG_HARDENING` 区**第二次**被抹 → 已改走 **L1**:`configCHECK_FOR_STACK_OVERFLOW=2` / `configUSE_MALLOC_FAILED_HOOK=1` 在 GUI 的 **FreeRTOS → Config parameters → Hook function related definitions** 里设(写进 `.ioc` 由模板生成,永不再丢;教训:**别凭印象断言"界面里没有该项"**,先翻标签页或查 `db/mcu/IP/*_Modes.xml`)。⚠️ 启用后 CubeMX 会在 `Src/freertos.c` 生成 `USER CODE BEGIN 4 / 5` 区(默认内容是**空桩**)——**钩子实现必须整体写进这两个区**(原先在 `USER CODE Application` 区的实现要搬走,否则同名函数两处定义 = **编译错**;配套 helper `hook_ptr_ok()` 须在钩子之前 → 放 `USER CODE BEGIN PD` 区)。审计项 `stack-overflow-hook-impl` / `malloc-failed-hook-impl` / `stack-overflow-hook-helper` 分别校验 4 / 5 / PD 三个区。⚠️ **不要依赖"自造标记区"**:2026-09-29 实测 CubeMX **换了一代中断函数模板**(旧 `HardFault_Handler`/`HardFault_Report` 自造区 → 新函数体内 `HardFault_IRQn 0`/`W1_HardFault_IRQn 0`),旧区**连同内容一起消失**(看起来仍像"遵守了规则")。**生成区内无标记位可用的改动**分两种处理(2026-10-09 更新):① 要**去掉/替换生成区里的一句调用** → 用宏改名升级为 **L2**:例 `Src/iwdg.c` 的 `USER CODE BEGIN 0` 里放空实现 `iwdg_deferred_start()` + `#define HAL_IWDG_Init(a_h) iwdg_deferred_start(a_h)`,生成那句 `HAL_IWDG_Init(&hiwdg)` 自动变空操作(`.map` 证实 `Removing ... HAL_IWDG_Init, 94 bytes`),生成区保持原样、**重生成零 diff**,不必再手工删。⚠ **IWDG 启动后无法停止**(唯一手段是复位),所以不能"反初始化",只能在启动前拦截。② 要**替换生成实体的定义**(4 个 Fault Handler 本体——本代模板整函数重写)→ 同一招升级为 **L2**(2026-10-09 已做):我们的 `__asm void X_Handler` 定义放 `USER CODE BEGIN 0`,**必须写在 `#define X X_Gen` 之前**(写在之后会被自己的宏改名),生成区恢复 CubeMX 原样(`HardFault_IRQn 0` / `W1_HardFault_IRQn 0` 标记区照留),不再用锚点注释。`.map` 复核口径:`startup_*.o(RESET) refers to stm32f4xx_it.o(.emb_text) for HardFault_Handler` + 4 个 `*_Gen` 各 `Removing ... (2 bytes)`。**L3 清单已清空:重生成应当零 diff**;若仍有 diff,跑审计脚本定位(`protected` 项的断言只能用代码符号,注释会被剥掉)。方法论详见 `docs/子系统设计/看门狗设计说明.md` §10.1/§10.2
|
||||
- **4 个 Cortex-M4 Fault 全部采集现场**(2026-09-29):`HardFault` / `MemManage` / `BusFault` / `UsageFault` 均为手工 `__asm` Handler(取 MSP/PSP 帧指针)→ `B` 到 per-fault C 入口 `fault_entry_{hard,mem,bus,usage}`(各自带故障名)→ `hard_fault_report(tag, frame)` 打 `[ERR] <TAG> PC/LR/xPSR/R0-R3/CFSR/HFSR[/BFAR][/MMFAR]` 后 `NVIC_SystemReset()`。**后三者默认禁用**(复位后 `SHCSR` 只开 HardFault),不使能就会被**升级成 HardFault**、日志看不出根因 → `fault_handler_enable()`(`Src/stm32f4xx_it.c` 的 `USER CODE BEGIN 1` 区)在 `app_main_init()` **第一行**调用。**刻意不启用** `CCR.UNALIGN_TRP` / `CCR.DIV_0_TRP`(会把普通 bug 升级成死机)。⚠️ **ARMCC `__asm` 取不到 C 的数据符号**(`ADR r1, xxx_tag` 报 `A1516E: Bad symbol`,去 static 也无效)且无法传字符串参数 → 故障名必须由 C 侧 per-fault 入口带上,不要在 `__asm` 里取 C 变量
|
||||
- **`SHCSR` 的 Fault 使能位无法在 CubeMX 配置**(2026-10-01 查模板源码证实):CubeMX 对 System Exception **只生成优先级(且仅当优先级 ≠ 0),从不生成使能代码**——`tpl_main_c.ftl` 的 `MX_NVIC_Init()` 用 `[#if vector.systemHandler=="false"]` 挡住 `HAL_NVIC_EnableIRQ`,而 `NVIC-STM32F417_Modes.xml` 里 `MemoryManagement/BusFault/UsageFault_IRQn` 都标 `Y`(=systemHandler);**全部模板中 `SHCSR` 只出现 1 次**且是 MPU 清理的 `&= ~MEMFAULTENA`(禁用),无任何 `|=` 置位;MPU 路径也堵死(`.ioc` 无 MPU,且 `HAL_MPU_Enable` 只置 `MEMFAULTENA` 一位)。⚠️ **`.ioc` 的 `NVIC.MemoryManagement_IRQn=true` 不代表已使能**(第 1 段只是"向量在 NVIC 表里可见")——别误以为勾了 Enabled 就等于开了故障。结论:只能手工写(现放 `USER CODE BEGIN 1` 区,属 L2,重生成不丢)+ 审计项 `fault-handler-enable` 兜底
|
||||
- **`MDK-ARM/*.uvprojx` 每次 `@build` 后被 Keil UV4 自动改写,改动一律不入库**(2026-09-29 实测):UV4 构建时会把工程 XML 归一化成它本机风格的旧式写法(`<Tag />` → `<Tag></Tag>`、XML 声明加 `standalone="no"`),并把 `PackID` 降到**本机装的 DFP 版本**(本机实测 `3.1.1` → `3.0.0`,属**降级**)。这些差异是环境相关的,提交后会把别人的 Pack 版本也一起改掉。**判断方法**:`git diff MDK-ARM/STM32F407-Demo.uvprojx` 里若只见 XML 格式/`PackID` 差异而**无源文件挂载增删** → `git checkout -- MDK-ARM/STM32F407-Demo.uvprojx` 丢弃。只有真的新增/删除源文件(`grep 文件名` 计数变化)才提交
|
||||
- **ARMCC 内联汇编语法(2026-09-29 踩坑,共 3 坑)**:① `__asm volatile ("MRS %0, lr" : "=r" (x))` 是 **GCC 语法**,ARMCC 报 `#18: expected a ")"`;② `__asm { MRS x, lr }` 块内联会把寄存器名当 C 标识符报 `#20: identifier "lr" is undefined`;③ `__asm` 块里**取不到 C 的数据符号**,`ADR r1, xxx_tag` 报 `A1516E: Bad symbol 'xxx_tag'`(去掉 `static` 也无效——汇编器只认自己作用域内的符号)。**结论**:整个函数用 `__asm void f(void) { ... }`;要传"字符串/表格"这类 C 侧数据,就让 `__asm` 只取寄存器参数然后 `B` 到一个普通 C 函数,由 C 侧补上。若要覆盖 CubeMX 生成的同名函数,用 `#define f f_Gen`(在生成实体之前)把生成实体改名——链接器会把未被引用的 `f_Gen` 自动移除(`.map` 可见 `Removing ... (N bytes)`),**零代码开销且不删生成区任何一行**
|
||||
- FreeRTOS V10.3.1(CMSIS-RTOS V2);HAL 时基 = TIM7(非 SysTick),tick = 1ms;NVIC 4 位优先级分组,外设中断优先级 ≥ 5
|
||||
- **内核启动(`osKernelStart`)前禁止任何 FreeRTOS/CMSIS-RTOS API 调用**(2026-09-20 启动失败实证根因,修复 `82411cf`):`osMessageQueueNew`/`osMessageQueuePut`/`osMutexNew`/`osSemaphoreNew` 等在内核启动前调用会杀死启动(09/19~09/20 "每次必死" 的代码层根因 = `syslog_mcu_init` 在内核启动前调用 `osMessageQueueNew`)。内核启动前代码的标准模式:纯寄存器/HAL 轮询操作 + **无锁静态缓冲**(如 `s_preboot[8]` 暂存内核前日志)+ **内核启动后惰性创建真队列**(任务上下文首次使用时,见 `syslog_drain`);凡在内核启动前执行的初始化函数里不得创建任何 CMSIS 对象
|
||||
- CH395F SPI:每次事务须 `ch395f_spi_begin()/end()` 包裹;分频系数**必须为 8**(PCB 约束,勿"优化")
|
||||
- **就绪屏障位必须 `osFlagsNoClear`**(2026-09-21 实证):`osEventFlagsWait` **默认消费**所等位,而 `SYS_EVT_NET_READY`/`SYS_EVT_STORAGE_READY` 是**状态位**——被 netTask/uiTask/timeSync 先消费掉后,`ftpTask` 的 `WaitAll(两位)` 永不成立 → FTP 21 端口静默不监听(无日志、无报错,纯靠可观测性才揪出来)。就绪位一律 `osFlagsWaitAny|osFlagsNoClear` / `osFlagsWaitAll|osFlagsNoClear`;命令位(`SYS_EVT_CFG_SAVE`/`SYS_EVT_NET_RECONFIG`)保持消费型等待
|
||||
- **内存预算紧张**:新增任何任务/缓冲/栈前必须先核算 RAM 余量(`docs/内存管理/`、`待处理内容.md`);**栈数据来源**:`& '.venv\Scripts\python.exe' scripts/stack_report.py -s .dsh\skills\serial-assistant\scripts\state\serial.log`(Keil 静态调用图 → 每任务最坏链,+ 串口 `[STACK]` 运行时水位)与 `scripts/ram_report.py`(.map 静态占用);改栈或加大局部变量前后各跑一次
|
||||
- **自研代码禁用 C 库堆分配**(2026-09-18,P0-1):禁止 `malloc/calloc/realloc/strdup/free`——本机工具链 MicroLIB 的 C 堆由 `startup_stm32f407xx.s` 的 `Heap_Size EQU 0x200` 限定为 **512 B**,KB 级分配必然返回 NULL 且**模拟器(宿主 CRT 堆)完全正常、照不出来**。需要动态内存只用 `pvPortMalloc/vPortFree`(FreeRTOS 堆 `configTOTAL_HEAP_SIZE`),或像配置引擎那样把分配收进平台后端(`cfg_store_alloc/free/heap_free`:MCU=FreeRTOS 堆、模拟器=宿主堆,共享核心零分配符号);分配失败必须有 `FAIL` 日志 + 自检上报,暂态峰值要写编译期预算
|
||||
- **FatFs 打开模式陷阱(2026-09-20 台架实测,`Lib/FatFs` 移植版)**:`FA_OPEN_APPEND`(0x30) = `FA_OPEN_ALWAYS|FA_SEEKEND`,**不含 `FA_WRITE`(0x02)**——不带写位打开的文件 `f_write` 恒返回 `FR_DENIED`(7)(/log 写失败真根因);追加写必须用 `FA_WRITE|FA_OPEN_APPEND`。该移植的 `f_write` 簇分配失败时返回 `FR_OK`+短写(无错误码),判失败必须校验写回字节数。标志位为旧式编号(`FA_READ`=0x01 / `FA_WRITE`=0x02 / `FA_CREATE_NEW`=0x04 / `FA_CREATE_ALWAYS`=0x08 / `FA_OPEN_ALWAYS`=0x10 / `FA_OPEN_APPEND`=0x30 / `FA_SEEKEND`=0x20(ff.c 内部定义)),与上游 FatFs R0.14 不同,勿照抄上游代码示例
|
||||
- **FatFs 文件名约束(2026-09-20 台架实证,`.fschk` 无限重格式化事故根因)**:本移植 `Lib/FatFs/ffconf.h` 为 `FF_USE_LFN=0`,**只支持严格 8.3 短名**——主名≤8 字符、扩展≤3 字符、**不得以点开头**(`.fschk` 会被 `ff.c create_short` 把点后当扩展域解析超限 → `f_open` 恒返 `FR_INVALID_NAME`(6),纯 RAM 名字判定、不碰盘)。ff.c `create_short` 全部拒绝点已加 DEBUG 打印:`[ERR][FATFS] SFN reject (原因) seg='名字片段' bad_ch=坏字符`,非法名字调用看开机日志即可定位。自愈类逻辑**严禁**把名字/参数类错误码(`FR_INVALID_NAME` 等)归入卷损坏触发集(整卷重格式化不可逆)
|
||||
- **ADC 取数只用 `tpafe5160_snapshot()`(原子快照)**(2026-10-10 修 E-44/E-45/E-58):旧的 `get_buf()` / `data_ready()` / `clear_ready()` 已从驱动**删除**——`get_buf` 只给缓冲区指针、任务再逐通道拷,两步之间被 BUSY EXTI 抢占并跨过一帧周期就拷回**两帧混合**数据(8 通道失去同一时刻性,且无错误指示);`s_ready` 电平标志有三个写者,`start_conv_irq` 触发前清 0 = **重触发本身就会抹掉一帧未消费数据**。现口径:帧序号只增不清(`tpafe5160_frame_seq()`)、取数=临界区内拷贝(`tpafe5160_snapshot(dst, count)` 返回 1/0/-1,**仅任务上下文**)、FRSTDATA 全程无跳变(芯片不驱动并行总线)由 `tpafe5160_frstdata_err()` 计数且 `tpafe5160_init()` 首帧自检直接返回 `-3` + `[ERR]`。**消费者侧(L1 采集层)**:`App/algo/adc_input.c` 提供 `adc_input_poll()`(= TPAFE 快照 + STM32 ADC1 9 通道顺序扫描)与 `adc_input_raw()`(**按语义** `adc_semantic_t` 取数,不写死下标);映射表自检在 `adc_input_init()`(查每个 STM32 语义通道确实在扫描 rank 里,防"映射到从不扫描的通道 → 恒读陈旧值")
|
||||
- **接 ADC / RS-485 消费者前的强制门禁(E-59,2026-10-10 固化)**:`adc_task` / `rs485_task` 从空桩转为真实消费者**之前**必须确认——① 驱动契约已修:TPAFE **E-44 / E-45 / E-58 已修 ✅**、RS485 **E-36 / E-37 已修 ✅** 但 **E-50(单缓冲无同步 → 包撕裂)未修 ❌**、**E-49(TX 超时拉 DE)未修 ❌**;② 栈预算:建任务前后各跑一次 `scripts/stack_report.py`。**`adc_task` 已过闸(B1,2026-10-10)**:消费者本体 + `adcTask`(512 words / Normal,`USER CODE` 区手工创建,同 timeSyncTask)+ 栈预算(静态 **560 B** / 余 **72%**,2026-10-10 实测)+ IWDG 心跳(`TASK_WD_ADC`)均已落地;**`rs485_task` 仍被挡(E-50 / E-49 未修)**。另按 N-18:V0.1 上两片转换器之间**禁止**做相位/功率/功率因数类运算
|
||||
- **`DBG_*` 每条日志约 90 B Flash + 216 B 栈**(2026-10-02 S10 实测;2026-10-10 修正栈量):`dbg_log.h` 的 `DBG_INFO/ERROR/DEBUG` 在**每个调用点内联**行缓冲 `char __con_line[DBG_LINE_BUF_MAX]`(默认 = `console_log.h` 的 `DBG_CON_LINE_MAX` **192 B**)+ 时间戳缓冲 **24 B**(已并入同一数组,原为独立 32 B)+ 完整 `snprintf` 调用序列 → 单条约 **90 B Flash**(字符串常量另计)。给新模块接入日志时按"条数 × 90 B"估算 Flash 代价(实测 `sd2506_init` 加 6 条 = +576 B;`app_main` 加 3 条 = +180 B);**深调用链模块另需核算栈**——每条日志在栈上多占一份行缓冲、会进 `stack_report.py` 的最坏链:实测 defaultTask 那条链上 9 个帧各含一份 → **约 1.5 KB 纯粹是日志行缓冲**(2026-10-10 把该链 5312 → 2776 B 的主要杠杆之一)。故**日志不是免费的**——已覆盖的步骤不要再加"进入前/完成后"重复锚点,只在真正黑洞(原先静默返回)处补
|
||||
- **深调用链模块收紧日志行缓冲:`#define DBG_LINE_BUF_MAX 128U`,写在 `#include "dbg_log.h"` 之前**(2026-10-10 加)。已收紧:`nand_ftl.c`/`gd5f2gq5ue.c`/`cfg_file.c`/`net_cfg.c`/`net_socket.c`/`ch395f.c`/`time_sync.c`/`default_task.c`。⚠ **取值必须 ≥ 该模块最长日志行**(时间戳 20 + 标签 + 正文),否则该行被截断——各文件注释已写出实测行长。改前/改后各跑一次 `stack_report.py`
|
||||
`Src/main.c`(CubeMX 初始化)→ `app_main_init` → `osKernelStart` → 9 任务(`osKernelStart` 之前全是内核启动前代码:禁调任何内核 API,见「平台约束」)
|
||||
|
||||
- `app_main_init`(时间子系统优先,使后续日志带时间戳):`sd2506_init` → `sys_clock_init` → `rs485_init` → `gd5f2gq5ue_init` → `tpafe5160_init` → `adc_input_init`(L1 采集层)→ SD2506 诊断 → **启动 TIM3 1 kHz 采样节拍**
|
||||
- **就绪屏障**(`App/app/sys_events.h` Event Flags,取代魔法延时):`SYS_EVT_NET_READY` / `STORAGE_READY` / `FTP_READY` / `TIMESYNC_READY`;消费者用 `osEventFlagsWait` 阻塞等待(就绪位 `osFlagsNoClear`)
|
||||
- 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,`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`
|
||||
- 全景 → `docs/架构设计/项目架构总览.md`;启动时序与内核前模式 → `docs/架构设计/项目启动顺序管理.md`
|
||||
|
||||
<!-- TASK_TABLE:BEGIN generated by scripts/gen_task_table.py -->
|
||||
| Task | 栈 (words) | 优先级 |
|
||||
@@ -67,72 +58,126 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v
|
||||
| testTask | 2048 | BelowNormal |
|
||||
| uiTask | 4096 | BelowNormal |
|
||||
| timeSyncTask | 512 | Normal |
|
||||
| adcTask | 512 | Normal |
|
||||
| adcTask | 640 | AboveNormal |
|
||||
| iec104Task | 640 | Normal |
|
||||
| protectTask | 512 | Normal |
|
||||
<!-- TASK_TABLE:END -->
|
||||
|
||||
(实际值以 `Src/freertos.c` 为准;testTask 仅在启用测试套件时创建。改动栈/缓冲前先核对此表与实测。2026-09-21 上调:defaultTask 512→1024(扛 mount/mkfs/FAT探针/cfg/syslog 深层 FatFS I/O,旧值经 `configCHECK_FOR_STACK_OVERFLOW=2` 钩子实测溢出)、testTask 1024→2048(存储套件 deep call chain + 局部缓冲溢出)。FreeRTOS 堆已迁 CCM,任务栈合计已不占 SRAM;2026-09-21 可观测性落地后实测:ftpTask 峰值曾达 3480B/4096B(余量 15%,低于 1024B 底线)→ 已按 `docs/内存管理/RAM使用分析与优化方案.md` §1.4 把 lftpd 9 处大局部缓冲静态化(BSS +4.6KB),复测峰值 **1184B/4096B(余量 71%)**;2026-10-10 新增 adcTask 后 **7 任务全 PASS**(adcTask 静态 560 B / 余 72%))
|
||||
(实际值以 `Src/freertos.c` 为准,本表由 `scripts/gen_task_table.py` 生成、`task-table` 钩子校验;testTask 仅在启用测试套件时创建。FreeRTOS 堆已迁 CCM,任务栈不占 SRAM;**每次抬栈的实测最坏链与余量台账** → `docs/内存管理/RAM使用分析与优化方案.md` §1.4 与 `docs/内存管理/内存内容分配策略.md` §6)
|
||||
|
||||
## 架构要点
|
||||
## 铁律:源码编辑与编码
|
||||
|
||||
`Src/main.c`(CubeMX 初始化)→ `app_main_init` → `osKernelStart` → 7 任务(`osKernelStart` 之前全是内核启动前代码:禁调任何 FreeRTOS/CMSIS-RTOS API,见「平台约束」)
|
||||
- 改代码**只用 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`,勿整文件重写)
|
||||
- **字符串字面量必须纯 ASCII**:无 BOM 文件里奇数个汉字会吞掉结束引号(偶数侥幸通过)→ 中文只写注释
|
||||
- 怀疑乱码先统计 `EF BF BD` 字节数(勿信终端/Read 渲染);**不要急著 `git checkout`**(会丢弃全部未提交工作)——用 Edit 只修坏行
|
||||
- 行尾由 `.gitattributes` 写死(`* text=auto eol=lf`;`*.bat`/`*.cmd` 强制 CRLF):**不依赖 `core.autocrlf`、不要把工作区文件改回 CRLF** → 权威口径 `docs/文档规范.md` §2
|
||||
- 代码规范 V1.0:小写下划线命名(`g_`/`s_`/`p_`/`a_` 前缀)、头文件保护宏 `XXX_H`(禁 `__` 前缀)、常量在右、文件头模块注释块 → `docs/代码规范/`
|
||||
|
||||
- `app_main_init`(时间子系统优先,使后续日志带时间戳):`sd2506_init` → `sys_clock_init`(墙钟;RTC 故障时降级默认时间基准并返回 -1 触发"时钟芯片异常"自检;`sys_clock_get` 每 30 天惰性再基准化防 49.7 天 tick 回绕)→ `rs485_init` → `gd5f2gq5ue_init` → `tpafe5160_init` → **`adc_input_init`(L1 采集层)** → SD2506 诊断 → **启动 TIM6 1 kHz 采样节拍**(`HAL_TIM_Base_Start_IT(&htim6)`,2026-10-10;ISR 内触发 CONVST,见下)
|
||||
- **就绪屏障**(`App/app/sys_events.h` Event Flags,取代魔法延时):`SYS_EVT_NET_READY`(netTask 置位)/ `STORAGE_READY`(defaultTask:卷挂载 + 网络配置加载完成)/ `FTP_READY` / `TIMESYNC_READY`;消费者用 `osEventFlagsWait` 阻塞等待
|
||||
- netTask:`net_stack_bring_up`(配置已由 defaultTask 在屏障前加载,本任务只读 RAM 副本 → `net_init` + PHY 强制 100M 全双工 → 递增网络代次号)→ 置 NET_READY → 循环 `net_poll` + `net_process_messages`;消费 `SYS_EVT_NET_RECONFIG` → 立即重建网络(新 IP 免重启生效,各 TCP 服务据代次号重建监听)
|
||||
- defaultTask:`f_mount` forced(无卷自动 `f_mkfs` FAT32;挂载后卷健康探针:小写两击确认,FAT 区陈旧/擦除时自动重格式化自愈,`/config.ini` 回落工厂默认——2026-09-20 /log 写失败修复)→ 网络配置加载(`net_cfg_load`;旧档迁移已于 2026-10-10 删除,见「模块约定」网口配置条)→ STORAGE_READY → LED 闪烁;同时是 syslog(`/log/run.log`·`selftest.log`,定长 64B 记录)的**唯一写者**与**配置引擎属主**(`cfg_file` 静态态单任务收敛于此:UI 保存请求执行 `net_cfg_save_run` + 2s `cfg_file_poll`,经 `SYS_EVT_CFG_SAVE` 事件唤醒;netTask/uiTask 不得直接触碰配置引擎,见 P1-3)
|
||||
- adcTask(**B1,2026-10-10;L1 层 v2.3,2026-10-10**):**纯消费者**——每 1 ms 调 `adc_input_poll()`(`App/algo/adc_input.c`)= TPAFE `tpafe5160_snapshot()` 临界区原子快照(8 通道)+ **STM32 ADC1 9 通道软件触发顺序扫描**(L1-3)→ 帧统计 + 每 10 s 心跳日志;取数一律经 `adc_input_raw()` **按语义**(L1-4,不写死下标);只取数、不做保护运算(V0.1 仅 TPAFE 三路 `UA`/`UN`/`IA` 测量有效,见 N-18)。**采样节拍(1 kHz)由 TIM6 更新中断硬件触发**(`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 TIM6 分支内调 `tpafe5160_start_conv_irq()`,2026-10-10);本任务 1 ms 轮询仅作消费节奏。手写于 `Src/freertos.c` 的 `USER CODE` 区(同 timeSyncTask,512 words / Normal),纳入 IWDG `required_mask`(`TASK_WD_ADC`)。详见 `docs/子系统设计/ADC采样节拍设计说明.md`、`App/algo/README.md`
|
||||
- **采集与算法层(L1/L2)落在 `App/algo/`**(2026-10-10 建):现有 `adc_input.c/h` = L1(语义通道映射 + STM32 ADC1 扫描);L2 相量层待建。新算法模块一律进这里,勿塞进 `App/task/`(任务只做消费/编排);见 `App/algo/README.md`
|
||||
- 全景 `docs/架构设计/项目架构总览.md`(六层分层 / 启动时序 / 存储·网络·UI 栈 / 目录组织)
|
||||
## 构建与验证
|
||||
|
||||
- `@build` = `& '.venv\Scripts\python.exe' .dsh\skills\keil-build\scripts\keil_build.py`(仓库根执行)。退出码 **0=PASS / 1=构建失败 / 2=UV4 未找到 / 3=用法**;日志 `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 即本次不通过**(刻意)
|
||||
- **烧录必须 `-Rst -Run`**(`stlink-flash` skill 的 `flash`):裸 `-P` 会报 `Programming Complete`,但目标核处于死循环/异常时写入并不落地("反复重下载、芯片一直是旧镜像、二分法失效"的根因)。**烧后必须回读校验**(`stlink_flash.py cmp` = `-CmpFile`,`No difference found` 才算真正写入);⚠ `-CmpFile` 连接时会**复位目标** → 必须排在**采日志之后** → `docs/测试说明/ST-LINK_CLI_复位使用说明.md`
|
||||
- ⚠️ 旧绝对路径 `D:\Code\DTU 程序\...` 出现即失效(仓库已搬迁),一律改用仓库相对路径
|
||||
|
||||
## Python 环境
|
||||
|
||||
- 统一入口 `& '.venv\Scripts\python.exe'`(3.12,fontTools/Pillow 已装);PATH 上的 `python` 是 msys64 的,**不要用它**跑依赖第三方库的脚本;纯标准库脚本任意 python3 均可
|
||||
- 重建/换机:`<本机任一 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)
|
||||
|
||||
- 任一失败即拦截,共 **31 个钩子**(逐个详解 → `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` + 主机侧单测 13 个(`-unit-test` 后缀)+ `test-suite-switch` + `registration-check`(双端注册反向校验,N-23)+ `mem-budget`(RAM 92% 预算门禁,N-27)
|
||||
- **范围 = 项目代码**:第三方(`Lib/`、`Middlewares/`、CMSIS、HAL、`lftpd`、`dhara`、`MDK-ARM`)排除;`cppcheck`/`coding-standard` 另排除 `test/`,规范检查再排除 CubeMX 生成的 `Src/`/`Inc/`。**`simulator/` 已纳入全部钩子**(2026-10-11)——生成物只靠 `.gitignore` 挡,**不在 `exclude` 里写目录名**(教训见 `docs/代码规范/pre-commit使用说明.md` §5.5)
|
||||
- ⚠ **pre-commit 4.x 已移除 `always: true`**(实测对 `local` 钩子只告警 `Unexpected key(s)` 并跳过)→ 想"总是跑"的钩子**必须让 `files:` 覆盖到所有会触发它的文件**,没有兜底开关
|
||||
- **改写型钩子(`trailing-whitespace` + `end-of-file-fixer`)豁免 CubeMX/Keil 生成文件**(配置里 YAML 锚点 `&cubemx-generated`);⚠ **只读检查不受影响**:`cppcheck` / 乱码 / `cubemx-survival` / `task-table` / `mem-numbers` 仍覆盖 `Src/`、`Inc/`
|
||||
- 提交信息:标题 ≤100 字符、**正文每行 ≤80**(`.gitlint` + gitlint 默认)→ 长说明一律用**短标题 + `git commit -F <UTF-8 无 BOM 文件>` 多行正文**(中文长标题极易超限,且 `-m` 会被终端代码页转码破坏)
|
||||
- **CI**(`.gitea/workflows/ci.yml`)只跑 L0 静态门禁 + L0.5 主机侧单测,**刻意不跑 `@build`**(需 Keil 授权)、不含 L1/L2/L3(需真机);本地等价自查 `& '.venv\Scripts\python.exe' scripts/ci_local.py`——**runner 未就绪时这是 L0 的唯一保障**
|
||||
|
||||
## 平台约束
|
||||
|
||||
**CubeMX 与 Fault**
|
||||
|
||||
- `Inc/`/`Src/` 是 CubeMX 生成文件(带 `USER CODE BEGIN/END` 标记):自定义代码**只写标记区之间**;`Drivers/BSP/` 为纯手工代码
|
||||
- **重生成会覆盖标记区之外的一切**(`.ioc` 的 `KeepUserCode=true` **只**保护 USER CODE 区;实测两次重生成各覆盖 4~5 处定制,其中两处**不报编译错**、只在真出故障时暴露)。防丢失按三层优先级 **L1 写进 `.ioc` → L2 搬进 USER CODE 区 → L3 审计兜底**;**每次重生成后立刻跑 `python scripts/check_cubemx_survival.py`**(已挂 `cubemx-survival` 钩子)。方法论与全部实测坑(自造标记区会被模板换代抹掉、宏改名 `#define f f_Gen` 让位、FreeRTOS 钩子必须写进 `USER CODE BEGIN 4/5` 区…)→ `docs/子系统设计/看门狗设计说明.md` §10
|
||||
- **ARMCC `__asm` 只有一种合法形态**:整函数 `__asm void f(void) { ... }`;GCC 内联语法报 `#18`、块内联把寄存器名当 C 标识符报 `#20`、**取不到 C 的数据符号**(`A1516E: Bad symbol`)→ 三条坑与可用写法 `docs/子系统设计/看门狗设计说明.md` §10.4
|
||||
- **4 个 Cortex-M4 Fault 全部采集现场**(手工 `__asm` Handler 取 MSP/PSP → `B` 到 per-fault C 入口 `fault_entry_{hard,mem,bus,usage}` → `hard_fault_report` 打 `[ERR] <TAG> …` 后 `NVIC_SystemReset()`);**MemManage/BusFault/UsageFault 默认禁用**,不使能就被升级成 HardFault、日志看不出根因 → `fault_handler_enable()` 必须在 `app_main_init()` **第一行**;**`SHCSR` 的使能位无法在 CubeMX 配置**(模板只生成优先级)→ 看门狗设计说明 §10
|
||||
|
||||
**RTOS 与内存**
|
||||
|
||||
- **`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`
|
||||
- **内存预算紧张**:新增任何任务/缓冲/栈前先核算 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**
|
||||
|
||||
**采集链(ADC / RS-485)**
|
||||
|
||||
- **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`
|
||||
|
||||
**存储与网络外设**
|
||||
|
||||
- **FatFs(`Lib/FatFs` 移植版)两条台架实证陷阱**:① 追加写必须 `FA_WRITE|FA_OPEN_APPEND`——该移植 `FA_OPEN_APPEND` **不含 `FA_WRITE`**,不带写位打开的文件 `f_write` 恒返 `FR_DENIED`;且 `f_write` 簇分配失败时返回 `FR_OK`+短写(无错误码)→ **判失败必须校验写回字节数**。② `FF_USE_LFN=0` → **只支持严格 8.3 短名**(不得以点开头),`.fschk` 恒返 `FR_INVALID_NAME`;自愈类逻辑**严禁**把名字/参数类错误码归入卷损坏触发集 → 完整口径与标志位编号表 `docs/架构设计/STM32F4-Base存储架构说明.md` §10.1
|
||||
- **CH395F SPI:每次事务须 `ch395f_spi_begin()/end()` 包裹;分频系数必须为 8**(PCB 约束,勿"优化")
|
||||
|
||||
**日志与工具链**
|
||||
|
||||
- **`DBG_*` 每条日志约 90 B Flash + 216 B 栈**(每个调用点内联行缓冲 + 时间戳缓冲)→ 日志不是免费的,已覆盖的步骤别加重复锚点,只在原先静默返回的黑洞处补;**深调用链模块在 `#include "dbg_log.h"` 之前 `#define DBG_LINE_BUF_MAX 128U`**(取值必须 ≥ 该模块最长日志行,否则该行被截断)
|
||||
- **`MDK-ARM/*.uvprojx` 每次 `@build` 后被 UV4 自动改写,改动一律不入库**(XML 归一化 + `PackID` 降到本机 DFP 版本);判据与例外(真的增删源文件才提交)→ **`keil-build` skill**
|
||||
|
||||
## 硬件引脚
|
||||
|
||||
| 外设 | 接口 | 引脚 |
|
||||
|------|------|------|
|
||||
| CH395F 以太网 | SPI2 | PB12 CS, PB13 SCK, PB14 SDO, PB15 SDI |
|
||||
| GD5F2GQ5UE NAND | SPI1 | PE0 CS, PB3 SCK, PB4 MISO, PB5 MOSI, PB8 WP, PE1 HOLD |
|
||||
| TPAFE5160 ADC | 并行16位 | PG0-PG15 数据, PC8 RD, PC9 CONVST, PC7 BUSY(V0.1;FRSTDATA=PC6,OS0/OS1/OS2=PA12/PA11/PA8) |
|
||||
| ADC 通道语义(V0.1,17 路 = 8×TPAFE + 9×STM32) | 见映射表 | **网名 `AINn` ≠ 板级 `ADCn` ≠ 转换器通道号**(13/17 路置换);语义 UA/UB/UC/UN=`AIN1~4`、IA/IB/IC/IN=`AIN8~11`、V_ZL2=温度、其余 8 路备用——取数**必须查** `docs/硬件平台/ADC通道映射说明.md`(TPAFE `buf[2]=IA`/`buf[5]=UN`/`buf[6]=UA`;网名 ADC17=PC4 用 `ADC_CHANNEL_14`,**不是** `ADC_CHANNEL_12`) |
|
||||
| ADC 通道归属(V0.1 **硬件缺陷**) | 8 路电气量被分片 | **UA/UN/IA 走 TPAFE5160、UB/UC/IB/IC/IN 走 STM32 ADC 是缺陷,不是有意分配**(2026-10-10 更正;跨转换器无共同采样触发 → 无同一时刻性/相位不确定,16 位 ±5 V 双极性 vs 12 位 0~3.3 V 单极性)→ V0.1 上主板侧**禁止**跨转换器做相位/功率/功率因数/矢量运算;**下一版硬件(V0.2)须把 8 路电气量全部接 TPAFE**(登记簿 N-18,理由与改版方案见 `docs/硬件平台/ADC通道映射说明.md` §1) |
|
||||
| USART1 调试串口 | UART | PA9 TX, PA10 RX (115200) |
|
||||
| RS-485 | UART4 | PC10 TX, PC11 RX, PA15 DE(半双工) |
|
||||
| 6x LED | GPIO | PC5, PB0, PB1, PB2, PF11, PF12(低电平点亮) |
|
||||
- **完整引脚表(含 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)
|
||||
|
||||
## 模块约定
|
||||
|
||||
- **CH395F 单连接 socket**:监听与数据通道复用同一 Socket(CONNECT 后转数据通道,DISCONNECT/TIMEOUT 后重开 → LISTEN);所有 CH395F 中断在 `net_poll()` 内经 `GET_GLOB_INT_STATUS` 集中读取并清除,**不得**在 ISR 或其他地方单独处理
|
||||
- 所有 `net_*` socket 操作经 `netMsgQueue` 消息队列在 netTask 串行执行(线程安全);非 netTask 任务直接调 `net_*` API,勿直接碰 CH395F 驱动
|
||||
- **`net_*` 公开 API 的错误码经"任务通知值"回传**(2026-09-21 实证):`net_msg_t` 是**按值拷入队列**的,netTask 改的是自己那份副本,调用方永远看不到 → 错误码搭 `xTaskNotify()` 通知值回来(失败=负的具体错误码如 `NET_ERR_WOULDBLOCK`、成功=字节数/描述符),公开 API 恢复 errno 后对外统一返回 -1。**勿给 `net_msg_t` 加"回传用"字段**(调用方只会读到栈垃圾;曾据此"修"错误码,导致 `ui_net_sink` 每次空轮询都误判为真错误 → 连上即 `net_close`)
|
||||
- **单连接 socket 的断开语义**:监听 Socket 在 CONNECT 后自身即数据通道(`g_client == g_listen`),EOF 时 net 层已 `auto_relisten`;应用层**不得**在 EOF 时 `net_close` 该 fd(会连监听一起毁掉 → 端口"连接被拒"),仅当数据 fd ≠ 监听 fd(模拟器多连接后端)时才关
|
||||
- 网口配置:`/config.ini` [network] 段(工厂默认 192.168.1.100/24、gw .1;MAC 全零 = 芯片内置;UI 保存立即生效;**旧档 `/cfg/netcfg.bin` 迁移已于 2026-10-10 删除**——`net_cfg_legacy.c/h` 整体移除,旧档若存在则被忽略(不读/不删/不迁移)、配置回落工厂默认;删除理由 = 该一次性路径把整条配置写链挂进启动期调用图,defaultTask 静态最坏链 +600 B,且本仓库无携带旧档的设备)→ `docs/子系统设计/网络配置说明.md`
|
||||
- **新 .c 文件必须手动注册到两处**才能双端编译:`MDK-ARM/STM32F407-Demo.uvprojx` + `simulator/CMakeLists.txt`(用 `new-source-file` skill);⚠ **若新文件落在工程里还没有的新目录,还要把该目录加进 `uvprojx` 的 `<IncludePath>`**(2026-10-10 实测:只挂 `<File>` 时该 `.c` 自己能编过——它与自己的头同目录,但**其他 TU `#include` 它的头会报 `cannot open source input file`**,报错落在消费者文件上、极易误判)
|
||||
- **`MDK-ARM/*.uvprojx` 每次 `@build` 后被 Keil UV4 自动改写,改动一律不入库**(2026-09-29 实测):UV4 构建时会把工程 XML 归一化成它本机风格的旧式写法(`<Tag />` → `<Tag></Tag>`、XML 声明加 `standalone="no"`),并把 `PackID` 降到**本机装的 DFP 版本**(本机实测 `3.1.1` → `3.0.0`,属**降级**)。这些差异是环境相关的,提交后会把别人的 Pack 版本也一起改掉。**判断方法**:`git diff MDK-ARM/STM32F407-Demo.uvprojx` 里若只见 XML 格式/`PackID` 差异而**无源文件挂载增删** → `git checkout -- MDK-ARM/STM32F407-Demo.uvprojx` 丢弃。只有真的新增/删除源文件(`grep 文件名` 计数变化)才提交
|
||||
- 测试构建:`test/board/test_config.h` 的 `TEST_SUITE_CH395F/STORAGE/GD5F/RTC` **互斥**(一次只能启用一个,`#error`);全部注释 = 产品构建;**该开关受 pre-commit 钩子 `test-suite-switch` 门禁**(默认要求产品构建,跑完套件回归须改回;确需提交测试构建用 `ALLOW_TEST_BUILD=1` 显式放行——2026-10-01 实测已提交 HEAD 里 `TEST_SUITE_CH395F` 长期开启,纯靠人记得改,故补此门禁);**破坏性用例默认关闭**:同文件 `ENABLE_DESTRUCTIVE_TESTS`(默认注释)控制写/擦 NAND、格式化、坏块注入与 FatFS 写类用例——关闭时套件为**只读模式**(开机横幅 `mode: READ-ONLY`,可随时在在用设备上跑、不动卷内容与容量),只在**产品首次上电/产线首启**或明确需要时打开(`mode: DESTRUCTIVE`);2026-09-21 实测一轮破坏性 GD5F 运行即让 dhara 多记一个坏块(容量 95248 → 95200;该计数持久化在 dhara journal checkpoint 头里,普通恢复模式清不掉,需全片擦除——见 `docs/测试说明/存储维护与容量恢复.md`);CH395F 套件 `test/board/ch395f_test.h`:硬件层 Phase 1~5 与 NET 层 Phase 6~11 互斥;CH395F 硬件层模式下 netTask(基础线程:无条件创建、全构建唯一实例)入口自挂起(不运行),testTask 独占 CH395F
|
||||
- **改驱动前先读对应 `docs/问题记录/*_Trap_Records.md`**(CH395F / GD5F2GQ5UE / SD2506);各模块测试指南见 `docs/测试说明/`;**存储维护/容量恢复**(全片擦除恢复 dhara 坏块记账)见 `docs/测试说明/存储维护与容量恢复.md`
|
||||
- **LVGL I1 绘制缓冲含 palette 头(2026-09-21 实证)**:索引色格式 draw buffer 布局 = `[palette 8B][pixel data stride×h]`(`lv_draw_buf_create` 加 8B、`lv_draw_buf_goto_xy`/`lv_refr get_max_row` 都按此跳/扣),但 `lv_display_set_buffers`/`lv_draw_buf_init` 的容量校验**只看 `stride×h`**——只给 3200B 会被静默接受:末 8 个像素字节写到缓冲外(曾打脏相邻 `s_frame_box[0]` 指针 → HardFault),且 flush 收到的首地址含 palette(整帧错位 8 字节)。`ui_port.c` 按 `stride×h + palette` 供给并在 flush 时 `+palette` 取像素;详见 `docs/UI设计/UI_设计架构.md` §1.4 第 8 条
|
||||
- **MCU 端必须显式应用 dark mono 主题(2026-09-21 实证,远端"纯白屏"根因)**:`lv_display_create()` 会按 `lv_theme_mono_init(disp, false)`(白底黑字)初始化**单例**主题,且 `lv_display_set_theme()` 只把主题重应用到 `screens[0]`(底层)→ **活动屏幕仍是 LVGL 类默认浅灰 `F5F5F5`**;而 UI 内容恒用 `lv_color_white()` 绘制 → 白底白字不可见、远端整帧 99% 为亮(客户端显示纯白)。修法(`ui_port.c`):`lv_theme_mono_init(disp, true, ...)` + `lv_display_set_theme()` + 对活动屏幕再 `lv_theme_apply()`(实测 `F5F5F5` → `000000`);同时显式写 I1 调色板头 8B(`[0]=黑 [1]=白`,LVGL 不替调用方初始化);详见 `docs/UI设计/UI_设计架构.md` §1.4 第 9 条
|
||||
|
||||
## 串口助手 serial_tool
|
||||
|
||||
- 入口:`& '.venv\Scripts\python.exe' .dsh/skills/serial-assistant/scripts/serial_tool.py <命令>`(daemon 自动拉起;全部命令 5s 超时、JSON 输出、exit 0/1/2/3)
|
||||
- 全量历史 = 日志文件 `.dsh/skills/serial-assistant/scripts/state/serial.log`(RX/TX/SYS 时间戳行);agent 直接 Read/Grep 文件,**无采集窗口仪式**;20MB 上限自动截头保尾 10MB
|
||||
- 常用:`open`(幂等)/ `send <文本>`(或 `--hex 0D0A`)/ `log --tail N --grep P [-i] [--regex]`(**先过滤后截尾**)/ `clear`(代际+1)/ `wait-idle --settle-ms 8000 --max-ms 30000 [--first-timeout-ms 12000]` / `status` / `close`
|
||||
- 串口端口单一配置源:`.serialtest/config.json`(当前 COM6,2026-09-21 台架重插后由 COM7 变更;2026-10-10 由 `.opencode/serial_config.json` 迁入;串口采集/测试实现已收敛到 `.dsh/skills/serial-assistant` + `serial-boot-test`,仓库内不再有 `tools/serial_tool`/`test/host/run_boot_test` 副本);端口变化**只改这个文件**
|
||||
- 命令/工作流/故障排查 `docs/测试说明/串口助手使用指南.md`;契约/daemon 行为/陷阱 `docs/测试说明/串口助手设计说明.md`
|
||||
- **抓开机日志铁律(2026-09-20 实测;②③ 已于 2026-10-10 修复)**:① 顺序 `open` → `clear` → 烧录/复位 → `wait-idle` → 读日志校验标记(**采集必须先于烧录**,否则错过开机头几行);② `wait-idle` 的 CLI 超时现按 `--max-ms` **自动推导**,不再需要手工带 `--timeout`(旧版固定 5s 会在 daemon 还在等时掐断连接、daemon.log 刷 traceback);③ `log --grep` 已改为**先过滤后截尾**——旧实现先 `tail`(默认 50)后 grep,开机 marker 必然查不到,这才是当年"`log --grep` 实测不可信"的真根因,现可直接作判定依据(Read/Grep 文件仍等效)
|
||||
- **`wait-idle` 判据(2026-10-10 修)**:`no_data` = **本次等待期间零字节**(旧实现比的是 daemon 累计 `rx_bytes` 是否为 0,常驻 daemon 下永不触发 → 板子全程静默也误报 `idle:true`);**首字节到达前不启动静默计时**(`--first-timeout-ms` 兜底,`run_boot_test.py --first-wait-ms` 默认 12000);返回的 `rx_bytes` 是本次增量而非累计
|
||||
- **采集卫生与查询面(2026-10-10 第三批)**:无换行残行 **64KB 封顶**(板子跑飞/烧录期的二进制垃圾旧实现无限攒内存,还会把后续每条正常行拼成一条读不出的长行);`open`/`close`/`clear` = **会话边界**,残行一并丢弃(旧实现里上一段被复位截断的半行会粘进新会话第一行,开机日志首行因此不可读);`cap_truncate` **按行边界**截头(旧实现按字节切 → 截出来的首行是半行,破坏"只含完整行"不变量);`open` 不带 `--dtr/--rts` = **保持当前线路状态**(旧实现把"没给"当 False,重开即静默拉低),显式给则现场应用并在 `note` 回显;`ports` 回 `configured_present` + `hint`(系统里只有一个候选才给,多候选不猜),`open` 的 `port not found` 也带 `[hint: ...]`;`status` 回 `rx_age_ms`
|
||||
- ⚠️ **看到两个 `serial_tool.py --serve` 进程是正常的**(2026-10-10 实测):`.venv\Scripts\python.exe` 是 venv stub,会派生基解释器(本机 = `C:\ProgramData\anaconda3\python.exe`)子进程跑真 daemon,`status.daemon.pid` 报的是子进程;**别把这对父子当成"多 daemon 并存"去 taskkill**(HTTP 口独占绑定 `SO_EXCLUSIVEADDRUSE` 已生效,`status.daemon.exclusive_bind: true` 可验)。真要清的是 `status`/`meta.json` 里那个 pid 之外的**独立**残留实例
|
||||
- **`open` 报"拒绝访问/端口被占用"时**:先查**残留旧 daemon**(上一会话遗留的 `serial_tool.py --serve` python 进程——口句柄已死但进程仍占着串口与 HTTP 口 18765)→ `taskkill /F /PID <旧PID>` → 重新 `open`(自动拉起新 daemon,系统自动释放口);改 `.serialtest/config.json` 后同样必须先杀旧 daemon 再 `open`(daemon 启动时缓存配置)
|
||||
|
||||
## LVGL PC 模拟器
|
||||
|
||||
- `simulator/`(CMake + SDL2 预编译库)与 MCU 端共用 UI 源码(`ui_menu.c`、`ui_font_cn_12.c`),免烧录开发 UI:`& "simulator\build_sim.bat"`(按本机 VS 自动选生成器:16=2019 / 17=2022 / 18=2026,**勿把生成器版本写死**;`build/` 缓存生成器与本机 VS 不符时自动删除重建)→ 运行 `simulator\build\Debug\lvgl_sim.exe`
|
||||
- 自检:`--cfgtest`(配置闭环)/ `--logtest`(日志引擎)/ `--standby shot.bin`(待机屏截图)/ `--idle <秒>`
|
||||
- UI 改动一律走 **`ui-verify` skill 五步门禁**;含新汉字的 UI 文案提交前先跑 **`font-regen` skill** 重生成中文字体
|
||||
- 所有 `net_*` socket 操作经 `netMsgQueue` 在 netTask 串行执行;非 netTask 任务直接调 `net_*` API,勿直接碰 CH395F 驱动
|
||||
- **`net_*` 公开 API 的错误码经"任务通知值"回传**:`net_msg_t` 是**按值拷入队列**的,netTask 改的是自己那份副本,调用方永远看不到 → 错误码搭 `xTaskNotify()` 通知值回来。**勿给 `net_msg_t` 加"回传用"字段**(调用方只会读到栈垃圾)
|
||||
- **单连接 socket 的断开语义**:EOF 时 net 层已 `auto_relisten`,应用层**不得**在 EOF 时 `net_close` 该 fd(会连监听一起毁掉 → 端口"连接被拒");仅当数据 fd ≠ 监听 fd(模拟器多连接后端)时才关
|
||||
- 网口配置:`/config.ini` [network] 段(工厂默认 192.168.1.100/24、gw .1;MAC 全零 = 芯片内置;UI 保存立即生效;**旧档 `/cfg/netcfg.bin` 迁移已于 2026-10-10 删除**,旧档存在则被忽略);同文件 **[ftp] 段 = FTP 鉴权**(`user` / `pwd_hash` = SHA-256(user‖password) / `write`,出厂默认 admin/admin,默认口令在用有启动告警;连续 5 次失败锁 60 s)→ `docs/子系统设计/网络配置说明.md`
|
||||
- **新 .c 文件必须手动注册到两处**才能双端编译:`MDK-ARM/STM32F407-Demo.uvprojx` + `simulator/CMakeLists.txt`(用 `new-source-file` skill);⚠ **新文件落在还没有的新目录,还要把该目录加进 `uvprojx` 的 `<IncludePath>`**——否则报错落在**消费者 TU** 上(`cannot open source input file`),极易误判
|
||||
- 测试构建:`test/board/test_config.h` 的 `TEST_SUITE_CH395F/STORAGE/GD5F/RTC` **互斥**(一次只能一个,`#error`);全部注释 = 产品构建,**受 `test-suite-switch` 钩子门禁**;`ENABLE_DESTRUCTIVE_TESTS` **默认关闭**(关闭时套件只读模式,可随时在在用设备上跑)——⚠ 跑一轮破坏性 GD5F 即让 dhara 多记一个坏块且该计数持久化,普通恢复清不掉 → `docs/测试说明/存储维护与容量恢复.md`;板级套件运行用 **`board-test-suite` skill**
|
||||
- **改驱动前先读对应 `docs/问题记录/*_Trap_Records.md`**(CH395F / GD5F2GQ5UE / SD2506);各模块测试指南见 `docs/测试说明/`
|
||||
- **改 UI / 绘制缓冲前读两条实证坑**(I1 palette 头越界、白底白字整屏纯白)→ `docs/UI设计/UI_设计架构.md` §1.4 第 8/9 条,**`ui-verify` skill** 亦载
|
||||
|
||||
## 文档与图形
|
||||
|
||||
- 配置文件 `docs/子系统设计/配置文件设计说明.md`;墙钟 `docs/子系统设计/系统墙钟时间维护说明.md`;控制台单写者日志 `docs/子系统设计/控制台单写者日志设计说明.md`;代码规范 `docs/代码规范/`;测试说明 `docs/测试说明/`;审计发现与待办 `docs/待处理内容.md`(统一登记簿:P-x 审计缺陷 / N·S·T-x 观察 / S1x·A-x 测试架构专项 / 阶段排序 / 诊断基线);引用关系图 `docs/架构设计/引用关系图说明.md`
|
||||
- **登记簿类文档「完成即销项」**(2026-09-26 定规):`docs/待处理内容.md` 这类跨文档待办登记簿,事项修复/关闭后**直接从表中删除该行**(不保留"已消化"行、不建归档区)——结论与证据应已沉淀进对应模块文档/问题记录,登记簿只留**未办项**。销项时必须:① 先核对目标文档确已含完整记录(删了不丢信息)② 同步清理其他文档对该编号的引用(改为指向目标文档,或注明"已销项")③ 改完跑 `check_doc_links.py --all` + pre-commit
|
||||
- 图形走 **`draw-diagram` skill**:精确布局图(协议帧/分层/屏障)用 `tools/arch_diagram/gen_*.py` 生成器(产物放 `docs/images/`,脚本与 .svg 一并提交);拓扑图(流程/时序/状态机)用 Mermaid;**勿手绘 ASCII 框图**
|
||||
- **文件引用关系图**走 `tools/refgraph`:`& 'tools\refgraph\gen_refgraph.bat'` 生成文件级 + 模块级引用图(产物 `docs/images/引用关系图.*`,脚本与 .dot/.svg/.json 一并提交,PNG 本地预览不提交);项目文件变化后重跑更新;渲染依赖 Graphviz(winget 可装)
|
||||
- **登记簿类文档「完成即销项」**:`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
|
||||
- 文档写作规范(编码/BOM/链接/表格/一致性钩子)→ `docs/文档规范.md`;全量索引与推荐阅读顺序 → `docs/README.md`
|
||||
|
||||
## 边界(Ask first / Never)
|
||||
|
||||
**Ask first**(须用户明确指示才做,均不是任何任务的隐含步骤):`git commit` / `git push`(构建与测试通过后停下等指令)· 开 `ENABLE_DESTRUCTIVE_TESTS` 跑破坏性套件(持久代价 → 模块约定)· CubeMX 重生成(→ `cubemx-regen` skill)· 登记簿删行销项(→ 文档与图形)· 建任务/抬栈/加大缓冲(先跑预算脚本 → 平台约束)
|
||||
|
||||
**Never**(任何任务):用 `Get-Content`/`Set-Content`/`cmd` 重写含中文文件、用 `git checkout`/`reset --hard` 丢弃未提交工作(→ 铁律)· 在 `USER CODE` 区之外写定制代码、自研代码写 C 库堆分配(→ 平台约束)· 非 defaultTask 直接触碰配置引擎、非 netTask 直接碰 CH395F 驱动(→ 模块约定)· 提交 UV4 抖动的 `*.uvprojx`、提交 `.mnemon/`(记忆工具本地状态,已 `.gitignore`)
|
||||
|
||||
## 按主题查哪份文档
|
||||
|
||||
| 主题 | 去处 |
|
||||
|---|---|
|
||||
| 全量索引 / 推荐阅读顺序 | `docs/README.md` |
|
||||
| 架构全景 / 启动时序与内核前模式 | `docs/架构设计/项目架构总览.md`、`docs/架构设计/项目启动顺序管理.md` |
|
||||
| 产品需求基线(做到什么程度) | `docs/产品需求/产品需求设计文档.md` |
|
||||
| 看门狗 + CubeMX 重生成存活方法论 + ARMCC `__asm` | `docs/子系统设计/看门狗设计说明.md` §10 |
|
||||
| ADC 采样节拍 / 通道映射 / V0.1 硬件缺陷 | `docs/子系统设计/ADC采样节拍设计说明.md`、`docs/硬件平台/ADC通道映射说明.md` |
|
||||
| 存储栈(FatFs / dhara / lftpd / 两条 FatFs 陷阱) | `docs/架构设计/STM32F4-Base存储架构说明.md` |
|
||||
| RAM / 栈预算 / 容量台账 | `docs/内存管理/RAM使用分析与优化方案.md`、`内存内容分配策略.md` |
|
||||
| pre-commit 31 个钩子逐个详解 | `docs/代码规范/pre-commit使用说明.md` |
|
||||
| 代码规范 V1.0 / cppcheck 口径 | `docs/代码规范/嵌入式C语言代码规范_V1.0.md`、`cppcheck使用说明.md` |
|
||||
| 自动化测试 / 实机测试 / 板级套件 | `docs/测试说明/自动化测试流程.md`、`实机测试实施指南.md`、`board-test-suite` skill |
|
||||
| 烧录与复位语义 | `docs/测试说明/ST-LINK_CLI_复位使用说明.md` |
|
||||
| 串口助手 | **`serial-assistant` skill**(命令/判据/陷阱,用到自动加载)、`docs/测试说明/串口助手使用指南.md`、`串口助手设计说明.md` |
|
||||
| UI 架构 / 模拟器 / UI 自动化回归 | **`lvgl-simulator` skill**(构建/自检/报错速查,用到自动加载)、`docs/UI设计/UI_设计架构.md`、`LVGL模拟器部署与使用说明.md`、`UI_自动化测试流程.md`;UI 改动回归走 **`ui-verify` skill** |
|
||||
| 待办与审计发现登记簿 | `docs/任务规划/README.md` |
|
||||
| 多任务并行开发(开空间 / 合并回收 / 台架与构建排队) | **`task-worktree` skill**(`@task`,用到自动加载)、`docs/环境与部署/并行任务工作树方案.md` |
|
||||
| 换机 / 工具路径环境变量 / CI | `docs/环境与部署/` |
|
||||
|
||||
+256
-16
@@ -1,27 +1,267 @@
|
||||
# App/algo/ — 采集与算法层(L1 / L2)
|
||||
|
||||
> **自研代码**,属 `docs/算法/继电保护功能复现规划.md` §2.1 分层中的 **L1(采集与预处理)**
|
||||
> 与 **L2(相量计算,拟)**。原则:**算法与 I/O 分离**——本层不碰寄存器、不碰任务调度,
|
||||
> 只做"数"(码值 → 工程量)与"相量"(工程量 → 复相量),从而可成为主机侧单元测试
|
||||
> (L0.5)的天然适格对象(落实登记簿 **A-4**)。
|
||||
> 与 **L2(相量计算)**。原则:**算法与 I/O、与调度分离**——本层不建任务、不做保护判据、
|
||||
> 不碰寄存器(唯一例外见 §3.1),只做"数"(原始码值 → 语义通道)与"相量"
|
||||
> (采样序列 → 复相量),因而可被主机侧单元测试(L0.5)离线全覆盖(落实登记簿 **A-4**)。
|
||||
>
|
||||
> 任务侧(`App/task/adc_task.c`)只做**消费与编排**;任何新算法一律落在本目录,勿塞进 `App/task/`。
|
||||
|
||||
## 现有模块
|
||||
## 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-6(软件侧) |
|
||||
|
||||
依赖关系(自下而上,无环):`adc_input.h`(纯枚举 + 声明)← `sample_ring` ← `phasor`;
|
||||
`prot_set` **独立**(仅 `stdint.h`);`prot_elm` ← `prot_set` + `phasor`。
|
||||
`sample_ring.c` / `phasor.c` / `adc_health.c` / `prot_set.c` / `prot_elm.c` **零平台依赖**
|
||||
(只用 `math.h` / `stddef.h` / `stdint.h` 与本目录头);
|
||||
`adc_input.c` 另依赖 `adc.h`(`hadc1`)与 `tpafe5160.h`(快照),主机侧以桩头替身。
|
||||
|
||||
## 2. 数据流(一拍 = TIM3 1 kHz 节拍)
|
||||
|
||||
```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 --> GET["phasor_get(idx)"]
|
||||
GET --> CONS["消费者:adcTask 日志(现状)<br/>UI / 自检 / L3 保护任务(待接)"]
|
||||
```
|
||||
|
||||
初始化落点:`adc_input_init()` 与 `phasor_init()` 由 `App/app/app_main.c` 的
|
||||
`app_main_init()` 在**内核启动前**调用(三者 `*_init()` 均无 RTOS 依赖、幂等);
|
||||
`phasor_init()` 内部会连带 `sample_ring_init()`,等价于"刚上电"。
|
||||
|
||||
## 3. 逐文件说明
|
||||
|
||||
### 3.1 `adc_input.c` / `adc_input.h` — L1 ADC 输入层
|
||||
|
||||
**解决的问题**:V0.1 上 8 路电气量被两片转换器分片(硬件缺陷 **N-18**),且网名 `AINn`
|
||||
≠ 板级 `ADCn` ≠ 转换器通道号(13/17 路置换)。若消费侧直接索引驱动缓冲,改版即全线错位。
|
||||
本层把"哪一路"收进一张表,上层**按语义取数、不写死下标**。
|
||||
|
||||
| API | 干什么 | 注意 |
|
||||
|---|---|---|
|
||||
| `adc_input.c/h` | L1 | **ADC 输入层**:① **语义通道映射**(L1-4)——`UA/UB/UC/UN/IA/IB/IC/IN/温度` → 物理通道,消费侧**按语义取数、不写死下标**;② **STM32 ADC1 9 通道软件触发顺序扫描**(L1-3);③ TPAFE5160 原子快照消费 |
|
||||
| `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`——四类成因不同,**勿混读** |
|
||||
|
||||
## 关键约定
|
||||
**要点**
|
||||
|
||||
- **语义映射的唯一权威口径**是 `docs/硬件平台/ADC通道映射说明.md`(V0.1 有 13/17 路被置换,
|
||||
网名编号 ≠ 通道号)。V0.1 上 8 路电气量分属两片转换器(硬件缺陷 **N-18**):`UA/UN/IA`
|
||||
在 TPAFE5160、`UB/UC/IB/IC/IN` 在 STM32 ADC1。**V0.2 改版只改映射表内容**,本层 API
|
||||
与上层语义不动。
|
||||
- **取数一律经 `adc_input_raw()`**(按语义),禁止在消费侧直接索引驱动缓冲或写死下标。
|
||||
- 本层**不做**跨转换器的相位 / 功率 / 功率因数 / 矢量类运算(V0.1 硬件缺陷 N-18 限制)。
|
||||
- 语义映射表 `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` 那种零依赖形态,才能免桩单测。
|
||||
|
||||
## 相关文档
|
||||
### 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 落实进度注。
|
||||
|
||||
### 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()` 之后调 |
|
||||
| `prot_elm_eval(now_ms, meas)` | **纯判据**:定值 RAM + 测量数组 → 全元件状态机 | 主机单测入口;NULL 测量按 0 |
|
||||
| `prot_elm_run(now_ms)` | 适配层:`phasor_get()` 组测量 → eval | 任一相量未就绪 → **整拍跳过**(不引入陈旧判据) |
|
||||
| `prot_elm_state()` / `prot_elm_active_mask()` | 状态快照 / 动作位图 | 供 L4 出口与心跳日志 |
|
||||
|
||||
**要点**
|
||||
|
||||
- **滞回**:已启动用返回门槛(`定值 × 返回系数`),未启动用动作门槛——
|
||||
过量/欠量方向由实例表 `over` 位统一处理(`UV` 是唯一欠量实例)。
|
||||
- **定时限**:`(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 外部通道)。
|
||||
零序过流(IN 走 STM32)与过频/低频(待频率算法)**故意不在表中**。
|
||||
- **板端接线已落地(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
|
||||
|
||||
## 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[31]` + 封存/位图/闭锁 | 136 B(**未接线前被链接器移除,实测 0**) |
|
||||
| `prot_elm.c` `s_rt[7]`(16 B/实例) | 112 B(同上,实测 0) |
|
||||
| 运行期栈:`phasor_run()` 的 `win[20]` | 80 B/帧 |
|
||||
|
||||
改容量(`SAMPLE_RING_CYCLES` / 实例数 / `ADC_SEM_COUNT`)前后各跑一次
|
||||
`& '.venv\Scripts\python.exe' scripts/stack_report.py`(口径见 `mem-budget` skill)。
|
||||
|
||||
## 6. 构建与测试
|
||||
|
||||
- **挂载**:七个 `.c` 已登记在 `MDK-ARM/STM32F407-Demo.uvprojx`,`../App/algo` 已在 `<IncludePath>` 内。
|
||||
本层**不进** `simulator/CMakeLists.txt`(UI 模拟器不跑采集链路)。
|
||||
- **主机侧单测(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` |
|
||||
| 定值模型(30 用例:范围校验 / all-or-nothing 装载 / 封存-自检-闭锁 / Q10 往返 / 元数据表自检) | `& '.venv\Scripts\python.exe' test/host/prot_set/run_unit_test.py` | `prot-set-unit-test` |
|
||||
| 元件框架(27 用例:滞回与定时限 / 欠量方向 / 投退 / 闭锁联动 / 时基回绕 / 位图聚合) | `& '.venv\Scripts\python.exe' test/host/prot_elm/run_unit_test.py` | `prot-elm-unit-test` |
|
||||
|
||||
改本目录任一 `.c/.h`,提交时对应钩子自动跑;详见 `docs/测试说明/相量层单元测试说明.md`。
|
||||
- **板端**:`@build` 0 错误 0 警告 → `@test` 看 `[ADC_TASK]` 首拍/心跳日志(`phasor(...)` 行、
|
||||
`ring` / `skip` 计数、`adc1_rc` / `adc1_err`、`tgap`);健康判据 = 零 `[ERR]`
|
||||
(`adc_health_report` 的告警全部以 `[ERR]` 开头 → 误报会被闭环直接抓住)。
|
||||
|
||||
## 7. 新增一个算法模块的固定动作
|
||||
|
||||
1. 在本目录建 `xxx.c/h`,文件头写模块注释块(规范 V1.0),**尽量零平台依赖**以便免桩单测。
|
||||
2. 用 `new-source-file` skill 注册到 `MDK-ARM/STM32F407-Demo.uvprojx`(目录已在 IncludePath,
|
||||
若新建子目录则必须补 IncludePath)。
|
||||
3. 加 `test/host/<模块>/` 主机单测 + `.pre-commit-config.yaml` 钩子(照 `phasor-unit-test` 抄)。
|
||||
4. 消费者写在 `App/task/`,本层只出"数";跨转换器运算的禁令见 §4。
|
||||
|
||||
## 8. 相关文档
|
||||
|
||||
- 通道映射权威口径:`docs/硬件平台/ADC通道映射说明.md`
|
||||
- 分层与路线图:`docs/算法/继电保护功能复现规划.md`(§2.1 分层、§3.1 L1 清单)
|
||||
- 采样节拍:`docs/子系统设计/ADC采样节拍设计说明.md`
|
||||
- 分层与路线图(L1/L2 清单与状态):`docs/算法/继电保护功能复现规划.md`
|
||||
- 傅氏算法与有效值口径:`docs/算法/傅氏算法详解.md`
|
||||
- 采样节拍(TIM3 1 kHz + TRGO 触发 ADC1/DMA):`docs/子系统设计/ADC采样节拍设计说明.md`
|
||||
- 相量层单测细则:`docs/测试说明/相量层单元测试说明.md`
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
/*
|
||||
* 模块名称:ADC 采样有效性校验(L1 采集与预处理,规划项 L1-6)
|
||||
* 模块功能:见 adc_health.h 文件头。本文件为**纯逻辑实现**:输入计数快照、
|
||||
* 输出故障位/事件位,零平台依赖(仅 stdint.h)→ 主机侧免桩单测。
|
||||
* 适用平台:STM32F407ZGTx + TPAFE5160
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-11
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-11 王建锋 创建:L1-6 采样有效性校验
|
||||
*/
|
||||
|
||||
#include "adc_health.h"
|
||||
|
||||
/* ===================== 内部状态 ===================== */
|
||||
|
||||
/* 基线是否已建立:首次 check() 只记基线、不判定(开机首拍无参照系) */
|
||||
static uint8_t s_primed;
|
||||
|
||||
/* 上次看到的计数(差值法判"有无前进",回绕安全) */
|
||||
static uint32_t s_tpafe_seq;
|
||||
static uint32_t s_tpafe_frst;
|
||||
static uint32_t s_stm32_seq;
|
||||
static uint32_t s_stm32_errs;
|
||||
static uint32_t s_stm32_gaps;
|
||||
static uint32_t s_stm32_hw;
|
||||
|
||||
/* 两侧"最后一次看到序号前进"的时刻(判静默) */
|
||||
static uint32_t s_tpafe_adv_ms;
|
||||
static uint32_t s_stm32_adv_ms;
|
||||
|
||||
/* 故障位 */
|
||||
static uint32_t s_flags; /* 电平位(stall 可自愈;backward 锁存在此) */
|
||||
static uint32_t s_sticky; /* 锁存位(含电平位历史并集) */
|
||||
static uint32_t s_tpafe_gaps;
|
||||
|
||||
/* ===================== 对外接口 ===================== */
|
||||
|
||||
/*
|
||||
* 函数功能:采样有效性校验初始化(幂等)
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 限定条件:无(内核启动前可调)
|
||||
* 函数说明:清全部内部状态;判定从下一次 check() 的首拍基线重新开始。
|
||||
*/
|
||||
void adc_health_init(void) {
|
||||
s_primed = 0u;
|
||||
s_tpafe_seq = 0u;
|
||||
s_tpafe_frst = 0u;
|
||||
s_stm32_seq = 0u;
|
||||
s_stm32_errs = 0u;
|
||||
s_stm32_gaps = 0u;
|
||||
s_stm32_hw = 0u;
|
||||
s_tpafe_adv_ms = 0u;
|
||||
s_stm32_adv_ms = 0u;
|
||||
s_flags = 0u;
|
||||
s_sticky = 0u;
|
||||
s_tpafe_gaps = 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按计数快照做一次有效性判定
|
||||
* 入口参数:in - 计数快照;NULL 则本次跳过(out 不动)
|
||||
* out - 判定结果;NULL 则仅更新内部状态
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅任务上下文(与 adc_input_poll() 同节奏,每拍一次)
|
||||
* 函数说明:判定顺序 = 建基线 → 序号差值(倒退/丢帧/前进)→ 静默时长 →
|
||||
* 错误计数增量。events 仅本次有效;flags/sticky 为持续状态。
|
||||
*/
|
||||
void adc_health_check(const adc_health_in_t *in, adc_health_status_t *out) {
|
||||
uint32_t events = 0u;
|
||||
|
||||
if (in == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* 1) 首拍基线:只记录,不判定(开机时序号已非 0,无"上一拍"参照) */
|
||||
if (s_primed == 0u) {
|
||||
s_primed = 1u;
|
||||
s_tpafe_seq = in->tpafe_seq;
|
||||
s_tpafe_frst = in->tpafe_frst_err;
|
||||
s_stm32_seq = in->stm32_seq;
|
||||
s_stm32_errs = in->stm32_errs;
|
||||
s_stm32_gaps = in->stm32_gaps;
|
||||
s_stm32_hw = in->stm32_hw_errs;
|
||||
s_tpafe_adv_ms = in->now_ms;
|
||||
s_stm32_adv_ms = in->now_ms;
|
||||
} else {
|
||||
uint32_t d; /* 序号差值(两侧共用,cppcheck:作用域收在本块) */
|
||||
|
||||
/* 2) TPAFE 序号差值:倒退 = 单调性破坏(锁存);>1 = 丢帧(事件 + 计数) */
|
||||
d = in->tpafe_seq - s_tpafe_seq;
|
||||
if ((int32_t)d < 0) {
|
||||
s_flags |= ADC_HF_SEQ_BACKWARD;
|
||||
s_sticky |= ADC_HF_SEQ_BACKWARD;
|
||||
s_tpafe_seq = in->tpafe_seq; /* 重新对齐,避免持续误判 */
|
||||
s_tpafe_adv_ms = in->now_ms;
|
||||
} else {
|
||||
if (d > 1u) {
|
||||
s_tpafe_gaps += (d - 1u);
|
||||
events |= ADC_HE_TPAFE_GAP;
|
||||
}
|
||||
if (d >= 1u) {
|
||||
s_tpafe_seq = in->tpafe_seq;
|
||||
s_tpafe_adv_ms = in->now_ms;
|
||||
}
|
||||
}
|
||||
|
||||
/* 3) STM32 序号差值:同一口径(gap 计数在 adc_input 侧另有细账,
|
||||
* 这里只取"前进/倒退/静默"三态,避免两处判据打架) */
|
||||
d = in->stm32_seq - s_stm32_seq;
|
||||
if ((int32_t)d < 0) {
|
||||
s_flags |= ADC_HF_SEQ_BACKWARD;
|
||||
s_sticky |= ADC_HF_SEQ_BACKWARD;
|
||||
s_stm32_seq = in->stm32_seq;
|
||||
s_stm32_adv_ms = in->now_ms;
|
||||
} else if (d >= 1u) {
|
||||
s_stm32_seq = in->stm32_seq;
|
||||
s_stm32_adv_ms = in->now_ms;
|
||||
}
|
||||
|
||||
/* 4) 静默判定(电平位,可自愈):距最后一次前进 >= 门限 */
|
||||
if ((uint32_t)(int32_t)(in->now_ms - s_tpafe_adv_ms) >= ADC_HEALTH_STALL_MS) {
|
||||
s_flags |= ADC_HF_TPAFE_STALL;
|
||||
s_sticky |= ADC_HF_TPAFE_STALL;
|
||||
} else {
|
||||
s_flags &= ~ADC_HF_TPAFE_STALL;
|
||||
}
|
||||
if ((uint32_t)(int32_t)(in->now_ms - s_stm32_adv_ms) >= ADC_HEALTH_STALL_MS) {
|
||||
s_flags |= ADC_HF_STM32_STALL;
|
||||
s_sticky |= ADC_HF_STM32_STALL;
|
||||
} else {
|
||||
s_flags &= ~ADC_HF_STM32_STALL;
|
||||
}
|
||||
|
||||
/* 5) 错误计数增量(事件 + 锁存):三类计数正常应恒为 0 增量 */
|
||||
if (in->tpafe_frst_err != s_tpafe_frst) {
|
||||
events |= ADC_HE_FRSTDATA;
|
||||
s_sticky |= ADC_HF_FRSTDATA;
|
||||
s_tpafe_frst = in->tpafe_frst_err;
|
||||
}
|
||||
if (in->stm32_errs != s_stm32_errs) {
|
||||
events |= ADC_HE_STM32_ERR;
|
||||
s_sticky |= ADC_HF_STM32_ERR;
|
||||
s_stm32_errs = in->stm32_errs;
|
||||
}
|
||||
if (in->stm32_gaps != s_stm32_gaps) {
|
||||
events |= ADC_HE_STM32_GAP;
|
||||
s_stm32_gaps = in->stm32_gaps;
|
||||
}
|
||||
if (in->stm32_hw_errs != s_stm32_hw) {
|
||||
events |= ADC_HE_HW_ERR;
|
||||
s_sticky |= ADC_HF_HW_ERR;
|
||||
s_stm32_hw = in->stm32_hw_errs;
|
||||
}
|
||||
}
|
||||
|
||||
if (out != 0) {
|
||||
out->flags = s_flags;
|
||||
out->events = events;
|
||||
out->sticky = s_sticky;
|
||||
out->tpafe_gaps = s_tpafe_gaps;
|
||||
out->tpafe_stall_ms =
|
||||
((s_flags & ADC_HF_TPAFE_STALL) != 0u)
|
||||
? (uint32_t)(int32_t)(in->now_ms - s_tpafe_adv_ms)
|
||||
: 0u;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读当前电平故障位
|
||||
* 入口参数:无
|
||||
* 返 回 值:见 adc_health.h ADC_HF_TPAFE_STALL/STM32_STALL/SEQ_BACKWARD
|
||||
* 限定条件:无
|
||||
* 函数说明:与 check() 出参 flags 同源。
|
||||
*/
|
||||
uint32_t adc_health_flags(void) {
|
||||
return s_flags;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读锁存故障位
|
||||
* 入口参数:无
|
||||
* 返 回 值:电平位 + FRSTDATA/STM32_ERR/HW_ERR 的并集
|
||||
* 限定条件:无
|
||||
* 函数说明:出现过即留痕,直到 adc_health_init() 才清。
|
||||
*/
|
||||
uint32_t adc_health_sticky(void) {
|
||||
return s_sticky;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读 TPAFE 累计丢帧点数
|
||||
* 入口参数:无
|
||||
* 返 回 值:序号前进 >1 的超出部分累计
|
||||
* 限定条件:无
|
||||
* 函数说明:与 STM32 侧 adc_input_stm32_gaps() 对称的 TPAFE 侧细账。
|
||||
*/
|
||||
uint32_t adc_health_tpafe_gaps(void) {
|
||||
return s_tpafe_gaps;
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
* 模块名称:ADC 采样有效性校验(L1 采集与预处理,规划项 L1-6)
|
||||
* 模块功能:对两片转换器的"节拍是否还在喂数据"做**判定层**(观测计数由
|
||||
* `adc_input` / `tpafe5160` 驱动提供,本模块只做判定与告警位):
|
||||
* ① 静默(stall):帧序号连续 >= ADC_HEALTH_STALL_MS 无前进;
|
||||
* ② 丢帧(gap):帧序号一次前进 >1(消费者超拍,中间帧被覆盖);
|
||||
* ③ 单调性破坏:帧序号倒退(计数器被改写 / 驱动 bug,锁存);
|
||||
* ④ 错误计数增量:FRSTDATA 跳变 / HAL ADC·DMA 错误 / STM32 停摆计数。
|
||||
* ⚠ 本层**纯逻辑**:不碰寄存器、不取数、不打日志——输入是调用方
|
||||
* 采集的计数快照,输出是故障位;日志与自检上报由消费者
|
||||
* (`App/task/adc_task.c`)按位执行 → 主机侧可单测(L0.5,A-4)。
|
||||
* 适用平台:STM32F407ZGTx + TPAFE5160
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-11
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-11 王建锋 创建:L1-6 采样有效性校验(静默/丢帧/单调性/错误增量)
|
||||
*/
|
||||
|
||||
#ifndef ADC_HEALTH_H
|
||||
#define ADC_HEALTH_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 静默判定门限(ms):帧序号连续无前进达到该时长即判静默。
|
||||
* 名义节拍 1 kHz → 正常每拍 +1;100 拍无前进不可能是抖动,只可能是
|
||||
* 节拍源/转换器/EXTI 真停(消费者超拍只会造成 gap=d>1,不会 d=0)。 */
|
||||
#define ADC_HEALTH_STALL_MS 100U
|
||||
|
||||
/* 电平故障位(adc_health_flags(),当前活跃;stall 可自愈清除,backward 锁存)
|
||||
* ——供后续 L5-4「自检失败闭锁」直接消费 */
|
||||
#define ADC_HF_TPAFE_STALL (1uL << 0) /* TPAFE 帧序号静默 */
|
||||
#define ADC_HF_STM32_STALL (1uL << 1) /* STM32 DMA 完成计数静默 */
|
||||
#define ADC_HF_SEQ_BACKWARD (1uL << 2) /* 帧序号倒退(锁存,不自愈) */
|
||||
|
||||
/* 锁存故障位(adc_health_sticky(),出现过即置位、直到 init 才清;
|
||||
* 与电平位共用高位编号,gap 类"常态性超拍"不入锁存) */
|
||||
#define ADC_HF_FRSTDATA (1uL << 3) /* FRSTDATA 跳变计数有增量(锁存) */
|
||||
#define ADC_HF_STM32_ERR (1uL << 4) /* STM32 停摆计数有增量(锁存) */
|
||||
#define ADC_HF_HW_ERR (1uL << 5) /* HAL ADC/DMA 错误回调有增量(锁存) */
|
||||
|
||||
/* 边沿事件位(adc_health_check() 出参 events,仅本次调用有效)。
|
||||
* ⚠ FRSTDATA/STM32_ERR/HW_ERR 三个事件位与同名锁存位**编号一致**——
|
||||
* 消费者可直接用 events 掩码对 sticky/logged 位图做"每类留痕一次"的编排 */
|
||||
#define ADC_HE_TPAFE_GAP (1uL << 6) /* 本次 TPAFE 序号前进 >1 */
|
||||
#define ADC_HE_STM32_GAP (1uL << 7) /* 本次 STM32 gap 计数有增量 */
|
||||
#define ADC_HE_STM32_ERR (1uL << 4) /* 本次 STM32 停摆计数有增量 */
|
||||
#define ADC_HE_FRSTDATA (1uL << 3) /* 本次 FRSTDATA 计数有增量 */
|
||||
#define ADC_HE_HW_ERR (1uL << 5) /* 本次 HAL 错误计数有增量 */
|
||||
|
||||
/* 计数快照(由调用方采集,本模块不触碰任何驱动/外设) */
|
||||
typedef struct {
|
||||
uint32_t tpafe_seq; /* tpafe5160_frame_seq() */
|
||||
uint32_t tpafe_frst_err; /* tpafe5160_frstdata_err()(驱动为 uint16,此处放宽) */
|
||||
uint32_t stm32_seq; /* adc_input_stm32_seq() */
|
||||
uint32_t stm32_errs; /* adc_input_stm32_errs() */
|
||||
uint32_t stm32_gaps; /* adc_input_stm32_gaps() */
|
||||
uint32_t stm32_hw_errs; /* adc_input_stm32_hw_errs() */
|
||||
uint32_t now_ms; /* osKernelGetTickCount()(tick 回绕由差值法吸收) */
|
||||
} adc_health_in_t;
|
||||
|
||||
/* 判定结果快照 */
|
||||
typedef struct {
|
||||
uint32_t flags; /* 电平故障位 ADC_HF_TPAFE/STM32_STALL/SEQ_BACKWARD */
|
||||
uint32_t events; /* 本次边沿事件位 ADC_HE_xxx */
|
||||
uint32_t sticky; /* 锁存故障位(含电平位的历史并集) */
|
||||
uint32_t tpafe_gaps; /* TPAFE 累计丢帧点数(序号前进 >1 的超出部分之和) */
|
||||
uint32_t tpafe_stall_ms; /* TPAFE 当前静默时长(0=正常) */
|
||||
} adc_health_status_t;
|
||||
|
||||
/*
|
||||
* 函数功能:采样有效性校验初始化(幂等,内核启动前可调)
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 限定条件:无
|
||||
* 函数说明:清全部状态;首次 adc_health_check() 只建基线、不判定。
|
||||
*/
|
||||
void adc_health_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:按计数快照做一次有效性判定(每拍调用,开销为常数次整数运算)
|
||||
* 入口参数:in - 计数快照(NULL 则本次跳过,out 不动)
|
||||
* out - 判定结果(NULL 则仅更新内部状态)
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅任务上下文(与 adc_input_poll() 同节奏调用)
|
||||
* 函数说明:首次调用建立基线(不产生事件/故障)。序号差值按
|
||||
* `(int32_t)(now - prev)` 口径计算,tick/计数回绕均安全。
|
||||
*/
|
||||
void adc_health_check(const adc_health_in_t *in, adc_health_status_t *out);
|
||||
|
||||
/*
|
||||
* 函数功能:读当前电平故障位
|
||||
* 入口参数:无
|
||||
* 返 回 值:ADC_HF_TPAFE_STALL / ADC_HF_STM32_STALL / ADC_HF_SEQ_BACKWARD 组合
|
||||
* 限定条件:无
|
||||
* 函数说明:供 L5-4 闭锁判定与 UI;与 check() 出参 flags 同源。
|
||||
*/
|
||||
uint32_t adc_health_flags(void);
|
||||
|
||||
/*
|
||||
* 函数功能:读锁存故障位(出现过即置位,直到 init 才清)
|
||||
* 入口参数:无
|
||||
* 返 回 值:电平位 + ADC_HF_FRSTDATA/STM32_ERR/HW_ERR 的并集
|
||||
* 限定条件:无
|
||||
* 函数说明:stall 自愈后 sticky 仍保留该次故障痕迹("曾经停摆过"必须留痕)。
|
||||
*/
|
||||
uint32_t adc_health_sticky(void);
|
||||
|
||||
/*
|
||||
* 函数功能:读 TPAFE 累计丢帧点数
|
||||
* 入口参数:无
|
||||
* 返 回 值:序号前进 >1 的超出部分累计(与 STM32 侧 adc_input_stm32_gaps() 对称)
|
||||
* 限定条件:无
|
||||
* 函数说明:消费者超拍(单拍耗时 > 1 ms)所致;非零本身不是闭锁条件,
|
||||
* 但持续增长说明相量窗出现时间空洞,须排查耗时。
|
||||
*/
|
||||
uint32_t adc_health_tpafe_gaps(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* ADC_HEALTH_H */
|
||||
+192
-38
@@ -1,12 +1,17 @@
|
||||
/*
|
||||
* 模块名称:ADC 输入层(L1 采集与预处理)
|
||||
* 模块功能:语义通道映射(L1-4)+ STM32 ADC1 顺序扫描(L1-3)+ TPAFE 快照消费。
|
||||
* 模块功能:语义通道映射(L1-4)+ STM32 ADC1 采样消费(L1-3,TIM3 TRGO 触发 +
|
||||
* DMA 双缓冲)+ TPAFE 快照消费。
|
||||
* 消费侧经 adc_input_raw() 按语义取数,不写死下标。
|
||||
* 适用平台:STM32F407ZGTx + TPAFE5160
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-10 王建锋 创建:L1-3(STM32 ADC1 扫描)+ L1-4(语义映射)
|
||||
* v2.0 2026-10-10 王建锋 STM32 侧由"软件触发 + 逐通道轮询"改为
|
||||
* "TIM3 TRGO 硬件触发 + DMA 双缓冲消费":本层不再触发
|
||||
* 也不等待转换(消除最坏 2~3 ms 忙等);新增三个 HAL
|
||||
* 回调(ISR 内只计数)与 seq / hw_errs 观测量
|
||||
*/
|
||||
|
||||
#include "adc_input.h"
|
||||
@@ -16,11 +21,17 @@
|
||||
#include "adc.h" /* hadc1:MX_ADC1_Init() 已配 9 通道 / 144 cycles / 软件触发 */
|
||||
#include "tpafe5160.h" /* tpafe5160_snapshot():临界区原子快照(AGENTS 铁律) */
|
||||
|
||||
/* STM32 ADC1 单次转换轮询超时(ms)。单通道 ≈ 7.4 µs(144+12 cycles @ 21 MHz),
|
||||
* 2 ms 余量充裕;超时即判本次扫描失败,绝不长时间阻塞消费节拍 */
|
||||
#define ADC_INPUT_POLL_TIMEOUT_MS 2U
|
||||
/* STM32 ADC1 通道号上限(F4 上 ADC_CHANNEL_0..15 的宏值即通道号 0..15) */
|
||||
#define ADC_INPUT_STM32_CH_MAX 16U
|
||||
/* DMA 搬运缓冲长度 = 2 x 9 = 18:**双缓冲(乒乓)**。
|
||||
* ADC1 配 ExternalTrigConv=TIM3_TRGO + DMAContinuousRequests=ENABLE + DMA CIRCULAR,
|
||||
* 每拍(TIM3 TRGO,1 kHz)自动搬 9 个 → 每 2 拍绕一圈:半传输中断 = 前半 9 个就绪,
|
||||
* 传输完成中断 = 后半 9 个就绪。消费侧永远读"刚完成的那一半",而 DMA 正在写另一半
|
||||
* → 无撕裂、无需临界区。代价:STM32 侧数据固定滞后 1 拍(1 ms),9 路同一延迟,
|
||||
* 对 20 点 DFT 窗是纯传输延迟、不引入通道间偏差。 */
|
||||
#define ADC_INPUT_STM32_DMA_LEN (2U * ADC_INPUT_STM32_CH_NUM)
|
||||
/* 连续"无新帧"达到该拍数才计一次停摆故障(单拍是相位伪影,见 stm32_take 注释) */
|
||||
#define ADC_INPUT_STALL_RUN_MAX 2U
|
||||
|
||||
/* ===== 语义映射表(L1-4)=====
|
||||
* 权威口径:docs/硬件平台/ADC通道映射说明.md(2026-10-09 硬件确认的语义分配)。
|
||||
@@ -49,6 +60,17 @@ static const uint8_t k_stm32_rank_ch[ADC_INPUT_STM32_CH_NUM] = {
|
||||
|
||||
/* STM32 ADC1 最新一帧原始码值,按 ADC1 通道号索引(0..15) */
|
||||
static uint16_t s_stm32_raw[ADC_INPUT_STM32_CH_MAX];
|
||||
/* DMA 目的缓冲(乒乓两半区,按 Rank 序 0..8 / 9..17)。
|
||||
* ⚠ **必须落在 SRAM(0x20000000 段)**:ADC1 挂的是 **DMA2**,而 F4 的 DMA2 不可达
|
||||
* CCM(0x10000000)。实测 .map 里本文件 .bss 在 RW_IRAM1 → 普通 static 即可;
|
||||
* 日后加大缓冲或改链接段时**必须用 .map 复核地址**——放错不报编译错、不报运行错,
|
||||
* 只是数据永远搬不回来。 */
|
||||
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 uint8_t s_stall_run; /* 连续"无新帧"拍数(相位伪影只会是 1 拍)*/
|
||||
/* TPAFE5160 最新一帧原始码值(16 位二进制补码),按驱动 buf 下标索引 */
|
||||
static int16_t s_tpafe_raw[ADC_INPUT_TPAFE_CH_NUM];
|
||||
static uint8_t s_tpafe_valid; /* TPAFE 侧是否已有有效帧 */
|
||||
@@ -57,35 +79,59 @@ static int s_stm32_rc; /* STM32 最近一次扫描状态码 */
|
||||
static uint32_t s_stm32_errs; /* STM32 扫描累计失败次数 */
|
||||
|
||||
/*
|
||||
* 函数功能:STM32 ADC1 顺序扫描一次(L1-3)
|
||||
* 函数功能:消费一帧 STM32 ADC1 采样(L1-3,v2.0 DMA 双缓冲版)
|
||||
* 入口参数:无
|
||||
* 返 回 值:0 - 成功;-1 - 启动失败;-2 - 转换轮询超时
|
||||
* 限定条件:仅任务上下文调用;MX_ADC1_Init() 已完成
|
||||
* 函数说明:软件触发一次 9 通道扫描(ScanConvMode=ENABLE、ContinuousConvMode=
|
||||
* DISABLE),按 Rank 顺序逐通道轮询 EOC 并读 DR,结果按 ADC1 通道号
|
||||
* 存入 s_stm32_raw[]。EOCSelection=SINGLE_CONV 且未用 DMA → 每个通道
|
||||
* 产生一次 EOC,可逐通道轮询(见 HAL_ADC_PollForConversion 说明)。
|
||||
* 返 回 值:0 - 取到新一帧;-2 - 本拍无新帧(TIM3 未启动 / ADC 停摆 / DMA 出错)
|
||||
* 限定条件:仅任务上下文调用;adc_input_init() 已挂好 DMA
|
||||
* 函数说明:本函数**既不触发转换也不等待转换**——转换由 TIM3 TRGO 硬件触发、由 DMA
|
||||
* 自动搬运。每拍只做"看完成计数是否前进 + 拷 9 个半字",故**零忙等**:
|
||||
* 原轮询版常态 adcp=122 µs、超时拍最坏 2~3 ms,现在两档都降到 ~1 µs 量级。
|
||||
* 读的是"上一拍刚完成的那一半"乒乓缓冲(DMA 此刻正在写另一半)→ 无撕裂。
|
||||
* ⚠ 计数未前进 = 本拍 STM32 侧**无有效数据**,必须清 s_stm32_valid,绝不
|
||||
* 沿用上一拍旧值(同 2026-10-10 修的陈旧帧缺陷口径)→ adc_input_raw()
|
||||
* 返 -2 → adc_task 走既有"整帧跳过"路径;下一拍自动恢复。
|
||||
*/
|
||||
static int stm32_scan(void) {
|
||||
uint8_t i;
|
||||
static int stm32_take(void) {
|
||||
uint32_t seq = s_dma_seq; /* 单次 32 位读:与 ISR 自增不产生撕裂 */
|
||||
const uint16_t *src;
|
||||
uint8_t half;
|
||||
uint8_t i;
|
||||
|
||||
if (HAL_ADC_Start(&hadc1) != HAL_OK) {
|
||||
s_stm32_rc = ADC_INPUT_RC_START;
|
||||
s_stm32_errs++;
|
||||
return ADC_INPUT_RC_START;
|
||||
if (seq == s_dma_seq_seen) {
|
||||
s_stm32_valid = 0u;
|
||||
s_stm32_rc = ADC_INPUT_RC_STALL;
|
||||
/* 单拍"无新帧"**多半是消费者相位伪影,不是故障**:本层在 TIM3 触发瞬间被唤醒,
|
||||
* 而该拍的 DMA 完成落在 ~67 us 之后;若上一拍被日志/抢占拖过这 67 us,本拍就会
|
||||
* 看到"计数没前进"(那个完成已被上一拍消费掉)。这种伪影**只会连续 1 拍**——
|
||||
* DMA 若真停,第 2 拍仍不会前进。故只有连续 >= ADC_INPUT_STALL_RUN_MAX 拍
|
||||
* 无新帧才计入故障计数(实测开机日志突发期约 21~26 次伪影、稳态每 10 s 心跳 1 次,
|
||||
* 全部为单拍;见 docs/子系统设计/ADC采样节拍设计说明.md §10.5) */
|
||||
if (s_stall_run < 255u) {
|
||||
s_stall_run++;
|
||||
}
|
||||
if (s_stall_run >= ADC_INPUT_STALL_RUN_MAX) {
|
||||
s_stm32_errs++;
|
||||
}
|
||||
return ADC_INPUT_RC_STALL;
|
||||
}
|
||||
|
||||
if ((uint32_t)(seq - s_dma_seq_seen) > 1u) {
|
||||
/* 计数一次前进 >1 = 中间有帧没被读走(乒乓缓冲已被覆盖)→ **真丢帧**,
|
||||
* 与"无新帧"分开计数:前者是消费者超拍(本任务单拍耗时 > 1 ms),
|
||||
* 后者多半是相位伪影 */
|
||||
s_stm32_gaps++;
|
||||
}
|
||||
s_stall_run = 0u;
|
||||
s_dma_seq_seen = seq;
|
||||
/* 半区下标由 seq 奇偶推出,**不再另读 ISR 变量**:DMA 从 NDTR=18 起、每拍搬 9 个,
|
||||
* 故第 1/3/5... 次完成的是前半区(下标 0)、第 2/4/6... 次是后半区(下标 1)。
|
||||
* 只读 seq 一个量 → 消费侧与 ISR 之间不存在"读到一半被换半区"的窗口。 */
|
||||
half = (uint8_t)(1u - (seq & 1u));
|
||||
src = &s_stm32_dma[((uint32_t)half * ADC_INPUT_STM32_CH_NUM)];
|
||||
|
||||
for (i = 0u; i < ADC_INPUT_STM32_CH_NUM; i++) {
|
||||
if (HAL_ADC_PollForConversion(&hadc1, ADC_INPUT_POLL_TIMEOUT_MS) != HAL_OK) {
|
||||
(void)HAL_ADC_Stop(&hadc1);
|
||||
s_stm32_rc = ADC_INPUT_RC_TIMEOUT;
|
||||
s_stm32_errs++;
|
||||
return ADC_INPUT_RC_TIMEOUT;
|
||||
}
|
||||
s_stm32_raw[k_stm32_rank_ch[i]] = (uint16_t)HAL_ADC_GetValue(&hadc1);
|
||||
s_stm32_raw[k_stm32_rank_ch[i]] = src[i];
|
||||
}
|
||||
|
||||
(void)HAL_ADC_Stop(&hadc1);
|
||||
s_stm32_valid = 1u;
|
||||
s_stm32_rc = ADC_INPUT_RC_OK;
|
||||
return ADC_INPUT_RC_OK;
|
||||
@@ -131,10 +177,11 @@ static int map_check(void) {
|
||||
/*
|
||||
* 函数功能:ADC 输入层初始化
|
||||
* 入口参数:无
|
||||
* 返 回 值:0 - 成功;-1 - 语义映射表自检失败
|
||||
* 返 回 值:0 - 成功;-1 - 语义映射表自检失败;-2 - DMA 挂载失败
|
||||
* 限定条件:内核启动前调用(无 RTOS 依赖);STM32 ADC1 外设已由 MX_ADC1_Init 配好
|
||||
* 函数说明:清空原始码值缓冲与统计,并做一次映射表自洽性自检;本层不重复配置
|
||||
* 外设,也不启动任何转换(STM32 侧为软件触发,按拍扫描)。
|
||||
* 函数说明:清空原始码值缓冲与统计 + 映射表自洽性自检 + **挂载一次 ADC1 DMA**
|
||||
* (此后转换由 TIM3 TRGO 硬件触发、DMA 自动搬运,本层不再触发转换)。
|
||||
* 须在启动采样节拍(app_main.c 步骤 6)之前调用。
|
||||
*/
|
||||
int adc_input_init(void) {
|
||||
uint8_t i;
|
||||
@@ -142,6 +189,9 @@ int adc_input_init(void) {
|
||||
for (i = 0u; i < ADC_INPUT_STM32_CH_MAX; i++) {
|
||||
s_stm32_raw[i] = 0u;
|
||||
}
|
||||
for (i = 0u; i < ADC_INPUT_STM32_DMA_LEN; i++) {
|
||||
s_stm32_dma[i] = 0u;
|
||||
}
|
||||
for (i = 0u; i < ADC_INPUT_TPAFE_CH_NUM; i++) {
|
||||
s_tpafe_raw[i] = 0;
|
||||
}
|
||||
@@ -150,17 +200,39 @@ int adc_input_init(void) {
|
||||
s_stm32_valid = 0u;
|
||||
s_stm32_rc = ADC_INPUT_RC_NOT_RUN;
|
||||
s_stm32_errs = 0u;
|
||||
return map_check();
|
||||
s_dma_seq = 0u;
|
||||
s_dma_seq_seen = 0u;
|
||||
s_stm32_hw_errs = 0u;
|
||||
s_stm32_gaps = 0u;
|
||||
s_stall_run = 0u;
|
||||
|
||||
if (map_check() != 0) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
/* 挂**一次** DMA,之后不再重启:ADC 由 TIM3 TRGO 每拍触发一轮 9 通道转换,
|
||||
* DMA 在 CIRCULAR 模式下自动搬运、硬件自动重载 NDTR → 每 2 拍绕一圈(乒乓)。
|
||||
* 内核启动前调用安全:只碰寄存器/HAL,不创建任何 CMSIS 对象(AGENTS 铁律)。
|
||||
* 此刻 TIM3 尚未启动(app_main.c 步骤 6)→ 不会发生任何转换或搬运。 */
|
||||
if (HAL_ADC_Start_DMA(&hadc1, (uint32_t *)s_stm32_dma,
|
||||
ADC_INPUT_STM32_DMA_LEN) != HAL_OK) {
|
||||
s_stm32_rc = ADC_INPUT_RC_START;
|
||||
s_stm32_errs++;
|
||||
return -2;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:采集一拍(TPAFE 原子快照 + STM32 ADC1 顺序扫描)
|
||||
* 函数功能:采集一拍(TPAFE 原子快照 + 消费一帧 STM32 ADC1 DMA 采样)
|
||||
* 入口参数:无
|
||||
* 返 回 值:TPAFE 快照结果(1=有新帧 0=无新帧 -1=参数错)
|
||||
* 限定条件:仅**任务上下文**调用(TPAFE 快照走临界区)
|
||||
* 函数说明:先取 TPAFE 一帧,再做 STM32 ADC1 扫描;两者相互独立,STM32 扫描
|
||||
* 失败不影响 TPAFE 快照与返回值语义(STM32 侧状态另由
|
||||
* adc_input_stm32_rc() 暴露)。
|
||||
* 函数说明:先取 TPAFE 一帧,再消费 STM32 侧最近一帧 DMA 双缓冲;两者相互独立,
|
||||
* STM32 侧无新帧不影响 TPAFE 快照与返回值语义(其状态另由
|
||||
* adc_input_stm32_rc() / adc_input_stm32_seq() 暴露)。
|
||||
* ⚠ STM32 侧数据比 TPAFE 侧**滞后 1 拍**(乒乓双缓冲的固有延迟,
|
||||
* 9 路一致);按 N-18,V0.1 本就禁止跨转换器做矢量运算。
|
||||
*/
|
||||
int adc_input_poll(void) {
|
||||
int r = tpafe5160_snapshot(s_tpafe_raw, (uint8_t)ADC_INPUT_TPAFE_CH_NUM);
|
||||
@@ -169,7 +241,7 @@ int adc_input_poll(void) {
|
||||
s_tpafe_valid = 1u;
|
||||
}
|
||||
|
||||
(void)stm32_scan();
|
||||
(void)stm32_take();
|
||||
return r;
|
||||
}
|
||||
|
||||
@@ -226,12 +298,94 @@ int adc_input_stm32_rc(void) {
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读取 STM32 ADC1 扫描累计失败次数
|
||||
* 函数功能:读取 STM32 ADC1 停摆故障累计次数
|
||||
* 入口参数:无
|
||||
* 返 回 值:累计失败次数(启动失败或转换超时均计入)
|
||||
* 返 回 值:DMA 挂载失败次数 + "连续 >=2 拍无新帧"的次数
|
||||
* 限定条件:无
|
||||
* 函数说明:单调递增,供心跳日志观测。
|
||||
* 函数说明:单调递增,供心跳日志观测。单拍无新帧(相位伪影)**不计入**,
|
||||
* 丢帧由 adc_input_stm32_gaps() 单独计数。
|
||||
*/
|
||||
uint32_t adc_input_stm32_errs(void) {
|
||||
return s_stm32_errs;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读取 STM32 ADC1 的丢帧次数
|
||||
* 入口参数:无
|
||||
* 返 回 值:DMA 完成计数一次前进 >1 的次数(中间那帧已被乒乓缓冲覆盖)
|
||||
* 限定条件:无
|
||||
* 函数说明:这是"消费者超拍"的直接证据(本任务单拍耗时 > 1 ms,如突发日志突发期);
|
||||
* 与 adc_input_stm32_errs()(节拍源停摆)是两类不同的故障。
|
||||
*/
|
||||
uint32_t adc_input_stm32_gaps(void) {
|
||||
return s_stm32_gaps;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读取 STM32 ADC1 的 DMA 整帧完成计数
|
||||
* 入口参数:无
|
||||
* 返 回 值:DMA 半区完成中断 + 传输完成中断的累计次数(每拍 +1)
|
||||
* 限定条件:无
|
||||
* 函数说明:这是 STM32 侧"节拍是否还在喂数据"的直接观测量:与 TPAFE 的
|
||||
* tpafe5160_frame_seq() 对称。它若停增而 TIM3 仍在跑 → DMA/ADC 侧停摆。
|
||||
*/
|
||||
uint32_t adc_input_stm32_seq(void) {
|
||||
return s_dma_seq;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读取 STM32 ADC1 的 HAL 层错误回调累计次数
|
||||
* 入口参数:无
|
||||
* 返 回 值:HAL_ADC_ErrorCallback 触发次数(DMA 传输错误等)
|
||||
* 限定条件:无
|
||||
* 函数说明:与 adc_input_stm32_errs()(消费侧"无新帧")分开计数,便于区分
|
||||
* "节拍源停了"与"DMA 出错"两类故障。
|
||||
*/
|
||||
uint32_t adc_input_stm32_hw_errs(void) {
|
||||
return s_stm32_hw_errs;
|
||||
}
|
||||
|
||||
/* ===== HAL 回调(ISR 上下文)=====
|
||||
* ⚠ 三个回调都在中断里跑(DMA2_Stream4_IRQn,抢占优先级 6)→ **只允许**写 volatile
|
||||
* 计数/标志,不得调用任何 CMSIS-RTOS API、不得日志、不得拷贝大缓冲。
|
||||
* 搬运与消费全在任务侧(stm32_take())。 */
|
||||
|
||||
/*
|
||||
* 函数功能:DMA 传输完成回调 = 后半区(下标 1)9 个采样就绪
|
||||
* 入口参数:hadc - 触发回调的 ADC 句柄
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅 ISR 上下文;覆盖 HAL 的 __weak 实现(本板只有 ADC1)
|
||||
* 函数说明:见 stm32_take() 的奇偶推半区口径。
|
||||
*/
|
||||
void HAL_ADC_ConvCpltCallback(ADC_HandleTypeDef *hadc) {
|
||||
if (hadc == &hadc1) {
|
||||
s_dma_seq++;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:DMA 半传输完成回调 = 前半区(下标 0)9 个采样就绪
|
||||
* 入口参数:hadc - 触发回调的 ADC 句柄
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅 ISR 上下文;覆盖 HAL 的 __weak 实现
|
||||
* 函数说明:DMA 长度 18 = 2 x 9,故半传输即"一帧就绪",与传输完成交替出现。
|
||||
*/
|
||||
void HAL_ADC_ConvHalfCpltCallback(ADC_HandleTypeDef *hadc) {
|
||||
if (hadc == &hadc1) {
|
||||
s_dma_seq++;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:ADC/DMA 错误回调
|
||||
* 入口参数:hadc - 触发回调的 ADC 句柄
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅 ISR 上下文;覆盖 HAL 的 __weak 实现
|
||||
* 函数说明:只计数。错误后 DMA 可能已停 → 消费侧会看到 s_dma_seq 停增(rc=STALL、
|
||||
* 每拍 skip),并由 IWDG 心跳日志暴露,不在 ISR 里尝试恢复。
|
||||
*/
|
||||
void HAL_ADC_ErrorCallback(ADC_HandleTypeDef *hadc) {
|
||||
if (hadc == &hadc1) {
|
||||
s_stm32_hw_errs++;
|
||||
}
|
||||
}
|
||||
|
||||
+52
-16
@@ -3,7 +3,8 @@
|
||||
* 模块功能:把两片转换器的原始码值按**语义**(UA/UB/UC/UN/IA/IB/IC/IN/温度)统一
|
||||
* 暴露,使上层(UI / 自检 / 保护算法)**按语义取数,不写死下标**:
|
||||
* ① 语义通道映射表(L1-4)——权威口径 docs/硬件平台/ADC通道映射说明.md;
|
||||
* ② STM32 ADC1 9 通道软件触发顺序扫描(L1-3)。
|
||||
* ② STM32 ADC1 9 通道采样(L1-3)——由 **TIM3 TRGO 硬件触发 + DMA 双缓冲
|
||||
* 搬运**(v2.0),本层只消费、不触发、不等待。
|
||||
* V0.1 上 8 路电气量分属两片转换器(硬件缺陷 N-18):UA/UN/IA 走
|
||||
* TPAFE5160(16 位 / ±5 V 双极性),UB/UC/IB/IC/IN 走 STM32 ADC1
|
||||
* (12 位 / 0~3.3 V 单极性)。V0.2 改版(8 路电气量全部接 TPAFE)
|
||||
@@ -13,6 +14,10 @@
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* 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()
|
||||
*/
|
||||
|
||||
#ifndef ADC_INPUT_H
|
||||
@@ -56,29 +61,32 @@ typedef struct {
|
||||
/* STM32 ADC1 顺序扫描通道数(Rank 1..9) */
|
||||
#define ADC_INPUT_STM32_CH_NUM 9U
|
||||
|
||||
/* STM32 ADC1 最近一次扫描状态码 */
|
||||
#define ADC_INPUT_RC_OK (0) /* 扫描成功 */
|
||||
#define ADC_INPUT_RC_START (-1) /* HAL_ADC_Start 失败 */
|
||||
#define ADC_INPUT_RC_TIMEOUT (-2) /* 转换轮询超时 */
|
||||
#define ADC_INPUT_RC_NOT_RUN (-3) /* 尚未扫描过 */
|
||||
/* STM32 ADC1 最近一次取帧状态码(v2.0: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 出错) */
|
||||
#define ADC_INPUT_RC_NOT_RUN (-3) /* 尚未取过帧 */
|
||||
|
||||
/*
|
||||
* 函数功能:ADC 输入层初始化
|
||||
* 入口参数:无
|
||||
* 返 回 值:0 - 成功;-1 - 语义映射表自检失败(映射指向了不会被扫描的通道)
|
||||
* 返 回 值:0 - 成功;-1 - 语义映射表自检失败(映射指向了不会被扫描的通道);
|
||||
* -2 - STM32 ADC1 的 DMA 挂载失败
|
||||
* 限定条件:内核启动前调用(无 RTOS 依赖);STM32 ADC1 外设已由 MX_ADC1_Init 配好
|
||||
* 函数说明:清空原始码值缓冲与统计,并做一次映射表自洽性自检;本层不重复配置外设。
|
||||
* 函数说明:清缓冲与统计 + 映射表自洽性自检 + 挂载一次 ADC1 DMA(此后转换由
|
||||
* TIM3 TRGO 硬件触发、DMA 自动搬运)。须在启动采样节拍之前调用。
|
||||
*/
|
||||
int adc_input_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:采集一拍(TPAFE 原子快照 + STM32 ADC1 顺序扫描)
|
||||
* 函数功能:采集一拍(TPAFE 原子快照 + 消费一帧 STM32 ADC1 DMA 采样)
|
||||
* 入口参数:无
|
||||
* 返 回 值:TPAFE 快照结果(1=有新帧 0=无新帧 -1=参数错);STM32 侧状态另见
|
||||
* adc_input_stm32_rc()
|
||||
* adc_input_stm32_rc() / adc_input_stm32_seq()
|
||||
* 限定条件:仅**任务上下文**调用(TPAFE 快照走临界区)
|
||||
* 函数说明:本函数只搬运原始码值,不做标定/运算。STM32 侧扫描失败不阻断 TPAFE
|
||||
* 快照,也不影响返回值语义。
|
||||
* 函数说明:本函数只搬运原始码值,不做标定/运算;STM32 侧**零忙等**(不触发、
|
||||
* 不等待转换)。STM32 侧无新帧不阻断 TPAFE 快照,也不影响返回值语义。
|
||||
* ⚠ STM32 侧数据滞后 1 拍(乒乓双缓冲固有延迟,9 路一致)。
|
||||
*/
|
||||
int adc_input_poll(void);
|
||||
|
||||
@@ -103,7 +111,7 @@ const adc_channel_desc_t *adc_input_desc(adc_semantic_t ch);
|
||||
int adc_input_raw(adc_semantic_t ch, int32_t *out);
|
||||
|
||||
/*
|
||||
* 函数功能:读取 STM32 ADC1 最近一次扫描状态码
|
||||
* 函数功能:读取 STM32 ADC1 最近一次取帧状态码
|
||||
* 入口参数:无
|
||||
* 返 回 值:ADC_INPUT_RC_xxx
|
||||
* 限定条件:无
|
||||
@@ -112,14 +120,42 @@ int adc_input_raw(adc_semantic_t ch, int32_t *out);
|
||||
int adc_input_stm32_rc(void);
|
||||
|
||||
/*
|
||||
* 函数功能:读取 STM32 ADC1 扫描累计失败次数
|
||||
* 函数功能:读取 STM32 ADC1 停摆故障累计次数
|
||||
* 入口参数:无
|
||||
* 返 回 值:累计失败次数(启动失败或转换超时均计入)
|
||||
* 返 回 值:DMA 挂载失败次数 + "连续 >=2 拍无新帧"的次数
|
||||
* 限定条件:无
|
||||
* 函数说明:单调递增,供心跳日志观测。
|
||||
* 函数说明:单调递增,供心跳日志观测。单拍无新帧(消费者相位伪影)不计入。
|
||||
*/
|
||||
uint32_t adc_input_stm32_errs(void);
|
||||
|
||||
/*
|
||||
* 函数功能:读取 STM32 ADC1 的丢帧次数
|
||||
* 入口参数:无
|
||||
* 返 回 值:DMA 完成计数一次前进 >1 的次数(中间那帧已被覆盖,未及消费)
|
||||
* 限定条件:无
|
||||
* 函数说明:与 errs 分开——gap = 消费者超拍(单拍耗时 > 1 ms),
|
||||
* err = 节拍源/ DMA 停摆。两类故障处置不同,混在一起就查不了。
|
||||
*/
|
||||
uint32_t adc_input_stm32_gaps(void);
|
||||
|
||||
/*
|
||||
* 函数功能:读取 STM32 ADC1 的 DMA 整帧完成计数
|
||||
* 入口参数:无
|
||||
* 返 回 值:DMA 半区/整区完成中断的累计次数(正常时每拍 +1)
|
||||
* 限定条件:无
|
||||
* 函数说明:与 tpafe5160_frame_seq() 对称的 STM32 侧"节拍是否还在喂数据"观测量。
|
||||
*/
|
||||
uint32_t adc_input_stm32_seq(void);
|
||||
|
||||
/*
|
||||
* 函数功能:读取 STM32 ADC1 的 HAL 层错误回调累计次数
|
||||
* 入口参数:无
|
||||
* 返 回 值:HAL_ADC_ErrorCallback 触发次数(DMA 传输错误等)
|
||||
* 限定条件:无
|
||||
* 函数说明:与 adc_input_stm32_errs() 分开计数,用于区分"节拍源停了"与"DMA 出错"。
|
||||
*/
|
||||
uint32_t adc_input_stm32_hw_errs(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,205 @@
|
||||
/*
|
||||
* 模块名称:相量计算层(L2 相量计算)
|
||||
* 模块功能:全周傅氏算法(单频 DFT)——系数表预乘 √2/N,fAmSQ 直接即有效值。
|
||||
* 覆盖 L2-1 系数表 / L2-2 实例 / L2-3 有效值 / L2-6 滑动窗 / L2-7 双区交换。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-10 王建锋 创建:L2-1/2/3/6/7
|
||||
*/
|
||||
|
||||
#include "phasor.h"
|
||||
|
||||
#include <math.h>
|
||||
#include <stddef.h>
|
||||
|
||||
#include "sample_ring.h"
|
||||
|
||||
/* ===== L2-1 全周傅氏系数表 =====
|
||||
* g_fFFTSin20[k] = sin(2πk/N)·(√2/N)
|
||||
* g_fFFTCos20[k] = cos(2πk/N)·(√2/N) N = PHASOR_N = 20
|
||||
* 权威口径:docs/算法/傅氏算法详解.md §2.3("把 √2 藏进系数表")。
|
||||
* 本表为**离线生成**的 float 常量(不在运行期调 sinf/cosf):确定、无 libm 精度依赖。
|
||||
* 主机侧单测 test/host/phasor/ 会**逐项按上式复核本表**(防转录错),
|
||||
* 并以合成正弦做端到端复核(读数误差 ≤ 1%)。
|
||||
* 精确零点(sin(π)=0、cos(π/2)=0)直接写 0.0f,不保留浮点噪声。 */
|
||||
static const float k_fft_sin[PHASOR_N] = {
|
||||
+0.000000000e+00f, +2.185080200e-02f, +4.156269506e-02f, +5.720613897e-02f, +6.724984944e-02f,
|
||||
+7.071068138e-02f, +6.724984944e-02f, +5.720613897e-02f, +4.156269506e-02f, +2.185080200e-02f,
|
||||
+0.000000000e+00f, -2.185080200e-02f, -4.156269506e-02f, -5.720613897e-02f, -6.724984944e-02f,
|
||||
-7.071068138e-02f, -6.724984944e-02f, -5.720613897e-02f, -4.156269506e-02f, -2.185080200e-02f,
|
||||
};
|
||||
|
||||
static const float k_fft_cos[PHASOR_N] = {
|
||||
+7.071068138e-02f, +6.724984944e-02f, +5.720613897e-02f, +4.156269506e-02f, +2.185080200e-02f,
|
||||
+0.000000000e+00f, -2.185080200e-02f, -4.156269506e-02f, -5.720613897e-02f, -6.724984944e-02f,
|
||||
-7.071068138e-02f, -6.724984944e-02f, -5.720613897e-02f, -4.156269506e-02f, -2.185080200e-02f,
|
||||
+0.000000000e+00f, +2.185080200e-02f, +4.156269506e-02f, +5.720613897e-02f, +6.724984944e-02f,
|
||||
};
|
||||
|
||||
/* √2:幅值 = √2 × 有效值(与系数表的 √2 折算互逆) */
|
||||
#define PHASOR_SQRT2 1.4142135623730951f
|
||||
|
||||
/* ===== 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"
|
||||
* 拆为两行(IA 电流 / UA 电压),并补出 C04 的五次谐波轨(L1-9)。
|
||||
* 新增实例只需在表尾加一行,其余代码自动适配。 */
|
||||
static const phasor_inst_t k_inst[] = {
|
||||
{ "C01-IA-F1", ADC_SEM_IA, 1u }, /* A 相电流基波(L3-1 过流 / L3-3 过负荷) */
|
||||
{ "C02-UA-F1", ADC_SEM_UA, 1u }, /* A 相电压基波(L3-4 过压/欠压、L3-5 频率) */
|
||||
{ "C03-UN-F1", ADC_SEM_UN, 1u }, /* 零序电压基波(L3-6 零序过压) */
|
||||
{ "C04-IA-F2", ADC_SEM_IA, 2u }, /* 二次谐波(L3-7 涌流制动) */
|
||||
{ "C05-IA-F5", ADC_SEM_IA, 5u }, /* 五次谐波(L1-9 选线) */
|
||||
};
|
||||
|
||||
#define PHASOR_INST_N (sizeof(k_inst) / sizeof(k_inst[0]))
|
||||
|
||||
/* ===== L2-7 双区(计算区 / 使用区)=====
|
||||
* 本拍结果先写入"计算区",全部实例写完后**一次性**切换 s_use_zone
|
||||
* (单字节 store,Cortex-M 上原子),消费侧读"使用区" → 无锁、无撕裂。
|
||||
* s_use_zone 声明 volatile:阻止编译器把结果写入挪到切换之后
|
||||
* (本平台单核顺序执行,无需额外内存屏障)。
|
||||
* 今日唯一消费者是 adcTask 自身(同拍读写、实际无竞争),本机制为后续
|
||||
* 独立保护任务(L3)预留。 */
|
||||
static phasor_t s_zone[2][PHASOR_INST_N];
|
||||
static volatile uint8_t s_use_zone;
|
||||
static uint8_t s_ready; /* 使用区是否已产出过有效结果 */
|
||||
|
||||
/*
|
||||
* 函数功能:清零一个相量
|
||||
* 入口参数:p - 目标相量
|
||||
* 返 回 值:无
|
||||
* 限定条件:p 非空
|
||||
* 函数说明:静态辅助。
|
||||
*/
|
||||
static void phasor_zero(phasor_t *p) {
|
||||
p->fRe = 0.0f;
|
||||
p->fIm = 0.0f;
|
||||
p->fAm = 0.0f;
|
||||
p->fAmSQ = 0.0f;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:对 n 点窗口算 harm 次相量(L2-1 系数表 + L2-3 有效值)
|
||||
* 入口参数:x - 采样窗(按时间序,旧 → 新)
|
||||
* n - 窗长(必须等于 PHASOR_N)
|
||||
* harm - 谐波次数(>=1)
|
||||
* out - 输出相量
|
||||
* 返 回 值:无
|
||||
* 限定条件:无
|
||||
* 函数说明:纯函数。索引步进 = 谐波次数,故同一张表可算任意次谐波。
|
||||
*/
|
||||
void phasor_calc(const float *x, uint8_t n, uint8_t harm, phasor_t *out) {
|
||||
float re = 0.0f;
|
||||
float im = 0.0f;
|
||||
uint8_t i;
|
||||
|
||||
if ((x == NULL) || (out == NULL) || (n != (uint8_t)PHASOR_N) || (harm == 0u)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (i = 0u; i < n; i++) {
|
||||
uint8_t idx = (uint8_t)(((unsigned int)i * harm) % n);
|
||||
re += x[i] * k_fft_sin[idx];
|
||||
im += x[i] * k_fft_cos[idx];
|
||||
}
|
||||
|
||||
out->fRe = re;
|
||||
out->fIm = im;
|
||||
out->fAmSQ = sqrtf((re * re) + (im * im)); /* 系数已折算 √2/N → 直接即有效值 */
|
||||
out->fAm = out->fAmSQ * PHASOR_SQRT2; /* 幅值 = √2 × 有效值 */
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:相量层初始化
|
||||
* 入口参数:无
|
||||
* 返 回 值:0 - 成功
|
||||
* 限定条件:内核启动前调用
|
||||
* 函数说明:清双区结果与有效标志,并复位采样环缓冲;可重复调用(幂等)。
|
||||
*/
|
||||
int phasor_init(void) {
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0u; i < (uint8_t)PHASOR_INST_N; i++) {
|
||||
phasor_zero(&s_zone[0][i]);
|
||||
phasor_zero(&s_zone[1][i]);
|
||||
}
|
||||
s_use_zone = 0u;
|
||||
s_ready = 0u;
|
||||
sample_ring_init();
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取相量实例数(L2-2)
|
||||
* 入口参数:无
|
||||
* 返 回 值:实例个数
|
||||
* 限定条件:无
|
||||
* 函数说明:供遍历 / 日志打印。
|
||||
*/
|
||||
uint8_t phasor_inst_count(void) {
|
||||
return (uint8_t)PHASOR_INST_N;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取相量实例描述
|
||||
* 入口参数:idx - 实例下标
|
||||
* 返 回 值:指向静态实例表的指针;idx 越界返回 NULL
|
||||
* 限定条件:无
|
||||
* 函数说明:返回静态存储地址,调用方不得修改。
|
||||
*/
|
||||
const phasor_inst_t *phasor_inst(uint8_t idx) {
|
||||
if (idx >= (uint8_t)PHASOR_INST_N) {
|
||||
return NULL;
|
||||
}
|
||||
return &k_inst[idx];
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:重算全部实例 + 双区原子切换(L2-6 / L2-7)
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅任务上下文调用;须先 sample_ring_push_frame() 压入本拍数据
|
||||
* 函数说明:任一实例数据不足一周波 → 本拍整体不产出(不切换使用区),
|
||||
* 避免出现"部分新部分旧"的混合结果。
|
||||
*/
|
||||
void phasor_run(void) {
|
||||
float win[PHASOR_N];
|
||||
uint8_t w = (uint8_t)(1u - s_use_zone); /* 计算区 = 非使用区 */
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0u; i < (uint8_t)PHASOR_INST_N; i++) {
|
||||
if (sample_ring_window(k_inst[i].sem, (uint8_t)PHASOR_N, win) != 0) {
|
||||
return; /* 尚未攒满一周波:本拍不产出 */
|
||||
}
|
||||
phasor_calc(win, (uint8_t)PHASOR_N, k_inst[i].harm, &s_zone[w][i]);
|
||||
}
|
||||
|
||||
s_use_zone = w; /* 原子切换(单字节 store) */
|
||||
s_ready = 1u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读实例结果(使用区,无锁)
|
||||
* 入口参数:idx - 实例下标
|
||||
* out - 输出相量
|
||||
* 返 回 值:0 - 成功;-1 - 参数错;-2 - 尚无有效结果
|
||||
* 限定条件:无
|
||||
* 函数说明:先取一次使用区下标再索引,避免读写期间下标变化。
|
||||
*/
|
||||
int phasor_get(uint8_t idx, phasor_t *out) {
|
||||
uint8_t z;
|
||||
|
||||
if ((out == NULL) || (idx >= (uint8_t)PHASOR_INST_N)) {
|
||||
return -1;
|
||||
}
|
||||
if (s_ready == 0u) {
|
||||
return -2;
|
||||
}
|
||||
z = s_use_zone;
|
||||
*out = s_zone[z][idx];
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
/*
|
||||
* 模块名称:相量计算层(L2 相量计算)
|
||||
* 模块功能:全周傅氏(全周波傅里叶)算法——对一周波 20 点采样做单频 DFT,
|
||||
* 产出基波/谐波相量。系数表**预乘 √2/N**(口径见
|
||||
* `docs/算法/傅氏算法详解.md` §2.3),故 `fAmSQ = √(fRe²+fIm²)`
|
||||
* **直接就是有效值**,无需再除 √2。
|
||||
* 覆盖规划 §3.2:L2-1 系数表 / L2-2 实例 / L2-3 有效值输出 /
|
||||
* L2-6 每 1 ms 滑动窗 / L2-7 双区指针原子交换。
|
||||
* ⚠ 本层**纯算法**:不碰寄存器、不碰任务调度、不自己取数(输入由
|
||||
* `sample_ring` 提供)→ 主机侧可单测(L0.5,落实登记簿 A-4)。
|
||||
* 适用平台:STM32F407ZGTx(Cortex-M4F,硬件 FPU)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-10 王建锋 创建:L2-1/2/3/6/7(基波 + 谐波相量)
|
||||
*/
|
||||
|
||||
#ifndef PHASOR_H
|
||||
#define PHASOR_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "adc_input.h" /* adc_semantic_t:实例按语义通道绑定 */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 每周波采样点数(1000 Hz @ 50 Hz,见 docs/子系统设计/ADC采样节拍设计说明.md) */
|
||||
#define PHASOR_N 20U
|
||||
|
||||
/* 相量结果(**有效值口径**)。字段名沿用参考系统 `{fRe, fIm, fAm, fAmSQ}`
|
||||
* (规划 §10.2 术语表:实部/虚部/幅值/有效值):
|
||||
* fRe - 实部 = a1/√2(有效值相量实部)
|
||||
* fIm - 虚部 = b1/√2(有效值相量虚部)
|
||||
* fAm - 幅值 = √2·√(fRe²+fIm²)(正弦峰值 A1)
|
||||
* fAmSQ - 有效值 = √(fRe²+fIm²)(系数已折算 √2/N,故直接即有效值)
|
||||
* 相位(L2-4)按需由 `atan2(fIm, fRe)` 导出,不常驻本结构。 */
|
||||
typedef struct {
|
||||
float fRe;
|
||||
float fIm;
|
||||
float fAm;
|
||||
float fAmSQ;
|
||||
} phasor_t;
|
||||
|
||||
/* 相量实例(L2-2):**数据驱动**——新增实例只需在实例表尾加一行 */
|
||||
typedef struct {
|
||||
const char *name; /* 实例名(日志 / UI 用) */
|
||||
adc_semantic_t sem; /* 语义通道 */
|
||||
uint8_t harm; /* 谐波次数(1=基波,2=二次,5=五次…) */
|
||||
} phasor_inst_t;
|
||||
|
||||
/*
|
||||
* 函数功能:相量层初始化
|
||||
* 入口参数:无
|
||||
* 返 回 值:0 - 成功
|
||||
* 限定条件:内核启动前调用(无 RTOS 依赖)
|
||||
* 函数说明:清双区结果与有效标志,并复位采样环缓冲;可重复调用(幂等)。
|
||||
*/
|
||||
int phasor_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:取相量实例数(L2-2)
|
||||
* 入口参数:无
|
||||
* 返 回 值:实例个数
|
||||
* 限定条件:无
|
||||
* 函数说明:供遍历 / 日志打印。
|
||||
*/
|
||||
uint8_t phasor_inst_count(void);
|
||||
|
||||
/*
|
||||
* 函数功能:取相量实例描述
|
||||
* 入口参数:idx - 实例下标
|
||||
* 返 回 值:指向静态实例表的指针;idx 越界返回 NULL
|
||||
* 限定条件:无
|
||||
* 函数说明:返回静态存储地址,调用方不得修改。
|
||||
*/
|
||||
const phasor_inst_t *phasor_inst(uint8_t idx);
|
||||
|
||||
/*
|
||||
* 函数功能:重算全部实例并从计算区原子切换到使用区(L2-6 滑动窗 + L2-7 双区交换)
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅任务上下文调用;须先经 `sample_ring_push_frame()` 压入本拍数据
|
||||
* 函数说明:以"当前时刻为终止的前一周波"为窗,**20 点全部重算**(暴力法,
|
||||
* 无累积误差)。**任一实例数据不足一周波则本拍整体不产出**(不切换
|
||||
* 使用区),避免部分新部分旧的混合结果。每拍更新,故相量刷新周期 = 1 ms。
|
||||
*/
|
||||
void phasor_run(void);
|
||||
|
||||
/*
|
||||
* 函数功能:读实例结果(使用区,无锁)
|
||||
* 入口参数:idx - 实例下标
|
||||
* out - 输出相量
|
||||
* 返 回 值:0 - 成功;-1 - 参数错;-2 - 尚无有效结果(未攒满一周波)
|
||||
* 限定条件:无
|
||||
* 函数说明:读的是"使用区",与正在写的"计算区"物理隔离 → 无撕裂、无锁。
|
||||
*/
|
||||
int phasor_get(uint8_t idx, phasor_t *out);
|
||||
|
||||
/*
|
||||
* 函数功能:对 n 点窗口算 harm 次相量(L2-1 系数表 + L2-3 有效值)
|
||||
* 入口参数:x - 采样窗(按时间序,旧 → 新;长度须 = PHASOR_N)
|
||||
* n - 窗长(必须等于 PHASOR_N)
|
||||
* harm - 谐波次数(>=1)
|
||||
* out - 输出相量
|
||||
* 返 回 值:无(参数非法时 out 保持不动)
|
||||
* 限定条件:无
|
||||
* 函数说明:**纯函数**(无状态),可脱离采样环独立调用,便于单测与复用。
|
||||
* 索引步进 = 谐波次数即可取任意次谐波(sin 以 2π 为周期,
|
||||
* 故 `sin(2π·((n·h) mod N)/N) ≡ sin(2π·n·h/N)`)。
|
||||
*/
|
||||
void phasor_calc(const float *x, uint8_t n, uint8_t harm, phasor_t *out);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* PHASOR_H */
|
||||
@@ -0,0 +1,243 @@
|
||||
/*
|
||||
* 模块名称:保护元件框架(L3,P1 子集)——实现
|
||||
* 模块功能:见 prot_elm.h。判据核 prot_elm_eval() 为纯逻辑(prot_set RAM +
|
||||
* 测量数组 → 状态机),prot_elm_run() 是 phasor→测量的薄适配层。
|
||||
*
|
||||
* 创建日期:2026-10-11
|
||||
*/
|
||||
#include "prot_elm.h"
|
||||
#include "prot_set.h"
|
||||
#include "phasor.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* 实例配置(ROM 常量表,数据驱动——新增元件只加一行,同 phasor k_inst 模式) */
|
||||
typedef struct {
|
||||
const char *name; /* 日志名 */
|
||||
uint8_t over; /* 1 = 过量(越上限动作),0 = 欠量(越下限动作) */
|
||||
prot_set_id_t en_id; /* 投退定值 */
|
||||
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"…),run() 适配层取数用 */
|
||||
} pe_cfg_t;
|
||||
|
||||
static const pe_cfg_t k_pe[PE_COUNT] = {
|
||||
{ "OC1-IA", 1u, PS_OC1_EN, PS_OC1_CUR, PS_OC1_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OC2-IA", 1u, PS_OC2_EN, PS_OC2_CUR, PS_OC2_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OC3-IA", 1u, PS_OC3_EN, PS_OC3_CUR, PS_OC3_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OL-IA", 1u, PS_OL_EN, PS_OL_CUR, PS_OL_DLY, PS_RET_OVER, "C01" },
|
||||
{ "OV-UA", 1u, PS_OV_EN, PS_OV_VOL, PS_OV_DLY, PS_RET_OVER, "C02" },
|
||||
{ "UV-UA", 0u, PS_UV_EN, PS_UV_VOL, PS_UV_DLY, PS_RET_UNDER, "C02" },
|
||||
{ "3U0-UN", 1u, PS_3U0_EN, PS_3U0_VOL, PS_3U0_DLY, PS_RET_OVER, "C03" },
|
||||
};
|
||||
|
||||
/* 运行时状态(picked/asserted 的对外快照 + 延时起点 + 相量实例索引缓存) */
|
||||
typedef struct {
|
||||
uint8_t enabled;
|
||||
uint8_t picked;
|
||||
uint8_t asserted;
|
||||
uint32_t t0; /* picked 起始时基(picked=0 时无意义) */
|
||||
uint8_t pidx; /* 相量实例索引;0xFF = 未解析/未找到 */
|
||||
} pe_rt_t;
|
||||
|
||||
static pe_rt_t s_rt[PE_COUNT];
|
||||
|
||||
/* 跨上下文复归请求(单字标志,volatile;B5:104 YK11 在 iec104Task 落请求) */
|
||||
static volatile uint8_t s_reset_req;
|
||||
|
||||
/*
|
||||
* 函数功能:按名前缀在 phasor 实例表里找索引(开机解析一次)
|
||||
* 入口参数:prefix - 相量实例名前缀(如 "C01")
|
||||
* 返 回 值:实例索引;0xFF = 未找到(run() 对该元件整拍跳过)
|
||||
*/
|
||||
static uint8_t pe_find_phasor(const char *prefix) {
|
||||
uint8_t i, n = phasor_inst_count();
|
||||
|
||||
for (i = 0u; i < n; i++) {
|
||||
const phasor_inst_t *pi = phasor_inst(i);
|
||||
uint8_t j = 0u, hit = 1u;
|
||||
|
||||
if (pi == 0) {
|
||||
continue;
|
||||
}
|
||||
while ((prefix[j] != '\0') && (pi->name[j] != '\0')) {
|
||||
if (prefix[j] != pi->name[j]) {
|
||||
hit = 0u;
|
||||
break;
|
||||
}
|
||||
j++;
|
||||
}
|
||||
if ((hit != 0u) && (prefix[j] == '\0')) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0xFFu;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:复位全部元件状态并解析相量索引(定值不动;幂等)
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 限定条件:须在 phasor_init() 之后调用(否则实例表名查不到 → 全 0xFF)
|
||||
*/
|
||||
void prot_elm_init(void) {
|
||||
uint32_t i;
|
||||
|
||||
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
|
||||
s_rt[i].enabled = 0u;
|
||||
s_rt[i].picked = 0u;
|
||||
s_rt[i].asserted = 0u;
|
||||
s_rt[i].t0 = 0u;
|
||||
s_rt[i].pidx = pe_find_phasor(k_pe[i].pprefix);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:登记"复归元件状态"请求(任意上下文安全,见 prot_elm.h)
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 函数说明:只置单字标志;真正的状态复位在下一拍 eval 头部(消费者上下文)执行。
|
||||
*/
|
||||
void prot_elm_request_reset(void) {
|
||||
s_reset_req = 1u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:纯判据步进——按当前定值 + 给定测量值推进所有元件
|
||||
* 入口参数:now_ms - 单调时基(tick ms,差值法回绕安全)
|
||||
* meas - PE_COUNT 个测量值(工程量 A/V);NULL 视为全 0
|
||||
* 返 回 值:无
|
||||
* 函数说明:投退=0 或定值闭锁(L5-1)→ 状态整体复位、不判据;
|
||||
* 滞回 = 已启动用返回门槛(定值×系数)、未启动用动作门槛;
|
||||
* 延时 dly=0 → 启动同拍即动作;返回即时(P4 接出口时再补返回延时)。
|
||||
*/
|
||||
void prot_elm_eval(uint32_t now_ms, const float meas[PE_COUNT]) {
|
||||
uint8_t locked = prot_set_locked();
|
||||
uint32_t i;
|
||||
|
||||
/* 跨上下文复归请求在此消费(YK11 经 request_reset,见 prot_elm.h) */
|
||||
if (s_reset_req != 0u) {
|
||||
s_reset_req = 0u;
|
||||
prot_elm_init();
|
||||
locked = prot_set_locked();
|
||||
}
|
||||
|
||||
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
|
||||
const pe_cfg_t *cfg = &k_pe[i];
|
||||
uint8_t cond;
|
||||
float m, set, ret;
|
||||
|
||||
s_rt[i].enabled = ((locked == 0u) && (prot_set_get_ctl(cfg->en_id) != 0u)) ? 1u : 0u;
|
||||
if (s_rt[i].enabled == 0u) {
|
||||
/* 退出/闭锁:状态整体复位(参考元件闭锁分支的 Rly/Timer 复位) */
|
||||
s_rt[i].picked = 0u;
|
||||
s_rt[i].asserted = 0u;
|
||||
s_rt[i].t0 = 0u;
|
||||
continue;
|
||||
}
|
||||
|
||||
m = (meas != 0) ? meas[i] : 0.0f;
|
||||
set = prot_set_get_f(cfg->set_id);
|
||||
ret = set * prot_set_get_f(cfg->ret_id);
|
||||
|
||||
/* 滞回:已启动用返回门槛,未启动用动作门槛(参考 M_GRly_OverRelay) */
|
||||
if (cfg->over != 0u) {
|
||||
cond = (s_rt[i].picked != 0u) ? (uint8_t)(m > ret) : (uint8_t)(m > set);
|
||||
} else {
|
||||
cond = (s_rt[i].picked != 0u) ? (uint8_t)(m < ret) : (uint8_t)(m < set);
|
||||
}
|
||||
|
||||
if (cond != 0u) {
|
||||
float dly;
|
||||
|
||||
if (s_rt[i].picked == 0u) {
|
||||
s_rt[i].picked = 1u;
|
||||
s_rt[i].t0 = now_ms;
|
||||
}
|
||||
dly = prot_set_get_f(cfg->dly_id);
|
||||
if (dly < 0.0f) {
|
||||
dly = 0.0f;
|
||||
}
|
||||
/* 差值法比较,tick 回绕安全;dly=0 → 启动同拍即动作 */
|
||||
if ((uint32_t)(int32_t)(now_ms - s_rt[i].t0) >= (uint32_t)dly) {
|
||||
s_rt[i].asserted = 1u;
|
||||
}
|
||||
} else {
|
||||
s_rt[i].picked = 0u;
|
||||
s_rt[i].asserted = 0u;
|
||||
s_rt[i].t0 = 0u;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:适配层——从 phasor 使用区取 rms 组装测量数组后调判据核
|
||||
* 入口参数:now_ms - 单调时基(tick ms)
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅保护消费者任务上下文;工程量↔码值换算 = 1(L1-5 标定未实现)
|
||||
* 函数说明:任一相量未就绪(未满一周波)→ **整拍跳过**,不引入陈旧/半新测量。
|
||||
*/
|
||||
void prot_elm_run(uint32_t now_ms) {
|
||||
float meas[PE_COUNT];
|
||||
phasor_t p;
|
||||
uint32_t i;
|
||||
|
||||
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
|
||||
if ((s_rt[i].pidx == 0xFFu) || (phasor_get(s_rt[i].pidx, &p) != 0)) {
|
||||
return;
|
||||
}
|
||||
meas[i] = p.fAmSQ;
|
||||
}
|
||||
prot_elm_eval(now_ms, meas);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读单元件状态快照(拷贝语义)
|
||||
* 入口参数:id - 元件编号;out - 快照输出(NULL 直接返回)
|
||||
* 返 回 值:无;越界编号输出全零
|
||||
*/
|
||||
void prot_elm_state(prot_elm_id_t id, prot_elm_state_t *out) {
|
||||
if (out == 0) {
|
||||
return;
|
||||
}
|
||||
out->enabled = 0u;
|
||||
out->picked = 0u;
|
||||
out->asserted = 0u;
|
||||
out->pick_ms = 0u;
|
||||
if (id < PE_COUNT) {
|
||||
out->enabled = s_rt[(uint32_t)id].enabled;
|
||||
out->picked = s_rt[(uint32_t)id].picked;
|
||||
out->asserted = s_rt[(uint32_t)id].asserted;
|
||||
out->pick_ms = s_rt[(uint32_t)id].t0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:聚合动作位图
|
||||
* 入口参数:无
|
||||
* 返 回 值:bit i = 元件 i asserted(供 L4 出口 / 心跳日志)
|
||||
*/
|
||||
uint32_t prot_elm_active_mask(void) {
|
||||
uint32_t mask = 0u;
|
||||
uint32_t i;
|
||||
|
||||
for (i = 0u; i < (uint32_t)PE_COUNT; i++) {
|
||||
if (s_rt[i].asserted != 0u) {
|
||||
mask |= (1uL << i);
|
||||
}
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取实例名(日志用)
|
||||
* 入口参数:id - 元件编号
|
||||
* 返 回 值:ROM 常量名;越界编号返回 "PE??"
|
||||
*/
|
||||
const char *prot_elm_name(prot_elm_id_t id) {
|
||||
if (id >= PE_COUNT) {
|
||||
return "PE??";
|
||||
}
|
||||
return k_pe[(uint32_t)id].name;
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 模块名称:保护元件框架(L3,P1 子集)
|
||||
* 模块功能:通用"越限 + 返回系数滞回 + 定时限"元件核 + 实例表(数据驱动,
|
||||
* 同 phasor 实例表模式)。判据为**纯逻辑**(吃测量值 + 定值),
|
||||
* 主机侧可离线单测;出口/SOE(L4)与任务编排为后续。
|
||||
*
|
||||
* V0.1 有效实例(N-18:仅 TPAFE 三路):
|
||||
* OC1/OC2/OC3 三段式相间过流(A 相 IA,L3-1)
|
||||
* OL 过负荷(A 相 IA,L3-3)
|
||||
* OV/UV 过压/欠压(UA,L3-4)
|
||||
* 3U0 零序过压(UN 外部零序电压通道,L3-6)
|
||||
* ⚠ 过频/低频(L3-5)随频率测量算法(P2)落地,本表暂不含。
|
||||
*
|
||||
* 与参考系统的口径差异(有意简化,均已在规划文档登记):
|
||||
* - 返回门槛 = 定值 × 返回系数(参考经 CN_SET_RET 独立定值,等价);
|
||||
* - 返回**即时**(参考 GRly_TimeRelay 带 10 ms 返回延时——V0.1 无出口硬件,
|
||||
* 抖动只影响日志,P4 接出口时再补返回延时);
|
||||
* - 复压/方向/无压无流闭锁(dwSetFyCtrl 类)随 P2/P3 元件扩展;
|
||||
* - 工程量↔码值换算系数暂为 1(L1-5 标定未实现,同 phasor 层口径)。
|
||||
*
|
||||
* 线程约定:init 内核启动前或保护消费者任务上下文;eval/run 仅保护消费者任务
|
||||
* 上下文(B5 起 = protectTask);request_reset 任意上下文(跨上下文复归请求,
|
||||
* 如 104 遥控 YK11 在 iec104Task 落请求、下一拍 eval 头部消费)。板端接线
|
||||
* 走栈预算 + IWDG 心跳门禁。
|
||||
*
|
||||
* 创建日期:2026-10-11
|
||||
*/
|
||||
#ifndef PROT_ELM_H
|
||||
#define PROT_ELM_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* 元件实例编号(枚举序 = 实例表序 = 测量数组序) */
|
||||
typedef enum {
|
||||
PE_OC1 = 0, /* 过流 Ⅰ 段(IA) */
|
||||
PE_OC2, /* 过流 Ⅱ 段(IA) */
|
||||
PE_OC3, /* 过流 Ⅲ 段(IA) */
|
||||
PE_OL, /* 过负荷(IA) */
|
||||
PE_OV, /* 过压(UA) */
|
||||
PE_UV, /* 欠压(UA,欠量方向) */
|
||||
PE_3U0, /* 零序过压(UN) */
|
||||
PE_COUNT
|
||||
} prot_elm_id_t;
|
||||
|
||||
/* 单元件状态快照(读侧拷贝语义,见 prot_elm_state) */
|
||||
typedef struct {
|
||||
uint8_t enabled; /* 本拍投入(定值投退 && 未闭锁) */
|
||||
uint8_t picked; /* 越限启动(滞回后电平) */
|
||||
uint8_t asserted; /* 定时限到点(动作) */
|
||||
uint32_t pick_ms; /* 本次启动起始时基(0 = 未启动过;asserted 延时观测用) */
|
||||
} prot_elm_state_t;
|
||||
|
||||
/*
|
||||
* 函数功能:复位全部元件状态(定值不动;幂等,内核启动前可调)
|
||||
* 限定条件:内核启动前,或保护消费者任务上下文(跨上下文请用 request_reset)
|
||||
*/
|
||||
void prot_elm_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:登记"复归元件状态"请求(任意上下文安全,单字标志)
|
||||
* 函数说明:下一拍 prot_elm_eval() 头部消费并执行 prot_elm_init()。
|
||||
* 104 遥控 YK11(iec104Task 上下文)必须走本接口,不得直接调
|
||||
* prot_elm_init()(会与 protectTask 的 eval 竞态)。
|
||||
*/
|
||||
void prot_elm_request_reset(void);
|
||||
|
||||
/*
|
||||
* 函数功能:纯判据步进——按当前定值(prot_set RAM)+ 给定测量值推进所有元件
|
||||
* 入口参数:now_ms - 单调时基(tick ms,回绕安全:差值法)
|
||||
* meas - PE_COUNT 个测量值(工程量:A / V),NULL 视为全 0
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅保护消费者任务上下文;调用方保证定值已装载
|
||||
* 函数说明:越限方向由实例表决定;投退=0 或定值闭锁(L5-1)→ 状态复位、
|
||||
* 不判据。延时判定用 (uint32_t)(int32_t)(now - t0) >= dly,回绕安全。
|
||||
*/
|
||||
void prot_elm_eval(uint32_t now_ms, const float meas[PE_COUNT]);
|
||||
|
||||
/*
|
||||
* 函数功能:适配层——从 phasor 使用区取 rms 组装测量数组后调 prot_elm_eval
|
||||
* 入口参数:now_ms - 同上
|
||||
* 说明:phasor 未就绪(-2,未满一周波)的实例本拍按"测量=上一拍值"处理会
|
||||
* 引入陈旧判据 → 直接**整拍跳过**(不 eval),与 adc_task 跳拍同口径。
|
||||
*/
|
||||
void prot_elm_run(uint32_t now_ms);
|
||||
|
||||
/* 状态快照拷贝(NULL 安全;越界全零) */
|
||||
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);
|
||||
|
||||
/* 实例名(ROM 常量,日志用) */
|
||||
const char *prot_elm_name(prot_elm_id_t id);
|
||||
|
||||
#endif /* PROT_ELM_H */
|
||||
@@ -0,0 +1,437 @@
|
||||
/*
|
||||
* 模块名称:保护定值模型(P1,L5-1 载体)——实现
|
||||
* 模块功能:见 prot_set.h。纯逻辑,零平台依赖(仅 stdint.h + 自身头)。
|
||||
*
|
||||
* ⚠ 默认值/范围为拟定值(参考 EEPROM 出厂默认表不在本仓库,规划 §10.3 待补);
|
||||
* 通讯地址逐项对照参考 Comm/Pub/Const_CommSetPara.c g_tCommSetParaTab。
|
||||
*
|
||||
* 创建日期:2026-10-11
|
||||
*/
|
||||
#include "prot_set.h"
|
||||
|
||||
#include <string.h> /* memcpy(set_sealed 影子区拷贝,B7) */
|
||||
|
||||
/* 元数据表:顺序必须与 prot_set_id_t 严格一致(逐行带地址注释,同 phasor
|
||||
* k_inst 的朴素行式风格) */
|
||||
const prot_set_meta_t g_prot_set_meta[PS_ID_COUNT] = {
|
||||
/* addr ctl default min max 说明 */
|
||||
{ 0x8856, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) }, /* OC1 投退 */
|
||||
{ 0x8857, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) }, /* OC1 跳闸投退 */
|
||||
{ 0x8858, 0u, PROT_SET_Q10(10.0), 0, PROT_SET_Q10(100.0) }, /* OC1 电流 A */
|
||||
{ 0x8859, 0u, PROT_SET_Q10(0.0), 0, PROT_SET_Q10(60000) }, /* OC1 延时 ms */
|
||||
{ 0x885A, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) },
|
||||
{ 0x885B, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) },
|
||||
{ 0x885C, 0u, PROT_SET_Q10(5.0), 0, PROT_SET_Q10(100.0) },
|
||||
{ 0x885D, 0u, PROT_SET_Q10(500.0), 0, PROT_SET_Q10(60000) },
|
||||
{ 0x885E, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) },
|
||||
{ 0x885F, 1u, PROT_SET_Q10(0), 0, PROT_SET_Q10(1) }, /* OC3 默认告警 */
|
||||
{ 0x8860, 0u, PROT_SET_Q10(2.0), 0, PROT_SET_Q10(100.0) },
|
||||
{ 0x8861, 0u, PROT_SET_Q10(3000.0), 0, PROT_SET_Q10(60000) },
|
||||
{ 0x8871, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) }, /* 过负荷投退 */
|
||||
{ 0x8872, 1u, PROT_SET_Q10(0), 0, PROT_SET_Q10(1) }, /* 默认告警 */
|
||||
{ 0x8873, 0u, PROT_SET_Q10(1.0), 0, PROT_SET_Q10(100.0) },
|
||||
{ 0x8874, 0u, PROT_SET_Q10(10000.0),0, PROT_SET_Q10(60000) },
|
||||
{ 0x0000, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) }, /* OV 投退(本地) */
|
||||
{ 0x8029, 0u, PROT_SET_Q10(120.0), 0, PROT_SET_Q10(200.0) }, /* 过压 V */
|
||||
{ 0x802A, 0u, PROT_SET_Q10(10000.0),0, PROT_SET_Q10(60000) },
|
||||
{ 0x0000, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) }, /* UV 投退(本地) */
|
||||
{ 0x8027, 0u, PROT_SET_Q10(20.0), 0, PROT_SET_Q10(200.0) }, /* 欠压 V */
|
||||
{ 0x8028, 0u, PROT_SET_Q10(10000.0),0, PROT_SET_Q10(60000) },
|
||||
{ 0x888E, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) }, /* 3U0 投退 */
|
||||
{ 0x888F, 0u, PROT_SET_Q10(15.0), 0, PROT_SET_Q10(200.0) }, /* 3U0 V */
|
||||
{ 0x8890, 0u, PROT_SET_Q10(5000.0), 0, PROT_SET_Q10(60000) },
|
||||
{ 0x0000, 0u, PROT_SET_Q10(0.95), PROT_SET_Q10(0.5), PROT_SET_Q10(1.0) }, /* 过量返回系数 */
|
||||
{ 0x0000, 0u, PROT_SET_Q10(1.05), PROT_SET_Q10(1.0), PROT_SET_Q10(1.5) }, /* 欠量返回系数 */
|
||||
{ 0x8820, 0u, PROT_SET_Q10(10000.0),PROT_SET_Q10(100.0), PROT_SET_Q10(500000.0) },
|
||||
{ 0x8821, 0u, PROT_SET_Q10(100.0), PROT_SET_Q10(1.0), PROT_SET_Q10(200.0) },
|
||||
{ 0x8825, 0u, PROT_SET_Q10(100.0), PROT_SET_Q10(1.0), PROT_SET_Q10(10000.0) },
|
||||
{ 0x8826, 0u, PROT_SET_Q10(5.0), PROT_SET_Q10(1.0), PROT_SET_Q10(10.0) },
|
||||
{ 0x0000, 1u, PROT_SET_Q10(1), 0, PROT_SET_Q10(1) }, /* 全局软压板投退(YK12,B7) */
|
||||
};
|
||||
|
||||
static uint32_t s_val[2][PS_ID_COUNT]; /* 双缓冲值区(uint32 承载 int32,校验和按字覆盖)
|
||||
* B7:写者改影子区后单字交换 s_act,
|
||||
* 读侧(protectTask selfcheck/get)永远看到一致快照 */
|
||||
static volatile uint8_t s_act; /* 活跃区下标(0/1);交换 = 单字存储 */
|
||||
static uint32_t s_sum; /* 封存的正码 */
|
||||
static uint32_t s_sumn; /* 封存的反码 */
|
||||
static uint32_t s_bad_mask; /* load_raw/parse 越界位图 */
|
||||
static uint8_t s_locked; /* 定值非法 → 闭锁(L5-1) */
|
||||
static uint8_t s_dirty; /* 未落盘定值改动(B7 持久化脏标志) */
|
||||
|
||||
/*
|
||||
* 函数功能:sum32 覆盖指定值区(参考 Fun_CalSum32 同款:32 位字累加自然回绕)
|
||||
* 入口参数:vals - PS_ID_COUNT 个字的值区
|
||||
* 返 回 值:校验和正码
|
||||
*/
|
||||
static uint32_t prot_sum_buf(const uint32_t *vals) {
|
||||
uint32_t sum = 0u;
|
||||
uint32_t i;
|
||||
|
||||
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
|
||||
sum += vals[i];
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:把元数据表默认值刷进 RAM 值区
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
*/
|
||||
static void prot_load_defaults(void) {
|
||||
uint32_t i;
|
||||
|
||||
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
|
||||
s_val[s_act][i] = (uint32_t)g_prot_set_meta[i].def;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:装载出厂默认值并封存校验和(幂等)
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
*/
|
||||
void prot_set_init(void) {
|
||||
prot_load_defaults();
|
||||
s_bad_mask = 0u;
|
||||
s_locked = 0u;
|
||||
s_dirty = 0u;
|
||||
prot_set_seal();
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取最近一次 load_raw 的越界位图
|
||||
* 入口参数:无
|
||||
* 返 回 值:bit i = 第 i 项越界(load_raw 失败后有效)
|
||||
*/
|
||||
uint32_t prot_set_bad_mask(void) {
|
||||
return s_bad_mask;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:整表装载外部值(P4 起由 cfg_file 定值段调用),逐项范围校验
|
||||
* 入口参数:vals - PS_ID_COUNT 个 Q10 值
|
||||
* 返 回 值:0 全部合法(已装载并封存)/ -1 存在越界项或空指针(整表不装载)
|
||||
* 函数说明:all-or-nothing——宁可全旧不可半新(定值一致性优先);
|
||||
* 越界项经 prot_set_bad_mask() 指认。
|
||||
*/
|
||||
int prot_set_load_raw(const int32_t *vals) {
|
||||
uint32_t mask = 0u;
|
||||
uint32_t cur_act;
|
||||
uint32_t *shadow;
|
||||
uint32_t i;
|
||||
|
||||
if (vals == 0) {
|
||||
return -1;
|
||||
}
|
||||
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
|
||||
if ((vals[i] < g_prot_set_meta[i].min) || (vals[i] > g_prot_set_meta[i].max)) {
|
||||
mask |= (1uL << i);
|
||||
}
|
||||
}
|
||||
s_bad_mask = mask;
|
||||
if (mask != 0u) {
|
||||
return -1;
|
||||
}
|
||||
/* 原子装载(B7):写影子区 → 封存 → 单字交换;读侧无中间态 */
|
||||
cur_act = s_act;
|
||||
shadow = s_val[cur_act ^ 1u];
|
||||
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
|
||||
shadow[i] = (uint32_t)vals[i];
|
||||
}
|
||||
s_sum = prot_sum_buf(shadow);
|
||||
s_sumn = ~s_sum;
|
||||
s_locked = 0u;
|
||||
s_act = (uint8_t)(cur_act ^ 1u);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:单项写入(范围校验)
|
||||
* 入口参数:id - 定值项编号;q10 - Q10 定点值
|
||||
* 返 回 值:0 / -1 越界或非法编号拒写(原值不动)
|
||||
* 函数说明:**不自动重封校验和**——批量改完必须显式 prot_set_seal(),
|
||||
* 否则 selfcheck 判"定值被非法改写"(参考系统改定值走重写整表)。
|
||||
*/
|
||||
int prot_set_set(prot_set_id_t id, int32_t q10) {
|
||||
if (id >= PS_ID_COUNT) {
|
||||
return -1;
|
||||
}
|
||||
if ((q10 < g_prot_set_meta[id].min) || (q10 > g_prot_set_meta[id].max)) {
|
||||
return -1;
|
||||
}
|
||||
s_val[s_act][(uint32_t)id] = (uint32_t)q10;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:单项**原子**写入+封存(B7,收口 B5 竞态窗口)
|
||||
* 入口参数:id - 定值项编号;q10 - Q10 定点值
|
||||
* 返 回 值:0 / -1 越界或非法编号拒写(原值不动)
|
||||
* 函数说明:影子区拷贝 → 改影子 → 按影子重封校验和 → 单字交换活跃区。
|
||||
* selfcheck 任何时刻看到的都是"值区与封存一致"的完整快照
|
||||
* (区别于 prot_set_set+seal 批处理路径;两路径勿在一次批处理中交错)。
|
||||
* 置脏标志 → 104 任务通知 defaultTask(配置引擎属主)落盘。
|
||||
*/
|
||||
int prot_set_set_sealed(prot_set_id_t id, int32_t q10) {
|
||||
uint32_t cur_act;
|
||||
uint32_t *shadow;
|
||||
|
||||
if (id >= PS_ID_COUNT) {
|
||||
return -1;
|
||||
}
|
||||
if ((q10 < g_prot_set_meta[id].min) || (q10 > g_prot_set_meta[id].max)) {
|
||||
return -1;
|
||||
}
|
||||
cur_act = s_act;
|
||||
shadow = s_val[cur_act ^ 1u];
|
||||
(void)memcpy(shadow, s_val[cur_act], sizeof(s_val[0]));
|
||||
shadow[(uint32_t)id] = (uint32_t)q10;
|
||||
s_sum = prot_sum_buf(shadow);
|
||||
s_sumn = ~s_sum;
|
||||
s_locked = 0u;
|
||||
s_act = (uint8_t)(cur_act ^ 1u); /* 单字交换:读侧原子切换 */
|
||||
s_dirty = 1u;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:脏标志三件套(B7 持久化握手)
|
||||
* 入口参数:无
|
||||
* 返 回 值:peek/take = 1 存在未落盘改动 / 0 干净;mark 无返回
|
||||
* 函数说明:peek 非消费(iec104Task 每轮通知,直到落盘成功);
|
||||
* take 消费(defaultTask 落盘前);mark 写盘失败回置(等下轮重试)。
|
||||
* 单字节标志,多读者安全;写者只有 set_sealed(置)与属主任务(清/置)。
|
||||
*/
|
||||
uint8_t prot_set_dirty_peek(void) {
|
||||
return s_dirty;
|
||||
}
|
||||
|
||||
uint8_t prot_set_dirty_take(void) {
|
||||
uint8_t d = s_dirty;
|
||||
|
||||
s_dirty = 0u;
|
||||
return d;
|
||||
}
|
||||
|
||||
void prot_set_dirty_mark(void) {
|
||||
s_dirty = 1u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读定值 Q10 原值
|
||||
* 入口参数:id - 定值项编号
|
||||
* 返 回 值:Q10 值;非法编号返回 0
|
||||
*/
|
||||
int32_t prot_set_get(prot_set_id_t id) {
|
||||
if (id >= PS_ID_COUNT) {
|
||||
return 0;
|
||||
}
|
||||
return (int32_t)s_val[s_act][(uint32_t)id];
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读定值工程量浮点值
|
||||
* 入口参数:id - 定值项编号
|
||||
* 返 回 值:Q10 → float;非法编号返回 0.0
|
||||
*/
|
||||
float prot_set_get_f(prot_set_id_t id) {
|
||||
return PROT_SET_FROM_Q10(prot_set_get(id));
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读投退/枚举项
|
||||
* 入口参数:id - 定值项编号
|
||||
* 返 回 值:!=0 视为投入(1/0)
|
||||
*/
|
||||
uint8_t prot_set_get_ctl(prot_set_id_t id) {
|
||||
return (prot_set_get(id) != 0) ? 1u : 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:实时计算校验和(不读封存值)
|
||||
* 入口参数:无
|
||||
* 返 回 值:sum32 正码
|
||||
*/
|
||||
uint32_t prot_set_checksum(void) {
|
||||
return prot_sum_buf(s_val[s_act]);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:封存当前值区的正/反码对(合法改定值闭环的最后一步)
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 函数说明:封存同时解除闭锁——selfcheck 失配后的唯一合法解锁路径。
|
||||
* ⚠ 批处理路径(prot_set_set 多次 + 本函数收尾)在 set 与 seal
|
||||
* 之间存在"已改值未封存"窗口,selfcheck 会如实判失配(防篡改语义);
|
||||
* 板端单项写一律走 prot_set_set_sealed(原子,无此窗口)。
|
||||
*/
|
||||
void prot_set_seal(void) {
|
||||
s_sum = prot_sum_buf(s_val[s_act]);
|
||||
s_sumn = ~s_sum;
|
||||
s_locked = 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:定值自检(L5-1)——重算 sum32 并与封存值比对
|
||||
* 入口参数:无
|
||||
* 返 回 值:0 通过 / -1 失配(已置闭锁)
|
||||
* 函数说明:失配 → s_locked 置位并保持,直到下一次 seal;
|
||||
* 元件层每拍读 prot_set_locked() 决定整体复位(联动闭锁)。
|
||||
* B7 双缓冲 + seqlock 式复核:先读封存和、再读值区、再读封存和,
|
||||
* 两次不同 = 检查期间发生了原子提交 → 本轮不作数(下拍再查),
|
||||
* 杜绝"提交瞬间被巡检撞见"的误锁(收口 B5 竞态窗口)。
|
||||
*/
|
||||
int prot_set_selfcheck(void) {
|
||||
uint32_t sum_a = s_sum;
|
||||
uint32_t calc = prot_sum_buf(s_val[s_act]);
|
||||
uint32_t sum_b = s_sum;
|
||||
|
||||
if (sum_a != sum_b) {
|
||||
/* 检查期间有 set_sealed/load_raw 提交:快照不一致,本轮跳过 */
|
||||
return (s_locked != 0u) ? -1 : 0;
|
||||
}
|
||||
if ((calc != sum_a) || (sum_a != ~s_sumn)) {
|
||||
s_locked = 1u; /* 值区与封存不符:定值被非法改写 → 闭锁 */
|
||||
}
|
||||
return (s_locked != 0u) ? -1 : 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读闭锁状态
|
||||
* 入口参数:无
|
||||
* 返 回 值:1 = 定值非法(保护应整体闭锁)/ 0 = 正常
|
||||
*/
|
||||
uint8_t prot_set_locked(void) {
|
||||
return s_locked;
|
||||
}
|
||||
|
||||
/* ==================== 持久化 blob(B7,纯逻辑) ========================= */
|
||||
|
||||
/* 小端编解码(blob 跨端一致;主机单测与 MCU 同一份字节口径) */
|
||||
|
||||
/*
|
||||
* 函数功能:16 位值按小端写入缓冲
|
||||
* 入口参数:p - 写位置(≥2 字节余量);v - 值
|
||||
* 返 回 值:无
|
||||
*/
|
||||
static void s_put16(uint8_t *p, uint16_t v) {
|
||||
p[0] = (uint8_t)(v & 0xFFu);
|
||||
p[1] = (uint8_t)(v >> 8);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:32 位值按小端写入缓冲
|
||||
* 入口参数:p - 写位置(≥4 字节余量);v - 值
|
||||
* 返 回 值:无
|
||||
*/
|
||||
static void s_put32(uint8_t *p, uint32_t v) {
|
||||
p[0] = (uint8_t)(v & 0xFFu);
|
||||
p[1] = (uint8_t)((v >> 8) & 0xFFu);
|
||||
p[2] = (uint8_t)((v >> 16) & 0xFFu);
|
||||
p[3] = (uint8_t)(v >> 24);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:从小端缓冲读 16 位值
|
||||
* 入口参数:p - 读位置
|
||||
* 返 回 值:值 uint16_t
|
||||
*/
|
||||
static uint16_t s_get16(const uint8_t *p) {
|
||||
return (uint16_t)((uint32_t)p[0] | ((uint32_t)p[1] << 8));
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:从小端缓冲读 32 位值
|
||||
* 入口参数:p - 读位置
|
||||
* 返 回 值:值 uint32_t
|
||||
*/
|
||||
static uint32_t s_get32(const uint8_t *p) {
|
||||
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16)
|
||||
| ((uint32_t)p[3] << 24);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:把当前值区序列化为 blob(校验和按值区现算,不依赖封存值)
|
||||
* 入口参数:p_buf - 输出缓冲;cap - 容量;p_len - 实际长度输出
|
||||
* 返 回 值:0 / -1 参数非法或容量不足
|
||||
*/
|
||||
int prot_set_store_serialize(uint8_t *p_buf, uint32_t cap, uint32_t *p_len) {
|
||||
const uint32_t *vals = s_val[s_act];
|
||||
uint32_t sum = 0u;
|
||||
uint32_t i;
|
||||
|
||||
if ((p_buf == 0) || (p_len == 0) || (cap < PROT_SET_BLOB_SZ)) {
|
||||
return -1;
|
||||
}
|
||||
s_put32(p_buf, PROT_SET_BLOB_MAGIC);
|
||||
s_put16(p_buf + 4u, (uint16_t)PROT_SET_BLOB_VER);
|
||||
s_put16(p_buf + 6u, (uint16_t)PS_ID_COUNT);
|
||||
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
|
||||
s_put32(p_buf + PROT_SET_BLOB_HDR_SZ + 4u * i, vals[i]);
|
||||
sum += vals[i];
|
||||
}
|
||||
s_put32(p_buf + PROT_SET_BLOB_SZ - 8u, sum);
|
||||
s_put32(p_buf + PROT_SET_BLOB_SZ - 4u, ~sum);
|
||||
*p_len = PROT_SET_BLOB_SZ;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:解析 blob 并**整表原子**装载(all-or-nothing)
|
||||
* 入口参数:p_buf/len - blob 字节流
|
||||
* 返 回 值:0 已装载并封存 / -1 拒绝(magic/版本/长度/校验对不符或任一项
|
||||
* 越界 → 活跃值区一字不动;越界项经 prot_set_bad_mask() 指认)
|
||||
* 函数说明:解码/校验全在影子区进行,只有全部通过才交换活跃区——
|
||||
* 损坏 blob 不可能污染在用的定值(掉电/写坏/版本换代同一路径)。
|
||||
*/
|
||||
int prot_set_store_parse(const uint8_t *p_buf, uint32_t len) {
|
||||
uint32_t cur_act;
|
||||
uint32_t *shadow;
|
||||
uint32_t sum = 0u;
|
||||
uint32_t mask = 0u;
|
||||
uint32_t i;
|
||||
|
||||
if ((p_buf == 0) || (len != PROT_SET_BLOB_SZ)) {
|
||||
return -1;
|
||||
}
|
||||
if (s_get32(p_buf) != PROT_SET_BLOB_MAGIC) {
|
||||
return -1;
|
||||
}
|
||||
if (s_get16(p_buf + 4u) != (uint16_t)PROT_SET_BLOB_VER) {
|
||||
return -1;
|
||||
}
|
||||
if (s_get16(p_buf + 6u) != (uint16_t)PS_ID_COUNT) {
|
||||
return -1;
|
||||
}
|
||||
cur_act = s_act;
|
||||
shadow = s_val[cur_act ^ 1u];
|
||||
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
|
||||
uint32_t v = s_get32(p_buf + PROT_SET_BLOB_HDR_SZ + 4u * i);
|
||||
|
||||
shadow[i] = v;
|
||||
sum += v;
|
||||
}
|
||||
if (sum != s_get32(p_buf + PROT_SET_BLOB_SZ - 8u)) {
|
||||
return -1; /* 校验和正码不符(影子区未交换前不可见,无害) */
|
||||
}
|
||||
if (~sum != s_get32(p_buf + PROT_SET_BLOB_SZ - 4u)) {
|
||||
return -1; /* 反码对不符 */
|
||||
}
|
||||
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
|
||||
if (((int32_t)shadow[i] < g_prot_set_meta[i].min)
|
||||
|| ((int32_t)shadow[i] > g_prot_set_meta[i].max)) {
|
||||
mask |= (1uL << i);
|
||||
}
|
||||
}
|
||||
s_bad_mask = mask;
|
||||
if (mask != 0u) {
|
||||
return -1; /* 校验对一致但含越界项:同样整表拒绝 */
|
||||
}
|
||||
s_sum = sum;
|
||||
s_sumn = ~sum;
|
||||
s_locked = 0u;
|
||||
s_act = (uint8_t)(cur_act ^ 1u);
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* 模块名称:保护定值模型(P1,L5-1 载体)
|
||||
* 模块功能:保护定值的 RAM 模型——编号 / 通讯地址 / 范围 / 默认值 / 校验和自检,
|
||||
* 以及 B7 定值持久化的**纯逻辑层**(blob 序列化/解析 + 原子装载)。
|
||||
* 文件读写胶水在 App/cfg/prot_set_store.c(MCU 端)。
|
||||
*
|
||||
* 设计口径(docs/算法/继电保护功能复现规划.md §5):
|
||||
* - 定点存储:Q10(×1024),与参考系统定点系数 2^10 一致(§5.3);
|
||||
* - 通讯地址:与参考 g_tCommSetParaTab(Comm/Pub/Const_CommSetPara.c)逐项对齐,
|
||||
* 基址 0x8820(=34848);告警类沿用运行参数段 0x8027/0x8029;
|
||||
* - 校验和(L5-1):sum32 正码 + 反码成对封存(参考 tagPRS 的
|
||||
* dwTagSum/dwTagSumN 模式),selfcheck 失配 → 置闭锁;
|
||||
* - ⚠ 默认值/范围为**拟定值**:参考 EEPROM 出厂默认表未在本仓库,
|
||||
* 待对照后逐项修订(规划 §10.3 待补项);范围先按 DTU 馈线典型量级。
|
||||
*
|
||||
* 线程约定(B7 定):装载(init/store_parse)在 defaultTask(存储 bring-up,
|
||||
* STORAGE_READY 前);单项写在 iec104Task(C_SE/YK12,经 set_sealed 原子路径);
|
||||
* 落盘在 defaultTask(配置引擎属主)。读在保护消费者任务(快照值原子)。
|
||||
* 双缓冲交换 = 单字写,读侧永远看到一致快照;写者之间靠启动时序串行
|
||||
* (装载先于 NET_READY,104 写在其后)。
|
||||
*
|
||||
* 创建日期:2026-10-11
|
||||
*/
|
||||
#ifndef PROT_SET_H
|
||||
#define PROT_SET_H
|
||||
|
||||
#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))
|
||||
|
||||
/* 定值项编号(枚举序 = RAM 数组序 = 校验和覆盖序,只增不删不改序) */
|
||||
typedef enum {
|
||||
/* --- 三段式相间过流(A 相,L3-1):投退/跳闸投退/电流/延时 --- */
|
||||
PS_OC1_EN = 0, /* 0x8856 Ⅰ段投退 */
|
||||
PS_OC1_TRIP, /* 0x8857 Ⅰ段跳闸投退(0 告警 / 1 跳闸) */
|
||||
PS_OC1_CUR, /* 0x8858 Ⅰ段电流定值(A,二次值) */
|
||||
PS_OC1_DLY, /* 0x8859 Ⅰ段延时(ms) */
|
||||
PS_OC2_EN, /* 0x885A */
|
||||
PS_OC2_TRIP, /* 0x885B */
|
||||
PS_OC2_CUR, /* 0x885C */
|
||||
PS_OC2_DLY, /* 0x885D */
|
||||
PS_OC3_EN, /* 0x885E */
|
||||
PS_OC3_TRIP, /* 0x885F */
|
||||
PS_OC3_CUR, /* 0x8860 */
|
||||
PS_OC3_DLY, /* 0x8861 */
|
||||
/* --- 过负荷(A 相,L3-3) --- */
|
||||
PS_OL_EN, /* 0x8871 */
|
||||
PS_OL_TRIP, /* 0x8872 */
|
||||
PS_OL_CUR, /* 0x8873 */
|
||||
PS_OL_DLY, /* 0x8874 */
|
||||
/* --- 过压 / 欠压(UA,L3-4;沿用参考告警段 0x8029/0x8027) --- */
|
||||
PS_OV_EN, /* 本地投退(参考告警类无独立投退,V0.1 补) */
|
||||
PS_OV_VOL, /* 0x8029 过电压门限(V,二次值) */
|
||||
PS_OV_DLY, /* 0x802A 过电压延时(ms) */
|
||||
PS_UV_EN, /* 本地投退 */
|
||||
PS_UV_VOL, /* 0x8027 低电压门限(V,二次值) */
|
||||
PS_UV_DLY, /* 0x8028 低电压延时(ms) */
|
||||
/* --- 零序过压(UN,L3-6) --- */
|
||||
PS_3U0_EN, /* 0x888E */
|
||||
PS_3U0_VOL, /* 0x888F 3U0 定值(V,二次值) */
|
||||
PS_3U0_DLY, /* 0x8890 延时(ms) */
|
||||
/* --- 公共 --- */
|
||||
PS_RET_OVER, /* 过量返回系数(返回门槛 = 定值 × 系数,<1) */
|
||||
PS_RET_UNDER, /* 欠量返回系数(返回门槛 = 定值 × 系数,>1) */
|
||||
PS_PT_PRI, /* 0x8820 PT 一次额定(V) */
|
||||
PS_PT_SEC, /* 0x8821 PT 二次额定(V) */
|
||||
PS_CT_PRI, /* 0x8825 CT 一次额定(A) */
|
||||
PS_CT_SEC, /* 0x8826 CT 二次额定(A) */
|
||||
/* --- 全局软压板(B7):104 YK12 两段式投退;0 = 保护退出服务 --- */
|
||||
PS_GLOBAL_EN, /* 本地项(无通讯地址,经 YK12 写、C_RD 不可读) */
|
||||
PS_ID_COUNT
|
||||
} prot_set_id_t;
|
||||
|
||||
/* 定值项元数据(ROM 常量表,校验和自检不覆盖它——只覆盖 RAM 值区) */
|
||||
typedef struct {
|
||||
uint16_t comm_addr; /* 通讯信息体地址(0 = 本地项,不上送) */
|
||||
uint8_t is_ctl; /* 1 = 投退/枚举项(0/1),0 = 模拟量项 */
|
||||
int32_t def; /* 默认值(Q10;投退项为 0/1 的 Q10) */
|
||||
int32_t min; /* 下限(Q10) */
|
||||
int32_t max; /* 上限(Q10) */
|
||||
} prot_set_meta_t;
|
||||
|
||||
extern const prot_set_meta_t g_prot_set_meta[PS_ID_COUNT];
|
||||
|
||||
/*
|
||||
* 函数功能:装载出厂默认值并封存校验和(幂等,内核启动前可调)
|
||||
*/
|
||||
void prot_set_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:整表装载外部值(P4 起由 cfg_file 定值段调用),逐项范围校验
|
||||
* 入口参数:vals - PS_ID_COUNT 个 Q10 值
|
||||
* 返 回 值:0 全部合法(已装载并封存)/ -1 存在越界项(**整表不装载**,
|
||||
* 返回前可经 prot_set_bad_mask() 取越界位图)
|
||||
*/
|
||||
int prot_set_load_raw(const int32_t *vals);
|
||||
|
||||
/* 越界位图(bit i = 第 i 项越界;load_raw 失败后有效) */
|
||||
uint32_t prot_set_bad_mask(void);
|
||||
|
||||
/*
|
||||
* 函数功能:单项写入(范围校验;**不自动重封校验和**——批量改完必须
|
||||
* 显式 prot_set_seal(),否则 selfcheck 判"定值被非法改写")
|
||||
* 返 回 值:0 / -1 越界拒写
|
||||
*/
|
||||
int prot_set_set(prot_set_id_t id, int32_t q10);
|
||||
|
||||
int32_t prot_set_get(prot_set_id_t id); /* Q10 原值 */
|
||||
float prot_set_get_f(prot_set_id_t id); /* 工程量浮点 */
|
||||
uint8_t prot_set_get_ctl(prot_set_id_t id); /* 投退项:!=0 视为投入 */
|
||||
|
||||
/* 校验和(L5-1):sum32 覆盖 RAM 值区;seal 封存正/反码对 */
|
||||
uint32_t prot_set_checksum(void);
|
||||
void prot_set_seal(void);
|
||||
/* 自检:重算并与封存值比对,失配 → 置闭锁并返回 -1(已闭锁也返回 -1) */
|
||||
int prot_set_selfcheck(void);
|
||||
uint8_t prot_set_locked(void);
|
||||
|
||||
/*
|
||||
* 函数功能:单项**原子**写入+封存(B7,收口 B5 竞态窗口)
|
||||
* 入口参数:id - 定值项编号;q10 - Q10 定点值
|
||||
* 返 回 值:0 / -1 越界或非法编号拒写(原值不动)
|
||||
* 函数说明:影子缓冲拷贝 → 改影子 → 重封校验和 → 单字交换活跃区。
|
||||
* selfcheck 永远看到"值区与封存一致"的完整快照,不存在
|
||||
* "已改值未封存"中间态(区别于 prot_set_set+seal 的批处理路径,
|
||||
* 两条路径不得在一次批处理中途交错)。置脏标志(待持久化)。
|
||||
*/
|
||||
int prot_set_set_sealed(prot_set_id_t id, int32_t q10);
|
||||
|
||||
/* 脏标志(存在未落盘的定值改动):set_sealed 置位;load/init 不置 */
|
||||
uint8_t prot_set_dirty_peek(void); /* 非消费(生产者用它通知属主任务) */
|
||||
uint8_t prot_set_dirty_take(void); /* 消费(属主任务落盘前取) */
|
||||
void prot_set_dirty_mark(void); /* 写盘失败回置(等下轮重试) */
|
||||
|
||||
/* ---------------- 持久化 blob(B7;纯逻辑,主机可单测) ----------------
|
||||
* 格式(小端):magic 'PSET'(4B) + version u16 + count u16
|
||||
* + values u32×count + sum32 u32 + ~sum32 u32
|
||||
* 版本 1;count 必须等于 PS_ID_COUNT(定值表增项 → 旧 blob 整体作废,
|
||||
* 编号不回收但格式换代,宁可用出厂默认也不半装载)。 */
|
||||
#define PROT_SET_BLOB_MAGIC 0x50534554uL /* 字节序 P,S,E,T */
|
||||
#define PROT_SET_BLOB_VER 1u
|
||||
#define PROT_SET_BLOB_HDR_SZ 8u /* magic(4)+ver(2)+cnt(2) */
|
||||
#define PROT_SET_BLOB_SZ (PROT_SET_BLOB_HDR_SZ + 4u * (uint32_t)PS_ID_COUNT + 8u)
|
||||
|
||||
/*
|
||||
* 函数功能:把当前值区序列化为 blob(校验和按值区现算,不依赖封存值)
|
||||
* 返 回 值:0 / -1 缓冲不足或参数非法;*p_len 输出实际长度
|
||||
*/
|
||||
int prot_set_store_serialize(uint8_t *p_buf, uint32_t cap, uint32_t *p_len);
|
||||
|
||||
/*
|
||||
* 函数功能:解析 blob 并**整表原子**装载(all-or-nothing)
|
||||
* 入口参数:p_buf/len - blob 字节流
|
||||
* 返 回 值:0 已装载并封存 / -1 拒绝(magic/版本/长度/校验对不符或
|
||||
* 任一项越界 → 值区一字不动;越界项经 prot_set_bad_mask() 指认)
|
||||
*/
|
||||
int prot_set_store_parse(const uint8_t *p_buf, uint32_t len);
|
||||
|
||||
#endif /* PROT_SET_H */
|
||||
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 模块名称:采样环缓冲(L2 相量计算的输入)
|
||||
* 模块功能:按语义通道保存最近 2 个周波(40 点)采样值,供 L2 相量计算按窗取数。
|
||||
* 适用平台:STM32F407ZGTx(纯算法,无寄存器/无 RTOS 依赖)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-10 王建锋 创建:L2 输入环缓冲(20 点/周波 × 2 周波)
|
||||
*/
|
||||
|
||||
#include "sample_ring.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
/* 采样环:[语义通道][点]。按语义通道索引(0..ADC_SEM_COUNT-1),
|
||||
* 故消费侧"按语义取数",不依赖任何物理通道号或转换器归属。 */
|
||||
static float s_ring[ADC_SEM_COUNT][SAMPLE_RING_DEPTH];
|
||||
/* 写指针:下一个待写入的点下标(0..SAMPLE_RING_DEPTH-1) */
|
||||
static uint16_t s_head;
|
||||
/* 已累积点数(饱和于 SAMPLE_RING_DEPTH) */
|
||||
static uint16_t s_filled;
|
||||
/* 累计入环帧数(不饱和,供心跳/丢帧观测) */
|
||||
static uint32_t s_frames;
|
||||
|
||||
/*
|
||||
* 函数功能:采样环缓冲初始化
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 限定条件:无
|
||||
* 函数说明:清空全部通道历史与计数;可重复调用(幂等)。
|
||||
*/
|
||||
void sample_ring_init(void) {
|
||||
uint8_t c;
|
||||
uint16_t i;
|
||||
|
||||
for (c = 0u; c < (uint8_t)ADC_SEM_COUNT; c++) {
|
||||
for (i = 0u; i < SAMPLE_RING_DEPTH; i++) {
|
||||
s_ring[c][i] = 0.0f;
|
||||
}
|
||||
}
|
||||
s_head = 0u;
|
||||
s_filled = 0u;
|
||||
s_frames = 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:压入一拍(一帧)全部语义通道的采样值
|
||||
* 入口参数:values - 按语义通道顺序排列的一帧采样值(长度须 = ADC_SEM_COUNT)
|
||||
* n - 实际长度(必须等于 ADC_SEM_COUNT)
|
||||
* 返 回 值:0 - 成功;-1 - 参数错
|
||||
* 限定条件:同一帧内各通道须同刻(同族量同刻性由上游 L1 负责)
|
||||
* 函数说明:整帧写入完毕后**统一**推进写指针——单帧内不存在"半新半旧",
|
||||
* 消费侧取到的窗永远落在整帧边界上(无帧内撕裂)。
|
||||
*/
|
||||
int sample_ring_push_frame(const float *values, uint8_t n) {
|
||||
uint8_t c;
|
||||
|
||||
if ((values == NULL) || (n != (uint8_t)ADC_SEM_COUNT)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
for (c = 0u; c < (uint8_t)ADC_SEM_COUNT; c++) {
|
||||
s_ring[c][s_head] = values[c];
|
||||
}
|
||||
|
||||
s_head = (uint16_t)((s_head + 1u) % SAMPLE_RING_DEPTH);
|
||||
if (s_filled < SAMPLE_RING_DEPTH) {
|
||||
s_filled++;
|
||||
}
|
||||
s_frames++;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取指定语义通道最近 n 点的窗(按时间序,旧 → 新)
|
||||
* 入口参数:ch - 语义通道
|
||||
* n - 取点数(1..SAMPLE_RING_DEPTH)
|
||||
* out - 输出缓冲(长度须 >= n)
|
||||
* 返 回 值:0 - 成功;-1 - 参数错;-2 - 已累积点数不足 n
|
||||
* 限定条件:无
|
||||
* 函数说明:out[n-1] 为最新一点。最老点下标 = head - n(模 DEPTH)。
|
||||
*/
|
||||
int sample_ring_window(adc_semantic_t ch, uint8_t n, float *out) {
|
||||
uint16_t start;
|
||||
uint16_t i;
|
||||
|
||||
if ((out == NULL) || ((unsigned int)ch >= (unsigned int)ADC_SEM_COUNT)) {
|
||||
return -1;
|
||||
}
|
||||
if ((n == 0u) || (n > SAMPLE_RING_DEPTH)) {
|
||||
return -1;
|
||||
}
|
||||
if (s_filled < (uint16_t)n) {
|
||||
return -2;
|
||||
}
|
||||
|
||||
start = (uint16_t)((s_head + SAMPLE_RING_DEPTH - (uint16_t)n) % SAMPLE_RING_DEPTH);
|
||||
for (i = 0u; i < (uint16_t)n; i++) {
|
||||
out[i] = s_ring[ch][(uint16_t)((start + i) % SAMPLE_RING_DEPTH)];
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读已累积点数(饱和于 SAMPLE_RING_DEPTH)
|
||||
* 入口参数:无
|
||||
* 返 回 值:已累积点数(0..SAMPLE_RING_DEPTH)
|
||||
* 限定条件:无
|
||||
* 函数说明:供上层判断"数据是否够一周波"。
|
||||
*/
|
||||
uint16_t sample_ring_filled(void) {
|
||||
return s_filled;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读累计入环帧数(不饱和)
|
||||
* 入口参数:无
|
||||
* 返 回 值:累计入环帧数
|
||||
* 限定条件:无
|
||||
* 函数说明:供心跳日志 / 丢帧观测(L1-6 判据之一)。
|
||||
*/
|
||||
uint32_t sample_ring_frames(void) {
|
||||
return s_frames;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
/*
|
||||
* 模块名称:采样环缓冲(L2 相量计算的输入)
|
||||
* 模块功能:按**语义通道**保存最近若干周波的采样值,供 L2 相量计算按窗取数。
|
||||
* V0.1 上 8 路电气量分属两片转换器(硬件缺陷 N-18),本层只做"按时序
|
||||
* 存/取",**不做任何跨通道运算**——同刻性由上游(L1)保证,本层不假设。
|
||||
* 容量取 **2 个周波(40 点)**,与参考系统 `CN_BUFSAM_CYCLE = 2`
|
||||
* (规划 §10.1)一致,为后续加长数据窗(1.5~2 周波,抗衰减直流,
|
||||
* 见 `傅氏算法详解.md` §9 Q4)预留空间。
|
||||
* 适用平台:STM32F407ZGTx(**纯算法**:不碰寄存器、不碰 RTOS → 可主机侧单测 L0.5)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-10 王建锋 创建:L2 输入环缓冲(20 点/周波 × 2 周波)
|
||||
*/
|
||||
|
||||
#ifndef SAMPLE_RING_H
|
||||
#define SAMPLE_RING_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "adc_input.h" /* adc_semantic_t / ADC_SEM_COUNT:本层按语义通道存取 */
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 每周波采样点数(1000 Hz @ 50 Hz,见 docs/子系统设计/ADC采样节拍设计说明.md) */
|
||||
#define SAMPLE_RING_CYCLE_N 20U
|
||||
/* 环容量(周波数)——与参考系统 CN_BUFSAM_CYCLE 一致 */
|
||||
#define SAMPLE_RING_CYCLES 2U
|
||||
/* 环深度(点):一周波点数 × 周波数 */
|
||||
#define SAMPLE_RING_DEPTH (SAMPLE_RING_CYCLE_N * SAMPLE_RING_CYCLES)
|
||||
|
||||
/*
|
||||
* 函数功能:采样环缓冲初始化
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 限定条件:内核启动前调用(无 RTOS 依赖)
|
||||
* 函数说明:清空全部通道历史与计数;可重复调用(幂等)。
|
||||
*/
|
||||
void sample_ring_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:压入一拍(一帧)全部语义通道的采样值
|
||||
* 入口参数:values - 按语义通道顺序排列的一帧采样值(浮点工程量,长度须 = ADC_SEM_COUNT)
|
||||
* n - 实际长度(必须等于 ADC_SEM_COUNT,否则拒收)
|
||||
* 返 回 值:0 - 成功;-1 - 参数错(空指针或长度不符)
|
||||
* 限定条件:调用方保证同一帧内各通道**同刻**(同族量同刻性由上游 L1 负责)
|
||||
* 函数说明:整帧一次性写入,写完统一推进写指针——单帧内不可能出现"半新半旧",
|
||||
* 即天然无帧内撕裂。消费侧读到的窗永远是**整帧**边界。
|
||||
*/
|
||||
int sample_ring_push_frame(const float *values, uint8_t n);
|
||||
|
||||
/*
|
||||
* 函数功能:取指定语义通道最近 n 点的窗(按时间序,旧 → 新)
|
||||
* 入口参数:ch - 语义通道
|
||||
* n - 取点数(1..SAMPLE_RING_DEPTH)
|
||||
* out - 输出缓冲(长度须 >= n)
|
||||
* 返 回 值:0 - 成功;-1 - 参数错;-2 - 已累积点数不足 n
|
||||
* 限定条件:无
|
||||
* 函数说明:out[n-1] 为最新一点。相量层以本函数取"以当前时刻为终止的一周波"。
|
||||
*/
|
||||
int sample_ring_window(adc_semantic_t ch, uint8_t n, float *out);
|
||||
|
||||
/*
|
||||
* 函数功能:读已累积点数(饱和于 SAMPLE_RING_DEPTH)
|
||||
* 入口参数:无
|
||||
* 返 回 值:已累积点数(0..SAMPLE_RING_DEPTH)
|
||||
* 限定条件:无
|
||||
* 函数说明:供上层判断"数据是否够一周波"。
|
||||
*/
|
||||
uint16_t sample_ring_filled(void);
|
||||
|
||||
/*
|
||||
* 函数功能:读累计入环帧数(**不饱和**,供心跳/丢帧观测)
|
||||
* 入口参数:无
|
||||
* 返 回 值:累计入环帧数(32 位,1 kHz 下约 49.7 天回绕)
|
||||
* 限定条件:无
|
||||
* 函数说明:与 L1 的 `tpafe5160_frame_seq()` 对照可判丢帧(L1-6 采样有效性校验
|
||||
* 的判据之一,本层只提供计数,不做判定)。
|
||||
*/
|
||||
uint32_t sample_ring_frames(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SAMPLE_RING_H */
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
* 零代码引用(grep 仅本文件有定义)——"开关名存实亡"会
|
||||
* 误导评审者以为模块可裁剪。现如实标注为**预留占位**:
|
||||
* 当前**不门控任何代码**,各模块一律编译进产品。是否/
|
||||
* 如何接线取决于产品定义(待处理内容.md §2 阶段 B0),
|
||||
* 如何接线取决于产品定义(docs/任务规划/README.md §2 阶段 B0),
|
||||
* 确认后再在真实初始化点(如 app_main.c 的 rs485_init /
|
||||
* tpafe5160_init)加门控;在此之前**不要**把它们当开关用。
|
||||
*/
|
||||
|
||||
+77
-16
@@ -17,15 +17,21 @@
|
||||
#include "main.h" /* ST_DIR4_GPIO_Port / ST_DIR4_Pin 等 CubeMX 宏 */
|
||||
#include "stm32f4xx_it.h" /* fault_handler_enable() */
|
||||
#include "usart.h" /* huart5 */
|
||||
#include "tim.h" /* htim6:1 kHz 采样节拍(TIM6 更新中断触发 CONVST) */
|
||||
#include "tim.h" /* htim3:1 kHz 采样节拍(TIM3 更新中断触发 CONVST + TRGO 触发 ADC1) */
|
||||
#include "rs485.h"
|
||||
#include "sd2506.h"
|
||||
#include "sys_clock.h"
|
||||
#include "gd5f2gq5ue.h"
|
||||
#include "tpafe5160.h"
|
||||
#include "adc_input.h" /* L1 采集层:adc_input_init()(语义映射 + STM32 ADC1 扫描) */
|
||||
#include "phasor.h" /* L2 相量层:phasor_init()(系数表 + 实例 + 双区交换) */
|
||||
#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 "test_config.h" /* 套件开关:身份戳的构建变体串(FW_BUILD_VARIANT)依赖本头,
|
||||
* 改 TEST_SUITE_* / ENABLE_DESTRUCTIVE_TESTS 必重编本 TU →
|
||||
* __DATE__/__TIME__ 随之刷新,见 app_main_init() 的戳注释 */
|
||||
|
||||
#define DBG_TAG "[APP]"
|
||||
#include "dbg_log.h"
|
||||
@@ -36,6 +42,26 @@
|
||||
* 放这里而非 main.c,是因为需要与其它应用级全局量同层、且日志侧更好引用。 */
|
||||
volatile uint32_t g_rcc_csr_latched;
|
||||
|
||||
/*
|
||||
* 构建变体串(2026-10-11):把"测试套件开关"编进固件身份戳,见 app_main_init() 的戳注释。
|
||||
* TEST_SUITE_NAME 由 test/board/test_config.h 定义(产品构建 = "NONE");
|
||||
* 模拟器等未纳入该头的编译单元退化为空串,不影响戳的可搜索性。
|
||||
*/
|
||||
#if defined(TEST_SUITE_NAME)
|
||||
#define FW_BUILD_SUITE " suite=" TEST_SUITE_NAME
|
||||
#else
|
||||
#define FW_BUILD_SUITE ""
|
||||
#endif
|
||||
|
||||
#if defined(ENABLE_DESTRUCTIVE_TESTS)
|
||||
#define FW_BUILD_DSTR " dstr=on"
|
||||
#else
|
||||
#define FW_BUILD_DSTR " dstr=off"
|
||||
#endif
|
||||
|
||||
/* 身份戳后缀:一段连续 ASCII,保证 .hex 里可原样搜到(run_boot_test.py 抠戳用) */
|
||||
#define FW_BUILD_VARIANT FW_BUILD_SUITE FW_BUILD_DSTR
|
||||
|
||||
/*
|
||||
* 函数功能:把 RCC_CSR 锁存值翻译为可读的复位原因串(见手册 RM0090 §6.3.22)
|
||||
* 入口参数:csr - 锁存的 RCC_CSR 值
|
||||
@@ -91,7 +117,7 @@ static void reset_cause_str(uint32_t csr, char *buf, unsigned int len) {
|
||||
* 3) GD5F NAND
|
||||
* 4) TPAFE5160 ADC → ADC 输入层(L1,语义映射 + STM32 ADC1)
|
||||
* 5) SD2506 诊断信息
|
||||
* 6) 启动 TIM6 1 kHz 采样节拍(ISR 内触发 CONVST)
|
||||
* 6) 启动 TIM3 1 kHz 采样节拍(ISR 内脉冲 CONVST;同一更新事件经 TRGO 硬件触发 ADC1 + DMA)
|
||||
*/
|
||||
int app_main_init(void) {
|
||||
/* 0) 使能 MemManage / BusFault / UsageFault(复位后默认只有 HardFault 使能)。
|
||||
@@ -113,8 +139,15 @@ int app_main_init(void) {
|
||||
* 用途:@test 把"芯片里正在跑的镜像"和"刚编出来的镜像"对上号。此前开机标记
|
||||
* 全是固定串(App init OK / Network Init / ...),旧镜像也照样全中,于是
|
||||
* "反复重下载、芯片一直是旧镜像"这类事故在自动闭环里完全不可见
|
||||
* (AGENTS.md 烧录铁律记的正是它)。代价:1 条 DBG ≈ 90 B Flash + 30 B 常量。 */
|
||||
DBG_INFO("fw build: " __DATE__ " " __TIME__);
|
||||
* (AGENTS.md 烧录铁律记的正是它)。代价:1 条 DBG ≈ 90 B Flash + 30 B 常量。
|
||||
* ⚠ 戳只在**本 TU 被重编**时才刷新(增量构建),故本文件必须 #include
|
||||
* "test_config.h":否则只切套件宏(TEST_SUITE_* / ENABLE_DESTRUCTIVE_TESTS)
|
||||
* 不重编本 TU,产品镜像与测试镜像顶着同一个戳,戳校验对"两种构建互换"失明
|
||||
* (2026-10-11 实测:切宏后的测试构建与产品构建的戳一模一样,只有回读校验
|
||||
* 才发现镜像不同)。两条防线:① 头依赖 → 改宏必重编、戳必刷新;
|
||||
* ② FW_BUILD_VARIANT 把套件名与破坏性开关编进戳内容 → 即便戳时间相同,
|
||||
* 产品串与测试串也不同。代价:约 20 B 常量。 */
|
||||
DBG_INFO("fw build: " __DATE__ " " __TIME__ FW_BUILD_VARIANT);
|
||||
|
||||
/* 0) 复位原因上报(锁存值由 Src/main.c 在 HAL_Init() 后立即采得)。
|
||||
* 放在日志引擎之后、业务初始化之前:任何一次非上电复位都值得在
|
||||
@@ -183,17 +216,40 @@ int app_main_init(void) {
|
||||
}
|
||||
|
||||
/* 4b) ADC 输入层(L1)初始化:清原始码值缓冲与统计 + **语义映射表自洽性自检**
|
||||
* (映射指向了不会被扫描的通道 → 返回 -1,避免"读到恒为陈旧值"的静默错)。
|
||||
* STM32 ADC1 外设已由 MX_ADC1_Init()(main.c)配好 9 通道 / 144 cycles /
|
||||
* 软件触发,本层不重复配置、也不启动转换(软件触发按拍扫描)。
|
||||
* 须在启动采样节拍(步骤 6)之前。 */
|
||||
if (adc_input_init() != 0) {
|
||||
DBG_ERROR("ADC input layer: map self-check FAILED");
|
||||
syslog_post(SYSLOG_SELFTEST, "ADC map fault");
|
||||
} else {
|
||||
DBG_INFO("ADC input layer init: OK");
|
||||
* (映射指向了不会被扫描的通道 → 返回 -1,避免"读到恒为陈旧值"的静默错)
|
||||
* + 挂载一次 STM32 ADC1 的 DMA(失败返回 -2)。
|
||||
* ADC1 外设已由 MX_ADC1_Init()(main.c)配好 9 通道 / 144 cycles /
|
||||
* **TIM3 TRGO 外部触发 + DMA 连续请求**,本层不重复配置、也不触发转换。
|
||||
* 须在启动采样节拍(步骤 6)之前——先挂 DMA 再放节拍,才不会漏第一拍。 */
|
||||
{
|
||||
int adc_rc = adc_input_init();
|
||||
|
||||
if (adc_rc == -1) {
|
||||
DBG_ERROR("ADC input layer: map self-check FAILED");
|
||||
syslog_post(SYSLOG_SELFTEST, "ADC map fault");
|
||||
} else if (adc_rc != 0) {
|
||||
DBG_ERROR("ADC input layer: DMA arm FAILED (rc=%d)", adc_rc);
|
||||
syslog_post(SYSLOG_SELFTEST, "ADC dma fault");
|
||||
} else {
|
||||
DBG_INFO("ADC input layer init: OK");
|
||||
}
|
||||
}
|
||||
|
||||
/* 4c) 相量层(L2)初始化:清双区结果与有效标志 + 复位采样环缓冲。
|
||||
* 纯算法层(无外设 / 无 RTOS 依赖,见 docs/算法/继电保护功能复现规划.md
|
||||
* §3.2),须在启动采样节拍(步骤 6)之前。幂等、当前无失败路径
|
||||
* (返回值保留供后续自检扩展)。 */
|
||||
(void)phasor_init();
|
||||
DBG_INFO("phasor layer init: OK (inst=%u)", (unsigned)phasor_inst_count());
|
||||
|
||||
/* 4d) 保护定值 + 元件层初始化(B5):出厂默认定值封存校验和 + 元件状态
|
||||
* 复位并解析相量索引(须在 phasor_init 之后)。内核前完成 → iec104
|
||||
* 定值读/protectTask 首拍前定值必已装载。判据消费在 protectTask。 */
|
||||
prot_set_init();
|
||||
prot_elm_init();
|
||||
DBG_INFO("protection init: OK (set=%u elm=%u)",
|
||||
(unsigned)PS_ID_COUNT, (unsigned)PE_COUNT);
|
||||
|
||||
/* 5) SD2506 RTC 诊断信息 */
|
||||
{
|
||||
/* 读取 ID */
|
||||
@@ -223,10 +279,15 @@ int app_main_init(void) {
|
||||
sd_vbat / 1000, (sd_vbat % 1000) / 10);
|
||||
}
|
||||
|
||||
/* 6) 启动 1 kHz 采样节拍:TIM6 更新中断 → ISR 内触发 CONVST(Src/main.c)。
|
||||
/* 6) 启动 1 kHz 采样节拍:TIM3 更新事件 → ① ISR 内脉冲 CONVST(Src/main.c)
|
||||
* ② 同一事件经 TRGO 硬件触发 STM32 ADC1 一轮 9 通道转换(DMA 自动搬运)。
|
||||
* ⚠ 必须在 tpafe5160_init() 成功之后启动,否则 ISR 会在驱动首帧自检
|
||||
* (E-44 有效性探针)期间乱发 CONVST。消费端 adcTask 退化为纯消费者。 */
|
||||
(void)HAL_TIM_Base_Start_IT(&htim6);
|
||||
* (E-44 有效性探针)期间乱发 CONVST。消费端 adcTask 退化为纯消费者。
|
||||
* ⚠ 节拍源 v2.0 由 TIM6 改为 **TIM3**:F4 上 ADC1 的外部触发只认
|
||||
* TIM1/TIM2/TIM3/TIM4/TIM5/TIM8 的 CC 或 TRGO,**TIM6/TIM7 的 TRGO 只接
|
||||
* DAC**(查 CubeMX DB 与本仓库 stm32f4xx_hal_adc.h 的 EXTSEL 表证实)
|
||||
* → 要让两片转换器由同一个硬件事件驱动,节拍源必须换成 TIM3。 */
|
||||
(void)HAL_TIM_Base_Start_IT(&htim3);
|
||||
|
||||
DBG_INFO("App init OK");
|
||||
return 0;
|
||||
|
||||
@@ -31,6 +31,9 @@ extern osEventFlagsId_t sys_evtHandle;
|
||||
#define SYS_EVT_CFG_SAVE (1u << 5) /* UI:网口配置保存请求(defaultTask = 配置引擎属主,消费并写盘) */
|
||||
#define SYS_EVT_CONSOLE_TX (1u << 6) /* 控制台单写者:生产者入环形缓冲后置位,defaultTask 消费并发送
|
||||
* (消费型命令位,非就绪位——就绪位必须 osFlagsNoClear) */
|
||||
#define SYS_EVT_PROT_SET_SAVE (1u << 7) /* 保护定值落盘请求(B7):iec104Task 检测到定值脏置位,
|
||||
* defaultTask(配置引擎属主)消费并写 /prot_set.bin
|
||||
* (消费型命令位;写盘失败脏标志保留 → 下轮再通知重试) */
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
|
||||
+3
-1
@@ -10,11 +10,13 @@
|
||||
| `cfg_file.c/h` | **配置引擎核心**:解析 / 查询 / 保存 / 2s 周期 `cfg_file_poll` |
|
||||
| `cfg_file_store.h` | **存储后端抽象接口**(MCU=FreeRTOS 堆 / 模拟器=宿主堆,共享核心零分配符号) |
|
||||
| `cfg_file_store_mcu.c` | **MCU 端后端实现**:`/config.ini`(临时名由目标路径同目录推导 + `f_rename` 原子写) |
|
||||
| `prot_set_store.c/h` | **保护定值持久化胶水(B7)**:`/prot_set.bin` blob 读写(blob 格式/校验/原子装载在 `App/algo/prot_set.c` 纯逻辑层);装载 = storage bring-up(屏障前),落盘 = defaultTask 消费 `SYS_EVT_PROT_SET_SAVE` |
|
||||
|
||||
## 设计要点
|
||||
|
||||
- **单任务收敛**:`cfg_file` 为静态态,只有 defaultTask 操作;UI 保存请求经
|
||||
`SYS_EVT_CFG_SAVE` 事件唤醒 defaultTask 执行(netTask/uiTask 不得直接调用)
|
||||
`SYS_EVT_CFG_SAVE` 事件唤醒 defaultTask 执行(netTask/uiTask 不得直接调用);
|
||||
定值落盘同构——iec104Task 只置事件位(`SYS_EVT_PROT_SET_SAVE`),不碰文件系统
|
||||
- **零分配共享核心**:核心逻辑不含任何分配符号,动态内存收进平台后端
|
||||
(`cfg_store_alloc/free`:MCU→`pvPortMalloc`,模拟器→宿主堆)
|
||||
- **原子写**:写 `/config.TMP` 后 `f_rename` 替换,避免掉电损坏
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* 模块名称:prot_set_store(B7 定值持久化)——实现
|
||||
* 模块功能:见 prot_set_store.h。blob 格式/校验/原子装载全在 prot_set.c
|
||||
* (纯逻辑,主机单测覆盖),本文件只做"读文件→parse、
|
||||
* serialize→原子写"的平台胶水。
|
||||
* 文件口径:"/prot_set.bin"(卷根,8.3 合规,FF_USE_LFN=0 约束);
|
||||
* 原子写临时名由 cfg_store_write_atomic 按目标目录推导
|
||||
* (固定 config.TMP),与 /config.ini 的保存同在 defaultTask
|
||||
* 串行执行,无并发竞争。
|
||||
* 创建日期:2026-10-12(B7)
|
||||
*/
|
||||
#include "prot_set_store.h"
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include "cfg_file_store.h"
|
||||
#include "prot_set.h"
|
||||
|
||||
#define PROT_SET_STORE_PATH "/prot_set.bin"
|
||||
|
||||
/* blob 收发缓冲(静态:defaultTask 栈深已是全项目最深链,勿再压 144 B 局部) */
|
||||
static uint8_t s_blob[PROT_SET_BLOB_SZ];
|
||||
|
||||
/*
|
||||
* 函数功能:开机装载定值 blob
|
||||
* 入口参数:无
|
||||
* 返 回 值:0 已装载 / 1 文件缺失或读失败(保持默认)/ -1 损坏被拒
|
||||
* 函数说明:读失败与"文件不存在"同归 1——启动期卷锁无竞争(本任务独占
|
||||
* bring-up),try-lock 失败即真 I/O 故障,此时保持出厂默认并让
|
||||
* 调用方按"无档"处理是安全侧(定值仍封存自洽,L5-1 不误锁)。
|
||||
*/
|
||||
int prot_set_store_load(void) {
|
||||
size_t nread = 0u;
|
||||
|
||||
if (cfg_store_read(PROT_SET_STORE_PATH, s_blob, sizeof(s_blob), &nread) != 0) {
|
||||
return 1;
|
||||
}
|
||||
if (prot_set_store_parse(s_blob, (uint32_t)nread) != 0) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:落盘当前定值(消费脏标志;失败回置等重试)
|
||||
* 入口参数:无
|
||||
* 返 回 值:0 无待写或成功 / -1 写失败
|
||||
*/
|
||||
int prot_set_store_save(void) {
|
||||
uint32_t len = 0u;
|
||||
|
||||
if (prot_set_dirty_take() == 0u) {
|
||||
return 0;
|
||||
}
|
||||
if (prot_set_store_serialize(s_blob, (uint32_t)sizeof(s_blob), &len) != 0) {
|
||||
prot_set_dirty_mark();
|
||||
return -1;
|
||||
}
|
||||
if (cfg_store_write_atomic(PROT_SET_STORE_PATH, s_blob, (size_t)len) != 0) {
|
||||
prot_set_dirty_mark(); /* 卷忙/写失败:脏保留,iec104Task 下轮再通知 */
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* 模块名称:prot_set_store
|
||||
* 模块功能:保护定值持久化胶水(B7)——把 prot_set 的纯逻辑 blob
|
||||
* (prot_set_store_serialize/parse)落到 cfg 存储后端的
|
||||
* "/prot_set.bin"(MCU = NAND/FatFs 原子写,模拟器 = 宿主文件系统)。
|
||||
* 装载在 defaultTask 存储 bring-up(STORAGE_READY 前);
|
||||
* 落盘在 defaultTask(配置引擎属主,iec104Task 经
|
||||
* SYS_EVT_PROT_SET_SAVE 投递请求,本模块只被属主调用)。
|
||||
* 适用平台:双端接口(MCU 与模拟器共用 cfg_file_store 抽象)
|
||||
* 创建日期:2026-10-12(B7)
|
||||
*/
|
||||
#ifndef PROT_SET_STORE_H
|
||||
#define PROT_SET_STORE_H
|
||||
|
||||
/*
|
||||
* 函数功能:开机装载定值 blob(整表原子,all-or-nothing)
|
||||
* 返回值:0 - 已装载;1 - 文件缺失(首启正常,保持出厂默认);
|
||||
* -1 - blob 损坏/越界被拒(保持出厂默认,调用方留痕)
|
||||
* 限定条件:仅 defaultTask、卷已挂载(storage_bring_up 内调用)
|
||||
*/
|
||||
int prot_set_store_load(void);
|
||||
|
||||
/*
|
||||
* 函数功能:落盘当前定值(内部先消费脏标志;无改动 = 空操作返回 0)
|
||||
* 返回值:0 - 无待写或写成功;-1 - 写失败(脏标志已回置,等下轮重试)
|
||||
* 限定条件:仅 defaultTask 调用(配置引擎属主;原子写 = 临时文件 + rename,
|
||||
* 与 /config.ini 共用同目录 config.TMP——两者都在本任务串行执行,无并发)
|
||||
*/
|
||||
int prot_set_store_save(void);
|
||||
|
||||
#endif /* PROT_SET_STORE_H */
|
||||
@@ -16,7 +16,7 @@
|
||||
* 无 stall 日志,纯节奏性饥饿)。修复:发送故障后立即结束本轮
|
||||
* drain 并进入 300ms 发送冷却(冷却期内 drain 直接让位主循环),
|
||||
* 保证喂狗节奏 ≤ ~300ms。根治(USART1 TX-only 灭 ORE 机制)
|
||||
* 另行推进(.ioc 结构性修改 + CubeMX 重生成,见待处理内容①)。
|
||||
* 另行推进(.ioc 结构性修改 + CubeMX 重生成,见 docs/任务规划/登记簿.md S-3)。
|
||||
* 2026-10-02 王建锋 round 8b 加固:发送超时判据改用 DWT CYCCNT(时间源无关)。
|
||||
* 台架 round 8a 取证(见 docs/子系统设计/看门狗设计说明.md 2026-10-02 变更记录)定位到:defaultTask
|
||||
* 冻结在 console 突发行内、无 STALL、复位为 IWDGRSTF。唯一自洽机理 =
|
||||
|
||||
+11
-4
@@ -1,11 +1,18 @@
|
||||
# App/protocol/ — 通信协议(预留)
|
||||
# App/protocol/ — 通信协议(纯逻辑层)
|
||||
|
||||
> **占位目录**:规划用于存放自定义通信协议实现(解析 / 组帧 / 状态机)。
|
||||
> 当前为空(仅 `.gitkeep`),协议相关实现暂分散在:
|
||||
> - RS-485 侧:`App/task/rs485_task.c` + `App/util/crc.c` + `App/util/ringbuf.c`
|
||||
> 存放**跨接口复用的协议逻辑**(解析 / 组帧 / 状态机),零 I/O、依赖注入,
|
||||
> 可被任意任务/接口承载;板端 I/O 胶水在对应 task(如 `App/task/iec104_serv.c`)。
|
||||
> 其余协议实现暂分散在:
|
||||
> - RS-485 侧:`App/task/rs485_task.c`(CRC 在驱动内)
|
||||
> - 网络侧:`Drivers/BSP/NET/`
|
||||
> - 远端显示:`docs/UI设计/RemoDispBus协议与接收设计说明.md`
|
||||
|
||||
## 已落地协议
|
||||
|
||||
| 子目录 | 协议 | 说明 |
|
||||
|---|---|---|
|
||||
| `iec104/` | IEC 60870-5-104 被控站(TCP 2404 上行) | **纯逻辑**三件套(asdu/pts/link,零 I/O,数据源 vtable 注入)→ 全栈 L0.5 主机单测;板端胶水与点表决策 → `docs/子系统设计/IEC104上行设计说明.md` |
|
||||
|
||||
## 何时使用本目录
|
||||
|
||||
当出现**跨接口复用的协议逻辑**(如统一的帧编解码器、协议状态机)时,落到此处,
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 ASDU 编解码(应用层,纯逻辑)
|
||||
* 模块功能:实现 iec104_asdu.h——ASDU 头解析/组装、IOA 3B 小端、IEEE754
|
||||
* 短浮点小端、CP56Time2a ↔ epoch 毫秒(UTC 分解,Hinnant civil
|
||||
* 日历算法,不依赖 time.h → ARMCC/MSVC 双端可移植)。
|
||||
* 线程约定:全部纯函数,无状态 → 任意上下文可调。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
*/
|
||||
#include "iec104_asdu.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/* COT 首字节:bit0..5 = 值,bit6 = PN,bit7 = T;RBT 复用 bit4(C_IC 语义) */
|
||||
#define COT_VAL_MASK 0x3FU
|
||||
#define COT_FLAG_MASK 0xC0U
|
||||
|
||||
/*
|
||||
* 函数功能:解析 ASDU 头(6B),信息体起点经 obj/obj_len 指出
|
||||
* 入口参数:buf/len = ASDU 首字节与总长;hdr = 输出
|
||||
* 返 回 值:0 成功 / -1 空指针或长度不足 6B
|
||||
*/
|
||||
int iec104_asdu_parse_hdr(const uint8_t *buf, uint16_t len, iec104_asdu_hdr_t *hdr) {
|
||||
if ((buf == NULL) || (hdr == NULL) || (len < 6u)) {
|
||||
return -1;
|
||||
}
|
||||
hdr->type = buf[0];
|
||||
hdr->vsq = buf[1];
|
||||
hdr->cot = (uint8_t)(buf[2] & COT_VAL_MASK);
|
||||
hdr->cot_hi = (uint8_t)(buf[2] & 0xC0u);
|
||||
hdr->oa = buf[3];
|
||||
hdr->ca = (uint16_t)(buf[4] | ((uint16_t)buf[5] << 8));
|
||||
hdr->obj = &buf[6];
|
||||
hdr->obj_len = (uint16_t)(len - 6u);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:组装 ASDU 头(OA 恒 0,EVI 由调用方置 buf[3];RBT 位透传)
|
||||
* 入口参数:cot = 6bit 原因值;cot_hi = 标志位(T/PN/RBT);ca = 公共地址
|
||||
* 返 回 值:固定 6(头字节数)
|
||||
*/
|
||||
uint16_t iec104_asdu_put_hdr(uint8_t *buf, uint8_t type, uint8_t vsq,
|
||||
uint8_t cot, uint8_t cot_hi, uint16_t ca) {
|
||||
buf[0] = type;
|
||||
buf[1] = vsq;
|
||||
buf[2] = (uint8_t)(((uint8_t)(cot & COT_VAL_MASK))
|
||||
| (uint8_t)(cot_hi & COT_FLAG_MASK)
|
||||
| (uint8_t)(cot_hi & 0x10u)); /* RBT 位透传 */
|
||||
buf[3] = 0u; /* OA = 0(EVI 由调用方按需置位) */
|
||||
buf[4] = (uint8_t)(ca & 0xFFu);
|
||||
buf[5] = (uint8_t)(ca >> 8);
|
||||
return 6u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:IOA 3 字节小端写入
|
||||
*/
|
||||
void iec104_put_ioa(uint8_t *buf, uint32_t ioa) {
|
||||
buf[0] = (uint8_t)(ioa & 0xFFu);
|
||||
buf[1] = (uint8_t)((ioa >> 8) & 0xFFu);
|
||||
buf[2] = (uint8_t)((ioa >> 16) & 0xFFu);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:IOA 3 字节小端读取
|
||||
*/
|
||||
uint32_t iec104_get_ioa(const uint8_t *buf) {
|
||||
return (uint32_t)buf[0] | ((uint32_t)buf[1] << 8) | ((uint32_t)buf[2] << 16);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:IEEE754 短浮点 → 小端字节序(104 恒小端,宿主字节序无关)
|
||||
*/
|
||||
void iec104_put_float(uint8_t *buf, float v) {
|
||||
/* 位模式小端:memcpy 不假设宿主字节序,逐字节拆给目标(104 恒小端) */
|
||||
uint8_t tmp[sizeof(float)];
|
||||
uint32_t i;
|
||||
|
||||
(void)memcpy(tmp, &v, sizeof(tmp));
|
||||
for (i = 0u; i < sizeof(tmp); i++) {
|
||||
#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
|
||||
buf[i] = tmp[sizeof(tmp) - 1u - i];
|
||||
#else
|
||||
buf[i] = tmp[i];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:小端字节序 → IEEE754 短浮点
|
||||
*/
|
||||
float iec104_get_float(const uint8_t *buf) {
|
||||
uint8_t tmp[sizeof(float)];
|
||||
uint32_t i;
|
||||
float v;
|
||||
|
||||
for (i = 0u; i < sizeof(tmp); i++) {
|
||||
#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
|
||||
tmp[i] = buf[sizeof(tmp) - 1u - i];
|
||||
#else
|
||||
tmp[i] = buf[i];
|
||||
#endif
|
||||
}
|
||||
(void)memcpy(&v, tmp, sizeof(v));
|
||||
return v;
|
||||
}
|
||||
|
||||
/* ---------------- CP56Time2a ↔ epoch ---------------- */
|
||||
|
||||
/* 2000-01-01 00:00:00 UTC 的 epoch 毫秒(钳位下限) */
|
||||
#define EPOCH_2000_MS 946684800000ULL
|
||||
|
||||
/*
|
||||
* 函数功能:公历(y>=1)→ 距 1970-01-01 天数(Hinnant civil_from_days 正向)
|
||||
* 说明:纯整数算法;m 取 1..12。
|
||||
*/
|
||||
static uint32_t s_days_from_civil(uint32_t y, uint32_t m, uint32_t d) {
|
||||
uint32_t yy = y - ((m <= 2u) ? 1u : 0u);
|
||||
uint32_t era = yy / 400u;
|
||||
uint32_t yoe = yy - (era * 400u);
|
||||
uint32_t mp = (m > 2u) ? (m - 3u) : (m + 9u); /* 3 月 = 月序 0 */
|
||||
uint32_t doy = ((153u * mp) + 2u) / 5u + d - 1u;
|
||||
uint32_t doe = (yoe * 365u) + (yoe / 4u) - (yoe / 100u) + doy;
|
||||
|
||||
return (era * 146097u) + doe - 719468u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:距 1970-01-01 天数 → 公历(Hinnant 反向)
|
||||
* 入口参数:z_in = 天数;y/m/d = 输出
|
||||
*/
|
||||
static void s_civil_from_days(uint32_t z_in, uint32_t *y, uint32_t *m, uint32_t *d) {
|
||||
uint32_t z = z_in + 719468u;
|
||||
uint32_t era = z / 146097u;
|
||||
uint32_t doe = z - (era * 146097u);
|
||||
uint32_t yoe = (doe - (doe / 1460u) + (doe / 36524u) - (doe / 146096u)) / 365u;
|
||||
uint32_t yy = yoe + (era * 400u);
|
||||
uint32_t doy = doe - ((365u * yoe) + (yoe / 4u) - (yoe / 100u));
|
||||
uint32_t mp = ((5u * doy) + 2u) / 153u;
|
||||
uint32_t mon = mp + ((mp < 10u) ? 3u : 0u);
|
||||
|
||||
*d = doy - (((153u * mp) + 2u) / 5u) + 1u;
|
||||
if (mp >= 10u) {
|
||||
mon = mp - 9u;
|
||||
}
|
||||
*m = mon;
|
||||
*y = yy + ((mon <= 2u) ? 1u : 0u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:CP56Time2a → epoch 毫秒(UTC 分解)
|
||||
* 入口参数:buf = 7B;epoch_ms = 输出
|
||||
* 返 回 值:0 成功 / -2 IV(时间无效,调用方应拒绝)/ -1 字段越界
|
||||
*/
|
||||
int iec104_cp56_to_epoch_ms(const uint8_t *buf, uint64_t *epoch_ms) {
|
||||
uint32_t ms_min, minute, hour, day, mon, year;
|
||||
|
||||
if ((buf == NULL) || (epoch_ms == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
if ((buf[2] & 0x80u) != 0u) {
|
||||
return -2; /* IV:时间无效,调用方应拒绝 */
|
||||
}
|
||||
ms_min = (uint32_t)buf[0] | ((uint32_t)buf[1] << 8); /* 0..59999 */
|
||||
minute = (uint32_t)buf[2] & 0x3Fu;
|
||||
hour = (uint32_t)buf[3] & 0x1Fu;
|
||||
day = (uint32_t)buf[4] & 0x1Fu;
|
||||
mon = (uint32_t)buf[5] & 0x0Fu;
|
||||
year = 2000u + ((uint32_t)buf[6] & 0x7Fu);
|
||||
if ((day == 0u) || (day > 31u) || (mon == 0u) || (mon > 12u)
|
||||
|| (hour > 23u) || (minute > 59u) || (ms_min > 59999u)) {
|
||||
return -1;
|
||||
}
|
||||
*epoch_ms = ((uint64_t)s_days_from_civil(year, mon, day) * 86400000ULL)
|
||||
+ ((uint64_t)hour * 3600000ULL)
|
||||
+ ((uint64_t)minute * 60000ULL)
|
||||
+ (uint64_t)ms_min;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:epoch 毫秒 → CP56Time2a(钳位 [2000-01-01, 2127-12-31],IV 恒 0)
|
||||
* 返 回 值:固定 7(字节数)
|
||||
*/
|
||||
uint16_t iec104_epoch_ms_to_cp56(uint64_t epoch_ms, uint8_t *buf) {
|
||||
uint64_t days, rem, clamped;
|
||||
uint32_t y, m, d, ms_min;
|
||||
|
||||
clamped = (epoch_ms < EPOCH_2000_MS) ? EPOCH_2000_MS : epoch_ms;
|
||||
days = clamped / 86400000ULL;
|
||||
rem = clamped % 86400000ULL;
|
||||
s_civil_from_days((uint32_t)days, &y, &m, &d);
|
||||
if (y > 2127u) { /* 年字段 7bit 上限(2000+127) */
|
||||
y = 2127u;
|
||||
m = 12u;
|
||||
d = 31u;
|
||||
rem = 86399999ULL;
|
||||
}
|
||||
ms_min = (uint32_t)(rem % 60000ULL);
|
||||
buf[0] = (uint8_t)(ms_min & 0xFFu);
|
||||
buf[1] = (uint8_t)(ms_min >> 8);
|
||||
buf[2] = (uint8_t)((rem / 60000ULL) % 60ULL); /* min,IV=0 */
|
||||
buf[3] = (uint8_t)((rem / 3600000ULL) % 24ULL); /* hour */
|
||||
buf[4] = (uint8_t)(d & 0x1Fu); /* day */
|
||||
buf[5] = (uint8_t)(m & 0x0Fu); /* month */
|
||||
buf[6] = (uint8_t)((y - 2000u) & 0x7Fu); /* year */
|
||||
return 7u;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 ASDU 编解码(应用层,纯逻辑)
|
||||
* 模块功能:104 规约应用服务数据单元(ASDU)的类型/传送原因常量、组帧与解析。
|
||||
* **零 I/O、零平台依赖**(仅 stdint.h)→ 主机侧 L0.5 可离线单测
|
||||
* (登记簿 A-4 约束 1"协议解析与硬件 I/O 分离")。
|
||||
* 口径来源:docs/算法/继电保护功能复现规划.md §1.5(B0 产品定义)——
|
||||
* 公共地址 2 字节、传送原因 2 字节、信息体地址 IOA **3 字节**(101 为 2 字节);
|
||||
* 遥测上送**短浮点** M_ME_NC_1(系数 = 1);遥信双点上双点、其余单点;
|
||||
* 遥控**双点命令**(选择 82H / 执行 02H,即 QDC 的 S 位 + 值 2)。
|
||||
* 帧结构(本层只管 ASDU,APCI 6 字节头归 iec104_link):
|
||||
* [type 1B][vsq 1B][cot 2B][ca 2B]( [ioa 3B][元素...] )*n
|
||||
* 多信息体地址对象(SQ=0)每对象带独立 IOA;顺序对象(SQ=1)单 IOA + n 元素。
|
||||
* 创建日期:2026-10-12(B4,W1 功能主线,登记簿见 docs/任务规划/)
|
||||
*/
|
||||
#ifndef IEC104_ASDU_H
|
||||
#define IEC104_ASDU_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* ---------------- 类型标识(DL/T 634.5104,仅定义本期子集) ---------------- */
|
||||
#define IEC104_T_M_SP_NA_1 1U /* 单点遥信 SIQ */
|
||||
#define IEC104_T_M_DP_NA_1 3U /* 双点遥信 DIQ */
|
||||
#define IEC104_T_M_ME_NC_1 13U /* 遥测短浮点 float+QDS(§1.5 口径) */
|
||||
#define IEC104_T_C_SE_NC_1 50U /* 定值写(短浮点)float+QDS */
|
||||
#define IEC104_T_C_DC_NA_1 52U /* 双点遥控 QDC(82H 选 / 02H 执行) */
|
||||
#define IEC104_T_C_IC_NA_1 100U /* 总召 RQT */
|
||||
#define IEC104_T_C_RD_NA_1 102U /* 读单个测量值 IOA only */
|
||||
#define IEC104_T_C_CS_NA_1 103U /* 时钟同步 CP56Time2a */
|
||||
#define IEC104_T_C_TS_NA_1 104U /* 试验 FBP 2B */
|
||||
#define IEC104_T_C_RP_NA_1 105U /* 进程复位 RPA 1B */
|
||||
|
||||
/* ---------------- 传送原因 COT(低字节 6bit;高字节 T|PN|RBT/…) ---------- */
|
||||
#define IEC104_COT_PERIODIC 1U /* 周期/循环 */
|
||||
#define IEC104_COT_BG_SCAN 2U /* 背景扫描(总召响应) */
|
||||
#define IEC104_COT_SPONT 3U /* 突发(cos) */
|
||||
#define IEC104_COT_INIT 4U /* 初始化(总召 + RBT=1 时用) */
|
||||
#define IEC104_COT_REQ 5U /* 请求(C_RD 响应) */
|
||||
#define IEC104_COT_ACT 6U /* 激活 */
|
||||
#define IEC104_COT_ACTCON 7U /* 激活确认 */
|
||||
#define IEC104_COT_ACTTERM 8U /* 激活终止 */
|
||||
#define IEC104_COT_DEACT 9U /* deactivate */
|
||||
#define IEC104_COT_DEACTCON 10U /* 停止激活确认 */
|
||||
#define IEC104_COT_UNK_COT 12U /* 未知传送原因 */
|
||||
#define IEC104_COT_UNK_CA 13U /* 未知公共地址 */
|
||||
#define IEC104_COT_UNK_IOA 14U /* 未知信息体地址(+EVI) */
|
||||
#define IEC104_COT_BIT_T 0x80U /* COT 高字节 bit7:试验 */
|
||||
#define IEC104_COT_BIT_PN 0x40U /* COT 高字节 bit6:否定确认 */
|
||||
#define IEC104_COT_BIT_RBT 0x10U /* COT 高字节 bit4:请求链路仿真(总召初始化) */
|
||||
|
||||
/* ---------------- VSQ ---------------- */
|
||||
#define IEC104_VSQ_SQ 0x80U /* 1 = 顺序地址(单 IOA + n 元素) */
|
||||
#define IEC104_VSQ_N(n) ((uint8_t)((n) & 0x7FU))
|
||||
|
||||
/* ---------------- 遥信/遥测品质描述位(本期只用溢出/异常/非当前) --------- */
|
||||
#define IEC104_QDS_OV 0x01U
|
||||
#define IEC104_QDS_BL 0x10U /* blocked(闭锁) */
|
||||
#define IEC104_QDS_NT 0x20U /* not top(暂态品质,遥测未就绪用) */
|
||||
#define IEC104_QDS_IV 0x80U /* invalid(无效,通道不可用用) */
|
||||
#define IEC104_DIQ_IV 0x80U
|
||||
#define IEC104_SIQ_IV 0x80U
|
||||
|
||||
/* ---------------- 双点值(DIQ/QDC 低 2bit) ---------------- */
|
||||
#define IEC104_DPC_DONTCARE 0U
|
||||
#define IEC104_DPC_OFF 1U
|
||||
#define IEC104_DPC_ON 2U
|
||||
#define IEC104_QDC_S 0x80U /* 1 = 选择,0 = 执行(§1.5:82H/02H) */
|
||||
|
||||
/* ---------------- 总召 RQT ---------------- */
|
||||
#define IEC104_RQT_GROUP1 20U /* 总查询(组 1) */
|
||||
|
||||
/* ---------------- ASDU 头(解析产物) ---------------- */
|
||||
typedef struct {
|
||||
uint8_t type; /* 类型标识 */
|
||||
uint8_t vsq; /* 可变结构限定词 */
|
||||
uint8_t cot; /* 传送原因低 6bit */
|
||||
uint8_t cot_hi; /* COT 首字节的高位标志(T=0x80 / PN=0x40 / RBT=0x10 原样保留) */
|
||||
uint8_t oa; /* 源发站地址(第 2 COT 字节;bit7 = EVI) */
|
||||
uint16_t ca; /* 公共地址(链路地址 = ASDU 公共地址,2B) */
|
||||
const uint8_t *obj; /* 信息体区首址(指向调用方缓冲,生命周期同 on_rx) */
|
||||
uint16_t obj_len; /* 信息体区长度 */
|
||||
} iec104_asdu_hdr_t;
|
||||
|
||||
/*
|
||||
* 函数功能:解析 ASDU 头(不解析信息体——各命令的体解析在 iec104_link 内做)
|
||||
* 入口参数:buf - ASDU 首址(不含 APCI);len - ASDU 长度
|
||||
* hdr - 输出头
|
||||
* 返 回 值:0 成功 / -1 参数或长度错(len < 6)
|
||||
* 函数说明:纯只读,不改动 buf。
|
||||
*/
|
||||
int iec104_asdu_parse_hdr(const uint8_t *buf, uint16_t len, iec104_asdu_hdr_t *hdr);
|
||||
|
||||
/*
|
||||
* 函数功能:写 ASDU 头
|
||||
* 入口参数:buf - 目标缓冲(容量须 ≥ 6);type/vsq/cot/cot_hi/ca 见常量
|
||||
* 返 回 值:固定 6(已写字节数)
|
||||
*/
|
||||
uint16_t iec104_asdu_put_hdr(uint8_t *buf, uint8_t type, uint8_t vsq,
|
||||
uint8_t cot, uint8_t cot_hi, uint16_t ca);
|
||||
|
||||
/* ---------------- 小工具(IOA 3B 小端 / IEEE 短浮点小端) ---------------- */
|
||||
|
||||
/* IOA(3 字节小端)写入 */
|
||||
void iec104_put_ioa(uint8_t *buf, uint32_t ioa);
|
||||
/* IOA(3 字节小端)读取 */
|
||||
uint32_t iec104_get_ioa(const uint8_t *buf);
|
||||
/* IEEE754 float 按 104 小端写入(memcpy 位模式,不做字节序假设) */
|
||||
void iec104_put_float(uint8_t *buf, float v);
|
||||
/* IEEE754 float 按 104 小端读取 */
|
||||
float iec104_get_float(const uint8_t *buf);
|
||||
|
||||
/* ---------------- CP56Time2a(C_CS 时钟同步用) ---------------- */
|
||||
|
||||
/*
|
||||
* 函数功能:解析 CP56Time2a(7 字节)为 epoch 毫秒
|
||||
* 入口参数:buf - 7B 首址;epoch_ms - 输出(UTC 毫秒)
|
||||
* 返 回 值:0 成功 / -1 参数错 / -2 IV 位置起(时间无效,调用方应拒绝)
|
||||
* 函数说明:年字段仅 7bit(0..127,基准 2000);月/日/时/分按 1.. 语义;
|
||||
* 不处理 SU(夏令时)位——忽略。
|
||||
*/
|
||||
int iec104_cp56_to_epoch_ms(const uint8_t *buf, uint64_t *epoch_ms);
|
||||
|
||||
/*
|
||||
* 函数功能:epoch 毫秒 → CP56Time2a(7 字节,UTC 分解)
|
||||
* 入口参数:epoch_ms - UTC 毫秒;buf - 目标 7B
|
||||
* 返 回 值:固定 7
|
||||
* 函数说明:epoch_ms < 2000-01-01 时钳到 2000(年字段下限);IV 清 0。
|
||||
*/
|
||||
uint16_t iec104_epoch_ms_to_cp56(uint64_t epoch_ms, uint8_t *buf);
|
||||
|
||||
#endif /* IEC104_ASDU_H */
|
||||
@@ -0,0 +1,777 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 链路层(实现)
|
||||
* 模块功能:实现 iec104_link.h——APCI 流重组与 U/S/I 编解码、STARTDT/STOPDT/
|
||||
* TESTFR、k/w 窗与 t1/t2/t3(差值法,tick 回绕安全)、总召分片、
|
||||
* COV 突发(k 窗满暂存不丢)、周期上送、遥控/定值两段式、C_RD、
|
||||
* C_CS 对时、错误应答(COT 12/13/14 + EVI)。
|
||||
* 失败语义:坏帧/序号错/N(R) 越窗/缓冲溢出 → 实例置 IDLE(链路故障),
|
||||
* 由胶水层检测 IDLE 跳变断开 socket;本层不感知 I/O。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
*/
|
||||
#include "iec104_link.h"
|
||||
|
||||
#include "iec104_asdu.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/* ---------------- APCI 常量 ---------------- */
|
||||
#define APCI_START 0x68u
|
||||
#define APCI_CTRL_LEN 4u /* len 字段最小值(纯控制域) */
|
||||
#define APCI_LEN_MIN 4u
|
||||
#define APCI_LEN_MAX 252u
|
||||
#define SEQ_MOD 32768u /* 序号 15bit */
|
||||
|
||||
/* U 帧控制字节(DL/T 634.5104 表:低 2 位恒 11) */
|
||||
#define U_STARTDT_ACT 0x07u
|
||||
#define U_STARTDT_CON 0x0Bu
|
||||
#define U_STOPDT_ACT 0x13u
|
||||
#define U_STOPDT_CON 0x23u
|
||||
#define U_TESTFR_ACT 0x43u
|
||||
#define U_TESTFR_CON 0x83u
|
||||
|
||||
/* 本实现单 ASDU 信息体区上限(背景 YC 组 8×5=40 B 最大) */
|
||||
#define BODY_MAX 64u
|
||||
/* 周期上送周期(COT=1) */
|
||||
#define PERIOD_MS 60000UL
|
||||
|
||||
/*
|
||||
* 函数功能:超时判定(now 到达/越过 deadline,tick 回绕安全)
|
||||
* 返 回 值:1 已到期 / 0 未到期
|
||||
*/
|
||||
static int s_expired(uint32_t now, uint32_t deadline) {
|
||||
return (((int32_t)(now - deadline)) >= 0) ? 1 : 0;
|
||||
}
|
||||
|
||||
/* ---------------- 发送原语 ---------------- */
|
||||
|
||||
/*
|
||||
* 函数功能:发 U 帧(STARTDT/STOPDT/TESTFR act|con)
|
||||
* 说明:t1 只约束"需确认的发送"(标准语义)——TESTFR act 置挂起并起 t1;
|
||||
* STARTDT/STOPDT con 不启动 t1(主站可合法静默,靠 t3 探活)。
|
||||
*/
|
||||
static void s_send_u(iec104_link_t *lk, uint8_t q) {
|
||||
uint8_t buf[6];
|
||||
|
||||
if (lk->cfg->send_apdu == NULL) {
|
||||
return;
|
||||
}
|
||||
buf[0] = APCI_START;
|
||||
buf[1] = APCI_CTRL_LEN;
|
||||
buf[2] = q;
|
||||
buf[3] = 0u;
|
||||
buf[4] = 0u;
|
||||
buf[5] = 0u;
|
||||
lk->cfg->send_apdu(lk->ctx, buf, 6u);
|
||||
/* t1 只约束"需确认的发送"(标准语义):TESTFR act 等 con;
|
||||
* STARTDT/STOPDT con 不启动 t1——主站可合法静默,靠 t3 探活 */
|
||||
if (q == U_TESTFR_ACT) {
|
||||
lk->testfr_pending = 1u;
|
||||
lk->t1_deadline = lk->now_ms + IEC104_T1_MS;
|
||||
}
|
||||
lk->t3_deadline = lk->now_ms + IEC104_T3_MS;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:发 S 帧(确认已收序号,rx_unacked 清零)
|
||||
* 说明:S 不需对端确认 → 不起 t1,只刷 t3。
|
||||
*/
|
||||
static void s_send_s(iec104_link_t *lk) {
|
||||
uint8_t buf[6];
|
||||
uint16_t r = (uint16_t)(lk->rx_seq << 1);
|
||||
|
||||
if (lk->cfg->send_apdu == NULL) {
|
||||
return;
|
||||
}
|
||||
buf[0] = APCI_START;
|
||||
buf[1] = APCI_CTRL_LEN;
|
||||
buf[2] = 0x01u;
|
||||
buf[3] = 0u;
|
||||
buf[4] = (uint8_t)(r & 0xFFu);
|
||||
buf[5] = (uint8_t)(r >> 8);
|
||||
lk->cfg->send_apdu(lk->ctx, buf, 6u);
|
||||
lk->rx_unacked = 0u;
|
||||
lk->t3_deadline = lk->now_ms + IEC104_T3_MS; /* S 不需确认 → 不起 t1 */
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:发一个 I 帧(asdu 由调用方组好,N(S)/N(R) 由本层填)
|
||||
* 返 回 值:0 已发出 / -1 k 窗满或超长(调用方保留数据下次重试)
|
||||
*/
|
||||
static int s_send_i(iec104_link_t *lk, const uint8_t *asdu, uint16_t len) {
|
||||
uint8_t buf[6u + BODY_MAX];
|
||||
uint16_t s16;
|
||||
uint16_t r16;
|
||||
|
||||
if ((lk->cfg->send_apdu == NULL) || (len > BODY_MAX)) {
|
||||
return -1;
|
||||
}
|
||||
if (lk->tx_unacked >= IEC104_K_MAX) {
|
||||
return -1;
|
||||
}
|
||||
buf[0] = APCI_START;
|
||||
buf[1] = (uint8_t)(APCI_CTRL_LEN + len);
|
||||
s16 = (uint16_t)(lk->tx_seq << 1);
|
||||
r16 = (uint16_t)(lk->rx_seq << 1);
|
||||
buf[2] = (uint8_t)(s16 & 0xFFu);
|
||||
buf[3] = (uint8_t)(s16 >> 8);
|
||||
buf[4] = (uint8_t)(r16 & 0xFFu);
|
||||
buf[5] = (uint8_t)(r16 >> 8);
|
||||
(void)memcpy(&buf[6], asdu, len);
|
||||
lk->tx_seq = (uint16_t)((lk->tx_seq + 1u) % SEQ_MOD);
|
||||
lk->tx_unacked++;
|
||||
lk->cfg->send_apdu(lk->ctx, buf, (uint16_t)(6u + len));
|
||||
lk->t1_deadline = lk->now_ms + IEC104_T1_MS;
|
||||
lk->t3_deadline = lk->now_ms + IEC104_T3_MS;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:组 ASDU(hdr [+ ioa] + body)并发 I 帧
|
||||
* 说明:evi=1 → OA bit7(EVI,未知 IOA/类型/地址类应答)
|
||||
* 返 回 值:同 s_send_i
|
||||
*/
|
||||
static int s_send_asdu(iec104_link_t *lk, uint8_t type, uint8_t vsq,
|
||||
uint8_t cot, uint8_t cot_hi,
|
||||
uint8_t with_ioa, uint32_t ioa,
|
||||
const uint8_t *body, uint16_t body_len, uint8_t evi) {
|
||||
uint8_t buf[6u + BODY_MAX];
|
||||
uint16_t n;
|
||||
|
||||
n = iec104_asdu_put_hdr(buf, type, vsq, cot, cot_hi, lk->cfg->ca);
|
||||
if (evi != 0u) {
|
||||
buf[3] |= 0x80u; /* OA bit7 = EVI */
|
||||
}
|
||||
if (with_ioa != 0u) {
|
||||
iec104_put_ioa(&buf[n], ioa);
|
||||
n += 3u;
|
||||
}
|
||||
if (body_len > 0u) {
|
||||
if ((uint32_t)n + (uint32_t)body_len > (uint32_t)sizeof(buf)) {
|
||||
return -1;
|
||||
}
|
||||
(void)memcpy(&buf[n], body, body_len);
|
||||
n += body_len;
|
||||
}
|
||||
return s_send_i(lk, buf, n);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:命令应答(actcon/deactcon 等)——原样回显信息体(ioa+限定词)
|
||||
* 入口参数:pn = 1 置否定确认位
|
||||
*/
|
||||
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;
|
||||
}
|
||||
if (n > 0u) {
|
||||
(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);
|
||||
}
|
||||
|
||||
/* ---------------- 数据上送 ---------------- */
|
||||
|
||||
/*
|
||||
* 函数功能:单点上送遥测(M_ME_NC_1 SQ=0,值+QDS)
|
||||
* 返 回 值:同 s_send_i
|
||||
*/
|
||||
static int s_send_yc_single(iec104_link_t *lk, uint8_t idx, uint8_t cot) {
|
||||
uint8_t body[5];
|
||||
float v;
|
||||
uint8_t qds;
|
||||
|
||||
(void)iec104_pts_yc_read(idx, &v, &qds);
|
||||
iec104_put_float(body, v);
|
||||
body[4] = qds;
|
||||
return s_send_asdu(lk, IEC104_T_M_ME_NC_1, 1u, cot, 0u, 1u,
|
||||
iec104_pts_yc_ioa(idx), body, 5u, 0u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:总召/周期扫描一步(phase 游标 = interp_yx_idx:
|
||||
* 0 双点组→1 单点组→2 YC 组→3 频率→4 actterm)
|
||||
* 入口参数:budget = 本 poll 发送预算(成功一步减一);periodic = 周期扫描
|
||||
* 返 回 值:0 本步已发出 / -1 k 窗满原地保留(poll 循环必须就此停步,
|
||||
* 否则死循环——2026-10-12 单测抓到)
|
||||
*/
|
||||
static int s_scan_step(iec104_link_t *lk, uint8_t *budget, uint8_t periodic) {
|
||||
uint8_t body[BODY_MAX];
|
||||
uint8_t n = 0u;
|
||||
uint8_t i;
|
||||
uint8_t d;
|
||||
uint8_t qds;
|
||||
float v;
|
||||
int rc = -1;
|
||||
|
||||
switch (lk->interp_yx_idx) {
|
||||
case 0u: /* YX 双点组 SQ=1,IOA 1..4 */
|
||||
for (i = 0u; i < 4u; i++) {
|
||||
iec104_pts_yx_read(i, &d);
|
||||
body[n++] = d;
|
||||
}
|
||||
rc = s_send_asdu(lk, IEC104_T_M_DP_NA_1, (uint8_t)(IEC104_VSQ_SQ | 4u),
|
||||
lk->interp_cot, 0u, 1u, 1u, body, n, 0u);
|
||||
break;
|
||||
case 1u: /* YX 单点组 SQ=1,IOA 5..33 */
|
||||
for (i = 4u; i < 33u; i++) {
|
||||
iec104_pts_yx_read(i, &d);
|
||||
body[n++] = d;
|
||||
}
|
||||
rc = s_send_asdu(lk, IEC104_T_M_SP_NA_1, (uint8_t)(IEC104_VSQ_SQ | 29u),
|
||||
lk->interp_cot, 0u, 1u, 5u, body, n, 0u);
|
||||
break;
|
||||
case 2u: /* YC 组 SQ=1,IOA 0x4011..0x4018 */
|
||||
for (i = 0u; i < 8u; i++) {
|
||||
(void)iec104_pts_yc_read(i, &v, &qds);
|
||||
iec104_put_float(&body[n], v);
|
||||
n += 4u;
|
||||
body[n++] = qds;
|
||||
}
|
||||
rc = s_send_asdu(lk, IEC104_T_M_ME_NC_1, (uint8_t)(IEC104_VSQ_SQ | 8u),
|
||||
lk->interp_cot, 0u, 1u, 0x4011u, body, n, 0u);
|
||||
break;
|
||||
case 3u: /* 频率单点 SQ=0,IOA 0x401D */
|
||||
(void)iec104_pts_yc_read(8u, &v, &qds);
|
||||
iec104_put_float(&body[n], v);
|
||||
n += 4u;
|
||||
body[n++] = qds;
|
||||
rc = s_send_asdu(lk, IEC104_T_M_ME_NC_1, 1u, lk->interp_cot, 0u,
|
||||
1u, 0x401Du, body, n, 0u);
|
||||
break;
|
||||
default: /* actterm(仅总召;周期扫描不走这步) */
|
||||
iec104_put_ioa(&body[n], 0u);
|
||||
n += 3u;
|
||||
body[n++] = IEC104_RQT_GROUP1;
|
||||
rc = s_send_asdu(lk, IEC104_T_C_IC_NA_1, 1u, IEC104_COT_ACTTERM, 0u,
|
||||
0u, 0u, body, n, 0u);
|
||||
break;
|
||||
}
|
||||
if (rc != 0) {
|
||||
return -1; /* k 窗满:下次 poll 重试本 phase */
|
||||
}
|
||||
(*budget)--;
|
||||
lk->interp_yx_idx++;
|
||||
if (periodic != 0u) {
|
||||
if (lk->interp_yx_idx >= 4u) {
|
||||
lk->interp_step = 0u; /* 周期只发 YC(phase 2..3),无 actterm */
|
||||
}
|
||||
} else if (lk->interp_yx_idx >= 5u) {
|
||||
lk->interp_step = 0u;
|
||||
lk->init_pending = 0u;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:突发(COV)上送——pend 暂存逐点 SQ=0 COT=3,发成功才清位
|
||||
*/
|
||||
static void s_spont_step(iec104_link_t *lk, uint8_t *budget) {
|
||||
uint8_t body[5];
|
||||
uint8_t i;
|
||||
uint8_t d;
|
||||
int rc;
|
||||
|
||||
while ((*budget > 0u) && ((lk->pend_yx != 0u) || (lk->pend_yc != 0u))) {
|
||||
if (lk->pend_yx != 0u) {
|
||||
i = 0u;
|
||||
while (i < 33u) {
|
||||
if ((lk->pend_yx & (1uLL << i)) != 0u) {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
iec104_pts_yx_read(i, &d);
|
||||
body[0] = d;
|
||||
rc = s_send_asdu(lk,
|
||||
(i < 4u) ? IEC104_T_M_DP_NA_1
|
||||
: IEC104_T_M_SP_NA_1,
|
||||
1u, IEC104_COT_SPONT, 0u, 1u,
|
||||
(uint32_t)i + 1u, body, 1u, 0u);
|
||||
if (rc != 0) {
|
||||
return; /* k 窗满:pend 位保留,下次续发 */
|
||||
}
|
||||
lk->pend_yx &= ~(1uLL << i);
|
||||
} else {
|
||||
i = 0u;
|
||||
while (i < 16u) {
|
||||
if ((lk->pend_yc & (uint16_t)(1u << i)) != 0u) {
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
rc = s_send_yc_single(lk, i, IEC104_COT_SPONT);
|
||||
if (rc != 0) {
|
||||
return;
|
||||
}
|
||||
lk->pend_yc &= (uint16_t)~(1u << i);
|
||||
}
|
||||
(*budget)--;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 命令处理 ---------------- */
|
||||
|
||||
/*
|
||||
* 函数功能:C_IC_NA_1 总召处理(ACT → actcon + 启动扫描;DEACT → deactcon)
|
||||
* 说明:RBT(bit4)落在 COT 6bit 值域内,ACT+RBT 上收为 22——先剥再判;
|
||||
* 首召/RBT → COT=4(init),后续 COT=2(bg)
|
||||
*/
|
||||
static void s_h_ic(iec104_link_t *lk, const iec104_asdu_hdr_t *h) {
|
||||
/* RBT(bit4)落在 COT 6bit 值域内:ACT+RBT 上收为 22——先剥 RBT 再判值 */
|
||||
uint8_t cot_val = (uint8_t)(h->cot & 0xEFu);
|
||||
|
||||
if (cot_val == IEC104_COT_DEACT) {
|
||||
lk->interp_step = 0u;
|
||||
s_cmd_con(lk, IEC104_T_C_IC_NA_1, h->obj, h->obj_len,
|
||||
IEC104_COT_DEACTCON, 0u);
|
||||
return;
|
||||
}
|
||||
if (cot_val != IEC104_COT_ACT) {
|
||||
return;
|
||||
}
|
||||
if ((h->obj_len < 4u) || (h->obj[3] != IEC104_RQT_GROUP1)) {
|
||||
s_cmd_con(lk, IEC104_T_C_IC_NA_1, h->obj, h->obj_len,
|
||||
IEC104_COT_ACTCON, 1u);
|
||||
return;
|
||||
}
|
||||
s_cmd_con(lk, IEC104_T_C_IC_NA_1, h->obj, h->obj_len, IEC104_COT_ACTCON, 0u);
|
||||
lk->interp_step = 1u;
|
||||
lk->interp_yx_idx = 0u;
|
||||
lk->interp_cot = ((lk->init_pending != 0u)
|
||||
|| ((h->cot & IEC104_COT_BIT_RBT) != 0u))
|
||||
? (uint8_t)IEC104_COT_INIT : (uint8_t)IEC104_COT_BG_SCAN;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:C_CS_NA_1 对时(CP56 → epoch → cfg->clock_sync)
|
||||
* 说明:IV/字段坏/系统侧拒绝 → actcon + PN
|
||||
*/
|
||||
static void s_h_cs(iec104_link_t *lk, const iec104_asdu_hdr_t *h) {
|
||||
uint64_t epoch = 0u;
|
||||
int rc = -1;
|
||||
|
||||
if (h->obj_len >= 10u) {
|
||||
rc = iec104_cp56_to_epoch_ms(&h->obj[3], &epoch);
|
||||
if ((rc == 0) && (lk->cfg->clock_sync != NULL)) {
|
||||
rc = lk->cfg->clock_sync(lk->ctx, epoch);
|
||||
}
|
||||
}
|
||||
s_cmd_con(lk, IEC104_T_C_CS_NA_1, h->obj, h->obj_len, IEC104_COT_ACTCON,
|
||||
(rc == 0) ? 0u : 1u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:C_DC_NA_1 遥控两段式(选择记 ioa+dpc;执行须匹配,否则 PN)
|
||||
*/
|
||||
static void s_h_dc(iec104_link_t *lk, const iec104_asdu_hdr_t *h) {
|
||||
uint32_t ioa;
|
||||
uint8_t qdc;
|
||||
uint8_t sel;
|
||||
uint8_t dpc;
|
||||
int rc;
|
||||
|
||||
if (h->obj_len < 4u) {
|
||||
return;
|
||||
}
|
||||
ioa = iec104_get_ioa(h->obj);
|
||||
qdc = h->obj[3];
|
||||
sel = (uint8_t)(((qdc & IEC104_QDC_S) != 0u) ? 1u : 0u);
|
||||
dpc = (uint8_t)(qdc & 0x03u);
|
||||
if (sel != 0u) {
|
||||
rc = iec104_pts_yk_exec(ioa, dpc, 1u);
|
||||
if (rc == 0) {
|
||||
lk->yk_sel_ioa = ioa;
|
||||
lk->yk_sel_dpc = dpc;
|
||||
}
|
||||
} else {
|
||||
if ((lk->yk_sel_ioa == ioa) && (lk->yk_sel_dpc == dpc)) {
|
||||
rc = iec104_pts_yk_exec(ioa, dpc, 0u);
|
||||
} else {
|
||||
rc = -1; /* 无选择/失配 → 否定确认 */
|
||||
}
|
||||
lk->yk_sel_ioa = 0u;
|
||||
lk->yk_sel_dpc = 0u;
|
||||
}
|
||||
s_cmd_con(lk, IEC104_T_C_DC_NA_1, h->obj, h->obj_len, IEC104_COT_ACTCON,
|
||||
(rc == 0) ? 0u : 1u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:C_SE_NC_1 定值两段式写(选择段只校验;执行段落盘)
|
||||
* 说明:越界/未知地址 → actcon + PN
|
||||
*/
|
||||
static void s_h_se(iec104_link_t *lk, const iec104_asdu_hdr_t *h) {
|
||||
uint32_t addr;
|
||||
float v;
|
||||
uint8_t sel;
|
||||
int rc;
|
||||
|
||||
if (h->obj_len < 8u) {
|
||||
return;
|
||||
}
|
||||
addr = iec104_get_ioa(h->obj);
|
||||
v = iec104_get_float(&h->obj[3]);
|
||||
sel = (uint8_t)(((h->obj[7] & IEC104_QDC_S) != 0u) ? 1u : 0u);
|
||||
rc = iec104_pts_set_write(addr, v, sel);
|
||||
s_cmd_con(lk, IEC104_T_C_SE_NC_1, h->obj, h->obj_len, IEC104_COT_ACTCON,
|
||||
(rc == 0) ? 0u : 1u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:C_RD_NA_1 读单个信息体(YC 命中 → M_ME COT5;定值命中 → 同;
|
||||
* 未知 IOA → 原样回 C_RD + COT14 + EVI)
|
||||
*/
|
||||
static void s_h_rd(iec104_link_t *lk, const iec104_asdu_hdr_t *h) {
|
||||
uint32_t ioa;
|
||||
uint8_t i;
|
||||
float v;
|
||||
|
||||
if (h->obj_len < 3u) {
|
||||
return;
|
||||
}
|
||||
ioa = iec104_get_ioa(h->obj);
|
||||
for (i = 0u; i < iec104_pts_yc_count(); i++) {
|
||||
if (iec104_pts_yc_ioa(i) == ioa) {
|
||||
s_send_yc_single(lk, i, IEC104_COT_REQ);
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (iec104_pts_set_read(ioa, &v) == 0) {
|
||||
uint8_t body[5];
|
||||
|
||||
iec104_put_float(body, v);
|
||||
body[4] = 0u;
|
||||
(void)s_send_asdu(lk, IEC104_T_M_ME_NC_1, 1u, IEC104_COT_REQ, 0u,
|
||||
1u, ioa, body, 5u, 0u);
|
||||
return;
|
||||
}
|
||||
(void)s_send_asdu(lk, IEC104_T_C_RD_NA_1, 1u, IEC104_COT_UNK_IOA, 0u,
|
||||
1u, ioa, NULL, 0u, 1u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:ASDU 分派(公共地址校验 → 类型路由)
|
||||
* 说明:错 ca → COT13+EVI;未知类型 → COT12+EVI(原类型回)
|
||||
*/
|
||||
static void s_h_asdu(iec104_link_t *lk, const uint8_t *asdu, uint16_t len) {
|
||||
iec104_asdu_hdr_t h;
|
||||
|
||||
if (iec104_asdu_parse_hdr(asdu, len, &h) != 0) {
|
||||
return;
|
||||
}
|
||||
if (h.ca != lk->cfg->ca) {
|
||||
(void)s_send_asdu(lk, h.type, 1u, IEC104_COT_UNK_CA, 0u,
|
||||
0u, 0u, NULL, 0u, 1u);
|
||||
return;
|
||||
}
|
||||
switch (h.type) {
|
||||
case IEC104_T_C_IC_NA_1:
|
||||
s_h_ic(lk, &h);
|
||||
break;
|
||||
case IEC104_T_C_CS_NA_1:
|
||||
s_h_cs(lk, &h);
|
||||
break;
|
||||
case IEC104_T_C_DC_NA_1:
|
||||
s_h_dc(lk, &h);
|
||||
break;
|
||||
case IEC104_T_C_SE_NC_1:
|
||||
s_h_se(lk, &h);
|
||||
break;
|
||||
case IEC104_T_C_RD_NA_1:
|
||||
s_h_rd(lk, &h);
|
||||
break;
|
||||
case IEC104_T_C_TS_NA_1:
|
||||
s_cmd_con(lk, IEC104_T_C_TS_NA_1, h.obj, h.obj_len,
|
||||
IEC104_COT_ACTCON, 0u);
|
||||
break;
|
||||
case IEC104_T_C_RP_NA_1:
|
||||
iec104_pts_cov_check(NULL); /* 进程复位:重建快照(清历史) */
|
||||
s_cmd_con(lk, IEC104_T_C_RP_NA_1, h.obj, h.obj_len,
|
||||
IEC104_COT_ACTCON, 0u);
|
||||
break;
|
||||
default:
|
||||
(void)s_send_asdu(lk, h.type, 1u, IEC104_COT_UNK_COT, 0u,
|
||||
0u, 0u, NULL, 0u, 1u);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- APDU 分派 ---------------- */
|
||||
|
||||
/*
|
||||
* 函数功能:U 帧处理(STARTDT/STOPDT act → con + 态迁移;TESTFR act → con;
|
||||
* TESTFR con → 清挂起)
|
||||
*/
|
||||
static void s_h_u(iec104_link_t *lk, uint8_t q) {
|
||||
switch (q) {
|
||||
case U_STARTDT_ACT:
|
||||
s_send_u(lk, U_STARTDT_CON);
|
||||
lk->state = IEC104_ST_RUN;
|
||||
lk->interp_step = 0u;
|
||||
lk->yk_sel_ioa = 0u;
|
||||
break;
|
||||
case U_STOPDT_ACT:
|
||||
s_send_u(lk, U_STOPDT_CON);
|
||||
lk->state = IEC104_ST_STOPPED;
|
||||
lk->interp_step = 0u;
|
||||
break;
|
||||
case U_TESTFR_ACT:
|
||||
s_send_u(lk, U_TESTFR_CON);
|
||||
break;
|
||||
case U_TESTFR_CON:
|
||||
lk->testfr_pending = 0u;
|
||||
break;
|
||||
default:
|
||||
break; /* con/未知 U:t1/t3 已由外层刷新 */
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:N(R) 校验并更新 tx_unacked(V(A) ≤ V(A)' ≤ V(S),即 0 ≤ d ≤ 窗内)
|
||||
* 说明:N(R) 倒退(早于上次确认)= 协议违例 → 链路故障(标准语义);
|
||||
* tx_unacked 恒 ≤ k(发送侧门控),故 d ≤ tx_unacked 已蕴含窗校验
|
||||
* 返 回 值:0 / -1 违例
|
||||
*/
|
||||
static int s_apply_nr(iec104_link_t *lk, uint16_t nr) {
|
||||
uint16_t d = (uint16_t)(((uint32_t)lk->tx_seq + SEQ_MOD - (uint32_t)nr)
|
||||
% SEQ_MOD);
|
||||
|
||||
if ((uint32_t)d > (uint32_t)lk->tx_unacked) {
|
||||
return -1;
|
||||
}
|
||||
lk->tx_unacked = d;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:单个完整 APDU 分派(U/S/I);任何接收只刷 t3(不刷 t1)
|
||||
* 说明:OPEN 态只认 U;I 帧 N(S) 不连续 → IDLE;RUN 态才处理 ASDU;
|
||||
* 收满 w 个未确认 → 回 S
|
||||
*/
|
||||
static void s_h_apdu(iec104_link_t *lk, const uint8_t *p, uint16_t total) {
|
||||
const uint8_t *c = &p[2]; /* 控制域 4B */
|
||||
uint16_t ns;
|
||||
uint16_t nr;
|
||||
|
||||
lk->t3_deadline = lk->now_ms + IEC104_T3_MS;
|
||||
if ((c[0] & 0x03u) == 0x03u) {
|
||||
s_h_u(lk, c[0]);
|
||||
return;
|
||||
}
|
||||
if (lk->state == IEC104_ST_OPEN) {
|
||||
return; /* OPEN 态只认 U 帧 */
|
||||
}
|
||||
if ((c[0] & 0x01u) != 0u) { /* S 格式 */
|
||||
nr = (uint16_t)(((uint16_t)c[2] | ((uint16_t)c[3] << 8)) >> 1);
|
||||
if (s_apply_nr(lk, nr) != 0) {
|
||||
lk->state = IEC104_ST_IDLE;
|
||||
}
|
||||
return;
|
||||
}
|
||||
/* I 格式 */
|
||||
ns = (uint16_t)(((uint16_t)c[0] | ((uint16_t)c[1] << 8)) >> 1);
|
||||
nr = (uint16_t)(((uint16_t)c[2] | ((uint16_t)c[3] << 8)) >> 1);
|
||||
if (ns != lk->rx_seq) {
|
||||
lk->state = IEC104_ST_IDLE; /* 序号错 → 链路故障 */
|
||||
return;
|
||||
}
|
||||
if (s_apply_nr(lk, nr) != 0) {
|
||||
lk->state = IEC104_ST_IDLE;
|
||||
return;
|
||||
}
|
||||
lk->rx_seq = (uint16_t)((ns + 1u) % SEQ_MOD);
|
||||
lk->rx_unacked++;
|
||||
if (lk->rx_unacked == 1u) {
|
||||
lk->t2_deadline = lk->now_ms + IEC104_T2_MS;
|
||||
}
|
||||
if (lk->state == IEC104_ST_RUN) {
|
||||
s_h_asdu(lk, &p[6], (uint16_t)(total - 6u));
|
||||
}
|
||||
if (lk->rx_unacked >= IEC104_W_MAX) {
|
||||
s_send_s(lk);
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------- 公开接口 ---------------- */
|
||||
|
||||
/*
|
||||
* 函数功能:链路实例初始化(全清零 + 绑配置与上下文,态 = IDLE)
|
||||
*/
|
||||
void iec104_link_init(iec104_link_t *lk, const iec104_link_cfg_t *cfg,
|
||||
void *ctx) {
|
||||
(void)memset(lk, 0, sizeof(*lk));
|
||||
lk->cfg = cfg;
|
||||
lk->ctx = ctx;
|
||||
lk->state = IEC104_ST_IDLE;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:TCP 连接建立 → OPEN(序号/窗口/定时/暂存全部复位,init_pending=1)
|
||||
*/
|
||||
void iec104_link_open(iec104_link_t *lk, uint32_t now_ms) {
|
||||
lk->state = IEC104_ST_OPEN;
|
||||
lk->tx_seq = 0u;
|
||||
lk->rx_seq = 0u;
|
||||
lk->tx_unacked = 0u;
|
||||
lk->rx_unacked = 0u;
|
||||
lk->testfr_pending = 0u;
|
||||
lk->init_pending = 1u;
|
||||
lk->rx_len = 0u;
|
||||
lk->yk_sel_ioa = 0u;
|
||||
lk->yk_sel_dpc = 0u;
|
||||
lk->interp_step = 0u;
|
||||
lk->interp_yx_idx = 0u;
|
||||
lk->pend_yx = 0u;
|
||||
lk->pend_yc = 0u;
|
||||
lk->now_ms = now_ms;
|
||||
lk->t1_deadline = now_ms + IEC104_T1_MS;
|
||||
lk->t2_deadline = now_ms + IEC104_T2_MS;
|
||||
lk->t3_deadline = now_ms + IEC104_T3_MS;
|
||||
lk->period_next = now_ms + PERIOD_MS;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:链路断开/故障 → IDLE(清重组缓冲与所有暂存)
|
||||
*/
|
||||
void iec104_link_close(iec104_link_t *lk) {
|
||||
lk->state = IEC104_ST_IDLE;
|
||||
lk->rx_len = 0u;
|
||||
lk->interp_step = 0u;
|
||||
lk->pend_yx = 0u;
|
||||
lk->pend_yc = 0u;
|
||||
lk->yk_sel_ioa = 0u;
|
||||
lk->yk_sel_dpc = 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:喂入收到的原始字节(APCI 流重组:半帧/粘帧逐处)
|
||||
* 说明:坏起始符/坏长度/缓冲溢出 → IDLE(胶水层检测后断开 socket)
|
||||
*/
|
||||
void iec104_link_on_rx(iec104_link_t *lk, const uint8_t *data, uint16_t len,
|
||||
uint32_t now_ms) {
|
||||
lk->now_ms = now_ms;
|
||||
if ((lk->state == IEC104_ST_IDLE) || (data == NULL)) {
|
||||
return;
|
||||
}
|
||||
if ((uint32_t)len > (uint32_t)(IEC104_RXBUF_SZ - lk->rx_len)) {
|
||||
lk->state = IEC104_ST_IDLE; /* 重组缓冲溢出 = 坏流 */
|
||||
lk->rx_len = 0u;
|
||||
return;
|
||||
}
|
||||
(void)memcpy(&lk->rx_buf[lk->rx_len], data, len);
|
||||
lk->rx_len = (uint16_t)(lk->rx_len + len);
|
||||
|
||||
while (lk->rx_len >= 2u) {
|
||||
uint16_t lf;
|
||||
uint16_t need;
|
||||
|
||||
if (lk->rx_buf[0] != APCI_START) {
|
||||
lk->state = IEC104_ST_IDLE;
|
||||
break;
|
||||
}
|
||||
lf = lk->rx_buf[1];
|
||||
if ((lf < APCI_LEN_MIN) || (lf > APCI_LEN_MAX)) {
|
||||
lk->state = IEC104_ST_IDLE;
|
||||
break;
|
||||
}
|
||||
need = (uint16_t)(lf + 2u);
|
||||
if (lk->rx_len < need) {
|
||||
break; /* 半帧:等下次 on_rx */
|
||||
}
|
||||
s_h_apdu(lk, lk->rx_buf, need);
|
||||
if (lk->state == IEC104_ST_IDLE) {
|
||||
break;
|
||||
}
|
||||
if (need < lk->rx_len) {
|
||||
(void)memmove(lk->rx_buf, &lk->rx_buf[need],
|
||||
(uint16_t)(lk->rx_len - need));
|
||||
}
|
||||
lk->rx_len = (uint16_t)(lk->rx_len - need);
|
||||
}
|
||||
if (lk->state == IEC104_ST_IDLE) {
|
||||
lk->rx_len = 0u;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:周期节拍(胶水层每次服务循环调用一次,时基由调用方注入)
|
||||
* 说明:t1 超时 → close;t3 到期 → TESTFR act;t2 到期 → S;
|
||||
* RUN 态按优先级:总召续扫 > 周期续扫 > COV 突发 > 周期触发
|
||||
*/
|
||||
void iec104_link_poll(iec104_link_t *lk, uint32_t now_ms) {
|
||||
uint8_t budget = IEC104_POLL_BUDGET;
|
||||
|
||||
lk->now_ms = now_ms;
|
||||
if ((lk->state != IEC104_ST_RUN) && (lk->state != IEC104_ST_STOPPED)) {
|
||||
return;
|
||||
}
|
||||
if (((lk->tx_unacked > 0u) || (lk->testfr_pending != 0u))
|
||||
&& (s_expired(now_ms, lk->t1_deadline) != 0)) {
|
||||
iec104_link_close(lk); /* t1:未确认 I/未答 TESTFR 超时 = 故障 */
|
||||
return;
|
||||
}
|
||||
if (s_expired(now_ms, lk->t3_deadline) != 0) {
|
||||
s_send_u(lk, U_TESTFR_ACT);
|
||||
}
|
||||
if ((lk->rx_unacked > 0u) && (s_expired(now_ms, lk->t2_deadline) != 0)) {
|
||||
s_send_s(lk);
|
||||
}
|
||||
if (lk->state != IEC104_ST_RUN) {
|
||||
return;
|
||||
}
|
||||
if (lk->interp_step == 1u) {
|
||||
while ((budget > 0u) && (lk->interp_step == 1u)) {
|
||||
if (s_scan_step(lk, &budget, 0u) != 0) {
|
||||
break; /* k 窗满:本 poll 停步(防死循环) */
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (lk->interp_step == 3u) {
|
||||
while ((budget > 0u) && (lk->interp_step == 3u)) {
|
||||
if (s_scan_step(lk, &budget, 1u) != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ((lk->pend_yx != 0u) || (lk->pend_yc != 0u)) {
|
||||
s_spont_step(lk, &budget);
|
||||
if (budget == 0u) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (s_expired(now_ms, lk->period_next) != 0) {
|
||||
lk->interp_step = 3u;
|
||||
lk->interp_yx_idx = 2u; /* 周期只扫 YC(phase 2..3) */
|
||||
lk->interp_cot = IEC104_COT_PERIODIC;
|
||||
lk->period_next = now_ms + PERIOD_MS;
|
||||
while ((budget > 0u) && (lk->interp_step == 3u)) {
|
||||
if (s_scan_step(lk, &budget, 1u) != 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
{
|
||||
iec104_pts_cov_t cov;
|
||||
|
||||
iec104_pts_cov_check(&cov);
|
||||
lk->pend_yx |= cov.yx_changed;
|
||||
lk->pend_yc |= cov.yc_changed;
|
||||
s_spont_step(lk, &budget);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:查询链路状态(胶水层检测 IDLE 跳变以断开/重建 socket)
|
||||
*/
|
||||
iec104_state_t iec104_link_state(const iec104_link_t *lk) {
|
||||
return lk->state;
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 链路层(APCI + 状态机,纯逻辑)
|
||||
* 模块功能:TCP 流 → APDU 重组、U/S/I 帧编解码、STARTDT/STOPDT/TESTFR、
|
||||
* k/w 流量窗与 t1/t2/t3 超时、发送序号 N(S)/N(R) 管理,以及应用层
|
||||
* 流程编排:总召(act→背景扫描→actterm,首召 RBT 用 COT=4)、
|
||||
* 突发上送(COV,COT=3)、周期上送(COT=1)、读单个(C_RD→COT=5)、
|
||||
* 双点遥控两段式(选择/执行,失配否定确认 PN)、定值读(C_RD)/
|
||||
* 定值写(C_SE_NC 选择=范围校验 / 执行=写入+封存)、时钟同步(C_CS
|
||||
* → 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)。
|
||||
* 参数(DL/T 634.5104 标准默认,§1.5 未另定):k=12 w=8 t1=15s t2=10s t3=20s。
|
||||
* 端口:TCP **2404**(§1.5 B0 定义;本 DTU = 被控站/服务器,主站连入)。
|
||||
* 线程约定:单实例单任务上下文(板端 = 104 服务在 netTask 消息循环内驱动,
|
||||
* 见 iec104_serv 胶水层);非线程安全,禁止多任务共享一实例。
|
||||
* 失败语义:send_apdu 内**不得阻塞**;发送失败由胶水层断链(主站会重连),
|
||||
* 本层不感知发送失败。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
*/
|
||||
#ifndef IEC104_LINK_H
|
||||
#define IEC104_LINK_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#include "iec104_pts.h"
|
||||
|
||||
/* 服务端口(§1.5:以太网 TCP 2404) */
|
||||
#define IEC104_PORT 2404U
|
||||
|
||||
/* 链路参数(标准默认值) */
|
||||
#define IEC104_K_MAX 12U /* 未确认 I 帧上限 */
|
||||
#define IEC104_W_MAX 8U /* 收到 I 帧未发确认的强制 S 阈值 */
|
||||
#define IEC104_T1_MS 15000UL /* 发送/确认超时(超时 = 链路故障) */
|
||||
#define IEC104_T2_MS 10000UL /* 无 I 发送时强制 S 确认超时(< t1) */
|
||||
#define IEC104_T3_MS 20000UL /* 空闲发 TESTFR 周期(< t1) */
|
||||
|
||||
/* TCP 流重组缓冲:APDU 上限 253 + 半帧余量 */
|
||||
#define IEC104_RXBUF_SZ 512U
|
||||
|
||||
/* 单次 poll 发送预算(ASDU 数)——防总召大扫描饿死宿主任务 */
|
||||
#define IEC104_POLL_BUDGET 4U
|
||||
|
||||
/* 链路状态 */
|
||||
typedef enum {
|
||||
IEC104_ST_IDLE = 0, /* TCP 未建立(或已断开) */
|
||||
IEC104_ST_OPEN, /* TCP 已建立,等 STARTDT act */
|
||||
IEC104_ST_RUN, /* STARTDT 已确认,数据服务开 */
|
||||
IEC104_ST_STOPPED /* STOPDT 已确认:停数据,保 S/TESTFR */
|
||||
} iec104_state_t;
|
||||
|
||||
/* 配置与回调(ctx 原样回传给回调) */
|
||||
typedef struct {
|
||||
uint16_t ca; /* 公共地址 = 链路地址(2B,§1.5) */
|
||||
/* 发一个完整 APDU(含 6B APCI 头)。不得阻塞;生命周期仅回调内有效 */
|
||||
void (*send_apdu)(void *ctx, const uint8_t *apdu, uint16_t len);
|
||||
/* C_CS 时钟同步:epoch 毫秒;NULL 或返回 -1 → 否定确认 */
|
||||
int (*clock_sync)(void *ctx, uint64_t epoch_ms);
|
||||
} iec104_link_cfg_t;
|
||||
|
||||
/* 链路实例(胶水层静态持有,零堆) */
|
||||
typedef struct {
|
||||
const iec104_link_cfg_t *cfg;
|
||||
void *ctx;
|
||||
iec104_state_t state;
|
||||
uint8_t init_pending; /* 1 = 下次总召按初始化(COT=4 + RBT) */
|
||||
/* 序号(0..32767,帧内 ×2) */
|
||||
uint16_t tx_seq;
|
||||
uint16_t rx_seq;
|
||||
uint16_t tx_unacked; /* 已发 I 未获认(k 窗) */
|
||||
uint16_t rx_unacked; /* 已收 I 未确认(w 阈) */
|
||||
uint8_t testfr_pending; /* 1 = TESTFR act 未获 con(t1 同样约束它) */
|
||||
/* 定时器到期时刻(now_ms 域;差值法判超时,回绕安全) */
|
||||
uint32_t t1_deadline;
|
||||
uint32_t t2_deadline;
|
||||
uint32_t t3_deadline;
|
||||
uint32_t now_ms;
|
||||
/* TCP 流重组 */
|
||||
uint8_t rx_buf[IEC104_RXBUF_SZ];
|
||||
uint16_t rx_len;
|
||||
/* 总召进度:0 无 / 1 背景扫描中 / 2 待发 actterm */
|
||||
uint8_t interp_step;
|
||||
uint8_t interp_yc_idx; /* 背景扫描 YC 游标 */
|
||||
uint8_t interp_yx_idx; /* 背景扫描 YX 游标 */
|
||||
uint8_t interp_cot; /* 本次总召用的 COT(BG_SCAN 或 INIT) */
|
||||
/* 遥控选择暂存(执行段比对) */
|
||||
uint32_t yk_sel_ioa; /* 0 = 无选择 */
|
||||
uint8_t yk_sel_dpc;
|
||||
/* 周期上送(COT=1)下次触发时刻 */
|
||||
uint32_t period_next;
|
||||
/* COV 待发暂存(k 窗满时保留到下次 poll,变化不许丢) */
|
||||
uint64_t pend_yx;
|
||||
uint16_t pend_yc;
|
||||
} iec104_link_t;
|
||||
|
||||
/*
|
||||
* 函数功能:初始化实例(置 IDLE、清序号;不发送任何帧)
|
||||
* 入口参数:lk - 实例;cfg/ctx - 配置与回调上下文
|
||||
*/
|
||||
void iec104_link_init(iec104_link_t *lk, const iec104_link_cfg_t *cfg, void *ctx);
|
||||
|
||||
/*
|
||||
* 函数功能:TCP 连接建立(新连接或重连后调用)
|
||||
* 入口参数:now_ms - 当前时基
|
||||
* 函数说明:序号清零、init_pending 置位(首召 = 初始化总召)、进 OPEN。
|
||||
*/
|
||||
void iec104_link_open(iec104_link_t *lk, uint32_t now_ms);
|
||||
|
||||
/*
|
||||
* 函数功能:TCP 断开(对端断开或胶水层发送失败主动断链时调用)
|
||||
* 函数说明:回 IDLE、清重组缓冲与选择暂存;不发送任何帧。
|
||||
*/
|
||||
void iec104_link_close(iec104_link_t *lk);
|
||||
|
||||
/*
|
||||
* 函数功能:喂入 TCP 收到的流片段(任意切分/粘帧)
|
||||
* 入口参数:data/len - 流片段;now_ms - 当前时基
|
||||
* 函数说明:内部重组,凑齐 APDU 即处理;坏帧(起始符/长度非法)→ 视同链路
|
||||
* 故障,实例回 IDLE(胶水层检测到 IDLE 跳变应断开 socket)。
|
||||
* 返回后实例仍为 RUN 不代表对端合法——仅表示未触发断链条件。
|
||||
*/
|
||||
void iec104_link_on_rx(iec104_link_t *lk, const uint8_t *data, uint16_t len,
|
||||
uint32_t now_ms);
|
||||
|
||||
/*
|
||||
* 函数功能:周期驱动(定时器 + COV + 总召分片 + 周期上送)
|
||||
* 入口参数:now_ms - 当前时基(建议 ≤100 ms 调一次)
|
||||
* 函数说明:仅 RUN 态产生数据帧;STOPPED 态只处理 t2/t3(S/TESTFR)。
|
||||
* 单次调用发送量 ≤ IEC104_POLL_BUDGET 个 ASDU。
|
||||
*/
|
||||
void iec104_link_poll(iec104_link_t *lk, uint32_t now_ms);
|
||||
|
||||
/* 当前状态(日志/看门狗用) */
|
||||
iec104_state_t iec104_link_state(const iec104_link_t *lk);
|
||||
|
||||
#endif /* IEC104_LINK_H */
|
||||
@@ -0,0 +1,437 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 点表与数据源适配(实现)
|
||||
* 模块功能:实现 iec104_pts.h——YC/YX/YK/定值点表(ROM)、板端默认源
|
||||
* (phasor / prot_elm / prot_set)、COV 快照比对(遥信变位 +
|
||||
* 遥测死区)。点表口径见 iec104_pts.h 文件头与设计文档 §3。
|
||||
* 线程约定:init/bind 建服务前;其余仅 104 服务任务上下文(快照非原子)。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
*/
|
||||
#include "iec104_pts.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
#include "iec104_asdu.h" /* QDS/DIQ 品质位常量 */
|
||||
#include "phasor.h" /* 板端源:遥测 = 基波有效值 */
|
||||
#include "prot_elm.h" /* 板端源:遥信 = 元件状态 */
|
||||
#include "prot_set.h" /* 板端源:定值 + 闭锁 */
|
||||
#include "adc_input.h" /* adc_semantic_t(phasor 实例按语义绑定) */
|
||||
|
||||
/* ---------------- YC 表(§1.5 点号 4011+;死区 = 拟定码值,L1-5 后复核) -- */
|
||||
typedef struct {
|
||||
uint32_t ioa;
|
||||
int8_t sem; /* 绑定的 phasor 语义通道(-1 = 无 phasor 源,恒无效) */
|
||||
float deadband; /* 死区(工程量绝对值) */
|
||||
} pts_yc_def_t;
|
||||
|
||||
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 */
|
||||
};
|
||||
#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];
|
||||
|
||||
/* ---------------- YX 表(IOA = 1..33;双点 = 开关位置类) ------------------ */
|
||||
#define YX_N 33u
|
||||
#define YX_DP_N 4u /* idx 0..3 = 双点(开关位置,V0.1 无硬件 → IV) */
|
||||
|
||||
/* 有效位图:YX15~18(idx14~17)+ YX23(idx22)+ YX31(idx30) */
|
||||
#define YX_VALID_MASK (((1uLL << 14u) | (1uLL << 15u) | (1uLL << 16u) \
|
||||
| (1uLL << 17u) | (1uLL << 22u) | (1uLL << 30u)))
|
||||
|
||||
/* ---------------- 数据源(默认 = 板端;bind_src 可整体替换) -------------- */
|
||||
static int s_src_yc_read(uint8_t idx, float *out);
|
||||
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);
|
||||
|
||||
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
|
||||
};
|
||||
static const iec104_pts_src_t *s_src;
|
||||
|
||||
/* ---------------- COV 快照 ---------------- */
|
||||
static float s_yc_last[YC_N];
|
||||
static uint16_t s_yc_have; /* bit i = 已有上次快照 */
|
||||
static uint64_t s_yx_last;
|
||||
static uint8_t s_yx_snap_ok;
|
||||
|
||||
/* ============================ 公开接口 ================================== */
|
||||
|
||||
/*
|
||||
* 函数功能:初始化点表模块(扫 phasor 基波实例映射、恢复默认源、清快照)
|
||||
*/
|
||||
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;
|
||||
s_yx_snap_ok = 0u;
|
||||
s_yx_last = 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:替换数据源(NULL = 恢复板端默认源;单测注入假源)
|
||||
*/
|
||||
void iec104_pts_bind_src(const iec104_pts_src_t *src) {
|
||||
s_src = (src != NULL) ? src : &k_default_src;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:遥测点数
|
||||
*/
|
||||
uint8_t iec104_pts_yc_count(void) {
|
||||
return YC_N;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:遥测点 IOA(按下标;越界 → 0)
|
||||
*/
|
||||
uint32_t iec104_pts_yc_ioa(uint8_t idx) {
|
||||
return (idx < YC_N) ? k_yc[idx].ioa : 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读遥测(值 + 品质 descriptor;源 -2 → NT,其余失败 → IV)
|
||||
* 返 回 值:0 有效 / -1 无效
|
||||
*/
|
||||
int iec104_pts_yc_read(uint8_t idx, float *v, uint8_t *qds) {
|
||||
int rc;
|
||||
|
||||
if (v != NULL) {
|
||||
*v = 0.0f;
|
||||
}
|
||||
if ((idx >= YC_N) || (s_src == NULL) || (s_src->yc_read == NULL)) {
|
||||
if (qds != NULL) {
|
||||
*qds = IEC104_QDS_IV;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
rc = s_src->yc_read(idx, v);
|
||||
if (rc == 0) {
|
||||
if (qds != NULL) {
|
||||
*qds = 0u;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (v != NULL) {
|
||||
*v = 0.0f;
|
||||
}
|
||||
if (qds != NULL) {
|
||||
*qds = (rc == -2) ? IEC104_QDS_NT : IEC104_QDS_IV;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:遥信点数
|
||||
*/
|
||||
uint8_t iec104_pts_yx_count(void) {
|
||||
return (uint8_t)YX_N;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:遥信点 IOA(idx + 1;越界 → 0)
|
||||
*/
|
||||
uint32_t iec104_pts_yx_ioa(uint8_t idx) {
|
||||
return ((uint8_t)idx < YX_N) ? ((uint32_t)idx + 1u) : 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:该遥信是否双点型(idx < 4 = 开关位置类)
|
||||
*/
|
||||
uint8_t iec104_pts_yx_is_dp(uint8_t idx) {
|
||||
return ((uint8_t)idx < YX_DP_N) ? 1u : 0u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读遥信(无效通道 → DIQ_IV;双点 → DPC 编码)
|
||||
*/
|
||||
void iec104_pts_yx_read(uint8_t idx, uint8_t *out) {
|
||||
uint64_t bits;
|
||||
uint64_t valid;
|
||||
uint8_t on;
|
||||
|
||||
if (out == NULL) {
|
||||
return;
|
||||
}
|
||||
if ((idx >= YX_N) || (s_src == NULL) || (s_src->yx_bits == NULL)
|
||||
|| (s_src->yx_valid == NULL)) {
|
||||
*out = IEC104_DIQ_IV;
|
||||
return;
|
||||
}
|
||||
valid = s_src->yx_valid();
|
||||
if (((valid >> idx) & 1u) == 0u) {
|
||||
*out = IEC104_DIQ_IV;
|
||||
return;
|
||||
}
|
||||
bits = s_src->yx_bits();
|
||||
on = (uint8_t)((bits >> idx) & 1u);
|
||||
if (iec104_pts_yx_is_dp(idx) != 0u) {
|
||||
*out = (on != 0u) ? (uint8_t)IEC104_DPC_ON : (uint8_t)IEC104_DPC_OFF;
|
||||
} else {
|
||||
*out = on;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:COV 快照比对(遥信 = 有效位翻转;遥测 = 死区)
|
||||
* 入口参数:cov = 输出变位位图(NULL = 只刷快照)
|
||||
*/
|
||||
void iec104_pts_cov_check(iec104_pts_cov_t *cov) {
|
||||
iec104_pts_cov_t local;
|
||||
|
||||
(void)memset(&local, 0, sizeof(local));
|
||||
|
||||
/* YX:变位 = 有效位上的位翻转(无效位不参与,避免 IV 通道抖动成"变位") */
|
||||
if ((s_src != NULL) && (s_src->yx_bits != NULL)
|
||||
&& (s_src->yx_valid != NULL)) {
|
||||
uint64_t bits = s_src->yx_bits() & s_src->yx_valid();
|
||||
|
||||
if (s_yx_snap_ok != 0u) {
|
||||
local.yx_changed = bits ^ s_yx_last;
|
||||
}
|
||||
s_yx_last = bits;
|
||||
s_yx_snap_ok = 1u;
|
||||
}
|
||||
|
||||
/* YC:死区比对(仅对"本次有效 且 上次有快照"的点判定) */
|
||||
if ((s_src != NULL) && (s_src->yc_read != NULL)) {
|
||||
uint8_t i;
|
||||
|
||||
for (i = 0u; i < YC_N; i++) {
|
||||
float v;
|
||||
|
||||
if (s_src->yc_read(i, &v) != 0) {
|
||||
continue;
|
||||
}
|
||||
if (((s_yc_have >> i) & 1u) != 0u) {
|
||||
float d = v - s_yc_last[i];
|
||||
|
||||
if (d < 0.0f) {
|
||||
d = -d;
|
||||
}
|
||||
if (d > k_yc[i].deadband) {
|
||||
local.yc_changed |= (uint16_t)(1u << i);
|
||||
}
|
||||
}
|
||||
s_yc_last[i] = v;
|
||||
s_yc_have |= (uint16_t)(1u << i);
|
||||
}
|
||||
}
|
||||
|
||||
if (cov != NULL) {
|
||||
*cov = local;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:遥控两段式(转发数据源;select=1 选择段 / 0 执行段)
|
||||
* 返 回 值:0 接受 / -1 拒绝(调用方回 PN)
|
||||
*/
|
||||
int iec104_pts_yk_exec(uint32_t ioa, uint8_t dpc, uint8_t select) {
|
||||
if ((s_src == NULL) || (s_src->yk_exec == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
return s_src->yk_exec(ioa, dpc, select);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按通讯地址读定值(工程量浮点)
|
||||
* 返 回 值:0 / -1 未知地址
|
||||
*/
|
||||
int iec104_pts_set_read(uint32_t addr, float *out) {
|
||||
if ((s_src == NULL) || (s_src->set_read == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
return s_src->set_read(addr, out);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:按通讯地址写定值(select=1 只校验;0 执行写入)
|
||||
* 返 回 值:0 / -1 越界或未知地址
|
||||
*/
|
||||
int iec104_pts_set_write(uint32_t addr, float v, uint8_t select) {
|
||||
if ((s_src == NULL) || (s_src->set_write == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
return s_src->set_write(addr, v, select);
|
||||
}
|
||||
|
||||
/* ============================ 板端默认源 ================================= */
|
||||
|
||||
/*
|
||||
* 函数功能:板端默认源 - 遥测 = 基波相量有效值(fAmSQ)
|
||||
* 返 回 值:0 / -1 无 phasor 源(削顶通道/频率未实现)/ -2 未满一周波(NT)
|
||||
*/
|
||||
static int s_src_yc_read(uint8_t idx, float *out) {
|
||||
phasor_t p;
|
||||
int8_t pi;
|
||||
|
||||
if ((idx >= YC_N) || (out == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
pi = s_yc_phasor_idx[idx];
|
||||
if (pi < 0) {
|
||||
return -1; /* 无 phasor 源(削顶通道/频率未实现) */
|
||||
}
|
||||
if (phasor_get((uint8_t)pi, &p) != 0) {
|
||||
return -2; /* 未满一周波:未就绪(NT) */
|
||||
}
|
||||
*out = p.fAmSQ; /* 有效值口径(系数已折算) */
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:板端默认源 - 遥信位图(保护元件动作态 + 定值闭锁)
|
||||
*/
|
||||
static uint64_t s_src_yx_bits(void) {
|
||||
prot_elm_state_t oc1;
|
||||
prot_elm_state_t oc2;
|
||||
prot_elm_state_t oc3;
|
||||
prot_elm_state_t ol;
|
||||
prot_elm_state_t u0;
|
||||
uint64_t bits = 0u;
|
||||
|
||||
prot_elm_state(PE_OC1, &oc1);
|
||||
prot_elm_state(PE_OC2, &oc2);
|
||||
prot_elm_state(PE_OC3, &oc3);
|
||||
prot_elm_state(PE_OL, &ol);
|
||||
prot_elm_state(PE_3U0, &u0);
|
||||
if ((oc1.asserted != 0u) || (oc2.asserted != 0u) || (oc3.asserted != 0u)
|
||||
|| (u0.asserted != 0u)) {
|
||||
bits |= (1uLL << 14u); /* YX15 故障总 */
|
||||
}
|
||||
if (oc1.asserted != 0u) {
|
||||
bits |= (1uLL << 15u); /* YX16 过流 I 段 */
|
||||
}
|
||||
if (oc2.asserted != 0u) {
|
||||
bits |= (1uLL << 16u); /* YX17 过流 II 段 */
|
||||
}
|
||||
if (oc3.asserted != 0u) {
|
||||
bits |= (1uLL << 17u); /* YX18 过流 III 段 */
|
||||
}
|
||||
if (ol.asserted != 0u) {
|
||||
bits |= (1uLL << 22u); /* YX23 过载 */
|
||||
}
|
||||
if (prot_set_locked() != 0u) {
|
||||
bits |= (1uLL << 30u); /* YX31 保护闭锁 */
|
||||
}
|
||||
return bits;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:板端默认源 - 遥信有效位图(V0.1 = YX15~18/23/31)
|
||||
*/
|
||||
static uint64_t s_src_yx_valid(void) {
|
||||
return YX_VALID_MASK;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:板端默认源 - 遥控(YK11 复归 → prot_elm_request_reset;
|
||||
* YK12 全局软压板 → PS_GLOBAL_EN 两段式转真,B7)
|
||||
* 返 回 值:0 / -1 无出口硬件或未知 IOA(YK1~4/13/14 负向确认)
|
||||
* 函数说明:本回调跑在 iec104Task 上下文,**不得**直接 prot_elm_init()
|
||||
* (与 protectTask 的 eval 竞态)→ 落请求,下一拍 eval 头部消费(B5)。
|
||||
* YK12 走 prot_set_set_sealed(原子写+封存,B7);protectTask 下一拍
|
||||
* 读到压板态即复位元件并停判据(protect_task.c)。
|
||||
*/
|
||||
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();
|
||||
}
|
||||
return 0;
|
||||
case 0x600Bu: /* YK12 全局软压板(PS_GLOBAL_EN)两段式 */
|
||||
if ((dpc != (uint8_t)IEC104_DPC_ON) && (dpc != (uint8_t)IEC104_DPC_OFF)) {
|
||||
return -1; /* 非法双点命令:选择段即拒 */
|
||||
}
|
||||
if (select != 0u) {
|
||||
return 0; /* 选择段 = 只校验 */
|
||||
}
|
||||
return prot_set_set_sealed(PS_GLOBAL_EN,
|
||||
(dpc == (uint8_t)IEC104_DPC_ON)
|
||||
? PROT_SET_Q10(1) : PROT_SET_Q10(0));
|
||||
default:
|
||||
return -1; /* 其余 YK:无出口硬件/无对应能力 → 拒绝 */
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:板端默认源 - 定值读(扫 g_prot_set_meta 通讯地址映射)
|
||||
* 返 回 值:0 / -1 未知地址(本地项 comm_addr=0 不可寻址,B7 起显式排除)
|
||||
*/
|
||||
static int s_src_set_read(uint32_t addr, float *out) {
|
||||
uint32_t i;
|
||||
|
||||
if (out == NULL) {
|
||||
return -1;
|
||||
}
|
||||
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)) {
|
||||
*out = prot_set_get_f((prot_set_id_t)i);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:板端默认源 - 定值写(Q10 范围校验;执行段 prot_set_set_sealed
|
||||
* 原子写+封存,B7 收口 B5 竞态窗口;置脏 → 104 任务通知落盘)
|
||||
* 返 回 值:0 / -1 越界或未知地址(本地项 comm_addr=0 不可寻址)
|
||||
*/
|
||||
static int s_src_set_write(uint32_t addr, float v, uint8_t select) {
|
||||
uint32_t i;
|
||||
int32_t q10;
|
||||
|
||||
for (i = 0u; i < (uint32_t)PS_ID_COUNT; i++) {
|
||||
const prot_set_meta_t *meta = &g_prot_set_meta[i];
|
||||
|
||||
if ((meta->comm_addr == 0u) || (meta->comm_addr != (uint16_t)addr)) {
|
||||
continue;
|
||||
}
|
||||
q10 = PROT_SET_Q10(v);
|
||||
if ((q10 < meta->min) || (q10 > meta->max)) {
|
||||
return -1; /* 越界:选择段即拒(执行段同样拒) */
|
||||
}
|
||||
if (select != 0u) {
|
||||
return 0; /* 选择段 = 只做范围校验 */
|
||||
}
|
||||
return prot_set_set_sealed((prot_set_id_t)i, q10);
|
||||
}
|
||||
return -1; /* 未知地址 */
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 点表与数据源适配(纯逻辑 + 源注入)
|
||||
* 模块功能:104 点表(遥测 YC / 遥信 YX / 遥控 YK / 定值)的定义、读取与变化检测
|
||||
* (COV:遥信变位 + 遥测死区)。**数据源经 vtable 注入**:板端绑
|
||||
* phasor/prot_elm/prot_set 真实源;主机单测绑假源 → 协议栈全链
|
||||
* (asdu+pts+link)可离线跑(登记簿 A-4 约束 1)。
|
||||
* 点表口径(docs/算法/继电保护功能复现规划.md §1.5,验收 = 省电科院点表):
|
||||
* - YC:IOA 0x4011~0x4018 + 0x401d(频率),短浮点、系数 1;
|
||||
* ⚠ V0.1 有效通道仅 UA/UN/IA(TPAFE 三路,§1.6)——其余点**存在但置 IV**
|
||||
* (无效位),频率随 L1-7 落地前同样置 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)——
|
||||
* YK11 信号复归(→ prot_elm_request_reset)与 YK12 全局软压板
|
||||
* (→ PS_GLOBAL_EN 两段式转真,B7)转真执行,其余一律否定确认(COT|PN),如实回;
|
||||
* - 定值:IOA = prot_set 元数据 comm_addr(0x8820 基址 + 告警段 0x8027~0x802A;
|
||||
* comm_addr=0 本地项不可寻址),
|
||||
* 读经 C_RD、写经 C_SE_NC(选择/执行两段,执行段原子写+封存并置脏,
|
||||
* 持久化 /prot_set.bin 由 defaultTask 执行,B7);
|
||||
* - 固有参数 8001H~800AH:本期**不实现**(字符串类无法用短浮点上送),
|
||||
* 按未知 IOA(COT 14 + EVI)应答,登记为待办(设计文档)。
|
||||
* 线程约定:init/bind 内核启动前或建服务前;cov_check/read 仅 104 服务任务上下文。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
*/
|
||||
#ifndef IEC104_PTS_H
|
||||
#define IEC104_PTS_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
/* ---------------- 数据源 vtable(NULL 项 = 该能力不可用 → 按无效/拒绝处理) -- */
|
||||
typedef struct {
|
||||
/* 遥测读:idx = YC 表下标;out = 工程量浮点。
|
||||
* 返回 0 有效 / -1 无效(发 IV)/ -2 数据未就绪(发 NT,如相量未满一周波) */
|
||||
int (*yc_read)(uint8_t idx, float *out);
|
||||
/* 遥信位图:bit i = YX 表下标 i 的"合/动作"态(双点按 ON 计) */
|
||||
uint64_t (*yx_bits)(void);
|
||||
/* 遥信有效位图:bit i = 1 该点有效;0 → 发 IV(无硬件/未实现) */
|
||||
uint64_t (*yx_valid)(void);
|
||||
/* 遥控:select=1 选择段 / 0 执行段(dpc 已剥 S 位)。返回 0 接受 / -1 拒绝 */
|
||||
int (*yk_exec)(uint32_t ioa, uint8_t dpc, uint8_t select);
|
||||
/* 定值读:addr = 通讯地址;返回 0 存在 / -1 未知地址 */
|
||||
int (*set_read)(uint32_t addr, float *out);
|
||||
/* 定值写:select=1 预置校验(范围)/ 0 执行写入。返回 0 / -1 越界或未知 */
|
||||
int (*set_write)(uint32_t addr, float v, uint8_t select);
|
||||
} iec104_pts_src_t;
|
||||
|
||||
/* ---------------- 变化检测结果(cov_check 输出) ---------------- */
|
||||
typedef struct {
|
||||
uint64_t yx_changed; /* bit i = YX 下标 i 变位(相对上次快照) */
|
||||
uint16_t yc_changed; /* bit i = YC 下标 i 越过死区(相对上次快照) */
|
||||
} iec104_pts_cov_t;
|
||||
|
||||
/*
|
||||
* 函数功能:点表层初始化(清快照、绑板端默认源)
|
||||
* 限定条件:内核启动前或 104 服务启动前,调一次
|
||||
*/
|
||||
void iec104_pts_init(void);
|
||||
|
||||
/*
|
||||
* 函数功能:覆盖数据源(主机单测注假源)
|
||||
* 入口参数:src - vtable(浅拷贝,调用方保生命周期);NULL = 恢复板端默认源
|
||||
*/
|
||||
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 */
|
||||
int iec104_pts_yc_read(uint8_t idx, float *v, uint8_t *qds);
|
||||
|
||||
/* ---------------- YX ---------------- */
|
||||
uint8_t iec104_pts_yx_count(void);
|
||||
uint32_t iec104_pts_yx_ioa(uint8_t idx);
|
||||
uint8_t iec104_pts_yx_is_dp(uint8_t idx); /* 1 = 双点(M_DP) */
|
||||
/* 读 YX:out = DIQ/SIQ 值(含 IV)。 */
|
||||
void iec104_pts_yx_read(uint8_t idx, uint8_t *out);
|
||||
|
||||
/* ---------------- COV(每 poll 一次) ---------------- */
|
||||
/*
|
||||
* 函数功能:读全表快照并与上次比对(变位 + 死区),输出变化集
|
||||
* 入口参数:cov - 输出(可为 NULL = 只刷快照不取变化)
|
||||
* 函数说明:首次调用只建快照、变化集全 0(突发由初始化/总召覆盖)。
|
||||
* 死区 = 点表内置绝对值(§1.5 运行参数 8020H 段本期取内置默认)。
|
||||
*/
|
||||
void iec104_pts_cov_check(iec104_pts_cov_t *cov);
|
||||
|
||||
/* ---------------- 遥控 ---------------- */
|
||||
/* 遥控两段式(转发 src->yk_exec;无源 = 一律拒绝)。返回 0 接受 / -1 拒绝 */
|
||||
int iec104_pts_yk_exec(uint32_t ioa, uint8_t dpc, uint8_t select);
|
||||
|
||||
/* ---------------- 定值 ---------------- */
|
||||
/* 按通讯地址读定值(转发 src->set_read)。返回 0 / -1 未知地址 */
|
||||
int iec104_pts_set_read(uint32_t addr, float *out);
|
||||
/* 按通讯地址写定值(两段)。返回 0 / -1 */
|
||||
int iec104_pts_set_write(uint32_t addr, float v, uint8_t select);
|
||||
|
||||
#endif /* IEC104_PTS_H */
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
| `default_task.c/h` | defaultTask | Normal | FTL + FatFS 挂载、配置引擎属主、syslog 唯一写者、LED 指示 |
|
||||
| `net_task.c/h` | netTask | Normal | `net_stack_bring_up` → 置 NET_READY → 循环 `net_poll` + `net_process_messages` |
|
||||
| `ui_task.c/h` | uiTask | BelowNormal | LVGL 界面(菜单 / 内容页 / 待机)+ 推流 |
|
||||
| `adc_task.c/h` | (ADC 任务) | — | ADC 采集消费:TPAFE5160 快照 + STM32 ADC1 顺序扫描,经 [`../algo/adc_input.c`](../algo/README.md) **按语义**取数(采样节拍由 TIM6 硬件触发) |
|
||||
| `adc_task.c/h` | adcTask | AboveNormal | ADC 采集消费:TPAFE5160 快照 + 消费 STM32 ADC1 DMA 乒乓缓冲一帧,经 [`../algo/adc_input.c`](../algo/README.md) **按语义**取数(采样节拍由 TIM3 硬件触发) |
|
||||
| `rs485_task.c/h` | (RS-485 任务) | — | 半双工 RS-485 协议通信 |
|
||||
| `../sync/time_sync.c` | timeSyncTask | Normal | 墙钟时间同步(实现不在本目录,见 [`../sync/README.md`](../sync/README.md)) |
|
||||
|
||||
|
||||
+303
-32
@@ -1,15 +1,20 @@
|
||||
/*
|
||||
* 模块名称:ADC Sampling Task
|
||||
* 模块功能:ADC 采集消费——TPAFE5160 快照 + STM32 ADC1 顺序扫描(经 L1 层
|
||||
* App/algo/adc_input.c 按语义取数)+ 帧统计(节拍由 TIM6 硬件定时器提供)
|
||||
* 模块功能:ADC 采集消费 + L2 相量计算——TPAFE5160 快照 + STM32 ADC1 DMA 乒乓帧消费
|
||||
* (经 L1 层 App/algo/adc_input.c 按语义取数)→ 压入采样环
|
||||
* (App/algo/sample_ring.c)→ 全周傅氏相量重算(App/algo/phasor.c)
|
||||
* + 帧统计(节拍由 TIM3 硬件定时器提供)
|
||||
* 状态说明:**B1 已落地(2026-10-10)**——本任务从空桩转为真实消费者:
|
||||
* 用 tpafe5160_snapshot() 原子取数(AGENTS 铁律),统计帧数/无新帧次数,
|
||||
* 并纳入看门狗 required_mask(TASK_WD_ADC)。
|
||||
* **采样节拍(v2.2,2026-10-10)**:由 **TIM6 更新中断(1 kHz)** 硬件触发——
|
||||
* **采样节拍(v2.2,2026-10-10)**:由 **TIM3 更新中断(1 kHz)** 硬件触发——
|
||||
* ISR 内调 tpafe5160_start_conv_irq()(Src/main.c 的
|
||||
* HAL_TIM_PeriodElapsedCallback);本任务**不再自行触发**,退化为**纯消费者**,
|
||||
* 1ms 轮询(osDelayUntil)仅作消费节奏。详见
|
||||
* docs/子系统设计/ADC采样节拍设计说明.md。
|
||||
* HAL_TIM_PeriodElapsedCallback);本任务**不再自行触发**,退化为**纯消费者**。
|
||||
* **消费唤醒(v2.7,2026-10-10,Task #32 P1-5 方案 a)**:由 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——
|
||||
* 本任务每拍调 adc_input_poll()(TPAFE 快照 + STM32 ADC1 9 通道扫描),
|
||||
* 经 adc_input_raw() **按语义**读通道(L1-3/L1-4),不再直接触碰驱动缓冲。
|
||||
@@ -30,32 +35,183 @@
|
||||
* 为真后落日志。写者未注册时控制台走无锁直发,与
|
||||
* defaultTask 的存储初始化日志字节级交错(台架实测);
|
||||
* 就绪前挂起上报,就绪后补报最近一帧。
|
||||
* v2.2 2026-10-10 王建锋 采样节拍改由 TIM6 更新中断(1 kHz)硬件触发;本任务
|
||||
* v2.2 2026-10-10 王建锋 采样节拍改由 TIM3 更新中断(1 kHz)硬件触发;本任务
|
||||
* 删除自行触发,退化为纯消费者(见文件头"采样节拍")。
|
||||
* v2.3 2026-10-10 王建锋 L1-3/L1-4:取数改走 App/algo/adc_input.c(语义映射 +
|
||||
* STM32 ADC1 9 通道扫描);首帧日志补全 8 路电气量 + 温度,
|
||||
* 心跳日志增 adc1_rc / adc1_err 观测 STM32 侧健康度。
|
||||
* v2.4 2026-10-10 王建锋 L2:接入相量层——每拍把 9 路语义值压入采样环
|
||||
* (sample_ring)并重算全部相量实例(phasor_run);
|
||||
* 攒满一周波后**首拍即落一次相量日志**(开机证据,不等
|
||||
* 10 s 心跳),心跳日志增 ring / skip 计数与各实例相量
|
||||
* (Re/Im/有效值,×0.1 定点打印,因 MicroLIB 的 snprintf
|
||||
* 无 %f)。
|
||||
* ⚠ L1-5 通道标定未实现(缺标准源)→ 暂以**原始码值**
|
||||
* 充作工程量(系数 = 1),故相量读数只证明**链路通**,
|
||||
* 不可作幅值精度判据(同 L1-3 的替代口径)。
|
||||
* v2.5 2026-10-10 王建锋 Task #32 的 P2 测量埋点:单拍加常开耗时探针
|
||||
* (task_prof ADC_BEAT),用于判定本任务是否超拍
|
||||
* (名义节拍 1 ms = 168000 周期)而挤占低优先级任务
|
||||
* v2.6 2026-10-10 王建锋 Task #32 的 P0 修复(两处):
|
||||
* §5 无新帧仍压环 → 改为**仅 r == 1 时**压环 + 重算
|
||||
* (消除重复采样污染 20 点 DFT 窗的系统性误差);
|
||||
* §4.3 osDelayUntil 返回值被丢弃 → 落后时一次性重同步
|
||||
* (原写法会永久落后 → 不再阻塞 → 空转抢占 uiTask)
|
||||
* v2.7 2026-10-10 王建锋 Task #32 P1-5 方案 a(事件驱动消费):
|
||||
* 消费唤醒 osDelayUntil(1ms) → osThreadFlagsWait(ADC_WAKE_FLAG,
|
||||
* osWaitForever),标志由 TIM3 ISR 经 adc_tick_irq_notify() 置位;
|
||||
* 删除 ADC_TASK_BEAT_MS 与 next(P0-2 的重同步因此不再需要);
|
||||
* 新增"消费者就绪"门 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 双缓冲
|
||||
* 消费,adc_input_poll() 不再忙等;心跳日志增
|
||||
* adc1_seq / adc1_hw(adc1_err 语义改为"本拍无新帧")
|
||||
* v2.9 2026-10-11 王建锋 L1-6 采样有效性校验:每拍调 adc_health_check()
|
||||
* (App/algo/adc_health.c,纯逻辑判定:静默/丢帧/
|
||||
* 单调性/错误计数增量)——严重事件与故障电平上升沿
|
||||
* 打 DBG_ERROR + 每类每次开机一次 SYSLOG_SELFTEST;
|
||||
* 恢复打 DBG_INFO;心跳日志增 tgap(TPAFE 侧丢帧,
|
||||
* 与 adc1_gap 对称)。判定不在本文件(A-4:算法与
|
||||
* I/O 分离),本文件只做"消费计数 → 告警"的编排
|
||||
*/
|
||||
|
||||
#include "adc_task.h"
|
||||
#include "cmsis_os.h"
|
||||
#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 "adc_health.h" /* L1-6:采样有效性校验(静默/丢帧/单调性/错误增量) */
|
||||
#include "tpafe5160.h"
|
||||
#include "task_watchdog.h"
|
||||
#include "console_log.h"
|
||||
#include "sys_log.h" /* SYSLOG_SELFTEST:采样故障留痕(仅故障才写) */
|
||||
#include "task_prof.h" /* P2 常开探针:单拍耗时(Task #32) */
|
||||
|
||||
#define DBG_TAG "[ADC_TASK]"
|
||||
#include "dbg_log.h"
|
||||
|
||||
/* 消费轮询周期(ms)。采样节拍由 TIM6(1 kHz)提供,本周期仅作消费节奏 */
|
||||
#define ADC_TASK_BEAT_MS 1U
|
||||
/* 心跳日志周期(ms):周期性上报帧统计,作为采集链路存活的观测面 */
|
||||
/* 心跳日志周期(ms):周期性上报帧统计,作为采集链路存活的观测面。
|
||||
* (原 ADC_TASK_BEAT_MS = 1U 的消费轮询周期已随 v2.7 事件驱动改造删除——
|
||||
* 消费节拍改由 TIM3 ISR 置位 ADC_WAKE_FLAG 提供,与生产同源,不再自定周期) */
|
||||
#define ADC_TASK_REPORT_MS 10000U
|
||||
|
||||
/* 统计:已消费帧数 / 无新帧次数(首拍必为无新帧,之后应稳定为 0 增量)。
|
||||
* 原始码值缓冲由 L1 层 App/algo/adc_input.c 持有(消费侧按语义取数,不写死下标) */
|
||||
static uint32_t s_frames_consumed;
|
||||
static uint32_t s_frames_idle;
|
||||
/* 因通道尚未首次成功而**整拍跳过入环**的次数(不向相量窗注入 0 点) */
|
||||
static uint32_t s_frames_skip;
|
||||
|
||||
/* L1-6 告警编排状态:
|
||||
* s_health_prev - 上次电平故障位(判上升/下降沿,避免每拍重复告警)
|
||||
* s_health_logged- 已写 SYSLOG_SELFTEST 的位(每类每次开机只留痕一次,
|
||||
* 防持续故障刷屏把 32KB selftest.log 写满) */
|
||||
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;
|
||||
|
||||
/* 消费者就绪门:由 adcTask 自己在进入等待前置 1;单写者 = 本任务,ISR 只读。
|
||||
* ⚠ 为什么必须有这道门:TIM3 在 app_main_init()(Src/main.c:140)里就
|
||||
* HAL_TIM_Base_Start_IT 启动,**远早于** osKernelInitialize()/MX_FREERTOS_Init()/
|
||||
* osKernelStart()(Src/main.c:144/145/148)→ "节拍源已在跑"与"消费者已存在"之间有
|
||||
* 约 100 ms 窗口,其间 adcTaskHandle 还是 NULL、调度器也还没起。
|
||||
* osThreadFlagsSet() 对 NULL 句柄本身会安全返回 osErrorParameter(cmsis_os2.c 的入参
|
||||
* 检查),但"句柄已赋值、调度器未启动"这段(freertos.c 建任务 → osKernelStart)仍会让
|
||||
* ISR 版内核 API 在调度器外跑。本仓库对这类坑有实测记录:Src/main.c 的 TIM7 分支注释
|
||||
* (2026-10-02,port.c 优先级断言把系统卡死在 ISR 内)。就绪门把这条时序变成**显式约束**,
|
||||
* 而不是依赖 FreeRTOS 内部实现细节。 */
|
||||
static volatile uint8_t s_consumer_ready;
|
||||
|
||||
/*
|
||||
* 函数功能:TIM3 采样节拍的 ISR 入口——置位唤醒 adcTask 消费一拍
|
||||
* 入口参数:无
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅 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 时
|
||||
* 多个脉冲合并成一次唤醒,下一轮 adc_input_poll() 返回 r==0 记 idle,不会重复处理同一帧
|
||||
*/
|
||||
void adc_tick_irq_notify(void) {
|
||||
if (s_consumer_ready == 0u) {
|
||||
return;
|
||||
}
|
||||
(void)osThreadFlagsSet(adcTaskHandle, ADC_WAKE_FLAG);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:打印全部相量实例(×0.1 定点)
|
||||
* 入口参数:tag - 前缀标记("first" 首次产出 / "hb" 周期心跳)
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅任务上下文;调用方须保证 dbg_console_writer_ready() 为真
|
||||
* 函数说明:MicroLIB 的 snprintf **不支持 %f** → 以 ×0.1 定点整数打印
|
||||
* (打印值 = 实际值 × 10,分辨率 0.1)。逐实例数据驱动输出,
|
||||
* 实例表加行即自动覆盖。⚠ L1-5 标定未实现 → 读数单位是**原始码值**。
|
||||
*/
|
||||
static void log_phasors(const char *tag) {
|
||||
uint8_t k;
|
||||
|
||||
for (k = 0u; k < phasor_inst_count(); k++) {
|
||||
const phasor_inst_t *d = phasor_inst(k);
|
||||
phasor_t p;
|
||||
|
||||
if ((d != NULL) && (phasor_get(k, &p) == 0)) {
|
||||
DBG_INFO("phasor(%s) %s: Re=%ld Im=%ld rms=%ld (x0.1)",
|
||||
tag, d->name,
|
||||
(long)(p.fRe * 10.0f),
|
||||
(long)(p.fIm * 10.0f),
|
||||
(long)(p.fAmSQ * 10.0f));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:L1-6 判定结果的告警编排(边沿 → 日志 + 自检留痕)
|
||||
* 入口参数:hs - adc_health_check() 判定结果快照
|
||||
* 返 回 值:无
|
||||
* 限定条件:仅任务上下文;日志仍受写者就绪门控(SYSLOG 不受——内核前有
|
||||
* s_preboot 缓冲,故障痕迹不因开机窗口丢失)
|
||||
* 函数说明:策略分级(判定本身不在本文件,见 App/algo/adc_health.c)——
|
||||
* ① 严重事件(FRSTDATA / HAL 错误 / STM32 停摆计数增量):这三类
|
||||
* 计数增量恒应为 0,出现即故障 → 每次 DBG_ERROR;
|
||||
* ② 电平故障上升沿(静默/序号倒退)→ DBG_ERROR;下降沿(静默自愈)
|
||||
* → DBG_INFO("恢复"必须可见,否则只看到一次错误无从判断是否还在);
|
||||
* ③ SYSLOG_SELFTEST 每类每次开机**只留痕一次**(持续故障刷屏会把
|
||||
* 32KB selftest.log 写满,也淹没其它模块的故障记录);
|
||||
* ④ gap(丢帧)**不告警**:消费者超拍是已知现象(心跳日志
|
||||
* adc1_gap / tgap 即观测面),告警会淹没真故障。
|
||||
*/
|
||||
static void adc_health_report(const adc_health_status_t *hs) {
|
||||
uint32_t serious = hs->events & (ADC_HE_FRSTDATA | ADC_HE_STM32_ERR | ADC_HE_HW_ERR);
|
||||
uint32_t rise = hs->flags & ~s_health_prev;
|
||||
uint32_t fall = s_health_prev & ~hs->flags;
|
||||
|
||||
if ((serious != 0u) && (dbg_console_writer_ready() != 0u)) {
|
||||
DBG_ERROR("sampling fault: events=0x%lx sticky=0x%lx",
|
||||
(unsigned long)hs->events, (unsigned long)hs->sticky);
|
||||
}
|
||||
if ((rise != 0u) && (dbg_console_writer_ready() != 0u)) {
|
||||
DBG_ERROR("health: flags=0x%lx stall_ms=%lu tgap=%lu",
|
||||
(unsigned long)hs->flags,
|
||||
(unsigned long)hs->tpafe_stall_ms,
|
||||
(unsigned long)hs->tpafe_gaps);
|
||||
}
|
||||
if (((serious | rise) & ~s_health_logged) != 0u) {
|
||||
(void)syslog_post(SYSLOG_SELFTEST, "adc sampling fault");
|
||||
s_health_logged |= (serious | rise);
|
||||
}
|
||||
if ((fall != 0u) && (dbg_console_writer_ready() != 0u)) {
|
||||
DBG_INFO("health: recovered flags=0x%lx sticky=0x%lx",
|
||||
(unsigned long)hs->flags, (unsigned long)hs->sticky);
|
||||
}
|
||||
s_health_prev = hs->flags;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:ADC 任务初始化
|
||||
@@ -67,6 +223,8 @@ static uint32_t s_frames_idle;
|
||||
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");
|
||||
return 0;
|
||||
}
|
||||
@@ -76,25 +234,45 @@ int adc_task_init(void) {
|
||||
* 入口参数:arg - FreeRTOS 传入参数(未使用)
|
||||
* 返 回 值:无
|
||||
* 限定条件:adc_task_init() 已调用;内核已启动
|
||||
* 函数说明:每 1ms 采集一拍(adc_input_poll():TPAFE 原子快照 + STM32 ADC1 9 通道
|
||||
* 顺序扫描)并统计。采样节拍由 TIM6 硬件定时器提供(ISR 内触发 CONVST),
|
||||
* 本任务不再触发。稳态下 consumed 每拍 +1、idle 不再增长;idle 持续增长 =
|
||||
* EXTI 未在读帧或 TIM6 未启动(异常),由周期心跳日志暴露。
|
||||
* 函数说明:每拍采集一次(adc_input_poll():TPAFE 原子快照 + 消费一帧 STM32 ADC1 DMA 乒乓缓冲)
|
||||
* 并统计。采样节拍由 TIM3 硬件定时器提供(ISR 内触发 CONVST),本任务不再触发;
|
||||
* **本任务的唤醒同样由该 TIM3 ISR 置位 ADC_WAKE_FLAG 驱动**(v2.7 事件驱动,
|
||||
* 不再自定 1 ms 周期)。稳态下 consumed 每拍 +1、idle 不再增长;idle 持续增长 =
|
||||
* EXTI 未在读帧或 TIM3 未启动(异常),由周期心跳日志暴露。
|
||||
* ⚠ 若 TIM3 停摆,本任务会**永久阻塞**在 osThreadFlagsWait → TASK_WD_ADC 缺席 →
|
||||
* IWDG 复位。这是刻意的 fail-safe("节拍源缺失"不被静默放过),不是缺陷。
|
||||
*/
|
||||
void adc_task_func(void *arg) {
|
||||
uint32_t next = osKernelGetTickCount();
|
||||
uint32_t report_at = next + ADC_TASK_REPORT_MS;
|
||||
uint32_t report_at = osKernelGetTickCount() + ADC_TASK_REPORT_MS;
|
||||
uint8_t first_logged = 0u;
|
||||
uint8_t first_phasor_logged = 0u;
|
||||
|
||||
(void)arg;
|
||||
|
||||
for (;;) {
|
||||
int r;
|
||||
/* 开闸:此刻任务已存在、调度器必已运行 → ISR 的唤醒从此生效。
|
||||
* 放在进入循环之前:置位与等待之间不会漏唤醒(线程标志粘滞) */
|
||||
s_consumer_ready = 1u;
|
||||
|
||||
/* 1) 采集一拍:L1 层 adc_input_poll() = TPAFE 原子快照 + STM32 ADC1 顺序扫描
|
||||
* (仅任务上下文;TPAFE 侧 1=有新帧 0=无新帧 -1=参数错)。
|
||||
* 采样节拍由 TIM6 更新中断提供(ISR 内触发 CONVST),本任务不触发。 */
|
||||
for (;;) {
|
||||
int r;
|
||||
uint32_t now = osKernelGetTickCount(); /* 本拍时基:健康判定 + 心跳共用一份 */
|
||||
|
||||
/* P2 探针:单拍起点。名义节拍 1 ms = 168000 周期;若实测**持续**超过,
|
||||
* 本任务(v2.7 起为 osPriorityAboveNormal)会持续抢占 uiTask
|
||||
* (osPriorityBelowNormal),是"UI 心跳缺席"的候选占用者之一 */
|
||||
task_prof_enter(TASK_PROF_ADC_BEAT);
|
||||
|
||||
/* 1) 采集一拍:L1 层 adc_input_poll() = TPAFE 原子快照 + 消费一帧 STM32 ADC1
|
||||
* 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 个半字)。
|
||||
* ⚠ adcm(整拍)的 ~2.1 ms 峰值来自每 10 s 一次的心跳日志,不是本段,勿混读 */
|
||||
task_prof_enter(TASK_PROF_ADC_POLL);
|
||||
r = adc_input_poll();
|
||||
task_prof_exit(TASK_PROF_ADC_POLL);
|
||||
if (r == 1) {
|
||||
s_frames_consumed++;
|
||||
} else if (r == 0) {
|
||||
@@ -103,7 +281,63 @@ void adc_task_func(void *arg) {
|
||||
/* r == -1:参数错。调用点 count 为常量,不可达;不计入 idle 以区分异常 */
|
||||
}
|
||||
|
||||
/* 2) 首帧码值上报:**写者就绪后**才落日志。写者未注册时 con_append 走
|
||||
/* 2) L2 输入 + 相量重算:把本拍 9 路语义值转浮点帧压入采样环,再重算全部
|
||||
* 相量实例(L2-6 滑动窗:每拍以"当前时刻为终止的前一周波"整窗重算)。
|
||||
*
|
||||
* ⚠ P0 修复(2026-10-10,Task #32 §5):**仅在本拍确有新帧(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)。
|
||||
*
|
||||
* 任一通道读不到(尚未首次成功)→ 本拍**整帧跳过**:不向窗内注入 0 点
|
||||
* (单个 0 点会显著污染 20 点窗),并计入 skip 观测。 */
|
||||
if (r == 1) {
|
||||
float frame[ADC_SEM_COUNT];
|
||||
uint8_t s;
|
||||
uint8_t all_ok = 1u;
|
||||
|
||||
for (s = 0u; s < (uint8_t)ADC_SEM_COUNT; s++) {
|
||||
int32_t code = 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;
|
||||
}
|
||||
|
||||
if (all_ok != 0u) {
|
||||
(void)sample_ring_push_frame(frame, (uint8_t)ADC_SEM_COUNT);
|
||||
phasor_run();
|
||||
} else {
|
||||
s_frames_skip++;
|
||||
}
|
||||
}
|
||||
|
||||
/* 2b) L1-6 采样有效性校验(每拍,常数次整数运算):把驱动侧观测计数交给
|
||||
* 纯逻辑判定层,本任务只做告警编排。⚠ 计数一律取**本拍 poll 之后**
|
||||
* 的值——与刚消费的帧同一时点,避免"下一拍的增量被提前看到"。 */
|
||||
{
|
||||
adc_health_in_t h_in;
|
||||
adc_health_status_t h_st;
|
||||
|
||||
h_in.tpafe_seq = tpafe5160_frame_seq();
|
||||
h_in.tpafe_frst_err = (uint32_t)tpafe5160_frstdata_err();
|
||||
h_in.stm32_seq = adc_input_stm32_seq();
|
||||
h_in.stm32_errs = adc_input_stm32_errs();
|
||||
h_in.stm32_gaps = adc_input_stm32_gaps();
|
||||
h_in.stm32_hw_errs = adc_input_stm32_hw_errs();
|
||||
h_in.now_ms = now;
|
||||
adc_health_check(&h_in, &h_st);
|
||||
adc_health_report(&h_st);
|
||||
}
|
||||
|
||||
/* 3) 首帧码值上报:**写者就绪后**才落日志。写者未注册时 con_append 走
|
||||
* 无锁直发路径,与 defaultTask 的存储初始化日志会字节级交错
|
||||
* (2026-10-10 台架实测);此时挂起上报,就绪后补报最近一帧。
|
||||
* 8 路电气量 + 温度全部经 adc_input_raw() **按语义**取数(L1-4),
|
||||
@@ -129,30 +363,67 @@ void adc_task_func(void *arg) {
|
||||
(long)ia, (long)ib, (long)ic, (long)in, (long)temp);
|
||||
}
|
||||
|
||||
/* 3) 看门狗心跳(本任务已纳入 required_mask) */
|
||||
/* 4) 首拍相量上报:攒满一周波(20 ms)后立即落一次,作为 L2 链路的开机证据。
|
||||
* 不依赖 10 s 心跳 → 台架 @test 的 settle 窗口内即可见(否则会被静默判据
|
||||
* 截断,见 docs/测试说明/自动化测试流程.md 的 settle 口径)。 */
|
||||
if ((first_phasor_logged == 0u) && (dbg_console_writer_ready() != 0u)) {
|
||||
phasor_t p;
|
||||
|
||||
if (phasor_get(0u, &p) == 0) {
|
||||
first_phasor_logged = 1u;
|
||||
log_phasors("first");
|
||||
}
|
||||
}
|
||||
|
||||
/* 5) 看门狗心跳(本任务已纳入 required_mask) */
|
||||
task_wd_beat(TASK_WD_ADC);
|
||||
|
||||
/* 4) 周期心跳日志:帧统计 + 驱动侧 FRSTDATA 错误计数。
|
||||
* 写者就绪前抑制(同 2) 的交错理由);到点无论是否落日志都推进下个上报点 */
|
||||
/* 6) 周期心跳日志:帧统计 + 驱动侧 FRSTDATA 错误计数 + L2 相量。
|
||||
* 写者就绪前抑制(同 3) 的交错理由);到点无论是否落日志都推进下个上报点 */
|
||||
{
|
||||
uint32_t now = osKernelGetTickCount();
|
||||
|
||||
if ((int32_t)(now - report_at) >= 0) {
|
||||
if (dbg_console_writer_ready() != 0u) {
|
||||
DBG_INFO("frames=%lu idle=%lu seq=%lu frst_err=%u adc1_rc=%d adc1_err=%lu",
|
||||
/* STM32 侧观测量(v2.0 DMA 版),四类各有独立成因:
|
||||
* adc1_rc = 最近一拍取帧状态(0 OK / -2 本拍无新帧)
|
||||
* adc1_seq = DMA 整帧完成计数(正常时 ≈ seq,恒定小偏移)
|
||||
* adc1_gap = 丢帧:seq 一次前进 >1 → 本任务超拍(单拍 > 1 ms)
|
||||
* adc1_err = 停摆:连续 >=2 拍无新帧 → 节拍源/DMA 真停
|
||||
* adc1_hw = HAL ADC/DMA 错误回调
|
||||
* 判读:adc1_seq 停增而 seq 仍涨 → STM32 侧停摆;
|
||||
* adc1_gap 增长 → 本任务耗时超拍(看 adcm/adcn);
|
||||
* tgap = TPAFE 侧同类丢帧计数(L1-6,与 adc1_gap 对称) */
|
||||
DBG_INFO("frames=%lu idle=%lu skip=%lu seq=%lu frst_err=%u ring=%lu"
|
||||
" adc1_rc=%d adc1_err=%lu adc1_seq=%lu adc1_gap=%lu adc1_hw=%lu"
|
||||
" tgap=%lu",
|
||||
(unsigned long)s_frames_consumed,
|
||||
(unsigned long)s_frames_idle,
|
||||
(unsigned long)s_frames_skip,
|
||||
(unsigned long)tpafe5160_frame_seq(),
|
||||
(unsigned)tpafe5160_frstdata_err(),
|
||||
(unsigned long)sample_ring_frames(),
|
||||
adc_input_stm32_rc(),
|
||||
(unsigned long)adc_input_stm32_errs());
|
||||
(unsigned long)adc_input_stm32_errs(),
|
||||
(unsigned long)adc_input_stm32_seq(),
|
||||
(unsigned long)adc_input_stm32_gaps(),
|
||||
(unsigned long)adc_input_stm32_hw_errs(),
|
||||
(unsigned long)adc_health_tpafe_gaps());
|
||||
|
||||
log_phasors("hb");
|
||||
}
|
||||
report_at = now + ADC_TASK_REPORT_MS;
|
||||
}
|
||||
}
|
||||
|
||||
/* 5) 等下一个节拍(固定周期;落后则立即补齐,不累积漂移) */
|
||||
next += ADC_TASK_BEAT_MS;
|
||||
(void)osDelayUntil(next);
|
||||
/* 7) 等下一次节拍唤醒(v2.7 事件驱动,Task #32 P1-5 方案 a)
|
||||
* 原实现是 osDelayUntil(next += 1ms):把消费唤醒**钉在 FreeRTOS tick(1 ms)边界**
|
||||
* → 与 TIM3 生产之间有 0~1 ms 随机相位抖动(日志里的 idle 正是这么来的),
|
||||
* 且一旦超拍,next 就永久落后 → 任务不再阻塞 → 空转抢占低优先级 uiTask
|
||||
* (P0-2 修的正是它)。现在唤醒与生产**同源**: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 探针:单拍耗时结算(不含等待) */
|
||||
(void)osThreadFlagsWait(ADC_WAKE_FLAG, osFlagsWaitAny, osWaitForever);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
* 创建日期:2026-07-19
|
||||
* 修改记录:
|
||||
* v1.0 2026-07-19 王建锋 创建初始版本
|
||||
* v1.1 2026-10-10 王建锋 Task #32 P1-5 方案 a:新增 ADC_WAKE_FLAG 与
|
||||
* adc_tick_irq_notify()——消费节拍由 osDelayUntil(1ms)
|
||||
* 轮询改为 TIM3 ISR 事件驱动唤醒
|
||||
*/
|
||||
|
||||
#ifndef ADC_TASK_H
|
||||
@@ -18,6 +21,17 @@ 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」只约束状态位) */
|
||||
#define ADC_WAKE_FLAG 0x0001U
|
||||
|
||||
/* ISR 入口:由 Src/main.c 的 HAL_TIM_PeriodElapsedCallback(TIM3 分支)调用。
|
||||
* 内部自带"消费者就绪"门与句柄判空 → 调度器启动前调用也安全(理由见 .c 注释)。
|
||||
* 收在 adc_task.c 而非 main.c 内联:main.c 不碰任何内核句柄,与 task_wd_tick_probe() 同构 */
|
||||
void adc_tick_irq_notify(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
+66
-3
@@ -19,6 +19,10 @@
|
||||
* FTP 文件操作与 syslog/配置的卷访问自此串行化
|
||||
* (2026-10-02 台架:并发 FTL 写 → journal 竞争 →
|
||||
* 本任务主循环喂狗点饥饿 → IWDG 误复位)
|
||||
* v1.2 2026-10-10 王建锋 Task #32 的 P2 测量接线:主循环周期上报各任务最坏
|
||||
* 耗时(task_prof_report),并在**心跳位图出现 1→0 边沿**
|
||||
* 的瞬间留一份现场快照(task_prof_report_raw,裸 UART);
|
||||
* 另给控制台排空与 syslog 落盘各加一个耗时探针
|
||||
*/
|
||||
|
||||
#include "default_task.h"
|
||||
@@ -31,6 +35,7 @@
|
||||
#include "nand_ftl.h"
|
||||
#include "net_cfg.h"
|
||||
#include "cfg_file.h"
|
||||
#include "prot_set_store.h"
|
||||
#include "sys_log.h"
|
||||
#include "sys_events.h"
|
||||
/* 深调用链模块(本任务 = 存储 bring-up + 配置引擎属主 + syslog 唯一写者,
|
||||
@@ -39,6 +44,7 @@
|
||||
#define DBG_LINE_BUF_MAX 128U
|
||||
#include "dbg_log.h"
|
||||
#include "task_watchdog.h"
|
||||
#include "task_prof.h" /* P2 测量:各任务最坏耗时上报(Task #32) */
|
||||
#include "vol_lock.h"
|
||||
|
||||
/* 任务句柄与属性块(CubeMX 生成全局,定义于 Src/freertos.c):
|
||||
@@ -57,6 +63,10 @@ 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;
|
||||
|
||||
/* Private defines -----------------------------------------------------------*/
|
||||
/* 栈水位观测节奏:首次报告须晚于 bring-up 深链(mount/mkfs/探针/cfg/syslog),
|
||||
@@ -64,9 +74,11 @@ extern const osThreadAttr_t adcTask_attributes;
|
||||
#define STACK_PROBE_FIRST_MS 5000u
|
||||
#define STACK_PROBE_PERIOD_MS 2000u
|
||||
/* S11(2026-10-03):堆低水位告警阈值(B)。
|
||||
* 依据:2026-10-03 上板两次开机的 min_ever 实测下限 3,712 B(configTOTAL_HEAP_SIZE
|
||||
* 依据:2026-10-03 上板两次开机的 min_ever 实测下限 3,712 B(当时 configTOTAL_HEAP_SIZE
|
||||
* = 48KB)——低于此值说明堆余量 <8%,新增任务/缓冲随时可能分配失败(隐性风险)。
|
||||
* 阈值取 3,072 B 留出运行间 ~600B 波动余量,避免把正常波动误报为故障。 */
|
||||
* 阈值取 3,072 B 留出运行间 ~600B 波动余量,避免把正常波动误报为故障。
|
||||
* 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 修复):启动期存储挂载失败后,
|
||||
* 主循环按 2s 周期重跑完整 bring-up 的次数上限;耗尽则维持无存储降级
|
||||
@@ -88,6 +100,8 @@ static const stack_probe_task_t s_stack_probe[] = {
|
||||
{ "ui", &uiTaskHandle, &uiTask_attributes },
|
||||
{ "timesync", &timeSyncTaskHandle, &timeSyncTask_attributes },
|
||||
{ "adc", &adcTaskHandle, &adcTask_attributes },
|
||||
{ "iec104", &iec104TaskHandle, &iec104Task_attributes },
|
||||
{ "protect", &protectTaskHandle, &protectTask_attributes },
|
||||
};
|
||||
|
||||
#define STACK_PROBE_COUNT (sizeof(s_stack_probe) / sizeof(s_stack_probe[0]))
|
||||
@@ -185,6 +199,22 @@ void default_task_func(void *argument) {
|
||||
task_wd_seg(TASK_WD_SEG_POLL);
|
||||
/* 心跳抽样与喂狗判定(内部节流至 100ms;全员到位才喂,否则累计停喂) */
|
||||
task_wd_poll();
|
||||
/* P2 测量(Task #32):某位由"到过"变为"缺席"的瞬间留一份现场快照。
|
||||
* 走裸 UART 直发——缺席窗口内环形缓冲可能积压,这两行必须出去。
|
||||
*
|
||||
* ⚠ 判据用**位图边沿**,不能用 miss 累计数:`task_wd_miss_count()` 返回的是
|
||||
* 单调累计的 s_miss_total(永不清零),开机 bring-up 阶段任务尚未全部启动时
|
||||
* 就已经 >0 → 若以"归零"判恢复,则首次缺席之后再也不会触发。
|
||||
* 位图边沿(某位 1→0)天然"每个 episode 只报一次":此后 prev 也不含该位。 */
|
||||
{
|
||||
static uint32_t s_prof_prev_mask;
|
||||
const uint32_t mask = task_wd_last_mask();
|
||||
|
||||
if ((s_prof_prev_mask != 0u) && ((s_prof_prev_mask & ~mask) != 0u)) {
|
||||
task_prof_report_raw();
|
||||
}
|
||||
s_prof_prev_mask = mask;
|
||||
}
|
||||
/* 本任务自己的心跳 */
|
||||
task_wd_beat(TASK_WD_DEFAULT);
|
||||
/* 等待"保存网络配置"或"控制台待发":任一即醒;无事件 50ms 后返回
|
||||
@@ -192,12 +222,18 @@ void default_task_func(void *argument) {
|
||||
task_wd_seg(TASK_WD_SEG_EVTWAIT);
|
||||
if (sys_evtHandle != NULL) {
|
||||
(void)osEventFlagsWait(sys_evtHandle,
|
||||
SYS_EVT_CFG_SAVE | SYS_EVT_CONSOLE_TX,
|
||||
SYS_EVT_CFG_SAVE | SYS_EVT_CONSOLE_TX
|
||||
| SYS_EVT_PROT_SET_SAVE,
|
||||
osFlagsWaitAny, 50u);
|
||||
}
|
||||
/* 控制台唯一发送点:把环形缓冲中的日志按块发出(无待发时立即返回) */
|
||||
task_wd_seg(TASK_WD_SEG_DRAIN);
|
||||
/* P2 探针:控制台排空单独计时。它内部**忙等** UART TXE(单次预算
|
||||
* DBG_CON_DRAIN_BUDGET_MS=250ms),且本任务为 osPriorityNormal →
|
||||
* 是"某个 Normal 任务连续占用 CPU 把 UI 饿死"的头号候选 */
|
||||
task_prof_enter(TASK_PROF_DEF_DRAIN);
|
||||
dbg_console_drain();
|
||||
task_prof_exit(TASK_PROF_DEF_DRAIN);
|
||||
/* 控制台 TX 故障留痕:由本任务(syslog 唯一写者)在 drain **之外**补记 run.log。
|
||||
* console_log 内部刻意不调 syslog_post —— 那会构成日志调用图成环,
|
||||
* 让 armlink 静态栈深在每个任务上都多叠一条日志尾巴(见 console_log.c) */
|
||||
@@ -208,6 +244,9 @@ void default_task_func(void *argument) {
|
||||
#if !defined(TEST_SUITE_STORAGE)
|
||||
task_wd_seg(TASK_WD_SEG_CFGSAVE);
|
||||
net_cfg_save_run();
|
||||
/* 保护定值落盘(B7):内部消费脏标志,无改动 = 空操作;
|
||||
* 写失败脏回置 → iec104Task 下轮 poll 再投递事件 = 天然重试 */
|
||||
(void)prot_set_store_save();
|
||||
#endif
|
||||
/* 配置文件变更轮询(2s 周期;沉降去抖在引擎内) */
|
||||
if ((cfg_poll_cnt++ % 40u) == 0u) {
|
||||
@@ -252,11 +291,21 @@ void default_task_func(void *argument) {
|
||||
#if !defined(TEST_SUITE_STORAGE)
|
||||
/* 排空运行期日志(上电/重启分类与自检故障落盘) */
|
||||
task_wd_seg(TASK_WD_SEG_SYSLOG);
|
||||
/* P2 探针:syslog 落盘(NAND/FTL 写,最坏数百 ms)单独计时 */
|
||||
task_prof_enter(TASK_PROF_DEF_SYSLOG);
|
||||
syslog_drain();
|
||||
task_prof_exit(TASK_PROF_DEF_SYSLOG);
|
||||
#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 → 被调度饿死(本轮迟迟跑不完)
|
||||
* uiw 大 → uiTask 自身渲染拖长
|
||||
* [PROF2] 的 defd/netp/adcp 谁在同一窗口抬起来 → 谁就是占用者 */
|
||||
task_prof_report();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -483,6 +532,20 @@ static FRESULT storage_bring_up(FATFS *fs) {
|
||||
* 文件缺失/损坏 = 工厂默认,不阻塞组网)。须在置位 STORAGE_READY 前,
|
||||
* 使"存储就绪"屏障蕴含"配置已就绪",netTask 组网时配置恒有效 */
|
||||
(void)net_cfg_load();
|
||||
/* 装载保护定值持久化档(B7):文件缺失 = 出厂默认(首启正常,
|
||||
* 静默);blob 损坏/越界 = 整表拒绝保持默认 + 自检留痕
|
||||
* (all-or-nothing,prot_set_store_parse 保证一字不半装载)。
|
||||
* 须在 STORAGE_READY 前——netTask 等此屏障,104 定值写必在装载后 */
|
||||
{
|
||||
int ps_rc = prot_set_store_load();
|
||||
|
||||
if (ps_rc == 0) {
|
||||
DBG_INFO("[CFG] prot set loaded (/prot_set.bin)");
|
||||
} else if (ps_rc < 0) {
|
||||
DBG_ERROR("[CFG] prot set blob CORRUPT, defaults in use");
|
||||
syslog_post(SYSLOG_SELFTEST, "prot set corrupt");
|
||||
}
|
||||
}
|
||||
/* 广播"存储就绪"(= 卷挂载 + 配置加载完成),唤醒依赖文件系统的任务 */
|
||||
if (sys_evtHandle != NULL) {
|
||||
osEventFlagsSet(sys_evtHandle, SYS_EVT_STORAGE_READY);
|
||||
|
||||
@@ -0,0 +1,302 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 被控站服务(板端胶水任务)
|
||||
* 模块功能:2404 监听 + 单连接 socket 收发 + 把字节流喂给 iec104_link 纯逻辑
|
||||
* 层;对时经 sys_clock_set_unix 落墙钟;数据源走 iec104_pts 默认源
|
||||
* (phasor / prot_elm / prot_set)。协议语义全部在 App/protocol/iec104,
|
||||
* 本文件只做 I/O 编排(A-4 约束 1)。
|
||||
* 线程约定:iec104Task 独占(NET_READY 屏障后循环,100ms 节拍 + 看门狗心跳)。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
*/
|
||||
#include "iec104_serv.h"
|
||||
|
||||
#define DBG_LINE_BUF_MAX 128U /* 深链模块日志缓冲收紧(time_sync 先例) */
|
||||
#define DBG_TAG "[I104]"
|
||||
|
||||
#include "iec104_link.h"
|
||||
#include "iec104_pts.h"
|
||||
#include "net_socket.h"
|
||||
#include "prot_set.h" /* 定值脏标志 peek(B7 落盘通知) */
|
||||
#include "sys_clock.h"
|
||||
#include "sys_events.h"
|
||||
#include "task_watchdog.h"
|
||||
#include "dbg_log.h"
|
||||
|
||||
#include "cmsis_os2.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
/* 服务周期:COV 延迟 ≤100ms(远小于 SOE 5000ms 验收线);心跳间隔同值 */
|
||||
#define IEC104_POLL_MS 100u
|
||||
/* 接收搬运缓冲(静态,不入栈;link 层内部另有 512B 重组缓冲) */
|
||||
#define IEC104_RX_SZ 128u
|
||||
/* 兜底节拍:门控判"无数据"时每 32 轮无条件收一次(ui_net_sink RM 先例,
|
||||
* 防 RECV_OK 中断丢失把数据永久藏住);须为 2 的幂 */
|
||||
#define IEC104_LIVENESS_DIV 32u
|
||||
/* 连续发送失败阈值:同 ui_net_sink E-04(短写=对端收坏帧) */
|
||||
#define IEC104_SEND_FAIL_MAX 3u
|
||||
|
||||
/* ---------------- 服务状态(iec104Task 独占) ---------------- */
|
||||
static iec104_link_t s_link;
|
||||
static int s_listen = -1;
|
||||
static int s_client = -1; /* 单连接模型:MCU 上即 s_listen */
|
||||
static uint32_t s_gen; /* 网络代次(重建自愈,ui_net_sink 先例) */
|
||||
static uint8_t s_rx[IEC104_RX_SZ];
|
||||
static uint8_t s_liveness_ctr;
|
||||
static uint8_t s_send_fail_run;
|
||||
static uint8_t s_conn_dead; /* 发送连败 → 本轮 link 调用返回后拆链 */
|
||||
|
||||
/* 前向声明(cfg 初始化引用) */
|
||||
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);
|
||||
|
||||
/* V0.1 公共地址固定 1(点表口径;可配置项挂二期) */
|
||||
static const iec104_link_cfg_t s_cfg = { 1u, s_send_apdu, s_clock_sync };
|
||||
|
||||
/*
|
||||
* 函数功能:link 层发送回调——经 net_send 串行 RPC 发出一个 APDU
|
||||
* 入口参数:ctx 未用;apdu/len = 完整 APCI 帧
|
||||
* 返回值:无
|
||||
* 限定条件:s_client >= 0(在 link 调用栈内执行,即本任务上下文)
|
||||
* 函数说明:短写/失败累计(E-04 先例);连续 3 次 → 置 s_conn_dead,
|
||||
* 由 s_service_link 在 link 调用返回后统一拆链(严禁在回调里拆)
|
||||
*/
|
||||
static void s_send_apdu(void *ctx, const uint8_t *apdu, uint16_t len) {
|
||||
int ret;
|
||||
|
||||
(void)ctx;
|
||||
if (s_client < 0) {
|
||||
return;
|
||||
}
|
||||
ret = net_send(s_client, apdu, (int)len, 0);
|
||||
if (ret == (int)len) {
|
||||
s_send_fail_run = 0u;
|
||||
return;
|
||||
}
|
||||
if (s_send_fail_run < 255u) {
|
||||
s_send_fail_run++;
|
||||
}
|
||||
DBG_ERROR("i104 send short: ret=%d/%u run=%u", ret, (unsigned)len,
|
||||
(unsigned)s_send_fail_run);
|
||||
if (s_send_fail_run >= IEC104_SEND_FAIL_MAX) {
|
||||
s_conn_dead = 1u;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:link 层对时回调——epoch 毫秒落系统墙钟(含 SD2506 RTC 写回)
|
||||
* 入口参数:ctx 未用;epoch_ms = C_CS 解析出的 UTC 时刻
|
||||
* 返回值:0 成功 / -1 失败(link 层回 actcon+PN)
|
||||
* 函数说明:uint32 秒域上限 2106-02-07,越界显式拒绝(CP56 域到 2127)
|
||||
*/
|
||||
static int s_clock_sync(void *ctx, uint64_t epoch_ms) {
|
||||
uint64_t secs = epoch_ms / 1000ULL;
|
||||
|
||||
(void)ctx;
|
||||
if (secs > 0xFFFFFFFFULL) {
|
||||
return -1;
|
||||
}
|
||||
return sys_clock_set_unix((uint32_t)secs);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:创建并监听 2404(socket+bind+listen,抄 ui_net_sink bind_listen)
|
||||
* 返回值:0 成功(s_listen 写回)/ -1 失败
|
||||
* 限定条件:net 子系统可用(NET_READY 屏障后)
|
||||
*/
|
||||
static int s_bind_listen(void) {
|
||||
int ls;
|
||||
struct net_sockaddr_in addr;
|
||||
|
||||
ls = net_socket(NET_AF_INET, NET_SOCK_STREAM);
|
||||
if (ls < 0) {
|
||||
return -1;
|
||||
}
|
||||
(void)memset(&addr, 0, sizeof(addr));
|
||||
addr.sin_family = NET_AF_INET;
|
||||
addr.sin_port = net_htons(IEC104_SERV_PORT);
|
||||
addr.sin_addr.s_addr = NET_INADDR_ANY;
|
||||
if (net_bind(ls, (struct net_sockaddr *)&addr, (int)sizeof(addr)) < 0) {
|
||||
net_close(ls);
|
||||
return -1;
|
||||
}
|
||||
if (net_listen(ls) < 0) {
|
||||
net_close(ls);
|
||||
return -1;
|
||||
}
|
||||
s_listen = ls;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:主动断开当前 104 连接并重建监听(协议故障/t1 超时/发送连败)
|
||||
* 入口参数:why = 日志原因串(ASCII)
|
||||
* 返回值:无
|
||||
* 函数说明:与 EOF 路径不同——协议违例时主站不会自行断开,必须本端 close
|
||||
* 促其重连;单连接模型下 client 即 listen,close 后统一重建
|
||||
* (time_sync 会话收尾先例,非芯片侧 auto_relisten 风暴路径)
|
||||
*/
|
||||
static void s_drop_conn(const char *why) {
|
||||
DBG_ERROR("i104 drop: %s", why);
|
||||
if (s_client >= 0) {
|
||||
net_close(s_client);
|
||||
}
|
||||
s_client = -1;
|
||||
s_listen = -1;
|
||||
iec104_link_close(&s_link);
|
||||
s_send_fail_run = 0u;
|
||||
s_conn_dead = 0u;
|
||||
s_liveness_ctr = 0u;
|
||||
if (s_bind_listen() < 0) {
|
||||
s_listen = -1; /* 主循环下轮重试 */
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:link 层节拍服务 + 故障检测(poll 后查 IDLE 跳变/发送连败)
|
||||
* 入口参数:now = 注入时基(tick ms)
|
||||
* 返回值:无
|
||||
*/
|
||||
static void s_service_link(uint32_t now) {
|
||||
iec104_link_poll(&s_link, now);
|
||||
if (s_conn_dead != 0u) {
|
||||
s_drop_conn("send fail");
|
||||
return;
|
||||
}
|
||||
if (iec104_link_state(&s_link) == IEC104_ST_IDLE) {
|
||||
s_drop_conn("protocol fault");
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:单次服务循环(代次自愈 → 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 层
|
||||
* 已 auto_relisten,close 会连监听一起毁)
|
||||
*/
|
||||
static void s_serv_poll(void) {
|
||||
uint32_t now = osKernelGetTickCount();
|
||||
int n;
|
||||
|
||||
/* 网络重建(代次变化):旧监听/客户端随芯片复位失效 → 重建 */
|
||||
if (net_get_generation() != s_gen) {
|
||||
if ((s_client >= 0) && (s_client != s_listen)) {
|
||||
net_close(s_client);
|
||||
}
|
||||
if (s_listen >= 0) {
|
||||
net_close(s_listen);
|
||||
s_listen = -1;
|
||||
}
|
||||
s_client = -1;
|
||||
iec104_link_close(&s_link);
|
||||
s_send_fail_run = 0u;
|
||||
s_conn_dead = 0u;
|
||||
s_liveness_ctr = 0u;
|
||||
s_gen = net_get_generation();
|
||||
(void)s_bind_listen();
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_listen < 0) {
|
||||
(void)s_bind_listen(); /* 启动失败/被毁:重试 */
|
||||
return;
|
||||
}
|
||||
|
||||
if (s_client < 0) {
|
||||
int c;
|
||||
|
||||
if (net_can_accept(s_listen) == 0) {
|
||||
if (net_get_sock(s_listen) == NULL) {
|
||||
s_listen = -1; /* 控制块消失:交回上方重建 */
|
||||
}
|
||||
return;
|
||||
}
|
||||
c = net_accept(s_listen, NULL, NULL);
|
||||
if (c >= 0) {
|
||||
s_client = c;
|
||||
s_send_fail_run = 0u;
|
||||
s_conn_dead = 0u;
|
||||
s_liveness_ctr = 0u;
|
||||
iec104_link_open(&s_link, now);
|
||||
DBG_INFO("i104: accepted fd=%d", c);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* 门控 + 兜底:无数据多数轮直接走 link 节拍;每 32 轮无条件收一次 */
|
||||
if (net_can_read(s_client) == 0) {
|
||||
if (((++s_liveness_ctr) & (IEC104_LIVENESS_DIV - 1u)) != 0u) {
|
||||
s_service_link(now);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
s_liveness_ctr = 0u;
|
||||
}
|
||||
|
||||
n = net_recv(s_client, s_rx, (int)sizeof(s_rx), NET_MSG_DONTWAIT);
|
||||
if (n > 0) {
|
||||
iec104_link_on_rx(&s_link, s_rx, (uint16_t)n, now);
|
||||
} else if (n == 0) {
|
||||
/* 对端关闭:net 层已 auto_relisten;client==listen 时严禁 close */
|
||||
if (s_client != s_listen) {
|
||||
net_close(s_client); /* 模拟器多连接后端:数据通道独立 */
|
||||
}
|
||||
s_client = -1;
|
||||
s_liveness_ctr = 0u;
|
||||
iec104_link_close(&s_link);
|
||||
if ((net_get_sock(s_listen) == NULL) && (s_bind_listen() < 0)) {
|
||||
s_listen = -1;
|
||||
}
|
||||
return;
|
||||
} else if (net_get_errno() != NET_ERR_WOULDBLOCK) {
|
||||
s_drop_conn("recv err");
|
||||
return;
|
||||
}
|
||||
|
||||
s_service_link(now);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:iec104Task 任务体(freertos.c USER CODE 区创建)
|
||||
* 入口参数:argument 未用
|
||||
* 返回值:无(不返回)
|
||||
* 函数说明:NET_READY 屏障(100ms 分片 + 每片喂心跳,timeSyncTask 先例)→
|
||||
* 点表/链路 init + 建监听 → 100ms 服务循环(每轮喂看门狗)
|
||||
*/
|
||||
void iec104_serv_task(void *argument) {
|
||||
(void)argument;
|
||||
|
||||
if (sys_evtHandle != NULL) {
|
||||
for (;;) {
|
||||
uint32_t r = osEventFlagsWait(sys_evtHandle, SYS_EVT_NET_READY,
|
||||
osFlagsWaitAny | osFlagsNoClear, 100u);
|
||||
if ((r & osFlagsError) == 0u) {
|
||||
break; /* 就绪位是状态位(NoClear),不消费 */
|
||||
}
|
||||
task_wd_beat(TASK_WD_IEC104);
|
||||
}
|
||||
}
|
||||
|
||||
iec104_pts_init();
|
||||
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);
|
||||
} else {
|
||||
DBG_INFO("i104: listening on %u", (unsigned)IEC104_SERV_PORT);
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
task_wd_beat(TASK_WD_IEC104);
|
||||
s_serv_poll();
|
||||
/* 定值改动(C_SE 执行段 / YK12)→ 通知属主任务落盘 /prot_set.bin(B7)。
|
||||
* peek 非消费:写盘失败时脏标志仍在,下一轮再投递 = 天然重试;
|
||||
* 本任务不碰文件系统(配置引擎属主 = defaultTask,模块约定) */
|
||||
if ((prot_set_dirty_peek() != 0u) && (sys_evtHandle != NULL)) {
|
||||
(void)osEventFlagsSet(sys_evtHandle, SYS_EVT_PROT_SET_SAVE);
|
||||
}
|
||||
osDelay(IEC104_POLL_MS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
/*
|
||||
* 模块名称:IEC 60870-5-104 被控站服务(对外接口)
|
||||
* 模块功能:声明 iec104Task 任务体与监听端口;协议实现在 App/protocol/iec104/,
|
||||
* 本接口仅供 Src/freertos.c 创建任务使用。
|
||||
* 创建日期:2026-10-12(B4)
|
||||
*/
|
||||
#ifndef IEC104_SERV_H
|
||||
#define IEC104_SERV_H
|
||||
|
||||
/* IEC 60870-5-104 被控站监听端口(DL/T 634.5104 标准端口) */
|
||||
#define IEC104_SERV_PORT 2404u
|
||||
|
||||
/*
|
||||
* 函数功能:iec104Task 任务体(在 Src/freertos.c USER CODE 区 osThreadNew 创建)
|
||||
* 入口参数:argument - 未用
|
||||
* 返回值:无(任务不返回)
|
||||
*/
|
||||
void iec104_serv_task(void *argument);
|
||||
|
||||
#endif /* IEC104_SERV_H */
|
||||
@@ -18,6 +18,9 @@
|
||||
* freertos.c USER CODE 区重复创建已删除
|
||||
* v1.7 2026-10-02 王建锋 P1-8:初始组网失败/重建失败 → 自检记录 + 30s 周期重试
|
||||
* (原实现:初始失败 = netTask 无限驻留;重建失败 = 静默丢配置)
|
||||
* v1.8 2026-10-10 王建锋 Task #32 的 P2 测量埋点:net_poll() 单独计时——
|
||||
* 本任务每 ~1ms 分派一次消息队列,若驱动轮询被拉长,
|
||||
* 其它任务(尤其 UI 的 net_accept 往返)的等待随之拉长
|
||||
*/
|
||||
|
||||
#include <string.h>
|
||||
@@ -31,6 +34,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) */
|
||||
|
||||
#define DBG_TAG "[NET_TASK]"
|
||||
#include "dbg_log.h"
|
||||
@@ -267,7 +271,11 @@ void net_task_func(void const *arg) {
|
||||
}
|
||||
}
|
||||
|
||||
/* P2 探针:驱动轮询单独计时。本循环每 ~1ms 一次,是 netTask 回执
|
||||
* (UI 的 net_accept 在等它)的节奏来源;此值变大 = UI 心跳被拖长 */
|
||||
task_prof_enter(TASK_PROF_NET_POLL);
|
||||
net_poll();
|
||||
task_prof_exit(TASK_PROF_NET_POLL);
|
||||
net_process_messages();
|
||||
osDelay(1);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 模块名称:保护任务(L3 板端接线,B5)——实现
|
||||
* 模块功能:见 protect_task.h。每拍三件事:
|
||||
* ① L5-4 采样故障闭锁:adc_health_flags() 电平位命中 → 元件状态
|
||||
* 复位(防陈旧判据)+ 本拍不 eval;
|
||||
* ② prot_set_selfcheck()(L5-1 定值完整性巡检,31 项 sum32,开销
|
||||
* 可忽略;失配 → prot_set 内部闭锁,eval 自动复位全元件);
|
||||
* ③ prot_elm_run()(phasor rms → 判据步进)+ 动作位图边沿日志;
|
||||
* ④ 全局软压板 PS_GLOBAL_EN(104 YK12 两段式投退,B7):退出 =
|
||||
* 复位元件 + 停判据(与采样闭锁同构,独立边沿日志)。
|
||||
* 定值/元件初始化在 app_main_init(内核前,B5);本任务只消费。
|
||||
* 线程约定:prot_elm_run / prot_set_selfcheck 仅本任务上下文(prot_elm.h);
|
||||
* 跨上下文复归(104 YK11)经 prot_elm_request_reset 落请求。
|
||||
* 创建日期:2026-10-12(B5)
|
||||
*/
|
||||
#include "protect_task.h"
|
||||
|
||||
#include "cmsis_os2.h"
|
||||
|
||||
#define DBG_TAG "[PROT]"
|
||||
#define DBG_LINE_BUF_MAX 128U
|
||||
#include "dbg_log.h"
|
||||
|
||||
#include "adc_health.h"
|
||||
#include "console_log.h" /* dbg_console_writer_ready():启动锚点行门控(B6) */
|
||||
#include "prot_elm.h"
|
||||
#include "prot_set.h"
|
||||
#include "task_watchdog.h"
|
||||
|
||||
/* 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)
|
||||
|
||||
static uint32_t s_prev_active; /* 上拍动作位图(边沿日志用) */
|
||||
static uint8_t s_prev_hf_lock; /* 上拍采样闭锁态(边沿日志用) */
|
||||
static uint8_t s_prev_plate_out; /* 上拍全局软压板退出态(边沿日志用,B7) */
|
||||
|
||||
/*
|
||||
* 函数功能:动作位图变化沿日志(逐变化元件打名字与状态)
|
||||
* 入口参数:active - 本拍 prot_elm_active_mask()
|
||||
* 函数说明:V0.1 无出口硬件(规划 R7),动作 = 遥信 + 日志;SOE 落盘随 L4。
|
||||
*/
|
||||
static void s_log_active_edge(uint32_t active) {
|
||||
uint32_t chg = active ^ s_prev_active;
|
||||
uint32_t i;
|
||||
|
||||
for (i = 0u; (i < (uint32_t)PE_COUNT) && (chg != 0u); i++) {
|
||||
const uint32_t bit = 1u << i;
|
||||
|
||||
if ((chg & bit) != 0u) {
|
||||
prot_elm_state_t st;
|
||||
|
||||
chg &= ~bit;
|
||||
prot_elm_state((prot_elm_id_t)i, &st);
|
||||
DBG_INFO("elm %s asserted=%u picked=%u",
|
||||
prot_elm_name((prot_elm_id_t)i),
|
||||
(unsigned)st.asserted, (unsigned)st.picked);
|
||||
}
|
||||
}
|
||||
s_prev_active = active;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:protectTask 线程入口
|
||||
* 入口参数:argument - 未用
|
||||
* 返 回 值:无(不返回)
|
||||
* 函数说明:无就绪屏障(判据只依赖 phasor 使用区,未就绪时 prot_elm_run
|
||||
* 整拍自跳;采样闭锁另有 adc_health 电平位兜底)→ 上电即跑。
|
||||
* 每拍喂看门狗(TASK_WD_PROTECT,产品 required_mask 0x1F7)。
|
||||
*/
|
||||
void protect_task(void *argument) {
|
||||
uint8_t start_logged = 0u; /* 启动锚点行留痕(writer-ready 门控,B6) */
|
||||
|
||||
(void)argument;
|
||||
|
||||
for (;;) {
|
||||
uint32_t now = osKernelGetTickCount();
|
||||
uint32_t hf = adc_health_flags();
|
||||
|
||||
/* 启动锚点行:控制台写者注册后首拍补打(2026-10-10 门控纪律,adcTask
|
||||
* 首帧同款)。写者注册前 con_append 走无锁裸发,与 defaultTask 启动日志
|
||||
* 逐字节交错——B6 台架实证:本行曾损坏为 "st[art: cycle=220ms elm=70"
|
||||
* 致 @test 锚点 MISS(控制台单写者日志设计说明.md §3.4)。 */
|
||||
if ((start_logged == 0u) && (dbg_console_writer_ready() != 0u)) {
|
||||
DBG_INFO("protect task start: cycle=%ums elm=%u",
|
||||
(unsigned)PROTECT_TASK_CYCLE_MS, (unsigned)PE_COUNT);
|
||||
start_logged = 1u;
|
||||
}
|
||||
|
||||
if ((hf & PROT_HF_LOCK_MASK) != 0u) {
|
||||
/* L5-4:测量链不可信 → 复位元件(本任务上下文,直接 init 合法)
|
||||
* 并跳过判据;stall 自愈后下一拍自动恢复 */
|
||||
if (s_prev_hf_lock == 0u) {
|
||||
DBG_ERROR("sampling FAULT -> protection blocked (hf=0x%08lX)",
|
||||
(unsigned long)(hf & PROT_HF_LOCK_MASK));
|
||||
}
|
||||
s_prev_hf_lock = 1u;
|
||||
prot_elm_init();
|
||||
} else {
|
||||
if (s_prev_hf_lock != 0u) {
|
||||
DBG_INFO("sampling recovered -> protection resumed");
|
||||
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");
|
||||
s_prev_plate_out = 1u;
|
||||
}
|
||||
prot_elm_init();
|
||||
} else {
|
||||
if (s_prev_plate_out != 0u) {
|
||||
DBG_INFO("global plate IN -> protection enabled");
|
||||
s_prev_plate_out = 0u;
|
||||
}
|
||||
prot_elm_run(now);
|
||||
}
|
||||
}
|
||||
|
||||
s_log_active_edge(prot_elm_active_mask());
|
||||
|
||||
task_wd_beat(TASK_WD_PROTECT);
|
||||
osDelay(PROTECT_TASK_CYCLE_MS);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 模块名称:保护任务(L3 板端接线,B5)
|
||||
* 模块功能:protectTask 线程入口——以固定节拍驱动保护判据
|
||||
* (`prot_elm_run()`:phasor rms → 元件步进),并落实 L5-4 采样
|
||||
* 故障闭锁(`adc_health_flags()` 电平位 → 元件状态复位 + 本拍不判据)。
|
||||
* 判据本体在 `App/algo/prot_elm.c`(纯逻辑,L0.5 已覆盖),本文件
|
||||
* 只做消费/编排(AGENTS 目录地图:task 不装算法)。
|
||||
* 线程约定:仅本任务调 prot_elm_run / prot_set_selfcheck(prot_elm.h 线程约定);
|
||||
* prot_set 写在 P4 前无第二写者(104 定值写经 pts→prot_set,见注)。
|
||||
* 创建日期:2026-10-12(B5)
|
||||
*/
|
||||
#ifndef PROTECT_TASK_H
|
||||
#define PROTECT_TASK_H
|
||||
|
||||
/* 判据节拍(ms):V0.1 元件吃 50 Hz 基波 rms(phasor 每工频周更新)→ 20 ms
|
||||
* = 一个工频周;延时精度由 prot_elm 的 now_ms 差值法保证,与节拍无关。
|
||||
* 规划 §4 的"1 ms 高优先级"是参考系统瞬时值元件口径,随 P2 瞬时值元件再评。 */
|
||||
#define PROTECT_TASK_CYCLE_MS 20u
|
||||
|
||||
/*
|
||||
* 函数功能:protectTask 线程入口(freertos.c USER CODE 区创建)
|
||||
*/
|
||||
void protect_task(void *argument);
|
||||
|
||||
#endif /* PROTECT_TASK_H */
|
||||
@@ -2,7 +2,7 @@
|
||||
* 模块名称:RS485 Communication Task
|
||||
* 模块功能:RS-485 轮询收发、协议帧解析与响应
|
||||
* 状态说明:**预留桩(未启用)**——采集/协议链路尚未实现,产品定义未定
|
||||
* (见 docs/待处理内容.md 阶段 B0/B2)。当前 init/func 为空实现,
|
||||
* (见 docs/任务规划/暂缓-RS485.md,B2 暂缓)。当前 init/func 为空实现,
|
||||
* 不收发、不解析。
|
||||
* 适用平台:STM32F407ZGTx
|
||||
* 作者:王建锋
|
||||
|
||||
+43
-3
@@ -6,6 +6,18 @@
|
||||
* 创建日期:2026-08-30
|
||||
* 修改记录:
|
||||
* v1.1 2026-09-14 王建锋 新增待机显示页初始化(ui_standby_init,空闲 60s 切入)
|
||||
* v1.2 2026-10-10 王建锋 Task #32 的 P2 测量埋点:主循环加常开探针
|
||||
* (循环周期 / 单轮工作耗时 / ui_net_sink_poll 往返),
|
||||
* 用于把"心跳缺席"拆成 饿死 / 阻塞在往返 / 自身拖长
|
||||
* 三种互斥机理(判读见 docs/子系统设计/看门狗设计说明.md §11.4)
|
||||
* v1.3 2026-10-10 王建锋 Task #32 的 P1-4a(解耦心跳,看门狗设计说明 §11.3):把 task_wd_beat
|
||||
* (TASK_WD_UI) 提到 ui_net_sink_poll() **之前**。原写法心跳
|
||||
* 在推流往返之后 → 心跳周期 = 渲染 + 一次 netTask 往返 + 10ms,
|
||||
* 实测 uiw≈uis≈50ms(心跳被 netTask 往返主导),心跳实际测的是
|
||||
* "netTask 响应性"而非"UI 活着"。提到往返之前后,心跳测的是
|
||||
* "UI 自己被调度到了"。注:若往返**永久**阻塞,本改仍不能救
|
||||
* (心跳只发一次)→ 真要彻底去掉依赖须走事件驱动 accept
|
||||
* (NET_EVENT_CONNECTED 回调),待复现快照确认阻塞机理后再定。
|
||||
*/
|
||||
#include "ui_task.h"
|
||||
|
||||
@@ -22,6 +34,7 @@
|
||||
#include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */
|
||||
#include "net_latency_test.h" /* M3 探针:ui_loop(生产循环实测周期) */
|
||||
#include "task_watchdog.h"
|
||||
#include "task_prof.h" /* P2 常开探针:循环周期 / 工作耗时 / 推流往返 */
|
||||
|
||||
/*
|
||||
* 函数功能:待机屏底部时钟提供回调(平台差异:MCU = sys_clock_get_str)
|
||||
@@ -114,7 +127,21 @@ void ui_task_func(void *argument) {
|
||||
uint8_t tm_loop_valid = 0U;
|
||||
#endif
|
||||
|
||||
/* P2 常开探针(Task #32):TEST_LATENCY_PROBE 只在 CH395F 套件构建下打开,
|
||||
* 且样本消费者是 testTask(产品构建里不跑)→ 产品构建拿不到任何 UI 周期数据。
|
||||
* 故此处独立埋点,恒定生效、只写本模块的静态计数,由 defaultTask 周期上报。 */
|
||||
|
||||
while (1) {
|
||||
/* 循环周期用 enter/exit 测:exit 结算"上轮起点 → 本轮起点",enter 记本轮起点。
|
||||
* 首轮 exit 时尚未 enter 过 → 内部直接返回,不会拿 0 当起点。
|
||||
* 选 enter/exit 而非"自己存一个 prev"的额外收益:两次循环之间段内标志保持为 1
|
||||
* → 报告可读出"本轮已进行多久"。**这正是'uiTask 是否被饿住'的直接证据**:
|
||||
* 若该值在两次报告间持续增长而 uic 不动,就是被调度饿死/阻塞,而非渲染拖长。 */
|
||||
task_prof_exit(TASK_PROF_UI_PERIOD);
|
||||
task_prof_enter(TASK_PROF_UI_PERIOD);
|
||||
|
||||
const uint32_t prof_t0 = task_prof_cyc(); /* 单轮工作耗时基准 */
|
||||
|
||||
#if defined(TEST_LATENCY_PROBE)
|
||||
if (tm_loop_valid != 0U) {
|
||||
lat_probe_add_seg("ui_loop", lat_mark_us(&tm_loop));
|
||||
@@ -123,10 +150,23 @@ void ui_task_func(void *argument) {
|
||||
tm_loop_valid = 1U;
|
||||
#endif
|
||||
ui_port_step(10);
|
||||
ui_net_sink_poll();
|
||||
/* 心跳:ui_port_step 内含 LVGL 渲染,单轮耗时可能逼近 10ms 量级;
|
||||
* 100ms 抽样窗内可容纳多轮,此埋点足够 */
|
||||
/* 心跳**先于**推流往返发出(P1-4a,看门狗设计说明 §11.3):UI 心跳测的是
|
||||
* "UI 自身被调度到了",而非"netTask 回执快慢"。原写法心跳在 sink 之后,
|
||||
* 致实测 uiw≈uis≈50ms(心跳被 netTask 往返主导)。提到往返之前后,
|
||||
* 即使往返偶发变慢,心跳也不被其拖垮。
|
||||
* ⚠ 若往返**永久**阻塞(xTaskNotifyWait portMAX_DELAY 不返回),本改只能发
|
||||
* 一次心跳,救不了 → 彻底解耦须走事件驱动 accept,见文件头 v1.3 说明。 */
|
||||
task_wd_beat(TASK_WD_UI);
|
||||
/* 推流轮询段单独 enter/exit:它内部经 net_accept → net_wait_result 走到
|
||||
* xTaskNotifyWait(portMAX_DELAY) 的**无限期阻塞**。若 uiTask 永久卡在这里,
|
||||
* 样本永不结算,只有 task_prof 的"仍在段内已 X us"能证明它卡在何处 */
|
||||
task_prof_enter(TASK_PROF_UI_SINK);
|
||||
ui_net_sink_poll();
|
||||
task_prof_exit(TASK_PROF_UI_SINK);
|
||||
|
||||
/* 单轮工作耗时(不含 osDelay):与循环周期对比即得"调度延迟" */
|
||||
task_prof_sample(TASK_PROF_UI_WORK, task_prof_cyc() - prof_t0);
|
||||
|
||||
osDelay(10);
|
||||
}
|
||||
}
|
||||
|
||||
+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 销项)。
|
||||
> 保留只会造成"有统一实现"的误导,故整体移除(详见 `docs/任务规划/排期详述.md` E-15(C5))。
|
||||
|
||||
## 设计要点
|
||||
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
/*
|
||||
* 模块名称:sha256
|
||||
* 模块功能:SHA-256 安全散列(FIPS 180-4)实现——纯标准库、零堆分配
|
||||
* 适用平台:STM32F407ZGTx / PC 主机侧单测(双端共用)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-11
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-11 王建锋 创建初始版本(N-19 FTP 鉴权;标准向量由
|
||||
* test/host/sha256 主机单测钉死)
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
#include <string.h>
|
||||
|
||||
#include "sha256.h"
|
||||
|
||||
/* 私有宏定义区 */
|
||||
#define SHA256_ROTR32(x, n) (((x) >> (n)) | ((x) << (32u - (n))))
|
||||
#define SHA256_CH(x, y, z) (((x) & (y)) ^ ((~(x)) & (z)))
|
||||
#define SHA256_MAJ(x, y, z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
|
||||
#define SHA256_EP0(x) (SHA256_ROTR32(x, 2u) ^ SHA256_ROTR32(x, 13u) ^ SHA256_ROTR32(x, 22u))
|
||||
#define SHA256_EP1(x) (SHA256_ROTR32(x, 6u) ^ SHA256_ROTR32(x, 11u) ^ SHA256_ROTR32(x, 25u))
|
||||
#define SHA256_SIG0(x) (SHA256_ROTR32(x, 7u) ^ SHA256_ROTR32(x, 18u) ^ ((x) >> 3u))
|
||||
#define SHA256_SIG1(x) (SHA256_ROTR32(x, 17u) ^ SHA256_ROTR32(x, 19u) ^ ((x) >> 10u))
|
||||
|
||||
/* 私有变量定义区 */
|
||||
/* 轮常量(FIPS 180-4 §4.2.2.2:前 64 个素数立方根小数部分前 32 位) */
|
||||
static const uint32_t s_k[64] = {
|
||||
0x428a2f98u, 0x71374491u, 0xb5c0fbcfu, 0xe9b5dba5u,
|
||||
0x3956c25bu, 0x59f111f1u, 0x923f82a4u, 0xab1c5ed5u,
|
||||
0xd807aa98u, 0x12835b01u, 0x243185beu, 0x550c7dc3u,
|
||||
0x72be5d74u, 0x80deb1feu, 0x9bdc06a7u, 0xc19bf174u,
|
||||
0xe49b69c1u, 0xefbe4786u, 0x0fc19dc6u, 0x240ca1ccu,
|
||||
0x2de92c6fu, 0x4a7484aau, 0x5cb0a9dcu, 0x76f988dau,
|
||||
0x983e5152u, 0xa831c66du, 0xb00327c8u, 0xbf597fc7u,
|
||||
0xc6e00bf3u, 0xd5a79147u, 0x06ca6351u, 0x14292967u,
|
||||
0x27b70a85u, 0x2e1b2138u, 0x4d2c6dfcu, 0x53380d13u,
|
||||
0x650a7354u, 0x766a0abbu, 0x81c2c92eu, 0x92722c85u,
|
||||
0xa2bfe8a1u, 0xa81a664bu, 0xc24b8b70u, 0xc76c51a3u,
|
||||
0xd192e819u, 0xd6990624u, 0xf40e3585u, 0x106aa070u,
|
||||
0x19a4c116u, 0x1e376c08u, 0x2748774cu, 0x34b0bcb5u,
|
||||
0x391c0cb3u, 0x4ed8aa4au, 0x5b9cca4fu, 0x682e6ff3u,
|
||||
0x748f82eeu, 0x78a5636fu, 0x84c87814u, 0x8cc70208u,
|
||||
0x90befffau, 0xa4506cebu, 0xbef9a3f7u, 0xc67178f2u,
|
||||
};
|
||||
|
||||
/* 函数定义区 */
|
||||
|
||||
/*
|
||||
* 函数功能:处理一个 64 B 分组(消息扩展 + 64 轮压缩)
|
||||
* 入口参数:p_ctx - 上下文 p_block - 64 B 分组(大端字序)
|
||||
* 返回值:无
|
||||
* 限定条件:p_block 恰为 64 B 有效数据
|
||||
* 函数说明:w[64] 为栈上消息扩展数组(256 B,ftpTask 4 KB 栈可容纳)
|
||||
*/
|
||||
static void s_transform(sha256_ctx_t *p_ctx, const uint8_t *p_block) {
|
||||
uint32_t w[64];
|
||||
uint32_t a;
|
||||
uint32_t b;
|
||||
uint32_t c;
|
||||
uint32_t d;
|
||||
uint32_t e;
|
||||
uint32_t f;
|
||||
uint32_t g;
|
||||
uint32_t h;
|
||||
|
||||
for (uint32_t i = 0u; i < 16u; i++) {
|
||||
w[i] = ((uint32_t)p_block[4u * i] << 24) |
|
||||
((uint32_t)p_block[4u * i + 1u] << 16) |
|
||||
((uint32_t)p_block[4u * i + 2u] << 8) |
|
||||
(uint32_t)p_block[4u * i + 3u];
|
||||
}
|
||||
for (uint32_t i = 16u; i < 64u; i++) {
|
||||
w[i] = SHA256_SIG1(w[i - 2u]) + w[i - 7u] +
|
||||
SHA256_SIG0(w[i - 15u]) + w[i - 16u];
|
||||
}
|
||||
|
||||
a = p_ctx->state[0];
|
||||
b = p_ctx->state[1];
|
||||
c = p_ctx->state[2];
|
||||
d = p_ctx->state[3];
|
||||
e = p_ctx->state[4];
|
||||
f = p_ctx->state[5];
|
||||
g = p_ctx->state[6];
|
||||
h = p_ctx->state[7];
|
||||
|
||||
for (uint32_t i = 0u; i < 64u; i++) {
|
||||
uint32_t t1 = h + SHA256_EP1(e) + SHA256_CH(e, f, g) + s_k[i] + w[i];
|
||||
uint32_t t2 = SHA256_EP0(a) + SHA256_MAJ(a, b, c);
|
||||
h = g;
|
||||
g = f;
|
||||
f = e;
|
||||
e = d + t1;
|
||||
d = c;
|
||||
c = b;
|
||||
b = a;
|
||||
a = t1 + t2;
|
||||
}
|
||||
|
||||
p_ctx->state[0] += a;
|
||||
p_ctx->state[1] += b;
|
||||
p_ctx->state[2] += c;
|
||||
p_ctx->state[3] += d;
|
||||
p_ctx->state[4] += e;
|
||||
p_ctx->state[5] += f;
|
||||
p_ctx->state[6] += g;
|
||||
p_ctx->state[7] += h;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:初始化上下文(清零 + 置初始链接变量,FIPS 180-4 §5.3.3)
|
||||
* 入口参数:p_ctx - 上下文
|
||||
* 返回值:无
|
||||
* 限定条件:p_ctx 为 NULL 时静默返回(fail-safe)
|
||||
*/
|
||||
void sha256_init(sha256_ctx_t *p_ctx) {
|
||||
if (p_ctx == NULL) {
|
||||
return;
|
||||
}
|
||||
(void)memset(p_ctx, 0, sizeof(*p_ctx));
|
||||
/* 初始链接变量(FIPS 180-4 §5.3.3:前 8 个素数平方根小数部分前 32 位) */
|
||||
p_ctx->state[0] = 0x6a09e667u;
|
||||
p_ctx->state[1] = 0xbb67ae85u;
|
||||
p_ctx->state[2] = 0x3c6ef372u;
|
||||
p_ctx->state[3] = 0xa54ff53au;
|
||||
p_ctx->state[4] = 0x510e527fu;
|
||||
p_ctx->state[5] = 0x9b05688cu;
|
||||
p_ctx->state[6] = 0x1f83d9abu;
|
||||
p_ctx->state[7] = 0x5be0cd19u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:流式喂入数据(任意长度,内部按 64 B 分组压缩)
|
||||
* 入口参数:p_ctx - 上下文 p_data - 数据 len - 字节数
|
||||
* 返回值:无
|
||||
* 限定条件:p_ctx/p_data 为 NULL 时静默返回
|
||||
*/
|
||||
void sha256_update(sha256_ctx_t *p_ctx, const void *p_data, size_t len) {
|
||||
const uint8_t *p = (const uint8_t *)p_data;
|
||||
|
||||
if ((p_ctx == NULL) || (p == NULL)) {
|
||||
return;
|
||||
}
|
||||
for (size_t i = 0u; i < len; i++) {
|
||||
p_ctx->buf[p_ctx->buf_len] = p[i];
|
||||
p_ctx->buf_len++;
|
||||
if (p_ctx->buf_len >= SHA256_BLOCK_SIZE) {
|
||||
s_transform(p_ctx, p_ctx->buf);
|
||||
p_ctx->bitlen += (uint64_t)SHA256_BLOCK_SIZE * 8u;
|
||||
p_ctx->buf_len = 0u;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:收尾输出 32 B 摘要(补位 + 长度字段 + 末次压缩)
|
||||
* 入口参数:p_ctx - 上下文 p_out - 32 B 摘要输出
|
||||
* 返回值:无
|
||||
* 限定条件:p_ctx/p_out 为 NULL 时静默返回;收尾后上下文清零(防敏感残留)
|
||||
*/
|
||||
void sha256_final(sha256_ctx_t *p_ctx, uint8_t p_out[SHA256_DIGEST_SIZE]) {
|
||||
uint64_t bitlen;
|
||||
uint8_t pad = 0x80u;
|
||||
uint8_t zero = 0x00u;
|
||||
uint8_t len_be[8];
|
||||
|
||||
if ((p_ctx == NULL) || (p_out == NULL)) {
|
||||
return;
|
||||
}
|
||||
/* 总长 = 完整分组累计 + 暂存区(必须在补位前定稿) */
|
||||
bitlen = p_ctx->bitlen + (uint64_t)p_ctx->buf_len * 8u;
|
||||
|
||||
(void)sha256_update(p_ctx, &pad, 1u);
|
||||
while (p_ctx->buf_len != 56u) {
|
||||
(void)sha256_update(p_ctx, &zero, 1u);
|
||||
}
|
||||
for (uint32_t i = 0u; i < 8u; i++) {
|
||||
len_be[i] = (uint8_t)((bitlen >> (56u - 8u * i)) & 0xFFu);
|
||||
}
|
||||
(void)sha256_update(p_ctx, len_be, 8u);
|
||||
|
||||
for (uint32_t i = 0u; i < 8u; i++) {
|
||||
p_out[4u * i] = (uint8_t)(p_ctx->state[i] >> 24);
|
||||
p_out[4u * i + 1u] = (uint8_t)(p_ctx->state[i] >> 16);
|
||||
p_out[4u * i + 2u] = (uint8_t)(p_ctx->state[i] >> 8);
|
||||
p_out[4u * i + 3u] = (uint8_t)(p_ctx->state[i]);
|
||||
}
|
||||
(void)memset(p_ctx, 0, sizeof(*p_ctx));
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:一次性计算 32 B 摘要(init+update+final 便捷封装)
|
||||
* 入口参数:p_data - 数据 len - 字节数 p_out - 摘要输出
|
||||
* 返回值:无
|
||||
* 限定条件:栈上上下文 ≈108 B;NULL 参数由子函数兜底
|
||||
*/
|
||||
void sha256_calc(const void *p_data, size_t len, uint8_t p_out[SHA256_DIGEST_SIZE]) {
|
||||
sha256_ctx_t ctx;
|
||||
|
||||
sha256_init(&ctx);
|
||||
sha256_update(&ctx, p_data, len);
|
||||
sha256_final(&ctx, p_out);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:hex 字符 → 半字节值
|
||||
* 入口参数:c - 字符 p_out - 输出 0~15
|
||||
* 返回值:0 - 合法;-1 - 非法 int
|
||||
* 限定条件:p_out 非空
|
||||
*/
|
||||
static int s_hex_nibble(char c, uint8_t *p_out) {
|
||||
uint8_t v;
|
||||
|
||||
if ((c >= '0') && (c <= '9')) {
|
||||
v = (uint8_t)(c - '0');
|
||||
} else if ((c >= 'a') && (c <= 'f')) {
|
||||
v = (uint8_t)(c - 'a') + 10u;
|
||||
} else if ((c >= 'A') && (c <= 'F')) {
|
||||
v = (uint8_t)(c - 'A') + 10u;
|
||||
} else {
|
||||
return -1;
|
||||
}
|
||||
*p_out = v;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:摘要与 64 位 hex 串恒定时间比较(大小写不敏感)
|
||||
* 入口参数:p_digest - 32 B 摘要 p_hex - 64 字符 hex 串
|
||||
* 返回值:1 - 相等;0 - 不等 / 长度非 64 / 非法 hex / NULL
|
||||
* 限定条件:比较段不短路(XOR 累加器);hex 非法属配置错误,早退不构成口令侧信道
|
||||
*/
|
||||
int sha256_hex_eq(const uint8_t p_digest[SHA256_DIGEST_SIZE], const char *p_hex) {
|
||||
uint8_t acc = 0u;
|
||||
uint8_t hi;
|
||||
uint8_t lo;
|
||||
|
||||
if ((p_digest == NULL) || (p_hex == NULL)) {
|
||||
return 0;
|
||||
}
|
||||
if (strlen(p_hex) != (SHA256_DIGEST_SIZE * 2u)) {
|
||||
return 0;
|
||||
}
|
||||
for (uint32_t i = 0u; i < SHA256_DIGEST_SIZE; i++) {
|
||||
if ((s_hex_nibble(p_hex[2u * i], &hi) != 0) ||
|
||||
(s_hex_nibble(p_hex[2u * i + 1u], &lo) != 0)) {
|
||||
/* hex 非法 = 配置错误(可信来源),早退不构成口令侧信道 */
|
||||
return 0;
|
||||
}
|
||||
acc |= (uint8_t)(p_digest[i] ^ (uint8_t)((hi << 4) | lo));
|
||||
}
|
||||
return (acc == 0u) ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* 模块名称:sha256
|
||||
* 模块功能:SHA-256 安全散列(FIPS 180-4)——流式 + 一次性接口,外加摘要与
|
||||
* 64 位 hex 串的恒定时间比较;纯标准库、零堆分配,MCU 与主机侧
|
||||
* 单测共用(无平台依赖)
|
||||
* 适用平台:STM32F407ZGTx / PC 主机侧单测(双端共用)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-11
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-11 王建锋 创建初始版本(N-19 FTP 鉴权:[ftp] pwd_hash 校验)
|
||||
*/
|
||||
|
||||
#ifndef SHA256_H
|
||||
#define SHA256_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#define SHA256_DIGEST_SIZE 32u /* 摘要字节数 */
|
||||
#define SHA256_BLOCK_SIZE 64u /* 分组字节数 */
|
||||
|
||||
/* 散列上下文(≈108 B,调用方栈上或静态均可) */
|
||||
typedef struct {
|
||||
uint32_t state[8]; /* 链接变量 */
|
||||
uint64_t bitlen; /* 已处理的完整分组累计比特数 */
|
||||
uint8_t buf[SHA256_BLOCK_SIZE];/* 未满分组的暂存区 */
|
||||
uint32_t buf_len; /* 暂存区已用字节数 */
|
||||
} sha256_ctx_t;
|
||||
|
||||
/*
|
||||
* 函数功能:初始化散列上下文
|
||||
* 入口参数:p_ctx - 上下文(不可为 NULL) sha256_ctx_t *
|
||||
* 返回值:无
|
||||
* 限定条件:无
|
||||
*/
|
||||
void sha256_init(sha256_ctx_t *p_ctx);
|
||||
|
||||
/*
|
||||
* 函数功能:喂入一段消息(可多次调用,任意切分)
|
||||
* 入口参数:p_ctx - 上下文 p_data - 数据 len - 字节数
|
||||
* 返回值:无
|
||||
* 限定条件:p_ctx 已 init;p_data 为 NULL 或 len=0 时空操作
|
||||
*/
|
||||
void sha256_update(sha256_ctx_t *p_ctx, const void *p_data, size_t len);
|
||||
|
||||
/*
|
||||
* 函数功能:补位收尾并输出 32 B 摘要(调用后上下文被清零,不可续用)
|
||||
* 入口参数:p_ctx - 上下文 p_out - 32 B 输出缓冲(不可为 NULL)
|
||||
* 返回值:无
|
||||
* 限定条件:p_ctx 已 init
|
||||
*/
|
||||
void sha256_final(sha256_ctx_t *p_ctx, uint8_t p_out[SHA256_DIGEST_SIZE]);
|
||||
|
||||
/*
|
||||
* 函数功能:一次性计算整段消息的 SHA-256
|
||||
* 入口参数:p_data - 数据 len - 字节数 p_out - 32 B 输出缓冲
|
||||
* 返回值:无
|
||||
* 限定条件:p_data/p_out 非空
|
||||
*/
|
||||
void sha256_calc(const void *p_data, size_t len, uint8_t p_out[SHA256_DIGEST_SIZE]);
|
||||
|
||||
/*
|
||||
* 函数功能:恒定时间比较 32 B 摘要与 64 字符 hex 串(大小写不敏感)
|
||||
* 入口参数:p_digest - 32 B 摘要 p_hex - 64 字符 hex 串(可 NULL)
|
||||
* 返回值:1 - 相等;0 - 不等 / hex 非法 / 参数为空 int
|
||||
* 函数说明:hex 来自配置文件(可信来源),格式非法早退不构成侧信道;
|
||||
* 摘要与 hex 内容的比较逐字节累积异或,耗时只随长度(固定 64)变化
|
||||
*/
|
||||
int sha256_hex_eq(const uint8_t p_digest[SHA256_DIGEST_SIZE], const char *p_hex);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* SHA256_H */
|
||||
@@ -0,0 +1,300 @@
|
||||
/*
|
||||
* 模块名称:Task Profiler(任务耗时最坏值探针)实现
|
||||
* 模块功能:接口见 task_prof.h。本文件只做三件事:
|
||||
* ① 惰性使能 DWT CYCCNT 并读值;② 维护每探针的 count/max/last/glitch
|
||||
* 与"在段内"标志;③ 把全部字段格式化成一整行 `[PROF]` 交给控制台单写者。
|
||||
* 适用平台:STM32F407ZGTx(仅 MCU 端)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-10 王建锋 创建(Task #32 的 P2 测量埋点)
|
||||
*
|
||||
* 设计依据:docs/子系统设计/看门狗设计说明.md §11.4(探针字段语义与实测基线)
|
||||
*/
|
||||
|
||||
#include "task_prof.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include "main.h" /* DWT / CoreDebug / SystemCoreClock / HAL_GetTick() */
|
||||
#include "console_log.h" /* dbg_console_write_line / dbg_console_write_raw */
|
||||
|
||||
/* Private defines -----------------------------------------------------------*/
|
||||
|
||||
/* 报告节奏:首次延后(bring-up 深链期间不占串口),此后周期一行。
|
||||
* 2 s 与 defaultTask 的 [STACK] 报告同节奏,便于两条序列交叉比对。 */
|
||||
#define TASK_PROF_FIRST_MS 5000u
|
||||
#define TASK_PROF_PERIOD_MS 2000u
|
||||
|
||||
/* 合理耗时上界(周期数)。超过即判为"CYCCNT 中途被重置"造成的无符号回绕
|
||||
* (差值 ~2^32),丢弃而不污染 max。取 10 s 对应周期数:远高于真实最坏段
|
||||
* 耗时(停摆窗口 < 2.5 s 即复位),又远低于 2^32,不会误杀真实数据。
|
||||
* 168 MHz 下 10 s = 1.68e9 < 2^32,本身不溢出。 */
|
||||
#define TASK_PROF_GLITCH_CYC (SystemCoreClock * 10u)
|
||||
|
||||
/* Private types -------------------------------------------------------------*/
|
||||
|
||||
typedef struct {
|
||||
volatile uint32_t count; /* 已结算样本数 */
|
||||
volatile uint32_t max; /* 最坏耗时(周期数) */
|
||||
volatile uint32_t last; /* 最近一次耗时(周期数) */
|
||||
volatile uint32_t glitch; /* 丢弃的样本数(DWT 不可用 / 计数器被重置) */
|
||||
volatile uint32_t t_start; /* 进段时刻(周期数;仅 enter/exit 类探针使用) */
|
||||
volatile uint32_t in_seg; /* 1 = 此刻仍在段内(读侧据此算"已停留多久") */
|
||||
} task_prof_slot_t;
|
||||
|
||||
/* Private variables ---------------------------------------------------------*/
|
||||
|
||||
static task_prof_slot_t s_prof[TASK_PROF_ID_COUNT];
|
||||
static uint8_t s_dwt_ok; /* 1 = DWT CYCCNT 已使能 */
|
||||
static uint8_t s_prof_reported; /* 1 = 已首次报告(节流状态) */
|
||||
static uint32_t s_prof_last; /* 上次报告时刻(ms) */
|
||||
static char s_prof_line[DBG_CON_LINE_MAX]; /* 报告行缓冲(仅 defaultTask 用) */
|
||||
|
||||
/* Private function prototypes -----------------------------------------------*/
|
||||
|
||||
static uint32_t prof_cyc_to_us(uint32_t cyc);
|
||||
static void prof_send(int n, uint32_t raw);
|
||||
static void prof_emit(uint32_t raw);
|
||||
|
||||
/*
|
||||
* 函数功能:周期数 → 微秒
|
||||
* 入口参数:cyc - 周期数
|
||||
* 返回值:微秒数(SystemCoreClock 低于 1 MHz 时返回 0)
|
||||
* 限定条件:任意上下文可调(纯算术)
|
||||
* 函数说明:MicroLIB 的 snprintf 不支持 %f → 一律以整数微秒上报。
|
||||
*/
|
||||
static uint32_t prof_cyc_to_us(uint32_t cyc) {
|
||||
uint32_t per_us = SystemCoreClock / 1000000u;
|
||||
|
||||
if (per_us == 0u) {
|
||||
return 0u;
|
||||
}
|
||||
return cyc / per_us;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:把已格式化在静态行缓冲里的行按指定通道发出
|
||||
* 入口参数:n - snprintf 返回的长度(<= 0 = 格式化失败,丢弃)
|
||||
* raw - 0 = 入环形缓冲;非 0 = 裸 UART 直发
|
||||
* 返回值:无
|
||||
* 限定条件:仅 defaultTask 调用(唯一串口写者)
|
||||
* 函数说明:长度按静态缓冲容量夹取,防 snprintf 截断后越界。
|
||||
*/
|
||||
static void prof_send(int n, uint32_t raw) {
|
||||
const unsigned cap = (unsigned)sizeof(s_prof_line);
|
||||
unsigned len;
|
||||
|
||||
if (n <= 0) {
|
||||
return;
|
||||
}
|
||||
len = ((unsigned)n >= cap) ? (cap - 1u) : (unsigned)n;
|
||||
|
||||
if (raw != 0u) {
|
||||
dbg_console_write_raw(s_prof_line, len);
|
||||
} else {
|
||||
(void)dbg_console_write_line(s_prof_line, len);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:格式化并输出 `[PROF]` + `[PROF2]` 两行(静态行缓冲,零栈开销)
|
||||
* 入口参数:raw - 0 = 入环形缓冲(周期报告);非 0 = 裸 UART 直发(停摆现场)
|
||||
* 返回值:无
|
||||
* 限定条件:仅 defaultTask 调用(唯一串口写者,无并发;故可复用静态缓冲)
|
||||
* 函数说明:字段含义(括号内为打印单位):
|
||||
* [PROF] t HAL tick(ms)
|
||||
* uic/uip/uipn/uil uiTask 循环次数 / 周期最坏 / 本轮已进行 / 周期最近(us)
|
||||
* uiw/uis/uisn uiTask 单轮工作最坏 / 推流往返最坏 / 仍在推流段内(us)
|
||||
* adc/adcm/adcn adcTask 单拍次数 / 单拍最坏 / 仍在拍内(us)
|
||||
* [PROF2] defd/defs defaultTask 控制台排空最坏 / syslog 落盘最坏(us)
|
||||
* netp/netn netTask 驱动轮询最坏 / 仍在轮询内(us)
|
||||
* adcp/adcq adcTask ADC 单次最坏 / 仍在 ADC 内(us)
|
||||
* 两行的 gl 丢弃样本数(DWT 不可用或计数器被重置)
|
||||
* "仍在段内"为 0 表示不在该段内——某任务若永久阻塞在某段,该段的
|
||||
* count 不再增长,只有这个字段能证明它卡在哪里。
|
||||
*/
|
||||
static void prof_emit(uint32_t raw) {
|
||||
const uint32_t t = HAL_GetTick();
|
||||
const uint32_t now = task_prof_cyc();
|
||||
uint32_t uipn = 0u;
|
||||
uint32_t uisn = 0u;
|
||||
uint32_t adcn = 0u;
|
||||
uint32_t netn = 0u;
|
||||
uint32_t adcq = 0u;
|
||||
uint32_t gl = 0u;
|
||||
uint32_t i;
|
||||
int n;
|
||||
|
||||
if (s_prof[TASK_PROF_UI_PERIOD].in_seg != 0u) {
|
||||
uipn = now - s_prof[TASK_PROF_UI_PERIOD].t_start;
|
||||
}
|
||||
if (s_prof[TASK_PROF_UI_SINK].in_seg != 0u) {
|
||||
uisn = now - s_prof[TASK_PROF_UI_SINK].t_start;
|
||||
}
|
||||
if (s_prof[TASK_PROF_ADC_BEAT].in_seg != 0u) {
|
||||
adcn = now - s_prof[TASK_PROF_ADC_BEAT].t_start;
|
||||
}
|
||||
if (s_prof[TASK_PROF_NET_POLL].in_seg != 0u) {
|
||||
netn = now - s_prof[TASK_PROF_NET_POLL].t_start;
|
||||
}
|
||||
if (s_prof[TASK_PROF_ADC_POLL].in_seg != 0u) {
|
||||
adcq = now - s_prof[TASK_PROF_ADC_POLL].t_start;
|
||||
}
|
||||
for (i = 0u; i < (uint32_t)TASK_PROF_ID_COUNT; i++) {
|
||||
gl += s_prof[i].glitch;
|
||||
}
|
||||
|
||||
/* 行 1:任务级(uiTask / adcTask) */
|
||||
n = snprintf(s_prof_line, sizeof(s_prof_line),
|
||||
"[PROF] t=%lu uic=%lu uip=%lu uipn=%lu uil=%lu uiw=%lu uis=%lu "
|
||||
"uisn=%lu adc=%lu adcm=%lu adcn=%lu gl=%lu\r\n",
|
||||
(unsigned long)t,
|
||||
(unsigned long)s_prof[TASK_PROF_UI_PERIOD].count,
|
||||
(unsigned long)prof_cyc_to_us(s_prof[TASK_PROF_UI_PERIOD].max),
|
||||
(unsigned long)prof_cyc_to_us(uipn),
|
||||
(unsigned long)prof_cyc_to_us(s_prof[TASK_PROF_UI_PERIOD].last),
|
||||
(unsigned long)prof_cyc_to_us(s_prof[TASK_PROF_UI_WORK].max),
|
||||
(unsigned long)prof_cyc_to_us(s_prof[TASK_PROF_UI_SINK].max),
|
||||
(unsigned long)prof_cyc_to_us(uisn),
|
||||
(unsigned long)s_prof[TASK_PROF_ADC_BEAT].count,
|
||||
(unsigned long)prof_cyc_to_us(s_prof[TASK_PROF_ADC_BEAT].max),
|
||||
(unsigned long)prof_cyc_to_us(adcn),
|
||||
(unsigned long)gl);
|
||||
prof_send(n, raw);
|
||||
|
||||
/* 行 2:段级(占用者指认:defaultTask 排空/落盘、netTask 驱动、adcTask ADC) */
|
||||
n = snprintf(s_prof_line, sizeof(s_prof_line),
|
||||
"[PROF2] t=%lu defd=%lu defs=%lu netp=%lu netn=%lu adcp=%lu "
|
||||
"adcq=%lu gl=%lu\r\n",
|
||||
(unsigned long)t,
|
||||
(unsigned long)prof_cyc_to_us(s_prof[TASK_PROF_DEF_DRAIN].max),
|
||||
(unsigned long)prof_cyc_to_us(s_prof[TASK_PROF_DEF_SYSLOG].max),
|
||||
(unsigned long)prof_cyc_to_us(s_prof[TASK_PROF_NET_POLL].max),
|
||||
(unsigned long)prof_cyc_to_us(netn),
|
||||
(unsigned long)prof_cyc_to_us(s_prof[TASK_PROF_ADC_POLL].max),
|
||||
(unsigned long)prof_cyc_to_us(adcq),
|
||||
(unsigned long)gl);
|
||||
prof_send(n, raw);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:读 DWT 周期计数器(懒使能;见 task_prof.h 的契约说明)
|
||||
* 入口参数:无
|
||||
* 返回值:当前 CYCCNT 值(周期数)
|
||||
* 限定条件:任意上下文可调(幂等;纯寄存器读写)
|
||||
* 函数说明:只置使能位、**不清零** CYCCNT——本模块只用差值,清零无意义,
|
||||
* 且会与 console_log.c 的同名惰性使能互相干扰(该模块对 CYCCNT
|
||||
* 清零后,在途的差值会变成一次无符号回绕,由 glitch 守卫丢弃)。
|
||||
*/
|
||||
uint32_t task_prof_cyc(void) {
|
||||
if (s_dwt_ok == 0u) {
|
||||
CoreDebug->DEMCR |= CoreDebug_DEMCR_TRCENA_Msk;
|
||||
DWT->CTRL |= DWT_CTRL_CYCCNTENA_Msk;
|
||||
if ((DWT->CTRL & DWT_CTRL_CYCCNTENA_Msk) != 0u) {
|
||||
s_dwt_ok = 1u;
|
||||
}
|
||||
}
|
||||
return DWT->CYCCNT;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:记录一次耗时样本(周期数)
|
||||
* 入口参数:id - 探针 ID;cyc - 耗时(周期数)
|
||||
* 返回值:无
|
||||
* 限定条件:单写者(每个 ID 只由一个任务调用)
|
||||
* 函数说明:不可用样本(cyc 为 0 或超出合理上界)只累加 glitch,不更新
|
||||
* max/last/count——否则一次计数器重置就会把最坏值抬到 ~2^32 周期。
|
||||
*/
|
||||
void task_prof_sample(task_prof_id_t id, uint32_t cyc) {
|
||||
task_prof_slot_t *p;
|
||||
|
||||
if ((uint32_t)id >= (uint32_t)TASK_PROF_ID_COUNT) {
|
||||
return;
|
||||
}
|
||||
p = &s_prof[id];
|
||||
|
||||
if ((cyc == 0u) || (cyc >= TASK_PROF_GLITCH_CYC)) {
|
||||
p->glitch++;
|
||||
return;
|
||||
}
|
||||
p->count++;
|
||||
p->last = cyc;
|
||||
if (cyc > p->max) {
|
||||
p->max = cyc;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:标记"进入某段"(观测"此刻仍在段内已多久")
|
||||
* 入口参数:id - 探针 ID
|
||||
* 返回值:无
|
||||
* 限定条件:单写者;须与 task_prof_exit 成对
|
||||
* 函数说明:写序为先 t_start 后 in_seg——读侧见 in_seg 为 1 时 t_start 必已有效。
|
||||
*/
|
||||
void task_prof_enter(task_prof_id_t id) {
|
||||
task_prof_slot_t *p;
|
||||
|
||||
if ((uint32_t)id >= (uint32_t)TASK_PROF_ID_COUNT) {
|
||||
return;
|
||||
}
|
||||
p = &s_prof[id];
|
||||
p->t_start = task_prof_cyc();
|
||||
p->in_seg = 1u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:结算一段耗时(与 task_prof_enter 成对)
|
||||
* 入口参数:id - 探针 ID
|
||||
* 返回值:无
|
||||
* 限定条件:单写者;须与 task_prof_enter 成对
|
||||
* 函数说明:先清"在段内"标志再结算,避免读侧把已结算的段误报为"仍在段内"。
|
||||
* **未 enter 过则直接返回**:让"先 exit 再 enter"的周期测量写法
|
||||
* 在首轮不把 0 当作起点(否则会记下一次虚假的超大样本)。
|
||||
*/
|
||||
void task_prof_exit(task_prof_id_t id) {
|
||||
uint32_t t0;
|
||||
|
||||
if ((uint32_t)id >= (uint32_t)TASK_PROF_ID_COUNT) {
|
||||
return;
|
||||
}
|
||||
if (s_prof[id].in_seg == 0u) {
|
||||
return;
|
||||
}
|
||||
t0 = s_prof[id].t_start;
|
||||
s_prof[id].in_seg = 0u;
|
||||
task_prof_sample(id, task_prof_cyc() - t0);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:周期性打印 [PROF] 行(内部节流)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:仅 defaultTask 主循环调用
|
||||
* 函数说明:首次 5 s 后报告(bring-up 深链已跑过),此后每 2 s 一行。
|
||||
*/
|
||||
void task_prof_report(void) {
|
||||
const uint32_t now = HAL_GetTick();
|
||||
|
||||
if (s_prof_reported == 0u) {
|
||||
if (now < TASK_PROF_FIRST_MS) {
|
||||
return;
|
||||
}
|
||||
s_prof_reported = 1u;
|
||||
} else if ((uint32_t)(now - s_prof_last) < TASK_PROF_PERIOD_MS) {
|
||||
return;
|
||||
}
|
||||
s_prof_last = now;
|
||||
prof_emit(0u);
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:停摆现场快照(同一 [PROF] 行,走裸 UART 直发)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:仅 defaultTask 调用(此时它就是唯一串口写者,无并发)
|
||||
* 函数说明:用于"首次心跳缺席"瞬间;缺席窗口内环形缓冲可能积压,
|
||||
* 裸直发保证这一行能出去。
|
||||
*/
|
||||
void task_prof_report_raw(void) {
|
||||
prof_emit(1u);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* 模块名称:Task Profiler(任务耗时最坏值探针)
|
||||
* 模块功能:用内核 DWT 周期计数器(CYCCNT,168 MHz 主频下 168 周期 = 1 us)为
|
||||
* 各任务主循环的**最坏耗时 / 执行次数 / "此刻是否仍在某段内"** 提供
|
||||
* 常开、低开销的观测面,经控制台单写者输出 `[PROF]` 行。
|
||||
* 用途:把"某任务心跳缺席"这一结果拆成三种互斥机理,供归因定位。
|
||||
* 适用平台:STM32F407ZGTx(仅 MCU 端:依赖 DWT 与 HAL 时基;模拟器/主机单测
|
||||
* 不编译本模块)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-10
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-10 王建锋 创建(Task #32 的 P2 测量埋点)
|
||||
*
|
||||
* 设计背景(docs/子系统设计/看门狗设计说明.md §11.4):
|
||||
* uiTask 偶发心跳缺席导致 IWDG 复位,但"占用者"无法从既有日志判定。
|
||||
* 同一现象至少对应三种互斥机理,观测上必须能区分:
|
||||
* ① 被调度饿死 → 循环周期长,而段内耗时短;
|
||||
* ② 阻塞在网络往返 → `ui_net_sink_poll()` 段内耗时长;若永久阻塞,
|
||||
* 该段样本**永不结算**,只有"此刻仍在段内已 X us"能证明;
|
||||
* ③ 自身渲染拖长 → 单轮工作耗时长。
|
||||
* 故每个探针记录 max / count / last,并对可疑段记录 enter/exit。
|
||||
*
|
||||
* ⚠ 开销:一次采样 = 1 次 DWT 读 + 数次比较/存储(< 20 周期);对 1 ms 节拍
|
||||
* (168000 周期)可忽略。无动态分配、无内核对象、无临界区、无阻塞。
|
||||
* ⚠ 单写者假设:同一探针 ID 只由**一个任务**写入(见下表),报告由
|
||||
* defaultTask 读取。32 位对齐读写在 Cortex-M4 上原子,读侧不会撕裂。
|
||||
*
|
||||
* 探针 ID 写入者 含义(enter/exit = 可读出"此刻仍在段内已多久")
|
||||
* UI_PERIOD uiTask 上轮循环起点 → 本轮循环起点(enter/exit)
|
||||
* UI_WORK uiTask 本轮起点 → 心跳前(含 LVGL 渲染 + 推流轮询)
|
||||
* UI_SINK uiTask ui_net_sink_poll() 单次往返(enter/exit)
|
||||
* ADC_BEAT adcTask 单拍起点 → 心跳后(含取数 + 压环 + 相量 + 心跳日志)
|
||||
* ADC_POLL adcTask adc_input_poll() 单次(enter/exit)——隔离 ADC 超时成本
|
||||
* DEF_DRAIN defaultTask dbg_console_drain()(enter/exit)——控制台忙等发送
|
||||
* DEF_SYSLOG defaultTask syslog_drain()(enter/exit)——NAND/FTL 写
|
||||
* NET_POLL netTask net_poll()(enter/exit)——CH395F 驱动轮询
|
||||
*
|
||||
* 后四个探针用于**指认占用者**:UI 心跳缺席时,谁的最坏耗时在同一窗口抬起来,
|
||||
* 谁就是挤占 CPU 的 Normal 任务(见 docs/子系统设计/看门狗设计说明.md §11.2 第 4 步)。
|
||||
*/
|
||||
|
||||
#ifndef TASK_PROF_H
|
||||
#define TASK_PROF_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
/* 探针 ID(顺序即内部槽位下标,新增只能追加) */
|
||||
typedef enum {
|
||||
TASK_PROF_UI_PERIOD = 0, /* uiTask 循环周期 */
|
||||
TASK_PROF_UI_WORK = 1, /* uiTask 单轮工作耗时(不含 osDelay) */
|
||||
TASK_PROF_UI_SINK = 2, /* ui_net_sink_poll() 往返耗时 */
|
||||
TASK_PROF_ADC_BEAT = 3, /* adcTask 单拍耗时 */
|
||||
TASK_PROF_ADC_POLL = 4, /* adc_input_poll() 耗时(隔离 ADC 超时成本) */
|
||||
TASK_PROF_DEF_DRAIN = 5, /* defaultTask 控制台排空耗时 */
|
||||
TASK_PROF_DEF_SYSLOG = 6, /* defaultTask syslog 落盘耗时 */
|
||||
TASK_PROF_NET_POLL = 7, /* netTask net_poll() 耗时(CH395F 驱动) */
|
||||
TASK_PROF_ID_COUNT
|
||||
} task_prof_id_t;
|
||||
|
||||
/*
|
||||
* 函数功能:读 DWT 周期计数器(懒使能;基准为"任意",仅供取差值)
|
||||
* 入口参数:无
|
||||
* 返回值:当前 CYCCNT 值(周期数)
|
||||
* 限定条件:任意上下文可调(幂等;纯寄存器读,不创建任何内核对象)
|
||||
* 函数说明:首次调用置 CoreDebug->DEMCR.TRCENA 与 DWT->CTRL.CYCCNTENA。
|
||||
* 刻意**不清零** CYCCNT:本模块只用差值,清零无意义,且会与
|
||||
* console_log.c 的同名惰性使能互相干扰。DWT 使能失败时恒返回 0,
|
||||
* 采样侧把 0 判为"不可用"(见 task_prof_sample)。
|
||||
*/
|
||||
uint32_t task_prof_cyc(void);
|
||||
|
||||
/*
|
||||
* 函数功能:记录一次耗时样本(周期数)
|
||||
* 入口参数:id - 探针 ID
|
||||
* cyc - 耗时(周期数;0 或超出合理上界视为不可用/计数器被重置)
|
||||
* 返回值:无
|
||||
* 限定条件:单写者(每个 ID 只由一个任务调用)
|
||||
* 函数说明:更新 count / last / max。`cyc == 0`(DWT 未使能)或
|
||||
* `cyc >= TASK_PROF_GLITCH_CYC`(计数器中途被重置造成的无符号回绕)
|
||||
* 只累加 glitch 计数、不污染 max——否则一次计数器重置就会把
|
||||
* 最坏值抬到 ~2^32 周期,掩盖真实数据。
|
||||
*/
|
||||
void task_prof_sample(task_prof_id_t id, uint32_t cyc);
|
||||
|
||||
/*
|
||||
* 函数功能:标记"进入某段"(用于观测"此刻仍在段内已多久")
|
||||
* 入口参数:id - 探针 ID
|
||||
* 返回值:无
|
||||
* 限定条件:单写者;须与 task_prof_exit 成对
|
||||
* 函数说明:记录进入时刻并置"在段内"标志。若该任务此后永久阻塞在该段,
|
||||
* 样本永不结算,只有报告侧的"仍在段内"字段能证明它卡在哪里。
|
||||
*/
|
||||
void task_prof_enter(task_prof_id_t id);
|
||||
|
||||
/*
|
||||
* 函数功能:结算一段耗时(与 task_prof_enter 成对)
|
||||
* 入口参数:id - 探针 ID
|
||||
* 返回值:无
|
||||
* 限定条件:单写者;须与 task_prof_enter 成对
|
||||
* 函数说明:清"在段内"标志 → 以 enter 时刻为起点记一次样本。
|
||||
* **未 enter 过(in_seg == 0)时直接返回**——这使"先 exit 再 enter"的
|
||||
* 周期测量写法(如 uiTask 的循环周期)在首轮不会拿 0 当起点,
|
||||
* 从而不产生一次虚假的超大样本。
|
||||
*/
|
||||
void task_prof_exit(task_prof_id_t id);
|
||||
|
||||
/*
|
||||
* 函数功能:周期性打印 `[PROF]` / `[PROF2]` 行(内部节流:首次 5 s 后,此后每 2 s)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:仅 defaultTask 主循环调用(唯一串口写者;静态行缓冲复用)
|
||||
* 函数说明:整行一次 `dbg_console_write_line` 入环形缓冲(行级原子)。
|
||||
* `[PROF]` = uiTask/adcTask 任务级(周期 / 工作 / 往返 / 单拍);
|
||||
* `[PROF2]` = 占用者段级(defaultTask 排空与落盘、netTask 驱动轮询、
|
||||
* adcTask ADC 单次),用于指认"谁把 CPU 占住了"。
|
||||
* 用**静态**行缓冲而非栈缓冲:本函数挂在 defaultTask 的最坏调用链上,
|
||||
* 栈上再放一个 192 B 行缓冲会挤占该任务的栈预算。
|
||||
*/
|
||||
void task_prof_report(void);
|
||||
|
||||
/*
|
||||
* 函数功能:停摆现场快照——同 `[PROF]` + `[PROF2]` 两行,走**裸 UART 直发**
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:仅 defaultTask 调用(此时它就是唯一串口写者,无并发)
|
||||
* 函数说明:用于"某任务心跳刚由'到过'变为'缺席'"的瞬间。缺席窗口内环形缓冲
|
||||
* 可能积压,裸直发保证这两行能出去(与 task_wd_poll 的 `MISS start`
|
||||
* 标记同款理由)。
|
||||
*/
|
||||
void task_prof_report_raw(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* TASK_PROF_H */
|
||||
@@ -191,7 +191,9 @@ static uint32_t wd_required_mask(void) {
|
||||
| (1u << (uint32_t)TASK_WD_FTP)
|
||||
| (1u << (uint32_t)TASK_WD_UI)
|
||||
| (1u << (uint32_t)TASK_WD_TIMESYNC)
|
||||
| (1u << (uint32_t)TASK_WD_ADC); /* adcTask 不依赖任何屏障,任何构建都置位 */
|
||||
| (1u << (uint32_t)TASK_WD_ADC) /* adcTask 不依赖任何屏障,任何构建都置位 */
|
||||
| (1u << (uint32_t)TASK_WD_IEC104) /* iec104Task 同 adcTask:任何构建都创建 */
|
||||
| (1u << (uint32_t)TASK_WD_PROTECT);/* protectTask 同:判据节拍必须心跳 */
|
||||
|
||||
#if defined(ENABLE_HW_LAYER_TESTS)
|
||||
/* 见函数说明:CH395F 硬件层测试构建 = default + 屏障等待者(等待分片喂位)
|
||||
@@ -200,7 +202,9 @@ static uint32_t wd_required_mask(void) {
|
||||
| (1u << (uint32_t)TASK_WD_FTP)
|
||||
| (1u << (uint32_t)TASK_WD_UI)
|
||||
| (1u << (uint32_t)TASK_WD_TIMESYNC)
|
||||
| (1u << (uint32_t)TASK_WD_ADC);
|
||||
| (1u << (uint32_t)TASK_WD_ADC)
|
||||
| (1u << (uint32_t)TASK_WD_IEC104)
|
||||
| (1u << (uint32_t)TASK_WD_PROTECT);
|
||||
#endif
|
||||
|
||||
return m;
|
||||
|
||||
@@ -31,6 +31,8 @@ typedef enum {
|
||||
TASK_WD_UI = 4,
|
||||
TASK_WD_TIMESYNC = 5,
|
||||
TASK_WD_ADC = 6, /* adcTask:1ms 采样节拍消费者(B1,2026-10-10) */
|
||||
TASK_WD_IEC104 = 7, /* iec104Task:104 上行服务(B4,2026-10-12) */
|
||||
TASK_WD_PROTECT = 8, /* protectTask:保护判据节拍(B5,2026-10-12) */
|
||||
TASK_WD_ID_MAX
|
||||
} task_wd_id_t;
|
||||
|
||||
|
||||
@@ -481,7 +481,7 @@ void ch395f_clear_recv_buf(uint8_t sock) {
|
||||
|
||||
/*
|
||||
* 函数功能:设置TCP KeepAlive空闲时间
|
||||
* 入口参数:time - 空闲时间(秒) uint32_t
|
||||
* 入口参数:time - 空闲时间(毫秒,须为 500 的倍数) uint32_t
|
||||
* 限定条件:必须在ch395f_init()之前调用
|
||||
*/
|
||||
void ch395f_set_keepalive_idle(uint32_t time) {
|
||||
@@ -503,7 +503,7 @@ void ch395f_set_keepalive_idle(uint32_t time) {
|
||||
|
||||
/*
|
||||
* 函数功能:设置TCP KeepAlive探测间隔
|
||||
* 入口参数:time - 探测间隔(秒) uint32_t
|
||||
* 入口参数:time - 探测间隔(毫秒,须为 500 的倍数) uint32_t
|
||||
* 限定条件:必须在ch395f_init()之前调用
|
||||
*/
|
||||
void ch395f_set_keepalive_intvl(uint32_t time) {
|
||||
@@ -1174,8 +1174,19 @@ void ch395f_write_send_buf(uint8_t sock, uint8_t *p_data, uint16_t len) {
|
||||
/* 计算总长度:1字节命令 + 1字节Socket + 2字节长度 + 数据 */
|
||||
total = 4U + len;
|
||||
if (total > CH395F_DMA_BUF_SIZE) {
|
||||
/* 异常金丝雀:与读路径对称——静默丢弃会让数据与错误一起消失(无从诊断),
|
||||
* 故必须留痕。正常路径不会到达此处(NET 层按 NET_SEND_CHUNK_MAX 分块)。 */
|
||||
DBG_ERROR("write_send_buf OVERSIZE: sock %u req %u > max %u, write dropped",
|
||||
(unsigned)sock, (unsigned)len, (unsigned)(CH395F_DMA_BUF_SIZE - 4U));
|
||||
return;
|
||||
}
|
||||
if (len > (uint16_t)CH395F_SEND_BUF_SIZE_BYTES) {
|
||||
/* §5.32:单次写入长度不得大于该 Socket 的发送缓冲区(本仓 = 2 块 = 1KB)。
|
||||
* 超出不会被命令拒绝,而是写穿发送缓冲导致数据错乱(Trap 20)→ 只告警不拦截,
|
||||
* 分块责任在调用方(NET 层 NET_SEND_CHUNK_MAX)。 */
|
||||
DBG_ERROR("write_send_buf OVERSIZE-SEND BUF: sock %u len %u > send buf %u (Trap 20)",
|
||||
(unsigned)sock, (unsigned)len, (unsigned)CH395F_SEND_BUF_SIZE_BYTES);
|
||||
}
|
||||
|
||||
/* 打包命令头到DMA发送缓冲区 */
|
||||
s_spi2_dma_tx_buf[0] = CH395F_CMD_WRITE_SEND_BUF_SN;
|
||||
|
||||
@@ -34,11 +34,13 @@ extern "C" {
|
||||
#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_GLOB_INT_STATUS_ALL 0x19U
|
||||
#define CH395F_CMD_GET_UNREACH_IPPT 0x28U
|
||||
#define CH395F_CMD_GET_PHY_STATUS 0x26U
|
||||
#define CH395F_CMD_GET_DHCP_STATUS 0x42U
|
||||
|
||||
/* 无输入数据,多字节输出(§5.16 / §5.14,勿按 1 字节读) */
|
||||
#define CH395F_CMD_GET_GLOB_INT_STATUS_ALL 0x19U /* 2 字节 */
|
||||
#define CH395F_CMD_GET_UNREACH_IPPT 0x28U /* 8 字节 */
|
||||
|
||||
/* 无输入数据,6 字节输出 */
|
||||
#define CH395F_CMD_GET_MAC_ADDR 0x40U
|
||||
|
||||
@@ -53,15 +55,17 @@ extern "C" {
|
||||
|
||||
/* 1 字节输入,1 字节输出 */
|
||||
#define CH395F_CMD_CHECK_EXIST 0x06U
|
||||
|
||||
/* 4 字节输入,无输出数据(§5.52,勿按 1 字节写) */
|
||||
#define CH395F_CMD_SET_FUN_PARA 0x55U
|
||||
|
||||
/* 2 字节输入,无输出数据 */
|
||||
/* 2 字节输入(低字节在前),无输出数据 */
|
||||
#define CH395F_CMD_SET_TTL 0x51U
|
||||
#define CH395F_CMD_SET_ARP 0x44U
|
||||
#define CH395F_CMD_SET_RETRAN_PERIOD 0x2BU
|
||||
|
||||
/* 3 字节输入,无输出数据 */
|
||||
#define CH395F_CMD_SET_BAUDRATE 0x02U
|
||||
#define CH395F_CMD_SET_RETRAN_PERIOD 0x2BU
|
||||
|
||||
/* 4 字节输入,无输出数据 */
|
||||
#define CH395F_CMD_SET_IP_ADDR 0x22U
|
||||
@@ -250,7 +254,9 @@ extern "C" {
|
||||
#define CH395F_GINT_STAT_SOCK5 0x0200U
|
||||
#define CH395F_GINT_STAT_SOCK6 0x0400U
|
||||
#define CH395F_GINT_STAT_SOCK7 0x0800U
|
||||
#define CH395F_GINT_STAT_DHCPV6 0x8000U
|
||||
/* bit12 DHCPv6:官方 SDK 定义为 (1<<12),但 §5.16 明确 CH395F 的 [12:15] 为
|
||||
* 保留位(仅 CH395X 系列产生)——本芯片上恒为 0,勿用作判据 */
|
||||
#define CH395F_GINT_STAT_DHCPV6 0x1000U
|
||||
|
||||
/*
|
||||
* Socket 中断状态位(CMD_GET_INT_STATUS_SN 返回值,来自手册表5-x)
|
||||
@@ -698,7 +704,7 @@ void ch395f_set_keepalive_enable(uint8_t sock, uint8_t enable);
|
||||
|
||||
/*
|
||||
* 函数功能:设置 TCP 重传次数
|
||||
* 入口参数:cnt - 重传次数 uint8_t(0=使用默认值5)
|
||||
* 入口参数:cnt - 重传次数 uint8_t(§5.17:默认 12 次,最大 20,>20 按 20 处理;无 0=默认语义)
|
||||
* 限定条件:必须在 ch395f_init() 之前调用
|
||||
*/
|
||||
void ch395f_set_retrans_count(uint8_t cnt);
|
||||
|
||||
@@ -56,14 +56,13 @@ volatile uint8_t g_spi1_dma_done;
|
||||
* 否则静默失败(CCM 首次扩容实验实证的根因)。页级大块传输(2048)仍走 DMA,
|
||||
* 源/目的全部为文件作用域静态缓冲。 */
|
||||
|
||||
/* DMA 可达内存区间:必须与分散加载文件 RW_IRAM1(0x20000000,128KB,
|
||||
* SRAM1+SRAM2)一致。CCM(0x10000000,64KB)DMA1/DMA2 物理不可达,
|
||||
* RW_CCM 区任何缓冲都不得进 DMA。阈值只能挡住"当前已知的小栈缓冲",
|
||||
* 挡不住"将来新加的 >1024B 栈缓冲";gd5f_dma_buf_ok() 把这条约定
|
||||
* 升级为运行时强制检查(2026-09-21 加固)。若将来新增其它 DMA 可达
|
||||
* 内存(如外部 SDRAM),此处区间必须同步扩展。 */
|
||||
#define GD5F_DMA_RAM_BASE 0x20000000UL
|
||||
#define GD5F_DMA_RAM_END 0x20020000UL
|
||||
/* DMA 可达内存窗口 GD5F_DMA_RAM_BASE/END 与 CCM 窗口常量已上移到 gd5f2gq5ue.h
|
||||
* (N-26,2026-10-12:nand_ftl.c 的 init 期自检也要用),并带编译期守卫
|
||||
* (白名单窗口与 CCM 永不相交、阈值契约)。此处只留语义提醒:
|
||||
* 窗口必须与分散加载 RW_IRAM1(0x20000000,128KB,SRAM1+SRAM2)一致;
|
||||
* CCM(0x10000000,64KB)DMA1/DMA2 物理不可达,RW_CCM 区任何缓冲都不得进 DMA。
|
||||
* 阈值只能挡住"当前已知的小栈缓冲",挡不住"将来新加的 >1024B 栈缓冲";
|
||||
* gd5f_dma_buf_ok() 把这条约定升级为运行时强制检查(2026-09-21 加固)。 */
|
||||
|
||||
/*
|
||||
* 函数功能:SPI1 TX DMA 完成回调(PROGRAM_LOAD 数据发送用)
|
||||
@@ -104,8 +103,7 @@ static int gd5f_private_bbt_find_best(void);
|
||||
static int gd5f_bbt_load(void);
|
||||
static int gd5f_bbt_save(void);
|
||||
|
||||
/* DMA 可达性守卫(见下方实现) */
|
||||
static int gd5f_dma_buf_ok(const void *p_buf, size_t size);
|
||||
/* DMA 可达性守卫:实现见下方(N-26 起为公开 API,声明在 gd5f2gq5ue.h) */
|
||||
|
||||
/* ======================== DMA 可达性守卫 ======================== */
|
||||
|
||||
@@ -120,7 +118,7 @@ static int gd5f_dma_buf_ok(const void *p_buf, size_t size);
|
||||
* 本函数把"栈/堆缓冲禁入 DMA"的约定升级为运行时强制约束:
|
||||
* 起止地址任一越出 SRAM 区间即判不可达。
|
||||
*/
|
||||
static int gd5f_dma_buf_ok(const void *p_buf, size_t size) {
|
||||
int gd5f_dma_buf_ok(const void *p_buf, size_t size) {
|
||||
uint32_t start = (uint32_t)(uintptr_t)p_buf;
|
||||
uint32_t end;
|
||||
|
||||
|
||||
@@ -77,6 +77,35 @@ extern "C" {
|
||||
*/
|
||||
#define GD5F_DMA_THRESHOLD 1024
|
||||
|
||||
/* ==================== DMA 可达内存窗口(N-26,2026-10-12) ====================
|
||||
* DMA1/DMA2 物理不可达 CCM(0x10000000~0x1000FFFF):以 CCM 缓冲做 DMA 源/目的
|
||||
* 会静默不落地(2026-09-20 首轮 CCM 扩容实验实证根因;口径见
|
||||
* docs/内存管理/内存内容分配策略.md §2 与 §4 规则 R2/R3)。
|
||||
* 白名单窗口必须与分散加载 RW_IRAM1(0x20000000,128KB,SRAM1+SRAM2)一致
|
||||
* (MDK-ARM/STM32F407-Demo/STM32F407-Demo.sct);将来新增其它 DMA 可达内存
|
||||
* (如外部 SDRAM)须同步扩展本窗口。
|
||||
* ⚠ 缓冲**自身地址**无法编译期断言:.bss 地址链接期才确定,armcc 的常量表达式
|
||||
* 取不到(__builtin 系在 MicroLIB 下亦不可用,见 Src/main.c Error_Handler 注释)。
|
||||
* 静态缓冲的落位由 scatter(RW_CCM 只列 heap_4.o)+ nand_ftl.c 的 init 期自检
|
||||
* (gd5f_dma_buf_ok)双层保证;动态传入缓冲由每次传输的守卫强制。 */
|
||||
#define GD5F_DMA_RAM_BASE 0x20000000UL
|
||||
#define GD5F_DMA_RAM_END 0x20020000UL
|
||||
#define GD5F_CCM_RAM_BASE 0x10000000UL
|
||||
#define GD5F_CCM_RAM_END 0x10010000UL
|
||||
|
||||
/* 编译期守卫(N-26):AC5 无 _Static_assert(C11 不支持),用项目惯用的
|
||||
* 负数组 typedef(同 App/cfg/cfg_file.c、Drivers/BSP/UI/ui_menu_tab.h)——
|
||||
* 条件不成立则数组尺寸 -1 → 编译错。
|
||||
* ① DMA 白名单窗口与 CCM 窗口**永不得重叠**(有人改上面常量时立即爆炸);
|
||||
* ② 阈值契约(见 GD5F_DMA_THRESHOLD 处注释):≥ 存储路径最大栈缓冲
|
||||
* (dhara meta 132 B = DHARA_META_SIZE 上限)且 < 页大小(页级传输保留 DMA)。 */
|
||||
typedef char gd5f_dma_window_no_ccm_check_t[
|
||||
((GD5F_DMA_RAM_END <= GD5F_CCM_RAM_BASE) || (GD5F_DMA_RAM_BASE >= GD5F_CCM_RAM_END))
|
||||
? 1 : -1];
|
||||
typedef char gd5f_dma_threshold_contract_check_t[
|
||||
((GD5F_DMA_THRESHOLD >= 132U) && (GD5F_DMA_THRESHOLD < GD5F_PAGE_SIZE))
|
||||
? 1 : -1];
|
||||
|
||||
/* 坏块表持久化:保留块池数量(从 NAND 顶部向下挑选出厂好块存储 BBT) */
|
||||
#define GD5F_BBT_POOL_COUNT 4
|
||||
|
||||
@@ -253,6 +282,18 @@ int32_t gd5f_program_exec(uint32_t page_addr);
|
||||
int32_t gd5f_block_erase(uint32_t block_addr);
|
||||
int32_t gd5f_check_ecc(void);
|
||||
|
||||
/*
|
||||
* 函数功能:判定缓冲整段是否位于 DMA 可达内存(GD5F_DMA_RAM_* 白名单窗口)
|
||||
* 入口参数:p_buf - 传输缓冲首地址 const void*
|
||||
* size - 传输字节数 size_t
|
||||
* 返回值:1 - 整段 DMA 可达;0 - 不可达(含 size=0 / 地址回绕 / 越窗)
|
||||
* 限定条件:无
|
||||
* 函数说明:N-26(2026-10-12)由 gd5f2gq5ue.c 私有守卫升级为公开 API——
|
||||
* 驱动内部每次 >阈值 传输前用它(不可达即回落 CPU 轮询),
|
||||
* FTL 层(nand_ftl.c)在 init 期用它自检静态页缓冲的落位。
|
||||
*/
|
||||
int gd5f_dma_buf_ok(const void *p_buf, size_t size);
|
||||
|
||||
/*
|
||||
* 函数功能:NAND 长耗时操作看门狗心跳弱钩子
|
||||
* 入口参数:无
|
||||
|
||||
@@ -228,6 +228,24 @@ DSTATUS disk_initialize(BYTE pdrv) {
|
||||
|
||||
DBG_INFO("Initializing NAND FTL...");
|
||||
|
||||
/* N-26 启动期地址自检(2026-10-12):本文件三个页缓冲是 SPI1 DMA 路径上
|
||||
* 唯一的**静态**缓冲(dhara_map_read/write → dhara_nand_read/prog/copy,
|
||||
* 2048 B > GD5F_DMA_THRESHOLD;s_page_buf 现仅装 dhara meta 132 B 走轮询,
|
||||
* 但它同为页尺寸且交给 dhara 内部使用,一并守住防将来越窗)。
|
||||
* DMA1/DMA2 不可达 CCM——若 scatter 改动/段放置把它们挪进 CCM,DMA 会
|
||||
* **静默不落地**(数据错而传输"成功")。缓冲地址链接期才确定,armcc
|
||||
* 无法编译期断言(见 gd5f2gq5ue.h 的窗口常量注释),故在 init 期自检:
|
||||
* 任一缓冲越出 DMA 白名单窗口 → 上报并拒绝挂载(存储降级、不挂死,
|
||||
* 与 E-31 的"保留卷、上报自检"形态一致)。每次传输的运行期守卫
|
||||
* (gd5f_read_from_cache/gd5f_program_load 内)保留作第二层兜底。 */
|
||||
if ((gd5f_dma_buf_ok(s_cache_buf, FTL_PAGE_SIZE) == 0) ||
|
||||
(gd5f_dma_buf_ok(s_copy_buf, FTL_PAGE_SIZE) == 0) ||
|
||||
(gd5f_dma_buf_ok(s_page_buf, FTL_PAGE_SIZE) == 0)) {
|
||||
DBG_ERROR("FTL page buffer not DMA-reachable (cache=0x%08lX), "
|
||||
"storage refused", (unsigned long)(uintptr_t)s_cache_buf);
|
||||
return STA_NOINIT;
|
||||
}
|
||||
|
||||
s_nand.log2_page_size = 11;
|
||||
s_nand.log2_ppb = 6;
|
||||
/* dhara 可见块数 = 总块数 - 保留块池(BBT 存储区不交给 dhara,避免被擦写) */
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
### 阻塞模型(L3 pending)
|
||||
|
||||
- 一期(RECV)已落地:`net_recv` 等支持带超时的阻塞(`pending` 挂起 + tick 唤醒)
|
||||
- 二期(ACCEPT + SEND)待做 → `docs/待处理内容.md` N-4
|
||||
- 二期(ACCEPT + SEND)待做 → `docs/任务规划/登记簿.md` N-4
|
||||
|
||||
### 周期对账自愈(Trap 22 修复)
|
||||
|
||||
@@ -61,5 +61,5 @@
|
||||
- 上层用法:`docs/网络/BSD_Socket_API_使用指南.md`
|
||||
- 内部调用关系:`docs/网络/BSD_Socket_API_函数调用关系.md`
|
||||
- 架构设计:`docs/网络/Net层阻塞模型架构设计.md`(阻塞等待 / pending 请求模型)
|
||||
- 二期设计基线:`docs/待处理内容.md` §5.1
|
||||
- 二期设计基线:`docs/任务规划/基线与待确认.md` §2.1
|
||||
- 测试:`docs/测试说明/CH395F_Test_Guide.md`、`docs/测试说明/FTP_Test_Guide.md`
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
#include "private/lftpd_io.h"
|
||||
#include "net_socket.h"
|
||||
#include "net_config.h"
|
||||
#include "lftpd_auth.h"
|
||||
#include "ftp_cfg.h"
|
||||
#include "ff.h"
|
||||
|
||||
typedef struct {
|
||||
@@ -62,6 +64,10 @@ static command_t commands[] = {
|
||||
{ NULL, NULL },
|
||||
};
|
||||
|
||||
/* N-19 FTP 鉴权:全局失败计数/锁定状态(跨会话持久——单连接服务器,全局
|
||||
* 计数即可;成功清零、锁定期满自动放行,见 lftpd_auth.c) */
|
||||
static ftp_auth_state_t s_auth_state;
|
||||
|
||||
static int send_response(int socket, int code, bool include_code,
|
||||
bool multiline_start, const char *format, ...)
|
||||
{
|
||||
@@ -100,8 +106,8 @@ static int send_response(int socket, int code, bool include_code,
|
||||
|
||||
static int send_list(int socket, const char *path)
|
||||
{
|
||||
static const char *directory_format = "drw-rw-rw- 1 owner group %13lu Jan 01 1970 %s";
|
||||
static const char *file_format = "-rw-rw-rw- 1 owner group %13lu Jan 01 1970 %s";
|
||||
static const char *directory_format = "drw-rw-rw- 1 owner group %13lu Jan 01 1970 %s\r\n";
|
||||
static const char *file_format = "-rw-rw-rw- 1 owner group %13lu Jan 01 1970 %s\r\n";
|
||||
|
||||
void *dp = lftpd_io_opendir(path);
|
||||
if (dp == NULL) {
|
||||
@@ -112,17 +118,39 @@ static int send_list(int socket, const char *path)
|
||||
uint32_t size;
|
||||
int is_dir;
|
||||
|
||||
/* N30-PROBE(临时实验):整份目录一次性发送(验证"背靠背多小段
|
||||
* 毒化芯片 TX"假设——每行一个 net_send 时 LIST#4 必挂) */
|
||||
static char batch[1536]; /* static: off-stack */
|
||||
int blen = 0;
|
||||
int trunc = 0;
|
||||
|
||||
while (lftpd_io_readdir(dp, name, sizeof(name), &size, &is_dir) == 0) {
|
||||
int n;
|
||||
if (is_dir) {
|
||||
send_multiline_response_line(socket, directory_format,
|
||||
(unsigned long)size, name);
|
||||
n = snprintf(batch + blen, sizeof(batch) - blen, directory_format,
|
||||
(unsigned long)size, name);
|
||||
} else {
|
||||
send_multiline_response_line(socket, file_format,
|
||||
(unsigned long)size, name);
|
||||
n = snprintf(batch + blen, sizeof(batch) - blen, file_format,
|
||||
(unsigned long)size, name);
|
||||
}
|
||||
if (n < 0 || (size_t)n >= sizeof(batch) - blen) {
|
||||
trunc = 1;
|
||||
break;
|
||||
}
|
||||
blen += n;
|
||||
}
|
||||
|
||||
lftpd_io_closedir(dp);
|
||||
|
||||
/* lftpd_inet_write 成功返回字节数、失败返回 -1(非 0=成功语义) */
|
||||
if (blen > 0) {
|
||||
if (lftpd_inet_write(socket, batch, blen) < 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
if (trunc) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -137,13 +165,33 @@ static int send_nlst(int socket, const char *path)
|
||||
uint32_t size;
|
||||
int is_dir;
|
||||
|
||||
/* N30-PROBE(临时实验):同 send_list,一次性批量发送 */
|
||||
static char batch[1536]; /* static: off-stack */
|
||||
int blen = 0;
|
||||
int trunc = 0;
|
||||
|
||||
while (lftpd_io_readdir(dp, name, sizeof(name), &size, &is_dir) == 0) {
|
||||
if (!is_dir) {
|
||||
send_multiline_response_line(socket, name);
|
||||
int n = snprintf(batch + blen, sizeof(batch) - blen, "%s\r\n", name);
|
||||
if (n < 0 || (size_t)n >= sizeof(batch) - blen) {
|
||||
trunc = 1;
|
||||
break;
|
||||
}
|
||||
blen += n;
|
||||
}
|
||||
}
|
||||
|
||||
lftpd_io_closedir(dp);
|
||||
|
||||
/* lftpd_inet_write 成功返回字节数、失败返回 -1(非 0=成功语义) */
|
||||
if (blen > 0) {
|
||||
if (lftpd_inet_write(socket, batch, blen) < 0) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
if (trunc) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -241,6 +289,14 @@ static int cmd_cwd(lftpd_client_t *client, const char *arg)
|
||||
|
||||
static int cmd_dele(lftpd_client_t *client, const char *arg)
|
||||
{
|
||||
/* N-19:写命令门——[ftp] write=0 时拒绝(鉴权门已保证此处必为已登录态) */
|
||||
const ftp_cfg_t *cfg = ftp_cfg_get();
|
||||
|
||||
if ((cfg == NULL) || (cfg->write_en == 0u)) {
|
||||
send_simple_response(client->socket, 550, STATUS_550);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (arg == NULL || strlen(arg) == 0) {
|
||||
send_simple_response(client->socket, 550, STATUS_550);
|
||||
return -1;
|
||||
@@ -374,7 +430,33 @@ static int cmd_noop(lftpd_client_t *client, const char *arg)
|
||||
|
||||
static int cmd_pass(lftpd_client_t *client, const char *arg)
|
||||
{
|
||||
send_simple_response(client->socket, 230, STATUS_230);
|
||||
const ftp_cfg_t *cfg = ftp_cfg_get();
|
||||
ftp_auth_creds_t creds;
|
||||
ftp_auth_result_t res;
|
||||
|
||||
if (cfg == NULL) {
|
||||
/* fail-closed:配置不可用即拒绝(s_cfg 静态常驻,正常不会走到) */
|
||||
send_simple_response(client->socket, 530, STATUS_530);
|
||||
return 0;
|
||||
}
|
||||
creds.p_user = cfg->user;
|
||||
creds.p_pwd_hash = cfg->pwd_hash;
|
||||
creds.write_en = cfg->write_en;
|
||||
res = ftp_auth_check(&creds, client->auth_user_ok,
|
||||
(arg != NULL) ? arg : "",
|
||||
&s_auth_state, (uint32_t)xTaskGetTickCount());
|
||||
if (res == FTP_AUTH_OK) {
|
||||
client->auth_logged_in = 1;
|
||||
send_simple_response(client->socket, 230, STATUS_230);
|
||||
return 0;
|
||||
}
|
||||
/* 预期事件(爆破/配错口令):用 INFO 而非 ERROR——[ERR] 是 @test Forbidden 标记 */
|
||||
if (res == FTP_AUTH_LOCKED) {
|
||||
lftpd_log_info("auth locked after %u failures", (unsigned)FTP_AUTH_FAIL_MAX);
|
||||
} else {
|
||||
lftpd_log_info("auth failed for user '%s'", client->auth_user);
|
||||
}
|
||||
send_simple_response(client->socket, 530, STATUS_530);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -500,6 +582,14 @@ static int cmd_size(lftpd_client_t *client, const char *arg)
|
||||
|
||||
static int cmd_stor(lftpd_client_t *client, const char *arg)
|
||||
{
|
||||
/* N-19:写命令门——[ftp] write=0 时拒绝(鉴权门已保证此处必为已登录态) */
|
||||
const ftp_cfg_t *cfg = ftp_cfg_get();
|
||||
|
||||
if ((cfg == NULL) || (cfg->write_en == 0u)) {
|
||||
send_simple_response(client->socket, 550, STATUS_550);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (client->data_socket == -1) {
|
||||
send_simple_response(client->socket, 425, STATUS_425);
|
||||
return -1;
|
||||
@@ -552,7 +642,20 @@ static int cmd_type(lftpd_client_t *client, const char *arg)
|
||||
|
||||
static int cmd_user(lftpd_client_t *client, const char *arg)
|
||||
{
|
||||
send_simple_response(client->socket, 230, STATUS_230);
|
||||
const ftp_cfg_t *cfg = ftp_cfg_get();
|
||||
|
||||
client->auth_logged_in = 0;
|
||||
client->auth_user[0] = '\0';
|
||||
client->auth_user_ok = 0;
|
||||
if ((arg != NULL) && (cfg != NULL)) {
|
||||
strncpy(client->auth_user, arg, sizeof(client->auth_user) - 1);
|
||||
client->auth_user[sizeof(client->auth_user) - 1] = '\0';
|
||||
client->auth_user_ok = (uint8_t)ftp_auth_user_match(cfg->user,
|
||||
client->auth_user);
|
||||
}
|
||||
/* 不论用户名对错一律 331:不泄露"用户是否存在";错用户名在 cmd_pass
|
||||
* 仍走一次同耗时哈希比较(时间拉平,见 lftpd_auth.h 说明 2) */
|
||||
send_simple_response(client->socket, 331, STATUS_331);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -595,6 +698,21 @@ static int handle_control_channel(lftpd_client_t *client)
|
||||
command_tmp[i] = (char)toupper((int)command_tmp[i]);
|
||||
}
|
||||
|
||||
/* N-19:未鉴权命令门——仅 QUIT/FEAT/SYST/NOOP 放行(客户端兼容需要,
|
||||
* 且无信息量);其余一律 530。USER/PASS 本身也在放行名单外,
|
||||
* 但它们在 gate 之前不可能通过 → 单独放行见下(USER/PASS 例外) */
|
||||
if (client->auth_logged_in == 0) {
|
||||
if ((strcmp(command_tmp, "USER") != 0) &&
|
||||
(strcmp(command_tmp, "PASS") != 0) &&
|
||||
(strcmp(command_tmp, "QUIT") != 0) &&
|
||||
(strcmp(command_tmp, "FEAT") != 0) &&
|
||||
(strcmp(command_tmp, "SYST") != 0) &&
|
||||
(strcmp(command_tmp, "NOOP") != 0)) {
|
||||
send_simple_response(client->socket, 530, STATUS_530);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
bool matched = false;
|
||||
for (int i = 0; commands[i].command; i++) {
|
||||
if (strcmp(commands[i].command, command_tmp) == 0) {
|
||||
@@ -640,6 +758,11 @@ int lftpd_start(const char *directory, int port, lftpd_t *lftpd)
|
||||
lftpd->port = port;
|
||||
lftpd->server_socket = -1; /* memset 清零后显式置"无监听器"(0 是合法 fd) */
|
||||
|
||||
/* N-19:出厂默认口令在用告警(台架锚点;改了 [ftp] pwd_hash 即消失) */
|
||||
if (ftp_cfg_is_default_pwd() != 0) {
|
||||
lftpd_log_info("using factory default password");
|
||||
}
|
||||
|
||||
while (true) {
|
||||
lftpd_task_wd_beat(); /* 看门狗:事件循环每轮喂心跳(ftpTask 主循环节拍点) */
|
||||
if (lftpd->server_socket < 0) {
|
||||
|
||||
@@ -6,6 +6,11 @@ typedef struct {
|
||||
char directory[256];
|
||||
int socket;
|
||||
int data_socket;
|
||||
/* N-19 FTP 鉴权会话态(每会话 memset 0 = 未登录;跨会话锁定计数在
|
||||
* lftpd.c 的 s_auth_state,不在此处) */
|
||||
char auth_user[32]; /* USER 阶段暂存的用户名 */
|
||||
uint8_t auth_user_ok; /* 1 = 用户名与 [ftp] user 匹配 */
|
||||
uint8_t auth_logged_in; /* 1 = PASS 校验通过 */
|
||||
} lftpd_client_t;
|
||||
|
||||
typedef struct {
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* 模块名称:lftpd_auth
|
||||
* 模块功能:FTP 鉴权状态机实现(纯函数,仅依赖 stdint/string 与 sha256)
|
||||
* 适用平台:STM32F407ZGTx / PC 主机侧单测(双端共用)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-11
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-11 王建锋 创建初始版本(N-19 FTP 鉴权;契约由
|
||||
* test/host/ftp_auth 主机单测钉死)
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "lftpd_auth.h"
|
||||
|
||||
#include "sha256.h"
|
||||
|
||||
/* 函数定义区 */
|
||||
|
||||
/*
|
||||
* 函数功能:用户名匹配(大小写不敏感)
|
||||
* 入口参数:p_expect - 配置用户名 p_user - 客户端 USER 参数
|
||||
* 返回值:1 - 匹配;0 - 不匹配或任一为 NULL int
|
||||
* 限定条件:无
|
||||
*/
|
||||
int ftp_auth_user_match(const char *p_expect, const char *p_user) {
|
||||
if ((p_expect == NULL) || (p_user == NULL)) {
|
||||
return 0;
|
||||
}
|
||||
for (size_t i = 0u; ; i++) {
|
||||
char a = p_expect[i];
|
||||
char b = p_user[i];
|
||||
|
||||
if ((a >= 'A') && (a <= 'Z')) {
|
||||
a = (char)(a + 32);
|
||||
}
|
||||
if ((b >= 'A') && (b <= 'Z')) {
|
||||
b = (char)(b + 32);
|
||||
}
|
||||
if (a != b) {
|
||||
return 0;
|
||||
}
|
||||
if (a == '\0') {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:口令校验 + 失败计数 + 限时锁定(鉴权契约核心)
|
||||
* 入口参数:p_creds - 凭据 user_ok - USER 阶段匹配结果 p_pass - 口令
|
||||
* p_state - 会话外共享状态 now_tick - 当前 tick(ms)
|
||||
* 返回值:FTP_AUTH_OK / FAIL / LOCKED(枚举)
|
||||
* 限定条件:锁定判定先于哈希(锁定期内不做哈希);用户名错也走哈希(耗时拉平);
|
||||
* tick 有符号差抗 32 位回绕;NULL 入参 fail-closed 返回 FAIL
|
||||
*/
|
||||
ftp_auth_result_t ftp_auth_check(const ftp_auth_creds_t *p_creds,
|
||||
uint8_t user_ok, const char *p_pass,
|
||||
ftp_auth_state_t *p_state, uint32_t now_tick) {
|
||||
sha256_ctx_t ctx;
|
||||
uint8_t digest[SHA256_DIGEST_SIZE];
|
||||
int ok;
|
||||
|
||||
if ((p_creds == NULL) || (p_state == NULL)) {
|
||||
return FTP_AUTH_FAIL; /* fail-closed */
|
||||
}
|
||||
|
||||
if (p_state->fail_count >= FTP_AUTH_FAIL_MAX) {
|
||||
/* 有符号差:now 在 lock_until 之前(含回绕)= 仍在锁定窗内 */
|
||||
if ((int32_t)(now_tick - p_state->lock_until_tick) < 0) {
|
||||
return FTP_AUTH_LOCKED;
|
||||
}
|
||||
p_state->fail_count = 0u; /* 锁定期满,重新起算 */
|
||||
}
|
||||
|
||||
/* 盐 = 期望用户名;用户名错也走同一哈希路径(耗时拉平,见头文件说明 2) */
|
||||
sha256_init(&ctx);
|
||||
if (p_creds->p_user != NULL) {
|
||||
sha256_update(&ctx, p_creds->p_user, strlen(p_creds->p_user));
|
||||
}
|
||||
if (p_pass != NULL) {
|
||||
sha256_update(&ctx, p_pass, strlen(p_pass));
|
||||
}
|
||||
sha256_final(&ctx, digest);
|
||||
|
||||
ok = ((user_ok != 0u) &&
|
||||
(sha256_hex_eq(digest, p_creds->p_pwd_hash) != 0)) ? 1 : 0;
|
||||
if (ok != 0) {
|
||||
p_state->fail_count = 0u;
|
||||
return FTP_AUTH_OK;
|
||||
}
|
||||
|
||||
p_state->fail_count++;
|
||||
if (p_state->fail_count >= FTP_AUTH_FAIL_MAX) {
|
||||
p_state->lock_until_tick = now_tick + FTP_AUTH_LOCK_TICKS;
|
||||
}
|
||||
return FTP_AUTH_FAIL;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* 模块名称:lftpd_auth
|
||||
* 模块功能:FTP 鉴权状态机(纯函数、零平台依赖,主机侧可测)——用户名匹配、
|
||||
* SHA-256(user‖password) 恒定时间校验、连续失败计数与限时锁定;
|
||||
* tick 由调用方注入(MCU = xTaskGetTickCount,单测 = 合成值)
|
||||
* 适用平台:STM32F407ZGTx / PC 主机侧单测(双端共用)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-11
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-11 王建锋 创建初始版本(N-19 FTP 鉴权;lftpd.c 为唯一消费者)
|
||||
*/
|
||||
|
||||
#ifndef LFTPD_AUTH_H
|
||||
#define LFTPD_AUTH_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define FTP_AUTH_FAIL_MAX 5u /* 连续失败 N 次后锁定 */
|
||||
#define FTP_AUTH_LOCK_TICKS 60000u /* 锁定时长(tick = 1 ms → 60 s) */
|
||||
|
||||
/* 一次校验所需的配置快照(由消费者从 ftp_cfg_t 组装,本模块不认配置引擎) */
|
||||
typedef struct {
|
||||
const char *p_user; /* 期望用户名 */
|
||||
const char *p_pwd_hash; /* 64 字符 hex 摘要 */
|
||||
uint8_t write_en; /* 透传位:写命令门由 lftpd 侧执行,本模块不判 */
|
||||
} ftp_auth_creds_t;
|
||||
|
||||
/* 鉴权状态(跨会话持久;单连接服务器,全局一份即可) */
|
||||
typedef struct {
|
||||
uint8_t fail_count; /* 连续失败计数(成功即清零) */
|
||||
uint32_t lock_until_tick; /* 锁定截止 tick(fail_count ≥ FAIL_MAX 时有效) */
|
||||
} ftp_auth_state_t;
|
||||
|
||||
typedef enum {
|
||||
FTP_AUTH_OK = 0, /* 口令正确 */
|
||||
FTP_AUTH_FAIL, /* 口令/用户名错误(已计入失败计数) */
|
||||
FTP_AUTH_LOCKED /* 锁定中(本次不校验、不计数) */
|
||||
} ftp_auth_result_t;
|
||||
|
||||
/*
|
||||
* 函数功能:用户名匹配(大小写不敏感,与配置引擎键名口径一致)
|
||||
* 入口参数:p_expect - 期望用户名 p_user - 来客输入
|
||||
* 返回值:1 - 匹配;0 - 不匹配或任一为 NULL int
|
||||
* 限定条件:无
|
||||
*/
|
||||
int ftp_auth_user_match(const char *p_expect, const char *p_user);
|
||||
|
||||
/*
|
||||
* 函数功能:口令校验 + 失败计数 + 限时锁定(一次 PASS 的完整判定)
|
||||
* 入口参数:p_creds - 配置快照 user_ok - USER 阶段的用户名匹配结果
|
||||
* p_pass - 来客口令(NULL 按空串) p_state - 鉴权状态
|
||||
* now_tick - 当前 tick(ms)
|
||||
* 返回值:ftp_auth_result_t
|
||||
* 限定条件:p_creds/p_state 非空(NULL 一律 FAIL,fail-closed)
|
||||
* 函数说明:1. 锁定中直接 LOCKED(不算哈希——锁定状态非机密,无侧信道);
|
||||
* tick 差用 (int32_t) 有符号比较,天然抗 32 位回绕
|
||||
* 2. 无论用户名对错都走同一条哈希路径(盐 = 期望用户名),
|
||||
* 耗时一致 → 不泄露"用户是否存在"
|
||||
* 3. 成功清零计数;失败累计到 FTP_AUTH_FAIL_MAX 上锁
|
||||
*/
|
||||
ftp_auth_result_t ftp_auth_check(const ftp_auth_creds_t *p_creds,
|
||||
uint8_t user_ok, const char *p_pass,
|
||||
ftp_auth_state_t *p_state, uint32_t now_tick);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* LFTPD_AUTH_H */
|
||||
@@ -1,27 +0,0 @@
|
||||
#include "private/lftpd_log.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdarg.h>
|
||||
#include "dbg_log.h"
|
||||
|
||||
|
||||
void lftpd_log_internal(const char* level, const char* format, ...) {
|
||||
static char buffer[256]; /* static: called at depth in ftpTask, keep off-stack */
|
||||
static char line[288]; /* static: 组装含前缀的整行,交给控制台单写者 */
|
||||
va_list args;
|
||||
va_start(args, format);
|
||||
int err = vsnprintf(buffer, sizeof(buffer), format, args);
|
||||
va_end(args);
|
||||
if (err >= sizeof(buffer)) {
|
||||
return;
|
||||
}
|
||||
/* 走控制台单写者(App/log/console_log.c):整行一次入缓冲,避免逐字符 fputc 交错 */
|
||||
int n = snprintf(line, sizeof(line), "%s %s\r\n", level, buffer);
|
||||
if (n > 0) {
|
||||
if (n > (int)sizeof(line) - 1) {
|
||||
n = (int)sizeof(line) - 1;
|
||||
}
|
||||
(void)dbg_console_write_line(line, (unsigned)n);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,554 @@
|
||||
/*
|
||||
* 模块名称:Network Listener Lifecycle
|
||||
* 模块功能:监听器生命周期(真机专属)—— 维持 fd 处于 LISTEN 态的全部机制:
|
||||
* 重建(open+listen)、重建前的沉降/隔离(Trap 23)、排空 RX 残渣、
|
||||
* D10 非阻塞重试、周期性自愈 pump(唯一的周期性重建决策者)。
|
||||
* 适用平台:STM32F407ZGTx + CH395F 以太网芯片
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-12
|
||||
* 修改记录:
|
||||
* 2026-10-12 王建锋 从 net_socket.c 整体抽出监听生命周期(N-32 §10 模块化)
|
||||
*
|
||||
* 职责与边界见 net_listener.h 头注释;设计依据见
|
||||
* docs/子系统设计/网络Socket监听生命周期设计说明.md §10。
|
||||
*
|
||||
* 模块内分层:
|
||||
* ① 策略常量与私有状态(s_relisten_*)
|
||||
* ② 机制:net_drain_chip_rx(排空)/ net_relisten_locked(重建一次)
|
||||
* ③ 对外接口:settle / try / clear / retry_tick / pump
|
||||
*
|
||||
* 依赖(全部为单向,无反向 include):
|
||||
* net_types.h(net_sock_t)/ net_socket.h(net_get_sock / NET_MAX_SOCKETS /
|
||||
* net_set_errno / NET_ERR_* / net_pending_active)/ ch395f.h(驱动)/
|
||||
* dbg_log.h / main.h(HAL_GetTick)
|
||||
*/
|
||||
#include <string.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "FreeRTOS.h"
|
||||
#include "task.h"
|
||||
#include "cmsis_os.h"
|
||||
|
||||
#include "net_socket.h" /* net_sock_t / net_get_sock / NET_MAX_SOCKETS / net_set_errno */
|
||||
#include "net_listener.h"
|
||||
#include "ch395f.h"
|
||||
#include "test_config.h" /* TEST_LATENCY_PROBE(链路时延探针编译开关) */
|
||||
#include "net_latency_test.h" /* L3 探针接口(relisten 恢复计时采样) */
|
||||
#include "main.h"
|
||||
|
||||
#include "dbg_log.h"
|
||||
|
||||
/* ===== ① 策略常量与私有状态 ===== */
|
||||
|
||||
/* Trap 23(2026-09-23 生产路径实测):异常释放(芯片 TIMEOUT,例如客户端中止上传)
|
||||
* 后的**隔离期**。实测时序:中止时对端仍有在途数据 → 芯片 TIMEOUT(此时 MCU 侧 recv=0)
|
||||
* 在途段随后落进芯片 RX 缓冲(约 2KB 残渣)
|
||||
* 若立即 open+listen,残渣被下一条连接当作自己的数据读走
|
||||
* 新上传只收 2KB 就被判定结束、回报成功(静默截断)
|
||||
* 处置:异常释放的 socket 先隔离 NET_RELISTEN_QUARANTINE_MS:期间持续排空 RX、主动 close 并排空中断
|
||||
* 等芯片回到 CLOSED/TCP_CLOSED;隔离期满才 open+listen
|
||||
* 上限 NET_RELISTEN_SETTLE_MS 后强制放行,保证监听器不会永久失效
|
||||
* 正常 FIN 释放(handle_disconnect_event)不走此路径——既有行为零改动
|
||||
*/
|
||||
#define NET_RELISTEN_QUARANTINE_MS 400U /* 异常释放最短隔离(等在途数据落尽 + 排空残渣) */
|
||||
#define NET_RELISTEN_GRACEFUL_MS 100U /* 正常释放最短沉降(等芯片完成上一条连接的收尾) */
|
||||
#define NET_RELISTEN_SETTLE_MS 800U /* 隔离上限(须 < NET_RELISTEN_DEADLINE_MS,
|
||||
* 否则隔离耗尽重试预算 → 监听被放弃,2026-09-23 实测) */
|
||||
/* 重监听前"清芯片收尾状态"的两个等级(s_relisten_quarantine 取值,取值语义见 net_listener.h)
|
||||
* 2026-09-24 台架实证:正常释放走的是既有"不复位芯片收尾状态"的路径
|
||||
* 残留的 DISCONNECT 会在复用同一 fd 的下一条连接上被投递(accept 后约 10~20ms)
|
||||
* 下一条上传被静默截断到 2048B(一个芯片 RX 缓冲)且回报 226
|
||||
* 只清理不等待(0ms)实测无效(4/4 仍截断):芯片需要时间完成上一条连接的内部收尾
|
||||
* 给足沉降即 4/4 通过
|
||||
*/
|
||||
#define NET_RELISTEN_SETTLE_QUARANTINE 1U
|
||||
#define NET_RELISTEN_SETTLE_GRACEFUL 2U
|
||||
|
||||
static uint8_t s_relisten_quarantine[NET_MAX_SOCKETS]; /* 0=免清;1=异常释放;2=正常释放 */
|
||||
static uint32_t s_relisten_settle_start[NET_MAX_SOCKETS]; /* 隔离起点(0 = 未开始) */
|
||||
static uint32_t s_relisten_settle_dead[NET_MAX_SOCKETS]; /* 隔离上限到期时刻 */
|
||||
|
||||
/* ===== Trap 09 非阻塞重监听重试(D10 修复,2026-09-22 M3 实测) =====
|
||||
* 旧实现:open 失败 → close → osDelay(500) → 重试,netTask 忙等 500ms(全网冻结)
|
||||
* 且实测恢复 588ms、期间客户端重连被拒
|
||||
* 新实现:首次失败即登记待重试,由 netTask 每轮 net_process_messages() 到点重试
|
||||
* 恢复时间 ≈ NET_RELISTEN_RETRY_MS 量级,任何时刻都不阻塞任务
|
||||
*/
|
||||
#define NET_RELISTEN_RETRY_MS 30U /* 重试间隔(Trap 09 沉降窗口;实测 100ms 首次必成,
|
||||
* 收紧到 30ms 以缩短恢复时间——未成则下一轮到点再来) */
|
||||
#define NET_RELISTEN_DEADLINE_MS 2000U /* 放弃时限(自首次尝试起) */
|
||||
|
||||
static uint8_t s_relisten_wait[NET_MAX_SOCKETS]; /* 1 = 有待重试的重监听 */
|
||||
static uint32_t s_relisten_next[NET_MAX_SOCKETS]; /* 下次重试时刻(tick) */
|
||||
static uint32_t s_relisten_dead[NET_MAX_SOCKETS]; /* 放弃时刻(tick) */
|
||||
#if defined(TEST_LATENCY_PROBE)
|
||||
static lat_tmark_t s_relisten_mark[NET_MAX_SOCKETS]; /* 恢复计时起点(首次尝试) */
|
||||
#endif
|
||||
|
||||
/* ===== ② 机制 ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:清空芯片该 socket 的 RX 残留(Trap 23)
|
||||
* 入口参数:sockfd - Socket 描述符 (0~7) int
|
||||
* 出口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:netTask 上下文;socket 即将 close/open(数据已无意义,丢弃)
|
||||
* 函数说明:1. 中止/异常断开时芯片 RX 缓冲可能残留上一条连接的数据(实测残留约一个缓冲 = 2048B)
|
||||
* 2. 残渣带入下一条连接会使其在约 2KB 处被芯片拆链(上传被静默截断)
|
||||
* 3. 关闭前主动读空即清除;分块读避免新增静态缓冲
|
||||
* (2026-10-09 实测:ARMCC 把本 scratch 与同函数内生命周期不重叠的
|
||||
* 192B 日志缓冲叠放,局部缓冲边际栈成本仅 ~16B,静态化反亏 256B BSS,勿再"优化")
|
||||
*/
|
||||
static void net_drain_chip_rx(int sockfd) {
|
||||
/* 复用分块缓冲(256B/块,避免新增静态缓冲) */
|
||||
uint8_t scratch[256];
|
||||
|
||||
/* 上限 16×256 = 4KB(缓冲仅 2KB),防死循环 */
|
||||
for (int guard = 0; guard < 16; guard++) {
|
||||
/* 查询芯片 RX 可用字节数 */
|
||||
int n = (int)ch395f_get_recv_len((uint8_t)sockfd);
|
||||
/* 已排空 → 结束 */
|
||||
if (n <= 0) {
|
||||
break;
|
||||
}
|
||||
/* 单块以缓冲容量为上限 */
|
||||
if (n > (int)sizeof(scratch)) {
|
||||
n = (int)sizeof(scratch);
|
||||
}
|
||||
/* 丢弃该块(读即清) */
|
||||
ch395f_read_recv_buf((uint8_t)sockfd, scratch, (uint16_t)n);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:单连接模式断连后自动重新进入 LISTEN(不清空控制块,保留 fd)
|
||||
* 入口参数:sockfd - 已断开(CLOSED)的 TCP 监听型 Socket 描述符 (0~7) int
|
||||
* 出口参数:无
|
||||
* 返回值:0 - 成功,-1 - 失败
|
||||
* 限定条件:netTask 上下文;控制块 in_use == 1;保留 local_port/auto_relisten
|
||||
* 函数说明:1. 复用原本地端口与协议,重新 OPEN + TCP_LISTEN
|
||||
* 2. 仅将 state 置回 LISTENING,不 memset(fd 与 auto_relisten 保持不变)
|
||||
* 3. 实现 BSD 风格"监听 Socket 常驻",应用层无需手动重监听
|
||||
* 4. **单次尝试**:失败立即返回(不 osDelay 忙等)
|
||||
* 5. Trap 09 沉降交给 net_listener_try / net_listener_retry_tick 非阻塞重试(见下)
|
||||
* 6. 旧实现在此 osDelay(500) 忙等,netTask 冻结 500ms、实测恢复 588ms(M3 台架 D10)
|
||||
* 7. N-32 §10:fd 由参数传入(不再用 `p_sock - s_net_socks` 反推),
|
||||
* 控制块经 net_get_sock() 取 —— 本模块**不访问** net_socket.c 的 static 数组
|
||||
*/
|
||||
static int net_relisten_locked(int sockfd) {
|
||||
/* 芯片命令返回状态 */
|
||||
uint8_t status;
|
||||
/* Socket 控制块(fd 由调用方保证有效) */
|
||||
net_sock_t *p_sock = net_get_sock(sockfd);
|
||||
|
||||
/* 控制块无效:拒绝(不应发生,调用方已判过) */
|
||||
if (p_sock == NULL) {
|
||||
return NET_ERR_RETURN_1(NET_ERR_BADF);
|
||||
}
|
||||
|
||||
/* 释放隔离期(Trap 23):重监听前先清芯片收尾状态 */
|
||||
if (s_relisten_quarantine[sockfd] != 0U) {
|
||||
/* 芯片状态两字节:[0]=Socket,[1]=TCP */
|
||||
uint8_t st[2] = {0, 0};
|
||||
/* 当前 tick */
|
||||
uint32_t now = HAL_GetTick();
|
||||
/* 隔离等级(1=异常释放 2=正常释放) */
|
||||
uint8_t lvl = s_relisten_quarantine[sockfd];
|
||||
/* 最短等待由释放来源决定(NET_RELISTEN_SETTLE_*):异常释放 400ms(等在途数据落尽)
|
||||
* 正常释放 100ms(等芯片内部收尾) */
|
||||
uint32_t hold = (lvl == NET_RELISTEN_SETTLE_QUARANTINE) ? NET_RELISTEN_QUARANTINE_MS
|
||||
: NET_RELISTEN_GRACEFUL_MS;
|
||||
/* 日志标签(台架分辨触发路径) */
|
||||
const char *tag = (lvl == NET_RELISTEN_SETTLE_QUARANTINE) ? "quarantine" : "settle";
|
||||
|
||||
/* 首次进入隔离期:记录起点与上限 */
|
||||
if (s_relisten_settle_start[sockfd] == 0U) {
|
||||
/* 隔离起点 = 本 tick */
|
||||
s_relisten_settle_start[sockfd] = now;
|
||||
/* 隔离上限 = 起点 + 800ms */
|
||||
s_relisten_settle_dead[sockfd] = now + NET_RELISTEN_SETTLE_MS;
|
||||
}
|
||||
/* 隔离期内持续排空 RX 残渣 */
|
||||
net_drain_chip_rx(sockfd);
|
||||
/* 主动关:清旧连接收尾状态 */
|
||||
ch395f_close_socket((uint8_t)sockfd);
|
||||
/* 读即清:本 Socket 中断状态寄存器 */
|
||||
(void)ch395f_get_sock_int_status((uint8_t)sockfd);
|
||||
/* 读即清:全局中断状态寄存器 */
|
||||
(void)ch395f_get_glob_int_status_all();
|
||||
/* 读芯片当前状态(两字节) */
|
||||
ch395f_get_socket_status((uint8_t)sockfd, st);
|
||||
|
||||
/* 芯片尚未回到 CLOSED/TCP_CLOSED,或未满最短等待 */
|
||||
if (((st[0] != CH395F_SOCKET_CLOSED) || (st[1] != CH395F_TCP_CLOSED) ||
|
||||
((uint32_t)(now - s_relisten_settle_start[sockfd]) < hold))) {
|
||||
/* 未到上限:交重试轮询(30ms 后复检) */
|
||||
if ((int32_t)(now - s_relisten_settle_dead[sockfd]) < 0) {
|
||||
DBG_INFO("sock%d relisten %s (sock=0x%02X tcp=0x%02X)",
|
||||
sockfd, tag, st[0], st[1]);
|
||||
/* 置 BUSY(结果回传调用方) */
|
||||
(void)net_set_errno(NET_ERR_BUSY);
|
||||
/* 交重试轮询(30ms 后复检) */
|
||||
return -1;
|
||||
}
|
||||
/* 隔离上限已至:强制放行(防监听器永久失效) */
|
||||
DBG_INFO("sock%d relisten %s timeout, force open (sock=0x%02X tcp=0x%02X)",
|
||||
sockfd, tag, st[0], st[1]);
|
||||
}
|
||||
/* 隔离完成(或强制放行) */
|
||||
s_relisten_quarantine[sockfd] = 0U;
|
||||
/* 清隔离起点(回到"未开始") */
|
||||
s_relisten_settle_start[sockfd] = 0U;
|
||||
/* 清上限时刻 */
|
||||
s_relisten_settle_dead[sockfd] = 0U;
|
||||
}
|
||||
|
||||
/* 重置协议类型为 TCP */
|
||||
ch395f_set_proto_type((uint8_t)sockfd, CH395F_PROTO_TYPE_TCP);
|
||||
/* 本地端口(沿用原监听) */
|
||||
ch395f_set_sour_port((uint8_t)sockfd, p_sock->local_port);
|
||||
|
||||
/* 打开芯片侧 Socket */
|
||||
status = ch395f_open_socket((uint8_t)sockfd);
|
||||
/* open 失败 → BUSY(Trap 09:芯片需沉降时间才能重开) */
|
||||
if (status != CH395F_ERR_SUCCESS) {
|
||||
/* Trap 09:断开后芯片需沉降时间才能重新 open。此处只清理并返回
|
||||
* (严禁 osDelay 忙等)→ 由重试轮询到点再来。 */
|
||||
/* 先关闭,交重试轮询(30ms 后复检) */
|
||||
ch395f_close_socket((uint8_t)sockfd);
|
||||
/* 置 BUSY(结果回传调用方) */
|
||||
(void)net_set_errno(NET_ERR_BUSY);
|
||||
/* 交重试轮询(30ms 后复检) */
|
||||
return -1;
|
||||
}
|
||||
/* 下发 TCP LISTEN 命令 */
|
||||
status = ch395f_tcp_listen((uint8_t)sockfd);
|
||||
/* LISTEN 失败 → BUSY(同样交重试) */
|
||||
if (status != CH395F_ERR_SUCCESS) {
|
||||
/* 置 BUSY(结果回传调用方) */
|
||||
return NET_ERR_RETURN_1(NET_ERR_BUSY);
|
||||
}
|
||||
|
||||
/* 状态置回 LISTENING(fd 保留) */
|
||||
p_sock->state = NET_SOCK_STATE_LISTENING;
|
||||
/* N-32(P1,设计文档 §5 P1.1 的关键约定):此处**刻意不清 session_dead**。
|
||||
* 重建监听只是"芯片侧恢复就位",与"应用是否已确认上一条会话结束"是两件事;
|
||||
* 清了它,抢先 relisten 之后消费者就再也得不到 EOF(B10"消费者卡死"原型)。
|
||||
* session_dead 只由新 CONNECT 成功(handle_connect_event)或 memset
|
||||
* (net_close/net_socket)清零。 */
|
||||
/* 新监听不得继承上一条连接的释放原因(Trap 23 待办 #3) */
|
||||
p_sock->released_abnormal = 0U;
|
||||
/* N-30(Trap 25):清零前把对端端口转登为幽灵甄别基准 */
|
||||
p_sock->last_peer_port = p_sock->remote_port;
|
||||
/* N-30:新监听会话,丢中断去抖计数清零 */
|
||||
p_sock->lost_conn_seen = 0U;
|
||||
/* 清对端端口(新连接待用) */
|
||||
p_sock->remote_port = 0;
|
||||
/* 清对端 IP */
|
||||
p_sock->remote_ip = 0;
|
||||
/* 清对端 IP 数组 */
|
||||
memset(p_sock->remote_ip_arr, 0, sizeof(p_sock->remote_ip_arr));
|
||||
/* 新开 Socket 发送缓冲按占用计(由 SEND_OK 清除) */
|
||||
p_sock->send_ready = 0;
|
||||
/* 清接收字节统计(新连接) */
|
||||
p_sock->recv_bytes = 0;
|
||||
/* auto_relisten 保持为 1,下次断连仍自动重监听 */
|
||||
|
||||
/* 读即清:仅本 Socket 的中断状态(防上一条会话残留误触发)。
|
||||
* 原为"全 8 个 Socket + 全局"读清,与 net_listen_locked 同源(B3,机制
|
||||
* 与 git 考古见该处注释):本处是每次断连 auto-relisten 的热路径,全量清
|
||||
* 会持续吞掉其它在用 Socket 的 CONNECT/DISCONNECT 等边沿事件。全局中断
|
||||
* 寄存器交 net_poll 集中读清(AGENTS 模块约定)。 */
|
||||
(void)ch395f_get_sock_int_status((uint8_t)sockfd);
|
||||
|
||||
/* 留痕:监听器重建完成 */
|
||||
DBG_INFO("sock%d auto-relisten ok", sockfd);
|
||||
/* 重监听成功 */
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ===== ③ 对外接口 ===== */
|
||||
|
||||
/*
|
||||
* 函数功能:登记「本 fd 的会话已结束,重建监听前需沉降/隔离」(契约见 net_listener.h)
|
||||
* 入口参数:sockfd - Socket 描述符 (0~7) int
|
||||
* level - 沉降等级(net_listener_settle_t,由释放来源决定)
|
||||
* 出口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:netTask 上下文;由 handle_disconnect_event / handle_timeout_event 调用
|
||||
* 函数说明:1. 只置事实,不做动作 —— 重建由 net_listener_pump / net_listener_try 负责
|
||||
* 2. 越界 sockfd 静默忽略(与旧直接写 s_relisten_quarantine 行为一致)
|
||||
*/
|
||||
void net_listener_settle(int sockfd, net_listener_settle_t level) {
|
||||
/* 描述符越界:静默忽略(与旧直接写数组行为一致,不设错误码) */
|
||||
if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) {
|
||||
return;
|
||||
}
|
||||
/* 登记沉降/隔离等级 —— 只置事实,重建由 pump / try 负责 */
|
||||
s_relisten_quarantine[sockfd] = (uint8_t)level;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:清除本 fd 的待重监听重试状态(关闭/新建 fd 时调用)
|
||||
* 入口参数:sockfd - Socket 描述符 (0~7)
|
||||
* 出口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:无
|
||||
* 函数说明:1. 清"待重试"标志与重试时刻,fd 关闭/新建时调用(防残留重试)
|
||||
*/
|
||||
void net_listener_clear(int sockfd) {
|
||||
/* 描述符越界:直接返回(本函数不设错误码) */
|
||||
if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) {
|
||||
return;
|
||||
}
|
||||
/* 清"待重试"标志 */
|
||||
s_relisten_wait[sockfd] = 0U;
|
||||
/* 清下次重试时刻 */
|
||||
s_relisten_next[sockfd] = 0U;
|
||||
/* 清放弃时刻 */
|
||||
s_relisten_dead[sockfd] = 0U;
|
||||
/* Trap 23:一并清沉降/隔离状态 */
|
||||
s_relisten_settle_dead[sockfd] = 0U;
|
||||
/* 清沉降起点 */
|
||||
s_relisten_settle_start[sockfd] = 0U;
|
||||
/* 一并清隔离标志:关闭/重建即确定性释放,
|
||||
* 防同 fd 下一生成继承上轮的 400/800ms
|
||||
* 隔离等待(2026-09-24 台架发现跨轮残留) */
|
||||
s_relisten_quarantine[sockfd] = 0U;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:标记重监听完成(清重试状态;探针构建下报告恢复计时样本)
|
||||
* 入口参数:sockfd - Socket 描述符 (0~7)
|
||||
* 出口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:netTask 上下文
|
||||
* 函数说明:1. 重监听成功收尾:清重试状态;探针构建(TEST_LATENCY_PROBE)下上报恢复计时样本
|
||||
*/
|
||||
static void net_relisten_done(int sockfd) {
|
||||
/* 清重试状态(复用清除函数) */
|
||||
net_listener_clear(sockfd);
|
||||
#if defined(TEST_LATENCY_PROBE)
|
||||
/* 上报恢复计时样本(首次尝试 → 成功) */
|
||||
lat_probe_add_seg("relisten", lat_mark_us(&s_relisten_mark[sockfd]));
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:尝试重建监听一次(**不阻塞**);失败则登记"待重试"(契约见 net_listener.h)
|
||||
* 入口参数:sockfd - Socket 描述符 (0~7) int
|
||||
* 出口参数:无
|
||||
* 返回值:0 - 成功(监听器已就绪);-1 - 本次失败或已在重试队列中 int
|
||||
* 限定条件:netTask 上下文;fd 为监听型(auto_relisten)
|
||||
* 函数说明:1. 已在重试队列时直接返回 -1(不重复尝试、不重置重试时刻)——
|
||||
* 否则每 10ms 一次的 accept 轮询会不断把重试时刻往后推,
|
||||
* 导致永不重试(重试饥饿)
|
||||
* 2. N-32 §10:替代此前散落在 net_socket.c 各处的 net_relisten_try()
|
||||
*/
|
||||
int net_listener_try(int sockfd) {
|
||||
/* 当前 tick */
|
||||
uint32_t now = HAL_GetTick();
|
||||
|
||||
/* 描述符越界 → BADF */
|
||||
if ((sockfd < 0) || (sockfd >= NET_MAX_SOCKETS)) {
|
||||
return NET_ERR_RETURN_1(NET_ERR_BADF);
|
||||
}
|
||||
/* 已在重试队列:不重复尝试、不重置时刻(防重试饥饿) */
|
||||
if (s_relisten_wait[sockfd] != 0U) {
|
||||
(void)net_set_errno(NET_ERR_BUSY);
|
||||
/* 已在重试队列:由 tick 负责 */
|
||||
return -1;
|
||||
}
|
||||
#if defined(TEST_LATENCY_PROBE)
|
||||
/* 恢复计时自首次尝试起 */
|
||||
lat_mark(&s_relisten_mark[sockfd]);
|
||||
#endif
|
||||
/* 本次尝试成功 */
|
||||
if (net_relisten_locked(sockfd) == 0) {
|
||||
/* 标记完成(清状态 + 探针样本) */
|
||||
net_relisten_done(sockfd);
|
||||
/* 监听器已就绪 */
|
||||
return 0;
|
||||
}
|
||||
/* 登记"待重试" */
|
||||
s_relisten_wait[sockfd] = 1U;
|
||||
/* 下次重试时刻 = now + 30ms */
|
||||
s_relisten_next[sockfd] = now + NET_RELISTEN_RETRY_MS;
|
||||
/* 放弃时刻 = now + 2s */
|
||||
s_relisten_dead[sockfd] = now + NET_RELISTEN_DEADLINE_MS;
|
||||
/* 本次失败(重试交 tick) */
|
||||
return -1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:D10 重试轮询(netTask 每轮调用一次,非阻塞)
|
||||
* 入口参数:无
|
||||
* 出口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:netTask 上下文(由 net_process_messages 每轮调用)
|
||||
* 函数说明:1. 到点重试待办的重监听;成功即报告探针样本
|
||||
* 2. 本 fd 有在途挂起 recv 时跳过(net_pending_active,L3 保险:
|
||||
* 重开 Socket 会摧毁在途连接)
|
||||
* 3. 超过 NET_RELISTEN_DEADLINE_MS 放弃并记错误日志(防监听器永久失效)
|
||||
*/
|
||||
void net_listener_retry_tick(void) {
|
||||
/* 当前 tick */
|
||||
uint32_t now = HAL_GetTick();
|
||||
/* Socket 循环索引 */
|
||||
int i;
|
||||
|
||||
for (i = 0; i < NET_MAX_SOCKETS; i++) {
|
||||
/* 本 fd 无待重试重监听:跳过 */
|
||||
if (s_relisten_wait[i] == 0U) {
|
||||
continue;
|
||||
}
|
||||
/* 本 fd 有在途挂起 recv:重开 Socket 会摧毁在途连接(L3 保险) */
|
||||
if (net_pending_active(i) != 0U) {
|
||||
continue;
|
||||
}
|
||||
/* 未到点(tick 差值按有符号比较,抗回绕) */
|
||||
if ((int32_t)(now - s_relisten_next[i]) < 0) {
|
||||
continue;
|
||||
}
|
||||
/* 已到放弃时限 */
|
||||
if ((int32_t)(now - s_relisten_dead[i]) >= 0) {
|
||||
/* 清重试状态 */
|
||||
net_listener_clear(i);
|
||||
/* 放弃并记错误日志 */
|
||||
DBG_ERROR("sock%d relisten give up (Trap 09 not cleared)", i);
|
||||
continue;
|
||||
}
|
||||
/* 重试成功 */
|
||||
if (net_relisten_locked(i) == 0) {
|
||||
/* 标记完成(清状态 + 探针样本) */
|
||||
net_relisten_done(i);
|
||||
/* 留痕:重试成功 */
|
||||
DBG_INFO("sock%d relisten retry ok", i);
|
||||
} else {
|
||||
/* 仍未成:置下次重试时刻 */
|
||||
s_relisten_next[i] = now + NET_RELISTEN_RETRY_MS;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 监听器 pump 周期(对账 200ms 的 1/2:比它快、又不至于像 50ms 那样
|
||||
* 频繁 close/open 芯片 Socket 与消费者收发撞车——2026-10-12 台架实测
|
||||
* 50ms 会把 uiTask 最坏单轮从 172ms 推到 1034ms) */
|
||||
#define NET_LISTENER_PUMP_PERIOD_MS 100U
|
||||
/* pump 去抖与冷却(沿用原对账③④ 的实测结论,取值不变):
|
||||
* 2026-09-23 实证——无去抖时监听被反复重建直至放弃 */
|
||||
#define NET_LISTENER_STABLE_N 2U /* 连续失配次数(去抖,≈200ms) */
|
||||
#define NET_LISTENER_COOLDOWN_MS 1000U /* 同一 socket 两次动手的最小间隔(防抖成风暴) */
|
||||
|
||||
/*
|
||||
* 函数功能:监听器 pump(N-32 P2「单一所有者」)—— 本层**唯一**的周期性
|
||||
* 重建监听决策者
|
||||
* 入口参数:无
|
||||
* 出口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:netTask 上下文(由 net_process_messages 每轮调用)
|
||||
* 函数说明:1. 设计意图(设计文档 §4 不变量 II):其它所有位置只**置事实**
|
||||
* (session_dead / 状态迁移 / 事件派发),不**做动作**——于是
|
||||
* "谁先看到 DISCONNECT 就谁重建"这类竞争在结构上消失
|
||||
* 2. 覆盖三种情形(前两种原属对账③④,第三种为 U11 缺口):
|
||||
* ① 本层 LISTENING 而芯片监听已失位 → 重建(原对账③)
|
||||
* ② 本层 CLOSED(会话已结束)而芯片不在 LISTEN → 重建(原对账④)
|
||||
* ③ 本层 CLOSED 而芯片**正在** LISTEN → **只做状态重同步**
|
||||
* (U11):芯片监听器是健康的,重下发 open+listen 会因 USE
|
||||
* 错误走进关闭分支、把正听着的 Socket 关掉(U12 同源事故);
|
||||
* 只把本层状态收回 LISTENING,**不发任何芯片命令**
|
||||
* 3. 去抖/冷却沿用对账的实测结论(2026-09-23 实证:无去抖时监听被
|
||||
* 反复重建直至放弃)
|
||||
* 4. session_dead **不在此清零**(与 net_relisten_locked 同一契约:
|
||||
* 重建监听 ≠ 应用已确认会话结束;清零只发生在 handle_connect_event)
|
||||
*/
|
||||
void net_listener_pump(void) {
|
||||
/* 上次轮次时刻 */
|
||||
static uint32_t s_last = 0U;
|
||||
/* 连续失配计数(去抖) */
|
||||
static uint8_t s_bad[NET_MAX_SOCKETS];
|
||||
/* 冷却到期时刻(防抖成风暴) */
|
||||
static uint32_t s_cool[NET_MAX_SOCKETS];
|
||||
/* 当前 tick */
|
||||
uint32_t now = HAL_GetTick();
|
||||
/* Socket 循环索引 */
|
||||
int i;
|
||||
|
||||
/* 未到 pump 周期:直接返回 */
|
||||
if ((uint32_t)(now - s_last) < NET_LISTENER_PUMP_PERIOD_MS) {
|
||||
return;
|
||||
}
|
||||
s_last = now;
|
||||
|
||||
for (i = 0; i < NET_MAX_SOCKETS; i++) {
|
||||
/* 取控制块 */
|
||||
net_sock_t *p_sock = net_get_sock(i);
|
||||
/* 芯片侧状态两字节 */
|
||||
uint8_t st[2] = {0, 0};
|
||||
/* 芯片当前是否在 LISTEN */
|
||||
int chip_listening;
|
||||
|
||||
/* 控制块无效:跳过 */
|
||||
if (p_sock == NULL) {
|
||||
s_bad[i] = 0U;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 认领门槛:仅监听型 fd(在用 + 自动重监听 + TCP + 已绑端口)。
|
||||
* auto_relisten 是决定性门:少了它,"本层 CLOSED + 已绑端口"的出向
|
||||
* 连接(板级 Phase 7 TC701~703 用 net_connect 绑本地口)会被误判为
|
||||
* 监听器并被 relisten(同对账④的既定口径) */
|
||||
if ((p_sock->in_use == 0U) ||
|
||||
(p_sock->auto_relisten == 0U) ||
|
||||
(p_sock->type != NET_SOCK_STREAM) ||
|
||||
(p_sock->local_port == 0U)) {
|
||||
s_bad[i] = 0U;
|
||||
continue;
|
||||
}
|
||||
/* 只处理与监听生命周期相关的两个状态 */
|
||||
if ((p_sock->state != NET_SOCK_STATE_LISTENING) &&
|
||||
(p_sock->state != NET_SOCK_STATE_CLOSED)) {
|
||||
s_bad[i] = 0U;
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 回读芯片状态(每监听型 fd 一条命令) */
|
||||
ch395f_get_socket_status((uint8_t)i, st);
|
||||
chip_listening = ((st[0] != CH395F_SOCKET_CLOSED) &&
|
||||
(st[1] == CH395F_TCP_LISTEN));
|
||||
|
||||
/* 芯片在听 */
|
||||
if (chip_listening) {
|
||||
s_bad[i] = 0U;
|
||||
/* 本层 CLOSED 而芯片在听 = U11(本层状态滞后)→ 只重同步 */
|
||||
if (p_sock->state == NET_SOCK_STATE_CLOSED) {
|
||||
DBG_INFO("sock%d pump: chip listening, resync CLOSED->LISTENING (U11)", i);
|
||||
/* 只改本层状态,**不发任何芯片命令**(同 net_resync_listen 的
|
||||
* 既定纪律,理由见该函数注释)。session_dead 不清 */
|
||||
p_sock->state = NET_SOCK_STATE_LISTENING;
|
||||
/* N-30(Trap 25):死连接对端端口转登幽灵甄别基准 + 去抖清零
|
||||
* —— 芯片监听器未被重开,芯片侧对端信息可能仍是上一条连接的,
|
||||
* 必须转登为基准,否则对账② 的幽灵拦截会失效(同 net_resync_listen) */
|
||||
p_sock->last_peer_port = p_sock->remote_port;
|
||||
p_sock->lost_conn_seen = 0U;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
/* 芯片不在听:情形①②同一处置——重建(本层唯一重建入口) */
|
||||
if (s_bad[i] < NET_LISTENER_STABLE_N) {
|
||||
s_bad[i]++;
|
||||
}
|
||||
/* 去抖未满或处于冷却期:本轮不动手 */
|
||||
if ((s_bad[i] < NET_LISTENER_STABLE_N) ||
|
||||
((int32_t)(now - s_cool[i]) < 0)) {
|
||||
continue;
|
||||
}
|
||||
s_bad[i] = 0U;
|
||||
s_cool[i] = now + NET_LISTENER_COOLDOWN_MS;
|
||||
DBG_INFO("sock%d pump: chip not listening (0x%02X/0x%02X, was state=%d) -> relisten",
|
||||
i, st[0], st[1], (int)p_sock->state);
|
||||
/* 非阻塞:失败由 D10 重试轮询接管 */
|
||||
(void)net_listener_try(i);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
/*
|
||||
* 模块名称:Network Listener Lifecycle (Header)
|
||||
* 模块功能:监听器生命周期模块的对外接口(**真机专属**)—— 维持 fd 处于
|
||||
* LISTEN 态的全部机制:重建、重建前的沉降/隔离(Trap 23)、排空 RX
|
||||
* 残渣、D10 非阻塞重试、周期性自愈 pump。
|
||||
* 适用平台:STM32F407ZGTx + CH395F 以太网芯片
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-12
|
||||
* 修改记录:
|
||||
* 2026-10-12 王建锋 从 net_socket.c 抽出监听生命周期(N-32 §10 模块化)
|
||||
*
|
||||
* 职责边界(详见 docs/子系统设计/网络Socket监听生命周期设计说明.md §10):
|
||||
* 本模块只负责「让一个监听型 fd 保持/回到 LISTEN 态」。
|
||||
*
|
||||
* **不属于本模块**(留在 net_socket.c):
|
||||
* - `net_listen_locked`(首次监听的**应用 API 路径**,含 BOUND/CREATED 状态门与缓冲)
|
||||
* - `net_accept_locked`(产出的是**连接**:LISTENING→ESTABLISHED 迁移)
|
||||
* - `net_lost_connect_probe`(做的是**补 accept**,即连接建立)
|
||||
* - `net_listener_armed` / `net_resync_listen`(双端公开 API,sim 也实现)
|
||||
*
|
||||
* ⚠ 本头文件**只被 net_socket.c include**(内部头),不进公开 net_socket.h ——
|
||||
* 故模拟器(simulator/sim/net_socket_sim.c)对本模块**零依赖**,CMakeLists 不编本文件。
|
||||
*/
|
||||
#ifndef NET_LISTENER_H
|
||||
#define NET_LISTENER_H
|
||||
|
||||
#include "net_types.h"
|
||||
|
||||
/*
|
||||
* 沉降/隔离等级:重建监听前"清芯片收尾状态"的强度
|
||||
* (替代此前外部直接写 s_relisten_quarantine[])
|
||||
* QUARANTINE - 异常释放(芯片 TIMEOUT / 对端 RST):对端可能仍有在途数据,
|
||||
* 须排空残渣 + 最短等 400 ms
|
||||
* GRACEFUL - 正常释放(芯片 DISCONNECT / 对端 FIN):无在途数据,
|
||||
* 清理 + 最短等 100 ms(等芯片完成上一条连接的内部收尾)
|
||||
*/
|
||||
typedef enum {
|
||||
NET_LISTENER_SETTLE_QUARANTINE = 1, /* 异常释放:隔离 400 ms + 排空 RX 残渣 */
|
||||
NET_LISTENER_SETTLE_GRACEFUL = 2, /* 正常释放:沉降 100 ms */
|
||||
} net_listener_settle_t;
|
||||
|
||||
/*
|
||||
* 函数功能:登记「本 fd 的会话已结束,重建监听前需沉降/隔离」
|
||||
* 入口参数:sockfd - Socket 描述符 (0~7) int
|
||||
* level - 沉降等级(释放来源决定,见 net_listener_settle_t)
|
||||
* 出口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:netTask 上下文;由 handle_disconnect_event / handle_timeout_event 调用
|
||||
* 函数说明:1. 只置事实,不做动作 —— 重建由 net_listener_pump / net_listener_try 负责
|
||||
* 2. 越界 sockfd 静默忽略(不设错误码,与旧直接写数组行为一致)
|
||||
*/
|
||||
void net_listener_settle(int sockfd, net_listener_settle_t level);
|
||||
|
||||
/*
|
||||
* 函数功能:尝试重建监听一次(**不阻塞**);失败则登记"待重试"
|
||||
* 入口参数:sockfd - Socket 描述符 (0~7) int
|
||||
* 出口参数:无
|
||||
* 返回值:0 - 成功(监听器已就绪);-1 - 本次失败或已在重试队列中 int
|
||||
* 限定条件:netTask 上下文;fd 为监听型(auto_relisten)
|
||||
* 函数说明:1. 已在重试队列时直接返回 -1(不重复尝试、不重置重试时刻)——
|
||||
* 否则每 10 ms 一次的 accept 轮询会不断把重试时刻往后推,
|
||||
* 导致永不重试(重试饥饿)
|
||||
* 2. 替代此前散落在 net_socket.c 各处的 net_relisten_try()
|
||||
*/
|
||||
int net_listener_try(int sockfd);
|
||||
|
||||
/*
|
||||
* 函数功能:清除本 fd 的待重试/沉降状态(关闭或新建 fd 时调用)
|
||||
* 入口参数:sockfd - Socket 描述符 (0~7) int
|
||||
* 出口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:无(越界静默返回)
|
||||
* 函数说明:防同 fd 下一生成继承上轮的重试队列或 400/800 ms 隔离等待
|
||||
*/
|
||||
void net_listener_clear(int sockfd);
|
||||
|
||||
/*
|
||||
* 函数功能:D10 重试轮询(netTask 每轮调用,非阻塞)
|
||||
* 入口参数:无
|
||||
* 出口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:netTask 上下文(由 net_process_messages 每轮调用)
|
||||
* 函数说明:到点重试待办的重监听;成功即报告探针样本;超时放弃并记错误日志
|
||||
*/
|
||||
void net_listener_retry_tick(void);
|
||||
|
||||
/*
|
||||
* 函数功能:监听器 pump —— 本层**唯一**的周期性重建决策者
|
||||
* 入口参数:无
|
||||
* 出口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:netTask 上下文(由 net_process_messages 每轮调用)
|
||||
* 函数说明:1. 覆盖三种情形:① LISTENING 而芯片失位 → 重建
|
||||
* ② CLOSED 而芯片不在 LISTEN → 重建
|
||||
* ③ CLOSED 而芯片**正在** LISTEN → 只做状态重同步(U11,不发芯片命令)
|
||||
* 2. 100 ms 周期 + 去抖 + 冷却(详见实现处注释)
|
||||
*/
|
||||
void net_listener_pump(void);
|
||||
|
||||
/* 注:本模块消费的宿主能力 `net_pending_active()` 声明在 net_socket.h
|
||||
* (它是 net_socket.c 的函数,须与配对头文件同处,见规范 S17 2.2)。 */
|
||||
|
||||
#endif /* NET_LISTENER_H */
|
||||
+441
-443
File diff suppressed because it is too large
Load Diff
@@ -150,6 +150,70 @@ int net_listen(int sockfd);
|
||||
*/
|
||||
int net_accept(int sockfd, struct net_sockaddr *addr, int *addrlen);
|
||||
|
||||
/*
|
||||
* 函数功能:查询监听 Socket 当前是否有可接受的连接(**纯 RAM 读,不发起 RPC**)
|
||||
* 入口参数:sockfd - 监听 Socket 描述符 int
|
||||
* 出口参数:无
|
||||
* 返回值:1 - 有连接可接受;0 - 无(含描述符无效) int
|
||||
* 限定条件:net_init() 已调用;任意任务上下文(含 uiTask 等实时性敏感任务)
|
||||
* 函数说明:1. 只读 Socket 控制块状态,**不经消息队列、不等 netTask 回执** →
|
||||
* 调用方不会被 netTask 的忙碌程度拖住(对比 net_accept:
|
||||
* osMessageQueuePut(osWaitForever) + xTaskNotifyWait(portMAX_DELAY),
|
||||
* 两者均**无上界**)
|
||||
* 2. 判据与 net_accept_locked() 完全一致:单连接模型下监听 Socket 自身
|
||||
* 在 CONNECT 事件后转 ESTABLISHED,即"有连接可接受"
|
||||
* 3. 用途 = **门控**:无连接时直接跳过 net_accept(),把"每轮一次无谓
|
||||
* RPC"降为 0(UI 推流 10 ms 轮询 = 100 msg/s 队列压力的根因)
|
||||
* 4. 跨任务读单字:netTask 是唯一写者,最坏两种结果都无害——
|
||||
* 晚一轮发现(netTask 每 1 ms 就 net_poll 一次),或扑空
|
||||
* (net_accept 返回 -1 + NET_ERR_WOULDBLOCK)
|
||||
* 5. 丢 CONNECT 中断由 net_reconcile_tick()(200 ms 对账 + 去抖)自愈,
|
||||
* 本函数**不承担**自愈责任
|
||||
*/
|
||||
int net_can_accept(int sockfd);
|
||||
|
||||
/*
|
||||
* 函数功能:查询监听 Socket 是否处于"应该尝试 accept"的就绪态(**纯 RAM 读,不发起 RPC**)
|
||||
* 入口参数:sockfd - 监听 Socket 描述符 int
|
||||
* 出口参数:无
|
||||
* 返回值:1 - 就绪(LISTENING,或 CLOSED + auto_relisten = 断连待重建态);0 - 不就绪 int
|
||||
* 限定条件:net_init() 已调用;任意任务上下文
|
||||
* 函数说明:1. N-32(P1 不变量 II):取代 `net_can_accept()`(只认 ESTABLISHED)作为
|
||||
* ui_net_sink 的 accept 门控——原门控使 :7003 永不进 `net_accept()`,
|
||||
* `net_lost_connect_probe`(丢 CONNECT 中断补 accept,N-30 实证的
|
||||
* "数据被吞/425"主根因修复)对该端口等于从未运行
|
||||
* 2. 放行 LISTENING:探针与 "closed-but-readable 接受" 都发生在
|
||||
* `net_accept_locked` **内部**,门控挡在外面就永远到不了
|
||||
* 3. 放行 CLOSED+auto_relisten **且 readable≠0**(有残留数据):
|
||||
* 保住 N-30 的 "closed-but-readable" 接受;**无残留时必须返回 0**,
|
||||
* 否则调用方会在整个 CLOSED 窗口内每轮发起 `net_accept`(RPC 无上界)
|
||||
* → 队列积压 → 喂不上狗(2026-10-12 P2a 台架实测 48 条 task stall)。
|
||||
* 重建由 `net_listener_pump` 负责,调用方无需催促
|
||||
* 4. 与 `net_can_accept` 的语义差异**有意为之**:本函数回答"这个监听器
|
||||
* 这一轮**值不值得**发起一次 accept",而非"一定有连接"
|
||||
* 5. 跨任务读单字,同 `net_can_accept` 的最坏情形论证
|
||||
*/
|
||||
int net_listener_armed(int sockfd);
|
||||
|
||||
/*
|
||||
* 函数功能:查询 Socket 是否"值得发一次 recv"(**纯 RAM 读,不发起 RPC**)
|
||||
* 入口参数:sockfd - Socket 描述符 int
|
||||
* 出口参数:无
|
||||
* 返回值:1 - 应发 recv(有数据可读,或连接已非 ESTABLISHED 需了结);0 - 不必发 int
|
||||
* 限定条件:net_init() 已调用;任意任务上下文
|
||||
* 函数说明:1. ESTABLISHED → 看 readable(net_poll 在 RECV_OK 置位、读空清零)
|
||||
* 2. **非 ESTABLISHED 一律返回 1**:连接已消失时必须让上层真的 recv 一次
|
||||
* 拿到 EOF/错误并自行复位;若把它门掉,"连接已不存在"会被永久藏住
|
||||
* (推流侧 g_client 永不复位 → 再也不接受新客户端)
|
||||
* 3. 描述符失效(net_get_sock 返回 NULL)同样返回 1 → 让 recv 报 BADF,
|
||||
* 走调用方既有的错误分支重建,而不是静默卡死
|
||||
* 4. ⚠ **不可作为唯一判据**:readable 只由 RECV_OK 中断置位,而
|
||||
* net_reconcile_tick 的三类对账**不含**"芯片 RX 非空但 readable=0"
|
||||
* → 中断丢失时本函数会恒返 0。调用方须保留**低频兜底 recv**
|
||||
* (ui_net_sink 每 32 轮无条件收一次,最坏延迟 ≈320 ms)
|
||||
*/
|
||||
int net_can_read(int sockfd);
|
||||
|
||||
/*
|
||||
* 函数功能:TCP Client 发起连接
|
||||
* 入口参数:sockfd - socket 描述符
|
||||
@@ -371,6 +435,34 @@ void net_process_messages(void);
|
||||
*/
|
||||
net_sock_t *net_get_sock(int sockfd);
|
||||
|
||||
/*
|
||||
* 函数功能:记录**本任务**最后一次失败的精确错误码(net 层内部通用工具)
|
||||
* 入口参数:err - 错误码(NET_ERR_xxx,负值)
|
||||
* 出口参数:无
|
||||
* 返回值:err 原值(便于写成 `return net_set_errno(NET_ERR_XXX);`)
|
||||
* 限定条件:无
|
||||
* 函数说明:1. 只写本任务的槽位,其他任务读不到、也改不了 → 消除跨任务覆写竞态
|
||||
* 2. N-32 §10 模块化:由 net_socket.c 的 static 提升为**模块间可见**,
|
||||
* 供 net_listener.c 等同目录内部模块复用(定义仍在 net_socket.c)
|
||||
*/
|
||||
int net_set_errno(int err);
|
||||
|
||||
/* 「记本任务错误码 + 统一失败返回 -1」的紧凑写法(取代 `s_net_errno = X; return -1;`)
|
||||
* N-32 §10:原定义在 net_socket.c,随 net_set_errno 一并提升到本头文件 */
|
||||
#define NET_ERR_RETURN_1(err) (net_set_errno(err), -1)
|
||||
|
||||
/*
|
||||
* 函数功能:查询本 fd 是否有在途挂起 recv(L3 pending)—— 供 net_listener.c 消费
|
||||
* 入口参数:sockfd - Socket 描述符 (0~7) int
|
||||
* 出口参数:无
|
||||
* 返回值:1 - 有挂起;0 - 无(含描述符越界) int
|
||||
* 限定条件:任意上下文(纯 RAM 读,不发起 RPC)
|
||||
* 函数说明:1. N-32 §10 模块化:net_listener_retry_tick 用它跳过"重开 Socket
|
||||
* 会摧毁在途连接"的 fd(L3 保险,原为直接读 s_net_pending[i].active)
|
||||
* 2. 定义在 net_socket.c(配对头即本文件)
|
||||
*/
|
||||
int net_pending_active(int sockfd);
|
||||
|
||||
/*
|
||||
* 函数功能:获取 Socket 本地地址(IP + Port)
|
||||
* 入口参数:sockfd - socket 描述符
|
||||
|
||||
@@ -179,6 +179,32 @@ typedef struct {
|
||||
* 连接的释放原因。 */
|
||||
uint8_t released_abnormal;
|
||||
|
||||
/* N-30(2026-10-11 裸机芯片台架实证):本 Socket 上一条被释放连接的**对端端口**。
|
||||
* 芯片拆链收尾窗口内 Socket 会假报 ESTABLISHED(幽灵态),此时回读的对端端口
|
||||
* = 旧连接端口;新连接的临时端口几乎必然不同。accept 路径补 accept 与对账②
|
||||
* 均以此甄别幽灵态(见 net_socket.c Trap 25)。跨 fd 释放保留(幽灵属芯片
|
||||
* Socket 级,与 fd 复用无关),新连接断开时刷新。 */
|
||||
uint16_t last_peer_port;
|
||||
/* N-30:accept 路径"丢 CONNECT 中断补 accept"的去抖计数(连续确认次数,
|
||||
* 节拍 = accept 轮询 100ms;新监听/补 accept 后清零) */
|
||||
uint8_t lost_conn_seen;
|
||||
|
||||
/* N-32 / B10 根治 P1(2026-10-12,设计文档 §2.3 不变量 I):本会话"已结束"的
|
||||
* **电平**(粘滞),取代 `state == CLOSED` 这一**边沿**。
|
||||
* 为什么必须电平化:单连接复用模型下同一个 fd 既是监听器又是连接(handle
|
||||
* aliasing),"关掉一条连接"与"毁掉一个监听器"在类型上无法区分;而"会话结束"
|
||||
* 原先只能靠 `state == CLOSED` 这一刻被观察到——一旦别的路径抢先 relisten
|
||||
* 回 LISTENING(对账④/`net_accept_locked` 的 CLOSED 分支),消费者再 recv
|
||||
* 就恒得 WOULDBLOCK,EOF 永远失踪(B10"消费者卡死"与新连接被旧会话静默吸收)。
|
||||
* session_dead 让"会话已结束"可重复查询、幂等:谁先看到不重要,谁都饿不着。
|
||||
* 置位:handle_disconnect_event 释放分支 / handle_timeout_event ESTABLISHED
|
||||
* 分支 / net_connect_locked 命令被拒 / 对账①(复用前者,:654)。
|
||||
* 清零:新 CONNECT 成功(handle_connect_event 两支)、net_close/net_socket
|
||||
* (memset 整块清零)、net_relisten_locked 的 peer 簿记**不清**——
|
||||
* 这是关键:重建监听与"应用是否已确认会话结束"必须解耦(design §5 P1.1)。
|
||||
* 跨 fd 保留语义同 released_abnormal(幽灵/陈旧事件属芯片 Socket 级)。 */
|
||||
uint8_t session_dead;
|
||||
|
||||
/* 统计信息 */
|
||||
uint32_t recv_bytes; /* 累计接收字节数 */
|
||||
uint32_t send_bytes; /* 累计发送字节数 */
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
* 模块名称:FTP 配置([ftp] 段)
|
||||
* 模块功能:RAM 副本 + 工厂默认 + cfg_file 引擎注册(键表 user/pwd_hash/write)
|
||||
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-11
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-11 王建锋 创建初始版本(N-19 FTP 鉴权)
|
||||
*/
|
||||
|
||||
/* 头文件包含区 */
|
||||
#include <stddef.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "ftp_cfg.h"
|
||||
|
||||
#include "cfg_file.h"
|
||||
|
||||
/* 私有变量定义区 */
|
||||
|
||||
/* 工厂默认(单一来源宏见 ftp_cfg.h;s_cfg 初值与其一致 = load 前也 fail-closed 可用) */
|
||||
static const ftp_cfg_t s_defaults = {
|
||||
FTP_CFG_DEF_USER,
|
||||
FTP_CFG_DEF_HASH,
|
||||
FTP_CFG_DEF_WRITE,
|
||||
};
|
||||
|
||||
/* RAM 副本(引擎原地更新;注册于 net_cfg_load 内 cfg_file_load 之前) */
|
||||
static ftp_cfg_t s_cfg = {
|
||||
FTP_CFG_DEF_USER,
|
||||
FTP_CFG_DEF_HASH,
|
||||
FTP_CFG_DEF_WRITE,
|
||||
};
|
||||
|
||||
/* [ftp] 段键表(ftp_cfg_t 布局内偏移) */
|
||||
static const cfg_key_def_t s_ftp_keys[] = {
|
||||
{ "user", CFG_VAL_STR, offsetof(ftp_cfg_t, user), FTP_CFG_USER_MAX },
|
||||
{ "pwd_hash", CFG_VAL_STR, offsetof(ftp_cfg_t, pwd_hash), FTP_CFG_HASH_MAX },
|
||||
{ "write", CFG_VAL_UINT, offsetof(ftp_cfg_t, write_en), 1u },
|
||||
};
|
||||
|
||||
/* 函数定义区 */
|
||||
|
||||
/*
|
||||
* 函数功能:大小写不敏感字符串相等判断
|
||||
* 入口参数:p_a - 串 A p_b - 串 B(均可 NULL)
|
||||
* 返回值:1 - 相等;0 - 不等或任一为 NULL int
|
||||
* 限定条件:无
|
||||
*/
|
||||
static int s_str_ci_eq(const char *p_a, const char *p_b) {
|
||||
if ((p_a == NULL) || (p_b == NULL)) {
|
||||
return 0;
|
||||
}
|
||||
for (size_t i = 0u; ; i++) {
|
||||
char a = p_a[i];
|
||||
char b = p_b[i];
|
||||
|
||||
if ((a >= 'A') && (a <= 'Z')) {
|
||||
a = (char)(a + 32);
|
||||
}
|
||||
if ((b >= 'A') && (b <= 'Z')) {
|
||||
b = (char)(b + 32);
|
||||
}
|
||||
if (a != b) {
|
||||
return 0;
|
||||
}
|
||||
if (a == '\0') {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:注册 [ftp] 段到 cfg_file 引擎(须在 cfg_file_load 之前调用)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:幂等(重复调用只注册一次);由 net_cfg_load 统一触发
|
||||
*/
|
||||
void ftp_cfg_register(void) {
|
||||
static uint8_t s_done;
|
||||
cfg_section_reg_t reg;
|
||||
|
||||
if (s_done != 0u) {
|
||||
return;
|
||||
}
|
||||
(void)memset(®, 0, sizeof(reg));
|
||||
reg.name = "ftp";
|
||||
reg.p_data = &s_cfg;
|
||||
reg.p_defaults = &s_defaults;
|
||||
reg.data_size = sizeof(ftp_cfg_t);
|
||||
reg.p_keys = s_ftp_keys;
|
||||
reg.key_count = 3u;
|
||||
reg.p_cb = NULL; /* 无"立即应用"需求:口令在下次 PASS 时生效 */
|
||||
(void)cfg_file_register(®);
|
||||
s_done = 1u;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:取 [ftp] 段 RAM 副本(load 前后均有效,load 前 = 工厂默认)
|
||||
* 入口参数:无
|
||||
* 返回值:配置指针(恒非 NULL,指向静态存储)
|
||||
* 限定条件:调用方只读
|
||||
*/
|
||||
const ftp_cfg_t *ftp_cfg_get(void) {
|
||||
return &s_cfg;
|
||||
}
|
||||
|
||||
/*
|
||||
* 函数功能:判断当前口令哈希是否仍为出厂默认(启动告警依据)
|
||||
* 入口参数:无
|
||||
* 返回值:1 - 仍是默认;0 - 已改 int
|
||||
* 限定条件:大小写不敏感比较
|
||||
*/
|
||||
int ftp_cfg_is_default_pwd(void) {
|
||||
return s_str_ci_eq(s_cfg.pwd_hash, FTP_CFG_DEF_HASH);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 模块名称:FTP 配置([ftp] 段)
|
||||
* 模块功能:FTP 服务鉴权配置——RAM 副本 + 工厂默认 + cfg_file 引擎注册
|
||||
* ([ftp] 段:user / pwd_hash / write);消费者(lftpd/lftpd_auth)
|
||||
* 经 ftp_cfg_get() 只读,配置引擎属主仍为 defaultTask
|
||||
* 适用平台:STM32F407ZGTx / PC 模拟器(两端共用)
|
||||
* 作者:王建锋
|
||||
* 创建日期:2026-10-11
|
||||
* 修改记录:
|
||||
* v1.0 2026-10-11 王建锋 创建初始版本(N-19 FTP 鉴权)
|
||||
*/
|
||||
|
||||
#ifndef FTP_CFG_H
|
||||
#define FTP_CFG_H
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
#define FTP_CFG_USER_MAX 16u /* 用户名缓冲长度(含 NUL) */
|
||||
#define FTP_CFG_HASH_MAX 65u /* SHA-256 hex 摘要 64 字符 + NUL */
|
||||
|
||||
/* 工厂默认:user=admin、口令=admin(pwd_hash = SHA256("adminadmin"))、write=1。
|
||||
* write=1 保持既有维护工作流(FTP 推 config.ini);生产配置应改口令并按需 write=0。
|
||||
* 出厂默认口令在用期间,lftpd_start 每次启动打 INFO 告警锚点(N-19) */
|
||||
#define FTP_CFG_DEF_USER "admin"
|
||||
#define FTP_CFG_DEF_HASH "d82494f05d6917ba02f7aaa29689ccb444bb73f20380876cb05d1f37537b7892"
|
||||
#define FTP_CFG_DEF_WRITE 1u
|
||||
|
||||
typedef struct {
|
||||
char user[FTP_CFG_USER_MAX]; /* 期望用户名(大小写不敏感) */
|
||||
char pwd_hash[FTP_CFG_HASH_MAX]; /* SHA-256(user‖password) hex(大小写不敏感) */
|
||||
uint8_t write_en; /* 1 = 允许 STOR/DELE;0 = 只读 */
|
||||
} ftp_cfg_t;
|
||||
|
||||
/*
|
||||
* 函数功能:向 cfg_file 引擎注册 [ftp] 段(幂等)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:cfg_file_load 之前调用(net_cfg_load 内统一编排)
|
||||
*/
|
||||
void ftp_cfg_register(void);
|
||||
|
||||
/*
|
||||
* 函数功能:取 [ftp] 段 RAM 副本(只读)
|
||||
* 入口参数:无
|
||||
* 返回值:恒非 NULL(静态常驻,load 前初值 = 工厂默认) const ftp_cfg_t *
|
||||
* 限定条件:无
|
||||
* 函数说明:defaultTask poll 重载瞬间可能读到撕裂值 → 该次登录失败
|
||||
* (fail-closed,重试即恢复;配置变更本就罕见)
|
||||
*/
|
||||
const ftp_cfg_t *ftp_cfg_get(void);
|
||||
|
||||
/*
|
||||
* 函数功能:判断当前 pwd_hash 是否仍为出厂默认口令哈希
|
||||
* 入口参数:无
|
||||
* 返回值:1 - 默认口令在用;0 - 已改 int
|
||||
* 限定条件:无
|
||||
*/
|
||||
int ftp_cfg_is_default_pwd(void);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif /* FTP_CFG_H */
|
||||
@@ -30,6 +30,8 @@
|
||||
|
||||
#include "cfg_file.h"
|
||||
|
||||
#include "ftp_cfg.h"
|
||||
|
||||
/* 深调用链模块(net_cfg_load/save 在 defaultTask 启动链与保存链上):
|
||||
* 日志行缓冲收到 128 B(默认 192),本模块最长行渲染后 < 128 → 不截断 */
|
||||
#define DBG_LINE_BUF_MAX 128U
|
||||
@@ -145,6 +147,8 @@ static bool s_mac_is_zero(const uint8_t *p_mac) {
|
||||
*/
|
||||
int net_cfg_load(void) {
|
||||
s_net_register();
|
||||
/* N-19:[ftp] 段一并注册(所有受管段必须先注册、后 load,引擎按注册表解析) */
|
||||
ftp_cfg_register();
|
||||
|
||||
/* 旧档 NCFG 迁移已于 2026-10-10 删除(net_cfg_legacy 移除;理由见文件头 v1.3)。
|
||||
* 若卷上仍残留 /cfg/netcfg.bin:不读、不删、不迁移 → 配置回落工厂默认 */
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
|
||||
## 关键点
|
||||
|
||||
- **周期重触发由 TIM6 更新中断(1 kHz)驱动**(2026-10-10):`app_main_init` 末尾 `HAL_TIM_Base_Start_IT(&htim6)` 启动节拍,`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 TIM6 分支内调 `tpafe5160_start_conv_irq()`(ISR 内仅两次 GPIO 写)。B1 阶段曾由 `adcTask` 每 1 ms 软触发(已改)
|
||||
- **周期重触发由 TIM3 更新事件(1 kHz)驱动**(2026-10-10):`app_main_init` 末尾 `HAL_TIM_Base_Start_IT(&htim3)` 启动节拍,`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 TIM3 分支内调 `tpafe5160_start_conv_irq()`(ISR 内仅两次 GPIO 写)。B1 阶段曾由 `adcTask` 每 1 ms 软触发(已改)。⚠ **节拍源必须是 TIM3 而非 TIM6**:TIM3 的同一个更新事件还经 **TRGO** 硬件触发 STM32 ADC1,两片转换器由此同源对齐;F4 上 TIM6/TIM7 的 TRGO 只接 DAC,喂不到 ADC1
|
||||
- **取数只用 `tpafe5160_snapshot(int16_t *dst, uint8_t count)`**(**仅任务上下文**,内部临界区原子拷贝;返回 1=新帧 / 0=无新帧 / -1=参数无效)。配套的 `tpafe5160_frame_seq()` 给帧序号、`tpafe5160_frstdata_err()` 给"FRSTDATA 无跳变"帧计数
|
||||
- ⚠️ **不要用缓冲区指针取数**:旧的 `get_buf()` / `data_ready()` / `clear_ready()` 已于 2026-10-10 **删除**(审计 E-45 / E-58)——`get_buf` 只给指针、任务再逐通道拷,中间被 BUSY EXTI 抢占就拷回**两帧混合**数据;`s_ready` 电平标志在 `start_conv_irq` 里被清 0,等于**用重触发去丢帧**。现由**帧序号**(只增不清)+ **临界区快照**取代
|
||||
- **`tpafe5160_init()` 做首帧有效性自检**(E-44):完整跑一帧转换+读取,若整帧读下来 FRSTDATA **没有发生任何跳变**(芯片没在驱动并行总线;板上上拉会把它钉死为高)→ 返回 `TPAFE5160_FRSTDATA_ERR`(-3) 并打 `[ERR] Init FRSTDATA no-toggle`。判据刻意不锁死"哪个 RD 边沿拉高/拉低"(手册只给"RD 下降沿 → FRSTDATA 高/低"两个跳变,本板未实测边沿次序),只要求"一帧内必须跳变" → 既抓死芯片,也不会因极性假设错误误杀好芯片
|
||||
- 采样数据的消费者 `App/task/adc_task.c` **已实现**(B1,2026-10-10:`tpafe5160_snapshot()` 原子取数 + 帧统计;`adcTask` 在 `Src/freertos.c` 的 `USER CODE` 区创建)。**v2.2(2026-10-10)**:采样节拍改由 TIM6 更新中断硬件触发,本任务退化为纯消费者
|
||||
- 采样数据的消费者 `App/task/adc_task.c` **已实现**(B1,2026-10-10:`tpafe5160_snapshot()` 原子取数 + 帧统计;`adcTask` 在 `Src/freertos.c` 的 `USER CODE` 区创建)。**v2.2(2026-10-10)**:采样节拍改由定时器更新中断硬件触发,本任务退化为纯消费者;**v2.8(2026-10-10)**:节拍源改 TIM3(同时经 TRGO 触发 ADC1 + DMA),STM32 侧改为消费 DMA 乒乓缓冲,单拍耗时 122 µs → **8 µs**
|
||||
- CONVST→BUSY→RD 三段时序对**建立/保持时间敏感**,改动需对照芯片手册
|
||||
- ⚠️ 本项目 ADC 相关功能**新增任务/缓冲前须核算 RAM**(见 `docs/内存管理/`);接消费者前的强制门禁见根 `AGENTS.md`「接 ADC / RS-485 消费者前的强制门禁」
|
||||
- ⚠️ **通道语义不是通道号**:`buf[i]` 对应哪个电气量必须查 `docs/硬件平台/ADC通道映射说明.md`(`buf[2]=IA`、`buf[5]=UN`、`buf[6]=UA`);V0.1 上 8 路电气量分属两片转换器属硬件缺陷(N-18)→ **禁止**跨转换器做相位/功率/功率因数类运算
|
||||
|
||||
@@ -15,6 +15,21 @@
|
||||
* 2026-10-02 王建锋 E-04:remo_send 校验 net_send 返回值(原丢弃)——短写/失败
|
||||
* 告警,连续 3 次失败阻断推送,新连接/网络重建解除阻断
|
||||
* (MCU 单连接模型不得 net_close 数据 fd,会连监听一起毁掉)
|
||||
* 2026-10-10 王建锋 I-1 门控 + I-2 兜底(Task #32 §9.3,销项 N-10):accept/recv
|
||||
* 前先用 net_can_accept()/net_can_read()(纯 RAM 读控制块)判
|
||||
* "有没有活",无活直接返回——台架常态(无客户端)从每 10 ms
|
||||
* 一次 RPC(100 msg/s,与 FTP 抢深度 8 的队列)降到 0 次。
|
||||
* 兜底:readable 仅由 RECV_OK 中断置位且 Trap 22 对账不含
|
||||
* "RX 非空但 readable=0",故每 32 轮无条件 recv 一次(≈320 ms);
|
||||
* 门控放行时若监听控制块已消失则置 g_listen=-1 触发重建,
|
||||
* 避免"门控把端口永久不再接受新客户端"这一新卡死路径
|
||||
* 2026-10-12 王建锋 N-32(B10 根治 P1,设计文档 §2.3 不变量 II):accept 门控
|
||||
* 由 net_can_accept()(只认 ESTABLISHED)改判
|
||||
* net_listener_armed()(LISTENING 或 CLOSED+auto_relisten)——
|
||||
* 旧门控在断连待重建窗口恒 0,:7003 永不进 net_accept,
|
||||
* net_lost_connect_probe 对该端口等于从未运行;同时 EOF /
|
||||
* 真错误两条路径不再自行 bind_listen(重建交回 net 层,
|
||||
* 消灭第三/第四个重建所有者),仅 fd 真消失时置 -1
|
||||
*/
|
||||
#include "ui_net_sink.h"
|
||||
|
||||
@@ -57,6 +72,18 @@ static uint32_t g_gen; /* 建立监听时的网络代次号(变化
|
||||
static uint8_t s_send_fail_run; /* 连续发送失败计数(E-04:0 = 正常) */
|
||||
static uint8_t s_sink_blocked; /* 推送阻断标志(1 = 停发帧,等客户端重连解除) */
|
||||
|
||||
/*
|
||||
* 兜底收帧节拍(门控失效保险,2026-10-10 I-2):
|
||||
* net_can_read() 的 readable 只由 RECV_OK 中断置位,而 Trap 22 的
|
||||
* net_reconcile_tick() 三类对账**不含**"芯片 RX 非空但 readable=0" →
|
||||
* 该中断一旦丢失,纯门控会把已到达的数据永久藏住(改前靠每轮 recv 内的
|
||||
* ch395f_get_recv_len() 顺带发现)。故每 RM_LIVENESS_DIV 轮**无条件**收一次:
|
||||
* 10 ms 节拍 → 最坏额外延迟 ≈320 ms,而 RPC 频率仍从 100/s 降到 ≈3/s。
|
||||
* 取值必须是 2 的幂(用 & 取模,免除法)。
|
||||
*/
|
||||
#define RM_LIVENESS_DIV 32U
|
||||
static uint8_t s_liveness_ctr; /* 兜底计数器(单写者 = 推流宿主任务) */
|
||||
|
||||
/*
|
||||
* 函数功能:计算 RemoDispBus 帧 payload 的 CRC 校验值
|
||||
* 入口参数:d - 待校验数据指针(len 为 0 时不访问)
|
||||
@@ -449,10 +476,20 @@ int ui_net_sink_start(uint16_t port) {
|
||||
* 限定条件:ui_net_sink_start() 已成功调用
|
||||
* 函数说明: 1. 网络代次变化(重建)时关闭旧监听/客户端并重建
|
||||
* 2. 无监听(启动失败)时重试启动
|
||||
* 3. 无客户端时尝试 accept,成功则复位状态机
|
||||
* 4. 有客户端时非阻塞 recv 逐字节喂入状态机
|
||||
* 5. 对端关闭/真错误时关闭客户端;若监听 Socket 随客户端失效
|
||||
* (MCU 单连接模型)则重建监听,等待重连
|
||||
* 3. 无客户端时**先门控**(net_listener_armed,纯 RAM 读)再 accept,成功则复位状态机。
|
||||
* 门控判据 = "监听器还在服役"(LISTENING 或 CLOSED+auto_relisten)而非
|
||||
* "本轮必然有连接"——后者会让断连待重建窗口内永不进 net_accept,
|
||||
* 把丢 CONNECT 中断的补 accept 探测永久挡在门外(N-32)
|
||||
* 4. 有客户端时**先门控**(net_can_read)再非阻塞 recv 逐字节喂入状态机;
|
||||
* 门控判为"无数据"时仍每 RM_LIVENESS_DIV 轮无条件收一次(兜底)
|
||||
* 5. 对端关闭/真错误时关闭客户端。**不再自行重建监听**(N-32 不变量 II):
|
||||
* recv 的 EOF/错误分支已触发 net 层 auto_relisten,另有对账④兜底;
|
||||
* 仅 g_listen 的 fd 真消失(net_get_sock 返 NULL)才置 -1 交回 2 重启
|
||||
* ⚠ **门控只消除"无谓的往返",不消除"往返本身无上界"**:一旦真有活要干
|
||||
* (accept / recv / 3204 B 的 LCDMEM 应答),仍是"入队 + 等 netTask 回执",
|
||||
* 最坏 ≈4 s。宿主 = uiTask 主循环;"搬出 uiTask"已完整评估并**决定不做**
|
||||
* (实测刷新最坏周期 = LVGL 帧周期地板,与网络无关),理由与残留暴露
|
||||
* 见 docs/UI设计/RemoDispBus协议与接收设计说明.md §3
|
||||
*/
|
||||
void ui_net_sink_poll(void) {
|
||||
int n;
|
||||
@@ -470,6 +507,7 @@ void ui_net_sink_poll(void) {
|
||||
rm_parser_reset();
|
||||
s_send_fail_run = 0U; /* 网络重建:失败累计清零、解除推送阻断(E-04) */
|
||||
s_sink_blocked = 0U;
|
||||
s_liveness_ctr = 0U; /* 网络重建:兜底节拍重新起算 */
|
||||
(void)ui_net_sink_start(g_port);
|
||||
return;
|
||||
}
|
||||
@@ -480,19 +518,44 @@ void ui_net_sink_poll(void) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* 无客户端时尝试接受连接 */
|
||||
/* 无客户端时尝试接受连接(N-32 P1 不变量 II:门控从 net_can_accept〔只认
|
||||
* ESTABLISHED〕换成 net_listener_armed〔LISTENING 或 CLOSED+auto_relisten〕)
|
||||
* 原因:MCU 单连接模型下断连后 fd 会停在 CLOSED 等 relisten,旧门控在该窗口
|
||||
* 内恒 0 → :7003 永不进 net_accept → net_lost_connect_probe 对该端口等于
|
||||
* 从未运行(丢 CONNECT 中断无第二信源);armed 放行后由 net_accept_locked
|
||||
* 的状态门自行裁决(含 RX 有数据的 closed-but-readable 接受) */
|
||||
if (g_client < 0) {
|
||||
if (net_listener_armed(g_listen) == 0) {
|
||||
/* 监听器已不在服役(fd 消失 / 非监听型)→ 置 -1 交回上方
|
||||
* g_listen < 0 分支重建监听。改前靠"每轮都 accept"撞出 BADF 来暴露,
|
||||
* 加了门控就必须显式兜这一条,否则端口永久不再接受新客户端 */
|
||||
if (net_get_sock(g_listen) == NULL) {
|
||||
g_listen = -1;
|
||||
}
|
||||
return;
|
||||
}
|
||||
int c = net_accept(g_listen, NULL, NULL);
|
||||
if (c >= 0) {
|
||||
g_client = c;
|
||||
rm_parser_reset();
|
||||
s_send_fail_run = 0U; /* 新连接:失败累计清零、解除推送阻断(E-04) */
|
||||
s_sink_blocked = 0U;
|
||||
s_liveness_ctr = 0U; /* 新连接:兜底节拍重新起算 */
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
/* 有客户端时非阻塞收帧,逐字节喂入解析状态机 */
|
||||
/* 有客户端时非阻塞收帧(I-1 门控 + I-2 兜底):
|
||||
* 无数据可读 → 多数轮次直接返回,省掉一次 net_recv 往返(该往返内部
|
||||
* osMessageQueuePut(osWaitForever) + xTaskNotifyWait(portMAX_DELAY),无上界);
|
||||
* 但每 RM_LIVENESS_DIV 轮无条件收一次,防 RECV_OK 中断丢失把数据永久藏住 */
|
||||
if (net_can_read(g_client) == 0) {
|
||||
if (((++s_liveness_ctr) & (RM_LIVENESS_DIV - 1U)) != 0U) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
s_liveness_ctr = 0U;
|
||||
}
|
||||
n = net_recv(g_client, g_rx, (int)sizeof(g_rx), NET_MSG_DONTWAIT);
|
||||
if (n > 0) {
|
||||
for (int i = 0; i < n; i++) {
|
||||
@@ -500,24 +563,40 @@ void ui_net_sink_poll(void) {
|
||||
}
|
||||
} else if (n == 0) {
|
||||
/* 对端关闭:单连接模型下监听通道与数据通道是同一个 Socket,net 层已在
|
||||
* net_recv_locked 的 CLOSED 分支自动把该 Socket 放回 LISTEN;此处绝不能再
|
||||
* net_recv_locked 的 EOF 分支自动把该 Socket 放回 LISTEN;此处绝不能再
|
||||
* net_close 它,否则监听一并被毁(后续连接被拒),且重建监听受 Trap 09 时序限制 */
|
||||
if (g_client != g_listen) {
|
||||
net_close(g_client); /* 多连接后端(PC 模拟器):数据通道独立,需显式关闭 */
|
||||
}
|
||||
g_client = -1;
|
||||
rm_parser_reset();
|
||||
if (net_get_sock(g_listen) == NULL && bind_listen() < 0) {
|
||||
g_listen = -1; /* 重建失败:交回主循环重试,避免拿失效 fd 反复 accept */
|
||||
/* N-32(P1 不变量 II):重建**不再**由本处自行 bind_listen。
|
||||
* ① net_recv_locked 的 EOF 分支已触发 auto_relisten(D10 非阻塞重试);
|
||||
* ② 对账④还会兜底补一次 relisten(Trap 22 实证事件会丢,两层不可互替)。
|
||||
* 本处再做一遍 = 第三个重建所有者,正是 §2.3 那 12 个所有者的同类。
|
||||
* 仅当 fd 真消失(net_get_sock 返 NULL = 网络重建)才置 -1,交回上方
|
||||
* 分支重新走 start → bind_listen */
|
||||
if (net_get_sock(g_listen) == NULL) {
|
||||
g_listen = -1; /* fd 真消失:交回主循环重试 */
|
||||
}
|
||||
} else {
|
||||
/* n < 0:区分"暂无数据"(继续等待)与"真错误"(断开并视情况重建监听) */
|
||||
if (net_get_errno() != NET_ERR_WOULDBLOCK) {
|
||||
net_close(g_client);
|
||||
/* N-32(P1):加 `g_client != g_listen` 护栏,与上面 n==0 分支
|
||||
* 的 :552 一致——MCU 单连接模型下 g_client 可能就是 g_listen 本身,
|
||||
* 无护栏的 net_close 会直接关掉监听 fd(:563,与 :134 注释"严禁"
|
||||
* 自相矛盾),端口随 Trap 09 沉降窗短死。PC 模拟器多连接后端下
|
||||
* 两者恒不等,护栏不改变其行为 */
|
||||
if (g_client != g_listen) {
|
||||
net_close(g_client); /* 多连接后端(PC 模拟器):数据通道独立,需显式关闭 */
|
||||
}
|
||||
g_client = -1;
|
||||
rm_parser_reset();
|
||||
if (net_get_sock(g_listen) == NULL && bind_listen() < 0) {
|
||||
g_listen = -1; /* 重建失败:交回主循环重试 */
|
||||
/* N-32(P1 不变量 II):同 EOF 分支——不再自行 bind_listen,
|
||||
* 重建交回 net 层(recv 的错误分支同样会走 relisten)。仅 fd 真消失
|
||||
* 时置 -1 交回主循环走 start */
|
||||
if (net_get_sock(g_listen) == NULL) {
|
||||
g_listen = -1; /* fd 真消失:交回主循环重试 */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,13 +40,20 @@ extern "C" {
|
||||
int ui_net_sink_start(uint16_t port);
|
||||
|
||||
/*
|
||||
* 函数功能:非阻塞轮询 RemoDispBus 协议
|
||||
* 函数功能:轮询 RemoDispBus 协议(接受连接、收帧、逐字节解析)
|
||||
* 入口参数:无
|
||||
* 返回值:无
|
||||
* 限定条件:ui_net_sink_start() 已成功调用
|
||||
* 函数说明:1. 无客户端时尝试接受连接,成功即上报 INIT
|
||||
* 2. 有客户端时用 DONTWAIT 收帧并解析(KEEPLIVE/INIT/KEY/LCDMEM)
|
||||
* 3. 对端关闭或异常时复位客户端,等待下次重监听,不阻塞 LVGL 刷新
|
||||
* 函数说明:1. 无客户端时先门控(net_can_accept,纯 RAM 读)再尝试接受连接
|
||||
* 2. 有客户端时先门控(net_can_read)再用 DONTWAIT 收帧并解析
|
||||
* (KEEPLIVE/INIT/KEY/LCDMEM);门控判为无数据时仍每 32 轮兜底收一次
|
||||
* 3. 对端关闭或异常时复位客户端,等待下次重监听
|
||||
* ⚠ **不是"零成本非阻塞"**:门控只消除无谓往返;真有活时仍是
|
||||
* "入队 + 等 netTask 回执"(osMessageQueuePut(osWaitForever) +
|
||||
* xTaskNotifyWait(portMAX_DELAY),均无上界;LCDMEM 应答 3204 B 最坏 ≈4 s)
|
||||
* → 会拖住宿主(uiTask)的刷新;"搬出 uiTask"已完整评估并**决定不做**
|
||||
* (实测刷新最坏周期 = LVGL 帧周期地板,与网络无关),见
|
||||
* docs/UI设计/RemoDispBus协议与接收设计说明.md §3
|
||||
*/
|
||||
void ui_net_sink_poll(void);
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
#define configTICK_RATE_HZ ((TickType_t)1000)
|
||||
#define configMAX_PRIORITIES ( 56 )
|
||||
#define configMINIMAL_STACK_SIZE ((uint16_t)128)
|
||||
#define configTOTAL_HEAP_SIZE ((size_t)49152)
|
||||
#define configTOTAL_HEAP_SIZE ((size_t)57344)
|
||||
#define configMAX_TASK_NAME_LEN ( 16 )
|
||||
#define configUSE_TRACE_FACILITY 1
|
||||
#define configUSE_16_BIT_TICKS 0
|
||||
|
||||
@@ -42,6 +42,10 @@ void MX_ADC1_Init(void);
|
||||
|
||||
/* USER CODE BEGIN Prototypes */
|
||||
|
||||
/* ADC1 上电自校准(RM0090 §13.3.6;本仓库 HAL 无 HAL_ADCEx_Calibration_Start,
|
||||
* 实现见 Src/adc.c 的 USER CODE BEGIN 1)。返回 0 = 完成,-1 = 等待超时 */
|
||||
int adc1_calibrate(void);
|
||||
|
||||
/* USER CODE END Prototypes */
|
||||
|
||||
#ifdef __cplusplus
|
||||
|
||||
+2
-1
@@ -55,6 +55,7 @@ void DebugMon_Handler(void);
|
||||
void DMA1_Stream3_IRQHandler(void);
|
||||
void DMA1_Stream4_IRQHandler(void);
|
||||
void EXTI9_5_IRQHandler(void);
|
||||
void TIM3_IRQHandler(void);
|
||||
void SPI1_IRQHandler(void);
|
||||
void SPI2_IRQHandler(void);
|
||||
void USART1_IRQHandler(void);
|
||||
@@ -62,11 +63,11 @@ void USART2_IRQHandler(void);
|
||||
void USART3_IRQHandler(void);
|
||||
void UART4_IRQHandler(void);
|
||||
void UART5_IRQHandler(void);
|
||||
void TIM6_DAC_IRQHandler(void);
|
||||
void TIM7_IRQHandler(void);
|
||||
void DMA2_Stream0_IRQHandler(void);
|
||||
void DMA2_Stream2_IRQHandler(void);
|
||||
void DMA2_Stream3_IRQHandler(void);
|
||||
void DMA2_Stream4_IRQHandler(void);
|
||||
void DMA2_Stream7_IRQHandler(void);
|
||||
/* USER CODE BEGIN EFP */
|
||||
|
||||
|
||||
@@ -32,13 +32,13 @@ extern "C" {
|
||||
|
||||
/* USER CODE END Includes */
|
||||
|
||||
extern TIM_HandleTypeDef htim6;
|
||||
extern TIM_HandleTypeDef htim3;
|
||||
|
||||
/* USER CODE BEGIN Private defines */
|
||||
|
||||
/* USER CODE END Private defines */
|
||||
|
||||
void MX_TIM6_Init(void);
|
||||
void MX_TIM3_Init(void);
|
||||
|
||||
/* USER CODE BEGIN Prototypes */
|
||||
|
||||
|
||||
+85
-27
@@ -7,22 +7,40 @@
|
||||
#ifndef LV_CONF_H
|
||||
#define LV_CONF_H
|
||||
|
||||
/* ⚠ 本文件与 simulator/lv_conf.h 必须保持**同一特性子集**,已由门禁校验:
|
||||
* `python scripts/check_lv_conf_sync.py`(pre-commit 钩子 lv-conf-sync,always 跑)。
|
||||
* 2026-10-11 清理:删掉 14 个 LVGL 9.3 **不存在**的选项(LVGL 8 / 9.4 模板残留,
|
||||
* 写了不生效,与已删的 LV_USE_THEME_BASIC 同类):LV_USE_PNG / JPG / ANIMATION /
|
||||
* ENCODER / MOUSEWHEEL / IMGBTN / METER / COLORWHEEL / TEXTLINE / ASSERT_USER_CODE /
|
||||
* ASSERT_LAYOUT、LV_DRAW_SW_YIELD / ASYNC、LV_COLOR_CHROMA_KEY。
|
||||
* 另把此前靠 lv_conf_internal 默认值隐式生效的开关显式固化为 0(见下"核心特性"块)。 */
|
||||
|
||||
/* 1bpp 单色 */
|
||||
#define LV_COLOR_DEPTH 1
|
||||
#define LV_COLOR_CHROMA_KEY 0x000000
|
||||
|
||||
/* 必需控件(菜单依赖 flex/label/button) */
|
||||
#define LV_USE_OBJ 1
|
||||
#define LV_USE_FLEX 1
|
||||
#define LV_USE_LABEL 1
|
||||
#define LV_USE_BUTTON 1
|
||||
/* 控件裁剪(F-2/F-3,2026-10-11):本项目 UI 全部自绘——实测 Drivers/BSP/UI、App、
|
||||
* simulator 三处**零控件构造函数、零 flex/grid 布局调用**,只用 lv_obj_create +
|
||||
* lv_theme_mono_init。此前这些类靠 lv_theme_simple(9.3 默认 =1)当锚点链进镜像:
|
||||
* label 4,042 + buttonmatrix 3,586 + textarea 3,632 + bar 1,800 + button 26
|
||||
* = 13,086 B;另 flex 2,776 + grid 2,592 = 5,368 B。
|
||||
* ⚠ 将来引入控件型页面(U2 原型库)时按需逐项打开,并同步 simulator/lv_conf.h
|
||||
* 后复跑 ui-verify 五步门禁。
|
||||
* 注:原 LV_USE_OBJ 在 9.3 不是有效选项(lv_conf_internal 无此宏),已删。 */
|
||||
#define LV_USE_FLEX 0 /* F-3:全仓无 lv_obj_set_flex_* 调用 */
|
||||
#define LV_USE_GRID 0 /* F-3:全仓无 lv_obj_set_grid_* 调用 */
|
||||
#define LV_USE_LABEL 0
|
||||
#define LV_USE_BUTTON 0
|
||||
#define LV_USE_BUTTONMATRIX 0
|
||||
#define LV_USE_BAR 0
|
||||
#define LV_USE_TEXTAREA 0
|
||||
#define LV_USE_MENU 0
|
||||
#define LV_USE_IMAGEBUTTON 0 /* 9.3 默认 1;显式关死,与 simulator 一致 */
|
||||
|
||||
/* 刷新周期(ms):离屏渲染,无需高刷 */
|
||||
#define LV_DEF_REFR_PERIOD 20
|
||||
|
||||
/* 关闭动画/日志,省 RAM 与代码 */
|
||||
#define LV_USE_ANIMATION 0
|
||||
/* 关闭日志,省 RAM 与代码。
|
||||
* 注:9.3 **没有** LV_USE_ANIMATION(动画属核心),原该行是无效项,已删。 */
|
||||
#define LV_USE_LOG 0
|
||||
|
||||
/* 不使用 LVGL 自带 OS 抽象,由 ui_task 手动调用 lv_timer_handler */
|
||||
@@ -34,13 +52,12 @@
|
||||
#define LV_MEM_SIZE (40 * 1024)
|
||||
#define LV_MEM_ADR 0 /* 0 = 内部静态分配 */
|
||||
|
||||
/* 断言全部关闭,减少体积 */
|
||||
/* 断言全部关闭,减少体积(9.3 只有这 4 个 ASSERT 宏;原 ASSERT_USER_CODE /
|
||||
* ASSERT_LAYOUT 是 LVGL 8 残留,已删) */
|
||||
#define LV_USE_ASSERT_NULL 0
|
||||
#define LV_USE_ASSERT_MALLOC 0
|
||||
#define LV_USE_ASSERT_USER_CODE 0
|
||||
#define LV_USE_ASSERT_OBJ 0
|
||||
#define LV_USE_ASSERT_STYLE 0
|
||||
#define LV_USE_ASSERT_LAYOUT 0
|
||||
|
||||
/* 内置字体:先启用 Latin 以通过编译;
|
||||
* 中文 1bpp 字库由 fontconverter 生成后通过 ui_font_cn_12 接入并替换默认字体 */
|
||||
@@ -66,27 +83,50 @@
|
||||
/* 文本编码 UTF-8(菜单含中文) */
|
||||
#define LV_TXT_ENC LV_TXT_ENC_UTF8
|
||||
|
||||
/* 主题:关闭默认主题,控件用内置基础样式,省代码 */
|
||||
/* 主题:只用 mono(ui_port.c 显式 lv_theme_mono_init(disp, true, ...) + set_theme + apply)。
|
||||
* ⚠ 9.3 **没有** LV_USE_THEME_BASIC —— 原那条是无效配置(写了也不生效,已删)。
|
||||
* ⚠ 关键:LV_USE_THEME_SIMPLE 默认 =1,lv_display_create() 会因此强链 lv_theme_simple,
|
||||
* 而它正是把上面那些控件类拉进镜像的锚点(主题本体 866 B + 锚定控件 13,086 B)。 */
|
||||
#define LV_USE_THEME_DEFAULT 0
|
||||
#define LV_USE_THEME_BASIC 0
|
||||
#define LV_USE_THEME_SIMPLE 0
|
||||
#define LV_USE_THEME_MONO 1
|
||||
|
||||
/* 绘制相关:1bpp 无需 GPU/抗锯齿 */
|
||||
/* 复杂绘制必须开启:主界面顶部开口浅 V 装饰含斜线,lv_line 斜段依赖 LV_DRAW_SW_COMPLEX,
|
||||
* 关闭则两腿不渲染(只画水平线)。原值靠 lv_conf_internal 默认(=1)隐式生效,此处显式固化。
|
||||
* 注意:LV_DRAW_SW_CIRCLE_CACHE_SIZE 须非零(默认 4),否则零长数组编译报错。 */
|
||||
#define LV_DRAW_SW_COMPLEX 1
|
||||
#define LV_DRAW_SW_YIELD 0
|
||||
#define LV_DRAW_SW_ASYNC 0
|
||||
/* 简单绘制层缓冲:9.3 默认 24 KB(从 LVGL 池里取),本项目 UI 不建层 →
|
||||
* 与 simulator 一致显式设 0(golden 回归已在 0 下逐字节验证) */
|
||||
#define LV_DRAW_LAYER_SIMPLE_BUF_SIZE 0
|
||||
/* 注:9.3 没有 LV_DRAW_SW_YIELD / LV_DRAW_SW_ASYNC(9.4 才有),原两行无效,已删。 */
|
||||
|
||||
/* 输入设备:启用键盘/编码器(远程按键走 keypad) */
|
||||
#define LV_USE_KEYBOARD 0
|
||||
#define LV_USE_MOUSEWHEEL 0
|
||||
#define LV_USE_ENCODER 0
|
||||
/* 软件渲染器的颜色格式支持(F-1,2026-10-11):显示为 I1(LV_COLOR_DEPTH 1),
|
||||
* 只有 blend_to_i1 会被用到。此前 11 个宏全靠 lv_conf_internal 默认(=1)隐式生效,
|
||||
* 把 7 个用不到的混合核整份链进镜像:实测 blend_to_* 合计 40,582 B,其中 i1 仅 5,272 B
|
||||
* → 死代码 35,310 B。每份 blend 文件都是文件级 #if 守卫,关掉 = 整个 TU 不参与编译。
|
||||
* ⚠ 字形 A8 位图是作为 **mask** 传入的,混合按**目的**格式(I1)分派 → 不需要
|
||||
* SUPPORT_A8/L8/AL88(此判断已由 simulator 的 I1-only 配置 + golden 回归验证)。
|
||||
* 取值与 simulator/lv_conf.h 保持一致(模拟器早已是 I1-only)。 */
|
||||
#define LV_DRAW_SW_SUPPORT_I1 1
|
||||
#define LV_DRAW_SW_SUPPORT_L8 0
|
||||
#define LV_DRAW_SW_SUPPORT_AL88 0
|
||||
#define LV_DRAW_SW_SUPPORT_A8 0
|
||||
#define LV_DRAW_SW_SUPPORT_RGB565 0
|
||||
#define LV_DRAW_SW_SUPPORT_RGB565_SWAPPED 0
|
||||
#define LV_DRAW_SW_SUPPORT_RGB565A8 0
|
||||
#define LV_DRAW_SW_SUPPORT_RGB888 0
|
||||
#define LV_DRAW_SW_SUPPORT_XRGB8888 0
|
||||
#define LV_DRAW_SW_SUPPORT_ARGB8888 0
|
||||
#define LV_DRAW_SW_SUPPORT_ARGB8888_PREMULTIPLIED 0
|
||||
|
||||
/* 不使用的第三方扩展 */
|
||||
#define LV_USE_PNG 0
|
||||
/* 输入设备:全部不用(远程按键经 RemoDispBus 直接注入,见 ui_net_sink)。
|
||||
* 注:9.3 没有 LV_USE_MOUSEWHEEL / LV_USE_ENCODER(LVGL 8 残留),已删;
|
||||
* LV_USE_KEYBOARD 原在本文件定义两次,去重(下方控件块保留一处)。 */
|
||||
|
||||
/* 不使用的第三方扩展(9.3 的图片解码宏是 LV_USE_LODEPNG / TJPGD,
|
||||
* 没有 LV_USE_PNG / LV_USE_JPG —— LVGL 8 残留,已删) */
|
||||
#define LV_USE_BMP 0
|
||||
#define LV_USE_JPG 0
|
||||
#define LV_USE_GIF 0
|
||||
#define LV_USE_QRCODE 0
|
||||
#define LV_USE_BARCODE 0
|
||||
@@ -94,18 +134,16 @@
|
||||
#define LV_USE_GRIDNAV 0
|
||||
#define LV_USE_FRAGMENT 0
|
||||
|
||||
/* 裁剪未用控件,减小体积(menu/label/btn/img 等保留) */
|
||||
/* 裁剪未用控件,减小体积(本 UI 只用 lv_obj + 自绘,见文件头 F-2/F-3 说明)。
|
||||
* 注:LV_USE_COLORWHEEL / IMGBTN / METER / TEXTLINE 在 9.3 不存在(LVGL 8 残留),已删。 */
|
||||
#define LV_USE_ANIMIMG 0
|
||||
#define LV_USE_ARC 0
|
||||
#define LV_USE_CHART 0
|
||||
#define LV_USE_CHECKBOX 0
|
||||
#define LV_USE_COLORWHEEL 0
|
||||
#define LV_USE_DROPDOWN 0
|
||||
#define LV_USE_IMGBTN 0
|
||||
#define LV_USE_KEYBOARD 0
|
||||
#define LV_USE_LED 0
|
||||
#define LV_USE_LIST 0
|
||||
#define LV_USE_METER 0
|
||||
#define LV_USE_MSGBOX 0
|
||||
#define LV_USE_ROLLER 0
|
||||
#define LV_USE_SCALE 0
|
||||
@@ -121,6 +159,26 @@
|
||||
#define LV_USE_CALENDAR 0
|
||||
#define LV_USE_CANVAS 0
|
||||
#define LV_USE_IMAGE 0 /* 菜单未用图片控件;lv_menu 已关,无图标依赖 */
|
||||
#define LV_USE_TEXTLINE 0
|
||||
|
||||
/* 核心特性:此前全靠 lv_conf_internal 默认值隐式生效(默认 =1 的整份链进固件),
|
||||
* 现显式固化为 0,并与 simulator/lv_conf.h 对齐(check_lv_conf_sync.py 校验)。
|
||||
* 其中 LV_USE_OBSERVER 默认 1 → 固件此前一直白链 lv_observer.c。 */
|
||||
#define LV_USE_OBSERVER 0
|
||||
#define LV_USE_FLOAT 0
|
||||
#define LV_USE_MATRIX 0
|
||||
#define LV_USE_VECTOR_GRAPHIC 0
|
||||
#define LV_USE_OBJ_PROPERTY 0
|
||||
#define LV_USE_GESTURE_RECOGNITION 0
|
||||
#define LV_USE_COLOR_FILTER 0
|
||||
#define LV_USE_DRAW_PXP 0 /* NXP 并行像素引擎,STM32 无关 */
|
||||
#define LV_WIDGETS_HAS_DEFAULT_VALUE 0
|
||||
/* 以下两组是已关父项(LV_USE_LABEL / LV_USE_THEME_DEFAULT)的子选项,本身惰性,
|
||||
* 但显式写死可防"将来只在一端启用父项"时的静默漂移 */
|
||||
#define LV_LABEL_TEXT_SELECTION 0
|
||||
#define LV_LABEL_LONG_TXT_HINT 0
|
||||
#define LV_LABEL_WAIT_CHAR_COUNT 0
|
||||
#define LV_THEME_DEFAULT_DARK 0
|
||||
#define LV_THEME_DEFAULT_GROW 0
|
||||
#define LV_THEME_DEFAULT_TRANSITION_TIME 0
|
||||
|
||||
#endif /* LV_CONF_H */
|
||||
|
||||
@@ -78,7 +78,9 @@ static inline uint8_t /* LV_ATTRIBUTE_FAST_MEM */ get_bit(const uint8_t * buf, i
|
||||
|
||||
static inline void * /* LV_ATTRIBUTE_FAST_MEM */ drawbuf_next_row(const void * buf, uint32_t stride);
|
||||
|
||||
#if LV_DRAW_SW_SUPPORT_RGB565_SWAPPED /* 本地补丁:与定义处守卫一致,见文件末尾 */
|
||||
static inline lv_color16_t /* LV_ATTRIBUTE_FAST_MEM */ lv_color16_from_u16(uint16_t raw);
|
||||
#endif
|
||||
|
||||
/**********************
|
||||
* STATIC VARIABLES
|
||||
@@ -1271,6 +1273,12 @@ static inline uint8_t LV_ATTRIBUTE_FAST_MEM get_bit(const uint8_t * buf, int32_t
|
||||
return (buf[bit_idx / 8] >> (7 - (bit_idx % 8))) & 1;
|
||||
}
|
||||
|
||||
/* 本地补丁(2026-10-11,随 F-1 裁剪 LV_DRAW_SW_SUPPORT_* 引入):
|
||||
* 本 helper 的唯一使用点全部在 `#if LV_DRAW_SW_SUPPORT_RGB565_SWAPPED` 块内
|
||||
* (本文件 1107/1125/1146/1168/1190 行),但上游 9.3.0 漏了定义处的守卫 →
|
||||
* 关掉 RGB565_SWAPPED 后 ARMCC 报 #177-D "declared but never referenced"。
|
||||
* 补上与使用点一致的守卫;升级 LVGL 时若上游已修可去掉。 */
|
||||
#if LV_DRAW_SW_SUPPORT_RGB565_SWAPPED
|
||||
static inline lv_color16_t LV_ATTRIBUTE_FAST_MEM lv_color16_from_u16(uint16_t raw)
|
||||
{
|
||||
lv_color16_t c;
|
||||
@@ -1279,5 +1287,6 @@ static inline lv_color16_t LV_ATTRIBUTE_FAST_MEM lv_color16_from_u16(uint16_t ra
|
||||
c.blue = raw & 0x1F;
|
||||
return c;
|
||||
}
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
@@ -337,7 +337,7 @@
|
||||
<MiscControls>--diag_suppress=1,68,111,188,191,546,1293</MiscControls>
|
||||
<Define>USE_HAL_DRIVER,STM32F407xx,NDEBUG</Define>
|
||||
<Undefine />
|
||||
<IncludePath>../Inc;../Drivers/STM32F4xx_HAL_Driver/Inc;../Drivers/STM32F4xx_HAL_Driver/Inc/Legacy;../Drivers/CMSIS/Device/ST/STM32F4xx/Include;../Drivers/CMSIS/Include;../Drivers/BSP;../Drivers/BSP/CH395F;../Drivers/BSP/GD5F2GQ5UE;../Drivers/BSP/TPAFE5160;../Drivers/BSP/SD2506;../Drivers/BSP/RS485;../Drivers/BSP/NET;../Drivers/BSP/NET/lftpd;../Drivers/BSP/NETCFG;../App;../App/app;../App/algo;../App/cfg;../App/fs;../App/log;../App/sync;../App/task;../App/util;../Middlewares/Third_Party/FreeRTOS/Source/include;../Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2;../Middlewares/Third_Party/FreeRTOS/Source/portable/RVDS/ARM_CM4F;../Lib/dhara;../Lib/FatFs;../test/board;../Lib/lvgl;../Drivers/BSP/UI;../Lib</IncludePath>
|
||||
<IncludePath>../Inc;../Drivers/STM32F4xx_HAL_Driver/Inc;../Drivers/STM32F4xx_HAL_Driver/Inc/Legacy;../Drivers/CMSIS/Device/ST/STM32F4xx/Include;../Drivers/CMSIS/Include;../Drivers/BSP;../Drivers/BSP/CH395F;../Drivers/BSP/GD5F2GQ5UE;../Drivers/BSP/TPAFE5160;../Drivers/BSP/SD2506;../Drivers/BSP/RS485;../Drivers/BSP/NET;../Drivers/BSP/NET/lftpd;../Drivers/BSP/NETCFG;../App;../App/app;../App/algo;../App/cfg;../App/fs;../App/log;../App/sync;../App/task;../App/protocol/iec104;../App/util;../Middlewares/Third_Party/FreeRTOS/Source/include;../Middlewares/Third_Party/FreeRTOS/Source/CMSIS_RTOS_V2;../Middlewares/Third_Party/FreeRTOS/Source/portable/RVDS/ARM_CM4F;../Lib/dhara;../Lib/FatFs;../test/board;../Lib/lvgl;../Drivers/BSP/UI;../Lib</IncludePath>
|
||||
</VariousControls>
|
||||
</Cads>
|
||||
<Aads>
|
||||
@@ -887,6 +887,11 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\cfg\cfg_file_store_mcu.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>prot_set_store.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\cfg\prot_set_store.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>sys_log_mcu.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
@@ -907,6 +912,16 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\util\task_watchdog.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>task_prof.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\util\task_prof.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>sha256.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\util\sha256.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>vol_lock.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
@@ -937,11 +952,61 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\task\adc_task.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>iec104_serv.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\task\iec104_serv.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>protect_task.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\task\protect_task.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>iec104_asdu.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\protocol\iec104\iec104_asdu.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>iec104_pts.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\protocol\iec104\iec104_pts.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>iec104_link.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\protocol\iec104\iec104_link.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>adc_input.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\algo\adc_input.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>sample_ring.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\algo\sample_ring.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>phasor.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\algo\phasor.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>adc_health.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\algo\adc_health.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>prot_set.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\algo\prot_set.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>prot_elm.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\App\algo\prot_elm.c</FilePath>
|
||||
</File>
|
||||
</Files>
|
||||
</Group>
|
||||
<Group>
|
||||
@@ -1518,11 +1583,21 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\Drivers\BSP\NET\net_socket.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>net_listener.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\Drivers\BSP\NET\net_listener.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>net_cfg.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\Drivers\BSP\NETCFG\net_cfg.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>ftp_cfg.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\Drivers\BSP\NETCFG\ftp_cfg.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>nand_ftl.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
@@ -1533,6 +1608,11 @@
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\Drivers\BSP\NET\lftpd\lftpd.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>lftpd_auth.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
<FilePath>..\Drivers\BSP\NET\lftpd\lftpd_auth.c</FilePath>
|
||||
</File>
|
||||
<File>
|
||||
<FileName>lftpd_inet.c</FileName>
|
||||
<FileType>1</FileType>
|
||||
|
||||
@@ -31,7 +31,9 @@ def rd(sock):
|
||||
def raw_login(ip, port, timeout=10.0):
|
||||
s = socket.create_connection((ip, port), timeout=timeout)
|
||||
rd(s) # 220 banner
|
||||
s.sendall(b"USER anonymous\r\n"); rd(s)
|
||||
# N-19: anonymous login is rejected (530) -> use the factory default account
|
||||
s.sendall(b"USER admin\r\n"); rd(s)
|
||||
s.sendall(b"PASS admin\r\n"); rd(s)
|
||||
s.sendall(b"TYPE I\r\n"); rd(s)
|
||||
return s
|
||||
|
||||
@@ -50,7 +52,7 @@ def followup_ok(ip, port, size=4096):
|
||||
data = bytes(random.getrandbits(8) for _ in range(size))
|
||||
want = hashlib.sha256(data).hexdigest()
|
||||
try:
|
||||
f = FTP(); f.connect(ip, port, timeout=15.0); f.login("anonymous", "x")
|
||||
f = FTP(); f.connect(ip, port, timeout=15.0); f.login("admin", "admin")
|
||||
f.storbinary("STOR /ab_ok.bin", io.BytesIO(data))
|
||||
buf = bytearray(); f.retrbinary("RETR /ab_ok.bin", buf.extend)
|
||||
try:
|
||||
|
||||
@@ -75,11 +75,13 @@ main.c
|
||||
| testTask | 2048 words | BelowNormal | 硬件测试入口 |
|
||||
| uiTask | 4096 words | BelowNormal | LVGL UI 界面 |
|
||||
| timeSyncTask | 512 words | Normal | 墙钟时间同步 |
|
||||
| adcTask | 512 words | Normal | TPAFE5160 采样(1ms 节拍 + 原子快照) |
|
||||
| adcTask | 640 words | AboveNormal | TPAFE5160 采样(TIM3 1kHz 事件驱动 + 原子快照 + STM32 ADC DMA) |
|
||||
| iec104Task | 640 words | Normal | |
|
||||
| protectTask | 512 words | Normal | |
|
||||
<!-- TASK_TABLE:END -->
|
||||
|
||||
> 上表与 `Src/freertos.c` 的 `osThreadAttr_t` 一致(2026-09-25 校准);改动栈大小前先读
|
||||
> `docs/内存管理/RAM使用分析与优化方案.md` 的定栈规则与 `docs/待处理内容.md` 的内存预算项。
|
||||
> `docs/内存管理/RAM使用分析与优化方案.md` 的定栈规则与 `docs/任务规划/基线与待确认.md` 的内存预算项。
|
||||
|
||||
### 任务启动顺序管理
|
||||
|
||||
@@ -163,7 +165,7 @@ STM32F4-Base/
|
||||
│ └── time_sync_server.py # 时间同步推送服务
|
||||
├── docs/ # 项目文档(全量索引见 docs/README.md)
|
||||
│ ├── README.md # 文档索引:目录结构 + 推荐阅读顺序 + 全量清单
|
||||
│ ├── 待处理内容.md # 统一待办登记簿
|
||||
│ ├── 任务规划/ # 统一待办登记簿(README 排序权威 + 拆分文件)
|
||||
│ ├── 架构设计/ # 项目架构总览 / 存储架构 / 启动顺序 / 配图流程 / 引用关系图
|
||||
│ ├── 子系统设计/ # 配置 / 网络 / 墙钟 / 日志 / 运行自检记录 设计说明
|
||||
│ ├── 网络/ # BSD Socket API 使用指南 + 内部函数调用关系
|
||||
|
||||
+31
-15
@@ -9,7 +9,9 @@ ADC1.Channel-7\#ChannelRegularConversion=ADC_CHANNEL_6
|
||||
ADC1.Channel-8\#ChannelRegularConversion=ADC_CHANNEL_7
|
||||
ADC1.Channel-9\#ChannelRegularConversion=ADC_CHANNEL_14
|
||||
ADC1.ClockPrescaler=ADC_CLOCK_SYNC_PCLK_DIV4
|
||||
ADC1.IPParameters=Rank-1\#ChannelRegularConversion,master,Channel-1\#ChannelRegularConversion,SamplingTime-1\#ChannelRegularConversion,NbrOfConversionFlag,ScanConvMode,Rank-2\#ChannelRegularConversion,Channel-2\#ChannelRegularConversion,SamplingTime-2\#ChannelRegularConversion,Rank-3\#ChannelRegularConversion,Channel-3\#ChannelRegularConversion,SamplingTime-3\#ChannelRegularConversion,Rank-4\#ChannelRegularConversion,Channel-4\#ChannelRegularConversion,SamplingTime-4\#ChannelRegularConversion,Rank-5\#ChannelRegularConversion,Channel-5\#ChannelRegularConversion,SamplingTime-5\#ChannelRegularConversion,Rank-6\#ChannelRegularConversion,Channel-6\#ChannelRegularConversion,SamplingTime-6\#ChannelRegularConversion,Rank-7\#ChannelRegularConversion,Channel-7\#ChannelRegularConversion,SamplingTime-7\#ChannelRegularConversion,Rank-8\#ChannelRegularConversion,Channel-8\#ChannelRegularConversion,SamplingTime-8\#ChannelRegularConversion,Rank-9\#ChannelRegularConversion,Channel-9\#ChannelRegularConversion,SamplingTime-9\#ChannelRegularConversion,NbrOfConversion,ClockPrescaler
|
||||
ADC1.DMAContinuousRequests=ENABLE
|
||||
ADC1.ExternalTrigConv=ADC_EXTERNALTRIGCONV_T3_TRGO
|
||||
ADC1.IPParameters=Rank-1\#ChannelRegularConversion,master,Channel-1\#ChannelRegularConversion,SamplingTime-1\#ChannelRegularConversion,NbrOfConversionFlag,ScanConvMode,Rank-2\#ChannelRegularConversion,Channel-2\#ChannelRegularConversion,SamplingTime-2\#ChannelRegularConversion,Rank-3\#ChannelRegularConversion,Channel-3\#ChannelRegularConversion,SamplingTime-3\#ChannelRegularConversion,Rank-4\#ChannelRegularConversion,Channel-4\#ChannelRegularConversion,SamplingTime-4\#ChannelRegularConversion,Rank-5\#ChannelRegularConversion,Channel-5\#ChannelRegularConversion,SamplingTime-5\#ChannelRegularConversion,Rank-6\#ChannelRegularConversion,Channel-6\#ChannelRegularConversion,SamplingTime-6\#ChannelRegularConversion,Rank-7\#ChannelRegularConversion,Channel-7\#ChannelRegularConversion,SamplingTime-7\#ChannelRegularConversion,Rank-8\#ChannelRegularConversion,Channel-8\#ChannelRegularConversion,SamplingTime-8\#ChannelRegularConversion,Rank-9\#ChannelRegularConversion,Channel-9\#ChannelRegularConversion,SamplingTime-9\#ChannelRegularConversion,NbrOfConversion,ClockPrescaler,DMAContinuousRequests,ExternalTrigConv
|
||||
ADC1.NbrOfConversion=9
|
||||
ADC1.NbrOfConversionFlag=1
|
||||
ADC1.Rank-1\#ChannelRegularConversion=1
|
||||
@@ -35,13 +37,24 @@ ADC1.master=1
|
||||
CAD.formats=[]
|
||||
CAD.pinconfig=Dual
|
||||
CAD.provider=Component Search Engine
|
||||
Dma.ADC1.6.Direction=DMA_PERIPH_TO_MEMORY
|
||||
Dma.ADC1.6.FIFOMode=DMA_FIFOMODE_DISABLE
|
||||
Dma.ADC1.6.Instance=DMA2_Stream4
|
||||
Dma.ADC1.6.MemDataAlignment=DMA_MDATAALIGN_HALFWORD
|
||||
Dma.ADC1.6.MemInc=DMA_MINC_ENABLE
|
||||
Dma.ADC1.6.Mode=DMA_CIRCULAR
|
||||
Dma.ADC1.6.PeriphDataAlignment=DMA_PDATAALIGN_HALFWORD
|
||||
Dma.ADC1.6.PeriphInc=DMA_PINC_DISABLE
|
||||
Dma.ADC1.6.Priority=DMA_PRIORITY_LOW
|
||||
Dma.ADC1.6.RequestParameters=Instance,Direction,PeriphInc,MemInc,PeriphDataAlignment,MemDataAlignment,Mode,Priority,FIFOMode
|
||||
Dma.Request0=USART1_TX
|
||||
Dma.Request1=USART1_RX
|
||||
Dma.Request2=SPI2_RX
|
||||
Dma.Request3=SPI2_TX
|
||||
Dma.Request4=SPI1_RX
|
||||
Dma.Request5=SPI1_TX
|
||||
Dma.RequestsNb=6
|
||||
Dma.Request6=ADC1
|
||||
Dma.RequestsNb=7
|
||||
Dma.SPI1_RX.4.Direction=DMA_PERIPH_TO_MEMORY
|
||||
Dma.SPI1_RX.4.FIFOMode=DMA_FIFOMODE_DISABLE
|
||||
Dma.SPI1_RX.4.Instance=DMA2_Stream0
|
||||
@@ -109,7 +122,7 @@ FREERTOS.Queues01=netMsgQueue,8,net_msg_t,0,Dynamic,NULL,NULL
|
||||
FREERTOS.Tasks01=defaultTask,24,1024,StartDefaultTask,Default,NULL,Dynamic,NULL,NULL;netTask,24,960,StartNetTask,Default,NULL,Dynamic,NULL,NULL;ftpTask,24,1024,StartFtpTask,As weak,NULL,Dynamic,NULL,NULL;testTask,16,2048,start_test_task,As weak,NULL,Dynamic,NULL,NULL;uiTask,16,4096,StartUiTask,Default,NULL,Dynamic,NULL,NULL
|
||||
FREERTOS.configCHECK_FOR_STACK_OVERFLOW=2
|
||||
FREERTOS.configENABLE_FPU=1
|
||||
FREERTOS.configTOTAL_HEAP_SIZE=49152
|
||||
FREERTOS.configTOTAL_HEAP_SIZE=57344
|
||||
FREERTOS.configUSE_MALLOC_FAILED_HOOK=1
|
||||
File.Version=6
|
||||
GPIO.groupedBy=Group By Peripherals
|
||||
@@ -121,7 +134,7 @@ Mcu.CPN=STM32F407ZGT6
|
||||
Mcu.Family=STM32F4
|
||||
Mcu.IP0=ADC1
|
||||
Mcu.IP1=DMA
|
||||
Mcu.IP10=TIM6
|
||||
Mcu.IP10=TIM3
|
||||
Mcu.IP11=UART4
|
||||
Mcu.IP12=UART5
|
||||
Mcu.IP13=USART1
|
||||
@@ -204,7 +217,7 @@ Mcu.Pin65=PE1
|
||||
Mcu.Pin66=VP_FREERTOS_VS_CMSIS_V2
|
||||
Mcu.Pin67=VP_IWDG_VS_IWDG
|
||||
Mcu.Pin68=VP_SYS_VS_tim7
|
||||
Mcu.Pin69=VP_TIM6_VS_ClockSourceINT
|
||||
Mcu.Pin69=VP_TIM3_VS_ClockSourceINT
|
||||
Mcu.Pin7=PA4
|
||||
Mcu.Pin8=PA5
|
||||
Mcu.Pin9=PA6
|
||||
@@ -217,9 +230,10 @@ MxDb.Version=DB.6.0.181
|
||||
NVIC.BusFault_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false\:false
|
||||
NVIC.DMA1_Stream3_IRQn=true\:7\:0\:true\:false\:true\:true\:false\:true\:true
|
||||
NVIC.DMA1_Stream4_IRQn=true\:7\:0\:true\:false\:true\:true\:false\:true\:true
|
||||
NVIC.DMA2_Stream0_IRQn=true\:7\:0\:true\:false\:true\:true\:false\:true\:true
|
||||
NVIC.DMA2_Stream2_IRQn=true\:9\:0\:true\:false\:true\:true\:false\:true\:true
|
||||
NVIC.DMA2_Stream0_IRQn=true\:7\:0\:true\:false\:true\:false\:false\:true\:true
|
||||
NVIC.DMA2_Stream2_IRQn=true\:9\:0\:true\:false\:true\:false\:false\:true\:true
|
||||
NVIC.DMA2_Stream3_IRQn=true\:7\:0\:true\:false\:true\:true\:false\:true\:true
|
||||
NVIC.DMA2_Stream4_IRQn=true\:6\:0\:true\:false\:true\:false\:false\:true\:true
|
||||
NVIC.DMA2_Stream7_IRQn=true\:9\:0\:true\:false\:true\:true\:false\:true\:true
|
||||
NVIC.DebugMonitor_IRQn=true\:0\:0\:false\:false\:true\:false\:false\:false\:false
|
||||
NVIC.EXTI9_5_IRQn=true\:6\:0\:true\:false\:true\:true\:true\:true\:true
|
||||
@@ -236,7 +250,7 @@ NVIC.SavedPendsvIrqHandlerGenerated=true
|
||||
NVIC.SavedSvcallIrqHandlerGenerated=true
|
||||
NVIC.SavedSystickIrqHandlerGenerated=true
|
||||
NVIC.SysTick_IRQn=true\:15\:0\:false\:false\:false\:true\:false\:true\:false
|
||||
NVIC.TIM6_DAC_IRQn=true\:6\:0\:true\:false\:true\:true\:true\:true\:true
|
||||
NVIC.TIM3_IRQn=true\:6\:0\:true\:false\:true\:true\:true\:true\:true
|
||||
NVIC.TIM7_IRQn=true\:15\:0\:true\:false\:true\:false\:false\:true\:true
|
||||
NVIC.TimeBase=TIM7_IRQn
|
||||
NVIC.TimeBaseIP=TIM7
|
||||
@@ -604,7 +618,7 @@ ProjectManager.ToolChainLocation=
|
||||
ProjectManager.UAScriptAfterPath=
|
||||
ProjectManager.UAScriptBeforePath=
|
||||
ProjectManager.UnderRoot=false
|
||||
ProjectManager.functionlistsort=1-SystemClock_Config-RCC-false-HAL-false,2-MX_GPIO_Init-GPIO-false-HAL-true,3-MX_DMA_Init-DMA-false-HAL-true,4-MX_USART1_UART_Init-USART1-false-HAL-true,5-MX_SPI2_Init-SPI2-false-HAL-true,6-MX_SPI1_Init-SPI1-false-HAL-true,7-MX_I2C1_Init-I2C1-false-HAL-true,8-MX_UART4_Init-UART4-false-HAL-true,9-MX_UART5_Init-UART5-false-HAL-true,10-MX_USART2_UART_Init-USART2-false-HAL-true,11-MX_USART3_UART_Init-USART3-false-HAL-true,12-MX_IWDG_Init-IWDG-false-HAL-true,13-MX_ADC1_Init-ADC1-false-HAL-true
|
||||
ProjectManager.functionlistsort=1-SystemClock_Config-RCC-false-HAL-false,2-MX_GPIO_Init-GPIO-false-HAL-true,3-MX_DMA_Init-DMA-false-HAL-true,4-MX_USART1_UART_Init-USART1-false-HAL-true,5-MX_SPI2_Init-SPI2-false-HAL-true,6-MX_SPI1_Init-SPI1-false-HAL-true,7-MX_I2C1_Init-I2C1-false-HAL-true,8-MX_UART4_Init-UART4-false-HAL-true,9-MX_UART5_Init-UART5-false-HAL-true,10-MX_USART2_UART_Init-USART2-false-HAL-true,11-MX_USART3_UART_Init-USART3-false-HAL-true,12-MX_IWDG_Init-IWDG-false-HAL-true,13-MX_ADC1_Init-ADC1-false-HAL-true,14-MX_TIM3_Init-TIM3-false-HAL-true
|
||||
RCC.48MHZClocksFreq_Value=84000000
|
||||
RCC.AHBFreq_Value=168000000
|
||||
RCC.APB1CLKDivider=RCC_HCLK_DIV4
|
||||
@@ -670,10 +684,12 @@ SPI2.Direction=SPI_DIRECTION_2LINES
|
||||
SPI2.IPParameters=VirtualType,Mode,Direction,CalculateBaudRate,BaudRatePrescaler
|
||||
SPI2.Mode=SPI_MODE_MASTER
|
||||
SPI2.VirtualType=VM_MASTER
|
||||
TIM6.AutoReloadPreload=TIM_AUTORELOAD_PRELOAD_ENABLE
|
||||
TIM6.IPParameters=Prescaler,Period,AutoReloadPreload
|
||||
TIM6.Period=999
|
||||
TIM6.Prescaler=83
|
||||
TIM3.AutoReloadPreload=TIM_AUTORELOAD_PRELOAD_ENABLE
|
||||
TIM3.IPParameters=Prescaler,Period,AutoReloadPreload,TIM_MasterSlaveMode,TIM_MasterOutputTrigger
|
||||
TIM3.Period=999
|
||||
TIM3.Prescaler=83
|
||||
TIM3.TIM_MasterOutputTrigger=TIM_TRGO_UPDATE
|
||||
TIM3.TIM_MasterSlaveMode=TIM_MASTERSLAVEMODE_ENABLE
|
||||
UART4.IPParameters=VirtualMode
|
||||
UART4.VirtualMode=Asynchronous
|
||||
UART5.IPParameters=VirtualMode
|
||||
@@ -690,7 +706,7 @@ VP_IWDG_VS_IWDG.Mode=IWDG_Activate
|
||||
VP_IWDG_VS_IWDG.Signal=IWDG_VS_IWDG
|
||||
VP_SYS_VS_tim7.Mode=TIM7
|
||||
VP_SYS_VS_tim7.Signal=SYS_VS_tim7
|
||||
VP_TIM6_VS_ClockSourceINT.Mode=Enable_Timer
|
||||
VP_TIM6_VS_ClockSourceINT.Signal=TIM6_VS_ClockSourceINT
|
||||
VP_TIM3_VS_ClockSourceINT.Mode=Internal
|
||||
VP_TIM3_VS_ClockSourceINT.Signal=TIM3_VS_ClockSourceINT
|
||||
board=custom
|
||||
rtos.0.ip=FREERTOS
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
/* USER CODE END 0 */
|
||||
|
||||
ADC_HandleTypeDef hadc1;
|
||||
DMA_HandleTypeDef hdma_adc1;
|
||||
|
||||
/* ADC1 init function */
|
||||
void MX_ADC1_Init(void)
|
||||
@@ -48,11 +49,11 @@ void MX_ADC1_Init(void)
|
||||
hadc1.Init.ScanConvMode = ENABLE;
|
||||
hadc1.Init.ContinuousConvMode = DISABLE;
|
||||
hadc1.Init.DiscontinuousConvMode = DISABLE;
|
||||
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_NONE;
|
||||
hadc1.Init.ExternalTrigConv = ADC_SOFTWARE_START;
|
||||
hadc1.Init.ExternalTrigConvEdge = ADC_EXTERNALTRIGCONVEDGE_RISING;
|
||||
hadc1.Init.ExternalTrigConv = ADC_EXTERNALTRIGCONV_T3_TRGO;
|
||||
hadc1.Init.DataAlign = ADC_DATAALIGN_RIGHT;
|
||||
hadc1.Init.NbrOfConversion = 9;
|
||||
hadc1.Init.DMAContinuousRequests = DISABLE;
|
||||
hadc1.Init.DMAContinuousRequests = ENABLE;
|
||||
hadc1.Init.EOCSelection = ADC_EOC_SINGLE_CONV;
|
||||
if (HAL_ADC_Init(&hadc1) != HAL_OK)
|
||||
{
|
||||
@@ -141,7 +142,15 @@ void MX_ADC1_Init(void)
|
||||
Error_Handler();
|
||||
}
|
||||
/* USER CODE BEGIN ADC1_Init 2 */
|
||||
|
||||
/* STM32F4 ADC 上电**自校准**:RM0090 §13.3.6 / AN4976 要求的必需步骤,CubeMX 不生成,
|
||||
* 且本仓库的 HAL 是裁剪子集、**没有** HAL_ADCEx_Calibration_Start()(已核实
|
||||
* stm32f4xx_hal_adc_ex.c/.h 均无该函数)→ 用本文件 USER CODE BEGIN 1 里的等价实现。
|
||||
* 必须在首次转换之前完成,否则 ADC 存在失调误差(未校准的偏移可达数十 LSB)。
|
||||
* 放 USER CODE 区 = L2,重生成不丢;由 cubemx-survival 审计项 adc1-calibration 兜底。 */
|
||||
if (adc1_calibrate() != 0)
|
||||
{
|
||||
Error_Handler();
|
||||
}
|
||||
/* USER CODE END ADC1_Init 2 */
|
||||
|
||||
}
|
||||
@@ -182,6 +191,25 @@ void HAL_ADC_MspInit(ADC_HandleTypeDef* adcHandle)
|
||||
GPIO_InitStruct.Pull = GPIO_NOPULL;
|
||||
HAL_GPIO_Init(ADC17_UB_GPIO_Port, &GPIO_InitStruct);
|
||||
|
||||
/* ADC1 DMA Init */
|
||||
/* ADC1 Init */
|
||||
hdma_adc1.Instance = DMA2_Stream4;
|
||||
hdma_adc1.Init.Channel = DMA_CHANNEL_0;
|
||||
hdma_adc1.Init.Direction = DMA_PERIPH_TO_MEMORY;
|
||||
hdma_adc1.Init.PeriphInc = DMA_PINC_DISABLE;
|
||||
hdma_adc1.Init.MemInc = DMA_MINC_ENABLE;
|
||||
hdma_adc1.Init.PeriphDataAlignment = DMA_PDATAALIGN_HALFWORD;
|
||||
hdma_adc1.Init.MemDataAlignment = DMA_MDATAALIGN_HALFWORD;
|
||||
hdma_adc1.Init.Mode = DMA_CIRCULAR;
|
||||
hdma_adc1.Init.Priority = DMA_PRIORITY_LOW;
|
||||
hdma_adc1.Init.FIFOMode = DMA_FIFOMODE_DISABLE;
|
||||
if (HAL_DMA_Init(&hdma_adc1) != HAL_OK)
|
||||
{
|
||||
Error_Handler();
|
||||
}
|
||||
|
||||
__HAL_LINKDMA(adcHandle,DMA_Handle,hdma_adc1);
|
||||
|
||||
/* USER CODE BEGIN ADC1_MspInit 1 */
|
||||
|
||||
/* USER CODE END ADC1_MspInit 1 */
|
||||
@@ -215,6 +243,8 @@ void HAL_ADC_MspDeInit(ADC_HandleTypeDef* adcHandle)
|
||||
|
||||
HAL_GPIO_DeInit(ADC17_UB_GPIO_Port, ADC17_UB_Pin);
|
||||
|
||||
/* ADC1 DMA DeInit */
|
||||
HAL_DMA_DeInit(adcHandle->DMA_Handle);
|
||||
/* USER CODE BEGIN ADC1_MspDeInit 1 */
|
||||
|
||||
/* USER CODE END ADC1_MspDeInit 1 */
|
||||
@@ -222,6 +252,64 @@ void HAL_ADC_MspDeInit(ADC_HandleTypeDef* adcHandle)
|
||||
}
|
||||
|
||||
/* USER CODE BEGIN 1 */
|
||||
/* CR2.CAL(bit 3)。⚠ 本仓库的 CMSIS 设备头 stm32f407xx.h 是裁剪版,**未定义**
|
||||
* ADC_CR2_CAL(已核实:CR2 只有 ADON/CONT/DMA/DDS/EOCS/ALIGN/JEXTSEL/JEXTEN/
|
||||
* JSWSTART/EXTSEL/EXTEN/SWSTART,没有 CAL)
|
||||
* → 按 RM0090 在此补齐,命名避开 CMSIS 前缀以免将来重定义冲突。 */
|
||||
#define ADC1_CAL_BIT (0x1UL << 3U)
|
||||
/* 断电/校准等待上限(ms)。CAL 实测在数十 µs 内清零,10 ms 是 100 倍以上余量 */
|
||||
#define ADC1_CAL_TIMEOUT_MS (10U)
|
||||
/* ADON 0→1 后的稳定等待(µs),同 HAL 的 ADC_STAB_DELAY_US 口径 */
|
||||
#define ADC1_STAB_DELAY_US (20U)
|
||||
|
||||
/*
|
||||
* 函数功能:ADC1 上电自校准(等价于 HAL_ADCEx_Calibration_Start,本仓库 HAL 无此函数)
|
||||
* 入口参数:无(操作 ADC1 寄存器)
|
||||
* 返 回 值:0 - 校准完成;-1 - 等待超时(ADON 未清零 / CAL 未清零)
|
||||
* 限定条件:MX_ADC1_Init() 之后、**首次转换之前**;内核启动前可调用
|
||||
* (只用 HAL_GetTick 与寄存器,不触碰任何 CMSIS/FreeRTOS 对象)
|
||||
* 函数说明:RM0090 §13.3.6 时序——① ADC 必须处于关闭态(清 ADON 并等其生效);
|
||||
* ② 置 ADON 上电,等 Tstab;③ 置 CAL 启动自校准;④ 完成后**硬件自动**清 CAL,
|
||||
* 等其清零即完成。校准值存在 ADC 内部偏置寄存器,此后所有转换都带上校正。
|
||||
* 缺这一步的后果不是"读不到"而是"读得有偏"——正是 §7.2 那类
|
||||
* adc1_err/异常值的候选根因之一,见 docs/硬件平台/ADC通道映射说明.md。
|
||||
*/
|
||||
int adc1_calibrate(void)
|
||||
{
|
||||
uint32_t tickstart;
|
||||
|
||||
/* 1) 前置条件:ADC 必须关闭 */
|
||||
CLEAR_BIT(ADC1->CR2, ADC_CR2_ADON);
|
||||
tickstart = HAL_GetTick();
|
||||
while ((ADC1->CR2 & ADC_CR2_ADON) != 0U)
|
||||
{
|
||||
if ((HAL_GetTick() - tickstart) > ADC1_CAL_TIMEOUT_MS)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
/* 2) 上电(ADON 0→1),等待稳定后再启动校准 */
|
||||
SET_BIT(ADC1->CR2, ADC_CR2_ADON);
|
||||
{
|
||||
__IO uint32_t wait_loop_index = (ADC1_STAB_DELAY_US * (SystemCoreClock / 1000000U));
|
||||
while (wait_loop_index != 0U)
|
||||
{
|
||||
wait_loop_index--;
|
||||
}
|
||||
}
|
||||
|
||||
/* 3) 启动自校准,等硬件把 CAL 清零 */
|
||||
SET_BIT(ADC1->CR2, ADC1_CAL_BIT);
|
||||
tickstart = HAL_GetTick();
|
||||
while ((ADC1->CR2 & ADC1_CAL_BIT) != 0U)
|
||||
{
|
||||
if ((HAL_GetTick() - tickstart) > ADC1_CAL_TIMEOUT_MS)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
/* USER CODE END 1 */
|
||||
|
||||
|
||||
@@ -59,6 +59,9 @@ void MX_DMA_Init(void)
|
||||
/* DMA2_Stream3_IRQn interrupt configuration */
|
||||
HAL_NVIC_SetPriority(DMA2_Stream3_IRQn, 7, 0);
|
||||
HAL_NVIC_EnableIRQ(DMA2_Stream3_IRQn);
|
||||
/* DMA2_Stream4_IRQn interrupt configuration */
|
||||
HAL_NVIC_SetPriority(DMA2_Stream4_IRQn, 6, 0);
|
||||
HAL_NVIC_EnableIRQ(DMA2_Stream4_IRQn);
|
||||
/* DMA2_Stream7_IRQn interrupt configuration */
|
||||
HAL_NVIC_SetPriority(DMA2_Stream7_IRQn, 9, 0);
|
||||
HAL_NVIC_EnableIRQ(DMA2_Stream7_IRQn);
|
||||
|
||||
+50
-6
@@ -39,6 +39,8 @@
|
||||
#include "sys_events.h"
|
||||
#include "default_task.h"
|
||||
#include "adc_task.h" /* B1(2026-10-10):ADC 采集消费者(手写任务,见 Variables 区) */
|
||||
#include "iec104_serv.h" /* B4(2026-10-12):IEC 104 上行服务(手写任务,见 Variables 区) */
|
||||
#include "protect_task.h" /* B5(2026-10-12):保护判据节拍任务(手写任务,同上) */
|
||||
/* USER CODE END Includes */
|
||||
|
||||
/* Private typedef -----------------------------------------------------------*/
|
||||
@@ -113,10 +115,19 @@ static int hook_ptr_ok(const void *p)
|
||||
* 靠审计 WARN 提示),建监听 → net_bind → CH395F → 日志尾。2026-10-10 整改后
|
||||
* 静态最坏链 1024 B(整改前 1736 B)、实测峰值 616 B;⚠ 余量 1024 B **正好**
|
||||
* 等于 ISR 底线 → 本任务链上再加任何调用(或新日志行)都必须先抬栈。
|
||||
* adcTask 512 words:本区下方手工定义(同上,不在 `.ioc` 里)。B1(2026-10-10)
|
||||
* ADC 采集消费者:链上仅 tpafe5160_start_conv_irq + tpafe5160_snapshot(临界区
|
||||
* 原子拷贝)+ task_wd_beat + 周期 DBG_INFO(每 10s 一条)。栈值以
|
||||
* `scripts/stack_report.py` 实测最坏链为准,改动前后各跑一次。
|
||||
* adcTask 640 words:本区下方手工定义(同上,不在 `.ioc` 里)。B1(2026-10-10)
|
||||
* ADC 采集消费者;L2 相量(2026-10-11)+ L1-6 有效性校验(同日)后链上为
|
||||
* adc_input_poll + sample_ring/phasor + adc_health_report(DBG_ERROR +
|
||||
* syslog_post 回显尾)。静态最坏链 1080 B,512 words 时余 968 B < 1024 B
|
||||
* ISR 底线 → 抬至 640 words(2.5 KB,堆余量 min_ever 7152→≈6640 B,可承受)。
|
||||
* 栈值以 `scripts/stack_report.py` 实测最坏链为准,改动前后各跑一次。
|
||||
* iec104Task 640 words:B4(2026-10-12)104 上行服务(2404 监听 + link 状态
|
||||
* 机)。link/pts 缓冲全静态;stack_report 实测静态最坏链 1456 B(拆链
|
||||
* 重建监听 → net_bind → SPI 故障 → 日志尾,日志主导)。512 words 时余
|
||||
* 592 B < 1024 B ISR 底线 → 抬至 640(2.5 KB)。
|
||||
* protectTask 512 words:B5(2026-10-12)保护判据节拍(prot_elm_run 消费者
|
||||
* + L5-4 采样闭锁)。stack_report 实测静态最坏链 872 B(float 判据 +
|
||||
* phasor 快照 + 日志尾),余 1176 B / 57% ≥ ISR 底线 → 512 保持。
|
||||
*/
|
||||
/* 存储测试变量(g_storage_stats / s_perf_buf)已迁至 test/board/storage_test_task.c */
|
||||
osThreadId_t timeSyncTaskHandle;
|
||||
@@ -125,11 +136,38 @@ const osThreadAttr_t timeSyncTask_attributes = {
|
||||
.stack_size = 512 * 4,
|
||||
.priority = (osPriority_t) osPriorityNormal,
|
||||
};
|
||||
/* B1(2026-10-10):ADC 采集消费者(1ms 采样节拍 + 原子快照 + 帧统计)。
|
||||
* 手写于此而非 .ioc(同 timeSyncTask:不在 Tasks01 里 → 重生成会丢,靠审计 WARN 提示)。 */
|
||||
/* B1(2026-10-10):ADC 采集消费者(TIM6 1 kHz 采样节拍 + 原子快照 + 帧统计)。
|
||||
* 手写于此而非 .ioc(同 timeSyncTask:不在 Tasks01 里 → 重生成会丢,靠审计 WARN 提示)。
|
||||
* ⚠ 优先级 AboveNormal(Task #32 P1-5 方案 a,2026-10-10):消费改由 TIM6 ISR 事件驱动后
|
||||
* 提优先级,使"被唤醒 → 立即抢占其余 Normal 任务",唤醒延迟固定为 µs 级。
|
||||
* 代价:本任务单拍最坏实测 adcm≈2.1 ms 会在 AboveNormal 上挤占 net/ftp/default/timeSync,
|
||||
* 其中 ADC 的 2 ms 忙等(App/algo/adc_input.c)尤其敏感 → 硬化方向见
|
||||
* docs/子系统设计/ADC采样节拍设计说明.md §10.4。 */
|
||||
osThreadId_t adcTaskHandle;
|
||||
const osThreadAttr_t adcTask_attributes = {
|
||||
.name = "adcTask",
|
||||
.stack_size = 640 * 4,
|
||||
.priority = (osPriority_t) osPriorityAboveNormal,
|
||||
};
|
||||
/* B4(2026-10-12):IEC 60870-5-104 被控站服务(2404 监听 + 链路状态机消费)。
|
||||
* 手写于此而非 .ioc(同 timeSyncTask/adcTask:不在 Tasks01 里 → 重生成会丢,
|
||||
* 靠审计 WARN 提示)。640 words:stack_report 实测静态最坏链 1456 B(拆链→
|
||||
* 重建监听→net_bind→SPI 故障→日志尾,日志主导);512 words 时余 592 B
|
||||
* < 1024 B ISR 底线 → 抬至 640(2.5 KB,CCM 堆 min_ever 再降 512 B,可承受)。 */
|
||||
osThreadId_t iec104TaskHandle;
|
||||
const osThreadAttr_t iec104Task_attributes = {
|
||||
.name = "iec104Task",
|
||||
.stack_size = 640 * 4,
|
||||
.priority = (osPriority_t) osPriorityNormal,
|
||||
};
|
||||
/* B5(2026-10-12):保护判据节拍(prot_elm_run 消费者 + L5-4 采样闭锁)。
|
||||
* 手写于此而非 .ioc(同上)。512 words:stack_report 实测静态最坏链 872 B
|
||||
* (float 判据 + phasor 快照 + 日志尾),余 1176 B / 57% ≥ ISR 底线 → 保持。
|
||||
* 优先级 Normal:V0.1 无出口硬件(规划 R7),判据是 20ms 软实时,不需要
|
||||
* adcTask 的 AboveNormal;规划 §4 的"1ms/高"随 P2 瞬时值元件再评。 */
|
||||
osThreadId_t protectTaskHandle;
|
||||
const osThreadAttr_t protectTask_attributes = {
|
||||
.name = "protectTask",
|
||||
.stack_size = 512 * 4,
|
||||
.priority = (osPriority_t) osPriorityNormal,
|
||||
};
|
||||
@@ -313,6 +351,12 @@ void MX_FREERTOS_Init(void) {
|
||||
* (内核启动前)完成;本任务只做运行期采样。 */
|
||||
adcTaskHandle = osThreadNew(adc_task_func, NULL, &adcTask_attributes);
|
||||
|
||||
/* B4(2026-10-12):IEC 60870-5-104 上行服务任务。任务体内先等 NET_READY
|
||||
* 屏障(100ms 分片 + 每片喂心跳,timeSyncTask 先例),再建 2404 监听并
|
||||
* 100ms 循环服务;纳入 IWDG required_mask(TASK_WD_IEC104,任何构建都创建)。 */
|
||||
iec104TaskHandle = osThreadNew(iec104_serv_task, NULL, &iec104Task_attributes);
|
||||
protectTaskHandle = osThreadNew(protect_task, NULL, &protectTask_attributes);
|
||||
|
||||
/* P2-13(2026-10-04):产品构建(未选任何 TEST_SUITE_*)不需要 testTask。
|
||||
* 上方生成区按 .ioc FREERTOS.Tasks01 无条件创建它,其 2048 words(8KB) 栈 + TCB
|
||||
* 取自 CCM 堆;而产品构建下 start_test_task() 仅打印一行后 osDelay(1000) 永久空转,
|
||||
|
||||
+40
-10
@@ -42,6 +42,7 @@
|
||||
#include "app_main.h"
|
||||
#include "sys_clock.h"
|
||||
#include "task_watchdog.h" /* task_wd_tick_probe():round 8b 时间源存活性取证 */
|
||||
#include "adc_task.h" /* adc_tick_irq_notify():TIM6 节拍 → 唤醒 adcTask(P1-5 方案 a) */
|
||||
/* USER CODE END Includes */
|
||||
|
||||
/* Private typedef -----------------------------------------------------------*/
|
||||
@@ -56,6 +57,11 @@
|
||||
|
||||
/* Private macro -------------------------------------------------------------*/
|
||||
/* USER CODE BEGIN PM */
|
||||
/* N-20(2026-10-12):本文件唯一的 DBG_* 调用点是 Error_Handler;其帧经
|
||||
* MX_*_Init 调用链叠在 MSP 上,而 MSP 仅 1KB(startup Stack_Size 0x400)
|
||||
* → 行缓冲收紧到 128 B(本文件最长行 = Error_Handler 行 ≈78 B < 128,不截断)。
|
||||
* 日志成本口径见 dbg_log.h 的 DBG_LINE_BUF_MAX 注释。 */
|
||||
#define DBG_LINE_BUF_MAX 128U
|
||||
#define DBG_TAG "[MAIN]"
|
||||
#include "dbg_log.h"
|
||||
/* USER CODE END PM */
|
||||
@@ -132,7 +138,7 @@ int main(void)
|
||||
MX_USART3_UART_Init();
|
||||
MX_IWDG_Init();
|
||||
MX_ADC1_Init();
|
||||
MX_TIM6_Init();
|
||||
MX_TIM3_Init();
|
||||
/* USER CODE BEGIN 2 */
|
||||
HAL_Delay(100);
|
||||
|
||||
@@ -246,13 +252,22 @@ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
|
||||
task_wd_tick_probe();
|
||||
}
|
||||
|
||||
/* 1 kHz 采样节拍:TIM6 更新中断内脉冲 CONVST(仅两次 GPIO 写,亚 µs)。
|
||||
* TIM6 与 BUSY EXTI(EXTI9_5_IRQn) 同为抢占优先级 6 → 互不抢占,
|
||||
/* 1 kHz 采样节拍:TIM3 更新事件内脉冲 CONVST(仅两次 GPIO 写,亚 µs),
|
||||
* 再唤醒 adcTask 消费这一拍(Task #32 P1-5 方案 a:ISR 只发唤醒,计算全留在任务)。
|
||||
* 同一个更新事件还经 TRGO 硬件触发 STM32 ADC1 一轮 9 通道转换(DMA 自动搬运)
|
||||
* → 两片转换器由同一硬件事件驱动,采样时刻对齐。
|
||||
* TIM3 与 BUSY EXTI(EXTI9_5_IRQn) 同为抢占优先级 6 → 互不抢占,
|
||||
* 读 RD 时序不会被下一次 CONVST 脉冲打断。详见
|
||||
* docs/子系统设计/ADC采样节拍设计说明.md。放 USER CODE 区(L2)。 */
|
||||
if (htim->Instance == TIM6)
|
||||
* docs/子系统设计/ADC采样节拍设计说明.md §10。放 USER CODE 区(L2)。
|
||||
* ⚠ 节拍源 v2.0 由 TIM6 改为 TIM3:F4 上 ADC1 的外部触发清单里没有 TIM6 TRGO
|
||||
* (TIM6/TIM7 的 TRGO 只接 DAC),详见 App/app/app_main.c 步骤 6 注释。
|
||||
* ⚠ 唤醒必须走 adc_tick_irq_notify(),**不要**在这里直接 osThreadFlagsSet:
|
||||
* TIM3 在 app_main_init() 就启动,远早于建任务与 osKernelStart,
|
||||
* "消费者就绪"门在 adc_task.c 里(同本文件 TIM7 分支 2026-10-02 的实测坑)。 */
|
||||
if (htim->Instance == TIM3)
|
||||
{
|
||||
tpafe5160_start_conv_irq();
|
||||
adc_tick_irq_notify();
|
||||
}
|
||||
/* USER CODE END Callback 1 */
|
||||
}
|
||||
@@ -264,11 +279,26 @@ void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
|
||||
void Error_Handler(void)
|
||||
{
|
||||
/* USER CODE BEGIN Error_Handler_Debug */
|
||||
/* User can add his own implementation to report the HAL error return state */
|
||||
__disable_irq();
|
||||
while (1)
|
||||
{
|
||||
}
|
||||
/* N-20 统一降级(2026-10-12):旧形态 __disable_irq()+while(1) = 单处外设初始化
|
||||
* 失败即整机挂死,只剩 IWDG 兜底且无任何自检日志。现:打 1 条自检日志 +
|
||||
* NVIC_SystemReset()(与 4 个 Fault 处理同款自恢复;IWDG 是最后兜底而非唯一
|
||||
* 出路;复位归因 SFTRSTF → 开机日志 "software (NVIC_SystemReset)")。
|
||||
* ⚠ 不得 __disable_irq():日志必须能走串口。
|
||||
* 调用方识别(为何不打地址):本函数无参数、调用点全在 CubeMX 生成区(不可加标签);
|
||||
* ⚠ __builtin_return_address(0) **不可用**——AC5 前端接受但编成库函数调用,
|
||||
* MicroLIB 无该符号 → 链接错 L6218E(2026-10-12 台架实证,勿再尝试);
|
||||
* ARMCC __asm 只有整函数形态且取不到 C 符号、带不动 C 日志(看门狗设计说明 §10.4)。
|
||||
* → 子系统定位靠启动日志锚点:失败点在"最后一条成功日志"与 main() 的
|
||||
* MX_*_Init 顺序(GPIO→DMA→USART1→SPI2→SPI1→I2C1→UART4→UART5→USART2→
|
||||
* USART3→IWDG→ADC1→TIM3→app_main_init)之间。
|
||||
* 日志路径内核前安全:调用点全部在 osKernelStart 之前,控制台写者未注册
|
||||
* → con_append 走 dbg_console_write_raw(DWT/计数有界轮询,huart1 未就绪
|
||||
* 即返回),sys_clock_get_str 未初始化时打占位串——全程无内核 API。
|
||||
* 日志成本:仅这 1 条(≈90 B Flash;行缓冲已在本文件 PM 区收到 128 B)。
|
||||
* 重生成存活:函数体在 USER CODE Error_Handler_Debug 区(L2,KeepUserCode
|
||||
* 逐字保留);另登记 check_cubemx_survival.py 审计项兜底(L3)。 */
|
||||
DBG_ERROR("boot init failure, resetting (locate MX_*_Init via boot order)");
|
||||
NVIC_SystemReset();
|
||||
/* USER CODE END Error_Handler_Debug */
|
||||
}
|
||||
#ifdef USE_FULL_ASSERT
|
||||
|
||||
+30
-15
@@ -238,13 +238,14 @@ __asm void UsageFault_Handler(void)
|
||||
/* USER CODE END 0 */
|
||||
|
||||
/* External variables --------------------------------------------------------*/
|
||||
extern DMA_HandleTypeDef hdma_adc1;
|
||||
extern DMA_HandleTypeDef hdma_spi1_rx;
|
||||
extern DMA_HandleTypeDef hdma_spi1_tx;
|
||||
extern DMA_HandleTypeDef hdma_spi2_rx;
|
||||
extern DMA_HandleTypeDef hdma_spi2_tx;
|
||||
extern SPI_HandleTypeDef hspi1;
|
||||
extern SPI_HandleTypeDef hspi2;
|
||||
extern TIM_HandleTypeDef htim6;
|
||||
extern TIM_HandleTypeDef htim3;
|
||||
extern DMA_HandleTypeDef hdma_usart1_tx;
|
||||
extern DMA_HandleTypeDef hdma_usart1_rx;
|
||||
extern UART_HandleTypeDef huart4;
|
||||
@@ -398,6 +399,20 @@ void EXTI9_5_IRQHandler(void)
|
||||
/* USER CODE END EXTI9_5_IRQn 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief This function handles TIM3 global interrupt.
|
||||
*/
|
||||
void TIM3_IRQHandler(void)
|
||||
{
|
||||
/* USER CODE BEGIN TIM3_IRQn 0 */
|
||||
|
||||
/* USER CODE END TIM3_IRQn 0 */
|
||||
HAL_TIM_IRQHandler(&htim3);
|
||||
/* USER CODE BEGIN TIM3_IRQn 1 */
|
||||
|
||||
/* USER CODE END TIM3_IRQn 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief This function handles SPI1 global interrupt.
|
||||
*/
|
||||
@@ -496,20 +511,6 @@ void UART5_IRQHandler(void)
|
||||
/* USER CODE END UART5_IRQn 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief This function handles TIM6 global interrupt, DAC1 and DAC2 underrun error interrupts.
|
||||
*/
|
||||
void TIM6_DAC_IRQHandler(void)
|
||||
{
|
||||
/* USER CODE BEGIN TIM6_DAC_IRQn 0 */
|
||||
|
||||
/* USER CODE END TIM6_DAC_IRQn 0 */
|
||||
HAL_TIM_IRQHandler(&htim6);
|
||||
/* USER CODE BEGIN TIM6_DAC_IRQn 1 */
|
||||
|
||||
/* USER CODE END TIM6_DAC_IRQn 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief This function handles TIM7 global interrupt.
|
||||
*/
|
||||
@@ -566,6 +567,20 @@ void DMA2_Stream3_IRQHandler(void)
|
||||
/* USER CODE END DMA2_Stream3_IRQn 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief This function handles DMA2 stream4 global interrupt.
|
||||
*/
|
||||
void DMA2_Stream4_IRQHandler(void)
|
||||
{
|
||||
/* USER CODE BEGIN DMA2_Stream4_IRQn 0 */
|
||||
|
||||
/* USER CODE END DMA2_Stream4_IRQn 0 */
|
||||
HAL_DMA_IRQHandler(&hdma_adc1);
|
||||
/* USER CODE BEGIN DMA2_Stream4_IRQn 1 */
|
||||
|
||||
/* USER CODE END DMA2_Stream4_IRQn 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief This function handles DMA2 stream7 global interrupt.
|
||||
*/
|
||||
|
||||
@@ -24,78 +24,85 @@
|
||||
|
||||
/* USER CODE END 0 */
|
||||
|
||||
TIM_HandleTypeDef htim6;
|
||||
TIM_HandleTypeDef htim3;
|
||||
|
||||
/* TIM6 init function */
|
||||
void MX_TIM6_Init(void)
|
||||
/* TIM3 init function */
|
||||
void MX_TIM3_Init(void)
|
||||
{
|
||||
|
||||
/* USER CODE BEGIN TIM6_Init 0 */
|
||||
/* USER CODE BEGIN TIM3_Init 0 */
|
||||
|
||||
/* USER CODE END TIM6_Init 0 */
|
||||
/* USER CODE END TIM3_Init 0 */
|
||||
|
||||
TIM_ClockConfigTypeDef sClockSourceConfig = {0};
|
||||
TIM_MasterConfigTypeDef sMasterConfig = {0};
|
||||
|
||||
/* USER CODE BEGIN TIM6_Init 1 */
|
||||
/* USER CODE BEGIN TIM3_Init 1 */
|
||||
|
||||
/* USER CODE END TIM6_Init 1 */
|
||||
htim6.Instance = TIM6;
|
||||
htim6.Init.Prescaler = 83;
|
||||
htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
|
||||
htim6.Init.Period = 999;
|
||||
htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
|
||||
if (HAL_TIM_Base_Init(&htim6) != HAL_OK)
|
||||
/* USER CODE END TIM3_Init 1 */
|
||||
htim3.Instance = TIM3;
|
||||
htim3.Init.Prescaler = 83;
|
||||
htim3.Init.CounterMode = TIM_COUNTERMODE_UP;
|
||||
htim3.Init.Period = 999;
|
||||
htim3.Init.ClockDivision = TIM_CLOCKDIVISION_DIV1;
|
||||
htim3.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_ENABLE;
|
||||
if (HAL_TIM_Base_Init(&htim3) != HAL_OK)
|
||||
{
|
||||
Error_Handler();
|
||||
}
|
||||
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
|
||||
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
|
||||
if (HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig) != HAL_OK)
|
||||
sClockSourceConfig.ClockSource = TIM_CLOCKSOURCE_INTERNAL;
|
||||
if (HAL_TIM_ConfigClockSource(&htim3, &sClockSourceConfig) != HAL_OK)
|
||||
{
|
||||
Error_Handler();
|
||||
}
|
||||
/* USER CODE BEGIN TIM6_Init 2 */
|
||||
sMasterConfig.MasterOutputTrigger = TIM_TRGO_UPDATE;
|
||||
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_ENABLE;
|
||||
if (HAL_TIMEx_MasterConfigSynchronization(&htim3, &sMasterConfig) != HAL_OK)
|
||||
{
|
||||
Error_Handler();
|
||||
}
|
||||
/* USER CODE BEGIN TIM3_Init 2 */
|
||||
|
||||
/* USER CODE END TIM6_Init 2 */
|
||||
/* USER CODE END TIM3_Init 2 */
|
||||
|
||||
}
|
||||
|
||||
void HAL_TIM_Base_MspInit(TIM_HandleTypeDef* tim_baseHandle)
|
||||
{
|
||||
|
||||
if(tim_baseHandle->Instance==TIM6)
|
||||
if(tim_baseHandle->Instance==TIM3)
|
||||
{
|
||||
/* USER CODE BEGIN TIM6_MspInit 0 */
|
||||
/* USER CODE BEGIN TIM3_MspInit 0 */
|
||||
|
||||
/* USER CODE END TIM6_MspInit 0 */
|
||||
/* TIM6 clock enable */
|
||||
__HAL_RCC_TIM6_CLK_ENABLE();
|
||||
/* USER CODE END TIM3_MspInit 0 */
|
||||
/* TIM3 clock enable */
|
||||
__HAL_RCC_TIM3_CLK_ENABLE();
|
||||
|
||||
/* TIM6 interrupt Init */
|
||||
HAL_NVIC_SetPriority(TIM6_DAC_IRQn, 6, 0);
|
||||
HAL_NVIC_EnableIRQ(TIM6_DAC_IRQn);
|
||||
/* USER CODE BEGIN TIM6_MspInit 1 */
|
||||
/* TIM3 interrupt Init */
|
||||
HAL_NVIC_SetPriority(TIM3_IRQn, 6, 0);
|
||||
HAL_NVIC_EnableIRQ(TIM3_IRQn);
|
||||
/* USER CODE BEGIN TIM3_MspInit 1 */
|
||||
|
||||
/* USER CODE END TIM6_MspInit 1 */
|
||||
/* USER CODE END TIM3_MspInit 1 */
|
||||
}
|
||||
}
|
||||
|
||||
void HAL_TIM_Base_MspDeInit(TIM_HandleTypeDef* tim_baseHandle)
|
||||
{
|
||||
|
||||
if(tim_baseHandle->Instance==TIM6)
|
||||
if(tim_baseHandle->Instance==TIM3)
|
||||
{
|
||||
/* USER CODE BEGIN TIM6_MspDeInit 0 */
|
||||
/* USER CODE BEGIN TIM3_MspDeInit 0 */
|
||||
|
||||
/* USER CODE END TIM6_MspDeInit 0 */
|
||||
/* USER CODE END TIM3_MspDeInit 0 */
|
||||
/* Peripheral clock disable */
|
||||
__HAL_RCC_TIM6_CLK_DISABLE();
|
||||
__HAL_RCC_TIM3_CLK_DISABLE();
|
||||
|
||||
/* TIM6 interrupt Deinit */
|
||||
HAL_NVIC_DisableIRQ(TIM6_DAC_IRQn);
|
||||
/* USER CODE BEGIN TIM6_MspDeInit 1 */
|
||||
/* TIM3 interrupt Deinit */
|
||||
HAL_NVIC_DisableIRQ(TIM3_IRQn);
|
||||
/* USER CODE BEGIN TIM3_MspDeInit 1 */
|
||||
|
||||
/* USER CODE END TIM6_MspDeInit 1 */
|
||||
/* USER CODE END TIM3_MspDeInit 1 */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+22
-12
@@ -11,7 +11,7 @@
|
||||
```
|
||||
docs/
|
||||
├── README.md 本索引(阅读顺序 + 全量清单)
|
||||
├── 待处理内容.md 统一待办登记簿(高频入口,保留在顶层)
|
||||
├── 任务规划/ 统一待办登记簿(README 排序权威 + 6 个拆分文件)
|
||||
├── 产品需求/ 产品需求设计文档(面向产品的需求基线)
|
||||
├── 架构设计/ 全局架构与设计方法论
|
||||
├── 子系统设计/ 各功能子系统的设计说明
|
||||
@@ -53,7 +53,7 @@ docs/
|
||||
- **应用/驱动开发** → §4 架构设计 → §5 子系统设计 → §7 问题记录(**改驱动前必读对应 Trap Records**)
|
||||
- **测试/验证** → §6 测试说明 → §9.2 环境与部署
|
||||
- **UI 开发** → §8 UI 设计 → §6 测试说明(串口助手、UI 自动化测试流程)
|
||||
- **接手/评审** → §9.1 评审报告 → §9.3 待处理内容(统一待办登记簿)
|
||||
- **接手/评审** → §9.1 评审报告 → §9.3 任务规划(统一待办登记簿,见 `任务规划/README.md`)
|
||||
|
||||
---
|
||||
|
||||
@@ -88,8 +88,9 @@ docs/
|
||||
| [`系统墙钟时间维护说明.md`](子系统设计/系统墙钟时间维护说明.md) | `sys_clock`:RTC 基准、tick 回绕防护、30 天惰性再基准化 |
|
||||
| [`控制台单写者日志设计说明.md`](子系统设计/控制台单写者日志设计说明.md) | 控制台日志单写者模型(避免多任务交错输出) |
|
||||
| [`运行与自检记录设计说明.md`](子系统设计/运行与自检记录设计说明.md) | `/log/run.log`·`selftest.log` 定长记录与待机显示 |
|
||||
| [`看门狗设计说明.md`](子系统设计/看门狗设计说明.md) | IWDG(CubeMX 配置)+ 任务心跳位图:选型与超时推导、掩码与埋点、取证机制、验证记录;已台架闭环(V2/V4/V5) |
|
||||
| [`ADC采样节拍设计说明.md`](子系统设计/ADC采样节拍设计说明.md) | **ADC 采集链路采样节拍**:现状(任务周期软触发)→ 目标(TIM6 更新中断触发 CONVST)的方案选型、CubeMX 配置规格、代码改造清单、时序/优先级分析与验收判据;**规划中(未实现)** |
|
||||
| [`看门狗设计说明.md`](子系统设计/看门狗设计说明.md) | IWDG(CubeMX 配置)+ 任务心跳位图:选型与超时推导、掩码与埋点、取证机制、验证记录;已台架闭环(V2/V4/V5)。**§11 = 任务级饥饿的诊断方法与 `[PROF]` 实测基线**(Task #32 沉淀:判读口径、量化归因、三条脆弱点、明确不做的四条) |
|
||||
| [`ADC采样节拍设计说明.md`](子系统设计/ADC采样节拍设计说明.md) | **ADC 采集链路采样节拍**:TIM3 1 kHz 更新事件同时触发 TPAFE CONVST 与(经 TRGO)STM32 ADC1 + DMA 乒乓缓冲;方案选型、CubeMX 配置规格、代码改造清单、时序/优先级分析、验收判据与实测剖面;**已落地并上板闭环** |
|
||||
| [`IEC104上行设计说明.md`](子系统设计/IEC104上行设计说明.md) | **B4 上行规约**:IEC 60870-5-104 被控站(TCP 2404)范围/点表(V0.1 有效子集)/链路流程(k/w/t1~t3、总召、COV、遥控两段式、定值)/netTask 接入决策/预算与测试方案;协议核心纯逻辑 + L0.5 全栈单测 |
|
||||
| [`网络/BSD_Socket_API_使用指南.md`](网络/BSD_Socket_API_使用指南.md) | **上层视图**:BSD Socket API 公开语义、示例、常见问题 |
|
||||
| [`网络/BSD_Socket_API_函数调用关系.md`](网络/BSD_Socket_API_函数调用关系.md) | **内部视图**:netTask / 消息队列 / L3 挂起 / 重试对账的调用关系与设计意图 |
|
||||
| [`网络/Net层阻塞模型架构设计.md`](网络/Net层阻塞模型架构设计.md) | **等待模型**:pending 请求架构、五路完成语义、设计边界与性能特征 |
|
||||
@@ -124,7 +125,11 @@ docs/
|
||||
|---|---|
|
||||
| [`看门狗单元测试说明.md`](测试说明/看门狗单元测试说明.md) | Task Watchdog 判定状态机(位图 / 掩码 / 节流 / tick 回绕),39 用例 |
|
||||
| [`系统时钟单元测试说明.md`](测试说明/系统时钟单元测试说明.md) | sys_clock 时间换算 / 星期 Zeller / 30 天惰性再基准化 / RTC 故障降级,100 断言点 |
|
||||
| [`配置文件单元测试说明.md`](测试说明/配置文件单元测试说明.md) | cfg_file INI 解析 / 值编解码 / load-save-poll 契约 / 超长守卫,139 断言点(含抓出的外科手术保存死码 P0) |
|
||||
| [`配置文件单元测试说明.md`](测试说明/配置文件单元测试说明.md) | cfg_file INI 解析 / 值编解码 / load-save-poll 契约 / 超长守卫,149 断言点(含抓出的外科手术保存死码 P0) |
|
||||
| [`ADC输入层单元测试说明.md`](测试说明/ADC输入层单元测试说明.md) | adc_input 语义通道映射 / ADC1 顺序扫描(**Rank↔通道判据**)/ poll 契约 / HAL 故障路径,70 用例 |
|
||||
| [`相量层单元测试说明.md`](测试说明/相量层单元测试说明.md) | phasor 全周傅氏系数表 / 幅值·相位 / 谐波抑制与提取 / 频偏 / 采样环 / 双区交换,57 用例 |
|
||||
| [`卷锁单元测试说明.md`](测试说明/卷锁单元测试说明.md) | vol_lock 创建 / 获取 / 释放契约(**fail-open 不变量**:创建失败仍返回 1 且零阻塞)/ 幂等 / 超时透传,33 断言点 |
|
||||
| [`时间同步单元测试说明.md`](测试说明/时间同步单元测试说明.md) | time_sync 分片喂狗延时 / 监听退避 / 时间包重组 / 任务主循环(**N-17 的全部逻辑贡献者**),80 断言点 |
|
||||
|
||||
**模块测试指南**(动手写测试前先读)
|
||||
|
||||
@@ -172,6 +177,7 @@ docs/
|
||||
| [`RAM使用分析与优化方案.md`](内存管理/RAM使用分析与优化方案.md) | **内存主文档**:占用分析、优化方案、定栈规则 |
|
||||
| [`内存内容分配策略.md`](内存管理/内存内容分配策略.md) | 各内存区(SRAM/CCM)的分配策略 |
|
||||
| [`存储栈RAM优化调研.md`](内存管理/存储栈RAM优化调研.md) | 存储栈 RAM 优化调研与结论 |
|
||||
| [`RAM预算台账.md`](内存管理/RAM预算台账.md) | **预算台账**:子系统配额、CCM 分配、趋势基线与 92% 门禁(N-27 三件套,2026-10-12) |
|
||||
| `data/` | `ram_report_*.md` / `stack_report_*.md` 快照数据(由 `scripts/` 生成) |
|
||||
|
||||
### 7.3 问题记录(**改驱动前必读**)
|
||||
@@ -207,19 +213,20 @@ docs/
|
||||
|
||||
| 文档 | 内容 |
|
||||
|---|---|
|
||||
| [`问题记录/项目审查与优化建议报告.md`](问题记录/项目审查与优化建议报告.md) | 项目全景审查:文档/代码/架构/可靠性问题清单(D-1~D-7、N-xx、Px-xx)——**体系层评审报告,非待办池**(待办见 §9.3 `待处理内容.md`) |
|
||||
| [`问题记录/项目审查与优化建议报告.md`](问题记录/项目审查与优化建议报告.md) | 项目全景审查:文档/代码/架构/可靠性问题清单(未闭环项:D-1、N-xx、Px-xx;**已修复项按「修复即销项」删除**,去处见其变更记录)——**体系层评审报告,非待办池**(待办见 `任务规划/README.md`) |
|
||||
|
||||
### 9.2 环境与部署
|
||||
|
||||
| 文档 | 内容 |
|
||||
|---|---|
|
||||
| [`项目部署环境要求.md`](环境与部署/项目部署环境要求.md) | 开发机软件环境、目标板资源预算、现场部署与验收步骤 |
|
||||
| [`并行任务工作树方案.md`](环境与部署/并行任务工作树方案.md) | **多任务并行开发机制**:`git worktree` 隔离、目录与共享/独占分配(junction 规则)、`task-worktree`(`@task`)SKILL 命令面、单写者文档与合并残留审计、台架锁;**已落地并端到端实测**(见其 §11) |
|
||||
|
||||
### 9.3 台账与外部资料
|
||||
|
||||
| 位置 | 内容 |
|
||||
|---|---|
|
||||
| [`待处理内容.md`](待处理内容.md) | **统一待办登记簿**——审计缺陷(P-x)+ 台架/审计观察(N/S/T-x)+ 测试与架构专项(S1x/A-x)+ 任务排序(阶段 A/B/C/D)+ 诊断基线;只保留未闭环项,完成即删 |
|
||||
| [`任务规划/`](任务规划/README.md) | **统一待办登记簿(文件夹)**——`README.md` = 排序权威(波次 W1~W7)+ 编号空间 + 维护约定;`W1-采集保护收尾.md` = 当前波次任务卡(里程碑 × 并行道 × 五字段);`登记簿.md` = N/S/T-x 长期观察;`排期详述.md` / `暂缓-RS485.md` / `测试与架构专项.md` / `基线与待确认.md` = 波内详表 / 冻结簇 / 专项 / 基线;只保留未闭环项,完成即删 |
|
||||
| `硬件平台/` | 网表详细分析报告与语义图(V0.0 / V0.1);**[`ADC通道映射说明.md`](硬件平台/ADC通道映射说明.md)——V0.1 十七路 ADC 语义/网名/通道号权威映射表(固件取数、UI 显示、自检注入以此为准)** |
|
||||
| `芯片手册/` | 外部芯片数据手册转录(CH395F / GD5F2GQ5UE / SD2506 / TPAFE5160 / RTL8305NBI 等) |
|
||||
| `images/` | 文档技术配图(`.svg` 产物,由 `tools/arch_diagram/`、`tools/refgraph/` 生成) |
|
||||
@@ -242,19 +249,19 @@ docs/
|
||||
|
||||
## 11. 已知文档问题(待清理)
|
||||
|
||||
> 来源:《项目审查与优化建议报告》D-3/D-7。
|
||||
> 来源:《项目审查与优化建议报告》D-3/D-7(两条已销项,正文已移入 [`文档规范.md`](文档规范.md) 与 pre-commit 门禁)。
|
||||
> **D-3 / D-6 / D-7 已处理完毕**(2026-09-25):全库链接存在性检查通过(66 文档 0 失效)、任务表与
|
||||
> `Src/freertos.c` 一致、文档规范已建立。下表保留**仍需跟踪**的项。
|
||||
|
||||
| 问题 | 位置 | 说明 |
|
||||
|---|---|---|
|
||||
| 指向未实现工具 | `UI_界面制作流程.md`、`UI_自动化测试流程.md` → `tools/ui_mock/` | 文档承诺了未实现的 UI 渲染/比对工具(其引用已按 §3.3 标注「已删除」状态词) |
|
||||
| 产物缺失 | `内存管理/RAM使用分析与优化方案.md` → `scripts/ram_baseline.tsv` | 方案写了、工具未建(引用已加 `[计划]` 标记) |
|
||||
| 指向未实现工具 | `UI_界面制作流程.md`、`UI_自动化测试流程.md` → `tools/ui_mock/` | 该目录**始终不存在**;golden 回归实际落在 `simulator/tests/`(四脚本)。引用待改指,见 `docs/问题记录/项目审查与优化建议报告.md` §6.1 |
|
||||
| ~~产物缺失~~ | ~~`内存管理/RAM使用分析与优化方案.md` → `scripts/ram_baseline.tsv`~~ | ✅ 已建(2026-10-12,N-27 前半):`ram_baseline.py` + `ram_baseline.tsv` + [`RAM预算台账.md`](内存管理/RAM预算台账.md) + `mem-budget` 钩子 |
|
||||
| ~~目录名含空格~~ | ~~`UI 设计/`~~ | ✅ 已修复(2026-09-25,D-6):改名为 `UI设计/`,全仓库 67 处引用同步;命名约定见 [`文档规范.md`](文档规范.md) §1 |
|
||||
| ~~缺一致性门禁~~ | — | ✅ 已落地(2026-09-25/26,D-7 三档齐):`scripts/check_doc_links.py`(存在性)+ `scripts/gen_task_table.py`(数值)+ `scripts/check_mem_numbers.py`(体积),接入 pre-commit `doc-links` / `task-table` / `mem-numbers` 钩子 |
|
||||
| ~~`docs/文档规范.md` 未创建~~ | — | ✅ 已创建(2026-09-25):[`文档规范.md`](文档规范.md) |
|
||||
|
||||
> 完整清单与建议见 [`问题记录/项目审查与优化建议报告.md`](问题记录/项目审查与优化建议报告.md) §3.3。
|
||||
> 未闭环的文档类问题见 [`问题记录/项目审查与优化建议报告.md`](问题记录/项目审查与优化建议报告.md) §3.1(体量与 D-1)与 §3.4(复核后仅剩三条:D-1、语义断链、元信息漂移)。
|
||||
|
||||
---
|
||||
|
||||
@@ -262,9 +269,12 @@ docs/
|
||||
|
||||
| 日期 | 变更 | 说明 |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | 新增环境与部署 | 新建 `docs/环境与部署/并行任务工作树方案.md`(**多任务并行开发机制规格**:`git worktree` 隔离、目录与共享/独占分配、`task-worktree` / `@task` skill 设计、单写者文档与合并残留审计、台架锁;**已落地并端到端实测**);本索引 §9.2 补登记 |
|
||||
| 2026-10-11 | 功能 | N-19 FTP 鉴权:`/config.ini` 新增 `[ftp]` 段(`user` / `pwd_hash` = SHA-256(user‖password) / `write`)+ 恒定时间比较 + 连续 5 次失败锁 60 s + 未登录命令门;出厂默认 admin/admin,默认口令在用有启动告警。新增 `App/util/sha256.*`、`Drivers/BSP/NETCFG/ftp_cfg.*`、`Drivers/BSP/NET/lftpd/lftpd_auth.*`;主机侧单测 `test/host/sha256/`(13 例)+ `test/host/ftp_auth/`(36 例),pre-commit 钩子 26→28;台架脚本 `test/host/ftp_auth_bench.py`。文档同步:`docs/子系统设计/网络配置说明.md`、`docs/架构设计/STM32F4-Base存储架构说明.md`、`docs/代码规范/pre-commit使用说明.md`、`docs/产品需求/产品需求设计文档.md` |
|
||||
| 2026-10-11 | 文档调整 | `AGENTS编写指南.md` 新建后**同日删除**(要点已沉淀入 `AGENTS.md` 宏观重构与本索引变更记录,指南类内容不单独成文);撤销 §1 目录树与 §7.1b 登记;同日 `AGENTS.md` 完成宏观重构(补项目概况/目录地图、平台约束分组、边界块) |
|
||||
| 2026-10-10 | 新增子系统设计 | 新建 `docs/子系统设计/ADC采样节拍设计说明.md`(ADC 采集链路 1 kHz 硬件定时器触发的 CubeMX 配置规格与代码改造清单,B1 遗留项规划);本索引 §5.1 补登记 |
|
||||
| 2026-10-10 | 新增算法专题 | 新建 `docs/算法/继电保护功能复现规划.md`(保护链路复现总规划);本索引 §1 目录树补 `算法/`、§5 拆为 5.1 功能子系统 / 5.2 算法专题并登记 `算法/` 两篇文档 |
|
||||
| 2026-10-05 | 新增产品需求文档 | 新建 `docs/产品需求/产品需求设计文档.md`(面向产品的需求基线);本索引新增 §3 产品需求、§2 推荐阅读顺序置顶、§1 目录树补 `产品需求/`;后续章节号顺延(架构设计 §4 … 变更记录 §12) |
|
||||
| 2026-10-05 | 新增产品需求文档 | 新建 `docs/产品需求/产品需求设计文档.md`(面向产品的需求基线);本索引新增 §3 产品需求、§2 推荐阅读顺序置顶、§1 目录树补 `产品需求/`;后续章节号顺延(本文 §4 … 本文 §12) |
|
||||
| 2026-09-25 | 目录重组 + 索引建立 | 顶层 16 篇散落 md 归入 5 个新目录(`架构设计/` `子系统设计/` `网络/` `审查记录/` `环境与部署/`);全仓库 90 处引用同步;新建本索引(D-5) |
|
||||
| 2026-09-25 | D-6 目录去空格 | `docs/UI 设计/` → `docs/UI设计/`;全仓库 67 处引用同步(含 URL 编码形态 `UI%20设计`);命名约定写入 §10 |
|
||||
| 2026-09-25 | D-7 文档-代码一致性门禁 | 新建 `scripts/check_doc_links.py`(链接存在性,含行内代码豁免/就近状态词/`[计划]` 标记)、`scripts/gen_task_table.py`(任务表由 `Src/freertos.c` 生成/校验,锚点 `TASK_TABLE:BEGIN/END`);接入 pre-commit `doc-links`/`task-table` 钩子;全库 66 文档 0 失效 |
|
||||
|
||||
@@ -239,6 +239,23 @@ if (n > 0) {
|
||||
2. **最小帧长判据统一**:`remo_handle` 以 `n < 5` 拦截(最短合法帧 5 字节),两端一致——旧 MCU `n < 6` 会丢 5 字节 KEEPLIVE/INIT 帧的隐患已随统一消除。
|
||||
3. **断线重连统一**:`ui_net_sink_poll` 在客户端断开后用 `net_get_sock(g_listen) == NULL` 判监听是否失效(MCU 单连接模型下监听随客户端一起失效),失效即 `bind_listen()` 重建——两端一致,并修复了 MCU 单连接模型断线后无法重连的隐患。
|
||||
4. **协议兼容**:线格式与命令语义不变,任一端都能与 DTU-RemoteLCD 参考客户端互通;双端行为一致已由 e2e 回归(`simulator/tests/check_remo_sink.py`,0/25600 位差异)与双端构建(Keil + CMake 均 0 错误 0 警告)验证。
|
||||
5. **驱动者是宿主,不是协议自己**:`ui_net_sink_poll()` 由宿主周期调用——MCU 端目前 = **uiTask 主循环 10 ms**,模拟器 = `main.c` 主循环。协议本身不含节拍、不阻塞。
|
||||
|
||||
> ✅ **门控已落地(2026-10-10)**:`ui_net_sink_poll()` 现在**先查后动**——`net_can_accept()` /
|
||||
> `net_can_read()` 纯 RAM 读控制块判"有没有活",无活直接返回,不再每 10 ms 发一次
|
||||
> `net_accept`(原 100 msg/s 队列压力,与 FTP 抢深度 8 的队列)。门控判"无数据"时仍
|
||||
> **每 32 轮无条件 recv 一次**兜底(`readable` 只由 RECV_OK 中断置位,中断丢失时纯门控
|
||||
> 会把数据永久藏住)。模拟器后端恒返回"就绪",行为逐位不变。
|
||||
>
|
||||
> ⚠ **残留暴露(已知,刻意不处理)**:门控只消除"无谓的往返",消除不了"往返本身无上界"——
|
||||
> 客户端在连且 netTask 拥塞时,3204 B 的 LCDMEM 应答仍可能让宿主任务等最长 ≈4 s。
|
||||
> "把推流搬出 uiTask(独立任务 + 帧 seqlock)"已完整评估并**决定不做**:实测刷新最坏周期
|
||||
> (`uip` ≈61 ms)是 **LVGL 帧周期地板**、与网络无关,搬任务不会让刷新变快;成本却含
|
||||
> **必改 `.ioc`** 的重生成与全套回归,收益只剩极端并发下的观感隔离。若日后它成为实际问题,
|
||||
> 正确方向是先治 netTask 排队公平性(登记簿 N-4 二期),不是把 UI 搬走。
|
||||
> 观测手段:`[PROF]` 的 `uis`(往返最坏)与 `uisn`(此刻仍在推流段内多久)——
|
||||
> **`uisn` 持续非 0 就是这一格真发生的铁证**;字段语义与判读见
|
||||
> [`../子系统设计/看门狗设计说明.md`](../子系统设计/看门狗设计说明.md) §11.4。
|
||||
|
||||
---
|
||||
|
||||
|
||||
+11
-11
@@ -1,7 +1,7 @@
|
||||
# UI 界面制作流程(参考图 → LVGL)
|
||||
|
||||
> **用途**:规范"参考图 → 设计规格 → LVGL 代码 → 双端验证"的完整管线,规定每一步的**动作、产物、放行条件(gate)**,保证过程可重复、可交接。
|
||||
> **适用**:所有 UI 新界面 / 界面大改;小改动走 §10 快速通道。
|
||||
> **适用**:所有 UI 新界面 / 界面大改;小改动走「小改动快速通道」一节(见文末第十节)。
|
||||
> **配套文档**:`UI_设计架构.md`(系统如何工作)、`菜单页面设计.md`(菜单/导航行为与数据流设计)、`LVGL模拟器部署与使用说明.md`(模拟器)、`UI_中文字体生成与配置.md`(字体生成)、`UI_自动化测试流程.md`(P2/P3 回归门禁的可执行版本:脚本 + 池水位常量 + 黄金参考更新)。
|
||||
> **首个实例**:界面 01 主界面(根页),规格 `mockups/01_主界面_说明.md`(本文所有示例数字均取自该试点)。
|
||||
|
||||
@@ -24,7 +24,7 @@ flowchart TD
|
||||
P3 -- "Gate: @build 0 错 0 警 + @test PASS + 上机目检通过" --> P4(["P4 归档<br/>→ 变更记录/台账 + 随代码提交(代码+字体+字集+规格+文档)"])
|
||||
```
|
||||
|
||||
> 小改动(文案/间距/纯交互/字体重生成)走 §10 快速通道,无需全流程。
|
||||
> 小改动(文案/间距/纯交互/字体重生成)走「小改动快速通道」一节(见文末第十节),无需全流程。
|
||||
|
||||
### 1.1 各阶段职责分工
|
||||
|
||||
@@ -160,7 +160,7 @@ flowchart TD
|
||||
| 黑/白背景 | `lv_style_set_bg_color` + `lv_style_set_bg_opa` | 1bpp 仅纯黑/纯白 |
|
||||
| 文本 | `lv_label_create` + `lv_label_set_text_static` | 静态字符串(C5) |
|
||||
| 文字对齐 | 标签定位 / `lv_label_set_long_mode` | 默认左对齐(见 §3.6 推论) |
|
||||
| 选中反色 | `LV_STATE_CHECKED` 样式(bg/文字色互换) | 条目对象**自身**带 CHECKED(防白底白字,架构 §5.5) |
|
||||
| 选中反色 | `LV_STATE_CHECKED` 样式(bg/文字色互换) | 条目对象**自身**带 CHECKED(防白底白字,`UI_设计架构.md` §5.5) |
|
||||
| 隐藏状态 | `LV_OBJ_FLAG_HIDDEN` | 页面/状态切换 |
|
||||
| 圆角/阴影/透明/渐变 | **禁止**(C8) | 规格里就不该有 |
|
||||
|
||||
@@ -175,8 +175,8 @@ flowchart TD
|
||||
### 4.3 翻译纪律
|
||||
1. **逐参数翻译**:代码中每个 px 值必须能在规格"布局参数表"中找到出处;禁止"看着办"
|
||||
2. 样式沿用现有 `s_style_flat` 模式(静态样式、一次性初始化、后加优先覆盖主题)
|
||||
3. 焦点对象恒为**页面容器**;条目不入输入组(架构 §5.4)
|
||||
4. 容器**永不删除**(indev 释放事件 use-after-free,架构 §5.2);只建/毁框体(行自绘、无对象)
|
||||
3. 焦点对象恒为**页面容器**;条目不入输入组(`UI_设计架构.md` §5.4)
|
||||
4. 容器**永不删除**(indev 释放事件 use-after-free,`UI_设计架构.md` §5.2);只建/毁框体(行自绘、无对象)
|
||||
5. 编码纪律(C6):Edit 工具精确替换;文件保持 UTF-8 BOM 不变
|
||||
6. 注释按代码规范 V1.0:文件头模块块 + 函数头块(功能/参数/返回/限定/说明)
|
||||
|
||||
@@ -241,7 +241,7 @@ flowchart TD
|
||||
|
||||
### 5.4 Gate(进入 P3)
|
||||
- [ ] 模拟器出图 `shot.png` 与规格参数表逐项一致、与参考图结构一致(§5.2,忽略 5.3)
|
||||
- [ ] 导航行为符合键位语义表(架构 §3.4)
|
||||
- [ ] 导航行为符合键位语义表(`UI_设计架构.md` §3.4)
|
||||
- [ ] 自动化回归 PASS:`check_menu_rows.py` + `check_content_pages.py`(`UI_自动化测试流程.md`)
|
||||
|
||||
---
|
||||
@@ -307,17 +307,17 @@ flowchart TD
|
||||
| 用了字体子集外的字 | 该字显示空白 | P0 字符集核对 + P1 字体重生成 |
|
||||
| 字体缺字形(如宋体无 ▶) | 该字符显示成方框 □ | `make_font.py` 已内置 `merge_arrow_glyph()` 自动补回;其余缺字形同法(从有该字的字体提取位图合并,§4.4) |
|
||||
| 新 .c 只注册了 Keil(或只改了 CMake) | 单端编译缺函数/静默缺失 | 双端注册(C9) |
|
||||
| 模拟器主循环调 `SDL_PollEvent` | 键盘全部无响应 | 事件只由 LVGL SDL 驱动内部轮询(架构 §5.7) |
|
||||
| 选中行"白底白字" | 选中文字不可见 | 历史坑(2026-09-02):子 label 自身 card 样式总命中、继承不到父选中色;现行行自绘(绘制时给色、无样式继承)已根除(架构 §5.5) |
|
||||
| 按键处理里删除焦点容器 | use-after-free 崩溃 | 容器持久不删(架构 §5.2) |
|
||||
| 模拟器主循环调 `SDL_PollEvent` | 键盘全部无响应 | 事件只由 LVGL SDL 驱动内部轮询(`UI_设计架构.md` §5.7) |
|
||||
| 选中行"白底白字" | 选中文字不可见 | 历史坑(2026-09-02):子 label 自身 card 样式总命中、继承不到父选中色;现行行自绘(绘制时给色、无样式继承)已根除(`UI_设计架构.md` §5.5) |
|
||||
| 按键处理里删除焦点容器 | use-after-free 崩溃 | 容器持久不删(`UI_设计架构.md` §5.2) |
|
||||
| LVGL 池爆 40KB | 对象创建失败/画面残缺 | §4.5 预算 + 按需建毁 |
|
||||
| 极性配反 | 黑屏/白屏/不可读 | 模拟器 mono 主题 `dark_mode` 参数 + MCU 端背景色一致 |
|
||||
| 步进宽接近/超出格宽的标签居中 | 亚像素错位/不对称 | 左对齐 + 字体侧边距(§3.6) |
|
||||
| 关闭了 `LV_DRAW_SW_COMPLEX`(省代码) | `lv_line` 斜线不绘制(如主界面顶部"浅 V"两腿消失,只剩水平线) | `LV_DRAW_SW_COMPLEX` 两端显式 =1;`LV_DRAW_SW_CIRCLE_CACHE_SIZE` 须非零(默认 4,否则零长数组 C2466)(架构 §关键约束 7) |
|
||||
| 关闭了 `LV_DRAW_SW_COMPLEX`(省代码) | `lv_line` 斜线不绘制(如主界面顶部"浅 V"两腿消失,只剩水平线) | `LV_DRAW_SW_COMPLEX` 两端显式 =1;`LV_DRAW_SW_CIRCLE_CACHE_SIZE` 须非零(默认 4,否则零长数组 C2466)(`UI_设计架构.md` §关键约束 7) |
|
||||
| 斜线用细 `line_width`(如 2px) | 1bpp 阈值吃掉斜线边缘,腿偏细、比设计稿细 | 斜线加粗(腿 3px),或按段拆 `lv_line` 分粗细(如"浅 V"两腿 3 / 水平线 2) |
|
||||
| 规格引入翻译不出来的效果 | P1 翻译不出来 | 1bpp 禁区(C8),P0 阶段就拦下 |
|
||||
| UI 源文件(UTF-8 BOM)里 `snprintf` 格式串含中文 | 模拟器 cl.exe 报 C4819(代码页 936 误读 UTF-8 字节) | 格式化输出用 `lv_snprintf`(LVGL 自带,双端一致),勿用标准库 `snprintf` |
|
||||
| 常驻样式用动态 `lv_style_t` + set_prop | LVGL 池永久占用:首次 set_prop 分配 `values_and_props` 且永不释放(x64 实测 88 B/个) | 常驻样式一律常量样式:`LV_STYLE_CONST_INIT(var, props[])` + `lv_style_const_prop_t[]`(属性存 Flash、零池分配,见架构 §5.6) |
|
||||
| 常驻样式用动态 `lv_style_t` + set_prop | LVGL 池永久占用:首次 set_prop 分配 `values_and_props` 且永不释放(x64 实测 88 B/个) | 常驻样式一律常量样式:`LV_STYLE_CONST_INIT(var, props[])` + `lv_style_const_prop_t[]`(属性存 Flash、零池分配,见`UI_设计架构.md` §5.6) |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
|
||||
|
||||
| 步 | 脚本/命令 | 门禁内容 | 预期输出 |
|
||||
|---|---|---|---|
|
||||
| 1 | `simulator/tests/check_menu_rows.py` | 菜单 8 状态:选中行白带 / 非选行黑底白字 / 框体 1px 边框 / 往返像素一致(S3==S1、S6==S1、S7==S2)+ S0 池基线 | `PASS: 8 状态全部断言通过(8 图,池基线 4784 B)` |
|
||||
| 1 | `simulator/tests/check_menu_rows.py` | 菜单 8 状态:选中行白带 / 非选行黑底白字 / 框体 1px 边框 / 往返像素一致(S3==S1、S6==S1、S7==S2)+ S0 池基线 | `PASS: 8 状态全部断言通过(8 图,池基线 5368 B)` |
|
||||
| 2 | `simulator/tests/check_content_pages.py` | ① 文本级断言(字形模板匹配,50 条,独立于 `ref/`)→ ② 11 页截图与 `ref/` 黄金参考**逐字节**比对 + 开页-关页往返池水位 | `PASS: 11 内容页 + 往返池水位全部通过`(文本断言先于像素比对,失败即中止) |
|
||||
| 3 | `keil-build` skill(`keil_build.py`) | MCU 端编译(UI 源码双端共用,改一端必须过另一端) | 退出码 0 = BUILD_OK |
|
||||
| 4 | `scripts/check_coding_standard.py` | 代码规范 V1.0 机械检查 | PASS |
|
||||
@@ -44,8 +44,12 @@ 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 模拟器) |
|
||||
| `POOL_AFTER_ROUNDTRIP` | check_content_pages.py | **5448 B** | 开页-关页往返后水位 = 基线 + 一次性残留 80 B |
|
||||
|
||||
> 水位历史:4784 →(2026-09-14 待机模块常驻 +656)5440 / 5520 →(2026-10-11 裁剪 LVGL 控件类 −72)5368 / 5448。
|
||||
> 后一次漂移已核实为**有意且更优**:像素逐字节未变(11 页黄金参考在裁剪**前**生成、裁剪后全部一致),
|
||||
> 且一次性残留仍是 80 B(5448 − 5368)→ 泄漏特征未变,只是 `lv_theme_apply` 不再为已不存在的控件类登记样式条目。
|
||||
|
||||
**漂移处理**:菜单/页面结构或样式变更导致门禁失败时,先确认新值合理(残留分解见
|
||||
`菜单页面设计.md` §9.5),再更新脚本常量并在本文变更记录登记。**禁止为过门禁盲目改常量**——
|
||||
@@ -102,3 +106,4 @@ cmake --build simulator/build --config Debug # 0) 构建模拟器(须 0 警
|
||||
| 2026-09-10 | 新增 `simulator/tests/regen_ref_png.py`(由 `ref/*.bin` 批量重生成目检 PNG,默认 `--scale 6 --invert`);`ref/*.png` 加入 `.gitignore`(本地调试产物不入库);§四 补"重生成目检 PNG"步骤(修复"刷了 `.bin` 参考但 `.png` 仍是旧渲染 → 看似乱码") |
|
||||
| 2026-09-10 | `simulator/tests/ref/` **整目录不入库**(`.bin` 黄金参考 + `.png` 目检图均为本地生成物):`git rm --cached` 原有 3 个 `.bin`,`.gitignore` 改为忽略整目录;首次/换机后先 `--update` 生成参考;`--update` 先跑文本断言防误刷;§四 重写 |
|
||||
| 2026-09-13 | 管线同步:`UI_界面制作流程.md` 移除 HTML 样机阶段(五阶段 → 四阶段 P0~P4),本文定位改为"P2/P3 门禁的可执行版本";§六 关系描述更新为 P0 规格 → P1 编码 → P2 本文回归 → P3 上机目检 → P4 归档;移除 `tools/ui_mock/` 引用 |
|
||||
| 2026-10-11 | **裁剪 LVGL 死代码(F-1/F-2/F-3)时的门禁记录**:先用**未裁剪**配置 `--update` 生成 11 页黄金参考,再切到裁剪配置重编重跑 → **逐字节全部一致**(菜单 8 状态像素一致),证明裁剪零视觉影响。池水位门禁随之 5440/5520 → **5368/5448**(−72 B;一次性残留仍 80 B,故非泄漏变化,见 §三 水位历史)。顺带揪出并修掉 `Lib/lv_conf.h` 与 `simulator/lv_conf.h` 的**长期漂移**(模拟器早已 I1-only、MCU 端未裁)→ 规则见 `UI_设计架构.md` §1.4 第 10 条,量化见 `../内存管理/RAM使用分析与优化方案.md` §2.6.4 |
|
||||
|
||||
+11
-2
@@ -23,7 +23,7 @@ MCU **无物理屏幕**:LVGL 做 160×160 **1bpp 单色离屏渲染**,帧缓
|
||||
| `Drivers/BSP/UI/ui_net_sink.c/h` | **共用** | RemoDispBus 服务端(收键 + 回送帧缓冲),MCU/PC 双端同一份;平台后端 `net_socket` + `ui_net_sink_backend`(见 RemoDispBus 协议文档) |
|
||||
| `Drivers/BSP/UI/ui_font_cn_12.c` | **共用** | 中文 1bpp 子集字体(`lv_font_conv` 生成物) |
|
||||
| `App/task/ui_task.c/h` | MCU | UI 任务:等 NET_READY → 初始化 → 10ms 刷新循环 |
|
||||
| `Lib/lv_conf.h` | MCU | LVGL 配置(1bpp / 40KB 内存池 / 默认字体) |
|
||||
| `Lib/lv_conf.h` | MCU | LVGL 配置(1bpp / 40KB 内存池 / 默认字体);⚠ **必须与 `simulator/lv_conf.h` 同一特性子集**,见 §1.4 第 10 条 |
|
||||
| `simulator/`(main.c / sim/ / lv_conf.h / CMakeLists.txt) | PC | SDL2 模拟器(替代 ui_port,共用全部 UI 源码);支持 `--screenshot` / `--keys <seq>` 无头截图验证 |
|
||||
| `tools/font_gen/`(extract_cn.py / make_font.py)+ `ui_cn_chars.txt` | 工具 | 字符集提取 / 字体生成 |
|
||||
| `tools/menu_gen/gen_menu.py` + `ui_menu_tree.txt` | 工具 | 菜单声明源 → 生成 ui_menu_tab.c/h |
|
||||
@@ -43,7 +43,7 @@ MCU **无物理屏幕**:LVGL 做 160×160 **1bpp 单色离屏渲染**,帧缓
|
||||
|
||||
### 1.4 硬约束(改 UI 必须遵守)
|
||||
|
||||
1. **RAM 紧张**:新增页面/控件/缓冲前必须核算 LVGL 池(40KB)与任务栈余量(见 `待处理内容.md`)
|
||||
1. **RAM 紧张**:新增页面/控件/缓冲前必须核算 LVGL 池(40KB)与任务栈余量(见 `docs/任务规划/基线与待确认.md` §4 内存预算)
|
||||
2. **新增汉字必须重新生成字体**(`tools/font_gen/` 两步),否则显示空白
|
||||
3. 文本一律用静态字符串(`lv_label_set_text_static`),不 strdup
|
||||
4. UI 相关文件为 **UTF-8 带 BOM**(`Drivers/BSP/UI/*`、`ui_task.c`、`Lib/lv_conf.h`),其余源码无 BOM;禁止 PowerShell 整文件重写(编码铁律,见 AGENTS.md)
|
||||
@@ -64,6 +64,15 @@ MCU **无物理屏幕**:LVGL 做 160×160 **1bpp 单色离屏渲染**,帧缓
|
||||
`lv_display_set_theme()` + 对**活动屏幕**再 `lv_theme_apply()`(实测 `bg_before=F5F5F5` → `bg_after=000000`)。
|
||||
另外 I1 缓冲头 8 B 调色板 LVGL 不替调用方初始化(上电全 0 = 两项透明黑),须显式写 `[0]=黑 [1]=白`;
|
||||
渲染器本身按亮度阈值置位(`lv_draw_sw_blend_to_i1.c`:亮 → 位=1),与客户端"位=1 即亮"的约定一致。
|
||||
10. **两份 `lv_conf.h` 必须是同一特性子集(2026-10-11 定;F-1/F-2/F-3 落地时暴露的漂移)**:`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` 默认值隐式生效,把用不到的代码整份链进镜像 = **实测 64,576 B 死代码(旧镜像的 23.5%)**。现两端对齐为同一子集:
|
||||
* 绘制核只留 `LV_DRAW_SW_SUPPORT_I1`(显示是 1bpp;字形 A8 位图是作为 **mask** 传入、混合按**目的**格式分派,故不需要 A8/L8/AL88);
|
||||
* 控件全关(`label`/`button`/`buttonmatrix`/`bar`/`textarea`/`image`/`keyboard`)——全仓(`Drivers/BSP/UI`、`App`、`simulator`)**零控件构造函数**,只用 `lv_obj_create` + `lv_theme_mono_init`;
|
||||
* `LV_USE_THEME_SIMPLE 0`:它是把上述控件类拉进镜像的**锚点**(`lv_display_create()` 会强链 `lv_theme_simple_init`);⚠ 9.3 **没有** `LV_USE_THEME_BASIC`,旧配置里那条是无效项,已删;
|
||||
* `LV_USE_FLEX 0` / `LV_USE_GRID 0`:全仓无 `lv_obj_set_flex_*` / `lv_obj_set_grid_*` 调用。
|
||||
⚠ 裁剪后 ARMCC 报 `#177-D`(上游 9.3.0 在 `lv_draw_sw_blend_to_i1.c` 漏了 `lv_color16_from_u16` 的定义处守卫)→ 本仓已按使用点补 `#if LV_DRAW_SW_SUPPORT_RGB565_SWAPPED`(原型 + 定义两处),升级 LVGL 时若上游已修可去掉。
|
||||
⚠ 将来引入控件型页面(U2 原型库)时**两端同步**逐项打开并复跑 `ui-verify`。量化与验证记录见 `../内存管理/RAM使用分析与优化方案.md` §2.6.4。
|
||||
✅ **已入门禁**(2026-10-11):`scripts/check_lv_conf_sync.py` = pre-commit 钩子 `lv-conf-sync`。比对的是**生效值**(文件未定义则取 `lv_conf_internal.h` 兜底默认),并抓"写了 LVGL 不认的宏名"与同文件重复定义。首次跑即揪出两端共 **26 个 9.3 不存在的选项**(LVGL 8 / 9.4 模板残留,与 `LV_USE_THEME_BASIC` 同类)、**3 处同文件重复定义**、**10 项 MCU 端靠默认值隐式生效的开关**(含默认 =1 的 `LV_USE_OBSERVER` / `LV_USE_DRAW_PXP` / `LV_USE_IMAGEBUTTON`)——已全部清理/显式固化。
|
||||
✅ **门禁触发范围**(2026-10-11):`lv-conf-sync` 的 `files:` 同时匹配 `Lib/lv_conf.h`、**`simulator/lv_conf.h`** 与脚本自身 → **两端任一改动都会跑**。此前顶层 `exclude` 整目录挡掉 `simulator/`,模拟器侧改动拦不到;已删该条并把 `simulator/` 下自研代码整体纳入 pre-commit(cppcheck 当天即揪出 `sim_syslog.c` 一处真实空指针解引用),详见 `../代码规范/pre-commit使用说明.md` §5.5。
|
||||
|
||||
---
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user