/*
 * Purpose: Driver for Creative emu10k1x audio controller
 *
 * This device is usually called as SB Live! 5.1 and it has been used in 
 * some Dell machines. However it has nothing to do with the original
 * SB Live! design.
 */
/*
 * This file is part of Open Sound System
 *
 * Copyright (C) 4Front Technologies 1996-2008.
 *
 * This software is released under the BSD license.
 * See the COPYING file included in the main directory of this source
 * distribution for the license terms and conditions
 */


#include "emu10k1x_cfg.h"
#include "oss_pci.h"
#include "ac97.h"
#include "midi_core.h"
#include "remux.h"

#define PCI_VENDOR_ID_CREATIVE 		0x1102
#define PCI_DEVICE_ID_CREATIVE_EMU10K1X 0x0006

#define USE_DUALBUF

/*
 * Indirect registers
 */

#define PTBA	0x000
#define PTBS	0x001
#define PTCA	0x002
#define PFBA	0x004
#define PFBS	0x005
#define CPFA	0x006
#define PFEA	0x007
#define CPCAV	0x008
#define RFBA	0x010
#define RFBS	0x011
#define CRFA	0x012
#define CRCAV	0x013
#define CDL	0x020
#define CDR	0x030
#define SA	0x040
#define EA_aux	0x041
#define SCS0	0x042
#define SCS1	0x043
#define SCS2	0x044
#define SPC	0x045
#define WMARK	0x046
#define MUDAT	0x047
#define MUCMD	0x048
#define RCD	0x050

/* Interrupt bits
 */

#define INTR_RFF	(1<<19)
#define INTR_RFH	(1<<16)
#define INTR_PFF	(1<<11)
#define INTR_PFH	(1<<8)
#define INTR_EAI	(1<<29)
#define INTR_PCI	1
#define INTR_UART_RX	2
#define INTR_UART_TX	4
#define INTR_AC97	0x10
#define INTR_GPIO	0x40

#define PLAY_INTR_ENABLE (INTR_PFF|INTR_PFH)
#define RECORD_INTR_ENABLE (INTR_RFF|INTR_RFH)

#define BUFSIZE 32*1024
#define MAX_PORTC	3

extern int emu10k1x_spdif_enable;

typedef struct
{
  int audio_dev;
  int port_number;
  int open_mode;
  int trigger_bits;
  int audio_enabled;
  int channels;
  int fmt;
  int speed;
  unsigned char *playbuf, *recbuf;
  oss_native_word playbuf_phys, recbuf_phys;

  int play_cfrag, play_chalf, rec_cfrag, rec_chalf;
}
emu10k1x_portc;

typedef struct
{
  oss_device_t *osdev;
  int loaded;
  oss_native_word base;
  oss_mutex_t mutex;
  oss_mutex_t low_mutex;
  int irq;
  char *card_name;
  unsigned int subvendor;

  int mixer_dev;
  ac97_devc ac97devc;

/*
 * UART
 */
  oss_midi_inputbyte_t midi_input_intr;
  int midi_opened, midi_disabled;
  volatile unsigned char input_byte;
  int midi_dev;
  int mpu_attached;

  emu10k1x_portc portc[MAX_PORTC];

}
emu10k1x_devc;


static void emu10k1xuartintr (emu10k1x_devc * devc);

static unsigned int
read_reg (emu10k1x_devc * devc, int reg, int chn)
{
  oss_native_word flags;
  unsigned int val;

  MUTEX_ENTER_IRQDISABLE (devc->low_mutex, flags);
  OUTL (devc->osdev, (reg << 16) | (chn & 0xffff), devc->base + 0x00);	/* Pointer */
  val = INL (devc->osdev, devc->base + 0x04);	/* Data */
  MUTEX_EXIT_IRQRESTORE (devc->low_mutex, flags);

/* printk("Read reg %03x (ch %d) = %08x\n", reg, chn, val); */
  return val;
}

static void
write_reg (emu10k1x_devc * devc, int reg, int chn, unsigned int value)
{
  oss_native_word flags;

  /* printk("Write reg %03x (ch %d) = %08x\n", reg, chn, value); */
  MUTEX_ENTER_IRQDISABLE (devc->low_mutex, flags);
  OUTL (devc->osdev, (reg << 16) | (chn & 0xffff), devc->base + 0x00);	/* Pointer */
  OUTL (devc->osdev, value, devc->base + 0x04);	/* Data */
  MUTEX_EXIT_IRQRESTORE (devc->low_mutex, flags);
#if 0
  {
    char tmp[100];
    int ok = 1;
    sprintf (tmp, "@w%d %04x/%s %x", chn, reg, emu_regname (reg, &ok), value);
    if (ok)
      oss_do_timing (tmp);
  }
#endif
}

