dsh-wch-debug —— WCH / CH32 单片机调试插件
用 OpenOCD + CLI 调试沁恒(WCH)CH32 系列单片机的 DSH 插件:把 WCH-Link 调试、 固件烧录、寄存器/内存读写、gdb 连调、常驻调试会话这一整套能力交给 agent, 并且从设计上不可能污染你的 IDE 与后续烧录。
插件 id @local/dsh-wch-debug (cordis id: wch-debug)
工具 20 个,分五组
技能 wch-debug
外部依赖 无 npm 依赖;需要本机 C:\MounRiver 里的 OpenOCD / 工具链 / WCH-Link
为什么需要这个插件(不是「又一个烧录脚本」)
1. 工作区污染 —— 一次手滑,之后所有烧录都坏
OpenOCD 在不给任何 -f 且不给 -c 时会自动 source 当前工作目录里的
ocd.cfg(搜索路径第 1 项就是 CWD)。而 MounRiver Studio 恰恰是以它自己的
...\WCH\OpenOCD\OpenOCD\bin\ 为 CWD 启动 openocd 的 —— 那个目录里**出厂就躺着**
一个 ocd.cfg:
interface cmsis-dap
transport select swd
source [find target/ch32f1x.cfg]
它和 bin\wch32f103.cfg 逐字节相同,意思是 ARM / SW-DP。
谁在 bin 里试过一次命令、留下一个 cfg,之后所有 WCH-Link 操作都会被静默顶成 ARM 模式,
而目标却是 RISC-V 的 CH32V 系列 —— 连不上、或者连上了你也看不懂为什么。
本插件的对策
- openocd 整棵子树(30.9 MB / 1816 文件)复制一份到插件内
vendor\openocd\(bin/、scripts/、share/相对布局照抄,默认搜索路径仍然可用)。 - 永不写 IDE 树、永不把 IDE 树当 CWD。
assertPrivateWorkdir()是一道硬门禁:CWD 不在插件私有区就抛WorkspaceViolation。 - 每次执行都在
.run\sessions\<id>\这个一次性私有目录里,跑完即弃。 - cfg 只走绝对路径
-f(argv 传参),既不依赖隐式source,也不在 cfg 文件里写source <路径>。 - 主动剥掉子进程环境里的
OPENOCD_SCRIPTS(本机它指向~\.eide\tools\openocd-0.12.0\scripts,优先级高于自带脚本库,且那里没有ch32f1x.cfg)。
2. 断点残值固化进固件 —— 芯片上电就停在断点,再也进不了程序
软件断点是改写目标内存里的指令实现的(RISC-V 上写 ebreak,ARM 上写 bkpt),
只有删除时才会写回原指令。而 flash 按扇区擦写,粒度远大于一个断点。于是:
会话里 bp 0x… → 目标内存那条指令被改成 ebreak
会话异常终止 / 清了但没写回 → 这几个字节留在目标里
下一次烧录 program … → flash 按扇区擦写,ebreak 随镜像一起被编程
下一次上电 → 停在断点,程序跑不起来
本插件的对策
- 每次收尾都清断点再放跑:
catch {rbp all}→reset run。 - 清断点用
catch { … }包住,避免「目标正在跑时清理报 Target not halted」被误判成失败。 - 绝对不 taskkill 常驻会话:只有 telnet 已经不通(进程卡死)才退回强杀,并且报告里明说 「这次是强杀的,目标里可能残留断点,下次烧录前先清」。
- 报告里始终打印这次到底跑了什么收尾(
收尾(已写进 -c):清断点rbp all→ `reset run)。
⚠️ 收尾引擎踩过的第二个坑:一次把所有行写进去,等于只发了第一行
第一版 telnet 实现是 sock.write(lines.join('\n') + '\n'),然后在第一个提示符上就
finish() + destroy()。后果非常隐蔽:
发出去:halt \n catch {rbp all} \n … \n shutdown
收到 halt 的提示符 → 立刻 destroy socket
→ openocd 还没来得及读第 4 行,剩下的行全丢
现象是 wch_session_close 每一步都回显 ✓ … → prompt(因为提示符确实拿到了),
但 12 秒后进程还活着、telnet 端口还开着,日志里根本没有 shutdown command invoked。
即「报告说清了、实际没清」—— 与下面 foreach a [bp] 那个坑是同一个形状:
看起来做了,其实没做。
现在改成:发一行 → 等这一行的提示符或断连 → 再发下一行,并把每条命令各自的结果收进 results,
报告里逐条打勾。shutdown 是特例(它不回提示符,直接关连接),
所以「连接被关掉」本身就是它生效的证据。
⚠️ 实证纠正:清断点的常见写法其实是空操作
本插件第一版用的是流传很广的 foreach a [bp] { rbp $a },前提是「bp 无参返回全部断点号」。
实测这个前提是错的:
$ bp ← 无参不是断点号列表,是给人看的字符串
Breakpoint(IVA): 0x20000010, 0x4, 1869182051
$ foreach a [bp] { rbp $a }
Invalid command argument
addr option value ('Breakpoint(IVA):') is not valid
$ bp ← 断点还在,一个都没清掉
Breakpoint(IVA): 0x20000010, 0x4, 1869182051
TCL 按空白切词,$a 第一个值是 Breakpoint(IVA):,rbp 拿到非法 addr 报错 ——
而外面套的 catch 把错吞了:一个断点都没清掉,却看不到任何异常。
这正是用户踩过的那类事故的形状(「声称清理了,其实没清」)。
正确写法是 rbp all。之前因为「手册里没见过」而回避了它,其实问本构建自己就有权威答案:
$ help rbp
rbp 'all' | address
remove breakpoint
实测 rbp all 生效(两条断点一次清空,bp 随后回空)。教训:
能用官方文档字符串证实的,就别用「流传的写法」猜;catch 只能容忍无害失败,
不能用来包一个自己没验证过的命令。
实测:本机这条路径上「残值写进内存」没有复现(但纪律照旧)
本机 CH32V307 + 本构建,bp <addr> <len> 出来的是 Breakpoint(IVA) —— 走青稞 V4 的
IVA(硬件断点)机制。逐字节实测:下断点前 / halt 下设置后 / resume 之后再 halt /
rbp all 清理后,RAM 与 flash 都完全相同,始终没有出现 ebreak(0x00100073)。
纪律照旧,三个理由:① 硬件断点槽位有限,用满后 OpenOCD 会退回软件断点,那时就真改指令字节了;
② ARM 侧(cortex_m)的软件断点就是 bkpt 改写;③ 断点残留在目标上会让后续 reset run
停在意外位置 —— 一样是「烧完不跑」。别把「这次没复现」当成「不用清」。
3. 判据 —— 退出码会骗人
openocd.exe 的命令失败时也可能返回 0。更糟的是 -l <log> 会把 Error: 从
stdout/stderr 里挪走,只看捕获流就漏判。所以:
- 判据 = 文本里有
^Error:→ 失败(不论退出码);无Error:但退出码 ≠ 0 → 失败;两者干净才算过。 - 默认不给
-l(我们自己已经捕获 stdout+stderr);给了-l就把日志读回来合并进判据文本。 - 烧录的成功判据是
** Verified OK **+ 收尾的reset run,不是「命令返回了」。
安装
// 在 DSH 里(一次调用)
plugin_manager({ action: 'install_bundle', target: 'C:\\Users\\lishuo\\Documents\\DSH_Project\\插件开发\\dsh-wch-debug' })
看返回里的 application 字段:applied 才算生效。
(若返回 changed:false + application:'failed' + error.code:'ambiguous-install',
那是缓存的旧失败,不必重启:set_plugin(target:'include:wch-debug', enabled:false) 再 enabled:true。)
验证:
plugin_manager({ action: 'list_plugins' }) // 看 fiberPhase: 'active'
cordis_inspect_query({ platform:'host', provider:'Tool', method:'listTools' }) // 看 20 个 wch_* 工具
工具名是按依赖顺序注册的,插件加载时控制台会打印:
[wch-debug] 已注册 20 个工具: wch_env, wch_devices, … , wch_clean
[wch-debug] 已注册技能 provider: wch-debug(skills/ 下的 SKILL.md)
工具面(20 个)
第一次用,先跑 wch_env —— 它一次把插件根、隔离副本版本、IDE 原件位置、工具链、
WCH-Link 当前模式与串口、残留 openocd 进程、端口占用、以及「现在能不能干活」全报出来。
A 组:环境 / 清单 / 帮助(只读)
| 工具 | 干什么 |
|---|---|
wch_env |
环境自检。隔离副本版本与 5 个 cfg、IDE 原件位置与出厂残留、工具链(gcc/gdb/objcopy…)、WCH-LinkUtility、WCH-Link 的 USB 节点与当前模式(RV / ARM)、残留 openocd 进程、3333/4444/6666 占用 |
wch_devices |
CH32 型号表(20 个,key 如 ch32v307):core / flash / RAM / 该用哪个 cfg / 需要 WCH-Link 处于哪种模式 / 证据来源(local = 本机实测,public = 公开资料)。不传参数列全部 |
wch_projects |
扫 C:\MounRiver\Project 下的 Link.ld,列出各工程的 FLASH / RAM / entry / 推断型号 |
wch_workspace |
action: audit 卫生审计(P1/P2 分级 + 修复建议)· quarantine 把出厂残留 ocd.cfg / ocd_try.* 挪进隔离区(只动副本,不动 IDE 原件)· prune 清超过 24h 的会话目录 |
wch_help |
排错剧本:first_time / not_connecting / flash_ok_but_dead / bp_residue / session / tools |
B 组:只看不改(读完就 reset run 放跑,绝不把芯片留在调试态)
| 工具 | 干什么 |
|---|---|
wch_probe |
连一次目标并报告:WCH-Link 型号/固件、时钟、调试模块(datacount / progbufsize)、RISC-V XLEN / misa、目标名、flash device id 与容量、PC/SP |
wch_regs |
读寄存器;RISC-V 还能读 CSR(mstatus / mcause / mepc) |
wch_mem |
读一段内存并本地排 hexdump(实现走 dump_image 落文件再排版,比解析 mdw 文本可靠)。约 2.8 kB/s,读 4 KiB ≈ 1.5 s |
wch_flash_info |
flash probe 详情:device id、容量、bank 基址 |
wch_read_flash |
读出一段(或整片)成 .bin,落到 backups/。走 flash read_bank。整片 256 KiB ≈ 88 s,超时按字节数自动算,报告里给预期秒数 |
C 组:写目标(都需要 confirm:true)
| 工具 | 干什么 |
|---|---|
wch_flash |
烧录(elf / hex / bin / s19)。默认先备份原 flash 到 backups/,再清断点 → program … verify → 逐个清断点 → reset run。可传 address、verify:false、backup:false |
wch_erase |
擦除(整片 / 指定扇区区间)。页面数按 pageSize 自己算,不用未经证实的 last |
wch_reset |
mode: run / halt / init / halt_run |
wch_ocd |
逃生舱:把任意 openocd 命令串喂给隔离副本执行。仍然强制私有 CWD、强制绝对 -f、不加载任何隐式 cfg。用来做本插件没封装的检查(看断点、mdw、自定义序列…)。footer 可选 run(默认)/ halt / none |
D 组:常驻会话与连调
| 工具 | 干什么 |
|---|---|
wch_session_open |
起一个常驻 openocd:3333 gdb / 4444 telnet / 6666 tcl。同一个 WCH-Link 同时只能被一个 openocd 占用 —— 起之前先 wch_session_list |
wch_session_cmd |
往会话里打命令(走 telnet),保持调试上下文:halt / bp / step / mdw / reg … |
wch_session_close |
先清断点再 shutdown(不是 taskkill)。只有 telnet 不通才 force 强杀,并警告可能残留 |
wch_session_list |
列活的会话(PID / 端口 / 目标 / 存活判定) |
wch_gdb |
用 MounRiver 工具链里的 gdb 做批处理调试:action: symbols(不连硬件,只查 ELF 里的符号/地址 —— 找 main 地址去下硬件断点就用它)/ load(走 gdb 的 flash 编程)/ inspect(halt + 寄存器 + bt + 栈内存 + 放跑)/ raw |
E 组:清理
| 工具 | 干什么 |
|---|---|
wch_clean |
action: audit 工作区卫生审计 · scan 列私有运行区与会话目录 · prune 清过期会话 · kill 清残留 openocd 进程 |
技能
skills/wch-debug/SKILL.md 是给 agent 看的操作手册(不是工具清单的复述):什么时候该用
一次性工具、什么时候该起会话、连不上按什么顺序排除、烧录成功但板子不跑怎么查、
断点残值怎么确认与清掉。工具描述里写不下的判断依据放这里。
目录结构
dsh-wch-debug/
├─ package.json name @local/dsh-wch-debug / type module / dsh.bundle.patch
├─ cordis.patch.yml - insert: - id: wch-debug / name: "@local/dsh-wch-debug"
├─ index.js apply(ctx):注册 20 工具 + 技能 provider
├─ lib/
│ ├─ define-tool.js 零依赖的 defineTool 本地实现(见下「为什么自带」)
│ ├─ frontmatter.js SKILL.md 的 YAML frontmatter 解析
│ ├─ skills.js 技能 provider(list / get 契约)
│ ├─ result.js ok / fail / output / clip / kvBlock / hexDump … 统一返回形状
│ ├─ env.js MounRiver 树探测、WCH-Link 模式、工具链、进程与端口
│ ├─ devices.js CH32 型号表 + 目标解析 + 链接脚本扫描
│ ├─ hygiene.js ★ 工作区门禁、会话目录、污染扫描、收尾命令常量
│ ├─ ocd.js ★ argv 构造、私有 CWD 执行、判据、各序列
│ ├─ tools-env.js A 组
│ ├─ tools-debug.js B/C 组
│ └─ tools-session.js D/E 组
├─ skills/wch-debug/SKILL.md
├─ references/
│ ├─ openocd-wch-commands.md 684 行:WCH 专用 cfg 矩阵、命令参考、污染机理专章、排错表
│ └─ raw/ 从 WCH-Link 手册 PDF 抽出的文本(中/英)
├─ scripts/
│ ├─ register-check.mjs 注册自检:工具数 / schema / 技能 provider(不用真机)
│ ├─ selfcheck.mjs 环境 + 门禁 + 真机只读探针
│ ├─ smoke-reload.mjs 重载冒烟:十几秒,只读,改完代码 reload 后跑一遍
│ ├─ e2e-hardware.mjs 真机端到端(回写校验 + 断点卫生来回验证 + IDE 树零写入)
│ ├─ e2e-protect.mjs 真机端到端(读保护闭环:开 → 裸烧必败 → unprotect 烧进 → 关)
│ ├─ chain-ab.mjs 实验:到底哪条命令会中断 `-c` 链(决定几何必须单独跑一次)
│ ├─ bench-read.mjs 读吞吐实测第一轮(路径反斜杠的坑就是它暴露的)
│ └─ bench-read2.mjs 读吞吐实测定稿(决定超时与 read_bank 选型)
├─ vendor/openocd/ ★ 隔离副本(30.9 MB / 1816 文件),永不与 IDE 共用
├─ .run/ 私有运行区:每个会话一个一次性目录(24h 后清)
└─ backups/ 烧录前自动备份的原 flash
为什么自带一份 define-tool.js / skills.js
本地 link: 装载的插件,Node 按真实路径向上找 node_modules,而
@deepseek-ai/dsh-tools 等宿主包只存在于宿主模块图里(Desktop 下在 app.asar 内,磁盘上没有)。
所以插件里不能 import 任何 @deepseek-ai/*,否则整行 failed to import。
lib/define-tool.js 就是 defineTool 的等价本地实现(同一套 options:parameters DSL、
output.schema / output.render、execute(args, exec)、timeoutMs…)。
设计上的硬约束(改代码前请先读)
CWD 必须在插件私有区。所有 openocd 子进程都经
runOpenocd();它先过assertPrivateWorkdir()。forbiddenRoots()= MounRiver 根 + IDE OpenOCD 树 + 副本体。cfg 只走
-f绝对路径,且所有路径用正斜杠。不要在 cfg 文件里写source <路径>—— OpenOCD 0.11 的 TCL 从文件内容取的路径走窄字符fopen,含非 ASCII 的路径必然 GBK 乱码:插件开发→鎻掍欢寮€鍙?→No such file or directory。 同一路径经 argv(-f,宽字符)完全正常 —— 这是实测结论,别再改回去。 同理-c字符串里的C:\...会被 TCL 吃掉反斜杠(见上文「路径一律正斜杠」)。buildArgs()对双引号和反斜杠都设了硬闸门。收尾必须清断点 +
reset run。见上文第 2 条。并且要区分「写进了 argv」与「真的执行了」—— 见下面第 7 条。写操作要
confirm:true。这不是权限确认,是防手滑的机械闸门。ARM 族用
bin\wch32f103.cfg,不要用bin\wch-arm.cfg—— 后者第 3 行写死相对路径source [find ../share/openocd/scripts/target/ch32f1x.cfg],CWD ≠bin就报Can't find ../share/...。前者内容等价但走find,与 CWD 无关。RV 与 ARM 是四项整体切换:RV =
adapter driver wlinke+transport select sdi+wlink_set_address+sdi newtap … -irlen 5 -expected-id 0x00001; ARM =adapter driver cmsis-dap+transport select swd+ch32f1x.cfg。 WCH-Link 的硬件模式不匹配时不会报「模式不对」,只会报连不上 —— 用wch_env看mode。-c链会中途断掉,且断得毫无提示。OpenOCD 把多个-c串成一条 TCL 脚本: 任一命令报错 → 后面所有命令全部被静默丢掉。后果正是本插件最怕的那类 —— footer 里的rbp与reset run一起没了,断点残值留在 flash 里、目标停在调试态。 两条对策都已落地:chainComplete(text):日志里必须有shutdown command invoked(脚本走完的最后一步), 缺席就说明链断了。报告里会显式打出 ⚠️。这条比「有没有Error:」更接近根因 —— 错误文本可能被catch吞掉,断链藏不住。收尾兜底:主链失败时
runOpenocd()会自动另起一次 openocd 进程只做收尾 (init → halt → 清断点 → reset run),不指望主链能走到 footer。具体踩点:
flash info 0在本驱动上打完扇区表就会报错(Flash protection check is not implemented), 裸着写就会把后面的命令连shutdown一起带走。但「套上catch」不是解法 —— 套上之后链是保住了,扇区表却一个字节都不打(parsed.flash.sectors变undefined), 于是wch_flash_info静默丢掉几何。这个 bug 静态看不出来,只有真机跑才露馅。 A–E 五组对照实验(scripts/chain-ab.mjs,每组尾部挂reg pc当探针):组 命令 exit 尾随 pc shutdown 扇区表行数 A 裸 flash protect_check 00 ✅ ✅ — B 裸 flash info 01 ❌ ❌ 288 C catch包住两条0 ✅ ✅ 0 D 裸 protect_check + catch {flash info 0}0 ✅ ✅ 0 E catch {flash protect_check 0}0 ✅ ✅ — 结论两条路:要几何 → 裸
flash info且单独跑一次(geometrySequence()); 要链完整 → 完全别放flash info(套catch也拿不到表)。 所以wch_flash_info跑两次,主链只放flash protect_check(裸,A 组证明它不断链)。同理
probeSequence把reg pc排在flash probe 0前面:万一 flash 探失败, 至少还有 PC 能证明探针活着。
判据也不能只看「文本里有没有
Error:」 —— 这是第 3 条的镜像错误,一样会把成功判成失败。 实测:回写整片 flash 时日志里同时出现Error: checksum mismatch - attempting binary compare ** Verified OK **而退出码是 0。
checksum mismatch - attempting binary compare是「checksum 预校验不一致 → 退化为逐字节比对」的降级提示,不是失败结论(真失败会另有Verify failed)。 为此lib\ocd.js加了BENIGN_ERRORS+classifyError():parseOcd把这类行收进parsed.benign而不是parsed.errors,并附一条注说明它为什么无害。 白名单必须逐条实测过才准进 —— 往里塞一个真错误,等于把判据阉了。
未能证实、或实测有害的写法(代码里刻意避开)
| 写法 | 为什么不用 |
|---|---|
foreach a [bp] { rbp $a }(清断点) |
实测是空操作:bp 无参返回的是给人看的字符串,rbp 拿到非法 addr 报错、被 catch 吞掉,断点一个都没清。改用 rbp all(本构建 help rbp 原文支持,实测一次清空) |
bp <addr>(只给地址) |
实测报用法行、不设断点。必须带长度:bp <addr> 4;要硬件断点再加 hw |
rwp all |
help rwp 只给了 rwp address,没有 all。改为从 [wp] 里用 {0x[0-9a-fA-F]{8}} 抠地址逐个删 |
flash erase_sector 0 0 last |
last 未证实。改用扇区号自己算(注意扇区是 1 KiB,不是 256 B) |
flash verify 单独命令 |
改用 program … verify 与 verify_image |
-o / gdb_speed_override |
未证实,不依赖 |
riscv set_mem_access sysbus / abstract |
实测会把读打瘸:sysbus=skipped (unsupported size)、abstract=failed。显式指定会关掉 progbuf→sysbus→abstract 的自动回退链,而 WCH 驱动靠的正是默认的 progbuf 抽象访问。慢就慢,别提速 |
riscv set_prefer_sba on |
已废弃(openocd 自己会警告改用 set_mem_access),且实测毫无加速 |
mdw 读大段再解析文本 |
实测约 2000 µs/B,比读文件慢一个数量级(dump_image / flash read_bank 是 335 / 326 µs/B) |
-l <logFile> 指望 Error: 留在捕获流里 |
-l 会把消息重定向进文件,stdout/stderr 只剩 banner → 判据退化成「只看退出码」。默认不加 -l;给了就必须把文件读回来并入判据文本 |
在 cfg 里 source <绝对路径> |
非 ASCII 路径必挂:Jim-TCL 走窄字符 fopen,插件开发 被按 GBK 解成 鎻掍欢寮€鍙? → couldn't read file。cfg 一律由 argv 的 -f 传(Windows 宽字符安全) |
| openocd 命令里出现 Windows 反斜杠路径 | TCL 会吃转义:\U→U、\b→退格,路径直接烂掉。buildArgs() 现在硬拒绝含 \ 的命令串,要求先 toSlash() |
实测性能(决定超时与用法)
WCH-Link 的 SDI 通道每次内存访问都要一趟 USB 往返,所以读吞吐只有 ~2.8 kB/s, 而且与适配器时钟几乎无关(把 6000 kHz 调低并不会更快)。
| 方式 | 4 KiB | 16 KiB | 32 KiB | 边际 |
|---|---|---|---|---|
flash read_bank |
2011 B/s | — | 3065 B/s | 326 µs/B |
dump_image |
1676 B/s | 2620 B/s | 2718 B/s | 335 µs/B |
mdw(解析回显文本) |
— | — | — | ≈ 2000 µs/B |
推论:整片 256 KiB 读一次约 88 s。 这是物理限制,不是卡住。
所以:
wch_read_flash/wch_mem的超时按字节数算(20000 + bytes × 0.35 ms × 2,下限 60 s), 不再用固定 90 s —— 第一版就是固定 90 s,整片备份正好超时。- 报告里始终打印「预期耗时 约 N s」,并说明「慢是 SDI 的物理限制,不是卡住」, 免得下一个人去杀进程、去重试,反而更糟。
- 要快就传
length只读要看的那一段。 - 读 flash 用
flash read_bank(语义正确 + 略快),读 RAM 用dump_image。 - 烧录用
TIMEOUTS.flash = 900 s:program … verify= 擦 + 写 + 一轮完整回读比对, 256 KiB 全流程约 150 s 起。
⚠️ 给 openocd 的路径一律正斜杠
-c 里的字符串会先过 TCL 的转义处理,而反斜杠是 TCL 的转义引导符:
\b 退格、\n 换行、\t 制表、\v/\f/\a、\0~\7、\x、\u…
于是:
-c "flash read_bank 0 C:\Users\lishuo\AppData\Local\Temp\wch-bench\bank.bin 0 4096"
↑ \U→U \A→A \L→L \T→T \w→w \b→(退格符) ↑
实际变成 C:UserslishuoAppDataLocalTempwch-benchank.bin
openocd 只会报 Error: couldn't open C:Users...ank.bin / Could not open file —— 看不出原因。
规矩:所有路径先过 toSlash()(\ → /;openocd/Jim-TCL 完全接受 C:/a/b)。
buildArgs() 里已经加了闸门:任何 -c 命令串里出现反斜杠就直接抛错,
并在消息里给出正斜杠版本,而不是让 openocd 报天书。
反直觉的一点:同一个路径经
-f传(-f C:\中文\...\wch-riscv.cfg)是正常的 —— 它走 Windows 宽字符 argv。而经文件内容source <路径>或**-c字符串**就会坏。 详见下文「硬约束」第 2 条。
自检
cd C:\Users\lishuo\Documents\DSH_Project\插件开发\dsh-wch-debug
node scripts/register-check.mjs # 不需要硬件:工具数 / schema / 技能 provider
node scripts/selfcheck.mjs # 环境 + 工作区门禁 + 真机只读探针
node scripts/smoke-reload.mjs # 重载冒烟:十几秒、只读,改代码后跑这个最快
node scripts/e2e-hardware.mjs # 真机端到端(会回写 flash;--skip-flash 只跑只读)
node scripts/e2e-protect.mjs # 真机端到端(读保护闭环:会短暂开关读保护)
node scripts/chain-ab.mjs # 实验:哪条命令会中断 `-c` 链(几秒,只读)
e2e-hardware.mjs 里有两项是专门盯着 m00001 那个坑的:
- 回写校验(identity round-trip):先把整片 flash 读出来存
.bin,再把同一份烧回去。 内容没变 → 板子状态零改变,但真的走了一遍erase → write → verify → 清断点 → reset run。 - 断点卫生来回验证:读 RAM 与 flash 基线 → 会话里下断点(
bp <addr> 4)→ 在halt状态、resume之后、rbp all清理之后各读一次并断言与基线相同。 实测结论:本机 CH32V307 走Breakpoint(IVA)硬件断点,全程没有出现ebreak—— 也就是说「残值写进内存」这条路径在本机没有复现(细节与为什么纪律照旧,见上文第 2 条)。 这一步的价值在于把这些结论钉成可重跑的断言,换驱动/换型号时会立刻暴露差异。 - IDE 树零写入:跑之前/之后对
...\OpenOCD\OpenOCD\bin做「文件名 + 大小 + mtime」快照, 断言完全一致。
环境
| 项 | 值 |
|---|---|
| MounRiver 根 | C:\MounRiver(可用 MOUNRIVER_ROOT 覆盖) |
| IDE OpenOCD 原件 | C:\MounRiver\MounRiver_Studio2\resources\app\resources\win32\components\WCH\OpenOCD\OpenOCD |
| 插件副本 | <插件>\vendor\openocd(OpenOCD 0.11.0+dev-snapshot,version.txt = v2.12) |
| 工具链 | …\components\WCH\Toolchain\:riscv-wch-elf-(GCC 12.2)/ riscv32-wch-elf-(GCC 15.2)/ riscv-none-embed-(GCC 8.2)/ arm-none-eabi-(GCC 9.3.1) |
| WCH-LinkUtility | …\components\WCH\Others\SWDTool\default\WCH-LinkUtility.exe(固件升级 / 手动下载,本插件不代跑) |
| 默认目标 | ch32v307(CH32V307EVT:FLASH 256K @ 0x00000000、RAM 64K @ 0x20000000、entry _start) |
| 实测 | WCH-LinkE mode:RV fw 2.18、6000 kHz、datacount=2 progbufsize=8、XLEN=32 misa=0x40901125、flash device id 0xf53a8220 |
容量口径:
flash probe报的 288 kB 是整片物理 flash(含厂商区),用户可用区按链接脚本 是 256 kB。地址计算以链接脚本 / ELF 为准。
真机验证记录
CH32V307EVT + WCH-LinkE(RV 模式)上跑通的完整闭环:
wch_env → wch_devices → wch_probe → wch_flash_info → wch_regs → wch_mem
→ wch_read_flash(262144 B) → wch_flash 回写同一份 → verify OK → reset run
→ session 内 bp(IVA 硬件断点;内存逐字节比对未变)→ close(rbp all → shutdown)
→ wch_gdb(info registers / x/4xw) → wch_clean audit → 无残留进程、端口全空
→ IDE bin 树 25 个文件名字/大小/mtime 全部未变
许可
MIT —— 见 LICENSE。覆盖本插件自己的代码与文档。
例外:vendor/openocd/ 与 references/raw/ 是第三方产物
(WCH 定制的 OpenOCD 0.11.0+dev-snapshot,与两份从 WCH-Link 手册 PDF 抽出的文本),
不适用 MIT —— 详见 THIRD-PARTY.md。
OpenOCD 遵循其上游 GNU GPL v2(vendor/openocd/distro-info/licenses/)。
打包它的唯一理由是设计上的那条硬要求:插件必须带自己的隔离副本,绝不碰 IDE 那棵树。
仓库里刻意没有的东西
.gitignore 排除的不是顺手清理,是两条有理由的:
backups/:从用户板子里读出来的整片固件镜像(CH32V307 一份 256 KiB)。 既是本地状态,也是能直接还原别人板子的二进制。vendor/openocd/bin/里的ocd.cfg/ocd_try.err/ocd_try.out: WCH 出厂就带在 IDEbin\里的污染文件,ocd.cfg与wch32f103.cfg逐字节相同。 它正是「所有烧录被静默顶成 ARM/SWD」那条事故链的物证 —— 这个插件存在的头号理由就是防它,公开发布时不该把这份毒一起发出去。** 本地保留(保持副本与 IDE 树一致,便于复现原事故),repo 里没有。 因此新克隆的副本比 IDE 树少 3 个文件,wch_workspace(action:"audit")会报得更干净 —— 这是预期的**,不是副本缺文件。
No comments yet. Be the first to write one.