diff --git a/boards.txt b/boards.txt index 798564387..36a536090 100644 --- a/boards.txt +++ b/boards.txt @@ -72,6 +72,9 @@ giga.debug.server.openocd.scripts.2=target/stm32h7x_dual_bank.cfg giga.debug.cortex-debug.custom.request=attach giga.debug.svd_file={runtime.platform.path}/svd/STM32H747_CM7.svd +giga.build.ota.magic=0x23410266 +giga.build.ota.sketch_offset=0xA0000 + ########################################################################################## nano33ble.name=Arduino Nano 33 BLE @@ -256,6 +259,9 @@ portentah7.bootloader.interface=0 portentah7.bootloader.file=zephyr-{build.variant}.bin portentah7.bootloader.address=0x08040000 +portentah7.build.ota.magic=0x2341025B +portentah7.build.ota.sketch_offset=0xA0000 + portentah7.debug.tool=gdb portentah7.debug.server.openocd.scripts.0=interface/{programmer.protocol}.cfg portentah7.debug.server.openocd.scripts.1={programmer.transport_script} @@ -308,6 +314,9 @@ nicla_vision.bootloader.interface=0 nicla_vision.bootloader.file=zephyr-{build.variant}.bin nicla_vision.bootloader.address=0x08040000 +nicla_vision.build.ota.magic=0x2341025F +nicla_vision.build.ota.sketch_offset=0xA0000 + nicla_vision.debug.tool=gdb nicla_vision.debug.server.openocd.scripts.0=interface/{programmer.protocol}.cfg nicla_vision.debug.server.openocd.scripts.1={programmer.transport_script} @@ -445,9 +454,12 @@ portentac33.upload.wait_for_upload_port=false portentac33.upload.native_usb=true portentac33.upload.dfuse= +portentac33.sfu.file=loader/blobs/c33_sfu.bin +portentac33.sfu.size=0x20000 + portentac33.bootloader.tool=dfu-util portentac33.bootloader.tool.default=dfu-util -portentac33.bootloader.file=zephyr-{build.variant}.bin +portentac33.bootloader.file=zephyr-{build.variant}-sfu.bin portentac33.bootloader.interface=0 portentac33.bootloader.address=0x10000 portentac33.bootloader.pid=0x0368 @@ -458,6 +470,9 @@ portentac33.leave.dfuse={empty} -Q portentac33.loader.args.with_sketch={empty} -w portentac33.sketch.args.without_loader={empty} -w +portentac33.build.ota.magic=0x23410068 +portentac33.build.ota.sketch_offset=0xF0000 + ########################################################################################## opta.name=Arduino Opta @@ -510,6 +525,9 @@ opta.debug.server.openocd.scripts.2=target/stm32h7x_dual_bank.cfg opta.debug.cortex-debug.custom.request=attach opta.debug.svd_file={runtime.platform.path}/svd/STM32H747_CM7.svd +opta.build.ota.magic=0x23410064 +opta.build.ota.sketch_offset=0xA0000 + ########################################################################################## nano_matter.name=Arduino Nano Matter diff --git a/cores/arduino/llext_wrappers.c b/cores/arduino/llext_wrappers.c index 0ea624939..8168076c8 100644 --- a/cores/arduino/llext_wrappers.c +++ b/cores/arduino/llext_wrappers.c @@ -109,6 +109,7 @@ W2(char *, strstr, const char *, const char *) W2(int, strcmp, const char *, const char *) W3(int, strncmp, const char *, const char *, size_t) W2(int, strcasecmp, const char *, const char *) +W3(int, strncasecmp, const char *, const char *, size_t) W3(char *, strncpy, char *, const char *, size_t) W2(char *, strcat, char *, const char *) W2(char *, strcpy, char *, const char *) @@ -216,6 +217,7 @@ VN(__aeabi_fcmpun) VN(__aeabi_d2iz) VN(__aeabi_d2uiz) VN(__aeabi_d2lz) +VN(__aeabi_d2ulz) VN(__aeabi_i2d) VN(__aeabi_ui2d) VN(__aeabi_l2d) diff --git a/cores/arduino/ota_api.h b/cores/arduino/ota_api.h new file mode 100644 index 000000000..e97bf3460 --- /dev/null +++ b/cores/arduino/ota_api.h @@ -0,0 +1,11 @@ +/* + * Copyright (c) Arduino s.r.l. and/or its affiliated companies + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifdef CONFIG_OTA + +#include "../../loader/ota/ota_api.h" + +#endif // CONFIG_OTA diff --git a/extra/build.sh b/extra/build.sh index 4678b134a..cbaf42cf6 100755 --- a/extra/build.sh +++ b/extra/build.sh @@ -57,6 +57,10 @@ if ! [ -z "$chosen_board" ]; then if [ x$2 == x"--debug" ]; then args+=(-- -DEXTRA_CONF_FILE=../extra/debug.conf) fi + + # check if the board requires to prepend a second stage bootloader to the loader + sfu_file=$(jq -cr '.sfu_file' <<< "$chosen_board") + sfu_size=$(jq -cr '.sfu_size' <<< "$chosen_board") else # expect Zephyr-compatible target and args target=$1 @@ -125,6 +129,17 @@ done cp ${BUILD_DIR}/zephyr/zephyr.dts firmwares/zephyr-$variant.dts cp ${BUILD_DIR}/zephyr/.config firmwares/zephyr-$variant.config +# Some boards, like the C33, need a second stage bootloader to enable OTA upgrades, +# if the file is specified in boards.txt under .sfu.file, prepend it to the loader binary into a +# firmwares/zephyr-$variant-sfu.bin file. +# this file needs to be specified in bootloader.file=zephyr-{build.variant}-sfu.bin +if [ "$sfu_file" ] ; then + dd if=${sfu_file} of=firmwares/zephyr-$variant-sfu.bin conv=notrunc status=none + + offset=$(printf "%d" "${sfu_size}") + dd if=firmwares/zephyr-$variant.bin of=firmwares/zephyr-$variant-sfu.bin bs=1 seek=${offset} conv=notrunc status=none +fi + # Generate the provides.ld file for linked builds echo "Generating exported symbol scripts" extra/gen_provides.py "${BUILD_DIR}/zephyr/zephyr.elf" -T > ${VARIANT_DIR}/tls-syms.S diff --git a/extra/get_board_details.sh b/extra/get_board_details.sh index a70d8b8b0..94efb8696 100755 --- a/extra/get_board_details.sh +++ b/extra/get_board_details.sh @@ -24,6 +24,8 @@ for BOARD in $(get_boards); do ARTIFACT=$(get_board_field $BOARD "build\\.artifact") ARTIFACT=${ARTIFACT:-zephyr_contrib} UPLOAD_OFFSET=$(get_board_field $BOARD "upload\\.offset") + SFU_FILE=$(get_board_field $BOARD "sfu\\.file") + SFU_SIZE=$(get_board_field $BOARD "sfu\\.size") ARTIFACT_JSON=extra/artifacts/$ARTIFACT.json if ! [ -f "$ARTIFACT_JSON" ] ; then @@ -51,7 +53,9 @@ for BOARD in $(get_boards); do "hals": "$HALS", "artifact": "$ARTIFACT", "subarch": "$SUBARCH", - "upload_offset": "$UPLOAD_OFFSET" + "upload_offset": "$UPLOAD_OFFSET", + "sfu_file": "$SFU_FILE", + "sfu_size": "$SFU_SIZE" } EOF done | jq -crs . diff --git a/loader/CMakeLists.txt b/loader/CMakeLists.txt index 5e6f2c9f2..d132ba12d 100644 --- a/loader/CMakeLists.txt +++ b/loader/CMakeLists.txt @@ -18,6 +18,7 @@ find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE}) project(app LANGUAGES C CXX) add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/blobs) +add_subdirectory_ifdef(CONFIG_OTA ${CMAKE_CURRENT_SOURCE_DIR}/ota) # Memory region relocation based on Kconfig # Relocate LLEXT heap if region specified @@ -44,3 +45,4 @@ target_compile_definitions(app PUBLIC _XOPEN_SOURCE=700) target_compile_definitions(app PUBLIC _GNU_SOURCE) target_link_libraries(app PUBLIC blobs) +target_link_libraries_ifdef(CONFIG_OTA app PUBLIC ota) diff --git a/loader/Kconfig b/loader/Kconfig index 9965f3757..31b291958 100644 --- a/loader/Kconfig +++ b/loader/Kconfig @@ -23,3 +23,5 @@ config MAIN_STACK_REGION depends on CODE_DATA_RELOCATION help Specify the memory region for main stack. + +rsource "ota/Kconfig" diff --git a/loader/blobs/c33_sfu.bin b/loader/blobs/c33_sfu.bin new file mode 100644 index 000000000..52732e055 Binary files /dev/null and b/loader/blobs/c33_sfu.bin differ diff --git a/loader/llext_exports.c b/loader/llext_exports.c index 4c2c6e8a9..587b41950 100644 --- a/loader/llext_exports.c +++ b/loader/llext_exports.c @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -53,6 +54,7 @@ EXPORT_LIBC_SYM(strstr); EXPORT_LIBC_SYM(strncmp); EXPORT_LIBC_SYM(strncpy); EXPORT_LIBC_SYM(strcasecmp); +EXPORT_LIBC_SYM(strncasecmp); EXPORT_LIBC_SYM(strcmp); EXPORT_LIBC_SYM(strlen); EXPORT_LIBC_SYM(strnlen); @@ -349,6 +351,7 @@ EXPORT_LIBC_SYM(gettimeofday); #endif EXPORT_SYMBOL(sys_clock_settime); + EXPORT_SYMBOL(mktime); EXPORT_SYMBOL(gmtime); @@ -409,6 +412,7 @@ EXPORT_AEABI_SYM(__aeabi_fcmpun); EXPORT_AEABI_SYM(__aeabi_d2iz); EXPORT_AEABI_SYM(__aeabi_d2uiz); EXPORT_AEABI_SYM(__aeabi_d2lz); +EXPORT_AEABI_SYM(__aeabi_d2ulz); EXPORT_AEABI_SYM(__aeabi_i2d); EXPORT_AEABI_SYM(__aeabi_ui2d); EXPORT_AEABI_SYM(__aeabi_l2d); diff --git a/loader/main.c b/loader/main.c index 0947559ea..c8fa42fa2 100644 --- a/loader/main.c +++ b/loader/main.c @@ -24,6 +24,7 @@ LOG_MODULE_REGISTER(sketch); #include #include "../cores/arduino/zephyr_sketch_header.h" +#include "ota/ota_sketch_check.h" #define SKETCH_RAM_BUFFER_LEN 131072 @@ -136,6 +137,10 @@ static int loader(const struct shell *sh) { return rc; } +#if defined(CONFIG_OTA) + try_ota_update(fa); +#endif + uintptr_t base_addr = DT_PARTITION_ADDR(DT_NODELABEL(user_sketch)); char header[SKETCH_HEADER_LEN]; diff --git a/loader/ota/CMakeLists.txt b/loader/ota/CMakeLists.txt new file mode 100644 index 000000000..079b7c8b2 --- /dev/null +++ b/loader/ota/CMakeLists.txt @@ -0,0 +1,15 @@ +# SPDX-License-Identifier: Apache-2.0 + +zephyr_interface_library_named(ota) + +target_sources(app PRIVATE ${CMAKE_CURRENT_LIST_DIR}/ota_sketch_check.c) +target_sources(app PRIVATE ${CMAKE_CURRENT_LIST_DIR}/ota_llext_exports.c) +zephyr_include_directories(${CMAKE_CURRENT_LIST_DIR}) + +target_sources_ifdef(CONFIG_BOARD_ARDUINO_PORTENTA_C33 app PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/ota_portentac33.c) + +target_sources_ifdef(CONFIG_SOC_SERIES_STM32H7X app PRIVATE + ${CMAKE_CURRENT_LIST_DIR}/ota_arduino_stm32h7.c) + +target_sources(app PRIVATE ${CMAKE_CURRENT_LIST_DIR}/ota_sketch.c) diff --git a/loader/ota/Kconfig b/loader/ota/Kconfig new file mode 100644 index 000000000..89b68bc63 --- /dev/null +++ b/loader/ota/Kconfig @@ -0,0 +1,58 @@ +# +# Copyright (c) Arduino s.r.l. and/or its affiliated companies +# +# SPDX-License-Identifier: Apache-2.0 +# + +menuconfig OTA + bool "Enable lookup for sketch update at boot" + depends on FILE_SYSTEM + select REBOOT + select RTC if SOC_SERIES_STM32H7X + default n + help + Enable checking for an update inside of the filesystem. If a file inside of + OTA_SKETCH_UPDATE_PATH is found during the boot, the OTA procedure is started and the + current sketch binary is replaced + +if OTA + +config OTA_SKETCH_UPDATE_PATH + string "The path it is looked for ota" + default "/ota:/UPDATE_SKETCH.BIN" + help + the path that is checked for a sketch new binary + +config OTA_SKETCH_TEMP_PATH_POSTFIX + string "temporary file name postfix for sketch ota not ready" + default ".TEMP" + help + String that is appended to sketch ota filename when it is still not ready to be loaded, + a rename operation is required to make the ota file enabled for next reboot + +config OTA_LOADER_UPDATE_PATH + string "The path it is looked for ota" + default "/ota:/UPDATE.BIN" + help + the path that is checked for the loader new binary + +config OTA_LOADER_TEMP_PATH_POSTFIX + string "temporary file name postfix for loader ota not ready" + default "" if SOC_SERIES_STM32H7X + default ".TEMP" + help + String that is appended to loader ota filename when it is still not ready to be loaded, + a rename operation is required to make the ota file enabled for next reboot + +config OTA_TRANSCRIPTION_CHUNK_SIZE + int "The size of the stack allocated chunk in bytes for transcription" + default 1024 + help + The size of the stack allocated chunk in bytes for ota file transcription + +module = OTA +module-str = OTA +module-help = Sets log level for OTA +source "subsys/logging/Kconfig.template.log_config" + +endif diff --git a/loader/ota/ota_api.h b/loader/ota/ota_api.h new file mode 100644 index 000000000..d84ecc029 --- /dev/null +++ b/loader/ota/ota_api.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) Arduino s.r.l. and/or its affiliated companies + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define OTA_SKETCH_FILENAME CONFIG_OTA_SKETCH_UPDATE_PATH CONFIG_OTA_SKETCH_TEMP_PATH_POSTFIX +#define OTA_LOADER_FILENAME CONFIG_OTA_LOADER_UPDATE_PATH CONFIG_OTA_LOADER_TEMP_PATH_POSTFIX + +int ota_sketch_ready(); +int ota_sketch_start(); + +int ota_loader_ready(); +int ota_loader_start(); + +#ifdef __cplusplus +} +#endif diff --git a/loader/ota/ota_arduino_stm32h7.c b/loader/ota/ota_arduino_stm32h7.c new file mode 100644 index 000000000..739ae02af --- /dev/null +++ b/loader/ota/ota_arduino_stm32h7.c @@ -0,0 +1,55 @@ +/* + * Copyright (c) Arduino s.r.l. and/or its affiliated companies + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ota_api.h" +#include +#include +#include +#include + +#include +LOG_MODULE_REGISTER(ota_loader, CONFIG_OTA_LOG_LEVEL); + +#define OTA_MAGIC_BOOT 0x07AA +// #define OTA_STORAGE_TYPE 0xA4 // QSPI_FLASH | FATFS | MBR +#define OTA_STORAGE_TYPE ((1 << 2) | (1 << 5) | (1 << 7)) +#define OTA_MBR_PART 2 + +int ota_loader_ready() { + struct fs_dirent entry; + int ret; + + if (fs_stat(OTA_LOADER_FILENAME, &entry) < 0) { + LOG_WRN("tried to perform an ota without binary present"); + return -1; + } + + if (sizeof(CONFIG_OTA_LOADER_TEMP_PATH_POSTFIX) > 1) { + ret = fs_rename(OTA_LOADER_FILENAME, CONFIG_OTA_LOADER_UPDATE_PATH); + if (ret < 0) { + LOG_ERR("unable to rename ota file: %d", ret); + return ret; + } + } + + stm32_backup_domain_enable_access(); + LL_RTC_BAK_SetRegister(RTC, RTC_BKP_DR0, OTA_MAGIC_BOOT); + LL_RTC_BAK_SetRegister(RTC, RTC_BKP_DR1, OTA_STORAGE_TYPE); + LL_RTC_BAK_SetRegister(RTC, RTC_BKP_DR2, OTA_MBR_PART); + LL_RTC_BAK_SetRegister(RTC, RTC_BKP_DR3, entry.size); + stm32_backup_domain_disable_access(); + + LOG_INF("OTA marked as ready"); + + return 0; +} + +int ota_loader_start() { + LOG_INF("OTA started, reboot in progress"); + + sys_reboot(SYS_REBOOT_COLD); + return 0; // unreachable +} diff --git a/loader/ota/ota_llext_exports.c b/loader/ota/ota_llext_exports.c new file mode 100644 index 000000000..39947fc4e --- /dev/null +++ b/loader/ota/ota_llext_exports.c @@ -0,0 +1,14 @@ +/* + * Copyright (c) Arduino s.r.l. and/or its affiliated companies + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ota_api.h" +#include + +EXPORT_SYMBOL(ota_sketch_ready); +EXPORT_SYMBOL(ota_sketch_start); + +EXPORT_SYMBOL(ota_loader_ready); +EXPORT_SYMBOL(ota_loader_start); diff --git a/loader/ota/ota_portentac33.c b/loader/ota/ota_portentac33.c new file mode 100644 index 000000000..335f6fbc1 --- /dev/null +++ b/loader/ota/ota_portentac33.c @@ -0,0 +1,41 @@ +/* + * Copyright (c) Arduino s.r.l. and/or its affiliated companies + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include "ota_api.h" + +#include +#include + +#include +LOG_MODULE_REGISTER(ota_loader, CONFIG_OTA_LOG_LEVEL); + +int ota_loader_ready() { + // C33: SFU checks for UPDATE.BIN.OTA on every boot — no action needed + struct fs_dirent entry; + int ret; + + if (fs_stat(OTA_LOADER_FILENAME, &entry) < 0) { + LOG_WRN("tried to perform an ota without binary present"); + return -1; + } + + if (sizeof(CONFIG_OTA_LOADER_TEMP_PATH_POSTFIX) > 1) { + ret = fs_rename(OTA_LOADER_FILENAME, CONFIG_OTA_LOADER_UPDATE_PATH); + + if (ret < 0) { + LOG_ERR("unable to rename ota file: %d", ret); + } + } + + LOG_INF("OTA marked as ready"); + return ret; +} + +int ota_loader_start() { + sys_reboot(SYS_REBOOT_COLD); + + return 0; // unreachable +} diff --git a/loader/ota/ota_sketch.c b/loader/ota/ota_sketch.c new file mode 100644 index 000000000..6014b737e --- /dev/null +++ b/loader/ota/ota_sketch.c @@ -0,0 +1,42 @@ +/* + * Copyright (c) Arduino s.r.l. and/or its affiliated companies + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "ota_api.h" + +#include +LOG_MODULE_REGISTER(ota_sketch, CONFIG_OTA_LOG_LEVEL); + +int ota_sketch_ready() { + struct fs_dirent entry; + int ret; + + if (fs_stat(OTA_SKETCH_FILENAME, &entry) < 0) { + LOG_WRN("tried to perform an ota without binary present"); + return -1; + } + + if (sizeof(CONFIG_OTA_SKETCH_TEMP_PATH_POSTFIX) > 1) { + ret = fs_rename(OTA_SKETCH_FILENAME, CONFIG_OTA_SKETCH_UPDATE_PATH); + + if (ret < 0) { + LOG_ERR("unable to rename ota file: %d", ret); + } + } + + LOG_INF("OTA marked as ready"); + + return ret; +} + +int ota_sketch_start() { + LOG_INF("OTA started, reboot in progress"); + + sys_reboot(SYS_REBOOT_COLD); + return 0; +} diff --git a/loader/ota/ota_sketch_check.c b/loader/ota/ota_sketch_check.c new file mode 100644 index 000000000..dced0c415 --- /dev/null +++ b/loader/ota/ota_sketch_check.c @@ -0,0 +1,194 @@ +/* + * Copyright (c) Arduino s.r.l. and/or its affiliated companies + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +LOG_MODULE_REGISTER(ota, CONFIG_OTA_LOG_LEVEL); + +#include "ota_sketch_check.h" +#include "../../cores/arduino/zephyr_sketch_header.h" + +#include +#include +#include + +#include + +static int verify_sketch_header(const struct flash_area *fa, const struct sketch_header_v1 *hdr, + struct fs_dirent *entry) { + int rc = 0; + + LOG_DBG("Header ver: %02x, len: %d, magic: %04x, flags: %02X", hdr->ver, hdr->len, hdr->magic, + hdr->flags); + + if (sketch_header_v1_verify(hdr) != 0) { + LOG_ERR("invalid sketch header (ver=0x%x magic=0x%x)", hdr->ver, hdr->magic); + return -1; + } + + LOG_INF("sketch length = %u bytes", (unsigned)hdr->len); + + /* Bounds-check before the destructive erase. */ + if (hdr->len > fa->fa_size) { + LOG_ERR("sketch too large for partition (%u > %u)", (unsigned)hdr->len, + (unsigned)fa->fa_size); + return -1; + } + + if ((off_t)hdr->len > entry->size) { + LOG_ERR("header len exceeds file size (%u > %ld)", (unsigned)hdr->len, (long)entry->size); + return -1; + } + + return rc; +} + +static int perform_ota(const struct flash_area *fa, struct fs_file_t *file, + const struct sketch_header_v1 *hdr) { + int rc = 0; + + size_t wbs = flash_area_align(fa) > 0 ? flash_area_align(fa) : 1; + + uint8_t chunk[CONFIG_OTA_TRANSCRIPTION_CHUNK_SIZE]; + __ASSERT(wbs < CONFIG_OTA_TRANSCRIPTION_CHUNK_SIZE, + "OTA internal buffer is smaller(%d) than write block size(%d)", + CONFIG_OTA_TRANSCRIPTION_CHUNK_SIZE, wbs); + + __ASSERT(CONFIG_OTA_TRANSCRIPTION_CHUNK_SIZE % wbs == 0, + "OTA internal buffer(%d) is not multiple of write block " + "size(%d)" CONFIG_OTA_TRANSCRIPTION_CHUNK_SIZE, + wbs); + + off_t offset = 0; + size_t remaining = hdr->len; + ssize_t n; + size_t wlen; + + LOG_INF("erasing flash partition (%u bytes)...", (unsigned)fa->fa_size); + rc = flash_area_erase(fa, 0, fa->fa_size); + + if (rc) { + LOG_ERR("flash erase failed, rc %d — retry on next boot", rc); + goto exit; + } + + if ((rc = fs_seek(file, 0, FS_SEEK_SET))) { + LOG_ERR("failed to seek to start of file, rc %d — retry on next boot", rc); + goto exit; + } + + LOG_INF("rewriting sketch binary"); + + while (remaining > 0 && + (n = fs_read(file, chunk, remaining < sizeof(chunk) ? remaining : sizeof(chunk))) > 0) { + wlen = (size_t)n; + /* Pad the final partial chunk to the write-block-size with 0xFF + * (erased value); flash rejects unaligned writes. */ + if (wlen % wbs != 0) { + memset(chunk + wlen, 0xff, ROUND_UP(wlen, wbs) - wlen); + wlen = ROUND_UP(wlen, wbs); + } + + if ((rc = flash_area_write(fa, offset, chunk, wlen))) { + LOG_ERR("flash write failed at offset %ld, rc %d — retry on next boot", (long)offset, + rc); + + goto exit; + } + offset += n; + remaining -= n; + } + + if (remaining > 0) { + LOG_ERR("short read, %u bytes missing — retry on next boot", (unsigned)remaining); + goto exit; + } + + LOG_INF("wrote %ld bytes to flash", (long)offset); +exit: + return rc; +} + +/* + * Install /ota:/UPDATE.BIN into the sketch partition if present. The sketch + * validates the OTA payload (magic, CRC32, decompression) before writing it, + * so the loader trusts the file and only re-checks the inner sketch_header_v1. + * + * On failure before the erase, UPDATE.BIN is removed and the existing sketch + * boots. On failure after it, the partition is already partial so UPDATE.BIN + * is kept and the next boot retries — the only way back without DFU. + */ +int try_ota_update(const struct flash_area *fa) { + int rc = 0; + + struct fs_file_t file = {0}; + struct fs_dirent entry; + + struct sketch_header_v1 *hdr = NULL; + char header[SKETCH_HEADER_LEN]; + bool delete = false; + + LOG_DBG("checking for ota in \"%s\"", CONFIG_OTA_SKETCH_UPDATE_PATH); + + if (fa == NULL) { + rc = -1; + goto exit; + } + + if (fs_stat(CONFIG_OTA_SKETCH_UPDATE_PATH, &entry) != 0) { + LOG_DBG("no update pending"); + goto exit; + } + + LOG_INF("update pending, validating..."); + + fs_file_t_init(&file); + rc = fs_open(&file, CONFIG_OTA_SKETCH_UPDATE_PATH, FS_O_READ); + if (rc < 0) { + LOG_ERR("failed to open %s, rc %d", CONFIG_OTA_SKETCH_UPDATE_PATH, rc); + delete = true; + goto exit; + } + + if (entry.size < SKETCH_HEADER_LEN) { + LOG_ERR("file too small (%ld bytes)", (long)entry.size); + delete = true; + rc = -1; + goto exit; + } + + rc = fs_read(&file, header, SKETCH_HEADER_LEN); + if (rc != SKETCH_HEADER_LEN) { + LOG_ERR("failed to read header"); + delete = true; + rc = -1; + goto exit; + } + + hdr = (struct sketch_header_v1 *)(header + 7); + if ((rc = verify_sketch_header(fa, hdr, &entry))) { + delete = true; + goto exit; + } + + /* Partition becomes inconsistent past here; keep UPDATE.BIN on failure. */ + LOG_INF("performing ota"); + if ((rc = perform_ota(fa, &file, hdr))) { + goto exit; + } + delete = true; + LOG_INF("update complete"); +exit: + + if (file.mp != NULL && (rc = fs_close(&file))) { + LOG_ERR("error on fs_close %d", rc); + } + + if (delete && (rc = fs_unlink(CONFIG_OTA_SKETCH_UPDATE_PATH))) { + LOG_ERR("error on fs_unlink %d", rc); + } + + return rc; +} diff --git a/loader/ota/ota_sketch_check.h b/loader/ota/ota_sketch_check.h new file mode 100644 index 000000000..6b669b314 --- /dev/null +++ b/loader/ota/ota_sketch_check.h @@ -0,0 +1,20 @@ +/* + * Copyright (c) Arduino s.r.l. and/or its affiliated companies + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// rename this into sketch ota check or something +// not part of OTA api, internal use +// TODO remove parameter, the flash area should be provided by config +int try_ota_update(const struct flash_area *fa); + +#ifdef __cplusplus +} +#endif diff --git a/platform.txt b/platform.txt index 4eee89658..bc8821cf4 100644 --- a/platform.txt +++ b/platform.txt @@ -69,6 +69,10 @@ build.link_mode=dynamic build.boot_mode=wait upload.extension=elf-zsk.bin +# OTA generation defaults. Boards opt in by setting build.ota.magic and +# build.ota.sketch_offset; with magic empty, postobjcopy.3 is a no-op. +build.ota.magic= +build.ota.sketch_offset= build.ldscript.path={runtime.platform.path}/variants/_ldscripts build.link_command="{compiler.path}{compiler.c.elf.cmd}" "-L{build.path}" "-L{build.variant.path}" {compiler.c.elf.flags} {compiler.c.elf.extra_flags} {build.extra_flags} {build.extra_ldflags} {compiler.zephyr.common_ldflags} {compiler.ldflags} {object_files} -Wl,--start-group "{build.path}/{archive_file}" {compiler.zephyr.extra_ldflags} {compiler.libraries.ldflags} -Wl,--end-group {build.link_args.{build.link_mode}} @@ -163,6 +167,7 @@ recipe.objcopy.hex.pattern="{compiler.path}{compiler.elf2hex.cmd}" {compiler.elf ## Mangle the file recipe.hooks.objcopy.postobjcopy.1.pattern="{runtime.tools.zephyr-sketch-tool.path}/zephyr-sketch-tool" {build.zsk_args.debug} {build.zsk_args.mode-{build.link_mode}} {build.zsk_args.startup-mode-{build.boot_mode}} "{build.path}/{build.project_name}.elf" recipe.hooks.objcopy.postobjcopy.2.pattern="{runtime.tools.zephyr-sketch-tool.path}/zephyr-sketch-tool" {build.zsk_args.debug} {build.zsk_args.mode-{build.link_mode}} {build.zsk_args.startup-mode-{build.boot_mode}} "{build.path}/{build.project_name}.bin" +recipe.hooks.objcopy.postobjcopy.3.pattern="{runtime.tools.zephyr-sketch-tool.path}/zephyr-sketch-tool" -ota -ota-loader "{runtime.platform.path}/firmwares/zephyr-{build.variant}.bin" -ota-sketch "{build.path}/{build.project_name}.{upload.extension}" -ota-offset {build.ota.sketch_offset} -ota-magic {build.ota.magic} -output "{build.path}/{build.project_name}" ## Compute size recipe.size.pattern="{runtime.tools.zephyr-check-size.path}/zephyr-check-size" {build.link_mode} "{runtime.platform.path}/firmwares/zephyr-{build.variant}.config" "{build.path}/{build.project_name}.{upload.extension}" "{build.path}/{build.project_name}.elf" diff --git a/tools/zephyr-sketch-tool/ota.go b/tools/zephyr-sketch-tool/ota.go index 860a92de7..f1a331bd7 100644 --- a/tools/zephyr-sketch-tool/ota.go +++ b/tools/zephyr-sketch-tool/ota.go @@ -78,17 +78,24 @@ func runOTA(loaderPath, sketchPath, offsetStr, magicStr, outputBase string) erro suffix string payload []byte compress bool + header bool label string }{ - {".ota", sketch, false, "sketch"}, - {".lzss.ota", sketch, true, "sketch (LZSS)"}, - {"-bundle.ota", merged, false, "loader+sketch"}, - {"-bundle.lzss.ota", merged, true, "loader+sketch (LZSS)"}, + {".ota", sketch, false, true, "sketch"}, + {".lzss.ota", sketch, true, true, "sketch (LZSS)"}, + {"-bundle.bin", merged, false, false, "loader+sketch"}, + {"-bundle.ota", merged, false, true, "loader+sketch"}, + {"-bundle.lzss.ota", merged, true, true, "loader+sketch (LZSS)"}, } for _, v := range variants { path := base + v.suffix - out := buildOTA(uint32(magic), v.payload, v.compress) + + out := v.payload + if v.header { + out = buildOTA(uint32(magic), v.payload, v.compress) + } + if err := os.WriteFile(path, out, 0644); err != nil { return fmt.Errorf("write %s: %w", path, err) } diff --git a/variants/arduino_giga_r1_stm32h747xx_m7/arduino_giga_r1_stm32h747xx_m7.conf b/variants/arduino_giga_r1_stm32h747xx_m7/arduino_giga_r1_stm32h747xx_m7.conf index bb0db0fd7..5a5bdee72 100644 --- a/variants/arduino_giga_r1_stm32h747xx_m7/arduino_giga_r1_stm32h747xx_m7.conf +++ b/variants/arduino_giga_r1_stm32h747xx_m7/arduino_giga_r1_stm32h747xx_m7.conf @@ -212,3 +212,5 @@ CONFIG_NET_CONFIG_AUTO_INIT=n CONFIG_NET_CONNECTION_MANAGER=y #CONFIG_WIFI_NM_MAX_MANAGED_INTERFACES=2 CONFIG_NET_L2_ETHERNET_MGMT=y + +CONFIG_OTA=y diff --git a/variants/arduino_nicla_vision_stm32h747xx_m7/arduino_nicla_vision_stm32h747xx_m7.conf b/variants/arduino_nicla_vision_stm32h747xx_m7/arduino_nicla_vision_stm32h747xx_m7.conf index f6c0395c4..9e3f6191f 100644 --- a/variants/arduino_nicla_vision_stm32h747xx_m7/arduino_nicla_vision_stm32h747xx_m7.conf +++ b/variants/arduino_nicla_vision_stm32h747xx_m7/arduino_nicla_vision_stm32h747xx_m7.conf @@ -193,3 +193,5 @@ CONFIG_VIDEO_BUFFER_POOL_ALIGN=32 CONFIG_VIDEO_GC2145=y CONFIG_FLASH=y + +CONFIG_OTA=y diff --git a/variants/arduino_opta_stm32h747xx_m7/arduino_opta_stm32h747xx_m7.conf b/variants/arduino_opta_stm32h747xx_m7/arduino_opta_stm32h747xx_m7.conf index df52fe27c..292ef12b6 100644 --- a/variants/arduino_opta_stm32h747xx_m7/arduino_opta_stm32h747xx_m7.conf +++ b/variants/arduino_opta_stm32h747xx_m7/arduino_opta_stm32h747xx_m7.conf @@ -191,3 +191,5 @@ CONFIG_BT_RX_STACK_SIZE=4096 CONFIG_STM32H7_BOOT_M4_AT_INIT=n CONFIG_I2C_STM32_V2_TIMING=y + +CONFIG_OTA=y diff --git a/variants/arduino_portenta_c33_r7fa6m5bh3cfc/arduino_portenta_c33_r7fa6m5bh3cfc.conf b/variants/arduino_portenta_c33_r7fa6m5bh3cfc/arduino_portenta_c33_r7fa6m5bh3cfc.conf index 278b9f9ce..6a2c9f620 100644 --- a/variants/arduino_portenta_c33_r7fa6m5bh3cfc/arduino_portenta_c33_r7fa6m5bh3cfc.conf +++ b/variants/arduino_portenta_c33_r7fa6m5bh3cfc/arduino_portenta_c33_r7fa6m5bh3cfc.conf @@ -187,5 +187,7 @@ CONFIG_FS_FATFS_CUSTOM_MOUNT_POINTS="wlan,ota,SD" CONFIG_DYNAMIC_INTERRUPTS=n CONFIG_SPI_ASYNC=y +CONFIG_LED=y -CONFIG_LED=y \ No newline at end of file +CONFIG_OTA=y +CONFIG_OTA_LOG_LEVEL_DBG=y diff --git a/variants/arduino_portenta_c33_r7fa6m5bh3cfc/arduino_portenta_c33_r7fa6m5bh3cfc.overlay b/variants/arduino_portenta_c33_r7fa6m5bh3cfc/arduino_portenta_c33_r7fa6m5bh3cfc.overlay index 122c24ef2..514f66719 100644 --- a/variants/arduino_portenta_c33_r7fa6m5bh3cfc/arduino_portenta_c33_r7fa6m5bh3cfc.overlay +++ b/variants/arduino_portenta_c33_r7fa6m5bh3cfc/arduino_portenta_c33_r7fa6m5bh3cfc.overlay @@ -25,13 +25,19 @@ &flash0 { partitions { + /delete-node/ partition@10000; + mcuboot: partition@0 { label = "mcuboot"; reg = <0x00000000 DT_SIZE_K(64)>; }; - code_partition: partition@10000 { + sfu_partition: partition@10000 { + label = "sfu"; + reg = <0x00010000 (DT_SIZE_K(128))>; + }; + code_partition: partition@30000 { label = "image-0"; - reg = <0x00010000 (DT_SIZE_M(1) - DT_SIZE_K(64))>; + reg = <0x00030000 (DT_SIZE_M(1) - DT_SIZE_K(64) - DT_SIZE_K(128))>; }; user_sketch: partition@100000 { label = "user_sketch"; diff --git a/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.conf b/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.conf index 7d5555dca..0dab568f0 100644 --- a/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.conf +++ b/variants/arduino_portenta_h7_stm32h747xx_m7/arduino_portenta_h7_stm32h747xx_m7.conf @@ -204,3 +204,5 @@ CONFIG_SYSTEM_WORKQUEUE_STACK_SIZE=2048 CONFIG_BT_RX_STACK_SIZE=4096 CONFIG_STM32H7_BOOT_M4_AT_INIT=n + +CONFIG_OTA=y