[RFC,v3] clk: imx8mm: Add dram freq switch support
diff mbox series

Message ID d2ff5121bced3e5632ff246a51e1f56ee3fe03f9.1563476560.git.leonard.crestez@nxp.com
State Superseded
Headers show
Series
  • [RFC,v3] clk: imx8mm: Add dram freq switch support
Related show

Commit Message

Leonard Crestez July 18, 2019, 7:06 p.m. UTC
Add a compound clock encapsulating dram frequency switch support for
imx8m chips. This allows higher-level DVFS code to manipulate dram
frequency using standard clock framework APIs.

Only some preparation is done inside the kernel, the actual freq switch
is performed from TF-A code which runs from an SRAM area. After the freq
is changed the rates and parents are refreshed on linux side.

A "clk_hw_reinit_parent" function is added to deal with external
reparenting. It's similar to CLK_GET_RATE_NOCACHE but for muxes (and
needs to be called explicitly).

Signed-off-by: Leonard Crestez <leonard.crestez@nxp.com>

---
Objections were raised to earlier hacky versions that this doesn't
really belong inside clk, however if we need to "refresh" clk tree after
a complex frequency switch then it makes sense to do it inside a clk
provider rather than some other random driver.

There are other clk implementations which internally wrap multiple
clocks or deal with DDR or are implemented via SMC: imx_clk_cpu,
tegra/clk-emc and rockchip/clk-ddr.

Out-of-tree ATF patches are required, this branch can be used for
testing: https://github.com/cdleonard/arm-trusted-firmware/commits/imx_2.0.y_caf_busfreq

Firmware API could be adjusted to make this more palatable for
inclusion; for example maybe info about new parents could be provided so
that CLK can enable them in advance? In pratice they're always on.

Also a linux branch with extra patches for testing:
https://github.com/cdleonard/linux/commits/next_imx8mm_busfreq

Changes since v2:
* Remove IRQ handling (thanks Jacky for ATF patch)
* Fetch supported rates from firmware instead of hardcoding imx8mm-evk.
Should now work for all imx8m chips/board/ddr types
* Add clk_hw_reinit_parent instead of explicit set_parent
* Use fewer consumer APIs in provider
* Explicitly mark dram_alt/apb with GET_RATE_NOCACHE
Link to v2: https://patchwork.kernel.org/patch/11021565/

Patch
diff mbox series

diff --git a/drivers/clk/clk.c b/drivers/clk/clk.c
index b1c79a58d734..be9663b1e254 100644
--- a/drivers/clk/clk.c
+++ b/drivers/clk/clk.c
@@ -2388,10 +2388,35 @@  void clk_hw_reparent(struct clk_hw *hw, struct clk_hw *new_parent)
 		return;
 
 	clk_core_reparent(hw->core, !new_parent ? NULL : new_parent->core);
 }
 
