/*
	linux/kernel/blk_drv/mcd.c - Mitsumi CDROM driver

	Copyright (C) 1992  Martin Harriss

	martin@bdsi.com

	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, 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.

	HISTORY

	0.1	First attempt - internal use only
	0.2	Cleaned up delays and use of timer - alpha release
	0.3	Audio support added
	0.3.1 Changes for mitsumi CRMC LU005S march version
		   (stud11@cc4.kuleuven.ac.be)
        0.3.2 bug fixes to the ioclts and merged with ALPHA0.99-pl12
		   (Jon Tombs <jon@robots.ox.ac.uk>)
        0.3.3 Added more #defines and mcd_setup()
   		   (Jon Tombs <jon@gtex02.us.es>)

	October 1993 Bernd Huebner and Ruediger Helsch, Unifix Software GmbH,
	Braunschweig, Germany: Total rework to speed up data read operation.
	Also enabled definition of irq and address from bootstrap, using the
	environment. linux/init/main.c must be patched to export the env.
	November 93 added code for FX001 S,D (single & double speed).
	February 94 added code for broken M 5/6 series of 16-bit single speed.
*/

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

#include <linux/errno.h>
#include <linux/signal.h>
#include <linux/sched.h>
#include <linux/timer.h>
#include <linux/fs.h>
#include <linux/kernel.h>
#include <linux/cdrom.h>
#include <linux/ioport.h>
#include <linux/string.h>
#include <linux/delay.h>
#include <linux/mm.h>


/* #define REALLY_SLOW_IO  */
#include <asm/system.h>
#include <asm/io.h>
#include <asm/segment.h>

#define MAJOR_NR MITSUMI_CDROM_MAJOR
#include </usr/src/linux/drivers/block/blk.h>
#include "unifix.h"

#if 0
static int mcd_sizes[] = { 0 };
#endif

static int mcdPresent = 0;

#if 0
#define TEST1 /* <int-..> */
#define TEST2 /* do_mcd_req */
#define TEST3 */ /* MCD_S_state */
#define TEST4 /* QUICK_LOOP-counter */
#define TEST5 */ /* port(1) state */
#endif


#if 1
#define QUICK_LOOP_DELAY udelay(45)  /* use udelay */
#define QUICK_LOOP_COUNT 20
#else
#define QUICK_LOOP_DELAY
#define QUICK_LOOP_COUNT 140 /* better wait constant time */
#endif
/* #define DOUBLE_QUICK_ONLY */

#define CURRENT_VALID \
  (CURRENT && MAJOR(CURRENT -> dev) == MAJOR_NR && CURRENT -> cmd == READ \
   && CURRENT -> sector != -1)
#define MFL_STATUSorDATA (MFL_STATUS | MFL_DATA)
#define MCD_BUF_SIZ 16
static volatile int mcd_transfer_is_active;
static char mcd_buf[2048*MCD_BUF_SIZ];	/* buffer for block size conversion */
static volatile int mcd_buf_bn[MCD_BUF_SIZ], mcd_next_bn;
static volatile int mcd_buf_in, mcd_buf_out = -1;
static volatile int mcd_error;
static int mcd_open_count;

#ifdef MODULE
int init_module(void);
void cleanup_module(void);
#endif

enum mcd_state_e {
  MCD_S_IDLE,   /* 0 */
  MCD_S_START,  /* 1 */
  MCD_S_MODE, /* 2 */
  MCD_S_READ,   /* 3 */
  MCD_S_DATA,   /* 4 */
  MCD_S_STOP,   /* 5 */
  MCD_S_STOPPING /* 6 */
};
static volatile enum mcd_state_e mcd_state = MCD_S_IDLE;
static int mcd_mode = -1;
static int MCMD_DATA_READ= MCMD_PLAY_READ;
#define READ_TIMEOUT 3000
#define WORK_AROUND_MITSUMI_BUG_92
#define WORK_AROUND_MITSUMI_BUG_93
#ifdef WORK_AROUND_MITSUMI_BUG_93
int mitsumi_bug_93_wait = 0;
#endif /* WORK_AROUND_MITSUMI_BUG_93 */

static short mcd_port = MCD_BASE_ADDR;
static int   mcd_irq  = MCD_INTR_NR;

static int McdTimeout, McdTries;
static struct wait_queue *mcd_waitq = NULL;

static struct mcd_DiskInfo DiskInfo;
static struct mcd_Toc Toc[MAX_TRACKS];
static struct mcd_Play_msf mcd_Play;

static int audioStatus;
static char mcdDiskChanged;
static char tocUpToDate;
static char mcdVersion;

static void mcd_transfer(void);
static void mcd_poll(void);
static void mcd_invalidate_buffers(void);
static void do_mcd_request(void);
static void hsg2msf(long hsg, struct msf *msf);
static void bin2bcd(unsigned char *p);
static int bcd2bin(unsigned char bcd);
static int mcdStatus(void);
static void sendMcdCmd(int cmd, struct mcd_Play_msf *params);
static int getMcdStatus(int timeout);
static int GetQChannelInfo(struct mcd_Toc *qp);
static int updateToc(void);
static int GetDiskInfo(void);
static int GetToc(void);
static int getValue(unsigned char *result);


void mcd_setup(char *str, int *ints)
{
   if (ints[0] > 0)
      mcd_port = ints[1];
   if (ints[0] > 1)
      mcd_irq  = ints[2];
#ifdef WORK_AROUND_MITSUMI_BUG_93
   if (ints[0] > 2)
      mitsumi_bug_93_wait = ints[3];
#endif /* WORK_AROUND_MITSUMI_BUG_93 */
}


