You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
188 lines
6.8 KiB
188 lines
6.8 KiB
# 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)
|
|
|