/*
 * The Mitsumi CDROM interface
 * Copyright (C) 1995 Heiko Schlittermann
 * $Id: mcd.c,v 1.9 1995/01/28 01:45:27 root Rel $
 * VERSION: 048
 * 
 * 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; see the file COPYING.  If not, write to
 * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * Thanks to
 *  The Linux Community at all and ...
 *	Martin Harris (he wrote the first Mitsumi Driver)
 *  Eberhard Moenkeberg (he gave me much support)
 *  Bernd Huebner, Ruediger Helsch (Unifix-Software Gmbh, they
 *		rewrote the original driver)
 *  John Tombs, Bjorn Ekwall (module support)
 *  Daniel v. Mosnenck (he sent me the Technical and Programming Reference)
 *  ... somebody forgotten?
 *  
 */


#if RCS
static const char *mcd_c_version
		= "$Id: mcd.c,v 1.9 1995/01/28 01:45:27 root Rel $";
#endif

#include <linux/config.h>
#if MODULE
#include <linux/module.h>
#include <linux/version.h> 
#else
#define MOD_INC_USE_COUNT
#define MOD_DEC_USE_COUNT
#define MOD_IN_USE 1
#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/mm.h>
#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 "mcd.h"

/* CONSTANTS *******************************************************/

const int REQUEST_SIZE = 200;
const int DIRECT_SIZE = 200;

/* DECLARATIONS ****************************************************/ 
struct msf {
	unsigned char minute;
	unsigned char second;
	unsigned char frame;
};

/* Info as read by the read toc info command. */
struct s_mcd_toc {
    unsigned char firsttrack;	/* bcd */
    unsigned char lasttrack;	/* bcd */
	struct msf len;				
	struct msf first;
};

/* Per drive/controller stuff **************************************/

struct s_drive_stuff {
	/* waitquenes */
    struct wait_queue *busyq;
    struct wait_queue *lockq;
    struct wait_queue *sleepq;

 	/* flags */
    volatile int introk;	/* status of last irq operation */
    volatile int busy;		/* drive performs an operation */
    volatile int lock;		/* exclusive usage */

	/* cd infos */
    struct s_mcd_toc toc;
	struct msf last_session;
	int xa;					/* 1 if xa disk */
	int multisession;		/* 1 if multisession */

	/* `buffer' control */
    volatile int valid;
    volatile int pending;
    volatile int off_direct;
    volatile int off_requested;

	/* adds and odds */
    char *port;			/* holds the drives base address */
    int irq;			/* irq used by this drive */
    int minor;			/* minor number of this drive */
    int present;		/* 0 none, 1 single speed, 2 double speed */
    char readcmd;		/* read cmd depends on single/double speed */
    unsigned long changed;	/* last jiff the media was changed */
    int users;				/* keeps track of open/close */
    int lastsector;			/* last block accessible */
    int errno;				/* last operation's error */
    int st;					/* last operations status (as in the TR) */

};

/* Prototypes ******************************************************/ 

/*	The following prototypes are already declared elsewhere.  They are
 	repeated here to show what's going on.  And to sense, if they're
	changed elsewhere. */

/* declared in blk.h */
unsigned long mcd_init(unsigned long mem_start, unsigned long mem_end);
void do_mcd_request(void);

/* already declared in fs/isofs */
int check_mcd_media_change(int, int);

/* already declared in init/main */
void mcd_setup(char *, int *);

/*	Indirect exported functions. These functions are exported by their
	addresses, such as mcd_open and mcd_close in the 
	structure fops. */

/* ???  exported by the mcd_sigaction struct */
static void mcd_intr(int, struct pt_regs*);

/* exported by file_ops */
static int mcd_open(struct inode*, struct file*);
static void mcd_close(struct inode*, struct file*);
static int mcd_ioctl(struct inode*, struct file*, unsigned int, unsigned long);

/* misc internal support functions */
static void log2msf(unsigned int, struct msf*);
static unsigned int msf2log(const struct msf*);
static unsigned int uint2bcd(unsigned int);
static unsigned int bcd2uint(unsigned char);
static int minor(int*);
static char *port(int*);
static int irq(int*);
static void mcd_delay(struct s_drive_stuff*, long jifs);
static int mcd_transfer(struct s_drive_stuff*, char* buf, int sector, int nr_sectors);
static int mcd_talk(struct s_drive_stuff*, 
		const char* cmd, char *buffer,
		int size, int timeout);

