1692 lines
43 KiB
C
Executable File
1692 lines
43 KiB
C
Executable File
// SPDX-License-Identifier: GPL-2.0-only
|
|
/*
|
|
* Copyright (c) 2011-2021, The Linux Foundation. All rights reserved.
|
|
* Copyright (c) 2022-2024, Qualcomm Innovation Center, Inc. All rights reserved.
|
|
*/
|
|
|
|
#include <linux/cdev.h>
|
|
#include <linux/debugfs.h>
|
|
#include <linux/delay.h>
|
|
#include <linux/device.h>
|
|
#include <linux/io.h>
|
|
#include <linux/module.h>
|
|
#include <linux/mutex.h>
|
|
#include <linux/of.h>
|
|
#include <linux/platform_device.h>
|
|
#include <linux/seq_file.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/string.h>
|
|
#include <linux/uaccess.h>
|
|
|
|
#include <linux/soc/qcom/qcom_aoss.h>
|
|
#include <linux/soc/qcom/smem.h>
|
|
#include <soc/qcom/qcom_stats.h>
|
|
#include <clocksource/arm_arch_timer.h>
|
|
|
|
#if IS_ENABLED(CONFIG_SEC_PM)
|
|
#include <trace/events/power.h>
|
|
|
|
#define MAX_BUF_LEN 512
|
|
#define MSM_ARCH_TIMER_FREQ 19200000
|
|
#define GET_SEC(A) ((A) / (MSM_ARCH_TIMER_FREQ))
|
|
#define GET_MSEC(A) (((A) / (MSM_ARCH_TIMER_FREQ / 1000)) % 1000)
|
|
|
|
#define SOC_NUM_RECORDS 5
|
|
u64 last_accumulated[SOC_NUM_RECORDS];
|
|
char buf[MAX_BUF_LEN];
|
|
#endif /* CONFIG_SEC_PM */
|
|
|
|
#define RPM_DYNAMIC_ADDR 0x14
|
|
#define RPM_DYNAMIC_ADDR_MASK 0xFFFF
|
|
|
|
#define STAT_TYPE_OFFSET 0x0
|
|
#define COUNT_OFFSET 0x4
|
|
#define LAST_ENTERED_AT_OFFSET 0x8
|
|
#define LAST_EXITED_AT_OFFSET 0x10
|
|
#define ACCUMULATED_OFFSET 0x18
|
|
#define CLIENT_VOTES_OFFSET 0x20
|
|
|
|
#define DDR_STATS_MAGIC_KEY 0xA1157A75
|
|
#define DDR_STATS_MAX_NUM_MODES 0x14
|
|
#define MAX_DRV 28
|
|
#define MAX_MSG_LEN 64
|
|
#define DRV_ABSENT 0xdeaddead
|
|
#define DRV_INVALID 0xffffdead
|
|
#define VOTE_MASK 0x3fff
|
|
#define VOTE_X_SHIFT 14
|
|
|
|
#define DDR_STATS_MAGIC_KEY_ADDR 0x0
|
|
#define DDR_STATS_NUM_MODES_ADDR 0x4
|
|
#define DDR_STATS_ENTRY_ADDR 0x8
|
|
#define DDR_STATS_NAME_ADDR 0x0
|
|
#define DDR_STATS_COUNT_ADDR 0x4
|
|
#define DDR_STATS_DURATION_ADDR 0x8
|
|
|
|
#define MAX_ISLAND_STATS_NAME_LENGTH 16
|
|
#define MAX_ISLAND_STATS 6
|
|
#define ISLAND_STATS_PID 2 /* ADSP PID */
|
|
#define ISLAND_STATS_SMEM_ID 653
|
|
#define LLC_ISLAND_STATS_SMEM_ID 661
|
|
|
|
#define STATS_BASEMINOR 0
|
|
#define STATS_MAX_MINOR 1
|
|
#define STATS_DEVICE_NAME "stats"
|
|
#define SUBSYSTEM_STATS_MAGIC_NUM (0x9d)
|
|
#define SUBSYSTEM_STATS_OTHERS_NUM (-2)
|
|
|
|
#define APSS_IOCTL _IOR(SUBSYSTEM_STATS_MAGIC_NUM, 0, \
|
|
struct sleep_stats *)
|
|
#define MODEM_IOCTL _IOR(SUBSYSTEM_STATS_MAGIC_NUM, 1, \
|
|
struct sleep_stats *)
|
|
#define WPSS_IOCTL _IOR(SUBSYSTEM_STATS_MAGIC_NUM, 2, \
|
|
struct sleep_stats *)
|
|
#define ADSP_IOCTL _IOR(SUBSYSTEM_STATS_MAGIC_NUM, 3, \
|
|
struct sleep_stats *)
|
|
#define ADSP_ISLAND_IOCTL _IOR(SUBSYSTEM_STATS_MAGIC_NUM, 4, \
|
|
struct sleep_stats *)
|
|
#define CDSP_IOCTL _IOR(SUBSYSTEM_STATS_MAGIC_NUM, 5, \
|
|
struct sleep_stats *)
|
|
#define SLPI_IOCTL _IOR(SUBSYSTEM_STATS_MAGIC_NUM, 6, \
|
|
struct sleep_stats *)
|
|
#define GPU_IOCTL _IOR(SUBSYSTEM_STATS_MAGIC_NUM, 7, \
|
|
struct sleep_stats *)
|
|
#define DISPLAY_IOCTL _IOR(SUBSYSTEM_STATS_MAGIC_NUM, 8, \
|
|
struct sleep_stats *)
|
|
#define SLPI_ISLAND_IOCTL _IOR(SUBSYSTEM_STATS_MAGIC_NUM, 9, \
|
|
struct sleep_stats *)
|
|
|
|
#define AOSD_IOCTL _IOR(SUBSYSTEM_STATS_MAGIC_NUM, 10, \
|
|
struct sleep_stats *)
|
|
|
|
#define CXSD_IOCTL _IOR(SUBSYSTEM_STATS_MAGIC_NUM, 11, \
|
|
struct sleep_stats *)
|
|
|
|
#define DDR_IOCTL _IOR(SUBSYSTEM_STATS_MAGIC_NUM, 12, \
|
|
struct sleep_stats *)
|
|
|
|
#define DDR_STATS_IOCTL _IOR(SUBSYSTEM_STATS_MAGIC_NUM, 13, \
|
|
struct sleep_stats *)
|
|
|
|
struct subsystem_data {
|
|
const char *name;
|
|
u32 smem_item;
|
|
u32 pid;
|
|
bool not_present;
|
|
};
|
|
|
|
static struct subsystem_data subsystems[] = {
|
|
{ "modem", 605, 1 },
|
|
{ "wpss", 605, 13 },
|
|
{ "adsp", 606, 2 },
|
|
{ "cdsp", 607, 5 },
|
|
{ "cdsp1", 607, 12 },
|
|
{ "gpdsp0", 607, 17 },
|
|
{ "gpdsp1", 607, 18 },
|
|
{ "slpi", 608, 3 },
|
|
{ "gpu", 609, 0 },
|
|
{ "display", 610, 0 },
|
|
{ "adsp_island", 613, 2 },
|
|
{ "slpi_island", 613, 3 },
|
|
{ "apss", 631, QCOM_SMEM_HOST_ANY },
|
|
};
|
|
|
|
struct stats_config {
|
|
size_t stats_offset;
|
|
size_t ddr_stats_offset;
|
|
size_t cx_vote_offset;
|
|
size_t num_records;
|
|
bool appended_stats_avail;
|
|
bool dynamic_offset;
|
|
bool subsystem_stats_in_smem;
|
|
bool read_ddr_votes;
|
|
bool read_ddr_his;
|
|
bool read_cx_final_vote;
|
|
bool ddr_freq_update;
|
|
bool island_stats_avail;
|
|
bool llc_island_stats_avail;
|
|
};
|
|
|
|
struct stats_data {
|
|
bool appended_stats_avail;
|
|
void __iomem *base;
|
|
};
|
|
|
|
struct stats_drvdata {
|
|
void __iomem *base;
|
|
const struct stats_config *config;
|
|
struct stats_data *d;
|
|
struct dentry *root;
|
|
dev_t dev_no;
|
|
struct class *stats_class;
|
|
struct device *stats_device;
|
|
struct cdev stats_cdev;
|
|
struct mutex lock;
|
|
struct qmp *qmp;
|
|
ktime_t ddr_freqsync_msg_time;
|
|
};
|
|
|
|
static struct stats_drvdata *drv;
|
|
|
|
struct sleep_stats {
|
|
u32 stat_type;
|
|
u32 count;
|
|
u64 last_entered_at;
|
|
u64 last_exited_at;
|
|
u64 accumulated;
|
|
};
|
|
|
|
struct appended_stats {
|
|
u32 client_votes;
|
|
u32 reserved[3];
|
|
};
|
|
|
|
struct island_stats {
|
|
char name[MAX_ISLAND_STATS_NAME_LENGTH];
|
|
u32 count;
|
|
u64 last_entered_at;
|
|
u64 last_exited_at;
|
|
u64 accumulated;
|
|
u32 vid;
|
|
u32 task_id;
|
|
u32 reserved[3];
|
|
};
|
|
|
|
#if IS_ENABLED(CONFIG_SEC_PM)
|
|
#define MAX_SLEEP_STATS_COUNT 10
|
|
#define MAX_SLEEP_STATS_NAME 16
|
|
|
|
static char sys_names[MAX_SLEEP_STATS_COUNT][MAX_SLEEP_STATS_NAME];
|
|
|
|
static int max_subsys_count;
|
|
static int subsys_idx[MAX_SLEEP_STATS_COUNT];
|
|
#endif
|
|
|
|
static bool subsystem_stats_debug_on;
|
|
/* Subsystem stats before and after suspend */
|
|
static struct sleep_stats *b_subsystem_stats;
|
|
static struct sleep_stats *a_subsystem_stats;
|
|
/* System sleep stats before and after suspend */
|
|
static struct sleep_stats *b_system_stats;
|
|
static struct sleep_stats *a_system_stats;
|
|
static DEFINE_MUTEX(sleep_stats_mutex);
|
|
|
|
#define DSP_SLEEP_DEBUG_ON
|
|
|
|
#if defined(DSP_SLEEP_DEBUG_ON)
|
|
#include <linux/samsung/debug/sec_debug.h>
|
|
#include <linux/workqueue.h>
|
|
#include <linux/remoteproc.h>
|
|
#define MAX_COUNT 10
|
|
|
|
struct _dsp_entry {
|
|
char name[4];
|
|
uint64_t entry_sec;
|
|
uint64_t entry_msec;
|
|
uint64_t prev_exit_sec;
|
|
uint64_t prev_exit_msec;
|
|
uint64_t error_count;
|
|
struct timespec64 interval;
|
|
int (*ssr)(void);
|
|
} DSP_ENTRY[1]; // 0 : CDSP, 1 : ADSP - adsp is disabled for the time being.
|
|
struct cdsp_loader_private {
|
|
void *pil_h;
|
|
struct kobject *boot_cdsp_obj;
|
|
struct attribute_group *attr_group;
|
|
};
|
|
|
|
static struct cdsp_loader_private *priv = NULL;
|
|
static struct device_node *cdsp_node = NULL;
|
|
#endif
|
|
|
|
static inline void get_sleep_stat_name(u32 type, char *stat_type)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < sizeof(u32); i++) {
|
|
stat_type[i] = type & 0xff;
|
|
type = type >> 8;
|
|
}
|
|
strim(stat_type);
|
|
}
|
|
|
|
bool has_system_slept(bool *debug_aoss)
|
|
{
|
|
int i;
|
|
bool sleep_flag = true;
|
|
char stat_type[sizeof(u32) + 1] = {0};
|
|
bool aosd_entered = false, cxsd_entered = false;
|
|
|
|
for (i = 0; i < drv->config->num_records; i++) {
|
|
get_sleep_stat_name(b_system_stats[i].stat_type, stat_type);
|
|
if (b_system_stats[i].count == a_system_stats[i].count) {
|
|
pr_info("System %s has not entered sleep\n", stat_type);
|
|
sleep_flag = false;
|
|
continue;
|
|
}
|
|
|
|
if (!strcmp(stat_type, "cxsd"))
|
|
cxsd_entered = true;
|
|
|
|
if (!strcmp(stat_type, "aosd"))
|
|
aosd_entered = true;
|
|
|
|
}
|
|
|
|
if (cxsd_entered && !aosd_entered)
|
|
*debug_aoss = true;
|
|
|
|
return sleep_flag;
|
|
}
|
|
EXPORT_SYMBOL_GPL(has_system_slept);
|
|
|
|
bool has_subsystem_slept(void)
|
|
{
|
|
int i;
|
|
bool sleep_flag = true;
|
|
|
|
for (i = 0; i < ARRAY_SIZE(subsystems); i++) {
|
|
if (subsystems[i].not_present)
|
|
continue;
|
|
|
|
if ((b_subsystem_stats[i].count == a_subsystem_stats[i].count) &&
|
|
(a_subsystem_stats[i].last_exited_at >
|
|
a_subsystem_stats[i].last_entered_at)) {
|
|
pr_info("Subsystem %s has not entered sleep\n", subsystems[i].name);
|
|
sleep_flag = false;
|
|
}
|
|
}
|
|
|
|
return sleep_flag;
|
|
}
|
|
EXPORT_SYMBOL_GPL(has_subsystem_slept);
|
|
|
|
void subsystem_sleep_debug_enable(bool enable)
|
|
{
|
|
subsystem_stats_debug_on = enable;
|
|
}
|
|
EXPORT_SYMBOL_GPL(subsystem_sleep_debug_enable);
|
|
|
|
static inline int qcom_stats_copy_to_user(unsigned long arg, struct sleep_stats *stats,
|
|
unsigned long size)
|
|
{
|
|
|
|
return copy_to_user((void __user *)arg, stats, size);
|
|
}
|
|
#if defined(DSP_SLEEP_DEBUG_ON)
|
|
extern bool dump_enabled(void);
|
|
extern void set_dump_enabled(int val);
|
|
|
|
void cdsp_restart(struct work_struct *work)
|
|
{
|
|
int prev_dump_collection = 0;
|
|
|
|
pr_err("%s start", __func__);
|
|
if (!priv)
|
|
return;
|
|
|
|
prev_dump_collection = dump_enabled();
|
|
set_dump_enabled(0);
|
|
|
|
phandle rproc_phandle;
|
|
int sz = 0;
|
|
sz = of_property_read_u32(cdsp_node, "qcom,rproc-handle", &rproc_phandle);
|
|
priv->pil_h = rproc_get_by_phandle(rproc_phandle);
|
|
|
|
pr_debug("%s: going to call rpoc_shutdown for cdsp\n", __func__);
|
|
rproc_shutdown(priv->pil_h);
|
|
msleep(800);
|
|
pr_debug("%s: going to call rproc_boot for cdsp\n", __func__);
|
|
rproc_boot(priv->pil_h);
|
|
|
|
set_dump_enabled(prev_dump_collection);
|
|
pr_err("%s end", __func__);
|
|
}
|
|
static DECLARE_WORK(dsp_ssr, cdsp_restart);
|
|
#endif
|
|
static inline void qcom_stats_update_accumulated_duration(struct sleep_stats *stats)
|
|
{
|
|
/*
|
|
* If a subsystem is in sleep when reading the sleep stats from SMEM
|
|
* adjust the accumulated sleep duration to show actual sleep time.
|
|
* This ensures that the displayed stats are real when used for
|
|
* the purpose of computing battery utilization.
|
|
*/
|
|
if (stats->last_entered_at > stats->last_exited_at)
|
|
stats->accumulated += (__arch_counter_get_cntvct() - stats->last_entered_at);
|
|
}
|
|
|
|
static inline void qcom_stats_copy(struct sleep_stats *src, struct sleep_stats *dst)
|
|
{
|
|
dst->stat_type = src->stat_type;
|
|
dst->count = src->count;
|
|
dst->last_entered_at = src->last_entered_at;
|
|
dst->last_exited_at = src->last_exited_at;
|
|
dst->accumulated = src->accumulated;
|
|
}
|
|
|
|
static bool ddr_stats_is_freq_overtime(struct sleep_stats *data)
|
|
{
|
|
if ((data->count == 0) && (drv->config->ddr_freq_update))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
static u64 qcom_stats_fill_ddr_stats(void __iomem *reg, struct sleep_stats *data, u32 *entry_count)
|
|
{
|
|
u64 accumulated_duration = 0;
|
|
int i;
|
|
|
|
*entry_count = readl_relaxed(reg + DDR_STATS_NUM_MODES_ADDR);
|
|
if (*entry_count > DDR_STATS_MAX_NUM_MODES) {
|
|
pr_err("Invalid entry count\n");
|
|
return 0;
|
|
}
|
|
|
|
reg += DDR_STATS_ENTRY_ADDR;
|
|
|
|
for (i = 0; i < *entry_count; i++) {
|
|
data[i].count = readl_relaxed(reg + DDR_STATS_COUNT_ADDR);
|
|
if ((i >= 0x4) && (ddr_stats_is_freq_overtime(&data[i]))) {
|
|
pr_err("ddr_stats: Freq update failed\n");
|
|
return 0;
|
|
}
|
|
|
|
data[i].stat_type = readl_relaxed(reg + DDR_STATS_NAME_ADDR);
|
|
data[i].last_entered_at = 0xDEADDEAD;
|
|
data[i].last_exited_at = 0xDEADDEAD;
|
|
data[i].accumulated = readq_relaxed(reg + DDR_STATS_DURATION_ADDR);
|
|
|
|
accumulated_duration += data[i].accumulated;
|
|
reg += sizeof(struct sleep_stats) - 2 * sizeof(u64);
|
|
}
|
|
|
|
return accumulated_duration;
|
|
}
|
|
|
|
static int qcom_stats_device_open(struct inode *inode, struct file *file)
|
|
{
|
|
struct stats_drvdata *drv = NULL;
|
|
|
|
if (!inode || !inode->i_cdev || !file)
|
|
return -EINVAL;
|
|
|
|
drv = container_of(inode->i_cdev, struct stats_drvdata, stats_cdev);
|
|
file->private_data = drv;
|
|
|
|
return 0;
|
|
}
|
|
|
|
int qcom_stats_ddr_freqsync_msg(void)
|
|
{
|
|
static const char buf[MAX_MSG_LEN] = "{class: ddr, action: freqsync}";
|
|
int ret = 0;
|
|
|
|
if (!drv || !drv->qmp || !drv->config->read_ddr_votes)
|
|
return -ENODEV;
|
|
|
|
mutex_lock(&drv->lock);
|
|
ret = qmp_send(drv->qmp, buf, sizeof(buf));
|
|
if (ret) {
|
|
pr_err("Error sending qmp message: %d\n", ret);
|
|
mutex_unlock(&drv->lock);
|
|
return ret;
|
|
}
|
|
|
|
mutex_unlock(&drv->lock);
|
|
|
|
drv->ddr_freqsync_msg_time = ktime_get_boottime();
|
|
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL_GPL(qcom_stats_ddr_freqsync_msg);
|
|
|
|
static int qcom_stats_ddr_freq_sync(int *modes, struct sleep_stats *stat)
|
|
{
|
|
void __iomem *reg = NULL;
|
|
u32 entry_count, name;
|
|
ktime_t now;
|
|
int i, j, ret;
|
|
|
|
if (drv->config->read_ddr_votes) {
|
|
ret = qcom_stats_ddr_freqsync_msg();
|
|
if (ret)
|
|
return ret;
|
|
|
|
now = ktime_get_boottime();
|
|
while (now < drv->ddr_freqsync_msg_time) {
|
|
udelay(500);
|
|
now = ktime_get_boottime();
|
|
}
|
|
}
|
|
|
|
reg = drv->base + drv->config->ddr_stats_offset;
|
|
qcom_stats_fill_ddr_stats(reg, stat, &entry_count);
|
|
|
|
if (drv->config->read_ddr_votes) {
|
|
for (i = 0, j = 0; i < entry_count; i++) {
|
|
name = (stat[i].stat_type >> 8) & 0xFF;
|
|
if (name == 0x1 && !stat[i].count)
|
|
break;
|
|
++j;
|
|
}
|
|
if (j < DDR_STATS_MAX_NUM_MODES)
|
|
*modes = j;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static long qcom_stats_device_ioctl(struct file *file, unsigned int cmd,
|
|
unsigned long arg)
|
|
{
|
|
struct stats_drvdata *drv = file->private_data;
|
|
const struct subsystem_data *subsystem = NULL;
|
|
struct sleep_stats *stat;
|
|
struct sleep_stats *temp;
|
|
void __iomem *reg = NULL;
|
|
unsigned long size = sizeof(struct sleep_stats);
|
|
u32 stats_id;
|
|
int ret;
|
|
|
|
mutex_lock(&sleep_stats_mutex);
|
|
if (cmd != DDR_STATS_IOCTL)
|
|
stat = kzalloc(sizeof(struct sleep_stats), GFP_KERNEL);
|
|
else
|
|
stat = kcalloc(DDR_STATS_MAX_NUM_MODES, sizeof(struct sleep_stats), GFP_KERNEL);
|
|
if (!stat) {
|
|
mutex_unlock(&sleep_stats_mutex);
|
|
return -ENOMEM;
|
|
}
|
|
|
|
switch (cmd) {
|
|
case MODEM_IOCTL:
|
|
subsystem = &subsystems[0];
|
|
break;
|
|
case WPSS_IOCTL:
|
|
subsystem = &subsystems[1];
|
|
break;
|
|
case ADSP_IOCTL:
|
|
subsystem = &subsystems[2];
|
|
break;
|
|
case CDSP_IOCTL:
|
|
subsystem = &subsystems[3];
|
|
break;
|
|
case SLPI_IOCTL:
|
|
subsystem = &subsystems[4];
|
|
break;
|
|
case GPU_IOCTL:
|
|
subsystem = &subsystems[5];
|
|
break;
|
|
case DISPLAY_IOCTL:
|
|
subsystem = &subsystems[6];
|
|
break;
|
|
case ADSP_ISLAND_IOCTL:
|
|
subsystem = &subsystems[7];
|
|
break;
|
|
case SLPI_ISLAND_IOCTL:
|
|
subsystem = &subsystems[8];
|
|
break;
|
|
case APSS_IOCTL:
|
|
subsystem = &subsystems[9];
|
|
break;
|
|
case AOSD_IOCTL:
|
|
stats_id = 0;
|
|
if (drv->config->num_records > stats_id)
|
|
reg = drv->d[stats_id].base;
|
|
break;
|
|
case CXSD_IOCTL:
|
|
stats_id = 1;
|
|
if (drv->config->num_records > stats_id)
|
|
reg = drv->d[stats_id].base;
|
|
break;
|
|
case DDR_IOCTL:
|
|
stats_id = 2;
|
|
if (drv->config->num_records > stats_id)
|
|
reg = drv->d[stats_id].base;
|
|
break;
|
|
case DDR_STATS_IOCTL:
|
|
break;
|
|
default:
|
|
pr_err("Incorrect command error\n");
|
|
ret = -EINVAL;
|
|
goto exit;
|
|
}
|
|
|
|
if (subsystem) {
|
|
/* Items are allocated lazily, so lookup pointer each time */
|
|
temp = qcom_smem_get(subsystem->pid, subsystem->smem_item, NULL);
|
|
if (IS_ERR(temp)) {
|
|
ret = -EIO;
|
|
goto exit;
|
|
}
|
|
|
|
qcom_stats_copy(temp, stat);
|
|
|
|
qcom_stats_update_accumulated_duration(stat);
|
|
|
|
ret = qcom_stats_copy_to_user(arg, stat, size);
|
|
} else if (reg) {
|
|
memcpy_fromio(stat, reg, sizeof(*stat));
|
|
|
|
qcom_stats_update_accumulated_duration(stat);
|
|
|
|
ret = qcom_stats_copy_to_user(arg, stat, size);
|
|
} else {
|
|
int modes = DDR_STATS_MAX_NUM_MODES;
|
|
|
|
ret = qcom_stats_ddr_freq_sync(&modes, stat);
|
|
if (ret)
|
|
goto exit;
|
|
|
|
ret = qcom_stats_copy_to_user(arg, stat, modes * size);
|
|
}
|
|
|
|
exit:
|
|
kfree(stat);
|
|
mutex_unlock(&sleep_stats_mutex);
|
|
return ret;
|
|
}
|
|
|
|
static const struct file_operations qcom_stats_device_fops = {
|
|
.owner = THIS_MODULE,
|
|
.open = qcom_stats_device_open,
|
|
.unlocked_ioctl = qcom_stats_device_ioctl,
|
|
};
|
|
|
|
int ddr_stats_get_freq_count(void)
|
|
{
|
|
u32 entry_count, name;
|
|
u32 freq_count = 0;
|
|
void __iomem *reg;
|
|
int i;
|
|
|
|
if (!drv || !drv->qmp || !drv->config->read_ddr_votes)
|
|
return -ENODEV;
|
|
|
|
reg = drv->base + drv->config->ddr_stats_offset;
|
|
entry_count = readl_relaxed(reg + DDR_STATS_NUM_MODES_ADDR);
|
|
if (entry_count > DDR_STATS_MAX_NUM_MODES) {
|
|
pr_err("Invalid entry count\n");
|
|
return 0;
|
|
}
|
|
|
|
reg += DDR_STATS_ENTRY_ADDR;
|
|
|
|
for (i = 0; i < entry_count; i++) {
|
|
name = readl_relaxed(reg + DDR_STATS_NAME_ADDR);
|
|
name = (name >> 8) & 0xFF;
|
|
if (name == 0x1)
|
|
freq_count++;
|
|
|
|
reg += sizeof(struct sleep_stats) - 2 * sizeof(u64);
|
|
}
|
|
|
|
return freq_count;
|
|
}
|
|
EXPORT_SYMBOL_GPL(ddr_stats_get_freq_count);
|
|
|
|
int ddr_stats_get_residency(int freq_count, struct ddr_freq_residency *data)
|
|
{
|
|
struct sleep_stats stat[DDR_STATS_MAX_NUM_MODES];
|
|
void __iomem *reg;
|
|
u32 name, entry_count;
|
|
ktime_t now;
|
|
int i, j;
|
|
|
|
if (freq_count < 0 || !data)
|
|
return -EINVAL;
|
|
|
|
if (!drv || !drv->qmp || !drv->config->read_ddr_votes)
|
|
return -ENODEV;
|
|
|
|
now = ktime_get_boottime();
|
|
while (now < drv->ddr_freqsync_msg_time) {
|
|
udelay(500);
|
|
now = ktime_get_boottime();
|
|
}
|
|
|
|
mutex_lock(&drv->lock);
|
|
|
|
reg = drv->base + drv->config->ddr_stats_offset;
|
|
qcom_stats_fill_ddr_stats(reg, stat, &entry_count);
|
|
|
|
for (i = 0, j = 0; i < entry_count; i++) {
|
|
name = stat[i].stat_type;
|
|
if (((name >> 8) & 0xFF) == 0x1 && stat[i].count) {
|
|
data[j].freq = name >> 16;
|
|
data[j].residency = stat[i].accumulated;
|
|
if (++j > freq_count)
|
|
break;
|
|
}
|
|
}
|
|
|
|
mutex_unlock(&drv->lock);
|
|
|
|
return j;
|
|
}
|
|
EXPORT_SYMBOL_GPL(ddr_stats_get_residency);
|
|
|
|
int ddr_stats_get_ss_count(void)
|
|
{
|
|
return drv->config->read_ddr_votes ? MAX_DRV : -EOPNOTSUPP;
|
|
}
|
|
EXPORT_SYMBOL_GPL(ddr_stats_get_ss_count);
|
|
|
|
static void __iomem *qcom_stats_get_ddr_stats_data_addr(void)
|
|
{
|
|
void __iomem *reg = NULL;
|
|
u32 vote_offset;
|
|
u32 entry_count;
|
|
|
|
reg = drv->base + drv->config->ddr_stats_offset;
|
|
entry_count = readl_relaxed(reg + DDR_STATS_NUM_MODES_ADDR);
|
|
if (entry_count > DDR_STATS_MAX_NUM_MODES) {
|
|
pr_err("Invalid entry count\n");
|
|
return NULL;
|
|
}
|
|
|
|
vote_offset = DDR_STATS_ENTRY_ADDR;
|
|
vote_offset += entry_count * (sizeof(struct sleep_stats) - 2 * sizeof(u64));
|
|
reg = drv->base + drv->config->ddr_stats_offset + vote_offset;
|
|
|
|
return reg;
|
|
}
|
|
|
|
int ddr_stats_get_ss_vote_info(int ss_count,
|
|
struct ddr_stats_ss_vote_info *vote_info)
|
|
{
|
|
static const char buf[MAX_MSG_LEN] = "{class: ddr, res: drvs_ddr_votes}";
|
|
u32 val[MAX_DRV];
|
|
void __iomem *reg;
|
|
int ret, i;
|
|
|
|
if (!vote_info || !(ss_count == MAX_DRV) || !drv)
|
|
return -ENODEV;
|
|
|
|
if (!drv->qmp)
|
|
return -EOPNOTSUPP;
|
|
|
|
mutex_lock(&drv->lock);
|
|
ret = qmp_send(drv->qmp, buf, sizeof(buf));
|
|
if (ret) {
|
|
pr_err("Error sending qmp message: %d\n", ret);
|
|
mutex_unlock(&drv->lock);
|
|
return ret;
|
|
}
|
|
|
|
reg = qcom_stats_get_ddr_stats_data_addr();
|
|
if (!reg) {
|
|
pr_err("Error getting ddr stats data addr\n");
|
|
mutex_unlock(&drv->lock);
|
|
return -EINVAL;
|
|
}
|
|
|
|
for (i = 0; i < ss_count; i++, reg += sizeof(u32)) {
|
|
val[i] = readl_relaxed(reg);
|
|
if (val[i] == DRV_ABSENT) {
|
|
vote_info[i].ab = DRV_ABSENT;
|
|
vote_info[i].ib = DRV_ABSENT;
|
|
continue;
|
|
} else if (val[i] == DRV_INVALID) {
|
|
vote_info[i].ab = DRV_INVALID;
|
|
vote_info[i].ib = DRV_INVALID;
|
|
continue;
|
|
}
|
|
|
|
vote_info[i].ab = (val[i] >> VOTE_X_SHIFT) & VOTE_MASK;
|
|
vote_info[i].ib = val[i] & VOTE_MASK;
|
|
}
|
|
|
|
mutex_unlock(&drv->lock);
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(ddr_stats_get_ss_vote_info);
|
|
|
|
static void cxvt_info_fill_data(void __iomem *reg, u32 entry_count,
|
|
u32 *data)
|
|
{
|
|
int i;
|
|
|
|
for (i = 0; i < entry_count; i++) {
|
|
data[i] = readl_relaxed(reg);
|
|
reg += sizeof(u32);
|
|
}
|
|
}
|
|
|
|
int cx_stats_get_ss_vote_info(int ss_count,
|
|
struct qcom_stats_cx_vote_info *vote_info)
|
|
{
|
|
static const char buf[MAX_MSG_LEN] = "{class: misc_debug, res: cx_vote}";
|
|
void __iomem *reg;
|
|
int ret;
|
|
int i, j;
|
|
u32 data[((MAX_DRV + 0x3) & (~0x3))/4 + 1];
|
|
u32 entry_count = 0;
|
|
|
|
if (!vote_info || !(ss_count == MAX_DRV) || !drv)
|
|
return -ENODEV;
|
|
|
|
if (!drv->qmp || !drv->config->cx_vote_offset)
|
|
return -EOPNOTSUPP;
|
|
|
|
mutex_lock(&drv->lock);
|
|
ret = qmp_send(drv->qmp, buf, sizeof(buf));
|
|
if (ret) {
|
|
pr_err("Error sending qmp message: %d\n", ret);
|
|
mutex_unlock(&drv->lock);
|
|
return ret;
|
|
}
|
|
|
|
reg = qcom_stats_get_ddr_stats_data_addr();
|
|
if (!reg) {
|
|
pr_err("Error getting ddr stats data addr\n");
|
|
mutex_unlock(&drv->lock);
|
|
return -EINVAL;
|
|
}
|
|
|
|
if (drv->config->read_cx_final_vote)
|
|
entry_count = ((MAX_DRV + 0x3) & (~0x3))/4 + 1;
|
|
else
|
|
entry_count = ((MAX_DRV + 0x3) & (~0x3))/4;
|
|
cxvt_info_fill_data(reg, entry_count, data);
|
|
for (i = 0, j = 0; i < ((MAX_DRV + 0x3) & (~0x3))/4; i++, j += 4) {
|
|
vote_info[j].level = (data[i] & 0xff);
|
|
vote_info[j+1].level = ((data[i] & 0xff00) >> 8);
|
|
vote_info[j+2].level = ((data[i] & 0xff0000) >> 16);
|
|
vote_info[j+3].level = ((data[i] & 0xff000000) >> 24);
|
|
}
|
|
|
|
if (drv->config->read_cx_final_vote)
|
|
vote_info[j].level = (u8)data[i];
|
|
|
|
mutex_unlock(&drv->lock);
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(cx_stats_get_ss_vote_info);
|
|
|
|
int ddr_stats_get_change_his(struct ddr_stats_change_his_info *ddr_his_info)
|
|
{
|
|
static const char buf[MAX_MSG_LEN] = "{class: misc_debug, res: ddr_his}";
|
|
int ret;
|
|
void __iomem *reg;
|
|
|
|
if (!drv || !drv->qmp || !drv->config->read_ddr_his)
|
|
return -ENODEV;
|
|
|
|
mutex_lock(&drv->lock);
|
|
ret = qmp_send(drv->qmp, buf, sizeof(buf));
|
|
if (ret) {
|
|
pr_err("Error sending qmp message: %d\n", ret);
|
|
mutex_unlock(&drv->lock);
|
|
return ret;
|
|
}
|
|
|
|
reg = qcom_stats_get_ddr_stats_data_addr();
|
|
if (!reg) {
|
|
pr_err("Error getting ddr stats data addr\n");
|
|
mutex_unlock(&drv->lock);
|
|
return -EINVAL;
|
|
}
|
|
|
|
memcpy_fromio(ddr_his_info, reg, sizeof(struct ddr_stats_change_his_info));
|
|
mutex_unlock(&drv->lock);
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(ddr_stats_get_change_his);
|
|
|
|
int llc_stats_get_active_scids(struct llc_island_stats_active_scids *llc_active_scids)
|
|
{
|
|
static struct llc_island_stats_active_scids *local_scids;
|
|
|
|
if (!drv || !drv->config->llc_island_stats_avail)
|
|
return -ENODEV;
|
|
|
|
mutex_lock(&drv->lock);
|
|
if (IS_ERR_OR_NULL(local_scids)) {
|
|
local_scids = qcom_smem_get(
|
|
ISLAND_STATS_PID,
|
|
LLC_ISLAND_STATS_SMEM_ID,
|
|
NULL);
|
|
if (IS_ERR_OR_NULL(local_scids)) {
|
|
mutex_unlock(&drv->lock);
|
|
return -ENOMEM;
|
|
}
|
|
} else {
|
|
memcpy_fromio(llc_active_scids, local_scids, sizeof(*local_scids));
|
|
}
|
|
|
|
mutex_unlock(&drv->lock);
|
|
|
|
return 0;
|
|
}
|
|
EXPORT_SYMBOL_GPL(llc_stats_get_active_scids);
|
|
|
|
static void qcom_print_stats(struct seq_file *s, const struct sleep_stats *stat)
|
|
{
|
|
u64 accumulated = stat->accumulated;
|
|
/*
|
|
* If a subsystem is in sleep when reading the sleep stats adjust
|
|
* the accumulated sleep duration to show actual sleep time.
|
|
*/
|
|
if (stat->last_entered_at > stat->last_exited_at)
|
|
accumulated += arch_timer_read_counter() - stat->last_entered_at;
|
|
|
|
seq_printf(s, "Count: %u\n", stat->count);
|
|
seq_printf(s, "Last Entered At: %llu\n", stat->last_entered_at);
|
|
seq_printf(s, "Last Exited At: %llu\n", stat->last_exited_at);
|
|
seq_printf(s, "Accumulated Duration: %llu\n", accumulated);
|
|
}
|
|
|
|
static int qcom_subsystem_sleep_stats_show(struct seq_file *s, void *unused)
|
|
{
|
|
struct subsystem_data *subsystem = s->private;
|
|
struct sleep_stats *stat;
|
|
|
|
/* Items are allocated lazily, so lookup pointer each time */
|
|
stat = qcom_smem_get(subsystem->pid, subsystem->smem_item, NULL);
|
|
if (IS_ERR(stat))
|
|
return 0;
|
|
|
|
qcom_print_stats(s, stat);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int qcom_soc_sleep_stats_show(struct seq_file *s, void *unused)
|
|
{
|
|
struct stats_data *d = s->private;
|
|
void __iomem *reg = d->base;
|
|
struct sleep_stats stat;
|
|
|
|
memcpy_fromio(&stat, reg, sizeof(stat));
|
|
qcom_print_stats(s, &stat);
|
|
|
|
if (d->appended_stats_avail) {
|
|
struct appended_stats votes;
|
|
|
|
memcpy_fromio(&votes, reg + CLIENT_VOTES_OFFSET, sizeof(votes));
|
|
seq_printf(s, "Client Votes: %#x\n", votes.client_votes);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static void print_ddr_stats(struct seq_file *s, int *count,
|
|
struct sleep_stats *data, u64 accumulated_duration)
|
|
{
|
|
u32 cp_idx = 0;
|
|
u32 name, duration = 0;
|
|
|
|
if (accumulated_duration)
|
|
duration = (data->accumulated * 100) / accumulated_duration;
|
|
|
|
name = (data->stat_type >> 8) & 0xFF;
|
|
if (name == 0x0) {
|
|
name = (data->stat_type) & 0xFF;
|
|
*count = *count + 1;
|
|
seq_printf(s,
|
|
"LPM %d:\tName:0x%x\tcount:%u\tDuration (ticks):%llu (~%u%%)\n",
|
|
*count, name, data->count, data->accumulated, duration);
|
|
} else if (name == 0x1) {
|
|
cp_idx = data->stat_type & 0x1F;
|
|
name = data->stat_type >> 16;
|
|
|
|
if (!name || !data->count)
|
|
return;
|
|
|
|
seq_printf(s,
|
|
"Freq %uMhz:\tCP IDX:%u\tcount:%u\tDuration (ticks):%llu (~%u%%)\n",
|
|
name, cp_idx, data->count, data->accumulated, duration);
|
|
}
|
|
}
|
|
|
|
static int ddr_stats_show(struct seq_file *s, void *d)
|
|
{
|
|
struct sleep_stats data[DDR_STATS_MAX_NUM_MODES];
|
|
void __iomem *reg = s->private;
|
|
u32 entry_count;
|
|
u64 accumulated_duration = 0, accumulated_duration_ddr_mode = 0;
|
|
int i, lpm_count = 0;
|
|
|
|
accumulated_duration = qcom_stats_fill_ddr_stats(reg, data, &entry_count);
|
|
|
|
for (i = 0; i < DDR_STATS_NUM_MODES_ADDR; i++)
|
|
accumulated_duration_ddr_mode += data[i].accumulated;
|
|
|
|
for (i = 0; i < DDR_STATS_NUM_MODES_ADDR; i++)
|
|
print_ddr_stats(s, &lpm_count, &data[i], accumulated_duration_ddr_mode);
|
|
|
|
if (!accumulated_duration) {
|
|
seq_puts(s, "ddr_stats: Freq update failed.\n");
|
|
return 0;
|
|
}
|
|
|
|
accumulated_duration -= accumulated_duration_ddr_mode;
|
|
for (i = DDR_STATS_NUM_MODES_ADDR; i < entry_count; i++)
|
|
print_ddr_stats(s, &lpm_count, &data[i], accumulated_duration);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int island_stats_show(struct seq_file *s, void *unused)
|
|
{
|
|
struct island_stats *stat;
|
|
int i;
|
|
|
|
/* Items are allocated lazily, so lookup pointer each time */
|
|
stat = qcom_smem_get(ISLAND_STATS_PID, ISLAND_STATS_SMEM_ID, NULL);
|
|
if (IS_ERR(stat))
|
|
return 0;
|
|
|
|
for (i = 0; i < MAX_ISLAND_STATS; i++) {
|
|
if (!strcmp(stat[i].name, "DEADDEAD"))
|
|
continue;
|
|
|
|
seq_printf(s, "Name: %s\n", stat[i].name);
|
|
seq_printf(s, "Count: %u\n", stat[i].count);
|
|
seq_printf(s, "Last Entered At: %llu\n", stat[i].last_entered_at);
|
|
seq_printf(s, "Last Exited At: %llu\n", stat[i].last_exited_at);
|
|
seq_printf(s, "Accumulated Duration: %llu\n", stat[i].accumulated);
|
|
seq_printf(s, "Vid: %u\n", stat[i].vid);
|
|
seq_printf(s, "task_id: %u\n", stat[i].task_id);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int vote_info_show(struct seq_file *s, void *d)
|
|
{
|
|
int i = 0;
|
|
struct ddr_stats_ss_vote_info ddr_vote_info[MAX_DRV];
|
|
struct qcom_stats_cx_vote_info cx_vote_info[MAX_DRV];
|
|
|
|
ddr_stats_get_ss_vote_info(MAX_DRV, ddr_vote_info);
|
|
cx_stats_get_ss_vote_info(MAX_DRV, cx_vote_info);
|
|
|
|
for (i = 0; i < MAX_DRV; i++) {
|
|
pr_info("PM: vote info: drv%d:\tcx(%d),\tddr(%d,\t%d)\n",
|
|
i, cx_vote_info[i].level, ddr_vote_info[i].ab, ddr_vote_info[i].ib);
|
|
seq_printf(s, "drv%d:\tcx(%d),\tddr(%d,\t%d)\n",
|
|
i, cx_vote_info[i].level, ddr_vote_info[i].ab, ddr_vote_info[i].ib);
|
|
}
|
|
|
|
|
|
return 0;
|
|
}
|
|
|
|
DEFINE_SHOW_ATTRIBUTE(qcom_soc_sleep_stats);
|
|
DEFINE_SHOW_ATTRIBUTE(qcom_subsystem_sleep_stats);
|
|
DEFINE_SHOW_ATTRIBUTE(ddr_stats);
|
|
DEFINE_SHOW_ATTRIBUTE(island_stats);
|
|
DEFINE_SHOW_ATTRIBUTE(vote_info);
|
|
|
|
static int qcom_create_stats_device(struct stats_drvdata *drv)
|
|
{
|
|
int ret;
|
|
|
|
ret = alloc_chrdev_region(&drv->dev_no, STATS_BASEMINOR, STATS_MAX_MINOR,
|
|
STATS_DEVICE_NAME);
|
|
if (ret)
|
|
return ret;
|
|
|
|
cdev_init(&drv->stats_cdev, &qcom_stats_device_fops);
|
|
ret = cdev_add(&drv->stats_cdev, drv->dev_no, 1);
|
|
if (ret) {
|
|
unregister_chrdev_region(drv->dev_no, 1);
|
|
return ret;
|
|
}
|
|
|
|
drv->stats_class = class_create(STATS_DEVICE_NAME);
|
|
if (IS_ERR_OR_NULL(drv->stats_class)) {
|
|
cdev_del(&drv->stats_cdev);
|
|
unregister_chrdev_region(drv->dev_no, 1);
|
|
return PTR_ERR(drv->stats_class);
|
|
}
|
|
|
|
drv->stats_device = device_create(drv->stats_class, NULL, drv->dev_no, NULL,
|
|
STATS_DEVICE_NAME);
|
|
if (IS_ERR_OR_NULL(drv->stats_device)) {
|
|
class_destroy(drv->stats_class);
|
|
cdev_del(&drv->stats_cdev);
|
|
unregister_chrdev_region(drv->dev_no, 1);
|
|
return PTR_ERR(drv->stats_device);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
static void qcom_create_island_stat_files(struct dentry *root, void __iomem *reg,
|
|
struct stats_data *d,
|
|
const struct stats_config *config)
|
|
{
|
|
if (!config->island_stats_avail)
|
|
return;
|
|
|
|
debugfs_create_file("island_stats", 0400, root, NULL, &island_stats_fops);
|
|
}
|
|
|
|
static void qcom_create_vote_info_files(struct dentry *root, void __iomem *reg,
|
|
struct stats_data *d,
|
|
const struct stats_config *config)
|
|
{
|
|
debugfs_create_file("vote_info", 0400, root, NULL, &vote_info_fops);
|
|
}
|
|
|
|
static void qcom_create_ddr_stat_files(struct dentry *root, void __iomem *reg,
|
|
struct stats_data *d,
|
|
const struct stats_config *config)
|
|
{
|
|
size_t stats_offset;
|
|
u32 key;
|
|
|
|
if (!config->ddr_stats_offset)
|
|
return;
|
|
|
|
stats_offset = config->ddr_stats_offset;
|
|
|
|
key = readl_relaxed(reg + stats_offset + DDR_STATS_MAGIC_KEY_ADDR);
|
|
if (key == DDR_STATS_MAGIC_KEY)
|
|
debugfs_create_file("ddr_stats", 0400, root, reg + stats_offset, &ddr_stats_fops);
|
|
}
|
|
|
|
static void qcom_create_soc_sleep_stat_files(struct dentry *root, void __iomem *reg,
|
|
struct stats_data *d,
|
|
const struct stats_config *config)
|
|
{
|
|
char stat_type[sizeof(u32) + 1] = {0};
|
|
size_t stats_offset = config->stats_offset;
|
|
u32 offset = 0, type;
|
|
int i;
|
|
|
|
/*
|
|
* On RPM targets, stats offset location is dynamic and changes from target
|
|
* to target and sometimes from build to build for same target.
|
|
*
|
|
* In such cases the dynamic address is present at 0x14 offset from base
|
|
* address in devicetree. The last 16bits indicates the stats_offset.
|
|
*/
|
|
if (config->dynamic_offset) {
|
|
stats_offset = readl(reg + RPM_DYNAMIC_ADDR);
|
|
stats_offset &= RPM_DYNAMIC_ADDR_MASK;
|
|
}
|
|
|
|
for (i = 0; i < config->num_records; i++) {
|
|
d[i].base = reg + offset + stats_offset;
|
|
|
|
/*
|
|
* Read the low power mode name and create debugfs file for it.
|
|
* The names read could be of below,
|
|
* (may change depending on low power mode supported).
|
|
* For rpmh-sleep-stats: "aosd", "cxsd" and "ddr".
|
|
* For rpm-sleep-stats: "vmin" and "vlow".
|
|
*/
|
|
type = readl(d[i].base);
|
|
get_sleep_stat_name(type, stat_type);
|
|
debugfs_create_file(stat_type, 0400, root, &d[i],
|
|
&qcom_soc_sleep_stats_fops);
|
|
#if IS_ENABLED(CONFIG_SEC_PM)
|
|
/* Store each system's name */
|
|
strcpy(sys_names[i], stat_type);
|
|
#endif
|
|
|
|
offset += sizeof(struct sleep_stats);
|
|
if (d[i].appended_stats_avail)
|
|
offset += sizeof(struct appended_stats);
|
|
}
|
|
}
|
|
|
|
static void qcom_create_subsystem_stat_files(struct dentry *root,
|
|
const struct stats_config *config,
|
|
struct device_node *node)
|
|
{
|
|
int i, j, n_subsystems;
|
|
const char *name;
|
|
|
|
if (!config->subsystem_stats_in_smem)
|
|
return;
|
|
|
|
n_subsystems = of_property_count_strings(node, "ss-name");
|
|
if (n_subsystems < 0)
|
|
return;
|
|
|
|
for (i = 0; i < n_subsystems; i++) {
|
|
of_property_read_string_index(node, "ss-name", i, &name);
|
|
|
|
for (j = 0; j < ARRAY_SIZE(subsystems); j++) {
|
|
if (!strcmp(subsystems[j].name, name)) {
|
|
debugfs_create_file(subsystems[j].name, 0400, root,
|
|
(void *)&subsystems[j],
|
|
&qcom_subsystem_sleep_stats_fops);
|
|
#if IS_ENABLED(CONFIG_SEC_PM)
|
|
/* Store each subsystem's idx */
|
|
subsys_idx[max_subsys_count++] = j;
|
|
#endif
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#if IS_ENABLED(CONFIG_SEC_PM)
|
|
static char *print_soc_stats(char *buf_ptr, const char *annotation)
|
|
{
|
|
size_t stats_offset = drv->config->stats_offset;
|
|
struct sleep_stats stat;
|
|
char stat_type[sizeof(u32) + 1] = {0};
|
|
unsigned int duration_sec, duration_msec;
|
|
u64 accumulated;
|
|
u32 offset = 0, type;
|
|
bool is_exit = (!strcmp("exit", annotation)) ? true : false;
|
|
int i, j;
|
|
|
|
if (drv->config->dynamic_offset) {
|
|
stats_offset = readl(drv->base + RPM_DYNAMIC_ADDR);
|
|
stats_offset &= RPM_DYNAMIC_ADDR_MASK;
|
|
}
|
|
|
|
buf_ptr += sprintf(buf_ptr, "PM: %s: ", annotation);
|
|
|
|
for (i = 0; i < drv->config->num_records; i++) {
|
|
/* Get soc_stat's name */
|
|
drv->d[i].base = drv->base + offset + stats_offset;
|
|
type = readl(drv->d[i].base);
|
|
|
|
for (j = 0; j < sizeof(u32); j++) {
|
|
stat_type[j] = type & 0xff;
|
|
type = type >> 8;
|
|
}
|
|
strim(stat_type);
|
|
|
|
/* Get soc_stat's sleep info */
|
|
memcpy_fromio(&stat, drv->d[i].base, sizeof(stat));
|
|
|
|
accumulated = stat.accumulated;
|
|
if (stat.last_entered_at > stat.last_exited_at)
|
|
accumulated += arch_timer_read_counter()
|
|
- stat.last_entered_at;
|
|
|
|
/* Check non-sleep issue */
|
|
if (is_exit && accumulated == last_accumulated[i])
|
|
buf_ptr += sprintf(buf_ptr, "*");
|
|
last_accumulated[i] = accumulated;
|
|
|
|
/* Calculate accumulated duration */
|
|
duration_sec = GET_SEC(accumulated);
|
|
duration_msec = GET_MSEC(accumulated);
|
|
|
|
buf_ptr += sprintf(buf_ptr, "%s(%d, %u.%u), ",
|
|
stat_type,
|
|
stat.count,
|
|
duration_sec, duration_msec);
|
|
|
|
/* Move to next soc_stat */
|
|
offset += sizeof(struct sleep_stats);
|
|
if (drv->d[i].appended_stats_avail)
|
|
offset += sizeof(struct appended_stats);
|
|
}
|
|
buf_ptr += sprintf(buf_ptr, "\n");
|
|
|
|
return buf_ptr;
|
|
}
|
|
|
|
static char *print_subsystem_stats(char *buf_ptr, const char *annotation)
|
|
{
|
|
struct subsystem_data *subsystem;
|
|
struct sleep_stats *stat;
|
|
unsigned int duration_sec, duration_msec;
|
|
u64 accumulated;
|
|
int idx, i;
|
|
|
|
#if defined(DSP_SLEEP_DEBUG_ON)
|
|
struct _dsp_entry *dsp_entry = NULL;
|
|
int is_debug_low = 0;
|
|
unsigned int debug_level = 0;
|
|
bool is_exit = (!strcmp("exit", annotation)) ? true : false;
|
|
#endif
|
|
buf_ptr += sprintf(buf_ptr, "PM: %s: ", annotation);
|
|
|
|
for (i = 0; i < max_subsys_count; i++) {
|
|
idx = subsys_idx[i];
|
|
|
|
/* Get each subsystem's info */
|
|
subsystem = &subsystems[idx];
|
|
stat = qcom_smem_get(subsystem->pid, subsystem->smem_item, NULL);
|
|
if (IS_ERR(stat)) {
|
|
pr_err("%s: Failed to get qcom_smem for %s, ret=%ld\n", __func__,
|
|
subsystems[idx].name, PTR_ERR(stat));
|
|
|
|
/* Even though getting info from smem is failed, next subsystem should be checked */
|
|
continue;
|
|
}
|
|
|
|
/* Calculate accumulated duration */
|
|
accumulated = stat->accumulated;
|
|
if (stat->last_entered_at > stat->last_exited_at)
|
|
accumulated += arch_timer_read_counter()
|
|
- stat->last_entered_at;
|
|
|
|
duration_sec = GET_SEC(accumulated);
|
|
duration_msec = GET_MSEC(accumulated);
|
|
#if defined(DSP_SLEEP_DEBUG_ON)
|
|
dsp_entry = (!strcmp(subsystem->name, "cdsp")) ? &DSP_ENTRY[0] : NULL;
|
|
|
|
if (dsp_entry != NULL) {
|
|
if (!is_exit) {
|
|
// entry
|
|
dsp_entry->entry_sec = duration_sec;
|
|
dsp_entry->entry_msec = duration_msec;
|
|
} else {
|
|
//exit
|
|
/* Error detected if exit duration is same as entry */
|
|
if((duration_sec == dsp_entry->entry_sec &&
|
|
duration_msec == dsp_entry->entry_msec) &&
|
|
(duration_sec == dsp_entry->prev_exit_sec &&
|
|
duration_msec == dsp_entry->prev_exit_msec)) {
|
|
struct timespec64 curr_kts = ktime_to_timespec64(ktime_get_boottime());
|
|
if (dsp_entry->interval.tv_sec != 0) {
|
|
time64_t diff_kts = curr_kts.tv_sec - dsp_entry->interval.tv_sec;
|
|
if (diff_kts > 60) { // don't update error count within 1 min
|
|
dsp_entry->error_count++;
|
|
printk("entry error cnt : %llu\n", dsp_entry->error_count);
|
|
dsp_entry->interval = ktime_to_timespec64(ktime_get_boottime());
|
|
}
|
|
} else {
|
|
dsp_entry->interval = ktime_to_timespec64(ktime_get_boottime());
|
|
}
|
|
} else {
|
|
dsp_entry->error_count = 0;
|
|
}
|
|
dsp_entry->prev_exit_sec = duration_sec;
|
|
dsp_entry->prev_exit_msec = duration_msec;
|
|
}
|
|
}
|
|
#endif
|
|
buf_ptr += sprintf(buf_ptr, "%s(%d, %u.%u), ",
|
|
subsystem->name,
|
|
stat->count,
|
|
duration_sec, duration_msec);
|
|
}
|
|
|
|
#if defined(DSP_SLEEP_DEBUG_ON)
|
|
// 0 : CDSP, 1 : ADSP
|
|
for (i = 0; i < sizeof(DSP_ENTRY) / sizeof(struct _dsp_entry); i++) {
|
|
dsp_entry = &DSP_ENTRY[i];
|
|
if(dsp_entry->error_count > MAX_COUNT) {
|
|
debug_level = sec_debug_level();
|
|
|
|
switch (debug_level) {
|
|
case SEC_DEBUG_LEVEL_LOW:
|
|
is_debug_low = 1;
|
|
break;
|
|
case SEC_DEBUG_LEVEL_MID:
|
|
is_debug_low = 0;
|
|
break;
|
|
}
|
|
|
|
if (!is_debug_low) {
|
|
pr_err("entry error cnt : %llu\n", dsp_entry->error_count);
|
|
pr_err("Intentional crash for %s\n", dsp_entry->name);
|
|
BUG_ON(1);
|
|
} else {
|
|
dsp_entry->error_count = 0;
|
|
pr_err("reset entry error cnt : %llu\n", dsp_entry->error_count);
|
|
pr_err("Intentional cdsp subsystem restart\n");
|
|
schedule_work(&dsp_ssr);
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
return buf_ptr;
|
|
}
|
|
|
|
static void sec_sleep_stats_show(const char *annotation)
|
|
{
|
|
char *buf_ptr = buf;
|
|
|
|
buf_ptr = print_soc_stats(buf_ptr, annotation);
|
|
buf_ptr = print_subsystem_stats(buf_ptr, annotation);
|
|
|
|
buf_ptr -= 2;
|
|
buf_ptr += sprintf(buf_ptr, "\n");
|
|
|
|
pr_info("%s", buf);
|
|
}
|
|
|
|
static void qcom_stats_debug_suspend_trace_probe(void *unused,
|
|
const char *action, int val, bool start)
|
|
{
|
|
/*
|
|
* SUSPEND
|
|
* start(1), val(1), action(machine_suspend)
|
|
*/
|
|
if (start && val > 0 && !strcmp("machine_suspend", action))
|
|
sec_sleep_stats_show("entry");
|
|
|
|
/*
|
|
* RESUME
|
|
*start(0), val(1), action(machine_suspend)
|
|
*/
|
|
if (!start && val > 0 && !strcmp("machine_suspend", action))
|
|
sec_sleep_stats_show("exit");
|
|
}
|
|
#endif
|
|
|
|
static int qcom_stats_probe(struct platform_device *pdev)
|
|
{
|
|
void __iomem *reg;
|
|
struct dentry *root;
|
|
const struct stats_config *config;
|
|
struct stats_data *d;
|
|
int i;
|
|
int ret;
|
|
#if defined(DSP_SLEEP_DEBUG_ON)
|
|
priv = NULL;
|
|
cdsp_node = NULL;
|
|
#endif
|
|
#if IS_ENABLED(CONFIG_SEC_PM)
|
|
/* Register callback for cheking subsystem stats */
|
|
ret = register_trace_suspend_resume(
|
|
qcom_stats_debug_suspend_trace_probe, NULL);
|
|
if (ret) {
|
|
pr_err("%s: Failed to register suspend trace callback, ret=%d\n",
|
|
__func__, ret);
|
|
}
|
|
#endif
|
|
|
|
config = device_get_match_data(&pdev->dev);
|
|
if (!config)
|
|
return -ENODEV;
|
|
|
|
reg = devm_platform_get_and_ioremap_resource(pdev, 0, NULL);
|
|
if (IS_ERR(reg))
|
|
return -ENOMEM;
|
|
|
|
d = devm_kcalloc(&pdev->dev, config->num_records,
|
|
sizeof(*d), GFP_KERNEL);
|
|
if (!d)
|
|
return -ENOMEM;
|
|
|
|
drv = devm_kzalloc(&pdev->dev, sizeof(*drv), GFP_KERNEL);
|
|
if (!drv)
|
|
return -ENOMEM;
|
|
|
|
for (i = 0; i < config->num_records; i++)
|
|
d[i].appended_stats_avail = config->appended_stats_avail;
|
|
|
|
root = debugfs_create_dir("qcom_stats", NULL);
|
|
|
|
qcom_create_subsystem_stat_files(root, config, pdev->dev.of_node);
|
|
qcom_create_soc_sleep_stat_files(root, reg, d, config);
|
|
qcom_create_ddr_stat_files(root, reg, d, config);
|
|
qcom_create_island_stat_files(root, reg, d, config);
|
|
qcom_create_vote_info_files(root, reg, d, config);
|
|
|
|
drv->d = d;
|
|
drv->config = config;
|
|
drv->base = reg;
|
|
drv->root = root;
|
|
drv->ddr_freqsync_msg_time = 0;
|
|
mutex_init(&drv->lock);
|
|
|
|
ret = qcom_create_stats_device(drv);
|
|
if (ret)
|
|
goto fail_create_stats_device;
|
|
|
|
if (config->read_ddr_votes && config->ddr_stats_offset) {
|
|
drv->qmp = qmp_get(&pdev->dev);
|
|
if (IS_ERR(drv->qmp)) {
|
|
ret = PTR_ERR(drv->qmp);
|
|
goto fail;
|
|
}
|
|
}
|
|
|
|
subsystem_stats_debug_on = false;
|
|
b_subsystem_stats = devm_kcalloc(&pdev->dev, ARRAY_SIZE(subsystems),
|
|
sizeof(struct sleep_stats), GFP_KERNEL);
|
|
if (!b_subsystem_stats) {
|
|
ret = -ENOMEM;
|
|
goto fail;
|
|
}
|
|
|
|
a_subsystem_stats = devm_kcalloc(&pdev->dev, ARRAY_SIZE(subsystems),
|
|
sizeof(struct sleep_stats), GFP_KERNEL);
|
|
if (!a_subsystem_stats) {
|
|
ret = -ENOMEM;
|
|
goto fail;
|
|
}
|
|
|
|
b_system_stats = devm_kcalloc(&pdev->dev, drv->config->num_records,
|
|
sizeof(struct sleep_stats), GFP_KERNEL);
|
|
if (!b_system_stats) {
|
|
ret = -ENOMEM;
|
|
goto fail;
|
|
}
|
|
|
|
a_system_stats = devm_kcalloc(&pdev->dev, drv->config->num_records,
|
|
sizeof(struct sleep_stats), GFP_KERNEL);
|
|
if (!a_system_stats) {
|
|
ret = -ENOMEM;
|
|
goto fail;
|
|
}
|
|
|
|
platform_set_drvdata(pdev, drv);
|
|
#if defined(DSP_SLEEP_DEBUG_ON)
|
|
priv = platform_get_drvdata(pdev);
|
|
cdsp_node = pdev->dev.of_node;
|
|
#endif
|
|
return 0;
|
|
|
|
fail:
|
|
device_destroy(drv->stats_class, drv->dev_no);
|
|
class_destroy(drv->stats_class);
|
|
cdev_del(&drv->stats_cdev);
|
|
unregister_chrdev_region(drv->dev_no, 1);
|
|
fail_create_stats_device:
|
|
debugfs_remove_recursive(drv->root);
|
|
return ret;
|
|
}
|
|
|
|
static int qcom_stats_remove(struct platform_device *pdev)
|
|
{
|
|
struct stats_drvdata *drv = platform_get_drvdata(pdev);
|
|
|
|
device_destroy(drv->stats_class, drv->dev_no);
|
|
class_destroy(drv->stats_class);
|
|
cdev_del(&drv->stats_cdev);
|
|
unregister_chrdev_region(drv->dev_no, 1);
|
|
|
|
debugfs_remove_recursive(drv->root);
|
|
|
|
#if IS_ENABLED(CONFIG_SEC_PM)
|
|
unregister_trace_suspend_resume(
|
|
qcom_stats_debug_suspend_trace_probe, NULL);
|
|
#endif
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int qcom_stats_suspend(struct device *dev)
|
|
{
|
|
struct stats_drvdata *drv = dev_get_drvdata(dev);
|
|
struct sleep_stats *tmp;
|
|
void __iomem *reg = NULL;
|
|
int i;
|
|
u32 stats_id = 0;
|
|
|
|
if (!subsystem_stats_debug_on)
|
|
return 0;
|
|
|
|
mutex_lock(&sleep_stats_mutex);
|
|
for (i = 0; i < ARRAY_SIZE(subsystems); i++) {
|
|
tmp = qcom_smem_get(subsystems[i].pid, subsystems[i].smem_item, NULL);
|
|
if (IS_ERR(tmp)) {
|
|
subsystems[i].not_present = true;
|
|
continue;
|
|
} else
|
|
subsystems[i].not_present = false;
|
|
qcom_stats_copy(tmp, b_subsystem_stats + i);
|
|
}
|
|
|
|
for (i = 0; i < drv->config->num_records; i++, stats_id++) {
|
|
if (drv->config->num_records > stats_id)
|
|
reg = drv->d[stats_id].base;
|
|
if (reg)
|
|
memcpy_fromio(b_system_stats + i, reg, sizeof(struct sleep_stats));
|
|
}
|
|
mutex_unlock(&sleep_stats_mutex);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int qcom_stats_resume(struct device *dev)
|
|
{
|
|
struct stats_drvdata *drv = dev_get_drvdata(dev);
|
|
struct sleep_stats *tmp;
|
|
void __iomem *reg = NULL;
|
|
int i;
|
|
u32 stats_id = 0;
|
|
|
|
if (!subsystem_stats_debug_on)
|
|
return 0;
|
|
|
|
mutex_lock(&sleep_stats_mutex);
|
|
for (i = 0; i < ARRAY_SIZE(subsystems); i++) {
|
|
if (subsystems[i].not_present)
|
|
continue;
|
|
tmp = qcom_smem_get(subsystems[i].pid, subsystems[i].smem_item, NULL);
|
|
if (IS_ERR(tmp))
|
|
continue;
|
|
qcom_stats_copy(tmp, a_subsystem_stats + i);
|
|
}
|
|
|
|
for (i = 0; i < drv->config->num_records; i++, stats_id++) {
|
|
if (drv->config->num_records > stats_id)
|
|
reg = drv->d[stats_id].base;
|
|
if (reg)
|
|
memcpy_fromio(a_system_stats + i, reg, sizeof(struct sleep_stats));
|
|
}
|
|
mutex_unlock(&sleep_stats_mutex);
|
|
|
|
return 0;
|
|
}
|
|
|
|
static const struct stats_config rpm_data = {
|
|
.stats_offset = 0,
|
|
.num_records = 2,
|
|
.appended_stats_avail = true,
|
|
.dynamic_offset = true,
|
|
.subsystem_stats_in_smem = false,
|
|
};
|
|
|
|
/* Older RPM firmwares have the stats at a fixed offset instead */
|
|
static const struct stats_config rpm_data_dba0 = {
|
|
.stats_offset = 0xdba0,
|
|
.num_records = 2,
|
|
.appended_stats_avail = true,
|
|
.dynamic_offset = false,
|
|
.subsystem_stats_in_smem = false,
|
|
};
|
|
|
|
static const struct stats_config rpmh_data_sdm845 = {
|
|
.stats_offset = 0x48,
|
|
.num_records = 2,
|
|
.appended_stats_avail = false,
|
|
.dynamic_offset = false,
|
|
.subsystem_stats_in_smem = true,
|
|
};
|
|
|
|
static const struct stats_config rpmh_data = {
|
|
.stats_offset = 0x48,
|
|
.ddr_stats_offset = 0xb8,
|
|
.num_records = 3,
|
|
.appended_stats_avail = false,
|
|
.dynamic_offset = false,
|
|
.subsystem_stats_in_smem = true,
|
|
};
|
|
|
|
static const struct stats_config rpmh_v2_data = {
|
|
.stats_offset = 0x48,
|
|
.ddr_stats_offset = 0xb8,
|
|
.cx_vote_offset = 0xb8,
|
|
.num_records = 3,
|
|
.appended_stats_avail = false,
|
|
.dynamic_offset = false,
|
|
.subsystem_stats_in_smem = true,
|
|
.read_ddr_votes = true,
|
|
};
|
|
|
|
static const struct stats_config rpmh_v3_data = {
|
|
.stats_offset = 0x48,
|
|
.ddr_stats_offset = 0xb8,
|
|
.cx_vote_offset = 0xb8,
|
|
.num_records = 3,
|
|
.appended_stats_avail = false,
|
|
.dynamic_offset = false,
|
|
.subsystem_stats_in_smem = true,
|
|
.read_ddr_votes = true,
|
|
.ddr_freq_update = true,
|
|
};
|
|
|
|
static const struct stats_config rpmh_v4_data = {
|
|
.stats_offset = 0x48,
|
|
.ddr_stats_offset = 0xb8,
|
|
.cx_vote_offset = 0xb8,
|
|
.num_records = 3,
|
|
.appended_stats_avail = false,
|
|
.dynamic_offset = false,
|
|
.subsystem_stats_in_smem = true,
|
|
.read_ddr_votes = true,
|
|
.read_ddr_his = true,
|
|
.ddr_freq_update = true,
|
|
.read_cx_final_vote = true,
|
|
.island_stats_avail = true,
|
|
.llc_island_stats_avail = true,
|
|
};
|
|
|
|
static const struct of_device_id qcom_stats_table[] = {
|
|
{ .compatible = "qcom,apq8084-rpm-stats", .data = &rpm_data_dba0 },
|
|
{ .compatible = "qcom,msm8226-rpm-stats", .data = &rpm_data_dba0 },
|
|
{ .compatible = "qcom,msm8916-rpm-stats", .data = &rpm_data_dba0 },
|
|
{ .compatible = "qcom,msm8974-rpm-stats", .data = &rpm_data_dba0 },
|
|
{ .compatible = "qcom,rpm-stats", .data = &rpm_data },
|
|
{ .compatible = "qcom,rpmh-stats", .data = &rpmh_data },
|
|
{ .compatible = "qcom,rpmh-stats-v2", .data = &rpmh_v2_data },
|
|
{ .compatible = "qcom,rpmh-stats-v3", .data = &rpmh_v3_data },
|
|
{ .compatible = "qcom,rpmh-stats-v4", .data = &rpmh_v4_data },
|
|
{ .compatible = "qcom,sdm845-rpmh-stats", .data = &rpmh_data_sdm845 },
|
|
{ }
|
|
};
|
|
MODULE_DEVICE_TABLE(of, qcom_stats_table);
|
|
|
|
static const struct dev_pm_ops qcom_stats_pm_ops = {
|
|
.suspend_late = qcom_stats_suspend,
|
|
.resume_early = qcom_stats_resume,
|
|
};
|
|
|
|
static struct platform_driver qcom_stats = {
|
|
.probe = qcom_stats_probe,
|
|
.remove = qcom_stats_remove,
|
|
.driver = {
|
|
.name = "qcom_stats",
|
|
.of_match_table = qcom_stats_table,
|
|
.pm = &qcom_stats_pm_ops,
|
|
},
|
|
};
|
|
|
|
static int __init qcom_stats_init(void)
|
|
{
|
|
return platform_driver_register(&qcom_stats);
|
|
}
|
|
late_initcall(qcom_stats_init);
|
|
|
|
static void __exit qcom_stats_exit(void)
|
|
{
|
|
platform_driver_unregister(&qcom_stats);
|
|
}
|
|
module_exit(qcom_stats_exit)
|
|
|
|
MODULE_DESCRIPTION("Qualcomm Technologies, Inc. (QTI) Stats driver");
|
|
MODULE_LICENSE("GPL v2");
|