/*
 * SGI IOC3 master driver and IRQ demuxer
 *
 * Copyright (c) 2005 Stanislaw Skowronek <skylark@linux-mips.org>
 * Heavily based on similar work by:
 *   Brent Casavant <bcasavan@sgi.com> - IOC4 master driver
 *   Pat Gefre <pfg@sgi.com> - IOC3 serial port IRQ demuxer
 */

#include <linux/config.h>
#include <linux/errno.h>
#include <linux/module.h>
#include <linux/pci.h>
#include <linux/interrupt.h>
#include <linux/spinlock.h>
#include <linux/delay.h>
#include <linux/ioc3.h>
#include <linux/rwsem.h>

#define IOC3_PCI_SIZE 0x100000

static LIST_HEAD(ioc3_devices);
static int ioc3_counter;
static DECLARE_RWSEM(ioc3_devices_rwsem);

static struct ioc3_submodule *ioc3_submodules[IOC3_MAX_SUBMODULES];
static struct ioc3_submodule *ioc3_ethernet;
static rwlock_t ioc3_submodules_lock = RW_LOCK_UNLOCKED;

/* NIC probing code */

#define GPCR_MLAN_EN    0x00200000      /* enable MCR to pin 8 */

static inline unsigned mcr_pack(unsigned pulse, unsigned sample)
{
	return (pulse << 10) | (sample << 2);
}

static int nic_wait(struct ioc3_driver_data *idd)
{
	volatile unsigned mcr;

        do {
                mcr = (volatile unsigned)idd->vma->mcr;
        } while (!(mcr & 2));

        return mcr & 1;
}

static int nic_reset(struct ioc3_driver_data *idd)
{
        int presence;
	unsigned long flags;

	local_irq_save(flags);
	idd->vma->mcr = mcr_pack(500, 65);
	presence = nic_wait(idd);
	local_irq_restore(flags);

	udelay(500);

        return presence;
}

static inline int nic_read_bit(struct ioc3_driver_data *idd)
{
	int result;
	unsigned long flags;

	local_irq_save(flags);
	idd->vma->mcr = mcr_pack(6, 13);
	result = nic_wait(idd);
	local_irq_restore(flags);

	udelay(500);

	return result;
}

static inline void nic_write_bit(struct ioc3_driver_data *idd, int bit)
{
	if (bit)
		idd->vma->mcr = mcr_pack(6, 110);
	else
		idd->vma->mcr = mcr_pack(80, 30);

	nic_wait(idd);
}

static unsigned nic_read_byte(struct ioc3_driver_data *idd)
{
	unsigned result = 0;
	int i;

	for (i = 0; i < 8; i++)
		result = (result >> 1) | (nic_read_bit(idd) << 7);

	return result;
}

static void nic_write_byte(struct ioc3_driver_data *idd, int byte)
{
	int i, bit;

	for (i = 8; i; i--) {
		bit = byte & 1;
		byte >>= 1;

		nic_write_bit(idd, bit);
	}
}

static unsigned long
nic_find(struct ioc3_driver_data *idd, int *last, unsigned long addr)
{
	int a, b, index, disc;

	nic_reset(idd);

	/* Search ROM.  */
	nic_write_byte(idd, 0xF0);

	/* Algorithm from ``Book of iButton Standards''.  */
	for (index = 0, disc = 0; index < 64; index++) {
		a = nic_read_bit(idd);
		b = nic_read_bit(idd);

		if (a && b) {
			printk(KERN_WARNING "IOC3 NIC search failed.\n");
			*last = 0;
			return 0;
		}

		if (!a && !b) {
			if (index == *last) {
				addr |= 1UL << index;
			} else if (index > *last) {
				addr &= ~(1UL << index);
				disc = index;
			} else if ((addr & (1UL << index)) == 0)
				disc = index;
			nic_write_bit(idd, (addr>>index)&1);
			continue;
		} else {
			if (a)
				addr |= 1UL << index;
			else
				addr &= ~(1UL << index);
			nic_write_bit(idd, a);
			continue;
		}
	}
	*last = disc;
	return addr;
}

