1 | // SPDX-License-Identifier: GPL-2.0-only |
2 | /* |
3 | * Copyright (C) 2012 Calxeda, Inc. |
4 | * |
5 | * This driver provides the clk notifier callbacks that are used when |
6 | * the cpufreq-dt driver changes to frequency to alert the highbank |
7 | * EnergyCore Management Engine (ECME) about the need to change |
8 | * voltage. The ECME interfaces with the actual voltage regulators. |
9 | */ |
10 | |
11 | #define pr_fmt(fmt) KBUILD_MODNAME ": " fmt |
12 | |
13 | #include <linux/kernel.h> |
14 | #include <linux/module.h> |
15 | #include <linux/clk.h> |
16 | #include <linux/cpu.h> |
17 | #include <linux/err.h> |
18 | #include <linux/of.h> |
19 | #include <linux/pl320-ipc.h> |
20 | #include <linux/platform_device.h> |
21 | |
22 | #define HB_CPUFREQ_CHANGE_NOTE 0x80000001 |
23 | #define HB_CPUFREQ_IPC_LEN 7 |
24 | #define HB_CPUFREQ_VOLT_RETRIES 15 |
25 | |
26 | static int hb_voltage_change(unsigned int freq) |
27 | { |
28 | u32 msg[HB_CPUFREQ_IPC_LEN] = {HB_CPUFREQ_CHANGE_NOTE, freq / 1000000}; |
29 | |
30 | return pl320_ipc_transmit(data: msg); |
31 | } |
32 | |
33 | static int hb_cpufreq_clk_notify(struct notifier_block *nb, |
34 | unsigned long action, void *hclk) |
35 | { |
36 | struct clk_notifier_data *clk_data = hclk; |
37 | int i = 0; |
38 | |
39 | if (action == PRE_RATE_CHANGE) { |
40 | if (clk_data->new_rate > clk_data->old_rate) |
41 | while (hb_voltage_change(freq: clk_data->new_rate)) |
42 | if (i++ > HB_CPUFREQ_VOLT_RETRIES) |
43 | return NOTIFY_BAD; |
44 | } else if (action == POST_RATE_CHANGE) { |
45 | if (clk_data->new_rate < clk_data->old_rate) |
46 | while (hb_voltage_change(freq: clk_data->new_rate)) |
47 | if (i++ > HB_CPUFREQ_VOLT_RETRIES) |
48 | return NOTIFY_BAD; |
49 | } |
50 | |
51 | return NOTIFY_DONE; |
52 | } |
53 | |
54 | static struct notifier_block hb_cpufreq_clk_nb = { |
55 | .notifier_call = hb_cpufreq_clk_notify, |
56 | }; |
57 | |
58 | static int __init hb_cpufreq_driver_init(void) |
59 | { |
60 | struct platform_device_info devinfo = { .name = "cpufreq-dt" , }; |
61 | struct device *cpu_dev; |
62 | struct clk *cpu_clk; |
63 | struct device_node *np; |
64 | int ret; |
65 | |
66 | if ((!of_machine_is_compatible(compat: "calxeda,highbank" )) && |
67 | (!of_machine_is_compatible(compat: "calxeda,ecx-2000" ))) |
68 | return -ENODEV; |
69 | |
70 | cpu_dev = get_cpu_device(cpu: 0); |
71 | if (!cpu_dev) { |
72 | pr_err("failed to get highbank cpufreq device\n" ); |
73 | return -ENODEV; |
74 | } |
75 | |
76 | np = of_node_get(node: cpu_dev->of_node); |
77 | if (!np) { |
78 | pr_err("failed to find highbank cpufreq node\n" ); |
79 | return -ENOENT; |
80 | } |
81 | |
82 | cpu_clk = clk_get(dev: cpu_dev, NULL); |
83 | if (IS_ERR(ptr: cpu_clk)) { |
84 | ret = PTR_ERR(ptr: cpu_clk); |
85 | pr_err("failed to get cpu0 clock: %d\n" , ret); |
86 | goto out_put_node; |
87 | } |
88 | |
89 | ret = clk_notifier_register(clk: cpu_clk, nb: &hb_cpufreq_clk_nb); |
90 | if (ret) { |
91 | pr_err("failed to register clk notifier: %d\n" , ret); |
92 | goto out_put_node; |
93 | } |
94 | |
95 | /* Instantiate cpufreq-dt */ |
96 | platform_device_register_full(pdevinfo: &devinfo); |
97 | |
98 | out_put_node: |
99 | of_node_put(node: np); |
100 | return ret; |
101 | } |
102 | module_init(hb_cpufreq_driver_init); |
103 | |
104 | static const struct of_device_id __maybe_unused hb_cpufreq_of_match[] = { |
105 | { .compatible = "calxeda,highbank" }, |
106 | { .compatible = "calxeda,ecx-2000" }, |
107 | { }, |
108 | }; |
109 | MODULE_DEVICE_TABLE(of, hb_cpufreq_of_match); |
110 | |
111 | MODULE_AUTHOR("Mark Langsdorf <mark.langsdorf@calxeda.com>" ); |
112 | MODULE_DESCRIPTION("Calxeda Highbank cpufreq driver" ); |
113 | MODULE_LICENSE("GPL" ); |
114 | |