
Openocd
- 415 installs
- 534 repo stars
- Updated June 29, 2026
- zhinkgit/embeddedskills
openocd is an agent skill that runs OpenOCD for ARM probe bring-up, firmware flash, GDB/Telnet debugging, and Semihosting or ITM output capture.
About
openocd is an agent skill for solo hardware and firmware builders who need OpenOCD driven from the terminal without juggling opaque one-off scripts. It wraps probe detection, image programming, flash erase modes, GDB server startup, target reset, interactive Telnet debugging, structured GDB sessions, and Semihosting or ITM trace capture for ARM targets. Configuration splits environment-level skill/config.json (executable path, scripts directory, GDB and Telnet ports, gdb_exe, and operation_mode for direct run, risk summary, or confirm-before-execute) from per-project .e-style settings described in the skill. Prerequisites are a installed OpenOCD binary, Python 3 stdlib only, appropriate debugger drivers, and Arm GNU Toolchain when you use GDB subcommands. Use it while iterating firmware on real boards when you need repeatable flash-and-debug steps your coding agent can invoke instead of manual copy-paste of OpenOCD commands.
- Probe connectivity, firmware program (.elf/.hex/.bin), and Flash erase (auto|mass|sector)
- GDB Server on configurable port plus Telnet halt/resume/step, registers, memory, hardware breakpoints, run-to
- GDB command sequences, backtrace and locals shortcuts via arm-none-eabi-gdb
- Semihosting printf capture and ITM/SWO output observation
- Supports ST-Link, CMSIS-DAP, DAPLink, FTDI with skill/config.json operation modes 1–3
Openocd by the numbers
- 415 all-time installs (skills.sh)
- +23 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #131 of 550 CLI & Terminal skills by installs in the Skillselion catalog
- Security screen: LOW risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/zhinkgit/embeddedskills --skill openocdAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 415 |
|---|---|
| repo stars | ★ 534 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 29, 2026 |
| Repository | zhinkgit/embeddedskills ↗ |
What it does
Flash firmware, start GDB/Telnet debug sessions, and capture Semihosting or ITM output on ARM boards via OpenOCD from your agent.
Who is it for?
Best when you're on STM32-class ARM boards using ST-Link, CMSIS-DAP, or DAPLink and want agent-assisted flash and debug.
Skip if: Pure web or mobile app developers with no on-target debugging, or teams forbidden from shell access to hardware tools.
When should I use this skill?
You need OpenOCD probe detection, firmware programming, flash erase, GDB server, target reset, Telnet or GDB interactive debug, or Semihosting/ITM output on ARM hardware.
What you get
Your agent executes configured OpenOCD operations—flash, erase, debug server, and trace capture—using paths and ports from config.json with optional safety confirmation modes.
- Programmed firmware image on target flash
- Running GDB/Telnet debug session or captured Semihosting/ITM log output
By the numbers
- Default GDB port 3333 and Telnet port 4444 in config
- Flash erase modes: auto, mass, and sector
- Three operation_mode values: direct execute, risk summary, confirm before execute
Files
OpenOCD 下载与调试
本 skill 提供 OpenOCD 的探针探测、固件烧录、Flash 擦除、GDB Server 启动、目标复位、 Telnet 在线调试、GDB 源码级调试和 Semihosting 输出捕获能力。
配置
环境级配置(skill/config.json)
skill 目录下的 config.json 包含环境级配置(工具路径、端口号等),首次使用前确认 exe 路径正确:
{
"exe": "openocd",
"scripts_dir": "",
"gdb_port": 3333,
"telnet_port": 4444,
"gdb_exe": "",
"operation_mode": 1
}exe:openocd.exe 路径或命令名(必填)scripts_dir:OpenOCD 配置脚本目录,为空时使用 OpenOCD 内置路径gdb_port:GDB Server 端口,默认 3333telnet_port:Telnet 端口,默认 4444gdb_exe:arm-none-eabi-gdb 路径,GDB 调试子命令(run/backtrace/locals)需要operation_mode:1直接执行 /2输出风险摘要但不阻塞 /3执行前确认
工程级配置(.embeddedskills/config.json)
board/interface/target 等工程参数统一在工作区的 .embeddedskills/config.json 中管理:
{
"openocd": {
"board": "",
"interface": "interface/stlink.cfg",
"target": "target/stm32f4x.cfg",
"adapter_speed": "4000",
"transport": "swd",
"tpiu_name": "stm32f4x.tpiu",
"traceclk": "168000000",
"pin_freq": "2000000"
}
}board:board 配置(如board/stm32f4discovery.cfg),优先级高于 interface+targetinterface:interface 配置(如interface/stlink.cfg)target:target 配置(如target/stm32f4x.cfg)adapter_speed:调试速率 kHztransport:传输协议(swd/jtag)tpiu_name/traceclk/pin_freq:ITM/SWO 观测所需的 TPIU 参数
参数解析优先级:CLI 显式参数 > `.embeddedskills/config.json`(工程级)> `skill/config.json`(环境级)> `.embeddedskills/state.json` > 默认值/报错
成功执行后,确认过的参数会自动写回工程配置。
子命令
基础操作
| 子命令 | 用途 | 风险 |
|---|---|---|
probe | 验证 board 或 interface+target 组合,探测目标连通性 | 低 |
flash | 烧录固件(.elf / .hex / .bin) | 高 |
erase | 擦除目标 Flash | 高 |
reset / reset-init | 复位目标芯片 | 高 |
targets / flash-banks / adapter-info | 查询底层 target / flash / adapter 信息 | 低 |
raw | 执行受控 OpenOCD 原生命令 | 高 |
GDB Server
| 子命令 | 用途 | 风险 |
|---|---|---|
gdb-server | 启动 GDB Server,保持运行等待 GDB 连接 | 低 |
gdb backtrace/locals | 快捷获取调用栈和局部变量 | 低 |
gdb break/continue/next/step/finish/until | one-shot 执行流控制 | 低 |
gdb frame/print/watch/disassemble/threads/crash-report | one-shot 源码级诊断 | 低 |
Telnet 在线调试
| 子命令 | 用途 | 风险 |
|---|---|---|
halt | 暂停 CPU,返回 PC/xPSR | 低 |
resume | 恢复 CPU 运行 | 低 |
step | 单步执行(支持 --count N) | 低 |
reg | 查看所有 CPU 寄存器 | 低 |
read-mem | 读内存(--width 8/16/32,--length N) | 低 |
write-mem | 写内存(--width 8/16/32) | 高 |
bp | 设置硬件断点 | 低 |
rbp | 移除断点 | 低 |
run-to | 运行到指定地址(设置断点 + resume + 等待命中) | 低 |
Semihosting
| 子命令 | 用途 | 风险 |
|---|---|---|
semihosting | 启用 ARM Semihosting 并捕获目标 printf 输出 | 低 |
itm | 基于 TPIU/ITM 读取 SWO/ITM 观测数据 | 低 |
执行流程
1. 读取 skill/config.json,确认 exe 路径有效 2. 读取 .embeddedskills/config.json 获取工程级配置(board/interface/target 等) 3. 读取 .embeddedskills/state.json 获取历史状态 4. 参数解析优先级:CLI 显式参数 > `.embeddedskills/config.json`(工程级)> `skill/config.json`(环境级)> `.embeddedskills/state.json` > 默认值/报错 5. 已知 board 时优先使用 -f board/*.cfg,否则组合 -f interface/*.cfg -f target/*.cfg 6. board、interface、target 同时缺失时,不自动拼接组合,直接要求用户补充 7. 按 operation_mode 决定是否需要确认后执行 8. 根据子命令调用对应脚本 9. 统一解析输出中的 Info、Error 和就绪日志,返回结构化结果 10. 成功执行后,将确认过的参数写回 .embeddedskills/config.json
脚本调用
skill 目录下有五个 Python 脚本,使用标准库实现,无额外依赖。
openocd_run.py — 探测 / 烧录 / 擦除 / 复位
# 探测连通性(使用 board)
python <skill-dir>/scripts/openocd_run.py probe --board board/stm32f4discovery.cfg --json
# 探测连通性(使用 interface + target)
python <skill-dir>/scripts/openocd_run.py probe --interface interface/stlink.cfg --target target/stm32f4x.cfg --json
# 烧录 ELF 固件
python <skill-dir>/scripts/openocd_run.py flash --file build/app.elf --interface interface/stlink.cfg --target target/stm32f4x.cfg --json
# 烧录 BIN 固件(必须提供地址)
python <skill-dir>/scripts/openocd_run.py flash --file build/app.bin --address 0x08000000 --interface interface/stlink.cfg --target target/stm32f4x.cfg --json
# 擦除 Flash(自动选择 mass/sector)
python <skill-dir>/scripts/openocd_run.py erase --mode auto --interface interface/stlink.cfg --target target/stm32f4x.cfg --json
# 复位目标
python <skill-dir>/scripts/openocd_run.py reset --mode halt --interface interface/stlink.cfg --target target/stm32f4x.cfg --json
# 查询 target 列表
python <skill-dir>/scripts/openocd_run.py targets --interface interface/stlink.cfg --target target/stm32f4x.cfg --json
# 查询 flash bank
python <skill-dir>/scripts/openocd_run.py flash-banks --interface interface/stlink.cfg --target target/stm32f4x.cfg --json
# 执行受控 raw 命令
python <skill-dir>/scripts/openocd_run.py raw --interface interface/stlink.cfg --target target/stm32f4x.cfg --command "init" "targets" --json通用可选参数:--board <cfg>、--search <目录>、--adapter-speed <kHz>、--transport <swd|jtag>、--exe <openocd路径>
openocd_gdb.py — GDB Server 启动与调试
# 启动 GDB Server(保持运行)
python <skill-dir>/scripts/openocd_gdb.py server --interface interface/stlink.cfg --target target/stm32f4x.cfg --json
# 无子命令时默认 server(向后兼容)
python <skill-dir>/scripts/openocd_gdb.py --interface interface/stlink.cfg --target target/stm32f4x.cfg --json
# 执行自定义 GDB 命令序列
python <skill-dir>/scripts/openocd_gdb.py run --gdb-exe arm-none-eabi-gdb --elf build/app.elf --interface interface/stlink.cfg --target target/stm32f4x.cfg --commands "break main" "continue" "backtrace" "info locals" --json
# 快捷获取调用栈
python <skill-dir>/scripts/openocd_gdb.py backtrace --gdb-exe arm-none-eabi-gdb --elf build/app.elf --interface interface/stlink.cfg --target target/stm32f4x.cfg --json
# 快捷查看局部变量
python <skill-dir>/scripts/openocd_gdb.py locals --gdb-exe arm-none-eabi-gdb --elf build/app.elf --interface interface/stlink.cfg --target target/stm32f4x.cfg --json可选参数:--gdb-port、--telnet-port、--search、--adapter-speed、--transport、--board
openocd_telnet.py — Telnet 在线调试
# 暂停 CPU
python <skill-dir>/scripts/openocd_telnet.py halt --interface interface/stlink.cfg --target target/stm32f4x.cfg --json
# 恢复运行
python <skill-dir>/scripts/openocd_telnet.py resume --interface interface/stlink.cfg --target target/stm32f4x.cfg --json
# 单步 5 次
python <skill-dir>/scripts/openocd_telnet.py step --count 5 --interface interface/stlink.cfg --target target/stm32f4x.cfg --json
# 查看寄存器
python <skill-dir>/scripts/openocd_telnet.py reg --interface interface/stlink.cfg --target target/stm32f4x.cfg --json
# 读内存(32bit x 16 个字)
python <skill-dir>/scripts/openocd_telnet.py read-mem --address 0x20000000 --length 16 --width 32 --interface interface/stlink.cfg --target target/stm32f4x.cfg --json
# 写内存
python <skill-dir>/scripts/openocd_telnet.py write-mem --address 0x20000000 --value 0xDEADBEEF --width 32 --interface interface/stlink.cfg --target target/stm32f4x.cfg --json
# 设置硬件断点
python <skill-dir>/scripts/openocd_telnet.py bp --address 0x08001234 --bp-length 2 --interface interface/stlink.cfg --target target/stm32f4x.cfg --json
# 移除断点
python <skill-dir>/scripts/openocd_telnet.py rbp --address 0x08001234 --interface interface/stlink.cfg --target target/stm32f4x.cfg --json
# 运行到指定地址(设置断点 + resume + 等待)
python <skill-dir>/scripts/openocd_telnet.py run-to --address 0x08001234 --timeout-ms 3000 --interface interface/stlink.cfg --target target/stm32f4x.cfg --json通用可选参数:--board、--search、--adapter-speed、--transport、--gdb-port、--telnet-port
openocd_semihosting.py — Semihosting 输出捕获
# 捕获 semihosting 输出(持续到 Ctrl+C)
python <skill-dir>/scripts/openocd_semihosting.py --interface interface/stlink.cfg --target target/stm32f4x.cfg --json
# 捕获 30 秒
python <skill-dir>/scripts/openocd_semihosting.py --timeout 30 --interface interface/stlink.cfg --target target/stm32f4x.cfg --jsonopenocd_itm.py — ITM/SWO 观测
# 使用配置中的 TPIU 参数持续读取 ITM
python <skill-dir>/scripts/openocd_itm.py --interface interface/stlink.cfg --target target/stm32f4x.cfg --json
# 指定 TPIU 名称和端口
python <skill-dir>/scripts/openocd_itm.py \
--interface interface/stlink.cfg --target target/stm32f4x.cfg \
--tpiu-name stm32f4x.tpiu --traceclk 168000000 --pin-freq 2000000 \
--itm-port 0 --itm-port 1 --jsonopenocd_itm.py 会启动独立 OpenOCD Server,打开 TPIU/ITM 配置后,把 trace 输出统一包装成 JSON Lines。
调试典型工作流
快速检查(Telnet)
halt -> reg -> read-mem 0x20000000 -> step -> resume适合快速查看当前 CPU 状态和内存内容。
断点调试(Telnet)
run-to 0x08001234 -> reg -> read-mem -> resume运行到指定地址后暂停,检查寄存器和内存。
源码级调试(GDB)
gdb run --elf app.elf --commands "break main" "continue" "backtrace" "info locals"使用 ELF 文件提供符号信息,进行函数级断点和变量查看。
Semihosting 输出
semihosting捕获目标通过 printf(SVC 指令)输出的调试信息,类似 J-Link RTT。
输出格式
所有脚本以 JSON 格式返回,基础字段为 status(ok/error)、action、summary、details,并可能附带 context、artifacts、metrics、state、next_actions、timing。流式观测命令使用 JSON Lines,并统一输出 source、channel_type、stream_type。
成功示例:
{
"status": "ok",
"action": "halt",
"summary": "已暂停,PC=0x08000298",
"details": {
"pc": "0x08000298",
"xpsr": "0x01000000",
"msp": "0x20020000",
"halted": true
}
}GDB 调用栈示例:
{
"status": "ok",
"action": "backtrace",
"summary": "GDB backtrace 执行成功",
"details": {
"gdb_port": 3333,
"frames": [
{"frame": 0, "function": "main", "location": "src/main.c:42"}
]
}
}错误示例:
{
"status": "error",
"action": "halt",
"error": {
"code": "server_failed",
"message": "OpenOCD 启动失败或超时"
}
}核心规则
- 不自动猜测
board、interface、target的组合,缺失时必须询问用户 - 已知
board时优先使用 board 配置,不再需要 interface+target - 参数解析优先级为:CLI 显式参数 > `.embeddedskills/config.json`(工程级)> `skill/config.json`(环境级)> `.embeddedskills/state.json` > 默认值/报错
.bin文件必须显式提供烧录地址,缺失时报错- STM32F4 等已映射 target 在
erase --mode auto下会先reset halt,再优先使用 mass erase erase --mode mass在未命中映射时直接返回mass_erase_unsupported,避免误以为已做整片擦除- 检测到 Flash 锁保护时只提示,不自动解锁
- 连接失败时给出排查建议(检查连线、供电、驱动、cfg 路径),不自动尝试更激进参数
- 烧录、擦除、复位在参数完整且用户意图明确时直接执行
gdb-server启动后返回端口和连接方式,进程保持运行- GDB 调试(run/backtrace/locals)需要配置
gdb_exe(arm-none-eabi-gdb 路径) - Telnet 调试命令每次启动独立的 OpenOCD Server,执行完自动关闭
- Semihosting 通过 Telnet 启用后持续读取 OpenOCD stderr 输出
- 结果回显中始终包含 cfg 组合、端口和执行动作
- 产物路径(elf/file)不进入工程配置,仍只依赖
state.json last_flash/last_debug/last_observe等运行状态继续写入state.json
参考
遇到 board/interface/target 配置问题时可查阅 references/common_targets.md。
{
"exe": "openocd",
"scripts_dir": "",
"gdb_port": 3333,
"telnet_port": 4444,
"gdb_exe": "",
"operation_mode": 1
}
openocd
Claude Code skill,通过 OpenOCD 进行探针探测、固件烧录、Flash 擦除、GDB Server 启动、目标复位、Telnet 在线调试、GDB 源码级调试以及 Semihosting/ITM 输出捕获。支持 ST-Link、CMSIS-DAP、DAPLink、FTDI 等开源调试器。
功能
- 探针与目标连通性探测
- 固件烧录(.elf / .hex / .bin)
- Flash 擦除(支持
auto|mass|sector模式) - GDB Server 启动(供 GDB 连接进行源码级调试)
- 目标复位(支持 halt/run 模式)
- Telnet 在线调试:halt / resume / step / 寄存器查看 / 内存读写 / 硬件断点 / run-to
- GDB 调试交互:执行自定义 GDB 命令序列、快捷调用栈查看、局部变量查看
- Semihosting 输出捕获:捕获目标
printf输出(ARM Semihosting,类似 J-Link RTT) - ITM/SWO 观测:基于 TPIU/ITM 读取 SWO 输出
环境要求
- OpenOCD — 安装后确保
openocd可执行或填写完整路径 - Python 3.x(仅标准库,无额外依赖)
- 调试器驱动(ST-Link 需要 ST 官方驱动或 WinUSB/libusb,CMSIS-DAP 免驱)
- Arm GNU Toolchain(GDB 调试子命令需要
arm-none-eabi-gdb)
配置
环境级配置(skill/config.json)
复制 config.example.json 为 config.json,根据实际环境修改:
{
"exe": "openocd",
"scripts_dir": "",
"gdb_port": 3333,
"telnet_port": 4444,
"gdb_exe": "",
"operation_mode": 1
}| 字段 | 必填 | 说明 |
|---|---|---|
exe | 是 | openocd 路径或命令名 |
scripts_dir | 否 | OpenOCD 配置脚本目录,为空时使用内置路径 |
gdb_port | 否 | GDB Server 端口,默认 3333 |
telnet_port | 否 | Telnet 端口,默认 4444 |
gdb_exe | 否 | arm-none-eabi-gdb 路径,GDB 调试子命令需要 |
operation_mode | 否 | 1 直接执行 / 2 输出风险摘要 / 3 执行前确认 |
工程级配置(.embeddedskills/config.json)
board/interface/target 等工程参数统一在工作区的 .embeddedskills/config.json 中管理:
{
"openocd": {
"board": "",
"interface": "interface/stlink.cfg",
"target": "target/stm32f4x.cfg",
"adapter_speed": "4000",
"transport": "swd",
"tpiu_name": "stm32f4x.tpiu",
"traceclk": "168000000",
"pin_freq": "2000000"
}
}参数解析优先级:CLI 参数 > 工程配置 > state.json > 默认值
成功执行后,确认过的参数会自动写回工程配置。
当前实现的基础命令还包括 targets、flash-banks、adapter-info、raw 和 gdb-server,观测命令除了 semihosting 还支持 itm。
擦除行为
erase --mode auto:优先使用 target 映射到的 mass erase,未命中时回退 sector eraseerase --mode mass:强制整片擦除;当前 target 没有映射时返回mass_erase_unsupportederase --mode sector:强制按 bank 执行flash erase_sector <bank> 0 last.bin烧录必须显式提供地址,例如0x08000000
OpenOCD 常见 Board / Interface / Target 配置速查表
OpenOCD 通过 .cfg 配置文件组合来描述调试链路。优先使用 board 配置(已包含 interface 和 target),否则手动组合 interface + target。
Interface(调试器)
| 调试器类型 | 配置文件 |
|---|---|
| ST-Link V2 | interface/stlink.cfg |
| ST-Link V3 | interface/stlink.cfg |
| CMSIS-DAP | interface/cmsis-dap.cfg |
| DAPLink | interface/cmsis-dap.cfg |
| J-Link | interface/jlink.cfg |
| FTDI 系列 | interface/ftdi/minimodule.cfg 等 |
Target(目标芯片)
STMicroelectronics
| 系列 | 配置文件 |
|---|---|
| STM32F0 | target/stm32f0x.cfg |
| STM32F1 | target/stm32f1x.cfg |
| STM32F2 | target/stm32f2x.cfg |
| STM32F3 | target/stm32f3x.cfg |
| STM32F4 | target/stm32f4x.cfg |
| STM32F7 | target/stm32f7x.cfg |
| STM32G0 | target/stm32g0x.cfg |
| STM32G4 | target/stm32g4x.cfg |
| STM32H7 | target/stm32h7x.cfg |
| STM32L0 | target/stm32l0x.cfg |
| STM32L1 | target/stm32l1x.cfg |
| STM32L4 | target/stm32l4x.cfg |
| STM32U5 | target/stm32u5x.cfg |
| STM32WB | target/stm32wbx.cfg |
| STM32WL | target/stm32wlx.cfg |
GigaDevice
| 系列 | 配置文件 |
|---|---|
| GD32F1x3 | target/stm32f1x.cfg(兼容) |
| GD32F3x0 | target/stm32f1x.cfg(兼容) |
| GD32F4xx | target/stm32f4x.cfg(兼容) |
| GD32E103 | target/stm32f1x.cfg(兼容) |
GigaDevice 芯片通常兼容对应 STM32 系列的 target 配置。
Nordic Semiconductor
| 系列 | 配置文件 |
|---|---|
| nRF51 | target/nrf51.cfg |
| nRF52 | target/nrf52.cfg |
NXP
| 系列 | 配置文件 |
|---|---|
| LPC1768 | target/lpc1768.cfg |
| LPC4088 | target/lpc4088.cfg |
ESP32
| 系列 | 配置文件 |
|---|---|
| ESP32 | target/esp32.cfg |
| ESP32-S2 | target/esp32s2.cfg |
| ESP32-S3 | target/esp32s3.cfg |
| ESP32-C3 | target/esp32c3.cfg |
Board(开发板,已包含 interface + target)
| 开发板 | 配置文件 |
|---|---|
| STM32F4 Discovery | board/stm32f4discovery.cfg |
| STM32F429 Discovery | board/stm32f429disc1.cfg |
| STM32F746 Discovery | board/stm32f746g-disco.cfg |
| STM32 Nucleo-F401RE | board/st_nucleo_f4.cfg |
| STM32 Nucleo-L476RG | board/st_nucleo_l476rg.cfg |
| nRF52-DK | board/nordic_nrf52_dk.cfg |
查找完整列表
如果上表未包含目标配置,可通过以下方式查找:
1. 列出 OpenOCD 自带配置:ls <openocd-scripts-dir>/target/ 2. 在 OpenOCD 官方文档搜索: https://openocd.org/doc-release/html/index.html 3. 运行 openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "init; targets; shutdown" 验证组合
"""openocd skill 私有 GDB 工具。"""
from __future__ import annotations
import re
import subprocess
import sys
from pathlib import Path
from typing import Any
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
from openocd_runtime import hidden_subprocess_kwargs
INTROSPECTION_ACTIONS = {
"backtrace",
"locals",
"break",
"frame",
"print",
"watch",
"threads",
"disassemble",
"crash-report",
}
def run_gdb_commands(gdb_exe: str, elf_file: str, target_remote: str, commands: list[str], timeout: int = 30) -> dict:
gdb_init = ["set pagination off", "set confirm off", "set width 0"]
if elf_file:
gdb_init.append(f"file {elf_file}")
gdb_init.append(f"target remote {target_remote}")
cmd = [gdb_exe, "--batch", "--nx"]
for item in gdb_init + commands + ["quit"]:
cmd.extend(["-ex", item])
try:
proc = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=timeout,
encoding="utf-8",
errors="replace",
**hidden_subprocess_kwargs(),
)
return {
"status": "ok" if proc.returncode == 0 else "error",
"stdout": proc.stdout,
"stderr": proc.stderr,
"returncode": proc.returncode,
}
except subprocess.TimeoutExpired:
return {"status": "error", "error": f"GDB 执行超时({timeout}s)"}
except Exception as exc: # pragma: no cover - 兜底异常
return {"status": "error", "error": str(exc)}
def require_action_expr(action: str, expr: str | None, hint: str) -> str:
if not expr:
raise ValueError(f"{action} 必须提供 {hint}")
return expr
def build_gdb_commands(action: str, expr: str | None = None, *, halt_before: bool = True) -> list[str]:
commands: list[str] = []
if halt_before and action in INTROSPECTION_ACTIONS | {"next", "step", "finish", "until"}:
commands.append("monitor halt")
if action == "run":
raise ValueError("run 需要由调用方直接提供 commands")
if action == "backtrace":
commands.append("backtrace")
elif action == "locals":
commands.append("info locals")
elif action == "break":
commands.extend([f"break {require_action_expr(action, expr, '--expr')}", "info breakpoints"])
elif action == "continue":
commands.append("monitor resume")
elif action == "next":
commands.append("next")
elif action == "step":
commands.append("step")
elif action == "finish":
commands.append("finish")
elif action == "until":
commands.append(f"until {expr}" if expr else "until")
elif action == "frame":
commands.append(f"frame {require_action_expr(action, expr, '--expr <帧号>')}")
elif action == "print":
commands.append(f"print {require_action_expr(action, expr, '--expr')}")
elif action == "watch":
commands.extend([f"watch {require_action_expr(action, expr, '--expr')}", "info breakpoints"])
elif action == "disassemble":
commands.append(f"disassemble {expr}" if expr else "disassemble")
elif action == "threads":
commands.extend(["info threads", "thread apply all backtrace 1"])
elif action == "crash-report":
commands.extend(
[
"backtrace full",
"info registers",
"frame 0",
"info locals",
"info threads",
"disassemble /m $pc,$pc+32",
]
)
else:
raise ValueError(f"未知 GDB 子命令: {action}")
return commands
def _parse_frames(stdout: str) -> list[dict[str, Any]]:
frames = []
for line in stdout.splitlines():
match = re.match(
r"#(?P<index>\d+)\s+(?:(?P<address>0x[0-9a-fA-F]+)\s+in\s+)?(?P<function>[^\s(]+)?\s*\((?P<args>[^)]*)\)(?:\s+at\s+(?P<location>.+))?",
line.strip(),
)
if not match:
continue
frame = {
"frame": int(match.group("index")),
"function": match.group("function") or "??",
}
if match.group("address"):
frame["address"] = match.group("address")
if match.group("args"):
frame["args"] = match.group("args").strip()
if match.group("location"):
frame["location"] = match.group("location").strip()
frames.append(frame)
return frames
def _parse_variables(stdout: str) -> dict[str, str]:
variables: dict[str, str] = {}
for line in stdout.splitlines():
match = re.match(r"^([A-Za-z_][\w.\->\[\]]*)\s*=\s*(.+)$", line.strip())
if match:
variables[match.group(1)] = match.group(2).strip()
return variables
def _parse_registers(stdout: str) -> dict[str, str]:
registers: dict[str, str] = {}
for line in stdout.splitlines():
match = re.match(r"^([A-Za-z_][\w]*)\s+(0x[0-9a-fA-F]+)\b(.*)$", line.strip())
if match:
registers[match.group(1)] = match.group(2)
return registers
def _parse_threads(stdout: str) -> list[dict[str, Any]]:
threads: list[dict[str, Any]] = []
for line in stdout.splitlines():
match = re.match(r"^([* ])\s*(\d+)\s+Thread\s+(.+)$", line.strip())
if match:
threads.append(
{
"selected": match.group(1) == "*",
"id": int(match.group(2)),
"description": match.group(3).strip(),
}
)
return threads
def _parse_disassembly(stdout: str) -> list[dict[str, str]]:
items: list[dict[str, str]] = []
for line in stdout.splitlines():
match = re.match(r"^(=>)?\s*(0x[0-9a-fA-F]+)(?:\s+<([^>]+)>)?:\s+(.+)$", line.strip())
if match:
item = {
"address": match.group(2),
"instruction": match.group(4).strip(),
}
if match.group(1):
item["selected"] = "true"
if match.group(3):
item["symbol"] = match.group(3).strip()
items.append(item)
return items
def _extract_source_location(stdout: str, frames: list[dict[str, Any]]) -> str:
for frame in frames:
location = frame.get("location", "")
if location:
return location
match = re.search(r'at\s+([A-Za-z]:)?[^:\n]+\:\d+', stdout)
return match.group(0).replace("at ", "").strip() if match else ""
def _parse_selected_frame(stdout: str) -> dict[str, Any]:
match = re.search(r"#(?P<index>\d+)\s+.+?(?:at\s+(?P<location>.+))?$", stdout, re.MULTILINE)
if not match:
return {}
selected = {"frame": int(match.group("index"))}
if match.group("location"):
selected["location"] = match.group("location").strip()
return selected
def parse_gdb_output(stdout: str, action: str) -> dict:
frames = _parse_frames(stdout)
variables = _parse_variables(stdout)
registers = _parse_registers(stdout)
threads = _parse_threads(stdout)
disassembly = _parse_disassembly(stdout)
parsed: dict[str, Any] = {"output": stdout}
if frames:
parsed["frames"] = frames
if variables:
parsed["variables"] = variables
if registers:
parsed["registers"] = registers
if threads:
parsed["threads"] = threads
if disassembly:
parsed["disassembly"] = disassembly
selected_frame = _parse_selected_frame(stdout)
if not selected_frame and frames:
selected_frame = frames[0]
if selected_frame:
parsed["selected_frame"] = selected_frame
source_location = _extract_source_location(stdout, frames)
if source_location:
parsed["source_location"] = source_location
if action == "print":
match = re.search(r"\$\d+\s*=\s*(.+)", stdout)
if match:
parsed["value"] = match.group(1).strip()
return parsed
"""OpenOCD GDB Server 启动与 one-shot 调试。"""
from __future__ import annotations
import argparse
import os
import signal
import subprocess
import sys
import time
from pathlib import Path
from shutil import which
ROOT_DIR = Path(__file__).resolve().parents[2]
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
from openocd_gdb_common import build_gdb_commands, parse_gdb_output, run_gdb_commands # noqa: E402
from openocd_runtime import ( # noqa: E402
build_artifacts,
default_config_path,
get_state_entry,
hidden_subprocess_kwargs,
is_missing,
load_json_file,
load_local_config,
load_project_config,
load_workspace_state,
make_result,
make_timing,
normalize_path,
now_iso,
output_json,
parameter_context,
resolve_param,
save_project_config,
update_state_entry,
workspace_root,
)
GDB_ACTIONS = [
"server",
"run",
"backtrace",
"locals",
"break",
"continue",
"next",
"step",
"finish",
"until",
"frame",
"print",
"watch",
"disassemble",
"threads",
"crash-report",
]
def build_openocd_cmd(
exe: str,
board: str = "",
interface: str = "",
target: str = "",
search: str = "",
adapter_speed: str = "",
transport: str = "",
gdb_port: int = 3333,
telnet_port: int = 4444,
) -> list[str]:
cmd = [exe]
if search:
cmd.extend(["-s", search])
if board:
cmd.extend(["-f", board])
else:
if interface:
cmd.extend(["-f", interface])
if target:
cmd.extend(["-f", target])
if adapter_speed:
cmd.extend(["-c", f"adapter speed {adapter_speed}"])
if transport:
cmd.extend(["-c", f"transport select {transport}"])
cmd.extend(["-c", f"gdb_port {gdb_port}"])
cmd.extend(["-c", f"telnet_port {telnet_port}"])
return cmd
def start_openocd_server(cmd: list[str]) -> subprocess.Popen:
popen_kwargs = hidden_subprocess_kwargs()
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP if sys.platform == "win32" else 0
if popen_kwargs.get("creationflags"):
creationflags |= popen_kwargs["creationflags"]
return subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
creationflags=creationflags,
startupinfo=popen_kwargs.get("startupinfo"),
)
def wait_server_ready(proc: subprocess.Popen, gdb_port: int, timeout: int = 15) -> tuple[bool, list[str]]:
started = time.time()
errors: list[str] = []
ready = False
while time.time() - started < timeout:
if proc.poll() is not None:
remaining = proc.stderr.read()
for line in remaining.splitlines():
if "Error:" in line:
errors.append(line.strip())
return False, errors
line = proc.stderr.readline()
if not line:
time.sleep(0.1)
continue
stripped = line.strip()
if "Error:" in stripped:
errors.append(stripped)
if f"Listening on port {gdb_port}" in stripped or "listening on" in stripped.lower():
ready = True
break
if not ready:
return False, errors
critical = [
"open failed",
"init mode failed",
"no device found",
"cannot connect",
"error connecting dp",
"examination failed",
"failed to read memory",
"failed to write memory",
"cannot read idr",
"polling failed",
]
critical_errors = [item for item in errors if any(keyword in item.lower() for keyword in critical)]
if critical_errors:
return False, critical_errors
return True, errors
def cleanup(proc: subprocess.Popen | None) -> None:
if proc and proc.poll() is None:
try:
if sys.platform == "win32":
proc.terminate()
else:
proc.send_signal(signal.SIGTERM)
proc.wait(timeout=5)
except (subprocess.TimeoutExpired, OSError):
proc.kill()
def add_common_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument("--exe", default=None, help="openocd 路径")
parser.add_argument("--board", default=None, help="board 配置文件")
parser.add_argument("--interface", default=None, help="interface 配置文件")
parser.add_argument("--target", default=None, help="target 配置文件")
parser.add_argument("--search", default=None, help="额外配置脚本搜索目录")
parser.add_argument("--adapter-speed", default=None, help="调试速率 kHz")
parser.add_argument("--transport", default=None, choices=["", "swd", "jtag"], help="传输协议")
parser.add_argument("--gdb-port", type=int, default=None, help="GDB 端口")
parser.add_argument("--telnet-port", type=int, default=None, help="Telnet 端口")
parser.add_argument("--gdb-exe", default=None, help="arm-none-eabi-gdb 路径")
parser.add_argument("--elf", default=None, help="ELF 文件路径")
parser.add_argument("--config", default=None, help="skill config.json 路径")
parser.add_argument("--workspace", default=None, help="workspace 根目录,默认当前目录")
parser.add_argument("--json", action="store_true", dest="as_json")
def build_parser(legacy_server: bool) -> argparse.ArgumentParser:
if legacy_server:
parser = argparse.ArgumentParser(description="OpenOCD GDB Server 启动")
add_common_args(parser)
return parser
parser = argparse.ArgumentParser(description="OpenOCD GDB Server 启动与调试")
sub = parser.add_subparsers(dest="command")
for name in GDB_ACTIONS:
sub_parser = sub.add_parser(name, help=f"GDB {name}")
add_common_args(sub_parser)
if name == "run":
sub_parser.add_argument("--commands", nargs="+", required=True, help="GDB 命令序列")
elif name in {"break", "frame", "print", "watch"}:
sub_parser.add_argument("--expr", required=True, help="表达式或参数")
elif name in {"until", "disassemble"}:
sub_parser.add_argument("--expr", default=None, help="表达式或参数")
return parser
def _legacy_mode() -> bool:
return not (len(sys.argv) > 1 and sys.argv[1] in GDB_ACTIONS)
def _state_lookup(state: dict) -> dict:
last_build = get_state_entry(state, "last_build")
last_flash = get_state_entry(state, "last_flash")
last_debug = get_state_entry(state, "last_debug")
artifacts = last_build.get("artifacts", {})
return {
"board": last_debug.get("board") or last_flash.get("board"),
"interface": last_debug.get("interface") or last_flash.get("interface"),
"target": last_debug.get("target") or last_flash.get("target"),
"search": last_debug.get("search"),
"adapter_speed": last_debug.get("adapter_speed") or last_flash.get("adapter_speed"),
"transport": last_debug.get("transport") or last_flash.get("transport"),
"gdb_port": last_debug.get("gdb_port"),
"telnet_port": last_debug.get("telnet_port"),
"elf_file": last_build.get("debug_file") or last_build.get("elf_file") or artifacts.get("debug_file"),
"debug_file": last_build.get("debug_file") or artifacts.get("debug_file"),
}
def resolve_openocd_params(args, project_config: dict, state_lookup: dict) -> dict:
"""解析 OpenOCD 工程级参数,优先级: CLI > 工程配置 > state.json"""
# board: CLI > 工程配置 > state
board = args.board
board_source = "cli"
if is_missing(board):
board = project_config.get("board")
board_source = "project_config"
if is_missing(board):
board = state_lookup.get("board")
board_source = "state"
# interface: CLI > 工程配置 > state
interface = args.interface
interface_source = "cli"
if is_missing(interface):
interface = project_config.get("interface")
interface_source = "project_config"
if is_missing(interface):
interface = state_lookup.get("interface")
interface_source = "state"
# target: CLI > 工程配置 > state
target = args.target
target_source = "cli"
if is_missing(target):
target = project_config.get("target")
target_source = "project_config"
if is_missing(target):
target = state_lookup.get("target")
target_source = "state"
# adapter_speed: CLI > 工程配置 > state
adapter_speed = args.adapter_speed
adapter_speed_source = "cli"
if is_missing(adapter_speed):
adapter_speed = project_config.get("adapter_speed")
adapter_speed_source = "project_config"
if is_missing(adapter_speed):
adapter_speed = state_lookup.get("adapter_speed")
adapter_speed_source = "state"
# transport: CLI > 工程配置 > state
transport = args.transport
transport_source = "cli"
if is_missing(transport):
transport = project_config.get("transport")
transport_source = "project_config"
if is_missing(transport):
transport = state_lookup.get("transport")
transport_source = "state"
return {
"board": board,
"board_source": board_source,
"interface": interface,
"interface_source": interface_source,
"target": target,
"target_source": target_source,
"adapter_speed": adapter_speed,
"adapter_speed_source": adapter_speed_source,
"transport": transport,
"transport_source": transport_source,
}
def _summary(command: str, parsed: dict) -> str:
if command == "server":
return "gdb server 已就绪"
if command == "backtrace" and parsed.get("frames"):
return f"backtrace 完成,frames={len(parsed['frames'])}"
if command == "locals" and parsed.get("variables"):
return f"locals 完成,variables={len(parsed['variables'])}"
if command == "threads" and parsed.get("threads"):
return f"threads 完成,threads={len(parsed['threads'])}"
if command == "print" and parsed.get("value"):
return f"print 完成,value={parsed['value']}"
return f"gdb {command} 完成"
def _metrics(parsed: dict) -> dict:
metrics: dict[str, int] = {}
if parsed.get("frames"):
metrics["frames"] = len(parsed["frames"])
if parsed.get("variables"):
metrics["variables"] = len(parsed["variables"])
if parsed.get("registers"):
metrics["registers"] = len(parsed["registers"])
if parsed.get("threads"):
metrics["threads"] = len(parsed["threads"])
if parsed.get("disassembly"):
metrics["instructions"] = len(parsed["disassembly"])
return metrics
def main() -> None:
legacy_server = _legacy_mode()
parser = build_parser(legacy_server)
args = parser.parse_args()
if legacy_server:
args.command = "server"
started_at = now_iso()
started_ts = time.time()
workspace = workspace_root(args.workspace)
config_path = normalize_path(args.config or str(default_config_path(__file__)))
config = load_json_file(config_path)
state = load_workspace_state(str(workspace))
state_lookup = _state_lookup(state)
project_config = load_project_config(str(workspace))
# 解析 OpenOCD 工程级参数
oc_params = resolve_openocd_params(args, project_config, state_lookup)
parameter_sources: dict[str, str] = {}
try:
exe, parameter_sources["exe"] = resolve_param(
"exe",
args.exe,
config=config,
config_keys=["exe"],
required=True,
)
# 从工程配置或 state 解析 board/interface/target
board = oc_params["board"]
parameter_sources["board"] = oc_params["board_source"]
interface = oc_params["interface"]
parameter_sources["interface"] = oc_params["interface_source"]
target = oc_params["target"]
parameter_sources["target"] = oc_params["target_source"]
adapter_speed = oc_params["adapter_speed"]
parameter_sources["adapter_speed"] = oc_params["adapter_speed_source"]
transport = oc_params["transport"]
parameter_sources["transport"] = oc_params["transport_source"]
search, parameter_sources["search"] = resolve_param(
"search",
args.search,
config=config,
config_keys=["scripts_dir"],
state_record=state_lookup,
state_keys=["search"],
)
gdb_port, parameter_sources["gdb_port"] = resolve_param(
"gdb_port",
args.gdb_port,
config=config,
config_keys=["gdb_port"],
state_record=state_lookup,
state_keys=["gdb_port"],
)
telnet_port, parameter_sources["telnet_port"] = resolve_param(
"telnet_port",
args.telnet_port,
config=config,
config_keys=["telnet_port"],
state_record=state_lookup,
state_keys=["telnet_port"],
)
except ValueError as exc:
result = make_result(
status="error",
action=getattr(args, "command", "server"),
summary=str(exc),
details={},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "missing_param", "message": str(exc)},
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print(f"错误: {exc}", file=sys.stderr)
sys.exit(1)
if not board and not interface and not target:
message = "必须提供 --board 或 --interface + --target"
result = make_result(
status="error",
action=args.command,
summary=message,
details={},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "missing_config", "message": message},
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print(f"错误: {message}", file=sys.stderr)
sys.exit(1)
gdb_exe = None
elf_file = None
if args.command != "server":
try:
gdb_exe, parameter_sources["gdb_exe"] = resolve_param(
"gdb_exe",
args.gdb_exe,
config=config,
config_keys=["gdb_exe"],
normalize_as_path=True,
)
if is_missing(gdb_exe) and not is_missing(project_config.get("gdb_exe")):
gdb_exe = normalize_path(project_config.get("gdb_exe"))
parameter_sources["gdb_exe"] = "project_config:gdb_exe"
if is_missing(gdb_exe):
discovered = which("arm-none-eabi-gdb") or which("arm-none-eabi-gdb.exe")
if discovered:
gdb_exe = normalize_path(discovered)
parameter_sources["gdb_exe"] = "path"
if is_missing(gdb_exe):
raise ValueError("缺少必要参数: gdb_exe")
elf_file, parameter_sources["elf"] = resolve_param(
"elf",
args.elf,
config=config,
config_keys=["default_elf"],
state_record=state_lookup,
state_keys=["elf_file", "debug_file"],
normalize_as_path=True,
)
except ValueError as exc:
result = make_result(
status="error",
action=args.command,
summary=str(exc),
details={},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "missing_param", "message": str(exc)},
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print(f"错误: {exc}", file=sys.stderr)
sys.exit(1)
if not os.path.isfile(gdb_exe):
message = f"arm-none-eabi-gdb 不存在: {gdb_exe}"
result = make_result(
status="error",
action=args.command,
summary=message,
details={},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "gdb_not_found", "message": message},
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print(f"错误: {message}", file=sys.stderr)
sys.exit(1)
cmd = build_openocd_cmd(
exe=exe,
board=board or "",
interface=interface or "",
target=target or "",
search=search or "",
adapter_speed=str(adapter_speed or ""),
transport=transport or "",
gdb_port=int(gdb_port or 3333),
telnet_port=int(telnet_port or 4444),
)
proc: subprocess.Popen | None = None
try:
proc = start_openocd_server(cmd)
ready, errors = wait_server_ready(proc, int(gdb_port or 3333))
if not ready:
message = "; ".join(errors) if errors else "GDB Server 启动失败或超时"
result = make_result(
status="error",
action=args.command,
summary="GDB Server 启动失败",
details={"errors": errors},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "gdbserver_failed", "message": message},
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print(f"[{args.command}] 失败 — {message}", file=sys.stderr)
sys.exit(1)
if args.command == "server":
state_info = update_state_entry(
"last_debug",
{
"provider": "openocd",
"action": "server",
"board": board or "",
"interface": interface or "",
"target": target or "",
"search": search or "",
"adapter_speed": adapter_speed or "",
"transport": transport or "",
"gdb_port": int(gdb_port or 3333),
"telnet_port": int(telnet_port or 4444),
},
str(workspace),
)
result = make_result(
status="ok",
action="server",
summary=_summary("server", {}),
details={
"gdb_port": int(gdb_port or 3333),
"telnet_port": int(telnet_port or 4444),
"pid": proc.pid,
"connect_cmd": f"arm-none-eabi-gdb -ex 'target remote localhost:{int(gdb_port or 3333)}'",
"warnings": errors,
},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
state=state_info,
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print("[gdb-server] GDB Server 已就绪")
print(f" GDB 端口: {int(gdb_port or 3333)}")
print(f" Telnet 端口: {int(telnet_port or 4444)}")
print(f" PID: {proc.pid}")
try:
proc.wait()
except KeyboardInterrupt:
pass
return
try:
if args.command == "run":
gdb_commands = list(args.commands)
else:
gdb_commands = build_gdb_commands(args.command, getattr(args, "expr", None))
except ValueError as exc:
result = make_result(
status="error",
action=args.command,
summary=str(exc),
details={},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "invalid_args", "message": str(exc)},
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print(f"错误: {exc}", file=sys.stderr)
sys.exit(1)
gdb_result = run_gdb_commands(gdb_exe, elf_file or "", f"localhost:{int(gdb_port or 3333)}", gdb_commands)
elapsed_ms = (time.time() - started_ts) * 1000
if gdb_result["status"] == "error":
result = make_result(
status="error",
action=args.command,
summary="GDB 执行失败",
details={"gdb_port": int(gdb_port or 3333), "errors": errors},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
artifacts=build_artifacts(debug_file=elf_file),
error={"code": "gdb_error", "message": gdb_result.get("error", gdb_result.get("stderr", "GDB 执行失败"))},
timing=make_timing(started_at, elapsed_ms),
)
else:
parsed = parse_gdb_output(gdb_result["stdout"], args.command)
artifacts = build_artifacts(debug_file=elf_file)
state_info = update_state_entry(
"last_debug",
{
"provider": "openocd",
"action": args.command,
"board": board or "",
"interface": interface or "",
"target": target or "",
"search": search or "",
"adapter_speed": adapter_speed or "",
"transport": transport or "",
"gdb_port": int(gdb_port or 3333),
"telnet_port": int(telnet_port or 4444),
"debug_file": elf_file or "",
"artifacts": artifacts,
},
str(workspace),
)
# 写回确认过的参数到工程配置
save_project_config(str(workspace), {
"board": board or "",
"interface": interface or "",
"target": target or "",
"adapter_speed": adapter_speed or "",
"transport": transport or "",
})
result = make_result(
status="ok",
action=args.command,
summary=_summary(args.command, parsed),
details={
"gdb_port": int(gdb_port or 3333),
"telnet_port": int(telnet_port or 4444),
"commands": gdb_commands,
"output": parsed.get("output", ""),
"returncode": gdb_result.get("returncode", 0),
"warnings": errors,
**{key: value for key, value in parsed.items() if key != "output"},
},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
artifacts=artifacts,
metrics=_metrics(parsed),
state=state_info,
next_actions=["可继续基于 last_debug 复用 cfg 组合和 debug_file"],
timing=make_timing(started_at, elapsed_ms),
)
if args.as_json:
output_json(result)
elif result["status"] == "ok":
print(f"[gdb-{args.command}] {result['summary']}")
output = result.get("details", {}).get("output", "")
if output:
print(output)
else:
print(f"[gdb-{args.command}] 失败 — {result['error']['message']}", file=sys.stderr)
sys.exit(1)
except FileNotFoundError:
message = f"openocd 不存在或不在 PATH 中: {exe}"
result = make_result(
status="error",
action=args.command,
summary=message,
details={},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "exe_not_found", "message": message},
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print(f"错误: {message}", file=sys.stderr)
sys.exit(1)
finally:
if args.command != "server":
cleanup(proc)
if __name__ == "__main__":
main()
"""OpenOCD ITM/SWO 观测。
基于 OpenOCD 官方 TPIU/SWO 命令:
- $tpiu_name configure -protocol uart -output :<port> -traceclk <Hz> [-pin-freq <Hz>]
- $tpiu_name enable
- itm port <n> on / itm ports on
"""
from __future__ import annotations
import argparse
import signal
import socket
import subprocess
import sys
import time
from pathlib import Path
ROOT_DIR = Path(__file__).resolve().parents[2]
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
from openocd_runtime import ( # noqa: E402
default_config_path,
emit_stream_record,
get_state_entry,
hidden_subprocess_kwargs,
is_missing,
load_json_file,
load_local_config,
load_project_config,
load_workspace_state,
make_result,
make_timing,
normalize_path,
now_iso,
output_json,
parameter_context,
resolve_param,
save_project_config,
update_state_entry,
workspace_root,
)
def build_openocd_cmd(
exe: str,
board: str = "",
interface: str = "",
target: str = "",
search: str = "",
adapter_speed: str = "",
transport: str = "",
gdb_port: int = 3333,
telnet_port: int = 4444,
trace_port: int = 3443,
tpiu_name: str = "",
traceclk: str = "",
pin_freq: str = "",
itm_ports: list[str] | None = None,
) -> list[str]:
cmd = [exe]
if search:
cmd.extend(["-s", search])
if board:
cmd.extend(["-f", board])
else:
if interface:
cmd.extend(["-f", interface])
if target:
cmd.extend(["-f", target])
if adapter_speed:
cmd.extend(["-c", f"adapter speed {adapter_speed}"])
if transport:
cmd.extend(["-c", f"transport select {transport}"])
cmd.extend(["-c", f"gdb_port {gdb_port}", "-c", f"telnet_port {telnet_port}"])
tpiu_cmd = f"{tpiu_name} configure -protocol uart -output :{trace_port} -traceclk {traceclk}"
if pin_freq:
tpiu_cmd += f" -pin-freq {pin_freq}"
cmd.extend(["-c", tpiu_cmd, "-c", "init", "-c", "reset init", "-c", f"{tpiu_name} enable"])
if itm_ports:
for port in itm_ports:
cmd.extend(["-c", f"itm port {port} on"])
else:
cmd.extend(["-c", "itm ports on"])
return cmd
def start_openocd_server(cmd: list[str]) -> subprocess.Popen:
popen_kwargs = hidden_subprocess_kwargs()
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP if sys.platform == "win32" else 0
if popen_kwargs.get("creationflags"):
creationflags |= popen_kwargs["creationflags"]
return subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
creationflags=creationflags,
startupinfo=popen_kwargs.get("startupinfo"),
)
def wait_server_ready(proc: subprocess.Popen, trace_port: int, timeout: int = 15) -> tuple[bool, list[str]]:
started = time.time()
lines: list[str] = []
ready = False
ready_deadline = 0.0
critical_keywords = [
"error:",
"failed to start adapter's trace",
"not supported by the device",
]
while time.time() - started < timeout:
if proc.poll() is not None:
lines.extend(proc.stderr.read().splitlines())
return False, lines
line = proc.stderr.readline()
if not line:
time.sleep(0.1)
if ready and time.time() >= ready_deadline:
return True, lines
continue
stripped = line.strip()
lines.append(stripped)
lowered = stripped.lower()
if any(keyword in lowered for keyword in critical_keywords):
return False, lines
if f"port {trace_port}" in lowered or "trace data" in lowered:
ready = True
ready_deadline = time.time() + 1.0
return ready, lines
def cleanup(proc: subprocess.Popen | None) -> None:
if proc and proc.poll() is None:
try:
if sys.platform == "win32":
proc.terminate()
else:
proc.send_signal(signal.SIGTERM)
proc.wait(timeout=5)
except (subprocess.TimeoutExpired, OSError):
proc.kill()
def _state_lookup(state: dict) -> dict:
last_debug = get_state_entry(state, "last_debug")
last_flash = get_state_entry(state, "last_flash")
return {
"board": last_debug.get("board") or last_flash.get("board"),
"interface": last_debug.get("interface") or last_flash.get("interface"),
"target": last_debug.get("target") or last_flash.get("target"),
"search": last_debug.get("search"),
"adapter_speed": last_debug.get("adapter_speed") or last_flash.get("adapter_speed"),
"transport": last_debug.get("transport") or last_flash.get("transport"),
}
def resolve_openocd_params(args, project_config: dict, state_lookup: dict) -> dict:
"""解析 OpenOCD 工程级参数,优先级: CLI > 工程配置 > state.json"""
# board: CLI > 工程配置 > state
board = args.board
board_source = "cli"
if is_missing(board):
board = project_config.get("board")
board_source = "project_config"
if is_missing(board):
board = state_lookup.get("board")
board_source = "state"
# interface: CLI > 工程配置 > state
interface = args.interface
interface_source = "cli"
if is_missing(interface):
interface = project_config.get("interface")
interface_source = "project_config"
if is_missing(interface):
interface = state_lookup.get("interface")
interface_source = "state"
# target: CLI > 工程配置 > state
target = args.target
target_source = "cli"
if is_missing(target):
target = project_config.get("target")
target_source = "project_config"
if is_missing(target):
target = state_lookup.get("target")
target_source = "state"
# adapter_speed: CLI > 工程配置 > state
adapter_speed = args.adapter_speed
adapter_speed_source = "cli"
if is_missing(adapter_speed):
adapter_speed = project_config.get("adapter_speed")
adapter_speed_source = "project_config"
if is_missing(adapter_speed):
adapter_speed = state_lookup.get("adapter_speed")
adapter_speed_source = "state"
# transport: CLI > 工程配置 > state
transport = args.transport
transport_source = "cli"
if is_missing(transport):
transport = project_config.get("transport")
transport_source = "project_config"
if is_missing(transport):
transport = state_lookup.get("transport")
transport_source = "state"
# tpiu_name: CLI > 工程配置
tpiu_name = args.tpiu_name
tpiu_name_source = "cli"
if is_missing(tpiu_name):
tpiu_name = project_config.get("tpiu_name")
tpiu_name_source = "project_config"
# traceclk: CLI > 工程配置
traceclk = args.traceclk
traceclk_source = "cli"
if is_missing(traceclk):
traceclk = project_config.get("traceclk")
traceclk_source = "project_config"
# pin_freq: CLI > 工程配置
pin_freq = args.pin_freq
pin_freq_source = "cli"
if is_missing(pin_freq):
pin_freq = project_config.get("pin_freq")
pin_freq_source = "project_config"
return {
"board": board,
"board_source": board_source,
"interface": interface,
"interface_source": interface_source,
"target": target,
"target_source": target_source,
"adapter_speed": adapter_speed,
"adapter_speed_source": adapter_speed_source,
"transport": transport,
"transport_source": transport_source,
"tpiu_name": tpiu_name,
"tpiu_name_source": tpiu_name_source,
"traceclk": traceclk,
"traceclk_source": traceclk_source,
"pin_freq": pin_freq,
"pin_freq_source": pin_freq_source,
}
def main() -> None:
parser = argparse.ArgumentParser(description="OpenOCD ITM 输出捕获")
parser.add_argument("--exe", default=None, help="openocd 路径")
parser.add_argument("--board", default=None, help="board 配置文件")
parser.add_argument("--interface", default=None, help="interface 配置文件")
parser.add_argument("--target", default=None, help="target 配置文件")
parser.add_argument("--search", default=None, help="额外配置脚本搜索目录")
parser.add_argument("--adapter-speed", default=None, help="调试速率 kHz")
parser.add_argument("--transport", default=None, choices=["", "swd", "jtag"], help="传输协议")
parser.add_argument("--gdb-port", type=int, default=None, help="GDB 端口")
parser.add_argument("--telnet-port", type=int, default=None, help="Telnet 端口")
parser.add_argument("--trace-port", type=int, default=3443, help="OpenOCD trace TCP 端口")
parser.add_argument("--tpiu-name", default=None, help="TPIU/SWO 对象名,例如 stm32l1.tpiu")
parser.add_argument("--traceclk", default=None, help="TRACECLKIN 频率 Hz")
parser.add_argument("--pin-freq", default=None, help="SWO pin 频率 Hz")
parser.add_argument("--itm-port", action="append", dest="itm_ports", help="启用的 ITM stimulus port,可多次传入")
parser.add_argument("--workspace", default=None, help="workspace 根目录,默认当前目录")
parser.add_argument("--config", default=None, help="skill config.json 路径")
parser.add_argument("--json", action="store_true", dest="as_json")
args = parser.parse_args()
started_at = now_iso()
started_ts = time.time()
workspace = workspace_root(args.workspace)
config_path = normalize_path(args.config or str(default_config_path(__file__)))
config = load_json_file(config_path)
state = load_workspace_state(str(workspace))
state_lookup = _state_lookup(state)
project_config = load_project_config(str(workspace))
# 解析 OpenOCD 工程级参数
oc_params = resolve_openocd_params(args, project_config, state_lookup)
parameter_sources: dict[str, str] = {}
try:
exe, parameter_sources["exe"] = resolve_param("exe", args.exe, config=config, config_keys=["exe"], required=True)
# 从工程配置或 state 解析 board/interface/target
board = oc_params["board"]
parameter_sources["board"] = oc_params["board_source"]
interface = oc_params["interface"]
parameter_sources["interface"] = oc_params["interface_source"]
target = oc_params["target"]
parameter_sources["target"] = oc_params["target_source"]
adapter_speed = oc_params["adapter_speed"]
parameter_sources["adapter_speed"] = oc_params["adapter_speed_source"]
transport = oc_params["transport"]
parameter_sources["transport"] = oc_params["transport_source"]
search, parameter_sources["search"] = resolve_param("search", args.search, config=config, config_keys=["scripts_dir"], state_record=state_lookup, state_keys=["search"])
# tpiu_name, traceclk, pin_freq 从工程配置解析
tpiu_name = oc_params["tpiu_name"]
parameter_sources["tpiu_name"] = oc_params["tpiu_name_source"]
traceclk = oc_params["traceclk"]
parameter_sources["traceclk"] = oc_params["traceclk_source"]
pin_freq = oc_params["pin_freq"]
parameter_sources["pin_freq"] = oc_params["pin_freq_source"]
except ValueError as exc:
result = make_result(
status="error",
action="itm",
summary=str(exc),
details={},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "missing_param", "message": str(exc)},
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print(f"错误: {exc}", file=sys.stderr)
sys.exit(1)
if not board and not interface and not target:
message = "必须提供 --board 或 --interface + --target"
result = make_result(
status="error",
action="itm",
summary=message,
details={},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "missing_config", "message": message},
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print(f"错误: {message}", file=sys.stderr)
sys.exit(1)
proc = None
trace_sock = None
try:
proc = start_openocd_server(
build_openocd_cmd(
exe=exe,
board=board or "",
interface=interface or "",
target=target or "",
search=search or "",
adapter_speed=str(adapter_speed or ""),
transport=transport or "",
gdb_port=int(args.gdb_port or config.get("gdb_port", 3333)),
telnet_port=int(args.telnet_port or config.get("telnet_port", 4444)),
trace_port=args.trace_port,
tpiu_name=tpiu_name,
traceclk=traceclk,
pin_freq=pin_freq or "",
itm_ports=args.itm_ports,
)
)
ready, lines = wait_server_ready(proc, args.trace_port)
if not ready:
message = "; ".join(line for line in lines if line) or "OpenOCD ITM 初始化失败"
result = make_result(
status="error",
action="itm",
summary=message,
details={"server_log": lines},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "server_failed", "message": message},
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print(f"错误: {message}", file=sys.stderr)
sys.exit(1)
last_trace_error: OSError | None = None
trace_deadline = time.time() + 5.0
while time.time() < trace_deadline:
try:
trace_sock = socket.create_connection(("127.0.0.1", args.trace_port), timeout=1.0)
trace_sock.settimeout(0.5)
last_trace_error = None
break
except OSError as exc:
last_trace_error = exc
if proc.poll() is not None:
break
time.sleep(0.2)
if trace_sock is None:
raise OSError(last_trace_error or "trace socket not ready")
update_state_entry(
"last_observe",
{
"provider": "openocd",
"action": "itm",
"board": board or "",
"interface": interface or "",
"target": target or "",
"search": search or "",
"adapter_speed": adapter_speed or "",
"transport": transport or "",
"channel_type": "itm",
"stream_type": "binary",
"source": "openocd",
},
str(workspace),
)
# 写回确认过的参数到工程配置
save_project_config(str(workspace), {
"board": board or "",
"interface": interface or "",
"target": target or "",
"adapter_speed": adapter_speed or "",
"transport": transport or "",
"tpiu_name": tpiu_name or "",
"traceclk": traceclk or "",
"pin_freq": pin_freq or "",
})
while True:
try:
chunk = trace_sock.recv(4096)
except socket.timeout:
if proc.poll() is not None:
break
continue
if not chunk:
if proc.poll() is not None:
break
continue
text = chunk.decode("utf-8", errors="replace")
emit_stream_record(source="openocd", channel_type="itm", text=text, as_json=args.as_json, stream_type="event")
except FileNotFoundError:
message = f"openocd 不存在或不在 PATH 中: {exe}"
result = make_result(
status="error",
action="itm",
summary=message,
details={},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "exe_not_found", "message": message},
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print(f"错误: {message}", file=sys.stderr)
sys.exit(1)
except OSError as exc:
message = f"ITM trace 连接失败: {exc}"
result = make_result(
status="error",
action="itm",
summary=message,
details={},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "trace_connect_failed", "message": message},
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print(f"错误: {message}", file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
pass
finally:
if trace_sock:
trace_sock.close()
cleanup(proc)
if __name__ == "__main__":
main()
"""OpenOCD 探针探测、固件烧录、Flash 擦除、目标复位与底层查询。"""
from __future__ import annotations
import argparse
import os
import re
import subprocess
import sys
import time
from pathlib import Path
ROOT_DIR = Path(__file__).resolve().parents[2]
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
from openocd_runtime import ( # noqa: E402
build_artifacts,
default_config_path,
get_state_entry,
hidden_subprocess_kwargs,
is_missing,
load_json_file,
load_local_config,
load_project_config,
load_workspace_state,
make_result,
make_timing,
normalize_path,
now_iso,
output_json,
parameter_context,
resolve_param,
save_project_config,
update_state_entry,
workspace_root,
)
ERROR_PATTERNS = [
(r"Error:\s*open failed", "adapter_open_failed", "调试器打开失败,请检查 USB 连接和驱动"),
(r"Error:\s*Failed to open device", "adapter_open_failed", "调试器打开失败,请检查 USB 连接和驱动"),
(r"Error:\s*No.+device found", "no_device", "未找到调试器设备,请检查 USB 连接和驱动"),
(r"Error:\s*unable to find.+cfg", "cfg_not_found", "未找到指定的配置文件,请确认 cfg 路径"),
(r"Error:\s*Transport .+ is not selected", "transport_error", "传输协议未选择,请检查 transport 设置"),
(r"Error:\s*init mode failed", "init_failed", "初始化失败,请检查连线、供电和配置组合"),
(r"Error:\s*Could not verify flash", "verify_failed", "固件校验失败,Flash 可能损坏或写保护"),
(r"Error:\s*flash write failed", "flash_write_failed", "Flash 写入失败,请检查固件文件和目标状态"),
(r"Error:\s*timed out while waiting for target halted", "target_timeout", "等待目标暂停超时,请检查连接"),
(r"Error:\s*Target not halted", "target_not_halted", "目标未暂停,擦除或写入前需要先 halt 目标"),
(r"Error:\s*couldn't bind .+ to socket", "port_busy", "端口被占用,请检查是否有其他 OpenOCD 实例运行"),
(r"Error:\s*Target not examined yet", "target_not_examined", "目标未初始化,请检查 target 配置"),
(r"Error:\s*flash bank", "flash_bank_error", "Flash bank 配置错误,请确认 target 配置匹配芯片"),
(r"Error:\s*device is read protected", "read_protected", "芯片读保护已开启,需要先解锁(本工具不自动解锁)"),
(r"failed erasing sectors", "erase_failed", "Flash 扇区擦除失败,请检查目标是否 halt、读写保护状态及 flash bank 配置"),
(r"mass erase failed", "mass_erase_failed", "整片擦除失败,请检查目标是否 halt、读写保护状态及 target 配置"),
(r"Error:\s*Cannot connect", "cannot_connect", "无法连接目标,请检查连线、供电和接口类型"),
(r"Error:\s*Could not connect", "cannot_connect", "无法连接目标,请检查连线、供电和接口类型"),
]
ALL_ACTIONS = ["probe", "flash", "erase", "reset", "reset-init", "targets", "flash-banks", "adapter-info", "raw"]
def build_openocd_cmd(
exe: str,
board: str = "",
interface: str = "",
target: str = "",
search: str = "",
adapter_speed: str = "",
transport: str = "",
extra_commands: list[str] | None = None,
) -> list[str]:
cmd = [exe]
if search:
cmd.extend(["-s", search])
if board:
cmd.extend(["-f", board])
else:
if interface:
cmd.extend(["-f", interface])
if target:
cmd.extend(["-f", target])
if adapter_speed:
cmd.extend(["-c", f"adapter speed {adapter_speed}"])
if transport:
cmd.extend(["-c", f"transport select {transport}"])
for command in extra_commands or []:
cmd.extend(["-c", command])
return cmd
def infer_mass_erase_command(target: str, board: str) -> str:
cfg = (target or board).lower()
command_map = {
"stm32f0": "stm32f1x mass_erase 0",
"stm32f1": "stm32f1x mass_erase 0",
"stm32f2": "stm32f2x mass_erase 0",
"stm32f3": "stm32f1x mass_erase 0",
"stm32f4": "stm32f4x mass_erase 0",
"stm32f7": "stm32f2x mass_erase 0",
"stm32g0": "stm32l4x mass_erase 0",
"stm32g4": "stm32l4x mass_erase 0",
"stm32h7": "stm32h7x mass_erase 0",
"stm32l0": "stm32l0x mass_erase 0",
"stm32l1": "stm32lx mass_erase 0",
"stm32l4": "stm32l4x mass_erase 0",
"stm32u5": "stm32l4x mass_erase 0",
"gd32f1": "stm32f1x mass_erase 0",
"gd32f4": "stm32f4x mass_erase 0",
}
for key, command in command_map.items():
if key in cfg:
return command
return ""
def build_action_commands(
action: str,
*,
board: str = "",
target: str = "",
file: str = "",
address: str = "",
reset_mode: str = "run",
bank: str = "",
erase_mode: str = "auto",
raw_commands: list[str] | None = None,
) -> tuple[list[str], str | None]:
if action == "probe":
return ["init", "targets", "shutdown"], None
if action == "targets":
return ["init", "targets", "shutdown"], None
if action == "flash-banks":
return ["init", "flash banks", "shutdown"], None
if action == "adapter-info":
return ["adapter name", "transport list", "adapter speed", "shutdown"], None
if action == "flash":
file_abs = os.path.abspath(file).replace("\\", "/")
if file.lower().endswith(".bin"):
return ["init", f"program {{{file_abs}}} verify reset exit {address}"], None
return ["init", f"program {{{file_abs}}} verify reset exit"], None
if action == "erase":
bank_idx = bank if bank else "0"
mass_erase_cmd = infer_mass_erase_command(target, board)
commands = ["init", "reset halt"]
if erase_mode == "mass":
commands.append(mass_erase_cmd)
elif erase_mode == "sector":
commands.append(f"flash erase_sector {bank_idx} 0 last")
else:
commands.append(mass_erase_cmd or f"flash erase_sector {bank_idx} 0 last")
commands.append("shutdown")
return commands, None
if action in ("reset", "reset-init"):
if action == "reset-init" or reset_mode == "init":
return ["init", "reset init", "shutdown"], None
if reset_mode == "halt":
return ["init", "reset halt", "shutdown"], None
return ["init", "reset run", "shutdown"], None
if action == "raw":
return (raw_commands or []) + ["shutdown"], None
return [], "unknown_action"
def parse_output(combined: str, action: str) -> dict:
result = {"raw": combined}
for pattern, code, message in ERROR_PATTERNS:
if re.search(pattern, combined, re.IGNORECASE):
return {"error_code": code, "error_message": message, "raw": combined}
if action in ("probe", "targets"):
target_match = re.search(r"Info\s*:\s*(\S+\.cm\S*)\s", combined)
if target_match:
result["core"] = target_match.group(1)
tap_match = re.search(r"Info\s*:\s*JTAG tap:\s*(\S+)", combined)
if tap_match:
result["jtag_tap"] = tap_match.group(1)
result["targets"] = [line.strip() for line in combined.splitlines() if line.strip() and "tap/device found" not in line.lower()]
elif action == "flash":
speed_match = re.search(r"wrote\s+(\d+)\s+bytes.*?in\s+([\d.]+)s\s+\(([\d.]+)\s+KiB/s\)", combined)
if speed_match:
result["bytes_written"] = int(speed_match.group(1))
result["elapsed_s"] = float(speed_match.group(2))
result["speed_kbps"] = float(speed_match.group(3))
if "verified OK" in combined or "** Verified OK **" in combined:
result["verified"] = True
if "** Programming Finished **" in combined:
result["programmed"] = True
elif action == "erase":
if "erased sectors" in combined.lower():
result["erased"] = True
result["mode"] = "sector"
sector_match = re.search(
r"erased sectors\s+(\d+)\s+through\s+(\d+)\s+on flash bank\s+(\d+)\s+in\s+([\d.]+)s",
combined,
re.IGNORECASE,
)
if sector_match:
result["first_sector"] = int(sector_match.group(1))
result["last_sector"] = int(sector_match.group(2))
result["bank"] = int(sector_match.group(3))
result["elapsed_s"] = float(sector_match.group(4))
elif "mass erase complete" in combined.lower():
result["erased"] = True
result["mode"] = "mass"
elif action == "flash-banks":
result["flash_banks"] = [line.strip() for line in combined.splitlines() if "flash bank" in line.lower()]
elif action == "adapter-info":
name_match = re.search(r"adapter name:\s*(.+)", combined, re.IGNORECASE)
if name_match:
result["adapter_name"] = name_match.group(1).strip()
transport_match = re.search(r"Transport\s+\w+\s+available", combined)
if transport_match:
result["transport_info"] = transport_match.group(0)
return result
def run_openocd(
exe: str,
action: str,
board: str = "",
interface: str = "",
target: str = "",
search: str = "",
adapter_speed: str = "",
transport: str = "",
file: str = "",
address: str = "",
reset_mode: str = "run",
bank: str = "",
erase_mode: str = "auto",
raw_commands: list[str] | None = None,
) -> dict:
if not board and not interface and not target:
return {"status": "error", "action": action, "error": {"code": "missing_config", "message": "必须提供 --board 或 --interface + --target"}}
if action == "flash":
if not file:
return {"status": "error", "action": action, "error": {"code": "missing_file", "message": "flash 必须提供 --file 固件文件路径"}}
if file.lower().endswith(".bin") and not address:
return {"status": "error", "action": action, "error": {"code": "missing_address", "message": ".bin 文件必须提供 --address 烧录地址"}}
action_commands, error_code = build_action_commands(
action,
board=board,
target=target,
file=file,
address=address,
reset_mode=reset_mode,
bank=bank,
erase_mode=erase_mode,
raw_commands=raw_commands,
)
if error_code:
return {"status": "error", "action": action, "error": {"code": error_code, "message": f"未知动作: {action}"}}
if action == "erase" and not any("flash erase_sector" in item or "mass_erase" in item for item in action_commands):
return {"status": "error", "action": action, "error": {"code": "mass_erase_unsupported", "message": "当前 target/board 未配置 mass erase 命令,请改用 --mode sector 或补充映射"}}
started = time.time()
try:
proc = subprocess.run(
build_openocd_cmd(
exe=exe,
board=board,
interface=interface,
target=target,
search=search,
adapter_speed=adapter_speed,
transport=transport,
extra_commands=action_commands,
),
capture_output=True,
text=True,
timeout=120,
encoding="utf-8",
errors="replace",
**hidden_subprocess_kwargs(),
)
except FileNotFoundError:
return {"status": "error", "action": action, "error": {"code": "exe_not_found", "message": f"openocd 不存在或不在 PATH 中: {exe}"}}
except subprocess.TimeoutExpired:
return {"status": "error", "action": action, "error": {"code": "timeout", "message": "OpenOCD 执行超时(120s)"}}
except Exception as exc: # pragma: no cover
return {"status": "error", "action": action, "error": {"code": "exec_error", "message": str(exc)}}
elapsed_ms = int((time.time() - started) * 1000)
combined = proc.stderr + "\n" + proc.stdout
parsed = parse_output(combined, action)
if "error_code" in parsed:
return {
"status": "error",
"action": action,
"error": {"code": parsed["error_code"], "message": parsed["error_message"]},
"details": {"board": board, "interface": interface, "target": target, "elapsed_ms": elapsed_ms, "returncode": proc.returncode},
}
details = {"board": board, "interface": interface, "target": target, "elapsed_ms": elapsed_ms, "returncode": proc.returncode}
details.update({key: value for key, value in parsed.items() if key != "raw"})
summary = f"{action} 成功"
if action == "flash" and parsed.get("speed_kbps"):
summary = f"flash 成功,{parsed['bytes_written']} bytes @ {parsed['speed_kbps']} KiB/s"
elif action == "erase" and parsed.get("mode") == "mass":
summary = "erase 成功,整片擦除完成"
elif action == "erase" and parsed.get("mode") == "sector":
summary = f"erase 成功,sector {parsed.get('first_sector', 0)}-{parsed.get('last_sector', 'last')}"
status = "ok"
if proc.returncode != 0:
if action == "flash" and parsed.get("verified"):
status = "ok"
elif action in ("probe", "targets") and (parsed.get("jtag_tap") or parsed.get("core")):
status = "ok"
else:
status = "error"
error_lines = re.findall(r"Error:\s*(.+)", combined)
return {
"status": "error",
"action": action,
"error": {"code": "command_failed", "message": error_lines[-1].strip() if error_lines else f"执行返回非零退出码: {proc.returncode}"},
"details": details,
}
return {"status": status, "action": action, "summary": summary, "details": details}
def _state_lookup(state: dict) -> dict:
last_build = get_state_entry(state, "last_build")
last_flash = get_state_entry(state, "last_flash")
last_debug = get_state_entry(state, "last_debug")
artifacts = last_build.get("artifacts", {})
return {
"board": last_debug.get("board") or last_flash.get("board"),
"interface": last_debug.get("interface") or last_flash.get("interface"),
"target": last_debug.get("target") or last_flash.get("target"),
"search": last_debug.get("search"),
"adapter_speed": last_debug.get("adapter_speed") or last_flash.get("adapter_speed"),
"transport": last_debug.get("transport") or last_flash.get("transport"),
"flash_file": last_build.get("flash_file") or artifacts.get("flash_file"),
}
def resolve_openocd_params(args, project_config: dict, state_lookup: dict) -> dict:
"""解析 OpenOCD 工程级参数,优先级: CLI > 工程配置 > state.json"""
# board: CLI > 工程配置 > state
board = args.board
board_source = "cli"
if is_missing(board):
board = project_config.get("board")
board_source = "project_config"
if is_missing(board):
board = state_lookup.get("board")
board_source = "state"
# interface: CLI > 工程配置 > state
interface = args.interface
interface_source = "cli"
if is_missing(interface):
interface = project_config.get("interface")
interface_source = "project_config"
if is_missing(interface):
interface = state_lookup.get("interface")
interface_source = "state"
# target: CLI > 工程配置 > state
target = args.target
target_source = "cli"
if is_missing(target):
target = project_config.get("target")
target_source = "project_config"
if is_missing(target):
target = state_lookup.get("target")
target_source = "state"
# adapter_speed: CLI > 工程配置 > state
adapter_speed = args.adapter_speed
adapter_speed_source = "cli"
if is_missing(adapter_speed):
adapter_speed = project_config.get("adapter_speed")
adapter_speed_source = "project_config"
if is_missing(adapter_speed):
adapter_speed = state_lookup.get("adapter_speed")
adapter_speed_source = "state"
# transport: CLI > 工程配置 > state
transport = args.transport
transport_source = "cli"
if is_missing(transport):
transport = project_config.get("transport")
transport_source = "project_config"
if is_missing(transport):
transport = state_lookup.get("transport")
transport_source = "state"
return {
"board": board,
"board_source": board_source,
"interface": interface,
"interface_source": interface_source,
"target": target,
"target_source": target_source,
"adapter_speed": adapter_speed,
"adapter_speed_source": adapter_speed_source,
"transport": transport,
"transport_source": transport_source,
}
def main() -> None:
parser = argparse.ArgumentParser(description="OpenOCD 探针探测/固件烧录/擦除/复位")
parser.add_argument("action", choices=ALL_ACTIONS)
parser.add_argument("--exe", default=None, help="openocd 路径")
parser.add_argument("--board", default=None, help="board 配置文件")
parser.add_argument("--interface", default=None, help="interface 配置文件")
parser.add_argument("--target", default=None, help="target 配置文件")
parser.add_argument("--search", default=None, help="额外配置脚本搜索目录")
parser.add_argument("--adapter-speed", default=None, help="调试速率 kHz")
parser.add_argument("--transport", default=None, choices=["", "swd", "jtag"], help="传输协议")
parser.add_argument("--file", default=None, help="固件文件路径(flash 用)")
parser.add_argument("--address", default=None, help="烧录地址(flash .bin 用)")
parser.add_argument("--mode", default="run", choices=["halt", "run", "init", "auto", "mass", "sector"], help="reset/erase 模式")
parser.add_argument("--bank", default=None, help="Flash bank 编号(erase 用,默认 0)")
parser.add_argument("--command", nargs="+", default=None, help="raw 模式下执行的 OpenOCD 命令列表")
parser.add_argument("--config", default=None, help="skill config.json 路径")
parser.add_argument("--workspace", default=None, help="workspace 根目录,默认当前目录")
parser.add_argument("--json", action="store_true", dest="as_json")
args = parser.parse_args()
started_at = now_iso()
started_ts = time.time()
workspace = workspace_root(args.workspace)
config_path = normalize_path(args.config or str(default_config_path(__file__)))
config = load_json_file(config_path)
state = load_workspace_state(str(workspace))
state_lookup = _state_lookup(state)
project_config = load_project_config(str(workspace))
# 解析 OpenOCD 工程级参数
oc_params = resolve_openocd_params(args, project_config, state_lookup)
parameter_sources: dict[str, str] = {}
try:
exe, parameter_sources["exe"] = resolve_param("exe", args.exe, config=config, config_keys=["exe"], required=True)
# 从工程配置或 state 解析 board/interface/target
board = oc_params["board"]
parameter_sources["board"] = oc_params["board_source"]
interface = oc_params["interface"]
parameter_sources["interface"] = oc_params["interface_source"]
target = oc_params["target"]
parameter_sources["target"] = oc_params["target_source"]
adapter_speed = oc_params["adapter_speed"]
parameter_sources["adapter_speed"] = oc_params["adapter_speed_source"]
transport = oc_params["transport"]
parameter_sources["transport"] = oc_params["transport_source"]
search, parameter_sources["search"] = resolve_param("search", args.search, config=config, config_keys=["scripts_dir"], state_record=state_lookup, state_keys=["search"])
file_path, parameter_sources["file"] = resolve_param("file", args.file, config=config, config_keys=["default_file"], state_record=state_lookup, state_keys=["flash_file"], normalize_as_path=True)
except ValueError as exc:
result = make_result(
status="error",
action=args.action,
summary=str(exc),
details={},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "missing_param", "message": str(exc)},
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print(f"错误: {exc}", file=sys.stderr)
sys.exit(1)
if args.action == "raw" and int(config.get("operation_mode", 1)) >= 3:
message = "operation_mode=3 时禁止直接执行 raw 命令,请先切换模式或显式确认后再执行"
result = make_result(
status="error",
action="raw",
summary=message,
details={},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "confirmation_required", "message": message},
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print(f"错误: {message}", file=sys.stderr)
sys.exit(1)
if args.action == "flash" and file_path and not os.path.isfile(file_path):
message = f"固件文件不存在: {file_path}"
result = make_result(
status="error",
action="flash",
summary=message,
details={},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "file_not_found", "message": message},
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print(f"错误: {message}", file=sys.stderr)
sys.exit(1)
raw_result = run_openocd(
exe=exe,
action=args.action,
board=board or "",
interface=interface or "",
target=target or "",
search=search or "",
adapter_speed=str(adapter_speed or ""),
transport=transport or "",
file=file_path or "",
address=args.address or "",
reset_mode=args.mode if args.action == "reset" else "run",
bank=args.bank or "",
erase_mode=args.mode if args.action == "erase" else "auto",
raw_commands=args.command,
)
elapsed_ms = (time.time() - started_ts) * 1000
if raw_result["status"] == "error":
result = make_result(
status="error",
action=args.action,
summary=raw_result["error"]["message"],
details=raw_result.get("details", {}),
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error=raw_result["error"],
timing=make_timing(started_at, elapsed_ms),
)
else:
details = raw_result.get("details", {})
artifacts = build_artifacts(flash_file=file_path, input_file=file_path)
metrics = {}
if "bytes_written" in details:
metrics["bytes_written"] = details["bytes_written"]
if "speed_kbps" in details:
metrics["speed_kbps"] = details["speed_kbps"]
state_info = None
if args.action == "flash":
state_info = update_state_entry(
"last_flash",
{
"provider": "openocd",
"action": "flash",
"board": board or "",
"interface": interface or "",
"target": target or "",
"search": search or "",
"adapter_speed": adapter_speed or "",
"transport": transport or "",
"flash_file": file_path or "",
"artifacts": artifacts,
},
str(workspace),
)
# 写回确认过的参数到工程配置
save_project_config(str(workspace), {
"board": board or "",
"interface": interface or "",
"target": target or "",
"adapter_speed": adapter_speed or "",
"transport": transport or "",
})
result = make_result(
status="ok",
action=args.action,
summary=raw_result["summary"],
details=details,
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
artifacts=artifacts,
metrics=metrics,
state=state_info,
next_actions=["可继续复用 last_flash/last_debug 串联后续流程"] if args.action == "flash" else None,
timing=make_timing(started_at, elapsed_ms),
)
if args.as_json:
output_json(result)
return
if result["status"] == "ok":
print(f"[{args.action}] {result['summary']}")
else:
print(f"[{args.action}] 失败 — {result['error']['message']}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
"""openocd skill 私有运行时工具。"""
from __future__ import annotations
import json
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from shutil import which
from typing import Any
STATE_DIR_NAME = ".embeddedskills"
STATE_FILE_NAME = "state.json"
PROJECT_CONFIG_FILE_NAME = "config.json"
SKILL_NAME = "openocd"
def now_iso() -> str:
return datetime.now().astimezone().isoformat(timespec="seconds")
def default_config_path(script_file: str) -> Path:
return Path(script_file).resolve().parents[1] / "config.json"
def load_local_config(script_file: str = "") -> dict:
"""加载 skill/config.json(环境级配置)"""
if script_file:
config_path = default_config_path(script_file)
else:
# 尝试从调用栈推断路径
import inspect
frame = inspect.currentframe()
if frame and frame.f_back:
caller_file = frame.f_back.f_globals.get("__file__", "")
if caller_file:
config_path = default_config_path(caller_file)
else:
config_path = Path(__file__).resolve().parents[1] / "config.json"
else:
config_path = Path(__file__).resolve().parents[1] / "config.json"
return load_json_file(config_path)
def save_local_config(data: dict, script_file: str = "") -> None:
"""保存环境级配置到 skill/config.json"""
if script_file:
config_path = default_config_path(script_file)
else:
config_path = Path(__file__).resolve().parents[1] / "config.json"
save_json_file(config_path, data)
def load_project_config(workspace: str | None = None) -> dict:
"""从 workspace/.embeddedskills/config.json 读取本 skill 的工程级配置
参数: workspace - 工作区路径,None 时使用 cwd
返回: 该 skill 对应的配置字典
"""
ws_root = workspace_root(workspace)
project_config_path = ws_root / STATE_DIR_NAME / PROJECT_CONFIG_FILE_NAME
full_config = load_json_file(project_config_path)
return full_config.get(SKILL_NAME, {})
def save_project_config(workspace: str | None = None, values: dict | None = None) -> None:
"""写回工程级配置到 workspace/.embeddedskills/config.json
- 只更新本 skill 的配置部分,不覆盖其他 skill 的配置
- 目录不存在时自动创建 .embeddedskills/
- openocd_runtime 中 skill_name 硬编码为 "openocd"
"""
if values is None:
values = {}
ws_root = workspace_root(workspace)
project_config_path = ws_root / STATE_DIR_NAME / PROJECT_CONFIG_FILE_NAME
# 读取现有配置(如果存在)
full_config = load_json_file(project_config_path)
if not isinstance(full_config, dict):
full_config = {}
# 只更新本 skill 的配置部分
full_config[SKILL_NAME] = {**(full_config.get(SKILL_NAME) or {}), **values}
save_json_file(project_config_path, full_config)
def output_json(data: dict, *, indent: int = 2) -> None:
sys.stdout.reconfigure(encoding="utf-8")
print(json.dumps(data, ensure_ascii=False, indent=indent), flush=True)
def is_missing(value: Any) -> bool:
return value is None or value == ""
def normalize_path(value: str | None) -> str:
if is_missing(value):
return ""
return str(Path(str(value)).expanduser().resolve())
def load_json_file(path: str | Path) -> dict:
file_path = Path(path)
if not file_path.exists():
return {}
try:
return json.loads(file_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, OSError):
return {}
def save_json_file(path: str | Path, data: dict) -> None:
file_path = Path(path)
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
def hidden_subprocess_kwargs() -> dict:
if sys.platform != "win32":
return {}
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = getattr(subprocess, "SW_HIDE", 0)
return {
"creationflags": getattr(subprocess, "CREATE_NO_WINDOW", 0),
"startupinfo": startupinfo,
}
def workspace_root(workspace: str | None = None) -> Path:
if not is_missing(workspace):
return Path(str(workspace)).expanduser().resolve()
return Path.cwd().resolve()
def load_workspace_state(workspace: str | None = None) -> dict:
return load_json_file(workspace_root(workspace) / STATE_DIR_NAME / STATE_FILE_NAME)
def save_workspace_state(state: dict, workspace: str | None = None) -> Path:
file_path = workspace_root(workspace) / STATE_DIR_NAME / STATE_FILE_NAME
save_json_file(file_path, state)
return file_path
def get_state_entry(state: dict | None, key: str) -> dict:
if not isinstance(state, dict):
return {}
value = state.get(key, {})
return value if isinstance(value, dict) else {}
def update_state_entry(category: str, record: dict, workspace: str | None = None) -> dict:
state = load_workspace_state(workspace)
state[category] = {**record, "timestamp": record.get("timestamp") or now_iso()}
file_path = save_workspace_state(state, workspace)
return {
"workspace": str(workspace_root(workspace)),
"file": str(file_path),
"updated_keys": [category],
category: state[category],
}
def _first_resolved(mapping: dict, keys: list[str]) -> tuple[Any, str | None]:
for key in keys:
value = mapping.get(key)
if not is_missing(value):
return value, key
return None, None
def resolve_param(
name: str,
cli_value: Any,
*,
config: dict | None = None,
config_keys: list[str] | None = None,
state_record: dict | None = None,
state_keys: list[str] | None = None,
required: bool = False,
normalize_as_path: bool = False,
) -> tuple[Any, str]:
if not is_missing(cli_value):
value = cli_value
source = "cli"
else:
value = None
source = ""
if config and config_keys:
value, config_key = _first_resolved(config, config_keys)
if not is_missing(value):
source = f"config:{config_key}"
if is_missing(value) and state_record and state_keys:
value, state_key = _first_resolved(state_record, state_keys)
if not is_missing(value):
source = f"state:{state_key}"
if is_missing(value) and name == "exe":
discovered = which("openocd") or which("openocd.exe")
if discovered:
value = discovered
source = "path"
else:
value = "openocd"
source = "default"
if normalize_as_path and not is_missing(value):
value = normalize_path(str(value))
if required and is_missing(value):
raise ValueError(f"缺少必要参数: {name}")
return value, source
def compact_dict(data: dict | None) -> dict:
if not isinstance(data, dict):
return {}
return {key: value for key, value in data.items() if value not in (None, "", [], {})}
def build_artifacts(**paths: str) -> dict:
return {key: normalize_path(str(value)) for key, value in paths.items() if not is_missing(value)}
def make_result(
*,
status: str,
action: str,
summary: str,
details: dict | None = None,
context: dict | None = None,
artifacts: dict | None = None,
metrics: dict | None = None,
state: dict | None = None,
next_actions: list[str] | None = None,
timing: dict | None = None,
error: dict | None = None,
) -> dict:
result = {"status": status, "action": action, "summary": summary, "details": compact_dict(details)}
optional = {
"context": compact_dict(context),
"artifacts": compact_dict(artifacts),
"metrics": compact_dict(metrics),
"state": compact_dict(state),
"timing": compact_dict(timing),
}
for key, value in optional.items():
if value:
result[key] = value
if next_actions:
result["next_actions"] = [item for item in next_actions if item]
if error:
result["error"] = compact_dict(error)
return result
def make_timing(started_at: str, elapsed_ms: int | float) -> dict:
return {"started_at": started_at, "finished_at": now_iso(), "elapsed_ms": int(elapsed_ms)}
def parameter_context(*, provider: str, workspace: str | None = None, parameter_sources: dict | None = None, config_path: str | None = None) -> dict:
context = {"provider": provider, "workspace": str(workspace_root(workspace))}
if parameter_sources:
context["parameter_sources"] = compact_dict(parameter_sources)
if not is_missing(config_path):
context["config_path"] = normalize_path(str(config_path))
return context
def emit_stream_record(*, source: str, channel_type: str, text: str, as_json: bool, stream_type: str = "text", channel: int | None = None, extra: dict | None = None) -> None:
if as_json:
record = {
"timestamp": now_iso(),
"source": source,
"channel_type": channel_type,
"stream_type": stream_type,
"text": text.rstrip("\r\n"),
}
if channel is not None:
record["channel"] = channel
if extra:
record.update(compact_dict(extra))
print(json.dumps(record, ensure_ascii=False), flush=True)
else:
print(text, end="" if text.endswith(("\n", "\r")) else "\n", flush=True)
"""OpenOCD Semihosting 输出捕获。"""
from __future__ import annotations
import argparse
import queue
import re
import signal
import socket
import subprocess
import sys
import threading
import time
from pathlib import Path
ROOT_DIR = Path(__file__).resolve().parents[2]
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
from openocd_runtime import ( # noqa: E402
emit_stream_record,
default_config_path,
get_state_entry,
hidden_subprocess_kwargs,
is_missing,
load_json_file,
load_local_config,
load_project_config,
load_workspace_state,
make_result,
make_timing,
normalize_path,
now_iso,
output_json,
parameter_context,
resolve_param,
save_project_config,
update_state_entry,
workspace_root,
)
LOG_PREFIXES = re.compile(r"^(Info|Warn|Error|Debug)\s*:", re.IGNORECASE)
STATUS_PATTERNS = [
"Listening on port",
"halted due to",
"target state:",
"shutdown command invoked",
"GDB",
"accepting",
"dropped",
]
def build_openocd_cmd(
exe: str,
board: str = "",
interface: str = "",
target: str = "",
search: str = "",
adapter_speed: str = "",
transport: str = "",
gdb_port: int = 3333,
telnet_port: int = 4444,
) -> list[str]:
cmd = [exe]
if search:
cmd.extend(["-s", search])
if board:
cmd.extend(["-f", board])
else:
if interface:
cmd.extend(["-f", interface])
if target:
cmd.extend(["-f", target])
if adapter_speed:
cmd.extend(["-c", f"adapter speed {adapter_speed}"])
if transport:
cmd.extend(["-c", f"transport select {transport}"])
cmd.extend(["-c", f"gdb_port {gdb_port}"])
cmd.extend(["-c", f"telnet_port {telnet_port}"])
return cmd
def start_openocd_server(cmd: list[str]) -> subprocess.Popen:
popen_kwargs = hidden_subprocess_kwargs()
creationflags = subprocess.CREATE_NEW_PROCESS_GROUP if sys.platform == "win32" else 0
if popen_kwargs.get("creationflags"):
creationflags |= popen_kwargs["creationflags"]
return subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
creationflags=creationflags,
startupinfo=popen_kwargs.get("startupinfo"),
)
def wait_server_ready(proc: subprocess.Popen, telnet_port: int, timeout: int = 15) -> tuple[bool, list[str]]:
started = time.time()
errors: list[str] = []
ready = False
while time.time() - started < timeout:
if proc.poll() is not None:
remaining = proc.stderr.read()
errors.extend([line.strip() for line in remaining.splitlines() if "Error:" in line])
return False, errors
line = proc.stderr.readline()
if not line:
time.sleep(0.1)
continue
stripped = line.strip()
if "Error:" in stripped:
errors.append(stripped)
if f"Listening on port {telnet_port}" in stripped or "listening on" in stripped.lower():
ready = True
break
if not ready:
return False, errors
critical = ["open failed", "init mode failed", "no device found", "cannot connect", "error connecting dp", "examination failed"]
critical_errors = [item for item in errors if any(keyword in item.lower() for keyword in critical)]
if critical_errors:
return False, critical_errors
return True, errors
def cleanup_proc(proc: subprocess.Popen | None) -> None:
if proc and proc.poll() is None:
try:
if sys.platform == "win32":
proc.terminate()
else:
proc.send_signal(signal.SIGTERM)
proc.wait(timeout=5)
except (subprocess.TimeoutExpired, OSError):
proc.kill()
def _read_until_prompt(sock: socket.socket) -> str:
buf = b""
while True:
decoded = buf.decode("utf-8", errors="replace")
if decoded.endswith("> ") or "\n> " in decoded or "\r> " in decoded:
prompt_pos = decoded.rfind("\n> ")
if prompt_pos == -1:
prompt_pos = decoded.rfind("\r> ")
if prompt_pos == -1 and decoded.endswith("> "):
prompt_pos = len(decoded) - 2
return decoded[:prompt_pos].strip() if prompt_pos >= 0 else decoded.strip()
try:
chunk = sock.recv(4096)
if not chunk:
return buf.decode("utf-8", errors="replace").strip()
buf += chunk
except socket.timeout:
return buf.decode("utf-8", errors="replace").strip()
def telnet_send_multi(host: str, port: int, commands: list[str], timeout: float = 5.0) -> list[str]:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
responses: list[str] = []
try:
sock.connect((host, port))
_read_until_prompt(sock)
for command in commands:
sock.sendall((command + "\n").encode("utf-8"))
responses.append(_read_until_prompt(sock))
return responses
finally:
sock.close()
def is_semihosting_line(line: str) -> bool:
stripped = line.strip()
if not stripped:
return False
if LOG_PREFIXES.match(stripped):
return False
return not any(item in stripped for item in STATUS_PATTERNS)
def _state_lookup(state: dict) -> dict:
last_debug = get_state_entry(state, "last_debug")
last_flash = get_state_entry(state, "last_flash")
return {
"board": last_debug.get("board") or last_flash.get("board"),
"interface": last_debug.get("interface") or last_flash.get("interface"),
"target": last_debug.get("target") or last_flash.get("target"),
"search": last_debug.get("search"),
"adapter_speed": last_debug.get("adapter_speed") or last_flash.get("adapter_speed"),
"transport": last_debug.get("transport") or last_flash.get("transport"),
"gdb_port": last_debug.get("gdb_port"),
"telnet_port": last_debug.get("telnet_port"),
}
def resolve_openocd_params(args, project_config: dict, state_lookup: dict) -> dict:
"""解析 OpenOCD 工程级参数,优先级: CLI > 工程配置 > state.json"""
# board: CLI > 工程配置 > state
board = args.board
board_source = "cli"
if is_missing(board):
board = project_config.get("board")
board_source = "project_config"
if is_missing(board):
board = state_lookup.get("board")
board_source = "state"
# interface: CLI > 工程配置 > state
interface = args.interface
interface_source = "cli"
if is_missing(interface):
interface = project_config.get("interface")
interface_source = "project_config"
if is_missing(interface):
interface = state_lookup.get("interface")
interface_source = "state"
# target: CLI > 工程配置 > state
target = args.target
target_source = "cli"
if is_missing(target):
target = project_config.get("target")
target_source = "project_config"
if is_missing(target):
target = state_lookup.get("target")
target_source = "state"
# adapter_speed: CLI > 工程配置 > state
adapter_speed = args.adapter_speed
adapter_speed_source = "cli"
if is_missing(adapter_speed):
adapter_speed = project_config.get("adapter_speed")
adapter_speed_source = "project_config"
if is_missing(adapter_speed):
adapter_speed = state_lookup.get("adapter_speed")
adapter_speed_source = "state"
# transport: CLI > 工程配置 > state
transport = args.transport
transport_source = "cli"
if is_missing(transport):
transport = project_config.get("transport")
transport_source = "project_config"
if is_missing(transport):
transport = state_lookup.get("transport")
transport_source = "state"
return {
"board": board,
"board_source": board_source,
"interface": interface,
"interface_source": interface_source,
"target": target,
"target_source": target_source,
"adapter_speed": adapter_speed,
"adapter_speed_source": adapter_speed_source,
"transport": transport,
"transport_source": transport_source,
}
def main() -> None:
parser = argparse.ArgumentParser(description="OpenOCD Semihosting 输出捕获")
parser.add_argument("--exe", default=None, help="openocd 路径")
parser.add_argument("--board", default=None, help="board 配置文件")
parser.add_argument("--interface", default=None, help="interface 配置文件")
parser.add_argument("--target", default=None, help="target 配置文件")
parser.add_argument("--search", default=None, help="额外配置脚本搜索目录")
parser.add_argument("--adapter-speed", default=None, help="调试速率 kHz")
parser.add_argument("--transport", default=None, choices=["", "swd", "jtag"], help="传输协议")
parser.add_argument("--gdb-port", type=int, default=None, help="GDB 端口")
parser.add_argument("--telnet-port", type=int, default=None, help="Telnet 端口")
parser.add_argument("--timeout", type=int, default=0, help="捕获时长秒数,0=持续到 Ctrl+C")
parser.add_argument("--config", default=None, help="skill config.json 路径")
parser.add_argument("--workspace", default=None, help="workspace 根目录,默认当前目录")
parser.add_argument("--json", action="store_true", dest="as_json")
args = parser.parse_args()
started_at = now_iso()
started_ts = time.time()
workspace = workspace_root(args.workspace)
config_path = normalize_path(args.config or str(default_config_path(__file__)))
config = load_json_file(config_path)
state = load_workspace_state(str(workspace))
state_lookup = _state_lookup(state)
project_config = load_project_config(str(workspace))
# 解析 OpenOCD 工程级参数
oc_params = resolve_openocd_params(args, project_config, state_lookup)
parameter_sources: dict[str, str] = {}
try:
exe, parameter_sources["exe"] = resolve_param("exe", args.exe, config=config, config_keys=["exe"], required=True)
# 从工程配置或 state 解析 board/interface/target
board = oc_params["board"]
parameter_sources["board"] = oc_params["board_source"]
interface = oc_params["interface"]
parameter_sources["interface"] = oc_params["interface_source"]
target = oc_params["target"]
parameter_sources["target"] = oc_params["target_source"]
adapter_speed = oc_params["adapter_speed"]
parameter_sources["adapter_speed"] = oc_params["adapter_speed_source"]
transport = oc_params["transport"]
parameter_sources["transport"] = oc_params["transport_source"]
search, parameter_sources["search"] = resolve_param(
"search",
args.search,
config=config,
config_keys=["scripts_dir"],
state_record=state_lookup,
state_keys=["search"],
)
gdb_port, parameter_sources["gdb_port"] = resolve_param(
"gdb_port",
args.gdb_port,
config=config,
config_keys=["gdb_port"],
state_record=state_lookup,
state_keys=["gdb_port"],
)
telnet_port, parameter_sources["telnet_port"] = resolve_param(
"telnet_port",
args.telnet_port,
config=config,
config_keys=["telnet_port"],
state_record=state_lookup,
state_keys=["telnet_port"],
)
except ValueError as exc:
result = make_result(
status="error",
action="semihosting",
summary=str(exc),
details={},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "missing_param", "message": str(exc)},
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print(f"错误: {exc}", file=sys.stderr)
sys.exit(1)
if not board and not interface and not target:
message = "必须提供 --board 或 --interface + --target"
result = make_result(
status="error",
action="semihosting",
summary=message,
details={},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "missing_config", "message": message},
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print(f"错误: {message}", file=sys.stderr)
sys.exit(1)
cmd = build_openocd_cmd(
exe=exe,
board=board or "",
interface=interface or "",
target=target or "",
search=search or "",
adapter_speed=str(adapter_speed or ""),
transport=transport or "",
gdb_port=int(gdb_port or 3333),
telnet_port=int(telnet_port or 4444),
)
proc: subprocess.Popen | None = None
try:
proc = start_openocd_server(cmd)
ready, errors = wait_server_ready(proc, int(telnet_port or 4444))
if not ready:
message = "; ".join(errors) if errors else "OpenOCD 启动失败或超时"
result = make_result(
status="error",
action="semihosting",
summary="Semihosting 服务启动失败",
details={"errors": errors},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "server_failed", "message": message},
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print(f"错误: {message}", file=sys.stderr)
sys.exit(1)
try:
telnet_send_multi("localhost", int(telnet_port or 4444), ["halt", "arm semihosting enable", "resume"])
except (ConnectionError, OSError) as exc:
result = make_result(
status="error",
action="semihosting",
summary="Telnet 连接失败",
details={},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "telnet_failed", "message": f"Telnet 连接失败: {exc}"},
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print(f"错误: {result['error']['message']}", file=sys.stderr)
sys.exit(1)
update_state_entry(
"last_observe",
{
"provider": "openocd",
"action": "semihosting",
"board": board or "",
"interface": interface or "",
"target": target or "",
"search": search or "",
"adapter_speed": adapter_speed or "",
"transport": transport or "",
"channel_type": "semihosting",
"stream_type": "text",
"source": "openocd",
},
str(workspace),
)
# 写回确认过的参数到工程配置
save_project_config(str(workspace), {
"board": board or "",
"interface": interface or "",
"target": target or "",
"adapter_speed": adapter_speed or "",
"transport": transport or "",
})
if not args.as_json:
print("Semihosting 已启用,等待输出(Ctrl+C 退出):", file=sys.stderr, flush=True)
print("-" * 40, file=sys.stderr, flush=True)
line_queue: queue.SimpleQueue[str | None] = queue.SimpleQueue()
def _stderr_reader() -> None:
try:
while True:
line = proc.stderr.readline()
if not line:
break
line_queue.put(line)
finally:
line_queue.put(None)
threading.Thread(target=_stderr_reader, daemon=True).start()
monitor_started = time.time()
stream_closed = False
while True:
if args.timeout > 0 and (time.time() - monitor_started) >= args.timeout:
break
if proc.poll() is not None:
break
try:
line = line_queue.get_nowait()
except queue.Empty:
time.sleep(0.02)
continue
if line is None:
stream_closed = True
time.sleep(0.02)
continue
if is_semihosting_line(line):
emit_stream_record(
source="openocd",
channel_type="semihosting",
text=line,
as_json=args.as_json,
stream_type="text",
)
while True:
try:
line = line_queue.get_nowait()
except queue.Empty:
break
if line is None:
stream_closed = True
break
if is_semihosting_line(line):
emit_stream_record(
source="openocd",
channel_type="semihosting",
text=line,
as_json=args.as_json,
stream_type="text",
)
except FileNotFoundError:
message = f"openocd 不存在或不在 PATH 中: {exe}"
result = make_result(
status="error",
action="semihosting",
summary=message,
details={},
context=parameter_context(provider="openocd", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "exe_not_found", "message": message},
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print(f"错误: {message}", file=sys.stderr)
sys.exit(1)
except KeyboardInterrupt:
if not args.as_json:
print("\n已停止 semihosting 捕获", file=sys.stderr, flush=True)
finally:
if proc and proc.poll() is None:
try:
telnet_send_multi("localhost", int(telnet_port or 4444), ["halt", "arm semihosting disable", "shutdown"], timeout=2.0)
except (ConnectionError, OSError):
pass
cleanup_proc(proc)
if __name__ == "__main__":
main()
# OpenOCD 烧录模板
# 用法: openocd -f <interface.cfg> -f <target.cfg> -f flash.cfg
# 使用前替换 {{FILE}} 和 {{ADDRESS}}(仅 .bin 需要地址)
init
program {{FILE}} verify reset exit
# OpenOCD GDB Server 模板
# 用法: openocd -f <interface.cfg> -f <target.cfg> -f gdb_server.cfg
gdb_port {{GDB_PORT}}
telnet_port {{TELNET_PORT}}
Related skills
How it compares
Hardware debug orchestration skill—not a cloud deploy or mobile navigation pattern pack.
FAQ
Who is openocd for?
Developers and small teams doing ARM firmware who already use OpenOCD and want Claude Code or similar agents to run flash, GDB server, and Telnet debug steps from SKILL.md.
When should I use openocd?
During Build integrations while bringing up boards, after each firmware build that needs programming, or when you need GDB backtraces, Semihosting printf, or ITM/SWO traces without leaving the agent session.
Is openocd safe to install?
It can erase flash and reset hardware; review the Security Audits panel on this page, set operation_mode to confirm-before-run in config.json, and never point it at production equipment you cannot recover.