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