1# Copyright (c) 2017 Linaro Limited.
2# Copyright (c) 2023 Nordic Semiconductor ASA.
3#
4# SPDX-License-Identifier: Apache-2.0
5
6'''Runner base class for flashing with nrf tools.'''
7
8import abc
9import contextlib
10import functools
11import os
12import shlex
13import subprocess
14import sys
15from collections import deque
16from pathlib import Path
17from re import escape, fullmatch
18
19from zephyr_ext_common import ZEPHYR_BASE
20
21sys.path.append(os.fspath(Path(__file__).parent.parent.parent))
22import zephyr_module
23
24from runners.core import RunnerCaps, ZephyrBinaryRunner
25
26try:
27    from intelhex import IntelHex
28except ImportError:
29    IntelHex = None
30
31ErrNotAvailableBecauseProtection = 24
32ErrVerify = 25
33
34UICR_RANGES = {
35    'nrf53': {
36        'Application': (0x00FF8000, 0x00FF8800),
37        'Network': (0x01FF8000, 0x01FF8800),
38    },
39    'nrf54h': {
40        'Application': (0x0FFF8000, 0x0FFF8800),
41        'Network': (0x0FFFA000, 0x0FFFA800),
42    },
43    'nrf54l': {
44        'Application': (0x00FFD000, 0x00FFDA00),
45    },
46    'nrf91': {
47        'Application': (0x00FF8000, 0x00FF8800),
48    },
49    'nrf92': {
50        'Application': (0x0FFF8000, 0x0FFF8800),
51        'Network': (0x0FFFA000, 0x0FFFA800),
52    },
53}
54
55# Relative to the root of the hal_nordic module
56SUIT_STARTER_PATH = Path('zephyr/blobs/suit/bin/suit_manifest_starter.hex')
57
58@functools.cache
59def _get_suit_starter():
60    path = None
61    modules = zephyr_module.parse_modules(ZEPHYR_BASE)
62    for m in modules:
63        if 'hal_nordic' in m.meta.get('name'):
64            path = Path(m.project)
65            break
66
67    if not path:
68        raise RuntimeError("hal_nordic project missing in the manifest")
69
70    suit_starter = path / SUIT_STARTER_PATH
71    if not suit_starter.exists():
72        raise RuntimeError("Unable to find suit manifest starter file, "
73                           "please make sure to run \'west blobs fetch "
74                           "hal_nordic\'")
75
76    return str(suit_starter.resolve())
77
78class NrfBinaryRunner(ZephyrBinaryRunner):
79    '''Runner front-end base class for nrf tools.'''
80
81    def __init__(self, cfg, family, softreset, pinreset, dev_id, erase=False,
82                 reset=True, tool_opt=None, force=False, recover=False):
83        super().__init__(cfg)
84        self.hex_ = cfg.hex_file
85        # The old --nrf-family options takes upper-case family names
86        self.family = family.lower() if family else None
87        self.softreset = softreset
88        self.pinreset = pinreset
89        self.dev_id = dev_id
90        self.erase = bool(erase)
91        self.reset = bool(reset)
92        self.force = force
93        self.recover = bool(recover)
94
95        # Only applicable for nrfutil
96        self.suit_starter = False
97
98        self.tool_opt = []
99        if tool_opt is not None:
100            for opts in [shlex.split(opt) for opt in tool_opt]:
101                self.tool_opt += opts
102
103    @classmethod
104    def _capabilities(cls, mult_dev_ids=False):
105        return RunnerCaps(commands={'flash'}, dev_id=True,
106                          mult_dev_ids=mult_dev_ids, erase=True, reset=True,
107                          tool_opt=True)
108
109    @classmethod
110    def _dev_id_help(cls) -> str:
111        return '''Device identifier. Use it to select the J-Link Serial Number
112                  of the device connected over USB. '*' matches one or more
113                  characters/digits'''
114
115    @classmethod
116    def do_add_parser(cls, parser):
117        parser.add_argument('--nrf-family',
118                            choices=['NRF51', 'NRF52', 'NRF53', 'NRF54L',
119                                     'NRF54H', 'NRF91', 'NRF92'],
120                            help='''MCU family; still accepted for
121                            compatibility only''')
122        # Not using a mutual exclusive group for softreset and pinreset due to
123        # the way dump_runner_option_help() works in run_common.py
124        parser.add_argument('--softreset', required=False,
125                            action='store_true',
126                            help='use softreset instead of pinreset')
127        parser.add_argument('--pinreset', required=False,
128                            action='store_true',
129                            help='use pinreset instead of softreset')
130        parser.add_argument('--snr', required=False, dest='dev_id',
131                            help='obsolete synonym for -i/--dev-id')
132        parser.add_argument('--force', required=False,
133                            action='store_true',
134                            help='Flash even if the result cannot be guaranteed.')
135        parser.add_argument('--recover', required=False,
136                            action='store_true',
137                            help='''erase all user available non-volatile
138                            memory and disable read back protection before
139                            flashing (erases flash for both cores on nRF53)''')
140
141        parser.set_defaults(reset=True)
142
143    @classmethod
144    def args_from_previous_runner(cls, previous_runner, args):
145        # Propagate the chosen device ID to next runner
146        if args.dev_id is None:
147            args.dev_id = previous_runner.dev_id
148
149    def ensure_snr(self):
150        # dev_id can be None, str or list of str
151        dev_id = self.dev_id
152        if isinstance(dev_id, list):
153            if len(dev_id) == 0:
154                dev_id = None
155            elif len(dev_id) == 1:
156                dev_id = dev_id[0]
157            else:
158                self.dev_id = [d.lstrip("0") for d in dev_id]
159                return
160        if not dev_id or "*" in dev_id:
161            dev_id = self.get_board_snr(dev_id or "*")
162        self.dev_id = dev_id.lstrip("0")
163
164    @abc.abstractmethod
165    def do_get_boards(self):
166        ''' Return an array of Segger SNRs '''
167
168    def get_boards(self):
169        snrs = self.do_get_boards()
170        if not snrs:
171            raise RuntimeError('Unable to find a board; '
172                               'is the board connected?')
173        return snrs
174
175    @staticmethod
176    def verify_snr(snr):
177        if snr == '0':
178            raise RuntimeError('The Segger SNR obtained is 0; '
179                                'is a debugger already connected?')
180
181    def get_board_snr(self, glob):
182        # Use nrfjprog or nrfutil to discover connected boards.
183        #
184        # If there's exactly one board connected, it's safe to assume
185        # the user wants that one. Otherwise, bail unless there are
186        # multiple boards and we are connected to a terminal, in which
187        # case use print() and input() to ask what the user wants.
188
189        re_glob = escape(glob).replace(r"\*", ".+")
190        snrs = [snr for snr in self.get_boards() if fullmatch(re_glob, snr)]
191
192        if len(snrs) == 0:
193            raise RuntimeError(
194                'There are no boards connected{}.'.format(
195                        f" matching '{glob}'" if glob != "*" else ""))
196        elif len(snrs) == 1:
197            board_snr = snrs[0]
198            self.verify_snr(board_snr)
199            print(f"Using board {board_snr}")
200            return board_snr
201        elif not sys.stdin.isatty():
202            raise RuntimeError(
203                f'refusing to guess which of {len(snrs)} '
204                'connected boards to use. (Interactive prompts '
205                'disabled since standard input is not a terminal.) '
206                'Please specify a serial number on the command line.')
207
208        snrs = sorted(snrs)
209        print('There are multiple boards connected{}.'.format(
210                        f" matching '{glob}'" if glob != "*" else ""))
211        for i, snr in enumerate(snrs, 1):
212            print(f'{i}. {snr}')
213
214        p = f'Please select one with desired serial number (1-{len(snrs)}): '
215        while True:
216            try:
217                value = input(p)
218            except EOFError:
219                sys.exit(0)
220            try:
221                value = int(value)
222            except ValueError:
223                continue
224            if 1 <= value <= len(snrs):
225                break
226
227        return snrs[value - 1]
228
229    def ensure_family(self):
230        # Ensure self.family is set.
231
232        if self.family is not None:
233            return
234
235        if self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF51X'):
236            self.family = 'nrf51'
237        elif self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF52X'):
238            self.family = 'nrf52'
239        elif self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF53X'):
240            self.family = 'nrf53'
241        elif self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF54LX'):
242            self.family = 'nrf54l'
243        elif self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF54HX'):
244            self.family = 'nrf54h'
245        elif self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF91X'):
246            self.family = 'nrf91'
247        elif self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF92X'):
248            self.family = 'nrf92'
249        else:
250            raise RuntimeError(f'unknown nRF; update {__file__}')
251
252    def hex_refers_region(self, region_start, region_end):
253        for segment_start, _ in self.hex_contents.segments():
254            if region_start <= segment_start <= region_end:
255                return True
256        return False
257
258    def hex_get_uicrs(self):
259        hex_uicrs = {}
260
261        if self.family in UICR_RANGES:
262            for uicr_core, uicr_range in UICR_RANGES[self.family].items():
263                if self.hex_refers_region(*uicr_range):
264                    hex_uicrs[uicr_core] = uicr_range
265
266        return hex_uicrs
267
268    def flush(self, force=False):
269        try:
270            self.flush_ops(force=force)
271        except subprocess.CalledProcessError as cpe:
272            if cpe.returncode == ErrNotAvailableBecauseProtection:
273                if self.family == 'nrf53':
274                    family_help = (
275                        '  Note: your target is an nRF53; all flash memory '
276                        'for both the network and application cores will be '
277                        'erased prior to reflashing.')
278                else:
279                    family_help = (
280                        '  Note: this will recover and erase all flash memory '
281                        'prior to reflashing.')
282                self.logger.error(
283                    'Flashing failed because the target '
284                    'must be recovered.\n'
285                    '  To fix, run "west flash --recover" instead.\n' +
286                    family_help)
287            if cpe.returncode == ErrVerify and self.hex_get_uicrs():
288                # If there is data in the UICR region it is likely that the
289                # verify failed due to the UICR not been erased before, so giving
290                # a warning here will hopefully enhance UX.
291                self.logger.warning(
292                    'The hex file contains data placed in the UICR, which '
293                    'may require a full erase before reprogramming. Run '
294                    'west flash again with --erase, or --recover.'
295                )
296            raise
297
298
299    def recover_target(self):
300        if self.family in ('nrf53', 'nrf54h', 'nrf92'):
301            self.logger.info(
302                'Recovering and erasing flash memory for both the network '
303                'and application cores.')
304        else:
305            self.logger.info('Recovering and erasing all flash memory.')
306
307        # The network core of the nRF53 needs to be recovered first due to the
308        # fact that recovering it erases the flash of *both* cores. Since a
309        # recover operation unlocks the core and then flashes a small image that
310        # keeps the debug access port open, recovering the network core last
311        # would result in that small image being deleted from the app core.
312        # In the case of the 54H, the order is indifferent.
313        if self.family in ('nrf53', 'nrf54h', 'nrf92'):
314            self.exec_op('recover', core='Network')
315
316        self.exec_op('recover')
317
318    def _get_core(self):
319        if self.family in ('nrf54h', 'nrf92'):
320            if (self.build_conf.getboolean('CONFIG_SOC_NRF54H20_CPUAPP') or
321                self.build_conf.getboolean('CONFIG_SOC_NRF54H20_CPUFLPR') or
322                self.build_conf.getboolean('CONFIG_SOC_NRF54H20_CPUPPR') or
323                self.build_conf.getboolean('CONFIG_SOC_NRF9280_CPUAPP')):
324                return 'Application'
325            if (self.build_conf.getboolean('CONFIG_SOC_NRF54H20_CPURAD') or
326                self.build_conf.getboolean('CONFIG_SOC_NRF9280_CPURAD')):
327                return 'Network'
328            raise RuntimeError(f'Core not found for family: {self.family}')
329
330        if self.family in ('nrf53'):
331            if self.build_conf.getboolean('CONFIG_SOC_NRF5340_CPUAPP'):
332                return 'Application'
333            if self.build_conf.getboolean('CONFIG_SOC_NRF5340_CPUNET'):
334                return 'Network'
335            raise RuntimeError(f'Core not found for family: {self.family}')
336
337        return None
338
339    def program_hex(self):
340        # Get the command use to actually program self.hex_.
341        self.logger.info(f'Flashing file: {self.hex_}')
342
343        # What type of erase/core arguments should we pass to the tool?
344        core = self._get_core()
345
346        if self.family in ('nrf54h', 'nrf92'):
347            erase_arg = 'ERASE_NONE'
348
349            generated_uicr = self.build_conf.getboolean('CONFIG_NRF_REGTOOL_GENERATE_UICR')
350
351            if generated_uicr and not self.hex_get_uicrs().get(core):
352                raise RuntimeError(
353                    f"Expected a UICR to be contained in: {self.hex_}\n"
354                    "Please ensure that the correct version of nrf-regtool is "
355                    "installed, then run 'west build --cmake' to try again."
356                )
357
358            if self.erase:
359                self.exec_op('erase', core='Application', kind='all')
360                self.exec_op('erase', core='Network', kind='all')
361
362            # Manage SUIT artifacts.
363            # This logic should be executed only once per build.
364            # Use sysbuild board qualifiers to select the context,
365            # with which the artifacts will be programmed.
366            if self.build_conf.get('CONFIG_BOARD_QUALIFIERS') == self.sysbuild_conf.get(
367                'SB_CONFIG_BOARD_QUALIFIERS'
368            ):
369                mpi_hex_dir = Path(os.path.join(self.cfg.build_dir, 'zephyr'))
370
371                # Handle Manifest Provisioning Information
372                if self.sysbuild_conf.getboolean('SB_CONFIG_SUIT_MPI_GENERATE'):
373                    app_mpi_hex_file = os.fspath(
374                        mpi_hex_dir / self.sysbuild_conf.get('SB_CONFIG_SUIT_MPI_APP_AREA_PATH'))
375                    rad_mpi_hex_file = os.fspath(
376                        mpi_hex_dir / self.sysbuild_conf.get('SB_CONFIG_SUIT_MPI_RAD_AREA_PATH')
377                    )
378                    if os.path.exists(app_mpi_hex_file):
379                        self.op_program(
380                            app_mpi_hex_file,
381                            'ERASE_NONE',
382                            None,
383                            defer=True,
384                            core='Application',
385                        )
386                    if os.path.exists(rad_mpi_hex_file):
387                        self.op_program(
388                            rad_mpi_hex_file,
389                            'ERASE_NONE',
390                            None,
391                            defer=True,
392                            core='Network',
393                        )
394
395                # Handle SUIT root manifest if application manifests are not used.
396                # If an application firmware is built, the root envelope is merged
397                # with other application manifests as well as the output HEX file.
398                if core != 'Application' and self.sysbuild_conf.get('SB_CONFIG_SUIT_ENVELOPE'):
399                    app_root_envelope_hex_file = os.fspath(
400                        mpi_hex_dir / 'suit_installed_envelopes_application_merged.hex'
401                    )
402                    if os.path.exists(app_root_envelope_hex_file):
403                        self.op_program(
404                            app_root_envelope_hex_file,
405                            'ERASE_NONE',
406                            None,
407                            defer=True,
408                            core='Application',
409                        )
410
411            if not self.erase and generated_uicr:
412                self.exec_op('erase', core=core, kind='uicr')
413        else:
414            if self.erase:
415                erase_arg = 'ERASE_ALL'
416            else:
417                erase_arg = 'ERASE_RANGES_TOUCHED_BY_FIRMWARE'
418
419        xip_ranges = {
420            'nrf52': (0x12000000, 0x19FFFFFF),
421            'nrf53': (0x10000000, 0x1FFFFFFF),
422        }
423        ext_mem_erase_opt = None
424        if self.family in xip_ranges:
425            xip_start, xip_end = xip_ranges[self.family]
426            if self.hex_refers_region(xip_start, xip_end):
427                ext_mem_erase_opt = erase_arg
428
429        self.op_program(self.hex_, erase_arg, ext_mem_erase_opt, defer=True, core=core)
430        self.flush(force=False)
431
432
433    def reset_target(self):
434        sw_reset = "RESET_HARD" if self.family in ('nrf54h', 'nrf92') else "RESET_SYSTEM"
435        # Default to soft reset on nRF52 only, because ICs in these series can
436        # reconfigure the reset pin as a regular GPIO
437        default = sw_reset if self.family == 'nrf52' else "RESET_PIN"
438        kind = (sw_reset if self.softreset else "RESET_PIN" if
439                self.pinreset else default)
440
441        if self.family == 'nrf52' and kind == "RESET_PIN":
442            # Write to the UICR enabling nRESET in the corresponding pin
443            self.exec_op('pinreset-enable')
444
445        self.logger.debug(f'Reset kind: {kind}')
446        self.exec_op('reset', kind=kind)
447
448    @abc.abstractmethod
449    def do_require(self):
450        ''' Ensure the tool is installed '''
451
452    def _check_suit_starter(self, op):
453        op = op['operation']
454        if op['type'] not in ('erase', 'recover', 'program'):
455            return None
456        if op['type'] == 'program' and op['options']['chip_erase_mode'] != "ERASE_UICR":
457            return None
458
459        file = _get_suit_starter()
460        self.logger.debug(f'suit starter: {file}')
461
462        return file
463
464    def op_program(self, hex_file, erase, ext_mem_erase, defer=False, core=None):
465        args = self._op_program(hex_file, erase, ext_mem_erase)
466        self.exec_op('program', defer, core, **args)
467
468    def _op_program(self, hex_file, erase, ext_mem_erase):
469        args = {'firmware': {'file': hex_file},
470                'options': {'chip_erase_mode': erase, 'verify': 'VERIFY_READ'}}
471        if ext_mem_erase:
472            args['options']['ext_mem_erase_mode'] = ext_mem_erase
473
474        return args
475
476    def exec_op(self, op, defer=False, core=None, **kwargs):
477
478        def _exec_op(op, defer=False, core=None, **kwargs):
479            _op = f'{op}'
480            op = {'operation': {'type': _op}}
481            if core:
482                op['core'] = core
483            op['operation'].update(kwargs)
484            self.logger.debug(f'defer: {defer} op: {op}')
485            if defer or not self.do_exec_op(op, force=False):
486                self.ops.append(op)
487            return op
488
489        _op = _exec_op(op, defer, core, **kwargs)
490        # Check if the suit manifest starter needs programming
491        if self.suit_starter and self.family == 'nrf54h':
492            file = self._check_suit_starter(_op)
493            if file:
494                args = self._op_program(file, 'ERASE_NONE', None)
495                _exec_op('program', defer, core, **args)
496
497    @abc.abstractmethod
498    def do_exec_op(self, op, force=False):
499        ''' Execute an operation. Return True if executed, False if not.
500            Throws subprocess.CalledProcessError with the appropriate
501            returncode if a failure arises.'''
502
503    def flush_ops(self, force=True):
504        ''' Execute any remaining ops in the self.ops array.
505            Throws subprocess.CalledProcessError with the appropriate
506            returncode if a failure arises.
507            Subclasses can override this method for special handling of
508            queued ops.'''
509        self.logger.debug('Flushing ops')
510        while self.ops:
511            self.do_exec_op(self.ops.popleft(), force)
512
513    def do_run(self, command, **kwargs):
514        self.do_require()
515
516        if self.softreset and self.pinreset:
517            raise RuntimeError('Options --softreset and --pinreset are mutually '
518                               'exclusive.')
519
520        self.ensure_output('hex')
521        if IntelHex is None:
522            raise RuntimeError('Python dependency intelhex was missing; '
523                               'see the getting started guide for details on '
524                               'how to fix')
525        self.hex_contents = IntelHex()
526        with contextlib.suppress(FileNotFoundError):
527            self.hex_contents.loadfile(self.hex_, format='hex')
528
529        self.ensure_snr()
530        self.ensure_family()
531
532        self.ops = deque()
533
534        if self.recover:
535            self.recover_target()
536        self.program_hex()
537        if self.reset:
538            self.reset_target()
539        # All done, now flush any outstanding ops
540        self.flush(force=True)
541
542        self.logger.info(f'Board(s) with serial number(s) {self.dev_id} '
543                          'flashed successfully.')
544