Files
STM32F4-Base/AGENTS.md
T
wandering1 4a4ad6bc5f chore(全仓): @build/@test 旧文档别名清退为技能调用名
现状口径:/keil-build 与 /serial-boot-test 是 DSH 会话的技能调用名,
不是 shell 命令;「名 + 命令行参数」的命令形态改为可执行体
run_boot_test.py 的真实调用。

- AGENTS.md:别名说明重写为「技能调用名不是 shell 命令」,两条目改
  「编译(/keil-build)」「串口回归(/serial-boot-test)」
- keil-build / serial-boot-test 两 skill 调用名与示例同步;task-worktree
  方案文档去掉 @build/@test 惯例从句;@task 保留(不在点名范围)
- 123 文件现状陈述全改;台架捕获、变更记录/时间线、rename 映射输入
  等 41 行历史证据保留旧名
- gen_memory_svg.py 6 处措辞随改,3 张 SVG 产物同批重跑
- 顺手补并行方案同行漏网计数 51→52 钩子

验证:提交期快钩子对 123 文件实跑全 Passed;check_doc_links --all
rc=0(3038 处、无法判定 0);check_mermaid --all rc=0;YAML 解析 52
钩子不变;skill-size rc=0;py_compile 全过;字节卫生零漂移。
2026-10-30 13:03:12 +08:00

