[PATCH 04/11] clk: rockchip: add special cpu clock type

Heiko Stübner heiko at sntech.de
Wed Apr 16 09:39:23 PDT 2014


To change the ARMCLK, Rockchip SoCs at least down to the Cortex-A8 RK2918
need special care. This includes reparenting the the armclk to a secondary
parent for the duration of the parent rate-change and also the re-setting
of tightly coupled children clocks.

Therefore define a special clock type for the cpuclk that attaches a notifier
to the parent pll and handles the necessary steps in a clock notifier.

Also implemented are the necessary functions to handle the cpu clock of rk3188.

Signed-off-by: Heiko Stuebner <heiko at sntech.de>
---
 drivers/clk/rockchip/Makefile  |   1 +
 drivers/clk/rockchip/clk-cpu.c | 434 +++++++++++++++++++++++++++++++++++++++++
 drivers/clk/rockchip/clk.c     |  18 ++
 drivers/clk/rockchip/clk.h     |   9 +
 4 files changed, 462 insertions(+)
 create mode 100644 drivers/clk/rockchip/clk-cpu.c

diff --git a/drivers/clk/rockchip/Makefile b/drivers/clk/rockchip/Makefile
index 2cb9164..7748062 100644
--- a/drivers/clk/rockchip/Makefile
+++ b/drivers/clk/rockchip/Makefile
@@ -5,3 +5,4 @@
 obj-y	+= clk-rockchip.o
 obj-y	+= clk.o
 obj-y	+= clk-pll.o