static void
recording_intr (emu10k1x_devc * devc, emu10k1x_portc * portc, int status)
{
#ifdef USE_DUALBUF
  unsigned char *frombuf, *tobuf;
  dmap_p dmap = audio_engines[portc->audio_dev]->dmap_in;
  oss_native_word flags;

  MUTEX_ENTER_IRQDISABLE (devc->mutex, flags);
  /* "Auto sync" the play half counters with the device */
  if (status & INTR_RFH)	/* 1st half completed */
    portc->rec_chalf = 0;	/* Reuse the first half */
  else
    portc->rec_chalf = 1;	/* Reuse the second half */

  tobuf = dmap->dmabuf + (portc->rec_cfrag * dmap->fragment_size);
  frombuf = portc->recbuf + (portc->rec_chalf * dmap->fragment_size);

  memcpy (tobuf, frombuf, dmap->fragment_size);

/* printk("rec %d/%d\n", portc->rec_cfrag, portc->rec_chalf); */
  portc->rec_cfrag = (portc->rec_cfrag + 1) % dmap->nfrags;
  portc->rec_chalf = !portc->rec_chalf;

  MUTEX_EXIT_IRQRESTORE (devc->mutex, flags);
#endif
  oss_audio_inputintr (portc->audio_dev, 0);
}

static void
playback_intr (emu10k1x_devc * devc, emu10k1x_portc * portc,
	       unsigned int status)
{
#ifdef USE_DUALBUF
  dmap_p dmap = audio_engines[portc->audio_dev]->dmap_out;
  unsigned char *frombuf, *tobuf;
  oss_native_word flags;

  MUTEX_ENTER_IRQDISABLE (devc->mutex, flags);
  /* "Auto sync" the play half counters with the device */
  if (status & (INTR_PFH << portc->port_number))	/* 1st half completed */
    {
      portc->play_chalf = 0;	/* Reuse the first half */
    }
  else
    {
      portc->play_chalf = 1;	/* Reuse the second half */
    }


  frombuf = dmap->dmabuf + (portc->play_cfrag * dmap->fragment_size);
  tobuf = portc->playbuf + (portc->play_chalf * dmap->fragment_size);

  memcpy (tobuf, frombuf, dmap->fragment_size);

/* printk("play %d/%d\n", portc->play_cfrag, portc->play_chalf); */
  portc->play_cfrag = (portc->play_cfrag + 1) % dmap->nfrags;
  portc->play_chalf = !portc->play_chalf;

  MUTEX_EXIT_IRQRESTORE (devc->mutex, flags);
#endif
  oss_audio_outputintr (portc->audio_dev, 0);
}

static int
emu10k1xintr (oss_device_t * osdev)
{
  int serviced = 0;
  unsigned int status;
  emu10k1x_devc *devc = (emu10k1x_devc *) osdev->devc;
  int portnum;

  status = INL (devc->osdev, devc->base + 0x08);

  if (status & 0x2)		/* MIDI RX interrupt */
    {
      emu10k1xuartintr (devc);
      serviced = 1;
    }

  if (status & (INTR_PFF | INTR_PFH | INTR_RFF | INTR_RFH))
    {
      for (portnum = 0; portnum < 3; portnum++)
	{
	  emu10k1x_portc *portc = &devc->portc[portnum];

	  if ((portc->trigger_bits & PCM_ENABLE_OUTPUT) &&
	      (status & ((INTR_PFF | INTR_PFH) << portc->port_number)))
	    playback_intr (devc, portc, status);
	  if ((portc->trigger_bits & PCM_ENABLE_INPUT) &&
	      (status & (INTR_RFF | INTR_RFH)))
	    recording_intr (devc, portc, status);

	}
      serviced = 1;
      OUTL (devc->osdev, status, devc->base + 0x08);	/* Acknowledge */
    }
  return serviced;
}

/*ARGSUSED*/
static int
emu10k1x_set_rate (int dev, int arg)
{
  emu10k1x_portc *portc = audio_engines[dev]->portc;

  return portc->speed = 48000;
}

static short
emu10k1x_set_channels (int dev, short arg)
{
  emu10k1x_portc *portc = audio_engines[dev]->portc;

  if (arg == 0)
    return portc->channels;

  if (portc->open_mode & OPEN_READ)
    return portc->channels = 2;

  if (arg != 1 && arg != 2)
    return portc->channels = 2;
  return portc->channels = arg;
}

static unsigned int
emu10k1x_set_format (int dev, unsigned int arg)
{
  emu10k1x_portc *portc = audio_engines[dev]->portc;

  if (arg == 0)
    return portc->fmt;

  if (arg == AFMT_AC3)
    if ((portc->open_mode & OPEN_READ) || !emu10k1x_spdif_enable)
      arg = AFMT_S16_LE;

  if (arg != AFMT_AC3 && arg != AFMT_S16_LE)
    return portc->fmt = AFMT_S16_LE;

  return portc->fmt = arg;
}

/*ARGSUSED*/
static int
emu10k1x_ioctl (int dev, unsigned int cmd, ioctl_arg arg)
{
  return -EINVAL;
}

static void emu10k1x_trigger (int dev, int state);

static void
emu10k1x_reset (int dev)
{
  emu10k1x_trigger (dev, 0);
}

