
/*
    opti9xx.c - ALSA driver for OPTi 82C9xx based soundcards.
    Copyright (C) 1998-99 by Massimo Piccioni <piccio@caronte.csr.unibo.it>

    This code is under development at the Italian Ministry of Air Defence,
    Sixth Division (oh, che pace ...), Rome.

    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.
*/

/*
    This code provides support for most OPTi 82C9xx chips based soundcards.

    All non-PnP chips (i.e. 82C928, 82C929 and 82C930) are fully supported.

    Unfortunately I have no experience with 82C924/5 based soundcards, so
    support for these chips is unreliable in both PnP and non-PnP modes.

    This driver version provides initial PnP support (see PnP considerations
    section below).
    All 82C931 based soundcards should be supported in both PnP (tested) and
    non-PnP (not tested) modes.

    This code supports only one soundcard per system, due to several hardware
    limitations.

    This driver allow the user to enable and fully configure the on-board
    cdrom drive interface (i.e. IDE/ATAPI, Sony, Mitsumi and Panasonic)
    of several OPTi based soundcards.
    Search for the NON-SCSI/ATAPI cdrom support documentation in the Linux
    kernel sources.

    Sources:
        OPTi Inc. 82C9xx databooks (available from ftp.opti.com).
        Analog Devices Inc. AD1845/8 databooks.
        Crystal Semiconductor Corp. CS4231A databook.
        Plug and Play ISA Specification, version 1.0a.
        Advanced Linux Sound Architecture documentation and sources.

    Version history:
        0.1:  82C9xx detection and configuration only
        1.0:  basic sound support added (ad1848 and opl3)
        1.1:  cdrom and game port configuration added
        1.1c: cdrom and game port configuration improved
        1.2:  most code rewritten, cs4231 support added
        1.3:  mpu-401 support added (82C929+ chips only)
        1.3a: minor fixing
        2.0:  82C9xx detection improved, initial PnP support added
        2.0a: minor fixing

    Todo:
        improve PnP support
*/

/*
    Chips description coming soon ...
*/

/*
    PnP considerations:
        At this moment, PnP features are fully supported with 82C931 based
        soundcards only. Since 82C933 chip seems to be 82C931-compatible, it
        should be supported (not tested).

        Having an old system should not result in troubles, because ALSA isapnp
        module supports PnP features even in presence of non-PnP BIOSes.

        If your soundcard has a jumper to enable/disable PnP features, please
        test this driver with both configurations and report your experiences.

        If this driver cannot find your PnP card, please report kernel messages
        and /proc/isapnp.
*/

/*
    Sound cards tested:
        Audio 16 Pro  EPC-SOUN9301      (82C930A based)
        ExpertColor  MED-3931 v2.0      (82C931 based)
        ExpertMedia Sound 16  MED-1600  (82C928 based - AD1848)
        Mozart  S601206-G               (OTI601 based - CS4231)
        Pro 16 3D  EPC-S9310302 v3.3    (82C931 based)
        Sound Player S-928              (82C928 based - AD1848)

    Cdrom drives tested:
        CreativeLabs CD200F (Funai E2550UA), Panasonic i/f
        Matsushita-Kotobuki CR-562-B, Panasonic i/f

    Things of interest:
        Several soundcards (such as ExpertMedia Sound 16) use jumpers to
        configure the on-board cdrom drive interface, so software configuration
        doesn't work (of course).

        Be careful when configuring the on-board cdrom drive interface as IDE,
        because the system may hang (i/o and irq conflicts).

        I have no problems with my CreativeLabs CD200F cdrom drive (a Funai
        E2550UA drive with Panasonic proprietary interface) connected to the
        on-board interface of a S-928 soundcard (see the cdrom configuration
        section below). I simply load the sbpcd kernel module after this.
        See the sbpcd documentation for more informations (available in the
        /usr/src/linux/Documentation/cdrom directory).

        Another interesting thing: the contemporary presence of an 82C928
        based (not PnP) sound card and an IBM Auto 16/4 Token Ring ISA PnP
        adapter, may cause several problems (i.e. Linux might not detect the
        token ring adapter), due to an i/o conflict between the soundcard
        game port (0xa20 fixed?) and the tr adapter base port (0xa20 or 0xa24).
        It happens because PnP BIOS is not able to detect what i/o ports the
        soundcard is using, so the first 'available' i/o port (i.e. 0xa20) is
        assigned to the tr adapter (i/o conflict).
        A fine solution is to use the isapnptools to reconfigure your PnP
        adapters (then you can load the ibmtr kernel module without any
        problem).
        Another solution is to disable the sound card game port in this module
        (loaded before the ibmtr module, see the game port configuration
        section below).

        Finally, Mozart soundcards are also supported, due to some kind of
        compatibility between OPTi 82C928 and OAK OTI601 chips (see mozart.c
        in the ALSA sources).
        I was not able to get the on-board cdrom interface working with a
        Mozart soundcard (broken board?). Unfortunately I have no documentation
        about the OTI601 chip, so any suggestion is welcome.
*/

/*
    Module options:
        snd_port        codec base i/o port: 0x530,0xe80,0xf40,0x604
        snd_irq         WSS irq num: 5*,7,9,10,11 (*: 82C930+ only) 
        snd_dma1        WSS dma1 num: 0,1,3
        snd_dma2        WSS dma2 num: 0,1
        snd_dma1_size   dma1 size in kB: 8,16,32,64
        snd_dma2_size   dma2 size in kB: 8,16,32,64
        snd_mpu_port    mpu-401 base i/o port: 0x300,0x310,0x320,0x330
        snd_mpu_irq     mpu-401 irq num: 5,7,9,10
        cd_if           cdrom interface: disabled,ide1,ide2,mitsumi,panasonic,
                        sony
        cd_port         cdrom i/o port: 0x320,0x330,0x340,0x360
        cd_irq          cdrom irq num: -1,5,7,9,10,11          (-1 disables)
        cd_drq          cdrom drq num: -1,0,1,3                (-1 disables)
        game            game port: enabled,disabled

    Default values:
        snd_xxx         auto (probed and configured)
        cd_if           disabled
        cd_port         0x340
        cd_irq          disabled
        cd_drq          disabled
        game            disabled

    Examples:
        modprobe snd-opti9xx snd_irq=9
        modprobe snd-opti9xx cd_port=0x360 game=enabled snd_port=0xe80
        modprobe snd-opti9xx snd_port=0xf40 snd_irq=9 snd_dma2=1
        modprobe snd-opti9xx snd_mpu_port=0x310 snd_irq=10
*/

