| /* |
| * drivers/amlogic/pm/gx_pm.c |
| * |
| * Copyright (C) 2017 Amlogic, Inc. All rights reserved. |
| * |
| * 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/pm.h> |
| #include <linux/suspend.h> |
| #include <linux/module.h> |
| #include <linux/platform_device.h> |
| #include <linux/clk.h> |
| #include <linux/fs.h> |
| #include <linux/delay.h> |
| #include <linux/uaccess.h> |
| #include <linux/io.h> |
| #include <linux/amlogic/iomap.h> |
| #include <linux/init.h> |
| #include <linux/of.h> |
| #include <linux/psci.h> |
| #include <asm/compiler.h> |
| #include <linux/errno.h> |
| #include <linux/suspend.h> |
| #include <asm/suspend.h> |
| #include <linux/of_address.h> |
| #include <linux/input.h> |
| #include <linux/cpuidle.h> |
| #include <asm/cpuidle.h> |
| #include <uapi/linux/psci.h> |
| #include <linux/arm-smccc.h> |
| #include <linux/amlogic/pm.h> |
| #include <linux/kobject.h> |
| #include <../kernel/power/power.h> |
| #include <linux/amlogic/scpi_protocol.h> |
| #include "vad_power.h" |
| #include <linux/syscore_ops.h> |
| |
| typedef unsigned long (psci_fn)(unsigned long, unsigned long, |
| unsigned long, unsigned long); |
| |
| static unsigned long __invoke_psci_fn_smc(unsigned long function_id, |
| unsigned long arg0, unsigned long arg1, |
| unsigned long arg2) |
| { |
| struct arm_smccc_res res; |
| |
| arm_smccc_smc(function_id, arg0, arg1, arg2, 0, 0, 0, 0, &res); |
| return res.a0; |
| } |
| |
| static u32 psci_get_version(void) |
| { |
| return __invoke_psci_fn_smc(PSCI_0_2_FN_PSCI_VERSION, 0, 0, 0); |
| } |
| |
| |
| #undef pr_fmt |
| #define pr_fmt(fmt) "gxbb_pm: " fmt |
| |
| static void __iomem *debug_reg; |
| static void __iomem *exit_reg; |
| static int max_idle_lvl; |
| static suspend_state_t pm_state; |
| static unsigned int resume_reason; |
| |
| |
| /* |
| *0x10000 : bit[16]=1:control cpu suspend to power down |
| *cpu_suspend(0, meson_system_suspend); |
| */ |
| static void meson_gx_suspend(void) |
| { |
| pr_info("enter meson_pm_suspend!\n"); |
| arm_cpuidle_suspend(max_idle_lvl); |
| /* cpu_suspend(0, meson_system_suspend); |
| */ |
| pr_info("... wake up\n"); |
| } |
| |
| static int meson_pm_prepare(void) |
| { |
| pr_info("enter meson_pm_prepare!\n"); |
| return 0; |
| } |
| |
| static int meson_gx_enter(suspend_state_t state) |
| { |
| int ret = 0; |
| |
| switch (state) { |
| case PM_SUSPEND_STANDBY: |
| case PM_SUSPEND_MEM: |
| meson_gx_suspend(); |
| break; |
| default: |
| ret = -EINVAL; |
| } |
| return ret; |
| } |
| |
| static void meson_pm_finish(void) |
| { |
| pr_info("enter meson_pm_finish!\n"); |
| } |
| |
| /* |
| * get_resume_reason always return last resume reason. |
| */ |
| unsigned int get_resume_reason(void) |
| { |
| unsigned int val = 0; |
| |
| if (exit_reg) |
| val = (readl(exit_reg) >> 28) & 0xf; |
| return val; |
| } |
| EXPORT_SYMBOL_GPL(get_resume_reason); |
| |
| /* |
| * get_resume_method return last resume reason. |
| * It can be cleared by clr_resume_method(). |
| */ |
| unsigned int get_resume_method(void) |
| { |
| return resume_reason; |
| } |
| EXPORT_SYMBOL_GPL(get_resume_method); |
| |
| static void set_resume_method(unsigned int val) |
| { |
| resume_reason = val; |
| } |
| |
| static int clr_suspend_notify(struct notifier_block *nb, |
| unsigned long event, void *dummy) |
| { |
| if (event == PM_SUSPEND_PREPARE) |
| set_resume_method(UDEFINED_WAKEUP); |
| |
| return NOTIFY_OK; |
| } |
| |
| static struct notifier_block clr_suspend_notifier = { |
| .notifier_call = clr_suspend_notify, |
| }; |
| |
| |
| static const struct platform_suspend_ops meson_gx_ops = { |
| .enter = meson_gx_enter, |
| .prepare = meson_pm_prepare, |
| .finish = meson_pm_finish, |
| .valid = suspend_valid_only_mem, |
| }; |
| |
| unsigned int is_pm_freeze_mode(void) |
| { |
| if (pm_state == PM_SUSPEND_FREEZE) |
| return 1; |
| else |
| return 0; |
| } |
| EXPORT_SYMBOL(is_pm_freeze_mode); |
| |
| static int frz_begin(void) |
| { |
| pm_state = PM_SUSPEND_FREEZE; |
| |
| return 0; |
| } |
| |
| static void frz_end(void) |
| { |
| pm_state = PM_SUSPEND_ON; |
| } |
| |
| static const struct platform_freeze_ops meson_gx_frz_ops = { |
| .begin = frz_begin, |
| .end = frz_end, |
| }; |
| |
| static unsigned int suspend_reason; |
| ssize_t suspend_reason_show(struct device *dev, struct device_attribute *attr, |
| char *buf) |
| { |
| unsigned int len; |
| |
| if (scpi_get_wakeup_reason(&suspend_reason)) |
| return -EPERM; |
| len = sprintf(buf, "%d\n", suspend_reason); |
| |
| return len; |
| } |
| ssize_t suspend_reason_store(struct device *dev, struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| int ret; |
| |
| ret = kstrtouint(buf, 0, &suspend_reason); |
| |
| switch (ret) { |
| case 1: |
| __invoke_psci_fn_smc(0x82000042, suspend_reason, 0, 0); |
| break; |
| default: |
| return -EINVAL; |
| } |
| return count; |
| } |
| |
| DEVICE_ATTR(suspend_reason, 0664, suspend_reason_show, suspend_reason_store); |
| |
| ssize_t time_out_show(struct device *dev, struct device_attribute *attr, |
| char *buf) |
| { |
| unsigned int val = 0, len; |
| |
| val = readl(debug_reg); |
| len = sprintf(buf, "%d\n", val); |
| |
| return len; |
| } |
| |
| static int sys_time_out; |
| ssize_t time_out_store(struct device *dev, struct device_attribute *attr, |
| const char *buf, size_t count) |
| { |
| unsigned int time_out; |
| int ret; |
| |
| ret = kstrtouint(buf, 10, &time_out); |
| switch (ret) { |
| case 0: |
| sys_time_out = time_out; |
| writel(time_out, debug_reg); |
| break; |
| default: |
| return -EINVAL; |
| } |
| |
| return count; |
| } |
| |
| DEVICE_ATTR(time_out, 0664, time_out_show, time_out_store); |
| |
| int gx_pm_syscore_suspend(void) |
| { |
| if (sys_time_out) |
| writel_relaxed(sys_time_out, debug_reg); |
| return 0; |
| } |
| |
| void gx_pm_syscore_resume(void) |
| { |
| sys_time_out = 0; |
| set_resume_method(get_resume_reason()); |
| } |
| |
| static struct syscore_ops gx_pm_syscore_ops = { |
| .suspend = gx_pm_syscore_suspend, |
| .resume = gx_pm_syscore_resume, |
| }; |
| |
| static int __init gx_pm_init_ops(void) |
| { |
| register_syscore_ops(&gx_pm_syscore_ops); |
| return 0; |
| } |
| |
| static int meson_pm_probe(struct platform_device *pdev) |
| { |
| struct device_node *cpu_node; |
| struct device_node *state_node; |
| struct pm_data *p_data; |
| struct device *dev = &pdev->dev; |
| int count = 0, ret; |
| u32 ver = psci_get_version(); |
| u32 paddr = 0; |
| |
| pr_info("enter meson_pm_probe!\n"); |
| |
| if (PSCI_VERSION_MAJOR(ver) == 0 && PSCI_VERSION_MINOR(ver) == 2) { |
| cpu_node = of_get_cpu_node(0, NULL); |
| if (!cpu_node) { |
| pr_info("cpu node get fail!\n"); |
| return -1; |
| } |
| while ((state_node = of_parse_phandle(cpu_node, |
| "cpu-idle-states", count))) { |
| count++; |
| of_node_put(state_node); |
| } |
| if (count) |
| max_idle_lvl = count; |
| else { |
| max_idle_lvl = -1; |
| pr_info("get system suspend level fail!\n"); |
| return -1; |
| } |
| pr_info("system suspend level: %d\n", max_idle_lvl); |
| suspend_set_ops(&meson_gx_ops); |
| } |
| |
| p_data = devm_kzalloc(&pdev->dev, sizeof(struct pm_data), GFP_KERNEL); |
| if (!p_data) |
| return -ENOMEM; |
| p_data->dev = dev; |
| dev_set_drvdata(dev, p_data); |
| |
| vad_wakeup_power_init(pdev, p_data); |
| |
| ret = of_property_read_u32(pdev->dev.of_node, |
| "debug_reg", &paddr); |
| if (!ret) { |
| pr_debug("debug_reg: 0x%x\n", paddr); |
| debug_reg = ioremap(paddr, 0x4); |
| if (IS_ERR_OR_NULL(debug_reg)) |
| goto uniomap; |
| } |
| |
| ret = of_property_read_u32(pdev->dev.of_node, |
| "exit_reg", &paddr); |
| if (!ret) { |
| pr_debug("exit_reg: 0x%x\n", paddr); |
| exit_reg = ioremap(paddr, 0x4); |
| if (IS_ERR_OR_NULL(exit_reg)) |
| goto uniomap; |
| } |
| |
| device_create_file(&pdev->dev, &dev_attr_suspend_reason); |
| device_create_file(&pdev->dev, &dev_attr_time_out); |
| |
| dev->power.wakeup = devm_kzalloc(&pdev->dev, |
| sizeof(struct wakeup_source), GFP_KERNEL); |
| wakeup_source_init(dev->power.wakeup, "ddrwakesrc"); |
| pm_stay_awake(dev); |
| #ifdef CONFIG_AMLOGIC_LEGACY_EARLY_SUSPEND |
| if (lgcy_early_suspend_init()) |
| return -1; |
| #endif |
| freeze_set_ops(&meson_gx_frz_ops); |
| gx_pm_init_ops(); |
| |
| ret = register_pm_notifier(&clr_suspend_notifier); |
| if (unlikely(ret)) |
| return ret; |
| |
| pr_info("meson_pm_probe done\n"); |
| return 0; |
| uniomap: |
| if (debug_reg) |
| iounmap(debug_reg); |
| return -ENXIO; |
| } |
| |
| int pm_suspend_noirq(struct device *dev) |
| { |
| vad_wakeup_power_suspend(dev); |
| return 0; |
| } |
| |
| int pm_resume_noirq(struct device *dev) |
| { |
| vad_wakeup_power_resume(dev); |
| return 0; |
| } |
| |
| static const struct dev_pm_ops meson_pm_noirq_ops = { |
| .suspend_noirq = pm_suspend_noirq, |
| .resume_noirq = pm_resume_noirq, |
| }; |
| |
| static int meson_pm_remove(struct platform_device *pdev) |
| { |
| if (pdev->dev.power.wakeup) |
| pm_relax(&pdev->dev); |
| return 0; |
| } |
| |
| static const struct of_device_id amlogic_pm_dt_match[] = { |
| {.compatible = "amlogic, pm", |
| }, |
| {} |
| }; |
| |
| static struct platform_driver meson_pm_driver = { |
| .driver = { |
| .name = "pm-meson", |
| .owner = THIS_MODULE, |
| .of_match_table = amlogic_pm_dt_match, |
| .pm = &meson_pm_noirq_ops, |
| }, |
| .probe = meson_pm_probe, |
| .remove = meson_pm_remove, |
| }; |
| |
| static int __init meson_pm_init(void) |
| { |
| return platform_driver_register(&meson_pm_driver); |
| } |
| |
| late_initcall(meson_pm_init); |