int
check_mcd_media_change(int full_dev, int flag)
{
   int retval, target;


#if 1	 /* the below is not reliable */
   return 0;
#endif
   target = MINOR(full_dev);

   if (target > 0) {
      printk("mcd: Mitsumi CD-ROM request error: invalid device.\n");
      return 0;
   }

   retval = mcdDiskChanged;
   if (!flag)
   {
      mcdDiskChanged = 0;
   }

   return retval;
}


/*
 * Do a 'get status' command and get the result.  Only use from the top half
 * because it calls 'getMcdStatus' which sleeps.
 */

static int
statusCmd(void)
{
	int st, retry;

	for (retry = 0; retry < MCD_RETRY_ATTEMPTS; retry++)
	{

		outb(MCMD_GET_STATUS, MCDPORT(0));	/* send get-status cmd */
		st = getMcdStatus(MCD_STATUS_DELAY);
		if (st != -1)
			break;
	}

	return st;
}


/*
 * Send a 'Play' command and get the status.  Use only from the top half.
 */

static int
mcdPlay(struct mcd_Play_msf *arg)
{
	int retry, st;

	for (retry = 0; retry < MCD_RETRY_ATTEMPTS; retry++)
	{
		sendMcdCmd(MCMD_PLAY_READ, arg);
		st = getMcdStatus(2 * MCD_STATUS_DELAY);
		if (st != -1)
			break;
	}

	return st;
}


long
msf2hsg(struct msf *mp)
{
	return bcd2bin(mp -> frame)
		+ bcd2bin(mp -> sec) * 75
		+ bcd2bin(mp -> min) * 4500
		- 150;
}


