/* $Id: $
 *  linux/kernel/wd7000.c
 *
 *  Copyright (C) 1992  Thomas Wuensche
 *	closely related to the aha1542 driver from Tommy Thorn
 *	( as close as different hardware allows on a lowlevel-driver :-) )
 *
 *  Revised (and renamed) by John Boyd <boyd@cis.ohio-state.edu> to
 *  accomodate Eric Youngdale's modifications to scsi.c.  Nov 1992.
 *
 *  Additional changes to support scatter/gather.  Dec. 1992.  tw/jb
 */

#include <stdarg.h>
#include <linux/kernel.h>
#include <linux/head.h>
#include <linux/types.h>
#include <linux/string.h>
#include <linux/sched.h>
#include <asm/system.h>
#include <asm/dma.h>
#include <asm/io.h>
#include "../blk.h"
#include "scsi.h"
#include "hosts.h"

/* #define DEBUG  */

#include "wd7000.h"


#ifdef DEBUG
#define DEB(x) x
#else
#define DEB(x)
#endif


/*
   Driver data structures:
   - mb and scbs are required for interfacing with the host adapter.
     An SCB has extra fields not visible to the adapter; mb's
     _cannot_ do this, since the adapter assumes they are contiguous in
     memory, 4 bytes each, with ICMBs following OGMBs, and uses this fact
     to access them.
   - An icb is for host-only (non-SCSI) commands.  ICBs are 16 bytes each;
     the additional bytes are used only by the driver.
   - For now, a pool of SCBs are kept in global storage by this driver,
     and are allocated and freed as needed.

  The 7000-FASST2 marks OGMBs empty as soon as it has _started_ a command,
  not when it has finished.  Since the SCB must be around for completion,
  problems arise when SCBs correspond to OGMBs, which may be reallocated
  earlier (or delayed unnecessarily until a command completes).
  Mailboxes are used as transient data structures, simply for
  carrying SCB addresses to/from the 7000-FASST2.

  Note also since SCBs are not "permanently" associated with mailboxes,
  there is no need to keep a global list of Scsi_Cmnd pointers indexed
  by OGMB.   Again, SCBs reference their Scsi_Cmnds directly, so mailbox
  indices need not be involved.
*/

static struct {
       struct wd_mailbox ogmb[OGMB_CNT]; 
       struct wd_mailbox icmb[ICMB_CNT];
} mb;
static int next_ogmb = 0;   /* to reduce contention at mailboxes */

static Scb scbs[MAX_SCBS];
static Scb *scbfree = NULL;

static int wd7000_host = 0;
static unchar controlstat = 0;

static unchar rev_1 = 0, rev_2 = 0;  /* filled in by wd7000_revision */

#define wd7000_intr_ack()  outb(0,INTR_ACK)

#define WAITnexttimeout 3000000


static inline void wd7000_enable_intr(void)
{
    controlstat |= INT_EN;
    outb(controlstat,CONTROL);
}


static inline void wd7000_enable_dma(void)
{
    controlstat |= DMA_EN;
    outb(controlstat,CONTROL);
    set_dma_mode(DMA_CH, DMA_MODE_CASCADE);
    enable_dma(DMA_CH);
}


#define WAIT(port, mask, allof, noneof)					\
 { register WAITbits;							\
   register WAITtimeout = WAITnexttimeout;				\
   while (1) {								\
     WAITbits = inb(port) & (mask);					\
     if ((WAITbits & (allof)) == (allof) && ((WAITbits & (noneof)) == 0)) \
       break;                                                         	\
     if (--WAITtimeout == 0) goto fail;					\
   }									\
 }


static inline void delay( unsigned how_long )
{
     unsigned long time = jiffies + how_long;

     while (jiffies < time);
}


static inline int command_out(unchar *cmdp, int len)
{
    while (len--)  {
        WAIT(ASC_STAT, STATMASK, CMD_RDY, 0);
	outb(*cmdp++, COMMAND);
    }
    return 1;

fail:
    printk("wd7000_out WAIT failed(%d): ", len+1);
    return 0;
}

