/*
 * Freescale STMP37XX/STMP378X framebuffer driver
 *
 * Author: Vitaly Wool <vital@embeddedalley.com>
 *
 * Copyright 2008-2009 Freescale Semiconductor, Inc. All Rights Reserved.
 * Copyright 2008 Embedded Alley Solutions, Inc All Rights Reserved.
 */

/*
 * The code contained herein is licensed under the GNU General Public
 * License. You may obtain a copy of the GNU General Public License
 * Version 2 or later at the following locations:
 *
 * http://www.opensource.org/licenses/gpl-license.html
 * http://www.gnu.org/copyleft/gpl.html
 */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/device.h>
#include <linux/platform_device.h>
#include <linux/input.h>
#include <linux/interrupt.h>
#include <linux/fb.h>
#include <linux/init.h>
#include <linux/list.h>
#include <linux/mutex.h>
#include <linux/dma-mapping.h>
#include <linux/err.h>
#include <linux/uaccess.h>
#include <linux/cpufreq.h>

#include <mach/hardware.h>
#include <mach/regs-pinctrl.h>
#include <mach/regs-lcdif.h>
#include <mach/regs-clkctrl.h>
#include <mach/regs-apbh.h>
#include <mach/lcdif.h>

#include <mach/stmp3xxx.h>

#define NUM_SCREENS	1

enum {
	F_DISABLE = 0,
	F_ENABLE,
	F_REENABLE,
};

struct stmp3xxx_fb_data {
	struct fb_info info;
	struct stmp3xxx_platform_fb_data *pdata;
	struct work_struct work;
	struct mutex blank_mutex;
	u32 state;
	u32 task_state;
	ssize_t mem_size;
	ssize_t map_size;
	dma_addr_t phys_start;
	dma_addr_t cur_phys;
	int dma_irq;
	int err_irq;
	void *virt_start;
	struct device *dev;
	wait_queue_head_t vsync_wait_q;
	u32 vsync_count;
	void *par;
};

/* forward declaration */
static int stmp3xxxfb_blank(int blank, struct fb_info *info);
static unsigned char *default_panel_name;
static struct stmp3xxx_fb_data *cdata;
static void init_timings(struct stmp3xxx_fb_data *data);

static void stmp3xxxfb_enable_controller(struct stmp3xxx_fb_data *data)
{
	struct stmp3xxx_platform_fb_entry *pentry = data->pdata->cur;

	if (!data || !data->pdata || !data->pdata->cur)
		return;

	stmp3xxx_init_lcdif();
	init_timings(data);
	pentry->init_panel(data->dev, data->phys_start,
			data->info.fix.smem_len, data->pdata->cur);
	pentry->run_panel();

	if (pentry->blank_panel)
		pentry->blank_panel(FB_BLANK_UNBLANK);
}

static void stmp3xxxfb_disable_controller(struct stmp3xxx_fb_data *data)
{
	struct stmp3xxx_platform_fb_entry *pentry = data->pdata->cur;

	if (!data || !data->pdata || !data->pdata->cur)
		return;

	if (pentry->blank_panel)
		pentry->blank_panel(FB_BLANK_POWERDOWN);

	if (pentry->stop_panel)
		pentry->stop_panel();
	pentry->release_panel(data->dev, pentry);
}

static void set_controller_state(struct stmp3xxx_fb_data *data, u32 state)
{
	struct stmp3xxx_platform_fb_entry *pentry = data->pdata->cur;
	struct fb_info *info = &data->info;
	u32 old_state;

	mutex_lock(&data->blank_mutex);
	old_state = data->state;
	pr_debug("%s, old_state %d, state %d\n", __func__, old_state, state);

	switch (state) {
	case F_DISABLE:
		/*
		 * Disable controller
		 */
		if (old_state != F_DISABLE) {
			data->state = F_DISABLE;
			stmp3xxxfb_disable_controller(data);
		}
		break;

	case F_REENABLE:
		/*
		 * Re-enable the controller when panel changed.
		 */
		if (old_state == F_ENABLE) {
			stmp3xxxfb_disable_controller(data);

			pentry = data->pdata->cur = data->pdata->next;
			info->fix.smem_len = pentry->y_res * pentry->x_res *
						pentry->bpp / 8;
			info->screen_size = info->fix.smem_len;
			memset((void *)info->screen_base, 0, info->screen_size);

			stmp3xxxfb_enable_controller(data);

			data->state = F_ENABLE;
		} else if (old_state == F_DISABLE) {
			pentry = data->pdata->cur = data->pdata->next;
			info->fix.smem_len = pentry->y_res * pentry->x_res *
						pentry->bpp / 8;
			info->screen_size = info->fix.smem_len;
			memset((void *)info->screen_base, 0, info->screen_size);

			data->state = F_DISABLE;
		}
		break;

	case F_ENABLE:
		if (old_state != F_ENABLE) {
			data->state = F_ENABLE;
			stmp3xxxfb_enable_controller(data);
		}
		break;
	}
	mutex_unlock(&data->blank_mutex);

}

