/*
 * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/*
 * Copyright (c) 2007 by  Lukas Turek <turek@ksvi.mff.cuni.cz>
 * Copyright (c) 2007 by  Jiri Svoboda <jirik.svoboda@seznam.cz>
 * Copyright (c) 2007 by  Martin Krulis <martin.krulis@matfyz.cz>
 * Copyright (c) 2006 by Damien Bergamini <damien.bergamini@free.fr>
 * Copyright (c) 2006 by Florian Stoehr <ich@florian-stoehr.de>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 */

/*
 * ZD1211 wLAN driver
 * USB communication
 *
 * Manage USB communication with the ZD-based device.
 */

#include <sys/byteorder.h>
#include <sys/stream.h>
#include <sys/strsun.h>
#include <sys/mac_provider.h>

#include "zyd.h"
#include "zyd_reg.h"

static int zyd_usb_disconnect(dev_info_t *dip);
static int zyd_usb_reconnect(dev_info_t *dip);
static zyd_res zyd_usb_data_in_start_request(struct zyd_usb *uc);

static zyd_usb_info_t usb_ids[] = {
	{0x53, 0x5301, ZYD_ZD1211B},
	{0x105, 0x145f, ZYD_ZD1211},
	{0x411, 0xda, ZYD_ZD1211B},
	{0x471, 0x1236, ZYD_ZD1211B},
	{0x471, 0x1237, ZYD_ZD1211B},
	{0x50d, 0x705c, ZYD_ZD1211B},
	{0x586, 0x3401, ZYD_ZD1211},
	{0x586, 0x3402, ZYD_ZD1211},
	{0x586, 0x3407, ZYD_ZD1211},
	{0x586, 0x3409, ZYD_ZD1211},
	{0x586, 0x3410, ZYD_ZD1211B},
	{0x586, 0x3412, ZYD_ZD1211B},
	{0x586, 0x3413, ZYD_ZD1211B},
	{0x586, 0x340a, ZYD_ZD1211B},
	{0x586, 0x340f, ZYD_ZD1211B},
	{0x79b, 0x4a, ZYD_ZD1211},
	{0x79b, 0x62, ZYD_ZD1211B},
	{0x7b8, 0x6001, ZYD_ZD1211},
	{0x83a, 0x4505, ZYD_ZD1211B},
	{0xace, 0x1211, ZYD_ZD1211},
	{0xace, 0x1215, ZYD_ZD1211B},
	{0xb05, 0x170c, ZYD_ZD1211},
	{0xb05, 0x171b, ZYD_ZD1211B},
	{0xb3b, 0x1630, ZYD_ZD1211},
	{0xb3b, 0x5630, ZYD_ZD1211},
	{0xbaf, 0x121, ZYD_ZD1211B},
	{0xcde, 0x1a, ZYD_ZD1211B},
	{0xdf6, 0x9071, ZYD_ZD1211},
	{0xdf6, 0x9075, ZYD_ZD1211},
	{0x126f, 0xa006, ZYD_ZD1211},
	{0x129b, 0x1666, ZYD_ZD1211},
	{0x129b, 0x1667, ZYD_ZD1211B},
	{0x13b1, 0x1e, ZYD_ZD1211},
	{0x13b1, 0x24, ZYD_ZD1211B},
	{0x1435, 0x711, ZYD_ZD1211},
	{0x14ea, 0xab13, ZYD_ZD1211},
	{0x157e, 0x300b, ZYD_ZD1211},
	{0x157e, 0x300d, ZYD_ZD1211B},
	{0x157e, 0x3204, ZYD_ZD1211},
	{0x1582, 0x6003, ZYD_ZD1211B},
	{0x1740, 0x2000, ZYD_ZD1211},
	{0x2019, 0x5303, ZYD_ZD1211B},
	{0x6891, 0xa727, ZYD_ZD1211}
};

/*
 * Get mac rev for usb vendor/product id.
 */
zyd_mac_rev_t
zyd_usb_mac_rev(uint16_t vendor, uint16_t product)
{
	int i;

	ZYD_DEBUG((ZYD_DBG_USB, "matching device usb%x,%x\n", vendor, product));
	for (i = 0; i < sizeof (usb_ids) / sizeof (zyd_usb_info_t); i++) {
		if (vendor == usb_ids[i].vendor_id &&
		    product == usb_ids[i].product_id)
			return (usb_ids[i].mac_rev);
	}

	ZYD_DEBUG((ZYD_DBG_USB, "assuming ZD1211B\n"));
	return (ZYD_ZD1211B);
}

/*
 * Vendor-specific write to the default control pipe.
 */
static zyd_res
zyd_usb_ctrl_send(struct zyd_usb *uc, uint8_t request, uint16_t value,
    uint8_t *data, uint16_t len)
{
	int err;
	int retry = 0;
	mblk_t *msg;
	usb_ctrl_setup_t setup;

	/* Always clean structures before use */
	bzero(&setup, sizeof (setup));
	setup.bmRequestType =
	    USB_DEV_REQ_TYPE_VENDOR | USB_DEV_REQ_HOST_TO_DEV;
	setup.bRequest = request;
	setup.wValue = value;
	setup.wIndex = 0;
	setup.wLength = len;
	setup.attrs = USB_ATTRS_NONE;

	if ((msg = allocb(len, BPRI_HI)) == NULL)
		return (ZYD_FAILURE);

	bcopy(data, msg->b_wptr, len);
	msg->b_wptr += len;

	while ((err = usb_pipe_ctrl_xfer_wait(uc->cdata->dev_default_ph,
	    &setup, &msg, NULL, NULL, 0)) != USB_SUCCESS) {
		if (retry++ > 3)
			break;
	}

	freemsg(msg);

	if (err != USB_SUCCESS) {
		ZYD_DEBUG((ZYD_DBG_USB,
		    "control pipe send failure (%d)\n", err));
		return (ZYD_FAILURE);
	}

	return (ZYD_SUCCESS);
}

