/*-
 * Copyright (c) 1999 Michael Smith
 * 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 AND CONTRIBUTORS ``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 OR CONTRIBUTORS 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.
 *
 *	$FreeBSD: src/sys/dev/amr/amr.c,v 1.7 2000/02/17 23:33:57 msmith Exp $
 */

/*
 * Driver for the AMI MegaRaid family of controllers
 */

#include <sys/param.h>
#include <sys/systm.h>
#include <sys/malloc.h>
#include <sys/kernel.h>

#include <sys/buf.h>
#include <sys/bus.h>
#include <sys/conf.h>
#include <sys/devicestat.h>
#include <sys/disk.h>

#include <machine/resource.h>
#include <machine/bus.h>
#include <machine/clock.h>
#include <sys/rman.h>

#include <dev/amr/amrio.h>
#include <dev/amr/amrreg.h>
#include <dev/amr/amrvar.h>

#if 0
#define debug(fmt, args...)	printf("%s: " fmt "\n", __FUNCTION__ , ##args)
#else
#define debug(fmt, args...)
#endif

#define AMR_CDEV_MAJOR	132

static struct cdevsw amr_cdevsw = {
		/* open */	amr_open,
		/* close */	amr_close,
		/* read */	noread,
		/* write */	nowrite,
		/* ioctl */	amr_ioctl,
		/* poll */	nopoll,
		/* mmap */	nommap,
		/* strategy */	nostrategy,
		/* name */ 	"amr",
		/* maj */	AMR_CDEV_MAJOR,
		/* dump */	nodump,
		/* psize */ 	nopsize,
		/* flags */	0,
		/* bmaj */	254	/* XXX magic no-bdev */
};

static int	cdev_registered = 0;
devclass_t	amr_devclass;

/*
 * Command wrappers
 */
static int			amr_query_controller(struct amr_softc *sc);
static void			*amr_enquiry(struct amr_softc *sc, size_t bufsize, 
					     u_int8_t cmd, u_int8_t cmdsub, u_int8_t cmdqual);
static int			amr_flush(struct amr_softc *sc);
static void			amr_startio(struct amr_softc *sc);
static void			amr_completeio(struct amr_command *ac);

/*
 * Command processing.
 */
static int			amr_wait_command(struct amr_command *ac);
static int			amr_poll_command(struct amr_command *ac);
static int			amr_getslot(struct amr_command *ac);
static void			amr_mapcmd(struct amr_command *ac);
static void			amr_unmapcmd(struct amr_command *ac);
static int			amr_start(struct amr_command *ac);
static int			amr_done(struct amr_softc *sc);
static void			amr_complete(struct amr_softc *sc);

/*
 * Command buffer allocation.
 */
static struct amr_command	*amr_alloccmd(struct amr_softc *sc);
static void			amr_releasecmd(struct amr_command *ac);
static void			amr_freecmd(struct amr_command *ac);

/*
 * Interface-specific shims
 */
static void			amr_quartz_submit_command(struct amr_softc *sc);
static int			amr_quartz_get_work(struct amr_softc *sc, struct amr_mailbox *mbsave);
static void			amr_quartz_attach_mailbox(struct amr_softc *sc);

static void			amr_std_submit_command(struct amr_softc *sc);
static int			amr_std_get_work(struct amr_softc *sc, struct amr_mailbox *mbsave);
static void			amr_std_attach_mailbox(struct amr_softc *sc);

/*
 * Debugging
 */
static void			amr_printcommand(struct amr_command *ac);

/********************************************************************************
 ********************************************************************************
                                                                Public Interfaces
 ********************************************************************************
 ********************************************************************************/

/********************************************************************************
 * Free all of the resources associated with (sc)
 *
 * Should not be called if the controller is active.
 */
void
amr_free(struct amr_softc *sc)
{
    struct amr_command	*ac;
    u_int8_t		*p;
    
    debug("called");

    
    /* throw away any command buffers */
    while ((ac = TAILQ_FIRST(&sc->amr_freecmds)) != NULL) {
	TAILQ_REMOVE(&sc->amr_freecmds, ac, ac_link);
	amr_freecmd(ac);
    }

    /* destroy data-transfer DMA tag */
    if (sc->amr_buffer_dmat)
	bus_dma_tag_destroy(sc->amr_buffer_dmat);

    /* free and destroy DMA memory and tag for s/g lists */
    if (sc->amr_sgtable)
	bus_dmamem_free(sc->amr_sg_dmat, sc->amr_sgtable, sc->amr_sg_dmamap);
    if (sc->amr_sg_dmat)
	bus_dma_tag_destroy(sc->amr_sg_dmat);

    /* free and destroy DMA memory and tag for mailbox */
    if (sc->amr_mailbox) {
	p = (u_int8_t *)sc->amr_mailbox;
	bus_dmamem_free(sc->amr_sg_dmat, p - 16, sc->amr_sg_dmamap);
    }
    if (sc->amr_sg_dmat)
	bus_dma_tag_destroy(sc->amr_sg_dmat);

    /* disconnect the interrupt handler */
    if (sc->amr_intr)
	bus_teardown_intr(sc->amr_dev, sc->amr_irq, sc->amr_intr);
    if (sc->amr_irq != NULL)
	bus_release_resource(sc->amr_dev, SYS_RES_IRQ, 0, sc->amr_irq);

    /* destroy the parent DMA tag */
    if (sc->amr_parent_dmat)
	bus_dma_tag_destroy(sc->amr_parent_dmat);

    /* release the register window mapping */
    if (sc->amr_reg != NULL)
	bus_release_resource(sc->amr_dev,
			     (sc->amr_type == AMR_TYPE_QUARTZ) ? SYS_RES_MEMORY : SYS_RES_IOPORT,
			     AMR_CFG_BASE, sc->amr_reg);
}

/********************************************************************************
 * Allocate and map the scatter/gather table in bus space.
 */
static void
amr_dma_map_sg(void *arg, bus_dma_segment_t *segs, int nseg, int error)
{
    struct amr_softc	*sc = (struct amr_softc *)arg;

    debug("called");

    /* save base of s/g table's address in bus space */
    sc->amr_sgbusaddr = segs->ds_addr;
}