#define OPTi9XX_VERSION		"v2.0a 22apr99"

#define OPTi9XX_DEBUG
#undef OPTi9XX_DEBUG_MC

#define __SND_OSS_COMPAT	/* why not? */
#define SND_MAIN_OBJECT_FILE
#include "driver.h"
#include "ad1848.h"
#include "cs4231.h"
#include "initval.h"
#include "mpu401.h"
#include "opl3.h"

#ifndef ESUCCESS
#define ESUCCESS	0
#endif	/* ESUCCESS */

#define opti9xx_printk( args... )	snd_printk( "opti9xx: " ##args )

/* on-board interfaces configuration section */
/* cdrom type selection */
static struct {
	char *name;
	unsigned char bits;
} cd_if_table[] = {
	{ "disabled",	0x00 },		/* all */
	{ "ide1",	0x05 },		/* c930 */
	{ "ide2",	0x04 },		/* c924, c925, c930, c931 */
	{ "mitsumi",	0x02 },		/* c924, c928, c929 */
	{ "panasonic",	0x03 },		/* c924, c928, c929 */
	{ "sony",	0x01 }		/* c924, c928, c929 */
};

/* cdrom ioports (Sony, Mitsumi or Panasonic interfaces only) */
static struct {
	int port;
	unsigned char bits;
} cd_port_table[] = {
	{ 0x320,	0x03 },		
	{ 0x330,	0x01 },
	{ 0x340,	0x00 },		
	{ 0x360,	0x02 }
};

/* cdrom irq (Sony, Mitsumi, Panasonic) */
static struct {
	int irq;
	unsigned char bits;
} cd_irq_table[] = {
	{ -1,	0x00 },		/* disabled */
	{ 5,	0x01 },
	{ 7,	0x02 },
	{ 9,	0x04 },
	{ 10,	0x05 },
	{ 11,	0x06 }
};

/* cdrom drq (Sony, Mitsumi, Panasonic) */
static struct {
	int drq;
	unsigned char bits;
} cd_drq_table[] = {
	{ -1,	0x03 },		/* disabled */
	{ 0,	0x01 },
	{ 1,	0x02 },
	{ 3,	0x00 }
};

/* game port */
static struct {
	char *name;
	unsigned char bits;
} game_en_table[] = {
	{ "enabled",	0x00 },	
	{ "disabled",	0x01 }
};

/* OPTi chips supported */
enum chipsets {
	c928 = 0,	c929 = 1,	c924 = 2,	c925 = 3,
	c930 = 4,	c931 = 5,	c933 = 6,	unsupported = c933
};

/* chip-specific parameters */
static struct {
	char *name;
	unsigned char pwd;
	int mc_base;
	char mc_regs;
	char mc_size;
	int pwd_reg;
} chip_info[] = {
	{ "82C928",		0xe2,	0xf8d,	6,	6,	2 },
	{ "82C929",		0xe3,	0xf8d,	6,	6,	2 },
	{ "82C924",		0xe5,	0xf8d,	12,	9,	2 },
	{ "82C925",		0xe5,	0xf8d,	14,	9,	2 },
	{ "82C930",		0xe4,	0xf8f,	12,	1,	0 },
	{ "82C931",		0xe4,	0xf8d,	26,	1,	0 },
	{ "unsupported",	0x00,	0x000,	0,	0,	0 }
};

#define OPTi9XX_CHIP		chip_info[opti9xx_chip].name
#define OPTi9XX_MC_BASE		chip_info[opti9xx_chip].mc_base
#define OPTi9XX_MC_REGS		chip_info[opti9xx_chip].mc_regs
#define OPTi9XX_MC_SIZE		chip_info[opti9xx_chip].mc_size
#define OPTi9XX_PASSWD		chip_info[opti9xx_chip].pwd

#define OPTi9XX_MC(x)		x
#define OPTi92X_MC_REG(x)	OPTi9XX_MC_BASE + OPTi9XX_MC(x - 1)
#define OPTi9XX_PWD_REG		OPTi9XX_MC_BASE + \
				chip_info[opti9xx_chip].pwd_reg

#define OPTi924_MC_INDEX_IREG	OPTi92X_MC_REG(8)
#define OPTi924_MC_DATA_IREG	OPTi92X_MC_REG(9)
#define OPTi924_MC_DREGS	7

#define OPTi93X_MC_INDEX_IREG	opti93x_mc_index
#define OPTi93X_MC_DATA_IREG	OPTi93X_MC_INDEX_IREG + 1
#define OPTi93X_MC_IREGS	2

#define OPTi9XX_ENABLE_MPU	0x80
#define OPTi9XX_ENABLE_WSS	0x80

#define OPTi93X_ENABLE_PWD	0x00
#define OPTi93X_INDEX_BITS	((OPTi93X_MC_INDEX_IREG & (1 << 8)) >> 4)|\
				((OPTi93X_MC_INDEX_IREG & (0xf0)) >> 4)

#define WSS_CONFIG_PORT		snd_port
#define WSS_CONFIG_SIZE		4
#define WSS_BASE_PORT		WSS_CONFIG_PORT + WSS_CONFIG_SIZE
#define WSS_STATUS_PORT		WSS_BASE_PORT + 2
#define WSS_SIZE		4

#define OPTi9XX_OPL3_LPORT	snd_opl_port
#define OPTi9XX_OPL3_RPORT	OPTi9XX_OPL3_LPORT + 2

#define OPTi9XX_HW_AD1848	0
#define OPTi9XX_HW_CS4231	1

#define MPU401_TOTAL_SIZE	2
#define OPL3_TOTAL_SIZE		4
#define WSS_TOTAL_SIZE		WSS_SIZE + WSS_CONFIG_SIZE

