The i.MX6 SoC in HDMI controller has one more on-SoC I2C bus dedicated to read EDID data from a connected monitor. The controller does not fully support I2C communications, only specific forms of I2C transfer patters can be programmed, but having those limitations in mind it seems reasonable to add another I2C bus driver representing iMX6 HDMI DDC bus. The bus can be used to read monitor's EDID or access EEPROM, but communication to most of the I2C devices will fail or be invalid. The iMX6 HDMI DDC I2C bus may be used separately from main iMX6 HDMI controller code, for instance as an additional I2C bus with limited capabilities. Signed-off-by: Vladimir Zapolskiy <vladimir_zapolskiy@xxxxxxxxxx> Cc: Wolfram Sang <wsa@xxxxxxxxxxxxx> Cc: Philipp Zabel <p.zabel@xxxxxxxxxxxxxx> Cc: linux-i2c@xxxxxxxxxxxxxxx --- drivers/i2c/busses/Kconfig | 13 + drivers/i2c/busses/Makefile | 1 + drivers/i2c/busses/i2c-imx-hdmi.c | 510 +++++++++++++++++++++++++++++++++++++ 3 files changed, 524 insertions(+) create mode 100644 drivers/i2c/busses/i2c-imx-hdmi.c diff --git a/drivers/i2c/busses/Kconfig b/drivers/i2c/busses/Kconfig index 06e99eb..82848a9 100644 --- a/drivers/i2c/busses/Kconfig +++ b/drivers/i2c/busses/Kconfig @@ -1045,4 +1045,17 @@ config SCx200_ACB This support is also available as a module. If so, the module will be called scx200_acb. +config I2C_IMX_HDMI + tristate "IMX6 HDMI DDC I2C master interface" + depends on ARCH_MXC + default n + help + Say Y here if you want to use the HDMI DDC bus controller + found on the Freescale i.MX6 processors. Most probably you + don't need this driver, if HDMI monitor is supposed to be + connected over regular iMX6 I2C bus. + + This driver can also be built as a module. If so, the module + will be called i2c-imx-hdmi. + endmenu diff --git a/drivers/i2c/busses/Makefile b/drivers/i2c/busses/Makefile index 78d56c5..49bebb0 100644 --- a/drivers/i2c/busses/Makefile +++ b/drivers/i2c/busses/Makefile @@ -99,6 +99,7 @@ obj-$(CONFIG_I2C_ACORN) += i2c-acorn.o obj-$(CONFIG_I2C_BCM_KONA) += i2c-bcm-kona.o obj-$(CONFIG_I2C_CROS_EC_TUNNEL) += i2c-cros-ec-tunnel.o obj-$(CONFIG_I2C_ELEKTOR) += i2c-elektor.o +obj-$(CONFIG_I2C_IMX_HDMI) += i2c-imx-hdmi.o obj-$(CONFIG_I2C_PCA_ISA) += i2c-pca-isa.o obj-$(CONFIG_I2C_SIBYTE) += i2c-sibyte.o obj-$(CONFIG_SCx200_ACB) += scx200_acb.o diff --git a/drivers/i2c/busses/i2c-imx-hdmi.c b/drivers/i2c/busses/i2c-imx-hdmi.c new file mode 100644 index 0000000..f9b0118 --- /dev/null +++ b/drivers/i2c/busses/i2c-imx-hdmi.c @@ -0,0 +1,510 @@ +/* + * I2C master driver for iMX6 HDMI DDC bus + * + * Copyright (C) 2013-2014 Mentor Graphics + * + * 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. + */ + +/* + * According to the iMX6 Reference Manual only two types of transactions + * are supported by HDMI I2C Master Interface in Normal Mode: + * + * A) one byte data write transaction (I2C spec 2 bytes transmission): + * master S|slave addr[6:0]|0| |slave reg[7:0]| |data[7:0]| |P + * slave | Ack | | Ack | | Ack | + * + * B) one byte data read transaction (I2C spec write/read combined format): + * master S|slave addr[6:0]|0| |slave reg[7:0]| | ... + * slave | Ack | | Ack | ... + * + * master ... Sr|slave addr[6:0]|1| | | Ack |P + * slave ... | Ack |data[7:0]| + * + * + * The technical limitations of the iMX6 HDMI E-DDC bus does not allow + * to call it an I2C compatible bus, however relativery large subset of + * I2C transactions can be decomposed into aforementioned data read/write + * operations and many I2C devices correctly support those operations. + */ + +#include <linux/clk.h> +#include <linux/i2c.h> +#include <linux/init.h> +#include <linux/io.h> +#include <linux/interrupt.h> +#include <linux/kernel.h> +#include <linux/module.h> +#include <linux/of_device.h> + +/* iMX6 HDMI shared registers */ +#define HDMI_IH_I2CM_STAT0 0x0105 +#define HDMI_IH_MUTE_I2CM_STAT0 0x0185 + +/* HDMI_IH_I2CM_STAT0 / HDMI_IH_MUTE_I2CM_STAT0 register bits */ +#define HDMI_IH_I2CM_STAT0_ERROR BIT(0) +#define HDMI_IH_I2CM_STAT0_DONE BIT(1) + +/* iMX6 HDMI I2C master (E-DDC) register offsets */ +#define HDMI_I2CM_SLAVE 0x7E00 +#define HDMI_I2CM_ADDRESS 0x7E01 +#define HDMI_I2CM_DATAO 0x7E02 +#define HDMI_I2CM_DATAI 0x7E03 +#define HDMI_I2CM_OPERATION 0x7E04 +#define HDMI_I2CM_INT 0x7E05 +#define HDMI_I2CM_CTLINT 0x7E06 +#define HDMI_I2CM_DIV 0x7E07 +#define HDMI_I2CM_SEGADDR 0x7E08 +#define HDMI_I2CM_SOFTRSTZ 0x7E09 +#define HDMI_I2CM_SEGPTR 0x7E0A +#define HDMI_I2CM_SS_SCL_HCNT_1_ADDR 0x7E0B +#define HDMI_I2CM_SS_SCL_HCNT_0_ADDR 0x7E0C +#define HDMI_I2CM_SS_SCL_LCNT_1_ADDR 0x7E0D +#define HDMI_I2CM_SS_SCL_LCNT_0_ADDR 0x7E0E +#define HDMI_I2CM_FS_SCL_HCNT_1_ADDR 0x7E0F +#define HDMI_I2CM_FS_SCL_HCNT_0_ADDR 0x7E10 +#define HDMI_I2CM_FS_SCL_LCNT_1_ADDR 0x7E11 +#define HDMI_I2CM_FS_SCL_LCNT_0_ADDR 0x7E12 + +/* HDMI_I2CM_OPERATION register bits */ +#define HDMI_I2CM_OPERATION_READ BIT(0) +#define HDMI_I2CM_OPERATION_READ_EXT BIT(1) +#define HDMI_I2CM_OPERATION_WRITE BIT(4) + +/* HDMI_I2CM_INT register bits */ +#define HDMI_I2CM_INT_DONE_MASK BIT(2) +#define HDMI_I2CM_INT_DONE_POL BIT(3) + +/* HDMI_I2CM_CTLINT register bits */ +#define HDMI_I2CM_CTLINT_ARB_MASK BIT(2) +#define HDMI_I2CM_CTLINT_ARB_POL BIT(3) +#define HDMI_I2CM_CTLINT_NAC_MASK BIT(6) +#define HDMI_I2CM_CTLINT_NAC_POL BIT(7) + + +struct imx_hdmi_i2c { + struct i2c_adapter adap; + struct device *dev; + bool initialized; + + void __iomem *base; + int irq; + struct clk *isfr_clk; + struct clk *iahb_clk; + + spinlock_t lock; + u8 stat; + struct completion cmp; + + u8 slave_reg; + bool is_regaddr; +}; + +static void hdmi_writeb(struct imx_hdmi_i2c *pd, unsigned int reg, u8 value) +{ + dev_dbg(pd->dev, "write: reg 0x%04x, val 0x%02x\n", reg, value); + writeb_relaxed(value, pd->base + reg); +} + +static u8 hdmi_readb(struct imx_hdmi_i2c *pd, unsigned int reg) +{ + u8 value; + + value = readb_relaxed(pd->base + reg); + dev_dbg(pd->dev, "read: reg 0x%04x, val 0x%02x\n", reg, value); + + return value; +} + +/* Initialize iMX6 HDMI DDC I2CM controller */ +static void imx_hdmi_hwinit(struct imx_hdmi_i2c *pd) +{ + unsigned long flags; + + spin_lock_irqsave(&pd->lock, flags); + + /* Set Fast Mode speed */ + hdmi_writeb(pd, HDMI_I2CM_DIV, 0x0b); + + /* Software reset */ + hdmi_writeb(pd, HDMI_I2CM_SOFTRSTZ, 0x00); + + /* Set done, not acknowledged and arbitration interrupt polarities */ + hdmi_writeb(pd, HDMI_I2CM_INT, HDMI_I2CM_INT_DONE_POL); + hdmi_writeb(pd, HDMI_I2CM_CTLINT, + HDMI_I2CM_CTLINT_NAC_POL | HDMI_I2CM_CTLINT_ARB_POL); + + /* Clear DONE and ERROR interrupts */ + hdmi_writeb(pd, HDMI_IH_I2CM_STAT0, + HDMI_IH_I2CM_STAT0_ERROR | HDMI_IH_I2CM_STAT0_DONE); + + /* Mute DONE and ERROR interrupts */ + hdmi_writeb(pd, HDMI_IH_MUTE_I2CM_STAT0, + HDMI_IH_I2CM_STAT0_ERROR | HDMI_IH_I2CM_STAT0_DONE); + + pd->initialized = true; + + spin_unlock_irqrestore(&pd->lock, flags); +} + +static irqreturn_t imx_hdmi_i2c_isr(int irq, void *dev) +{ + struct imx_hdmi_i2c *pd = dev; + unsigned long flags; + + spin_lock_irqsave(&pd->lock, flags); + + /* irq is shared, make sure driver probe is completed */ + if (!pd->initialized) { + spin_unlock_irqrestore(&pd->lock, flags); + return IRQ_NONE; + } + + pd->stat = hdmi_readb(pd, HDMI_IH_I2CM_STAT0); + if (!pd->stat) { + spin_unlock_irqrestore(&pd->lock, flags); + return IRQ_NONE; + } + + dev_dbg(pd->dev, "irq: 0x%02x, addr: 0x%02x, reg: 0x%02x\n", + pd->stat, hdmi_readb(pd, HDMI_I2CM_SLAVE), + hdmi_readb(pd, HDMI_I2CM_ADDRESS)); + + hdmi_writeb(pd, HDMI_IH_I2CM_STAT0, pd->stat); + complete(&pd->cmp); + + spin_unlock_irqrestore(&pd->lock, flags); + + return IRQ_HANDLED; +} + +static int xfer_read(struct imx_hdmi_i2c *pd, unsigned char *buf, int length) +{ + int stat; + unsigned long flags; + + spin_lock_irqsave(&pd->lock, flags); + + if (!pd->is_regaddr) { + dev_dbg(pd->dev, "set read register address to 0\n"); + pd->slave_reg = 0x00; + pd->is_regaddr = true; + } + + while (length--) { + hdmi_writeb(pd, HDMI_I2CM_ADDRESS, pd->slave_reg++); + hdmi_writeb(pd, HDMI_I2CM_OPERATION, HDMI_I2CM_OPERATION_READ); + pd->stat = 0; + + spin_unlock_irqrestore(&pd->lock, flags); + + stat = wait_for_completion_interruptible_timeout(&pd->cmp, + HZ / 10); + if (!stat) + return -ETIMEDOUT; + if (stat < 0) + return stat; + + spin_lock_irqsave(&pd->lock, flags); + + /* Check for error condition on the bus */ + if (pd->stat & HDMI_IH_I2CM_STAT0_ERROR) { + spin_unlock_irqrestore(&pd->lock, flags); + return -EIO; + } + + *buf++ = hdmi_readb(pd, HDMI_I2CM_DATAI); + } + + spin_unlock_irqrestore(&pd->lock, flags); + + return 0; +} + +static int xfer_write(struct imx_hdmi_i2c *pd, unsigned char *buf, int length) +{ + int stat; + unsigned long flags; + + spin_lock_irqsave(&pd->lock, flags); + + if (!pd->is_regaddr) { + if (length) { + /* Use the first write byte as register address */ + pd->slave_reg = buf[0]; + length--; + buf++; + } else { + dev_dbg(pd->dev, "set write register address to 0\n"); + pd->slave_reg = 0x00; + } + pd->is_regaddr = true; + } + + while (length--) { + hdmi_writeb(pd, HDMI_I2CM_DATAO, *buf++); + hdmi_writeb(pd, HDMI_I2CM_ADDRESS, pd->slave_reg++); + hdmi_writeb(pd, HDMI_I2CM_OPERATION, HDMI_I2CM_OPERATION_WRITE); + pd->stat = 0; + + spin_unlock_irqrestore(&pd->lock, flags); + + stat = wait_for_completion_interruptible_timeout(&pd->cmp, + HZ / 10); + if (!stat) + return -ETIMEDOUT; + if (stat < 0) + return stat; + + spin_lock_irqsave(&pd->lock, flags); + + /* Check for error condition on the bus */ + if (pd->stat & HDMI_IH_I2CM_STAT0_ERROR) { + spin_unlock_irqrestore(&pd->lock, flags); + return -EIO; + } + } + + spin_unlock_irqrestore(&pd->lock, flags); + + return 0; +} + +static int i2c_xfer(struct i2c_adapter *adap, struct i2c_msg *msgs, int num) +{ + struct imx_hdmi_i2c *pd = i2c_get_adapdata(adap); + + int i, ret; + u8 addr; + unsigned long flags; + + dev_dbg(pd->dev, "xfer: num: %d, addr: 0x%x\n", num, msgs[0].addr); + + spin_lock_irqsave(&pd->lock, flags); + + hdmi_writeb(pd, HDMI_IH_MUTE_I2CM_STAT0, 0x00); + + /* Set slave device address from the first transaction */ + addr = msgs[0].addr; + hdmi_writeb(pd, HDMI_I2CM_SLAVE, addr); + + /* Set slave device register address on transfer */ + pd->is_regaddr = false; + + spin_unlock_irqrestore(&pd->lock, flags); + + for (i = 0; i < num; i++) { + dev_dbg(pd->dev, "xfer: num: %d/%d, len: %d, flags: 0x%x\n", + i + 1, num, msgs[i].len, msgs[i].flags); + + if (msgs[i].addr != addr) { + dev_warn(pd->dev, + "unsupported transaction, changed slave address\n"); + ret = -EOPNOTSUPP; + break; + } + + if (msgs[i].len == 0) { + dev_dbg(pd->dev, + "unsupported transaction %d/%d, no data\n", + i + 1, num); + ret = -EOPNOTSUPP; + break; + } + + if (msgs[i].flags & I2C_M_RD) + ret = xfer_read(pd, msgs[i].buf, msgs[i].len); + else + ret = xfer_write(pd, msgs[i].buf, msgs[i].len); + + if (ret < 0) + break; + } + + if (!ret) + ret = num; + + spin_lock_irqsave(&pd->lock, flags); + + /* Mute DONE and ERROR interrupts */ + hdmi_writeb(pd, HDMI_IH_MUTE_I2CM_STAT0, + HDMI_IH_I2CM_STAT0_ERROR | HDMI_IH_I2CM_STAT0_DONE); + + spin_unlock_irqrestore(&pd->lock, flags); + + return ret; +} + +static u32 i2c_func(struct i2c_adapter *adapter) +{ + return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL; +} + +static struct i2c_algorithm imx_hdmi_algorithm = { + .master_xfer = i2c_xfer, + .functionality = i2c_func, +}; + +static int imx_hdmi_i2c_probe(struct platform_device *pdev) +{ + struct imx_hdmi_i2c *pd; + struct i2c_adapter *adap; + struct resource *res; + struct platform_device *hdmi_pdev; + int ret; + + pd = devm_kzalloc(&pdev->dev, sizeof(*pd), GFP_KERNEL); + if (!pd) + return -ENOMEM; + pd->dev = &pdev->dev; + + /* get platform resources from HDMI controller device */ + hdmi_pdev = to_platform_device(pdev->dev.parent); + if (!hdmi_pdev) + return -ENODEV; + + dev_dbg(pd->dev, "platform device: %s\n", hdmi_pdev->name); + + /* + * iMX6 HDMI controller memory region is shared among device drivers, + * therefore do not ioremap_resource() it exclusively. + */ + res = platform_get_resource(hdmi_pdev, IORESOURCE_MEM, 0); + if (!res) + return -ENODEV; + + pd->base = devm_ioremap(pd->dev, res->start, resource_size(res)); + if (IS_ERR(pd->base)) { + dev_err(pd->dev, "unable to ioremap: %ld\n", PTR_ERR(pd->base)); + return PTR_ERR(pd->base); + } + + pd->irq = platform_get_irq(hdmi_pdev, 0); + if (pd->irq < 0) { + dev_err(pd->dev, "no irq resource\n"); + return pd->irq; + } + + ret = devm_request_irq(pd->dev, pd->irq, imx_hdmi_i2c_isr, + IRQF_SHARED, "imx hdmi i2c", pd); + if (ret < 0) { + dev_err(pd->dev, "unable to request irq: %d\n", ret); + return ret; + } + + pd->isfr_clk = devm_clk_get(&hdmi_pdev->dev, "isfr"); + if (IS_ERR(pd->isfr_clk)) { + ret = PTR_ERR(pd->isfr_clk); + dev_err(pd->dev, "unable to get HDMI isfr clk: %d\n", ret); + return ret; + } + + ret = clk_prepare_enable(pd->isfr_clk); + if (ret) { + dev_err(pd->dev, "cannot enable HDMI isfr clock: %d\n", ret); + return ret; + } + + pd->iahb_clk = devm_clk_get(&hdmi_pdev->dev, "iahb"); + if (IS_ERR(pd->iahb_clk)) { + ret = PTR_ERR(pd->iahb_clk); + dev_err(pd->dev, "unable to get HDMI iahb clk: %d\n", ret); + goto err_isfr; + } + + ret = clk_prepare_enable(pd->iahb_clk); + if (ret) { + dev_err(pd->dev, "cannot enable HDMI iahb clock: %d\n", ret); + goto err_isfr; + } + + spin_lock_init(&pd->lock); + init_completion(&pd->cmp); + + adap = &pd->adap; + adap->owner = THIS_MODULE; + adap->algo = &imx_hdmi_algorithm; + adap->dev.parent = pd->dev; + adap->nr = pdev->id; + adap->dev.of_node = pdev->dev.of_node; + strlcpy(adap->name, pdev->name, sizeof(adap->name)); + i2c_set_adapdata(adap, pd); + + ret = i2c_add_numbered_adapter(adap); + if (ret) { + dev_err(pd->dev, "cannot add numbered adapter\n"); + goto err_register; + } + + platform_set_drvdata(pdev, pd); + + /* reset HDMI DDC I2C master controller */ + imx_hdmi_hwinit(pd); + + dev_info(pd->dev, "registered %s I2C bus driver\n", adap->name); + + return 0; + + err_register: + clk_disable_unprepare(pd->iahb_clk); + err_isfr: + clk_disable_unprepare(pd->isfr_clk); + + return ret; +} + +static int imx_hdmi_i2c_remove(struct platform_device *pdev) +{ + struct imx_hdmi_i2c *pd = platform_get_drvdata(pdev); + + dev_dbg(pd->dev, "deregistering %s I2C bus driver\n", pd->adap.name); + + /* deregister irq handler early */ + devm_free_irq(pd->dev, pd->irq, pd); + + pd->initialized = false; + platform_set_drvdata(pdev, NULL); + + clk_disable_unprepare(pd->iahb_clk); + clk_disable_unprepare(pd->isfr_clk); + + i2c_del_adapter(&pd->adap); + + return 0; +} + +static const struct of_device_id imx_hdmi_ddc_dt_ids[] = { + { .compatible = "fsl,imx6q-hdmi-ddc", }, + { }, +}; +MODULE_DEVICE_TABLE(of, imx_hdmi_ddc_dt_ids); + +static struct platform_driver imx_hdmi_i2c_driver = { + .driver = { + .name = "imx_hdmi_i2c", + .owner = THIS_MODULE, + .of_match_table = imx_hdmi_ddc_dt_ids, + }, + .probe = imx_hdmi_i2c_probe, + .remove = imx_hdmi_i2c_remove, +}; + +static int __init imx_hdmi_i2c_init(void) +{ + return platform_driver_register(&imx_hdmi_i2c_driver); +} + +static void __exit imx_hdmi_i2c_exit(void) +{ + platform_driver_unregister(&imx_hdmi_i2c_driver); +} + +subsys_initcall(imx_hdmi_i2c_init); +module_exit(imx_hdmi_i2c_exit); + +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("iMX6 HDMI DDC I2C master driver"); +MODULE_AUTHOR("Vladimir Zapolskiy <vladimir_zapolskiy@xxxxxxxxxx>"); -- 1.7.10.4 -- To unsubscribe from this list: send the line "unsubscribe linux-i2c" in the body of a message to majordomo@xxxxxxxxxxxxxxx More majordomo info at http://vger.kernel.org/majordomo-info.html