
Jlink
- 420 installs
- 534 repo stars
- Updated June 29, 2026
- zhinkgit/embeddedskills
jlink is an agent skill for solo embedded developers who debug and ship firmware through SEGGER J-Link on Windows-oriented paths (JLink.exe, JLinkGDBServerCL, RTT client, optional arm-none-eabi-gdb).
About
jlink is an agent skill for solo embedded developers who debug and ship firmware through SEGGER J-Link on Windows-oriented paths (JLink.exe, JLinkGDBServerCL, RTT client, optional arm-none-eabi-gdb). It documents configuration via skill/config.json cloned from config.example.json, including serial number, RTT telnet port, SWO commands, and operation mode. Agents can use it to verify probe attachment, program images, inspect memory and registers, stream RTT logs, and run GDB-backed source debugging—work that spans bring-up during build and diagnosis during ship. Python standard library wraps CLI invocation without extra pip deps. Use when your board support package is ready but flashing and live debug still require repeatable probe commands instead of ad-hoc GUI clicking.
- Firmware programming for .hex, .bin, and .elf images
- Probe connectivity checks, memory read/write, registers, and target reset
- RTT log streaming and SWO event wrapping via external viewer
- Online debug: halt, resume, single-step, breakpoints
- GDB source-level debug with stack and locals via arm-none-eabi-gdb
Jlink by the numbers
- 420 all-time installs (skills.sh)
- +23 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #104 of 596 Debugging 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 jlinkAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 420 |
|---|---|
| repo stars | ★ 534 |
| Security audit | 3 / 3 scanners passed |
| Last updated | June 29, 2026 |
| Repository | zhinkgit/embeddedskills ↗ |
How do I flash firmware and debug ARM embedded targets over SEGGER J-Link with RTT, SWO, and GDB from your agent.?
Flash firmware and debug ARM embedded targets over SEGGER J-Link with RTT, SWO, and GDB from your agent.
Who is it for?
Best when you're working on debugging and need structured help with jlink.
Skip if: Teams with no debugging needs, or anyone wanting a generic chat assistant without this specific workflow.
When should I use this skill?
When you need to flash firmware and debug ARM embedded targets over SEGGER J-Link with RTT, SWO, and GDB from your agent., or when jlink is an agent skill for solo embedded developers who debug and ship firmware through
What you get
Structured output aligned to jlink: Firmware programming for .hex, .bin, and .elf images, Probe connectivity checks, memory read/write, registers, and target reset.
Files
J-Link 下载与在线调试
本 skill 提供 J-Link 探针的设备探测、固件烧录、内存读写、寄存器查看、目标复位、RTT 日志读取,以及轻量在线调试和 GDB 源码级调试能力。
配置
环境级配置(skill/config.json)
skill 目录下的 config.json 包含环境级配置(工具路径、端口号等),首次使用前确认 exe 路径正确:
{
"exe": "C:\\Program Files\\SEGGER\\JLink\\JLink.exe",
"gdbserver_exe": "C:\\Program Files\\SEGGER\\JLink\\JLinkGDBServerCL.exe",
"rtt_exe": "C:\\Program Files\\SEGGER\\JLink\\JLinkRTTClient.exe",
"gdb_exe": "C:\\Program Files\\Arm\\GNU Toolchain mingw-w64-x86_64-arm-none-eabi\\bin\\arm-none-eabi-gdb.exe",
"serial_no": "",
"rtt_telnet_port": 0,
"swo_command": [],
"operation_mode": 1
}exe:JLink.exe 完整路径(必填)gdbserver_exe:JLinkGDBServerCL.exe 路径,RTT 和 GDB 调试需要rtt_exe:JLinkRTTClient.exe 路径gdb_exe:arm-none-eabi-gdb 路径,GDB 源码级调试需要serial_no:默认探针序列号,多探针场景下使用rtt_telnet_port:RTT 端口,0 表示使用工具默认值swo_command:可选,完整 SWO viewer 命令数组,供jlink_swo.py包装operation_mode:1直接执行 /2输出风险摘要但不阻塞 /3执行前确认
工程级配置(.embeddedskills/config.json)
设备参数(device/interface/speed)统一在工作区的 .embeddedskills/config.json 中管理:
{
"jlink": {
"device": "STM32F407VG",
"interface": "SWD",
"speed": "4000"
}
}device:芯片型号(如 STM32F407VG、GD32F470ZG)interface:调试接口,SWD 或 JTAG,默认 SWDspeed:调试速率 kHz,默认 4000
参数解析优先级:CLI 显式参数 > `.embeddedskills/config.json`(工程级)> `skill/config.json`(环境级)> `.embeddedskills/state.json` > 默认值/报错
成功执行后,确认过的 device/interface/speed 会自动写回工程配置。
子命令
基础操作
| 子命令 | 用途 | 风险 |
|---|---|---|
info | 探测探针与目标连通性 | 低 |
flash | 烧录固件(.hex / .bin / .elf) | 高 |
read-mem | 读取内存区域 | 低 |
write-mem | 写入内存 | 高 |
regs | 查看 CPU 寄存器 | 低 |
reset | 复位目标芯片 | 高 |
rtt | 读取 RTT 日志输出 | 低 |
swo | 包装外部 SWO viewer 为统一事件流 | 低 |
在线调试(JLink Commander)
| 子命令 | 用途 | 风险 |
|---|---|---|
halt | 暂停 CPU,返回寄存器状态 | 低 |
go | 恢复 CPU 运行 | 低 |
step | 单步执行(支持指定步数),返回执行的指令和寄存器 | 低 |
run-to | 设置断点并运行,等待命中后返回状态 | 低 |
GDB 源码级调试
| 子命令 | 用途 | 依赖 |
|---|---|---|
gdb backtrace/locals | 查看调用栈和局部变量 | arm-none-eabi-gdb |
gdb break/continue/next/step/finish/until | one-shot 控制执行流 | arm-none-eabi-gdb |
gdb frame/print/watch/disassemble/threads/crash-report | one-shot 源码级诊断 | arm-none-eabi-gdb |
执行流程
1. 读取 skill/config.json,确认 exe 路径有效 2. 读取 .embeddedskills/config.json 获取工程级配置(device/interface/speed) 3. 读取 .embeddedskills/state.json 获取历史状态 4. 参数解析优先级:CLI 显式参数 > `.embeddedskills/config.json`(工程级)> `skill/config.json`(环境级)> `.embeddedskills/state.json` > 默认值/报错 5. 若当前动作需要 device 且仍为空,直接要求用户补充,绝不猜测 6. 多探针场景未指定 serial_no 时,列出探针让用户选择,不自动选择 7. 按 operation_mode 决定是否需要确认后执行 8. 使用模板生成临时 .jlink 命令文件,调用 JLink.exe 时带 -NoGui 1 -ExitOnError 1 -AutoConnect 1 9. 解析输出和返回码,返回结构化结果 10. 成功执行后,将确认过的 device/interface/speed 写回 .embeddedskills/config.json
脚本调用
skill 目录下有四个 Python 脚本,使用标准库实现,无额外依赖。
jlink_exec.py — 基础操作 + 轻量调试
# 探测连通性
python <skill-dir>/scripts/jlink_exec.py info --device GD32F470ZG --json
# 烧录固件
python <skill-dir>/scripts/jlink_exec.py flash --file build/app.hex --device GD32F470ZG --json
# 烧录 .bin(必须提供地址)
python <skill-dir>/scripts/jlink_exec.py flash --file build/app.bin --device GD32F470ZG --address 0x08000000 --json
# 读取内存
python <skill-dir>/scripts/jlink_exec.py read-mem --address 0x08000000 --length 256 --device GD32F470ZG --json
# 写入内存
python <skill-dir>/scripts/jlink_exec.py write-mem --address 0x20000000 --value 0x12345678 --device GD32F470ZG --json
# 查看寄存器
python <skill-dir>/scripts/jlink_exec.py regs --device GD32F470ZG --json
# 复位目标
python <skill-dir>/scripts/jlink_exec.py reset --device GD32F470ZG --json
# 暂停 CPU
python <skill-dir>/scripts/jlink_exec.py halt --device GD32F470ZG --json
# 恢复运行
python <skill-dir>/scripts/jlink_exec.py go --device GD32F470ZG --json
# 单步执行(3 步)
python <skill-dir>/scripts/jlink_exec.py step --device GD32F470ZG --count 3 --json
# 运行到断点地址
python <skill-dir>/scripts/jlink_exec.py run-to --device GD32F470ZG --address 0x08001234 --timeout-ms 3000 --json通用可选参数:--interface SWD|JTAG、--speed 4000、--serial-no <序列号>、--exe <JLink.exe路径>
jlink_rtt.py — RTT 日志读取
python <skill-dir>/scripts/jlink_rtt.py --device GD32F470ZG --json可选参数:--serial-no、--channel、--encoding、--rtt-port、--gdbserver-exe <路径>、--rtt-exe <路径>
RTT 工作原理:脚本先通过 JLinkGDBServerCL.exe 建立调试连接,再启动 JLinkRTTClient.exe 读取 RTT 数据。--json 模式输出 JSON Lines。
jlink_swo.py — SWO 事件流包装
# 使用 config.json 里的 swo_command
python <skill-dir>/scripts/jlink_swo.py --json
# 或显式传入 viewer 命令
python <skill-dir>/scripts/jlink_swo.py \
--viewer-cmd JLinkSWOViewerCL.exe -device GD32F470ZG -itf SWD -speed 4000 \
--jsonjlink_swo.py 不直接实现 SWO 协议,而是把外部 viewer 的 stdout/stderr 统一包装成 JSON Lines,便于上层 workflow 或 AI 继续消费。
jlink_gdb.py — GDB 源码级调试(需要 arm-none-eabi-gdb)
# 执行自定义 GDB 命令序列
python <skill-dir>/scripts/jlink_gdb.py run \
--gdbserver-exe <路径> --gdb-exe <arm-none-eabi-gdb路径> \
--device GD32F470ZG --elf build/app.elf \
--commands "break main" "continue" "backtrace" "info locals" --json
# 快捷:获取调用栈
python <skill-dir>/scripts/jlink_gdb.py backtrace \
--gdbserver-exe <路径> --gdb-exe <路径> \
--device GD32F470ZG --elf build/app.elf --json
# 快捷:查看局部变量
python <skill-dir>/scripts/jlink_gdb.py locals \
--gdbserver-exe <路径> --gdb-exe <路径> \
--device GD32F470ZG --elf build/app.elf --jsonGDB 调试需要 ELF 文件才能进行源码级调试(断点到函数名、查看变量)。没有 ELF 时仍可使用地址级调试。
输出格式
所有脚本以 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=0x08049ABC",
"details": {
"device": "GD32F470ZG",
"registers": { "PC": "0x08049ABC", "R0": "0x00000004", "..." : "..." }
}
}step 示例(包含执行的指令):
{
"status": "ok",
"action": "step",
"summary": "单步3次,PC=0x08049AB4",
"details": {
"steps": [
{ "address": "0x08049AB8", "opcode": "80 1B", "instruction": "SUBS R0, R0, R6" },
{ "address": "0x08049ABA", "opcode": "A8 42", "instruction": "CMP R0, R5" },
{ "address": "0x08049ABC", "opcode": "FA D3", "instruction": "BCC #-0x0C" }
],
"registers": { "PC": "0x08049AB4", "..." : "..." }
}
}run-to 示例(断点命中):
{
"status": "ok",
"action": "run-to",
"summary": "断点命中 @ 0x08049AB4,PC=0x08049AB4",
"details": {
"bp_address": "0x08049AB4",
"bp_hit": true,
"registers": { "PC": "0x08049AB4", "..." : "..." }
}
}核心规则
- 不自动猜测
device芯片型号,缺失时必须询问用户 - 多探针场景不自动选择探针,必须让用户指定序列号
- 参数解析优先级为:CLI 显式参数 > `.embeddedskills/config.json`(工程级)> `skill/config.json`(环境级)> `.embeddedskills/state.json` > 默认值/报错
.bin文件必须显式提供烧录地址,缺失时报错- 连接失败时给出排查建议(检查连线、供电、接口类型、速度),不自动尝试更激进参数
- 烧录、写内存、复位在参数完整且用户意图明确时直接执行
run-to的断点在单次 JLink 会话内完成设置和清除,不存在跨会话 handle 问题- 结果回显中始终包含目标芯片、接口类型和执行动作
- 产物路径(elf/file)不进入工程配置,仍只依赖
state.json last_flash/last_debug等运行状态继续写入state.json
调试典型工作流
快速排查(JLink Commander)
halt → regs → read-mem → step → go适合查看当前执行位置、寄存器状态、内存值,无需 ELF 和 GDB。
断点调试(JLink Commander)
run-to(address) → regs → read-mem → go在指定地址设置断点并等待命中,查看此时的状态。
源码级调试(GDB)
gdb run --elf app.elf --commands "break main" "continue" "backtrace" "info locals"需要 ELF 文件和 arm-none-eabi-gdb,支持函数名断点和变量查看。
参考
遇到芯片型号问题时可查阅 references/common_devices.md。
{
"exe": "C:\\Program Files\\SEGGER\\JLink\\JLink.exe",
"gdbserver_exe": "C:\\Program Files\\SEGGER\\JLink\\JLinkGDBServerCL.exe",
"rtt_exe": "C:\\Program Files\\SEGGER\\JLink\\JLinkRTTClient.exe",
"gdb_exe": "C:\\Program Files\\Arm\\GNU Toolchain mingw-w64-x86_64-arm-none-eabi\\bin\\arm-none-eabi-gdb.exe",
"serial_no": "",
"rtt_telnet_port": 0,
"swo_command": [],
"operation_mode": 1
}
jlink
Claude Code skill,通过 J-Link 探针进行嵌入式设备的固件烧录、内存读写、寄存器查看、RTT/SWO 日志读取和在线调试。
功能
- 探针与目标连通性探测
- 固件烧录(.hex / .bin / .elf)
- 内存读写、寄存器查看、目标复位
- RTT 日志实时读取
- SWO 事件流包装(通过外部 viewer)
- 在线调试:暂停/恢复/单步/断点运行
- GDB 源码级调试:调用栈、局部变量查看
环境要求
- SEGGER J-Link Software — 提供 JLink.exe、JLinkGDBServerCL.exe、JLinkRTTClient.exe
- Python 3.x(仅标准库,无额外依赖)
- GDB 调试需要
arm-none-eabi-gdb(随 Arm GNU Toolchain 安装)
配置
环境级配置(skill/config.json)
复制 config.example.json 为 config.json,根据实际安装路径修改:
{
"exe": "C:\\Program Files\\SEGGER\\JLink\\JLink.exe",
"gdbserver_exe": "C:\\Program Files\\SEGGER\\JLink\\JLinkGDBServerCL.exe",
"rtt_exe": "C:\\Program Files\\SEGGER\\JLink\\JLinkRTTClient.exe",
"gdb_exe": "C:\\Program Files\\Arm\\GNU Toolchain mingw-w64-x86_64-arm-none-eabi\\bin\\arm-none-eabi-gdb.exe",
"serial_no": "",
"rtt_telnet_port": 0,
"swo_command": [],
"operation_mode": 1
}| 字段 | 必填 | 说明 |
|---|---|---|
exe | 是 | JLink.exe 完整路径 |
gdbserver_exe | 否 | JLinkGDBServerCL.exe 路径,RTT 和 GDB 调试需要 |
rtt_exe | 否 | JLinkRTTClient.exe 路径,RTT 需要 |
gdb_exe | 否 | arm-none-eabi-gdb 路径,GDB 源码级调试需要 |
serial_no | 否 | 探针序列号,多探针场景使用 |
rtt_telnet_port | 否 | RTT 端口,0 使用工具默认值 |
swo_command | 否 | 外部 SWO viewer 的完整命令数组,供 jlink_swo.py 包装 |
operation_mode | 否 | 1 直接执行 / 2 输出风险摘要 / 3 执行前确认 |
工程级配置(.embeddedskills/config.json)
设备参数(device/interface/speed)统一在工作区的 .embeddedskills/config.json 中管理:
{
"jlink": {
"device": "STM32F407VG",
"interface": "SWD",
"speed": "4000"
}
}参数解析优先级:CLI 参数 > 工程配置 > state.json > 默认值
成功执行后,确认过的 device/interface/speed 会自动写回工程配置。
常见芯片 Device 名称速查表
在 J-Link Commander 中使用的芯片型号名称(区分大小写)。
STMicroelectronics
| 系列 | Device 名称示例 |
|---|---|
| STM32F0 | STM32F030R8, STM32F072RB |
| STM32F1 | STM32F103C8, STM32F103RB, STM32F103ZE |
| STM32F2 | STM32F207VG, STM32F207ZG |
| STM32F3 | STM32F303CC, STM32F303VC |
| STM32F4 | STM32F401RE, STM32F407VG, STM32F429ZI |
| STM32F7 | STM32F746ZG, STM32F767ZI |
| STM32G0 | STM32G030K8, STM32G071RB |
| STM32G4 | STM32G431KB, STM32G474RE |
| STM32H7 | STM32H743ZI, STM32H750VB |
| STM32L0 | STM32L053R8, STM32L073RZ |
| STM32L1 | STM32L151RB, STM32L152RE |
| STM32L4 | STM32L431RC, STM32L476RG |
| STM32U5 | STM32U575ZI, STM32U585AI |
| STM32WB | STM32WB55RG |
| STM32WL | STM32WLE5JC |
GigaDevice
| 系列 | Device 名称示例 |
|---|---|
| GD32F1 | GD32F103C8, GD32F130G8 |
| GD32F3 | GD32F303CC, GD32F350RB |
| GD32F4 | GD32F407VG, GD32F450ZI |
| GD32E1 | GD32E103C8, GD32E230C8 |
| GD32E5 | GD32E507ZE |
Nordic Semiconductor
| 系列 | Device 名称示例 |
|---|---|
| nRF51 | nRF51822_xxAA |
| nRF52 | nRF52832_xxAA, nRF52840_xxAA |
| nRF53 | nRF5340_xxAA_APP, nRF5340_xxAA_NET |
| nRF91 | nRF9160_xxAA |
NXP
| 系列 | Device 名称示例 |
|---|---|
| LPC | LPC1768, LPC54608J512 |
| i.MX RT | MIMXRT1052xxx5B, MIMXRT1062xxx5A |
| Kinetis | MK64FN1M0xxx12 |
Microchip (Atmel)
| 系列 | Device 名称示例 |
|---|---|
| SAM | ATSAMD21G18, ATSAME70Q21 |
查找完整列表
如果上表未包含目标芯片,可通过以下方式查找:
1. 打开 J-Link Commander,输入 ExpDevList 查看完整列表 2. 在 SEGGER Wiki 搜索: https://wiki.segger.com/Supported_devices 3. 使用 JLink.exe 的 -device ? 参数列出所有支持设备
"""J-Link 设备探测、烧录、内存读写、寄存器查看、复位、在线调试"""
import argparse
import json
import os
import re
import subprocess
import sys
import tempfile
import time
from pathlib import Path
# 添加 runtime 模块路径
SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPT_DIR))
from jlink_runtime import (
load_local_config,
load_project_config,
save_project_config,
load_workspace_state,
get_state_entry,
update_state_entry,
workspace_root,
normalize_path,
hidden_subprocess_kwargs,
is_missing,
)
# J-Link Commander 命令模板
TEMPLATES = {
"info": "si {interface}\nspeed {speed}\nconnect\nsleep 200\nexit\n",
"flash_hex": "si {interface}\nspeed {speed}\nconnect\nloadfile {file}\nr\ng\nexit\n",
"flash_bin": "si {interface}\nspeed {speed}\nconnect\nloadbin {file},{address}\nr\ng\nexit\n",
"read_mem": "si {interface}\nspeed {speed}\nconnect\nhalt\nmem{width} {address},{length}\nexit\n",
"write_mem": "si {interface}\nspeed {speed}\nconnect\nhalt\nw{width} {address},{value}\nexit\n",
"regs": "si {interface}\nspeed {speed}\nconnect\nhalt\nregs\nexit\n",
"reset": "si {interface}\nspeed {speed}\nconnect\nr\ng\nexit\n",
"halt": "si {interface}\nspeed {speed}\nconnect\nhalt\nregs\nexit\n",
"go": "si {interface}\nspeed {speed}\nconnect\ng\nexit\n",
"step": "si {interface}\nspeed {speed}\nconnect\nhalt\n{step_commands}regs\nexit\n",
"run_to": "si {interface}\nspeed {speed}\nconnect\nSetBP {address}\ng\nsleep {timeout_ms}\nhalt\nregs\nexit\n",
}
# 错误模式匹配
ERROR_PATTERNS = [
(r"Cannot connect to target", "cannot_connect_target", "无法连接目标芯片,请检查连线、供电和接口类型"),
(r"Could not find core", "core_not_found", "未找到内核,请确认 device 是否匹配目标芯片"),
(r"No J-Link found", "no_jlink_found", "未检测到 J-Link 探针,请确认 USB 连接和驱动"),
(r"Multiple J-Links found", "multiple_jlinks", "检测到多个 J-Link 探针,请通过 --serial-no 指定序列号"),
(r"Could not open file", "file_not_found", "无法打开固件文件,请确认路径正确"),
(r"Unknown device", "unknown_device", "未知芯片型号,请确认 --device 参数"),
(r"VTarget too low", "vtarget_low", "目标电压过低,请检查目标板供电"),
]
def build_jlink_cmd(exe: str, device: str, script_path: str, serial_no: str = "") -> list:
"""构建 JLink.exe 命令行"""
cmd = [exe, "-NoGui", "1", "-ExitOnError", "1", "-AutoConnect", "1"]
cmd.extend(["-Device", device])
if serial_no:
cmd.extend(["-SelectEmuBySN", serial_no])
cmd.extend(["-CommandFile", script_path])
return cmd
def parse_registers(stdout: str) -> dict:
"""从 JLink 输出中解析寄存器值"""
registers = {}
# 匹配 "REG = HEXVALUE" 或 "REG= HEXVALUE" 格式
reg_lines = re.findall(r"(\w+)\s*=\s*([0-9A-Fa-f]{8})", stdout)
if reg_lines:
registers = {name: f"0x{val}" for name, val in reg_lines}
return registers
def parse_pc(stdout: str) -> str:
"""从输出中提取 PC 值"""
m = re.search(r"PC\s*=\s*([0-9A-Fa-f]{8})", stdout)
return f"0x{m.group(1)}" if m else ""
def parse_output(stdout: str, action: str) -> dict:
"""解析 JLink.exe 输出,提取关键信息"""
result = {"raw": stdout}
# 检查错误模式
for pattern, code, message in ERROR_PATTERNS:
if re.search(pattern, stdout, re.IGNORECASE):
return {"error_code": code, "error_message": message, "raw": stdout}
# info: 提取固件版本和目标信息
if action == "info":
fw = re.search(r"Firmware:\s+(.+)", stdout)
sn = re.search(r"S/N:\s+(\d+)", stdout)
vtarget = re.search(r"VTref=(\d+\.\d+)V", stdout)
device_match = re.search(r"Device \"(.+?)\" selected", stdout)
if fw:
result["firmware"] = fw.group(1).strip()
if sn:
result["serial_no"] = sn.group(1).strip()
if vtarget:
result["vtarget_v"] = float(vtarget.group(1))
if device_match:
result["device"] = device_match.group(1)
# flash: 提取烧录信息
elif action == "flash":
speed = re.search(r"Downloading\s+\d+\s+bytes?\s.*?(\d+\.\d+)\s*KB/s", stdout)
if speed:
result["speed_kbps"] = float(speed.group(1))
if "O.K." in stdout or "Verify successful" in stdout or "Download verified successfully" in stdout:
result["verified"] = True
# read-mem: 提取内存数据
elif action == "read-mem":
mem_lines = re.findall(r"^([0-9A-Fa-f]{8}) = (.+)$", stdout, re.MULTILINE)
if mem_lines:
result["memory"] = []
for addr, data in mem_lines:
cleaned = data.strip()
result["memory"].append({"address": f"0x{addr}", "data": cleaned})
# regs / halt: 提取寄存器值
elif action in ("regs", "halt"):
regs = parse_registers(stdout)
if regs:
result["registers"] = regs
# step: 提取执行的指令和寄存器
elif action == "step":
# 匹配 step 输出: ADDR: OPCODE INSTRUCTION
instructions = re.findall(
r"^([0-9A-Fa-f]{8}):\s+([0-9A-Fa-f ]+?)\s{2,}(.+)$", stdout, re.MULTILINE
)
if instructions:
result["steps"] = []
for addr, opcode, instr in instructions:
result["steps"].append({
"address": f"0x{addr}",
"opcode": opcode.strip(),
"instruction": instr.strip(),
})
regs = parse_registers(stdout)
if regs:
result["registers"] = regs
# run-to: 提取断点命中状态和寄存器
elif action == "run-to":
m = re.search(r"Breakpoint set @ addr 0x([0-9A-Fa-f]+)\s*\(Handle = (\d+)\)", stdout)
if m:
result["bp_address"] = f"0x{m.group(1)}"
result["bp_handle"] = int(m.group(2))
elif "Could not set" in stdout:
return {"error_code": "bp_set_failed", "error_message": "断点设置失败,可能硬件断点槽已满", "raw": stdout}
regs = parse_registers(stdout)
if regs:
result["registers"] = regs
# 判断是否命中断点(PC == 断点地址)
pc = parse_pc(stdout)
if m and pc:
bp_addr = f"0x{m.group(1)}"
result["bp_hit"] = pc.upper() == bp_addr.upper()
return result
def run_jlink(exe: str, device: str, action: str, interface: str = "SWD",
speed: str = "4000", serial_no: str = "", file: str = "",
address: str = "", length: str = "256", value: str = "",
width: str = "32", step_count: int = 1,
timeout_ms: str = "2000") -> dict:
"""执行 JLink Commander 命令"""
start_time = time.time()
# 选择模板
if action == "flash":
if file.lower().endswith(".bin"):
if not address:
return {
"status": "error",
"action": action,
"error": {"code": "missing_address", "message": ".bin 文件必须提供 --address 烧录地址"},
}
template = TEMPLATES["flash_bin"]
else:
template = TEMPLATES["flash_hex"]
else:
template_key = action.replace("-", "_")
if template_key in TEMPLATES:
template = TEMPLATES[template_key]
else:
return {
"status": "error",
"action": action,
"error": {"code": "unknown_action", "message": f"未知子命令: {action}"},
}
# width 映射
width_map = {"8": "8", "16": "16", "32": "32"}
w = width_map.get(width, "32")
# step 命令: 生成多条 step 指令
step_commands = ""
if action == "step":
step_commands = "".join(["step\n" for _ in range(step_count)])
# 渲染命令脚本
script_content = template.format(
interface=interface, speed=speed, file=file,
address=address, length=length, value=value, width=w,
step_commands=step_commands, timeout_ms=timeout_ms,
)
# 写入临时文件
with tempfile.NamedTemporaryFile(mode="w", suffix=".jlink", delete=False, encoding="utf-8") as f:
f.write(script_content)
script_path = f.name
try:
if not os.path.isfile(exe):
return {
"status": "error",
"action": action,
"error": {"code": "exe_not_found", "message": f"JLink.exe 不存在: {exe}"},
}
if file and not os.path.isfile(file):
return {
"status": "error",
"action": action,
"error": {"code": "file_not_found", "message": f"固件文件不存在: {file}"},
}
cmd = build_jlink_cmd(exe, device, script_path, serial_no)
try:
proc = subprocess.run(
cmd, capture_output=True, text=True, timeout=120, encoding="utf-8", errors="replace",
**hidden_subprocess_kwargs()
)
except subprocess.TimeoutExpired:
return {
"status": "error",
"action": action,
"error": {"code": "timeout", "message": "JLink.exe 执行超时(120s)"},
}
except Exception as e:
return {
"status": "error",
"action": action,
"error": {"code": "exec_error", "message": str(e)},
}
elapsed_ms = int((time.time() - start_time) * 1000)
parsed = parse_output(proc.stdout, action)
if "error_code" in parsed:
return {
"status": "error",
"action": action,
"error": {"code": parsed["error_code"], "message": parsed["error_message"]},
"details": {"device": device, "elapsed_ms": elapsed_ms, "errorlevel": proc.returncode},
}
# 构建摘要
summary_map = {
"info": "探测成功",
"flash": "烧录成功",
"read-mem": "内存读取成功",
"write-mem": "内存写入成功",
"regs": "寄存器读取成功",
"reset": "复位成功",
"halt": f"已暂停,PC={parse_pc(proc.stdout)}",
"go": "已恢复运行",
"step": f"单步{step_count}次,PC={parse_pc(proc.stdout)}",
"run-to": f"运行至断点,PC={parse_pc(proc.stdout)}",
}
summary = summary_map.get(action, "执行成功")
# 补充 run-to 摘要
if action == "run-to" and "bp_hit" in parsed:
if parsed["bp_hit"]:
summary = f"断点命中 @ {parsed['bp_address']},PC={parse_pc(proc.stdout)}"
else:
summary = f"超时未命中断点 @ {parsed.get('bp_address', '?')},当前 PC={parse_pc(proc.stdout)}"
details = {
"device": device,
"interface": interface,
"speed_khz": int(speed),
"elapsed_ms": elapsed_ms,
"errorlevel": proc.returncode,
}
if serial_no:
details["serial_no"] = serial_no
# 合并解析结果
for k, v in parsed.items():
if k != "raw":
details[k] = v
# 判断状态: returncode!=0 可能只是警告,需结合输出判断
if proc.returncode != 0 and "error_code" not in parsed:
if action == "flash" and parsed.get("verified"):
status = "ok"
else:
status = "error"
summary = f"执行返回非零退出码: {proc.returncode}"
else:
status = "ok"
return {
"status": status,
"action": action,
"summary": summary,
"details": details,
}
finally:
try:
os.unlink(script_path)
except OSError:
pass
def output_json(data: dict):
sys.stdout.reconfigure(encoding="utf-8")
print(json.dumps(data, ensure_ascii=False, indent=2))
ALL_ACTIONS = [
"info", "flash", "read-mem", "write-mem", "regs", "reset",
"halt", "go", "step", "run-to",
]
def resolve_device_params(args):
"""解析 exe/device/interface/speed/serial_no 参数。
优先级:
- exe / serial_no: CLI > 环境级配置
- device / interface / speed: CLI > 工程配置 > state.json > 默认值
"""
workspace = workspace_root(args.workspace)
local_config = load_local_config(__file__)
project_config = load_project_config(str(workspace))
state = load_workspace_state(str(workspace))
# 从 state 获取历史值
last_flash = get_state_entry(state, "last_flash")
last_debug = get_state_entry(state, "last_debug")
# device: CLI > 工程配置 > state > 报错
device = args.device
device_source = "cli"
if is_missing(device):
device = project_config.get("device")
device_source = "project_config"
if is_missing(device):
device = last_flash.get("device") or last_debug.get("device")
device_source = "state"
# interface: CLI > 工程配置 > state > 默认 SWD
interface = args.interface
interface_source = "cli"
if is_missing(interface):
interface = project_config.get("interface")
interface_source = "project_config"
if is_missing(interface):
interface = last_flash.get("interface") or last_debug.get("interface")
interface_source = "state"
if is_missing(interface):
interface = "SWD"
interface_source = "default"
# speed: CLI > 工程配置 > state > 默认 4000
speed = args.speed
speed_source = "cli"
if is_missing(speed):
speed = project_config.get("speed")
speed_source = "project_config"
if is_missing(speed):
speed = last_flash.get("speed") or last_debug.get("speed")
speed_source = "state"
if is_missing(speed):
speed = "4000"
speed_source = "default"
# exe: CLI > 环境级配置
exe = args.exe
exe_source = "cli"
if is_missing(exe):
exe = local_config.get("exe", "")
exe_source = "config" if not is_missing(exe) else ""
if not is_missing(exe):
exe = normalize_path(str(exe))
# serial_no: CLI > 环境级配置 > state
serial_no = args.serial_no
serial_no_source = "cli"
if is_missing(serial_no):
serial_no = local_config.get("serial_no", "")
serial_no_source = "config" if not is_missing(serial_no) else ""
if is_missing(serial_no):
serial_no = last_flash.get("serial_no") or last_debug.get("serial_no") or ""
serial_no_source = "state" if not is_missing(serial_no) else ""
return {
"exe": exe,
"exe_source": exe_source,
"device": device,
"device_source": device_source,
"interface": interface,
"interface_source": interface_source,
"speed": speed,
"speed_source": speed_source,
"serial_no": serial_no,
"serial_no_source": serial_no_source,
}
def main():
parser = argparse.ArgumentParser(description="J-Link 设备探测/烧录/内存读写/寄存器/复位/在线调试")
parser.add_argument("action", choices=ALL_ACTIONS)
parser.add_argument("--exe", default="", help="JLink.exe 路径")
parser.add_argument("--device", default=None, help="芯片型号(如 STM32F407VG)")
parser.add_argument("--interface", default=None, help="调试接口")
parser.add_argument("--speed", default=None, help="调试速率 kHz")
parser.add_argument("--serial-no", default="", help="探针序列号")
parser.add_argument("--file", default="", help="固件文件路径(flash 用)")
parser.add_argument("--address", default="", help="地址(flash .bin / read-mem / write-mem / bp-set 用)")
parser.add_argument("--length", default="256", help="读取长度(read-mem 用)")
parser.add_argument("--value", default="", help="写入值(write-mem 用)")
parser.add_argument("--width", default="32", choices=["8", "16", "32"], help="数据宽度")
parser.add_argument("--count", type=int, default=1, help="单步次数(step 用)")
parser.add_argument("--timeout-ms", default="2000", help="run-to 等待断点命中的超时毫秒数")
parser.add_argument("--workspace", default=None, help="workspace 根目录,默认当前目录")
parser.add_argument("--json", action="store_true", dest="as_json")
args = parser.parse_args()
# 解析参数
params = resolve_device_params(args)
workspace = workspace_root(args.workspace)
# 检查 device 是否已提供
if is_missing(params["device"]):
result = {
"status": "error", "action": args.action,
"error": {"code": "missing_device", "message": "必须提供 --device 芯片型号,或通过 .embeddedskills/config.json 配置"},
}
if args.as_json:
output_json(result)
else:
print(f"错误: {result['error']['message']}", file=sys.stderr)
sys.exit(1)
# 参数校验
if args.action == "flash" and not args.file:
result = {
"status": "error", "action": "flash",
"error": {"code": "missing_file", "message": "flash 必须提供 --file 固件文件路径"},
}
if args.as_json:
output_json(result)
else:
print(f"错误: {result['error']['message']}", file=sys.stderr)
sys.exit(1)
if args.action == "write-mem" and (not args.address or not args.value):
result = {
"status": "error", "action": "write-mem",
"error": {"code": "missing_params", "message": "write-mem 必须提供 --address 和 --value"},
}
if args.as_json:
output_json(result)
else:
print(f"错误: {result['error']['message']}", file=sys.stderr)
sys.exit(1)
if args.action == "read-mem" and not args.address:
result = {
"status": "error", "action": "read-mem",
"error": {"code": "missing_address", "message": "read-mem 必须提供 --address"},
}
if args.as_json:
output_json(result)
else:
print(f"错误: {result['error']['message']}", file=sys.stderr)
sys.exit(1)
if args.action == "run-to" and not args.address:
result = {
"status": "error", "action": "run-to",
"error": {"code": "missing_address", "message": "run-to 必须提供 --address 断点地址"},
}
if args.as_json:
output_json(result)
else:
print(f"错误: {result['error']['message']}", file=sys.stderr)
sys.exit(1)
result = run_jlink(
exe=params["exe"],
device=params["device"],
action=args.action,
interface=params["interface"],
speed=params["speed"],
serial_no=params["serial_no"],
file=args.file,
address=args.address,
length=args.length,
value=args.value,
width=args.width,
step_count=args.count,
timeout_ms=args.timeout_ms,
)
# 成功执行后,写回确认过的参数到工程配置
if result.get("status") == "ok":
save_project_config(str(workspace), {
"device": params["device"],
"interface": params["interface"],
"speed": params["speed"],
})
# 同时更新 state.json
if args.action in ("flash", "reset", "halt", "go", "step", "run-to", "info"):
state_action = "last_flash" if args.action == "flash" else "last_debug"
update_state_entry(
state_action,
{
"provider": "jlink",
"action": args.action,
"device": params["device"],
"interface": params["interface"],
"speed": params["speed"],
"serial_no": params["serial_no"] or "",
},
str(workspace),
)
if args.as_json:
# 添加参数来源信息
if "details" not in result:
result["details"] = {}
result["details"]["parameter_sources"] = {
"exe": params["exe_source"],
"device": params["device_source"],
"interface": params["interface_source"],
"speed": params["speed_source"],
"serial_no": params["serial_no_source"],
}
output_json(result)
else:
if result["status"] == "ok":
print(f"[{args.action}] {result.get('summary', '成功')}")
details = result.get("details", {})
if "registers" in details:
# 只显示核心寄存器
core_regs = ["PC", "R0", "R1", "R2", "R3", "R4", "R5", "R6", "R7",
"R8", "R9", "R10", "R11", "R12", "MSP", "PSP", "XPSR"]
for name in core_regs:
if name in details["registers"]:
print(f" {name:>5s} = {details['registers'][name]}")
if "steps" in details:
for s in details["steps"]:
print(f" {s['address']}: {s['opcode']:16s} {s['instruction']}")
if "memory" in details:
for m in details["memory"]:
print(f" {m['address']}: {m['data']}")
if "bp_hit" in details:
hit = "命中" if details["bp_hit"] else "未命中(超时)"
print(f" 断点: {details.get('bp_address', '?')} — {hit}")
else:
err = result.get("error", {})
print(f"[{args.action}] 失败 — {err.get('message', '未知错误')}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()
"""jlink skill 私有 GDB 工具。"""
from __future__ import annotations
import re
import subprocess
from pathlib import Path
from typing import Any
from jlink_runtime import hidden_subprocess_kwargs
INTROSPECTION_ACTIONS = {
"backtrace",
"locals",
"frame",
"print",
"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 "{Path(elf_file).resolve().as_posix()}"')
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(),
)
combined_output = "\n".join(part for part in (proc.stdout, proc.stderr) if part)
critical_patterns = [
r"No such file or directory",
r"No symbol table is loaded",
r'Function ".+?" not defined',
r'No symbol ".+?" in current context',
]
has_critical_error = any(re.search(pattern, combined_output) for pattern in critical_patterns)
return {
"status": "ok" if proc.returncode == 0 and not has_critical_error else "error",
"stdout": combined_output,
"stderr": proc.stderr,
"returncode": proc.returncode,
}
except subprocess.TimeoutExpired as exc:
stdout = exc.stdout or ""
stderr = exc.stderr or ""
combined_output = "\n".join(part for part in (stdout, stderr) if part)
return {
"status": "timeout",
"stdout": combined_output,
"stderr": stderr,
"returncode": None,
"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("continue")
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
"""J-Link one-shot GDB 源码级调试。"""
from __future__ import annotations
import argparse
import os
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 jlink_gdb_common import build_gdb_commands, parse_gdb_output, run_gdb_commands # noqa: E402
from jlink_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,
)
ALL_COMMANDS = [
"run",
"backtrace",
"locals",
"break",
"continue",
"next",
"step",
"finish",
"until",
"frame",
"print",
"watch",
"disassemble",
"threads",
"crash-report",
]
def find_free_port() -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind(("", 0))
return sock.getsockname()[1]
def start_gdbserver(
gdbserver_exe: str,
device: str,
interface: str = "SWD",
speed: str = "4000",
serial_no: str = "",
gdb_port: int = 0,
) -> tuple[subprocess.Popen, int]:
if not gdb_port:
gdb_port = find_free_port()
cmd = [
gdbserver_exe,
"-device",
device,
"-if",
interface,
"-speed",
speed,
"-port",
str(gdb_port),
"-noir",
"-LocalhostOnly",
"-nologtofile",
"-singlerun",
]
if serial_no:
cmd.extend(["-select", f"USB={serial_no}"])
proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
**hidden_subprocess_kwargs(new_process_group=True),
)
return proc, gdb_port
def wait_gdbserver_ready(proc: subprocess.Popen, timeout: int = 15) -> tuple[bool, str]:
started = time.time()
captured: list[str] = []
while time.time() - started < timeout:
if proc.poll() is not None:
captured.append(proc.stderr.read())
return False, "\n".join(line for line in captured if line).strip()
line = proc.stdout.readline()
if not line:
time.sleep(0.1)
continue
captured.append(line.strip())
if "Waiting for GDB connection" in line or "Connected to target" in line:
return True, "\n".join(captured)
if "Cannot connect" in line or "Could not connect" in line:
return False, "\n".join(captured)
return False, "\n".join(captured)
def cleanup(procs: list[subprocess.Popen]) -> None:
for proc in procs:
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("--gdbserver-exe", default=None, help="JLinkGDBServerCL.exe 路径")
parser.add_argument("--gdb-exe", default=None, help="arm-none-eabi-gdb 路径")
parser.add_argument("--device", default=None, help="芯片型号")
parser.add_argument("--elf", default=None, help="ELF 文件路径")
parser.add_argument("--interface", default=None, help="调试接口")
parser.add_argument("--speed", default=None, help="调试速率 kHz")
parser.add_argument("--serial-no", default=None, help="探针序列号")
parser.add_argument("--gdb-port", type=int, default=0, help="GDB 端口,0=自动")
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() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(description="J-Link GDB Server 调试")
sub = parser.add_subparsers(dest="command")
for name in ALL_COMMANDS:
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 _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 {
"device": last_debug.get("device") or last_flash.get("device"),
"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"),
"serial_no": last_debug.get("serial_no") or last_flash.get("serial_no"),
"interface": last_debug.get("interface") or last_flash.get("interface"),
"speed": last_debug.get("speed") or last_flash.get("speed"),
}
def resolve_device_params(args, project_config: dict, state_lookup: dict) -> dict:
"""解析 device/interface/speed 参数,优先级: CLI > 工程配置 > state.json > default"""
# device: CLI > 工程配置 > state > 报错
device = args.device
device_source = "cli"
if is_missing(device):
device = project_config.get("device")
device_source = "project_config"
if is_missing(device):
device = state_lookup.get("device")
device_source = "state"
# interface: CLI > 工程配置 > state > 默认 SWD
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"
if is_missing(interface):
interface = "SWD"
interface_source = "default"
# speed: CLI > 工程配置 > state > 默认 4000
speed = args.speed
speed_source = "cli"
if is_missing(speed):
speed = project_config.get("speed")
speed_source = "project_config"
if is_missing(speed):
speed = state_lookup.get("speed")
speed_source = "state"
if is_missing(speed):
speed = "4000"
speed_source = "default"
return {
"device": device,
"device_source": device_source,
"interface": interface,
"interface_source": interface_source,
"speed": speed,
"speed_source": speed_source,
}
def _summary(command: str, parsed: dict) -> str:
if command == "continue" and parsed.get("timed_out"):
return "continue 已执行,目标在超时窗口内未停下"
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:
parser = build_parser()
args = parser.parse_args()
if not args.command:
parser.print_help()
sys.exit(1)
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))
# 解析 device/interface/speed 参数
dev_params = resolve_device_params(args, project_config, state_lookup)
parameter_sources: dict[str, str] = {}
try:
gdbserver_exe, parameter_sources["gdbserver_exe"] = resolve_param(
"gdbserver_exe",
args.gdbserver_exe,
config=config,
config_keys=["gdbserver_exe"],
required=True,
normalize_as_path=True,
workspace=str(workspace),
)
gdb_exe, parameter_sources["gdb_exe"] = resolve_param(
"gdb_exe",
args.gdb_exe,
config=config,
config_keys=["gdb_exe"],
required=True,
normalize_as_path=True,
workspace=str(workspace),
)
# device 从工程配置或 state 解析
device = dev_params["device"]
parameter_sources["device"] = dev_params["device_source"]
if is_missing(device):
raise ValueError("缺少必要参数: device")
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,
workspace=str(workspace),
)
interface = dev_params["interface"]
parameter_sources["interface"] = dev_params["interface_source"]
speed = dev_params["speed"]
parameter_sources["speed"] = dev_params["speed_source"]
serial_no, parameter_sources["serial_no"] = resolve_param(
"serial_no",
args.serial_no,
config=config,
config_keys=["serial_no"],
state_record=state_lookup,
state_keys=["serial_no"],
)
except ValueError as exc:
result = make_result(
status="error",
action=args.command,
summary=str(exc),
details={},
context=parameter_context(
provider="jlink",
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(gdbserver_exe):
message = f"JLinkGDBServerCL.exe 不存在: {gdbserver_exe}"
result = make_result(
status="error",
action=args.command,
summary=message,
details={},
context=parameter_context(provider="jlink", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "gdbserver_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)
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="jlink", 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)
procs: list[subprocess.Popen] = []
try:
gdb_proc, gdb_port = start_gdbserver(
gdbserver_exe=gdbserver_exe,
device=device,
interface=interface or "SWD",
speed=speed or "4000",
serial_no=serial_no or "",
gdb_port=args.gdb_port,
)
procs.append(gdb_proc)
ready, server_output = wait_gdbserver_ready(gdb_proc)
if not ready:
result = make_result(
status="error",
action=args.command,
summary="GDB Server 启动失败",
details={"device": device, "server_output": server_output},
context=parameter_context(
provider="jlink",
workspace=str(workspace),
parameter_sources=parameter_sources,
config_path=config_path,
),
error={"code": "gdbserver_failed", "message": server_output or "GDB Server 启动失败"},
timing=make_timing(started_at, (time.time() - started_ts) * 1000),
)
if args.as_json:
output_json(result)
else:
print(f"[gdb-{args.command}] 失败 — {result['error']['message']}", file=sys.stderr)
sys.exit(1)
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="jlink",
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:{gdb_port}", gdb_commands)
elapsed_ms = (time.time() - started_ts) * 1000
if gdb_result["status"] == "timeout" and args.command == "continue":
parsed = parse_gdb_output(gdb_result.get("stdout", ""), args.command)
parsed["timed_out"] = True
artifacts = build_artifacts(debug_file=elf_file)
state_info = update_state_entry(
"last_debug",
{
"provider": "jlink",
"action": args.command,
"device": device,
"interface": interface or "SWD",
"speed": speed or "4000",
"serial_no": serial_no or "",
"debug_file": elf_file or "",
"artifacts": artifacts,
},
str(workspace),
)
save_project_config(str(workspace), {
"device": device,
"interface": interface or "SWD",
"speed": speed or "4000",
})
result = make_result(
status="ok",
action=args.command,
summary=_summary(args.command, parsed),
details={
"device": device,
"gdb_port": gdb_port,
"commands": gdb_commands,
"output": parsed.get("output", ""),
"server_output": server_output,
"returncode": gdb_result.get("returncode", 0),
**{key: value for key, value in parsed.items() if key != "output"},
},
context=parameter_context(
provider="jlink",
workspace=str(workspace),
parameter_sources=parameter_sources,
config_path=config_path,
),
artifacts=artifacts,
metrics=_metrics(parsed),
state=state_info,
next_actions=["目标已继续运行,如需停下请再次执行 halt/backtrace/run"],
timing=make_timing(started_at, elapsed_ms),
)
elif gdb_result["status"] == "error" or gdb_result["status"] == "timeout":
result = make_result(
status="error",
action=args.command,
summary="GDB 执行失败",
details={"device": device, "gdb_port": gdb_port},
context=parameter_context(
provider="jlink",
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": "jlink",
"action": args.command,
"device": device,
"interface": interface or "SWD",
"speed": speed or "4000",
"serial_no": serial_no or "",
"debug_file": elf_file or "",
"artifacts": artifacts,
},
str(workspace),
)
# 写回确认过的参数到工程配置
save_project_config(str(workspace), {
"device": device,
"interface": interface or "SWD",
"speed": speed or "4000",
})
result = make_result(
status="ok",
action=args.command,
summary=_summary(args.command, parsed),
details={
"device": device,
"gdb_port": gdb_port,
"commands": gdb_commands,
"output": parsed.get("output", ""),
"server_output": server_output,
"returncode": gdb_result.get("returncode", 0),
**{key: value for key, value in parsed.items() if key != "output"},
},
context=parameter_context(
provider="jlink",
workspace=str(workspace),
parameter_sources=parameter_sources,
config_path=config_path,
),
artifacts=artifacts,
metrics=_metrics(parsed),
state=state_info,
next_actions=["可继续基于 last_debug 复用 device/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)
finally:
cleanup(procs)
if __name__ == "__main__":
main()
"""J-Link RTT 日志读取。"""
from __future__ import annotations
import argparse
import os
import queue
import signal
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 jlink_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 start_gdbserver(
gdbserver_exe: str,
device: str,
interface: str = "SWD",
speed: str = "4000",
serial_no: str = "",
rtt_port: int = 0,
) -> subprocess.Popen:
cmd = [
gdbserver_exe,
"-device",
device,
"-if",
interface,
"-speed",
speed,
"-noir",
"-LocalhostOnly",
"-nologtofile",
"-singlerun",
]
if serial_no:
cmd.extend(["-select", f"USB={serial_no}"])
if rtt_port:
cmd.extend(["-RTTTelnetPort", str(rtt_port)])
return subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
**hidden_subprocess_kwargs(new_process_group=True),
)
def wait_gdbserver_ready(proc: subprocess.Popen, timeout: int = 15) -> tuple[bool, str]:
started = time.time()
captured: list[str] = []
while time.time() - started < timeout:
if proc.poll() is not None:
captured.append(proc.stderr.read())
return False, "\n".join(line for line in captured if line).strip()
line = proc.stdout.readline()
if not line:
time.sleep(0.1)
continue
captured.append(line.strip())
if "Waiting for GDB connection" in line or "Connected to target" in line or "J-Link is connected" in line:
return True, "\n".join(captured)
if "Cannot connect" in line or "Could not connect" in line:
return False, "\n".join(captured)
return False, "\n".join(captured)
def start_rtt_client(rtt_exe: str, rtt_port: int = 19021) -> subprocess.Popen:
cmd = [rtt_exe]
if rtt_port:
cmd.extend(["-LocalEcho", "Off", "-RTTTelnetPort", str(rtt_port)])
return subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
**hidden_subprocess_kwargs(),
)
def cleanup(procs: list[subprocess.Popen]) -> None:
for proc in procs:
if not proc:
continue
try:
if proc.poll() is None:
if sys.platform == "win32":
proc.terminate()
else:
proc.send_signal(signal.SIGTERM)
proc.wait(timeout=5)
except (subprocess.TimeoutExpired, OSError):
pass
if proc.poll() is None:
if sys.platform == "win32":
subprocess.run(
["taskkill", "/PID", str(proc.pid), "/T", "/F"],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
**hidden_subprocess_kwargs(),
)
else:
proc.kill()
def start_stream_reader(stream) -> queue.Queue:
line_queue: queue.Queue = queue.Queue()
def _reader() -> None:
try:
for line in iter(stream.readline, ""):
line_queue.put(line)
finally:
line_queue.put(None)
threading.Thread(target=_reader, daemon=True).start()
return line_queue
def _state_lookup(state: dict) -> dict:
last_debug = get_state_entry(state, "last_debug")
last_flash = get_state_entry(state, "last_flash")
return {
"device": last_debug.get("device") or last_flash.get("device"),
"serial_no": last_debug.get("serial_no") or last_flash.get("serial_no"),
"interface": last_debug.get("interface") or last_flash.get("interface"),
"speed": last_debug.get("speed") or last_flash.get("speed"),
}
def resolve_device_params(args, project_config: dict, state_lookup: dict) -> dict:
"""解析 device/interface/speed 参数,优先级: CLI > 工程配置 > state.json > default"""
# device: CLI > 工程配置 > state > 报错
device = args.device
device_source = "cli"
if is_missing(device):
device = project_config.get("device")
device_source = "project_config"
if is_missing(device):
device = state_lookup.get("device")
device_source = "state"
# interface: CLI > 工程配置 > state > 默认 SWD
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"
if is_missing(interface):
interface = "SWD"
interface_source = "default"
# speed: CLI > 工程配置 > state > 默认 4000
speed = args.speed
speed_source = "cli"
if is_missing(speed):
speed = project_config.get("speed")
speed_source = "project_config"
if is_missing(speed):
speed = state_lookup.get("speed")
speed_source = "state"
if is_missing(speed):
speed = "4000"
speed_source = "default"
return {
"device": device,
"device_source": device_source,
"interface": interface,
"interface_source": interface_source,
"speed": speed,
"speed_source": speed_source,
}
def main() -> None:
parser = argparse.ArgumentParser(description="J-Link RTT 日志读取")
parser.add_argument("--device", default=None, help="芯片型号")
parser.add_argument("--gdbserver-exe", default=None, help="JLinkGDBServerCL.exe 路径")
parser.add_argument("--rtt-exe", default=None, help="JLinkRTTClient.exe 路径")
parser.add_argument("--interface", default=None, help="调试接口")
parser.add_argument("--speed", default=None, help="调试速率 kHz")
parser.add_argument("--serial-no", default=None, help="探针序列号")
parser.add_argument("--channel", type=int, default=0, help="RTT 通道")
parser.add_argument("--rtt-port", type=int, default=None, help="RTT Telnet 端口")
parser.add_argument("--duration", type=float, default=0, help="读取时长(秒),0=持续运行")
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))
# 解析 device/interface/speed 参数
dev_params = resolve_device_params(args, project_config, state_lookup)
parameter_sources: dict[str, str] = {}
try:
# device 从工程配置或 state 解析
device = dev_params["device"]
parameter_sources["device"] = dev_params["device_source"]
if is_missing(device):
raise ValueError("缺少必要参数: device")
gdbserver_exe, parameter_sources["gdbserver_exe"] = resolve_param(
"gdbserver_exe",
args.gdbserver_exe,
config=config,
config_keys=["gdbserver_exe"],
required=True,
normalize_as_path=True,
workspace=str(workspace),
)
rtt_exe, parameter_sources["rtt_exe"] = resolve_param(
"rtt_exe",
args.rtt_exe,
config=config,
config_keys=["rtt_exe"],
required=True,
normalize_as_path=True,
workspace=str(workspace),
)
interface = dev_params["interface"]
parameter_sources["interface"] = dev_params["interface_source"]
speed = dev_params["speed"]
parameter_sources["speed"] = dev_params["speed_source"]
serial_no, parameter_sources["serial_no"] = resolve_param(
"serial_no",
args.serial_no,
config=config,
config_keys=["serial_no"],
state_record=state_lookup,
state_keys=["serial_no"],
)
rtt_port, parameter_sources["rtt_port"] = resolve_param(
"rtt_port",
args.rtt_port,
config=config,
config_keys=["rtt_telnet_port"],
)
except ValueError as exc:
result = make_result(
status="error",
action="rtt",
summary=str(exc),
details={},
context=parameter_context(provider="jlink", 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(gdbserver_exe):
message = f"JLinkGDBServerCL.exe 不存在: {gdbserver_exe}"
result = make_result(
status="error",
action="rtt",
summary=message,
details={},
context=parameter_context(provider="jlink", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "gdbserver_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)
if not os.path.isfile(rtt_exe):
message = f"JLinkRTTClient.exe 不存在: {rtt_exe}"
result = make_result(
status="error",
action="rtt",
summary=message,
details={},
context=parameter_context(provider="jlink", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "rtt_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)
rtt_port_value = int(rtt_port or 19021)
procs: list[subprocess.Popen] = []
try:
if not args.as_json:
print("正在启动 JLinkGDBServerCL ...", file=sys.stderr, flush=True)
gdb_proc = start_gdbserver(
gdbserver_exe=gdbserver_exe,
device=device,
interface=interface or "SWD",
speed=speed or "4000",
serial_no=serial_no or "",
rtt_port=rtt_port_value,
)
procs.append(gdb_proc)
ready, server_output = wait_gdbserver_ready(gdb_proc)
if not ready:
result = make_result(
status="error",
action="rtt",
summary="RTT 服务启动失败",
details={"device": device, "server_output": server_output},
context=parameter_context(provider="jlink", workspace=str(workspace), parameter_sources=parameter_sources, config_path=config_path),
error={"code": "gdbserver_failed", "message": server_output or "GDB Server 启动失败或连接超时"},
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)
state_info = update_state_entry(
"last_observe",
{
"provider": "jlink",
"action": "rtt",
"device": device,
"interface": interface or "SWD",
"speed": speed or "4000",
"serial_no": serial_no or "",
"channel_type": "rtt",
"stream_type": "text",
"source": "jlink",
},
str(workspace),
)
# 写回确认过的参数到工程配置
save_project_config(str(workspace), {
"device": device,
"interface": interface or "SWD",
"speed": speed or "4000",
})
if not args.as_json:
print("GDB Server 就绪,启动 RTT Client ...", file=sys.stderr, flush=True)
rtt_proc = start_rtt_client(rtt_exe, rtt_port_value)
procs.append(rtt_proc)
line_queue = start_stream_reader(rtt_proc.stdout)
if not args.as_json:
print("RTT 输出开始(Ctrl+C 退出):", file=sys.stderr, flush=True)
print("-" * 40, file=sys.stderr, flush=True)
while True:
if args.duration > 0 and (time.time() - started_ts) >= args.duration:
break
try:
line = line_queue.get(timeout=0.1)
except queue.Empty:
if rtt_proc.poll() is not None:
break
continue
if line is None:
if rtt_proc.poll() is not None:
break
continue
stripped = line.strip()
if (
stripped.startswith("###RTT Client:")
or stripped.startswith("SEGGER J-Link")
or stripped.startswith("Process:")
or stripped == ""
or stripped.startswith("***")
or stripped.startswith("---")
):
continue
emit_stream_record(
source="jlink",
channel_type="rtt",
text=line,
as_json=args.as_json,
stream_type="text",
channel=args.channel,
extra={"device": device},
)
except KeyboardInterrupt:
if not args.as_json:
print("\n已停止 RTT 读取", file=sys.stderr, flush=True)
finally:
cleanup(procs)
if __name__ == "__main__":
main()
"""jlink skill 私有运行时工具。"""
from __future__ import annotations
import json
import os
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Any
STATE_DIR_NAME = ".embeddedskills"
STATE_FILE_NAME = "state.json"
PROJECT_CONFIG_FILE_NAME = "config.json"
SKILL_NAME = "jlink"
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/
- jlink_runtime 中 skill_name 硬编码为 "jlink"
"""
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 normalize_path_with_base(value: str | None, base: str | Path | None = None) -> str:
if is_missing(value):
return ""
path = Path(str(value)).expanduser()
if base and not path.is_absolute():
path = Path(base) / path
return str(path.resolve())
def _serialize_state_value(value: Any, workspace: Path) -> Any:
if isinstance(value, dict):
return {key: _serialize_state_value(item, workspace) for key, item in value.items()}
if isinstance(value, list):
return [_serialize_state_value(item, workspace) for item in value]
if not isinstance(value, str) or "://" in value:
return value
path = Path(value).expanduser()
if not path.is_absolute():
return value
try:
return Path(os.path.relpath(path.resolve(), workspace)).as_posix()
except ValueError:
return value
def hidden_subprocess_kwargs(*, new_process_group: bool = False) -> dict:
if sys.platform != "win32":
return {}
creationflags = getattr(subprocess, "CREATE_NO_WINDOW", 0)
if new_process_group:
creationflags |= getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = getattr(subprocess, "SW_HIDE", 0)
return {
"creationflags": creationflags,
"startupinfo": startupinfo,
}
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 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:
ws_root = workspace_root(workspace)
file_path = ws_root / STATE_DIR_NAME / STATE_FILE_NAME
save_json_file(file_path, _serialize_state_value(state, ws_root))
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:
ws_root = workspace_root(workspace)
state = load_workspace_state(workspace)
state[category] = _serialize_state_value({**record, "timestamp": record.get("timestamp") or now_iso()}, ws_root)
file_path = save_workspace_state(state, workspace)
return {
"workspace": str(ws_root),
"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,
workspace: str | None = None,
) -> 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 normalize_as_path and not is_missing(value):
value = normalize_path_with_base(str(value), workspace_root(workspace))
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)
"""J-Link SWO 观测包装层。
说明:
- J-Link 不同版本的 SWO CLI 工具名和参数差异较大。
- 本脚本不硬编码具体 viewer,可通过 --viewer-cmd 或 config.json 中的 swo_command 提供完整命令。
"""
from __future__ import annotations
import argparse
import os
import queue
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 jlink_runtime import ( # noqa: E402
default_config_path,
emit_stream_record,
get_state_entry,
hidden_subprocess_kwargs,
load_json_file,
load_project_config,
load_workspace_state,
make_result,
make_timing,
normalize_path,
now_iso,
output_json,
parameter_context,
update_state_entry,
workspace_root,
)
def start_stream_reader(stream) -> queue.Queue:
line_queue: queue.Queue = queue.Queue()
def _reader() -> None:
try:
for line in iter(stream.readline, ""):
line_queue.put(line)
finally:
line_queue.put(None)
threading.Thread(target=_reader, daemon=True).start()
return line_queue
def _auto_viewer_cmd(config: dict, project_config: dict, state: dict) -> list[str]:
jlink_exe = config.get("exe", "")
if not jlink_exe:
return []
exe_path = Path(str(jlink_exe)).expanduser()
viewer = exe_path.with_name("JLinkSWOViewerCL.exe")
if not viewer.exists():
return []
last_debug = get_state_entry(state, "last_debug")
last_flash = get_state_entry(state, "last_flash")
device = project_config.get("device") or last_debug.get("device") or last_flash.get("device")
if not device:
return []
# JLinkSWOViewerCL 不同版本支持的参数差异很大。
# 这里退回到最稳妥的最小命令,只传 device,避免因 -itf/-speed 不兼容直接失败。
return [str(viewer), "-device", str(device)]
def main() -> None:
parser = argparse.ArgumentParser(description="J-Link SWO 输出捕获")
parser.add_argument("--viewer-cmd", nargs="+", default=None, help="完整 SWO 采集命令,例如 JLinkSWOViewerCL 的调用参数")
parser.add_argument("--duration", type=float, default=0, help="采集时长(秒),0=持续运行")
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, passthrough = parser.parse_known_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)
project_config = load_project_config(str(workspace))
state = load_workspace_state(str(workspace))
viewer_cmd = list(args.viewer_cmd or config.get("swo_command") or _auto_viewer_cmd(config, project_config, state))
viewer_cmd.extend(passthrough)
if not viewer_cmd:
message = "缺少 SWO viewer 命令,请通过 --viewer-cmd 或 jlink/config.json.swo_command 提供"
result = make_result(
status="error",
action="swo",
summary=message,
details={},
context=parameter_context(provider="jlink", workspace=str(workspace), config_path=config_path),
error={"code": "missing_viewer_cmd", "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
try:
proc = subprocess.Popen(
viewer_cmd,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
encoding="utf-8",
errors="replace",
cwd=str(workspace),
**hidden_subprocess_kwargs(),
)
line_queue = start_stream_reader(proc.stdout)
update_state_entry(
"last_observe",
{
"provider": "jlink",
"action": "swo",
"channel_type": "swo",
"stream_type": "text",
"source": "jlink",
},
str(workspace),
)
while True:
if args.duration > 0 and (time.time() - started_ts) >= args.duration:
break
try:
line = line_queue.get(timeout=0.1)
except queue.Empty:
if proc.poll() is not None:
break
continue
if line is None:
if proc.poll() is not None:
break
continue
emit_stream_record(source="jlink", channel_type="swo", text=line, as_json=args.as_json, stream_type="text")
except FileNotFoundError:
message = f"无法启动 SWO viewer: {viewer_cmd[0]}"
result = make_result(
status="error",
action="swo",
summary=message,
details={"viewer_cmd": viewer_cmd},
context=parameter_context(provider="jlink", workspace=str(workspace), config_path=config_path),
error={"code": "viewer_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:
pass
finally:
if proc and proc.poll() is None:
try:
proc.terminate()
proc.wait(timeout=5)
except (subprocess.TimeoutExpired, OSError):
pass
if proc.poll() is None and sys.platform == "win32":
subprocess.run(
["taskkill", "/PID", str(proc.pid), "/T", "/F"],
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
**hidden_subprocess_kwargs(),
)
elif proc.poll() is None:
proc.kill()
if __name__ == "__main__":
main()
si {interface}
speed {speed}
connect
loadfile {file}
r
g
exit
si {interface}
speed {speed}
connect
mem32 {address},{length}
exit
si {interface}
speed {speed}
connect
r
g
exit
Related skills
FAQ
What does jlink do?
jlink is an agent skill for solo embedded developers who debug and ship firmware through SEGGER J-Link on Windows-oriented paths (JLink.exe, JLinkGDBServerCL, RTT client, optional arm-none-eabi-gdb).
When should I use jlink?
When you need to flash firmware and debug ARM embedded targets over SEGGER J-Link with RTT, SWO, and GDB from your agent., or when jlink is an agent skill for solo embedded developers who debug and ship firmware through segger j-link on windows-oriented paths (jlink.
What are the main capabilities?
Firmware programming for .hex, .bin, and .elf images; Probe connectivity checks, memory read/write, registers, and target reset; RTT log streaming and SWO event wrapping via external viewer.
Is Jlink safe to install?
skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.