blob: d50110a0d53837e0254478d3e2ece09e7a92e723 [file] [log] [blame]
// SPDX-License-Identifier: GPL-2.0
//
// Copyright (c) 2018 MediaTek Inc.
// Author: Weiyi Lu <weiyi.lu@mediatek.com>
#define pr_fmt(fmt) "[clkchk] " fmt
#include <linux/clk-provider.h>
#include <linux/syscore_ops.h>
#include "clkchk.h"
#define AEE_EXCP_CHECK_PLL_FAIL 0
#define CLKDBG_CCF_API_4_4 1
#define MAX_PLLS 32
#if AEE_EXCP_CHECK_PLL_FAIL
#include <mt-plat/aee.h>
#endif
#if !CLKDBG_CCF_API_4_4
/* backward compatible */
static const char *clk_hw_get_name(const struct clk_hw *hw)
{
return __clk_get_name(hw->clk);
}
static bool clk_hw_is_prepared(const struct clk_hw *hw)
{
return __clk_is_prepared(hw->clk);
}
static bool clk_hw_is_enabled(const struct clk_hw *hw)
{
return __clk_is_enabled(hw->clk);
}
static unsigned long clk_hw_get_rate(const struct clk_hw *hw)
{
return __clk_get_rate(hw->clk);
}
static struct clk_hw *clk_hw_get_parent(const struct clk_hw *hw)
{
return __clk_get_hw(clk_get_parent(hw->clk));
}
#endif /* !CLKDBG_CCF_API_4_4 */
static struct clkchk_cfg_t *clkchk_cfg;
static const char *ccf_state(struct clk_hw *hw)
{
if (__clk_get_enable_count(hw->clk))
return "enabled";
if (clk_hw_is_prepared(hw))
return "prepared";
return "disabled";
}
static void print_enabled_clks(void)
{
const char * const *cn = clkchk_cfg->all_clk_names;
pr_warn("enabled clks:\n");
for (; *cn != NULL; cn++) {
struct clk *c = __clk_lookup(*cn);
struct clk_hw *c_hw = __clk_get_hw(c);
struct clk_hw *p_hw;
if (IS_ERR_OR_NULL(c) || c_hw == NULL)
continue;
p_hw = clk_hw_get_parent(c_hw);
if (p_hw == NULL)
continue;
if (!clk_hw_is_prepared(c_hw) &&
__clk_get_enable_count(c) <= 0U)
continue;
pr_warn("[%-17s: %8s, %3d, %3d, %10ld, %17s]\n",
clk_hw_get_name(c_hw),
ccf_state(c_hw),
clk_hw_is_prepared(c_hw),
__clk_get_enable_count(c),
clk_hw_get_rate(c_hw),
p_hw != NULL ? clk_hw_get_name(p_hw) : "- ");
}
}
static void check_pll_off(void)
{
static struct clk *off_plls[MAX_PLLS];
struct clk **c;
int invalid = 0;
char buf[128] = {0};
int n = 0;
if (off_plls[0] == NULL) {
const char * const *pn = clkchk_cfg->off_pll_names;
struct clk **end = off_plls + MAX_PLLS - 1;
for (c = off_plls; *pn != NULL && c < end; pn++, c++)
*c = __clk_lookup(*pn);
}
for (c = off_plls; *c != NULL; c++) {
struct clk_hw *c_hw = __clk_get_hw(*c);
if (c_hw == NULL)
continue;
if (!clk_hw_is_prepared(c_hw) && !clk_hw_is_enabled(c_hw))
continue;
n += snprintf(buf + n, sizeof(buf) - (size_t)n, "%s ",
clk_hw_get_name(c_hw));
invalid++;
}
if (invalid == 0)
return;
/* invalid. output debug info */
pr_warn("unexpected unclosed PLL: %s\n", buf);
print_enabled_clks();
#if AEE_EXCP_CHECK_PLL_FAIL
if (clkchk_cfg->aee_excp_on_fail)
aee_kernel_exception("clkchk", "unclosed PLL: %s\n", buf);
#endif
if (clkchk_cfg->warn_on_fail)
WARN_ON(true);
}
static int clkchk_syscore_suspend(void)
{
check_pll_off();
return 0;
}
static void clkchk_syscore_resume(void)
{
}
static struct syscore_ops clkchk_syscore_ops = {
.suspend = clkchk_syscore_suspend,
.resume = clkchk_syscore_resume,
};
int clkchk_init(struct clkchk_cfg_t *cfg)
{
const char * const *c;
bool match = false;
if (cfg == NULL || cfg->compatible == NULL
|| cfg->all_clk_names == NULL || cfg->off_pll_names == NULL) {
pr_warn("Invalid clkchk_cfg.\n");
return -EINVAL;
}
clkchk_cfg = cfg;
for (c = cfg->compatible; *c != NULL; c++) {
if (of_machine_is_compatible(*c) != 0) {
match = true;
break;
}
}
if (!match)
return -ENODEV;
register_syscore_ops(&clkchk_syscore_ops);
return 0;
}