192 lines
41 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# STM32F4-Base
**工业级嵌入式 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。
**任何代码修改后必须先 `/keil-build` 验证(0 错误 0 警告)再提交;硬件闭环用 `/serial-boot-test`。**
> 本文件 = agent 入口:宏观概况 + 每次都在场的铁律在这里,细节在 `docs/` 与 skills(文末索引)。同一事实只在一处详述(`docs/文档规范.md` 同此要求)。
## 项目概况
**现状一句话**:平台能力(网络 / 存储 / 时间 / UI / 日志 / 看门狗 / FTP / 配置)**已实现并台架验收**;采集主线已闭环到业务消费者(语义取数与有效性→全周傅氏相量→测频与遥测快照→保护判据 protectTask→104 遥信/遥测+UI,B5/批 2);⚠ **保护判据现处于「未标定闭锁」态**——产品构建下 `adc_cal_flags()` 的 `ADC_CAL_F_VALID` 未置位,`protect_task.c:98` 直接停走判据,等两点标定写入 `[adc_cal] valid` 后自动解卡(用户裁定 2026-10-25:保持闭锁,不用万用表走捷径);**RS-485 采集尚未接线**(B2 按用户决策冻结,见 `docs/任务规划/暂缓-RS485.md`)——当前最大功能缺口,接线节奏受 **RS-485 接线门禁**约束(见「平台约束」)。
**数据主线**(能力视角;分层图与跨层依赖表 → `docs/架构设计/项目架构总览.md`):
- **采集**:TPAFE5160(计量级 8 路,并行总线)+ 片内 ADC1(9 路,DMA)→ TIM3 1 kHz 节拍 + 原子快照 → adcTask → 语义取数与有效性校验 → 全周傅氏相量 → **测频与遥测快照(`meas` 200 ms 双区,104 与 UI 共用同一份上游,批 2)**+ 保护判据(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 源码**,远端可看图 + 注键
**目录地图**(只列顶层——稳定边界;细结构自己 `ls`,别背路径;**仓库根不放一次性产物**——提交信息草稿 / 临时改写脚本 / 门禁输出与日志一律进 `ci-artifacts/`(已忽略,口径见 `.gitignore` 尾注),根目录只留工程配置与两份生成器输入 `ui_menu_tree.txt`、`ui_cn_chars.txt`):
| 目录 | 职责 | 可改性 |
|---|---|---|
| `Src/` `Inc/` | CubeMX 生成(外设初始化 / 中断向量 / `freertos.c` 任务表) | **只写 `USER CODE` 区** |
| `App/` | 应用层:`app/` 启动与就绪事件 · `task/` 任务编排 · `algo/` 采集与算法分层(语义取数与有效性 → 保护判据) · `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 工作流 · 串口/烧录配置 | 手写 |
**分层边界**:上层只依赖相邻下层(业务 → UI/基础服务 → 中间件 → BSP 驱动 → HAL/RTOS → 硬件,**单向**);新算法模块进 `App/algo/`,勿塞 `App/task/`(任务只做消费/编排);新 `.c` 必须双端注册(**`new-source-file` skill**)。
## 架构要点
`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`(语义取数与有效性层)→ 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`)
- **采集与算法层(语义取数与有效性/全周傅氏相量/保护判据)落在 `App/algo/`**:`adc_input`/`adc_health` = 语义取数与有效性,`sample_ring`/`phasor` = 全周傅氏相量,`freq` = 测频与遥测快照(B8,自适应窗),`prot_set`/`prot_elm` = 保护判据。板端消费者 = **protectTask**(B5,2026-10-12):20 ms 节拍 `prot_elm_run()` + 自检失败闭锁(采样故障闭锁)+ 定值校验和巡检;跨上下文复归(104 YK11)经 `prot_elm_request_reset()` 落请求。纳入 IWDG `required_mask`(`TASK_WD_PROTECT`,产品 `0x1F7`)。→ `docs/算法/采集与算法层设计说明.md`(设计与判据)、`App/algo/README.md`(目录索引)
- 全景 → `docs/架构设计/项目架构总览.md`;启动时序与内核前模式 → `docs/架构设计/项目启动顺序管理.md`
<!-- TASK_TABLE:BEGIN generated by scripts/gen_task_table.py -->
| Task | 栈 (words) | 优先级 |
|---|---|---|
| defaultTask | 1280 | Normal |
| netTask | 960 | Normal |
| ftpTask | 1024 | Normal |
| testTask | 2048 | BelowNormal |
| uiTask | 4096 | BelowNormal |
| timeSyncTask | 640 | Normal |
| adcTask | 640 | AboveNormal |
| iec104Task | 640 | Normal |
| protectTask | 512 | Normal |
<!-- TASK_TABLE:END -->
(实际值以 `Src/freertos.c` 为准,本表由 `scripts/gen_task_table.py` 生成、`task-table` 钩子校验;testTask 仅在启用测试套件时创建。FreeRTOS 堆已迁 CCM,任务栈不占 SRAM;**每次抬栈的实测最坏链与余量台账** → `docs/内存管理/RAM使用分析与优化方案.md` §2.3(任务栈配置与实测峰值)与 §4.0.6(静态最坏链与判定)、`docs/内存管理/内存内容分配策略.md` §6(容量台账))
## 铁律:源码编辑与编码
- 改代码**只用 Edit 工具**(精确子串替换 / replaceAll);**绝不用 `Get-Content`/`Set-Content`/`cmd` 重写含中文的文件**——整文件重写按系统默认编码重解码 → `U+FFFD` 乱码且不可逆
- **Edit 会静默剥掉文件原有的 UTF-8 BOM**:带 BOM 的文件(`Drivers/BSP/UI/*`、`App/task/ui_task.c`、`App/cfg/cfg_file.c`、`Lib/lv_conf.h`)被 Edit 后丢 BOM → ARMCC 改按 CP936 解码 → 中文字面量报 `#870-D: invalid multibyte character sequence`(**只有重编译该文件才暴露,历史"0 警告"不代表安全**)。改完须核对 BOM 并按原样补回(只前缀 3 字节 `EF BB BF`,勿整文件重写)。**已机械化:`bom-guard` 钩子**(`scripts/check_bom_drift.py`:HEAD 有 BOM 而工作区没有即拦;上面这份名单与钩子 `files` 正则、脚本内 `BOM_LIST` **三处互指**,脚本每次跑都核一次,单独改一处会在别人的提交里被拦下)
- **字符串字面量必须纯 ASCII**:无 BOM 文件里奇数个汉字会吞掉结束引号(偶数侥幸通过)→ 中文只写注释
- **注释里写作用域/路径别写星号形态**:`Drivers/BSP/UI/**` 里的 `/*` 会被 ARMCC 当第二次注释起点(诊断 9-D),C 块注释不嵌套;且**每个包含者各刷一行**、只在重编译那个 TU 时才暴露。写全目录(`Drivers/BSP/UI/`)或用尖括号(`<Drivers/BSP/UI/ui_x.c>`)——**已机械化 `nested-comment`** 钩子,存量 2 处基线只减不增 → `docs/门禁说明/nested-comment.md`
- 怀疑乱码先统计 `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/代码规范/`
- **手改 `.dsh/skills/**/SKILL.md` 有 65536 字节写侧硬上限**(dsh-mnemon 插件 `validateSkillContent()` 只在写侧校验,**加载侧不查**):超限的技能照常 `skill()` 可读、照常进技能目录,但自进化每次「生成草案」都在落盘那一步被拒 → 建议永久卡在「生成中」、界面只剩转圈、连「忽略」都点不动(实测 2026-10-29 两个技能同日各卡一次,第二次的文件 72,601 字节)。加内容前先看体积:`node "$HOME/.dsh/scripts/mnemon-skill-ledger-doctor.mjs" --root .`(只报不改);正文控制在 ~48 KB(≥75% doctor 就报 CLOSE)。**新增的深规程写进同目录 `reference/<篇名>.md`,正文只留可执行判据的摘要 + 指针,且不重排既有步骤号**。已卡死的取证与自愈 → **`mnemon-skill-ledger-stuck-repair` skill**。**已机械化:`skill-size` 钩子**(`scripts/check_skill_size.py`,纯标准库;`--selftest` 五形态、`--root` 可指到用户级 `~/.dsh/skills` 手工量)
## 构建与验证
- **门禁时机(用户裁定 2026-10-13)**:`/keil-build` / `scripts/ci_local.py` / `check_doc_links.py` **只在准备提交时跑**(用户让 commit 后统一跑一遍),**不是每次中间改动后都跑**——多步改动期间直接改即可,验证集中在提交门禁;`/serial-boot-test` 硬件闭环仍按任务需要跑
- **`/keil-build` / `/serial-boot-test` / `/task-worktree` 是 DSH 会话的技能调用名、不是 shell 命令**(bash 里直接敲是 command not found / syntax error)——下面 `=` 后的命令行才是可执行形态(PowerShell 形态,bash 下路径正斜杠)
- **编译**(`/keil-build`)= `& '.venv\Scripts\python.exe' .dsh\skills\keil-build\scripts\keil_build.py`(仓库根执行)。退出码 **0=PASS / 1=构建失败 / 2=环境错误(UV4/工程/档案)/ 3=用法 / 4=build 锁被占用**;日志 `MDK-ARM/build_log.txt`;换机必设 `DTU_KEIL_UV4` → 细节 **`keil-build` skill** + `docs/环境与部署/本地工具路径环境变量方案.md`
- **串口回归**(`/serial-boot-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 均可
- ⚠ **钩子 entry 的裸 `python` 不等于 .venv**:2026-10-26 真实提交实测 pre-commit 把它解析成**机器级**解释器(无 fontTools)→ 字体链生成器报 `No module named fontTools`、`generated-fresh` 假红(同族第二次发作见 `scripts/ci_local.py` 文件头 2026-10-11「静态门禁全灭」)。凡依赖第三方库的门禁脚本,一律在脚本内自己按「显式 var > 当前解释器 > 仓库 .venv > PATH」解析并**探针校验**(装了 pre-commit ≠ 装了 fontTools),但**别把 entry 写死成某台机器的绝对路径**(那等于把门禁绑死在一台机器上)→ `docs/门禁说明/generated-fresh.md` §4
- 重建/换机:`<本机任一 python ≥3.10> -m venv .venv` + `pip install -r requirements.txt`(含 `pre-commit`;`DTU_PRECOMMIT_PYTHON` 可指向别的装了 pre-commit 的解释器);`requirements.txt` 必须**纯 ASCII**(Windows 下 pip 按 GBK 代码页解码)
## 提交门禁(pre-commit)
- 任一失败即拦截,共 **52 个钩子**(**口径 = YAML 解析后的 hook 条目数**:自查命令与「声明 / 文档 / 实跑」三数同源核对见 `docs/门禁说明/README.md` §2 首条——⚠ 直接 grep 配置里的声明行会多算一条**注释掉的**候选钩子(比口径多 1),不是口径;**配置注释里也别复述那个 grep 字面量**,它会让文本计数再虚高 1(2026-10-27 实测踩过);加一条钩子就同批改 AGENTS 与门禁索引两处计数。2026-10-25 道E 连加 `bom-guard` / `setview-check` / `uinav-selftest` 三条,2026-10-26 加 `generated-fresh` 一条,2026-10-27 加 `nested-comment` 一条;2026-10-28 加 `id-check-new` 一条并同批退役 `id-check`(口径 52 → 51:编号门禁换牙——新的只查新增行、零语料,旧的八项全库校验连语料一起移入 `scripts/archive/id_gate/`,手工普查与恢复机器面见 `docs/编号规范.md` §8.5);2026-10-29 加 `skill-size` 一条(51 → 52),同日按用户裁定**退役 `id-check-new`**(52 → **51**:只查新增行的编号门禁整体退场,脚本与白名单一起移入 `scripts/archive/id_gate/`,编号形态此后无机器面,留档 `docs/门禁说明/id-check-new.md`);2026-10-30 加 `mermaid-syntax` 一条(51 → **52**:mermaid 围栏内确定性语法——坏图此前只能靠肉眼在渲染端发现,实证见专篇);**每种门禁一篇详解(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径)→ `docs/门禁说明/README.md` 索引**;通用机制与整体口径 → `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` + `bom-guard` / `setview-check` / `uinav-selftest`(2026-10-25 道E 那三条:前半句的加钩流水里有它们、逐项枚举里漏了,补上后本行各项相加才等于 51;2026-10-30 又加 `mermaid-syntax` 后此句的"等于"目标数为 52)+ 主机侧单测 **22 根专属钩子**(`-unit-test` 后缀 = 有独立 `files:` 触发面、需要秒级归因的那个子集;2026-10-25 增补 adc-cal / prot-dim / vol-idle-sync / ui-data / meas / run-par 六根)——**这是口径不是缺口**:`test/host/` 实测 44 套带 `run_unit_test.py`,其余 **22 套**(含 `test/host/ui_sink` 与录波三套)没有专属钩子,但每次 CI / `scripts/ci_local.py` 都被 `test-coverage` 总闸全量真跑并守基线回退,**失败照样拦**,只是归因落在总闸输出上;**没有专属钩子 ≠ 没被跑**,22 套台账与补钩子的判据 → `docs/门禁说明/host-unit-tests.md` §6 第 4 条。**Lead 裁定 2026-10-27:不补第 23 根**——读「这套跑没跑」去看总闸输出,别数钩子(原写「23 个 / 含 ui-sink 七个」是双重错:既把 22 说成 23,又宣称 ui-sink 有钩子——配置历史里 `ui-sink-unit-test` 从未存在)+ `test-coverage`(**覆盖率总闸**:跑全部 44 套并守基线回退;缺 mscoverage 组件时按用户裁定 **FAIL**、不 skip,2026-10-24)+ `test-suite-switch` + `registration-check`(双端注册反向校验 + UI 页面描述符表接线,登记簿条目「rs485_task 白名单缺口」)+ `generated-fresh`(**生成器幂等性**:把 `tools/menu_gen/gen_menu.py`、中文字库链、`tools/arch_diagram/gen_*.py` 拷进**系统临时目录**重跑,与工作区**逐字节**必须一致——钩子只读、不写仓库也不 stash 别人在飞的改动;实证 `0f7b5a87` 改了模板文字没重跑生成物,接线前实测另有 5 张 `docs/images/` 配图已漂 → 钉成只减不增基线,**业务生成物零豁免面** → `docs/门禁说明/generated-fresh.md`)+ `nested-comment`(**块注释嵌套**:注释体内再出现 /* 会被 ARMCC 报 9-D,且**按包含者数量刷屏**、只在重编译该 TU 时才暴露;本仓诱因就是「在注释里写作用域/通配路径」,改法是写全目录或用尖括号 <Drivers/BSP/UI/ui_x.c>;接线当日实扫 403 个 .c/.h,新发 0、存量 2 处(`test/host/common/` 两份假件头)钉只减不增基线 → `docs/门禁说明/nested-comment.md`)+ `skill-size`(**SKILL.md 不超插件写侧上限 65536 字节**:dsh-mnemon 只在写侧校验、加载侧不查,超限技能照常可读却每次「生成草案」被拒、永久卡在「生成中」;另查 `reference/` 指针悬空与 BOM → `docs/门禁说明/skill-size.md`)+ `mermaid-syntax`(**mermaid 围栏语法预检**:` ```mermaid ` 块里只查「错了必渲染失败」三条——图类型头 / `-.` 后直接跟管道(本坑:屏板链路设计说明 §1 坏图靠肉眼在渲染端发现,期间各文档门禁全绿)/ subgraph 与 sequence 块的 `end` 计数配对;零语料空基线、`docs/芯片手册/` 与 doc-links 同排除面 → `docs/门禁说明/mermaid-syntax.md`)+ `mem-budget`(RAM 92% 预算门禁,登记簿条目「缩堆迁 CCM 后半(暂缓)」)+ `font-charset`(UI 字符集差集检查,防缺字白框)+ `ui-golden`(UI 像素回归,菜单 8 状态 + 内容页 66 张黄金参考)+ `layer-deps`(**分层方向**:`Drivers/BSP/**` 不得 include `App/**` 的头;存量 3 条基线只减不增(2026-10-25 记录 reader 注入削掉 UI 记录 3 条边,6→3))+ `bsp-err`(**BSP 错误码与超时命名约定**:公开 API 不得拿 `HAL_StatusTypeDef`/`bool` 当错误通道、`*_TIMEOUT` 宏不得取负值、非 void 须写「返回值:」、**返回错误码的函数不得裸丢弃返回值**(须接住或显式 `(void)`;实测编译器与 cppcheck 都不报这类,只有这条会响);约定正文 `Drivers/BSP/bsp_err.h`,存量基线只减不增(2026-10-23 首清点 19 条 → 已还 18;`consume-result` 上线又暴露 37 条存量丢弃 → **现 38 条**))。**提交期实跑 49 根**(2026-10-30 起,前值是 48;三根慢钩子让位的裁定 2026-10-28):`cppcheck` / `test-cppcheck` / `test-coverage` 三根带 `stages: [manual]`——实测合计 ~165 s = 一次带代码提交耗时的三分之二,提交期只跑快的那些;这三根由**远端 CI** 强制(`.gitea/workflows/ci.yml` 的「手动阶段门禁」步,逐根 `--hook-stage manual`)与 `scripts/ci_local.py` 的 manual 段,本地手工复跑 `python -m pre_commit run --all-files --hook-stage manual <hook-id>`(一次只能一个 id)
- **范围 = 项目代码**:第三方(`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`)只跑静态门禁 + 主机侧纯逻辑单测,**刻意不跑 `/keil-build`**(需 Keil 授权)、不含采集链分层(需真机)。**远程 CI 的全部资料(服务端开关 / runner 部署与保活 / workflow / 排障 / 实测坑)集中在 [`docs/CI/README.md`](docs/CI/README.md),不放本节**——它**只跑在本机一台 runner `win-stm32-ci` 上**,其他机器与项目都不涉及。日常改代码的等价自查是 `& '.venv\Scripts\python.exe' scripts/ci_local.py`(静态门禁 + 提交信息门禁 + 主机侧纯逻辑单测)——**runner 不在线也不影响本地门禁,不能因"已配 CI"放宽本地要求**
## 平台约束
**CubeMX 与 Fault**
- `Inc/`/`Src/` 是 CubeMX 生成文件(带 `USER CODE BEGIN/END` 标记):自定义代码**只写标记区之间**;`Drivers/BSP/` 为纯手工代码
- **重生成会覆盖标记区之外的一切**(`.ioc` 的 `KeepUserCode=true` **只**保护 USER CODE 区;实测两次重生成各覆盖 4~5 处定制,其中两处**不报编译错**、只在真出故障时暴露)。防丢失按三层优先级 **写进 `.ioc` → 搬进 USER CODE 区 → 审计兜底**;**每次重生成后立刻跑 `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`):**2026-10-23 起由提交门禁机器拦截**(`coding-standard` 的 CODE-RULE-20,唯一豁免 = 平台分配后端的宿主侧实现,见 `docs/门禁说明/coding-standard.md` §7):MicroLIB 的 C 堆由 `startup_stm32f407xx.s` 的 `Heap_Size EQU 0x200` 限定为 **512 B**,KB 级分配必然返回 NULL,而**模拟器(宿主 CRT 堆)完全正常、照不出来** → 只用 `pvPortMalloc/vPortFree`,或把分配收进平台后端 → `docs/内存管理/RAM使用分析与优化方案.md`
- **内存预算紧张**:新增任何任务/缓冲/栈前先核算 RAM 余量;改栈或加大局部变量**前后各跑一次** `scripts/stack_report.py`(静态最坏链 + 串口 `[STACK]` 水位)与 `scripts/ram_report.py`(.map 静态占用);**总量红线已机械化**:`mem-budget` 钩子拦 RW_IRAM1/RW_CCM >92%,子系统配额 / CCM 台账 / 趋势见 `docs/内存管理/RAM预算台账.md`(**每次 `/keil-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 消费者前的强制门禁(RS-485 接线门禁)**:先确认驱动契约已修——TPAFE 三条 ✅(原子快照取数 / 帧序号取代就绪电平标志 / FRSTDATA 数据有效性校验);RS485 已修两条 ✅,但 **单缓冲包撕裂、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` 每次 `/keil-build` 后被 UV4 自动改写,改动一律不入库**(XML 归一化 + `PackID` 降到本机 DFP 版本);判据与例外(真的增删源文件才提交)→ **`keil-build` skill**
## 硬件引脚
- **完整引脚表(含 SD2506 I2C1、SWD、调试口)→ `docs/架构设计/系统架构与硬件规格.md` §2.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**(登记簿条目「双转换器无共同采样触发」)
## 模块约定
- **CH395F 单连接 socket**:监听与数据通道复用同一 Socket(CONNECT 后转数据通道,DISCONNECT/TIMEOUT 后重开 → LISTEN);所有 CH395F 中断在 `net_poll()` 内经 `GET_GLOB_INT_STATUS` 集中读取并清除,**不得**在 ISR 或其他地方单独处理。⚠ 全局寄存器**整体读即清、芯片无按位清除手段**,而 net_poll 只在全局位为 1 时才读该 Socket 的 SINT → "顺手全量清一遍"等于把别的在用 Socket 的门焊死(B3 实证)。**现存读清点全量审计 + 新增一处读清的三条准入判据 → `docs/网络/网络自愈架构设计.md` §4.10**
- 所有 `net_*` socket 操作经 `netMsgQueue` 在 netTask 串行执行;非 netTask 任务直接调 `net_*` API,勿直接碰 CH395F 驱动
- **`net_*` 公开 API 的错误码经"任务通知值"回传**:`net_msg_t` 是**按值拷入队列**的,netTask 改的是自己那份副本,调用方永远看不到 → 错误码搭 `xTaskNotify()` 通知值回来。**勿给 `net_msg_t` 加"回传用"字段**(调用方只会读到栈垃圾)
- **单连接 socket 的断开语义**:EOF 时 net 层已 `auto_relisten`,应用层**不得**在 EOF 时 `net_close` 该 fd(会连监听一起毁掉 → 端口"连接被拒");仅当数据 fd ≠ 监听 fd(模拟器多连接后端)时才关
- 网口配置:`/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/任务规划/README.md` 这类待办登记簿,事项修复/关闭后**直接从表中删除该行**(不保留"已消化"行)——结论与证据应已沉淀进对应模块文档/问题记录。销项时必须:① 先核对目标文档确已含完整记录 ② 同步清理其他文档对该编号的引用 ③ 改完跑 `check_doc_links.py --all` + pre-commit。**新条目标识(用户裁定 2026-10-14;2026-10-29 收口为一律不再发号)**:`#` 列填**语义短语**(如 `沉降窗 800ms 上限只留痕`),**不再新增字母+数字编号**——"编号不回收"只写规则却不留痕迹就不可执行(销项删行后仓库里毫无痕迹,2026-10-14 实测就发生过「销项号被重新取走」的撞号事故),语义标识自带唯一性;**存量编号(登记簿 N-x / S-x / T-x 与专项 A-x)已于 2026-10-26 全部转成语义短语**,旧号进 `scripts/archive/id_gate/id_aliases.json` 标 `retired-to-phrase`、不得再指;**「数据可信」任务卡号(中文前缀「卡」+ 数字,14 个)已于 2026-10-27 随卡销项整族清退**,换算表 `scripts/archive/id_gate/编号治理历史.md` §9.3——这一族**刻意不进 `id_aliases.json`**:中文前缀不进 check_ids 语料,登记了机器也看不见,**没有机器识别面**,停发只靠规范与评审;**已随销项消失的号永久作废**、不得复用 → `docs/任务规划/README.md` §0
- 图形走 **`draw-diagram` skill**:精确布局图用 `tools/arch_diagram/gen_*.py` 生成器(产物放 `docs/images/`,**脚本与产物同一笔提交**:改生成器没重跑,下一个碰巧重跑的人会看到一段「不是我改的」diff——已机械化 `generated-fresh`,重跑到临时目录逐字节比对 → `docs/文档规范.md` §4.4);拓扑图用 Mermaid;**勿手绘 ASCII 框图**。文件引用关系图走 `tools/refgraph`(`& 'tools\refgraph\gen_refgraph.bat'`)
- **改名 / 挪目录一律走 `scripts/rename_ref.py`(先 dry-run,确认后 `--apply`)**:`git mv` + 自动改写全库引用(反引号引用 / Markdown 链接 / 图片 `../` 层级换算 / `#include` / `uvprojx` `<FilePath>` / `CMakeLists.txt`),落盘后自动跑 `check_doc_links --all` 复核——手工扫引用既慢又漏(一次改名历史上波及 10~30 文件)。引用章节用「`路径.md` §N.M」**紧邻写法**(`doc-links` 校验编号是否还在:紧邻/裸名/前缀/别名/自引用**全部拦提交**(钩子带 `--sections-strict`);每次跑打印「可判定比例」,`--section-todo` 出待改清单,昵称歧义就登记 `scripts/section_aliases.json` 或写清楚)→ `docs/文档规范.md` §3.5 / §3.6。**编号改名走 `scripts/rename_ids.py`**(映射表驱动、**显式 scope**、干跑默认、自动写 `scripts/archive/id_gate/id_aliases.json` 的 scoped 旧号→新号条目;`[示例]`/`[计划]` 行与变更记录表不改写;**绝不全局查找替换**——同一裸形字面量可能同时属多个族,全局替换等于造新撞号,实测 `S3` 三义)
- 文档写作规范(编码/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` |
| **每个提交门禁一篇详解**(查什么 / 拦下过什么真事 / 限制与盲区 / 基线口径) | `docs/门禁说明/README.md`(52 个钩子 → 30 篇索引,其中 2 篇是退役留档 + 1 指针) |
| **还想给东西发编号?**(**2026-10-29 用户裁定:一律不发**,写语义短语;§4.1 五个存量族的既有号只读引用——「停发」不等于「暂缓」,也不等于「可以清号」)/ 编号门禁现状(**两道都已退役**:全库八项校验 2026-10-28、只查新增行 2026-10-29;此后无机器面,只靠 §1 与评审) | `docs/编号规范.md`(§1 默认答案、§4.1 存量族〔穷举〕与『已停发』栏、§8.5 手工普查与恢复机器面) |
| 提交门禁通用机制(阶段 / stash 陷阱 / 触发面 / 增量拦截三段式) | `docs/代码规范/pre-commit使用说明.md` §2 |
| 代码规范 V1.0 / cppcheck 口径 | `docs/代码规范/嵌入式C语言代码规范_V1.0.md`、`cppcheck使用说明.md` |
| 自动化测试 / 实机测试 / 板级套件 | `docs/测试说明/流程与规范/自动化测试流程.md`、`实机测试实施指南.md`、`board-test-suite` skill |
| 烧录与复位语义 | `docs/测试说明/测试工具/ST-LINK_CLI_复位使用说明.md` |
| 串口助手 | **`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` |
| 多任务并行开发(开空间 / 合并回收 / 台架与构建排队);**DSH worktree 插件的仓内树怎么接上**(`.dsh-worktrees/` 缺 `.venv`、`plugin adopt`、锁归属 `pw:<name>`) | **`task-worktree` skill**(`@task` 仅为文档别名非可执行命令,显式调用 `/task-worktree`,用到自动加载;插件桥 → 其 F 节)、`docs/环境与部署/并行任务工作树方案.md` §3.4 |
| 换机 / 工具路径环境变量 | `docs/环境与部署/` |
| 远程 CI(Gitea Actions,**只跑本机一台 runner**) | [`docs/CI/README.md`](docs/CI/README.md) |