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