Add retained-RAM staging for nRF52 mOTA

This commit is contained in:
mikecarper committed 2026-09-05 12:08:21 -07:00
1 parent 9155ea58c9
commit 2d50bad70a
25 files changed
+2071 -83

No files matched your search

+80
View File
@@ -0,0 +1,80 @@
/* S140 v6 application layout with a reset-retained 64 KiB mOTA arena. */
SEARCH_DIR(.)
GROUP(-lgcc -lc -lnosys)
MEMORY
{
FLASH (rx) : ORIGIN = 0x26000, LENGTH = 0xED000 - 0x26000
/* 8-byte retained clock state followed by the 72-byte MOTAHYB1 record. */
PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 80
/* Normal application data, heap, and stack end below the mOTA arena. */
RAM (rwx) : ORIGIN = 0x20006000 + 80, LENGTH = 0x20030000 - 0x20006000 - 80
/* Fixed staging arena retained across the software-reset bootloader handoff. */
MOTA_RAM (rwx) : ORIGIN = 0x20030000, LENGTH = 0x10000
}
SECTIONS
{
. = ALIGN(4);
.persistent (NOLOAD) :
{
KEEP(*(.persistent_magic))
KEEP(*(.persistent_data))
. = ALIGN(4);
} > PERSISTENT_RAM
/* Export the direct-access handoff ABI even though no input section owns it. */
__mota_hybrid_handoff_start__ = ADDR(.persistent) + SIZEOF(.persistent);
__mota_hybrid_handoff_end__ = ORIGIN(PERSISTENT_RAM) + LENGTH(PERSISTENT_RAM);
ASSERT(ADDR(.persistent) == 0x20006000,
"mOTA retained clock prefix start must remain fixed")
ASSERT(SIZEOF(.persistent) == 8,
"mOTA hybrid retained clock prefix must remain exactly 8 bytes")
ASSERT(__mota_hybrid_handoff_start__ == 0x20006008,
"mOTA hybrid handoff start must remain fixed")
ASSERT(__mota_hybrid_handoff_end__ == 0x20006050,
"mOTA hybrid handoff end must remain fixed")
ASSERT(__mota_hybrid_handoff_end__ - __mota_hybrid_handoff_start__ == 72,
"mOTA hybrid handoff must remain exactly 72 bytes")
ASSERT(ORIGIN(RAM) == __mota_hybrid_handoff_end__,
"normal RAM must start immediately after the mOTA handoff")
. = ALIGN(4);
.svc_data :
{
PROVIDE(__start_svc_data = .);
KEEP(*(.svc_data))
PROVIDE(__stop_svc_data = .);
} > RAM
.fs_data :
{
PROVIDE(__start_fs_data = .);
KEEP(*(.fs_data))
PROVIDE(__stop_fs_data = .);
} > RAM
/* No input section owns this memory. These symbols are the direct-access ABI. */
.mota_ram (NOLOAD) :
{
PROVIDE(__mota_ram_start__ = .);
. += LENGTH(MOTA_RAM);
PROVIDE(__mota_ram_end__ = .);
} > MOTA_RAM
ASSERT(ORIGIN(MOTA_RAM) == 0x20030000,
"mOTA RAM arena start must remain fixed")
ASSERT(LENGTH(MOTA_RAM) == 0x10000,
"mOTA RAM arena must remain exactly 64 KiB")
ASSERT(ORIGIN(RAM) + LENGTH(RAM) == ORIGIN(MOTA_RAM),
"normal RAM must end at the mOTA arena")
ASSERT(ORIGIN(MOTA_RAM) + LENGTH(MOTA_RAM) == 0x20040000,
"mOTA RAM arena must end at physical SRAM top")
}
INCLUDE "nrf52_common.ld"
+80
View File
@@ -0,0 +1,80 @@
/* S140 v7 application layout with a reset-retained 64 KiB mOTA arena. */
SEARCH_DIR(.)
GROUP(-lgcc -lc -lnosys)
MEMORY
{
FLASH (rx) : ORIGIN = 0x27000, LENGTH = 0xED000 - 0x27000
/* 8-byte retained clock state followed by the 72-byte MOTAHYB1 record. */
PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 80
/* Normal application data, heap, and stack end below the mOTA arena. */
RAM (rwx) : ORIGIN = 0x20006000 + 80, LENGTH = 0x20030000 - 0x20006000 - 80
/* Fixed staging arena retained across the software-reset bootloader handoff. */
MOTA_RAM (rwx) : ORIGIN = 0x20030000, LENGTH = 0x10000
}
SECTIONS
{
. = ALIGN(4);
.persistent (NOLOAD) :
{
KEEP(*(.persistent_magic))
KEEP(*(.persistent_data))
. = ALIGN(4);
} > PERSISTENT_RAM
/* Export the direct-access handoff ABI even though no input section owns it. */
__mota_hybrid_handoff_start__ = ADDR(.persistent) + SIZEOF(.persistent);
__mota_hybrid_handoff_end__ = ORIGIN(PERSISTENT_RAM) + LENGTH(PERSISTENT_RAM);
ASSERT(ADDR(.persistent) == 0x20006000,
"mOTA retained clock prefix start must remain fixed")
ASSERT(SIZEOF(.persistent) == 8,
"mOTA hybrid retained clock prefix must remain exactly 8 bytes")
ASSERT(__mota_hybrid_handoff_start__ == 0x20006008,
"mOTA hybrid handoff start must remain fixed")
ASSERT(__mota_hybrid_handoff_end__ == 0x20006050,
"mOTA hybrid handoff end must remain fixed")
ASSERT(__mota_hybrid_handoff_end__ - __mota_hybrid_handoff_start__ == 72,
"mOTA hybrid handoff must remain exactly 72 bytes")
ASSERT(ORIGIN(RAM) == __mota_hybrid_handoff_end__,
"normal RAM must start immediately after the mOTA handoff")
. = ALIGN(4);
.svc_data :
{
PROVIDE(__start_svc_data = .);
KEEP(*(.svc_data))
PROVIDE(__stop_svc_data = .);
} > RAM
.fs_data :
{
PROVIDE(__start_fs_data = .);
KEEP(*(.fs_data))
PROVIDE(__stop_fs_data = .);
} > RAM
/* No input section owns this memory. These symbols are the direct-access ABI. */
.mota_ram (NOLOAD) :
{
PROVIDE(__mota_ram_start__ = .);
. += LENGTH(MOTA_RAM);
PROVIDE(__mota_ram_end__ = .);
} > MOTA_RAM
ASSERT(ORIGIN(MOTA_RAM) == 0x20030000,
"mOTA RAM arena start must remain fixed")
ASSERT(LENGTH(MOTA_RAM) == 0x10000,
"mOTA RAM arena must remain exactly 64 KiB")
ASSERT(ORIGIN(RAM) + LENGTH(RAM) == ORIGIN(MOTA_RAM),
"normal RAM must end at the mOTA arena")
ASSERT(ORIGIN(MOTA_RAM) + LENGTH(MOTA_RAM) == 0x20040000,
"mOTA RAM arena must end at physical SRAM top")
}
INCLUDE "nrf52_common.ld"
+15 -9
View File
@@ -12,7 +12,7 @@ provisioned locally before either application or bootloader OTA.
| Layout | Application limit | Staged boot package | Work area | Handoff / capability |
| --- | ---: | ---: | ---: | --- |
| XIAO-module raw QSPI | below `0xE0000` | external QSPI offset 0 | dedicated internal `0xE0000..0xEA000` scratch | source `0x51`, flags `0x0E` |
| Qualified internal-flash target | normal `0xED000` limit | shared internal slot, exact start `0xE2000` | the same eleven-page slot; no second reservation | source `0xED`, flags `0x0A` |
| Qualified internal-flash target | normal `0xED000` flash limit; top 64 KiB SRAM reserved for application-delta staging | shared internal slot, exact start `0xE2000` | the same eleven-page slot; no second flash reservation | source `0xED`, flags `0x0A` |
| MeshTower V2 microSD | normal `0xED000` limit | contiguous `/meshcore-ota.mota` | dynamic internal `0xE0000..0xEA000` scratch; live image must end by `0xE0000` | source `0x53`, flags `0x09` |
The exact SD target is
@@ -53,10 +53,13 @@ ownership. Upgrade preview.12 through USB/BLE DFU or SWD. Both fmt2 application
and fmt3 bootloader OTA then require the installed BLM2 metadata to match the
live SoftDevice/application layout; neither MeshCore nor OTAFIX uses sector 1.
The internal path does not change the application linker or permanently set
aside separate app-OTA, boot-package, and scratch regions. The ordinary
bottom-aligned internal store holds one container at a time: either an
application delta or a bootloader package.
The internal path does not change the application's `0xED000` flash limit or
permanently set aside separate app-OTA, boot-package, and flash-scratch
regions. Qualified applications do use a dedicated linker that reserves the
top 64 KiB of SRAM for hybrid application-delta staging and a retained
authorization record. That SRAM is never part of a bootloader-update package.
The ordinary bottom-aligned internal flash store holds one container prefix at
a time: either an application delta or the complete bootloader package.
The exact bootloader container is 41,330 bytes: 365 bytes of signed mOTA
metadata, a 40 KiB payload, and the five-byte trailer. Below the normal
@@ -68,10 +71,13 @@ erasing and compacts the payload forward in place to the page-aligned raw range
MBR to copy that image over `0xF4000..0xFE000`.
An ordinary application delta can be smaller or larger than this eleven-page
shape. It bottom-aligns dynamically below `0xED000` and may begin below
`0xE2000`; its detools workspace must stop at its actual container start, while
the reconstructed application must stop below `0xED000`. The two package kinds
are mutually exclusive because they use the same store.
shape. On the qualified profile, a delta larger than one page uses the minimum
page-aligned flash prefix ending at `0xED000` and keeps up to 64 KiB of its
logical tail in reset-retained SRAM; its detools workspace must stop at the
actual flash-prefix start. A one-page-or-smaller delta stays wholly in flash.
In either case the reconstructed application must stop below `0xED000`. The
two package kinds are mutually exclusive because they use the same flash
store, and a bootloader package never uses the volatile SRAM suffix.
For internal-self-update builds, an absent or corrupt live `EndF` disables
**all** internal staging before the first erase. The older 608 KiB rescue
+32 -16
View File
@@ -394,7 +394,9 @@ There is no separate availability structure. **Block `i` is present <=> `leaves[
**Commit order per block (crash-safe):** (1) verify proof, (2) write block payload to its offset, (3) write
`leaves[i]` **last**. A power loss before step 3 leaves the slot erased -> the block is simply re-fetched
(idempotent). On boot a node rebuilds an in-RAM present-bitmap by scanning `leaves[]`.
(idempotent). On boot a node rebuilds an in-RAM present-bitmap by scanning `leaves[]` for a persistent,
reopenable store. A hybrid nRF52 transfer is the deliberate exception: its SRAM-backed payload suffix is
volatile, so the application refuses to adopt that staged header instead of rebuilding partial progress.
**Resume (`OtaManager::resumeStaged` + `OtaStore::checkpoint`/`reopen`):** an interrupted fetch resumes from
the staged container after a reboot - re-parse the stored manifest, recompute geometry, count present
@@ -407,11 +409,14 @@ package and keeps target `0` as a MID-only wildcard. Stores keep `leaves[]` in R
auto-GC, preserving resumable progress. The debug/operator equivalent is `ota dev resume <MID8>`; after a
reboot the MID is mandatory, while a no-argument form may only reuse a still-active session MID. It never
uses the `nullptr` automatic-adoption path, so a malformed MID or no active MID fails closed.
Hybrid nRF52 staging cannot enter this resume path after an application restart, even if its flash prefix
still contains metadata; the complete logical container must be fetched again.
**Flash-store note (RX-safe writes):** a flash page-erase halts the CPU (~85 ms on nRF52) and starves LoRa
RX, so the flash stores (`OtaStoreFlashNrf52`/`OtaStoreFlashEsp32`) **coalesce writes to the erase unit**
(4 KB page / sector) and commit each once off the per-packet path - RAM stays O(one page), not O(image). A
small delta that fits page 0 does zero flash I/O until COMPLETE.
(4 KB page / sector) and commit each once off the per-packet path. Ordinary stores keep RAM at O(one page),
not O(image); a qualified hybrid nRF52 profile additionally reserves one fixed 64 KiB staging arena. A small
delta that fits page 0 does zero flash I/O until COMPLETE.
---
@@ -932,16 +937,26 @@ retains its `.part` file; it does not erase the unrelated manual-install store.
- **nRF52 internal staging ceiling:** an internal-store application derives the ceiling from facts available
in every build, not a board-name list. A companion that actually links the internal ExtraFS datastore stays
below `0xD4000`; a default linker region or a role that does not mount ExtraFS can reclaim the unused
100 KiB through `0xED000`. An internal bootloader-self-update target keeps that normal linker and ceiling;
it does not reserve a second boot-package or scratch region. The application uses a larger-than-legacy
window only when the
installed bootloader advertises the GPREGRET2 ceiling-handoff capability. The bootloader treats every
unknown/legacy handoff value as
`0xD4000`, and accepts a container only at the bottom-aligned position for the selected ceiling.
100 KiB through `0xED000`. A qualified internal bootloader-self-update target keeps that normal flash
ceiling and does not reserve a second boot-package or flash-scratch region, but it uses a dedicated
application linker that reserves the top 64 KiB of SRAM (`0x20030000..0x20040000`) plus a 72-byte
retained authorization record after the existing persistent clock bytes. Application deltas larger than
one flash page use a deterministic page-aligned flash prefix ending at `0xED000` and place up to 64 KiB
of the logical container tail in that SRAM. Packages of one page or less, and bootloader-update packages,
remain wholly in flash. Hybrid staging is enabled only when the installed bootloader contains exactly one
valid `MOTARAMA` capability marker; otherwise a larger hybrid-profile fetch fails before erase. Immediately
before a software reset the application publishes a valid-last `MOTAHYB1` record binding the split geometry
and normalized container hash. The bootloader consumes it once and rejects power-loss, stale-reset, corrupt,
or mismatched-RAM cases before its first application write. A hybrid transfer cannot resume after an
application restart because its suffix is deliberately volatile. The bootloader treats every unknown/legacy
GPREGRET2 handoff value as `0xD4000`, and accepts a flash-only container only at the bottom-aligned position
for the selected ceiling.
- **nRF52 dynamic apply window:** the post-build hook records the resolved app base, linked app end,
internal-ExtraFS/SD/QSPI storage flags, and desired staging ceiling immediately before `EndF`. `motatool` reads that authenticated
firmware record and chooses `memory_size` from the actual patch size and bottom-aligned stage address;
firmware without the record retains the conservative `0x98000` default. Before writing `APRV`, an
internal-ExtraFS/SD/QSPI/hybrid-RAM storage flags, and desired staging ceiling immediately before `EndF`.
`motatool` reads that authenticated firmware record and chooses `memory_size` from the actual patch size
and bottom-aligned stage address. For a hybrid base it charges only the deterministic flash prefix against
that workspace; the retained-RAM suffix is still part of the same hashed logical container.
Firmware without the record retains the conservative `0x98000` default. Before writing `APRV`, an
internal-store app validates the staged-address bound; an external SD/QSPI app validates the full detools
geometry against the application workspace. The bootloader independently parses and validates the same
geometry before its first application write. Expanded auto-sized packages require a bootloader
@@ -1013,10 +1028,11 @@ retains its `.part` file; it does not erase the unrelated manual-install store.
It holds either an ordinary app delta or the exact 41,330-byte v3 container, never both. The boot package
bottom-aligns at `0xE2000`; a hash-valid live `EndF` must prove the complete running image ends at or below
that address before the first erase. OTAFIX reads each source window before erasing and compacts the
payload forward in the same eleven pages to raw `0xE2000..0xEC000`; no separate scratch bank or special
application linker exists. GPREGRET `0x6B` plus GPREGRET2 `0xED` selects boot update, while ordinary app
apply uses GPREGRET `0x6A` plus the same storage source. Exact installed/candidate capability flags are
`0x0A` (`STAGE_CEILING|BOOT_UPDATE`). Ordinary deltas remain dynamically sized, may start below
payload forward in the same eleven pages to raw `0xE2000..0xEC000`; no separate flash scratch bank exists.
The qualified application linker reserves a 64 KiB SRAM arena only for hybrid application deltas; the
bootloader package remains wholly in flash. GPREGRET `0x6B` plus GPREGRET2 `0xED` selects boot update,
while ordinary app apply uses GPREGRET `0x6A` plus the same storage source. Exact installed/candidate
capability flags are `0x0A` (`STAGE_CEILING|BOOT_UPDATE`). Ordinary deltas remain dynamically sized, may start below
`0xE2000`, and reconstruct only below the normal `0xED000` app ceiling. The same signature, explicit
confirmation, exact identity, vector, CRC, and single-marker rules as the XIAO path apply. Bootloader FULL
admission is isolated from ordinary application FULL policy, and privileged partials are never resumed
+32 -11
View File
@@ -1,9 +1,10 @@
"""Enable shared-slot internal bootloader update support on qualified nRF52840 targets.
The bootloader package uses the ordinary bottom-aligned internal OTA store
below 0xED000; no second linker reservation exists. This pre-script only binds
the feature macro to the exact release inventory and fails closed if a target
does not use the normal S140 v6/v7 1 MiB application layout.
below 0xED000. Qualified internal-only targets also use a dedicated linker that
reserves the top 64 KiB of SRAM for hybrid flash-prefix/RAM-suffix staging.
This pre-script binds both features to the exact release inventory and fails
closed if a target does not use the normal S140 v6/v7 1 MiB application layout.
"""
Import("env")
@@ -30,7 +31,10 @@ requested = (
if requested:
# The macro is injected here so direct PIO and build.sh synthetic aliases
# cannot silently differ. Package admission is runtime-capacity based.
env.AppendUnique(CPPDEFINES=["OTA_INTERNAL_BOOTLOADER_UPDATE"])
env.AppendUnique(CPPDEFINES=[
"OTA_INTERNAL_BOOTLOADER_UPDATE",
"OTA_HYBRID_RAM_STORE",
])
board = env.BoardConfig()
# Most variants inherit the framework's top-level ``build.ldscript``.
# A few otherwise ordinary Adafruit nRF52840 boards (notably Keepteen LT1)
@@ -40,17 +44,34 @@ if requested:
board.get("build.ldscript", "")
or board.get("build.arduino.ldscript", "")
)
linker_versions = {
"nrf52840_s140_v6.ld": "v6",
"nrf52840_s140_v7.ld": "v7",
}
# Compare the complete filename, not merely a suffix: a custom linker such
# as ``board_nrf52840_s140_v6.ld`` must not be mistaken for the qualified
# normal map and silently replaced with the mOTA layout.
current_name = current.replace("\\", "/").rsplit("/", 1)[-1]
suffix = linker_versions.get(current_name)
mcu = str(board.get("build.mcu", "")).lower()
if mcu != "nrf52840" or not (
current.endswith("nrf52840_s140_v6.ld")
or current.endswith("nrf52840_s140_v7.ld")
):
if mcu != "nrf52840" or suffix is None:
raise RuntimeError(
"shared-slot bootloader update requires an nRF52840 with the normal "
f"S140 v6/v7 non-ExtraFS linker; found mcu={mcu or 'none'} "
f"linker={current or 'none'}"
)
print(
"nRF52 shared-slot bootloader update: normal linker %s; runtime EndF headroom required"
% current
mota_linker = Path(env["PROJECT_DIR"]) / "boards" / (
"nrf52840_s140_%s_mota64.ld" % suffix
)
if not mota_linker.is_file():
raise RuntimeError("missing mOTA hybrid linker: %s" % mota_linker)
# The framework builder normally resolves LDSCRIPT_PATH from BoardConfig.
# Set both so this remains deterministic whether PlatformIO invokes this
# pre-script before or after that framework step.
board.update("build.ldscript", str(mota_linker))
env.Replace(LDSCRIPT_PATH=str(mota_linker))
print(
"nRF52 shared-slot bootloader update: %s; fixed 64 KiB mOTA RAM arena; runtime EndF headroom required"
% mota_linker.name
)
+210 -11
View File
@@ -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;
+8 -2
View File
@@ -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;
}
+61
View File
@@ -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.
+8 -2
View File
@@ -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)
+12 -4
View File
@@ -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
+67 -2
View File
@@ -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
+149
View File
@@ -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
+256 -17
View File
@@ -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;
+25 -1
View File
@@ -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
@@ -0,0 +1,14 @@
#pragma once
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
int flash_nrf5x_write(uint32_t address, const void* data, uint32_t length);
void flash_nrf5x_flush(void);
#ifdef __cplusplus
}
#endif
@@ -0,0 +1,6 @@
#pragma once
#include <atomic>
#define __DMB() std::atomic_signal_fence(std::memory_order_seq_cst)
#define __DSB() std::atomic_signal_fence(std::memory_order_seq_cst)
@@ -0,0 +1,251 @@
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>
#if defined(_WIN32)
#include <windows.h>
#else
#include <sys/mman.h>
#endif
// Exercise the production nRF52 store, including its fixed-address flash and
// reset-retained SRAM bridge, with process-local mappings at the target
// addresses. Compile-time test overrides model the application linker bounds
// without relying on platform-specific absolute linker symbols.
static bool g_fail_next_flash_write = false;
static uint32_t g_flash_writes = 0;
extern "C" int flash_nrf5x_write(uint32_t address, const void* data,
uint32_t length) {
++g_flash_writes;
if (g_fail_next_flash_write) {
g_fail_next_flash_write = false;
return -1;
}
std::memcpy(reinterpret_cast<void*>(static_cast<uintptr_t>(address)), data,
length);
return static_cast<int>(length);
}
extern "C" void flash_nrf5x_flush(void) {}
#include "../../../src/helpers/ota/OtaStoreFlashNrf52.cpp"
namespace mesh {
namespace ota {
bool ota_self_firmware(SelfFwInfo& out) {
out = SelfFwInfo();
out.valid = true;
out.image_len = 0x00080000u;
return true;
}
bool mota_parse_manifest(const uint8_t*, uint32_t, MotaManifest&) {
return false;
}
} // namespace ota
} // namespace mesh
#define CHECK(condition) do { if (!(condition)) { \
std::fprintf(stderr, "FAIL line %d: %s\n", __LINE__, #condition); \
std::exit(1); \
} } while (0)
static constexpr uintptr_t FLASH_MAP_START = 0x00020000u;
static constexpr size_t FLASH_MAP_SIZE = 0x000E0000u;
static constexpr uintptr_t RAM_MAP_START = 0x20000000u;
static constexpr size_t RAM_MAP_SIZE = 0x00040000u;
static void map_target_region(uintptr_t address, size_t length) {
#if defined(_WIN32)
void* const mapped = VirtualAlloc(reinterpret_cast<void*>(address), length,
MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE);
#else
void* const mapped = mmap(reinterpret_cast<void*>(address), length,
PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS | MAP_FIXED, -1, 0);
if (mapped == MAP_FAILED) {
std::perror("mmap");
std::exit(1);
}
#endif
CHECK(mapped == reinterpret_cast<void*>(address));
}
static void install_bootloader_capabilities() {
using namespace mesh::ota;
auto* const bootloader = reinterpret_cast<uint8_t*>(
static_cast<uintptr_t>(MOTA_NRF52_BL_START));
std::memset(bootloader, 0xFF,
MOTA_NRF52_BL_END - MOTA_NRF52_BL_START);
std::memcpy(bootloader, OTA_BL_MAGIC, sizeof(OTA_BL_MAGIC));
std::memset(bootloader + 8, 0, 8);
bootloader[8] = 3;
bootloader[10] = 1u << 2;
bootloader[12] = OTA_BL_STORAGE_STAGE_CEILING;
uint8_t* const ram_cap = bootloader + 32;
std::memcpy(ram_cap, MOTA_RAM_CAP_MAGIC, sizeof(MOTA_RAM_CAP_MAGIC));
mota_hybrid_wr16(ram_cap + 8, MOTA_RAM_CAP_ABI);
mota_hybrid_wr16(ram_cap + 10, MOTA_HYBRID_AUTH_LEN);
mota_hybrid_wr32(ram_cap + 12, MOTA_NRF52_HYBRID_RAM_SIZE);
}
static std::vector<uint8_t> patterned_container(uint32_t total) {
using namespace mesh::ota;
std::vector<uint8_t> bytes(total);
for (uint32_t i = 0; i < total; ++i)
bytes[i] = static_cast<uint8_t>((i * 37u + 11u) & 0xFFu);
std::memcpy(bytes.data(), MOTA_MAGIC, sizeof(MOTA_MAGIC));
bytes[4] = static_cast<uint8_t>(total);
bytes[5] = static_cast<uint8_t>(total >> 8);
bytes[6] = static_cast<uint8_t>(total >> 16);
bytes[7] = static_cast<uint8_t>(total >> 24);
return bytes;
}
static void reset_target_memory() {
std::memset(reinterpret_cast<void*>(FLASH_MAP_START), 0xFF, FLASH_MAP_SIZE);
std::memset(reinterpret_cast<void*>(RAM_MAP_START), 0, RAM_MAP_SIZE);
install_bootloader_capabilities();
g_fail_next_flash_write = false;
g_flash_writes = 0;
}
static void full_hybrid_lifecycle_crosses_the_backing_boundary() {
using namespace mesh::ota;
reset_target_memory();
const uint32_t total = MOTA_NRF52_HYBRID_RAM_SIZE +
MOTA_NRF52_FLASH_PAGE + 123u;
const uint32_t payload_offset = 205u;
const uint32_t payload_size = total - payload_offset - 5u;
const auto bytes = patterned_container(total);
uint8_t hash[32];
for (uint8_t i = 0; i < sizeof(hash); ++i) hash[i] = i;
OtaStoreFlashNrf52 store;
CHECK(store.plan_layout(false, 0x81000u, payload_offset, payload_size, false));
CHECK(store.begin(total));
CHECK(store.is_hybrid());
CHECK(store.write_start() == 0x000EB000u);
CHECK(store.flash_len() == 2u * MOTA_NRF52_FLASH_PAGE);
CHECK(store.ram_len() == total - store.flash_len());
CHECK(store.write(0, bytes.data(), total));
std::vector<uint8_t> readback(total);
CHECK(store.read(0, readback.data(), total));
CHECK(readback == bytes);
CHECK(store.finalize());
CHECK(store.data() == nullptr);
CHECK(store.read(0, readback.data(), total));
CHECK(readback == bytes);
auto* const auth = reinterpret_cast<uint8_t*>(MOTA_HYBRID_AUTH_ADDR);
for (uint32_t i = 0; i < MOTA_HYBRID_AUTH_LEN; ++i) CHECK(auth[i] == 0);
CHECK(store.approve_for_application(hash));
CHECK(std::memcmp(reinterpret_cast<void*>(store.write_start() + 8u +
MOTA_OFF_APPROVAL), APPROVAL_YES,
sizeof(APPROVAL_YES)) == 0);
for (uint32_t i = 0; i < MOTA_HYBRID_AUTH_LEN; ++i) CHECK(auth[i] == 0);
CHECK(store.publish_hybrid_handoff());
CHECK(mota_hybrid_auth_valid(auth));
CHECK(mota_hybrid_rd32(auth + 16u) == total);
CHECK(mota_hybrid_rd32(auth + 20u) == store.write_start());
CHECK(mota_hybrid_rd32(auth + 24u) == store.flash_len());
CHECK(mota_hybrid_rd32(auth + 28u) == store.ram_len());
CHECK(std::memcmp(auth + 32u, hash, sizeof(hash)) == 0);
}
static void split_trailer_is_committed_to_flash_and_ram() {
using namespace mesh::ota;
reset_target_memory();
const uint32_t total = MOTA_NRF52_FLASH_PAGE + 1u;
const uint32_t payload_offset = 205u;
const auto bytes = patterned_container(total);
uint8_t hash[32] = {0xA5};
OtaStoreFlashNrf52 store;
CHECK(store.plan_layout(false, 0x80000u, payload_offset,
total - payload_offset - 5u, false));
CHECK(store.begin(total));
CHECK(store.flash_len() == MOTA_NRF52_FLASH_PAGE);
CHECK(store.ram_len() == 1u);
CHECK(store.write(0, bytes.data(), total));
CHECK(store.finalize());
const uint32_t trailer = total - 5u;
CHECK(std::memcmp(reinterpret_cast<void*>(store.write_start() + trailer),
bytes.data() + trailer, 4u) == 0);
CHECK(*reinterpret_cast<uint8_t*>(MOTA_NRF52_HYBRID_RAM_START) ==
bytes.back());
CHECK(store.approve_for_application(hash));
CHECK(store.publish_hybrid_handoff());
}
static void failed_persistence_never_publishes_a_handoff() {
using namespace mesh::ota;
reset_target_memory();
const uint32_t total = MOTA_NRF52_FLASH_PAGE + 1u;
const uint32_t payload_offset = 205u;
const auto bytes = patterned_container(total);
uint8_t hash[32] = {0x33};
OtaStoreFlashNrf52 store;
CHECK(store.plan_layout(false, 0x80000u, payload_offset,
total - payload_offset - 5u, false));
CHECK(store.begin(total));
CHECK(store.write(0, bytes.data(), total));
g_fail_next_flash_write = true;
CHECK(!store.finalize());
CHECK(!store.approve_for_application(hash));
CHECK(!store.publish_hybrid_handoff());
const auto* const auth = reinterpret_cast<const uint8_t*>(
MOTA_HYBRID_AUTH_ADDR);
for (uint32_t i = 0; i < MOTA_HYBRID_AUTH_LEN; ++i) CHECK(auth[i] == 0);
}
static void hybrid_state_is_one_shot_and_not_reopened_after_restart() {
using namespace mesh::ota;
reset_target_memory();
const uint32_t total = MOTA_NRF52_FLASH_PAGE + 1u;
const uint32_t payload_offset = 205u;
const auto bytes = patterned_container(total);
uint8_t hash[32] = {0x77};
OtaStoreFlashNrf52 store;
CHECK(store.plan_layout(false, 0x80000u, payload_offset,
total - payload_offset - 5u, false));
CHECK(store.begin(total));
CHECK(store.write(0, bytes.data(), total));
CHECK(store.approve_for_application(hash));
CHECK(store.publish_hybrid_handoff());
CHECK(mota_hybrid_auth_valid(reinterpret_cast<const uint8_t*>(
MOTA_HYBRID_AUTH_ADDR)));
OtaStoreFlashNrf52 after_restart;
CHECK(!mota_hybrid_auth_valid(reinterpret_cast<const uint8_t*>(
MOTA_HYBRID_AUTH_ADDR)));
CHECK(!after_restart.reopen());
CHECK(after_restart.staged_size() == 0u);
}
int main() {
map_target_region(FLASH_MAP_START, FLASH_MAP_SIZE);
map_target_region(RAM_MAP_START, RAM_MAP_SIZE);
full_hybrid_lifecycle_crosses_the_backing_boundary();
std::puts("PASS: hybrid boundary lifecycle");
split_trailer_is_committed_to_flash_and_ram();
std::puts("PASS: split trailer");
failed_persistence_never_publishes_a_handoff();
std::puts("PASS: persistence failure stays closed");
hybrid_state_is_one_shot_and_not_reopened_after_restart();
std::puts("PASS: restart cannot reopen volatile suffix");
std::puts("4 OtaStoreFlashNrf52 hybrid lifecycle checks passed");
return 0;
}
+283
View File
@@ -0,0 +1,283 @@
#!/usr/bin/env python3
"""Build-wiring and memory-map contracts for the nRF52840 hybrid mOTA linkers."""
import ast
import contextlib
import io
import os
from pathlib import Path
import re
import runpy
import unittest
from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "nrf52_internal_bootloader_link.py"
V6_TARGET = "RAK_3401_repeater_lora_ota_no_external_sensors"
V7_TARGET = "KeepteenLT1_repeater_lora_ota_no_external_sensors"
class FakeBoardConfig:
def __init__(self, values):
self.values = dict(values)
self.updates = []
def get(self, key, default=None):
return self.values.get(key, default)
def update(self, key, value):
self.values[key] = value
self.updates.append((key, value))
class FakeEnvironment:
def __init__(self, project, pioenv, board):
self.values = {
"PROJECT_DIR": str(project),
"PIOENV": pioenv,
}
self.board = board
self.cppdefines = []
self.replacements = {}
def __getitem__(self, key):
return self.values[key]
def BoardConfig(self):
return self.board
def AppendUnique(self, **values):
self.assert_only_key(values, "CPPDEFINES")
for value in values["CPPDEFINES"]:
if value not in self.cppdefines:
self.cppdefines.append(value)
def Replace(self, **values):
self.replacements.update(values)
@staticmethod
def assert_only_key(values, expected):
if set(values) != {expected}:
raise AssertionError(f"unexpected fake-SCons call: {values!r}")
def run_selector(*, pioenv, board_values, project=ROOT, requested=False):
board = FakeBoardConfig(board_values)
environment = FakeEnvironment(project, pioenv, board)
process_environment = (
{"MESHCORE_NRF52_INTERNAL_BOOTLOADER_UPDATE": "1"}
if requested else {}
)
output = io.StringIO()
with mock.patch.dict(os.environ, process_environment, clear=True):
with contextlib.redirect_stdout(output):
runpy.run_path(
str(SCRIPT),
init_globals={
"Import": lambda name: None,
"env": environment,
},
)
return environment, output.getvalue()
def integer_expression(expression):
"""Evaluate the integer +/- expressions used by these MEMORY declarations."""
tree = ast.parse(expression.strip(), mode="eval")
def evaluate(node):
if isinstance(node, ast.Expression):
return evaluate(node.body)
if isinstance(node, ast.Constant) and type(node.value) is int:
return node.value
if isinstance(node, ast.UnaryOp) and isinstance(node.op, (ast.UAdd, ast.USub)):
value = evaluate(node.operand)
return value if isinstance(node.op, ast.UAdd) else -value
if isinstance(node, ast.BinOp) and isinstance(node.op, (ast.Add, ast.Sub)):
left = evaluate(node.left)
right = evaluate(node.right)
return left + right if isinstance(node.op, ast.Add) else left - right
raise ValueError(f"unsupported linker expression: {expression!r}")
return evaluate(tree)
def memory_regions(path):
text = path.read_text(encoding="utf-8")
match = re.search(r"\bMEMORY\s*\{(?P<body>.*?)\}\s*SECTIONS\b", text, re.S)
if not match:
raise AssertionError(f"no MEMORY block in {path}")
regions = {}
pattern = re.compile(
r"^\s*(?P<name>[A-Za-z_]\w*)\s*\([^)]*\)\s*:\s*"
r"ORIGIN\s*=\s*(?P<origin>.*?),\s*LENGTH\s*=\s*(?P<length>.*?)\s*$",
re.M,
)
for region in pattern.finditer(match.group("body")):
regions[region.group("name")] = (
integer_expression(region.group("origin")),
integer_expression(region.group("length")),
)
return regions
def c_integer(path, name):
text = path.read_text(encoding="utf-8")
match = re.search(
rf"\b{re.escape(name)}\s*=\s*(0x[0-9A-Fa-f]+|[0-9]+)u?\s*;",
text,
)
if not match:
raise AssertionError(f"cannot find {name} in {path}")
return int(match.group(1), 0)
class Nrf52InternalBootloaderSelectorTest(unittest.TestCase):
def test_direct_v6_target_selects_hybrid_linker_and_both_features(self):
environment, output = run_selector(
pioenv=V6_TARGET,
board_values={
"build.mcu": "nrf52840",
"build.ldscript": "boards/nrf52840_s140_v6.ld",
},
)
expected = str(ROOT / "boards" / "nrf52840_s140_v6_mota64.ld")
self.assertEqual(
environment.cppdefines,
["OTA_INTERNAL_BOOTLOADER_UPDATE", "OTA_HYBRID_RAM_STORE"],
)
self.assertEqual(environment.board.values["build.ldscript"], expected)
self.assertEqual(environment.replacements, {"LDSCRIPT_PATH": expected})
self.assertIn("nrf52840_s140_v6_mota64.ld", output)
def test_arduino_v7_fallback_selects_v7_hybrid_linker(self):
environment, output = run_selector(
pioenv=V7_TARGET,
board_values={
"build.mcu": "NRF52840",
"build.arduino.ldscript": "nrf52840_s140_v7.ld",
},
)
expected = str(ROOT / "boards" / "nrf52840_s140_v7_mota64.ld")
self.assertEqual(environment.board.updates, [("build.ldscript", expected)])
self.assertEqual(environment.replacements["LDSCRIPT_PATH"], expected)
self.assertIn("nrf52840_s140_v7_mota64.ld", output)
def test_build_sh_environment_flag_activates_a_synthetic_alias(self):
environment, _ = run_selector(
pioenv="Heltec_tower_v2_repeater",
requested=True,
board_values={
"build.mcu": "nrf52840",
"build.ldscript": "boards/nrf52840_s140_v6.ld",
},
)
self.assertIn("OTA_HYBRID_RAM_STORE", environment.cppdefines)
self.assertTrue(environment.replacements["LDSCRIPT_PATH"].endswith("_v6_mota64.ld"))
def test_unselected_nrf52_environment_is_untouched(self):
environment, output = run_selector(
pioenv="RAK_3401_repeater",
board_values={
"build.mcu": "nrf52840",
"build.ldscript": "boards/nrf52840_s140_v6.ld",
},
)
self.assertEqual(environment.cppdefines, [])
self.assertEqual(environment.board.updates, [])
self.assertEqual(environment.replacements, {})
self.assertEqual(output, "")
def test_wrong_mcu_and_nonstandard_linkers_fail_closed(self):
cases = (
("nrf52832", "boards/nrf52840_s140_v6.ld"),
("nrf52840", "boards/nrf52840_s140_v6_extrafs.ld"),
("nrf52840", "boards/custom_nrf52840_s140_v6.ld"),
("nrf52840", ""),
)
for mcu, linker in cases:
with self.subTest(mcu=mcu, linker=linker):
with self.assertRaisesRegex(RuntimeError, "normal S140 v6/v7"):
run_selector(
pioenv=V6_TARGET,
board_values={
"build.mcu": mcu,
"build.ldscript": linker,
},
)
def test_missing_selected_linker_fails_before_mutating_board(self):
board = FakeBoardConfig({
"build.mcu": "nrf52840",
"build.ldscript": "nrf52840_s140_v6.ld",
})
environment = FakeEnvironment(ROOT, V6_TARGET, board)
with mock.patch.object(Path, "is_file", return_value=False):
with mock.patch.dict(os.environ, {}, clear=True):
with self.assertRaisesRegex(RuntimeError, "missing mOTA hybrid linker"):
runpy.run_path(
str(SCRIPT),
init_globals={
"Import": lambda name: None,
"env": environment,
},
)
self.assertEqual(board.updates, [])
self.assertEqual(environment.replacements, {})
class Nrf52HybridLinkerContractTest(unittest.TestCase):
def test_v6_and_v7_maps_match_runtime_handoff_abi(self):
handoff = ROOT / "src" / "helpers" / "ota" / "OtaHybridHandoff.h"
layout = ROOT / "src" / "helpers" / "ota" / "OtaFlashLayout_nrf52.h"
auth_start = c_integer(handoff, "MOTA_HYBRID_AUTH_ADDR")
auth_len = c_integer(handoff, "MOTA_HYBRID_AUTH_LEN")
arena_start = c_integer(layout, "MOTA_NRF52_HYBRID_RAM_START")
arena_len = c_integer(layout, "MOTA_NRF52_HYBRID_RAM_SIZE")
for version, flash_start in (("v6", 0x26000), ("v7", 0x27000)):
with self.subTest(version=version):
hybrid_path = ROOT / "boards" / f"nrf52840_s140_{version}_mota64.ld"
normal_path = ROOT / "boards" / f"nrf52840_s140_{version}.ld"
hybrid = memory_regions(hybrid_path)
normal = memory_regions(normal_path)
self.assertEqual(hybrid["FLASH"], normal["FLASH"])
self.assertEqual(hybrid["FLASH"], (flash_start, 0xED000 - flash_start))
self.assertEqual(hybrid["PERSISTENT_RAM"], (0x20006000, 8 + auth_len))
self.assertEqual(auth_start, 0x20006000 + 8)
self.assertEqual(hybrid["RAM"][0], auth_start + auth_len)
self.assertEqual(hybrid["RAM"][0] + hybrid["RAM"][1], arena_start)
self.assertEqual(hybrid["MOTA_RAM"], (arena_start, arena_len))
self.assertEqual(arena_start + arena_len, 0x20040000)
self.assertEqual(
normal["RAM"][1] - hybrid["RAM"][1],
arena_len + auth_len,
)
def test_linkers_export_and_assert_every_fixed_handoff_boundary(self):
required = (
"__mota_hybrid_handoff_start__ = ADDR(.persistent) + SIZEOF(.persistent);",
"__mota_hybrid_handoff_end__ = ORIGIN(PERSISTENT_RAM) + LENGTH(PERSISTENT_RAM);",
"ASSERT(ADDR(.persistent) == 0x20006000,",
"ASSERT(SIZEOF(.persistent) == 8,",
"ASSERT(__mota_hybrid_handoff_start__ == 0x20006008,",
"ASSERT(__mota_hybrid_handoff_end__ == 0x20006050,",
"ASSERT(__mota_hybrid_handoff_end__ - __mota_hybrid_handoff_start__ == 72,",
"ASSERT(ORIGIN(RAM) == __mota_hybrid_handoff_end__,",
"ASSERT(ORIGIN(RAM) + LENGTH(RAM) == ORIGIN(MOTA_RAM),",
"ASSERT(ORIGIN(MOTA_RAM) + LENGTH(MOTA_RAM) == 0x20040000,",
".mota_ram (NOLOAD)",
'INCLUDE "nrf52_common.ld"',
)
for version in ("v6", "v7"):
with self.subTest(version=version):
path = ROOT / "boards" / f"nrf52840_s140_{version}_mota64.ld"
text = path.read_text(encoding="utf-8")
for contract in required:
self.assertIn(contract, text)
if __name__ == "__main__":
unittest.main()
+91 -1
View File
@@ -39,7 +39,7 @@ TEST(OtaBootResult, AcceptsOnlyOtafixApplyDiagnostics) {
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xB0), 0xB0);
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xB8), 0xB8);
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xBC), 0xBC);
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xBD), 0xBD); // SD authorization failure
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xBD), 0xBD); // retained-source authorization/handoff failure
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xC0), 0xC0);
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xC8), 0xC8);
EXPECT_EQ(ota_nrf52_boot_result_or_zero(0xCF), 0xCF);
@@ -673,6 +673,96 @@ TEST(OtaBootPackage, CapabilityScannerRejectsAnOtherwiseValidUnalignedMarker) {
EXPECT_FALSE(ota_bl_caps_scan_aligned(image, sizeof(image), true, qspi_profile).present);
}
TEST(OtaHybridCapability, RequiresOneExactAlignedMOTARAMARecord) {
uint8_t marker[16] = {0};
memcpy(marker, MOTA_RAM_CAP_MAGIC, sizeof(MOTA_RAM_CAP_MAGIC));
mota_hybrid_wr16(marker + 8u, MOTA_RAM_CAP_ABI);
mota_hybrid_wr16(marker + 10u, MOTA_HYBRID_AUTH_LEN);
mota_hybrid_wr32(marker + 12u, MOTA_NRF52_HYBRID_RAM_SIZE);
OtaRamCaps parsed;
ASSERT_TRUE(ota_ram_caps_marker_parse(marker, parsed));
EXPECT_TRUE(ota_bootloader_supports_hybrid(parsed));
EXPECT_EQ(parsed.abi, MOTA_RAM_CAP_ABI);
EXPECT_EQ(parsed.record_len, MOTA_HYBRID_AUTH_LEN);
EXPECT_EQ(parsed.arena_size, MOTA_NRF52_HYBRID_RAM_SIZE);
uint8_t image[64];
memset(image, 0xFF, sizeof(image));
memcpy(image + 1u, marker, sizeof(marker));
EXPECT_FALSE(ota_ram_caps_scan_aligned(image, sizeof(image)).present);
memset(image, 0xFF, sizeof(image));
memcpy(image + 4u, marker, sizeof(marker));
EXPECT_TRUE(ota_bootloader_supports_hybrid(
ota_ram_caps_scan_aligned(image, sizeof(image))));
// Two valid capabilities are ambiguous; a malformed decoy is ignored.
memcpy(image + 24u, marker, sizeof(marker));
EXPECT_FALSE(ota_ram_caps_scan_aligned(image, sizeof(image)).present);
image[24u + 12u] ^= 1u;
EXPECT_TRUE(ota_ram_caps_scan_aligned(image, sizeof(image)).present);
uint8_t bad[sizeof(marker)];
memcpy(bad, marker, sizeof(bad));
bad[8] = 2u;
EXPECT_FALSE(ota_ram_caps_marker_parse(bad, parsed));
memcpy(bad, marker, sizeof(bad));
bad[10] = (uint8_t)(MOTA_HYBRID_AUTH_LEN - 1u);
EXPECT_FALSE(ota_ram_caps_marker_parse(bad, parsed));
memcpy(bad, marker, sizeof(bad));
bad[12] ^= 1u;
EXPECT_FALSE(ota_ram_caps_marker_parse(bad, parsed));
}
TEST(OtaHybridHandoff, EncodesExactGeometryAndRejectsCorruption) {
const uint32_t total = 17u * MOTA_NRF52_FLASH_PAGE;
uint32_t start = 0, flash = 0, ram = 0;
ASSERT_TRUE(mota_nrf52_hybrid_stage_plan(
total, MOTA_NRF52_APP_BASE_S140_V6,
MOTA_NRF52_APP_BASE_S140_V6 + 512u * 1024u,
MOTA_NRF52_STAGE_CEILING_EXPANDED, start, flash, ram));
ASSERT_EQ(flash, MOTA_NRF52_FLASH_PAGE);
ASSERT_EQ(ram, MOTA_NRF52_HYBRID_RAM_SIZE);
uint8_t hash[32];
for (uint8_t i = 0; i < sizeof(hash); ++i) hash[i] = (uint8_t)(i * 7u);
uint8_t record[MOTA_HYBRID_AUTH_LEN];
ASSERT_TRUE(mota_hybrid_auth_encode(
record, total, start, flash, ram, hash));
EXPECT_TRUE(mota_hybrid_auth_valid(record));
EXPECT_EQ(memcmp(record, MOTA_HYBRID_AUTH_MAGIC, 8u), 0);
EXPECT_EQ(mota_hybrid_rd16(record + 8u), MOTA_HYBRID_AUTH_VERSION);
EXPECT_EQ(mota_hybrid_rd16(record + 10u), MOTA_HYBRID_AUTH_LEN);
EXPECT_EQ(record[12], MOTA_HYBRID_AUTH_PURPOSE_APP);
EXPECT_EQ(record[13], MOTA_HYBRID_AUTH_FORMAT_APP);
EXPECT_EQ(mota_hybrid_rd32(record + 16u), total);
EXPECT_EQ(mota_hybrid_rd32(record + 20u), start);
EXPECT_EQ(mota_hybrid_rd32(record + 24u), flash);
EXPECT_EQ(mota_hybrid_rd32(record + 28u), ram);
EXPECT_EQ(memcmp(record + 32u, hash, sizeof(hash)), 0);
const uint32_t crc = mota_hybrid_rd32(record + 64u);
EXPECT_EQ(crc, mota_hybrid_crc32(record, 64u));
EXPECT_EQ(mota_hybrid_rd32(record + 68u), ~crc);
record[32] ^= 1u;
EXPECT_FALSE(mota_hybrid_auth_valid(record));
record[32] ^= 1u;
record[68] ^= 1u;
EXPECT_FALSE(mota_hybrid_auth_valid(record));
// The descriptor accepts only the frozen minimum flash charge and ED000
// ceiling. Legacy-D4000 or overcharged splits cannot be authorized.
EXPECT_FALSE(mota_hybrid_auth_encode(
record, total, start - MOTA_NRF52_FLASH_PAGE,
flash + MOTA_NRF52_FLASH_PAGE,
ram - MOTA_NRF52_FLASH_PAGE, hash));
EXPECT_FALSE(mota_hybrid_auth_encode(
record, total,
MOTA_NRF52_STAGE_CEILING_LEGACY - MOTA_NRF52_FLASH_PAGE,
MOTA_NRF52_FLASH_PAGE, ram, hash));
}
TEST(OtaBootPackage, LegacyAndCurrentBootloadersHaveSeparateCapabilityViews) {
const uint8_t internal_profile = OTA_BL_PROFILE_INTERNAL_BOOT_UPDATE;
const uint8_t legacy_marker[16] = {
+54
View File
@@ -183,6 +183,60 @@ TEST(OtaFlashPlan, ExpandedCeilingAddsExactly100KiBForV6AndV7) {
EXPECT_EQ(start + CAP_V7_EXPANDED, EXPANDED);
}
TEST(OtaFlashPlan, HybridUsesTheFrozenFlashChargeAndRamSuffix) {
uint32_t start = 0, flash = 0, ram = 0;
const uint32_t page = MOTA_NRF52_FLASH_PAGE;
ASSERT_TRUE(mota_nrf52_hybrid_stage_plan(
page + 1u, APP_V6, APP_END_V6, EXPANDED, start, flash, ram));
EXPECT_EQ(start, EXPANDED - page);
EXPECT_EQ(flash, page);
EXPECT_EQ(ram, 1u);
ASSERT_TRUE(mota_nrf52_hybrid_stage_plan(
17u * page, APP_V6, APP_END_V6, EXPANDED, start, flash, ram));
EXPECT_EQ(start, EXPANDED - page);
EXPECT_EQ(flash, page);
EXPECT_EQ(ram, MOTA_NRF52_HYBRID_RAM_SIZE);
ASSERT_TRUE(mota_nrf52_hybrid_stage_plan(
17u * page + 1u, APP_V6, APP_END_V6,
EXPANDED, start, flash, ram));
EXPECT_EQ(start, EXPANDED - 2u * page);
EXPECT_EQ(flash, 2u * page);
EXPECT_EQ(ram, 15u * page + 1u);
ASSERT_TRUE(mota_nrf52_hybrid_stage_plan(
17u * page, APP_V7, APP_END_V7,
EXPANDED, start, flash, ram));
EXPECT_EQ(start, EXPANDED - page);
EXPECT_EQ(flash, page);
EXPECT_EQ(ram, MOTA_NRF52_HYBRID_RAM_SIZE);
}
TEST(OtaFlashPlan, HybridFailsClosedOutsideItsExactProfile) {
uint32_t start = 0xAAAAAAAAu, flash = 0xBBBBBBBBu, ram = 0xCCCCCCCCu;
const uint32_t page = MOTA_NRF52_FLASH_PAGE;
EXPECT_FALSE(mota_nrf52_hybrid_stage_plan(
MOTA_NRF52_CONTAINER_MIN_SIZE, APP_V6, APP_END_V6,
EXPANDED, start, flash, ram));
EXPECT_FALSE(mota_nrf52_hybrid_stage_plan(
page, APP_V6, APP_END_V6, EXPANDED, start, flash, ram));
EXPECT_FALSE(mota_nrf52_hybrid_stage_plan(
17u * page, APP_V6, APP_END_V6, LEGACY, start, flash, ram));
EXPECT_FALSE(mota_nrf52_hybrid_stage_plan(
17u * page + 1u, APP_V6, EXPANDED - page,
EXPANDED, start, flash, ram));
EXPECT_FALSE(mota_nrf52_hybrid_stage_plan(
17u * page, 0x00028000u, 0x00028000u,
EXPANDED, start, flash, ram));
EXPECT_FALSE(mota_nrf52_hybrid_stage_plan(
UINT32_MAX, APP_V6, APP_END_V6,
EXPANDED, start, flash, ram));
EXPECT_EQ(start, 0xAAAAAAAAu);
EXPECT_EQ(flash, 0xBBBBBBBBu);
EXPECT_EQ(ram, 0xCCCCCCCCu);
}
TEST(OtaFlashPlan, RejectsAppOutsideSelectedRegion) {
uint32_t start = 0x1234ABCD;
EXPECT_FALSE(mota_nrf52_stage_plan(4096, APP_V6, LEGACY + 1, LEGACY, start));
+26
View File
@@ -61,6 +61,32 @@ class OtaDevRebootGuardContractTest(unittest.TestCase):
reboot_at = maintenance.index("ota::ota_reboot_to_apply()")
self.assertLess(guard_at, reboot_at)
def test_dev_clear_cannot_mutate_an_armed_update(self):
cli = (ROOT / "src/helpers/ota/OtaCli.cpp").read_text()
dev = function_body(
cli,
"static bool handle_dev(const char* d, char* reply, OtaContext& c) {",
)
clear_at = dev.index('strncmp(d, "clear", 5)')
clear = dev[
clear_at : dev.index(
'} else {\n strcpy(reply, "ota dev:', clear_at
)
]
guard_at = clear.index("if (c.apply_pending)")
mutations = (
"c.manager.clear_primary()",
"c.releaseServeBuffer()",
"c.manager.reset_session()",
"c.fetch_store.discard()",
)
for mutation in mutations:
self.assertLess(guard_at, clear.index(mutation), mutation)
guard = clear[guard_at : clear.index("const bool was_folder")]
self.assertIn('strcpy(reply, "ERR update is armed; reboot is pending")', guard)
self.assertIn("return true", guard)
if __name__ == "__main__":
unittest.main()
+61
View File
@@ -0,0 +1,61 @@
#!/usr/bin/env python3
"""Execute the real nRF52 flash/SRAM hybrid staging store on host mappings."""
from pathlib import Path
import shutil
import subprocess
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[1]
FIXTURE = ROOT / "test/fixtures/ota_store_flash_nrf52_hybrid"
class OtaStoreFlashNrf52HybridTest(unittest.TestCase):
def test_real_store_hybrid_lifecycle(self):
compiler = shutil.which("g++") or shutil.which("clang++")
if compiler is None:
self.skipTest("a host C++17 compiler is required")
# Build outside the checkout. On Windows an application-control policy
# may deny linker output or execution from newly-created repository
# subdirectories, and a denied executable can leave that directory
# locked during TemporaryDirectory cleanup.
with tempfile.TemporaryDirectory(prefix="meshcore-ota-hybrid-") as directory:
binary = Path(directory) / "ota-store-hybrid.exe"
compiled = subprocess.run(
[
compiler,
"-std=c++17",
"-Wall",
"-Wextra",
"-Werror",
"-DNRF52_PLATFORM=1",
"-DOTA_FLASH_STORE=1",
"-DOTA_HYBRID_RAM_STORE=1",
"-DMOTA_NRF52_TEST_APP_BASE=0x00026000u",
"-DMOTA_NRF52_TEST_LAYOUT_STAGE_CEILING=0x000ED000u",
f"-I{FIXTURE / 'mocks'}",
f"-I{ROOT / 'src'}",
str(FIXTURE / "test_ota_store_flash_nrf52_hybrid.cpp"),
"-o",
str(binary),
],
capture_output=True,
text=True,
timeout=60,
)
self.assertEqual(compiled.returncode, 0, compiled.stdout + compiled.stderr)
checked = subprocess.run(
[str(binary)], capture_output=True, text=True, timeout=10
)
self.assertEqual(checked.returncode, 0, checked.stdout + checked.stderr)
self.assertIn(
"4 OtaStoreFlashNrf52 hybrid lifecycle checks passed",
checked.stdout,
)
self.assertEqual(checked.stdout.count("PASS:"), 4)
if __name__ == "__main__":
unittest.main()
+43 -1
View File
@@ -73,6 +73,7 @@ NRF52_APP_END = 0x000ED000
NRF52_BOOT_CONTAINER_SIZE = 41330
NRF52_SHARED_BOOT_STAGE_START = 0x000E2000
NRF52_FLASH_PAGE = 4096
NRF52_HYBRID_RAM_SIZE = 65536
# A validated nRF52 firmware carries this record immediately before EndF. It lets an offline packager
# derive the actual app base and staging ceiling from the built artifact, without a board-name allowlist.
@@ -84,6 +85,7 @@ NRF52_LAYOUT_FLAG_SD = 0x01
NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS = 0x02
NRF52_LAYOUT_FLAG_QSPI = 0x04
NRF52_LAYOUT_FLAG_BOOTLOADER_SCRATCH = 0x08
NRF52_LAYOUT_FLAG_HYBRID_RAM = 0x10
XIAO_BOOT_BOARD_ID_BASE = 0x28860044
XIAO_BOOT_BOARD_ID_SENSE = 0x28860045
@@ -302,6 +304,10 @@ class Nrf52Layout:
def bootloader_scratch(self) -> bool:
return bool(self.flags & NRF52_LAYOUT_FLAG_BOOTLOADER_SCRATCH)
@property
def hybrid_ram(self) -> bool:
return bool(self.flags & NRF52_LAYOUT_FLAG_HYBRID_RAM)
def nrf52_stage_ceiling_for_layout(linked_app_end: int, uses_internal_extrafs: bool) -> int:
"""Select a safe staging ceiling from linker geometry and actual secondary-storage type."""
if uses_internal_extrafs:
@@ -311,6 +317,33 @@ def nrf52_stage_ceiling_for_layout(linked_app_end: int, uses_internal_extrafs: b
return NRF52_EXTRAFS_START
def nrf52_hybrid_stage_plan(total_size: int, app_base: int, app_end: int,
stage_ceiling: int) -> Optional[Tuple[int, int, int]]:
"""Return the frozen (flash_start, flash_len, ram_len) split, or None.
Keep this byte-for-byte equivalent to
mota_nrf52_hybrid_stage_plan() in OtaFlashLayout_nrf52.h. Hybrid is an
expanded ED000-only profile; at least one complete flash page contains the
header, manifest, leaves, and APRV word.
"""
if (stage_ceiling != NRF52_APP_END or
app_base not in (NRF52_APP_BASE_S140_V6,
NRF52_APP_BASE_S140_V7) or
not (app_base <= app_end <= stage_ceiling) or
total_size < 8 + 197 + 5 or total_size > 0xFFFFFFFF):
return None
flash_needed = max(NRF52_FLASH_PAGE,
max(0, total_size - NRF52_HYBRID_RAM_SIZE))
flash_len = ((flash_needed + NRF52_FLASH_PAGE - 1) //
NRF52_FLASH_PAGE) * NRF52_FLASH_PAGE
if flash_len >= total_size or flash_len > stage_ceiling - app_end:
return None
ram_len = total_size - flash_len
if not (0 < ram_len <= NRF52_HYBRID_RAM_SIZE):
return None
return stage_ceiling - flash_len, flash_len, ram_len
def build_nrf52_layout(layout: Nrf52Layout) -> bytes:
if layout.app_base not in (NRF52_APP_BASE_S140_V6, NRF52_APP_BASE_S140_V7):
raise ValueError(f"unsupported nRF52 app base 0x{layout.app_base:X}")
@@ -322,13 +355,22 @@ def build_nrf52_layout(layout: Nrf52Layout) -> bytes:
if not (layout.app_base < layout.linked_app_end <= NRF52_APP_END):
raise ValueError("invalid nRF52 app region")
known_flags = (NRF52_LAYOUT_FLAG_SD | NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS |
NRF52_LAYOUT_FLAG_QSPI | NRF52_LAYOUT_FLAG_BOOTLOADER_SCRATCH)
NRF52_LAYOUT_FLAG_QSPI | NRF52_LAYOUT_FLAG_BOOTLOADER_SCRATCH |
NRF52_LAYOUT_FLAG_HYBRID_RAM)
if layout.flags & ~known_flags:
raise ValueError(f"unsupported nRF52 layout flags 0x{layout.flags:X}")
if layout.sd_backed and layout.qspi_backed:
raise ValueError("nRF52 layout cannot use both SD and QSPI staging")
if layout.external_backed and layout.flags & NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS:
raise ValueError("nRF52 external staging cannot also reserve internal ExtraFS")
if layout.hybrid_ram:
if (layout.external_backed or
layout.flags & NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS):
raise ValueError("nRF52 hybrid RAM staging requires internal flash without ExtraFS")
if (layout.linked_app_end != NRF52_APP_END or
layout.stage_ceiling != NRF52_APP_END):
raise ValueError(
"nRF52 hybrid RAM staging requires the exact 0xED000 internal-only profile")
if layout.bootloader_scratch:
qspi_shape = (layout.qspi_backed and not layout.sd_backed and
layout.linked_app_end == NRF52_BOOT_SCRATCH_START)
+59 -1
View File
@@ -91,6 +91,57 @@ def _cppdef(name): # value of a -D<name>=<value>
return None
def _normalized_linker_path(value, project_dir):
"""Resolve one SCons linker-path value without guessing among candidates."""
if isinstance(value, (list, tuple)):
if len(value) != 1:
return ""
value = value[0]
if hasattr(value, "get_abspath"):
value = value.get_abspath()
raw = str(value or "").strip().strip('"').strip("'")
if not raw:
return ""
if not os.path.isabs(raw):
raw = os.path.join(str(project_dir), raw)
return os.path.normcase(os.path.abspath(raw))
def _validate_nrf52_hybrid_profile(app_start, linked_app_end,
hybrid_ram, internal_bootloader_update,
active_linker, configured_linker,
project_dir):
"""Fail closed unless both macros and the dedicated SRAM linker agree."""
if hybrid_ram != internal_bootloader_update:
raise RuntimeError(
"nRF52 hybrid RAM staging and internal bootloader update must be enabled together")
if not hybrid_ram:
return
suffix_by_base = {
ml.NRF52_APP_BASE_S140_V6: "v6",
ml.NRF52_APP_BASE_S140_V7: "v7",
}
suffix = suffix_by_base.get(app_start)
if suffix is None or linked_app_end != ml.NRF52_APP_END:
raise RuntimeError(
"nRF52 hybrid RAM staging requires the exact S140 v6/v7 0xED000 profile")
expected = os.path.join(
str(project_dir), "boards", f"nrf52840_s140_{suffix}_mota64.ld")
expected = _normalized_linker_path(expected, project_dir)
if not expected or not os.path.isfile(expected):
raise RuntimeError("nRF52 hybrid RAM linker is missing")
for label, value in (("active", active_linker),
("configured", configured_linker)):
actual = _normalized_linker_path(value, project_dir)
if actual != expected:
shown = str(value or "none")
raise RuntimeError(
f"nRF52 hybrid RAM staging requires {expected}; "
f"{label} linker is {shown}")
def _board_maximum_size(build_env):
"""Return PlatformIO's resolved application-size limit, including per-env overrides."""
try:
@@ -215,8 +266,14 @@ def _append_endf_hex(source, target, env): # Intel-HEX path (nRF52: app f
sd_backed = _cppdef("OTA_SD_STORE") is not None
qspi_backed = _cppdef("OTA_QSPI_STORE") is not None
qspi_bootloader_update = _cppdef("OTA_QSPI_BOOTLOADER_UPDATE") is not None
hybrid_ram = _cppdef("OTA_HYBRID_RAM_STORE") is not None
internal_bootloader_update = _cppdef("OTA_INTERNAL_BOOTLOADER_UPDATE") is not None
sd_bootloader_update = _cppdef("OTA_SD_BOOTLOADER_UPDATE") is not None
board = env.BoardConfig()
_validate_nrf52_hybrid_profile(
app_start, linked_app_end, hybrid_ram, internal_bootloader_update,
env.get("LDSCRIPT_PATH", ""), board.get("build.ldscript", ""),
env["PROJECT_DIR"])
if sd_backed and qspi_backed:
raise RuntimeError("nRF52 build cannot enable both SD and QSPI OTA stores")
if qspi_backed and _cppdef("QSPIFLASH") is not None:
@@ -242,7 +299,8 @@ def _append_endf_hex(source, target, env): # Intel-HEX path (nRF52: app f
layout_flags = ((ml.NRF52_LAYOUT_FLAG_SD if sd_backed else 0) |
(ml.NRF52_LAYOUT_FLAG_QSPI if qspi_backed else 0) |
(ml.NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS if internal_extrafs else 0) |
(ml.NRF52_LAYOUT_FLAG_BOOTLOADER_SCRATCH if qspi_bootloader_update else 0))
(ml.NRF52_LAYOUT_FLAG_BOOTLOADER_SCRATCH if qspi_bootloader_update else 0) |
(ml.NRF52_LAYOUT_FLAG_HYBRID_RAM if hybrid_ram else 0))
layout = ml.Nrf52Layout(app_start, linked_app_end, stage_ceiling, layout_flags)
body = ml.ensure_nrf52_layout(raw_body, layout)
ident = _firmware_ident()
+148 -5
View File
@@ -60,6 +60,13 @@ def test_internal_bootloader_target_wiring_is_central_and_not_duplicated():
}
assert gat_targets <= set(inventory)
assert not any("GAT562_Mesh_Watch13" in target for target in inventory)
assert not any("xiao" in target.lower() for target in inventory)
assert not any("sdcard" in target.lower() for target in inventory)
assert {
"Heltec_t096_repeater_lora_ota_no_external_sensors",
"Heltec_t114_repeater_lora_ota_no_external_sensors",
"RAK_3401_repeater_lora_ota_no_external_sensors",
} <= set(inventory)
target_header = (root / "src/helpers/ota/OtaTargets.h").read_text(encoding="utf-8")
for target in inventory:
@@ -75,6 +82,83 @@ def test_internal_bootloader_target_wiring_is_central_and_not_duplicated():
assert "scripts/nrf52_internal_bootloader_link.py" not in recipe
assert "export MESHCORE_NRF52_INTERNAL_BOOTLOADER_UPDATE=1" in recipe
selector = (root / "scripts/nrf52_internal_bootloader_link.py").read_text(
encoding="utf-8")
assert '"OTA_HYBRID_RAM_STORE"' in selector
assert "nrf52840_s140_%s_mota64.ld" in selector
assert "env[\"PIOENV\"] in direct_targets" in selector
for version, app_base in (("v6", "0x26000"), ("v7", "0x27000")):
linker = (root / f"boards/nrf52840_s140_{version}_mota64.ld").read_text(
encoding="utf-8")
assert f"ORIGIN = {app_base}" in linker
assert "PERSISTENT_RAM (rwx) : ORIGIN = 0x20006000, LENGTH = 80" in linker
assert "MOTA_RAM (rwx) : ORIGIN = 0x20030000, LENGTH = 0x10000" in linker
assert ".mota_ram (NOLOAD)" in linker
ordinary = (root / f"boards/nrf52840_s140_{version}.ld").read_text(
encoding="utf-8")
assert "MOTA_RAM" not in ordinary
# Explicit external-storage and ExtraFS profiles remain on their existing
# linkers and are absent from the exact activation inventory.
tower = (root / "variants/heltec_tower_v2/platformio.ini").read_text(
encoding="utf-8")
assert "[env:Heltec_tower_v2_sdcard_repeater_lora_ota_no_external_sensors]" in tower
assert "board_build.ldscript = boards/nrf52840_s140_v6_sd_ota.ld" in tower
assert "board_build.ldscript = boards/nrf52840_s140_v6_extrafs.ld" in tower
xiao = (root / "variants/xiao_nrf52/platformio.ini").read_text(
encoding="utf-8")
assert "board_build.ldscript = boards/nrf52840_s140_v7.ld" in xiao
def test_nrf52_hybrid_endf_profile_fails_closed():
root = Path(__file__).resolve().parents[2]
script = root / "tools/mota/pio_endf.py"
spec = importlib.util.spec_from_file_location("pio_endf_policy_test", script)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
module.Import = lambda _name: None
module.env = {"PROJECT_DIR": str(root), "CPPDEFINES": []}
spec.loader.exec_module(module)
validate = module._validate_nrf52_hybrid_profile
correct = root / "boards/nrf52840_s140_v6_mota64.ld"
validate(ml.NRF52_APP_BASE_S140_V6, ml.NRF52_APP_END,
True, True, correct, correct, root)
correct_v7 = root / "boards/nrf52840_s140_v7_mota64.ld"
validate(ml.NRF52_APP_BASE_S140_V7, ml.NRF52_APP_END,
True, True, [correct_v7], correct_v7, root)
# Ordinary profiles do not need a linker proof.
validate(ml.NRF52_APP_BASE_S140_V6, ml.NRF52_EXTRAFS_START,
False, False, "", "", root)
def rejected(*args):
try:
validate(*args)
assert False, "invalid nRF52 hybrid build profile accepted"
except RuntimeError as exc:
return str(exc)
assert "enabled together" in rejected(
ml.NRF52_APP_BASE_S140_V6, ml.NRF52_APP_END,
True, False, correct, correct, root)
assert "enabled together" in rejected(
ml.NRF52_APP_BASE_S140_V6, ml.NRF52_APP_END,
False, True, correct, correct, root)
assert "exact S140" in rejected(
ml.NRF52_APP_BASE_S140_V6, ml.NRF52_EXTRAFS_START,
True, True, correct, correct, root)
assert "exact S140" in rejected(
0x00028000, ml.NRF52_APP_END,
True, True, correct, correct, root)
ordinary = root / "boards/nrf52840_s140_v6.ld"
assert "active linker" in rejected(
ml.NRF52_APP_BASE_S140_V6, ml.NRF52_APP_END,
True, True, ordinary, correct, root)
assert "configured linker" in rejected(
ml.NRF52_APP_BASE_S140_V6, ml.NRF52_APP_END,
True, True, correct, ordinary, root)
def test_full_esp32_profile_unifies_usb_logging_and_wifi_mqtt():
root = Path(__file__).resolve().parents[2]
@@ -437,10 +521,9 @@ def test_measured_full_companion_promotions_are_exact_and_bounded():
assert "+<helpers/esp32/SerialWifiInterface.cpp>" in profile
assert "meshadventurer_sx1262_companion_radio_full" in profile
assert "meshadventurer_sx1268_companion_radio_full" in profile
assert "-DMAX_CONTACTS=160" in profile
assert "-DMAX_GROUP_CHANNELS=30" in profile
assert "-DOFFLINE_QUEUE_SIZE=64" in profile
assert "160 contacts, 30 channels, and 64 queued frames" in profile
assert ("-DMAX_CONTACTS=100 -DMAX_GROUP_CHANNELS=30 "
"-DOFFLINE_QUEUE_SIZE=16") in profile
assert "100 contacts, 30 channels, and 16 queued frames" in profile
def test_full_companion_wireless_startup_and_psram_contacts_are_resilient():
@@ -1083,6 +1166,19 @@ def test_nrf52_layout_record_roundtrip_and_policy():
ml.ensure_nrf52_layout(_fw(9, 2048), shared_internal))
assert ml.parse_nrf52_layout(shared_internal_image) == shared_internal
assert not shared_internal.bootloader_scratch
hybrid = ml.Nrf52Layout(
ml.NRF52_APP_BASE_S140_V6, ml.NRF52_APP_END,
ml.NRF52_APP_END, ml.NRF52_LAYOUT_FLAG_HYBRID_RAM)
hybrid_image, _ = ml.ensure_endf(
ml.ensure_nrf52_layout(_fw(10, 2048), hybrid))
assert ml.parse_nrf52_layout(hybrid_image) == hybrid
assert hybrid.hybrid_ram and not hybrid.external_backed
assert ml.build_nrf52_layout(hybrid) == bytes.fromhex(
"6d4f54414c617931011018000060020000d00e0000d00e00")
hybrid_v7 = ml.Nrf52Layout(
ml.NRF52_APP_BASE_S140_V7, ml.NRF52_APP_END,
ml.NRF52_APP_END, ml.NRF52_LAYOUT_FLAG_HYBRID_RAM)
assert ml.build_nrf52_layout(hybrid_v7)
assert ((ml.NRF52_APP_END - ml.NRF52_BOOT_CONTAINER_SIZE) &
~(ml.NRF52_FLASH_PAGE - 1)) == ml.NRF52_SHARED_BOOT_STAGE_START
try:
@@ -1095,7 +1191,10 @@ def test_nrf52_layout_record_roundtrip_and_policy():
for flags in (
ml.NRF52_LAYOUT_FLAG_SD | ml.NRF52_LAYOUT_FLAG_QSPI,
ml.NRF52_LAYOUT_FLAG_QSPI | ml.NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS,
0x10,
ml.NRF52_LAYOUT_FLAG_HYBRID_RAM | ml.NRF52_LAYOUT_FLAG_SD,
ml.NRF52_LAYOUT_FLAG_HYBRID_RAM | ml.NRF52_LAYOUT_FLAG_QSPI,
ml.NRF52_LAYOUT_FLAG_HYBRID_RAM |
ml.NRF52_LAYOUT_FLAG_INTERNAL_EXTRAFS,
):
try:
ml.build_nrf52_layout(ml.Nrf52Layout(
@@ -1105,6 +1204,50 @@ def test_nrf52_layout_record_roundtrip_and_policy():
except ValueError:
pass
# The flag is also a promise that the application used the dedicated
# mota64 RAM linker. A shorter flash linker with no ExtraFS mounted may
# ordinarily reclaim ED000 for staging, but it cannot reserve SRAM.
for linked_end, ceiling in (
(ml.NRF52_EXTRAFS_START, ml.NRF52_APP_END),
(ml.NRF52_APP_END, ml.NRF52_EXTRAFS_START),
):
try:
ml.build_nrf52_layout(ml.Nrf52Layout(
ml.NRF52_APP_BASE_S140_V6, linked_end, ceiling,
ml.NRF52_LAYOUT_FLAG_HYBRID_RAM))
assert False, "non-mota64 hybrid layout accepted"
except ValueError:
pass
def test_nrf52_hybrid_stage_plan_matches_frozen_geometry():
app = ml.NRF52_APP_BASE_S140_V6
app_end = app + 512 * 1024
page = ml.NRF52_FLASH_PAGE
ceiling = ml.NRF52_APP_END
assert ml.nrf52_hybrid_stage_plan(8 + 197 + 5, app, app_end, ceiling) is None
assert ml.nrf52_hybrid_stage_plan(page, app, app_end, ceiling) is None
assert ml.nrf52_hybrid_stage_plan(page + 1, app, app_end, ceiling) == (
ceiling - page, page, 1)
assert ml.nrf52_hybrid_stage_plan(17 * page, app, app_end, ceiling) == (
ceiling - page, page, 16 * page)
assert ml.nrf52_hybrid_stage_plan(17 * page + 1, app, app_end, ceiling) == (
ceiling - 2 * page, 2 * page, 15 * page + 1)
# Hybrid is never admitted against the D4000/ExtraFS ceiling, and exact
# runtime app headroom must fit the charged flash prefix.
assert ml.nrf52_hybrid_stage_plan(
17 * page, app, app_end, ml.NRF52_EXTRAFS_START) is None
assert ml.nrf52_hybrid_stage_plan(
17 * page + 1, app, ceiling - page, ceiling) is None
assert ml.nrf52_hybrid_stage_plan(
17 * page, 0x00028000, app_end, ceiling) is None
v7 = ml.NRF52_APP_BASE_S140_V7
assert ml.nrf52_hybrid_stage_plan(
17 * page, v7, v7 + 512 * 1024, ceiling) == (
ceiling - page, page, 16 * page)
def _write_boot_continuity(image, offset, *, version=0x0117010D,
family=ml.BOOT_CONTINUITY_FAMILY_S140,