diff options
Diffstat (limited to 'drivers/devfreq/exynos/exynos5_bus.c')
| -rw-r--r-- | drivers/devfreq/exynos/exynos5_bus.c | 432 | 
1 files changed, 432 insertions, 0 deletions
diff --git a/drivers/devfreq/exynos/exynos5_bus.c b/drivers/devfreq/exynos/exynos5_bus.c new file mode 100644 index 00000000000..6cd0392e279 --- /dev/null +++ b/drivers/devfreq/exynos/exynos5_bus.c @@ -0,0 +1,432 @@ +/* + * Copyright (c) 2012 Samsung Electronics Co., Ltd. + *		http://www.samsung.com/ + * + * EXYNOS5 INT clock frequency scaling support using DEVFREQ framework + * Based on work done by Jonghwan Choi <jhbird.choi@samsung.com> + * Support for only EXYNOS5250 is present. + * + * 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. + * + */ + +#include <linux/module.h> +#include <linux/devfreq.h> +#include <linux/io.h> +#include <linux/pm_opp.h> +#include <linux/slab.h> +#include <linux/suspend.h> +#include <linux/clk.h> +#include <linux/delay.h> +#include <linux/platform_device.h> +#include <linux/pm_qos.h> +#include <linux/regulator/consumer.h> +#include <linux/of_address.h> +#include <linux/of_platform.h> + +#include "exynos_ppmu.h" + +#define MAX_SAFEVOLT			1100000 /* 1.10V */ +/* Assume that the bus is saturated if the utilization is 25% */ +#define INT_BUS_SATURATION_RATIO	25 + +enum int_level_idx { +	LV_0, +	LV_1, +	LV_2, +	LV_3, +	LV_4, +	_LV_END +}; + +enum exynos_ppmu_list { +	PPMU_RIGHT, +	PPMU_END, +}; + +struct busfreq_data_int { +	struct device *dev; +	struct devfreq *devfreq; +	struct regulator *vdd_int; +	struct busfreq_ppmu_data ppmu_data; +	unsigned long curr_freq; +	bool disabled; + +	struct notifier_block pm_notifier; +	struct mutex lock; +	struct pm_qos_request int_req; +	struct clk *int_clk; +}; + +struct int_bus_opp_table { +	unsigned int idx; +	unsigned long clk; +	unsigned long volt; +}; + +static struct int_bus_opp_table exynos5_int_opp_table[] = { +	{LV_0, 266000, 1025000}, +	{LV_1, 200000, 1025000}, +	{LV_2, 160000, 1025000}, +	{LV_3, 133000, 1025000}, +	{LV_4, 100000, 1025000}, +	{0, 0, 0}, +}; + +static int exynos5_int_setvolt(struct busfreq_data_int *data, +				unsigned long volt) +{ +	return regulator_set_voltage(data->vdd_int, volt, MAX_SAFEVOLT); +} + +static int exynos5_busfreq_int_target(struct device *dev, unsigned long *_freq, +			      u32 flags) +{ +	int err = 0; +	struct platform_device *pdev = container_of(dev, struct platform_device, +						    dev); +	struct busfreq_data_int *data = platform_get_drvdata(pdev); +	struct dev_pm_opp *opp; +	unsigned long old_freq, freq; +	unsigned long volt; + +	rcu_read_lock(); +	opp = devfreq_recommended_opp(dev, _freq, flags); +	if (IS_ERR(opp)) { +		rcu_read_unlock(); +		dev_err(dev, "%s: Invalid OPP.\n", __func__); +		return PTR_ERR(opp); +	} + +	freq = dev_pm_opp_get_freq(opp); +	volt = dev_pm_opp_get_voltage(opp); +	rcu_read_unlock(); + +	old_freq = data->curr_freq; + +	if (old_freq == freq) +		return 0; + +	dev_dbg(dev, "targeting %lukHz %luuV\n", freq, volt); + +	mutex_lock(&data->lock); + +	if (data->disabled) +		goto out; + +	if (freq > exynos5_int_opp_table[0].clk) +		pm_qos_update_request(&data->int_req, freq * 16 / 1000); +	else +		pm_qos_update_request(&data->int_req, -1); + +	if (old_freq < freq) +		err = exynos5_int_setvolt(data, volt); +	if (err) +		goto out; + +	err = clk_set_rate(data->int_clk, freq * 1000); + +	if (err) +		goto out; + +	if (old_freq > freq) +		err = exynos5_int_setvolt(data, volt); +	if (err) +		goto out; + +	data->curr_freq = freq; +out: +	mutex_unlock(&data->lock); +	return err; +} + +static int exynos5_int_get_dev_status(struct device *dev, +				      struct devfreq_dev_status *stat) +{ +	struct platform_device *pdev = container_of(dev, struct platform_device, +						    dev); +	struct busfreq_data_int *data = platform_get_drvdata(pdev); +	struct busfreq_ppmu_data *ppmu_data = &data->ppmu_data; +	int busier_dmc; + +	exynos_read_ppmu(ppmu_data); +	busier_dmc = exynos_get_busier_ppmu(ppmu_data); + +	stat->current_frequency = data->curr_freq; + +	/* Number of cycles spent on memory access */ +	stat->busy_time = ppmu_data->ppmu[busier_dmc].count[PPMU_PMNCNT3]; +	stat->busy_time *= 100 / INT_BUS_SATURATION_RATIO; +	stat->total_time = ppmu_data->ppmu[busier_dmc].ccnt; + +	return 0; +} + +static struct devfreq_dev_profile exynos5_devfreq_int_profile = { +	.initial_freq		= 160000, +	.polling_ms		= 100, +	.target			= exynos5_busfreq_int_target, +	.get_dev_status		= exynos5_int_get_dev_status, +}; + +static int exynos5250_init_int_tables(struct busfreq_data_int *data) +{ +	int i, err = 0; + +	for (i = LV_0; i < _LV_END; i++) { +		err = dev_pm_opp_add(data->dev, exynos5_int_opp_table[i].clk, +				exynos5_int_opp_table[i].volt); +		if (err) { +			dev_err(data->dev, "Cannot add opp entries.\n"); +			return err; +		} +	} + +	return 0; +} + +static int exynos5_busfreq_int_pm_notifier_event(struct notifier_block *this, +		unsigned long event, void *ptr) +{ +	struct busfreq_data_int *data = container_of(this, +					struct busfreq_data_int, pm_notifier); +	struct dev_pm_opp *opp; +	unsigned long maxfreq = ULONG_MAX; +	unsigned long freq; +	unsigned long volt; +	int err = 0; + +	switch (event) { +	case PM_SUSPEND_PREPARE: +		/* Set Fastest and Deactivate DVFS */ +		mutex_lock(&data->lock); + +		data->disabled = true; + +		rcu_read_lock(); +		opp = dev_pm_opp_find_freq_floor(data->dev, &maxfreq); +		if (IS_ERR(opp)) { +			rcu_read_unlock(); +			err = PTR_ERR(opp); +			goto unlock; +		} +		freq = dev_pm_opp_get_freq(opp); +		volt = dev_pm_opp_get_voltage(opp); +		rcu_read_unlock(); + +		err = exynos5_int_setvolt(data, volt); +		if (err) +			goto unlock; + +		err = clk_set_rate(data->int_clk, freq * 1000); + +		if (err) +			goto unlock; + +		data->curr_freq = freq; +unlock: +		mutex_unlock(&data->lock); +		if (err) +			return NOTIFY_BAD; +		return NOTIFY_OK; +	case PM_POST_RESTORE: +	case PM_POST_SUSPEND: +		/* Reactivate */ +		mutex_lock(&data->lock); +		data->disabled = false; +		mutex_unlock(&data->lock); +		return NOTIFY_OK; +	} + +	return NOTIFY_DONE; +} + +static int exynos5_busfreq_int_probe(struct platform_device *pdev) +{ +	struct busfreq_data_int *data; +	struct busfreq_ppmu_data *ppmu_data; +	struct dev_pm_opp *opp; +	struct device *dev = &pdev->dev; +	struct device_node *np; +	unsigned long initial_freq; +	unsigned long initial_volt; +	int err = 0; +	int i; + +	data = devm_kzalloc(&pdev->dev, sizeof(struct busfreq_data_int), +				GFP_KERNEL); +	if (data == NULL) { +		dev_err(dev, "Cannot allocate memory.\n"); +		return -ENOMEM; +	} + +	ppmu_data = &data->ppmu_data; +	ppmu_data->ppmu_end = PPMU_END; +	ppmu_data->ppmu = devm_kzalloc(dev, +				       sizeof(struct exynos_ppmu) * PPMU_END, +				       GFP_KERNEL); +	if (!ppmu_data->ppmu) { +		dev_err(dev, "Failed to allocate memory for exynos_ppmu\n"); +		return -ENOMEM; +	} + +	np = of_find_compatible_node(NULL, NULL, "samsung,exynos5250-ppmu"); +	if (np == NULL) { +		pr_err("Unable to find PPMU node\n"); +		return -ENOENT; +	} + +	for (i = 0; i < ppmu_data->ppmu_end; i++) { +		/* map PPMU memory region */ +		ppmu_data->ppmu[i].hw_base = of_iomap(np, i); +		if (ppmu_data->ppmu[i].hw_base == NULL) { +			dev_err(&pdev->dev, "failed to map memory region\n"); +			return -ENOMEM; +		} +	} +	data->pm_notifier.notifier_call = exynos5_busfreq_int_pm_notifier_event; +	data->dev = dev; +	mutex_init(&data->lock); + +	err = exynos5250_init_int_tables(data); +	if (err) +		return err; + +	data->vdd_int = devm_regulator_get(dev, "vdd_int"); +	if (IS_ERR(data->vdd_int)) { +		dev_err(dev, "Cannot get the regulator \"vdd_int\"\n"); +		return PTR_ERR(data->vdd_int); +	} + +	data->int_clk = devm_clk_get(dev, "int_clk"); +	if (IS_ERR(data->int_clk)) { +		dev_err(dev, "Cannot get clock \"int_clk\"\n"); +		return PTR_ERR(data->int_clk); +	} + +	rcu_read_lock(); +	opp = dev_pm_opp_find_freq_floor(dev, +			&exynos5_devfreq_int_profile.initial_freq); +	if (IS_ERR(opp)) { +		rcu_read_unlock(); +		dev_err(dev, "Invalid initial frequency %lu kHz.\n", +		       exynos5_devfreq_int_profile.initial_freq); +		return PTR_ERR(opp); +	} +	initial_freq = dev_pm_opp_get_freq(opp); +	initial_volt = dev_pm_opp_get_voltage(opp); +	rcu_read_unlock(); +	data->curr_freq = initial_freq; + +	err = clk_set_rate(data->int_clk, initial_freq * 1000); +	if (err) { +		dev_err(dev, "Failed to set initial frequency\n"); +		return err; +	} + +	err = exynos5_int_setvolt(data, initial_volt); +	if (err) +		return err; + +	platform_set_drvdata(pdev, data); + +	busfreq_mon_reset(ppmu_data); + +	data->devfreq = devm_devfreq_add_device(dev, &exynos5_devfreq_int_profile, +					   "simple_ondemand", NULL); +	if (IS_ERR(data->devfreq)) +		return PTR_ERR(data->devfreq); + +	err = devm_devfreq_register_opp_notifier(dev, data->devfreq); +	if (err < 0) { +		dev_err(dev, "Failed to register opp notifier\n"); +		return err; +	} + +	err = register_pm_notifier(&data->pm_notifier); +	if (err) { +		dev_err(dev, "Failed to setup pm notifier\n"); +		return err; +	} + +	/* TODO: Add a new QOS class for int/mif bus */ +	pm_qos_add_request(&data->int_req, PM_QOS_NETWORK_THROUGHPUT, -1); + +	return 0; +} + +static int exynos5_busfreq_int_remove(struct platform_device *pdev) +{ +	struct busfreq_data_int *data = platform_get_drvdata(pdev); + +	pm_qos_remove_request(&data->int_req); +	unregister_pm_notifier(&data->pm_notifier); + +	return 0; +} + +#ifdef CONFIG_PM_SLEEP +static int exynos5_busfreq_int_resume(struct device *dev) +{ +	struct platform_device *pdev = container_of(dev, struct platform_device, +						    dev); +	struct busfreq_data_int *data = platform_get_drvdata(pdev); +	struct busfreq_ppmu_data *ppmu_data = &data->ppmu_data; + +	busfreq_mon_reset(ppmu_data); +	return 0; +} +static const struct dev_pm_ops exynos5_busfreq_int_pm = { +	.resume	= exynos5_busfreq_int_resume, +}; +#endif +static SIMPLE_DEV_PM_OPS(exynos5_busfreq_int_pm_ops, NULL, +			 exynos5_busfreq_int_resume); + +/* platform device pointer for exynos5 devfreq device. */ +static struct platform_device *exynos5_devfreq_pdev; + +static struct platform_driver exynos5_busfreq_int_driver = { +	.probe		= exynos5_busfreq_int_probe, +	.remove		= exynos5_busfreq_int_remove, +	.driver		= { +		.name		= "exynos5-bus-int", +		.owner		= THIS_MODULE, +		.pm		= &exynos5_busfreq_int_pm_ops, +	}, +}; + +static int __init exynos5_busfreq_int_init(void) +{ +	int ret; + +	ret = platform_driver_register(&exynos5_busfreq_int_driver); +	if (ret < 0) +		goto out; + +	exynos5_devfreq_pdev = +		platform_device_register_simple("exynos5-bus-int", -1, NULL, 0); +	if (IS_ERR(exynos5_devfreq_pdev)) { +		ret = PTR_ERR(exynos5_devfreq_pdev); +		goto out1; +	} + +	return 0; +out1: +	platform_driver_unregister(&exynos5_busfreq_int_driver); +out: +	return ret; +} +late_initcall(exynos5_busfreq_int_init); + +static void __exit exynos5_busfreq_int_exit(void) +{ +	platform_device_unregister(exynos5_devfreq_pdev); +	platform_driver_unregister(&exynos5_busfreq_int_driver); +} +module_exit(exynos5_busfreq_int_exit); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("EXYNOS5 busfreq driver with devfreq framework");  | 
