| /* |
| * 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/clk-provider.h> |
| #include <linux/console.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/of.h> |
| #include <linux/of_address.h> |
| #include <linux/of_platform.h> |
| #include <linux/platform_device.h> |
| #include <linux/pm.h> |
| #include <linux/pm_runtime.h> |
| #include <linux/pm_clock.h> |
| #include <linux/slab.h> |
| #include <linux/syscore_ops.h> |
| |
| #include <soc/imx/fsl_sip.h> |
| #include <soc/imx8/sc/sci.h> |
| |
| #include "pm-domain-imx8.h" |
| |
| static sc_ipc_t pm_ipc_handle; |
| static sc_rsrc_t early_power_on_rsrc[] = { |
| SC_R_LAST, SC_R_LAST, SC_R_LAST, SC_R_LAST, SC_R_LAST, |
| SC_R_LAST, SC_R_LAST, SC_R_LAST, SC_R_LAST, SC_R_LAST, |
| }; |
| static sc_rsrc_t rsrc_debug_console; |
| |
| #define IMX8_WU_MAX_IRQS (((SC_R_LAST + 31) / 32 ) * 32 ) |
| static sc_rsrc_t irq2rsrc[IMX8_WU_MAX_IRQS]; |
| static sc_rsrc_t wakeup_rsrc_id[IMX8_WU_MAX_IRQS / 32]; |
| static DEFINE_SPINLOCK(imx8_wu_lock); |
| static DEFINE_MUTEX(rsrc_pm_list_lock); |
| |
| enum imx_pd_state { |
| PD_LP, |
| PD_OFF, |
| }; |
| |
| struct clk_stat { |
| struct clk *clk; |
| struct clk *parent; |
| unsigned long rate; |
| }; |
| |
| static int imx8_pd_power(struct generic_pm_domain *domain, bool power_on) |
| { |
| struct imx8_pm_domain *pd; |
| sc_err_t sci_err = SC_ERR_NONE; |
| |
| pd = container_of(domain, struct imx8_pm_domain, pd); |
| |
| if (pd->rsrc_id == SC_R_NONE) |
| return 0; |
| |
| /* keep uart console power on for no_console_suspend */ |
| if (pd->rsrc_id == rsrc_debug_console && |
| !console_suspend_enabled && !power_on) |
| return 0; |
| |
| /* keep resource power on if it is a wakeup source */ |
| if (!power_on && ((1 << pd->rsrc_id % 32) & |
| wakeup_rsrc_id[pd->rsrc_id / 32])) |
| return 0; |
| |
| sci_err = sc_pm_set_resource_power_mode(pm_ipc_handle, pd->rsrc_id, |
| (power_on) ? SC_PM_PW_MODE_ON : |
| pd->pd.state_idx ? SC_PM_PW_MODE_OFF : SC_PM_PW_MODE_LP); |
| if (sci_err) { |
| pr_err("Failed power operation on resource %d sc_err %d\n", |
| pd->rsrc_id, sci_err); |
| return -EINVAL; |
| } |
| |
| /* keep HDMI TX resource power on */ |
| if (power_on && (pd->rsrc_id == SC_R_HDMI || |
| pd->rsrc_id == SC_R_HDMI_I2S || |
| pd->rsrc_id == SC_R_HDMI_I2C_0 || |
| pd->rsrc_id == SC_R_HDMI_PLL_0 || |
| pd->rsrc_id == SC_R_HDMI_PLL_1)) |
| pd->pd.flags |= GENPD_FLAG_ALWAYS_ON; |
| |
| return 0; |
| } |
| |
| static int imx8_pd_power_on(struct generic_pm_domain *domain) |
| { |
| struct imx8_pm_domain *pd; |
| struct imx8_pm_rsrc_clks *imx8_rsrc_clk; |
| int ret = 0; |
| |
| pd = container_of(domain, struct imx8_pm_domain, pd); |
| |
| ret = imx8_pd_power(domain, true); |
| if (ret) |
| return ret; |
| |
| if (!list_empty(&pd->clks) && (pd->pd.state_idx == PD_OFF)) { |
| |
| if (pd->clk_state_saved) { |
| /* |
| * The SS is powered on restore the clock rates that |
| * may be lost. |
| */ |
| list_for_each_entry(imx8_rsrc_clk, &pd->clks, node) { |
| |
| if (imx8_rsrc_clk->parent) |
| clk_set_parent(imx8_rsrc_clk->clk, |
| imx8_rsrc_clk->parent); |
| |
| if (imx8_rsrc_clk->rate) { |
| /* |
| * Need to read the clock so that rate in |
| * Linux is reset. |
| */ |
| clk_get_rate(imx8_rsrc_clk->clk); |
| /* Restore the clock rate. */ |
| clk_set_rate(imx8_rsrc_clk->clk, |
| imx8_rsrc_clk->rate); |
| } |
| } |
| } else if (pd->clk_state_may_lost) { |
| struct clk_stat *clk_stats; |
| int count = 0; |
| int i = 0; |
| /* |
| * The SS is powered down before without saving clk rates, |
| * try to restore the lost clock rates if any |
| * |
| * As a parent clk rate restore will cause the clk recalc |
| * to all possible child clks which may result in child clk |
| * previous state lost due to power domain lost before, we |
| * have to first walk through all child clks to retrieve the |
| * state via clk_hw_get_rate which bypassed the clk recalc, |
| * then we can restore them one by one. |
| */ |
| list_for_each_entry(imx8_rsrc_clk, &pd->clks, node) |
| count++; |
| |
| clk_stats = kzalloc(count * sizeof(*clk_stats), GFP_KERNEL); |
| if (!clk_stats) { |
| pr_warn("%s: failed to alloc mem for clk state recovery\n", pd->name); |
| return -ENOMEM; |
| } |
| |
| list_for_each_entry(imx8_rsrc_clk, &pd->clks, node) { |
| clk_stats[i].clk = imx8_rsrc_clk->clk; |
| clk_stats[i].parent = clk_get_parent(imx8_rsrc_clk->clk); |
| clk_stats[i].rate = clk_hw_get_rate(__clk_get_hw(imx8_rsrc_clk->clk)); |
| i++; |
| } |
| |
| for (i = 0; i < count; i++) { |
| /* restore parent first */ |
| if (clk_stats[i].parent) |
| clk_set_parent(clk_stats[i].clk, clk_stats[i].parent); |
| |
| if (clk_stats[i].rate) { |
| /* invalid cached rate first by get rate once */ |
| clk_get_rate(clk_stats[i].clk); |
| /* restore the lost rate */ |
| clk_set_rate(clk_stats[i].clk, clk_stats[i].rate); |
| } |
| } |
| |
| kfree(clk_stats); |
| } |
| } |
| |
| return 0; |
| } |
| |
| static int imx8_pd_power_off(struct generic_pm_domain *domain) |
| { |
| struct imx8_pm_domain *pd; |
| struct imx8_pm_rsrc_clks *imx8_rsrc_clk; |
| |
| pd = container_of(domain, struct imx8_pm_domain, pd); |
| |
| if (!list_empty(&pd->clks) && (pd->pd.state_idx == PD_OFF)) { |
| /* |
| * The SS is going to be powered off, store the clock rates |
| * that may be lost. |
| */ |
| list_for_each_entry(imx8_rsrc_clk, &pd->clks, node) { |
| imx8_rsrc_clk->parent = clk_get_parent(imx8_rsrc_clk->clk); |
| imx8_rsrc_clk->rate = clk_hw_get_rate(__clk_get_hw(imx8_rsrc_clk->clk)); |
| } |
| pd->clk_state_saved = true; |
| pd->clk_state_may_lost = false; |
| } else if (pd->pd.state_idx == PD_OFF) { |
| pd->clk_state_saved = false; |
| pd->clk_state_may_lost = true; |
| } else { |
| pd->clk_state_saved = false; |
| pd->clk_state_may_lost = false; |
| } |
| return imx8_pd_power(domain, false); |
| } |
| |
| static int imx8_attach_dev(struct generic_pm_domain *genpd, struct device *dev) |
| { |
| struct imx8_pm_domain *pd; |
| struct device_node *node = dev->of_node; |
| struct of_phandle_args clkspec; |
| int rc, index, num_clks; |
| |
| pd = container_of(genpd, struct imx8_pm_domain, pd); |
| |
| num_clks = of_count_phandle_with_args(node, "assigned-clocks", |
| "#clock-cells"); |
| if (num_clks == -EINVAL) |
| pr_err("%s: Invalid value of assigned-clocks property at %s\n", |
| pd->name, node->full_name); |
| |
| for (index = 0; index < num_clks; index++) { |
| struct imx8_pm_rsrc_clks *imx8_rsrc_clk; |
| |
| rc = of_parse_phandle_with_args(node, "assigned-clocks", |
| "#clock-cells", index, &clkspec); |
| if (rc < 0) { |
| /* skip empty (null) phandles */ |
| if (rc == -ENOENT) |
| continue; |
| else |
| return rc; |
| } |
| if (clkspec.np == node) |
| return 0; |
| |
| imx8_rsrc_clk = devm_kzalloc(dev, sizeof(*imx8_rsrc_clk), |
| GFP_KERNEL); |
| if (!imx8_rsrc_clk) |
| return -ENOMEM; |
| |
| imx8_rsrc_clk->dev = dev; |
| imx8_rsrc_clk->clk = of_clk_get_from_provider(&clkspec); |
| if (!IS_ERR(imx8_rsrc_clk->clk)) |
| list_add_tail(&imx8_rsrc_clk->node, &pd->clks); |
| } |
| return 0; |
| } |
| |
| static void imx8_detach_dev(struct generic_pm_domain *genpd, struct device *dev) |
| { |
| struct imx8_pm_domain *pd; |
| struct imx8_pm_rsrc_clks *imx8_rsrc_clk, *tmp; |
| |
| pd = container_of(genpd, struct imx8_pm_domain, pd); |
| |
| /* Free all the clock entry nodes. */ |
| if (list_empty(&pd->clks)) |
| return; |
| |
| list_for_each_entry_safe(imx8_rsrc_clk, tmp, &pd->clks, node) { |
| /* only delete those clocks belonged to this devive */ |
| if (imx8_rsrc_clk->dev != dev) |
| continue; |
| list_del(&imx8_rsrc_clk->node); |
| devm_kfree(dev, imx8_rsrc_clk); |
| } |
| } |
| |
| static int imx8_pm_domains_suspend(void) |
| { |
| struct arm_smccc_res res; |
| unsigned int i; |
| |
| for (i = 0; i < IMX8_WU_MAX_IRQS / 32; i++) { |
| if (wakeup_rsrc_id[i] != 0) { |
| arm_smccc_smc(FSL_SIP_WAKEUP_SRC, |
| FSL_SIP_WAKEUP_SRC_IRQSTEER, 0, |
| 0, 0, 0, 0, 0, &res); |
| return 0; |
| } |
| } |
| |
| arm_smccc_smc(FSL_SIP_WAKEUP_SRC, |
| FSL_SIP_WAKEUP_SRC_SCU, 0, |
| 0, 0, 0, 0, 0, &res); |
| |
| return 0; |
| } |
| |
| static void imx8_pm_domains_resume(void) |
| { |
| sc_err_t sci_err = SC_ERR_NONE; |
| int i; |
| |
| for (i = 0; i < (sizeof(early_power_on_rsrc) / |
| sizeof(sc_rsrc_t)); i++) { |
| if (early_power_on_rsrc[i] != SC_R_LAST) { |
| sci_err = sc_pm_set_resource_power_mode(pm_ipc_handle, |
| early_power_on_rsrc[i], SC_PM_PW_MODE_ON); |
| if (sci_err != SC_ERR_NONE) |
| pr_err("fail to power on resource %d\n", |
| early_power_on_rsrc[i]); |
| } |
| } |
| } |
| |
| struct syscore_ops imx8_pm_domains_syscore_ops = { |
| .suspend = imx8_pm_domains_suspend, |
| .resume = imx8_pm_domains_resume, |
| }; |
| |
| static void imx8_pd_setup(struct imx8_pm_domain *pd) |
| { |
| pd->pd.states = kzalloc(2 * sizeof(struct genpd_power_state), GFP_KERNEL); |
| BUG_ON(!pd->pd.states); |
| |
| pd->pd.power_off = imx8_pd_power_off; |
| pd->pd.power_on = imx8_pd_power_on; |
| pd->pd.attach_dev = imx8_attach_dev; |
| pd->pd.detach_dev = imx8_detach_dev; |
| |
| pd->pd.states[0].power_off_latency_ns = 25000; |
| pd->pd.states[0].power_on_latency_ns = 25000; |
| pd->pd.states[1].power_off_latency_ns = 2500000; |
| pd->pd.states[1].power_on_latency_ns = 2500000; |
| |
| pd->pd.state_count = 2; |
| } |
| |
| static int __init imx8_add_pm_domains(struct device_node *parent, |
| struct generic_pm_domain *genpd_parent) |
| { |
| struct device_node *np; |
| static int index; |
| |
| for_each_child_of_node(parent, np) { |
| struct imx8_pm_domain *imx8_pd; |
| sc_rsrc_t rsrc_id; |
| u32 wakeup_irq; |
| |
| if (!of_device_is_available(np)) |
| continue; |
| |
| imx8_pd = kzalloc(sizeof(*imx8_pd), GFP_KERNEL); |
| if (!imx8_pd) |
| return -ENOMEM; |
| |
| if (!of_property_read_string(np, "name", &imx8_pd->pd.name)) |
| imx8_pd->name = imx8_pd->pd.name; |
| |
| if (!of_property_read_u32(np, "reg", &rsrc_id)) |
| imx8_pd->rsrc_id = rsrc_id; |
| |
| if (imx8_pd->rsrc_id != SC_R_NONE) { |
| imx8_pd_setup(imx8_pd); |
| |
| if (of_property_read_bool(np, "early_power_on") |
| && index < (sizeof(early_power_on_rsrc) / |
| sizeof(sc_rsrc_t))) { |
| early_power_on_rsrc[index++] = imx8_pd->rsrc_id; |
| } |
| if (of_property_read_bool(np, "debug_console")) |
| rsrc_debug_console = imx8_pd->rsrc_id; |
| if (!of_property_read_u32(np, "wakeup-irq", |
| &wakeup_irq)) |
| irq2rsrc[wakeup_irq] = imx8_pd->rsrc_id; |
| } |
| INIT_LIST_HEAD(&imx8_pd->clks); |
| pm_genpd_init(&imx8_pd->pd, NULL, true); |
| |
| if (genpd_parent) |
| pm_genpd_add_subdomain(genpd_parent, &imx8_pd->pd); |
| |
| of_genpd_add_provider_simple(np, &imx8_pd->pd); |
| |
| imx8_add_pm_domains(np, &imx8_pd->pd); |
| } |
| return 0; |
| } |
| |
| static int __init imx8_init_pm_domains(void) |
| { |
| struct device_node *np; |
| sc_err_t sci_err; |
| sc_rsrc_t rsrc_id; |
| uint32_t mu_id; |
| |
| /* skip pm domains for non-SCFW system */ |
| if (!of_find_compatible_node(NULL, NULL, "nxp,imx8-pd")) |
| return 0; |
| |
| pr_info("***** imx8_init_pm_domains *****\n"); |
| |
| for_each_compatible_node(np, NULL, "nxp,imx8-pd") { |
| struct imx8_pm_domain *imx8_pd; |
| |
| if (!of_device_is_available(np)) |
| continue; |
| |
| imx8_pd = kzalloc(sizeof(struct imx8_pm_domain), GFP_KERNEL); |
| if (!imx8_pd) { |
| pr_err("%s: failed to allocate memory for domain\n", |
| __func__); |
| return -ENOMEM; |
| } |
| if (!of_property_read_string(np, "name", &imx8_pd->pd.name)) |
| imx8_pd->name = imx8_pd->pd.name; |
| |
| if (!of_property_read_u32(np, "reg", &rsrc_id)) |
| imx8_pd->rsrc_id = rsrc_id; |
| |
| if (imx8_pd->rsrc_id != SC_R_NONE) |
| imx8_pd_setup(imx8_pd); |
| |
| INIT_LIST_HEAD(&imx8_pd->clks); |
| |
| pm_genpd_init(&imx8_pd->pd, NULL, true); |
| of_genpd_add_provider_simple(np, &imx8_pd->pd); |
| imx8_add_pm_domains(np, &imx8_pd->pd); |
| } |
| |
| sci_err = sc_ipc_getMuID(&mu_id); |
| if (sci_err != SC_ERR_NONE) { |
| pr_info("Cannot obtain MU ID\n"); |
| return sci_err; |
| } |
| |
| sci_err = sc_ipc_open(&pm_ipc_handle, mu_id); |
| register_syscore_ops(&imx8_pm_domains_syscore_ops); |
| |
| return 0; |
| } |
| |
| early_initcall(imx8_init_pm_domains); |
| |
| static int imx8_wu_irq_set_wake(struct irq_data *d, unsigned int on) |
| { |
| unsigned int idx = irq2rsrc[d->hwirq] / 32; |
| u32 mask = 1 << irq2rsrc[d->hwirq] % 32; |
| |
| spin_lock(&imx8_wu_lock); |
| wakeup_rsrc_id[idx] = on ? wakeup_rsrc_id[idx] | mask : |
| wakeup_rsrc_id[idx] & ~mask; |
| spin_unlock(&imx8_wu_lock); |
| |
| return 0; |
| } |
| |
| static struct irq_chip imx8_wu_chip = { |
| .name = "IMX8-WU", |
| .irq_eoi = irq_chip_eoi_parent, |
| .irq_mask = irq_chip_mask_parent, |
| .irq_unmask = irq_chip_unmask_parent, |
| .irq_retrigger = irq_chip_retrigger_hierarchy, |
| .irq_set_wake = imx8_wu_irq_set_wake, |
| .irq_set_affinity = irq_chip_set_affinity_parent, |
| }; |
| |
| static int imx8_wu_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 imx8_wu_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 >= IMX8_WU_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, |
| &imx8_wu_chip, NULL); |
| |
| parent_fwspec = *fwspec; |
| parent_fwspec.fwnode = domain->parent->fwnode; |
| |
| return irq_domain_alloc_irqs_parent(domain, irq, nr_irqs, |
| &parent_fwspec); |
| } |
| |
| static const struct irq_domain_ops imx8_wu_domain_ops = { |
| .translate = imx8_wu_domain_translate, |
| .alloc = imx8_wu_domain_alloc, |
| .free = irq_domain_free_irqs_common, |
| }; |
| |
| static int __init imx8_wu_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, IMX8_WU_MAX_IRQS, |
| node, &imx8_wu_domain_ops, |
| NULL); |
| if (!domain) |
| return -ENOMEM; |
| |
| return 0; |
| } |
| IRQCHIP_DECLARE(imx8_wakeup_unit, "fsl,imx8-wu", imx8_wu_init); |
| |
| /*** debugfs support ***/ |
| |
| #ifdef CONFIG_DEBUG_FS |
| #include <linux/pm.h> |
| #include <linux/device.h> |
| #include <linux/debugfs.h> |
| #include <linux/seq_file.h> |
| #include <linux/init.h> |
| #include <linux/kobject.h> |
| |
| #define SC_PM_PW_MODE_FAIL (SC_PM_PW_MODE_ON + 1) |
| static struct dentry *imx8_rsrc_pm_debugfs_dir; |
| |
| static int imx8_rsrc_pm_summary_one(struct seq_file *s, |
| sc_rsrc_t rsrc_id) |
| { |
| static const char * const status_lookup[] = { |
| [SC_PM_PW_MODE_OFF] = "OFF", |
| [SC_PM_PW_MODE_STBY] = "STBY", |
| [SC_PM_PW_MODE_LP] = "LP", |
| [SC_PM_PW_MODE_ON] = "ON", |
| [SC_PM_PW_MODE_FAIL] = "FAIL", |
| }; |
| sc_err_t sci_err = SC_ERR_NONE; |
| sc_pm_power_mode_t mode; |
| char state[16]; |
| |
| sci_err = sc_pm_get_resource_power_mode(pm_ipc_handle, rsrc_id, &mode); |
| if (sci_err) { |
| pr_debug("failed to get power mode on resource %d, ret %d\n", |
| rsrc_id, sci_err); |
| mode = SC_PM_PW_MODE_FAIL; |
| } |
| |
| if (WARN_ON(mode >= ARRAY_SIZE(status_lookup))) |
| return 0; |
| |
| snprintf(state, sizeof(state), "%s", status_lookup[mode]); |
| seq_printf(s, "%-30d %-15s ", rsrc_id, state); |
| seq_puts(s, "\n"); |
| |
| return 0; |
| } |
| |
| static int imx8_rsrc_pm_summary_show(struct seq_file *s, void *data) |
| { |
| int ret = 0; |
| int i; |
| |
| seq_puts(s, "resource_id power_mode\n"); |
| seq_puts(s, "---------------------------------------------\n"); |
| |
| ret = mutex_lock_interruptible(&rsrc_pm_list_lock); |
| if (ret) |
| return -ERESTARTSYS; |
| |
| for (i = 0; i < SC_R_LAST; i++) { |
| ret = imx8_rsrc_pm_summary_one(s, i); |
| if (ret) |
| break; |
| } |
| mutex_unlock(&rsrc_pm_list_lock); |
| |
| return ret; |
| } |
| |
| static int imx8_rsrc_pm_summary_open(struct inode *inode, struct file *file) |
| { |
| return single_open(file, imx8_rsrc_pm_summary_show, NULL); |
| } |
| |
| static const struct file_operations imx8_rsrc_pm_summary_fops = { |
| .open = imx8_rsrc_pm_summary_open, |
| .read = seq_read, |
| .llseek = seq_lseek, |
| .release = single_release, |
| }; |
| |
| static int __init imx8_pm_debug_init(void) |
| { |
| struct dentry *d; |
| |
| /* skip for non-SCFW system */ |
| if (!of_find_compatible_node(NULL, NULL, "nxp,imx8-pd")) |
| return 0; |
| |
| imx8_rsrc_pm_debugfs_dir = debugfs_create_dir("imx_rsrc_pm", NULL); |
| |
| if (!imx8_rsrc_pm_debugfs_dir) |
| return -ENOMEM; |
| |
| d = debugfs_create_file("imx_rsrc_pm_summary", 0444, |
| imx8_rsrc_pm_debugfs_dir, NULL, |
| &imx8_rsrc_pm_summary_fops); |
| if (!d) |
| return -ENOMEM; |
| |
| return 0; |
| } |
| late_initcall(imx8_pm_debug_init); |
| |
| static void __exit imx8_rsrc_pm_debug_exit(void) |
| { |
| debugfs_remove_recursive(imx8_rsrc_pm_debugfs_dir); |
| } |
| __exitcall(imx8_rsrc_pm_debug_exit); |
| #endif /* CONFIG_DEBUG_FS */ |