static void
emu10k1x_reset_input (int dev)
{
  emu10k1x_portc *portc = audio_engines[dev]->portc;
  emu10k1x_trigger (dev, portc->trigger_bits & ~PCM_ENABLE_INPUT);
}

static void
emu10k1x_reset_output (int dev)
{
  emu10k1x_portc *portc = audio_engines[dev]->portc;
  emu10k1x_trigger (dev, portc->trigger_bits & ~PCM_ENABLE_OUTPUT);
}

/*ARGSUSED*/
static int
emu10k1x_open (int dev, int mode, int open_flags)
{
  emu10k1x_portc *portc = audio_engines[dev]->portc;
  emu10k1x_devc *devc = audio_engines[dev]->devc;
  oss_native_word flags;

  MUTEX_ENTER_IRQDISABLE (devc->mutex, flags);

  if (portc->open_mode)
    {
      MUTEX_EXIT_IRQRESTORE (devc->mutex, flags);
      return -EBUSY;
    }

  portc->open_mode = mode;
  portc->audio_enabled = ~mode;

  MUTEX_EXIT_IRQRESTORE (devc->mutex, flags);
  return 0;
}

static void
emu10k1x_close (int dev, int mode)
{
  emu10k1x_portc *portc = audio_engines[dev]->portc;

  emu10k1x_trigger (dev, 0);
  portc->open_mode = 0;
  portc->audio_enabled &= ~mode;
}

/*ARGSUSED*/
static void
emu10k1x_output_block (int dev, oss_native_word buf, int count, int fragsize,
		       int intrflag)
{
}

/*ARGSUSED*/
static void
emu10k1x_start_input (int dev, oss_native_word buf, int count, int fragsize,
		      int intrflag)
{
  emu10k1x_portc *portc = audio_engines[dev]->portc;

  portc->audio_enabled |= PCM_ENABLE_INPUT;
  portc->trigger_bits &= ~PCM_ENABLE_INPUT;
}


static void
emu10k1x_trigger (int dev, int state)
{
  emu10k1x_devc *devc = audio_engines[dev]->devc;
  emu10k1x_portc *portc = audio_engines[dev]->portc;
  int tmp;
  oss_native_word flags;

  MUTEX_ENTER_IRQDISABLE (devc->mutex, flags);

  if (portc->open_mode & OPEN_WRITE)
    {
      if (state & PCM_ENABLE_OUTPUT)
	{
	  if ((portc->audio_enabled & PCM_ENABLE_OUTPUT) &&
	      !(portc->trigger_bits & PCM_ENABLE_OUTPUT))
	    {
	      /* Enable play channel and set mono/stereo mode */
	      tmp = read_reg (devc, SA, 0);
	      tmp &= ~(0x10000 << portc->port_number);
	      if (portc->channels == 1)
		tmp |= (0x10000 << portc->port_number);
	      tmp |= 1 << portc->port_number;
	      write_reg (devc, SA, 0, tmp);

	      portc->trigger_bits |= PCM_ENABLE_OUTPUT;
	    }
	}

      else
	{
	  if ((portc->audio_enabled & PCM_ENABLE_OUTPUT) &&
	      (portc->trigger_bits & PCM_ENABLE_OUTPUT))
	    {
	      portc->audio_enabled &= ~PCM_ENABLE_OUTPUT;
	      portc->trigger_bits &= ~PCM_ENABLE_OUTPUT;

	      /* Disable Play channel */
	      tmp = read_reg (devc, SA, 0);
	      tmp &= ~(1 << portc->port_number);
	      write_reg (devc, SA, 0, tmp);
	    }
	}
    }

  if (portc->open_mode & OPEN_READ)
    {
      if (state & PCM_ENABLE_INPUT)
	{
	  if ((portc->audio_enabled & PCM_ENABLE_INPUT) &&
	      !(portc->trigger_bits & PCM_ENABLE_INPUT))
	    {
	      /* Enable Rec Channel */
	      tmp = read_reg (devc, SA, 0);
	      tmp |= 0x100;
	      write_reg (devc, SA, 0, tmp);
	      portc->trigger_bits |= PCM_ENABLE_INPUT;
	    }
	}
      else
	{
	  if ((portc->audio_enabled & PCM_ENABLE_INPUT) &&
	      (portc->trigger_bits & PCM_ENABLE_INPUT))
	    {
	      portc->audio_enabled &= ~PCM_ENABLE_INPUT;
	      portc->trigger_bits &= ~PCM_ENABLE_INPUT;

	      /* disable channel */
	      tmp = read_reg (devc, SA, 0);
	      tmp &= ~0x100;
	      write_reg (devc, SA, 0, tmp);
	    }
	}
    }

  MUTEX_EXIT_IRQRESTORE (devc->mutex, flags);
}