static void stmp3xxxfb_task(struct work_struct *work)
{
	struct stmp3xxx_fb_data *data =
			container_of(work, struct stmp3xxx_fb_data, work);

	u32 state = xchg(&data->task_state, -1);
	pr_debug("%s: state = %d, data->task_state = %d\n",
				__func__, state, data->task_state);

	set_controller_state(data, state);
}

static void stmp3xxx_schedule_work(struct stmp3xxx_fb_data *data, u32 state)
{
	unsigned long flags;

	local_irq_save(flags);

	data->task_state = state;
	schedule_work(&data->work);

	local_irq_restore(flags);
}

static irqreturn_t lcd_irq_handler(int irq, void *dev_id)
{
	struct stmp3xxx_fb_data *data = dev_id;
	u32 status_lcd = __raw_readl(REGS_LCDIF_BASE + HW_LCDIF_CTRL1);
	u32 status_apbh = __raw_readl(REGS_APBH_BASE + HW_APBH_CTRL1);
	pr_debug("%s: irq %d\n", __func__, irq);

	if (status_apbh & BM_APBH_CTRL1_CH0_CMDCMPLT_IRQ)
		__raw_writel(BM_APBH_CTRL1_CH0_CMDCMPLT_IRQ,
			REGS_APBH_BASE + HW_APBH_CTRL1_CLR);

	if (status_lcd & BM_LCDIF_CTRL1_VSYNC_EDGE_IRQ) {
		pr_debug("%s: VSYNC irq\n", __func__);
		data->vsync_count++;
		__raw_writel(BM_LCDIF_CTRL1_VSYNC_EDGE_IRQ,
			REGS_LCDIF_BASE + HW_LCDIF_CTRL1_CLR);
		wake_up_interruptible(&data->vsync_wait_q);
	}
	if (status_lcd & BM_LCDIF_CTRL1_CUR_FRAME_DONE_IRQ) {
		pr_debug("%s: frame done irq\n", __func__);
		__raw_writel(BM_LCDIF_CTRL1_CUR_FRAME_DONE_IRQ,
			REGS_LCDIF_BASE + HW_LCDIF_CTRL1_CLR);
		data->vsync_count++;
	}
	if (status_lcd & BM_LCDIF_CTRL1_UNDERFLOW_IRQ) {
		pr_debug("%s: underflow irq\n", __func__);
		__raw_writel(BM_LCDIF_CTRL1_UNDERFLOW_IRQ,
			REGS_LCDIF_BASE + HW_LCDIF_CTRL1_CLR);
	}
	if (status_lcd & BM_LCDIF_CTRL1_OVERFLOW_IRQ) {
		pr_debug("%s: overflow irq\n", __func__);
		__raw_writel(BM_LCDIF_CTRL1_OVERFLOW_IRQ,
			REGS_LCDIF_BASE + HW_LCDIF_CTRL1_CLR);
	}
	return IRQ_HANDLED;
}

static struct fb_var_screeninfo stmp3xxxfb_default __devinitdata = {
	.activate =	FB_ACTIVATE_TEST,
	.height =	-1,
	.width =	-1,
	.pixclock =	20000,
	.left_margin =	64,
	.right_margin =	64,
	.upper_margin =	32,
	.lower_margin =	32,
	.hsync_len =	64,
	.vsync_len =	2,
	.vmode =	FB_VMODE_NONINTERLACED,
};

