/*	$NetBSD: siop.c,v 1.15 2024/02/08 19:44:08 andvar Exp $	*/
/*
 * Copyright (c) 2010 KIYOHARA Takashi
 * All rights reserved.
 *
 * 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.
 *
 * 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.
 */

#include <lib/libsa/stand.h>
#include <lib/libkern/libkern.h>

#include <dev/microcode/siop/siop.out>

#include "boot.h"
#include "sdvar.h"

#ifdef DEBUG
#define DPRINTF(x)	printf x
#else
#define DPRINTF(x)
#endif

#define ALLOC(T, A)	\
		(T *)(((uint32_t)alloc(sizeof(T) + (A)) + (A)) & ~((A) - 1))
#define VTOPHYS(va)	(uint32_t)(va)
#define DEVTOV(pa)	(uint32_t)(pa)
#define wbinv(adr, siz)	_wbinv(VTOPHYS(adr), (uint32_t)(siz))
#define inv(adr, siz)	_inv(VTOPHYS(adr), (uint32_t)(siz))

/* 53c810 supports little endian */
#define htoc32(x)	htole32(x)
#define ctoh32(x)	le32toh(x)

static void siop_pci_reset(int);

static void siop_setuptables(struct siop_adapter *, struct siop_xfer *,
			     struct scsi_xfer *);
static void siop_ma(struct siop_adapter *, struct scsi_xfer *);
static void siop_sdp(struct siop_adapter *, struct siop_xfer *,
		     struct scsi_xfer *, int);
static void siop_update_resid(struct siop_adapter *, struct siop_xfer *,
			      struct scsi_xfer *, int);

static int siop_intr(struct siop_adapter *);
static void siop_scsicmd_end(struct siop_adapter *, struct scsi_xfer *);
static int siop_scsi_request(struct siop_adapter *, struct scsi_xfer *);
static void siop_start(struct siop_adapter *, struct scsi_xfer *);
static void siop_xfer_setup(struct siop_xfer *, void *);

static int siop_add_reselsw(struct siop_adapter *, int, int);
static void siop_update_scntl3(struct siop_adapter *, int, int);

static int _scsi_inquire(struct siop_adapter *, int, int, int, char *);
static void scsi_request_sense(struct siop_adapter *, struct scsi_xfer *);
static int scsi_interpret_sense(struct siop_adapter *, struct scsi_xfer *);
static int scsi_probe(struct siop_adapter *);

static struct siop_adapter adapt;


static void
siop_pci_reset(int addr)
{
	int dmode, ctest5;
	const int maxburst = 4;			/* 53c810 */

	dmode = readb(addr + SIOP_DMODE);

	ctest5 = readb(addr + SIOP_CTEST5);
	writeb(addr + SIOP_CTEST4, readb(addr + SIOP_CTEST4) & ~CTEST4_BDIS);
	ctest5 &= ~CTEST5_BBCK;
	ctest5 |= (maxburst - 1) & CTEST5_BBCK;
	writeb(addr + SIOP_CTEST5, ctest5);

	dmode |= DMODE_ERL;
	dmode &= ~DMODE_BL_MASK;
	dmode |= ((maxburst - 1) << DMODE_BL_SHIFT) & DMODE_BL_MASK;
	writeb(addr + SIOP_DMODE, dmode);
}


static void
siop_setuptables(struct siop_adapter *adp, struct siop_xfer *xfer,
		 struct scsi_xfer *xs)
{
	int msgoffset = 1;

	xfer->siop_tables.id =
	    htoc32((adp->clock_div << 24) | (xs->target << 16));
	memset(xfer->siop_tables.msg_out, 0, sizeof(xfer->siop_tables.msg_out));
	/* request sense doesn't disconnect */
	if (xs->cmd->opcode == SCSI_REQUEST_SENSE)
		xfer->siop_tables.msg_out[0] = MSG_IDENTIFY(xs->lun, 0);
	else
		xfer->siop_tables.msg_out[0] = MSG_IDENTIFY(xs->lun, 1);

	xfer->siop_tables.t_msgout.count = htoc32(msgoffset);
	xfer->siop_tables.status =
	    htoc32(SCSI_SIOP_NOSTATUS); /* set invalid status */

	xfer->siop_tables.cmd.count = htoc32(xs->cmdlen);
	xfer->siop_tables.cmd.addr = htoc32(local_to_PCI((u_long)xs->cmd));
	if (xs->datalen != 0) {
		xfer->siop_tables.data[0].count = htoc32(xs->datalen);
		xfer->siop_tables.data[0].addr =
		    htoc32(local_to_PCI((u_long)xs->data));
	}
}

static void
siop_ma(struct siop_adapter *adp, struct scsi_xfer *xs)
{
	int offset, dbc;

	/*
	 * compute how much of the current table didn't get handled when
	 * a phase mismatch occurs
	 */
	if (xs->datalen == 0)
	    return; /* no valid data transfer */

	offset = readb(adp->addr + SIOP_SCRATCHA + 1);
	if (offset >= SIOP_NSG) {
		printf("bad offset in siop_sdp (%d)\n", offset);
		return;
	}
	dbc = readl(adp->addr + SIOP_DBC) & 0x00ffffff;
	xs->resid = dbc;
}

