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