Skip to content

Commit 0fdb64d

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 ef934eb commit 0fdb64d

7 files changed

Lines changed: 181 additions & 1 deletion

File tree

boards.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ 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+
7376
##########################################################################################
7477

7578
nano33ble.name=Arduino Nano 33 BLE
@@ -310,6 +313,9 @@ portentah7.bootloader.interface=0
310313
portentah7.bootloader.file=zephyr-{build.variant}.bin
311314
portentah7.bootloader.address=0x08040000
312315

316+
portentah7.build.ota.magic=0x2341025B
317+
portentah7.build.ota.sketch_offset=0xA0000
318+
313319
portentah7.debug.tool=gdb
314320
portentah7.debug.server.openocd.scripts.0=interface/{programmer.protocol}.cfg
315321
portentah7.debug.server.openocd.scripts.1={programmer.transport_script}
@@ -378,6 +384,9 @@ nicla_vision.bootloader.interface=0
378384
nicla_vision.bootloader.file=zephyr-{build.variant}.bin
379385
nicla_vision.bootloader.address=0x08040000
380386

387+
nicla_vision.build.ota.magic=0x2341025F
388+
nicla_vision.build.ota.sketch_offset=0xA0000
389+
381390
nicla_vision.debug.tool=gdb
382391
nicla_vision.debug.server.openocd.scripts.0=interface/{programmer.protocol}.cfg
383392
nicla_vision.debug.server.openocd.scripts.1={programmer.transport_script}
@@ -560,6 +569,9 @@ portentac33.bootloader.interface=0
560569
portentac33.bootloader.address=0x10000
561570
portentac33.bootloader.dfuse=-Q
562571

572+
portentac33.build.ota.magic=0x23410068
573+
portentac33.build.ota.sketch_offset=0xF0000
574+
563575
##########################################################################################
564576

565577
opta.name=Arduino Opta
@@ -628,6 +640,9 @@ opta.debug.server.openocd.scripts.2=target/stm32h7x_dual_bank.cfg
628640
opta.debug.cortex-debug.custom.request=attach
629641
opta.debug.svd_file={runtime.platform.path}/svd/STM32H747_CM7.svd
630642

643+
opta.build.ota.magic=0x23410064
644+
opta.build.ota.sketch_offset=0xA0000
645+
631646
##########################################################################################
632647

633648
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 *)

extra/artifacts/_common.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
{
3232
"packager": "arduino",
3333
"name": "zephyr-sketch-tool",
34-
"version": "0.3.0"
34+
"version": "0.4.0"
3535
},
3636
{
3737
"packager": "arduino",

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

56+
# OTA generation defaults. Boards opt in by setting build.ota.magic and
57+
# build.ota.sketch_offset; with magic empty, postobjcopy.3 is a no-op.
58+
build.ota.magic=
59+
build.ota.sketch_offset=
5660
build.ldscript.path={runtime.platform.path}/variants/_ldscripts
5761
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}}
5862

@@ -142,6 +146,7 @@ recipe.objcopy.hex.pattern="{compiler.path}{compiler.elf2hex.cmd}" {compiler.elf
142146
## Mangle the file
143147
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"
144148
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"
149+
recipe.hooks.objcopy.postobjcopy.3.pattern="{runtime.tools.zephyr-sketch-tool.path}/zephyr-sketch-tool" -ota -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} -ota-output "{build.path}/{build.project_name}"
145150

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

0 commit comments

Comments
 (0)