nPM1300 regulator driver desyncs refcount from EN latch when VBAT is in BUCK UVLO (~3.4 V), leaving rail stuck on

Affected

  • zephyr/drivers/regulator/regulator_npm13xx.c (current driver)
  • zephyr/drivers/regulator/regulator_npm1300.c in older NCS / Zephyr trees

Reproduced on a nRF9151 + nPM1300 with BUCK2 configured as a non-always-on 3.3 V rail and VBAT below the BUCK UVLO threshold at boot.

Symptom

regulator_disable(buck2) returns 0 but the EN latch is never cleared on the PMIC. When VBAT recovers above UVLO, BUCK2 powers up again. Workaround we are running today is to bypass the framework and write BUCK2ENCLR (base 0x04, offset 0x03) directly via mfd_npm13xx_reg_write().

Root cause

get_enabled() reads BUCK?STATUS / LDSWSTATUS, which reflect the runtime "powered/regulating" state, not the latched ENABLE register. The two diverge in UVLO/dropout: the EN latch is set but the converter is not regulating, so the STATUS bit reads 0.

regulator_npm13xx_init() passes that false into regulator_common_init(), which therefore initialises data->refcnt = 0. Later, regulator_disable() in drivers/regulator/regulator_common.c short-circuits on the if (data->refcnt > 0) guard and never calls api->disable(), so EN_CLR is never written. Once VBAT comes back, the latched EN bit re-energises the rail.

Above ~3.4 V the bug is invisible: STATUS = 1, refcnt is correctly seeded to 1, and the first regulator_disable() decrements to 0 and writes EN_CLR as expected.

Proposed fix

In regulator_npm13xx_init(), when get_enabled() reports off, explicitly write EN_CLR before calling regulator_common_init(). This re-syncs the HW EN latch with the framework's refcnt = 0 assumption.

If the devicetree node carries regulator-boot-on / regulator-always-on, regulator_common_init() immediately re-enables the rail and bumps refcnt, so behaviour for those nodes is unchanged.

Same change needed in the older regulator_npm1300.c.

Patch

diff --git a/drivers/regulator/regulator_npm13xx.c b/drivers/regulator/regulator_npm13xx.c
--- a/drivers/regulator/regulator_npm13xx.c
+++ b/drivers/regulator/regulator_npm13xx.c
@@ -628,12 +628,33 @@ int regulator_npm13xx_init(const struct device *dev)
        if (!device_is_ready(config->mfd)) {
                return -ENODEV;
        }

        ret = get_enabled(dev, &enabled);
        if (ret < 0) {
                return ret;
        }

+       /*
+        * get_enabled() reads BUCK?STATUS / LDSWSTATUS, which report the
+        * converter's *runtime* powered state, not the latched ENABLE bit.
+        * On a nPM1300 BUCK, when VBAT/VSYS sits in UVLO/dropout (~3.4 V
+        * with hysteresis), the converter is enabled but not regulating, so
+        * STATUS reads 0 even though EN_SET was previously written. If we
+        * seed the regulator framework refcount with that false-negative,
+        * the SW view (refcnt = 0) and HW view (EN latch = 1) desync:
+        * a subsequent regulator_disable() hits the refcnt > 0 guard in
+        * regulator_common.c and returns without issuing EN_CLR, leaving
+        * the rail enabled in HW once VBAT climbs back above UVLO.
+        *
+        * Resync HW with the framework's view: when STATUS says off, force
+        * EN_CLR so the EN latch matches refcnt = 0. If the devicetree
+        * requests boot-on, regulator_common_init() will re-enable below.
+        */
+       if (!enabled) {
+               ret = regulator_npm13xx_disable(dev);
+               if (ret < 0) {
+                       return ret;
+               }
+       }
+
        ret = regulator_common_init(dev, enabled);
        if (ret < 0) {
                return ret;
        }

Repro recipe

  1. Drain VBAT to ~3.3 V. Or use a power supply at that level.
  2. Cold-boot.
  3. Observe BUCK2 EN latch via BUCKSTATUS / BUCK2ENSET after calling regulator_disable() — rail re-energises when VBAT is restored.

Goes away with the patch (or with the manual EN_CLR write).

Related