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