/* static variables ************************************************/

static struct wait_queue *mcd_modclean = 0;
static struct s_drive_stuff mcd_drive_stuff[MCD_NDRIVES];
static struct s_drive_stuff *mcd_irq_map[16] =
		{0, 0, 0, 0, 0, 0, 0, 0,
		0, 0, 0, 0, 0, 0, 0, 0};

static struct file_operations mcd_fops = {
	NULL,			/* lseek - use kernel default */
	block_read,		/* read - general block-dev read */
	block_write,	/* write - general block-dev write */
	NULL,			/* no readdir */
	NULL,			/* no select */
	mcd_ioctl,		/* ioctl() */
	NULL,			/* no mmap */
	mcd_open,		/* open() */
	mcd_close,		/* close() */
	NULL			/* fsync */
};

/* KERNEL INTERFACE FUNCTIONS **************************************/ 

static int mcd_ioctl(struct inode* ip, struct file* fp, 
		unsigned int cmd, unsigned long arg)
{ 
	struct s_drive_stuff *stuff = &mcd_drive_stuff[MINOR(ip->i_rdev)];

	if (!stuff->present) return -ENXIO;
	if (!ip) return -EINVAL;

#if MCD_DEBUG_IOCTL
		printk(MCD ":: ioctl() ");
	switch (cmd) {
		case CDROMMULTISESSION: printk("MULTISESSION"); break;
		case CDROMEJECT: printk("EJECT"); break;
		default: break;
	}
	printk("\n");
#endif
		
	switch (cmd) {
		case CDROMMULTISESSION: {
			int ans;
			struct cdrom_multisession ms;
			if (0 != (ans = verify_area(VERIFY_READ, (void*) arg, 
					sizeof(struct cdrom_multisession))))
				return ans;
				
			memcpy_fromfs(&ms, (void*) arg, sizeof(struct cdrom_multisession));
			if (ms.addr_format == CDROM_MSF) {
				ms.addr.msf.minute = bcd2uint(stuff->last_session.minute);
				ms.addr.msf.second = bcd2uint(stuff->last_session.second);
				ms.addr.msf.frame = bcd2uint(stuff->last_session.frame);
			} else if (ms.addr_format == CDROM_LBA)
				ms.addr.lba = msf2log(&stuff->last_session);
			else
				return -EINVAL;
			ms.xa_flag = stuff->xa;

			if (0 != (ans = verify_area(VERIFY_WRITE, (void*) arg,
					sizeof(struct cdrom_multisession))))
				return ans;

			memcpy_tofs((void*) arg, &ms, sizeof(struct cdrom_multisession));
#if MCD_DEBUG_IOCTL
			if (ms.addr_format == CDROM_MSF) 
				printk(MCD ":: ioctl() done (%d, %02x:%02x.%02x [%02x:%02x.%02x])\n",
						ms.xa_flag, 
						ms.addr.msf.minute,
						ms.addr.msf.second,
						ms.addr.msf.frame,
						stuff->last_session.minute,
						stuff->last_session.second,
						stuff->last_session.frame);
			else
				printk(MCD ":: ioctl() done (%d, 0x%08x [%02x:%02x.%02x])\n",
						ms.xa_flag,
						ms.addr.lba,
						stuff->last_session.minute,
						stuff->last_session.second,
						stuff->last_session.frame);
#endif
			return 0;
		}

		case CDROMEJECT: {
			char cmd[]  = { MCD_CMD_EJECT };
		    int tries;
		    for (tries = 3; tries; tries--) 
				if (-1 != mcd_talk(stuff, cmd, NULL, 0, 500)) return 0;
		    if (!tries) return -EIO;
		}

		default:
	    	printk(MCD ": ioctl(): unknown request 0x%04x\n", cmd);
	    	return -EINVAL;

	}
}

