/*                                                                                               600       0       0        30411  5362514763  10152  0                                                                                                    ustar   root                            root                                                                                                                                                                                                                  
 *
 *  This file is in2000.c, written and
 *  Copyright (C) 1993  Brad McLean
 *
 * Disclaimer:
 * Note:  This is ugly.  I know it, I wrote it, but my whole
 * focus was on getting the damn thing up and out quickly.
 * Future stuff that would be nice:  Command chaining, and
 * a local queue of commands would speed stuff up considerably.
 * Disconnection needs some supporting code.  All of this
 * is beyond the scope of what I wanted to address, but if you
 * have time and patience, more power to you.
 * Also, there are some constants scattered throughout that
 * should have defines, and I should have built functions to
 * address the registers on the WD chip.
 * Oh well, I'm out of time for this project.
 * The one good thing to be said is that you can use the card.
 */

/*
 * This module was updated by Shaun Savage first on 5-13-93
 * At that time the write was fixed, irq detection, and some
 * timing stuff.  since that time other problems were fixed.
 * On 7-20-93 this file was updated for patch level 11
 * There are still problems with it but it work on 95% of
 * the machines.  There are still problems with it woring with
 * IDE drives, as swap drive and HD that support reselection.
 * But for most people it will work.
 */

#include <linux/kernel.h>
#include <linux/head.h>
#include <linux/types.h>
#include <linux/string.h>

#include <linux/sched.h>
#include <asm/dma.h>

#include <asm/system.h>
#include <asm/io.h>
#include "../blk.h"
#include "scsi.h"
#include "hosts.h"

#include "in2000.h"
/*#define DEBUG */
#ifdef DEBUG
#define DEB(x) x
#else
#define DEB(x)
#endif

/* These functions are based on include/asm/io.h */

inline static unsigned short inw( unsigned short port )
{
   unsigned short _v;
   
   __asm__ volatile ("inw %1,%0"
		     :"=a" (_v):"d" ((unsigned short) port));
   return _v;
}

inline static void outw( unsigned short value, unsigned short port )
{
   __asm__ volatile ("outw %0,%1"
			: /* no outputs */
			:"a" ((unsigned short) value),
			"d" ((unsigned short) port));
}

static unsigned int base;
static unsigned char irq_level;
unsigned short *in2000_dataptr;
int	in2000_datalen;
char	in2000_datawrite;
Scsi_Cmnd *in2000_SCptr = 0;
void (*in2000_done)(Scsi_Cmnd *);

int in2000_test_port(int index)
{
    char    tmp;

    tmp = inb(INFLED);
    if ( (~tmp&3) != index )	/* First, see if the DIP switch values match */
    	return 0;
    tmp = inb(INVERS);
/* Add some extra sanity checks here */
    return 1;
}

/*
 * retreive the current transaction counter from the WD
 */

unsigned in2000_txcnt(void)
{
    unsigned total=0;

    outb(TXCNTH,INSTAT);
    total = (inb(INDATA) & 0xff) << 16;
    outb(TXCNTM,INSTAT);
    total += (inb(INDATA) & 0xff) << 8;
    outb(TXCNTL,INSTAT);
    total += (inb(INDATA) & 0xff);
    return total;
}

/*
 * Note: the FIFO is screwy, and has a counter granularity of 16 bytes, so
 * we have to reconcile the FIFO counter, the transaction byte count from the
 * WD chip, and of course, our desired transaction size.  It may look strange,
 * and could probably use improvement, but it works, for now.
 */

void in2000_fifo_out(char flag)
{
    unsigned count;
    unsigned total;

    if ( flag )
	count = 2048;
    else
    {
    	count=(inb(INFCNT)&0xfe) << 3;	/* How many can I transfer */
	total = count;
	count = in2000_txcnt();
	if ( count <= in2000_datalen )
	    count = in2000_datalen - count;
	else
	    count = 0;
    }
    if ( count > in2000_datalen )
	count = in2000_datalen;
    count >>= 1;	/* Words, not bytes */
    while ( count-- )
    {	/*
	 * Question: Could I use OUTSW ?
	 * I don't know right now, so we'll wait and see.
	 */
	outw(*in2000_dataptr++,INFIFO);
	in2000_datalen-=2;
    }
    if ( ! in2000_datalen )
    {
	count = 8;
	while ( count-- )
	    outw(0,INFIFO);
	outb(2,ININTR); /* Mask FIFO Interrupts */
    }
}