+/**
+ * clk_hw_reinit_parent - update clock tree after reparent outside framework
+ * @clk: clock source
+ * @parent: parent clock source
+ *
+ * This function should be used after a clock is reparented externally (for
+ * example with a firmware call or some ASM sequence).
+ *
+ * It will call clk_ops.get_parent again and reassign parents.
+ */
+void clk_hw_reinit_parent(struct clk_hw *hw)
+{
+	struct clk_core *new_parent, *old_parent;
+
+	lockdep_assert_held(&prepare_lock);
+	if (!hw)
+		return;
+
+	new_parent = __clk_init_parent(hw->core);
+	old_parent = __clk_set_parent_before(hw->core, new_parent);
+	clk_core_reparent(hw->core, new_parent);
+	__clk_set_parent_after(hw->core, new_parent, old_parent);
+}
+EXPORT_SYMBOL_GPL(clk_hw_reinit_parent);
+
 /**
  * clk_has_parent - check if a clock is a possible parent for another
  * @clk: clock source
  * @parent: parent clock source
  *
diff --git a/drivers/clk/imx/Makefile b/drivers/clk/imx/Makefile
index 05641c64b317..0fc7195d6d3a 100644
--- a/drivers/clk/imx/Makefile
+++ b/drivers/clk/imx/Makefile
@@ -10,10 +10,11 @@  obj-$(CONFIG_MXC_CLK) += \
 	clk-fixup-div.o \
 	clk-fixup-mux.o \
 	clk-frac-pll.o \
 	clk-gate-exclusive.o \
 	clk-gate2.o \
+	clk-imx8m-dram.o \
 	clk-pfd.o \
 	clk-pfdv2.o \
 	clk-pllv1.o \
 	clk-pllv2.o \
 	clk-pllv3.o \
diff --git a/drivers/clk/imx/clk-imx8m-dram.c b/drivers/clk/imx/clk-imx8m-dram.c
new file mode 100644
index 000000000000..d6971fe72cbe
--- /dev/null
+++ b/drivers/clk/imx/clk-imx8m-dram.c
@@ -0,0 +1,242 @@ 
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Copyright (c) 2019 NXP
+ */
+
+#include <linux/arm-smccc.h>
+#include <linux/clk.h>
+#include <linux/clk-provider.h>
+#include <linux/device.h>
+#include <linux/interrupt.h>
+#include <linux/slab.h>
+#include "clk.h"
+
+#define IMX_SIP_DDR_DVFS                	0xc2000004
+
+/* Values starting from 0 switch to specific frequency */
+#define IMX_SIP_DDR_FREQ_SET_HIGH		0x00
+
+/* Deprecated after moving IRQ handling to ATF */
+#define IMX_SIP_DDR_DVFS_WAIT_CHANGE		0x0F
+
+/* Query available frequencies. */
+#define IMX_SIP_DDR_DVFS_GET_FREQ_COUNT		0x10
+#define IMX_SIP_DDR_DVFS_GET_FREQ_INFO		0x11
+
+/* Hardware limitation */
+#define IMX8M_DRAM_MAX_OPP			4
+
+struct imx8m_dram_opp {
+	unsigned long	rate;
+	unsigned int	smcarg;
+};
+
+/*
+ * This clk wraps the following structure (abridged):
+ *
+ * +----------+       |\            +------+
+ * | dram_pll |-------|M| dram_core |      |
+ * +----------+       |U|---------->| D    |
+ *                 /--|X|           |  D   |
+ *   dram_alt_root |  |/            |   R  |
+ *                 |                |    C |
+ *            +---------+           |      |
+ *            |FIX DIV/4|           |      |
+ *            +---------+           |      |
+ *  composite:     |                |      |
+ * +----------+    |                |      |
+ * | dram_alt |----/                |      |
+ * +----------+                     |      |
+ * | dram_apb |-------------------->|      |
+ * +----------+                     +------+
+ *
+ * The DDR data rate is 4x dram_core
+ *
+ * The APB interface is only used for control registers and can otherwise
+ * be shut off.
+ *
+ * The dram_pll is used for higher rates and dram_alt is used for lower rates.
+ */
+struct dram_clk {
+	struct clk_hw	hw;
+	struct clk_hw	*dram_core;
+	struct clk_hw	*dram_apb;
+	struct clk_hw	*dram_pll;
+	struct clk_hw	*dram_alt;
+	struct clk_hw	*dram_alt_root;
+
+	unsigned int		opp_count;
+	struct imx8m_dram_opp	table[IMX8M_DRAM_MAX_OPP];
+};
+
+static inline struct dram_clk *to_dram_clk(struct clk_hw *hw)
+{
+	return container_of(hw, struct dram_clk, hw);
+}
+
+static int update_bus_freq(int target_freq)
+{
+	struct arm_smccc_res res;
+	u32 online_cpus = 0;
+	int cpu = 0;
+
+	local_irq_disable();
+
+	for_each_online_cpu(cpu)
+		online_cpus |= (1 << (cpu * 8));
+
+	/* change the ddr freqency */
+	arm_smccc_smc(IMX_SIP_DDR_DVFS, target_freq, online_cpus,
+		0, 0, 0, 0, 0, &res);
+
+	local_irq_enable();
+
+	return 0;
+}
+
+/* Round UP */
+static struct imx8m_dram_opp *dram_clk_find_rate(
+		struct dram_clk *priv,
+		unsigned long rate)
+{
+	int i;
+
+	for (i = priv->opp_count - 1; i >= 0; --i)
+		if (priv->table[i].rate >= rate)
+			return &priv->table[i];
+
+	return &priv->table[0];
+}
+
+/* Round UP taking min and max into account */
+static int dram_clk_determine_rate(
+		struct clk_hw *hw,
+		struct clk_rate_request *req)
+{
+	struct dram_clk *priv = to_dram_clk(hw);
+	unsigned long tab_rate;
+	int i;
+
+	for (i = priv->opp_count - 1; i >= 0; --i) {
+		tab_rate = priv->table[i].rate;
+		if (tab_rate >= req->rate &&
+		    tab_rate >= req->min_rate &&
+		    tab_rate <= req->max_rate)
+		{
+			req->rate = tab_rate;
+			return 0;
+		}
+	}
+
+	return -EINVAL;
+}
+
+static int dram_clk_set_rate(
+		struct clk_hw *hw,
+		unsigned long rate,
+		unsigned long parent_rate)
+{
+	struct dram_clk *priv = to_dram_clk(hw);
+	struct imx8m_dram_opp *opp = dram_clk_find_rate(priv, rate);
+	int ret;
+
+	/*
+	 * The actual switch is done inside ATF, here just reload parents.
+	 * all we do here is reload parents
+	 */
+	clk_prepare_enable(priv->dram_alt_root->clk);
+	clk_prepare_enable(priv->dram_pll->clk);
+	ret = update_bus_freq(opp->smcarg);
+	clk_hw_reinit_parent(priv->dram_alt);
+	clk_hw_reinit_parent(priv->dram_apb);
+	clk_hw_reinit_parent(priv->dram_core);
+	clk_disable_unprepare(priv->dram_alt_root->clk);
+	clk_disable_unprepare(priv->dram_pll->clk);
+
+	if (ret == 0)
+		pr_debug("%s freq set to %lu\n", clk_hw_get_name(hw), opp->rate);
+	else
+		pr_err("%s freq set fail: %d\n", clk_hw_get_name(hw), ret);
+
+	return ret;
+}
+
+static unsigned long dram_clk_recalc_rate(struct clk_hw *hw, unsigned long parent_rate)
+{
+	struct dram_clk *priv = to_dram_clk(hw);
+
+	return clk_hw_get_rate(priv->dram_core);
+}
+
+static const struct clk_ops dram_clk_ops = {
+	.determine_rate	= dram_clk_determine_rate,
+	.recalc_rate	= dram_clk_recalc_rate,
+	.set_rate	= dram_clk_set_rate,
+};
+
+struct clk* imx8m_dram_clk(
+		const char *name, const char* parent_name,
+		struct clk_hw* dram_core,
+		struct clk_hw* dram_apb,
+		struct clk_hw* dram_pll,
+		struct clk_hw* dram_alt,
+		struct clk_hw* dram_alt_root)
+{
+	struct arm_smccc_res res;
+	struct dram_clk *priv;
+	struct clk *clk;
+	struct clk_init_data init;
+	int opp_count, index;
+	int err;
+
+	/*
+	 * Count available frequencies
+	 * An error here means DDR DVFS not supported by firmware
+	 */
+	arm_smccc_smc(IMX_SIP_DDR_DVFS, IMX_SIP_DDR_DVFS_GET_FREQ_COUNT,
+			0, 0, 0, 0, 0, 0, &res);
+	opp_count = res.a0;
+	if (opp_count <= 0 || opp_count > IMX8M_DRAM_MAX_OPP)
+		return ERR_PTR(-ENOSYS);
+
+	priv = kzalloc(sizeof(*priv), GFP_KERNEL);
+	if (!priv)
+		return ERR_PTR(-ENOMEM);
+
+	priv->dram_apb = dram_apb;
+	priv->dram_core = dram_core;
+	priv->dram_pll = dram_pll;
+	priv->dram_alt = dram_alt;
+	priv->dram_alt_root = dram_alt_root;
+
+	priv->opp_count = opp_count;
+	for (index = 0; index < opp_count; ++index) {
+		arm_smccc_smc(IMX_SIP_DDR_DVFS, IMX_SIP_DDR_DVFS_GET_FREQ_INFO,
+				index, 0, 0, 0, 0, 0, &res);
+		/* Results should be strictly positive */
+		if ((long)res.a0 <= 0) {
+			err = -ENOSYS;
+			goto err_free_priv;
+		}
+		priv->table[index].smcarg = index;
+		priv->table[index].rate = res.a0 * 250000;
+	}
+
+	init.name = name;
+	init.ops = &dram_clk_ops;
+	init.flags = CLK_IS_CRITICAL;
+	init.parent_names = &parent_name;
+	init.num_parents = 1;
+
+	priv->hw.init = &init;
+	clk = clk_register(NULL, &priv->hw);
+	if (IS_ERR(clk)) {
+		err = PTR_ERR(clk);
+		goto err_free_priv;
+	}
+	return clk;
+
+err_free_priv:
+	kfree(priv);
+	return ERR_PTR(err);
+}
diff --git a/drivers/clk/imx/clk-imx8mm.c b/drivers/clk/imx/clk-imx8mm.c
index 6b8e75df994d..e37442a12fed 100644
--- a/drivers/clk/imx/clk-imx8mm.c
+++ b/drivers/clk/imx/clk-imx8mm.c
@@ -523,12 +523,14 @@  static int __init imx8mm_clocks_init(struct device_node *ccm_node)
 	/* IPG */
 	clks[IMX8MM_CLK_IPG_ROOT] = imx_clk_divider2("ipg_root", "ahb", base + 0x9080, 0, 1);
 	clks[IMX8MM_CLK_IPG_AUDIO_ROOT] = imx_clk_divider2("ipg_audio_root", "audio_ahb", base + 0x9180, 0, 1);
 
 	/* IP */
