Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions boards.txt
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ 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
giga.build.ota.pack_command_legacy="{runtime.tools.ota-pack.path}/ota-pack" -loader "{runtime.platform.path}/firmwares/zephyr-{build.variant}.bin" -sketch "{build.path}/{build.project_name}.{upload.extension}" -offset {build.ota.sketch_offset} -magic {build.ota.magic} -output "{build.path}/{build.project_name}.legacy.ota"
giga.build.ota.pack_command="{runtime.tools.ota-pack.path}/ota-pack" --sketch-only -sketch "{build.path}/{build.project_name}.{upload.extension}" -magic {build.ota.magic} -output "{build.path}/{build.project_name}.ota"

##########################################################################################

nano33ble.name=Arduino Nano 33 BLE
Expand Down Expand Up @@ -310,6 +315,11 @@ 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.build.ota.pack_command_legacy="{runtime.tools.ota-pack.path}/ota-pack" -loader "{runtime.platform.path}/firmwares/zephyr-{build.variant}.bin" -sketch "{build.path}/{build.project_name}.{upload.extension}" -offset {build.ota.sketch_offset} -magic {build.ota.magic} -output "{build.path}/{build.project_name}.legacy.ota"
portentah7.build.ota.pack_command="{runtime.tools.ota-pack.path}/ota-pack" --sketch-only -sketch "{build.path}/{build.project_name}.{upload.extension}" -magic {build.ota.magic} -output "{build.path}/{build.project_name}.ota"

portentah7.debug.tool=gdb
portentah7.debug.server.openocd.scripts.0=interface/{programmer.protocol}.cfg
portentah7.debug.server.openocd.scripts.1={programmer.transport_script}
Expand Down Expand Up @@ -560,6 +570,9 @@ portentac33.bootloader.interface=0
portentac33.bootloader.address=0x10000
portentac33.bootloader.dfuse=-Q

portentac33.build.ota.magic=0x23410068
portentac33.build.ota.pack_command="{runtime.tools.ota-pack.path}/ota-pack" --sketch-only -sketch "{build.path}/{build.project_name}.{upload.extension}" -magic {build.ota.magic} -output "{build.path}/{build.project_name}.ota"

##########################################################################################

opta.name=Arduino Opta
Expand Down Expand Up @@ -628,6 +641,11 @@ 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
opta.build.ota.pack_command_legacy="{runtime.tools.ota-pack.path}/ota-pack" -loader "{runtime.platform.path}/firmwares/zephyr-{build.variant}.bin" -sketch "{build.path}/{build.project_name}.{upload.extension}" -offset {build.ota.sketch_offset} -magic {build.ota.magic} -output "{build.path}/{build.project_name}.legacy.ota"
opta.build.ota.pack_command="{runtime.tools.ota-pack.path}/ota-pack" --sketch-only -sketch "{build.path}/{build.project_name}.{upload.extension}" -magic {build.ota.magic} -output "{build.path}/{build.project_name}.ota"

##########################################################################################

nano_matter.name=Arduino Nano Matter
Expand Down
2 changes: 2 additions & 0 deletions loader/llext_exports.c
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
#include <strings.h>
#include <zephyr/llext/symbol.h>
#include <zephyr/usb/usb_device.h>
#include <zephyr/sys/reboot.h>
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
Expand Down Expand Up @@ -289,6 +290,7 @@ EXPORT_SYMBOL(k_work_submit_to_queue);

EXPORT_SYMBOL(time);
EXPORT_SYMBOL(sys_clock_settime);
EXPORT_SYMBOL(sys_reboot);
EXPORT_SYMBOL(mktime);

EXPORT_SYMBOL(printf);
Expand Down
149 changes: 149 additions & 0 deletions loader/main.c
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,13 @@ LOG_MODULE_REGISTER(sketch);
#include <zephyr/usb/usb_device.h>

#include <zephyr/devicetree/fixed-partitions.h>
#include <zephyr/fs/fs.h>

