Add saved regulator modes and battery-gated crash recovery

This commit is contained in:
mikecarper committed 2026-10-18 21:10:58 -07:00
1 parent 515bb57cdf
commit c4a1d485ff
70 files changed
+5335 -2472

No files matched your search

+7
View File
@@ -10,6 +10,7 @@
#include <helpers/MorseRx.h>
#include <helpers/AGCResetScheduler.h>
#include <helpers/BeaconConfig.h>
#include <helpers/RegulatorMode.h>
#ifndef RADIO_LIVENESS_SOFT_ONLY
#include <helpers/RadioLivenessTracker.h>
#endif
@@ -36,6 +37,12 @@ public:
*/
class Radio {
public:
virtual uint8_t regulatorModes() const { return 0; }
virtual power::RegulatorMode defaultRegulatorMode() const { return power::RegulatorMode::Default; }
// Cache a selection before the first hardware initialization. Save later
// choices for the next reboot; never change a live RX/TX regulator here.
virtual bool configureBootRegulator(power::RegulatorMode) { return false; }
virtual power::RegulatorMode activeRegulatorMode() const { return power::RegulatorMode::Default; }
static constexpr uint32_t CarrierWaveDefaultMillis = 10000;
static constexpr uint32_t CarrierWaveMaxMillis = 60000;
virtual bool supportsCarrierWave() const { return false; }
+9
View File
@@ -5,6 +5,7 @@
#include <math.h>
#include "helpers/UsbLogging.h"
#include "helpers/BatteryChargeSupport.h"
#include "helpers/RegulatorMode.h"
#include "helpers/PowerSavingDiagnostics.h"
#if defined(ARDUINO) && (defined(RP2040_PLATFORM) || defined(STM32_PLATFORM))
#include <Arduino.h>
@@ -145,6 +146,14 @@ public:
virtual void onBeforeTransmit() { }
virtual void onAfterTransmit() { }
virtual void reboot() = 0;
virtual uint8_t mcuRegulatorModes() const { return 0; }
virtual power::RegulatorMode defaultMcuRegulatorMode() const {
return power::RegulatorMode::Default;
}
virtual bool applyMcuRegulator(power::RegulatorMode) { return false; }
virtual power::RegulatorMode activeMcuRegulatorMode() const {
return power::RegulatorMode::Default;
}
// Reboot into a UF2-capable bootloader when the platform supports the
// retained reset request. Returns false only when unsupported or rejected;
// a successful implementation resets and does not return.
+2
View File
@@ -1,3 +1,4 @@
#include <helpers/RegulatorSettings.h>
#include <Arduino.h>
#include "CarrierTools.h"
#include "PacketFileLoggingConfig.h"
@@ -2978,6 +2979,7 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, char* command, char* re
#if MESH_BATTERY_CHARGE_CONTROL
if (mesh::power::handleBatteryChargeCommand(*_board, command, reply, 160)) return;
#endif
if (mesh::power::handleRegulatorCommand(command, reply, 157)) return;
if (mesh::handleUsbLoggingWatchdogCommand(command, reply, 160)) return;
if (mesh::powerdiag::handleCommand(command, reply, 157, millis())) return;
if (handleManagementCommand(command, reply)) return;
+127 -10
View File
@@ -9,6 +9,7 @@
#include <target.h>
#include "UserGpioPinPolicy.h"
#include "PowerSavingDiagnostics.h"
#include "Nrf52RegulatorPlatform.h"
#include <bluefruit.h>
#include "ble_gap.h"
@@ -29,6 +30,87 @@ static uint16_t ota_conn_handle = BLE_CONN_HANDLE_INVALID;
static bool ota_active = false;
static bool ota_ble_started = false;
// Intentional reboot paths share the recovery policy's clean-reset marker.
// Keep this optional for standalone board/library builds without the policy.
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
namespace {
volatile mesh::power::RecoveryRecord regulator_recovery_record
__attribute__((section(".regulator_recovery"), used, aligned(4)));
static_assert(sizeof(mesh::power::RecoveryRecord) <= 64,
"nRF52 regulator recovery exceeds retained allocation");
uint32_t regulator_reset_reason = 0;
uint8_t regulator_shutdown_reason = 0;
}
extern "C" {
extern uint8_t __regulator_recovery_start__[] __attribute__((weak));
extern uint8_t __regulator_recovery_end__[] __attribute__((weak));
}
// Capture before the core's SystemInit constructor applies reset errata and
// before power management clears GPREGRET2. This also runs on boards without
// NRF52_POWER_MANAGEMENT; the recovery policy never writes either GPREGRET.
static void __attribute__((constructor(101))) nrf52_regulator_reset_capture() {
regulator_reset_reason = NRF_POWER->RESETREAS;
regulator_shutdown_reason = NRF_POWER->GPREGRET2;
}
namespace mesh { namespace power {
volatile RecoveryRecord& platformRecoveryRecord() {
return regulator_recovery_record;
}
bool platformRecoverySupported() {
return reinterpret_cast<uintptr_t>(__regulator_recovery_start__) == 0x20006050UL
&& reinterpret_cast<uintptr_t>(__regulator_recovery_end__) == 0x20006090UL
&& reinterpret_cast<uintptr_t>(&regulator_recovery_record) == 0x20006050UL;
}
ResetKind platformResetKind() {
// RESETREAS is sticky. Boards without power management otherwise retain
// wake bits forever and could hide a later real watchdog failure.
static bool acknowledged = false;
if (!acknowledged) {
uint8_t sd_enabled = 0;
if (mesh_nrf52::softdeviceIsEnabled(sd_enabled) != NRF_SUCCESS)
return ResetKind::Unknown;
if (sd_enabled) {
if (sd_power_reset_reason_clr(0xFFFFFFFFUL) != NRF_SUCCESS)
return ResetKind::Unknown;
} else {
NRF_POWER->RESETREAS = 0xFFFFFFFFUL;
}
acknowledged = true;
}
// A deliberate SYSTEMOFF, reset-pin press, or debugger event never counts
// against DCDC, even if another latched reason bit is also present.
if (regulator_shutdown_reason == 0x4C // low voltage
|| regulator_shutdown_reason == 0x55 // user shutdown
|| regulator_shutdown_reason == 0x42) // boot protection
return ResetKind::Intentional;
const uint32_t reason = regulator_reset_reason;
if (reason & POWER_RESETREAS_RESETPIN_Msk) return ResetKind::Intentional;
#ifdef POWER_RESETREAS_DIF_Msk
if (reason & POWER_RESETREAS_DIF_Msk) return ResetKind::Intentional;
#endif
uint32_t wakes = 0;
#ifdef POWER_RESETREAS_OFF_Msk
wakes |= POWER_RESETREAS_OFF_Msk;
#endif
#ifdef POWER_RESETREAS_LPCOMP_Msk
wakes |= POWER_RESETREAS_LPCOMP_Msk;
#endif
#ifdef POWER_RESETREAS_VBUS_Msk
wakes |= POWER_RESETREAS_VBUS_Msk;
#endif
if (reason & wakes) return ResetKind::Wake;
if (reason & (POWER_RESETREAS_DOG_Msk | POWER_RESETREAS_LOCKUP_Msk
| POWER_RESETREAS_SREQ_Msk)) return ResetKind::Crash;
return reason == 0 ? ResetKind::Cold : ResetKind::Unknown;
}
}} // namespace mesh::power
// A normal internal-flash operation completes in milliseconds. One minute is
// deliberately generous for other legitimate application work while still
// recovering an indefinitely blocked SoftDevice flash wait without requiring
@@ -113,7 +195,7 @@ bool NRF52Board::rebootToUf2Bootloader() {
NRF_POWER->GPREGRET = DFU_MAGIC_UF2_RESET;
}
NVIC_SystemReset();
reboot();
return true;
}
@@ -140,6 +222,46 @@ void NRF52Board::begin() {
#endif
}
void NRF52Board::reboot() {
if (meshRegulatorCleanReset) meshRegulatorCleanReset();
NVIC_SystemReset();
}
uint8_t NRF52Board::mcuRegulatorModes() const {
return cpu_dcdc_supported
? mesh::power::REGULATOR_DCDC | mesh::power::REGULATOR_LDO : 0;
}
mesh::power::RegulatorMode NRF52Board::defaultMcuRegulatorMode() const {
return cpu_dcdc_supported ? mesh::power::RegulatorMode::Dcdc
: mesh::power::RegulatorMode::Default;
}
mesh::power::RegulatorMode NRF52Board::activeMcuRegulatorMode() const {
return cpu_dcdc_supported ? cpu_regulator_mode
: mesh::power::RegulatorMode::Default;
}
bool NRF52Board::applyMcuRegulator(mesh::power::RegulatorMode mode) {
using mesh::power::RegulatorMode;
if (!cpu_dcdc_supported) return false;
if (mode == RegulatorMode::Default) mode = defaultMcuRegulatorMode();
if (mode != RegulatorMode::Dcdc && mode != RegulatorMode::Ldo) return false;
uint8_t sd_enabled = 0;
if (mesh_nrf52::softdeviceIsEnabled(sd_enabled) != NRF_SUCCESS) return false;
const bool dcdc = mode == RegulatorMode::Dcdc;
if (sd_enabled) {
if (sd_power_dcdc_mode_set(dcdc ? NRF_POWER_DCDC_ENABLE
: NRF_POWER_DCDC_DISABLE) != NRF_SUCCESS)
return false;
} else {
NRF_POWER->DCDCEN = dcdc ? 1 : 0;
}
cpu_regulator_mode = mode;
return true;
}
void NRF52Board::loop() {
#ifdef NRF52_POWER_MANAGEMENT
if (!mesh::power::voltagePolicySupported()) return;
@@ -454,15 +576,10 @@ void NRF52Board::armVbusWake() {
void NRF52BoardDCDC::begin() {
NRF52Board::begin();
// Enable DC/DC converter for improved power efficiency
uint8_t sd_enabled = 0;
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
if (sd_enabled) {
sd_power_dcdc_mode_set(NRF_POWER_DCDC_ENABLE);
} else {
NRF_POWER->DCDCEN = 1;
}
// Only boards which explicitly call this method approve the external LC
// circuitry. The common startup hook applies saved choices after the
// board's battery ADC and filesystem are ready; do not enable DCDC here.
cpu_dcdc_supported = true;
}
bool NRF52Board::isExternalPowered() {
+8 -1
View File
@@ -3,6 +3,7 @@
#include <Arduino.h>
#include <MeshCore.h>
#include <helpers/KeyValueStore.h>
#include <helpers/RegulatorMode.h>
#if defined(NRF52_PLATFORM)
@@ -35,6 +36,8 @@ class NRF52Board : public mesh::MainBoard {
protected:
uint8_t startup_reason;
char *ota_name;
bool cpu_dcdc_supported = false;
mesh::power::RegulatorMode cpu_regulator_mode = mesh::power::RegulatorMode::Ldo;
#ifdef NRF52_POWER_MANAGEMENT
void pwrmgtInit() { initPowerMgr(); }
@@ -65,7 +68,11 @@ public:
void serviceWatchdog() override;
virtual uint8_t getStartupReason() const override { return startup_reason; }
virtual float getMCUTemperature() override;
virtual void reboot() override { NVIC_SystemReset(); }
void reboot() override;
uint8_t mcuRegulatorModes() const override;
mesh::power::RegulatorMode defaultMcuRegulatorMode() const override;
bool applyMcuRegulator(mesh::power::RegulatorMode mode) override;
mesh::power::RegulatorMode activeMcuRegulatorMode() const override;
bool rebootToUf2Bootloader() override;
virtual void shutdownPeripherals();
virtual void powerOff() override;
+15
View File
@@ -0,0 +1,15 @@
#pragma once
#if defined(NRF52_PLATFORM)
#include "RegulatorRecovery.h"
namespace mesh { namespace power {
volatile RecoveryRecord& platformRecoveryRecord();
ResetKind platformResetKind();
// False for an unsupported linker layout, so an unreserved orphan section
// cannot accidentally enable automatic recovery.
bool platformRecoverySupported();
}} // namespace mesh::power
#endif
+16
View File
@@ -0,0 +1,16 @@
#pragma once
#include <stdint.h>
namespace mesh { namespace power {
enum class RegulatorMode : uint8_t { Default = 0, Dcdc = 1, Ldo = 2 };
constexpr uint8_t REGULATOR_DCDC = 1;
constexpr uint8_t REGULATOR_LDO = 2;
inline bool regulatorModeSupported(uint8_t modes, RegulatorMode mode) {
return modes != 0 && (mode == RegulatorMode::Default
|| (mode == RegulatorMode::Dcdc && (modes & REGULATOR_DCDC))
|| (mode == RegulatorMode::Ldo && (modes & REGULATOR_LDO)));
}
}} // namespace mesh::power
+58
View File
@@ -0,0 +1,58 @@
#include "RegulatorPlatform.h"
#if defined(ESP32)
#include <esp_system.h>
#include <esp_attr.h>
#include <soc/soc_caps.h>
#if SOC_RTC_SLOW_MEM_SUPPORTED || SOC_RTC_FAST_MEM_SUPPORTED
#define MESH_REGULATOR_RTC 1
static RTC_NOINIT_ATTR volatile mesh::power::RecoveryRecord regulator_record;
#else
static volatile mesh::power::RecoveryRecord regulator_record;
#endif
namespace mesh { namespace power {
volatile RecoveryRecord& platformRecoveryRecord() { return regulator_record; }
bool platformRecoverySupported() {
#ifdef MESH_REGULATOR_RTC
return true;
#else
return false;
#endif
}
ResetKind platformResetKind() {
switch (esp_reset_reason()) {
case ESP_RST_PANIC:
case ESP_RST_INT_WDT:
case ESP_RST_TASK_WDT:
case ESP_RST_WDT: return ResetKind::Crash;
case ESP_RST_DEEPSLEEP: return ResetKind::Wake;
case ESP_RST_SW: return ResetKind::Intentional;
case ESP_RST_POWERON:
case ESP_RST_EXT: return ResetKind::Cold;
// A supply failure is not evidence that the DC/DC regulator is faulty.
default: return ResetKind::Unknown;
}
}
}}
#elif defined(RP2040_PLATFORM)
#include <hardware/watchdog.h>
// Arduino-Pico's linker explicitly keeps this outside BSS and above the
// boot-ROM's scratch area. No watchdog scratch registers are commandeered.
static volatile mesh::power::RecoveryRecord regulator_record
__attribute__((section(".uninitialized_data.regulator_recovery"), used));
namespace mesh { namespace power {
volatile RecoveryRecord& platformRecoveryRecord() { return regulator_record; }
bool platformRecoverySupported() { return true; }
ResetKind platformResetKind() {
if (watchdog_enable_caused_reboot()) return ResetKind::Crash;
return watchdog_caused_reboot() ? ResetKind::Intentional : ResetKind::Cold;
}
}}
#elif !defined(NRF52_PLATFORM)
namespace mesh { namespace power {
static volatile RecoveryRecord regulator_record;
volatile RecoveryRecord& platformRecoveryRecord() { return regulator_record; }
bool platformRecoverySupported() { return false; }
ResetKind platformResetKind() { return ResetKind::Unknown; }
}}
#endif
+8
View File
@@ -0,0 +1,8 @@
#pragma once
#include "RegulatorRecovery.h"
namespace mesh { namespace power {
volatile RecoveryRecord& platformRecoveryRecord();
ResetKind platformResetKind();
bool platformRecoverySupported();
}}
+206
View File
@@ -0,0 +1,206 @@
#pragma once
#include <stddef.h>
#include <stdint.h>
namespace mesh { namespace power {
constexpr uint8_t RECOVERY_MCU = 1;
constexpr uint8_t RECOVERY_RADIO = 2;
constexpr uint8_t RECOVERY_DOMAINS = RECOVERY_MCU | RECOVERY_RADIO;
constexpr uint8_t REGULATOR_CRASH_THRESHOLD = 3;
constexpr uint32_t REGULATOR_HEALTHY_MS = 300000;
constexpr uint32_t REGULATOR_BATTERY_FRESH_MS = 60000;
enum class ResetKind : uint8_t { Cold, Intentional, Wake, Crash, Unknown };
// Keep this POD in reset-retained RAM, never in flash. The checksum is committed
// last; an interrupted write or an uninitialised retained region cannot add a
// crash to the next boot. Only the sticky fallback mask needs flash storage.
struct RecoveryRecord {
uint32_t magic;
uint16_t version;
uint16_t bytes;
uint32_t startedAt;
uint32_t heartbeatAt;
uint32_t batteryAt;
uint8_t activeDcdcMask;
uint8_t fallbackMask;
uint8_t mcuCrashes;
uint8_t radioCrashes;
uint8_t batteryPercent;
uint8_t flags;
uint16_t reserved;
uint32_t crc;
};
static_assert(sizeof(RecoveryRecord) <= 64, "regulator recovery retention budget");
static_assert(sizeof(RecoveryRecord) == 32, "regulator recovery record layout");
static_assert(offsetof(RecoveryRecord, crc) == 28, "regulator recovery CRC layout");
class RecoveryPolicy {
public:
explicit RecoveryPolicy(volatile RecoveryRecord& retained) : retained_(retained), state_{} {}
// Read the previous boot exactly once, before arm(). A cold/intentional/wake
// or unclassified reset breaks a streak, but never undoes a saved fallback.
void begin(ResetKind reset, uint8_t savedFallbackMask) {
RecoveryRecord previous{};
read(previous);
const bool valid = isValid(previous);
state_ = RecoveryRecord{};
state_.magic = kMagic;
state_.version = kVersion;
state_.bytes = sizeof(RecoveryRecord);
state_.fallbackMask = savedFallbackMask & RECOVERY_DOMAINS;
if (valid) {
state_.fallbackMask |= previous.fallbackMask;
if (reset == ResetKind::Crash && eligible(previous)) {
state_.mcuCrashes = nextCount(previous, RECOVERY_MCU, previous.mcuCrashes);
state_.radioCrashes = nextCount(previous, RECOVERY_RADIO, previous.radioCrashes);
if (state_.mcuCrashes >= REGULATOR_CRASH_THRESHOLD)
state_.fallbackMask |= RECOVERY_MCU;
if (state_.radioCrashes >= REGULATOR_CRASH_THRESHOLD)
state_.fallbackMask |= RECOVERY_RADIO;
}
}
// Leave the new boot disarmed until its actual selected modes and measured
// battery are known. A crash before that point cannot extend the streak.
commit();
}
// Call before enabling either DC-DC regulator / starting the radio. Each bit
// describes an actually selected DC-DC domain, not mere board capability.
void arm(uint8_t activeDcdcMask, uint32_t now, uint8_t percent, bool valid) {
state_.activeDcdcMask = activeDcdcMask & RECOVERY_DOMAINS & ~state_.fallbackMask;
state_.startedAt = state_.heartbeatAt = now;
state_.flags = kArmed;
setBattery(now, percent, valid);
commit();
}
// Heartbeats must not refresh the battery timestamp: a stale/unknown reading
// must never turn a USB-only device or a discharged battery into an event.
void heartbeat(uint32_t now) {
state_.heartbeatAt = now;
if ((state_.flags & kArmed) && !(state_.flags & kHealthy) &&
elapsed(now, state_.startedAt) >= REGULATOR_HEALTHY_MS) {
state_.mcuCrashes = state_.radioCrashes = 0;
state_.flags |= kHealthy;
}
commit();
}
void updateBattery(uint32_t now, uint8_t percent, bool valid) {
setBattery(now, percent, valid);
commit();
}
void updateActiveDomains(uint8_t activeDcdcMask) {
state_.activeDcdcMask = activeDcdcMask & RECOVERY_DOMAINS & ~state_.fallbackMask;
commit();
}
// Convenience for callers that really take a fresh battery measurement each
// tick. Use heartbeat() separately when measurements are less frequent.
void tick(uint32_t now, uint8_t percent, bool valid) {
setBattery(now, percent, valid);
heartbeat(now);
}
void markCleanReset() {
state_.flags = (state_.flags & ~kArmed) | kClean;
state_.mcuCrashes = state_.radioCrashes = 0;
commit();
}
// A deliberate user mode change resets only that domain's learned fallback.
void clearDomain(uint8_t domains) {
domains &= RECOVERY_DOMAINS;
state_.fallbackMask &= ~domains;
if (domains & RECOVERY_MCU) state_.mcuCrashes = 0;
if (domains & RECOVERY_RADIO) state_.radioCrashes = 0;
commit();
}
uint8_t fallbackMask() const { return state_.fallbackMask; }
uint8_t crashCount(uint8_t domain) const {
return domain == RECOVERY_MCU ? state_.mcuCrashes :
domain == RECOVERY_RADIO ? state_.radioCrashes : 0;
}
private:
static constexpr uint32_t kMagic = 0x52504752; // RGPR
static constexpr uint16_t kVersion = 1;
static constexpr uint8_t kArmed = 1;
static constexpr uint8_t kClean = 2;
static constexpr uint8_t kBatteryValid = 4;
static constexpr uint8_t kHealthy = 8;
static constexpr uint8_t kFlags = kArmed | kClean | kBatteryValid | kHealthy;
volatile RecoveryRecord& retained_;
RecoveryRecord state_;
static uint32_t elapsed(uint32_t now, uint32_t then) { return now - then; }
static uint32_t checksum(const RecoveryRecord& record) {
uint32_t crc = 0xffffffff;
const uint8_t* bytes = reinterpret_cast<const uint8_t*>(&record);
for (size_t i = 0; i < offsetof(RecoveryRecord, crc); ++i) {
crc ^= bytes[i];
for (uint8_t bit = 0; bit < 8; ++bit)
crc = (crc >> 1) ^ (0xedb88320u & (0u - (crc & 1u)));
}
return ~crc;
}
static bool isValid(const RecoveryRecord& record) {
return record.magic == kMagic && record.version == kVersion &&
record.bytes == sizeof(RecoveryRecord) && record.reserved == 0 &&
!(record.activeDcdcMask & ~RECOVERY_DOMAINS) &&
!(record.fallbackMask & ~RECOVERY_DOMAINS) &&
!(record.flags & ~kFlags) && record.batteryPercent <= 100 &&
record.mcuCrashes <= REGULATOR_CRASH_THRESHOLD &&
record.radioCrashes <= REGULATOR_CRASH_THRESHOLD &&
record.crc == checksum(record);
}
static bool eligible(const RecoveryRecord& record) {
return (record.flags & (kArmed | kClean | kBatteryValid)) == (kArmed | kBatteryValid) &&
record.batteryPercent >= 50 &&
elapsed(record.heartbeatAt, record.batteryAt) <= REGULATOR_BATTERY_FRESH_MS;
}
static uint8_t nextCount(const RecoveryRecord& previous, uint8_t domain, uint8_t count) {
if (!(previous.activeDcdcMask & domain) || (previous.fallbackMask & domain)) return 0;
return count < REGULATOR_CRASH_THRESHOLD ? count + 1 : REGULATOR_CRASH_THRESHOLD;
}
void setBattery(uint32_t now, uint8_t percent, bool valid) {
state_.batteryAt = now;
state_.batteryPercent = percent <= 100 ? percent : 0;
if (valid && percent <= 100) state_.flags |= kBatteryValid;
else state_.flags &= ~kBatteryValid;
}
void read(RecoveryRecord& result) const {
const volatile uint8_t* source = reinterpret_cast<const volatile uint8_t*>(&retained_);
uint8_t* target = reinterpret_cast<uint8_t*>(&result);
for (size_t i = 0; i < sizeof(result); ++i) target[i] = source[i];
}
void commit() {
state_.crc = checksum(state_);
// Invalidate before changing any payload, then publish its CRC last. No
// flash writes, heap allocation, interrupt dependencies, or platform APIs.
retained_.magic = 0;
retained_.crc = 0;
volatile uint8_t* target = reinterpret_cast<volatile uint8_t*>(&retained_);
const uint8_t* source = reinterpret_cast<const uint8_t*>(&state_);
for (size_t i = sizeof(state_.magic); i < offsetof(RecoveryRecord, crc); ++i)
target[i] = source[i];
retained_.magic = state_.magic;
retained_.crc = state_.crc;
}
};
}} // namespace mesh::power
+264
View File
@@ -0,0 +1,264 @@
#include "RegulatorSettings.h"
#include "RegulatorPlatform.h"
#include "NRF52VoltageRules.h"
#include "NRF52VoltagePolicy.h"
#include "PersistentStoreFormat.h"
#include <Arduino.h>
#include <Dispatcher.h>
#include <stdio.h>
#include <string.h>
namespace mesh { namespace power {
namespace {
MainBoard* main_board = nullptr;
Radio* main_radio = nullptr;
FILESYSTEM* settings_fs = nullptr;
RegulatorMode requested[2] = {RegulatorMode::Default, RegulatorMode::Default};
uint8_t saved_fallback = 0;
bool initialized = false;
bool fallback_saved = true;
uint32_t last_service = 0, last_battery = 0, last_save = 0;
RecoveryPolicy& recovery() {
static RecoveryPolicy policy(platformRecoveryRecord());
return policy;
}
const char* const path = "/regulator_v1";
const char* const temporary = "/regulator_v1.tmp";
const char* const backup = "/regulator_v1.bak";
void encode(RegulatorMode mcu, RegulatorMode radio, uint8_t fallback, uint8_t (&data)[12]) {
memcpy(data, "REG1", 4);
data[4] = uint8_t(mcu); data[5] = uint8_t(radio);
data[6] = fallback; data[7] = 0;
storage::writeLE32(data + 8, storage::updateCRC32(0xffffffffUL, data, 8));
}
bool readFile(const char* name, uint8_t (&data)[12]) {
if (!settings_fs) return false;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
File file(*settings_fs);
if (!file.open(name, FILE_O_READ)) return false;
#else
File file = settings_fs->open(name, "r");
if (!file) return false;
#endif
const bool complete = file.size() == sizeof(data)
&& file.read(data, sizeof(data)) == sizeof(data);
file.close();
return complete && memcmp(data, "REG1", 4) == 0
&& data[4] <= 2 && data[5] <= 2 && data[6] <= RECOVERY_DOMAINS
&& data[7] == 0 && storage::readLE32(data + 8)
== storage::updateCRC32(0xffffffffUL, data, 8);
}
bool save(RegulatorMode mcu, RegulatorMode radio, uint8_t fallback) {
if (!settings_fs) return false;
settings_fs->remove(temporary);
if (settings_fs->exists(temporary)) return false;
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
File file(*settings_fs);
if (!file.open(temporary, FILE_O_WRITE)) return false;
#elif defined(RP2040_PLATFORM)
File file = settings_fs->open(temporary, "w");
if (!file) return false;
#else
File file = settings_fs->open(temporary, "w", true);
if (!file) return false;
#endif
uint8_t data[12], checked[12];
encode(mcu, radio, fallback, data);
const bool written = file.write(data, sizeof(data)) == sizeof(data);
file.close();
if (!written || !readFile(temporary, checked)
|| memcmp(data, checked, sizeof(data)) != 0) return false;
if (settings_fs->exists(backup) && !settings_fs->remove(backup)) return false;
if (settings_fs->exists(path) && !settings_fs->rename(path, backup)) return false;
if (!settings_fs->rename(temporary, path)) {
if (settings_fs->exists(backup)) settings_fs->rename(backup, path);
return false;
}
settings_fs->remove(backup);
saved_fallback = fallback;
return true;
}
uint8_t readBatteryPercent(bool& valid) {
uint16_t samples[3];
for (unsigned i = 0; i < 3; ++i) {
samples[i] = main_board->getBattMilliVolts();
if (i != 2) delay(5);
}
if (samples[0] > samples[1]) { auto v = samples[0]; samples[0] = samples[1]; samples[1] = v; }
if (samples[1] > samples[2]) { auto v = samples[1]; samples[1] = samples[2]; samples[2] = v; }
if (samples[0] > samples[1]) samples[1] = samples[0];
const uint16_t mv = samples[1];
valid = mv >= 2000 && mv <= 5500;
if (!valid) return 0;
#if defined(NRF52_PLATFORM) && defined(NRF52_POWER_MANAGEMENT)
// Board ADC methods already return calibrated millivolts.
return configuredBatteryPercent(mv);
#else
return batteryPercent(mv, 3000, 4200);
#endif
}
RegulatorMode effective(bool mcu) {
RegulatorMode mode = requested[mcu ? 0 : 1];
if (mode == RegulatorMode::Default) mode = mcu
? main_board->defaultMcuRegulatorMode() : main_radio->defaultRegulatorMode();
const uint8_t bit = mcu ? RECOVERY_MCU : RECOVERY_RADIO;
if (mode == RegulatorMode::Dcdc && (recovery().fallbackMask() & bit)) mode = RegulatorMode::Ldo;
return mode;
}
}
const char* regulatorModeName(RegulatorMode mode) {
return mode == RegulatorMode::Dcdc ? "dcdc" : mode == RegulatorMode::Ldo ? "ldo" : "auto";
}
uint8_t configuredRegulatorModes(bool mcu) {
if (!main_board || !main_radio) return 0;
return mcu ? main_board->mcuRegulatorModes() : main_radio->regulatorModes();
}
RegulatorMode requestedRegulatorMode(bool mcu) { return requested[mcu ? 0 : 1]; }
RegulatorMode activeRegulatorMode(bool mcu) {
if (!main_board || !main_radio) return RegulatorMode::Default;
return mcu ? main_board->activeMcuRegulatorMode() : main_radio->activeRegulatorMode();
}
const char* activeRegulatorModeName(bool mcu) {
const auto mode = activeRegulatorMode(mcu);
return mode == RegulatorMode::Default ? "unknown" : regulatorModeName(mode);
}
bool regulatorFallbackActive(bool mcu) {
return initialized && (recovery().fallbackMask() & (mcu ? RECOVERY_MCU : RECOVERY_RADIO));
}
bool regulatorRecoveryAvailable() { return initialized && platformRecoverySupported(); }
bool beginRegulatorSettings(MainBoard& board, Radio& radio, FILESYSTEM* fs) {
if (initialized) return true;
main_board = &board; main_radio = &radio; settings_fs = fs;
if (!board.mcuRegulatorModes() && !radio.regulatorModes()) return true;
uint8_t data[12];
bool loaded = readFile(path, data);
if (!loaded && readFile(backup, data)) {
loaded = true;
settings_fs->remove(path);
settings_fs->rename(backup, path);
}
if (loaded) {
requested[0] = RegulatorMode(data[4]); requested[1] = RegulatorMode(data[5]);
saved_fallback = data[6];
}
recovery().begin(platformRecoverySupported() ? platformResetKind() : ResetKind::Unknown,
saved_fallback);
initialized = true;
uint8_t active_dcdc = 0;
bool valid = false;
const uint8_t percent = readBatteryPercent(valid);
const uint32_t now = millis();
const auto cpu_mode = effective(true), radio_mode = effective(false);
if (configuredRegulatorModes(true) && cpu_mode == RegulatorMode::Dcdc) active_dcdc |= RECOVERY_MCU;
if (configuredRegulatorModes(false) && radio_mode == RegulatorMode::Dcdc) active_dcdc |= RECOVERY_RADIO;
recovery().arm(active_dcdc, now, percent, valid);
fallback_saved = recovery().fallbackMask() == saved_fallback
|| save(requested[0], requested[1], recovery().fallbackMask());
bool okay = true;
if (configuredRegulatorModes(true)) {
if (!board.applyMcuRegulator(cpu_mode)) {
active_dcdc &= ~RECOVERY_MCU;
recovery().arm(active_dcdc, now, percent, valid);
okay = false;
}
}
if (configuredRegulatorModes(false) && !radio.configureBootRegulator(radio_mode)) {
active_dcdc &= ~RECOVERY_RADIO;
recovery().updateActiveDomains(active_dcdc);
okay = false;
}
last_service = last_battery = last_save = now;
return okay;
}
void serviceRegulatorSettings() {
if (!initialized) return;
const uint32_t now = millis();
if (uint32_t(now - last_service) < 1000) return;
last_service = now;
const uint8_t active_dcdc =
(activeRegulatorMode(true) == RegulatorMode::Dcdc ? RECOVERY_MCU : 0)
| (activeRegulatorMode(false) == RegulatorMode::Dcdc ? RECOVERY_RADIO : 0);
recovery().updateActiveDomains(active_dcdc);
if (uint32_t(now - last_battery) >= 30000) {
bool valid = false;
const uint8_t percent = readBatteryPercent(valid);
last_battery = millis();
recovery().updateBattery(last_battery, percent, valid);
}
recovery().heartbeat(millis());
if (!fallback_saved && uint32_t(now - last_save) >= 30000) {
last_save = now;
fallback_saved = save(requested[0], requested[1], recovery().fallbackMask());
}
}
bool handleRegulatorCommand(const char* command, char* reply, size_t capacity) {
const bool get = strncmp(command, "get ", 4) == 0;
const bool set = strncmp(command, "set ", 4) == 0;
if (!get && !set) return false;
const char* key = command + 4;
bool mcu;
size_t length;
if (strncmp(key, "mcu.regulator", 13) == 0) { mcu = true; length = 13; }
else if (strncmp(key, "radio.regulator", 15) == 0) { mcu = false; length = 15; }
else return false;
if (key[length] != 0 && key[length] != ' ') return false;
if (!initialized || !configuredRegulatorModes(mcu)) {
snprintf(reply, capacity, "Error: %s regulator selection unsupported", mcu ? "MCU" : "radio");
return true;
}
const char* value = key + length;
if (get) {
if (*value) snprintf(reply, capacity, "Error: unexpected argument");
else snprintf(reply, capacity, "> %s; active %s; fallback %s; crashes %u; recovery %s%s",
regulatorModeName(requestedRegulatorMode(mcu)), activeRegulatorModeName(mcu),
regulatorFallbackActive(mcu) ? "ldo" : "off",
unsigned(recovery().crashCount(mcu ? RECOVERY_MCU : RECOVERY_RADIO)),
regulatorRecoveryAvailable() ? "on" : "unsupported",
fallback_saved ? "" : "; fallback not saved");
return true;
}
while (*value == ' ') ++value;
RegulatorMode mode;
if (strcmp(value, "auto") == 0) mode = RegulatorMode::Default;
else if (strcmp(value, "dcdc") == 0) mode = RegulatorMode::Dcdc;
else if (strcmp(value, "ldo") == 0) mode = RegulatorMode::Ldo;
else { snprintf(reply, capacity, "Error: use set %s.regulator auto|dcdc|ldo", mcu ? "mcu" : "radio"); return true; }
if (!regulatorModeSupported(configuredRegulatorModes(mcu), mode)) {
snprintf(reply, capacity, "Error: regulator mode unsupported by this hardware");
return true;
}
const uint8_t domain = mcu ? RECOVERY_MCU : RECOVERY_RADIO;
const uint8_t fallback = recovery().fallbackMask() & ~domain;
const auto cpu = mcu ? mode : requested[0], rf = mcu ? requested[1] : mode;
if (!save(cpu, rf, fallback)) {
snprintf(reply, capacity, "Error: regulator setting save failed");
return true;
}
requested[mcu ? 0 : 1] = mode;
recovery().clearDomain(domain);
fallback_saved = true;
snprintf(reply, capacity, "OK - %s regulator %s saved; reboot required", mcu ? "MCU" : "radio", regulatorModeName(mode));
return true;
}
void markRegulatorCleanReset() {
if (initialized) recovery().markCleanReset();
else {
RecoveryPolicy policy(platformRecoveryRecord());
policy.begin(ResetKind::Intentional, 0);
policy.markCleanReset();
}
}
}} // namespace mesh::power
extern "C" void meshRegulatorCleanReset() {
// Intentional resets can happen before the settings/filesystem is ready.
mesh::power::markRegulatorCleanReset();
}
+23
View File
@@ -0,0 +1,23 @@
#pragma once
#include <helpers/IdentityStore.h>
#include "RegulatorMode.h"
namespace mesh {
class MainBoard;
class Radio;
namespace power {
// Called once after the primary filesystem and board ADC are ready, before
// display/radio/wireless initialization. Choices never alter an active radio.
bool beginRegulatorSettings(MainBoard& board, Radio& radio, FILESYSTEM* fs);
void serviceRegulatorSettings();
bool handleRegulatorCommand(const char* command, char* reply, size_t capacity);
const char* regulatorModeName(RegulatorMode mode);
uint8_t configuredRegulatorModes(bool mcu);
RegulatorMode requestedRegulatorMode(bool mcu);
RegulatorMode activeRegulatorMode(bool mcu);
const char* activeRegulatorModeName(bool mcu);
bool regulatorFallbackActive(bool mcu);
bool regulatorRecoveryAvailable();
}}
extern "C" void meshRegulatorCleanReset();
+4
View File
@@ -35,6 +35,9 @@
#if defined(NRF52_PLATFORM) && defined(USE_TINYUSB)
#include <Adafruit_TinyUSB.h>
#endif
#if defined(NRF52_PLATFORM)
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
#endif
#if MESH_ESP32_HWCDC_SESSION_GUARD
#include "UsbAsciiBinarySwitch.h"
#endif
@@ -1907,6 +1910,7 @@ extern "C" void meshTinyUsbCdcLineStateChanged(uint8_t instance, bool dtr,
cdc_line_coding_t coding;
tud_cdc_get_line_coding(&coding);
if (coding.bit_rate == 1200) {
if (meshRegulatorCleanReset) meshRegulatorCleanReset();
TinyUSB_Port_EnterDFU();
}
} else if (instance == 0) {
+4 -1
View File
@@ -21,6 +21,7 @@ static const char* const WC_ALLOWED_SET_KEYS[] = {
"name", "bluetooth.name", "bluetooth.mac", "bluetooth.stealth", "lost.reply",
"lat", "lon", "radio", "tx", "af", "rxdelay", "txdelay",
"cad", "radio.rxgain", "radio.fem.rxgain", "radio.fem.txgain", "radio.rxps", "powersaving",
"mcu.regulator", "radio.regulator",
"repeat",
"display.mode", "display.timeout", "display.usb.mode", "display.usb.timeout",
"display.inbox", "display.rotation",
@@ -70,7 +71,9 @@ static inline bool wcSetKeyRequiresReboot(const char* key) {
return key != NULL
&& (wcIsEspNowChannelKey(key)
|| strcmp(key, "bluetooth.mac") == 0
|| strcmp(key, "bluetooth.stealth") == 0);
|| strcmp(key, "bluetooth.stealth") == 0
|| strcmp(key, "mcu.regulator") == 0
|| strcmp(key, "radio.regulator") == 0);
}
// The admin password maps to the top-level `password` command, not a setter, so
File diff suppressed because it is too large. Load diff
+15 -1
View File
@@ -13,6 +13,7 @@ static_assert(sizeof(WEBCONFIG_AP_PREFIX) <= 28,
#include <ArduinoJson.h>
#include <helpers/ui/DisplayPowerSettings.h>
#include <helpers/ui/DisplayBuildFlags.h>
#include <helpers/RegulatorSettings.h>
#include <Preferences.h>
#include <esp_wifi.h>
#include <esp_arduino_version.h>
@@ -2331,11 +2332,23 @@ void WebConfigServer::handleLogout(AsyncWebServerRequest* req) {
req->send(res);
}
static void webConfigRegulatorsSnapshot(JsonObject regulators) {
regulators["recovery"] = mesh::power::regulatorRecoveryAvailable();
for (uint8_t domain = 0; domain < 2; ++domain) {
const bool mcu = domain == 0;
JsonObject entry = regulators.createNestedObject(mcu ? "mcu" : "radio");
entry["modes"] = mesh::power::configuredRegulatorModes(mcu);
entry["requested"] = mesh::power::regulatorModeName(mesh::power::requestedRegulatorMode(mcu));
entry["active"] = mesh::power::activeRegulatorModeName(mcu);
entry["fallback"] = mesh::power::regulatorFallbackActive(mcu);
}
}
void WebConfigServer::handleConfigGet(AsyncWebServerRequest* req) {
if (_mode == MODE_OFF) { req->send(503); return; }
if (!checkAuth(req)) { req->send(401, "application/json", "{\"error\":\"auth\"}"); return; }
DynamicJsonDocument doc(6144);
DynamicJsonDocument doc(6656);
{
WCLock lock(_mux);
NodeSnapshot node = {};
@@ -2400,6 +2413,7 @@ void WebConfigServer::handleConfigGet(AsyncWebServerRequest* req) {
#if defined(MESH_PRIMARY_ESPNOW) && MESH_PRIMARY_ESPNOW
wifi["espnow_channel"] = mesh::wifi::loadConfiguredEspNowChannel();
#endif
webConfigRegulatorsSnapshot(doc.createNestedObject("regulators"));
#ifdef WITH_MQTT_BRIDGE
MQTTPrefs* obs = static_cast<MQTTPrefs*>(_mqtt_prefs);
+3
View File
@@ -18,6 +18,7 @@
#include "OtaSelf.h" // SelfFwInfo / ota_self_firmware (running-image base_hash gate)
#endif
#elif defined(NRF52_PLATFORM)
extern "C" void meshRegulatorCleanReset() __attribute__((weak));
#include "OtaVerify.h"
#include "OtaSelf.h"
#include "OtaFlashLayout_nrf52.h"
@@ -498,6 +499,7 @@ static bool ota_nrf52_clear_reset_reasons() {
}
void ota_reboot_to_apply() { // public: set the apply magic + reset (does not return)
if (meshRegulatorCleanReset) meshRegulatorCleanReset();
uint8_t stage_handoff = GPREGRET2_OTA_STAGE_LEGACY;
#if defined(OTA_RAK_AUTO_STORE) || defined(OTA_TOWER_AUTO_STORE)
if (g_nrf52_apply_store) {
@@ -546,6 +548,7 @@ void ota_reboot_to_apply() { // public: set the apply magic +
}
void ota_reboot_to_bootloader_update() {
if (meshRegulatorCleanReset) meshRegulatorCleanReset();
uint8_t source = GPREGRET2_OTA_STAGE_QSPI;
#if defined(OTA_INTERNAL_BOOTLOADER_UPDATE)
source = GPREGRET2_OTA_STAGE_EXPANDED;
+18 -5
View File
@@ -1,6 +1,7 @@
#pragma once
#include <RadioLib.h>
#include "RadioRegulatorState.h"
#include "MeshCore.h"
#include "RXPowerSaving.h"
#include "LR1110RxRecovery.h"
@@ -19,6 +20,9 @@
#endif
class CustomLR1110 : public LR1110 {
// Existing LR11xx recipes already require the fitted DC/DC network.
mesh::power::RadioRegulatorState _regulator{mesh::power::REGULATOR_DCDC | mesh::power::REGULATOR_LDO,
mesh::power::RegulatorMode::Dcdc};
uint32_t _preambleMillis = 66;
uint32_t _maxPayloadMillis = 3934;
uint32_t _activityAt = 0;
@@ -33,6 +37,11 @@ class CustomLR1110 : public LR1110 {
public:
CustomLR1110(Module *mod) : LR1110(mod) { }
uint8_t regulatorModes() const { return _regulator.modes(); }
mesh::power::RegulatorMode defaultRegulatorMode() const { return _regulator.defaultMode(); }
bool configureBootRegulator(mesh::power::RegulatorMode mode) { return _regulator.configureBoot(mode); }
mesh::power::RegulatorMode activeRegulatorMode() const { return _regulator.activeMode(); }
void setRfSwitchTable(const uint32_t (&pins)[Module::RFSWITCH_MAX_PINS],
const Module::RfSwitchMode_t table[]) {
_morse_rf_switch.capture(this->mod, pins, table);
@@ -40,12 +49,14 @@ class CustomLR1110 : public LR1110 {
}
int16_t beginMorseReceive(float frequency_mhz) {
const auto regulator = _regulator.beginInitialization();
_profileSwitch.invalidate();
_profilePacketDirty = false;
int16_t status = LR1110::beginGFSK(frequency_mhz,
mesh::MorseRx::ReceiverBitRateKbps, 0.6f, 4.8f, 2, 16, tcxoVoltage);
if (status == RADIOLIB_ERR_NONE) status = applyMeshCoreTcxoDelay();
if (status == RADIOLIB_ERR_NONE) status = setRegulatorDCDC();
if (status == RADIOLIB_ERR_NONE) status = regulator == mesh::power::RegulatorMode::Ldo
? setRegulatorLDO() : setRegulatorDCDC();
if (status == RADIOLIB_ERR_NONE) status = _morse_rf_switch.restore(
[this](uint8_t enable, const uint8_t* modes) {
return setDioAsRfSwitch(enable, modes[0], modes[1], modes[2],
@@ -60,7 +71,7 @@ class CustomLR1110 : public LR1110 {
}
// Infinite continuous RX samples envelope power independently of the
// FSK packet detector; no direct-data pin or TX/PA activation is used.
return status;
return _regulator.finishInitialization(status);
}
void setProfileSwitchOptimization(bool enabled) {
@@ -233,14 +244,16 @@ class CustomLR1110 : public LR1110 {
int16_t begin(float freq = 434.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7,
uint8_t syncWord = RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, int8_t power = 10,
uint16_t preambleLength = 8, float tcxoVoltage = 1.6) {
const auto regulator = _regulator.beginInitialization();
_profileSwitch.invalidate();
_profilePacketDirty = false;
int16_t state = LR1110::begin(freq, bw, sf, cr, syncWord, power, preambleLength,
tcxoVoltage);
if (state == RADIOLIB_ERR_NONE) state = applyMeshCoreTcxoDelay();
// RadioLib begin() defaults to LDO; use the LR1110 DC/DC regulator.
if (state == RADIOLIB_ERR_NONE) state = setRegulatorDCDC();
return state;
// Native begin() resets to LDO; restore the boot choice on every reset.
if (state == RADIOLIB_ERR_NONE) state = regulator == mesh::power::RegulatorMode::Ldo
? setRegulatorLDO() : setRegulatorDCDC();
return _regulator.finishInitialization(state);
}
int16_t applyMeshCoreTcxoDelay() {
@@ -13,6 +13,16 @@ class CustomLR1110Wrapper : public RadioLibWrapper {
DeepInitCallback _deep_init;
public:
uint8_t regulatorModes() const override { return static_cast<CustomLR1110*>(_radio)->regulatorModes(); }
mesh::power::RegulatorMode defaultRegulatorMode() const override {
return static_cast<CustomLR1110*>(_radio)->defaultRegulatorMode();
}
bool configureBootRegulator(mesh::power::RegulatorMode mode) override {
return static_cast<CustomLR1110*>(_radio)->configureBootRegulator(mode);
}
mesh::power::RegulatorMode activeRegulatorMode() const override {
return static_cast<CustomLR1110*>(_radio)->activeRegulatorMode();
}
~CustomLR1110Wrapper() { unregisterInterruptAction(); }
CustomLR1110Wrapper(CustomLR1110& radio, mesh::MainBoard& board)
: RadioLibWrapper(radio, board), _deep_init(NULL) { }
+18 -5
View File
@@ -1,11 +1,15 @@
#pragma once
#include <RadioLib.h>
#include "RadioRegulatorState.h"
#include "MeshCore.h"
#include "LR11x0MorseRfSwitch.h"
#include "../MorseRx.h"
class CustomLR1121 : public LR1121 {
// Existing LR11xx recipes already require the fitted DC/DC network.
mesh::power::RadioRegulatorState _regulator{mesh::power::REGULATOR_DCDC | mesh::power::REGULATOR_LDO,
mesh::power::RegulatorMode::Dcdc};
uint32_t _preambleMillis = 66;
uint32_t _maxPayloadMillis = 3934;
uint32_t _activityAt = 0;
@@ -18,6 +22,11 @@ class CustomLR1121 : public LR1121 {
public:
CustomLR1121(Module *mod) : LR1121(mod) { }
uint8_t regulatorModes() const { return _regulator.modes(); }
mesh::power::RegulatorMode defaultRegulatorMode() const { return _regulator.defaultMode(); }
bool configureBootRegulator(mesh::power::RegulatorMode mode) { return _regulator.configureBoot(mode); }
mesh::power::RegulatorMode activeRegulatorMode() const { return _regulator.activeMode(); }
int16_t setProfileProtocol(uint8_t syncWord) {
int16_t status = LR1121::setSyncWord(syncWord);
if (status == RADIOLIB_ERR_NONE) status = LR1121::setCRC(2);
@@ -33,9 +42,11 @@ class CustomLR1121 : public LR1121 {
}
int16_t beginMorseReceive(float frequency_mhz) {
const auto regulator = _regulator.beginInitialization();
int16_t status = LR1121::beginGFSK(frequency_mhz,
mesh::MorseRx::ReceiverBitRateKbps, 0.6f, 4.8f, 2, 16, tcxoVoltage);
if (status == RADIOLIB_ERR_NONE) status = setRegulatorDCDC();
if (status == RADIOLIB_ERR_NONE) status = regulator == mesh::power::RegulatorMode::Ldo
? setRegulatorLDO() : setRegulatorDCDC();
if (status == RADIOLIB_ERR_NONE) status = _morse_rf_switch.restore(
[this](uint8_t enable, const uint8_t* modes) {
return setDioAsRfSwitch(enable, modes[0], modes[1], modes[2],
@@ -46,17 +57,19 @@ class CustomLR1121 : public LR1121 {
syncWordLength = preambleDetLength = 0;
status = LR1121::setCRC(0);
}
return status;
return _regulator.finishInitialization(status);
}
int16_t begin(float freq = 434.0, float bw = 125.0, uint8_t sf = 9, uint8_t cr = 7,
uint8_t syncWord = RADIOLIB_LR11X0_LORA_SYNC_WORD_PRIVATE, int8_t power = 10,
uint16_t preambleLength = 8, float tcxoVoltage = 1.6) {
const auto regulator = _regulator.beginInitialization();
int16_t state = LR1121::begin(freq, bw, sf, cr, syncWord, power, preambleLength,
tcxoVoltage);
// RadioLib begin() defaults to LDO; use the LR1121 DC/DC regulator.
if (state == RADIOLIB_ERR_NONE) state = setRegulatorDCDC();
return state;
// Native begin() resets to LDO; restore the boot choice on every reset.
if (state == RADIOLIB_ERR_NONE) state = regulator == mesh::power::RegulatorMode::Ldo
? setRegulatorLDO() : setRegulatorDCDC();
return _regulator.finishInitialization(state);
}
size_t getPacketLength(bool update) override {
@@ -8,6 +8,16 @@ class CustomLR1121Wrapper : public RadioLibWrapper {
using DeepInitCallback = bool (*)();
DeepInitCallback _deep_init = nullptr;
public:
uint8_t regulatorModes() const override { return static_cast<CustomLR1121*>(_radio)->regulatorModes(); }
mesh::power::RegulatorMode defaultRegulatorMode() const override {
return static_cast<CustomLR1121*>(_radio)->defaultRegulatorMode();
}
bool configureBootRegulator(mesh::power::RegulatorMode mode) override {
return static_cast<CustomLR1121*>(_radio)->configureBootRegulator(mode);
}
mesh::power::RegulatorMode activeRegulatorMode() const override {
return static_cast<CustomLR1121*>(_radio)->activeRegulatorMode();
}
~CustomLR1121Wrapper() { unregisterInterruptAction(); }
CustomLR1121Wrapper(CustomLR1121& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool supportsMorseTx() const override { return true; }
+26 -8
View File
@@ -1,6 +1,7 @@
#pragma once
#include <RadioLib.h>
#include "RadioRegulatorState.h"
#include "MeshCore.h"
#include "LR2021Band.h"
#include "../MorseRx.h"
@@ -21,6 +22,13 @@
#endif
class CustomLR2021 : public LR2021 {
#if LR2021_USE_DCDC
mesh::power::RadioRegulatorState _regulator{mesh::power::REGULATOR_DCDC | mesh::power::REGULATOR_LDO,
mesh::power::RegulatorMode::Dcdc};
#else
// An LDO-only layout must not enable SIMO without its external inductor.
mesh::power::RadioRegulatorState _regulator{mesh::power::REGULATOR_LDO, mesh::power::RegulatorMode::Ldo};
#endif
uint32_t _preambleMillis = 66;
uint32_t _maxPayloadMillis = 3934;
uint32_t _activityAt = 0;
@@ -130,6 +138,11 @@ class CustomLR2021 : public LR2021 {
public:
CustomLR2021(Module *mod) : LR2021(mod) { irqDioNum = LR2021_IRQ_DIO; }
uint8_t regulatorModes() const { return _regulator.modes(); }
mesh::power::RegulatorMode defaultRegulatorMode() const { return _regulator.defaultMode(); }
bool configureBootRegulator(mesh::power::RegulatorMode mode) { return _regulator.configureBoot(mode); }
mesh::power::RegulatorMode activeRegulatorMode() const { return _regulator.activeMode(); }
#if LR2021_TX_PREARM_MS > 0
using LR2021::standby;
int16_t standby() override {
@@ -164,13 +177,16 @@ class CustomLR2021 : public LR2021 {
}
int16_t beginMorseReceive(float frequency_mhz) {
const auto regulator = _regulator.beginInitialization();
const uint8_t gain = mesh::lr2021::isHighBand(frequency_mhz) ? gainModeHf : gainModeLf;
int16_t status = LR2021::beginGFSK(frequency_mhz,
mesh::MorseRx::ReceiverBitRateKbps, 0.6f, 4.8f, 2, 16, tcxoVoltage);
// Native modem initialization resets the regulator to LDO. Verified
// SIMO boards must restore DC/DC before admitting RX after any reset.
// Native initialization resets to LDO. SIMO is allowed only on verified
// layouts; an explicit LDO choice on those boards is checked as well.
if (status == RADIOLIB_ERR_NONE && regulator == mesh::power::RegulatorMode::Dcdc)
status = setRegulatorDCDC();
#if LR2021_USE_DCDC
if (status == RADIOLIB_ERR_NONE) status = setRegulatorDCDC();
else if (status == RADIOLIB_ERR_NONE) status = setRegulatorLDO();
#endif
if (status == RADIOLIB_ERR_NONE) status = restoreRfSwitchTable();
if (status == RADIOLIB_ERR_NONE) status = LR2021::setRxBoostedGainMode(gain);
@@ -183,10 +199,11 @@ class CustomLR2021 : public LR2021 {
status = LR2021::setCRC(0);
}
// launchMode() retains this chip's band-aware RX RF switch selection.
return status;
return _regulator.finishInitialization(status);
}
bool std_init(SPIClass* spi = NULL) {
const auto regulator = _regulator.beginInitialization();
#ifdef LR2021_TCXO_VOLTAGE
const float tcxo = LR2021_TCXO_VOLTAGE;
#else
@@ -220,15 +237,16 @@ class CustomLR2021 : public LR2021 {
if (status != RADIOLIB_ERR_NONE) {
mesh::usbLoggingPort().print("ERROR: radio init failed: ");
mesh::usbLoggingPort().println(status);
_regulator.finishInitialization(status);
return false; // fail
}
// Also reached when restoring LoRa after Morse RX or hard recovery.
if (regulator == mesh::power::RegulatorMode::Dcdc) status = setRegulatorDCDC();
#if LR2021_USE_DCDC
// Also reached by radioDeepInit() when restoring LoRa after Morse RX.
// Check this before later setters can overwrite a regulator error.
status = setRegulatorDCDC();
if (status != RADIOLIB_ERR_NONE) return false;
else status = setRegulatorLDO();
#endif
if (_regulator.finishInitialization(status) != RADIOLIB_ERR_NONE) return false;
status = setCRC(2);
if (status != RADIOLIB_ERR_NONE) return false;
@@ -16,6 +16,16 @@
class CustomLR2021Wrapper : public RadioLibWrapper {
public:
uint8_t regulatorModes() const override { return static_cast<CustomLR2021*>(_radio)->regulatorModes(); }
mesh::power::RegulatorMode defaultRegulatorMode() const override {
return static_cast<CustomLR2021*>(_radio)->defaultRegulatorMode();
}
bool configureBootRegulator(mesh::power::RegulatorMode mode) override {
return static_cast<CustomLR2021*>(_radio)->configureBootRegulator(mode);
}
mesh::power::RegulatorMode activeRegulatorMode() const override {
return static_cast<CustomLR2021*>(_radio)->activeRegulatorMode();
}
~CustomLR2021Wrapper() { unregisterInterruptAction(); }
CustomLR2021Wrapper(CustomLR2021& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool supportsMorseTx() const override { return true; }
+30 -3
View File
@@ -1,6 +1,7 @@
#pragma once
#include <RadioLib.h>
#include "RadioRegulatorState.h"
#include <math.h>
#include "MeshCore.h"
#include "RXPowerSaving.h"
@@ -39,6 +40,12 @@
#endif
class CustomSX1262 : public SX1262 {
#if defined(SX126X_USE_REGULATOR_LDO) && SX126X_USE_REGULATOR_LDO
mesh::power::RadioRegulatorState _regulator{mesh::power::REGULATOR_LDO, mesh::power::RegulatorMode::Ldo};
#else
mesh::power::RadioRegulatorState _regulator{mesh::power::REGULATOR_DCDC | mesh::power::REGULATOR_LDO,
mesh::power::RegulatorMode::Dcdc};
#endif
uint32_t _preambleMillis = 66;
uint32_t _maxPayloadMillis = 3934;
uint32_t _activityAt = 0;
@@ -52,6 +59,11 @@ class CustomSX1262 : public SX1262 {
public:
CustomSX1262(Module *mod) : SX1262(mod) { }
uint8_t regulatorModes() const { return _regulator.modes(); }
mesh::power::RegulatorMode defaultRegulatorMode() const { return _regulator.defaultMode(); }
bool configureBootRegulator(mesh::power::RegulatorMode mode) { return _regulator.configureBoot(mode); }
mesh::power::RegulatorMode activeRegulatorMode() const { return _regulator.activeMode(); }
void invalidateMorseModemCache() {
_profileSwitch.invalidate();
_coldStandby = true;
@@ -296,6 +308,10 @@ class CustomSX1262 : public SX1262 {
uint8_t syncWord = RADIOLIB_SX126X_SYNC_WORD_PRIVATE, int8_t power = 10,
uint16_t preambleLength = 8, float tcxoVoltage = 1.6,
bool useRegulatorLDO = false) {
useRegulatorLDO = _regulator.beginInitialization(useRegulatorLDO
? mesh::power::RegulatorMode::Ldo : mesh::power::RegulatorMode::Dcdc)
== mesh::power::RegulatorMode::Ldo;
MESH_DEBUG_PRINTLN("SX1262 regulator requested: %s", useRegulatorLDO ? "LDO" : "DC-DC");
_profileSwitch.invalidate();
// A held dual-profile oscillator must not be selected during reset or
// setup, before DIO3/TCXO configuration has been restored. Restore the
@@ -309,7 +325,20 @@ class CustomSX1262 : public SX1262 {
tcxoVoltage, useRegulatorLDO);
if (state == RADIOLIB_ERR_NONE) state = applyMeshCoreTcxoDelay();
standbyXOSC = savedStandbyXosc;
return state;
return _regulator.finishInitialization(state);
}
// The GFSK reset used by Morse must retain the same boot choice and must
// invalidate reported active state when native initialization fails.
int16_t beginFSK(float freq = 434.0, float br = 4.8, float freqDev = 5.0,
float rxBw = 156.2, int8_t power = 10, uint16_t preambleLength = 16,
float tcxoVoltage = 1.6, bool useRegulatorLDO = false) override {
useRegulatorLDO = _regulator.beginInitialization(useRegulatorLDO
? mesh::power::RegulatorMode::Ldo : mesh::power::RegulatorMode::Dcdc)
== mesh::power::RegulatorMode::Ldo;
const int16_t status = SX1262::beginFSK(freq, br, freqDev, rxBw, power,
preambleLength, tcxoVoltage, useRegulatorLDO);
return _regulator.finishInitialization(status);
}
// Read tcxoVoltage back from the chip object rather than from the argument:
@@ -379,8 +408,6 @@ class CustomSX1262 : public SX1262 {
constexpr bool useRegulatorLDO = false;
#endif
MESH_DEBUG_PRINTLN("SX1262 regulator requested: %s", useRegulatorLDO ? "LDO" : "DC-DC");
#if defined(STATION_G3_ESP32)
// Reinitialization/watchdog recovery must keep the boot-selected slot;
// the build's historical slot macros are not the live SPI routing.
@@ -37,6 +37,16 @@ protected:
}
public:
uint8_t regulatorModes() const override { return static_cast<CustomSX1262*>(_radio)->regulatorModes(); }
mesh::power::RegulatorMode defaultRegulatorMode() const override {
return static_cast<CustomSX1262*>(_radio)->defaultRegulatorMode();
}
bool configureBootRegulator(mesh::power::RegulatorMode mode) override {
return static_cast<CustomSX1262*>(_radio)->configureBootRegulator(mode);
}
mesh::power::RegulatorMode activeRegulatorMode() const override {
return static_cast<CustomSX1262*>(_radio)->activeRegulatorMode();
}
~CustomSX1262Wrapper() { unregisterInterruptAction(); }
CustomSX1262Wrapper(CustomSX1262& radio, mesh::MainBoard& board) : RadioLibWrapper(radio, board) { }
bool supportsCarrierWave() const override { return true; }
@@ -0,0 +1,46 @@
#pragma once
#include <helpers/RegulatorMode.h>
namespace mesh { namespace power {
// A boot selection never writes the radio. Once initialization starts, freeze
// it so recovery and temporary modem changes cannot apply a pending CLI edit.
class RadioRegulatorState {
const uint8_t _modes;
const RegulatorMode _default;
RegulatorMode _requested = RegulatorMode::Default;
RegulatorMode _effective = RegulatorMode::Default;
RegulatorMode _active = RegulatorMode::Default;
bool _started = false;
public:
RadioRegulatorState(uint8_t modes, RegulatorMode default_mode)
: _modes(modes), _default(default_mode) { }
uint8_t modes() const { return _modes; }
RegulatorMode defaultMode() const { return _default; }
RegulatorMode activeMode() const { return _active; }
bool configureBoot(RegulatorMode mode) {
if (_started || !regulatorModeSupported(_modes, mode)) return false;
_requested = mode;
return true;
}
RegulatorMode beginInitialization(RegulatorMode legacy_default = RegulatorMode::Default) {
_started = true;
_active = RegulatorMode::Default; // no successful application in this init yet
const RegulatorMode fallback = regulatorModeSupported(_modes, legacy_default)
&& legacy_default != RegulatorMode::Default ? legacy_default : _default;
_effective = _requested == RegulatorMode::Default ? fallback : _requested;
return _effective;
}
int16_t finishInitialization(int16_t status) {
_active = status == 0 ? _effective : RegulatorMode::Default;
return status;
}
};
}} // namespace mesh::power