/*
 * Vendor-specific read from the default control pipe.
 */
static zyd_res
zyd_usb_ctrl_recv(struct zyd_usb *uc, uint8_t request, uint16_t value,
    uint8_t *data, uint16_t len)
{
	int err;
	mblk_t *msg, *tmp_msg;
	usb_ctrl_setup_t setup;
	size_t msg_len;

	ASSERT(data != NULL);

	bzero(&setup, sizeof (setup));
	setup.bmRequestType =
	    USB_DEV_REQ_TYPE_VENDOR | USB_DEV_REQ_DEV_TO_HOST;
	setup.bRequest = request;
	setup.wValue = value;
	setup.wIndex = 0;
	setup.wLength = len;
	setup.attrs = USB_ATTRS_NONE;

	/* Pointer msg must be either set to NULL or point to a valid mblk! */
	msg = NULL;
	err = usb_pipe_ctrl_xfer_wait(uc->cdata->dev_default_ph,
	    &setup, &msg, NULL, NULL, 0);

	if (err != USB_SUCCESS) {
		ZYD_WARN("control pipe receive failure (%d)\n", err);
		return (ZYD_FAILURE);
	}

	msg_len = msgsize(msg);

	if (msg_len != len) {
		ZYD_WARN("control pipe failure: "
		    "received %d bytes, %d expected\n", (int)msg_len, len);
		return (ZYD_FAILURE);
	}

	if (msg->b_cont != NULL) {
		/* Fragmented message, concatenate */
		tmp_msg = msgpullup(msg, -1);
		freemsg(msg);
		msg = tmp_msg;
	}

	/*
	 * Now we can be sure the message is in a single block
	 * so we can copy it.
	 */
	bcopy(msg->b_rptr, data, len);
	freemsg(msg);

	return (ZYD_SUCCESS);
}

/*
 * Load firmware into the chip.
 */
zyd_res
zyd_usb_loadfirmware(struct zyd_usb *uc, uint8_t *fw, size_t size)
{
	uint16_t addr;
	uint8_t stat;

	ZYD_DEBUG((ZYD_DBG_FW, "firmware size: %lu\n", size));

	addr = ZYD_FIRMWARE_START_ADDR;
	while (size > 0) {
		const uint16_t mlen = (uint16_t)min(size, 4096);

		if (zyd_usb_ctrl_send(uc, ZYD_DOWNLOADREQ, addr, fw, mlen)
		    != USB_SUCCESS)
			return (ZYD_FAILURE);

		addr += mlen / 2;
		fw += mlen;
		size -= mlen;
	}

	/* check whether the upload succeeded */
	if (zyd_usb_ctrl_recv(uc, ZYD_DOWNLOADSTS, 0, &stat, sizeof (stat))
	    != ZYD_SUCCESS)
		return (ZYD_FAILURE);

	return ((stat & 0x80) ? ZYD_FAILURE : ZYD_SUCCESS);
}

/*
 * Return a specific alt_if from the device descriptor tree.
 */
static usb_alt_if_data_t *
usb_lookup_alt_if(usb_client_dev_data_t *cdd, uint_t config,
    uint_t interface, uint_t alt)
{
	usb_cfg_data_t *dcfg;
	usb_if_data_t *cfgif;
	usb_alt_if_data_t *ifalt;

	/*
	 * Assume everything is in the tree for now,
	 * (USB_PARSE_LVL_ALL)
	 * so we can directly index the array.
	 */

	/* Descend to configuration, configs are 1-based */
	if (config < 1 || config > cdd->dev_n_cfg)
		return (NULL);
	dcfg = &cdd->dev_cfg[config - 1];

	/* Descend to interface */
	if (interface > dcfg->cfg_n_if - 1)
		return (NULL);
	cfgif = &dcfg->cfg_if[interface];

	/* Descend to alt */
	if (alt > cfgif->if_n_alt - 1)
		return (NULL);
	ifalt = &cfgif->if_alt[alt];

	return (ifalt);
}

/*
 * Print all endpoints of an alt_if.
 */
static void
usb_list_all_endpoints(usb_alt_if_data_t *ifalt)
{
	usb_ep_data_t *ep_data;
	usb_ep_descr_t *ep_descr;
	int i;

	for (i = 0; i < ifalt->altif_n_ep; i++) {
		ep_data = &ifalt->altif_ep[i];
		ep_descr = &ep_data->ep_descr;
		cmn_err(CE_NOTE, "EP: %u\n", ep_descr->bEndpointAddress);
	}
}

/*
 * For the given alt_if, find an endpoint with the given
 * address and direction.
 *
 *      ep_direction    USB_EP_DIR_IN or USB_EP_DIR_OUT
 */
static usb_ep_data_t *
usb_find_endpoint(usb_alt_if_data_t *alt_if,
    uint_t ep_address, uint_t ep_direction)
{
	usb_ep_data_t *ep_data;
	usb_ep_descr_t *ep_descr;
	uint_t ep_addr, ep_dir;
	int i;

	for (i = 0; i < alt_if->altif_n_ep; i++) {
		ep_data = &alt_if->altif_ep[i];
		ep_descr = &ep_data->ep_descr;
		ep_addr = ep_descr->bEndpointAddress & USB_EP_NUM_MASK;
		ep_dir = ep_descr->bEndpointAddress & USB_EP_DIR_MASK;

		if (ep_addr == ep_address && ep_dir == ep_direction) {
			return (ep_data);
		}
	}

	ZYD_WARN("no endpoint with addr %u, dir %u\n", ep_address,
	    ep_direction);
	return (NULL);
}

