aboutsummaryrefslogtreecommitdiffstats
path: root/drivers/video/samsung_duallcd/lcdfreq.c
diff options
context:
space:
mode:
Diffstat (limited to 'drivers/video/samsung_duallcd/lcdfreq.c')
-rw-r--r--drivers/video/samsung_duallcd/lcdfreq.c492
1 files changed, 492 insertions, 0 deletions
diff --git a/drivers/video/samsung_duallcd/lcdfreq.c b/drivers/video/samsung_duallcd/lcdfreq.c
new file mode 100644
index 0000000..8c25c22
--- /dev/null
+++ b/drivers/video/samsung_duallcd/lcdfreq.c
@@ -0,0 +1,492 @@
+/* linux/driver/video/samsung/lcdfreq.c
+ *
+ * EXYNOS4 - support LCD PixelClock change at runtime
+ *
+ * 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/reboot.h>
+#include <linux/suspend.h>
+#include <linux/cpufreq.h>
+#include <linux/sysfs.h>
+#include <linux/gcd.h>
+#include <linux/clk.h>
+#include <linux/spinlock.h>
+
+#include <plat/clock.h>
+#include <plat/clock-clksrc.h>
+#include <plat/regs-fb-s5p.h>
+
+#include "s3cfb.h"
+
+#define LCDFREQ_LIMIT_HZ 40
+
+enum lcdfreq_level_idx {
+ LEVEL_NORMAL,
+ LEVEL_LIMIT,
+ LCDFREQ_LEVEL_END
+};
+
+struct lcdfreq_t {
+ u32 level;
+ u32 hz;
+ u32 vclk;
+ u32 cmu_clkdiv;
+ u32 pixclock;
+};
+
+struct lcdfreq_info {
+ struct lcdfreq_t table[LCDFREQ_LEVEL_END];
+ enum lcdfreq_level_idx level;
+ atomic_t usage;
+ struct mutex lock;
+ spinlock_t slock;
+
+ u32 enable;
+ struct device *dev;
+
+ struct notifier_block pm_noti;
+ struct notifier_block reboot_noti;
+
+ struct delayed_work work;
+
+ struct early_suspend early_suspend;
+};
+
+int get_div(struct s3cfb_global *ctrl)
+{
+ struct clksrc_clk *src_clk;
+ u32 clkdiv;
+
+ src_clk = container_of(ctrl->clock, struct clksrc_clk, clk);
+ clkdiv = __raw_readl(src_clk->reg_div.reg);
+ clkdiv &= 0xf;
+
+ return clkdiv;
+}
+
+int set_div(struct s3cfb_global *ctrl, u32 div)
+{
+ struct lcdfreq_info *lcdfreq = ctrl->data;
+ struct clksrc_clk *src_clk;
+ unsigned long flags;
+ u32 cfg, clkdiv, count = 1000000;
+/* unsigned long timeout = jiffies + msecs_to_jiffies(500); */
+
+ src_clk = container_of(ctrl->clock, struct clksrc_clk, clk);
+
+ do {
+ spin_lock_irqsave(&lcdfreq->slock, flags);
+ clkdiv = __raw_readl(src_clk->reg_div.reg);
+
+ if ((clkdiv & 0xf) == div) {
+ spin_unlock_irqrestore(&lcdfreq->slock, flags);
+ return -EINVAL;
+ }
+
+ clkdiv &= ~(0xff);
+ clkdiv |= (div << 4 | div);
+ cfg = (readl(ctrl->ielcd_regs + S3C_VIDCON1) & S3C_VIDCON1_VSTATUS_MASK);
+ if (cfg == S3C_VIDCON1_VSTATUS_ACTIVE) {
+ cfg = (readl(ctrl->ielcd_regs + S3C_VIDCON1) & S3C_VIDCON1_VSTATUS_MASK);
+ if (cfg == S3C_VIDCON1_VSTATUS_FRONT) {
+ writel(clkdiv, src_clk->reg_div.reg);
+ spin_unlock_irqrestore(&lcdfreq->slock, flags);
+ dev_info(ctrl->dev, "\t%x, count=%d\n", __raw_readl(src_clk->reg_div.reg), 1000000-count);
+ return 0;
+ }
+ }
+ spin_unlock_irqrestore(&lcdfreq->slock, flags);
+ count--;
+ } while (count);
+/* } while (time_before(jiffies, timeout)); */
+
+ dev_err(ctrl->dev, "%s fail, div=%d\n", __func__, div);
+
+ return -EINVAL;
+}
+
+static int set_lcdfreq_div(struct device *dev, enum lcdfreq_level_idx level)
+{
+ struct fb_info *fb = dev_get_drvdata(dev);
+ struct s3cfb_window *win = fb->par;
+ struct s3cfb_global *fbdev = get_fimd_global(win->id);
+
+ struct lcdfreq_info *lcdfreq = fbdev->data;
+
+ u32 div, ret;
+
+ mutex_lock(&lcdfreq->lock);
+
+ if (unlikely(fbdev->system_state == POWER_OFF || !lcdfreq->enable)) {
+ dev_err(dev, "%s reject. %d, %d\n", __func__, fbdev->system_state, lcdfreq->enable);
+ ret = -EINVAL;
+ goto exit;
+ }
+
+ div = lcdfreq->table[level].cmu_clkdiv;
+
+ ret = set_div(fbdev, div);
+
+ if (ret) {
+ dev_err(dev, "fail to change lcd freq\n");
+ goto exit;
+ }
+
+ lcdfreq->level = level;
+
+exit:
+ mutex_unlock(&lcdfreq->lock);
+
+ return ret;
+}
+
+static int lcdfreq_lock(struct device *dev)
+{
+ struct fb_info *fb = dev_get_drvdata(dev);
+ struct s3cfb_window *win = fb->par;
+ struct s3cfb_global *fbdev = get_fimd_global(win->id);
+ struct lcdfreq_info *lcdfreq = fbdev->data;
+
+ int ret;
+
+ if (!atomic_read(&lcdfreq->usage))
+ ret = set_lcdfreq_div(dev, LEVEL_LIMIT);
+ else {
+ dev_err(dev, "lcd freq is already limit state\n");
+ return -EINVAL;
+ }
+
+ if (!ret) {
+ mutex_lock(&lcdfreq->lock);
+ atomic_inc(&lcdfreq->usage);
+ mutex_unlock(&lcdfreq->lock);
+ schedule_delayed_work(&lcdfreq->work, 0);
+ }
+
+ return ret;
+}
+
+static int lcdfreq_lock_free(struct device *dev)
+{
+ struct fb_info *fb = dev_get_drvdata(dev);
+ struct s3cfb_window *win = fb->par;
+ struct s3cfb_global *fbdev = get_fimd_global(win->id);
+ struct lcdfreq_info *lcdfreq = fbdev->data;
+
+ int ret;
+
+ if (atomic_read(&lcdfreq->usage))
+ ret = set_lcdfreq_div(dev, LEVEL_NORMAL);
+ else {
+ dev_err(dev, "lcd freq is already normal state\n");
+ return -EINVAL;
+ }
+
+ if (!ret) {
+ mutex_lock(&lcdfreq->lock);
+ atomic_dec(&lcdfreq->usage);
+ mutex_unlock(&lcdfreq->lock);
+ cancel_delayed_work(&lcdfreq->work);
+ }
+
+ return ret;
+}
+
+int get_divider(struct fb_info *fb)
+{
+ struct s3cfb_window *win = fb->par;
+ struct s3cfb_global *fbdev = get_fimd_global(win->id);
+ struct lcdfreq_info *lcdfreq = fbdev->data;
+ struct clksrc_clk *sclk;
+ struct clk *clk;
+ u32 rate, reg, i;
+ u8 fimd_div;
+
+ sclk = container_of(fbdev->clock, struct clksrc_clk, clk);
+ clk = clk_get_parent(clk_get_parent(fbdev->clock));
+ rate = clk_get_rate(clk);
+
+ lcdfreq->table[LEVEL_NORMAL].cmu_clkdiv =
+ DIV_ROUND_CLOSEST(rate, lcdfreq->table[LEVEL_NORMAL].vclk);
+
+ lcdfreq->table[LEVEL_LIMIT].cmu_clkdiv =
+ DIV_ROUND_CLOSEST(rate, lcdfreq->table[LEVEL_LIMIT].vclk);
+
+ fimd_div = gcd(lcdfreq->table[LEVEL_NORMAL].cmu_clkdiv, lcdfreq->table[LEVEL_LIMIT].cmu_clkdiv);
+
+ lcdfreq->table[LEVEL_NORMAL].cmu_clkdiv /= fimd_div;
+ lcdfreq->table[LEVEL_LIMIT].cmu_clkdiv /= fimd_div;
+
+ dev_info(fb->dev, "%s rate is %d, fimd divider=%d\n", clk->name, rate, fimd_div);
+
+ for (i = 0; i < LCDFREQ_LEVEL_END; i++) {
+ lcdfreq->table[i].cmu_clkdiv--;
+ dev_info(fb->dev, "%dHZ divider is %d\n",
+ lcdfreq->table[i].hz, lcdfreq->table[i].cmu_clkdiv);
+ }
+
+ reg = (readl(fbdev->regs + S3C_VIDCON0) & (S3C_VIDCON0_CLKVAL_F(0xff))) >> 6;
+ reg++;
+
+ if (fimd_div != reg)
+ return -EINVAL;
+
+ reg = (readl(sclk->reg_div.reg)) >> sclk->reg_div.shift;
+ reg &= 0xf;
+
+ if (lcdfreq->table[LEVEL_NORMAL].cmu_clkdiv != reg)
+ return -EINVAL;
+
+ return 0;
+}
+
+static ssize_t level_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct fb_info *fb = dev_get_drvdata(dev);
+ struct s3cfb_window *win = fb->par;
+ struct s3cfb_global *fbdev = get_fimd_global(win->id);
+ struct lcdfreq_info *lcdfreq = fbdev->data;
+
+ if (unlikely(fbdev->system_state == POWER_OFF || !lcdfreq->enable)) {
+ dev_err(dev, "%s reject. %d, %d\n", __func__, fbdev->system_state, lcdfreq->enable);
+ return -EINVAL;
+ }
+
+ return sprintf(buf, "%dHZ, div=%d\n", lcdfreq->table[lcdfreq->level].hz, get_div(fbdev));
+}
+
+static ssize_t level_store(struct device *dev,
+ struct device_attribute *attr, const char *buf, size_t count)
+{
+ unsigned int value;
+ int ret;
+
+ ret = strict_strtoul(buf, 0, (unsigned long *)&value);
+
+ dev_info(dev, "\t%s :: value=%d\n", __func__, value);
+
+ if (value >= LCDFREQ_LEVEL_END)
+ return -EINVAL;
+
+ if (value)
+ ret = lcdfreq_lock(dev);
+ else
+ ret = lcdfreq_lock_free(dev);
+
+ if (ret) {
+ dev_err(dev, "%s fail\n", __func__);
+ return -EINVAL;
+ }
+
+ return count;
+}
+
+static ssize_t usage_show(struct device *dev,
+ struct device_attribute *attr, char *buf)
+{
+ struct fb_info *fb = dev_get_drvdata(dev);
+ struct s3cfb_window *win = fb->par;
+ struct s3cfb_global *fbdev = get_fimd_global(win->id);
+ struct lcdfreq_info *lcdfreq = fbdev->data;
+
+ return sprintf(buf, "%d\n", atomic_read(&lcdfreq->usage));
+}
+
+static DEVICE_ATTR(level, S_IRUGO|S_IWUSR, level_show, level_store);
+static DEVICE_ATTR(usage, S_IRUGO, usage_show, NULL);
+
+static struct attribute *lcdfreq_attributes[] = {
+ &dev_attr_level.attr,
+ &dev_attr_usage.attr,
+ NULL,
+};
+
+static struct attribute_group lcdfreq_attr_group = {
+ .name = "lcdfreq",
+ .attrs = lcdfreq_attributes,
+};
+
+static void lcdfreq_early_suspend(struct early_suspend *h)
+{
+ struct lcdfreq_info *lcdfreq =
+ container_of(h, struct lcdfreq_info, early_suspend);
+
+ dev_info(lcdfreq->dev, "%s\n", __func__);
+
+ mutex_lock(&lcdfreq->lock);
+ lcdfreq->enable = false;
+ lcdfreq->level = LEVEL_NORMAL;
+ atomic_set(&lcdfreq->usage, 0);
+ mutex_unlock(&lcdfreq->lock);
+
+ return ;
+}
+
+static void lcdfreq_late_resume(struct early_suspend *h)
+{
+ struct lcdfreq_info *lcdfreq =
+ container_of(h, struct lcdfreq_info, early_suspend);
+
+ dev_info(lcdfreq->dev, "%s\n", __func__);
+
+ mutex_lock(&lcdfreq->lock);
+ lcdfreq->enable = true;
+ mutex_unlock(&lcdfreq->lock);
+
+ return;
+}
+
+static int lcdfreq_pm_notifier_event(struct notifier_block *this,
+ unsigned long event, void *ptr)
+{
+ struct lcdfreq_info *lcdfreq =
+ container_of(this, struct lcdfreq_info, pm_noti);
+
+ dev_info(lcdfreq->dev, "%s :: event=%ld\n", __func__, event);
+
+ switch (event) {
+ case PM_SUSPEND_PREPARE:
+ mutex_lock(&lcdfreq->lock);
+ lcdfreq->enable = false;
+ lcdfreq->level = LEVEL_NORMAL;
+ atomic_set(&lcdfreq->usage, 0);
+ mutex_unlock(&lcdfreq->lock);
+ return NOTIFY_OK;
+ case PM_POST_RESTORE:
+ case PM_POST_SUSPEND:
+ mutex_lock(&lcdfreq->lock);
+ lcdfreq->enable = true;
+ mutex_unlock(&lcdfreq->lock);
+ return NOTIFY_OK;
+ }
+ return NOTIFY_DONE;
+}
+
+static int lcdfreq_reboot_notify(struct notifier_block *this,
+ unsigned long code, void *unused)
+{
+ struct lcdfreq_info *lcdfreq =
+ container_of(this, struct lcdfreq_info, reboot_noti);
+
+ mutex_lock(&lcdfreq->lock);
+ lcdfreq->enable = false;
+ lcdfreq->level = LEVEL_NORMAL;
+ atomic_set(&lcdfreq->usage, 0);
+ mutex_unlock(&lcdfreq->lock);
+
+ dev_info(lcdfreq->dev, "%s\n", __func__);
+
+ return NOTIFY_DONE;
+}
+
+static void lcdfreq_status_work(struct work_struct *work)
+{
+ struct lcdfreq_info *lcdfreq =
+ container_of(work, struct lcdfreq_info, work.work);
+
+ u32 hz = lcdfreq->table[lcdfreq->level].hz;
+
+ cancel_delayed_work(&lcdfreq->work);
+
+ dev_info(lcdfreq->dev, "\tHZ=%d, usage=%d\n", hz, atomic_read(&lcdfreq->usage));
+
+ schedule_delayed_work(&lcdfreq->work, HZ*120);
+}
+
+int lcdfreq_init(struct fb_info *fb)
+{
+ struct s3cfb_window *win = fb->par;
+ struct s3cfb_global *fbdev = get_fimd_global(win->id);
+ struct s3cfb_lcd *lcd = fbdev->lcd;
+ struct fb_var_screeninfo *var = &fb->var;
+
+ struct lcdfreq_info *lcdfreq = NULL;
+ u32 vclk = 0;
+ int ret;
+
+ lcdfreq = kzalloc(sizeof(struct lcdfreq_info), GFP_KERNEL);
+ if (!lcdfreq) {
+ pr_err("fail to allocate for lcdfreq\n");
+ ret = -ENOMEM;
+ goto err_1;
+ }
+
+ fbdev->data = lcdfreq;
+
+ lcdfreq->dev = fb->dev;
+ lcdfreq->level = LEVEL_NORMAL;
+
+ vclk = (lcd->freq *
+ (var->left_margin + var->right_margin
+ + var->hsync_len + var->xres) *
+ (var->upper_margin + var->lower_margin
+ + var->vsync_len + var->yres));
+ lcdfreq->table[LEVEL_NORMAL].level = LEVEL_NORMAL;
+ lcdfreq->table[LEVEL_NORMAL].vclk = vclk;
+ lcdfreq->table[LEVEL_NORMAL].pixclock = var->pixclock;
+ lcdfreq->table[LEVEL_NORMAL].hz = lcd->freq;
+
+ vclk = (LCDFREQ_LIMIT_HZ *
+ (var->left_margin + var->right_margin
+ + var->hsync_len + var->xres) *
+ (var->upper_margin + var->lower_margin
+ + var->vsync_len + var->yres));
+ lcdfreq->table[LEVEL_LIMIT].level = LEVEL_LIMIT;
+ lcdfreq->table[LEVEL_LIMIT].vclk = vclk;
+ lcdfreq->table[LEVEL_LIMIT].pixclock = KHZ2PICOS(vclk/1000);
+ lcdfreq->table[LEVEL_LIMIT].hz = LCDFREQ_LIMIT_HZ;
+
+ ret = get_divider(fb);
+ if (ret < 0) {
+ pr_err("fail to init lcd freq switch");
+ fbdev->data = NULL;
+ goto err_1;
+ }
+
+ atomic_set(&lcdfreq->usage, 0);
+ mutex_init(&lcdfreq->lock);
+ spin_lock_init(&lcdfreq->slock);
+
+ INIT_DELAYED_WORK_DEFERRABLE(&lcdfreq->work, lcdfreq_status_work);
+
+ ret = sysfs_create_group(&fb->dev->kobj, &lcdfreq_attr_group);
+ if (ret < 0) {
+ pr_err("fail to add sysfs entries, %d\n", __LINE__);
+ goto err_2;
+ }
+
+ lcdfreq->early_suspend.suspend = lcdfreq_early_suspend;
+ lcdfreq->early_suspend.resume = lcdfreq_late_resume;
+ lcdfreq->early_suspend.level = EARLY_SUSPEND_LEVEL_STOP_DRAWING + 1;
+
+ register_early_suspend(&lcdfreq->early_suspend);
+
+ lcdfreq->pm_noti.notifier_call = lcdfreq_pm_notifier_event;
+ lcdfreq->reboot_noti.notifier_call = lcdfreq_reboot_notify;
+
+ if (register_reboot_notifier(&lcdfreq->reboot_noti)) {
+ pr_err("fail to setup reboot notifier\n");
+ goto err_3;
+ }
+
+ lcdfreq->enable = true;
+
+ dev_info(lcdfreq->dev, "%s is done\n", __func__);
+
+ return 0;
+
+err_3:
+ sysfs_remove_group(&fb->dev->kobj, &lcdfreq_attr_group);
+
+err_2:
+ kfree(lcdfreq);
+
+err_1:
+ return ret;
+
+}
+