static int
mcd_ioctl(struct inode *ip, struct file *fp, unsigned int cmd,
						unsigned long arg)
{
	int i, st;
	struct mcd_Toc qInfo;
	struct cdrom_ti ti;
	struct cdrom_tochdr tocHdr;
	struct cdrom_msf msf;
	struct cdrom_tocentry entry;
	struct mcd_Toc *tocPtr;
	struct cdrom_subchnl subchnl;
	struct cdrom_volctrl volctrl;

	if (!ip)
		return -EINVAL;

	st = statusCmd();
	if (st < 0)
		return -EIO;

	if (!tocUpToDate)
	{
		i = updateToc();
		if (i < 0)
			return i;	/* error reading TOC */
	}

	switch (cmd)
	{
	case CDROMSTART:     /* Spin up the drive */
		/* Don't think we can do this.  Even if we could,
 		 * I think the drive times out and stops after a while
		 * anyway.  For now, ignore it.
		 */

		return 0;

	case CDROMSTOP:      /* Spin down the drive */
		outb(MCMD_STOP, MCDPORT(0));
		i = getMcdStatus(MCD_STATUS_DELAY);

		/* should we do anything if it fails? */

		audioStatus = CDROM_AUDIO_NO_STATUS;
		return 0;

	case CDROMPAUSE:     /* Pause the drive */
		if (audioStatus != CDROM_AUDIO_PLAY)
			return -EINVAL;

		outb(MCMD_STOP, MCDPORT(0));
		i = getMcdStatus(MCD_STATUS_DELAY);

		if (GetQChannelInfo(&qInfo) < 0)
		{
			/* didn't get q channel info */

			audioStatus = CDROM_AUDIO_NO_STATUS;
			return 0;
		}

		mcd_Play.start = qInfo.diskTime;	/* remember restart point */

		audioStatus = CDROM_AUDIO_PAUSED;
		return 0;

	case CDROMRESUME:    /* Play it again, Sam */
		if (audioStatus != CDROM_AUDIO_PAUSED)
			return -EINVAL;

		/* restart the drive at the saved position. */

		i = mcdPlay(&mcd_Play);
		if (i < 0)
		{
			audioStatus = CDROM_AUDIO_ERROR;
			return -EIO;
		}

		audioStatus = CDROM_AUDIO_PLAY;
		return 0;

	case CDROMPLAYTRKIND:     /* Play a track.  This currently ignores index. */

		st = verify_area(VERIFY_READ, (void *) arg, sizeof ti);
		if (st)
			return st;

		memcpy_fromfs(&ti, (void *) arg, sizeof ti);

		if (ti.cdti_trk0 < DiskInfo.first
			|| ti.cdti_trk0 > DiskInfo.last
			|| ti.cdti_trk1 < ti.cdti_trk0)
		{
			return -EINVAL;
		}

		if (ti.cdti_trk1 > DiskInfo.last)
			ti. cdti_trk1 = DiskInfo.last;

		mcd_Play.start = Toc[ti.cdti_trk0].diskTime;
		mcd_Play.end = Toc[ti.cdti_trk1 + 1].diskTime;

#ifdef MCD_DEBUG
printk("play: %02x:%02x.%02x to %02x:%02x.%02x\n",
	mcd_Play.start.min, mcd_Play.start.sec, mcd_Play.start.frame,
	mcd_Play.end.min, mcd_Play.end.sec, mcd_Play.end.frame);
#endif

		i = mcdPlay(&mcd_Play);
		if (i < 0)
		{
			audioStatus = CDROM_AUDIO_ERROR;
			return -EIO;
		}

		audioStatus = CDROM_AUDIO_PLAY;
		return 0;

	case CDROMPLAYMSF:   /* Play starting at the given MSF address. */

		if (audioStatus == CDROM_AUDIO_PLAY) {
		  outb(MCMD_STOP, MCDPORT(0));
		  i = getMcdStatus(MCD_STATUS_DELAY);
		  audioStatus = CDROM_AUDIO_NO_STATUS;
		}

		st = verify_area(VERIFY_READ, (void *) arg, sizeof msf);
		if (st)
			return st;

		memcpy_fromfs(&msf, (void *) arg, sizeof msf);

		/* convert to bcd */

		bin2bcd(&msf.cdmsf_min0);
		bin2bcd(&msf.cdmsf_sec0);
		bin2bcd(&msf.cdmsf_frame0);
		bin2bcd(&msf.cdmsf_min1);
		bin2bcd(&msf.cdmsf_sec1);
		bin2bcd(&msf.cdmsf_frame1);

		mcd_Play.start.min = msf.cdmsf_min0;
		mcd_Play.start.sec = msf.cdmsf_sec0;
		mcd_Play.start.frame = msf.cdmsf_frame0;
		mcd_Play.end.min = msf.cdmsf_min1;
		mcd_Play.end.sec = msf.cdmsf_sec1;
		mcd_Play.end.frame = msf.cdmsf_frame1;

#ifdef MCD_DEBUG
printk("play: %02x:%02x.%02x to %02x:%02x.%02x\n",
mcd_Play.start.min, mcd_Play.start.sec, mcd_Play.start.frame,
mcd_Play.end.min, mcd_Play.end.sec, mcd_Play.end.frame);
#endif

		i = mcdPlay(&mcd_Play);
		if (i < 0)
		{
			audioStatus = CDROM_AUDIO_ERROR;
			return -EIO;
		}

		audioStatus = CDROM_AUDIO_PLAY;
		return 0;

	case CDROMREADTOCHDR:        /* Read the table of contents header */
		st = verify_area(VERIFY_WRITE, (void *) arg, sizeof tocHdr);
		if (st)
			return st;

		tocHdr.cdth_trk0 = DiskInfo.first;
		tocHdr.cdth_trk1 = DiskInfo.last;
		memcpy_tofs((void *) arg, &tocHdr, sizeof tocHdr);
		return 0;

	case CDROMREADTOCENTRY:      /* Read an entry in the table of contents */

		st = verify_area(VERIFY_WRITE, (void *) arg, sizeof entry);
		if (st)
			return st;

		memcpy_fromfs(&entry, (void *) arg, sizeof entry);
		if (entry.cdte_track == CDROM_LEADOUT)
			/* XXX */
			tocPtr = &Toc[DiskInfo.last + 1];

		else if (entry.cdte_track > DiskInfo.last
				|| entry.cdte_track < DiskInfo.first)
			return -EINVAL;

		else
			tocPtr = &Toc[entry.cdte_track];

		entry.cdte_adr = tocPtr -> ctrl_addr;
		entry.cdte_ctrl = tocPtr -> ctrl_addr >> 4;

		if (entry.cdte_format == CDROM_LBA)
			entry.cdte_addr.lba = msf2hsg(&tocPtr -> diskTime);

		else if (entry.cdte_format == CDROM_MSF)
		{
			entry.cdte_addr.msf.minute = bcd2bin(tocPtr -> diskTime.min);
			entry.cdte_addr.msf.second = bcd2bin(tocPtr -> diskTime.sec);
			entry.cdte_addr.msf.frame = bcd2bin(tocPtr -> diskTime.frame);
		}

		else
			return -EINVAL;

		memcpy_tofs((void *) arg, &entry, sizeof entry);
		return 0;

	case CDROMSUBCHNL:   /* Get subchannel info */

		st = verify_area(VERIFY_WRITE, (void *) arg, sizeof subchnl);
		if (st)
			return st;

		memcpy_fromfs(&subchnl, (void *) arg, sizeof subchnl);

		if (GetQChannelInfo(&qInfo) < 0)
			return -EIO;

		subchnl.cdsc_audiostatus = audioStatus;
		subchnl.cdsc_adr = qInfo.ctrl_addr;
		subchnl.cdsc_ctrl = qInfo.ctrl_addr >> 4;
		subchnl.cdsc_trk = bcd2bin(qInfo.track);
		subchnl.cdsc_ind = bcd2bin(qInfo.pointIndex);

		if (subchnl.cdsc_format == CDROM_LBA)
		{
			subchnl.cdsc_absaddr.lba = msf2hsg(&qInfo.diskTime);
			subchnl.cdsc_reladdr.lba = msf2hsg(&qInfo.trackTime);
		}

		else if (subchnl.cdsc_format == CDROM_MSF)
		{
			subchnl.cdsc_absaddr.msf.minute = bcd2bin(qInfo.diskTime.min);
			subchnl.cdsc_absaddr.msf.second = bcd2bin(qInfo.diskTime.sec);
			subchnl.cdsc_absaddr.msf.frame = bcd2bin(qInfo.diskTime.frame);

			subchnl.cdsc_reladdr.msf.minute = bcd2bin(qInfo.trackTime.min);
			subchnl.cdsc_reladdr.msf.second = bcd2bin(qInfo.trackTime.sec);
			subchnl.cdsc_reladdr.msf.frame = bcd2bin(qInfo.trackTime.frame);
		}

		else
			return -EINVAL;

		memcpy_tofs((void *) arg, &subchnl, sizeof subchnl);
		return 0;

	case CDROMVOLCTRL:   /* Volume control */
		st = verify_area(VERIFY_READ, (void *) arg, sizeof(volctrl));
		if (st)
			return st;

		memcpy_fromfs(&volctrl, (char *) arg, sizeof(volctrl));
		outb(MCMD_SET_VOLUME, MCDPORT(0));
		outb(volctrl.channel0, MCDPORT(0));
		outb(255, MCDPORT(0));
		outb(volctrl.channel1, MCDPORT(0));
		outb(255, MCDPORT(0));

		i = getMcdStatus(MCD_STATUS_DELAY);
		if (i < 0)
			return -EIO;

		{
			char a, b, c, d;

			getValue(&a);
			getValue(&b);
			getValue(&c);
			getValue(&d);
		}

		return 0;

	case CDROMEJECT:
 	       /* all drives can atleast stop! */
 		if (audioStatus == CDROM_AUDIO_PLAY) {
 		  outb(MCMD_STOP, MCDPORT(0));
 		  i = getMcdStatus(MCD_STATUS_DELAY);
 		}

 		audioStatus = CDROM_AUDIO_NO_STATUS;

		outb(MCMD_EJECT, MCDPORT(0));
		/*
		 * the status (i) shows failure on all but the FX drives.
		 * But nothing we can do about that in software!
		 * So just read the status and forget it. - Jon.
		 */
 		i = getMcdStatus(MCD_STATUS_DELAY);
		return 0;
	default:
		return -EINVAL;
	}
}


