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):
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
53        self.tool_opt = []
54        if tool_opt is not None:
55            for opts in [shlex.split(opt) for opt in tool_opt]:
56                self.tool_opt += opts
57
58    @classmethod
59    def name(cls):
60        return 'linkserver'
61
62    @classmethod
63    def capabilities(cls):
64        return RunnerCaps(commands={'flash', 'debug', 'debugserver', 'attach'},
65                          dev_id=True, flash_addr=True, erase=True,
66                          tool_opt=True, file=True)
67
68    @classmethod
69    def do_add_parser(cls, parser):
70        parser.add_argument('--device', required=True, help='device name')
71
72        parser.add_argument('--core', required=False, help='core of the device')
73
74        parser.add_argument('--probe', default='#1',
75                            help='interface to use (index, or serial number, default is #1')
76
77        parser.add_argument('--tui', default=False, action='store_true',
78                            help='if given, GDB uses -tui')
79
80        parser.add_argument('--gdb-port', default=DEFAULT_LINKSERVER_GDB_PORT,
81                            help=f'gdb port to open, defaults to {DEFAULT_LINKSERVER_GDB_PORT}')
82
83        parser.add_argument('--semihost-port', default=DEFAULT_LINKSERVER_SEMIHOST_PORT,
84                            help='semihost port to open, defaults to the empty string '
85                            'and runs a gdb server')
86        # keep this, we have to assume that the default 'commander' is on PATH
87        parser.add_argument('--linkserver', default=DEFAULT_LINKSERVER_EXE,
88                            help=f'''LinkServer executable, default is
89                            {DEFAULT_LINKSERVER_EXE}''')
90        # user may need to override settings.
91        parser.add_argument('--override', required=False, action='append',
92                            help='''configuration overrides as defined bylinkserver.
93                            Example: /device/memory/0/location=0xcafecafe''')
94
95    @classmethod
96    def do_create(cls, cfg, args):
97
98        print(f"RUNNER - gdb_port = {args.gdb_port}, semih port = {args.semihost_port}")
99        return LinkServerBinaryRunner(cfg, args.device, args.core,
100                                 linkserver=args.linkserver,
101                                 dt_flash=args.dt_flash,
102                                 erase=args.erase,
103                                 probe=args.probe,
104                                 semihost_port=args.semihost_port,
105                                 gdb_port=args.gdb_port,
106                                 override=args.override,
107                                 tui=args.tui, tool_opt=args.tool_opt)
108
109    @property
110    def linkserver_version_str(self):
111
112        if not hasattr(self, '_linkserver_version'):
113            linkserver_version_cmd=[self.linkserver, "-v"]
114            ls_output=self.check_output(linkserver_version_cmd)
115            self.linkserver_version = str(ls_output.split()[1].decode()).lower()
116
117        return self.linkserver_version
118
119    def do_run(self, command, **kwargs):
120
121        self.linkserver = self.require(self.linkserver)
122        self.logger.info(f'LinkServer: {self.linkserver}, version {self.linkserver_version_str}')
123
124        if command == 'flash':
125            self.flash(**kwargs)
126        else:
127            if self.core is not None:
128                _cmd_core = [ "-c",  self.core ]
129            else:
130                _cmd_core = []
131
132            linkserver_cmd = ([self.linkserver] +
133                              ["gdbserver"]    +
134                              ["--probe", str(self.probe) ] +
135                              ["--gdb-port", str(self.gdb_port )] +
136                              ["--semihost-port", str(self.semihost_port) ] +
137                              _cmd_core +
138                              self.override_cli +
139                              [self.device])
140
141            self.logger.debug(f'LinkServer cmd:  + {linkserver_cmd}')
142
143            if command in ('debug', 'attach'):
144                if self.elf_name is  None or not os.path.isfile(self.elf_name):
145                    raise ValueError('Cannot debug; elf file required')
146
147                gdb_cmd = ([self.gdb_cmd] +
148                           self.tui_arg +
149                           [self.elf_name] +
150                           ['-ex', f'target remote {self.gdb_host}:{self.gdb_port}'])
151
152                if command == 'debug':
153                    gdb_cmd += [ '-ex', 'load', '-ex', 'monitor reset']
154
155                if command == 'attach':
156                    linkserver_cmd += ['--attach']
157
158                self.run_server_and_client(linkserver_cmd, gdb_cmd)
159
160            elif command == 'debugserver':
161                if self.gdb_host:
162                    raise ValueError('Cannot run debugserver with --gdb-host')
163
164                self.check_call(linkserver_cmd)
165
166    def do_erase(self, **kwargs):
167
168        linkserver_cmd = ([self.linkserver, "flash"] + ["--probe", str(self.probe)] +
169                          [self.device] + ["erase"])
170        self.logger.debug("flash erase command = " + str(linkserver_cmd))
171        self.check_call(linkserver_cmd)
172
173    def _build_override_cli(self):
174
175        override_cli = []
176
177        if self.override is not None:
178            for ov in self.override:
179                override_cli = (override_cli + ["-o", str(ov)])
180
181        return override_cli
182
183    def flash(self, **kwargs):
184        linkserver_cmd = (
185            [self.linkserver, "flash"]
186            + ["--probe", str(self.probe)]
187            + self.override_cli
188            + [self.device]
189        )
190        self.logger.debug(f'LinkServer cmd:  + {linkserver_cmd}')
191
192        if self.erase:
193            self.do_erase()
194
195        # Use hex, bin or elf file provided by the buildsystem.
196        # Preferring .hex over .bin and .elf
197        if self.supports_hex() and self.hex_name is not None and os.path.isfile(self.hex_name):
198            flash_cmd = (["load", self.hex_name])
199        # Preferring .bin over .elf
200        elif self.bin_name is not None and os.path.isfile(self.bin_name):
201            if self.dt_flash:
202                load_addr = self.flash_address_from_build_conf(self.build_conf)
203            else:
204                self.logger.critical("no load flash address could be found...")
205                raise RuntimeError("no load flash address could be found...")
206
207            flash_cmd = (["load", "--addr", str(load_addr), self.bin_name])
208        elif self.elf_name is not None and os.path.isfile(self.elf_name):
209            flash_cmd = (["load", self.elf_name])
210        else:
211            err = 'Cannot flash; no hex ({}), bin ({}) or elf ({}) files found.'
212            raise ValueError(err.format(self.hex_name, self.bin_name, self.elf_name))
213
214        # Flash the selected file
215        linkserver_cmd = linkserver_cmd + flash_cmd
216        self.logger.debug("flash command = " + str(linkserver_cmd))
217        kwargs = {}
218        if not self.logger.isEnabledFor(logging.DEBUG):
219            if self.linkserver_version_str < "v1.3.15":
220                kwargs['stderr'] = subprocess.DEVNULL
221            else:
222                kwargs['stdout'] = subprocess.DEVNULL
223
224        self.check_call(linkserver_cmd, **kwargs)
225
226    def supports_hex(self):
227        # v1.5.30 has added flash support for Intel Hex files.
228        return self.linkserver_version_str >= "v1.5.30"
229