root/usr/src/uts/common/io/mii/mii.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 2010 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/*
 * Copyright (c) 2018, Joyent, Inc.
 */

/*
 * mii - MII/PHY support for MAC drivers
 *
 * Utility module to provide a consistent interface to a MAC driver accross
 * different implementations of PHY devices
 */

#include <sys/types.h>
#include <sys/debug.h>
#include <sys/errno.h>
#include <sys/param.h>
#include <sys/kmem.h>
#include <sys/conf.h>
#include <sys/ddi.h>
#include <sys/sunddi.h>
#include <sys/modctl.h>
#include <sys/cmn_err.h>
#include <sys/policy.h>
#include <sys/note.h>
#include <sys/strsun.h>
#include <sys/miiregs.h>
#include <sys/mac_provider.h>
#include <sys/mac_ether.h>
#include <sys/mii.h>
#include "miipriv.h"

#define MII_SECOND      1000000

/* indices into error array */
enum {
        MII_EOK = 0,
        MII_ERESET,
        MII_ESTART,
        MII_ENOPHY,
        MII_ECHECK,
        MII_ELOOP,
};

static const char * const mii_errors[] = {
        "",
        "Failure resetting PHY.",
        "Failure starting PHY.",
        "No Ethernet PHY found.",
        "Failure reading PHY (removed?)",
        "Failure setting loopback."
};

/* Indexed by XCVR_ type */
static const char * const mii_xcvr_types[] = {
        "Undefined",
        "Unknown",
        "10 Mbps",
        "100BASE-T4",
        "100BASE-X",
        "100BASE-T2",
        "1000BASE-X",
        "1000BASE-T"
};

/* state machine */
typedef enum {
        MII_STATE_PROBE = 0,
        MII_STATE_RESET,
        MII_STATE_START,
        MII_STATE_RUN,
        MII_STATE_LOOPBACK,
} mii_tstate_t;

struct mii_handle {
        dev_info_t      *m_dip;
        void            *m_private;
        mii_ops_t       m_ops;

        kt_did_t        m_tq_id;
        kmutex_t        m_lock;
        kcondvar_t      m_cv;
        ddi_taskq_t     *m_tq;
        int             m_flags;

        boolean_t       m_started;
        boolean_t       m_suspending;
        boolean_t       m_suspended;
        int             m_error;
        mii_tstate_t    m_tstate;

#define MII_FLAG_EXIT           0x1     /* exit the thread */
#define MII_FLAG_STOP           0x2     /* shutdown MII monitoring */
#define MII_FLAG_RESET          0x4     /* reset the MII */
#define MII_FLAG_PROBE          0x8     /* probe for PHYs */
#define MII_FLAG_NOTIFY         0x10    /* notify about a change */
#define MII_FLAG_SUSPEND        0x20    /* monitoring suspended */
#define MII_FLAG_MACRESET       0x40    /* send reset to MAC */
#define MII_FLAG_PHYSTART       0x80    /* start up the PHY */

        /* device name for printing, e.g. "hme0" */
        char            m_name[MODMAXNAMELEN + 16];

        int             m_addr;
        phy_handle_t    m_phys[32];
        phy_handle_t    m_bogus_phy;
        phy_handle_t    *m_phy;

        link_state_t    m_link;

        /* these start out undefined, but get values due to mac_prop_set */
        int             m_en_aneg;
        int             m_en_10_hdx;
        int             m_en_10_fdx;
        int             m_en_100_t4;
        int             m_en_100_hdx;
        int             m_en_100_fdx;
        int             m_en_1000_hdx;
        int             m_en_1000_fdx;
        int             m_en_flowctrl;

        boolean_t       m_cap_pause;
        boolean_t       m_cap_asmpause;
};


static void _mii_task(void *);
static void _mii_probe_phy(phy_handle_t *);
static void _mii_probe(mii_handle_t);
static int _mii_reset(mii_handle_t);
static int _mii_loopback(mii_handle_t);
static void _mii_notify(mii_handle_t);
static int _mii_check(mii_handle_t);
static int _mii_start(mii_handle_t);

/*
 * Loadable module structures/entrypoints
 */

extern struct mod_ops mod_misc_ops;

static struct modlmisc modlmisc = {
        &mod_miscops,
        "802.3 MII support",
};

static struct modlinkage modlinkage = {
        MODREV_1, &modlmisc, NULL
};

int
_init(void)
{
        return (mod_install(&modlinkage));
}

int
_fini(void)
{
        return (mod_remove(&modlinkage));
}

int
_info(struct modinfo *modinfop)
{
        return (mod_info(&modlinkage, modinfop));
}

void
_mii_error(mii_handle_t mh, int errno)
{
        /*
         * This dumps an error message, but it avoids filling the log with
         * repeated error messages.
         */
        if (mh->m_error != errno) {
                cmn_err(CE_WARN, "%s: %s", mh->m_name, mii_errors[errno]);
                mh->m_error = errno;
        }
}

/*
 * Known list of specific PHY probes.
 */
typedef boolean_t (*phy_probe_t)(phy_handle_t *);
phy_probe_t _phy_probes[] = {
        phy_natsemi_probe,
        phy_intel_probe,
        phy_qualsemi_probe,
        phy_cicada_probe,
        phy_marvell_probe,
        phy_realtek_probe,
        phy_other_probe,
        NULL
};

/*
 * MII Interface functions
 */

mii_handle_t
mii_alloc_instance(void *private, dev_info_t *dip, int inst, mii_ops_t *ops)
{
        mii_handle_t    mh;
        char            tqname[16];

        if (ops->mii_version != MII_OPS_VERSION) {
                cmn_err(CE_WARN, "%s: incompatible MII version (%d)",
                    ddi_driver_name(dip), ops->mii_version);
                return (NULL);
        }
        mh = kmem_zalloc(sizeof (*mh), KM_SLEEP);

        (void) snprintf(mh->m_name, sizeof (mh->m_name), "%s%d",
            ddi_driver_name(dip), inst);

        /* DDI will prepend the driver name */
        (void) snprintf(tqname, sizeof (tqname), "mii%d", inst);

        mh->m_dip = dip;
        mh->m_ops = *ops;
        mh->m_private = private;
        mh->m_suspended = B_FALSE;
        mh->m_started = B_FALSE;
        mh->m_tstate = MII_STATE_PROBE;
        mh->m_link = LINK_STATE_UNKNOWN;
        mh->m_error = MII_EOK;
        mh->m_addr = -1;
        mutex_init(&mh->m_lock, NULL, MUTEX_DRIVER, NULL);
        cv_init(&mh->m_cv, NULL, CV_DRIVER, NULL);

        mh->m_tq = ddi_taskq_create(dip, tqname, 1, TASKQ_DEFAULTPRI, 0);
        if (mh->m_tq == NULL) {
                cmn_err(CE_WARN, "%s: unable to create MII monitoring task",
                    ddi_driver_name(dip));
                cv_destroy(&mh->m_cv);
                mutex_destroy(&mh->m_lock);
                kmem_free(mh, sizeof (*mh));
                return (NULL);
        }

        /*
         * Initialize user prefs by loading properties.  Ultimately,
         * Brussels interfaces would be superior here.
         */
#define GETPROP(name)   ddi_prop_get_int(DDI_DEV_T_ANY, dip, 0, name, -1)
        mh->m_en_aneg = GETPROP("adv_autoneg_cap");
        mh->m_en_10_hdx = GETPROP("adv_10hdx_cap");
        mh->m_en_10_fdx = GETPROP("adv_10fdx_cap");
        mh->m_en_100_hdx = GETPROP("adv_100hdx_cap");
        mh->m_en_100_fdx = GETPROP("adv_100fdx_cap");
        mh->m_en_100_t4 = GETPROP("adv_100T4_cap");
        mh->m_en_1000_hdx = GETPROP("adv_1000hdx_cap");
        mh->m_en_1000_fdx = GETPROP("adv_1000fdx_cap");

        mh->m_cap_pause = B_FALSE;
        mh->m_cap_asmpause = B_FALSE;

        bzero(&mh->m_bogus_phy, sizeof (mh->m_bogus_phy));
        mh->m_bogus_phy.phy_link = LINK_STATE_UNKNOWN;
        mh->m_bogus_phy.phy_duplex = LINK_DUPLEX_UNKNOWN;
        mh->m_bogus_phy.phy_addr = 0xff;
        mh->m_bogus_phy.phy_type = XCVR_NONE;
        mh->m_bogus_phy.phy_id = (uint32_t)-1;
        mh->m_bogus_phy.phy_loopback = PHY_LB_NONE;
        mh->m_bogus_phy.phy_flowctrl = LINK_FLOWCTRL_NONE;
        mh->m_phy = &mh->m_bogus_phy;

        for (int i = 0; i < 32; i++) {
                mh->m_phys[i].phy_mii = mh;
        }
        mh->m_bogus_phy.phy_mii = mh;

        return (mh);
}

mii_handle_t
mii_alloc(void *private, dev_info_t *dip, mii_ops_t *ops)
{
        return (mii_alloc_instance(private, dip, ddi_get_instance(dip), ops));
}

