Skip to content

Commit 16b01b6

Browse files
committed
loader: add ota support
On boot, install /ota:/UPDATE.BIN into the user_sketch partition when the OTA_UPDATE_PENDING sentinel is present. Validates the sketch header, bounds, and keeps the sentinel on post-erase failure for retry. Signed-off-by: Gilberto Conti <g.conti@arduino.cc>
1 parent 48be02b commit 16b01b6

5 files changed

Lines changed: 175 additions & 0 deletions

File tree

boards.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ giga.debug.server.openocd.scripts.2=target/stm32h7x_dual_bank.cfg
7070
giga.debug.cortex-debug.custom.request=attach
7171
giga.debug.svd_file={runtime.platform.path}/svd/STM32H747_CM7.svd
7272

73+
giga.build.ota.magic=0x23410266
74+
giga.build.ota.sketch_offset=0xA0000
75+
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"
76+
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"
77+
7378
##########################################################################################
7479

7580
nano33ble.name=Arduino Nano 33 BLE
@@ -310,6 +315,11 @@ portentah7.bootloader.interface=0
310315
portentah7.bootloader.file=zephyr-{build.variant}.bin
311316
portentah7.bootloader.address=0x08040000
312317

318+
portentah7.build.ota.magic=0x2341025B
319+
portentah7.build.ota.sketch_offset=0xA0000
320+
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"
321+
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"
322+
313323
portentah7.debug.tool=gdb
314324
portentah7.debug.server.openocd.scripts.0=interface/{programmer.protocol}.cfg
315325
portentah7.debug.server.openocd.scripts.1={programmer.transport_script}
@@ -560,6 +570,9 @@ portentac33.bootloader.interface=0
560570
portentac33.bootloader.address=0x10000
561571
portentac33.bootloader.dfuse=-Q
562572

573+
portentac33.build.ota.magic=0x23410068
574+
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"
575+
563576
##########################################################################################
564577

565578
opta.name=Arduino Opta
@@ -628,6 +641,11 @@ opta.debug.server.openocd.scripts.2=target/stm32h7x_dual_bank.cfg
628641
opta.debug.cortex-debug.custom.request=attach
629642
opta.debug.svd_file={runtime.platform.path}/svd/STM32H747_CM7.svd
630643

644+
opta.build.ota.magic=0x23410064
645+
opta.build.ota.sketch_offset=0xA0000
646+
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"
647+
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"
648+
631649
##########################################################################################
632650

633651
nano_matter.name=Arduino Nano Matter

loader/llext_exports.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <strings.h>
99
#include <zephyr/llext/symbol.h>
1010
#include <zephyr/usb/usb_device.h>
11+
#include <zephyr/sys/reboot.h>
1112
#include <ctype.h>
1213
#include <stdio.h>
1314
#include <stdlib.h>
@@ -289,6 +290,7 @@ EXPORT_SYMBOL(k_work_submit_to_queue);
289290

290291
EXPORT_SYMBOL(time);
291292
EXPORT_SYMBOL(sys_clock_settime);
293+
EXPORT_SYMBOL(sys_reboot);
292294
EXPORT_SYMBOL(mktime);
293295

294296
EXPORT_SYMBOL(printf);

loader/main.c

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,13 @@ LOG_MODULE_REGISTER(sketch);
2424
#include <zephyr/usb/usb_device.h>
2525

2626
#include <zephyr/devicetree/fixed-partitions.h>
27+
#include <zephyr/fs/fs.h>
2728

2829
#define HEADER_LEN 16
2930