enum zyd_usb_use_attr
{
	ZYD_USB_USE_ATTR = 1,
	ZYD_USB_NO_ATTR = 0
};

/*
 * Open a pipe to a given endpoint address/direction in the given
 * alt_if. Furthemore, if use_attr == ZYD_USB_USE_ATTR,
 * check whether the endpoint's transfer type is attr.
 */
static zyd_res
zyd_usb_open_pipe(struct zyd_usb *uc,
    usb_alt_if_data_t *alt_if,
    uint_t ep_address,
    uint_t ep_direction,
    uint_t attr,
    enum zyd_usb_use_attr use_attr,
    usb_pipe_handle_t *pipe, usb_ep_data_t *endpoint)
{
	usb_pipe_policy_t pipe_policy;

	*endpoint = *usb_find_endpoint(alt_if, ep_address, ep_direction);

	if ((use_attr == ZYD_USB_USE_ATTR) &&
	    (endpoint->ep_descr.bmAttributes & USB_EP_ATTR_MASK) != attr) {

		ZYD_WARN("endpoint %u/%s is not of type %s\n", ep_address,
		    (ep_direction == USB_EP_DIR_IN) ? "IN" : "OUT",
		    (attr == USB_EP_ATTR_BULK) ? "bulk" : "intr");
		return (ZYD_FAILURE);
	}

	bzero(&pipe_policy, sizeof (usb_pipe_policy_t));
	pipe_policy.pp_max_async_reqs = ZYD_USB_REQ_COUNT;

	if (usb_pipe_open(uc->dip, &endpoint->ep_descr,
	    &pipe_policy, USB_FLAGS_SLEEP, pipe) != USB_SUCCESS) {
		ZYD_WARN("failed to open pipe %u\n", ep_address);
		return (ZYD_FAILURE);
	}

	return (ZYD_SUCCESS);
}

/*
 * Open communication pipes.
 *
 * The following pipes are used by the ZD1211:
 *
 *      1/OUT BULK
 *      2/IN  BULK
 *      3/IN  INTR
 *      4/OUT BULK or INTR
 */
zyd_res
zyd_usb_open_pipes(struct zyd_usb *uc)
{
	usb_alt_if_data_t *alt_if;

	ZYD_DEBUG((ZYD_DBG_USB, "opening pipes\n"));

	alt_if = usb_lookup_alt_if(uc->cdata, ZYD_USB_CONFIG_NUMBER,
	    ZYD_USB_IFACE_INDEX, ZYD_USB_ALT_IF_INDEX);

	if (alt_if == NULL) {
		ZYD_WARN("alt_if not found\n");
		return (ZYD_FAILURE);
	}

#ifdef DEBUG
	if (zyd_dbg_flags & ZYD_DBG_USB)
		usb_list_all_endpoints(alt_if);
#endif

	if (zyd_usb_open_pipe(uc, alt_if, 1, USB_EP_DIR_OUT, USB_EP_ATTR_BULK,
	    ZYD_USB_USE_ATTR, &uc->pipe_data_out, &uc->ep_data_out) !=
	    ZYD_SUCCESS) {
		ZYD_WARN("failed to open data OUT pipe\n");
		goto fail;
	}

	if (zyd_usb_open_pipe(uc, alt_if, 2, USB_EP_DIR_IN, USB_EP_ATTR_BULK,
	    ZYD_USB_USE_ATTR, &uc->pipe_data_in, &uc->ep_data_in) !=
	    ZYD_SUCCESS) {
		ZYD_WARN("failed to open data IN pipe\n");
		goto fail;
	}

	if (zyd_usb_open_pipe(uc, alt_if, 3, USB_EP_DIR_IN, USB_EP_ATTR_INTR,
	    ZYD_USB_USE_ATTR, &uc->pipe_cmd_in, &uc->ep_cmd_in) !=
	    ZYD_SUCCESS) {
		ZYD_WARN("failed to open command IN pipe\n");
		goto fail;
	}

	/*
	 * Pipe 4/OUT is either a bulk or interrupt pipe.
	 */
	if (zyd_usb_open_pipe(uc, alt_if, 4, USB_EP_DIR_OUT, 0,
	    ZYD_USB_NO_ATTR, &uc->pipe_cmd_out, &uc->ep_cmd_out) !=
	    ZYD_SUCCESS) {
		ZYD_WARN("failed to open command OUT pipe\n");
		goto fail;
	}

	return (ZYD_SUCCESS);

fail:
	zyd_usb_close_pipes(uc);
	return (ZYD_FAILURE);
}

/*
 * Close communication pipes.
 */
void
zyd_usb_close_pipes(struct zyd_usb *uc)
{
	ZYD_DEBUG((ZYD_DBG_USB, "closing pipes\n"));

	if (uc->pipe_data_out != NULL) {
		usb_pipe_close(uc->dip, uc->pipe_data_out, USB_FLAGS_SLEEP,
		    NULL, NULL);
		uc->pipe_data_out = NULL;
	}

	if (uc->pipe_data_in != NULL) {
		usb_pipe_close(uc->dip, uc->pipe_data_in, USB_FLAGS_SLEEP,
		    NULL, NULL);
		uc->pipe_data_in = NULL;
	}

	if (uc->pipe_cmd_in != NULL) {
		usb_pipe_close(uc->dip, uc->pipe_cmd_in, USB_FLAGS_SLEEP,
		    NULL, NULL);
		uc->pipe_cmd_in = NULL;
	}

	if (uc->pipe_cmd_out != NULL) {
		usb_pipe_close(uc->dip, uc->pipe_cmd_out, USB_FLAGS_SLEEP,
		    NULL, NULL);
		uc->pipe_cmd_out = NULL;
	}
}