static int
amr_sglist_map(struct amr_softc *sc)
{
    size_t	segsize;
    int		error;

    debug("called");

    /* destroy any existing mappings */
    if (sc->amr_sgtable)
	bus_dmamem_free(sc->amr_sg_dmat, sc->amr_sgtable, sc->amr_sg_dmamap);
    if (sc->amr_sg_dmat)
	bus_dma_tag_destroy(sc->amr_sg_dmat);

    /*
     * Create a single tag describing a region large enough to hold all of
     * the s/g lists we will need.
     */
    segsize = sizeof(struct amr_sgentry) * AMR_NSEG * sc->amr_maxio;
    error = bus_dma_tag_create(sc->amr_parent_dmat, 	/* parent */
			       1, 0, 			/* alignment, boundary */
			       BUS_SPACE_MAXADDR,	/* lowaddr */
			       BUS_SPACE_MAXADDR, 	/* highaddr */
			       NULL, NULL, 		/* filter, filterarg */
			       segsize, 1,		/* maxsize, nsegments */
			       BUS_SPACE_MAXSIZE_32BIT,	/* maxsegsize */
			       0,			/* flags */
			       &sc->amr_sg_dmat);
    if (error != 0) {
	device_printf(sc->amr_dev, "can't allocate scatter/gather DMA tag\n");
	return(ENOMEM);
    }

    /*
     * Allocate enough s/g maps for all commands and permanently map them into
     * controller-visible space.
     *	
     * XXX this assumes we can get enough space for all the s/g maps in one 
     * contiguous slab.  We may need to switch to a more complex arrangement where
     * we allocate in smaller chunks and keep a lookup table from slot to bus address.
     */
    error = bus_dmamem_alloc(sc->amr_sg_dmat, (void **)&sc->amr_sgtable, BUS_DMA_NOWAIT, &sc->amr_sg_dmamap);
    if (error) {
	device_printf(sc->amr_dev, "can't allocate s/g table\n");
	return(ENOMEM);
    }
    bus_dmamap_load(sc->amr_sg_dmat, sc->amr_sg_dmamap, sc->amr_sgtable, segsize, amr_dma_map_sg, sc, 0);
    return(0);
}

/********************************************************************************
 * Allocate and set up mailbox areas for the controller (sc)
 *
 * The basic mailbox structure should be 16-byte aligned.  This means that the
 * mailbox64 structure has 4 bytes hanging off the bottom.
 */
static void
amr_map_mailbox(void *arg, bus_dma_segment_t *segs, int nseg, int error)
{
    struct amr_softc	*sc = (struct amr_softc *)arg;
    
    debug("called");

    /* save phsyical base of the basic mailbox structure */
    sc->amr_mailboxphys = segs->ds_addr + 16;
}

static int
amr_setup_mbox(struct amr_softc *sc)
{
    int		error;
    u_int8_t	*p;
    
    debug("called");

    /*
     * Create a single tag describing a region large enough to hold the entire
     * mailbox.
     */
    error = bus_dma_tag_create(sc->amr_parent_dmat,	/* parent */
			       16, 0,			/* alignment, boundary */
			       BUS_SPACE_MAXADDR,	/* lowaddr */
			       BUS_SPACE_MAXADDR,	/* highaddr */
			       NULL, NULL,		/* filter, filterarg */
			       sizeof(struct amr_mailbox) + 16, 1, /* maxsize, nsegments */
			       BUS_SPACE_MAXSIZE_32BIT,	/* maxsegsize */
			       0,			/* flags */
			       &sc->amr_mailbox_dmat);
    if (error != 0) {
	device_printf(sc->amr_dev, "can't allocate mailbox tag\n");
	return(ENOMEM);
    }

    /*
     * Allocate the mailbox structure and permanently map it into
     * controller-visible space.
     */
    error = bus_dmamem_alloc(sc->amr_mailbox_dmat, (void **)&p, BUS_DMA_NOWAIT, 
			     &sc->amr_mailbox_dmamap);
    if (error) {
	device_printf(sc->amr_dev, "can't allocate mailbox memory\n");
	return(ENOMEM);
    }
    bus_dmamap_load(sc->amr_mailbox_dmat, sc->amr_mailbox_dmamap, p, 
		    sizeof(struct amr_mailbox64), amr_map_mailbox, sc, 0);
    /*
     * Conventional mailbox is inside the mailbox64 region.
     */
    bzero(p, sizeof(struct amr_mailbox64));
    sc->amr_mailbox64 = (struct amr_mailbox64 *)(p + 12);
    sc->amr_mailbox = (struct amr_mailbox *)(p + 16);

    if (sc->amr_type == AMR_TYPE_STD) {
	/* XXX we have to tell the controller where we put it */
    }
    return(0);
}


/********************************************************************************
 * Initialise the controller and softc.
 */