31+
#define OTA_SENTINEL_PATH "/ota:/OTA_UPDATE_PENDING"
32+
#define OTA_UPDATE_PATH "/ota:/UPDATE.BIN"
33+
3034
struct sketch_header_v1 {
3135
uint8_t ver; // @ 0x07
3236
uint32_t len; // @ 0x08
@@ -116,6 +120,147 @@ struct backup_store {
116120
};
117121
volatile __stm32_backup_sram_section struct backup_store backup;
118122

123+
#if defined(CONFIG_FILE_SYSTEM)
124+
/*
125+
* Install a pending OTA update if one is present on /ota:.
126+
*
127+
* Trigger: /ota:/OTA_UPDATE_PENDING is a zero-byte sentinel dropped by
128+
* the sketch (Arduino_OTA_Loader.cpp) immediately before sys_reboot.
129+
*
130+
* Recovery policy on failure:
131+
* - Pre-erase errors (bad header, bad bounds, file too small): the
132+
* source file is unrecoverable, so the sentinel and UPDATE.BIN are
133+
* both removed and the loader proceeds to boot the existing sketch.
134+
* - Post-erase errors (flash write fault, truncated read mid-stream):
135+
* the partition is already partially written, so the SENTINEL is
136+
* KEPT IN PLACE. The next boot will retry from the same UPDATE.BIN,
137+
* which is the only way back from an in-progress flash without DFU.
138+
* If the failure is persistent the user must recover externally.
139+
*/
140+
static int try_ota_update(const struct flash_area *fa) {
141+
struct fs_dirent entry;
142+
int rc;
143+
144+
/* Check for pending OTA update */
145+
if (fs_stat(OTA_SENTINEL_PATH, &entry) != 0) {
146+
printk("OTA: no update pending\n");
147+
return 0;
148+
}
149+
150+
printk("OTA: update pending, validating...\n");
151+
152+
/* Open UPDATE.BIN */
153+
struct fs_file_t file;
154+
fs_file_t_init(&file);
155+
rc = fs_open(&file, OTA_UPDATE_PATH, FS_O_READ);
156+
if (rc < 0) {
157+
printk("OTA: failed to open %s, rc %d\n", OTA_UPDATE_PATH, rc);
158+
fs_unlink(OTA_SENTINEL_PATH);
159+
return -1;
160+
}
161+
162+
/* Get file size */
163+
fs_seek(&file, 0, FS_SEEK_END);
164+
off_t file_size = fs_tell(&file);
165+
fs_seek(&file, 0, FS_SEEK_SET);
166+
167+
if (file_size < HEADER_LEN) {
168+
printk("OTA: file too small (%ld bytes)\n", (long)file_size);
169+
fs_close(&file);
170+
fs_unlink(OTA_SENTINEL_PATH);
171+
fs_unlink(OTA_UPDATE_PATH);
172+
return -1;
173+
}
174+
175+
/* Read and validate sketch header */
176+
char header[HEADER_LEN];
177+
rc = fs_read(&file, header, HEADER_LEN);
178+
if (rc != HEADER_LEN) {
179+
printk("OTA: failed to read header\n");
180+
fs_close(&file);
181+
fs_unlink(OTA_SENTINEL_PATH);
182+
fs_unlink(OTA_UPDATE_PATH);
183+
return -1;
184+
}
185+
186+
struct sketch_header_v1 *hdr = (struct sketch_header_v1 *)(header + 7);
187+
if (hdr->ver != 0x1 || hdr->magic != 0x2341) {
188+
printk("OTA: invalid sketch header (ver=0x%x magic=0x%x)\n", hdr->ver, hdr->magic);
189+
fs_close(&file);
190+
fs_unlink(OTA_SENTINEL_PATH);
191+
fs_unlink(OTA_UPDATE_PATH);
192+
return -1;
193+
}
194+
195+
size_t sketch_len = hdr->len;
196+
printk("OTA: sketch length = %u bytes\n", (unsigned)sketch_len);
197+
198+
/* Bounds-check header before the destructive erase: refuse to brick
199+
* the device on a malformed/oversized header or a truncated file. */
200+
if (sketch_len > fa->fa_size) {
201+
printk("OTA: sketch too large for partition (%u > %u)\n", (unsigned)sketch_len,
202+
(unsigned)fa->fa_size);
203+
fs_close(&file);
204+
fs_unlink(OTA_SENTINEL_PATH);
205+
fs_unlink(OTA_UPDATE_PATH);
206+
return -1;
207+
}
208+
if ((off_t)sketch_len > file_size) {
209+
printk("OTA: header len exceeds file size (%u > %ld)\n", (unsigned)sketch_len,
210+
(long)file_size);
211+
fs_close(&file);
212+
fs_unlink(OTA_SENTINEL_PATH);
213+
fs_unlink(OTA_UPDATE_PATH);
214+
return -1;
215+
}
216+
217+
/* From here on the partition is about to become inconsistent.
218+
* On failure we leave the sentinel + UPDATE.BIN in place so the
219+
* next boot can retry. */
220+
printk("OTA: erasing flash partition (%u bytes)...\n", (unsigned)fa->fa_size);
221+
rc = flash_area_erase(fa, 0, fa->fa_size);
222+
if (rc) {
223+
printk("OTA: flash erase failed, rc %d — retry on next boot\n", rc);
224+
fs_close(&file);
225+
return -1;
226+
}
227+
228+
/* Write sketch data from file to flash in chunks */
229+
fs_seek(&file, 0, FS_SEEK_SET);
230+
uint8_t chunk[4096];
231+
off_t offset = 0;
232+
size_t remaining = sketch_len;
233+
ssize_t n;
234+
while (remaining > 0 &&
235+
(n = fs_read(&file, chunk, remaining < sizeof(chunk) ? remaining : sizeof(chunk))) > 0) {
236+
rc = flash_area_write(fa, offset, chunk, n);
237+
if (rc) {
238+
printk("OTA: flash write failed at offset %ld, rc %d — retry on next boot\n",
239+
(long)offset, rc);
240+
fs_close(&file);
241+
return -1;
242+
}
243+
offset += n;
244+
remaining -= n;
245+
}
246+
fs_close(&file);
247+
248+
if (remaining > 0) {
249+
printk("OTA: short read, %u bytes missing — retry on next boot\n", (unsigned)remaining);
250+
return -1;
251+
}
252+
253+
printk("OTA: wrote %ld bytes to flash\n", (long)offset);
254+
255+
/* Success — clear sentinel and update file so the next boot is normal. */
256+
fs_unlink(OTA_SENTINEL_PATH);
257+
fs_unlink(OTA_UPDATE_PATH);
258+
259+
printk("OTA: update complete\n");
260+
return 0;
261+
}
262+
#endif /* CONFIG_FILE_SYSTEM */
263+
119264
static int loader(const struct shell *sh) {
120265
const struct flash_area *fa;
121266
int rc;
@@ -127,6 +272,10 @@ static int loader(const struct shell *sh) {
127272
return rc;
128273
}
129274

275+
#if defined(CONFIG_FILE_SYSTEM)
276+
try_ota_update(fa);
277+
#endif
278+
130279
uintptr_t base_addr =
131280
DT_REG_ADDR(DT_GPARENT(DT_NODELABEL(user_sketch))) + DT_REG_ADDR(DT_NODELABEL(user_sketch));
132281

loader/prj.conf

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,5 @@ CONFIG_RTC=y
4949
CONFIG_RTC_ALARM=y
5050
CONFIG_RTC_UPDATE=y
5151
CONFIG_RTC_CALIBRATION=y
52+
53+
CONFIG_REBOOT=y

platform.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ build.link_mode=dynamic
5353
build.boot_mode=wait
5454
upload.extension=elf-zsk.bin
5555

56+
build.ota.pack_command=
57+
build.ota.pack_command_legacy=
5658
build.ldscript.path={runtime.platform.path}/variants/_ldscripts
5759
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}}
5860

@@ -142,6 +144,8 @@ recipe.objcopy.hex.pattern="{compiler.path}{compiler.elf2hex.cmd}" {compiler.elf
142144
## Mangle the file
143145
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"
144146
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"
147+
recipe.hooks.objcopy.postobjcopy.3.pattern={build.ota.pack_command}
148+
recipe.hooks.objcopy.postobjcopy.4.pattern={build.ota.pack_command_legacy}
145149

146150
## Compute size
147151
recipe.size.pattern="{compiler.path}{compiler.size.cmd}" -A "{build.path}/{build.project_name}.elf"

0 commit comments

Comments
 (0)