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