/*ARGSUSED*/
static int
emu10k1x_prepare_for_input (int dev, int bsize, int bcount)
{
  emu10k1x_devc *devc = audio_engines[dev]->devc;
  emu10k1x_portc *portc = audio_engines[dev]->portc;
  dmap_p dmap = audio_engines[dev]->dmap_in;
  oss_native_word flags;

  MUTEX_ENTER_IRQDISABLE (devc->mutex, flags);
#ifndef USE_DUALBUF
  /* Single buffering mode */
  dmap->nfrags = 2;
  dmap->bytes_in_use = dmap->nfrags * dmap->fragment_size;
  write_reg (devc, RFBA, 0, dmap->dmabuf_phys);
  write_reg (devc, RFBS, 0, (dmap->bytes_in_use - 4) << 16);
#else
  write_reg (devc, RFBA, 0, portc->recbuf_phys);
  write_reg (devc, RFBS, 0, (dmap->fragment_size * 2) << 16);
#endif
  memset (portc->recbuf, 0, BUFSIZE);
  portc->rec_cfrag = portc->rec_chalf = 0;
  portc->audio_enabled &= ~PCM_ENABLE_INPUT;
  portc->trigger_bits &= ~PCM_ENABLE_INPUT;
  MUTEX_EXIT_IRQRESTORE (devc->mutex, flags);

  return 0;
}

/*ARGSUSED*/
static int
emu10k1x_prepare_for_output (int dev, int bsize, int bcount)
{
  emu10k1x_devc *devc = audio_engines[dev]->devc;
  emu10k1x_portc *portc = audio_engines[dev]->portc;
  dmap_p dmap = audio_engines[dev]->dmap_out;
  unsigned int tmp;
  oss_native_word flags;

  MUTEX_ENTER_IRQDISABLE (devc->mutex, flags);
  if (portc->fmt == AFMT_AC3)
    portc->channels = 2;

  write_reg (devc, PTBA, portc->port_number, 0);
  write_reg (devc, PTBS, portc->port_number, 0);
  write_reg (devc, PTCA, portc->port_number, 0);

  write_reg (devc, CPFA, portc->port_number, 0);
  write_reg (devc, PFEA, portc->port_number, 0);
  write_reg (devc, CPCAV, portc->port_number, 0);

#ifndef USE_DUALBUF
  /* Single buffering mode */
  dmap->nfrags = 2;
  dmap->bytes_in_use = dmap->nfrags * dmap->fragment_size;
  write_reg (devc, PFBA, portc->port_number, dmap->dmabuf_phys);
  write_reg (devc, PFBS, portc->port_number, (dmap->bytes_in_use - 4) << 16);
#else
  /* Dual buffering mode */
  write_reg (devc, PFBA, portc->port_number, portc->playbuf_phys);
  write_reg (devc, PFBS, portc->port_number, (dmap->fragment_size * 2) << 16);
#endif
  memset (portc->playbuf, 0, BUFSIZE);
  portc->play_cfrag = portc->play_chalf = 0;

  if (portc->fmt == AFMT_AC3)
    {
      tmp = read_reg (devc, EA_aux, 0);
      tmp &= ~(0x03 << (portc->port_number * 2));
      if (portc->port_number == 2)
	tmp &= ~0x10000;
      write_reg (devc, EA_aux, 0, tmp);
      write_reg (devc, SCS0 + portc->port_number, 0, 0x02108506);	/* Data */
    }
  else
    {
      tmp = read_reg (devc, EA_aux, 0);
      tmp |= (0x03 << (portc->port_number * 2));

      if (emu10k1x_spdif_enable == 0)
	if (portc->port_number == 2)
	  tmp |= 0x10000;

      write_reg (devc, EA_aux, 0, tmp);
      write_reg (devc, SCS0 + portc->port_number, 0, 0x02108504);	/* Audio */
    }

  portc->audio_enabled |= PCM_ENABLE_OUTPUT;
  portc->trigger_bits &= ~PCM_ENABLE_OUTPUT;

  MUTEX_EXIT_IRQRESTORE (devc->mutex, flags);
  return 0;
}

static int
emu10k1x_alloc_buffer (int dev, dmap_t * dmap, int direction)
{
  int err;
  emu10k1x_devc *devc = audio_engines[dev]->devc;
  emu10k1x_portc *portc = audio_engines[dev]->portc;
  oss_native_word phaddr;

  if ((err = oss_alloc_dmabuf (dev, dmap, direction)) < 0)
    return err;

  if (direction == OPEN_READ)
    {
      if (portc->port_number == 0)
	{
	  portc->recbuf =
	    CONTIG_MALLOC (devc->osdev, BUFSIZE, MEMLIMIT_32BITS, &phaddr);
	  if (portc->recbuf == NULL)
	    return -ENOMEM;
	  portc->recbuf_phys = phaddr;
	}
    }
  else
    {
      portc->playbuf =
	CONTIG_MALLOC (devc->osdev, BUFSIZE, MEMLIMIT_32BITS, &phaddr);
      if (portc->playbuf == NULL)
	return -ENOMEM;
      portc->playbuf_phys = phaddr;
    }
  return 0;
}

