/*
 * Copyright (c) 1994 Ludd, University of Lule}, Sweden.
 * All rights reserved.
 *
 * Written by Olof Johansson (offe@ludd.luth.se) 1995.
 * Based on code written by Theo de Raadt (deraadt@fsa.ca).
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. All advertising materials mentioning features or use of this software
 *    must display the following acknowledgement:
 *     This product includes software developed at Ludd, University of Lule}.
 * 4. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

 /* All bugs are subject to removal without further notice */

/*
 * offe 01/07/95
 *
 * This version of the driver _still_ doesn't implement scatter/gather for the
 * WD7000-FASST2. This is due to the fact that my controller doesn't seem to
 * support it. That, and the lack of documentation makes it impossible for
 * me to implement it.
 * What I've done instead is allocated a local buffer, contiguous buffer big
 * enough to handle the requests. I haven't seen any read/write bigger than 64k,
 * so I allocate a buffer of 64+16k. The data that needs to be DMA'd to/from
 * the controller is copied to/from that buffer before/after the command is
 * sent to the card.
 */

#include "wds.h"
#if NWDS > 0

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/buf.h>

#include <scsi/scsiconf.h>

#include <machine/clock.h>

#include <vm/vm.h>
#include <vm/pmap.h>

#include <i386/isa/isa_device.h>

static struct scsi_device wds_dev =
{
 	NULL,
	NULL,
	NULL,
	NULL,
	"wds",
	0,
	{ 0, 0 }
};

/*
  XXX   THIS SHOULD BE FIXED!
  I haven't got the KERNBASE-version to work, but on my system the kernel
  is at virtual address 0xFxxxxxxx, responding to physical address
  0x0xxxxxxx.
#define PHYSTOKV(x)	((x) + KERNBASE)
*/
#define PHYSTOKV(x)	((x) | 0xf0000000)
#define KVTOPHYS(x)	vtophys(x)
/* 0x10000 (64k) should be enough. But just to be sure... */
#define BUFSIZ 		0x12000


/* WD7000 registers */
#define WDS_STAT		0	/* read */
#define WDS_IRQSTAT		1	/* read */

#define WDS_CMD			0	/* write */
#define WDS_IRQACK		1	/* write */
#define WDS_HCR			2	/* write */

/* WDS_STAT (read) defs */
#define WDS_IRQ			0x80
#define WDS_RDY			0x40
#define WDS_REJ			0x20
#define WDS_INIT		0x10

/* WDS_IRQSTAT (read) defs */
#define WDSI_MASK		0xc0
#define WDSI_ERR		0x00
#define WDSI_MFREE		0x80
#define WDSI_MSVC		0xc0

/* WDS_CMD (write) defs */
#define WDSC_NOOP		0x00
#define WDSC_INIT		0x01
#define WDSC_DISUNSOL		0x02
#define WDSC_ENAUNSOL		0x03
#define WDSC_IRQMFREE		0x04
#define WDSC_SCSIRESETSOFT	0x05
#define WDSC_SCSIRESETHARD	0x06
#define WDSC_MSTART(m)		(0x80 + (m))
#define WDSC_MMSTART(m)		(0xc0 + (m))

/* WDS_HCR (write) defs */
#define WDSH_IRQEN		0x08
#define WDSH_DRQEN		0x04
#define WDSH_SCSIRESET		0x02
#define WDSH_ASCRESET		0x01

struct wds_cmd {
  u_char cmd;
  u_char targ;
  u_char scb[12];	/*u_char scb[12];*/
  u_char stat;
  u_char venderr;
  u_char len[3];
  u_char data[3];
  u_char next[3];
  u_char write;
  u_char xx[6];
};

struct wds_req {
  struct wds_cmd cmd;
  struct wds_cmd sense;
  struct scsi_xfer *sxp;
  int busy, polled;
  int done, ret, ombn;
};