void do_mcd_request()
{
    int dev;
    struct s_drive_stuff *stuff;

#if MCD_DEBUG_REQUEST
    printk(MCD ":: do_mcd_request() called\n")
#endif

  again:

#if MCD_DEBUG_REQUEST
    printk(MCD ":: do_mcd_request() repeated\n");
#endif

	if ((CURRENT == NULL) || (CURRENT->dev < 0)) return;

    stuff = &mcd_drive_stuff[MINOR(CURRENT->dev)];

    INIT_REQUEST;
    dev = MINOR(CURRENT->dev);

	if ((dev < 0) || (dev >= MCD_NDRIVES) || (stuff->present == 0)) {
		printk(MCD "do_mcd_request(): bad device: 0x%04x\n", CURRENT->dev);
		end_request(0);
		goto again;
    }

    switch (CURRENT->cmd) {
      case WRITE:
	  printk(MCD ": do_mcd_request(): attempt to write to cd!!\n");
	  end_request(0);
	  break;

      case READ:
	  stuff->errno = 0;
	  while (CURRENT->nr_sectors) {
	      int i;

	      if (-1 == (i = mcd_transfer(
				      stuff,
				      CURRENT->buffer,
				      CURRENT->sector,
				      CURRENT->nr_sectors))) {
		  printk(MCD " read error\n");
		  if (stuff->errno == MCD_EOM) {
		      CURRENT->sector += CURRENT->nr_sectors;
		      CURRENT->nr_sectors = 0;
		  }
		  end_request(0);
		  goto again;
	      }
	      CURRENT->sector += i;
	      CURRENT->nr_sectors -= i;
	      CURRENT->buffer += (i * 512);

	  }

	  end_request(1);
	  break;

      default:
	  panic(MCD "do_mcd_request: unknown command.\n");
	  break;
    }

    goto again;
}

