diff options
Diffstat (limited to 'drivers/devfreq/imx8m-ddrc.c')
| -rw-r--r-- | drivers/devfreq/imx8m-ddrc.c | 471 | 
1 files changed, 471 insertions, 0 deletions
| diff --git a/drivers/devfreq/imx8m-ddrc.c b/drivers/devfreq/imx8m-ddrc.c new file mode 100644 index 000000000000..bc82d3653bff --- /dev/null +++ b/drivers/devfreq/imx8m-ddrc.c @@ -0,0 +1,471 @@ +// SPDX-License-Identifier: GPL-2.0 +/* + * Copyright 2019 NXP + */ + +#include <linux/module.h> +#include <linux/device.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/devfreq.h> +#include <linux/pm_opp.h> +#include <linux/clk.h> +#include <linux/clk-provider.h> +#include <linux/arm-smccc.h> + +#define IMX_SIP_DDR_DVFS			0xc2000004 + +/* Query available frequencies. */ +#define IMX_SIP_DDR_DVFS_GET_FREQ_COUNT		0x10 +#define IMX_SIP_DDR_DVFS_GET_FREQ_INFO		0x11 + +/* + * This should be in a 1:1 mapping with devicetree OPPs but + * firmware provides additional info. + */ +struct imx8m_ddrc_freq { +	unsigned long rate; +	unsigned long smcarg; +	int dram_core_parent_index; +	int dram_alt_parent_index; +	int dram_apb_parent_index; +}; + +/* Hardware limitation */ +#define IMX8M_DDRC_MAX_FREQ_COUNT 4 + +/* + * i.MX8M DRAM Controller clocks have 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 dram_pll is used for higher rates and dram_alt is used for lower rates. + * + * Frequency switching is implemented in TF-A (via SMC call) and can change the + * configuration of the clocks, including mux parents. The dram_alt and + * dram_apb clocks are "imx composite" and their parent can change too. + * + * We need to prepare/enable the new mux parents head of switching and update + * their information afterwards. + */ +struct imx8m_ddrc { +	struct devfreq_dev_profile profile; +	struct devfreq *devfreq; + +	/* For frequency switching: */ +	struct clk *dram_core; +	struct clk *dram_pll; +	struct clk *dram_alt; +	struct clk *dram_apb; + +	int freq_count; +	struct imx8m_ddrc_freq freq_table[IMX8M_DDRC_MAX_FREQ_COUNT]; +}; + +static struct imx8m_ddrc_freq *imx8m_ddrc_find_freq(struct imx8m_ddrc *priv, +						    unsigned long rate) +{ +	struct imx8m_ddrc_freq *freq; +	int i; + +	/* +	 * Firmware reports values in MT/s, so we round-down from Hz +	 * Rounding is extra generous to ensure a match. +	 */ +	rate = DIV_ROUND_CLOSEST(rate, 250000); +	for (i = 0; i < priv->freq_count; ++i) { +		freq = &priv->freq_table[i]; +		if (freq->rate == rate || +				freq->rate + 1 == rate || +				freq->rate - 1 == rate) +			return freq; +	} + +	return NULL; +} + +static void imx8m_ddrc_smc_set_freq(int target_freq) +{ +	struct arm_smccc_res res; +	u32 online_cpus = 0; +	int cpu; + +	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(); +} + +static struct clk *clk_get_parent_by_index(struct clk *clk, int index) +{ +	struct clk_hw *hw; + +	hw = clk_hw_get_parent_by_index(__clk_get_hw(clk), index); + +	return hw ? hw->clk : NULL; +} + +static int imx8m_ddrc_set_freq(struct device *dev, struct imx8m_ddrc_freq *freq) +{ +	struct imx8m_ddrc *priv = dev_get_drvdata(dev); +	struct clk *new_dram_core_parent; +	struct clk *new_dram_alt_parent; +	struct clk *new_dram_apb_parent; +	int ret; + +	/* +	 * Fetch new parents +	 * +	 * new_dram_alt_parent and new_dram_apb_parent are optional but +	 * new_dram_core_parent is not. +	 */ +	new_dram_core_parent = clk_get_parent_by_index( +			priv->dram_core, freq->dram_core_parent_index - 1); +	if (!new_dram_core_parent) { +		dev_err(dev, "failed to fetch new dram_core parent\n"); +		return -EINVAL; +	} +	if (freq->dram_alt_parent_index) { +		new_dram_alt_parent = clk_get_parent_by_index( +				priv->dram_alt, +				freq->dram_alt_parent_index - 1); +		if (!new_dram_alt_parent) { +			dev_err(dev, "failed to fetch new dram_alt parent\n"); +			return -EINVAL; +		} +	} else +		new_dram_alt_parent = NULL; + +	if (freq->dram_apb_parent_index) { +		new_dram_apb_parent = clk_get_parent_by_index( +				priv->dram_apb, +				freq->dram_apb_parent_index - 1); +		if (!new_dram_apb_parent) { +			dev_err(dev, "failed to fetch new dram_apb parent\n"); +			return -EINVAL; +		} +	} else +		new_dram_apb_parent = NULL; + +	/* increase reference counts and ensure clks are ON before switch */ +	ret = clk_prepare_enable(new_dram_core_parent); +	if (ret) { +		dev_err(dev, "failed to enable new dram_core parent: %d\n", +			ret); +		goto out; +	} +	ret = clk_prepare_enable(new_dram_alt_parent); +	if (ret) { +		dev_err(dev, "failed to enable new dram_alt parent: %d\n", +			ret); +		goto out_disable_core_parent; +	} +	ret = clk_prepare_enable(new_dram_apb_parent); +	if (ret) { +		dev_err(dev, "failed to enable new dram_apb parent: %d\n", +			ret); +		goto out_disable_alt_parent; +	} + +	imx8m_ddrc_smc_set_freq(freq->smcarg); + +	/* update parents in clk tree after switch. */ +	ret = clk_set_parent(priv->dram_core, new_dram_core_parent); +	if (ret) +		dev_warn(dev, "failed to set dram_core parent: %d\n", ret); +	if (new_dram_alt_parent) { +		ret = clk_set_parent(priv->dram_alt, new_dram_alt_parent); +		if (ret) +			dev_warn(dev, "failed to set dram_alt parent: %d\n", +				 ret); +	} +	if (new_dram_apb_parent) { +		ret = clk_set_parent(priv->dram_apb, new_dram_apb_parent); +		if (ret) +			dev_warn(dev, "failed to set dram_apb parent: %d\n", +				 ret); +	} + +	/* +	 * Explicitly refresh dram PLL rate. +	 * +	 * Even if it's marked with CLK_GET_RATE_NOCACHE the rate will not be +	 * automatically refreshed when clk_get_rate is called on children. +	 */ +	clk_get_rate(priv->dram_pll); + +	/* +	 * clk_set_parent transfer the reference count from old parent. +	 * now we drop extra reference counts used during the switch +	 */ +	clk_disable_unprepare(new_dram_apb_parent); +out_disable_alt_parent: +	clk_disable_unprepare(new_dram_alt_parent); +out_disable_core_parent: +	clk_disable_unprepare(new_dram_core_parent); +out: +	return ret; +} + +static int imx8m_ddrc_target(struct device *dev, unsigned long *freq, u32 flags) +{ +	struct imx8m_ddrc *priv = dev_get_drvdata(dev); +	struct imx8m_ddrc_freq *freq_info; +	struct dev_pm_opp *new_opp; +	unsigned long old_freq, new_freq; +	int ret; + +	new_opp = devfreq_recommended_opp(dev, freq, flags); +	if (IS_ERR(new_opp)) { +		ret = PTR_ERR(new_opp); +		dev_err(dev, "failed to get recommended opp: %d\n", ret); +		return ret; +	} +	dev_pm_opp_put(new_opp); + +	old_freq = clk_get_rate(priv->dram_core); +	if (*freq == old_freq) +		return 0; + +	freq_info = imx8m_ddrc_find_freq(priv, *freq); +	if (!freq_info) +		return -EINVAL; + +	/* +	 * Read back the clk rate to verify switch was correct and so that +	 * we can report it on all error paths. +	 */ +	ret = imx8m_ddrc_set_freq(dev, freq_info); + +	new_freq = clk_get_rate(priv->dram_core); +	if (ret) +		dev_err(dev, "ddrc failed freq switch to %lu from %lu: error %d. now at %lu\n", +			*freq, old_freq, ret, new_freq); +	else if (*freq != new_freq) +		dev_err(dev, "ddrc failed freq update to %lu from %lu, now at %lu\n", +			*freq, old_freq, new_freq); +	else +		dev_dbg(dev, "ddrc freq set to %lu (was %lu)\n", +			*freq, old_freq); + +	return ret; +} + +static int imx8m_ddrc_get_cur_freq(struct device *dev, unsigned long *freq) +{ +	struct imx8m_ddrc *priv = dev_get_drvdata(dev); + +	*freq = clk_get_rate(priv->dram_core); + +	return 0; +} + +static int imx8m_ddrc_get_dev_status(struct device *dev, +				     struct devfreq_dev_status *stat) +{ +	struct imx8m_ddrc *priv = dev_get_drvdata(dev); + +	stat->busy_time = 0; +	stat->total_time = 0; +	stat->current_frequency = clk_get_rate(priv->dram_core); + +	return 0; +} + +static int imx8m_ddrc_init_freq_info(struct device *dev) +{ +	struct imx8m_ddrc *priv = dev_get_drvdata(dev); +	struct arm_smccc_res res; +	int index; + +	/* An error here means DDR DVFS API 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); +	priv->freq_count = res.a0; +	if (priv->freq_count <= 0 || +			priv->freq_count > IMX8M_DDRC_MAX_FREQ_COUNT) +		return -ENODEV; + +	for (index = 0; index < priv->freq_count; ++index) { +		struct imx8m_ddrc_freq *freq = &priv->freq_table[index]; + +		arm_smccc_smc(IMX_SIP_DDR_DVFS, IMX_SIP_DDR_DVFS_GET_FREQ_INFO, +			      index, 0, 0, 0, 0, 0, &res); +		/* Result should be strictly positive */ +		if ((long)res.a0 <= 0) +			return -ENODEV; + +		freq->rate = res.a0; +		freq->smcarg = index; +		freq->dram_core_parent_index = res.a1; +		freq->dram_alt_parent_index = res.a2; +		freq->dram_apb_parent_index = res.a3; + +		/* dram_core has 2 options: dram_pll or dram_alt_root */ +		if (freq->dram_core_parent_index != 1 && +				freq->dram_core_parent_index != 2) +			return -ENODEV; +		/* dram_apb and dram_alt have exactly 8 possible parents */ +		if (freq->dram_alt_parent_index > 8 || +				freq->dram_apb_parent_index > 8) +			return -ENODEV; +		/* dram_core from alt requires explicit dram_alt parent */ +		if (freq->dram_core_parent_index == 2 && +				freq->dram_alt_parent_index == 0) +			return -ENODEV; +	} + +	return 0; +} + +static int imx8m_ddrc_check_opps(struct device *dev) +{ +	struct imx8m_ddrc *priv = dev_get_drvdata(dev); +	struct imx8m_ddrc_freq *freq_info; +	struct dev_pm_opp *opp; +	unsigned long freq; +	int i, opp_count; + +	/* Enumerate DT OPPs and disable those not supported by firmware */ +	opp_count = dev_pm_opp_get_opp_count(dev); +	if (opp_count < 0) +		return opp_count; +	for (i = 0, freq = 0; i < opp_count; ++i, ++freq) { +		opp = dev_pm_opp_find_freq_ceil(dev, &freq); +		if (IS_ERR(opp)) { +			dev_err(dev, "Failed enumerating OPPs: %ld\n", +				PTR_ERR(opp)); +			return PTR_ERR(opp); +		} +		dev_pm_opp_put(opp); + +		freq_info = imx8m_ddrc_find_freq(priv, freq); +		if (!freq_info) { +			dev_info(dev, "Disable unsupported OPP %luHz %luMT/s\n", +					freq, DIV_ROUND_CLOSEST(freq, 250000)); +			dev_pm_opp_disable(dev, freq); +		} +	} + +	return 0; +} + +static void imx8m_ddrc_exit(struct device *dev) +{ +	dev_pm_opp_of_remove_table(dev); +} + +static int imx8m_ddrc_probe(struct platform_device *pdev) +{ +	struct device *dev = &pdev->dev; +	struct imx8m_ddrc *priv; +	const char *gov = DEVFREQ_GOV_USERSPACE; +	int ret; + +	priv = devm_kzalloc(dev, sizeof(*priv), GFP_KERNEL); +	if (!priv) +		return -ENOMEM; + +	platform_set_drvdata(pdev, priv); + +	ret = imx8m_ddrc_init_freq_info(dev); +	if (ret) { +		dev_err(dev, "failed to init firmware freq info: %d\n", ret); +		return ret; +	} + +	priv->dram_core = devm_clk_get(dev, "core"); +	if (IS_ERR(priv->dram_core)) { +		ret = PTR_ERR(priv->dram_core); +		dev_err(dev, "failed to fetch core clock: %d\n", ret); +		return ret; +	} +	priv->dram_pll = devm_clk_get(dev, "pll"); +	if (IS_ERR(priv->dram_pll)) { +		ret = PTR_ERR(priv->dram_pll); +		dev_err(dev, "failed to fetch pll clock: %d\n", ret); +		return ret; +	} +	priv->dram_alt = devm_clk_get(dev, "alt"); +	if (IS_ERR(priv->dram_alt)) { +		ret = PTR_ERR(priv->dram_alt); +		dev_err(dev, "failed to fetch alt clock: %d\n", ret); +		return ret; +	} +	priv->dram_apb = devm_clk_get(dev, "apb"); +	if (IS_ERR(priv->dram_apb)) { +		ret = PTR_ERR(priv->dram_apb); +		dev_err(dev, "failed to fetch apb clock: %d\n", ret); +		return ret; +	} + +	ret = dev_pm_opp_of_add_table(dev); +	if (ret < 0) { +		dev_err(dev, "failed to get OPP table\n"); +		return ret; +	} + +	ret = imx8m_ddrc_check_opps(dev); +	if (ret < 0) +		goto err; + +	priv->profile.polling_ms = 1000; +	priv->profile.target = imx8m_ddrc_target; +	priv->profile.get_dev_status = imx8m_ddrc_get_dev_status; +	priv->profile.exit = imx8m_ddrc_exit; +	priv->profile.get_cur_freq = imx8m_ddrc_get_cur_freq; +	priv->profile.initial_freq = clk_get_rate(priv->dram_core); + +	priv->devfreq = devm_devfreq_add_device(dev, &priv->profile, +						gov, NULL); +	if (IS_ERR(priv->devfreq)) { +		ret = PTR_ERR(priv->devfreq); +		dev_err(dev, "failed to add devfreq device: %d\n", ret); +		goto err; +	} + +	return 0; + +err: +	dev_pm_opp_of_remove_table(dev); +	return ret; +} + +static const struct of_device_id imx8m_ddrc_of_match[] = { +	{ .compatible = "fsl,imx8m-ddrc", }, +	{ /* sentinel */ }, +}; +MODULE_DEVICE_TABLE(of, imx8m_ddrc_of_match); + +static struct platform_driver imx8m_ddrc_platdrv = { +	.probe		= imx8m_ddrc_probe, +	.driver = { +		.name	= "imx8m-ddrc-devfreq", +		.of_match_table = of_match_ptr(imx8m_ddrc_of_match), +	}, +}; +module_platform_driver(imx8m_ddrc_platdrv); + +MODULE_DESCRIPTION("i.MX8M DDR Controller frequency driver"); +MODULE_AUTHOR("Leonard Crestez <leonard.crestez@nxp.com>"); +MODULE_LICENSE("GPL v2"); | 
