kernel-brax3-ubuntu-touch/drivers/clk/mediatek/mtk-pd-chk.c
erascape f319b992b1 kernel-5.15: Initial import brax3 UT kernel
* halium configs enabled

Signed-off-by: erascape <erascape@proton.me>
2025-09-23 15:17:10 +00:00

563 lines
12 KiB
C

// SPDX-License-Identifier: GPL-2.0
//
// Copyright (c) 2021 MediaTek Inc.
// Author: Owen Chen <owen.chen@mediatek.com>
#include <linux/clk-provider.h>
#include <linux/device.h>
#include <linux/interrupt.h>
#include <linux/platform_device.h>
#include <linux/pm_domain.h>
#include <linux/pm_runtime.h>
#include <linux/of.h>
#include <linux/of_address.h>
#include <linux/of_platform.h>
#include <linux/slab.h>
#include <linux/types.h>
#include "mtk-pd-chk.h"
#include "mt-plat/aee.h"
#define TAG "[pdchk] "
#define MAX_PD_NUM 128
#define MAX_BUF_LEN 512
#define POWER_ON_STA 1
#define POWER_OFF_STA 0
#define ENABLE_PD_CHK_CG 0
#define DEVN_LEN 20
#define PD_SUSPEND_ALLOW 0
static struct platform_device *pd_pdev[MAX_PD_NUM];
static struct generic_pm_domain *pds[MAX_PD_NUM];
static struct notifier_block mtk_pd_notifier[MAX_PD_NUM];
static bool pd_evt[MAX_PD_NUM];
static const struct pdchk_ops *pdchk_ops;
static bool bug_on_stat;
static atomic_t check_enabled;
static int hwv_irq;
static bool is_in_pd_list(unsigned int id)
{
if (pdchk_ops == NULL || pdchk_ops->is_in_pd_list == NULL)
return false;
return pdchk_ops->is_in_pd_list(id);
}
static void pd_debug_dump(unsigned int id, unsigned int pwr_sta)
{
if (pdchk_ops == NULL || pdchk_ops->debug_dump == NULL)
return;
pdchk_ops->debug_dump(id, pwr_sta);
}
static void pd_log_dump(unsigned int id, unsigned int pwr_sta)
{
if (pdchk_ops == NULL || pdchk_ops->log_dump == NULL)
return;
pdchk_ops->log_dump(id, pwr_sta);
}
static struct pd_check_swcg *get_subsys_cg(unsigned int id)
{
if (pdchk_ops == NULL || pdchk_ops->get_subsys_cg == NULL)
return NULL;
return pdchk_ops->get_subsys_cg(id);
}
/*
* pm_domain status check
*/
static int pdchk_pd_is_on(int pd_id)
{
struct pd_msk *pm;
if (pdchk_ops == NULL || pdchk_ops->get_pd_pwr_msk == NULL) {
if (pdchk_ops == NULL || pdchk_ops->get_pd_pwr_status == NULL)
return false;
else
return pdchk_ops->get_pd_pwr_status(pd_id);
}
pm = pdchk_ops->get_pd_pwr_msk(pd_id);
return pwr_hw_is_on(pm->sta_type, pm->pwr_val);
}
static const char * const prm_status_name[] = {
"active",
"resuming",
"suspended",
"suspending",
};
static int pdchk_suspend_is_in_usage(struct generic_pm_domain *pd)
{
struct pm_domain_data *pdd;
int valid = 0;
if (IS_ERR_OR_NULL(pd))
return 0;
list_for_each_entry(pdd, &pd->dev_list, list_node) {
struct device *d = pdd->dev;
if (atomic_read(&d->power.usage_count) > 1)
valid = 1;
}
return valid;
}
static void pdchk_dump_enabled_power_domain(struct generic_pm_domain *pd)
{
struct pm_domain_data *pdd;
if (IS_ERR_OR_NULL(pd))
return;
list_for_each_entry(pdd, &pd->dev_list, list_node) {
struct device *d = pdd->dev;
if (!d)
continue;
pr_notice("\t%c (%-80s %3d %3d %3d %3d %3d: %10s)\n",
pm_runtime_active(d) ? '+' : '-',
dev_name(d),
atomic_read(&d->power.usage_count),
d->power.is_noirq_suspended,
d->power.syscore,
d->power.direct_complete,
d->power.must_resume,
//d->power.disable_depth ? "unsupport" :
d->power.runtime_error ? "error" :
d->power.runtime_status < ARRAY_SIZE(prm_status_name) ?
prm_status_name[d->power.runtime_status] : "UFO");
}
}
static bool __check_mtcmos_off(int *pd_id, bool dump_en)
{
int valid = 0;
for (; *pd_id != PD_NULL; pd_id++) {
if (!pdchk_pd_is_on(*pd_id))
continue;
if (dump_en) {
/* dump devicelist belongs to current power domain */
if (pdchk_suspend_is_in_usage(pds[*pd_id]) > 0) {
pr_notice("suspend warning[0m: %s is on\n", pds[*pd_id]->name);
pdchk_dump_enabled_power_domain(pds[*pd_id]);
valid++;
}
}
}
if (valid)
return true;
return false;
}
static bool check_mtcmos_off(void)
{
int *pd_id;
int ret = 0;
if (pdchk_ops == NULL || pdchk_ops->get_off_mtcmos_id == NULL)
goto OUT;
pd_id = pdchk_ops->get_off_mtcmos_id();
ret = __check_mtcmos_off(pd_id, true);
if (pdchk_ops == NULL || pdchk_ops->get_notice_mtcmos_id == NULL)
goto OUT;
pd_id = pdchk_ops->get_notice_mtcmos_id();
__check_mtcmos_off(pd_id, false);
if (ret)
return true;
OUT:
return false;
}
static bool pdchk_retry_bug_on(bool reset_cnt)
{
if (pdchk_ops == NULL || pdchk_ops->pdchk_suspend_retry == NULL)
return true;
return pdchk_ops->pdchk_suspend_retry(reset_cnt);
}
static bool is_mtcmos_chk_bug_on(void)
{
if (pdchk_ops == NULL || pdchk_ops->is_mtcmos_chk_bug_on == NULL)
return false;
return pdchk_ops->is_mtcmos_chk_bug_on();
}
static void pdchk_set_bug_on_stat(bool stat)
{
bug_on_stat = stat;
}
bool pdchk_get_bug_on_stat(void)
{
return bug_on_stat;
}
EXPORT_SYMBOL(pdchk_get_bug_on_stat);
void pdchk_dump_trace_evt(void)
{
if (pdchk_ops == NULL || pdchk_ops->dump_power_event == NULL)
return;
pdchk_ops->dump_power_event();
}
EXPORT_SYMBOL(pdchk_dump_trace_evt);
static int pdchk_dev_pm_suspend(struct device *dev)
{
atomic_inc(&check_enabled);
if (check_mtcmos_off()) {
if (!pdchk_retry_bug_on(false))
return -1;
if (is_mtcmos_chk_bug_on())
pdchk_set_bug_on_stat(true);
#if IS_ENABLED(CONFIG_MTK_AEE_FEATURE)
aee_kernel_warning_api(__FILE__, __LINE__,
DB_OPT_DEFAULT, "pd-chk",
"fail to disable clk/pd in suspend\n");
#endif
} else {
pdchk_retry_bug_on(true);
}
return 0;
}
static int pdchk_dev_pm_resume(struct device *dev)
{
atomic_dec(&check_enabled);
return 0;
}
const struct dev_pm_ops pdchk_dev_pm_ops = {
.suspend = pdchk_dev_pm_suspend,
.resume = pdchk_dev_pm_resume,
};
EXPORT_SYMBOL(pdchk_dev_pm_ops);
#if ENABLE_PD_CHK_CG
static void dump_subsys_reg(unsigned int id)
{
if (pdchk_ops == NULL || pdchk_ops->dump_subsys_reg == NULL)
return;
return pdchk_ops->dump_subsys_reg(id);
}
static unsigned int check_cg_state(struct pd_check_swcg *swcg, unsigned int id)
{
char *buf;
int enable_count = 0;
int len = 0;
if (!swcg)
return 0;
buf = kzalloc(MAX_BUF_LEN, GFP_KERNEL);
while (swcg->name) {
if (!IS_ERR_OR_NULL(swcg->c)) {
if (__clk_is_enabled(swcg->c) > 0) {
len += snprintf(buf + len, PAGE_SIZE - len,
"%s[%-20s]: en\n", __func__,
__clk_get_name(swcg->c));
enable_count++;
}
}
swcg++;
}
if (enable_count > 0)
pr_notice("%s\n", buf);
kfree(buf);
return enable_count;
}
static void mtk_check_subsys_swcg(unsigned int id)
{
struct pd_check_swcg *swcg = get_subsys_cg(id);
int ret;
if (!swcg)
return;
/* check if Subsys CGs are still on */
ret = check_cg_state(swcg, id);
if (ret)
dump_subsys_reg(id);
}
#endif
static void pdchk_trace_power_event(unsigned int id, unsigned int pwr_sta)
{
if (pdchk_ops == NULL || pdchk_ops->trace_power_event == NULL)
return;
pdchk_ops->trace_power_event(id, pwr_sta);
}
/*
* pm_domain event receive
*/
static int mtk_pd_dbg_dump(struct notifier_block *nb,
unsigned long flags, void *data)
{
switch (flags) {
case GENPD_NOTIFY_PRE_ON:
pd_evt[nb->priority] = POWER_ON_STA;
if (atomic_read(&check_enabled)
&& pdchk_suspend_is_in_usage(pds[nb->priority])) {
dump_stack();
pdchk_dump_enabled_power_domain(pds[nb->priority]);
}
break;
case GENPD_NOTIFY_PRE_OFF:
pd_evt[nb->priority] = POWER_OFF_STA;
#if ENABLE_PD_CHK_CG
mtk_check_subsys_swcg(nb->priority);
#endif
if (atomic_read(&check_enabled)
&& pdchk_suspend_is_in_usage(pds[nb->priority])) {
dump_stack();
pdchk_dump_enabled_power_domain(pds[nb->priority]);
}
break;
case GENPD_NOTIFY_ON:
if (pd_evt[nb->priority] == POWER_OFF_STA) {
/* dump devicelist belongs to current power domain */
pdchk_dump_enabled_power_domain(pds[nb->priority]);
pd_debug_dump(nb->priority, PD_PWR_ON);
}
if (pd_evt[nb->priority] == POWER_ON_STA)
pd_log_dump(nb->priority, PD_PWR_ON);
pdchk_trace_power_event(nb->priority, POWER_ON_STA);
break;
case GENPD_NOTIFY_OFF:
if (pd_evt[nb->priority] == POWER_ON_STA) {
/* dump devicelist belongs to current power domain */
pdchk_dump_enabled_power_domain(pds[nb->priority]);
pd_debug_dump(nb->priority, PD_PWR_OFF);
}
if (pd_evt[nb->priority] == POWER_OFF_STA)
pd_log_dump(nb->priority, PD_PWR_OFF);
pdchk_trace_power_event(nb->priority, POWER_OFF_STA);
break;
default:
pr_notice("cannot get flags identify\n");
break;
}
return NOTIFY_OK;
}
#if PD_SUSPEND_ALLOW
static bool pdchk_suspend_allow(unsigned int id)
{
int *pd_id;
if (pdchk_ops == NULL || pdchk_ops->get_suspend_allow_id == NULL)
return false;
pd_id = pdchk_ops->get_suspend_allow_id();
for (; *pd_id != PD_NULL; pd_id++) {
if (*pd_id == id)
return true;
}
return false;
}
#endif
static int set_genpd_notify(void)
{
struct device_node *node = NULL;
unsigned int node_cnt = 0;
int r = 0;
int i = 0;
do {
unsigned int pd_idx = 0;
node = of_find_node_with_property(node, "#power-domain-cells");
if (!node)
break;
do {
struct of_phandle_args pa;
char pd_dev_name[DEVN_LEN];
if (!is_in_pd_list(i)) {
pd_idx++;
i++;
continue;
}
pa.np = node;
pa.args[0] = pd_idx;
pa.args_count = 1;
snprintf(pd_dev_name, DEVN_LEN, "power-domain-chk-%d", i);
pd_pdev[i] = platform_device_alloc(pd_dev_name, 0);
if (!pd_pdev[i]) {
pr_notice("create pd-%d device fail\n", i);
return -ENOMEM;
}
r = of_genpd_add_device(&pa, &pd_pdev[i]->dev);
if (r == -EINVAL) {
pr_notice("add pd device fail\n");
platform_device_put(pd_pdev[i]);
break;
} else if (r != 0)
pr_notice("%s(): of_genpd_add_device(%d)\n", __func__, r);
pds[i] = pd_to_genpd(pd_pdev[i]->dev.pm_domain);
if (IS_ERR(pds[i])) {
pr_notice("pd-%d is err\n", i);
platform_device_put(pd_pdev[i]);
break;
}
mtk_pd_notifier[i].notifier_call = mtk_pd_dbg_dump;
mtk_pd_notifier[i].priority = i;
r = dev_pm_genpd_add_notifier(&pd_pdev[i]->dev, &mtk_pd_notifier[i]);
if (r) {
pr_notice("pd-%s notifier err\n", pds[i]->name);
platform_device_put(pd_pdev[i]);
break;
}
pr_notice("pd-%s add to notifier\n", pds[i]->name);
dev_pm_syscore_device(&pd_pdev[i]->dev, true);
pm_runtime_enable(&pd_pdev[i]->dev);
pm_runtime_get_noresume(&pd_pdev[i]->dev);
pm_runtime_put_noidle(&pd_pdev[i]->dev);
pd_idx++;
i++;
} while (!r && i < MAX_PD_NUM);
node_cnt++;
pr_notice("%d\n", node_cnt);
} while (node && i < MAX_PD_NUM);
if (!node_cnt) {
pr_notice("no power domain defined at dts node\n");
return -ENODEV;
}
return r;
}
static void pdchk_swcg_init_common(struct pd_check_swcg *swcg)
{
if (!swcg)
return;
while (swcg->name) {
struct clk *c = clk_chk_lookup(swcg->name);
if (IS_ERR_OR_NULL(c))
pr_notice("[%17s: NULL]\n", swcg->name);
else
swcg->c = c;
swcg++;
}
}
void pdchk_common_init(const struct pdchk_ops *ops)
{
int i;
pdchk_ops = ops;
for (i = 0; i < MAX_PD_NUM; i++) {
struct pd_check_swcg *swcg = get_subsys_cg(i);
if (!swcg)
continue;
/* fill the 'struct clk *' ptr of every CGs*/
pdchk_swcg_init_common(swcg);
}
atomic_set(&check_enabled, 0);
set_genpd_notify();
}
EXPORT_SYMBOL(pdchk_common_init);
static void pdchk_check_hwv_irq_sta(void)
{
if (pdchk_ops == NULL || pdchk_ops->check_hwv_irq_sta == NULL)
return;
pdchk_ops->check_hwv_irq_sta();
}
static irqreturn_t pdchk_hwv_irq_handler(int irq, void *dev_id)
{
disable_irq_nosync(irq);
if (likely(irq == hwv_irq))
pdchk_check_hwv_irq_sta();
return IRQ_HANDLED;
}
void pdchk_hwv_irq_init(struct platform_device *pdev)
{
int ret;
hwv_irq = platform_get_irq_byname(pdev, "hwv_irq");
if (hwv_irq < 0) {
pr_notice("[pdchk] get hwv irq is not support\n");
} else {
ret = request_irq(hwv_irq, pdchk_hwv_irq_handler,
IRQF_TRIGGER_NONE, "HWV IRQ", NULL);
if (ret < 0) {
pr_notice("[pdchk]hwv require irq fail %d %d\n",
hwv_irq, ret);
} else {
ret = enable_irq_wake(hwv_irq);
if (ret < 0)
pr_notice("[pdchk]hwv wake fail:%d,%d\n",
hwv_irq, ret);
}
}
}
EXPORT_SYMBOL(pdchk_hwv_irq_init);