blob: 32900ba6689d499ac938ea585ae965f3026b9684 [file] [log] [blame]
// SPDX-License-Identifier: (GPL-2.0+ OR MIT)
/*
* Copyright (c) 2019 Amlogic, Inc. All rights reserved.
*/
#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/init.h>
#include <linux/of.h>
#include <linux/psci.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/power_domain.h>
#include <linux/syscore_ops.h>
#undef pr_fmt
#define pr_fmt(fmt) "gxbb_pm: " fmt
#ifdef CONFIG_AMLOGIC_LEGACY_EARLY_SUSPEND
static DEFINE_MUTEX(early_suspend_lock);
static DEFINE_MUTEX(sysfs_trigger_lock);
static LIST_HEAD(early_suspend_handlers);
/* In order to handle legacy early_suspend driver,
* here we export sysfs interface
* for user space to write /sys/power/early_suspend_trigger to trigger
* early_suspend/late resume call back. If user space do not trigger
* early_suspend/late_resume, this op will be done
* by PM_SUSPEND_PREPARE notify.
*/
unsigned int sysfs_trigger;
unsigned int early_suspend_state;
bool is_clr_exit_reg;
/*
* Avoid run early_suspend/late_resume repeatly.
*/
unsigned int already_early_suspend;
void register_early_suspend(struct early_suspend *handler)
{
struct list_head *pos;
mutex_lock(&early_suspend_lock);
list_for_each(pos, &early_suspend_handlers) {
struct early_suspend *e;
e = list_entry(pos, struct early_suspend, link);
if (e->level > handler->level)
break;
}
list_add_tail(&handler->link, pos);
mutex_unlock(&early_suspend_lock);
}
EXPORT_SYMBOL(register_early_suspend);
void unregister_early_suspend(struct early_suspend *handler)
{
mutex_lock(&early_suspend_lock);
list_del(&handler->link);
mutex_unlock(&early_suspend_lock);
}
EXPORT_SYMBOL(unregister_early_suspend);
static inline void early_suspend(void)
{
struct early_suspend *pos;
mutex_lock(&early_suspend_lock);
if (!already_early_suspend)
already_early_suspend = 1;
else
goto end_early_suspend;
pr_info("%s: call handlers\n", __func__);
list_for_each_entry(pos, &early_suspend_handlers, link)
if (pos->suspend) {
pr_info("%s: %ps\n", __func__, pos->suspend);
pos->suspend(pos);
}
pr_info("%s: done\n", __func__);
end_early_suspend:
mutex_unlock(&early_suspend_lock);
}
static inline void late_resume(void)
{
struct early_suspend *pos;
mutex_lock(&early_suspend_lock);
if (already_early_suspend)
already_early_suspend = 0;
else
goto end_late_resume;
pr_info("%s: call handlers\n", __func__);
list_for_each_entry_reverse(pos, &early_suspend_handlers, link)
if (pos->resume) {
pr_info("%s: %ps\n", __func__, pos->resume);
pos->resume(pos);
}
pr_info("%s: done\n", __func__);
end_late_resume:
mutex_unlock(&early_suspend_lock);
}
static ssize_t early_suspend_trigger_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
unsigned int len;
len = sprintf(buf, "%d\n", early_suspend_state);
return len;
}
static ssize_t early_suspend_trigger_store(struct device *dev,
struct device_attribute *attr,
const char *buf, size_t count)
{
int ret;
ret = kstrtouint(buf, 0, &early_suspend_state);
pr_info("early_suspend_state=%d\n", early_suspend_state);
if (ret)
return -EINVAL;
mutex_lock(&sysfs_trigger_lock);
sysfs_trigger = 1;
if (early_suspend_state == 0)
late_resume();
else if (early_suspend_state == 1)
early_suspend();
mutex_unlock(&sysfs_trigger_lock);
return count;
}
static DEVICE_ATTR_RW(early_suspend_trigger);
void lgcy_early_suspend(void)
{
mutex_lock(&sysfs_trigger_lock);
if (!sysfs_trigger)
early_suspend();
mutex_unlock(&sysfs_trigger_lock);
}
void lgcy_late_resume(void)
{
mutex_lock(&sysfs_trigger_lock);
if (!sysfs_trigger)
late_resume();
mutex_unlock(&sysfs_trigger_lock);
}
static int lgcy_early_suspend_notify(struct notifier_block *nb,
unsigned long event, void *dummy)
{
if (event == PM_SUSPEND_PREPARE)
lgcy_early_suspend();
if (event == PM_POST_SUSPEND)
lgcy_late_resume();
return NOTIFY_OK;
}
static struct notifier_block lgcy_early_suspend_notifier = {
.notifier_call = lgcy_early_suspend_notify,
};
unsigned int lgcy_early_suspend_exit(struct platform_device *pdev)
{
int ret;
device_remove_file(&pdev->dev, &dev_attr_early_suspend_trigger);
ret = unregister_pm_notifier(&lgcy_early_suspend_notifier);
return ret;
}
#endif /*CONFIG_AMLOGIC_LEGACY_EARLY_SUSPEND*/
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 void __iomem *debug_reg;
static void __iomem *exit_reg;
static suspend_state_t pm_state;
static unsigned int resume_reason;
/*
* get_resume_reason always return last resume reason.
*/
unsigned int get_resume_reason(void)
{
unsigned int val = 0;
/*For tm2 SoC, we need use scpi to get this reason*/
if (exit_reg)
val = readl_relaxed(exit_reg) & 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,
};
unsigned int is_pm_s2idle_mode(void)
{
if (pm_state == PM_SUSPEND_TO_IDLE)
return 1;
else
return 0;
}
EXPORT_SYMBOL_GPL(is_pm_s2idle_mode);
/*Call it as suspend_reason because of historical reasons. */
/*Actually, we should call it wakeup_reason. */
static unsigned int suspend_reason;
ssize_t suspend_reason_show(struct device *dev,
struct device_attribute *attr,
char *buf)
{
unsigned int len;
suspend_reason = get_resume_reason();
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;
}
static DEVICE_ATTR_RW(suspend_reason);
ssize_t time_out_show(struct device *dev, struct device_attribute *attr,
char *buf)
{
unsigned int val = 0, len;
val = readl_relaxed(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_relaxed(time_out, debug_reg);
break;
default:
return -EINVAL;
}
return count;
}
static DEVICE_ATTR_RW(time_out);
static struct attribute *meson_pm_attrs[] = {
&dev_attr_suspend_reason.attr,
&dev_attr_time_out.attr,
#ifdef CONFIG_AMLOGIC_LEGACY_EARLY_SUSPEND
&dev_attr_early_suspend_trigger.attr,
#endif
NULL,
};
ATTRIBUTE_GROUPS(meson_pm);
static struct class meson_pm_class = {
.name = "meson_pm",
.owner = THIS_MODULE,
.class_groups = meson_pm_groups,
};
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)
{
unsigned int irq_pwrctrl;
int err;
pr_info("enter %s!\n", __func__);
if (!of_property_read_u32(pdev->dev.of_node,
"irq_pwrctrl", &irq_pwrctrl)) {
pwr_ctrl_irq_set(irq_pwrctrl, 1, 0);
}
debug_reg = of_iomap(pdev->dev.of_node, 0);
if (!debug_reg)
return -ENOMEM;
exit_reg = of_iomap(pdev->dev.of_node, 1);
if (!exit_reg)
return -ENOMEM;
err = class_register(&meson_pm_class);
if (unlikely(err))
return err;
device_init_wakeup(&pdev->dev, true);
pm_stay_awake(&pdev->dev);
gx_pm_init_ops();
#ifdef CONFIG_AMLOGIC_LEGACY_EARLY_SUSPEND
err = register_pm_notifier(&lgcy_early_suspend_notifier);
if (unlikely(err))
return err;
#endif
if (of_property_read_bool(pdev->dev.of_node, "clr_reboot_mode"))
is_clr_exit_reg = true;
else
is_clr_exit_reg = false;
err = register_pm_notifier(&clr_suspend_notifier);
if (unlikely(err))
return err;
pr_info("%s done\n", __func__);
return 0;
}
static int __exit meson_pm_remove(struct platform_device *pdev)
{
if (debug_reg)
iounmap(debug_reg);
if (exit_reg)
iounmap(exit_reg);
device_remove_file(&pdev->dev, &dev_attr_suspend_reason);
device_remove_file(&pdev->dev, &dev_attr_time_out);
if (pdev->dev.power.wakeup)
pm_relax(&pdev->dev);
device_init_wakeup(&pdev->dev, false);
#ifdef CONFIG_AMLOGIC_LEGACY_EARLY_SUSPEND
lgcy_early_suspend_exit(pdev);
#endif
return 0;
}
static const struct of_device_id amlogic_pm_dt_match[] = {
{.compatible = "amlogic, pm",
},
{}
};
static void meson_pm_shutdown(struct platform_device *pdev)
{
if ((exit_reg) && (is_clr_exit_reg))
writel_relaxed(0, exit_reg);
}
static struct platform_driver meson_pm_driver = {
.probe = meson_pm_probe,
.driver = {
.name = "pm-meson",
.owner = THIS_MODULE,
.of_match_table = amlogic_pm_dt_match,
},
.remove = __exit_p(meson_pm_remove),
.shutdown = meson_pm_shutdown,
};
module_platform_driver(meson_pm_driver);
MODULE_AUTHOR("Amlogic");
MODULE_DESCRIPTION("Amlogic suspend driver");
MODULE_LICENSE("GPL");