From e938a5a31a9e71da83fda7ff5c4febe2c7134001 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Arg=C3=BCelles?= Date: Tue, 5 Sep 2023 10:43:46 +0700 Subject: [PATCH] west: add NXP S32 Debug Probe runner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The NXP S32 Debug Probe is a JTAG-based probe that enables debugging on NXP S32 devices. This probe is designed to work in conjunction with NXP S32 Design Studio and this runner offers a wrapper to launch a debug session from cli. `flash` command is not implemented at the moment because presently there are no zephyr boards that can make use of it and test it. Signed-off-by: Manuel Argüelles --- boards/common/nxp_s32dbg.board.cmake | 6 + scripts/west_commands/runners/__init__.py | 1 + scripts/west_commands/runners/nxp_s32dbg.py | 334 ++++++++++++++++++++ scripts/west_commands/tests/test_imports.py | 1 + 4 files changed, 342 insertions(+) create mode 100644 boards/common/nxp_s32dbg.board.cmake create mode 100644 scripts/west_commands/runners/nxp_s32dbg.py diff --git a/boards/common/nxp_s32dbg.board.cmake b/boards/common/nxp_s32dbg.board.cmake new file mode 100644 index 00000000000..edd49ea305f --- /dev/null +++ b/boards/common/nxp_s32dbg.board.cmake @@ -0,0 +1,6 @@ +# Copyright 2023 NXP +# SPDX-License-Identifier: Apache-2.0 + +board_set_flasher_ifnset(nxp_s32dbg) +board_set_debugger_ifnset(nxp_s32dbg) +board_finalize_runner_args(nxp_s32dbg) diff --git a/scripts/west_commands/runners/__init__.py b/scripts/west_commands/runners/__init__.py index f4340bc19b0..850efcd5668 100644 --- a/scripts/west_commands/runners/__init__.py +++ b/scripts/west_commands/runners/__init__.py @@ -45,6 +45,7 @@ _names = [ 'nrfjprog', 'nrfutil', 'nsim', + 'nxp_s32dbg', 'openocd', 'pyocd', 'qemu', diff --git a/scripts/west_commands/runners/nxp_s32dbg.py b/scripts/west_commands/runners/nxp_s32dbg.py new file mode 100644 index 00000000000..e2065f3980f --- /dev/null +++ b/scripts/west_commands/runners/nxp_s32dbg.py @@ -0,0 +1,334 @@ +# Copyright 2023 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 typing import Dict, List, Optional, Union + +from runners.core import (BuildConfiguration, RunnerCaps, RunnerConfig, + ZephyrBinaryRunner) + +NXP_S32DBG_USB_CLASS = 'NXP Probes' +NXP_S32DBG_USB_VID = 0x15a2 +NXP_S32DBG_USB_PID = 0x0067 + + +@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: Optional[str] = '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: Optional[str] = None, + tool_opt: Optional[List[str]] = None) -> None: + super(NXPS32DebugProbeRunner, self).__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: Optional[str] = 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[:
]" + where
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 priviledged permissions to access the device info + macaddr_pattern = r'(?:[0-9a-f]{2}[:]){5}[0-9a-f]{2}' + if platform.system() == 'Windows': + cmd = f'pnputil /enum-devices /connected /class "{NXP_S32DBG_USB_CLASS}"' + serialid_pattern = f'instance id: +usb\\\\.*\\\\({macaddr_pattern})' + 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: + raise RuntimeError('error while looking for debug probes connected') + + 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) -> Optional[Dict[str, str]]: + """Execution environment used for the client process.""" + if platform.system() == 'Windows': + python_lib = (self.s32ds_path / 'S32DS' / 'build_tools' / 'msys32' + / 'mingw32' / 'lib' / 'python2.7') + return { + **os.environ, + 'PYTHONPATH': f'{python_lib}{os.pathsep}{python_lib / "site-packages"}' + } + + return None + + @property + def script_globals(self) -> Dict[str, Optional[Union[str, int]]]: + """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) diff --git a/scripts/west_commands/tests/test_imports.py b/scripts/west_commands/tests/test_imports.py index 774d4f7d722..274840f8cbf 100644 --- a/scripts/west_commands/tests/test_imports.py +++ b/scripts/west_commands/tests/test_imports.py @@ -35,6 +35,7 @@ def test_runner_imports(): 'nios2', 'nrfjprog', 'nrfutil', + 'nxp_s32dbg', 'openocd', 'pyocd', 'qemu',