/* pci-serial.c: A PCI serial port (e.g. modem) activator for Linux. */
/*
	This driver is an activator for PCI serial devices.

	Written/copyright 1999-2002 by Donald Becker.

	This software may be used and distributed according to the terms of
	the GNU General Public License (GPL), incorporated herein by reference.
	Drivers based on or derived from this code fall under the GPL and must
	retain the authorship, copyright and license notice.  This file is not
	a complete program and may only be used when the entire operating
	system is licensed under the GPL.

	The author may be reached as becker@scyld.com, or C/O
	Scyld Computing Corporation
	410 Severn Ave., Suite 210
	Annapolis MD 21403

	Support information and updates available at
	http://www.scyld.com/network/updates.html
*/

static const char *version =
"pci-serial.c:v1.03 7/30/2002 Donald Becker http://www.scyld.com/index.html\n";

/* A few user-configurable values. */

/* Message enable level: 0..31 = no..all messages.  See NETIF_MSG docs. */
static int debug = 1;

/* 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>
#if defined(MODULE) && defined(CONFIG_MODVERSIONS) && ! defined(MODVERSIONS)
#define MODVERSIONS
#endif

#include <linux/version.h>
#if defined(MODVERSIONS)
#include <linux/modversions.h>
#endif
#include <linux/module.h>

#include <linux/kernel.h>
#if LINUX_VERSION_CODE >= 0x20400
#include <linux/slab.h>
#else
#include <linux/malloc.h>
#endif
#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>
#include "kern_compat.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

MODULE_AUTHOR("Donald Becker <becker@cesdis.gsfc.nasa.gov>");
MODULE_DESCRIPTION("PCI hot-swap serial port activator");
MODULE_LICENSE("GPL");
MODULE_PARM(debug, "i");
MODULE_PARM_DESC(debug, "Driver message level (0-31)");

#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

/*
				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 > 1) {
		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);

	if (debug > 2) {
		int i;
		printk(KERN_DEBUG "pci-serial: 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, status %d\n", serial.port, serial.irq, line);
	} 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);
	}
	for (pci_index = 0; pci_index < 0xff; pci_index++) {
		if (pcibios_find_class((PCI_CLASS_COMMUNICATION_SERIAL <<8) | 0x02,
							   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: "make pci-serial.o"
 *  alt-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:
 */
