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>
189 lines
6.8 KiB
Python
189 lines
6.8 KiB
Python
# 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)
|