static void nic_addr(struct ioc3_driver_data *idd, unsigned long addr)
{
	int index;

	nic_reset(idd);
	nic_write_byte(idd, 0xF0);
	for (index = 0; index < 64; index++) {
		nic_read_bit(idd);
		nic_read_bit(idd);
		nic_write_bit(idd, (addr>>index)&1);
	}
}

static void crc16_byte(unsigned int *crc, unsigned char db)
{
	int i;

	for(i=0;i<8;i++) {
		*crc <<= 1;
		if((db^(*crc>>16)) & 1)
			*crc ^= 0x8005;
		db >>= 1;
	}
	*crc &= 0xFFFF;
}

static unsigned int crc16_area(unsigned char *dbs, int size, unsigned int crc)
{
	while(size--)
		crc16_byte(&crc, *(dbs++));
	return crc;
}

static void crc8_byte(unsigned int *crc, unsigned char db)
{
	int i,f;

	for(i=0;i<8;i++) {
		f = (*crc ^ db) & 1;
		*crc >>= 1;
		db >>= 1;
		if(f)
			*crc ^= 0x8c;
	}
	*crc &= 0xff;
}

static unsigned int crc8_addr(unsigned long addr)
{
	int i;
	unsigned int crc = 0x00;

	for(i=0;i<8;i++)
		crc8_byte(&crc, addr>>(i<<3));
	return crc;
}

static void
read_redir_page(struct ioc3_driver_data *idd, unsigned long addr, int page,
			unsigned char *redir, unsigned char *data)
{
	int loops = 16, i;

	while(redir[page] != 0xFF) {
		page = redir[page]^0xFF;
		loops--;
		if(loops<0) {
			printk(KERN_ERR "IOC3: NIC circular redirection\n");
			return;
		}
	}
	loops = 3;
	while(loops>0) {
		nic_addr(idd, addr);
		nic_write_byte(idd, 0xF0);
		nic_write_byte(idd, (page << 5) & 0xE0);
		nic_write_byte(idd, (page >> 3) & 0x1F);
		for(i=0;i<0x20;i++)
			data[i] = nic_read_byte(idd);
		if(crc16_area(data, 0x20, 0x0000) == 0x800d)
			return;
		loops--;
	}
	printk(KERN_ERR "IOC3: CRC error in data page\n");
	for(i=0;i<0x20;i++)
		data[i] = 0x00;
}

static void
read_redir_map(struct ioc3_driver_data *idd, unsigned long addr,
					 unsigned char *redir)
{
	int i,j,loops = 3,crc_ok;
	unsigned int crc;

	while(loops>0) {
		crc_ok = 1;
		nic_addr(idd, addr);
		nic_write_byte(idd, 0xAA);
		nic_write_byte(idd, 0x00);
		nic_write_byte(idd, 0x01);
		for(i=0;i<64;i+=8) {
			for(j=0;j<8;j++)
				redir[i+j] = nic_read_byte(idd);
			crc = crc16_area(redir+i, 8, (i==0)?0x8707:0x0000);
			crc16_byte(&crc, nic_read_byte(idd));
			crc16_byte(&crc, nic_read_byte(idd));
			if(crc != 0x800d)
				crc_ok = 0;
		}
		if(crc_ok)
			return;
		loops--;
	}
	printk(KERN_ERR "IOC3: CRC error in redirection page\n");
	for(i=0;i<64;i++)
		redir[i] = 0xFF;
}