/*
 * Take care of the different block sizes between cdrom and Linux.
 * When Linux gets variable block sizes this will probably go away.
 */

static void
mcd_transfer(void)
{
  if (CURRENT_VALID) {
    while (CURRENT -> nr_sectors) {
      int bn = CURRENT -> sector / 4;
      int i;
      for (i = 0; i < MCD_BUF_SIZ && mcd_buf_bn[i] != bn; ++i)
	;
      if (i < MCD_BUF_SIZ) {
	int offs = (i * 4 + (CURRENT -> sector & 3)) * 512;
	int nr_sectors = 4 - (CURRENT -> sector & 3);
	if (mcd_buf_out != i) {
	  mcd_buf_out = i;
	  if (mcd_buf_bn[i] != bn) {
	    mcd_buf_out = -1;
	    continue;
	  }
	}
	if (nr_sectors > CURRENT -> nr_sectors)
	  nr_sectors = CURRENT -> nr_sectors;
	memcpy(CURRENT -> buffer, mcd_buf + offs, nr_sectors * 512);
	CURRENT -> nr_sectors -= nr_sectors;
	CURRENT -> sector += nr_sectors;
	CURRENT -> buffer += nr_sectors * 512;
      } else {
	mcd_buf_out = -1;
	break;
      }
    }
  }
}


/*
 * We only seem to get interrupts after an error.
 * Just take the interrupt and clear out the status reg.
 */

static void
#if INTERRUPTHAS1_ARG
mcd_interrupt(int unused)
#else
mcd_interrupt(int unused, struct pt_regs* regs)
#endif
{
	int st;

	st = inb(MCDPORT(1)) & 0xFF;
#ifdef TEST1
		printk("<int1-%02X>", st);
#endif
	if (!(st & MFL_STATUS))
	{
		st = inb(MCDPORT(0)) & 0xFF;
#ifdef TEST1
		printk("<int0-%02X>", st);
#endif
		if ((st & 0xFF) != 0xFF)
		  mcd_error = st ? st & 0xFF : -1;
	}
}


static void
do_mcd_request(void)
{
#ifdef TEST2
  printk(" do_mcd_request(%ld+%ld)\n", CURRENT -> sector, CURRENT -> nr_sectors);
#endif
  mcd_transfer_is_active = 1;
  while (CURRENT_VALID) {
    if (CURRENT->bh) {
      if (!CURRENT->bh->b_lock)
	panic(DEVICE_NAME ": block not locked");
    }
    mcd_transfer();
    if (CURRENT -> nr_sectors == 0) {
      end_request(1);
    } else {
      mcd_buf_out = -1;		/* Want to read a block not in buffer */
      if (mcd_state == MCD_S_IDLE) {
	if (!tocUpToDate) {
	  if (updateToc() < 0) {
	    while (CURRENT_VALID)
	      end_request(0);
	    break;
	  }
	}
	mcd_state = MCD_S_START;
	McdTries = 5;
	SET_TIMER(mcd_poll, 1);
      }
      break;
    }
  }
  mcd_transfer_is_active = 0;
#ifdef TEST2
  printk(" do_mcd_request ends\n");
#endif
}



