1# Copyright (c) 2017 Linaro Limited.
2#
3# SPDX-License-Identifier: Apache-2.0
4
5'''Runner for debugging with J-Link.'''
6
7import argparse
8import ipaddress
9import logging
10import os
11import re
12import shlex
13import socket
14import subprocess
15import sys
16import tempfile
17import time
18from pathlib import Path
19
20from runners.core import FileType, RunnerCaps, ZephyrBinaryRunner
21
22try:
23    import pylink
24    from pylink.library import Library
25    MISSING_REQUIREMENTS = False
26except ImportError:
27    MISSING_REQUIREMENTS = True
28
29# Populated in do_add_parser()
30DEFAULT_JLINK_EXE = None
31DEFAULT_JLINK_GDB_PORT = 2331
32DEFAULT_JLINK_RTT_PORT = 19021
33
34def is_ip(ip):
35    if not ip:
36        return False
37    try:
38        ipaddress.ip_address(ip.split(':')[0])
39    except ValueError:
40        return False
41    return True
42
43def is_tunnel(tunnel):
44    return tunnel.startswith("tunnel:") if tunnel else False
45
46class ToggleAction(argparse.Action):
47
48    def __call__(self, parser, args, ignored, option):
49        setattr(args, self.dest, not option.startswith('--no-'))
50
51class JLinkBinaryRunner(ZephyrBinaryRunner):
52    '''Runner front-end for the J-Link GDB server.'''
53
54    def __init__(self, cfg, device, dev_id=None,
55                 commander=DEFAULT_JLINK_EXE,
56                 dt_flash=True, erase=True, reset=False,
57                 iface='swd', speed='auto', flash_script = None,
58                 loader=None, flash_sram=False,
59                 gdbserver='JLinkGDBServer',
60                 gdb_host='',
61                 gdb_port=DEFAULT_JLINK_GDB_PORT,
62                 rtt_port=DEFAULT_JLINK_RTT_PORT,
63                 tui=False, tool_opt=None, dev_id_type=None):
64        super().__init__(cfg)
65        self.file = cfg.file
66        self.file_type = cfg.file_type
67        self.hex_name = cfg.hex_file
68        self.bin_name = cfg.bin_file
69        self.elf_name = cfg.elf_file
70        self.mot_name = cfg.mot_file
71        self.gdb_cmd = [cfg.gdb] if cfg.gdb else None
72        self.device = device
73        self.dev_id = dev_id
74        self.commander = commander
75        self.flash_script = flash_script
76        self.dt_flash = dt_flash
77        self.flash_sram = flash_sram
78        self.erase = erase
79        self.reset = reset
80        self.gdbserver = gdbserver
81        self.iface = iface
82        self.speed = speed
83        self.gdb_host = gdb_host
84        self.gdb_port = gdb_port
85        self.tui_arg = ['-tui'] if tui else []
86        self.loader = loader
87        self.rtt_port = rtt_port
88        self.dev_id_type = dev_id_type
89
90        self.tool_opt = []
91        if tool_opt is not None:
92            for opts in [shlex.split(opt) for opt in tool_opt]:
93                self.tool_opt += opts
94
95    @staticmethod
96    def _detect_dev_id_type(dev_id):
97        """Detect device type based on dev_id pattern."""
98        if not dev_id:
99            return None
100
101        # Check if dev_id is numeric (serialno)
102        if re.match(r'^[0-9]+$', dev_id):
103            return 'serialno'
104
105        # Check if dev_id is an existing file (tty)
106        if os.path.exists(dev_id):
107            return 'tty'
108
109        # Check if dev_id is a valid IPv4
110        if is_ip(dev_id):
111            return 'ip'
112
113        # Check if dev_id is a tunnel
114        if is_tunnel(dev_id):
115            return 'tunnel'
116
117        # No match found
118        return None
119
120    @classmethod
121    def name(cls):
122        return 'jlink'
123
124    @classmethod
125    def capabilities(cls):
126        return RunnerCaps(commands={'flash', 'debug', 'debugserver', 'attach', 'rtt'},
127                          dev_id=True, flash_addr=True, erase=True, reset=True,
128                          tool_opt=True, file=True, rtt=True)
129
130    @classmethod
131    def dev_id_help(cls) -> str:
132        return '''Device identifier. Can be either a serial number (for a USB connection), a path
133                  to a tty device, an IP address or a tunnel address. User can enforce the type of
134                  the identifier with --dev-id-type.
135                  If not specified, the first USB device detected is used. '''
136
137    @classmethod
138    def tool_opt_help(cls) -> str:
139        return "Additional options for JLink Commander, e.g. '-autoconnect 1'"
140
141    @staticmethod
142    def default_jlink():
143        global DEFAULT_JLINK_EXE
144
145        if sys.platform == 'win32':
146            # JLink.exe can collide with the JDK executable of the same name
147            # Locate the executable using the registry
148            try:
149                import winreg
150
151                # Note that when multiple JLink versions are installed on the
152                # machine this points to the one that was installed
153                # last, and not to the latest version.
154                key = winreg.OpenKeyEx(
155                    winreg.HKEY_CURRENT_USER, r"Software\SEGGER\J-Link")
156                DEFAULT_JLINK_EXE = (
157                    Path(winreg.QueryValueEx(key, "InstallPath")[0])
158                    / "JLink.exe")
159            except Exception:
160                # Not found via the registry, hope that $PATH is correct
161                DEFAULT_JLINK_EXE = "JLink.exe"
162        else:
163            DEFAULT_JLINK_EXE = "JLinkExe"
164
165    @classmethod
166    def do_add_parser(cls, parser):
167
168        # Find the default JLink executable
169        cls.default_jlink()
170
171        # Required:
172        parser.add_argument('--device', required=True, help='device name')
173
174        # Optional:
175        parser.add_argument('--loader', required=False, dest='loader',
176                            help='specifies a loader type')
177        parser.add_argument('--id', required=False, dest='dev_id',
178                            help='obsolete synonym for -i/--dev-id')
179        parser.add_argument('--iface', default='swd',
180                            help='interface to use, default is swd')
181        parser.add_argument('--speed', default='auto',
182                            help='interface speed, default is autodetect')
183        parser.add_argument('--flash-script', default=None,
184                            help='Custom flashing script, default is None')
185        parser.add_argument('--tui', default=False, action='store_true',
186                            help='if given, GDB uses -tui')
187        parser.add_argument('--gdbserver', default='JLinkGDBServer',
188                            help='GDB server, default is JLinkGDBServer')
189        parser.add_argument('--gdb-host', default='',
190                            help='custom gdb host, defaults to the empty string '
191                            'and runs a gdb server')
192        parser.add_argument('--gdb-port', default=DEFAULT_JLINK_GDB_PORT,
193                            help=f'pyocd gdb port, defaults to {DEFAULT_JLINK_GDB_PORT}')
194        parser.add_argument('--commander', default=DEFAULT_JLINK_EXE,
195                            help=f'''J-Link Commander, default is
196                            {DEFAULT_JLINK_EXE}''')
197        parser.add_argument('--reset-after-load', '--no-reset-after-load',
198                            dest='reset', nargs=0,
199                            action=ToggleAction,
200                            help='obsolete synonym for --reset/--no-reset')
201        parser.add_argument('--rtt-client', default='JLinkRTTClient',
202                            help='RTT client, default is JLinkRTTClient')
203        parser.add_argument('--rtt-port', default=DEFAULT_JLINK_RTT_PORT,
204                            help=f'jlink rtt port, defaults to {DEFAULT_JLINK_RTT_PORT}')
205        parser.add_argument('--flash-sram', default=False, action='store_true',
206                            help='if given, flashing the image to SRAM and '
207                            'modify PC register to be SRAM base address')
208        parser.add_argument('--dev-id-type', choices=['auto', 'serialno', 'tty', 'ip', 'tunnel'],
209                            default='auto', help='Device type. "auto" (default) auto-detects '
210                            'the type, or specify explicitly')
211
212        parser.set_defaults(reset=False)
213
214    @classmethod
215    def do_create(cls, cfg, args):
216        return JLinkBinaryRunner(cfg, args.device,
217                                 dev_id=args.dev_id,
218                                 commander=args.commander,
219                                 dt_flash=args.dt_flash,
220                                 flash_sram=args.flash_sram,
221                                 erase=args.erase,
222                                 reset=args.reset,
223                                 iface=args.iface, speed=args.speed,
224                                 flash_script=args.flash_script,
225                                 gdbserver=args.gdbserver,
226                                 loader=args.loader,
227                                 gdb_host=args.gdb_host,
228                                 gdb_port=args.gdb_port,
229                                 rtt_port=args.rtt_port,
230                                 tui=args.tui, tool_opt=args.tool_opt,
231                                 dev_id_type=args.dev_id_type)
232
233    def print_gdbserver_message(self):
234        if not self.thread_info_enabled:
235            thread_msg = '; no thread info available'
236        elif self.supports_thread_info:
237            thread_msg = '; thread info enabled'
238        else:
239            thread_msg = '; update J-Link software for thread info'
240        self.logger.info('J-Link GDB server running on port '
241                         f'{self.gdb_port}{thread_msg}')
242
243    def print_rttserver_message(self):
244        self.logger.info(f'J-Link RTT server running on port {self.rtt_port}')
245
246    @property
247    def jlink_version(self):
248        # Get the J-Link version as a (major, minor, rev) tuple of integers.
249        #
250        # J-Link's command line tools provide neither a standalone
251        # "--version" nor help output that contains the version. Hack
252        # around this deficiency by using the third-party pylink library
253        # to load the shared library distributed with the tools, which
254        # provides an API call for getting the version.
255        if not hasattr(self, '_jlink_version'):
256            # pylink 0.14.0/0.14.1 exposes JLink SDK DLL (libjlinkarm) in
257            # JLINK_SDK_STARTS_WITH, while other versions use JLINK_SDK_NAME
258            if pylink.__version__ in ('0.14.0', '0.14.1'):
259                sdk = Library.JLINK_SDK_STARTS_WITH
260            else:
261                sdk = Library.JLINK_SDK_NAME
262
263            plat = sys.platform
264            if plat.startswith('win32'):
265                libname = Library.get_appropriate_windows_sdk_name() + '.dll'
266            elif plat.startswith('linux'):
267                libname = sdk + '.so'
268            elif plat.startswith('darwin'):
269                libname = sdk + '.dylib'
270            else:
271                self.logger.warning(f'unknown platform {plat}; assuming UNIX')
272                libname = sdk + '.so'
273
274            lib = Library(dllpath=os.fspath(Path(self.commander).parent /
275                                            libname))
276            version = int(lib.dll().JLINKARM_GetDLLVersion())
277            self.logger.debug('JLINKARM_GetDLLVersion()=%s', version)
278            # The return value is an int with 2 decimal digits per
279            # version subfield.
280            self._jlink_version = (version // 10000,
281                                   (version // 100) % 100,
282                                   version % 100)
283
284        return self._jlink_version
285
286    @property
287    def jlink_version_str(self):
288        # Converts the numeric revision tuple to something human-readable.
289        if not hasattr(self, '_jlink_version_str'):
290            major, minor, rev = self.jlink_version
291            rev_str = chr(ord('a') + rev - 1) if rev else ''
292            self._jlink_version_str = f'{major}.{minor:02}{rev_str}'
293        return self._jlink_version_str
294
295    @property
296    def supports_nogui(self):
297        # -nogui was introduced in J-Link Commander v6.80
298        return self.jlink_version >= (6, 80, 0)
299
300    @property
301    def supports_thread_info(self):
302        # RTOSPlugin_Zephyr was introduced in 7.11b
303        return self.jlink_version >= (7, 11, 2)
304
305    @property
306    def supports_loader(self):
307        return self.jlink_version >= (7, 70, 4)
308
309    def do_run(self, command, **kwargs):
310
311        if MISSING_REQUIREMENTS:
312            raise RuntimeError('one or more Python dependencies were missing; '
313                               "see the getting started guide for details on "
314                               "how to fix")
315        # Convert commander to a real absolute path. We need this to
316        # be able to find the shared library that tells us what
317        # version of the tools we're using.
318        self.commander = os.fspath(
319            Path(self.require(self.commander)).resolve())
320        self.logger.debug(f'JLink executable: {self.commander}')
321        self.logger.info(f'JLink version: {self.jlink_version_str}')
322
323        rtos = self.thread_info_enabled and self.supports_thread_info
324        plugin_dir = os.fspath(Path(self.commander).parent / 'GDBServer' /
325                               'RTOSPlugin_Zephyr')
326        big_endian = self.build_conf.getboolean('CONFIG_BIG_ENDIAN')
327
328        # Determine device type for GDB server
329        if self.dev_id:
330            if self.dev_id_type == 'auto':
331                # Auto-detect device type
332                detected_type = self._detect_dev_id_type(self.dev_id)
333            else:
334                # Use manually specified device type
335                detected_type = self.dev_id_type
336
337            # Build device selection for GDB server
338            if detected_type == 'serialno':
339                device_select = f'usb={self.dev_id}'
340            elif detected_type == 'tty':
341                device_select = f'tty={self.dev_id}'
342            elif detected_type == 'ip':
343                device_select = f'ip={self.dev_id}'
344            elif detected_type == 'tunnel':
345                device_select = f'tunnel={self.dev_id}'
346            else:
347                # Fallback to legacy behavior (usb)
348                device_select = f'usb={self.dev_id}'
349                self.logger.warning(f'"{self.dev_id}" does not match any known pattern')
350        else:
351            device_select = 'usb'
352
353        server_cmd = (
354            [self.gdbserver]
355            + ['-select', device_select]
356            + ['-port', str(self.gdb_port)]
357            + ['-if', self.iface]
358            + ['-speed', self.speed]
359            + ['-device', self.device]
360            + ['-silent']
361            + ['-endian', 'big' if big_endian else 'little']
362            + ['-singlerun']
363            + (['-nogui'] if self.supports_nogui else [])
364            + (['-rtos', plugin_dir] if rtos else [])
365            + ['-rtttelnetport', str(self.rtt_port)]
366            + self.tool_opt
367        )
368
369        if command == 'flash':
370            self.flash(**kwargs)
371        elif command == 'debugserver':
372            if self.gdb_host:
373                raise ValueError('Cannot run debugserver with --gdb-host')
374            self.require(self.gdbserver)
375            self.print_gdbserver_message()
376            self.check_call(server_cmd)
377        elif command == 'rtt':
378            self.print_gdbserver_message()
379            self.print_rttserver_message()
380            server_cmd += ['-nohalt']
381            server_proc = self.popen_ignore_int(server_cmd)
382            try:
383                sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
384                # wait for the port to be open
385                while server_proc.poll() is None:
386                    try:
387                        sock.connect(('localhost', self.rtt_port))
388                        break
389                    except ConnectionRefusedError:
390                        time.sleep(0.1)
391                self.run_telnet_client('localhost', self.rtt_port, sock)
392            except Exception as e:
393                self.logger.error(e)
394            finally:
395                server_proc.terminate()
396                server_proc.wait()
397        else:
398            if self.gdb_cmd is None:
399                raise ValueError('Cannot debug; gdb is missing')
400            if self.file is not None:
401                if self.file_type != FileType.ELF:
402                    raise ValueError('Cannot debug; elf file required')
403                elf_name = self.file
404            elif self.elf_name is None:
405                raise ValueError('Cannot debug; elf is missing')
406            else:
407                elf_name = self.elf_name
408            client_cmd = (self.gdb_cmd +
409                          self.tui_arg +
410                          [elf_name] +
411                          ['-ex', f'target remote {self.gdb_host}:{self.gdb_port}'])
412            if command == 'debug':
413                client_cmd += ['-ex', 'monitor halt',
414                               '-ex', 'monitor reset',
415                               '-ex', 'load']
416                if self.reset:
417                    client_cmd += ['-ex', 'monitor reset']
418            if not self.gdb_host:
419                self.require(self.gdbserver)
420                self.print_gdbserver_message()
421                self.run_server_and_client(server_cmd, client_cmd)
422            else:
423                self.run_client(client_cmd)
424
425    def get_default_flash_commands(self):
426        lines = [
427            'ExitOnError 1',  # Treat any command-error as fatal
428            'r',  # Reset and halt the target
429            'BE' if self.build_conf.getboolean('CONFIG_BIG_ENDIAN') else 'LE'
430        ]
431
432        if self.erase:
433            lines.append('erase') # Erase all flash sectors
434
435        # Get the build artifact to flash
436        if self.file is not None:
437            # use file provided by the user
438            if not os.path.isfile(self.file):
439                err = 'Cannot flash; file ({}) not found'
440                raise ValueError(err.format(self.file))
441
442            flash_file = self.file
443
444            if self.file_type == FileType.HEX:
445                flash_cmd = f'loadfile "{self.file}"'
446            elif self.file_type == (FileType.BIN or FileType.MOT):
447                if self.flash_sram:
448                    flash_addr = self.sram_address_from_build_conf(self.build_conf)
449                elif self.dt_flash:
450                    flash_addr = self.flash_address_from_build_conf(self.build_conf)
451                else:
452                    flash_addr = 0
453                flash_cmd = f'loadfile "{self.file}" 0x{flash_addr:x}'
454            else:
455                err = 'Cannot flash; jlink runner only supports hex and bin files'
456                raise ValueError(err)
457
458        else:
459            # Use hex, bin or elf file provided by the buildsystem.
460            # Preferring .hex over .mot, .bin and .elf
461            if self.hex_name is not None and os.path.isfile(self.hex_name):
462                flash_file = self.hex_name
463                flash_cmd = f'loadfile "{self.hex_name}"'
464            # Preferring .mot over .bin and .elf
465            elif self.mot_name is not None and os.path.isfile(self.mot_name):
466                flash_file = self.mot_name
467                flash_cmd = f'loadfile {self.mot_name}'
468            # Preferring .bin over .elf
469            elif self.bin_name is not None and os.path.isfile(self.bin_name):
470                if self.flash_sram:
471                    flash_addr = self.sram_address_from_build_conf(self.build_conf)
472                elif self.dt_flash:
473                    flash_addr = self.flash_address_from_build_conf(self.build_conf)
474                else:
475                    flash_addr = 0
476                flash_file = self.bin_name
477                flash_cmd = f'loadfile "{self.bin_name}" 0x{flash_addr:x}'
478            elif self.elf_name is not None and os.path.isfile(self.elf_name):
479                flash_file = self.elf_name
480                flash_cmd = f'loadfile "{self.elf_name}"'
481            elif self.mot_name is not None and os.path.isfile(self.mot_name):
482                flash_file = self.mot_name
483                flash_cmd = f'loadfile {self.mot_name}'
484            else:
485                err = 'Cannot flash; no hex ({}), bin ({}) or mot ({})  files found.'
486                raise ValueError(err.format(self.hex_name, self.bin_name))
487
488        # Flash the selected build artifact
489        lines.append(flash_cmd)
490
491        if self.reset:
492            lines.append('r') # Reset and halt the target
493
494        if self.flash_sram:
495            sram_addr = self.sram_address_from_build_conf(self.build_conf)
496            lines.append(f'WReg PC 0x{sram_addr:x}') # Change PC to start of SRAM
497
498        lines.append('g') # Start the CPU
499
500        # Reset the Debug Port CTRL/STAT register
501        # Under normal operation this is done automatically, but if other
502        # JLink tools are running, it is not performed.
503        # The J-Link scripting layer chains commands, meaning that writes are
504        # not actually performed until after the next operation. After writing
505        # the register, read it back to perform this flushing.
506        lines.append('writeDP 1 0')
507        lines.append('readDP 1')
508
509        lines.append('q') # Close the connection and quit
510
511        self.logger.debug('JLink commander script:\n' +
512                          '\n'.join(lines))
513        return flash_file, lines
514
515    def run_flash_cmd(self, fname, flash_file, **kwargs):
516        loader_details = ""
517        if self.supports_loader and self.loader:
518            loader_details = "?" + self.loader
519
520        # Determine device type for Commander
521        if self.dev_id:
522            if self.dev_id_type == 'auto':
523                # Auto-detect device type
524                detected_type = self._detect_dev_id_type(self.dev_id)
525            else:
526                # Use manually specified device type
527                detected_type = self.dev_id_type
528
529            # Build device selection for Commander
530            if detected_type == 'serialno':
531                device_args = ['-USB', f'{self.dev_id}']
532            elif detected_type == 'tty':
533                device_args = ['-USB', f'{self.dev_id}']  # J-Link Commander uses -USB for tty
534            elif detected_type == 'ip':
535                device_args = ['-IP', f'{self.dev_id}']
536            elif detected_type == 'tunnel':
537                device_args = ['-IP', f'{self.dev_id}']  # Treat tunnel as IP
538            else:
539                # Fallback to legacy behavior (USB)
540                device_args = ['-USB', f'{self.dev_id}']
541                self.logger.warning(f'"{self.dev_id}" does not match any known pattern')
542        else:
543            device_args = []
544
545        cmd = (
546            [self.commander]
547            + device_args
548            + (['-nogui', '1'] if self.supports_nogui else [])
549            + ['-if', self.iface]
550            + ['-speed', self.speed]
551            + ['-device', self.device + loader_details]
552            + ['-CommanderScript', fname]
553            + (['-nogui', '1'] if self.supports_nogui else [])
554            + self.tool_opt
555        )
556
557        if flash_file:
558            self.logger.info(f'Flashing file: {flash_file}')
559        kwargs = {}
560        if not self.logger.isEnabledFor(logging.DEBUG):
561            kwargs['stdout'] = subprocess.DEVNULL
562        self.check_call(cmd, **kwargs)
563
564    def flash(self, **kwargs):
565        fname = self.flash_script
566        if fname is None:
567            # Don't use NamedTemporaryFile: the resulting file can't be
568            # opened again on Windows.
569            with tempfile.TemporaryDirectory(suffix='jlink') as d:
570                flash_file, lines = self.get_default_flash_commands()
571                fname = os.path.join(d, 'runner.jlink')
572                with open(fname, 'wb') as f:
573                    f.writelines(bytes(line + '\n', 'utf-8') for line in lines)
574
575                self.run_flash_cmd(fname, flash_file, **kwargs)
576        else:
577            self.run_flash_cmd(fname, None, **kwargs)
578