1# Copyright (c) 2017 Linaro Limited.
2# Copyright (c) 2019 Nordic Semiconductor ASA.
3#
4# SPDX-License-Identifier: Apache-2.0
5
6'''Runner for flashing with nrfjprog.'''
7
8import os
9from pathlib import Path
10import shlex
11import subprocess
12import sys
13from re import fullmatch, escape
14
15from runners.core import ZephyrBinaryRunner, RunnerCaps
16
17try:
18    from intelhex import IntelHex
19except ImportError:
20    IntelHex = None
21
22# Helper function for inspecting hex files.
23# has_region returns True if hex file has any contents in a specific region
24# region_filter is a callable that takes an address as argument and
25# returns True if that address is in the region in question
26def has_region(regions, hex_file):
27    if IntelHex is None:
28        raise RuntimeError('one or more Python dependencies were missing; '
29                           "see the getting started guide for details on "
30                           "how to fix")
31
32    try:
33        ih = IntelHex(hex_file)
34        return any((len(ih[rs:re]) > 0) for (rs, re) in regions)
35    except FileNotFoundError:
36        return False
37
38# https://infocenter.nordicsemi.com/index.jsp?topic=%2Fug_nrf_cltools%2FUG%2Fcltools%2Fnrf_nrfjprogexe_return_codes.html&cp=9_1_3_1
39UnavailableOperationBecauseProtectionError = 16
40
41class NrfJprogBinaryRunner(ZephyrBinaryRunner):
42    '''Runner front-end for nrfjprog.'''
43
44    def __init__(self, cfg, family, softreset, snr, erase=False,
45                 tool_opt=[], force=False, recover=False):
46        super().__init__(cfg)
47        self.hex_ = cfg.hex_file
48        self.family = family
49        self.softreset = softreset
50        self.snr = snr
51        self.erase = bool(erase)
52        self.force = force
53        self.recover = bool(recover)
54
55        self.tool_opt = []
56        for opts in [shlex.split(opt) for opt in tool_opt]:
57            self.tool_opt += opts
58
59    @classmethod
60    def name(cls):
61        return 'nrfjprog'
62
63    @classmethod
64    def capabilities(cls):
65        return RunnerCaps(commands={'flash'}, erase=True)
66
67    @classmethod
68    def do_add_parser(cls, parser):
69        parser.add_argument('--nrf-family',
70                            choices=['NRF51', 'NRF52', 'NRF53', 'NRF91'],
71                            help='''MCU family; still accepted for
72                            compatibility only''')
73        parser.add_argument('--softreset', required=False,
74                            action='store_true',
75                            help='use reset instead of pinreset')
76        parser.add_argument('--snr', required=False,
77                            help="""Serial number of board to use.
78                            '*' matches one or more characters/digits.""")
79        parser.add_argument('--tool-opt', default=[], action='append',
80                            help='''Additional options for nrfjprog,
81                            e.g. "--recover"''')
82        parser.add_argument('--force', required=False,
83                            action='store_true',
84                            help='Flash even if the result cannot be guaranteed.')
85        parser.add_argument('--recover', required=False,
86                            action='store_true',
87                            help='''erase all user available non-volatile
88                            memory and disable read back protection before
89                            flashing (erases flash for both cores on nRF53)''')
90
91    @classmethod
92    def do_create(cls, cfg, args):
93        return NrfJprogBinaryRunner(cfg, args.nrf_family, args.softreset,
94                                    args.snr, erase=args.erase,
95                                    tool_opt=args.tool_opt, force=args.force,
96                                    recover=args.recover)
97
98    def ensure_snr(self):
99        if not self.snr or "*" in self.snr:
100            self.snr = self.get_board_snr(self.snr or "*")
101        self.snr = self.snr.lstrip("0")
102
103    def get_boards(self):
104        snrs = self.check_output(['nrfjprog', '--ids'])
105        snrs = snrs.decode(sys.getdefaultencoding()).strip().splitlines()
106        if not snrs:
107            raise RuntimeError('"nrfjprog --ids" did not find a board; '
108                               'is the board connected?')
109        return snrs
110
111    @staticmethod
112    def verify_snr(snr):
113        if snr == '0':
114            raise RuntimeError('"nrfjprog --ids" returned 0; '
115                                'is a debugger already connected?')
116
117    def get_board_snr(self, glob):
118        # Use nrfjprog --ids to discover connected boards.
119        #
120        # If there's exactly one board connected, it's safe to assume
121        # the user wants that one. Otherwise, bail unless there are
122        # multiple boards and we are connected to a terminal, in which
123        # case use print() and input() to ask what the user wants.
124
125        re_glob = escape(glob).replace(r"\*", ".+")
126        snrs = [snr for snr in self.get_boards() if fullmatch(re_glob, snr)]
127
128        if len(snrs) == 0:
129            raise RuntimeError(
130                'There are no boards connected{}.'.format(
131                        f" matching '{glob}'" if glob != "*" else ""))
132        elif len(snrs) == 1:
133            board_snr = snrs[0]
134            self.verify_snr(board_snr)
135            print("Using board {}".format(board_snr))
136            return board_snr
137        elif not sys.stdin.isatty():
138            raise RuntimeError(
139                f'refusing to guess which of {len(snrs)} '
140                'connected boards to use. (Interactive prompts '
141                'disabled since standard input is not a terminal.) '
142                'Please specify a serial number on the command line.')
143
144        snrs = sorted(snrs)
145        print('There are multiple boards connected{}.'.format(
146                        f" matching '{glob}'" if glob != "*" else ""))
147        for i, snr in enumerate(snrs, 1):
148            print('{}. {}'.format(i, snr))
149
150        p = 'Please select one with desired serial number (1-{}): '.format(
151                len(snrs))
152        while True:
153            try:
154                value = input(p)
155            except EOFError:
156                sys.exit(0)
157            try:
158                value = int(value)
159            except ValueError:
160                continue
161            if 1 <= value <= len(snrs):
162                break
163
164        return snrs[value - 1]
165
166    def ensure_family(self):
167        # Ensure self.family is set.
168
169        if self.family is not None:
170            return
171
172        if self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF51X'):
173            self.family = 'NRF51'
174        elif self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF52X'):
175            self.family = 'NRF52'
176        elif self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF53X'):
177            self.family = 'NRF53'
178        elif self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF91X'):
179            self.family = 'NRF91'
180        else:
181            raise RuntimeError(f'unknown nRF; update {__file__}')
182
183    def check_force_uicr(self):
184        # On SoCs without --sectoranduicrerase, we want to fail by
185        # default if the application contains UICR data and we're not sure
186        # that the flash will succeed.
187
188        # A map from SoCs which need this check to their UICR address
189        # ranges. If self.family isn't in here, do nothing.
190        uicr_ranges = {
191            'NRF53': ((0x00FF8000, 0x00FF8800),
192                      (0x01FF8000, 0x01FF8800)),
193            'NRF91': ((0x00FF8000, 0x00FF8800),),
194        }
195
196        if self.family not in uicr_ranges:
197            return
198
199        uicr = uicr_ranges[self.family]
200
201        if not self.uicr_data_ok and has_region(uicr, self.hex_):
202            # Hex file has UICR contents, and that's not OK.
203            raise RuntimeError(
204                'The hex file contains data placed in the UICR, which '
205                'needs a full erase before reprogramming. Run west '
206                'flash again with --force, --erase, or --recover.')
207
208    @property
209    def uicr_data_ok(self):
210        # True if it's OK to try to flash even with UICR data
211        # in the image; False otherwise.
212
213        return self.force or self.erase or self.recover
214
215    def recover_target(self):
216        if self.family == 'NRF53':
217            self.logger.info(
218                'Recovering and erasing flash memory for both the network '
219                'and application cores.')
220        else:
221            self.logger.info('Recovering and erasing all flash memory.')
222
223        if self.family == 'NRF53':
224            self.check_call(['nrfjprog', '--recover', '-f', self.family,
225                             '--coprocessor', 'CP_NETWORK',
226                             '--snr', self.snr])
227
228        self.check_call(['nrfjprog', '--recover',  '-f', self.family,
229                         '--snr', self.snr])
230
231    def program_hex(self):
232        # Get the nrfjprog command use to actually program self.hex_.
233        self.logger.info('Flashing file: {}'.format(self.hex_))
234
235        # What type of erase argument should we pass to nrfjprog?
236        if self.erase:
237            erase_arg = '--chiperase'
238        else:
239            if self.family == 'NRF52':
240                erase_arg = '--sectoranduicrerase'
241            else:
242                erase_arg = '--sectorerase'
243
244        # What nrfjprog commands do we need to flash this target?
245        program_commands = []
246        if self.family == 'NRF53':
247            # nRF53 requires special treatment due to the extra coprocessor.
248            self.program_hex_nrf53(erase_arg, program_commands)
249        else:
250            # It's important for tool_opt to come last, so it can override
251            # any options that we set here.
252            program_commands.append(['nrfjprog', '--program', self.hex_,
253                                     erase_arg, '-f', self.family,
254                                     '--snr', self.snr] +
255                                    self.tool_opt)
256
257        try:
258            for command in program_commands:
259                self.check_call(command)
260        except subprocess.CalledProcessError as cpe:
261            if cpe.returncode == UnavailableOperationBecauseProtectionError:
262                if self.family == 'NRF53':
263                    family_help = (
264                        '  Note: your target is an nRF53; all flash memory '
265                        'for both the network and application cores will be '
266                        'erased prior to reflashing.')
267                else:
268                    family_help = (
269                        '  Note: this will recover and erase all flash memory '
270                        'prior to reflashing.')
271                self.logger.error(
272                    'Flashing failed because the target '
273                    'must be recovered.\n'
274                    '  To fix, run "west flash --recover" instead.\n' +
275                    family_help)
276            raise
277
278    def program_hex_nrf53(self, erase_arg, program_commands):
279        # program_hex() helper for nRF53.
280
281        # *********************** NOTE *******************************
282        # self.hex_ can contain code for both the application core and
283        # the network core.
284        #
285        # We can't assume, for example, that
286        # CONFIG_SOC_NRF5340_CPUAPP=y means self.hex_ only contains
287        # data for the app core's flash: the user can put arbitrary
288        # addresses into one of the files in HEX_FILES_TO_MERGE.
289        #
290        # Therefore, on this family, we may need to generate two new
291        # hex files, one for each core, and flash them individually
292        # with the correct '--coprocessor' arguments.
293        #
294        # Kind of hacky, but it works, and nrfjprog is not capable of
295        # flashing to both cores at once. If self.hex_ only affects
296        # one core's flash, then we skip the extra work to save time.
297        # ************************************************************
298
299        def add_program_cmd(hex_file, coprocessor):
300            program_commands.append(
301                ['nrfjprog', '--program', hex_file, erase_arg,
302                 '-f', 'NRF53', '--snr', self.snr,
303                 '--coprocessor', coprocessor] + self.tool_opt)
304
305        full_hex = IntelHex()
306        full_hex.loadfile(self.hex_, format='hex')
307        min_addr, max_addr = full_hex.minaddr(), full_hex.maxaddr()
308
309        # Base address of network coprocessor's flash. From nRF5340
310        # OPS. We should get this from DTS instead if multiple values
311        # are possible, but this is fine for now.
312        net_base = 0x01000000
313
314        if min_addr < net_base <= max_addr:
315            net_hex, app_hex = IntelHex(), IntelHex()
316
317            for start, stop in full_hex.segments():
318                segment_hex = net_hex if start >= net_base else app_hex
319                segment_hex.merge(full_hex[start:stop])
320
321            hex_path = Path(self.hex_)
322            hex_dir, hex_name = hex_path.parent, hex_path.name
323
324            net_hex_file = os.fspath(hex_dir / f'GENERATED_CP_NETWORK_{hex_name}')
325            app_hex_file = os.fspath(
326                hex_dir / f'GENERATED_CP_APPLICATION_{hex_name}')
327
328            self.logger.info(
329                f'{self.hex_} targets both nRF53 coprocessors; '
330                f'splitting it into: {net_hex_file} and {app_hex_file}')
331
332            net_hex.write_hex_file(net_hex_file)
333            app_hex.write_hex_file(app_hex_file)
334
335            add_program_cmd(net_hex_file, 'CP_NETWORK')
336            add_program_cmd(app_hex_file, 'CP_APPLICATION')
337        else:
338            coprocessor = 'CP_NETWORK' if max_addr >= net_base else 'CP_APPLICATION'
339            add_program_cmd(self.hex_, coprocessor)
340
341    def reset_target(self):
342        if self.family == 'NRF52' and not self.softreset:
343            self.check_call(['nrfjprog', '--pinresetenable', '-f', self.family,
344                             '--snr', self.snr])  # Enable pin reset
345
346        if self.softreset:
347            self.check_call(['nrfjprog', '--reset', '-f', self.family,
348                             '--snr', self.snr])
349        else:
350            self.check_call(['nrfjprog', '--pinreset', '-f', self.family,
351                             '--snr', self.snr])
352
353    def do_run(self, command, **kwargs):
354        self.require('nrfjprog')
355
356        self.ensure_output('hex')
357        self.ensure_snr()
358        self.ensure_family()
359        self.check_force_uicr()
360
361        if self.recover:
362            self.recover_target()
363        self.program_hex()
364        self.reset_target()
365
366        self.logger.info(f'Board with serial number {self.snr} '
367                         'flashed successfully.')
368