root/usr/src/uts/common/io/scsi/adapters/pmcs/pmcs_intr.c
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright (c) 2009, 2010, Oracle and/or its affiliates. All rights reserved.
 */

/*
 * This file contains functions that are called via interrupts.
 */

#include <sys/scsi/adapters/pmcs/pmcs.h>

#ifdef  DEBUG
#define VALID_IOMB_CHECK(p, w, m, b, c)                                 \
        if (!(w & PMCS_IOMB_VALID)) {                                   \
                char l[64];                                             \
                (void) snprintf(l, sizeof (l),                          \
                    "%s: INVALID IOMB (oq_ci=%u oq_pi=%u)", __func__, b, c); \
                pmcs_print_entry(pwp, PMCS_PRT_DEBUG, l, m);            \
                STEP_OQ_ENTRY(pwp, PMCS_OQ_EVENTS, b, 1);               \
                continue;                                               \
        }
#define WRONG_OBID_CHECK(pwp, w, q)     \
        if (((w & PMCS_IOMB_OBID_MASK) >> PMCS_IOMB_OBID_SHIFT) != q) { \
                pmcs_prt(pwp, PMCS_PRT_DEBUG, NULL, NULL,               \
                    "%s: COMPLETION WITH WRONG OBID (0x%x)", __func__,  \
                    (w & PMCS_IOMB_OBID_MASK) >> PMCS_IOMB_OBID_SHIFT); \
        }
#else
#define VALID_IOMB_CHECK(a, b, c, d, e)
#define WRONG_OBID_CHECK(a, b, c)
#endif

#define OQLIM_CHECK(p, l)                               \
        if (++l == (p)->ioq_depth) {                    \
                pmcs_prt(p, PMCS_PRT_DEBUG, NULL, NULL, \
                    "%s: possible ob queue overflow",   \
                    __func__);                          \
                break;                                  \
        }

#define COPY_OUTBOUND(p, w, l, n, a, x, q, c)                           \
        n = ((w & PMCS_IOMB_BC_MASK) >> PMCS_IOMB_BC_SHIFT);            \
        a = PMCS_QENTRY_SIZE;                                           \
        (void) memcpy(l, x, PMCS_QENTRY_SIZE);                          \
        if (n > 1) {                                                    \
                a <<= 1;                                                \
                (void) memcpy(&l[PMCS_QENTRY_SIZE],                     \
                    GET_OQ_ENTRY(p, q, c, 1), PMCS_QENTRY_SIZE);        \
        }                                                               \
        pmcs_prt(p, PMCS_PRT_DEBUG3, NULL, NULL,                        \
            "%s: ptr %p ci %d w0 %x nbuf %d",                           \
            __func__, (void *)x, ci, w0, n)

