[PATCH v5 4/4] regulator: qcom-rpmh: Add debounce delay before disabling regulator
From: Jishnu Prakash
Date: Tue Oct 27 2026 - 08:57:09 EST
Some consumers rapidly toggle a regulator off and back on, e.g. a
driver that disables its supply on -EPROBE_DEFER and re-enables it on
the next probe attempt. Sending the disable to RPMh immediately causes
needless power cycling of the rail.
Add a 'qcom,regulator-off-debounce-delay-us' property. When set, a
disable vote queues a cancelable delayed work item instead of sending
the request to RPMh immediately. A subsequent enable vote within the
delay cancels the pending work and leaves the rail untouched; if no
enable vote arrives, the disable is sent once the delay elapses.
This is independent of 'regulator-off-on-delay-us', which enforces a
minimum off time once a disable has actually reached RPMh; the two can
be used together. See the comments at struct rpmh_vreg's @lock,
@debounce_delay and @off_on_delay fields for the locking rationale and
how the two delays interact.
When debounce_delay is configured, off_on_delay is read from the
core-parsed constraint but tracked locally (see @off_on_delay) and
enforced in enable(), since it must be measured from the deferred
RPMh disable rather than from when disable() returns.
Signed-off-by: Jishnu Prakash <jishnu.prakash@xxxxxxxxxxxxxxxx>
---
drivers/regulator/qcom-rpmh-regulator.c | 166 ++++++++++++++++++++++++++++++--
1 file changed, 159 insertions(+), 7 deletions(-)
diff --git a/drivers/regulator/qcom-rpmh-regulator.c b/drivers/regulator/qcom-rpmh-regulator.c
index 25c14de3cd8b..28060bcfe48f 100644
--- a/drivers/regulator/qcom-rpmh-regulator.c
+++ b/drivers/regulator/qcom-rpmh-regulator.c
@@ -5,9 +5,12 @@
#define pr_fmt(fmt) "%s: " fmt, __func__
#include <linux/bits.h>
+#include <linux/delay.h>
#include <linux/err.h>
#include <linux/kernel.h>
+#include <linux/ktime.h>
#include <linux/module.h>
+#include <linux/mutex.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/slab.h>
@@ -15,6 +18,7 @@
#include <linux/regulator/driver.h>
#include <linux/regulator/machine.h>
#include <linux/regulator/of_regulator.h>
+#include <linux/workqueue.h>
#include <soc/qcom/cmd-db.h>
#include <soc/qcom/rpmh.h>
@@ -160,6 +164,33 @@ struct rpmh_vreg_hw_data {
* @voltage_selector: Selector used for get_voltage_sel() and
* set_voltage_sel() callbacks
* @mode: RPMh VRM regulator current framework mode
+ * @rdev: Regulator device pointer registered for this
+ * regulator
+ * @delayed_off_work: Delayed worker used to defer sending a disable
+ * request to RPMh, so that a quick subsequent
+ * enable vote can cancel it instead of causing a
+ * disable/enable bounce on the rail
+ * @debounce_delay: Time in microseconds to wait for an enable vote
+ * after a disable vote, before actually sending
+ * the disable request to RPMh. 0 once torn down
+ * by rpmh_regulator_cancel_delayed_off_work().
+ * @off_on_delay: Like regulator_desc's off_on_delay, but tracked
+ * locally and measured from @last_off instead of
+ * from when disable() returns, since that return
+ * may precede the real RPMh disable by up to
+ * @debounce_delay.
+ * @last_off: Time the disable request was last actually sent
+ * to RPMh. Only used when debounce_delay is set.
+ * @lock: Serializes disable() against the teardown
+ * action, since both make a read-then-act decision
+ * on @debounce_delay that must be atomic to avoid
+ * queuing @delayed_off_work after teardown has
+ * swept it. enable() needs no lock: its only
+ * shared-state access is cancel_delayed_work_sync(),
+ * which already serializes against
+ * @delayed_off_work. @enabled and @rdev are also
+ * written by @delayed_off_work without this lock
+ * held, so they use READ_ONCE()/WRITE_ONCE() there.
*/
struct rpmh_vreg {
struct device *dev;
@@ -172,6 +203,12 @@ struct rpmh_vreg {
bool bypassed;
int voltage_selector;
unsigned int mode;
+ struct regulator_dev *rdev;
+ struct delayed_work delayed_off_work;
+ unsigned int debounce_delay;
+ unsigned int off_on_delay;
+ ktime_t last_off;
+ struct mutex lock;
};
/**
@@ -290,13 +327,17 @@ static int rpmh_regulator_is_enabled(struct regulator_dev *rdev)
{
struct rpmh_vreg *vreg = rdev_get_drvdata(rdev);
- return vreg->enabled;
+ /*
+ * Also written by delayed_off_work without vreg->lock held, so
+ * READ_ONCE()/WRITE_ONCE() are used instead of locking here and at
+ * the write site in _rpmh_regulator_set_enable_state().
+ */
+ return READ_ONCE(vreg->enabled);
}
-static int rpmh_regulator_set_enable_state(struct regulator_dev *rdev,
- bool enable)
+static int _rpmh_regulator_set_enable_state(struct rpmh_vreg *vreg,
+ struct regulator_dev *rdev, bool enable)
{
- struct rpmh_vreg *vreg = rdev_get_drvdata(rdev);
struct tcs_cmd cmd = {
.addr = vreg->addr + RPMH_REGULATOR_REG_ENABLE,
.data = enable,
@@ -313,19 +354,76 @@ static int rpmh_regulator_set_enable_state(struct regulator_dev *rdev,
ret = rpmh_regulator_send_request(vreg, &cmd, enable);
if (!ret)
- vreg->enabled = enable;
+ WRITE_ONCE(vreg->enabled, enable);
return ret;
}
+static void rpmh_delayed_off_work(struct work_struct *work)
+{
+ struct rpmh_vreg *vreg = container_of(work,
+ struct rpmh_vreg, delayed_off_work.work);
+
+ /*
+ * No locking needed. cancel_delayed_work_sync() already guarantees
+ * this work has either not started or has fully finished by the
+ * time enable()/teardown proceed to touch shared state.
+ */
+ if (!_rpmh_regulator_set_enable_state(vreg, READ_ONCE(vreg->rdev), false))
+ vreg->last_off = ktime_get_boottime();
+}
+
static int rpmh_regulator_enable(struct regulator_dev *rdev)
{
- return rpmh_regulator_set_enable_state(rdev, true);
+ struct rpmh_vreg *vreg = rdev_get_drvdata(rdev);
+ s64 remaining;
+
+ /* See struct rpmh_vreg's @lock for why no lock is needed here. */
+ if (cancel_delayed_work_sync(&vreg->delayed_off_work)) {
+ /* Queued disable never reached RPMh, so undo disable()'s flip. */
+ WRITE_ONCE(vreg->enabled, true);
+ return 0;
+ }
+
+ if (vreg->off_on_delay) {
+ remaining = ktime_us_delta(ktime_add_us(vreg->last_off,
+ vreg->off_on_delay),
+ ktime_get_boottime());
+ if (remaining > 0)
+ fsleep(remaining);
+ }
+
+ return _rpmh_regulator_set_enable_state(vreg, rdev, true);
}
static int rpmh_regulator_disable(struct regulator_dev *rdev)
{
- return rpmh_regulator_set_enable_state(rdev, false);
+ struct rpmh_vreg *vreg = rdev_get_drvdata(rdev);
+
+ guard(mutex)(&vreg->lock);
+
+ /*
+ * vreg->rdev is also read by delayed_off_work without this lock
+ * held, so READ_ONCE()/WRITE_ONCE() are used instead of locking
+ * there too.
+ */
+ WRITE_ONCE(vreg->rdev, rdev);
+
+ if (vreg->debounce_delay) {
+ /*
+ * Flip the logical state now so is_enabled() does not keep
+ * reporting "enabled" while the real RPMh disable is still
+ * queued. Without this, a quick re-enable would see the stale
+ * state and skip calling back into enable(), letting this
+ * queued disable run regardless.
+ */
+ WRITE_ONCE(vreg->enabled, false);
+ queue_delayed_work(system_percpu_wq, &vreg->delayed_off_work,
+ usecs_to_jiffies(vreg->debounce_delay));
+ return 0;
+ }
+
+ return _rpmh_regulator_set_enable_state(vreg, rdev, false);
}
static int rpmh_regulator_vrm_set_mode_bypass(struct rpmh_vreg *vreg,
@@ -481,6 +579,36 @@ static const struct regulator_ops rpmh_regulator_xob_ops = {
.is_enabled = rpmh_regulator_is_enabled,
};
+static void rpmh_regulator_cancel_delayed_off_work(void *data)
+{
+ struct rpmh_vreg *vreg = data;
+ bool pending;
+
+ /*
+ * Mark the feature torn down so a disable() racing with this
+ * teardown cannot queue delayed_off_work after the queue has
+ * already been cleared.
+ */
+ scoped_guard(mutex, &vreg->lock)
+ vreg->debounce_delay = 0;
+
+ /*
+ * Lock is not held across this cancel_delayed_work_sync(): a running
+ * instance may be waiting on an RPMh ACK, and the lock would needlessly
+ * block disable() for that duration.
+ */
+ pending = cancel_delayed_work_sync(&vreg->delayed_off_work);
+
+ /*
+ * If a disable() was queued, run it to ensure regulator hardware
+ * state is synced correctly with software state.
+ */
+ scoped_guard(mutex, &vreg->lock) {
+ if (pending)
+ _rpmh_regulator_set_enable_state(vreg, vreg->rdev, false);
+ }
+}
+
/**
* rpmh_regulator_init_vreg() - initialize all attributes of an rpmh-regulator
* @vreg: Pointer to the individual rpmh-regulator resource
@@ -552,6 +680,13 @@ static int rpmh_regulator_init_vreg(struct rpmh_vreg *vreg, struct device *dev,
vreg->always_wait_for_ack = of_property_read_bool(node,
"qcom,always-wait-for-ack");
+ of_property_read_u32(node, "qcom,regulator-off-debounce-delay-us",
+ &vreg->debounce_delay);
+
+ mutex_init(&vreg->lock);
+
+ INIT_DELAYED_WORK(&vreg->delayed_off_work, rpmh_delayed_off_work);
+
vreg->rdesc.owner = THIS_MODULE;
vreg->rdesc.type = REGULATOR_VOLTAGE;
vreg->rdesc.ops = vreg->hw_data->ops;
@@ -561,6 +696,16 @@ static int rpmh_regulator_init_vreg(struct rpmh_vreg *vreg, struct device *dev,
if (!init_data)
return -ENOMEM;
+ if (vreg->debounce_delay) {
+ /*
+ * Enforce off_on_delay locally instead of via the core: it
+ * must be measured from the deferred RPMh disable, not from
+ * when disable() returns.
+ */
+ vreg->off_on_delay = init_data->constraints.off_on_delay;
+ init_data->constraints.off_on_delay = 0;
+ }
+
if (rpmh_data->hw_data->regulator_type == XOB &&
init_data->constraints.min_uV &&
init_data->constraints.min_uV == init_data->constraints.max_uV) {
@@ -581,6 +726,13 @@ static int rpmh_regulator_init_vreg(struct rpmh_vreg *vreg, struct device *dev,
return ret;
}
+ if (vreg->debounce_delay) {
+ ret = devm_add_action_or_reset(dev, rpmh_regulator_cancel_delayed_off_work,
+ vreg);
+ if (ret)
+ return ret;
+ }
+
dev_dbg(dev, "%pOFn regulator registered for RPMh resource %s @ 0x%05X\n",
node, rpmh_resource_name, vreg->addr);
--
2.43.0