Commit e938a5a3 authored by Manuel Argüelles's avatar Manuel Argüelles Committed by Martí Bolívar
Browse files

west: add NXP S32 Debug Probe runner



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: default avatarManuel Argüelles <manuel.arguelles@nxp.com>
parent 1bc53ff8
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
# 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)
+1 −0
Original line number Diff line number Diff line
@@ -45,6 +45,7 @@ _names = [
    'nrfjprog',
    'nrfutil',
    'nsim',
    'nxp_s32dbg',
    'openocd',
    'pyocd',
    'qemu',
+334 −0
Original line number Diff line number Diff line
# 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[:<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 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)
+1 −0
Original line number Diff line number Diff line
@@ -35,6 +35,7 @@ def test_runner_imports():
                    'nios2',
                    'nrfjprog',
                    'nrfutil',
                    'nxp_s32dbg',
                    'openocd',
                    'pyocd',
                    'qemu',