/*
 * Send a sequence of bytes to a bulk pipe.
 *
 *      uc      pointer to usb module state
 *      data    pointer to a buffer of bytes
 *      len     size of the buffer (bytes)
 */
/*ARGSUSED*/
static void
zyd_data_out_cb(usb_pipe_handle_t pipe, usb_bulk_req_t *req)
{
	struct zyd_softc *sc = (struct zyd_softc *)req->bulk_client_private;
	struct ieee80211com *ic = &sc->ic;
	boolean_t resched;

	if (req->bulk_completion_reason != USB_CR_OK)
		ZYD_DEBUG((ZYD_DBG_USB, "data OUT exception\n"));

	(void) zyd_serial_enter(sc, ZYD_NO_SIG);
	if (sc->tx_queued > 0)
		sc->tx_queued--;
	else
		ZYD_DEBUG((ZYD_DBG_TX, "tx queue underrun\n"));

	if (sc->resched && (sc->tx_queued < ZYD_TX_LIST_COUNT)) {
		resched = sc->resched;
		sc->resched = B_FALSE;
	}
	zyd_serial_exit(sc);

	if (resched)
		mac_tx_update(ic->ic_mach);

	usb_free_bulk_req(req);
}

/*
 * Called when the transfer from zyd_usb_bulk_pipe_send() terminates
 * or an exception occurs on the pipe.
 */
/*ARGSUSED*/
static void
zyd_bulk_pipe_cb(usb_pipe_handle_t pipe, struct usb_bulk_req *req)
{
	struct zyd_cb_lock *lock;
	lock = (struct zyd_cb_lock *)req->bulk_client_private;

	/* Just signal that something happened */
	zyd_cb_lock_signal(lock);
}

static zyd_res
zyd_usb_bulk_pipe_send(struct zyd_usb *uc,
    usb_pipe_handle_t pipe, const void *data, size_t len)
{
	usb_bulk_req_t *send_req;
	mblk_t *mblk;
	int res;
	struct zyd_cb_lock lock;

	send_req = usb_alloc_bulk_req(uc->dip, len, USB_FLAGS_SLEEP);
	if (send_req == NULL) {
		ZYD_WARN("failed to allocate bulk request\n");
		return (ZYD_FAILURE);
	}
	send_req->bulk_len = (uint_t)len;
	send_req->bulk_client_private = (usb_opaque_t)&lock;
	send_req->bulk_attributes = USB_ATTRS_AUTOCLEARING;
	send_req->bulk_timeout = 5;
	send_req->bulk_cb = zyd_bulk_pipe_cb;
	send_req->bulk_exc_cb = zyd_bulk_pipe_cb;

	mblk = send_req->bulk_data;
	bcopy(data, mblk->b_wptr, len);
	mblk->b_wptr += len;

	zyd_cb_lock_init(&lock);

	res = usb_pipe_bulk_xfer(pipe, send_req, USB_FLAGS_NOSLEEP);
	if (res != USB_SUCCESS) {
		ZYD_DEBUG((ZYD_DBG_USB,
		    "failed writing to bulk OUT pipe (%d)\n", res));
		usb_free_bulk_req(send_req);
		zyd_cb_lock_destroy(&lock);
		return (ZYD_FAILURE);
	}

	if (zyd_cb_lock_wait(&lock, 1000000) != ZYD_SUCCESS) {
		ZYD_WARN("timeout - pipe reset\n");
		usb_pipe_reset(uc->dip, pipe, USB_FLAGS_SLEEP, NULL, 0);
		(void) zyd_cb_lock_wait(&lock, -1);
		res = ZYD_FAILURE;
	} else {
		res = (send_req->bulk_completion_reason == USB_CR_OK) ?
		    ZYD_SUCCESS : ZYD_FAILURE;
	}

	usb_free_bulk_req(send_req);
	zyd_cb_lock_destroy(&lock);
	return (res);
}

/*
 * Called when the transfer from zyd_usb_intr_pipe_send() terminates
 * or an exception occurs on the pipe.
 */
/*ARGSUSED*/
static void
zyd_intr_pipe_cb(usb_pipe_handle_t pipe, struct usb_intr_req *req)
{
	struct zyd_cb_lock *lock;
	lock = (struct zyd_cb_lock *)req->intr_client_private;

	/* Just signal that something happened */
	zyd_cb_lock_signal(lock);
}

/*
 * Send a sequence of bytes to an interrupt pipe.
 *
 *      uc      pointer to usb module state
 *      data    pointer to a buffer of bytes
 *      len     size of the buffer (bytes)
 */