static int wss_dma1_bits[] = {
	1,	2,	0,	3
};

static int wss_irq_bits[] = {
	0,	0,	0,	0,	0,	5,
	0,	1,	0,	2,	3,	4
};

static int wss_valid_ports[] = {
	0x530,	0xe80,	0xf40,	0x604
};

static int wss_valid_dma1[] = {
	3,	1,	0,	-1
};

static int wss_valid_dma2[][2] = {
	{ 1,	-1 },
	{ 0,	-1 },
	{ -1, 	-1 },
	{ 0,	-1 }
};

static int wss_valid_irqs[] = {
	5,	/* 82C93x based soundcards only */
	9,	10,	11,
	7,	/* parallel port, troubles with several mother boards */
	-1
};

static int mpu_irq_bits[] = {
	0,	0,	0,	0,	0,	2,
	0,	3,	0,	0,	1
};

static int mpu_valid_ports[] = {
	0x330,	0x320,	0x310,	0x300
};

static int mpu_valid_irqs[] = {
	5,	9,	10,
	7,	/* parallel port, troubles with several mother boards */
	-1
};

/* WSS configuration (module options) */
int snd_dma1 = SND_AUTO_DMA;
int snd_dma2 = SND_AUTO_DMA;
int snd_dma1_size = SND_DEFAULT_DMA_SIZE1;
int snd_dma2_size = SND_DEFAULT_DMA_SIZE1;
char *snd_id = SND_DEFAULT_STR1;
int snd_index = SND_DEFAULT_IDX1;
int snd_irq = SND_AUTO_IRQ;
int snd_opl_port = 0x388;
int snd_mpu_irq = SND_AUTO_IRQ;
int snd_mpu_port = SND_AUTO_PORT;
int snd_port = SND_AUTO_PORT;

/* cdrom drive and game port configuration section (module options) */
int cd_drq = -1;
char *cd_if = "disabled";
int cd_irq = -1;
int cd_port = 0x340;
/* default: game port disabled (i/o conflict with an ibmtr adapter) */
char *game = "disabled";

#ifdef MODULE_PARM
MODULE_AUTHOR("Massimo Piccioni <piccio@caronte.csr.unibo.it>");
MODULE_DESCRIPTION("OPTi 82C9xx based soundcards support for Linux.");
/* cdrom interface configuration */
MODULE_PARM(cd_drq, "i");
MODULE_PARM_DESC(cd_drq, "cdrom drive dma number");
MODULE_PARM(cd_irq, "i");
MODULE_PARM_DESC(cd_irq, "cdrom drive irq number");
MODULE_PARM(cd_port, "i");
MODULE_PARM_DESC(cd_port, "cdrom drive interface port");
MODULE_PARM(cd_if, "s");
MODULE_PARM_DESC(cd_if, "cdrom drive interface type");
/* game port configuration */
MODULE_PARM(game, "s");
MODULE_PARM_DESC(game, "game port enable");
/* WSS configuration */
MODULE_PARM(snd_dma1, "i");
MODULE_PARM_DESC(snd_dma1, "WSS dma1 number");
MODULE_PARM(snd_dma1_size, "i");
MODULE_PARM_DESC(snd_dma1_size, "WSS dma1 size in kB");
MODULE_PARM(snd_dma2, "i");
MODULE_PARM_DESC(snd_dma2, "WSS dma2 number");
MODULE_PARM(snd_dma2_size, "i");
MODULE_PARM_DESC(snd_dma2_size, "WSS dma2 size in kB");
MODULE_PARM(snd_irq, "i");
MODULE_PARM_DESC(snd_irq, "WSS irq number");
MODULE_PARM(snd_port, "i");
MODULE_PARM_DESC(snd_port, "sound base i/o port");
/* MPU-401 configuration */
MODULE_PARM(snd_mpu_irq, "i");
MODULE_PARM_DESC(snd_mpu_irq, "MPU-401 irq number");
MODULE_PARM(snd_mpu_port, "i");
MODULE_PARM_DESC(snd_mpu_port, "MPU-401 base i/o port");
/* OPL3 configuration */
MODULE_PARM(snd_opl_port, "i");
MODULE_PARM_DESC(snd_opl_port, "OPL3 base i/o port");
#endif	/* MODULE_PARM */

/* internal data */
snd_card_t *opti9xx_card = NULL;
int opti9xx_chip = unsupported;

snd_dma_t *snd_dma1_ptr = NULL;
snd_dma_t *snd_dma2_ptr = NULL;
snd_irq_t *snd_irq_ptr = NULL;
snd_rawmidi_t *snd_midi = NULL;
snd_kmixer_t *snd_mixer = NULL;
snd_irq_t *snd_mpu_irq_ptr = NULL;
snd_synth_t *snd_opl3 = NULL;
snd_pcm_t *snd_pcm = NULL;

unsigned char cd_drq_sel = 0xff;
unsigned char cd_if_sel = 0xff;
unsigned char cd_irq_sel = 0xff;
unsigned int cd_port_sel = 0xff;
unsigned char game_sel = 0xff;

#ifdef CONFIG_ISAPNP
int opti9xx_isapnp_id = -1;
struct isapnp_dev *opti9xx_isapnp_dev = NULL;

