|  | # Copyright 2023, 2025 NXP | 
|  | # SPDX-License-Identifier: Apache-2.0 | 
|  | """ | 
|  | Runner for NXP S32 Debug Probe. | 
|  | """ | 
|  |  | 
|  | import argparse | 
|  | import os | 
|  | import platform | 
|  | import re | 
|  | import shlex | 
|  | import subprocess | 
|  | import sys | 
|  | import tempfile | 
|  | from dataclasses import dataclass | 
|  | from pathlib import Path | 
|  |  | 
|  | from runners.core import BuildConfiguration, RunnerCaps, RunnerConfig, ZephyrBinaryRunner | 
|  |  | 
|  | NXP_S32DBG_USB_VID = 0x1FC9 | 
|  | NXP_S32DBG_USB_PID = 0x014D | 
|  |  | 
|  | @dataclass | 
|  | class NXPS32DebugProbeConfig: | 
|  | """NXP S32 Debug Probe configuration parameters.""" | 
|  | conn_str: str = 's32dbg' | 
|  | server_port: int = 45000 | 
|  | speed: int = 16000 | 
|  | remote_timeout: int = 30 | 
|  | reset_type: str | None = 'default' | 
|  | reset_delay: int = 0 | 
|  |  | 
|  |  | 
|  | class NXPS32DebugProbeRunner(ZephyrBinaryRunner): | 
|  | """Runner front-end for NXP S32 Debug Probe.""" | 
|  |  | 
|  | def __init__(self, | 
|  | runner_cfg: RunnerConfig, | 
|  | probe_cfg: NXPS32DebugProbeConfig, | 
|  | core_name: str, | 
|  | soc_name: str, | 
|  | soc_family_name: str, | 
|  | start_all_cores: bool, | 
|  | s32ds_path: str | None = None, | 
|  | tool_opt: list[str] | None = None) -> None: | 
|  | super().__init__(runner_cfg) | 
|  | self.elf_file: str = runner_cfg.elf_file or '' | 
|  | self.probe_cfg: NXPS32DebugProbeConfig = probe_cfg | 
|  | self.core_name: str = core_name | 
|  | self.soc_name: str = soc_name | 
|  | self.soc_family_name: str = soc_family_name | 
|  | self.start_all_cores: bool = start_all_cores | 
|  | self.s32ds_path_override: str | None = s32ds_path | 
|  |  | 
|  | self.tool_opt: list[str] = [] | 
|  | if tool_opt: | 
|  | for opt in tool_opt: | 
|  | self.tool_opt.extend(shlex.split(opt)) | 
|  |  | 
|  | build_cfg = BuildConfiguration(runner_cfg.build_dir) | 
|  | self.arch = build_cfg.get('CONFIG_ARCH').replace('"', '') | 
|  |  | 
|  | @classmethod | 
|  | def name(cls) -> str: | 
|  | return 'nxp_s32dbg' | 
|  |  | 
|  | @classmethod | 
|  | def capabilities(cls) -> RunnerCaps: | 
|  | return RunnerCaps(commands={'debug', 'debugserver', 'attach'}, | 
|  | dev_id=True, tool_opt=True) | 
|  |  | 
|  | @classmethod | 
|  | def dev_id_help(cls) -> str: | 
|  | return '''Debug probe connection string as in "s32dbg[:<address>]" | 
|  | where <address> can be the IP address if TAP is available via Ethernet, | 
|  | the serial ID of the probe or empty if TAP is available via USB.''' | 
|  |  | 
|  | @classmethod | 
|  | def tool_opt_help(cls) -> str: | 
|  | return '''Additional options for GDB client when used with "debug" or "attach" commands | 
|  | or for GTA server when used with "debugserver" command.''' | 
|  |  | 
|  | @classmethod | 
|  | def do_add_parser(cls, parser: argparse.ArgumentParser) -> None: | 
|  | parser.add_argument('--core-name', | 
|  | required=True, | 
|  | help='Core name as supported by the debug probe (e.g. "R52_0_0")') | 
|  | parser.add_argument('--soc-name', | 
|  | required=True, | 
|  | help='SoC name as supported by the debug probe (e.g. "S32Z270")') | 
|  | parser.add_argument('--soc-family-name', | 
|  | required=True, | 
|  | help='SoC family name as supported by the debug probe (e.g. "s32z2e2")') | 
|  | parser.add_argument('--start-all-cores', | 
|  | action='store_true', | 
|  | help='Start all SoC cores and not just the one being debugged. ' | 
|  | 'Use together with "debug" command.') | 
|  | parser.add_argument('--s32ds-path', | 
|  | help='Override the path to NXP S32 Design Studio installation. ' | 
|  | 'By default, this runner will try to obtain it from the system ' | 
|  | 'path, if available.') | 
|  | parser.add_argument('--server-port', | 
|  | default=NXPS32DebugProbeConfig.server_port, | 
|  | type=int, | 
|  | help='GTA server port') | 
|  | parser.add_argument('--speed', | 
|  | default=NXPS32DebugProbeConfig.speed, | 
|  | type=int, | 
|  | help='JTAG interface speed') | 
|  | parser.add_argument('--remote-timeout', | 
|  | default=NXPS32DebugProbeConfig.remote_timeout, | 
|  | type=int, | 
|  | help='Number of seconds to wait for the remote target responses') | 
|  |  | 
|  | @classmethod | 
|  | def do_create(cls, cfg: RunnerConfig, args: argparse.Namespace) -> 'NXPS32DebugProbeRunner': | 
|  | probe_cfg = NXPS32DebugProbeConfig(args.dev_id, | 
|  | server_port=args.server_port, | 
|  | speed=args.speed, | 
|  | remote_timeout=args.remote_timeout) | 
|  |  | 
|  | return NXPS32DebugProbeRunner(cfg, probe_cfg, args.core_name, args.soc_name, | 
|  | args.soc_family_name, args.start_all_cores, | 
|  | s32ds_path=args.s32ds_path, tool_opt=args.tool_opt) | 
|  |  | 
|  | @staticmethod | 
|  | def find_usb_probes() -> list[str]: | 
|  | """Return a list of debug probe serial numbers connected via USB to this host.""" | 
|  | # use system's native commands to enumerate and retrieve the USB serial ID | 
|  | # to avoid bloating this runner with third-party dependencies that often | 
|  | # require privileged permissions to access the device info | 
|  | macaddr_pattern = r'(?:[0-9a-f]{2}[:]){5}[0-9a-f]{2}' | 
|  | if platform.system() == 'Windows': | 
|  | cmd = 'pnputil /enum-devices /connected' | 
|  | serialid_pattern = 'instance id: +usb\\\\' \ | 
|  | f'VID_{NXP_S32DBG_USB_VID:04X}&PID_{NXP_S32DBG_USB_PID:04X}\\\\' \ | 
|  | f'({macaddr_pattern})'.lower() | 
|  | else: | 
|  | cmd = f'lsusb -v -d {NXP_S32DBG_USB_VID:x}:{NXP_S32DBG_USB_PID:x}' | 
|  | serialid_pattern = f'iserial +.*({macaddr_pattern})' | 
|  |  | 
|  | try: | 
|  | outb = subprocess.check_output(shlex.split(cmd), stderr=subprocess.DEVNULL) | 
|  | out = outb.decode('utf-8').strip().lower() | 
|  | except subprocess.CalledProcessError as err: | 
|  | raise RuntimeError('error while looking for debug probes connected') from err | 
|  |  | 
|  | devices: list[str] = [] | 
|  | if out and 'no devices were found' not in out: | 
|  | devices = re.findall(serialid_pattern, out) | 
|  |  | 
|  | return sorted(devices) | 
|  |  | 
|  | @classmethod | 
|  | def select_probe(cls) -> str: | 
|  | """ | 
|  | Find debugger probes connected and return the serial number of the one selected. | 
|  |  | 
|  | If there are multiple debugger probes connected and this runner is being executed | 
|  | in a interactive prompt, ask the user to select one of the probes. | 
|  | """ | 
|  | probes_snr = cls.find_usb_probes() | 
|  | if not probes_snr: | 
|  | raise RuntimeError('there are no debug probes connected') | 
|  | elif len(probes_snr) == 1: | 
|  | return probes_snr[0] | 
|  | else: | 
|  | if not sys.stdin.isatty(): | 
|  | raise RuntimeError( | 
|  | f'refusing to guess which of {len(probes_snr)} connected probes to use ' | 
|  | '(Interactive prompts disabled since standard input is not a terminal). ' | 
|  | 'Please specify a device ID on the command line.') | 
|  |  | 
|  | print('There are multiple debug probes connected') | 
|  | for i, probe in enumerate(probes_snr, 1): | 
|  | print(f'{i}. {probe}') | 
|  |  | 
|  | prompt = f'Please select one with desired serial number (1-{len(probes_snr)}): ' | 
|  | while True: | 
|  | try: | 
|  | value: int = int(input(prompt)) | 
|  | except EOFError: | 
|  | sys.exit(0) | 
|  | except ValueError: | 
|  | continue | 
|  | if 1 <= value <= len(probes_snr): | 
|  | break | 
|  | return probes_snr[value - 1] | 
|  |  | 
|  | @property | 
|  | def runtime_environment(self) -> dict[str, str] | None: | 
|  | """Execution environment used for the client process.""" | 
|  | if platform.system() == 'Windows': | 
|  | python_lib = (self.s32ds_path / 'S32DS' / 'build_tools' / 'msys32' | 
|  | / 'mingw64' / 'lib' / 'python3.10') | 
|  | return { | 
|  | **os.environ, | 
|  | 'PYTHONPATH': f'{python_lib}{os.pathsep}{python_lib / "site-packages"}{os.pathsep}' | 
|  | f'{python_lib / "lib-dynload"}' | 
|  | } | 
|  |  | 
|  | return None | 
|  |  | 
|  | @property | 
|  | def script_globals(self) -> dict[str, str | int | None]: | 
|  | """Global variables required by the debugger scripts.""" | 
|  | return { | 
|  | '_PROBE_IP': self.probe_cfg.conn_str, | 
|  | '_JTAG_SPEED': self.probe_cfg.speed, | 
|  | '_GDB_SERVER_PORT': self.probe_cfg.server_port, | 
|  | '_RESET_TYPE': self.probe_cfg.reset_type, | 
|  | '_RESET_DELAY': self.probe_cfg.reset_delay, | 
|  | '_REMOTE_TIMEOUT': self.probe_cfg.remote_timeout, | 
|  | '_CORE_NAME': f'{self.soc_name}_{self.core_name}', | 
|  | '_SOC_NAME': self.soc_name, | 
|  | '_IS_LOGGING_ENABLED': False, | 
|  | '_FLASH_NAME': None,    # not supported | 
|  | '_SECURE_TYPE': None,   # not supported | 
|  | '_SECURE_KEY': None,    # not supported | 
|  | } | 
|  |  | 
|  | def server_commands(self) -> list[str]: | 
|  | """Get launch commands to start the GTA server.""" | 
|  | server_exec = str(self.s32ds_path / 'S32DS' / 'tools' / 'S32Debugger' | 
|  | / 'Debugger' / 'Server' / 'gta' / 'gta') | 
|  | cmd = [server_exec, '-p', str(self.probe_cfg.server_port)] | 
|  | return cmd | 
|  |  | 
|  | def client_commands(self) -> list[str]: | 
|  | """Get launch commands to start the GDB client.""" | 
|  | if self.arch == 'arm': | 
|  | client_exec_name = 'arm-none-eabi-gdb-py' | 
|  | elif self.arch == 'arm64': | 
|  | client_exec_name = 'aarch64-none-elf-gdb-py' | 
|  | else: | 
|  | raise RuntimeError(f'architecture {self.arch} not supported') | 
|  |  | 
|  | client_exec = str(self.s32ds_path / 'S32DS' / 'tools' / 'gdb-arm' | 
|  | / 'arm32-eabi' / 'bin' / client_exec_name) | 
|  | cmd = [client_exec] | 
|  | return cmd | 
|  |  | 
|  | def get_script(self, name: str) -> Path: | 
|  | """ | 
|  | Get the file path of a debugger script with the given name. | 
|  |  | 
|  | :param name: name of the script, without the SoC family name prefix | 
|  | :returns: path to the script | 
|  | :raises RuntimeError: if file does not exist | 
|  | """ | 
|  | script = (self.s32ds_path / 'S32DS' / 'tools' / 'S32Debugger' / 'Debugger' / 'scripts' | 
|  | / self.soc_family_name / f'{self.soc_family_name}_{name}.py') | 
|  | if not script.exists(): | 
|  | raise RuntimeError(f'script not found: {script}') | 
|  | return script | 
|  |  | 
|  | def do_run(self, command: str, **kwargs) -> None: | 
|  | """ | 
|  | Execute the given command. | 
|  |  | 
|  | :param command: command name to execute | 
|  | :raises RuntimeError: if target architecture or host OS is not supported | 
|  | :raises MissingProgram: if required tools are not found in the host | 
|  | """ | 
|  | if platform.system() not in ('Windows', 'Linux'): | 
|  | raise RuntimeError(f'runner not supported on {platform.system()} systems') | 
|  |  | 
|  | if self.arch not in ('arm', 'arm64'): | 
|  | raise RuntimeError(f'architecture {self.arch} not supported') | 
|  |  | 
|  | app_name = 's32ds' if platform.system() == 'Windows' else 's32ds.sh' | 
|  | self.s32ds_path = Path(self.require(app_name, path=self.s32ds_path_override)).parent | 
|  |  | 
|  | if not self.probe_cfg.conn_str: | 
|  | self.probe_cfg.conn_str = f's32dbg:{self.select_probe()}' | 
|  | self.logger.info(f'using debug probe {self.probe_cfg.conn_str}') | 
|  |  | 
|  | if command in ('attach', 'debug'): | 
|  | self.ensure_output('elf') | 
|  | self.do_attach_debug(command, **kwargs) | 
|  | else: | 
|  | self.do_debugserver(**kwargs) | 
|  |  | 
|  | def do_attach_debug(self, command: str, **kwargs) -> None: | 
|  | """ | 
|  | Launch the GTA server and GDB client to start a debugging session. | 
|  |  | 
|  | :param command: command name to execute | 
|  | """ | 
|  | gdb_script: list[str] = [] | 
|  |  | 
|  | # setup global variables required for the scripts before sourcing them | 
|  | for name, val in self.script_globals.items(): | 
|  | gdb_script.append(f'py {name} = {repr(val)}') | 
|  |  | 
|  | # load platform-specific debugger script | 
|  | if command == 'debug': | 
|  | if self.start_all_cores: | 
|  | startup_script = self.get_script('generic_bareboard_all_cores') | 
|  | else: | 
|  | startup_script = self.get_script('generic_bareboard') | 
|  | else: | 
|  | startup_script = self.get_script('attach') | 
|  | gdb_script.append(f'source {startup_script}') | 
|  |  | 
|  | # executes the SoC and board initialization sequence | 
|  | if command == 'debug': | 
|  | gdb_script.append('py board_init()') | 
|  |  | 
|  | # initializes the debugger connection to the core specified | 
|  | gdb_script.append('py core_init()') | 
|  |  | 
|  | gdb_script.append(f'file {Path(self.elf_file).as_posix()}') | 
|  | if command == 'debug': | 
|  | gdb_script.append('load') | 
|  |  | 
|  | with tempfile.TemporaryDirectory(suffix='nxp_s32dbg') as tmpdir: | 
|  | gdb_cmds = Path(tmpdir) / 'runner.nxp_s32dbg' | 
|  | gdb_cmds.write_text('\n'.join(gdb_script), encoding='utf-8') | 
|  | self.logger.debug(gdb_cmds.read_text(encoding='utf-8')) | 
|  |  | 
|  | server_cmd = self.server_commands() | 
|  | client_cmd = self.client_commands() | 
|  | client_cmd.extend(['-x', gdb_cmds.as_posix()]) | 
|  | client_cmd.extend(self.tool_opt) | 
|  |  | 
|  | self.run_server_and_client(server_cmd, client_cmd, env=self.runtime_environment) | 
|  |  | 
|  | def do_debugserver(self, **kwargs) -> None: | 
|  | """Start the GTA server on a given port with the given extra parameters from cli.""" | 
|  | server_cmd = self.server_commands() | 
|  | server_cmd.extend(self.tool_opt) | 
|  | self.check_call(server_cmd) |