static void
siop_clearfifo(struct siop_adapter *adp)
{
	int timo = 0;
	uint8_t ctest3 = readb(adp->addr + SIOP_CTEST3);

	DPRINTF(("DMA FIFO not empty!\n"));
	writeb(adp->addr + SIOP_CTEST3, ctest3 | CTEST3_CLF);
	while ((readb(adp->addr + SIOP_CTEST3) & CTEST3_CLF) != 0) {
		delay(1);
		if (++timo > 1000) {
			printf("Clear FIFO failed!\n");
			writeb(adp->addr + SIOP_CTEST3,
			    readb(adp->addr + SIOP_CTEST3) & ~CTEST3_CLF);
			return;
		}
	}
}

static void
siop_sdp(struct siop_adapter *adp, struct siop_xfer *xfer, struct scsi_xfer *xs,
	 int offset)
{

	if (xs->datalen == 0)
	    return; /* no data pointers to save */

	/*
	 * offset == SIOP_NSG may be a valid condition if we get a Save data
	 * pointer when the xfer is done. Just ignore the Save data pointer
	 * in this case
	 */
	if (offset == SIOP_NSG)
		return;
	/*
	 * Save data pointer. We do this by adjusting the tables to point
	 * at the beginning of the data not yet transferred.
	 * offset points to the first table with untransferred data.
	 */

	/*
	 * before doing that we decrease resid from the amount of data which
	 * has been transferred.
	 */
	siop_update_resid(adp, xfer, xs, offset);

#if 0
	/*
	 * First let see if we have a resid from a phase mismatch. If so,
	 * we have to adjst the table at offset to remove transferred data.
	 */
	if (siop_cmd->flags & CMDFL_RESID) {
		scr_table_t *table;

		siop_cmd->flags &= ~CMDFL_RESID;
		table = &xfer->siop_tables.data[offset];
		/* "cut" already transferred data from this table */
		table->addr =
		    htoc32(ctoh32(table->addr) + ctoh32(table->count) -
							siop_cmd->resid);
		table->count = htoc32(siop_cmd->resid);
	}
#endif

	/*
	 * now we can remove entries which have been transferred.
	 * We just move the entries with data left at the beginning of the
	 * tables
	 */
	memmove(xfer->siop_tables.data, &xfer->siop_tables.data[offset],
	    (SIOP_NSG - offset) * sizeof(scr_table_t));
}

static void
siop_update_resid(struct siop_adapter *adp, struct siop_xfer *xfer,
		  struct scsi_xfer *xs, int offset)
{
	int i;

	if (xs->datalen == 0)
	    return; /* no data to transfer */

	/*
	 * update resid. First account for the table entries which have
	 * been fully completed.
	 */
	for (i = 0; i < offset; i++)
		xs->resid -= ctoh32(xfer->siop_tables.data[i].count);
#if 0
	/*
	 * if CMDFL_RESID is set, the last table (pointed by offset) is a
	 * partial transfers. If not, offset points to the entry following
	 * the last full transfer.
	 */
	if (siop_cmd->flags & CMDFL_RESID) {
		scr_table_t *table = &xfer->siop_tables.data[offset];

		xs->resid -= ctoh32(table->count) - xs->resid;
	}
#endif
}


#define CALL_SCRIPT(ent)	writel(adp->addr + SIOP_DSP, scriptaddr + ent);

