[PATCH 2/7] irqchip/irq-qcom-mpm: Register MPM under CPU cluster power domain

From: Sneh Mankad

Date: Mon Jul 13 2026 - 06:27:02 EST


MPM irqchip needs to notify RPM (Resource Power Manager) processor to read
the latest wake up capable interrupts when the CPU cluster is entering the
deepest idle state. This is done by sending IPC interrupt to RPM and is
implemented as .power_off() callback by registering MPM as parent power
domain to CPU cluster.

Such implementation introduces a hard probe dependency between MPM irqchip
and CPU cluster power domains. That is MPM irqchip needs to finish probe
before PSCI power domains are probed. MPM irqchip can be build as module
and can get later inserted where as PSCI power domains is not a module.

For in-built driver cases too PSCI domain gets probed first and later MPM
irqchip leading to failure of CPUidle states.

Detailed flow of the non-working scenario:

psci-cpuidle-domain.c probe
--> dt_idle_pd_init_topology()
--> of_genpd_add_subdomain()
--> genpd_get_from_provider()
--> fails to find parent MPM genPD provider
--> returns -EPROBE_DEFER.

irq-qcom-mpm.c probe
--> of_genpd_add_provider_simple()
--> genpd_add_provider()
--> MPM added as a genPD provider.

Now when psci_cpuidle_probe() is called to probe the CPU idle states, it
tries to map the states to the mentioned power-domains.

But since power domains probe has been deferred, psci_cpuidle_probe() too
will return -EPROBE_DEFER.

