/*
    amd756.c - Part of lm_sensors, Linux kernel modules for hardware
              monitoring

    Copyright (c) 1999-2002 Merlin Hughes <merlin@merlin.org>

    Shamelessly ripped from i2c-piix4.c:

    Copyright (c) 1998, 1999  Frodo Looijaard <frodol@dds.nl> and
    Philip Edelbrock <phil@netroedge.com>

    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., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

/*
    2002-04-08: Added nForce support. (Csaba Halasz)
    2002-10-03: Fixed nForce PnP I/O port. (Michael Steil)
    2002-12-28: Rewritten into something that resembles a Linux driver (hch)
    2003-11-29: Added back AMD8111 removed by the previous rewrite.
                (Philip Pokorny)
    2004-02-15: Don't register driver to avoid driver conflicts.
                (Daniel Rune Jensen)
*/

/*
   Supports AMD756, AMD766, AMD768, AMD8111 and nVidia nForce
   Note: we assume there can only be one device, with one SMBus interface.
*/

#include <linux/module.h>
#include <linux/pci.h>
#include <linux/kernel.h>
#include <linux/stddef.h>
#include <linux/sched.h>
#include <linux/ioport.h>
#include <linux/i2c.h>
#include <linux/init.h>
#include <asm/io.h>
#include "version.h"

#define DRV_NAME	"i2c-amd756"

/* AMD756 SMBus address offsets */
#define SMB_ADDR_OFFSET        0xE0
#define SMB_IOSIZE             16
#define SMB_GLOBAL_STATUS      (0x0 + amd756_ioport)
#define SMB_GLOBAL_ENABLE      (0x2 + amd756_ioport)
#define SMB_HOST_ADDRESS       (0x4 + amd756_ioport)
#define SMB_HOST_DATA          (0x6 + amd756_ioport)
#define SMB_HOST_COMMAND       (0x8 + amd756_ioport)
#define SMB_HOST_BLOCK_DATA    (0x9 + amd756_ioport)
#define SMB_HAS_DATA           (0xA + amd756_ioport)
#define SMB_HAS_DEVICE_ADDRESS (0xC + amd756_ioport)
#define SMB_HAS_HOST_ADDRESS   (0xE + amd756_ioport)
#define SMB_SNOOP_ADDRESS      (0xF + amd756_ioport)

/* PCI Address Constants */

/* address of I/O space */
#define SMBBA     0x058		/* mh */
#define SMBBANFORCE     0x014

/* general configuration */
#define SMBGCFG   0x041		/* mh */

/* silicon revision code */
#define SMBREV    0x008

/* Other settings */
#define MAX_TIMEOUT 500

/* AMD756 constants */
#define AMD756_QUICK        0x00
#define AMD756_BYTE         0x01
#define AMD756_BYTE_DATA    0x02
#define AMD756_WORD_DATA    0x03
#define AMD756_PROCESS_CALL 0x04
#define AMD756_BLOCK_DATA   0x05


static unsigned short amd756_ioport = 0;

/* 
  SMBUS event = I/O 28-29 bit 11
     see E0 for the status bits and enabled in E2
     
*/

#define GS_ABRT_STS (1 << 0)
#define GS_COL_STS (1 << 1)
#define GS_PRERR_STS (1 << 2)
#define GS_HST_STS (1 << 3)
#define GS_HCYC_STS (1 << 4)
#define GS_TO_STS (1 << 5)
#define GS_SMB_STS (1 << 11)

#define GS_CLEAR_STS (GS_ABRT_STS | GS_COL_STS | GS_PRERR_STS | \
  GS_HCYC_STS | GS_TO_STS )

#define GE_CYC_TYPE_MASK (7)
#define GE_HOST_STC (1 << 3)
#define GE_ABORT (1 << 5)