static zyd_res
zyd_usb_intr_pipe_send(struct zyd_usb *uc,
    usb_pipe_handle_t pipe, const void *data, size_t len)
{
	usb_intr_req_t *send_req;
	mblk_t *mblk;
	int res;
	struct zyd_cb_lock lock;

	send_req = usb_alloc_intr_req(uc->dip, len, USB_FLAGS_SLEEP);
	if (send_req == NULL) {
		ZYD_WARN("failed to allocate interupt request\n");
		return (ZYD_FAILURE);
	}
	send_req->intr_len = (uint_t)len;
	send_req->intr_client_private = (usb_opaque_t)&lock;
	send_req->intr_attributes = USB_ATTRS_AUTOCLEARING;
	send_req->intr_timeout = 5;
	send_req->intr_cb = zyd_intr_pipe_cb;
	send_req->intr_exc_cb = zyd_intr_pipe_cb;

	mblk = send_req->intr_data;
	bcopy(data, mblk->b_wptr, len);
	mblk->b_wptr += len;

	zyd_cb_lock_init(&lock);

	res = usb_pipe_intr_xfer(pipe, send_req, 0);
	if (res != USB_SUCCESS) {
		ZYD_DEBUG((ZYD_DBG_USB,
		    "failed writing to intr/out pipe (%d)\n", res));
		usb_free_intr_req(send_req);
		zyd_cb_lock_destroy(&lock);
		return (ZYD_FAILURE);
	}

	if (zyd_cb_lock_wait(&lock, 1000000) != ZYD_SUCCESS) {
		ZYD_WARN("timeout - pipe reset\n");
		usb_pipe_reset(uc->dip, pipe, USB_FLAGS_SLEEP, NULL, 0);
		(void) zyd_cb_lock_wait(&lock, -1);
		res = ZYD_FAILURE;
	} else {
		res = (send_req->intr_completion_reason == USB_CR_OK) ?
		    ZYD_SUCCESS : ZYD_FAILURE;
	}

	usb_free_intr_req(send_req);
	zyd_cb_lock_destroy(&lock);
	return (res);
}

/*
 * Send a sequence of bytes to the cmd_out pipe. (in a single USB transfer)
 *
 *      uc      pointer to usb module state
 *      data    pointer to a buffer of bytes
 *      len     size of the buffer (bytes)
 */
static zyd_res
zyd_usb_cmd_pipe_send(struct zyd_usb *uc, const void *data, size_t len)
{
	zyd_res res;
	uint8_t type;

	/* Determine the type of cmd_out */
	type = uc->ep_cmd_out.ep_descr.bmAttributes & USB_EP_ATTR_MASK;
	if (type == USB_EP_ATTR_BULK)
		res = zyd_usb_bulk_pipe_send(uc, uc->pipe_cmd_out, data, len);
	else
		res = zyd_usb_intr_pipe_send(uc, uc->pipe_cmd_out, data, len);

	return (res);
}


/*
 * Format and send a command to the cmd_out pipe.
 *
 *      uc      pointer to usb module state
 *      code    ZD command code (16-bit)
 *      data    raw buffer containing command data
 *      len     size of the data buffer (bytes)
 */
zyd_res
zyd_usb_cmd_send(struct zyd_usb *uc,
    uint16_t code, const void *data, size_t len)
{
	zyd_res res;
	struct zyd_cmd cmd;

	cmd.cmd_code = LE_16(code);
	bcopy(data, cmd.data, len);

	res = zyd_usb_cmd_pipe_send(uc, &cmd, sizeof (uint16_t) + len);
	if (res != ZYD_SUCCESS) {
		ZYD_DEBUG((ZYD_DBG_USB, "failed writing command (%d)\n", res));
		return (ZYD_FAILURE);
	}

	return (ZYD_SUCCESS);
}

/*
 * Issue an ioread request.
 *
 * Issues a ZD ioread command (with a vector of addresses passed in raw
 * form as in_data) and blocks until the response is received
 * and filled into the response buffer.
 *
 *      uc              pointer to usb module state
 *      in_data         pointer to request data
 *      in_len          request data size (bytes)
 *      out_data        pointer to response buffer
 *      out_len         response buffer size (bytes)
 */
zyd_res
zyd_usb_ioread_req(struct zyd_usb *uc,
    const void *in_data, size_t in_len, void *out_data, size_t out_len)
{
	zyd_res res;
	int cnt;

	/* Initialise io_read structure */
	uc->io_read.done = B_FALSE;
	uc->io_read.buffer = out_data;
	uc->io_read.buf_len = (int)out_len;

	uc->io_read.pending = B_TRUE;

	res = zyd_usb_cmd_send(uc, ZYD_CMD_IORD, in_data, in_len);
	if (res != ZYD_SUCCESS) {
		ZYD_DEBUG((ZYD_DBG_USB, "IO read request: pipe failure(%d)\n"));
		return (ZYD_FAILURE);
	}

	cnt = 0;
	while (uc->io_read.done != B_TRUE && cnt < 500) {
		delay(drv_usectohz(10 * 1000));
		++cnt;
	}

	if (uc->io_read.done != B_TRUE) {
		ZYD_WARN("I/O read request: timeout\n");
		return (ZYD_FAILURE);
	}

	if (uc->io_read.exc != B_FALSE) {
		ZYD_DEBUG((ZYD_DBG_USB, "I/O read request: exception\n"));
		return (ZYD_FAILURE);
	}

	return (ZYD_SUCCESS);
}


/*
 * Called when data arrives from the cmd_in pipe.
 */
/*ARGSUSED*/
static void
zyd_cmd_in_cb(usb_pipe_handle_t pipe, usb_intr_req_t *req)
{
	struct zyd_usb *uc;
	struct zyd_ioread *rdp;
	mblk_t *mblk, *tmp_blk;
	unsigned char *data;
	size_t len;
	uint16_t code;

	uc = (struct zyd_usb *)req->intr_client_private;
	ASSERT(uc != NULL);
	rdp = &uc->io_read;
	mblk = req->intr_data;

	if (mblk->b_cont != NULL) {
		/* Fragmented message, concatenate */
		tmp_blk = msgpullup(mblk, -1);
		data = tmp_blk->b_rptr;
		len = MBLKL(tmp_blk);
	} else {
		/* Non-fragmented message, use directly */
		tmp_blk = NULL;
		data = mblk->b_rptr;
		len = MBLKL(mblk);
	}

	code = LE_16(*(uint16_t *)(uintptr_t)data);
	if (code != ZYD_RESPONSE_IOREAD) {
		/* Other response types not handled yet */
		usb_free_intr_req(req);
		return;
	}

	if (rdp->pending != B_TRUE) {
		ZYD_WARN("no ioread pending\n");
		usb_free_intr_req(req);
		return;
	}
	rdp->pending = B_FALSE;

	/* Now move on to the data part */
	data += sizeof (uint16_t);
	len -= sizeof (uint16_t);
	if (rdp->buf_len > len) {
		ZYD_WARN("too few bytes received\n");
	}

	bcopy(data, rdp->buffer, rdp->buf_len);

	if (tmp_blk != NULL)
		freemsg(tmp_blk);

	rdp->exc = B_FALSE;
	rdp->done = B_TRUE;
	usb_free_intr_req(req);
}