-	clks[IMX8MM_CLK_DRAM_ALT] = imx8m_clk_composite("dram_alt", imx8mm_dram_alt_sels, base + 0xa000);
-	clks[IMX8MM_CLK_DRAM_APB] = imx8m_clk_composite_critical("dram_apb", imx8mm_dram_apb_sels, base + 0xa080);
+	clks[IMX8MM_CLK_DRAM_ALT] = __imx8m_clk_composite("dram_alt", imx8mm_dram_alt_sels, base + 0xa000,
+			CLK_GET_RATE_NOCACHE);
+	clks[IMX8MM_CLK_DRAM_APB] = __imx8m_clk_composite("dram_apb", imx8mm_dram_apb_sels, base + 0xa080,
+			CLK_GET_RATE_NOCACHE | CLK_IS_CRITICAL);
 	clks[IMX8MM_CLK_VPU_G1] = imx8m_clk_composite("vpu_g1", imx8mm_vpu_g1_sels, base + 0xa100);
 	clks[IMX8MM_CLK_VPU_G2] = imx8m_clk_composite("vpu_g2", imx8mm_vpu_g2_sels, base + 0xa180);
 	clks[IMX8MM_CLK_DISP_DTRC] = imx8m_clk_composite("disp_dtrc", imx8mm_disp_dtrc_sels, base + 0xa200);
 	clks[IMX8MM_CLK_DISP_DC8000] = imx8m_clk_composite("disp_dc8000", imx8mm_disp_dc8000_sels, base + 0xa280);
 	clks[IMX8MM_CLK_PCIE1_CTRL] = imx8m_clk_composite("pcie1_ctrl", imx8mm_pcie1_ctrl_sels, base + 0xa300);
