/*
 * This file is subject to the terms and conditions of the GNU General Public
 * License.  See the file "COPYING" in the main directory of this archive
 * for more details.
 *
 * Copyright (C) 2012 MIPS Technologies, Inc.  All rights reserved.
 */
#include <linux/delay.h>
#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/spinlock.h>
#include <linux/platform_device.h>
#include <linux/init.h>
#include <linux/errno.h>
#include <linux/i2c.h>
#include <linux/slab.h>

#define PIC32_I2CxCON		0x0000
#define PIC32_I2CxCONCLR	0x0004
#define PIC32_I2CxCONSET	0x0008
#define PIC32_I2CxCONINV	0x000C
#define  I2CCON_ON		(1<<15)
#define  I2CCON_FRZ		(1<<14)
#define  I2CCON_SIDL		(1<<13)
#define  I2CCON_SCLREL		(1<<12)
#define  I2CCON_STRICT		(1<<11)
#define  I2CCON_A10M		(1<<10)
#define  I2CCON_DISSLW		(1<<9)
#define  I2CCON_SMEN		(1<<8)
#define  I2CCON_GCEN		(1<<7)
#define  I2CCON_STREN		(1<<6)
#define  I2CCON_ACKDT		(1<<5)
#define  I2CCON_ACKEN		(1<<4)
#define  I2CCON_RCEN		(1<<3)
#define  I2CCON_PEN		(1<<2)
#define  I2CCON_RSEN		(1<<1)
#define  I2CCON_SEN		(1<<0)

#define PIC32_I2CxSTAT		0x0010
#define PIC32_I2CxSTATCLR	0x0014
#define PIC32_I2CxSTATSET	0x0018
#define PIC32_I2CxSTATINV	0x001C
#define  I2CSTAT_ACKSTAT	(1<<15)
#define  I2CSTAT_TRSTAT		(1<<14)
#define  I2CSTAT_BCL		(1<<10)
#define  I2CSTAT_GCSTAT		(1<<9)
#define  I2CSTAT_ADD10		(1<<8)
#define  I2CSTAT_IWCOL		(1<<7)
#define  I2CSTAT_I2COV		(1<<6)
#define  I2CSTAT_DA		(1<<5)
#define  I2CSTAT_P		(1<<4)
#define  I2CSTAT_S		(1<<3)
#define  I2CSTAT_RW		(1<<2)
#define  I2CSTAT_RBF		(1<<1)
#define  I2CSTAT_TBF		(1<<0)

#define PIC32_I2CxADD		0x0020
#define PIC32_I2CxADDCLR	0x0024
#define PIC32_I2CxADDSET	0x0028
#define PIC32_I2CxADDINV	0x002C
#define PIC32_I2CxMSK		0x0030
#define PIC32_I2CxMSKCLR	0x0034
#define PIC32_I2CxMSKSET	0x0038
#define PIC32_I2CxMSKINV	0x003C
#define PIC32_I2CxBRG		0x0040
#define PIC32_I2CxBRGCLR	0x0044
#define PIC32_I2CxBRGSET	0x0048
#define PIC32_I2CxBRGINV	0x004C
#define PIC32_I2CxTRN		0x0050
#define PIC32_I2CxTRNCLR	0x0054
#define PIC32_I2CxTRNSET	0x0058
#define PIC32_I2CxTRNINV	0x005C
#define PIC32_I2CxRCV		0x0060

struct i2c_platform_data {
	u32	base;
	struct i2c_adapter adap;
	u32	xfer_timeout;
	u32	ack_timeout;
	u32	ctl_timeout;
};

extern u32 pic32_bus_readl(u32 reg);
extern void pic32_bus_writel(u32 val, u32 reg);

static inline void
StartI2C(struct i2c_platform_data *adap)
{
	pr_debug("StartI2C\n");
	pic32_bus_writel(I2CCON_SEN, adap->base + PIC32_I2CxCONSET);
}

static inline void
StopI2C(struct i2c_platform_data *adap)
{
	pr_debug("StopI2C\n");
	pic32_bus_writel(I2CCON_PEN, adap->base + PIC32_I2CxCONSET);
}

static inline void
AckI2C(struct i2c_platform_data *adap)
{
	pr_debug("AckI2C\n");
	pic32_bus_writel(I2CCON_ACKDT, adap->base + PIC32_I2CxCONCLR);
	pic32_bus_writel(I2CCON_ACKEN, adap->base + PIC32_I2CxCONSET);
}

static inline void
NotAckI2C(struct i2c_platform_data *adap)
{
	pr_debug("NakI2C\n");
	pic32_bus_writel(I2CCON_ACKDT, adap->base + PIC32_I2CxCONSET);
	pic32_bus_writel(I2CCON_ACKEN, adap->base + PIC32_I2CxCONSET);
}