static inline Scb *alloc_scb(void)
{
    Scb *scb;
    unsigned long flags;

    save_flags(flags);
    cli();

    if (scbfree == NULL)  {
        panic("wd7000: can't allocate free SCB.\n");
	restore_flags(flags);
	return NULL;
    }
    scb = scbfree;  scbfree = scb->next;
    memset(scb, 0, sizeof(Scb));  scb->next = NULL;

    restore_flags(flags);

    return scb;
}


static inline void free_scb( Scb *scb )
{
    unsigned long flags;

    save_flags(flags);
    cli();

    memset(scb, 0, sizeof(Scb));
    scb->next = scbfree;  scbfree = scb;

    restore_flags(flags);
}


static inline void init_scbs(void)
{
    int i;
    unsigned long flags;

    save_flags(flags);
    cli();

    scbfree = &(scbs[0]);
    for (i = 0;  i < MAX_SCBS-1;  i++)  scbs[i].next = &(scbs[i+1]);
    scbs[MAX_SCBS-1].next = NULL;

    restore_flags(flags);
}    
    

static int mail_out( Scb *scbptr )
/*
 *  Note: this can also be used for ICBs; just cast to the parm type.
 */
{
    int i, ogmb;
    unsigned long flags;

    DEB(printk("wd7000_scb_out: %06x");)

    /* We first look for a free outgoing mailbox */
    save_flags(flags);
    cli();
    ogmb = next_ogmb;
    for (i = 0; i < OGMB_CNT; i++) {
	if (mb.ogmb[ogmb].status == 0)  {
	    DEB(printk(" using OGMB %x",ogmb));
	    mb.ogmb[ogmb].status = 1;
	    any2scsi(mb.ogmb[ogmb].scbptr, scbptr);

	    next_ogmb = (ogmb+1) % OGMB_CNT;
	    break;
	}  else
	    ogmb = (++ogmb) % OGMB_CNT;
    }
    restore_flags(flags);
    DEB(printk(", scb is %x",scbptr);)

    if (i >= OGMB_CNT) {
        DEB(printk(", no free OGMBs.\n");)
	/* Alternatively, issue "interrupt on free OGMB", and sleep... */
        return 0;
    }

    wd7000_enable_intr(); 
    do  {
        WAIT(ASC_STAT,STATMASK,CMD_RDY,0);
	outb(START_OGMB|ogmb, COMMAND);
	WAIT(ASC_STAT,STATMASK,CMD_RDY,0);
    }  while (inb(ASC_STAT) & CMD_REJ);

    DEB(printk(", awaiting interrupt.\n");)
    return 1;

fail:
    DEB(printk(", WAIT timed out.\n");)
    return 0;
}


int make_code(unsigned hosterr, unsigned scsierr)
{   
#ifdef DEBUG
    int in_error = hosterr;
#endif

    switch ((hosterr>>8)&0xff){
	case 0:	/* Reserved */
		hosterr = DID_ERROR;
		break;
	case 1:	/* Command Complete, no errors */
		hosterr = DID_OK;
		break;
	case 2: /* Command complete, error logged in scb status (scsierr) */ 
		hosterr = DID_OK;
		break;
	case 4:	/* Command failed to complete - timeout */
		hosterr = DID_TIME_OUT;
		break;
	case 5:	/* Command terminated; Bus reset by external device */
		hosterr = DID_RESET;
		break;
	case 6:	/* Unexpected Command Received w/ host as target */
		hosterr = DID_BAD_TARGET;
		break;
	case 80: /* Unexpected Reselection */
        case 81: /* Unexpected Selection */
		hosterr = DID_BAD_INTR;
		break;
        case 82: /* Abort Command Message  */
		hosterr = DID_ABORT;
		break;
	case 83: /* SCSI Bus Software Reset */
	case 84: /* SCSI Bus Hardware Reset */
		hosterr = DID_RESET;
		break;
        default: /* Reserved */
		hosterr = DID_ERROR;
		break;
	}
#ifdef DEBUG
    if (scsierr||hosterr)
        printk("\nSCSI command error: SCSI %02x host %04x return %d",
	       scsierr,in_error,hosterr);
#endif
    return scsierr | (hosterr << 16);
}