@@ -660,10 +662,18 @@  static int __init imx8mm_clocks_init(struct device_node *ccm_node)
 	clks[IMX8MM_CLK_GPT_3M] = imx_clk_fixed_factor("gpt_3m", "osc_24m", 1, 8);
 
 	clks[IMX8MM_CLK_DRAM_ALT_ROOT] = imx_clk_fixed_factor("dram_alt_root", "dram_alt", 1, 4);
 	clks[IMX8MM_CLK_DRAM_CORE] = imx_clk_mux2_flags("dram_core_clk", base + 0x9800, 24, 1, imx8mm_dram_core_sels, ARRAY_SIZE(imx8mm_dram_core_sels), CLK_IS_CRITICAL);
 
+	clks[IMX8MM_CLK_DRAM] = imx8m_dram_clk(
+			"dram", "dram_core_clk",
+			__clk_get_hw(clks[IMX8MM_CLK_DRAM_CORE]),
+			__clk_get_hw(clks[IMX8MM_CLK_DRAM_APB]),
+			__clk_get_hw(clks[IMX8MM_DRAM_PLL_OUT]),
+			__clk_get_hw(clks[IMX8MM_CLK_DRAM_ALT]),
+			__clk_get_hw(clks[IMX8MM_CLK_DRAM_ALT_ROOT]));
+
 	clks[IMX8MM_CLK_ARM] = imx_clk_cpu("arm", "arm_a53_div",
 					   clks[IMX8MM_CLK_A53_DIV],
 					   clks[IMX8MM_CLK_A53_SRC],
 					   clks[IMX8MM_ARM_PLL_OUT],
 					   clks[IMX8MM_CLK_24M]);