int
amr_attach(struct amr_softc *sc)
{
    int			rid, error;

    /*
     * Initialise per-controller queues.
     */
    TAILQ_INIT(&sc->amr_work);
    TAILQ_INIT(&sc->amr_freecmds);
    bufq_init(&sc->amr_bufq);

    /*
     * Configure for this controller type.
     */
    if (sc->amr_type == AMR_TYPE_QUARTZ) {
	sc->amr_submit_command = amr_quartz_submit_command;
	sc->amr_get_work       = amr_quartz_get_work;
	sc->amr_attach_mailbox = amr_quartz_attach_mailbox;
    } else {
	sc->amr_submit_command = amr_std_submit_command;
	sc->amr_get_work       = amr_std_get_work;
	sc->amr_attach_mailbox = amr_std_attach_mailbox;
    }

    /*
     * Allocate and connect our interrupt.
     */
    rid = 0;
    sc->amr_irq = bus_alloc_resource(sc->amr_dev, SYS_RES_IRQ, &rid, 0, ~0, 1, RF_SHAREABLE | RF_ACTIVE);
    if (sc->amr_irq == NULL) {
        device_printf(sc->amr_dev, "couldn't allocate interrupt\n");
        amr_free(sc);
        return(ENXIO);
    }
    error = bus_setup_intr(sc->amr_dev, sc->amr_irq, INTR_TYPE_BIO,  amr_intr, sc, &sc->amr_intr);
    if (error) {
        device_printf(sc->amr_dev, "couldn't set up interrupt\n");
        amr_free(sc);
        return(ENXIO);
    }

    /*
     * Create DMA tag for mapping buffers into controller-addressable space.
     */
    error = bus_dma_tag_create(sc->amr_parent_dmat,     /* parent */
                               1, 0,                    /* alignment, boundary */
                               BUS_SPACE_MAXADDR,       /* lowaddr */
                               BUS_SPACE_MAXADDR,       /* highaddr */
                               NULL, NULL,              /* filter, filterarg */
                               MAXBSIZE, AMR_NSEG,      /* maxsize, nsegments */
                               BUS_SPACE_MAXSIZE_32BIT, /* maxsegsize */
                               0,                       /* flags */
                               &sc->amr_buffer_dmat);
    if (error != 0) {
        device_printf(sc->amr_dev, "can't allocate buffer DMA tag\n");
        return(ENOMEM);
    }

    /*
     * Allocate and set up mailbox in a bus-visible fashion, attach to controller.
     */
    if ((error = amr_setup_mbox(sc)) != 0)
	return(error);
    sc->amr_attach_mailbox(sc);

    /*
     * Build a temporary set of scatter/gather buffers.
     */
    sc->amr_maxio = 2;
    if (amr_sglist_map(sc))
	return(ENXIO);

    /*
     * Quiz controller for features and limits.
     */
    if (amr_query_controller(sc))
	return(ENXIO);

    /*
     * Rebuild the scatter/gather buffers now we know how many we need.
     */
    if (amr_sglist_map(sc))
	return(ENXIO);

    return(0);
}

/********************************************************************************
 * Locate disk resources and attach children to them.
 */
void
amr_startup(struct amr_softc *sc)
{
    struct amr_logdrive	*dr;
    int			i, error;
    
    debug("called");

    /* get up-to-date drive information */
    if (amr_query_controller(sc)) {
	device_printf(sc->amr_dev, "couldn't scan controller for drives\n");
	return;
    }

    /* iterate over available drives */
    for (i = 0, dr = &sc->amr_drive[0]; (i < AMR_MAXLD) && (dr->al_size != 0xffffffff); i++, dr++) {
	/* are we already attached to this drive? */
	if (dr->al_disk == 0) {
	    /* generate geometry information */
	    if (dr->al_size > 0x200000) {	/* extended translation? */
		dr->al_heads = 255;
		dr->al_sectors = 63;
	    } else {
		dr->al_heads = 64;
		dr->al_sectors = 32;
	    }
	    dr->al_cylinders = dr->al_size / (dr->al_heads * dr->al_sectors);
	    
	    dr->al_disk = device_add_child(sc->amr_dev, NULL, -1);
	    if (dr->al_disk == 0)
		device_printf(sc->amr_dev, "device_add_child failed\n");
	    device_set_ivars(dr->al_disk, dr);
	}
    }
    
    if ((error = bus_generic_attach(sc->amr_dev)) != 0)
	device_printf(sc->amr_dev, "bus_generic_attach returned %d\n", error);
    
    /* mark controller back up */
    sc->amr_state &= ~AMR_STATE_SHUTDOWN;

    /* interrupts will be enabled before we do anything more */
    sc->amr_state |= AMR_STATE_INTEN;
}

/********************************************************************************
 * Disconnect from the controller completely, in preparation for unload.
 */
int
amr_detach(device_t dev)
{
    struct amr_softc	*sc = device_get_softc(dev);
    int			error;

    debug("called");

    if (sc->amr_state & AMR_STATE_OPEN)
	return(EBUSY);

    if ((error = amr_shutdown(dev)))
	return(error);

    amr_free(sc);

    /*
     * Deregister the control device on last detach.
     */
    if (--cdev_registered == 0)
	cdevsw_remove(&amr_cdevsw);

    return(0);
}

/********************************************************************************
 * Bring the controller down to a dormant state and detach all child devices.
 *
 * This function is called before detach, system shutdown, or before performing
 * an operation which may add or delete system disks.  (Call amr_startup to
 * resume normal operation.)
 *
 * Note that we can assume that the bufq on the controller is empty, as we won't
 * allow shutdown if any device is open.
 */
int
amr_shutdown(device_t dev)
{
    struct amr_softc	*sc = device_get_softc(dev);
    struct amrd_softc	*ad;
    int			i, s, error;

    debug("called");

    s = splbio();
    error = 0;

    /* assume we're going to shut down */
    sc->amr_state |= AMR_STATE_SHUTDOWN;
    for (i = 0; i < AMR_MAXLD; i++) {
	if (sc->amr_drive[i].al_disk != 0) {
	    ad = device_get_softc(sc->amr_drive[i].al_disk);
	    if (ad->amrd_flags & AMRD_OPEN) {		/* drive is mounted, abort shutdown */
		sc->amr_state &= ~AMR_STATE_SHUTDOWN;
		device_printf(sc->amr_drive[i].al_disk, "still open, can't shutdown\n");
		error = EBUSY;
		goto out;
	    }
	}
    }

    /* flush controller */
    device_printf(sc->amr_dev, "flushing cache...");
    if (amr_flush(sc)) {
	printf("failed\n");
    } else {
	printf("done\n");
    }
    
    /* delete all our child devices */
    for (i = 0; i < AMR_MAXLD; i++) {
	if (sc->amr_drive[i].al_disk != 0) {
	    if ((error = device_delete_child(sc->amr_dev, sc->amr_drive[i].al_disk)) != 0)
		goto out;
	    sc->amr_drive[i].al_disk = 0;
	}
    }
    bus_generic_detach(sc->amr_dev);

 out:
    splx(s);
    return(error);
}

/********************************************************************************
 * Bring the controller to a quiescent state, ready for system suspend.
 */
