mirror of
https://github.com/mikecarper/MeshCore.git
synced 2026-10-20 17:22:01 +00:00
651 lines
25 KiB
C++
651 lines
25 KiB
C++
#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 "OtaHybridHandoff.h"
|
|
#include <string.h>
|
|
#include "nrf.h"
|
|
#include "flash/flash_nrf5x.h" // Adafruit core internal-flash driver (SoftDevice-safe; LittleFS path)
|
|
|
|
namespace mesh {
|
|
namespace ota {
|
|
|
|
namespace {
|
|
|
|
static void clear_hybrid_auth_record() {
|
|
#if defined(OTA_HYBRID_RAM_STORE)
|
|
volatile uint32_t* const retained =
|
|
reinterpret_cast<volatile uint32_t*>(MOTA_HYBRID_AUTH_ADDR);
|
|
for (uint32_t i = 0; i < MOTA_HYBRID_AUTH_LEN / 4u; ++i)
|
|
retained[i] = 0u;
|
|
__DMB();
|
|
__DSB();
|
|
#endif
|
|
}
|
|
|
|
static bool publish_hybrid_auth_record(
|
|
const uint8_t record[MOTA_HYBRID_AUTH_LEN]) {
|
|
#if defined(OTA_HYBRID_RAM_STORE)
|
|
if (!mota_hybrid_auth_valid(record)) return false;
|
|
volatile uint32_t* const retained =
|
|
reinterpret_cast<volatile uint32_t*>(MOTA_HYBRID_AUTH_ADDR);
|
|
// Invalidate first, publish the body, then make the first magic word live.
|
|
// Any reset during this sequence leaves a record OTAFIX rejects.
|
|
retained[0] = 0u;
|
|
__DMB();
|
|
for (uint32_t i = 1u; i < MOTA_HYBRID_AUTH_LEN / 4u; ++i)
|
|
retained[i] = mota_hybrid_rd32(record + i * 4u);
|
|
__DMB();
|
|
retained[0] = mota_hybrid_rd32(record);
|
|
__DMB();
|
|
__DSB();
|
|
for (uint32_t i = 0; i < MOTA_HYBRID_AUTH_LEN / 4u; ++i)
|
|
if (retained[i] != mota_hybrid_rd32(record + i * 4u)) return false;
|
|
return true;
|
|
#else
|
|
(void)record;
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
} // namespace
|
|
|
|
OtaStoreFlashNrf52::OtaStoreFlashNrf52() {
|
|
// A descriptor is valid for one immediate software-reset handoff only.
|
|
// OTAFIX consumes it before an application starts; clear any stale bytes if
|
|
// a reset reached this constructor without entering the hybrid path.
|
|
clear_hybrid_auth_record();
|
|
}
|
|
|
|
void OtaStoreFlashNrf52::reset_session() {
|
|
_write_start = 0;
|
|
_stage_ceiling = MOTA_NRF52_STAGE_CEILING_LEGACY;
|
|
_flash_len = 0;
|
|
_ram_len = 0;
|
|
_total = 0;
|
|
_hybrid = false;
|
|
_preserve_payload_pages = false;
|
|
_pay_idx = 0;
|
|
_flushed = false;
|
|
_io_ok = true;
|
|
_planned_bootloader = false;
|
|
_planned_hybrid = false;
|
|
_planned_total = 0;
|
|
_planned_start = 0;
|
|
_planned_flash_len = 0;
|
|
_planned_ram_len = 0;
|
|
memset(_hybrid_handoff, 0, sizeof(_hybrid_handoff));
|
|
_hybrid_handoff_ready = false;
|
|
}
|
|
|
|
// 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_hybrid = false;
|
|
_planned_total = 0;
|
|
_planned_start = 0;
|
|
_planned_flash_len = 0;
|
|
_planned_ram_len = 0;
|
|
const uint64_t total64 = (uint64_t)payload_off + payload_size + 5u;
|
|
if (total64 > UINT32_MAX) return false;
|
|
|
|
if (!is_bootloader) {
|
|
#if defined(OTA_HYBRID_RAM_STORE)
|
|
const uint32_t total = (uint32_t)total64;
|
|
const uint32_t app_base = mota_nrf52_app_base();
|
|
const uint32_t stage_ceiling = ota_nrf52_effective_stage_ceiling();
|
|
uint32_t app_end = 0;
|
|
uint32_t start = 0;
|
|
if (!protected_app_end(app_base, stage_ceiling, app_end)) return false;
|
|
uint32_t flash_len = 0, ram_len = 0;
|
|
const OtaRamCaps ram_caps = ota_bootloader_ram_caps();
|
|
// The layout record tells the offline packager to size detools' workspace
|
|
// against this exact split. Use it deterministically for every application
|
|
// delta larger than the mandatory flash page; choosing a smaller pure-
|
|
// flash start merely because the bytes fit could invalidate that geometry.
|
|
if (!is_full && ota_bootloader_supports_hybrid(ram_caps) &&
|
|
stage_ceiling == MOTA_NRF52_STAGE_CEILING_EXPANDED &&
|
|
mota_nrf52_hybrid_stage_plan(
|
|
total, app_base, app_end, stage_ceiling,
|
|
start, flash_len, ram_len)) {
|
|
_planned_hybrid = true;
|
|
_planned_total = total;
|
|
_planned_start = start;
|
|
_planned_flash_len = flash_len;
|
|
_planned_ram_len = ram_len;
|
|
return true;
|
|
}
|
|
// A container no larger than one page cannot have a non-empty SRAM suffix
|
|
// under the frozen split formula. Preserve the established flash path for
|
|
// that edge case; all larger hybrid-profile application packages fail
|
|
// closed if the installed bootloader lacks MOTARAMA.
|
|
if (total <= MOTA_NRF52_FLASH_PAGE)
|
|
return mota_nrf52_stage_plan(
|
|
total, app_base, app_end, stage_ceiling, start);
|
|
return false;
|
|
#else
|
|
(void)is_full;
|
|
return true;
|
|
#endif
|
|
}
|
|
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
|
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_update_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::read_staged_header(
|
|
void*, uint32_t address, uint32_t& total_size) {
|
|
const uint8_t* header = (const uint8_t*)(uintptr_t)address;
|
|
if (memcmp(header, MOTA_MAGIC, sizeof(MOTA_MAGIC)) != 0) return false;
|
|
total_size = rd_u32le(header + sizeof(MOTA_MAGIC));
|
|
return true;
|
|
}
|
|
|
|
bool OtaStoreFlashNrf52::invalidate_staged_header(
|
|
void* context, uint32_t address) {
|
|
OtaStoreFlashNrf52* store = static_cast<OtaStoreFlashNrf52*>(context);
|
|
const uint8_t* page = (const uint8_t*)(uintptr_t)address;
|
|
memcpy(store->_meta_page, page, PG);
|
|
|
|
// Magic prevents application reopen; approval prevents an explicitly
|
|
// triggered legacy bootloader from accepting a half-cleared container.
|
|
// Rewrite the page once so both durable markers are consumed together.
|
|
memset(store->_meta_page, 0, sizeof(MOTA_MAGIC));
|
|
memset(store->_meta_page + 8u + MOTA_OFF_APPROVAL, 0,
|
|
sizeof(APPROVAL_YES));
|
|
if (flash_nrf5x_write(address, store->_meta_page, PG) < 0) {
|
|
store->_io_ok = false;
|
|
return false;
|
|
}
|
|
flash_nrf5x_flush();
|
|
if (!flash_matches(address, store->_meta_page, PG)) {
|
|
store->_io_ok = false;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool OtaStoreFlashNrf52::discard() {
|
|
clear_hybrid_auth_record();
|
|
const uint32_t app_base = mota_nrf52_app_base();
|
|
const uint32_t stage_ceiling = ota_nrf52_effective_stage_ceiling();
|
|
uint32_t app_end = 0;
|
|
if (!protected_app_end(app_base, stage_ceiling, app_end)) {
|
|
reset_session();
|
|
_io_ok = false;
|
|
return false;
|
|
}
|
|
|
|
uint32_t invalidated = 0;
|
|
const bool ok = mota_nrf52_discard_staged_headers(
|
|
app_base, app_end, stage_ceiling, this, read_staged_header,
|
|
invalidate_staged_header, &invalidated);
|
|
OTA_DBG("OTA flash: discard invalidated=%u ok=%d\n",
|
|
(unsigned)invalidated, (int)ok);
|
|
reset_session();
|
|
_io_ok = ok;
|
|
return ok;
|
|
}
|
|
|
|
bool OtaStoreFlashNrf52::flush_page(uint32_t page_idx, const uint8_t* buf) {
|
|
if (!_io_ok || _flash_len < PG || page_idx >= _flash_len / PG ||
|
|
_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;
|
|
if (!protected_app_end(app_base, stage_ceiling, app_end)) return 0;
|
|
const uint32_t flash_capacity =
|
|
mota_nrf52_stage_capacity(app_base, app_end, stage_ceiling);
|
|
#if defined(OTA_HYBRID_RAM_STORE)
|
|
const OtaRamCaps ram_caps = ota_bootloader_ram_caps();
|
|
if (stage_ceiling == MOTA_NRF52_STAGE_CEILING_EXPANDED &&
|
|
flash_capacity >= PG && ota_bootloader_supports_hybrid(ram_caps) &&
|
|
flash_capacity <= UINT32_MAX - MOTA_NRF52_HYBRID_RAM_SIZE) {
|
|
return flash_capacity + MOTA_NRF52_HYBRID_RAM_SIZE;
|
|
}
|
|
#endif
|
|
return flash_capacity;
|
|
}
|
|
|
|
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;
|
|
if (!_hybrid || pos < _flash_len) {
|
|
uint32_t page_end = (pos / PG + 1) * PG;
|
|
if (end > page_end) end = page_end; // a run stays within one flash page,
|
|
if (_hybrid && end > _flash_len) end = _flash_len; // and one backing region,
|
|
}
|
|
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)
|
|
if (_hybrid && pos >= _flash_len)
|
|
return (const uint8_t*)(uintptr_t)(MOTA_NRF52_HYBRID_RAM_START +
|
|
pos - _flash_len);
|
|
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));
|
|
if (_hybrid && pos >= _flash_len)
|
|
return (uint8_t*)(uintptr_t)(MOTA_NRF52_HYBRID_RAM_START +
|
|
pos - _flash_len);
|
|
uint32_t page = pos / PG;
|
|
if (page == 0) return _meta_page + pos;
|
|
if (page > _pay_idx) {
|
|
if (!flush_pay()) return nullptr;
|
|
_pay_idx = page;
|
|
// A resumed page can contain verified blocks on either side of this hole.
|
|
// Those blocks retain their leaf markers, so erasing their cached bytes here
|
|
// would silently corrupt them when this page is checkpointed or finalized.
|
|
if (_preserve_payload_pages)
|
|
memcpy(_pay_page, (const uint8_t*)(uintptr_t)(_write_start + page * PG), PG);
|
|
else
|
|
memset(_pay_page, 0xFF, PG); // fresh capture, no committed bytes yet
|
|
}
|
|
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 bool planned_hybrid = _planned_hybrid;
|
|
const uint32_t planned_total = _planned_total;
|
|
const uint32_t planned_start = _planned_start;
|
|
const uint32_t planned_flash_len = _planned_flash_len;
|
|
const uint32_t planned_ram_len = _planned_ram_len;
|
|
clear_hybrid_auth_record();
|
|
reset_session();
|
|
|
|
// 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 = 0;
|
|
uint32_t flash_len = 0;
|
|
uint32_t ram_len = 0;
|
|
if (planned_hybrid) {
|
|
#if defined(OTA_HYBRID_RAM_STORE)
|
|
if (planned_bootloader || total_size != planned_total ||
|
|
stage_ceiling != MOTA_NRF52_STAGE_CEILING_EXPANDED ||
|
|
!ota_bootloader_supports_hybrid(ota_bootloader_ram_caps()) ||
|
|
!mota_nrf52_hybrid_stage_plan(
|
|
total_size, app_base, app_end, stage_ceiling,
|
|
start, flash_len, ram_len) ||
|
|
start != planned_start || flash_len != planned_flash_len ||
|
|
ram_len != planned_ram_len) {
|
|
return false;
|
|
}
|
|
#else
|
|
return false;
|
|
#endif
|
|
} else {
|
|
if (!mota_nrf52_stage_plan(
|
|
total_size, app_base, app_end, stage_ceiling, start)) {
|
|
return false;
|
|
}
|
|
if (planned_bootloader && start != planned_start) return false;
|
|
flash_len = stage_ceiling - start;
|
|
}
|
|
|
|
_write_start = start;
|
|
_stage_ceiling = stage_ceiling;
|
|
_flash_len = flash_len;
|
|
_ram_len = ram_len;
|
|
_total = total_size;
|
|
_hybrid = planned_hybrid;
|
|
memset(_meta_page, 0xFF, PG); // assemble page 0 in RAM; 0xFF = erased sentinel (unfilled leaf slots)
|
|
memset(_trailer, 0xFF, sizeof(_trailer));
|
|
#if defined(OTA_HYBRID_RAM_STORE)
|
|
if (_hybrid)
|
|
memset((void*)(uintptr_t)MOTA_NRF52_HYBRID_RAM_START, 0xFF, _ram_len);
|
|
#endif
|
|
_pay_idx = 0;
|
|
_flushed = false;
|
|
_io_ok = true;
|
|
_planned_bootloader = planned_bootloader;
|
|
_planned_hybrid = planned_hybrid;
|
|
_planned_total = planned_total;
|
|
_planned_start = planned_start;
|
|
_planned_flash_len = planned_flash_len;
|
|
_planned_ram_len = planned_ram_len;
|
|
OTA_DBG("OTA flash: begin total=%u start=%08x flash=%u ram=%u app_end=%08x ceiling=%08x\n",
|
|
(unsigned)total_size, (unsigned)start, (unsigned)flash_len,
|
|
(unsigned)ram_len, (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
|
|
const uint32_t trailer_off = _total - sizeof(_trailer);
|
|
uint32_t flash_bytes = sizeof(_trailer);
|
|
if (_hybrid) {
|
|
flash_bytes = trailer_off < _flash_len ? _flash_len - trailer_off : 0u;
|
|
if (flash_bytes > sizeof(_trailer)) flash_bytes = sizeof(_trailer);
|
|
}
|
|
if (flash_bytes != 0u) {
|
|
const uint32_t trailer_addr = _write_start + trailer_off;
|
|
if (flash_nrf5x_write(trailer_addr, _trailer, flash_bytes) < 0) {
|
|
_io_ok = false;
|
|
return false;
|
|
}
|
|
flash_nrf5x_flush();
|
|
if (!flash_matches(trailer_addr, _trailer, flash_bytes)) {
|
|
_io_ok = false;
|
|
return false;
|
|
}
|
|
}
|
|
if (_hybrid && flash_bytes < sizeof(_trailer)) {
|
|
const uint32_t logical = trailer_off + flash_bytes;
|
|
uint8_t* const dst = (uint8_t*)(uintptr_t)(
|
|
MOTA_NRF52_HYBRID_RAM_START + logical - _flash_len);
|
|
memcpy(dst, _trailer + flash_bytes, sizeof(_trailer) - flash_bytes);
|
|
__DMB();
|
|
__DSB();
|
|
if (memcmp(dst, _trailer + flash_bytes,
|
|
sizeof(_trailer) - flash_bytes) != 0) {
|
|
_io_ok = false;
|
|
return false;
|
|
}
|
|
}
|
|
_flushed = true;
|
|
return true;
|
|
}
|
|
|
|
bool OtaStoreFlashNrf52::approve_for_application(
|
|
const uint8_t normalized_container_hash[32]) {
|
|
clear_hybrid_auth_record();
|
|
_hybrid_handoff_ready = false;
|
|
if (!finalize() || _total < 8u + MOTA_MFL + 5u) return false;
|
|
const uint32_t offset = 8u + MOTA_OFF_APPROVAL;
|
|
if (offset + sizeof(APPROVAL_YES) > _flash_len) 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));
|
|
if (!_hybrid) return true;
|
|
#if defined(OTA_HYBRID_RAM_STORE)
|
|
if (!normalized_container_hash ||
|
|
!mota_hybrid_auth_encode(
|
|
_hybrid_handoff, _total, _write_start, _flash_len, _ram_len,
|
|
normalized_container_hash)) {
|
|
_io_ok = false;
|
|
return false;
|
|
}
|
|
_hybrid_handoff_ready = true;
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
bool OtaStoreFlashNrf52::publish_hybrid_handoff() {
|
|
#if defined(OTA_HYBRID_RAM_STORE)
|
|
if (!_hybrid || !_hybrid_handoff_ready || !_flushed || !_io_ok ||
|
|
!mota_hybrid_auth_valid(_hybrid_handoff)) {
|
|
return false;
|
|
}
|
|
return publish_hybrid_auth_record(_hybrid_handoff);
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
|
bool OtaStoreFlashNrf52::approve_for_bootloader() {
|
|
if (_hybrid || !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.
|
|
// Address ordering does not establish age: an old small container can mask a newer larger checkpoint.
|
|
bool OtaStoreFlashNrf52::reopen() {
|
|
return reopenFor(nullptr, 0);
|
|
}
|
|
|
|
bool OtaStoreFlashNrf52::reopenFor(const uint8_t* want_mid, uint32_t expected_target) {
|
|
// A retained-SRAM suffix cannot survive a power cycle or app restart as a
|
|
// resumable store. Reset all in-memory split state and adopt only a complete
|
|
// flash span whose advertised total fits before the selected ceiling.
|
|
reset_session();
|
|
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;
|
|
uint32_t selected_start = 0;
|
|
uint32_t selected_total = 0;
|
|
uint32_t selected_target = 0;
|
|
bool selected_bootloader = 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;
|
|
if (!mota_parse_manifest(p + 8u, MOTA_MFL, manifest)) continue;
|
|
const uint64_t payload_off = 8u + (uint64_t)MOTA_MFL + (uint64_t)manifest.block_count * 4u;
|
|
if (payload_off > PG || payload_off + manifest.payload_size + 5u != total) continue;
|
|
const bool bootloader = manifest.is_bootloader();
|
|
if (bootloader) {
|
|
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
|
SelfFwInfo fi;
|
|
if (!ota_bootloader_self_update_caps_valid(ota_bootloader_update_caps()) ||
|
|
!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;
|
|
}
|
|
if (want_mid && (memcmp(manifest.merkle_root, want_mid, 4) != 0 ||
|
|
(expected_target != 0 && manifest.target_id != expected_target))) continue;
|
|
// Without an explicit MID, every valid candidate participates in ambiguity
|
|
// detection; neither target nor firmware version proves which was newest.
|
|
if (selected_start != 0) return false;
|
|
selected_start = start;
|
|
selected_total = total;
|
|
selected_target = manifest.target_id;
|
|
selected_bootloader = bootloader;
|
|
}
|
|
if (selected_start == 0 ||
|
|
(expected_target != 0 && selected_target != expected_target)) return false;
|
|
_write_start = selected_start;
|
|
_stage_ceiling = stage_ceiling;
|
|
_flash_len = stage_ceiling - selected_start;
|
|
_ram_len = 0;
|
|
_total = selected_total;
|
|
_hybrid = false;
|
|
_preserve_payload_pages = true;
|
|
memcpy(_meta_page, (const uint8_t*)(uintptr_t)selected_start, PG);
|
|
// The tail is deferred until finalize and can still hold old flash bytes.
|
|
// Reconstruct only fixed framing; the manager must verify the manifest and
|
|
// rehash every present payload block before the container becomes complete.
|
|
memcpy(_trailer, MOTA_TRAILER, sizeof(_trailer));
|
|
_pay_idx = 0;
|
|
_flushed = false;
|
|
_io_ok = true;
|
|
_planned_bootloader = selected_bootloader;
|
|
_planned_total = selected_bootloader ? selected_total : 0u;
|
|
_planned_start = selected_bootloader ? selected_start : 0u;
|
|
OTA_DBG("OTA flash: reopen total=%u start=%08x ceiling=%08x\n",
|
|
(unsigned)_total, (unsigned)_write_start, (unsigned)stage_ceiling);
|
|
return true;
|
|
}
|
|
|
|
} // namespace ota
|
|
} // namespace mesh
|
|
|
|
#endif
|