Skip to content

Conversation

@entr0p1
Copy link

@entr0p1 entr0p1 commented Jan 17, 2026

This PR implements initial stages of power management functionality for nRF52840 nodes to prevent lockups and flash corruption associated with low voltage. This is designed to be modular and enabled per-variant.

Phase 1 (this PR) includes:

  • Startup lockout - minimum voltage reading from VBAT is required in order to proceed with the boot. If voltage is deemed too low, the board will enter the shutdown state. This is gated if there is external power present (e.g. 5V, USB) to prevent erroneous lockouts if the board is charging.
  • LPCOMP wake - when voltage readings from the battery return to a healthy state (i.e. indicating charging), a board reset is issued and the board will boot back up without external intervention. This is enabled when shutdown is initiated from Startup lockout.

@entr0p1
Copy link
Author

entr0p1 commented Jan 17, 2026

Leaving this in a draft state so it can be tested as this is a major refactor to centralise more of the code away from individual variants, and tidy up general sloppiness/hacky code from the v1 implementation.

@mattzzw
Copy link
Contributor

mattzzw commented Jan 18, 2026

Would this work also e.g. with nfr52 based systems that do not have voltage divider resistors for the ADC port in place? (Thinking e.g. of ProMicro)

@entr0p1
Copy link
Author

entr0p1 commented Jan 18, 2026

Would this work also e.g. with nfr52 based systems that do not have voltage divider resistors for the ADC port in place? (Thinking e.g. of ProMicro)

Great question, likely not (at least in the current implementation). I've been trying to track down a concrete schematic for the promicro but because there are so many iterations of it, I can't get a reliable view of the board. If you happen to have one, that would go a long way to getting a more definitive answer.

@mattzzw
Copy link
Contributor

mattzzw commented Jan 18, 2026

