/*
 * 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.
 */

/*
 * 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 *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 const char *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 flags, uint_t sz, void *val, uint_t *permp)
{
	phy_handle_t	*ph;
	int		err = 0;
	uint_t		perm;
	boolean_t	dfl = flags & MAC_PROP_DEFAULT;

	_NOTE(ARGUNUSED(name));

	if (sz < 1)
		return (EINVAL);

	mutex_enter(&mh->m_lock);

	ph = mh->m_phy;
	perm = MAC_PROP_PERM_RW;

#define	CASE_PROP_ABILITY(PROP, VAR)					\
	case MAC_PROP_ADV_##PROP:					\
		perm = MAC_PROP_PERM_READ;				\
		*(uint8_t *)val =					\
		    dfl ? ph->phy_cap_##VAR : ph->phy_adv_##VAR;	\
		break;							\
									\
	case MAC_PROP_EN_##PROP:					\
		if (!ph->phy_cap_##VAR)					\
			perm = MAC_PROP_PERM_READ;			\
		*(uint8_t *)val =					\
		    dfl ? ph->phy_cap_##VAR : ph->phy_en_##VAR;		\
		break;

	switch (num) {
	case MAC_PROP_DUPLEX:
		perm = MAC_PROP_PERM_READ;
		if (sz >= sizeof (link_duplex_t)) {
			bcopy(&ph->phy_duplex, val, sizeof (link_duplex_t));
		} else {
			err = EINVAL;
		}
		break;

	case MAC_PROP_SPEED:
		perm = MAC_PROP_PERM_READ;
		if (sz >= sizeof (uint64_t)) {
			uint64_t speed = ph->phy_speed * 1000000ull;
			bcopy(&speed, val, sizeof (speed));
		} else {
			err = EINVAL;
		}
		break;

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

	case MAC_PROP_FLOWCTRL:
		if (sz >= sizeof (link_flowctrl_t)) {
			bcopy(&ph->phy_flowctrl, val,
			    sizeof (link_flowctrl_t));
		} else {
			err = EINVAL;
		}
		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;
	}

	if (err == 0) {
		*permp = perm;
	}

	mutex_exit(&mh->m_lock);

	return (err);
}

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:
		if (sz < sizeof (link_flowctrl_t)) {
			rv = EINVAL;
		} else {
			link_flowctrl_t	fc;
			boolean_t chg;

			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);
}