+obj-y	+= clk-cpu.o
diff --git a/drivers/clk/rockchip/clk-cpu.c b/drivers/clk/rockchip/clk-cpu.c
new file mode 100644
index 0000000..b149d03
--- /dev/null
+++ b/drivers/clk/rockchip/clk-cpu.c
@@ -0,0 +1,434 @@
+/*
+ * Copyright (c) 2014 MundoReader S.L.
+ * Author: Heiko Stuebner <heiko at sntech.de>
+ *
+ * based on clk/samsung/clk-cpu.c
+ * Copyright (c) 2014 Samsung Electronics Co., Ltd.
+ * Author: Thomas Abraham <thomas.ab at samsung.com>
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ *
+ * This file contains the utility functions to register the cpu clocks
+ * for rockchip platforms.
+ */
+
+#include <linux/of.h>
+#include <linux/slab.h>
+#include <linux/io.h>
+#include <linux/clk-provider.h>
+
+#include "clk.h"
+
+/**
+ * struct samsung_cpuclk: information about clock supplied to a CPU core.
+ * @hw:		handle between ccf and cpu clock.
+ * @alt_parent:	alternate parent clock to use when switching the speed
+ *		of the primary parent clock.
+ * @reg:	base register for cpu-clock values.
+ * @clk_nb:	clock notifier registered for changes in clock speed of the
+ *		primary parent clock.
+ * @data:	optional data which the acutal instantiation of this clock
+ *		can use.
+ */
+struct rockchip_cpuclk {
+	struct clk_hw		hw;
+	struct clk		*alt_parent;
+	void __iomem		*reg_base;
+	struct notifier_block	clk_nb;
+	void			*data;
+	spinlock_t		*lock;
+};
+
+#define to_rockchip_cpuclk_hw(hw) container_of(hw, struct rockchip_cpuclk, hw)
+#define to_rockchip_cpuclk_nb(nb) \
+			container_of(nb, struct rockchip_cpuclk, clk_nb)
+
+/**
+ * struct rockchip_cpuclk_soc_data: soc specific data for cpu clocks.
+ * @parser:	pointer to a function that can parse SoC specific data.
+ * @ops:	clock operations to be used for this clock.
+ * @clk_cb:	the clock notifier callback to be called for changes in the
+ *		clock rate of the primary parent clock.
+ *
+ * This structure provides SoC specific data for ARM clocks. Based on
+ * the compatible value of the clock controller node, the value of the
+ * fields in this structure can be populated.
+ */
+struct rockchip_cpuclk_soc_data {
+	int			(*parser)(struct device_node *, void **);
+	const struct clk_ops	*ops;
+	int (*clk_cb)(struct notifier_block *nb, unsigned long evt, void *data);
+};
+
+static unsigned long _calc_div(unsigned long prate, unsigned long drate)
+{
+	unsigned long div = prate / drate;
+	return (!(prate % drate)) ? div-- : div;
+}
+
+/**
+ * struct rk2928_cpuclk_data: config data for rk2928/rk3066/rk3188 cpu clocks.
+ * @prate:	frequency of the parent clock.
+ * @clksel0:	value to be programmed in the clksel0 register.
+ * @clksel1:	value to be programmed in the clksel1 register.
+ *
+ * This structure holds the divider configuration data for for core clocks
+ * directly depending on the cpu clockspeed. The parent frequency at which
+ * these values are vaild is specified in @prate.
+ */
+struct rk2928_cpuclk_data {
+	unsigned long		prate;
+	u32			clksel0;
+	u32			clksel1;
+};
+
+/**
+ * struct rk2928_reg_data: describes register offsets and masks of the cpuclock
+ * @div_core_shift:	core divider offset used to divider the pll value
+ * @div_core_mask:	core divider mask
+ * @mux_core_shift:	offset of the core multiplexer
+ */
+struct rk2928_reg_data {
+	u8		div_core_shift;
+	u32		div_core_mask;
+	u8		mux_core_shift;
+};
+
+/*
+ * This clock notifier is called when the frequency of the parent clock
+ * of cpuclk is to be changed. This notifier handles the setting up all
+ * the divider clocks, remux to temporary parent and handling the safe
+ * frequency levels when using temporary parent.
+ */
+static int rk2928_cpuclk_common_notifier_cb(struct notifier_block *nb,
+					unsigned long event, void *data,
+					const struct rk2928_reg_data *reg_data)
+{
+	struct clk_notifier_data *ndata = data;
+	struct rockchip_cpuclk *cpuclk = to_rockchip_cpuclk_nb(nb);
+	struct rk2928_cpuclk_data *cpuclk_data = cpuclk->data;
+	unsigned long alt_prate, alt_div;
+
+	if (!cpuclk_data)
+		return NOTIFY_BAD;
+
+	switch (event) {
+	case PRE_RATE_CHANGE:
+		alt_prate = clk_get_rate(cpuclk->alt_parent);
+
+		/* pre-rate change. find out the divider values */
+		while (cpuclk_data->prate != ndata->new_rate) {
+			if (cpuclk_data->prate == 0)
+				return NOTIFY_BAD;
+			cpuclk_data++;
+		}
+
+		/*
+		 * if the new and old parent clock speed is less than the
+		 * clock speed of the alternate parent, then it should be
+		 * ensured that at no point the cpuclk speed is more than
+		 * the old_prate until the dividers are set.
+		 */
+		if (ndata->old_rate < alt_prate &&
+					ndata->new_rate < alt_prate) {
+			alt_div = _calc_div(alt_prate, ndata->old_rate);
+			writel_relaxed(HIWORD_UPDATE(alt_div,
+						     reg_data->div_core_mask,
+						     reg_data->div_core_shift),
+				      cpuclk->reg_base + RK2928_CLKSEL_CON(0));
+		}
+
+		/* mux to alternate parent */
+		writel(HIWORD_UPDATE(1, 1, reg_data->mux_core_shift),
+		       cpuclk->reg_base + RK2928_CLKSEL_CON(0));
+
+		/* set new divider values for depending clocks */
+		writel(cpuclk_data->clksel0,
+		       cpuclk->reg_base + RK2928_CLKSEL_CON(0));
+		writel_relaxed(cpuclk_data->clksel1,
+		       cpuclk->reg_base + RK2928_CLKSEL_CON(1));
+		break;
+	case POST_RATE_CHANGE:
+		/* post-rate change event, re-mux back to primary parent */
+		writel(HIWORD_UPDATE(0, 1, reg_data->mux_core_shift),
+		       cpuclk->reg_base + RK2928_CLKSEL_CON(0));
+
+		/* remove any core dividers */
+		writel(HIWORD_UPDATE(0, reg_data->div_core_mask,
+				     reg_data->div_core_shift),
+		       cpuclk->reg_base + RK2928_CLKSEL_CON(0));
+		break;
+	}
+
+	return NOTIFY_OK;
+}
+
+#define RK2928_DIV_CORE_SHIFT		0
+#define RK2928_DIV_CORE_MASK		0x1f
+#define RK2928_MUX_CORE_SHIFT		7
+
+static unsigned long rockchip_rk2928_cpuclk_recalc_rate(struct clk_hw *hw,
+				unsigned long parent_rate)
+{
+	struct rockchip_cpuclk *cpuclk = to_rockchip_cpuclk_hw(hw);
+	u32 clksel0 = readl_relaxed(cpuclk->reg_base + RK2928_CLKSEL_CON(0));
+
+	clksel0 >>= RK2928_DIV_CORE_SHIFT;
+	clksel0 &= RK2928_DIV_CORE_MASK;
+	return parent_rate / (clksel0 + 1);
+}
+
+static const struct clk_ops rk2928_cpuclk_ops = {
+	.recalc_rate = rockchip_rk2928_cpuclk_recalc_rate,
+};
+
+static const struct rk2928_reg_data rk2928_data = {
+	.div_core_shift = RK2928_DIV_CORE_SHIFT,
+	.div_core_mask = RK2928_DIV_CORE_MASK,
+	.mux_core_shift = RK2928_MUX_CORE_SHIFT,
+};
+
+static int rk2928_cpuclk_notifier_cb(struct notifier_block *nb,
+				     unsigned long event, void *data)
+{
+	return rk2928_cpuclk_common_notifier_cb(nb, event, data, &rk2928_data);
+}
+
+static const struct rockchip_cpuclk_soc_data rk2928_cpuclk_soc_data = {
+	.ops = &rk2928_cpuclk_ops,
+	.clk_cb = rk2928_cpuclk_notifier_cb,
+};
+
+#define RK3066_MUX_CORE_SHIFT		8
+
+static const struct rk2928_reg_data rk3066_data = {
+	.div_core_shift = RK2928_DIV_CORE_SHIFT,
+	.div_core_mask = RK2928_DIV_CORE_MASK,
+	.mux_core_shift = RK3066_MUX_CORE_SHIFT,
+};
+
+static int rk3066_cpuclk_notifier_cb(struct notifier_block *nb,
+				     unsigned long event, void *data)
+{
+	return rk2928_cpuclk_common_notifier_cb(nb, event, data, &rk3066_data);
+}
+
+static const struct rockchip_cpuclk_soc_data rk3066_cpuclk_soc_data = {
+	.ops = &rk2928_cpuclk_ops,
+	.clk_cb = rk3066_cpuclk_notifier_cb,
+};
+
+#define RK3188_DIV_CORE_SHIFT		9
+#define RK3188_DIV_CORE_MASK		0x1f
+#define RK3188_MUX_CORE_SHIFT		8
+#define RK3188_DIV_CORE_PERIPH_MASK	0x3
+#define RK3188_DIV_CORE_PERIPH_SHIFT	6
+#define RK3188_DIV_ACLK_CORE_MASK	0x7
+#define RK3188_DIV_ACLK_CORE_SHIFT	3
+
+#define RK3188_CLKSEL0(div_core_periph) \
+		HIWORD_UPDATE(div_core_periph, RK3188_DIV_CORE_PERIPH_MASK,\
+				 RK3188_DIV_CORE_PERIPH_SHIFT)
+#define RK3188_CLKSEL1(div_aclk_core) \
+		HIWORD_UPDATE(div_aclk_core, RK3188_DIV_ACLK_CORE_MASK,\
+				 RK3188_DIV_ACLK_CORE_SHIFT)
+
+static unsigned long rockchip_rk3188_cpuclk_recalc_rate(struct clk_hw *hw,
+				unsigned long parent_rate)
+{
+	struct rockchip_cpuclk *cpuclk = to_rockchip_cpuclk_hw(hw);
+	u32 clksel0 = readl_relaxed(cpuclk->reg_base + RK2928_CLKSEL_CON(0));
+
+	clksel0 >>= RK3188_DIV_CORE_SHIFT;
+	clksel0 &= RK3188_DIV_CORE_MASK;
+	return parent_rate / (clksel0 + 1);
+}
+
+static const struct clk_ops rk3188_cpuclk_ops = {
+	.recalc_rate = rockchip_rk3188_cpuclk_recalc_rate,
+};
+
+static const struct rk2928_reg_data rk3188_data = {
+	.div_core_shift = RK3188_DIV_CORE_SHIFT,
+	.div_core_mask = RK3188_DIV_CORE_MASK,
+	.mux_core_shift = RK3188_MUX_CORE_SHIFT,
+};
+
+static int rk3188_cpuclk_notifier_cb(struct notifier_block *nb,
+				unsigned long event, void *data)
+{
+	return rk2928_cpuclk_common_notifier_cb(nb, event, data, &rk3188_data);
+}
+
+/*
+ * parse divider configuration data from dt for all the cpu clock domain
+ * clocks in rk3188 and compatible SoC's.
+ */
+static int __init rk3188_cpuclk_parser(struct device_node *np, void **data)
+{
+	struct rk2928_cpuclk_data *tdata;
+	int proplen, ret, num_rows, i, col;
+	u32 cfg[10], cells;
+
+	ret = of_property_read_u32(np, "#rockchip,armclk-cells", &cells);
+	if (ret)
+		return -EINVAL;
+
+	proplen = of_property_count_u32_elems(np,
+					      "rockchip,armclk-divider-table");
+	if (proplen < 0)
+		return proplen;
+	if (!proplen || proplen % cells)
+		return -EINVAL;
+
+	num_rows = proplen / cells;
+
+	*data = kzalloc(sizeof(*tdata) * (num_rows + 1), GFP_KERNEL);
+	if (!*data)
+		return -ENOMEM;
+
+	tdata = *data;
+
+	for (i = 0; i < num_rows; i++, tdata++) {
+		for (col = 0; col < cells; col++) {
+			ret = of_property_read_u32_index(np,
+					"rockchip,armclk-divider-table",
+					i * cells + col, &cfg[col]);
+			if (ret) {
+				pr_err("%s: failed to read col %d in row %d of %d\n",
+				       __func__, col, i, num_rows);
+				kfree(*data);
+				return ret;
+			}
+		}
+
+		tdata->prate = cfg[0] * 1000;
+		tdata->clksel0 = RK3188_CLKSEL0(cfg[1]);
+		tdata->clksel1 = RK3188_CLKSEL1(cfg[2]);
+	}
+	tdata->prate = 0;
+	return 0;
+}
+
+static const struct rockchip_cpuclk_soc_data rk3188_cpuclk_soc_data = {
+	.parser = rk3188_cpuclk_parser,
+	.ops = &rk3188_cpuclk_ops,
+	.clk_cb = rk3188_cpuclk_notifier_cb,
+};
+
+static const struct of_device_id rockchip_clock_ids_cpuclk[] = {
+	{ .compatible = "rockchip,rk2928-cru",
+			.data = &rk2928_cpuclk_soc_data, },
+	{ .compatible = "rockchip,rk3066-cru",
+			.data = &rk3066_cpuclk_soc_data, },
+	{ .compatible = "rockchip,rk3188-cru",
+			.data = &rk3188_cpuclk_soc_data, },
+	{ /* sentinel */ },
+};
+
+/**
+ * rockchip_clk_register_cpuclk: register arm clock with ccf.
+ * @lookup_id: cpuclk clock output id for the clock controller.
+ * @parent_names: names of the parent clocks for cpuclk.
+ * @num_parents: number of parent clocks
+ * @base: base address of the clock controller from which cpuclk is generated.
+ * @np: device tree node pointer of the clock controller.
+ * @ops: clock ops for this clock (optional)
+ */
+struct clk *rockchip_clk_register_cpuclk(const char *name,
+			const char **parent_names, unsigned int num_parents,
+			void __iomem *reg_base, struct device_node *np,
+			spinlock_t *lock)
+{
+	struct rockchip_cpuclk *cpuclk;
+	struct clk_init_data init;
+	const struct rockchip_cpuclk_soc_data *soc_data;
+	const struct of_device_id *match;
+	struct clk *clk;
+	int ret;
+
+	if (!np) {
+		pr_err("%s: missing device node\n", __func__);
+		return ERR_PTR(-EINVAL);
+	}
+
+	match = of_match_node(rockchip_clock_ids_cpuclk, np);
+	if (!match) {
+		pr_err("%s: no matching cpuclock found\n", __func__);
+		return ERR_PTR(-EINVAL);
+	}
+
+	soc_data = match->data;
+
+	if (!soc_data->clk_cb)
+		return ERR_PTR(-EINVAL);
+
+	if (num_parents != 2) {
+		pr_err("%s: missing alternative parent clock\n", __func__);
+		return ERR_PTR(-EINVAL);
+	}
+
+	cpuclk = kzalloc(sizeof(*cpuclk), GFP_KERNEL);
+	if (!cpuclk)
+		return ERR_PTR(-ENOMEM);
+
+	init.name = name;
+	init.flags = CLK_SET_RATE_PARENT;
+	init.parent_names = parent_names;
+	init.num_parents = 1;
+	init.ops = soc_data->ops;
+
+	cpuclk->hw.init = &init;
+	cpuclk->reg_base = reg_base;
+	cpuclk->lock = lock;
+
+	cpuclk->alt_parent = __clk_lookup(parent_names[1]);
+	if (!cpuclk->alt_parent) {
+		pr_err("%s: could not lookup alternate parent\n",
+		       __func__);
+		ret = -EINVAL;
+		goto free_cpuclk;
+	}
+
+	ret = clk_prepare_enable(cpuclk->alt_parent);
+	if (ret) {
+		pr_err("%s: could not enable alternate parent\n",
+		       __func__);
+		goto free_cpuclk;
+	}
+
+	if (soc_data->parser) {
+		ret = soc_data->parser(np, &cpuclk->data);
+		if (ret) {
+			pr_err("%s: error %d in parsing %s clock data",
+					__func__, ret, name);
+			ret = -EINVAL;
+			goto free_cpuclk;
+		}
+	}
+
+	cpuclk->clk_nb.notifier_call = soc_data->clk_cb;
+	if (clk_notifier_register(__clk_lookup(parent_names[0]),
+			&cpuclk->clk_nb)) {
+		pr_err("%s: failed to register clock notifier for %s\n",
+				__func__, name);
+		goto free_cpuclk_data;
+	}
+
+	clk = clk_register(NULL, &cpuclk->hw);
+	if (IS_ERR(clk)) {
+		pr_err("%s: could not register cpuclk %s\n", __func__,	name);
+		ret = PTR_ERR(clk);
+		goto free_cpuclk_data;
+	}
+
+	return clk;
+
+free_cpuclk_data:
+	kfree(cpuclk->data);
+free_cpuclk:
+	kfree(cpuclk);
+	return ERR_PTR(ret);
+}
diff --git a/drivers/clk/rockchip/clk.c b/drivers/clk/rockchip/clk.c
index 53e604d..4a626f8 100644
--- a/drivers/clk/rockchip/clk.c
+++ b/drivers/clk/rockchip/clk.c
@@ -74,6 +74,24 @@ void __init rockchip_clk_register_plls(struct rockchip_pll_clock *list,
 	}
 }
 