/*
 * Called when an exception occurs on the cmd_in pipe.
 */
/*ARGSUSED*/
static void
zyd_cmd_in_exc_cb(usb_pipe_handle_t pipe, usb_intr_req_t *req)
{
	struct zyd_usb *uc;
	struct zyd_ioread *rdp;

	ZYD_DEBUG((ZYD_DBG_USB, "command IN exception\n"));

	uc = (struct zyd_usb *)req->intr_client_private;
	ASSERT(uc != NULL);
	rdp = &uc->io_read;

	if (rdp->pending == B_TRUE) {
		rdp->exc = B_TRUE;
		rdp->done = B_TRUE;
	}
	usb_free_intr_req(req);
}

/*
 * Start interrupt polling on the cmd_in pipe.
 */
zyd_res
zyd_usb_cmd_in_start_polling(struct zyd_usb *uc)
{
	usb_intr_req_t *intr_req;
	int res;

	intr_req = usb_alloc_intr_req(uc->dip, 0, USB_FLAGS_SLEEP);
	if (intr_req == NULL) {
		ZYD_WARN("failed to allocate interrupt request\n");
		return (ZYD_FAILURE);
	}

	intr_req->intr_attributes = USB_ATTRS_SHORT_XFER_OK;
	intr_req->intr_len = uc->ep_cmd_in.ep_descr.wMaxPacketSize;
	intr_req->intr_cb = zyd_cmd_in_cb;
	intr_req->intr_exc_cb = zyd_cmd_in_exc_cb;
	intr_req->intr_client_private = (usb_opaque_t)uc;

	res = usb_pipe_intr_xfer(uc->pipe_cmd_in, intr_req, USB_FLAGS_NOSLEEP);
	if (res != USB_SUCCESS) {
		ZYD_WARN("failed starting command IN polling: pipe failure\n");
		usb_free_intr_req(intr_req);
		return (ZYD_FAILURE);
	}

	return (ZYD_SUCCESS);
}

/*
 * Stop interrupt polling on the cmd_in pipe.
 */
void
zyd_usb_cmd_in_stop_polling(struct zyd_usb *uc)
{
	ZYD_DEBUG((ZYD_DBG_USB, "stopping command IN polling\n"));

	usb_pipe_stop_intr_polling(uc->pipe_cmd_in, USB_FLAGS_SLEEP);
}

/*
 * Called when data arrives on the data_in pipe.
 */
/*ARGSUSED*/
static void
zyd_data_in_cb(usb_pipe_handle_t pipe, usb_bulk_req_t *req)
{
	struct zyd_softc *sc;
	struct zyd_usb *uc;
	mblk_t *mblk, *tmp_blk;
	struct zyd_rx_desc *desc;
	unsigned char *data;
	size_t len;

	uc = (struct zyd_usb *)req->bulk_client_private;
	ASSERT(uc != NULL);
	sc = ZYD_USB_TO_SOFTC(uc);
	ASSERT(sc != NULL);
	mblk = req->bulk_data;

	/* Fragmented STREAMS message? */
	if (mblk->b_cont != NULL) {
		/* Fragmented, concatenate it into a single block */
		tmp_blk = msgpullup(mblk, -1);
		if (tmp_blk == NULL) {
			ZYD_WARN("failed to concatenate fragments\n");
			goto error;
		}
		data = tmp_blk->b_rptr;
		len = MBLKL(tmp_blk);
	} else {
		/* Not fragmented, use directly */
		tmp_blk = NULL;
		data = mblk->b_rptr;
		len = MBLKL(mblk);
	}

	if (len < 2) {
		ZYD_WARN("received usb transfer too short\n");
		goto error;
	}

	/*
	 * If this is a composite packet, the last two bytes contain
	 * two special signature bytes.
	 */
	desc = (struct zyd_rx_desc *)(data + len) - 1;
	/* multi-frame transfer */
	if (LE_16(desc->tag) == ZYD_TAG_MULTIFRAME) {
		const uint8_t *p = data, *end = data + len;
		int i;

		ZYD_DEBUG((ZYD_DBG_RX, "composite packet\n"));

		for (i = 0; i < ZYD_MAX_RXFRAMECNT; i++) {
			const uint16_t len16 = LE_16(desc->len[i]);
			if (len16 == 0 || p + len16 > end)
				break;
			zyd_receive(ZYD_USB_TO_SOFTC(uc), p, len16);
			/* next frame is aligned on a 32-bit boundary */
			p += (len16 + 3) & ~3;
		}
	} else {
		/* single-frame transfer */
		zyd_receive(ZYD_USB_TO_SOFTC(uc), data, MBLKL(mblk));
	}

error:
	if (tmp_blk != NULL)
		freemsg(tmp_blk);

	usb_free_bulk_req(req);

	if (!sc->running)
		return;

	if (zyd_usb_data_in_start_request(uc) != ZYD_SUCCESS) {
		ZYD_WARN("error restarting data_in transfer\n");
	}
}