#define WDSX_SCSICMD		0x00
#define WDSX_OPEN_RCVBUF	0x80
#define WDSX_RCV_CMD		0x81
#define WDSX_RCV_DATA		0x82
#define WDSX_RCV_DATASTAT	0x83
#define WDSX_SND_DATA		0x84
#define WDSX_SND_DATASTAT	0x85
#define WDSX_SND_CMDSTAT	0x86
#define WDSX_READINIT		0x88
#define WDSX_READSCSIID		0x89
#define WDSX_SETUNSOLIRQMASK	0x8a
#define WDSX_GETUNSOLIRQMASK	0x8b
#define WDSX_GETFIRMREV		0x8c
#define WDSX_EXECDIAG		0x8d
#define WDSX_SETEXECPARM	0x8e
#define WDSX_GETEXECPARM	0x8f

struct wds_mb {
  u_char stat;
  u_char addr[3];
};
/* ICMB status value */
#define ICMB_OK			0x01
#define ICMB_OKERR		0x02
#define ICMB_ETIME		0x04
#define ICMB_ERESET		0x05
#define ICMB_ETARCMD		0x06
#define ICMB_ERESEL		0x80
#define ICMB_ESEL		0x81
#define ICMB_EABORT		0x82
#define ICMB_ESRESET		0x83
#define ICMB_EHRESET		0x84

struct wds_setup {
  u_char cmd;
  u_char scsi_id;
  u_char buson_t;
  u_char busoff_t;
  u_char xx;
  u_char mbaddr[3];
  u_char nomb;
  u_char nimb;
};

#define WDS_NOMB	8
#define WDS_NIMB	8
#define MAXSIMUL	8

static int wdsunit=0;

static u_char wds_data[NWDS][BUFSIZ];
static u_char wds_data_in_use[NWDS];

static struct wds {
  int addr;
  struct wds_req wdsr[MAXSIMUL];
  struct wds_mb ombs[WDS_NOMB], imbs[WDS_NIMB];
  struct scsi_link sc_link;
} wds[NWDS];

static int wdsprobe(struct isa_device *);
static void wds_minphys(struct buf *);
static struct wds_req *wdsr_alloc(int);
static int32_t wds_scsi_cmd(struct scsi_xfer *);
static u_int32_t wds_adapter_info(int);
static ointhand2_t wdsintr;
static int wds_done(int, struct wds_cmd *, u_char);
static int wdsattach(struct isa_device *);
static int wds_init(struct isa_device *);
static int wds_cmd(int, u_char *, int);
static void wds_wait(int, int, int);

struct isa_driver wdsdriver =
{
  wdsprobe,
  wdsattach,
  "wds"
};

static struct scsi_adapter wds_switch =
{
	wds_scsi_cmd,
	wds_minphys,
	0,
	0,
	wds_adapter_info,
	"wds",
	{0,0}
};

int
wdsprobe(struct isa_device *dev)
{
  if(wdsunit > NWDS)
    return 0;

  dev->id_unit = wdsunit;	/* XXX WRONG! */
  wds[wdsunit].addr = dev->id_iobase;

  if(wds_init(dev) != 0)
    return 0;
  wdsunit++;
  return 8;
}

void
wds_minphys(struct buf *bp)
{
  if(bp->b_bcount > BUFSIZ)
    bp->b_bcount = BUFSIZ;
}

struct wds_req *
wdsr_alloc(int unit)
{
  struct wds_req *r;
  int x;
  int i;

  r = NULL;
  x = splbio();
  for(i=0; i<MAXSIMUL; i++)
    if(!wds[unit].wdsr[i].busy)
    {
      r = &wds[unit].wdsr[i];
      r->busy = 1;
      break;
    }
  if(!r)
  {
    splx(x);
    return NULL;
  }

  r->ombn = -1;
  for(i=0; i<WDS_NOMB; i++)
    if(!wds[unit].ombs[i].stat)
    {
      wds[unit].ombs[i].stat = 1;
      r->ombn = i;
      break;
    }
  if(r->ombn == -1 )
  {
    r->busy = 0;
    splx(x);
    return NULL;
  }
  splx(x);
  return r;
}

