Files
linux/drivers/pmdomain/ti/ti_sci_pm_domains.c
Tomi Valkeinen 0b5fe1c4ab pmdomain: ti-sci: Set PD on/off state according to the HW state
At the moment the driver sets the power state of all the PDs it creates
to off, regardless of the actual HW state. This has two drawbacks:

1) The kernel cannot disable unused PDs automatically for power saving,
   as it thinks they are off already

2) A more specific case (but perhaps applicable to other scenarios
   also): bootloader enabled splash-screen cannot be kept on the screen.

The issue in 2) is that the driver framework automatically enables the
device's PD before calling probe() and disables it after the probe().
This means that when the display subsystem (DSS) driver probes, but e.g.
fails due to deferred probing, the DSS PD gets turned off and the driver
cannot do anything to affect that.

Solving the 2) requires more changes to actually keep the PD on during
the boot, but a prerequisite for it is to have the correct power state
for the PD.

The downside with this patch is that it takes time to call the 'is_on'
op, and we need to call it for each PD. In my tests with AM62 SK, using
defconfig, I see an increase from ~3.5ms to ~7ms. However, the added
feature is valuable, so in my opinion it's worth it.

The performance could probably be improved with a new firmware API which
returns the power states of all the PDs.

There's also a related HW issue at play here: if the DSS IP is enabled
and active, and its PD is turned off without first disabling the DSS
display outputs, the DSS IP will hang and causes the kernel to halt if
and when the DSS driver accesses the DSS registers the next time.

With the current upstream kernel, with this patch applied, this means
that if the bootloader enables the display, and the DSS driver is
compiled as a module, the kernel will at some point disable unused PDs,
including the DSS PD. When the DSS module is later loaded, it will hang
the kernel.

The same issue is already there, even without this patch, as the DSS
driver may hit deferred probing, which causes the PD to be turned off,
and leading to kernel halt when the DSS driver is probed again. This
issue has been made quite rare with some arrangements in the DSS
driver's probe, but it's still there.