static int
siop_intr(struct siop_adapter *adp)
{
	struct siop_xfer *siop_xfer = NULL;
	struct scsi_xfer *xs = NULL;
	u_long scriptaddr = local_to_PCI((u_long)adp->script);
	int offset, target, lun, tag, restart = 0, need_reset = 0;
	uint32_t dsa, irqcode;
	uint16_t sist;
	uint8_t dstat, sstat1, istat;

	istat = readb(adp->addr + SIOP_ISTAT);
	if ((istat & (ISTAT_INTF | ISTAT_DIP | ISTAT_SIP)) == 0)
		return 0;
	if (istat & ISTAT_INTF) {
		printf("INTRF\n");
		writeb(adp->addr + SIOP_ISTAT, ISTAT_INTF);
	}
	if ((istat & (ISTAT_DIP | ISTAT_SIP | ISTAT_ABRT)) ==
	    (ISTAT_DIP | ISTAT_ABRT))
		/* clear abort */
		writeb(adp->addr + SIOP_ISTAT, 0);
	/* use DSA to find the current siop_cmd */
	dsa = readl(adp->addr + SIOP_DSA);
	if (dsa >= local_to_PCI((u_long)adp->xfer) &&
	    dsa < local_to_PCI((u_long)adp->xfer) + SIOP_TABLE_SIZE) {
		dsa -= local_to_PCI((u_long)adp->xfer);
		siop_xfer = adp->xfer;
		_inv((u_long)siop_xfer, sizeof(*siop_xfer));

		xs = adp->xs;
	}

	if (istat & ISTAT_DIP)
		dstat = readb(adp->addr + SIOP_DSTAT);
	if (istat & ISTAT_SIP) {
		if (istat & ISTAT_DIP)
			delay(10);
		/*
		 * Can't read sist0 & sist1 independently, or we have to
		 * insert delay
		 */
		sist = readw(adp->addr + SIOP_SIST0);
		sstat1 = readb(adp->addr + SIOP_SSTAT1);

		if ((sist & SIST0_MA) && need_reset == 0) {
			if (siop_xfer) {
				int scratcha0;

				dstat = readb(adp->addr + SIOP_DSTAT);
				/*
				 * first restore DSA, in case we were in a S/G
				 * operation.
				 */
				writel(adp->addr + SIOP_DSA,
				    local_to_PCI((u_long)siop_xfer));
				scratcha0 = readb(adp->addr + SIOP_SCRATCHA);
				switch (sstat1 & SSTAT1_PHASE_MASK) {
				case SSTAT1_PHASE_STATUS:
				/*
				 * previous phase may be aborted for any reason
				 * ( for example, the target has less data to
				 * transfer than requested). Compute resid and
				 * just go to status, the command should
				 * terminate.
				 */
					if (scratcha0 & A_flag_data)
						siop_ma(adp, xs);
					else if ((dstat & DSTAT_DFE) == 0)
						siop_clearfifo(adp);
					CALL_SCRIPT(Ent_status);
					return 1;
				case SSTAT1_PHASE_MSGIN:
				/*
				 * target may be ready to disconnect
				 * Compute resid which would be used later
				 * if a save data pointer is needed.
				 */
					if (scratcha0 & A_flag_data)
						siop_ma(adp, xs);
					else if ((dstat & DSTAT_DFE) == 0)
						siop_clearfifo(adp);
					writeb(adp->addr + SIOP_SCRATCHA,
					    scratcha0 & ~A_flag_data);
					CALL_SCRIPT(Ent_msgin);
					return 1;
				}
				printf("unexpected phase mismatch %d\n",
				    sstat1 & SSTAT1_PHASE_MASK);
			} else
				printf("phase mismatch without command\n");
			need_reset = 1;
		}
		if (sist & (SIST1_STO << 8)) {
			/* selection time out, assume there's no device here */
			if (siop_xfer) {
				xs->error = XS_SELTIMEOUT;
				goto end;
			} else
				printf("selection timeout without command\n");
		}

		/* Else it's an unhandled exception (for now). */
		printf("unhandled scsi interrupt,"
		    " sist=0x%x sstat1=0x%x DSA=0x%x DSP=0x%lx\n",
		    sist, sstat1, dsa, 
		    readl(adp->addr + SIOP_DSP) - scriptaddr);
		if (siop_xfer) {
			xs->error = XS_SELTIMEOUT;
			goto end;
		}
		need_reset = 1;
	}
	if (need_reset) {
reset:
		printf("XXXXX: fatal error, need reset the bus...\n");
		return 1;
	}

//scintr:
	if ((istat & ISTAT_DIP) && (dstat & DSTAT_SIR)) { /* script interrupt */
		irqcode = readl(adp->addr + SIOP_DSPS);
		/*
		 * no command, or an inactive command is only valid for a
		 * reselect interrupt
		 */
		if ((irqcode & 0x80) == 0) {
			if (siop_xfer == NULL) {
				printf(
				    "script interrupt 0x%x with invalid DSA\n",
				    irqcode);
				goto reset;
			}
		}
		switch(irqcode) {
		case A_int_err:
			printf("error, DSP=0x%lx\n",
			    readl(adp->addr + SIOP_DSP) - scriptaddr);
			if (xs) {
				xs->error = XS_SELTIMEOUT;
				goto end;
			} else {
				goto reset;
			}
		case A_int_reseltarg:
			printf("reselect with invalid target\n");
			goto reset;
		case A_int_resellun:
			target = readb(adp->addr + SIOP_SCRATCHA) & 0xf;
			lun = readb(adp->addr + SIOP_SCRATCHA + 1);
			tag = readb(adp->addr + SIOP_SCRATCHA + 2);
			if (target != adp->xs->target ||
			    lun != adp->xs->lun ||
			    tag != 0) {
				printf("unknown resellun:"
				    " target %d lun %d tag %d\n",
				    target, lun, tag);
				goto reset;
			}
			siop_xfer = adp->xfer;
			dsa = local_to_PCI((u_long)siop_xfer);
			writel(adp->addr + SIOP_DSP,
			    dsa + sizeof(struct siop_common_xfer) +
			    Ent_ldsa_reload_dsa);
			_wbinv((u_long)siop_xfer, sizeof(*siop_xfer));
			return 1;
		case A_int_reseltag:
			printf("reselect with invalid tag\n");
			goto reset;
		case A_int_disc:
			offset = readb(adp->addr + SIOP_SCRATCHA + 1);
			siop_sdp(adp, siop_xfer, xs, offset);
#if 0
			/* we start again with no offset */
			siop_cmd->saved_offset = SIOP_NOOFFSET;
#endif
			_wbinv((u_long)siop_xfer, sizeof(*siop_xfer));
			CALL_SCRIPT(Ent_script_sched);
			return 1;
		case A_int_resfail:
			printf("reselect failed\n");
			return  1;
		case A_int_done:
			if (xs == NULL) {
				printf("done without command, DSA=0x%lx\n",
				    local_to_PCI((u_long)adp->xfer));
				return 1;
			}
			/* update resid.  */
			offset = readb(adp->addr + SIOP_SCRATCHA + 1);
#if 0
			/*
			 * if we got a disconnect between the last data phase
			 * and the status phase, offset will be 0. In this
			 * case, siop_cmd->saved_offset will have the proper
			 * value if it got updated by the controller
			 */
			if (offset == 0 &&
			    siop_cmd->saved_offset != SIOP_NOOFFSET)
				offset = siop_cmd->saved_offset;
#endif
			siop_update_resid(adp, siop_xfer, xs, offset);
			goto end;
		default:
			printf("unknown irqcode %x\n", irqcode);
			if (xs) {
				xs->error = XS_SELTIMEOUT;
				goto end;
			}
			goto reset;
		}
		return 1;
	}
	/* We just shouldn't get there */
	panic("siop_intr: I shouldn't be there !");

	return 1;

end:
	/*
	 * restart the script now if command completed properly
	 * Otherwise wait for siop_scsicmd_end(), we may need to cleanup the
	 * queue
	 */
	xs->status = ctoh32(siop_xfer->siop_tables.status);
	if (xs->status == SCSI_OK)
		writel(adp->addr + SIOP_DSP, scriptaddr + Ent_script_sched);
	else
		restart = 1;
	siop_scsicmd_end(adp, xs);
	if (restart)
		writel(adp->addr + SIOP_DSP, scriptaddr + Ent_script_sched);

	return 1;
}