static void wd7000_scsi_done(Scsi_Cmnd * SCpnt)
{
    DEB(printk("wd7000_scsi_done: %06x\n",SCpnt);)
    SCpnt->SCp.phase = 0;
}


void wd7000_intr_handle(int irq)
{
    int flag, icmb, errstatus, icmb_status;
    int host_error, scsi_error;
    Scb *scb;             /* for SCSI commands */
    unchar *icb;          /* for host commands */
    Scsi_Cmnd *SCpnt;

    flag = inb(INTR_STAT);
    DEB(printk("wd7000_intr_handle: intr stat = %02x",flag);)

    if (!(inb(ASC_STAT)&0x80)){ 
	DEB(printk("\nwd7000_intr_handle: phantom interrupt...\n");)
	wd7000_intr_ack();
	return; 
    }

    /* check for an incoming mailbox */
    if ((flag & 0x40) == 0) {
        /*  for a free OGMB - need code for this case... */
        DEB(printk("wd7000_intr_handle: free outgoing mailbox\n");)
        wd7000_intr_ack();
	return;
    }
    /* The interrupt is for an incoming mailbox */
    icmb = flag & 0x3f;
    scb = (struct scb *) scsi2int(mb.icmb[icmb].scbptr);
    icmb_status = mb.icmb[icmb].status;
    mb.icmb[icmb].status = 0;

#ifdef DEBUG
    printk(" ICMB %d posted for SCB/ICB %06x, status %02x, vue %02x",
 	   icmb, scb, icmb_status, scb->vue );
#endif

    if (!(scb->op & 0x80))  {   /* an SCB is done */
        SCpnt = scb->SCpnt;
	if (--(SCpnt->SCp.phase) <= 0)  {  /* all scbs for SCpnt are done */
	    host_error = scb->vue | (icmb_status << 8);
	    scsi_error = scb->status;
	    errstatus = make_code(host_error,scsi_error);    
	    SCpnt->result = errstatus;

 	    if (SCpnt->host_scribble != NULL)
 	        scsi_free(SCpnt->host_scribble,WD7000_SCRIBBLE);
 	    free_scb(scb);

	    SCpnt->scsi_done(SCpnt);
	}
    }  else  {    /* an ICB is done */
        icb = (unchar *) scb;
        icb[ICB_STATUS] = icmb_status;
	icb[ICB_PHASE] = 0;
    }

    wd7000_intr_ack();
    DEB(printk(".\n");)
    return;
}


