atmel_lcdfb: backlight control

On the sam9 EK boards, the LCD backlight is hooked up to a PWM output from
the LCD controller.  It's controlled by "contrast" registers though.

This patch lets boards declare that they have that kind of backlight
control.  The driver can then export this control, letting screenblank and
other operations actually take effect ...  reducing the typically
substantial power drain from the backlight.

Note that it's not fully cooked
  - doesn't force backlight off during system suspend
  - the "power" and "blank" events may not be done right
This should be easily added in the future.

[nicolas.ferre@atmel.com: remove unneeded inline and rename functions]
Signed-off-by: David Brownell <dbrownell@users.sourceforge.net>
Signed-off-by: Nicolas Ferre <nicolas.ferre@atmel.com>
Cc: Andrew Victor <linux@maxim.org.za>
Cc: Russell King <rmk@arm.linux.org.uk>
Cc: "Antonino A. Daplas" <adaplas@pol.net>
Signed-off-by: Andrew Morton <akpm@linux-foundation.org>
Signed-off-by: Linus Torvalds <torvalds@linux-foundation.org>
diff --git a/drivers/video/atmel_lcdfb.c b/drivers/video/atmel_lcdfb.c
index 5d22ea5..fc65c02 100644
--- a/drivers/video/atmel_lcdfb.c
+++ b/drivers/video/atmel_lcdfb.c
@@ -16,6 +16,7 @@
 #include <linux/fb.h>
 #include <linux/init.h>
 #include <linux/delay.h>
+#include <linux/backlight.h>
 
 #include <asm/arch/board.h>
 #include <asm/arch/cpu.h>
@@ -69,6 +70,107 @@
 }
 #endif
 
