// SPDX-License-Identifier: GPL-2.0 // // Copyright (c) 2020 MediaTek Inc. // Author: Owen Chen #define pr_fmt(fmt) "[clkchk] " fmt #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "clk-mtk.h" #include "clkchk.h" #include "mt-plat/aee.h" #define MAX_CLK_NUM 1024 #define PLL_LEN 20 #define INV_MSK 0xFFFFFFFF #define PWR_STA_BIT BIT(30) #define PWR_STA_2ND_BIT BIT(31) bool check_bypass_status; void __attribute__((weak)) clkchk_set_cfg(void) { } static const struct clkchk_ops *clkchk_ops; static struct notifier_block mtk_clkchk_notifier; static int hwv_irq; void set_clkchk_ops(const struct clkchk_ops *ops) { clkchk_ops = ops; } EXPORT_SYMBOL(set_clkchk_ops); /* * for mtcmos debug */ bool is_valid_reg(void __iomem *addr) { #if IS_ENABLED(CONFIG_64BIT) return ((u64)addr & 0xf0000000) != 0UL || (((u64)addr >> 32U) & 0xf0000000) != 0UL; #else return ((u32)addr & 0xf0000000) != 0U; #endif } EXPORT_SYMBOL(is_valid_reg); const struct regname *get_all_regnames(void) { if (clkchk_ops == NULL || clkchk_ops->get_all_regnames == NULL) return NULL; return clkchk_ops->get_all_regnames(); } EXPORT_SYMBOL(get_all_regnames); void clkchk_devapc_dump(void) { if (clkchk_ops == NULL || clkchk_ops->devapc_dump == NULL) return; clkchk_ops->devapc_dump(); } EXPORT_SYMBOL(clkchk_devapc_dump); static const char *get_provider_name(struct device_node *node, u32 *cells) { const char *name; const char *p; u32 cc = 0; if (of_property_read_u32(node, "#clock-cells", &cc) != 0) cc = 0; if (cells != NULL) *cells = cc; if (cc == 0U) { if (of_property_read_string(node, "clock-output-names", &name) < 0) name = node->name; return name; } if (of_property_read_string(node, "compatible", &name) < 0) name = node->name; p = strchr(name, (int)'-'); if (p != NULL) return p + 1; else return name; } static void setup_provider_clk(struct provider_clk *pvdck) { const char *pvdname = pvdck->provider_name; struct pvd_msk *pm; if (!pvdname) return; if (clkchk_ops == NULL || clkchk_ops->get_pvd_pwr_mask == NULL) { pvdck->pwr_mask = INV_MSK; return; } pm = clkchk_ops->get_pvd_pwr_mask(); for (; pm->pvdname != NULL; pm++) { if (!strcmp(pvdname, pm->pvdname)) { pvdck->pwr_mask = pm->pwr_mask; pvdck->sta_type = pm->sta_type; return; } } pvdck->pwr_mask = INV_MSK; } struct provider_clk *get_all_provider_clks(void) { static struct provider_clk provider_clks[MAX_CLK_NUM]; struct device_node *node = NULL; unsigned int n = 0; if (provider_clks[0].ck != NULL) return provider_clks; do { const char *node_name; u32 cells; node = of_find_node_with_property(node, "#clock-cells"); if (node == NULL) break; node_name = get_provider_name(node, &cells); if (cells != 0U) { unsigned int i; for (i = 0; i < MAX_CLK_NUM; i++) { struct of_phandle_args pa; struct clk *ck; pa.np = node; pa.args[0] = i; pa.args_count = 1; ck = of_clk_get_from_provider(&pa); if (PTR_ERR(ck) == -EINVAL) break; else if (IS_ERR_OR_NULL(ck)) continue; provider_clks[n].ck = ck; provider_clks[n].ck_name = __clk_get_name(ck); provider_clks[n].idx = i; provider_clks[n].provider_name = node_name; setup_provider_clk(&provider_clks[n]); ++n; } } } while (node != NULL && n < MAX_CLK_NUM); return provider_clks; } EXPORT_SYMBOL(get_all_provider_clks); static struct provider_clk *__clk_chk_lookup_pvdck(const char *name) { struct provider_clk *pvdck = get_all_provider_clks(); for (; pvdck->ck != NULL; pvdck++) { if (!strcmp(pvdck->ck_name, name)) return pvdck; } return NULL; } static struct clk *__clk_chk_lookup(const char *name) { struct provider_clk *pvdck = __clk_chk_lookup_pvdck(name); if (pvdck) return pvdck->ck; return NULL; } struct clk *clk_chk_lookup(const char *name) { return __clk_chk_lookup(name); } EXPORT_SYMBOL(clk_chk_lookup); static s32 *read_spm_pwr_status_array(void) { if (clkchk_ops == NULL || clkchk_ops->get_spm_pwr_status_array == NULL) return ERR_PTR(-EINVAL); return clkchk_ops->get_spm_pwr_status_array(); } int pwr_hw_is_on(enum PWR_STA_TYPE type, s32 val) { u32 *pval = read_spm_pwr_status_array(); u32 sta = 0; if (type == PWR_STA) { if ((pval[PWR_STA] & val) != val && (pval[PWR_STA2] & val) != val) return 0; else if ((pval[PWR_STA] & val) == val && (pval[PWR_STA2] & val) == val) return 1; else return -1; } else if (type == PWR_CON_STA) { if (clkchk_ops == NULL || clkchk_ops->get_spm_pwr_status == NULL) { if (clkchk_ops != NULL && clkchk_ops->get_pwr_status != NULL) sta = clkchk_ops->get_pwr_status(val); } else sta = clkchk_ops->get_spm_pwr_status(val); if ((sta & PWR_STA_BIT) != PWR_STA_BIT && (sta & PWR_STA_2ND_BIT) != PWR_STA_2ND_BIT) return 0; else if ((sta & PWR_STA_BIT) == PWR_STA_BIT && (sta & PWR_STA_2ND_BIT) == PWR_STA_2ND_BIT) return 1; else return -1; } else { if ((pval[type] & val) == val) return 1; else if ((pval[type] & val) != 0) return -1; else return 0; } } EXPORT_SYMBOL(pwr_hw_is_on); int clkchk_pvdck_is_on(struct provider_clk *pvdck) { int pd_idx; if (!pvdck) return -1; if (clkchk_ops == NULL || clkchk_ops->is_pwr_on == NULL) { if (pvdck->pwr_mask == INV_MSK) { if (clkchk_ops == NULL || clkchk_ops->get_pvd_pwr_data_idx == NULL) return 0; pd_idx = clkchk_ops->get_pvd_pwr_data_idx(pvdck->provider_name); if (pd_idx >= 0) return pwr_hw_is_on(PWR_CON_STA, pd_idx); else return 1; } if (!pvdck->pwr_mask) { return 1; } return pwr_hw_is_on(pvdck->sta_type, pvdck->pwr_mask); } return clkchk_ops->is_pwr_on(pvdck); } EXPORT_SYMBOL(clkchk_pvdck_is_on); bool clkchk_pvdck_is_prepared(struct provider_clk *pvdck) { struct clk_hw *hw; if (clkchk_pvdck_is_on(pvdck) == 1) { hw = __clk_get_hw(pvdck->ck); if (IS_ERR_OR_NULL(hw)) return false; return clk_hw_is_prepared(hw); } return false; } EXPORT_SYMBOL(clkchk_pvdck_is_prepared); bool clkchk_pvdck_is_enabled(struct provider_clk *pvdck) { struct clk_hw *hw; if (clkchk_pvdck_is_on(pvdck) == 1) { hw = __clk_get_hw(pvdck->ck); if (IS_ERR_OR_NULL(hw)) return false; if (IS_ERR_OR_NULL(clk_hw_get_parent(hw))) return false; if (!clk_hw_is_enabled(clk_hw_get_parent(hw))) return false; return clk_hw_is_enabled(hw); } return false; } EXPORT_SYMBOL(clkchk_pvdck_is_enabled); static const char *ccf_state(struct provider_clk *pvdck) { if (clkchk_pvdck_is_enabled(pvdck)) return "enabled"; if (clkchk_pvdck_is_prepared(pvdck)) return "prepared"; return "disabled"; } static void dump_enabled_clks(struct provider_clk *pvdck) { const char * const *pll_names; const char *c_name; const char *p_name; const char *comp_name; struct clk_hw *c_hw = __clk_get_hw(pvdck->ck); struct clk_hw *p_hw; if (!clkchk_pvdck_is_prepared(pvdck) && !clkchk_pvdck_is_enabled(pvdck)) return; if (clkchk_ops == NULL || clkchk_ops->get_off_pll_names == NULL) return; if (IS_ERR_OR_NULL(c_hw)) return; c_name = clk_hw_get_name(c_hw); p_hw = clk_hw_get_parent(c_hw); if (IS_ERR_OR_NULL(p_hw)) return; p_name = clk_hw_get_name(c_hw); comp_name = clk_hw_get_name(p_hw); while (strcmp(comp_name, "clk26m")) { p_name = p_hw ? clk_hw_get_name(p_hw) : NULL; p_hw = clk_hw_get_parent(p_hw); if (IS_ERR_OR_NULL(p_hw)) return; comp_name = clk_hw_get_name(p_hw); } pll_names = clkchk_ops->get_off_pll_names(); for (; *pll_names != NULL && p_name != NULL; pll_names++) { if (!strncmp(p_name, *pll_names, PLL_LEN)) { p_hw = clk_hw_get_parent(c_hw); pr_notice("[%-21s: %8s, %3d, %3d, %10ld, %21s]\n", c_name, ccf_state(pvdck), clkchk_pvdck_is_prepared(pvdck), clkchk_pvdck_is_enabled(pvdck), clk_hw_get_rate(c_hw), p_hw ? clk_hw_get_name(p_hw) : "None"); break; } } } static bool __check_pll_off(const char * const *name) { int valid = 0; for (; *name != NULL; name++) { struct provider_clk *pvdck = __clk_chk_lookup_pvdck(*name); if (!clkchk_pvdck_is_enabled(pvdck)) continue; pr_notice("suspend warning[0m: %s is on\n", *name); if (check_bypass_status) { bool bypass_name_is_equal = false; const char * const *bypass_name; bypass_name = clkchk_ops->get_bypass_pll_name(); for (; *bypass_name != NULL; bypass_name++) if (!strcmp(*bypass_name, *name)) { bypass_name_is_equal = true; pr_notice("clk-chk bypass %s\n", bypass_name); continue; } if (bypass_name_is_equal) continue; } valid++; } if (valid) return true; return false; } static bool check_pll_off(void) { const char * const *name; int ret = 0; if (clkchk_ops == NULL || clkchk_ops->get_off_pll_names == NULL) return false; name = clkchk_ops->get_off_pll_names(); ret = __check_pll_off(name); if (clkchk_ops == NULL || clkchk_ops->get_notice_pll_names == NULL) return false; name = clkchk_ops->get_notice_pll_names(); __check_pll_off(name); if (ret) return true; return false; } static bool is_pll_chk_bug_on(void) { if (clkchk_ops == NULL || clkchk_ops->is_pll_chk_bug_on == NULL) return false; return clkchk_ops->is_pll_chk_bug_on(); } static bool clkchk_retry_bug_on(bool reset_cnt) { if (clkchk_ops == NULL || clkchk_ops->suspend_retry == NULL) return true; return clkchk_ops->suspend_retry(reset_cnt); } /* * clkchk vf table checking */ static int get_vcore_opp(void) { if (clkchk_ops == NULL || clkchk_ops->get_vcore_opp == NULL) return VCORE_NULL; return clkchk_ops->get_vcore_opp(); } static void warn_vcore(int opp, const char *clk_name, int rate, int id) { int vf_opp; if (!clkchk_ops || !clkchk_ops->get_vf_opp) return; vf_opp = clkchk_ops->get_vf_opp(id, opp); if ((opp >= 0) && (id >= 0) && (vf_opp > 0) && ((rate/1000) > vf_opp)) { pr_notice("%s Choose %d FAIL!!!![MAX(%d/%d): %d]\r\n", clk_name, rate/1000, id, opp, vf_opp); BUG_ON(1); } } static int mtk_mux2id(const char **mux_name) { int i = 0; if (!clkchk_ops || !clkchk_ops->get_vf_name || !clkchk_ops->get_vf_num) return -EINVAL; for (i = 0; clkchk_ops->get_vf_num(); i++) { if (strcmp(*mux_name, clkchk_ops->get_vf_name(i)) == 0) return i; } return -EINVAL; } /* The clocks have a mechanism for synchronizing rate changes. */ static int mtk_clk_rate_change(struct notifier_block *nb, unsigned long flags, void *data) { struct clk_notifier_data *ndata = data; struct clk_hw *hw = __clk_get_hw(ndata->clk); const char *clk_name = __clk_get_name(hw->clk); int vcore_opp = get_vcore_opp(); if (vcore_opp == VCORE_NULL) return -EINVAL; if (flags == PRE_RATE_CHANGE && clk_name) { warn_vcore(vcore_opp, clk_name, ndata->new_rate, mtk_mux2id(&clk_name)); } return NOTIFY_OK; } static struct notifier_block mtk_clk_notifier = { .notifier_call = mtk_clk_rate_change, }; int mtk_clk_check_muxes(void) { struct clk *clk; int i; if (!clkchk_ops || !clkchk_ops->get_vf_name || !clkchk_ops->get_vf_num) return -EINVAL; for (i = 0; i < clkchk_ops->get_vf_num(); i++) { const char *name = clkchk_ops->get_vf_name(i); if (!name) continue; pr_notice("name: %s\n", name); clk = __clk_chk_lookup(name); clk_notifier_register(clk, &mtk_clk_notifier); } return 0; } EXPORT_SYMBOL_GPL(mtk_clk_check_muxes); static void clkchk_dump_pll_reg(bool bug_on) { if (clkchk_ops == NULL || clkchk_ops->dump_pll_reg == NULL) return; clkchk_ops->dump_pll_reg(bug_on); } static int clk_chk_dev_pm_suspend(struct device *dev) { struct provider_clk *pvdck = get_all_provider_clks(); if (check_pll_off()) { for (; pvdck->ck != NULL; pvdck++) dump_enabled_clks(pvdck); if (!clkchk_retry_bug_on(false)) return -1; if (is_pll_chk_bug_on() || pdchk_get_bug_on_stat()) { clkchk_dump_pll_reg(false); BUG_ON(1); } #if IS_ENABLED(CONFIG_MTK_AEE_FEATURE) aee_kernel_warning_api(__FILE__, __LINE__, DB_OPT_DEFAULT, "clk-chk", "fail to disable clk/pd in suspend\n"); #endif } else { clkchk_retry_bug_on(true); } return 0; } const struct dev_pm_ops clk_chk_dev_pm_ops = { .suspend_noirq = clk_chk_dev_pm_suspend, .resume_noirq = NULL, }; EXPORT_SYMBOL(clk_chk_dev_pm_ops); /* * for clock exception event handling */ static void clkchk_dump_hwv_history(struct regmap *regmap, u32 id) { if (clkchk_ops == NULL || clkchk_ops->dump_hwv_history == NULL) return; clkchk_ops->dump_hwv_history(regmap, id); } static void clkchk_get_bus_reg(void) { if (clkchk_ops == NULL || clkchk_ops->get_bus_reg == NULL) return; clkchk_ops->get_bus_reg(); } static void clkchk_dump_bus_reg(struct regmap *regmap, u32 ofs) { if (clkchk_ops == NULL || clkchk_ops->dump_bus_reg == NULL) return; clkchk_ops->dump_bus_reg(regmap, ofs); } static bool clkchk_is_cg_chk_pwr_on(void) { if (clkchk_ops == NULL || clkchk_ops->is_cg_chk_pwr_on == NULL) return false; return clkchk_ops->is_cg_chk_pwr_on(); } static void clkchk_cg_chk(const char *name) { struct provider_clk *pvdck; pvdck = __clk_chk_lookup_pvdck(name); if (!clkchk_pvdck_is_on(pvdck)) pr_notice("clk %s access without power on\n", name); } static void clkchk_trace_clk_event(const char *name, unsigned int clk_sta) { if (clkchk_ops == NULL || clkchk_ops->trace_clk_event == NULL) return; clkchk_ops->trace_clk_event(name, clk_sta); } static void clkchk_trigger_trace_dump(unsigned int enable) { if (clkchk_ops == NULL || clkchk_ops->trigger_trace_dump == NULL) return; clkchk_ops->trigger_trace_dump(enable); } static int clkchk_evt_handling(struct notifier_block *nb, unsigned long flags, void *data) { struct clk_event_data *clkd; if (!data) return NOTIFY_OK; clkd = (struct clk_event_data *)data; switch (clkd->event_type) { case CLK_EVT_HWV_CG_TIMEOUT: case CLK_EVT_IPI_CG_TIMEOUT: clkchk_dump_hwv_history(clkd->hwv_regmap, clkd->id); clkchk_dump_bus_reg(clkd->regmap, clkd->ofs); break; case CLK_EVT_HWV_CG_CHK_PWR: if (clkchk_is_cg_chk_pwr_on()) clkchk_cg_chk(clkd->name); break; case CLK_EVT_LONG_BUS_LATENCY: clkchk_get_bus_reg(); break; case CLK_EVT_HWV_PLL_TIMEOUT: clkchk_dump_pll_reg(true); break; case CLK_EVT_CLK_TRACE: clkchk_trace_clk_event(clkd->name, clkd->id); break; case CLK_EVT_TRIGGER_TRACE_DUMP: clkchk_trigger_trace_dump(clkd->id); break; case CLK_EVT_SET_PARENT_TIMEOUT: clkchk_dump_bus_reg(clkd->regmap, clkd->ofs); break; case CLK_EVT_BYPASS_PLL: if (clkd->ofs) check_bypass_status = true; else check_bypass_status = false; break; default: pr_notice("cannot get flags identify\n"); break; } return NOTIFY_OK; } int set_clkchk_notify(void) { int r = 0; mtk_clkchk_notifier.notifier_call = clkchk_evt_handling; r = register_mtk_clk_notifier(&mtk_clkchk_notifier); if (r) pr_err("clk-chk notifier register err(%d)\n", r); return r; } EXPORT_SYMBOL(set_clkchk_notify); static void clkchk_check_hwv_irq_sta(void) { if (clkchk_ops == NULL || clkchk_ops->check_hwv_irq_sta == NULL) return; clkchk_ops->check_hwv_irq_sta(); } static irqreturn_t clkchk_hwv_irq_handler(int irq, void *dev_id) { disable_irq_nosync(irq); if (likely(irq == hwv_irq)) clkchk_check_hwv_irq_sta(); return IRQ_HANDLED; } void clkchk_hwv_irq_init(struct platform_device *pdev) { int ret; hwv_irq = platform_get_irq_byname(pdev, "hwv_irq"); if (hwv_irq < 0) { pr_notice("[clkchk] get hwv irq is not support\n"); } else { ret = request_irq(hwv_irq, clkchk_hwv_irq_handler, IRQF_TRIGGER_NONE, "HWV IRQ", NULL); if (ret < 0) { pr_notice("[clkchk]hwv require irq fail %d %d\n", hwv_irq, ret); } else { ret = enable_irq_wake(hwv_irq); if (ret < 0) pr_notice("[clkchk]hwv wake fail:%d,%d\n", hwv_irq, ret); } } } EXPORT_SYMBOL(clkchk_hwv_irq_init);