static int mcd_open(struct inode *ip, struct file *fp)
{
    struct s_drive_stuff *stuff;
	static unsigned long changed = 0;

#if MCD_DEBUG_OPENCLOSE 
    printk(MCD ":: open()\n");
#endif

    stuff = &mcd_drive_stuff[MINOR(ip->i_rdev)];

    if (!stuff->present) return -ENXIO;

	/* close the door */
	{
		char cmd[] = { MCD_CMD_CLOSE };
		mcd_talk(stuff, cmd, NULL, 0, 500);
	}

    /* get the drives status */
	{
		char cmd[] = { MCD_CMD_REQUEST_MULTI };
		char info[4];
		int tries;

#if MCD_DEBUG_OPENCLOSE
		printk(MCD ":: Request multi\n");
#endif

		for (tries = 6; 
				tries && (-1 == mcd_talk(stuff, cmd, info, 4, 500));
				tries--) 
			;

		if (!tries) return -EIO;

		if (info[0] > 2)
			printk(MCD ": unknown multisession value (%d)\n", info[0]);

		stuff->multisession = ((info[0] == 1) || (info[0] == 2));
		memcpy(&stuff->last_session, &info[1], 3);

		/* multisession ? */
		if (!stuff->multisession)
			stuff->last_session.second = 2;


#if MCD_DEBUG_OPENCLOSE 
		printk(MCD ":: multisession: %d\n", info[0]);
		printk(MCD ":: last session at: %02d:%02d.%02d\n", 
			stuff->last_session.minute,
			stuff->last_session.second,
			stuff->last_session.frame);
#endif
    }


  	/* request the disks table of contents (aka diskinfo) */
    {
		char cmd[] = { MCD_CMD_REQUEST_TOC };

		if (-1 == (mcd_talk(stuff,
				cmd, (char *) &stuff->toc, sizeof stuff->toc, 200)))
			return -EIO;
	}

    stuff->toc.firsttrack = bcd2uint(stuff->toc.firsttrack);
    stuff->toc.lasttrack = bcd2uint(stuff->toc.lasttrack);

    stuff->lastsector = (CD_FRAMESIZE / 512) 
			* (msf2log(&stuff->toc.first) + msf2log(&stuff->toc.len)) 
			- 1;

#if MCD_DEBUG_OPENCLOSE
    printk(MCD ":: diskinfo: first track: %d last track: %d\n" \
	    "    start=%02x:%02x.%02x end=%02x:%02x.%02x\n" \
	    "    first block: %d\n" \
	    "    len        : %d\n" \
	    "    last sector: %d\n",
	    stuff->toc.firsttrack,
	    stuff->toc.lasttrack,
	    stuff->toc.first.minute,
	    stuff->toc.first.second,
	    stuff->toc.first.frame,
	    stuff->toc.len.minute,
	    stuff->toc.len.second,
	    stuff->toc.len.frame,
	    msf2log(&stuff->toc.first),
	    msf2log(&stuff->toc.len),
	    stuff->lastsector);
#endif

	/* The media was changed, we'll have to reinit the drive partially */
	if (changed < stuff->changed) {
		changed = stuff->changed;

#if MCD_DEBUG_OPENCLOSE
		printk(MCD ":: re-init\n");
#endif
		/* re-enable irq generation */
		{
			char cmd[] = { MCD_CMD_CONFIG };
			cmd[0] = 3;						/* only 3 bytes used */
			cmd[OPCODE + 1] = MCD_IRQEN;	/* select irq mode */
			cmd[OPCODE + 2] = 0x05;			/* pre, err irq enable */
			if (-1 == mcd_talk(stuff, cmd, NULL, 0, 100))
				return -EIO;
		}


		/* try to get the first sector ... */
		{
			char buf[512];
			int ans;
			int tries;

			stuff->xa = 0;

			for (tries = 6; tries; tries--) {
				char cmd[] = { MCD_CMD_DATAMODE_SET };

#if MCD_DEBUG_OPENCLOSE
				printk(MCD ":: try %s\n", stuff->xa ? "xa" : "normal");
#endif
				/* set data mode */
				cmd[OPCODE + 1] = stuff->xa ? MCD_DATAMODE2 : MCD_DATAMODE1;

				if (-1 == mcd_talk(stuff, cmd, NULL, 0, 500)) return -EIO;

				while (0 == (ans = mcd_transfer(stuff, buf, 0, 1))) 
					;

				if (ans == 1) break;

				stuff->xa = !stuff->xa; 
			}
			if (!tries) return -EIO;
		}

		/* xa disks will be read in raw mode, others not */
		{
			char cmd[] = { MCD_CMD_MODE_SET };
			cmd[OPCODE + 1] = 
					stuff->xa ? (MCD_RAW | MCD_MUTE_CONTROL) : MCD_MUTE_CONTROL;
			if (-1 == (mcd_talk(stuff, cmd, NULL, 0, 200))) return -EIO;
		}
	}

	printk(MCD ": %s%s Disk found\n", 
			stuff->xa ? "XA / " : "",
			stuff->multisession ? "Multi Session" : "Single Session");

    MOD_INC_USE_COUNT;

    stuff->users++;
    return 0;
}

static void mcd_close(struct inode *ip, struct file *fp)
{
    struct s_drive_stuff *stuff;

#if MCD_DEBUG_OPENCLOSE
    printk(MCD ":: close()\n");
#endif

    stuff = &mcd_drive_stuff[MINOR(ip->i_rdev)];

    if (0 == --stuff->users) {
		sync_dev(ip->i_rdev);
		/** invalidate_inodes(ip->i_rdev); */
		invalidate_buffers(ip->i_rdev);
    }
    MOD_DEC_USE_COUNT;

    if (!MOD_IN_USE)
	wake_up_interruptible(&mcd_modclean);

    return;
}

int check_mcd_media_change(int full_dev, int flag)
/*	Return: 1 if media chaged since last call to 
			  this function
			0 else
	Setting flag to 0 resets the changed state. */

{
    printk(MCD ":: check_mcd_media_change(0x%x, %d) called\n",
	    full_dev, flag);
    return 1;
}

void mcd_setup(char *str, int *pi)
{
#if MCD_DEBUG
    printk(MCD ":: setup(%s, %d) called\n",
	    str, pi[0]);
#endif
}

/* DIRTY PART ******************************************************/ 

