// SPDX-License-Identifier: GPL-2.0 // // Copyright (c) 2021 MediaTek Inc. // Author: Owen Chen #include #include #include #include #include #include #include #include #include #include #include #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);