static unsigned int snd_opti9xx_isapnp_table[] = {
	/* c924 */
	c924,
	(ISAPNP_VENDOR('O','P','T')<<16|ISAPNP_DEVICE(0x0924)),    /* device */
	(ISAPNP_VENDOR('O','P','T')<<16|ISAPNP_FUNCTION(0x0000)),  /* audio  */
	(ISAPNP_VENDOR('O','P','T')<<16|ISAPNP_FUNCTION(0x0001)),  /* game   */
	(ISAPNP_VENDOR('O','P','T')<<16|ISAPNP_FUNCTION(0x0002)),  /* mpu401 */
	(ISAPNP_VENDOR('O','P','T')<<16|ISAPNP_FUNCTION(0x0003)),  /* cdrom  */
	(ISAPNP_VENDOR('O','P','T')<<16|ISAPNP_FUNCTION(0x0004)),  /* modem  */
	(ISAPNP_VENDOR('O','P','T')<<16|ISAPNP_FUNCTION(0x0005)),  /* master */
	0,
	/* c931 */
	c931,
	(ISAPNP_VENDOR('O','P','T')<<16|ISAPNP_DEVICE(0x0931)),    /* device */
	(ISAPNP_VENDOR('O','P','T')<<16|ISAPNP_FUNCTION(0x9310)),  /* audio  */
	(ISAPNP_VENDOR('O','P','T')<<16|ISAPNP_FUNCTION(0x0001)),  /* game   */
	(ISAPNP_VENDOR('O','P','T')<<16|ISAPNP_FUNCTION(0x0002)),  /* mpu401 */
	(ISAPNP_VENDOR('O','P','T')<<16|ISAPNP_FUNCTION(0xffff)),  /* aux0   */
	0,
	0	/* that's all */
};
#endif	/* CONFIG_ISAPNP */

int opti93x_mc_index = 0xe0e;		/* 0x[ef][0..f]e are valid values */ 

void (*opti9xx_wss_irq_ptr)(snd_pcm_t *pcm, unsigned char status) = NULL;


void snd_opti9xx_use_inc(snd_card_t *card)
{
	MOD_INC_USE_COUNT;	/* wow, someone is using this driver! */
}

void snd_opti9xx_use_dec(snd_card_t *card)
{
	MOD_DEC_USE_COUNT;	/* see you soon ;) */
}

/* oops with my own 82C931 based PnP card */
void snd_opti9xx_dummy_interrupt(snd_pcm_t *pcm, unsigned char status)
{
#ifdef OPTi9XX_DEBUG
	opti9xx_printk("snd_opti9xx_dummy_interrupt(...) called\n");
#endif	/* OPTi9XX_DEBUG */
}

/* mpu irq handling routine */
void snd_opti9xx_mpu_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
	snd_mpu401_uart_interrupt(snd_midi);
}

/* wss irq handling routine, see card-ad1848.c or card-cs4231.c */
void snd_opti9xx_wss_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
	register unsigned char status;

	if ((status = inb(WSS_STATUS_PORT)) & 0x01)
		opti9xx_wss_irq_ptr(snd_pcm, status);
}


unsigned char snd_opti9xx_read(unsigned int idx)
{
	unsigned long flags;
	unsigned char retval = 0xff;

	if (idx > OPTi9XX_MC_REGS)
		return retval;

	snd_cli(&flags);

	outb(OPTi9XX_PASSWD, OPTi9XX_PWD_REG);
	switch (opti9xx_chip) {
	case c924:
	case c925:
		if (idx > OPTi924_MC_DREGS) {
			outb(idx, OPTi924_MC_INDEX_IREG);
			outb(OPTi9XX_PASSWD, OPTi9XX_PWD_REG);
			retval = inb(OPTi924_MC_DATA_IREG);
			break;
		}
	case c928:
	case c929:
		retval = inb(OPTi92X_MC_REG(idx));
		break;
	case c930:
	case c931:
		outb(idx, OPTi93X_MC_INDEX_IREG);
		outb(OPTi9XX_PASSWD, OPTi9XX_PWD_REG);
		retval = inb(OPTi93X_MC_DATA_IREG);
		break;
	default:	/* uff */
	}

	snd_sti(&flags);

#ifdef OPTi9XX_DEBUG_MC
	opti9xx_printk("0x%x read from MC%d reg\n", retval, idx);
#endif	/* OPTi9XX_DEBUG_MC */

	return retval;
}

void snd_opti9xx_write(unsigned int idx, unsigned char value)
{
	unsigned long flags;

	if (idx > OPTi9XX_MC_REGS)
		return;

	snd_cli(&flags);

	outb(OPTi9XX_PASSWD, OPTi9XX_PWD_REG);
	switch (opti9xx_chip) {
	case c924:
	case c925:
		if (idx > OPTi924_MC_DREGS) {
			outb(idx, OPTi924_MC_INDEX_IREG);
			outb(OPTi9XX_PASSWD, OPTi9XX_PWD_REG);
			outb(value, OPTi924_MC_DATA_IREG);
			break;
		}
	case c928:
	case c929:
		outb(value, OPTi92X_MC_REG(idx));
		break;
	case c930:
	case c931:
		outb(idx, OPTi93X_MC_INDEX_IREG);
		outb(OPTi9XX_PASSWD, OPTi9XX_PWD_REG);
		outb(value, OPTi93X_MC_DATA_IREG);
		break;
	default:	/* uff */
	}

	snd_sti(&flags);

#ifdef OPTi9XX_DEBUG_MC
	opti9xx_printk("written 0x%x to MC%d reg\n", value, idx);
#endif	/* OPTi9XX_DEBUG_MC */
}

#define snd_opti9xx_conf(idx, value, mask) \
	snd_opti9xx_write(idx, \
		(snd_opti9xx_read(idx) & ~(mask)) | ((value) & (mask)) \
	)

#ifdef OPTi9XX_DEBUG
void snd_opti9xx_dump(void)
{
	int i;

	opti9xx_printk("dumping mc registers:");

	for (i = 0; i < OPTi9XX_MC_REGS; i++) {
		if (!(i % 10)) {
			printk("\n");
			opti9xx_printk("");
		}

		printk("0x%2.2x ", snd_opti9xx_read(i + 1));
	}

	printk("\n");
}
#endif	/* OPTi9XX_DEBUG */


int snd_opti9xx_config_fix_codec_delay(int codec)
{
	if (opti9xx_chip >= c930)
		return -ENOSYS;

	if ((codec < OPTi9XX_HW_AD1848) || (codec > OPTi9XX_HW_CS4231))
		return -EINVAL;
	
	snd_opti9xx_conf(OPTi9XX_MC(5), codec << 1, 0x02);
	return ESUCCESS;
}