static void mcd_delay(struct s_drive_stuff *stuff, long jifs)
/*	This routine is used for sleeping while initialisation - it seems that
 	there are no other means available. May be we could use a simple count
 	loop w/ jumps to itself, but I wanna make this independend of cpu
 	speed.  */
{
    unsigned long tout = jiffies + jifs;

    if (jifs < 0) return;

#if MODULE
	/*	timer are available */
    current->timeout = tout;
    while (current->timeout)
		interruptible_sleep_on(&stuff->sleepq);
#else
	/* timer are _not_ available at initialisation time */
    if (stuff->present) {
		current->state = TASK_INTERRUPTIBLE;
		current->timeout = tout;
		interruptible_sleep_on(&stuff->sleepq);
    } else
		while (jiffies < tout) {
	    current->timeout = jiffies;
	    scedule();
	}
#endif
}

static void mcd_intr(int irq, struct pt_regs* regs)
{
    struct s_drive_stuff *stuff;

    stuff = mcd_irq_map[irq];

    if (!stuff->busy) {
		printk(MCD ": unexpected interrupt\n");
		return;
    }

#if MCD_DEBUG_IRQ 
	{
		unsigned char x;

		/* if not ok read the next byte as the drives status */
		if (0 == (stuff->introk = (~(x = inb(MCD_RREG_STATUS)) & MCD_RBIT_DTEN))) 
			printk(MCD ":: intr(%d) failed: 0x%02x 0x%02x\n", 
					irq, x, inb(MCD_RREG_DATA));
		else  
			printk(MCD ":: intr(%d) ok: 0x%02x\n", irq, x);
	}
#else
	/* if not ok read the next byte as the drives status */
    if (0 == (stuff->introk = (~(inb(MCD_RREG_STATUS)) & MCD_RBIT_DTEN))) 
		inb(MCD_RREG_DATA);
#endif
	
    stuff->busy = 0;
    wake_up_interruptible(&stuff->busyq);
}


static int mcd_talk(struct s_drive_stuff *stuff, const char *cmd,
/*
 Send a command to the drive, wait for the result.
 in	: struct s_drive_stuff* --
 	  char* cmd -- the command as { nr of bytes, byte 0, byte 1, ... }
 	  char* buffer -- there the result will be stored
	  int size -- buffer size
	  int timeout -- 
 out: -1 on timeout
 	  drives status otherwise
	  drive status will be stored to stuff->st too
 */
	char *buffer, int size, int timeout)
{
    const DELAY = 1;				/* minimum delay */
	int st;

    while (stuff->lock)
		interruptible_sleep_on(&stuff->lockq);

    stuff->lock = 1;
    stuff->valid = 0;	

#if MCD_DEBUG_TALK == 1
	printk(MCD ":: talk() called\n");
#endif

    outsb(MCD_WREG_DATA, cmd + 1, (unsigned char) *cmd);

	/* first comes the status, always, - we'll read one byte more then
	 * spec'd by size */
	st = -1;
	while ((st == -1) || size--) {
		int to = 0;

		/* wait for the status bit */
		{
			unsigned long limit = jiffies + timeout;
			while((inb(MCD_RREG_STATUS) & MCD_RBIT_STEN)  
					&& !(to = jiffies > limit)) {
				mcd_delay(stuff, DELAY);
			}
		}

		if (!to) {

			/* status read? */
			if (st == -1) {
				/* read the status byte */
				st = stuff->st = (unsigned char) inb(MCD_RREG_DATA);

#if MCD_DEBUG_TALK
				printk(MCD ":: talk(): got status 0x%02x\n", st);
#endif
				/* media change will be handled here ... */
				if (e_changed(st)) {
					printk(MCD ": Media changed.\n");
					stuff->changed = jiffies;
				}

				/* command error will be handled here ... */
				if (e_cmderr(st)) {
					printk(MCD ": command error\n");
					st = stuff->st = -1;
					break;
				}
			} else {
				/* read the answer */
				*buffer++ = inb(MCD_RREG_DATA); 
			}
		} else {
			printk(MCD ": talk() timed out.\n");
			st = stuff->st = -1;
			break;
		}
	}

    stuff->lock = 0;
    wake_up_interruptible(&stuff->lockq);
    return st;
}

/* MODULE STUFF ***********************************************************/
#ifdef MODULE

int init_module(void)
{
	int i;
	int drives = 0;

	mcd_init(0, 0);

	for (i = 0; i <= MCD_NDRIVES; i++) drives += mcd_drive_stuff[i].present;

    if (!drives) {
		printk(MCD ": can't init any drive.\n");
		return -EIO;
    }

    return 0;
}

