diff options
Diffstat (limited to 'drivers/video/mxc/ldb.c')
-rw-r--r-- | drivers/video/mxc/ldb.c | 645 |
1 files changed, 645 insertions, 0 deletions
diff --git a/drivers/video/mxc/ldb.c b/drivers/video/mxc/ldb.c new file mode 100644 index 00000000000..401ebb4c3a5 --- /dev/null +++ b/drivers/video/mxc/ldb.c @@ -0,0 +1,645 @@ +/* + * Copyright (C) 2011 Freescale Semiconductor, Inc. All Rights Reserved. + */ + +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/*! + * @file mxc_ldb.c + * + * @brief This file contains the LDB driver device interface and fops + * functions. + */ +#include <linux/types.h> +#include <linux/init.h> +#include <linux/platform_device.h> +#include <linux/err.h> +#include <linux/clk.h> +#include <linux/console.h> +#include <linux/io.h> +#include <linux/ipu.h> +#include <linux/mxcfb.h> +#include <linux/regulator/consumer.h> +#include <linux/spinlock.h> +#include <linux/fsl_devices.h> +#include <mach/hardware.h> +#include <mach/clock.h> +#include "mxc_dispdrv.h" + +#define DISPDRV_LDB "ldb" + +#define LDB_BGREF_RMODE_MASK 0x00008000 +#define LDB_BGREF_RMODE_INT 0x00008000 +#define LDB_BGREF_RMODE_EXT 0x0 + +#define LDB_DI1_VS_POL_MASK 0x00000400 +#define LDB_DI1_VS_POL_ACT_LOW 0x00000400 +#define LDB_DI1_VS_POL_ACT_HIGH 0x0 +#define LDB_DI0_VS_POL_MASK 0x00000200 +#define LDB_DI0_VS_POL_ACT_LOW 0x00000200 +#define LDB_DI0_VS_POL_ACT_HIGH 0x0 + +#define LDB_BIT_MAP_CH1_MASK 0x00000100 +#define LDB_BIT_MAP_CH1_JEIDA 0x00000100 +#define LDB_BIT_MAP_CH1_SPWG 0x0 +#define LDB_BIT_MAP_CH0_MASK 0x00000040 +#define LDB_BIT_MAP_CH0_JEIDA 0x00000040 +#define LDB_BIT_MAP_CH0_SPWG 0x0 + +#define LDB_DATA_WIDTH_CH1_MASK 0x00000080 +#define LDB_DATA_WIDTH_CH1_24 0x00000080 +#define LDB_DATA_WIDTH_CH1_18 0x0 +#define LDB_DATA_WIDTH_CH0_MASK 0x00000020 +#define LDB_DATA_WIDTH_CH0_24 0x00000020 +#define LDB_DATA_WIDTH_CH0_18 0x0 + +#define LDB_CH1_MODE_MASK 0x0000000C +#define LDB_CH1_MODE_EN_TO_DI1 0x0000000C +#define LDB_CH1_MODE_EN_TO_DI0 0x00000004 +#define LDB_CH1_MODE_DISABLE 0x0 +#define LDB_CH0_MODE_MASK 0x00000003 +#define LDB_CH0_MODE_EN_TO_DI1 0x00000003 +#define LDB_CH0_MODE_EN_TO_DI0 0x00000001 +#define LDB_CH0_MODE_DISABLE 0x0 + +#define LDB_SPLIT_MODE_EN 0x00000010 + +struct ldb_data { + struct platform_device *pdev; + struct mxc_dispdrv_entry *disp_ldb; + uint32_t *reg; + uint32_t *control_reg; + uint32_t *gpr3_reg; + struct regulator *lvds_bg_reg; + int mode; + bool inited; + struct clk *di_clk[2]; + struct clk *ldb_di_clk[2]; + struct ldb_setting { + bool active; + bool clk_en; + int ipu; + int di; + } setting[2]; + struct notifier_block nb; +}; + +static int g_ldb_mode; + +static struct fb_videomode ldb_modedb[] = { + { + "LDB-XGA", 60, 1024, 768, 15385, + 220, 40, + 21, 7, + 60, 10, + 0, + FB_VMODE_NONINTERLACED, + FB_MODE_IS_DETAILED,}, + { + "LDB-1080P60", 60, 1920, 1080, 7692, + 100, 40, + 30, 3, + 10, 2, + 0, + FB_VMODE_NONINTERLACED, + FB_MODE_IS_DETAILED,}, +}; +static int ldb_modedb_sz = ARRAY_SIZE(ldb_modedb); + +static int bits_per_pixel(int pixel_fmt) +{ + switch (pixel_fmt) { + case IPU_PIX_FMT_BGR24: + case IPU_PIX_FMT_RGB24: + return 24; + break; + case IPU_PIX_FMT_BGR666: + case IPU_PIX_FMT_RGB666: + case IPU_PIX_FMT_LVDS666: + return 18; + break; + default: + break; + } + return 0; +} + +static int valid_mode(int pixel_fmt) +{ + return ((pixel_fmt == IPU_PIX_FMT_RGB24) || + (pixel_fmt == IPU_PIX_FMT_BGR24) || + (pixel_fmt == IPU_PIX_FMT_LVDS666) || + (pixel_fmt == IPU_PIX_FMT_RGB666) || + (pixel_fmt == IPU_PIX_FMT_BGR666)); +} + +/* + * "ldb=spl0/1" -- split mode on DI0/1 + * "ldb=dul0/1" -- dual mode on DI0/1 + * "ldb=sin0/1" -- single mode on LVDS0/1 + * "ldb=sep0/1" -- separate mode begin from LVDS0/1 + * + * there are two LVDS channels(LVDS0 and LVDS1) which can transfer video + * datas, there two channels can be used as split/dual/single/separate mode. + * + * split mode means display data from DI0 or DI1 will send to both channels + * LVDS0+LVDS1. + * dual mode means display data from DI0 or DI1 will be duplicated on LVDS0 + * and LVDS1, it said, LVDS0 and LVDS1 has the same content. + * single mode means only work for DI0/DI1->LVDS0 or DI0/DI1->LVDS1. + * separate mode means you can make DI0/DI1->LVDS0 and DI0/DI1->LVDS1 work + * at the same time. + */ +static int __init ldb_setup(char *options) +{ + if (!strcmp(options, "spl0")) + g_ldb_mode = LDB_SPL_DI0; + else if (!strcmp(options, "spl1")) + g_ldb_mode = LDB_SPL_DI1; + else if (!strcmp(options, "dul0")) + g_ldb_mode = LDB_DUL_DI0; + else if (!strcmp(options, "dul1")) + g_ldb_mode = LDB_DUL_DI1; + else if (!strcmp(options, "sin0")) + g_ldb_mode = LDB_SIN0; + else if (!strcmp(options, "sin1")) + g_ldb_mode = LDB_SIN1; + else if (!strcmp(options, "sep0")) + g_ldb_mode = LDB_SEP0; + else if (!strcmp(options, "sep1")) + g_ldb_mode = LDB_SEP1; + + return 1; +} +__setup("ldb=", ldb_setup); + +static int find_ldb_setting(struct ldb_data *ldb, struct fb_info *fbi) +{ + char *id_di[] = { + "DISP3 BG", + "DISP3 BG - DI1", + }; + char id[16]; + int i; + + for (i = 0; i < 2; i++) { + if (ldb->setting[i].active) { + memset(id, 0, 16); + memcpy(id, id_di[ldb->setting[i].di], + strlen(id_di[ldb->setting[i].di])); + id[4] += ldb->setting[i].ipu; + if (!strcmp(id, fbi->fix.id)) + return i; + } + } + return -EINVAL; +} + +int ldb_fb_event(struct notifier_block *nb, unsigned long val, void *v) +{ + struct ldb_data *ldb = container_of(nb, struct ldb_data, nb); + struct fb_event *event = v; + struct fb_info *fbi = event->info; + int setting_idx, di; + + setting_idx = find_ldb_setting(ldb, fbi); + if (setting_idx < 0) + return 0; + + di = ldb->setting[setting_idx].di; + + fbi->mode = (struct fb_videomode *)fb_match_mode(&fbi->var, + &fbi->modelist); + + if (!fbi->mode) { + dev_warn(&ldb->pdev->dev, + "LDB: can not find mode for xres=%d, yres=%d\n", + fbi->var.xres, fbi->var.yres); + if (ldb->setting[setting_idx].clk_en) { + clk_disable(ldb->ldb_di_clk[di]); + ldb->setting[setting_idx].clk_en = false; + } + return 0; + } + + switch (val) { + case FB_EVENT_PREMODE_CHANGE: + { + uint32_t reg; + uint32_t pixel_clk, rounded_pixel_clk; + struct clk *ldb_clk_parent; + + /* vsync setup */ + reg = readl(ldb->control_reg); + if (fbi->var.sync & FB_SYNC_VERT_HIGH_ACT) { + if (di == 0) + reg = (reg & ~LDB_DI0_VS_POL_MASK) + | LDB_DI0_VS_POL_ACT_HIGH; + else + reg = (reg & ~LDB_DI1_VS_POL_MASK) + | LDB_DI1_VS_POL_ACT_HIGH; + } else { + if (di == 0) + reg = (reg & ~LDB_DI0_VS_POL_MASK) + | LDB_DI0_VS_POL_ACT_LOW; + else + reg = (reg & ~LDB_DI1_VS_POL_MASK) + | LDB_DI1_VS_POL_ACT_LOW; + } + writel(reg, ldb->control_reg); + + /* clk setup */ + pixel_clk = (PICOS2KHZ(fbi->var.pixclock)) * 1000UL; + ldb_clk_parent = clk_get_parent(ldb->ldb_di_clk[di]); + if ((ldb->mode == LDB_SPL_DI0) || (ldb->mode == LDB_SPL_DI1)) + clk_set_rate(ldb_clk_parent, pixel_clk * 7 / 2); + else + clk_set_rate(ldb_clk_parent, pixel_clk * 7); + rounded_pixel_clk = clk_round_rate(ldb->ldb_di_clk[di], + pixel_clk); + clk_set_rate(ldb->ldb_di_clk[di], rounded_pixel_clk); + clk_enable(ldb->ldb_di_clk[di]); + ldb->setting[setting_idx].clk_en = true; + break; + } + case FB_EVENT_BLANK: + { + if (*((int *)event->data) == FB_BLANK_UNBLANK) { + if (!ldb->setting[setting_idx].clk_en) { + clk_enable(ldb->ldb_di_clk[di]); + ldb->setting[setting_idx].clk_en = true; + } + } else { + if (ldb->setting[setting_idx].clk_en) { + clk_disable(ldb->ldb_di_clk[di]); + ldb->setting[setting_idx].clk_en = false; + } + } + } + default: + break; + } + return 0; +} + +static int ldb_disp_init(struct mxc_dispdrv_entry *disp) +{ + int ret = 0, i; + struct ldb_data *ldb = mxc_dispdrv_getdata(disp); + struct mxc_dispdrv_setting *setting = mxc_dispdrv_getsetting(disp); + struct fsl_mxc_ldb_platform_data *plat_data = ldb->pdev->dev.platform_data; + struct resource *res; + uint32_t base_addr; + uint32_t reg, setting_idx; + + /* if input format not valid, make RGB666 as default*/ + if (!valid_mode(setting->if_fmt)) { + dev_warn(&ldb->pdev->dev, "Input pixel format not valid" + " use default RGB666\n"); + setting->if_fmt = IPU_PIX_FMT_RGB666; + } + + if (!ldb->inited) { + char di_clk[] = "ipu1_di0_clk"; + char ldb_clk[] = "ldb_di0_clk"; + int lvds_channel = 0; + + res = platform_get_resource(ldb->pdev, IORESOURCE_MEM, 0); + if (IS_ERR(res)) + return -ENOMEM; + + base_addr = res->start; + ldb->reg = ioremap(base_addr, res->end - res->start + 1); + ldb->control_reg = ldb->reg + 2; + ldb->gpr3_reg = ldb->reg + 3; + + ldb->lvds_bg_reg = regulator_get(&ldb->pdev->dev, plat_data->lvds_bg_reg); + if (!IS_ERR(ldb->lvds_bg_reg)) { + regulator_set_voltage(ldb->lvds_bg_reg, 2500000, 2500000); + regulator_enable(ldb->lvds_bg_reg); + } + + /* ipu selected by platform data setting */ + setting->dev_id = plat_data->ipu_id; + + reg = readl(ldb->control_reg); + + /* refrence resistor select */ + reg &= ~LDB_BGREF_RMODE_MASK; + if (plat_data->ext_ref) + reg |= LDB_BGREF_RMODE_EXT; + else + reg |= LDB_BGREF_RMODE_INT; + + /* TODO: now only use SPWG data mapping for both channel */ + reg &= ~(LDB_BIT_MAP_CH0_MASK | LDB_BIT_MAP_CH1_MASK); + reg |= LDB_BIT_MAP_CH0_SPWG | LDB_BIT_MAP_CH1_SPWG; + + /* channel mode setting */ + reg &= ~(LDB_CH0_MODE_MASK | LDB_CH1_MODE_MASK); + reg &= ~(LDB_DATA_WIDTH_CH0_MASK | LDB_DATA_WIDTH_CH1_MASK); + + if (bits_per_pixel(setting->if_fmt) == 24) + reg |= LDB_DATA_WIDTH_CH0_24 | LDB_DATA_WIDTH_CH1_24; + else + reg |= LDB_DATA_WIDTH_CH0_18 | LDB_DATA_WIDTH_CH1_18; + + if (g_ldb_mode) + ldb->mode = g_ldb_mode; + else + ldb->mode = plat_data->mode; + + if (ldb->mode == LDB_SPL_DI0) { + reg |= LDB_SPLIT_MODE_EN | LDB_CH0_MODE_EN_TO_DI0 + | LDB_CH1_MODE_EN_TO_DI0; + setting->disp_id = 0; + } else if (ldb->mode == LDB_SPL_DI1) { + reg |= LDB_SPLIT_MODE_EN | LDB_CH0_MODE_EN_TO_DI1 + | LDB_CH1_MODE_EN_TO_DI1; + setting->disp_id = 1; + } else if (ldb->mode == LDB_DUL_DI0) { + reg &= ~LDB_SPLIT_MODE_EN; + reg |= LDB_CH0_MODE_EN_TO_DI0 | LDB_CH1_MODE_EN_TO_DI0; + setting->disp_id = 0; + } else if (ldb->mode == LDB_DUL_DI1) { + reg &= ~LDB_SPLIT_MODE_EN; + reg |= LDB_CH0_MODE_EN_TO_DI1 | LDB_CH1_MODE_EN_TO_DI1; + setting->disp_id = 1; + } else if (ldb->mode == LDB_SIN0) { + reg &= ~LDB_SPLIT_MODE_EN; + setting->disp_id = plat_data->disp_id; + if (setting->disp_id == 0) + reg |= LDB_CH0_MODE_EN_TO_DI0; + else + reg |= LDB_CH0_MODE_EN_TO_DI1; + } else if (ldb->mode == LDB_SIN1) { + reg &= ~LDB_SPLIT_MODE_EN; + setting->disp_id = plat_data->disp_id; + if (setting->disp_id == 0) + reg |= LDB_CH1_MODE_EN_TO_DI0; + else + reg |= LDB_CH1_MODE_EN_TO_DI1; + } else { /* separate mode*/ + setting->disp_id = plat_data->disp_id; + + /* first output is LVDS0 or LVDS1 */ + if (ldb->mode == LDB_SEP0) + lvds_channel = 0; + else + lvds_channel = 1; + + reg &= ~LDB_SPLIT_MODE_EN; + + if ((lvds_channel == 0) && (setting->disp_id == 0)) + reg |= LDB_CH0_MODE_EN_TO_DI0; + else if ((lvds_channel == 0) && (setting->disp_id == 1)) + reg |= LDB_CH0_MODE_EN_TO_DI1; + else if ((lvds_channel == 1) && (setting->disp_id == 0)) + reg |= LDB_CH1_MODE_EN_TO_DI0; + else + reg |= LDB_CH1_MODE_EN_TO_DI1; + + if (bits_per_pixel(setting->if_fmt) == 24) { + if (lvds_channel == 0) + reg &= ~LDB_DATA_WIDTH_CH1_24; + else + reg &= ~LDB_DATA_WIDTH_CH0_24; + } else { + if (lvds_channel == 0) + reg &= ~LDB_DATA_WIDTH_CH1_18; + else + reg &= ~LDB_DATA_WIDTH_CH0_18; + } + } + + writel(reg, ldb->control_reg); + + /* clock setting */ + ldb_clk[6] += setting->disp_id; + ldb->ldb_di_clk[0] = clk_get(&ldb->pdev->dev, ldb_clk); + if (IS_ERR(ldb->ldb_di_clk[0])) { + dev_err(&ldb->pdev->dev, "get ldb clk0 failed\n"); + iounmap(ldb->reg); + return PTR_ERR(ldb->ldb_di_clk[0]); + } + di_clk[3] += setting->dev_id; + di_clk[7] += setting->disp_id; + ldb->di_clk[0] = clk_get(&ldb->pdev->dev, di_clk); + if (IS_ERR(ldb->di_clk[0])) { + dev_err(&ldb->pdev->dev, "get di clk0 failed\n"); + iounmap(ldb->reg); + return PTR_ERR(ldb->di_clk[0]); + } + + dev_dbg(&ldb->pdev->dev, "ldb_clk to di clk: %s -> %s\n", ldb_clk, di_clk); + + /* fb notifier for clk setting */ + ldb->nb.notifier_call = ldb_fb_event, + ret = fb_register_client(&ldb->nb); + if (ret < 0) { + iounmap(ldb->reg); + return ret; + } + + setting_idx = 0; + ldb->inited = true; + } else { /* second time for separate mode */ + char di_clk[] = "ipu1_di0_clk"; + char ldb_clk[] = "ldb_di0_clk"; + int lvds_channel; + + if ((ldb->mode == LDB_SPL_DI0) || + (ldb->mode == LDB_SPL_DI1) || + (ldb->mode == LDB_DUL_DI0) || + (ldb->mode == LDB_DUL_DI1) || + (ldb->mode == LDB_SIN0) || + (ldb->mode == LDB_SIN1)) { + dev_err(&ldb->pdev->dev, "for second ldb disp" + "ldb mode should in separate mode\n"); + return -EINVAL; + } + + setting->dev_id = plat_data->ipu_id; + setting->disp_id = !plat_data->disp_id; + + /* second output is LVDS0 or LVDS1 */ + if (ldb->mode == LDB_SEP0) + lvds_channel = 1; + else + lvds_channel = 0; + + reg = readl(ldb->control_reg); + if ((lvds_channel == 0) && (setting->disp_id == 0)) + reg |= LDB_CH0_MODE_EN_TO_DI0; + else if ((lvds_channel == 0) && (setting->disp_id == 1)) + reg |= LDB_CH0_MODE_EN_TO_DI1; + else if ((lvds_channel == 1) && (setting->disp_id == 0)) + reg |= LDB_CH1_MODE_EN_TO_DI0; + else + reg |= LDB_CH1_MODE_EN_TO_DI1; + + if (bits_per_pixel(setting->if_fmt) == 24) { + if (lvds_channel == 0) + reg |= LDB_DATA_WIDTH_CH0_24; + else + reg |= LDB_DATA_WIDTH_CH1_24; + } else { + if (lvds_channel == 0) + reg |= LDB_DATA_WIDTH_CH0_18; + else + reg |= LDB_DATA_WIDTH_CH1_18; + } + writel(reg, ldb->control_reg); + + /* clock setting */ + ldb_clk[6] += setting->disp_id; + ldb->ldb_di_clk[1] = clk_get(&ldb->pdev->dev, ldb_clk); + if (IS_ERR(ldb->ldb_di_clk[1])) { + dev_err(&ldb->pdev->dev, "get ldb clk1 failed\n"); + return PTR_ERR(ldb->ldb_di_clk[1]); + } + di_clk[3] += setting->dev_id; + di_clk[7] += setting->disp_id; + ldb->di_clk[1] = clk_get(&ldb->pdev->dev, di_clk); + if (IS_ERR(ldb->di_clk[1])) { + dev_err(&ldb->pdev->dev, "get di clk1 failed\n"); + return PTR_ERR(ldb->di_clk[1]); + } + + dev_dbg(&ldb->pdev->dev, "ldb_clk to di clk: %s -> %s\n", ldb_clk, di_clk); + + setting_idx = 1; + } + + /* + * ldb_di0_clk -> ipux_di0_clk + * ldb_di1_clk -> ipux_di1_clk + */ + clk_set_parent(ldb->di_clk[setting_idx], + ldb->ldb_di_clk[setting_idx]); + + /* must use spec video mode defined by driver */ + ret = fb_find_mode(&setting->fbi->var, setting->fbi, setting->dft_mode_str, + ldb_modedb, ldb_modedb_sz, NULL, setting->default_bpp); + if (ret != 1) + fb_videomode_to_var(&setting->fbi->var, &ldb_modedb[0]); + + INIT_LIST_HEAD(&setting->fbi->modelist); + for (i = 0; i < ldb_modedb_sz; i++) { + struct fb_videomode m; + fb_var_to_videomode(&m, &setting->fbi->var); + if (fb_mode_is_equal(&m, &ldb_modedb[i])) { + fb_add_videomode(&ldb_modedb[i], + &setting->fbi->modelist); + break; + } + } + + /* save current ldb setting for fb notifier */ + ldb->setting[setting_idx].active = true; + ldb->setting[setting_idx].ipu = setting->dev_id; + ldb->setting[setting_idx].di = setting->disp_id; + + return ret; +} + +static void ldb_disp_deinit(struct mxc_dispdrv_entry *disp) +{ + struct ldb_data *ldb = mxc_dispdrv_getdata(disp); + int i; + + writel(0, ldb->control_reg); + + for (i = 0; i < 2; i++) { + clk_disable(ldb->ldb_di_clk[i]); + clk_put(ldb->ldb_di_clk[i]); + } + + fb_unregister_client(&ldb->nb); + + iounmap(ldb->reg); +} + +static struct mxc_dispdrv_driver ldb_drv = { + .name = DISPDRV_LDB, + .init = ldb_disp_init, + .deinit = ldb_disp_deinit, +}; + +/*! + * This function is called by the driver framework to initialize the LDB + * device. + * + * @param dev The device structure for the LDB passed in by the + * driver framework. + * + * @return Returns 0 on success or negative error code on error + */ +static int ldb_probe(struct platform_device *pdev) +{ + int ret = 0; + struct ldb_data *ldb; + + ldb = kzalloc(sizeof(struct ldb_data), GFP_KERNEL); + if (!ldb) { + ret = -ENOMEM; + goto alloc_failed; + } + + ldb->pdev = pdev; + ldb->disp_ldb = mxc_dispdrv_register(&ldb_drv); + mxc_dispdrv_setdata(ldb->disp_ldb, ldb); + + dev_set_drvdata(&pdev->dev, ldb); + +alloc_failed: + return ret; +} + +static int ldb_remove(struct platform_device *pdev) +{ + struct ldb_data *ldb = dev_get_drvdata(&pdev->dev); + + mxc_dispdrv_unregister(ldb->disp_ldb); + kfree(ldb); + return 0; +} + +static struct platform_driver mxcldb_driver = { + .driver = { + .name = "mxc_ldb", + }, + .probe = ldb_probe, + .remove = ldb_remove, +}; + +static int __init ldb_init(void) +{ + return platform_driver_register(&mxcldb_driver); +} + +static void __exit ldb_uninit(void) +{ + platform_driver_unregister(&mxcldb_driver); +} + +module_init(ldb_init); +module_exit(ldb_uninit); + +MODULE_AUTHOR("Freescale Semiconductor, Inc."); +MODULE_DESCRIPTION("MXC LDB driver"); +MODULE_LICENSE("GPL"); |