You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
334 lines
14 KiB
334 lines
14 KiB
# 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)
|
|
|