static void
siop_scsicmd_end(struct siop_adapter *adp, struct scsi_xfer *xs)
{

	switch(xs->status) {
	case SCSI_OK:
		xs->error = XS_NOERROR;
		break;
	case SCSI_BUSY:
	case SCSI_CHECK:
	case SCSI_QUEUE_FULL:
		xs->error = XS_BUSY;
		break;
	case SCSI_SIOP_NOCHECK:
		/*
		 * don't check status, xs->error is already valid
		 */
		break;
	case SCSI_SIOP_NOSTATUS:
		/*
		 * the status byte was not updated, cmd was
		 * aborted
		 */
		xs->error = XS_SELTIMEOUT;
		break;
	default:
		printf("invalid status code %d\n", xs->status);
		xs->error = XS_DRIVER_STUFFUP;
	}
	_inv((u_long)xs->cmd, xs->cmdlen);
	if (xs->datalen != 0)
		_inv((u_long)xs->data, xs->datalen);
	xs->xs_status = XS_STS_DONE;
}

static int
siop_scsi_request(struct siop_adapter *adp, struct scsi_xfer *xs)
{
	void *xfer = adp->xfer;
	int timo, error;

	if (adp->sel_t != xs->target) {
		const int free_lo = __arraycount(siop_script);
		int i;
		void *scriptaddr = (void *)local_to_PCI((u_long)adp->script);

		if (adp->sel_t != -1)
			adp->script[Ent_resel_targ0 / 4 + adp->sel_t * 2] =
			    htoc32(0x800c00ff);

		for (i = 0; i < __arraycount(lun_switch); i++)
			adp->script[free_lo + i] = htoc32(lun_switch[i]);
		adp->script[free_lo + E_abs_lunsw_return_Used[0]] =
		    htoc32(scriptaddr + Ent_lunsw_return);

		siop_add_reselsw(adp, xs->target, free_lo);

		adp->sel_t = xs->target;
	}

restart:

	siop_setuptables(adp, xfer, xs);

	/* load the DMA maps */
	if (xs->datalen != 0)
		_inv((u_long)xs->data, xs->datalen);
	_wbinv((u_long)xs->cmd, xs->cmdlen);

	_wbinv((u_long)xfer, sizeof(struct siop_xfer));
	siop_start(adp, xs);

	adp->xs = xs;
	timo = 0;
	while (!(xs->xs_status & XS_STS_DONE)) {
		delay(1000);
		siop_intr(adp);

		if (timo++ > 3000) {		/* XXXX: 3sec */
			printf("%s: timeout\n", __func__);
			return ETIMEDOUT;
		}
	}

	if (xs->error != XS_NOERROR) {
		if (xs->error == XS_BUSY || xs->status == SCSI_CHECK)
			scsi_request_sense(adp, xs);

		switch (xs->error) {
		case XS_SENSE:
		case XS_SHORTSENSE:
			error = scsi_interpret_sense(adp, xs);
			break;
		case XS_RESOURCE_SHORTAGE:
			printf("adapter resource shortage\n");

			/* FALLTHROUGH */
		case XS_BUSY:
			error = EBUSY;
			break;
		case XS_REQUEUE:
			printf("XXXX: requeue...\n");
			error = ERESTART;
			break;
		case XS_SELTIMEOUT:
		case XS_TIMEOUT:
			error = EIO;
			break;
		case XS_RESET:
			error = EIO;
			break;
		case XS_DRIVER_STUFFUP:
			printf("generic HBA error\n");
			error = EIO;
			break;
		default:
			printf("invalid return code from adapter: %d\n",
			    xs->error);
			error = EIO;
			break;
		}
		if (error == ERESTART) {
			xs->error = XS_NOERROR;
			xs->status = SCSI_OK;
			xs->xs_status &= ~XS_STS_DONE;
			goto restart;
		}
		return error;
	}
	return 0;
}