static struct fb_fix_screeninfo stmp3xxxfb_fix __devinitdata = {
	.id =		"stmp3xxxfb",
	.type =		FB_TYPE_PACKED_PIXELS,
	.visual =       FB_VISUAL_TRUECOLOR,
	.xpanstep =	0,
	.ypanstep =	0,
	.ywrapstep =	0,
	.accel =	FB_ACCEL_NONE,
};

int stmp3xxxfb_get_info(struct fb_var_screeninfo *var,
			struct fb_fix_screeninfo *fix)
{
	if (!cdata)
		return -ENODEV;

	*var = cdata->info.var;
	*fix = cdata->info.fix;
	return 0;
}

void stmp3xxxfb_cfg_pxp(int enable, dma_addr_t pxp_phys)
{
	if (enable)
		cdata->pdata->cur->pan_display(pxp_phys);
	else
		cdata->pdata->cur->pan_display(cdata->cur_phys);
}

static int stmp3xxxfb_mmap(struct fb_info *info, struct vm_area_struct *vma)
{
	struct stmp3xxx_fb_data *data = (struct stmp3xxx_fb_data *)info;

	unsigned long off = vma->vm_pgoff << PAGE_SHIFT;

	if (off < info->fix.smem_len)
		return dma_mmap_writecombine(data->dev, vma,
				data->virt_start,
				data->phys_start,
				info->fix.smem_len);
	else
		return -EINVAL;
}

static int stmp3xxxfb_setcolreg(u_int regno, u_int red, u_int green, u_int blue,
			 u_int transp, struct fb_info *info)
{
	if (regno >= 256)	/* no. of hw registers */
		return 1;
	/*
	* Program hardware... do anything you want with transp
	*/

	/* grayscale works only partially under directcolor */
	if (info->var.grayscale) {
		/* grayscale = 0.30*R + 0.59*G + 0.11*B */
		red = green = blue =
		    (red * 77 + green * 151 + blue * 28) >> 8;
	}

	/* Directcolor:
	 *   var->{color}.offset contains start of bitfield
	 *   var->{color}.length contains length of bitfield
	 *   {hardwarespecific} contains width of RAMDAC
	 *   cmap[X] is programmed to
	 *	(X << red.offset) | (X << green.offset) | (X << blue.offset)
	 *   RAMDAC[X] is programmed to (red, green, blue)
	 *
	 * Pseudocolor:
	 *    uses offset = 0 && length = RAMDAC register width.
	 *    var->{color}.offset is 0
	 *    var->{color}.length contains widht of DAC
	 *    cmap is not used
	 *    RAMDAC[X] is programmed to (red, green, blue)
	 * Truecolor:
	 *    does not use DAC. Usually 3 are present.
	 *    var->{color}.offset contains start of bitfield
	 *    var->{color}.length contains length of bitfield
	 *    cmap is programmed to
	 *	(red << red.offset) | (green << green.offset) |
	 *	(blue << blue.offset) | (transp << transp.offset)
	 *    RAMDAC does not exist
	 */
#define CNVT_TOHW(val, width) ((((val)<<(width))+0x7FFF-(val))>>16)
	switch (info->fix.visual) {
	case FB_VISUAL_TRUECOLOR:
	case FB_VISUAL_PSEUDOCOLOR:
		red = CNVT_TOHW(red, info->var.red.length);
		green = CNVT_TOHW(green, info->var.green.length);
		blue = CNVT_TOHW(blue, info->var.blue.length);
		transp = CNVT_TOHW(transp, info->var.transp.length);
		break;
	case FB_VISUAL_DIRECTCOLOR:
		red = CNVT_TOHW(red, 8);	/* expect 8 bit DAC */
		green = CNVT_TOHW(green, 8);
		blue = CNVT_TOHW(blue, 8);
		/* hey, there is bug in transp handling... */
		transp = CNVT_TOHW(transp, 8);
		break;
	}
#undef CNVT_TOHW
	/* Truecolor has hardware independent palette */
	if (info->fix.visual == FB_VISUAL_TRUECOLOR) {

		if (regno >= 16)
			return 1;

		((u32 *) (info->pseudo_palette))[regno] =
				(red << info->var.red.offset) |
				(green << info->var.green.offset) |
				(blue << info->var.blue.offset) |
				(transp << info->var.transp.offset);
	}
	return 0;
}

