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