static void read_nic(struct ioc3_driver_data *idd, unsigned long addr)
{
	unsigned char redir[64];
	unsigned char data[64],part[32];
	int i,j;

	/* read redirections */
	read_redir_map(idd, addr, redir);
	/* read data pages */
	read_redir_page(idd, addr, 0, redir, data);
	read_redir_page(idd, addr, 1, redir, data+32);
	/* assemble the part # */
	j=0;
	for(i=0;i<19;i++)
		if(data[i+11] != ' ')
			part[j++] = data[i+11];
	for(i=0;i<6;i++)
		if(data[i+32] != ' ')
			part[j++] = data[i+32];
	part[j] = 0;
	/* skip Octane power supplies */
	if(!strncmp(part, "060-0035-", 9))
		return;
	if(!strncmp(part, "060-0038-", 9))
		return;
	strcpy(idd->nic_part, part);
	/* assemble the serial # */
	j=0;
	for(i=0;i<10;i++)
		if(data[i+1] != ' ')
			idd->nic_serial[j++] = data[i+1];
	idd->nic_serial[j] = 0;
}

static void read_mac(struct ioc3_driver_data *idd, unsigned long addr)
{
	int i, loops = 3;
	unsigned char data[13];

	while(loops>0) {
		nic_addr(idd, addr);
		nic_write_byte(idd, 0xF0);
		nic_write_byte(idd, 0x00);
		nic_write_byte(idd, 0x00);
		nic_read_byte(idd);
		for(i=0;i<13;i++)
			data[i] = nic_read_byte(idd);
		if(crc16_area(data, 13, 0x0000) == 0x800d) {
			for(i=10;i>4;i--)
				idd->nic_mac[10-i] = data[i];
			return;
		}
		loops--;
	}
	printk(KERN_ERR "IOC3: CRC error in MAC address\n");
	for(i=0;i<6;i++)
		idd->nic_mac[i] = 0x00;
}

static void probe_nic(struct ioc3_driver_data *idd)
{
        int save = 0, loops = 3;
        unsigned long first, addr;

        idd->vma->gpcr_s = GPCR_MLAN_EN;

        while(loops>0) {
                idd->nic_part[0] = 0;
                idd->nic_serial[0] = 0;
                addr = first = nic_find(idd, &save, 0);
                if(!first)
                        return;
                while(1) {
                        if(crc8_addr(addr))
                                break;
                        else {
                                switch(addr & 0xFF) {
                                case 0x0B:
                                        read_nic(idd, addr);
                                        break;
                                case 0x09:
                                case 0x89:
                                case 0x91:
                                        read_mac(idd, addr);
                                        break;
                                }
                        }
                        addr = nic_find(idd, &save, addr);
                        if(addr == first)
                                return;
                }
                loops--;
        }
        printk(KERN_ERR "IOC3: CRC error in NIC address\n");
}

/* Interrupts */

static inline void
write_ireg(struct ioc3_driver_data *idd, uint32_t val, int which)
{
	unsigned long flags;

	spin_lock_irqsave(&idd->ir_lock, flags);
	switch (which) {
	case IOC3_W_IES:
		writel(val, &idd->vma->sio_ies);
		break;
	case IOC3_W_IEC:
		writel(val, &idd->vma->sio_iec);
		break;
	}
	spin_unlock_irqrestore(&idd->ir_lock, flags);
}
static inline uint32_t get_pending_intrs(struct ioc3_driver_data *idd)
{
	unsigned long flag;
	uint32_t intrs = 0;

	spin_lock_irqsave(&idd->ir_lock, flag);
	intrs = readl(&idd->vma->sio_ir);
	intrs &= readl(&idd->vma->sio_ies);
	spin_unlock_irqrestore(&idd->ir_lock, flag);
	return intrs;
}

