/* pci-serial.c: A PCI serial port (e.g. modem) activator for Linux. */
/*
	Written/copyright 1999 by Donald Becker.

	This software may be used and distributed according to the terms
	of the GNU Public License, incorporated herein by reference.
	All other rights reserved.

	This driver is an activator for PCI serial devices.

	The author may be reached as becker@CESDIS.edu, or C/O
	USRA Center of Excellence in Space Data and Information Sciences
	   Code 930.5, NASA Goddard Space Flight Center, Greenbelt MD 20771

	Support and updates available at
	http://cesdis.gsfc.nasa.gov/linux/pcmcia/pcmcia.html
*/

static const char *version =
"pci-serial.c:v1.00 7/21/99 Donald Becker http://cesdis.gsfc.nasa.gov/linux/pcmcia/pcmcia.html\n";

/* A few user-configurable values. */

/* Operational parameters that usually are not changed. */

#if !defined(__OPTIMIZE__)  ||  !defined(__KERNEL__)
#warning  You must compile this file with the correct options!
#warning  See the last lines of the source file.
#error You must compile this driver with "-O".
#endif

#include <linux/config.h>
#include <linux/version.h>
#ifdef MODULE
#ifdef MODVERSIONS
#include <linux/modversions.h>
#endif
#include <linux/module.h>
#else
#define MOD_INC_USE_COUNT
#define MOD_DEC_USE_COUNT
#endif

#include <linux/kernel.h>
#include <linux/malloc.h>
#include <linux/pci.h>
#if LINUX_VERSION_CODE < 0x20155
#include <linux/bios32.h>
#define PCI_SUPPORT 1
#else
#define PCI_SUPPORT 2
#endif
#include <linux/major.h>
#include <linux/serial.h>

#include <asm/io.h>

#if ! defined (LINUX_VERSION_CODE) || LINUX_VERSION_CODE < 0x20000
#warning This driver version is only for kernel versions 2.0.0 and later.
#endif

#if defined(MODULE) && LINUX_VERSION_CODE >= 0x20115
MODULE_AUTHOR("Donald Becker <becker@cesdis.gsfc.nasa.gov>");
MODULE_DESCRIPTION("PCI serial port activator");
MODULE_PARM(debug, "i");
#endif
#if LINUX_VERSION_CODE < 0x20123
#define test_and_set_bit(val, addr) set_bit(val, addr)
#endif
#if LINUX_VERSION_CODE < 0x20155
#define PCI_SUPPORT_VER1
#define pci_present pcibios_present
#endif

static int debug = 1;

/*
				Theory of Operation

I. Board Compatibility

This device driver is designed for PCI serial ports.


II. Board-specific settings

N/A

III. Operation

IVb. References

IVc. Errata

*/

/* The rest of these values should never change. */

static struct cb_serial_info {
	struct cb_serial_info *next;
	long ioaddr;
	int major, minor;
	char dev_name[8];
	u32 subsystem_id;
	u8 pci_bus, pci_devfn, irq;
} *cb_serial_list;