void in2000_fifo_in(void)
{
    unsigned count,count2;
    unsigned total;

    count = inb(INFCNT) & 0xe1;
    do
    {
	count2 = count;
	count = inb(INFCNT) & 0xe1;
    } while ( count != count2 );
    count = inb(INFCNT) & 0xfe;
    count <<= 3;
    total = count;
    count = in2000_txcnt();
    if ( count <= in2000_datalen )
	count = in2000_datalen - count;
    else
	count = 0;
    if ( count > in2000_datalen )
	count2 = in2000_datalen >> 1;
    else
	count2 = count >> 1;
    count >>= 1;
    count -= count2;
    while ( count2-- )
    {	/*
	 * Again, could I use INSW?
         */
	*in2000_dataptr++ = inw(INFIFO);
	in2000_datalen -=2;
    }
    while ( count-- )
    	inw(INFIFO);	/* Throw away some stuff */
    if ( ! in2000_datalen )
	outb(2,ININTR); /* Mask FIFO Interrupts */
}

const char *in2000_info(void)
{
    static char buffer[] = "";
    return buffer;
}

void in2000_intr_handle(int foo)
	{
    int result=0;
    int count,auxstatus,scsistatus,cmdphase,scsibyte;
    int action=0;
    Scsi_Cmnd *SCptr;

	while ( ( (count = inb(INFCNT)) & 1 ) || ( inb(INSTAT) & 0x80 ) )
		{
   		auxstatus = inb(INSTAT);
   		outb(SCSIST,INSTAT);
   		scsistatus = inb(INDATA);
   		outb(CMDPHAS,INSTAT);
   		cmdphase = inb(INDATA);
   		outb(TARGETU,INSTAT);
   		scsibyte = inb(INDATA);	/* Get the scsi status byte */
   		DEB(printk("(in2000:%02x %02x %02x %02x %02x)",count,auxstatus,
			scsistatus,cmdphase,scsibyte));
   		if ( in2000_datalen && in2000_dataptr )
   			{
   			if ( in2000_datawrite )
				in2000_fifo_out(0);
			else
			    in2000_fifo_in();
   			}
		if ( auxstatus & 0x80 /*count & 1*/ )
			{	/* There is a WD Chip interrupt */
			outb(2,ININTR);	/* Disable fifo interrupts */
			result = DID_OK << 16;
			if ((scsistatus != 0x16) && (scsistatus != 0x85))
			   	printk("(in2000:%02x %02x %02x %02x %02x)",count,auxstatus,
					scsistatus,cmdphase,scsibyte);
			switch ( scsistatus & 0xf0 )
				{
				case	0x00:	/* Card Reset Completed */
		action = 3;
		break;
	case	0x10:	/* Successful Command Completion */
		if ( scsistatus & 0x8 )
		    action = 1;
		break;
	case	0x20:	/* Command Paused or Aborted */
		if ( scsistatus & 0x8  )
		    action = 1;
		else if ( (scsistatus & 7) < 2 )
		    action = 2;
		else
		    result = DID_ABORT << 16;
		break;
	case	0x40:	/* Terminated */
		if ( scsistatus & 0x8 )
		     action = 1;
		else if ( (scsistatus & 7) > 2 )
		     action = 2;
		else
		    result = DID_TIME_OUT << 16;
		break;
	case	0x80:	/* Service Required */
		if ( scsistatus & 0x8 )
			action = 1;
		else
			action = 2;
		break;
	}
	outb(0,INFLED);
	switch ( action )
	{
	case	0x02:	/* Issue an abort */
		outb(COMMAND,INSTAT);
		outb(1,INDATA); 	/* ABORT COMMAND */
		result = DID_ABORT << 16;
	case	0x00:	/* Basically all done */
		if ( ! in2000_SCptr )
		    return;
		in2000_SCptr->result = result | scsibyte;
		SCptr = in2000_SCptr;
		in2000_SCptr = 0;
		in2000_datalen = 0;
		in2000_dataptr = 0;
		if ( in2000_done )
		     (*in2000_done)(SCptr);
		break;
	case	0x01:	/* We need to reissue a command */
		outb(CMDPHAS,INSTAT);
		switch ( scsistatus & 7 )
		{
		case	0:
		case	1:
		case	4:
		case	5:
		case	6:
		case	7:
			outb(0x41,INDATA);
			break;
		case	2:
			outb(0x30,INDATA);
			break;
		case	3:
			outb(0x45,INDATA);	/* To go to status phase, eliminate data */
			outb(TXCNTH,INSTAT);
			outb(0,INDATA);
			outb(TXCNTM,INSTAT);
			outb(0,INDATA);
			outb(TXCNTL,INSTAT);
			outb(0,INDATA);
			in2000_datalen = 0;
			in2000_dataptr = 0;
			break;
		}
		outb(COMMAND,INSTAT);
		outb(8,INDATA);	 /* RESTART THE COMMAND */
		break;
	case	0x03:	/* Finish up a Card Reset */
		outb(TIMEOUT,INSTAT);	/* I got these values by reverse Engineering */
		outb(0x1f,INDATA);	/* the Always' bios. */
		outb(CONTROL,INSTAT);
		outb(0,INDATA);
		outb(SYNCTXR,INSTAT);
		outb(0x40,INDATA);
		break;
	}
    }
}
}