static irqreturn_t ioc3_intr_io(int irq, void *arg, struct pt_regs *regs)
{
	unsigned long flags;
	struct ioc3_driver_data *idd = (struct ioc3_driver_data *)arg;
	int handled = 1, id;
	unsigned int pending;

	read_lock_irqsave(&ioc3_submodules_lock, flags);

	if(idd->dual_irq && idd->vma->eisr) {
		/* send Ethernet IRQ to the driver */
		if(ioc3_ethernet && idd->active[ioc3_ethernet->id] &&
						ioc3_ethernet->intr) {
			handled = handled && !ioc3_ethernet->intr(ioc3_ethernet,
							idd, 0, regs);
		}
	}
	pending = get_pending_intrs(idd);	/* look at the IO IRQs */

	for(id=0;id<IOC3_MAX_SUBMODULES;id++) {
		if(idd->active[id] && ioc3_submodules[id]
				&& (pending & ioc3_submodules[id]->irq_mask)
				&& ioc3_submodules[id]->intr) {
			write_ireg(idd, ioc3_submodules[id]->irq_mask,
							IOC3_W_IEC);
			if(!ioc3_submodules[id]->intr(ioc3_submodules[id],
				   idd, pending & ioc3_submodules[id]->irq_mask,
					regs))
				pending &= ~ioc3_submodules[id]->irq_mask;
			if (ioc3_submodules[id]->reset_mask)
				write_ireg(idd, ioc3_submodules[id]->irq_mask,
							IOC3_W_IES);
		}
	}
	read_unlock_irqrestore(&ioc3_submodules_lock, flags);
	if(pending) {
		printk(KERN_WARNING
		  "IOC3: Pending IRQs 0x%08x discarded and disabled\n",pending);
		write_ireg(idd, pending, IOC3_W_IEC);
		handled = 1;
	}
	return handled?IRQ_HANDLED:IRQ_NONE;
}

static irqreturn_t ioc3_intr_eth(int irq, void *arg, struct pt_regs *regs)
{
	unsigned long flags;
	struct ioc3_driver_data *idd = (struct ioc3_driver_data *)arg;
	int handled = 1;

	if(!idd->dual_irq)
		return IRQ_NONE;
	read_lock_irqsave(&ioc3_submodules_lock, flags);
	if(ioc3_ethernet && idd->active[ioc3_ethernet->id]
				&& ioc3_ethernet->intr)
		handled = handled && !ioc3_ethernet->intr(ioc3_ethernet, idd, 0,
								regs);
	read_unlock_irqrestore(&ioc3_submodules_lock, flags);
	return handled?IRQ_HANDLED:IRQ_NONE;
}

void ioc3_enable(struct ioc3_submodule *is,
				struct ioc3_driver_data *idd, unsigned int irqs)
{
	write_ireg(idd, irqs & is->irq_mask, IOC3_W_IES);
}

void ioc3_ack(struct ioc3_submodule *is, struct ioc3_driver_data *idd,
				unsigned int irqs)
{
	writel(irqs & is->irq_mask, &idd->vma->sio_ir);
}

void ioc3_disable(struct ioc3_submodule *is,
				struct ioc3_driver_data *idd, unsigned int irqs)
{
	write_ireg(idd, irqs & is->irq_mask, IOC3_W_IEC);
}

void ioc3_gpcr_set(struct ioc3_driver_data *idd, unsigned int val)
{
	unsigned long flags;
	spin_lock_irqsave(&idd->gpio_lock, flags);
	writel(val, &idd->vma->gpcr_s);
	spin_unlock_irqrestore(&idd->gpio_lock, flags);
}

/* Keep it simple, stupid! */
static int find_slot(void **tab, int max)
{
	int i;
	for(i=0;i<max;i++)
		if(!(tab[i]))
			return i;
	return -1;
}

/* Register an IOC3 submodule */
int ioc3_register_submodule(struct ioc3_submodule *is)
{
	struct ioc3_driver_data *idd;
	int alloc_id;
	unsigned long flags;

	write_lock_irqsave(&ioc3_submodules_lock, flags);
	alloc_id = find_slot((void **)ioc3_submodules, IOC3_MAX_SUBMODULES);
	if(alloc_id != -1) {
		ioc3_submodules[alloc_id] = is;
		if(is->ethernet) {
			if(ioc3_ethernet==NULL)
				ioc3_ethernet=is;
			else
				printk(KERN_WARNING
				  "IOC3 Ethernet module already registered!\n");
		}
	}
	write_unlock_irqrestore(&ioc3_submodules_lock, flags);

	if(alloc_id == -1) {
		printk(KERN_WARNING "Increase IOC3_MAX_SUBMODULES!\n");
		return -ENOMEM;
	}

	is->id=alloc_id;

	/* Initialize submodule for each IOC3 */
	if (!is->probe)
		return 0;

	down_read(&ioc3_devices_rwsem);
	list_for_each_entry(idd, &ioc3_devices, list) {
		/* set to 1 for IRQs in probe */
		idd->active[alloc_id] = 1;
		idd->active[alloc_id] = !is->probe(is, idd);
	}
	up_read(&ioc3_devices_rwsem);

	return 0;
}