#define EVT_PRT(hwp, msg, phy)  \
        pmcs_prt(hwp, PMCS_PRT_DEBUG, NULL, NULL, "Phy 0x%x: %s", phy, # msg)


/*
 * Map the link rate reported in the event to the SAS link rate value
 */
static uint8_t
pmcs_link_rate(uint32_t event_link_rate)
{
        uint8_t sas_link_rate = 0;

        switch (event_link_rate) {
        case 1:
                sas_link_rate = SAS_LINK_RATE_1_5GBIT;
                break;
        case 2:
                sas_link_rate = SAS_LINK_RATE_3GBIT;
                break;
        case 4:
                sas_link_rate = SAS_LINK_RATE_6GBIT;
                break;
        }

        return (sas_link_rate);
}

/*
 * Called with pwrk lock
 */
static void
pmcs_complete_work(pmcs_hw_t *pwp, pmcwork_t *pwrk, uint32_t *iomb, size_t amt)
{
#ifdef  DEBUG
        pwp->ltime[pwp->lti] = gethrtime();
        pwp->ltags[pwp->lti++] = pwrk->htag;
#endif
        pwrk->htag |= PMCS_TAG_DONE;

        /*
         * If the command has timed out, leave it in that state.
         */
        if (pwrk->state != PMCS_WORK_STATE_TIMED_OUT) {
                pwrk->state = PMCS_WORK_STATE_INTR;
                pwrk->onwire = 0;
        }

        pmcs_complete_work_impl(pwp, pwrk, iomb, amt);
}

static void
pmcs_work_not_found(pmcs_hw_t *pwp, uint32_t htag, uint32_t *iomb)
{
#ifdef  DEBUG
        int i;
        hrtime_t now;
        char buf[64];

        (void) snprintf(buf, sizeof (buf),
            "unable to find work structure for tag 0x%x", htag);

        pmcs_print_entry(pwp, PMCS_PRT_DEBUG1, buf, iomb);
        if (htag == 0) {
                return;
        }
        now = gethrtime();
        for (i = 0; i < 256; i++) {
                mutex_enter(&pwp->dbglock);
                if (pwp->ltags[i] == htag) {
                        pmcs_prt(pwp, PMCS_PRT_DEBUG1, NULL, NULL,
                            "same tag already completed (%llu us ago)",
                            (unsigned long long) (now - pwp->ltime[i]));
                }
                if (pwp->ftags[i] == htag) {
                        pmcs_prt(pwp, PMCS_PRT_DEBUG1, NULL, NULL,
                            "same tag started (line %d) (%llu ns ago)",
                            pwp->ftag_lines[i], (unsigned long long)
                            (now - pwp->ftime[i]));
                }
                mutex_exit(&pwp->dbglock);
        }
#else
        char buf[64];
        (void) snprintf(buf, sizeof (buf),
            "unable to find work structure for tag 0x%x", htag);
        pmcs_print_entry(pwp, PMCS_PRT_DEBUG1, buf, iomb);
#endif
}


static void
pmcs_process_io_completion(pmcs_hw_t *pwp, pmcs_iocomp_cb_t *ioccb, size_t amt)
{
        pmcwork_t *pwrk;
        uint32_t tag_type;
        uint32_t htag = LE_32(((uint32_t *)((void *)ioccb->iomb))[1]);

        pwrk = pmcs_tag2wp(pwp, htag, B_FALSE);
        if (pwrk == NULL) {
                pmcs_work_not_found(pwp, htag, (void *)&ioccb->iomb);
                kmem_cache_free(pwp->iocomp_cb_cache, ioccb);
                return;
        }

        pwrk->htag |= PMCS_TAG_DONE;

        /*
         * If the command has timed out, leave it in that state.
         */
        if (pwrk->state != PMCS_WORK_STATE_TIMED_OUT) {
                pwrk->state = PMCS_WORK_STATE_INTR;
                pwrk->onwire = 0;
        }

        /*
         * Some SATA and SAS commands are run in "WAIT" mode.
         * We can tell this from the tag type. In this case,
         * we just do a wakeup (not a callback).
         */
        tag_type = PMCS_TAG_TYPE(pwrk->htag);
        if (tag_type == PMCS_TAG_TYPE_WAIT) {
                ASSERT(PMCS_TAG_TYPE(pwrk->htag) == PMCS_TAG_TYPE_WAIT);
                if (pwrk->arg && amt) {
                        (void) memcpy(pwrk->arg, ioccb->iomb, amt);
                }
                cv_signal(&pwrk->sleep_cv);
                mutex_exit(&pwrk->lock);
                kmem_cache_free(pwp->iocomp_cb_cache, ioccb);
                return;
        }
        ASSERT(tag_type == PMCS_TAG_TYPE_CBACK);

#ifdef  DEBUG
        pwp->ltime[pwp->lti] = gethrtime();
        pwp->ltags[pwp->lti++] = pwrk->htag;
#endif

        ioccb->pwrk = pwrk;

        /*
         * Only update state to IOCOMPQ if we were in the INTR state.
         * Any other state (e.g. TIMED_OUT, ABORTED) needs to remain.
         */
        if (pwrk->state == PMCS_WORK_STATE_INTR) {
                pwrk->state = PMCS_WORK_STATE_IOCOMPQ;
        }

        mutex_enter(&pwp->cq_lock);
        if (pwp->iocomp_cb_tail) {
                pwp->iocomp_cb_tail->next = ioccb;
                pwp->iocomp_cb_tail = ioccb;
        } else {
                pwp->iocomp_cb_head = ioccb;
                pwp->iocomp_cb_tail = ioccb;
        }
        ioccb->next = NULL;
        mutex_exit(&pwp->cq_lock);

        mutex_exit(&pwrk->lock);
        /* Completion queue will be run at end of pmcs_iodone_intr */
}


static void
pmcs_process_completion(pmcs_hw_t *pwp, void *iomb, size_t amt)
{
        pmcwork_t *pwrk;
        uint32_t htag = LE_32(((uint32_t *)iomb)[1]);

        pwrk = pmcs_tag2wp(pwp, htag, B_FALSE);
        if (pwrk == NULL) {
                pmcs_work_not_found(pwp, htag, iomb);
                return;
        }

        pmcs_complete_work(pwp, pwrk, iomb, amt);
        /*
         * The pwrk lock is now released
         */
}

static void
pmcs_kill_port(pmcs_hw_t *pwp, int portid)
{
        pmcs_phy_t *pptr = pwp->ports[portid];

        if (pptr == NULL) {
                return;
        }

        /*
         * Clear any subsidiary phys
         */
        mutex_enter(&pwp->lock);

        for (pptr = pwp->root_phys; pptr; pptr = pptr->sibling) {
                pmcs_lock_phy(pptr);
                if (pptr->link_rate && pptr->portid == portid &&
                    pptr->subsidiary) {
                        pmcs_clear_phy(pwp, pptr);
                }
                pmcs_unlock_phy(pptr);
        }

        pptr = pwp->ports[portid];
        pwp->ports[portid] = NULL;
        mutex_exit(&pwp->lock);

        pmcs_lock_phy(pptr);
        pmcs_kill_changed(pwp, pptr, 0);
        pmcs_unlock_phy(pptr);

        RESTART_DISCOVERY(pwp);
        pmcs_prt(pwp, PMCS_PRT_INFO, NULL, NULL, "PortID 0x%x Cleared", portid);
}

void
pmcs_process_sas_hw_event(pmcs_hw_t *pwp, void *iomb, size_t amt)
{
        uint32_t w1 = LE_32(((uint32_t *)iomb)[1]);
        uint32_t w3 = LE_32(((uint32_t *)iomb)[3]);
        char buf[32];
        uint8_t phynum = IOP_EVENT_PHYNUM(w1);
        uint8_t portid = IOP_EVENT_PORTID(w1);
        pmcs_iport_t *iport;
        pmcs_phy_t *pptr, *subphy, *tphyp;
        int need_ack = 0;
        int primary;

        switch (IOP_EVENT_EVENT(w1)) {
        case IOP_EVENT_PHY_STOP_STATUS:
                if (IOP_EVENT_STATUS(w1)) {
                        pmcs_prt(pwp, PMCS_PRT_DEBUG, NULL, NULL,
                            "PORT %d failed to stop (0x%x)",
                            phynum, IOP_EVENT_STATUS(w1));
                } else {
                        pmcs_prt(pwp, PMCS_PRT_DEBUG_CONFIG, NULL, NULL,
                            "PHY 0x%x Stopped", phynum);
                        mutex_enter(&pwp->lock);
                        pptr = pwp->root_phys + phynum;
                        pmcs_lock_phy(pptr);
                        mutex_exit(&pwp->lock);
                        if (pptr->configured) {
                                pmcs_kill_changed(pwp, pptr, 0);
                        } else {
                                pmcs_set_changed(pwp, pptr, B_TRUE, 0);
                        }
                        pmcs_unlock_phy(pptr);
                        RESTART_DISCOVERY(pwp);
                }
                /* Reposition htag to the 'expected' position. */
                ((uint32_t *)iomb)[1] = ((uint32_t *)iomb)[2];
                pmcs_process_completion(pwp, iomb, amt);
                break;
        case IOP_EVENT_SAS_PHY_UP:
        {
                static const uint8_t sas_identify_af_endian_xfvec[] = {
                        0x5c, 0x5a, 0x56, 0x00
                };
                pmcs_phy_t *rp;
                sas_identify_af_t af;
                uint64_t phy_id, wwn;

                /*
                 * If we're not at running state, don't do anything
                 */
                mutex_enter(&pwp->lock);
                if (pwp->state != STATE_RUNNING) {
                        mutex_exit(&pwp->lock);
                        break;
                }
                pptr = pwp->root_phys + phynum;
                pmcs_lock_phy(pptr);

                /*
                 * No need to lock the primary root PHY.  It can never go
                 * away, and we're only concerned with the port width and
                 * the portid, both of which only ever change in this function.
                 */
                rp = pwp->ports[portid];

                mutex_exit(&pwp->lock);

                pmcs_endian_transform(pwp, &af, &((uint32_t *)iomb)[4],
                    sas_identify_af_endian_xfvec);

                /* Copy the remote address into our phy handle */
                (void) memcpy(pptr->sas_address, af.sas_address, 8);
                wwn = pmcs_barray2wwn(pptr->sas_address);
                phy_id = (uint64_t)af.phy_identifier;

                /*
                 * Check to see if there is a PortID already active.
                 */
                if (rp) {
                        if (rp->portid != portid) {
                                pmcs_unlock_phy(pptr);
                                pmcs_prt(pwp, PMCS_PRT_DEBUG, pptr, NULL,
                                    "PortID 0x%x: PHY 0x%x SAS LINK UP IS FOR "
                                    "A DIFFERENT PORTID 0x%x", rp->portid,
                                    phynum, portid);
                                break;
                        }

                        /*
                         * If the dtype isn't NOTHING, then this is actually
                         * the primary PHY for this port.  It probably went
                         * down and came back up, so be sure not to mark it
                         * as a subsidiary.
                         */
                        if (pptr->dtype == NOTHING) {
                                pptr->subsidiary = 1;
                        }
                        pptr->link_rate =
                            pmcs_link_rate(IOP_EVENT_LINK_RATE(w1));
                        pptr->portid = portid;
                        pptr->dead = 0;
                        pmcs_unlock_phy(pptr);

                        rp->width = IOP_EVENT_NPIP(w3);

                        /* Add this PHY to the phymap */
                        if (sas_phymap_phy_add(pwp->hss_phymap, phynum,
                            pwp->sas_wwns[0], wwn) != DDI_SUCCESS) {
                                pmcs_prt(pwp, PMCS_PRT_DEBUG, pptr, NULL,
                                    "Unable to add phy %u for 0x%" PRIx64 ".0x%"
                                    PRIx64, phynum, pwp->sas_wwns[rp->phynum],
                                    wwn);
                        }

                        /*
                         * Get our iport, if attached, and set it up.  Update
                         * the PHY's phymask props while we're locked.
                         */
                        pmcs_lock_phy(pptr);
                        pmcs_update_phy_pm_props(pptr, (1ULL << phynum),
                            (1ULL << phy_id), B_TRUE);
                        pmcs_unlock_phy(pptr);
                        iport = pmcs_get_iport_by_wwn(pwp, wwn);
                        if (iport) {
                                primary = !pptr->subsidiary;

                                mutex_enter(&iport->lock);
                                if (primary) {
                                        iport->pptr = pptr;
                                }
                                if (iport->ua_state == UA_ACTIVE) {
                                        pmcs_add_phy_to_iport(iport, pptr);
                                        pptr->iport = iport;
                                }
                                mutex_exit(&iport->lock);
                                pmcs_rele_iport(iport);
                        }

                        pmcs_update_phy_pm_props(rp, (1ULL << phynum),
                            (1ULL << phy_id), B_TRUE);
                        pmcs_prt(pwp, PMCS_PRT_DEBUG_CONFIG, NULL, NULL,
                            "PortID 0x%x: PHY 0x%x SAS LINK UP WIDENS PORT "
                            "TO %d PHYS", portid, phynum, rp->width);

                        break;
                }

                /*
                 * Check to see if anything is here already
                 */
                if (pptr->dtype != NOTHING && pptr->configured) {
                        pmcs_unlock_phy(pptr);
                        pmcs_prt(pwp, PMCS_PRT_DEBUG, pptr, NULL,
                            "PortID 0x%x: SAS PHY 0x%x UP HITS EXISTING "
                            "CONFIGURED TREE", portid, phynum);
                        break;
                }

                if (af.address_frame_type != SAS_AF_IDENTIFY) {
                        pmcs_unlock_phy(pptr);
                        pmcs_prt(pwp, PMCS_PRT_DEBUG, pptr, NULL,
                            "SAS link up on phy 0x%x, "
                            "but unexpected frame type 0x%x found", phynum,
                            af.address_frame_type);
                        break;
                }
                pptr->width = IOP_EVENT_NPIP(w3);
                pptr->portid = portid;
                pptr->dead = 0;
                pptr->link_rate = pmcs_link_rate(IOP_EVENT_LINK_RATE(w1));

                /*
                 * Check to see whether this is an expander or an endpoint
                 */
                switch (af.device_type) {
                case SAS_IF_DTYPE_ENDPOINT:
                        pptr->pend_dtype = SAS;
                        pptr->dtype = SAS;
                        break;
                case SAS_IF_DTYPE_EDGE:
                case SAS_IF_DTYPE_FANOUT:
                        pptr->pend_dtype = EXPANDER;
                        pptr->dtype = EXPANDER;
                        break;
                default:
                        pmcs_prt(pwp, PMCS_PRT_DEBUG, pptr, NULL,
                            "unknown device type 0x%x", af.device_type);
                        pptr->pend_dtype = NOTHING;
                        pptr->dtype = NOTHING;
                        break;
                }

                /*
                 * If this is a direct-attached SAS drive, do the spinup
                 * release now.
                 */
                if (pptr->dtype == SAS) {
                        pptr->spinup_hold = 1;
                        pmcs_spinup_release(pwp, pptr);
                        pmcs_prt(pwp, PMCS_PRT_DEBUG_CONFIG, pptr, NULL,
                            "Release spinup hold on PHY 0x%x", phynum);
                }

                pmcs_set_changed(pwp, pptr, B_TRUE, 0);
                if (pptr->width > 1) {
                        pmcs_prt(pwp, PMCS_PRT_INFO, pptr, NULL,
                            "PortID 0x%x: PHY 0x%x SAS"
                            " LINK UP @ %s Gb with %d phys/s", portid, phynum,
                            pmcs_get_rate(pptr->link_rate), pptr->width);
                } else {
                        pmcs_prt(pwp, PMCS_PRT_INFO, pptr, NULL,
                            "PortID 0x%x: PHY 0x%x SAS"
                            " LINK UP @ %s Gb/s", portid, phynum,
                            pmcs_get_rate(pptr->link_rate));
                }
                pmcs_unlock_phy(pptr);

                /* Add this PHY to the phymap */
                if (sas_phymap_phy_add(pwp->hss_phymap, phynum,
                    pwp->sas_wwns[0], wwn) != DDI_SUCCESS) {
                        pmcs_prt(pwp, PMCS_PRT_DEBUG, pptr, NULL,
                            "Unable to add phy %u for 0x%" PRIx64 ".0x%"
                            PRIx64, phynum, pwp->sas_wwns[pptr->phynum], wwn);
                }

                /* Get a pointer to our iport and set it up if attached */
                iport = pmcs_get_iport_by_wwn(pwp, wwn);
                if (iport) {
                        primary = !pptr->subsidiary;

                        mutex_enter(&iport->lock);
                        if (primary) {
                                iport->pptr = pptr;
                        }
                        if (iport->ua_state == UA_ACTIVE) {
                                pmcs_add_phy_to_iport(iport, pptr);
                                pptr->iport = iport;
                        }
                        mutex_exit(&iport->lock);
                        pmcs_rele_iport(iport);
                }

                pmcs_lock_phy(pptr);
                pmcs_update_phy_pm_props(pptr, (1ULL << phynum),
                    (1ULL << phy_id), B_TRUE);
                pmcs_smhba_log_sysevent(pwp, ESC_SAS_PHY_EVENT,
                    SAS_PHY_ONLINE, pptr);
                pmcs_unlock_phy(pptr);

                mutex_enter(&pwp->lock);
                pwp->ports[portid] = pptr;
                mutex_exit(&pwp->lock);
                RESTART_DISCOVERY(pwp);

                break;
        }
        case IOP_EVENT_SATA_PHY_UP: {
                uint64_t wwn;
                /*
                 * If we're not at running state, don't do anything
                 */
                mutex_enter(&pwp->lock);
                if (pwp->state != STATE_RUNNING) {
                        mutex_exit(&pwp->lock);
                        break;
                }

                /*
                 * Check to see if anything is here already
                 */
                pmcs_lock_phy(pwp->root_phys + phynum);
                pptr = pwp->root_phys + phynum;
                mutex_exit(&pwp->lock);

                if (pptr->dtype != NOTHING && pptr->configured) {
                        pmcs_unlock_phy(pptr);
                        pmcs_prt(pwp, PMCS_PRT_DEBUG, pptr, NULL,
                            "PortID 0x%x: SATA PHY 0x%x"
                            " UP HITS EXISTING CONFIGURED TREE",
                            portid, phynum);
                        break;
                }

                pptr->width = 1;
                pptr->dead = 0;

                /*
                 * Install the PHY number in the least significant byte
                 * with a NAA=3 (locally assigned address) in the most
                 * significant nubble.
                 *
                 * Later, we'll either use that or dig a
                 * WWN out of words 108..111.
                 */
                pptr->sas_address[0] = 0x30;
                pptr->sas_address[1] = 0;
                pptr->sas_address[2] = 0;
                pptr->sas_address[3] = 0;
                pptr->sas_address[4] = 0;
                pptr->sas_address[5] = 0;
                pptr->sas_address[6] = 0;
                pptr->sas_address[7] = phynum;
                pptr->portid = portid;
                pptr->link_rate = pmcs_link_rate(IOP_EVENT_LINK_RATE(w1));
                pptr->dtype = SATA;
                pmcs_set_changed(pwp, pptr, B_TRUE, 0);
                pmcs_prt(pwp, PMCS_PRT_INFO, pptr, NULL,
                    "PortID 0x%x: PHY 0x%x SATA LINK UP @ %s Gb/s",
                    pptr->portid, phynum, pmcs_get_rate(pptr->link_rate));
                wwn = pmcs_barray2wwn(pptr->sas_address);
                pmcs_unlock_phy(pptr);

                /* Add this PHY to the phymap */
                if (sas_phymap_phy_add(pwp->hss_phymap, phynum,
                    pwp->sas_wwns[0], wwn) != DDI_SUCCESS) {
                        pmcs_prt(pwp, PMCS_PRT_DEBUG, pptr, NULL,
                            "Unable to add phy %u for 0x%" PRIx64 ".0x%"
                            PRIx64, phynum, pwp->sas_wwns[pptr->phynum],
                            wwn);
                }

                /* Get our iport, if attached, and set it up */
                iport = pmcs_get_iport_by_wwn(pwp, wwn);
                if (iport) {
                        mutex_enter(&iport->lock);
                        iport->pptr = pptr;
                        if (iport->ua_state == UA_ACTIVE) {
                                pmcs_add_phy_to_iport(iport, pptr);
                                pptr->iport = iport;
                                ASSERT(iport->nphy == 1);
                                iport->nphy = 1;
                        }
                        mutex_exit(&iport->lock);
                        pmcs_rele_iport(iport);
                }

                pmcs_lock_phy(pptr);
                pmcs_update_phy_pm_props(pptr, (1ULL << phynum), 1ULL, B_TRUE);
                pmcs_smhba_log_sysevent(pwp, ESC_SAS_PHY_EVENT,
                    SAS_PHY_ONLINE, pptr);
                pmcs_unlock_phy(pptr);

                mutex_enter(&pwp->lock);
                pwp->ports[pptr->portid] = pptr;
                mutex_exit(&pwp->lock);
                RESTART_DISCOVERY(pwp);
                break;
        }

        case IOP_EVENT_SATA_SPINUP_HOLD:
                tphyp = (pmcs_phy_t *)(pwp->root_phys + phynum);
                /*
                 * No need to lock the entire tree for this
                 */
                mutex_enter(&tphyp->phy_lock);
                tphyp->spinup_hold = 1;
                pmcs_spinup_release(pwp, tphyp);
                mutex_exit(&tphyp->phy_lock);
                break;
        case IOP_EVENT_PHY_DOWN: {
                uint64_t wwn;

                /*
                 * If we're not at running state, don't do anything
                 */
                mutex_enter(&pwp->lock);
                if (pwp->state != STATE_RUNNING) {
                        mutex_exit(&pwp->lock);
                        break;
                }
                pptr = pwp->ports[portid];

                subphy = pwp->root_phys + phynum;
                /*
                 * subphy is a pointer to the PHY corresponding to the incoming
                 * event. pptr points to the primary PHY for the corresponding
                 * port.  So, subphy and pptr may or may not be the same PHY,
                 * but that doesn't change what we need to do with each.
                 */
                ASSERT(subphy);
                mutex_exit(&pwp->lock);

                if (pptr == NULL) {
                        pmcs_prt(pwp, PMCS_PRT_DEBUG, NULL, NULL,
                            "PortID 0x%x: PHY 0x%x LINK DOWN- no portid ptr",
                            portid, phynum);
                        break;
                }
                if (IOP_EVENT_PORT_STATE(w3) == IOP_EVENT_PS_NIL) {
                        pmcs_prt(pwp, PMCS_PRT_INFO, NULL, NULL,
                            "PortID 0x%x: PHY 0x%x NOT VALID YET",
                            portid, phynum);
                        need_ack = 1;
                        break;
                }
                if (IOP_EVENT_PORT_STATE(w3) == IOP_EVENT_PS_IN_RESET) {
                        pmcs_prt(pwp, PMCS_PRT_INFO, NULL, NULL,
                            "PortID 0x%x: PHY 0x%x IN RESET",
                            portid, phynum);
                        /* Entire port is down due to a host-initiated reset */
                        mutex_enter(&pptr->phy_lock);
                        /* Clear the phymask props in pptr */
                        pmcs_update_phy_pm_props(pptr, pptr->att_port_pm_tmp,
                            pptr->tgt_port_pm_tmp, B_FALSE);
                        iport = pptr->iport;
                        mutex_exit(&pptr->phy_lock);
                        if (iport) {
                                mutex_enter(&iport->lock);
                                pmcs_iport_teardown_phys(iport);
                                mutex_exit(&iport->lock);
                        }

                        /* Clear down all PHYs in the port */
                        for (pptr = pwp->root_phys; pptr;
                            pptr = pptr->sibling) {
                                pmcs_lock_phy(pptr);
                                if (pptr->portid == portid) {
                                        pptr->pend_dtype = NOTHING;
                                        pptr->dtype = NOTHING;
                                        pptr->portid =
                                            PMCS_IPORT_INVALID_PORT_ID;
                                        if (pptr->valid_device_id) {
                                                pptr->deregister_wait = 1;
                                        }
                                }
                                pmcs_unlock_phy(pptr);
                                SCHEDULE_WORK(pwp, PMCS_WORK_DEREGISTER_DEV);
                                (void) ddi_taskq_dispatch(pwp->tq, pmcs_worker,
                                    pwp, DDI_NOSLEEP);
                        }

                        break;
                }

                if (IOP_EVENT_PORT_STATE(w3) == IOP_EVENT_PS_LOSTCOMM) {
                        pmcs_prt(pwp, PMCS_PRT_INFO, NULL, NULL,
                            "PortID 0x%x: PHY 0x%x TEMPORARILY DOWN",
                            portid, phynum);
                        need_ack = 1;
                        break;
                }

                if (IOP_EVENT_PORT_STATE(w3) == IOP_EVENT_PS_VALID) {

                        /*
                         * This is not the last phy in the port, so if this
                         * is the primary PHY, promote another PHY to primary.
                         */
                        if (pptr == subphy) {
                                primary = !subphy->subsidiary;
                                ASSERT(primary);

                                tphyp = pptr;
                                pptr = pmcs_promote_next_phy(tphyp);

                                if (pptr) {
                                        /* Update primary pptr in ports */
                                        pwp->ports[portid] = pptr;
                                        pmcs_prt(pwp, PMCS_PRT_DEBUG, pptr,
                                            NULL, "PortID 0x%x: PHY 0x%x "
                                            "promoted to primary", portid,
                                            pptr->phynum);
                                } else {
                                        pmcs_prt(pwp, PMCS_PRT_DEBUG, pptr,
                                            NULL, "PortID 0x%x: PHY 0x%x: "
                                            "unable to promote phy", portid,
                                            phynum);
                                }
                        }

                        /*
                         * Drop port width on the primary phy handle
                         * No need to lock the entire tree for this
                         */
                        mutex_enter(&pptr->phy_lock);
                        pmcs_update_phy_pm_props(pptr, subphy->att_port_pm_tmp,
                            subphy->tgt_port_pm_tmp, B_FALSE);
                        pptr->width = IOP_EVENT_NPIP(w3);
                        mutex_exit(&pptr->phy_lock);

                        /* Clear the iport reference and portid on the subphy */
                        mutex_enter(&subphy->phy_lock);
                        iport = subphy->iport;
                        subphy->iport = NULL;
                        subphy->portid = PMCS_PHY_INVALID_PORT_ID;
                        subphy->pend_dtype = NOTHING;
                        subphy->dtype = NOTHING;
                        mutex_exit(&subphy->phy_lock);

                        /*
                         * If the iport was set on this phy, decrement its
                         * nphy count and remove this phy from the phys list.
                         */
                        if (iport) {
                                mutex_enter(&iport->lock);
                                if (iport->ua_state == UA_ACTIVE) {
                                        pmcs_remove_phy_from_iport(iport,
                                            subphy);
                                }
                                mutex_exit(&iport->lock);
                        }

                        pmcs_lock_phy(subphy);
                        wwn = pmcs_barray2wwn(pptr->sas_address);
                        if (subphy->subsidiary)
                                pmcs_clear_phy(pwp, subphy);
                        pmcs_unlock_phy(subphy);

                        /* Remove this PHY from the phymap */
                        if (sas_phymap_phy_rem(pwp->hss_phymap, phynum) !=
                            DDI_SUCCESS) {
                                pmcs_prt(pwp, PMCS_PRT_DEBUG, pptr, NULL,
                                    "Unable to remove phy %u for 0x%" PRIx64
                                    ".0x%" PRIx64, phynum,
                                    pwp->sas_wwns[pptr->phynum], wwn);
                        }

                        pmcs_prt(pwp, PMCS_PRT_INFO, pptr, NULL,
                            "PortID 0x%x: PHY 0x%x LINK DOWN NARROWS PORT "
                            "TO %d PHYS", portid, phynum, pptr->width);
                        break;
                }
                if (IOP_EVENT_PORT_STATE(w3) != IOP_EVENT_PS_INVALID) {
                        pmcs_prt(pwp, PMCS_PRT_INFO, NULL, NULL,
                            "PortID 0x%x: PHY 0x%x LINK DOWN NOT HANDLED "
                            "(state 0x%x)", portid, phynum,
                            IOP_EVENT_PORT_STATE(w3));
                        need_ack = 1;
                        break;
                }
                /* Remove this PHY from the phymap */
                if (sas_phymap_phy_rem(pwp->hss_phymap, phynum) !=
                    DDI_SUCCESS) {
                        pmcs_prt(pwp, PMCS_PRT_DEBUG, pptr, NULL,
                            "Unable to remove phy %u for 0x%" PRIx64
                            ".0x%" PRIx64, phynum,
                            pwp->sas_wwns[pptr->phynum], wwn);
                }

                pmcs_prt(pwp, PMCS_PRT_DEBUG, NULL, NULL,
                    "PortID 0x%x: PHY 0x%x LINK DOWN (port invalid)",
                    portid, phynum);

                /*
                 * Last PHY on the port.
                 * Assumption: pptr and subphy are both "valid".  In fact,
                 * they should be one and the same.
                 *
                 * Drop port width on the primary phy handle
                 * Report the event and clear its PHY pm props while we've
                 * got the lock
                 */
                ASSERT(pptr == subphy);
                mutex_enter(&pptr->phy_lock);
                pptr->width = 0;
                pmcs_update_phy_pm_props(pptr, pptr->att_port_pm_tmp,
                    pptr->tgt_port_pm_tmp, B_FALSE);
                pmcs_smhba_log_sysevent(pwp, ESC_SAS_PHY_EVENT,
                    SAS_PHY_OFFLINE, pptr);
                mutex_exit(&pptr->phy_lock);

                /* Clear the iport reference and portid on the subphy */
                pmcs_lock_phy(subphy);
                iport = subphy->iport;
                subphy->deregister_wait = 1;
                subphy->iport = NULL;
                subphy->portid = PMCS_PHY_INVALID_PORT_ID;
                subphy->pend_dtype = NOTHING;
                subphy->dtype = NOTHING;
                pmcs_unlock_phy(subphy);
                SCHEDULE_WORK(pwp, PMCS_WORK_DEREGISTER_DEV);
                (void) ddi_taskq_dispatch(pwp->tq, pmcs_worker,
                    pwp, DDI_NOSLEEP);

                /*
                 * If the iport was set on this phy, decrement its
                 * nphy count and remove this phy from the phys list.
                 * Also, clear the iport's pptr as this port is now
                 * down.
                 */
                if (iport) {
                        mutex_enter(&iport->lock);
                        if (iport->ua_state == UA_ACTIVE) {
                                pmcs_remove_phy_from_iport(iport, subphy);
                                iport->pptr = NULL;
                                iport->ua_state = UA_PEND_DEACTIVATE;
                        }
                        mutex_exit(&iport->lock);
                }

                pmcs_lock_phy(subphy);
                if (subphy->subsidiary)
                        pmcs_clear_phy(pwp, subphy);
                pmcs_unlock_phy(subphy);

                /*
                 * Since we're now really dead, it's time to clean up.
                 */
                pmcs_kill_port(pwp, portid);
                need_ack = 1;

                break;
        }
        case IOP_EVENT_BROADCAST_CHANGE:
                pmcs_prt(pwp, PMCS_PRT_DEBUG_CONFIG, NULL, NULL,
                    "PortID 0x%x: PHY 0x%x Broadcast Change", portid, phynum);
                need_ack = 1;
                mutex_enter(&pwp->lock);
                pptr = pwp->ports[portid];
                if (pptr) {
                        pmcs_lock_phy(pptr);
                        if (pptr->phynum == phynum) {
                                pmcs_set_changed(pwp, pptr, B_TRUE, 0);
                        }
                        pmcs_smhba_log_sysevent(pwp, ESC_SAS_HBA_PORT_BROADCAST,
                            SAS_PORT_BROADCAST_CHANGE, pptr);
                        pmcs_unlock_phy(pptr);
                }
                mutex_exit(&pwp->lock);
                RESTART_DISCOVERY(pwp);
                break;
        case IOP_EVENT_BROADCAST_SES:
                EVT_PRT(pwp, IOP_EVENT_BROADCAST_SES, phynum);
                mutex_enter(&pwp->lock);
                pptr = pwp->ports[portid];
                mutex_exit(&pwp->lock);
                if (pptr) {
                        pmcs_lock_phy(pptr);
                        pmcs_smhba_log_sysevent(pwp, ESC_SAS_HBA_PORT_BROADCAST,
                            SAS_PORT_BROADCAST_SES, pptr);
                        pmcs_unlock_phy(pptr);
                }
                break;
        case IOP_EVENT_PHY_ERR_INBOUND_CRC:
        {
                char buf[32];
                (void) snprintf(buf, sizeof (buf), "Inbound PHY CRC error");
                need_ack = 1;
                break;
        }
        case IOP_EVENT_HARD_RESET_RECEIVED:
                EVT_PRT(pwp, IOP_EVENT_HARD_RESET_RECEIVED, phynum);
                break;
        case IOP_EVENT_EVENT_ID_FRAME_TIMO:
                EVT_PRT(pwp, IOP_EVENT_EVENT_ID_FRAME_TIMO, phynum);
                break;
        case IOP_EVENT_BROADCAST_EXP:
                pmcs_prt(pwp, PMCS_PRT_INFO, NULL, NULL,
                    "PortID 0x%x: PHY 0x%x Broadcast Exp Change",
                    portid, phynum);
                /*
                 * Comparing Section 6.8.1.4 of SMHBA (rev 7) spec and Section
                 * 7.2.3 of SAS2 (Rev 15) spec,
                 * _BROADCAST_EXPANDER event corresponds to _D01_4 primitive
                 */
                mutex_enter(&pwp->lock);
                pptr = pwp->ports[portid];
                mutex_exit(&pwp->lock);
                if (pptr) {
                        pmcs_lock_phy(pptr);
                        pmcs_smhba_log_sysevent(pwp, ESC_SAS_HBA_PORT_BROADCAST,
                            SAS_PORT_BROADCAST_D01_4, pptr);
                        pmcs_unlock_phy(pptr);
                }
                break;
        case IOP_EVENT_PHY_START_STATUS:
                switch (IOP_EVENT_STATUS(w1)) {
                case IOP_PHY_START_OK:
                        pmcs_prt(pwp, PMCS_PRT_DEBUG_CONFIG, NULL, NULL,
                            "PHY 0x%x Started", phynum);
                        break;
                case IOP_PHY_START_ALREADY:
                        pmcs_prt(pwp, PMCS_PRT_INFO, NULL, NULL,
                            "PHY 0x%x Started (Already)", phynum);
                        break;
                case IOP_PHY_START_INVALID:
                        pmcs_prt(pwp, PMCS_PRT_WARN, NULL, NULL,
                            "PHY 0x%x failed to start (invalid phy)", phynum);
                        break;
                case IOP_PHY_START_ERROR:
                        pmcs_prt(pwp, PMCS_PRT_WARN, NULL, NULL,
                            "PHY 0x%x Start Error", phynum);
                        break;
                default:
                        pmcs_prt(pwp, PMCS_PRT_WARN, NULL, NULL,
                            "PHY 0x%x failed to start (0x%x)", phynum,
                            IOP_EVENT_STATUS(w1));
                        break;
                }
                /* Reposition htag to the 'expected' position. */
                ((uint32_t *)iomb)[1] = ((uint32_t *)iomb)[2];
                pmcs_process_completion(pwp, iomb, amt);
                break;
        case IOP_EVENT_PHY_ERR_INVALID_DWORD:
                need_ack = 1;
                EVT_PRT(pwp, IOP_EVENT_PHY_ERR_INVALID_DWORD, phynum);
                break;
        case IOP_EVENT_PHY_ERR_DISPARITY_ERROR:
                need_ack = 1;
                EVT_PRT(pwp, IOP_EVENT_PHY_ERR_DISPARITY_ERROR, phynum);
                break;
        case IOP_EVENT_PHY_ERR_CODE_VIOLATION:
                need_ack = 1;
                EVT_PRT(pwp, IOP_EVENT_PHY_ERR_CODE_VIOLATION, phynum);
                break;
        case IOP_EVENT_PHY_ERR_LOSS_OF_DWORD_SYN:
                need_ack = 1;
                EVT_PRT(pwp, IOP_EVENT_PHY_ERR_LOSS_OF_DWORD_SYN, phynum);
                break;
        case IOP_EVENT_PHY_ERR_PHY_RESET_FAILD:
                need_ack = 1;
                EVT_PRT(pwp, IOP_EVENT_PHY_ERR_PHY_RESET_FAILD, phynum);
                break;
        case IOP_EVENT_PORT_RECOVERY_TIMER_TMO:
                EVT_PRT(pwp, IOP_EVENT_PORT_RECOVERY_TIMER_TMO, phynum);
                break;
        case IOP_EVENT_PORT_RECOVER:
                EVT_PRT(pwp, IOP_EVENT_PORT_RECOVER, phynum);
                break;
        case IOP_EVENT_PORT_INVALID:
                mutex_enter(&pwp->lock);
                if (pwp->state != STATE_RUNNING) {
                        mutex_exit(&pwp->lock);
                        break;
                }
                mutex_exit(&pwp->lock);
                pmcs_kill_port(pwp, portid);
                pmcs_prt(pwp, PMCS_PRT_INFO, NULL, NULL,
                    "PortID 0x%x: PORT Now Invalid", portid);
                break;
        case IOP_EVENT_PORT_RESET_TIMER_TMO:
                EVT_PRT(pwp, IOP_EVENT_PORT_RESET_TIMER_TMO, phynum);
                break;
        case IOP_EVENT_PORT_RESET_COMPLETE:
                EVT_PRT(pwp, IOP_EVENT_PORT_RESET_COMPLETE, phynum);
                break;
        case IOP_EVENT_BROADCAST_ASYNC_EVENT:
                EVT_PRT(pwp, IOP_EVENT_BROADCAST_ASYNC_EVENT, phynum);
                /*
                 * Comparing Section 6.8.1.4 of SMHBA (rev 7) spec and Section
                 * 7.2.3 of SAS2 (Rev 15) spec,
                 * _BROADCAST_ASYNC event corresponds to _D04_7 primitive
                 */
                mutex_enter(&pwp->lock);
                pptr = pwp->ports[portid];
                mutex_exit(&pwp->lock);
                if (pptr) {
                        pmcs_lock_phy(pptr);
                        pmcs_smhba_log_sysevent(pwp, ESC_SAS_HBA_PORT_BROADCAST,
                            SAS_PORT_BROADCAST_D04_7, pptr);
                        pmcs_unlock_phy(pptr);
                }
                break;
        default:
                (void) snprintf(buf, sizeof (buf),
                    "unknown SAS H/W Event PHY 0x%x", phynum);
                pmcs_print_entry(pwp, PMCS_PRT_DEBUG, buf, iomb);
                break;
        }
        if (need_ack) {
                mutex_enter(&pwp->lock);
                /*
                 * Don't lock the entire tree for this.  Just grab the mutex
                 * on the root PHY.
                 */
                tphyp = pwp->root_phys + phynum;
                mutex_enter(&tphyp->phy_lock);
                tphyp->hw_event_ack = w1;
                mutex_exit(&tphyp->phy_lock);
                mutex_exit(&pwp->lock);
                pmcs_ack_events(pwp);
        }
}

static void
pmcs_process_echo_completion(pmcs_hw_t *pwp, void *iomb, size_t amt)
{
        echo_test_t fred;
        pmcwork_t *pwrk;
        uint32_t *msg = iomb, htag = LE_32(msg[1]);
        pwrk = pmcs_tag2wp(pwp, htag, B_FALSE);
        if (pwrk) {
                (void) memcpy(&fred, &((uint32_t *)iomb)[2], sizeof (fred));
                fred.ptr[0]++;
                msg[2] = LE_32(PMCOUT_STATUS_OK);
                pmcs_complete_work(pwp, pwrk, msg, amt);
        } else {
                pmcs_print_entry(pwp, PMCS_PRT_DEBUG,
                    "ECHO completion with no work structure", iomb);
        }
}

static void
pmcs_process_ssp_event(pmcs_hw_t *pwp, void *iomb, size_t amt)
{
        _NOTE(ARGUNUSED(amt));
        uint32_t status, htag, *w;
        pmcwork_t *pwrk;
        pmcs_phy_t *phyp = NULL;
        char *path;

        w = iomb;
        htag = LE_32(w[1]);
        status = LE_32(w[2]);


        pwrk = pmcs_tag2wp(pwp, htag, B_FALSE);
        if (pwrk == NULL) {
                path = "????";
        } else {
                phyp = pwrk->phy;
                path = pwrk->phy->path;
        }

        if (status != PMCOUT_STATUS_XFER_CMD_FRAME_ISSUED) {
                char buf[20];
                const char *emsg = pmcs_status_str(status);

                if (emsg == NULL) {
                        (void) snprintf(buf, sizeof (buf), "Status 0x%x",
                            status);
                        emsg = buf;
                }
                pmcs_prt(pwp, PMCS_PRT_DEBUG, phyp, NULL, "%s: Bad SAS Status "
                    "(tag 0x%x) %s on %s", __func__, htag, emsg, path);
                if (pwrk != NULL) {
                        /*
                         * There may be pending command on a target device.
                         * Or, it may be a double fault.
                         */
                        pmcs_start_ssp_event_recovery(pwp, pwrk, iomb, amt);
                }
        } else {
                pmcs_prt(pwp, PMCS_PRT_DEBUG2, phyp, NULL,
                    "%s: tag %x put onto the wire for %s",
                    __func__, htag, path);
                if (pwrk) {
                        pwrk->onwire = 1;
                        mutex_exit(&pwrk->lock);
                }
        }
}

static void
pmcs_process_sata_event(pmcs_hw_t *pwp, void *iomb, size_t amt)
{
        _NOTE(ARGUNUSED(amt));
        pmcwork_t *pwrk = NULL;
        pmcs_phy_t *pptr = NULL;
        uint32_t status, htag, *w;
        char *path = NULL;

        w = iomb;
        htag = LE_32(w[1]);
        status = LE_32(w[2]);

        /*
         * If the status is PMCOUT_STATUS_XFER_ERROR_ABORTED_NCQ_MODE,
         * we have to issue a READ LOG EXT ATA (page 0x10) command
         * to the device. In this case, htag is not valid.
         *
         * If the status is PMCOUT_STATUS_XFER_CMD_FRAME_ISSUED, we're
         * just noting that an I/O got put onto the wire.
         *
         * Othewise, other errors are indicative that things need to
         * be aborted.
         */
        if (htag) {
                pwrk = pmcs_tag2wp(pwp, htag, B_TRUE);
        }

        if (pwrk) {
                pptr = pwrk->phy;
                path = pptr->path;
        } else {
                pmcs_prt(pwp, PMCS_PRT_DEBUG, NULL, NULL, "%s: "
                    "cannot find work structure for SATA completion", __func__);
                return;
        }

        if (status != PMCOUT_STATUS_XFER_CMD_FRAME_ISSUED) {
                char buf[20];
                const char *emsg = pmcs_status_str(status);

                if (emsg == NULL) {
                        (void) snprintf(buf, sizeof (buf), "Status 0x%x",
                            status);
                        emsg = buf;
                }
                if (status == PMCOUT_STATUS_XFER_ERROR_ABORTED_NCQ_MODE) {
                        pptr->need_rl_ext = 1;
                        htag = 0;
                } else {
                        pptr->abort_pending = 1;
                }
                pmcs_prt(pwp, PMCS_PRT_DEBUG, pptr, NULL,
                    "%s: Bad SATA Status (tag 0x%x) %s on %s",
                    __func__, htag, emsg, path);
                SCHEDULE_WORK(pwp, PMCS_WORK_ABORT_HANDLE);
                /*
                 * Unlike SSP devices, we let the abort we
                 * schedule above force the completion of
                 * problem commands.
                 */
                mutex_exit(&pwrk->lock);
        } else {
                pmcs_prt(pwp, PMCS_PRT_DEBUG2, pptr, NULL,
                    "%s: tag %x put onto the wire for %s",
                    __func__, htag, path);
                pwrk->onwire = 1;
                mutex_exit(&pwrk->lock);
        }

        mutex_exit(&pptr->phy_lock);
}

static void
pmcs_process_abort_completion(pmcs_hw_t *pwp, void *iomb, size_t amt)
{
        pmcs_phy_t *pptr;
        struct pmcwork *pwrk;
        uint32_t htag = LE_32(((uint32_t *)iomb)[1]);
        uint32_t status = LE_32(((uint32_t *)iomb)[2]);
        uint32_t scope = LE_32(((uint32_t *)iomb)[3]) & 0x1;
        char *path;

        pwrk = pmcs_tag2wp(pwp, htag, B_TRUE);
        if (pwrk == NULL) {
                pmcs_prt(pwp, PMCS_PRT_DEBUG, NULL, NULL,
                    "%s: cannot find work structure for ABORT", __func__);
                return;
        }

        pptr = pwrk->phy;
        if (pptr) {
                pptr->abort_pending = 0;
                pptr->abort_sent = 0;

                /*
                 * Don't do this if the status was ABORT_IN_PROGRESS and
                 * the scope bit was set
                 */
                if ((status != PMCOUT_STATUS_IO_ABORT_IN_PROGRESS) || !scope) {
                        pptr->abort_all_start = 0;
                        cv_signal(&pptr->abort_all_cv);
                }
                path = pptr->path;
                mutex_exit(&pptr->phy_lock);
        } else {
                path = "(no phy)";
        }

        switch (status) {
        case PMCOUT_STATUS_OK:
                if (scope) {
                        pmcs_prt(pwp, PMCS_PRT_DEBUG, pptr, NULL,
                            "%s: abort all succeeded for %s. (htag=0x%x)",
                            __func__, path, htag);
                } else {
                        pmcs_prt(pwp, PMCS_PRT_DEBUG, pptr, NULL,
                            "%s: abort tag 0x%x succeeded for %s. (htag=0x%x)",
                            __func__, pwrk->abt_htag, path, htag);
                }
                break;

        case PMCOUT_STATUS_IO_NOT_VALID:
                if (scope) {
                        pmcs_prt(pwp, PMCS_PRT_DEBUG, pptr, NULL,
                            "%s: ABORT %s failed (DEV NOT VALID) for %s. "
                            "(htag=0x%x)", __func__, scope ? "all" : "tag",
                            path, htag);
                } else {
                        pmcs_prt(pwp, PMCS_PRT_DEBUG, pptr, NULL,
                            "%s: ABORT %s failed (I/O NOT VALID) for %s. "
                            "(htag=0x%x)", __func__, scope ? "all" : "tag",
                            path, htag);
                }
                break;

        case PMCOUT_STATUS_IO_ABORT_IN_PROGRESS:
                pmcs_prt(pwp, PMCS_PRT_DEBUG, pptr, NULL, "%s: ABORT %s failed "
                    "for %s, htag 0x%x (ABORT IN PROGRESS)", __func__,
                    scope ? "all" : "tag", path, htag);
                break;

        default:
                pmcs_prt(pwp, PMCS_PRT_DEBUG, pptr, NULL, "%s: Unknown status "
                    "%d for ABORT %s, htag 0x%x, PHY %s", __func__, status,
                    scope ? "all" : "tag", htag, path);
                break;
        }

        pmcs_complete_work(pwp, pwrk, iomb, amt);
}

static void
pmcs_process_general_event(pmcs_hw_t *pwp, uint32_t *iomb)
{
        uint32_t htag;
        char local[60];
        struct pmcwork *pwrk;
        int i;

        if (LE_32(iomb[1]) == INBOUND_IOMB_V_BIT_NOT_SET) {
                (void) snprintf(local, sizeof (local),
                    "VALID bit not set on INBOUND IOMB");
        } else if (LE_32(iomb[1]) ==
            INBOUND_IOMB_OPC_NOT_SUPPORTED) {
                (void) snprintf(local, sizeof (local),
                    "opcode not set on inbound IOMB");
        } else {
                (void) snprintf(local, sizeof (local),
                    "unknown GENERAL EVENT status (0x%x)",
                    LE_32(iomb[1]));
        }
        /* Pull up bad IOMB into usual position */
        for (i = 0; i < PMCS_MSG_SIZE - 2; i++) {
                iomb[i] = iomb[i+2];
        }
        /* overwrite status with an error */
        iomb[2] = LE_32(PMCOUT_STATUS_PROG_ERROR);
        iomb[PMCS_MSG_SIZE - 2] = 0;
        iomb[PMCS_MSG_SIZE - 1] = 0;
        htag = LE_32(iomb[1]);
        pmcs_print_entry(pwp, PMCS_PRT_DEBUG, local, iomb);
        pwrk = pmcs_tag2wp(pwp, htag, B_FALSE);
        if (pwrk) {
                pmcs_complete_work(pwp, pwrk, iomb, PMCS_QENTRY_SIZE);
        }
}

void
pmcs_general_intr(pmcs_hw_t *pwp)
{
        char local[PMCS_QENTRY_SIZE << 1];
        uint32_t w0, pi, ci;
        uint32_t *ptr, nbuf, lim = 0;
        size_t amt;

        ci = pmcs_rd_oqci(pwp, PMCS_OQ_GENERAL);
        pi = pmcs_rd_oqpi(pwp, PMCS_OQ_GENERAL);

        while (ci != pi) {
                OQLIM_CHECK(pwp, lim);
                ptr = GET_OQ_ENTRY(pwp, PMCS_OQ_GENERAL, ci, 0);
                w0 = LE_32(ptr[0]);
                VALID_IOMB_CHECK(pwp, w0, ptr, ci, pi);
                WRONG_OBID_CHECK(pwp, w0, PMCS_OQ_GENERAL);
                COPY_OUTBOUND(pwp, w0, local, nbuf, amt, ptr,
                    PMCS_OQ_GENERAL, ci);

                switch (w0 & PMCS_IOMB_OPCODE_MASK) {
                case PMCOUT_SSP_COMPLETION:
                        /*
                         * We only get SSP completion here for Task Management
                         * completions.
                         */
                case PMCOUT_SMP_COMPLETION:
                case PMCOUT_LOCAL_PHY_CONTROL:
                case PMCOUT_DEVICE_REGISTRATION:
                case PMCOUT_DEREGISTER_DEVICE_HANDLE:
                case PMCOUT_GET_NVMD_DATA:
                case PMCOUT_SET_NVMD_DATA:
                case PMCOUT_GET_DEVICE_STATE:
                case PMCOUT_SET_DEVICE_STATE:
                        pmcs_process_completion(pwp, local, amt);
                        break;
                case PMCOUT_SSP_ABORT:
                case PMCOUT_SATA_ABORT:
                case PMCOUT_SMP_ABORT:
                        pmcs_process_abort_completion(pwp, local, amt);
                        break;
                case PMCOUT_SSP_EVENT:
                        pmcs_process_ssp_event(pwp, local, amt);
                        break;
                case PMCOUT_ECHO:
                        pmcs_process_echo_completion(pwp, local, amt);
                        break;
                case PMCOUT_SAS_HW_EVENT_ACK_ACK:
                        if (LE_32(ptr[2]) != SAS_HW_EVENT_ACK_OK) {
                                pmcs_prt(pwp, PMCS_PRT_DEBUG, NULL, NULL,
                                    "SAS H/W EVENT ACK/ACK Status=0x%b",
                                    LE_32(ptr[2]), "\020\4InvParm\3"
                                    "InvPort\2InvPhy\1InvSEA");
                        }
                        pmcs_process_completion(pwp, local, amt);
                        break;
                case PMCOUT_SKIP_ENTRIES:
                        pmcs_prt(pwp, PMCS_PRT_DEBUG3, NULL, NULL,
                            "%s: skip %d entries", __func__, nbuf);
                        break;
                default:
                        (void) snprintf(local, sizeof (local),
                            "%s: unhandled message", __func__);
                        pmcs_print_entry(pwp, PMCS_PRT_DEBUG, local, ptr);
                        break;
                }
                STEP_OQ_ENTRY(pwp, PMCS_OQ_GENERAL, ci, nbuf);
        }
        if (lim) {
                SYNC_OQ_ENTRY(pwp, PMCS_OQ_GENERAL, ci, pi);
        }
}

/*
 * pmcs_check_intr_coal
 *
 * This function makes a determination on the dynamic value of the
 * interrupt coalescing timer register.  We only use this for I/O
 * completions.
 *
 * The basic algorithm is as follows:
 *
 * PMCS_MAX_IO_COMPS_PER_INTR: The maximum number of I/O completions per
 * I/O completion interrupt.  We won't increase the interrupt coalescing
 * timer if we're already processing this many completions per interrupt
 * beyond the threshold.
 *
 * Values in io_intr_coal structure:
 *
 * intr_latency: The average number of nsecs between interrupts during
 * the echo test.  Used to help determine whether to increase the coalescing
 * timer.
 *
 * intr_threshold: Calculated number of interrupts beyond which we may
 * increase the timer.  This value is calculated based on the calculated
 * interrupt latency during the ECHO test and the current value of the
 * coalescing timer.
 *
 * nsecs_between_intrs: Total number of nsecs between all the interrupts
 * in the current timeslice.
 *
 * last_io_comp: Time of the last I/O interrupt.
 *
 * num_io_completions: Number of I/O completions during the slice
 *
 * num_intrs: Number of I/O completion interrupts during the slice
 *
 * max_io_completions: Number of times we hit >= PMCS_MAX_IO_COMPS_PER_INTR
 * during interrupt processing.
 *
 * PMCS_MAX_IO_COMPS_LOWAT_SHIFT/HIWAT_SHIFT
 * Low and high marks used to determine whether we processed enough interrupts
 * that contained the maximum number of I/O completions to warrant increasing
 * the timer
 *
 * intr_coal_timer: The current value of the register (in usecs)
 *
 * timer_on: B_TRUE means we are using the timer
 *
 * The timer is increased if we processed more than intr_threshold interrupts
 * during the quantum and the number of interrupts containing the maximum
 * number of I/O completions is between PMCS_MAX_IO_COMPS_LOWAT_SHIFT and
 * _HIWAT_SHIFT
 *
 * If the average time between completions is greater than twice
 * the current timer value, the timer value is decreased.
 *
 * If we did not take any interrupts during a quantum, we turn the timer off.
 */
void
pmcs_check_intr_coal(void *arg)
{
        pmcs_hw_t       *pwp = (pmcs_hw_t *)arg;
        uint32_t        avg_nsecs;
        clock_t         lbolt, ret;
        pmcs_io_intr_coal_t *ici;

        ici = &pwp->io_intr_coal;
        mutex_enter(&pwp->ict_lock);
        while (ici->stop_thread == B_FALSE) {
                /*
                 * Wait for next time quantum... collect stats
                 */
                lbolt = ddi_get_lbolt();
                while (ici->stop_thread == B_FALSE) {
                        ret = cv_timedwait(&pwp->ict_cv, &pwp->ict_lock,
                            lbolt + ici->quantum);
                        if (ret == -1) {
                                break;
                        }
                }

                if (ici->stop_thread == B_TRUE) {
                        continue;
                }

                DTRACE_PROBE1(pmcs__check__intr__coal, pmcs_io_intr_coal_t *,
                    &pwp->io_intr_coal);

                /*
                 * Determine whether to adjust timer
                 */
                if (ici->num_intrs == 0) {
                        /*
                         * If timer is off, nothing more to do.
                         */
                        if (!pwp->io_intr_coal.timer_on) {
                                continue;
                        }

                        /*
                         * No interrupts.  Turn off the timer.
                         */
                        pmcs_wr_topunit(pwp, PMCS_INT_COALESCING_CONTROL, 0);

                        if (pwp->odb_auto_clear & (1 << PMCS_MSIX_IODONE)) {
                                pmcs_wr_topunit(pwp, PMCS_OBDB_AUTO_CLR,
                                    pwp->odb_auto_clear);
                        }

                        ici->timer_on = B_FALSE;
                        ici->max_io_completions = 0;
                        ici->num_intrs = 0;
                        ici->int_cleared = B_FALSE;
                        ici->num_io_completions = 0;
                        DTRACE_PROBE1(pmcs__intr__coalesce__timer__off,
                            pmcs_io_intr_coal_t *, ici);
                        continue;
                }

                avg_nsecs = ici->nsecs_between_intrs / ici->num_intrs;

                if ((ici->num_intrs > ici->intr_threshold) &&
                    (ici->max_io_completions > (ici->num_intrs >>
                    PMCS_MAX_IO_COMPS_LOWAT_SHIFT)) &&
                    (ici->max_io_completions < (ici->num_intrs >>
                    PMCS_MAX_IO_COMPS_HIWAT_SHIFT))) {
                        pmcs_set_intr_coal_timer(pwp, INCREASE_TIMER);
                } else if (avg_nsecs >
                    (ici->intr_coal_timer * 1000 * 2)) {
                        pmcs_set_intr_coal_timer(pwp, DECREASE_TIMER);
                }

                /*
                 * Reset values for new sampling period.
                 */
                ici->max_io_completions = 0;
                ici->nsecs_between_intrs = 0;
                ici->num_intrs = 0;
                ici->num_io_completions = 0;

                /*
                 * If a firmware event log file is configured, check to see
                 * if it needs to be written to the file.  We do this here
                 * because writing to a file from a callout thread (i.e.
                 * from the watchdog timer) can cause livelocks.
                 */
                if (pwp->fwlog_file) {
                        mutex_exit(&pwp->ict_lock);
                        pmcs_gather_fwlog(pwp);
                        mutex_enter(&pwp->ict_lock);
                }
        }

        mutex_exit(&pwp->ict_lock);
        thread_exit();
}

void
pmcs_iodone_intr(pmcs_hw_t *pwp)
{
        char local[PMCS_QENTRY_SIZE << 1];
        pmcs_iocomp_cb_t *ioccb;
        uint32_t w0, ci, pi, nbuf, lim = 0, niodone = 0, iomb_opcode;
        size_t amt;
        uint32_t *ptr;
        hrtime_t curtime = gethrtime();

        ci = pmcs_rd_oqci(pwp, PMCS_OQ_IODONE);
        pi = pmcs_rd_oqpi(pwp, PMCS_OQ_IODONE);

        while (ci != pi) {
                OQLIM_CHECK(pwp, lim);
                ptr = GET_OQ_ENTRY(pwp, PMCS_OQ_IODONE, ci, 0);
                w0 = LE_32(ptr[0]);
                VALID_IOMB_CHECK(pwp, w0, ptr, ci, pi);
                WRONG_OBID_CHECK(pwp, w0, PMCS_OQ_IODONE);
                iomb_opcode = (w0 & PMCS_IOMB_OPCODE_MASK);

                if ((iomb_opcode == PMCOUT_SSP_COMPLETION) ||
                    (iomb_opcode == PMCOUT_SATA_COMPLETION)) {
                        ioccb =
                            kmem_cache_alloc(pwp->iocomp_cb_cache, KM_NOSLEEP);
                        if (ioccb == NULL) {
                                pmcs_prt(pwp, PMCS_PRT_WARN, NULL, NULL,
                                    "%s: kmem_cache_alloc failed", __func__);
                                break;
                        }

                        COPY_OUTBOUND(pwp, w0, ioccb->iomb, nbuf, amt, ptr,
                            PMCS_OQ_IODONE, ci);

                        niodone++;
                        pmcs_process_io_completion(pwp, ioccb, amt);
                } else {
                        COPY_OUTBOUND(pwp, w0, local, nbuf, amt, ptr,
                            PMCS_OQ_IODONE, ci);

                        switch (iomb_opcode) {
                        case PMCOUT_ECHO:
                                pmcs_process_echo_completion(pwp, local, amt);
                                break;
                        case PMCOUT_SATA_EVENT:
                                pmcs_process_sata_event(pwp, local, amt);
                                break;
                        case PMCOUT_SSP_EVENT:
                                pmcs_process_ssp_event(pwp, local, amt);
                                break;
                        case PMCOUT_SKIP_ENTRIES:
                                pmcs_prt(pwp, PMCS_PRT_DEBUG3, NULL, NULL,
                                    "%s: skip %d entries", __func__, nbuf);
                                break;
                        default:
                                (void) snprintf(local, sizeof (local),
                                    "%s: unhandled message", __func__);
                                pmcs_print_entry(pwp, PMCS_PRT_DEBUG, local,
                                    ptr);
                                break;
                        }
                }

                STEP_OQ_ENTRY(pwp, PMCS_OQ_IODONE, ci, nbuf);
        }

        if (lim != 0) {
                SYNC_OQ_ENTRY(pwp, PMCS_OQ_IODONE, ci, pi);
        }

        /*
         * Update the interrupt coalescing timer check stats and run
         * completions for queued up commands.
         */

        if (niodone > 0) {
                /*
                 * If we can't get the lock, then completions are either
                 * already running or will be scheduled to do so shortly.
                 */
                if (mutex_tryenter(&pwp->cq_lock) != 0) {
                        PMCS_CQ_RUN_LOCKED(pwp);
                        mutex_exit(&pwp->cq_lock);
                }

                mutex_enter(&pwp->ict_lock);
                pwp->io_intr_coal.nsecs_between_intrs +=
                    curtime - pwp->io_intr_coal.last_io_comp;
                pwp->io_intr_coal.num_intrs++;
                pwp->io_intr_coal.num_io_completions += niodone;
                if (niodone >= PMCS_MAX_IO_COMPS_PER_INTR) {
                        pwp->io_intr_coal.max_io_completions++;
                }
                pwp->io_intr_coal.last_io_comp = gethrtime();
                mutex_exit(&pwp->ict_lock);
        }
}

void
pmcs_event_intr(pmcs_hw_t *pwp)
{
        char local[PMCS_QENTRY_SIZE << 1];
        uint32_t w0, ci, pi, nbuf, lim =  0;
        size_t amt;
        uint32_t *ptr;

        ci = pmcs_rd_oqci(pwp, PMCS_OQ_EVENTS);
        pi = pmcs_rd_oqpi(pwp, PMCS_OQ_EVENTS);

        while (ci != pi) {
                OQLIM_CHECK(pwp, lim);
                ptr = GET_OQ_ENTRY(pwp, PMCS_OQ_EVENTS, ci, 0);
                w0 = LE_32(ptr[0]);
                VALID_IOMB_CHECK(pwp, w0, ptr, ci, pi);
                WRONG_OBID_CHECK(pwp, w0, PMCS_OQ_EVENTS);
                COPY_OUTBOUND(pwp, w0, local, nbuf, amt, ptr,
                    PMCS_OQ_EVENTS, ci);

                switch (w0 & PMCS_IOMB_OPCODE_MASK) {
                case PMCOUT_ECHO:
                        pmcs_process_echo_completion(pwp, local, amt);
                        break;
                case PMCOUT_SATA_EVENT:
                        pmcs_process_sata_event(pwp, local, amt);
                        break;
                case PMCOUT_SSP_EVENT:
                        pmcs_process_ssp_event(pwp, local, amt);
                        break;
                case PMCOUT_GENERAL_EVENT:
                        pmcs_process_general_event(pwp, ptr);
                        break;
                case PMCOUT_DEVICE_HANDLE_REMOVED:
                {
                        uint32_t port = IOP_EVENT_PORTID(LE_32(ptr[1]));
                        uint32_t did = LE_32(ptr[2]);
                        pmcs_prt(pwp, PMCS_PRT_DEBUG, NULL, NULL,
                            "PortID 0x%x device_id 0x%x removed", port, did);
                        break;
                }
                case PMCOUT_SAS_HW_EVENT:
                        if (nbuf > 1) {
                                pmcs_prt(pwp, PMCS_PRT_INFO, NULL, NULL,
                                    "multiple SAS HW_EVENT (%d) responses "
                                    "in EVENT OQ", nbuf);
                        }
                        pmcs_process_sas_hw_event(pwp, local, PMCS_QENTRY_SIZE);
                        break;
                case PMCOUT_FW_FLASH_UPDATE:
                case PMCOUT_GET_TIME_STAMP:
                case PMCOUT_GET_DEVICE_STATE:
                case PMCOUT_SET_DEVICE_STATE:
                case PMCOUT_SAS_DIAG_EXECUTE:
                        pmcs_process_completion(pwp, local, amt);
                        break;
                case PMCOUT_SKIP_ENTRIES:
                        pmcs_prt(pwp, PMCS_PRT_DEBUG3, NULL, NULL,
                            "%s: skip %d entries", __func__, nbuf);
                        break;
                default:
                        (void) snprintf(local, sizeof (local),
                            "%s: unhandled message", __func__);
                        pmcs_print_entry(pwp, PMCS_PRT_DEBUG, local, ptr);
                        break;
                }
                STEP_OQ_ENTRY(pwp, PMCS_OQ_EVENTS, ci, nbuf);
        }
        if (lim) {
                SYNC_OQ_ENTRY(pwp, PMCS_OQ_EVENTS, ci, pi);
        }
}

void
pmcs_timed_out(pmcs_hw_t *pwp, uint32_t htag, const char *func)
{
#ifdef  DEBUG
        hrtime_t now = gethrtime();
        int i;

        for (i = 0; i < 256; i++) {
                if (pwp->ftags[i] == htag) {
                        pmcs_prt(pwp, PMCS_PRT_DEBUG, NULL, NULL,
                            "Inbound msg (tag 0x%8x) timed out - "
                            "was started %llu ns ago in %s:%d",
                            htag, (unsigned long long) (now - pwp->ftime[i]),
                            func, pwp->ftag_lines[i]);
                        return;
                }
        }
#endif
        pmcs_prt(pwp, PMCS_PRT_DEBUG, NULL, NULL,
            "Inbound Message (tag 0x%08x) timed out- was started in %s",
            htag, func);
}