Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for upgrading nRF51 soft device and bootloader #439

Merged
merged 10 commits into from
Jan 30, 2024
162 changes: 151 additions & 11 deletions cflib/bootloader/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@
__author__ = 'Bitcraze AB'
__all__ = ['Bootloader']

Target = namedtuple('Target', ['platform', 'target', 'type'])
FlashArtifact = namedtuple('FlashArtifact', ['content', 'target'])
Target = namedtuple('Target', ['platform', 'target', 'type', 'provides', 'requires'])
FlashArtifact = namedtuple('FlashArtifact', ['content', 'target', 'release'])


class Bootloader:
Expand Down Expand Up @@ -131,6 +131,51 @@ def start_bootloader(self, warm_boot=False, cf=None):
def get_target(self, target_id):
return self._cload.request_info_update(target_id)

def _get_current_nrf51_sd_version(self):
if self._cload.targets[TargetTypes.NRF51].start_page == 88:
return 'sd-s110'
elif self._cload.targets[TargetTypes.NRF51].start_page == 108:
return 'sd-s130'
else:
raise Exception('Unknown soft device running on nRF51')

def _get_required_nrf51_sd_version(self, flash_artifacts: List[FlashArtifact]):
required_nrf_sd_version = None
for a in flash_artifacts:
if a.target.target == 'nrf51':
for r in a.target.requires:
if required_nrf_sd_version is None:
required_nrf_sd_version = r
if required_nrf_sd_version != r:
raise Exception('Cannot flash nRF51, conflicting requirements: {} and {}'.format(
required_nrf_sd_version, r))

return required_nrf_sd_version

def _get_provided_nrf51_sd_version(self, flash_artifacts: List[FlashArtifact]):
provided_nrf_sd_version = None
for a in flash_artifacts:
if a.target.target == 'nrf51':
for r in a.target.provides:
if provided_nrf_sd_version is None:
provided_nrf_sd_version = r
if provided_nrf_sd_version != r:
raise Exception('Cannot flash nRF51, conflicting requirements: {} and {}'.format(
provided_nrf_sd_version, r))

return provided_nrf_sd_version

def _get_provided_nrf51_bl_version(self, flash_artifacts: List[FlashArtifact]):
provided_nrf_bl_version = None
for a in flash_artifacts:
if a.target.target == 'nrf51' and a.target.type == 'bootloader+softdevice':
if provided_nrf_bl_version is None:
provided_nrf_bl_version = a.release
else:
raise Exception('One and only one bootloader+softdevice in zip file supported')

return provided_nrf_bl_version