/* Unregister an IOC3 submodule */
void ioc3_unregister_submodule(struct ioc3_submodule *is)
{
	struct ioc3_driver_data *idd;
	unsigned long flags;

	write_lock_irqsave(&ioc3_submodules_lock, flags);
	if(ioc3_submodules[is->id]==is)
		ioc3_submodules[is->id]=NULL;
	else
		printk(KERN_WARNING
			"IOC3 submodule %s has wrong ID.\n",is->name);
	if(ioc3_ethernet==is)
		ioc3_ethernet = NULL;
	write_unlock_irqrestore(&ioc3_submodules_lock, flags);

	/* Remove submodule for each IOC3 */
	down_read(&ioc3_devices_rwsem);
	list_for_each_entry(idd, &ioc3_devices, list)
		if(idd->active[is->id]) {
			if(is->remove)
				if(is->remove(is, idd))
					printk(KERN_WARNING
					       "%s: IOC3 submodule %s remove failed "
					       "for pci_dev %s.\n",
					       __FUNCTION__, module_name(is->owner),
					       pci_name(idd->pdev));
			idd->active[is->id] = 0;
			if(is->irq_mask)
				write_ireg(idd, is->irq_mask, IOC3_W_IEC);
		}
	up_read(&ioc3_devices_rwsem);
}

/*********************
 * Device management *
 *********************/

static char *
ioc3_class_names[]={"unknown", "IP27 BaseIO", "IP30 system", "MENET 1/2/3",
			"MENET 4", "CADduo", "Altix Serial"};

