#include "OtaStoreFlashNrf52.h" #if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) #include "OtaSelf.h" #include "OtaBlInfo.h" #include "MotaContainer.h" #include "OtaDebug.h" #include "OtaByteIO.h" // align_down / rd_u32le (flash-page geometry + header read) #include #include "flash/flash_nrf5x.h" // Adafruit core internal-flash driver (SoftDevice-safe; LittleFS path) namespace mesh { namespace ota { // A valid EndF gives the exact live-image extent. Legacy app-only builds keep // the conservative rescue fallback. Shared internal bootloader-update builds // reject every package kind without EndF because their normal linker may place // live application bytes anywhere through the 0xED000 stage ceiling. static bool protected_app_end(uint32_t app_base, uint32_t stage_ceiling, uint32_t& app_end) { SelfFwInfo fi; const bool valid = ota_self_firmware(fi) && fi.valid; #if defined(OTA_INTERNAL_BOOTLOADER_UPDATE) const bool refuse_without_endf = true; #else const bool refuse_without_endf = false; #endif return mota_nrf52_protected_app_end( app_base, stage_ceiling, valid, valid ? fi.image_len : 0u, refuse_without_endf, app_end); } bool OtaStoreFlashNrf52::plan_layout(bool is_full, uint32_t image_size, uint32_t payload_off, uint32_t payload_size, bool is_bootloader) { _planned_bootloader = false; _planned_total = 0; _planned_start = 0; if (!is_bootloader) return true; #if defined(OTA_INTERNAL_BOOTLOADER_UPDATE) const uint64_t total64 = (uint64_t)payload_off + payload_size + 5u; if (!is_full || image_size != 40u * 1024u || payload_size != 40u * 1024u || payload_off != 365u || total64 != MOTA_NRF52_BOOT_CONTAINER_SIZE || !ota_bootloader_self_update_caps_valid(ota_bootloader_caps())) return false; // This is the release-blocking no-EndF gate: package kind is known before // begin(), and no page may be erased until the exact live app extent proves // the shared E2000..ED000 slot is free. SelfFwInfo fi; uint32_t start; const uint32_t app_base = mota_nrf52_app_base(); if (!ota_self_firmware(fi) || !fi.valid || fi.image_len > UINT32_MAX - app_base || !mota_nrf52_shared_boot_stage_plan( (uint32_t)total64, app_base, true, app_base + fi.image_len, start)) return false; _planned_bootloader = true; _planned_total = (uint32_t)total64; _planned_start = start; return true; #else (void)is_full; (void)image_size; (void)payload_off; (void)payload_size; return false; #endif } // Write one whole 4 KB page from `buf` to flash (erase + program, ~85 ms). `buf` is PG bytes, 0xFF-padded // past the container, so the program is clean. The selected ceiling and _write_start are page-aligned, // and the container ends below the ceiling, so a full-page write never reaches protected storage. static bool flash_matches(uint32_t addr, const uint8_t* expected, uint32_t n) { const volatile uint8_t* actual = (const volatile uint8_t*)(uintptr_t)addr; for (uint32_t i = 0; i < n; i++) if (actual[i] != expected[i]) return false; return true; } bool OtaStoreFlashNrf52::flush_page(uint32_t page_idx, const uint8_t* buf) { if (!_io_ok || _write_start > _stage_ceiling - PG || page_idx > (_stage_ceiling - PG - _write_start) / PG) { _io_ok = false; return false; } uint32_t addr = _write_start + page_idx * PG; OTA_DBG("OTA flash: write page %u @ %08x\n", (unsigned)page_idx, (unsigned)addr); if (flash_nrf5x_write(addr, buf, PG) < 0) { _io_ok = false; return false; } flash_nrf5x_flush(); if (!flash_matches(addr, buf, PG)) { _io_ok = false; return false; } return true; } uint32_t OtaStoreFlashNrf52::capacity() const { const uint32_t app_base = mota_nrf52_app_base(); const uint32_t stage_ceiling = ota_nrf52_effective_stage_ceiling(); uint32_t app_end; return protected_app_end(app_base, stage_ceiling, app_end) ? mota_nrf52_stage_capacity(app_base, app_end, stage_ceiling) : 0; } bool OtaStoreFlashNrf52::flush_pay() { return _pay_idx == 0 || flush_page(_pay_idx, _pay_page); // _pay_idx 0 == no payload page open } uint32_t OtaStoreFlashNrf52::run(uint32_t pos, uint32_t remain) const { uint32_t trailer = _total - 5; // container is always >= 13 bytes (begin checks) if (pos >= trailer) return remain; // tail: caller bounds remain to <= 5 already uint32_t end = pos + remain; uint32_t page_end = (pos / PG + 1) * PG; if (end > page_end) end = page_end; // a run stays within one flash page, if (end > trailer) end = trailer; // and never crosses into the trailer tail return end - pos; } const uint8_t* OtaStoreFlashNrf52::read_slot(uint32_t pos) const { if (pos >= _total - 5) return _trailer + (pos - (_total - 5)); // trailer tail (RAM until finalize) uint32_t page = pos / PG; if (page == 0) return _meta_page + pos; // pinned page 0 (incl. leaves) if (page == _pay_idx) return _pay_page + (pos - page * PG); // current sliding payload page return (const uint8_t*)(uintptr_t)(_write_start + pos); // already flushed -> memory-mapped } uint8_t* OtaStoreFlashNrf52::write_slot(uint32_t pos) { if (pos >= _total - 5) return _trailer + (pos - (_total - 5)); uint32_t page = pos / PG; if (page == 0) return _meta_page + pos; if (page > _pay_idx) { if (!flush_pay()) return nullptr; _pay_idx = page; memset(_pay_page, 0xFF, PG); // advance, fresh page } if (page == _pay_idx) return _pay_page + (pos - page * PG); return nullptr; // page < _pay_idx: already flushed } bool OtaStoreFlashNrf52::begin(uint32_t total_size) { const bool planned_bootloader = _planned_bootloader; const uint32_t planned_total = _planned_total; const uint32_t planned_start = _planned_start; clear(); // never collide with the running application image (its extent comes from its EndF trailer) const uint32_t app_base = mota_nrf52_app_base(); const uint32_t stage_ceiling = ota_nrf52_effective_stage_ceiling(); uint32_t app_end; if (planned_bootloader) { #if defined(OTA_INTERNAL_BOOTLOADER_UPDATE) SelfFwInfo fi; uint32_t checked_start; if (stage_ceiling != MOTA_NRF52_APP_END || total_size != planned_total || !ota_self_firmware(fi) || !fi.valid || fi.image_len > UINT32_MAX - app_base || !mota_nrf52_shared_boot_stage_plan( total_size, app_base, true, app_base + fi.image_len, checked_start) || checked_start != planned_start) return false; app_end = app_base + fi.image_len; #else return false; #endif } else if (!protected_app_end(app_base, stage_ceiling, app_end)) { return false; } // Bottom-align below the selected ceiling and reject unless it sits above the running image. The // approval path later verifies the patch's detools workspace ends at/below this exact start. uint32_t start; if (!mota_nrf52_stage_plan(total_size, app_base, app_end, stage_ceiling, start)) return false; if (planned_bootloader && start != planned_start) return false; _write_start = start; _stage_ceiling = stage_ceiling; _total = total_size; memset(_meta_page, 0xFF, PG); // assemble page 0 in RAM; 0xFF = erased sentinel (unfilled leaf slots) memset(_trailer, 0xFF, sizeof(_trailer)); _pay_idx = 0; _flushed = false; _io_ok = true; _planned_bootloader = planned_bootloader; _planned_total = planned_total; _planned_start = planned_start; OTA_DBG("OTA flash: begin total=%u start=%08x app_end=%08x ceiling=%08x\n", (unsigned)total_size, (unsigned)start, (unsigned)app_end, (unsigned)stage_ceiling); return true; // no pre-erase: each page is erased by its own (single) flush } bool OtaStoreFlashNrf52::write(uint32_t offset, const uint8_t* d, uint32_t len) { if ((uint64_t)offset + len > _total || !_io_ok) return false; for (uint32_t pos = offset, end = offset + len; pos < end; ) { uint32_t n = run(pos, end - pos); uint8_t* dst = write_slot(pos); if (!_io_ok) return false; if (dst) { memcpy(dst, d, n); } else { // out-of-order write to an already-flushed page: read-modify-write straight to flash. Safe -- the // driver erases the page before programming, so re-touching it never breaks writes-per-word. OTA_DBG("OTA flash: RMW page %u (out-of-order) @ off %u\n", (unsigned)(pos / PG), (unsigned)pos); uint32_t addr = _write_start + pos; if (flash_nrf5x_write(addr, d, n) < 0) { _io_ok = false; return false; } flash_nrf5x_flush(); if (!flash_matches(addr, d, n)) { _io_ok = false; return false; } } pos += n; d += n; } return true; } bool OtaStoreFlashNrf52::read(uint32_t offset, uint8_t* buf, uint32_t len) const { if ((uint64_t)offset + len > _total || !_io_ok) return false; for (uint32_t pos = offset, end = offset + len; pos < end; ) { uint32_t n = run(pos, end - pos); memcpy(buf, read_slot(pos), n); pos += n; buf += n; } return true; } bool OtaStoreFlashNrf52::finalize() { if (_flushed) return _io_ok; if (_total == 0 || !_io_ok) return false; OTA_DBG("OTA flash: finalize total=%u\n", (unsigned)_total); if (!flush_pay()) return false; // the last (highest) payload page, if one is open if (!flush_page(0, _meta_page)) return false; // header + manifest + leaves + first payload bytes uint32_t trailer_addr = _write_start + _total - 5; if (flash_nrf5x_write(trailer_addr, _trailer, 5) < 0) { _io_ok = false; return false; } flash_nrf5x_flush(); if (!flash_matches(trailer_addr, _trailer, 5)) { _io_ok = false; return false; } _flushed = true; return true; } #if defined(OTA_INTERNAL_BOOTLOADER_UPDATE) bool OtaStoreFlashNrf52::approve_for_bootloader() { if (!finalize() || _total < 8u + MOTA_MFL + 5u) return false; const uint32_t offset = 8u + MOTA_OFF_APPROVAL; if (offset + sizeof(APPROVAL_YES) > _total) return false; const uint32_t address = _write_start + offset; if (flash_nrf5x_write(address, APPROVAL_YES, sizeof(APPROVAL_YES)) < 0) { _io_ok = false; return false; } flash_nrf5x_flush(); if (!flash_matches(address, APPROVAL_YES, sizeof(APPROVAL_YES))) { _io_ok = false; return false; } memcpy(_meta_page + offset, APPROVAL_YES, sizeof(APPROVAL_YES)); return true; } #endif // Persist mid-transfer progress so a reboot can resume. Order matters for consistency: flush the open // payload page FIRST, then page 0 (the leaf-progress markers) -- so every block whose leaf is now in flash // also has its payload in flash. Infrequent (every OTA_CHECKPOINT_BLOCKS blocks), so the 2 extra page // erases don't matter; at LoRa block rates it's roughly once per many minutes. void OtaStoreFlashNrf52::checkpoint() { if (_total == 0 || _flushed || !_io_ok) return; if (!flush_pay()) return; // keep _pay_idx open; it may still receive writes flush_page(0, _meta_page); // header + manifest + leaves accumulated so far } // Re-attach to a container already staged in flash (after a reboot), without erasing. The container is // bottom-aligned (begin: start = (ceiling - total) & ~(PG-1)) and flash is memory-mapped, so scan page // starts from just below that ceiling down to the app end for MOTA_MAGIC with a self-consistent total; adopt // the first match (highest address = most recent for the common single-container case). The manager then // parses the loaded manifest and validates geometry/root, so a stale leftover is rejected there. bool OtaStoreFlashNrf52::reopen() { const uint32_t app_base = mota_nrf52_app_base(); const uint32_t stage_ceiling = ota_nrf52_effective_stage_ceiling(); uint32_t app_end; if (!protected_app_end(app_base, stage_ceiling, app_end)) return false; for (uint32_t start = align_down(stage_ceiling - PG, PG); start >= app_end; start -= PG) { const uint8_t* p = (const uint8_t*)(uintptr_t)start; if (memcmp(p, MOTA_MAGIC, 4) != 0) continue; uint32_t total = rd_u32le(p + 4); if (!mota_nrf52_container_span_valid( start, stage_ceiling, total, 8u + MOTA_MFL + 5u)) continue; uint32_t want; // must be valid + placed exactly where begin() would have staged it (same bounds fn) MotaManifest manifest; const bool manifest_ok = mota_parse_manifest(p + 8u, MOTA_MFL, manifest); const bool bootloader = manifest_ok && manifest.is_bootloader(); if (bootloader) { #if defined(OTA_INTERNAL_BOOTLOADER_UPDATE) SelfFwInfo fi; if (!ota_self_firmware(fi) || !fi.valid || fi.image_len > UINT32_MAX - app_base || !mota_nrf52_shared_boot_stage_plan( total, app_base, true, app_base + fi.image_len, want) || want != start) continue; #else continue; #endif } else if (!mota_nrf52_stage_plan( total, app_base, app_end, stage_ceiling, want) || want != start) { continue; } _write_start = start; _stage_ceiling = stage_ceiling; _total = total; memcpy(_meta_page, p, PG); // load page 0 (header+manifest+leaves) into RAM to continue memcpy(_trailer, p + (total - 5), 5); // recover the trailer tail (flushed at last finalize, if any) _pay_idx = 0; _flushed = false; _io_ok = true; _planned_bootloader = bootloader; _planned_total = bootloader ? total : 0u; _planned_start = bootloader ? start : 0u; OTA_DBG("OTA flash: reopen total=%u start=%08x ceiling=%08x\n", (unsigned)total, (unsigned)start, (unsigned)stage_ceiling); return true; } return false; } } // namespace ota } // namespace mesh #endif