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