/*ARGSUSED*/
static int
emu10k1x_free_buffer (int dev, dmap_t * dmap, int direction)
{
  emu10k1x_devc *devc = audio_engines[dev]->devc;
  emu10k1x_portc *portc = audio_engines[dev]->portc;

  oss_free_dmabuf (dev, dmap);

  if (portc->playbuf != NULL)
    {
      CONTIG_FREE (devc->osdev, portc->playbuf, BUFSIZE);
      portc->playbuf = NULL;
    }

  if (portc->recbuf != NULL)
    {
      CONTIG_FREE (devc->osdev, portc->recbuf, BUFSIZE);
      portc->recbuf = NULL;
    }

  return 0;
}

#if 0
static int
emu10k1x_get_buffer_pointer (int dev, dmap_t * dmap, int direction)
{
  unsigned int p = 0;

  emu10k1x_devc *devc = audio_engines[dev]->devc;
  emu10k1x_portc *portc = audio_engines[dev]->portc;

  dmap = audio_engines[dev]->dmap_out;
  if (direction == PCM_ENABLE_OUTPUT)
    p = read_reg (devc, CPFA, portc->port_number);

  if (direction == PCM_ENABLE_INPUT)
    p = read_reg (devc, CRFA, portc->port_number);

  p %= (dmap->bytes_in_use - 4);

  return p;
}
#endif

static audiodrv_t emu10k1x_audio_driver = {
  emu10k1x_open,
  emu10k1x_close,
  emu10k1x_output_block,
  emu10k1x_start_input,
  emu10k1x_ioctl,
  emu10k1x_prepare_for_input,
  emu10k1x_prepare_for_output,
  emu10k1x_reset,
  NULL,
  NULL,
  emu10k1x_reset_input,
  emu10k1x_reset_output,
  emu10k1x_trigger,
  emu10k1x_set_rate,
  emu10k1x_set_format,
  emu10k1x_set_channels,
  NULL,
  NULL,
  NULL,
  NULL,
  emu10k1x_alloc_buffer,
  emu10k1x_free_buffer,
  NULL,
  NULL,
  NULL				/*emu10k1x_get_buffer_pointer */
};


#define MUADAT   0x47
#define MUACMD   0x48
#define MUASTAT   0x48

static __inline__ int
emu10k1xuart_status (emu10k1x_devc * devc)
{
  return read_reg (devc, MUASTAT, 0);
}

#define input_avail(devc) (!(emu10k1xuart_status(devc)&INPUT_AVAIL))
#define output_ready(devc)      (!(emu10k1xuart_status(devc)&OUTPUT_READY))
static void
emu10k1xuart_cmd (emu10k1x_devc * devc, unsigned char cmd)
{
  write_reg (devc, MUACMD, 0, cmd);
}

static __inline__ int
emu10k1xuart_read (emu10k1x_devc * devc)
{
  return read_reg (devc, MUADAT, 0);
}

static __inline__ void
emu10k1xuart_write (emu10k1x_devc * devc, unsigned char byte)
{
  write_reg (devc, MUADAT, 0, byte);
}

#define OUTPUT_READY    0x40
#define INPUT_AVAIL     0x80
#define MPU_ACK         0xFE
#define MPU_RESET       0xFF
#define UART_MODE_ON    0x3F

static int reset_emu10k1xuart (emu10k1x_devc * devc);
static void enter_uart_mode (emu10k1x_devc * devc);

static void
emu10k1xuart_input_loop (emu10k1x_devc * devc)
{
  while (input_avail (devc))
    {
      unsigned char c = emu10k1xuart_read (devc);

      if (c == MPU_ACK)
	devc->input_byte = c;
      else if (devc->midi_opened & OPEN_READ && devc->midi_input_intr)
	devc->midi_input_intr (devc->midi_dev, c);
    }
}

static void
emu10k1xuartintr (emu10k1x_devc * devc)
{
  emu10k1xuart_input_loop (devc);
}

/*ARGSUSED*/
static int
emu10k1xuart_open (int dev, int mode, oss_midi_inputbyte_t inputbyte,
		   oss_midi_inputbuf_t inputbuf,
		   oss_midi_outputintr_t outputintr)
{
  emu10k1x_devc *devc = (emu10k1x_devc *) midi_devs[dev]->devc;

  if (devc->midi_opened)
    {
      return -EBUSY;
    }

  while (input_avail (devc))
    emu10k1xuart_read (devc);

  devc->midi_input_intr = inputbyte;
  devc->midi_opened = mode;
  enter_uart_mode (devc);
  devc->midi_disabled = 0;

  return 0;
}

/*ARGSUSED*/
static void
emu10k1xuart_close (int dev, int mode)
{
  emu10k1x_devc *devc = (emu10k1x_devc *) midi_devs[dev]->devc;

  reset_emu10k1xuart (devc);
  oss_udelay (10);
  enter_uart_mode (devc);
  reset_emu10k1xuart (devc);
  devc->midi_opened = 0;
}