int
amr_suspend(device_t dev)
{
    struct amr_softc	*sc = device_get_softc(dev);

    debug("called");

    sc->amr_state |= AMR_STATE_SUSPEND;

    /* flush controller */
    device_printf(sc->amr_dev, "flushing cache...");
    printf("%s\n", amr_flush(sc) ? "failed" : "done");
    
    return(0);
}

/********************************************************************************
 * Bring the controller back to a state ready for operation.
 */
int
amr_resume(device_t dev)
{
    struct amr_softc	*sc = device_get_softc(dev);

    debug("called");

    sc->amr_state &= ~AMR_STATE_SUSPEND;

    return(0);
}

/*******************************************************************************
 * Take an interrupt, or be poked by other code to look for interrupt-worthy
 * status.
 */
void
amr_intr(void *arg)
{
    struct amr_softc	*sc = (struct amr_softc *)arg;
    int			worked;

    debug("called on %p", sc);

    /* spin collecting finished commands, process them if we find anything */
    worked = 0;
    while (amr_done(sc))
	worked = 1;
    if (worked)
	amr_complete(sc);
};

/*******************************************************************************
 * Receive a buf structure from a child device and queue it on a particular
 * disk resource, then poke the disk resource to start as much work as it can.
 */
int
amr_submit_buf(struct amr_softc *sc, struct buf *bp)
{
    int		s;

    debug("called");

    s = splbio();
    bufq_insert_tail(&sc->amr_bufq, bp);
    splx(s);
    sc->amr_waitbufs++;
    amr_startio(sc);
    return(0);
}

/********************************************************************************
 * Accept an open operation on the control device.
 */
int
amr_open(dev_t dev, int flags, int fmt, struct proc *p)
{
    int			unit = minor(dev);
    struct amr_softc	*sc = devclass_get_softc(amr_devclass, unit);

    sc->amr_state |= AMR_STATE_OPEN;
    return(0);
}

/********************************************************************************
 * Accept the last close on the control device.
 */
int
amr_close(dev_t dev, int flags, int fmt, struct proc *p)
{
    int			unit = minor(dev);
    struct amr_softc	*sc = devclass_get_softc(amr_devclass, unit);

    sc->amr_state &= ~AMR_STATE_OPEN;
    return (0);
}

/********************************************************************************
 * Handle controller-specific control operations.
 */
int
amr_ioctl(dev_t dev, u_long cmd, caddr_t addr, int32_t flag, struct proc *p)
{
    
    switch(cmd) {
    default:	
	return(ENOTTY);
    }
}

/********************************************************************************
 * Handle operations requested by a drive connected to this controller.
 */
int
amr_submit_ioctl(struct amr_softc *sc, struct amr_logdrive *drive, u_long cmd, 
		 caddr_t addr, int32_t flag, struct proc *p)
{
    return(ENOTTY);
}

/********************************************************************************
 ********************************************************************************
                                                                 Command Wrappers
 ********************************************************************************
 ********************************************************************************/

/********************************************************************************
 * Interrogate the controller for the operational parameters we require.
 */
static int
amr_query_controller(struct amr_softc *sc)
{
    void	*buf;
    int		i;

    /* try to issue an ENQUIRY3 command */
    if ((buf = amr_enquiry(sc, 2048, AMR_CMD_CONFIG, AMR_CONFIG_ENQ3, 
			   AMR_CONFIG_ENQ3_SOLICITED_FULL)) == NULL) {

	struct amr_enquiry	*ae;

	/* failed, try the old ENQUIRY command */
	if ((ae = (struct amr_enquiry *)amr_enquiry(sc, 2048, AMR_CMD_ENQUIRY, 0, 0)) == NULL) {
	    device_printf(sc->amr_dev, "could not obtain configuration data from controller\n");
	    return(1);
	}
	/* first-time enquiry? */
	if (sc->amr_maxdrives == 0) {
	    device_printf(sc->amr_dev, "firmware %.4s bios %.4s  %dMB memory\n", 
			  ae->ae_adapter.aa_firmware, ae->ae_adapter.aa_bios,
			  ae->ae_adapter.aa_memorysize);
	}
	sc->amr_maxdrives = 8;
	sc->amr_maxio = ae->ae_adapter.aa_maxio;
	for (i = 0; i < ae->ae_ldrv.al_numdrives; i++) {
	    sc->amr_drive[i].al_size = ae->ae_ldrv.al_size[i];
	    sc->amr_drive[i].al_state = ae->ae_ldrv.al_state[i];
	    sc->amr_drive[i].al_properties = ae->ae_ldrv.al_properties[i];
	    debug("  drive %d: %d state %x properties %x\n", i, sc->amr_drive[i].al_size,
		  sc->amr_drive[i].al_state, sc->amr_drive[i].al_properties);
	}
	for (; i < AMR_MAXLD; i++)
	    sc->amr_drive[i].al_size = 0xffffffff;
	free(ae, M_DEVBUF);
    } else {
	free(buf, M_DEVBUF);
	sc->amr_maxdrives = 40;
	
	/* get static product info */
	if ((buf = amr_enquiry(sc, 2048, AMR_CMD_CONFIG, AMR_CONFIG_PRODINFO, 0)) == NULL) {
	    device_printf(sc->amr_dev, "controller supports 40ld but CONFIG_PRODINFO failed\n");
	    return(1);
	}
	free(buf, M_DEVBUF);
	device_printf(sc->amr_dev, "40LD firmware unsupported; send controller to msmith@freebsd.org\n");
	return(1);
    }
    return(0);
}

/********************************************************************************
 * Run a generic enquiry-style command.
 */
