Add retained-RAM staging for nRF52 mOTA
This commit is contained in:
1 parent
23bf83277c
commit
49dcba5c6e
25 files changed
+2071
-83
No files matched your search
+210
-11
@@ -458,6 +458,39 @@ bool ota_apply_verify_slot(ApplyState&) { return false; }
|
||||
bool ota_apply_arm() { return false; }
|
||||
bool ota_apply_detools_mota(const uint8_t*, uint32_t, const SignerAllowlist&, ApplyState& st, char* msg) { st = ApplyState(); strcpy(msg, "use ota_apply_mota_nrf52"); return false; }
|
||||
|
||||
#if defined(OTA_FLASH_STORE)
|
||||
// Points at OtaContext's long-lived member only after the application package
|
||||
// has passed every gate and APRV was committed. The retained descriptor must
|
||||
// not be published until the command reply has drained and reset is imminent.
|
||||
static OtaStoreFlashNrf52* g_nrf52_apply_store = nullptr;
|
||||
#endif
|
||||
|
||||
static void ota_nrf52_set_reset_handoff(uint8_t request, uint8_t source) {
|
||||
uint8_t sd_en = 0;
|
||||
sd_softdevice_is_enabled(&sd_en);
|
||||
if (sd_en) { // POWER is SD-restricted while the SoftDevice runs
|
||||
sd_power_gpregret_clr(1, 0xFFFFFFFF);
|
||||
sd_power_gpregret_set(1, source);
|
||||
sd_power_gpregret_clr(0, 0xFFFFFFFF);
|
||||
if (request) sd_power_gpregret_set(0, request);
|
||||
} else {
|
||||
NRF_POWER->GPREGRET2 = source;
|
||||
NRF_POWER->GPREGRET = request;
|
||||
}
|
||||
}
|
||||
|
||||
static bool ota_nrf52_clear_reset_reasons() {
|
||||
uint8_t sd_en = 0;
|
||||
if (sd_softdevice_is_enabled(&sd_en) != NRF_SUCCESS) return false;
|
||||
if (sd_en) {
|
||||
return sd_power_reset_reason_clr(0xFFFFFFFFu) == NRF_SUCCESS;
|
||||
}
|
||||
NRF_POWER->RESETREAS = NRF_POWER->RESETREAS;
|
||||
__DMB();
|
||||
__DSB();
|
||||
return NRF_POWER->RESETREAS == 0u;
|
||||
}
|
||||
|
||||
void ota_reboot_to_apply() { // public: set the apply magic + reset (does not return)
|
||||
uint8_t stage_handoff = GPREGRET2_OTA_STAGE_LEGACY;
|
||||
#if defined(OTA_SD_STORE)
|
||||
@@ -465,19 +498,24 @@ void ota_reboot_to_apply() { // public: set the apply magic +
|
||||
#elif defined(OTA_QSPI_STORE)
|
||||
stage_handoff = GPREGRET2_OTA_STAGE_QSPI;
|
||||
#elif defined(OTA_FLASH_STORE)
|
||||
stage_handoff = mota_nrf52_flash_stage_handoff(ota_nrf52_effective_stage_ceiling());
|
||||
#endif
|
||||
uint8_t sd_en = 0;
|
||||
sd_softdevice_is_enabled(&sd_en);
|
||||
if (sd_en) { // POWER is SD-restricted while the SoftDevice runs
|
||||
sd_power_gpregret_clr(1, 0xFFFFFFFF);
|
||||
sd_power_gpregret_set(1, stage_handoff);
|
||||
sd_power_gpregret_clr(0, 0xFFFFFFFF);
|
||||
sd_power_gpregret_set(0, GPREGRET_OTA_APPLY);
|
||||
if (g_nrf52_apply_store && g_nrf52_apply_store->is_hybrid()) {
|
||||
// The bootloader admits retained SRAM only after a clean software-reset
|
||||
// handoff. Clear every prior reset reason immediately before publishing
|
||||
// the valid-last descriptor, then make A6/6A the only apply trigger.
|
||||
if (!ota_nrf52_clear_reset_reasons() ||
|
||||
!g_nrf52_apply_store->publish_hybrid_handoff()) {
|
||||
// Never fall back to scanning the flash prefix as a legacy container:
|
||||
// it intentionally advertises a total larger than its flash span.
|
||||
ota_nrf52_set_reset_handoff(0u, 0xBDu);
|
||||
NVIC_SystemReset();
|
||||
return;
|
||||
}
|
||||
stage_handoff = GPREGRET2_OTA_STAGE_HYBRID;
|
||||
} else {
|
||||
NRF_POWER->GPREGRET2 = stage_handoff;
|
||||
NRF_POWER->GPREGRET = GPREGRET_OTA_APPLY;
|
||||
stage_handoff = mota_nrf52_flash_stage_handoff(ota_nrf52_effective_stage_ceiling());
|
||||
}
|
||||
#endif
|
||||
ota_nrf52_set_reset_handoff(GPREGRET_OTA_APPLY, stage_handoff);
|
||||
NVIC_SystemReset(); // does not return
|
||||
}
|
||||
|
||||
@@ -514,6 +552,9 @@ static bool ota_apply_mota_nrf52_impl(const uint8_t* buf, uint32_t len,
|
||||
const uint8_t* rescue_base_hash,
|
||||
uint32_t local_target_id,
|
||||
ApplyState& st, char* msg) {
|
||||
#if defined(OTA_FLASH_STORE)
|
||||
g_nrf52_apply_store = nullptr;
|
||||
#endif
|
||||
st = ApplyState();
|
||||
MotaManifest m;
|
||||
if (!mota_parse(buf, len, m)) { strcpy(msg, "parse failed"); return false; }
|
||||
@@ -638,6 +679,164 @@ bool ota_rescue_mota_nrf52(const uint8_t* buf, uint32_t len, const SignerAllowli
|
||||
return ota_apply_mota_nrf52_impl(buf, len, allow, operator_base_hash, local_target_id, st, msg);
|
||||
}
|
||||
|
||||
#if defined(OTA_FLASH_STORE)
|
||||
bool ota_apply_mota_nrf52(OtaStoreFlashNrf52& store,
|
||||
const SignerAllowlist& allow,
|
||||
ApplyState& st, char* msg) {
|
||||
static const size_t CAP = 96;
|
||||
g_nrf52_apply_store = nullptr;
|
||||
st = ApplyState();
|
||||
|
||||
uint8_t header[8];
|
||||
uint8_t manifest[MOTA_MFL];
|
||||
const uint32_t total = store.staged_size();
|
||||
if (total < MOTA_NRF52_CONTAINER_MIN_SIZE ||
|
||||
!store.read(0, header, sizeof(header)) ||
|
||||
memcmp(header, MOTA_MAGIC, sizeof(MOTA_MAGIC)) != 0 ||
|
||||
rd_u32le(header + 4u) != total ||
|
||||
!store.read(8u, manifest, sizeof(manifest))) {
|
||||
strcpy(msg, "internal container parse failed");
|
||||
return false;
|
||||
}
|
||||
|
||||
MotaManifest m;
|
||||
if (!mota_parse_manifest(manifest, sizeof(manifest), m)) {
|
||||
strcpy(msg, "internal manifest parse failed");
|
||||
return false;
|
||||
}
|
||||
if (m.is_bootloader()) {
|
||||
strcpy(msg, "use explicit ota bootloader install");
|
||||
return false;
|
||||
}
|
||||
if (m.is_full() || m.codec_id != CODEC_DETOOLS_INPLACE) {
|
||||
strcpy(msg, "not an in-place delta");
|
||||
return false;
|
||||
}
|
||||
|
||||
const uint64_t payload_off64 =
|
||||
8u + MOTA_MFL + (uint64_t)m.block_count * 4u;
|
||||
if (payload_off64 > UINT32_MAX ||
|
||||
payload_off64 + m.payload_size + 5u != total) {
|
||||
strcpy(msg, "internal container layout mismatch");
|
||||
return false;
|
||||
}
|
||||
const uint32_t payload_off = (uint32_t)payload_off64;
|
||||
const uint32_t app_base = mota_nrf52_app_base();
|
||||
const uint32_t app_ceiling = mota_nrf52_application_ceiling();
|
||||
if (!mota_nrf52_target_image_fits(app_base, m.image_size,
|
||||
app_ceiling)) {
|
||||
strcpy(msg, "target image exceeds linked application region");
|
||||
return false;
|
||||
}
|
||||
|
||||
st.image_size = m.image_size;
|
||||
memcpy(st.image_hash, m.image_hash, sizeof(st.image_hash));
|
||||
st.manifest_ok = true;
|
||||
|
||||
const OtaBlCaps bl = ota_bootloader_app_caps();
|
||||
if (!bl.present) {
|
||||
strcpy(msg, "bootloader has no OTA-apply support; update it first");
|
||||
return false;
|
||||
}
|
||||
if (bl.apply_abi < m.format_ver ||
|
||||
!(bl.codec_mask & (1u << m.codec_id))) {
|
||||
snprintf(msg, CAP,
|
||||
"bootloader cannot apply update (abi=%u codecs=0x%x; need %u/%u)",
|
||||
bl.apply_abi, bl.codec_mask, m.format_ver, m.codec_id);
|
||||
return false;
|
||||
}
|
||||
if (store.is_hybrid() &&
|
||||
!ota_bootloader_supports_hybrid(ota_bootloader_ram_caps())) {
|
||||
strcpy(msg, "bootloader lacks hybrid RAM staging support");
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify through the logical OtaStore. A hybrid container is deliberately
|
||||
// non-contiguous; data() is never consulted by this path. The resulting SHA
|
||||
// is normalized across APRV bytes and is the handoff-record hash contract.
|
||||
VerifyResult vr = ota_verify(
|
||||
static_cast<const OtaStore&>(store), allow, manifest);
|
||||
st.sig_ok = vr.sig_ok;
|
||||
st.trusted = vr.trusted;
|
||||
if (!vr.root_ok || !vr.payload_ok || !vr.image_ok ||
|
||||
!vr.container_hash_ok) {
|
||||
strcpy(msg, "payload hash mismatch (incomplete or corrupt .mota)");
|
||||
return false;
|
||||
}
|
||||
|
||||
SelfFwInfo fi;
|
||||
if (!ota_self_firmware(fi) || !fi.valid) {
|
||||
strcpy(msg, "cannot read running firmware (no EndF)");
|
||||
return false;
|
||||
}
|
||||
if (!m.base_hash || memcmp(m.base_hash, fi.body_hash, 8u) != 0) {
|
||||
strcpy(msg, "not built for the running firmware (base mismatch)");
|
||||
return false;
|
||||
}
|
||||
st.slot_ok = true;
|
||||
if (vr.is_signed) {
|
||||
if (!vr.sig_ok) {
|
||||
strcpy(msg, "bad signature");
|
||||
return false;
|
||||
}
|
||||
if (!vr.trusted) {
|
||||
strcpy(msg, "untrusted signer (pubkey not in allowlist)");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
uint8_t patch_header[32];
|
||||
const uint32_t patch_header_len =
|
||||
m.payload_size < sizeof(patch_header)
|
||||
? m.payload_size : (uint32_t)sizeof(patch_header);
|
||||
InplacePatchDims d;
|
||||
if (!store.read(payload_off, patch_header, patch_header_len) ||
|
||||
!parse_inplace_patch_dims(patch_header, patch_header_len, d)) {
|
||||
strcpy(msg, "bad in-place patch header");
|
||||
return false;
|
||||
}
|
||||
if (d.memory == 0u || d.segment != MOTA_NRF52_FLASH_PAGE ||
|
||||
d.shift > d.memory || d.shift % d.segment != 0u ||
|
||||
d.from > d.memory - d.shift || d.from != fi.image_len ||
|
||||
d.to > d.memory || d.to != m.image_size ||
|
||||
!mota_nrf52_internal_patch_workspace_valid(
|
||||
d.memory, app_base, store.write_start(), m.image_size,
|
||||
app_ceiling)) {
|
||||
strcpy(msg, "invalid in-place patch geometry");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (store.is_hybrid()) {
|
||||
uint32_t planned_start = 0, planned_flash = 0, planned_ram = 0;
|
||||
if (fi.image_len > UINT32_MAX - app_base ||
|
||||
!mota_nrf52_hybrid_stage_plan(
|
||||
total, app_base, app_base + fi.image_len,
|
||||
MOTA_NRF52_STAGE_CEILING_EXPANDED,
|
||||
planned_start, planned_flash, planned_ram) ||
|
||||
planned_start != store.write_start() ||
|
||||
planned_flash != store.flash_len() ||
|
||||
planned_ram != store.ram_len()) {
|
||||
strcpy(msg, "hybrid staging geometry mismatch");
|
||||
return false;
|
||||
}
|
||||
} else if (store.ram_len() != 0u) {
|
||||
strcpy(msg, "internal staging geometry mismatch");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!store.approve_for_application(vr.container_hash)) {
|
||||
strcpy(msg, "internal approval/handoff preparation failed");
|
||||
return false;
|
||||
}
|
||||
g_nrf52_apply_store = &store;
|
||||
snprintf(msg, CAP,
|
||||
"verified%s%s; rebooting into bootloader after this reply",
|
||||
vr.is_signed ? " (signer trusted)" : " (unsigned)",
|
||||
store.is_hybrid() ? " in flash+RAM" : "");
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE)
|
||||
static const size_t NRF52_APPLY_MSG_CAP = 96;
|
||||
|
||||
|
||||
@@ -80,6 +80,13 @@ bool ota_apply_detools_mota(const uint8_t* buf, uint32_t len,
|
||||
// Returns true (msg = "verified...") when approved, false (msg = the first failing gate) otherwise.
|
||||
bool ota_apply_mota_nrf52(const uint8_t* buf, uint32_t len,
|
||||
const SignerAllowlist& allow, ApplyState& st, char* msg);
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE)
|
||||
class OtaStoreFlashNrf52;
|
||||
// Streaming internal-store path. Required for the hybrid source because its
|
||||
// flash prefix and retained-SRAM suffix have no single contiguous address.
|
||||
bool ota_apply_mota_nrf52(OtaStoreFlashNrf52& store,
|
||||
const SignerAllowlist& allow, ApplyState& st, char* msg);
|
||||
#endif
|
||||
// Recovery for failed app-side EndF validation. This is available only through the explicit
|
||||
// `ota rescue install <base_hash16>` CLI command and never through normal or automatic installation.
|
||||
bool ota_rescue_mota_nrf52(const uint8_t* buf, uint32_t len,
|
||||
@@ -115,7 +122,6 @@ bool ota_prepare_bootloader_update_nrf52(OtaStoreQspiNrf52& store,
|
||||
#endif
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && \
|
||||
defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
class OtaStoreFlashNrf52;
|
||||
bool ota_prepare_bootloader_update_nrf52(OtaStoreFlashNrf52& store,
|
||||
const SignerAllowlist& allow,
|
||||
const OtaBootloaderIdentity& installed,
|
||||
@@ -137,7 +143,7 @@ bool ota_installed_bootloader_identity(OtaBootloaderIdentity& out);
|
||||
inline uint8_t ota_nrf52_boot_result_or_zero(uint8_t value) {
|
||||
return ((value >= 0x90u && value <= 0x9Fu) ||
|
||||
// B0..BC are the ordinary apply progress/failure/success results;
|
||||
// SD-backed OTAFIX additionally uses BD for an authorization failure.
|
||||
// Retained-source OTAFIX paths use BD for authorization/handoff failure.
|
||||
(value >= 0xB0u && value <= 0xBDu) ||
|
||||
(value >= 0xC0u && value <= 0xCFu)) ? value : 0u;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#include "OtaFlashLayout_nrf52.h"
|
||||
#endif
|
||||
#include "OtaHybridHandoff.h"
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
@@ -29,6 +30,56 @@ struct OtaBlCaps {
|
||||
uint8_t storage_flags = 0; // OTA_BL_STORAGE_* capability bits
|
||||
};
|
||||
|
||||
// Independent marker for the reset-retained hybrid source. Keep this separate
|
||||
// from MOTABLDR: bootloader self-update policy relies on the exact deployed
|
||||
// storage_flags byte, and older applications must continue to parse it.
|
||||
struct OtaRamCaps {
|
||||
bool present = false;
|
||||
uint16_t abi = 0;
|
||||
uint16_t record_len = 0;
|
||||
uint32_t arena_size = 0;
|
||||
};
|
||||
|
||||
inline bool ota_ram_caps_marker_parse(const uint8_t raw[16],
|
||||
OtaRamCaps& out) {
|
||||
out = OtaRamCaps();
|
||||
if (!raw || memcmp(raw, MOTA_RAM_CAP_MAGIC,
|
||||
sizeof(MOTA_RAM_CAP_MAGIC)) != 0) {
|
||||
return false;
|
||||
}
|
||||
const uint16_t abi = mota_hybrid_rd16(raw + 8u);
|
||||
const uint16_t record_len = mota_hybrid_rd16(raw + 10u);
|
||||
const uint32_t arena_size = mota_hybrid_rd32(raw + 12u);
|
||||
if (abi != MOTA_RAM_CAP_ABI || record_len != MOTA_HYBRID_AUTH_LEN ||
|
||||
arena_size != MOTA_NRF52_HYBRID_RAM_SIZE) {
|
||||
return false;
|
||||
}
|
||||
out.present = true;
|
||||
out.abi = abi;
|
||||
out.record_len = record_len;
|
||||
out.arena_size = arena_size;
|
||||
return true;
|
||||
}
|
||||
|
||||
inline OtaRamCaps ota_ram_caps_scan_aligned(const uint8_t* bytes, size_t len) {
|
||||
OtaRamCaps result;
|
||||
if (!bytes) return result;
|
||||
uint8_t matches = 0;
|
||||
for (size_t off = 0; off + 16u <= len; off += 4u) {
|
||||
OtaRamCaps candidate;
|
||||
if (!ota_ram_caps_marker_parse(bytes + off, candidate)) continue;
|
||||
if (++matches != 1u) return OtaRamCaps();
|
||||
result = candidate;
|
||||
}
|
||||
return matches == 1u ? result : OtaRamCaps();
|
||||
}
|
||||
|
||||
inline bool ota_bootloader_supports_hybrid(const OtaRamCaps& caps) {
|
||||
return caps.present && caps.abi == MOTA_RAM_CAP_ABI &&
|
||||
caps.record_len == MOTA_HYBRID_AUTH_LEN &&
|
||||
caps.arena_size == MOTA_NRF52_HYBRID_RAM_SIZE;
|
||||
}
|
||||
|
||||
static const uint8_t OTA_BL_STORAGE_SD = 0x01;
|
||||
static const uint8_t OTA_BL_STORAGE_STAGE_CEILING = 0x02;
|
||||
static const uint8_t OTA_BL_STORAGE_QSPI = 0x04;
|
||||
@@ -184,6 +235,16 @@ inline OtaBlCaps ota_bootloader_app_caps() {
|
||||
#endif
|
||||
}
|
||||
|
||||
inline OtaRamCaps ota_bootloader_ram_caps() {
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_HYBRID_RAM_STORE)
|
||||
const uint8_t* lo = (const uint8_t*)(uintptr_t)MOTA_NRF52_BL_START;
|
||||
const uint8_t* hi = (const uint8_t*)(uintptr_t)MOTA_NRF52_BL_END;
|
||||
return ota_ram_caps_scan_aligned(lo, (size_t)(hi - lo));
|
||||
#else
|
||||
return OtaRamCaps();
|
||||
#endif
|
||||
}
|
||||
|
||||
// Scan the same region using the stricter, fail-closed rules required before
|
||||
// replacing the bootloader itself. Legacy bootloaders remain valid for normal
|
||||
// application deltas, but can never enter this privileged path.
|
||||
|
||||
@@ -1127,7 +1127,8 @@ static bool handle_dev(const char* d, char* reply, OtaContext& c) {
|
||||
strcpy(reply, "ERR completed fetch is in the host folder, not local verification storage");
|
||||
return true;
|
||||
}
|
||||
#if defined(NRF52_PLATFORM) && (defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE))
|
||||
#if defined(NRF52_PLATFORM) && \
|
||||
(defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE) || defined(OTA_FLASH_STORE))
|
||||
if (c.manager.fetchState() == OtaManager::COMPLETE) {
|
||||
VerifyResult r = ota_verify(static_cast<const OtaStore&>(c.fetch_store), c.allow);
|
||||
sprintf(reply, "verify parsed=%d root=%d payload=%d img=%d signed=%d sig=%d trust=%d | ok=%d auto=%d",
|
||||
@@ -1137,7 +1138,8 @@ static bool handle_dev(const char* d, char* reply, OtaContext& c) {
|
||||
}
|
||||
#endif
|
||||
const uint8_t* buf; uint32_t len;
|
||||
#if defined(NRF52_PLATFORM) && (defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE))
|
||||
#if defined(NRF52_PLATFORM) && \
|
||||
(defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE) || defined(OTA_FLASH_STORE))
|
||||
buf = c.serve_buf; len = c.serve_buf ? c.serve_expected : 0;
|
||||
#else
|
||||
if (c.manager.fetchState() == OtaManager::COMPLETE) { buf = c.fetch_store.data(); len = c.fetch_store.staged_size(); }
|
||||
@@ -1184,6 +1186,10 @@ static bool handle_dev(const char* d, char* reply, OtaContext& c) {
|
||||
}
|
||||
|
||||
} else if (strncmp(d, "clear", 5) == 0) {
|
||||
if (c.apply_pending) {
|
||||
strcpy(reply, "ERR update is armed; reboot is pending");
|
||||
return true;
|
||||
}
|
||||
const bool was_folder = c.fetch_to_folder;
|
||||
bool was_sd_archive = false;
|
||||
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
|
||||
|
||||
@@ -258,13 +258,21 @@ struct OtaContext {
|
||||
bool ok;
|
||||
#if defined(NRF52_PLATFORM) && (defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE))
|
||||
ok = ota_apply_mota_nrf52(fetch_store, allow, apply_st, msg);
|
||||
#elif defined(NRF52_PLATFORM)
|
||||
#elif defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE)
|
||||
if (rescue_base_hash) {
|
||||
ok = ota_rescue_mota_nrf52(fetch_store.data(), fetch_store.staged_size(), allow,
|
||||
rescue_base_hash, manager.target(), apply_st, msg);
|
||||
if (fetch_store.is_hybrid()) {
|
||||
strncpy(msg, "rescue cannot use volatile hybrid staging", 96);
|
||||
msg[95] = 0;
|
||||
ok = false;
|
||||
} else {
|
||||
ok = ota_rescue_mota_nrf52(fetch_store.data(), fetch_store.staged_size(), allow,
|
||||
rescue_base_hash, manager.target(), apply_st, msg);
|
||||
}
|
||||
} else {
|
||||
ok = ota_apply_mota_nrf52(fetch_store.data(), fetch_store.staged_size(), allow, apply_st, msg);
|
||||
ok = ota_apply_mota_nrf52(fetch_store, allow, apply_st, msg);
|
||||
}
|
||||
#elif defined(NRF52_PLATFORM)
|
||||
ok = ota_apply_mota_nrf52(fetch_store.data(), fetch_store.staged_size(), allow, apply_st, msg);
|
||||
#elif defined(ESP32_PLATFORM) && defined(OTA_FLASH_STORE)
|
||||
ok = ota_apply_detools_mota(fetch_store, allow, apply_st, msg);
|
||||
#else
|
||||
|
||||
@@ -15,7 +15,9 @@ namespace ota {
|
||||
|
||||
static const uint32_t MOTA_NRF52_APP_BASE_S140_V6 = 0x00026000u;
|
||||
static const uint32_t MOTA_NRF52_APP_BASE_S140_V7 = 0x00027000u;
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#if defined(MOTA_NRF52_TEST_APP_BASE)
|
||||
inline uint32_t mota_nrf52_app_base() { return MOTA_NRF52_TEST_APP_BASE; }
|
||||
#elif defined(NRF52_PLATFORM)
|
||||
extern "C" uint32_t __flash_arduino_start[]; // nrf52_common.ld: ORIGIN(FLASH)
|
||||
extern "C" uint32_t __flash_arduino_end[]; // nrf52_common.ld: ORIGIN(FLASH) + LENGTH(FLASH)
|
||||
extern "C" const uint8_t g_meshcore_internal_extrafs __attribute__((weak));
|
||||
@@ -54,6 +56,13 @@ static const uint8_t GPREGRET2_OTA_STAGE_LEGACY = 0xD4u;
|
||||
static const uint8_t GPREGRET2_OTA_STAGE_EXPANDED = 0xEDu;
|
||||
static const uint8_t GPREGRET2_OTA_STAGE_QSPI = 0x51u;
|
||||
static const uint8_t GPREGRET2_OTA_STAGE_SD = 0x53u;
|
||||
static const uint8_t GPREGRET2_OTA_STAGE_HYBRID = 0xA6u;
|
||||
|
||||
// Fixed SRAM reserved by the dedicated internal-only mOTA linker. A hybrid
|
||||
// container is one logical stream: a page-aligned flash prefix followed by a
|
||||
// reset-retained suffix at this address.
|
||||
static const uint32_t MOTA_NRF52_HYBRID_RAM_START = 0x20030000u;
|
||||
static const uint32_t MOTA_NRF52_HYBRID_RAM_SIZE = 0x00010000u;
|
||||
|
||||
inline uint8_t mota_nrf52_flash_stage_handoff(uint32_t effective_stage_ceiling) {
|
||||
if (effective_stage_ceiling == MOTA_NRF52_STAGE_CEILING_EXPANDED)
|
||||
@@ -123,7 +132,9 @@ inline uint32_t mota_nrf52_stage_ceiling_for_layout(uint32_t linked_app_end,
|
||||
}
|
||||
|
||||
inline uint32_t mota_nrf52_layout_stage_ceiling() {
|
||||
#if defined(NRF52_PLATFORM)
|
||||
#if defined(MOTA_NRF52_TEST_LAYOUT_STAGE_CEILING)
|
||||
return MOTA_NRF52_TEST_LAYOUT_STAGE_CEILING;
|
||||
#elif defined(NRF52_PLATFORM)
|
||||
// DataStore.cpp defines this symbol only for a companion that really constructs internal ExtraFS.
|
||||
// An undefined weak symbol has address zero, so other roles do not inherit nrf52_base's broad EXTRAFS
|
||||
// feature define as a false-positive storage reservation.
|
||||
@@ -273,6 +284,51 @@ inline bool mota_nrf52_stage_plan(uint32_t total_size, uint32_t app_base, uint32
|
||||
mota_nrf52_layout_stage_ceiling(), out_start);
|
||||
}
|
||||
|
||||
// Plan the exact split used by a hybrid-capable profile for every application
|
||||
// delta larger than one page. Reserve as little flash as possible (but always
|
||||
// one complete page for the header/manifest/approval), leaving the largest
|
||||
// possible detools workspace below it. The remainder is the reset-retained
|
||||
// SRAM suffix.
|
||||
inline bool mota_nrf52_hybrid_stage_plan(uint32_t total_size,
|
||||
uint32_t app_base,
|
||||
uint32_t app_end,
|
||||
uint32_t stage_ceiling,
|
||||
uint32_t& out_start,
|
||||
uint32_t& out_flash_len,
|
||||
uint32_t& out_ram_len) {
|
||||
if (stage_ceiling != MOTA_NRF52_STAGE_CEILING_EXPANDED ||
|
||||
(app_base != MOTA_NRF52_APP_BASE_S140_V6 &&
|
||||
app_base != MOTA_NRF52_APP_BASE_S140_V7) ||
|
||||
!mota_nrf52_layout_valid(app_base, stage_ceiling) ||
|
||||
app_end < app_base || app_end > stage_ceiling ||
|
||||
total_size < MOTA_NRF52_CONTAINER_MIN_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t flash_needed = total_size > MOTA_NRF52_HYBRID_RAM_SIZE
|
||||
? total_size - MOTA_NRF52_HYBRID_RAM_SIZE : 0u;
|
||||
if (flash_needed < MOTA_NRF52_FLASH_PAGE)
|
||||
flash_needed = MOTA_NRF52_FLASH_PAGE;
|
||||
if (flash_needed > UINT32_MAX - (MOTA_NRF52_FLASH_PAGE - 1u))
|
||||
return false;
|
||||
const uint32_t flash_len =
|
||||
(flash_needed + MOTA_NRF52_FLASH_PAGE - 1u) &
|
||||
~(MOTA_NRF52_FLASH_PAGE - 1u);
|
||||
if (flash_len >= total_size || flash_len > stage_ceiling - app_end)
|
||||
return false;
|
||||
|
||||
const uint32_t start = stage_ceiling - flash_len;
|
||||
const uint32_t ram_len = total_size - flash_len;
|
||||
if (start < app_end || ram_len == 0u ||
|
||||
ram_len > MOTA_NRF52_HYBRID_RAM_SIZE)
|
||||
return false;
|
||||
|
||||
out_start = start;
|
||||
out_flash_len = flash_len;
|
||||
out_ram_len = ram_len;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Durable internal-store cancellation has to work even when the live
|
||||
// OtaStore object is empty (for example, autofetch is off after a reboot). It
|
||||
// also has to invalidate every older bottom-aligned header: invalidating only
|
||||
@@ -294,6 +350,15 @@ inline bool mota_nrf52_staged_header_matches_layout(
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t hybrid_flash_len = 0, hybrid_ram_len = 0;
|
||||
if (effective_stage_ceiling == MOTA_NRF52_STAGE_CEILING_EXPANDED &&
|
||||
mota_nrf52_hybrid_stage_plan(
|
||||
total_size, app_base, app_end, effective_stage_ceiling,
|
||||
planned, hybrid_flash_len, hybrid_ram_len) &&
|
||||
planned == address) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// A device that moved from the legacy D4000 ceiling to the expanded ED000
|
||||
// ceiling can still contain an older, otherwise reopenable legacy header.
|
||||
// The expanded layout has no filesystem in that range, so it is safe to
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
#pragma once
|
||||
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "OtaFlashLayout_nrf52.h"
|
||||
|
||||
// Reset-retained authorization for the nRF52840 hybrid internal-flash/SRAM
|
||||
// staging source. The application publishes this record only after streaming
|
||||
// verification and approval. OTAFIX copies and consumes it before trusting any
|
||||
// retained bytes. A power loss destroys the SRAM suffix and therefore makes
|
||||
// the hash check fail closed before the first application write.
|
||||
|
||||
namespace mesh {
|
||||
namespace ota {
|
||||
|
||||
static const uintptr_t MOTA_HYBRID_AUTH_ADDR = 0x20006008u;
|
||||
static const uint16_t MOTA_HYBRID_AUTH_VERSION = 1u;
|
||||
static const uint16_t MOTA_HYBRID_AUTH_LEN = 72u;
|
||||
static const uint8_t MOTA_HYBRID_AUTH_PURPOSE_APP = 1u;
|
||||
static const uint8_t MOTA_HYBRID_AUTH_FORMAT_APP = 2u;
|
||||
static const uint8_t MOTA_HYBRID_AUTH_MAGIC[8] = {
|
||||
'M', 'O', 'T', 'A', 'H', 'Y', 'B', '1'};
|
||||
|
||||
static const uint8_t MOTA_RAM_CAP_MAGIC[8] = {
|
||||
'M', 'O', 'T', 'A', 'R', 'A', 'M', 'A'};
|
||||
static const uint16_t MOTA_RAM_CAP_ABI = 1u;
|
||||
static const uint16_t MOTA_RAM_CAP_RECORD_LEN = MOTA_HYBRID_AUTH_LEN;
|
||||
|
||||
static_assert(MOTA_HYBRID_AUTH_ADDR + MOTA_HYBRID_AUTH_LEN == 0x20006050u,
|
||||
"hybrid auth must occupy the dedicated retained-RAM tail");
|
||||
static_assert(MOTA_NRF52_HYBRID_RAM_START + MOTA_NRF52_HYBRID_RAM_SIZE ==
|
||||
0x20040000u,
|
||||
"hybrid arena must occupy the top 64 KiB of nRF52840 SRAM");
|
||||
|
||||
inline uint16_t mota_hybrid_rd16(const uint8_t* p) {
|
||||
return (uint16_t)p[0] | ((uint16_t)p[1] << 8);
|
||||
}
|
||||
|
||||
inline uint32_t mota_hybrid_rd32(const uint8_t* p) {
|
||||
return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
|
||||
((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
|
||||
}
|
||||
|
||||
inline void mota_hybrid_wr16(uint8_t* p, uint16_t value) {
|
||||
p[0] = (uint8_t)value;
|
||||
p[1] = (uint8_t)(value >> 8);
|
||||
}
|
||||
|
||||
inline void mota_hybrid_wr32(uint8_t* p, uint32_t value) {
|
||||
p[0] = (uint8_t)value;
|
||||
p[1] = (uint8_t)(value >> 8);
|
||||
p[2] = (uint8_t)(value >> 16);
|
||||
p[3] = (uint8_t)(value >> 24);
|
||||
}
|
||||
|
||||
inline uint32_t mota_hybrid_crc32(const uint8_t* data, size_t len) {
|
||||
uint32_t crc = 0xFFFFFFFFu;
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
crc ^= data[i];
|
||||
for (uint8_t bit = 0; bit < 8u; ++bit)
|
||||
crc = (crc >> 1) ^
|
||||
(0xEDB88320u & (uint32_t)-(int32_t)(crc & 1u));
|
||||
}
|
||||
return ~crc;
|
||||
}
|
||||
|
||||
inline bool mota_hybrid_auth_geometry_valid(uint32_t total,
|
||||
uint32_t flash_start,
|
||||
uint32_t flash_len,
|
||||
uint32_t ram_len) {
|
||||
if (total < MOTA_NRF52_CONTAINER_MIN_SIZE ||
|
||||
flash_len < MOTA_NRF52_FLASH_PAGE ||
|
||||
(flash_start & (MOTA_NRF52_FLASH_PAGE - 1u)) != 0u ||
|
||||
(flash_len & (MOTA_NRF52_FLASH_PAGE - 1u)) != 0u ||
|
||||
flash_start > UINT32_MAX - flash_len ||
|
||||
(uint64_t)flash_len + ram_len != total || ram_len == 0u ||
|
||||
ram_len > MOTA_NRF52_HYBRID_RAM_SIZE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t flash_needed = total > MOTA_NRF52_HYBRID_RAM_SIZE
|
||||
? total - MOTA_NRF52_HYBRID_RAM_SIZE : 0u;
|
||||
if (flash_needed < MOTA_NRF52_FLASH_PAGE)
|
||||
flash_needed = MOTA_NRF52_FLASH_PAGE;
|
||||
if (flash_needed > UINT32_MAX - (MOTA_NRF52_FLASH_PAGE - 1u))
|
||||
return false;
|
||||
const uint32_t expected_flash_len =
|
||||
(flash_needed + MOTA_NRF52_FLASH_PAGE - 1u) &
|
||||
~(MOTA_NRF52_FLASH_PAGE - 1u);
|
||||
if (flash_len != expected_flash_len || flash_len >= total)
|
||||
return false;
|
||||
// Hybrid staging is qualified only for dedicated no-ExtraFS profiles. It
|
||||
// must never reinterpret a legacy D4000 handoff as permission to use SRAM.
|
||||
return flash_start + flash_len ==
|
||||
MOTA_NRF52_STAGE_CEILING_EXPANDED;
|
||||
}
|
||||
|
||||
inline bool mota_hybrid_auth_encode(
|
||||
uint8_t out[MOTA_HYBRID_AUTH_LEN], uint32_t total,
|
||||
uint32_t flash_start, uint32_t flash_len, uint32_t ram_len,
|
||||
const uint8_t normalized_container_sha256[32]) {
|
||||
if (!out || !normalized_container_sha256 ||
|
||||
!mota_hybrid_auth_geometry_valid(
|
||||
total, flash_start, flash_len, ram_len)) {
|
||||
return false;
|
||||
}
|
||||
memset(out, 0, MOTA_HYBRID_AUTH_LEN);
|
||||
memcpy(out, MOTA_HYBRID_AUTH_MAGIC, sizeof(MOTA_HYBRID_AUTH_MAGIC));
|
||||
mota_hybrid_wr16(out + 8u, MOTA_HYBRID_AUTH_VERSION);
|
||||
mota_hybrid_wr16(out + 10u, MOTA_HYBRID_AUTH_LEN);
|
||||
out[12] = MOTA_HYBRID_AUTH_PURPOSE_APP;
|
||||
out[13] = MOTA_HYBRID_AUTH_FORMAT_APP;
|
||||
mota_hybrid_wr32(out + 16u, total);
|
||||
mota_hybrid_wr32(out + 20u, flash_start);
|
||||
mota_hybrid_wr32(out + 24u, flash_len);
|
||||
mota_hybrid_wr32(out + 28u, ram_len);
|
||||
memcpy(out + 32u, normalized_container_sha256, 32u);
|
||||
const uint32_t crc = mota_hybrid_crc32(out, 64u);
|
||||
mota_hybrid_wr32(out + 64u, crc);
|
||||
mota_hybrid_wr32(out + 68u, ~crc);
|
||||
return true;
|
||||
}
|
||||
|
||||
inline bool mota_hybrid_auth_valid(
|
||||
const uint8_t record[MOTA_HYBRID_AUTH_LEN]) {
|
||||
if (!record ||
|
||||
memcmp(record, MOTA_HYBRID_AUTH_MAGIC,
|
||||
sizeof(MOTA_HYBRID_AUTH_MAGIC)) != 0 ||
|
||||
mota_hybrid_rd16(record + 8u) != MOTA_HYBRID_AUTH_VERSION ||
|
||||
mota_hybrid_rd16(record + 10u) != MOTA_HYBRID_AUTH_LEN ||
|
||||
record[12] != MOTA_HYBRID_AUTH_PURPOSE_APP ||
|
||||
record[13] != MOTA_HYBRID_AUTH_FORMAT_APP ||
|
||||
mota_hybrid_rd16(record + 14u) != 0u) {
|
||||
return false;
|
||||
}
|
||||
const uint32_t crc = mota_hybrid_rd32(record + 64u);
|
||||
return mota_hybrid_auth_geometry_valid(
|
||||
mota_hybrid_rd32(record + 16u),
|
||||
mota_hybrid_rd32(record + 20u),
|
||||
mota_hybrid_rd32(record + 24u),
|
||||
mota_hybrid_rd32(record + 28u)) &&
|
||||
mota_hybrid_crc32(record, 64u) == crc &&
|
||||
mota_hybrid_rd32(record + 68u) == ~crc;
|
||||
}
|
||||
|
||||
} // namespace ota
|
||||
} // namespace mesh
|
||||
@@ -7,22 +7,79 @@
|
||||
#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;
|
||||
_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
|
||||
@@ -46,11 +103,54 @@ 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;
|
||||
if (!is_bootloader) return true;
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
_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()))
|
||||
@@ -118,6 +218,7 @@ bool OtaStoreFlashNrf52::invalidate_staged_header(
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -139,7 +240,8 @@ bool OtaStoreFlashNrf52::discard() {
|
||||
}
|
||||
|
||||
bool OtaStoreFlashNrf52::flush_page(uint32_t page_idx, const uint8_t* buf) {
|
||||
if (!_io_ok || _write_start > _stage_ceiling - PG ||
|
||||
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;
|
||||
@@ -156,8 +258,18 @@ 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;
|
||||
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() {
|
||||
@@ -168,14 +280,20 @@ 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 (!_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
|
||||
@@ -184,6 +302,9 @@ const uint8_t* OtaStoreFlashNrf52::read_slot(uint32_t pos) const {
|
||||
|
||||
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) {
|
||||
@@ -196,8 +317,12 @@ uint8_t* OtaStoreFlashNrf52::write_slot(uint32_t pos) {
|
||||
|
||||
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)
|
||||
@@ -224,23 +349,57 @@ bool OtaStoreFlashNrf52::begin(uint32_t total_size) {
|
||||
|
||||
// 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;
|
||||
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;
|
||||
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);
|
||||
_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
|
||||
}
|
||||
|
||||
@@ -282,17 +441,90 @@ bool OtaStoreFlashNrf52::finalize() {
|
||||
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; }
|
||||
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 (!finalize() || _total < 8u + MOTA_MFL + 5u) return false;
|
||||
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;
|
||||
@@ -326,6 +558,10 @@ void OtaStoreFlashNrf52::checkpoint() {
|
||||
// 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() {
|
||||
// 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;
|
||||
@@ -358,7 +594,10 @@ bool OtaStoreFlashNrf52::reopen() {
|
||||
}
|
||||
_write_start = start;
|
||||
_stage_ceiling = stage_ceiling;
|
||||
_flash_len = stage_ceiling - start;
|
||||
_ram_len = 0;
|
||||
_total = total;
|
||||
_hybrid = false;
|
||||
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;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "OtaStore.h"
|
||||
#include "OtaFlashLayout_nrf52.h"
|
||||
#include "OtaHybridHandoff.h"
|
||||
|
||||
// Persistent flash-backed OtaStore for nRF52840. Stages the received `.mota` below the selected
|
||||
// filesystem-safe ceiling, bottom-aligned so the bootloader can find it. Internal ExtraFS roles stop
|
||||
@@ -37,12 +38,18 @@ class OtaStoreFlashNrf52 : public OtaStore {
|
||||
|
||||
uint32_t _write_start = 0; // flash address of container offset 0 (page-aligned)
|
||||
uint32_t _stage_ceiling = MOTA_NRF52_STAGE_CEILING_LEGACY;
|
||||
uint32_t _flash_len = 0; // logical prefix backed by internal flash
|
||||
uint32_t _ram_len = 0; // logical suffix backed by the fixed SRAM arena
|
||||
uint32_t _total = 0; // staged container size (0 = none)
|
||||
bool _hybrid = false; // flash prefix + reset-retained SRAM suffix
|
||||
bool _flushed = false; // finalize() committed everything to flash
|
||||
bool _io_ok = true; // cleared on a failed/out-of-bounds flash write or readback mismatch
|
||||
bool _planned_bootloader = false; // manifest-kind decision made before begin()/any erase
|
||||
bool _planned_hybrid = false;
|
||||
uint32_t _planned_total = 0;
|
||||
uint32_t _planned_start = 0;
|
||||
uint32_t _planned_flash_len = 0;
|
||||
uint32_t _planned_ram_len = 0;
|
||||
|
||||
uint8_t _meta_page[PG]; // pinned flash page 0 (header + manifest + leaves + 1st payload)
|
||||
uint8_t _pay_page[PG]; // sliding buffer for one payload page (index _pay_idx)
|
||||
@@ -63,6 +70,7 @@ class OtaStoreFlashNrf52 : public OtaStore {
|
||||
static bool invalidate_staged_header(void* context, uint32_t address);
|
||||
|
||||
public:
|
||||
OtaStoreFlashNrf52();
|
||||
bool plan_layout(bool is_full, uint32_t image_size, uint32_t payload_off,
|
||||
uint32_t payload_size, bool is_bootloader) override;
|
||||
bool begin(uint32_t total_size) override;
|
||||
@@ -77,6 +85,12 @@ public:
|
||||
void checkpoint() override; // persist page 0 (leaves) + the open payload page so a reboot can resume
|
||||
bool reopen() override; // re-attach to a container already staged in flash (scan for it)
|
||||
|
||||
// Mark a verified application package approved. For a hybrid package this
|
||||
// also prepares (but does not yet publish) the reset-retained authorization;
|
||||
// publication happens immediately before the software-reset handoff.
|
||||
bool approve_for_application(const uint8_t normalized_container_hash[32]);
|
||||
bool publish_hybrid_handoff();
|
||||
|
||||
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
|
||||
// Clear the manifest approval word only after the privileged signed-image gates pass. OTAFIX scans
|
||||
// this page-aligned shared container after the ED handoff and repeats the structural, identity,
|
||||
@@ -88,8 +102,18 @@ public:
|
||||
|
||||
// Contiguous view (flash is memory-mapped). VALID ONLY AFTER finalize() - before that, page 0 and the
|
||||
// tail are still in RAM. OtaManager/OtaCli/verify use this only once the transfer is COMPLETE.
|
||||
const uint8_t* data() const { return (_flushed && _io_ok) ? (const uint8_t*)(uintptr_t)_write_start : nullptr; }
|
||||
const uint8_t* data() const {
|
||||
return (_flushed && _io_ok && !_hybrid)
|
||||
? (const uint8_t*)(uintptr_t)_write_start : nullptr;
|
||||
}
|
||||
uint32_t write_start() const { return _write_start; }
|
||||
uint32_t flash_len() const { return _flash_len; }
|
||||
uint32_t ram_len() const { return _ram_len; }
|
||||
bool is_hybrid() const { return _hybrid; }
|
||||
|
||||
private:
|
||||
uint8_t _hybrid_handoff[MOTA_HYBRID_AUTH_LEN] = {0};
|
||||
bool _hybrid_handoff_ready = false;
|
||||
};
|
||||
|
||||
} // namespace ota
|
||||
|
||||
Reference in new issue
Block a user