backlight: Add Samsung LTV350QV LCD driver
authorHaavard Skinnemoen <hskinnemoen@atmel.com>
Sun, 2 Sep 2007 22:15:49 +0000 (23:15 +0100)
committerRichard Purdie <rpurdie@rpsys.net>
Thu, 11 Oct 2007 21:24:12 +0000 (22:24 +0100)
This patch adds support for powering on and off the Samsung LTV350QV LCD
panel via SPI.  The driver responds to framebuffer power management, it
powers off the panel on reboot/halt/poweroff.  It can also be controlled
through sysfs.  The panel is powered up when the module is loaded, and off
when the module is unloaded.  Verified on AVR32 STK1000.

Signed-off-by: Haavard Skinnemoen <hskinnemoen@atmel.com>
Signed-off-by: David Brownell <dbrownell@users.sourceforge.net>
Signed-off-by: Richard Purdie <rpurdie@rpsys.net>
drivers/video/backlight/Kconfig
drivers/video/backlight/Makefile
drivers/video/backlight/ltv350qv.c [new file with mode: 0644]
drivers/video/backlight/ltv350qv.h [new file with mode: 0644]

index 2580f5fa2486e4caefbed82b3bbd357901926151..b6f936a091855d2ee97c7f30ad70060ac0db2243 100644 (file)
@@ -24,6 +24,18 @@ config LCD_CLASS_DEVICE
          To have support for your specific LCD panel you will have to
          select the proper drivers which depend on this option.
 
+config LCD_LTV350QV
+       tristate "Samsung LTV350QV LCD Panel"
+       depends on LCD_CLASS_DEVICE && SPI_MASTER
+       default n
+       help
+         If you have a Samsung LTV350QV LCD panel, say y to include a
+         power control driver for it.  The panel starts up in power
+         off state, so you need this driver in order to see any
+         output.
+
+         The LTV350QV panel is present on all ATSTK1000 boards.
+
 #
 # Backlight
 #
index c6e2266f63e2d6d7f728f196d31b65864d8699f5..965a78b18118f09d70a3ef9dcdf429d40d725e90 100644 (file)
@@ -1,6 +1,8 @@
 # Backlight & LCD drivers
 
 obj-$(CONFIG_LCD_CLASS_DEVICE)     += lcd.o
+obj-$(CONFIG_LCD_LTV350QV)     += ltv350qv.o
+
 obj-$(CONFIG_BACKLIGHT_CLASS_DEVICE) += backlight.o
 obj-$(CONFIG_BACKLIGHT_CORGI)  += corgi_bl.o
 obj-$(CONFIG_BACKLIGHT_HP680)  += hp680_bl.o
