mirror of
https://source.denx.de/u-boot/u-boot.git
synced 2025-08-15 19:56:58 +02:00
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 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. Some drivers implement their own mechanism for that, but it is probably best to add this feature in the uclass and share the common code across drivers. In order to avoid breaking existing drivers, refcounting is only enabled if the number of subdomains a device node supports is explicitly set in the probe function. ->xlate() callbacks will return the power domain ID which is then being used as the array index to reach the correct refcounter. As we do not want to break existing users while stile getting interesting error codes, the implementation is split between: - a low-level helper reporting error codes if the requested transition could not be operated, - a higher-level helper ignoring the "non error" codes, like EALREADY and EBUSY. CI tests using power domains are slightly updated to make sure the count of on/off calls is even and the results match what we *now* expect. They are also extended to test the low-level functions. Signed-off-by: Miquel Raynal <miquel.raynal@bootlin.com>
262 lines
6.4 KiB
C
262 lines
6.4 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* Copyright (c) 2016, NVIDIA CORPORATION.
|
|
*/
|
|
|
|
#define LOG_CATEGORY UCLASS_POWER_DOMAIN
|
|
|
|
#include <dm.h>
|
|
#include <log.h>
|
|
#include <malloc.h>
|
|
#include <power-domain.h>
|
|
#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;
|
|
}
|
|
|
|
static int power_domain_of_xlate_default(struct power_domain *power_domain,
|
|
struct ofnode_phandle_args *args)
|
|
{
|
|
debug("%s(power_domain=%p)\n", __func__, power_domain);
|
|
|
|
if (args->args_count != 1) {
|
|
debug("Invalid args_count: %d\n", args->args_count);
|
|
return -EINVAL;
|
|
}
|
|
|
|
power_domain->id = args->args[0];
|
|
|
|
return 0;
|
|
}
|
|
|
|
int power_domain_get_by_index(struct udevice *dev,
|
|
struct power_domain *power_domain, int index)
|
|
{
|
|
struct ofnode_phandle_args args;
|
|
int ret;
|
|
struct udevice *dev_power_domain;
|
|
struct power_domain_ops *ops;
|
|
|
|
debug("%s(dev=%p, power_domain=%p)\n", __func__, dev, power_domain);
|
|
|
|
ret = dev_read_phandle_with_args(dev, "power-domains",
|
|
"#power-domain-cells", 0, index,
|
|
&args);
|
|
if (ret) {
|
|
debug("%s: dev_read_phandle_with_args failed: %d\n",
|
|
__func__, ret);
|
|
return ret;
|
|
}
|
|
|
|
ret = uclass_get_device_by_ofnode(UCLASS_POWER_DOMAIN, args.node,
|
|
&dev_power_domain);
|
|
if (ret) {
|
|
debug("%s: uclass_get_device_by_ofnode failed: %d\n",
|
|
__func__, ret);
|
|
return ret;
|
|
}
|
|
ops = power_domain_dev_ops(dev_power_domain);
|
|
|
|
power_domain->dev = dev_power_domain;
|
|
if (ops->of_xlate)
|
|
ret = ops->of_xlate(power_domain, &args);
|
|
else
|
|
ret = power_domain_of_xlate_default(power_domain, &args);
|
|
if (ret) {
|
|
debug("of_xlate() failed: %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
ret = ops->request ? ops->request(power_domain) : 0;
|
|
if (ret) {
|
|
debug("ops->request() failed: %d\n", ret);
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int power_domain_get_by_name(struct udevice *dev,
|
|
struct power_domain *power_domain, const char *name)
|
|
{
|
|
int index;
|
|
|
|
index = dev_read_stringlist_search(dev, "power-domain-names", name);
|
|
if (index < 0) {
|
|
debug("fdt_stringlist_search() failed: %d\n", index);
|
|
return index;
|
|
}
|
|
|
|
return power_domain_get_by_index(dev, power_domain, index);
|
|
}
|
|
|
|
int power_domain_get(struct udevice *dev, struct power_domain *power_domain)
|
|
{
|
|
return power_domain_get_by_index(dev, power_domain, 0);
|
|
}
|
|
|
|
int power_domain_free(struct power_domain *power_domain)
|
|
{
|
|
struct power_domain_ops *ops = power_domain_dev_ops(power_domain->dev);
|
|
|
|
debug("%s(power_domain=%p)\n", __func__, power_domain);
|
|
|
|
return ops->rfree ? ops->rfree(power_domain) : 0;
|
|
}
|
|
|
|
int power_domain_on_lowlevel(struct power_domain *power_domain)
|
|
{
|
|
struct power_domain_priv *priv = dev_get_uclass_priv(power_domain->dev);
|
|
struct power_domain_plat *plat = dev_get_uclass_plat(power_domain->dev);
|
|
struct power_domain_ops *ops = power_domain_dev_ops(power_domain->dev);
|
|
int *on_count = plat->subdomains ? &priv->on_count[power_domain->id] : NULL;
|
|
int ret;
|
|
|
|
/* Refcounting is not enabled on all drivers by default */
|
|
if (on_count) {
|
|
debug("Enable power domain %s.%ld: %d -> %d (%s)\n",
|
|
power_domain->dev->name, power_domain->id, *on_count, *on_count + 1,
|
|
(((*on_count + 1) > 1) ? "EALREADY" : "todo"));
|
|
|
|
(*on_count)++;
|
|
if (*on_count > 1)
|
|
return -EALREADY;
|
|
}
|
|
|
|
ret = ops->on ? ops->on(power_domain) : 0;
|
|
if (ret) {
|
|
if (on_count)
|
|
(*on_count)--;
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
int power_domain_off_lowlevel(struct power_domain *power_domain)
|
|
{
|
|
struct power_domain_priv *priv = dev_get_uclass_priv(power_domain->dev);
|
|
struct power_domain_plat *plat = dev_get_uclass_plat(power_domain->dev);
|
|
struct power_domain_ops *ops = power_domain_dev_ops(power_domain->dev);
|
|
int *on_count = plat->subdomains ? &priv->on_count[power_domain->id] : NULL;
|
|
int ret;
|
|
|
|
/* Refcounting is not enabled on all drivers by default */
|
|
if (on_count) {
|
|
debug("Disable power domain %s.%ld: %d -> %d (%s%s)\n",
|
|
power_domain->dev->name, power_domain->id, *on_count, *on_count - 1,
|
|
(((*on_count) <= 0) ? "EALREADY" : ""),
|
|
(((*on_count - 1) > 0) ? "BUSY" : "todo"));
|
|
|
|
if (*on_count <= 0)
|
|
return -EALREADY;
|
|
|
|
(*on_count)--;
|
|
if (*on_count > 0)
|
|
return -EBUSY;
|
|
}
|
|
|
|
ret = ops->off ? ops->off(power_domain) : 0;
|
|
if (ret) {
|
|
if (on_count)
|
|
(*on_count)++;
|
|
|
|
return ret;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
#if CONFIG_IS_ENABLED(OF_REAL)
|
|
static int dev_power_domain_ctrl(struct udevice *dev, bool on)
|
|
{
|
|
struct power_domain pd;
|
|
int i, count, ret = 0;
|
|
|
|
count = dev_count_phandle_with_args(dev, "power-domains",
|
|
"#power-domain-cells", 0);
|
|
for (i = 0; i < count; i++) {
|
|
ret = power_domain_get_by_index(dev, &pd, i);
|
|
if (ret)
|
|
return ret;
|
|
if (on)
|
|
ret = power_domain_on(&pd);
|
|
else
|
|
ret = power_domain_off(&pd);
|
|
}
|
|
|
|
/*
|
|
* For platforms with parent and child power-domain devices
|
|
* we may not run device_remove() on the power-domain parent
|
|
* because it will result in removing its children and switching
|
|
* off their power-domain parent. So we will get here again and
|
|
* again and will be stuck in an endless loop.
|
|
*/
|
|
if (count > 0 && !on && dev_get_parent(dev) == pd.dev &&
|
|
device_get_uclass_id(dev) == UCLASS_POWER_DOMAIN)
|
|
return ret;
|
|
|
|
/*
|
|
* power_domain_get() bound the device, thus
|
|
* we must remove it again to prevent unbinding
|
|
* active devices (which would result in unbind
|
|
* error).
|
|
*/
|
|
if (count > 0 && !on)
|
|
device_remove(pd.dev, DM_REMOVE_NORMAL);
|
|
|
|
return ret;
|
|
}
|
|
|
|
int dev_power_domain_on(struct udevice *dev)
|
|
{
|
|
return dev_power_domain_ctrl(dev, true);
|
|
}
|
|
|
|
int dev_power_domain_off(struct udevice *dev)
|
|
{
|
|
return dev_power_domain_ctrl(dev, false);
|
|
}
|
|
#endif /* OF_REAL */
|
|
|
|
static int power_domain_post_probe(struct udevice *dev)
|
|
{
|
|
struct power_domain_priv *priv = dev_get_uclass_priv(dev);
|
|
struct power_domain_plat *plat = dev_get_uclass_plat(dev);
|
|
|
|
if (plat->subdomains) {
|
|
priv->on_count = calloc(sizeof(int), plat->subdomains);
|
|
if (!priv->on_count)
|
|
return -ENOMEM;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int power_domain_pre_remove(struct udevice *dev)
|
|
{
|
|
struct power_domain_priv *priv = dev_get_uclass_priv(dev);
|
|
struct power_domain_plat *plat = dev_get_uclass_plat(dev);
|
|
|
|
if (plat->subdomains)
|
|
free(priv->on_count);
|
|
|
|
return 0;
|
|
}
|
|
|
|
UCLASS_DRIVER(power_domain) = {
|
|
.id = UCLASS_POWER_DOMAIN,
|
|
.name = "power_domain",
|
|
.post_probe = power_domain_post_probe,
|
|
.pre_remove = power_domain_pre_remove,
|
|
.per_device_auto = sizeof(struct power_domain_priv),
|
|
.per_device_plat_auto = sizeof(struct power_domain_plat),
|
|
};
|