void
mii_set_pauseable(mii_handle_t mh, boolean_t pauseable, boolean_t asymetric)
{
        phy_handle_t    *ph;

        mutex_enter(&mh->m_lock);
        ph = mh->m_phy;
        ph->phy_cap_pause = mh->m_cap_pause = pauseable;
        ph->phy_cap_asmpause = mh->m_cap_asmpause = asymetric;
        if (pauseable) {
                mh->m_en_flowctrl = LINK_FLOWCTRL_BI;
        } else {
                mh->m_en_flowctrl = LINK_FLOWCTRL_NONE;
        }
        mutex_exit(&mh->m_lock);
}

void
mii_free(mii_handle_t mh)
{
        mutex_enter(&mh->m_lock);
        mh->m_started = B_FALSE;
        cv_broadcast(&mh->m_cv);
        mutex_exit(&mh->m_lock);

        ddi_taskq_destroy(mh->m_tq);
        mutex_destroy(&mh->m_lock);
        cv_destroy(&mh->m_cv);
        kmem_free(mh, sizeof (*mh));
}

void
mii_reset(mii_handle_t mh)
{
        mutex_enter(&mh->m_lock);
        if (mh->m_tstate > MII_STATE_RESET)
                mh->m_tstate = MII_STATE_RESET;
        cv_broadcast(&mh->m_cv);
        mutex_exit(&mh->m_lock);
}

void
mii_suspend(mii_handle_t mh)
{
        mutex_enter(&mh->m_lock);
        while ((!mh->m_suspended) && (mh->m_started)) {
                mh->m_suspending = B_TRUE;
                cv_broadcast(&mh->m_cv);
                cv_wait(&mh->m_cv, &mh->m_lock);
        }
        mutex_exit(&mh->m_lock);
}

void
mii_resume(mii_handle_t mh)
{
        mutex_enter(&mh->m_lock);

        switch (mh->m_tstate) {
        case MII_STATE_PROBE:
                break;
        case MII_STATE_RESET:
        case MII_STATE_START:
        case MII_STATE_RUN:
                /* let monitor thread deal with this */
                mh->m_tstate = MII_STATE_RESET;
                break;

        case MII_STATE_LOOPBACK:
                /* loopback is handled synchronously */
                (void) _mii_loopback(mh);
                break;
        }

        mh->m_suspended = B_FALSE;
        cv_broadcast(&mh->m_cv);
        mutex_exit(&mh->m_lock);
}

void
mii_start(mii_handle_t mh)
{
        mutex_enter(&mh->m_lock);
        if (!mh->m_started) {
                mh->m_tstate = MII_STATE_PROBE;
                mh->m_started = B_TRUE;
                if (ddi_taskq_dispatch(mh->m_tq, _mii_task, mh, DDI_NOSLEEP) !=
                    DDI_SUCCESS) {
                        cmn_err(CE_WARN,
                            "%s: unable to start MII monitoring task",
                            mh->m_name);
                        mh->m_started = B_FALSE;
                }
        }
        cv_broadcast(&mh->m_cv);
        mutex_exit(&mh->m_lock);
}

void
mii_stop(mii_handle_t mh)
{
        mutex_enter(&mh->m_lock);
        mh->m_started = B_FALSE;
        /*
         * Reset link state to unknown defaults, since we're not
         * monitoring it anymore.  We'll reprobe all link state later.
         */
        mh->m_link = LINK_STATE_UNKNOWN;
        mh->m_phy = &mh->m_bogus_phy;
        cv_broadcast(&mh->m_cv);
        mutex_exit(&mh->m_lock);
        /*
         * Notify the MAC driver.  This will allow it to call back
         * into the MAC framework to clear any previous link state.
         */
        _mii_notify(mh);
}

void
mii_probe(mii_handle_t mh)
{
        mutex_enter(&mh->m_lock);
        _mii_probe(mh);
        mutex_exit(&mh->m_lock);
}

void
mii_check(mii_handle_t mh)
{
        mutex_enter(&mh->m_lock);
        cv_broadcast(&mh->m_cv);
        mutex_exit(&mh->m_lock);
}

int
mii_get_speed(mii_handle_t mh)
{
        phy_handle_t    *ph = mh->m_phy;

        return (ph->phy_speed);
}

link_duplex_t
mii_get_duplex(mii_handle_t mh)
{
        phy_handle_t    *ph = mh->m_phy;

        return (ph->phy_duplex);
}

link_state_t
mii_get_state(mii_handle_t mh)
{
        phy_handle_t    *ph = mh->m_phy;

        return (ph->phy_link);
}

link_flowctrl_t
mii_get_flowctrl(mii_handle_t mh)
{
        phy_handle_t    *ph = mh->m_phy;

        return (ph->phy_flowctrl);
}

int
mii_get_loopmodes(mii_handle_t mh, lb_property_t *modes)
{
        phy_handle_t    *ph = mh->m_phy;
        int             cnt = 0;
        lb_property_t   lmodes[MII_LOOPBACK_MAX];

        lmodes[cnt].lb_type = normal;
        (void) strlcpy(lmodes[cnt].key, "normal", sizeof (lmodes[cnt].key));
        lmodes[cnt].value = PHY_LB_NONE;
        cnt++;

        if (ph->phy_cap_1000_fdx ||
            ph->phy_cap_100_fdx ||
            ph->phy_cap_10_fdx) {
                /* we only support full duplex internal phy testing */
                lmodes[cnt].lb_type = internal;
                (void) strlcpy(lmodes[cnt].key, "PHY",
                    sizeof (lmodes[cnt].key));
                lmodes[cnt].value = PHY_LB_INT_PHY;
                cnt++;
        }

        if (ph->phy_cap_1000_fdx) {
                lmodes[cnt].lb_type = external;
                (void) strlcpy(lmodes[cnt].key, "1000Mbps",
                    sizeof (lmodes[cnt].key));
                lmodes[cnt].value = PHY_LB_EXT_1000;
                cnt++;
        }

        if (ph->phy_cap_100_fdx) {
                lmodes[cnt].lb_type = external;
                (void) strlcpy(lmodes[cnt].key, "100Mbps",
                    sizeof (lmodes[cnt].key));
                lmodes[cnt].value = PHY_LB_EXT_100;
                cnt++;
        }

        if (ph->phy_cap_10_fdx) {
                lmodes[cnt].lb_type = external;
                (void) strlcpy(lmodes[cnt].key, "10Mbps",
                    sizeof (lmodes[cnt].key));
                lmodes[cnt].value = PHY_LB_EXT_10;
                cnt++;
        }

        if (modes) {
                bcopy(lmodes, modes, sizeof (lb_property_t) * cnt);
        }

        return (cnt);
}

uint32_t
mii_get_loopback(mii_handle_t mh)
{
        phy_handle_t    *ph = mh->m_phy;

        return (ph->phy_loopback);
}

int
mii_set_loopback(mii_handle_t mh, uint32_t loop)
{
        phy_handle_t    *ph;
        int             rv;

        mutex_enter(&mh->m_lock);
        ph = mh->m_phy;

        if ((!mh->m_started) || (!ph->phy_present) ||
            (loop >= mii_get_loopmodes(mh, NULL))) {
                return (EINVAL);
        }

        ph->phy_loopback = loop;
        rv = _mii_loopback(mh);
        if (rv == DDI_SUCCESS) {
                mh->m_tstate = MII_STATE_LOOPBACK;
        }
        cv_broadcast(&mh->m_cv);
        mutex_exit(&mh->m_lock);

        return (rv == DDI_SUCCESS ? 0 : EIO);
}

uint32_t
mii_get_id(mii_handle_t mh)
{
        phy_handle_t    *ph = mh->m_phy;

        return (ph->phy_id);
}

int
mii_get_addr(mii_handle_t mh)
{
        return (mh->m_addr);
}

/* GLDv3 helpers */

boolean_t
mii_m_loop_ioctl(mii_handle_t mh, queue_t *wq, mblk_t *mp)
{
        struct iocblk   *iocp;
        int             rv = 0;
        int             cnt;
        lb_property_t   modes[MII_LOOPBACK_MAX];
        lb_info_sz_t    sz;
        int             cmd;
        uint32_t        mode;

        iocp = (void *)mp->b_rptr;
        cmd = iocp->ioc_cmd;

        switch (cmd) {
        case LB_SET_MODE:
        case LB_GET_INFO_SIZE:
        case LB_GET_INFO:
        case LB_GET_MODE:
                break;

        default:
                return (B_FALSE);
        }

        if (mp->b_cont == NULL) {
                miocnak(wq, mp, 0, EINVAL);
                return (B_TRUE);
        }

        switch (cmd) {
        case LB_GET_INFO_SIZE:
                cnt = mii_get_loopmodes(mh, modes);
                if (iocp->ioc_count != sizeof (sz)) {
                        rv = EINVAL;
                } else {
                        sz = cnt * sizeof (lb_property_t);
                        bcopy(&sz, mp->b_cont->b_rptr, sizeof (sz));
                }
                break;

        case LB_GET_INFO:
                cnt = mii_get_loopmodes(mh, modes);
                if (iocp->ioc_count != (cnt * sizeof (lb_property_t))) {
                        rv = EINVAL;
                } else {
                        bcopy(modes, mp->b_cont->b_rptr, iocp->ioc_count);
                }
                break;

        case LB_GET_MODE:
                if (iocp->ioc_count != sizeof (mode)) {
                        rv = EINVAL;
                } else {
                        mode = mii_get_loopback(mh);
                        bcopy(&mode, mp->b_cont->b_rptr, sizeof (mode));
                }
                break;

        case LB_SET_MODE:
                rv = secpolicy_net_config(iocp->ioc_cr, B_FALSE);
                if (rv != 0)
                        break;
                if (iocp->ioc_count != sizeof (mode)) {
                        rv = EINVAL;
                        break;
                }
                bcopy(mp->b_cont->b_rptr, &mode, sizeof (mode));
                rv = mii_set_loopback(mh, mode);
                break;
        }

        if (rv == 0) {
                miocack(wq, mp, iocp->ioc_count, 0);
        } else {
                miocnak(wq, mp, 0, rv);
        }
        return (B_TRUE);
}