diff --git a/drivers/video/backlight/ltv350qv.c b/drivers/video/backlight/ltv350qv.c
new file mode 100644 (file)
index 0000000..2eb206b
--- /dev/null
@@ -0,0 +1,330 @@
+/*
+ * Power control for Samsung LTV350QV Quarter VGA LCD Panel
+ *
+ * Copyright (C) 2006, 2007 Atmel Corporation
+ *
+ * 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/delay.h>
+#include <linux/err.h>
+#include <linux/fb.h>
+#include <linux/init.h>
+#include <linux/lcd.h>
+#include <linux/module.h>
+#include <linux/spi/spi.h>
+
+#include "ltv350qv.h"
+
+#define POWER_IS_ON(pwr)       ((pwr) <= FB_BLANK_NORMAL)
+
+struct ltv350qv {
+       struct spi_device       *spi;
+       u8                      *buffer;
+       int                     power;
+       struct lcd_device       *ld;
+};
+
+/*
+ * The power-on and power-off sequences are taken from the
+ * LTV350QV-F04 data sheet from Samsung. The register definitions are
+ * taken from the S6F2002 command list also from Samsung. Both
+ * documents are distributed with the AVR32 Linux BSP CD from Atmel.
+ *
+ * There's still some voodoo going on here, but it's a lot better than
+ * in the first incarnation of the driver where all we had was the raw
+ * numbers from the initialization sequence.
+ */
+static int ltv350qv_write_reg(struct ltv350qv *lcd, u8 reg, u16 val)
+{
+       struct spi_message msg;
+       struct spi_transfer index_xfer = {
+               .len            = 3,
+               .cs_change      = 1,
+       };
+       struct spi_transfer value_xfer = {
+               .len            = 3,
+       };
+
+       spi_message_init(&msg);
+
+       /* register index */
+       lcd->buffer[0] = LTV_OPC_INDEX;
+       lcd->buffer[1] = 0x00;
+       lcd->buffer[2] = reg & 0x7f;
+       index_xfer.tx_buf = lcd->buffer;
+       spi_message_add_tail(&index_xfer, &msg);
+
+       /* register value */
+       lcd->buffer[4] = LTV_OPC_DATA;
+       lcd->buffer[5] = val >> 8;
+       lcd->buffer[6] = val;
+       value_xfer.tx_buf = lcd->buffer + 4;
+       spi_message_add_tail(&value_xfer, &msg);
+
+       return spi_sync(lcd->spi, &msg);
+}
+
+/* The comments are taken straight from the data sheet */
+static int ltv350qv_power_on(struct ltv350qv *lcd)
+{
+       int ret;
+
+       /* Power On Reset Display off State */
+       if (ltv350qv_write_reg(lcd, LTV_PWRCTL1, 0x0000))
+               goto err;
+       msleep(15);
+
+       /* Power Setting Function 1 */
+       if (ltv350qv_write_reg(lcd, LTV_PWRCTL1, LTV_VCOM_DISABLE))
+               goto err;
+       if (ltv350qv_write_reg(lcd, LTV_PWRCTL2, LTV_VCOML_ENABLE))
+               goto err_power1;
+
+       /* Power Setting Function 2 */
+       if (ltv350qv_write_reg(lcd, LTV_PWRCTL1,
+                              LTV_VCOM_DISABLE | LTV_DRIVE_CURRENT(5)
+                              | LTV_SUPPLY_CURRENT(5)))
+               goto err_power2;
+
+       msleep(55);
+
+       /* Instruction Setting */
+       ret = ltv350qv_write_reg(lcd, LTV_IFCTL,
+                                LTV_NMD | LTV_REV | LTV_NL(0x1d));
+       ret |= ltv350qv_write_reg(lcd, LTV_DATACTL,
+                                 LTV_DS_SAME | LTV_CHS_480
+                                 | LTV_DF_RGB | LTV_RGB_BGR);
+       ret |= ltv350qv_write_reg(lcd, LTV_ENTRY_MODE,
+                                 LTV_VSPL_ACTIVE_LOW
+                                 | LTV_HSPL_ACTIVE_LOW
+                                 | LTV_DPL_SAMPLE_RISING
+                                 | LTV_EPL_ACTIVE_LOW
+                                 | LTV_SS_RIGHT_TO_LEFT);
+       ret |= ltv350qv_write_reg(lcd, LTV_GATECTL1, LTV_CLW(3));
+       ret |= ltv350qv_write_reg(lcd, LTV_GATECTL2,
+                                 LTV_NW_INV_1LINE | LTV_FWI(3));
+       ret |= ltv350qv_write_reg(lcd, LTV_VBP, 0x000a);
+       ret |= ltv350qv_write_reg(lcd, LTV_HBP, 0x0021);
+       ret |= ltv350qv_write_reg(lcd, LTV_SOTCTL, LTV_SDT(3) | LTV_EQ(0));
+       ret |= ltv350qv_write_reg(lcd, LTV_GAMMA(0), 0x0103);
+       ret |= ltv350qv_write_reg(lcd, LTV_GAMMA(1), 0x0301);
+       ret |= ltv350qv_write_reg(lcd, LTV_GAMMA(2), 0x1f0f);
+       ret |= ltv350qv_write_reg(lcd, LTV_GAMMA(3), 0x1f0f);
+       ret |= ltv350qv_write_reg(lcd, LTV_GAMMA(4), 0x0707);
+       ret |= ltv350qv_write_reg(lcd, LTV_GAMMA(5), 0x0307);
+       ret |= ltv350qv_write_reg(lcd, LTV_GAMMA(6), 0x0707);
+       ret |= ltv350qv_write_reg(lcd, LTV_GAMMA(7), 0x0000);
+       ret |= ltv350qv_write_reg(lcd, LTV_GAMMA(8), 0x0004);
+       ret |= ltv350qv_write_reg(lcd, LTV_GAMMA(9), 0x0000);
+       if (ret)
+               goto err_settings;
+
+       /* Wait more than 2 frames */
+       msleep(20);
+
+       /* Display On Sequence */
+       ret = ltv350qv_write_reg(lcd, LTV_PWRCTL1,
+                                LTV_VCOM_DISABLE | LTV_VCOMOUT_ENABLE
+                                | LTV_POWER_ON | LTV_DRIVE_CURRENT(5)
+                                | LTV_SUPPLY_CURRENT(5));
+       ret |= ltv350qv_write_reg(lcd, LTV_GATECTL2,
+                                 LTV_NW_INV_1LINE | LTV_DSC | LTV_FWI(3));
+       if (ret)
+               goto err_disp_on;
+
+       /* Display should now be ON. Phew. */
+       return 0;
+
+err_disp_on:
+       /*
+        * Try to recover. Error handling probably isn't very useful
+        * at this point, just make a best effort to switch the panel
+        * off.
+        */
+       ltv350qv_write_reg(lcd, LTV_PWRCTL1,
+                          LTV_VCOM_DISABLE | LTV_DRIVE_CURRENT(5)
+                          | LTV_SUPPLY_CURRENT(5));
+       ltv350qv_write_reg(lcd, LTV_GATECTL2,
+                          LTV_NW_INV_1LINE | LTV_FWI(3));
+err_settings:
+err_power2:
+err_power1:
+       ltv350qv_write_reg(lcd, LTV_PWRCTL2, 0x0000);
+       msleep(1);
+err:
+       ltv350qv_write_reg(lcd, LTV_PWRCTL1, LTV_VCOM_DISABLE);
+       return -EIO;
+}
+
+static int ltv350qv_power_off(struct ltv350qv *lcd)
+{
+       int ret;
+
+       /* Display Off Sequence */
+       ret = ltv350qv_write_reg(lcd, LTV_PWRCTL1,
+                                LTV_VCOM_DISABLE
+                                | LTV_DRIVE_CURRENT(5)
+                                | LTV_SUPPLY_CURRENT(5));
+       ret |= ltv350qv_write_reg(lcd, LTV_GATECTL2,
+                                 LTV_NW_INV_1LINE | LTV_FWI(3));
+
+       /* Power down setting 1 */
+       ret |= ltv350qv_write_reg(lcd, LTV_PWRCTL2, 0x0000);
+
+       /* Wait at least 1 ms */
+       msleep(1);
+
+       /* Power down setting 2 */
+       ret |= ltv350qv_write_reg(lcd, LTV_PWRCTL1, LTV_VCOM_DISABLE);
+
+       /*
+        * No point in trying to recover here. If we can't switch the
+        * panel off, what are we supposed to do other than inform the
+        * user about the failure?
+        */
+       if (ret)
+               return -EIO;
+
+       /* Display power should now be OFF */
+       return 0;
+}
+
+static int ltv350qv_power(struct ltv350qv *lcd, int power)
+{
+       int ret = 0;
+
+       if (POWER_IS_ON(power) && !POWER_IS_ON(lcd->power))
+               ret = ltv350qv_power_on(lcd);
+       else if (!POWER_IS_ON(power) && POWER_IS_ON(lcd->power))
+               ret = ltv350qv_power_off(lcd);
+
+       if (!ret)
+               lcd->power = power;
+
+       return ret;
+}
+
+static int ltv350qv_set_power(struct lcd_device *ld, int power)
+{
+       struct ltv350qv *lcd = lcd_get_data(ld);
+
+       return ltv350qv_power(lcd, power);
+}
+
+static int ltv350qv_get_power(struct lcd_device *ld)
+{
+       struct ltv350qv *lcd = lcd_get_data(ld);
+
+       return lcd->power;
+}
+
+static struct lcd_ops ltv_ops = {
+       .get_power      = ltv350qv_get_power,
+       .set_power      = ltv350qv_set_power,
+};
+
+static int __devinit ltv350qv_probe(struct spi_device *spi)
+{
+       struct ltv350qv *lcd;
+       struct lcd_device *ld;
+       int ret;
+
+       lcd = kzalloc(sizeof(struct ltv350qv), GFP_KERNEL);
+       if (!lcd)
+               return -ENOMEM;
+
+       lcd->spi = spi;
+       lcd->power = FB_BLANK_POWERDOWN;
+       lcd->buffer = kzalloc(8, GFP_KERNEL);
+
+       ld = lcd_device_register("ltv350qv", &spi->dev, lcd, &ltv_ops);
+       if (IS_ERR(ld)) {
+               ret = PTR_ERR(ld);
+               goto out_free_lcd;
+       }
+       lcd->ld = ld;
+
+       ret = ltv350qv_power(lcd, FB_BLANK_UNBLANK);
+       if (ret)
+               goto out_unregister;
+
+       dev_set_drvdata(&spi->dev, lcd);
+
+       return 0;
+
+out_unregister:
+       lcd_device_unregister(ld);
+out_free_lcd:
+       kfree(lcd);
+       return ret;
+}
+
+static int __devexit ltv350qv_remove(struct spi_device *spi)
+{
+       struct ltv350qv *lcd = dev_get_drvdata(&spi->dev);
+
+       ltv350qv_power(lcd, FB_BLANK_POWERDOWN);
+       lcd_device_unregister(lcd->ld);
+       kfree(lcd);
+
+       return 0;
+}
+
+#ifdef CONFIG_PM
+static int ltv350qv_suspend(struct spi_device *spi, pm_message_t state)
+{
+       struct ltv350qv *lcd = dev_get_drvdata(&spi->dev);
+
+       return ltv350qv_power(lcd, FB_BLANK_POWERDOWN);
+}
+
+static int ltv350qv_resume(struct spi_device *spi)
+{
+       struct ltv350qv *lcd = dev_get_drvdata(&spi->dev);
+
+       return ltv350qv_power(lcd, FB_BLANK_UNBLANK);
+}
+#else
+#define ltv350qv_suspend       NULL
+#define ltv350qv_resume                NULL
+#endif
+
+/* Power down all displays on reboot, poweroff or halt */
+static void ltv350qv_shutdown(struct spi_device *spi)
+{
+       struct ltv350qv *lcd = dev_get_drvdata(&spi->dev);
+
+       ltv350qv_power(lcd, FB_BLANK_POWERDOWN);
+}
+
+static struct spi_driver ltv350qv_driver = {
+       .driver = {
+               .name           = "ltv350qv",
+               .bus            = &spi_bus_type,
+               .owner          = THIS_MODULE,
+       },
+
+       .probe          = ltv350qv_probe,
+       .remove         = __devexit_p(ltv350qv_remove),
+       .shutdown       = ltv350qv_shutdown,
+       .suspend        = ltv350qv_suspend,
+       .resume         = ltv350qv_resume,
+};
+
+static int __init ltv350qv_init(void)
+{
+       return spi_register_driver(&ltv350qv_driver);
+}
+
+static void __exit ltv350qv_exit(void)
+{
+       spi_unregister_driver(&ltv350qv_driver);
+}
+module_init(ltv350qv_init);
+module_exit(ltv350qv_exit);
+
+MODULE_AUTHOR("Haavard Skinnemoen <hskinnemoen@atmel.com>");
+MODULE_DESCRIPTION("Samsung LTV350QV LCD Driver");
+MODULE_LICENSE("GPL");
diff --git a/drivers/video/backlight/ltv350qv.h b/drivers/video/backlight/ltv350qv.h
new file mode 100644 (file)
index 0000000..189112e
--- /dev/null
@@ -0,0 +1,95 @@
+/*
+ * Register definitions for Samsung LTV350QV Quarter VGA LCD Panel
+ *
+ * Copyright (C) 2006, 2007 Atmel Corporation
+ *
+ * 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.
+ */
+#ifndef __LTV350QV_H
+#define __LTV350QV_H
+
+#define LTV_OPC_INDEX  0x74
+#define LTV_OPC_DATA   0x76
+
+#define LTV_ID         0x00            /* ID Read */
+#define LTV_IFCTL      0x01            /* Display Interface Control */
+#define LTV_DATACTL    0x02            /* Display Data Control */
+#define LTV_ENTRY_MODE 0x03            /* Entry Mode */
+#define LTV_GATECTL1   0x04            /* Gate Control 1 */
+#define LTV_GATECTL2   0x05            /* Gate Control 2 */
+#define LTV_VBP                0x06            /* Vertical Back Porch */
+#define LTV_HBP                0x07            /* Horizontal Back Porch */
+#define LTV_SOTCTL     0x08            /* Source Output Timing Control */
+#define LTV_PWRCTL1    0x09            /* Power Control 1 */
+#define LTV_PWRCTL2    0x0a            /* Power Control 2 */
+#define LTV_GAMMA(x)   (0x10 + (x))    /* Gamma control */
+
+/* Bit definitions for LTV_IFCTL */
+#define LTV_IM                 (1 << 15)
+#define LTV_NMD                        (1 << 14)
+#define LTV_SSMD               (1 << 13)
+#define LTV_REV                        (1 <<  7)
+#define LTV_NL(x)              (((x) & 0x001f) << 0)
+
+/* Bit definitions for LTV_DATACTL */
+#define LTV_DS_SAME            (0 << 12)
+#define LTV_DS_D_TO_S          (1 << 12)
+#define LTV_DS_S_TO_D          (2 << 12)
+#define LTV_CHS_384            (0 <<  9)
+#define LTV_CHS_480            (1 <<  9)
+#define LTV_CHS_492            (2 <<  9)
+#define LTV_DF_RGB             (0 <<  6)
+#define LTV_DF_RGBX            (1 <<  6)
+#define LTV_DF_XRGB            (2 <<  6)
+#define LTV_RGB_RGB            (0 <<  2)
+#define LTV_RGB_BGR            (1 <<  2)
+#define LTV_RGB_GRB            (2 <<  2)
+#define LTV_RGB_RBG            (3 <<  2)
+
+/* Bit definitions for LTV_ENTRY_MODE */
+#define LTV_VSPL_ACTIVE_LOW    (0 << 15)
+#define LTV_VSPL_ACTIVE_HIGH   (1 << 15)
+#define LTV_HSPL_ACTIVE_LOW    (0 << 14)
+#define LTV_HSPL_ACTIVE_HIGH   (1 << 14)
+#define LTV_DPL_SAMPLE_RISING  (0 << 13)
+#define LTV_DPL_SAMPLE_FALLING (1 << 13)
+#define LTV_EPL_ACTIVE_LOW     (0 << 12)
+#define LTV_EPL_ACTIVE_HIGH    (1 << 12)
+#define LTV_SS_LEFT_TO_RIGHT   (0 <<  8)
+#define LTV_SS_RIGHT_TO_LEFT   (1 <<  8)
+#define LTV_STB                        (1 <<  1)
+
+/* Bit definitions for LTV_GATECTL1 */
+#define LTV_CLW(x)             (((x) & 0x0007) << 12)
+#define LTV_GAON               (1 <<  5)
+#define LTV_SDR                        (1 <<  3)
+
+/* Bit definitions for LTV_GATECTL2 */
+#define LTV_NW_INV_FRAME       (0 << 14)
+#define LTV_NW_INV_1LINE       (1 << 14)
+#define LTV_NW_INV_2LINE       (2 << 14)
+#define LTV_DSC                        (1 << 12)
+#define LTV_GIF                        (1 <<  8)
+#define LTV_FHN                        (1 <<  7)
+#define LTV_FTI(x)             (((x) & 0x0003) << 4)
+#define LTV_FWI(x)             (((x) & 0x0003) << 0)
+
+/* Bit definitions for LTV_SOTCTL */
+#define LTV_SDT(x)             (((x) & 0x0007) << 10)
+#define LTV_EQ(x)              (((x) & 0x0007) <<  2)
+
+/* Bit definitions for LTV_PWRCTL1 */
+#define LTV_VCOM_DISABLE       (1 << 14)
+#define LTV_VCOMOUT_ENABLE     (1 << 11)
+#define LTV_POWER_ON           (1 <<  9)
+#define LTV_DRIVE_CURRENT(x)   (((x) & 0x0007) << 4)   /* 0=off, 5=max */
+#define LTV_SUPPLY_CURRENT(x)  (((x) & 0x0007) << 0)   /* 0=off, 5=max */
+
+/* Bit definitions for LTV_PWRCTL2 */
+#define LTV_VCOML_ENABLE       (1 << 13)
+#define LTV_VCOML_VOLTAGE(x)   (((x) & 0x001f) << 8)   /* 0=1V, 31=-1V */
+#define LTV_VCOMH_VOLTAGE(x)   (((x) & 0x001f) << 0)   /* 0=3V, 31=4.5V */
+
+#endif /* __LTV350QV_H */