The ProMicro target is based on the "faketec" PCB (https://github.com/gargomoma/fakeTec_pcb). There are multiple variations of the PCB but mostly adding peripherals like FETs, power regulators, solar chargers etc.
I doubt that the circuitry for measuring battery voltage is different for the multiple PCB versions.

I was only able to find the schematic for V5
V5: https://github.com/gargomoma/fakeTec_pcb/blob/main/design_files/ShimonHoranek_fakeTecv5schematics.pdf

@entr0p1
Copy link
Author

entr0p1 commented Jan 18, 2026

OK so short answer is yes it should work:

  • According to the schematic the pin mapping should match the Xiao nRF52840 that I've been testing with (PIN_VBAT_READ (17) -> P0.31 = AIN7). It will work with LPCOMP wake.
  • If the caveats below prove problematic, there is actually a hardware BMS in the schematic which provides over-discharge protection (so you can fall back on this). Not sure if it auto-recovers though but you would think it would.

There are however a couple of caveats (citation needed from people who know this stuff better):

  • The voltage divider is high-impedance (20MOhm) which may cause it to be a tad inaccurate but I'm not certain
  • The current ADC_MULTIPLIER value in our code is "1.815" which, if the above is true, we should actually have it set to 2.0

@entr0p1
Copy link
Author

entr0p1 commented Jan 18, 2026

Sometimes we just need to send it and see. I'll add in code for the promicro variant if you're willing to test.

@entr0p1
Copy link
Author

entr0p1 commented Jan 18, 2026

I've found some duplication in the code so will undo the commits, fix that up and force push the branch back up to keep things tidy. I'll add the promicro support at the same time.

@oltaco
Copy link
Contributor

oltaco commented Jan 19, 2026

Would this work also e.g. with nfr52 based systems that do not have voltage divider resistors for the ADC port in place? (Thinking e.g. of ProMicro)

If you can't measure battery voltage then it's not going to work. I personally haven't seen any promicro-based boards that don't implement a voltage divider for battery reading though

@oltaco
Copy link
Contributor

oltaco commented Jan 19, 2026

  • The current ADC_MULTIPLIER value in our code is "1.815" which, if the above is true, we should actually have it set to 2.0

The thing is that promicro based boards don't necessarily all use the same voltage dividers. Promicro repeaters do allow setting the adc.multiplier value via the CLI though 👍

Comment on lines 10 to 14
// 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to be careful here, as the bootloader uses GPREGRET as well, but I think these values will be OK.

#define DFU_MAGIC_OTA_APPJUM BOOTLOADER_DFU_START // 0xB1
#define DFU_MAGIC_OTA_RESET 0xA8
#define DFU_MAGIC_SERIAL_ONLY_RESET 0x4e
#define DFU_MAGIC_UF2_RESET 0x57
#define DFU_MAGIC_SKIP 0x6d

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Found this out while going over it again, there's apparently a GPREGRET2 we can use. Just looking into that.

@entr0p1 entr0p1 force-pushed the powermgt-nrf52840-v2 branch from ee73ddb to 94376f8 Compare January 19, 2026 13:58
@entr0p1
Copy link
Author

entr0p1 commented Jan 19, 2026

Cleaned up and simplfied the code (thanks to those who helped behind the scenes). Have switched GPREGRET over to GPREGRET2 and run a manual shutdown via the same route a boot lockout shutdown would take, and confirmed the LPCOMP wake works and so does the GPREGRET2 and RESETREAS value reads.

Ready for a review I think. I've also added the promicro board support for others to test.

@entr0p1 entr0p1 marked this pull request as ready for review January 19, 2026 14:02
@entr0p1
Copy link
Author

entr0p1 commented Jan 20, 2026

BTW - once the reviews are done and the devs are happy, I'll clean up the commits into one.

Copy link
Contributor

@oltaco oltaco left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, just a few comments/suggestions 👍

Comment on lines 43 to 46
#ifdef NRF52_POWER_MANAGEMENT
// Boot voltage protection check (may not return if voltage too low)
checkBootVoltage(&power_config);
#endif
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this happen before the power is supplied to the radio a few lines above?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good call, I missed that. Fixed up on local and will be in the next commit.

#ifdef NRF52_POWER_MANAGEMENT
bool isExternalPowered() override;
uint16_t getBootVoltage() override { return boot_voltage_mv; }
virtual uint32_t getResetReason() const override { return reset_reason; }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as above can just use readResetReason()

Comment on lines +30 to +36
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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

reset reason can be accessed by readResetReason(), adafruit caches the value at early startup before it gets cleared.
GPREGRET2 shouldn't get cleared so you can access that at anytime, but if you use readResetReason() you probably don't need to force priority

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I noticed anecdotally online that the adafruit version can be a bit clunky/unreliable sometimes, have you found it to work OK? If so, I can change it over. GPREGRET2 is configured now though and working.

Comment on lines 58 to 60
configureLpcompWake(power_config.lpcomp_ain_channel, power_config.lpcomp_ref_eighths);
enterSystemOff(SHUTDOWN_REASON_USER);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should LPCOMP wake be disabled when the user has explicitly requested shutdown via the UI?

Copy link
Author

@entr0p1 entr0p1 Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was in two minds about this when I added it and still am. What I'm trying to achieve is a means for the user to force a power management state change early or if they wish to test how it will respond with their setup.

I wanted to differentiate this without people getting arthritis in their thumbs from typing some long arduous command like "set pwrmgt.state shutdownandwake". I think there's probably merit in allowing both ways to exist; shutdown with and without LPCOMP wake.

What do you think?

@entr0p1 entr0p1 force-pushed the powermgt-nrf52840-v2 branch from 94376f8 to 346c9d3 Compare January 21, 2026 12:32
@entr0p1
Copy link
Author

entr0p1 commented Jan 21, 2026

Alright, commits cleaned up and have cleaned up the code a bit more and fixed some sequencing issues for some boards. Its now implemented for the Heltec T114, Xiao nRF52840, RAK4631, and Promicro. Let me know how this one looks, thanks everyone for all your effort going over this with me 100 times :)

@entr0p1
Copy link
Author

entr0p1 commented Jan 21, 2026

FYI some testing is showing reliability issues for the Promicro. I'm working on it, will mark this as a draft again just so we don't merge bad code. Should have a solution in the next ~24hrs.

@entr0p1 entr0p1 marked this pull request as draft January 21, 2026 22:31
@oltaco
Copy link
Contributor

oltaco commented Jan 21, 2026

As noted by @entr0p1 I had some issues during initial testing with a promicro last night. Low voltage boot lockout works as expected but LPCOMP doesn't wake reliably on rise using a lab power supply hooked up to the battery input. I will test some more with Xiao NRF52 / RAK4631 / Heltec T114 tonight and see what the results are.

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
@entr0p1 entr0p1 force-pushed the powermgt-nrf52840-v2 branch from 346c9d3 to 1f59e52 Compare January 23, 2026 06:19
@entr0p1 entr0p1 marked this pull request as ready for review January 23, 2026 06:20
@entr0p1
Copy link
Author

entr0p1 commented Jan 23, 2026

Another big cleanup. VBUS wake added, lockout voltage lowered (to avoid breaking LiFePo4 batteries attached to battery pins), dropped the promicro support due to the inconsistencies with the various flavours of the board.

@mattzzw
Copy link
Contributor

mattzzw commented Jan 23, 2026

Looking through the nrf52840 datasheet I stumbled across the POFCON register (Address offset: 0x510
Power-fail comparator configuration).
I have not seen references to the POF in this PR. Would this be something to consider?