static inline int
IdleI2C(struct i2c_platform_data *adap)
{
	int i;

	pr_debug("IdleI2C\n");
	for (i = 0; i < adap->ctl_timeout; i++) {
		if (((pic32_bus_readl(adap->base + PIC32_I2CxCON) &
		     (I2CCON_ACKEN | I2CCON_RCEN | I2CCON_PEN | I2CCON_RSEN |
		      I2CCON_SEN)) == 0) &&
		    ((pic32_bus_readl(adap->base + PIC32_I2CxSTAT) &
		     (I2CSTAT_TRSTAT)) == 0))
			return 0;
		udelay(1);
	}
	return -ETIMEDOUT;
}

static inline u32
MasterWriteI2C(struct i2c_platform_data *adap, u32 byte)
{
	pr_debug("MasterWriteI2C\n");

	pic32_bus_writel(byte, adap->base + PIC32_I2CxTRN);

	return pic32_bus_readl(adap->base + PIC32_I2CxSTAT) & I2CSTAT_IWCOL;
}

static inline u32
MasterReadI2C(struct i2c_platform_data *adap)
{
	pr_debug("MasterReadI2C\n");

	pic32_bus_writel(I2CCON_RCEN, adap->base + PIC32_I2CxCONSET);

	while (pic32_bus_readl(adap->base + PIC32_I2CxCON) & I2CCON_RCEN)
		;

	pic32_bus_writel(I2CSTAT_I2COV, adap->base + PIC32_I2CxSTATCLR);

	return pic32_bus_readl(adap->base + PIC32_I2CxRCV);
}

static int
do_address(struct i2c_platform_data *adap, unsigned int addr, int rd)
{
	pr_debug("doaddress\n");

	IdleI2C(adap);
	StartI2C(adap);
	IdleI2C(adap);

	addr <<= 1;
	if (rd)
		addr |= 1;

	if (MasterWriteI2C(adap, addr))
		return -EIO;
	IdleI2C(adap);
	if (pic32_bus_readl(adap->base + PIC32_I2CxSTAT) & I2CSTAT_ACKSTAT)
		return -EIO;
	return 0;
}

static int
i2c_read(struct i2c_platform_data *adap, unsigned char *buf,
		    unsigned int len)
{
	int	i;
	u32	data;

	pr_debug("i2c_read\n");

	i = 0;
	while (i < len) {
		data = MasterReadI2C(adap);
		buf[i++] = data;
		if (i < len)
			AckI2C(adap);
		else
			NotAckI2C(adap);
	}

	StopI2C(adap);
	IdleI2C(adap);
	return 0;
}

static int
i2c_write(struct i2c_platform_data *adap, unsigned char *buf,
		     unsigned int len)
{
	int	i;
	u32	data;

	pr_debug("i2c_write\n");

	i = 0;
	while (i < len) {
		data = buf[i];
		if (MasterWriteI2C(adap, data))
			return -EIO;
		IdleI2C(adap);
		if (pic32_bus_readl(adap->base + PIC32_I2CxSTAT) &
		    I2CSTAT_ACKSTAT)
			return -EIO;
		i++;
	}

	StopI2C(adap);
	IdleI2C(adap);
	return 0;
}

static int
platform_xfer(struct i2c_adapter *i2c_adap, struct i2c_msg *msgs, int num)
{
	struct i2c_platform_data *adap = i2c_adap->algo_data;
	struct i2c_msg *p;
	int i, err = 0;

	pr_debug("platform_xfer\n");
	for (i = 0; i < num; i++) {
#define __BUFSIZE 80
		int ii;
		static char buf[__BUFSIZE];
		char *b = buf;

		p = &msgs[i];
		b += sprintf(buf, " [%d bytes]", p->len);
		if ((p->flags & I2C_M_RD) == 0) {
			for (ii = 0; ii < p->len; ii++) {
				if (b < &buf[__BUFSIZE-4]) {
					b += sprintf(b, " %02x", p->buf[ii]);
				} else {
					strcat(b, "...");
					break;
				}
			}
		}
		pr_debug("xfer%d: DevAddr: %04x Op:%s Data:%s\n", i, p->addr,
			 (p->flags & I2C_M_RD) ? "Rd" : "Wr", buf);
	}


	for (i = 0; !err && i < num; i++) {
		p = &msgs[i];
		err = do_address(adap, p->addr, p->flags & I2C_M_RD);
		if (err || !p->len)
			continue;
		if (p->flags & I2C_M_RD)
			err = i2c_read(adap, p->buf, p->len);
		else
			err = i2c_write(adap, p->buf, p->len);
	}

	/* Return the number of messages processed, or the error code. */
	if (err == 0)
		err = num;

	return err;
}