static void *
amr_enquiry(struct amr_softc *sc, size_t bufsize, u_int8_t cmd, u_int8_t cmdsub, u_int8_t cmdqual)
{
    struct amr_command	*ac;
    void		*result;
    u_int8_t		*mbox;
    int			error;

    debug("called");

    error = 1;
    result = NULL;
    
    /* get ourselves a command buffer */
    if ((ac = amr_alloccmd(sc)) == NULL)
	goto out;
    /* allocate the response structure */
    if ((result = malloc(bufsize, M_DEVBUF, M_NOWAIT)) == NULL)
	goto out;
    /* get a command slot */
    ac->ac_flags |= AMR_CMD_PRIORITY | AMR_CMD_DATAOUT;
    if (amr_getslot(ac))
	goto out;
    
    /* map the command so the controller can see it */
    ac->ac_data = result;
    ac->ac_length = bufsize;
    amr_mapcmd(ac);
    
    /* build the command proper */
    mbox = (u_int8_t *)&ac->ac_mailbox;		/* XXX want a real structure for this? */
    mbox[0] = cmd;
    mbox[2] = cmdsub;
    mbox[3] = cmdqual;
    ac->ac_mailbox.mb_physaddr = ac->ac_dataphys;

    /* run the command in polled/wait mode as suits the current mode */
    if ((sc->amr_state & AMR_STATE_INTEN) ? amr_wait_command(ac) : amr_poll_command(ac))
	goto out;
    error = ac->ac_status;
    
 out:
    if (ac != NULL)
	amr_releasecmd(ac);
    if ((error != 0) && (result != NULL)) {
	free(result, M_DEVBUF);
	result = NULL;
    }
    return(result);
}

/********************************************************************************
 * Flush the controller's internal cache, return status.
 */
static int
amr_flush(struct amr_softc *sc)
{
    struct amr_command	*ac;
    int			error;

    /* get ourselves a command buffer */
    error = 1;
    if ((ac = amr_alloccmd(sc)) == NULL)
	goto out;
    /* get a command slot */
    ac->ac_flags |= AMR_CMD_PRIORITY | AMR_CMD_DATAOUT;
    if (amr_getslot(ac))
	goto out;
    
    /* build the command proper */
    ac->ac_mailbox.mb_command = AMR_CMD_FLUSH;

    /* run the command in polled/wait mode as suits the current mode */
    if ((sc->amr_state & AMR_STATE_INTEN) ? amr_wait_command(ac) : amr_poll_command(ac))
	goto out;
    error = ac->ac_status;
    
 out:
    if (ac != NULL)
	amr_releasecmd(ac);
    return(error);
}

/********************************************************************************
 * Pull as much work off the softc's work queue as possible and give it to the
 * controller.  Leave a couple of slots free for emergencies.
 *
 * We avoid running at splbio() whenever possible.
 */
static void
amr_startio(struct amr_softc *sc)
{
    struct amr_command	*ac;
    struct amrd_softc	*amrd;
    struct buf		*bp;
    int			blkcount;
    int			driveno;
    int			cmd;
    int			s;

    /* spin until something prevents us from doing any work */
    s = splbio();
    for (;;) {

	/* see if there's work to be done */
	if ((bp = bufq_first(&sc->amr_bufq)) == NULL)
	    break;
	/* get a command */
	if ((ac = amr_alloccmd(sc)) == NULL)
	    break;
	/* get a slot for the command */
	if (amr_getslot(ac) != 0) {
	    amr_releasecmd(ac);
	    break;
	}
	/* get the buf containing our work */
	bufq_remove(&sc->amr_bufq, bp);
	sc->amr_waitbufs--;
	splx(s);
	
	/* connect the buf to the command */
	ac->ac_complete = amr_completeio;
	ac->ac_private = bp;
	ac->ac_data = bp->b_data;
	ac->ac_length = bp->b_bcount;
	if (bp->b_flags & B_READ) {
	    ac->ac_flags |= AMR_CMD_DATAIN;
	    cmd = AMR_CMD_LREAD;
	} else {
	    ac->ac_flags |= AMR_CMD_DATAOUT;
	    cmd = AMR_CMD_LWRITE;
	}
	
	/* map the command so the controller can work with it */
	amr_mapcmd(ac);
	
	/* build a suitable I/O command (assumes 512-byte rounded transfers) */
	amrd = (struct amrd_softc *)bp->b_driver1;
	driveno = amrd->amrd_drive - &sc->amr_drive[0];
	blkcount = bp->b_bcount / AMR_BLKSIZE;

	if ((bp->b_pblkno + blkcount) > sc->amr_drive[driveno].al_size)
	    device_printf(sc->amr_dev, "I/O beyond end of unit (%u,%d > %u)\n", 
			  bp->b_pblkno, blkcount, sc->amr_drive[driveno].al_size);

	/*
	 * Build the I/O command.
	 */
	ac->ac_mailbox.mb_command = cmd;
	ac->ac_mailbox.mb_blkcount = blkcount;
	ac->ac_mailbox.mb_lba = bp->b_pblkno;
	ac->ac_mailbox.mb_physaddr = ac->ac_sgphys;
	ac->ac_mailbox.mb_drive = driveno;
	ac->ac_mailbox.mb_nsgelem = ac->ac_nsgent;

	/* try to give command to controller */
	if (amr_start(ac) != 0) {
	    /* fail the command */
	    ac->ac_status = AMR_STATUS_WEDGED;
	    amr_completeio(ac);
	}
	s = splbio();
    }
    splx(s);
}

/********************************************************************************
 * Handle completion of an I/O command.
 */
static void
amr_completeio(struct amr_command *ac)
{
    struct amr_softc	*sc = ac->ac_sc;
    struct buf		*bp = (struct buf *)ac->ac_private;
    
    if (ac->ac_status != AMR_STATUS_SUCCESS) {	/* could be more verbose here? */
	bp->b_error = EIO;
	bp->b_flags |= B_ERROR;

	switch(ac->ac_status) {
	    /* XXX need more information on I/O error reasons */
	default:
	    device_printf(sc->amr_dev, "I/O error - %x\n", ac->ac_status);
	    amr_printcommand(ac);
	    break;
	}
    }
    amr_releasecmd(ac);
    amrd_intr(bp);
}

/********************************************************************************
 ********************************************************************************
                                                               Command Processing
 ********************************************************************************
 ********************************************************************************/

/********************************************************************************
 * Take a command, submit it to the controller and sleep until it completes
 * or fails.  Interrupts must be enabled, returns nonzero on error.
 */
