docs(AGENTS): 入口重构——补宏观概况与边界块,瘦身51KB→25.8KB,子域知识入skills
CI / L0 静态门禁 (pre-commit) (push) Has been cancelled
CI / L0.5 主机侧单测 (MSVC) (push) Has been cancelled

- AGENTS.md 51,067→25,828 B(−50%):审计→迁移→删除,逐节 diff 零信息损失
- 按调研(agents.md 官方/aihero/airflow)补「项目概况」:一句话定位、
  数据主线、顶层目录地图、分层边界;平台约束 17 条分 5 组
- 新增「边界(Ask first / Never)」:git commit/push 须明确指示首次入库
- 串口助手、LVGL 模拟器两节移入 serial-assistant / 新建 lvgl-simulator skill
  (开关语义按 main.c 实锤);LVGL 两条实证坑移入 ui-verify skill
- 9 条入口独有细节迁移至对应设计文档(存储/看门狗/启动顺序/文档规范/
  pre-commit 使用说明/keil-build skill);@test 细节指向自动化测试流程
- 新增 docs/AGENTS编写指南.md:六原则/模板/反模式/维护纪律 + 本仓案例
- 修正 3 处对 AGENTS 串口节的过时引用;docs/README.md 登记新文档
This commit is contained in:
2026-10-11 10:44:50 +08:00
parent 3b90107181
commit eddbedeee8
15 changed files with 364 additions and 118 deletions
+3 -1
View File
@@ -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 由
常驻服务派生,服务启动**之后**设置的用户级环境变量不会自动进来。查
+52
View File
@@ -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 §4)
| 现象 | 根因 |
|---|---|
| 找不到 `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** |
+1 -1
View File
@@ -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「陷阱」节),不是驱动问题。
## 其它项目怎么套
+6 -1
View File
@@ -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
+139 -103
View File
@@ -1,66 +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 / 配置)**已实现并台架验收**;**ADC / RS-485 这两个 DTU 核心采集职责尚未接线到业务消费者**——当前最大功能缺口,接线节奏受 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 保护(软件侧已落地,待接任务)
- **存储**:GD5F SPI NAND → dhara FTL → FatFs → `/log` 运行与自检记录、`/config.ini` 配置、FTP 根目录
- **网络**:CH395F → netTask 串行 socket 栈 → FTP(21) / 对时(8888) / RemoDispBus 远程显示(7003)
- **人机**: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 工作流 · 串口/烧录配置 | 手写 |
- 任一失败即拦截,共 **26 个钩子**(`.pre-commit-config.yaml`,2026-10-11 清点):通用 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` + `lv-conf-sync`(**两份 `lv_conf.h` 特性子集一致**,见 `docs/UI设计/UI_设计架构.md` §1.4 第 10 条)+ `cubemx-survival`(重生成存活审计)+ 主机侧单测 10 个 `watchdog` / `sys-clock` / `cfg-file` / `phasor` / `adc-input` / `vol-lock` / `time-sync` / `adc-health` / `prot-set` / `prot-elm`(均 `-unit-test` 后缀)+ `test-suite-switch`(测试开关=产品构建)。范围仅项目代码(第三方 `Lib/`/`Middlewares/`/CMSIS/HAL/`lftpd`/`dhara`/`MDK-ARM` 排除;`cppcheck`/`coding-standard` 另排除 `test/`,规范检查再排除 CubeMX 生成的 `Src/`/`Inc/`)
- **`simulator/` 已纳入全部钩子**(2026-10-11 修正):顶层 `exclude` 原先整目录挡 `simulator/`,实际效果是把 **21 个本项目自研文件**(模拟器入口 `main.c`、`sim/*` 平台后端、`tests/*.py` 回归脚本、`lv_conf.h`/`dbg_log.h`、3 个 README)挡在门禁外——而那些生成物(`build/`、`SDL2/`、`tests/ref/`、`shot*.{bin,png}`、`__pycache__/`)**已在 `.gitignore` 里**,pre-commit 只看 git 索引,根本不需要重复豁免。纳入当天 `cppcheck` 即揪出 `sim/sim_syslog.c` 一处**真实空指针解引用**(`if (p_epoch == NULL || …) { *p_epoch = 0u; … }`),`coding-standard` 报 61 处(44 处函数左大括号 Allman 风格 / 8 处缺函数注释 / 3 处缺文件头 / 2 处 `__` 前缀保护宏 / 2 处 `sprintf` / 2 处 S11 误报)已全部处理;顺带细化 **S11 只禁"用路径找文件"**——`<sys/stat.h>` 这类尖括号系统头带子目录不再误报(全仓库复核:仅这 2 处项目代码判定变化)。⚠ **写模拟器代码与固件代码同标准**
- ⚠ **pre-commit 4.x 已移除 `always: true`**(实测对 `local` 钩子只告警 `Unexpected key(s)` 并跳过)→ 想要"总是跑"的钩子**必须让 `files:` 覆盖到所有会触发它的文件**,没有兜底开关。例:`lv-conf-sync` 的 `files:` 同时匹配 `Lib/lv_conf.h`、`simulator/lv_conf.h` 与脚本自身,两端任一改动都会跑
- **改写型钩子(`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` 触发)——清单 23 项 + 哈希基线漂移检测,直接列出"被覆盖了哪几处"。**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 DMA 乒乓缓冲**,v2.0 起零忙等)与 `adc_input_raw()`(**按语义** `adc_semantic_t` 取数,不写死下标);映射表自检在 `adc_input_init()`(查每个 STM32 语义通道确实在扫描 rank 里,防"映射到从不扫描的通道 → 恒读陈旧值");`adc_input_init()` 末尾 `HAL_ADC_Start_DMA()` **只挂一次**(此后每拍由硬件自走),失败返回 -2 并上报自检
- **F4 上 ADC1 的硬件触发源与 DMA 流是硬约束(2026-10-10 查 CubeMX DB + 本仓 HAL 头证实,别再凭印象)**:① **ADC1 的外部触发清单里没有 TIM6/TIM7 TRGO**——只有 T1_CC1/2/3、T2_CC2/3/4、T2_TRGO、T3_CC1、**T3_TRGO**、T4_CC4、T5_CC1/2/3、T8_CC1、**T8_TRGO**(+EXTI15);TIM6/TIM7 的 TRGO 在 F4 上**只接 DAC**(`T6_TRGO` 只存在于 CubeMX DB 的 `Expression="STM32F7"` 块里)。故"一个定时器同时驱动两片转换器"必须用 **TIM3**(PSC 83 / ARR 999 → 1 kHz,`TIM_MasterOutputTrigger=TIM_TRGO_UPDATE`)。② **ADC1 在 DMA2 上只挂 Stream0 或 Stream4**(Stream1=ADC3、Stream2/3=ADC2)→ 给 ADC1 选 **Stream4**(空闲)才不会把 SPI1_RX/USART1_RX 挤走;CubeMX 会自动挑 Stream0 并**连锁重排**在用外设,且**流的 NVIC 优先级按流号绑定**、换流即被重置为 **0**(违反"外设中断 ≥5",会抢占 FreeRTOS 临界区)→ 每次改 DMA 流后必须回 NVIC 页核对 `Src/dma.c` 的 `HAL_NVIC_SetPriority`。③ **`DMAContinuousRequests` 必须 ENABLE**(DDS 位;关掉 = 一轮 9 通道只发**一次** DMA 请求,静默丢 8 路,不报错)。④ DMA 目的缓冲**必须在 SRAM**(ADC1 走 DMA2,**DMA2 不可达 CCM 0x10000000**)→ 加大缓冲后跑 `.map` 复核地址。⑤ 消费侧用 **乒乓双缓冲**(DMA 长度 = 2×9,半传输/传输完成两个回调各代表一帧就绪),半区下标由**完成计数的奇偶**推出(只读一个 32 位计数 → 与 ISR 无竞态);STM32 侧数据因此**固定滞后 1 拍**
- **STM32F4 ADC 上电必须自校准(2026-10-10 补,此前项目从未做过)**:CubeMX **不生成**这一步,且本仓库裁剪版 HAL **没有** `HAL_ADCEx_Calibration_Start`、裁剪版 CMSIS 头 `stm32f407xx.h` **没有** `ADC_CR2_CAL`(bit3)→ 只能按 RM0090 §13.3.6 手写:`Src/adc.c` 的 `adc1_calibrate()`(`USER CODE BEGIN 1` 区,自建 `ADC1_CAL_BIT`)+ `MX_ADC1_Init()` 末尾 `USER CODE BEGIN ADC1_Init 2` 调用;时序 = 清 ADON→等生效→置 ADON→置 CAL→等 CAL 由硬件清零。缺它**不报任何错**,只是读数带失调(实测校准让 ADC1 六路同向偏移 +10~+63 LSB,TPAFE 侧不变)。审计项 `adc1-calibration-call` / `adc1-calibration-impl` 兜底。⚠ 顺带一条编译坑:**块注释里别写 `JEXT*/EXT*` 这类串**——其中的 `*/` 会提前闭合注释,报 `#7: unrecognized token`
- **接 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 / **AboveNormal**——P1-5 方案 a 提优先级,`USER CODE` 区手工创建,同 timeSyncTask)+ 栈预算(静态 **560 B** / 余 **72%**,B1 实测;P1-5 后复测 **832 B** / 余 **59%**,见下)+ 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` → 7 任务(`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 软件侧(**尚无任务调用 `prot_elm_run()`**——链接器移除、零足迹,板端接线随 protectTask 立项走 E-59 同款门禁)。→ `App/algo/README.md`
- 全景 → `docs/架构设计/项目架构总览.md`;启动时序与内核前模式 → `docs/架构设计/项目启动顺序管理.md`
<!-- TASK_TABLE:BEGIN generated by scripts/gen_task_table.py -->
| Task | 栈 (words) | 优先级 |
@@ -74,70 +61,119 @@ STM32F407ZGTx @168MHz(HSE 8MHz → PLL M=4/N=168/P=2);**仅 Keil MDK-ARM v
| adcTask | 640 | AboveNormal |
<!-- 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%);同日 P1-5 后复测 adcTask **832 B / 余 59%**(新增 `osThreadFlagsWait` 调用链,仍高于 1024 B 底线);2026-10-11 L2 相量 + L1-6 有效性校验上链后静态最坏链 **1080 B**(`adc_health_report`→`syslog_post` 控制台回显尾)→ **adcTask 抬栈 512→640 words**,复测余 **1480 B / 57%**,堆 min_ever 7152→6640 B)
(实际值以 `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 诊断 → **启动 TIM3 1 kHz 采样节拍**(`HAL_TIM_Base_Start_IT(&htim3)`,2026-10-10;ISR 内触发 CONVST + 同一事件经 TRGO 触发 ADC1,见下)
- **就绪屏障**(`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;P1-5 事件驱动消费;v2.8 节拍源 TIM3 + STM32 侧 DMA,2026-10-10**):**纯消费者**——每拍调 `adc_input_poll()`(`App/algo/adc_input.c`)= TPAFE `tpafe5160_snapshot()` 临界区原子快照(8 通道)+ **消费一帧 STM32 ADC1 DMA 双缓冲**(L1-3)→ 帧统计 + 每 10 s 心跳日志;取数一律经 `adc_input_raw()` **按语义**(L1-4,不写死下标);只取数、不做保护运算(V0.1 仅 TPAFE 三路 `UA`/`UN`/`IA` 测量有效,见 N-18)。**采样节拍(1 kHz)由 TIM3 更新事件硬件提供**(`Src/main.c` 的 `HAL_TIM_PeriodElapsedCallback` 在 TIM3 分支内调 `tpafe5160_start_conv_irq()`;同一更新事件经 **TRGO** 触发 ADC1 一轮 9 通道转换 → 两片转换器同源对齐);**消费唤醒与该节拍同源**——同一分支再调 `adc_tick_irq_notify()` 置位 `ADC_WAKE_FLAG`,任务用 `osThreadFlagsWait(..., osWaitForever)` 等唤醒(不再 1 ms 轮询;实测 `idle` 由 1~649 降到 **0**)。⚠ ISR 侧**只能**走 `adc_tick_irq_notify()`——它内含"消费者就绪"门(TIM3 在 `app_main_init` 就启动,比调度器早 ≈100 ms)。手写于 `Src/freertos.c` 的 `USER CODE` 区(同 timeSyncTask,**640 words** / **AboveNormal**——2026-10-11 L2 相量 + L1-6 有效性校验上链后静态最坏链 1080 B,512 words 余量跌破 1024 B 底线故抬栈),纳入 IWDG `required_mask`(`TASK_WD_ADC`)。详见 `docs/子系统设计/ADC采样节拍设计说明.md` §10、`App/algo/README.md`
- **采集与算法层(L1/L2/L3)落在 `App/algo/`**(2026-10-10 建):现有 `adc_input.c/h` = L1(语义通道映射 + STM32 ADC1 DMA 消费)、`adc_health.c/h` = L1-6(采样有效性判定:静默/丢帧/单调性/错误增量,纯逻辑免桩单测)、`sample_ring.c/h` + `phasor.c/h` = L2(40 点采样环 + 全周傅氏 5 实例 + 双区交换,2026-10-11 台架闭环)、`prot_set.c/h` + `prot_elm.c/h` = L3 软件侧(Q10 定值模型 + 校验和自检闭锁 + 通用"滞回+定时限"元件框架 7 实例,2026-10-11;**尚无任务调用 `prot_elm_run()`**——链接器移除、零足迹,板端接线随 protectTask 立项走 E-59 同款门禁)。新算法模块一律进这里,勿塞进 `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)
- 任一失败即拦截,共 **26 个钩子**(逐个详解 → `docs/代码规范/pre-commit使用说明.md`):通用 5 + `gitlint` + `cppcheck` / `test-cppcheck` + `no-mojibake` + `coding-standard` + 文档三件套 `doc-links` / `task-table` / `mem-numbers` + `lv-conf-sync`(两份 `lv_conf.h` 特性子集一致)+ `cubemx-survival` + 主机侧单测 10 个(`-unit-test` 后缀)+ `test-suite-switch`
- **范围 = 项目代码**:第三方(`Lib/`、`Middlewares/`、CMSIS、HAL、`lftpd`、`dhara`、`MDK-ARM`)排除;`cppcheck`/`coding-standard` 另排除 `test/`,规范检查再排除 CubeMX 生成的 `Src/`/`Inc/`。**`simulator/` 已纳入全部钩子**(2026-10-11)——生成物只靠 `.gitignore` 挡,**不在 `exclude` 里写目录名**(教训见使用说明 §5.5)
- ⚠ **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 静态占用)→ `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 <秒>`
- **模拟器代码与固件同标准受检**(2026-10-11):`simulator/` 下自研文件全部纳入 pre-commit(`cppcheck` / `coding-standard` / 乱码 / 改写型钩子 / `doc-links` / `lv-conf-sync`),生成物只靠 `.gitignore` 挡、**不在 `exclude` 里写目录名**(详见 `docs/代码规范/pre-commit使用说明.md` §5.5)。改 `simulator/lv_conf.h` 会触发 `lv-conf-sync`;改模拟器 C/H 会触发 cppcheck + 规范 V1.0
- 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 删除**,旧档存在则被忽略)→ `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/待处理内容.md` 这类待办登记簿,事项修复/关闭后**直接从表中删除该行**(不保留"已消化"行)——结论与证据应已沉淀进对应模块文档/问题记录。销项时必须:① 先核对目标文档确已含完整记录 ② 同步清理其他文档对该编号的引用 ③ 改完跑 `check_doc_links.py --all` + pre-commit
- 图形走 **`draw-diagram` skill**:精确布局图用 `tools/arch_diagram/gen_*.py` 生成器(产物放 `docs/images/`,脚本与 .svg 一并提交);拓扑图用 Mermaid;**勿手绘 ASCII 框图**。文件引用关系图走 `tools/refgraph`(`& 'tools\refgraph\gen_refgraph.bat'`)
- 文档写作规范(编码/BOM/链接/表格/一致性钩子)→ `docs/文档规范.md`;**改本文件前先读** `docs/AGENTS编写指南.md`(入口文件的取舍与维护纪律);全量索引与推荐阅读顺序 → `docs/README.md`
## 边界(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 26 个钩子逐个详解 | `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/待处理内容.md` |
| 换机 / 工具路径环境变量 / CI | `docs/环境与部署/` |
+109
View File
@@ -0,0 +1,109 @@
# AGENTS 编写指南——一份好的 AGENTS.md 应该怎么写
> **定位**:回答"AGENTS.md 该写什么、不该写什么、怎么维护"。适用于本仓库 `AGENTS.md` 与任何采用 [AGENTS.md 开放格式](https://agents.md/) 的项目。
> **依据**:官方规范 + 两篇实践指南 + 一个标杆范例 + 本仓库实测(51 KB → 25 KB 瘦身与重构,见 §8)。
> **关联**:文档体系通用约定 → [`文档规范.md`](文档规范.md);人类索引 → [`README.md`](README.md)。
---
## 1. 定位:它是谁的什么
| 文件 | 读者 | 内容 |
|---|---|---|
| `README.md` | 人类贡献者 | 项目介绍、快速上手、贡献指南 |
| `AGENTS.md` | **AI 编码代理** | 构建/测试命令、约定、约束、坑——那些"放 README 会碍事、但 agent 每次都需要"的上下文 |
[官方定义](https://agents.md/):*"Think of it as a README for agents"*、*"anything you'd tell a new teammate belongs here"*。它没有必填字段、纯 Markdown,但**位置即命运**——它坐在每次请求的上下文最前面,写什么就得为什么付费(§3 P3)。
## 2. 一句话定位(写之前先回答)
文件第一行必须让 agent 回答"**这是什么东西、给谁用、跑在哪**"。[aihero 指南](https://www.aihero.dev/a-complete-guide-to-agents-md)的说法:这句话是**锚定后续每个决策的角色提示**——"嵌入式固件"与"Next.js 网站"会让 agent 对同一个"加个日志"请求做出完全不同的权衡。没有这句话,后面所有规则都失去解释框架。
反例:直接以"编码规范"开头——agent 知道规矩,不知道为什么有这些规矩。
## 3. 六条核心原则
| # | 原则 | 为什么 | 反例信号 |
|---|---|---|---|
| P1 | **一句话定位打头**(§2) | 所有规则的锚点 | 文件从规范讲起,产品是什么要翻三节才拼出来 |
| P2 | **宏观先行**:概况 → 目录地图 → 架构边界 → 再是操作规则 | [airflow 范例](https://github.com/apache/airflow/blob/main/AGENTS.md)把 Repository Structure 与 Architecture Boundaries 放在编码规范之前;agent 先知道"代码怎么摆、边界在哪"才不会把算法塞进任务文件 | 规则堆完才出现"架构要点",或根本没有宏观视图 |
| P3 | **指令预算**:模型只能稳定遵循约 150~200 条指令,且**每个 token 每次请求都加载**([Humanlayer](https://www.humanlayer.dev/blog/writing-a-good-claude-md) / aihero) | 膨胀不是"浪费",是**稀释**——不相关的指令会实际降低执行质量 | 文件 >30 KB;同一主题超过 5 条规则 |
| P4 | **渐进披露**:入口只留面包屑,细节进 `docs/` 与 skills;大仓用嵌套 AGENTS.md(就近生效,[官方推荐](https://agents.md/)) | 用到才加载 = 零常驻成本。本仓库把串口助手、模拟器整节移进 skill 后入口各减数 KB 而能力无损 | 入口里出现完整命令表/参数表/FAQ |
| P5 | **描述能力,不写易漂移路径**(aihero):目录地图只到**顶层稳定边界**,细节让 agent 自己 `ls` | 深层路径随重构漂移,agent 会**拒绝相信眼前的真实代码**去服从过期地图 | 入口罗列到三级以下的文件清单 |
| P6 | **每条 = 禁令/事实 + 一句为什么 + 指针**(本仓库实测) | "为什么"让 agent 能外推到未列举的场景;只有禁令会催生新坑的新禁令(回 P3 雪球) | 只有"禁止 X"没有半句原因;或大段叙事讲踩坑史 |
## 4. 推荐结构(模板)
综合[官方常用节](https://agents.md/)(Project overview / Build & test / Code style / Testing / Security)与 airflow 实践:
```
# 项目名
一句话定位(P1)+ 平台/工具链一行 + 最高频命令一行(如"改完必须 build")
## 项目概况 产品现状一句话(含最大缺口)· 数据/调用主线(能力级,P5)
· 顶层目录地图(目录|职责|可改性 三列)· 分层边界
## 架构要点 运行时视图(任务/线程/启动顺序),机器生成表用标记包裹(§5)
## 操作规则 编辑铁律 · 构建与测试命令(含退出码判读)· 提交门禁
## 领域约束 平台/硬件/协议陷阱——每条带为什么(P6),按主题分组
## 索引 按主题查哪份文档(表格;子域知识入口=skills)
```
要点:**构建/测试命令必须给全**(官方:agent 会自动执行文中列出的检查命令并在完成前修复失败);**边界用"必须/禁止"级措辞**——airflow 的 `Never` / `Ask first` 两栏清单是范例。
## 5. 机器可读契约(本仓库经验)
入口文件里凡是被脚本/钩子消费的部分,用**显式标记**声明契约,人改脚本改都撞不上:
- 生成表用锚点包裹:`<!-- TASK_TABLE:BEGIN generated by scripts/gen_task_table.py -->`,由 `--check` 钩子校验与代码一致(同类:`MEM_SNAPSHOT:BEGIN/END`)→ `代码规范/pre-commit使用说明.md`
- **段名是跨文档引用键**:其它文档以「AGENTS.md 平台约束」这类**段名**做引用,改名 = 静默断链。重构时段名保持稳定,删节可以、改名要全库 grep 引用
- 入口自身纳入 `doc-links` 类门禁:文中路径全部可验证存在
## 6. 不该进入口的内容(反模式清单)
| 反模式 | 为什么坏 | 去处 |
|---|---|---|
| **规则雪球**(每次 agent 犯错加一条) | [aihero](https://www.aihero.dev/a-complete-guide-to-agents-md):数月后成 ball of mud,互相矛盾、无人通读 | 同类规则合并成一条带"为什么"的总则 |
| **自动生成全量 dump**(`/init` 一把梭) | 生成器追求"面面俱到"而非克制,等于开局就把预算花光 | 生成后必须人工按 P3 裁剪 |
| **历史叙事**(日期、commit 号、踩坑过程) | 决策依据进 changelog/问题记录;入口只留结论 | `docs/问题记录/`、文档变更记录 |
| **从 docs 抄全表**(引脚表、命令表、FAQ) | 双写必漂移,违反单一事实源 | 一行指针(本仓库「完成即销项」约定) |
| **个人偏好**(某人的缩进口味) | 每次请求全员付费 | 个人级 agent 配置 / editorconfig |
| **过期路径与旧工具名** | 比没有更糟——agent 会服从它 | 定期瘦身(§7) |
## 7. 维护纪律
1. **单一事实源**:同一事实只在一处详述,其余位置用链接指向(本仓库 `docs/文档规范.md` 同此要求)。
2. **瘦身方法论**(本仓库实测,先审计后动刀):全量条目普查 → 逐条核对去处(docs/skills 是否已完整承接)→ 缺口先迁移 → 再删除 → 跑链接/一致性门禁。**禁止**没核对去处就删。
3. **skills 是披露层**:子域工作流(构建工具、串口、模拟器…)整段进 skill,入口索引行留名——目录常驻、全文用到才加载。
4. **改完跑门禁**:链接存在性、生成表一致性、表格格式、编码(本仓库 `check_doc_links.py --all` + `pre-commit`)。
5. **体量红线**:入口超过 ~30 KB 就触发瘦身流程(P3);新增一节前先问"这条是否**每次**都需要在场"。
## 8. 本仓库案例:51 KB → 25 KB
| 步骤 | 动作 | 结果 |
|---|---|---|
| 1 普查 | 脚本统计各节行数/字节数,49 个主题逐条核对 docs 承接情况 | 48/49 主题文档已有完整承接,9 条入口独有 → 先迁移 |
| 2 迁移 | 叙事史/日期/commit 号回写各设计文档;串口、模拟器两节移入 skills | 零信息损失(逐节 diff 验证) |
| 3 删除 | 入口重写:禁令 + 一句为什么 + 指针 | 51,067 → 21,795 B(−57%) |
| 4 重构 | 按 P1/P2 补一句话定位 + 项目概况(数据主线/目录地图/分层边界),平台约束 17 条分 5 组 | 25,306 B,段名与机器标记全保留,门禁全绿 |
**教训**:瘦身最大的风险不是删错,而是**删了唯一副本**——所以"先核对去处"必须先于任何删除;第二大风险是**改段名断引用**——跨文档按段名引用在 Markdown 链接检查里不可见。
## 9. 质量自查清单
- [ ] 第一行能否让新 agent 说出"这是什么、跑在哪、用什么工具链"?
- [ ] 前五节之内能否回答"代码怎么摆、哪些目录不能碰"?
- [ ] 每条规则是否带"为什么"(或本身自明)?
- [ ] 有没有任何一条在 docs/skills 已有更全版本?(有 → 删,留指针)
- [ ] 构建/测试命令是否可直接复制执行、含退出码判读?
- [ ] 生成表/锚点标记是否完好?段名是否未改(或已全库同步引用)?
- [ ] 文件多大?>30 KB 触发瘦身。
- [ ] 最近一次 agent 犯错,是"缺规则"还是"规则太多没执行"?(后者 → 删规则而不是加规则)
## 10. 参考资料
| 来源 | 要点 |
|---|---|
| [agents.md 官方规范](https://agents.md/) | 定位/常用节/嵌套文件/60k+ 项目案例(openai/codex、apache/airflow 等) |
| [A Complete Guide To AGENTS.md](https://www.aihero.dev/a-complete-guide-to-agents-md)(Matt Pocock) | 指令预算、渐进披露、能力>路径、反雪球、skills 披露层 |
| [Writing a good CLAUDE.md](https://www.humanlayer.dev/blog/writing-a-good-claude-md)(Humanlayer) | "instruction budget"概念出处:前沿模型约 150~200 条指令 |
| [apache/airflow AGENTS.md](https://github.com/apache/airflow/blob/main/AGENTS.md) | 标杆结构:Repository Structure / Architecture Boundaries / Ask first–Never / References |
+3
View File
@@ -20,6 +20,7 @@ docs/
├── 测试说明/ 测试流程、模块测试指南、工具说明
├── 代码规范/ 编码规范与静态检查门禁
├── 文档规范.md 本文档体系的规范(命名/链接/门禁/结构)
├── AGENTS编写指南.md 如何写好 AGENTS.md(入口文件的取舍、结构与维护纪律)
├── 内存管理/ RAM 预算、分配策略与优化调研
├── 问题记录/ 模块陷阱台账、专项审计与项目评审报告
├── 环境与部署/ 环境搭建与部署核对清单
@@ -168,6 +169,7 @@ docs/
| 文档 | 内容 |
|---|---|
| [`文档规范.md`](文档规范.md) | **文档体系强制约定**:目录/命名(无空格)、编码换行、链接形态(互链用根相对、图片用目录相对)、`[计划]` 标记、门禁三档、结构要素与常见错误速查 |
| [`AGENTS编写指南.md`](AGENTS编写指南.md) | **如何写好 AGENTS.md**(agent 入口文件):定位与一句话锚点、六条核心原则(指令预算/渐进披露/能力>路径)、推荐结构模板、机器可读契约、反模式清单、瘦身方法论 + 本仓库 51KB→25KB 实测案例与外部标杆调研(agents.md 官方 / airflow / aihero) |
### 7.2 内存管理(**新增任务/缓冲/栈前必读**)
@@ -266,6 +268,7 @@ docs/
| 日期 | 变更 | 说明 |
|---|---|---|
| 2026-10-11 | 新增文档 | 新建 [`AGENTS编写指南.md`](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) |
+4 -4
View File
@@ -314,11 +314,11 @@ cppcheck 是开源 C/C++ 静态分析工具,**不运行代码**,通过语法
| 项 | 说明 |
|---|---|
| **触发条件** | commit message 标题超 100 字符 / 含 body-is-missing 警告 |
| **触发条件** | commit message 标题超 100 字符 / **正文单行超 80 字符** / 其他格式规则 |
| **处理方式** | **阻止提交**(commit-msg 阶段) |
| **当前规则** | 宽松:不强制 Conventional Commits,兼容中文标题 |
| **典型报错** | `T1 Title exceeds max length (159>100)` |
| **修复方式** | 精简 commit message 标题到 100 字符以内,或拆分为多行(标题+空行+body) |
| **当前规则** | 宽松:不强制 Conventional Commits,兼容中文标题。配置源是仓库根 `.gitlint`(**不是** hook args):`ignore=body-is-missing`(**不强制写正文**)+ `[title-max-length] line-length=100`;正文行宽走 gitlint 默认 **80**(未关) |
| **典型报错** | `T1 Title exceeds max length (159>100)`、`B4 Line too long (87>80)` |
| **修复方式** | 标题压到 100 字符内(中文长标题极易超限);长说明一律用**短标题 + `git commit -F <文件>` 多行正文**,正文每行 ≤80 字符。消息文件必须 **UTF-8 无 BOM**——用 `-F 文件` 而非 `-m "…"`,防终端代码页转码破坏中文 |
### 2.15 lv-conf-sync — 两份 `lv_conf.h` 特性子集一致性(2026-10-11 加)
@@ -43,7 +43,7 @@ HAL 并发语义(本仓库实现实测,`stm32f4xx_hal_uart.c:1135`):轮
| # | 约束 | 出处 |
|---|------|------|
| K1 | 内核启动前禁止创建/使用任何 FreeRTOS/CMSIS-RTOS 对象 | AGENTS.md 平台约束(2026-09-20 启动失败实证根因)→ 本模块内核前纯 BSS/寄存器机制 |
| K2 | 禁止 `malloc/calloc/realloc`(C 堆仅 512B) | AGENTS.md P0-1 → 全部状态静态 BSS |
| K2 | 禁止 `malloc/calloc/realloc`(C 堆仅 512B) | AGENTS.md 平台约束(禁 C 库堆分配条)→ 全部状态静态 BSS |
| K3 | HAL 生成文件(`Src/`、`Inc/`)不含应用代码 | 2026-09-25 迁移动机 → 实现位于 `App/log/` |
| K4 | 生产者阻塞时间有界 | 最多 `DBG_CON_WAIT_MS`(200ms),日志不得阻塞业务 |
| K5 | RAM 预算紧张,新增静态内存必须核算 | §8 |
@@ -518,6 +518,20 @@ python scripts/check_cubemx_survival.py --write # 确认改动无误后重建
> **工作流**:每次 CubeMX 重生成后**立刻**跑一次该脚本——它直接列出"被覆盖了哪几处 + 修复提示",比事后靠故障现象反推快得多。
### 10.4 ARMCC `__asm` 的三条语法约束(2026-09-29 踩坑)
> **出处**:自 `AGENTS.md`「ARMCC 内联汇编语法」条目迁入(AGENTS.md 瘦身,不再当事实存放地)。4 个 Fault Handler 是手工 `__asm`(§10.2 的 L2 条);ARMCC 的内联汇编只有一种合法形态,以下三种写法全部实测踩坑:
| # | 尝试写法 | ARMCC 实际报错 | 根因 |
|---|---|---|---|
| ① | `__asm volatile ("MRS %0, lr" : "=r" (x))` | `#18: expected a ")"` | 这是 **GCC 内联汇编语法**,ARMCC 不支持 |
| ② | `__asm { MRS x, lr }`(块内联) | `#20: identifier "lr" is undefined` | 块内联把寄存器名当 **C 标识符**解析 |
| ③ | `__asm` 块内 `ADR r1, xxx_tag` 取 C 侧字符串/表格 | `A1516E: Bad symbol 'xxx_tag'` | `__asm` 块里**取不到 C 的数据符号**;去掉 `static` 也无效——汇编器只认自己作用域内的符号 |
**结论 / 可用写法**:整个函数写成 `__asm void f(void) { ... }`;要传"字符串/表格"这类 C 侧数据,就让 `__asm` 只取**寄存器参数**然后 `B` 到一个普通 C 函数、由 C 侧补上——所以 4 个 Fault 的故障名必须由 C 侧 per-fault 入口 `fault_entry_{hard,mem,bus,usage}` 带上,`__asm` 里不取 C 变量。
**覆盖 CubeMX 生成的同名函数**(对 §10.2 那条的补充):用 `#define f f_Gen`(**写在生成实体之前**)把生成实体改名,链接器会把未被引用的 `f_Gen` 自动移除(`.map` 可见 `Removing ... (N bytes)`)——**零代码开销且不删生成区任何一行**。
---
## 11. 任务级饥饿的诊断方法与实测基线(Task #32 沉淀,2026-10-10)
@@ -663,3 +677,4 @@ enter/exit 段还能读出"此刻仍在段内已多久"——**永久阻塞的
| 2026-10-10 | **adcTask 纳入监视(B1):必需掩码 `0x37`→`0x77`** | B1(ADC 采集链路)落地时新增 `adcTask`(`App/task/adc_task.c`:1 ms 采样节拍触发 CONVST + 消费 `tpafe5160_snapshot()`)并纳入 IWDG 监视:`task_wd_id_t` 新增 `TASK_WD_ADC = 6`;`wd_required_mask()` 产品 `0x37`→`0x77`、`ENABLE_HW_LAYER_TESTS` `0x35`→`0x75`(adcTask **不依赖任何就绪屏障** → 任何构建都置位)。adcTask 主循环每 1 ms `task_wd_beat(TASK_WD_ADC)`;**不做 NAND I/O → 不登记 FTL 心跳路由**(无需 `task_wd_bind()`)。主机侧单测 `beat_all()` 与掩码断言(`tc_003`/`tc_005`/`tc_011` 的 `0x37`→`0x77`)同步,39 断言全绿。任务由 `Src/freertos.c` 的 `USER CODE` 区手工创建(同 timeSyncTask,不在 `.ioc` 里),栈 512 words、优先级 Normal,`stack_report.py` 实测最坏链 504 B(余 1544 B / 75%)。见 `docs/算法/继电保护功能复现规划.md` §7 |
| 2026-10-10 | **B1 `@test` 实机闭环:adcTask 掩码真机验证** | 台架闭环(`.dsh/skills/serial-boot-test/scripts/run_boot_test.py`)确认 `adcTask` 纳入掩码后**无 adc 相关误复位**:`[STACK] adc peak_used=496/2048 free_min=1552 (75%)`、`frames/seq` 每 10 s 严格 +10000(=1000 Hz)、`idle=0`/`frst_err=0`、全片回读 `No difference found`。启动早期两次 `MISS start snap=0x67 req=0x77` 仅缺 bit4(`timeSync` 未启动,属正常就绪窗;adc 的 bit6 自始置位),掩码随后收敛、全程无复位。同轮另修 `dbg_console_writer_ready()` 门控(adcTask 周期日志在写者注册前抑制,见 `控制台单写者日志设计说明.md`) |
| 2026-10-10 | **新增 §11:任务级饥饿的诊断方法与实测基线** | 承接 Task #32(`task stall: mask=0x67 req=0x77`,缺 `TASK_WD_UI`)结案后的沉淀——原分析文档 `docs/问题记录/UI任务看门狗停摆分析.md` 按"完成即销项"删除,可复用部分并入本文:**§11.1** 三条易误读的判读口径(启动期 `MISS start 0x67` 良性、`miss=` 是累计不是连续、`reset cause` 不能区分 IWDG 与台架);**§11.2** 诊断四步(先看掩码缺几位 → 量化占空比 → 用计数速率反证空转 → 用 `[PROF]` 指认占用者;本例否掉"CPU 饱和",实测 10%~13%);**§11.3** 三条结构性脆弱点(uiTask 唯一 BelowNormal/心跳依赖 netTask 往返/自定截止期不可恢复)与两个顺带修掉的**正确性**缺陷(无新帧仍压环 ≈3% 频率失配、失败拍静默沿用旧值);**§11.4** `[PROF]`/`[PROF2]` 全字段语义 + v2.8 基线(含 `netp=0` 是真实值、`adcm` 峰值来自心跳日志、`uip` 地板 = LVGL 帧周期)与三机理判读口诀;**§11.5** 三条教训(门控须留低频兜底、门控会消灭暴露路径、事件驱动要核对节拍源与消费者创建时刻);**§11.6** 明确不做的四条及理由。§9.1 的 R7 行改指本文 §11.3 |
| 2026-10-11 | **新增 §10.4:ARMCC `__asm` 的三条语法约束** | AGENTS.md 瘦身:「ARMCC 内联汇编语法」条目的完整细节(GCC 语法报 `#18`、块内联把寄存器名当 C 标识符报 `#20`、`__asm` 取不到 C 数据符号 `A1516E`、可用写法 = 整函数 `__asm void f(void)` + `B` 到 C 入口、`#define f f_Gen` 让位机理)迁入本文,AGENTS.md 只留一行禁令与指针 |
+4 -2
View File
@@ -57,8 +57,10 @@
| 文件尾 | 必须以单个换行结束(`end-of-file-fixer`) |
| 乱码 | 禁止出现 `U+FFFD`(替换字符,字节序列 `EF BF BD`) |
> ⚠️ **本仓库 BOM 铁律**(见 `AGENTS.md`):C 源码中 `Drivers/BSP/UI/*`、`App/task/ui_task.c`、`App/cfg/cfg_file.c`、`Lib/lv_conf.h` 等**带 BOM**,被工具改写后 BOM 会静默丢失 → ARMCC 改按 CP936 解码 → 中文字面量编译失败。
> **文档(`.md`)不受此限**,一律用无 BOM UTF-8。但用脚本批量改写文档时,仍须**按原样回写换行符**(本仓库 `core.autocrlf=true` 且无 `.gitattributes`,工作区换行混杂),否则会产生整文件行尾漂移的巨型 diff。
> ⚠️ **本仓库 BOM 铁律**(见 `AGENTS.md`「铁律:源码编辑与编码」):C 源码中 `Drivers/BSP/UI/*`、`App/task/ui_task.c`、`App/cfg/cfg_file.c`、`Lib/lv_conf.h` 等**带 BOM**,被工具改写后 BOM 会静默丢失 → ARMCC 改按 CP936 解码 → 中文字面量编译失败。
> **文档(`.md`)不受此限**,一律用无 BOM UTF-8。
>
> **行尾策略(2026-10-09 起写死进 `.gitattributes`,本节是权威口径)**:`* 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 决定。用脚本批量改写文档时仍须**按原样回写换行符**,否则会产生整文件行尾漂移的巨型 diff。
---
@@ -565,7 +565,7 @@ if (nand_ftl_format() == 0) {
7. **单写者规则** — 存储由 **defaultTask 独占**:挂载 / `f_mkfs` / 卷健康探针 / `net_cfg_load` / syslog 落盘 / 配置引擎全收敛于它。netTask / uiTask **不得**直接触碰配置引擎或 FatFS。启用存储测试套件(`TEST_SUITE_STORAGE`)时存储所有权整体移交 testTask,defaultTask 必须**完全跳过**存储 bring-up(`App/task/default_task.c` 中 `#if !defined(TEST_SUITE_STORAGE)` 包裹,2026-09-25 自 `Src/freertos.c` 迁出)——否则后台文件写与测试的 `nand_ftl_deinit` 掉电模拟并发,互相踩坏 FTL/FatFs 状态(实测:mkfs 半途冻结 + BBT 被误写)。
8. **内存布局与 DMA/CCM 约束** — FreeRTOS 堆(含全部任务栈)已迁至 **CCM `0x10000000`**(64KB,直连 M4 零等待),但 **DMA1/DMA2 物理不可达 CCM**。GD5F 驱动 `GD5F_DMA_THRESHOLD = 1024`:栈上的小缓冲(dhara meta 读 132B、`dhara_nand_is_free` 64B)一律走 **CPU 轮询**、绝不经 DMA;页级大块(2048)才走 DMA 且源/目的**全部是文件作用域静态缓冲**(落 SRAM)。新增任何 DMA 收发缓冲必须①文件作用域静态②大小≥阈值③确认落 SRAM。违反任一条 = 静默数据损坏(CCM 首轮扩容实验实证)。换料注意:克隆片可能缺 SRAM3(CCM),必须重测。
9. **LVGL 绘制缓冲越界护栏** — LVGL 9.3.0 对 I1(1bpp) 整帧渲染会在其算出的 `stride*h`(160×160/8=3200) 之后**多写 1 个字节**,紧邻 `g_ui_draw_buf` 之后的 `.bss`(如 `ui_menu_view` 的 `s_frame_box`)指针最低字节被打成 `0xFF` → 野指针 → HardFault(`PC=lv_obj_get_scroll_y`)。`ui_port.c` 的 `g_ui_draw_buf` 已预留 `UI_DRAW_BUF_GUARD=32` 字节护栏吸收该越界写(逻辑尺寸仍按 `UI_FB_SIZE` 上报,渲染数学不变)。**勿删护栏、勿把缓冲缩到与邻接 `.bss` 紧邻**。Lib/LVGL 为第三方不改。
10. **FatFs 严格 8.3 短名** — 详见 AGENTS。补充:含冒号 `:` 的名字被 FatFs 先解析为**盘符前缀** → `f_open` 返回 `FR_INVALID_DRIVE`(11) 而非 `FR_INVALID_NAME`(6);两者都属合法拒绝(测试用例须同时接受)。
11. **FA_OPEN_APPEND 陷阱 / 短写** — 详见 AGENTS。追加写必须 `FA_WRITE | FA_OPEN_APPEND`;`f_write` 簇分配失败返回 `FR_OK`+短写(无错误码),判失败必须校验写回字节数。
10. **FatFs 严格 8.3 短名** — 本移植 `Lib/FatFs/ffconf.h` 为 `FF_USE_LFN=0`,**只支持严格 8.3 短名**:主名 ≤8 字符、扩展 ≤3 字符、**不得以点开头**(2026-09-20 台架实证,`.fschk` 无限重格式化事故根因)。`.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` 等)归入"卷损坏"触发集(整卷重格式化不可逆)。补充:含冒号 `:` 的名字被 FatFs 先解析为**盘符前缀** → `f_open` 返回 `FR_INVALID_DRIVE`(11) 而非 `FR_INVALID_NAME`(6);两者都属合法拒绝(测试用例须同时接受)。
11. **FA_OPEN_APPEND 陷阱 / 短写** — `FA_OPEN_APPEND`(0x30) = `FA_OPEN_ALWAYS|FA_SEEKEND`,**不含 `FA_WRITE`(0x02)**——不带写位打开的文件 `f_write` 恒返回 `FR_DENIED`(7)(这是当年 `/log` 写失败的真根因);追加写必须写 `FA_WRITE | FA_OPEN_APPEND`(2026-09-20 台架实测,`Lib/FatFs` 移植版)。该移植的 `f_write` 在簇分配失败时返回 `FR_OK` + 短写(**没有错误码**)→ 判失败必须校验写回的字节数。标志位为**旧式编号**:`FA_READ`=0x01 / `FA_WRITE`=0x02 / `FA_CREATE_NEW`=0x04 / `FA_CREATE_ALWAYS`=0x08 / `FA_OPEN_ALWAYS`=0x10 / `FA_OPEN_APPEND`=0x30 / `FA_SEEKEND`=0x20(`ff.c` 内部定义),与上游 FatFs R0.14 不同,**勿照抄上游代码示例**。
12. **崩溃可观测性(勿屏蔽)** — 已开启 `configCHECK_FOR_STACK_OVERFLOW=2` + `configUSE_MALLOC_FAILED_HOOK` + HardFault 现场报告(`Src/stm32f4xx_it.c`:裸 UART 打印 PC/LR/xPSR + CFSR/HFSR/BFAR/MMFAR 后复位,对照 `.map` 定位符号)。defaultTask **1024** words、testTask **2048** words 是**实测下限,勿降**。开机日志出现 `[ERR] STACK OVERFLOW task=` / `[ERR] HARDFAULT ...` / `MALLOC FAILED` 是精确定位线索,禁止屏蔽或降级。
13. **BBT 恢复模式** — 运行期坏块会随 TC-STO-03 注入累积;当介质被**误标脏**(如 DMA 误打 CCM 栈导致随机块被写坏并持久化)时,打开 `test/board/storage_test.h` 的 `ENABLE_STORAGE_RECOVERY_TESTS` 编译**恢复构建**,testTask 会执行 `gd5f2gq5ue_bbt_rebuild()`(出厂扫描清除运行期误标)+ `nand_ftl_format()` 清账后复位。恢复后容量回到基线(即扣除 BBT 保留池后的正常容量,非出厂裸容量)。
+23 -1
View File
@@ -237,6 +237,28 @@ if (sys_evtHandle != NULL) {
---
## 7. 总结
## 7. 内核启动前(`osKernelStart` 之前)的编码模式
> 本节是根目录 `AGENTS.md`「平台约束」中这条铁律的完整细节迁入处(2026-10-11,AGENTS.md 瘦身,不再当事实存放地)。
**禁令:`osKernelStart` 之前禁止调用任何 FreeRTOS / CMSIS-RTOS 创建类 API**——`osMessageQueueNew` / `osMessageQueuePut` / `osMutexNew` / `osSemaphoreNew` 等在内核启动前调用会**直接杀死启动**。
**2026-09-20 启动失败实证根因**:`syslog_mcu_init` 在内核启动前调用 `osMessageQueueNew`——这就是 09/19~09/20 "每次必死" 的代码层根因(修复提交 `82411cf`)。机理(见 `App/log/sys_log_mcu.c` 中 `syslog_mcu_init` 的函数注释):`app_main_init()` 运行在 `osKernelInitialize()` **之前**(`Src/main.c` 顺序 = `app_main_init` → `osKernelInitialize` → `MX_FREERTOS_Init` → `osKernelStart`),创建类 API 会在这一窗口提前拉起内核,其后的任务创建落在未完成初始化的内核状态上执行 → 启动失败。
**内核启动前代码的标准模式三件套**(现成参照:`App/log/sys_log_mcu.c`):
1. **纯寄存器 / HAL 轮询操作**:启动段初始化只走不依赖内核的裸驱动路径;
2. **无锁静态缓冲**:内核前产生的日志先暂存到 `s_preboot[8]`(`SYSLOG_QUEUE_MAX = 8`,纯 BSS 数组,不碰任何内核对象;RTOS 前单生产者、后单消费者,故免锁)。投递点 `syslog_post` 按内核状态(`osKernelGetState()`)自动分流:内核未运行 → 写 `s_preboot`;已运行 → 走真队列;
3. **内核启动后惰性创建真队列**:真队列推迟到**任务上下文首次使用时**才创建——见 `syslog_drain`(仅 defaultTask 调用,调度器已在运行,`s_queue == NULL` 时才 `osMessageQueueNew`)。
**推论**:凡在内核启动前执行的初始化函数里,**不得创建任何 CMSIS 对象**。落地效果:`syslog_mcu_init` 现为"内核启动前安全空操作"(直接 `return 0`),真实初始化全部后移到运行期。
> **与 §3.3 的边界(勿混读)**:§3.3 说"CMSIS-RTOS v2 允许在 `osKernelInitialize()` 之后、`osKernelStart()` 之前创建内核对象"——那是 **CubeMX 生成器**在 `MX_FREERTOS_Init` 里创建事件组 / 任务的专用窗口;**手写代码**(尤其 `app_main_init` 这条早于 `osKernelInitialize` 的链)不享受该窗口,一律按本节禁令执行。
**配套判读**:违反本禁令的后果是启动段死机(无输出 + LED 不闪,此时没有任务、没有看门狗保护),定位方法见 §6 第 8 条「启动段(`osKernelStart` 之前)死机的判读法」——开机日志的最后一行 = 卡住的那一步。
---
## 8. 总结
**不要靠"等多少毫秒"来赌依赖就绪,而要靠"对方声明自己就绪了"来驱动启动**——Event Flags 把隐式的时间耦合变成显式的事件依赖,启动顺序自然清晰、健壮、易扩展。
@@ -1,7 +1,7 @@
# Python 串口助手(serial_tool)使用指南
> **定位**:面向使用者的操作手册——命令怎么敲、输出怎么看、常见场景怎么组合、出问题怎么排查。
> **关联文档**:设计决策与实现细节见 `串口助手设计说明.md`;@test 自动化闭环见 `自动化测试流程.md`;`AGENTS.md`「串口调试」节有一页速查。
> **关联文档**:设计决策与实现细节见 `串口助手设计说明.md`;@test 自动化闭环见 `自动化测试流程.md`;`serial-assistant` skill 有一页速查(用到自动加载,AGENTS.md 不再重复收录)。
> **版本**:v1.1(与 `.dsh/skills/serial-assistant/scripts/serial_tool.py` 当前实现一致,2026-09-15)
## 1. 它是什么(60 秒版)
@@ -3,7 +3,7 @@
> **版本**:v1.1(需求 + 设计 + 实施记录)
> **日期**:2026-09-15
> **状态**:**已实施**(S1–S6 完成;本机验收 A1–A5/A8 通过,A6/A7 正路径待 CH340 硬件接入后补验)
> **关联文档**:`串口助手使用指南.md`(命令用法/工作流/排障) · `自动化测试流程.md` · `AGENTS.md`(串口调试/自动化测试节) · `.serialtest/config.json`(端口单一配置源,2026-10-10 由 `.opencode/serial_config.json` 迁入)
> **关联文档**:`串口助手使用指南.md`(命令用法/工作流/排障) · `自动化测试流程.md` · `serial-assistant` skill(命令速查/陷阱) · `.serialtest/config.json`(端口单一配置源,2026-10-10 由 `.opencode/serial_config.json` 迁入)
> **取代**:`serial-mcp-server_串口调试.md`(实施时删除,工具历史并入本文 §12)
## 1. 背景与目标