summaryrefslogtreecommitdiff
path: root/drivers/clk/sunxi/clk-sunxi.c
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/clk/sunxi/clk-sunxi.c')
-rw-r--r--drivers/clk/sunxi/clk-sunxi.c209
1 files changed, 209 insertions, 0 deletions
diff --git a/drivers/clk/sunxi/clk-sunxi.c b/drivers/clk/sunxi/clk-sunxi.c
index 838b22aa8b67..8195f543c2fc 100644
--- a/drivers/clk/sunxi/clk-sunxi.c
+++ b/drivers/clk/sunxi/clk-sunxi.c
@@ -17,12 +17,16 @@
#include <linux/clk.h>
#include <linux/clk-provider.h>
#include <linux/clkdev.h>
+#include <linux/delay.h>
+#include <linux/mfd/syscon.h>
#include <linux/of.h>
#include <linux/of_address.h>
+#include <linux/regmap.h>
#include <linux/reset-controller.h>
#include <linux/slab.h>
#include <linux/spinlock.h>
#include <linux/log2.h>
+#include <asm/io.h>
#include "clk-factors.h"
@@ -200,6 +204,32 @@ static void sun8i_a23_get_pll1_factors(struct factors_request *req)
}
/**
+ * sun9i_a80_get_pllcxcpux_factors() - calculates n and p factors for PLLCxCPUX
+ * PLLCxCPUX rate is calculated as follows
+ * rate = parent_rate * n / p;
+ * parent_rate should always be 24MHz
+ */
+static void sun9i_a80_get_pllcxcpux_factors(struct factors_request *req)
+{
+ /* We can operate only on MHz, this will make our life easier. */
+ u32 freq_mhz = req->rate / 1000000;
+ u32 parent_freq_mhz = req->parent_rate / 1000000;
+
+ /* Round down the frequency to the closest multiple of 24. */
+ u32 round_freq_24 = 24 * (freq_mhz / 24);
+
+ /* We don't model the external divider (P), which is required for
+ rates below 288MHz, so clip to 288MHz to avoid violating the
+ constraints on N. */
+ if (round_freq_24 < 288)
+ round_freq_24 = 288;
+
+ req->rate = round_freq_24 * 1000000;
+ req->n = freq_mhz / parent_freq_mhz;
+ req->p = 0;
+}
+
+/**
* sun4i_get_pll5_factors() - calculates n, k factors for PLL5
* PLL5 rate is calculated as follows
* rate = parent_rate * n * (k + 1)
@@ -453,6 +483,13 @@ static const struct clk_factors_config sun8i_a23_pll1_config = {
.n_start = 1,
};
+static const struct clk_factors_config sun9i_a80_pllcxcpux_config = {
+ .nshift = 8,
+ .nwidth = 8,
+ .pshift = 16,
+ .pwidth = 1,
+};
+
static const struct clk_factors_config sun4i_pll5_config = {
.nshift = 8,
.nwidth = 5,
@@ -513,6 +550,12 @@ static const struct factors_data sun8i_a23_pll1_data __initconst = {
.getter = sun8i_a23_get_pll1_factors,
};
+static const struct factors_data sun9i_a80_pllcxcpux_data __initconst = {
+ .enable = 31,
+ .table = &sun9i_a80_pllcxcpux_config,
+ .getter = sun9i_a80_get_pllcxcpux_factors,
+};
+
static const struct factors_data sun7i_a20_pll4_data __initconst = {
.enable = 31,
.table = &sun4i_pll5_config,
@@ -597,6 +640,15 @@ static void __init sun8i_pll1_clk_setup(struct device_node *node)
CLK_OF_DECLARE(sun8i_pll1, "allwinner,sun8i-a23-pll1-clk",
sun8i_pll1_clk_setup);
+static void __init sun9i_pllcxcpux_clk_setup(struct device_node *node)
+{
+ sunxi_factors_clk_setup(node, &sun9i_a80_pllcxcpux_data);
+}
+CLK_OF_DECLARE(sun9i_pllc0cpux, "allwinner,sun9i-a80-pllc0cpux-clk",
+ sun9i_pllcxcpux_clk_setup);
+CLK_OF_DECLARE(sun9i_pllc1cpux, "allwinner,sun9i-a80-pllc1cpux-clk",
+ sun9i_pllcxcpux_clk_setup);
+
static void __init sun7i_pll4_clk_setup(struct device_node *node)
{
sunxi_factors_clk_setup(node, &sun7i_a20_pll4_data);
@@ -655,6 +707,14 @@ static const struct mux_data sun8i_h3_ahb2_mux_data __initconst = {
.shift = 0,
};
+static const struct mux_data sun9i_c0cpux_mux_data __initconst = {
+ .shift = 0,
+};
+
+static const struct mux_data sun9i_c1cpux_mux_data __initconst = {
+ .shift = 8,
+};
+
static struct clk * __init sunxi_mux_clk_setup(struct device_node *node,
const struct mux_data *data)
{
@@ -731,6 +791,155 @@ static void __init sun8i_ahb2_clk_setup(struct device_node *node)
CLK_OF_DECLARE(sun8i_ahb2, "allwinner,sun8i-h3-ahb2-clk",
sun8i_ahb2_clk_setup);
+/*
+ * This notifier function is called for the pre-rate and post-rate change
+ * notifications of the parent clock of AXI0 (C0CPUX) and AXI1 (C1CPUX)
+ * to ensure that AXI0 and AXI1 remain as high as possible and below 600MHz
+ * when using DVFS.
+ */
+
+#define SUN9I_CxCPUXCLKCFG_AXIMASK (0x7)
+#define SUN9I_AXI_CLKMAX (600000000)
+
+struct sun9i_cpuclk_nb {
+ struct regmap* clkcfg;
+ struct notifier_block clk_nb;
+};
+
+static int sun9i_cpuclk_read_axi_rate(struct regmap *clkcfg)
+{
+ u32 val;
+
+ /* Read the current divider settings */
+ regmap_read(clkcfg, 0, &val);
+ return val & SUN9I_CxCPUXCLKCFG_AXIMASK;
+}
+
+static int sun9i_cpuclk_calc_axi_rate(struct regmap *clkcfg, unsigned long new_rate)
+{
+ /* Calculate the target divider for the new clock rate */
+ return (new_rate - 1) / SUN9I_AXI_CLKMAX;
+}
+
+static int sun9i_cpuclk_pre_rate_change(struct regmap *clkcfg, struct clk_notifier_data *ndata)
+{
+ u32 old = sun9i_cpuclk_read_axi_rate(clkcfg);
+ u32 new = sun9i_cpuclk_calc_axi_rate(clkcfg, ndata->new_rate);
+
+ /* If the new_rate requires a larger divider (i.e. lower AXI rate), we must
+ change the divider before the parent changes its rate. */
+ if (new > old) {
+ pr_debug("%s: increasing AXI div: 0x%x -> 0x%x\n", __func__, old, new);
+ regmap_update_bits(clkcfg, 0, SUN9I_CxCPUXCLKCFG_AXIMASK, new);
+ }
+
+ return 0;
+}
+
+static int sun9i_cpuclk_post_rate_change(struct regmap *clkcfg, struct clk_notifier_data *ndata)
+{
+ u32 old = sun9i_cpuclk_read_axi_rate(clkcfg);
+ u32 new = sun9i_cpuclk_calc_axi_rate(clkcfg, ndata->new_rate);
+
+ /* If the new_rate supports a smaller divider (i.e. faster AXI rate), we can now
+ change the divider after the parent has completed its rate change. */
+ if (new < old) {
+ pr_debug("%s: decreasing AXI div: 0x%x -> 0x%x\n", __func__, old, new);
+ regmap_update_bits(clkcfg, 0, SUN9I_CxCPUXCLKCFG_AXIMASK, new);
+ }
+
+ return 0;
+}
+
+static int sun9i_cpuclk_notifier_cb(struct notifier_block *nb,
+ unsigned long event, void *data)
+{
+ struct clk_notifier_data *ndata = data;
+ struct sun9i_cpuclk_nb *cpuclk_nb = container_of(nb, struct sun9i_cpuclk_nb, clk_nb);
+ struct regmap *clkcfg = cpuclk_nb->clkcfg;
+ int ret;
+
+ pr_debug("%s: event %lu, old_rate %lu, new_rate: %lu\n",
+ __func__, event, ndata->old_rate, ndata->new_rate);
+
+ if (event == PRE_RATE_CHANGE)
+ ret = sun9i_cpuclk_pre_rate_change(clkcfg, ndata);
+ else if (event == POST_RATE_CHANGE)
+ ret = sun9i_cpuclk_post_rate_change(clkcfg, ndata);
+
+ return notifier_from_errno(ret);
+}
+
+static void __init sun9i_cxcpux_notifier_setup(struct device_node *node, struct clk* clk)
+{
+ struct sun9i_cpuclk_nb *nb;
+ struct regmap *clkcfg;
+ u32 val;
+ int ret;
+
+ clkcfg = syscon_regmap_lookup_by_phandle(node, "allwinner,sun9i-a80-clkcfg");
+ if (!clkcfg) {
+ pr_warn("%s: could not retrieve CLKCFG via allwinner,sun9i-a80-clkcfg property\n",
+ of_node_full_name(node));
+ return;
+ }
+
+ if (of_property_read_u32(node, "allwinner,sun9i-a80-clkcfg-default", &val))
+ pr_warn("%s: no defaults (allwinner,sun9i-a80-clkcfg-default) for CLKCFG\n",
+ of_node_full_name(node));
+ else
+ regmap_write(clkcfg, 0, val);
+
+ nb = kzalloc(sizeof(*nb), GFP_KERNEL);
+ if (!nb) {
+ pr_err("%s: failed to allocate notifier block for clk rate changes\n",
+ of_node_full_name(node));
+ return;
+ }
+ nb->clkcfg = clkcfg;
+ nb->clk_nb.notifier_call = sun9i_cpuclk_notifier_cb;
+ ret = clk_notifier_register(clk, &nb->clk_nb);
+ if (ret) {
+ pr_err("%s: failed to register clock notifier\n",
+ of_node_full_name(node));
+ kfree(nb);
+ return;
+ }
+}
+
+static void __init sun9i_c0cpux_clk_setup(struct device_node *node)
+{
+ struct clk *clk;
+
+ clk = sunxi_mux_clk_setup(node, &sun9i_c0cpux_mux_data);
+ if (!clk)
+ return;
+
+ sun9i_cxcpux_notifier_setup(node, clk);
+
+ /* Protect CPU clock */
+ __clk_get(clk);
+ clk_prepare_enable(clk);
+}
+CLK_OF_DECLARE(sun9i_c0cpux, "allwinner,sun9i-a80-c0cpux-clk",
+ sun9i_c0cpux_clk_setup);
+
+static void __init sun9i_c1cpux_clk_setup(struct device_node *node)
+{
+ struct clk *clk;
+
+ clk = sunxi_mux_clk_setup(node, &sun9i_c1cpux_mux_data);
+ if (!clk)
+ return;
+
+ sun9i_cxcpux_notifier_setup(node, clk);
+
+ /* Protect CPU clock */
+ __clk_get(clk);
+ clk_prepare_enable(clk);
+}
+CLK_OF_DECLARE(sun9i_c1cpux, "allwinner,sun9i-a80-c1cpux-clk",
+ sun9i_c1cpux_clk_setup);
/**
* sunxi_divider_clk_setup() - Setup function for simple divider clocks