int snd_opti9xx_resources_mpu401(void)
{
	int i = -1, error;
	int requested_irq[] = { snd_mpu_irq, -1 };

	if (snd_mpu_port == SND_AUTO_PORT) {
		for (i = 0; i < 4; i++)
			if (!snd_register_ioport(opti9xx_card,
				mpu_valid_ports[i], MPU401_TOTAL_SIZE,
				"opti9xx - mpu401", NULL)) {
				snd_mpu_port = mpu_valid_ports[i];
				break;
			}
		if (snd_mpu_port == SND_AUTO_PORT)
			return -EBUSY;
	}
	else if (snd_register_ioport(opti9xx_card,
		snd_mpu_port, MPU401_TOTAL_SIZE,
		"opti9xx - mpu401", NULL))
		return -EBUSY;

	if ((error = snd_register_interrupt(opti9xx_card,
		"opti9xx - mpu401",
		snd_mpu_irq, SND_IRQ_TYPE_ISA, snd_opti9xx_mpu_interrupt,
		NULL,
		(snd_mpu_irq == SND_AUTO_IRQ) ?  mpu_valid_irqs : requested_irq,
		&snd_mpu_irq_ptr)))
		return error;

	snd_mpu_irq = snd_mpu_irq_ptr->irq;

#ifdef CONFIG_ISAPNP
	if (opti9xx_isapnp_dev)
		return ESUCCESS;
#endif	/* CONFIG_ISAPNP */

	if (i < 0) {
		for (i = 0; i < 4; i++)
			if (snd_mpu_port == mpu_valid_ports[i])
				break;
		if (i == 4)
			return -EINVAL;
	}

	snd_opti9xx_conf(OPTi9XX_MC(6), OPTi9XX_ENABLE_MPU |
		(i << 5) | (mpu_irq_bits[snd_mpu_irq] << 3),
		0xf8);

	return ESUCCESS;
}

int snd_opti9xx_resources_wss_base(void)
{
	int i = -1;

	if (snd_port == SND_AUTO_PORT) {
		for (i = 0; i < 4; i++)
			if (!snd_register_ioport(opti9xx_card,
				wss_valid_ports[i], WSS_TOTAL_SIZE,
				"opti9xx - wss", NULL)) {
				snd_port = wss_valid_ports[i];
				break;
			}
		if (snd_port == SND_AUTO_PORT)
			return -EBUSY;
	}
	else if (snd_register_ioport(opti9xx_card,
		snd_port, WSS_TOTAL_SIZE,
		"opti9xx - wss", NULL))
		return -EBUSY;

#ifdef CONFIG_ISAPNP
	if (opti9xx_isapnp_dev)
		return ESUCCESS;
#endif	/* CONFIG_ISAPNP */

	if (i < 0) {
		for (i = 0; i < 4; i++)
			if (snd_port == wss_valid_ports[i])
				break;
		if (i == 4)
			return -EINVAL;
	}

	snd_opti9xx_conf(OPTi9XX_MC(1), i << 4, 0x30);

	return ESUCCESS;
}

int snd_opti9xx_resources_wss_dma1(void)
{
	int error;
	int requested_dma[] = { snd_dma1, -1 };

	if ((error = snd_register_dma_channel(opti9xx_card,
		"opti9xx - wss",
		snd_dma1, SND_DMA_TYPE_ISA, snd_dma1_size,
		(snd_dma1 == SND_AUTO_DMA) ?
			wss_valid_dma1 :
			requested_dma,
		&snd_dma1_ptr)))
		return error;

	snd_dma1 = snd_dma1_ptr->dma;

	return ESUCCESS;
}

int snd_opti9xx_resources_wss_dma2(void)
{
	int error;
	int requested_dma[] = { snd_dma2, -1 };

	if ((error = snd_register_dma_channel(opti9xx_card,
		"opti9xx - wss",
		snd_dma2, SND_DMA_TYPE_ISA, snd_dma2_size,
		(snd_dma2 == SND_AUTO_DMA) ?
			wss_valid_dma2[snd_dma1] :
			requested_dma,
		&snd_dma2_ptr)))
		return error;

	snd_dma2 = snd_dma2_ptr->dma;

	return ESUCCESS;
}

int snd_opti9xx_resources_wss_irq(void)
{
	int error;
	int requested_irq[] = { snd_irq, -1 };

	if ((error = snd_register_interrupt(opti9xx_card,
		"opti9xx - wss",
		snd_irq, SND_IRQ_TYPE_ISA, snd_opti9xx_wss_interrupt,
		NULL,
		(snd_irq == SND_DEFAULT_IRQ1) ?
			wss_valid_irqs + ((opti9xx_chip < c930) ? 1 : 0) :
			requested_irq,
		&snd_irq_ptr)))
		return error;

	snd_irq = snd_irq_ptr->irq;

	return ESUCCESS;
}


int snd_opti9xx_register_ad1848(void)
{
	int error;

	snd_unregister_dma_channels(opti9xx_card);
	if ((error = snd_opti9xx_resources_wss_dma1()))
		return error;

#ifdef CONFIG_ISAPNP
	if (!opti9xx_isapnp_dev)
#endif	/* CONFIG_ISAPNP */
		outb(wss_irq_bits[snd_irq] << 3 | wss_dma1_bits[snd_dma1],
			WSS_CONFIG_PORT);

	snd_pcm = snd_ad1848_new_device(opti9xx_card,
		WSS_BASE_PORT,
		snd_irq_ptr,
		snd_dma1_ptr,
		AD1848_HW_DETECT);

	if (!snd_pcm)
		return -ENODEV;

#ifdef OPTi9XX_DEBUG
	opti9xx_printk("found an AD1848 compatible device at 0x%x, ",
		WSS_BASE_PORT);
	printk("irq %d, dma %d.\n", snd_irq, snd_dma1);
#endif	/* OPTi9XX_DEBUG */

	if ((error = snd_pcm_register(snd_pcm, 0)))
		return error;

	opti9xx_wss_irq_ptr = snd_ad1848_interrupt;

	snd_opti9xx_config_fix_codec_delay(OPTi9XX_HW_AD1848);

	return ESUCCESS;
}