int32_t
wds_scsi_cmd(struct scsi_xfer *sxp)
{
  struct wds_req *r;
  int unit = sxp->sc_link->adapter_unit;
  int base;
  u_char c;
  int i;

  base = wds[unit].addr;

  if( sxp->flags & SCSI_RESET)
  {
    printf("reset!\n");
    return COMPLETE;
  }

  r = wdsr_alloc(unit);
  if(r==NULL)
  {
    printf("no request slot available!\n");
    sxp->error = XS_DRIVER_STUFFUP;
    return TRY_AGAIN_LATER;
  }
  r->done = 0;
  r->sxp = sxp;

  if(sxp->flags & SCSI_DATA_UIO)
  {
    printf("UIO!\n");
    sxp->error = XS_DRIVER_STUFFUP;
    return TRY_AGAIN_LATER;
  }

  scsi_uto3b(KVTOPHYS(&r->cmd),wds[unit].ombs[r->ombn].addr);

  bzero(&r->cmd, sizeof r->cmd);
  r->cmd.cmd = WDSX_SCSICMD;
  r->cmd.targ = (sxp->sc_link->target << 5) | sxp->sc_link->lun;
  bcopy(sxp->cmd, &r->cmd.scb, sxp->cmdlen<12 ? sxp->cmdlen : 12);
  scsi_uto3b(sxp->datalen, r->cmd.len);

  if(wds_data_in_use[unit])
  {
    sxp->error = XS_DRIVER_STUFFUP;
    return TRY_AGAIN_LATER;
  }
  else
    wds_data_in_use[unit] = 1;

  if(sxp->datalen && !(sxp->flags&SCSI_DATA_IN))
    bcopy(sxp->data, wds_data[unit], sxp->datalen);

  scsi_uto3b(sxp->datalen ? KVTOPHYS(wds_data[unit]) : 0, r->cmd.data);

  r->cmd.write = (sxp->flags&SCSI_DATA_IN)? 0x80 : 0x00;

  scsi_uto3b(KVTOPHYS(&r->sense),r->cmd.next);

  bzero(&r->sense, sizeof r->sense);
  r->sense.cmd = r->cmd.cmd;
  r->sense.targ = r->cmd.targ;
  r->sense.scb[0] = REQUEST_SENSE;
  scsi_uto3b(KVTOPHYS(&sxp->sense),r->sense.data);
  scsi_uto3b(sizeof(sxp->sense), r->sense.len);
  r->sense.write = 0x80;

  if(sxp->flags & SCSI_NOMASK)
  {
    outb(base+WDS_HCR, WDSH_DRQEN);
    r->polled = 1;
  } else
  {
    outb(base+WDS_HCR, WDSH_IRQEN|WDSH_DRQEN);
    r->polled = 0;
  }

  c = WDSC_MSTART(r->ombn);

  if( wds_cmd(base, &c, sizeof c) != 0)
  {
    printf("wds%d: unable to start outgoing mbox\n", unit);
    r->busy = 0;
    wds[unit].ombs[r->ombn].stat = 0;

    return TRY_AGAIN_LATER;
  }

  if(sxp->flags & SCSI_NOMASK)
  {
  repoll:

    i = 0;
    while(!(inb(base+WDS_STAT) & WDS_IRQ))
    {

      DELAY(20000);
      if(++i == 20)
      {
        outb(base + WDS_IRQACK, 0);
	/*r->busy = 0;*/
	sxp->error = XS_TIMEOUT;
	return HAD_ERROR;
      }
    }
    wdsintr(unit);
    if(r->done)
    {
      r->sxp->flags |= ITSDONE;
      r->busy = 0;
      return r->ret;
    }
    goto repoll;
  }

  return SUCCESSFULLY_QUEUED;
}

u_int32_t
wds_adapter_info(int unit)
{
  return 1;
}

