162306a36Sopenharmony_ci// SPDX-License-Identifier: GPL-2.0-only
262306a36Sopenharmony_ci/*
362306a36Sopenharmony_ci * Copyright (C) 2012 Calxeda, Inc.
462306a36Sopenharmony_ci *
562306a36Sopenharmony_ci * This driver provides the clk notifier callbacks that are used when
662306a36Sopenharmony_ci * the cpufreq-dt driver changes to frequency to alert the highbank
762306a36Sopenharmony_ci * EnergyCore Management Engine (ECME) about the need to change
862306a36Sopenharmony_ci * voltage. The ECME interfaces with the actual voltage regulators.
962306a36Sopenharmony_ci */
1062306a36Sopenharmony_ci
1162306a36Sopenharmony_ci#define pr_fmt(fmt)	KBUILD_MODNAME ": " fmt
1262306a36Sopenharmony_ci
1362306a36Sopenharmony_ci#include <linux/kernel.h>
1462306a36Sopenharmony_ci#include <linux/module.h>
1562306a36Sopenharmony_ci#include <linux/clk.h>
1662306a36Sopenharmony_ci#include <linux/cpu.h>
1762306a36Sopenharmony_ci#include <linux/err.h>
1862306a36Sopenharmony_ci#include <linux/of.h>
1962306a36Sopenharmony_ci#include <linux/pl320-ipc.h>
2062306a36Sopenharmony_ci#include <linux/platform_device.h>
2162306a36Sopenharmony_ci
2262306a36Sopenharmony_ci#define HB_CPUFREQ_CHANGE_NOTE	0x80000001
2362306a36Sopenharmony_ci#define HB_CPUFREQ_IPC_LEN	7
2462306a36Sopenharmony_ci#define HB_CPUFREQ_VOLT_RETRIES	15
2562306a36Sopenharmony_ci
2662306a36Sopenharmony_cistatic int hb_voltage_change(unsigned int freq)
2762306a36Sopenharmony_ci{
2862306a36Sopenharmony_ci	u32 msg[HB_CPUFREQ_IPC_LEN] = {HB_CPUFREQ_CHANGE_NOTE, freq / 1000000};
2962306a36Sopenharmony_ci
3062306a36Sopenharmony_ci	return pl320_ipc_transmit(msg);
3162306a36Sopenharmony_ci}
3262306a36Sopenharmony_ci
3362306a36Sopenharmony_cistatic int hb_cpufreq_clk_notify(struct notifier_block *nb,
3462306a36Sopenharmony_ci				unsigned long action, void *hclk)
3562306a36Sopenharmony_ci{
3662306a36Sopenharmony_ci	struct clk_notifier_data *clk_data = hclk;
3762306a36Sopenharmony_ci	int i = 0;
3862306a36Sopenharmony_ci
3962306a36Sopenharmony_ci	if (action == PRE_RATE_CHANGE) {
4062306a36Sopenharmony_ci		if (clk_data->new_rate > clk_data->old_rate)
4162306a36Sopenharmony_ci			while (hb_voltage_change(clk_data->new_rate))
4262306a36Sopenharmony_ci				if (i++ > HB_CPUFREQ_VOLT_RETRIES)
4362306a36Sopenharmony_ci					return NOTIFY_BAD;
4462306a36Sopenharmony_ci	} else if (action == POST_RATE_CHANGE) {
4562306a36Sopenharmony_ci		if (clk_data->new_rate < clk_data->old_rate)
4662306a36Sopenharmony_ci			while (hb_voltage_change(clk_data->new_rate))
4762306a36Sopenharmony_ci				if (i++ > HB_CPUFREQ_VOLT_RETRIES)
4862306a36Sopenharmony_ci					return NOTIFY_BAD;
4962306a36Sopenharmony_ci	}
5062306a36Sopenharmony_ci
5162306a36Sopenharmony_ci	return NOTIFY_DONE;
5262306a36Sopenharmony_ci}
5362306a36Sopenharmony_ci
5462306a36Sopenharmony_cistatic struct notifier_block hb_cpufreq_clk_nb = {
5562306a36Sopenharmony_ci	.notifier_call = hb_cpufreq_clk_notify,
5662306a36Sopenharmony_ci};
5762306a36Sopenharmony_ci
5862306a36Sopenharmony_cistatic int __init hb_cpufreq_driver_init(void)
5962306a36Sopenharmony_ci{
6062306a36Sopenharmony_ci	struct platform_device_info devinfo = { .name = "cpufreq-dt", };
6162306a36Sopenharmony_ci	struct device *cpu_dev;
6262306a36Sopenharmony_ci	struct clk *cpu_clk;
6362306a36Sopenharmony_ci	struct device_node *np;
6462306a36Sopenharmony_ci	int ret;
6562306a36Sopenharmony_ci
6662306a36Sopenharmony_ci	if ((!of_machine_is_compatible("calxeda,highbank")) &&
6762306a36Sopenharmony_ci		(!of_machine_is_compatible("calxeda,ecx-2000")))
6862306a36Sopenharmony_ci		return -ENODEV;
6962306a36Sopenharmony_ci
7062306a36Sopenharmony_ci	cpu_dev = get_cpu_device(0);
7162306a36Sopenharmony_ci	if (!cpu_dev) {
7262306a36Sopenharmony_ci		pr_err("failed to get highbank cpufreq device\n");
7362306a36Sopenharmony_ci		return -ENODEV;
7462306a36Sopenharmony_ci	}
7562306a36Sopenharmony_ci
7662306a36Sopenharmony_ci	np = of_node_get(cpu_dev->of_node);
7762306a36Sopenharmony_ci	if (!np) {
7862306a36Sopenharmony_ci		pr_err("failed to find highbank cpufreq node\n");
7962306a36Sopenharmony_ci		return -ENOENT;
8062306a36Sopenharmony_ci	}
8162306a36Sopenharmony_ci
8262306a36Sopenharmony_ci	cpu_clk = clk_get(cpu_dev, NULL);
8362306a36Sopenharmony_ci	if (IS_ERR(cpu_clk)) {
8462306a36Sopenharmony_ci		ret = PTR_ERR(cpu_clk);
8562306a36Sopenharmony_ci		pr_err("failed to get cpu0 clock: %d\n", ret);
8662306a36Sopenharmony_ci		goto out_put_node;
8762306a36Sopenharmony_ci	}
8862306a36Sopenharmony_ci
8962306a36Sopenharmony_ci	ret = clk_notifier_register(cpu_clk, &hb_cpufreq_clk_nb);
9062306a36Sopenharmony_ci	if (ret) {
9162306a36Sopenharmony_ci		pr_err("failed to register clk notifier: %d\n", ret);
9262306a36Sopenharmony_ci		goto out_put_node;
9362306a36Sopenharmony_ci	}
9462306a36Sopenharmony_ci
9562306a36Sopenharmony_ci	/* Instantiate cpufreq-dt */
9662306a36Sopenharmony_ci	platform_device_register_full(&devinfo);
9762306a36Sopenharmony_ci
9862306a36Sopenharmony_ciout_put_node:
9962306a36Sopenharmony_ci	of_node_put(np);
10062306a36Sopenharmony_ci	return ret;
10162306a36Sopenharmony_ci}
10262306a36Sopenharmony_cimodule_init(hb_cpufreq_driver_init);
10362306a36Sopenharmony_ci
10462306a36Sopenharmony_cistatic const struct of_device_id __maybe_unused hb_cpufreq_of_match[] = {
10562306a36Sopenharmony_ci	{ .compatible = "calxeda,highbank" },
10662306a36Sopenharmony_ci	{ .compatible = "calxeda,ecx-2000" },
10762306a36Sopenharmony_ci	{ },
10862306a36Sopenharmony_ci};
10962306a36Sopenharmony_ciMODULE_DEVICE_TABLE(of, hb_cpufreq_of_match);
11062306a36Sopenharmony_ci
11162306a36Sopenharmony_ciMODULE_AUTHOR("Mark Langsdorf <mark.langsdorf@calxeda.com>");
11262306a36Sopenharmony_ciMODULE_DESCRIPTION("Calxeda Highbank cpufreq driver");
11362306a36Sopenharmony_ciMODULE_LICENSE("GPL");
114