static inline u_long get_line_length(int xres_virtual, int bpp)
{
	u_long length;

	length = xres_virtual * bpp;
	length = (length + 31) & ~31;
	length >>= 3;
	return length;
}

static int get_matching_pentry(struct stmp3xxx_platform_fb_entry *pentry,
			       void *data, int ret_prev)
{
	struct fb_var_screeninfo *info = data;
	pr_debug("%s: %d:%d:%d vs %d:%d:%d\n", __func__,
		pentry->x_res, pentry->y_res, pentry->bpp,
		info->yres, info->xres, info->bits_per_pixel);
	if (pentry->x_res == info->yres && pentry->y_res == info->xres &&
	    pentry->bpp == info->bits_per_pixel)
		ret_prev = (int)pentry;
	return ret_prev;
}

static int get_matching_pentry_by_name(
		struct stmp3xxx_platform_fb_entry *pentry,
		void *data,
		int ret_prev)
{
	unsigned char *name = data;
	if (!strcmp(pentry->name, name))
		ret_prev = (int)pentry;
	return ret_prev;
}

/*
 * This routine actually sets the video mode. It's in here where we
 * the hardware state info->par and fix which can be affected by the
 * change in par. For this driver it doesn't do much.
 *
 * XXX: REVISIT
 */
static int stmp3xxxfb_set_par(struct fb_info *info)
{
	struct stmp3xxx_fb_data *data = (struct stmp3xxx_fb_data *)info;
	struct stmp3xxx_platform_fb_data *pdata = data->pdata;
	struct stmp3xxx_platform_fb_entry *pentry;
	pentry = (void *)stmp3xxx_lcd_iterate_pdata(pdata,
						get_matching_pentry,
						&info->var);

	dev_dbg(data->dev, "%s: xres %d, yres %d, bpp %d\n",
		__func__,
		info->var.xres,
		info->var.yres,
		info->var.bits_per_pixel);
	if (!pentry)
		return -EINVAL;

	info->fix.line_length = get_line_length(info->var.xres_virtual,
						info->var.bits_per_pixel);

	if (pentry == pdata->cur || !pdata->cur)
		return 0;

	/* init next panel */
	pdata->next = pentry;

	set_controller_state(data, F_REENABLE);

	return 0;
}

static int stmp3xxxfb_check_var(struct fb_var_screeninfo *var,
				struct fb_info *info)
{
	u32 line_length;
	struct stmp3xxx_fb_data *data = (struct stmp3xxx_fb_data *)info;
	struct stmp3xxx_platform_fb_data *pdata = data->pdata;

	/*
	 *  FB_VMODE_CONUPDATE and FB_VMODE_SMOOTH_XPAN are equal!
	 *  as FB_VMODE_SMOOTH_XPAN is only used internally
	 */

	if (var->vmode & FB_VMODE_CONUPDATE) {
		var->vmode |= FB_VMODE_YWRAP;
		var->xoffset = info->var.xoffset;
		var->yoffset = info->var.yoffset;
	}

	pr_debug("%s: xres %d, yres %d, bpp %d\n", __func__,
		var->xres,  var->yres, var->bits_per_pixel);
	/*
	 *  Some very basic checks
	 */
	if (!var->xres)
		var->xres = 1;
	if (!var->yres)
		var->yres = 1;
	if (var->xres > var->xres_virtual)
		var->xres_virtual = var->xres;
	if (var->yres > var->yres_virtual)
		var->yres_virtual = var->yres;

	if (var->xres_virtual < var->xoffset + var->xres)
		var->xres_virtual = var->xoffset + var->xres;
	if (var->yres_virtual < var->yoffset + var->yres)
		var->yres_virtual = var->yoffset + var->yres;

	line_length = get_line_length(var->xres_virtual, var->bits_per_pixel);
	dev_dbg(data->dev,
		"line_length %d, var->yres_virtual %d, data->mem_size %d\n",
		line_length, var->yres_virtual, data->mem_size);
	if (line_length * var->yres_virtual > data->map_size)
		return -ENOMEM;

	if (!stmp3xxx_lcd_iterate_pdata(pdata, get_matching_pentry, var))
		return -EINVAL;

