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.

Added src/helpers/nrf52/Nrf52EarlyBoot.cpp:
- Early register capture before SystemInit()

Added src/helpers/nrf52/Nrf52PowerMgt.h:
- Phase 1 API (PowerMgtConfig, PowerMgtState, initState, checkBootVoltage, configureLpcompWake, enterSystemOff)

Added src/helpers/nrf52/Nrf52PowerMgt.cpp:
- Initial Implementation

Modified src/MeshCore.h:
- Added power management virtual methods to MainBoard

Added first supported board (Xiao nRF52840) for testing.
@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.

- Simplified and brought power management code into NRF52Board
- Switched to using GPREGRET2 instead of GPREGRET to avoid clashing with bootloader use of GPREGRET
@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

Comment on lines +58 to +59
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?

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.

3 participants