+void __init rockchip_clk_register_armclk(unsigned int lookup_id,
+			const char *name, const char **parent_names,
+			unsigned int num_parents, void __iomem *reg_base,
+			struct device_node *np)
+{
+	struct clk *clk;
+
+	clk = rockchip_clk_register_cpuclk(name, parent_names, num_parents,
+					   reg_base, np, &clk_lock);
+	if (IS_ERR(clk)) {
+		pr_err("%s: failed to register clock %s\n", __func__,
+			name);
+		return;
+	}
+
+	rockchip_clk_add_lookup(clk, lookup_id);
+}
+
 void __init rockchip_clk_register_mux(struct rockchip_mux_clock *list,
 				      unsigned int nr_clk)
 {
diff --git a/drivers/clk/rockchip/clk.h b/drivers/clk/rockchip/clk.h
index fdb63c2..a86d058 100644
--- a/drivers/clk/rockchip/clk.h
+++ b/drivers/clk/rockchip/clk.h
@@ -103,6 +103,11 @@ struct clk *rockchip_clk_register_pll(struct rockchip_pll_clock *pll_clk,
 				void __iomem *base, void __iomem *reg_lock,
 				spinlock_t *lock);
 
+struct clk *rockchip_clk_register_cpuclk(const char *name,
+			const char **parent_names, unsigned int num_parents,
+			void __iomem *reg_base, struct device_node *np,
+			spinlock_t *lock);
+
 #define PNAME(x) static const char *x[] __initconst
 
 /**
@@ -222,5 +227,9 @@ void rockchip_clk_register_gate(struct rockchip_gate_clock *clk_list,
 				unsigned int nr_clk);
 void rockchip_clk_register_plls(struct rockchip_pll_clock *pll_list,
 				unsigned int nr_pll, void __iomem *reg_lock);
+void rockchip_clk_register_armclk(unsigned int lookup_id,
+			const char *name, const char **parent_names,
+			unsigned int num_parents, void __iomem *reg_base,
+			struct device_node *np);
 
 #endif
-- 
1.9.0





More information about the linux-arm-kernel mailing list