1# Copyright 2023-2024 NXP
2# Copyright (c) 2017 Linaro Limited.
3#
4# SPDX-License-Identifier: Apache-2.0
5#
6# Based on jlink.py
7
8'''Runner for debugging with NXP's LinkServer.'''
9
10import logging
11import os
12import shlex
13import subprocess
14import sys
15
16from runners.core import RunnerCaps, ZephyrBinaryRunner
17
18DEFAULT_LINKSERVER_EXE = 'Linkserver.exe' if sys.platform == 'win32' else 'LinkServer'
19DEFAULT_LINKSERVER_GDB_PORT =  3333
20DEFAULT_LINKSERVER_SEMIHOST_PORT = 8888
21
22class LinkServerBinaryRunner(ZephyrBinaryRunner):
23    '''Runner front-end for NXP Linkserver'''
24    def __init__(self, cfg, device, core,
25                 linkserver=DEFAULT_LINKSERVER_EXE,
26                 dt_flash=True, erase=True,
27                 probe='#1',
28                 gdb_host='',
29                 gdb_port=DEFAULT_LINKSERVER_GDB_PORT,
30                 semihost_port=DEFAULT_LINKSERVER_SEMIHOST_PORT,
31                 override=None,
32                 tui=False, tool_opt=None, batch=False):
33        super().__init__(cfg)
34        self.file = cfg.file
35        self.file_type = cfg.file_type
36        self.hex_name = cfg.hex_file
37        self.bin_name = cfg.bin_file
38        self.elf_name = cfg.elf_file
39        self.gdb_cmd = cfg.gdb if cfg.gdb else None
40        self.device = device
41        self.core = core
42        self.linkserver = linkserver
43        self.dt_flash = dt_flash
44        self.erase = erase
45        self.probe = probe
46        self.gdb_host = gdb_host
47        self.gdb_port = gdb_port
48        self.semihost_port = semihost_port
49        self.tui_arg = ['-tui'] if tui else []
50        self.override = override if override else []
51        self.override_cli = self._build_override_cli()
52        self.is_batch = batch
53
54        self.tool_opt = []
55        if tool_opt is not None:
56            for opts in [shlex.split(opt) for opt in tool_opt]:
57                self.tool_opt += opts
58
59    @classmethod
60    def name(cls):
61        return 'linkserver'
62
63    @classmethod
64    def capabilities(cls):
65        return RunnerCaps(commands={'flash', 'debug', 'debugserver', 'attach'},
66                          dev_id=True, flash_addr=True, erase=True,
67                          tool_opt=True, file=True, batch_debug=True)
68
69    @classmethod
70    def do_add_parser(cls, parser):
71        parser.add_argument('--device', required=True, help='device name')
72
73        parser.add_argument('--core', required=False, help='core of the device')
74
75        parser.add_argument('--probe', default='#1',
76                            help='interface to use (index, or serial number, default is #1')
77
78        parser.add_argument('--tui', default=False, action='store_true',
79                            help='if given, GDB uses -tui')
80
81        parser.add_argument('--gdb-port', default=DEFAULT_LINKSERVER_GDB_PORT,
82                            help=f'gdb port to open, defaults to {DEFAULT_LINKSERVER_GDB_PORT}')
83
84        parser.add_argument('--semihost-port', default=DEFAULT_LINKSERVER_SEMIHOST_PORT,
85                            help='semihost port to open, defaults to the empty string '
86                            'and runs a gdb server')
87        # keep this, we have to assume that the default 'commander' is on PATH
88        parser.add_argument('--linkserver', default=DEFAULT_LINKSERVER_EXE,
89                            help=f'''LinkServer executable, default is
90                            {DEFAULT_LINKSERVER_EXE}''')
91        # user may need to override settings.
92        parser.add_argument('--override', required=False, action='append',
93                            help='''configuration overrides as defined bylinkserver.
94                            Example: /device/memory/0/location=0xcafecafe''')
95
96    @classmethod
97    def do_create(cls, cfg, args):
98
99        print(f"RUNNER - gdb_port = {args.gdb_port}, semih port = {args.semihost_port}")
100        return LinkServerBinaryRunner(cfg, args.device, args.core,
101                                 linkserver=args.linkserver,
102                                 dt_flash=args.dt_flash,
103                                 erase=args.erase,
104                                 probe=args.probe,
105                                 semihost_port=args.semihost_port,
106                                 gdb_port=args.gdb_port,
107                                 override=args.override,
108                                 tui=args.tui, tool_opt=args.tool_opt,
109                                 batch=args.batch)
110
111    @property
112    def linkserver_version_str(self):
113
114        if not hasattr(self, '_linkserver_version'):
115            linkserver_version_cmd=[self.linkserver, "-v"]
116            ls_output=self.check_output(linkserver_version_cmd)
117            self.linkserver_version = str(ls_output.split()[1].decode()).lower()
118
119        return self.linkserver_version
120
121    def do_run(self, command, **kwargs):
122
123        self.linkserver = self.require(self.linkserver)
124        self.logger.info(f'LinkServer: {self.linkserver}, version {self.linkserver_version_str}')
125
126        if command == 'flash':
127            self.flash(**kwargs)
128        else:
129            if self.core is not None:
130                _cmd_core = [ "-c",  self.core ]
131            else:
132                _cmd_core = []
133
134            linkserver_cmd = ([self.linkserver] +
135                              ["gdbserver"]    +
136                              ["--probe", str(self.probe) ] +
137                              ["--gdb-port", str(self.gdb_port )] +
138                              ["--semihost-port", str(self.semihost_port) ] +
139                              _cmd_core +
140                              self.override_cli +
141                              [self.device])
142
143            self.logger.debug(f'LinkServer cmd:  + {linkserver_cmd}')
144
145            if command in ('debug', 'attach'):
146                if self.elf_name is  None or not os.path.isfile(self.elf_name):
147                    raise ValueError('Cannot debug; elf file required')
148
149                gdb_cmd = ([self.gdb_cmd] +
150                           self.tui_arg +
151                           [self.elf_name] +
152                           ['-batch' if self.is_batch else ''] +
153                           ['-ex', f'target remote {self.gdb_host}:{self.gdb_port}'])
154
155                if command == 'debug':
156                    # If the flash node points to ram, linkserver treats
157                    # the ram as inaccessible and does not flash.
158                    gdb_cmd += ['-ex', 'set mem inaccessible-by-default off']
159                    gdb_cmd += ['-ex', 'monitor reset', '-ex', 'load']
160                    if self.is_batch:
161                        gdb_cmd += ['-ex', 'monitor ondisconnect cont', '-ex',
162                                    'monitor kill_server', '-ex', 'quit']
163
164                if command == 'attach':
165                    linkserver_cmd += ['--attach']
166
167                self.run_server_and_client(linkserver_cmd, gdb_cmd)
168
169            elif command == 'debugserver':
170                if self.gdb_host:
171                    raise ValueError('Cannot run debugserver with --gdb-host')
172
173                self.check_call(linkserver_cmd)
174
175    def do_erase(self, **kwargs):
176
177        linkserver_cmd = ([self.linkserver, "flash"] + ["--probe", str(self.probe)] +
178                          [self.device] + ["erase"])
179        self.logger.debug("flash erase command = " + str(linkserver_cmd))
180        self.check_call(linkserver_cmd)
181
182    def _build_override_cli(self):
183
184        override_cli = []
185
186        if self.override is not None:
187            for ov in self.override:
188                override_cli = (override_cli + ["-o", str(ov)])
189
190        return override_cli
191
192    def flash(self, **kwargs):
193        linkserver_cmd = (
194            [self.linkserver, "flash"]
195            + ["--probe", str(self.probe)]
196            + self.override_cli
197            + [self.device]
198        )
199        self.logger.debug(f'LinkServer cmd:  + {linkserver_cmd}')
200
201        if self.erase:
202            self.do_erase()
203
204        # Use hex, bin or elf file provided by the buildsystem.
205        # Preferring .hex over .bin and .elf
206        if self.supports_hex() and self.hex_name is not None and os.path.isfile(self.hex_name):
207            flash_cmd = (["load", self.hex_name])
208        # Preferring .bin over .elf
209        elif self.bin_name is not None and os.path.isfile(self.bin_name):
210            if self.dt_flash:
211                load_addr = self.flash_address_from_build_conf(self.build_conf)
212            else:
213                self.logger.critical("no load flash address could be found...")
214                raise RuntimeError("no load flash address could be found...")
215
216            flash_cmd = (["load", "--addr", str(load_addr), self.bin_name])
217        elif self.elf_name is not None and os.path.isfile(self.elf_name):
218            flash_cmd = (["load", self.elf_name])
219        else:
220            err = 'Cannot flash; no hex ({}), bin ({}) or elf ({}) files found.'
221            raise ValueError(err.format(self.hex_name, self.bin_name, self.elf_name))
222
223        # Flash the selected file
224        linkserver_cmd = linkserver_cmd + flash_cmd
225        self.logger.debug("flash command = " + str(linkserver_cmd))
226        kwargs = {}
227        if not self.logger.isEnabledFor(logging.DEBUG):
228            if self.linkserver_version_str < "v1.3.15":
229                kwargs['stderr'] = subprocess.DEVNULL
230            else:
231                kwargs['stdout'] = subprocess.DEVNULL
232
233        self.check_call(linkserver_cmd, **kwargs)
234
235    def supports_hex(self):
236        # v1.5.30 has added flash support for Intel Hex files.
237        return self.linkserver_version_str >= "v1.5.30"
238