int snd_opti9xx_register_cs4231(void)
{
	int error;

	if ((error = snd_opti9xx_resources_wss_dma1()))
		return error;

	if ((error = snd_opti9xx_resources_wss_dma2()))
		return error;

#ifdef CONFIG_ISAPNP
	if (!opti9xx_isapnp_dev)
#endif	/* CONFIG_ISAPNP */
		outb(wss_irq_bits[snd_irq] << 3 |
			wss_dma1_bits[snd_dma1] |
			0x04,
			WSS_CONFIG_PORT);

	snd_pcm = snd_cs4231_new_device(opti9xx_card,
		WSS_BASE_PORT,
		snd_irq_ptr,
		snd_dma1_ptr, snd_dma2_ptr,
		(opti9xx_chip < c930) ? CS4231_HW_DETECT : CS4231_HW_CS4231,
		0);

	if (!snd_pcm)
		return -ENODEV;

#ifdef OPTi9XX_DEBUG
	opti9xx_printk("found a CS4231 compatible device at 0x%x, ",
		WSS_BASE_PORT);
	printk("irq %d, dma %d,%d.\n", snd_irq, snd_dma1, snd_dma2);
#endif	/* OPTi9XX_DEBUG */

	if ((error = snd_pcm_register(snd_pcm, 0)))
		return error;

	opti9xx_wss_irq_ptr = snd_cs4231_interrupt;

	snd_opti9xx_config_fix_codec_delay(OPTi9XX_HW_CS4231);

	return ESUCCESS;
}

int snd_opti9xx_register_mpu401(void)
{
	int error;

	if (opti9xx_chip < c929)
		return -ENODEV;

	if ((error = snd_opti9xx_resources_mpu401()))
		return error;

	snd_midi = snd_mpu401_uart_new_device(opti9xx_card,
		MPU401_HW_MPU401,
		snd_mpu_port,
		snd_mpu_irq);

	if (!snd_midi)
		return -ENODEV;

#ifdef OPTi9XX_DEBUG
	opti9xx_printk("found a MPU-401 compatible device at 0x%x, irq %d.\n",
		snd_mpu_port, snd_mpu_irq);
#endif	/* OPTi9XX_DEBUG */

	if ((error = snd_rawmidi_register(snd_midi, 0)))
		return error;

	return ESUCCESS;
}

int snd_opti9xx_register_opl3(void)
{
	int error;

	if ((error = snd_register_ioport(opti9xx_card,
		OPTi9XX_OPL3_LPORT, OPL3_TOTAL_SIZE,
		"opti9xx - opl3", NULL)))
		return error;

	snd_opl3 = snd_opl3_new_device(opti9xx_card,
		OPTi9XX_OPL3_LPORT,
		OPTi9XX_OPL3_RPORT,
		OPL3_HW_OPL3, 0);

	if (!snd_opl3)
		return -ENODEV;

#ifdef OPTi9XX_DEBUG
	opti9xx_printk("found an OPL3 compatible device at 0x%x.\n",
		OPTi9XX_OPL3_LPORT);
#endif	/* OPTi9XX_DEBUG */

	if ((error = snd_synth_register(snd_opl3)))
		return error;

	return ESUCCESS;
}

int snd_opti9xx_register_wss(void)
{
	int error;

	if ((error = snd_opti9xx_resources_wss_base()))
		return error;

	if ((error = snd_opti9xx_resources_wss_irq()))
		return error;

	if (snd_opti9xx_register_cs4231())
		return snd_opti9xx_register_ad1848();

	return ESUCCESS;
}

int snd_opti9xx_register(void)
{
	int error;

	if ((error = snd_opti9xx_register_wss()))
		return error;

	snd_mixer =(opti9xx_wss_irq_ptr == snd_ad1848_interrupt) ?
		snd_ad1848_new_mixer(snd_pcm, 0) :
		snd_cs4231_new_mixer(snd_pcm, 0);

	if (!snd_mixer)
		return -ENXIO;

	if ((error = snd_mixer_register(snd_mixer, 0)))
		return error;

	if ((error = snd_opti9xx_register_opl3()))
		/* return error; */	/* not needed */
		;

	if ((error = snd_opti9xx_register_mpu401()))
		/* return error; */	/* not needed */
		;

	strcpy(opti9xx_card->abbreviation, OPTi9XX_CHIP);
	sprintf(opti9xx_card->shortname,
		"OPTi %s based soundcard", OPTi9XX_CHIP);
	sprintf(opti9xx_card->longname,
		"OPTi %s based soundcard, WSS at 0x%x, irq %i, dma %i,%i",
		OPTi9XX_CHIP, WSS_BASE_PORT, snd_irq, snd_dma1, snd_dma2);

	opti9xx_card->type=SND_CARD_TYPE_OPTI9XX;

	if (snd_card_register(opti9xx_card))
		return -ENOMEM;

#ifdef OPTi9XX_DEBUG
	snd_opti9xx_dump();
#endif	/* OPTi9XX_DEBUG */
	return ESUCCESS;
}


void snd_opti9xx_config_isa(void)
{
	/* WSS enabled, SB disabled */
	if (opti9xx_chip < c930)
		snd_opti9xx_conf(OPTi9XX_MC(1), OPTi9XX_ENABLE_WSS, 0x80);
	else
		snd_opti9xx_conf(OPTi9XX_MC(6), OPTi9XX_ENABLE_WSS >> 6, 0x03);

	/* cdrom i/f and game port configuration */
	snd_opti9xx_conf(OPTi9XX_MC(1), (cd_if_sel << 1) | game_sel, 0x0f);
	if (opti9xx_chip < c925)
		snd_opti9xx_conf(OPTi9XX_MC(2),
			(cd_port_sel << 6) | (cd_irq_sel << 2) | cd_drq_sel,
			0xdf);

	switch (opti9xx_chip) {
	case c924:
	case c925:
		snd_opti9xx_conf(OPTi9XX_MC(2), 0x00, 0x20);
		snd_opti9xx_conf(OPTi9XX_MC(3), 0xf0, 0xff);
		snd_opti9xx_conf(OPTi9XX_MC(4), 0x00, 0x0c);
		break;
	case c928:
	case c929:
		snd_opti9xx_conf(OPTi9XX_MC(2), 0x00, 0x20);
		snd_opti9xx_conf(OPTi9XX_MC(3), 0xa2, 0xae);
		snd_opti9xx_conf(OPTi9XX_MC(4), 0x00, 0x0c);
		break;
	case c930:
	case c931:
		snd_opti9xx_conf(OPTi9XX_MC(3), 0x00, 0xff);
		snd_opti9xx_conf(OPTi9XX_MC(4), 0x10, 0x3c);
		snd_opti9xx_conf(OPTi9XX_MC(5), 0x00, 0xbf);
		/* snd_opti9xx_conf(OPTi9XX_MC(5), 0x20, 0xbf); */
		break;
	}
}