static int
emu10k1xuart_out (int dev, unsigned char midi_byte)
{
  int timeout;
  emu10k1x_devc *devc = (emu10k1x_devc *) midi_devs[dev]->devc;
  oss_native_word flags;

  /*
   * Test for input since pending input seems to block the output.
   */

  MUTEX_ENTER_IRQDISABLE (devc->mutex, flags);

  if (input_avail (devc))
    emu10k1xuart_input_loop (devc);

  MUTEX_EXIT_IRQRESTORE (devc->mutex, flags);

  /*
   * Sometimes it takes about 130000 loops before the output becomes ready
   * (After reset). Normally it takes just about 10 loops.
   */

  for (timeout = 130000; timeout > 0 && !output_ready (devc); timeout--);

  if (!output_ready (devc))
    {
      cmn_err (CE_WARN, "UART timeout - Device not responding\n");
      devc->midi_disabled = 1;
      reset_emu10k1xuart (devc);
      enter_uart_mode (devc);
      return 1;
    }

  emu10k1xuart_write (devc, midi_byte);
  return 1;
}

/*ARGSUSED*/
static int
emu10k1xuart_ioctl (int dev, unsigned cmd, ioctl_arg arg)
{
  return -EINVAL;
}

static midi_driver_t emu10k1x_midi_driver = {
  emu10k1xuart_open,
  emu10k1xuart_close,
  emu10k1xuart_ioctl,
  emu10k1xuart_out
};


static void
enter_uart_mode (emu10k1x_devc * devc)
{
  int ok, timeout;
  oss_native_word flags;

  MUTEX_ENTER_IRQDISABLE (devc->mutex, flags);
  for (timeout = 30000; timeout > 0 && !output_ready (devc); timeout--);

  devc->input_byte = 0;
  emu10k1xuart_cmd (devc, UART_MODE_ON);

  ok = 0;
  for (timeout = 50000; timeout > 0 && !ok; timeout--)
    if (devc->input_byte == MPU_ACK)
      ok = 1;
    else if (input_avail (devc))
      if (emu10k1xuart_read (devc) == MPU_ACK)
	ok = 1;

  MUTEX_EXIT_IRQRESTORE (devc->mutex, flags);
}


void
attach_emu10k1xuart (emu10k1x_devc * devc)
{
  enter_uart_mode (devc);

  devc->midi_dev =
    oss_install_mididev (OSS_MIDI_DRIVER_VERSION, "EMU10K1X", "SB P16X UART",
			 &emu10k1x_midi_driver, sizeof (midi_driver_t), NULL,
			 /*&std_midi_synth, */
			 0, devc, devc->osdev);
  devc->midi_opened = 0;
}

static int
reset_emu10k1xuart (emu10k1x_devc * devc)
{
  int ok, timeout, n;

  /*
   * Send the RESET command. Try again if no success at the first time.
   */

  ok = 0;

  for (n = 0; n < 2 && !ok; n++)
    {
      for (timeout = 30000; timeout > 0 && !output_ready (devc); timeout--);

      devc->input_byte = 0;
      emu10k1xuart_cmd (devc, MPU_RESET);

      /*
       * Wait at least 25 msec. This method is not accurate so let's make the
       * loop bit longer. Cannot sleep since this is called during boot.
       */

      for (timeout = 50000; timeout > 0 && !ok; timeout--)
	if (devc->input_byte == MPU_ACK)	/* Interrupt */
	  ok = 1;
	else if (input_avail (devc))
	  if (emu10k1xuart_read (devc) == MPU_ACK)
	    ok = 1;

    }



  if (ok)
    emu10k1xuart_input_loop (devc);	/*
					 * Flush input before enabling interrupts
					 */

  return ok;
}


int
probe_emu10k1xuart (emu10k1x_devc * devc)
{
  int ok = 0;
  oss_native_word flags;

  DDB (cmn_err (CE_CONT, "Entered probe_emu10k1xuart\n"));

  devc->midi_input_intr = NULL;
  devc->midi_opened = 0;
  devc->input_byte = 0;

  MUTEX_ENTER_IRQDISABLE (devc->mutex, flags);
  ok = reset_emu10k1xuart (devc);
  MUTEX_EXIT_IRQRESTORE (devc->mutex, flags);

  if (ok)
    {
      DDB (cmn_err (CE_CONT, "Reset UART401 OK\n"));
    }
  else
    {
      DDB (cmn_err
	   (CE_WARN, "Reset UART401 failed (no hardware present?).\n"));
      DDB (cmn_err
	   (CE_WARN, "mpu401 status %02x\n", emu10k1xuart_status (devc)));
    }

  DDB (cmn_err (CE_WARN, "emu10k1xuart detected OK\n"));
  return ok;
}

void
unload_emu10k1xuart (emu10k1x_devc * devc)
{
  reset_emu10k1xuart (devc);
}


static void
attach_mpu (emu10k1x_devc * devc)
{
  devc->mpu_attached = 1;
  attach_emu10k1xuart (devc);
}


