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