static void
mcd_poll(void)
{
  int st;


  if (mcd_error) {
    if (mcd_error & 0xA5) {
      printk("mcd: I/O error 0x%02x", mcd_error);
      if (mcd_error & 0x80)
	printk(" (Door open)");
      if (mcd_error & 0x20)
	printk(" (Disk changed)");
      if (mcd_error & 0x04)
	printk(" (Read error)");
      printk("\n");
      mcd_invalidate_buffers();
    }
    mcd_error = 0;
    mcd_state = MCD_S_STOP;
  }



 immediatly:
  switch (mcd_state) {



  case MCD_S_IDLE:
#ifdef TEST3
    printk("MCD_S_IDLE\n");
#endif
    return;



  case MCD_S_START:
#ifdef TEST3
    printk("MCD_S_START\n");
#endif

    outb(MCMD_GET_STATUS, MCDPORT(0));
    mcd_state = mcd_mode == 1 ? MCD_S_READ : MCD_S_MODE;
    McdTimeout = 3000;
    break;



  case MCD_S_MODE:
#ifdef TEST3
    printk("MCD_S_MODE\n");
#endif

    if ((st = mcdStatus()) != -1) {

      if (st & MST_DSK_CHG) {
	mcdDiskChanged = 1;
	tocUpToDate = 0;
	mcd_invalidate_buffers();
      }

    set_mode_immediatly:

      if ((st & MST_DOOR_OPEN) || !(st & MST_READY)) {
	mcdDiskChanged = 1;
	tocUpToDate = 0;
	if (mcd_transfer_is_active) {
	  mcd_state = MCD_S_START;
	  goto immediatly;
	}
	printk((st & MST_DOOR_OPEN) ? "mcd: door open\n" : "mcd: disk removed\n");
	mcd_state = MCD_S_IDLE;
	while (CURRENT_VALID)
	  end_request(0);
	return;
      }

      outb(MCMD_SET_MODE, MCDPORT(0));
      outb(1, MCDPORT(0));
      mcd_mode = 1;
      mcd_state = MCD_S_READ;
      McdTimeout = 3000;

    }
    break;



  case MCD_S_READ:
#ifdef TEST3
    printk("MCD_S_READ\n");
#endif

    if ((st = mcdStatus()) != -1) {

      if (st & MST_DSK_CHG) {
	mcdDiskChanged = 1;
	tocUpToDate = 0;
	mcd_invalidate_buffers();
      }

    read_immediatly:

      if ((st & MST_DOOR_OPEN) || !(st & MST_READY)) {
	mcdDiskChanged = 1;
	tocUpToDate = 0;
	if (mcd_transfer_is_active) {
	  mcd_state = MCD_S_START;
	  goto immediatly;
	}
	printk((st & MST_DOOR_OPEN) ? "mcd: door open\n" : "mcd: disk removed\n");
	mcd_state = MCD_S_IDLE;
	while (CURRENT_VALID)
	  end_request(0);
	return;
      }

      if (CURRENT_VALID) {
	struct mcd_Play_msf msf;
	mcd_next_bn = CURRENT -> sector / 4;
	hsg2msf(mcd_next_bn, &msf.start);
	msf.end.min = ~0;
	msf.end.sec = ~0;
	msf.end.frame = ~0;
	sendMcdCmd(MCMD_DATA_READ, &msf);
	mcd_state = MCD_S_DATA;
	McdTimeout = READ_TIMEOUT;
      } else {
	mcd_state = MCD_S_STOP;
	goto immediatly;
      }

    }
    break;


  case MCD_S_DATA:
#ifdef TEST3
    printk("MCD_S_DATA\n");
#endif

    st = inb(MCDPORT(1)) & (MFL_STATUSorDATA);
  data_immediatly:
#ifdef TEST5
    printk("Status %02x\n",st);
#endif
    switch (st) {

    case MFL_DATA:
#ifdef WARN_IF_READ_FAILURE
      if (McdTries == 5)
	printk("mcd: read of block %d failed\n", mcd_next_bn);
#endif
      if (!McdTries--) {
	printk("mcd: read of block %d failed, giving up\n", mcd_next_bn);
	if (mcd_transfer_is_active) {
	  McdTries = 0;
	  break;
	}
	if (CURRENT_VALID)
	  end_request(0);
	McdTries = 5;
      }
      mcd_state = MCD_S_START;
      McdTimeout = READ_TIMEOUT;
      goto immediatly;

    case MFL_STATUSorDATA:
      break;

    default:
      McdTries = 5;
      if (!CURRENT_VALID && mcd_buf_in == mcd_buf_out) {
	mcd_state = MCD_S_STOP;
	goto immediatly;
      }
      mcd_buf_bn[mcd_buf_in] = -1;
      READ_DATA(MCDPORT(0), mcd_buf + 2048 * mcd_buf_in, 2048);
      mcd_buf_bn[mcd_buf_in] = mcd_next_bn++;
      if (mcd_buf_out == -1)
	mcd_buf_out = mcd_buf_in;
      mcd_buf_in = mcd_buf_in + 1 == MCD_BUF_SIZ ? 0 : mcd_buf_in + 1;
      if (!mcd_transfer_is_active) {
	while (CURRENT_VALID) {
	  mcd_transfer();
	  if (CURRENT -> nr_sectors == 0)
	    end_request(1);
	  else
	    break;
	}
      }

      if (CURRENT_VALID
	  && (CURRENT -> sector / 4 < mcd_next_bn ||
	      CURRENT -> sector / 4 > mcd_next_bn + 16)) {
	mcd_state = MCD_S_STOP;
	goto immediatly;
      }
      McdTimeout = READ_TIMEOUT;
#ifdef DOUBLE_QUICK_ONLY
      if (MCMD_DATA_READ != MCMD_PLAY_READ)
#endif
      {
	int count= QUICK_LOOP_COUNT;
	while (count--) {
          QUICK_LOOP_DELAY;
	  if ((st = (inb(MCDPORT(1))) & (MFL_STATUSorDATA)) != (MFL_STATUSorDATA)) {
#   ifdef TEST4
/*	    printk("Quickloop success at %d\n",QUICK_LOOP_COUNT-count); */
	    printk(" %d ",QUICK_LOOP_COUNT-count);
#   endif
	    goto data_immediatly;
	  }
	}
#   ifdef TEST4
/*      printk("Quickloop ended at %d\n",QUICK_LOOP_COUNT); */
	printk("ended ");
#   endif
      }
      break;
    }
    break;



  case MCD_S_STOP:
#ifdef TEST3
    printk("MCD_S_STOP\n");
#endif

#ifdef WORK_AROUND_MITSUMI_BUG_93
    if (!mitsumi_bug_93_wait)
      goto do_not_work_around_mitsumi_bug_93_1;

    McdTimeout = mitsumi_bug_93_wait;
    mcd_state = 9+3+1;
    break;

  case 9+3+1:
    if (McdTimeout)
      break;

  do_not_work_around_mitsumi_bug_93_1:
#endif /* WORK_AROUND_MITSUMI_BUG_93 */

    outb(MCMD_STOP, MCDPORT(0));

#ifdef WORK_AROUND_MITSUMI_BUG_92
    if ((inb(MCDPORT(1)) & MFL_STATUSorDATA) == MFL_STATUS) {
      int i = 4096;
      do {
	inb(MCDPORT(0));
      } while ((inb(MCDPORT(1)) & MFL_STATUSorDATA) == MFL_STATUS && --i);
      outb(MCMD_STOP, MCDPORT(0));
      if ((inb(MCDPORT(1)) & MFL_STATUSorDATA) == MFL_STATUS) {
	i = 4096;
	do {
	  inb(MCDPORT(0));
	} while ((inb(MCDPORT(1)) & MFL_STATUSorDATA) == MFL_STATUS && --i);
	outb(MCMD_STOP, MCDPORT(0));
      }
    }
#endif /* WORK_AROUND_MITSUMI_BUG_92 */

    mcd_state = MCD_S_STOPPING;
    McdTimeout = 1000;
    break;

  case MCD_S_STOPPING:
#ifdef TEST3
    printk("MCD_S_STOPPING\n");
#endif

    if ((st = mcdStatus()) == -1 && McdTimeout)
      break;

    if ((st != -1) && (st & MST_DSK_CHG)) {
      mcdDiskChanged = 1;
      tocUpToDate = 0;
      mcd_invalidate_buffers();
    }

#ifdef WORK_AROUND_MITSUMI_BUG_93
    if (!mitsumi_bug_93_wait)
      goto do_not_work_around_mitsumi_bug_93_2;

    McdTimeout = mitsumi_bug_93_wait;
    mcd_state = 9+3+2;
    break;

  case 9+3+2:
    if (McdTimeout)
      break;

    st = -1;

  do_not_work_around_mitsumi_bug_93_2:
#endif /* WORK_AROUND_MITSUMI_BUG_93 */

#ifdef TEST3
    printk("CURRENT_VALID %d mcd_mode %d\n",
	   CURRENT_VALID, mcd_mode);
#endif

    if (CURRENT_VALID) {
      if (st != -1) {
	if (mcd_mode == 1)
	  goto read_immediatly;
	else
	  goto set_mode_immediatly;
      } else {
	mcd_state = MCD_S_START;
	McdTimeout = 1;
      }
    } else {
      mcd_state = MCD_S_IDLE;
      return;
    }
    break;

  default:
    printk("mcd: invalid state %d\n", mcd_state);
    return;
  }

  if (!McdTimeout--) {
    printk("mcd: timeout in state %d\n", mcd_state);
    mcd_state = MCD_S_STOP;
  }

  SET_TIMER(mcd_poll, 1);
}



