/* $NetBSD: siop.c,v 1.10 2024/07/02 05:34:08 rin 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.
*/
/*
* 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 */
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 adjust 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];
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)
printf("PHASE STATUS: siop_clearfifo...\n");
// 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)
printf("PHASE MSGIN: siop_clearfifo...\n");
// 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");
}
//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);
/*
* 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);
/* 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;
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 have
* 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;
/* otherwise use the default */
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:
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;
}
}
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,
SCSIPI_INQUIRY_LENGTH_SCSI2, &buf) != 0)
continue;
/* 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);