Skip to content

Commit 5933152

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 ac5b151 commit 5933152

6 files changed

Lines changed: 192 additions & 0 deletions

File tree

boards.txt

Lines changed: 25 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_loader="{runtime.tools.ota-pack.path}/ota-pack" {build.ota.loader.extra_flags} {build.ota.compress_flag} -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}.loader.ota"
76+
giga.build.ota.pack_command_sketch="{runtime.tools.ota-pack.path}/ota-pack" --sketch-only {build.ota.compress_flag} -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_loader="{runtime.tools.ota-pack.path}/ota-pack" {build.ota.loader.extra_flags} {build.ota.compress_flag} -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}.loader.ota"
321+
portentah7.build.ota.pack_command_sketch="{runtime.tools.ota-pack.path}/ota-pack" --sketch-only {build.ota.compress_flag} -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}
@@ -378,6 +388,11 @@ nicla_vision.bootloader.interface=0
378388
nicla_vision.bootloader.file=zephyr-{build.variant}.bin
379389
nicla_vision.bootloader.address=0x08040000
380390

391+
nicla_vision.build.ota.magic=0x2341025F
392+
nicla_vision.build.ota.sketch_offset=0xA0000
393+
nicla_vision.build.ota.pack_command_loader="{runtime.tools.ota-pack.path}/ota-pack" {build.ota.loader.extra_flags} {build.ota.compress_flag} -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}.loader.ota"
394+
nicla_vision.build.ota.pack_command_sketch="{runtime.tools.ota-pack.path}/ota-pack" --sketch-only {build.ota.compress_flag} -sketch "{build.path}/{build.project_name}.{upload.extension}" -magic {build.ota.magic} -output "{build.path}/{build.project_name}.ota"
395+
381396
nicla_vision.debug.tool=gdb
382397
nicla_vision.debug.server.openocd.scripts.0=interface/{programmer.protocol}.cfg
383398
nicla_vision.debug.server.openocd.scripts.1={programmer.transport_script}
@@ -560,6 +575,11 @@ portentac33.bootloader.interface=0
560575
portentac33.bootloader.address=0x10000
561576
portentac33.bootloader.dfuse=-Q
562577

578+
portentac33.build.ota.magic=0x23410068
579+
portentac33.build.ota.sketch_offset=0xF0000
580+
portentac33.build.ota.pack_command_loader="{runtime.tools.ota-pack.path}/ota-pack" {build.ota.loader.extra_flags} {build.ota.compress_flag} -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}.loader.ota"
581+
portentac33.build.ota.pack_command_sketch="{runtime.tools.ota-pack.path}/ota-pack" --sketch-only {build.ota.compress_flag} -sketch "{build.path}/{build.project_name}.{upload.extension}" -magic {build.ota.magic} -output "{build.path}/{build.project_name}.ota"
582+
563583
##########################################################################################
564584

565585
opta.name=Arduino Opta
@@ -628,6 +648,11 @@ opta.debug.server.openocd.scripts.2=target/stm32h7x_dual_bank.cfg
628648
opta.debug.cortex-debug.custom.request=attach
629649
opta.debug.svd_file={runtime.platform.path}/svd/STM32H747_CM7.svd
630650

651+
opta.build.ota.magic=0x23410064
652+
opta.build.ota.sketch_offset=0xA0000
653+
opta.build.ota.pack_command_loader="{runtime.tools.ota-pack.path}/ota-pack" {build.ota.loader.extra_flags} {build.ota.compress_flag} -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}.loader.ota"
654+
opta.build.ota.pack_command_sketch="{runtime.tools.ota-pack.path}/ota-pack" --sketch-only {build.ota.compress_flag} -sketch "{build.path}/{build.project_name}.{upload.extension}" -magic {build.ota.magic} -output "{build.path}/{build.project_name}.ota"
655+
631656
##########################################################################################
632657

633658
nano_matter.name=Arduino Nano Matter

cores/arduino/llext_wrappers.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ W2(char *, strstr, const char *, const char *)
9191
W2(int, strcmp, const char *, const char *)
9292
W3(int, strncmp, const char *, const char *, size_t)
9393
W2(int, strcasecmp, const char *, const char *)
94+
W3(int, strncasecmp, const char *, const char *, size_t)
9495
W3(char *, strncpy, char *, const char *, size_t)
9596
W2(char *, strcat, char *, const char *)
9697
W2(char *, strcpy, char *, const char *)

loader/llext_exports.c

Lines changed: 3 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>
@@ -44,6 +45,7 @@ EXPORT_LIBC_SYM(strstr);
4445
EXPORT_LIBC_SYM(strncmp);
4546
EXPORT_LIBC_SYM(strncpy);
4647
EXPORT_LIBC_SYM(strcasecmp);
48+
EXPORT_LIBC_SYM(strncasecmp);
4749
EXPORT_LIBC_SYM(strcmp);
4850
EXPORT_LIBC_SYM(strlen);
4951
EXPORT_LIBC_SYM(strnlen);
@@ -303,6 +305,7 @@ EXPORT_SYMBOL(k_work_submit_to_queue);
303305

304306
EXPORT_SYMBOL(time);
305307
EXPORT_SYMBOL(sys_clock_settime);
308+
EXPORT_SYMBOL(sys_reboot);
306309
EXPORT_SYMBOL(mktime);
307310

308311
EXPORT_SYMBOL(printf);

loader/main.c

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,12 @@ 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_UPDATE_PATH "/ota:/UPDATE.BIN"
32+
3033
struct sketch_header_v1 {
3134
uint8_t ver; // @ 0x07
3235
uint32_t len; // @ 0x08
@@ -116,6 +119,153 @@ struct backup_store {
116119
};
117120
volatile __stm32_backup_sram_section struct backup_store backup;
118121

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

280+
#if defined(CONFIG_FILE_SYSTEM)
281+
try_ota_update(fa);
282+
#endif
283+
130284
uintptr_t base_addr =
131285
DT_REG_ADDR(DT_GPARENT(DT_NODELABEL(user_sketch))) + DT_REG_ADDR(DT_NODELABEL(user_sketch));
132286

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: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ build.link_mode=dynamic
5353
build.boot_mode=wait
5454
upload.extension=elf-zsk.bin
5555

56+
build.ota.pack_command_sketch=
57+
build.ota.pack_command_loader=
58+
build.ota.loader.extra_flags=
59+
# Set to "-compress" (e.g. via --build-property) to enable LZSS in the .ota payload.
60+
build.ota.compress_flag=
5661
build.ldscript.path={runtime.platform.path}/variants/_ldscripts
5762
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}}
5863

@@ -142,6 +147,8 @@ recipe.objcopy.hex.pattern="{compiler.path}{compiler.elf2hex.cmd}" {compiler.elf
142147
## Mangle the file
143148
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"
144149
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"
150+
recipe.hooks.objcopy.postobjcopy.3.pattern={build.ota.pack_command_sketch}
151+
recipe.hooks.objcopy.postobjcopy.4.pattern={build.ota.pack_command_loader}
145152

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

0 commit comments

Comments
 (0)