static void
siop_start(struct siop_adapter *adp, struct scsi_xfer *xs)
{
	struct siop_xfer *siop_xfer = adp->xfer;
	uint32_t dsa, *script = adp->script;
	int slot;
	void *scriptaddr = (void *)local_to_PCI((u_long)script);
	const int siop_common_xfer_size = sizeof(struct siop_common_xfer);

	/*
	 * The queue management here is a bit tricky: the script always looks
	 * at the slot from first to last, so if we always use the first
	 * free slot commands can stay at the tail of the queue ~forever.
	 * The algorithm used here is to restart from the head when we know
	 * that the queue is empty, and only add commands after the last one.
	 * When we're at the end of the queue wait for the script to clear it.
	 * The best thing to do here would be to implement a circular queue,
	 * but using only 53c720 features this can be "interesting".
	 * A mid-way solution could be to implement 2 queues and swap orders.
	 */
	slot = adp->currschedslot;
	/*
	 * If the instruction is 0x80000000 (JUMP foo, IF FALSE) the slot is
	 * free. As this is the last used slot, all previous slots are free,
	 * we can restart from 0.
	 */
	if (ctoh32(script[(Ent_script_sched_slot0 / 4) + slot * 2]) ==
	    0x80000000) {
		slot = adp->currschedslot = 0;
	} else {
		slot++;
	}
	/*
	 * find a free scheduler slot and load it.
	 */
#define SIOP_NSLOTS	0x40
	for (; slot < SIOP_NSLOTS; slot++) {
		/*
		 * If cmd if 0x80000000 the slot is free
		 */
		if (ctoh32(script[(Ent_script_sched_slot0 / 4) + slot * 2]) ==
		    0x80000000)
			break;
	}
	if (slot == SIOP_NSLOTS) {
		/*
		 * no more free slot, no need to continue. freeze the queue
		 * and requeue this command.
		 */
		printf("no mode free slot\n");
		return;
	}

	/* patch scripts with DSA addr */
	dsa = local_to_PCI((u_long)siop_xfer);

	/* CMD script: MOVE MEMORY addr */
	siop_xfer->resel[E_ldsa_abs_slot_Used[0]] =
	    htoc32(scriptaddr + Ent_script_sched_slot0 + slot * 8);
	_wbinv((u_long)siop_xfer, sizeof(*siop_xfer));
	/* scheduler slot: JUMP ldsa_select */
	script[(Ent_script_sched_slot0 / 4) + slot * 2 + 1] =
	    htoc32(dsa + siop_common_xfer_size + Ent_ldsa_select);
	/*
	 * Change JUMP cmd so that this slot will be handled
	 */
	script[(Ent_script_sched_slot0 / 4) + slot * 2] = htoc32(0x80080000);
	adp->currschedslot = slot;

	/* make sure SCRIPT processor will read valid data */
	_wbinv((u_long)script, SIOP_SCRIPT_SIZE);
	/* Signal script it has some work to do */
	writeb(adp->addr + SIOP_ISTAT, ISTAT_SIGP);
	/* and wait for IRQ */
}

static void
siop_xfer_setup(struct siop_xfer *xfer, void *scriptaddr)
{
	const int off_msg_in = offsetof(struct siop_common_xfer, msg_in);
	const int off_status = offsetof(struct siop_common_xfer, status);
	uint32_t dsa, *scr;
	int i;

	memset(xfer, 0, sizeof(*xfer));
	dsa = local_to_PCI((u_long)xfer);
	xfer->siop_tables.t_msgout.count = htoc32(1);
	xfer->siop_tables.t_msgout.addr = htoc32(dsa);
	xfer->siop_tables.t_msgin.count = htoc32(1);
	xfer->siop_tables.t_msgin.addr = htoc32(dsa + off_msg_in);
	xfer->siop_tables.t_extmsgin.count = htoc32(2);
	xfer->siop_tables.t_extmsgin.addr = htoc32(dsa + off_msg_in + 1);
	xfer->siop_tables.t_extmsgdata.addr = htoc32(dsa + off_msg_in + 3);
	xfer->siop_tables.t_status.count = htoc32(1);
	xfer->siop_tables.t_status.addr = htoc32(dsa + off_status);

	/* The select/reselect script */
	scr = xfer->resel;
	for (i = 0; i < __arraycount(load_dsa); i++)
		scr[i] = htoc32(load_dsa[i]);

	/*
	 * 0x78000000 is a 'move data8 to reg'. data8 is the second
	 * octet, reg offset is the third.
	 */
	scr[Ent_rdsa0 / 4] = htoc32(0x78100000 | ((dsa & 0x000000ff) <<  8));
	scr[Ent_rdsa1 / 4] = htoc32(0x78110000 | ( dsa & 0x0000ff00       ));
	scr[Ent_rdsa2 / 4] = htoc32(0x78120000 | ((dsa & 0x00ff0000) >>  8));
	scr[Ent_rdsa3 / 4] = htoc32(0x78130000 | ((dsa & 0xff000000) >> 16));
	scr[E_ldsa_abs_reselected_Used[0]] =
	    htoc32(scriptaddr + Ent_reselected);
	scr[E_ldsa_abs_reselect_Used[0]] = htoc32(scriptaddr + Ent_reselect);
	scr[E_ldsa_abs_selected_Used[0]] = htoc32(scriptaddr + Ent_selected);
	scr[E_ldsa_abs_data_Used[0]] =
	    htoc32(dsa + sizeof(struct siop_common_xfer) + Ent_ldsa_data);
	/* JUMP foo, IF FALSE - used by MOVE MEMORY to clear the slot */
	scr[Ent_ldsa_data / 4] = htoc32(0x80000000);
}

static int
siop_add_reselsw(struct siop_adapter *adp, int target, int lunsw_off)
{
	uint32_t *script = adp->script;
	int reseloff;
	void *scriptaddr = (void *)local_to_PCI((u_long)adp->script);

	/*
	 * add an entry to resel switch
	 */
	reseloff = Ent_resel_targ0 / 4 + target * 2;
	if ((ctoh32(script[reseloff]) & 0xff) != 0xff) {
		/* it's not free */
		printf("siop: resel switch full\n");
		return EBUSY;
	}

	/* JUMP abs_foo, IF target | 0x80; */
	script[reseloff + 0] = htoc32(0x800c0080 | target);
	script[reseloff + 1] =
	    htoc32(scriptaddr + lunsw_off * 4 + Ent_lun_switch_entry);

	siop_update_scntl3(adp, target, lunsw_off);
	return 0;
}

static void
siop_update_scntl3(struct siop_adapter *adp, int target, int lunsw_off)
{
	uint32_t *script = adp->script;

	/* MOVE target->id >> 24 TO SCNTL3 */
	script[lunsw_off + (Ent_restore_scntl3 / 4)] =
	    htoc32(0x78030000 | ((adp->clock_div >> 16) & 0x0000ff00));
	/* MOVE target->id >> 8 TO SXFER */
	script[lunsw_off + (Ent_restore_scntl3 / 4) + 2] =
	    htoc32(0x78050000 | (0x000000000 & 0x0000ff00));
	_wbinv((u_long)script, SIOP_SCRIPT_SIZE);
}