void cleanup_module(void)
{
    int i;

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

    if (MOD_IN_USE) {
		printk(MCD ": driver busy, cleanup delayed\n");
		interruptible_sleep_on(&mcd_modclean);
    }
	
    for (i = 0; i <= MCD_NDRIVES; i++) {
		struct s_drive_stuff *stuff = &mcd_drive_stuff[i];
		if (!stuff->present) continue;
		release_region((unsigned long) stuff->port, MCD_IO_SIZE);

		free_irq(stuff->irq);
    }

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

#endif 	/* MODULE */

/* Suport functions ************************************************/

unsigned long mcd_init(unsigned long mem_start, unsigned long mem_end)
{
	int drive;
	int mcd_drive_map[][3] = MCD_DRIVEMAP;

	printk(MCD ": Version 048 $Id: mcd.c,v 1.9 1995/01/28 01:45:27 root Rel $ \n");

	for (drive = 0; drive < MCD_NDRIVES; drive++) {


		char version[2] = {'\0', 0};
		struct s_drive_stuff *stuff = &mcd_drive_stuff[drive];

		stuff->present = 0;
		printk(MCD ": install drive %d\n", drive); 

		{	/* clean the stuff */
			char* p;
			char* e = (char*) stuff + sizeof(struct s_drive_stuff);
			for (p = (char*) stuff; p < e; p++)
				*p = 0;
			stuff->changed = jiffies;
		}
			

		/*
		 * The kernel maintains a list of used i/o addresses.  First we'll
		 * check, if our address is unused.
		 */

		stuff->port = port(mcd_drive_map[drive]);

		if (0 != check_region((int) stuff->port, MCD_IO_SIZE)) {
			printk(MCD ": i/o ports at 0x%3p .. 0x%3p not available\n",
					stuff->port, stuff->port + MCD_IO_SIZE);
					stuff->port = 0;
			continue; /* next drive */
		}

		/*
		 * ok, the port is known and available, stuff->port is set.
		 */

		#if MCD_DEBUG_INIT
		printk(MCD ":: i/o port is available at 0x%3p\n", stuff->port);
		#endif

		{	/* get the drives version (double/single speed) */
			int st;
			int i;

			for (i = 2; i; i--) {
				char cmd[] = { MCD_CMD_REQUEST_VERSION };

				if (-1 != (st = mcd_talk(stuff, cmd,
						version, sizeof version, 500)))
					break;
			}

			/* talk failed, next drive */
			if (st == -1) continue;

			switch (version[0]) {
			case 'D':
				stuff->readcmd = MCD_DREAD;
				stuff->present = 2;
				break;
			case 'F':
				stuff->readcmd = MCD_SREAD;
				stuff->present = 1;
				break;
			default:
				stuff->present = 0;
				break;
			}
		}

		if (!stuff->present) {
			printk(MCD ": no drive found.\n");
			continue; /* next drive */
		}

		/*
	 	 * the drives version is known too, let's register it to the kernel
		 */

		if (register_blkdev(MAJOR_NR, MCD, &mcd_fops) != 0) {
			printk(MCD ": unable to get major %d for " DEVICE_NAME "\n",
					MAJOR_NR);
			continue; /* next drive */
		}

		blk_dev[MAJOR_NR].request_fn = DEVICE_REQUEST;
		read_ahead[MAJOR_NR] = READ_AHEAD;

		/*
		 * blksize_size[MAJOR_NR] = BLKSIZES;
		 */

		/* subscribe to irq and i/o ports */
		stuff->irq = irq(mcd_drive_map[drive]);

		if (request_irq(stuff->irq, mcd_intr, SA_INTERRUPT, MCD))
		{
			printk(MCD ": init() unable to get IRQ %d for "
			DEVICE_NAME "\n", stuff->irq);
			stuff->irq = 0;
			continue;
		}

		mcd_irq_map[stuff->irq] = stuff;

		/*
		 * we have requested the irq and set the mapping of irq to minor
		 * number, we have to claim for our i/o region
		 */

		request_region((unsigned long) stuff->port, MCD_IO_SIZE, MCD);

		/*	Now init the hardware */
		{

#if MCD_DEBUG_INIT
			printk(MCD ":: hwinit()\n");
#endif

			outb(0, MCD_WREG_CHN);		/* no dma, no irq -> hardware */
			outb(0, MCD_WREG_RESET);	/* hw reset */

		/* get garbage */
		{
			int i;
			mcd_delay(stuff, 50);
			for (i = 100; i; i--) (void) inb(MCD_RREG_STATUS);
		}

		/* do soft reset */
		{ 
			char cmd[] = { MCD_CMD_RESET };
			mcd_talk(stuff, cmd, NULL, 0, 100);
		}

#if WE_KNOW_WHY
		outb(0x50, MCD_WREG_CHN);	/* irq 11 -> channel register */
#endif
		outb(0x10, MCD_WREG_CHN);	/* irq 11 -> channel register */

		/* Disable dma */
		{
			char cmd[] = {MCD_CMD_CONFIG};
			cmd[0] = 3;						/* only 3 bytes used */
			cmd[OPCODE + 1] = MCD_DMASEL;	/* select dma mode */
			cmd[OPCODE + 2] = 0x00;			/* no dma */
			mcd_talk(stuff, cmd, NULL, 0, 100);
		}

		/* Enable Interrupts */
		{
			char cmd[] = {MCD_CMD_CONFIG};
			cmd[0] = 3;						/* only 3 bytes used */
			cmd[OPCODE + 1] = MCD_IRQEN;	/* select irq mode */
			cmd[OPCODE + 2] = 0x05;	/* pre, err irq enable */
			mcd_talk(stuff, cmd, NULL, 0, 100);
		}

	} 	/* hardware init done */


		stuff->minor = minor(mcd_drive_map[drive]);

		printk(MCD ": " DEVICE_NAME " installed at 0x%3p, irq %d.\n"
			   MCD ": Firmware version %c %d\n",
			   stuff->port, stuff->irq, version[0], version[1]);
		}

	return mem_start;
}


static int mcd_transfer(struct s_drive_stuff *stuff,
		char *p, int sector, int nr_sectors)
/*	This does actually the transfer from the drive.
	Return:	-1 on timeout
			else status byte (as in stuff->st) */
{

    int off;
    int done = 0;

#if MCD_DEBUG_TRANSFER
    printk(MCD ":: transfer(.., .., sector %d, count %d) [\n", sector, nr_sectors);
#endif

    while (stuff->lock)
		interruptible_sleep_on(&stuff->lockq);

    if (stuff->valid
			&& (sector >= stuff->pending)
			&& (sector < stuff->off_direct)) {


	off = stuff->off_requested < (off = sector + nr_sectors)
			? stuff->off_requested : off;

	stuff->lock = current->pid;

	do {
	    int sig = 0;
	    int to = 0;

		/* wait for the drive become idle */
	    current->timeout = jiffies + 500;
	    while (stuff->busy) {
			interruptible_sleep_on(&stuff->busyq);
			if ((sig = (current->signal && ~current->blocked))
					|| (to = (current->timeout == 0))) {
				break;
			} 	
		}

	    current->timeout = 0;

		/* test for possible errors */
	    if (((stuff->busy == 0) && !stuff->introk)
				|| sig
				|| to) {
			if ((stuff->busy == 0) && !stuff->introk)
				printk(MCD ": failure in data request\n");
			else if (to)
				printk(MCD ": timeout\n");
			else if (sig)
				printk(MCD ": got signal 0x%lx\n", current->signal);

			stuff->lock = 0;
			stuff->busy = 0;
			wake_up_interruptible(&stuff->lockq);
			wake_up_interruptible(&stuff->busyq);
			stuff->errno = MCD_E;
#if MCD_DEBUG_TRANSFER
			printk(MCD ":: ] transfer()\n");
#endif
			return -1;
	    }

		/* test if it's the first sector of a block,
		 * there we have to skip some bytes as we read raw data */
		if (stuff->xa && (0 == (stuff->pending & 3))) {
			const int HEAD = CD_FRAMESIZE_RAW - CD_XA_TAIL - CD_FRAMESIZE;
#if MCD_DEBUG_TRANSFER
			printk(MCD "::     sector %d, skipping %d header bytes\n", 
				stuff->pending, HEAD);
#endif
			insb(MCD_RREG_DATA, p, HEAD);
		}

		/* now actually read the data */

#if MCD_DEBUG_TRANSFER
		printk(MCD "::     read sector %d\n", stuff->pending);
#endif

	    insb(MCD_RREG_DATA, p, 512); 

		/* test if it's the last sector of a block,
		 * if so, we have to expect an interrupt and to skip some
		 * data too */
		if ((stuff->busy = (3 == (stuff->pending & 3))) && stuff->xa) {
			char dummy[CD_XA_TAIL];
#if MCD_DEBUG_TRANSFER 
			printk(MCD "::     sector %d, skipping %d trailing bytes\n", 
				stuff->pending, CD_XA_TAIL);
#endif
			insb(MCD_RREG_DATA, &dummy[0], CD_XA_TAIL);
		}


	    if (stuff->pending == sector) {
			p += 512;
			done++;
			sector++;
	    }
	}
	while (++(stuff->pending) < off);

	stuff->lock = 0;
	wake_up_interruptible(&stuff->lockq);

    } else {

		static unsigned char cmd[] = {
			0,
			0, 0, 0,
			0, 0, 0
		};

		cmd[0] = stuff->readcmd;

		stuff->valid = 1;
		stuff->pending = sector & ~3;

		/* do some sanity checks */
#if MCD_DEBUG_TRANSFER
		printk(MCD "::    request sector %d\n", stuff->pending);
#endif

		if (stuff->pending > stuff->lastsector) {
			printk(MCD ": transfer() sector %d from nirvana requested.\n",
				stuff->pending);
			stuff->errno = MCD_EOM;
#if MCD_DEBUG_TRANSFER
			printk(MCD ":: ] transfer()\n");
#endif

			return -1;
		}

		if ((stuff->off_direct = stuff->pending + DIRECT_SIZE)
			> stuff->lastsector + 1)
			stuff->off_direct = stuff->lastsector + 1;
		if ((stuff->off_requested = stuff->pending + REQUEST_SIZE)
			> stuff->lastsector + 1)
			stuff->off_requested = stuff->lastsector + 1;

#if MCD_DEBUG_TRANSFER
		printk(MCD "::     pending      : %d\n", stuff->pending);
		printk(MCD "::     off_direct   : %d\n", stuff->off_direct);
		printk(MCD "::     off_requested: %d\n", stuff->off_requested);
#endif

		{
			struct msf pending;
			log2msf(stuff->pending / 4, &pending);
			cmd[1] = pending.minute;
			cmd[2] = pending.second;
			cmd[3] = pending.frame;

		}

		stuff->busy = 1;
		cmd[6] = (unsigned char) (stuff->off_requested - stuff->pending) / 4;

		outsb(MCD_WREG_DATA, cmd, sizeof cmd);

    }

    stuff->off_direct = (stuff->off_direct += done) < stuff->off_requested
	    ? stuff->off_direct : stuff->off_requested;

#if MCD_DEBUG_TRANSFER
	printk(MCD ":: ] transfer()\n");
#endif
    return done;
}


/*	Access to elements of the mcd_drive_map members */

static int minor(int *ip) { return ip[0]; }
static char* port(int *ip) { return (char*) ip[1]; }
static int irq(int *ip) { return ip[2]; }

/*	Misc number converter */

static unsigned int bcd2uint(unsigned char c)
{ return (c >> 4) * 10 + (c & 0x0f); }

static unsigned int uint2bcd(unsigned int ival)
{ return ((ival / 10) << 4) | (ival % 10); }

static void log2msf(unsigned int l, struct msf* pmsf)
{
    l += CD_BLOCK_OFFSET;
    pmsf->minute = uint2bcd(l / 4500), l %= 4500;
    pmsf->second = uint2bcd(l / 75);
    pmsf->frame = uint2bcd(l % 75);
}

static unsigned int msf2log(const struct msf* pmsf)
{
    return bcd2uint(pmsf->frame)
    + bcd2uint(pmsf->second) * 75
    + bcd2uint(pmsf->minute) * 4500
    - CD_BLOCK_OFFSET;
}
