| // SPDX-License-Identifier: GPL-2.0 |
| /* |
| * PM domains for CPUs via genpd - managed by cpuidle-psci. |
| * |
| * Copyright (C) 2019 Linaro Ltd. |
| * Author: Ulf Hansson <ulf.hansson@linaro.org> |
| * |
| */ |
| |
| #define pr_fmt(fmt) "CPUidle PSCI: " fmt |
| |
| #include <linux/cpu.h> |
| #include <linux/device.h> |
| #include <linux/kernel.h> |
| #include <linux/platform_device.h> |
| #include <linux/pm_domain.h> |
| #include <linux/pm_runtime.h> |
| #include <linux/psci.h> |
| #include <linux/slab.h> |
| #include <linux/string.h> |
| |
| #include "cpuidle-psci.h" |
| |
| struct psci_pd_provider { |
| struct list_head link; |
| struct device_node *node; |
| }; |
| |
| static LIST_HEAD(psci_pd_providers); |
| static bool psci_pd_allow_domain_state; |
| |
| static int psci_pd_power_off(struct generic_pm_domain *pd) |
| { |
| struct genpd_power_state *state = &pd->states[pd->state_idx]; |
| u32 *pd_state; |
| |
| if (!state->data) |
| return 0; |
| |
| if (!psci_pd_allow_domain_state) |
| return -EBUSY; |
| |
| /* OSI mode is enabled, set the corresponding domain state. */ |
| pd_state = state->data; |
| psci_set_domain_state(*pd_state); |
| |
| return 0; |
| } |
| |
| static int psci_pd_parse_state_nodes(struct genpd_power_state *states, |
| int state_count) |
| { |
| int i, ret; |
| u32 psci_state, *psci_state_buf; |
| |
| for (i = 0; i < state_count; i++) { |
| ret = psci_dt_parse_state_node(to_of_node(states[i].fwnode), |
| &psci_state); |
| if (ret) |
| goto free_state; |
| |
| psci_state_buf = kmalloc(sizeof(u32), GFP_KERNEL); |
| if (!psci_state_buf) { |
| ret = -ENOMEM; |
| goto free_state; |
| } |
| *psci_state_buf = psci_state; |
| states[i].data = psci_state_buf; |
| } |
| |
| return 0; |
| |
| free_state: |
| i--; |
| for (; i >= 0; i--) |
| kfree(states[i].data); |
| return ret; |
| } |
| |
| static int psci_pd_parse_states(struct device_node *np, |
| struct genpd_power_state **states, int *state_count) |
| { |
| int ret; |
| |
| /* Parse the domain idle states. */ |
| ret = of_genpd_parse_idle_states(np, states, state_count); |
| if (ret) |
| return ret; |
| |
| /* Fill out the PSCI specifics for each found state. */ |
| ret = psci_pd_parse_state_nodes(*states, *state_count); |
| if (ret) |
| kfree(*states); |
| |
| return ret; |
| } |
| |
| static void psci_pd_free_states(struct genpd_power_state *states, |
| unsigned int state_count) |
| { |
| int i; |
| |
| for (i = 0; i < state_count; i++) |
| kfree(states[i].data); |
| kfree(states); |
| } |
| |
| static int psci_pd_init(struct device_node *np) |
| { |
| struct generic_pm_domain *pd; |
| struct psci_pd_provider *pd_provider; |
| struct dev_power_governor *pd_gov; |
| struct genpd_power_state *states = NULL; |
| int ret = -ENOMEM, state_count = 0; |
| |
| pd = kzalloc(sizeof(*pd), GFP_KERNEL); |
| if (!pd) |
| goto out; |
| |
| pd_provider = kzalloc(sizeof(*pd_provider), GFP_KERNEL); |
| if (!pd_provider) |
| goto free_pd; |
| |
| pd->name = kasprintf(GFP_KERNEL, "%pOF", np); |
| if (!pd->name) |
| goto free_pd_prov; |
| |
| /* |
| * Parse the domain idle states and let genpd manage the state selection |
| * for those being compatible with "domain-idle-state". |
| */ |
| ret = psci_pd_parse_states(np, &states, &state_count); |
| if (ret) |
| goto free_name; |
| |
| pd->free_states = psci_pd_free_states; |
| pd->name = kbasename(pd->name); |
| pd->power_off = psci_pd_power_off; |
| pd->states = states; |
| pd->state_count = state_count; |
| pd->flags |= GENPD_FLAG_IRQ_SAFE | GENPD_FLAG_CPU_DOMAIN; |
| |
| /* Use governor for CPU PM domains if it has some states to manage. */ |
| pd_gov = state_count > 0 ? &pm_domain_cpu_gov : NULL; |
| |
| ret = pm_genpd_init(pd, pd_gov, false); |
| if (ret) { |
| psci_pd_free_states(states, state_count); |
| goto free_name; |
| } |
| |
| ret = of_genpd_add_provider_simple(np, pd); |
| if (ret) |
| goto remove_pd; |
| |
| pd_provider->node = of_node_get(np); |
| list_add(&pd_provider->link, &psci_pd_providers); |
| |
| pr_debug("init PM domain %s\n", pd->name); |
| return 0; |
| |
| remove_pd: |
| pm_genpd_remove(pd); |
| free_name: |
| kfree(pd->name); |
| free_pd_prov: |
| kfree(pd_provider); |
| free_pd: |
| kfree(pd); |
| out: |
| pr_err("failed to init PM domain ret=%d %pOF\n", ret, np); |
| return ret; |
| } |
| |
| static void psci_pd_remove(void) |
| { |
| struct psci_pd_provider *pd_provider, *it; |
| struct generic_pm_domain *genpd; |
| |
| list_for_each_entry_safe(pd_provider, it, &psci_pd_providers, link) { |
| of_genpd_del_provider(pd_provider->node); |
| |
| genpd = of_genpd_remove_last(pd_provider->node); |
| if (!IS_ERR(genpd)) |
| kfree(genpd); |
| |
| of_node_put(pd_provider->node); |
| list_del(&pd_provider->link); |
| kfree(pd_provider); |
| } |
| } |
| |
| static int psci_pd_init_topology(struct device_node *np, bool add) |
| { |
| struct device_node *node; |
| struct of_phandle_args child, parent; |
| int ret; |
| |
| for_each_child_of_node(np, node) { |
| if (of_parse_phandle_with_args(node, "power-domains", |
| "#power-domain-cells", 0, &parent)) |
| continue; |
| |
| child.np = node; |
| child.args_count = 0; |
| |
| ret = add ? of_genpd_add_subdomain(&parent, &child) : |
| of_genpd_remove_subdomain(&parent, &child); |
| of_node_put(parent.np); |
| if (ret) { |
| of_node_put(node); |
| return ret; |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int psci_pd_add_topology(struct device_node *np) |
| { |
| return psci_pd_init_topology(np, true); |
| } |
| |
| static void psci_pd_remove_topology(struct device_node *np) |
| { |
| psci_pd_init_topology(np, false); |
| } |
| |
| static void psci_cpuidle_domain_sync_state(struct device *dev) |
| { |
| /* |
| * All devices have now been attached/probed to the PM domain topology, |
| * hence it's fine to allow domain states to be picked. |
| */ |
| psci_pd_allow_domain_state = true; |
| } |
| |
| static const struct of_device_id psci_of_match[] = { |
| { .compatible = "arm,psci-1.0" }, |
| {} |
| }; |
| |
| static int psci_cpuidle_domain_probe(struct platform_device *pdev) |
| { |
| struct device_node *np = pdev->dev.of_node; |
| struct device_node *node; |
| int ret = 0, pd_count = 0; |
| |
| if (!np) |
| return -ENODEV; |
| |
| /* Currently limit the hierarchical topology to be used in OSI mode. */ |
| if (!psci_has_osi_support()) |
| return 0; |
| |
| /* |
| * Parse child nodes for the "#power-domain-cells" property and |
| * initialize a genpd/genpd-of-provider pair when it's found. |
| */ |
| for_each_child_of_node(np, node) { |
| if (!of_find_property(node, "#power-domain-cells", NULL)) |
| continue; |
| |
| ret = psci_pd_init(node); |
| if (ret) |
| goto put_node; |
| |
| pd_count++; |
| } |
| |
| /* Bail out if not using the hierarchical CPU topology. */ |
| if (!pd_count) |
| return 0; |
| |
| /* Link genpd masters/subdomains to model the CPU topology. */ |
| ret = psci_pd_add_topology(np); |
| if (ret) |
| goto remove_pd; |
| |
| /* Try to enable OSI mode. */ |
| ret = psci_set_osi_mode(); |
| if (ret) { |
| pr_warn("failed to enable OSI mode: %d\n", ret); |
| psci_pd_remove_topology(np); |
| goto remove_pd; |
| } |
| |
| pr_info("Initialized CPU PM domain topology\n"); |
| return 0; |
| |
| put_node: |
| of_node_put(node); |
| remove_pd: |
| if (pd_count) |
| psci_pd_remove(); |
| pr_err("failed to create CPU PM domains ret=%d\n", ret); |
| return ret; |
| } |
| |
| static struct platform_driver psci_cpuidle_domain_driver = { |
| .probe = psci_cpuidle_domain_probe, |
| .driver = { |
| .name = "psci-cpuidle-domain", |
| .of_match_table = psci_of_match, |
| .sync_state = psci_cpuidle_domain_sync_state, |
| }, |
| }; |
| |
| static int __init psci_idle_init_domains(void) |
| { |
| return platform_driver_register(&psci_cpuidle_domain_driver); |
| } |
| subsys_initcall(psci_idle_init_domains); |
| |
| struct device *psci_dt_attach_cpu(int cpu) |
| { |
| struct device *dev; |
| |
| dev = dev_pm_domain_attach_by_name(get_cpu_device(cpu), "psci"); |
| if (IS_ERR_OR_NULL(dev)) |
| return dev; |
| |
| pm_runtime_irq_safe(dev); |
| if (cpu_online(cpu)) |
| pm_runtime_get_sync(dev); |
| |
| return dev; |
| } |
| |
| void psci_dt_detach_cpu(struct device *dev) |
| { |
| if (IS_ERR_OR_NULL(dev)) |
| return; |
| |
| dev_pm_domain_detach(dev, false); |
| } |