summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPhilipp Tomsich <philipp.tomsich@theobroma-systems.com>2016-09-05 15:10:47 +0200
committerKlaus Goger <klaus.goger@theobroma-systems.com>2016-09-18 15:53:08 +0200
commitd54b8d414515bb28e27cd1934487f751f783ea65 (patch)
tree4191877b255ac761ee45f7f478fc604034a41871
parent2b1c0989dfe4c5ae321500a6a76eabe4e1c67e58 (diff)
clk: sunxi: Add cpu clock support for the sun9i (A80)
The calculation of the PLL factors (N and P) for PLL_C0CPUX and PLL_C1CPUX on the sun9iw1p1 (Allwinner A80) does not fully model the encoding of the factor P (external divider). Consequently, we force the frequency of these clocks to be above 288MHz. The AXI0 and AXI1 clocks are derived from the C0CPUX and C1CPUX mux-outputs using a divider. Qualification of the A80-Q7 module shows that 600MHz is a reasonable limit for both AXI clocks providing good performance while mainaining system reliability. The original Linux 3.4 BSP from Allwinner set these dividers through the CPUS firmware (the settings used are listed in their cpufreq driver) cpufreq support. Our earlier bootloader versions for the A80-Q7 set the dividers to their worst-case settings and kept these unchanged through the system runtime. However, when performing DVFS, this would reduce system performance. We now use a notifier to perform adjustments before and after rate changes to keep the the AXI operating as close to, but below, 600MHz. Full support for the AXI divider uses the following device-tree bindings: - the AXI divider registers need to be modeled as "syscon" devices, as in this example: axi1: c1cpux_clkcfg@06000058 { compatible = "allwinner,sun9i-a80-c1clkcfg", "syscon"; reg = <0x06000058 0x4>; }; - the CPU-clock needs to refer to these entries (and should provide a sane default for the AXI divider to be used during bootup): allwinner,sun9i-a80-clkcfg = <&axi1>; allwinner,sun9i-a80-clkcfg-default = <0x102>; If any of these are missing, some of the functionality (or even all) to keep the AXI clocks within their limits will be disabled. If the bootloader sets us up with a conservative setting, this will still be okay. Signed-off-by: Philipp Tomsich <philipp.tomsich@theobroma-systems.com>
-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