static void
mcd_invalidate_buffers(void)
{
  int i;
  for (i = 0; i < MCD_BUF_SIZ; ++i)
    mcd_buf_bn[i] = -1;
  mcd_buf_out = -1;
}


/*
 * Open the device special file.  Check that a disk is in.
 */

int
mcd_open(struct inode *ip, struct file *fp)
{
	int st;

	if (mcdPresent == 0)
		return -ENXIO;			/* no hardware */

	if (!mcd_open_count && mcd_state == MCD_S_IDLE) {

	mcd_invalidate_buffers();

	st = statusCmd();			/* check drive status */
	if (st == -1)
		return -EIO;			/* drive doesn't respond */

	if ((st & MST_READY) == 0)		/* no disk in drive */
	{
		printk("mcd: no disk in drive\n");
		return -EIO;
	}

	if (updateToc() < 0)
		return -EIO;

	}
	++mcd_open_count;
	MOD_INC_USE_COUNT;

	return 0;
}


/*
 * On close, we flush all mcd blocks from the buffer cache.
 */

static void
mcd_release(struct inode * inode, struct file * file)
{
  if (!--mcd_open_count) {
	mcd_invalidate_buffers();
	sync_dev(inode->i_rdev);
	invalidate_buffers(inode -> i_rdev);
  }
	MOD_DEC_USE_COUNT;
}


static struct file_operations mcd_fops = {
	NULL,			/* lseek - default */
	block_read,		/* read - general block-dev read */
	block_write,		/* write - general block-dev write */
	NULL,			/* readdir - bad */
	NULL,			/* select */
	mcd_ioctl,		/* ioctl */
	NULL,			/* mmap */
	mcd_open,		/* open */
	mcd_release,		/* release */
	NULL			/* fsync */
};


/*
 * Test for presence of drive and initialize it.  Called at boot time.
 */

unsigned long
mcd_init(unsigned long mem_start, unsigned long mem_end)
{
	int count;
	unsigned char result[3];

	if (mcd_port <= 0 || mcd_irq <= 0) {
	  printk("skip mcd_init\n");
	  return mem_start;
	}

	printk("mcd=0x%x,%d: ", mcd_port, mcd_irq);

	if (register_blkdev(MAJOR_NR, "mcd", &mcd_fops) != 0)
	{
		printk("Unable to get major %d for Mitsumi CD-ROM\n",
		       MAJOR_NR);
		return mem_start;
	}

        if (check_region(mcd_port, 4)) {
	  printk("Init failed, I/O port (%X) already in use\n",
		 mcd_port);
	  return mem_start;
	}
	
	blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;
	read_ahead[MAJOR_NR] = 4;

	/* check for card */

	outb(0, MCDPORT(1));			/* send reset */
	for (count = 0; count < 2000000; count++)
		(void) inb(MCDPORT(1));		/* delay a bit */

	outb(0x40, MCDPORT(0));			/* send get-stat cmd */
	for (count = 0; count < 2000000; count++)
		if (!(inb(MCDPORT(1)) & MFL_STATUS))
			break;

	if (count >= 2000000) {
		printk("Init failed. No mcd device at 0x%x irq %d\n",
		     mcd_port, mcd_irq);
		return mem_start;
	}
	count = inb(MCDPORT(0));		/* pick up the status */
	
	outb(MCMD_GET_VERSION,MCDPORT(0));
	for(count=0;count<3;count++)
		if(getValue(result+count)) {
			printk("mitsumi get version failed at 0x%d\n",
			       mcd_port);
			return mem_start;
		}	

	if (result[0] == result[1] && result[1] == result[2])
		return mem_start;

	printk("Mitsumi status, type and version : %02X %c %x\n",
	       result[0],result[1],result[2]);

	if (result[1] == 'D') MCMD_DATA_READ= 0xC1;

	mcdVersion=result[2];

	if (mcdVersion >=4)
		outb(4,MCDPORT(2)); 	/* magic happens */

	/* don't get the IRQ until we know for sure the drive is there */

	if (request_irq(mcd_irq, mcd_interrupt,
				SA_INTERRUPT, "mcd")) 
	{
		printk("Unable to get IRQ%d for Mitsumi CD-ROM\n", mcd_irq);
		return mem_start;
	}
	request_region(mcd_port, 4, "unifix cd rom driver");

	outb(MCMD_CONFIG_DRIVE, MCDPORT(0));
	outb(0x02,MCDPORT(0));
	outb(0x00,MCDPORT(0));
	getValue(result);

	outb(MCMD_CONFIG_DRIVE, MCDPORT(0));
	outb(0x10,MCDPORT(0));
	outb(0x04,MCDPORT(0));
	getValue(result);

	mcd_invalidate_buffers();
	mcdPresent = 1;
	printk("\n");
	return mem_start;
}


