452 lines
12 KiB
C
Executable File
452 lines
12 KiB
C
Executable File
/*
|
|
* Copyright (c) 2020, The Linux Foundation. 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 version 2 and
|
|
* only version 2 as published by the Free Software Foundation.
|
|
*
|
|
* 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/init.h>
|
|
#include <linux/module.h>
|
|
#include <linux/sec_class.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/of.h>
|
|
#include <linux/of_device.h>
|
|
#include <linux/irq.h>
|
|
#include <linux/of_gpio.h>
|
|
#include <linux/gpio.h>
|
|
#include <linux/device.h>
|
|
#include <linux/proc_fs.h>
|
|
#include <linux/sec_debug.h>
|
|
#include <linux/seq_file.h>
|
|
#include <linux/mfd/sec_ap_pmic.h>
|
|
#include <trace/events/power.h>
|
|
#include <linux/suspend.h>
|
|
#if IS_ENABLED(CONFIG_SEC_PM_LOG)
|
|
#include <linux/sec_pm_log.h>
|
|
#endif
|
|
|
|
#define WS_LOG_PERIOD 5
|
|
#define MAX_WAKE_SOURCES_LEN 256
|
|
|
|
static struct device *sec_ap_pmic_dev;
|
|
static struct sec_ap_pmic_info *sec_ap_pmic_data;
|
|
|
|
extern void pm_get_active_wakeup_sources(char *pending_wakeup_source, size_t max);
|
|
|
|
static void wake_sources_print_acquired(void)
|
|
{
|
|
char wake_sources_acquired[MAX_WAKE_SOURCES_LEN];
|
|
|
|
pm_get_active_wakeup_sources(wake_sources_acquired, MAX_WAKE_SOURCES_LEN);
|
|
pr_info("PM: %s\n", wake_sources_acquired);
|
|
}
|
|
|
|
static void wake_sources_print_acquired_work(struct work_struct *work)
|
|
{
|
|
struct sec_ap_pmic_info *info = container_of(to_delayed_work(work),
|
|
struct sec_ap_pmic_info, ws_work);
|
|
|
|
wake_sources_print_acquired();
|
|
schedule_delayed_work(&info->ws_work, info->ws_log_period * HZ);
|
|
}
|
|
|
|
static ssize_t manual_reset_show(struct device *in_dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
int ret = sec_get_s2_reset(SEC_PON_KPDPWR_RESIN);
|
|
|
|
pr_info("%s: ret=%d\n", __func__, ret);
|
|
return sprintf(buf, "%d\n", !ret);
|
|
}
|
|
|
|
static ssize_t manual_reset_store(struct device *in_dev,
|
|
struct device_attribute *attr, const char *buf, size_t len)
|
|
{
|
|
int onoff = 0;
|
|
|
|
if (kstrtoint(buf, 10, &onoff))
|
|
return -EINVAL;
|
|
|
|
pr_info("%s: onoff=%d\n", __func__, onoff);
|
|
#if IS_ENABLED(CONFIG_SEC_CRASHKEY_LONG)
|
|
if (onoff)
|
|
sec_crashkey_long_connect_to_input_evnet();
|
|
else
|
|
sec_crashkey_long_disconnect_from_input_event();
|
|
#endif
|
|
|
|
return len;
|
|
}
|
|
static DEVICE_ATTR_RW(manual_reset);
|
|
|
|
static ssize_t wake_enabled_show(struct device *in_dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
int en = (sec_get_pm_key_wk_init(SEC_PON_KPDPWR) &&
|
|
sec_get_pm_key_wk_init(SEC_PON_RESIN)) ? 1 : 0;
|
|
|
|
return sprintf(buf, "%d\n", en);
|
|
}
|
|
|
|
static ssize_t wake_enabled_store(struct device *in_dev,
|
|
struct device_attribute *attr, const char *buf, size_t len)
|
|
{
|
|
int onoff;
|
|
int ret;
|
|
|
|
if (kstrtoint(buf, 10, &onoff) < 0)
|
|
return -EINVAL;
|
|
|
|
pr_info("%s: onoff=%d\n", __func__, onoff);
|
|
|
|
ret = sec_set_pm_key_wk_init(SEC_PON_KPDPWR, onoff);
|
|
pr_info("%s: PWR ret=%d\n", __func__, ret);
|
|
|
|
ret = sec_set_pm_key_wk_init(SEC_PON_RESIN, onoff);
|
|
pr_info("%s: RESIN ret=%d\n", __func__, ret);
|
|
|
|
return len;
|
|
}
|
|
static DEVICE_ATTR_RW(wake_enabled);
|
|
|
|
#if IS_ENABLED(CONFIG_SEC_GPIO_DUMP)
|
|
static ssize_t gpio_dump_show(struct device *in_dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
return sprintf(buf, "%d\n", (gpio_dump_enabled) ? 1 : 0);
|
|
}
|
|
|
|
static ssize_t gpio_dump_store(struct device *in_dev,
|
|
struct device_attribute *attr, const char *buf, size_t len)
|
|
{
|
|
int onoff;
|
|
|
|
if (kstrtoint(buf, 10, &onoff) < 0)
|
|
return -EINVAL;
|
|
|
|
pr_info("%s: onoff=%d\n", __func__, onoff);
|
|
gpio_dump_enabled = (onoff) ? true : false;
|
|
|
|
return len;
|
|
}
|
|
static DEVICE_ATTR_RW(gpio_dump);
|
|
#endif
|
|
|
|
/* VDD/IDDQ info */
|
|
#define PARAM0_IVALID 1
|
|
#define PARAM0_LESS_THAN_0 2
|
|
|
|
#define DEFAULT_LEN_STR 1023
|
|
|
|
#define default_scnprintf(buf, offset, fmt, ...) \
|
|
do { \
|
|
offset += scnprintf(&(buf)[offset], DEFAULT_LEN_STR - (size_t)offset, \
|
|
fmt, ##__VA_ARGS__); \
|
|
} while (0)
|
|
|
|
static void check_format(char *buf, ssize_t *size, int max_len_str)
|
|
{
|
|
int i = 0, cnt = 0, pos = 0;
|
|
|
|
if (!buf || *size <= 0)
|
|
return;
|
|
|
|
if (*size >= max_len_str)
|
|
*size = max_len_str - 1;
|
|
|
|
while (i < *size && buf[i]) {
|
|
if (buf[i] == '"') {
|
|
cnt++;
|
|
pos = i;
|
|
}
|
|
|
|
if ((buf[i] < 0x20) || (buf[i] == 0x5C) || (buf[i] > 0x7E))
|
|
buf[i] = ' ';
|
|
i++;
|
|
}
|
|
|
|
if (cnt % 2) {
|
|
if (pos == *size - 1) {
|
|
buf[*size - 1] = '\0';
|
|
} else {
|
|
buf[*size - 1] = '"';
|
|
buf[*size] = '\0';
|
|
}
|
|
}
|
|
}
|
|
|
|
static int get_param0(const char *name)
|
|
{
|
|
struct device_node *np = of_find_node_by_path("/soc/sec_ap_param");
|
|
u32 val;
|
|
int ret;
|
|
|
|
if (!np) {
|
|
pr_err("No sec_avi_data found\n");
|
|
return -PARAM0_IVALID;
|
|
}
|
|
|
|
ret = of_property_read_u32(np, name, &val);
|
|
if (ret) {
|
|
pr_err("failed to get %s from node\n", name);
|
|
return -PARAM0_LESS_THAN_0;
|
|
}
|
|
|
|
return val;
|
|
}
|
|
|
|
#define GET_V(A) ((A) / 1000)
|
|
#define GET_MV(A) ((A) % 1000)
|
|
static ssize_t show_ap_info(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
ssize_t info_size = 0;
|
|
|
|
/* currently, support only for GC_OPV */
|
|
default_scnprintf(buf, info_size, "\"GC_OPV_3\":\"%d.%03d\"", GET_V(get_param0("go")), GET_MV(get_param0("go")));
|
|
default_scnprintf(buf, info_size, ",\"GC_PRM\":\"%d\"", get_param0("gi"));
|
|
default_scnprintf(buf, info_size, ",\"DOUR\":\"%d\"", get_param0("dour"));
|
|
default_scnprintf(buf, info_size, ",\"DOUB\":\"%d\"", get_param0("doub"));
|
|
|
|
check_format(buf, &info_size, DEFAULT_LEN_STR);
|
|
|
|
return info_size;
|
|
}
|
|
static DEVICE_ATTR(ap_info, 0440, show_ap_info, NULL);
|
|
|
|
#define PBGT_PHOT_TYPE 0
|
|
#define PBGT_PHOT_LVL 0
|
|
static ssize_t show_phot_info(struct device *dev,
|
|
struct device_attribute *attr, char *buf)
|
|
{
|
|
ssize_t info_size = 0;
|
|
|
|
default_scnprintf(buf, info_size, "\"TYPE\":\"%d\"", PBGT_PHOT_TYPE);
|
|
default_scnprintf(buf, info_size, ",\"COUNT\":\"%d\"",
|
|
sec_ap_pmic_data->ocpw_cnt - sec_ap_pmic_data->ocpw_cnt_reset_offset);
|
|
default_scnprintf(buf, info_size, ",\"TIME\":\"%d\"",
|
|
sec_ap_pmic_data->ocpw_time - sec_ap_pmic_data->ocpw_time_reset_offset);
|
|
default_scnprintf(buf, info_size, ",\"LEVEL\":\"%d\"", PBGT_PHOT_LVL);
|
|
|
|
check_format(buf, &info_size, DEFAULT_LEN_STR);
|
|
|
|
sec_ap_pmic_data->ocpw_time_reset_offset = sec_ap_pmic_data->ocpw_time;
|
|
sec_ap_pmic_data->ocpw_cnt_reset_offset = sec_ap_pmic_data->ocpw_cnt;
|
|
|
|
return info_size;
|
|
}
|
|
static DEVICE_ATTR(phot_info, 0440, show_phot_info, NULL);
|
|
|
|
static struct attribute *sec_ap_pmic_attributes[] = {
|
|
#if IS_ENABLED(CONFIG_SEC_GPIO_DUMP)
|
|
&dev_attr_gpio_dump.attr,
|
|
#endif
|
|
&dev_attr_phot_info.attr,
|
|
&dev_attr_ap_info.attr,
|
|
&dev_attr_manual_reset.attr,
|
|
&dev_attr_wake_enabled.attr,
|
|
NULL,
|
|
};
|
|
|
|
static struct attribute_group sec_ap_pmic_attr_group = {
|
|
.attrs = sec_ap_pmic_attributes,
|
|
};
|
|
|
|
#if IS_ENABLED(CONFIG_SEC_GPIO_DUMP)
|
|
static void gpio_state_debug_suspend_trace_probe(void *unused,
|
|
const char *action, int val, bool start)
|
|
{
|
|
/* SUSPEND: start(1), val(1), action(machine_suspend) */
|
|
if (gpio_dump_enabled && start && val > 0 && !strcmp("machine_suspend", action)) {
|
|
sec_ap_gpio_debug_print();
|
|
sec_pmic_gpio_debug_print();
|
|
}
|
|
}
|
|
#endif
|
|
|
|
static irqreturn_t ocp_warn_irq_thread(int irq, void *irq_data)
|
|
{
|
|
struct sec_ap_pmic_info *info = irq_data;
|
|
int warn_state = gpio_get_value(info->ocp_warn_gpio);
|
|
|
|
#if IS_ENABLED(CONFIG_SEC_PM_LOG)
|
|
if (warn_state == 1) {
|
|
info->ocpw_start_time = ktime_get();
|
|
ss_thermal_print("ocp_warn: %d, %d\n", warn_state, ++(info->ocpw_cnt));
|
|
} else if (info->ocpw_start_time) {
|
|
info->ocpw_time += ktime_to_ms(ktime_get() - info->ocpw_start_time);
|
|
ss_thermal_print("ocp_warn: %d, accu(%d ms)\n", warn_state, info->ocpw_time);
|
|
info->ocpw_start_time = 0;
|
|
}
|
|
#endif
|
|
|
|
return IRQ_HANDLED;
|
|
}
|
|
|
|
static int suspend_resume_pm_event(struct notifier_block *notifier,
|
|
unsigned long pm_event, void *unused)
|
|
{
|
|
struct sec_ap_pmic_info *info = container_of(notifier,
|
|
struct sec_ap_pmic_info, sec_pm_debug_nb);
|
|
|
|
switch (pm_event) {
|
|
case PM_SUSPEND_PREPARE:
|
|
cancel_delayed_work_sync(&info->ws_work);
|
|
break;
|
|
case PM_POST_SUSPEND:
|
|
schedule_delayed_work(&info->ws_work, info->ws_log_period * HZ);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return NOTIFY_DONE;
|
|
}
|
|
|
|
static int sec_ap_pmic_probe(struct platform_device *pdev)
|
|
{
|
|
struct device_node *node = pdev->dev.of_node;
|
|
struct sec_ap_pmic_info *info;
|
|
int err;
|
|
|
|
if (!node) {
|
|
dev_err(&pdev->dev, "device-tree data is missing\n");
|
|
return -ENXIO;
|
|
}
|
|
|
|
info = devm_kzalloc(&pdev->dev, sizeof(*info), GFP_KERNEL);
|
|
if (!info) {
|
|
dev_err(&pdev->dev, "%s: Fail to alloc info\n", __func__);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
info->ocp_warn_gpio = of_get_named_gpio(node, "sec_pm_debug,ocp_warn_irq", 0);
|
|
if (info->ocp_warn_gpio < 0) {
|
|
pr_err("%s: Error reading irq from dt = %d\n", __func__, info->ocp_warn_gpio);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
info->ocp_warn_irq = gpio_to_irq(info->ocp_warn_gpio);
|
|
if (info->ocp_warn_irq > 0) {
|
|
err = request_threaded_irq(info->ocp_warn_irq,
|
|
NULL, ocp_warn_irq_thread,
|
|
IRQF_TRIGGER_RISING | IRQF_TRIGGER_FALLING | IRQF_ONESHOT,
|
|
"ocp-warn-irq", info);
|
|
if (err) {
|
|
pr_info("%s: Failed to request ocp_warn_irq: %d\n", __func__, err);
|
|
goto err_device_create;
|
|
}
|
|
} else {
|
|
pr_info("%s: Failed to gpio_to_irq: %d\n", __func__, info->ocp_warn_irq);
|
|
err = -1;
|
|
goto err_device_create;
|
|
}
|
|
|
|
info->ocpw_cnt = 0;
|
|
|
|
platform_set_drvdata(pdev, info);
|
|
info->dev = &pdev->dev;
|
|
sec_ap_pmic_data = info;
|
|
|
|
#if IS_ENABLED(CONFIG_SEC_CLASS)
|
|
sec_ap_pmic_dev = sec_device_create(NULL, "ap_pmic");
|
|
|
|
if (unlikely(IS_ERR(sec_ap_pmic_dev))) {
|
|
pr_err("%s: Failed to create ap_pmic device\n", __func__);
|
|
err = PTR_ERR(sec_ap_pmic_dev);
|
|
goto err_device_create;
|
|
}
|
|
|
|
err = sysfs_create_group(&sec_ap_pmic_dev->kobj,
|
|
&sec_ap_pmic_attr_group);
|
|
if (err < 0) {
|
|
pr_err("%s: Failed to create sysfs group\n", __func__);
|
|
goto err_device_create;
|
|
}
|
|
#endif
|
|
|
|
#if IS_ENABLED(CONFIG_SEC_GPIO_DUMP)
|
|
/* Register callback for cheking subsystem stats */
|
|
err = register_trace_suspend_resume(
|
|
gpio_state_debug_suspend_trace_probe, NULL);
|
|
if (err) {
|
|
pr_err("%s: Failed to register suspend trace callback, ret=%d\n",
|
|
__func__, err);
|
|
}
|
|
#endif
|
|
|
|
/* Set to default logging period (5s) */
|
|
info->ws_log_period = WS_LOG_PERIOD;
|
|
|
|
/* Register PM notifier */
|
|
info->sec_pm_debug_nb.notifier_call = suspend_resume_pm_event;
|
|
err = register_pm_notifier(&info->sec_pm_debug_nb);
|
|
if (err) {
|
|
dev_err(info->dev, "%s: failed to register PM notifier(%d)\n",
|
|
__func__, err);
|
|
return err;
|
|
}
|
|
|
|
INIT_DELAYED_WORK(&info->ws_work, wake_sources_print_acquired_work);
|
|
schedule_delayed_work(&info->ws_work, info->ws_log_period * HZ);
|
|
|
|
pr_info("%s: ap_pmic successfully inited.\n", __func__);
|
|
|
|
return 0;
|
|
|
|
#if IS_ENABLED(CONFIG_SEC_CLASS)
|
|
err_device_create:
|
|
sec_device_destroy(sec_ap_pmic_dev->devt);
|
|
return err;
|
|
#endif
|
|
}
|
|
|
|
static int sec_ap_pmic_remove(struct platform_device *pdev)
|
|
{
|
|
#if IS_ENABLED(CONFIG_SEC_GPIO_DUMP)
|
|
int ret;
|
|
|
|
ret = unregister_trace_suspend_resume(
|
|
gpio_state_debug_suspend_trace_probe, NULL);
|
|
#endif
|
|
|
|
#if IS_ENABLED(CONFIG_SEC_CLASS)
|
|
if (sec_ap_pmic_dev) {
|
|
sec_device_destroy(sec_ap_pmic_dev->devt);
|
|
}
|
|
#endif
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct of_device_id sec_ap_pmic_match_table[] = {
|
|
{ .compatible = "samsung,sec-ap-pmic" },
|
|
{}
|
|
};
|
|
|
|
static struct platform_driver sec_ap_pmic_driver = {
|
|
.driver = {
|
|
.name = "samsung,sec-ap-pmic",
|
|
.of_match_table = sec_ap_pmic_match_table,
|
|
},
|
|
.probe = sec_ap_pmic_probe,
|
|
.remove = sec_ap_pmic_remove,
|
|
};
|
|
|
|
module_platform_driver(sec_ap_pmic_driver);
|
|
|
|
MODULE_DESCRIPTION("sec_ap_pmic driver");
|
|
MODULE_SOFTDEP("pre: sec_class");
|
|
MODULE_LICENSE("GPL");
|
|
MODULE_SOFTDEP("pre: sec_crashkey_long");
|
|
MODULE_SOFTDEP("pre: pm8941-pwrkey");
|
|
MODULE_AUTHOR("Jiman Cho <jiman85.cho@samsung.com");
|
|
MODULE_LICENSE("GPL");
|