/*
 * Called when an exception occurs on the data_in pipe.
 */
/*ARGSUSED*/
static void
zyd_data_in_exc_cb(usb_pipe_handle_t pipe, usb_bulk_req_t *req)
{
	struct zyd_usb *uc;

	ZYD_DEBUG((ZYD_DBG_USB, "data IN exception\n"));

	uc = (struct zyd_usb *)req->bulk_client_private;
	ASSERT(uc != NULL);

	usb_free_bulk_req(req);
}

/*
 * Start a receive request on the data_in pipe.
 */
static zyd_res
zyd_usb_data_in_start_request(struct zyd_usb *uc)
{
	usb_bulk_req_t *req;
	int res;

	req = usb_alloc_bulk_req(uc->dip, ZYD_RX_BUF_SIZE, USB_FLAGS_SLEEP);
	if (req == NULL) {
		ZYD_WARN("failed to allocate bulk IN request\n");
		return (ZYD_FAILURE);
	}

	req->bulk_len = (uint_t)ZYD_RX_BUF_SIZE;
	req->bulk_timeout = 0;
	req->bulk_client_private = (usb_opaque_t)uc;
	req->bulk_attributes =
	    USB_ATTRS_SHORT_XFER_OK | USB_ATTRS_AUTOCLEARING;
	req->bulk_cb = zyd_data_in_cb;
	req->bulk_exc_cb = zyd_data_in_exc_cb;

	res = usb_pipe_bulk_xfer(uc->pipe_data_in, req, USB_FLAGS_NOSLEEP);
	if (res != USB_SUCCESS) {
		ZYD_WARN("error starting receive request on data_in pipe\n");
		usb_free_bulk_req(req);
		return (ZYD_FAILURE);
	}

	return (ZYD_SUCCESS);
}


/*
 * Start receiving packets on the data_in pipe.
 */
zyd_res
zyd_usb_data_in_enable(struct zyd_usb *uc)
{
	for (int i = 0; i < ZYD_RX_LIST_COUNT; i++) {
		if (zyd_usb_data_in_start_request(uc) != ZYD_SUCCESS) {
			ZYD_WARN("failed to start data IN requests\n");
			return (ZYD_FAILURE);
		}
	}
	return (ZYD_SUCCESS);
}

/*
 * Stop receiving packets on the data_in pipe.
 */
void
zyd_usb_data_in_disable(struct zyd_usb *uc)
{
	usb_pipe_reset(uc->dip, uc->pipe_data_in, USB_FLAGS_SLEEP,
	    NULL, NULL);
}

/*
 * Send a packet to data_out.
 *
 * A packet consists of a zyd_tx_header + the IEEE802.11 frame.
 */
zyd_res
zyd_usb_send_packet(struct zyd_usb *uc, mblk_t *mp)
{
	usb_bulk_req_t *send_req;
	int res;

	send_req = usb_alloc_bulk_req(uc->dip, 0, USB_FLAGS_SLEEP);
	if (send_req == NULL) {
		ZYD_WARN("failed to allocate bulk request\n");
		return (ZYD_FAILURE);
	}

	send_req->bulk_len = msgdsize(mp);
	send_req->bulk_data = mp;
	send_req->bulk_timeout = 5;
	send_req->bulk_attributes = USB_ATTRS_AUTOCLEARING;
	send_req->bulk_client_private = (usb_opaque_t)ZYD_USB_TO_SOFTC(uc);
	send_req->bulk_cb = zyd_data_out_cb;
	send_req->bulk_exc_cb = zyd_data_out_cb;
	send_req->bulk_completion_reason = 0;
	send_req->bulk_cb_flags = 0;

	res = usb_pipe_bulk_xfer(uc->pipe_data_out, send_req, 0);
	if (res != USB_SUCCESS) {
		ZYD_DEBUG((ZYD_DBG_USB,
		    "failed writing to bulk/out pipe (%d)\n", res));
		usb_free_bulk_req(send_req);
		return (USB_FAILURE);
	}

	return (USB_SUCCESS);
}

/*
 * Initialize USB device communication and USB module state.
 *
 *      uc      pointer to usb module state
 *      dip     pointer to device info structure
 */
zyd_res
zyd_usb_init(struct zyd_softc *sc)
{
	struct zyd_usb *uc = &sc->usb;
	dev_info_t *dip = sc->dip;
	int ures;

	uc->dip = dip;

	ures = usb_client_attach(uc->dip, USBDRV_VERSION, 0);
	if (ures != USB_SUCCESS) {
		ZYD_WARN("usb_client_attach failed, error code: %d\n", ures);
		return (ZYD_FAILURE);
	}

	/*
	 * LVL_ALL is needed for later endpoint scanning,
	 * and the tree must not be freed before that.
	 */
	ures = usb_get_dev_data(uc->dip, &uc->cdata, USB_PARSE_LVL_ALL, 0);
	if (ures != USB_SUCCESS) {
		ZYD_WARN("usb_get_dev_data failed, error code: %d\n", ures);
		ASSERT(uc->cdata == NULL);
		goto fail;
	}

	ures = usb_reset_device(uc->dip, USB_RESET_LVL_DEFAULT);
	if (ures != USB_SUCCESS) {
		ZYD_WARN("usb_reset_device failed, error code: %d\n", ures);
		goto fail;
	}

	uc->connected = B_TRUE;

	ures = usb_register_hotplug_cbs(dip, zyd_usb_disconnect,
	    zyd_usb_reconnect);
	if (ures != USB_SUCCESS) {
		ZYD_WARN("usb_register_hotplug_cbs failed, error code: %d\n",
		    ures);
		goto fail;
	}

	return (ZYD_SUCCESS);
fail:
	usb_client_detach(uc->dip, uc->cdata);
	uc->cdata = NULL;
	return (ZYD_FAILURE);
}

