diff options
Diffstat (limited to 'arch/arm/mach-exynos/dispfreq_opp_exynos4.c')
-rw-r--r-- | arch/arm/mach-exynos/dispfreq_opp_exynos4.c | 609 |
1 files changed, 609 insertions, 0 deletions
diff --git a/arch/arm/mach-exynos/dispfreq_opp_exynos4.c b/arch/arm/mach-exynos/dispfreq_opp_exynos4.c new file mode 100644 index 0000000..fafdd62 --- /dev/null +++ b/arch/arm/mach-exynos/dispfreq_opp_exynos4.c @@ -0,0 +1,609 @@ +/* linux/arch/arm/mach-exynos/dispfreq_opp_exynos4.c + * + * Copyright (c) 2011 Samsung Electronics Co., Ltd. + * http://www.samsung.com/ + * + * EXYNOS4 - Display frequency scaling support with OPP + * + * 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. +*/ + +/* This feature was derived from exynos4 display of devfreq + * which was made by Mr Myungjoo Ham. + */ + +#include <linux/kernel.h> +#include <linux/err.h> +#include <linux/opp.h> +#include <linux/mutex.h> +#include <linux/suspend.h> +#include <linux/notifier.h> +#include <linux/slab.h> +#include <linux/platform_device.h> +#include <linux/pm_qos_params.h> +#include <linux/devfreq/exynos4_display.h> + + +#include <linux/list.h> +#include <linux/pm_qos_params.h> +#include <mach/cpufreq.h> +#include <mach/dev.h> +#include <linux/device.h> + +#define DEVFREQ_OPTION_FREQ_LUB 0x0 +#define DEVFREQ_OPTION_FREQ_GLB 0x1 + +#define EXYNOS4_DISPLAY_ON 1 +#define EXYNOS4_DISPLAY_OFF 0 + +#define DEFAULT_DELAY_TIME 1000 /* us (millisecond) */ + +enum exynos_display_type { + TYPE_DISPLAY_EXYNOS4210, + TYPE_DISPLAY_EXYNOS4x12, +}; + +/* Define opp table which include various frequency level */ +struct exynos4_dispfreq_opp_table { + unsigned int idx; + unsigned long clk; + unsigned long volt; +}; + +struct exynos4_dispfreq_pm_qos_table { + unsigned long freq; /* 0 if this is the last element */ + s32 qos_value; +}; + +struct exynos4_dispfreq_data { + struct device *dev; + struct opp *curr_opp; + struct delayed_work wq_lowfreq; + struct notifier_block nb; + struct notifier_block nb_pm; + struct notifier_block nb_qos; + + unsigned long initial_freq; + int qos_type; + struct exynos4_dispfreq_pm_qos_table *qos_list; + + unsigned long previous_freq; + unsigned long min_freq; + unsigned long max_freq; + unsigned long qos_min_freq; + + enum exynos_display_type type; + unsigned int state; + struct mutex lock; +}; + +/* Define frequency level */ +enum exynos4_dispfreq_clk_level_idx { + LV_0 = 0, + LV_1, + _LV_END +}; + +static struct exynos4_dispfreq_opp_table exynos_dispfreq_clk_table[] = { + {LV_0, 40, 0 }, + {LV_1, 60, 0 }, + {0, 0, 0 }, +}; + +static struct pm_qos_request_list qos_wrapper[DVFS_LOCK_ID_END]; + +/* Wrappers for obsolete legacy kernel hack (busfreq_lock/lock_free) */ +int exynos4_busfreq_lock(unsigned int nId, enum busfreq_level_request lvl) +{ + s32 qos_value; + + if (WARN(nId >= DVFS_LOCK_ID_END, "incorrect nId.")) + return -EINVAL; + if (WARN(lvl >= BUS_LEVEL_END, "incorrect level.")) + return -EINVAL; + + switch (lvl) { + case BUS_L0: + qos_value = 400000; + break; + case BUS_L1: + qos_value = 267000; + break; + case BUS_L2: + qos_value = 133000; + break; + default: + qos_value = 0; + } + + if (qos_wrapper[nId].pm_qos_class == 0) { + pm_qos_add_request(&qos_wrapper[nId], + PM_QOS_BUS_QOS, qos_value); + } else { + pm_qos_update_request(&qos_wrapper[nId], qos_value); + } + + return 0; +} + +void exynos4_busfreq_lock_free(unsigned int nId) +{ + if (WARN(nId >= DVFS_LOCK_ID_END, "incorrect nId.")) + return; + + if (qos_wrapper[nId].pm_qos_class) + pm_qos_update_request(&qos_wrapper[nId], + PM_QOS_BUS_DMA_THROUGHPUT_DEFAULT_VALUE); +} + +/* + * The exynos-display driver send newly frequency to display client + * if it receive event from sender device. + * List of display client device + * - FIMD and so on + */ +static BLOCKING_NOTIFIER_HEAD(exynos4_display_notifier_client_list); + + +int exynos4_display_register_client(struct notifier_block *nb) +{ + return blocking_notifier_chain_register( + &exynos4_display_notifier_client_list, nb); +} +EXPORT_SYMBOL(exynos4_display_register_client); + +int exynos4_display_unregister_client(struct notifier_block *nb) +{ + return blocking_notifier_chain_unregister( + &exynos4_display_notifier_client_list, nb); +} +EXPORT_SYMBOL(exynos4_display_unregister_client); + +static int devfreq_powersave_func(struct exynos4_dispfreq_data *data, + unsigned long *freq) +{ + *freq = data->min_freq; + return 0; +} + + +static int exynos4_dispfreq_profile_target(struct device *dev, + unsigned long *_freq, u32 options); + +/** + * update_devfreq() - Reevaluate the device and configure frequency. + * @devfreq: the devfreq instance. + * + * Note: Lock devfreq->lock before calling update_devfreq + * This function is exported for governors. + */ +static int exynos4_dispfreq_update(struct exynos4_dispfreq_data *data) +{ + unsigned long freq; + int err = 0; + u32 options = 0; + + if (!mutex_is_locked(&data->lock)) { + WARN(true, "devfreq->lock must be locked by the caller.\n"); + return -EINVAL; + } + + /* Reevaluate the proper frequency */ + err = devfreq_powersave_func(data, &freq); + if (err) + return err; + + /* + * Adjust the freuqency with user freq and QoS. + * + * List from the highest proiority + * min_freq + * max_freq + * qos_min_freq + */ + + if (data->qos_min_freq && freq < data->qos_min_freq) { + freq = data->qos_min_freq; + options &= ~(1 << 0); + options |= DEVFREQ_OPTION_FREQ_LUB; + } + if (data->max_freq && freq > data->max_freq) { + freq = data->max_freq; + options &= ~(1 << 0); + options |= DEVFREQ_OPTION_FREQ_GLB; + } + if (data->min_freq && freq < data->min_freq) { + freq = data->min_freq; + options &= ~(1 << 0); + options |= DEVFREQ_OPTION_FREQ_LUB; + } + + err = exynos4_dispfreq_profile_target(data->dev, &freq, options); + if (err) + return err; + + data->previous_freq = freq; + return err; +} + +static int exynos4_dispfreq_send_event_to_display(unsigned long val, void *v) +{ + return blocking_notifier_call_chain( + &exynos4_display_notifier_client_list, val, v); +} + +static int exynos4_dispfreq_opp_notifier_call(struct notifier_block *nb, + unsigned long val, void *devp) +{ + struct exynos4_dispfreq_data *data = container_of(nb, + struct exynos4_dispfreq_data, nb); + int ret; + + mutex_lock(&data->lock); + ret = exynos4_dispfreq_update(data); + mutex_unlock(&data->lock); + + return ret; +} + +struct opp *devfreq_recommended_opp(struct device *dev, unsigned long *freq, + bool floor) +{ + struct opp *opp; + + if (floor) { + opp = opp_find_freq_floor(dev, freq); + + if (opp == ERR_PTR(-ENODEV)) + opp = opp_find_freq_ceil(dev, freq); + } else { + opp = opp_find_freq_ceil(dev, freq); + + if (opp == ERR_PTR(-ENODEV)) + opp = opp_find_freq_floor(dev, freq); + } + + return opp; +} + + +static int exynos4_dispfreq_profile_target(struct device *dev, + unsigned long *_freq, u32 options) +{ + /* Inform display client of new frequency */ + struct exynos4_dispfreq_data *data = dev_get_drvdata(dev); + struct opp *opp = devfreq_recommended_opp(dev, _freq, options & + DEVFREQ_OPTION_FREQ_GLB); + unsigned long old_freq = opp_get_freq(data->curr_opp); + unsigned long new_freq = opp_get_freq(opp); + + /* TODO: No longer use fb notifier to identify LCD on/off state and + have yet alternative feature of it. So, exynos4-display change + refresh rate of display clinet irrespective of LCD state until + proper feature will be implemented. */ + if (old_freq == new_freq) + return 0; + + opp = opp_find_freq_floor(dev, &new_freq); + data->curr_opp = opp; + + switch (new_freq) { + case EXYNOS4_DISPLAY_LV_HF: + if (delayed_work_pending(&data->wq_lowfreq)) + cancel_delayed_work(&data->wq_lowfreq); + + exynos4_dispfreq_send_event_to_display( + EXYNOS4_DISPLAY_LV_HF, NULL); + break; + case EXYNOS4_DISPLAY_LV_LF: + schedule_delayed_work(&data->wq_lowfreq, + msecs_to_jiffies(DEFAULT_DELAY_TIME)); + break; + } + + return 0; +} + +static int exynos4_dispfreq_qos_notifier_call(struct notifier_block *nb, + unsigned long value, void *devp) +{ + struct exynos4_dispfreq_data *data = container_of(nb, + struct exynos4_dispfreq_data, nb_qos); + int ret; + int i; + unsigned long default_value = 0; + struct exynos4_dispfreq_pm_qos_table *qos_list = data->qos_list; + bool qos_use_max = true; + + if (!qos_list) + return NOTIFY_DONE; + + mutex_lock(&data->lock); + + switch (data->qos_type) { + case PM_QOS_CPU_DMA_LATENCY: + default_value = PM_QOS_CPU_DMA_LAT_DEFAULT_VALUE; + break; + case PM_QOS_NETWORK_LATENCY: + default_value = PM_QOS_NETWORK_LAT_DEFAULT_VALUE; + break; + case PM_QOS_NETWORK_THROUGHPUT: + default_value = PM_QOS_NETWORK_THROUGHPUT_DEFAULT_VALUE; + break; + case PM_QOS_BUS_DMA_THROUGHPUT: + default_value = PM_QOS_BUS_DMA_THROUGHPUT_DEFAULT_VALUE; + break; + case PM_QOS_DISPLAY_FREQUENCY: + default_value = PM_QOS_DISPLAY_FREQUENCY_DEFAULT_VALUE; + break; + default: + /* Won't do any check to detect "default" state */ + break; + } + + if (value == default_value) { + data->qos_min_freq = 0; + goto update; + } + + for (i = 0; qos_list[i].freq; i++) { + /* QoS Met */ + if ((qos_use_max && qos_list[i].qos_value >= value) || + (!qos_use_max && qos_list[i].qos_value <= value)) { + data->qos_min_freq = qos_list[i].freq; + goto update; + } + } + + /* Use the highest QoS freq */ + if (i > 0) + data->qos_min_freq = qos_list[i - 1].freq; + +update: + ret = exynos4_dispfreq_update(data); + mutex_unlock(&data->lock); + return ret; +} + +/* + * Register exynos-display as client to pm notifer + * - This callback gets called when something important happens in pm state. + */ +static int exynos4_dispfreq_pm_notifier_callback(struct notifier_block *this, + unsigned long event, void *_data) +{ + struct exynos4_dispfreq_data *data = container_of(this, + struct exynos4_dispfreq_data, nb_pm); + + if (data->state == EXYNOS4_DISPLAY_OFF) + return NOTIFY_OK; + + switch (event) { + case PM_SUSPEND_PREPARE: + mutex_lock(&data->lock); + data->state = EXYNOS4_DISPLAY_OFF; + mutex_unlock(&data->lock); + + if (delayed_work_pending(&data->wq_lowfreq)) + cancel_delayed_work(&data->wq_lowfreq); + + return NOTIFY_OK; + + case PM_POST_RESTORE: + case PM_POST_SUSPEND: + mutex_lock(&data->lock); + data->state = EXYNOS4_DISPLAY_ON; + mutex_unlock(&data->lock); + + return NOTIFY_OK; + } + + return NOTIFY_DONE; +} + +/* + * Enable/disable exynos-display operation + */ +static void exynos4_dispfreq_disable(struct exynos4_dispfreq_data *data) +{ + struct opp *opp; + unsigned long freq = EXYNOS4_DISPLAY_LV_DEFAULT; + + /* Cancel workqueue which set low frequency of display client + * if it is pending state before executing workqueue. */ + if (delayed_work_pending(&data->wq_lowfreq)) + cancel_delayed_work(&data->wq_lowfreq); + + /* Set high frequency(default) of display client */ + exynos4_dispfreq_send_event_to_display(freq, NULL); + + mutex_lock(&data->lock); + data->state = EXYNOS4_DISPLAY_OFF; + mutex_unlock(&data->lock); + + /* Find opp object with high frequency */ + opp = opp_find_freq_floor(data->dev, &freq); + if (IS_ERR(opp)) { + dev_err(data->dev, + "invalid initial frequency %lu kHz.\n", freq); + } else + data->curr_opp = opp; +} + +static void exynos4_display_enable(struct exynos4_dispfreq_data *data) +{ + data->state = EXYNOS4_DISPLAY_ON; +} + +/* + * Timer to set display with low frequency state after 1 second + */ +static void exynos4_dispfreq_set_lowfreq(struct work_struct *work) +{ + exynos4_dispfreq_send_event_to_display(EXYNOS4_DISPLAY_LV_LF, NULL); +} + +static __devinit int exynos4_dispfreq_probe(struct platform_device *pdev) +{ + struct exynos4_dispfreq_data *data; + struct device *dev = &pdev->dev; + struct opp *opp; + int ret = 0; + int i; + struct exynos4_dispfreq_pm_qos_table *qos_list; + struct srcu_notifier_head *nh; + + data = kzalloc(sizeof(struct exynos4_dispfreq_data), GFP_KERNEL); + if (!data) { + dev_err(dev, "cannot allocate memory.\n"); + return -ENOMEM; + } + data->dev = dev; + data->state = EXYNOS4_DISPLAY_ON; + data->initial_freq = EXYNOS4_DISPLAY_LV_DEFAULT; + mutex_init(&data->lock); + + /* Register OPP entries */ + for (i = 0 ; i < _LV_END ; i++) { + ret = opp_add(dev, exynos_dispfreq_clk_table[i].clk, + exynos_dispfreq_clk_table[i].volt); + if (ret) { + dev_err(dev, "cannot add opp entries.\n"); + goto err_alloc_mem; + } + } + + + /* Find opp object with init frequency */ + opp = opp_find_freq_floor(dev, &data->initial_freq); + if (IS_ERR(opp)) { + dev_err(dev, "invalid initial frequency %lu kHz.\n", + data->initial_freq); + ret = PTR_ERR(opp); + goto err_alloc_mem; + } + data->curr_opp = opp; + + /* Initialize QoS */ + qos_list = kzalloc( + (sizeof(struct exynos4_dispfreq_pm_qos_table) * _LV_END), + GFP_KERNEL); + if (!data) { + dev_err(dev, "cannot allocate memory.\n"); + goto err_alloc_mem; + } + for (i = 0 ; i < _LV_END ; i++) { + qos_list[i].freq = exynos_dispfreq_clk_table[i].clk; + qos_list[i].qos_value = exynos_dispfreq_clk_table[i].clk; + } + + /* Register exynos4_display as client to opp notifier */ + memset(&data->nb, 0, sizeof(data->nb)); + data->nb.notifier_call = exynos4_dispfreq_opp_notifier_call; + nh = opp_get_notifier(dev); + ret = srcu_notifier_chain_register(nh, &data->nb); + if (ret < 0) { + dev_err(dev, "failed to get pm notifier: %d\n", ret); + goto err_reg_pm_opp; + } + + data->qos_type = PM_QOS_DISPLAY_FREQUENCY; + data->qos_list = qos_list; + + /* Register exynos4_display as client to pm qos notifier */ + memset(&data->nb_qos, 0, sizeof(data->nb_qos)); + data->nb_qos.notifier_call = exynos4_dispfreq_qos_notifier_call; + ret = pm_qos_add_notifier(data->qos_type, &data->nb_qos); + if (ret < 0) { + dev_err(dev, "failed to get pm notifier: %d\n", ret); + goto err_reg_pm_qos; + } + + /* Register exynos4_display as client to pm notifier */ + memset(&data->nb_pm, 0, sizeof(data->nb_pm)); + data->nb_pm.notifier_call = exynos4_dispfreq_pm_notifier_callback; + ret = register_pm_notifier(&data->nb_pm); + if (ret < 0) { + dev_err(dev, "failed to get pm notifier: %d\n", ret); + goto err_reg_pm; + } + + INIT_DELAYED_WORK(&data->wq_lowfreq, exynos4_dispfreq_set_lowfreq); + + platform_set_drvdata(pdev, data); + + return 0; + +err_reg_pm: + pm_qos_remove_notifier(data->qos_type, &data->nb_qos); + +err_reg_pm_qos: + srcu_notifier_chain_unregister(nh, &data->nb); + +err_reg_pm_opp: + kfree(data->qos_list); + +err_alloc_mem: + kfree(data); + + return ret; +} + +static __devexit int exynos4_dispfreq_remove(struct platform_device *pdev) +{ + struct device *dev = &pdev->dev; + struct exynos4_dispfreq_data *data = pdev->dev.platform_data; + struct srcu_notifier_head *nh = opp_get_notifier(dev); + + unregister_pm_notifier(&data->nb_pm); + exynos4_dispfreq_disable(data); + + pm_qos_remove_notifier(data->qos_type, &data->nb_qos); + srcu_notifier_chain_unregister(nh, &data->nb); + + kfree(data->qos_list); + kfree(data); + + return 0; +} + +static int exynos4_dispfreq_suspend(struct device *dev) +{ + /* TODO */ + return 0; +} + +static int exynos4_dispfreq_resume(struct device *dev) +{ + /* TODO */ + return 0; +} + +static const struct dev_pm_ops exynos4_dispfreq_pm = { + .suspend = exynos4_dispfreq_suspend, + .resume = exynos4_dispfreq_resume, +}; + +static struct platform_driver exynos4_dispfreq_driver = { + .probe = exynos4_dispfreq_probe, + .remove = __devexit_p(exynos4_dispfreq_remove), + .driver = { + .name = "exynos4-dispfreq", + .owner = THIS_MODULE, + .pm = &exynos4_dispfreq_pm, + }, +}; + +static int __init exynos4_dispfreq_init(void) +{ + return platform_driver_register(&exynos4_dispfreq_driver); +} +late_initcall(exynos4_dispfreq_init); + +static void __exit exynos4_dispfreq_exit(void) +{ + platform_driver_unregister(&exynos4_dispfreq_driver); +} +module_exit(exynos4_dispfreq_exit); |