static int amd756_transaction(void)
{
	int temp;
	int result = 0;
	int timeout = 0;

	pr_debug(DRV_NAME
	       ": Transaction (pre): GS=%04x, GE=%04x, ADD=%04x, DAT=%04x\n",
	       inw_p(SMB_GLOBAL_STATUS), inw_p(SMB_GLOBAL_ENABLE),
	       inw_p(SMB_HOST_ADDRESS), inb_p(SMB_HOST_DATA));

	/* Make sure the SMBus host is ready to start transmitting */
	if ((temp = inw_p(SMB_GLOBAL_STATUS)) & (GS_HST_STS | GS_SMB_STS)) {
		pr_debug(DRV_NAME ": SMBus busy (%04x). Waiting...\n", temp);
		do {
			i2c_delay(1);
			temp = inw_p(SMB_GLOBAL_STATUS);
		} while ((temp & (GS_HST_STS | GS_SMB_STS)) &&
		         (timeout++ < MAX_TIMEOUT));
		/* If the SMBus is still busy, we give up */
		if (timeout >= MAX_TIMEOUT) {
			pr_debug(DRV_NAME ": Busy wait timeout (%04x)\n", temp);
			goto abort;
		}
		timeout = 0;
	}

	/* start the transaction by setting the start bit */
	outw_p(inw(SMB_GLOBAL_ENABLE) | GE_HOST_STC, SMB_GLOBAL_ENABLE);

	/* We will always wait for a fraction of a second! */
	do {
		i2c_delay(1);
		temp = inw_p(SMB_GLOBAL_STATUS);
	} while ((temp & GS_HST_STS) && (timeout++ < MAX_TIMEOUT));

	/* If the SMBus is still busy, we give up */
	if (timeout >= MAX_TIMEOUT) {
		pr_debug(DRV_NAME ": Completion timeout!\n");
		goto abort;
	}

	if (temp & GS_PRERR_STS) {
		result = -1;
		pr_debug(DRV_NAME ": SMBus Protocol error (no response)!\n");
	}

	if (temp & GS_COL_STS) {
		result = -1;
		printk(KERN_WARNING DRV_NAME ": SMBus collision!\n");
	}

	if (temp & GS_TO_STS) {
		result = -1;
		pr_debug(DRV_NAME ": SMBus protocol timeout!\n");
	}

	if (temp & GS_HCYC_STS)
		pr_debug(DRV_NAME ": SMBus protocol success!\n");

	outw_p(GS_CLEAR_STS, SMB_GLOBAL_STATUS);

#ifdef DEBUG
	if (((temp = inw_p(SMB_GLOBAL_STATUS)) & GS_CLEAR_STS) != 0x00) {
		pr_debug(DRV_NAME
		         ": Failed reset at end of transaction (%04x)\n", temp);
	}

	pr_debug(DRV_NAME
		 ": Transaction (post): GS=%04x, GE=%04x, ADD=%04x, DAT=%04x\n",
		 inw_p(SMB_GLOBAL_STATUS), inw_p(SMB_GLOBAL_ENABLE),
		 inw_p(SMB_HOST_ADDRESS), inb_p(SMB_HOST_DATA));
#endif

	return result;

 abort:
	printk(KERN_WARNING DRV_NAME ": Sending abort.\n");
	outw_p(inw(SMB_GLOBAL_ENABLE) | GE_ABORT, SMB_GLOBAL_ENABLE);
	i2c_delay(100);
	outw_p(GS_CLEAR_STS, SMB_GLOBAL_STATUS);
	return -1;
}

/* Return -1 on error. */