static int ioc3_class(struct ioc3_driver_data *idd)
{
	int res = IOC3_CLASS_NONE;
	/* NIC-based logic */
	if(!strncmp(idd->nic_part, "030-0891-", 9))
		res = IOC3_CLASS_BASE_IP30;
	if(!strncmp(idd->nic_part, "030-1155-", 9))
		res = IOC3_CLASS_CADDUO;
	if(!strncmp(idd->nic_part, "030-1657-", 9))
		res = IOC3_CLASS_SERIAL;
	if(!strncmp(idd->nic_part, "030-1664-", 9))
		res = IOC3_CLASS_SERIAL;
	/* total random heuristics */
#ifdef CONFIG_SGI_IP27
	if(!idd->nic_part[0])
		res = IOC3_CLASS_BASE_IP27;
#endif
	/* print educational message */
	printk(KERN_INFO "IOC3 part: [%s], serial: [%s] => class %s\n",
			idd->nic_part, idd->nic_serial, ioc3_class_names[res]);
	return res;
}
/* Adds a new instance of an IOC3 card */
static int ioc3_probe(struct pci_dev *pdev, const struct pci_device_id *pci_id)
{
	struct ioc3_driver_data *idd;
	uint32_t pcmd;
	int ret, id;

	/* Enable IOC3 and take ownership of it */
	if ((ret = pci_enable_device(pdev))) {
		printk(KERN_WARNING
		       "%s: Failed to enable IOC3 device for pci_dev %s.\n",
		       __FUNCTION__, pci_name(pdev));
		goto out;
	}
	pci_set_master(pdev);

#ifdef USE_64BIT_DMA
        ret = pci_set_dma_mask(pdev, 0xffffffffffffffffULL);
        if (!ret) {
                ret = pci_set_consistent_dma_mask(pdev, 0xffffffffffffffffULL);
                if (ret < 0) {
                        printk(KERN_WARNING "%s: Unable to obtain 64 bit DMA "
                               "for consistent allocations\n",
				__FUNCTION__);
                }
	}
#endif

	/* Set up per-IOC3 data */
	idd = kmalloc(sizeof(struct ioc3_driver_data), GFP_KERNEL);
	if (!idd) {
		printk(KERN_WARNING
		       "%s: Failed to allocate IOC3 data for pci_dev %s.\n",
		       __FUNCTION__, pci_name(pdev));
		ret = -ENODEV;
		goto out_idd;
	}
	memset(idd, 0, sizeof(struct ioc3_driver_data));
	spin_lock_init(&idd->ir_lock);
	spin_lock_init(&idd->gpio_lock);
	idd->pdev = pdev;

	/* Map all IOC3 registers.  These are shared between subdevices
	 * so the main IOC3 module manages them.
	 */
	idd->pma = pci_resource_start(pdev, 0);
	if (!idd->pma) {
		printk(KERN_WARNING
		       "%s: Unable to find IOC3 resource "
		       "for pci_dev %s.\n",
		       __FUNCTION__, pci_name(pdev));
		ret = -ENODEV;
		goto out_pci;
	}
	if (!request_region(idd->pma, IOC3_PCI_SIZE, "ioc3")) {
		printk(KERN_WARNING
		       "%s: Unable to request IOC3 region "
		       "for pci_dev %s.\n",
		       __FUNCTION__, pci_name(pdev));
		ret = -ENODEV;
		goto out_pci;
	}
	idd->vma = ioremap(idd->pma, IOC3_PCI_SIZE);
	if (!idd->vma) {
		printk(KERN_WARNING
		       "%s: Unable to remap IOC3 region "
		       "for pci_dev %s.\n",
		       __FUNCTION__, pci_name(pdev));
		ret = -ENODEV;
		goto out_misc_region;
	}

	/* Track PCI-device specific data */
	pci_set_drvdata(pdev, idd);
	down_write(&ioc3_devices_rwsem);
	list_add(&idd->list, &ioc3_devices);
	idd->id = ioc3_counter++;
	up_write(&ioc3_devices_rwsem);

	idd->gpdr_shadow = idd->vma->gpdr;

	/* Read IOC3 NIC contents */
	probe_nic(idd);

	/* Detect IOC3 class */
	idd->class = ioc3_class(idd);

	/* Initialize IOC3 */
       pci_read_config_dword(pdev, PCI_COMMAND, &pcmd);
       pci_write_config_dword(pdev, PCI_COMMAND,
                               pcmd | PCI_COMMAND_MEMORY |
                               PCI_COMMAND_PARITY | PCI_COMMAND_SERR |
                               PCI_SCR_DROP_MODE_EN);

	write_ireg(idd, ~0, IOC3_W_IEC);
	writel(~0, &idd->vma->sio_ir);

	/* Set up IRQs */
	if(idd->class == IOC3_CLASS_BASE_IP30
				|| idd->class == IOC3_CLASS_BASE_IP27) {
		writel(0, &idd->vma->eier);
		writel(~0, &idd->vma->eisr);

		idd->dual_irq = 1;
		if (!request_irq(pdev->irq, ioc3_intr_eth, SA_SHIRQ,
				 "ioc3-eth", (void *)idd)) {
			idd->irq_eth = pdev->irq;
		} else {
			printk(KERN_WARNING
			       "%s : request_irq fails for IRQ 0x%x\n ",
			       __FUNCTION__, pdev->irq);
		}
		if (!request_irq(pdev->irq+2, ioc3_intr_io, SA_SHIRQ,
				 "ioc3-io", (void *)idd)) {
			idd->irq_io = pdev->irq+2;
		} else {
			printk(KERN_WARNING
			       "%s : request_irq fails for IRQ 0x%x\n ",
			       __FUNCTION__, pdev->irq+2);
		}
	} else {
		if (!request_irq(pdev->irq, ioc3_intr_io, SA_SHIRQ,
				 "ioc3", (void *)idd)) {
			idd->irq_io = pdev->irq;
		} else {
			printk(KERN_WARNING
			       "%s : request_irq fails for IRQ 0x%x\n ",
			       __FUNCTION__, pdev->irq);
		}
	}

	/* Add this IOC3 to all submodules */
	read_lock(&ioc3_submodules_lock);
	for(id=0;id<IOC3_MAX_SUBMODULES;id++)
		if(ioc3_submodules[id] && ioc3_submodules[id]->probe) {
			idd->active[id] = 1;
			idd->active[id] = !ioc3_submodules[id]->probe
						(ioc3_submodules[id], idd);
		}
	read_unlock(&ioc3_submodules_lock);

	printk(KERN_INFO "IOC3 Master Driver loaded for %s\n", pci_name(pdev));

	return 0;

out_misc_region:
	release_region(idd->pma, IOC3_PCI_SIZE);
out_pci:
	kfree(idd);
out_idd:
	pci_disable_device(pdev);
out:
	return ret;
}

