@@ -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
*
@@ -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 \
new file mode 100644
@@ -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);
+}
@@ -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]);
@@ -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
@@ -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
@@ -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)
{
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/