int serial_attach(int bus, int devfn)
{
    struct serial_struct serial;
    int line;
	u16 device_id, vendor_id, pci_cmd;
	u32 addr0, subsystem_id, pwr_cmd;
	u8 irq;
	long ioaddr;

	if (debug) {
		printk(KERN_INFO "serial_attach(bus %d, function %d).\n",
			   bus, devfn);
	}
	pcibios_read_config_dword(bus, devfn, PCI_BASE_ADDRESS_0, &addr0);
	if ( ! (addr0 & 1))
		pcibios_read_config_dword(bus, devfn, PCI_BASE_ADDRESS_1, &addr0);
	pcibios_read_config_byte(bus, devfn, PCI_INTERRUPT_LINE, &irq);
	pcibios_read_config_word(bus, devfn, PCI_VENDOR_ID, &vendor_id);
	pcibios_read_config_word(bus, devfn, PCI_DEVICE_ID, &device_id);
	pcibios_read_config_dword(bus, devfn, PCI_SUBSYSTEM_ID, &subsystem_id);
	pcibios_read_config_dword(bus, devfn, 0x44, &pwr_cmd);
	pcibios_write_config_dword(bus, devfn, 0x44, pwr_cmd & ~3);
	pcibios_read_config_word(bus, devfn, PCI_COMMAND, &pci_cmd);
	ioaddr = addr0 & ~3;
	if (ioaddr == 0 || irq == 0) {
		printk(KERN_ERR "A CardBus serial port was not assigned an %s.\n",
			   ioaddr == 0 ? "I/O address" : "IRQ");
		return 0;
	}
	if (debug) {
		printk(KERN_INFO " PCI command register was %4.4x.\n", pci_cmd);
		printk(KERN_INFO "serial_attach(bus %d, function %d), device %4.4x "
			   "IRQ %d IO %lx subsystem ID %8.8x.\n", bus, devfn, device_id,
			   irq, ioaddr, subsystem_id);
	}
	/* Insert vendor-specific magic here. */
	serial.port = ioaddr;
    serial.irq = irq;
    serial.flags = ASYNC_SHARE_IRQ;
    line = register_serial(&serial);

	{
		int i;
		printk(" register dump at %#lx:", ioaddr);
		for (i = 0; i < 8; i++)
			printk(" %2.2x", inb(ioaddr + i));
		printk(".\n");
	}

	if (line < 0) {
		printk(KERN_NOTICE "serial_cb: register_serial() at 0x%04x, "
			   "irq %d failed\n", serial.port, serial.irq);
	} else {
		struct cb_serial_info *info =
			kmalloc(sizeof(struct cb_serial_info), GFP_KERNEL);
		memset(info, 0, sizeof(struct cb_serial_info));
		sprintf(info->dev_name, "ttyS%d", line);
		info->major = TTY_MAJOR;
		info->minor = 0x40 + line;
		info->pci_bus = bus;
		info->pci_devfn = devfn;
		info->ioaddr = ioaddr;
		info->subsystem_id = subsystem_id;
		info->next = cb_serial_list;
		cb_serial_list = info;
		MOD_INC_USE_COUNT;
		return 1;
	}
	return 0;
}

static void serial_detach(void)
{
	struct cb_serial_info *info, **infop;
	if (debug)
		printk(KERN_INFO "serial_detach()\n");
	for (infop = &cb_serial_list; *infop; *infop = (*infop)->next)
		if (1)
			break;
	info = *infop;
	if (info == NULL)
		return;
#if 0
	unregister_serial(node->minor - 0x40);
#endif
	*infop = info->next;
	kfree(info);
	MOD_DEC_USE_COUNT;
	if (debug)
		printk(KERN_INFO "serial_detach() done.\n");
}


#ifdef MODULE

int init_module(void)
{
	int cards_found = 0;
	int pci_index;
	unsigned char pci_bus, pci_device_fn;

	printk(KERN_INFO "%s", version);
	
	if ( ! pcibios_present())
		return -ENODEV;

	for (pci_index = 0; pci_index < 0xff; pci_index++) {
		if (pcibios_find_class (PCI_CLASS_COMMUNICATION_OTHER << 8, pci_index,
								&pci_bus, &pci_device_fn)
			!= PCIBIOS_SUCCESSFUL)
			break;
		cards_found++;
		serial_attach(pci_bus, pci_device_fn);
	}
	return cards_found ? 0 : -ENODEV;
}

void cleanup_module(void)
{
	return;
}

#endif  /* MODULE */

/*
 * Local variables:
 *  compile-command: "gcc -DMODULE -D__KERNEL__ -Wall -Wstrict-prototypes -O6 -c pci-serial.c"
 *  c-indent-level: 4
 *  c-basic-offset: 4
 *  tab-width: 4
 * End:
 */