Using the power-fail comparator (POF) is optional. When enabled, it can provide an early warning to the
CPU of an impending power supply failure.
To enable and configure the power-fail comparator, see the register POFCON on page 97.
When the supply voltage falls below the defined threshold, the power-fail comparator generates an event
(POFWARN) that can be used by an application to prepare for power failure. This event is also generated
when the supply voltage is already below the threshold at the time the power-fail comparator is enabled,
or if the threshold is re-configured to a level above the supply voltage.

Edit: combined with the ability to enter a proper reset state caused by brown out detection, this could help bringing back NRf based solar repeaters that died because of a lack of sunlight.

@ripplebiz ripplebiz merged commit e744adf into meshcore-dev:dev Jan 24, 2026
@entr0p1
Copy link
Author

entr0p1 commented Jan 24, 2026

I have not seen references to the POF in this PR. Would this be something to consider?

Good pickup!

Short answer - yes, it was considered and from what I can understand of the codebase, I came to the conclusion it won't work with the current software architecture (bluefruit limitation, not MeshCore).

Long answer - we can configure POF just fine, but we can't actually pick up any events from it to then take action from. There are 3 public APIs (via softdevice) we can call for POF:

  • sd_power_pof_threshold_set
  • sd_power_pof_thresholdvddh_set
  • sd_power_pof_enable

Once configured, if POF is tripped, it will fire the event NRF_EVT_POWER_FAILURE_WARNING. Normally we can learn of this event firing via the SoC event queue using sd_evt_get(), but we have something else in play here; Bluefruit. Bluefruit already has a loop in its code that calls sd_evt_get() and takes action based on the events). Unfortunately NRF_EVT_POWER_FAILURE_WARNING is not one of those events.

The next issue is calling sd_evt_get() actually clears the event queue. So even if we decided to run our own loop to pick up the POF event, we ultimately add a race between us and bluefruit and risk either:
a) clearing important events that Bluefruit might need to handle for us
b) not getting the event at all because Bluefruit got there first

This boils down to us needing to either modify Bluefruit to handle the event (which means we now have to maintain that as well), or we need to find a way that doesn't involve POF. I decided the latter. Read on if you want more details.

Even longer answer:
There are more phases of this power management code; the first (this PR) being pre-runtime focused: low-voltage boot lockout with (attempted) self-recovery via a board reset when voltage is sane again.

Later phases focus on runtime (MeshCore is already booted and running) and introduce "states" the board will enter to prevent brownout. Again, these are based on battery voltage thresholds.

These states are:

  1. Conserve: Warning level threshold - this is entirely in software. When the threshold is crossed, load shedding is activated. This is meant to help the board survive short-term issues like a low battery overnight. The following are disabled in this state: All adverts, repeats, guest logins.
  2. Sleep (nRF Deep Sleep): Critical level threshold - all radio activity is suspended, and the board is placed into deep sleep with a wake timer. On wake, the board bumps the clock forward (to ensure we keep it in-sync), rescans the voltage and take action based on the reading:
    a) Voltage still in sleep threshold - Set another wake timer and go back to sleep
    b) Voltage has recovered (multiple consecutive readings) - transition to Conserve (if in threshold) or back to Normal operations
    c) Voltage is worse - transition to next severity state if threshold matched
  3. Shutdown (SYSTEMOFF): "Danger zone" threshold - a brownout is imminent and at best we are facing CPU instability/lockup, at worst flash corruption. From here, we configure LPCOMP and VBUS wake if available (so the board can power itself back up if things normalise), and power down the board completely (go to SYSTEMOFF state).

It might sound heavy-handed, but for perspective from my tests (zero charging, using a single 2600mAh Li-ion cell) it took:

  • ~2-3 days to reach Conserve
  • ~3 days to reach deep sleep
  • ~3-5 days to reach shutdown

Basically if I wanted to test this, I had to set aside 2 full weeks before the board actually would turn off fully.

As with the boot lockout, there is a gate in the states code that says if external power is applied to the board's USB/5V input (VBUS), we immediately return to normal state (or boot back up) and we do not transition to any states. Also, since this feature runs at runtime (i.e. after the flash has been initialised), we can have it as an optional thing so the user can choose if they want it or not. I have it disabled by default in my own branch.

At this stage its too early to tell if this will be a desirable feature for both the users and maintainers, however I can attest to it working extremely well and buying literal weeks of time and reducing my board flash death rate from "almost guaranteed" to "not a single occurrence" :) We will see how the boot lockout code is received and go from there.

@mattzzw
Copy link
Contributor

mattzzw commented Jan 24, 2026

Thanks a lot for taking the time to get back to my question and giving such an elaborate and interesting answer. Much appreciated.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants