Primary Git Repository for the Zephyr Project. Zephyr is a new generation, scalable, optimized, secure RTOS for multiple hardware architectures.
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

# 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)