int in2000_queuecommand(Scsi_Cmnd * SCpnt, void (*done)(Scsi_Cmnd *))
{
    unchar direction;
    unchar *cmd = (unchar *) SCpnt->cmnd;
    unchar target = SCpnt->target;
    void *buff = SCpnt->request_buffer;
    int bufflen = SCpnt->request_bufflen;
    int size,loop;
    int timeout;
    DEB(int i);

    /*
     * This SCSI command has no data phase, but unfortunately the mid-level
     * SCSI drivers ask for 256 bytes of data xfer.  Our card hangs if you
     * do this, so we protect against it here.  It would be nice if the mid-
     * level could be changed, but who knows if that would break other host
     * adapter drivers.
     */
    if ( *cmd == TEST_UNIT_READY )
	bufflen = 0;

    /*
     * What it looks like.  Boy did I get tired of reading it's output.
     */
#ifdef DEBUG
    if (*cmd == READ_10 || *cmd == WRITE_10)
	i = xscsi2int((cmd+2));
    else if (*cmd == READ_6 || *cmd == WRITE_6)
	i = scsi2int((cmd+2));
    else
	i = -1;
    printk("in2000qcmd: dev %d pos %d len %d ", target, i, bufflen);
    printk("scsi cmd:");
    for (i = 0; i < (COMMAND_SIZE(*cmd)); i++) printk("%02x ", cmd[i]);
    printk("\n");
#endif
    direction = 1;
    if (*cmd == READ_10 || *cmd == READ_6)
	direction = 1;
    else if (*cmd == WRITE_10 || *cmd == WRITE_6)
	direction = 0;
    size = COMMAND_SIZE(*cmd);	/* CDB length */ 
    /*
     * Setup our current pointers
     * This is where you would allocate a control structure in a queue,
     * If you were going to upgrade this to do multiple issue.
     * Note that datalen and dataptr exist because we can change the
     * values during the course of the operation, while managing the
     * FIFO.
     * Note the nasty little first clause.  In theory, the mid-level
     * drivers should never hand us more than one command at a time,
     * but just in case someone gets cute in configuring the driver,
     * we'll protect them, although not very politely.
     */
    if ( in2000_SCptr )
    {
	printk("in2000_queue_command waiting for free command block!\n");
	while ( in2000_SCptr );
    }
    for ( timeout = jiffies + 5; timeout > jiffies; )
    {
	if ( ! ( inb(INSTAT) & 0xb0 ) )
	{
	    timeout = 0;
	    break;
	}
	else
	{
		inb(INSTAT);
	    outb(SCSIST,INSTAT);
	    inb(INDATA);
	    outb(CMDPHAS,INSTAT);
	    inb(INDATA);
	    outb(TARGETU,INSTAT);
	    inb(INDATA);
	}
    }
    if ( timeout )
    {
	printk("in2000_queue_command timeout!\n");
	SCpnt->result = DID_TIME_OUT << 16;
	(*done)(SCpnt);
	return 1;
    }
    in2000_datalen = bufflen;
    in2000_dataptr = (unsigned short*)buff;
    in2000_done = done;
    in2000_SCptr = SCpnt;
    /*
     * Write the CDB to the card, then the LUN, the length, and the target.
     */
    for ( loop=0; loop < size; loop++ )
    {
	outb(TOTSECT+loop,INSTAT);
	outb(cmd[loop],INDATA);
    }
    outb(TARGETU,INSTAT);
    outb(SCpnt->lun & 7,INDATA);
    SCpnt->host_scribble = NULL;
    outb(TXCNTH,INSTAT);
    outb(bufflen>>16,INDATA);
    outb(TXCNTM,INSTAT);
    outb(bufflen>>8,INDATA);
    outb(TXCNTL,INSTAT);
    outb(bufflen,INDATA);
    outb(DESTID,INSTAT);
    outb(target&7,INDATA);
    /*
     * Set up the FIFO
     */
    outb(0,INFRST);
    if ( direction == 1 )
    {
	in2000_datawrite = 0;
	outb(0,INFWRT);
    }
    else
    {
	in2000_datawrite = 1;
/*	in2000_fifo_out(1);*/	/* preload the outgoing fifo */
    }
    /*
     * Start it up
     */
    outb(CONTROL,INSTAT);	/* WD BUS Mode */
    outb(0x4C,INDATA);
    if ( bufflen )
        outb(0,ININTR);		/* Enable FIFO */
	if (direction == 0)
		in2000_fifo_out(1);
    outb(COMMAND,INSTAT);
    outb(0,INNLED);
    outb(8,INDATA);
    return 0;
}

