1# Copyright (c) 2025 STMicroelectronics 2# 3# SPDX-License-Identifier: Apache-2.0 4 5""" 6Runner for debugging applications using the ST-LINK GDB server 7from STMicroelectronics, provided as part of the STM32CubeCLT. 8""" 9 10import argparse 11import platform 12import re 13import shutil 14from pathlib import Path 15 16from runners.core import MissingProgram, RunnerCaps, RunnerConfig, ZephyrBinaryRunner 17 18STLINK_GDB_SERVER_DEFAULT_PORT = 61234 19 20 21class STLinkGDBServerRunner(ZephyrBinaryRunner): 22 @classmethod 23 def _get_stm32cubeclt_paths(cls) -> tuple[Path, Path]: 24 """ 25 Returns a tuple of two elements of class pathlib.Path: 26 [0]: path to the ST-LINK_gdbserver executable 27 [1]: path to the "STM32CubeProgrammer/bin" folder 28 """ 29 30 def find_highest_clt_version(tools_folder: Path) -> Path | None: 31 if not tools_folder.is_dir(): 32 return None 33 34 # List all CubeCLT installations present in tools folder 35 CUBECLT_FLDR_RE = re.compile(r"stm32cubeclt_([1-9]).(\d+).(\d+)", re.IGNORECASE) 36 installations: list[tuple[int, Path]] = [] 37 for f in tools_folder.iterdir(): 38 m = CUBECLT_FLDR_RE.match(f.name) 39 if m is not None: 40 # Compute a number that can be easily compared 41 # from the STM32CubeCLT version number 42 major, minor, revis = int(m[1]), int(m[2]), int(m[3]) 43 ver_num = major * 1000000 + minor * 1000 + revis 44 installations.append((ver_num, f)) 45 46 if len(installations) == 0: 47 return None 48 49 # Sort candidates and return the path to the most recent version 50 most_recent_install = sorted(installations, key=lambda e: e[0], reverse=True)[0] 51 return most_recent_install[1] 52 53 cur_platform = platform.system() 54 55 # Attempt to find via shutil.which() 56 if cur_platform in ["Linux", "Windows"]: 57 gdbserv = shutil.which("ST-LINK_gdbserver") 58 cubeprg = shutil.which("STM32_Programmer_CLI") 59 if gdbserv and cubeprg: 60 # Return the parent of cubeprg as [1] should be the path 61 # to the folder containing STM32_Programmer_CLI, not the 62 # path to the executable itself 63 return (Path(gdbserv), Path(cubeprg).parent) 64 65 # Search in OS-specific paths 66 search_path: str 67 tool_suffix = "" 68 if cur_platform == "Linux": 69 search_path = "/opt/st/" 70 elif cur_platform == "Windows": 71 search_path = "C:\\ST\\" 72 tool_suffix = ".exe" 73 elif cur_platform == "Darwin": 74 search_path = "/opt/ST/" 75 else: 76 raise RuntimeError("Unsupported OS") 77 78 clt = find_highest_clt_version(Path(search_path)) 79 if clt is None: 80 raise MissingProgram("ST-LINK_gdbserver (from STM32CubeCLT)") 81 82 gdbserver_path = clt / "STLink-gdb-server" / "bin" / f"ST-LINK_gdbserver{tool_suffix}" 83 cubeprg_bin_path = clt / "STM32CubeProgrammer" / "bin" 84 85 return (gdbserver_path, cubeprg_bin_path) 86 87 @classmethod 88 def name(cls) -> str: 89 return "stlink_gdbserver" 90 91 @classmethod 92 def capabilities(cls) -> RunnerCaps: 93 return RunnerCaps(commands={"attach", "debug", "debugserver"}, dev_id=True, extload=True) 94 95 @classmethod 96 def extload_help(cls) -> str: 97 return "External Loader for ST-Link GDB server" 98 99 @classmethod 100 def do_add_parser(cls, parser: argparse.ArgumentParser): 101 # Expose a subset of the ST-LINK GDB server arguments 102 parser.add_argument( 103 "--swd", action='store_true', default=True, help="Enable SWD debug mode" 104 ) 105 parser.add_argument("--apid", type=int, default=0, help="Target DAP ID") 106 parser.add_argument( 107 "--port-number", 108 type=int, 109 default=STLINK_GDB_SERVER_DEFAULT_PORT, 110 help="Port number for GDB client", 111 ) 112 113 @classmethod 114 def do_create(cls, cfg: RunnerConfig, args: argparse.Namespace) -> "STLinkGDBServerRunner": 115 return STLinkGDBServerRunner( 116 cfg, args.swd, args.apid, args.dev_id, args.port_number, args.extload 117 ) 118 119 def __init__( 120 self, 121 cfg: RunnerConfig, 122 swd: bool, 123 ap_id: int | None, 124 stlink_serial: str | None, 125 gdb_port: int, 126 external_loader: str | None, 127 ): 128 super().__init__(cfg) 129 self.ensure_output('elf') 130 131 self._swd = swd 132 self._gdb_port = gdb_port 133 self._stlink_serial = stlink_serial 134 self._ap_id = ap_id 135 self._external_loader = external_loader 136 137 def do_run(self, command: str, **kwargs): 138 if command in ["attach", "debug", "debugserver"]: 139 self.do_attach_debug_debugserver(command) 140 else: 141 raise ValueError(f"{command} not supported") 142 143 def do_attach_debug_debugserver(self, command: str): 144 # self.ensure_output('elf') is called in constructor 145 # and validated that self.cfg.elf_file is non-null. 146 # This assertion is required for the test framework, 147 # which doesn't have this insight - it should never 148 # trigger in real-world scenarios. 149 assert self.cfg.elf_file is not None 150 elf_path = Path(self.cfg.elf_file).as_posix() 151 152 gdb_args = ["-ex", f"target remote :{self._gdb_port}", elf_path] 153 154 (gdbserver_path, cubeprg_path) = STLinkGDBServerRunner._get_stm32cubeclt_paths() 155 gdbserver_cmd = [gdbserver_path.as_posix()] 156 gdbserver_cmd += ["--stm32cubeprogrammer-path", str(cubeprg_path.absolute())] 157 gdbserver_cmd += ["--port-number", str(self._gdb_port)] 158 gdbserver_cmd += ["--apid", str(self._ap_id)] 159 gdbserver_cmd += ["--halt"] 160 161 if self._swd: 162 gdbserver_cmd.append("--swd") 163 164 if command == "attach": 165 gdbserver_cmd += ["--attach"] 166 else: # debug/debugserver 167 gdbserver_cmd += ["--initialize-reset"] 168 gdb_args += ["-ex", f"load {elf_path}"] 169 170 if self._stlink_serial: 171 gdbserver_cmd += ["--serial-number", self._stlink_serial] 172 173 if self._external_loader: 174 extldr_path = cubeprg_path / "ExternalLoader" / self._external_loader 175 if not extldr_path.exists(): 176 raise RuntimeError(f"External loader {self._external_loader} does not exist") 177 gdbserver_cmd += ["--extload", str(extldr_path)] 178 179 self.require(gdbserver_cmd[0]) 180 181 if command == "debugserver": 182 self.check_call(gdbserver_cmd) 183 elif self.cfg.gdb is None: # attach/debug 184 raise RuntimeError("GDB is required for attach/debug") 185 else: # attach/debug 186 gdb_cmd = [self.cfg.gdb] + gdb_args 187 self.require(gdb_cmd[0]) 188 self.run_server_and_client(gdbserver_cmd, gdb_cmd) 189