static int
emu10k1x_ac97_read (void *devc_, int wAddr)
{
  emu10k1x_devc *devc = devc_;
  int dtemp = 0, i;
  oss_native_word flags;

  MUTEX_ENTER_IRQDISABLE (devc->low_mutex, flags);
  OUTB (devc->osdev, wAddr, devc->base + 0x1e);
  for (i = 0; i < 10000; i++)
    if (INB (devc->osdev, devc->base + 0x1e) & 0x80)
      break;
  dtemp = INW (devc->osdev, devc->base + 0x1c);
  MUTEX_EXIT_IRQRESTORE (devc->low_mutex, flags);

  return dtemp & 0xffff;
}

static int
emu10k1x_ac97_write (void *devc_, int wAddr, int wData)
{
  emu10k1x_devc *devc = devc_;
  int i;
  oss_native_word flags;

  MUTEX_ENTER_IRQDISABLE (devc->low_mutex, flags);
  OUTB (devc->osdev, wAddr, devc->base + 0x1e);
  for (i = 0; i < 10000; i++)
    if (INB (devc->osdev, devc->base + 0x1e) & 0x80)
      break;
  OUTW (devc->osdev, wData, devc->base + 0x1c);
  MUTEX_EXIT_IRQRESTORE (devc->low_mutex, flags);

  return 0;
}

static const int bindings[MAX_PORTC] = {
  DSP_BIND_FRONT,
  DSP_BIND_SURR,
  DSP_BIND_CENTER_LFE
};

static void
install_audio_devices (emu10k1x_devc * devc)
{
  int i;
  unsigned int tmp;
  int firstdev = -1;
  char name[64];

#if 0
  if (emu10k1x_spdif_enable == 1)
    n = 2;
#endif

  /* Enable play interrupts for all 3 channels */
  for (i = 0; i < MAX_PORTC; i++)
    {
      tmp = INL (devc->osdev, devc->base + 0x0c);
      tmp |= PLAY_INTR_ENABLE << i;
      OUTL (devc->osdev, tmp, devc->base + 0x0c);
    }

  /* Enable record interrupts */
  tmp = INL (devc->osdev, devc->base + 0x0c);
  tmp |= RECORD_INTR_ENABLE;
  OUTL (devc->osdev, tmp, devc->base + 0x0c);

  for (i = 0; i < MAX_PORTC; i++)
    {
      int adev, flags;
      emu10k1x_portc *portc = &devc->portc[i];

      flags = ADEV_AUTOMODE | ADEV_FIXEDRATE | ADEV_16BITONLY | ADEV_COLD;

      switch (i)
	{
	case 0:
	  sprintf (name, "%s (front)", devc->card_name);
	  break;
	case 1:
	  sprintf (name, "%s (surround)", devc->card_name);
	  break;
	case 2:
	  if (emu10k1x_spdif_enable == 1)
	    sprintf (name, "%s (SPDIF)", devc->card_name);
	  else
	    sprintf (name, "%s (center/LFE)", devc->card_name);
	  break;
	}

      if (i == 0)
	flags |= ADEV_DUPLEX;
      else
	flags |= ADEV_NOINPUT;

      if ((adev = oss_install_audiodev (OSS_AUDIO_DRIVER_VERSION,
					devc->osdev,
					devc->osdev,
					name,
					&emu10k1x_audio_driver,
					sizeof (audiodrv_t),
					flags, AFMT_S16_LE | AFMT_AC3, NULL,
					-1)) < 0)
	{
	  return;
	}

      if (i == 0)
	firstdev = adev;
      audio_engines[adev]->portc = portc;
      audio_engines[adev]->devc = devc;
      audio_engines[adev]->mixer_dev = devc->mixer_dev;
      audio_engines[adev]->rate_source = firstdev;
      audio_engines[adev]->min_rate = 48000;
      audio_engines[adev]->max_rate = 48000;
      audio_engines[adev]->caps |= PCM_CAP_FREERATE;
      /*audio_engines[adev]->max_block = BUFSIZE / 2; *//*  Never change this */
      audio_engines[adev]->fixed_rate = 48000;
      audio_engines[adev]->binding = bindings[i];
      audio_engines[adev]->vmix_flags = VMIX_MULTIFRAG;
      portc->audio_dev = adev;
      portc->open_mode = 0;
      portc->port_number = i;
      portc->channels = 2;
      portc->fmt = AFMT_S16_LE;
    }

#ifdef USE_REMUX
  if (firstdev >= 0)
    {
      if (emu10k1x_spdif_enable == 1)
	{
	  sprintf (name, "%s 4.0 output", devc->card_name);
	  remux_install (name, devc->osdev, firstdev, firstdev + 1, -1, -1);
	}
      else
	{
	  sprintf (name, "%s 5.1 output", devc->card_name);
	  remux_install (name, devc->osdev, firstdev, firstdev + 1,
			 firstdev + 2, -1);
	}
    }
#endif
}

