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                 erase_mode=None, ext_erase_mode=None, reset=True,
83                 tool_opt=None, force=False, recover=False):
84        super().__init__(cfg)
85        self.hex_ = cfg.hex_file
86        # The old --nrf-family options takes upper-case family names
87        self.family = family.lower() if family else None
88        self.softreset = softreset
89        self.pinreset = pinreset
90        self.dev_id = dev_id
91        self.erase = bool(erase)
92        self.erase_mode = erase_mode
93        self.ext_erase_mode = ext_erase_mode
94        self.reset = bool(reset)
95        self.force = force
96        self.recover = bool(recover)
97
98        # Only applicable for nrfutil
99        self.suit_starter = False
100
101        self.tool_opt = []
102        if tool_opt is not None:
103            for opts in [shlex.split(opt) for opt in tool_opt]:
104                self.tool_opt += opts
105
106    @classmethod
107    def _capabilities(cls, mult_dev_ids=False):
108        return RunnerCaps(commands={'flash'}, dev_id=True,
109                          mult_dev_ids=mult_dev_ids, erase=True, reset=True,
110                          tool_opt=True)
111
112    @classmethod
113    def _dev_id_help(cls) -> str:
114        return '''Device identifier. Use it to select the J-Link Serial Number
115                  of the device connected over USB. '*' matches one or more
116                  characters/digits'''
117
118    @classmethod
119    def do_add_parser(cls, parser):
120        parser.add_argument('--nrf-family',
121                            choices=['NRF51', 'NRF52', 'NRF53', 'NRF54L',
122                                     'NRF54H', 'NRF91', 'NRF92'],
123                            help='''MCU family; still accepted for
124                            compatibility only''')
125        # Not using a mutual exclusive group for softreset and pinreset due to
126        # the way dump_runner_option_help() works in run_common.py
127        parser.add_argument('--softreset', required=False,
128                            action='store_true',
129                            help='use softreset instead of pinreset')
130        parser.add_argument('--pinreset', required=False,
131                            action='store_true',
132                            help='use pinreset instead of softreset')
133        parser.add_argument('--snr', required=False, dest='dev_id',
134                            help='obsolete synonym for -i/--dev-id')
135        parser.add_argument('--force', required=False,
136                            action='store_true',
137                            help='Flash even if the result cannot be guaranteed.')
138        parser.add_argument('--recover', required=False,
139                            action='store_true',
140                            help='''erase all user available non-volatile
141                            memory and disable read back protection before
142                            flashing (erases flash for both cores on nRF53)''')
143        parser.add_argument('--erase-mode', required=False,
144                            choices=['none', 'ranges', 'all'],
145                            help='Select the type of erase operation for the '
146                                 'internal non-volatile memory')
147        parser.add_argument('--ext-erase-mode', required=False,
148                            choices=['none', 'ranges', 'all'],
149                            help='Select the type of erase operation for the '
150                                 'external non-volatile memory')
151
152        parser.set_defaults(reset=True)
153
154    @classmethod
155    def args_from_previous_runner(cls, previous_runner, args):
156        # Propagate the chosen device ID to next runner
157        if args.dev_id is None:
158            args.dev_id = previous_runner.dev_id
159
160    def ensure_snr(self):
161        # dev_id can be None, str or list of str
162        dev_id = self.dev_id
163        if isinstance(dev_id, list):
164            if len(dev_id) == 0:
165                dev_id = None
166            elif len(dev_id) == 1:
167                dev_id = dev_id[0]
168            else:
169                self.dev_id = [d.lstrip("0") for d in dev_id]
170                return
171        if not dev_id or "*" in dev_id:
172            dev_id = self.get_board_snr(dev_id or "*")
173        self.dev_id = dev_id.lstrip("0")
174
175    @abc.abstractmethod
176    def do_get_boards(self):
177        ''' Return an array of Segger SNRs '''
178
179    def get_boards(self):
180        snrs = self.do_get_boards()
181        if not snrs:
182            raise RuntimeError('Unable to find a board; '
183                               'is the board connected?')
184        return snrs
185
186    @staticmethod
187    def verify_snr(snr):
188        if snr == '0':
189            raise RuntimeError('The Segger SNR obtained is 0; '
190                                'is a debugger already connected?')
191
192    def get_board_snr(self, glob):
193        # Use nrfjprog or nrfutil to discover connected boards.
194        #
195        # If there's exactly one board connected, it's safe to assume
196        # the user wants that one. Otherwise, bail unless there are
197        # multiple boards and we are connected to a terminal, in which
198        # case use print() and input() to ask what the user wants.
199
200        re_glob = escape(glob).replace(r"\*", ".+")
201        snrs = [snr for snr in self.get_boards() if fullmatch(re_glob, snr)]
202
203        if len(snrs) == 0:
204            raise RuntimeError(
205                'There are no boards connected{}.'.format(
206                        f" matching '{glob}'" if glob != "*" else ""))
207        elif len(snrs) == 1:
208            board_snr = snrs[0]
209            self.verify_snr(board_snr)
210            print(f"Using board {board_snr}")
211            return board_snr
212        elif not sys.stdin.isatty():
213            raise RuntimeError(
214                f'refusing to guess which of {len(snrs)} '
215                'connected boards to use. (Interactive prompts '
216                'disabled since standard input is not a terminal.) '
217                'Please specify a serial number on the command line.')
218
219        snrs = sorted(snrs)
220        print('There are multiple boards connected{}.'.format(
221                        f" matching '{glob}'" if glob != "*" else ""))
222        for i, snr in enumerate(snrs, 1):
223            print(f'{i}. {snr}')
224
225        p = f'Please select one with desired serial number (1-{len(snrs)}): '
226        while True:
227            try:
228                value = input(p)
229            except EOFError:
230                sys.exit(0)
231            try:
232                value = int(value)
233            except ValueError:
234                continue
235            if 1 <= value <= len(snrs):
236                break
237
238        return snrs[value - 1]
239
240    def ensure_family(self):
241        # Ensure self.family is set.
242
243        if self.family is not None:
244            return
245
246        if self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF51X'):
247            self.family = 'nrf51'
248        elif self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF52X'):
249            self.family = 'nrf52'
250        elif self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF53X'):
251            self.family = 'nrf53'
252        elif self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF54LX'):
253            self.family = 'nrf54l'
254        elif self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF54HX'):
255            self.family = 'nrf54h'
256        elif self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF91X'):
257            self.family = 'nrf91'
258        elif self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF92X'):
259            self.family = 'nrf92'
260        else:
261            raise RuntimeError(f'unknown nRF; update {__file__}')
262
263    def hex_refers_region(self, region_start, region_end):
264        for segment_start, _ in self.hex_contents.segments():
265            if region_start <= segment_start <= region_end:
266                return True
267        return False
268
269    def hex_get_uicrs(self):
270        hex_uicrs = {}
271
272        if self.family in UICR_RANGES:
273            for uicr_core, uicr_range in UICR_RANGES[self.family].items():
274                if self.hex_refers_region(*uicr_range):
275                    hex_uicrs[uicr_core] = uicr_range
276
277        return hex_uicrs
278
279    def flush(self, force=False):
280        try:
281            self.flush_ops(force=force)
282        except subprocess.CalledProcessError as cpe:
283            if cpe.returncode == ErrNotAvailableBecauseProtection:
284                if self.family == 'nrf53':
285                    family_help = (
286                        '  Note: your target is an nRF53; all flash memory '
287                        'for both the network and application cores will be '
288                        'erased prior to reflashing.')
289                else:
290                    family_help = (
291                        '  Note: this will recover and erase all flash memory '
292                        'prior to reflashing.')
293                self.logger.error(
294                    'Flashing failed because the target '
295                    'must be recovered.\n'
296                    '  To fix, run "west flash --recover" instead.\n' +
297                    family_help)
298            if cpe.returncode == ErrVerify and self.hex_get_uicrs():
299                # If there is data in the UICR region it is likely that the
300                # verify failed due to the UICR not been erased before, so giving
301                # a warning here will hopefully enhance UX.
302                self.logger.warning(
303                    'The hex file contains data placed in the UICR, which '
304                    'may require a full erase before reprogramming. Run '
305                    'west flash again with --erase, or --recover.'
306                )
307            raise
308
309
310    def recover_target(self):
311        if self.family in ('nrf53', 'nrf54h', 'nrf92'):
312            self.logger.info(
313                'Recovering and erasing flash memory for both the network '
314                'and application cores.')
315        else:
316            self.logger.info('Recovering and erasing all flash memory.')
317
318        # The network core of the nRF53 needs to be recovered first due to the
319        # fact that recovering it erases the flash of *both* cores. Since a
320        # recover operation unlocks the core and then flashes a small image that
321        # keeps the debug access port open, recovering the network core last
322        # would result in that small image being deleted from the app core.
323        if self.family in ('nrf53', 'nrf92'):
324            self.exec_op('recover', core='Network')
325
326        self.exec_op('recover')
327
328    def _get_core(self):
329        if self.family in ('nrf54h', 'nrf92'):
330            if (self.build_conf.getboolean('CONFIG_SOC_NRF54H20_CPUAPP') or
331                self.build_conf.getboolean('CONFIG_SOC_NRF54H20_CPUFLPR') or
332                self.build_conf.getboolean('CONFIG_SOC_NRF54H20_CPUPPR') or
333                self.build_conf.getboolean('CONFIG_SOC_NRF9280_CPUAPP')):
334                return 'Application'
335            if (self.build_conf.getboolean('CONFIG_SOC_NRF54H20_CPURAD') or
336                self.build_conf.getboolean('CONFIG_SOC_NRF9280_CPURAD')):
337                return 'Network'
338            raise RuntimeError(f'Core not found for family: {self.family}')
339
340        if self.family in ('nrf53'):
341            if self.build_conf.getboolean('CONFIG_SOC_NRF5340_CPUAPP'):
342                return 'Application'
343            if self.build_conf.getboolean('CONFIG_SOC_NRF5340_CPUNET'):
344                return 'Network'
345            raise RuntimeError(f'Core not found for family: {self.family}')
346
347        return None
348
349    def _get_erase_mode(self, mode):
350        if not mode:
351            return None
352        elif mode == "none":
353            return "ERASE_NONE"
354        elif mode == "ranges":
355            return "ERASE_RANGES_TOUCHED_BY_FIRMWARE"
356        elif mode == "all":
357            return "ERASE_ALL"
358        else:
359            raise RuntimeError(f"Invalid erase mode: {mode}")
360
361    def program_hex(self):
362        # Get the command use to actually program self.hex_.
363        self.logger.info(f'Flashing file: {self.hex_}')
364
365        # What type of erase/core arguments should we pass to the tool?
366        core = self._get_core()
367
368        if self.family in ('nrf54h', 'nrf92'):
369            erase_arg = 'ERASE_NONE'
370
371            regtool_generated_uicr = self.build_conf.getboolean('CONFIG_NRF_REGTOOL_GENERATE_UICR')
372
373            if regtool_generated_uicr and not self.hex_get_uicrs().get(core):
374                raise RuntimeError(
375                    f"Expected a UICR to be contained in: {self.hex_}\n"
376                    "Please ensure that the correct version of nrf-regtool is "
377                    "installed, then run 'west build --cmake' to try again."
378                )
379
380            if self.erase:
381                if self.family == 'nrf54h':
382                    self.exec_op('erase', kind='all')
383                else:
384                    self.exec_op('erase', core='Application', kind='all')
385                    self.exec_op('erase', core='Network', kind='all')
386
387            # Manage SUIT artifacts.
388            # This logic should be executed only once per build.
389            # Use sysbuild board qualifiers to select the context,
390            # with which the artifacts will be programmed.
391            if self.build_conf.get('CONFIG_BOARD_QUALIFIERS') == self.sysbuild_conf.get(
392                'SB_CONFIG_BOARD_QUALIFIERS'
393            ):
394                mpi_hex_dir = Path(os.path.join(self.cfg.build_dir, 'zephyr'))
395
396                # Handle Manifest Provisioning Information
397                if self.sysbuild_conf.getboolean('SB_CONFIG_SUIT_MPI_GENERATE'):
398                    app_mpi_hex_file = os.fspath(
399                        mpi_hex_dir / self.sysbuild_conf.get('SB_CONFIG_SUIT_MPI_APP_AREA_PATH'))
400                    rad_mpi_hex_file = os.fspath(
401                        mpi_hex_dir / self.sysbuild_conf.get('SB_CONFIG_SUIT_MPI_RAD_AREA_PATH')
402                    )
403                    if os.path.exists(app_mpi_hex_file):
404                        self.op_program(
405                            app_mpi_hex_file,
406                            'ERASE_NONE',
407                            None,
408                            defer=True,
409                            core='Application',
410                        )
411                    if os.path.exists(rad_mpi_hex_file):
412                        self.op_program(
413                            rad_mpi_hex_file,
414                            'ERASE_NONE',
415                            None,
416                            defer=True,
417                            core='Network',
418                        )
419
420                # Handle SUIT root manifest if application manifests are not used.
421                # If an application firmware is built, the root envelope is merged
422                # with other application manifests as well as the output HEX file.
423                if core != 'Application' and self.sysbuild_conf.get('SB_CONFIG_SUIT_ENVELOPE'):
424                    app_root_envelope_hex_file = os.fspath(
425                        mpi_hex_dir / 'suit_installed_envelopes_application_merged.hex'
426                    )
427                    if os.path.exists(app_root_envelope_hex_file):
428                        self.op_program(
429                            app_root_envelope_hex_file,
430                            'ERASE_NONE',
431                            None,
432                            defer=True,
433                            core='Application',
434                        )
435
436            if self.build_conf.getboolean("CONFIG_NRF_HALTIUM_GENERATE_UICR"):
437                zephyr_build_dir = Path(self.cfg.build_dir) / 'zephyr'
438
439                self.op_program(
440                    str(zephyr_build_dir / 'uicr.hex'),
441                    'ERASE_NONE',
442                    None,
443                    defer=True,
444                    core='Application',
445                )
446
447                if self.build_conf.getboolean("CONFIG_NRF_HALTIUM_UICR_PERIPHCONF"):
448                    self.op_program(
449                        str(zephyr_build_dir / 'periphconf.hex'),
450                        'ERASE_NONE',
451                        None,
452                        defer=True,
453                        core='Application',
454                    )
455
456            if not self.erase and regtool_generated_uicr:
457                self.exec_op('erase', core=core, kind='uicr')
458        else:
459            if self.erase:
460                erase_arg = 'ERASE_ALL'
461            elif self.family == 'nrf54l':
462                erase_arg = self._get_erase_mode(self.erase_mode) or 'ERASE_NONE'
463            else:
464                erase_arg = 'ERASE_RANGES_TOUCHED_BY_FIRMWARE'
465
466        xip_ranges = {
467            'nrf52': (0x12000000, 0x19FFFFFF),
468            'nrf53': (0x10000000, 0x1FFFFFFF),
469        }
470        ext_mem_erase_opt = None
471        if self.family in xip_ranges:
472            xip_start, xip_end = xip_ranges[self.family]
473            if self.hex_refers_region(xip_start, xip_end):
474                # Default to pages for the external memory
475                ext_mem_erase_opt = self._get_erase_mode(self.ext_erase_mode) or \
476                        (erase_arg if erase_arg == 'ERASE_ALL' else \
477                                    'ERASE_RANGES_TOUCHED_BY_FIRMWARE')
478
479        if not ext_mem_erase_opt and self.ext_erase_mode:
480            self.logger.warning('Option --ext-erase-mode ignored, no parts of the '
481                                'image refer to external memory')
482
483        self.logger.debug(f'Erase modes: chip:{erase_arg} ext_mem:'
484                          f'{ext_mem_erase_opt}')
485
486        # Temp hack while waiting for nrfutil Network support for NRF54H20 with IronSide
487        if self.family == 'nrf54h' and core == 'Network':
488            core = "Application"
489
490        self.op_program(self.hex_, erase_arg, ext_mem_erase_opt, defer=True, core=core)
491
492        if self.erase or self.recover:
493            # provision keys if keyfile.json exists in the build directory
494            keyfile = Path(self.cfg.build_dir).parent / 'keyfile.json'
495            if keyfile.exists():
496                self.logger.info(f'Provisioning key file: {keyfile}')
497                self.exec_op('x-provision-keys', keyfile=str(keyfile), defer=True)
498
499        self.flush(force=False)
500
501
502    def reset_target(self):
503        sw_reset = "RESET_HARD" if self.family in ('nrf54h', 'nrf92') else "RESET_SYSTEM"
504        # Default to soft reset on nRF52 only, because ICs in these series can
505        # reconfigure the reset pin as a regular GPIO
506        default = sw_reset if self.family == 'nrf52' else "RESET_PIN"
507        kind = (sw_reset if self.softreset else "RESET_PIN" if
508                self.pinreset else default)
509
510        if self.family == 'nrf52' and kind == "RESET_PIN":
511            # Write to the UICR enabling nRESET in the corresponding pin
512            self.exec_op('pinreset-enable')
513
514        self.logger.debug(f'Reset kind: {kind}')
515        self.exec_op('reset', kind=kind)
516
517    @abc.abstractmethod
518    def do_require(self):
519        ''' Ensure the tool is installed '''
520
521    def _check_suit_starter(self, op):
522        op = op['operation']
523        if op['type'] not in ('erase', 'recover', 'program'):
524            return None
525        if op['type'] == 'program' and op['options']['chip_erase_mode'] != "ERASE_UICR":
526            return None
527
528        file = _get_suit_starter()
529        self.logger.debug(f'suit starter: {file}')
530
531        return file
532
533    def op_program(self, hex_file, erase, ext_mem_erase, defer=False, core=None):
534        args = self._op_program(hex_file, erase, ext_mem_erase)
535        self.exec_op('program', defer, core, **args)
536
537    def _op_program(self, hex_file, erase, ext_mem_erase):
538        args = {'firmware': {'file': hex_file},
539                'options': {'chip_erase_mode': erase, 'verify': 'VERIFY_READ'}}
540        if ext_mem_erase:
541            args['options']['ext_mem_erase_mode'] = ext_mem_erase
542
543        return args
544
545    def exec_op(self, op, defer=False, core=None, **kwargs):
546
547        def _exec_op(op, defer=False, core=None, **kwargs):
548            _op = f'{op}'
549            op = {'operation': {'type': _op}}
550            if core:
551                op['core'] = core
552            op['operation'].update(kwargs)
553            self.logger.debug(f'defer: {defer} op: {op}')
554            if defer or not self.do_exec_op(op, force=False):
555                self.ops.append(op)
556            return op
557
558        _op = _exec_op(op, defer, core, **kwargs)
559        # Check if the suit manifest starter needs programming
560        if self.suit_starter and self.family == 'nrf54h':
561            file = self._check_suit_starter(_op)
562            if file:
563                args = self._op_program(file, 'ERASE_NONE', None)
564                _exec_op('program', defer, core, **args)
565
566    @abc.abstractmethod
567    def do_exec_op(self, op, force=False):
568        ''' Execute an operation. Return True if executed, False if not.
569            Throws subprocess.CalledProcessError with the appropriate
570            returncode if a failure arises.'''
571
572    def flush_ops(self, force=True):
573        ''' Execute any remaining ops in the self.ops array.
574            Throws subprocess.CalledProcessError with the appropriate
575            returncode if a failure arises.
576            Subclasses can override this method for special handling of
577            queued ops.'''
578        self.logger.debug('Flushing ops')
579        while self.ops:
580            self.do_exec_op(self.ops.popleft(), force)
581
582    def do_run(self, command, **kwargs):
583        self.do_require()
584
585        if self.softreset and self.pinreset:
586            raise RuntimeError('Options --softreset and --pinreset are mutually '
587                               'exclusive.')
588
589        if self.erase and self.erase_mode:
590            raise RuntimeError('Options --erase and --erase-mode are mutually '
591                               'exclusive.')
592
593        if self.erase and self.ext_erase_mode:
594            raise RuntimeError('Options --erase and --ext-erase-mode are mutually '
595                               'exclusive.')
596
597        self.ensure_family()
598
599        if self.family != 'nrf54l' and self.erase_mode:
600            raise RuntimeError('Option --erase-mode can only be used with the '
601                               'nRF54L family.')
602
603        self.ensure_output('hex')
604        if IntelHex is None:
605            raise RuntimeError('Python dependency intelhex was missing; '
606                               'see the getting started guide for details on '
607                               'how to fix')
608        self.hex_contents = IntelHex()
609        with contextlib.suppress(FileNotFoundError):
610            self.hex_contents.loadfile(self.hex_, format='hex')
611
612        self.ensure_snr()
613
614        self.ops = deque()
615
616        if self.recover:
617            self.recover_target()
618        self.program_hex()
619        if self.reset:
620            self.reset_target()
621        # All done, now flush any outstanding ops
622        self.flush(force=True)
623
624        self.logger.info(f'Board(s) with serial number(s) {self.dev_id} '
625                          'flashed successfully.')
626