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