static void
select_out3_mode (emu10k1x_devc * devc, int mode)
{
  /*
   * Set the out3/spdif combo jack format.
   * mode0=analog rear/center, 1=spdif
   */

  if (mode == 0)
    {
      write_reg (devc, SPC, 0, 0x00000700);
      write_reg (devc, EA_aux, 0, 0x0001000f);
    }
  else
    {
      write_reg (devc, SPC, 0, 0x00000000);
      write_reg (devc, EA_aux, 0, 0x0000070f);
    }
}

int
emu10k1x_attach (oss_device_t * osdev)
{
  unsigned char pci_irq_line, pci_revision;
  unsigned short pci_command, vendor, device;
  unsigned int pci_ioaddr;
  unsigned int subvendor;
  int err;
  emu10k1x_devc *devc = NULL;

  DDB (cmn_err (CE_WARN, "Entered EMU10K1X probe routine\n"));

  pci_read_config_word (osdev, PCI_VENDOR_ID, &vendor);
  pci_read_config_word (osdev, PCI_DEVICE_ID, &device);

  if (vendor != PCI_VENDOR_ID_CREATIVE ||
      device != PCI_DEVICE_ID_CREATIVE_EMU10K1X)

    return 0;

  pci_read_config_dword (osdev, 0x2c, &subvendor);
  pci_read_config_byte (osdev, PCI_REVISION_ID, &pci_revision);
  pci_read_config_word (osdev, PCI_COMMAND, &pci_command);
  pci_read_config_irq (osdev, PCI_INTERRUPT_LINE, &pci_irq_line);
  pci_read_config_dword (osdev, PCI_BASE_ADDRESS_0, &pci_ioaddr);

  pci_command |= PCI_COMMAND_MASTER | PCI_COMMAND_IO;
  pci_command &= ~(PCI_COMMAND_SERR | PCI_COMMAND_PARITY);
  pci_write_config_word (osdev, PCI_COMMAND, pci_command);

  if (pci_ioaddr == 0)
    {
      cmn_err (CE_WARN, "I/O address not assigned by BIOS.\n");
      return 0;
    }

  if (pci_irq_line == 0)
    {
      cmn_err (CE_WARN, "IRQ not assigned by BIOS.\n");
      return 0;
    }

  if ((devc = PMALLOC (osdev, sizeof (*devc))) == NULL)
    {
      cmn_err (CE_WARN, "Out of memory\n");
      return 0;
    }

  devc->osdev = osdev;
  osdev->devc = devc;
  devc->card_name = "Sound Blaster Live (P16X)";
  devc->subvendor = subvendor;

  devc->base = MAP_PCI_IOADDR (devc->osdev, 0, pci_ioaddr);
  devc->base &= ~0x3;

  devc->irq = pci_irq_line;

  MUTEX_INIT (devc->osdev, devc->mutex, MH_DRV);
  MUTEX_INIT (devc->osdev, devc->low_mutex, MH_DRV + 1);

  oss_register_device (osdev, devc->card_name);

  if ((err =
       oss_register_interrupts (devc->osdev, 0, emu10k1xintr, NULL)) < 0)
    {
      cmn_err (CE_WARN, "Can't register interrupt handler, err=%d\n", err);
      return 0;
    }

/*
 * Init mixer
 */
  devc->mixer_dev = ac97_install (&devc->ac97devc, devc->card_name,
				  emu10k1x_ac97_read, emu10k1x_ac97_write,
				  devc, devc->osdev);
  if (devc->mixer_dev < 0)
    {
      cmn_err (CE_WARN, "Mixer install failed - cannot continue\n");
      return 0;
    }

  write_reg (devc, SCS0, 0, 0x02108504);
  write_reg (devc, SCS1, 0, 0x02108504);
  write_reg (devc, SCS2, 0, 0x02108504);
  select_out3_mode (devc, emu10k1x_spdif_enable);
  OUTL (devc->osdev, 0x00000000, devc->base + 0x18);	/* GPIO */
  OUTL (devc->osdev, INTR_PCI | INTR_UART_RX, devc->base + 0x0c);
  OUTL (devc->osdev, 0x00000009, devc->base + 0x14);	/* Enable audio */
  install_audio_devices (devc);
  attach_mpu (devc);
  return 1;
}

static void
unload_mpu (emu10k1x_devc * devc)
{
  if (devc->mpu_attached)
    {
      unload_emu10k1xuart (devc);
      devc->mpu_attached = 0;
    }
}

int
emu10k1x_detach (oss_device_t * osdev)
{
  emu10k1x_devc *devc = (emu10k1x_devc *) osdev->devc;

  if (oss_disable_device (osdev) < 0)
    return 0;

  write_reg (devc, SA, 0, 0);
  OUTL (devc->osdev, 0x00000000, devc->base + 0x0c);	/* Interrupt disable */
  OUTL (devc->osdev, 0x00000001, devc->base + 0x14);

  unload_mpu (devc);

  oss_unregister_interrupts (devc->osdev);

  MUTEX_CLEANUP (devc->mutex);
  MUTEX_CLEANUP (devc->low_mutex);

  oss_unregister_device (osdev);
  return 1;
}