/*
 * Deinitialize USB device communication.
 */
void
zyd_usb_deinit(struct zyd_softc *sc)
{
	struct zyd_usb *uc = &sc->usb;

	usb_unregister_hotplug_cbs(sc->dip);

	usb_client_detach(uc->dip, uc->cdata);
	uc->cdata = NULL;
	uc->connected = B_FALSE;
}

/*
 * Device connected
 */
static int
zyd_usb_reconnect(dev_info_t *dip)
{
	struct zyd_softc *sc;
	struct zyd_usb *uc;

	sc = ddi_get_soft_state(zyd_ssp, ddi_get_instance(dip));
	ASSERT(sc != NULL);
	uc = &sc->usb;
	ASSERT(!uc->connected);

	if (sc->suspended)
		ZYD_DEBUG((ZYD_DBG_RESUME | ZYD_DBG_USB,
		    "reconnect before resume\n"));

	/* check device changes after disconnect */
	if (usb_check_same_device(sc->dip, NULL, USB_LOG_L2, -1,
	    USB_CHK_BASIC | USB_CHK_CFG, NULL) != USB_SUCCESS) {
		ZYD_DEBUG((ZYD_DBG_USB, "different device connected\n"));
		return (DDI_FAILURE);
	}

	(void) zyd_serial_enter(sc, ZYD_NO_SIG);
	if (zyd_hw_init(sc) != ZYD_SUCCESS) {
		ZYD_WARN("failed to reinit hardware\n");
		zyd_serial_exit(sc);
		return (DDI_FAILURE);
	}
	if (sc->running) {
		if (zyd_hw_start(sc) != ZYD_SUCCESS) {
			ZYD_WARN("failed to restart hardware\n");
			zyd_serial_exit(sc);
			goto fail;
		}
	}
	zyd_serial_exit(sc);

	uc->connected = B_TRUE;

	return (DDI_SUCCESS);
fail:
	usb_client_detach(uc->dip, uc->cdata);
	uc->cdata = NULL;
	return (DDI_FAILURE);
}

static int
zyd_usb_disconnect(dev_info_t *dip)
{
	struct zyd_softc *sc;
	struct zyd_usb *uc;

	sc = ddi_get_soft_state(zyd_ssp, ddi_get_instance(dip));
	ASSERT(sc != NULL);
	uc = &sc->usb;

	if (!uc->connected) {
		ZYD_DEBUG((ZYD_DBG_USB, "different device disconnected\n"));
		return (DDI_FAILURE);
	}
	uc->connected = B_FALSE;

	if (sc->suspended) {
		ZYD_DEBUG((ZYD_DBG_USB, "disconnect after suspend\n"));
		return (DDI_SUCCESS);
	}
	ieee80211_new_state(&sc->ic, IEEE80211_S_INIT, -1);

	(void) zyd_serial_enter(sc, ZYD_NO_SIG);
	zyd_hw_stop(sc);
	zyd_hw_deinit(sc);
	zyd_serial_exit(sc);

	return (DDI_SUCCESS);
}

int
zyd_suspend(struct zyd_softc *sc)
{
	struct zyd_usb *uc = &sc->usb;

	if (!uc->connected) {
		ZYD_DEBUG((ZYD_DBG_USB | ZYD_DBG_RESUME,
		    "suspend after disconnect\n"));
		sc->suspended = B_TRUE;
		return (DDI_SUCCESS);
	}
	ZYD_DEBUG((ZYD_DBG_RESUME, "suspend\n"));

	sc->suspended = B_TRUE;
	ieee80211_new_state(&sc->ic, IEEE80211_S_INIT, -1);

	(void) zyd_serial_enter(sc, ZYD_NO_SIG);
	zyd_hw_stop(sc);
	zyd_hw_deinit(sc);
	zyd_serial_exit(sc);

	ZYD_DEBUG((ZYD_DBG_RESUME, "suspend complete\n"));
	return (DDI_SUCCESS);
}

int
zyd_resume(struct zyd_softc *sc)
{
	struct zyd_usb *uc = &sc->usb;

	if (!uc->connected) {
		ZYD_DEBUG((ZYD_DBG_USB | ZYD_DBG_RESUME,
		    "resume after disconnect\n"));
		sc->suspended = B_FALSE;
		return (DDI_SUCCESS);
	}
	ZYD_DEBUG((ZYD_DBG_RESUME, "resume\n"));

	/* check device changes after disconnect */
	if (usb_check_same_device(sc->dip, NULL, USB_LOG_L2, -1,
	    USB_CHK_BASIC | USB_CHK_CFG, NULL) != USB_SUCCESS) {
		ZYD_WARN("different device connected to same port\n");
		sc->suspended = B_FALSE;
		uc->connected = B_FALSE;
		return (DDI_SUCCESS);
	}

	(void) zyd_serial_enter(sc, ZYD_NO_SIG);
	if (zyd_hw_init(sc) != ZYD_SUCCESS) {
		ZYD_WARN("failed to reinit hardware\n");
		zyd_serial_exit(sc);
		return (DDI_FAILURE);
	}
	if (sc->running) {
		if (zyd_hw_start(sc) != ZYD_SUCCESS) {
			ZYD_WARN("failed to restart hardware\n");
			zyd_serial_exit(sc);
			return (DDI_FAILURE);
		}
	}
	zyd_serial_exit(sc);

	sc->suspended = B_FALSE;

	ZYD_DEBUG((ZYD_DBG_RESUME, "resume complete\n"));
	return (DDI_SUCCESS);
}