static void hsg2msf(long hsg, struct msf *msf)
{
	hsg += 150;
	msf -> min = hsg / 4500;
	hsg %= 4500;
	msf -> sec = hsg / 75;
	msf -> frame = hsg % 75;

	bin2bcd(&msf -> min);		/* convert to BCD */
	bin2bcd(&msf -> sec);
	bin2bcd(&msf -> frame);
}


static void
bin2bcd(unsigned char *p)
{
	int u, t;

	u = *p % 10;
	t = *p / 10;
	*p = u | (t << 4);
}

static int
bcd2bin(unsigned char bcd)
{
	return (bcd >> 4) * 10 + (bcd & 0xF);
}


/*
 * See if a status is ready from the drive and return it
 * if it is ready.
 */

static int
mcdStatus(void)
{
	int i;
	int st;

	st = inb(MCDPORT(1)) & MFL_STATUS;
	if (!st)
	{
		i = inb(MCDPORT(0)) & 0xFF;
		return i;
	}
	else
		return -1;
}


/*
 * Send a play or read command to the drive
 */

static void
sendMcdCmd(int cmd, struct mcd_Play_msf *params)
{
	outb(cmd, MCDPORT(0));
	outb(params -> start.min, MCDPORT(0));
	outb(params -> start.sec, MCDPORT(0));
	outb(params -> start.frame, MCDPORT(0));
	outb(params -> end.min, MCDPORT(0));
	outb(params -> end.sec, MCDPORT(0));
	outb(params -> end.frame, MCDPORT(0));
}


/*
 * Timer interrupt routine to test for status ready from the drive.
 * (see the next routine)
 */

static void
mcdStatTimer(void)
{
	if (!(inb(MCDPORT(1)) & MFL_STATUS))
	{
		wake_up(&mcd_waitq);
		return;
	}

	McdTimeout--;
	if (McdTimeout <= 0)
	{
		wake_up(&mcd_waitq);
		return;
	}

	SET_TIMER(mcdStatTimer, 1);
}


/*
 * Wait for a status to be returned from the drive.  The actual test
 * (see routine above) is done by the timer interrupt to avoid
 * excessive rescheduling.
 */

static int
getMcdStatus(int timeout)
{
	int st;

	McdTimeout = timeout;
	SET_TIMER(mcdStatTimer, 1);
	sleep_on(&mcd_waitq);
	if (McdTimeout <= 0)
		return -1;

	st = inb(MCDPORT(0)) & 0xFF;
	if (st == 0xFF)
		return -1;

	if ((st & MST_BUSY) == 0 && audioStatus == CDROM_AUDIO_PLAY)
		/* XXX might be an error? look at q-channel? */
		audioStatus = CDROM_AUDIO_COMPLETED;

	if (st & MST_DSK_CHG)
	{
		mcdDiskChanged = 1;
		tocUpToDate = 0;
		audioStatus = CDROM_AUDIO_NO_STATUS;
	}

	return st;
}


/*
 * Read a value from the drive.  Should return quickly, so a busy wait
 * is used to avoid excessive rescheduling.
 */

static int
getValue(unsigned char *result)
{
	int count;
	int s;

	for (count = 0; count < 2000; count++)
		if (!(inb(MCDPORT(1)) & MFL_STATUS))
			break;

	if (count >= 2000)
	{
		printk("mcd: getValue timeout\n");
		return -1;
	}

	s = inb(MCDPORT(0)) & 0xFF;
	*result = (unsigned char) s;
	return 0;
}


/*
 * Read the current Q-channel info.  Also used for reading the
 * table of contents.
 */

int
GetQChannelInfo(struct mcd_Toc *qp)
{
	unsigned char notUsed;
	int retry;

	for (retry = 0; retry < MCD_RETRY_ATTEMPTS; retry++)
	{
		outb(MCMD_GET_Q_CHANNEL, MCDPORT(0));
		if (getMcdStatus(MCD_STATUS_DELAY) != -1)
			break;
	}

	if (retry >= MCD_RETRY_ATTEMPTS)
		return -1;

	if (getValue(&qp -> ctrl_addr) < 0) return -1;
	if (getValue(&qp -> track) < 0) return -1;
	if (getValue(&qp -> pointIndex) < 0) return -1;
	if (getValue(&qp -> trackTime.min) < 0) return -1;
	if (getValue(&qp -> trackTime.sec) < 0) return -1;
	if (getValue(&qp -> trackTime.frame) < 0) return -1;
	if (getValue(&notUsed) < 0) return -1;
	if (getValue(&qp -> diskTime.min) < 0) return -1;
	if (getValue(&qp -> diskTime.sec) < 0) return -1;
	if (getValue(&qp -> diskTime.frame) < 0) return -1;

	return 0;
}


