diff options
Diffstat (limited to 'drivers/mfd/mxc-hdmi-core.c')
-rw-r--r-- | drivers/mfd/mxc-hdmi-core.c | 670 |
1 files changed, 670 insertions, 0 deletions
diff --git a/drivers/mfd/mxc-hdmi-core.c b/drivers/mfd/mxc-hdmi-core.c new file mode 100644 index 00000000000..24afc502cea --- /dev/null +++ b/drivers/mfd/mxc-hdmi-core.c @@ -0,0 +1,670 @@ +/* + * Copyright (C) 2011 Freescale Semiconductor, Inc. + * + * 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/init.h> +#include <linux/slab.h> +#include <linux/device.h> +#include <linux/err.h> +#include <linux/io.h> +#include <linux/clk.h> +#include <linux/spinlock.h> +#include <linux/irq.h> +#include <linux/interrupt.h> +#include <linux/dma-mapping.h> +#include <linux/of.h> + +#include <linux/platform_device.h> +#include <linux/regulator/machine.h> +#include <asm/mach-types.h> + +#include <mach/clock.h> +#include <mach/mxc_hdmi.h> +#include <mach/ipu-v3.h> +#include "../mxc/ipu3/ipu_prv.h" +#include <linux/mfd/mxc-hdmi-core.h> +#include <linux/fsl_devices.h> + +struct mxc_hdmi_data { + struct platform_device *pdev; + unsigned long __iomem *reg_base; + unsigned long reg_phys_base; + struct device *dev; +}; + +static unsigned long hdmi_base; +struct clk *isfr_clk; +struct clk *iahb_clk; +static unsigned int irq_enable_cnt; +spinlock_t irq_spinlock; +bool irq_initialized; +bool irq_enabled; +unsigned int sample_rate; +unsigned long pixel_clk_rate; +struct clk *pixel_clk; +int hdmi_ratio; +int mxc_hdmi_ipu_id; +int mxc_hdmi_disp_id; + +u8 hdmi_readb(unsigned int reg) +{ + u8 value; + + value = __raw_readb(hdmi_base + reg); + + pr_debug("hdmi rd: 0x%04x = 0x%02x\n", reg, value); + + return value; +} + +#ifdef DEBUG +static bool overflow_lo; +static bool overflow_hi; + +bool hdmi_check_overflow(void) +{ + u8 val, lo, hi; + + val = hdmi_readb(HDMI_IH_FC_STAT2); + lo = (val & HDMI_IH_FC_STAT2_LOW_PRIORITY_OVERFLOW) != 0; + hi = (val & HDMI_IH_FC_STAT2_HIGH_PRIORITY_OVERFLOW) != 0; + + if ((lo != overflow_lo) || (hi != overflow_hi)) { + pr_debug("%s LowPriority=%d HighPriority=%d <=======================\n", + __func__, lo, hi); + overflow_lo = lo; + overflow_hi = hi; + return true; + } + return false; +} +#else +bool hdmi_check_overflow(void) +{ + return false; +} +#endif + +void hdmi_writeb(u8 value, unsigned int reg) +{ + hdmi_check_overflow(); + pr_debug("hdmi wr: 0x%04x = 0x%02x\n", reg, value); + __raw_writeb(value, hdmi_base + reg); + hdmi_check_overflow(); +} + +void hdmi_mask_writeb(u8 data, unsigned int reg, u8 shift, u8 mask) +{ + u8 value = hdmi_readb(reg) & ~mask; + value |= (data << shift) & mask; + hdmi_writeb(value, reg); +} + +unsigned int hdmi_read4(unsigned int reg) +{ + /* read a four byte address from registers */ + return (hdmi_readb(reg + 3) << 24) | + (hdmi_readb(reg + 2) << 16) | + (hdmi_readb(reg + 1) << 8) | + hdmi_readb(reg); +} + +void hdmi_write4(unsigned int value, unsigned int reg) +{ + /* write a four byte address to hdmi regs */ + hdmi_writeb(value & 0xff, reg); + hdmi_writeb((value >> 8) & 0xff, reg + 1); + hdmi_writeb((value >> 16) & 0xff, reg + 2); + hdmi_writeb((value >> 24) & 0xff, reg + 3); +} + +void hdmi_irq_init() +{ + /* First time IRQ is initialized, set enable_cnt to 1, + * since IRQ starts out enabled after request_irq */ + if (!irq_initialized) { + irq_enable_cnt = 1; + irq_initialized = true; + irq_enabled = true; + } +} + +void hdmi_irq_enable(int irq) +{ + unsigned long flags; + + spin_lock_irqsave(&irq_spinlock, flags); + + if (!irq_enabled) { + enable_irq(irq); + irq_enabled = true; + } + + irq_enable_cnt++; + + spin_unlock_irqrestore(&irq_spinlock, flags); +} + +unsigned int hdmi_irq_disable(int irq) +{ + unsigned long flags; + + spin_lock_irqsave(&irq_spinlock, flags); + + WARN_ON (irq_enable_cnt == 0); + + irq_enable_cnt--; + + /* Only disable HDMI IRQ if IAHB clk is off */ + if ((irq_enable_cnt == 0) && (clk_get_usecount(iahb_clk) == 0)) { + disable_irq_nosync(irq); + irq_enabled = false; + spin_unlock_irqrestore(&irq_spinlock, flags); + return IRQ_DISABLE_SUCCEED; + } + + spin_unlock_irqrestore(&irq_spinlock, flags); + + return IRQ_DISABLE_FAIL; +} + +static void initialize_hdmi_ih_mutes(void) +{ + u8 ih_mute; + + /* + * Boot up defaults are: + * HDMI_IH_MUTE = 0x03 (disabled) + * HDMI_IH_MUTE_* = 0x00 (enabled) + */ + + /* Disable top level interrupt bits in HDMI block */ + ih_mute = hdmi_readb(HDMI_IH_MUTE) | + HDMI_IH_MUTE_MUTE_WAKEUP_INTERRUPT | + HDMI_IH_MUTE_MUTE_ALL_INTERRUPT; + + hdmi_writeb(ih_mute, HDMI_IH_MUTE); + + /* by default mask all interrupts */ + hdmi_writeb(0xff, HDMI_VP_MASK); + hdmi_writeb(0xff, HDMI_FC_MASK0); + hdmi_writeb(0xff, HDMI_FC_MASK1); + hdmi_writeb(0xff, HDMI_FC_MASK2); + hdmi_writeb(0xff, HDMI_PHY_MASK0); + hdmi_writeb(0xff, HDMI_PHY_I2CM_INT_ADDR); + hdmi_writeb(0xff, HDMI_PHY_I2CM_CTLINT_ADDR); + hdmi_writeb(0xff, HDMI_AUD_INT); + hdmi_writeb(0xff, HDMI_AUD_SPDIFINT); + hdmi_writeb(0xff, HDMI_AUD_HBR_MASK); + hdmi_writeb(0xff, HDMI_GP_MASK); + hdmi_writeb(0xff, HDMI_A_APIINTMSK); + hdmi_writeb(0xff, HDMI_CEC_MASK); + hdmi_writeb(0xff, HDMI_I2CM_INT); + hdmi_writeb(0xff, HDMI_I2CM_CTLINT); + + /* Disable interrupts in the IH_MUTE_* registers */ + hdmi_writeb(0xff, HDMI_IH_MUTE_FC_STAT0); + hdmi_writeb(0xff, HDMI_IH_MUTE_FC_STAT1); + hdmi_writeb(0xff, HDMI_IH_MUTE_FC_STAT2); + hdmi_writeb(0xff, HDMI_IH_MUTE_AS_STAT0); + hdmi_writeb(0xff, HDMI_IH_MUTE_PHY_STAT0); + hdmi_writeb(0xff, HDMI_IH_MUTE_I2CM_STAT0); + hdmi_writeb(0xff, HDMI_IH_MUTE_CEC_STAT0); + hdmi_writeb(0xff, HDMI_IH_MUTE_VP_STAT0); + hdmi_writeb(0xff, HDMI_IH_MUTE_I2CMPHY_STAT0); + hdmi_writeb(0xff, HDMI_IH_MUTE_AHBDMAAUD_STAT0); + + /* Enable top level interrupt bits in HDMI block */ + ih_mute &= ~(HDMI_IH_MUTE_MUTE_WAKEUP_INTERRUPT | + HDMI_IH_MUTE_MUTE_ALL_INTERRUPT); + hdmi_writeb(ih_mute, HDMI_IH_MUTE); +} + +static void hdmi_set_clock_regenerator_n(unsigned int value) +{ + u8 val; + + hdmi_writeb(value & 0xff, HDMI_AUD_N1); + hdmi_writeb((value >> 8) & 0xff, HDMI_AUD_N2); + hdmi_writeb((value >> 16) & 0x0f, HDMI_AUD_N3); + + /* nshift factor = 0 */ + val = hdmi_readb(HDMI_AUD_CTS3); + val &= ~HDMI_AUD_CTS3_N_SHIFT_MASK; + hdmi_writeb(val, HDMI_AUD_CTS3); +} + +static void hdmi_set_clock_regenerator_cts(unsigned int cts) +{ + u8 val; + + /* Must be set/cleared first */ + val = hdmi_readb(HDMI_AUD_CTS3); + val &= ~HDMI_AUD_CTS3_CTS_MANUAL; + hdmi_writeb(val, HDMI_AUD_CTS3); + + hdmi_writeb(cts & 0xff, HDMI_AUD_CTS1); + hdmi_writeb((cts >> 8) & 0xff, HDMI_AUD_CTS2); + hdmi_writeb(((cts >> 16) & HDMI_AUD_CTS3_AUDCTS19_16_MASK) | + HDMI_AUD_CTS3_CTS_MANUAL, HDMI_AUD_CTS3); +} + +static unsigned int hdmi_compute_n(unsigned int freq, unsigned long pixel_clk, + unsigned int ratio) +{ + unsigned int n = (128 * freq) / 1000; + + switch (freq) { + case 32000: + if (pixel_clk == 25170000) + n = (ratio == 150) ? 9152 : 4576; + else if (pixel_clk == 27020000) + n = (ratio == 150) ? 8192 : 4096; + else if (pixel_clk == 74170000 || pixel_clk == 148350000) + n = 11648; + else + n = 4096; + break; + + case 44100: + if (pixel_clk == 25170000) + n = 7007; + else if (pixel_clk == 74170000) + n = 17836; + else if (pixel_clk == 148350000) + n = (ratio == 150) ? 17836 : 8918; + else + n = 6272; + break; + + case 48000: + if (pixel_clk == 25170000) + n = (ratio == 150) ? 9152 : 6864; + else if (pixel_clk == 27020000) + n = (ratio == 150) ? 8192 : 6144; + else if (pixel_clk == 74170000) + n = 11648; + else if (pixel_clk == 148350000) + n = (ratio == 150) ? 11648 : 5824; + else + n = 6144; + break; + + case 88200: + n = hdmi_compute_n(44100, pixel_clk, ratio) * 2; + break; + + case 96000: + n = hdmi_compute_n(48000, pixel_clk, ratio) * 2; + break; + + case 176400: + n = hdmi_compute_n(44100, pixel_clk, ratio) * 4; + break; + + case 192000: + n = hdmi_compute_n(48000, pixel_clk, ratio) * 4; + break; + + default: + break; + } + + return n; +} + +static unsigned int hdmi_compute_cts(unsigned int freq, unsigned long pixel_clk, + unsigned int ratio) +{ + unsigned int cts = 0; + switch (freq) { + case 32000: + if (pixel_clk == 297000000) { + cts = 222750; + break; + } + case 48000: + case 96000: + case 192000: + switch (pixel_clk) { + case 25200000: + case 27000000: + case 54000000: + case 74250000: + case 148500000: + cts = pixel_clk / 1000; + break; + case 297000000: + cts = 247500; + break; + /* + * All other TMDS clocks are not supported by + * DWC_hdmi_tx. The TMDS clocks divided or + * multiplied by 1,001 coefficients are not + * supported. + */ + default: + break; + } + break; + case 44100: + case 88200: + case 176400: + switch (pixel_clk) { + case 25200000: + cts = 28000; + break; + case 27000000: + cts = 30000; + break; + case 54000000: + cts = 60000; + break; + case 74250000: + cts = 82500; + break; + case 148500000: + cts = 165000; + break; + case 297000000: + cts = 247500; + break; + default: + break; + } + break; + default: + break; + } + if (ratio == 100) + return cts; + else + return (cts * ratio) / 100; +} + +static void hdmi_get_pixel_clk(void) +{ + struct ipu_soc *ipu; + unsigned long rate; + + if (pixel_clk == NULL) { + ipu = ipu_get_soc(mxc_hdmi_ipu_id); + pixel_clk = clk_get(ipu->dev, "pixel_clk_0"); + if (IS_ERR(pixel_clk)) { + pr_err("%s could not get pixel_clk_0\n", __func__); + return; + } + } + + rate = clk_get_rate(pixel_clk); + if (rate != 0) + pixel_clk_rate = rate; +} + +static void hdmi_set_clk_regenerator(void) +{ + unsigned int clk_n, clk_cts; + + clk_n = hdmi_compute_n(sample_rate, pixel_clk_rate, hdmi_ratio); + clk_cts = hdmi_compute_cts(sample_rate, pixel_clk_rate, hdmi_ratio); + + if (clk_cts == 0) { + pr_debug("%s: pixel clock not supported: %d\n", + __func__, (int)pixel_clk_rate); + return; + } + + clk_enable(isfr_clk); + clk_enable(iahb_clk); + + pr_debug("%s: samplerate=%d ratio=%d pixelclk=%d N=%d cts=%d\n", + __func__, sample_rate, hdmi_ratio, (int)pixel_clk_rate, + clk_n, clk_cts); + + hdmi_set_clock_regenerator_n(clk_n); + hdmi_set_clock_regenerator_cts(clk_cts); + + clk_disable(iahb_clk); + clk_disable(isfr_clk); +} + +/* Need to run this before phy is enabled the first time to prevent + * overflow condition in HDMI_IH_FC_STAT2 */ +void hdmi_init_clk_regenerator(void) +{ + if (pixel_clk_rate == 0) { + pixel_clk_rate = 74250000; + hdmi_set_clk_regenerator(); + } +} + +void hdmi_clk_regenerator_update_pixel_clock(void) +{ + /* Get pixel clock from ipu */ + hdmi_get_pixel_clk(); + hdmi_set_clk_regenerator(); +} + +void hdmi_set_sample_rate(unsigned int rate) +{ + sample_rate = rate; + hdmi_set_clk_regenerator(); +} + +static void hdmi_init(int ipu_id, int disp_id) +{ + int hdmi_mux_setting; + + if ((ipu_id > 1) || (ipu_id < 0)) { + printk(KERN_ERR"Invalid IPU select for HDMI: %d. Set to 0\n", + ipu_id); + ipu_id = 0; + } + + if ((disp_id > 1) || (disp_id < 0)) { + printk(KERN_ERR"Invalid DI select for HDMI: %d. Set to 0\n", + disp_id); + disp_id = 0; + } + + /* Configure the connection between IPU1/2 and HDMI */ + hdmi_mux_setting = 2*ipu_id + disp_id; + + /* GPR3, bits 2-3 = HDMI_MUX_CTL */ + /*mxc_iomux_set_gpr_register(3, 2, 2, hdmi_mux_setting);*/ +} + +static struct fsl_mxc_hdmi_platform_data hdmi_vdata = { + .init = hdmi_init, +}; + +static int mxc_hdmi_core_probe(struct platform_device *pdev) +{ + struct fsl_mxc_hdmi_core_platform_data *pdata = pdev->dev.platform_data; + struct mxc_hdmi_data *hdmi_data; + struct resource *res; + int ret = 0; + struct platform_device_info pdevinfo_hdmi_v; + +#ifdef DEBUG + overflow_lo = false; + overflow_hi = false; +#endif + res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + if (!res) + return -ENOENT; + + hdmi_data = kzalloc(sizeof(struct mxc_hdmi_data), GFP_KERNEL); + if (!hdmi_data) { + dev_err(&pdev->dev, "Couldn't allocate mxc hdmi mfd device\n"); + return -ENOMEM; + } + hdmi_data->pdev = pdev; + + pixel_clk = NULL; + sample_rate = 48000; + pixel_clk_rate = 0; + hdmi_ratio = 100; + + irq_enable_cnt = 0; + irq_initialized = false; + irq_enabled = true; + spin_lock_init(&irq_spinlock); + + isfr_clk = clk_get(&hdmi_data->pdev->dev, "hdmi_isfr_clk"); + if (IS_ERR(isfr_clk)) { + ret = PTR_ERR(isfr_clk); + dev_err(&hdmi_data->pdev->dev, + "Unable to get HDMI isfr clk: %d\n", ret); + goto eclkg; + } + + ret = clk_enable(isfr_clk); + if (ret < 0) { + dev_err(&pdev->dev, "Cannot enable HDMI clock: %d\n", ret); + goto eclke; + } + + pr_debug("%s isfr_clk:%d\n", __func__, + (int)clk_get_rate(isfr_clk)); + + iahb_clk = clk_get(&hdmi_data->pdev->dev, "hdmi_iahb_clk"); + if (IS_ERR(iahb_clk)) { + ret = PTR_ERR(iahb_clk); + dev_err(&hdmi_data->pdev->dev, + "Unable to get HDMI iahb clk: %d\n", ret); + goto eclkg2; + } + + ret = clk_enable(iahb_clk); + if (ret < 0) { + dev_err(&pdev->dev, "Cannot enable HDMI clock: %d\n", ret); + goto eclke2; + } + + hdmi_data->reg_phys_base = res->start; + if (!request_mem_region(res->start, resource_size(res), + dev_name(&pdev->dev))) { + dev_err(&pdev->dev, "request_mem_region failed\n"); + ret = -EBUSY; + goto emem; + } + + hdmi_data->reg_base = ioremap(res->start, resource_size(res)); + if (!hdmi_data->reg_base) { + dev_err(&pdev->dev, "ioremap failed\n"); + ret = -ENOMEM; + goto eirq; + } + hdmi_base = (unsigned long)hdmi_data->reg_base; + + pr_debug("\n%s hdmi hw base = 0x%08x\n\n", __func__, (int)res->start); + + if (pdata) { + mxc_hdmi_ipu_id = pdata->ipu_id; + mxc_hdmi_disp_id = pdata->disp_id; + } else { + of_property_read_u32(pdev->dev.of_node, "ipu", &mxc_hdmi_ipu_id); + of_property_read_u32(pdev->dev.of_node, "di", &mxc_hdmi_disp_id); + } + + initialize_hdmi_ih_mutes(); + + /* Disable HDMI clocks until video/audio sub-drivers are initialized */ + clk_disable(isfr_clk); + clk_disable(iahb_clk); + + /* Replace platform data coming in with a local struct */ + platform_set_drvdata(pdev, hdmi_data); + + /* register hdmi video */ + res = platform_get_resource(pdev, IORESOURCE_IRQ, 0); + pdevinfo_hdmi_v.name = "mxc_hdmi"; + pdevinfo_hdmi_v.id = pdev->id; + pdevinfo_hdmi_v.res = res; + pdevinfo_hdmi_v.num_res = 1; + pdevinfo_hdmi_v.data = &hdmi_vdata; + pdevinfo_hdmi_v.size_data = sizeof(hdmi_vdata); + pdevinfo_hdmi_v.dma_mask = DMA_BIT_MASK(32); + pdevinfo_hdmi_v.parent = &pdev->dev; + platform_device_register_full(&pdevinfo_hdmi_v); + + return ret; + +eirq: + release_mem_region(res->start, resource_size(res)); +emem: + clk_disable(iahb_clk); +eclke2: + clk_put(iahb_clk); +eclkg2: + clk_disable(isfr_clk); +eclke: + clk_put(isfr_clk); +eclkg: + kfree(hdmi_data); + return ret; +} + + +static int __exit mxc_hdmi_core_remove(struct platform_device *pdev) +{ + struct mxc_hdmi_data *hdmi_data = platform_get_drvdata(pdev); + struct resource *res = platform_get_resource(pdev, IORESOURCE_MEM, 0); + + iounmap(hdmi_data->reg_base); + release_mem_region(res->start, resource_size(res)); + + kfree(hdmi_data); + + return 0; +} + +static const struct of_device_id mxc_hdmi_core_dt_ids[] = { + { .compatible = "fsl,imx6q-hdmi-core", }, + { /* sentinel */ } +}; + +static struct platform_driver mxc_hdmi_core_driver = { + .driver = { + .name = "mxc_hdmi_core", + .owner = THIS_MODULE, + .of_match_table = mxc_hdmi_core_dt_ids, + }, + .remove = __exit_p(mxc_hdmi_core_remove), +}; + +static int __init mxc_hdmi_core_init(void) +{ + return platform_driver_probe(&mxc_hdmi_core_driver, + mxc_hdmi_core_probe); +} + +static void __exit mxc_hdmi_core_exit(void) +{ + platform_driver_unregister(&mxc_hdmi_core_driver); +} + +subsys_initcall(mxc_hdmi_core_init); +module_exit(mxc_hdmi_core_exit); + +MODULE_DESCRIPTION("Core driver for Freescale i.Mx on-chip HDMI"); +MODULE_AUTHOR("Freescale Semiconductor, Inc."); +MODULE_LICENSE("GPL"); |