#ifdef CONFIG_ISAPNP
int snd_opti9xx_config_pnp(void)
{
	int error;
	unsigned int tmp;
	struct isapnp_logdev *logdev;
	struct isapnp_config cfg;

	/* normal configuration needed */
	snd_opti9xx_config_isa();

	/* audio device */
	tmp = snd_opti9xx_isapnp_table[opti9xx_isapnp_id + 1];
	logdev = isapnp_find_logdev(opti9xx_isapnp_dev, tmp >> 16,
		tmp & 0xffff, 0); 
	if (!logdev)
		return -EAGAIN;

	if ((error = isapnp_cfg_begin(logdev->dev->csn, logdev->number)))
		return error;

	if ((error = isapnp_config_init(&cfg, logdev))) {
		isapnp_cfg_end();
		return error;
	}

#define	PORT_OFFSET	(opti9xx_chip == c924) ? 1 : 0
	if (snd_port != SND_AUTO_PORT)
		cfg.port[0 + PORT_OFFSET] = WSS_BASE_PORT;
	if (snd_irq != SND_AUTO_IRQ)
		cfg.irq[0] = snd_irq;
	if (snd_dma1 != SND_AUTO_DMA)
		cfg.dma[0] = snd_dma1;
	if (snd_dma2 != SND_AUTO_DMA)
		cfg.dma[1] = snd_dma2;
	if (snd_opl_port != SND_AUTO_PORT)
		cfg.port[1 + PORT_OFFSET] = snd_opl_port;

	if ((error = isapnp_configure(&cfg))) {
		isapnp_cfg_end();
		return error;
	}

	snd_port = cfg.port[0 + PORT_OFFSET] - WSS_CONFIG_SIZE;
	snd_irq = cfg.irq[0];
	snd_dma1 = cfg.dma[0];
	snd_dma2 = cfg.dma[1];
	snd_opl_port = cfg.port[1 + PORT_OFFSET];
	if (opti9xx_chip > c930)
		OPTi93X_MC_INDEX_IREG = cfg.port[3] + 2;

	isapnp_activate(logdev->number);
	isapnp_cfg_end();

	/* game port */
	tmp = snd_opti9xx_isapnp_table[opti9xx_isapnp_id + 2];
	logdev = isapnp_find_logdev(opti9xx_isapnp_dev, tmp >> 16,
		tmp & 0xffff, 0); 
	if (logdev) {
		if ((error = isapnp_cfg_begin(logdev->dev->csn, logdev->number)))
			return error;

		if (!game_sel)
			isapnp_activate(logdev->number);
		else
			isapnp_deactivate(logdev->number);

		isapnp_cfg_end();
	}

	/* mpu-401 */
	tmp = snd_opti9xx_isapnp_table[opti9xx_isapnp_id + 3];
	logdev = isapnp_find_logdev(opti9xx_isapnp_dev, tmp >> 16,
		tmp & 0xffff, 0); 
	if (!logdev)
		return -EAGAIN;

	if ((error = isapnp_cfg_begin(logdev->dev->csn, logdev->number)))
		return error;

	if ((error = isapnp_config_init(&cfg, logdev))) {
		isapnp_cfg_end();
		return error;
	}

	if (snd_mpu_port != SND_AUTO_PORT)
		cfg.port[0] = snd_mpu_port;
	if (snd_mpu_irq != SND_AUTO_IRQ)
		cfg.irq[0] = snd_mpu_irq;

	if ((error = isapnp_configure(&cfg))) {
		isapnp_cfg_end();
		return error;
	}

	snd_mpu_port = cfg.port[0];
	snd_mpu_irq = cfg.irq[0];

	isapnp_activate(logdev->number);
	isapnp_cfg_end();
	return ESUCCESS;
}
#endif	/* CONFIG_ISAPNP */


int snd_opti92x_detect_isa(void)
{
	for (opti9xx_chip = c928; opti9xx_chip < c930; opti9xx_chip++) {
		unsigned char value, check;

		if (snd_register_ioport(opti9xx_card,
			OPTi9XX_MC_BASE, OPTi9XX_MC_SIZE,
			"opti92x - mc regs", NULL))
			continue;

		value = snd_opti9xx_read(OPTi9XX_MC(1));
		check = inb(OPTi9XX_MC_BASE + OPTi9XX_MC(1));
		if ((value != 0xff) && (value != check))
			if (value == snd_opti9xx_read(OPTi9XX_MC(1)))
				return ESUCCESS;

		snd_unregister_ioports(opti9xx_card);
	}

	opti9xx_chip = unsupported;
	return -ENODEV;
}

