Browse Source
Add the ST-Link GDB server (part of the STM32CubeCLT) as west runner. The STM32CubeCLT is an all-in-one multi-OS command-line toolset, which is part of the STM32Cube ecosystem, and notably includes a GDB server for debugging using ST-Link probes. This runner supports the "attach", "debug" and "debugserver" commands. Documentation: https://www.st.com/en/development-tools/stm32cubeclt.html Signed-off-by: Mathieu Choplain <mathieu.choplain@st.com>pull/91613/merge
6 changed files with 265 additions and 26 deletions
@ -0,0 +1,4 @@
@@ -0,0 +1,4 @@
|
||||
# SPDX-License-Identifier: Apache-2.0 |
||||
|
||||
board_set_debugger_ifnset(stlink_gdbserver) |
||||
board_finalize_runner_args(stlink_gdbserver) |
@ -0,0 +1,188 @@
@@ -0,0 +1,188 @@
|
||||
# Copyright (c) 2025 STMicroelectronics |
||||
# |
||||
# SPDX-License-Identifier: Apache-2.0 |
||||
|
||||
""" |
||||
Runner for debugging applications using the ST-LINK GDB server |
||||
from STMicroelectronics, provided as part of the STM32CubeCLT. |
||||
""" |
||||
|
||||
import argparse |
||||
import platform |
||||
import re |
||||
import shutil |
||||
from pathlib import Path |
||||
|
||||
from runners.core import MissingProgram, RunnerCaps, RunnerConfig, ZephyrBinaryRunner |
||||
|
||||
STLINK_GDB_SERVER_DEFAULT_PORT = 61234 |
||||
|
||||
|
||||
class STLinkGDBServerRunner(ZephyrBinaryRunner): |
||||
@classmethod |
||||
def _get_stm32cubeclt_paths(cls) -> tuple[Path, Path]: |
||||
""" |
||||
Returns a tuple of two elements of class pathlib.Path: |
||||
[0]: path to the ST-LINK_gdbserver executable |
||||
[1]: path to the "STM32CubeProgrammer/bin" folder |
||||
""" |
||||
|
||||
def find_highest_clt_version(tools_folder: Path) -> Path | None: |
||||
if not tools_folder.is_dir(): |
||||
return None |
||||
|
||||
# List all CubeCLT installations present in tools folder |
||||
CUBECLT_FLDR_RE = re.compile(r"stm32cubeclt_([1-9]).(\d+).(\d+)", re.IGNORECASE) |
||||
installations: list[tuple[int, Path]] = [] |
||||
for f in tools_folder.iterdir(): |
||||
m = CUBECLT_FLDR_RE.match(f.name) |
||||
if m is not None: |
||||
# Compute a number that can be easily compared |
||||
# from the STM32CubeCLT version number |
||||
major, minor, revis = int(m[1]), int(m[2]), int(m[3]) |
||||
ver_num = major * 1000000 + minor * 1000 + revis |
||||
installations.append((ver_num, f)) |
||||
|
||||
if len(installations) == 0: |
||||
return None |
||||
|
||||
# Sort candidates and return the path to the most recent version |
||||
most_recent_install = sorted(installations, key=lambda e: e[0], reverse=True)[0] |
||||
return most_recent_install[1] |
||||
|
||||
cur_platform = platform.system() |
||||
|
||||
# Attempt to find via shutil.which() |
||||
if cur_platform in ["Linux", "Windows"]: |
||||
gdbserv = shutil.which("ST-LINK_gdbserver") |
||||
cubeprg = shutil.which("STM32_Programmer_CLI") |
||||
if gdbserv and cubeprg: |
||||
# Return the parent of cubeprg as [1] should be the path |
||||
# to the folder containing STM32_Programmer_CLI, not the |
||||
# path to the executable itself |
||||
return (Path(gdbserv), Path(cubeprg).parent) |
||||
|
||||
# Search in OS-specific paths |
||||
search_path: str |
||||
tool_suffix = "" |
||||
if cur_platform == "Linux": |
||||
search_path = "/opt/st/" |
||||
elif cur_platform == "Windows": |
||||
search_path = "C:\\ST\\" |
||||
tool_suffix = ".exe" |
||||
elif cur_platform == "Darwin": |
||||
search_path = "/opt/ST/" |
||||
else: |
||||
raise RuntimeError("Unsupported OS") |
||||
|
||||
clt = find_highest_clt_version(Path(search_path)) |
||||
if clt is None: |
||||
raise MissingProgram("ST-LINK_gdbserver (from STM32CubeCLT)") |
||||
|
||||
gdbserver_path = clt / "STLink-gdb-server" / "bin" / f"ST-LINK_gdbserver{tool_suffix}" |
||||
cubeprg_bin_path = clt / "STM32CubeProgrammer" / "bin" |
||||
|
||||
return (gdbserver_path, cubeprg_bin_path) |
||||
|
||||
@classmethod |
||||
def name(cls) -> str: |
||||
return "stlink_gdbserver" |
||||
|
||||
@classmethod |
||||
def capabilities(cls) -> RunnerCaps: |
||||
return RunnerCaps(commands={"attach", "debug", "debugserver"}, dev_id=True, extload=True) |
||||
|
||||
@classmethod |
||||
def extload_help(cls) -> str: |
||||
return "External Loader for ST-Link GDB server" |
||||
|
||||
@classmethod |
||||
def do_add_parser(cls, parser: argparse.ArgumentParser): |
||||
# Expose a subset of the ST-LINK GDB server arguments |
||||
parser.add_argument( |
||||
"--swd", action='store_true', default=True, help="Enable SWD debug mode" |
||||
) |
||||
parser.add_argument("--apid", type=int, default=0, help="Target DAP ID") |
||||
parser.add_argument( |
||||
"--port-number", |
||||
type=int, |
||||
default=STLINK_GDB_SERVER_DEFAULT_PORT, |
||||
help="Port number for GDB client", |
||||
) |
||||
|
||||
@classmethod |
||||
def do_create(cls, cfg: RunnerConfig, args: argparse.Namespace) -> "STLinkGDBServerRunner": |
||||
return STLinkGDBServerRunner( |
||||
cfg, args.swd, args.apid, args.dev_id, args.port_number, args.extload |
||||
) |
||||
|
||||
def __init__( |
||||
self, |
||||
cfg: RunnerConfig, |
||||
swd: bool, |
||||
ap_id: int | None, |
||||
stlink_serial: str | None, |
||||
gdb_port: int, |
||||
external_loader: str | None, |
||||
): |
||||
super().__init__(cfg) |
||||
self.ensure_output('elf') |
||||
|
||||
self._swd = swd |
||||
self._gdb_port = gdb_port |
||||
self._stlink_serial = stlink_serial |
||||
self._ap_id = ap_id |
||||
self._external_loader = external_loader |
||||
|
||||
def do_run(self, command: str, **kwargs): |
||||
if command in ["attach", "debug", "debugserver"]: |
||||
self.do_attach_debug_debugserver(command) |
||||
else: |
||||
raise ValueError(f"{command} not supported") |
||||
|
||||
def do_attach_debug_debugserver(self, command: str): |
||||
# self.ensure_output('elf') is called in constructor |
||||
# and validated that self.cfg.elf_file is non-null. |
||||
# This assertion is required for the test framework, |
||||
# which doesn't have this insight - it should never |
||||
# trigger in real-world scenarios. |
||||
assert self.cfg.elf_file is not None |
||||
elf_path = Path(self.cfg.elf_file).as_posix() |
||||
|
||||
gdb_args = ["-ex", f"target remote :{self._gdb_port}", elf_path] |
||||
|
||||
(gdbserver_path, cubeprg_path) = STLinkGDBServerRunner._get_stm32cubeclt_paths() |
||||
gdbserver_cmd = [gdbserver_path.as_posix()] |
||||
gdbserver_cmd += ["--stm32cubeprogrammer-path", str(cubeprg_path.absolute())] |
||||
gdbserver_cmd += ["--port-number", str(self._gdb_port)] |
||||
gdbserver_cmd += ["--apid", str(self._ap_id)] |
||||
gdbserver_cmd += ["--halt"] |
||||
|
||||
if self._swd: |
||||
gdbserver_cmd.append("--swd") |
||||
|
||||
if command == "attach": |
||||
gdbserver_cmd += ["--attach"] |
||||
else: # debug/debugserver |
||||
gdbserver_cmd += ["--initialize-reset"] |
||||
gdb_args += ["-ex", f"load {elf_path}"] |
||||
|
||||
if self._stlink_serial: |
||||
gdbserver_cmd += ["--serial-number", self._stlink_serial] |
||||
|
||||
if self._external_loader: |
||||
extldr_path = cubeprg_path / "ExternalLoader" / self._external_loader |
||||
if not extldr_path.exists(): |
||||
raise RuntimeError(f"External loader {self._external_loader} does not exist") |
||||
gdbserver_cmd += ["--extload", str(extldr_path)] |
||||
|
||||
self.require(gdbserver_cmd[0]) |
||||
|
||||
if command == "debugserver": |
||||
self.check_call(gdbserver_cmd) |
||||
elif self.cfg.gdb is None: # attach/debug |
||||
raise RuntimeError("GDB is required for attach/debug") |
||||
else: # attach/debug |
||||
gdb_cmd = [self.cfg.gdb] + gdb_args |
||||
self.require(gdb_cmd[0]) |
||||
self.run_server_and_client(gdbserver_cmd, gdb_cmd) |
Loading…
Reference in new issue