int wd7000_queuecommand(Scsi_Cmnd * SCpnt, void (*done)(Scsi_Cmnd *))
{
    Scb *scb;
    Sgb *sgb;
    unchar *cdb;
    unchar idlun;
    short cdblen;

    cdb = (unchar *) SCpnt->cmnd;
    cdblen = COMMAND_SIZE(*cdb);
    idlun = ((SCpnt->target << 5) & 0xe0) | (SCpnt->lun & 7);
    SCpnt->scsi_done = done;
    SCpnt->SCp.phase = 1;
    scb = alloc_scb();
    scb->idlun = idlun;
    memcpy(scb->cdb, cdb, cdblen);
    scb->direc = 0x40;		/* Disable direction check */
    scb->SCpnt = SCpnt;         /* so we can find stuff later */
    SCpnt->host_scribble = NULL;
    DEB(printk("request_bufflen is %x, bufflen is %x\n",\
        SCpnt->request_bufflen, SCpnt->bufflen);)

    if (SCpnt->use_sg)  {
        struct scatterlist *sg = (struct scatterlist *) SCpnt->request_buffer;
        unsigned i;

        if (scsi_hosts[wd7000_host].sg_tablesize <= 0)  {
	    panic("wd7000_queuecommand: scatter/gather not supported.\n");
	}
#ifdef DEBUG
 	printk("Using scatter/gather with %d elements.\n",SCpnt->use_sg);
#endif
  	/*
 	    Allocate memory for a scatter/gather-list in wd7000 format.
 	    Save the pointer at host_scribble.
  	*/
#ifdef DEBUG
 	if (SCpnt->use_sg > WD7000_SG)
 	    panic("WD7000: requesting too many scatterblocks\n");
#endif
 	SCpnt->host_scribble = (unsigned char *) scsi_malloc(WD7000_SCRIBBLE);
	sgb = (Sgb *) SCpnt->host_scribble;
 	if (sgb == NULL)
            panic("wd7000_queuecommand: scsi_malloc() failed.\n");

 	scb->op = 1;
 	any2scsi(scb->dataptr, sgb);
 	any2scsi(scb->maxlen, SCpnt->use_sg * sizeof (Sgb) );

	for (i = 0;  i < SCpnt->use_sg;  i++)  {
 	    any2scsi(sgb->ptr, sg[i].address);
 	    any2scsi(sgb->len, sg[i].length);
 	    sgb++;
        }
 	DEB(printk("Using %d bytes for %d scatter/gather blocks\n",\
 	    scsi2int(scb->maxlen), SCpnt->use_sg);)
    }  else  {
	scb->op = 0;
	any2scsi(scb->dataptr, SCpnt->request_buffer);
	any2scsi(scb->maxlen, SCpnt->request_bufflen);
    }

    return mail_out(scb);
}


int wd7000_command(Scsi_Cmnd *SCpnt)
{
    wd7000_queuecommand(SCpnt, wd7000_scsi_done);

    while (SCpnt->SCp.phase > 0);  /* phase counts scbs down to 0 */

    return SCpnt->result;
}


int wd7000_init(void)
{   int i;
    unchar init_block[] = {
        INITIALIZATION, 7, BUS_ON, BUS_OFF, 0, 0, 0, 0, OGMB_CNT, ICMB_CNT
    };

    /* Reset the adapter. */
    outb(SCSI_RES|ASC_RES, CONTROL);
    delay(1);  /* reset pulse: this is 10ms, only need 25us */
    outb(0,CONTROL);  controlstat = 0;
    /*
       Wait 2 seconds, then expect Command Port Ready.

       I suspect something else needs to be done here, but I don't know
       what.  The OEM doc says power-up diagnostics take 2 seconds, and
       indeed, SCSI commands submitted before then will time out, but
       none of what follows seems deterred by _not_ waiting 2 secs.
    */
    delay(200);

    WAIT(ASC_STAT, STATMASK, CMD_RDY, 0);
    DEB(printk("wd7000_init: Power-on Diagnostics finished\n");)
    if (((i=inb(INTR_STAT)) != 1) && (i != 7)) {
	panic("wd7000_init: Power-on Diagnostics error\n"); 
	return 0;
    }
    
    /* Clear mailboxes */
    memset(&mb,0,sizeof (mb));
    /* Set up SCB free list */
    init_scbs();

    /* Set up init block */
    any2scsi(init_block+5,&mb);
    /* Execute init command */
    if (!command_out(init_block,sizeof(init_block)))  {
	panic("WD-7000 Initialization failed.\n"); 
	return 0;
    }
    
    /* Wait until init finished */
    WAIT(ASC_STAT, STATMASK, CMD_RDY | ASC_INI, 0);
    outb(DISABLE_UNS_INTR, COMMAND); 
    WAIT(ASC_STAT, STATMASK, CMD_RDY | ASC_INI, 0);

    /* Enable Interrupt and DMA */
    if (request_irq(IRQ_LVL, wd7000_intr_handle)) {
      panic("Unable to allocate IRQ for WD-7000.\n");
      return 0;
    };
    if(request_dma(DMA_CH)) {
      panic("Unable to allocate DMA channel for WD-7000.\n");
      free_irq(IRQ_LVL);
      return 0;
    };
    wd7000_enable_dma();
    wd7000_enable_intr();

    printk("WD-7000 initialized.\n");
    return 1;
  fail:
    return 0;					/* 0 = not ok */
}