static u32
platform_func(struct i2c_adapter *adap)
{
	pr_debug("platform_algo\n");
	return I2C_FUNC_I2C | I2C_FUNC_SMBUS_EMUL;
}

static const struct i2c_algorithm platform_algo = {
	.master_xfer	= platform_xfer,
	.functionality	= platform_func,
};

static void i2c_platform_setup(struct i2c_platform_data *priv)
{
	pr_debug("i2c_platform_setup\n");

	pic32_bus_writel(500, priv->base + PIC32_I2CxBRG);
	pic32_bus_writel(I2CCON_ON, priv->base + PIC32_I2CxCONCLR);
	pic32_bus_writel(I2CCON_ON, priv->base + PIC32_I2CxCONSET);
	pic32_bus_writel((I2CSTAT_BCL | I2CSTAT_IWCOL),
		(priv->base + PIC32_I2CxSTATCLR));
}

static void i2c_platform_disable(struct i2c_platform_data *priv)
{
	pr_debug("i2c_platform_disable\n");
}

static int i2c_platform_probe(struct platform_device *pdev)
{
	struct i2c_platform_data *priv;
	struct resource *r;
	int ret;

	pr_debug("i2c_platform_probe\n");
	r = platform_get_resource(pdev, IORESOURCE_MEM, 0);
	if (!r) {
		ret = -ENODEV;
		goto out;
	}

	priv = kzalloc(sizeof(struct i2c_platform_data), GFP_KERNEL);
	if (!priv) {
		ret = -ENOMEM;
		goto out;
	}

	/* FIXME: need to allocate resource in PIC32 space */
#if 0
	priv->base = bus_request_region(r->start, resource_size(r),
					  pdev->name);
#else
	priv->base = r->start;
#endif
	if (!priv->base) {
		ret = -EBUSY;
		goto out_mem;
	}

	priv->xfer_timeout = 200;
	priv->ack_timeout = 200;
	priv->ctl_timeout = 200;

	priv->adap.nr = pdev->id;
	priv->adap.algo = &platform_algo;
	priv->adap.algo_data = priv;
	priv->adap.dev.parent = &pdev->dev;
	strlcpy(priv->adap.name, "PIC32 I2C", sizeof(priv->adap.name));

	i2c_platform_setup(priv);

	ret = i2c_add_numbered_adapter(&priv->adap);
	if (ret == 0) {
		platform_set_drvdata(pdev, priv);
		return 0;
	}

	i2c_platform_disable(priv);

out_mem:
	kfree(priv);
out:
	return ret;
}

static int i2c_platform_remove(struct platform_device *pdev)
{
	struct i2c_platform_data *priv = platform_get_drvdata(pdev);

	pr_debug("i2c_platform_remove\n");
	platform_set_drvdata(pdev, NULL);
	i2c_del_adapter(&priv->adap);
	i2c_platform_disable(priv);
	kfree(priv);
	return 0;
}

#ifdef CONFIG_PM
static int
i2c_platform_suspend(struct platform_device *pdev, pm_message_t state)
{
	struct i2c_platform_data *priv = platform_get_drvdata(pdev);

	dev_dbg(&pdev->dev, "i2c_platform_disable\n");
	i2c_platform_disable(priv);

	return 0;
}

static int
i2c_platform_resume(struct platform_device *pdev)
{
	struct i2c_platform_data *priv = platform_get_drvdata(pdev);

	dev_dbg(&pdev->dev, "i2c_platform_setup\n");
	i2c_platform_setup(priv);

	return 0;
}
#else
#define i2c_platform_suspend	NULL
#define i2c_platform_resume	NULL
#endif

static struct platform_driver i2c_platform_driver = {
	.driver = {
		.name	= "i2c_pic32",
		.owner	= THIS_MODULE,
	},
	.probe		= i2c_platform_probe,
	.remove		= i2c_platform_remove,
	.suspend	= i2c_platform_suspend,
	.resume		= i2c_platform_resume,
};

static int __init
i2c_platform_init(void)
{
	pr_debug("i2c_platform_init\n");
	return platform_driver_register(&i2c_platform_driver);
}

static void __exit
i2c_platform_exit(void)
{
	pr_debug("i2c_platform_exit\n");
	platform_driver_unregister(&i2c_platform_driver);
}

MODULE_AUTHOR("Chris Dearman, MIPS Technologies INC.");
MODULE_DESCRIPTION("PIC32 I2C driver");
MODULE_LICENSE("GPL");

module_init(i2c_platform_init);
module_exit(i2c_platform_exit);
