/*
 * (C) Copyright 2001
 * Wolfgang Denk, DENX Software Engineering, wd@denx.de.
 *
 * See file CREDITS for list of people who contributed to this
 * project.
 *
 * 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 <ppcboot.h>
#include <mpc8xx.h>
#include <i2c.h>

#if defined(CONFIG_SOFT_I2C)

/*-----------------------------------------------------------------------
 * Set default values
 */
#ifndef	CFG_I2C_SPEED
#define	CFG_I2C_SPEED	50000
#endif

#ifndef	CFG_I2C_SLAVE
#define	CFG_I2C_SLAVE	0xFE
#endif

/*-----------------------------------------------------------------------
 * Definitions
 */

#define	PB_SCL		0x00000020	/* PB 26 */
#define PB_SDA		0x00000010	/* PB 27 */
#define I2C_ACK		0		/* PB_SDA level to ack a byte */
#define I2C_NOACK	1		/* PB_SDA level to noack a byte */

#define	SET_PB_BIT(bit)	do {						\
			immr->im_cpm.cp_pbdir |=  bit;	/* output */	\
			immr->im_cpm.cp_pbdat |=  bit;	/* set  1 */	\
			udelay (5);					\
		} while (0)

#define RESET_PB_BIT(bit) do {						\
			immr->im_cpm.cp_pbdir |=  bit;	/* output */	\
			immr->im_cpm.cp_pbdat &= ~bit;	/* set  0 */	\
			udelay (5);					\
		} while (0)

#define RESET_PB_BIT1(bit) do {						\
			immr->im_cpm.cp_pbdir |=  bit;	/* output */	\
			immr->im_cpm.cp_pbdat &= ~bit;	/* set  0 */	\
			udelay (1);					\
		} while (0)

/*-----------------------------------------------------------------------
 * Functions
 */
static void send_start	(void);
static void send_stop	(void);
static void send_ack	(int);
static void rcv_ack	(void);
static void send_data_1	(void);
static void send_data_0	(void);
static void read_addr	(uchar addr);
static void write_addr	(uchar addr);
static int  read_bit	(void);
static uchar read_byte	(int);
static void write_byte	(uchar byte);

/*-----------------------------------------------------------------------
 * START: High -> Low on SDA while SCL is High
 */
static void send_start (void)
{
	volatile immap_t *immr = (immap_t *)CFG_IMMR;

	SET_PB_BIT   (PB_SCL);
	RESET_PB_BIT (PB_SDA);
}

/*-----------------------------------------------------------------------
 * STOP: Low -> High on SDA while SCL is High
 */
static void send_stop (void)
{
	volatile immap_t *immr = (immap_t *)CFG_IMMR;

	RESET_PB_BIT (PB_SDA);
	SET_PB_BIT   (PB_SCL);
	SET_PB_BIT   (PB_SDA);
	udelay (5);
}

/*-----------------------------------------------------------------------
 * ack should be I2C_ACK or I2C_NOACK
 */
static void send_ack (int ack)
{
	volatile immap_t *immr = (immap_t *)CFG_IMMR;

	RESET_PB_BIT (PB_SCL);
	if (ack == I2C_ACK)
		RESET_PB_BIT (PB_SDA);
	else
		SET_PB_BIT   (PB_SDA);
	SET_PB_BIT   (PB_SCL);
	udelay (5);
	RESET_PB_BIT (PB_SCL);
}

/*-----------------------------------------------------------------------
 */
static void rcv_ack (void)
{
	volatile immap_t *immr = (immap_t *)CFG_IMMR;

	RESET_PB_BIT (PB_SCL);

	immr->im_cpm.cp_pbdir &= ~PB_SDA;	/* input */
	udelay (10);
	while (immr->im_cpm.cp_pbdat & PB_SDA)
		;
	udelay (5);
	SET_PB_BIT   (PB_SCL);
	RESET_PB_BIT (PB_SCL);
	SET_PB_BIT   (PB_SDA);
}

/*-----------------------------------------------------------------------
 */
static void send_data_1 (void)
{
	volatile immap_t *immr = (immap_t *)CFG_IMMR;

	RESET_PB_BIT1(PB_SCL);
	SET_PB_BIT   (PB_SDA);
	SET_PB_BIT   (PB_SCL);
}

/*-----------------------------------------------------------------------
 */
static void send_data_0 (void)
{
	volatile immap_t *immr = (immap_t *)CFG_IMMR;

	RESET_PB_BIT1(PB_SCL);
	RESET_PB_BIT (PB_SDA);
	SET_PB_BIT   (PB_SCL);
}


/*-----------------------------------------------------------------------
 */
static void read_addr (uchar addr)
{
	int i;

	addr = (addr << 1) | 0x01;

	for (i=0; i<8; ++i) {
		if (addr & 0x80) {
			send_data_1 ();
		} else {
			send_data_0 ();
		}
		addr <<= 1;
	}

	rcv_ack ();
}