void wd7000_revision(void)
{
    volatile unchar icb[ICB_LEN] = {0x8c};  /* read firmware revision level */

    icb[ICB_PHASE] = 1;
    mail_out( (struct scb *) icb );
    while (icb[ICB_PHASE]) /* wait for completion */;
    rev_1 = icb[1];
    rev_2 = icb[2];

    /*
        For boards at rev 7.0 or later, enable scatter/gather.
    */
    if (rev_1 >= 7)  scsi_hosts[wd7000_host].sg_tablesize = WD7000_SG;
}


static const char *wd_bases[] = {(char *)0xce000};
typedef struct {
    char * signature;
    unsigned offset;
    unsigned length;
} Signature;

static const Signature signatures[] = {{"SSTBIOS",0xd,0x7}};

#define NUM_SIGNATURES (sizeof(signatures)/sizeof(Signature))


int wd7000_detect(int hostnum)
/* 
 *  return non-zero on detection
 */
{
    int i,j;
    char const *base_address = NULL;

    for(i=0;i<(sizeof(wd_bases)/sizeof(char *));i++){
	for(j=0;j<NUM_SIGNATURES;j++){
	    if(!memcmp((void *)(wd_bases[i] + signatures[j].offset),
		(void *) signatures[j].signature,signatures[j].length)){
		    base_address=wd_bases[i];
		    printk("WD-7000 detected.\n");
	    }	
	}
    }
    if (base_address == NULL) return 0;

    /* Store our host number */
    wd7000_host = hostnum;

    wd7000_init();    
    wd7000_revision();  /* will set scatter/gather by rev level */

    return 1;
}



static void wd7000_append_info( char *info, const char *fmt, ... )
/*
 *  This is just so I can use vsprintf...
 */
{
    va_list args;
    extern int vsprintf(char *buf, const char *fmt, va_list args);

    va_start(args, fmt);
    vsprintf(info, fmt, args);
    va_end(args);

    return;
}


const char *wd7000_info(void)
{
    static char info[80] = "Western Digital WD-7000, Firmware Revision ";

    wd7000_revision();
    wd7000_append_info( info+strlen(info), "%d.%d.\n", rev_1, rev_2 );

    return info;
}

int wd7000_abort(Scsi_Cmnd * SCpnt, int i)
{
#ifdef DEBUG
    printk("wd7000_abort: Scsi_Cmnd = 0x%08x, code = %d ", SCpnt, i);
    printk("id %d lun %d cdb", SCpnt->target, SCpnt->lun);
    {  int j;  unchar *cdbj = (unchar *) SCpnt->cmnd;
       for (j=0; j < COMMAND_SIZE(*cdbj);  j++)  printk(" %02x", *(cdbj++));
       printk(" result %08x\n", SCpnt->result);
    }
#endif
    return 0;
}


int wd7000_reset(void)
{
#ifdef DEBUG
    printk("wd7000_reset\n");
#endif
    return 0;
}


int wd7000_biosparam(int size, int dev, int* ip)
/*
 *  This is borrowed directly from aha1542.c, but my disks are organized
 *   this way, so I think it will work OK.
 */
{
  ip[0] = 64;
  ip[1] = 32;
  ip[2] = (size + 2047) >> 11;
/*  if (ip[2] >= 1024) ip[2] = 1024; */
  return 0;
}