#define HEADER_LEN 16

#define OTA_SENTINEL_PATH "/ota:/OTA_UPDATE_PENDING"
#define OTA_UPDATE_PATH "/ota:/UPDATE.BIN"

struct sketch_header_v1 {
uint8_t ver; // @ 0x07
uint32_t len; // @ 0x08
Expand Down Expand Up @@ -116,6 +120,147 @@ struct backup_store {
};
volatile __stm32_backup_sram_section struct backup_store backup;

#if defined(CONFIG_FILE_SYSTEM)
/*
* Install a pending OTA update if one is present on /ota:.
*
* Trigger: /ota:/OTA_UPDATE_PENDING is a zero-byte sentinel dropped by
* the sketch (Arduino_OTA_Loader.cpp) immediately before sys_reboot.
*
* Recovery policy on failure:
* - Pre-erase errors (bad header, bad bounds, file too small): the
* source file is unrecoverable, so the sentinel and UPDATE.BIN are
* both removed and the loader proceeds to boot the existing sketch.
* - Post-erase errors (flash write fault, truncated read mid-stream):
* the partition is already partially written, so the SENTINEL is
* KEPT IN PLACE. The next boot will retry from the same UPDATE.BIN,
* which is the only way back from an in-progress flash without DFU.
* If the failure is persistent the user must recover externally.
*/
static int try_ota_update(const struct flash_area *fa) {
struct fs_dirent entry;
int rc;

/* Check for pending OTA update */
if (fs_stat(OTA_SENTINEL_PATH, &entry) != 0) {
printk("OTA: no update pending\n");
return 0;
}

printk("OTA: update pending, validating...\n");

/* Open UPDATE.BIN */
struct fs_file_t file;
fs_file_t_init(&file);
rc = fs_open(&file, OTA_UPDATE_PATH, FS_O_READ);
if (rc < 0) {
printk("OTA: failed to open %s, rc %d\n", OTA_UPDATE_PATH, rc);
fs_unlink(OTA_SENTINEL_PATH);
return -1;
}

/* Get file size */
fs_seek(&file, 0, FS_SEEK_END);
off_t file_size = fs_tell(&file);
fs_seek(&file, 0, FS_SEEK_SET);

if (file_size < HEADER_LEN) {
printk("OTA: file too small (%ld bytes)\n", (long)file_size);
fs_close(&file);
fs_unlink(OTA_SENTINEL_PATH);
fs_unlink(OTA_UPDATE_PATH);
return -1;
}

/* Read and validate sketch header */
char header[HEADER_LEN];
rc = fs_read(&file, header, HEADER_LEN);
if (rc != HEADER_LEN) {
printk("OTA: failed to read header\n");
fs_close(&file);
fs_unlink(OTA_SENTINEL_PATH);
fs_unlink(OTA_UPDATE_PATH);
return -1;
}

struct sketch_header_v1 *hdr = (struct sketch_header_v1 *)(header + 7);
if (hdr->ver != 0x1 || hdr->magic != 0x2341) {
printk("OTA: invalid sketch header (ver=0x%x magic=0x%x)\n", hdr->ver, hdr->magic);
fs_close(&file);
fs_unlink(OTA_SENTINEL_PATH);
fs_unlink(OTA_UPDATE_PATH);
return -1;
}

size_t sketch_len = hdr->len;
printk("OTA: sketch length = %u bytes\n", (unsigned)sketch_len);

/* Bounds-check header before the destructive erase: refuse to brick
* the device on a malformed/oversized header or a truncated file. */
if (sketch_len > fa->fa_size) {
printk("OTA: sketch too large for partition (%u > %u)\n", (unsigned)sketch_len,
(unsigned)fa->fa_size);
fs_close(&file);
fs_unlink(OTA_SENTINEL_PATH);
fs_unlink(OTA_UPDATE_PATH);
return -1;
}
if ((off_t)sketch_len > file_size) {
printk("OTA: header len exceeds file size (%u > %ld)\n", (unsigned)sketch_len,
(long)file_size);
fs_close(&file);
fs_unlink(OTA_SENTINEL_PATH);
fs_unlink(OTA_UPDATE_PATH);
return -1;
}

/* From here on the partition is about to become inconsistent.
* On failure we leave the sentinel + UPDATE.BIN in place so the
* next boot can retry. */
printk("OTA: erasing flash partition (%u bytes)...\n", (unsigned)fa->fa_size);
rc = flash_area_erase(fa, 0, fa->fa_size);
if (rc) {
printk("OTA: flash erase failed, rc %d — retry on next boot\n", rc);
fs_close(&file);
return -1;
}

/* Write sketch data from file to flash in chunks */
fs_seek(&file, 0, FS_SEEK_SET);
uint8_t chunk[4096];
off_t offset = 0;
size_t remaining = sketch_len;
ssize_t n;
while (remaining > 0 &&
(n = fs_read(&file, chunk, remaining < sizeof(chunk) ? remaining : sizeof(chunk))) > 0) {
rc = flash_area_write(fa, offset, chunk, n);
if (rc) {
printk("OTA: flash write failed at offset %ld, rc %d — retry on next boot\n",
(long)offset, rc);
fs_close(&file);
return -1;
}
offset += n;
remaining -= n;
}
fs_close(&file);

if (remaining > 0) {
printk("OTA: short read, %u bytes missing — retry on next boot\n", (unsigned)remaining);
return -1;
}

printk("OTA: wrote %ld bytes to flash\n", (long)offset);

/* Success — clear sentinel and update file so the next boot is normal. */
fs_unlink(OTA_SENTINEL_PATH);
fs_unlink(OTA_UPDATE_PATH);

printk("OTA: update complete\n");
return 0;
}
#endif /* CONFIG_FILE_SYSTEM */

static int loader(const struct shell *sh) {
const struct flash_area *fa;
int rc;
Expand All @@ -127,6 +272,10 @@ static int loader(const struct shell *sh) {
return rc;
}

#if defined(CONFIG_FILE_SYSTEM)
try_ota_update(fa);
#endif

uintptr_t base_addr =
DT_REG_ADDR(DT_GPARENT(DT_NODELABEL(user_sketch))) + DT_REG_ADDR(DT_NODELABEL(user_sketch));

Expand Down
2 changes: 2 additions & 0 deletions loader/prj.conf
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,5 @@ CONFIG_RTC=y
CONFIG_RTC_ALARM=y
CONFIG_RTC_UPDATE=y
CONFIG_RTC_CALIBRATION=y

CONFIG_REBOOT=y
4 changes: 4 additions & 0 deletions platform.txt
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ build.link_mode=dynamic
build.boot_mode=wait
upload.extension=elf-zsk.bin

build.ota.pack_command=
build.ota.pack_command_legacy=
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} --specs=nano.specs --specs=nosys.specs {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}}

Expand Down Expand Up @@ -142,6 +144,8 @@ 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={build.ota.pack_command}
recipe.hooks.objcopy.postobjcopy.4.pattern={build.ota.pack_command_legacy}

## Compute size
recipe.size.pattern="{compiler.path}{compiler.size.cmd}" -A "{build.path}/{build.project_name}.elf"
Expand Down
1 change: 1 addition & 0 deletions tools/ota-pack/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ota-pack

Check warning on line 1 in tools/ota-pack/.gitignore

View workflow job for this annotation

GitHub Actions / Scan code for licenses

Missing license and copyright information
3 changes: 3 additions & 0 deletions tools/ota-pack/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/arduino/ota-pack

Check warning on line 1 in tools/ota-pack/go.mod

View workflow job for this annotation

GitHub Actions / Scan code for licenses

File not scanned: 'application-package'

go 1.25.5
Loading
Loading