static int
amr_wait_command(struct amr_command *ac)
{
    struct amr_softc	*sc = ac->ac_sc;
    int			error, count;
    
    debug("called");

    ac->ac_complete = NULL;
    ac->ac_private = ac;
    if ((error = amr_start(ac)) != 0)
	return(error);
    
    count = 0;
    /* XXX better timeout? */
    while ((ac->ac_status == AMR_STATUS_BUSY) && (count < 30)) {
	tsleep(ac->ac_private, PRIBIO | PCATCH, "amrwcmd", hz);
    }
    
    if (ac->ac_status != 0) {
	device_printf(sc->amr_dev, "I/O error 0x%x\n", ac->ac_status);
	return(EIO);
    }
    return(0);
}

/********************************************************************************
 * Take a command, submit it to the controller and busy-wait for it to return.
 * Returns nonzero on error.  Can be safely called with interrupts enabled.
 */
static int
amr_poll_command(struct amr_command *ac)
{
    struct amr_softc	*sc = ac->ac_sc;
    int			error, count, s;

    debug("called");

    ac->ac_complete = NULL;
    ac->ac_private = NULL;
    if ((error = amr_start(ac)) != 0)
	return(error);

    count = 0;
    do {
	/* 
	 * Poll for completion, although the interrupt handler may beat us to it. 
	 * Note that the timeout here is somewhat arbitrary.
	 */
	amr_done(sc);
    } while ((ac->ac_status == AMR_STATUS_BUSY) && (count++ < 100000));
    s = splbio();
    if (ac->ac_status != AMR_STATUS_BUSY) {
	TAILQ_REMOVE(&sc->amr_work, ac, ac_link);
	sc->amr_workcount--;
	error = 0;
    } else {
	/* take the command out of the busy list, mark slot as bogus */
	sc->amr_busycmd[ac->ac_slot] = (struct amr_command *)sc;
	error = EIO;
	device_printf(sc->amr_dev, "I/O error 0x%x\n", ac->ac_status);
    }
    splx(s);
    return(error);
}

/********************************************************************************
 * Get a free command slot.
 */
static int
amr_getslot(struct amr_command *ac)
{
    struct amr_softc	*sc = ac->ac_sc;
    int			s, slot, limit;

    debug("called");
    
    /* enforce slot usage limit */
    limit = (ac->ac_flags & AMR_CMD_PRIORITY) ? sc->amr_maxio : sc->amr_maxio - 4;
    if (sc->amr_busycmdcount > limit)
	return(EBUSY);
    
    /*
     * Allocate a slot
     */
    s = splbio();
    for (slot = 0; slot < sc->amr_maxio; slot++) {
	if (sc->amr_busycmd[slot] == NULL)
	    break;
    }
    if (slot < sc->amr_maxio) {
	sc->amr_busycmdcount++;
	sc->amr_busycmd[slot] = ac;
    }
    splx(s);

    /* out of slots? */
    if (slot >= sc->amr_maxio)
	return(EBUSY);
    
    ac->ac_slot = slot;
    return(0);
}

/********************************************************************************
 * Map/unmap (ac)'s data in the controller's addressable space.
 */
static void
amr_setup_dmamap(void *arg, bus_dma_segment_t *segs, int nsegments, int error)
{
    struct amr_command	*ac = (struct amr_command *)arg;
    struct amr_softc	*sc = ac->ac_sc;
    struct amr_sgentry	*sg;
    int			i;

    debug("called");

    /* get base address of s/g table */
    sg = sc->amr_sgtable + (ac->ac_slot * AMR_NSEG);

    /* save s/g table information in command */
    ac->ac_nsgent = nsegments;
    ac->ac_sgphys = sc->amr_sgbusaddr + (ac->ac_slot * AMR_NSEG * sizeof(struct amr_sgentry));
    ac->ac_dataphys = segs[0].ds_addr;

    /* populate s/g table */
    for (i = 0; i < nsegments; i++, sg++) {
	sg->sg_addr = segs[i].ds_addr;
	sg->sg_count = segs[i].ds_len;
    }
}

static void
amr_mapcmd(struct amr_command *ac)
{
    struct amr_softc	*sc = ac->ac_sc;

    debug("called");

    /* if the command involves data at all */
    if (ac->ac_data != NULL) {
	
	/* map the data buffer into bus space and build the s/g list */
	bus_dmamap_load(sc->amr_buffer_dmat, ac->ac_dmamap, ac->ac_data, ac->ac_length, 
			amr_setup_dmamap, ac, 0);
	if (ac->ac_flags & AMR_CMD_DATAIN)
	    bus_dmamap_sync(sc->amr_buffer_dmat, ac->ac_dmamap, BUS_DMASYNC_PREREAD);
	if (ac->ac_flags & AMR_CMD_DATAOUT)
	    bus_dmamap_sync(sc->amr_buffer_dmat, ac->ac_dmamap, BUS_DMASYNC_PREWRITE);
    }
}

static void
amr_unmapcmd(struct amr_command *ac)
{
    struct amr_softc	*sc = ac->ac_sc;

    debug("called");

    /* if the command involved data at all */
    if (ac->ac_data != NULL) {
	
	if (ac->ac_flags & AMR_CMD_DATAIN)
	    bus_dmamap_sync(sc->amr_buffer_dmat, ac->ac_dmamap, BUS_DMASYNC_POSTREAD);
	if (ac->ac_flags & AMR_CMD_DATAOUT)
	    bus_dmamap_sync(sc->amr_buffer_dmat, ac->ac_dmamap, BUS_DMASYNC_POSTWRITE);

	bus_dmamap_unload(sc->amr_buffer_dmat, ac->ac_dmamap); 
    }
}

/********************************************************************************
 * Take a command and give it to the controller.  Take care of any completed
 * commands we encouter while waiting.
 */