+static const u32 contrast_ctr = ATMEL_LCDC_PS_DIV8
+		| ATMEL_LCDC_POL_POSITIVE
+		| ATMEL_LCDC_ENA_PWMENABLE;
+
+#ifdef CONFIG_BACKLIGHT_ATMEL_LCDC
+
+/* some bl->props field just changed */
+static int atmel_bl_update_status(struct backlight_device *bl)
+{
+	struct atmel_lcdfb_info *sinfo = bl_get_data(bl);
+	int			power = sinfo->bl_power;
+	int			brightness = bl->props.brightness;
+
+	/* REVISIT there may be a meaningful difference between
+	 * fb_blank and power ... there seem to be some cases
+	 * this doesn't handle correctly.
+	 */
+	if (bl->props.fb_blank != sinfo->bl_power)
+		power = bl->props.fb_blank;
+	else if (bl->props.power != sinfo->bl_power)
+		power = bl->props.power;
+
+	if (brightness < 0 && power == FB_BLANK_UNBLANK)
+		brightness = lcdc_readl(sinfo, ATMEL_LCDC_CONTRAST_VAL);
+	else if (power != FB_BLANK_UNBLANK)
+		brightness = 0;
+
+	lcdc_writel(sinfo, ATMEL_LCDC_CONTRAST_VAL, brightness);
+	lcdc_writel(sinfo, ATMEL_LCDC_CONTRAST_CTR,
+			brightness ? contrast_ctr : 0);
+
+	bl->props.fb_blank = bl->props.power = sinfo->bl_power = power;
+
+	return 0;
+}
+
+static int atmel_bl_get_brightness(struct backlight_device *bl)
+{
+	struct atmel_lcdfb_info *sinfo = bl_get_data(bl);
+
+	return lcdc_readl(sinfo, ATMEL_LCDC_CONTRAST_VAL);
+}
+
+static struct backlight_ops atmel_lcdc_bl_ops = {
+	.update_status = atmel_bl_update_status,
+	.get_brightness = atmel_bl_get_brightness,
+};
+
+static void init_backlight(struct atmel_lcdfb_info *sinfo)
+{
+	struct backlight_device	*bl;
+
+	sinfo->bl_power = FB_BLANK_UNBLANK;
+
+	if (sinfo->backlight)
+		return;
+
+	bl = backlight_device_register("backlight", &sinfo->pdev->dev,
+			sinfo, &atmel_lcdc_bl_ops);
+	if (IS_ERR(sinfo->backlight)) {
+		dev_err(&sinfo->pdev->dev, "error %ld on backlight register\n",
+				PTR_ERR(bl));
+		return;
+	}
+	sinfo->backlight = bl;
+
+	bl->props.power = FB_BLANK_UNBLANK;
+	bl->props.fb_blank = FB_BLANK_UNBLANK;
+	bl->props.max_brightness = 0xff;
+	bl->props.brightness = atmel_bl_get_brightness(bl);
+}
+
+static void exit_backlight(struct atmel_lcdfb_info *sinfo)
+{
+	if (sinfo->backlight)
+		backlight_device_unregister(sinfo->backlight);
+}
+
+#else
+
+static void init_backlight(struct atmel_lcdfb_info *sinfo)
+{
+	dev_warn(&sinfo->pdev->dev, "backlight control is not available\n");
+}
+
+static void exit_backlight(struct atmel_lcdfb_info *sinfo)
+{
+}
+
+#endif
+
+static void init_contrast(struct atmel_lcdfb_info *sinfo)
+{
+	/* have some default contrast/backlight settings */
+	lcdc_writel(sinfo, ATMEL_LCDC_CONTRAST_CTR, contrast_ctr);
+	lcdc_writel(sinfo, ATMEL_LCDC_CONTRAST_VAL, ATMEL_LCDC_CVAL_DEFAULT);
+
+	if (sinfo->lcdcon_is_backlight)
+		init_backlight(sinfo);
+}
+
 
 static struct fb_fix_screeninfo atmel_lcdfb_fix __initdata = {
 	.type		= FB_TYPE_PACKED_PIXELS,
@@ -390,10 +492,6 @@
 	/* Disable all interrupts */
 	lcdc_writel(sinfo, ATMEL_LCDC_IDR, ~0UL);
 
-	/* Set contrast */
-	value = ATMEL_LCDC_PS_DIV8 | ATMEL_LCDC_POL_POSITIVE | ATMEL_LCDC_ENA_PWMENABLE;
-	lcdc_writel(sinfo, ATMEL_LCDC_CONTRAST_CTR, value);
-	lcdc_writel(sinfo, ATMEL_LCDC_CONTRAST_VAL, ATMEL_LCDC_CVAL_DEFAULT);
 	/* ...wait for DMA engine to become idle... */
 	while (lcdc_readl(sinfo, ATMEL_LCDC_DMACON) & ATMEL_LCDC_DMABUSY)
 		msleep(10);
@@ -597,6 +695,7 @@
 		sinfo->default_monspecs = pdata_sinfo->default_monspecs;
 		sinfo->atmel_lcdfb_power_control = pdata_sinfo->atmel_lcdfb_power_control;
 		sinfo->guard_time = pdata_sinfo->guard_time;
+		sinfo->lcdcon_is_backlight = pdata_sinfo->lcdcon_is_backlight;
 	} else {
 		dev_err(dev, "cannot get default configuration\n");
 		goto free_info;
@@ -690,6 +789,9 @@
 		goto release_mem;
 	}
 
+	/* Initialize PWM for contrast or backlight ("off") */
+	init_contrast(sinfo);
+
 	/* interrupt */
 	ret = request_irq(sinfo->irq_base, atmel_lcdfb_interrupt, 0, pdev->name, info);
 	if (ret) {
@@ -741,6 +843,7 @@
 unregister_irqs:
 	free_irq(sinfo->irq_base, info);
 unmap_mmio:
+	exit_backlight(sinfo);
 	iounmap(sinfo->mmio);
 release_mem:
  	release_mem_region(info->fix.mmio_start, info->fix.mmio_len);
@@ -775,6 +878,7 @@
 	if (!sinfo)
 		return 0;
 
+	exit_backlight(sinfo);
 	if (sinfo->atmel_lcdfb_power_control)
 		sinfo->atmel_lcdfb_power_control(0);
 	unregister_framebuffer(info);
@@ -801,6 +905,9 @@
 
 static struct platform_driver atmel_lcdfb_driver = {
 	.remove		= __exit_p(atmel_lcdfb_remove),
+
+// FIXME need suspend, resume
+
 	.driver		= {
 		.name	= "atmel_lcdfb",
 		.owner	= THIS_MODULE,