/*
 * SCSI functions
 */

static int
_scsi_inquire(struct siop_adapter *adp, int t, int l, int buflen, char *buf)
{
	struct scsipi_inquiry *cmd = (struct scsipi_inquiry *)adp->cmd;
	struct scsipi_inquiry_data *inqbuf =
	    (struct scsipi_inquiry_data *)adp->data;
	struct scsi_xfer xs;
	int error;

	memset(cmd, 0, sizeof(*cmd));
	cmd->opcode = INQUIRY;
	cmd->length = SCSIPI_INQUIRY_LENGTH_SCSI2;
	memset(inqbuf, 0, sizeof(*inqbuf));

	memset(&xs, 0, sizeof(xs));
	xs.target = t;
	xs.lun = l;
	xs.cmdlen = sizeof(*cmd);
	xs.cmd = (void *)cmd;
	xs.datalen = SCSIPI_INQUIRY_LENGTH_SCSI2;
	xs.data = (void *)inqbuf;

	xs.error = XS_NOERROR;
	xs.resid = xs.datalen;
	xs.status = SCSI_OK;

	error = siop_scsi_request(adp, &xs);
	if (error != 0)
		return error;

	memcpy(buf, inqbuf, buflen);
	return 0;
}

static void
scsi_request_sense(struct siop_adapter *adp, struct scsi_xfer *xs)
{
	struct scsi_request_sense *cmd = adp->sense;
	struct scsi_sense_data *data = (struct scsi_sense_data *)adp->data;
	struct scsi_xfer sense;
	int error;

	memset(cmd, 0, sizeof(struct scsi_request_sense));
	cmd->opcode = SCSI_REQUEST_SENSE;
	cmd->length = sizeof(struct scsi_sense_data);
	memset(data, 0, sizeof(struct scsi_sense_data));

	memset(&sense, 0, sizeof(sense));
	sense.target = xs->target;
	sense.lun = xs->lun;
	sense.cmdlen = sizeof(struct scsi_request_sense);
	sense.cmd = (void *)cmd;
	sense.datalen = sizeof(struct scsi_sense_data);
	sense.data = (void *)data;

	sense.error = XS_NOERROR;
	sense.resid = sense.datalen;
	sense.status = SCSI_OK;

	error = siop_scsi_request(adp, &sense);
	switch (error) {
	case 0:
		/* we have a valid sense */
		xs->error = XS_SENSE;
		return;
	case EINTR:
		/* REQUEST_SENSE interrupted by bus reset. */
		xs->error = XS_RESET;
		return;
	case EIO:
		 /* request sense couldn't be performed */
		/*
		 * XXX this isn't quite right but we don't have anything
		 * better for now
		 */
		xs->error = XS_DRIVER_STUFFUP;
		return;
	default:
		 /* Notify that request sense failed. */
		xs->error = XS_DRIVER_STUFFUP;
		printf("request sense failed with error %d\n", error);
		return;
	}
}

/*
 * scsi_interpret_sense:
 *
 *	Look at the returned sense and act on the error, determining
 *	the unix error number to pass back.  (0 = report no error)
 *
 *	NOTE: If we return ERESTART, we are expected to haved
 *	thawed the device!
 *
 *	THIS IS THE DEFAULT ERROR HANDLER FOR SCSI DEVICES.
 */