static s32 amd756_access(struct i2c_adapter * adap, u16 addr,
		  unsigned short flags, char read_write,
		  u8 command, int size, union i2c_smbus_data * data)
{
	int i, len;

	/** TODO: Should I supporte the 10-bit transfers? */
	switch (size) {
	/* TODO: proc call is supported, I'm just not sure what to do here... */
	case I2C_SMBUS_QUICK:
		outw_p(((addr & 0x7f) << 1) | (read_write & 0x01),
		       SMB_HOST_ADDRESS);
		size = AMD756_QUICK;
		break;
	case I2C_SMBUS_BYTE:
		outw_p(((addr & 0x7f) << 1) | (read_write & 0x01),
		       SMB_HOST_ADDRESS);
		if (read_write == I2C_SMBUS_WRITE)
			outb_p(command, SMB_HOST_DATA);
		size = AMD756_BYTE;
		break;
	case I2C_SMBUS_BYTE_DATA:
		outw_p(((addr & 0x7f) << 1) | (read_write & 0x01),
		       SMB_HOST_ADDRESS);
		outb_p(command, SMB_HOST_COMMAND);
		if (read_write == I2C_SMBUS_WRITE)
			outw_p(data->byte, SMB_HOST_DATA);
		size = AMD756_BYTE_DATA;
		break;
	case I2C_SMBUS_WORD_DATA:
		outw_p(((addr & 0x7f) << 1) | (read_write & 0x01),
		       SMB_HOST_ADDRESS);
		outb_p(command, SMB_HOST_COMMAND);
		if (read_write == I2C_SMBUS_WRITE)
			outw_p(data->word, SMB_HOST_DATA);	/* TODO: endian???? */
		size = AMD756_WORD_DATA;
		break;
	case I2C_SMBUS_BLOCK_DATA:
		outw_p(((addr & 0x7f) << 1) | (read_write & 0x01),
		       SMB_HOST_ADDRESS);
		outb_p(command, SMB_HOST_COMMAND);
		if (read_write == I2C_SMBUS_WRITE) {
			len = data->block[0];
			if (len < 0)
				len = 0;
			if (len > 32)
				len = 32;
			outw_p(len, SMB_HOST_DATA);
			/* i = inw_p(SMBHSTCNT); Reset SMBBLKDAT */
			for (i = 1; i <= len; i++)
				outb_p(data->block[i],
				       SMB_HOST_BLOCK_DATA);
		}
		size = AMD756_BLOCK_DATA;
		break;
	default:
		printk
		    (KERN_WARNING "i2c-amd756.o: Unsupported transaction %d\n", size);
		return -1;
	}

	/* How about enabling interrupts... */
	outw_p(size & GE_CYC_TYPE_MASK, SMB_GLOBAL_ENABLE);

	if (amd756_transaction())	/* Error in transaction */
		return -1;

	if ((read_write == I2C_SMBUS_WRITE) || (size == AMD756_QUICK))
		return 0;


	switch (size) {
	case AMD756_BYTE:
		data->byte = inw_p(SMB_HOST_DATA);
		break;
	case AMD756_BYTE_DATA:
		data->byte = inw_p(SMB_HOST_DATA);
		break;
	case AMD756_WORD_DATA:
		data->word = inw_p(SMB_HOST_DATA);	/* TODO: endian???? */
		break;
	case AMD756_BLOCK_DATA:
		data->block[0] = inw_p(SMB_HOST_DATA) & 0x3f;
		if(data->block[0] > 32)
			data->block[0] = 32;
		/* i = inw_p(SMBHSTCNT); Reset SMBBLKDAT */
		for (i = 1; i <= data->block[0]; i++)
			data->block[i] = inb_p(SMB_HOST_BLOCK_DATA);
		break;
	}

	return 0;
}

static void amd756_inc(struct i2c_adapter *adapter)
{
#ifdef MODULE
	MOD_INC_USE_COUNT;
#endif
}

static void amd756_dec(struct i2c_adapter *adapter)
{
#ifdef MODULE
	MOD_DEC_USE_COUNT;
#endif
}

static u32 amd756_func(struct i2c_adapter *adapter)
{
	return I2C_FUNC_SMBUS_QUICK | I2C_FUNC_SMBUS_BYTE |
	    I2C_FUNC_SMBUS_BYTE_DATA | I2C_FUNC_SMBUS_WORD_DATA |
	    I2C_FUNC_SMBUS_BLOCK_DATA | I2C_FUNC_SMBUS_PROC_CALL;
}

static struct i2c_algorithm smbus_algorithm = {
	.name		= "Non-I2C SMBus adapter",
	.id		= I2C_ALGO_SMBUS,
	.smbus_xfer	= amd756_access,
	.functionality	= amd756_func,
};

struct i2c_adapter amd756_smbus = {
	.id		= I2C_ALGO_SMBUS | I2C_HW_SMBUS_AMD756,
	.algo		= &smbus_algorithm,
	.inc_use	= amd756_inc,
	.dec_use	= amd756_dec,
};

enum chiptype { AMD756, AMD766, AMD768, NFORCE, AMD8111 };
static const char* chipname[] = {
	"AMD756", "AMD766", "AMD768",
	"nVidia nForce", "AMD8111",
};

