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