blob: 180bf7f6f8d16a234c249defc13a7b77cc78df7c [file] [log] [blame]
/*
* 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/module.h>
#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");