|
| 1 | +# Copyright (c) 2025 STMicroelectronics |
| 2 | +# |
| 3 | +# SPDX-License-Identifier: Apache-2.0 |
| 4 | + |
| 5 | +""" |
| 6 | +Runner for debugging applications using the ST-LINK GDB server |
| 7 | +from STMicroelectronics, provided as part of the STM32CubeCLT. |
| 8 | +""" |
| 9 | + |
| 10 | +import argparse |
| 11 | +import platform |
| 12 | +import re |
| 13 | +import shutil |
| 14 | +from pathlib import Path |
| 15 | + |
| 16 | +from runners.core import MissingProgram, RunnerCaps, RunnerConfig, ZephyrBinaryRunner |
| 17 | + |
| 18 | +STLINK_GDB_SERVER_DEFAULT_PORT = 61234 |
| 19 | + |
| 20 | + |
| 21 | +class 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) |
0 commit comments