Merge pull request #3601 from cesanta/ota

OTA rollback nits
This commit is contained in:
Sergey Lyubka authored and GitHub committed 2026-06-13 19:56:31 +01:00
commit 493de5cf52
13 files changed
+507 -226

No files matched your search

+141 -77
View File
@@ -7533,11 +7533,6 @@ void mg_mgr_free(struct mg_mgr *mgr) {
void mg_mgr_init(struct mg_mgr *mgr) { void mg_mgr_init(struct mg_mgr *mgr) {
memset(mgr, 0, sizeof(*mgr)); memset(mgr, 0, sizeof(*mgr));
// Anchor mg_fw_version: a real store into a struct field the optimiser
// can't elide forces the linker to keep the section alive even when no
// OTA polling code runs it. Callers that want mgr->userdata for their own
// purposes simply overwrite it after this call returns
mgr->userdata = (void *) mg_fw_version;
#if MG_ENABLE_EPOLL #if MG_ENABLE_EPOLL
if ((mgr->epoll_fd = epoll_create1(EPOLL_CLOEXEC)) < 0) if ((mgr->epoll_fd = epoll_create1(EPOLL_CLOEXEC)) < 0)
MG_ERROR(("epoll_create1 errno %d", errno)); MG_ERROR(("epoll_create1 errno %d", errno));
@@ -9908,20 +9903,20 @@ void mg_tcpip_mapip(struct mg_connection *c, struct mg_addr *ip) {
#endif #endif
// Scannable version tag embedded in every firmware binary, for server-side // Scannable version tag embedded in every firmware binary, for server-side
// version extraction. Non-static so mg_mgr_init() can stash its address in // version extraction. __attribute__((used)) prevents --gc-sections from
// mgr->userdata: that store is what actually keeps -Wl,--gc-sections from // stripping it. The version string starts after the 11-char "MG_VERSION:" prefix.
// stripping it on builds that never poll for OTAs (an attribute alone does static const char mg_fw_version[] __attribute__((used)) =
// not - the linker drops unreferenced sections regardless of "used"/"retain") "MG_VERSION:" MG_OTA_FIRMWARE_VERSION;
const char mg_fw_version[] = "MG_VERSION:" MG_OTA_FIRMWARE_VERSION;
static bool s_autocommit_ok; // True after OTA server confirms "same version"
static struct mg_ota_state { static struct mg_ota_state {
char my_version[MG_OTA_MAX_VERSION_LEN];
char json_url[MG_OTA_MAX_URL_LEN]; char json_url[MG_OTA_MAX_URL_LEN];
char version[MG_OTA_MAX_VERSION_LEN]; char version[MG_OTA_MAX_VERSION_LEN];
char url[MG_OTA_MAX_URL_LEN]; char url[MG_OTA_MAX_URL_LEN];
size_t size; size_t size;
uint8_t sha256[32]; uint8_t sha256[32];
void (*fn)(const char *status); void (*fn)(const char *error_message);
} *s_ota; } *s_ota;
static void s_firmware_fn(struct mg_connection *c, int ev, void *ev_data); static void s_firmware_fn(struct mg_connection *c, int ev, void *ev_data);
@@ -9949,15 +9944,15 @@ static void s_version_fn(struct mg_connection *c, int ev, void *ev_data) {
mg_ota_device_id(id, sizeof(id)); mg_ota_device_id(id, sizeof(id));
id[sizeof(id) - 1] = '\0'; id[sizeof(id) - 1] = '\0';
mg_printf(c, mg_printf(c,
"GET %s%sarch=%d&version=%s&id=%s&interval=%d HTTP/1.1\r\n" "GET %s%sarch=%d&version=%s&id=%s&interval=%d&boot=%d HTTP/1.1\r\n"
"Host: %.*s\r\n" "Host: %.*s\r\n"
"Connection: close\r\n\r\n", "Connection: close\r\n\r\n",
uri, sep, MG_ARCH, s_ota->my_version, id, uri, sep, MG_ARCH, mg_fw_version + 11, id,
MG_OTA_PULL_INTERVAL_SECONDS, host.len, host.buf); MG_OTA_PULL_INTERVAL_SECONDS, MG_OTA_STATE_GET(), host.len, host.buf);
} else if (ev == MG_EV_HTTP_MSG) { } else if (ev == MG_EV_HTTP_MSG) {
struct mg_http_message *hm = (struct mg_http_message *) ev_data; struct mg_http_message *hm = (struct mg_http_message *) ev_data;
double result; double result;
MG_DEBUG(("Got metadata:\n%.*s", hm->body.len, hm->body.buf)); MG_DEBUG(("Got metadata: %.*s", hm->body.len, hm->body.buf));
if (mg_http_status(hm) != 200 || mg_json_get(hm->body, "$", NULL) != 0 || if (mg_http_status(hm) != 200 || mg_json_get(hm->body, "$", NULL) != 0 ||
!mg_json_unescape(hm->body, "$.version", s_ota->version, !mg_json_unescape(hm->body, "$.version", s_ota->version,
sizeof(s_ota->version)) || sizeof(s_ota->version)) ||
@@ -9969,7 +9964,8 @@ static void s_version_fn(struct mg_connection *c, int ev, void *ev_data) {
s_ota->fn(buf); s_ota->fn(buf);
mg_free(s_ota); mg_free(s_ota);
s_ota = NULL; s_ota = NULL;
} else if (strcmp(s_ota->version, s_ota->my_version) == 0) { } else if (strcmp(s_ota->version, mg_fw_version + 11) == 0) {
s_autocommit_ok = true;
s_ota->fn("Same version"); s_ota->fn("Same version");
mg_free(s_ota); mg_free(s_ota);
s_ota = NULL; s_ota = NULL;
@@ -10035,8 +10031,8 @@ static void s_firmware_fn(struct mg_connection *c, int ev, void *ev_data) {
} }
} }
void mg_ota_url_check(struct mg_mgr *mgr, const char *my_version, void mg_ota_url_check(struct mg_mgr *mgr, const char *json_url,
const char *json_url, void (*fn)(const char *status)) { void (*fn)(const char *error_message)) {
if (fn == NULL) fn = status_fn; if (fn == NULL) fn = status_fn;
if (s_ota != NULL) { if (s_ota != NULL) {
fn("OTA already in progress"); fn("OTA already in progress");
@@ -10045,7 +10041,6 @@ void mg_ota_url_check(struct mg_mgr *mgr, const char *my_version,
fn("Out of memory"); fn("Out of memory");
} else { } else {
struct mg_connection *c; struct mg_connection *c;
mg_snprintf(s_ota->my_version, sizeof(s_ota->my_version), "%s", my_version);
mg_snprintf(s_ota->json_url, sizeof(s_ota->json_url), "%s", json_url); mg_snprintf(s_ota->json_url, sizeof(s_ota->json_url), "%s", json_url);
MG_DEBUG(("Connecting to %s", json_url)); MG_DEBUG(("Connecting to %s", json_url));
c = mg_http_connect(mgr, s_ota->json_url, s_version_fn, NULL); c = mg_http_connect(mgr, s_ota->json_url, s_version_fn, NULL);
@@ -10062,10 +10057,24 @@ void mg_ota_url_check(struct mg_mgr *mgr, const char *my_version,
void mg_ota_poll(struct mg_mgr *mgr) { void mg_ota_poll(struct mg_mgr *mgr) {
static uint64_t t = 5000; // Fire first time 5 sec after boot static uint64_t t = 5000; // Fire first time 5 sec after boot
static uint64_t feed_timer; // Advances by 500ms per tick; tracks elapsed time
if (MG_OTA_STATE_GET() == MG_OTA_TESTING &&
mg_timer_expired(&feed_timer, 500, mg_millis())) {
if (feed_timer < (uint64_t) MG_OTA_ROLLBACK_TIMEOUT_SECONDS * 1000ULL) {
MG_OTA_ROLLBACK_TIMER_FEED(); // Feed watchdog every 500ms
} else if (s_autocommit_ok) {
MG_INFO(("Auto-committing firmware"));
MG_OTA_STATE_SET(MG_OTA_CONFIRMED); // Stop feeding → IWDG resets cleanly
} else {
MG_INFO(("No commit confirmation, rolling back"));
// Stop feeding → IWDG fires → resets into MG_OTA_FAILED → rollback
}
}
if (MG_OTA_URL != NULL && if (MG_OTA_URL != NULL &&
mg_timer_expired(&t, MG_OTA_PULL_INTERVAL_SECONDS * 1000, mg_millis())) { mg_timer_expired(&t, MG_OTA_PULL_INTERVAL_SECONDS * 1000, mg_millis())) {
mg_ota_url_check(mgr, MG_OTA_FIRMWARE_VERSION, MG_OTA_URL, mg_ota_url_check(mgr, MG_OTA_URL, MG_OTA_STATUS_FN);
MG_OTA_STATUS_FN);
} }
} }
@@ -11654,6 +11663,10 @@ static size_t flash_size(void) {
return (MG_REG(MG_FLASH_SIZE_REG_LOCATION) & 0xFFFF) * 1024; return (MG_REG(MG_FLASH_SIZE_REG_LOCATION) & 0xFFFF) * 1024;
} }
MG_IRAM static size_t last_sector_size(void) {
return (STM_DEV_ID >= 0x449 ? 256 : 128) * 1024;
}
MG_IRAM static int is_dualbank(void) { MG_IRAM static int is_dualbank(void) {
// only F42x/F43x series (0x419) support dual bank // only F42x/F43x series (0x419) support dual bank
return STM_DEV_ID == 0x419; return STM_DEV_ID == 0x419;
@@ -11771,12 +11784,7 @@ MG_IRAM static bool mg_stm32f_erase(void *addr) {
return ok; return ok;
} }
MG_IRAM static bool mg_stm32f_swap(void) { MG_IRAM static void single_bank_swap(char *p1, char *p2, size_t s, size_t ss);
// STM32 F42x/F43x support dual bank, however, the memory mapping
// change will not be carried through a hard reset. Therefore, we will use
// the single bank approach for this family as well.
return true;
}
static bool s_flash_irq_disabled; static bool s_flash_irq_disabled;
@@ -11810,19 +11818,54 @@ MG_IRAM static bool mg_stm32f_write(void *addr, const void *buf, size_t len) {
return ok; return ok;
} }
// just overwrite instead of swap MG_IRAM static bool mg_stm32f_swap(void) {
MG_IRAM void single_bank_swap(char *p1, char *p2, size_t size) { // STM32 F42x/F43x support dual bank, however, the memory mapping
// no stdlib calls here // change will not be carried through a hard reset. Therefore, we use
mg_stm32f_write(p1, p2, size); // the single-bank scratch-sector swap for this family as well.
*(volatile unsigned long *) 0xe000ed0c = 0x5fa0004; size_t ss = last_sector_size();
char *p1 = (char *) s_mg_flash_stm32f.start;
char *p2;
size_t s;
s_mg_flash_stm32f.size = flash_size();
p2 = p1 + s_mg_flash_stm32f.size / 2;
s = s_mg_flash_stm32f.size / 2 - ss;
MG_INFO(("Swapping partitions, %u bytes", s));
MG_INFO(("Do NOT power off..."));
MG_OTA_ROLLBACK_TIMER_FEED();
mg_log_level = MG_LL_NONE;
s_flash_irq_disabled = true;
single_bank_swap(p1, p2, s, ss);
return true; // unreachable
}
// True exchange between first half [p1..p1+s) and second half [p2..p2+s),
// using the last sector of the second half as scratch. Runs from RAM and resets.
MG_IRAM static void single_bank_swap(char *p1, char *p2, size_t s, size_t ss) {
char *scratch = p2 + s;
for (size_t ofs = 0; ofs < s; ofs += ss) {
MG_OTA_ROLLBACK_TIMER_FEED();
mg_stm32f_write(scratch, p1 + ofs, ss);
MG_OTA_ROLLBACK_TIMER_FEED();
mg_stm32f_write(p1 + ofs, p2 + ofs, ss);
MG_OTA_ROLLBACK_TIMER_FEED();
mg_stm32f_write(p2 + ofs, scratch, ss);
}
*(volatile uint32_t *) 0xe000ed0cU = 0x5fa0004U; // NVIC_SystemReset()
} }
bool mg_ota_begin(size_t new_firmware_size) { bool mg_ota_begin(size_t new_firmware_size) {
size_t max;
s_mg_flash_stm32f.size = flash_size(); s_mg_flash_stm32f.size = flash_size();
#ifdef __ZEPHYR__ #ifdef __ZEPHYR__
*((uint32_t *)0xE000ED94) = 0; *((uint32_t *)0xE000ED94) = 0;
MG_DEBUG(("Jailbreak %s", *((uint32_t *)0xE000ED94) == 0 ? "successful" : "failed")); MG_DEBUG(("Jailbreak %s", *((uint32_t *)0xE000ED94) == 0 ? "successful" : "failed"));
#endif #endif
max = s_mg_flash_stm32f.size / 2 - last_sector_size();
if (new_firmware_size > max) {
MG_ERROR(("Firmware %lu too big for single-bank OTA, max %lu",
new_firmware_size, max));
return false;
}
return mg_ota_flash_begin(new_firmware_size, &s_mg_flash_stm32f); return mg_ota_flash_begin(new_firmware_size, &s_mg_flash_stm32f);
} }
@@ -11831,19 +11874,7 @@ bool mg_ota_write(const void *buf, size_t len) {
} }
bool mg_ota_end(void) { bool mg_ota_end(void) {
if (mg_ota_flash_end(&s_mg_flash_stm32f)) { mg_ota_flash_end(&s_mg_flash_stm32f);
// Swap partitions. Pray power does not go away
MG_INFO(("Swapping partitions, size %u (%u sectors)",
s_mg_flash_stm32f.size, STM_DEV_ID == 0x449 ? 8 : 12));
MG_INFO(("Do NOT power off..."));
mg_log_level = MG_LL_NONE;
s_flash_irq_disabled = true;
char *p1 = (char *) s_mg_flash_stm32f.start;
char *p2 = p1 + s_mg_flash_stm32f.size / 2;
size_t size = s_mg_flash_stm32f.size / 2;
// Runs in RAM, will reset when finished
single_bank_swap(p1, p2, size);
}
return false; return false;
} }
struct mg_flash *mg_flash = &s_mg_flash_stm32f; struct mg_flash *mg_flash = &s_mg_flash_stm32f;
@@ -11863,7 +11894,7 @@ static bool mg_stm32h5_swap(void);
static struct mg_flash s_mg_flash_stm32h5 = { static struct mg_flash s_mg_flash_stm32h5 = {
(void *) 0x08000000, // Start (void *) 0x08000000, // Start
2 * 1024 * 1024, // Size, 2Mb 0, // Size, FLASHSIZE_BASE
8 * 1024, // Sector size, 8k 8 * 1024, // Sector size, 8k
16, // Align, 128 bit 16, // Align, 128 bit
mg_stm32h5_write, mg_stm32h5_write,
@@ -11879,6 +11910,18 @@ static struct mg_flash s_mg_flash_stm32h5 = {
#define FLASH_NSCCR (MG_FLASH_BASE + 0x30) #define FLASH_NSCCR (MG_FLASH_BASE + 0x30)
#define FLASH_OPTSR_CUR (MG_FLASH_BASE + 0x50) #define FLASH_OPTSR_CUR (MG_FLASH_BASE + 0x50)
#define FLASH_OPTSR_PRG (MG_FLASH_BASE + 0x54) #define FLASH_OPTSR_PRG (MG_FLASH_BASE + 0x54)
#ifndef FLASHSIZE_BASE
#define FLASHSIZE_BASE 0x08FFF80CUL
#endif
static size_t flash_size(void) {
uint32_t kb = MG_REG(FLASHSIZE_BASE) & 0xFFFF;
return (kb == 0 || kb == 0xFFFF) ? 2 * 1024 * 1024 : kb * 1024;
}
static uint32_t sectors_per_bank(void) {
return (uint32_t) (s_mg_flash_stm32h5.size / 2 / s_mg_flash_stm32h5.secsz);
}
static void flash_unlock(void) { static void flash_unlock(void) {
static bool unlocked = false; static bool unlocked = false;
@@ -11925,15 +11968,16 @@ static bool mg_stm32h5_erase(void *location) {
} else { } else {
uintptr_t diff = (char *) location - (char *) s_mg_flash_stm32h5.start; uintptr_t diff = (char *) location - (char *) s_mg_flash_stm32h5.start;
uint32_t sector = diff / s_mg_flash_stm32h5.secsz; uint32_t sector = diff / s_mg_flash_stm32h5.secsz;
uint32_t bank_sectors = sectors_per_bank();
uint32_t saved_cr = MG_REG(FLASH_NSCR); // Save CR value uint32_t saved_cr = MG_REG(FLASH_NSCR); // Save CR value
flash_unlock(); flash_unlock();
flash_clear_err(); flash_clear_err();
MG_REG(FLASH_NSCR) = 0; MG_REG(FLASH_NSCR) = 0;
if ((sector < 128 && flash_bank_is_swapped()) || if ((sector < bank_sectors && flash_bank_is_swapped()) ||
(sector > 127 && !flash_bank_is_swapped())) { (sector >= bank_sectors && !flash_bank_is_swapped())) {
MG_REG(FLASH_NSCR) |= MG_BIT(31); // Set FLASH_CR_BKSEL MG_REG(FLASH_NSCR) |= MG_BIT(31); // Set FLASH_CR_BKSEL
} }
if (sector > 127) sector -= 128; if (sector >= bank_sectors) sector -= bank_sectors;
MG_REG(FLASH_NSCR) |= MG_BIT(2) | (sector << 6); // Erase | sector_num MG_REG(FLASH_NSCR) |= MG_BIT(2) | (sector << 6); // Erase | sector_num
MG_REG(FLASH_NSCR) |= MG_BIT(5); // Start erasing MG_REG(FLASH_NSCR) |= MG_BIT(5); // Start erasing
flash_wait(); flash_wait();
@@ -11987,6 +12031,7 @@ static bool mg_stm32h5_write(void *addr, const void *buf, size_t len) {
} }
bool mg_ota_begin(size_t new_firmware_size) { bool mg_ota_begin(size_t new_firmware_size) {
s_mg_flash_stm32h5.size = flash_size();
#ifdef __ZEPHYR__ #ifdef __ZEPHYR__
*((uint32_t *)0xE000ED94) = 0; *((uint32_t *)0xE000ED94) = 0;
MG_DEBUG(("Jailbreak %s", *((uint32_t *)0xE000ED94) == 0 ? "successful" : "failed")); MG_DEBUG(("Jailbreak %s", *((uint32_t *)0xE000ED94) == 0 ? "successful" : "failed"));
@@ -11999,9 +12044,8 @@ bool mg_ota_write(const void *buf, size_t len) {
} }
bool mg_ota_end(void) { bool mg_ota_end(void) {
if (!mg_ota_flash_end(&s_mg_flash_stm32h5)) return false; mg_ota_flash_end(&s_mg_flash_stm32h5);
*(volatile uint32_t *) 0xe000ed0c = 0x5fa0004U; // NVIC_SystemReset() return false;
return true;
} }
struct mg_flash *mg_flash = &s_mg_flash_stm32h5; struct mg_flash *mg_flash = &s_mg_flash_stm32h5;
#endif #endif
@@ -12048,6 +12092,11 @@ static struct mg_flash s_mg_flash_stm32h7 = {
#define IS_DUALCORE() (MG_OTA == MG_OTA_STM32H7_DUAL_CORE) #define IS_DUALCORE() (MG_OTA == MG_OTA_STM32H7_DUAL_CORE)
MG_IRAM static size_t flash_size(void) {
size_t size = MG_REG(FLASH_SIZE_REG) * 1024;
return IS_DUALCORE() ? size / 2 : size;
}
MG_IRAM static bool is_dualbank(void) { MG_IRAM static bool is_dualbank(void) {
if (IS_DUALCORE()) { if (IS_DUALCORE()) {
// H745/H755 and H747/H757 are running on dual core. // H745/H755 and H747/H757 are running on dual core.
@@ -12133,8 +12182,27 @@ MG_IRAM static bool mg_stm32h7_erase(void *addr) {
return ok; return ok;
} }
MG_IRAM static void single_bank_swap(char *p1, char *p2, size_t s, size_t ss);
static bool s_flash_irq_disabled;
MG_IRAM static bool mg_stm32h7_swap(void) { MG_IRAM static bool mg_stm32h7_swap(void) {
if (!is_dualbank()) return true; s_mg_flash_stm32h7.size = flash_size();
if (!is_dualbank()) {
// True sector-by-sector swap using the last sector of the second half as
// scratch. Resets inside single_bank_swap, never returns.
size_t ss = s_mg_flash_stm32h7.secsz;
char *p1 = (char *) s_mg_flash_stm32h7.start;
char *p2 = p1 + s_mg_flash_stm32h7.size / 2;
size_t s = s_mg_flash_stm32h7.size / 2 - ss;
MG_INFO(("Swapping partitions, %u bytes (%u sectors)", s, s / ss));
MG_INFO(("Do NOT power off..."));
MG_OTA_ROLLBACK_TIMER_FEED();
mg_log_level = MG_LL_NONE;
s_flash_irq_disabled = true;
single_bank_swap(p1, p2, s, ss);
return true; // unreachable
}
uint32_t bank = FLASH_BASE1; uint32_t bank = FLASH_BASE1;
uint32_t desired = flash_bank_is_swapped(bank) ? 0 : MG_BIT(31); uint32_t desired = flash_bank_is_swapped(bank) ? 0 : MG_BIT(31);
flash_unlock(); flash_unlock();
@@ -12145,8 +12213,6 @@ MG_IRAM static bool mg_stm32h7_swap(void) {
return true; return true;
} }
static bool s_flash_irq_disabled;
MG_IRAM static bool mg_stm32h7_write(void *addr, const void *buf, size_t len) { MG_IRAM static bool mg_stm32h7_write(void *addr, const void *buf, size_t len) {
if ((len % s_mg_flash_stm32h7.align) != 0) { if ((len % s_mg_flash_stm32h7.align) != 0) {
MG_ERROR(("%lu is not aligned to %lu", len, s_mg_flash_stm32h7.align)); MG_ERROR(("%lu is not aligned to %lu", len, s_mg_flash_stm32h7.align));
@@ -12179,25 +12245,36 @@ MG_IRAM static bool mg_stm32h7_write(void *addr, const void *buf, size_t len) {
return ok; return ok;
} }
// just overwrite instead of swap // True sector-by-sector exchange between first half [p1..p1+s) and second half
// [p2..p2+s), using the sector at p2+s as scratch. Runs from RAM. Resets on
// completion. Symmetric: calling twice returns flash to original state.
MG_IRAM static void single_bank_swap(char *p1, char *p2, size_t s, size_t ss) { MG_IRAM static void single_bank_swap(char *p1, char *p2, size_t s, size_t ss) {
// no stdlib calls here char *scratch = p2 + s; // Last sector of second half, reserved as scratch
for (size_t ofs = 0; ofs < s; ofs += ss) { for (size_t ofs = 0; ofs < s; ofs += ss) {
mg_stm32h7_write(p1 + ofs, p2 + ofs, ss); MG_OTA_ROLLBACK_TIMER_FEED();
mg_stm32h7_write(scratch, p1 + ofs, ss); // Save p1[i] to scratch
MG_OTA_ROLLBACK_TIMER_FEED();
mg_stm32h7_write(p1 + ofs, p2 + ofs, ss); // Copy p2[i] to p1[i]
MG_OTA_ROLLBACK_TIMER_FEED();
mg_stm32h7_write(p2 + ofs, scratch, ss); // Copy scratch to p2[i]
} }
*(volatile uint32_t *) 0xe000ed0cU = 0x5fa0004U; // NVIC_SystemReset() *(volatile uint32_t *) 0xe000ed0cU = 0x5fa0004U; // NVIC_SystemReset()
} }
bool mg_ota_begin(size_t new_firmware_size) { bool mg_ota_begin(size_t new_firmware_size) {
s_mg_flash_stm32h7.size = MG_REG(FLASH_SIZE_REG) * 1024; s_mg_flash_stm32h7.size = flash_size();
if (IS_DUALCORE()) {
// Using only the 1st bank (mapped to CM7)
s_mg_flash_stm32h7.size /= 2;
}
#ifdef __ZEPHYR__ #ifdef __ZEPHYR__
*((uint32_t *) 0xE000ED94) = 0; *((uint32_t *) 0xE000ED94) = 0;
MG_DEBUG(("Jailbreak %s", *((uint32_t *) 0xE000ED94) == 0 ? "ok" : "failed")); MG_DEBUG(("Jailbreak %s", *((uint32_t *) 0xE000ED94) == 0 ? "ok" : "failed"));
#endif #endif
if (!is_dualbank()) {
// Last sector of the second half is reserved as swap scratch; enforce limit.
size_t max = s_mg_flash_stm32h7.size / 2 - s_mg_flash_stm32h7.secsz;
if (new_firmware_size > max) {
MG_ERROR(("Firmware %lu too big for single-bank OTA, max %lu", new_firmware_size, max));
return false;
}
}
return mg_ota_flash_begin(new_firmware_size, &s_mg_flash_stm32h7); return mg_ota_flash_begin(new_firmware_size, &s_mg_flash_stm32h7);
} }
@@ -12210,19 +12287,6 @@ bool mg_ota_end(void) {
if (is_dualbank()) { if (is_dualbank()) {
// Bank swap is deferred until reset, been executing in flash, reset // Bank swap is deferred until reset, been executing in flash, reset
*(volatile uint32_t *) 0xe000ed0cU = 0x5fa0004U; // NVIC_SystemReset() *(volatile uint32_t *) 0xe000ed0cU = 0x5fa0004U; // NVIC_SystemReset()
} else {
// Swap partitions. Pray power does not go away
MG_INFO(("Swapping partitions, size %u (%u sectors)",
s_mg_flash_stm32h7.size,
s_mg_flash_stm32h7.size / s_mg_flash_stm32h7.secsz));
MG_INFO(("Do NOT power off..."));
mg_log_level = MG_LL_NONE;
s_flash_irq_disabled = true;
// Runs in RAM, will reset when finished
single_bank_swap(
(char *) s_mg_flash_stm32h7.start,
(char *) s_mg_flash_stm32h7.start + s_mg_flash_stm32h7.size / 2,
s_mg_flash_stm32h7.size / 2, s_mg_flash_stm32h7.secsz);
} }
} }
return false; return false;
+106 -29
View File
@@ -194,6 +194,61 @@ extern "C" {
#define MG_OTA MG_OTA_STM32H7 #define MG_OTA MG_OTA_STM32H7
#endif #endif
// OTA rollback timer via IWDG. Fixed 10 s hang-detection window (prescaler
// /1024, LSI ~32 kHz: reload = 10 * 32000 / 1024 = 312). mg_ota_poll() feeds
// the watchdog every 500 ms while in MG_OTA_TESTING state; if the event loop
// hangs for 10 s, the IWDG fires and the device rolls back.
// STM32H7 names the peripheral IWDG1; all others use IWDG.
// The total evaluation period is set by MG_OTA_ROLLBACK_TIMEOUT_SECONDS.
// STM32H7 names the watchdog peripheral IWDG1; all other families use IWDG.
#if defined(IWDG1) && !defined(IWDG)
#define MG_IWDG IWDG1
#else
#define MG_IWDG IWDG
#endif
// PR=6 → /256 prescaler, RLR=1250 → ~10 s at LSI 32 kHz.
// Works on F4/F7/H5/H7; uses direct register writes so it is safe before HAL_Init().
#ifndef MG_OTA_ROLLBACK_TIMER_START
#define MG_OTA_ROLLBACK_TIMER_START() \
do { \
MG_IWDG->KR = 0xCCCCU; /* start + LSI */ \
MG_IWDG->KR = 0x5555U; /* unlock PR/RLR */ \
MG_IWDG->PR = 6U; /* /256 */ \
MG_IWDG->RLR = 1250U; /* 10 s */ \
while (MG_IWDG->SR & (IWDG_SR_PVU | IWDG_SR_RVU)) (void) 0; \
MG_IWDG->KR = 0xAAAAU; /* reload */ \
} while (0)
#endif
// Feed (reload) the IWDG. Called from mg_ota_poll() every 500 ms.
#ifndef MG_OTA_ROLLBACK_TIMER_FEED
#define MG_OTA_ROLLBACK_TIMER_FEED() (MG_IWDG->KR = 0xAAAAU)
#endif
// OTA state in a backup register that survives warm resets but clears on POR.
// STATE_GET reads work without any setup. STATE_SET enables the RTC APB clock
// and unlocks backup domain write access via direct register writes, so it is
// safe to call from any context (before or after HAL_Init).
// H5 (TAMP->BKP0R), H7 (RTC->BKP0R), F4/F7 (RTC->BKP0R).
#ifndef MG_OTA_STATE_GET
#if defined(TAMP)
#define MG_OTA_STATE_GET() (TAMP->BKP0R)
#define MG_OTA_STATE_SET(v) \
(RCC->APB3ENR |= RCC_APB3ENR_RTCAPBEN, PWR->DBPCR |= PWR_DBPCR_DBP, \
(TAMP->BKP0R = (uint32_t) (v)))
#elif defined(RCC_APB4ENR_RTCAPBEN)
#define MG_OTA_STATE_GET() (RTC->BKP0R)
#define MG_OTA_STATE_SET(v) \
(RCC->APB4ENR |= RCC_APB4ENR_RTCAPBEN, PWR->CR1 |= PWR_CR1_DBP, \
(RTC->BKP0R = (uint32_t) (v)))
#else
#define MG_OTA_STATE_GET() (RTC->BKP0R)
#define MG_OTA_STATE_SET(v) \
(RCC->APB1ENR |= RCC_APB1ENR_PWREN, PWR->CR |= PWR_CR_DBP, \
(RTC->BKP0R = (uint32_t) (v)))
#endif
#endif
// use HAL-defined execute-in-ram section // use HAL-defined execute-in-ram section
#ifndef MG_IRAM #ifndef MG_IRAM
#define MG_IRAM __attribute__((section(".RamFunc"))) #define MG_IRAM __attribute__((section(".RamFunc")))
@@ -4090,20 +4145,32 @@ bool mg_ota_end(void);
} while (0) } while (0)
#endif #endif
// MG_OTA_ROLLBACK_TIMER_START(seconds): arm a hardware watchdog as the rollback // Arm a hardware watchdog. mg_ota_poll() feeds it while in MG_OTA_TESTING state;
// deadline. If it fires before MG_OTA_STATE_SET(MG_OTA_CONFIRMED), the device // if the event loop hangs, it fires, resets the device, and MG_OTA_FAILED
// resets and MG_OTA_STATE_GET() == MG_OTA_FAILED triggers MG_OTA_ROLLBACK(). // triggers MG_OTA_ROLLBACK() on next boot. Timeout is arch-defined.
#ifndef MG_OTA_ROLLBACK_TIMER_START #ifndef MG_OTA_ROLLBACK_TIMER_START
#define MG_OTA_ROLLBACK_TIMER_START(seconds) (void) (seconds) #define MG_OTA_ROLLBACK_TIMER_START()
#endif
#ifndef MG_OTA_ROLLBACK_TIMER_FEED
#define MG_OTA_ROLLBACK_TIMER_FEED()
#endif #endif
// OTA state values for MG_OTA_STATE_GET / MG_OTA_STATE_SET. // OTA state values for MG_OTA_STATE_GET / MG_OTA_STATE_SET.
// MG_OTA_CONFIRMED: firmware is committed, normal operation. // MG_OTA_CONFIRMED: firmware is committed, normal operation.
// MG_OTA_TESTING: new firmware booted for the first time; IWDG is armed. // MG_OTA_TESTING: new firmware booted for the first time; IWDG is armed.
// If the device reboots while in this state, it transitions // If the device reboots while in this state, it transitions
// to MG_OTA_FAILED and rolls back on the next boot. // to MG_OTA_FAILED and rolls back on the next boot.
// MG_OTA_FAILED: previous boot did not commit in time; rollback on next boot. // MG_OTA_FAILED: previous boot did not commit in time; rollback on next boot.
enum { MG_OTA_CONFIRMED = 0, MG_OTA_TESTING = 1, MG_OTA_FAILED = 2 }; // MG_OTA_ROLLEDBACK: device rolled back to previous firmware. Sent to the OTA
// server as boot=3 on every poll until a new OTA succeeds,
// which overwrites this state with MG_OTA_TESTING.
enum {
MG_OTA_CONFIRMED = 0,
MG_OTA_TESTING = 1,
MG_OTA_FAILED = 2,
MG_OTA_ROLLEDBACK = 3
};
// Persistent OTA state storage. Override in mongoose_config.h to persist state across resets. // Persistent OTA state storage. Override in mongoose_config.h to persist state across resets.
#ifndef MG_OTA_STATE_GET #ifndef MG_OTA_STATE_GET
@@ -4113,31 +4180,47 @@ enum { MG_OTA_CONFIRMED = 0, MG_OTA_TESTING = 1, MG_OTA_FAILED = 2 };
#define MG_OTA_STATE_SET(val) (void) (val) #define MG_OTA_STATE_SET(val) (void) (val)
#endif #endif
// Call once at boot. Arms the rollback watchdog for (seconds) seconds when // Evaluation window for new firmware in seconds. mg_ota_poll() feeds the
// the new firmware is in TESTING state. Rolls back immediately if FAILED. // rollback watchdog while in MG_OTA_TESTING state and within this window;
// when it expires, auto-commits if the OTA server confirmed the same version,
// otherwise stops feeding and the watchdog triggers a rollback.
#ifndef MG_OTA_ROLLBACK_TIMEOUT_SECONDS
#define MG_OTA_ROLLBACK_TIMEOUT_SECONDS 120
#endif
// Call once at boot, after clock and backup-domain init (e.g. after HAL_Init()
// + SystemClock_Config() on CubeMX projects, or after hal_clock_init() on
// Mongoose tutorials). The backup domain must be write-accessible so the state
// transition TESTING → FAILED persists across resets.
// Arms the rollback watchdog if new firmware is in TESTING state, rolls back
// immediately if FAILED. mg_ota_poll() feeds the watchdog and auto-commits
// when the server confirms the same version within the window.
// boot (TESTING) → set FAILED → arm IWDG → run firmware // boot (TESTING) → set FAILED → arm IWDG → run firmware
// ├── commit called → set CONFIRMED → NVIC_SystemReset() → clean boot // ├── server ok + window expires → set CONFIRMED → IWDG resets cleanly
// └── IWDG fires → hard reset → boot → state is still FAILED → rollback // └── no server ok + window expires → stop feeding → IWDG fires → FAILED → rollback
#define MG_OTA_BOOT_CHECK(seconds) \ #define MG_OTA_BOOT_CHECK() \
do { \ do { \
if (MG_OTA_STATE_GET() == MG_OTA_FAILED) { \ if (MG_OTA_STATE_GET() == MG_OTA_FAILED) { \
MG_OTA_STATE_SET(MG_OTA_CONFIRMED); \ MG_OTA_STATE_SET(MG_OTA_ROLLEDBACK); \
MG_INFO(("Commit deadline expired, rolling back")); \ MG_INFO(("Commit deadline expired, rolling back")); \
MG_OTA_ROLLBACK(); \ MG_OTA_ROLLBACK(); \
} else if (MG_OTA_STATE_GET() == MG_OTA_TESTING) { \ } else if (MG_OTA_STATE_GET() == MG_OTA_TESTING) { \
MG_OTA_STATE_SET(MG_OTA_FAILED); \ MG_OTA_STATE_SET(MG_OTA_FAILED); \
MG_INFO(("New firmware: commit within %u sec or rolls back", \ MG_INFO(("New firmware: commit within %u sec or rolls back", \
(unsigned) (seconds))); \ (unsigned) MG_OTA_ROLLBACK_TIMEOUT_SECONDS)); \
MG_OTA_ROLLBACK_TIMER_START(seconds); \ MG_OTA_ROLLBACK_TIMER_START(); \
} \ } \
} while (0) } while (0)
// Pull-based OTA over HTTP. Fetches the given URL, which should return a JSON // Pull-based OTA over HTTP. Fetches metadata_url, which must return a JSON
// object like: { "version": "1.2.3", "url": "FIRMWARE_URL", "size": 324645 } // object like: { "version": "1.2.3", "url": "FIRMWARE_URL", "size": 324645 }
// If the current firmware version differs from the JSON version, downloads // If the server version differs from MG_OTA_FIRMWARE_VERSION, downloads
// FIRMWARE_URL and performs the OTA update. // FIRMWARE_URL and performs the OTA update. fn is called on every outcome:
void mg_ota_url_check(struct mg_mgr *mgr, const char *current_version, // NULL on successful flash, "Same version" when already up to date, or an
const char *metadata_url, void (*fn)(const char *status)); // error string on failure. Pass NULL to use the default handler which logs
// failures via MG_ERROR.
void mg_ota_url_check(struct mg_mgr *mgr, const char *metadata_url,
void (*fn)(const char *error_message));
// Firmware info URL for mg_ota_poll() which calls mg_ota_url_check(). // Firmware info URL for mg_ota_poll() which calls mg_ota_url_check().
// Example: "http://mongoose.ws/ota/u/0/ota.json". See http://mongoose.ws/ota/ // Example: "http://mongoose.ws/ota/u/0/ota.json". See http://mongoose.ws/ota/
@@ -4161,12 +4244,6 @@ void mg_ota_url_check(struct mg_mgr *mgr, const char *current_version,
#define MG_OTA_MAX_VERSION_LEN 64 #define MG_OTA_MAX_VERSION_LEN 64
#endif #endif
// Scannable "MG_VERSION:<MG_OTA_FIRMWARE_VERSION>" tag embedded in every
// firmware binary, for server-side extraction. mg_mgr_init() stashes its
// address in mgr->userdata, which is what keeps it from being stripped by
// -Wl,--gc-sections on builds that never poll for OTAs
extern const char mg_fw_version[];
// How often mg_ota_poll() checks for a firmware update, in seconds // How often mg_ota_poll() checks for a firmware update, in seconds
#ifndef MG_OTA_PULL_INTERVAL_SECONDS #ifndef MG_OTA_PULL_INTERVAL_SECONDS
#define MG_OTA_PULL_INTERVAL_SECONDS 60 #define MG_OTA_PULL_INTERVAL_SECONDS 60
+55
View File
@@ -62,6 +62,61 @@
#define MG_OTA MG_OTA_STM32H7 #define MG_OTA MG_OTA_STM32H7
#endif #endif
// OTA rollback timer via IWDG. Fixed 10 s hang-detection window (prescaler
// /1024, LSI ~32 kHz: reload = 10 * 32000 / 1024 = 312). mg_ota_poll() feeds
// the watchdog every 500 ms while in MG_OTA_TESTING state; if the event loop
// hangs for 10 s, the IWDG fires and the device rolls back.
// STM32H7 names the peripheral IWDG1; all others use IWDG.
// The total evaluation period is set by MG_OTA_ROLLBACK_TIMEOUT_SECONDS.
// STM32H7 names the watchdog peripheral IWDG1; all other families use IWDG.
#if defined(IWDG1) && !defined(IWDG)
#define MG_IWDG IWDG1
#else
#define MG_IWDG IWDG
#endif
// PR=6 → /256 prescaler, RLR=1250 → ~10 s at LSI 32 kHz.
// Works on F4/F7/H5/H7; uses direct register writes so it is safe before HAL_Init().
#ifndef MG_OTA_ROLLBACK_TIMER_START
#define MG_OTA_ROLLBACK_TIMER_START() \
do { \
MG_IWDG->KR = 0xCCCCU; /* start + LSI */ \
MG_IWDG->KR = 0x5555U; /* unlock PR/RLR */ \
MG_IWDG->PR = 6U; /* /256 */ \
MG_IWDG->RLR = 1250U; /* 10 s */ \
while (MG_IWDG->SR & (IWDG_SR_PVU | IWDG_SR_RVU)) (void) 0; \
MG_IWDG->KR = 0xAAAAU; /* reload */ \
} while (0)
#endif
// Feed (reload) the IWDG. Called from mg_ota_poll() every 500 ms.
#ifndef MG_OTA_ROLLBACK_TIMER_FEED
#define MG_OTA_ROLLBACK_TIMER_FEED() (MG_IWDG->KR = 0xAAAAU)
#endif
// OTA state in a backup register that survives warm resets but clears on POR.
// STATE_GET reads work without any setup. STATE_SET enables the RTC APB clock
// and unlocks backup domain write access via direct register writes, so it is
// safe to call from any context (before or after HAL_Init).
// H5 (TAMP->BKP0R), H7 (RTC->BKP0R), F4/F7 (RTC->BKP0R).
#ifndef MG_OTA_STATE_GET
#if defined(TAMP)
#define MG_OTA_STATE_GET() (TAMP->BKP0R)
#define MG_OTA_STATE_SET(v) \
(RCC->APB3ENR |= RCC_APB3ENR_RTCAPBEN, PWR->DBPCR |= PWR_DBPCR_DBP, \
(TAMP->BKP0R = (uint32_t) (v)))
#elif defined(RCC_APB4ENR_RTCAPBEN)
#define MG_OTA_STATE_GET() (RTC->BKP0R)
#define MG_OTA_STATE_SET(v) \
(RCC->APB4ENR |= RCC_APB4ENR_RTCAPBEN, PWR->CR1 |= PWR_CR1_DBP, \
(RTC->BKP0R = (uint32_t) (v)))
#else
#define MG_OTA_STATE_GET() (RTC->BKP0R)
#define MG_OTA_STATE_SET(v) \
(RCC->APB1ENR |= RCC_APB1ENR_PWREN, PWR->CR |= PWR_CR_DBP, \
(RTC->BKP0R = (uint32_t) (v)))
#endif
#endif
// use HAL-defined execute-in-ram section // use HAL-defined execute-in-ram section
#ifndef MG_IRAM #ifndef MG_IRAM
#define MG_IRAM __attribute__((section(".RamFunc"))) #define MG_IRAM __attribute__((section(".RamFunc")))
-5
View File
@@ -279,11 +279,6 @@ void mg_mgr_free(struct mg_mgr *mgr) {
void mg_mgr_init(struct mg_mgr *mgr) { void mg_mgr_init(struct mg_mgr *mgr) {
memset(mgr, 0, sizeof(*mgr)); memset(mgr, 0, sizeof(*mgr));
// Anchor mg_fw_version: a real store into a struct field the optimiser
// can't elide forces the linker to keep the section alive even when no
// OTA polling code runs it. Callers that want mgr->userdata for their own
// purposes simply overwrite it after this call returns
mgr->userdata = (void *) mg_fw_version;
#if MG_ENABLE_EPOLL #if MG_ENABLE_EPOLL
if ((mgr->epoll_fd = epoll_create1(EPOLL_CLOEXEC)) < 0) if ((mgr->epoll_fd = epoll_create1(EPOLL_CLOEXEC)) < 0)
MG_ERROR(("epoll_create1 errno %d", errno)); MG_ERROR(("epoll_create1 errno %d", errno));
+29 -17
View File
@@ -11,20 +11,18 @@
#endif #endif
// Scannable version tag embedded in every firmware binary, for server-side // Scannable version tag embedded in every firmware binary, for server-side
// version extraction. Non-static so mg_mgr_init() can stash its address in // version extraction. The version string starts after the 11-char "MG_VERSION:" prefix.
// mgr->userdata: that store is what actually keeps -Wl,--gc-sections from static const char mg_fw_version[] = "MG_VERSION:" MG_OTA_FIRMWARE_VERSION;
// stripping it on builds that never poll for OTAs (an attribute alone does
// not - the linker drops unreferenced sections regardless of "used"/"retain") static bool s_autocommit_ok; // True after OTA server confirms "same version"
const char mg_fw_version[] = "MG_VERSION:" MG_OTA_FIRMWARE_VERSION;
static struct mg_ota_state { static struct mg_ota_state {
char my_version[MG_OTA_MAX_VERSION_LEN];
char json_url[MG_OTA_MAX_URL_LEN]; char json_url[MG_OTA_MAX_URL_LEN];
char version[MG_OTA_MAX_VERSION_LEN]; char version[MG_OTA_MAX_VERSION_LEN];
char url[MG_OTA_MAX_URL_LEN]; char url[MG_OTA_MAX_URL_LEN];
size_t size; size_t size;
uint8_t sha256[32]; uint8_t sha256[32];
void (*fn)(const char *status); void (*fn)(const char *error_message);
} *s_ota; } *s_ota;
static void s_firmware_fn(struct mg_connection *c, int ev, void *ev_data); static void s_firmware_fn(struct mg_connection *c, int ev, void *ev_data);
@@ -52,15 +50,15 @@ static void s_version_fn(struct mg_connection *c, int ev, void *ev_data) {
mg_ota_device_id(id, sizeof(id)); mg_ota_device_id(id, sizeof(id));
id[sizeof(id) - 1] = '\0'; id[sizeof(id) - 1] = '\0';
mg_printf(c, mg_printf(c,
"GET %s%sarch=%d&version=%s&id=%s&interval=%d HTTP/1.1\r\n" "GET %s%sarch=%d&version=%s&id=%s&interval=%d&boot=%d HTTP/1.1\r\n"
"Host: %.*s\r\n" "Host: %.*s\r\n"
"Connection: close\r\n\r\n", "Connection: close\r\n\r\n",
uri, sep, MG_ARCH, s_ota->my_version, id, uri, sep, MG_ARCH, mg_fw_version + 11, id,
MG_OTA_PULL_INTERVAL_SECONDS, host.len, host.buf); MG_OTA_PULL_INTERVAL_SECONDS, MG_OTA_STATE_GET(), host.len, host.buf);
} else if (ev == MG_EV_HTTP_MSG) { } else if (ev == MG_EV_HTTP_MSG) {
struct mg_http_message *hm = (struct mg_http_message *) ev_data; struct mg_http_message *hm = (struct mg_http_message *) ev_data;
double result; double result;
MG_DEBUG(("Got metadata:\n%.*s", hm->body.len, hm->body.buf)); MG_DEBUG(("Got metadata: %.*s", hm->body.len, hm->body.buf));
if (mg_http_status(hm) != 200 || mg_json_get(hm->body, "$", NULL) != 0 || if (mg_http_status(hm) != 200 || mg_json_get(hm->body, "$", NULL) != 0 ||
!mg_json_unescape(hm->body, "$.version", s_ota->version, !mg_json_unescape(hm->body, "$.version", s_ota->version,
sizeof(s_ota->version)) || sizeof(s_ota->version)) ||
@@ -72,7 +70,8 @@ static void s_version_fn(struct mg_connection *c, int ev, void *ev_data) {
s_ota->fn(buf); s_ota->fn(buf);
mg_free(s_ota); mg_free(s_ota);
s_ota = NULL; s_ota = NULL;
} else if (strcmp(s_ota->version, s_ota->my_version) == 0) { } else if (strcmp(s_ota->version, mg_fw_version + 11) == 0) {
s_autocommit_ok = true;
s_ota->fn("Same version"); s_ota->fn("Same version");
mg_free(s_ota); mg_free(s_ota);
s_ota = NULL; s_ota = NULL;
@@ -138,8 +137,8 @@ static void s_firmware_fn(struct mg_connection *c, int ev, void *ev_data) {
} }
} }
void mg_ota_url_check(struct mg_mgr *mgr, const char *my_version, void mg_ota_url_check(struct mg_mgr *mgr, const char *json_url,
const char *json_url, void (*fn)(const char *status)) { void (*fn)(const char *error_message)) {
if (fn == NULL) fn = status_fn; if (fn == NULL) fn = status_fn;
if (s_ota != NULL) { if (s_ota != NULL) {
fn("OTA already in progress"); fn("OTA already in progress");
@@ -148,7 +147,6 @@ void mg_ota_url_check(struct mg_mgr *mgr, const char *my_version,
fn("Out of memory"); fn("Out of memory");
} else { } else {
struct mg_connection *c; struct mg_connection *c;
mg_snprintf(s_ota->my_version, sizeof(s_ota->my_version), "%s", my_version);
mg_snprintf(s_ota->json_url, sizeof(s_ota->json_url), "%s", json_url); mg_snprintf(s_ota->json_url, sizeof(s_ota->json_url), "%s", json_url);
MG_DEBUG(("Connecting to %s", json_url)); MG_DEBUG(("Connecting to %s", json_url));
c = mg_http_connect(mgr, s_ota->json_url, s_version_fn, NULL); c = mg_http_connect(mgr, s_ota->json_url, s_version_fn, NULL);
@@ -165,9 +163,23 @@ void mg_ota_url_check(struct mg_mgr *mgr, const char *my_version,
void mg_ota_poll(struct mg_mgr *mgr) { void mg_ota_poll(struct mg_mgr *mgr) {
static uint64_t t = 5000; // Fire first time 5 sec after boot static uint64_t t = 5000; // Fire first time 5 sec after boot
static uint64_t feed_timer; // Advances by 500ms per tick; tracks elapsed time
if (MG_OTA_STATE_GET() == MG_OTA_TESTING &&
mg_timer_expired(&feed_timer, 500, mg_millis())) {
if (feed_timer < (uint64_t) MG_OTA_ROLLBACK_TIMEOUT_SECONDS * 1000) {
MG_OTA_ROLLBACK_TIMER_FEED(); // Feed watchdog every 500ms
} else if (s_autocommit_ok) {
MG_INFO(("Auto-committing firmware"));
MG_OTA_STATE_SET(MG_OTA_CONFIRMED); // Stop feeding → IWDG resets cleanly
} else {
MG_INFO(("No commit confirmation, rolling back"));
// Stop feeding → IWDG fires → resets into MG_OTA_FAILED → rollback
}
}
if (MG_OTA_URL != NULL && if (MG_OTA_URL != NULL &&
mg_timer_expired(&t, MG_OTA_PULL_INTERVAL_SECONDS * 1000, mg_millis())) { mg_timer_expired(&t, MG_OTA_PULL_INTERVAL_SECONDS * 1000, mg_millis())) {
mg_ota_url_check(mgr, MG_OTA_FIRMWARE_VERSION, MG_OTA_URL, mg_ota_url_check(mgr, MG_OTA_URL, MG_OTA_STATUS_FN);
MG_OTA_STATUS_FN);
} }
} }
+51 -29
View File
@@ -82,20 +82,32 @@ bool mg_ota_end(void);
} while (0) } while (0)
#endif #endif
// MG_OTA_ROLLBACK_TIMER_START(seconds): arm a hardware watchdog as the rollback // Arm a hardware watchdog. mg_ota_poll() feeds it while in MG_OTA_TESTING state;
// deadline. If it fires before MG_OTA_STATE_SET(MG_OTA_CONFIRMED), the device // if the event loop hangs, it fires, resets the device, and MG_OTA_FAILED
// resets and MG_OTA_STATE_GET() == MG_OTA_FAILED triggers MG_OTA_ROLLBACK(). // triggers MG_OTA_ROLLBACK() on next boot. Timeout is arch-defined.
#ifndef MG_OTA_ROLLBACK_TIMER_START #ifndef MG_OTA_ROLLBACK_TIMER_START
#define MG_OTA_ROLLBACK_TIMER_START(seconds) (void) (seconds) #define MG_OTA_ROLLBACK_TIMER_START()
#endif
#ifndef MG_OTA_ROLLBACK_TIMER_FEED
#define MG_OTA_ROLLBACK_TIMER_FEED()
#endif #endif
// OTA state values for MG_OTA_STATE_GET / MG_OTA_STATE_SET. // OTA state values for MG_OTA_STATE_GET / MG_OTA_STATE_SET.
// MG_OTA_CONFIRMED: firmware is committed, normal operation. // MG_OTA_CONFIRMED: firmware is committed, normal operation.
// MG_OTA_TESTING: new firmware booted for the first time; IWDG is armed. // MG_OTA_TESTING: new firmware booted for the first time; IWDG is armed.
// If the device reboots while in this state, it transitions // If the device reboots while in this state, it transitions
// to MG_OTA_FAILED and rolls back on the next boot. // to MG_OTA_FAILED and rolls back on the next boot.
// MG_OTA_FAILED: previous boot did not commit in time; rollback on next boot. // MG_OTA_FAILED: previous boot did not commit in time; rollback on next boot.
enum { MG_OTA_CONFIRMED = 0, MG_OTA_TESTING = 1, MG_OTA_FAILED = 2 }; // MG_OTA_ROLLEDBACK: device rolled back to previous firmware. Sent to the OTA
// server as boot=3 on every poll until a new OTA succeeds,
// which overwrites this state with MG_OTA_TESTING.
enum {
MG_OTA_CONFIRMED = 0,
MG_OTA_TESTING = 1,
MG_OTA_FAILED = 2,
MG_OTA_ROLLEDBACK = 3
};
// Persistent OTA state storage. Override in mongoose_config.h to persist state across resets. // Persistent OTA state storage. Override in mongoose_config.h to persist state across resets.
#ifndef MG_OTA_STATE_GET #ifndef MG_OTA_STATE_GET
@@ -105,31 +117,47 @@ enum { MG_OTA_CONFIRMED = 0, MG_OTA_TESTING = 1, MG_OTA_FAILED = 2 };
#define MG_OTA_STATE_SET(val) (void) (val) #define MG_OTA_STATE_SET(val) (void) (val)
#endif #endif
// Call once at boot. Arms the rollback watchdog for (seconds) seconds when // Evaluation window for new firmware in seconds. mg_ota_poll() feeds the
// the new firmware is in TESTING state. Rolls back immediately if FAILED. // rollback watchdog while in MG_OTA_TESTING state and within this window;
// when it expires, auto-commits if the OTA server confirmed the same version,
// otherwise stops feeding and the watchdog triggers a rollback.
#ifndef MG_OTA_ROLLBACK_TIMEOUT_SECONDS
#define MG_OTA_ROLLBACK_TIMEOUT_SECONDS 120
#endif
// Call once at boot, after clock and backup-domain init (e.g. after HAL_Init()
// + SystemClock_Config() on CubeMX projects, or after hal_clock_init() on
// Mongoose tutorials). The backup domain must be write-accessible so the state
// transition TESTING → FAILED persists across resets.
// Arms the rollback watchdog if new firmware is in TESTING state, rolls back
// immediately if FAILED. mg_ota_poll() feeds the watchdog and auto-commits
// when the server confirms the same version within the window.
// boot (TESTING) → set FAILED → arm IWDG → run firmware // boot (TESTING) → set FAILED → arm IWDG → run firmware
// ├── commit called → set CONFIRMED → NVIC_SystemReset() → clean boot // ├── server ok + window expires → set CONFIRMED → IWDG resets cleanly
// └── IWDG fires → hard reset → boot → state is still FAILED → rollback // └── no server ok + window expires → stop feeding → IWDG fires → FAILED → rollback
#define MG_OTA_BOOT_CHECK(seconds) \ #define MG_OTA_BOOT_CHECK() \
do { \ do { \
if (MG_OTA_STATE_GET() == MG_OTA_FAILED) { \ if (MG_OTA_STATE_GET() == MG_OTA_FAILED) { \
MG_OTA_STATE_SET(MG_OTA_CONFIRMED); \ MG_OTA_STATE_SET(MG_OTA_ROLLEDBACK); \
MG_INFO(("Commit deadline expired, rolling back")); \ MG_INFO(("Commit deadline expired, rolling back")); \
MG_OTA_ROLLBACK(); \ MG_OTA_ROLLBACK(); \
} else if (MG_OTA_STATE_GET() == MG_OTA_TESTING) { \ } else if (MG_OTA_STATE_GET() == MG_OTA_TESTING) { \
MG_OTA_STATE_SET(MG_OTA_FAILED); \ MG_OTA_STATE_SET(MG_OTA_FAILED); \
MG_INFO(("New firmware: commit within %u sec or rolls back", \ MG_INFO(("New firmware: commit within %u sec or rolls back", \
(unsigned) (seconds))); \ (unsigned) MG_OTA_ROLLBACK_TIMEOUT_SECONDS)); \
MG_OTA_ROLLBACK_TIMER_START(seconds); \ MG_OTA_ROLLBACK_TIMER_START(); \
} \ } \
} while (0) } while (0)
// Pull-based OTA over HTTP. Fetches the given URL, which should return a JSON // Pull-based OTA over HTTP. Fetches metadata_url, which must return a JSON
// object like: { "version": "1.2.3", "url": "FIRMWARE_URL", "size": 324645 } // object like: { "version": "1.2.3", "url": "FIRMWARE_URL", "size": 324645 }
// If the current firmware version differs from the JSON version, downloads // If the server version differs from MG_OTA_FIRMWARE_VERSION, downloads
// FIRMWARE_URL and performs the OTA update. // FIRMWARE_URL and performs the OTA update. fn is called on every outcome:
void mg_ota_url_check(struct mg_mgr *mgr, const char *current_version, // NULL on successful flash, "Same version" when already up to date, or an
const char *metadata_url, void (*fn)(const char *status)); // error string on failure. Pass NULL to use the default handler which logs
// failures via MG_ERROR.
void mg_ota_url_check(struct mg_mgr *mgr, const char *metadata_url,
void (*fn)(const char *error_message));
// Firmware info URL for mg_ota_poll() which calls mg_ota_url_check(). // Firmware info URL for mg_ota_poll() which calls mg_ota_url_check().
// Example: "http://mongoose.ws/ota/u/0/ota.json". See http://mongoose.ws/ota/ // Example: "http://mongoose.ws/ota/u/0/ota.json". See http://mongoose.ws/ota/
@@ -153,12 +181,6 @@ void mg_ota_url_check(struct mg_mgr *mgr, const char *current_version,
#define MG_OTA_MAX_VERSION_LEN 64 #define MG_OTA_MAX_VERSION_LEN 64
#endif #endif
// Scannable "MG_VERSION:<MG_OTA_FIRMWARE_VERSION>" tag embedded in every
// firmware binary, for server-side extraction. mg_mgr_init() stashes its
// address in mgr->userdata, which is what keeps it from being stripped by
// -Wl,--gc-sections on builds that never poll for OTAs
extern const char mg_fw_version[];
// How often mg_ota_poll() checks for a firmware update, in seconds // How often mg_ota_poll() checks for a firmware update, in seconds
#ifndef MG_OTA_PULL_INTERVAL_SECONDS #ifndef MG_OTA_PULL_INTERVAL_SECONDS
#define MG_OTA_PULL_INTERVAL_SECONDS 60 #define MG_OTA_PULL_INTERVAL_SECONDS 60
+46 -24
View File
@@ -35,6 +35,10 @@ static size_t flash_size(void) {
return (MG_REG(MG_FLASH_SIZE_REG_LOCATION) & 0xFFFF) * 1024; return (MG_REG(MG_FLASH_SIZE_REG_LOCATION) & 0xFFFF) * 1024;
} }
MG_IRAM static size_t last_sector_size(void) {
return (STM_DEV_ID >= 0x449 ? 256 : 128) * 1024;
}
MG_IRAM static int is_dualbank(void) { MG_IRAM static int is_dualbank(void) {
// only F42x/F43x series (0x419) support dual bank // only F42x/F43x series (0x419) support dual bank
return STM_DEV_ID == 0x419; return STM_DEV_ID == 0x419;
@@ -152,12 +156,7 @@ MG_IRAM static bool mg_stm32f_erase(void *addr) {
return ok; return ok;
} }
MG_IRAM static bool mg_stm32f_swap(void) { MG_IRAM static void single_bank_swap(char *p1, char *p2, size_t s, size_t ss);
// STM32 F42x/F43x support dual bank, however, the memory mapping
// change will not be carried through a hard reset. Therefore, we will use
// the single bank approach for this family as well.
return true;
}
static bool s_flash_irq_disabled; static bool s_flash_irq_disabled;
@@ -191,19 +190,54 @@ MG_IRAM static bool mg_stm32f_write(void *addr, const void *buf, size_t len) {
return ok; return ok;
} }
// just overwrite instead of swap MG_IRAM static bool mg_stm32f_swap(void) {
MG_IRAM void single_bank_swap(char *p1, char *p2, size_t size) { // STM32 F42x/F43x support dual bank, however, the memory mapping
// no stdlib calls here // change will not be carried through a hard reset. Therefore, we use
mg_stm32f_write(p1, p2, size); // the single-bank scratch-sector swap for this family as well.
*(volatile unsigned long *) 0xe000ed0c = 0x5fa0004; size_t ss = last_sector_size();
char *p1 = (char *) s_mg_flash_stm32f.start;
char *p2;
size_t s;
s_mg_flash_stm32f.size = flash_size();
p2 = p1 + s_mg_flash_stm32f.size / 2;
s = s_mg_flash_stm32f.size / 2 - ss;
MG_INFO(("Swapping partitions, %u bytes", s));
MG_INFO(("Do NOT power off..."));
MG_OTA_ROLLBACK_TIMER_FEED();
mg_log_level = MG_LL_NONE;
s_flash_irq_disabled = true;
single_bank_swap(p1, p2, s, ss);
return true; // unreachable
}
// True exchange between first half [p1..p1+s) and second half [p2..p2+s),
// using the last sector of the second half as scratch. Runs from RAM and resets.
MG_IRAM static void single_bank_swap(char *p1, char *p2, size_t s, size_t ss) {
char *scratch = p2 + s;
for (size_t ofs = 0; ofs < s; ofs += ss) {
MG_OTA_ROLLBACK_TIMER_FEED();
mg_stm32f_write(scratch, p1 + ofs, ss);
MG_OTA_ROLLBACK_TIMER_FEED();
mg_stm32f_write(p1 + ofs, p2 + ofs, ss);
MG_OTA_ROLLBACK_TIMER_FEED();
mg_stm32f_write(p2 + ofs, scratch, ss);
}
*(volatile uint32_t *) 0xe000ed0cU = 0x5fa0004U; // NVIC_SystemReset()
} }
bool mg_ota_begin(size_t new_firmware_size) { bool mg_ota_begin(size_t new_firmware_size) {
size_t max;
s_mg_flash_stm32f.size = flash_size(); s_mg_flash_stm32f.size = flash_size();
#ifdef __ZEPHYR__ #ifdef __ZEPHYR__
*((uint32_t *)0xE000ED94) = 0; *((uint32_t *)0xE000ED94) = 0;
MG_DEBUG(("Jailbreak %s", *((uint32_t *)0xE000ED94) == 0 ? "successful" : "failed")); MG_DEBUG(("Jailbreak %s", *((uint32_t *)0xE000ED94) == 0 ? "successful" : "failed"));
#endif #endif
max = s_mg_flash_stm32f.size / 2 - last_sector_size();
if (new_firmware_size > max) {
MG_ERROR(("Firmware %lu too big for single-bank OTA, max %lu",
new_firmware_size, max));
return false;
}
return mg_ota_flash_begin(new_firmware_size, &s_mg_flash_stm32f); return mg_ota_flash_begin(new_firmware_size, &s_mg_flash_stm32f);
} }
@@ -212,19 +246,7 @@ bool mg_ota_write(const void *buf, size_t len) {
} }
bool mg_ota_end(void) { bool mg_ota_end(void) {
if (mg_ota_flash_end(&s_mg_flash_stm32f)) { mg_ota_flash_end(&s_mg_flash_stm32f);
// Swap partitions. Pray power does not go away
MG_INFO(("Swapping partitions, size %u (%u sectors)",
s_mg_flash_stm32f.size, STM_DEV_ID == 0x449 ? 8 : 12));
MG_INFO(("Do NOT power off..."));
mg_log_level = MG_LL_NONE;
s_flash_irq_disabled = true;
char *p1 = (char *) s_mg_flash_stm32f.start;
char *p2 = p1 + s_mg_flash_stm32f.size / 2;
size_t size = s_mg_flash_stm32f.size / 2;
// Runs in RAM, will reset when finished
single_bank_swap(p1, p2, size);
}
return false; return false;
} }
struct mg_flash *mg_flash = &s_mg_flash_stm32f; struct mg_flash *mg_flash = &s_mg_flash_stm32f;
+20 -7
View File
@@ -9,7 +9,7 @@ static bool mg_stm32h5_swap(void);
static struct mg_flash s_mg_flash_stm32h5 = { static struct mg_flash s_mg_flash_stm32h5 = {
(void *) 0x08000000, // Start (void *) 0x08000000, // Start
2 * 1024 * 1024, // Size, 2Mb 0, // Size, FLASHSIZE_BASE
8 * 1024, // Sector size, 8k 8 * 1024, // Sector size, 8k
16, // Align, 128 bit 16, // Align, 128 bit
mg_stm32h5_write, mg_stm32h5_write,
@@ -25,6 +25,18 @@ static struct mg_flash s_mg_flash_stm32h5 = {
#define FLASH_NSCCR (MG_FLASH_BASE + 0x30) #define FLASH_NSCCR (MG_FLASH_BASE + 0x30)
#define FLASH_OPTSR_CUR (MG_FLASH_BASE + 0x50) #define FLASH_OPTSR_CUR (MG_FLASH_BASE + 0x50)
#define FLASH_OPTSR_PRG (MG_FLASH_BASE + 0x54) #define FLASH_OPTSR_PRG (MG_FLASH_BASE + 0x54)
#ifndef FLASHSIZE_BASE
#define FLASHSIZE_BASE 0x08FFF80CUL
#endif
static size_t flash_size(void) {
uint32_t kb = MG_REG(FLASHSIZE_BASE) & 0xFFFF;
return (kb == 0 || kb == 0xFFFF) ? 2 * 1024 * 1024 : kb * 1024;
}
static uint32_t sectors_per_bank(void) {
return (uint32_t) (s_mg_flash_stm32h5.size / 2 / s_mg_flash_stm32h5.secsz);
}
static void flash_unlock(void) { static void flash_unlock(void) {
static bool unlocked = false; static bool unlocked = false;
@@ -71,15 +83,16 @@ static bool mg_stm32h5_erase(void *location) {
} else { } else {
uintptr_t diff = (char *) location - (char *) s_mg_flash_stm32h5.start; uintptr_t diff = (char *) location - (char *) s_mg_flash_stm32h5.start;
uint32_t sector = diff / s_mg_flash_stm32h5.secsz; uint32_t sector = diff / s_mg_flash_stm32h5.secsz;
uint32_t bank_sectors = sectors_per_bank();
uint32_t saved_cr = MG_REG(FLASH_NSCR); // Save CR value uint32_t saved_cr = MG_REG(FLASH_NSCR); // Save CR value
flash_unlock(); flash_unlock();
flash_clear_err(); flash_clear_err();
MG_REG(FLASH_NSCR) = 0; MG_REG(FLASH_NSCR) = 0;
if ((sector < 128 && flash_bank_is_swapped()) || if ((sector < bank_sectors && flash_bank_is_swapped()) ||
(sector > 127 && !flash_bank_is_swapped())) { (sector >= bank_sectors && !flash_bank_is_swapped())) {
MG_REG(FLASH_NSCR) |= MG_BIT(31); // Set FLASH_CR_BKSEL MG_REG(FLASH_NSCR) |= MG_BIT(31); // Set FLASH_CR_BKSEL
} }
if (sector > 127) sector -= 128; if (sector >= bank_sectors) sector -= bank_sectors;
MG_REG(FLASH_NSCR) |= MG_BIT(2) | (sector << 6); // Erase | sector_num MG_REG(FLASH_NSCR) |= MG_BIT(2) | (sector << 6); // Erase | sector_num
MG_REG(FLASH_NSCR) |= MG_BIT(5); // Start erasing MG_REG(FLASH_NSCR) |= MG_BIT(5); // Start erasing
flash_wait(); flash_wait();
@@ -133,6 +146,7 @@ static bool mg_stm32h5_write(void *addr, const void *buf, size_t len) {
} }
bool mg_ota_begin(size_t new_firmware_size) { bool mg_ota_begin(size_t new_firmware_size) {
s_mg_flash_stm32h5.size = flash_size();
#ifdef __ZEPHYR__ #ifdef __ZEPHYR__
*((uint32_t *)0xE000ED94) = 0; *((uint32_t *)0xE000ED94) = 0;
MG_DEBUG(("Jailbreak %s", *((uint32_t *)0xE000ED94) == 0 ? "successful" : "failed")); MG_DEBUG(("Jailbreak %s", *((uint32_t *)0xE000ED94) == 0 ? "successful" : "failed"));
@@ -145,9 +159,8 @@ bool mg_ota_write(const void *buf, size_t len) {
} }
bool mg_ota_end(void) { bool mg_ota_end(void) {
if (!mg_ota_flash_end(&s_mg_flash_stm32h5)) return false; mg_ota_flash_end(&s_mg_flash_stm32h5);
*(volatile uint32_t *) 0xe000ed0c = 0x5fa0004U; // NVIC_SystemReset() return false;
return true;
} }
struct mg_flash *mg_flash = &s_mg_flash_stm32h5; struct mg_flash *mg_flash = &s_mg_flash_stm32h5;
#endif #endif
+44 -24
View File
@@ -37,6 +37,11 @@ static struct mg_flash s_mg_flash_stm32h7 = {
#define IS_DUALCORE() (MG_OTA == MG_OTA_STM32H7_DUAL_CORE) #define IS_DUALCORE() (MG_OTA == MG_OTA_STM32H7_DUAL_CORE)
MG_IRAM static size_t flash_size(void) {
size_t size = MG_REG(FLASH_SIZE_REG) * 1024;
return IS_DUALCORE() ? size / 2 : size;
}
MG_IRAM static bool is_dualbank(void) { MG_IRAM static bool is_dualbank(void) {
if (IS_DUALCORE()) { if (IS_DUALCORE()) {
// H745/H755 and H747/H757 are running on dual core. // H745/H755 and H747/H757 are running on dual core.
@@ -122,8 +127,27 @@ MG_IRAM static bool mg_stm32h7_erase(void *addr) {
return ok; return ok;
} }
MG_IRAM static void single_bank_swap(char *p1, char *p2, size_t s, size_t ss);
static bool s_flash_irq_disabled;
MG_IRAM static bool mg_stm32h7_swap(void) { MG_IRAM static bool mg_stm32h7_swap(void) {
if (!is_dualbank()) return true; s_mg_flash_stm32h7.size = flash_size();
if (!is_dualbank()) {
// True sector-by-sector swap using the last sector of the second half as
// scratch. Resets inside single_bank_swap, never returns.
size_t ss = s_mg_flash_stm32h7.secsz;
char *p1 = (char *) s_mg_flash_stm32h7.start;
char *p2 = p1 + s_mg_flash_stm32h7.size / 2;
size_t s = s_mg_flash_stm32h7.size / 2 - ss;
MG_INFO(("Swapping partitions, %u bytes (%u sectors)", s, s / ss));
MG_INFO(("Do NOT power off..."));
MG_OTA_ROLLBACK_TIMER_FEED();
mg_log_level = MG_LL_NONE;
s_flash_irq_disabled = true;
single_bank_swap(p1, p2, s, ss);
return true; // unreachable
}
uint32_t bank = FLASH_BASE1; uint32_t bank = FLASH_BASE1;
uint32_t desired = flash_bank_is_swapped(bank) ? 0 : MG_BIT(31); uint32_t desired = flash_bank_is_swapped(bank) ? 0 : MG_BIT(31);
flash_unlock(); flash_unlock();
@@ -134,8 +158,6 @@ MG_IRAM static bool mg_stm32h7_swap(void) {
return true; return true;
} }
static bool s_flash_irq_disabled;
MG_IRAM static bool mg_stm32h7_write(void *addr, const void *buf, size_t len) { MG_IRAM static bool mg_stm32h7_write(void *addr, const void *buf, size_t len) {
if ((len % s_mg_flash_stm32h7.align) != 0) { if ((len % s_mg_flash_stm32h7.align) != 0) {
MG_ERROR(("%lu is not aligned to %lu", len, s_mg_flash_stm32h7.align)); MG_ERROR(("%lu is not aligned to %lu", len, s_mg_flash_stm32h7.align));
@@ -168,25 +190,36 @@ MG_IRAM static bool mg_stm32h7_write(void *addr, const void *buf, size_t len) {
return ok; return ok;
} }
// just overwrite instead of swap // True sector-by-sector exchange between first half [p1..p1+s) and second half
// [p2..p2+s), using the sector at p2+s as scratch. Runs from RAM. Resets on
// completion. Symmetric: calling twice returns flash to original state.
MG_IRAM static void single_bank_swap(char *p1, char *p2, size_t s, size_t ss) { MG_IRAM static void single_bank_swap(char *p1, char *p2, size_t s, size_t ss) {
// no stdlib calls here char *scratch = p2 + s; // Last sector of second half, reserved as scratch
for (size_t ofs = 0; ofs < s; ofs += ss) { for (size_t ofs = 0; ofs < s; ofs += ss) {
mg_stm32h7_write(p1 + ofs, p2 + ofs, ss); MG_OTA_ROLLBACK_TIMER_FEED();
mg_stm32h7_write(scratch, p1 + ofs, ss); // Save p1[i] to scratch
MG_OTA_ROLLBACK_TIMER_FEED();
mg_stm32h7_write(p1 + ofs, p2 + ofs, ss); // Copy p2[i] to p1[i]
MG_OTA_ROLLBACK_TIMER_FEED();
mg_stm32h7_write(p2 + ofs, scratch, ss); // Copy scratch to p2[i]
} }
*(volatile uint32_t *) 0xe000ed0cU = 0x5fa0004U; // NVIC_SystemReset() *(volatile uint32_t *) 0xe000ed0cU = 0x5fa0004U; // NVIC_SystemReset()
} }
bool mg_ota_begin(size_t new_firmware_size) { bool mg_ota_begin(size_t new_firmware_size) {
s_mg_flash_stm32h7.size = MG_REG(FLASH_SIZE_REG) * 1024; s_mg_flash_stm32h7.size = flash_size();
if (IS_DUALCORE()) {
// Using only the 1st bank (mapped to CM7)
s_mg_flash_stm32h7.size /= 2;
}
#ifdef __ZEPHYR__ #ifdef __ZEPHYR__
*((uint32_t *) 0xE000ED94) = 0; *((uint32_t *) 0xE000ED94) = 0;
MG_DEBUG(("Jailbreak %s", *((uint32_t *) 0xE000ED94) == 0 ? "ok" : "failed")); MG_DEBUG(("Jailbreak %s", *((uint32_t *) 0xE000ED94) == 0 ? "ok" : "failed"));
#endif #endif
if (!is_dualbank()) {
// Last sector of the second half is reserved as swap scratch; enforce limit.
size_t max = s_mg_flash_stm32h7.size / 2 - s_mg_flash_stm32h7.secsz;
if (new_firmware_size > max) {
MG_ERROR(("Firmware %lu too big for single-bank OTA, max %lu", new_firmware_size, max));
return false;
}
}
return mg_ota_flash_begin(new_firmware_size, &s_mg_flash_stm32h7); return mg_ota_flash_begin(new_firmware_size, &s_mg_flash_stm32h7);
} }
@@ -199,19 +232,6 @@ bool mg_ota_end(void) {
if (is_dualbank()) { if (is_dualbank()) {
// Bank swap is deferred until reset, been executing in flash, reset // Bank swap is deferred until reset, been executing in flash, reset
*(volatile uint32_t *) 0xe000ed0cU = 0x5fa0004U; // NVIC_SystemReset() *(volatile uint32_t *) 0xe000ed0cU = 0x5fa0004U; // NVIC_SystemReset()
} else {
// Swap partitions. Pray power does not go away
MG_INFO(("Swapping partitions, size %u (%u sectors)",
s_mg_flash_stm32h7.size,
s_mg_flash_stm32h7.size / s_mg_flash_stm32h7.secsz));
MG_INFO(("Do NOT power off..."));
mg_log_level = MG_LL_NONE;
s_flash_irq_disabled = true;
// Runs in RAM, will reset when finished
single_bank_swap(
(char *) s_mg_flash_stm32h7.start,
(char *) s_mg_flash_stm32h7.start + s_mg_flash_stm32h7.size / 2,
s_mg_flash_stm32h7.size / 2, s_mg_flash_stm32h7.secsz);
} }
} }
return false; return false;
+3 -5
View File
@@ -73,18 +73,16 @@ int main(void) {
hal_clock_init(); hal_clock_init();
hal_uart_init(UART_DEBUG, UART_DEBUG_TX_PIN, UART_DEBUG_RX_PIN, 115200); hal_uart_init(UART_DEBUG, UART_DEBUG_TX_PIN, UART_DEBUG_RX_PIN, 115200);
mg_log_set_fn(log_fn, UART_DEBUG); mg_log_set_fn(log_fn, UART_DEBUG);
MG_OTA_BOOT_CHECK(); // Must be called after clock init
hal_rng_init(); hal_rng_init();
hal_gpio_output(LED_1); hal_gpio_output(LED_1);
hal_gpio_output(LED_2); hal_gpio_output(LED_2);
hal_gpio_output(LED_3); hal_gpio_output(LED_3);
hal_ethernet_init(); hal_ethernet_init();
MG_INFO(("Initialised. CPU clock: %lu MHz", SystemCoreClock / 1000000)); MG_INFO(("Initialised. CPU clock: %lu MHz", SystemCoreClock / 1000000));
// Max delay 131 seconds
MG_OTA_BOOT_CHECK(30);
struct mg_mgr mgr; struct mg_mgr mgr;
mg_mgr_init(&mgr); mg_mgr_init(&mgr);
mg_http_listen(&mgr, "http://0.0.0.0", http_ev_handler, NULL); mg_http_listen(&mgr, "http://0.0.0.0", http_ev_handler, NULL);
@@ -24,18 +24,20 @@
} }
// OTA rollback timer via IWDG. PR=8 → /1024 on H563 (RM0481 §52.4.2). // OTA rollback timer via IWDG. PR=8 → /1024 on H563 (RM0481 §52.4.2).
// Max timeout: 4095 * 1024 / 32000 = 131 s. PVU/RVU must clear before // Fixed 10 s hang-detection window: RLR = 10 * 32000 / 1024 = 312.
// writing RLR/starting, otherwise the write is ignored (RM0481 §52.4.3). // PVU/RVU must clear before writing RLR/starting (RM0481 §52.4.3).
#define MG_OTA_ROLLBACK_TIMER_START(seconds) \ // mg_ota_poll() feeds the watchdog every 500ms while in MG_OTA_TESTING state.
#define MG_OTA_ROLLBACK_TIMER_START() \
do { \ do { \
IWDG->KR = 0x5555U; /* unlock */ \ IWDG->KR = 0x5555U; /* unlock */ \
IWDG->PR = 8U; /* /1024 */ \ IWDG->PR = 8U; /* /1024 */ \
while (IWDG->SR & IWDG_SR_PVU) (void) 0; /* wait PVU */ \ while (IWDG->SR & IWDG_SR_PVU) (void) 0; /* wait PVU */ \
IWDG->RLR = (uint32_t) ((seconds) * 32000 / 1024); \ IWDG->RLR = 312U; /* 10 s */ \
while (IWDG->SR & IWDG_SR_RVU) (void) 0; /* wait RVU */ \ while (IWDG->SR & IWDG_SR_RVU) (void) 0; /* wait RVU */ \
IWDG->KR = 0xCCCCU; /* start */ \ IWDG->KR = 0xCCCCU; /* start */ \
IWDG->KR = 0xAAAAU; /* reload RLR */ \ IWDG->KR = 0xAAAAU; /* reload RLR */ \
} while (0) } while (0)
#define MG_OTA_ROLLBACK_TIMER_FEED() (IWDG->KR = 0xAAAAU)
// OTA state in TAMP backup register 0 (survives all resets, clears on POR). // OTA state in TAMP backup register 0 (survives all resets, clears on POR).
// Requires hal_backup_domain_init() (called from hal_clock_init) to have run. // Requires hal_backup_domain_init() (called from hal_clock_init) to have run.
+1 -2
View File
@@ -80,8 +80,7 @@ int main(void) {
MG_INFO(("Initialised. CPU clock: %lu MHz", SystemCoreClock / 1000000)); MG_INFO(("Initialised. CPU clock: %lu MHz", SystemCoreClock / 1000000));
// Max delay 65 seconds MG_OTA_BOOT_CHECK(); // Must be called after clock init
MG_OTA_BOOT_CHECK(30);
struct mg_mgr mgr; struct mg_mgr mgr;
mg_mgr_init(&mgr); mg_mgr_init(&mgr);
@@ -24,18 +24,20 @@
// #define MG_TCPIP_MASK MG_IPV4(255, 255, 255, 0) // Netmask // #define MG_TCPIP_MASK MG_IPV4(255, 255, 255, 0) // Netmask
// OTA rollback timer via IWDG1. PR=7 → /512 on H723 (RM0468 §54.4.2). // OTA rollback timer via IWDG1. PR=7 → /512 on H723 (RM0468 §54.4.2).
// Max timeout: 4095 * 512 / 32000 = 65 s. // Fixed 10 s hang-detection window: RLR = 10 * 32000 / 512 = 625.
// RM0468 §54.4 sequence: 0xCCCC first (starts LSI), then 0x5555 to unlock, // RM0468 §54.4 sequence: 0xCCCC first (starts LSI), then 0x5555 to unlock,
// write PR+RLR, wait SR=0 (PVU|RVU), then 0xAAAA to reload the counter. // write PR+RLR, wait SR=0 (PVU|RVU), then 0xAAAA to reload the counter.
#define MG_OTA_ROLLBACK_TIMER_START(seconds) \ // mg_ota_poll() feeds the watchdog every 500ms while in MG_OTA_TESTING state.
#define MG_OTA_ROLLBACK_TIMER_START() \
do { \ do { \
IWDG1->KR = 0xCCCCU; /* start/LSI */ \ IWDG1->KR = 0xCCCCU; /* start/LSI */ \
IWDG1->KR = 0x5555U; /* unlock */ \ IWDG1->KR = 0x5555U; /* unlock */ \
IWDG1->PR = 7U; /* /512 */ \ IWDG1->PR = 7U; /* /512 */ \
IWDG1->RLR = (uint32_t) ((seconds) * 32000 / 512); \ IWDG1->RLR = 625U; /* 10 s */ \
while (IWDG1->SR & (IWDG_SR_PVU | IWDG_SR_RVU)) (void) 0; /* wait SR=0 */ \ while (IWDG1->SR & (IWDG_SR_PVU | IWDG_SR_RVU)) (void) 0; /* wait SR=0 */ \
IWDG1->KR = 0xAAAAU; /* reload */ \ IWDG1->KR = 0xAAAAU; /* reload */ \
} while (0) } while (0)
#define MG_OTA_ROLLBACK_TIMER_FEED() (IWDG1->KR = 0xAAAAU)
// OTA state in RTC backup register 0 (survives all resets, clears on POR). // OTA state in RTC backup register 0 (survives all resets, clears on POR).
// Requires hal_backup_domain_init() (called from hal_clock_init) to have run. // Requires hal_backup_domain_init() (called from hal_clock_init) to have run.