1089 lines
38 KiB
C++
1089 lines
38 KiB
C++
#if defined(NRF52_PLATFORM)
|
|
#include "NRF52Board.h"
|
|
#include "PowerManagementUtils.h"
|
|
#include "NRF52VoltagePolicy.h"
|
|
#include "Nrf52BootloaderVersion.h"
|
|
#include "nrf52/SoftDeviceState.h"
|
|
#include "nrf52/SleepCycleState.h"
|
|
#include "nrf52/ShutdownLeds.h"
|
|
#include "nrf52/BluetoothLed.h"
|
|
#include <target.h>
|
|
#include "UserGpioPinPolicy.h"
|
|
#include "PowerSavingDiagnostics.h"
|
|
#include "Nrf52RegulatorPlatform.h"
|
|
|
|
#include <bluefruit.h>
|
|
#include "ble_gap.h"
|
|
#include "ble_hci.h"
|
|
#include <nrf.h>
|
|
#include <nrf_nvic.h>
|
|
#include <nrf_soc.h>
|
|
#ifdef USE_TINYUSB
|
|
#include <Adafruit_TinyUSB.h>
|
|
#endif
|
|
|
|
#ifdef USE_CC310_HW_CRYPTO
|
|
#include <Adafruit_nRFCrypto.h>
|
|
#endif
|
|
|
|
static BLEDfu bledfu;
|
|
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));
|
|
extern "C" void meshRegulatorResumeRecovery() __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>(®ulator_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
|
|
// a physical power cycle. Builds can override this, or set it to 0 to disable
|
|
// the watchdog for diagnostics.
|
|
#ifndef NRF52_WATCHDOG_TIMEOUT_SECONDS
|
|
#define NRF52_WATCHDOG_TIMEOUT_SECONDS 60UL
|
|
#endif
|
|
|
|
#ifndef DFU_MAGIC_UF2_RESET
|
|
#define DFU_MAGIC_UF2_RESET 0x57
|
|
#endif
|
|
|
|
#if NRF52_WATCHDOG_TIMEOUT_SECONDS > 131071UL
|
|
#error "NRF52_WATCHDOG_TIMEOUT_SECONDS exceeds the nRF52 WDT counter range"
|
|
#endif
|
|
|
|
static void format_ota_reply(char reply[]) {
|
|
uint8_t mac_addr[6];
|
|
memset(mac_addr, 0, sizeof(mac_addr));
|
|
Bluefruit.getAddr(mac_addr);
|
|
sprintf(reply, "OK - mac: %02X:%02X:%02X:%02X:%02X:%02X", mac_addr[5], mac_addr[4], mac_addr[3],
|
|
mac_addr[2], mac_addr[1], mac_addr[0]);
|
|
}
|
|
|
|
namespace {
|
|
|
|
bool isExposedNrf52UserGpio(uint8_t pin) {
|
|
#if defined(HELTEC_T096)
|
|
// Physical P2/P3 header GPIOs from the T096 schematic. Firmware-owned
|
|
// radio, display, GPS, power, button, and I2C pins are removed separately.
|
|
static const uint8_t exposed[] = {
|
|
2, 4, 7, 8, 9, 10, 13, 15, 17, 20, 22, 23, 24, 25, 27, 29, 31,
|
|
32, 33, 34, 35, 36, 37, 38, 39, 42, 43, 45, 47
|
|
};
|
|
#elif defined(PROMICRO)
|
|
// D0-D17 are broken out on the ProMicro form factor.
|
|
if (pin <= 17) return true;
|
|
return false;
|
|
#elif defined(RAK_3401) || defined(RAK_4631)
|
|
// GPIO and bus signals exposed by the WisBlock base/IO connector.
|
|
static const uint8_t exposed[] = {
|
|
2, 3, 4, 5, 9, 10, 13, 14, 15, 16, 17, 19, 20, 21, 24, 25,
|
|
26, 28, 29, 30, 31, 33, 34
|
|
};
|
|
#else
|
|
return false;
|
|
#endif
|
|
|
|
#if defined(HELTEC_T096) || defined(RAK_3401) || defined(RAK_4631)
|
|
for (size_t i = 0; i < sizeof(exposed) / sizeof(exposed[0]); i++) {
|
|
if (pin == exposed[i]) return true;
|
|
}
|
|
#endif
|
|
return false;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool NRF52Board::isUserGpioAvailable(uint8_t pin) const {
|
|
// This board approval query also serves Companion alert outputs. CommonCLI's
|
|
// separate USER_GPIO_CONTROL flag still controls its GPIO command exposure.
|
|
if (pin >= PINS_COUNT || digitalPinToPinName(pin) == 0xFF) return false;
|
|
#if defined(RAK_3401) || defined(RAK_4631)
|
|
// Sensor startup can toggle these WisBlock slot pins while detecting GPS,
|
|
// and WB_IO2 also controls the switched peripheral rail on supported bases.
|
|
if (pin == WB_IO2 || pin == WB_IO4 || pin == WB_IO5) return false;
|
|
#endif
|
|
return isExposedNrf52UserGpio(pin) && !UserGpioPinPolicy::isFirmwareReserved(pin);
|
|
}
|
|
|
|
bool NRF52Board::rebootToUf2Bootloader() {
|
|
uint8_t sd_enabled = 0;
|
|
if (mesh_nrf52::softdeviceIsEnabled(sd_enabled) != NRF_SUCCESS) return false;
|
|
|
|
if (sd_enabled) {
|
|
if (sd_power_gpregret_clr(0, 0xFF) != NRF_SUCCESS
|
|
|| sd_power_gpregret_set(0, DFU_MAGIC_UF2_RESET) != NRF_SUCCESS) {
|
|
return false;
|
|
}
|
|
} else {
|
|
NRF_POWER->GPREGRET = DFU_MAGIC_UF2_RESET;
|
|
}
|
|
|
|
reboot();
|
|
return true;
|
|
}
|
|
|
|
static void connect_callback(uint16_t conn_handle) {
|
|
ota_conn_handle = conn_handle;
|
|
MESH_DEBUG_PRINTLN("BLE client connected");
|
|
}
|
|
|
|
static void disconnect_callback(uint16_t conn_handle, uint8_t reason) {
|
|
(void)reason;
|
|
if (ota_conn_handle == conn_handle) {
|
|
ota_conn_handle = BLE_CONN_HANDLE_INVALID;
|
|
}
|
|
|
|
MESH_DEBUG_PRINTLN("BLE client disconnected");
|
|
}
|
|
|
|
void NRF52Board::begin() {
|
|
startup_reason = BD_STARTUP_NORMAL;
|
|
|
|
#ifdef USE_CC310_HW_CRYPTO
|
|
// CC310 TRNG is higher quality and environment-independent vs radio RSSI noise.
|
|
nRFCrypto.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::applyLedPolicy() {
|
|
mesh_nrf52::applyBluetoothLedPolicy();
|
|
}
|
|
|
|
void NRF52Board::loop() {
|
|
#ifdef NRF52_POWER_MANAGEMENT
|
|
if (!mesh::power::voltagePolicySupported()) return;
|
|
static uint32_t last_check = 0;
|
|
static uint8_t low_count = 0;
|
|
static uint16_t last_threshold = 0;
|
|
const uint16_t threshold = mesh::power::configuredCutoff();
|
|
const uint32_t now = millis();
|
|
if (threshold != last_threshold) {
|
|
last_threshold = threshold;
|
|
low_count = 0;
|
|
last_check = now;
|
|
}
|
|
if (threshold == 0 || now - last_check < 30000UL) return;
|
|
last_check = now;
|
|
if (isExternalPowered()) {
|
|
low_count = 0;
|
|
return;
|
|
}
|
|
low_count = mesh::power::nextLowVoltageCount(
|
|
low_count, getBattMilliVolts(), threshold, false);
|
|
if (low_count >= 3) initiateShutdown(SHUTDOWN_REASON_LOW_VOLTAGE);
|
|
#endif
|
|
}
|
|
|
|
uint16_t NRF52Board::calibrateBatteryMillivolts(uint16_t raw_mv) const {
|
|
#ifdef NRF52_POWER_MANAGEMENT
|
|
if (supportsBatteryCalibration() && mesh::power::voltagePolicySupported()) {
|
|
return mesh::power::calibratedMillivolts(
|
|
raw_mv, mesh::power::configuredAdcPermille());
|
|
}
|
|
#endif
|
|
return mesh::MainBoard::calibrateBatteryMillivolts(raw_mv);
|
|
}
|
|
|
|
float NRF52Board::getAdcMultiplier() const {
|
|
#ifdef NRF52_POWER_MANAGEMENT
|
|
if (supportsBatteryCalibration() && mesh::power::voltagePolicySupported()) {
|
|
return mesh::power::configuredAdcPermille() / 1000.0f;
|
|
}
|
|
#endif
|
|
return mesh::MainBoard::getAdcMultiplier();
|
|
}
|
|
|
|
#if NRF52_WATCHDOG_TIMEOUT_SECONDS > 0 || MESH_ENABLE_EMERGENCY_POWER
|
|
static void reloadWatchdogChannels() {
|
|
// Ordinarily only RR0 is enabled. If a bootloader left the watchdog running
|
|
// with a different reload channel, service every enabled channel.
|
|
const uint32_t enabled_channels = NRF_WDT->RREN & 0xFFUL;
|
|
for (uint8_t channel = 0; channel < 8; channel++) {
|
|
if (enabled_channels & (1UL << channel)) {
|
|
NRF_WDT->RR[channel] = WDT_RR_RR_Reload;
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
void NRF52Board::feedWatchdog(bool enabled) {
|
|
#if NRF52_WATCHDOG_TIMEOUT_SECONDS > 0
|
|
// The nRF52 watchdog cannot be stopped after it starts. When the persisted
|
|
// setting is turned off, deliberately stop reloading it; the resulting
|
|
// watchdog reset is the only software-only way to return it to the stopped
|
|
// state. On that next boot the disabled preference prevents it starting.
|
|
const bool running = NRF_WDT->RUNSTATUS != 0;
|
|
if (!enabled) {
|
|
if (running && !watchdog_disable_reset_pending) {
|
|
watchdog_disable_reset_pending = true;
|
|
if (meshRegulatorCleanReset) meshRegulatorCleanReset();
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (!running) {
|
|
// Keep running during CPU sleep: the flash-driver failure this protects
|
|
// against sleeps in sd_app_evt_wait(). Pause while halted so breakpoints
|
|
// do not reset a board being debugged.
|
|
NRF_WDT->CONFIG =
|
|
(WDT_CONFIG_SLEEP_Run << WDT_CONFIG_SLEEP_Pos) |
|
|
(WDT_CONFIG_HALT_Pause << WDT_CONFIG_HALT_Pos);
|
|
NRF_WDT->CRV = (uint32_t)(NRF52_WATCHDOG_TIMEOUT_SECONDS * 32768UL);
|
|
NRF_WDT->RREN = WDT_RREN_RR0_Msk;
|
|
NRF_WDT->TASKS_START = 1;
|
|
}
|
|
|
|
reloadWatchdogChannels();
|
|
if (watchdog_disable_reset_pending) {
|
|
watchdog_disable_reset_pending = false;
|
|
// Cancel a planned watchdog-off reset with fresh recovery state. Feed
|
|
// first so the callback's battery sampling cannot race an imminent expiry.
|
|
if (meshRegulatorResumeRecovery) meshRegulatorResumeRecovery();
|
|
}
|
|
#else
|
|
(void)enabled;
|
|
#endif
|
|
}
|
|
|
|
void NRF52Board::serviceWatchdog() {
|
|
#if NRF52_WATCHDOG_TIMEOUT_SECONDS > 0
|
|
if (NRF_WDT->RUNSTATUS != 0) reloadWatchdogChannels();
|
|
#endif
|
|
}
|
|
|
|
bool NRF52Board::getSystemWatchdogRunning(bool& running) const {
|
|
// Read even if this build's feed policy is disabled: a bootloader can leave
|
|
// a watchdog running. The query must describe the hardware, not a preference.
|
|
running = NRF_WDT->RUNSTATUS != 0;
|
|
return true;
|
|
}
|
|
|
|
#ifdef NRF52_POWER_MANAGEMENT
|
|
// Power Management global variables
|
|
uint32_t g_nrf52_reset_reason = 0; // Reset/Startup reason
|
|
uint8_t g_nrf52_shutdown_reason = 0; // Shutdown reason
|
|
|
|
// Early constructor - runs before SystemInit() clears the registers
|
|
// Priority 101 ensures this runs before SystemInit (102) and before
|
|
// any C++ static constructors (default 65535)
|
|
static void __attribute__((constructor(101))) nrf52_early_reset_capture() {
|
|
g_nrf52_reset_reason = NRF_POWER->RESETREAS;
|
|
g_nrf52_shutdown_reason = NRF_POWER->GPREGRET2;
|
|
}
|
|
|
|
void NRF52Board::initPowerMgr() {
|
|
if (power_mgr_initialized) return;
|
|
|
|
// Copy early-captured register values
|
|
reset_reason = g_nrf52_reset_reason;
|
|
shutdown_reason = g_nrf52_shutdown_reason;
|
|
boot_voltage_mv = 0; // Will be set by checkBootVoltage()
|
|
|
|
// Clear registers for next boot
|
|
// Note: At this point SoftDevice may or may not be enabled
|
|
uint8_t sd_enabled = 0;
|
|
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
|
|
if (sd_enabled) {
|
|
sd_power_reset_reason_clr(0xFFFFFFFF);
|
|
sd_power_gpregret_clr(1, 0xFF);
|
|
} else {
|
|
NRF_POWER->RESETREAS = 0xFFFFFFFF; // Write 1s to clear
|
|
NRF_POWER->GPREGRET2 = 0;
|
|
}
|
|
|
|
// Log reset/shutdown info
|
|
if (shutdown_reason != SHUTDOWN_REASON_NONE) {
|
|
MESH_DEBUG_PRINTLN("PWRMGT: Reset = %s (0x%lX); Shutdown = %s (0x%02X)",
|
|
getResetReasonString(reset_reason), (unsigned long)reset_reason,
|
|
getShutdownReasonString(shutdown_reason), shutdown_reason);
|
|
} else {
|
|
MESH_DEBUG_PRINTLN("PWRMGT: Reset = %s (0x%lX)",
|
|
getResetReasonString(reset_reason), (unsigned long)reset_reason);
|
|
}
|
|
power_mgr_initialized = true;
|
|
}
|
|
|
|
const char* NRF52Board::getResetReasonString(uint32_t reason) {
|
|
if (reason & POWER_RESETREAS_RESETPIN_Msk) return "Reset Pin";
|
|
if (reason & POWER_RESETREAS_DOG_Msk) return "Watchdog";
|
|
if (reason & POWER_RESETREAS_SREQ_Msk) return "Soft Reset";
|
|
if (reason & POWER_RESETREAS_LOCKUP_Msk) return "CPU Lockup";
|
|
#ifdef POWER_RESETREAS_LPCOMP_Msk
|
|
if (reason & POWER_RESETREAS_LPCOMP_Msk) return "Wake from LPCOMP";
|
|
#endif
|
|
#ifdef POWER_RESETREAS_VBUS_Msk
|
|
if (reason & POWER_RESETREAS_VBUS_Msk) return "Wake from VBUS";
|
|
#endif
|
|
#ifdef POWER_RESETREAS_OFF_Msk
|
|
if (reason & POWER_RESETREAS_OFF_Msk) return "Wake from GPIO";
|
|
#endif
|
|
#ifdef POWER_RESETREAS_DIF_Msk
|
|
if (reason & POWER_RESETREAS_DIF_Msk) return "Debug Interface";
|
|
#endif
|
|
return "Cold Boot";
|
|
}
|
|
|
|
const char* NRF52Board::getShutdownReasonString(uint8_t reason) {
|
|
switch (reason) {
|
|
case SHUTDOWN_REASON_NONE: return "None";
|
|
case SHUTDOWN_REASON_LOW_VOLTAGE: return "Low Voltage";
|
|
case SHUTDOWN_REASON_USER: return "User Request";
|
|
case SHUTDOWN_REASON_BOOT_PROTECT: return "Boot Protection";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
|
|
bool NRF52Board::checkBootVoltage(const PowerMgtConfig* config) {
|
|
pwrmgtInit();
|
|
|
|
if (config == nullptr) return true;
|
|
|
|
// Use the median of three readings. A single unsettled ADC sample during a
|
|
// brownout must not put the device into a persistent SYSTEMOFF boot lock.
|
|
uint16_t samples[3];
|
|
for (uint8_t i = 0; i < 3; i++) {
|
|
samples[i] = getBattMilliVolts();
|
|
if (i != 2) delay(5);
|
|
}
|
|
boot_voltage_mv = mesh::power::medianVoltage(samples[0], samples[1], samples[2]);
|
|
|
|
const bool external = isExternalPowered();
|
|
// Loading the policy still accesses flash. Keep a hard floor ahead of it
|
|
// for boards whose compiled policy enables boot protection.
|
|
const uint16_t preflight = config->voltage_bootlock == 0 ? 0
|
|
: mesh::power::MIN_CUSTOM_MV;
|
|
if (mesh::power::shouldBootLock(boot_voltage_mv, preflight, external)) {
|
|
initiateShutdown(SHUTDOWN_REASON_BOOT_PROTECT);
|
|
return false;
|
|
}
|
|
|
|
mesh::power::loadVoltagePolicyAtBoot(config->voltage_bootlock);
|
|
boot_voltage_mv = calibrateBatteryMillivolts(boot_voltage_mv);
|
|
const uint16_t threshold = mesh::power::configuredBootlock();
|
|
if (threshold == 0 || external) return true;
|
|
|
|
MESH_DEBUG_PRINTLN("PWRMGT: Boot voltage = %u mV (threshold = %u mV)",
|
|
boot_voltage_mv, threshold);
|
|
|
|
// Only valid ADC readings (>1000 mV) can initiate protective shutdown.
|
|
if (mesh::power::shouldBootLock(boot_voltage_mv, threshold, false)) {
|
|
MESH_DEBUG_PRINTLN("PWRMGT: Boot voltage too low - entering protective shutdown");
|
|
|
|
initiateShutdown(SHUTDOWN_REASON_BOOT_PROTECT);
|
|
return false; // Should never reach this
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void NRF52Board::initiateShutdown(uint8_t reason) {
|
|
enterSystemOff(reason);
|
|
}
|
|
|
|
void NRF52Board::enterSystemOff(uint8_t reason) {
|
|
MESH_DEBUG_PRINTLN("PWRMGT: Entering SYSTEMOFF (%s)", getShutdownReasonString(reason));
|
|
|
|
// This path can run before BLE begin() during boot voltage protection.
|
|
// autoConnLed(false) is safe then too and stops future automatic LED writes.
|
|
Bluefruit.autoConnLed(false);
|
|
|
|
// Record shutdown reason in GPREGRET2
|
|
uint8_t sd_enabled = 0;
|
|
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
|
|
if (sd_enabled) {
|
|
sd_power_gpregret_clr(1, 0xFF);
|
|
sd_power_gpregret_set(1, reason);
|
|
} else {
|
|
NRF_POWER->GPREGRET2 = reason;
|
|
}
|
|
|
|
// Flush serial buffers
|
|
Serial.flush();
|
|
delay(100);
|
|
mesh_nrf52::turnOffShutdownLeds();
|
|
|
|
// Enter SYSTEMOFF
|
|
if (sd_enabled) {
|
|
uint32_t err = sd_power_system_off();
|
|
if (err == NRF_ERROR_SOFTDEVICE_NOT_ENABLED) { //SoftDevice not enabled
|
|
sd_enabled = 0;
|
|
}
|
|
}
|
|
|
|
if (!sd_enabled) {
|
|
// SoftDevice not available; write directly to POWER->SYSTEMOFF
|
|
NRF_POWER->SYSTEMOFF = POWER_SYSTEMOFF_SYSTEMOFF_Enter;
|
|
}
|
|
|
|
// If we get here, something went wrong. Reset to recover.
|
|
NVIC_SystemReset();
|
|
}
|
|
|
|
void NRF52Board::configureVoltageWake(uint8_t ain_channel, uint8_t refsel) {
|
|
// USB power should always be able to recover a device from SYSTEMOFF, even
|
|
// if voltage comparator setup is unavailable or invalid.
|
|
armVbusWake();
|
|
if (!power_mgr_initialized || !supportsVoltageWake()) {
|
|
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake skipped (power manager not ready/unsupported)");
|
|
return;
|
|
}
|
|
if (ain_channel > 7 || refsel > 15) {
|
|
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake skipped (invalid AIN/ref)");
|
|
return;
|
|
}
|
|
|
|
// LPCOMP is not managed by SoftDevice - direct register access required
|
|
// Halt and disable before reconfiguration
|
|
NRF_LPCOMP->TASKS_STOP = 1;
|
|
NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Disabled;
|
|
|
|
// Select analog input (AIN0-7 maps to PSEL 0-7)
|
|
NRF_LPCOMP->PSEL = ((uint32_t)ain_channel << LPCOMP_PSEL_PSEL_Pos) & LPCOMP_PSEL_PSEL_Msk;
|
|
|
|
// Reference: REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)
|
|
NRF_LPCOMP->REFSEL = ((uint32_t)refsel << LPCOMP_REFSEL_REFSEL_Pos) & LPCOMP_REFSEL_REFSEL_Msk;
|
|
|
|
// Detect UP events (voltage rises above threshold for battery recovery)
|
|
NRF_LPCOMP->ANADETECT = LPCOMP_ANADETECT_ANADETECT_Up;
|
|
|
|
// Do not add comparator hysteresis here. On divided battery inputs it can
|
|
// shift the effective wake point enough to strand a valid low-voltage cell.
|
|
NRF_LPCOMP->HYST = LPCOMP_HYST_HYST_NoHyst;
|
|
|
|
// Clear stale events/interrupts before enabling wake
|
|
NRF_LPCOMP->EVENTS_READY = 0;
|
|
NRF_LPCOMP->EVENTS_DOWN = 0;
|
|
NRF_LPCOMP->EVENTS_UP = 0;
|
|
NRF_LPCOMP->EVENTS_CROSS = 0;
|
|
|
|
NRF_LPCOMP->INTENCLR = 0xFFFFFFFF;
|
|
NRF_LPCOMP->INTENSET = LPCOMP_INTENSET_UP_Msk;
|
|
|
|
// Enable LPCOMP
|
|
NRF_LPCOMP->ENABLE = LPCOMP_ENABLE_ENABLE_Enabled;
|
|
NRF_LPCOMP->TASKS_START = 1;
|
|
|
|
// Wait for comparator to settle
|
|
for (uint8_t i = 0; i < 20 && !NRF_LPCOMP->EVENTS_READY; i++) {
|
|
delayMicroseconds(50);
|
|
}
|
|
|
|
if (refsel == 7) {
|
|
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=ARef)", ain_channel);
|
|
} else if (refsel <= 6) {
|
|
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=%d/8 VDD)", ain_channel, refsel + 1);
|
|
} else {
|
|
uint8_t ref_num = (uint8_t)((refsel - 8) * 2 + 1);
|
|
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake armed (AIN%d, ref=%d/16 VDD)", ain_channel, ref_num);
|
|
}
|
|
}
|
|
|
|
void NRF52Board::armVbusWake() {
|
|
// Configure VBUS (USB power) wake alongside (or instead of) LPCOMP.
|
|
uint8_t sd_enabled = 0;
|
|
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
|
|
if (sd_enabled) {
|
|
sd_power_usbdetected_enable(1);
|
|
} else {
|
|
NRF_POWER->EVENTS_USBDETECTED = 0;
|
|
NRF_POWER->INTENSET = POWER_INTENSET_USBDETECTED_Msk;
|
|
}
|
|
MESH_DEBUG_PRINTLN("PWRMGT: VBUS wake armed");
|
|
}
|
|
#endif
|
|
|
|
void NRF52BoardDCDC::begin() {
|
|
NRF52Board::begin();
|
|
// 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() {
|
|
mesh_nrf52::SleepCycleSnapshot cycle;
|
|
if (mesh_nrf52::sleepCycleState().snapshot(cycle))
|
|
return (cycle.usb_status & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
|
|
|
|
// Setup and callers outside a prepared loop retain the SDK access policy.
|
|
// Never reuse a cached value after a SoftDevice transition invalidated it.
|
|
uint8_t sd_enabled = 0;
|
|
if (mesh_nrf52::softdeviceIsEnabled(sd_enabled) != NRF_SUCCESS) return false;
|
|
|
|
if (sd_enabled) {
|
|
volatile uint32_t status = 0;
|
|
const uint32_t error = sd_power_usbregstatus_get((uint32_t*)&status);
|
|
__asm volatile ("" ::: "memory");
|
|
if (error != NRF_SUCCESS) return false;
|
|
const uint32_t usb_status = status;
|
|
return (usb_status & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
|
|
} else {
|
|
return (NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
|
|
}
|
|
}
|
|
|
|
void NRF52Board::prepareSleepCycle() {
|
|
auto& state = mesh_nrf52::sleepCycleState();
|
|
const uint32_t token = state.begin();
|
|
const auto application_context = []() {
|
|
return __get_IPSR() == 0 && __get_PRIMASK() == 0
|
|
&& __get_BASEPRI() == 0 && __get_FAULTMASK() == 0
|
|
// SDK critical regions can mask application IRQs in the NVIC even
|
|
// while the CPU mask registers are clear. This public field is
|
|
// volatile: read it afresh before queries and after the forced yield.
|
|
&& nrf_nvic_state.__cr_flag == 0
|
|
&& xTaskGetSchedulerState() == taskSCHEDULER_RUNNING;
|
|
};
|
|
if (!application_context()) {
|
|
state.publish(token, false, 0, NRF_ERROR_INVALID_STATE);
|
|
return;
|
|
}
|
|
#ifdef NRF_CRYPTOCELL
|
|
// The pinned Adafruit CC310 library polls (no-interrupts archive), but can
|
|
// leave its disabled NVIC IRQ pending. The SDK treats that as application
|
|
// work forever. Match the core's waitForEvent cleanup only at loop entry;
|
|
// never clear an enabled IRQ belonging to an interrupt-driven owner.
|
|
if (NVIC_GetEnableIRQ(CRYPTOCELL_IRQn) == 0)
|
|
NVIC_ClearPendingIRQ(CRYPTOCELL_IRQn);
|
|
#endif
|
|
uint8_t sd_enabled = 0;
|
|
uint32_t error = mesh_nrf52::softdeviceIsEnabled(sd_enabled);
|
|
uint32_t usb_status = 0;
|
|
if (error == NRF_SUCCESS) {
|
|
if (sd_enabled) {
|
|
volatile uint32_t status = 0;
|
|
error = sd_power_usbregstatus_get((uint32_t*)&status);
|
|
__asm volatile ("" ::: "memory");
|
|
usb_status = status;
|
|
} else {
|
|
usb_status = NRF_POWER->USBREGSTATUS;
|
|
}
|
|
}
|
|
if (error == NRF_SUCCESS && sd_enabled && state.current(token)) {
|
|
// Raw SEV/WFE does not acknowledge the SDK's separate application-IRQ
|
|
// bookkeeping. First force a real application PendSV while the loop task
|
|
// remains ready (so the RTOS idle task cannot consume that acknowledgement).
|
|
// The public SDK contract then makes this one early wait non-blocking.
|
|
// The pinned S140 6.1.1/7.3.0 forwarding/wait code confirms its private IRQ
|
|
// sequence cannot wrap back to the last acknowledged value. Do not access
|
|
// those SDK-private counters or rely on SEV alone to release an SDK wait.
|
|
taskYIELD();
|
|
if (!application_context() || (SCB->ICSR & SCB_ICSR_PENDSVSET_Msk)) {
|
|
error = NRF_ERROR_INVALID_STATE;
|
|
} else if (state.current(token)) {
|
|
error = sd_app_evt_wait();
|
|
}
|
|
}
|
|
state.publish(token, sd_enabled != 0, usb_status, error);
|
|
|
|
// FreeRTOS yields and the state-query SVC can both leave a stale event.
|
|
// Clear it only before a complete work/guard pass: at loop entry, or during
|
|
// explicit profile-idle preparation before all role sleep predicates. A radio
|
|
// interrupt after that pass begins must remain visible to the late wait.
|
|
__SEV();
|
|
__WFE();
|
|
}
|
|
|
|
bool NRF52Board::canIdleRadioProfile() {
|
|
#if defined(configTICK_RATE_HZ) && configTICK_RATE_HZ == 1024
|
|
// RTC1 belongs to FreeRTOS. Only observe its periodic wake; never alter its
|
|
// counter, compare channels, IRQ ownership, or tickless-idle bookkeeping.
|
|
return __get_IPSR() == 0 && __get_PRIMASK() == 0
|
|
&& __get_BASEPRI() == 0 && __get_FAULTMASK() == 0
|
|
&& nrf_nvic_state.__cr_flag == 0
|
|
&& xTaskGetSchedulerState() == taskSCHEDULER_RUNNING
|
|
&& !(SCB->ICSR & SCB_ICSR_PENDSVSET_Msk)
|
|
&& NRF_RTC1->PRESCALER == 31
|
|
&& (NRF_RTC1->INTENSET & RTC_INTENSET_TICK_Msk) != 0
|
|
&& NVIC_GetEnableIRQ(RTC1_IRQn) != 0;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
void NRF52Board::prepareProfileIdleCycle() {
|
|
if (canIdleRadioProfile()) {
|
|
#if (__FPU_USED == 1)
|
|
// The disabled FPU IRQ can retain an inexact exception from profile math.
|
|
// Clear that known source before refreshing the event baseline. An enabled
|
|
// owner's flags/IRQ belong to its handler, not this polling cleanup.
|
|
if (NVIC_GetEnableIRQ(FPU_IRQn) == 0) {
|
|
__set_FPSCR(__get_FPSCR() & ~0x0000009FUL);
|
|
(void)__get_FPSCR();
|
|
NVIC_ClearPendingIRQ(FPU_IRQn);
|
|
}
|
|
#endif
|
|
}
|
|
// This also revalidates context and SDK state, acknowledges a real PendSV
|
|
// when SoftDevice is on, and clears the old ARM event. All role predicates
|
|
// must run afterward; an interrupt during those checks is never erased.
|
|
prepareSleepCycle();
|
|
}
|
|
|
|
bool NRF52Board::isUsbDataConnected() {
|
|
#if defined(USE_TINYUSB)
|
|
#if defined(CFG_TUD_CDC) && CFG_TUD_CDC
|
|
return tud_mounted() && tud_cdc_connected();
|
|
#else
|
|
return tud_mounted();
|
|
#endif
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
bool NRF52Board::isUsbHostConnected() {
|
|
#if defined(USE_TINYUSB)
|
|
return tud_mounted();
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
|
|
void NRF52Board::sleep(uint32_t secs) {
|
|
(void)secs;
|
|
const bool diagnostics = mesh::powerdiag::enabled();
|
|
auto& state = mesh_nrf52::sleepCycleState();
|
|
mesh_nrf52::SleepCycleSnapshot cycle;
|
|
const bool prepared = state.take(cycle);
|
|
if (isRadioTestActive()) {
|
|
if (diagnostics) mesh::powerdiag::boardVeto(mesh::powerdiag::Reason::Board, millis(), 2);
|
|
delay(1);
|
|
return;
|
|
}
|
|
if (!prepared || !state.current(cycle.generation)) {
|
|
if (diagnostics) {
|
|
const uint32_t error = state.lastError();
|
|
if (error != NRF_SUCCESS) mesh::powerdiag::boardError(static_cast<int32_t>(error), millis());
|
|
else mesh::powerdiag::boardVeto(mesh::powerdiag::Reason::Board, millis(), 3);
|
|
}
|
|
delay(1);
|
|
return;
|
|
}
|
|
// Clear FPU interrupt flags to avoid insomnia
|
|
// see errata 87 for details https://docs.nordicsemi.com/bundle/errata_nRF52840_Rev3/page/ERR/nRF52840/Rev3/latest/anomaly_840_87.html
|
|
#if (__FPU_USED == 1)
|
|
__set_FPSCR(__get_FPSCR() & ~(0x0000009F));
|
|
(void) __get_FPSCR();
|
|
NVIC_ClearPendingIRQ(FPU_IRQn);
|
|
#endif
|
|
|
|
// On nRF52, we use event-driven sleep instead of timed sleep
|
|
// The 'secs' parameter is ignored - we wake on any interrupt
|
|
if (cycle.softdevice_enabled) {
|
|
// A single call is required here. If an interrupt arrived since the last
|
|
// wait, SoftDevice returns immediately so the main loop can service the
|
|
// flag or BLE queue before sleeping again.
|
|
if (diagnostics) mesh::powerdiag::sleepBegin(micros());
|
|
const uint32_t error = sd_app_evt_wait();
|
|
if (diagnostics) {
|
|
mesh::powerdiag::sleepEnd(micros());
|
|
if (error != NRF_SUCCESS) mesh::powerdiag::boardError(static_cast<int32_t>(error), millis());
|
|
}
|
|
} else {
|
|
// SoftDevice is disabled. Match the Adafruit core's race-preserving
|
|
// waitForEvent() sequence: the first WFE sleeps or consumes an already-
|
|
// pending event, then SEV/WFE reliably clears the event register before
|
|
// returning. SEV/WFE/WFE can erase the only GPIO edge that arrived between
|
|
// the caller's predicate and here.
|
|
if (diagnostics) mesh::powerdiag::sleepBegin(micros());
|
|
__WFE();
|
|
__SEV();
|
|
__WFE();
|
|
if (diagnostics) mesh::powerdiag::sleepEnd(micros());
|
|
}
|
|
}
|
|
|
|
// Temperature from NRF52 MCU
|
|
float NRF52Board::getMCUTemperature() {
|
|
uint8_t sd_enabled = 0;
|
|
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
|
|
if (sd_enabled) {
|
|
uint32_t err_code;
|
|
int32_t temp;
|
|
err_code = sd_temp_get(&temp);
|
|
if (err_code == NRF_SUCCESS) {
|
|
return (float)temp * 0.25f;
|
|
} else {
|
|
return NAN;
|
|
}
|
|
} else {
|
|
NRF_TEMP->TASKS_START = 1; // Start temperature measurement
|
|
|
|
long startTime = millis();
|
|
while (NRF_TEMP->EVENTS_DATARDY == 0) { // Wait for completion. Should complete in 50us
|
|
if(millis() - startTime > 5) { // To wait 5ms just in case
|
|
NRF_TEMP->TASKS_STOP = 1;
|
|
return NAN;
|
|
}
|
|
}
|
|
}
|
|
|
|
NRF_TEMP->EVENTS_DATARDY = 0; // Clear event flag
|
|
|
|
int32_t temp = NRF_TEMP->TEMP; // In 0.25 *C units
|
|
NRF_TEMP->TASKS_STOP = 1;
|
|
|
|
return temp * 0.25f; // Convert to *C
|
|
}
|
|
|
|
void NRF52Board::shutdownPeripherals() {
|
|
// Stop Bluefruit's LED owner before setting a final OFF latch. Its timer
|
|
// stop is queued, so allow the existing shutdown delay to drain a pending
|
|
// blinky callback before the final write below (also safe before BLE begin).
|
|
Bluefruit.autoConnLed(false);
|
|
|
|
// Power off the display if any
|
|
#ifdef DISPLAY_CLASS
|
|
if (display.isOn()) {
|
|
display.turnOff();
|
|
}
|
|
#endif
|
|
// Prep LoRa radio for power down
|
|
#ifdef P_LORA_RESET
|
|
digitalWrite(P_LORA_RESET, HIGH); // preload OUT latch so pinMode can't glitch NRESET low
|
|
pinMode(P_LORA_RESET, OUTPUT);
|
|
digitalWrite(P_LORA_RESET, LOW); // deliberate hardware reset (datasheet: >=100us)
|
|
delayMicroseconds(200);
|
|
digitalWrite(P_LORA_RESET, HIGH);
|
|
#endif
|
|
#if defined(P_LORA_SCLK) && defined(P_LORA_MISO) && defined(P_LORA_MOSI)
|
|
SPI.setPins(P_LORA_MISO, P_LORA_SCLK, P_LORA_MOSI);
|
|
SPI.begin(); // SPI may not be started on some shutdown paths, need it to shut down radio
|
|
#endif
|
|
#ifdef P_LORA_BUSY
|
|
pinMode(P_LORA_BUSY, INPUT);
|
|
uint32_t started_at = millis();
|
|
while (digitalRead(P_LORA_BUSY) && millis() - started_at < 10) {} //wait for radio to be ready
|
|
#endif
|
|
#ifdef P_LORA_NSS
|
|
pinMode(P_LORA_NSS, OUTPUT);
|
|
digitalWrite(P_LORA_NSS, HIGH);
|
|
#endif
|
|
// Power off LoRa
|
|
radio_driver.powerOff();
|
|
|
|
// RadioLib's IRQ setup can leave DIO1 as a GPIO wake source. Once the radio
|
|
// is asleep that line may float or remain asserted, immediately waking an
|
|
// nRF52 that is trying to enter SYSTEMOFF. Release the interrupt channel and
|
|
// explicitly disable pin sensing; board-specific code can then arm only its
|
|
// intended wake source (for example, the SenseCAP user button or LPCOMP).
|
|
#ifdef P_LORA_DIO_1
|
|
detachInterrupt(P_LORA_DIO_1);
|
|
pinMode(P_LORA_DIO_1, INPUT);
|
|
#endif
|
|
|
|
// Keep LoRa inactive during deepsleep
|
|
#ifdef P_LORA_NSS
|
|
digitalWrite(P_LORA_NSS, HIGH);
|
|
#endif
|
|
|
|
// Power off GPS if any
|
|
if(sensors.getLocationProvider() != NULL) {
|
|
sensors.getLocationProvider()->stop();
|
|
}
|
|
|
|
#ifdef USE_CC310_HW_CRYPTO
|
|
nRFCrypto.end();
|
|
#endif
|
|
|
|
// Flush serial buffers
|
|
Serial.flush();
|
|
delay(100);
|
|
mesh_nrf52::turnOffShutdownLeds();
|
|
}
|
|
|
|
void NRF52Board::powerOff() {
|
|
shutdownPeripherals();
|
|
// Board-specific hooks may signal button release after calling the base.
|
|
mesh_nrf52::turnOffShutdownLeds();
|
|
|
|
// Enter SYSTEMOFF
|
|
uint8_t sd_enabled = 0;
|
|
mesh_nrf52::softdeviceIsEnabled(sd_enabled);
|
|
if (sd_enabled) { // SoftDevice is enabled
|
|
sd_power_system_off();
|
|
} else { // SoftDevice is not enable
|
|
NRF_POWER->SYSTEMOFF = POWER_SYSTEMOFF_SYSTEMOFF_Enter;
|
|
}
|
|
}
|
|
|
|
#if MESH_ENABLE_EMERGENCY_POWER
|
|
bool NRF52Board::emergencySleepMilliseconds(uint32_t millisecs) {
|
|
// Blocking the sketch lets the existing RTOS idle/RTC machinery sleep the
|
|
// CPU. SYSTEMOFF cannot provide a timed wake on every supported nRF52 board.
|
|
if (millisecs == 0 || watchdog_disable_reset_pending || isRadioTestActive()
|
|
|| __get_IPSR() != 0 || __get_PRIMASK() != 0 || __get_BASEPRI() != 0
|
|
|| __get_FAULTMASK() != 0 || nrf_nvic_state.__cr_flag != 0
|
|
|| xTaskGetSchedulerState() != taskSCHEDULER_RUNNING) return false;
|
|
if (millisecs > 10000UL) millisecs = 10000UL;
|
|
// An enumerated host needs several sketch passes for cooperative CDC
|
|
// session cleanup and reply draining. Keep those passes prompt while RF
|
|
// stays asleep; detached operation retains the longer maintenance slice.
|
|
if (isUsbHostConnected() && millisecs > 100UL) millisecs = 100UL;
|
|
if (NRF_WDT->RUNSTATUS != 0) {
|
|
// A bootloader may have left a shorter watchdog than this build's default.
|
|
// Half its actual timeout leaves room for scheduling and wake processing.
|
|
const uint32_t watchdog_budget_ms = (NRF_WDT->CRV / 32768UL) * 500UL;
|
|
if (watchdog_budget_ms == 0) return false;
|
|
if (millisecs > watchdog_budget_ms) millisecs = watchdog_budget_ms;
|
|
}
|
|
const TickType_t ticks = pdMS_TO_TICKS(millisecs);
|
|
if (ticks == 0) return false;
|
|
// Service inherited watchdogs even when this build does not start one.
|
|
if (NRF_WDT->RUNSTATUS != 0) reloadWatchdogChannels();
|
|
vTaskDelay(ticks);
|
|
if (NRF_WDT->RUNSTATUS != 0) reloadWatchdogChannels();
|
|
return true;
|
|
}
|
|
#endif
|
|
|
|
void NRF52Board::enterDeepSleep(uint32_t secs) {
|
|
if (secs == 0) {
|
|
powerOff(); // Not wake up
|
|
} else {
|
|
shutdownPeripherals();
|
|
mesh_nrf52::turnOffShutdownLeds();
|
|
|
|
// Clear FPU interrupt flags to avoid insomnia
|
|
// see errata 87 for details
|
|
// https://docs.nordicsemi.com/bundle/errata_nRF52840_Rev3/page/ERR/nRF52840/Rev3/latest/anomaly_840_87.html
|
|
#if (__FPU_USED == 1)
|
|
__set_FPSCR(__get_FPSCR() & ~(0x0000009F));
|
|
(void)__get_FPSCR();
|
|
NVIC_ClearPendingIRQ(FPU_IRQn);
|
|
#endif
|
|
|
|
// Attemp to sleep
|
|
vTaskDelay(pdMS_TO_TICKS(secs * 1000));
|
|
|
|
// Reboot for a fresh recovery
|
|
reboot();
|
|
}
|
|
}
|
|
|
|
bool NRF52Board::getBootloaderVersion(char* out, size_t max_len) {
|
|
if (!out || max_len == 0u) return false;
|
|
out[0] = 0;
|
|
mesh::Nrf52BootloaderRegion region;
|
|
const uint32_t mbr_start = *(const volatile uint32_t*)(uintptr_t)0xFF8u;
|
|
if (!mesh::nrf52BootloaderRegion(NRF_FICR->CODEPAGESIZE, NRF_FICR->CODESIZE,
|
|
mbr_start, NRF_UICR->NRFFW[0], region)) return false;
|
|
return mesh::nrf52BootloaderVersion((const uint8_t*)(uintptr_t)region.start,
|
|
region.end - region.start, region.start,
|
|
bootloaderVersion, out, max_len);
|
|
}
|
|
|
|
bool NRF52Board::startOTAUpdate(const char *id, char reply[], bool force_ap) {
|
|
(void)id;
|
|
(void)force_ap;
|
|
|
|
if (ota_active) {
|
|
format_ota_reply(reply);
|
|
return true;
|
|
}
|
|
|
|
if (!ota_ble_started) {
|
|
// The application only needs the buttonless DFU control connection; the
|
|
// bootloader handles firmware data. Large ATT and queue reservations can
|
|
// make SoftDevice startup fail on an otherwise healthy repeater.
|
|
// All config***() calls must precede begin().
|
|
Bluefruit.configPrphConn(BLE_GATT_ATT_MTU_DEFAULT,
|
|
BLE_GAP_EVENT_LENGTH_MIN, 2, 1);
|
|
|
|
// Even a failed begin can leave SoftDevice enabled partway through SDK
|
|
// startup. Require a fresh authoritative query before the next sleep.
|
|
mesh_nrf52::sleepCycleState().invalidate();
|
|
mesh_nrf52::applyBluetoothLedPolicy();
|
|
if (!Bluefruit.begin(1, 0)) {
|
|
return false;
|
|
}
|
|
ota_ble_started = true;
|
|
mesh_nrf52::bluetoothLedStarted();
|
|
|
|
// To be consistent OTA DFU should be added first if it exists
|
|
bledfu.begin();
|
|
}
|
|
|
|
// Set max power. Accepted values are: -40, -30, -20, -16, -12, -8, -4, 0, 4
|
|
Bluefruit.setTxPower(4);
|
|
// Set the BLE device name
|
|
Bluefruit.setName(ota_name);
|
|
|
|
Bluefruit.Periph.setConnectCallback(connect_callback);
|
|
Bluefruit.Periph.setDisconnectCallback(disconnect_callback);
|
|
|
|
Bluefruit.Advertising.clearData();
|
|
Bluefruit.ScanResponse.clearData();
|
|
|
|
// Bluefruit.addName() can copy bytes past the GAP name when the optimized
|
|
// getName() reports the buffer capacity. Advertise the board's known name
|
|
// directly so DFU scanners see an exact, complete local name.
|
|
const size_t ota_name_len = strlen(ota_name);
|
|
if (ota_name_len > UINT8_MAX
|
|
|| !Bluefruit.Advertising.addFlags(BLE_GAP_ADV_FLAGS_LE_ONLY_GENERAL_DISC_MODE)
|
|
|| !Bluefruit.Advertising.addTxPower()
|
|
|| !Bluefruit.Advertising.addData(BLE_GAP_AD_TYPE_COMPLETE_LOCAL_NAME,
|
|
ota_name, (uint8_t)ota_name_len)
|
|
|| !Bluefruit.ScanResponse.addService(bledfu)) {
|
|
return false;
|
|
}
|
|
|
|
/* Start Advertising
|
|
- Enable auto advertising if disconnected
|
|
- Interval: fast mode = 20 ms, slow mode = 152.5 ms
|
|
- Timeout for fast mode is 30 seconds
|
|
- Start(timeout) with timeout = 0 will advertise forever (until connected)
|
|
|
|
For recommended advertising interval
|
|
https://developer.apple.com/library/content/qa/qa1931/_index.html
|
|
*/
|
|
Bluefruit.Advertising.restartOnDisconnect(true);
|
|
Bluefruit.Advertising.setInterval(32, 244); // in unit of 0.625 ms
|
|
Bluefruit.Advertising.setFastTimeout(30); // number of seconds in fast mode
|
|
if (!Bluefruit.Advertising.start(0)) return false; // 0 = advertise until stopped
|
|
|
|
ota_active = true;
|
|
format_ota_reply(reply);
|
|
|
|
return true;
|
|
}
|
|
|
|
bool NRF52Board::stopOTAUpdate(char reply[]) {
|
|
if (!ota_active) {
|
|
strcpy(reply, "OK - OTA not running");
|
|
return true;
|
|
}
|
|
|
|
Bluefruit.Advertising.restartOnDisconnect(false);
|
|
Bluefruit.Advertising.stop();
|
|
if (ota_conn_handle != BLE_CONN_HANDLE_INVALID) {
|
|
sd_ble_gap_disconnect(ota_conn_handle, BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
|
|
ota_conn_handle = BLE_CONN_HANDLE_INVALID;
|
|
}
|
|
ota_active = false;
|
|
|
|
strcpy(reply, "OK - OTA stopped");
|
|
return true;
|
|
}
|
|
#endif
|