	if (var->bits_per_pixel == 16) {
		/* RGBA 5551 */
		if (var->transp.length) {
			var->red.offset = 0;
			var->red.length = 5;
			var->green.offset = 5;
			var->green.length = 5;
			var->blue.offset = 10;
			var->blue.length = 5;
			var->transp.offset = 15;
			var->transp.length = 1;
		} else {	/* RGB 565 */
			var->red.offset = 0;
			var->red.length = 5;
			var->green.offset = 5;
			var->green.length = 6;
			var->blue.offset = 11;
			var->blue.length = 5;
			var->transp.offset = 0;
			var->transp.length = 0;
		}
	} else {
		var->red.offset = 16;
		var->red.length = 8;
		var->green.offset = 8;
		var->green.length = 8;
		var->blue.offset = 0;
		var->blue.length = 8;
	}

	var->red.msb_right = 0;
	var->green.msb_right = 0;
	var->blue.msb_right = 0;
	var->transp.msb_right = 0;

	return 0;
}


static int stmp3xxxfb_wait_for_vsync(u32 channel, struct fb_info *info)
{
	struct stmp3xxx_fb_data *data = (struct stmp3xxx_fb_data *)info;
	u32 count = data->vsync_count;
	int ret = 0;

	__raw_writel(BM_LCDIF_CTRL1_VSYNC_EDGE_IRQ_EN,
		REGS_LCDIF_BASE + HW_LCDIF_CTRL1_SET);
	ret = wait_event_interruptible_timeout(data->vsync_wait_q,
			count != data->vsync_count, HZ / 10);
	__raw_writel(BM_LCDIF_CTRL1_VSYNC_EDGE_IRQ_EN,
		REGS_LCDIF_BASE + HW_LCDIF_CTRL1_CLR);
	if (!ret) {
		dev_err(data->dev, "wait for vsync timed out\n");
		ret = -ETIMEDOUT;
	}
	return ret;
}

static int stmp3xxxfb_ioctl(struct fb_info *info, unsigned int cmd,
			unsigned long arg)
{
	u32 channel = 0;
	int ret = -EINVAL;

	switch (cmd) {
	case FBIO_WAITFORVSYNC:
		if (!get_user(channel, (__u32 __user *) arg))
			ret = stmp3xxxfb_wait_for_vsync(channel, info);
		break;
	default:
		break;
	}
	return ret;
}

static int stmp3xxxfb_blank(int blank, struct fb_info *info)
{
	struct stmp3xxx_fb_data *data = (struct stmp3xxx_fb_data *)info;
	int ret = 0;

	switch (blank) {
	case FB_BLANK_NORMAL:
	case FB_BLANK_VSYNC_SUSPEND:
	case FB_BLANK_HSYNC_SUSPEND:
	case FB_BLANK_POWERDOWN:
		pr_debug("%s: FB_BLANK_POWERDOWN\n", __func__);
		stmp3xxx_schedule_work(data, F_DISABLE);
		break;

	case FB_BLANK_UNBLANK:
		pr_debug("%s: FB_BLANK_UNBLANK\n", __func__);
		stmp3xxx_schedule_work(data, F_ENABLE);
		break;

	default:
		ret = -EINVAL;
	}
	return ret;
}

static int stmp3xxxfb_pan_display(struct fb_var_screeninfo *var,
				struct fb_info *info)
{
	struct stmp3xxx_fb_data *data = (struct stmp3xxx_fb_data *)info;
	int ret = 0;

	pr_debug("%s: var->xoffset %d, info->var.xoffset %d\n",
		__func__, var->xoffset, info->var.xoffset);
	/* check if var is valid; also, xpan is not supported */
	if (!var || (var->xoffset != info->var.xoffset) ||
	    (var->yoffset + var->yres > var->yres_virtual)) {
		ret = -EINVAL;
		goto out;
	}

	if (!data->pdata->cur->pan_display) {
		ret = -EINVAL;
		goto out;
	}

	/* update framebuffer visual */
	data->cur_phys = data->phys_start +
			info->fix.line_length * var->yoffset;
	data->pdata->cur->pan_display(data->cur_phys);
out:
	return ret;
}