int snd_opti93x_detect_isa(void)
{
	int error = -ENODEV;
	int hb, lb;

	for (hb = 0x00; (hb < 0x02) && error; hb++)
		for (lb = 0x00; (lb < 0x10) && error; lb++) {
			opti93x_mc_index = 0xe0e + (hb << 8) + (lb << 4);

			error = snd_register_ioport(opti9xx_card,
				OPTi93X_MC_INDEX_IREG, OPTi93X_MC_IREGS,
				"opti93x - mc regs", NULL);
		}

	if (error)
		return error;

	/* the 82C931 chip may appear as an 82C930 */
	for (opti9xx_chip = c931; opti9xx_chip >= c930; opti9xx_chip--) {
		unsigned char value, check;
		int i;

		if (snd_register_ioport(opti9xx_card,
			OPTi9XX_MC_BASE, OPTi9XX_MC_SIZE,
			"opti93x - mc regs", NULL))
			continue;

		outb(OPTi9XX_PASSWD, OPTi9XX_PWD_REG);
		outb(OPTi93X_ENABLE_PWD | OPTi93X_INDEX_BITS,
			OPTi9XX_MC_BASE);

		value = snd_opti9xx_read(OPTi9XX_MC(7));
		check = ~value;
		snd_opti9xx_write(OPTi9XX_MC(7), check);

		if (snd_opti9xx_read(OPTi9XX_MC(7)) == check) {
			for (i = 1; i <= OPTi9XX_MC_REGS; i++) {
				value = snd_opti9xx_read(OPTi9XX_MC(i));
				check = snd_opti9xx_read(OPTi9XX_MC(i +
					OPTi9XX_MC_REGS));

				if ((value != 0xff) && (check != 0xff))
					break;
			}

			if (i > OPTi9XX_MC_REGS)
				return ESUCCESS;
		}

		snd_unregister_ioports(opti9xx_card);
		if ((error = snd_register_ioport(opti9xx_card,
			OPTi93X_MC_INDEX_IREG, OPTi93X_MC_IREGS,
			"opti93x - mc regs", NULL)))
			return error;
	}

	opti9xx_chip = unsupported;
	return -ENODEV;
}

int snd_opti9xx_detect_isa(void)
{
	if (snd_opti92x_detect_isa() && snd_opti93x_detect_isa()) {
#ifdef OPTi9XX_DEBUG
		opti9xx_printk("cannot find any OPTi based ISA soundcard\n");
#endif	/* OPTi9XX_DEBUG */
		return -ENODEV;
	}

#ifdef OPTi9XX_DEBUG
	opti9xx_printk("found an OPTi %s chip (ISA) at 0x%x\n", OPTi9XX_CHIP,
		OPTi9XX_MC_BASE);
	snd_opti9xx_dump();
#endif	/* OPTi9XX_DEBUG */

	return ESUCCESS;
}

#ifdef CONFIG_ISAPNP
int snd_opti9xx_detect_pnp(void)
{
	int i;
	unsigned int tmp;

	for (i = 0; (opti9xx_chip = tmp = snd_opti9xx_isapnp_table[i++]);) {
		tmp = snd_opti9xx_isapnp_table[i++];

		if ((opti9xx_isapnp_dev = isapnp_find_device(tmp >> 16,
			tmp & 0xffff, 0))) {
			opti9xx_isapnp_id = i - 1;
#ifdef OPTi9XX_DEBUG
			opti9xx_printk("found an OPTi %s chip (PnP)\n",
				OPTi9XX_CHIP);
			snd_opti9xx_dump();
#endif	/* OPTi9XX_DEBUG */
			return ESUCCESS;
		}

		while (snd_opti9xx_isapnp_table[i++]);
	}

#ifdef OPTi9XX_DEBUG
	opti9xx_printk("cannot find any OPTi based PnP soundcard\n");
#endif	/* OPTi9XX_DEBUG */
	return -ENODEV;
}
#endif	/* CONFIG_ISAPNP */


int snd_opti9xx_probe(void)
{
	int error;
	opti9xx_wss_irq_ptr = snd_opti9xx_dummy_interrupt;

	opti9xx_card = snd_card_new(snd_index, snd_id,
		snd_opti9xx_use_inc,
		snd_opti9xx_use_dec);

	if (!opti9xx_card)
		return -ENOMEM;

#ifdef CONFIG_ISAPNP
	if (!(error = snd_opti9xx_detect_pnp()))
		error = snd_opti9xx_config_pnp();
	else
#else
	opti9xx_printk("uhm ... PnP not configured\n");
#endif	/* CONFIG_ISAPNP */

	if (!(error = snd_opti9xx_detect_isa()))
		snd_opti9xx_config_isa();

	if (!error)
		return snd_opti9xx_register();

	return error;
}


int snd_opti9xx_parse_options(void)
{
	int i;

	for (i = 0; i < 6; i++)
		if (!strcmp(cd_if_table[i].name, cd_if))
			break;
	if (i == 6)
		return -EINVAL;
	cd_if_sel = cd_if_table[i].bits;

	for (i = 0; i < 4; i++)
		if (cd_port_table[i].port == cd_port)
			break;
	if (i == 4)
		return -EINVAL;
	cd_port_sel = cd_port_table[i].bits;

	for (i = 0; i < 6; i++)
		if (cd_irq_table[i].irq == cd_irq)
			break;
	if (i == 6)
		return -EINVAL;
	cd_irq_sel = cd_irq_table[i].bits;

	for (i = 0; i < 4; i++)
		if (cd_drq_table[i].drq == cd_drq)
			break;
	if (i == 4)
		return -EINVAL;
	cd_drq_sel = cd_drq_table[i].bits;

	for (i = 0; i < 2; i++)
		if (!strcmp(game_en_table[i].name, game))
			break;
	if (i == 2)
		return -EINVAL;
	game_sel = game_en_table[i].bits;

	return ESUCCESS;
}


void cleanup_module(void)
{
	if (snd_midi) {
		snd_rawmidi_unregister(snd_midi);
		snd_rawmidi_free(snd_midi);
	}

	if (snd_opl3) {
		snd_synth_unregister(snd_opl3);
		snd_synth_free(snd_opl3);
	}

	if (snd_mixer) {
		snd_mixer_unregister(snd_mixer);
		snd_mixer_free(snd_mixer);
	}

	if (snd_pcm) {
		snd_pcm_unregister(snd_pcm);
		snd_pcm_free(snd_pcm);
	}

	if (opti9xx_card) {
		snd_card_unregister(opti9xx_card);
		snd_card_free(opti9xx_card);
	}
}

int init_module(void)
{
	int error;

#ifdef OPTi9XX_DEBUG
	opti9xx_printk("opti9xx.c: %s ISA/PnP\n", OPTi9XX_VERSION);
#endif	/* OPTi9XX_DEBUG */

	if ((error = snd_opti9xx_parse_options()))
		opti9xx_printk("please check module options\n");
	else if ((error = snd_opti9xx_probe()))
		 cleanup_module();

	return error;
}