static int
amr_start(struct amr_command *ac)
{
    struct amr_softc	*sc = ac->ac_sc;
    int			worked, done, s, i;
    
    debug("called");

    /* 
     * Save the slot number so that we can locate this command when complete.
     * Note that ident = 0 seems to be special, so we don't use it.
     */
    ac->ac_mailbox.mb_ident = ac->ac_slot + 1;

    /* set the busy flag when we copy the mailbox in */
    ac->ac_mailbox.mb_busy = 1;

    /* set impossible status so that a woken sleeper can tell the command is busy */
    ac->ac_status = AMR_STATUS_BUSY;

    /* spin waiting for the mailbox */
    debug("wait for mailbox");
    for (i = 100000, done = 0, worked = 0; (i > 0) && !done; i--) {
	s = splbio();
	
	/* is the mailbox free? */
	if (sc->amr_mailbox->mb_busy == 0) {
	    debug("got mailbox");
	    sc->amr_mailbox64->mb64_segment = 0;
	    bcopy(&ac->ac_mailbox, sc->amr_mailbox, AMR_MBOX_CMDSIZE);
	    sc->amr_submit_command(sc);
	    done = 1;
	    sc->amr_workcount++;
	    TAILQ_INSERT_TAIL(&sc->amr_work, ac, ac_link);

	    /* not free, try to clean up while we wait */
	} else {
	    debug("busy flag %x\n", sc->amr_mailbox->mb_busy);
	    worked = amr_done(sc);
	}
	splx(s);
    }
    
    /* do completion processing if we picked anything up */
    if (worked)
	amr_complete(sc);

    /* command is enqueued? */
    if (done) {
	ac->ac_stamp = time_second;
	debug("posted command");
	return(0);
    }
    
    /*
     * The controller wouldn't take the command.  Revoke the slot
     * that the command was given and return with a bad status.
     */
    sc->amr_busycmd[ac->ac_slot] = NULL;
    device_printf(sc->amr_dev, "controller wedged (not taking commands)\n");
    ac->ac_status = AMR_STATUS_WEDGED;
    return(EIO);
}

/********************************************************************************
 * Extract one or more completed commands from the controller (sc)
 *
 * Returns nonzero if any commands on the work queue were marked as completed.
 */
static int
amr_done(struct amr_softc *sc)
{
    struct amr_command	*ac;
    struct amr_mailbox	mbox;
    int			i, idx, result;
    
    debug("called");

    /* See if there's anything for us to do */
    result = 0;
    if (sc->amr_get_work(sc, &mbox)) {
	/* iterate over completed commands */
	for (i = 0; i < mbox.mb_nstatus; i++) {
	    /* get pointer to busy command */
	    idx = mbox.mb_completed[i] - 1;
	    ac = sc->amr_busycmd[idx];

	    /* really a busy command? */
	    if (ac != NULL) {

		/* pull the command from the busy index */
		sc->amr_busycmd[idx] = NULL;
		sc->amr_busycmdcount--;
		
		/* unmap data buffer */
		amr_unmapcmd(ac);

		/* aborted command? */
		if (ac == (struct amr_command *)sc) {
		    device_printf(sc->amr_dev, "aborted command completed (%d)\n", idx);
		    ac = NULL;

		    /* completed normally, save status */
		} else {
		    ac->ac_status = mbox.mb_status;
		    debug("completed command with status %x", mbox.mb_status);
		}
		result = 1;
	    }
	}
    }
    return(result);
}

/********************************************************************************
 * Do completion processing on done commands on (sc)
 */
static void
amr_complete(struct amr_softc *sc)
{
    struct amr_command	*ac, *nc;
    int			s, count;

    debug("called");

    count = 0;

    s = splbio();
    ac = TAILQ_FIRST(&sc->amr_work);
    while (ac != NULL) {
	nc = TAILQ_NEXT(ac, ac_link);

	/* Skip if command is still active */
	if (ac->ac_status != AMR_STATUS_BUSY) {
	    
	    /* 
	     * Is there a completion handler? 
	     */
	    if (ac->ac_complete != NULL) {

		/* remove and give to completion handler */
		TAILQ_REMOVE(&sc->amr_work, ac, ac_link);
		sc->amr_workcount--;
		ac->ac_complete(ac);
	    
		/* 
		 * Is someone sleeping on this one?
		 */
	    } else if (ac->ac_private != NULL) {

		/* remove and wake up */
		TAILQ_REMOVE(&sc->amr_work, ac, ac_link);
		sc->amr_workcount--;
		wakeup_one(ac->ac_private);

		/*
		 * Leave it for a polling caller.
		 */
	    } else {
	    }
	}
	ac = nc;
    }
    splx(s);
    
    /* queue more work if we can */
    amr_startio(sc);
}

/********************************************************************************
 ********************************************************************************
                                                        Command Buffer Management
 ********************************************************************************
 ********************************************************************************/

/********************************************************************************
 * Get a new command buffer.
 *
 * This may return NULL in low-memory cases.
 *
 * Note that using malloc() is expensive (the command buffer is << 1 page) but
 * necessary if we are to be a loadable module before the zone allocator is fixed.
 *
 * If possible, we recycle a command buffer that's been used before.
 *
 * XXX Note that command buffers are not cleaned out - it is the caller's 
 *     responsibility to ensure that all required fields are filled in before
 *     using a buffer.
 */
static struct amr_command *
amr_alloccmd(struct amr_softc *sc)
{
    struct amr_command	*ac;
    int			error;
    int			s;

    debug("called");

    s = splbio();
    if ((ac = TAILQ_FIRST(&sc->amr_freecmds)) != NULL)
	TAILQ_REMOVE(&sc->amr_freecmds, ac, ac_link);
    splx(s);

    /* allocate a new command buffer? */
    if (ac == NULL) {
	ac = (struct amr_command *)malloc(sizeof(*ac), M_DEVBUF, M_NOWAIT);
	if (ac != NULL) {
	    bzero(ac, sizeof(*ac));
	    ac->ac_sc = sc;
	    error = bus_dmamap_create(sc->amr_buffer_dmat, 0, &ac->ac_dmamap);
	    if (error) {
		free(ac, M_DEVBUF);
		return(NULL);
	    }
	}
    }
    bzero(&ac->ac_mailbox, sizeof(struct amr_mailbox));
    return(ac);
}

/********************************************************************************
 * Release a command buffer for recycling.
 *
 * XXX It might be a good idea to limit the number of commands we save for reuse
 *     if it's shown that this list bloats out massively.
 */