int
mii_m_getprop(mii_handle_t mh, const char *name, mac_prop_id_t num,
    uint_t sz, void *val)
{
        phy_handle_t    *ph;
        int             err = 0;

        _NOTE(ARGUNUSED(name));

        if (sz < 1)
                return (EINVAL);

        mutex_enter(&mh->m_lock);

        ph = mh->m_phy;

#define CASE_PROP_ABILITY(PROP, VAR)                                    \
        case MAC_PROP_ADV_##PROP:                                       \
                *(uint8_t *)val = ph->phy_adv_##VAR;                    \
                break;                                                  \
                                                                        \
        case MAC_PROP_EN_##PROP:                                        \
                *(uint8_t *)val = ph->phy_en_##VAR;                     \
                break;

        switch (num) {
        case MAC_PROP_DUPLEX:
                ASSERT(sz >= sizeof (link_duplex_t));
                bcopy(&ph->phy_duplex, val, sizeof (link_duplex_t));
                break;

        case MAC_PROP_SPEED: {
                uint64_t speed = ph->phy_speed * 1000000ull;
                ASSERT(sz >= sizeof (uint64_t));
                bcopy(&speed, val, sizeof (speed));
                break;
        }

        case MAC_PROP_AUTONEG:
                *(uint8_t *)val = ph->phy_adv_aneg;
                break;

        case MAC_PROP_FLOWCTRL:
                ASSERT(sz >= sizeof (link_flowctrl_t));
                bcopy(&ph->phy_flowctrl, val, sizeof (link_flowctrl_t));
                break;

        CASE_PROP_ABILITY(1000FDX_CAP, 1000_fdx)
        CASE_PROP_ABILITY(1000HDX_CAP, 1000_hdx)
        CASE_PROP_ABILITY(100T4_CAP, 100_t4)
        CASE_PROP_ABILITY(100FDX_CAP, 100_fdx)
        CASE_PROP_ABILITY(100HDX_CAP, 100_hdx)
        CASE_PROP_ABILITY(10FDX_CAP, 10_fdx)
        CASE_PROP_ABILITY(10HDX_CAP, 10_hdx)

        default:
                err = ENOTSUP;
                break;
        }

        mutex_exit(&mh->m_lock);

        return (err);
}

