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