static volatile int internal_done_flag = 0;
static volatile int internal_done_errcode = 0;

static void internal_done(Scsi_Cmnd * SCpnt)
{
    internal_done_errcode = SCpnt->result;
    ++internal_done_flag;
}

int in2000_command(Scsi_Cmnd * SCpnt)
{
    in2000_queuecommand(SCpnt, internal_done);

    while (!internal_done_flag);
    internal_done_flag = 0;
    return internal_done_errcode;
}

int in2000_detect(int hostnum)
{
    int base_tab[] = { 0x220,0x200,0x110,0x100 };
    int int_tab[] = { 15,14,11,10 };
    int loop;

    DEB(printk("in2000_detect: \n"));
    
    for ( loop=0; loop < 4; loop++ )
    {
	base = base_tab[loop];
	if ( in2000_test_port(loop))  break;
    }
    if ( loop == 4 )
	return 0;

    irq_level = int_tab [ ((~inb(INFLED)>>3)&0x3) ];

    printk("Configuring IN2000 at IO:%x, IRQ %d\n",base,
	   irq_level);

    outb(2,ININTR);	/* Shut off the FIFO first, so it won't ask for data.*/
    if (request_irq(irq_level,in2000_intr_handle))
    {
	printk("in2000_detect: Unable to allocate IRQ.\n");
	return 0;
    }
    outb(SCSIST,INSTAT);
    inb(INDATA);
    outb(OWNID,INSTAT);
    outb(0x7,INDATA);
    outb(COMMAND,INSTAT);
    outb(0,INDATA);
    return 1;
}

int in2000_abort(Scsi_Cmnd * SCpnt, int i)
{
    DEB(printk("in2000_abort\n"));
    printk("in2000_abort\n");
    /*
     * Ask no stupid questions, just order the abort.
     */
    outb(COMMAND,INSTAT);
    outb(1,INDATA);	/* Abort Command */
    return 0;
}

int in2000_reset(void)
{
    DEB(printk("in2000_reset called\n"));
    /*
     * Note: this is finished off by an incoming interrupt
     */
    outb(SCSIST,INSTAT);
    inb(INDATA);
    outb(OWNID,INSTAT);
    outb(0x7,INDATA);
    outb(COMMAND,INSTAT);
    outb(0,INDATA);
    return 0;
}

int in2000_biosparam(int size, int dev, int* iinfo)
	{
    DEB(printk("in2000_biosparam\n"));
    iinfo[0] = 64;
    iinfo[1] = 32;
    iinfo[2] = size >> 11;
/*  if (iinfo[2] >= 1024) iinfo[2] = 1024; */
    return 0;
    }