void
mii_m_propinfo(mii_handle_t mh, const char *name, mac_prop_id_t num,
    mac_prop_info_handle_t prh)
{
        phy_handle_t    *ph;

        _NOTE(ARGUNUSED(name));

        mutex_enter(&mh->m_lock);

        ph = mh->m_phy;

        switch (num) {
        case MAC_PROP_DUPLEX:
        case MAC_PROP_SPEED:
                mac_prop_info_set_perm(prh, MAC_PROP_PERM_READ);
                break;

        case MAC_PROP_AUTONEG:
                mac_prop_info_set_default_uint8(prh, ph->phy_cap_aneg);
                break;

#define CASE_PROP_PERM(PROP, VAR)                                       \
        case MAC_PROP_ADV_##PROP:                                       \
                mac_prop_info_set_perm(prh, MAC_PROP_PERM_READ);        \
                mac_prop_info_set_default_uint8(prh, ph->phy_cap_##VAR); \
                break;                                                  \
                                                                        \
        case MAC_PROP_EN_##PROP:                                        \
                if (!ph->phy_cap_##VAR)                                 \
                        mac_prop_info_set_perm(prh, MAC_PROP_PERM_READ); \
                mac_prop_info_set_default_uint8(prh, ph->phy_cap_##VAR); \
                break;

        CASE_PROP_PERM(1000FDX_CAP, 1000_fdx)
        CASE_PROP_PERM(1000HDX_CAP, 1000_hdx)
        CASE_PROP_PERM(100T4_CAP, 100_t4)
        CASE_PROP_PERM(100FDX_CAP, 100_fdx)
        CASE_PROP_PERM(100HDX_CAP, 100_hdx)
        CASE_PROP_PERM(10FDX_CAP, 10_fdx)
        CASE_PROP_PERM(10HDX_CAP, 10_hdx)
        }

        mutex_exit(&mh->m_lock);
}

int
mii_m_setprop(mii_handle_t mh, const char *name, mac_prop_id_t num,
    uint_t sz, const void *valp)
{
        phy_handle_t    *ph;
        boolean_t       *advp = NULL;
        boolean_t       *capp = NULL;
        int             *macpp = NULL;
        int             rv = ENOTSUP;

        _NOTE(ARGUNUSED(name));

        if (sz < 1)
                return (EINVAL);

        mutex_enter(&mh->m_lock);

        ph = mh->m_phy;

        /* we don't support changing parameters while in loopback mode */
        if (ph->phy_loopback != PHY_LB_NONE) {
                switch (num) {
                case MAC_PROP_EN_1000FDX_CAP:
                case MAC_PROP_EN_1000HDX_CAP:
                case MAC_PROP_EN_100FDX_CAP:
                case MAC_PROP_EN_100HDX_CAP:
                case MAC_PROP_EN_100T4_CAP:
                case MAC_PROP_EN_10FDX_CAP:
                case MAC_PROP_EN_10HDX_CAP:
                case MAC_PROP_AUTONEG:
                case MAC_PROP_FLOWCTRL:
                        return (EBUSY);
                }
        }

        switch (num) {
        case MAC_PROP_EN_1000FDX_CAP:
                capp = &ph->phy_cap_1000_fdx;
                advp = &ph->phy_en_1000_fdx;
                macpp = &mh->m_en_1000_fdx;
                break;
        case MAC_PROP_EN_1000HDX_CAP:
                capp = &ph->phy_cap_1000_hdx;
                advp = &ph->phy_en_1000_hdx;
                macpp = &mh->m_en_1000_hdx;
                break;
        case MAC_PROP_EN_100FDX_CAP:
                capp = &ph->phy_cap_100_fdx;
                advp = &ph->phy_en_100_fdx;
                macpp = &mh->m_en_100_fdx;
                break;
        case MAC_PROP_EN_100HDX_CAP:
                capp = &ph->phy_cap_100_hdx;
                advp = &ph->phy_en_100_hdx;
                macpp = &mh->m_en_100_hdx;
                break;
        case MAC_PROP_EN_100T4_CAP:
                capp = &ph->phy_cap_100_t4;
                advp = &ph->phy_en_100_t4;
                macpp = &mh->m_en_100_t4;
                break;
        case MAC_PROP_EN_10FDX_CAP:
                capp = &ph->phy_cap_10_fdx;
                advp = &ph->phy_en_10_fdx;
                macpp = &mh->m_en_10_fdx;
                break;
        case MAC_PROP_EN_10HDX_CAP:
                capp = &ph->phy_cap_10_hdx;
                advp = &ph->phy_en_10_hdx;
                macpp = &mh->m_en_10_hdx;
                break;
        case MAC_PROP_AUTONEG:
                capp = &ph->phy_cap_aneg;
                advp = &ph->phy_en_aneg;
                macpp = &mh->m_en_aneg;
                break;
        case MAC_PROP_FLOWCTRL: {
                link_flowctrl_t fc;
                boolean_t chg;

                ASSERT(sz >= sizeof (link_flowctrl_t));
                bcopy(valp, &fc, sizeof (fc));

                chg = fc == ph->phy_en_flowctrl ? B_FALSE : B_TRUE;
                switch (fc) {
                case LINK_FLOWCTRL_NONE:
                        ph->phy_en_pause = B_FALSE;
                        ph->phy_en_asmpause = B_FALSE;
                        ph->phy_en_flowctrl = fc;
                        break;
                /*
                 * Note that while we don't have a way to advertise
                 * that we can RX pause (we just won't send pause
                 * frames), we advertise full support.  The MAC driver
                 * will learn of the configuration via the saved value
                 * of the tunable.
                 */
                case LINK_FLOWCTRL_BI:
                case LINK_FLOWCTRL_RX:
                        if (ph->phy_cap_pause) {
                                ph->phy_en_pause = B_TRUE;
                                ph->phy_en_asmpause = B_TRUE;
                                ph->phy_en_flowctrl = fc;
                        } else {
                                rv = EINVAL;
                        }
                        break;

                /*
                 * Tell the other side that we can assert pause, but
                 * we cannot resend.
                 */
                case LINK_FLOWCTRL_TX:
                        if (ph->phy_cap_asmpause) {
                                ph->phy_en_pause = B_FALSE;
                                ph->phy_en_flowctrl = fc;
                                ph->phy_en_asmpause = B_TRUE;
                        } else {
                                rv = EINVAL;
                        }
                        break;
                default:
                        rv = EINVAL;
                        break;
                }
                if ((rv == 0) && chg) {
                        mh->m_en_flowctrl = fc;
                        mh->m_tstate = MII_STATE_RESET;
                        cv_broadcast(&mh->m_cv);
                }
                break;
        }

        default:
                rv = ENOTSUP;
                break;
        }

        if (capp && advp && macpp) {
                if (sz < sizeof (uint8_t)) {
                        rv = EINVAL;

                } else if (*capp) {
                        if (*advp != *(uint8_t *)valp) {
                                *advp = *(uint8_t *)valp;
                                *macpp = *(uint8_t *)valp;
                                mh->m_tstate = MII_STATE_RESET;
                                cv_broadcast(&mh->m_cv);
                        }
                        rv = 0;
                }
        }

        mutex_exit(&mh->m_lock);
        return (rv);
}

int
mii_m_getstat(mii_handle_t mh, uint_t stat, uint64_t *val)
{
        phy_handle_t    *ph;
        int             rv = 0;

        mutex_enter(&mh->m_lock);

        ph = mh->m_phy;

        switch (stat) {
        case MAC_STAT_IFSPEED:
                *val = ph->phy_speed * 1000000ull;
                break;
        case ETHER_STAT_LINK_DUPLEX:
                *val = ph->phy_duplex;
                break;
        case ETHER_STAT_LINK_AUTONEG:
                *val = !!(ph->phy_adv_aneg && ph->phy_lp_aneg);
                break;
        case ETHER_STAT_XCVR_ID:
                *val = ph->phy_id;
                break;
        case ETHER_STAT_XCVR_INUSE:
                *val = ph->phy_type;
                break;
        case ETHER_STAT_XCVR_ADDR:
                *val = ph->phy_addr;
                break;
        case ETHER_STAT_LINK_ASMPAUSE:
                *val = ph->phy_adv_asmpause && ph->phy_lp_asmpause &&
                    ph->phy_adv_pause != ph->phy_lp_pause;
                break;
        case ETHER_STAT_LINK_PAUSE:
                *val = (ph->phy_flowctrl == LINK_FLOWCTRL_BI) ||
                    (ph->phy_flowctrl == LINK_FLOWCTRL_RX);
                break;
        case ETHER_STAT_CAP_1000FDX:
                *val = ph->phy_cap_1000_fdx;
                break;
        case ETHER_STAT_CAP_1000HDX:
                *val = ph->phy_cap_1000_hdx;
                break;
        case ETHER_STAT_CAP_100FDX:
                *val = ph->phy_cap_100_fdx;
                break;
        case ETHER_STAT_CAP_100HDX:
                *val = ph->phy_cap_100_hdx;
                break;
        case ETHER_STAT_CAP_10FDX:
                *val = ph->phy_cap_10_fdx;
                break;
        case ETHER_STAT_CAP_10HDX:
                *val = ph->phy_cap_10_hdx;
                break;
        case ETHER_STAT_CAP_100T4:
                *val = ph->phy_cap_100_t4;
                break;
        case ETHER_STAT_CAP_AUTONEG:
                *val = ph->phy_cap_aneg;
                break;
        case ETHER_STAT_CAP_PAUSE:
                *val = ph->phy_cap_pause;
                break;
        case ETHER_STAT_CAP_ASMPAUSE:
                *val = ph->phy_cap_asmpause;
                break;

        case ETHER_STAT_LP_CAP_1000FDX:
                *val = ph->phy_lp_1000_fdx;
                break;
        case ETHER_STAT_LP_CAP_1000HDX:
                *val = ph->phy_lp_1000_hdx;
                break;
        case ETHER_STAT_LP_CAP_100FDX:
                *val = ph->phy_lp_100_fdx;
                break;
        case ETHER_STAT_LP_CAP_100HDX:
                *val = ph->phy_lp_100_hdx;
                break;
        case ETHER_STAT_LP_CAP_10FDX:
                *val = ph->phy_lp_10_fdx;
                break;
        case ETHER_STAT_LP_CAP_10HDX:
                *val = ph->phy_lp_10_hdx;
                break;
        case ETHER_STAT_LP_CAP_100T4:
                *val = ph->phy_lp_100_t4;
                break;
        case ETHER_STAT_LP_CAP_AUTONEG:
                *val = ph->phy_lp_aneg;
                break;
        case ETHER_STAT_LP_CAP_PAUSE:
                *val = ph->phy_lp_pause;
                break;
        case ETHER_STAT_LP_CAP_ASMPAUSE:
                *val = ph->phy_lp_asmpause;
                break;

        case ETHER_STAT_ADV_CAP_1000FDX:
                *val = ph->phy_adv_1000_fdx;
                break;
        case ETHER_STAT_ADV_CAP_1000HDX:
                *val = ph->phy_adv_1000_hdx;
                break;
        case ETHER_STAT_ADV_CAP_100FDX:
                *val = ph->phy_adv_100_fdx;
                break;
        case ETHER_STAT_ADV_CAP_100HDX:
                *val = ph->phy_adv_100_hdx;
                break;
        case ETHER_STAT_ADV_CAP_10FDX:
                *val = ph->phy_adv_10_fdx;
                break;
        case ETHER_STAT_ADV_CAP_10HDX:
                *val = ph->phy_adv_10_hdx;
                break;
        case ETHER_STAT_ADV_CAP_100T4:
                *val = ph->phy_adv_100_t4;
                break;
        case ETHER_STAT_ADV_CAP_AUTONEG:
                *val = ph->phy_adv_aneg;
                break;
        case ETHER_STAT_ADV_CAP_PAUSE:
                *val = ph->phy_adv_pause;
                break;
        case ETHER_STAT_ADV_CAP_ASMPAUSE:
                *val = ph->phy_adv_asmpause;
                break;

        default:
                rv = ENOTSUP;
                break;
        }
        mutex_exit(&mh->m_lock);

        return (rv);
}

/*
 * PHY support routines.  Private to the MII module and the vendor
 * specific PHY implementation code.
 */
uint16_t
phy_read(phy_handle_t *ph, uint8_t reg)
{
        mii_handle_t    mh = ph->phy_mii;

        return ((*mh->m_ops.mii_read)(mh->m_private, ph->phy_addr, reg));
}

void
phy_write(phy_handle_t *ph, uint8_t reg, uint16_t val)
{
        mii_handle_t    mh = ph->phy_mii;

        (*mh->m_ops.mii_write)(mh->m_private, ph->phy_addr, reg, val);
}

int
phy_reset(phy_handle_t *ph)
{
        ASSERT(mutex_owned(&ph->phy_mii->m_lock));

        /*
         * For our device, make sure its powered up and unisolated.
         */
        PHY_CLR(ph, MII_CONTROL,
            MII_CONTROL_PWRDN | MII_CONTROL_ISOLATE);

        /*
         * Finally reset it.
         */
        PHY_SET(ph, MII_CONTROL, MII_CONTROL_RESET);

        /*
         * Apparently some devices (DP83840A) like to have a little
         * bit of a wait before we start accessing anything else on
         * the PHY.
         */
        drv_usecwait(500);

        /*
         * Wait for reset to complete - probably very fast, but no
         * more than 0.5 sec according to spec.  It would be nice if
         * we could use delay() here, but MAC drivers may call
         * functions which hold this lock in interrupt context, so
         * sleeping would be a definite no-no.  The good news here is
         * that it seems to be the case that most devices come back
         * within only a few hundred usec.
         */
        for (int i = 500000; i; i -= 100) {
                if ((phy_read(ph, MII_CONTROL) & MII_CONTROL_RESET) == 0) {
                        /* reset completed */
                        return (DDI_SUCCESS);
                }
                drv_usecwait(100);
        }

        return (DDI_FAILURE);
}

int
phy_stop(phy_handle_t *ph)
{
        phy_write(ph, MII_CONTROL, MII_CONTROL_ISOLATE);

        return (DDI_SUCCESS);
}

int
phy_loop(phy_handle_t *ph)
{
        uint16_t        bmcr, gtcr;

        ASSERT(mutex_owned(&ph->phy_mii->m_lock));

        /*
         * Disable everything to start... we'll add in modes as we go.
         */
        ph->phy_adv_aneg = B_FALSE;
        ph->phy_adv_1000_fdx = B_FALSE;
        ph->phy_adv_1000_hdx = B_FALSE;
        ph->phy_adv_100_fdx = B_FALSE;
        ph->phy_adv_100_t4 = B_FALSE;
        ph->phy_adv_100_hdx = B_FALSE;
        ph->phy_adv_10_fdx = B_FALSE;
        ph->phy_adv_10_hdx = B_FALSE;
        ph->phy_adv_pause = B_FALSE;
        ph->phy_adv_asmpause = B_FALSE;

        bmcr = 0;
        gtcr = MII_MSCONTROL_MANUAL | MII_MSCONTROL_MASTER;

        switch (ph->phy_loopback) {
        case PHY_LB_NONE:
                /* We shouldn't be here */
                ASSERT(0);
                break;

        case PHY_LB_INT_PHY:
                bmcr |= MII_CONTROL_LOOPBACK;
                ph->phy_duplex = LINK_DUPLEX_FULL;
                if (ph->phy_cap_1000_fdx) {
                        bmcr |= MII_CONTROL_1GB | MII_CONTROL_FDUPLEX;
                        ph->phy_speed = 1000;
                } else if (ph->phy_cap_100_fdx) {
                        bmcr |= MII_CONTROL_100MB | MII_CONTROL_FDUPLEX;
                        ph->phy_speed = 100;
                } else if (ph->phy_cap_10_fdx) {
                        bmcr |= MII_CONTROL_FDUPLEX;
                        ph->phy_speed = 10;
                }
                break;

        case PHY_LB_EXT_10:
                bmcr = MII_CONTROL_FDUPLEX;
                ph->phy_speed = 10;
                ph->phy_duplex = LINK_DUPLEX_FULL;
                break;

        case PHY_LB_EXT_100:
                bmcr = MII_CONTROL_100MB | MII_CONTROL_FDUPLEX;
                ph->phy_speed = 100;
                ph->phy_duplex = LINK_DUPLEX_FULL;
                break;

        case PHY_LB_EXT_1000:
                bmcr = MII_CONTROL_1GB | MII_CONTROL_FDUPLEX;
                ph->phy_speed = 1000;
                ph->phy_duplex = LINK_DUPLEX_FULL;
                break;
        }

        ph->phy_link = LINK_STATE_UP;   /* force up for loopback */
        ph->phy_flowctrl = LINK_FLOWCTRL_NONE;

        switch (ph->phy_type) {
        case XCVR_1000T:
        case XCVR_1000X:
        case XCVR_100T2:
                phy_write(ph, MII_MSCONTROL, gtcr);
                break;
        }

        phy_write(ph, MII_CONTROL, bmcr);

        return (DDI_SUCCESS);
}

int
phy_start(phy_handle_t *ph)
{
        uint16_t        bmcr, anar, gtcr;
        ASSERT(mutex_owned(&ph->phy_mii->m_lock));

        ASSERT(ph->phy_loopback == PHY_LB_NONE);

        /*
         * No loopback overrides, so try to advertise everything
         * that is administratively enabled.
         */
        ph->phy_adv_aneg = ph->phy_en_aneg;
        ph->phy_adv_1000_fdx = ph->phy_en_1000_fdx;
        ph->phy_adv_1000_hdx = ph->phy_en_1000_hdx;
        ph->phy_adv_100_fdx = ph->phy_en_100_fdx;
        ph->phy_adv_100_t4 = ph->phy_en_100_t4;
        ph->phy_adv_100_hdx = ph->phy_en_100_hdx;
        ph->phy_adv_10_fdx = ph->phy_en_10_fdx;
        ph->phy_adv_10_hdx = ph->phy_en_10_hdx;
        ph->phy_adv_pause = ph->phy_en_pause;
        ph->phy_adv_asmpause = ph->phy_en_asmpause;

        /*
         * Limit properties to what the hardware can actually support.
         */
#define FILTER_ADV(CAP)         \
        if (!ph->phy_cap_##CAP) \
            ph->phy_adv_##CAP = 0

        FILTER_ADV(aneg);
        FILTER_ADV(1000_fdx);
        FILTER_ADV(1000_hdx);
        FILTER_ADV(100_fdx);
        FILTER_ADV(100_t4);
        FILTER_ADV(100_hdx);
        FILTER_ADV(10_fdx);
        FILTER_ADV(10_hdx);
        FILTER_ADV(pause);
        FILTER_ADV(asmpause);

#undef  FILTER_ADV

        /*
         * We need at least one valid mode.
         */
        if ((!ph->phy_adv_1000_fdx) &&
            (!ph->phy_adv_1000_hdx) &&
            (!ph->phy_adv_100_t4) &&
            (!ph->phy_adv_100_fdx) &&
            (!ph->phy_adv_100_hdx) &&
            (!ph->phy_adv_10_fdx) &&
            (!ph->phy_adv_10_hdx)) {

                phy_warn(ph,
                    "No valid link mode selected.  Powering down PHY.");

                PHY_SET(ph, MII_CONTROL, MII_CONTROL_PWRDN);

                ph->phy_link = LINK_STATE_DOWN;
                return (DDI_SUCCESS);
        }

        bmcr = 0;
        gtcr = 0;

        if (ph->phy_adv_aneg) {
                bmcr |= MII_CONTROL_ANE | MII_CONTROL_RSAN;
        }

        if ((ph->phy_adv_1000_fdx) || (ph->phy_adv_1000_hdx)) {
                bmcr |= MII_CONTROL_1GB;

        } else if (ph->phy_adv_100_fdx || ph->phy_adv_100_hdx ||
            ph->phy_adv_100_t4) {
                bmcr |= MII_CONTROL_100MB;
        }

        if (ph->phy_adv_1000_fdx || ph->phy_adv_100_fdx || ph->phy_adv_10_fdx) {
                bmcr |= MII_CONTROL_FDUPLEX;
        }

        if (ph->phy_type == XCVR_1000X) {
                /* 1000BASE-X (usually fiber) */
                anar = 0;
                if (ph->phy_adv_1000_fdx) {
                        anar |= MII_ABILITY_X_FD;
                }
                if (ph->phy_adv_1000_hdx) {
                        anar |= MII_ABILITY_X_HD;
                }
                if (ph->phy_adv_pause) {
                        anar |= MII_ABILITY_X_PAUSE;
                }
                if (ph->phy_adv_asmpause) {
                        anar |= MII_ABILITY_X_ASMPAUSE;
                }

        } else if (ph->phy_type == XCVR_100T2) {
                /* 100BASE-T2 */
                anar = 0;
                if (ph->phy_adv_100_fdx) {
                        anar |= MII_ABILITY_T2_FD;
                }
                if (ph->phy_adv_100_hdx) {
                        anar |= MII_ABILITY_T2_HD;
                }

        } else {
                anar = MII_AN_SELECTOR_8023;

                /* 1000BASE-T or 100BASE-X probably  */
                if (ph->phy_adv_1000_fdx) {
                        gtcr |= MII_MSCONTROL_1000T_FD;
                }
                if (ph->phy_adv_1000_hdx) {
                        gtcr |= MII_MSCONTROL_1000T;
                }
                if (ph->phy_adv_100_fdx) {
                        anar |= MII_ABILITY_100BASE_TX_FD;
                }
                if (ph->phy_adv_100_hdx) {
                        anar |= MII_ABILITY_100BASE_TX;
                }
                if (ph->phy_adv_100_t4) {
                        anar |= MII_ABILITY_100BASE_T4;
                }
                if (ph->phy_adv_10_fdx) {
                        anar |= MII_ABILITY_10BASE_T_FD;
                }
                if (ph->phy_adv_10_hdx) {
                        anar |= MII_ABILITY_10BASE_T;
                }
                if (ph->phy_adv_pause) {
                        anar |= MII_ABILITY_PAUSE;
                }
                if (ph->phy_adv_asmpause) {
                        anar |= MII_ABILITY_ASMPAUSE;
                }
        }

        ph->phy_link = LINK_STATE_DOWN;
        ph->phy_duplex = LINK_DUPLEX_UNKNOWN;
        ph->phy_speed = 0;

        phy_write(ph, MII_AN_ADVERT, anar);
        phy_write(ph, MII_CONTROL, bmcr & ~(MII_CONTROL_RSAN));

        switch (ph->phy_type) {
        case XCVR_1000T:
        case XCVR_1000X:
        case XCVR_100T2:
                phy_write(ph, MII_MSCONTROL, gtcr);
        }

        /*
         * Finally, this will start up autoneg if it is enabled, or
         * force link settings otherwise.
         */
        phy_write(ph, MII_CONTROL, bmcr);

        return (DDI_SUCCESS);
}


int
phy_check(phy_handle_t *ph)
{
        uint16_t control, status, lpar, msstat, anexp;
        int debounces = 100;

        ASSERT(mutex_owned(&ph->phy_mii->m_lock));

debounce:
        status = phy_read(ph, MII_STATUS);
        control = phy_read(ph, MII_CONTROL);

        if (status & MII_STATUS_EXTENDED) {
                lpar = phy_read(ph, MII_AN_LPABLE);
                anexp = phy_read(ph, MII_AN_EXPANSION);
        } else {
                lpar = 0;
                anexp = 0;
        }

        /*
         * We reread to clear any latched bits.  This also debounces
         * any state that might be in transition.
         */
        drv_usecwait(10);
        if ((status != phy_read(ph, MII_STATUS)) && debounces) {
                debounces--;
                goto debounce;
        }

        /*
         * Detect the situation where the PHY is removed or has died.
         * According to spec, at least one bit of status must be set,
         * and at least one bit must be clear.
         */
        if ((status == 0xffff) || (status == 0)) {
                ph->phy_speed = 0;
                ph->phy_duplex = LINK_DUPLEX_UNKNOWN;
                ph->phy_link = LINK_STATE_UNKNOWN;
                ph->phy_present = B_FALSE;
                return (DDI_FAILURE);
        }

        /* We only respect the link flag if we are not in loopback. */
        if ((ph->phy_loopback != PHY_LB_INT_PHY) &&
            ((status & MII_STATUS_LINKUP) == 0)) {
                ph->phy_speed = 0;
                ph->phy_duplex = LINK_DUPLEX_UNKNOWN;
                ph->phy_link = LINK_STATE_DOWN;
                return (DDI_SUCCESS);
        }

        ph->phy_link = LINK_STATE_UP;

        if ((control & MII_CONTROL_ANE) == 0) {

                ph->phy_lp_aneg = B_FALSE;
                ph->phy_lp_10_hdx = B_FALSE;
                ph->phy_lp_10_fdx = B_FALSE;
                ph->phy_lp_100_t4 = B_FALSE;
                ph->phy_lp_100_hdx = B_FALSE;
                ph->phy_lp_100_fdx = B_FALSE;
                ph->phy_lp_1000_hdx = B_FALSE;
                ph->phy_lp_1000_fdx = B_FALSE;

                /*
                 * We have no idea what our link partner might or might
                 * not be able to support, except that it appears to
                 * support the same mode that we have forced.
                 */
                if (control & MII_CONTROL_1GB) {
                        ph->phy_speed = 1000;
                } else if (control & MII_CONTROL_100MB) {
                        ph->phy_speed = 100;
                } else {
                        ph->phy_speed = 10;
                }
                ph->phy_duplex = control & MII_CONTROL_FDUPLEX ?
                    LINK_DUPLEX_FULL : LINK_DUPLEX_HALF;

                return (DDI_SUCCESS);
        }

        if (ph->phy_type == XCVR_1000X) {

                ph->phy_lp_10_hdx = B_FALSE;
                ph->phy_lp_10_fdx = B_FALSE;
                ph->phy_lp_100_t4 = B_FALSE;
                ph->phy_lp_100_hdx = B_FALSE;
                ph->phy_lp_100_fdx = B_FALSE;

                /* 1000BASE-X requires autonegotiation */
                ph->phy_lp_aneg = B_TRUE;
                ph->phy_lp_1000_fdx = !!(lpar & MII_ABILITY_X_FD);
                ph->phy_lp_1000_hdx = !!(lpar & MII_ABILITY_X_HD);
                ph->phy_lp_pause = !!(lpar & MII_ABILITY_X_PAUSE);
                ph->phy_lp_asmpause = !!(lpar & MII_ABILITY_X_ASMPAUSE);

        } else if (ph->phy_type == XCVR_100T2) {
                ph->phy_lp_10_hdx = B_FALSE;
                ph->phy_lp_10_fdx = B_FALSE;
                ph->phy_lp_100_t4 = B_FALSE;
                ph->phy_lp_1000_hdx = B_FALSE;
                ph->phy_lp_1000_fdx = B_FALSE;
                ph->phy_lp_pause = B_FALSE;
                ph->phy_lp_asmpause = B_FALSE;

                /* 100BASE-T2 requires autonegotiation */
                ph->phy_lp_aneg = B_TRUE;
                ph->phy_lp_100_fdx = !!(lpar & MII_ABILITY_T2_FD);
                ph->phy_lp_100_hdx = !!(lpar & MII_ABILITY_T2_HD);

        } else if (anexp & MII_AN_EXP_PARFAULT) {
                /*
                 * Parallel detection fault!  This happens when the
                 * peer does not use autonegotiation, and the
                 * detection logic reports more than one type of legal
                 * link is available.  Note that parallel detection
                 * can only happen with half duplex 10, 100, and
                 * 100TX4.  We also should not have got here, because
                 * the link state bit should have failed.
                 */
#ifdef  DEBUG
                phy_warn(ph, "Parallel detection fault!");
#endif
                ph->phy_lp_10_hdx = B_FALSE;
                ph->phy_lp_10_fdx = B_FALSE;
                ph->phy_lp_100_t4 = B_FALSE;
                ph->phy_lp_100_hdx = B_FALSE;
                ph->phy_lp_100_fdx = B_FALSE;
                ph->phy_lp_1000_hdx = B_FALSE;
                ph->phy_lp_1000_fdx = B_FALSE;
                ph->phy_lp_pause = B_FALSE;
                ph->phy_lp_asmpause = B_FALSE;
                ph->phy_speed = 0;
                ph->phy_duplex = LINK_DUPLEX_UNKNOWN;
                return (DDI_SUCCESS);

        } else {
                ph->phy_lp_aneg = !!(anexp & MII_AN_EXP_LPCANAN);

                /*
                 * Note: If the peer doesn't support autonegotiation, then
                 * according to clause 28.5.4.5, the link partner ability
                 * register will still have the right bits set.  However,
                 * gigabit modes cannot use legacy parallel detection.
                 */

                if ((ph->phy_type == XCVR_1000T) &&
                    (anexp & MII_AN_EXP_LPCANAN)) {

                        /* check for gige */
                        msstat = phy_read(ph, MII_MSSTATUS);

                        ph->phy_lp_1000_hdx =
                            !!(msstat & MII_MSSTATUS_LP1000T);

                        ph->phy_lp_1000_fdx =
                            !!(msstat & MII_MSSTATUS_LP1000T_FD);
                }

                ph->phy_lp_100_fdx = !!(lpar & MII_ABILITY_100BASE_TX_FD);
                ph->phy_lp_100_hdx = !!(lpar & MII_ABILITY_100BASE_TX);
                ph->phy_lp_100_t4 = !!(lpar & MII_ABILITY_100BASE_T4);
                ph->phy_lp_10_fdx = !!(lpar & MII_ABILITY_10BASE_T_FD);
                ph->phy_lp_10_hdx = !!(lpar & MII_ABILITY_10BASE_T);
                ph->phy_lp_pause = !!(lpar & MII_ABILITY_PAUSE);
                ph->phy_lp_asmpause = !!(lpar & MII_ABILITY_ASMPAUSE);
        }

        /* resolve link pause */
        if ((ph->phy_en_flowctrl == LINK_FLOWCTRL_BI) &&
            (ph->phy_lp_pause)) {
                ph->phy_flowctrl = LINK_FLOWCTRL_BI;
        } else if ((ph->phy_en_flowctrl == LINK_FLOWCTRL_RX) &&
            (ph->phy_lp_pause || ph->phy_lp_asmpause)) {
                ph->phy_flowctrl = LINK_FLOWCTRL_RX;
        } else if ((ph->phy_en_flowctrl == LINK_FLOWCTRL_TX) &&
            (ph->phy_lp_pause)) {
                ph->phy_flowctrl = LINK_FLOWCTRL_TX;
        } else {
                ph->phy_flowctrl = LINK_FLOWCTRL_NONE;
        }

        if (ph->phy_adv_1000_fdx && ph->phy_lp_1000_fdx) {
                ph->phy_speed = 1000;
                ph->phy_duplex = LINK_DUPLEX_FULL;

        } else if (ph->phy_adv_1000_hdx && ph->phy_lp_1000_hdx) {
                ph->phy_speed = 1000;
                ph->phy_duplex = LINK_DUPLEX_HALF;

        } else if (ph->phy_adv_100_fdx && ph->phy_lp_100_fdx) {
                ph->phy_speed = 100;
                ph->phy_duplex = LINK_DUPLEX_FULL;

        } else if (ph->phy_adv_100_t4 && ph->phy_lp_100_t4) {
                ph->phy_speed = 100;
                ph->phy_duplex = LINK_DUPLEX_HALF;

        } else if (ph->phy_adv_100_hdx && ph->phy_lp_100_hdx) {
                ph->phy_speed = 100;
                ph->phy_duplex = LINK_DUPLEX_HALF;

        } else if (ph->phy_adv_10_fdx && ph->phy_lp_10_fdx) {
                ph->phy_speed = 10;
                ph->phy_duplex = LINK_DUPLEX_FULL;

        } else if (ph->phy_adv_10_hdx && ph->phy_lp_10_hdx) {
                ph->phy_speed = 10;
                ph->phy_duplex = LINK_DUPLEX_HALF;

        } else {
#ifdef  DEBUG
                phy_warn(ph, "No common abilities.");
#endif
                ph->phy_speed = 0;
                ph->phy_duplex = LINK_DUPLEX_UNKNOWN;
        }

        return (DDI_SUCCESS);
}

int
phy_get_prop(phy_handle_t *ph, char *prop, int dflt)
{
        mii_handle_t    mh = ph->phy_mii;

        return (ddi_prop_get_int(DDI_DEV_T_ANY, mh->m_dip, 0, prop, dflt));
}

const char *
phy_get_name(phy_handle_t *ph)
{
        mii_handle_t    mh = ph->phy_mii;

        return (mh->m_name);
}

const char *
phy_get_driver(phy_handle_t *ph)
{
        mii_handle_t    mh = ph->phy_mii;

        return (ddi_driver_name(mh->m_dip));
}

void
phy_warn(phy_handle_t *ph, const char *fmt, ...)
{
        va_list va;
        char buf[256];

        (void) snprintf(buf, sizeof (buf), "%s: %s", phy_get_name(ph), fmt);

        va_start(va, fmt);
        vcmn_err(CE_WARN, buf, va);
        va_end(va);
}

/*
 * Internal support routines.
 */

void
_mii_notify(mii_handle_t mh)
{
        if (mh->m_ops.mii_notify != NULL) {
                mh->m_ops.mii_notify(mh->m_private, mh->m_link);
        }
}

void
_mii_probe_phy(phy_handle_t *ph)
{
        uint16_t        bmsr;
        uint16_t        extsr;
        mii_handle_t    mh = ph->phy_mii;


        /*
         * Apparently, PHY 0 is less likely to be physically
         * connected, and should always be the last one tried.  Most
         * single solution NICs use PHY1 for their built-in
         * transceiver.  NICs with an external MII will often place
         * the external PHY at address 1, and use address 0 for the
         * internal PHY.
         */

        ph->phy_id = 0;
        ph->phy_model = "PHY";
        ph->phy_vendor = "Unknown Vendor";

        /* done twice to clear any latched bits */
        bmsr = phy_read(ph, MII_STATUS);
        bmsr = phy_read(ph, MII_STATUS);
        if ((bmsr == 0) || (bmsr == 0xffff)) {
                ph->phy_present = B_FALSE;
                return;
        }

        if (bmsr & MII_STATUS_EXTSTAT) {
                extsr = phy_read(ph, MII_EXTSTATUS);
        } else {
                extsr = 0;
        }

        ph->phy_present = B_TRUE;
        ph->phy_id = ((uint32_t)phy_read(ph, MII_PHYIDH) << 16) |
            phy_read(ph, MII_PHYIDL);

        /* setup default handlers */
        ph->phy_reset = phy_reset;
        ph->phy_start = phy_start;
        ph->phy_stop = phy_stop;
        ph->phy_check = phy_check;
        ph->phy_loop = phy_loop;

        /*
         * We ignore the non-existent 100baseT2 stuff -- no
         * known products for it exist.
         */
        ph->phy_cap_aneg =      !!(bmsr & MII_STATUS_CANAUTONEG);
        ph->phy_cap_100_t4 =    !!(bmsr & MII_STATUS_100_BASE_T4);
        ph->phy_cap_100_fdx =   !!(bmsr & MII_STATUS_100_BASEX_FD);
        ph->phy_cap_100_hdx =   !!(bmsr & MII_STATUS_100_BASEX);
        ph->phy_cap_10_fdx =    !!(bmsr & MII_STATUS_10_FD);
        ph->phy_cap_10_hdx =    !!(bmsr & MII_STATUS_10);
        ph->phy_cap_1000_fdx =
            !!(extsr & (MII_EXTSTATUS_1000X_FD|MII_EXTSTATUS_1000T_FD));
        ph->phy_cap_1000_hdx =
            !!(extsr & (MII_EXTSTATUS_1000X | MII_EXTSTATUS_1000T));
        ph->phy_cap_pause =     mh->m_cap_pause;
        ph->phy_cap_asmpause =  mh->m_cap_asmpause;

        if (bmsr & MII_STATUS_10) {
                ph->phy_cap_10_hdx = B_TRUE;
                ph->phy_type = XCVR_10;
        }
        if (bmsr & MII_STATUS_10_FD) {
                ph->phy_cap_10_fdx = B_TRUE;
                ph->phy_type = XCVR_10;
        }
        if (bmsr & MII_STATUS_100T2) {
                ph->phy_cap_100_hdx = B_TRUE;
                ph->phy_type = XCVR_100T2;
        }
        if (bmsr & MII_STATUS_100T2_FD) {
                ph->phy_cap_100_fdx = B_TRUE;
                ph->phy_type = XCVR_100T2;
        }
        if (bmsr & MII_STATUS_100_BASE_T4) {
                ph->phy_cap_100_hdx = B_TRUE;
                ph->phy_type = XCVR_100T4;
        }
        if (bmsr & MII_STATUS_100_BASEX) {
                ph->phy_cap_100_hdx = B_TRUE;
                ph->phy_type = XCVR_100X;
        }
        if (bmsr & MII_STATUS_100_BASEX_FD) {
                ph->phy_cap_100_fdx = B_TRUE;
                ph->phy_type = XCVR_100X;
        }
        if (extsr & MII_EXTSTATUS_1000X) {
                ph->phy_cap_1000_hdx = B_TRUE;
                ph->phy_type = XCVR_1000X;
        }
        if (extsr & MII_EXTSTATUS_1000X_FD) {
                ph->phy_cap_1000_fdx = B_TRUE;
                ph->phy_type = XCVR_1000X;
        }
        if (extsr & MII_EXTSTATUS_1000T) {
                ph->phy_cap_1000_hdx = B_TRUE;
                ph->phy_type = XCVR_1000T;
        }
        if (extsr & MII_EXTSTATUS_1000T_FD) {
                ph->phy_cap_1000_fdx = B_TRUE;
                ph->phy_type = XCVR_1000T;
        }

        for (int j = 0; _phy_probes[j] != NULL; j++) {
                if ((*_phy_probes[j])(ph)) {
                        break;
                }
        }

#define INIT_ENABLE(CAP)        \
        ph->phy_en_##CAP = (mh->m_en_##CAP > 0) ? \
            mh->m_en_##CAP : ph->phy_cap_##CAP

        INIT_ENABLE(aneg);
        INIT_ENABLE(1000_fdx);
        INIT_ENABLE(1000_hdx);
        INIT_ENABLE(100_fdx);
        INIT_ENABLE(100_t4);
        INIT_ENABLE(100_hdx);
        INIT_ENABLE(10_fdx);
        INIT_ENABLE(10_hdx);

#undef  INIT_ENABLE
        ph->phy_en_flowctrl = mh->m_en_flowctrl;
        switch (ph->phy_en_flowctrl) {
        case LINK_FLOWCTRL_BI:
        case LINK_FLOWCTRL_RX:
                ph->phy_en_pause = B_TRUE;
                ph->phy_en_asmpause = B_TRUE;
                break;
        case LINK_FLOWCTRL_TX:
                ph->phy_en_pause = B_FALSE;
                ph->phy_en_asmpause = B_TRUE;
                break;
        default:
                ph->phy_en_pause = B_FALSE;
                ph->phy_en_asmpause = B_FALSE;
                break;
        }
}

void
_mii_probe(mii_handle_t mh)
{
        uint8_t         new_addr;
        uint8_t         old_addr;
        uint8_t         user_addr;
        uint8_t         curr_addr;
        phy_handle_t    *ph;
        int             pri = 0;
        int             first;

        user_addr = ddi_prop_get_int(DDI_DEV_T_ANY, mh->m_dip, 0,
            "phy-addr", -1);
        old_addr = mh->m_addr;
        new_addr = 0xff;

        /*
         * Apparently, PHY 0 is less likely to be physically
         * connected, and should always be the last one tried.  Most
         * single solution NICs use PHY1 for their built-in
         * transceiver.  NICs with an external MII will often place
         * the external PHY at address 1, and use address 0 for the
         * internal PHY.
         *
         * Some devices have a different preference however.  They can
         * override the default starting point of the search by
         * exporting a "first-phy" property.
         */

        first = ddi_prop_get_int(DDI_DEV_T_ANY, mh->m_dip, 0, "first-phy", 1);
        if ((first < 0) || (first > 31)) {
                first = 1;
        }
        for (int i = first; i < (first + 32); i++) {

                /*
                 * This is tricky: it lets us start searching at an
                 * arbitrary address instead of 0, dealing with the
                 * wrap-around at address 31 properly.
                 */
                curr_addr = i % 32;

                ph = &mh->m_phys[curr_addr];

                bzero(ph, sizeof (*ph));
                ph->phy_addr = curr_addr;
                ph->phy_mii = mh;

                _mii_probe_phy(ph);

                if (!ph->phy_present)
                        continue;

                if (curr_addr == user_addr) {
                        /*
                         * We always try to honor the user configured phy.
                         */
                        new_addr = curr_addr;
                        pri = 4;

                }

                /* two reads to clear latched bits */
                if ((phy_read(ph, MII_STATUS) & MII_STATUS_LINKUP) &&
                    (phy_read(ph, MII_STATUS) & MII_STATUS_LINKUP) &&
                    (pri < 3)) {
                        /*
                         * Link present is good.  We prefer this over
                         * a possibly disconnected link.
                         */
                        new_addr = curr_addr;
                        pri = 3;
                }
                if ((curr_addr == old_addr) && (pri < 2)) {
                        /*
                         * All else being equal, minimize change.
                         */
                        new_addr = curr_addr;
                        pri = 2;

                }
                if (pri < 1) {
                        /*
                         * But make sure we at least select a present PHY.
                         */
                        new_addr = curr_addr;
                        pri = 1;
                }
        }

        if (new_addr == 0xff) {
                mh->m_addr = -1;
                mh->m_phy = &mh->m_bogus_phy;
                _mii_error(mh, MII_ENOPHY);
        } else {
                mh->m_addr = new_addr;
                mh->m_phy = &mh->m_phys[new_addr];
                mh->m_tstate = MII_STATE_RESET;
                if (new_addr != old_addr) {
                        cmn_err(CE_CONT,
                            "?%s: Using %s Ethernet PHY at %d: %s %s\n",
                            mh->m_name, mii_xcvr_types[mh->m_phy->phy_type],
                            mh->m_addr, mh->m_phy->phy_vendor,
                            mh->m_phy->phy_model);
                        mh->m_link = LINK_STATE_UNKNOWN;
                }
        }
}

int
_mii_reset(mii_handle_t mh)
{
        phy_handle_t    *ph;
        boolean_t       notify;

        ASSERT(mutex_owned(&mh->m_lock));

        /*
         * Reset logic.  We want to isolate all the other
         * phys that are not in use.
         */
        for (int i = 0; i < 32; i++) {
                ph = &mh->m_phys[i];

                if (!ph->phy_present)
                        continue;

                /* Don't touch our own phy, yet. */
                if (ph == mh->m_phy)
                        continue;

                ph->phy_stop(ph);
        }

        ph = mh->m_phy;

        ASSERT(ph->phy_present);

        /* If we're resetting the PHY, then we want to notify loss of link */
        notify = (mh->m_link != LINK_STATE_DOWN);
        mh->m_link = LINK_STATE_DOWN;
        ph->phy_link = LINK_STATE_DOWN;
        ph->phy_speed = 0;
        ph->phy_duplex = LINK_DUPLEX_UNKNOWN;

        if (ph->phy_reset(ph) != DDI_SUCCESS) {
                _mii_error(mh, MII_ERESET);
                return (DDI_FAILURE);
        }

        /* Perform optional mac layer reset. */
        if (mh->m_ops.mii_reset != NULL) {
                mh->m_ops.mii_reset(mh->m_private);
        }

        /* Perform optional mac layer notification. */
        if (notify) {
                _mii_notify(mh);
        }
        return (DDI_SUCCESS);
}

int
_mii_loopback(mii_handle_t mh)
{
        phy_handle_t    *ph;

        ASSERT(mutex_owned(&mh->m_lock));

        ph = mh->m_phy;

        if (_mii_reset(mh) != DDI_SUCCESS) {
                return (DDI_FAILURE);
        }
        if (ph->phy_loopback == PHY_LB_NONE) {
                mh->m_tstate = MII_STATE_START;
                return (DDI_SUCCESS);
        }
        if (ph->phy_loop(ph) != DDI_SUCCESS) {
                _mii_error(mh, MII_ELOOP);
                return (DDI_FAILURE);
        }

        /* Just force loopback to link up. */
        mh->m_link = ph->phy_link = LINK_STATE_UP;
        _mii_notify(mh);

        return (DDI_SUCCESS);
}

int
_mii_start(mii_handle_t mh)
{
        phy_handle_t            *ph;

        ph = mh->m_phy;

        ASSERT(mutex_owned(&mh->m_lock));
        ASSERT(ph->phy_present);
        ASSERT(ph->phy_loopback == PHY_LB_NONE);

        if (ph->phy_start(ph) != DDI_SUCCESS) {
                _mii_error(mh, MII_ESTART);
                return (DDI_FAILURE);
        }
        /* clear the error state since we got a good startup! */
        mh->m_error = MII_EOK;
        return (DDI_SUCCESS);
}

int
_mii_check(mii_handle_t mh)
{
        link_state_t    olink;
        int             ospeed;
        link_duplex_t   oduplex;
        link_flowctrl_t ofctrl;
        phy_handle_t    *ph;

        ph = mh->m_phy;

        olink = mh->m_link;
        ospeed = ph->phy_speed;
        oduplex = ph->phy_duplex;
        ofctrl = ph->phy_flowctrl;

        ASSERT(ph->phy_present);

        if (ph->phy_check(ph) == DDI_FAILURE) {
                _mii_error(mh, MII_ECHECK);
                mh->m_link = LINK_STATE_UNKNOWN;
                _mii_notify(mh);
                return (DDI_FAILURE);
        }

        mh->m_link = ph->phy_link;

        /* if anything changed, notify! */
        if ((mh->m_link != olink) ||
            (ph->phy_speed != ospeed) ||
            (ph->phy_duplex != oduplex) ||
            (ph->phy_flowctrl != ofctrl)) {
                _mii_notify(mh);
        }

        return (DDI_SUCCESS);
}

void
_mii_task(void *_mh)
{
        mii_handle_t    mh = _mh;
        phy_handle_t    *ph;
        clock_t         wait;
        clock_t         downtime;

        mutex_enter(&mh->m_lock);

        for (;;) {

                /* If detaching, exit the thread. */
                if (!mh->m_started) {
                        break;
                }

                ph = mh->m_phy;

                /*
                 * If we're suspended or otherwise not supposed to be
                 * monitoring the link, just go back to sleep.
                 *
                 * Theoretically we could power down the PHY, but we
                 * don't bother.  (The link might be used for
                 * wake-on-lan!)  Another option would be to reduce
                 * power on the PHY if both it and the link partner
                 * support 10 Mbps mode.
                 */
                if (mh->m_suspending) {
                        mh->m_suspended = B_TRUE;
                        cv_broadcast(&mh->m_cv);
                }
                if (mh->m_suspended) {
                        mh->m_suspending = B_FALSE;
                        cv_wait(&mh->m_cv, &mh->m_lock);
                        continue;
                }

                switch (mh->m_tstate) {
                case MII_STATE_PROBE:
                        _mii_probe(mh);
                        ph = mh->m_phy;
                        if (!ph->phy_present) {
                                /*
                                 * If no PHY is found, wait a bit before
                                 * trying the probe again.  10 seconds ought
                                 * to be enough.
                                 */
                                wait = 10 * MII_SECOND;
                        } else {
                                wait = 0;
                        }
                        break;

                case MII_STATE_RESET:
                        if (_mii_reset(mh) == DDI_SUCCESS) {
                                mh->m_tstate = MII_STATE_START;
                                wait = 0;
                        } else {
                                /*
                                 * If an error occurred, wait a bit and
                                 * try again later.
                                 */
                                wait = 10 * MII_SECOND;
                        }
                        break;

                case MII_STATE_START:
                        /*
                         * If an error occurs, we're going to go back to
                         * probe or reset state.  Otherwise we go to run
                         * state.  In all cases we want to wait 1 second
                         * before doing anything else - either for link to
                         * settle, or to give other code a chance to run
                         * while we reset.
                         */
                        if (_mii_start(mh) == DDI_SUCCESS) {
                                /* reset watchdog to latest */
                                downtime = ddi_get_lbolt();
                                mh->m_tstate = MII_STATE_RUN;
                        } else {
                                mh->m_tstate = MII_STATE_PROBE;
                        }
                        wait = 0;
                        break;

                case MII_STATE_LOOPBACK:
                        /*
                         * In loopback mode we don't check anything,
                         * and just wait for some condition to change.
                         */
                        wait = (clock_t)-1;
                        break;

                case MII_STATE_RUN:
                default:
                        if (_mii_check(mh) == DDI_FAILURE) {
                                /*
                                 * On error (PHY removed?), wait a
                                 * short bit before reprobing or
                                 * resetting.
                                 */
                                wait = MII_SECOND;
                                mh->m_tstate = MII_STATE_PROBE;

                        } else if (mh->m_link == LINK_STATE_UP) {
                                /* got goood link, so reset the watchdog */
                                downtime = ddi_get_lbolt();
                                /* rescan again in a second */
                                wait = MII_SECOND;

                        } else if ((ddi_get_lbolt() - downtime) >
                            (drv_usectohz(MII_SECOND * 10))) {

                                /*
                                 * If we were down for 10 seconds,
                                 * hard reset the PHY.
                                 */
                                mh->m_tstate = MII_STATE_RESET;
                                wait = 0;

                        } else {
                                /*
                                 * Otherwise, if we are still down,
                                 * rescan the link much more
                                 * frequently.  We might be trying to
                                 * autonegotiate.
                                 */
                                wait = MII_SECOND / 4;
                        }
                        break;
                }

                switch (wait) {
                case 0:
                        break;

                case (clock_t)-1:
                        cv_wait(&mh->m_cv, &mh->m_lock);
                        break;

                default:
                        (void) cv_reltimedwait(&mh->m_cv, &mh->m_lock,
                            drv_usectohz(wait), TR_CLOCK_TICK);
                }
        }

        mutex_exit(&mh->m_lock);
}