static void
wdsintr(int unit)
{
  struct wds_cmd *pc, *vc;
  struct wds_mb *in;
  u_char stat;
  u_char c;

  if(!inb(wds[unit].addr+WDS_STAT) & WDS_IRQ)
  {
    outb(wds[unit].addr + WDS_IRQACK, 0);
    return;
  }

  c = inb(wds[unit].addr + WDS_IRQSTAT);
  if( (c&WDSI_MASK) == WDSI_MSVC)
  {
    c = c & ~WDSI_MASK;
    in = &wds[unit].imbs[c];

    pc = (struct wds_cmd *)scsi_3btou(in->addr);
    vc = (struct wds_cmd *)PHYSTOKV((intptr_t)(void *)pc);
    stat = in->stat;

    wds_done(unit, vc, stat);
    in->stat = 0;

    outb(wds[unit].addr + WDS_IRQACK, 0);
  }
}

int
wds_done(int unit, struct wds_cmd *c, u_char stat)
{
  struct wds_req *r;
  int i;

  r = (struct wds_req *)NULL;

  for(i=0; i<MAXSIMUL; i++)
    if( c == &wds[unit].wdsr[i].cmd && !wds[unit].wdsr[i].done)
    {
      r = &wds[unit].wdsr[i];
      break;
    }
  if(r == (struct wds_req *)NULL)
  {
    /* failed to find request! */
    return 1;
  }

  r->done = 1;
  wds[unit].ombs[r->ombn].stat = 0;
  r->ret = HAD_ERROR;
  switch(stat)
  {
  case ICMB_OK:
    r->ret = COMPLETE;
    if(r->sxp)
      r->sxp->resid = 0;
    break;
  case ICMB_OKERR:
    if(!(r->sxp->flags & SCSI_ERR_OK) && c->stat)
    {
      r->sxp->sense.error_code = c->venderr;
      r->sxp->error=XS_SENSE;
    }
    else
      r->sxp->error=XS_NOERROR;
    r->ret = COMPLETE;
    break;
  case ICMB_ETIME:
    r->sxp->error = XS_TIMEOUT;
    r->ret = HAD_ERROR;
    break;
  case ICMB_ERESET:
  case ICMB_ETARCMD:
  case ICMB_ERESEL:
  case ICMB_ESEL:
  case ICMB_EABORT:
  case ICMB_ESRESET:
  case ICMB_EHRESET:
    r->sxp->error = XS_DRIVER_STUFFUP;
    r->ret = HAD_ERROR;
    break;
  }

  if(r->sxp)
    if(r->sxp->datalen && (r->sxp->flags&SCSI_DATA_IN))
      bcopy(wds_data[unit],r->sxp->data,r->sxp->datalen);

  wds_data_in_use[unit] = 0;

  if(!r->polled)
  {
    r->sxp->flags |= ITSDONE;
    scsi_done(r->sxp);
  }

  r->busy = 0;

  return 0;
}

static int
wds_getvers(int unit)
{
  struct wds_req *r;
  int base;
  u_char c;
  int i;

  base = wds[unit].addr;

  r = wdsr_alloc(unit);
  if(!r)
  {
    printf("wds%d: no request slot available!\n", unit);
    return -1;
  }

  r->done = 0;
  r->sxp = NULL;

  scsi_uto3b(KVTOPHYS(&r->cmd), wds[unit].ombs[r->ombn].addr);

  bzero(&r->cmd, sizeof r->cmd);
  r->cmd.cmd = WDSX_GETFIRMREV;

  outb(base+WDS_HCR, WDSH_DRQEN);
  r->polled = 1;

  c = WDSC_MSTART(r->ombn);
  if(wds_cmd(base, (u_char *)&c, sizeof c))
  {
    printf("wds%d: version request failed\n", unit);
    r->busy = 0;
    wds[unit].ombs[r->ombn].stat = 0;
    return -1;
  }

  while(1)
  {
    i = 0;
    while( (inb(base+WDS_STAT) & WDS_IRQ) == 0)
    {
      DELAY(9000);
      if(++i == 20)
	return -1;
    }
    wdsintr(unit);
    if(r->done)
    {
      printf("wds%d: firmware version %d.%02d\n", unit,
	     r->cmd.targ, r->cmd.scb[0]);
      r->busy = 0;
      return 0;
    }
  }
}

