776 lines
19 KiB
C
776 lines
19 KiB
C
|
|
// SPDX-License-Identifier: GPL-2.0-only
|
||
|
|
/*
|
||
|
|
* Copyright (c) 2021, The Linux Foundation. All rights reserved.
|
||
|
|
* Copyright (c) 2022-2025, Qualcomm Innovation Center, Inc. All rights reserved.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#define pr_fmt(fmt) "%s: %s: " fmt, KBUILD_MODNAME, __func__
|
||
|
|
|
||
|
|
#include <linux/arm-smccc.h>
|
||
|
|
#include <linux/cdev.h>
|
||
|
|
#include <linux/compat.h>
|
||
|
|
#include <linux/delay.h>
|
||
|
|
#include <linux/device.h>
|
||
|
|
#include <linux/errno.h>
|
||
|
|
#include <linux/fs.h>
|
||
|
|
#include <linux/ioctl.h>
|
||
|
|
#include <linux/kernel.h>
|
||
|
|
#include <linux/kobject.h>
|
||
|
|
#include <linux/module.h>
|
||
|
|
#include <linux/of.h>
|
||
|
|
#include <linux/platform_device.h>
|
||
|
|
#include <linux/pm_wakeup.h>
|
||
|
|
#include <linux/remoteproc.h>
|
||
|
|
#include <linux/remoteproc/qcom_rproc.h>
|
||
|
|
#include <linux/suspend.h>
|
||
|
|
#include <linux/syscore_ops.h>
|
||
|
|
#include <linux/sysfs.h>
|
||
|
|
#include <linux/uaccess.h>
|
||
|
|
#include <soc/qcom/rpm-smd.h>
|
||
|
|
#include <soc/qcom/subsystem_sleep_stats.h>
|
||
|
|
#include <linux/soc/qcom/qcom_aoss.h>
|
||
|
|
|
||
|
|
#include "linux/power_state.h"
|
||
|
|
|
||
|
|
#if IS_ENABLED(CONFIG_ARCH_MONACO)
|
||
|
|
#define DS_ENTRY_SMC_ID 0xC3000924
|
||
|
|
#else
|
||
|
|
#define DS_ENTRY_SMC_ID 0xC3000923
|
||
|
|
#endif
|
||
|
|
|
||
|
|
#if IS_ENABLED(CONFIG_MSM_RPM_SMD)
|
||
|
|
#define RPM_XO_DS_REQ 0x73646f78
|
||
|
|
#define RPM_XO_DS_ID 0x0
|
||
|
|
#define RPM_XO_DS_KEY 0x62616e45
|
||
|
|
#define RPM_XO_DS_ENTER_VALUE 0x0
|
||
|
|
#define RPM_XO_DS_EXIT_VALUE 0x1
|
||
|
|
#endif
|
||
|
|
|
||
|
|
#define DS_NUM_PARAMETERS 1
|
||
|
|
#define DS_ENTRY 1
|
||
|
|
#define DS_EXIT 0
|
||
|
|
|
||
|
|
#define POWER_STATS_BASEMINOR 0
|
||
|
|
#define POWER_STATS_MAX_MINOR 1
|
||
|
|
#define POWER_STATE_DEVICE_NAME "power_state"
|
||
|
|
#define STRING_LEN 32
|
||
|
|
#define PS_PM_NOTIFIER_PRIORITY 100
|
||
|
|
#define PS_SSR_NOTIFIER_PRIORITY 0
|
||
|
|
|
||
|
|
#define MAX_QMP_MSG_SIZE 96
|
||
|
|
#define SUSBSYSTEM_SLEEP_CHECK_MAX_RETRIES 2
|
||
|
|
#define DELAY 50
|
||
|
|
|
||
|
|
enum power_states {
|
||
|
|
ACTIVE,
|
||
|
|
DEEPSLEEP,
|
||
|
|
HIBERNATE,
|
||
|
|
};
|
||
|
|
|
||
|
|
static char * const power_state[] = {
|
||
|
|
[ACTIVE] = "active",
|
||
|
|
[DEEPSLEEP] = "deepsleep",
|
||
|
|
[HIBERNATE] = "hibernate",
|
||
|
|
};
|
||
|
|
|
||
|
|
struct subsystem_event_data {
|
||
|
|
const char *name;
|
||
|
|
enum ps_event_type enter;
|
||
|
|
enum ps_event_type exit;
|
||
|
|
};
|
||
|
|
|
||
|
|
static struct subsystem_event_data event_data[] = {
|
||
|
|
{ "mpss", MDSP_BEFORE_POWERDOWN, MDSP_AFTER_POWERUP },
|
||
|
|
{ "lpass", ADSP_BEFORE_POWERDOWN, ADSP_AFTER_POWERUP },
|
||
|
|
{ "cdsp", CDSP_BEFORE_POWERDOWN, CDSP_AFTER_POWERUP },
|
||
|
|
{ "cdsp1", CDSP1_BEFORE_POWERDOWN, CDSP1_AFTER_POWERUP },
|
||
|
|
{ "gpdsp0", GPDSP0_BEFORE_POWERDOWN, GPDSP0_AFTER_POWERUP },
|
||
|
|
{ "gpdsp1", GPDSP1_BEFORE_POWERDOWN, GPDSP1_AFTER_POWERUP },
|
||
|
|
};
|
||
|
|
|
||
|
|
struct subsystem_data {
|
||
|
|
struct list_head list;
|
||
|
|
const char *name;
|
||
|
|
bool ignore_ssr;
|
||
|
|
enum ps_event_type enter;
|
||
|
|
enum ps_event_type exit;
|
||
|
|
phandle rproc_handle;
|
||
|
|
void *ssr_handle;
|
||
|
|
struct notifier_block ps_ssr_nb;
|
||
|
|
};
|
||
|
|
|
||
|
|
struct power_state_drvdata {
|
||
|
|
struct class *ps_class;
|
||
|
|
struct device *ps_dev;
|
||
|
|
struct cdev ps_cdev;
|
||
|
|
dev_t ps_dev_no;
|
||
|
|
struct kobject *ps_kobj;
|
||
|
|
struct kobj_attribute ps_ka;
|
||
|
|
struct kobj_attribute ds_ka;
|
||
|
|
struct kobj_attribute ds_allow_ka;
|
||
|
|
struct wakeup_source *ps_ws;
|
||
|
|
struct notifier_block ps_pm_nb;
|
||
|
|
struct qmp *qmp;
|
||
|
|
struct msm_rpm_kvp kvp_req;
|
||
|
|
struct syscore_ops ps_ops;
|
||
|
|
enum power_states current_state;
|
||
|
|
int subsys_count;
|
||
|
|
struct list_head sub_sys_list;
|
||
|
|
int deep_sleep_cnt;
|
||
|
|
int deep_sleep_allowed;
|
||
|
|
};
|
||
|
|
|
||
|
|
static struct power_state_drvdata *drv;
|
||
|
|
|
||
|
|
static int subsys_suspend(struct subsystem_data *ss_data, struct rproc *rproc, uint32_t state)
|
||
|
|
{
|
||
|
|
int ret = 0;
|
||
|
|
|
||
|
|
switch (state) {
|
||
|
|
case SUBSYS_DEEPSLEEP:
|
||
|
|
case SUBSYS_HIBERNATE:
|
||
|
|
ss_data->ignore_ssr = true;
|
||
|
|
#if IS_ENABLED(CONFIG_QCOM_DS_SKIP_Q6_STOP)
|
||
|
|
adsp_set_ops_stop(rproc, true);
|
||
|
|
#endif
|
||
|
|
rproc_shutdown(rproc);
|
||
|
|
ss_data->ignore_ssr = false;
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
pr_err("Invalid %s suspend state\n", ss_data->name);
|
||
|
|
ret = -1;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
static int subsys_resume(struct subsystem_data *ss_data, struct rproc *rproc, u32 state)
|
||
|
|
{
|
||
|
|
int ret = 0;
|
||
|
|
|
||
|
|
switch (state) {
|
||
|
|
case SUBSYS_DEEPSLEEP:
|
||
|
|
case SUBSYS_HIBERNATE:
|
||
|
|
ss_data->ignore_ssr = true;
|
||
|
|
#if IS_ENABLED(CONFIG_QCOM_DS_SKIP_Q6_STOP)
|
||
|
|
adsp_set_ops_stop(rproc, false);
|
||
|
|
#endif
|
||
|
|
ret = rproc_boot(rproc);
|
||
|
|
ss_data->ignore_ssr = false;
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
pr_err("Invalid %s resume state\n", ss_data->name);
|
||
|
|
ret = -1;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
static int subsystem_resume(struct power_state_drvdata *drv, u32 state)
|
||
|
|
{
|
||
|
|
struct subsystem_data *ss_data;
|
||
|
|
int ret = 0;
|
||
|
|
struct rproc *rproc = NULL;
|
||
|
|
|
||
|
|
list_for_each_entry(ss_data, &drv->sub_sys_list, list) {
|
||
|
|
pr_info("%s subsystem resume start\n", ss_data->name);
|
||
|
|
rproc = rproc_get_by_phandle(ss_data->rproc_handle);
|
||
|
|
if (!rproc)
|
||
|
|
return -ENODEV;
|
||
|
|
|
||
|
|
ret = subsys_resume(ss_data, rproc, state);
|
||
|
|
if (ret) {
|
||
|
|
pr_err("%s subsystem resume failed\n", ss_data->name);
|
||
|
|
BUG();
|
||
|
|
}
|
||
|
|
rproc_put(rproc);
|
||
|
|
pr_info("%s subsystem resume complete\n", ss_data->name);
|
||
|
|
}
|
||
|
|
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
static int subsystem_suspend(struct power_state_drvdata *drv, u32 state)
|
||
|
|
{
|
||
|
|
struct subsystem_data *ss_data;
|
||
|
|
int ret = 0;
|
||
|
|
struct rproc *rproc = NULL;
|
||
|
|
#if IS_ENABLED(CONFIG_QCOM_DS_SKIP_Q6_STOP)
|
||
|
|
bool subsys_sleep_status;
|
||
|
|
int i;
|
||
|
|
|
||
|
|
for (i = 0; i < SUSBSYSTEM_SLEEP_CHECK_MAX_RETRIES; i++) {
|
||
|
|
subsys_sleep_status = current_subsystem_sleep();
|
||
|
|
if (subsys_sleep_status)
|
||
|
|
break;
|
||
|
|
pr_err("subsytems are not in sleep retrying.....\n");
|
||
|
|
msleep(DELAY);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (i == SUSBSYSTEM_SLEEP_CHECK_MAX_RETRIES) {
|
||
|
|
pr_err("Error: still subsystems are not in sleep\n");
|
||
|
|
BUG();
|
||
|
|
}
|
||
|
|
#endif
|
||
|
|
|
||
|
|
list_for_each_entry(ss_data, &drv->sub_sys_list, list) {
|
||
|
|
pr_info("%s subsystem suspend start\n", ss_data->name);
|
||
|
|
rproc = rproc_get_by_phandle(ss_data->rproc_handle);
|
||
|
|
if (!rproc)
|
||
|
|
return -ENODEV;
|
||
|
|
|
||
|
|
ret = subsys_suspend(ss_data, rproc, state);
|
||
|
|
if (ret) {
|
||
|
|
pr_err("%s subsystem suspend failed\n", ss_data->name);
|
||
|
|
BUG();
|
||
|
|
}
|
||
|
|
rproc_put(rproc);
|
||
|
|
pr_info("%s subsystem suspend complete\n", ss_data->name);
|
||
|
|
}
|
||
|
|
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
static int ps_open(struct inode *inode, struct file *file)
|
||
|
|
{
|
||
|
|
struct power_state_drvdata *drv = NULL;
|
||
|
|
|
||
|
|
if (!inode || !inode->i_cdev || !file)
|
||
|
|
return -EINVAL;
|
||
|
|
|
||
|
|
drv = container_of(inode->i_cdev, struct power_state_drvdata, ps_cdev);
|
||
|
|
file->private_data = drv;
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
#if IS_ENABLED(CONFIG_MSM_RPM_SMD)
|
||
|
|
static int send_deep_sleep_vote(int state, struct power_state_drvdata *drv)
|
||
|
|
{
|
||
|
|
u32 val;
|
||
|
|
|
||
|
|
if (state == DS_ENTRY)
|
||
|
|
val = RPM_XO_DS_ENTER_VALUE;
|
||
|
|
else
|
||
|
|
val = RPM_XO_DS_EXIT_VALUE;
|
||
|
|
|
||
|
|
drv->kvp_req.key = RPM_XO_DS_KEY;
|
||
|
|
drv->kvp_req.data = (void *)&val;
|
||
|
|
drv->kvp_req.length = sizeof(val);
|
||
|
|
|
||
|
|
return msm_rpm_send_message(MSM_RPM_CTX_SLEEP_SET, RPM_XO_DS_REQ,
|
||
|
|
RPM_XO_DS_ID, &drv->kvp_req, 1);
|
||
|
|
}
|
||
|
|
#elif IS_ENABLED(CONFIG_NOTIFY_AOP)
|
||
|
|
static int send_deep_sleep_vote(int state, struct power_state_drvdata *drv)
|
||
|
|
{
|
||
|
|
char buf[MAX_QMP_MSG_SIZE] = {};
|
||
|
|
|
||
|
|
if (state == DS_ENTRY)
|
||
|
|
scnprintf(buf, sizeof(buf), "{class: deep_sleep, res: 1}");
|
||
|
|
else
|
||
|
|
scnprintf(buf, sizeof(buf), "{class: deep_sleep, res: 0}");
|
||
|
|
|
||
|
|
return qmp_send(drv->qmp, buf, sizeof(buf));
|
||
|
|
}
|
||
|
|
#else
|
||
|
|
static int send_deep_sleep_vote(int state, struct power_state_drvdata *drv)
|
||
|
|
{
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
#endif
|
||
|
|
|
||
|
|
static long ps_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
|
||
|
|
{
|
||
|
|
struct power_state_drvdata *drv = file->private_data;
|
||
|
|
int ret = 0;
|
||
|
|
|
||
|
|
switch (cmd) {
|
||
|
|
case LPM_ACTIVE:
|
||
|
|
case POWER_STATE_LPM_ACTIVE:
|
||
|
|
pr_debug("State changed to Active\n");
|
||
|
|
if (pm_suspend_via_firmware()) {
|
||
|
|
pm_suspend_clear_flags();
|
||
|
|
__pm_relax(drv->ps_ws);
|
||
|
|
}
|
||
|
|
drv->current_state = ACTIVE;
|
||
|
|
pr_info("low power mode exit complete\n");
|
||
|
|
break;
|
||
|
|
|
||
|
|
case ENTER_DEEPSLEEP:
|
||
|
|
case POWER_STATE_ENTER_DEEPSLEEP:
|
||
|
|
pr_debug("Enter Deep Sleep\n");
|
||
|
|
ret = subsystem_suspend(drv, SUBSYS_DEEPSLEEP);
|
||
|
|
drv->current_state = DEEPSLEEP;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case ENTER_HIBERNATE:
|
||
|
|
case POWER_STATE_ENTER_HIBERNATE:
|
||
|
|
pr_debug("Enter Hibernate\n");
|
||
|
|
ret = subsystem_suspend(drv, SUBSYS_HIBERNATE);
|
||
|
|
drv->current_state = HIBERNATE;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case EXIT_DEEPSLEEP_STATE:
|
||
|
|
case POWER_STATE_EXIT_DEEPSLEEP_STATE:
|
||
|
|
pr_debug("Exit Deep Sleep\n");
|
||
|
|
ret = subsystem_resume(drv, SUBSYS_DEEPSLEEP);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case EXIT_HIBERNATE_STATE:
|
||
|
|
case POWER_STATE_EXIT_HIBERNATE_STATE:
|
||
|
|
pr_debug("Exit Hibernate\n");
|
||
|
|
ret = subsystem_resume(drv, SUBSYS_HIBERNATE);
|
||
|
|
break;
|
||
|
|
|
||
|
|
case MODEM_SUSPEND:
|
||
|
|
case MODEM_EXIT:
|
||
|
|
case POWER_STATE_MODEM_SUSPEND:
|
||
|
|
case POWER_STATE_MODEM_EXIT:
|
||
|
|
case ADSP_SUSPEND:
|
||
|
|
case ADSP_EXIT:
|
||
|
|
case POWER_STATE_ADSP_SUSPEND:
|
||
|
|
case POWER_STATE_ADSP_EXIT:
|
||
|
|
pr_debug("Deprecated ioctl\n");
|
||
|
|
break;
|
||
|
|
|
||
|
|
default:
|
||
|
|
ret = -ENOIOCTLCMD;
|
||
|
|
pr_err("%s: Default\n", __func__);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
static long compat_ps_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
|
||
|
|
{
|
||
|
|
unsigned int nr = _IOC_NR(cmd);
|
||
|
|
|
||
|
|
return (long)ps_ioctl(file, nr, arg);
|
||
|
|
}
|
||
|
|
|
||
|
|
static const struct file_operations ps_fops = {
|
||
|
|
.owner = THIS_MODULE,
|
||
|
|
.open = ps_open,
|
||
|
|
.unlocked_ioctl = ps_ioctl,
|
||
|
|
.compat_ioctl = compat_ps_ioctl,
|
||
|
|
};
|
||
|
|
|
||
|
|
static int send_uevent(struct power_state_drvdata *drv, enum ps_event_type event)
|
||
|
|
{
|
||
|
|
char event_string[STRING_LEN];
|
||
|
|
char *envp[2] = { event_string, NULL };
|
||
|
|
|
||
|
|
scnprintf(event_string, ARRAY_SIZE(event_string), "POWER_STATE_EVENT = %d", event);
|
||
|
|
return kobject_uevent_env(&drv->ps_dev->kobj, KOBJ_CHANGE, envp);
|
||
|
|
}
|
||
|
|
|
||
|
|
static int ps_ssr_cb(struct notifier_block *nb, unsigned long opcode, void *data)
|
||
|
|
{
|
||
|
|
struct qcom_ssr_notify_data *notify_data = data;
|
||
|
|
struct subsystem_data *ss_data;
|
||
|
|
bool ss_present = false;
|
||
|
|
|
||
|
|
list_for_each_entry(ss_data, &drv->sub_sys_list, list) {
|
||
|
|
if (!strcmp(ss_data->name, notify_data->name)) {
|
||
|
|
ss_present = true;
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!ss_present || ss_data->ignore_ssr)
|
||
|
|
return NOTIFY_DONE;
|
||
|
|
|
||
|
|
switch (opcode) {
|
||
|
|
case QCOM_SSR_BEFORE_SHUTDOWN:
|
||
|
|
pr_debug("%s is shutdown\n", notify_data->name);
|
||
|
|
send_uevent(drv, ss_data->enter);
|
||
|
|
break;
|
||
|
|
case QCOM_SSR_AFTER_POWERUP:
|
||
|
|
pr_debug("%s: is powered up\n", notify_data->name);
|
||
|
|
send_uevent(drv, ss_data->exit);
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
pr_debug("%s: ignore ssr event\n", notify_data->name);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
return NOTIFY_DONE;
|
||
|
|
}
|
||
|
|
|
||
|
|
static int ps_pm_cb(struct notifier_block *nb, unsigned long event, void *unused)
|
||
|
|
{
|
||
|
|
struct power_state_drvdata *drv = container_of(nb, struct power_state_drvdata, ps_pm_nb);
|
||
|
|
int ret;
|
||
|
|
|
||
|
|
switch (event) {
|
||
|
|
case PM_SUSPEND_PREPARE:
|
||
|
|
if (drv->current_state == DEEPSLEEP) {
|
||
|
|
pr_info("Deep Sleep entry\n");
|
||
|
|
ret = send_deep_sleep_vote(DS_ENTRY, drv);
|
||
|
|
if (ret)
|
||
|
|
return NOTIFY_BAD;
|
||
|
|
pm_set_suspend_via_firmware();
|
||
|
|
} else {
|
||
|
|
pr_debug("RBSC Suspend\n");
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case PM_POST_SUSPEND:
|
||
|
|
if (pm_suspend_via_firmware()) {
|
||
|
|
pr_info("Deep Sleep exit\n");
|
||
|
|
|
||
|
|
ret = send_deep_sleep_vote(DS_EXIT, drv);
|
||
|
|
if (ret)
|
||
|
|
BUG_ON(1);
|
||
|
|
__pm_stay_awake(drv->ps_ws);
|
||
|
|
send_uevent(drv, EXIT_DEEP_SLEEP);
|
||
|
|
} else {
|
||
|
|
pr_debug("RBSC Resume\n");
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case PM_HIBERNATION_PREPARE:
|
||
|
|
pr_info("Hibernate entry\n");
|
||
|
|
|
||
|
|
send_uevent(drv, PREPARE_FOR_HIBERNATION);
|
||
|
|
drv->current_state = HIBERNATE;
|
||
|
|
break;
|
||
|
|
|
||
|
|
case PM_RESTORE_PREPARE:
|
||
|
|
pr_debug("Hibernate prepare\n");
|
||
|
|
break;
|
||
|
|
|
||
|
|
case PM_POST_HIBERNATION:
|
||
|
|
case PM_POST_RESTORE:
|
||
|
|
pr_info("Hibernate exit\n");
|
||
|
|
send_uevent(drv, EXIT_HIBERNATE);
|
||
|
|
break;
|
||
|
|
|
||
|
|
default:
|
||
|
|
WARN_ONCE(1, "Default case: PM Notifier\n");
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
|
||
|
|
return NOTIFY_DONE;
|
||
|
|
}
|
||
|
|
|
||
|
|
static void power_state_resume(void)
|
||
|
|
{
|
||
|
|
struct arm_smccc_res res;
|
||
|
|
|
||
|
|
if (pm_suspend_via_firmware()) {
|
||
|
|
arm_smccc_smc(DS_ENTRY_SMC_ID, DS_NUM_PARAMETERS, DS_EXIT, 0, 0, 0, 0, 0, &res);
|
||
|
|
drv->deep_sleep_cnt += 1;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static int power_state_suspend(void)
|
||
|
|
{
|
||
|
|
struct arm_smccc_res res;
|
||
|
|
|
||
|
|
if (pm_suspend_via_firmware()) {
|
||
|
|
arm_smccc_smc(DS_ENTRY_SMC_ID, DS_NUM_PARAMETERS, DS_ENTRY, 0, 0, 0, 0, 0, &res);
|
||
|
|
if (res.a0)
|
||
|
|
return res.a0;
|
||
|
|
}
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
static ssize_t deep_sleep_allowed_show(struct kobject *kobj, struct kobj_attribute *attr,
|
||
|
|
char *buf)
|
||
|
|
{
|
||
|
|
struct power_state_drvdata *drv =
|
||
|
|
container_of(attr, struct power_state_drvdata, ds_allow_ka);
|
||
|
|
|
||
|
|
return scnprintf(buf, PAGE_SIZE, "%u\n", drv->deep_sleep_allowed);
|
||
|
|
}
|
||
|
|
|
||
|
|
static ssize_t deep_sleep_allowed_store(struct kobject *kobj, struct kobj_attribute *attr,
|
||
|
|
const char *buf, size_t count)
|
||
|
|
{
|
||
|
|
struct power_state_drvdata *drv =
|
||
|
|
container_of(attr, struct power_state_drvdata, ds_allow_ka);
|
||
|
|
int val;
|
||
|
|
int ret;
|
||
|
|
|
||
|
|
ret = kstrtoint(buf, 10, &val);
|
||
|
|
if (ret) {
|
||
|
|
pr_err("Invalid argument passed\n");
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
drv->deep_sleep_allowed = val;
|
||
|
|
|
||
|
|
return count;
|
||
|
|
}
|
||
|
|
|
||
|
|
static ssize_t deep_sleep_cnt_show(struct kobject *kobj, struct kobj_attribute *attr,
|
||
|
|
char *buf)
|
||
|
|
{
|
||
|
|
struct power_state_drvdata *drv = container_of(attr, struct power_state_drvdata, ds_ka);
|
||
|
|
|
||
|
|
return scnprintf(buf, PAGE_SIZE, "%u\n", drv->deep_sleep_cnt);
|
||
|
|
}
|
||
|
|
|
||
|
|
static ssize_t deep_sleep_cnt_store(struct kobject *kobj, struct kobj_attribute *attr,
|
||
|
|
const char *buf, size_t count)
|
||
|
|
{
|
||
|
|
struct power_state_drvdata *drv = container_of(attr, struct power_state_drvdata, ds_ka);
|
||
|
|
int val;
|
||
|
|
int ret;
|
||
|
|
|
||
|
|
ret = kstrtoint(buf, 10, &val);
|
||
|
|
if (ret) {
|
||
|
|
pr_err("Invalid argument passed\n");
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
drv->deep_sleep_cnt = val;
|
||
|
|
|
||
|
|
return count;
|
||
|
|
}
|
||
|
|
|
||
|
|
static ssize_t state_show(struct kobject *kobj, struct kobj_attribute *attr, char *buf)
|
||
|
|
{
|
||
|
|
struct power_state_drvdata *drv = container_of(attr, struct power_state_drvdata, ps_ka);
|
||
|
|
int len = strlen(power_state[drv->current_state]);
|
||
|
|
|
||
|
|
return scnprintf(buf, len + 2, "%s\n", power_state[drv->current_state]);
|
||
|
|
}
|
||
|
|
|
||
|
|
static int power_state_dev_init(struct power_state_drvdata *drv)
|
||
|
|
{
|
||
|
|
int ret;
|
||
|
|
|
||
|
|
ret = alloc_chrdev_region(&drv->ps_dev_no, POWER_STATS_BASEMINOR,
|
||
|
|
POWER_STATS_MAX_MINOR, POWER_STATE_DEVICE_NAME);
|
||
|
|
if (ret)
|
||
|
|
return ret;
|
||
|
|
|
||
|
|
cdev_init(&drv->ps_cdev, &ps_fops);
|
||
|
|
ret = cdev_add(&drv->ps_cdev, drv->ps_dev_no, 1);
|
||
|
|
if (ret) {
|
||
|
|
unregister_chrdev_region(drv->ps_dev_no, 1);
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
drv->ps_class = class_create(THIS_MODULE, POWER_STATE_DEVICE_NAME);
|
||
|
|
if (IS_ERR_OR_NULL(drv->ps_class)) {
|
||
|
|
cdev_del(&drv->ps_cdev);
|
||
|
|
unregister_chrdev_region(drv->ps_dev_no, 1);
|
||
|
|
return PTR_ERR(drv->ps_class);
|
||
|
|
}
|
||
|
|
|
||
|
|
drv->ps_dev = device_create(drv->ps_class, NULL,
|
||
|
|
drv->ps_dev_no, NULL, POWER_STATE_DEVICE_NAME);
|
||
|
|
if (IS_ERR_OR_NULL(drv->ps_dev)) {
|
||
|
|
class_destroy(drv->ps_class);
|
||
|
|
cdev_del(&drv->ps_cdev);
|
||
|
|
unregister_chrdev_region(drv->ps_dev_no, 1);
|
||
|
|
return PTR_ERR(drv->ps_dev);
|
||
|
|
}
|
||
|
|
|
||
|
|
drv->ps_kobj = kobject_create_and_add(POWER_STATE_DEVICE_NAME, kernel_kobj);
|
||
|
|
if (!drv->ps_kobj) {
|
||
|
|
ret = -ENOMEM;
|
||
|
|
device_destroy(drv->ps_class, drv->ps_dev_no);
|
||
|
|
class_destroy(drv->ps_class);
|
||
|
|
cdev_del(&drv->ps_cdev);
|
||
|
|
unregister_chrdev_region(drv->ps_dev_no, 1);
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
sysfs_attr_init(&drv->ps_ka.attr);
|
||
|
|
drv->ps_ka.attr.mode = 0444;
|
||
|
|
drv->ps_ka.attr.name = "state";
|
||
|
|
drv->ps_ka.show = state_show;
|
||
|
|
|
||
|
|
ret = sysfs_create_file(drv->ps_kobj, &drv->ps_ka.attr);
|
||
|
|
if (ret)
|
||
|
|
goto exit;
|
||
|
|
|
||
|
|
sysfs_attr_init(&drv->ds_ka.attr);
|
||
|
|
drv->ds_ka.attr.mode = 0644;
|
||
|
|
drv->ds_ka.attr.name = "deep_sleep_cnt";
|
||
|
|
drv->ds_ka.show = deep_sleep_cnt_show;
|
||
|
|
drv->ds_ka.store = deep_sleep_cnt_store;
|
||
|
|
|
||
|
|
ret = sysfs_create_file(drv->ps_kobj, &drv->ds_ka.attr);
|
||
|
|
if (ret) {
|
||
|
|
sysfs_remove_file(drv->ps_kobj, &drv->ps_ka.attr);
|
||
|
|
goto exit;
|
||
|
|
}
|
||
|
|
|
||
|
|
sysfs_attr_init(&drv->ds_allow_ka.attr);
|
||
|
|
drv->ds_allow_ka.attr.mode = 0644;
|
||
|
|
drv->ds_allow_ka.attr.name = "deep_sleep_allowed";
|
||
|
|
drv->ds_allow_ka.show = deep_sleep_allowed_show;
|
||
|
|
drv->ds_allow_ka.store = deep_sleep_allowed_store;
|
||
|
|
|
||
|
|
ret = sysfs_create_file(drv->ps_kobj, &drv->ds_allow_ka.attr);
|
||
|
|
if (ret) {
|
||
|
|
sysfs_remove_file(drv->ps_kobj, &drv->ps_ka.attr);
|
||
|
|
sysfs_remove_file(drv->ps_kobj, &drv->ds_ka.attr);
|
||
|
|
goto exit;
|
||
|
|
}
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
|
||
|
|
exit:
|
||
|
|
kobject_put(drv->ps_kobj);
|
||
|
|
device_destroy(drv->ps_class, drv->ps_dev_no);
|
||
|
|
class_destroy(drv->ps_class);
|
||
|
|
cdev_del(&drv->ps_cdev);
|
||
|
|
unregister_chrdev_region(drv->ps_dev_no, 1);
|
||
|
|
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
static int power_state_probe(struct platform_device *pdev)
|
||
|
|
{
|
||
|
|
struct device_node *dn = pdev->dev.of_node;
|
||
|
|
struct subsystem_data *ss_data;
|
||
|
|
int ret, i, j;
|
||
|
|
const char *name;
|
||
|
|
phandle rproc_handle;
|
||
|
|
|
||
|
|
drv = devm_kzalloc(&pdev->dev, sizeof(*drv), GFP_KERNEL);
|
||
|
|
if (!drv)
|
||
|
|
return -ENOMEM;
|
||
|
|
|
||
|
|
if (IS_ENABLED(CONFIG_NOTIFY_AOP)) {
|
||
|
|
drv->qmp = qmp_get(&pdev->dev);
|
||
|
|
if (IS_ERR(drv->qmp))
|
||
|
|
return -EPROBE_DEFER;
|
||
|
|
}
|
||
|
|
|
||
|
|
drv->ps_pm_nb.notifier_call = ps_pm_cb;
|
||
|
|
drv->ps_pm_nb.priority = PS_PM_NOTIFIER_PRIORITY;
|
||
|
|
ret = register_pm_notifier(&drv->ps_pm_nb);
|
||
|
|
if (ret)
|
||
|
|
return ret;
|
||
|
|
|
||
|
|
drv->ps_ws = wakeup_source_register(&pdev->dev, POWER_STATE_DEVICE_NAME);
|
||
|
|
if (!drv->ps_ws)
|
||
|
|
goto remove_pm_notifier;
|
||
|
|
|
||
|
|
INIT_LIST_HEAD(&drv->sub_sys_list);
|
||
|
|
|
||
|
|
drv->subsys_count = of_property_count_strings(dn, "qcom,subsys-name");
|
||
|
|
if (drv->subsys_count < 0)
|
||
|
|
drv->subsys_count = 0;
|
||
|
|
for (i = 0; i < drv->subsys_count; i++) {
|
||
|
|
of_property_read_string_index(dn, "qcom,subsys-name", i, &name);
|
||
|
|
|
||
|
|
ret = of_property_read_u32_index(dn, "qcom,rproc-handle", i, &rproc_handle);
|
||
|
|
if (ret)
|
||
|
|
goto remove_ss;
|
||
|
|
|
||
|
|
ss_data = devm_kzalloc(&pdev->dev, sizeof(struct subsystem_data), GFP_KERNEL);
|
||
|
|
if (!ss_data) {
|
||
|
|
ret = -ENOMEM;
|
||
|
|
goto remove_ss;
|
||
|
|
}
|
||
|
|
|
||
|
|
ss_data->name = kstrdup_const(name, GFP_KERNEL);
|
||
|
|
ss_data->rproc_handle = rproc_handle;
|
||
|
|
|
||
|
|
ss_data->ps_ssr_nb.notifier_call = ps_ssr_cb;
|
||
|
|
ss_data->ps_ssr_nb.priority = PS_SSR_NOTIFIER_PRIORITY;
|
||
|
|
ss_data->ssr_handle = qcom_register_ssr_notifier(name, &ss_data->ps_ssr_nb);
|
||
|
|
ss_data->ignore_ssr = false;
|
||
|
|
if (IS_ERR(ss_data->ssr_handle)) {
|
||
|
|
ret = PTR_ERR(ss_data->ssr_handle);
|
||
|
|
goto remove_ss;
|
||
|
|
}
|
||
|
|
|
||
|
|
for (j = 0; j < ARRAY_SIZE(event_data); j++) {
|
||
|
|
if (!strcmp(event_data[j].name, name)) {
|
||
|
|
ss_data->enter = event_data[j].enter;
|
||
|
|
ss_data->exit = event_data[j].exit;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (!ss_data->enter) {
|
||
|
|
ret = -ENODEV;
|
||
|
|
goto remove_ss;
|
||
|
|
}
|
||
|
|
|
||
|
|
list_add_tail(&ss_data->list, &drv->sub_sys_list);
|
||
|
|
}
|
||
|
|
|
||
|
|
ret = power_state_dev_init(drv);
|
||
|
|
if (ret)
|
||
|
|
goto remove_ss;
|
||
|
|
|
||
|
|
drv->ps_ops.suspend = power_state_suspend;
|
||
|
|
drv->ps_ops.resume = power_state_resume;
|
||
|
|
register_syscore_ops(&drv->ps_ops);
|
||
|
|
|
||
|
|
dev_set_drvdata(&pdev->dev, drv);
|
||
|
|
return ret;
|
||
|
|
|
||
|
|
remove_ss:
|
||
|
|
list_for_each_entry(ss_data, &drv->sub_sys_list, list) {
|
||
|
|
qcom_unregister_ssr_notifier(ss_data->ssr_handle, &ss_data->ps_ssr_nb);
|
||
|
|
list_del(&ss_data->list);
|
||
|
|
}
|
||
|
|
INIT_LIST_HEAD(&drv->sub_sys_list);
|
||
|
|
wakeup_source_unregister(drv->ps_ws);
|
||
|
|
remove_pm_notifier:
|
||
|
|
unregister_pm_notifier(&drv->ps_pm_nb);
|
||
|
|
return ret;
|
||
|
|
}
|
||
|
|
|
||
|
|
static int power_state_remove(struct platform_device *pdev)
|
||
|
|
{
|
||
|
|
struct power_state_drvdata *drv = dev_get_drvdata(&pdev->dev);
|
||
|
|
struct subsystem_data *ss_data;
|
||
|
|
|
||
|
|
unregister_syscore_ops(&drv->ps_ops);
|
||
|
|
if (IS_ENABLED(CONFIG_NOTIFY_AOP))
|
||
|
|
qmp_put(drv->qmp);
|
||
|
|
|
||
|
|
list_for_each_entry(ss_data, &drv->sub_sys_list, list) {
|
||
|
|
qcom_unregister_ssr_notifier(ss_data->ssr_handle, &ss_data->ps_ssr_nb);
|
||
|
|
list_del(&ss_data->list);
|
||
|
|
}
|
||
|
|
|
||
|
|
INIT_LIST_HEAD(&drv->sub_sys_list);
|
||
|
|
wakeup_source_unregister(drv->ps_ws);
|
||
|
|
sysfs_remove_file(drv->ps_kobj, &drv->ds_ka.attr);
|
||
|
|
sysfs_remove_file(drv->ps_kobj, &drv->ps_ka.attr);
|
||
|
|
kobject_put(drv->ps_kobj);
|
||
|
|
device_destroy(drv->ps_class, drv->ps_dev_no);
|
||
|
|
class_destroy(drv->ps_class);
|
||
|
|
cdev_del(&drv->ps_cdev);
|
||
|
|
unregister_chrdev_region(drv->ps_dev_no, 1);
|
||
|
|
unregister_pm_notifier(&drv->ps_pm_nb);
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
static const struct of_device_id power_state_of_match[] = {
|
||
|
|
{ .compatible = "qcom,power-state", },
|
||
|
|
{ }
|
||
|
|
};
|
||
|
|
MODULE_DEVICE_TABLE(of, power_state_of_match);
|
||
|
|
|
||
|
|
static struct platform_driver power_state_driver = {
|
||
|
|
.probe = power_state_probe,
|
||
|
|
.remove = power_state_remove,
|
||
|
|
.driver = {
|
||
|
|
.name = "power-state",
|
||
|
|
.of_match_table = power_state_of_match,
|
||
|
|
.suppress_bind_attrs = true,
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
module_platform_driver(power_state_driver);
|
||
|
|
MODULE_DESCRIPTION("Qualcomm Technologies, Inc. (QTI) Power State Driver");
|
||
|
|
MODULE_LICENSE("GPL v2");
|