static int
scsi_interpret_sense(struct siop_adapter *adp, struct scsi_xfer *xs)
{
	struct scsi_sense_data *sense;
	u_int8_t key;
	int error = 0;
	uint32_t info;
	static const char *error_mes[] = {
		"soft error (corrected)",
		"not ready", "medium error",
		"non-media hardware failure", "illegal request",
		"unit attention", "readonly device",
		"no data found", "vendor unique",
		"copy aborted", "command aborted",
		"search returned equal", "volume overflow",
		"verify miscompare", "unknown error key"
	};

	sense = (struct scsi_sense_data *)xs->data;

	DPRINTF((" sense debug information:\n"));
	DPRINTF(("\tcode 0x%x valid %d\n",
		SSD_RCODE(sense->response_code),
		sense->response_code & SSD_RCODE_VALID ? 1 : 0));
	DPRINTF(("\tseg 0x%x key 0x%x ili 0x%x eom 0x%x fmark 0x%x\n",
		sense->segment,
		SSD_SENSE_KEY(sense->flags),
		sense->flags & SSD_ILI ? 1 : 0,
		sense->flags & SSD_EOM ? 1 : 0,
		sense->flags & SSD_FILEMARK ? 1 : 0));
	DPRINTF(("\ninfo: 0x%x 0x%x 0x%x 0x%x followed by %d "
		"extra bytes\n",
		sense->info[0],
		sense->info[1],
		sense->info[2],
		sense->info[3],
		sense->extra_len));

	switch (SSD_RCODE(sense->response_code)) {

		/*
		 * Old SCSI-1 and SASI devices respond with
		 * codes other than 70.
		 */
	case 0x00:		/* no error (command completed OK) */
		return 0;
	case 0x04:		/* drive not ready after it was selected */
		if (adp->sd->sc_flags & FLAGS_REMOVABLE)
			adp->sd->sc_flags &= ~FLAGS_MEDIA_LOADED;
		/* XXX - display some sort of error here? */
		return EIO;
	case 0x20:		/* invalid command */
		return EINVAL;
	case 0x25:		/* invalid LUN (Adaptec ACB-4000) */
		return EACCES;

		/*
		 * If it's code 70, use the extended stuff and
		 * interpret the key
		 */
	case 0x71:		/* delayed error */
		key = SSD_SENSE_KEY(sense->flags);
		printf(" DEFERRED ERROR, key = 0x%x\n", key);
		/* FALLTHROUGH */
	case 0x70:
		if ((sense->response_code & SSD_RCODE_VALID) != 0)
			info = _4btol(sense->info);
		else
			info = 0;
		key = SSD_SENSE_KEY(sense->flags);

		switch (key) {
		case SKEY_NO_SENSE:
		case SKEY_RECOVERED_ERROR:
			if (xs->resid == xs->datalen && xs->datalen) {
				/*
				 * Why is this here?
				 */
				xs->resid = 0;	/* not short read */
			}
		case SKEY_EQUAL:
			break;
		case SKEY_NOT_READY:
			if (adp->sd->sc_flags & FLAGS_REMOVABLE)
				adp->sd->sc_flags &= ~FLAGS_MEDIA_LOADED;
			if (sense->asc == 0x3A) {
				error = ENODEV; /* Medium not present */
			} else
				error = EIO;
			break;
		case SKEY_ILLEGAL_REQUEST:
			error = EINVAL;
			break;
		case SKEY_UNIT_ATTENTION:
			if (sense->asc == 0x29 &&
			    sense->ascq == 0x00) {
				/* device or bus reset */
				return ERESTART;
			}
			if (adp->sd->sc_flags & FLAGS_REMOVABLE)
				adp->sd->sc_flags &= ~FLAGS_MEDIA_LOADED;
			if (!(adp->sd->sc_flags & FLAGS_REMOVABLE))
				return ERESTART;
			error = EIO;
			break;
		case SKEY_DATA_PROTECT:
			error = EROFS;
			break;
		case SKEY_BLANK_CHECK:
			break;
		case SKEY_ABORTED_COMMAND:
			error = EIO;
			break;
		case SKEY_VOLUME_OVERFLOW:
			error = ENOSPC;
			break;
		default:
			error = EIO;
			break;
		}

		/* Print brief(er) sense information */
		printf("%s", error_mes[key - 1]);
		if ((sense->response_code & SSD_RCODE_VALID) != 0) {
			switch (key) {
			case SKEY_NOT_READY:
			case SKEY_ILLEGAL_REQUEST:
			case SKEY_UNIT_ATTENTION:
			case SKEY_DATA_PROTECT:
				break;
			case SKEY_BLANK_CHECK:
				printf(", requested size: %d (decimal)",
				    info);
				break;
			case SKEY_ABORTED_COMMAND:
				printf(", cmd 0x%x, info 0x%x",
				    xs->cmd->opcode, info);
				break;
			default:
				printf(", info = %d (decimal)", info);
			}
		}
		if (sense->extra_len != 0) {
			int n;
			printf(", data =");
			for (n = 0; n < sense->extra_len; n++)
				printf(" %x", sense->csi[n]);
		}
		printf("\n");
		return error;

	/*
	 * Some other code, just report it
	 */
	default:
		printf("Sense Error Code 0x%x",
			SSD_RCODE(sense->response_code));
		if ((sense->response_code & SSD_RCODE_VALID) != 0) {
			struct scsi_sense_data_unextended *usense =
			    (struct scsi_sense_data_unextended *)sense;
			printf(" at block no. %d (decimal)",
			    _3btol(usense->block));
		}
		printf("\n");
		return EIO;
	}
}

static int
scsi_probe(struct siop_adapter *adp)
{
	struct scsipi_inquiry_data *inqbuf;
	int found, t, l;
	uint8_t device;
	char buf[SCSIPI_INQUIRY_LENGTH_SCSI2],
	    product[sizeof(inqbuf->product) + 1];

	found = 0;
	for (t = 0; t < 8; t++) {
		if (t == adp->id)
			continue;
		for (l = 0; l < 8; l++) {
			if (_scsi_inquire(adp, t, l, sizeof(buf), buf) != 0)
				continue;

			inqbuf = (struct scsipi_inquiry_data *)buf;
			device = inqbuf->device & SID_TYPE;
			if (device == T_NODEVICE)
				continue;
			if (device != T_DIRECT &&
			    device != T_OPTICAL &&
			    device != T_SIMPLE_DIRECT)
				continue;

			memset(product, 0, sizeof(product));
			strncpy(product, inqbuf->product, sizeof(product) - 1);
			printf("/dev/disk/scsi/0%d%d: <%s>\n", t, l, product);
			found++;
		}
	}
	return found;
}

int
scsi_inquire(struct sd_softc *sd, int buflen, void *buf)
{
	struct siop_adapter *adp;
	int error;

	if (sd->sc_bus != 0)
		return ENOTSUP;
	if (adapt.addr == 0)
		return ENOENT;
	adp = &adapt;

	adp->sd = sd;
	error = _scsi_inquire(adp, sd->sc_target, sd->sc_lun, buflen, buf);
	adp->sd = NULL;

	return error;
}

/*
 * scsi_mode_sense
 *	get a sense page from a device
 */

int
scsi_mode_sense(struct sd_softc *sd, int byte2, int page,
		  struct scsi_mode_parameter_header_6 *data, int len)
{
	struct scsi_mode_sense_6 cmd;

	memset(&cmd, 0, sizeof(cmd));
	cmd.opcode = SCSI_MODE_SENSE_6;
	cmd.byte2 = byte2;
	cmd.page = page;
	cmd.length = len & 0xff;

