diff options
Diffstat (limited to 'drivers/soc/imx/busfreq-imx8mq.c')
-rw-r--r-- | drivers/soc/imx/busfreq-imx8mq.c | 553 |
1 files changed, 553 insertions, 0 deletions
diff --git a/drivers/soc/imx/busfreq-imx8mq.c b/drivers/soc/imx/busfreq-imx8mq.c new file mode 100644 index 000000000000..4a247a54a0e8 --- /dev/null +++ b/drivers/soc/imx/busfreq-imx8mq.c @@ -0,0 +1,553 @@ +/* + * Copyright 2017 NXP + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + */ + +#include <linux/arm-smccc.h> +#include <linux/busfreq-imx.h> +#include <linux/clk.h> +#include <linux/clk-provider.h> +#include <linux/cpumask.h> +#include <linux/device.h> +#include <linux/module.h> +#include <linux/interrupt.h> +#include <linux/irq.h> +#include <linux/mutex.h> +#include <linux/of.h> +#include <linux/of_address.h> +#include <linux/of_device.h> +#include <linux/platform_device.h> +#include <linux/proc_fs.h> +#include <linux/reboot.h> +#include <linux/sched.h> +#include <linux/slab.h> +#include <linux/smp.h> +#include <linux/suspend.h> + +#define FSL_SIP_DDR_DVFS 0xc2000004 + +#define HIGH_FREQ_3200MTS 0x0 +#define AUDIO_FREQ_400MTS 0x1 +#define LOW_BUS_FREQ_100MTS 0x2 +#define WAIT_BUS_FREQ_DONE 0xf + +static struct device *busfreq_dev; +static int low_bus_freq_mode; +static int audio_bus_freq_mode; +static int high_bus_freq_mode; +static int bus_freq_scaling_initialized; +static int bus_freq_scaling_is_active; +static int high_bus_count, audio_bus_count, low_bus_count; +static int cur_bus_freq_mode; +static int busfreq_suspended; +static bool cancel_reduce_bus_freq; + +static struct clk *dram_pll_clk; +static struct clk *sys1_pll_800m; +static struct clk *sys1_pll_400m; +static struct clk *sys1_pll_100m; +static struct clk *sys1_pll_40m; +static struct clk *dram_alt_src; +static struct clk *dram_alt_root; +static struct clk *dram_core_clk; +static struct clk *dram_apb_src; +static struct clk *dram_apb_pre_div; + +static struct delayed_work low_bus_freq_handler; +static struct delayed_work bus_freq_daemon; + +DEFINE_MUTEX(bus_freq_mutex); + +static irqreturn_t wait_in_wfe_irq(int irq, void *dev_id) +{ + struct arm_smccc_res res; + /* call smc trap to ATF */ + arm_smccc_smc(FSL_SIP_DDR_DVFS, WAIT_BUS_FREQ_DONE, 0, + 0, 0, 0, 0, 0, &res); + + return IRQ_HANDLED; +} + +static void 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(FSL_SIP_DDR_DVFS, target_freq, online_cpus, + 0, 0, 0, 0, 0, &res); + + local_irq_enable(); +} + +static void reduce_bus_freq(void) +{ + high_bus_freq_mode = 0; + + /* prepare the necessary clk before frequency change */ + clk_prepare_enable(sys1_pll_40m); + clk_prepare_enable(dram_alt_root); + + if (audio_bus_count) { + clk_prepare_enable(sys1_pll_400m); + + update_bus_freq(AUDIO_FREQ_400MTS); + + /* correct the clock tree info */ + clk_disable_unprepare(sys1_pll_400m); + clk_set_parent(dram_alt_src, sys1_pll_400m); + clk_set_parent(dram_core_clk, dram_alt_root); + clk_set_parent(dram_apb_src, sys1_pll_40m); + clk_set_rate(dram_apb_pre_div, 20000000); + + low_bus_freq_mode = 0; + audio_bus_freq_mode = 1; + cur_bus_freq_mode = BUS_FREQ_AUDIO; + } else { + clk_prepare_enable(sys1_pll_100m); + + update_bus_freq(LOW_BUS_FREQ_100MTS); + + /* correct the clock tree info */ + clk_disable_unprepare(sys1_pll_100m); + clk_set_parent(dram_alt_src, sys1_pll_100m); + clk_set_parent(dram_core_clk, dram_alt_root); + clk_set_parent(dram_apb_src, sys1_pll_40m); + clk_set_rate(dram_apb_pre_div, 20000000); + + low_bus_freq_mode = 1; + audio_bus_freq_mode = 0; + cur_bus_freq_mode = BUS_FREQ_LOW; + } + + clk_disable_unprepare(sys1_pll_40m); + clk_disable_unprepare(dram_alt_root); + + if (audio_bus_freq_mode) + printk(KERN_DEBUG "ddrc freq set to audio mode: 100MHz\n"); + if (low_bus_freq_mode) + printk(KERN_DEBUG "ddrc freq set to low bus mode: 25MHz\n"); +} + +static void reduce_bus_freq_handler(struct work_struct *work) +{ + mutex_lock(&bus_freq_mutex); + + if (!cancel_reduce_bus_freq) + reduce_bus_freq(); + + mutex_unlock(&bus_freq_mutex); +} + +static int set_low_bus_freq(void) +{ + if (busfreq_suspended) + return 0; + + if (!bus_freq_scaling_initialized || !bus_freq_scaling_is_active) + return 0; + + cancel_reduce_bus_freq = false; + + /* + * check to see if we need to got from low bus + * freq mode to audio bus freq mode. + * If so, the change needs to be done immediately. + */ + if (audio_bus_count && low_bus_freq_mode) + reduce_bus_freq(); + else + schedule_delayed_work(&low_bus_freq_handler, + usecs_to_jiffies(3000000)); + + return 0; +} + +static inline void cancel_low_bus_freq_handler(void) +{ + cancel_delayed_work(&low_bus_freq_handler); + cancel_reduce_bus_freq = true; +} + +static int set_high_bus_freq(int high_bus_freq) +{ + if (bus_freq_scaling_initialized || bus_freq_scaling_is_active) + cancel_low_bus_freq_handler(); + + if (busfreq_suspended) + return 0; + + if (!bus_freq_scaling_initialized || !bus_freq_scaling_is_active) + return 0; + + if (high_bus_freq_mode) + return 0; + + /* enable the clks needed in frequency */ + clk_prepare_enable(sys1_pll_800m); + clk_prepare_enable(dram_pll_clk); + + /* switch the DDR freqeuncy */ + update_bus_freq(0x0); + + /* correct the clock tree info */ + clk_set_parent(dram_apb_src, sys1_pll_800m); + clk_set_rate(dram_apb_pre_div, 200000000); + clk_set_parent(dram_core_clk, dram_pll_clk); + clk_disable_unprepare(sys1_pll_800m); + clk_disable_unprepare(dram_pll_clk); + + high_bus_freq_mode = 1; + audio_bus_freq_mode = 0; + low_bus_freq_mode = 0; + cur_bus_freq_mode = BUS_FREQ_HIGH; + + if (high_bus_freq_mode) + printk(KERN_DEBUG "ddrc freq set to high mode: 800MHz\n"); + + return 0; +} + +void request_bus_freq(enum bus_freq_mode mode) +{ + mutex_lock(&bus_freq_mutex); + + if (mode == BUS_FREQ_HIGH) + high_bus_count++; + else if (mode == BUS_FREQ_AUDIO) + audio_bus_count++; + else if (mode == BUS_FREQ_LOW) + low_bus_count++; + + if (busfreq_suspended || !bus_freq_scaling_initialized || + !bus_freq_scaling_is_active) { + mutex_unlock(&bus_freq_mutex); + return; + } + + cancel_low_bus_freq_handler(); + + if ((mode == BUS_FREQ_HIGH) && (!high_bus_freq_mode)) { + set_high_bus_freq(1); + mutex_unlock(&bus_freq_mutex); + return; + } + + if ((mode == BUS_FREQ_AUDIO) && (!high_bus_freq_mode) && + (!audio_bus_freq_mode)) { + set_low_bus_freq(); + mutex_unlock(&bus_freq_mutex); + return; + } + + mutex_unlock(&bus_freq_mutex); +} +EXPORT_SYMBOL(request_bus_freq); + +void release_bus_freq(enum bus_freq_mode mode) +{ + mutex_lock(&bus_freq_mutex); + if (mode == BUS_FREQ_HIGH) { + if (high_bus_count == 0) { + dev_err(busfreq_dev, "high bus count mismatch!\n"); + dump_stack(); + mutex_unlock(&bus_freq_mutex); + return; + } + high_bus_count--; + } else if (mode == BUS_FREQ_AUDIO) { + if (audio_bus_count == 0) { + dev_err(busfreq_dev, "audio bus count mismatch!\n"); + dump_stack(); + mutex_unlock(&bus_freq_mutex); + return; + } + audio_bus_count--; + } else if (mode == BUS_FREQ_LOW) { + if (low_bus_count == 0) { + dev_err(busfreq_dev, "low bus count mismatch!\n"); + dump_stack(); + mutex_unlock(&bus_freq_mutex); + return; + } + low_bus_count--; + } + + if (busfreq_suspended || !bus_freq_scaling_initialized || + !bus_freq_scaling_is_active) { + mutex_unlock(&bus_freq_mutex); + return; + } + + if ((!audio_bus_freq_mode) && (high_bus_count == 0) && + (audio_bus_count != 0)) { + set_low_bus_freq(); + mutex_unlock(&bus_freq_mutex); + return; + } + + if ((!low_bus_freq_mode) && (high_bus_count == 0) && + (audio_bus_count == 0)) { + set_low_bus_freq(); + mutex_unlock(&bus_freq_mutex); + } + + mutex_unlock(&bus_freq_mutex); +} +EXPORT_SYMBOL(release_bus_freq); + +int get_bus_freq_mode(void) +{ + return cur_bus_freq_mode; +} +EXPORT_SYMBOL(get_bus_freq_mode); + +static void bus_freq_daemon_handler(struct work_struct *work) +{ + mutex_lock(&bus_freq_mutex); + if ((!low_bus_freq_mode) && (high_bus_count == 0) && + (audio_bus_count == 0)) + set_low_bus_freq(); + mutex_unlock(&bus_freq_mutex); +} + +static ssize_t bus_freq_scaling_enable_show(struct device *dev, + struct device_attribute *attr, char *buf) +{ + if (bus_freq_scaling_is_active) + return sprintf(buf, "Bus frequency scaling is enabled\n"); + else + return sprintf(buf, "Bus frequency scaling is disabled\n"); +} + +static ssize_t bus_freq_scaling_enable_store(struct device *dev, + struct device_attribute *attr, + const char *buf, size_t size) +{ + if (strncmp(buf, "1", 1) == 0) { + bus_freq_scaling_is_active = 1; + set_high_bus_freq(1); + /* + * We set bus freq to higher at the beginning, + * so we use this daemon thread to make sure system + * can enter low bus mode if there is no high bus request pending + */ + schedule_delayed_work(&bus_freq_daemon, + usecs_to_jiffies(5000000)); + } else if (strncmp(buf, "0", 1) == 0) { + if (bus_freq_scaling_is_active) + set_high_bus_freq(1); + bus_freq_scaling_is_active = 0; + } + return size; +} + +static int bus_freq_pm_notify(struct notifier_block *nb, unsigned long event, + void *dummy) +{ + mutex_lock(&bus_freq_mutex); + + if (event == PM_SUSPEND_PREPARE) { + high_bus_count++; + set_high_bus_freq(1); + busfreq_suspended = 1; + } else if (event == PM_POST_SUSPEND) { + busfreq_suspended = 0; + high_bus_count--; + schedule_delayed_work(&bus_freq_daemon, + usecs_to_jiffies(5000000)); + } + + mutex_unlock(&bus_freq_mutex); + + return NOTIFY_OK; +} + +static int busfreq_reboot_notifier_event(struct notifier_block *this, + unsigned long event, void *ptr) +{ + /* System is rebooting. Set the system into high_bus_freq_mode. */ + request_bus_freq(BUS_FREQ_HIGH); + + return 0; +} + +static struct notifier_block imx_bus_freq_pm_notifier = { + .notifier_call = bus_freq_pm_notify, +}; + +static struct notifier_block imx_busfreq_reboot_notifier = { + .notifier_call = busfreq_reboot_notifier_event, +}; + +static DEVICE_ATTR(enable, 0644, bus_freq_scaling_enable_show, + bus_freq_scaling_enable_store); + +static int init_busfreq_irq(struct platform_device *busfreq_pdev) +{ + struct device *dev = &busfreq_pdev->dev; + u32 cpu; + int err; + + for_each_online_cpu(cpu) { + int irq; + /* + * set up a reserved interrupt to get all + * the active cores into a WFE state before + * changing the DDR frequency. + */ + irq = platform_get_irq(busfreq_pdev, cpu); + err = request_irq(irq, wait_in_wfe_irq, + IRQF_PERCPU, "ddrc", NULL); + if (err) { + dev_err(dev, "Busfreq request irq failed %d, err = %d\n", + irq, err); + return err; + } + err = irq_set_affinity(irq, cpumask_of(cpu)); + if (err) { + dev_err(dev, "busfreq can't set irq affinity irq = %d\n", irq); + return err; + } + } + + return 0; +} + +static int init_busfreq_clk(struct platform_device *pdev) +{ + dram_pll_clk = devm_clk_get(&pdev->dev, "dram_pll"); + sys1_pll_800m = devm_clk_get(&pdev->dev, "sys1_pll_800m"); + sys1_pll_400m = devm_clk_get(&pdev->dev, "sys1_pll_400m"); + sys1_pll_100m = devm_clk_get(&pdev->dev, "sys1_pll_100m"); + sys1_pll_40m = devm_clk_get(&pdev->dev, "sys1_pll_40m"); + dram_alt_src = devm_clk_get(&pdev->dev, "dram_alt_src"); + dram_alt_root = devm_clk_get(&pdev->dev, "dram_alt_root"); + dram_core_clk = devm_clk_get(&pdev->dev, "dram_core"); + dram_apb_src = devm_clk_get(&pdev->dev, "dram_apb_src"); + dram_apb_pre_div = devm_clk_get(&pdev->dev, "dram_apb_pre_div"); + + if (IS_ERR(dram_pll_clk) || IS_ERR(sys1_pll_400m) || IS_ERR(sys1_pll_100m) || + IS_ERR(sys1_pll_40m) || IS_ERR(dram_alt_src) || IS_ERR(dram_alt_root) || + IS_ERR(dram_core_clk) || IS_ERR(dram_apb_src) || IS_ERR(dram_apb_pre_div)) { + dev_err(&pdev->dev, "failed to get busfreq clk\n"); + return -EINVAL; + } + + return 0; +} + +/*! + * This is the probe routine for the bus frequency driver. + * + * @param pdev The platform device structure + * + * @return The function returns 0 on success + * + */ +static int busfreq_probe(struct platform_device *pdev) +{ + int err; + + busfreq_dev = &pdev->dev; + + /* get the clock for DDRC */ + err = init_busfreq_clk(pdev); + if (err) { + dev_err(busfreq_dev, "init clk failed\n"); + return err; + } + + /* init the irq used for ddr frequency change */ + err = init_busfreq_irq(pdev); + if (err) { + dev_err(busfreq_dev, "init busfreq irq failed!\n"); + return err; + } + + /* create the sysfs file */ + err = sysfs_create_file(&busfreq_dev->kobj, &dev_attr_enable.attr); + if (err) { + dev_err(busfreq_dev, + "Unable to register sysdev entry for BUSFREQ"); + return err; + } + + high_bus_freq_mode = 1; + low_bus_freq_mode = 0; + audio_bus_freq_mode = 0; + cur_bus_freq_mode = BUS_FREQ_HIGH; + + bus_freq_scaling_is_active = 1; + bus_freq_scaling_initialized = 1; + + INIT_DELAYED_WORK(&low_bus_freq_handler, reduce_bus_freq_handler); + INIT_DELAYED_WORK(&bus_freq_daemon, bus_freq_daemon_handler); + register_pm_notifier(&imx_bus_freq_pm_notifier); + register_reboot_notifier(&imx_busfreq_reboot_notifier); + + /* enter low bus mode if no high speed device enabled */ + schedule_delayed_work(&bus_freq_daemon, msecs_to_jiffies(10000)); + + return 0; +} + +static const struct of_device_id imx_busfreq_ids[] = { + { .compatible = "fsl,imx_busfreq", }, + { /*sentinel */} +}; + +static struct platform_driver busfreq_driver = { + .driver = { + .name = "imx_busfreq", + .owner = THIS_MODULE, + .of_match_table = imx_busfreq_ids, + }, + .probe = busfreq_probe, +}; + +/*! + * Initialise the busfreq_driver. + * + * @return The function always returns 0. + */ +static int __init busfreq_init(void) +{ + if (platform_driver_register(&busfreq_driver) != 0) + return -ENODEV; + + printk(KERN_INFO "Bus freq driver module loaded\n"); + + return 0; +} + +static void __exit busfreq_cleanup(void) +{ + sysfs_remove_file(&busfreq_dev->kobj, &dev_attr_enable.attr); + + /* Unregister the device structure */ + platform_driver_unregister(&busfreq_driver); + bus_freq_scaling_initialized = 0; +} + +module_init(busfreq_init); +module_exit(busfreq_cleanup); + +MODULE_AUTHOR("NXP Semiconductor, Inc."); +MODULE_DESCRIPTION("Busfreq driver"); +MODULE_LICENSE("GPL"); |