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) {
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;
+106 -29
View File
@@ -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:<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
#ifndef MG_OTA_PULL_INTERVAL_SECONDS
#define MG_OTA_PULL_INTERVAL_SECONDS 60
+55
View File
@@ -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")))
-5
View File
@@ -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));
+29 -17
View File
@@ -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);
}
}
+51 -29
View File
@@ -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:<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
#ifndef MG_OTA_PULL_INTERVAL_SECONDS
#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;
}
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;
+20 -7
View File
@@ -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
+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)
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;
+3 -5
View File
@@ -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);
@@ -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.
+1 -2
View File
@@ -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);
@@ -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.