/* Removes a particular instance of an IOC3 card. */
static void ioc3_remove(struct pci_dev *pdev)
{
	int id;
	struct ioc3_driver_data *idd;

	idd = pci_get_drvdata(pdev);

	/* Remove this IOC3 from all submodules */
	read_lock(&ioc3_submodules_lock);
	for(id=0;id<IOC3_MAX_SUBMODULES;id++)
		if(idd->active[id]) {
			if(ioc3_submodules[id] && ioc3_submodules[id]->remove)
				if(ioc3_submodules[id]->remove(ioc3_submodules[id],
								idd))
					printk(KERN_WARNING
					       "%s: IOC3 submodule 0x%s remove failed "
					       "for pci_dev %s.\n",
					        __FUNCTION__,
						module_name(ioc3_submodules[id]->owner),
					        pci_name(pdev));
			idd->active[id] = 0;
		}
	read_unlock(&ioc3_submodules_lock);

	/* Clear and disable all IRQs */
	write_ireg(idd, ~0, IOC3_W_IEC);
	writel(~0, &idd->vma->sio_ir);

	/* Release resources */
	free_irq(idd->irq_io, (void *)idd);
	if(idd->dual_irq)
		free_irq(idd->irq_eth, (void *)idd);
	iounmap(idd->vma);
	release_region(idd->pma, IOC3_PCI_SIZE);

	/* Disable IOC3 and relinquish */
	pci_disable_device(pdev);

	/* Remove and free driver data */
	down_write(&ioc3_devices_rwsem);
	list_del(&idd->list);
	up_write(&ioc3_devices_rwsem);
	kfree(idd);
}

static struct pci_device_id ioc3_id_table[] = {
	{PCI_VENDOR_ID_SGI, PCI_DEVICE_ID_SGI_IOC3, PCI_ANY_ID, PCI_ANY_ID},
	{0}
};

static struct pci_driver ioc3_driver = {
	.name = "IOC3",
	.id_table = ioc3_id_table,
	.probe = ioc3_probe,
	.remove = ioc3_remove,
};

MODULE_DEVICE_TABLE(pci, ioc3_id_table);

/*********************
 * Module management *
 *********************/

/* Module load */
static int __devinit ioc3_init(void)
{
	if (ia64_platform_is("sn2"))
		return pci_register_driver(&ioc3_driver);
	return 0;
}

/* Module unload */
static void __devexit ioc3_exit(void)
{
	pci_unregister_driver(&ioc3_driver);
}

module_init(ioc3_init);
module_exit(ioc3_exit);

MODULE_AUTHOR("Stanislaw Skowronek <skylark@linux-mips.org>");
MODULE_DESCRIPTION("PCI driver for SGI IOC3");
MODULE_LICENSE("GPL");

EXPORT_SYMBOL(ioc3_register_submodule);
EXPORT_SYMBOL(ioc3_unregister_submodule);
EXPORT_SYMBOL(ioc3_ack);
EXPORT_SYMBOL(ioc3_gpcr_set);
EXPORT_SYMBOL(ioc3_disable);
EXPORT_SYMBOL(ioc3_enable);