
It is very surprising that such an uclass, specifically designed to handle resources that may be shared by different devices, is not keeping the count of the number of times a power domain has been enabled/disabled to avoid shutting it down unexpectedly or disabling it several times.
Doing this causes troubles on eg. i.MX8MP because disabling power domains can be done in a recursive loops were the same power domain disabled up to 4 times in a row. PGCs seem to have tight FSM internal timings to respect and it is easy to produce a race condition that puts the power domains in an unstable state, leading to ADB400 errors and later crashes in Linux.
Signed-off-by: Miquel Raynal miquel.raynal@bootlin.com --- drivers/power/domain/power-domain-uclass.c | 37 ++++++++++++++++++++++++++++-- include/power-domain.h | 4 ++-- 2 files changed, 37 insertions(+), 4 deletions(-)
diff --git a/drivers/power/domain/power-domain-uclass.c b/drivers/power/domain/power-domain-uclass.c index 938bd8cbc9ffd1ba2109d702f886b6a99288d063..55a3d95d83c5aaac31acbd877199c87f8f6fb880 100644 --- a/drivers/power/domain/power-domain-uclass.c +++ b/drivers/power/domain/power-domain-uclass.c @@ -12,6 +12,10 @@ #include <power-domain-uclass.h> #include <dm/device-internal.h>
+struct power_domain_priv { + int on_count; +}; + static inline struct power_domain_ops *power_domain_dev_ops(struct udevice *dev) { return (struct power_domain_ops *)dev->driver->ops; @@ -110,19 +114,47 @@ int power_domain_free(struct power_domain *power_domain) int power_domain_on(struct power_domain *power_domain) { struct power_domain_ops *ops = power_domain_dev_ops(power_domain->dev); + struct power_domain_priv *priv = dev_get_uclass_priv(power_domain->dev); + int ret;
debug("%s(power_domain=%p)\n", __func__, power_domain);
- return ops->on ? ops->on(power_domain) : 0; + if (priv->on_count++ > 0) + return 0; + + ret = ops->on ? ops->on(power_domain) : 0; + if (ret) { + priv->on_count--; + return ret; + } + + return 0; }
int power_domain_off(struct power_domain *power_domain) { struct power_domain_ops *ops = power_domain_dev_ops(power_domain->dev); + struct power_domain_priv *priv = dev_get_uclass_priv(power_domain->dev); + int ret;
debug("%s(power_domain=%p)\n", __func__, power_domain);
- return ops->off ? ops->off(power_domain) : 0; + if (priv->on_count <= 0) { + debug("Power domain %s already off.\n", power_domain->dev->name); + priv->on_count--; + return 0; + } + + if (priv->on_count-- > 1) + return 0; + + ret = ops->off ? ops->off(power_domain) : 0; + if (ret) { + priv->on_count++; + return ret; + } + + return 0; }
#if CONFIG_IS_ENABLED(OF_REAL) @@ -180,4 +212,5 @@ int dev_power_domain_off(struct udevice *dev) UCLASS_DRIVER(power_domain) = { .id = UCLASS_POWER_DOMAIN, .name = "power_domain", + .per_device_auto = sizeof(struct power_domain_priv), }; diff --git a/include/power-domain.h b/include/power-domain.h index 18525073e5e3534fcbac6fae4e18462f29a4dc49..0266fc2f864ef3645d2a8b3c6fa66fdbd868799c 100644 --- a/include/power-domain.h +++ b/include/power-domain.h @@ -147,7 +147,7 @@ static inline int power_domain_free(struct power_domain *power_domain) #endif
/** - * power_domain_on - Enable power to a power domain. + * power_domain_on - Enable power to a power domain (with refcounting) * * @power_domain: A power domain struct that was previously successfully * requested by power_domain_get(). @@ -163,7 +163,7 @@ static inline int power_domain_on(struct power_domain *power_domain) #endif
/** - * power_domain_off - Disable power to a power domain. + * power_domain_off - Disable power to a power domain (with refcounting) * * @power_domain: A power domain struct that was previously successfully * requested by power_domain_get().