Add external QSPI staging for nRF52 OTA

This commit is contained in:
mikecarper committed 2026-08-20 17:51:52 -07:00
1 parent c98dfb6fe1
commit e287a1ffa2
40 files changed
+1671 -157

No files matched your search

+64 -17
View File
@@ -29,6 +29,9 @@
#if defined(OTA_SD_STORE)
#include "OtaStoreSdNrf52.h"
#endif
#if defined(OTA_QSPI_STORE)
#include "OtaStoreQspiNrf52.h"
#endif
#endif
namespace mesh {
@@ -442,7 +445,9 @@ bool ota_apply_detools_mota(const uint8_t*, uint32_t, const SignerAllowlist&, Ap
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_FLASH_STORE)
#if defined(OTA_QSPI_STORE)
stage_handoff = GPREGRET2_OTA_STAGE_QSPI;
#elif defined(OTA_FLASH_STORE)
if (ota_nrf52_effective_stage_ceiling() == MOTA_NRF52_STAGE_CEILING_EXPANDED)
stage_handoff = GPREGRET2_OTA_STAGE_EXPANDED;
#endif
@@ -589,28 +594,33 @@ 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_SD_STORE)
bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow,
ApplyState& st, char* msg) {
#if defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE)
static const size_t NRF52_APPLY_MSG_CAP = 96;
template <typename Store>
static bool ota_apply_mota_nrf52_external(Store& store, const SignerAllowlist& allow,
uint8_t storage_flag, const char* storage_name,
ApplyState& st, char* msg) {
st = ApplyState();
uint8_t hdr[8], manifest[MOTA_MFL];
uint32_t total = store.staged_size();
if (total < 8 + MOTA_MFL + 5 || !store.read(0, hdr, sizeof(hdr)) ||
memcmp(hdr, MOTA_MAGIC, 4) != 0 || rd_u32le(hdr + 4) != total ||
!store.read(8, manifest, sizeof(manifest))) {
strcpy(msg, "SD container parse failed");
snprintf(msg, NRF52_APPLY_MSG_CAP, "%s container parse failed", storage_name);
return false;
}
MotaManifest m;
if (!mota_parse_manifest(manifest, sizeof(manifest), m)) {
strcpy(msg, "SD manifest parse failed");
snprintf(msg, NRF52_APPLY_MSG_CAP, "%s manifest parse failed", storage_name);
return false;
}
const bool full = m.is_full() && m.codec_id == CODEC_FULL &&
m.payload_size == m.image_size;
const bool delta = !m.is_full() && m.codec_id == CODEC_DETOOLS_INPLACE;
if (!full && !delta) {
strcpy(msg, "nRF52 SD bootloader accepts full or in-place delta only");
snprintf(msg, NRF52_APPLY_MSG_CAP,
"nRF52 %s bootloader accepts full or in-place delta only", storage_name);
return false;
}
const uint32_t app_base = mota_nrf52_app_base();
@@ -624,14 +634,15 @@ bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow,
st.manifest_ok = true;
OtaBlCaps bl = ota_bootloader_caps();
if (!bl.present || !(bl.storage_flags & OTA_BL_STORAGE_SD)) {
strcpy(msg, "this bootloader has no SD OTA support - update the bootloader first");
if (!bl.present || !(bl.storage_flags & storage_flag)) {
snprintf(msg, NRF52_APPLY_MSG_CAP,
"this bootloader has no %s OTA support - update the bootloader first", storage_name);
return false;
}
if (bl.apply_abi < m.format_ver || !(bl.codec_mask & (1u << m.codec_id))) {
snprintf(msg, 159,
"bootloader cannot apply this SD update (abi=%u codecs=0x%x; need fmt=%u codec=%u)",
bl.apply_abi, bl.codec_mask, m.format_ver, m.codec_id);
snprintf(msg, NRF52_APPLY_MSG_CAP,
"bootloader cannot apply this %s update (abi=%u codecs=0x%x; need fmt=%u codec=%u)",
storage_name, bl.apply_abi, bl.codec_mask, m.format_ver, m.codec_id);
return false;
}
@@ -639,12 +650,13 @@ bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow,
st.sig_ok = vr.sig_ok;
st.trusted = vr.trusted;
if (!vr.root_ok || !vr.payload_ok || !vr.image_ok) {
strcpy(msg, "payload hash mismatch (incomplete or corrupt SD .mota)");
snprintf(msg, NRF52_APPLY_MSG_CAP,
"payload hash mismatch (incomplete or corrupt %s .mota)", storage_name);
return false;
}
SelfFwInfo fi;
if (delta) {
SelfFwInfo fi;
if (!ota_self_firmware(fi) || !fi.valid) {
strcpy(msg, "cannot read running firmware (no EndF)");
return false;
@@ -659,16 +671,51 @@ bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow,
if (!vr.sig_ok) { strcpy(msg, "bad signature"); return false; }
if (!vr.trusted) { strcpy(msg, "untrusted signer (pubkey not in allowlist)"); return false; }
}
if (delta) {
const uint64_t payload_off64 = 8u + MOTA_MFL + (uint64_t)m.block_count * 4u;
uint8_t patch_header[32]; // fixed byte + five detools varints (at most 26 bytes for uint32)
uint32_t header_len = m.payload_size < sizeof(patch_header) ? m.payload_size : sizeof(patch_header);
InplacePatchDims d;
if (payload_off64 > UINT32_MAX || payload_off64 + m.payload_size + 5u != total ||
!store.read((uint32_t)payload_off64, patch_header, header_len) ||
!parse_inplace_patch_dims(patch_header, header_len, d)) {
strcpy(msg, "bad in-place patch header");
return false;
}
if (!mota_nrf52_external_patch_geometry_valid(
d.memory, d.segment, d.shift, d.from, d.to,
MOTA_NRF52_APP_END - app_base, fi.image_len, m.image_size)) {
strcpy(msg, "invalid in-place patch geometry");
return false;
}
}
if (!store.approve_for_bootloader()) {
snprintf(msg, 159, "SD handoff failed: %s", store.last_error());
snprintf(msg, NRF52_APPLY_MSG_CAP, "%s handoff failed: %s", storage_name,
store.last_error());
return false;
}
sprintf(msg, "verified%s %s image on SD; rebooting into bootloader once this reply is sent",
vr.is_signed ? " (signer trusted)" : " (unsigned)", full ? "full" : "delta");
snprintf(msg, NRF52_APPLY_MSG_CAP,
"verified%s %s image on %s; rebooting into bootloader once this reply is sent",
vr.is_signed ? " (signer trusted)" : " (unsigned)", full ? "full" : "delta",
storage_name);
return true;
}
#endif
#if defined(OTA_SD_STORE)
bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store, const SignerAllowlist& allow,
ApplyState& st, char* msg) {
return ota_apply_mota_nrf52_external(store, allow, OTA_BL_STORAGE_SD, "SD", st, msg);
}
#endif
#if defined(OTA_QSPI_STORE)
bool ota_apply_mota_nrf52(OtaStoreQspiNrf52& store, const SignerAllowlist& allow,
ApplyState& st, char* msg) {
return ota_apply_mota_nrf52_external(store, allow, OTA_BL_STORAGE_QSPI, "QSPI", st, msg);
}
#endif
#else // native / other platforms
bool ota_apply_slot_info(uint32_t*, uint32_t*) { return false; }
+5
View File
@@ -86,6 +86,11 @@ class OtaStoreSdNrf52;
bool ota_apply_mota_nrf52(OtaStoreSdNrf52& store,
const SignerAllowlist& allow, ApplyState& st, char* msg);
#endif
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
class OtaStoreQspiNrf52;
bool ota_apply_mota_nrf52(OtaStoreQspiNrf52& store,
const SignerAllowlist& allow, ApplyState& st, char* msg);
#endif
// Commit the (already approved/armed) update and reboot into it - does NOT return. Call this only after
// a successful ota_apply_* AND after the confirmation reply has been delivered, so the operator knows
+1
View File
@@ -30,6 +30,7 @@ struct OtaBlCaps {
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;
// Scan the bootloader flash region for the marker. Returns {present=false} if not found / non-nRF52.
inline OtaBlCaps ota_bootloader_caps() {
+35 -10
View File
@@ -146,7 +146,8 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
snprintf(reply, 160,
"OTA seeder: status | stats | ls=find images | get <id> folder=capture | cancel | "
"announce | folder | config. LoRa install is disabled.");
#elif defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE)
#elif defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \
!defined(OTA_QSPI_STORE)
strcpy(reply,
"OTA: status | stats | ls | get <id> flash [rescue] | install | rescue install <hash16> | "
"cancel | announce | self | folder | config | key");
@@ -194,7 +195,10 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
// nRF52 applies via the bootloader - show (cached) whether it can, so `ota get`/`install` won't surprise.
// blrc = the bootloader's last apply code (diagnostic; 0xB8=success, see ota_delta.c).
const OtaBlCaps& bl = c.bootloaderCaps();
#if defined(OTA_SD_STORE)
#if defined(OTA_QSPI_STORE)
const char* bl_state = !bl.present ? "NONE" :
(bl.storage_flags & OTA_BL_STORAGE_QSPI) ? "QSPI" : "NO-QSPI";
#elif defined(OTA_SD_STORE)
const char* bl_state = !bl.present ? "NONE" :
(bl.storage_flags & OTA_BL_STORAGE_SD) ? "SD" : "NO-SD";
#else
@@ -269,7 +273,8 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
#if defined(NRF52_PLATFORM)
const OtaBlCaps& list_bl = c.bootloaderCaps();
#endif
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE)
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \
!defined(OTA_QSPI_STORE)
SelfFwInfo list_self;
bool list_has_endf = ota_self_firmware(list_self) && list_self.valid;
#endif
@@ -302,10 +307,13 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
&& h->codec < 16 && (list_bl.codec_mask & (1u << h->codec));
#if defined(OTA_SD_STORE)
installable = installable && (list_bl.storage_flags & OTA_BL_STORAGE_SD);
#elif defined(OTA_QSPI_STORE)
installable = installable && (list_bl.storage_flags & OTA_BL_STORAGE_QSPI);
#endif
#endif
if (!installable) fit = "unsupported";
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE)
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \
!defined(OTA_QSPI_STORE)
else if (!list_has_endf) fit = "rescue";
#endif
else fit = "same target";
@@ -428,9 +436,15 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
strcpy(reply, "ERR bootloader cannot apply an update staged on SD; update it over USB first");
return true;
}
#elif defined(OTA_QSPI_STORE)
if (!(bl.storage_flags & OTA_BL_STORAGE_QSPI)) {
strcpy(reply, "ERR bootloader cannot apply an update staged on QSPI; update it over USB first");
return true;
}
#endif
#endif
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE)
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \
!defined(OTA_QSPI_STORE)
SelfFwInfo self;
bool has_endf = ota_self_firmware(self) && self.valid;
if (!has_endf && !rescue) {
@@ -489,7 +503,7 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
c.manager.reset_session(); c.manager.want(0); c.manager.want_mid(nullptr);
c.fetch_to_folder = false;
c.manager.set_fetch_store(&c.fetch_store); // revert to the default flash store (a folder pull switched it)
#if defined(NRF52_PLATFORM) && !defined(OTA_SD_STORE)
#if defined(NRF52_PLATFORM) && !defined(OTA_SD_STORE) && !defined(OTA_QSPI_STORE)
c.manager.set_accept_full(false);
#endif
c.fetch_store.clear(); c.serving = false; c.serve_expected = 0; c.session_started_ms = 0;
@@ -517,7 +531,17 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
#if defined(NRF52_PLATFORM)
// nRF52 applies via the bootloader, so surface whether THIS device's bootloader can install this store.
const OtaBlCaps& bl = c.bootloaderCaps(); // cached (flash scanned once)
#if defined(OTA_SD_STORE)
#if defined(OTA_QSPI_STORE)
uint32_t qspi_capacity = c.fetch_store.capacity();
n += snprintf(reply + n, 160 - n, " | QSPI store:%s%uK",
qspi_capacity ? "" : "ERR ", (unsigned)(qspi_capacity / 1024));
if (bl.present && (bl.storage_flags & OTA_BL_STORAGE_QSPI))
snprintf(reply + n, 160 - n, " | bootloader: QSPI apply OK (abi=%u codecs=0x%x)",
bl.apply_abi, bl.codec_mask);
else
snprintf(reply + n, 160 - n,
" | bootloader: NO QSPI mota-apply support (install will refuse)");
#elif defined(OTA_SD_STORE)
if (bl.present && (bl.storage_flags & OTA_BL_STORAGE_SD))
snprintf(reply + n, 160 - n, " | bootloader: SD apply OK (abi=%u codecs=0x%x)", bl.apply_abi, bl.codec_mask);
else
@@ -538,7 +562,8 @@ bool handle_ota_command(const char* command, char* reply, mesh::MainBoard& board
// deliberately not an alias or automatic fallback: the operator must name `install` and provide
// the exact 8-byte base hash carried by the already-fetched package. The bootloader independently
// hashes the running app and refuses a mismatch before writing any application flash.
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && !defined(OTA_SEEDER_ONLY)
#if defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE) && !defined(OTA_SD_STORE) && \
!defined(OTA_QSPI_STORE) && !defined(OTA_SEEDER_ONLY)
if (c.fetch_to_folder) {
strcpy(reply, "ERR the complete update was captured to a folder, not staged for install; use `ota cancel`");
return true;
@@ -827,7 +852,7 @@ 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)
#if defined(NRF52_PLATFORM) && (defined(OTA_SD_STORE) || defined(OTA_QSPI_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",
@@ -837,7 +862,7 @@ 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)
#if defined(NRF52_PLATFORM) && (defined(OTA_SD_STORE) || defined(OTA_QSPI_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(); }
+15 -11
View File
@@ -10,7 +10,9 @@
#include "OtaFormat.h"
#include "OtaSelf.h" // ota_self_firmware() - prefer self-describing EndF identity at begin()
#include "OtaBlInfo.h" // bootloader OTA-apply capability marker (nRF52); cached after first read
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
#include "OtaStoreQspiNrf52.h"
#elif defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
#include "OtaStoreSdNrf52.h"
#include "OtaCacheSdNrf52.h"
#elif defined(NRF52_PLATFORM) && defined(OTA_FLASH_STORE)
@@ -31,12 +33,11 @@
#endif
// Per-device OTA singleton shared by the CLI (OtaCli) and the mesh adapter (the example's MyMesh).
// Holds the session engine, a staging store (fetch), a RAM serve buffer, and the signer allowlist.
// nRF52 stages into FLASH (OtaStoreFlashNrf52): a delta can be 100 KB+, too big to hold in RAM, and the
// COMPLETE container must persist so the bootloader can apply it after reboot. A flash page-erase halts
// the CPU (~85 ms) and starves the LoRa RX, so the store COALESCES writes to the 4 KB page (the erase
// unit) and commits each page once, off the per-packet path (see OtaManager.h) - RAM stays O(one page).
// (v1 has no mid-transfer resume; an interrupted fetch simply restarts.) ESP32/native use the RAM store.
// Holds the session engine, a persistent staging store (fetch), a RAM serve buffer, and the signer
// allowlist. nRF52 uses one of three stores selected by the exact target: internal flash for in-place
// deltas, raw microSD, or dedicated raw QSPI for full images and in-place deltas. Flash-backed stores
// coalesce writes at their erase-page boundary and checkpoint payload before leaf metadata, so an
// interrupted fetch can be verified and resumed without trusting stale progress markers.
namespace mesh {
namespace ota {
@@ -50,7 +51,8 @@ class FolderMotaStore; // pull destination over the seeder link (full type onl
#define OTA_SERVE_BUF_SIZE 1
// nRF52 self-serving streams from flash; this buffer is only for the manual `ota dev stage` helper.
// Keep it to one flash page so the OTA singleton does not consume another 16 KB of scarce SRAM.
#elif defined(NRF52_PLATFORM) && (defined(OTA_FLASH_STORE) || defined(OTA_SD_STORE))
#elif defined(NRF52_PLATFORM) && \
(defined(OTA_FLASH_STORE) || defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE))
#define OTA_SERVE_BUF_SIZE 4096
#else
#define OTA_SERVE_BUF_SIZE 16384
@@ -67,6 +69,8 @@ struct OtaContext {
// store object for OtaManager, while folder captures replace it with the
// host-backed FolderMotaStore for the duration of the pull.
OtaStoreRam<1> fetch_store;
#elif defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
OtaStoreQspiNrf52 fetch_store; // persistent raw QSPI staging, full + in-place delta
#elif defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
OtaStoreSdNrf52 fetch_store; // MeshTower V2: persistent SD staging, full + delta
OtaCacheSdNrf52 sd_cache; // persistent capture + source for every OTA container heard
@@ -161,7 +165,7 @@ struct OtaContext {
msg[95] = 0;
return false;
#else
#if !defined(NRF52_PLATFORM) || defined(OTA_SD_STORE)
#if !defined(NRF52_PLATFORM) || defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE)
if (rescue_base_hash) {
strncpy(msg, "rescue is only for internal-flash nRF52 builds", 96);
msg[95] = 0;
@@ -191,7 +195,7 @@ struct OtaContext {
}
}
bool ok;
#if defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
#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)
if (rescue_base_hash) {
@@ -446,7 +450,7 @@ struct OtaContext {
manager.set_accept_full(true);
manager.set_autofetch(OtaManager::AUTOFETCH_OFF);
autoinstall = AUTOINSTALL_OFF;
#elif defined(NRF52_PLATFORM) && defined(OTA_SD_STORE)
#elif defined(NRF52_PLATFORM) && (defined(OTA_SD_STORE) || defined(OTA_QSPI_STORE))
manager.set_accept_full(true);
manager.set_apply_codec(CODEC_DETOOLS_INPLACE);
#elif defined(NRF52_PLATFORM)
+15
View File
@@ -37,6 +37,7 @@ static const uint32_t MOTA_NRF52_FLASH_PAGE = 4096u;
static const uint8_t GPREGRET_OTA_APPLY = 0x6Au; // distinct from DFU magics 0x57/0x4E/0xA8
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;
// Firmware without a valid EndF and older host tooling fall back to this conservative apply workspace.
// New motatool builds read the firmware's appended layout record and derive memory_size from the actual
@@ -106,6 +107,20 @@ inline bool mota_nrf52_layout_valid(uint32_t app_base) {
return mota_nrf52_layout_valid(app_base, mota_nrf52_layout_stage_ceiling());
}
// Validate the five sizes encoded at the start of a detools in-place patch
// before an external SD/QSPI handoff is approved. External media leaves the
// complete application region available as workspace; the bootloader repeats
// these checks before its first destructive write.
inline bool mota_nrf52_external_patch_geometry_valid(uint32_t memory, uint32_t segment,
uint32_t shift, uint32_t from,
uint32_t to, uint32_t workspace_span,
uint32_t running_image_size,
uint32_t target_image_size) {
return memory != 0 && memory <= workspace_span && segment == MOTA_NRF52_FLASH_PAGE &&
shift <= memory && shift % segment == 0 && from <= memory - shift &&
from == running_image_size && to == target_image_size && to <= memory;
}
inline uint32_t mota_nrf52_stage_capacity(uint32_t app_base, uint32_t app_end,
uint32_t stage_ceiling) {
if (!mota_nrf52_layout_valid(app_base, stage_ceiling) || app_end < app_base ||
+701
View File
@@ -0,0 +1,701 @@
#include "OtaStoreQspiNrf52.h"
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
#include "OtaByteIO.h"
#include "OtaFlashLayout_nrf52.h"
#include "hal/nrf_qspi.h"
#include "nrf.h"
#include <Arduino.h>
#include <string.h>
namespace mesh {
namespace ota {
namespace {
#if defined(OTA_QSPI_SCK_PHYSICAL_PIN) && defined(OTA_QSPI_SCK_ARDUINO_PIN)
#error "QSPI SCK must use either a physical or Arduino pin override"
#elif !defined(OTA_QSPI_SCK_PHYSICAL_PIN) && !defined(OTA_QSPI_SCK_ARDUINO_PIN)
#define OTA_QSPI_SCK_ARDUINO_PIN PIN_QSPI_SCK
#endif
#if defined(OTA_QSPI_CS_PHYSICAL_PIN) && defined(OTA_QSPI_CS_ARDUINO_PIN)
#error "QSPI CS must use either a physical or Arduino pin override"
#elif !defined(OTA_QSPI_CS_PHYSICAL_PIN) && !defined(OTA_QSPI_CS_ARDUINO_PIN)
#define OTA_QSPI_CS_ARDUINO_PIN PIN_QSPI_CS
#endif
#if defined(OTA_QSPI_IO0_PHYSICAL_PIN) && defined(OTA_QSPI_IO0_ARDUINO_PIN)
#error "QSPI IO0 must use either a physical or Arduino pin override"
#elif !defined(OTA_QSPI_IO0_PHYSICAL_PIN) && !defined(OTA_QSPI_IO0_ARDUINO_PIN)
#define OTA_QSPI_IO0_ARDUINO_PIN PIN_QSPI_IO0
#endif
#if defined(OTA_QSPI_IO1_PHYSICAL_PIN) && defined(OTA_QSPI_IO1_ARDUINO_PIN)
#error "QSPI IO1 must use either a physical or Arduino pin override"
#elif !defined(OTA_QSPI_IO1_PHYSICAL_PIN) && !defined(OTA_QSPI_IO1_ARDUINO_PIN)
#define OTA_QSPI_IO1_ARDUINO_PIN PIN_QSPI_IO1
#endif
#if defined(OTA_QSPI_SCK_PHYSICAL_PIN)
static_assert((uint32_t)(OTA_QSPI_SCK_PHYSICAL_PIN) < 48, "invalid physical QSPI SCK pin");
#else
static_assert((uint32_t)(OTA_QSPI_SCK_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI SCK pin");
#endif
#if defined(OTA_QSPI_CS_PHYSICAL_PIN)
static_assert((uint32_t)(OTA_QSPI_CS_PHYSICAL_PIN) < 48, "invalid physical QSPI CS pin");
#else
static_assert((uint32_t)(OTA_QSPI_CS_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI CS pin");
#endif
#if defined(OTA_QSPI_IO0_PHYSICAL_PIN)
static_assert((uint32_t)(OTA_QSPI_IO0_PHYSICAL_PIN) < 48, "invalid physical QSPI IO0 pin");
#else
static_assert((uint32_t)(OTA_QSPI_IO0_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI IO0 pin");
#endif
#if defined(OTA_QSPI_IO1_PHYSICAL_PIN)
static_assert((uint32_t)(OTA_QSPI_IO1_PHYSICAL_PIN) < 48, "invalid physical QSPI IO1 pin");
#else
static_assert((uint32_t)(OTA_QSPI_IO1_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI IO1 pin");
#endif
#if defined(OTA_QSPI_IO2_NOT_CONNECTED)
#if defined(OTA_QSPI_IO2_ARDUINO_PIN) || defined(OTA_QSPI_IO2_PHYSICAL_PIN)
#error "QSPI IO2 cannot be both connected and disconnected"
#endif
#elif defined(OTA_QSPI_IO2_PHYSICAL_PIN)
static_assert((uint32_t)(OTA_QSPI_IO2_PHYSICAL_PIN) < 48, "invalid physical QSPI IO2 pin");
#elif defined(OTA_QSPI_IO2_ARDUINO_PIN)
static_assert((uint32_t)(OTA_QSPI_IO2_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI IO2 pin");
#else
#define OTA_QSPI_IO2_ARDUINO_PIN PIN_QSPI_IO2
static_assert((uint32_t)(OTA_QSPI_IO2_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI IO2 pin");
#endif
#if defined(OTA_QSPI_IO3_NOT_CONNECTED)
#if defined(OTA_QSPI_IO3_ARDUINO_PIN) || defined(OTA_QSPI_IO3_PHYSICAL_PIN)
#error "QSPI IO3 cannot be both connected and disconnected"
#endif
#elif defined(OTA_QSPI_IO3_PHYSICAL_PIN)
static_assert((uint32_t)(OTA_QSPI_IO3_PHYSICAL_PIN) < 48, "invalid physical QSPI IO3 pin");
#elif defined(OTA_QSPI_IO3_ARDUINO_PIN)
static_assert((uint32_t)(OTA_QSPI_IO3_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI IO3 pin");
#else
#define OTA_QSPI_IO3_ARDUINO_PIN PIN_QSPI_IO3
static_assert((uint32_t)(OTA_QSPI_IO3_ARDUINO_PIN) < PINS_COUNT, "invalid Arduino QSPI IO3 pin");
#endif
static uint8_t arduino_to_physical(uint32_t arduino_pin) {
return (uint8_t)g_ADigitalPinMap[arduino_pin];
}
static uint8_t qspi_sck_pin() {
#ifdef OTA_QSPI_SCK_PHYSICAL_PIN
return OTA_QSPI_SCK_PHYSICAL_PIN;
#else
return arduino_to_physical(OTA_QSPI_SCK_ARDUINO_PIN);
#endif
}
static uint8_t qspi_cs_pin() {
#ifdef OTA_QSPI_CS_PHYSICAL_PIN
return OTA_QSPI_CS_PHYSICAL_PIN;
#else
return arduino_to_physical(OTA_QSPI_CS_ARDUINO_PIN);
#endif
}
static uint8_t qspi_io0_pin() {
#ifdef OTA_QSPI_IO0_PHYSICAL_PIN
return OTA_QSPI_IO0_PHYSICAL_PIN;
#else
return arduino_to_physical(OTA_QSPI_IO0_ARDUINO_PIN);
#endif
}
static uint8_t qspi_io1_pin() {
#ifdef OTA_QSPI_IO1_PHYSICAL_PIN
return OTA_QSPI_IO1_PHYSICAL_PIN;
#else
return arduino_to_physical(OTA_QSPI_IO1_ARDUINO_PIN);
#endif
}
static uint8_t qspi_io2_pin() {
#ifdef OTA_QSPI_IO2_NOT_CONNECTED
return NRF_QSPI_PIN_NOT_CONNECTED;
#elif defined(OTA_QSPI_IO2_PHYSICAL_PIN)
return OTA_QSPI_IO2_PHYSICAL_PIN;
#else
return arduino_to_physical(OTA_QSPI_IO2_ARDUINO_PIN);
#endif
}
static uint8_t qspi_io3_pin() {
#ifdef OTA_QSPI_IO3_NOT_CONNECTED
return NRF_QSPI_PIN_NOT_CONNECTED;
#elif defined(OTA_QSPI_IO3_PHYSICAL_PIN)
return OTA_QSPI_IO3_PHYSICAL_PIN;
#else
return arduino_to_physical(OTA_QSPI_IO3_ARDUINO_PIN);
#endif
}
} // namespace
OtaStoreQspiNrf52::OtaStoreQspiNrf52() {
resetSession();
}
OtaStoreQspiNrf52::~OtaStoreQspiNrf52() {
releaseFlash();
}
void OtaStoreQspiNrf52::fail(const char *message) {
_io_ok = false;
strncpy(_error, message ? message : "QSPI error", sizeof(_error) - 1);
_error[sizeof(_error) - 1] = 0;
}
void OtaStoreQspiNrf52::resetSession() {
_total = 0;
_io_ok = true;
_meta_dirty = false;
_data_dirty = false;
_data_page_index = INVALID_PAGE;
memset(_meta_page, 0xFF, sizeof(_meta_page));
memset(_data_page, 0xFF, sizeof(_data_page));
memset(_known_pages, 0, sizeof(_known_pages));
_error[0] = 0;
}
bool OtaStoreQspiNrf52::pageKnown(uint32_t page) const {
return page < MAX_PAGES && (_known_pages[page >> 3] & (1u << (page & 7))) != 0;
}
void OtaStoreQspiNrf52::setPageKnown(uint32_t page) {
if (page < MAX_PAGES) _known_pages[page >> 3] |= (uint8_t)(1u << (page & 7));
}
bool OtaStoreQspiNrf52::waitReady(uint32_t timeout_ms) {
uint32_t started = millis();
while (!nrf_qspi_event_check(NRF_QSPI, NRF_QSPI_EVENT_READY)) {
if ((uint32_t)(millis() - started) >= timeout_ms) return false;
delay(1);
}
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
return true;
}
bool OtaStoreQspiNrf52::customInstruction(uint8_t opcode, uint8_t length, uint8_t *rx) {
nrf_qspi_cinstr_conf_t config;
memset(&config, 0, sizeof(config));
config.opcode = opcode;
config.length = (nrf_qspi_cinstr_len_t)length;
config.io2_level = true;
config.io3_level = true;
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
nrf_qspi_cinstr_transfer_start(NRF_QSPI, &config);
if (!waitReady(1000)) return false;
if (rx) nrf_qspi_cinstrdata_get(NRF_QSPI, config.length, rx);
return true;
}
bool OtaStoreQspiNrf52::ensureFlash() {
if (_qspi_ready) return true;
_error[0] = 0;
_io_ok = true;
#if defined(OTA_QSPI_POWER_PIN)
pinMode(OTA_QSPI_POWER_PIN, OUTPUT);
digitalWrite(OTA_QSPI_POWER_PIN, HIGH);
delay(2);
#elif defined(PIN_FLASH_EN)
pinMode(PIN_FLASH_EN, OUTPUT);
digitalWrite(PIN_FLASH_EN, HIGH);
delay(2);
#elif defined(QSPI_FLASH_EN)
pinMode(QSPI_FLASH_EN, OUTPUT);
digitalWrite(QSPI_FLASH_EN, HIGH);
delay(2);
#endif
NVIC_DisableIRQ(QSPI_IRQn);
NVIC_ClearPendingIRQ(QSPI_IRQn);
nrf_qspi_int_disable(NRF_QSPI, 0xFFFFFFFFUL);
nrf_qspi_pins_t pins;
pins.sck_pin = qspi_sck_pin();
pins.csn_pin = qspi_cs_pin();
pins.io0_pin = qspi_io0_pin();
pins.io1_pin = qspi_io1_pin();
pins.io2_pin = qspi_io2_pin();
pins.io3_pin = qspi_io3_pin();
nrf_qspi_pins_set(NRF_QSPI, &pins);
nrf_qspi_prot_conf_t protocol;
protocol.readoc = NRF_QSPI_READOC_FASTREAD;
protocol.writeoc = NRF_QSPI_WRITEOC_PP;
protocol.addrmode = NRF_QSPI_ADDRMODE_24BIT;
protocol.dpmconfig = false;
nrf_qspi_ifconfig0_set(NRF_QSPI, &protocol);
nrf_qspi_phy_conf_t physical;
physical.sck_delay = 5;
physical.dpmen = false;
physical.spi_mode = NRF_QSPI_MODE_0;
#ifdef OTA_QSPI_SCK_FREQUENCY
physical.sck_freq = static_cast<nrf_qspi_frequency_t>(OTA_QSPI_SCK_FREQUENCY);
#else
physical.sck_freq = NRF_QSPI_FREQ_32MDIV2;
#endif
nrf_qspi_ifconfig1_set(NRF_QSPI, &physical);
nrf_qspi_enable(NRF_QSPI);
_qspi_active = true;
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
nrf_qspi_task_trigger(NRF_QSPI, NRF_QSPI_TASK_ACTIVATE);
if (!waitReady(1000)) {
releaseFlash();
fail("QSPI activate timed out");
return false;
}
// Release from deep power-down, then identify capacity from the JEDEC byte.
if (!customInstruction(0xAB, NRF_QSPI_CINSTR_LEN_1B)) {
releaseFlash();
fail("QSPI wake failed");
return false;
}
_qspi_awake = true;
delayMicroseconds(50);
alignas(4) uint8_t jedec[4] = { 0, 0, 0, 0 };
if (!customInstruction(0x9F, NRF_QSPI_CINSTR_LEN_4B, jedec) || jedec[0] == 0 || jedec[0] == 0xFF ||
jedec[2] < 20 || jedec[2] > 24) {
releaseFlash();
fail("QSPI JEDEC ID/capacity unsupported");
return false;
}
#ifdef OTA_QSPI_EXPECTED_JEDEC_ID
const uint32_t jedec_id = ((uint32_t)jedec[0] << 16) | ((uint32_t)jedec[1] << 8) | jedec[2];
if (jedec_id != (uint32_t)OTA_QSPI_EXPECTED_JEDEC_ID) {
releaseFlash();
fail("QSPI JEDEC ID does not match target");
return false;
}
#endif
_flash_size = 1UL << jedec[2];
if (_flash_size > MAX_FLASH) {
releaseFlash();
fail("QSPI flash exceeds 24-bit staging limit");
return false;
}
#ifdef OTA_QSPI_EXPECTED_SIZE
if (_flash_size != (uint32_t)OTA_QSPI_EXPECTED_SIZE) {
releaseFlash();
fail("QSPI capacity does not match target");
return false;
}
#endif
_qspi_ready = true;
return true;
}
void OtaStoreQspiNrf52::releaseFlash() {
if (_qspi_awake) {
// The repeater can remain idle for hours after a capacity/status probe or
// a completed checkpoint. Put the NOR into deep power-down before
// releasing the nRF QSPI peripheral instead of leaving both active for
// the rest of the boot. ensureFlash() issues 0xAB on the next operation.
(void)customInstruction(0xB9, NRF_QSPI_CINSTR_LEN_1B);
delayMicroseconds(5);
}
// Match nrfx_qspi_uninit(): DEACTIVATE does not require a READY wait before
// disabling the peripheral. Trigger it after every successful ENABLE, even
// when wake or JEDEC identification failed before _qspi_ready was set.
if (_qspi_active) {
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
nrf_qspi_task_trigger(NRF_QSPI, NRF_QSPI_TASK_DEACTIVATE);
nrf_qspi_disable(NRF_QSPI);
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
}
_qspi_active = false;
_qspi_awake = false;
_qspi_ready = false;
#if defined(OTA_QSPI_POWER_PIN)
digitalWrite(OTA_QSPI_POWER_PIN, LOW);
#elif defined(PIN_FLASH_EN)
digitalWrite(PIN_FLASH_EN, LOW);
#elif defined(QSPI_FLASH_EN)
digitalWrite(QSPI_FLASH_EN, LOW);
#endif
}
bool OtaStoreQspiNrf52::dmaReadAligned(uint32_t address, uint32_t length) {
// nRF52840 QSPI EasyDMA requires a word-aligned RAM buffer and a transfer
// count that is a multiple of four. Keep the external address aligned too,
// so every supported SDK/peripheral revision receives the same safe shape.
if (!length || length > sizeof(_bounce) || ((address | length) & 3u) != 0 ||
(uint64_t)address + length > _flash_size) {
fail("QSPI aligned read invalid");
return false;
}
nrf_qspi_read_buffer_set(NRF_QSPI, _bounce, length, address);
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
nrf_qspi_task_trigger(NRF_QSPI, NRF_QSPI_TASK_READSTART);
if (!waitReady(5000)) {
fail("QSPI read timed out");
return false;
}
return true;
}
bool OtaStoreQspiNrf52::dmaWriteAligned(uint32_t address, uint32_t length) {
if (!length || length > sizeof(_bounce) || ((address | length) & 3u) != 0 ||
(address & (PROGRAM - 1)) + length > PROGRAM ||
(uint64_t)address + length > _flash_size) {
fail("QSPI aligned program invalid");
return false;
}
nrf_qspi_write_buffer_set(NRF_QSPI, _bounce, length, address);
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
nrf_qspi_task_trigger(NRF_QSPI, NRF_QSPI_TASK_WRITESTART);
if (!waitReady(10000)) {
fail("QSPI program timed out");
return false;
}
return true;
}
bool OtaStoreQspiNrf52::rawRead(uint32_t address, void *data, uint32_t length) {
if (length && !data) {
fail("QSPI read buffer missing");
return false;
}
if (!length) return true;
if (!ensureFlash() || (uint64_t)address + length > _flash_size) {
if (_io_ok) fail("QSPI read outside flash");
return false;
}
uint8_t *out = static_cast<uint8_t *>(data);
while (length) {
const uint32_t aligned_address = address & ~3u;
const uint32_t prefix = address - aligned_address;
uint32_t n = length;
if (n > sizeof(_bounce) - prefix) n = sizeof(_bounce) - prefix;
const uint32_t dma_length = (prefix + n + 3u) & ~3u;
if (!dmaReadAligned(aligned_address, dma_length)) return false;
memcpy(out, _bounce + prefix, n);
address += n;
out += n;
length -= n;
}
return true;
}
bool OtaStoreQspiNrf52::rawWrite(uint32_t address, const void *data, uint32_t length) {
if (length && !data) {
fail("QSPI program buffer missing");
return false;
}
if (!length) return true;
if (!ensureFlash() || (uint64_t)address + length > _flash_size) {
if (_io_ok) fail("QSPI program outside flash");
return false;
}
const uint8_t *in = static_cast<const uint8_t *>(data);
while (length) {
const uint32_t aligned_address = address & ~3u;
const uint32_t prefix = address - aligned_address;
const uint32_t page_room = PROGRAM - (aligned_address & (PROGRAM - 1));
uint32_t n = length;
if (n > page_room - prefix) n = page_room - prefix;
const uint32_t dma_length = (prefix + n + 3u) & ~3u;
// Read-modify-program the aligned EasyDMA window. NOR flash cannot change
// a programmed zero back to one without erasing the 4 KiB sector, so
// reject such a request instead of silently corrupting the container.
if (!dmaReadAligned(aligned_address, dma_length)) return false;
for (uint32_t i = 0; i < n; ++i) {
const uint8_t old_value = _bounce[prefix + i];
const uint8_t new_value = in[i];
if ((old_value & new_value) != new_value) {
fail("QSPI program requires erase");
return false;
}
_bounce[prefix + i] = new_value;
}
if (!dmaWriteAligned(aligned_address, dma_length)) return false;
address += n;
in += n;
length -= n;
}
return true;
}
bool OtaStoreQspiNrf52::rawErasePage(uint32_t address) {
if (!ensureFlash() || (address & (PAGE - 1)) != 0 || address > _flash_size - PAGE) return false;
nrf_qspi_erase_ptr_set(NRF_QSPI, address, NRF_QSPI_ERASE_LEN_4KB);
nrf_qspi_event_clear(NRF_QSPI, NRF_QSPI_EVENT_READY);
nrf_qspi_task_trigger(NRF_QSPI, NRF_QSPI_TASK_ERASESTART);
if (!waitReady(30000)) {
fail("QSPI erase timed out");
return false;
}
return true;
}
bool OtaStoreQspiNrf52::flushPage(uint32_t page, const uint8_t *data) {
if (!_io_ok || page >= MAX_PAGES || (uint64_t)(page + 1) * PAGE > _flash_size ||
!rawErasePage(page * PAGE) || !rawWrite(page * PAGE, data, PAGE)) {
if (_io_ok) fail("QSPI page write failed");
return false;
}
alignas(4) uint8_t verify[PROGRAM];
for (uint32_t off = 0; off < PAGE; off += sizeof(verify)) {
if (!rawRead(page * PAGE + off, verify, sizeof(verify)) ||
memcmp(verify, data + off, sizeof(verify)) != 0) {
fail("QSPI page verify failed");
return false;
}
}
setPageKnown(page);
return true;
}
bool OtaStoreQspiNrf52::flushMeta() {
if (!_meta_dirty) return _io_ok;
if (!flushPage(0, _meta_page)) return false;
_meta_dirty = false;
return true;
}
bool OtaStoreQspiNrf52::flushData() {
if (_data_page_index == INVALID_PAGE || !_data_dirty) return _io_ok;
if (!flushPage(_data_page_index, _data_page)) return false;
_data_dirty = false;
return true;
}
bool OtaStoreQspiNrf52::useDataPage(uint32_t page) {
if (page == 0 || page >= MAX_PAGES) return false;
if (_data_page_index == page) return true;
if (!flushData()) return false;
if (pageKnown(page)) {
if (!rawRead(page * PAGE, _data_page, PAGE)) {
fail("QSPI page load failed");
return false;
}
} else {
memset(_data_page, 0xFF, PAGE);
setPageKnown(page);
}
_data_page_index = page;
_data_dirty = false;
return true;
}
uint32_t OtaStoreQspiNrf52::capacity() const {
OtaStoreQspiNrf52 *self = const_cast<OtaStoreQspiNrf52 *>(this);
uint32_t result = self->ensureFlash() ? self->_flash_size : 0;
self->releaseFlash();
return result;
}
bool OtaStoreQspiNrf52::plan_layout(bool, uint32_t image_size, uint32_t, uint32_t payload_size) {
const uint32_t app_base = mota_nrf52_app_base();
if (image_size == 0 || payload_size == 0 || app_base >= MOTA_NRF52_APP_END ||
image_size > MOTA_NRF52_APP_END - app_base) {
fail("image exceeds nRF52 application region");
releaseFlash();
return false;
}
bool result = ensureFlash();
releaseFlash();
return result;
}
bool OtaStoreQspiNrf52::begin(uint32_t total_size) {
if (!ensureFlash() || total_size < 13 || total_size > _flash_size) {
if (_io_ok) fail("QSPI lacks space for update");
releaseFlash();
return false;
}
// Immediately invalidate an older raw container. Page zero is erased and
// replaced with the new header at the first checkpoint/finalize.
uint8_t zero[4] = { 0, 0, 0, 0 };
uint8_t check[sizeof(zero)];
if (!rawWrite(0, zero, sizeof(zero)) || !rawRead(0, check, sizeof(check)) ||
memcmp(check, zero, sizeof(zero)) != 0) {
fail("QSPI old-container invalidation failed");
releaseFlash();
return false;
}
resetSession();
_total = total_size;
releaseFlash();
return true;
}
bool OtaStoreQspiNrf52::set_meta_size(uint32_t meta_bytes) {
return _total >= 13 && meta_bytes <= PAGE;
}
bool OtaStoreQspiNrf52::write(uint32_t offset, const uint8_t *data, uint32_t len) {
if (!_io_ok || !data || (uint64_t)offset + len > _total) {
releaseFlash();
return false;
}
while (len) {
uint32_t page = offset / PAGE;
uint32_t in_page = offset & (PAGE - 1);
uint32_t n = PAGE - in_page;
if (n > len) n = len;
if (page == 0) {
memcpy(_meta_page + in_page, data, n);
_meta_dirty = true;
} else {
if (!useDataPage(page)) {
releaseFlash();
return false;
}
memcpy(_data_page + in_page, data, n);
_data_dirty = true;
}
offset += n;
data += n;
len -= n;
}
releaseFlash();
return true;
}
bool OtaStoreQspiNrf52::read(uint32_t offset, uint8_t *buf, uint32_t len) const {
OtaStoreQspiNrf52 *self = const_cast<OtaStoreQspiNrf52 *>(this);
if (!_io_ok || !buf || (uint64_t)offset + len > _total) {
self->releaseFlash();
return false;
}
while (len) {
uint32_t page = offset / PAGE;
uint32_t in_page = offset & (PAGE - 1);
uint32_t n = PAGE - in_page;
if (n > len) n = len;
if (page == 0) {
memcpy(buf, _meta_page + in_page, n);
} else if (page == _data_page_index) {
memcpy(buf, _data_page + in_page, n);
} else if (pageKnown(page)) {
if (!self->rawRead(offset, buf, n)) {
self->releaseFlash();
return false;
}
} else {
memset(buf, 0xFF, n);
}
offset += n;
buf += n;
len -= n;
}
self->releaseFlash();
return true;
}
bool OtaStoreQspiNrf52::finalize() {
if (!_total || !_io_ok) {
releaseFlash();
return false;
}
// Persist payload first, then the leaf-progress metadata that declares it.
bool result = flushData() && flushMeta();
releaseFlash();
return result;
}
void OtaStoreQspiNrf52::checkpoint() {
if (!_total || !_io_ok) {
releaseFlash();
return;
}
if (flushData()) flushMeta();
releaseFlash();
}
bool OtaStoreQspiNrf52::reopen() {
resetSession();
if (!ensureFlash()) {
releaseFlash();
return false;
}
uint8_t header[8];
if (!rawRead(0, header, sizeof(header)) || memcmp(header, MOTA_MAGIC, 4) != 0) {
releaseFlash();
return false;
}
uint32_t total = rd_u32le(header + 4);
if (total < 13 || total > _flash_size) {
releaseFlash();
return false;
}
uint8_t trailer[5];
if (!rawRead(total - sizeof(trailer), trailer, sizeof(trailer)) ||
memcmp(trailer, MOTA_TRAILER, sizeof(trailer)) != 0) {
releaseFlash();
return false;
}
if (!rawRead(0, _meta_page, PAGE)) {
fail("QSPI metadata load failed");
releaseFlash();
return false;
}
_total = total;
uint32_t pages = (total + PAGE - 1) / PAGE;
for (uint32_t page = 0; page < pages; page++)
setPageKnown(page);
releaseFlash();
return true;
}
void OtaStoreQspiNrf52::clear() {
bool ok = ensureFlash();
uint8_t zero[4] = { 0, 0, 0, 0 };
uint8_t check[sizeof(zero)];
if (ok) {
ok = rawWrite(0, zero, sizeof(zero)) && rawRead(0, check, sizeof(check)) &&
memcmp(check, zero, sizeof(zero)) == 0;
}
if (ok && _total >= 8 + MOTA_OFF_APPROVAL + sizeof(zero)) {
const uint32_t approval = 8 + MOTA_OFF_APPROVAL;
ok = rawWrite(approval, zero, sizeof(zero)) && rawRead(approval, check, sizeof(check)) &&
memcmp(check, zero, sizeof(zero)) == 0;
}
char saved_error[sizeof(_error)];
strncpy(saved_error, _error, sizeof(saved_error));
saved_error[sizeof(saved_error) - 1] = 0;
resetSession();
if (!ok) fail(saved_error[0] ? saved_error : "QSPI container invalidation failed");
releaseFlash();
}
bool OtaStoreQspiNrf52::approve_for_bootloader() {
if (!finalize()) return false;
const uint32_t approval = 8 + MOTA_OFF_APPROVAL;
if (approval + sizeof(APPROVAL_YES) > _total || !rawWrite(approval, APPROVAL_YES, sizeof(APPROVAL_YES))) {
fail("QSPI approval write failed");
releaseFlash();
return false;
}
uint8_t check[sizeof(APPROVAL_YES)];
if (!rawRead(approval, check, sizeof(check)) || memcmp(check, APPROVAL_YES, sizeof(check)) != 0) {
fail("QSPI approval verify failed");
releaseFlash();
return false;
}
memcpy(_meta_page + approval, APPROVAL_YES, sizeof(APPROVAL_YES));
releaseFlash();
return true;
}
} // namespace ota
} // namespace mesh
#endif
+95
View File
@@ -0,0 +1,95 @@
#pragma once
#if defined(OTA_QSPI_STORE) && defined(QSPIFLASH)
#error "OTA_QSPI_STORE raw staging cannot share a QSPI chip with QSPIFLASH"
#endif
#if defined(OTA_QSPI_SHARED_WISBLOCK_SPI) && defined(ETHERNET_ENABLED)
#error "WisBlock SPI OTA staging cannot share the bus/chip-select with Ethernet"
#endif
#if defined(OTA_QSPI_SHARED_WISBLOCK_SPI) && defined(OTA_SD_STORE)
#error "WisBlock SPI OTA staging cannot share the bus/chip-select with SD staging"
#endif
#if defined(OTA_QSPI_SHARED_WISBLOCK_SPI) && defined(RAK_3401)
#error "RAK3401's RAK13302 radio already owns the WisBlock SPI bus/chip-select"
#endif
#if defined(NRF52_PLATFORM) && defined(OTA_QSPI_STORE)
#include "OtaStore.h"
namespace mesh {
namespace ota {
// Persistent raw-QSPI staging for nRF52840 repeater builds. The complete .mota
// lives at external-flash offset zero, leaving the whole internal application
// region available to the bootloader as an in-place workspace. This store must
// never be enabled on a build that mounts the same chip as a filesystem.
class OtaStoreQspiNrf52 : public OtaStore {
static const uint32_t PAGE = 4096;
static const uint32_t PROGRAM = 256;
static const uint32_t MAX_FLASH = 16UL * 1024 * 1024;
static const uint32_t MAX_PAGES = MAX_FLASH / PAGE;
static const uint32_t INVALID_PAGE = 0xFFFFFFFFUL;
uint32_t _total = 0;
uint32_t _flash_size = 0;
bool _qspi_active = false;
bool _qspi_awake = false;
bool _qspi_ready = false;
bool _io_ok = true;
bool _meta_dirty = false;
bool _data_dirty = false;
uint32_t _data_page_index = INVALID_PAGE;
char _error[80] = { 0 };
alignas(4) uint8_t _meta_page[PAGE];
alignas(4) uint8_t _data_page[PAGE];
alignas(4) uint8_t _bounce[PROGRAM];
uint8_t _known_pages[MAX_PAGES / 8];
void fail(const char *message);
bool ensureFlash();
void releaseFlash();
bool waitReady(uint32_t timeout_ms);
bool customInstruction(uint8_t opcode, uint8_t length, uint8_t *rx = nullptr);
bool dmaReadAligned(uint32_t address, uint32_t length);
bool dmaWriteAligned(uint32_t address, uint32_t length);
bool rawRead(uint32_t address, void *data, uint32_t length);
bool rawWrite(uint32_t address, const void *data, uint32_t length);
bool rawErasePage(uint32_t address);
bool flushPage(uint32_t page, const uint8_t *data);
bool flushMeta();
bool flushData();
bool useDataPage(uint32_t page);
void resetSession();
bool pageKnown(uint32_t page) const;
void setPageKnown(uint32_t page);
public:
OtaStoreQspiNrf52();
~OtaStoreQspiNrf52() override;
bool begin(uint32_t total_size) override;
bool write(uint32_t offset, const uint8_t *data, uint32_t len) override;
bool read(uint32_t offset, uint8_t *buf, uint32_t len) const override;
uint32_t capacity() const override;
uint32_t staged_size() const override { return _total; }
void clear() override;
bool set_meta_size(uint32_t meta_bytes) override;
bool finalize() override;
void checkpoint() override;
bool reopen() override;
bool plan_layout(bool is_full, uint32_t image_size, uint32_t payload_off, uint32_t payload_size) override;
// Sets APRV only after all app-side verification gates have passed.
bool approve_for_bootloader();
const char *last_error() const { return _error; }
};
} // namespace ota
} // namespace mesh
#endif
+15 -1
View File
@@ -2,7 +2,7 @@
#include <stdint.h>
// AUTO-GENERATED by tools/mota/gen_targets.py - do not edit by hand.
// 552 OTA-capable PlatformIO envs. Maps target_id (= sha2-256:4 of the env name, LE uint32)
// 566 OTA-capable PlatformIO envs. Maps target_id (= sha2-256:4 of the env name, LE uint32)
// to the human-readable env name, so a node/tool can name a target seen over the air WITHOUT
// transmitting the string in the .mota / LoRa protocol. Regenerate when the OTA env set changes.
// Size-constrained receivers can set OTA_TARGET_NAME_TABLE=0. They still match targets by ID;
@@ -153,6 +153,7 @@ inline const char* ota_target_env_name(uint32_t target_id) {
{ 0x21519537, "Heltec_v3_sensor" },
{ 0x382bb181, "Heltec_v3_terminal_chat" },
{ 0x5a2980e6, "heltec_v4_3_companion_radio_ble_femoff" },
{ 0x90581c83, "heltec_v4_3_companion_radio_ble_ps_femoff" },
{ 0x356df17c, "heltec_v4_3_companion_radio_usb_femoff" },
{ 0x99a084fc, "heltec_v4_3_companion_radio_wifi_femoff" },
{ 0x64c9dfc9, "heltec_v4_3_companion_radio_wifi_mqtt_femoff" },
@@ -160,16 +161,23 @@ inline const char* ota_target_env_name(uint32_t target_id) {
{ 0x2c13b8e9, "heltec_v4_3_tft_companion_radio_ble_femoff" },
{ 0xb016979a, "heltec_v4_3_tft_companion_radio_usb_femoff" },
{ 0xb49b6b55, "heltec_v4_3_tft_companion_radio_wifi_femoff" },
{ 0x01d8f124, "heltec_v4_companion_radio_ble" },
{ 0x86fb85f0, "heltec_v4_companion_radio_ble_femon" },
{ 0xd33b0986, "heltec_v4_companion_radio_ble_ps_femon" },
{ 0x1a4d0096, "heltec_v4_companion_radio_usb" },
{ 0xa8f5940f, "heltec_v4_companion_radio_usb_femon" },
{ 0x1cf5e14c, "heltec_v4_companion_radio_usb_ps_femoff" },
{ 0x2707d3d1, "heltec_v4_companion_radio_usb_ps_femon" },
{ 0x86ac3b1b, "heltec_v4_companion_radio_wifi_femon" },
{ 0x7af848da, "heltec_v4_companion_radio_wifi_mqtt_femon" },
{ 0xd522a14f, "heltec_v4_expansionkit_repeater" },
{ 0x76657d76, "heltec_v4_expansionkit_repeater_observer_mqtt" },
{ 0x21ee03bd, "heltec_v4_expansionkit_room_server_observer_mqtt" },
{ 0x5977d941, "heltec_v4_expansionkit_tft_companion_radio_ble_femon" },
{ 0xe2a1907b, "heltec_v4_expansionkit_tft_companion_radio_ble_ps" },
{ 0xf75feb3e, "heltec_v4_kiss_modem" },
{ 0xbc9cdc70, "heltec_v4_r8_companion_radio_ble" },
{ 0x19e31fc6, "heltec_v4_r8_companion_radio_ble_ps" },
{ 0x36c78d86, "heltec_v4_r8_companion_radio_usb" },
{ 0xdf722326, "heltec_v4_r8_companion_radio_wifi" },
{ 0xfa901c87, "heltec_v4_r8_kiss_modem" },
@@ -411,6 +419,7 @@ inline const char* ota_target_env_name(uint32_t target_id) {
{ 0x76f3c984, "RAK_4631_repeater_bridge_rs232_serial1_lora_ota_no_external_sensors" },
{ 0x41e33a97, "RAK_4631_repeater_bridge_rs232_serial2_lora_ota_no_external_sensors" },
{ 0x29a0da19, "RAK_4631_repeater_lora_ota_no_external_sensors" },
{ 0x29bc38dc, "RAK_4631_repeater_rak15001_slot_c_lora_ota" },
{ 0xf1d3c5a8, "RAK_4631_terminal_chat" },
{ 0xc6d55752, "RAK_WisMesh_Tag_companion_radio_ble" },
{ 0x60683191, "RAK_WisMesh_Tag_companion_radio_usb" },
@@ -442,7 +451,9 @@ inline const char* ota_target_env_name(uint32_t target_id) {
{ 0x0ba4453d, "Station_G3_ESP32_kiss_modem" },
{ 0xf8d1958f, "Station_G3_ESP32_logging_repeater" },
{ 0x3c49caf8, "Station_G3_ESP32_repeater" },
{ 0x58689ef9, "Station_G3_ESP32_repeater_observer_mqtt" },
{ 0x91878dd8, "Station_G3_ESP32_room_server" },
{ 0x72a93c45, "Station_G3_ESP32_room_server_observer_mqtt" },
{ 0x4dbf035e, "t1000e_companion_radio_ble" },
{ 0xff4102d6, "t1000e_companion_radio_usb" },
{ 0x2f9a9185, "t1000e_kiss_modem" },
@@ -517,7 +528,9 @@ inline const char* ota_target_env_name(uint32_t target_id) {
{ 0xa5f4a562, "ThinkNode_M7_companion_radio_wifi" },
{ 0xef83222b, "ThinkNode_M7_kiss_modem" },
{ 0xc36ec867, "ThinkNode_M7_repeater" },
{ 0xb932238e, "ThinkNode_M7_repeater_observer_mqtt" },
{ 0x92f5bbea, "ThinkNode_M7_room_server" },
{ 0x26d91ab2, "ThinkNode_M7_room_server_observer_mqtt" },
{ 0x708ac4dc, "ThinkNode_M9_companion_radio_ble_" },
{ 0xdc51bf13, "ThinkNode_M9_companion_radio_usb_" },
{ 0xc27f8304, "ThinkNode_M9_companion_radio_wifi_" },
@@ -530,6 +543,7 @@ inline const char* ota_target_env_name(uint32_t target_id) {
{ 0x475a2a6a, "WioTrackerL1_companion_radio_ble" },
{ 0xf4175d2c, "WioTrackerL1_companion_radio_usb" },
{ 0x3d831006, "WioTrackerL1_kiss_modem" },
{ 0x556eb1bc, "WioTrackerL1_repeater" },
{ 0xb67370b3, "WioTrackerL1_repeater_lora_ota_no_external_sensors" },
{ 0x0c689b90, "WioTrackerL1_room_server" },
{ 0x91eac424, "WioTrackerL1Eink_companion_radio_ble" },