/*-----------------------------------------------------------------------
 * addr & 0xF0 -> Device Identifier
 * addr & 0x0E -> bank_num or device address << 1
 * addr & 0x01 -> 1 = read, 0 = write
 */
static void write_addr (uchar addr)
{
	volatile immap_t *immr = (immap_t *)CFG_IMMR;

	addr <<= 1;

	for (;;) {
		uchar a = addr;
		uint i;

		send_start ();

		for (i=0; i<8; ++i) {
			if (a & 0x80) {
				send_data_1 ();
			} else {
				send_data_0 ();
			}
			a <<= 1;
		}

		RESET_PB_BIT (PB_SCL);

		immr->im_cpm.cp_pbdir &= ~PB_SDA;	/* input */
		udelay (10);

		i = immr->im_cpm.cp_pbdat;
		udelay (5);
		SET_PB_BIT   (PB_SCL);
		RESET_PB_BIT (PB_SCL);
		SET_PB_BIT   (PB_SDA);

		if ((i & PB_SDA) == 0)
			break;

		send_stop();
	}
}

/*-----------------------------------------------------------------------
 */
static int read_bit (void)
{
	volatile immap_t *immr = (immap_t *)CFG_IMMR;
	int bit;

	RESET_PB_BIT (PB_SCL);
	SET_PB_BIT   (PB_SCL);
	bit = (immr->im_cpm.cp_pbdat & PB_SDA) ? 1 : 0;
	return (bit);
}

/*-----------------------------------------------------------------------
 * if last == 0, ACK the byte so can continue reading
 * else NO ACK to end the read
 */
static uchar read_byte (int last)
{
	volatile immap_t *immr = (immap_t *)CFG_IMMR;
	uchar byte = 0;
	int i;

	immr->im_cpm.cp_pbdir &= ~PB_SDA;		/* input */
	udelay (5);

	for (i=0; i<8; ++i) {
		byte = (byte << 1) | read_bit();
	}
	send_ack (last ? I2C_NOACK : I2C_ACK);

	return byte;
}

/*-----------------------------------------------------------------------
 */
static void write_byte (uchar byte)
{
	int i;
	for (i=0; i<8; ++i) {
		if (byte & 0x80) {
			send_data_1 ();
		} else {
			send_data_0 ();
		}
		byte <<= 1;
	}
	rcv_ack();
}

/*=====================================================================*/
/*=====================================================================*/

/*-----------------------------------------------------------------------
 * `speed' and `slaveaddr' not implemented
 */
void i2c_init (int speed, int slaveaddr)
{
	volatile immap_t *immr = (immap_t *)CFG_IMMR;

	immr->im_cpm.cp_pbpar &= ~(PB_SCL | PB_SDA);	/* GPIO */
	immr->im_cpm.cp_pbodr |=  (PB_SCL | PB_SDA);	/* Open Drain Output */

	RESET_PB_BIT (PB_SCL);
	RESET_PB_BIT (PB_SDA);
	SET_PB_BIT   (PB_SCL);
	SET_PB_BIT   (PB_SDA);		/* stop condition */
	udelay (25);
}

/*-----------------------------------------------------------------------
 *
 * addr:	Array of address bytes; for instance, to read from
 *              EEPROM with 8 bit page addresses you have to write 2
 *              address bytes: the block number, and the block
 *              offset.
 * alen:	Number of address bytes in "addr"
 * buffer:	where to store the read data in
 * len:		number of bytes to read
 *              Warning: it is expected that the device is really
 *              capable of reading `len' sequential bytes; no
 *              checking is done here
 */
int i2c_read (uchar *addr, int alen, uchar *buffer, int len)
{
	uchar *ap = addr;

	write_addr (*ap);
	while (--alen > 0) {
		write_byte (*++ap);
	}
	send_start ();

	read_addr  (*addr);
	while (len-- > 0) {
		*buffer++ = read_byte (len==0);
	}
	send_stop  ();
	return (0);	/* No error handling yet */
}

int i2c_write (uchar *addr, int alen, uchar *buffer, int len)
{
	uchar *ap = addr;

	write_addr (*ap);
	while (--alen > 0) {
		write_byte (*++ap);
	}

	while (len-- > 0) {
		write_byte (*buffer++);
	}
	send_stop  ();
	return (0);	/* No error handling yet */
}

uchar i2c_reg_read (uchar i2c_addr, uchar reg)
{
	char addr[2];
	char buf [1];

	i2c_init (CFG_I2C_SPEED, CFG_I2C_SLAVE);

	addr[0] = i2c_addr;
	addr[1] = reg;

	i2c_read (addr, 2, buf, 1);

	return (buf[0]);
}

void i2c_reg_write (uchar i2c_addr, uchar reg, uchar val)
{
	char addr[2];

	i2c_init (CFG_I2C_SPEED, CFG_I2C_SLAVE);

	addr[0] = i2c_addr;
	addr[1] = reg;

	i2c_write (addr, 2, &val, 1);
}

#endif	/* CONFIG_SOFT_I2C */
/*-----------------------------------------------------------------------
 */