static struct fb_ops stmp3xxxfb_ops = {
	.owner = THIS_MODULE,
	.fb_check_var = stmp3xxxfb_check_var,
	.fb_set_par = stmp3xxxfb_set_par,
	.fb_mmap = stmp3xxxfb_mmap,
	.fb_setcolreg = stmp3xxxfb_setcolreg,
	.fb_ioctl = stmp3xxxfb_ioctl,
	.fb_blank = stmp3xxxfb_blank,
	.fb_pan_display = stmp3xxxfb_pan_display,
	.fb_fillrect = cfb_fillrect,
	.fb_copyarea = cfb_copyarea,
	.fb_imageblit = cfb_imageblit,
};

static void init_timings(struct stmp3xxx_fb_data *data)
{
	unsigned phase_time;
	unsigned timings;

	/* Just use a phase_time of 1. As optimal as it gets, now. */
	phase_time = 1;

	/* Program all 4 timings the same */
	timings = phase_time;
	timings |= timings << 8;
	timings |= timings << 16;
	__raw_writel(timings, REGS_LCDIF_BASE + HW_LCDIF_TIMING);
}

#ifdef CONFIG_CPU_FREQ

struct stmp3xxxfb_notifier_block {
	struct stmp3xxx_fb_data *fb_data;
	struct notifier_block nb;
};

static int stmp3xxxfb_notifier(struct notifier_block *self,
				unsigned long phase, void *p)
{
	struct stmp3xxxfb_notifier_block *block =
		container_of(self, struct stmp3xxxfb_notifier_block, nb);
	struct stmp3xxx_fb_data *data = block->fb_data;
	struct stmp3xxx_platform_fb_entry *pentry = data->pdata->cur;
	u32 old_state = data->state;

	if (!data || !data->pdata || !data->pdata->cur)
		return NOTIFY_BAD;

	/* REVISIT */
	switch (phase) {
	case CPUFREQ_PRECHANGE:
		if (old_state == F_ENABLE)
			if (pentry->blank_panel)
				pentry->blank_panel(FB_BLANK_POWERDOWN);
		break;

	case CPUFREQ_POSTCHANGE:
		if (old_state == F_ENABLE)
			if (pentry->blank_panel)
				pentry->blank_panel(FB_BLANK_UNBLANK);
		break;

	default:
		dev_dbg(data->dev, "didn't handle notify %ld\n", phase);
	}

	return NOTIFY_DONE;
}

static struct stmp3xxxfb_notifier_block stmp3xxxfb_nb = {
	.nb = {
		.notifier_call = stmp3xxxfb_notifier,
	},
};
#endif /* CONFIG_CPU_FREQ */

static int get_max_memsize(struct stmp3xxx_platform_fb_entry *pentry,
			   void *data, int ret_prev)
{
	struct stmp3xxx_fb_data *fbdata = data;
	int sz = pentry->x_res * pentry->y_res * pentry->bpp / 8;
	fbdata->mem_size = sz < ret_prev ? ret_prev : sz;
	pr_debug("%s: mem_size now %d\n", __func__, fbdata->mem_size);
	return fbdata->mem_size;
}

