From 6804b6f54fbed1f0fdb86668ebb46274e4876fa6 Mon Sep 17 00:00:00 2001 From: Andrew Leech Date: Tue, 5 Apr 2022 08:25:54 +1000 Subject: [PATCH] stm32/boards/NUCLEO_WB55: Add error handling to firmware update scripts. In-the-field use of these FUS/WS firmware update scripts has exposed some weak points, causing corrupted FUS/WS firmware to be flashed to the unit. The problems are mostly caused with the ST GUI application, but sometimes from un-recognised failures during bin file transfer to the WB55 prior to running the rfcore_firmware.py script. Other failures were caused by incorrect load addresses being used, again both from user error copying the address from the HTML release notes to the GUI tool, but also from similarly not updating the address correctly in rfcore_firmware.py To guard against these errors and make it easier to prepare different versions, this commit adds a few features to the rfcore firmware update tools: - When creating the bin file, automatically parse the release note in the folder to get the correct address. - Add a footer to the bin file containing the name, version, CRC, address etc. - Before flashing rfcore, check if the same version is already installed. - Verify the CRC and obfuscation key before flashing bin. - Log the name and version of file being flashed. --- .../boards/NUCLEO_WB55/rfcore_firmware.py | 249 ++++++++++++++---- .../boards/NUCLEO_WB55/rfcore_makefirmware.py | 116 +++++++- 2 files changed, 308 insertions(+), 57 deletions(-) diff --git a/ports/stm32/boards/NUCLEO_WB55/rfcore_firmware.py b/ports/stm32/boards/NUCLEO_WB55/rfcore_firmware.py index 4085da90fd..7cc81b4c68 100644 --- a/ports/stm32/boards/NUCLEO_WB55/rfcore_firmware.py +++ b/ports/stm32/boards/NUCLEO_WB55/rfcore_firmware.py @@ -34,36 +34,54 @@ # # To perform a firmware update: # -# 1. Generate "obfuscated" binary images using rfcore_makefirmware.py -# ./boards/NUCLEO_WB55/rfcore_makefirmware.py ~/src/github.com/STMicroelectronics/STM32CubeWB/Projects/STM32WB_Copro_Wireless_Binaries/STM32WB5x/ /tmp +# 1. Generate "obfuscated" binary images using rfcore_makefirmware.py, eg. +# $ python3 ./boards/NUCLEO_WB55/rfcore_makefirmware.py ~/src/github.com/STMicroelectronics/STM32CubeWB/Projects/STM32WB_Copro_Wireless_Binaries/STM32WB5x/ /tmp # This will generate /tmp/{fus_102,fus_110,ws_ble_hci}.bin +# It may warn that stm32wb5x_FUS_fw_1_0_2.bin cannot be found, newer packs don't include this +# which can be ignored unless your currently flashed FUS is older than 1.0.2 # # 2. Copy required files to the device filesystem. -# In general, it's always safe to copy all three files and the updater will -# figure out what needs to be done. This is the recommended option. -# However, if you already have the latest FUS (1.1.0) installed, then just the -# WS firmware is required. -# If a FUS binary is present, then the existing WS will be removed so it's a good -# idea to always include the WS binary if updating FUS. -# Note that a WS binary will not be installed unless FUS 1.1.0 is installed. +# $ mpremote cp /tmp/fus_102.bin : +# $ mpremote cp /tmp/fus_110.bin : +# $ mpremote cp /tmp/ws_ble_hci.bin : +# $ mpremote cp ./boards/NUCLEO_WB55/rfcore_firmware.py : +# In general, it's always safe to copy all three files and the updater will +# figure out what needs to be done. This is the recommended option. +# However, if you already have the latest FUS (1.1.0) installed, then just the +# WS firmware is required. +# If a FUS binary is present, then the existing WS will be removed so it's a good +# idea to always include the WS binary if updating FUS. +# Note that a WS binary will not be installed unless FUS 1.1.0 is installed. # # 3. Ensure boot.py calls `rfcore_firmware.resume()`. -# The WB55 will reset several times during the firmware update process, so this -# script manages the update state using RTC backup registers. -# `rfcore_firmware.resume()` will continue the update operation on startup to -# resume any in-progress update operation, and either trigger another reset, or -# return 0 to indicate that the operation completed successfully, or a reason -# code (see REASON_* below) to indicate failure. +# $ mpremote exec "import rfcore_firmware; rfcore_firmware.install_boot()" +# The WB55 will reset several times during the firmware update process, so this +# script manages the update state using RTC backup registers. +# `rfcore_firmware.resume()` will continue the update operation on startup to +# resume any in-progress update operation, and either trigger another reset, or +# return 0 to indicate that the operation completed successfully, or a reason +# code (see REASON_* below) to indicate failure. # # 4. Call rfcore_firmware.check_for_updates() to start the update process. -# The device will then immediately reboot and when the firmware update completes, -# the status will be returned from rfcore_firmware.resume(). See the REASON_ codes below. -# You can use the built-in stm.rfcore_fw_version() to query the installed version -# from your application code. +# $ mpremote exec "import rfcore_firmware; rfcore_firmware.check_for_updates()" +# The device will then immediately reboot and when the firmware update completes, +# the status will be returned from rfcore_firmware.resume(). See the REASON_ codes below. +# You can use the built-in stm.rfcore_fw_version() to query the installed version +# from your application code. import struct, os -import machine, stm -from micropython import const + +try: + import machine, stm + from ubinascii import crc32 + from micropython import const +except ImportError: + # cpython + from binascii import crc32 + + machine = stm = None + const = lambda x: x + _OGF_VENDOR = const(0x3F) @@ -174,13 +192,6 @@ _PATH_FUS_102 = "fus_102.bin" _PATH_FUS_110 = "fus_110.bin" _PATH_WS_BLE_HCI = "ws_ble_hci.bin" -# This address is correct for versions up to v1.8 (assuming existing firmware deleted). -# Note any address from the end of the filesystem to the SFSA would be fine, but if -# the FUS is fixed in the future to use the specified address then these are the "correct" -# ones. -_ADDR_FUS = 0x080EC000 -_ADDR_WS_BLE_HCI = 0x080DC000 - # When installing the FUS/WS it can take a long time to return to the first # GET_STATE HCI command. # e.g. Installing stm32wb5x_BLE_Stack_full_fw.bin takes 3600ms to respond. @@ -242,10 +253,79 @@ class _Flash: machine.mem32[stm.FLASH + stm.FLASH_CR] = 0 -def _copy_file_to_flash(filename, addr): +def validate_crc(f): + """Should match copy of function in rfcore_makefirmware.py to confirm operation""" + f.seek(0) + file_crc = 0 + chunk = 16 * 1024 + buff = bytearray(chunk) + while True: + read = f.readinto(buff) + if read < chunk: + file_crc = crc32(buff[0:read], file_crc) + break + file_crc = crc32(buff, file_crc) + + file_crc = 0xFFFFFFFF & -file_crc - 1 + f.seek(0) + return file_crc == 0 + + +def check_file_details(filename): + with open(filename, "rb") as f: + if not validate_crc(f): + raise ValueError("file validation failed: incorrect crc") + + # Check the footer on the file + f.seek(-64, 2) + footer = f.read() + details = struct.unpack("<37sIIIIbbbII", footer) + ( + src_filename, + addr_1m, + addr_640k, + addr_512k, + addr_256k, + vers_major, + vers_minor, + vers_patch, + KEY, + crc, + ) = details + src_filename = src_filename.strip(b"\x00").decode() + if KEY != _OBFUSCATION_KEY: + raise ValueError("file validation failed: incorrect key") + + return ( + src_filename, + addr_1m, + addr_640k, + addr_512k, + addr_256k, + (vers_major, vers_minor, vers_patch), + ) + + +def _copy_file_to_flash(filename): flash = _Flash() flash.unlock() + # Reset any previously stored address + _write_target_addr(0) try: + ( + src_filename, + addr_1m, + addr_640k, + addr_512k, + addr_256k, + vers, + ) = check_file_details(filename) + + # TODO add support for querying the correct flash size on chip + addr = load_addr = addr_1m + + log(f"Writing {src_filename} v{vers[0]}.{vers[1]}.{vers[2]} to addr: 0x{addr:x}") + # Erase the entire staging area in flash. erase_addr = STAGING_AREA_START sfr_sfsa = machine.mem32[stm.FLASH + stm.FLASH_SFR] & 0xFF @@ -266,6 +346,9 @@ def _copy_file_to_flash(filename, addr): flash.write(addr, buf, sz, _OBFUSCATION_KEY) addr += 4096 + # Cache the intended target load address + _write_target_addr(load_addr) + finally: flash.lock() @@ -308,17 +391,25 @@ def _fus_fwdelete(): return _run_sys_hci_cmd(_OGF_VENDOR, _OCF_FUS_FW_DELETE) -def _fus_run_fwupgrade(addr): +def _fus_run_fwupgrade(): # Note: Address is ignored by the FUS (see comments above). + addr = _read_target_addr() + if not addr: + log(f"Update failed: Invalid load address: 0x{addr:x}") + return False + + log(f"Loading to: 0x{addr:x}") return _run_sys_hci_cmd(_OGF_VENDOR, _OCF_FUS_FW_UPGRADE, struct.pack("= _FUS_VERSION_102 and fus_version < _FUS_VERSION_110: log("FUS 1.0.2 detected") - if _stat_and_start_copy( - _PATH_FUS_110, _ADDR_FUS, _STATE_COPYING_FUS, _STATE_COPIED_FUS - ): + if _stat_and_start_copy(_PATH_FUS_110, _STATE_COPYING_FUS, _STATE_COPIED_FUS): continue else: log("FUS is up-to-date") if fus_version >= _FUS_VERSION_110: if _stat_and_start_copy( - _PATH_WS_BLE_HCI, _ADDR_WS_BLE_HCI, _STATE_COPYING_WS, _STATE_COPIED_WS + _PATH_WS_BLE_HCI, + _STATE_COPYING_WS, + _STATE_COPIED_WS, ): continue else: @@ -465,7 +562,7 @@ def resume(): if fus_is_idle(): log("FUS copy complete, installing") _write_state(_STATE_INSTALLING_FUS) - _fus_run_fwupgrade(_ADDR_FUS) + _fus_run_fwupgrade() else: log("FUS copy bad state") _write_failure_state(REASON_FLASH_FUS_BAD_STATE) @@ -519,7 +616,7 @@ def resume(): if fus_is_idle(): log("WS copy complete, installing") _write_state(_STATE_INSTALLING_WS) - _fus_run_fwupgrade(_ADDR_WS_BLE_HCI) + _fus_run_fwupgrade() else: log("WS copy bad state") _write_failure_state(REASON_FLASH_WS_BAD_STATE) @@ -556,9 +653,59 @@ def resume(): _write_failure_state(REASON_WS_VENDOR + result) +def install_boot(): + boot_py = "/flash/boot.py" + header = "" + mode = "w" + try: + with open(boot_py, "r") as boot: + header = "\n" + mode = "a" + for line in boot: + if "rfcore_firmware.resume()" in line: + print("Already installed.") + return + + print("boot.py exists, adding upgrade handler.") + except OSError: + print("boot.py doesn't exists, adding with upgrade handler.") + + with open(boot_py, mode) as boot: + boot.write(header) + boot.write("# Handle rfcore updates.\n") + boot.write("import rfcore_firmware\n") + boot.write("rfcore_firmware.resume()\n") + + # Start a firmware update. # This will immediately trigger a reset and start the update process on boot. -def check_for_updates(): - log("Starting firmware update") - _write_state(_STATE_WAITING_FOR_FUS) - machine.reset() +def check_for_updates(force=False): + ( + src_filename, + addr_1m, + addr_640k, + addr_512k, + addr_256k, + vers_fus, + ) = check_file_details(_PATH_FUS_110) + ( + src_filename, + addr_1m, + addr_640k, + addr_512k, + addr_256k, + vers_ws, + ) = check_file_details(_PATH_WS_BLE_HCI) + current_version_fus = stm.rfcore_fw_version(_FW_VERSION_FUS) + fus_uptodate = current_version_fus[0:3] == vers_fus + + current_version_ws = stm.rfcore_fw_version(_FW_VERSION_WS) + ws_uptodate = current_version_ws[0:3] == vers_ws + if fus_uptodate and ws_uptodate and not force: + log(f"Already up to date: fus: {current_version_fus}, ws: {current_version_ws}") + else: + log(f"Starting firmware update") + log(f" - fus: {current_version_fus} -> {vers_fus}") + log(f" - ws: {current_version_ws} -> {vers_ws}") + _write_state(_STATE_WAITING_FOR_FUS) + machine.reset() diff --git a/ports/stm32/boards/NUCLEO_WB55/rfcore_makefirmware.py b/ports/stm32/boards/NUCLEO_WB55/rfcore_makefirmware.py index 23f3d20f0c..6b2fb60892 100755 --- a/ports/stm32/boards/NUCLEO_WB55/rfcore_makefirmware.py +++ b/ports/stm32/boards/NUCLEO_WB55/rfcore_makefirmware.py @@ -30,28 +30,70 @@ # rfcore_firmware.py as well as instructions on how to use. import os +import re import struct import sys +from binascii import crc32 +from rfcore_firmware import validate_crc, _OBFUSCATION_KEY -# Must match rfcore_firmware.py. -_OBFUSCATION_KEY = 0x0573B55AA _FIRMWARE_FILES = { "stm32wb5x_FUS_fw_1_0_2.bin": "fus_102.bin", "stm32wb5x_FUS_fw.bin": "fus_110.bin", "stm32wb5x_BLE_HCILayer_fw.bin": "ws_ble_hci.bin", } +_RELEASE_NOTES = "Release_Notes.html" + + +def get_details(release_notes, filename): + if not release_notes: + return None + file_details = re.findall( + rb"%s,(((0x[\d\S]+?),)+[vV][\d\.]+)[<,]" % filename.encode(), + release_notes, + flags=re.DOTALL, + ) + # The release note has all past version details also, but current is at top + latest_details = file_details[0][0].split(b",") + addr_1m, addr_640k, addr_512k, addr_256k, version = latest_details + addr_1m = int(addr_1m, 0) + addr_640k = int(addr_640k, 0) + addr_512k = int(addr_512k, 0) + addr_256k = int(addr_256k, 0) + version = [int(v) for v in version.lower().lstrip(b"v").split(b".")] + return addr_1m, addr_640k, addr_512k, addr_256k, version def main(src_path, dest_path): - for src_file, dest_file in _FIRMWARE_FILES.items(): - src_file = os.path.join(src_path, src_file) + + # Load the release note to parse for important details + with open(os.path.join(src_path, _RELEASE_NOTES), "rb") as f: + release_notes = f.read() + # remove some formatting + release_notes = re.sub(rb"", b"", release_notes) + # flatten tables + release_notes = re.sub( + rb"\W*\n*\W*", + b",", + release_notes.replace(b"", b"").replace(b"", b""), + ) + if ( + b"Wireless Coprocessor Binary,STM32WB5xxG(1M),STM32WB5xxY(640k),STM32WB5xxE(512K),STM32WB5xxC(256K),Version," + not in release_notes + ): + raise SystemExit( + "Cannot determine binary load address, please confirm Coprocessor folder / Release Notes format." + ) + + for src_filename, dest_file in _FIRMWARE_FILES.items(): + src_file = os.path.join(src_path, src_filename) dest_file = os.path.join(dest_path, dest_file) if not os.path.exists(src_file): print("Unable to find: {}".format(src_file)) continue sz = 0 with open(src_file, "rb") as src: + crc = 0 with open(dest_file, "wb") as dest: while True: b = src.read(4) @@ -59,9 +101,71 @@ def main(src_path, dest_path): break (v,) = struct.unpack("