/*
 * Read the table of contents (TOC) and TOC header if neccessary
 */

static int
updateToc()
{
	if (tocUpToDate)
		return 0;

	if (GetDiskInfo() < 0)
		return -EIO;

	if (GetToc() < 0)
		return -EIO;

	tocUpToDate = 1;
	return 0;
}


/*
 * Read the table of contents header
 */

static int
GetDiskInfo()
{
	int retry;

	for (retry = 0; retry < MCD_RETRY_ATTEMPTS; retry++)
	{
		outb(MCMD_GET_DISK_INFO, MCDPORT(0));
		if (getMcdStatus(MCD_STATUS_DELAY) != -1)
			break;
	}

	if (retry >= MCD_RETRY_ATTEMPTS)
		return -1;

	if (getValue(&DiskInfo.first) < 0) return -1;
	if (getValue(&DiskInfo.last) < 0) return -1;

	DiskInfo.first = bcd2bin(DiskInfo.first);
	DiskInfo.last = bcd2bin(DiskInfo.last);

	if (getValue(&DiskInfo.diskLength.min) < 0) return -1;
	if (getValue(&DiskInfo.diskLength.sec) < 0) return -1;
	if (getValue(&DiskInfo.diskLength.frame) < 0) return -1;
	if (getValue(&DiskInfo.firstTrack.min) < 0) return -1;
	if (getValue(&DiskInfo.firstTrack.sec) < 0) return -1;
	if (getValue(&DiskInfo.firstTrack.frame) < 0) return -1;

#ifdef MCD_DEBUG
printk("Disk Info: first %d last %d length %02x:%02x.%02x first %02x:%02x.%02x\n",
	DiskInfo.first,
	DiskInfo.last,
	DiskInfo.diskLength.min,
	DiskInfo.diskLength.sec,
	DiskInfo.diskLength.frame,
	DiskInfo.firstTrack.min,
	DiskInfo.firstTrack.sec,
	DiskInfo.firstTrack.frame);
#endif

	return 0;
}


/*
 * Read the table of contents (TOC)
 */

static int
GetToc()
{
	int i, px;
	int limit;
	int retry;
	struct mcd_Toc qInfo;

	for (i = 0; i < MAX_TRACKS; i++)
		Toc[i].pointIndex = 0;

	i = DiskInfo.last + 3;

	for (retry = 0; retry < MCD_RETRY_ATTEMPTS; retry++)
	{
		outb(MCMD_STOP, MCDPORT(0));
		if (getMcdStatus(MCD_STATUS_DELAY) != -1)
			break;
	}

	if (retry >= MCD_RETRY_ATTEMPTS)
		return -1;

	for (retry = 0; retry < MCD_RETRY_ATTEMPTS; retry++)
	{
		outb(MCMD_SET_MODE, MCDPORT(0));
		outb(0x05, MCDPORT(0));			/* mode: toc */
		mcd_mode = 0x05;
		if (getMcdStatus(MCD_STATUS_DELAY) != -1)
			break;
	}

	if (retry >= MCD_RETRY_ATTEMPTS)
		return -1;

	for (limit = 300; limit > 0; limit--)
	{
		if (GetQChannelInfo(&qInfo) < 0)
			break;

		px = bcd2bin(qInfo.pointIndex);
		if (px > 0 && px < MAX_TRACKS && qInfo.track == 0)
			if (Toc[px].pointIndex == 0)
			{
				Toc[px] = qInfo;
				i--;
			}

		if (i <= 0)
			break;
	}

	Toc[DiskInfo.last + 1].diskTime = DiskInfo.diskLength;

	for (retry = 0; retry < MCD_RETRY_ATTEMPTS; retry++)
	{
                outb(MCMD_SET_MODE, MCDPORT(0));
                outb(0x01, MCDPORT(0));
		mcd_mode = 1;
                if (getMcdStatus(MCD_STATUS_DELAY) != -1)
                        break;
	}

#ifdef MCD_DEBUG
for (i = 1; i <= DiskInfo.last; i++)
printk("i = %2d ctl-adr = %02X track %2d px %02X %02X:%02X.%02X    %02X:%02X.%02X\n",
i, Toc[i].ctrl_addr, Toc[i].track, Toc[i].pointIndex,
Toc[i].trackTime.min, Toc[i].trackTime.sec, Toc[i].trackTime.frame,
Toc[i].diskTime.min, Toc[i].diskTime.sec, Toc[i].diskTime.frame);
for (i = 100; i < 103; i++)
printk("i = %2d ctl-adr = %02X track %2d px %02X %02X:%02X.%02X    %02X:%02X.%02X\n",
i, Toc[i].ctrl_addr, Toc[i].track, Toc[i].pointIndex,
Toc[i].trackTime.min, Toc[i].trackTime.sec, Toc[i].trackTime.frame,
Toc[i].diskTime.min, Toc[i].diskTime.sec, Toc[i].diskTime.frame);
#endif

	return limit > 0 ? 0 : -1;
}

/**
** Special functions used when the driver is a loadable module.
**/

#define MCD "mcd"

#ifdef MODULE
int init_module (void)
{
	mcd_init (0, 0);
	if (!mcdPresent) {
		printk (MCD ": can't init drive.\n");
		return -EIO;
	}
	return 0;
}

void cleanup_module (void)
{
	printk(MCD ": cleanup_module called\n");

	if (MOD_IN_USE)
		printk (MCD ": device busy, remove delayed\n");

	release_region((unsigned long) mcd_port, 4);

	free_irq (mcd_irq);

	if (unregister_blkdev (MAJOR_NR, MCD) != 0) {
		printk (MCD ": cleanup_module failed.\n");
	} else {
		printk (MCD ": cleanup_module succeeded.\n");
	}
}

#endif /* MODULE */