diff --git a/drivers/clk/imx/clk.h b/drivers/clk/imx/clk.h
index d94d9cb079d3..f0f42b3a5d8d 100644
--- a/drivers/clk/imx/clk.h
+++ b/drivers/clk/imx/clk.h
@@ -468,6 +468,15 @@  struct clk *imx8m_clk_composite_flags(const char *name,
 
 struct clk_hw *imx_clk_divider_gate(const char *name, const char *parent_name,
 		unsigned long flags, void __iomem *reg, u8 shift, u8 width,
 		u8 clk_divider_flags, const struct clk_div_table *table,
 		spinlock_t *lock);
+
+struct clk* imx8m_dram_clk(
+		const char *name, const char* parent_name,
+		struct clk_hw* dram_core,
+		struct clk_hw* dram_apb,
+		struct clk_hw* dram_pll,
+		struct clk_hw* dram_alt,
+		struct clk_hw* dram_alt_root);
+
 #endif
diff --git a/include/dt-bindings/clock/imx8mm-clock.h b/include/dt-bindings/clock/imx8mm-clock.h
index 07e6c686f3ef..dde146b923a8 100644
--- a/include/dt-bindings/clock/imx8mm-clock.h
+++ b/include/dt-bindings/clock/imx8mm-clock.h
@@ -246,8 +246,10 @@ 
 #define IMX8MM_CLK_GPIO5_ROOT			227
 
 #define IMX8MM_CLK_SNVS_ROOT			228
 #define IMX8MM_CLK_GIC				229
 
-#define IMX8MM_CLK_END				230
+#define IMX8MM_CLK_DRAM				230
+
+#define IMX8MM_CLK_END				231
 
 #endif
diff --git a/include/linux/clk-provider.h b/include/linux/clk-provider.h
index 2ae7604783dd..f85f1fb8621b 100644
--- a/include/linux/clk-provider.h
+++ b/include/linux/clk-provider.h
@@ -836,10 +836,11 @@  int __clk_mux_determine_rate_closest(struct clk_hw *hw,
 				     struct clk_rate_request *req);
 int clk_mux_determine_rate_flags(struct clk_hw *hw,
 				 struct clk_rate_request *req,
 				 unsigned long flags);
 void clk_hw_reparent(struct clk_hw *hw, struct clk_hw *new_parent);
+void clk_hw_reinit_parent(struct clk_hw *hw);
 void clk_hw_set_rate_range(struct clk_hw *hw, unsigned long min_rate,
 			   unsigned long max_rate);
 
 static inline void __clk_hw_set_clk(struct clk_hw *dst, struct clk_hw *src)
 {