static int __devinit stmp3xxxfb_probe(struct platform_device *pdev)
{
	int ret = 0;
	struct stmp3xxx_fb_data *data;
	struct resource *res;
	struct fb_info *info;
	struct stmp3xxx_platform_fb_data *pdata = pdev->dev.platform_data;
	struct stmp3xxx_platform_fb_entry *pentry;

	if (pdata == NULL) {
		ret = -ENODEV;
		goto out;
	}

	if (default_panel_name) {
		pentry = (void *)stmp3xxx_lcd_iterate_pdata(pdata,
					get_matching_pentry_by_name,
					default_panel_name);
		if (pentry) {
			stmp3xxx_lcd_move_pentry_up(pentry, pdata);
			pdata->cur = pentry;
		}
	}
	if (!default_panel_name || !pentry)
		pentry = pdata->cur;
	if (!pentry || !pentry->init_panel || !pentry->run_panel ||
	    !pentry->release_panel) {
		ret = -EINVAL;
		goto out;
	}

	data = (struct stmp3xxx_fb_data *)framebuffer_alloc(
				sizeof(struct stmp3xxx_fb_data) +
				sizeof(u32) * 256 -
				sizeof(struct fb_info), &pdev->dev);
	if (data == NULL) {
		ret = -ENOMEM;
		goto out;
	}

	cdata = data;
	data->dev = &pdev->dev;
	data->pdata = pdata;
	platform_set_drvdata(pdev, data);
	info = &data->info;

	dev_dbg(&pdev->dev, "resolution %dx%d, bpp %d\n", pentry->x_res,
		pentry->y_res, pentry->bpp);

	stmp3xxx_lcd_iterate_pdata(pdata, get_max_memsize, data);

	data->map_size = PAGE_ALIGN(data->mem_size) * NUM_SCREENS;
	dev_dbg(&pdev->dev, "memory to allocate: %d\n", data->map_size);

	data->virt_start = dma_alloc_writecombine(&pdev->dev,
					data->map_size,
					&data->phys_start,
					GFP_KERNEL);

	if (data->virt_start == NULL) {
		ret = -ENOMEM;
		goto out_dma;
	}
	dev_dbg(&pdev->dev, "allocated at %p:0x%x\n", data->virt_start,
		data->phys_start);
	mutex_init(&data->blank_mutex);
	INIT_WORK(&data->work, stmp3xxxfb_task);
	data->state = F_ENABLE;

	stmp3xxxfb_default.bits_per_pixel = pentry->bpp;
	/* NB: rotated */
	stmp3xxxfb_default.xres = pentry->y_res;
	stmp3xxxfb_default.yres = pentry->x_res;
	stmp3xxxfb_default.xres_virtual = pentry->y_res;
	stmp3xxxfb_default.yres_virtual = data->map_size /
					(pentry->y_res * pentry->bpp / 8);
	if (stmp3xxxfb_default.yres_virtual >= stmp3xxxfb_default.yres * 2)
		stmp3xxxfb_default.yres_virtual = stmp3xxxfb_default.yres * 2;
	else
		stmp3xxxfb_default.yres_virtual = stmp3xxxfb_default.yres;

	stmp3xxxfb_fix.smem_start = data->phys_start;
	stmp3xxxfb_fix.smem_len = pentry->y_res * pentry->x_res * pentry->bpp /
		8;
	stmp3xxxfb_fix.ypanstep = 1;

	switch (pentry->bpp) {
	case 32:
	case 24:
		stmp3xxxfb_default.red.offset = 16;
		stmp3xxxfb_default.red.length = 8;
		stmp3xxxfb_default.green.offset = 8;
		stmp3xxxfb_default.green.length = 8;
		stmp3xxxfb_default.blue.offset = 0;
		stmp3xxxfb_default.blue.length = 8;
		break;

	case 16:
		stmp3xxxfb_default.red.offset = 11;
		stmp3xxxfb_default.red.length = 5;
		stmp3xxxfb_default.green.offset = 5;
		stmp3xxxfb_default.green.length = 6;
		stmp3xxxfb_default.blue.offset = 0;
		stmp3xxxfb_default.blue.length = 5;
		break;

	default:
		dev_err(&pdev->dev, "unsupported bitwidth %d\n", pentry->bpp);
		ret = -EINVAL;
		goto out_dma;
	}

	info->screen_base = data->virt_start;
	info->fbops = &stmp3xxxfb_ops;
	info->var = stmp3xxxfb_default;
	info->fix = stmp3xxxfb_fix;
	info->pseudo_palette = &data->par;
	data->par = NULL;
	info->flags = FBINFO_FLAG_DEFAULT;

	init_waitqueue_head(&data->vsync_wait_q);
	data->vsync_count = 0;

	res = platform_get_resource(pdev, IORESOURCE_IRQ, 0);
	if (res == NULL) {
		dev_err(&pdev->dev, "cannot get IRQ resource\n");
		ret = -ENODEV;
		goto out_dma;
	}
	data->dma_irq = res->start;

	res = platform_get_resource(pdev, IORESOURCE_IRQ, 1);
	if (res == NULL) {
		dev_err(&pdev->dev, "cannot get IRQ resource\n");
		ret = -ENODEV;
		goto out_dma;
	}
	data->err_irq = res->start;

	ret = fb_alloc_cmap(&info->cmap, 256, 0);
	if (ret)
		goto out_cmap;

	stmp3xxx_init_lcdif();
	ret = pentry->init_panel(data->dev, data->phys_start,
				stmp3xxxfb_fix.smem_len, pentry);
	if (ret) {
		dev_err(&pdev->dev, "cannot initialize LCD panel\n");
		goto out_panel;
	}
	dev_dbg(&pdev->dev, "LCD panel initialized\n");
	init_timings(data);

	ret = request_irq(data->dma_irq, lcd_irq_handler, 0, "fb_dma", data);
	if (ret) {
		dev_err(&pdev->dev, "request_irq (%d) failed with error %d\n",
				data->dma_irq, ret);
		goto out_panel;
	}
	ret = request_irq(data->err_irq, lcd_irq_handler, 0, "fb_error", data);
	if (ret) {
		dev_err(&pdev->dev, "request_irq (%d) failed with error %d\n",
				data->err_irq, ret);
		goto out_irq;
	}
	ret = register_framebuffer(info);
	if (ret)
		goto out_register;

	pentry->run_panel();
	dev_dbg(&pdev->dev, "LCD DMA channel has been started\n");
	data->cur_phys = data->phys_start;
	dev_dbg(&pdev->dev, "LCD running now\n");

#ifdef CONFIG_CPU_FREQ
	stmp3xxxfb_nb.fb_data = data;
	cpufreq_register_notifier(&stmp3xxxfb_nb.nb,
				CPUFREQ_TRANSITION_NOTIFIER);
#endif /* CONFIG_CPU_FREQ */

	goto out;

out_register:
	free_irq(data->err_irq, data);
out_irq:
	free_irq(data->dma_irq, data);
out_panel:
	fb_dealloc_cmap(&info->cmap);
out_cmap:
	dma_free_writecombine(&pdev->dev, data->map_size, data->virt_start,
			data->phys_start);
out_dma:
	kfree(data);
out:
	return ret;
}