def flash(self, filename: str, targets: List[Target], cf=None, enable_console_log: Optional[bool] = False):
# Separate flash targets from decks
platform = self._get_platform_id()
Expand All @@ -142,14 +187,75 @@ def flash(self, filename: str, targets: List[Target], cf=None, enable_console_lo
if len(artifacts) == 0:
if len(targets) == 1:
content = open(filename, 'br').read()
artifacts = [FlashArtifact(content, targets[0])]
artifacts = [FlashArtifact(content, targets[0], None)]
else:
raise (Exception('Cannot flash a .bin to more than one target!'))

# Separate artifacts for flash and decks
flash_artifacts = [a for a in artifacts if a.target.platform == platform]
deck_artifacts = [a for a in artifacts if a.target.platform == 'deck']

# Handle the special case of flashing soft device and bootloader for nRF51
current_nrf_sd_version = self._get_current_nrf51_sd_version()
required_nrf_sd_version = self._get_required_nrf51_sd_version(flash_artifacts)
provided_nrf_sd_version = self._get_provided_nrf51_sd_version(flash_artifacts)
current_nrf_bl_version = None
if self._cload.targets[TargetTypes.NRF51].version is not None:
current_nrf_bl_version = str(self._cload.targets[TargetTypes.NRF51].version)
provided_nrf_bl_version = self._get_provided_nrf51_bl_version(flash_artifacts)

print('nRF51 has: {} and requires {} and upgrade provides {}. Current bootloader version is [{}] but upgrade '
'provides [{}]'.format(
current_nrf_sd_version, required_nrf_sd_version, provided_nrf_sd_version,
current_nrf_bl_version, provided_nrf_bl_version)
)

if required_nrf_sd_version is not None and \
current_nrf_sd_version != required_nrf_sd_version and \
provided_nrf_sd_version != required_nrf_sd_version:
raise Exception('Cannot flash nRF51: We have sd {}, need {} and have a zip with {}'.format(
current_nrf_sd_version, required_nrf_sd_version, provided_nrf_sd_version))

# To avoid always flashing the bootloader and soft device (these might never change again) first check
# if we really need to. The original version of the bootloader is reported as None.
should_flash_nrf_sd = True
if current_nrf_sd_version == required_nrf_sd_version and current_nrf_bl_version == provided_nrf_bl_version:
should_flash_nrf_sd = False
# elif provided_nrf_sd_version == None:
# should_flash_nrf_sd = False

if should_flash_nrf_sd:
print('Should flash nRF soft device')
rf51_sdbl_list = [x for x in flash_artifacts if x.target.type == 'bootloader+softdevice']
if len(rf51_sdbl_list) != 1:
raise Exception('Only support for one and only one bootloader+softdevice in zip file')
nrf51_sdbl = rf51_sdbl_list[0]

nrf_info = self._cload.targets[TargetTypes.NRF51]
# During bootloader update part of the firmware will be erased. If we're
# only flashing the bootloader and no firmware we should make sure the bootloader
# stays in bootloader mode and doesn't try to start the broken firmware, this is
# done by erasing the first page of the firmware.
self._internal_flash(FlashArtifact([0xFF] * nrf_info.page_size, nrf51_sdbl.target, None))
page = nrf_info.flash_pages - (len(nrf51_sdbl.content) // nrf_info.page_size)
self._internal_flash(artifact=nrf51_sdbl, page_override=page)

self._cload.reset_to_bootloader(TargetTypes.NRF51)
uri = self._cload.link.uri
self._cload.close()
print('Closing bootloader link and reconnecting to new bootloader (' + uri + ')')
self._cload = Cloader(uri,
info_cb=None,
in_boot_cb=None)
self._cload.open_bootloader_uri(uri)
print('Reconnected to new bootloader')
self._cload.check_link_and_get_info()
self._cload.request_info_update(TargetTypes.NRF51)

# Remove the softdevice+bootloader from the list of artifacts to flash
flash_artifacts = [a for a in flash_artifacts if a.target.type !=
'bootloader+softdevice'] # Also filter for nRF51 here?

# Flash the MCU flash
if len(targets) == 0 or len(flash_targets) > 0:
self._flash_flash(flash_artifacts, flash_targets)
Expand Down Expand Up @@ -232,14 +338,36 @@ def _get_flash_artifacts_from_zip(self, filename):
manifest = zf.read('manifest.json').decode('utf8')
manifest = json.loads(manifest)

if manifest['version'] != 1:
if manifest['version'] > 2:
raise Exception('Wrong manifest version')

print('Found manifest version: {}'.format(manifest['version']))

flash_artifacts = []
add_legacy_nRF51_s110 = False
for (file, metadata) in manifest['files'].items():
content = zf.read(file)
target = Target(metadata['platform'], metadata['target'], metadata['type'])
flash_artifacts.append(FlashArtifact(content, target))

# Handle version 1 of manifest where prerequisites for nRF soft-devices are not specified
requires = [] if 'requires' not in metadata else metadata['requires']
provides = [] if 'provides' not in metadata else metadata['provides']
if len(requires) == 0 and metadata['target'] == 'nrf51' and metadata['type'] == 'fw':
requires.append('sd-s110')
# If there is no requires for the nRF51 fw target then we also need the legacy s110
# so add this to the file list afterwards
add_legacy_nRF51_s110 = True

target = Target(metadata['platform'], metadata['target'], metadata['type'],
provides, requires)
flash_artifacts.append(FlashArtifact(content, target, metadata['release']))

if add_legacy_nRF51_s110:
print('Legacy format detected for manifest, adding s110+bl binary from distro')
from importlib.resources import files
content = files('resources.binaries').joinpath('nrf51-s110-and-bl.bin').read_bytes()
target = Target('cf2', 'nrf51', 'bootloader+softdevice', ['sd-s110'], [])
release = None
flash_artifacts.append(FlashArtifact(content, target, release))

return flash_artifacts

Expand All @@ -264,22 +392,28 @@ def close(self):
self._cload.close()
self._cload.link = None

def _internal_flash(self, artifact: FlashArtifact, current_file_number=1, total_files=1):
def _internal_flash(self, artifact: FlashArtifact, current_file_number=1, total_files=1, page_override=None):

target_info = self._cload.targets[TargetTypes.from_string(artifact.target.target)]

image = artifact.content
t_data = target_info

start_page = target_info.start_page
if page_override is not None:
start_page = page_override

# If used from a UI we need some extra things for reporting progress
factor = (100.0 * t_data.page_size) / len(image)
progress = 0

type_of_binary = 'Firmware'
if artifact.target.type == 'bootloader+softdevice':
type_of_binary = 'Bootloader+softdevice'

if self.progress_cb:
self.progress_cb(
'Firmware ({}/{}) Starting...'.format(current_file_number, total_files),
'{} ({}/{}) Starting...'.format(type_of_binary, current_file_number, total_files),
int(progress))
else:
sys.stdout.write(
Expand Down Expand Up @@ -322,7 +456,8 @@ def _internal_flash(self, artifact: FlashArtifact, current_file_number=1, total_

if self.progress_cb:
progress += factor
self.progress_cb('Firmware ({}/{}) Uploading buffer to {}...'.format(
self.progress_cb('{} ({}/{}) Uploading buffer to {}...'.format(
type_of_binary,
current_file_number,
total_files,
TargetTypes.to_string(t_data.id)),
Expand All @@ -335,7 +470,8 @@ def _internal_flash(self, artifact: FlashArtifact, current_file_number=1, total_
# Flash when the complete buffers are full
if ctr >= t_data.buffer_pages:
if self.progress_cb:
self.progress_cb('Firmware ({}/{}) Writing buffer to {}...'.format(
self.progress_cb('{} ({}/{}) Writing buffer to {}...'.format(
type_of_binary,
current_file_number,
total_files,
TargetTypes.to_string(t_data.id)),
Expand All @@ -362,7 +498,8 @@ def _internal_flash(self, artifact: FlashArtifact, current_file_number=1, total_

if ctr > 0:
if self.progress_cb:
self.progress_cb('Firmware ({}/{}) Writing buffer to {}...'.format(
self.progress_cb('{} ({}/{}) Writing buffer to {}...'.format(
type_of_binary,
current_file_number,
total_files,
TargetTypes.to_string(t_data.id)),
Expand All @@ -384,6 +521,9 @@ def _internal_flash(self, artifact: FlashArtifact, current_file_number=1, total_
' wrong radio link?' % self._cload.error_code)
raise Exception()

sys.stdout.write('\n')
sys.stdout.flush()

def _get_platform_id(self):
"""Get platform identifier used in the zip manifest for curr copter"""
identifier = 'cf1'
Expand Down
7 changes: 4 additions & 3 deletions cflib/bootloader/boottypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,16 +79,17 @@ def __init__(self, id):
self.buffer_pages = 0
self.flash_pages = 0
self.start_page = 0
self.version = None
self.cpuid = ''
self.data = None

def __str__(self):
ret = ''
ret += 'Target info: {} (0x{:X})\n'.format(
TargetTypes.to_string(self.id), self.id)
ret += 'Flash pages: %d | Page size: %d | Buffer pages: %d |' \
' Start page: %d\n' % (self.flash_pages, self.page_size,
self.buffer_pages, self.start_page)
ret += 'Flash pages: {} | Page size: {} | Buffer pages: {} |' \
' Start page: {} | Version: {} \n'.format(self.flash_pages, self.page_size,
self.buffer_pages, self.start_page, self.version)
ret += '%d KBytes of flash available for firmware image.' % (
(self.flash_pages - self.start_page) * self.page_size / 1024)
return ret
13 changes: 11 additions & 2 deletions cflib/bootloader/cloader.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,17 @@ def _update_info(self, target_id):
self.targets[target_id] = Target(target_id)
self.targets[target_id].addr = target_id
if len(answer.data) > 22:
self.targets[target_id].protocol_version = answer.datat[22]
self.protocol_version = answer.datat[22]
self.targets[target_id].protocol_version = answer.data[22]
self.protocol_version = answer.data[22]
if len(answer.data) > 23 and len(answer.data) > 26:
code_state = ''
if answer.data[24] & 0x80 != 0:
code_state = '+'
answer.data[24] &= 0x7F
major = struct.unpack('H', answer.data[23:25])[0]
minor = answer.data[25]
patch = answer.data[26]
self.targets[target_id].version = '{}.{}.{}{}'.format(major, minor, patch, code_state)
self.targets[target_id].page_size = tab[2]
self.targets[target_id].buffer_pages = tab[3]
self.targets[target_id].flash_pages = tab[4]
Expand Down
Binary file added resources/binaries/nrf51-s110-and-bl.bin
Binary file not shown.
13 changes: 13 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#!/usr/bin/env python3
import os
from glob import glob
from pathlib import Path

from setuptools import find_packages
Expand All @@ -7,6 +9,15 @@
directory = Path(__file__).parent
long_description = (directory / 'README.md').read_text()


def relative(lst, base=''):
return list(map(lambda x: base + os.path.basename(x), lst))


data_files = [
('binaries', glob('resources/binaries/*')),
]

setup(
name='cflib',
version='0.1.24',
Expand Down Expand Up @@ -45,4 +56,6 @@
'pre-commit'
],
},

data_files=data_files
)
Loading