	return scsi_command(sd, (void *)&cmd, sizeof(cmd), (void *)data, len);
}

int
scsi_command(struct sd_softc *sd, void *cmd, int cmdlen, void *data,
	     int datalen)
{
	struct siop_adapter *adp;
	struct scsi_xfer xs;
	int error;

	if (sd->sc_bus != 0)
		return ENOTSUP;
	if (adapt.addr == 0)
		return ENOENT;
	adp = &adapt;

	memcpy(adp->cmd, cmd, cmdlen);
	adp->sd = sd;

	memset(&xs, 0, sizeof(xs));
	xs.target = sd->sc_target;
	xs.lun = sd->sc_lun;
	xs.cmdlen = cmdlen;
	xs.cmd = adp->cmd;
	xs.datalen = datalen;
	xs.data = adp->data;

	xs.error = XS_NOERROR;
	xs.resid = datalen;
	xs.status = SCSI_OK;

	error = siop_scsi_request(adp, &xs);
	adp->sd = NULL;
	if (error != 0)
		return error;

	if (datalen > 0)
		memcpy(data, adp->data, datalen);
	return 0;
}

/*
 * Initialize the device.
 */
int
siop_init(int bus, int dev, int func)
{
	struct siop_adapter tmp;
	struct siop_xfer *xfer;
	struct scsipi_generic *cmd;
	struct scsi_request_sense *sense;
	uint32_t reg;
	u_long addr;
	uint32_t *script;
	int slot, id, i;
	void *scriptaddr;
	u_char *data;
	const int clock_div = 3;		/* 53c810 */

	slot = PCISlotnum(bus, dev, func);
	if (slot == -1)
		return ENOENT;

	addr = PCIAddress(slot, 1, PCI_MAPREG_TYPE_MEM);
	if (addr == 0xffffffff)
		return EINVAL;
	enablePCI(slot, 0, 1, 1);

	script = ALLOC(uint32_t, SIOP_SCRIPT_SIZE);
	if (script == NULL)
		return ENOMEM;
	scriptaddr = (void *)local_to_PCI((u_long)script);
	cmd = ALLOC(struct scsipi_generic, SIOP_SCSI_COMMAND_SIZE);
	if (cmd == NULL)
		return ENOMEM;
	sense = ALLOC(struct scsi_request_sense, SIOP_SCSI_COMMAND_SIZE);
	if (sense == NULL)
		return ENOMEM;
	data = ALLOC(u_char, SIOP_SCSI_DATA_SIZE);
	if (data == NULL)
		return ENOMEM;
	xfer = ALLOC(struct siop_xfer, sizeof(struct siop_xfer));
	if (xfer == NULL)
		return ENOMEM;
	siop_xfer_setup(xfer, scriptaddr);

	id = readb(addr + SIOP_SCID) & SCID_ENCID_MASK;

	/* reset bus */
	reg = readb(addr + SIOP_SCNTL1);
	writeb(addr + SIOP_SCNTL1, reg | SCNTL1_RST);
	delay(100);
	writeb(addr + SIOP_SCNTL1, reg);

	/* reset the chip */
	writeb(addr + SIOP_ISTAT, ISTAT_SRST);
	delay(1000);
	writeb(addr + SIOP_ISTAT, 0);

	/* init registers */
	writeb(addr + SIOP_SCNTL0, SCNTL0_ARB_MASK | SCNTL0_EPC | SCNTL0_AAP);
	writeb(addr + SIOP_SCNTL1, 0);
	writeb(addr + SIOP_SCNTL3, clock_div);
	writeb(addr + SIOP_SXFER, 0);
	writeb(addr + SIOP_DIEN, 0xff);
	writeb(addr + SIOP_SIEN0, 0xff & ~(SIEN0_CMP | SIEN0_SEL | SIEN0_RSL));
	writeb(addr + SIOP_SIEN1, 0xff & ~(SIEN1_HTH | SIEN1_GEN));
	writeb(addr + SIOP_STEST2, 0);
	writeb(addr + SIOP_STEST3, STEST3_TE);
	writeb(addr + SIOP_STIME0, (0xb << STIME0_SEL_SHIFT));
	writeb(addr + SIOP_SCID, id | SCID_RRE);
	writeb(addr + SIOP_RESPID0, 1 << id);
	writeb(addr + SIOP_DCNTL, DCNTL_COM);

	/* BeBox uses PCIC */
	writeb(addr + SIOP_STEST1, STEST1_SCLK);

	siop_pci_reset(addr);

	/* copy and patch the script */
	for (i = 0; i < __arraycount(siop_script); i++)
		script[i] = htoc32(siop_script[i]);
	for (i = 0; i < __arraycount(E_abs_msgin_Used); i++)
		script[E_abs_msgin_Used[i]] =
		    htoc32(scriptaddr + Ent_msgin_space);

	/* start script */
	_wbinv((u_long)script, SIOP_SCRIPT_SIZE);
	writel(addr + SIOP_DSP, (int)scriptaddr + Ent_reselect);

	memset(&tmp, 0, sizeof(tmp));
	tmp.id = id;
	tmp.clock_div = clock_div;
	tmp.addr = addr;
	tmp.script = script;
	tmp.xfer = xfer;
	tmp.cmd = cmd;
	tmp.sense = sense;
	tmp.data = data;
	tmp.currschedslot = 0;
	tmp.sel_t = -1;

	if (scsi_probe(&tmp) == 0)
		return ENXIO;
	adapt = tmp;
	return 0;
}