commit af5376a77e87 ("cpuidle: psci: Transition to the faux device
interface") transitioned cpuidle-psci to a faux device interface.

faux_device_create() calls faux_device_create_with_groups(), which ignores
the probe return value, and destroys the device if dev->driver is not set.

This will lead to psci_cpuidle_probe() not being called again, resulting in
all idle-state devices failing to init in SoCs setting MPM as a parent
power domain to CPU cluster.

cpuidle-psci.c init
--> faux_device_create()
...
--> psci_cpuidle_probe()
--> psci_idle_init_cpu()
...
--> psci_dt_cpu_init_topology()
...
-> dev_pm_domain_attach_by_name()
--> __genpd_dev_pm_attach()
--> genpd_get_from_provider()
--> fails to find CPU genPD provider
--> returns -EPROBE_DEFER
--> return value ignored and device
destroyed

Move the RPM notification handling to the GENPD_NOTIFY_PRE_OFF callback and
register MPM under the CPU cluster power domain. Use runtime PM to report
the default RPM_SUSPENDED state to genPD so that the CPU cluster power
domain can enter low power mode.

If MPM has not registered with CPU cluster power domain, utilize the CPU PM
notifications to manage RPM communication when the last CPU goes to power
collapse.

Fixes: a6199bb514d8 ("irqchip: Add Qualcomm MPM controller driver")
Signed-off-by: Sneh Mankad <sneh.mankad@xxxxxxxxxxxxxxxx>
---
drivers/irqchip/irq-qcom-mpm.c | 97 ++++++++++++++++++++++++++++++------------
1 file changed, 69 insertions(+), 28 deletions(-)

diff --git a/drivers/irqchip/irq-qcom-mpm.c b/drivers/irqchip/irq-qcom-mpm.c
index 181320528a47ac1bde6cf7d2d0a9f79499990092..01fd1843172aa22760d359281e39f0d9394bab6d 100644
--- a/drivers/irqchip/irq-qcom-mpm.c
+++ b/drivers/irqchip/irq-qcom-mpm.c
@@ -4,6 +4,8 @@
* Copyright (c) 2010-2020, The Linux Foundation. All rights reserved.
*/

+#include <linux/atomic.h>
+#include <linux/cpu_pm.h>
#include <linux/delay.h>
#include <linux/err.h>
#include <linux/init.h>
@@ -18,6 +20,7 @@
#include <linux/of_platform.h>
#include <linux/platform_device.h>
#include <linux/pm_domain.h>
+#include <linux/pm_runtime.h>
#include <linux/slab.h>
#include <linux/soc/qcom/irq.h>
#include <linux/spinlock.h>
@@ -84,7 +87,9 @@ struct qcom_mpm_priv {
unsigned int map_cnt;
unsigned int reg_stride;
struct irq_domain *domain;
- struct generic_pm_domain genpd;
+ struct notifier_block genpd_nb;
+ struct notifier_block mpm_pm;
+ atomic_t cpus_in_pm;
};

static u32 qcom_mpm_read(struct qcom_mpm_priv *priv, unsigned int reg,
@@ -292,10 +297,8 @@ static irqreturn_t qcom_mpm_handler(int irq, void *dev_id)
return ret;
}

-static int mpm_pd_power_off(struct generic_pm_domain *genpd)
+static int handle_rpm_notification(struct qcom_mpm_priv *priv)
{
- struct qcom_mpm_priv *priv = container_of(genpd, struct qcom_mpm_priv,
- genpd);
int i, ret;

for (i = 0; i < priv->reg_stride; i++)
@@ -307,10 +310,59 @@ static int mpm_pd_power_off(struct generic_pm_domain *genpd)
return ret;

mbox_client_txdone(priv->mbox_chan, 0);
-
return 0;
}

+static int mpm_pd_power_cb(struct notifier_block *nb, unsigned long action, void *d)
+{
+ struct qcom_mpm_priv *priv = container_of(nb, struct qcom_mpm_priv,
+ genpd_nb);
+
+ switch (action) {
+ case GENPD_NOTIFY_PRE_OFF:
+ if (handle_rpm_notification(priv))
+ return NOTIFY_BAD;
+ }
+
+ return NOTIFY_OK;
+}
+
+static int mpm_cpu_pm_callback(struct notifier_block *nfb,
+ unsigned long action, void *v)
+{
+ struct qcom_mpm_priv *priv = container_of(nfb, struct qcom_mpm_priv, mpm_pm);
+ int cpus_in_pm;
+
+ switch (action) {
+ case CPU_PM_ENTER:
+ cpus_in_pm = atomic_inc_return(&priv->cpus_in_pm);
+ /*
+ * NOTE: comments for num_online_cpus() point out that it's
+ * only a snapshot so we need to be careful. It should be OK
+ * for us to use, though. It's important for us not to miss
+ * if we're the last CPU going down so it would only be a
+ * problem if a CPU went offline right after we did the check
+ * AND that CPU was not idle AND that CPU was the last non-idle
+ * CPU. That can't happen. CPUs would have to come out of idle
+ * before the CPU could go offline.
+ */
+ if (cpus_in_pm < num_online_cpus())
+ return NOTIFY_OK;
+ break;
+ case CPU_PM_ENTER_FAILED:
+ case CPU_PM_EXIT:
+ atomic_dec(&priv->cpus_in_pm);
+ return NOTIFY_OK;
+ default:
+ return NOTIFY_DONE;
+ }
+
+ if (handle_rpm_notification(priv))
+ return NOTIFY_BAD;
+
+ return NOTIFY_OK;
+}
+
static bool gic_hwirq_is_mapped(struct mpm_gic_map *maps, int cnt, u32 hwirq)
{
int i;
@@ -327,7 +379,6 @@ static int qcom_mpm_probe(struct platform_device *pdev, struct device_node *pare
struct device_node *np = pdev->dev.of_node;
struct device *dev = &pdev->dev;
struct irq_domain *parent_domain;
- struct generic_pm_domain *genpd;
struct device_node *msgram_np;
struct qcom_mpm_priv *priv;
unsigned int pin_cnt;
@@ -415,26 +466,6 @@ static int qcom_mpm_probe(struct platform_device *pdev, struct device_node *pare
if (irq < 0)
return irq;

- genpd = &priv->genpd;
- genpd->flags = GENPD_FLAG_IRQ_SAFE;
- genpd->power_off = mpm_pd_power_off;
-
- genpd->name = devm_kasprintf(dev, GFP_KERNEL, "%s", dev_name(dev));
- if (!genpd->name)
- return -ENOMEM;
-
- ret = pm_genpd_init(genpd, NULL, false);
- if (ret) {
- dev_err(dev, "failed to init genpd: %d\n", ret);
- return ret;
- }
-
- ret = of_genpd_add_provider_simple(np, genpd);
- if (ret) {
- dev_err(dev, "failed to add genpd provider: %d\n", ret);
- goto remove_genpd;
- }
-
priv->mbox_client.dev = dev;
priv->mbox_client.knows_txdone = true;
priv->mbox_chan = mbox_request_channel(&priv->mbox_client, 0);
@@ -469,14 +500,24 @@ static int qcom_mpm_probe(struct platform_device *pdev, struct device_node *pare
goto remove_domain;
}

+ if (of_find_property(np, "power-domains", NULL)) {
+ devm_pm_runtime_enable(dev);
+ priv->genpd_nb.notifier_call = mpm_pd_power_cb;
+ ret = dev_pm_genpd_add_notifier(dev, &priv->genpd_nb);
+ } else {
+ priv->mpm_pm.notifier_call = mpm_cpu_pm_callback;
+ ret = cpu_pm_register_notifier(&priv->mpm_pm);
+ }
+
+ if (ret)
+ goto remove_domain;
+
return 0;

remove_domain:
irq_domain_remove(priv->domain);
free_mbox:
mbox_free_channel(priv->mbox_chan);
-remove_genpd:
- pm_genpd_remove(genpd);
return ret;
}


--
2.34.1