You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
448 lines
17 KiB
448 lines
17 KiB
# Copyright (c) 2017 Linaro Limited. |
|
# Copyright (c) 2023 Nordic Semiconductor ASA. |
|
# |
|
# SPDX-License-Identifier: Apache-2.0 |
|
|
|
'''Runner base class for flashing with nrf tools.''' |
|
|
|
import abc |
|
from collections import deque |
|
import os |
|
from pathlib import Path |
|
import shlex |
|
import subprocess |
|
import sys |
|
from re import fullmatch, escape |
|
|
|
from runners.core import ZephyrBinaryRunner, RunnerCaps |
|
|
|
try: |
|
from intelhex import IntelHex |
|
except ImportError: |
|
IntelHex = None |
|
|
|
ErrNotAvailableBecauseProtection = 24 |
|
ErrVerify = 25 |
|
|
|
UICR_RANGES = { |
|
'NRF53_FAMILY': { |
|
'NRFDL_DEVICE_CORE_APPLICATION': (0x00FF8000, 0x00FF8800), |
|
'NRFDL_DEVICE_CORE_NETWORK': (0x01FF8000, 0x01FF8800), |
|
}, |
|
'NRF54H_FAMILY': { |
|
'NRFDL_DEVICE_CORE_APPLICATION': (0x0FFF8000, 0x0FFF8800), |
|
'NRFDL_DEVICE_CORE_NETWORK': (0x0FFFA000, 0x0FFFA800), |
|
}, |
|
'NRF91_FAMILY': { |
|
'NRFDL_DEVICE_CORE_APPLICATION': (0x00FF8000, 0x00FF8800), |
|
} |
|
} |
|
|
|
class NrfBinaryRunner(ZephyrBinaryRunner): |
|
'''Runner front-end base class for nrf tools.''' |
|
|
|
def __init__(self, cfg, family, softreset, dev_id, erase=False, |
|
reset=True, tool_opt=[], force=False, recover=False, |
|
erase_all_uicrs=False): |
|
super().__init__(cfg) |
|
self.hex_ = cfg.hex_file |
|
if family and not family.endswith('_FAMILY'): |
|
family = f'{family}_FAMILY' |
|
self.family = family |
|
self.softreset = softreset |
|
self.dev_id = dev_id |
|
self.erase = bool(erase) |
|
self.reset = bool(reset) |
|
self.force = force |
|
self.recover = bool(recover) |
|
self.erase_all_uicrs = bool(erase_all_uicrs) |
|
|
|
self.tool_opt = [] |
|
for opts in [shlex.split(opt) for opt in tool_opt]: |
|
self.tool_opt += opts |
|
|
|
@classmethod |
|
def capabilities(cls): |
|
return RunnerCaps(commands={'flash'}, dev_id=True, erase=True, |
|
reset=True, tool_opt=True) |
|
|
|
@classmethod |
|
def dev_id_help(cls) -> str: |
|
return '''Device identifier. Use it to select the J-Link Serial Number |
|
of the device connected over USB. '*' matches one or more |
|
characters/digits''' |
|
|
|
@classmethod |
|
def do_add_parser(cls, parser): |
|
parser.add_argument('--nrf-family', |
|
choices=['NRF51', 'NRF52', 'NRF53', 'NRF54L', |
|
'NRF54H', 'NRF91'], |
|
help='''MCU family; still accepted for |
|
compatibility only''') |
|
parser.add_argument('--softreset', required=False, |
|
action='store_true', |
|
help='use reset instead of pinreset') |
|
parser.add_argument('--snr', required=False, dest='dev_id', |
|
help='obsolete synonym for -i/--dev-id') |
|
parser.add_argument('--force', required=False, |
|
action='store_true', |
|
help='Flash even if the result cannot be guaranteed.') |
|
parser.add_argument('--recover', required=False, |
|
action='store_true', |
|
help='''erase all user available non-volatile |
|
memory and disable read back protection before |
|
flashing (erases flash for both cores on nRF53)''') |
|
parser.add_argument('--erase-all-uicrs', required=False, |
|
action='store_true', |
|
help='''Erase all UICR registers before flashing |
|
(nRF54H only). When not set, only UICR registers |
|
present in the hex file will be erased.''') |
|
|
|
parser.set_defaults(reset=True) |
|
|
|
def ensure_snr(self): |
|
if not self.dev_id or "*" in self.dev_id: |
|
self.dev_id = self.get_board_snr(self.dev_id or "*") |
|
self.dev_id = self.dev_id.lstrip("0") |
|
|
|
@abc.abstractmethod |
|
def do_get_boards(self): |
|
''' Return an array of Segger SNRs ''' |
|
|
|
def get_boards(self): |
|
snrs = self.do_get_boards() |
|
if not snrs: |
|
raise RuntimeError('Unable to find a board; ' |
|
'is the board connected?') |
|
return snrs |
|
|
|
@staticmethod |
|
def verify_snr(snr): |
|
if snr == '0': |
|
raise RuntimeError('The Segger SNR obtained is 0; ' |
|
'is a debugger already connected?') |
|
|
|
def get_board_snr(self, glob): |
|
# Use nrfjprog or nrfutil to discover connected boards. |
|
# |
|
# If there's exactly one board connected, it's safe to assume |
|
# the user wants that one. Otherwise, bail unless there are |
|
# multiple boards and we are connected to a terminal, in which |
|
# case use print() and input() to ask what the user wants. |
|
|
|
re_glob = escape(glob).replace(r"\*", ".+") |
|
snrs = [snr for snr in self.get_boards() if fullmatch(re_glob, snr)] |
|
|
|
if len(snrs) == 0: |
|
raise RuntimeError( |
|
'There are no boards connected{}.'.format( |
|
f" matching '{glob}'" if glob != "*" else "")) |
|
elif len(snrs) == 1: |
|
board_snr = snrs[0] |
|
self.verify_snr(board_snr) |
|
print("Using board {}".format(board_snr)) |
|
return board_snr |
|
elif not sys.stdin.isatty(): |
|
raise RuntimeError( |
|
f'refusing to guess which of {len(snrs)} ' |
|
'connected boards to use. (Interactive prompts ' |
|
'disabled since standard input is not a terminal.) ' |
|
'Please specify a serial number on the command line.') |
|
|
|
snrs = sorted(snrs) |
|
print('There are multiple boards connected{}.'.format( |
|
f" matching '{glob}'" if glob != "*" else "")) |
|
for i, snr in enumerate(snrs, 1): |
|
print('{}. {}'.format(i, snr)) |
|
|
|
p = 'Please select one with desired serial number (1-{}): '.format( |
|
len(snrs)) |
|
while True: |
|
try: |
|
value = input(p) |
|
except EOFError: |
|
sys.exit(0) |
|
try: |
|
value = int(value) |
|
except ValueError: |
|
continue |
|
if 1 <= value <= len(snrs): |
|
break |
|
|
|
return snrs[value - 1] |
|
|
|
def ensure_family(self): |
|
# Ensure self.family is set. |
|
|
|
if self.family is not None: |
|
return |
|
|
|
if self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF51X'): |
|
self.family = 'NRF51_FAMILY' |
|
elif self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF52X'): |
|
self.family = 'NRF52_FAMILY' |
|
elif self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF53X'): |
|
self.family = 'NRF53_FAMILY' |
|
elif self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF54LX'): |
|
self.family = 'NRF54L_FAMILY' |
|
elif self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF54HX'): |
|
self.family = 'NRF54H_FAMILY' |
|
elif self.build_conf.getboolean('CONFIG_SOC_SERIES_NRF91X'): |
|
self.family = 'NRF91_FAMILY' |
|
else: |
|
raise RuntimeError(f'unknown nRF; update {__file__}') |
|
|
|
def hex_refers_region(self, region_start, region_end): |
|
for segment_start, _ in self.hex_contents.segments(): |
|
if region_start <= segment_start <= region_end: |
|
return True |
|
return False |
|
|
|
def hex_get_uicrs(self): |
|
hex_uicrs = {} |
|
|
|
if self.family in UICR_RANGES: |
|
for uicr_core, uicr_range in UICR_RANGES[self.family].items(): |
|
if self.hex_refers_region(*uicr_range): |
|
hex_uicrs[uicr_core] = uicr_range |
|
|
|
return hex_uicrs |
|
|
|
def flush(self, force=False): |
|
try: |
|
self.flush_ops(force=force) |
|
except subprocess.CalledProcessError as cpe: |
|
if cpe.returncode == ErrNotAvailableBecauseProtection: |
|
if self.family == 'NRF53_FAMILY': |
|
family_help = ( |
|
' Note: your target is an nRF53; all flash memory ' |
|
'for both the network and application cores will be ' |
|
'erased prior to reflashing.') |
|
else: |
|
family_help = ( |
|
' Note: this will recover and erase all flash memory ' |
|
'prior to reflashing.') |
|
self.logger.error( |
|
'Flashing failed because the target ' |
|
'must be recovered.\n' |
|
' To fix, run "west flash --recover" instead.\n' + |
|
family_help) |
|
if cpe.returncode == ErrVerify: |
|
# If there are data in the UICR region it is likely that the |
|
# verify failed du to the UICR not been erased before, so giving |
|
# a warning here will hopefully enhance UX. |
|
if self.hex_get_uicrs(): |
|
self.logger.warning( |
|
'The hex file contains data placed in the UICR, which ' |
|
'may require a full erase before reprogramming. Run ' |
|
'west flash again with --erase, or --recover.') |
|
raise |
|
|
|
|
|
def recover_target(self): |
|
if self.family == 'NRF53_FAMILY': |
|
self.logger.info( |
|
'Recovering and erasing flash memory for both the network ' |
|
'and application cores.') |
|
else: |
|
self.logger.info('Recovering and erasing all flash memory.') |
|
|
|
# The network core needs to be recovered first due to the fact that |
|
# recovering it erases the flash of *both* cores. Since a recover |
|
# operation unlocks the core and then flashes a small image that keeps |
|
# the debug access port open, recovering the network core last would |
|
# result in that small image being deleted from the app core. |
|
if self.family == 'NRF53_FAMILY': |
|
self.exec_op('recover', core='NRFDL_DEVICE_CORE_NETWORK') |
|
|
|
self.exec_op('recover') |
|
|
|
def program_hex(self): |
|
# Get the command use to actually program self.hex_. |
|
self.logger.info('Flashing file: {}'.format(self.hex_)) |
|
|
|
# What type of erase argument should we pass to the tool? |
|
if self.erase: |
|
erase_arg = 'ERASE_ALL' |
|
else: |
|
if self.family == 'NRF52_FAMILY': |
|
erase_arg = 'ERASE_PAGES_INCLUDING_UICR' |
|
else: |
|
erase_arg = 'ERASE_PAGES' |
|
|
|
xip_ranges = { |
|
'NRF52_FAMILY': (0x12000000, 0x19FFFFFF), |
|
'NRF53_FAMILY': (0x10000000, 0x1FFFFFFF), |
|
} |
|
qspi_erase_opt = None |
|
if self.family in xip_ranges: |
|
xip_start, xip_end = xip_ranges[self.family] |
|
if self.hex_refers_region(xip_start, xip_end): |
|
qspi_erase_opt = 'ERASE_ALL' |
|
|
|
# What tool commands do we need to flash this target? |
|
if self.family == 'NRF53_FAMILY': |
|
# nRF53 requires special treatment due to the extra coprocessor. |
|
self.program_hex_nrf53(erase_arg, qspi_erase_opt) |
|
elif self.family == 'NRF54H_FAMILY': |
|
self.program_hex_nrf54h() |
|
else: |
|
self.op_program(self.hex_, erase_arg, qspi_erase_opt, defer=True) |
|
|
|
self.flush(force=False) |
|
|
|
def program_hex_nrf54h(self): |
|
if self.erase_all_uicrs: |
|
uicrs = UICR_RANGES['NRF54H_FAMILY'] |
|
else: |
|
uicrs = self.hex_get_uicrs() |
|
|
|
for uicr_core, range in uicrs.items(): |
|
self.exec_op('erasepage', defer=True, core=uicr_core, page=range[0]) |
|
|
|
self.op_program(self.hex_, 'NO_ERASE', None, defer=True) |
|
|
|
def program_hex_nrf53(self, erase_arg, qspi_erase_opt): |
|
# program_hex() helper for nRF53. |
|
|
|
# *********************** NOTE ******************************* |
|
# self.hex_ can contain code for both the application core and |
|
# the network core. |
|
# |
|
# We can't assume, for example, that |
|
# CONFIG_SOC_NRF5340_CPUAPP=y means self.hex_ only contains |
|
# data for the app core's flash: the user can put arbitrary |
|
# addresses into one of the files in HEX_FILES_TO_MERGE. |
|
# |
|
# Therefore, on this family, we may need to generate two new |
|
# hex files, one for each core, and flash them individually |
|
# with the correct '--coprocessor' arguments. |
|
# |
|
# Kind of hacky, but it works, and the tools are not capable of |
|
# flashing to both cores at once. If self.hex_ only affects |
|
# one core's flash, then we skip the extra work to save time. |
|
# ************************************************************ |
|
|
|
# Address range of the network coprocessor's flash. From nRF5340 OPS. |
|
# We should get this from DTS instead if multiple values are possible, |
|
# but this is fine for now. |
|
net_flash_start = 0x01000000 |
|
net_flash_end = 0x0103FFFF |
|
|
|
# If there is nothing in the hex file for the network core, |
|
# only the application core is programmed. |
|
if not self.hex_refers_region(net_flash_start, net_flash_end): |
|
self.op_program(self.hex_, erase_arg, qspi_erase_opt, defer=True, |
|
core='NRFDL_DEVICE_CORE_APPLICATION') |
|
# If there is some content that addresses a region beyond the network |
|
# core flash range, two hex files are generated and the two cores |
|
# are programmed one by one. |
|
elif self.hex_contents.minaddr() < net_flash_start or \ |
|
self.hex_contents.maxaddr() > net_flash_end: |
|
|
|
net_hex, app_hex = IntelHex(), IntelHex() |
|
for start, end in self.hex_contents.segments(): |
|
if net_flash_start <= start <= net_flash_end: |
|
net_hex.merge(self.hex_contents[start:end]) |
|
else: |
|
app_hex.merge(self.hex_contents[start:end]) |
|
|
|
hex_path = Path(self.hex_) |
|
hex_dir, hex_name = hex_path.parent, hex_path.name |
|
|
|
net_hex_file = os.fspath( |
|
hex_dir / f'GENERATED_CP_NETWORK_{hex_name}') |
|
app_hex_file = os.fspath( |
|
hex_dir / f'GENERATED_CP_APPLICATION_{hex_name}') |
|
|
|
self.logger.info( |
|
f'{self.hex_} targets both nRF53 coprocessors; ' |
|
f'splitting it into: {net_hex_file} and {app_hex_file}') |
|
|
|
net_hex.write_hex_file(net_hex_file) |
|
app_hex.write_hex_file(app_hex_file) |
|
|
|
self.op_program(net_hex_file, erase_arg, None, defer=True, |
|
core='NRFDL_DEVICE_CORE_NETWORK') |
|
self.op_program(app_hex_file, erase_arg, qspi_erase_opt, defer=True, |
|
core='NRFDL_DEVICE_CORE_APPLICATION') |
|
# Otherwise, only the network core is programmed. |
|
else: |
|
self.op_program(self.hex_, erase_arg, None, defer=True, |
|
core='NRFDL_DEVICE_CORE_NETWORK') |
|
|
|
def reset_target(self): |
|
if self.family == 'NRF52_FAMILY' and not self.softreset: |
|
self.exec_op('pinreset-enable') |
|
|
|
if self.softreset: |
|
self.exec_op('reset', option="RESET_SYSTEM") |
|
else: |
|
self.exec_op('reset', option="RESET_PIN") |
|
|
|
@abc.abstractmethod |
|
def do_require(self): |
|
''' Ensure the tool is installed ''' |
|
|
|
def op_program(self, hex_file, erase, qspi_erase, defer=False, core=None): |
|
args = {'firmware': {'file': hex_file, 'format': 'NRFDL_FW_INTEL_HEX'}, |
|
'chip_erase_mode': erase, 'verify': 'VERIFY_READ'} |
|
if qspi_erase: |
|
args['qspi_erase_mode'] = qspi_erase |
|
self.exec_op('program', defer, core, **args) |
|
|
|
def exec_op(self, op, defer=False, core=None, **kwargs): |
|
_op = f'{op}' |
|
op = {'operation': {'type': _op}} |
|
if core: |
|
op['core'] = core |
|
op['operation'].update(kwargs) |
|
self.logger.debug(f'defer: {defer} op: {op}') |
|
if defer or not self.do_exec_op(op, force=False): |
|
self.ops.append(op) |
|
|
|
@abc.abstractmethod |
|
def do_exec_op(self, op, force=False): |
|
''' Execute an operation. Return True if executed, False if not. |
|
Throws subprocess.CalledProcessError with the appropriate |
|
returncode if a failure arises.''' |
|
|
|
def flush_ops(self, force=True): |
|
''' Execute any remaining ops in the self.ops array. |
|
Throws subprocess.CalledProcessError with the appropriate |
|
returncode if a failure arises. |
|
Subclasses can override this method for special handling of |
|
queued ops.''' |
|
self.logger.debug('Flushing ops') |
|
while self.ops: |
|
self.do_exec_op(self.ops.popleft(), force) |
|
|
|
def do_run(self, command, **kwargs): |
|
self.do_require() |
|
|
|
self.ensure_output('hex') |
|
if IntelHex is None: |
|
raise RuntimeError('one or more Python dependencies were missing; ' |
|
'see the getting started guide for details on ' |
|
'how to fix') |
|
self.hex_contents = IntelHex() |
|
try: |
|
self.hex_contents.loadfile(self.hex_, format='hex') |
|
except FileNotFoundError: |
|
pass |
|
|
|
self.ensure_snr() |
|
self.ensure_family() |
|
|
|
self.ops = deque() |
|
|
|
if self.recover: |
|
self.recover_target() |
|
self.program_hex() |
|
if self.reset: |
|
self.reset_target() |
|
# All done, now flush any outstanding ops |
|
self.flush(force=True) |
|
|
|
self.logger.info(f'Board with serial number {self.dev_id} ' |
|
'flashed successfully.')
|
|
|