static struct pci_device_id amd756_ids[] __devinitdata = {
	{PCI_VENDOR_ID_AMD, 0x740B, PCI_ANY_ID, PCI_ANY_ID, 0, 0, AMD756 },
	{PCI_VENDOR_ID_AMD, 0x7413, PCI_ANY_ID, PCI_ANY_ID, 0, 0, AMD766 },
	{PCI_VENDOR_ID_AMD, 0x7443, PCI_ANY_ID, PCI_ANY_ID, 0, 0, AMD768 },
	{PCI_VENDOR_ID_AMD, 0x746B, PCI_ANY_ID, PCI_ANY_ID, 0, 0, AMD8111 },
	{PCI_VENDOR_ID_NVIDIA, 0x01B4, PCI_ANY_ID, PCI_ANY_ID, 0, 0, NFORCE },
	{ 0, }
};

static int __devinit amd756_probe(struct pci_dev *pdev,
				  const struct pci_device_id *id)
{
	int nforce = (id->driver_data == NFORCE);
	int error;
	u8 temp;
	
	if (amd756_ioport) {
		printk(KERN_ERR DRV_NAME ": Only one device supported. "
		       "(you have a strange motherboard, btw..)\n");
		return -ENODEV;
	}

	if (nforce) {
		if (PCI_FUNC(pdev->devfn) != 1)
			return -ENODEV;

		pci_read_config_word(pdev, SMBBANFORCE, &amd756_ioport);
		amd756_ioport &= 0xfffc;
	} else { /* amd */
		if (PCI_FUNC(pdev->devfn) != 3)
			return -ENODEV;

		pci_read_config_byte(pdev, SMBGCFG, &temp);
		if ((temp & 128) == 0) {
			printk(KERN_ERR DRV_NAME
			       ": Error: SMBus controller I/O not enabled!\n");
			return -ENODEV;
		}

		/* Determine the address of the SMBus areas */
		/* Technically it is a dword but... */
		pci_read_config_word(pdev, SMBBA, &amd756_ioport);
		amd756_ioport &= 0xff00;
		amd756_ioport += SMB_ADDR_OFFSET;
	}

	if (!request_region(amd756_ioport, SMB_IOSIZE, "amd756-smbus")) {
		printk(KERN_ERR DRV_NAME
		       ": SMB region 0x%x already in use!\n", amd756_ioport);
		return -ENODEV;
	}

#ifdef DEBUG
	pci_read_config_byte(pdev, SMBREV, &temp);
	printk(KERN_DEBUG DRV_NAME ": SMBREV = 0x%X\n", temp);
	printk(KERN_DEBUG DRV_NAME ": AMD756_smba = 0x%X\n", amd756_ioport);
#endif

	sprintf(amd756_smbus.name, "SMBus %s adapter at %04x",
		chipname[id->driver_data], amd756_ioport);

	error = i2c_add_adapter(&amd756_smbus);
	if (error) {
		printk(KERN_ERR DRV_NAME
		       ": Adapter registration failed, module not inserted.\n");
		goto out_err;
	}

	return 0;

 out_err:
	release_region(amd756_ioport, SMB_IOSIZE);
	return error;
}


static int __init i2c_amd756_init(void)
{
	struct pci_dev *dev;
	const struct pci_device_id *id;

	printk(KERN_INFO "i2c-amd756.o version %s (%s)\n", LM_VERSION, LM_DATE);

 	pci_for_each_dev(dev) {
		id = pci_match_device(amd756_ids, dev);
		if (id && amd756_probe(dev, id) >= 0)
			return 0; 
	}

	return -ENODEV;
}


static void __exit i2c_amd756_exit(void)
{
	i2c_del_adapter(&amd756_smbus);
	release_region(amd756_ioport, SMB_IOSIZE);
}

MODULE_AUTHOR("Merlin Hughes <merlin@merlin.org>");
MODULE_DESCRIPTION("AMD756/766/768/8111 and nVidia nForce SMBus driver");
MODULE_LICENSE("GPL");

EXPORT_SYMBOL(amd756_smbus);

module_init(i2c_amd756_init)
module_exit(i2c_amd756_exit)