With recent change from Ulf (e.g. commit 13a4b7fb62 ("pmdomain: core:
Leave powered-on genpds on until late_initcall_sync")), the sync state
mechanism comes to rescue. It will keep the power domains enabled, until
the drivers have probed, or the sync-state is triggered via some other
mechanism (e.g. manually by the boot scripts).

Reviewed-by: Kevin Hilman <khilman@baylibre.com>
Tested-by: Kevin Hilman <khilman@baylibre.com>
Signed-off-by: Tomi Valkeinen <tomi.valkeinen@ideasonboard.com>
Signed-off-by: Ulf Hansson <ulf.hansson@linaro.org>
2025-09-12 14:27:15 +02:00

327 lines
8.5 KiB
C

// SPDX-License-Identifier: GPL-2.0-only
/*
* TI SCI Generic Power Domain Driver
*
* Copyright (C) 2015-2017 Texas Instruments Incorporated - http://www.ti.com/
* J Keerthy <j-keerthy@ti.com>
* Dave Gerlach <d-gerlach@ti.com>
*/
#include <linux/err.h>
#include <linux/module.h>
#include <linux/of.h>
#include <linux/platform_device.h>
#include <linux/pm_domain.h>
#include <linux/pm_qos.h>
#include <linux/pm_runtime.h>
#include <linux/slab.h>
#include <linux/soc/ti/ti_sci_protocol.h>
#include <dt-bindings/soc/ti,sci_pm_domain.h>
/**
* struct ti_sci_genpd_provider: holds common TI SCI genpd provider data
* @ti_sci: handle to TI SCI protocol driver that provides ops to
* communicate with system control processor.
* @dev: pointer to dev for the driver for devm allocs
* @pd_list: list of all the power domains on the device
* @data: onecell data for genpd core
*/
struct ti_sci_genpd_provider {
const struct ti_sci_handle *ti_sci;
struct device *dev;
struct list_head pd_list;
struct genpd_onecell_data data;
};
/**
* struct ti_sci_pm_domain: TI specific data needed for power domain
* @idx: index of the device that identifies it with the system
* control processor.
* @exclusive: Permissions for exclusive request or shared request of the
* device.
* @pd: generic_pm_domain for use with the genpd framework
* @node: link for the genpd list
* @parent: link to the parent TI SCI genpd provider
*/
struct ti_sci_pm_domain {
int idx;
u8 exclusive;
struct generic_pm_domain pd;
struct list_head node;
struct ti_sci_genpd_provider *parent;
};
#define genpd_to_ti_sci_pd(gpd) container_of(gpd, struct ti_sci_pm_domain, pd)
static inline bool ti_sci_pd_is_valid_constraint(s32 val)
{
return val != PM_QOS_RESUME_LATENCY_NO_CONSTRAINT;
}
#ifdef CONFIG_PM_SLEEP
static void ti_sci_pd_set_lat_constraint(struct device *dev, s32 val)
{
struct generic_pm_domain *genpd = pd_to_genpd(dev->pm_domain);
struct ti_sci_pm_domain *pd = genpd_to_ti_sci_pd(genpd);
const struct ti_sci_handle *ti_sci = pd->parent->ti_sci;
u16 val_ms;
int ret;
/* PM QoS latency unit is usecs, TI SCI uses msecs */
val_ms = val / USEC_PER_MSEC;
ret = ti_sci->ops.pm_ops.set_latency_constraint(ti_sci, val_ms, TISCI_MSG_CONSTRAINT_SET);
if (ret)
dev_err(dev, "ti_sci_pd: set latency constraint failed: ret=%d\n",
ret);
else
dev_dbg(dev, "ti_sci_pd: ID:%d set latency constraint %d\n",
pd->idx, val);
}
#endif
static inline void ti_sci_pd_set_wkup_constraint(struct device *dev)
{
struct generic_pm_domain *genpd = pd_to_genpd(dev->pm_domain);
struct ti_sci_pm_domain *pd = genpd_to_ti_sci_pd(genpd);
const struct ti_sci_handle *ti_sci = pd->parent->ti_sci;
int ret;
if (device_may_wakeup(dev)) {
/*
* If device can wakeup using IO daisy chain wakeups,
* we do not want to set a constraint.
*/
if (dev->power.wakeirq) {
dev_dbg(dev, "%s: has wake IRQ, not setting constraints\n", __func__);
return;
}
ret = ti_sci->ops.pm_ops.set_device_constraint(ti_sci, pd->idx,
TISCI_MSG_CONSTRAINT_SET);
if (!ret)
dev_dbg(dev, "ti_sci_pd: ID:%d set device constraint.\n", pd->idx);
}
}
/*
* ti_sci_pd_power_off(): genpd power down hook
* @domain: pointer to the powerdomain to power off
*/
static int ti_sci_pd_power_off(struct generic_pm_domain *domain)
{
struct ti_sci_pm_domain *pd = genpd_to_ti_sci_pd(domain);
const struct ti_sci_handle *ti_sci = pd->parent->ti_sci;
return ti_sci->ops.dev_ops.put_device(ti_sci, pd->idx);
}
/*
* ti_sci_pd_power_on(): genpd power up hook
* @domain: pointer to the powerdomain to power on
*/
static int ti_sci_pd_power_on(struct generic_pm_domain *domain)
{
struct ti_sci_pm_domain *pd = genpd_to_ti_sci_pd(domain);
const struct ti_sci_handle *ti_sci = pd->parent->ti_sci;
if (pd->exclusive)
return ti_sci->ops.dev_ops.get_device_exclusive(ti_sci,
pd->idx);
else
return ti_sci->ops.dev_ops.get_device(ti_sci, pd->idx);
}
#ifdef CONFIG_PM_SLEEP
static int ti_sci_pd_suspend(struct device *dev)
{
int ret;
s32 val;
ret = pm_generic_suspend(dev);
if (ret)
return ret;
val = dev_pm_qos_read_value(dev, DEV_PM_QOS_RESUME_LATENCY);
if (ti_sci_pd_is_valid_constraint(val))
ti_sci_pd_set_lat_constraint(dev, val);
ti_sci_pd_set_wkup_constraint(dev);
return 0;
}
#else
#define ti_sci_pd_suspend NULL
#endif
/*
* ti_sci_pd_xlate(): translation service for TI SCI genpds
* @genpdspec: DT identification data for the genpd
* @data: genpd core data for all the powerdomains on the device
*/
static struct generic_pm_domain *ti_sci_pd_xlate(
const struct of_phandle_args *genpdspec,
void *data)
{
struct genpd_onecell_data *genpd_data = data;
unsigned int idx = genpdspec->args[0];
if (genpdspec->args_count != 1 && genpdspec->args_count != 2)
return ERR_PTR(-EINVAL);
if (idx >= genpd_data->num_domains) {
pr_err("%s: invalid domain index %u\n", __func__, idx);
return ERR_PTR(-EINVAL);
}
if (!genpd_data->domains[idx])
return ERR_PTR(-ENOENT);
genpd_to_ti_sci_pd(genpd_data->domains[idx])->exclusive =
genpdspec->args[1];
return genpd_data->domains[idx];
}
static const struct of_device_id ti_sci_pm_domain_matches[] = {
{ .compatible = "ti,sci-pm-domain", },
{ },
};
MODULE_DEVICE_TABLE(of, ti_sci_pm_domain_matches);
static bool ti_sci_pm_idx_exists(struct ti_sci_genpd_provider *pd_provider, u32 idx)
{
struct ti_sci_pm_domain *pd;
list_for_each_entry(pd, &pd_provider->pd_list, node) {
if (pd->idx == idx)
return true;
}
return false;
}
static bool ti_sci_pm_pd_is_on(struct ti_sci_genpd_provider *pd_provider,
int pd_idx)
{
bool is_on;
int ret;
if (!pd_provider->ti_sci->ops.dev_ops.is_on)
return false;
ret = pd_provider->ti_sci->ops.dev_ops.is_on(pd_provider->ti_sci,
pd_idx, NULL, &is_on);
if (ret)
return false;
return is_on;
}
static int ti_sci_pm_domain_probe(struct platform_device *pdev)
{
struct device *dev = &pdev->dev;
struct ti_sci_genpd_provider *pd_provider;
struct ti_sci_pm_domain *pd;
struct device_node *np __free(device_node) = NULL;
struct of_phandle_args args;
u32 max_id = 0;
int index;
pd_provider = devm_kzalloc(dev, sizeof(*pd_provider), GFP_KERNEL);
if (!pd_provider)
return -ENOMEM;
pd_provider->ti_sci = devm_ti_sci_get_handle(dev);
if (IS_ERR(pd_provider->ti_sci))
return PTR_ERR(pd_provider->ti_sci);
pd_provider->dev = dev;
INIT_LIST_HEAD(&pd_provider->pd_list);
/* Find highest device ID used for power domains */
for_each_node_with_property(np, "power-domains") {
index = 0;
while (!of_parse_phandle_with_args(np, "power-domains",
"#power-domain-cells",
index, &args)) {
if (args.args_count >= 1 && args.np == dev->of_node) {
bool is_on;
of_node_put(args.np);
if (args.args[0] > max_id) {
max_id = args.args[0];
} else {
if (ti_sci_pm_idx_exists(pd_provider, args.args[0])) {
index++;
continue;
}
}
pd = devm_kzalloc(dev, sizeof(*pd), GFP_KERNEL);
if (!pd)
return -ENOMEM;
pd->pd.name = devm_kasprintf(dev, GFP_KERNEL,
"pd:%d",
args.args[0]);
if (!pd->pd.name)
return -ENOMEM;
pd->pd.power_off = ti_sci_pd_power_off;
pd->pd.power_on = ti_sci_pd_power_on;
pd->pd.flags |= GENPD_FLAG_ACTIVE_WAKEUP;
pd->idx = args.args[0];
pd->parent = pd_provider;
/*
* If SCI constraint functions are present, then firmware
* supports the constraints API.
*/
if (pd_provider->ti_sci->ops.pm_ops.set_device_constraint &&
pd_provider->ti_sci->ops.pm_ops.set_latency_constraint)
pd->pd.domain.ops.suspend = ti_sci_pd_suspend;
is_on = ti_sci_pm_pd_is_on(pd_provider,
pd->idx);
pm_genpd_init(&pd->pd, NULL, !is_on);
list_add(&pd->node, &pd_provider->pd_list);
} else {
of_node_put(args.np);
}
index++;
}
}
pd_provider->data.domains =
devm_kcalloc(dev, max_id + 1,
sizeof(*pd_provider->data.domains),
GFP_KERNEL);
if (!pd_provider->data.domains)
return -ENOMEM;
pd_provider->data.num_domains = max_id + 1;
pd_provider->data.xlate = ti_sci_pd_xlate;
list_for_each_entry(pd, &pd_provider->pd_list, node)
pd_provider->data.domains[pd->idx] = &pd->pd;
return of_genpd_add_provider_onecell(dev->of_node, &pd_provider->data);
}
static struct platform_driver ti_sci_pm_domains_driver = {
.probe = ti_sci_pm_domain_probe,
.driver = {
.name = "ti_sci_pm_domains",
.of_match_table = ti_sci_pm_domain_matches,
},
};
module_platform_driver(ti_sci_pm_domains_driver);
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("TI System Control Interface (SCI) Power Domain driver");
MODULE_AUTHOR("Dave Gerlach");