static int stmp3xxxfb_remove(struct platform_device *pdev)
{
	struct stmp3xxx_fb_data *data = platform_get_drvdata(pdev);

	set_controller_state(data, F_DISABLE);

	unregister_framebuffer(&data->info);
	framebuffer_release(&data->info);
	fb_dealloc_cmap(&data->info.cmap);
	free_irq(data->dma_irq, data);
	free_irq(data->err_irq, data);
	dma_free_writecombine(&pdev->dev, data->map_size, data->virt_start,
			data->phys_start);
	kfree(data);
	platform_set_drvdata(pdev, NULL);
	return 0;
}

#ifdef CONFIG_PM
static int stmp3xxxfb_suspend(struct platform_device *pdev, pm_message_t state)
{
	struct stmp3xxx_fb_data *data = platform_get_drvdata(pdev);

	set_controller_state(data, F_DISABLE);

	return 0;
}

static int stmp3xxxfb_resume(struct platform_device *pdev)
{
	struct stmp3xxx_fb_data *data = platform_get_drvdata(pdev);

	set_controller_state(data, F_ENABLE);
	return 0;
}
#else
#define stmp3xxxfb_suspend	NULL
#define	stmp3xxxfb_resume	NULL
#endif

static struct platform_driver stmp3xxxfb_driver = {
	.probe		= stmp3xxxfb_probe,
	.remove		= stmp3xxxfb_remove,
	.suspend	= stmp3xxxfb_suspend,
	.resume		= stmp3xxxfb_resume,
	.driver		= {
		.name   = "stmp3xxx-fb",
		.owner	= THIS_MODULE,
	},
};

static int __init stmp3xxxfb_init(void)
{
	return platform_driver_register(&stmp3xxxfb_driver);
}

static void __exit stmp3xxxfb_exit(void)
{
	platform_driver_unregister(&stmp3xxxfb_driver);
}

module_init(stmp3xxxfb_init);
module_exit(stmp3xxxfb_exit);

/*
 * LCD panel select
 */
static int __init default_panel_select(char *str)
{
	default_panel_name = str;
	return 0;
}
__setup("lcd_panel=", default_panel_select);

MODULE_AUTHOR("Vitaly Wool <vital@embeddedalley.com>");
MODULE_DESCRIPTION("STMP3xxx Framebuffer Driver");
MODULE_LICENSE("GPL");
