diff --git a/mongoose.c b/mongoose.c index ad6d5c54..a5c9f821 100644 --- a/mongoose.c +++ b/mongoose.c @@ -7533,11 +7533,6 @@ void mg_mgr_free(struct mg_mgr *mgr) { void mg_mgr_init(struct mg_mgr *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 ((mgr->epoll_fd = epoll_create1(EPOLL_CLOEXEC)) < 0) MG_ERROR(("epoll_create1 errno %d", errno)); @@ -9908,20 +9903,20 @@ void mg_tcpip_mapip(struct mg_connection *c, struct mg_addr *ip) { #endif // Scannable version tag embedded in every firmware binary, for server-side -// version extraction. Non-static so mg_mgr_init() can stash its address in -// mgr->userdata: that store is what actually keeps -Wl,--gc-sections from -// stripping it on builds that never poll for OTAs (an attribute alone does -// not - the linker drops unreferenced sections regardless of "used"/"retain") -const char mg_fw_version[] = "MG_VERSION:" MG_OTA_FIRMWARE_VERSION; +// version extraction. __attribute__((used)) prevents --gc-sections from +// stripping it. The version string starts after the 11-char "MG_VERSION:" prefix. +static const char mg_fw_version[] __attribute__((used)) = + "MG_VERSION:" MG_OTA_FIRMWARE_VERSION; + +static bool s_autocommit_ok; // True after OTA server confirms "same version" static struct mg_ota_state { - char my_version[MG_OTA_MAX_VERSION_LEN]; char json_url[MG_OTA_MAX_URL_LEN]; char version[MG_OTA_MAX_VERSION_LEN]; char url[MG_OTA_MAX_URL_LEN]; size_t size; uint8_t sha256[32]; - void (*fn)(const char *status); + void (*fn)(const char *error_message); } *s_ota; 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)); id[sizeof(id) - 1] = '\0'; 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" "Connection: close\r\n\r\n", - uri, sep, MG_ARCH, s_ota->my_version, id, - MG_OTA_PULL_INTERVAL_SECONDS, host.len, host.buf); + uri, sep, MG_ARCH, mg_fw_version + 11, id, + MG_OTA_PULL_INTERVAL_SECONDS, MG_OTA_STATE_GET(), host.len, host.buf); } else if (ev == MG_EV_HTTP_MSG) { struct mg_http_message *hm = (struct mg_http_message *) ev_data; 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 || !mg_json_unescape(hm->body, "$.version", 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); mg_free(s_ota); 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"); mg_free(s_ota); 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, - const char *json_url, void (*fn)(const char *status)) { +void mg_ota_url_check(struct mg_mgr *mgr, const char *json_url, + void (*fn)(const char *error_message)) { if (fn == NULL) fn = status_fn; if (s_ota != NULL) { 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"); } else { 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_DEBUG(("Connecting to %s", json_url)); 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) { 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 && 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_STATUS_FN); + mg_ota_url_check(mgr, MG_OTA_URL, MG_OTA_STATUS_FN); } } @@ -11654,6 +11663,10 @@ static size_t flash_size(void) { 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) { // only F42x/F43x series (0x419) support dual bank return STM_DEV_ID == 0x419; @@ -11771,12 +11784,7 @@ MG_IRAM static bool mg_stm32f_erase(void *addr) { return ok; } -MG_IRAM static bool mg_stm32f_swap(void) { - // 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; -} +MG_IRAM static void single_bank_swap(char *p1, char *p2, size_t s, size_t ss); 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; } -// just overwrite instead of swap -MG_IRAM void single_bank_swap(char *p1, char *p2, size_t size) { - // no stdlib calls here - mg_stm32f_write(p1, p2, size); - *(volatile unsigned long *) 0xe000ed0c = 0x5fa0004; +MG_IRAM static bool mg_stm32f_swap(void) { + // STM32 F42x/F43x support dual bank, however, the memory mapping + // change will not be carried through a hard reset. Therefore, we use + // the single-bank scratch-sector swap for this family as well. + 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) { + size_t max; s_mg_flash_stm32f.size = flash_size(); #ifdef __ZEPHYR__ *((uint32_t *)0xE000ED94) = 0; MG_DEBUG(("Jailbreak %s", *((uint32_t *)0xE000ED94) == 0 ? "successful" : "failed")); #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); } @@ -11831,19 +11874,7 @@ bool mg_ota_write(const void *buf, size_t len) { } bool mg_ota_end(void) { - if (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); - } + mg_ota_flash_end(&s_mg_flash_stm32f); return false; } 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 = { (void *) 0x08000000, // Start - 2 * 1024 * 1024, // Size, 2Mb + 0, // Size, FLASHSIZE_BASE 8 * 1024, // Sector size, 8k 16, // Align, 128 bit mg_stm32h5_write, @@ -11879,6 +11910,18 @@ static struct mg_flash s_mg_flash_stm32h5 = { #define FLASH_NSCCR (MG_FLASH_BASE + 0x30) #define FLASH_OPTSR_CUR (MG_FLASH_BASE + 0x50) #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 bool unlocked = false; @@ -11925,15 +11968,16 @@ static bool mg_stm32h5_erase(void *location) { } else { uintptr_t diff = (char *) location - (char *) s_mg_flash_stm32h5.start; 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 flash_unlock(); flash_clear_err(); MG_REG(FLASH_NSCR) = 0; - if ((sector < 128 && flash_bank_is_swapped()) || - (sector > 127 && !flash_bank_is_swapped())) { + if ((sector < bank_sectors && flash_bank_is_swapped()) || + (sector >= bank_sectors && !flash_bank_is_swapped())) { 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(5); // Start erasing 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) { + s_mg_flash_stm32h5.size = flash_size(); #ifdef __ZEPHYR__ *((uint32_t *)0xE000ED94) = 0; 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) { - if (!mg_ota_flash_end(&s_mg_flash_stm32h5)) return false; - *(volatile uint32_t *) 0xe000ed0c = 0x5fa0004U; // NVIC_SystemReset() - return true; + mg_ota_flash_end(&s_mg_flash_stm32h5); + return false; } struct mg_flash *mg_flash = &s_mg_flash_stm32h5; #endif @@ -12048,6 +12092,11 @@ static struct mg_flash s_mg_flash_stm32h7 = { #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) { if (IS_DUALCORE()) { // 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; } +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) { - 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 desired = flash_bank_is_swapped(bank) ? 0 : MG_BIT(31); flash_unlock(); @@ -12145,8 +12213,6 @@ MG_IRAM static bool mg_stm32h7_swap(void) { return true; } -static bool s_flash_irq_disabled; - MG_IRAM static bool mg_stm32h7_write(void *addr, const void *buf, size_t len) { if ((len % s_mg_flash_stm32h7.align) != 0) { 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; } -// 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) { - // 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) { - 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() } bool mg_ota_begin(size_t new_firmware_size) { - s_mg_flash_stm32h7.size = MG_REG(FLASH_SIZE_REG) * 1024; - if (IS_DUALCORE()) { - // Using only the 1st bank (mapped to CM7) - s_mg_flash_stm32h7.size /= 2; - } + s_mg_flash_stm32h7.size = flash_size(); #ifdef __ZEPHYR__ *((uint32_t *) 0xE000ED94) = 0; MG_DEBUG(("Jailbreak %s", *((uint32_t *) 0xE000ED94) == 0 ? "ok" : "failed")); #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); } @@ -12210,19 +12287,6 @@ bool mg_ota_end(void) { if (is_dualbank()) { // Bank swap is deferred until reset, been executing in flash, reset *(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; diff --git a/mongoose.h b/mongoose.h index 9d9410d6..c71cc53e 100644 --- a/mongoose.h +++ b/mongoose.h @@ -194,6 +194,61 @@ extern "C" { #define MG_OTA MG_OTA_STM32H7 #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 #ifndef MG_IRAM #define MG_IRAM __attribute__((section(".RamFunc"))) @@ -4090,20 +4145,32 @@ bool mg_ota_end(void); } while (0) #endif -// MG_OTA_ROLLBACK_TIMER_START(seconds): arm a hardware watchdog as the rollback -// deadline. If it fires before MG_OTA_STATE_SET(MG_OTA_CONFIRMED), the device -// resets and MG_OTA_STATE_GET() == MG_OTA_FAILED triggers MG_OTA_ROLLBACK(). +// Arm a hardware watchdog. mg_ota_poll() feeds it while in MG_OTA_TESTING state; +// if the event loop hangs, it fires, resets the device, and MG_OTA_FAILED +// triggers MG_OTA_ROLLBACK() on next boot. Timeout is arch-defined. #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 // OTA state values for MG_OTA_STATE_GET / MG_OTA_STATE_SET. -// MG_OTA_CONFIRMED: firmware is committed, normal operation. -// MG_OTA_TESTING: new firmware booted for the first time; IWDG is armed. -// If the device reboots while in this state, it transitions -// 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. -enum { MG_OTA_CONFIRMED = 0, MG_OTA_TESTING = 1, MG_OTA_FAILED = 2 }; +// MG_OTA_CONFIRMED: firmware is committed, normal operation. +// MG_OTA_TESTING: new firmware booted for the first time; IWDG is armed. +// If the device reboots while in this state, it transitions +// 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_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. #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) #endif -// Call once at boot. Arms the rollback watchdog for (seconds) seconds when -// the new firmware is in TESTING state. Rolls back immediately if FAILED. +// Evaluation window for new firmware in seconds. mg_ota_poll() feeds the +// 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 -// ├── commit called → set CONFIRMED → NVIC_SystemReset() → clean boot -// └── IWDG fires → hard reset → boot → state is still FAILED → rollback -#define MG_OTA_BOOT_CHECK(seconds) \ +// ├── server ok + window expires → set CONFIRMED → IWDG resets cleanly +// └── no server ok + window expires → stop feeding → IWDG fires → FAILED → rollback +#define MG_OTA_BOOT_CHECK() \ do { \ 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_OTA_ROLLBACK(); \ } else if (MG_OTA_STATE_GET() == MG_OTA_TESTING) { \ MG_OTA_STATE_SET(MG_OTA_FAILED); \ MG_INFO(("New firmware: commit within %u sec or rolls back", \ - (unsigned) (seconds))); \ - MG_OTA_ROLLBACK_TIMER_START(seconds); \ + (unsigned) MG_OTA_ROLLBACK_TIMEOUT_SECONDS)); \ + MG_OTA_ROLLBACK_TIMER_START(); \ } \ } 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 } -// If the current firmware version differs from the JSON version, downloads -// FIRMWARE_URL and performs the OTA update. -void mg_ota_url_check(struct mg_mgr *mgr, const char *current_version, - const char *metadata_url, void (*fn)(const char *status)); +// If the server version differs from MG_OTA_FIRMWARE_VERSION, downloads +// FIRMWARE_URL and performs the OTA update. fn is called on every outcome: +// NULL on successful flash, "Same version" when already up to date, or an +// 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(). // 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 #endif -// Scannable "MG_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 #ifndef MG_OTA_PULL_INTERVAL_SECONDS #define MG_OTA_PULL_INTERVAL_SECONDS 60 diff --git a/src/arch_cube.h b/src/arch_cube.h index 6d63275a..83a4e994 100644 --- a/src/arch_cube.h +++ b/src/arch_cube.h @@ -62,6 +62,61 @@ #define MG_OTA MG_OTA_STM32H7 #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 #ifndef MG_IRAM #define MG_IRAM __attribute__((section(".RamFunc"))) diff --git a/src/net.c b/src/net.c index c4d47a84..45e092a9 100644 --- a/src/net.c +++ b/src/net.c @@ -279,11 +279,6 @@ void mg_mgr_free(struct mg_mgr *mgr) { void mg_mgr_init(struct mg_mgr *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 ((mgr->epoll_fd = epoll_create1(EPOLL_CLOEXEC)) < 0) MG_ERROR(("epoll_create1 errno %d", errno)); diff --git a/src/ota.c b/src/ota.c index 0e6f102e..0de982d7 100644 --- a/src/ota.c +++ b/src/ota.c @@ -11,20 +11,18 @@ #endif // Scannable version tag embedded in every firmware binary, for server-side -// version extraction. Non-static so mg_mgr_init() can stash its address in -// mgr->userdata: that store is what actually keeps -Wl,--gc-sections from -// stripping it on builds that never poll for OTAs (an attribute alone does -// not - the linker drops unreferenced sections regardless of "used"/"retain") -const char mg_fw_version[] = "MG_VERSION:" MG_OTA_FIRMWARE_VERSION; +// version extraction. The version string starts after the 11-char "MG_VERSION:" prefix. +static 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 { - char my_version[MG_OTA_MAX_VERSION_LEN]; char json_url[MG_OTA_MAX_URL_LEN]; char version[MG_OTA_MAX_VERSION_LEN]; char url[MG_OTA_MAX_URL_LEN]; size_t size; uint8_t sha256[32]; - void (*fn)(const char *status); + void (*fn)(const char *error_message); } *s_ota; 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)); id[sizeof(id) - 1] = '\0'; 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" "Connection: close\r\n\r\n", - uri, sep, MG_ARCH, s_ota->my_version, id, - MG_OTA_PULL_INTERVAL_SECONDS, host.len, host.buf); + uri, sep, MG_ARCH, mg_fw_version + 11, id, + MG_OTA_PULL_INTERVAL_SECONDS, MG_OTA_STATE_GET(), host.len, host.buf); } else if (ev == MG_EV_HTTP_MSG) { struct mg_http_message *hm = (struct mg_http_message *) ev_data; 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 || !mg_json_unescape(hm->body, "$.version", 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); mg_free(s_ota); 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"); mg_free(s_ota); 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, - const char *json_url, void (*fn)(const char *status)) { +void mg_ota_url_check(struct mg_mgr *mgr, const char *json_url, + void (*fn)(const char *error_message)) { if (fn == NULL) fn = status_fn; if (s_ota != NULL) { 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"); } else { 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_DEBUG(("Connecting to %s", json_url)); 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) { 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 && 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_STATUS_FN); + mg_ota_url_check(mgr, MG_OTA_URL, MG_OTA_STATUS_FN); } } diff --git a/src/ota.h b/src/ota.h index ceac0038..b99f9308 100644 --- a/src/ota.h +++ b/src/ota.h @@ -82,20 +82,32 @@ bool mg_ota_end(void); } while (0) #endif -// MG_OTA_ROLLBACK_TIMER_START(seconds): arm a hardware watchdog as the rollback -// deadline. If it fires before MG_OTA_STATE_SET(MG_OTA_CONFIRMED), the device -// resets and MG_OTA_STATE_GET() == MG_OTA_FAILED triggers MG_OTA_ROLLBACK(). +// Arm a hardware watchdog. mg_ota_poll() feeds it while in MG_OTA_TESTING state; +// if the event loop hangs, it fires, resets the device, and MG_OTA_FAILED +// triggers MG_OTA_ROLLBACK() on next boot. Timeout is arch-defined. #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 // OTA state values for MG_OTA_STATE_GET / MG_OTA_STATE_SET. -// MG_OTA_CONFIRMED: firmware is committed, normal operation. -// MG_OTA_TESTING: new firmware booted for the first time; IWDG is armed. -// If the device reboots while in this state, it transitions -// 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. -enum { MG_OTA_CONFIRMED = 0, MG_OTA_TESTING = 1, MG_OTA_FAILED = 2 }; +// MG_OTA_CONFIRMED: firmware is committed, normal operation. +// MG_OTA_TESTING: new firmware booted for the first time; IWDG is armed. +// If the device reboots while in this state, it transitions +// 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_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. #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) #endif -// Call once at boot. Arms the rollback watchdog for (seconds) seconds when -// the new firmware is in TESTING state. Rolls back immediately if FAILED. +// Evaluation window for new firmware in seconds. mg_ota_poll() feeds the +// 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 -// ├── commit called → set CONFIRMED → NVIC_SystemReset() → clean boot -// └── IWDG fires → hard reset → boot → state is still FAILED → rollback -#define MG_OTA_BOOT_CHECK(seconds) \ +// ├── server ok + window expires → set CONFIRMED → IWDG resets cleanly +// └── no server ok + window expires → stop feeding → IWDG fires → FAILED → rollback +#define MG_OTA_BOOT_CHECK() \ do { \ 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_OTA_ROLLBACK(); \ } else if (MG_OTA_STATE_GET() == MG_OTA_TESTING) { \ MG_OTA_STATE_SET(MG_OTA_FAILED); \ MG_INFO(("New firmware: commit within %u sec or rolls back", \ - (unsigned) (seconds))); \ - MG_OTA_ROLLBACK_TIMER_START(seconds); \ + (unsigned) MG_OTA_ROLLBACK_TIMEOUT_SECONDS)); \ + MG_OTA_ROLLBACK_TIMER_START(); \ } \ } 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 } -// If the current firmware version differs from the JSON version, downloads -// FIRMWARE_URL and performs the OTA update. -void mg_ota_url_check(struct mg_mgr *mgr, const char *current_version, - const char *metadata_url, void (*fn)(const char *status)); +// If the server version differs from MG_OTA_FIRMWARE_VERSION, downloads +// FIRMWARE_URL and performs the OTA update. fn is called on every outcome: +// NULL on successful flash, "Same version" when already up to date, or an +// 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(). // 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 #endif -// Scannable "MG_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 #ifndef MG_OTA_PULL_INTERVAL_SECONDS #define MG_OTA_PULL_INTERVAL_SECONDS 60 diff --git a/src/ota_stm32f.c b/src/ota_stm32f.c index 2e1aacd5..61cbb9ff 100644 --- a/src/ota_stm32f.c +++ b/src/ota_stm32f.c @@ -35,6 +35,10 @@ static size_t flash_size(void) { 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) { // only F42x/F43x series (0x419) support dual bank return STM_DEV_ID == 0x419; @@ -152,12 +156,7 @@ MG_IRAM static bool mg_stm32f_erase(void *addr) { return ok; } -MG_IRAM static bool mg_stm32f_swap(void) { - // 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; -} +MG_IRAM static void single_bank_swap(char *p1, char *p2, size_t s, size_t ss); 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; } -// just overwrite instead of swap -MG_IRAM void single_bank_swap(char *p1, char *p2, size_t size) { - // no stdlib calls here - mg_stm32f_write(p1, p2, size); - *(volatile unsigned long *) 0xe000ed0c = 0x5fa0004; +MG_IRAM static bool mg_stm32f_swap(void) { + // STM32 F42x/F43x support dual bank, however, the memory mapping + // change will not be carried through a hard reset. Therefore, we use + // the single-bank scratch-sector swap for this family as well. + 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) { + size_t max; s_mg_flash_stm32f.size = flash_size(); #ifdef __ZEPHYR__ *((uint32_t *)0xE000ED94) = 0; MG_DEBUG(("Jailbreak %s", *((uint32_t *)0xE000ED94) == 0 ? "successful" : "failed")); #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); } @@ -212,19 +246,7 @@ bool mg_ota_write(const void *buf, size_t len) { } bool mg_ota_end(void) { - if (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); - } + mg_ota_flash_end(&s_mg_flash_stm32f); return false; } struct mg_flash *mg_flash = &s_mg_flash_stm32f; diff --git a/src/ota_stm32h5.c b/src/ota_stm32h5.c index 16848930..e6ebc204 100644 --- a/src/ota_stm32h5.c +++ b/src/ota_stm32h5.c @@ -9,7 +9,7 @@ static bool mg_stm32h5_swap(void); static struct mg_flash s_mg_flash_stm32h5 = { (void *) 0x08000000, // Start - 2 * 1024 * 1024, // Size, 2Mb + 0, // Size, FLASHSIZE_BASE 8 * 1024, // Sector size, 8k 16, // Align, 128 bit mg_stm32h5_write, @@ -25,6 +25,18 @@ static struct mg_flash s_mg_flash_stm32h5 = { #define FLASH_NSCCR (MG_FLASH_BASE + 0x30) #define FLASH_OPTSR_CUR (MG_FLASH_BASE + 0x50) #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 bool unlocked = false; @@ -71,15 +83,16 @@ static bool mg_stm32h5_erase(void *location) { } else { uintptr_t diff = (char *) location - (char *) s_mg_flash_stm32h5.start; 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 flash_unlock(); flash_clear_err(); MG_REG(FLASH_NSCR) = 0; - if ((sector < 128 && flash_bank_is_swapped()) || - (sector > 127 && !flash_bank_is_swapped())) { + if ((sector < bank_sectors && flash_bank_is_swapped()) || + (sector >= bank_sectors && !flash_bank_is_swapped())) { 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(5); // Start erasing 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) { + s_mg_flash_stm32h5.size = flash_size(); #ifdef __ZEPHYR__ *((uint32_t *)0xE000ED94) = 0; 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) { - if (!mg_ota_flash_end(&s_mg_flash_stm32h5)) return false; - *(volatile uint32_t *) 0xe000ed0c = 0x5fa0004U; // NVIC_SystemReset() - return true; + mg_ota_flash_end(&s_mg_flash_stm32h5); + return false; } struct mg_flash *mg_flash = &s_mg_flash_stm32h5; #endif diff --git a/src/ota_stm32h7.c b/src/ota_stm32h7.c index c6b01c45..95fa40cc 100644 --- a/src/ota_stm32h7.c +++ b/src/ota_stm32h7.c @@ -37,6 +37,11 @@ static struct mg_flash s_mg_flash_stm32h7 = { #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) { if (IS_DUALCORE()) { // 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; } +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) { - 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 desired = flash_bank_is_swapped(bank) ? 0 : MG_BIT(31); flash_unlock(); @@ -134,8 +158,6 @@ MG_IRAM static bool mg_stm32h7_swap(void) { return true; } -static bool s_flash_irq_disabled; - MG_IRAM static bool mg_stm32h7_write(void *addr, const void *buf, size_t len) { if ((len % s_mg_flash_stm32h7.align) != 0) { 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; } -// 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) { - // 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) { - 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() } bool mg_ota_begin(size_t new_firmware_size) { - s_mg_flash_stm32h7.size = MG_REG(FLASH_SIZE_REG) * 1024; - if (IS_DUALCORE()) { - // Using only the 1st bank (mapped to CM7) - s_mg_flash_stm32h7.size /= 2; - } + s_mg_flash_stm32h7.size = flash_size(); #ifdef __ZEPHYR__ *((uint32_t *) 0xE000ED94) = 0; MG_DEBUG(("Jailbreak %s", *((uint32_t *) 0xE000ED94) == 0 ? "ok" : "failed")); #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); } @@ -199,19 +232,6 @@ bool mg_ota_end(void) { if (is_dualbank()) { // Bank swap is deferred until reset, been executing in flash, reset *(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; diff --git a/tutorials/stm32/nucleo-h563zi/minimal/main.c b/tutorials/stm32/nucleo-h563zi/minimal/main.c index d19ea150..473589ba 100644 --- a/tutorials/stm32/nucleo-h563zi/minimal/main.c +++ b/tutorials/stm32/nucleo-h563zi/minimal/main.c @@ -73,18 +73,16 @@ int main(void) { hal_clock_init(); hal_uart_init(UART_DEBUG, UART_DEBUG_TX_PIN, UART_DEBUG_RX_PIN, 115200); mg_log_set_fn(log_fn, UART_DEBUG); + + MG_OTA_BOOT_CHECK(); // Must be called after clock init + hal_rng_init(); hal_gpio_output(LED_1); hal_gpio_output(LED_2); hal_gpio_output(LED_3); - hal_ethernet_init(); - MG_INFO(("Initialised. CPU clock: %lu MHz", SystemCoreClock / 1000000)); - // Max delay 131 seconds - MG_OTA_BOOT_CHECK(30); - struct mg_mgr mgr; mg_mgr_init(&mgr); mg_http_listen(&mgr, "http://0.0.0.0", http_ev_handler, NULL); diff --git a/tutorials/stm32/nucleo-h563zi/minimal/mongoose/mongoose_config.h b/tutorials/stm32/nucleo-h563zi/minimal/mongoose/mongoose_config.h index c88232e5..9479936f 100644 --- a/tutorials/stm32/nucleo-h563zi/minimal/mongoose/mongoose_config.h +++ b/tutorials/stm32/nucleo-h563zi/minimal/mongoose/mongoose_config.h @@ -24,18 +24,20 @@ } // 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 -// writing RLR/starting, otherwise the write is ignored (RM0481 §52.4.3). -#define MG_OTA_ROLLBACK_TIMER_START(seconds) \ +// Fixed 10 s hang-detection window: RLR = 10 * 32000 / 1024 = 312. +// PVU/RVU must clear before writing RLR/starting (RM0481 §52.4.3). +// mg_ota_poll() feeds the watchdog every 500ms while in MG_OTA_TESTING state. +#define MG_OTA_ROLLBACK_TIMER_START() \ do { \ IWDG->KR = 0x5555U; /* unlock */ \ IWDG->PR = 8U; /* /1024 */ \ 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 */ \ IWDG->KR = 0xCCCCU; /* start */ \ IWDG->KR = 0xAAAAU; /* reload RLR */ \ } while (0) +#define MG_OTA_ROLLBACK_TIMER_FEED() (IWDG->KR = 0xAAAAU) // 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. diff --git a/tutorials/stm32/nucleo-h723zg/minimal/main.c b/tutorials/stm32/nucleo-h723zg/minimal/main.c index 92922570..60ca9730 100644 --- a/tutorials/stm32/nucleo-h723zg/minimal/main.c +++ b/tutorials/stm32/nucleo-h723zg/minimal/main.c @@ -80,8 +80,7 @@ int main(void) { MG_INFO(("Initialised. CPU clock: %lu MHz", SystemCoreClock / 1000000)); - // Max delay 65 seconds - MG_OTA_BOOT_CHECK(30); + MG_OTA_BOOT_CHECK(); // Must be called after clock init struct mg_mgr mgr; mg_mgr_init(&mgr); diff --git a/tutorials/stm32/nucleo-h723zg/minimal/mongoose/mongoose_config.h b/tutorials/stm32/nucleo-h723zg/minimal/mongoose/mongoose_config.h index d6ac2109..1af698dd 100644 --- a/tutorials/stm32/nucleo-h723zg/minimal/mongoose/mongoose_config.h +++ b/tutorials/stm32/nucleo-h723zg/minimal/mongoose/mongoose_config.h @@ -24,18 +24,20 @@ // #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). -// 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, // 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 { \ IWDG1->KR = 0xCCCCU; /* start/LSI */ \ IWDG1->KR = 0x5555U; /* unlock */ \ 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 */ \ IWDG1->KR = 0xAAAAU; /* reload */ \ } while (0) +#define MG_OTA_ROLLBACK_TIMER_FEED() (IWDG1->KR = 0xAAAAU) // 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.