# DSH STM32 插件 · 规格书 v0.2 > 状态:需求已定稿,未开始实现。全部结论都有实测证据(见 §8、§12)。 > 上一版 `dsh-stm32-plugin-需求-v0.1.md` 已合并进本文,不再维护(v0.1 的 18 章有大量重复,且其中 D3/D4 两项假设已被实验推翻)。 --- ## 1. 定位与范围 **一句话**:给 STM32 工程提供"可编程的确定性执行层"——人/agent 负责意图,插件负责每一步的原子性、可验证性与回滚。 覆盖链路:建工程 → 改 `.ioc` → 生成代码 → 写 USER CODE → 编译 → 只烧录 / 烧录并调试 → 寄存器级验证。 **明确不做(v1)**: | 项 | 原因 | |---|---| | 驱动 CubeMX GUI(UI 自动化) | 不稳定,且已有无头路径 | | 时钟树可视化编辑 | 只读写 `.ioc` 的 RCC 键 | | 接管 CubeMX2 / `.ioc2` | 格式不同,会把现有工程搞乱 | | 升级固件包、HAL 迁移 | 只透传 `.ioc` 里的版本键 | | FreeRTOS/LwIP 图形化配置 | 只透传键,不做语义理解 | | 编码风格规范(MISRA 等) | 留给后续 skill,v1 只保证"只写 USER CODE 块" | ## 2. 痛点(全部来自实测,不是设想) | # | 痛点 | 证据 | |---|---|---| | P1 | F5 和 Ctrl+F5 都进调试会话,"只烧录"做不到 | cortex-debug 的 `noDebug` 分支也会 `sendContinue`;CHANGELOG 记 ^F5 = Run Without Debugging(Experimental) | | P2 | 配引脚/时钟必须开 GUI 手点 | CubeMX 无引脚级脚本 API | | P3 | CubeMX 脚本模式有假成功 | `STM32CubeMX.exe` 是 launcher,detach 出 javaw 后立刻 exit 0;`load + generate` 输出 "succesfully" 但无 `MX_GPIO_Init` | | P4 | 默认 `No_Debug` → 生成 `__HAL_AFIO_REMAP_SWJ_DISABLE()` → 烧完 SWD 永久失联 | `_scratch/Src/stm32f1xx_hal_msp.c:77`,需 BOOT0 抢救 | | P5 | 脚本模式只产 C 源,不产 startup / `.ld` / Drivers / CMake | E1/E2 实测(§8.3) | | P6 | 验证只能"看灯闪不闪" | 最终靠 `AFIO_MAPR(0x40010004)` + `GPIOC_ODR` 采样才敢确认 | ## 3. 核心原则(不变量,实现时不许破) | # | 原则 | 落地方式 | |---|---|---| | I1 | `.ioc` 是唯一真源 | 配置改动只改 `.ioc`,不改生成物 | | I2 | USER CODE 块之外不许手写 | `code_patch` 只做锚点插入,锚点缺失则拒绝并列出可用锚点 | | I3 | 改 `.ioc` 永远 plan → 确认 → apply | 无静默写盘;apply 必须带 `plan_id` 且源 SHA-256 与 plan 时一致 | | I4 | 复刻不发明 | 布局复刻 CubeMX 产物(§7);脚手架复制或模板快照,不手写 | | I5 | 外部工具单飞 | 同一时刻只允许一个 CubeMX / openocd / ninja 进程 | | I6 | 探针默认释放 | 调试默认在一次调用内开关;退出/取消必杀进程树 | | I7 | 先 build 再 flash | 构建失败绝不烧录 | | I8 | 每次交付都要可自证 | 寄存器值 / 断点命中 / 计数变量,而不是"看起来对" | ## 4. 工具清单(13 个) | 工具 | 作用 | 副作用 | 耗时 | 形态 | |---|---|---|---|---| | `stm32_env_probe` | 探测 CubeMX、固件包、gcc/gdb/openocd/cmake/ninja、探针 | 只读 | <3 s | 阻塞 | | `stm32_project_create` | 从 MCU 型号建工程骨架(`.ioc` + CMake 工程文件 + 固件包 Drivers) | 建目录 | ~2 s | 阻塞 | | `stm32_ioc_inspect` | `.ioc` → 结构化 JSON(引脚/信号/标签/时钟/项目设置) | 只读 | <1 s | 阻塞 | | `stm32_ioc_plan` | 变更计划:改哪些键、diff、源 SHA-256、`plan_id` | 只读 | <1 s | 阻塞 | | `stm32_ioc_apply` | 校验暂存副本 → 备份 → 替换 `.ioc` | 改 1 文件 | 90–150 s | **job** | | `stm32_generate` | CubeMX + 布局映射 + USER CODE 合并 + 补脚手架 | 改多文件 | 3–4 min | **job** | | `stm32_code_patch` | 只在 USER CODE 块内写 C 代码 | 改 1 文件 | <1 s | 阻塞 | | `stm32_build` | cmake + ninja,返回 flash/ram 占用与错误定位 | 改 `build/` | 20–60 s | **job** | | `stm32_flash` | **只烧录**:program + verify + reset,不启 gdb | 改芯片 | 5–15 s | 阻塞 | | `stm32_debug_run` | 一次调用内烧录 + 断点 + 取 bt/regs + 关闭 | 改芯片 | 5–20 s | 阻塞 | | `stm32_debug_session` / `_stop` | 显式申请长会话(租约,§6) | 占用探针 | — | 阻塞 | | `stm32_regs_read` | 直连 openocd 读任意地址(不启 gdb) | 只读 | 3–5 s | 阻塞 | | `stm32_doctor` | 自检:SWD 是否启用、时钟、栈指针、向量表、USER CODE 完整性、探针被谁占 | 只读 | 5–20 s | 阻塞 | **斜杠命令**(人直接触发):`/stm32-flash`(只烧录)、`/stm32-debug`(烧录并调试)、`/stm32-build`、`/stm32-doctor`。这是 P1 的正面解法——烧录与调试点两下,互不干扰。 **随插件下发的 SKILL**:`stm32-workflow`,固化红线(I1–I8)与本次踩过的坑(SWJ_DISABLE、沙箱卡死、假成功)。 ## 5. 工作流状态机 ``` [0] env_probe -> [1] project_create -> [2] ioc_inspect/plan/apply -> [3] generate | +------------------------------------------+ v [4] code_patch (USER CODE 内) -> [5] build -> [6] flash(只烧录) | +-> [6q] debug_run(烧录并调试) +- 失败回 [4] / [2] | v [7] doctor / regs_read 自证 ``` 每阶段契约:**输入(前置产物)→ 输出(结构化 JSON)→ 失败回退点**。任何阶段失败不许"猜着继续"——generate 后必须做空壳检测(P3)。 ## 6. 调试会话模型(租约式,默认用完就关) ### 6.1 硬约束 CMSIS-DAP 同一时刻只能被一个进程占用。「插件常驻 gdb」与「你在 VS Code 按 F5」互斥;冲突表现是 `Error: Error connecting DP: cannot read IDR`,**看不出被谁占**。 ### 6.2 三种形态 | 形态 | 生命周期 | 适合 | 代价 | |---|---|---|---| | `debug_run`(默认) | 单次调用内 open→load→break→continue→取 bt/regs→close | 验证型调试(有没有到 main、卡在哪、变量值) | 不能交互 | | `debug_session`(显式申请) | 空闲 60–90 s 自动关,操作续租,10 min 硬上限 | 交互式单步、反复读变量 | 期间占用探针 | | 常驻 gdb | 直到手关 | 长时间连续调试 | **会挡住 VS Code**,不采用 | ### 6.3 关闭规则 1. 默认走 `debug_run`:一次调用把该看的都看回来,**天然用完就关**。 2. `debug_session` 必须显式申请,返回 `session_id` + 剩余租约;每次 `debug_eval` 续租。 3. `build` / `flash` / `generate` 开始前若存在会话,**先关闭并说明**(避免"烧录时探针被自己占着")。 4. 插件进程退出 / job 取消 → 强制关闭,openocd 进程树 `taskkill /T /F`。 5. `doctor` 能回答"探针现在被谁占着",冲突时给可操作提示而非裸错误。 ### 6.4 T1 已确认:`turn/end` 钩子存在,按精确方式实现 用户要求:"agent 工作时会话可留;**agent 输出完、等下一次回复期间必须关闭**"。 **已定论(2026-10-22 查 dsh 实现)**:插件能精确知道轮次结束,不需要降级 TTL。 ```ts // 会话事件流:turn/start、turn/end、tool/result ... ctx.on("session/event", (session, event) => { if (event.type === "turn/end") closeDebugSession(session) // event.data = { turn, reason } }) ctx.on("session/disposed", (session) => closeDebugSession(session)) ctx.on("agent/turn-stopping", async ({ agent, turn }) => { /* 轮内提前收尾,可选 */ }) ``` 证据(本机 asar 内实现): | 位置 | 内容 | |---|---| | `dsh-agent-loop/lib/index.js:1029` | `this.session.append("turn/end", { turn, reason: turnEnds })` —— 每轮工具循环结束时追加,等于"我说完了" | | `dsh-workspace-changes/lib/index.js:1055-1062` | 消费者写法:`ctx.on("session/event", (session, event) => { … event.type === "turn/end" … })` | | `dsh-workspace-changes/lib/index.js:1064-1069` | 兜底 `session/disposed`,以及轮内钩子 `agent/turn-stopping`(`{ agent, turn }`) | | `dsh-agent/lib/index.js:105-108` | `turn/end` 的 `data` 含 `{ turn, reason }` | **实现约定(已在 M4 落地,见 §8.10)**: 1. `debug_session` 打开时向插件注册 `turn/end` 监听;事件到达 → 关闭会话并 `taskkill /T /F` openocd 进程树。 2. `session/disposed` 与插件 `dispose` 同路径清理,防进程泄漏。 3. 保留 TTL(60–90 s)+ 10 min 硬上限作为**安全网**,不是主路径:万一事件没到,探针也不会被永久占用。 4. `debug_run` 形态照旧(一次调用内开关),不依赖任何事件。 5. `build` / `flash` / `debug_run` / `generate` 开跑前先 `releaseProbeFor()`:会话开着就先关,避免"自己的会话占着探针"。 实际代码(`dsh-stm32/src/index.ts`): ```ts if (typeof ctx.on === "function") { ctx.on("session/event", (_session, event) => { if (event?.type !== "turn/end") return; void debugSessions.stop("turn/end:本轮输出结束,把探针还给你"); }); ctx.on("session/disposed", () => void debugSessions.closeAll("session/disposed")); ctx.on("dispose", () => void debugSessions.closeAll("dispose")); } ``` ## 7. 工程结构契约 ### 7.1 目标布局(= 当前 `demo/` 的真实布局) ``` demo.ioc <- 配置源,唯一真源 .mxproject <- CubeMX 生成记录(增量生成依据) CMakeLists.txt CMakePresets.json <- generator=Ninja, binaryDir=build/ cmake/{gcc-arm-none-eabi.cmake, starm-clang.cmake, stm32cubemx/CMakeLists.txt, user/CMakeLists.txt} startup_stm32f103xb.s <- 根目录 STM32F103XX_FLASH.ld <- 根目录 Core/Inc/{main.h, stm32f1xx_hal_conf.h, stm32f1xx_it.h} Core/Src/{main.c, stm32f1xx_hal_msp.c, stm32f1xx_it.c, syscalls.c, sysmem.c, system_stm32f1xx.c} Drivers/{STM32F1xx_HAL_Driver, CMSIS} .vscode/{launch.json, settings.json, c_cpp_properties.json} build//.elf ``` ### 7.2 契约 | ID | 要求 | 理由 | |---|---|---| | C1 | 新工程逐项复刻上表,不自创 `src/`、不重排 | `cmake/stm32cubemx/CMakeLists.txt` 的源文件清单是**写死**的(`Core/Src/main.c` 等,21–28 行),布局一变就"改了不生效且不报错" | | C2 | 生成物必须落 `Core/Src`、`Core/Inc` | CLI 固定写 `Src`/`Inc`(§8.3 E2),插件必须做搬运 + 合并 | | C3 | `.elf` 路径与名字一致(`build//.elf`) | 否则 VS Code F5 找不到可执行文件 | | C4 | `launch.json` 只**新增**配置 | 保留已有的「① 只烧录 / ② 烧录并调试」两条 | | C5 | 写入边界:只有 `Core/**` 的 USER CODE 区 + `.dsh-stm32/**` 可写,其余只读 | 防止"调试时项目文件被搞乱" | | C6 | 脚手架(startup/`.ld`/CMake/Drivers)只许"复制项目自身"或"模板快照(记录来源版本)",**禁止手写** | 手写必然与 CubeMX 版本漂移 | | C7 | 任何非 USER CODE 区的写入必须 plan + 确认 + 备份 | 与 I3 一致 | ## 8. CubeMX 集成:实测事实与能力边界 ### 8.1 调用方式(唯一可用) ``` & "\jre\bin\java.exe" -jar "\STM32CubeMX.exe" -q