| /* |
| * Copyright (C) 2016 Freescale Semiconductor, Inc. |
| * Copyright 2017 NXP |
| * |
| * This program is free software; you can redistribute it and/or |
| * modify it under the terms of the GNU General Public License |
| * as published by the Free Software Foundation; either version 2 |
| * of the License, or (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| */ |
| |
| #include <linux/arm-smccc.h> |
| #include <linux/clk.h> |
| #include <linux/cpumask.h> |
| #include <linux/delay.h> |
| #include <linux/io.h> |
| #include <linux/irq.h> |
| #include <linux/irqchip.h> |
| #include <linux/irqchip/arm-gic.h> |
| #include <linux/module.h> |
| #include <linux/of.h> |
| #include <linux/of_address.h> |
| #include <linux/of_irq.h> |
| #include <linux/platform_device.h> |
| #include <linux/regulator/consumer.h> |
| #include <linux/pm_domain.h> |
| #include <soc/imx/fsl_sip.h> |
| |
| #define GPC_MAX_IRQS (4 * 32) |
| |
| struct imx_gpc_pm_domain { |
| const char name[30]; |
| struct device *dev; |
| struct generic_pm_domain pd; |
| u32 gpc_domain_id; |
| struct clk **clks; |
| unsigned int num_clks; |
| struct regulator *reg; |
| }; |
| |
| enum imx_gpc_pm_domain_state { |
| GPC_PD_STATE_OFF, |
| GPC_PD_STATE_ON, |
| }; |
| |
| #define to_imx_gpc_pm_domain(_genpd) container_of(_genpd, struct imx_gpc_pm_domain, pd) |
| |
| static DEFINE_SPINLOCK(gpc_psci_lock); |
| static DEFINE_MUTEX(gpc_pd_mutex); |
| |
| /* USB1 port power domain */ |
| static bool usb1_on = false; |
| |
| static void imx_gpc_psci_irq_unmask(struct irq_data *d) |
| { |
| struct arm_smccc_res res; |
| |
| spin_lock(&gpc_psci_lock); |
| arm_smccc_smc(FSL_SIP_GPC, FSL_SIP_CONFIG_GPC_UNMASK, d->hwirq, |
| 0, 0, 0, 0, 0, &res); |
| spin_unlock(&gpc_psci_lock); |
| |
| irq_chip_unmask_parent(d); |
| } |
| |
| static void imx_gpc_psci_irq_mask(struct irq_data *d) |
| { |
| struct arm_smccc_res res; |
| |
| spin_lock(&gpc_psci_lock); |
| arm_smccc_smc(FSL_SIP_GPC, FSL_SIP_CONFIG_GPC_MASK, d->hwirq, |
| 0, 0, 0, 0, 0, &res); |
| spin_unlock(&gpc_psci_lock); |
| |
| irq_chip_mask_parent(d); |
| } |
| static int imx_gpc_psci_irq_set_wake(struct irq_data *d, unsigned int on) |
| { |
| struct arm_smccc_res res; |
| |
| spin_lock(&gpc_psci_lock); |
| arm_smccc_smc(FSL_SIP_GPC, FSL_SIP_CONFIG_GPC_SET_WAKE, d->hwirq, |
| on, 0, 0, 0, 0, &res); |
| spin_unlock(&gpc_psci_lock); |
| |
| return 0; |
| } |
| |
| static int imx_gpc_psci_irq_set_affinity(struct irq_data *d, |
| const struct cpumask *dest, bool force) |
| { |
| /* parse the cpu of irq affinity */ |
| struct arm_smccc_res res; |
| int cpu = cpumask_any_and(dest, cpu_online_mask); |
| |
| irq_chip_set_affinity_parent(d, dest, force); |
| |
| spin_lock(&gpc_psci_lock); |
| arm_smccc_smc(FSL_SIP_GPC, 0x4, d->hwirq, |
| cpu, 0, 0, 0, 0, &res); |
| spin_unlock(&gpc_psci_lock); |
| |
| return 0; |
| } |
| |
| static struct irq_chip imx_gpc_psci_chip = { |
| .name = "GPC-PSCI", |
| .irq_eoi = irq_chip_eoi_parent, |
| .irq_mask = imx_gpc_psci_irq_mask, |
| .irq_unmask = imx_gpc_psci_irq_unmask, |
| .irq_retrigger = irq_chip_retrigger_hierarchy, |
| .irq_set_wake = imx_gpc_psci_irq_set_wake, |
| .irq_set_affinity = imx_gpc_psci_irq_set_affinity, |
| }; |
| |
| static int imx_gpc_psci_domain_translate(struct irq_domain *d, |
| struct irq_fwspec *fwspec, |
| unsigned long *hwirq, |
| unsigned int *type) |
| { |
| if (is_of_node(fwspec->fwnode)) { |
| if (fwspec->param_count != 3) |
| return -EINVAL; |
| |
| /* No PPI should point to this domain */ |
| if (fwspec->param[0] != 0) |
| return -EINVAL; |
| |
| *hwirq = fwspec->param[1]; |
| *type = fwspec->param[2]; |
| return 0; |
| } |
| |
| return -EINVAL; |
| } |
| |
| static int imx_gpc_psci_domain_alloc(struct irq_domain *domain, |
| unsigned int irq, |
| unsigned int nr_irqs, void *data) |
| { |
| struct irq_fwspec *fwspec = data; |
| struct irq_fwspec parent_fwspec; |
| irq_hw_number_t hwirq; |
| int i; |
| |
| if (fwspec->param_count != 3) |
| return -EINVAL; /* Not GIC compliant */ |
| if (fwspec->param[0] != 0) |
| return -EINVAL; /* No PPI should point to this domain */ |
| |
| hwirq = fwspec->param[1]; |
| if (hwirq >= GPC_MAX_IRQS) |
| return -EINVAL; /* Can't deal with this */ |
| |
| for (i = 0; i < nr_irqs; i++) |
| irq_domain_set_hwirq_and_chip(domain, irq + i, hwirq + i, |
| &imx_gpc_psci_chip, NULL); |
| |
| parent_fwspec = *fwspec; |
| parent_fwspec.fwnode = domain->parent->fwnode; |
| |
| return irq_domain_alloc_irqs_parent(domain, irq, nr_irqs, |
| &parent_fwspec); |
| } |
| |
| static struct irq_domain_ops imx_gpc_psci_domain_ops = { |
| .translate = imx_gpc_psci_domain_translate, |
| .alloc = imx_gpc_psci_domain_alloc, |
| .free = irq_domain_free_irqs_common, |
| }; |
| |
| static int __init imx_gpc_psci_init(struct device_node *node, |
| struct device_node *parent) |
| { |
| struct irq_domain *parent_domain, *domain; |
| |
| if (!parent) { |
| pr_err("%s: no parent, giving up\n", node->full_name); |
| return -ENODEV; |
| } |
| |
| parent_domain = irq_find_host(parent); |
| if (!parent_domain) { |
| pr_err("%s: unable to obtain parent domain\n", node->full_name); |
| return -ENXIO; |
| } |
| |
| domain = irq_domain_add_hierarchy(parent_domain, 0, GPC_MAX_IRQS, |
| node, &imx_gpc_psci_domain_ops, |
| NULL); |
| if (!domain) |
| return -ENOMEM; |
| |
| return 0; |
| } |
| IRQCHIP_DECLARE(imx_gpc_psci, "fsl,imx8mq-gpc", imx_gpc_psci_init); |
| |
| static int imx_gpc_pd_power_on(struct generic_pm_domain *domain) |
| { |
| struct imx_gpc_pm_domain *pd = to_imx_gpc_pm_domain(domain); |
| struct arm_smccc_res res; |
| int index, ret = 0; |
| |
| if (usb1_on && pd->gpc_domain_id == 3) |
| return 0; |
| |
| /* power on the external supply */ |
| if (pd->reg) { |
| ret = regulator_enable(pd->reg); |
| if (ret) { |
| dev_warn(pd->dev, "failed to power up the reg%d\n", ret); |
| return ret; |
| } |
| } |
| |
| /* enable the necessary clks needed by the power domain */ |
| if (pd->num_clks) { |
| for (index = 0; index < pd->num_clks; index++) |
| clk_prepare_enable(pd->clks[index]); |
| } |
| |
| mutex_lock(&gpc_pd_mutex); |
| arm_smccc_smc(FSL_SIP_GPC, FSL_SIP_CONFIG_GPC_PM_DOMAIN, pd->gpc_domain_id, |
| GPC_PD_STATE_ON, 0, 0, 0, 0, &res); |
| mutex_unlock(&gpc_pd_mutex); |
| |
| if (pd->gpc_domain_id == 3) |
| usb1_on = true; |
| |
| return 0; |
| } |
| |
| static int imx_gpc_pd_power_off(struct generic_pm_domain *domain) |
| { |
| struct imx_gpc_pm_domain *pd = to_imx_gpc_pm_domain(domain); |
| struct arm_smccc_res res; |
| int index, ret = 0; |
| |
| if (pd->gpc_domain_id == 3) |
| return 0; |
| |
| mutex_lock(&gpc_pd_mutex); |
| arm_smccc_smc(FSL_SIP_GPC, FSL_SIP_CONFIG_GPC_PM_DOMAIN, pd->gpc_domain_id, |
| GPC_PD_STATE_OFF, 0, 0, 0, 0, &res); |
| mutex_unlock(&gpc_pd_mutex); |
| |
| /* power off the external supply */ |
| if (pd->reg) { |
| ret = regulator_disable(pd->reg); |
| if (ret) { |
| dev_warn(pd->dev, "failed to power off the reg%d\n", ret); |
| return ret; |
| } |
| } |
| |
| /* disable the necessary clks when power domain on finished */ |
| if (pd->num_clks) { |
| for (index = 0; index < pd->num_clks; index++) |
| clk_disable_unprepare(pd->clks[index]); |
| } |
| |
| return ret; |
| }; |
| |
| static int imx8m_pd_clk_init(struct device_node *np, |
| struct imx_gpc_pm_domain *domain) |
| { |
| struct property *pp; |
| struct clk **clks; |
| int index; |
| |
| pp = of_find_property(np, "clocks", NULL); |
| if (pp) |
| domain->num_clks = pp->length / 8; |
| else |
| domain->num_clks = 0; |
| |
| if (domain->num_clks) { |
| clks = kcalloc(domain->num_clks, sizeof(*clks), GFP_KERNEL); |
| if (!clks) { |
| domain->num_clks = 0; |
| domain->clks = NULL; |
| return -ENOMEM; |
| } |
| |
| domain->clks = clks; |
| } |
| |
| for (index = 0; index < domain->num_clks; index++) { |
| clks[index] = of_clk_get(np, index); |
| if (IS_ERR(clks[index])) { |
| for (index = 0; index < domain->num_clks; index++) { |
| if (!IS_ERR(clks[index])) |
| clk_put(clks[index]); |
| } |
| |
| domain->num_clks = 0; |
| domain->clks = NULL; |
| kfree(clks); |
| pr_warn("imx8m domain clock init failed\n"); |
| return -ENODEV; |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int imx8m_add_subdomain(struct device_node *parent, |
| struct generic_pm_domain *parent_pd) |
| { |
| struct device_node *child_node; |
| struct imx_gpc_pm_domain *child_domain; |
| int ret = 0; |
| |
| /* add each of the child domain of parent */ |
| for_each_child_of_node(parent, child_node) { |
| if (!of_device_is_available(child_node)) |
| continue; |
| |
| child_domain = kzalloc(sizeof(*child_domain), GFP_KERNEL); |
| if (!child_domain) |
| return -ENOMEM; |
| |
| ret = of_property_read_string(child_node, "domain-name", |
| &child_domain->pd.name); |
| if (ret) |
| goto exit; |
| |
| ret = of_property_read_u32(child_node, "domain-id", |
| &child_domain->gpc_domain_id); |
| if (ret) |
| goto exit; |
| |
| child_domain->pd.power_off = imx_gpc_pd_power_off; |
| child_domain->pd.power_on = imx_gpc_pd_power_on; |
| /* no reg for subdomains */ |
| child_domain->reg = NULL; |
| |
| imx8m_pd_clk_init(child_node, child_domain); |
| |
| /* power domains as off at boot */ |
| pm_genpd_init(&child_domain->pd, NULL, true); |
| |
| /* add subdomain of parent power domain */ |
| pm_genpd_add_subdomain(parent_pd, &child_domain->pd); |
| |
| ret = of_genpd_add_provider_simple(child_node, |
| &child_domain->pd); |
| if (ret) |
| pr_err("failed to add subdomain\n"); |
| } |
| |
| return 0; |
| exit: |
| kfree(child_domain); |
| return ret; |
| }; |
| |
| static int imx_gpc_pm_domain_probe(struct platform_device *pdev) |
| { |
| struct device *dev = &pdev->dev; |
| struct device_node *np = dev->of_node; |
| struct imx_gpc_pm_domain *imx_pm_domain; |
| int ret = 0; |
| |
| if (!np) { |
| dev_err(dev, "power domain device tree node not found\n"); |
| return -ENODEV; |
| } |
| |
| imx_pm_domain = devm_kzalloc(dev, sizeof(*imx_pm_domain), GFP_KERNEL); |
| if (!imx_pm_domain) |
| return -ENOMEM; |
| imx_pm_domain->dev = dev; |
| |
| ret = of_property_read_string(np, "domain-name", &imx_pm_domain->pd.name); |
| if (ret) { |
| dev_err(dev, "get domain name failed\n"); |
| return -EINVAL; |
| } |
| |
| ret = of_property_read_u32(np, "domain-id", &imx_pm_domain->gpc_domain_id); |
| if (ret) { |
| dev_err(dev, "get domain id failed\n"); |
| return -EINVAL; |
| } |
| |
| imx_pm_domain->reg = devm_regulator_get_optional(dev, "power"); |
| if (IS_ERR(imx_pm_domain->reg)) { |
| if (PTR_ERR(imx_pm_domain->reg) == -EPROBE_DEFER) |
| return -EPROBE_DEFER; |
| |
| imx_pm_domain->reg = NULL; |
| } |
| |
| imx8m_pd_clk_init(np, imx_pm_domain); |
| |
| imx_pm_domain->pd.power_off = imx_gpc_pd_power_off; |
| imx_pm_domain->pd.power_on = imx_gpc_pd_power_on; |
| /* all power domains as off at boot */ |
| pm_genpd_init(&imx_pm_domain->pd, NULL, true); |
| |
| ret = of_genpd_add_provider_simple(np, |
| &imx_pm_domain->pd); |
| |
| /* add subdomain */ |
| ret = imx8m_add_subdomain(np, &imx_pm_domain->pd); |
| if (ret) |
| dev_warn(dev, "please check the child power domain init\n"); |
| |
| return 0; |
| } |
| |
| static const struct of_device_id imx_gpc_pm_domain_ids[] = { |
| {.compatible = "fsl,imx8mq-pm-domain"}, |
| {.compatible = "fsl,imx8mm-pm-domain"}, |
| {}, |
| }; |
| |
| static struct platform_driver imx_gpc_pm_domain_driver = { |
| .driver = { |
| .name = "imx8m_gpc_pm_domain", |
| .owner = THIS_MODULE, |
| .of_match_table = imx_gpc_pm_domain_ids, |
| }, |
| .probe = imx_gpc_pm_domain_probe, |
| }; |
| |
| module_platform_driver(imx_gpc_pm_domain_driver); |
| |
| MODULE_AUTHOR("NXP"); |
| MODULE_DESCRIPTION("NXP i.MX8M GPC power domain driver"); |
| MODULE_LICENSE("GPL v2"); |