mirror of
https://github.com/meshcore-dev/MeshCore.git
synced 2026-03-29 15:10:10 +00:00
nRF52840 Power Management - Phase 1 - Boot Low VBAT Voltage Lockout
Added NRF52840 power management core functionality: - Boot‑voltage lockout - Initial support for shutdown/reset reason storage and capture (via RESETREAS/GPREGRET2) - LPCOMP wake (for voltage-driven shutdowns) - VBUS wake (for voltage-driven shutdowns) - Per-board shutdown handler for board-specific tasks - Exposed CLI queries for power‑management status in CommonCLI.cpp - Added documentation in docs/nrf52_power_management.md. - Enabled power management support in Xiao nRF52840, RAK4631, Heltec T114 boards
This commit is contained in:
213
docs/nrf52_power_management.md
Normal file
213
docs/nrf52_power_management.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# nRF52 Power Management
|
||||
|
||||
## Overview
|
||||
|
||||
The nRF52 Power Management module provides battery protection features to prevent over-discharge, minimise likelihood of brownout and flash corruption conditions existing, and enable safe voltage-based recovery.
|
||||
|
||||
## Features
|
||||
|
||||
### Boot Voltage Protection
|
||||
- Checks battery voltage immediately after boot and before mesh operations commence
|
||||
- If voltage is below a configurable threshold (e.g., 3300mV), the device configures voltage wake (LPCOMP + VBUS) and enters protective shutdown (SYSTEMOFF)
|
||||
- Prevents boot loops when battery is critically low
|
||||
- Skipped when external power (USB VBUS) is detected
|
||||
|
||||
### Voltage Wake (LPCOMP + VBUS)
|
||||
- Configures the nRF52's Low Power Comparator (LPCOMP) before entering SYSTEMOFF
|
||||
- Enables USB VBUS detection so external power can wake the device
|
||||
- Device automatically wakes when battery voltage rises above recovery threshold or when VBUS is detected
|
||||
|
||||
### Early Boot Register Capture
|
||||
- Captures RESETREAS (reset reason) and GPREGRET2 (shutdown reason) before SystemInit() clears them
|
||||
- Allows firmware to determine why it booted (cold boot, watchdog, LPCOMP wake, etc.)
|
||||
- Allows firmware to determine why it last shut down (user request, low voltage, boot protection)
|
||||
|
||||
### Shutdown Reason Tracking
|
||||
Shutdown reason codes (stored in GPREGRET2):
|
||||
| Code | Name | Description |
|
||||
|------|------|-------------|
|
||||
| 0x00 | NONE | Normal boot / no previous shutdown |
|
||||
| 0x4C | LOW_VOLTAGE | Runtime low voltage threshold reached |
|
||||
| 0x55 | USER | User requested powerOff() |
|
||||
| 0x42 | BOOT_PROTECT | Boot voltage protection triggered |
|
||||
|
||||
## Supported Boards
|
||||
|
||||
| Board | Implemented | LPCOMP wake | VBUS wake |
|
||||
|-------|-------------|-------------|-----------|
|
||||
| Seeed Studio XIAO nRF52840 (`xiao_nrf52`) | Yes | Yes | Yes |
|
||||
| RAK4631 (`rak4631`) | Yes | Yes | Yes |
|
||||
| Heltec T114 (`heltec_t114`) | Yes | Yes | Yes |
|
||||
| Promicro nRF52840 | No | No | No |
|
||||
| RAK WisMesh Tag | No | No | No |
|
||||
| Heltec Mesh Solar | No | No | No |
|
||||
| LilyGo T-Echo / T-Echo Lite | No | No | No |
|
||||
| SenseCAP Solar | No | No | No |
|
||||
| WIO Tracker L1 / L1 E-Ink | No | No | No |
|
||||
| WIO WM1110 | No | No | No |
|
||||
| Mesh Pocket | No | No | No |
|
||||
| Nano G2 Ultra | No | No | No |
|
||||
| ThinkNode M1/M3/M6 | No | No | No |
|
||||
| T1000-E | No | No | No |
|
||||
| Ikoka Nano/Stick/Handheld (nRF) | No | No | No |
|
||||
| Keepteen LT1 | No | No | No |
|
||||
| Minewsemi ME25LS01 | No | No | No |
|
||||
|
||||
Notes:
|
||||
- "Implemented" reflects Phase 1 (boot lockout + shutdown reason capture).
|
||||
- User power-off on Heltec T114 does not enable LPCOMP wake.
|
||||
- VBUS detection is used to skip boot lockout on external power, and VBUS wake is configured alongside LPCOMP when supported hardware exposes VBUS to the nRF52.
|
||||
|
||||
## Technical Details
|
||||
|
||||
### Architecture
|
||||
|
||||
The power management functionality is integrated into the `NRF52Board` base class in `src/helpers/NRF52Board.cpp`. Board variants provide hardware-specific configuration via a `PowerMgtConfig` struct and override `initiateShutdown(uint8_t reason)` to perform board-specific power-down work and conditionally enable voltage wake (LPCOMP + VBUS).
|
||||
|
||||
### Early Boot Capture
|
||||
|
||||
A static constructor with priority 101 in `NRF52Board.cpp` captures the RESETREAS and GPREGRET2 registers before:
|
||||
- SystemInit() (priority 102) - which clears RESETREAS
|
||||
- Static C++ constructors (default priority 65535)
|
||||
|
||||
This ensures we capture the true reset reason before any initialisation code runs.
|
||||
|
||||
### Board Implementation
|
||||
|
||||
To enable power management on a board variant:
|
||||
|
||||
1. **Enable in platformio.ini**:
|
||||
```ini
|
||||
-D NRF52_POWER_MANAGEMENT
|
||||
```
|
||||
|
||||
2. **Define configuration in variant.h**:
|
||||
```c
|
||||
#define PWRMGT_VOLTAGE_BOOTLOCK 3300 // Won't boot below this voltage (mV)
|
||||
#define PWRMGT_LPCOMP_AIN 7 // AIN channel for voltage sensing
|
||||
#define PWRMGT_LPCOMP_REFSEL 2 // REFSEL (0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)
|
||||
```
|
||||
|
||||
3. **Implement in board .cpp file**:
|
||||
```cpp
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
const PowerMgtConfig power_config = {
|
||||
.lpcomp_ain_channel = PWRMGT_LPCOMP_AIN,
|
||||
.lpcomp_refsel = PWRMGT_LPCOMP_REFSEL,
|
||||
.voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK
|
||||
};
|
||||
|
||||
void MyBoard::initiateShutdown(uint8_t reason) {
|
||||
// Board-specific shutdown preparation (e.g., disable peripherals)
|
||||
bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE ||
|
||||
reason == SHUTDOWN_REASON_BOOT_PROTECT);
|
||||
|
||||
if (enable_lpcomp) {
|
||||
configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);
|
||||
}
|
||||
|
||||
enterSystemOff(reason);
|
||||
}
|
||||
#endif
|
||||
|
||||
void MyBoard::begin() {
|
||||
NRF52Board::begin(); // or NRF52BoardDCDC::begin()
|
||||
// ... board setup ...
|
||||
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
checkBootVoltage(&power_config);
|
||||
#endif
|
||||
}
|
||||
```
|
||||
|
||||
For user-initiated shutdowns, `powerOff()` remains board-specific. Power management only arms LPCOMP for automated shutdown reasons (boot protection/low voltage).
|
||||
|
||||
4. **Declare override in board .h file**:
|
||||
```cpp
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
void initiateShutdown(uint8_t reason) override;
|
||||
#endif
|
||||
```
|
||||
|
||||
### Voltage Wake Configuration
|
||||
|
||||
The LPCOMP (Low Power Comparator) is configured to:
|
||||
- Monitor the specified AIN channel (0-7 corresponding to P0.02-P0.05, P0.28-P0.31)
|
||||
- Compare against VDD fraction reference (REFSEL: 0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16)
|
||||
- Detect UP events (voltage rising above threshold)
|
||||
- Use 50mV hysteresis for noise immunity
|
||||
- Wake the device from SYSTEMOFF when triggered
|
||||
|
||||
VBUS wake is enabled via the POWER peripheral USBDETECTED event whenever `configureVoltageWake()` is used. This requires USB VBUS to be routed to the nRF52 (typical on nRF52840 boards with native USB).
|
||||
|
||||
**LPCOMP Reference Selection (PWRMGT_LPCOMP_REFSEL)**:
|
||||
| REFSEL | Fraction | VBAT @ 1M/1M divider (VDD=3.0-3.3) | VBAT @ 1.5M/1M divider (VDD=3.0-3.3) |
|
||||
|--------|----------|------------------------------------|--------------------------------------|
|
||||
| 0 | 1/8 | 0.75-0.82 V | 0.94-1.03 V |
|
||||
| 1 | 2/8 | 1.50-1.65 V | 1.88-2.06 V |
|
||||
| 2 | 3/8 | 2.25-2.47 V | 2.81-3.09 V |
|
||||
| 3 | 4/8 | 3.00-3.30 V | 3.75-4.12 V |
|
||||
| 4 | 5/8 | 3.75-4.12 V | 4.69-5.16 V |
|
||||
| 5 | 6/8 | 4.50-4.95 V | 5.62-6.19 V |
|
||||
| 6 | 7/8 | 5.25-5.77 V | 6.56-7.22 V |
|
||||
| 7 | ARef | - | - |
|
||||
| 8 | 1/16 | 0.38-0.41 V | 0.47-0.52 V |
|
||||
| 9 | 3/16 | 1.12-1.24 V | 1.41-1.55 V |
|
||||
| 10 | 5/16 | 1.88-2.06 V | 2.34-2.58 V |
|
||||
| 11 | 7/16 | 2.62-2.89 V | 3.28-3.61 V |
|
||||
| 12 | 9/16 | 3.38-3.71 V | 4.22-4.64 V |
|
||||
| 13 | 11/16 | 4.12-4.54 V | 5.16-5.67 V |
|
||||
| 14 | 13/16 | 4.88-5.36 V | 6.09-6.70 V |
|
||||
| 15 | 15/16 | 5.62-6.19 V | 7.03-7.73 V |
|
||||
|
||||
**Important**: For boards with a voltage divider on the battery sense pin, LPCOMP measures the divided voltage. Use:
|
||||
`VBAT_threshold ≈ (VDD * fraction) * divider_scale`, where `divider_scale = (Rtop + Rbottom) / Rbottom` (e.g., 2.0 for 1M/1M, 2.5 for 1.5M/1M, 3.0 for XIAO).
|
||||
|
||||
### SoftDevice Compatibility
|
||||
|
||||
The power management code checks whether SoftDevice is enabled and uses the appropriate API:
|
||||
- When SD enabled: `sd_power_*` functions
|
||||
- When SD disabled: Direct register access (NRF_POWER->*)
|
||||
|
||||
This ensures compatibility regardless of BLE stack state.
|
||||
|
||||
## CLI Commands
|
||||
|
||||
Power management status can be queried via the CLI:
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `get pwrmgt.support` | Returns "supported" or "unsupported" |
|
||||
| `get pwrmgt.source` | Returns current power source - "battery" or "external" (5V/USB power) |
|
||||
| `get pwrmgt.bootreason` | Returns reset and shutdown reason strings |
|
||||
| `get pwrmgt.bootmv` | Returns boot voltage in millivolts |
|
||||
|
||||
On boards without power management enabled, all commands except `get pwrmgt.support` return:
|
||||
```
|
||||
ERROR: Power management not supported
|
||||
```
|
||||
|
||||
## Debug Output
|
||||
|
||||
When `MESH_DEBUG=1` is enabled, the power management module outputs:
|
||||
```
|
||||
DEBUG: PWRMGT: Reset = Wake from LPCOMP (0x20000); Shutdown = Low Voltage (0x4C)
|
||||
DEBUG: PWRMGT: Boot voltage = 3450 mV (threshold = 3300 mV)
|
||||
DEBUG: PWRMGT: LPCOMP wake configured (AIN7, ref=3/8 VDD)
|
||||
```
|
||||
|
||||
## Phase 2 (Planned)
|
||||
|
||||
- Runtime voltage monitoring
|
||||
- Voltage state machine (Normal -> Warning -> Critical -> Shutdown)
|
||||
- Configurable thresholds
|
||||
- Load shedding callbacks for power reduction
|
||||
- Deep sleep integration
|
||||
- Scheduled wake-up
|
||||
- Extended sleep with periodic monitoring
|
||||
|
||||
## References
|
||||
|
||||
- [nRF52840 Product Specification - POWER](https://infocenter.nordicsemi.com/topic/ps_nrf52840/power.html)
|
||||
- [nRF52840 Product Specification - LPCOMP](https://infocenter.nordicsemi.com/topic/ps_nrf52840/lpcomp.html)
|
||||
- [SoftDevice S140 API - Power Management](https://infocenter.nordicsemi.com/topic/sdk_nrf5_v17.1.0/group__nrf__sdm__api.html)
|
||||
@@ -56,6 +56,14 @@ public:
|
||||
virtual void setGpio(uint32_t values) {}
|
||||
virtual uint8_t getStartupReason() const = 0;
|
||||
virtual bool startOTAUpdate(const char* id, char reply[]) { return false; } // not supported
|
||||
|
||||
// Power management interface (boards with power management override these)
|
||||
virtual bool isExternalPowered() { return false; }
|
||||
virtual uint16_t getBootVoltage() { return 0; }
|
||||
virtual uint32_t getResetReason() const { return 0; }
|
||||
virtual const char* getResetReasonString(uint32_t reason) { return "Not available"; }
|
||||
virtual uint8_t getShutdownReason() const { return 0; }
|
||||
virtual const char* getShutdownReasonString(uint8_t reason) { return "Not available"; }
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -364,6 +364,33 @@ void CommonCLI::handleCommand(uint32_t sender_timestamp, const char* command, ch
|
||||
} else {
|
||||
sprintf(reply, "> %.3f", adc_mult);
|
||||
}
|
||||
// Power management commands
|
||||
} else if (memcmp(config, "pwrmgt.support", 14) == 0) {
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
strcpy(reply, "> supported");
|
||||
#else
|
||||
strcpy(reply, "> unsupported");
|
||||
#endif
|
||||
} else if (memcmp(config, "pwrmgt.source", 13) == 0) {
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
strcpy(reply, _board->isExternalPowered() ? "> external" : "> battery");
|
||||
#else
|
||||
strcpy(reply, "ERROR: Power management not supported");
|
||||
#endif
|
||||
} else if (memcmp(config, "pwrmgt.bootreason", 17) == 0) {
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
sprintf(reply, "> Reset: %s; Shutdown: %s",
|
||||
_board->getResetReasonString(_board->getResetReason()),
|
||||
_board->getShutdownReasonString(_board->getShutdownReason()));
|
||||
#else
|
||||
strcpy(reply, "ERROR: Power management not supported");
|
||||
#endif
|
||||
} else if (memcmp(config, "pwrmgt.bootmv", 13) == 0) {
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
sprintf(reply, "> %u mV", _board->getBootVoltage());
|
||||
#else
|
||||
strcpy(reply, "ERROR: Power management not supported");
|
||||
#endif
|
||||
} else {
|
||||
sprintf(reply, "??: %s", config);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "NRF52Board.h"
|
||||
|
||||
#include <bluefruit.h>
|
||||
#include <nrf_soc.h>
|
||||
|
||||
static BLEDfu bledfu;
|
||||
|
||||
@@ -21,6 +22,222 @@ void NRF52Board::begin() {
|
||||
startup_reason = BD_STARTUP_NORMAL;
|
||||
}
|
||||
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
#include "nrf.h"
|
||||
|
||||
// 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() {
|
||||
// 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;
|
||||
sd_softdevice_is_enabled(&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);
|
||||
}
|
||||
}
|
||||
|
||||
bool NRF52Board::isExternalPowered() {
|
||||
// Check if SoftDevice is enabled before using its API
|
||||
uint8_t sd_enabled = 0;
|
||||
sd_softdevice_is_enabled(&sd_enabled);
|
||||
|
||||
if (sd_enabled) {
|
||||
uint32_t usb_status;
|
||||
sd_power_usbregstatus_get(&usb_status);
|
||||
return (usb_status & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
|
||||
} else {
|
||||
return (NRF_POWER->USBREGSTATUS & POWER_USBREGSTATUS_VBUSDETECT_Msk) != 0;
|
||||
}
|
||||
}
|
||||
|
||||
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_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) {
|
||||
initPowerMgr();
|
||||
|
||||
// Read boot voltage
|
||||
boot_voltage_mv = getBattMilliVolts();
|
||||
|
||||
if (config->voltage_bootlock == 0) return true; // Protection disabled
|
||||
|
||||
// Skip check if externally powered
|
||||
if (isExternalPowered()) {
|
||||
MESH_DEBUG_PRINTLN("PWRMGT: Boot check skipped (external power)");
|
||||
boot_voltage_mv = getBattMilliVolts();
|
||||
return true;
|
||||
}
|
||||
|
||||
MESH_DEBUG_PRINTLN("PWRMGT: Boot voltage = %u mV (threshold = %u mV)",
|
||||
boot_voltage_mv, config->voltage_bootlock);
|
||||
|
||||
// Only trigger shutdown if reading is valid (>1000mV) AND below threshold
|
||||
// This prevents spurious shutdowns on ADC glitches or uninitialized reads
|
||||
if (boot_voltage_mv > 1000 && boot_voltage_mv < config->voltage_bootlock) {
|
||||
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));
|
||||
|
||||
// Record shutdown reason in GPREGRET2
|
||||
uint8_t sd_enabled = 0;
|
||||
sd_softdevice_is_enabled(&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);
|
||||
|
||||
// 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) {
|
||||
// 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;
|
||||
|
||||
// Enable 50mV hysteresis for noise immunity
|
||||
NRF_LPCOMP->HYST = LPCOMP_HYST_HYST_Hyst50mV;
|
||||
|
||||
// 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 before entering SYSTEMOFF
|
||||
for (uint8_t i = 0; i < 20 && !NRF_LPCOMP->EVENTS_READY; i++) {
|
||||
delayMicroseconds(50);
|
||||
}
|
||||
|
||||
if (refsel == 7) {
|
||||
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (AIN%d, ref=ARef)", ain_channel);
|
||||
} else if (refsel <= 6) {
|
||||
MESH_DEBUG_PRINTLN("PWRMGT: LPCOMP wake configured (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 configured (AIN%d, ref=%d/16 VDD)",
|
||||
ain_channel, ref_num);
|
||||
}
|
||||
|
||||
// Configure VBUS (USB power) wake alongside LPCOMP
|
||||
uint8_t sd_enabled = 0;
|
||||
sd_softdevice_is_enabled(&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 configured");
|
||||
}
|
||||
#endif
|
||||
|
||||
void NRF52BoardDCDC::begin() {
|
||||
NRF52Board::begin();
|
||||
|
||||
|
||||
@@ -5,15 +5,58 @@
|
||||
|
||||
#if defined(NRF52_PLATFORM)
|
||||
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
// Shutdown Reason Codes (stored in GPREGRET before SYSTEMOFF)
|
||||
#define SHUTDOWN_REASON_NONE 0x00
|
||||
#define SHUTDOWN_REASON_LOW_VOLTAGE 0x4C // 'L' - Runtime low voltage threshold
|
||||
#define SHUTDOWN_REASON_USER 0x55 // 'U' - User requested powerOff()
|
||||
#define SHUTDOWN_REASON_BOOT_PROTECT 0x42 // 'B' - Boot voltage protection
|
||||
|
||||
// Boards provide this struct with their hardware-specific settings and callbacks.
|
||||
struct PowerMgtConfig {
|
||||
// LPCOMP wake configuration (for voltage recovery from SYSTEMOFF)
|
||||
uint8_t lpcomp_ain_channel; // AIN0-7 for voltage sensing pin
|
||||
uint8_t lpcomp_refsel; // REFSEL value: 0-6=1/8..7/8, 7=ARef, 8-15=1/16..15/16
|
||||
|
||||
// Boot protection voltage threshold (millivolts)
|
||||
// Set to 0 to disable boot protection
|
||||
uint16_t voltage_bootlock;
|
||||
};
|
||||
#endif
|
||||
|
||||
class NRF52Board : public mesh::MainBoard {
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
void initPowerMgr();
|
||||
#endif
|
||||
|
||||
protected:
|
||||
uint8_t startup_reason;
|
||||
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
uint32_t reset_reason; // RESETREAS register value
|
||||
uint8_t shutdown_reason; // GPREGRET value (why we entered last SYSTEMOFF)
|
||||
uint16_t boot_voltage_mv; // Battery voltage at boot (millivolts)
|
||||
|
||||
bool checkBootVoltage(const PowerMgtConfig* config);
|
||||
void enterSystemOff(uint8_t reason);
|
||||
void configureVoltageWake(uint8_t ain_channel, uint8_t refsel);
|
||||
virtual void initiateShutdown(uint8_t reason);
|
||||
#endif
|
||||
|
||||
public:
|
||||
virtual void begin();
|
||||
virtual uint8_t getStartupReason() const override { return startup_reason; }
|
||||
virtual float getMCUTemperature() override;
|
||||
virtual void reboot() override { NVIC_SystemReset(); }
|
||||
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
bool isExternalPowered() override;
|
||||
uint16_t getBootVoltage() override { return boot_voltage_mv; }
|
||||
virtual uint32_t getResetReason() const override { return reset_reason; }
|
||||
uint8_t getShutdownReason() const override { return shutdown_reason; }
|
||||
const char* getResetReasonString(uint32_t reason) override;
|
||||
const char* getShutdownReasonString(uint8_t reason) override;
|
||||
#endif
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
@@ -3,6 +3,35 @@
|
||||
#include <Arduino.h>
|
||||
#include <Wire.h>
|
||||
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
// Static configuration for power management
|
||||
// Values come from variant.h defines
|
||||
const PowerMgtConfig power_config = {
|
||||
.lpcomp_ain_channel = PWRMGT_LPCOMP_AIN,
|
||||
.lpcomp_refsel = PWRMGT_LPCOMP_REFSEL,
|
||||
.voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK
|
||||
};
|
||||
|
||||
void T114Board::initiateShutdown(uint8_t reason) {
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
pinMode(GPS_EN, OUTPUT);
|
||||
digitalWrite(GPS_EN, LOW);
|
||||
#endif
|
||||
digitalWrite(SX126X_POWER_EN, LOW);
|
||||
|
||||
bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE ||
|
||||
reason == SHUTDOWN_REASON_BOOT_PROTECT);
|
||||
pinMode(PIN_BAT_CTL, OUTPUT);
|
||||
digitalWrite(PIN_BAT_CTL, enable_lpcomp ? HIGH : LOW);
|
||||
|
||||
if (enable_lpcomp) {
|
||||
configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);
|
||||
}
|
||||
|
||||
enterSystemOff(reason);
|
||||
}
|
||||
#endif // NRF52_POWER_MANAGEMENT
|
||||
|
||||
void T114Board::begin() {
|
||||
NRF52Board::begin();
|
||||
NRF_POWER->DCDCEN = 1;
|
||||
@@ -21,6 +50,11 @@ void T114Board::begin() {
|
||||
#endif
|
||||
|
||||
pinMode(SX126X_POWER_EN, OUTPUT);
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
// Boot voltage protection check (may not return if voltage too low)
|
||||
// We need to call this after we configure SX126X_POWER_EN as output but before we pull high
|
||||
checkBootVoltage(&power_config);
|
||||
#endif
|
||||
digitalWrite(SX126X_POWER_EN, HIGH);
|
||||
delay(10); // give sx1262 some time to power up
|
||||
}
|
||||
@@ -10,6 +10,11 @@
|
||||
#define MV_LSB (3000.0F / 4096.0F) // 12-bit ADC with 3.0V input range
|
||||
|
||||
class T114Board : public NRF52BoardOTA {
|
||||
protected:
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
void initiateShutdown(uint8_t reason) override;
|
||||
#endif
|
||||
|
||||
public:
|
||||
T114Board() : NRF52BoardOTA("T114_OTA") {}
|
||||
void begin();
|
||||
@@ -42,13 +47,13 @@ public:
|
||||
}
|
||||
|
||||
void powerOff() override {
|
||||
#ifdef LED_PIN
|
||||
#ifdef LED_PIN
|
||||
digitalWrite(LED_PIN, HIGH);
|
||||
#endif
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
#endif
|
||||
#if ENV_INCLUDE_GPS == 1
|
||||
pinMode(GPS_EN, OUTPUT);
|
||||
digitalWrite(GPS_EN, LOW);
|
||||
#endif
|
||||
#endif
|
||||
sd_power_system_off();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ build_flags = ${nrf52_base.build_flags}
|
||||
-I variants/heltec_t114
|
||||
-I src/helpers/ui
|
||||
-D HELTEC_T114
|
||||
-D NRF52_POWER_MANAGEMENT
|
||||
-D P_LORA_DIO_1=20
|
||||
-D P_LORA_NSS=24
|
||||
-D P_LORA_RESET=25
|
||||
|
||||
@@ -30,6 +30,14 @@
|
||||
|
||||
#define AREF_VOLTAGE (3.0)
|
||||
|
||||
// Power management boot protection threshold (millivolts)
|
||||
// Set to 0 to disable boot protection
|
||||
#define PWRMGT_VOLTAGE_BOOTLOCK 3300 // Won't boot below this voltage (mV)
|
||||
// LPCOMP wake configuration (voltage recovery from SYSTEMOFF)
|
||||
// AIN2 = P0.04 = BATTERY_PIN / PIN_VBAT_READ
|
||||
#define PWRMGT_LPCOMP_AIN 2
|
||||
#define PWRMGT_LPCOMP_REFSEL 1 // 2/8 VDD (~3.68-4.04V)
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Number of pins
|
||||
|
||||
|
||||
@@ -3,6 +3,28 @@
|
||||
|
||||
#include "RAK4631Board.h"
|
||||
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
// Static configuration for power management
|
||||
// Values set in variant.h defines
|
||||
const PowerMgtConfig power_config = {
|
||||
.lpcomp_ain_channel = PWRMGT_LPCOMP_AIN,
|
||||
.lpcomp_refsel = PWRMGT_LPCOMP_REFSEL,
|
||||
.voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK
|
||||
};
|
||||
|
||||
void RAK4631Board::initiateShutdown(uint8_t reason) {
|
||||
// Disable LoRa module power before shutdown
|
||||
digitalWrite(SX126X_POWER_EN, LOW);
|
||||
|
||||
if (reason == SHUTDOWN_REASON_LOW_VOLTAGE ||
|
||||
reason == SHUTDOWN_REASON_BOOT_PROTECT) {
|
||||
configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);
|
||||
}
|
||||
|
||||
enterSystemOff(reason);
|
||||
}
|
||||
#endif // NRF52_POWER_MANAGEMENT
|
||||
|
||||
void RAK4631Board::begin() {
|
||||
NRF52BoardDCDC::begin();
|
||||
pinMode(PIN_VBAT_READ, INPUT);
|
||||
@@ -21,6 +43,11 @@ void RAK4631Board::begin() {
|
||||
Wire.begin();
|
||||
|
||||
pinMode(SX126X_POWER_EN, OUTPUT);
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
// Boot voltage protection check (may not return if voltage too low)
|
||||
// We need to call this after we configure SX126X_POWER_EN as output but before we pull high
|
||||
checkBootVoltage(&power_config);
|
||||
#endif
|
||||
digitalWrite(SX126X_POWER_EN, HIGH);
|
||||
delay(10); // give sx1262 some time to power up
|
||||
}
|
||||
@@ -30,6 +30,11 @@
|
||||
#define ADC_MULTIPLIER (3 * 1.73 * 1.187 * 1000)
|
||||
|
||||
class RAK4631Board : public NRF52BoardDCDC, public NRF52BoardOTA {
|
||||
protected:
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
void initiateShutdown(uint8_t reason) override;
|
||||
#endif
|
||||
|
||||
public:
|
||||
RAK4631Board() : NRF52BoardOTA("RAK4631_OTA") {}
|
||||
void begin();
|
||||
|
||||
@@ -7,6 +7,7 @@ build_flags = ${nrf52_base.build_flags}
|
||||
-I variants/rak4631
|
||||
-D RAK_4631
|
||||
-D RAK_BOARD
|
||||
-D NRF52_POWER_MANAGEMENT
|
||||
-D PIN_BOARD_SCL=14
|
||||
-D PIN_BOARD_SDA=13
|
||||
-D PIN_GPS_TX=PIN_SERIAL1_RX
|
||||
|
||||
@@ -104,6 +104,14 @@ extern "C"
|
||||
static const uint8_t A7 = PIN_A7;
|
||||
#define ADC_RESOLUTION 14
|
||||
|
||||
// Power management boot protection threshold (millivolts)
|
||||
// Set to 0 to disable boot protection
|
||||
#define PWRMGT_VOLTAGE_BOOTLOCK 3300 // Won't boot below this voltage (mV)
|
||||
// LPCOMP wake configuration (voltage recovery from SYSTEMOFF)
|
||||
// AIN3 = P0.05 = PIN_A0 / PIN_VBAT_READ
|
||||
#define PWRMGT_LPCOMP_AIN 3
|
||||
#define PWRMGT_LPCOMP_REFSEL 4 // 5/8 VDD (~3.13-3.44V)
|
||||
|
||||
// Other pins
|
||||
#define PIN_AREF (2)
|
||||
#define PIN_NFC1 (9)
|
||||
|
||||
@@ -5,12 +5,40 @@
|
||||
|
||||
#include "XiaoNrf52Board.h"
|
||||
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
// Static configuration for power management
|
||||
// Values set in variant.h defines
|
||||
const PowerMgtConfig power_config = {
|
||||
.lpcomp_ain_channel = PWRMGT_LPCOMP_AIN,
|
||||
.lpcomp_refsel = PWRMGT_LPCOMP_REFSEL,
|
||||
.voltage_bootlock = PWRMGT_VOLTAGE_BOOTLOCK
|
||||
};
|
||||
|
||||
void XiaoNrf52Board::initiateShutdown(uint8_t reason) {
|
||||
bool enable_lpcomp = (reason == SHUTDOWN_REASON_LOW_VOLTAGE ||
|
||||
reason == SHUTDOWN_REASON_BOOT_PROTECT);
|
||||
|
||||
pinMode(VBAT_ENABLE, OUTPUT);
|
||||
digitalWrite(VBAT_ENABLE, enable_lpcomp ? LOW : HIGH);
|
||||
|
||||
if (enable_lpcomp) {
|
||||
configureVoltageWake(power_config.lpcomp_ain_channel, power_config.lpcomp_refsel);
|
||||
}
|
||||
|
||||
enterSystemOff(reason);
|
||||
}
|
||||
#endif // NRF52_POWER_MANAGEMENT
|
||||
|
||||
void XiaoNrf52Board::begin() {
|
||||
NRF52BoardDCDC::begin();
|
||||
|
||||
// Configure battery voltage ADC
|
||||
pinMode(PIN_VBAT, INPUT);
|
||||
pinMode(VBAT_ENABLE, OUTPUT);
|
||||
digitalWrite(VBAT_ENABLE, HIGH);
|
||||
digitalWrite(VBAT_ENABLE, LOW); // Enable VBAT divider for reading
|
||||
analogReadResolution(12);
|
||||
analogReference(AR_INTERNAL_3_0);
|
||||
delay(50); // Allow ADC to settle
|
||||
|
||||
#ifdef PIN_USER_BTN
|
||||
pinMode(PIN_USER_BTN, INPUT_PULLUP);
|
||||
@@ -27,9 +55,20 @@ void XiaoNrf52Board::begin() {
|
||||
digitalWrite(P_LORA_TX_LED, HIGH);
|
||||
#endif
|
||||
|
||||
// pinMode(SX126X_POWER_EN, OUTPUT);
|
||||
// digitalWrite(SX126X_POWER_EN, HIGH);
|
||||
delay(10); // give sx1262 some time to power up
|
||||
#ifdef NRF52_POWER_MANAGEMENT
|
||||
// Boot voltage protection check (may not return if voltage too low)
|
||||
checkBootVoltage(&power_config);
|
||||
#endif
|
||||
|
||||
delay(10); // Give sx1262 some time to power up
|
||||
}
|
||||
|
||||
uint16_t XiaoNrf52Board::getBattMilliVolts() {
|
||||
// https://wiki.seeedstudio.com/XIAO_BLE#q3-what-are-the-considerations-when-using-xiao-nrf52840-sense-for-battery-charging
|
||||
// VBAT_ENABLE must be LOW to read battery voltage
|
||||
digitalWrite(VBAT_ENABLE, LOW);
|
||||
int adcvalue = analogRead(PIN_VBAT);
|
||||
return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -6,7 +6,12 @@
|
||||
|
||||
#ifdef XIAO_NRF52
|
||||
|
||||
class XiaoNrf52Board : public NRF52BoardDCDC, public NRF52BoardOTA {
|
||||
class XiaoNrf52Board : public NRF52BoardDCDC, public NRF52BoardOTA {
|
||||
protected:
|
||||
#if NRF52_POWER_MANAGEMENT
|
||||
void initiateShutdown(uint8_t reason) override;
|
||||
#endif
|
||||
|
||||
public:
|
||||
XiaoNrf52Board() : NRF52BoardOTA("XIAO_NRF52_OTA") {}
|
||||
void begin();
|
||||
@@ -20,21 +25,7 @@ public:
|
||||
}
|
||||
#endif
|
||||
|
||||
uint16_t getBattMilliVolts() override {
|
||||
// Please read befor going further ;)
|
||||
// https://wiki.seeedstudio.com/XIAO_BLE#q3-what-are-the-considerations-when-using-xiao-nrf52840-sense-for-battery-charging
|
||||
|
||||
// We can't drive VBAT_ENABLE to HIGH as long
|
||||
// as we don't know wether we are charging or not ...
|
||||
// this is a 3mA loss (4/1500)
|
||||
digitalWrite(VBAT_ENABLE, LOW);
|
||||
int adcvalue = 0;
|
||||
analogReadResolution(12);
|
||||
analogReference(AR_INTERNAL_3_0);
|
||||
delay(10);
|
||||
adcvalue = analogRead(PIN_VBAT);
|
||||
return (adcvalue * ADC_MULTIPLIER * AREF_VOLTAGE) / 4.096;
|
||||
}
|
||||
uint16_t getBattMilliVolts() override;
|
||||
|
||||
const char* getManufacturerName() const override {
|
||||
return "Seeed Xiao-nrf52";
|
||||
|
||||
@@ -9,6 +9,7 @@ build_flags = ${nrf52_base.build_flags}
|
||||
-I variants/xiao_nrf52
|
||||
-UENV_INCLUDE_GPS
|
||||
-D NRF52_PLATFORM
|
||||
-D NRF52_POWER_MANAGEMENT
|
||||
-D XIAO_NRF52
|
||||
-D RADIO_CLASS=CustomSX1262
|
||||
-D WRAPPER_CLASS=CustomSX1262Wrapper
|
||||
|
||||
@@ -75,6 +75,21 @@ static const uint8_t D10 = 10;
|
||||
#define AREF_VOLTAGE (3.0)
|
||||
#define ADC_MULTIPLIER (3.0F) // 1M, 512k divider bridge
|
||||
|
||||
// Power management boot protection threshold (millivolts)
|
||||
// Set to 0 to disable boot protection
|
||||
#define PWRMGT_VOLTAGE_BOOTLOCK 3300 // Won't boot below this voltage
|
||||
|
||||
// LPCOMP wake configuration (voltage recovery from SYSTEMOFF)
|
||||
#define PWRMGT_LPCOMP_AIN 7 // AIN7 = P0.31 = PIN_VBAT
|
||||
// IMPORTANT: The XIAO exposes battery via a resistor divider (ADC_MULTIPLIER = 3.0).
|
||||
// LPCOMP measures the divided voltage, not the battery voltage directly.
|
||||
// Vpin = VDD * (REFSEL fraction), and VBAT ≈ Vpin * ADC_MULTIPLIER.
|
||||
//
|
||||
// Using 3/8 VDD gives a wake threshold above the boot protection point:
|
||||
// - If VDD ≈ 3.0V: VBAT ≈ (3.0 * 3/8) * 3 ≈ 3375mV
|
||||
// - If VDD ≈ 3.3V: VBAT ≈ (3.3 * 3/8) * 3 ≈ 3712mV
|
||||
#define PWRMGT_LPCOMP_REFSEL 2 // 3/8 VDD (~3.38-3.71V)
|
||||
|
||||
static const uint8_t A0 = PIN_A0;
|
||||
static const uint8_t A1 = PIN_A1;
|
||||
static const uint8_t A2 = PIN_A2;
|
||||
|
||||
Reference in New Issue
Block a user