static void
amr_releasecmd(struct amr_command *ac)
{
    int		s;
    
    debug("called");

    s = splbio();
    TAILQ_INSERT_HEAD(&ac->ac_sc->amr_freecmds, ac, ac_link);
    splx(s);
}

/********************************************************************************
 * Permanently discard a command buffer.
 */
static void
amr_freecmd(struct amr_command *ac) 
{
    struct amr_softc	*sc = ac->ac_sc;
    
    debug("called");

    bus_dmamap_destroy(sc->amr_buffer_dmat, ac->ac_dmamap);
    free(ac, M_DEVBUF);
}

/********************************************************************************
 ********************************************************************************
                                                         Interface-specific Shims
 ********************************************************************************
 ********************************************************************************/

/********************************************************************************
 * Tell the controller that the mailbox contains a valid command
 */
static void
amr_quartz_submit_command(struct amr_softc *sc)
{
    debug("called");

    sc->amr_mailbox->mb_poll = 0;
    sc->amr_mailbox->mb_ack = 0;
    /* XXX write barrier? */
    while(AMR_QGET_IDB(sc) & AMR_QIDB_SUBMIT)
	;				/* XXX aiee! what if it dies? */
    AMR_QPUT_IDB(sc, sc->amr_mailboxphys | AMR_QIDB_SUBMIT);
}

static void
amr_std_submit_command(struct amr_softc *sc)
{
    debug("called");

    /* XXX write barrier? */
    while (AMR_SGET_MBSTAT(sc) & AMR_SMBOX_BUSYFLAG)
	;				/* XXX aiee! what if it dies? */
    AMR_SPOST_COMMAND(sc);
}

/********************************************************************************
 * Claim any work that the controller has completed; acknowledge completion,
 * save details of the completion in (mbsave)
 */
static int
amr_quartz_get_work(struct amr_softc *sc, struct amr_mailbox *mbsave)
{
    int		s, worked;
    u_int32_t	outd;
    
    debug("called");

    worked = 0;
    s = splbio();

    /* work waiting for us? */
    if ((outd = AMR_QGET_ODB(sc)) == AMR_QODB_READY) {
	AMR_QPUT_ODB(sc, AMR_QODB_READY);

	/* save mailbox, which contains a list of completed commands */
	/* XXX read barrier? */
	bcopy(sc->amr_mailbox, mbsave, sizeof(*mbsave));

	/* acknowledge that we have the commands */
	AMR_QPUT_IDB(sc, sc->amr_mailboxphys | AMR_QIDB_ACK);
	while(AMR_QGET_IDB(sc) & AMR_QIDB_ACK)
	    ;				/* XXX aiee! what if it dies? */
	worked = 1;			/* got some work */
    }

    splx(s);
    return(worked);
}

static int
amr_std_get_work(struct amr_softc *sc, struct amr_mailbox *mbsave)
{
    int		s, worked;
    u_int8_t	istat;

    debug("called");

    worked = 0;
    s = splbio();

    /* check for valid interrupt status */
    istat = AMR_SGET_ISTAT(sc);
    if ((istat & AMR_SINTR_VALID) != 0) {
	AMR_SPUT_ISTAT(sc, istat);	/* ack interrupt status */

	/* save mailbox, which contains a list of completed commands */
	/* XXX read barrier? */
	bcopy(sc->amr_mailbox, mbsave, sizeof(*mbsave));

	AMR_SACK_INTERRUPT(sc);		/* acknowledge we have the mailbox */
	worked = 1;
    }

    splx(s);
    return(worked);
}

/********************************************************************************
 * Notify the controller of the mailbox location.
 */
static void
amr_quartz_attach_mailbox(struct amr_softc *sc)
{
    /* Quartz is given the mailbox location when a command is submitted */
}

static void
amr_std_attach_mailbox(struct amr_softc *sc)
{

    /* program the mailbox physical address */
    AMR_SBYTE_SET(sc, AMR_SMBOX_0, sc->amr_mailboxphys         & 0xff);
    AMR_SBYTE_SET(sc, AMR_SMBOX_1, (sc->amr_mailboxphys >>  8) & 0xff);
    AMR_SBYTE_SET(sc, AMR_SMBOX_2, (sc->amr_mailboxphys >> 16) & 0xff);
    AMR_SBYTE_SET(sc, AMR_SMBOX_3, (sc->amr_mailboxphys >> 24) & 0xff);
    AMR_SBYTE_SET(sc, AMR_SMBOX_ENABLE, AMR_SMBOX_ADDR);

    /* clear any outstanding interrupt and enable interrupts proper */
    AMR_SACK_INTERRUPT(sc);
    AMR_SENABLE_INTR(sc);
}

/********************************************************************************
 ********************************************************************************
                                                                        Debugging
 ********************************************************************************
 ********************************************************************************/

/********************************************************************************
 * Print the command (ac) in human-readable format
 */
static void
amr_printcommand(struct amr_command *ac)
{
    struct amr_softc	*sc = ac->ac_sc;
    struct amr_sgentry	*sg;
    int			i;
    
    device_printf(sc->amr_dev, "cmd %x  ident %d  drive %d\n",
		  ac->ac_mailbox.mb_command, ac->ac_mailbox.mb_ident, ac->ac_mailbox.mb_drive);
    device_printf(sc->amr_dev, "blkcount %d  lba %d\n", 
		  ac->ac_mailbox.mb_blkcount, ac->ac_mailbox.mb_lba);
    device_printf(sc->amr_dev, "virtaddr %p  length %lu\n", ac->ac_data, (unsigned long)ac->ac_length);
    device_printf(sc->amr_dev, "physaddr %08x  nsg %d\n",
		  ac->ac_mailbox.mb_physaddr, ac->ac_mailbox.mb_nsgelem);

    /* get base address of s/g table */
    sg = sc->amr_sgtable + (ac->ac_slot * AMR_NSEG);
    for (i = 0; i < ac->ac_mailbox.mb_nsgelem; i++, sg++)
	device_printf(sc->amr_dev, "  %x/%d\n", sg->sg_addr, sg->sg_count);
}
