blob: e2065f3980f99e75af7bde1636b2cf23d5341f40 [file] [log] [blame]
# 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)