int
wdsattach(struct isa_device *dev)
{
  int masunit;
  static u_long versprobe=0;	/* max 32 controllers */
  int unit = dev->id_unit;
  struct scsibus_data *scbus;

  dev->id_ointr = wdsintr;
  masunit = dev->id_unit;

  if( !(versprobe & (1<<masunit)))
  {
    versprobe |= (1<<masunit);
    if(wds_getvers(masunit)==-1)
      printf("wds%d: getvers failed\n", masunit);
  }

  printf("wds%d: using %d bytes for dma buffer\n",unit,BUFSIZ);

  wds[unit].sc_link.adapter_unit = unit;
  wds[unit].sc_link.adapter_targ = 7;
  wds[unit].sc_link.adapter = &wds_switch;
  wds[unit].sc_link.device = &wds_dev;
  wds[unit].sc_link.flags = SDEV_BOUNCE;

  /*
   * Prepare the scsibus_data area for the upperlevel
   * scsi code.
   */
  scbus = scsi_alloc_bus();
  if(!scbus)
    return 0;
  scbus->adapter_link = &wds[unit].sc_link;

  scsi_attachdevs(scbus);

  return 1;
}

int
wds_init(struct isa_device *dev)
{
  struct wds_setup init;
  int base;
  int unit, i;
  struct wds_cmd wc;

  unit = dev->id_unit;
  base = wds[unit].addr;

  /*
   * Sending a command causes the CMDRDY bit to clear.
   */

  outb(base+WDS_CMD, WDSC_NOOP);
  if( inb(base+WDS_STAT) & WDS_RDY)
    return 1;

  /*
   * the controller exists. reset and init.
   */
  outb(base+WDS_HCR, WDSH_ASCRESET|WDSH_SCSIRESET);
  DELAY(30);
  outb(base+WDS_HCR, 0);

  outb(base+WDS_HCR, WDSH_DRQEN);

  isa_dmacascade(dev->id_drq);

  if( (inb(base+WDS_STAT) & (WDS_RDY)) != WDS_RDY)
  {
    for(i=0; i<10; i++)
    {
      if( (inb(base+WDS_STAT) & (WDS_RDY)) == WDS_RDY)
	break;
      DELAY(40000);
    }
    if( (inb(base+WDS_STAT) & (WDS_RDY)) != WDS_RDY) /* probe timeout */
      return 1;
  }

  bzero(&init, sizeof init);
  init.cmd = WDSC_INIT;
  init.scsi_id = 7;
  init.buson_t = 24;
  init.busoff_t = 48;
  scsi_uto3b(KVTOPHYS(wds[unit].ombs), init.mbaddr);
  init.xx = 0;
  init.nomb = WDS_NOMB;
  init.nimb = WDS_NIMB;

  wds_wait(base+WDS_STAT, WDS_RDY, WDS_RDY);
  if( wds_cmd(base, (u_char *)&init, sizeof init) != 0)
  {
    printf("wds%d: wds_cmd failed\n", unit);
    return 1;
  }

  wds_wait(base+WDS_STAT, WDS_INIT, WDS_INIT);

  wds_wait(base+WDS_STAT, WDS_RDY, WDS_RDY);

  bzero(&wc,sizeof wc);
  wc.cmd = WDSC_DISUNSOL;
  if( wds_cmd(base, (char *)&wc, sizeof wc) != 0)
  {
    printf("wds%d: wds_cmd failed\n", unit);
    return 1;
  }

  return 0;
}

int
wds_cmd(int base, u_char *p, int l)
{
  int s=splbio();

  while(l--)
  {
    do
    {
      outb(base+WDS_CMD,*p);
      wds_wait(base+WDS_STAT,WDS_RDY,WDS_RDY);
    } while (inb(base+WDS_STAT) & WDS_REJ);
    p++;
  }

  wds_wait(base+WDS_STAT,WDS_RDY,WDS_RDY);

  splx(s);

  return 0;
}

void
wds_wait(int reg, int mask, int val)
{
  while((inb(reg) & mask) != val);
}

#endif
